微前端是目前比较热门的一种技术架构,挺多读者私底下问我其中的原理。为了讲清楚原理,我会带着大家从零开始实现一个微前端框架,其中包含了以下功能:
1、如何进行路由劫持
2、 如何渲染子应用
3、如何实现 JS 沙箱及样式隔离
4、提升体验性的功能
另外在实现的过程中,笔者还会聊聊目前有哪些技术方案可以去实现微前端以及做以上功能的时候有哪些实现方式。
微前端实现方案
微前端的实现方案有挺多,比如说:
1 、qiankun,icestark自己实现 JS 及样式隔离
2、emp,Webpack 5 Module Federation(联邦模块)方案
3、iframe 、WebComponent 等方案,浏览器原生隔离,但存在一些问题
更新:这里之前有一个错误,笔者错把 icestark 的技术方案说成了 iframe 的。
但是这么多实现方案解决的场景问题还是分为两类:
1、单实例:当前页面只存在一个子应用,一般使用 qiankun 就行
2、多实例:当前页面存在多个子应用,可以使用浏览器原生隔离方案,比如 iframe 或者 WebComponent 这些
当然了,并不是说单实例只能用 qiankun,浏览器原生隔离方案也是可行的,只要你接受它们带来的不足就行:
iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。
前置工作
在正式开始之前,我们需要搭建一下开发环境,这边大家可以任意选择主 / 子应用的技术栈,比如说主应用用 React,子应用用 Vue,自行选择即可。每个应用用对应的脚手架工具初始化项目就行,这边就不带着大家初始化项目了。记得如果是 React 项目的话,需要另外再执行一次yarn eject。
推荐大家直接使用笔者仓库里的 example 文件夹,该配置的都配置好了,大家只需要安心跟着笔者一步步做微前端就行。例子中主应用为 React,子应用为 Vue,最终我们生成的目录结构大致如下:
正文
在阅读正文前,我假定各位读者已经使用过微前端框架并了解其中的概念,比如说知晓主应用是负责整体布局以及子应用的配置及注册这类内容。如果还未使用过,推荐各位简略阅读下任一微前端框架使用文档。
应用注册
在有了主应用之后,我们需要先在主应用中注册子应用的信息,内容包含以下几块:
* name:子应用名词
* entry:子应用的资源入口
* container:主应用渲染子应用的节点
* activeRule:在哪些路由下渲染该子应用
其实这些信息和我们在项目中注册路由很像,entry可以看做需要渲染的组件,container可以看做路由渲染的节点,activeRule可以看做如何匹配路由的规则。
接下来我们先来实现这个注册子应用的函数:
上述实现很简单,就只需要将用户传入的 appList 保存起来即可
路由劫持
在有了子应用列表以后,我们需要启动微前端以便渲染相应的子应用,也就是需要判断路由来渲染相应的应用。但是在进行下一步前,我们需要先考虑一个问题:如何监听路由的变化来判断渲染哪个子应用? 对于非 SPA(单页应用) 架构的项目来说,这个完全不是什么问题,因为我们只需要在启动微前端的时候判断下当前 URL 并渲染应用即可;但是在 SPA 架构下,路由变化是不会引发页面刷新的,因此我们需要一个方式知晓路由的变化,从而判断是否需要切换子应用或者什么事都不干。 为了照顾不了解的读者,笔者这里先简略的聊一下路由原理。 目前单页应用使用路由的方式分为两种:
hash 模式,也就是 URL 中携带 #
histroy 模式,也就是常见的 URL 格式了
因此这两个事件我们肯定是需要去监听的。除此之外,调用 pushState 以及 replaceState 也会造成路由变化,但不会触发事件,因此我们还需要去重写这两个函数。
知道了该监听什么事件以及重写什么函数之后,接下来我们就来实现代码:
以上代码看着很多行,实际做的事情很简单,总体分为以下几步:
重写 pushState 以及 replaceState 方法,在方法中调用原有方法后执行如何处理子应用的逻辑
监听 hashchange 及 popstate 事件,事件触发后执行如何处理子应用的逻辑
重写监听 / 移除事件函数,如果应用监听了 hashchange 及 popstate 事件就将回调函数保存起来以备后用
应用生命周期
在实现路由劫持后,我们现在需要来考虑如果实现处理子应用的逻辑了,也就是如何处理子应用加载资源以及挂载和卸载子应用。看到这里,大家是不是觉得这和组件很类似。组件也同样需要处理这些事情,并且会暴露相应的生命周期给用户去干想干的事。 因此对于一个子应用来说,我们也需要去实现一套生命周期,既然子应用有生命周期,主应用肯定也有,而且也必然是相对应子应用生命周期的。 那么到这里我们大致可以整理出来主 / 子应用的生命周期。 对于主应用来说,分为以下三个生命周期:
beforeLoad:挂载子应用前
mounted:挂载子应用后
unmounted:卸载子应用
当然如果你想增加生命周期也是完全没问题的,笔者这里为了简便就只实现了三种。 对于子应用来说,通用也分为以下三个生命周期:
bootstrap:首次应用加载触发,常用于配置子应用全局信息
mount:应用挂载时触发,常用于渲染子应用
unmount:应用卸载时触发,常用于销毁子应用
接下来我们就来实现注册主应用生命周期函数:
因为是主应用的生命周期,所以我们在注册子应用的时候就顺带注册上了。
然后子应用的生命周期:
以上代码看着很多,实际实现也很简单,总结一下就是:
设置子应用状态,用于逻辑判断以及优化。比如说当一个应用状态为非 NOT_LOADED 时(每个应用初始都为 NOT_LOADED 状态),下次渲染该应用时就无需重复加载资源了
如需要处理逻辑,比如说 beforeLoad 我们需要加载子应用资源
执行主 / 子应用生命周期,这里需要注意下执行顺序,可以参考父子组件的生命周期执行顺序
完善路由劫持
实现应用生命周期以后,我们现在就能来完善先前路由劫持中没有做的「如何处理子应用」的这块逻辑。 这块逻辑在我们做完生命周期之后其实很简单,可以分为以下几步:
判断当前 URL 与之前的 URL 是否一致,如果一致则继续
利用当然 URL 去匹配相应的子应用,此时分为几种情况:
初次启动微前端,此时只需渲染匹配成功的子应用
未切换子应用,此时无需处理子应用
切换子应用,此时需要找出之前渲染过的子应用做卸载处理,然后渲染匹配成功的子应用
保存当前 URL,用于下一次第一步判断
理清楚步骤之后,我们就来实现它:
以上代码主体就是在按顺序执行生命周期函数,但是其中匹配路由的函数并未实现,因为我们需要先来考虑一些问题。 大家平时项目开发中肯定是用过路由的,那应该知道路由匹配的原则主要由两块组成: 嵌套关系 路径语法
改善型功能
prefetch 我们目前的做法是匹配一个子应用成功后才去加载子应用,这种方式其实不够高效。我们更希望用户在浏览当前子应用的时候就能把别的子应用资源也加载完毕,这样用户切换应用的时候就无需等待了。 实现起来代码不多,利用我们之前的 import-html-entry 就能马上做完了:
以上代码别的都没啥好说的,主要来聊下requestIdleCallback这个函数。
window.requestIdleCallback()方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。
我们利用这个函数实现在浏览器空闲时间再去进行 prefetch,其实这个函数在 React 中也有用到,无非内部实现了一个 polyfill 版本。因为这个 API 有一些问题(最快 50ms 响应一次)尚未解决,但是在我们的场景下不会有问题,所以可以直接使用。
资源缓存机制
当我们加载过一次资源后,用户肯定不希望下次再进入该应用的时候还需要再加载一次资源,因此我们需要实现资源的缓存机制。 上一小节我们因为使用到了 import-html-entry,内部自带了缓存机制。如果你想自己实现的话,可以参考内部的实现方式。 简单来说就是搞一个对象缓存下每次请求下来的文件内容,下次请求的时候先判断对象中存不存在值,存在的话直接拿出来用就行。
最后
文章到这里就完结了,谢谢大家对文章的喜爱
另外大家也可以在交流区提问,笔者会在空闲时间解答问题。