微前端
1.什么情况使用微前端,用来解决什么问题
当公司项目很多,不同团队又用的技术不同,可以搭建微前端,来整合。
1.技术切换不想重构已有业务
2.有历史包袱无法难以重构,继续迭代可以使用微前端
3.项目过大代码过多,打包很慢,需要使用新技术困难。
总结公司微前端的方案:使用了single-spa,将路由进行拆分不同的子应用,统一打包成systemjs,写了一个自定义插件输出打包后文件的cdn地址,父应用利用ejs模板区分测试正式,放置基础的js,配置父应用启动,匹配路径加载对应子应用。每次打包完都会执行所有子应用的json读取,插入到<script type="systemjs-importmap">中2.微前端的原理是什么
微前端出名的就是single-spa了,国内还有qiankun对其做了进一步的封装。
原理就类似一个状态机,子应用暴露,bootStrap,mount,unMount方法,父应用匹配对应的路由加载对应用执行对应方法。微前端会拦截路由去做子应用的加载和卸载的工作,qianKun还做了沙箱隔离(window)。qiankun
qiankun 基于 single-spa,增加了 css 和 js 隔离(沙箱),预加载等特性。原单页面应用如果是 webpack 编译构建的,可以很方便切换到 qiankun 框架。子应用的资源的加载使用了适合 qiankun 的 import-html-entry 框架(见下)。qiankun 生命周期
beforeLoad?: Lifecycle<T> | Array<Lifecycle<T>>; // function before app load
beforeMount?: Lifecycle<T> | Array<Lifecycle<T>>; // function before app mount
afterMount?: Lifecycle<T> | Array<Lifecycle<T>>; // function after app mount
beforeUnmount?: Lifecycle<T> | Array<Lifecycle<T>>; // function before app unmount
afterUnmount?: Lifecycle<T> | Array<Lifecycle<T>>; // function after app unmount
### single-spa 和 qiankun iframe
● single-spa : 仅实现了路由劫持和应用加载,缺陷:不够灵活,不能动态加载脚本,样式不隔离
● qiankun 基于 single-spa + sandbox + import-html-entry 做到了技术栈无关
● iframe 如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了。iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。
1. url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
2. UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
4. 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
其中有的问题比较好解决(问题 1),有的问题我们可以睁一只眼闭一只眼(问题 4),但有的问题我们则很难解决(问题 3)甚至无法解决(问题 2),而这些无法解决的问题恰恰又会给产品带来非常严重的体验问题使用
定义微应用路由表信息
js
// plugins/qiankun/apps.js
export default [
{
name: 'appVue', // 必选,微应用的名称,微应用之间必须确保唯一。
entry: '//localhost:8081', // 必选,微应用的 entry 地址。(微应用需解决跨域)
container: '#container', // 子应用挂载的div
activeRule: '/app-vue', // 微应用的激活规则。
props: {
customProps: {
version: '9.9.9',
frame: 'vue'
}
}, // 自定义的props参数,可在子应用中获取
loader: (boolean) => {
console.log(`loading状态${boolean}`)
} // 可选,loading 状态发生变化时会调用的方法。
},
{
name: 'project2', //
entry: '//localhost:8082/project2',
container: '#container',
activeRule: '/project2Rule',
props: {
customProps: {
version: '9.9.9',
frame: 'jQuery'
}
},
loader: (boolean) => {
console.log(`loading状态${boolean}`)
}
}
]在主应用中注册微应用
js
// plugins/qiankun/register.js
import {
registerMicroApps,
addGlobalUncaughtErrorHandler,
runAfterFirstMounted,
setDefaultMountApp,
start
} from 'qiankun'
// 引入微应用路由表
import apps from './apps'
function registerApps() {
registerMicroApps(apps, {
beforeLoad: [
(app) => {
console.log('[微应用] before load: ' + app.name)
}
],
beforeMount: [
(app) => {
console.log('[微应用] before mount: ' + app.name)
}
],
afterUnmount: [
(app) => {
console.log('[微应用] after unmount: ' + app.name)
}
]
})
// 设置主应用启动后默认进入的微应用。
// setDefaultMountApp(apps[0].activeRule);
// 第一个微应用 mount 后需要调用的方法,比如开启一些监控或者埋点脚本。
runAfterFirstMounted(() => console.log('开启监控'))
// 添加全局的未捕获异常处理器。
addGlobalUncaughtErrorHandler((event) => {
console.log('[全局捕获异常处理器触发]', event)
const { message } = event
if (message && message.includes('died in status LOADING_SOURCE_CODE')) {
alert('微应用加载失败,请检查应用是否可运行')
}
})
// 启动微应用
start({
// prefetch: true, // 可选,是否开启预加载,默认为 true。
// sandbox: true, // 可选,是否开启沙箱,默认为 true。//从而确保微应用的样式不会对全局造成影响。
// 默认情况下沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。
sandbox: {
// strictStyleIsolation: true, // 开启严格的样式隔离模式 文档说明不推荐
experimentalStyleIsolation: true // 实验性的样式隔离特性 qiankun 会改写子应用所添加的样式为所有样式规则增加一个特殊的选择器规则来限定其影响范围
}
// singular: true, // 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 true。
// fetch: () => {}, // 可选,自定义的 fetch 方法。
// getPublicPath: (url) => { console.log(url); },
// getTemplate: (tpl) => { console.log(tpl); },
// excludeAssetFilter: (assetUrl) => { console.log(assetUrl); }, // 可选,指定部分特殊的动态加载的微应用资源(css/js) 不被qiankun 劫持处理
})
}
export default registerApps初始化全局状态并返回通信方法
js
plugins / qiankun / actions.js
import { initGlobalState } from 'qiankun'
// 定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法。
const initialState = {
user: {
name: '',
head: '',
token: ''
}
}
const actions = initGlobalState(initialState)
export default actionsmain.js
js
import registerApps from './plugins/qiankun/register'
import actions from './plugins/qiankun/actions'
// 启动微应用
registerApps()
// 将通信方法绑定到原型上
Vue.prototype.$actions = actions在 src 目录新增 public-path.js
js
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}vue-router
js
const router = new VueRouter({
mode: 'history',
base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : process.env.BASE_URL,
routes
})隔离方案
js 隔离 sandbox
- iframe
js
const parent = window
const frame = document.createElement('iframe')
const data = [1, 2, 3, 4, 5, 6]
// 当前页面给 iframe 发送消息
frame.onload = function (e) {
frame.contentWindow.postMessage(data)
}
document.body.appendChild(frame)
// iframe 接收到消息后处理
const code = `return dataInIframe.filter((item) => item % 2 === 0)`
frame.contentWindow.addEventListener('message', function (e) {
const func = new frame.contentWindow.Function('dataInIframe', code)
parent.postMessage(func(e.data))
})
// 父页面接收 iframe 发送过来的消息
parent.addEventListener(
'message',
function (e) {
console.log('message from iframe:', e.data)
},
false
)- 快照 在微前端框架 qiankun 中提供了快照方案,其原理就是在应用加载之时保存最初的 window 对象,卸载应用之时通过 diff 操作记录改过的属性即制作快照,当再次激活应用的时候恢复之前的快照。该方案的缺点是会污染 window 导致,多个应用无法同时处于激活状态,优点是兼容性好。
js
// 保存差异的方式
function createSandbox() {
let originWindow = {}
let diffMap = {}
return {
toActive() {
originWindow = {}
// 保存初始window对象
Object.keys(window).forEach((prop) => {
originWindow[prop] = window[prop]
})
// 将上次退出的时候保存的差异还原回去,也就是恢复快照
Object.keys(diffMap).forEach((prop) => {
window[prop] = diffMap[prop]
})
},
toInActive() {
Object.keys(window).forEach((prop) => {
if (window[prop] !== originWindow[prop]) {
// 保存差异
diffMap[prop] = window[prop]
// 还原现场
window[prop] = originWindow[prop]
}
})
}
}
}
window.originData = '最初的window上的数据'
console.log(window.originData, window.a1, window.b1) // 最初的window上的数据 undefined undefined
const sandbox1 = createSandbox() // 创建应用的时候,同时创建沙箱
sandbox1.toActive() // 沙箱激活
window.a1 = 'aaaaa' // 应用修改window上的属性
console.log(window.originData, window.a1, window.b1) // 最初的window上的数据 aaaaa undefined
sandbox1.toInActive() // 切换应用前沙箱1退出
const sandbox2 = createSandbox() // 创建应用的时候,同时创建沙箱
sandbox2.toActive() // 沙箱激活
console.log(window.originData, window.a1, window.b1) // 最初的window上的数据 undefined undefined
window.b1 = 'bbbbb' // 应用修改window上的属性
console.log(window.originData, window.a1, window.b1) // 最初的window上的数据 undefined bbbbb 和上面的数据做个对比
sandbox2.toInActive() // 从应用2切换至1
sandbox1.toActive() // 从应用2切换至1
console.log(window.originData, window.a1, window.b1) // 最初的window上的数据 aaaaa undefined 和上面的数据做个对比
sandbox1.toInActive() // 从应用1切换至2
sandbox2.toActive() // 从应用1切换至2
console.log(window.originData, window.a1, window.b1) // 最初的window上的数据 undefined bbbbb 和上面的数据做个对比- 代理
js
function createProxySandBox() {
const rawWindow = window
const fakeWindow = {}
const proxy = new Proxy(fakeWindow, {
get: (target, p) => {
if (target.hasOwnProperty(p)) {
return target[p]
}
return rawWindow[p]
},
set(target, p, value) {
if (!target.hasOwnProperty(p) && rawWindow.hasOwnProperty(p)) {
rawWindow[p] = value
} else {
target[p] = value
}
}
})
return proxy
}
const sandbox1 = createProxySandBox()
;((window) => {
window.a = 'a'
})(sandbox1)
const sandbox2 = createProxySandBox()
;((window) => {
console.log(window.a)
window.a = 'fff'
})(sandbox2)
console.log(window.a)css 隔离
1. 协商好前缀
2. Element.attachShadow()
可以通过 Element.attachShadow(shadowRootInitshadowRootInit) 方法给指定的元素挂载一个 Shadow DOM,并且返回对 shadow-root 的引用
shadowRootInit: 一个 ShadowRootInit 字典,包括下列字段:
mode 模式
指定 Shadow DOM 树 封装模式 的字符串,可以是以下值:
open shadow root 元素可以从 js 外部访问根节点,例如使用 Element.shadowRoot:
element.shadowRoot; // 返回一个ShadowRoot对象
复制代码
closed 拒绝从 js 外部访问关闭的 shadow root 节点
element.shadowRoot; // 返回nullhtml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>shadow-dom</title>
<style>
h1 {
color: #db73ff !important;
}
</style>
</head>
<body>
<el-h1>
<h1>这是不支持shadow-dom的标题~</h1>
</el-h1>
<script>
if (document.body.createShadowRoot) {
// 作为 shadow-host 的元素节点
let host = document.querySelector('el-h1')
// 创建 shadow-root
let root = host.createShadowRoot()
// 为 shadow-root 添加内容
root.innerHTML =
'<h1 style="background-color: #2cacff; color: white;">这是支持shadow-dom的标题~</h1>'
}
</script>
</body>
</html>