阅读 1936

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

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

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

业务背景

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

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

  • 用户端必须是「一个系统」的心智,从域名到体验
  • 能够根据功能拆分成多个子应用,每个子应用独立开发独立部署
  • 子应用尽量保证跟传统单页面应用一样的开发体验,不要让开发者有太多学习成本
  • 所有子应用可被统一管理起来,不能无限制的泛滥

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

  • 代码量达到一定量的时候,单次构建时间很长,开发&发布效率极低
  • 代码库中依赖升级会影响整个应用,而代码量又非常大,导致回归成本极高
  • 变成一个非常臃肿的巨石应用,完全失去灵活性,无论是多人协作还是业务接入成本都会大大增加

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

技术调研

单/多页面应用

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

iframe

每个子应用独立开发独立部署,然后通过 iframe 的方式将这些应用嵌入到同一个系统中,这是一个很彻底的方案同时也是使用最多的一个方案,但 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>
  );
};
复制代码

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

沙箱与隔离

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

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

  • 大部分业务没有三方接入需求,支持非常彻底的沙箱机制没有太大意义,至少在我们目前落地的业务中还没有出现相互污染的问题
  • 对于可控的二方应用接入,我们推荐进行一些规范约定即可,比如不要污染全局变量、定时器及时清除、CSS 样式尽量通过前缀或者 CSS Modules 做隔离
  • 对于不可控的三方应用,建议暂时先通过 iframe 的方式接入
  • 沙箱机制有机会通过 Shadow DOM 和 Web Worker 之类的方案解决,还在探索中,欢迎交流

与 single-spa 的关系

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

  • single-spa 社区知名度更高,生态以及能力上会完善一些,这个是 icestark 要持续追赶的
  • 子应用方面:icestark 对子应用的侵入几乎可以忽略,使用成本更低,同时子应用也可以独立运行,而 single-spa 相对多了一些侵入,需要了解各种生命周期,子应用是否能单独运行需要确认下
  • API 设计层面:icestark 将应用路由类比为页面路由,类 react-router 的 API 设计更加简单直观一些
  • 架构设计上:icestark 更加简单,single-spa 要重一些,比如需要 single-spa-react/vue/preact 这种设计,具体可以看下两者的核心代码,其实都比较简单

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

最佳实践

  • 框架应用职责明确,只做整体布局以及子应用的注册管理,不做其他任何 UI 或逻辑,因为框架应用本质是一个中心化部件,中心化的部分越简单整个系统就越稳定
  • 子应用通过基准路由来划分,这样应用的管理更加简单直观,也很难出现应用间路由冲突的问题
  • 子应用尽量避免依赖框架应用的能力,比如框架层面提供一些全局的 API 或者组件都是非常不好的架构设计
  • 子应用的信息推荐通过配置平台管理,然后 server 端通过全局变量的方式暴露给框架应用,这样子应用的版本变化、增加删除等都不需要依赖框架应用发布

业务落地

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

阿里创作者平台

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

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

淘系小二工作台

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

微前端的未来

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

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

相关链接