随着业务发展,咱们的系统变得愈来愈庞大,给构建速度、静态资源大小以及应用性能带来了极大的挑战css
一个系统是由众多小模块组成的,大部分用户都不会拥有全部模块的权限,因此咱们的第一个优化方式就是 code split
,将每一个小模块的代码分割出来,按需加载,也取得了必定的效果html
然而,当系统数量愈来愈多时,用户开始抱怨入口太多,但愿由统一的入口来完成全部的功能,这个场景有几种解决方案前端
合并全部系统到一个大系统中webpack
作个应用框架,用 IFrame 嵌入目标系统git
开发统一导航栏,替换各系统的导航栏,在导航栏中经过 <a>
标签实现系统切换github
看上去
像在使用一个系统,如果用户切换的频率较高,则感觉更强烈localStorage/sessionStorage
等浏览器存储采用微前端架构,对应用进行改造web
再来梳理一下现状redis
全部系统都是基于内部的统一框架开发,拥有统同样式的顶部栏和侧边栏json
全部系统都拥有本身的 Nodejs 层,用于页面渲染和 API 请求转发bootstrap
全部系统都拥有不一样的域名,没有特定的域名子路径
不一样的系统有本身的小团队在开发,部分使用不一样版本的 React 和 Ant Design
开发广泛要求将来新功能模块的开发可使用与时俱进的技术
基于现状分析,微前端是一个能够去尝试的方向,因而便开始了踩坑之路,将现有的系统改形成为微前端的子应用
为了统一语言,现有的系统在下文称为子应用
咱们使用 qiankun 来做为微前端的实现库,(听说)能够快速实现改造
qiankun 是基于 single-spa 封装的,其内部实现的子应用加载机制,是基于浏览器 url 来实现的,经过第一段子路径来决定要加载哪一个子应用,好比
因此,为每一个子应用改造使得全部的访问都增长子路径,是咱们要作的第一步
为每一个路由增长前缀,koa 的代码示例以下
// 直接访问根路径,转发增长路由前缀 router.get('/', controller.redirect); // 渲染页面,这里的 authMiddleware 是校验中间件,实现登录校验逻辑 router.get('/appA/*', authMiddleware, controller.index); // 这里使用 ${子应用名} + '_apis' 来表示特定应用的 api 请求, // 方便在主应用中作区分进行转发,同时也方便 Nginx 配置转发(共享域名) router.use('/appA_apis/*', authMiddleware, controller.transfer); // 剩下的路由忽略 ... 复制代码
修改每一个在页面上的 api 请求,使之匹配 ${子应用名} + '_apis'
这一步相对比较麻烦,现有的子应用在页面代码中都写了 /apis
的前缀,若是不是在统一的地方处理的,改动起来会很是麻烦。基于现状,咱们用了一个取巧的方式:拦截全部 Ajax 请求,并根据须要修改其前缀。具体代码以下
(() => { if (!XMLHttpRequest.prototype['nativeOpen']) { XMLHttpRequest.prototype['nativeOpen'] = XMLHttpRequest.prototype.open; const customizeOpen = function(method, url, ...params) { if ( // 不须要修改前缀的请求,若是状况比较多,能够单独抽取出来 url.indexOf('hot-update.json') < 0 ) { // 将 /apis 前缀转化为 /appA_apis 前缀,这里是在框架里 // 处理成 routerPrefix 注入到 window 对象的 url = `${window['routerPrefix']}_${url.slice(1)}`; } this.nativeOpen(method, url, ...params); }; XMLHttpRequest.prototype.open = customizeOpen; } })(); 复制代码
修改静态文件的路径,修改原先的 /statics 路径,使之匹配 ${子应用名} + '_statics'
这一步大体就是 webpack 的配置了,主要是修改 output 和 publicPath 相关的配置,根据项目实际去操做便可,此处再也不赘述
通过以上步骤,子应用已经能够支持子路径的访问了,但这里还少了一步比较关键的,它不影响你的改造,可是会影响你改造以后用户的正常访问。好比,用户在收藏夹中保存了你的系统某个页面的地址,例如 xxx.site.com/pages/user
,此时若是你进行了部署,则会致使用户的访问出现 404,因此还须要在路由文件进行兼容
// 直接经过 URL 访问旧路由时,重定向到新的匹配路由,redirectToNewPrefix 的实现很简单,取出 ctx.url 而且替换掉原先的路由前缀便可 router.get('/pages/*', controller.redirectToNewPrefix); 复制代码
至此,咱们算是完成了 为子应用增长路由前缀
的工做。
参考官网,搭建一个最简单的主应用,只须要有一个用于挂载子应用的节点
<div id="subViewport"></div> 复制代码
而后调用 registerMicroApps
方法注册一会儿应用便可
registerMicroApps( [ { name: 'appA', entry: appAEntryMap[process.env.NODE_ENV], // 根据运行环境,加载应用对应的入口,如 'http://localhost:3000/appA' container: '#subViewport', activeRule: '/appA' }, { name: 'appB', // app name registered entry: appBEntryMap[process.env.NODE_ENV], container: '#subViewport', activeRule: '/appB' } ] ); setDefaultMountApp('/appA'); // 设置默认加载的应用,当路由匹配不到时会触发 start(); 复制代码
这里可能会出现 #subViewport
挂载的子应用没有占满容器的现象,查阅官方 issue,给出一个可解决的方案是经过 css 去控制,让该节点下渲染的子 div 占满容器(该 div 会注入 hash,故没法根据 id 或 class 去处理)
#subViewport {
width: 100%;
height: 100%;
> div {
width: 100%;
height: 100%;
}
}
复制代码
此步骤参考官方文档便可
另外,若是但愿子应用也能单独访问,则能够在入口 js 处增长代码
// 不是在 qiankun 框架中装载的时候,直接渲染 if (!window['__POWERED_BY_QIANKUN__']) { bootstrap().then(mount); } 复制代码
启动主应用,访问页面,发现一片空白,查看控制台,出现了跨域问题
Access to fetch at 'http://localhost:3000/appA' from origin 'http://localhost:4001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. 复制代码
qiankun 是使用 fetch 来获取子应用的 html 文件的,因此出现了跨域问题。处理起来也比较简单,因为自己是由 Nodejs 渲染出来的,只须要增长 koa2-cors
中间件便可解决问题
这里注意,若是是在开发模式下,须要 webpack-dev-server
也支持跨域,可参考 这篇文章
终于来到了很是关键的一个环节,API 请求的处理,这也是官网和 Demo 没有说起的环节,但倒是最重要的,决定了你的微前端改造是否成功
若是主应用、子应用以及后端 API 都是同一个域名,则自然地不用解决这个问题
如下方案都基于一个大前提:主应用、子应用都有各自的 Node 端处理页面渲染、登录校验和 API 转发工做
首先要清楚,qiankun 子应用在浏览器端发 api 请求时,其实是请求了主应用的 Node 端,url 为 /appA_apis/xxx
/appB_apis/xxx
这样的格式,而主应用的 Node 端是没有处理这些路由的逻辑的,故须要添加转发逻辑,把这些请求都转发到子应用的 Node 端去
先在主应用的配置文件添加子应用配置
subApps: [ { name: 'appA', prefix: '/appA_apis', // 子应用的 host,例如 http://localhost:3000 host: process.env['subApps.appA.host'] }, { name: 'appB', prefix: '/appB_apis', host: process.env['subApps.appB.host'] } ] 复制代码
而后在主应用的路由配置处,增长转发
subApps.forEach(subApp => { router.all(`${subApp.prefix}/*`, (ctx, next) => { // 转发请求到 `${subApp.host}/${ctx.url}`,注意参数要透传,content-type 也要保持一致,此处实现方式多种,不在此赘述 ... }) }) 复制代码
转发后会发现,API 请求在子应用的 Node 端没法经过校验,咱们先来看下 API 请求的校验过程
x-auth-token
(这个 key 是咱们的项目规定的,不是固定的)不难看出,主应用登录后生成的 x-auth-token
并无办法被子应用的 Node 端识别为有效的 session id
这里有两种作法
主应用和全部子应用共享同一个 session 存储,咱们项目用的是 redis,因此就是让全部应用共用同一个 redis
优势:简单粗暴,工做量较小
缺陷:共用存储可能会产生一些冲突,某一子应用的开发不注意时可能错误地覆盖掉其余子应用的关键数据;各子应用没法拥有特殊的用户信息(好比在 subA 的用户信息里面有一个主应用和其余子应用都没有的特别的字段)
子应用提供一个特殊的 SSO 接口,主应用在登录后,调用全部子应用的 SSO 接口并传输这个 x-auth-token
和加密后的用户帐号,让各子应用生成各自的 session
优势:存储分离;各子应用能够根据须要维护特殊的用户信息
缺陷:须要开发新接口;子应用数量较多时,登录动做的响应时间变长(须要确保每一个子应用的 SSO 接口都成功)
基于现状,咱们选择的是第二个方案,对用户帐号采用 RC4 对称加密,每一个子应用维护单独的 salt,主应用维护全部的 salt,子应用配置变成了
subApps: [ { name: 'appA', prefix: '/appA_apis', salt: 'appA', // 子应用的 host,例如 http://localhost:3000 host: process.env['subApps.appA.host'] } ] 复制代码
而后,在主应用登录完成后,调用子应用提供的 SSO 接口
for (const subApp of subApps) { // 阻塞调用接口,确保每一个请求都正确 await ... } 复制代码
通过以上步骤,咱们的页面请求问题就基本上解决了
最后,是子应用间的切换。一开始使用 React Router 的 Link 标签,发现没法从一个子应用切换到另外一个子应用,由于每一个子应用都拥有本身的路由,而每个路由的 history 都是调用 createBrowserHistory()
方法建立的
再次查看 qiankun 的文档,发现一句话
当微应用信息注册完以后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,全部 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
关键就在于触发这个浏览器 url 的变化。这里使用 window.history.pushState
方法,达成目的
history.pushState(null, linkPath, linkPath); 复制代码
完成了子应用的切换,又发现了另外一个现象:当子应用 A 切换到某一个路由时,切换到子应用 B 并进行操做;而后再次切换回子应用 A,url 并非子应用 A 刚刚卸载时的路径,但子应用 A 从新装载后会回到刚刚的页面。这对用户操做体验是好的,可是产生了 url 地址和真实呈现的界面不一致的现象
解决思路就是切换到子应用时,跳转至以前的路由,因此须要存储当前路由。因为只能影响当前打开的界面,故选择将该值存储到 sessionStorage
中
首先,须要切换子应用以前,记录当前的路由
sessionStorage.setItem('appA-currentRoute', window.location.href); 复制代码
而后,在子应用装载后,获取当前路由并跳转,而后删除记录的路由
const currentRoute = sessionStorage.getItem('appA-currentRoute'); if (currentRoute) { history.pushState(null, currentRoute, currentRoute); sessionStorage.setItem('appA-currentRoute', ''); } 复制代码
经过以上方案,实现了子应用切换的应用状态维护和 url 的匹配
至此,咱们完成了微前端的初步实践,基于微前端框架 qiankun,经过对原有系统的改造,以及开发一个主应用来做为容器,实现了多应用合并的效果,在应用间切换时的用户体验获得了很大的提升;同时,也考虑了兼容的问题,支持子应用单独访问,也兼容了原有的连接,自动重定向到正确的连接
微前端不是银弹,只有真正遇到业务问题,须要提升用户体验的时候,再考虑去引入。不过,在将来任何应用开发的初期,均可以预先考虑到 共享域名、微前端改造
等的需求,保证全部请求都有惟一子路径