微前端架构学习笔记

1,151 阅读5分钟

IMG_6756.JPG

一、微前端开发的特性

1、技术无关:各个开发团队都可以自行选择技术栈,不受同一项目中其它团队影响;

2、业务独立:各个交付产物都可以被独立使用,避免和其它交付产物耦合;

3、样式隔离:各个交付产物中的样式不会污染其它组件;

4、原生支持:各个交付产物都可以自由使用原生API,而非要求使用封装后的API。这第四点一般来说是很难实现的。

二、架构微前端要做哪些事情

1、微服务平台和微前端对比:

微前端是一种架构,它不是一种技术。当你去架构微前端的时候,需要做哪些事情呢,我们可以类比着微服务来找出要做的事情。

微服务微前端备注
服务独立服务,比如交易服务应用或者模块,比如导航服务是不会相互影响的
服务治理服务注册/发现/依赖管理/跟踪/降级/限流/日志/监控/运维应用的发现/路由/鉴权/监控/降级/运行/注销/聚合等需要一个/多个系统统一处理一些上层的事情
服务通信HTTP/RPC/中间件eventBus/sharedWorker/BroadcastChannel/LocalStorage

从上表中可以看到,架构微前端的时候,至少你可以分出来两个层,一个是服务层,一个是服务治理层。应用层中就是一个个的具体的应用或模块,而服务治理层是统一处理应用的一些基本问题,如应用的发现、路由、鉴权、监控、降级、运行、注销、聚合等等。

架构是与技术无关的。

2、微前端的基本结构:

image.png

上层是应用层,包含N个应用,下层是应用治理层,是个主工程。

  • 中心化路由,服务注册中心
  • 标识化应用
  • 设计一定的生命周期
  • 部署和配置自动化

其中,主框架的作用如下:

  • 应用的发现和调度;
  • 转场动画/日志/上报;
  • 应用隔离影响(应用1挂了要求不影响应用2)/CSS隔离影响;
  • 应用监控/降级(当正常的方案不可用时,需要能够快速切换到备选方案)/鉴权等;
  • 应用间通信机制。

如果主框架挂了怎么办?

一是要如何保证主工程不挂(优质的人力、重点Code Review等);

二是主框架挂了之后如何快速检测出来(监控),并修复(一个是为主框架提供备份,主框架挂了切到备份上;热修复能力)。

三、微服务的常见应用场景

1、将一个大型的单个应用拆分成多个独立的应用,通过导航和动态加载来实现无缝的切换。

2、将单个页面上内容拆分成不同的模块交给不同的团队去维护。

四、如何实施微前端

其实主要做两件事——拆分和聚合。

1、常见的拆分方式:

  • 大仓库拆分成独立的模块,分成不同的目录,但还是在一个大仓库中,采用统一的构建。
  • 大仓库通过monorepo methodology拆分多个npm包,npm包发布到npm仓库,或者通过git submodule的方式,集成到主项目。可以做到独立构建,但没有做到独立发布。
  • 大仓库拆分多个子仓库,构建部署出独立的在线服务/应用,通过iframe方式,或者WebComponent的方式聚合成一个完整的应用,这种方式既可以做到独立构建,也可以做到独立发布。
  • 大仓库拆分成多个子仓库,构建后生成JS/CSS/manifest.json等文件,主应用中去动态加载它们,集成到主框架中。这种方式既可以做到独立构建,也可以做到独立发布。

2、常见的技术方案:

1)通过iframe的方式聚合;

对于一些共用UI组件而言,仍然需要重复加载。这也就是iframe模式下的问题 iframe嵌套是一个比较大的问题; iframe在移动端适配的时候会存在一些兼容问题。 iframe会阻塞onload、占用连接池、多层嵌套页面崩溃。

iframe的优势也比较明显: 改造成本低,可以快速上线; 沙箱模型,各个模块天然隔离,不需要考虑样式污染和应用之间的相互影响。

iframe方式实现的Demo:

constructor(props) {
    super(props);
    this.gotoApp = this.gotoApp.bind(this);
    window.addEventListener('message', this.handleMessage.bind(this));
}

state = {
    currentApp: null
}

handleMessage(e) {
    // 定义一套标准的通信机制,比如,父子页面传递的数据含有type、data等字段
    const { data } = e;
    // 这里type定义为domReady,load,unload,openLink
    if (data.type === 'openLink') {
        // 处理子页面里面打开链接
        alert(data.data.url);
        // 将URL push到state里面
        window.history.pushState({}, '', data.data.url);
    }
}

render() {
    let { currentApp } = this.state;
    return (
        <div className="app">
            <header className="app-header">
                 { apps.map((app) => {
                     return (
                         <div className="item" onClick={() => {this.gotoApp(app)}}></div>
                     )
                 }) }
            </header>
            <div className="content">
                 {currentApp ? <iframe src="currentApp.link" onError="{this.handleException}" border=""></iframe>}
            </div>
        </div>
    );
}

handleException() {
    // 处理异常、降级方案等
}

gotoApp(app) {
    // 这里可以显示转场动画
    // 也可以添加日志,记录应用的使用情况
    
    this.setState({
        currentApp: app
    })
}
urlParse() {
    // 处理URL模块,解决URL无法记录的问题
}
// App.js
export default class extends Compoent {
    constructor(props) {
        super(props);
    }
    
    state = {
        currentApp: null
    }
    
    render() {
        return (
            <div className="app">
                <header className="app-header">
                    <div onClick={() => { openLink('/a/b') }}Learn React</div>
                </header>
            </div>
        )
    }
}
// client.js
// 定义一个SDK,负责进行父子页面的通信
// TODO: 应用之间的通信如何处理;如何解决子应用使用a标签,而不是通过代理到主应用的方式
let postMessage = function(data) {
    window.top.window.postMessage(data, 'http://localhost:3001')
}

window.addEventListener('DOMContentLoaded', () => {
    postMessage({
        type: 'domReady',
        data: {
            time: Date.now()
        }
    })
})

export function openLink(url) {
    postMessage({
        type: 'openLink',
        data: {
            url: url
        }
    })
}

2)使用WebComponent构建应用:通过WebComponent的方式把应用拆了,然后再聚合在一起;

3)在不同的框架之上设计通讯、加载机制,诸如Single-SPA。