微前端实践:ark(方舟)

446 阅读6分钟

本不想有太多的前戏,毕竟大家看多了各种各样的前戏,可能没啥胃口,但是思来想去,直接硬钢给个传送门,终究太生硬,所以还是要跟着剧本走,慢慢来...

什么是微前端?

在我们听说微前端之前可能经常听到服务端同学说什么微服务,而微前端也是因此而来(毕竟写牛逼框架的前端程序猿大多都是写后端出身,纯前端就只能,先去墙角哭会...)。无论微前端还是微服务,简单来说,就是把一个很大的项目工程,划分成一小块一小块工程,这些工程都是一个个独立的工程,无论你在这些工程中使用什么语言,什么框架,都不会影响其他工程,没有过多的限制,当然在服务端表现为各个独立的服务,在前端就表现为各个独立的页面。大致样子如下:

为什么要微前端?

在看完我那抽象的离奇的图之后,可能有所疑惑为什么要用微前端。其实如果在小型公司中,业务线或者应用没有那么庞大的时候,确实没必要用这个,我们使用任何技术都需要结合实际的场景,不要强拉硬拽,往往在某个合适的时间点,你会自然而然的运用相关需要的技术。大致在如下情况下,你可能会想用微前端:

  • 服务越来越大,或者业务线越来越多,代码越来越多,打包速度越来越慢。
  • 很细微的改动,需要发布整个庞大的应用
  • 代码耦合度极高,一个不留神的改动可能垮掉好多功能
  • 多人在一个仓库共同修改代码,维护风险比较高
  • ....

当然以上问题有其他方案去解决,微前端并不是唯一的,只是一个可行的方案。它可以帮助我们实现增量升级,单独发布,解耦代码,让团队成员更专注在独立的业务线或者功能上等。。。

现有方案:

iframe:

iframe实现微前端是一个非常简单的方案,已经有相当长的历史,一般在要求不高的项目中大可使用这种方案实现,尤其是后台内部应用中,千万别听网上好多人说iframe技术太老,太low,没啥技术含量,无法通信或者通信难度高,这些都是瞎扯,在恰当的时机选择最合适的才是最好的,最优的。唯独要注意的是,如果使用在移动端,要慎重,因为iframe在移动端表现感觉不是很乐观,这个时候需要考虑下其他方案。

前端加载器(single-spa、icestark、qiankun)

除了iframe方案,目前基本都是通过加载js资源或者拼凑html片段等方式来实现,比较成熟的有single-spaicestarkqiankun,有兴趣的可以去尝试用用看,这些框架都有相当的用户基数,其中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/… 可克隆代码后启动服务查看:

  1. git clone github.com/sadrun/ark.…
  2. cd ark/packages/ark && npm start
  3. 也可自行进入 packages/ark/example 执行:yarn && yarn start