目前前端存在的问题? 在目前所流行的主流框架 vue,react 等,他都是将属于一个单页面应用。在开发的过程中,随着业务的深入和复杂,将会带来逻辑定位问题、打包速度问题、部署上线问题。往往我们可能只是更改了一行 JS 代码,到最后发布的时候,整个项目却要整个重新打包编译发布。
公司可能之前的系统使用的是 JQ 或者其他框架进行开发,这个时候,我们想要追赶一下潮流。使用 react 或者 vue 进行开发。这个时候,我们就不得不对之前的项目使用新技术进行开发。
在如今的单页面应用里,所有的 JS 到最后都打包到一个Bundle.js
文件里,这就会导致线上用户第一次进入的时长比较长,对于前端性能统计中的FP,FCP,TTI
等一系列指标产生重大影响。虽然我们可以使用懒加载的形式去对代码进行拆分下载,但是依然会导致上述问题,因为懒加载是在路由发生变化的时候去加载的,此时此刻当你路由切换了,需要经过 DNS 解析,三次握手,然后传输,代码解析等等步骤,这其中也会耗费一些时间。
如何破局? 我们希望可以有这么一种技术或者架构:
它能够使各个子模块或者子系统进行隔离。这样我们在更新一个子模块的时候,我们只需要对这个子模块进行打包,发布上线。不会影响到其他模块。并且因为各个子系统之间相互隔离,项目就会拆分的轻量化,打包速度,前端性能等也会上去。并且因为各个子系统之前的相互隔离,这样就不会受限于技术栈的影响,你们各自系统只要能实现功能就行。
它能够使各个子系统进行数据共享。例如用户信息。
他能够对 JS,css 等进行相互隔离,防止出现污染问题。
微前端是什么? 微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于前端,即将 Web 应用由单一的单页面应用转变为多个小型前端应用聚合为一的应用。然后各个前端应用还可以独立运行、独立开发、独立部署。
微前端的方案 路由转发 当前的单页面应用的路由控制都是在前端进行。这就导致我们必须使用同一的技术栈,要不然react-dom-router
他指挥不了 vue 路由,vue-router
也指挥不了 react。这也会导致我们的项目必须在一个同一的项目里进行开发,因为跨项目的话,就算技术栈一样,A 也指挥不了 B 里面的路由跳转。所以我们可以直接将路由全都扔给后端来处理,访问不同的 url,返回不同的 html+js+css。如果要分享用户信息等,可以通过 cookie 等技术进行分享。因为每次路由匹配到的话,都会进行刷新,因此也防止了 JS,css 的污染问题
优点:
缺点:
每次跳转都相当于重新刷新了一次页面,不是页面内进行跳转。影响体验。 iframe 通过创建一个父程序,在父程序中监听路由的变化,卸载或加载相应的子程序 iframe。因每一个 iframe 就相当于一个单独的页面,所以 iframe 具有天然的 JS 和 css 隔离。在信息共享方面,我们可以使用 postMessage 或者 contentWindow 的方式进行。(我第一家公司的微前端就是这么搞的,因为是和腾讯合作的,腾讯负责做外面的壳子,我们做里面的内容,然后我们任何操作需要到外面都会用 postMessage 出去)
优点:
缺点:
web components 纯web components 开发。将每个子应用采用 web components 进行开发。纯 web-components 相当于自定义了一个 html 标签,我们就可以在任何的框架中进行使用此标签。
<template id="userInfo" > <div class ="user-box" > <p class ="user-name" > byeL</p > <p class ="user-sex" > 男</p > </div > </template>;class UserInfo extends HTMLElement { constructor ( ) { super (); var templateElem = document .getElementById('userInfo' ); var content = templateElem.content.cloneNode(true ); this .appendChild(content); } }window .customElements.define('user-info' , UserCard);
// 直接在html中使用<body > <link rel ="import" href ="./UserInfo.js" /> </body > // 在vue中使用 // a.vue // 需要在入口的main中引入userInfo<template > <user-info > </user-info > </template > // 需要在入口的main中引入userInfo // 在react中使用 class HelloMessage extends React.Component { render() { return<div > <user-info > </user-info > </div > ; } }
优点: 每个子应用拥有独立的 script 和 css,也可单独部署
缺点:需要对之前的子系统都要进行改造,并且通信方面较为复杂。
组合式应用路由分发。 每个子应用单独的打包,部署和运行。不过需要基于父应用进行路由管理。例如:有子应用 A 的路由是/testA,子应用 B 的路由是/testB,那么父应用在监听到/testA 的时候,如果此时处于/testB,那么首先会进行一个子应用 B 的卸载。完成之后,在去加载子应用 A。
优点:纯前端改造,相比于路由式,无刷新,体验感良好。
缺点:需要解决样式冲突,JS 污染问题,通信技术等。
目前主流方案–组合式应用路由分发。 css 冲突解决方案 类似于 vue 的 scoped。在打包的时候,对 css 选择器加上响应的属性,属性的 key 值是一些不重复的 hash 值,然后在选择的时候,使用属性选择器进行选择。
可以自定义前缀。在开发子模块之前,需要确定一个全局唯一的 css 前缀,然后在书写的过程中同一添加此前缀,或在根 root 上添加此前缀,使用 less 或 sass 作用域嵌套即可解。例如:
<div class ="rootA" > <span class ="rootA-span" > </span > </div > <style > .root { .rootA-span { // 书写你的css } }</style >
js 污染解决方案 js 污染解决方案主要思路:沙盒,其实就是每个子应用都有自己的运行环境。
缓存法:当我们的子页面加载到父类的基座中的时候,我们可以生成一个 map。在页面渲染之前,我们先把当前的 window 上的变量等都存储在这个 map 中。当页面卸载的时候,我们在遍历这个 map,将其数据在替换回去。 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 30 31 32 33 34 35 36 37 38 39 class Sandbox { constructor ( ) { this .cacheMy = {}; this .cacheBeforeWindow = {}; } showPage ( ) { this .cacheBeforeWindow = {}; for (const item in window ) { this .cacheBeforeWindow[item] = window [item]; } Object .keys(this .cacheMy).forEach((p ) => { window [p] = this .cacheMy[p]; }); } hidePage ( ) { for (const item in window ) { if (this .cacheBeforeWindow[item] !== window [item]) { this .cacheMy[item] = window [item]; window [item] = this .cacheBeforeWindow[item]; } } } }const diffSandbox = new Sandbox(); diffSandbox.showPage(); window .info = '我是子应用' ;console .log('页面激活,子应用对应的值' , window .info); diffSandbox.hidePage();console .log('页面卸载后,子应用的对应的值' , window .info); diffSandbox.showPage(); console .log('页面激活,子应用对应的值' , window .info);
使用代理的形式proxy
。监听 get 和 set 方法,针对当前路由进行 window 的属性或方法的存取 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 30 31 32 33 34 35 const windowMap = new Map ();const reseatWindow = {};let routerUrl = '' ;const handler = { get : function (obj, prop ) { const tempWindow = windowMap.get(routerUrl); console .log(windowMap, routerUrl); return tempWindow[prop]; }, set : function (obj, prop, value ) { if (!windowMap.has(routerUrl)) { windowMap.set(routerUrl, JSON .parse(JSON .stringify(reseatWindow))); } const tempWindow = windowMap.get(routerUrl); tempWindow[prop] = value; }, };let proxyWindow = new Proxy (reseatWindow, handler); proxyWindow.a = '我是父类的a属性的值' ; routerUrl = 'routeA' ; proxyWindow.a = '我是routerA的a属性的值' ; routerUrl = '' ;console .log(proxyWindow.a); routerUrl = 'routeA' ;console .log(proxyWindow.a);
使用single-SPA搭建微前端 准备工作 首先先用npx create-react-app xxx
分别创建app1,app2,main。
然后在main的根目录执行npm install --save react-router-dom single-spa
,
接着在app1 和 app2下执行npm run eject
。
修改一下webpack的配置,参考资料
const DEFAULT_PORT = parseInt (process.env.PORT, 10 ) || 3001 ;const HtmlWebpackPlugin = require ('html-webpack-plugin' ); output:{ library :"app1" , libraryTarget :"umd" , }
修改基座 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import { registerApplication, start} from "single-spa" function App ( ) { return ( <div className ="App" > <header className ="App-header" > <img src ={logo} className ="App-logo" alt ="logo" /> <p > <ul > // 用react-router来控制一下路由 <Link to ="/app1" > app1</Link > <Link to ="/app2" > app2</Link > </ul > </p > </header > // 子应用要挂载的容器 <div id ="container" > </div > </div > ); }export const createScript = (url ) => { return new Promise ((resolve, reject ) => { const script = document .createElement("script" ); script.src = url; script.onload = resolve; script.onerror = reject; const firstScript = document .getElementsByTagName("script" )[0 ]; firstScript.parentNode.appendChild(script, firstScript); }); };const loadApp = (url,globalVal )=> { return async ()=>{ await createScript(url +"/static/js/bundle.js" ); return window [globalVal]; } }const apps = [ { name :"app1" , app :loadApp("http://localhost:3001" ,"app1" ), activeWhen :location => location.pathname.startsWith("/app1" ), customProps :{} }, { name : "app2" , app : loadApp("http://localhost:3002" , "app2" ), activeWhen : location => location.pathname.startsWith("/app2" ), customProps : {} } ]for (let i = apps.length-1 ;i>=0 ;i--){ registerApplication(apps[i]); } start();
修改子应用 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 import singleSpaReact from 'single-spa-react' ;import App from './App' import './index.css' ;const reactLifecycles = singleSpaReact({ React, ReactDOM, domElementGetter : () => document .querySelector('#container' ), rootComponent : App });export const bootstrap =(props )=> { console .log("bootstrap" ) return reactLifecycles.bootstrap(()=> {}) }export const mount = (props )=> { console .log("mount" ); return reactLifecycles.mount(()=> {}) }export const unmount = (props )=> { console .log("unmount" ); return reactLifecycles.unmount(()=> {}) }
然后在main 和app1 下分别运行npm run start
,在main里面点一下看看是不是成了。 这样简单的一个微前端架构就搞定了。
singleSPA执行过程 参考代码
具体源码可以自行查看。大概看了一下挺容易理解的。
qiankun qiankun其实就是基于singleSPA的一套微前端实现库。如果singleSPA了解了,搞这个其实也挺容易的。
其实看他的快速上手就可以自己搭建起来。
不过有个点特别说明下。
不需要像singleSPA中cra创建的子类eject后要修改很多配置(其实singleSPA也可以不用修改这么多,只是按照他说的删掉一些文档中说不需要的东西)
config.output.library = `${name} -[name]` ; config.output.libraryTarget = 'umd' ; config.output.jsonpFunction = `webpackJsonp_${name} ` ; config.output.globalObject = 'window' ;
其余的按照qiankun中说的配置就好了。