网易七鱼是提供围绕客户服务与智能营销的 SaaS 平台。在七鱼业务中,有在线系统、呼叫系统、机器人、工单系统、数据大屏等业务线,它们分布在两个业务端,管理端和客服端。这两个端的功能框架相似,都是由外层框架(顶部导航、一级菜单)及中间的内容区组成。css
随着业务体量的增大与功能的增多,主系统做为一个巨石应用复杂度愈来愈高,全部的业务线耦合在一块儿,在系统构建、业务分离、开发维护方面带来了新的挑战。html
为解决以上问题,咱们最初采用了 「MPA + iframe」 的技术方案。先按业务维度从巨型单体应用中拆分出多个子应用,并用 React 技术栈对它们进行了重构,经过 iframe 的方式隔离新老技术栈。这些子应用基于 URL 解耦,每一个子应用能够独立开发、运行和部署。前端
采用「MPA + iframe」 的技术方案是一把双刃剑,用它能够较方便地解决现有的问题,但同时也带来了一些新的问题。react
用 MPA 方案能够容许子应用使用不一样技术栈,父子应用之间自然隔离,可是浏览器页面跳转时不能保持单页应用的流畅体验,父子应用通讯困难。webpack
用 iframe 能够方便地隔离新老技术栈,可是也带来了一些问题:git
问题 | 举例 | 较好的解决方案 |
---|---|---|
父子框架 URL 不一样步、浏览器前进后退按钮异常 | -- | 定义父子框架路由映射,利用 postMessage 和 history API 解决 |
父子框架 UI 不一样步 | 遮罩层只能遮盖 iframe 所在的区域、iframe 内的弹框没法相对外层页面居中 | 无 |
子框架的全局上下文与父框架彻底隔离,致使父子框架通讯困难、同步数据冗余 | -- | 无 |
加载慢,体验较差 | -- | 无 |
项目最开始时采用的开发框架是 NEJ(Nice Easy Javascript),它的依赖管理系统、控件系统等特性为早期的项目开发作出了很大的贡献,如今它完成了本身的历史使命,项目开始向 React 技术栈过渡。github
下图展现了应用框架现状:web
能够看到,整个系统中使用了 NEJ 和 React 两套技术栈。npm
React 外层框架内部嵌入的是 React 应用,这些应用分别引用了各自的外层框架,并经过 React 业务组件库复用。json
NEJ 外层框架内部的状况则比较复杂,部分场景嵌入的是 NEJ 应用,还有部分场景是经过 iframe 嵌入的 React 应用,这些 React 应用中的部分页面中也有经过 iframe 再次嵌入 NEJ 应用的场景。
由于 NEJ 老技术栈的组件支持匮乏,并且历史遗留代码较多,致使它们的开发和维护成本都很高。
目前前端工程正处于技术栈统一的过渡期,须要维护两套外层框架,后续将逐渐由 NEJ 转向 React。对于新增的应用,则直接采用 React 技术栈。
随着新应用的增多,外层框架被引用的次数愈来愈多,每次更新都须要发布多个应用,使用新技术栈外层框架的维护成本为愈来愈高。
微前端是目前比较火的话题,它是微服务在前端领域的扩展。它将前端总体拆分为多个更小、更易管理的片断,能够解决工程复杂度高、多技术栈共存、开发维护困难等问题。微前端的两大特性,微应用技术栈无关,每一个微应用能够独立开发、运行和部署,能够很好的匹配现有的业务场景。
所以咱们将目光转到了对现有应用进行微前端改造上。
将现有的应用进行微前端改造能够带来如下好处:
社区内的微前端解决方案有许多种,包括:
综合考虑业务场景、上手难度、文档友好性、代码入侵性、可维护性等方面,最终选择的微前端解决方案是 qiankun。接下来就是基于 qiankun 的微前端改造了。
七鱼的微前端改造,从技术层面涉及到 React、NEJ 两类技术栈,从业务层面涉及到管理端、客服端。
由于最终目的是全部前端工程统一到 React 技术栈,而管理端部分应用的外层框架已经用 React 重构过,因此先从管理端下手。
首先分别重新、老技术栈应用中选取一个应用进行改造,积累相关经验。应用选择的标准是无复杂的业务逻辑、流量少,以下降改造风险。新技术栈应用选的是首页应用,老技术栈应用选的是数据大屏应用。
来看一下七鱼微前端改造后的主页:
这里说明两个概念,基座应用(也称为主应用、框架应用等)和子应用(也称为微应用):
**
能够看到,上图用红框标出了主页的两个组成部分,外层框架(顶部导航、一级菜单)和中间内容区。
外层框架就是由基座应用控制的,经过监听 URL 进行路由分发、子应用调度等。内容区由一个或多个子应用控制,上图中的内容区就是由一个首页子应用控制的。
建立管理端基座工程 basic-admin;
const webpackConfig = { //... output: { //... library: `${packageName}-[name]`, // 此处的packageName为子应用名,如micro-bigscreen libraryTarget: 'umd', jsonpFunction: `webpackJsonp_${packageName}`, } };
新增微应用对应的内部路由,改造网关:
兼容七鱼 PC 客户端(低版本 Chrome 浏览器内核):
registerMicroApps( [ { name: 'micro-index', entry: '//' + location.hostname + '/_MicroIndex', container: '#subapp-container', activeRule: '/madmin/home', }, { name: 'micro-bigscreen', entry: '//' + location.hostname + '/_MicroBigscreen/index', container: '#subapp-container', activeRule: '/madmin/dashboard', } ] );
微前端改造后,全部管理端相关子应用的 URL 前缀为「/madmin/」,如主页的 URL 为「/madmin/home/」。服务网关须要将全部以「/madmin/」开头的路由定向到管理端基座应用。
结合网关的微前端架构图以下:
子应用有独立的仓库,部署完以后,将应用的发布产物注册到基座应用里,这些产物能够是子应用的访问地址,也能够是资源配置对象(scripts + styles + html)。
须要注意的是,在子应用与基座应用开发联调时,子应用读取的是基座应用的同步数据,Mock 的同步数据须要在基座应用中配置。同理,子应用用到的接口代理也须要在基座应用中配置全。
基座应用启动后会监听 URL 变化,当用户访问系统时,根据当前访问的 URL 和注册的路由信息,可以匹配到当前须要加载的子应用信息,而后去加载子应用的资源并渲染子应用。
当用户点击触发跳转时,若是路由变化触发的是一个内部 URL 跳转,会直接根据应用内部的路由逻辑渲染页面。若是路由变化触发的是跨应用的跳转,则从新回到上面的路由匹配的流程中。
下图是微前端改造后的应用框架:
按照上述的子应用改造过程,能够逐步完成管理端的微前端改造。接下来就是对客服端的微前端改造了。
虽然客服端与管理端的框架结构相似,可是它们的 URL 是解耦的,并且它们一级菜单和顶部导航的业务功能差异较大,共用同一个基座应用会致使应用复杂度太高,最好是另外建立一个客服端专用的基座应用,两个基座应用经过业务组件库复用组件。
将来总体的应用框架以下:
有了微前端的助力,整个系统能够更加平滑地进行技术栈升级,最终实现前端技术栈的统一,更高效地赋能业务发展。
babel-polyfill 不支持引用屡次(基座应用和子应用分别引用了一次),直接去除 babel-polyfill 会致使没法单独运行子应用,能够改用 idempodent-babel-polyfill。
资源路径有问题,须要配置运行时的 public path。
if (window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } else { __webpack_public_path__ = window.location.protocol + "//" + window.location.host + "/"; }
将 sandbox 设置为 strictStyleIsolation,会启用严格的样式隔离,原理是把子应用内容渲染到基座容器的 shadow dom 中,致使没法直接获取基座应用的 dom 元素。
取消 strictStyleIsolation,只设置 jsSandBox 为 true 就不会有问题。
样式隔离的最佳实践是采用约定式隔离:用 CSS 命名空间、CSS Module、css-in-js 等工程化手段,避免写全局样式。
开发环境使用 browserSync 进行浏览器同步,qiankun 框架经过浏览器的 fetch API 获取子应用的资源,会存在跨域问题,因此须要设置 cors
为 true。
browserSync({ //... cors: true });
添加条件判断,非 qiankun 环境下,走以前的运行环境。
修改 'entry.js' 的 render 条件:
if (!window.__POWERED_BY_QIANKUN__) { ReactDOM.render( <Root store={store} history={history} routes={routes}/>, document.getElementById('react-content') ); }
使用 ScriptExtHtmlWebpackPlugin 插件修改 webpack 配置,为每一个页面的入口 js 加 entry 属性。
tplPlugins.push( new ScriptExtHtmlWebpackPlugin({ custom: { test: /(?<!vendors.*)entry\.js$/, attribute: 'entry' } } ));
在子应用与基座应用开发联调时,子应用读取的是基座应用的全局配置。本地环境基座应用可能接入不少子应用,其余子应用用到的接口代理要配全,不然调不到接口。同理,Mock 的同步数据也要在基座应用配置全。
qiankun 框架经过浏览器的 fetch API 获取子应用的资源。Chrome 内核71及以前的版本,即便网址与调用脚本同源,fetch API 也不会自动发送 cookie。
须要在基座应用中启动应用时,对 fetch 进行显式的参数配置:
qiankun.start({ //... fetch: (url, init) => { return window.fetch(url, { ...init, credentials: 'same-origin' // 在当前域名内自动发送 cookie }); } });
定义一个与子应用名称一致的全局变量,生命周期钩子函数必须返回 promise,若是不支持 promise 须要引入 promise-polyfill。入口文件能够这样写:
(function(win) { // 此处的'micro-bigscreen'与注册到基座应用的子应用名称一致 win['micro-bigscreen'] = { bootstrap: function() { // 必须返回promise,不然子应用没法正常启动 return Promise.resolve(); }, mount: function() { return Promise.resolve(); }, unmount: function() { return Promise.resolve(); } }; })(window);
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。
基座应用中的相关代码:
const polyfillPcPlatform = () => { if (window.cefQuery) { Object.defineProperty(window, 'cefQuery2', { value: window.cefQuery, writable: true, configurable: true }); } if (window.cefQueryCancel) { Object.defineProperty(window, 'cefQueryCancel2', { value: window.cefQueryCancel, writable: true, configurable: true }); } }; //注册子应用 registerMicroApps( [ //... ], { beforeLoad: [ app => { // 兼容PC客户端 polyfillPcPlatform(); } ], //... } );
本次微前端实践基于 qiankun 框架,建立了管理端基座应用,将管理端首页和数据大屏应用进行了微前端改造,改造涉及 React 和 NEJ 两套技术栈,达到了如下目的:
微前端不是一个框架,而是一套架构体系,基座应用的建立和子应用的改造是它的基础设施,除了基础设施外还有配置中心和观察工具。配置中心包括参数配置、版本管理、发布策略等。观察工具备必定的运维职能,包括应用状态的可见、可控性等。
有了上述能力后,能够经过它们统一管控全部的微应用,为 SaaS 产品提供自由组合的能力,使技术为业务带来更大的价值。
更多技术内容,欢迎关注【网易智企技术+】公众号。