简短的概括:微前端痛点与解决问题
1、使用背景
1)在同一个页面可以使用多个前端框架(React, AngularJS, Vue 等);
2)用新框架编写新代码,无需重写已有的 app;
3)代码的延迟加载可以缩减初次加载时长;
2、主要解决的问题:
1)在一个 app 中不同的模块由不同的团队维护,而每个团队所用的技术栈可能不同,而且发版周期不同。
2)所使用的前端框架升级负担,新版本可能存在不兼容的更新,升级后可能对已有的业务产生 bug,造成难以升级。限制了前端框架新版本的使用。
一、为什么需要微前端
「~ 微前端导图 ~」
我们通过 3w (what,why,how) 的方式来讲解微前端
1、what? 什么是微前端?
微前端就是将不同的功能按照不同的维度拆分成多个子应用。通过主应用来加载这些子应用。
微前端的核心在于拆,拆完后再合!
微前端架构具备以下几个核心价值:(重要)(摘自 qiankun官方文档)
1)技术栈无关
主框架不限制接入应用的技术栈,微应用具备完全自主权;
2)独立开发、独立部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新;
3)增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略;
4)独立运行时
每个微应用之间状态隔离,运行时状态不共享;
2、why? 为什么去使用他?
1)不同团队间开发同一个应用技术栈不同怎么破?
2)希望每个团队都可以独立开发,独立部署怎么破?
3)项目中还需要老的应用代码怎么破?
我们是不是可以将一个应用划分成若干个子应用,将子应用打包成一个个的lib。当路径切换时加载不同的子应用。这样每个子应用都是独立的,技术栈也不用做限制了!从而解决了前端协同开发问题。
3、How? 怎么落地微前端?
2018年 Single-SPA 诞生了,single-spa 是一个用于前端微服务化的 JavaScript 前端解决方案(本身没有处理样式隔离,js 执行隔离)实现了路由劫持和应用加载。
说明:single-spa 解决了以应用为维度的路由,应用的注册,监听,最重要的是赋予了应用生命周期和生命周期相关事件。
*Single-SPA 缺陷:不够灵活,不能动态加载js文件;样式不隔离,没有js沙箱的机制。
2019年 qiankun 是微前端框架,提供了更加开箱即用的 API (single-spa + sandbox + import-html-entry),它基于 single-spa,具备 js 沙箱、样式隔离、HTML Loader、预加载 等微前端系统所需的能力。qiakun 升级 2.0 后,支持多个微应用的同时加载,有了这个特性,我们基本可以像接入 iframe 一样方便的接入微应用。
*总结:子应用可以独立构建,运行时动态加载,主子应用完全解耦,技术栈无关,靠的是协议接入(子应用必须导出 bootstrap,mount,unmount方法)
扩展:
1)Single-SPA 官网地址:
https://zh-hans.single-spa.js.org/docs/getting-started-overview
2)qiankun官网地址:
https://qiankun.umijs.org/zh
二、解决隔离的方案
1、css 隔离方案
子应用之间样式隔离:
Dynamic Stylesheet 动态样式表,当应用切换时移除老应用样式,添加新应用样式;
主应用和子应用之间的样式隔离:
1)BEM(Block Element Modifier ) 约定项目前缀;
2)css-Modules 打包时生成不冲突的选择器名;
3)Shadow DOM 真正意义上的隔离;
4)css-in-js
2、沙箱 shaowDom
*css 解决方法:
// dom的api
// 外界无法访问 shadow dom
let shadowDOM = document.getElementById('x').attachShadow({mode: 'closed'});
let pElm = document.createElement('p');
pElm.innerHTML = 'hello';
let styleElm = document.createElement('style');
styleElm.textContent = `
p{color: red}
`
shadowDOM.appendchild(styleElm);
shadowDOM.appendchild(pElm);
*JS 沙箱 proxy
快照沙箱简单理解:1年前拍一张 在拍一张 (将区别保存起来) 在回到一年前
源码实践
let sandbox = new SnapshotSandbox();
class SnapshotSandbox{
constructor(){
this.proxy = window; // window属性
this.modifyPropsmap = {}; // 记录在window上的修改
this.active();
}
active() { // 激活
this.windowSnapshot = {}; //拍照
for(const prop in window) {
if(window.hasOwnProperty(prop)){
this.windowsnapshot[prop] = window[prop];
}
}
object.keys(this.modifyPropsMap).forEach(p=>{
window[p] = this.modifyPropsMap[p];
})
}
inactive(){ // 失活
for(const prop in window){
if(window.hasOwnProperty(prop)){
if(window[prop] !== this.windowsnapshot[prop]){
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowsnapshot[prop]
}
}
}
}
}
// 应用的运行 从开始到结束, 切换后不会影响全局
((window)=> {
window.a = 1;
window.b = 2;
console.log(window.a,window.b);
sandbox.inactive();
console.log(window.a,window.b);
sandbox.active();
console.log(window.a,window.b);
})(sandbox.proxy); // sandbox.proxy 就是window
// 如果是多个子应用就不能使用这种方式了,es6的proxy
// 代理沙箱可以实现多应用沙箱。把不同的应用用不同的代理来处理
三、qiankun (乾坤) 项目实践
*将普通的项目改造成 qiankun 主应用基座,需要进行三步操作:
1、创建微应用容器 - 用于承载微应用,渲染显示微应用
2、注册微应用 - 设置微应用激活条件,微应用地址等等;
3、启动 qiankun;
扩展:主应用不限技术栈,只需要提供一个容器 DOM,然后注册微应用并 start 即可。
*微前端 qiankun 项目实践:主应用(基座)配置 react 17.0.2,子应用配置 vue 2.6.10
详细配置如下:
1. 主应用(基座)配置 react 17.0.2
1.1 主应用为子应用准备的 展示元素 (文件:src/App.js )
import {BrowserRouter as Router,Link} from 'react-router-dom'
function App() {
return (
<div className="App">
<Router>
<Link to="/vue">vue应用</Link>
</Router>
{/* 切换导航, 将微应用渲染container容器中 */}
<div id="container"></div>
</div>
);
}
export default App;
1.2 引入react 渲染,注册 registerApps (文件:src/index.js )
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './registerApps'
ReactDOM.render(
<App />,
document.getElementById('root')
);
1.3 在主应用中注册微应用(文件:src/registerApps.js)
1、安装 qiankun (建议安装:qiankun 2.X以上,支持多个微应用的同时加载)
yarn add qiankun 或者 npm i qiankun
相关配置信息:
// ------ Step1 引入 qiankun
import { registerMicroApps, start } from 'qiankun'; // 底层是基于single-spa
// ----- Step2 注册子应用
registerMicroApps([{
name: 'm-vue',
entry: '//localhost:20000',
container: '#container',
activeRule: '/vue',
},
], {
beforeLoad: () => {
console.log('加载前')
},
beforeMount: () => {
console.log('挂在前')
},
afterMount: () => {
console.log('挂载后')
},
beforeUnmount: () => {
console.log('销毁前')
},
afterUnmount: () => {
console.log('销毁后')
},
})
// ----- Step3 启动应用
start();
2. 子应用配置 vue 2.6.10
主应用基座只有一个主页,现在我们需要接入微应用。
qiankun 内部通过 import-entry-html 加载微应用,要求微应用需要导出生命周期钩子函数(见下图)。
从上图可以看出,qiankun 内部会校验微应用的生命周期钩子函数,如果微应用没有导出这三个生命周期钩子函数,则微应用会加载失败。
如果我们使用了脚手架搭建微应用的话,我们可以通过 webpack 配置在入口文件处导出这三个生命周期钩子函数。如果没有使用脚手架的话,也可以直接在微应用的 window 上挂载这三个生命周期钩子函数。
2.1 调整子应用 main.js 文件:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
// Vue.config.productionTip = false
let instance = null
function render(props) {
instance = new Vue({
router,
render: h => h(App)
}).$mount('#app'); // 这里是挂载到自己的html中 基座会拿到这个挂载后的html 将其插入进去
}
if (window.__POWERED_BY_QIANKUN__) { // 动态添加publicPath
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) { // 默认独立运行
render();
}
// 需要暴露接入协议
export async function bootstrap(props) {
console.log('[vue] vue app bootstraped');
};
export async function mount(props) {
console.log('[vue] props from main framework', props);
render(props);
}
export async function unmount(props) {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
router = null;
}
说明:导出相应的生命周期钩子函数。
微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。
*扩展资源:
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
/**
* (重要)应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
console.log('[vue] props from main framework', props);
storeTest(props);
render(props);
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
router = null;
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props);
}
2.2 新建 vue.config.js,配置如下
module.exports = {
devServer:{
port:10000,
headers:{
// 解决跨域
'Access-Control-Allow-Origin':'*'
}
},
configureWebpack:{
output:{
// 把子应用打包成 umd 库格式
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
}
}
}
*基于 qiankun 微前端项目 (实践代码库)
https://github.com/jiasx/mic-front-vue2.0
https://github.com/jiasx/mic-front-react