imsobear / blog

果同学的博客
161 stars 9 forks source link

面向大型工作台的微前端解决方案 icestark #60

Closed imsobear closed 4 years ago

imsobear commented 5 years ago

知乎专栏:https://zhuanlan.zhihu.com/p/88449415

TB1A_uikXP7gK0jSZFjXXc5aXXa-2000-1500.jpg

2017 年中旬,ICE 团队接到一个叫做「阿里创作平台」的项目,这个产品为创作者提供了入驻、帐号管理、内容管理、内容发布、粉丝运营、数据分析等等非常完备的功能,页面数 50+、项目一期有 3-6 个前端同时开发、业务未来有二方业务接入的需求……针对这些问题,传统的单页面应用方案实在有点力不从心,因此在详细的技术评估之后我们最终自研了一套叫做 AppLoader 的方案,而在此时社区可能还没有微前端这个概念。事实上这个方案也是近乎完美的解决了我们的业务问题,但由于场景比较受限因此我们一直没有将其对外。

2019 年年初,淘宝有一个非常重要的项目「小二工作台」,简单来讲「小二工作台」要打造面向运营小二的操作系统,解决多个后台体验不一致、频繁跳转效率低、重复建设等问题,于是 AppLoader 重出江湖。同时结合淘宝业务发展以及微前端这个概念在社区的普及度,我们判断未来此类业务场景会越来越多,因此我们对 AppLoader 做了一次能力和品牌的升级,同时面向社区开源,这便是 icestark 的由来。

业务背景

智者说:抛开业务场景谈技术方案(微前端)都是瞎扯淡,因此在介绍 icestark 之前,我们先了解下当时面临的业务背景是怎样的。

如前文所说,两年多之前我们接到「阿里创作平台」这个项目,这个产品承载了创作者从入驻到创作完整的生命周期,相比于普通业务有以下两个不同点:页面数非常多、未来有二方业务接入的需求,针对这两个特点前端方案需要核心解决的问题:

其中第二点提到的拆分子应用是方案的核心,在上面所说的业务背景下,这个项目的代码量一定会快速增多,那么如果是通过一个前端应用来管理所有代码,无论是当下流行的单页面应用还是传统的多页面应用都无法规避以下问题:

针对这些问题我们做了一轮完整的技术调研。

技术调研

单/多页面应用

如上所述,不具有可行性。

iframe

每个子应用独立开发独立部署,然后通过 iframe 的方式将这些应用嵌入到同一个系统中,这是一个很彻底的方案同时也是使用最多的一个方案,但 iframe 的体验问题一直是个难以解决的问题:

这些问题有的可以解决,有的很难解决,有的几乎无法解决,因此我们舍弃了 iframe 的方案。

封装框架组件

封装一个统一的框架 UI 组件,每个子应用自行接入框架组件,框架组件可以是一个 npm 或者覆盖式的 cdn 资源或者 vmcommon,这个就类似淘宝 PC 端业务接统一吊顶的场景。但是这个方案有以下几个问题:

事实上这个方案更适合于各个应用非常独立,从前端到后端服务全部都归属于某个业务方,而且各个业务方之间也比较独立,没有中心化管理的需求。

微前端?

比较遗憾的是在 2017 年中旬我们还没有了解到「微前端」这个概念(可能还没诞生?),至于今天社区中相对有名的微前端解决方案 single-spa 可能还没有或者知名度比较小,并没有进入到我们的调研列表里。

另外,微前端这个概念我也是今年才真正接触到,也才意识到我们两年前做的方案就是今天所谓的微前端。

自研 icestark

完成技术调研之后,我们最终决定自研一套方案即 icestark,这套方案具体的设计思路和使用方式参考下文。

架构设计

image.png

如上图所示,首先引入框架应用和子应用的概念,框架应用负责系统整体布局以及子应用的注册、加载与渲染,同时在设计原则上我们希望「子应用尽量保持跟传统单页面应用一样的开发体验」,保证子应用自身可独立运行、存量应用可快速迁移适配、增量应用跟传统方式开发体验一致。

在确认上述核心设计思路之后,接下来就是对问题进行具体拆解:子应用是一个传统的 SPA 应用(可包含一个或多个页面),会打包出 bundle 同时发布到 CDN,那么我们需要在框架应用中注册管理所有子应用,然后在适当的时机加载对应的子应用 bundle 并将其渲染到指定节点(系统布局里面)。在这个流程里核心要解决的技术问题如下。

1. 什么时候加载哪个子应用?

子应用包含多个页面即路由,只有页面路由的变化会引起子应用的切换,那么我们只要建立子应用和路由的映射关系即可。为每个子应用分配一个基准路由如 /seller ,这个子应用保证所有的路由定义在 /seller 下,那么当从其他路由跳转到 /seller 路由时我们就可以加载渲染 /seller 对应的子应用 bundle 了。PS:除了基准路由这种约束方式也支持其他更加松散的方式。

2. 如何捕获到系统中所有路由的变化?

icestark 通过劫持 history.pushState 和 history.replaceState 两个 API,同时监听 popstate 事件,保证能够捕获到到所有路由变化。当捕获到路由变化时,根据路由查找对应的子应用,如果对应的还是当前这个子应用则什么事情都不做,如果对应的是新的一个子应用则卸载之前的子应用,同时加载新的子应用并渲染之。

3. 如何将子应用的 bundle 渲染到指定节点?

框架应用有系统的 Layout,我们需要将子应用渲染到 Layout 里面,但是单页面应用都是直接通过 ReactDOM.render(<App />, document.getElementById('#root'))  的方式渲染,如果直接执行那么渲染的位置是无法被控制的,于是 icestark 为子应用提供了一个 getMountNode() 的 API 保证子应用能够渲染到指定的节点里。

4. 子应用使用不同的前端框架怎么办?

在我们内部使用时其实并没有考虑这个问题,因为我们内部目前都是 React 的技术栈,基本不存在这样的问题,但是如果要将这个方案开源,那这个特性是必须支持的。

比较有意思的是回顾上述的核心设计,icestark 对子应用的约束非常简单:路由需要规范最好是通过基准路由约束、需要渲染到指定节点里,那么子应用是通过 Vue 或者 ReactDOM 亦或是 jQuery 渲染都无所谓了,整体方案对此没有任何依赖。

这便是 icestark 核心的几块设计思路,接下来我们简单看下这套方案如何使用。

快速使用

开发框架应用

安装 iceworks CLI 工具:

$ npm i -g iceworks

通过命令行初始化一个框架应用:

$ mkdir icestark-framework-app && cd icestark-framework-app
$ iceworks init @icedesign/stark-layout-scaffold

完成初始化之后安装依赖然后通过 npm start 即可进行预览。

src/App.jsx 中我们可以看到核心的子应用注册代码:

import React from 'react';
import { AppRouter, AppRoute } from '@ice/stark';

export default class App extends React.Component {
  render() {
    return (
      <BasicLayout pathname={pathname}>
        <AppRouter
          onRouteChange={this.handleRouteChange}
          onAppLeave={this.handleAppLeave}
          onAppEnter={this.handleAppEnter}
        >
          <AppRoute
            path="/seller"
            basename="/seller"
            title="商家平台"
            url={[
              '//unpkg.com/icestark-child-seller/build/js/index.js',
              '//unpkg.com/icestark-child-seller/build/css/index.css',
            ]}
          />
          <AppRoute 
                        // ...
                    />
        </AppRouter>
      </BasicLayout>
    );
  }
}

子应用注册的核心信息:

{
  // 为子应用分配的基准路由
    path: '/seller',
  // 子应用的 bundle 地址,用来渲染子应用
    url: [
    '//unpkg.com/icestark-child-seller/build/js/index.js',
    '//unpkg.com/icestark-child-seller/build/css/index.css',
    ],
}

开发子应用

通过命令行初始化子应用:

$ mkdir icestark-child-app-test && cd icestark-child-app-test

# 基于 React 的子应用
$ iceworks init project @icedesign/stark-child-scaffold
# 基于 Vue 的子应用
$ iceworks init project @vue-materials/icestark-child-app

同样安装依赖执行 npm start 即可单独开发预览子应用,如果想在框架应用中预览,替换相关 bundle 地址即可。

相比于传统的单页面应用,icestark 的子应用有三个需要定制的地方:

  1. 需要主动注册&触发应用的卸载事件
  2. 应用渲染的节点需要通过 getMountNode() API 来获取
// src/index.jsx
import ReactDOM from 'react-dom';
import { getMountNode, registerAppLeave } from '@ice/stark-app';
import router from './router';

registerAppLeave(() => {
  ReactDOM.unmountComponentAtNode(mountNode);
});

ReactDOM.render(router(), getMountNode(document.getElementById('mountNode')));
  1. 路由需要定义在约定的基准路由下面:
// src/router.jsx
import { BrowserRouter as Router, Switch } from 'react-router-dom';
import { getBasename } from '@ice/stark-app';

export default () => {
  return (
    <Router basename={getBasename()}>
      <Switch>
            // ...
      </Switch>
    </Router>
  );
};

完整的文档请参考 子应用开发与迁移 。

沙箱与隔离

说到微前端一定逃不了沙箱的相关话题,但是针对这个问题目前业界还没有一个非常完美的机制,具体可参考文档 样式和脚本隔离,这里面有我们的一些探索。

针对这个问题我们的一些观点:

与 single-spa 的关系

icestark 与 single-spa 都属于微前端的解决方案,两者在能力上并无太大差别,这里简单梳理下个人的一些观点:

另外 qiankun 是对 single-spa 的一层封装,核心做了构建层面的一些约束以及沙箱能力,构建层面的约束(比如 umd)个人觉得会让子应用变复杂,不一定是一个好的方案,然后沙箱这块 icestark 是将 onAppEnter/onAppLeave 这种钩子暴露给框架应用,让业务自身去按需做一些比如全局变量冻结之类的事情。

最佳实践

业务落地

icestark 目前主要落地在阿里内部的业务,社区里可能也有几个项目,不过目前还没有做过专门的统计,如果有用到的话欢迎反馈给我们。

阿里创作者平台

包含 20+ 子应用,其中 5-8 个子应用由二方业务开发。

阿里健康-熙牛医疗云医院信息系统

淘系小二工作台

面向淘系运营小二的后台都将已子应用的方式接入小二工作台,打造面向运营小二的操作系统。

微前端的未来

微前端当下主要还是在解决工程问题,比如系统的解耦、多人协作之类的,所以其实去看下核心代码都是非常简单易懂的。在工程问题的基础上接下来我们会有两个方向:第一是探索沙箱机制,让二方业务更加安全的运行,同时让不可控的三方业务接入逐渐成为可能;第二针对微前端的业务场景逐步完善生态,比如一些鉴权之类的业务需求,这块有需求欢迎反馈。

最后欢迎评论交流 & 点赞 & star,以及通过钉钉群跟我们沟通。

相关链接