Open zhangsanshi opened 5 years ago
微前端,并不是一个新的技术名词,在 2016 年就被提出来了,具体可以参见链接。看过文章会发现,它是由后端微服务启发而提出来的。有人对它下了一个定义,使用不同 JavaScript 框架为多个团队构建现代 Web 应用程序的技术,策略和方法。根据这个定义,一方面可以看出它不是一个新技术,仅仅是一个新理念,另一方面我们会发现,在平时的工程实践中,可能已经做过相关的方案。本文主要讲的是,网易云控制台(简称:控制台)是如何实践微前端的。
优点:
缺点:
控制台前端由 30+ 模块组成,一共有 700+ 页面,由前端团队维护。最近组织架构调整,希望由后端业务团队维护自身的模块,并且提出了一些要求,整理如下:
微前端的实现方案有多种多样,其实方案之间也没有优劣之分,合适的才是最好的(方案前面文章有讲)。这里控制台使用的方案,是运行时通过 javascript 集成(在页面中引用所有模块的 script url)。下面简单讲一下,选择这种方案的原因。
javascript
script url
iframe
上面讲到模块间的依赖很复杂,那么对于控制台来说,模块间真正的依赖是什么。组件实现?接口信息?配置信息?路由?这里给出的答案是:全局模块需要模块的路由和配置信息用于初始化整个项目。而模块间的组件实现、接口信息可以有一定的冗余。所以,模块间的解耦集中在向全局提供其路由和配置信息,同时将对全局文件的引入改成访问全局变量的形式,至于模块间的,暂时可以不处理。
全局模块是否可以再次拆分依赖?如果考虑到复用,肯定需要二次拆分依赖。所以这里又将全局依赖划分为两部分,一部分被称为微前端解决方案,另一部分是控制台全局模块(业务相关)。
微前端解决方案包含了:vue 、cloud-ui(组件库)、vue-router 等 npm 包 以及页面启动引导器(这里暴露的全局变量为 mv),不含任何业务代码,访问相关属性均通过全局变量的方式暴露。其中为了防止用户修改 mv 的内容,使用了一个小窍门。另一方面为了大家使用时不需要看说明文档,就利用 typescript 写了相关的声明文件,通过 vscode 之类的编辑器,就可以做到自动提示。相关代码如下所示:
vue
cloud-ui
vue-router
npm
mv
typescript
vscode
// 使用 mv 下面的方法,在业务模块使用 mv.util('cookie') // 获取 util 这个命名空间下的 cookie 方法集
/** 获取全局定义 util */ function util(service: string): object; function util(service: 'cookie'): cookie; // 重载 interface cookie { /** * 设置 cookie * @param name cookie 名 * @param value cookie 值 * @param days 过期时间,天 */ setCookie(name:string, value:string, days:number): void; /** * 根据 cookie 名,获取 cookie * @param name cookie 名 * @return 不存在,返回 null,否则返回 cookie。 */ getCookie(name:string): null|string; }
控制台全局模块,包含了一些全局组件,方法等等,同时负责在合适的时机初始化路由、模块间的配置,最终由它真正启动整个项目。同时也会有一些方法,也需要和上述 cookie 类似的暴露方式,以及相关文档的编写。
cookie
模块的 entry 文件,它是很浅的一层,不含有任何业务代码(业务代码通过二次异步加载)。
entry
// vpc/index.js mv.module('vpc', { routes, config, });
写了一个脚本,用于模块内替换全局引入,如下:
import cookie from '@/utils/cookie'; // 会被脚本替换成 // const cookie = mv.util('cookie');
伪全局组件、方法降级,这里纯粹的人工活,简单进行分辨即可
在 webpack 里,定义多个 entry, 同时将 commonChunk 的配置删去,因为独立部署的原因,要求代码互不影响,所以必须删去 commonChunk。
webpack
commonChunk
{ entry: { vpc: 'src/module/vpc/index.js', main: 'src/module/main/index.js', bootstrap: 'src/module/bootstrap/index.js', }, }
打包策略调整,需要在打包时指定打哪些 entry,比如说 vpc 团队,只需要打包 vpc 这个 entry 即可。原前端团队需要打包 bootstrap、main 这俩 entry。
vpc
bootstrap
main
页面引入脚本方式调整,这里就不展开了。
发布工具链整合,全自动化。
通过上述的拆分,页面也是可以跑起来的,但这里需要考虑另一个问题。在前后端不分离部署的前提下,更新一个模块,后端就重启一次。在极端情况下,页面可能会挂掉数个小时,所以前后端必须部署分离。问题就转换成如何分离部署,一般的方案可能有如下两种:
全局的后端提供相关接口,同时注入相关变量到模板中。假如 vpc 模块有更新,就调用相关接口通知全局的模块,当用户刷新页面的时候,即可看到新模块。
在某个地方存放版本信息,如 https://xxx.com/vpc.js。同时在页面引入 <script src="https://xxx.com/vpc.js?t=timestamp"></script>。
https://xxx.com/vpc.js
<script src="https://xxx.com/vpc.js?t=timestamp"></script>
// vpc.js loadStatic({ js: [`hash.js`], css: [`hash.css`], });
控制台这里一开始选择的是第一种方案,但是这种方案有一个隐藏的坑,有些线上环境对网络的安全性有要求,外网无法访问相关环境的接口,幸好打包方案有考虑此种情况,最终降级上线。第二种方案需要加载的 script 太多了,性能堪忧。所以结合两种方案的优点,设计了第三种方案,后面成功上线所有环境。
script
存在一个中介服务,它会生成一个名字叫 staticInfo.js 的文件放在 nos 上,同时页面引入<script src="https://nos.com/staticInfo.js?t=timestamp"></script>。每当模块有更新,中介服务就会更新 staticInfo.js 中相关模块的信息。当用户刷新页面,该模块的代码就是最新的了。这样就做到了模块更新和代码引入相互解耦,同时和公共网络的第三方进行交流,就不存在网络不通的问题,同时将加载的脚本数量降为 1。
staticInfo.js
nos
<script src="https://nos.com/staticInfo.js?t=timestamp"></script>
// staticInfo.js loadStatic({ vpc: { js: [`hash.js`], css: [`hash.css`], }, main: { js: [`hash.js`], css: [`hash.css`], }, });
[entry].hash.js
chunk.[entry].hash.js
可以看到加载是分成四个层级,在第三层级用户就可以看到页面。但还是会对页面性能有一定的影响,后续可以对相关代码进行优化调整
code review
网易云控制台微前端探索实践
1. 前言
微前端,并不是一个新的技术名词,在 2016 年就被提出来了,具体可以参见链接。看过文章会发现,它是由后端微服务启发而提出来的。有人对它下了一个定义,使用不同 JavaScript 框架为多个团队构建现代 Web 应用程序的技术,策略和方法。根据这个定义,一方面可以看出它不是一个新技术,仅仅是一个新理念,另一方面我们会发现,在平时的工程实践中,可能已经做过相关的方案。本文主要讲的是,网易云控制台(简称:控制台)是如何实践微前端的。
2. 微前端的理解
优点:
缺点:
3. 背景
控制台前端由 30+ 模块组成,一共有 700+ 页面,由前端团队维护。最近组织架构调整,希望由后端业务团队维护自身的模块,并且提出了一些要求,整理如下:
4. 方案
4.1. 方案选择
微前端的实现方案有多种多样,其实方案之间也没有优劣之分,合适的才是最好的(方案前面文章有讲)。这里控制台使用的方案,是运行时通过
javascript
集成(在页面中引用所有模块的script url
)。下面简单讲一下,选择这种方案的原因。iframe
的方案,没有办法共享模块间的配置,而此类信息不适合全局模块收集,必须由相关模块暴露。4.2. 方案解析
上面讲到模块间的依赖很复杂,那么对于控制台来说,模块间真正的依赖是什么。组件实现?接口信息?配置信息?路由?这里给出的答案是:全局模块需要模块的路由和配置信息用于初始化整个项目。而模块间的组件实现、接口信息可以有一定的冗余。所以,模块间的解耦集中在向全局提供其路由和配置信息,同时将对全局文件的引入改成访问全局变量的形式,至于模块间的,暂时可以不处理。
全局模块是否可以再次拆分依赖?如果考虑到复用,肯定需要二次拆分依赖。所以这里又将全局依赖划分为两部分,一部分被称为微前端解决方案,另一部分是控制台全局模块(业务相关)。
微前端解决方案包含了:
vue
、cloud-ui
(组件库)、vue-router
等npm
包 以及页面启动引导器(这里暴露的全局变量为mv
),不含任何业务代码,访问相关属性均通过全局变量的方式暴露。其中为了防止用户修改mv
的内容,使用了一个小窍门。另一方面为了大家使用时不需要看说明文档,就利用typescript
写了相关的声明文件,通过vscode
之类的编辑器,就可以做到自动提示。相关代码如下所示:控制台全局模块,包含了一些全局组件,方法等等,同时负责在合适的时机初始化路由、模块间的配置,最终由它真正启动整个项目。同时也会有一些方法,也需要和上述
cookie
类似的暴露方式,以及相关文档的编写。模块的
entry
文件,它是很浅的一层,不含有任何业务代码(业务代码通过二次异步加载)。写了一个脚本,用于模块内替换全局引入,如下:
伪全局组件、方法降级,这里纯粹的人工活,简单进行分辨即可
在
webpack
里,定义多个entry
, 同时将commonChunk
的配置删去,因为独立部署的原因,要求代码互不影响,所以必须删去commonChunk
。打包策略调整,需要在打包时指定打哪些
entry
,比如说vpc
团队,只需要打包vpc
这个entry
即可。原前端团队需要打包bootstrap
、main
这俩entry
。页面引入脚本方式调整,这里就不展开了。
发布工具链整合,全自动化。
4.3. 前后端分离部署
通过上述的拆分,页面也是可以跑起来的,但这里需要考虑另一个问题。在前后端不分离部署的前提下,更新一个模块,后端就重启一次。在极端情况下,页面可能会挂掉数个小时,所以前后端必须部署分离。问题就转换成如何分离部署,一般的方案可能有如下两种:
全局的后端提供相关接口,同时注入相关变量到模板中。假如
vpc
模块有更新,就调用相关接口通知全局的模块,当用户刷新页面的时候,即可看到新模块。在某个地方存放版本信息,如
https://xxx.com/vpc.js
。同时在页面引入<script src="https://xxx.com/vpc.js?t=timestamp"></script>
。控制台这里一开始选择的是第一种方案,但是这种方案有一个隐藏的坑,有些线上环境对网络的安全性有要求,外网无法访问相关环境的接口,幸好打包方案有考虑此种情况,最终降级上线。第二种方案需要加载的
script
太多了,性能堪忧。所以结合两种方案的优点,设计了第三种方案,后面成功上线所有环境。存在一个中介服务,它会生成一个名字叫
staticInfo.js
的文件放在nos
上,同时页面引入<script src="https://nos.com/staticInfo.js?t=timestamp"></script>
。每当模块有更新,中介服务就会更新staticInfo.js
中相关模块的信息。当用户刷新页面,该模块的代码就是最新的了。这样就做到了模块更新和代码引入相互解耦,同时和公共网络的第三方进行交流,就不存在网络不通的问题,同时将加载的脚本数量降为 1。4.4. 整体流程梳理
staticInfo.js
,找到bootstrap
模块的信息,进行加载bootstrap
加载所有模块的[entry].hash.js
[entry].hash.js
加载完毕,再加载main
模块main
加载完毕后启动整个项目,根据路由的不同,再加载chunk.[entry].hash.js
。可以看到加载是分成四个层级,在第三层级用户就可以看到页面。但还是会对页面性能有一定的影响,后续可以对相关代码进行优化调整
4.5. 方案说明
npm
包的方式引入code review
工作模式,后续可以将工作交由模块业务方进行。5. 参考文档