2017 年中旬,ICE 团队接到一个叫作「阿里创做平台」的项目,这个产品为创做者提供了入驻、账号管理、内容管理、内容发布、粉丝运营、数据分析等等很是完备的功能,页面数 50+、项目一期有 3-6 个前端同时开发、业务将来有二方业务接入的需求……针对这些问题,传统的单页面应用方案实在有点力不从心,所以在详细的技术评估以后咱们最终自研了一套叫作 AppLoader 的方案,而在此时社区可能尚未微前端这个概念。事实上这个方案也是近乎完美的解决了咱们的业务问题,但因为场景比较受限所以咱们一直没有将其对外。javascript
2019 年年初,淘宝有一个很是重要的项目「小二工做台」,简单来说「小二工做台」要打造面向运营小二的操做系统,解决多个后台体验不一致、频繁跳转效率低、重复建设等问题,因而 AppLoader 重出江湖。同时结合淘宝业务发展以及微前端这个概念在社区的普及度,咱们判断将来此类业务场景会愈来愈多,所以咱们对 AppLoader 作了一次能力和品牌的升级,同时面向社区开源,这即是 icestark 的由来。css
智者说:抛开业务场景谈技术方案(微前端)都是瞎扯淡,所以在介绍 icestark 以前,咱们先了解下当时面临的业务背景是怎样的。前端
如前文所说,两年多以前咱们接到「阿里创做平台」这个项目,这个产品承载了创做者从入驻到创做完整的生命周期,相比于普通业务有如下两个不一样点:页面数很是多、将来有二方业务接入的需求,针对这两个特色前端方案须要核心解决的问题:vue
其中第二点提到的拆分子应用是方案的核心,在上面所说的业务背景下,这个项目的代码量必定会快速增多,那么若是是经过一个前端应用来管理全部代码,不管是当下流行的单页面应用仍是传统的多页面应用都没法规避如下问题:java
针对这些问题咱们作了一轮完整的技术调研。react
如上所述,不具备可行性。git
每一个子应用独立开发独立部署,而后经过 iframe 的方式将这些应用嵌入到同一个系统中,这是一个很完全的方案同时也是使用最多的一个方案,但 iframe 的体验问题一直是个难以解决的问题:github
这些问题有的能够解决,有的很难解决,有的几乎没法解决,所以咱们舍弃了 iframe 的方案。npm
封装一个统一的框架 UI 组件,每一个子应用自行接入框架组件,框架组件能够是一个 npm 或者覆盖式的 cdn 资源或者 vmcommon,这个就相似淘宝 PC 端业务接统一吊顶的场景。可是这个方案有如下几个问题:后端
事实上这个方案更适合于各个应用很是独立,从前端到后端服务所有都归属于某个业务方,并且各个业务方之间也比较独立,没有中心化管理的需求。
比较遗憾的是在 2017 年中旬咱们尚未了解到「微前端」这个概念(可能还没诞生?),至于今天社区中相对有名的微前端解决方案 single-spa 可能尚未或者知名度比较小,并无进入到咱们的调研列表里。
另外,微前端这个概念我也是今年才真正接触到,也才意识到咱们两年前作的方案就是今天所谓的微前端。
完成技术调研以后,咱们最终决定自研一套方案即 icestark,这套方案具体的设计思路和使用方式参考下文。
如上图所示,首先引入框架应用和子应用的概念,框架应用负责系统总体布局以及子应用的注册、加载与渲染,同时在设计原则上咱们但愿「子应用尽可能保持跟传统单页面应用同样的开发体验」,保证子应用自身可独立运行、存量应用可快速迁移适配、增量应用跟传统方式开发体验一致。
在确认上述核心设计思路以后,接下来就是对问题进行具体拆解:子应用是一个传统的 SPA 应用(可包含一个或多个页面),会打包出 bundle 同时发布到 CDN,那么咱们须要在框架应用中注册管理全部子应用,而后在适当的时机加载对应的子应用 bundle 并将其渲染到指定节点(系统布局里面)。在这个流程里核心要解决的技术问题以下。
子应用包含多个页面即路由,只有页面路由的变化会引发子应用的切换,那么咱们只要创建子应用和路由的映射关系便可。为每一个子应用分配一个基准路由如 /seller
,这个子应用保证全部的路由定义在 /seller
下,那么当从其余路由跳转到 /seller
路由时咱们就能够加载渲染 /seller
对应的子应用 bundle 了。PS:除了基准路由这种约束方式也支持其余更加松散的方式。
icestark 经过劫持 history.pushState
和 history.replaceState
两个 API,同时监听 popstate
事件,保证可以捕获到到全部路由变化。当捕获到路由变化时,根据路由查找对应的子应用,若是对应的仍是当前这个子应用则什么事情都不作,若是对应的是新的一个子应用则卸载以前的子应用,同时加载新的子应用并渲染之。
框架应用有系统的 Layout,咱们须要将子应用渲染到 Layout 里面,可是单页面应用都是直接经过 ReactDOM.render(<App />, document.getElementById('#root'))
的方式渲染,若是直接执行那么渲染的位置是没法被控制的,因而 icestark 为子应用提供了一个 getMountNode()
的 API 保证子应用可以渲染到指定的节点里。
在咱们内部使用时其实并无考虑这个问题,由于咱们内部目前都是 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 的子应用有三个须要定制的地方:
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'))); 复制代码
// 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> ); }; 复制代码
完整的文档请参考 子应用开发与迁移 。
说到微前端必定逃不了沙箱的相关话题,可是针对这个问题目前业界尚未一个很是完美的机制,具体可参考文档 样式和脚本隔离,这里面有咱们的一些探索。
针对这个问题咱们的一些观点:
icestark 与 single-spa 都属于微前端的解决方案,二者在能力上并没有太大差异,这里简单梳理下我的的一些观点:
另外 qiankun 是对 single-spa 的一层封装,核心作了构建层面的一些约束以及沙箱能力,构建层面的约束(好比 umd)我的以为会让子应用变复杂,不必定是一个好的方案,而后沙箱这块 icestark 是将 onAppEnter/onAppLeave
这种钩子暴露给框架应用,让业务自身去按需作一些好比全局变量冻结之类的事情。
icestark 目前主要落地在阿里内部的业务,社区里可能也有几个项目,不过目前尚未作过专门的统计,若是有用到的话欢迎反馈给咱们。
包含 20+ 子应用,其中 5-8 个子应用由二方业务开发。
面向淘系运营小二的后台都将已子应用的方式接入小二工做台,打造面向运营小二的操做系统。
微前端当下主要仍是在解决工程问题,好比系统的解耦、多人协做之类的,因此其实去看下核心代码都是很是简单易懂的。在工程问题的基础上接下来咱们会有两个方向:第一是探索沙箱机制,让二方业务更加安全的运行,同时让不可控的三方业务接入逐渐成为可能;第二针对微前端的业务场景逐步完善生态,好比一些鉴权之类的业务需求,这块有需求欢迎反馈。
最后欢迎评论交流 & 点赞 & star,以及经过钉钉群跟咱们沟通。