前言
其实方便点可以使用 qiankun 的微前端方案
依赖版本:
1 2 | "single-spa": "^5.5.2", "single-spa-vue": "^1.8.2", |
流程
主应用流程
-
启动由
system.js 接管,配置webpack 下out.libraryTarget 为system -
html 入口中通过importmap ,设置当前应用、子应用名称+地址 -
一般用法(
DOM 节点一直存在的情况下):registerApplication 注册子应用,通过system.js 引入,设置渲染路由activeWhen ,传递给子应用的参数customProps -
使用
Parcel 用法(DOM 节点不是一直存在的情况下):主应用也需要包裹singleSpaVue /singleSpaReact 等,然后registerApplication 自己,在某个组件(A)内使用由main.js/ts 在bootstraps /mount 时导出的mountParcel ,在某组件(A)挂载后,手动将子应用(当做组件用)挂载到这个组件的某个DOM 节点(见1.3)
子应用流程(Vue)
-
启动方式由
single-spa-vue 接管,可以判断window.singleSpaNavigate 为false 单独启动 -
配置在主应用的挂载点,
appOptions 下的el 设置,默认挂载到body 下 -
导出一些生命周期事件,至少如下三个:
bootstrap /mount /unmount ,可以在mount 下接收主应用传递的参数 -
异步组件需要使用:(不然主应用使用子应用会报错)
systemjs-webpack-interop 设置setPublicPath ;webpack 配置:config.output.jsonpFunction = 'wpJsonpFlightsWidget';
1、主项目的配置
- 例子1:Vue: json-util,路由
/sub-app - 例子2:React: md-note,在线,首页的时钟/日历是
vue 写的,使用single-spa 加载的
1.1 下载依赖
下载
1 | yarn add single-spa |
1.2 配置
在 HTML 入口
systemjs-importmap 也可以通过配置文件自动生成,这样也好区分开发环境跟生成环境不同的入口,注意打包后子应用的入口的跨域问题
- 使用
webpack 自动插入HTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | // systemJs-Importmap.js const isEnvDev = process.env.NODE_ENV === 'development'; // systemjs-importmap 的配置,通过webpack给html用 module.exports = [ { name: 'root-config', entry: './js/app.js', }, { name: '@vue-mf/calendar', entry: isEnvDev ? '//localhost:2333/js/app.js' : 'https://zero9527.github.io/vue-calendar/js/app.js', }, ]; // vue.config.js chainWebpack: config => { config.plugin('html').tap(args => { const importMap = { imports: {} }; systemJsImportmap.forEach(item => (importMap.imports[item.name] = item.entry)); args[0].systemJsImportmap = JSON.stringify(importMap, null, 2); return args; }); }, // public\\index.html <meta name="importmap-type" content="systemjs-importmap" /> <script type="systemjs-importmap"> <%= htmlWebpackPlugin.options.systemJsImportmap %> </script> <script src="./libs/systemjs/system.min.js"></script> <script src="./libs/systemjs/extras/amd.min.js"></script> <script src="./libs/systemjs/extras/named-exports.min.js"></script> <script src="./libs/systemjs/extras/named-register.min.js"></script> <script src="./libs/systemjs/extras/use-default.min.js"></script> <script> System.import('root-config'); </script> |
- 在
public/index.html 下手动添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <meta name="importmap-type" content="systemjs-importmap" /> <script type="systemjs-importmap"> { "imports": { "root-config": "//localhost:666/js/app.js", "@vue-mf/calendar": "//localhost:2333/js/app.js" } } </script> <script src="./libs/systemjs/system.min.js"></script> <script src="./libs/systemjs/extras/amd.min.js"></script> <script src="./libs/systemjs/extras/named-exports.min.js"></script> <script src="./libs/systemjs/extras/named-register.min.js"></script> <script src="./libs/systemjs/extras/use-default.min.js"></script> <script> System.import('root-config'); </script> |
- 里面的东西是一个
JSON ,注意格式!
这里配置当前应用的配置
1 2 3 4 5 6 7 8 | <script type="systemjs-importmap"> { "imports": { "root-config": "//localhost:666/js/app.js", "@vue-mf/calendar": "//localhost:2333/js/app.js" } } </script> |
- 子应用名称
@vue-mf/calendar ,在registerApplication 时,对应app: import('@vue-mf/calendar') 的名称,如
1 2 3 4 5 6 7 8 | registerApplication({ name: '@vue-mf/calendar', app: () => (window as any).System.import('@vue-mf/calendar'), activeWhen: '', customProps: { root: 'json-util', }, }); |
系统启动由 systemJS 接管
- html
1 2 3 | <script> System.import('root-config'); </script> |
- 对应的
webpack 配置
去掉文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // vue.config.js module.exports = { outputDir: 'docs', publicPath: './', filenameHashing: false, productionSourceMap: false, configureWebpack: config => { config.output.libraryTarget = 'system'; config.devServer = { port: 666, headers: { 'Access-Control-Allow-Origin': '*', }, disableHostCheck: true, historyApiFallback: true, }; }, }; |
注册子应用
- single-spa.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 | // src\\single-spa-config.ts import { registerApplication, start } from 'single-spa'; registerApplication({ name: '@vue-mf/calendar', app: () => (window as any).System.import('@vue-mf/calendar'), activeWhen: '', customProps: { root: 'json-util', }, }); start(); |
- 在
main.ts 中引入
其实在哪引入都可以,确保
1 2 | // src\\main.ts import './single-spa-config'; |
1.3 Parcel 配置
官方文档
翻译过来叫:包裹,可以在主应用将一个子应用当做组件,手动挂载、卸载使用,不限框架,
webpack 5 有一个Module Federation 也是可以跨项目使用组件的
什么时候用
把子应用当做一个组件使用,放在主应用的某个组件(A)下面时,
-
主应用:使用
singleSpaVue /singleSpaReact 包裹,然后registerApplication 自己,在某个组件(A)内使用由main.js/ts 在bootstraps /mount 时导出的mountParcel ,在某组件(A)挂载后,手动将子应用(当做组件用)挂载到这个组件的某个DOM 节点 -
子应用:不需要在主应用
registerAppliaction 注册,而是手动在某个组件(A)内手动挂载到某个DOM 节点
主应用改造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // src\\main.ts //... // **************** 主应用一般写法 **************** // // 子应用 registerAppliaction 注册 // new Vue({ // router, // render: (h: any) => h(App), // }).$mount('#json-util'); // **************** 主应用使用 Parcel 写法 **************** // 主应用使用 Parcel 挂载子应用(某组件下)的时候的写法 // 需要把当前应用当做子应用,然后 registerAppliaction 调用 const singleSpa = singleSpaVue({ Vue, appOptions: { el: '#json-util', render: (h: any) => h(App), router, }, }); // eslint-disable-next-line export let mountParcel: any; export const bootstrap = (props: any) => { mountParcel = props.mountParcel; return singleSpa.bootstrap(props); }; export const { mount, unmount } = singleSpa; |
注册子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import { registerApplication, start } from 'single-spa'; // 改为 Parcel 手动挂载子应用了,需要导出 mountParcel,已经用 singleVue 包裹了,所以要用 registerApplication 启动 registerApplication({ name: 'root-config', app: () => (window as any).System.import('root-config'), activeWhen: () => true, }); registerApplication({ name: '@vue-mf/calendar', app: () => (window as any).System.import('@vue-mf/calendar'), activeWhen: location => { return location.href.includes('/sub-app'); }, customProps: { root: 'json-util', }, }); // 改为 Parcel 手动挂载了,所有这个要去掉 // registerApplication({ // name: '@vue-mf/clock', // app: () => (window as any).System.import('@vue-mf/clock'), // activeWhen: location => { // return location.href.includes('/sub-app'); // }, // customProps: { // root: 'json-util', // }, // }); start(); |
手动挂载
某个组件(A)在
使用了 composition-api
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import { mountParcel } from '@/main'; const parcel = ref<any>(null); const mountClockParcel = () => { const routePath = ctx.root.$route.path; const domElement = document.getElementById('app-clock'); if (routePath === '/sub-app' && domElement) { const parcelConfig = (window as any).System.import('@vue-mf/clock'); parcel.value = mountParcel(parcelConfig, { domElement }); } else if (parcel.value) { parcel.value.unmount(); } }; onMounted(() => { mountClockParcel(); }); watch( () => ctx.root.$route.path, () => { mountClockParcel(); }, ); |
2、子项目的配置(Vue)
例子:vue-calendar
2.1 下载依赖
- 下载
single-spa-vue
1 | yarn add single-spa-vue |
- 下载
vue-cli-plugin-single-spa
解决这个问题
single-spa.min.js?25a2:2 single-spa minified message #37: See https://single-spa.js.org/error/?code=37&arg=application
1 | yarn add -D vue-cli-plugin-single-spa |
2.2 配置
- 应用入口
main.js/ts
注意:
appOptions 下,el 可以给当前应用配置在主应用的挂载DOM 节点,这个节点需要提前设置好;不提供el 的话默认挂载在body 下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // 其他的代码省略 import Vue from 'vue'; import singleSpaVue from 'single-spa-vue'; // ... // ============= 非 single-spa 单独启动 ============= if (!(window as any).singleSpaNavigate) { new Vue({ render: (h: any) => h(App), }).$mount('#app-calendar'); } // ============= single-spa 模式启动 ============= const vueLifeCycles = singleSpaVue({ Vue, appOptions: { // el:挂载的dom节点,在主项目需要有;没有el的话会添加到body下 el: '#app-calendar', render: (h: any) => h(App), }, }); export function bootstrap(props: object) { return vueLifeCycles.bootstrap(props); } export function mount(props: object) { console.log('mount: ', props); return vueLifeCycles.mount(props); } export function unmount(props: object) { return vueLifeCycles.unmount(props); } |
2.3 问题
问题描述
子项目使用异步组件
子应用使用异步组件,在主应用报错
1 | Uncaught TypeError: application '@vue-mf/calendar' died in status BOOTSTRAPPING: Object(...) is not a function |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <template> <div id="app-calendar"> <div class="title">Vue-Calendar</div> <Calendar /> </div> </template> <script lang="ts"> // 正常 import Calendar from '@/components/Calendar/index.vue'; // single-spa在主应用加载:不行 // const Calendar = () => import(@/components/Calendar/index.vue); // single-spa在主应用加载:不行 // import AsyncComponent from '@/components/AsyncComponent/index'; // single-spa 下使用异步组件,在主应用加载有问题 // const Calendar = AsyncComponent(() => // import( // /* webpackPrefetch: true */ // /* webpackChunkName: 'calendar' */ // '@/components/Calendar/index.vue' // ), // ); export default { name: 'App', components: { Calendar, }, }; </script> |
异步组件问题解决
子项目添加如下设置
1 2 3 4 5 6 | // src\\set-public-path.ts import { setPublicPath } from 'systemjs-webpack-interop'; if ((window as any).singleSpaNavigate) { setPublicPath('@vue-mf/calendar', 2); } |
1 2 | // vue.config.js config.output.jsonpFunction = 'wpJsonpFlightsWidget'; |
https://single-spa.js.org/docs/recommended-setup/#build-tools-webpack--rollup
参考
- single-spa
- 等等