Issues with Implementing Vue2 through Single-SPA in Angular

228 Views Asked by At

For some reasons, I have to integrate a Vue2 project into Angular, and I'm uncertain if this is a correct expectation, so I'm leaning towards the concept of micro-frontends. Up to now, I've tried Webpack's federated modules and web components, and currently, I'm exploring the Single SPA approach.

None of these are easy to implement, each with its problems, and now, I've encountered some situations with the Single SPA approach as well.

First, the issue: once Angular starts up and enters the screen, it immediately throws an error:

"Uncaught TypeError: application 'vue_app_2' died in status LOADING_SOURCE_CODE: Cannot convert undefined or null to object."

Next, when the dialog opens (at this point it attempts to load the Vue2 project through Single SPA), an error occurs with "System.import," and the returned module is empty. This can be traced in my code. The error message is:

"ERROR Error: Uncaught (in promise): Error: single-spa minified message #2: See https://single-spa.js.org/error/?code=2." However, after carefully reading through https://single-spa.js.org/error/?code=2

I'm at a loss for what else I can do. It's quite frustrating.

Is it even a mistake to expect to use Single SPA to embed content from, say, Vue (or React) within the Angular framework?

As a test, I've created a simple function in Angular that opens a mat dialog when a button is clicked. The opened dialog component attempts to load the Vue2 project through Single SPA. That's all there is to the test, and even that has failed. I'm including both the Vue2 and Angular projects here in hopes that someone can identify what I've done wrong.

Vue 2

Here are App.vue, main.js, router.js, vue.config.js, package.json

App.vue

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <ul class="top-menu">
      <li><router-link to="/">Hello World</router-link></li>
      <li><router-link to="/my-component">My Vue Component</router-link></li>
    </ul>
    <router-view/>
  </div>
</template>
<script>
export default {
  name: 'App',
};
</script>
<style>
…
</style>

main.js

import singleSpaVue from 'single-spa-vue';
import Vue from 'vue';
import App from './App.vue';
import router from "./router";

Vue.config.productionTip = false;

const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    render: (h) => h(App),
    router
  },
});

export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;

export default vueLifecycles;

console.log('check vueLifecycles', vueLifecycles);

if (!window.singleSpaNavigate) {
  console.log('without window.singleSpaNavigate');
  new Vue({
    render: h => h(App),
    router
  }).$mount('#app');
}else{
  console.log('has window.singleSpaNavigate');
}

router.js

import Vue from 'vue'
import Router from 'vue-router'
import MyComponent from './components/MyComponent.vue'
import HelloWorld from './components/HelloWorld.vue'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      component: HelloWorld
    },
    {
      path: '/my-component',
      component: MyComponent
    },
  ]
})

vue.config.js

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  outputDir: '../../apps/ng-app-1/src/assets/vue-wc/',
  filenameHashing: false,
  configureWebpack: {
    output: {
      library: 'vue_app_2',
      libraryTarget: 'umd',
    },
  }
})

package.json

{
  "name": "vue-app-2",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.8.3",
    "vue": "^2.6.14",
    "vue-custom-element": "~3.3.0",
    "vue-router": "^3.0.0",
    "single-spa-vue": "~3.0.0",
    "single-spa": "~5.9.5"
  },
  "devDependencies": {
    "@babel/core": "^7.12.16",
    "@babel/eslint-parser": "^7.12.16",
    "@vue/cli-plugin-babel": "~5.0.0",
    "@vue/cli-plugin-eslint": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "eslint": "^7.32.0",
    "eslint-plugin-vue": "^8.0.3",
    "vue-template-compiler": "^2.6.14"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "@babel/eslint-parser"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

Additionally, I've launched the compiled Vue2 project through Express on port 5566, and I've confirmed that http://localhost:5566/js/app.js can be accessed correctly. Now, on to the Angular part.

Angular

Here are index.html, main.ts, tsconfig.json, tsconfig.app.json, vue-dialog.component.ts, vue-dialog.component.html, angular.json.

index.html

…
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.10.3/system.min.js"></script>
…

main.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import * as singleSpa from 'single-spa';
import { AppModule } from './app/app.module'

singleSpa.registerApplication(
  'vue_app_2',
  () => System.import('http://localhost:5566/js/app.js').then(module => module.default),
  location => location.pathname.startsWith('')
);

singleSpa.start();

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

tsconfig.json

{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "ES2022",
    "module": "ES2022",
    "useDefineForClassFields": false,
    "lib": [
      "ES2022",
      "dom"
    ]
  },
  "angularCompilerOptions": {
    "enableI18nLegacyMessageIdFormat": false,
    "strictInjectionParameters": true,
    "strictInputAccessModifiers": true,
    "strictTemplates": true
  }
}

tsconfig.app.json

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app",
    "types": ["systemjs"]
  },
  "files": [
    "src/main.ts"
  ],
  "include": [
    "src/**/*.d.ts"
  ]
}

vue-dialog.component.ts

import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormControl } from '@angular/forms';

import * as singleSpa from 'single-spa';

@Component({
  selector: 'app-vue-dialog',
  templateUrl: './vue-dialog.component.html',
  styleUrls: ['./vue-dialog.component.scss']
})
export class VueDialogComponent implements OnInit, OnDestroy {

  private parcel?: singleSpa.Parcel;

  constructor(
    public dialogRef: MatDialogRef<VueDialogComponent>,
    @Inject(MAT_DIALOG_DATA) public data: FormControl<string>,
  ) {

  }

  ngOnInit() {
    System.import('http://localhost:5566/js/app.js').then(module => {
      const domElement = document.getElementById('vue-container');
      if (!domElement) {
        console.error('Vue container does not exist');
        return;
      }
      if (!module.default || typeof module.default.bootstrap !== 'function') {
        console.error('Parcel config is not correct', module.default);
        return;
      }
      this.parcel = singleSpa.mountRootParcel(module.default, { domElement });
    });
  }
  ngOnDestroy() {
    if (this.parcel) {
      this.parcel.unmount();
    }
  }
}

vue-dialog.component.html

<section class="vue-dialog">
  <div id="vue-container"></div>
</section>

angular.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "ng-app-1": {
      "projectType": "application",
      "schematics": {
        "@schematics/angular:component": {
          "style": "scss"
        }
      },
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/ng-app-1",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": [
              "zone.js"
            ],
            "tsConfig": "tsconfig.app.json",
            "inlineStyleLanguage": "scss",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": []
          },
          "configurations": {
            "production": {
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "500kb",
                  "maximumError": "1mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "2kb",
                  "maximumError": "4kb"
                }
              ],
              "outputHashing": "all"
            },
            "development": {
              "buildOptimizer": false,
              "optimization": false,
              "vendorChunk": true,
              "extractLicenses": false,
              "sourceMap": true,
              "namedChunks": true
            }
          },
          "defaultConfiguration": "production"
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "configurations": {
            "production": {
              "browserTarget": "ng-app-1:build:production"
            },
            "development": {
              "browserTarget": "ng-app-1:build:development"
            }
          },
          "defaultConfiguration": "development"
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "ng-app-1:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "polyfills": [
              "zone.js",
              "zone.js/testing"
            ],
            "tsConfig": "tsconfig.spec.json",
            "inlineStyleLanguage": "scss",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": []
          }
        }
      }
    }
  },
  "cli": {
    "analytics": "62a5979e-66cb-482d-8e9f-3653e47ad32a"
  }
}
0

There are 0 best solutions below