微前端在网易七鱼的实践 谁践踏了优雅 2023-01-04 15:57 231阅读 0赞 ## 一、前言 ## 网易七鱼是提供围绕客户服务与智能营销的 SaaS 平台。在七鱼业务中,有在线系统、呼叫系统、机器人、工单系统、数据大屏等业务线,它们分布在两个业务端,管理端和客服端。这两个端的功能框架类似,都是由外层框架(顶部导航、一级菜单)及中间的内容区组成。 ## 二、业务现状 ## 随着业务体量的增大与功能的增多,主系统作为一个巨石应用复杂度越来越高,所有的业务线耦合在一起,在系统构建、业务分离、开发维护方面带来了新的挑战。 为解决以上问题,我们最初采用了 「**MPA + iframe**」 的技术方案。先按业务维度从巨型单体应用中拆分出多个子应用,并用 React 技术栈对它们进行了重构,通过 iframe 的方式隔离新老技术栈。这些子应用基于 URL 解耦,每个子应用可以独立开发、运行和部署。 采用「MPA + iframe」 的技术方案是一把双刃剑,用它可以较方便地解决现有的问题,但同时也带来了一些新的问题。 用 **MPA** 方案可以允许子应用使用不同技术栈,父子应用之间天然隔离,但是浏览器页面跳转时不能保持单页应用的流畅体验,父子应用通信困难。 用 **iframe** 可以方便地隔离新老技术栈,但是也带来了一些问题: <table> <thead> <tr> <th>问题</th> <th>举例</th> <th>较好的解决方案</th> </tr> </thead> <tbody> <tr> <td>父子框架 URL 不同步、浏览器前进后退按钮异常</td> <td>--</td> <td>定义父子框架路由映射,利用 postMessage 和 history API 解决</td> </tr> <tr> <td>父子框架 UI 不同步</td> <td>遮罩层只能遮盖 iframe 所在的区域、iframe 内的弹框无法相对外层页面居中</td> <td>无</td> </tr> <tr> <td>子框架的全局上下文与父框架完全隔离,导致父子框架通信困难、同步数据冗余</td> <td>--</td> <td>无</td> </tr> <tr> <td>加载慢,体验较差</td> <td>--</td> <td>无</td> </tr> </tbody> </table> 项目最开始时采用的开发框架是 [NEJ(Nice Easy Javascript)][NEJ_Nice Easy Javascript],它的依赖管理系统、控件系统等特性为早期的项目开发做出了很大的贡献,现在它完成了自己的历史使命,项目开始向 React 技术栈过渡。 下图展示了应用框架现状: ![应用框架现状][1cf3fdf16e948533371cb912b8c9b586.png] 可以看到,整个系统中使用了 **NEJ** 和 **React** 两套技术栈。 React 外层框架内部嵌入的是 React 应用,这些应用分别引用了各自的外层框架,并通过 React 业务组件库复用。 NEJ 外层框架内部的情况则比较复杂,部分场景嵌入的是 NEJ 应用,还有部分场景是通过 iframe 嵌入的 React 应用,这些 React 应用中的部分页面中也有通过 iframe 再次嵌入 NEJ 应用的场景。 因为 NEJ 老技术栈的组件支持匮乏,而且历史遗留代码较多,导致它们的开发和维护成本都很高。 目前前端工程正处于技术栈统一的过渡期,需要维护两套外层框架,后续将逐渐由 NEJ 转向 React。对于新增的应用,则直接采用 React 技术栈。 随着新应用的增多,外层框架被引用的次数越来越多,每次更新都需要发布多个应用,使用新技术栈外层框架的维护成本为越来越高。 微前端是目前比较火的话题,它是微服务在前端领域的扩展。它**将前端整体拆分为多个更小、更易管理的片段**,可以解决**工程复杂度高、多技术栈共存、开发维护困难**等问题。微前端的**两大特性**,**微应用技术栈无关,每个微应用可以独立开发、运行和部署**,可以很好的匹配现有的业务场景。 因此我们将目光转到了对现有应用进行微前端改造上。 ## 三、微前端改造 ## ### 改造的好处 ### 将现有的应用进行微前端改造可以带来以下好处: * 积累实践经验,为将来从巨石应用拆分及微前端改造做准备; * 去除接入二方应用时使用的 iframe,优化产品体验; * 收敛外层框架,提升研发效率,降低维护成本; * 提供前端增量升级能力,后续可以更好地复用历史代码、实施渐进式重构; 社区内的微前端解决方案有许多种,包括: * [Single-spa][]:只解决了应用之间的加载方案,没有考虑其他的周边问题; * [qiankun][]:基于 single-spa,提供了更加**开箱即用的 API**,具备 **JS 沙箱、样式隔离、子应用并行**等能力; * [Icestark][]:约束了框架应用必须基于 React,不利于后续的技术栈优化; * [Magix][]:适合做单页应用的项目,不支持多个实例,不满足业务需求; * [Luigi][]:是一个基于 iframe 的微前端框架,仍有前文提到的 iframe 带来的产品体验问题; * [Ara Framework][]:是一个基于 [Airbnb's Hypernova][Airbnb_s Hypernova] 的,由服务端渲染延伸出的微前端框架,接入时对原应用的侵入较多; * [WidgetJS][]:是一个轻量级的微前端方案,文档不够友好; 综合考虑**业务场景、上手难度、文档友好性、代码入侵性、可维护性**等方面,最终选择的微前端解决方案是 qiankun。接下来就是基于 qiankun 的微前端改造了。 ### 业务分析与改造效果 ### 七鱼的微前端改造,从技术层面涉及到 React、NEJ 两类技术栈,从业务层面涉及到管理端、客服端。 因为最终目的是所有前端工程统一到 React 技术栈,而管理端部分应用的外层框架已经用 React 重构过,所以先从管理端下手。 首先分别从新、老技术栈应用中选取一个应用进行改造,积累相关经验。应用选择的标准是无复杂的业务逻辑、流量少,以降低改造风险。新技术栈应用选的是首页应用,老技术栈应用选的是数据大屏应用。 来看一下七鱼微前端改造后的主页: ![七鱼微前端改造后主页][f1a2d98e3892e01a401ade6bfd7f8fff.png] 这里说明两个概念,**基座应用(也称为主应用、框架应用等)和子应用(也称为微应用):** \*\* * **基座应用负责整体布局、子应用的配置和调度,**一般包含各个子应用公有的部分,比如外层框架; * **子应用负责自身业务逻辑的渲染;** 可以看到,上图用红框标出了主页的两个组成部分,外层框架(顶部导航、一级菜单)和中间内容区。 外层框架就是由基座应用控制的,**通过监听 URL 进行路由分发、子应用调度等**。内容区由一个或多个子应用控制,上图中的内容区就是由一个首页子应用控制的。 ### 大致的改造步骤 ### 1. 创建管理端基座工程 basic-admin; * 基座应用只包含各个子应用共有的部分; 2. 创建首页子工程 micro-index、大屏子工程 micro-bigscreen,以及相应的应用和集群; 3. 在项目的入口文件里,暴露相应的生命周期钩子,供 qiankun 识别; 4. 修改打包配置,使物料以 umd 的方式输出,以 webpack 为例: 1. `const webpackConfig = {` 2. ` //...` 3. ` output: {` 4. ` //...` 5. `` library: `${packageName}-[name]`, // 此处的packageName为子应用名,如micro-bigscreen`` 6. ` libraryTarget: 'umd',` 7. `` jsonpFunction: `webpackJsonp_${packageName}`,`` 8. ` }` 9. `};` 5. 新增微应用对应的内部路由,改造网关: * 内部路由用于注册子应用,正常情况下用户无法直接访问到; * 改造后的网关需要将所有匹配到基座 URL 前缀的请求,都定向到基座应用; 6. 兼容七鱼 PC 客户端(低版本 Chrome 浏览器内核): * qiankun 加载资源时依赖的 fetch API 的兼容性问题; * 因为 height 继承等导致的样式问题; 7. 在基座应用中调用 qiankun 的 API,将子应用注册到基座应用,如: 1. `registerMicroApps(` 2. `[` 3. ` {` 4. ` name: 'micro-index',` 5. ` entry: '//' + location.hostname + '/_MicroIndex',` 6. ` container: '#subapp-container',` 7. ` activeRule: '/madmin/home',` 8. ` },` 9. ` {` 10. ` name: 'micro-bigscreen',` 11. ` entry: '//' + location.hostname + '/_MicroBigscreen/index',` 12. ` container: '#subapp-container',` 13. ` activeRule: '/madmin/dashboard',` 14. ` }` 15. `]` 16. `);` ## 四、微前端架构下的业务变化 ## ### 服务网关的变化 ### 微前端改造后,所有管理端相关子应用的 URL 前缀为「/madmin/」,如主页的 URL 为「/madmin/home/」。**服务网关需要将所有以「/madmin/」开头的路由定向到管理端基座应用。** 结合网关的微前端架构图如下: ![微前端架构图][006cc61c59af79aa4ffb444d0fbb8703.png] ### 子应用的开发模式 ### 子应用有独立的仓库,部署完之后,**将应用的发布产物注册到基座应用里**,这些产物可以是子应用的访问地址,也可以是资源配置对象(scripts + styles + html)。 需要注意的是,在子应用与基座应用开发联调时,子应用读取的是基座应用的同步数据,Mock 的同步数据需要在基座应用中配置。同理,子应用用到的接口代理也需要在基座应用中配置全。 ### 基座应用的整体流程 ### 基座应用启动后会**监听 URL 变化**,当用户访问系统时,根据当前访问的 URL 和注册的路由信息,能够**匹配到当前需要加载的子应用信息**,然后去加载子应用的资源并**渲染子应用**。 当用户点击触发跳转时,如果路由变化触发的是一个内部 URL 跳转,会直接根据应用内部的路由逻辑渲染页面。如果路由变化触发的是跨应用的跳转,则重新回到上面的路由匹配的流程中。 下图是微前端改造后的应用框架: ![微前端改造后的应用框架][c7a79987b2bbe7d144cf55ca2331c41c.png] 按照上述的子应用改造过程,可以逐步完成管理端的微前端改造。接下来就是对客服端的微前端改造了。 虽然客服端与管理端的框架结构类似,但是它们的 URL 是解耦的,而且它们一级菜单和顶部导航的业务功能差别较大,共用同一个基座应用会导致应用复杂度过高,最好是另外创建一个客服端专用的基座应用,两个基座应用通过业务组件库复用组件。 未来整体的应用框架如下: ![未来整体的应用框架][a37216d33ebb58687031e6c33ac0d96e.png] 有了微前端的助力,整个系统可以更加平滑地进行技术栈升级,最终实现前端技术栈的统一,更高效地赋能业务发展。 ## 五、遇到的问题及解决方案 ## ### 1、子应用接入基座应用后,babel-polyfill 报错 ### babel-polyfill 不支持引用多次(基座应用和子应用分别引用了一次),直接去除 babel-polyfill 会导致无法单独运行子应用,可以改用 [idempodent-babel-polyfill][]。 ### 2、基座应用访问子应用资源报 404 错误 ### 资源路径有问题,需要配置运行时的 public path。 1. `if (window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__) {` 2. ` __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;` 3. `} else {` 4. ` __webpack_public_path__ = window.location.protocol + "//" + window.location.host + "/";` 5. `}` ### 3、报错提示找不到子应用容器 ### 将 sandbox 设置为 strictStyleIsolation,会启用严格的样式隔离,原理是把子应用内容渲染到基座容器的 shadow dom 中,导致无法直接获取基座应用的 dom 元素。 取消 strictStyleIsolation,只设置 jsSandBox 为 true 就不会有问题。 样式隔离的最佳实践是采用约定式隔离:用 **CSS 命名空间、CSS Module、css-in-js 等工程化手段**,避免写全局样式。 ### 4、本地联调时基座应用访问子应用资源时报跨域错误 ### 开发环境使用 browserSync 进行浏览器同步,qiankun 框架通过浏览器的 fetch API 获取子应用的资源,会存在跨域问题,所以需要设置 `cors` 为 true。 1. `browserSync({` 2. ` //...` 3. ` cors: true` 4. `});` ### 5、子应用引入 qiankun 生命周期后,无法独立运行 ### 添加条件判断,非 qiankun 环境下,走之前的运行环境。 修改 'entry.js' 的 render 条件: 1. `if (!window.__POWERED_BY_QIANKUN__) {` 2. ` ReactDOM.render(` 3. ` <Root store={store} history={history} routes={routes}/>, document.getElementById('react-content')` 4. `);` 5. `}` ### 6、本地联调时子应用因为有热加载导致报错 ### 使用 **ScriptExtHtmlWebpackPlugin** 插件修改 webpack 配置,为每个页面的入口 js 加 **entry** 属性。 1. `tplPlugins.push(` 2. ` new ScriptExtHtmlWebpackPlugin({` 3. ` custom: {` 4. ` test: /(?<!vendors.*)entry\.js$/,` 5. ` attribute: 'entry'` 6. ` }` 7. `}` 8. `));` ### 7、本地联调时子应用调用 Mock 接口或同步数据报错 ### 在子应用与基座应用开发联调时,子应用读取的是基座应用的全局配置。本地环境基座应用可能接入很多子应用,其他子应用用到的接口代理要配全,否则调不到接口。同理,Mock 的同步数据也要在基座应用配置全。 ### 8、低版本浏览器加载资源时 cookie 丢失 ### qiankun 框架通过浏览器的 fetch API 获取子应用的资源。Chrome 内核71及之前的版本,即使网址与调用脚本同源,fetch API 也不会自动发送 cookie。 需要在基座应用中启动应用时,对 fetch 进行显式的参数配置: 1. `qiankun.start({` 2. ` //...` 3. ` fetch: (url, init) => {` 4. ` return window.fetch(url, {` 5. ` ...init,` 6. ` credentials: 'same-origin' // 在当前域名内自动发送 cookie` 7. ` });` 8. `}` 9. `});` ### 9、非 React 环境引入 qiankun 生命周期的方式 ### 定义一个与子应用名称一致的全局变量,生命周期钩子函数必须返回 promise,如果不支持 promise 需要引入 promise-polyfill。入口文件可以这样写: 1. `(function(win) {` 2. ` // 此处的'micro-bigscreen'与注册到基座应用的子应用名称一致` 3. ` win['micro-bigscreen'] = {` 4. ` bootstrap: function() {` 5. ` // 必须返回promise,否则子应用无法正常启动` 6. ` return Promise.resolve();` 7. ` },` 8. ` mount: function() {` 9. ` return Promise.resolve();` 10. ` },` 11. ` unmount: function() {` 12. ` return Promise.resolve();` 13. ` }` 14. ` };` 15. `})(window);` ### 10、PC 客户端子应用变量访问报错:Uncaught TypeError: 'get' on proxy ### PC 客户端注入了 window.cefQuery 与 window.cefQueryCancel 变量,它们的属性描述符中 writable 与 configurable 都为 false,经过 JS 沙箱 Proxy 后直接访问它们会报错:Uncaught TypeError: 'get' on proxy。 因为只有子应用用到了沙箱,此报错只会影响子应用,基座应用不受影响。 解决方法是:分别从 window.cefQuery 与 window.cefQueryCancel 复制出新的变量 window.cefQuery2 与 window.cefQueryCancel2,修改它们的属性描述符 writable 与 configurable 为 true。然后将微前端子应用中引用 window.cefQuery 与 window.cefQueryCancel 的地方分别修改为 window.cefQuery2 与 window.cefQueryCancel2。 基座应用中的相关代码: 1. `const polyfillPcPlatform = () => {` 2. ` if (window.cefQuery) {` 3. ` Object.defineProperty(window, 'cefQuery2', {` 4. ` value: window.cefQuery,` 5. ` writable: true,` 6. ` configurable: true` 7. ` });` 8. `}` 9. ` if (window.cefQueryCancel) {` 10. ` Object.defineProperty(window, 'cefQueryCancel2', {` 11. ` value: window.cefQueryCancel,` 12. ` writable: true,` 13. ` configurable: true` 14. ` });` 15. `}` 16. `};` 17. `` 18. `//注册子应用` 19. `registerMicroApps(` 20. `[` 21. ` //...` 22. `],` 23. `{` 24. ` beforeLoad: [` 25. ` app => {` 26. ` // 兼容PC客户端` 27. ` polyfillPcPlatform();` 28. ` }` 29. ` ],` 30. ` //...` 31. `}` 32. `);` ## 六、总结 ## 本次微前端实践基于 qiankun 框架,创建了管理端基座应用,将管理端首页和数据大屏应用进行了微前端改造,改造涉及 React 和 NEJ 两套技术栈,达到了以下目的: 1. 积累了微前端实践经验,为将来从巨石应用拆分及微前端改造做准备; 2. 使管理端不同技术栈的二方应用接入不再需要使用 iframe,优化了产品体验; 3. 收敛了管理端外层框架,使新应用的接入不再需要理会顶部导航和一级菜单; 4. 提供了前端增量升级能力,后续可以更好地复用历史代码、实施渐进式重构; **微前端不是一个框架,而是一套架构体系**,基座应用的创建和子应用的改造是它的基础设施,除了基础设施外还有**配置中心和观察工具**。配置中心包括**参数配置、版本管理、发布策略**等。观察工具有一定的运维职能,包括**应用状态的可见、可控性**等。 有了上述能力后,可以通过它们统一管控所有的微应用,为 SaaS 产品提供自由组合的能力,使技术为业务带来更大的价值。 转载:[https://blog.csdn.net/netease\_im/article/details/111274813?utm\_medium=distribute.pc\_feed.none-task-blog-personrec\_tag-21.nonecase&depth\_1-utm\_source=distribute.pc\_feed.none-task-blog-personrec\_tag-21.nonecase&request\_id=5ff63efee291a7315fd42fbd][https_blog.csdn.net_netease_im_article_details_111274813_utm_medium_distribute.pc_feed.none-task-blog-personrec_tag-21.nonecase_depth_1-utm_source_distribute.pc_feed.none-task-blog-personrec_tag-21.nonecase_request_id_5ff63efee291a7315fd42fbd] [NEJ_Nice Easy Javascript]: https://github.com/genify/nej [1cf3fdf16e948533371cb912b8c9b586.png]: /images/20221119/aee48ab55d374186823a5a8ae80eaa06.png [Single-spa]: https://single-spa.js.org/ [qiankun]: https://qiankun.umijs.org/zh [Icestark]: https://ice.work/docs/icestark/about [Magix]: http://thx.github.io/magix/#!/index [Luigi]: https://luigi-project.io/ [Ara Framework]: https://ara-framework.github.io/website/ [Airbnb_s Hypernova]: https://github.com/airbnb/hypernova [WidgetJS]: https://github.com/aliyun/alibabacloud-console-widget [f1a2d98e3892e01a401ade6bfd7f8fff.png]: /images/20221119/96053390d89c42e394b10c43bc0126d6.png [006cc61c59af79aa4ffb444d0fbb8703.png]: /images/20221119/0d629468966740369ab9a5a6fb6af5a5.png [c7a79987b2bbe7d144cf55ca2331c41c.png]: /images/20221119/a1d11a00bd2f46ec9d95776373579e58.png [a37216d33ebb58687031e6c33ac0d96e.png]: /images/20221119/54bf4390cdf5475299eac684592b2dc6.png [idempodent-babel-polyfill]: https://www.npmjs.com/package/idempotent-babel-polyfill [https_blog.csdn.net_netease_im_article_details_111274813_utm_medium_distribute.pc_feed.none-task-blog-personrec_tag-21.nonecase_depth_1-utm_source_distribute.pc_feed.none-task-blog-personrec_tag-21.nonecase_request_id_5ff63efee291a7315fd42fbd]: https://blog.csdn.net/netease_im/article/details/111274813?utm_medium=distribute.pc_feed.none-task-blog-personrec_tag-21.nonecase&depth_1-utm_source=distribute.pc_feed.none-task-blog-personrec_tag-21.nonecase&request_id=5ff63efee291a7315fd42fbd
还没有评论,来说两句吧...