本不想有太多的前戏,毕竟大家看多了各种各样的前戏,可能没啥胃口,但是思来想去,直接硬钢给个传送门,终究太生硬,所以还是要跟着剧本走,慢慢来...
什么是微前端?
在我们听说微前端之前可能经常听到服务端同学说什么微服务,而微前端也是因此而来(毕竟写牛逼框架的前端程序猿大多都是写后端出身,纯前端就只能,先去墙角哭会...)。无论微前端还是微服务,简单来说,就是把一个很大的项目工程,划分成一小块一小块工程,这些工程都是一个个独立的工程,无论你在这些工程中使用什么语言,什么框架,都不会影响其他工程,没有过多的限制,当然在服务端表现为各个独立的服务,在前端就表现为各个独立的页面。大致样子如下:
为什么要微前端?
在看完我那抽象的离奇的图之后,可能有所疑惑为什么要用微前端。其实如果在小型公司中,业务线或者应用没有那么庞大的时候,确实没必要用这个,我们使用任何技术都需要结合实际的场景,不要强拉硬拽,往往在某个合适的时间点,你会自然而然的运用相关需要的技术。大致在如下情况下,你可能会想用微前端:
- 服务越来越大,或者业务线越来越多,代码越来越多,打包速度越来越慢。
- 很细微的改动,需要发布整个庞大的应用
- 代码耦合度极高,一个不留神的改动可能垮掉好多功能
- 多人在一个仓库共同修改代码,维护风险比较高
- ....
当然以上问题有其他方案去解决,微前端并不是唯一的,只是一个可行的方案。它可以帮助我们实现增量升级,单独发布,解耦代码,让团队成员更专注在独立的业务线或者功能上等。。。
现有方案:
iframe:
iframe实现微前端是一个非常简单的方案,已经有相当长的历史,一般在要求不高的项目中大可使用这种方案实现,尤其是后台内部应用中,千万别听网上好多人说iframe技术太老,太low,没啥技术含量,无法通信或者通信难度高,这些都是瞎扯,在恰当的时机选择最合适的才是最好的,最优的。唯独要注意的是,如果使用在移动端,要慎重,因为iframe在移动端表现感觉不是很乐观,这个时候需要考虑下其他方案。
前端加载器(single-spa、icestark、qiankun)
除了iframe方案,目前基本都是通过加载js资源或者拼凑html片段等方式来实现,比较成熟的有single-spa、icestark、qiankun,有兴趣的可以去尝试用用看,这些框架都有相当的用户基数,其中qiankun是基于single-spa实现的,single-spa用起来感觉难度不小。
服务端模版
在纯前端实现之外,服务端也可根据插入标记模版的方式,实现注入js或者html片段,返回给前端,当然这部分对于前端开发来说只做了解就好,因为他并不是严格意义上的微前端,只是服务端模版的拼凑。
ark (方舟)
为了更深入的了解通过前端加载器这样的方案实现的微前端,于是本着前端人一贯喜欢重复造轮子的精神,自己实现了一个前端微服务库,ark(方舟),仓库地址:github.com/sadrun/ark/…,借鉴了qiankun的实现思路,并且使用了qiankun作者实现的import-html-entry库,然后比较简单的实现了一个微前端库。
实现思路
1. 预加载资源
如果在应用注册配置中,有配置需要预加载的应用,则在初始化的同时去加载这些应用。
function registerMicroApps(apps: IApp[], config: ICustomConfig): void; //应用注册方法
type ActiveRule = string|RegExp|((location:any) => boolean); //应用匹配规则
interface ICustomConfig { //额外配置,目前只有首页配置
index?:string, //指定首页应用路径
[key:string]:any,
}
interface IApp { //应用注册信息
name:string, //应用名
entry:string | { styles?: string[], scripts?: string[], html?: string }, //入口资源,可以是指定html页面地址,也可以是相关js,样式,html片段等资源
render:(props:{ template:string, loading:boolean, name?:string})=> any, //渲染回调,在相关资源准备好之后会调用此方法,使用者可以在这个配置中写自己的应用
activeRule:ActiveRule, //应用匹配规则
isPreload?:boolean, //是否需要预加载应用
}
interface IRApp { //注册之后的应用
name:string, //应用名
loadAppFun: ()=>Promise<any>, //应用加载执行方法
preLoad:(()=>Promise<any>) | null, //预加载方法
isActive:(location:any) => (boolean | undefined) //应用匹配验证方法
}
const apps:IRApp[] = [];
2. 初始化路由
根据配置的路由规则,与当前页面路径匹配,找到当前有效的应用信息。并且通过popstate监听页面路由变化,根据路由变化得到当前有效的应用
function initRouter(config?: ICustomConfig): void;
3. 代理不分window事件
由于每个应用可能各自会绑定一些window事件,因此劫持window.addEventListener,将每个应用所绑定的事件记录下来,方便后续再切换路由时清除掉。
// window事件
const winAddListener = window.addEventListener;
const winRemoveListener = window.removeEventListener;
let curEventListener:IListener[] = [];
export function proxyEventListener(){
window.addEventListener = (event:string, handleFunction:any, useCapture:any=false)=>{
curEventListener.push({
event,
handleFunction,
useCapture,
});
winAddListener(event, handleFunction, useCapture);
}
}
export function clearEventListener(){
console.log('清除事件:',curEventListener);
curEventListener.forEach((listener:any)=>{
winRemoveListener(listener.event,listener.handleFunction,listener.useCapture)
});
curEventListener = [];
}
4. 加载资源
目前在各种方案中,加载资源有两种方式,一种是通过配置js资源,直接加载并执行。另一种是通过加载目标页面,分析各种资源然后加载执行,ark所采用的是后者,通过配置的页面地址,加载并分析样式,js资源,对于样式,直接嵌入到具体所请求的页面中,然后整体渲染在主框架下,再执行js。
5. 记录全局变量
在每个应用执行之前,记录当前全局变量,然后在应用被卸载的时候,清除掉所有全局变量,以免影响下个应用的执行。
let lastGlobalVariableIndex:number;
export function recordCurAppGlobalVariable() {
delete window.webpackJsonp;
lastGlobalVariableIndex = 0;
for (const p in window) {
if (!window.hasOwnProperty(p)){
continue;
}
lastGlobalVariableIndex += 1;
}
return lastGlobalVariableIndex;
}
export function clearOldAppGlobalVariable(){
const windowProps = [];
for (const p in window) {
if (!window.hasOwnProperty(p)) {
continue;
}
windowProps.push(p);
}
if(lastGlobalVariableIndex < windowProps.length){
console.log('清除全局变量:',windowProps.slice(lastGlobalVariableIndex));
windowProps.slice(lastGlobalVariableIndex).map((prop:any)=>{
delete window[prop];
})
}
}
全局变量计算规则
目前捕捉全局变量通用的规则是代理window对象,在设置全局变量的时候劫持并记录,然后再卸载的时候清除掉。在ark中,通过在应用加载前记录window属性的最后一位位置,然后在卸载的时候,从之前记录的最后一位位置开始逐个注销掉,以达到变量清除。
使用方式
import { registerMicroApps } from '@ark-plan/ark';
function render({ template, loading, name}) {
const container = document.getElementById('frameWork');
ReactDOM.render(
<Framework loading={loading} content={template} name={name}
/>,
container
);
}
render({ loading: true });
registerMicroApps(
[
{
name: 'home',
entry: '/home.html',
render,
activeRule:'/fe/home',
isPreload:true,
},
{
name: 'mall',
entry: '/mall.html',
render,
activeRule:'/fe/mall',
isPreload:false,
},
{
name: 'point',
entry: '/point.html',
render,
activeRule:'/fe/point',
isPreload:false,
},
],
{
index:'/fe/home'
}
);
具体demo地址:github.com/sadrun/ark/… 可克隆代码后启动服务查看:
- git clone github.com/sadrun/ark.…
- cd ark/packages/ark && npm start
- 也可自行进入 packages/ark/example 执行:yarn && yarn start