当项目过多时,往往需要把一个项目内嵌到另外一个项目中,也就会用到微前端。之前一般会用qiankun,但现在有了另一种选择-Micro-App。
微前端的概念是由ThoughtWorks在2016年提出的,它借鉴了微服务的架构理念,核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用融合为一个完整的应用,或者将原本运行已久、没有关联的几个应用融合为一个应用。微前端既可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活。
京东的微前端框架 MicroApp,借鉴了WebComponent的思想,通过js沙箱、样式隔离、元素隔离、路由隔离模拟实现了ShadowDom的隔离特性,并结合CustomElement将微前端封装成一个类WebComponent组件,从而实现微前端的组件化渲染,旨在降低上手难度、提升工作效率。
MicroApp和技术栈无关,也不和业务绑定,可以用于任何前端框架。
零、优势
1、使用简单
我们将所有功能都封装到一个类WebComponent组件中,从而实现在基座应用中嵌入一行代码即可渲染一个微前端应用。2、功能强大
MicroApp提供了js沙箱、样式隔离、元素隔离、路由隔离、预加载、数据通信等一系列完善的功能。3、兼容所有框架
为了保证各个业务之间独立开发、独立部署的能力,micro-app做了诸多兼容,在任何前端框架中都可以正常运行。
一、快速开始
主应用:
1、安装依赖
1 | npm i @micro-zoe/micro-app --save |
2、初始化micro-app
1 | // main.js |
3、加载子应用(name必传且不能重复)
1 | <micro-app name='my-app' url='http://localhost:3000/'></micro-app> |
子应用:
micro-app从主应用通过fetch加载子应用的静态资源,由于主应用与子应用的域名不一定相同,所以子应用需要支持跨域。
1 | location / { |
到此,最简单的micro-app就集成了。
二、配置项
开发一个Hello World浏览器插件很简单,只需要一个manifest.json配置文件,和一个popup.html文件即可。开发使用html.css.js,跟书写页面一样简单。
1 | name:应用名称(不能重复,name变化时会重新渲染) |
三、生命周期
生命周期
- created: 标签初始化后,加载资源前触发。
- beforemount: 加载资源完成后,开始渲染之前触发。
- mounted: 子应用渲染结束后触发。
- unmount: 子应用卸载时触发。
- error: 子应用加载出错时触发,只有会导致渲染终止的错误才会触发此生命周期。
监听生命周期
1 | <micro-app name='xx' url='xx' @created='created' @beforemount='beforemount' @mounted='mounted' @unmount='unmount' @error='error' /> |
全局事件
在子应用的加载过程中,micro-app会向子应用发送一系列事件,包括渲染、卸载等事件。
渲染事件:
1
2
3window.onmount = (data) => {
console.log('子应用已经渲染', data)
}卸载事件:
1
2
3
4
5
6
7window.onunmount = () => {
console.log('子应用已经卸载') // 执行卸载相关操作
}
window.addEventListener('onmount|unmount', function () {
console.log('子应用已经卸载') // 执行卸载相关操作
})
四、环境变量
- MICRO_APP_ENVIRONMENT
在子应用中通过 window.MICRO_APP_ENVIRONMENT 判断是否在微前端环境中
- MICRO_APP_NAME
在子应用中通过 window.MICRO_APP_NAME 获取应用的name值
- MICRO_APP_PUBLIC_PATH
用于设置webpack动态public-path,将子应用的静态资源补全为 http 开头的绝对地址
在子应用src目录下创建名称为public-path.js
1
2
3if (window.__MICRO_APP_ENVIRONMENT__) {
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}在子应用的入口文件的最顶部引入public-path.js
1
import './public-path'
- MICRO_APP_BASE_ROUTE
子应用的基础路径
- MICRO_APP_BASE_APPLICATION
判断当前应用是否是主应用 (在执行microApp.start()后此值才会生效)
- rawWindow
子应用里获取真实window(即主应用window)
- rawDocument
子应用里获取真实document(即主应用document)
五、JS沙箱
JS沙箱通过自定义的window、document拦截子应用的JS操作,实现一个相对独立的运行空间,避免全局变量污染,让每个子应用都拥有一个相对纯净的运行环境。
micro-app有两种沙箱模式:with沙箱和iframe沙箱,它们覆盖不同的使用场景且可以随意切换,默认情况下使用with沙箱,如果无法正常运行可以切换到iframe沙箱。
在沙箱环境中,顶层变量不会泄漏为全局变量。
例如:
在正常情况下,通过 var name 或 function name () {} 定义的顶层变量会泄漏为全局变量,通过window.name或name就可以全局访问,但是在沙箱环境下这些顶层变量无法泄漏为全局变量,window.name或name的值为undefined,导致出现问题。
六、虚拟路由系统
MicroApp通过拦截浏览器路由事件以及自定义的location、history,实现了一套虚拟路由系统,子应用运行在这套虚拟路由系统中,和主应用的路由进行隔离,避免相互影响。
路由模式
有5种模式,search、native、native-scope、pure、state;可以通过配置disable-memory-router:true 关闭;keep-router-state可保留子应用路由状态;
- search:默认模式,通常不需要特意设置,search模式下子应用的路由信息会作为query参数同步到浏览器地址上
- native:模式是指放开路由隔离,子应用和主应用共同基于浏览器路由进行渲染,它拥有更加直观和友好的路由体验
- native-scope:模式的功能和用法和native模式一样,唯一不同点在于native-scope模式下子应用的域名指向自身而非主应用
- pure:模式是指子应用独立于浏览器路由系统进行渲染,即不修改浏览器地址,也不增加路由堆栈,pure模式下的子应用更像是一个组件。
- state:模式是指基于浏览器history.state进行渲染的路由模式,在不修改浏览器地址的情况下模拟路由行为,相比其它路由模式更加简洁优雅,表现和iframe路由系统类似。
导航
主应用控制子应用跳转
1 | microApp.router.push|replace({name: 'my-app', path: 'http://localhost:3000/page1?id=9527'}) |
子应用控制主应用跳转(子应用无法直接控制主应用的跳转)
1 | 1、主应用注册:microApp.router.setBaseAppRouter(主应用的路由对象) |
子应用控制其它子应用跳转
1 | window.microApp.router.push|replace({name: 'my-app2', path: 'http://localhost:3000/page1'}) |
设置默认页面
子应用默认渲染首页,但可以通过设置defaultPage渲染指定的默认页面。
方式一:通过default-page属性设置
1
<micro-app default-page='页面地址'></micro-app>
方式二:通过router API设置
1
microApp.router.setDefaultPage({ name: '子应用名称', path: '页面地址' })
导航守卫
导航守卫用于监听子应用的路由变化,类似于vue-router的全局守卫,不同点是MicroApp的导航守卫无法取消跳转。
1 | microApp.router.beforeEach((to, from, appName) => { |
获取路由信息
主应用获取子应用的路由信息:
1 | microApp.router.current.get('my-app') |
子应用获取子应用的路由信息:
1 | window.microApp.router.current.get('my-app2') |
同步路由信息
在一些特殊情况下,主应用的跳转会导致浏览器地址上子应用信息丢失,此时可以主动调用方法,将子应用的路由信息同步到浏览器地址上。
指定子应用:
1 | microApp.router.attachToURL('my-app') |
所有正在运行的子应用:
microApp.router.attachAllToURL()
1 | ``` |
所有正在运行的子应用,包含预渲染应用:
1 | microApp.router.attachAllToURL({ includePreRender: true }) |
七、样式隔离、元素隔离
样式隔离
MicroApp的样式隔离是默认开启的,开启后会以
1、在所有子应用中禁用
1
2
3microApp.start({
disableScopecss: true, // 默认值false
})2、单个子应用中禁用
1
<micro-app name='xx' url='xx' disableScopecss='false'></micro-app>
3、在某一个css文件中禁用(可禁用单行 或 整个文件)
1
2
3/*! scopecss-disable */
.test { color: red }
/*! scopecss-enable */
元素隔离
元素隔离的概念来自ShadowDom,即ShadowDom中的元素可以和外部的元素重复但不会冲突,micro-app模拟实现了类似ShadowDom的功能,元素不会逃离
子应用中不能获取主应用中的元素,但主应用可以获取子应用中的。
八、数据通信
micro-app提供了一套灵活的数据通信机制,方便主应用和子应用之间的数据传输。
主应用和子应用之间的通信是绑定的,主应用只能向指定的子应用发送数据,子应用只能向主应用发送数据,这种方式可以有效的避免数据污染,防止多个子应用之间相互影响。
同时我们也提供了全局通信,方便跨应用之间的数据通信。
一、子应用获取来自主应用的数据
直接获取:
1 | window.microApp.getData() |
监听变化:
1 | window.microApp.addDataListener(dataListener: (data: Object) => any, autoTrigger?: boolean) |
二、子应用向主应用发送数据
子:
1 | window.microApp.dispatch({type: '子应用发送给主应用的数据'}, (data) => {}) |
主:
1 | microApp.addDataListener('my-app', (data) => { |
forceDispatch: dispatch方法会缓存每次发送的值,然后合并发给主应用,如果相同则不发送,强制发送可以使用forceDispatch
三、主应用向子应用发送数据
方式1: 通过data属性发送数据(data只接受对象类型,数据变化时会重新发送)
1
<micro-app name='my-app' url='xx' :data='dataForChild' />
方式2: 手动发送数据
1
microApp.setData('my-app', {type: '新的数据'}) // 强制发送用forceSetData
四、主应用获取来自子应用的数据
方式1:直接获取数据
1
microApp.getData(appName) // 返回子应用的data数据
方式2: 监听自定义事件 (datachange) // 数据在事件对象的detail.data字段中,子应用每次发送数据都会触发datachange
1
<micro-app name='my-app' url='xx' @datachange='handleDataChange' />
方式3: 绑定监听函数
1
microApp.addDataListener(appName: string, dataListener: (data: Object) => any, autoTrigger?: boolean)
五、清空数据
由于通信的数据会被缓存,即便子应用被卸载也不会清空,这可能会导致一些困扰,此时可以主动清空缓存数据来解决。
主应用:
方式一:配置项 - clear-data(子应用卸载时会同时清空主应用发送给当前子应用,和当前子应用发送给主应用的数据)
1
<micro-app clear-data></micro-app>
方式二:手动清空 - clearData
1
microApp.clearData('my-app')
子应用:
1 | window.microApp.clearData() |
全局数据通信
全局数据通信会向主应用和所有子应用发送数据,在跨应用通信的场景中适用。
主应用:
1 | microApp.setGlobalData({type: '全局数据'}, () => {}) // 发送全局数据 |
子应用: 跟主应用方法一样,只是把microApp改成 window.microApp
九、资源系统
一、资源路径自动补全
针对资源link、script、img、background-image、font-face; 如:子应用中引用图片/myapp/test.png,最终渲染时会补全为 ${子应用域名}/myapp/test.png
publicPath:如果自动补全失败,可以采用运行时publicPath方案解决(见第四章环境变量里 MICRO_APP_PUBLIC_PATH )
二、资源共享
当多个子应用拥有相同的js或css资源,可以指定这些资源在多个子应用之间共享,在子应用加载时直接从缓存中提取数据,从而提高渲染效率和性能。
方式一:globalAssets
1
2
3
4
5
6microApp.start({
globalAssets: {
js: ['js地址1', 'js地址2', ...], // js地址
css: ['css地址1', 'css地址2', ...], // css地址
}
})方式二:global 属性
1
2<link rel="stylesheet" href="xx.css" global>
<script src="xx.js" global></script>
三、资源过滤
方式一:excludeAssetFilter
1
2
3
4
5
6
7
8microApp.start({
excludeAssetFilter (assetUrl) {
if (assetUrl === 'xxx') {
return true // 返回true则micro-app不会劫持处理当前文件
}
return false
}
})方式二:配置 exclude 属性
1
2
3<link rel="stylesheet" href="xx.css" exclude>
<script src="xx.js" exclude></script>
<style exclude></style>
十、预加载
预加载是指在子应用尚未渲染时提前加载静态资源,从而提升子应用的首次渲染速度。
为了不影响主应用的性能,预加载会在浏览器空闲时间执行。
1 | microApp.preFetch(apps: app[] | () => app[], delay?: number) |
方式一:设置数组
1
2
3microApp.preFetch([
{ name: 'my-app4', url: 'xxx', level: 1, 'default-page': '/page2' }
])方式二:设置一个返回数组的函数
1
2
3microApp.preFetch(() => [
{ name: 'my-app4', url: 'xxx', level: 1, 'default-page': '/page2' }
])方式三:在start中设置预加载数组
1
2
3
4
5microApp.start({
preFetchApps: [
{ name: 'my-app4', url: 'xxx', level: 1, 'default-page': '/page2' }
]
})方式四:在start中设置一个返回预加载数组的函数
1
2
3
4
5microApp.start({
preFetchApps: () => [
{ name: 'my-app4', url: 'xxx', level: 1, 'default-page': '/page2' }
]
})
此外还可以全局设置预加载的延迟和level:
1 | microApp.start({ |
十一、umd模式
MicroApp支持两种渲染微前端的模式,默认模式和umd模式,推荐使用umd模式。
- 默认模式:子应用在初次渲染和后续渲染时会顺序执行所有js,以保证多次渲染的一致性。
- umd模式:子应用暴露出mount、unmount方法,此时只在初次渲染时执行所有js,后续渲染只会执行这两个方法,在多次渲染时具有更好的性能和内存表现。
开启umd方式(修改子应用的main.js)
1 | let app = null |
十二、keep-alive
在应用之间切换时,我们有时会想保留这些应用的状态,以便恢复用户的操作行为和提升重复渲染的性能,此时开启keep-alive模式可以达到这样的效果。
开启keep-alive后,应用卸载时不会销毁,而是推入后台运行。
1 | <micro-app name='xx' url='xx' keep-alive></micro-app> |
生命周期(created、beforemount、mounted、error、afterhidden、beforeshow、aftershow)
- 1、不会触发unmount
- 2、beforemount、mounted只在初始化的时候执行一次
- 3、多beforeshow、aftershow、afterhidden三个周期
子应用
keep-alive模式下,在子应用卸载、重新渲染时,micro-app都会向子应用发送名为appstate-change的自定义事件,子应用可以通过监听该事件获取当前状态,状态值可以通过事件对象属性e.detail.appState获取。
e.detail.appState的值有三个:afterhidden、beforeshow、aftershow,分别对应卸载、即将渲染、已经渲染。
应用初始化时不会触发appstate-change事件。
1 | window.addEventListener('appstate-change', function (e) { |
十三、多层嵌套、插件系统
多层嵌套
micro-app支持多层嵌套,即子应用可以嵌入其它子应用,但需要做一些修改(如A套B,B套C,则需要改B)。
1、修改tagName
1
2
3
4
5
6
7microApp.start({
// 必须是以`micro-app-`开头的小写字母,例如:micro-app-b、micro-app-b-c
tagName: 'micro-app-xxx'
})
// 将micro-app 换成 micro-app-xxx
<micro-app-xxx name='...' url='...'></micro-app-xxx>2、将B改成umd模式
参照章节十一
注:
1、无论嵌套多少层,name都要保证全局唯一
2、确保micro-app的版本一致,不同版本可能会导致冲突
插件系统
插件系统的主要作用就是对js进行修改,每一个js文件都会经过插件系统,我们可以对这些js进行拦截和处理,它通常用于修复js中的错误或向子应用注入一些全局变量。
可参照:https://jd-opensource.github.io/micro-app/docs.html#/zh-cn/plugins
十四、高级功能
- 1、自定义fetch
通过自定义fetch替换框架自带的fetch,可以修改fetch配置(添加cookie或header信息等等),或拦截HTML、JS、CSS等静态资源
- 2、excludeRunScriptFilter: 自定义屏蔽JS加载异常
可选择性屏蔽JS加载异常
3、inheritBaseBody: 子应用body标签是否采用基座标签,默认不采用
4、aHrefResolver: 自定义处理所有子应用 a 标签的 href 拼接方式
5、escapeIframeWindowEvents : iframe 模式 逃逸沙盒的window事件
6、disableIframeRootDocument : iframe模式禁用沙箱Document 默认为false
7、excludeRewriteIframeConstructor : iframe模式下排除对指定构造函数的Symbol.hasInstance属性重写
示例:
1 | microApp.start({ |