基于 qiankun 微前端实践- 从 0 到 1 篇

752 阅读6分钟

简短的概括:微前端痛点与解决问题

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 官网地址:

zh-hans.single-spa.js.org/docs/gettin…

2)qiankun官网地址:

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 domlet 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 引入 qiankunimport { 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 = falselet instance = nullfunction 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 微前端项目 (实践代码库)

github.com/jiasx/mic-f…

github.com/jiasx/mic-f…