一、概述
2018年 Single-SPA诞生了, single-spa 是一个用于前端微服务化的 JavaScript 前端解决方案 (本身没有处理样式隔离, js 执行隔离) 实现了路由劫持和应用加载。
2019年 qiankun 基于Single-SPA, 提供了更加开箱即用的 API ( single-spa + sandbox+ import-html-entry ) 做到了,技术栈无关、并且接入简单(像i frame 一样简单)
总结:子应用可以独立构建,运行时动态加载,主子应用完全解耦,技术栈无关,靠的是协议接入(子应用必须导出 bootstrap、mount、unmount方法)
微前端架构核心价值
- 技术栈无关
主框架不限制接入应用的技术栈,微应用具备完全自主权
- 独立开发、独立部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
- 增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
- 独立运行时
每个微应用之间状态隔离,运行时状态不共享
应用通信
基于URL来进行数据传递,但是传递消息能力弱
基于 CustomEvent 实现通信
基于props主子应用间通信
使用全局变量、 Redux 进行通信
公共依赖
CDN - externals
webpack 联邦模块
二、single-spa
1.构建子应用
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
| import singleSpaVue from 'single-spa-vue'; const appOptions = { el: '#vue', router, render: h = >h(App) }
if (!window.singleSpaNavigate) { delete appOptions.el; new Vue(appOptions).$mount('#app'); }
const vueLifeCycle = singleSpaVue({ Vue, appOptions });
export const bootstrap = vueLifeCycle.bootstrap; export const mount = vueLifeCycle.mount; export const unmount = vueLifeCycle.unmount; export default vueLifeCycle;
const router = new VueRouter({ mode: 'history', base: '/vue', routes })
|
2.配置库打包 (将子模块打包成类库)
1 2 3 4 5 6 7 8 9 10 11
| module.exports = { configureWebpack: { output: { library: 'singleVue', libraryTarget: 'umd' }, devServer: { port: 10000 } } }
|
3.主应用搭建(将子应用挂载到 id=”vue” 标签中)
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
| import Vue from 'vue' import App from './App.vue' import router from './router' import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI); const loadScript = async(url) = >{ await new Promise((resolve, reject) = >{ const script = document.createElement('script'); script.src = url; script.onload = resolve; script.onerror = reject; document.head.appendChild(script) }); } import { registerApplication, start } from 'single-spa'; registerApplication('singleVue', async() = >{ await loadScript('http://localhost:10000/js/chunk-vendors.js'); await loadScript('http://localhost:10000/js/app.js'); return window.singleVue }, location = >location.pathname.startsWith('/vue')) start(); new Vue({ router, render: h = >h(App) }).$mount('#app')
|
4.动态设置子应用 publicPath
1 2 3
| if (window.singleSpaNavigate) { __webpack_public_path__ = 'http://localhost:10000/' }
|
三、qiankun
1.主应用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import { registerMicroApps, start }from 'qiankun' const apps = [{ name: 'vueApp', entry: '//localhost:10000', container: '#vue', activeRule: '/vue' }, { name: 'reactApp', entry: '//localhost:20000', container: '#react', activeRule: '/react' }] registerMicroApps(apps);
start();
|
2.子应用 vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| let instance = null; function render() { instance = new Vue({ router, render: h = >h(App) }).$mount('#app') } if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } if (!window.__POWERED_BY_QIANKUN__) { render() } export async function bootstrap() {} export async function mount(props) { render(); } export async function unmount() { instance.$destroy(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| // 打包配置 module.exports = { devServer: { port: 10000, headers: { 'Access-Control-Allow-Origin': '*' } }, configureWebpack: { output: { library: 'vueApp', libraryTarget: 'umd' } } }
|
3.子应用react
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; function render() { ReactDOM.render( < React.StrictMode > <App / ></React.StrictMode>, document.getElementById('root') ); } if(!window.__POWERED_BY_QIANKUN__){ render() } export async function bootstrap() {} export async function mount() {render();} export async function unmount() { ReactDOM.unmountComponentAtNode(document.getElementById("root")); }
|
重写 react 中的 webpack 配置文件 ( config-overrides.js )
yarn add react-app-rewired –save-dev
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| module.exports = { webpack: (config) = >{ config.output.library = `reactApp`; config.output.libraryTarget = "umd"; config.output.publicPath = 'http://localhost:20000/' return config }, devServer: function(configFunction) { return function(proxy, allowedHost) { const config = configFunction(proxy, allowedHost); config.headers = { "Access-Control-Allow-Origin": "*", }; return config; }; }, };
|
配置 .env 文件
1 2
| PORT=20000 WDS_SOCKET_PORT=20000
|
React路由配置
1 2 3 4 5 6 7 8 9 10
| import { BrowserRouter, Route, Link } from "react-router-dom" const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react": ""; function App() { return ( < BrowserRouter basename = { BASE_NAME } > <Link to = "/" > 首页 < /Link> <Link to="/about ">关于</Link> <Route path=" / " exact render={() => <h1>hello home</h1>}></Route> <Route path=" / about " render={() => <h1>hello about</h1>}></Route> </BrowserRouter> ); }"
|
四、CSS隔离方案
子应用之间样式隔离
- Dynamic Stylesheet 动态样式表,当应用切换时移除老应用样式,添加新应用样式
主应用和子应用之间的样式隔离
五、沙箱机制
- 快照沙箱,在应用沙箱挂载或卸载时记录快照,在切换时依据快照恢复环境 (无法支持多实
例)