本博客将带你理解微前端是什么?
以及讲述微前端的原理,
然后全实战带你手搓一个基础的微前端的框架
1.什么是微前端
微前端,你可以把他理解成 后端服务中的微服务。
他把一个大项目拆成不同的小项目
他允许不同项目中使用不同的技术栈开发 (React 、next.js、Vue 、Angular)
这样的好处是:
1.减少单个项目的体积,避免了巨石项目的产生
2.提高了团队的协作效率,避免了多人协同开发的项目管理混乱和冲突
3.允许不同项目采用不同的技术栈开发,增强了项目拓展性
2.微前端的组成
微前端一般用 父应用(基底) 和 一堆子应用组成
父应用负责
1.维护跨应用、跨域的通信
2.决定什么时候加载/卸载 子应用
3.维护整个微前端的框架
子应用负责
1.提供整个应用的渲染/卸载函数给父应用
2.加载自身的业务逻辑
3.微前端的基本原理
不妨思考一下,我们的一个微前端的项目打包之后输出的是什么?
一个HTML文件、若干CSS样式文件、若干JS脚本文件
其实也就是说,任何一个前端项目,最终都只是“一堆可以被浏览器加载的静态资源” 。
不管你是 Vue、React、Next.js,还是 Angular
最终放到浏览器上执行的,永远只有 HTML / CSS / JS
放一个vue项目的HTML文件, 你应该见过
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
</body>
</html>
这个HTML文件提供了什么?
在body下一个叫app的容器
然后这个Vue项目的main.js 你也一定认识
import Vue from 'vue'
import App from './App.vue'
new Vue({
render:h=>h(App)
}).$mount(#app)
什么意思?
创建一个Vue的实例,把这个App这个虚拟dom 挂载到#app这个HTML容器上
本质是上是在这个容器执行JS脚本 和加载对应的CSS样式
微前端的原理是什么?
父项目,在运行时,去加载并运行子应用打包出来的资源
和Vue项目编译一样,父应用只需要提供一个挂载点,给子应用
然后去请求子应用打包之后的JS和CSS文件 ,
然后让他挂载在这个父应用提供的挂载点上去就行
父应用并不关心子应用是用什么技术栈写的
因为打包之后都只有JS和CSS文件
父应用只需要关心
1.什么时候加载
2.加载什么资源
3.什么时候卸载
既然已经了解了基本原理,接下来就开始我们的实战,通过具体的实例,给大伙讲解微前端的构成
4.微前端实战
4.1架构组成
其实微前端有很多种方案实现,
但是都不外乎都需要以下这个几个部分
-
1.子模块注册模块
-
2.loader加载子模块
-
3.调度模块
-
4.生命周期模块
-
5.沙箱隔离模块
具体流程是
1.调用模块(决定调用哪些子模块)
↓
2.注册模块查找对应模块
↓
3.loader模块加载对应的模块
↓
4.加载之后运行在沙箱环境
↓
5.提供生命周期模块 (让父应用决定什么时候卸载)
但是我今天要介绍的方案,比较返璞归真
一般正常的架构的方案,都需要你去专门写 子模块注册模块 和 调度模块
然后还需要
-
1.重写 window.history.popstate函数,监听路由的变化,
-
2.根据URL的变化去判断是否跳转到某个子模块
我今天介绍的方案,是这样
- 1.首先本身微前端的原理,就是父应用提供一个挂载DOM,给子应用挂载
- 2.那么我需要在src/view下新建一个MicroView文件夹
- 3.MicroView文件夹下创建一个Vue组件页面
- 4.vue组件页面专门给对应的子应用提供挂载点 (寄生容器)
- 5.至于调度模块,我把这个vue组件页面注册在Router/index.js模块下
- 6.父应用完全可以根据自己什么时候切入寄生容器,而什么时候加载子应用
总的来说,就是我把注册模块和调度模块都交给了Vue/React自带的Router的去自动管理了
这样减少了很多心智负担
路由文件:
import Vue from 'vue'
import VueRouter from 'vue-router'
export const router =new VueRouter({
mode:history,
routes:[
{
path:'/app1'
component:()=> import ('@/view/MicroView/app1')
},
{
path:'/app2'
component:()=> import ('@/view/MicroView/app2')
},
]
})
Microview/app1
<template>
<div id="app1">
</div>
</template>
<script>
<script/>
我们的寄生容器,只需要提供挂载点就行了
这样我们的调度模块、注册模块全部由父应用的路由模块来自动负责
4.2 loader模块
我们已经写好了调度模块和注册模块
那么我们现在要思考的就是,怎么加载子模块的CSS和JS,让他在<div id=app>这里执行并生效
那么我们就要开始封装loader模块了
第一步我们要干什么?
1.请求子模块打包之后的HTML文件
我们封装一个loader.js
他导出一个函数,这个函数接收一个对象
const Micro={
name:app1, 1.子模块的名字
URL:'https://host:300', 2.子模块的HTML文件地址
container:app1 3.子项目在父应用挂载的目标挂载点
}
loadMicroApp函数
export async funciton loadMircoApp( {name, URL, container} ){
--- 我们第一步要做的就是
--- 请求子模块的入口文件 也就是HTML文件
const html = await fetch(URL).then((res)=> res.text() )
}
在请求到HTML文件之后,我们使用fetch封装的对象方法 text() 把HTML文件转换成字符串
1.fetch(url) → 返回一个 Response 对象
2.Response 对象本身是 流式数据,不能直接当字符串用
3..text() 是 Response 对象的方法,用来把 HTML文件转换成字符串
然后我们需要解析这个HTML入口文件,分析他有哪些CSS样式 和JS脚本
这里需要我们对HTML文件进行解析
在loader/utils.js下封装
parseHTML()
export parseHTML( MicroHTML:string ) {
1.创建DOM解析器
const parser=new DOMParser()
2.把字符串解析成DOM树(对象)
const document=parser.parseFormString( MicroHTML, 'text/html' )
3.解析出js脚本和CSS样式资源元素
const scripts=document.querySelectorAll('script[src]')
const styles= document.querySelectorAll('link[rel="stylesheet"]' )
4.取出JS和CSS资源的URL地址
const ScriptsUrl = Array.from(scripts).map((item)=>item.src )
const StylesUrl = Array.from(styles).map((item)=> item.herf )
5.返回JS脚本 和CSS 样式的地址
return {
ScriptsUrl,
StylesUrl
}
}
然后在loadMicroApp()中使用
export async funciton loadMircoApp( {name, URL, container} ){
--- 我们第一步要做的就是
--- 1.请求子模块的入口文件 也就是HTML文件
const html = await fetch(URL).then((res)=> res.text() )
--- 2.解析JS 和CSS资源
const { scriptsUrl, stylesUrl } = parserHTML(html)
--- 3.根据Url资源地址去请求对应的 CSS和JS资源
--- 4.请求CSS资源并加载
await loaderStyle(stylesUrl) -------接下来就是封装这个函数
}
4.2.1 加载CSS资源
在utils.js中封装loaderStyle()
这个函数负责请求styles资源,并加载
export function loaderStyle(Urls){
1.请求全部的styles资源
const styles=await Promise.all(
全部转换成string文本
Urls.map((item)=> fetch.get(item) .then((res) => res.text() ) )
)
这里得到的styles是一个数组,其中每一个元素,都是一个CSS文件转换成的长string
2.我们把多个CSS文件 合并成一个文件CSS文件 通过换行符分割
const StylesAll = styles.join('\n')
3.创建一个style容器来存放全部的CSS文件
const stylesElement = document.createElement('style')
4.把CSS文件放入容器
stylesElement.textContent = StytlesAll
5.把这个元素加入到<head>中让样式生效
document.head.appendChild(stylesElement)
}
但是你发现问题没有?
子应用的样式如果就这样加入到全局样式
会污染全局样式
假如 ,父应用 和子应用 之间存在两个一模一样的类名? 或者说 两个子应用存在一模一样的CSS类名
那么后面的最后进来的类,会覆盖前面的类
造成全局的样式污染
所以我们需要进行样式隔离
样式隔离最简单的方式就是,子应用全部的类
都加上子应用的app挂载的目标容器的类
比如说
<template>
<div id="app1"> </div>
</template>
<script> <script/>
子应用要挂载到 id=app1的div上
那么我们只需要给全部的子应用的类,在前面加上 #app1 就行了
这件事在哪里做呢?
总不能在写子应用的时候,全部的类都加上 #app1吧
我们只要需要在父应用加载子应用的CSS文件的时候
用正则去匹配CSS文件,给全部的类加上 #app1就行
4.2.2 CSS样式隔离
在loader/utils.js 下面封装CSS隔离函数
CSSisolation()
函数参数: CSS样式 Styles 、 子应用的挂载点container
export function CSSisolation( Styles, container ){
1.匹配全部 不以{}开头,但是以 } 结尾字符串, 以此捕获全部的类名
return Styles.replace.(/([^ {}] +) \{ /g) , (match,Selector) =>{
2.跳过全部的@开头的
if(Selector.trim().startsWith('@') ){
return match ---- 直接return,不做修改
}
3.处理并列选择器
const ScopedStylesArr = Selector.split(',').map((item)=>{
let sel = item.trim()
4.不允许子应用的样式 影响全局样式
if( sel==='body' || sel==='html' || sel=== 'app' ){
return '${container}' --- 只允许修改最大的父类样式是container挂载点
}
5.给全部的类加上container
if(! sel.startsWith(container) ){
return '${container} ${sel}'
}
6.都没有匹配的直接返回
return sel
})
7.拼回成字符串
const ScopedStyles = ScopedStylesArr.join(',' )
return '${ScopedStyles} {'
}
}
然后使用这个函数
在loader/index.js: loadMicroApp()中
export async funciton loadMircoApp( {name, URL, container} ){
--- 我们第一步要做的就是
--- 1.请求子模块的入口文件 也就是HTML文件
const html = await fetch(URL).then((res)=> res.text() )
--- 2.解析JS 和CSS资源
const { scriptsUrl, stylesUrl } = parserHTML(html)
--- 3.根据Url资源地址去请求对应的 CSS和JS资源
--- 4.请求CSS资源并加载
await loaderStyle(stylesUrl, container) ----------- 传递container目标挂载点
}
在loader/utils.js :loaderStyle()
export function loaderStyle(Urls){
1.请求全部的styles资源
const styles=await Promise.all(
全部转换成string文本
Urls.map((item)=> fetch.get(item) .then((res) => res.text() ) )
)
这里得到的styles是一个数组,其中每一个元素,都是一个CSS文件转换成的长string
2.我们把多个CSS文件 合并成一个文件CSS文件 通过换行符分割
const StylesAll = styles.join('\n')
3.然后把这个CSS文件进行隔离处理
const ScopedStyles =CSSisolation(StylesALL) --------------------------------新增部分
4.创建一个style容器来存放全部的CSS文件
const stylesElement = document.createElement('style')
5.把CSS文件放入容器
stylesElement.textContent = ScopedStyles
6.把这个元素加入到<head>中让样式生效
document.head.appendChild(stylesElement)
}
但是现在又有了一个新的问题,
写完了加载逻辑,我们需要写卸载逻辑
什么时候卸载呢? 当子应用的生命周期结束,父应用切到其他子应用的时候
当前子应用的CSS样式应该被卸载 ,这样不会污染全局 和 其他子模块的样式
4.2.3 CSS样式卸载
卸载的逻辑其实非常简单,我们只需要在loaderStyles()函数
给StylesElement加多一个标记就行了
loader/utils.js :loaderStyle()
export function loaderStyle(Urls){
1.请求全部的styles资源
const styles=await Promise.all(
全部转换成string文本
Urls.map((item)=> fetch.get(item) .then((res) => res.text() ) )
)
这里得到的styles是一个数组,其中每一个元素,都是一个CSS文件转换成的长string
2.我们把多个CSS文件 合并成一个文件CSS文件 通过换行符分割
const StylesAll = styles.join('\n')
3.然后把这个CSS文件进行隔离处理
const ScopedStyles =CSSisolation(StylesALL)
4.创建一个style容器来存放全部的CSS文件
const stylesElement = document.createElement('style')
5.给这个容器加上一个标记元素
StylesElement.setAttribute('data-container',container) ---------------标记这个元素
6.把CSS文件放入容器
stylesElement.textContent = ScopedStyles
7.把这个元素加入到<head>中让样式生效
document.head.appendChild(stylesElement)
}
接下来我们可以卸载CSS样式函数了
export clearStyles(container){
1.找到这个目标元素
const stylesElement=document.querySelectorAll('style[data-container=" + 'container' + "]')
2.把这个元素从head上移除
if(styleElements && styleElements.length > 0){
styleElements.forEach((item) => item.remove())
}
}
这个函数的调用时机是,子应用被卸载的时候调用
也就是子应用的寄生容器页面被卸载的时候
<template>
<div id="app1"> </div>
</template>
<script>
unmount:(()=>{
clearStyles('#app1')
})
<script/>
4.2.4 JS沙箱隔离
接下来就是到我们,微前端架构最难的部分: JS沙箱隔离
这个时候你可能会疑惑,写完了CSS的加载逻辑、隔离逻辑、卸载逻辑
我们不是应该开始写JS的加载逻辑吗?
但在真正进入 JS 加载之前,我们必须先解决一个更基础、也更关键的问题:
如何保证子应用的 JS 在运行时,不会影响宿主应用和其他子应用?
JavaScript 没有天然的作用域边界,一旦多个子应用同时运行,就必然会互相污染。
我们需要隔离他们的运行环境,让每一个子应用都拥有一个window
但是不真正污染全局window
所以在真正 加载JS 和执行JS 之前, 我们需要先做的是搭建一个运行环境给他们
新建一个sandbox文件夹
下面建立一个proxy.js
export funciton createProxy(){
1.创建虚拟的全局环境
const FakeWindow = Object.create(null) 这里只是一个空对象
2.开启代理
const proxy = new Proxy(FakeWindo ,{
3.读取属性
get(target,key){
4.如果是全局属性,直接返回我们的 FakeWindow
if(key==="window" || key === "self" || key==="globalThis"){
return Proxy
}
5.如果是普通的属性 优先从FakeWind上读取,如果没有才去真正的全局window上取
const value =(key in target )? target[key] : window[key]
return value
}
6.写入属性
set(target,key,value){
7.只允许写入在FakeWindow上,不允许污染真正的全局
target[key]=value
return true
}
})
}
写完这个基础的沙箱隔离,搭建完成这个JS的运行环境,那么我们可以加载的JS的逻辑了
4.2.5 JS逻辑加载
然后在loadMicroApp()中使用
export async funciton loadMircoApp( {name, URL, container} ){
--- 我们第一步要做的就是
--- 1.请求子模块的入口文件 也就是HTML文件
const html = await fetch(URL).then((res)=> res.text() )
--- 2.解析JS 和CSS资源
const { scriptsUrl, stylesUrl } = parserHTML(html)
--- 3.根据Url资源地址去请求对应的 CSS和JS资源
--- 4.请求CSS资源并加载
await loaderStyle(stylesUrl)
--- 5.创建沙箱环境
const proxy = createProxy() -------------运行环境
--- 6.请求并加载JS
await loaderScript(scriptUrl,proxy) ------------ 加载运行
}
在loader/utils.js下封装 loaderScripts()
export function fetchScripts(Urls,proxy){
1.请求JS文件资源,并转换成string文本
const JScode = await Promise.all(
Url.map( (item)=> fetch(item).then( (res)=> res.text() ) )
)
2.串行执行,JS脚本的加载是要保证先后顺序的
for( const code of JScode ){
await execScript( code ,proxy ) -------封装的执行函数
}
}
export execScript(code, proxy ){
1.通过new Funciton来把String文本作为JS执行
const runner= new Function ('window','self','globalThis',code)
2.立即执行
runner(proxy,proxy,proxy)
}
4.2.6 JS副作用清除
接下来是什么呢?
你可以应该很自然的想到,当子应用卸载的时候
我们需要清除加载JS遗留下来的副作用
比如说,全局监听/定时器/计时器
原理很简单
当子应用全创建局事件、定时器、观察者等副作用,
在创建的那一刻,最终一定会落到的 window上的
那么实际上,他们就会去访问我们封装好了的FakeWindow 这个代理对象
那么我们只需要在他们创建这些事件的时候,记录下这些事件的Id
然后当寄生页面销毁的时候 ,根据这些id去销毁这些监听事件
sandbox/proxy.js:
createProxy()
export funciton createProxy(){
const timers=new Set() ---------------- 定时器id记录表
const intervals= new Set() -------------计时器id记录表
const eventlisteners= new Set() --------事件监听id记录表
const observers=new Set()---------------观察事件id记录表
接下来我们使用装饰器重写这些方法 核心就是在每次调用的时候,都记录一下id
重写setTimeout
funciton WrapSetTimeout(fn,delay,...args){
const id=window.setTimeout(fn,delay,...args)
timers.add(id) 记录这个id
return id
}
重写intervals
function WrapSetInterval(fn,delay,...agrs){
const id= window.setInerval(fn,delay,...args)
intervals.add(id) 记录
return id
}
重写addEventListener
function WrapAddEventListener(type,litstener,options){
eventlinsteners.add({ 记录事件
target:window,
type,
listener,
options
})
开启监听
window.addEventListener(type,listener,options)
}
重写removeEventListener
function WrapRemoveEventListener(type,listener,options){
//遍历查找
for( item of eventListeners){
if(item.type===type && item.listener===listener ){
eventlistener.delete(item)
break
}
}
window.removeEventListener(type,listener,options)
}
重写observe
const RawMutationsObserver= window.MutationObserver
function ProxyMuatationObeserver(callback){
创建实例
const instance new RawMutationsObserver(callback)
劫持实例的 disconnect 方法
const originDisconnect = instance.disconnect
instance.disconnect = function() {
observers.delete(instance); -------------- 开发者手动销毁时,从账本移除
return originDisconnect.call(this)
}
记录实例
observers.add(instance)
return instance
}
创建映射表
const EffectFunction ={
'setTimeout':WrapSetTimeout,
'setInterval':WrapSetInterval,
'addEventListener':WrapAddEventListener,
'removeEventListener':WrapRemoveEventListener,
'MutationObserver':ProxyMuatationObeserver
}
1.创建虚拟的全局环境
const FakeWindow = Object.create(null) 这里只是一个空对象
2.开启代理
const proxy = new Proxy(FakeWindo ,{
3.读取属性
get(target,key){
4.如果是全局属性,直接返回我们的 FakeWindow
if(key==="window" || key === "self" || key==="globalThis"){
return proxy
}
5.劫持全部的副作用函数,return重写的函数
if(key in EffectFunction){
return EffectFunction[key]
}
6.如果是普通的属性 优先从FakeWind上读取,如果没有才去真正的全局window上取
const value =(key in target )? target[key] : window[key]
return value
}
7.写入属性
set(target,key,value){
8.只允许写入在FakeWindow上,不允许污染真正的全局
target[key]=value
return true
}
})
8.清除副作用函数
function clearEffect(){
清除定时器
timers.forEach(id=> window.clearTimeout(id))
timers.clear() -----清空记录表
清除监听事件
eventListeners.forEach(({target,type,listener,options})=>{
target.removeEventListener(type,listener,options)
})
eventlisteners.clear() -----清空记录表
清空观察者
observers.forEach(observer => {
if (observer.disconnect) {
observer.disconnect()
}
})
observers.clear()-----------------清除观察者集合
清空时间间隔
intervals.forEach(id=>window.clearInterval(id))
intervals.clear()-----------------------------------------清除定时器ID集合
}
return {
proxy,
clearEffects() ------------把函数导出
}
}
然后在loader/index.js中
loadMicroApp()中使用
export async funciton loadMircoApp( {name, URL, container} ){
--- 我们第一步要做的就是
--- 1.请求子模块的入口文件 也就是HTML文件
const html = await fetch(URL).then((res)=> res.text() )
--- 2.解析JS 和CSS资源
const { scriptsUrl, stylesUrl } = parserHTML(html)
--- 3.根据Url资源地址去请求对应的 CSS和JS资源
--- 4.请求CSS资源并加载
await loaderStyle(stylesUrl)
--- 5.创建沙箱环境
const {proxy,clearEffect } = createProxy() -------------得到清除函数
--- 6.请求并加载JS
await loaderScript(scriptUrl,proxy)
}
4.2.7 子应用提供mount/unmount函数
这里以Vue文件为例
告诉大家怎么让子应用为父应用提供mount函数
这样能让父项目决定什么时候挂载/卸载 子模块
在loader/index.js中
loadMicroApp()中使用
export async funciton loadMircoApp( {name, URL, container} ){
--- 我们第一步要做的就是
--- 1.请求子模块的入口文件 也就是HTML文件
const html = await fetch(URL).then((res)=> res.text() )
--- 2.解析JS 和CSS资源
const { scriptsUrl, stylesUrl } = parserHTML(html)
--- 3.根据Url资源地址去请求对应的 CSS和JS资源
--- 4.请求CSS资源并加载
await loaderStyle(stylesUrl)
--- 5.创建沙箱环境
const {proxy,clearEffect } = createProxy()
--- 6.请求并加载JS
await loaderScript(scriptUrl,proxy)
--- 7.注入环境变量
proxy.__MICRO_FRONTEND__ = true ---------------------告诉子应用当前是在微前端环境中运行的
}
Vue2/main.js
import Vue from 'vue'
import App from './App.vue'
子应用实例
let appInstance =null
判断当前是什么环境
const isMicroFrontend = !!window.__MICRO_FRONTEND__
判断环境
if(!isMicroFrontend){
如果当前不是微前端环境,直接正常启动,挂到自己项目的HTMML的根节点
appInstance=new Vue({
render:h=>h(App)
})
appInstance.$mount(#app)
}
如果是微前端环境,提供函数
export async function microMount(container){ --------container是父应用传递进来的挂载点
创建实例
appInstance=new Vue({
render:h=>h(App)
})
appInstance.$mount(container) ---------------挂载到目标挂载点
}
export async function microUnmount(){
if(appInstance){
appInstance.$destroy()--------------销毁
appInstance=null ---------------------消除实例
}
或者更暴力稳妥的写法
这里通过parentNode节点移除组件挂载
if (appInstance.$el && appInstance.$el.parentNode) {
手动移除DOM,因为Vue2 mount会替换挂载点,导致父应用失去对DOM的引用
appInstance.$el.parentNode.removeChild(appInstance.$el)
}
}
子应用提供这两个函数,什么时候真正发生挂载,由父应用决定
然后把子模块的打包方式设置成UMD模式 (非常重要)
Webpack 会自动把这些 export 的函数 挂载到了 window[name] 上
这样我们父应用就能从代理window, 也就是proxy中拿到, 子应用的mount/unmount函数
至于什么是UMD打包模式,怎么设置UMD打包模式
这里就不多说了,你自己去了解就行
在loader/index.js中
loadMicroApp()中使用
export async funciton loadMircoApp( {name, URL, container} ){
--- 我们第一步要做的就是
--- 1.请求子模块的入口文件 也就是HTML文件
const html = await fetch(URL).then((res)=> res.text() )
--- 2.解析JS 和CSS资源
const { scriptsUrl, stylesUrl } = parserHTML(html)
--- 3.根据Url资源地址去请求对应的 CSS和JS资源
--- 4.请求CSS资源并加载
await loaderStyle(stylesUrl)
--- 5.创建沙箱环境
const {proxy,clearEffect } = createProxy()
--- 6.请求并加载JS
await loaderScript(scriptUrl,proxy)
--- 7.注入环境变量
proxy.__MICRO_FRONTEND__ = true
--- 8.拿到子应用挂载到proxy上的生命周期函数
const app =proxy[name] -------------如果没有开启umd打包模式,这里是拿不到的
---- 9.做一个判断
if(!app){
throw new Error(`子应用${name}导出的生命周期函数不存在`)
}
---- 10.调用子应用导出的生命周期函数 挂载到指定的容器
app.mount(container)
return {
----11. 返回一个卸载清除函数函数
MicroUnmount:()=>{
----12. 卸载子应用
app.unmount()
----13.清除CSS样式
clearStyles()
----14.清除JS副作用
clearEffect()
}
}
}
最终我们在寄生页面的使用
<template>
<div id="app1"> </div>
</template>
<script>
import loadMircoApp from '@/Micro/loader/index.js' --------导入加载流程函数
import { onMounted, onUnmounted } from 'vue' ------组件页面的渲染/卸载函数
let Instance =null
1.当这个页面被路由跳转进来开始加载的时候
onMounted( async() =>{
执行挂载逻辑
const microApp= await loadMircoApp({
name:app1,
URL:'http://test.com',
container:"#app1"
})
Instance = microApp
})
2.当父应用离开这个页面 页面会自动销毁
onUnmounted(()=>{
Instance.MicroUnmount() --------执行全部的清除函数
})
<script/>
4.2.8 生命周期函数管理
更加标准的做法是把把生命周期的管理抽离出来,不放在loader模块
大概类似这样子
import {clearStyle} from '../loader/utils'
----- 1.挂载子应用
export function mountApp(app, container) {
传递对象类型的props,包含container属性
app.mount({ container })
}
----- 2.卸载子应用
export function unmountApp(app,clearEffects,container) {
卸载子应用时,指定容器
try {
app.unmount(container)
} catch (error) {
console.warn('子应用卸载失败',error)
}
清除JS副作用
try {
clearEffects()
} catch (error) {
console.warn('清除JS副作用失败',error)
}
清除CSS样式
try {
clearStyle(container)
} catch (error) {
console.warn('清除CSS样式失败',error)
}
}
5.总结
本博客的实战部分就到此结束,基本上是带你手搓了一个简单基本的微前端架构
不过实际上的微前端架构考虑的东西还要更复杂一些
我在搭建自己的博客项目的时候 ,就使用自己搭建的架构
实际的生产中还是会遇到很多问题的
比如说
1.window部分函数对象要求this必须指向真正的window
你在get的时候,没办法用proxy代理,你必须返回真正的window指针
解法方法类似于
const value = (key in target) ? target[key] : window[key];
if(typeof value==='function' && /^[a-z]/.test(key)){
简单的判断:如果函数没有 prototype 属性(箭头函数),或者 key 是构造函数名
更好的方式:只对 window 上的原生方法 bind,避免 bind 用户定义的函数
if (value.toString().indexOf('[native code]') !== -1 && !key.startsWith('webkit')) {
try {
return value.bind(window)
} catch (e) {
bind 失败(比如是不可 bind 的函数),直接返回原值
return value
}
}
return value
2.部分属性必须挂载到真实的window上
set(target,key,value){
允许部分子模块逃逸出沙箱
if(key.startsWith('webpackHotUpdate')){
window[key]=value 白名单:将webpack热更新函数挂载到真实window上
return true
}
target[key]=value
return true
},
3.window的属性不单单只有 set、get、has
-
ownKeys -
getOwnPropertyDescriptor -
defineProperty -
deleteProperty
这些也要专门写隔离
不过讲到这里你估计也大概明白了微前端的基本框架和原理
基本入门之后,你就可以自己深入的去探索
如果希望更好的隔离的效果,可以参考qiankun框架的快照沙箱
我觉得是比单纯的代理window强的,无论是隔离还是拓展性
另外本文并没有教你怎么搭建微前端中的跨域通信
大概常见的几个方案有
1.props 注入
2.全局状态(store)
3.发布订阅(Event Bus)
4.URL / 路由通信
重点了解 2方案和3方案 即可
我认为在入门之后只要去对这些做了解,应该是能很轻松地搭建通信机制出来的
所以本文就不多做介绍了
本博客中没有去写 调度模块 和注册模块,
而且都把他交给了vue/React中天然的注册模块和调度模块--------Router路由机制
如果你想要一个完整的,不依赖router的路由模块的微前端框架
你可以尝试在我给出的示例上做改写,只需要增加调度模块和注册模块即可
最后,如果你觉得博客写的还行,对你学习微前端有帮助的话,麻烦给个点赞🐔