前言
其实方便点可以使用 qiankun 的微前端方案
依赖版本:
"single-spa": "^5.5.2",
"single-spa-react": "^2.14.0",
-
本文主应用(Vue):json-util,进去后路由手动切到
/sub-app就能看到了,这里时钟和日历是Vue项目作为子应用,另一个就是React项目作为子应用 -
子应用(React):react_single-spa_example
-
主应用(React):可以看这里 md-note,这里用到
Vue项目作为子应用,没有用到React项目作为子应用
流程
其实主应用流程,不管是
Vue还是React都差不多
主应用流程(Vue)
-
启动由
system.js接管,配置webpack下output.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.6)
- 主应用也需要包裹
子应用流程(React)
-
启动方式由
single-spa-react接管,可以判断window.singleSpaNavigate为false单独启动 -
配置在主应用的挂载点,
domElementGetter返回一个DOM节点,默认挂载到body下(使用Parcel的话就不需要配置) -
导出一些生命周期事件,至少如下三个:
bootstrap/mount/unmount,可以在mount下接收主应用传递的参数 -
设置子应用的
publicPath:systemjs-webpack-interop设置setPublicPath;记住名称要与主应用引入的一致
1、主应用(Vue)
主应用使用
Parcel引用子应用时,需要自身使用single-spa-react
1.1 下载依赖
yarn add single-spa
1.2 配置
single-spa-config.ts
在 src 下创建 single-spa-config.ts 文件
这里注意为什么包一层函数,因为 tree-shaking 的原因!!!
如果不包裹,在导入的地方
import '@/single-spa-config.ts'打包后就没了,但是开发环境还是在的,可以正常运行!!!
// src/single-spa-config.ts
import { registerApplication, start } from 'single-spa';
export default function singleSpaSetup() {
// 改为 Parcel 手动挂载子应用了,需要导出 mountParcel,已经用 singleVue 包裹了,所以要用 registerApplication 启动
registerApplication({
name: 'root-config',
app: () => (window as any).System.import('root-config'),
activeWhen: () => true,
});
start();
}
1.3 修改 webpack
Access-Control-Allow-Origin
开发环境添加 headers:
// vue.config.js
configureWebpack: config => {
config.output.libraryTarget = 'system';
config.devServer = {
port: 666,
headers: {
'Access-Control-Allow-Origin': '*',
},
disableHostCheck: true,
historyApiFallback: true,
};
},
libraryTarget
将 output.libraryTarget 改为 system
去掉文件 hash
// vue.config.js
filenameHashing: false,
1.4 HTML 入口
<!-- public/index.html -->
<meta name="importmap-type" content="systemjs-importmap" />
<script type="systemjs-importmap"></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>
1.5 webpack 配置自动导入
新建一个 systemJs-Importmap.js
里面就像这样
// 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
? '//zero9527.site/vue-calendar/js/app.js' // '//localhost:2333/js/app.js'
: '//zero9527.site/vue-calendar/js/app.js',
},
{
name: '@vue-mf/clock',
entry: isEnvDev
? '//zero9527.site/vue-clock/js/app.js' // '//localhost:2334/js/app.js'
: '//zero9527.site/vue-clock/js/app.js',
},
{
name: '@react-mf/test',
entry: isEnvDev
? '//localhost:2335/js/app.js'
: 'https://zero9527.github.io/clock/js/app.js',
},
];
配置 externals
注意! 如果设置了
externals,第三方包就不能通script的方式引入了,要用systemjs-importmap的方式引入(可以看看这个项目的配置 md-note)
配置 html-webpack-plugin 参数
可以是直接在 options 下增加参数,也可以 templateParameters,
区别是 templateParameters 可以直接在 HTML入口 引用,而 options 的话就要带一串东西
templateParameters很方便,但是不好加参数,还是options好加
- html 引入
<script type="systemjs-importmap"></script>
- 修改 htmlWebpackPlugin
// 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;
});
},
1.6 子应用 Appliaction
注意!
DOM节点应该一直存在(如果在子应用那里设置了挂载节点el的话,默认挂载在body下面),不然放在某个组件下面,第一次进入正常,但是再回来就会报错,找不到el的那个节点,
上面这种情况其实应该用
Parcel
项目入口如 src/main.ts 引入 single-spa-config.ts ,然后执行;
// src/single-spa-config.ts
import { registerApplication, start } from 'single-spa';
export default function singleSpaSetup() {
// 改为 Parcel 手动挂载子应用了,需要导出 mountParcel,已经用 singleVue 包裹了,所以要用 registerApplication 启动
registerApplication({
name: 'root-config',
app: () => (window as any).System.import('root-config'),
activeWhen: () => true,
});
start();
}
1.7 子应用 Parcel
翻译过来叫:包裹,可以在主应用将一个子应用当做组件,手动挂载、卸载使用,不限框架,webpack 5 有一个 Module Federation 也是可以跨项目使用组件的,更细粒化
使用 Parcel 用法(DOM 节点不是一直存在的情况下):
- 主应用也需要包裹
singleSpaVue/singleSpaReact等, - 然后
registerApplication自己, - 在某个组件(A)内使用由
main.js/ts在bootstraps/mount时导出的mountParcel, - 在某组件(A)挂载后,手动将子应用(当做组件用)挂载到这个组件的某个
DOM节点
下载 single-spa-vue
yarn add single-spa-vue
主应用入口 src/main.ts
// src\main.ts
import Vue from 'vue';
import singleSpaVue from 'single-spa-vue';
import VueCompositionApi from '@vue/composition-api';
import singleSpaSetup from '@/single-spa-config';
import Iconfont from '@/components/Iconfont/index.vue';
import router from './router';
import App from './App.vue';
singleSpaSetup();
Vue.use(VueCompositionApi);
Vue.component('icon-font', Iconfont);
Vue.config.productionTip = false;
// **************** 主应用一般写法 ****************
// // 子应用 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;
使用子应用的地方
src\views\SubApp\index.vue
<template>
<div class="sub-app">
<h3>sub-app</h3>
<div id="app-clock" />
<div id="app-calendar" />
<div id="app-reacttest" />
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted } from '@vue/composition-api';
import { mountParcel } from '@/main';
export default defineComponent({
name: 'SubApp',
setup() {
onMounted(() => {
mountParcelHandler('@vue-mf/clock', 'app-clock');
// mountParcelHandler('@vue-mf/calendar', 'app-calendar');
mountParcelHandler('@react-mf/test', 'app-reacttest');
});
const mountParcelHandler = (appName: string, domElementId: string) => {
const parcelConfig = (window as any).System.import(appName);
const domElement = document.getElementById(domElementId);
mountParcel(parcelConfig, { domElement });
};
},
});
</script>
<style lang="less"></style>
2、子应用配置(React)
2.1 下载依赖
- single-spa-react
yarn add single-spa-react
- systemjs-webpack-interop
yarn add systemjs-webpack-interop
- react-app-rewired
yarn add -D react-app-rewired
2.2 应用入口 src/index.tsx
import './set-public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import * as serviceWorker from './serviceWorker';
import App from './App';
import './index.css';
if (!(window as any).singleSpaNavigate) {
ReactDOM.render(<App />, document.getElementById('root'));
}
const domElementGetter = () => {
return document.getElementById('root')!;
};
// =========== single-spa模式 ===========
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
domElementGetter,
rootComponent: () => <App />,
});
export const { bootstrap, mount, unmount } = reactLifecycles;
serviceWorker.unregister();
2.3 设置 publicPath
注意 名称 要与主应用引入的一致
// src\set-public-path.ts
import { setPublicPath } from 'systemjs-webpack-interop';
if ((window as any).singleSpaNavigate) {
setPublicPath('@react-mf/test', 2);
}
2.4 修改 webpack 配置
使用 react-app-rewired 配置
config-overrides.js
// config-overrides.js
const path = require('path');
const pathResolve = (_path) => path.resolve(__dirname, _path);
process.env.PORT = 2335;
// react-app-rewired: webpack配置覆盖
module.exports = {
webpack: function override(config, env) {
config.entry = pathResolve('src/index.tsx');
config.resolve.alias = {
...config.resolve.alias,
'@': pathResolve('src'),
};
config.output = {
...config.output,
publicPath: '',
libraryTarget: 'system',
filename: 'js/app.js',
chunkFilename: 'js/[name].[contenthash:8].js',
};
// console.log(config.output);
// process.exit(1);
delete config.optimization;
return config;
},
devServer: function (configFunction) {
return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
return {
...config,
headers: {
'Access-Control-Allow-Origin': '*',
},
disableHostCheck: true,
sockHost: 'localhost',
sockPort: 2335,
port: 2335,
hot: true,
};
};
},
paths: function (paths, env) {
return paths;
},
jest: function (config) {
if (!config.testPathIgnorePatterns) {
config.testPathIgnorePatterns = [];
}
if (!process.env.RUN_COMPONENT_TESTS) {
config.testPathIgnorePatterns.push(
'<rootDir>/src/components/**/*.test.js'
);
}
if (!process.env.RUN_REDUCER_TESTS) {
config.testPathIgnorePatterns.push('<rootDir>/src/reducers/**/*.test.js');
}
return config;
},
};