微前端学习(qiankun、singleSpa)
一、微前端的优势
-
什么是微前端
-
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略
几个核心价值:技术栈无关,独立开发、独立部署,增量升级,独立运行时
-
-
特点
- 基于single-spa封装,提供了更加开箱即用的 API。
- HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
- 样式隔离,确保微应用之间样式互相不干扰。
- JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
- 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
- umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
二、知识点扩展
-
样式的隔离:
-
实现原理:影子根attachShadow() 开辟一个封闭的阴影盒子,避免其他样式的引用
-
attachShadow学习:https://developer.mozilla.org/zh-CN/docs/Web/API/Element/attachShadow
-
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>样式隔离演示</title> </head> <body> <div> <p>fur</p> <div id="shadow"></div> </div> <script> let shadow = document.getElementById('shadow') //可以省略 let shadowDOM = shadow.attachShadow({ mode: 'closed' }); //外界无法访间closed let pElm = document.createElement('p'); pElm.innerHTML = "hello fur" let styleElm = document.createElement("style"); styleElm.textContent = `p{color:red}` // 添加样式 shadowDOM.appendChild(styleElm) shadowDOM.appendChild(pElm) document.body.appendChild(pElm) // 添加到阴影外面,就无法控制其样式 </script> </body> </html>
-
-
快照沙箱
-
作用:隔离js,防止全局污染
-
实现原理:默认初始化先循环遍历保存一份以前的数据,切换inactive(失活状态下),先保存不同的数据(方便以后的数据恢复),然后恢复为以前的数据;切换active(激活状态),把数据恢复为刚刚保存的不同的数据。
-
j简单代码示例:(浅拷贝记录数据)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>快照沙箱实现示例</title> </head> <body> </body> <script> //快照沙箱dome class SnapshotSandbox { constructor() { this.proxy = window; // window属性 this.modifyPropsMap = { }; //记录在window上的修改 this.active(); } active() { //激活 this.windowSnapshot = { }; //拍照:保存全部window属性 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 (this.windowSnapshot[prop] !== window[prop]) { this.modifyPropsMap[prop] = window[prop]; ///保存变化 window[prop] = this.windowSnapshot[prop] //失活:变回原来 } } } } } let sandbox = new SnapshotSandbox(); ((window) => { // 1、实例化sandbox时默认激活了一次 window.a = 1 window.b = 2 console.log(window.a) //1 sandbox.inactive() //失活 console.log(window.a) //undefined sandbox.active() //激活 console.log(window.a) //1 })(sandbox.proxy); //sandbox.proxy就是window </script> </html>
-
-
js沙箱
-
作用:隔离js,防止全局污染
-
实现原理:利用**new Proxy()**包装代理不同的windon对象,代理对象之间会不干扰
-
-
Proxy()学习Script/Reference/Global_Objects/Proxy
-
代码示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>proxy多应用沙箱实现示例(js沙箱)</title> </head> <body> </body> <script> //proxy的demo class ProxySandbox { constructor() { const rawWindow = window; const fakeWindow = { } const proxy = new Proxy(fakeWindow, { set(target, p, value) { target[p] = value; return true }, get(target, p) { return target[p] || rawWindow[p]; } }) this.proxy = proxy } } /** * 原理: * 1、利用Proxy实例化不同的window对象 * 2、不同的对之间互不干预。 * */ let sandbox1 = new ProxySandbox(); let sandbox2 = new ProxySandbox(); window.a = 1; ((window) => { window.a = 'hello'; console.log(window.a) })(sandbox1.proxy); ((window) => { window.a = 'world'; console.log(window.a) })(sandbox2.proxy); </script> </html>
三、实现微前端的方法
-
iframe
- 特点:iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享。
- 缺点:
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中…
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
-
singleSpa
-
配置实现:
-
主应用配置
-
下载依赖:
npm i single-spa -savejs
-
main.js 中配置
import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import { registerApplication,start} from "single-spa" Vue.config.productionTip = false // 封装:script标签动态加载 async function loadScript(url){ return new Promise((resolve,reject)=>{ let script = document.createElement("script"); script.src = url; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }) } /** * singlespa 缺陷:不够灵活,不能动态加载js文件 * 样式不隔离,没有js沙箱机制 */ registerApplication("myApp", async ()=>{ console.log("加载子应用中..."); await loadScript("http://localhost:10000/js/chunk-vendors.js"); await loadScript("http://localhost:10000/js/app.js"); return window.sigleVue }, location=>location.pathname.startsWith("/vue"), // 匹配加载路由 ); // 开启应用 start(); new Vue({ router, store, render: h => h(App) }).$mount('#app')
-
挂载应用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hI6y7kKl-1603706137263)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20201026165342047.png)]
-
-
子应用配置(这里的子应用是vue,需要下载对应的依赖)
-
下载依赖:
npm i single-spa-vue -save
-
main.js 中配置
import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import singleSpaVue from 'single-spa-vue'; Vue.config.productionTip = false // 子应用配置 const appOptions = { el:"#vue", // 子应用挂载到父应用中id为vue的标签中 router, render:h=>h(App) } // 分条件动态运行项目 // 作为子应用运行 if(window.singleSpaNavigate){ __webpack_public_path__ = "http://localhost:10000/"; } // 作为独立项目运行 if(!window.singleSpaNavigate){ delete appOptions.el; new Vue(appOptions).$mount("#app") } // 创建劫持对象 const vueLifeCycle = singleSpaVue( { Vue, appOptions } ) /** * 协议接入 我定好了协议 父应用会调用这些方法 * 导出变量:umd类型挂载windows * */ export const bootstrap = vueLifeCycle.bootstrap; export const mount = vueLifeCycle.mount; export const unmount = vueLifeCycle.unmount; // new Vue({ // router, // store, // render: h => h(App) // }).$mount('#app')
-
-
-
缺点:
-
不够灵活,不能动态加载js文件
-
样式不隔离,没有js沙箱机制
-
-
四、qiankun 微服务实现
-
什么是qiankun:
- 官网:https://qiankun.umijs.org/zh
- 优点:
- 几乎包含所有构建微前端系统时所需要的基本能力,如 样式隔离、js 沙箱、预加载等。
- 任意 js 框架均可使用。微应用接入像使用接入一个 iframe 系统一样简单,但实际不是 iframe。
-
依赖安装:
$ yarn add qiankun # or npm i qiankun -S
-
基本使用
import { loadMicroApp } from 'qiankun'; // 单个应用注册加载 loadMicroApp({ name: 'reactApp', entry: '//localhost:7100', container: '#container', props: { slogan: 'Hello Qiankun' }, });
-
项目代码示例:
- 说明:
- 主应用是:vue+element-ui
- 2个子级应用:vue、react
- 说明:
-
主应用vue+element-ui配置:
- 下载依赖:
npm i qiankun -S
-
main.js 配置
import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import { registerMicroApps,start} from 'qiankun' Vue.use(ElementUI); Vue.config.productionTip = false // 多个应用配置 const apps = [ { name: 'vueApp', //应用名 entry:'//localhost:10000',//默认会加载这个html解析里面的js动态的执行(子应用必须支持跨域)fetch container: '#vue', //容器名 activeRule: '/vue', //激活路径 // props: {a:1} //父传子参 },{ name: 'reactApp', entry:'//localhost:20000', container: '#react', activeRule: '/react' }] registerMicroApps(apps) //注册应用,可以在此加入生命周期钩子 start({ prefetch:false//取消预加载 }) new Vue({ router, store, render: h => h(App) }).$mount('#app')
-
App.vue配置
<template> <div> <el-menu :router="true" mode="horizontal"> <!--基座中可以放自己的路由--> <el-menu-item index="/">Home</el-menu-item> <!--引用其他子应用--> <el-menu-item index="/vue">vue应用</el-menu-item> <el-menu-item index="/react">react应用</el-menu-item> </el-menu> <router-view></router-view> <!-- 挂载vue应用 --> <div id="vue"></div> <!-- 挂载react应用 --> <div id="react"></div> </div> </template> <style lang="less"></style>
-
vue子级应用配置
-
main.js 配置
import Vue from "vue"; import App from "./App.vue"; import router from "./router"; import store from "./store"; Vue.config.productionTip = false; let instance = null; function render(props) { new Vue({ router, render: (h) => h(App), }).$mount("#app"); } //这里是挂载到自己的html中,基座会拿到这个挂载后的html将其插入进去 // 父级应用加载 if (window.__POWERED_BY_QIANKUN__) { //动态添加路径 __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } // 独立运行时 if (!window.__POWERED_BY_QIANKUN__) { //独立运行 render(); } //子组件协议 export async function bootstrap(props) { } export async function mount(props) { render(props); //装载 } export async function unmount(props) { if(instance){ instance.$destroy(); //卸载 } } new Vue({ router, store, render: (h) => h(App), }).$mount("#app");
-
vue.config.js 配置
module.exports = { configureWebpack: { output: { library: "vueApp", // 打包输出文件名 libraryTarget: "umd", // 打包模块类型是umd }, devServer: { port: 10000, // 父应用是通过浏览器fetch方式加载打包的lib的,需要配置请求头跨域 headers: { "Access-Control-Allow-Origin": "*", }, }, }, }; /** * umd 模块类型:挂载变量到windows中 * eg:windows.vueApp.bootstrap\mount\unmount * */
-
react子级应用配置
- src/index.js 配置
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__) { //动态添加路径 __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } if (!window.__POWERED_BY_QIANKUN__) { //独立运行 render(); } render(); //子组件协议 export async function bootstrap(props) { } export async function mount(props) { render(props); //装载 } export async function unmount(props) { ReactDOM.unmountComponentAtNode(document.getElementById("root")); //卸载 }
-
根目录创建配置文件config-overrides.js
module.exports = { webpack:(config)=>{ config.output.library = 'reactApp'; config.output.libraryTarget = 'umd'; config.output.publicPath = "http://localhost:20000/"; return config; }, devServer: (configFunction)=>{ return function (proxy,allowedHost){ const config = configFunction(proxy,allowedHost); // config.port = "20000"; config.headers ={ "Access-Control-Allow-Origin":"*" // 配置跨域 } return config } } }
-
根目录创建配置文件.env
PORT=20000 WDS_SOCKET_PORT=20000
-
配置package.json
-
下载变线依赖:
npm i -save react-app-rewired
// 修改package.json文件(注意json文件这行注释要去掉) "scripts":{ "start":"react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-app-rewired eject" }
-
-
-
注意:__webpack_public_path__有可能会报错
-
效果图:
五、qiankun 微服务通讯方式
- 未完成,敬请期待。。。。