多项目融合方案
引子
- iframe标签嵌套,把n(n >=2)个项目部署在不同服务器上,切换 url 或者子父嵌套即可。
- Nginx转发页面,变成 "多页面" 应用。难度中等,需要在外搭建一层,然后配置 Nginx 转发。
- 微前端,利用single-spa、qiankun等微前端库。
iframe VS 微前端
使用iframe方式还是外跳的方式,都会有一段白屏加载时间,用户体验非常不好,而换成微前端的方式会发现跳转是非常丝滑的。
外跳方式:
微前端方式:
iframe的优点:
- 完全隔离了css和js,避免了各个系统之间的样式和js污染。
- 可以在子系统完全不修改的情况下嵌入进来。
iframe的缺点
- 页面加载问题: 影响主页面加载,阻塞onload事件,本身加载也很慢,页面缓存过多会导致电脑卡顿。
- 布局问题:iframe必须给一个指定的高度,否则会塌陷。解决办法:子系统实时计算高度并通过postMessage发送给主页面,主页面动态设置高度,修改子系统或者代理插入脚本。有些情况会出现多个滚动条,用户体验不佳。
微前端的优点
- 加载快,可以将所有系统共用的模块提取出来,实现按需加载,一次加载,其他的复用。
- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染。
- http请求少,服务器压力小。
微前端的缺点
- css和js需要制定规范,进行隔离。否则容易造成全局污染,尤其是vue的全局组件,全局钩子。
- 子系统需少量改动,是不影响子系统独立开发部署及其功能。
微前端
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
微前端架构具备以下几个核心价值:
技术栈无关 主框架不限制接入应用的技术栈,微应用具备完全自主权
独立开发、独立部署 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
独立运行时 每个微应用之间状态隔离,运行时状态不共享
qiankun
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
安装
yarn add qiankun # 或者 npm i qiankun -S
官网例子
主应用注册微应用并启动
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'reactApp',
entry: '//localhost:3000',
container: '#container',
activeRule: '/app-react',
},
{
name: 'vueApp',
entry: '//localhost:8080',
container: '#container',
activeRule: '/app-vue',
},
{
name: 'angularApp',
entry: '//localhost:4200',
container: '#container',
activeRule: '/app-angular',
},
]);
// 启动 qiankun
start();
微应用 改造
nuxt作为微应用改造
由于nuxt框架没有入口文件支持改造,引入一个第三方库进行改造Nuxt-Micro-Front-End
添加
@femessage/nuxt-micro-frontend
依赖到你的项目中yarn add @femessage/nuxt-micro-frontend -D # or npm install @femessage/nuxt-micro-frontend
添加
@femessage/nuxt-micro-frontend
到nuxt.config.js
的modules
属性中{ modules: [ // 一般使用 '@femessage/nuxt-micro-frontend', // 带上模块参数 ['@femessage/nuxt-micro-frontend', { /* module options */ }] ] }
在项目根目录创建入口文件 mef.js
export function bootstrap () { console.log('nuxt app bootstraped') } export async function mount (render, props) { props.onGlobalStateChange((state, prev) => { console.log('state change from nuxt', state) }) await render() } export function mounted (vm) { console.log('mounted', vm) } export function update (vm, props) { console.log(props) } export function beforeUnmount (vm, props) { console.log(vm) } export function unmount () { console.log('nuxt app unmount') }
配置nuxt.config.js
const isMicro = process.env.MODE === 'MICRO' export default { .... // 入口文件配置 MFE: isMicro ? { path: resolve(__dirname, './mfe.js'), force: true }: null, router: { extendRoutes(routes, resolve) { if(!isMicro) { return } routes.forEach(route => { // 添加路由前缀 与主应用的activeRule一致 route.path = '/micro_nuxt' + route.path }) } }, .... // 添加模块 modules: [].concat(isMicro ? ['@femessage/nuxt-micro-frontend']: []), .... }
微应用跨域问题
由于 qiankun 是通过 fetch 去获取微应用的引入的静态资源的,所以必须要求这些静态资源支持跨域
官网给的例子是修改webpack
const { name } = require('./package');
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
我这边直接在主应用代理了微应用请求。
nuxt下script脚本报错问题
// nuxt config
head: {
title,
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
],
link: [
// { rel: 'icon', type: 'image/x-icon', href: '/DeepWise_16x16.png' }
],
script: [
{ src: `/js/mathjax/MathJax.js?config=TeX-MML-AM_CHTML`},
]
},
子应用对脚本是相对路径引用,那么到主应用路径匹配就会有问题
[import-html-entry]: error occurs while executing normal script http://localhost:8080/js/mathjax/MathJax.js?config=TeX-MML-AM_CHTML
按照老办法就是主应用搞代理。
这种方式会让脚本资源正确引用,但也会引用两个不存在的资源,导致子应用报错。
问题定位了好久都没定位到为什么会去请求,如果资源链接换成CDN也会去请求,但相应的有对应资源不会报错。
后来想到了微前端数据共享,挂载在主应用的window上也可以,所以就在主应用去加载脚本去解决该问题。
// umi config
headScripts: [`/researchPage/js/mathjax/MathJax.js?config=TeX-MML-AM_CHTML`]
微应用打包之后 css 中的字体文件和图片加载 404
原因是 qiankun
将外链样式改成了内联样式,但是字体文件和背景图片的加载路径是相对路径。
而 css
文件一旦打包完成,就无法通过动态修改 publicPath
来修正其中的字体文件和背景图片的路径。
主要有以下几个解决方案:
- 所有图片等静态资源上传至
cdn
,css
中直接引用cdn
地址(推荐) - 借助
webpack
的url-loader
将字体文件和图片打包成base64
(适用于字体文件和图片体积小的项目)(推荐)
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|webp|woff2?|eot|ttf|otf)$/i,
use: [
{
loader: 'url-loader',
options: {},
},
],
},
],
},
};
借助
webpack
的file-loader
,在打包时给其注入完整路径(适用于字体文件和图片体积比较大的项目)// nuxt config const publicPath = isDev ? '_nuxt/' : 'researchPage/public/' ... build: { vendor: ['jquery'], transpile: [/^element-ui/], /* ** You can extend webpack config here */ publicPath: isDev ? '' : publicPath, extend (config, ctx) { if(!isDev){ // 微前端子应用字体路径 const rules = config?.module?.rules || [] for(let i=0, len = rules.length; i < len; i++){ if(String(rules[i].test) == String(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i)){ config.module.rules[i].use[0].options.publicPath = '/' + publicPath } } } } }
目前在项目中利用第三种方式,注入完整的路径。
Application died in status NOT_MOUNTED: Target container with #container not existed while xxx mounting!
这个报错通常出现在主应用为 vue 时,容器写在了路由页面并且使用了路由过渡效果,一些特殊的过渡效果会导致微应用在 mounting 的过程中容器不存在,解决办法就是换成其他的过渡效果,或者去掉路由过渡。
官方给出的错误信息,路由跳转的过渡效果会导致微应用在 mounting 的过程中容器不存在。
造成这样的原因是从全院到专病的时候有几个接口请求过慢导致的loading。
addGlobalUncaughtErrorHandler(event => {
console.log(event);
// Target container with #Research not existed while research mounting!
// Ignore ResizeObserver error
if (/Research/.test(event.message)) {
window.location.reload();
}
});
我这边的处理方案就是捕捉一下子应用错误,匹配相应的错误然后reload,体验不太好。
登录Token问题
跟后端商讨的登录问题是request中携带token,主应用登录后通过sessionStorage保存token,子应用读取。
首先遇到一个问题就是子应用拿到了token,但是请求却没携带token,经过定位,是nginx转发问题。
underscores_in_headers,这个参数默认值为:off,即默认忽略带下划线的 header。
server {
...
underscores_in_headers on;
...
}
修改了nginx后,发现请求还是不携带,后来想到ingress层是否也需要配置一下, 最后让运维同学在ingress也加一下这个配置,最终请求携带token了。
别问后端为什么不直接从cookie里拿,理由很多。
第二个问题就是sessionStorage和localStorage的问题了。
一直以来,我所以为的 sessionStorage 的生命周期是这样的:在 sessionStorage 中存储的数据会在当前浏览器的同一网站的多个标签页中共享,并在此网站的最后一个标签页被关闭后清除。注意:这是错误的。
MDN 查了一下,上面是这么说的:
...data stored in sessionStorage gets cleared when the page session ends...Opening a page in a new tab or window will cause a new session to be initiated, which differs from how session cookies work.
通过点击链接(或者用了 window.open
)打开的新标签页之间是属于同一个 session 的,但新开一个标签页总是会初始化一个新的 session,即使网站是一样的,它们也不属于同一个 session。
微前端部署
此次开发微前端过程中,最麻烦的一点在于没想好部署方案,导致后续来来回回去折腾。
最开始的部署方案是服务器部署,pm2去接管主应用的node服务,静态资源代理都写在了node层,子应用的nginx配置微调。
后来又改成了K8s部署,第一版的方案是打一个单镜像,包含了所有项目,经过了漫长的折腾,终于处理好了所有资源请求问题,也成功部署在了K8s,但这种方案有一个很大弊端,任何一个应用有更新都要重新打镜像,而且没有打通cicd,导致我成了一个“人肉CICD”。
这一版开发过程中的收获是把所有资源代理基本捋清楚了,把原本在node层代理全部干掉了,为最终方案扫清了障碍。
我们最终方案是所有应用单独部署,在ingress写一层代理,这样更新的时候互不干涉。
写在最后
完结散花