落地微前端

一、概述

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
});
// 子应用必须导出 以下生命周期 bootstrap、mount、unmount
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
// qiankun.config.js
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 动态样式表,当应用切换时移除老应用样式,添加新应用样式

主应用和子应用之间的样式隔离

  • BEM (Block Element Modifier) 约定项目前缀

  • CSS-Modules 打包时生成不冲突的选择器名

  • Shadow DOM 真正意义上的隔离

  • css-in-js

五、沙箱机制

  • 快照沙箱,在应用沙箱挂载或卸载时记录快照,在切换时依据快照恢复环境 (无法支持多实

例)

  • Proxy 代理沙箱,不影响全局环境