落地qiankun
项目背景
- 旧项目市场竞争力落后:项目是公司几年的B端项目,一直在维护,不过维护的成本越来越高,很多地方,不论是客户也好,还是公司内部的人都比较不满意。后台的代码量也越来来臃肿。所以公司当时新成立了一个部门对该项目做标品化的产品,后续用来抢占的该类型产品的市场。
- 技术栈落后,:因为当时该项目的还在维护,所以,公司要求需要对该项目中的部分模块可以最小化影响的平移,并且需要支持后续新开发功能模块可以使用时下最新的热门技术栈。
- 公司要求:作为一个B端项目来说,各个功能业务边界比较明确,虽然项目体量庞大,但是业务之间耦合度不算太高。也因为项目持续年限较久,业务的技术栈比较老旧,面临扩展困难,ui陈旧,第三方库也不敢轻易更换或升级,应公司要求,也到了不得不做的地步。
为什么选择微前端
在刚开始技术调研的时候,也并不是直接使用市面上的微前端框架方案,而是打算使用iframe,但是和旧项目团队成员沟通以及考虑到业务中的需要和后续的升级问题,还是排除了这个方案。
对于市面上常用的微前端库,为什么选择qiankun?
市面上常用的微前端库,当时开源出来的就只有sigle-spa、qiankun。由于sigle-spa这个框架本身设计是更加面向底层的,且在使用时过于原始,所以就选择了当时qiankun,而且qiankun当时也是基于sigle-spa进行封装的上层应用框架,使用更加简洁,社区对比也更加活跃,且有官方人员专门支持,所以就选择了qiankun。
因为选择比较少,所以在横向对比了iframe、nginx、npm、qiankun等方案后,还是选择了qiankun。在使用过程中,也确实遇到了很多问题,不过通过对qiankun原理掌握的比较清楚后,这些问题也得到了解决。
经过微前端的查分,在开发侧,基本做到了子项目的独立开发,独立发布,当然了,需要通过主应用联调的内容刚刚开始还存在一些问题,不过在子项目业务,以及范围完全理清后,也非常顺畅了。
子应用拆分按什么规则拆分的
子应用拆分首要考虑的是按照业务进行拆分。其中有很多细节要在选择的时候需要考虑。总的来说我们定下了几个标准:
- 保证核心业务的独立性,将无关的子业务拆分解耦
- 业务关联紧密的功能单元应用做成一个子应用,反之关联不紧密的可以考虑拆分成多个微应用,判断业务关联是否紧密的标准,其实很简单,就是看是否通信频繁。
- 其次还要看页面结构,如果结构不清晰,业务有交叉,就算是单独的业务,也会做成一个子应用。
- 还要考虑度的问题,一开始不用拆分的太细,因为流程已经很清晰了,所以如果有有拆分需求,在进行拆分也是可以的
qiankun的原理问题
1. qiankun支持vite吗?
不支持,因为qiankun是HTML Entry的机制,说白了,就是利用webpack打包的机制,将子应用的HTML内容直接读取到主应用中来,再进行处理。而vite本身是ES Module的,也就是说 vite 构建的 js 内容必须在 type=module 的 script 里面,这会导致qiankun其实拿不到子应用JS里面的内容执行。
虽然市面上有一些基于qiankun子应用可以使用vite的插件,但是我们并不建议使用,一是这样做有很大的隐患,肯定需要修改很多内容。二是我们的项目本身就是做拆分,因此在打包工具上做了统一。
虽然微前端不限制技术手段,但是规范化不做统一处理,那么我们的项目只会从一个问题,变成另外一个更大的问题
2. qiankun支持keep-alive吗,如果不支持,有没有自己实现的思路?
在 qiankun 中,实现 keep-alive 的需求有一定的挑战性。这是因为 qiankun 的设计理念是在子应用卸载时,将环境还原到子应用加载前的状态,以防止子应用对全局环境造成污染。这种设计理念与 keep-alive 的需求是相悖的,因为 keep-alive 需要保留子应用的状态,而不是在子应用卸载时将其状态清除
我们当时也出现了这种需求,我说一下我们的处理办法:
qiankun中还提供了手动加载函数loadMicroApp,我们可以获取当前激活子应用的对象。所以,我们要实现keep-alive,就需要借助这个函数
另外,我们要实现keep-alive实际上,我们只需要缓存子应用的具体页面就行,而子应用如果是vue的项目,是可以有自己的keep-alive对当前路由进行缓存的。所以,要实现keep-alive效果,我们就需要两步:
- 1、缓存子应用对象
- 2、子应用自身实现keep-alive效果
微前端打包发布问题
以自动化打包为思路进行讲解
公共依赖
如果主子应用使用的是相同的库或者包,微前端怎么解决重复加载导致资源浪费的问题?
虽然共享依赖并不建议,但如果你真的有这个需求,你可以在微应用中将公共依赖配置成 externals,然后在主应用中导入这些公共依赖。
qiankun 将子项目的外链 script 标签,内容请求到之后,会记录到一个全局变量中,下次再次使用,他会先从这个全局变量中取。这样就会实现内容的复用,只要保证两个链接的 url 一致即可。
为了节约那么一点打包空间,个人非常不建议这么做,这样子应用不能很方便的独立运行,这和微前端的理念是违背冲突的,另外由于关键对象都挂载到了window上,很容易引起子应用和主应用之间的冲突
具体实现
1. 在主应用中使用 registerMicroApp
注册子应用:
- 1). name: 子应用名称,唯一且必填
- 2). entry: 是微应用的入口地址,唯一且必填
- 3). container: 是微应用的DOM容器节点,必填
- 4). activeRule: 是微应用的激活规则,触发后会加载该应用
import { registerMicroApps, addGlobalUncaughtErrorHandler, start } from 'qiankun'
/**
* 注册子应用
* 第一个参数 - 子应用的注册信息
* 第二个参数 - 全局生命周期钩子
*/
registerMicroApps(apps, {
// qiankun 生命周期钩子 - 加载前
beforeLoad: app => {
if (Vue.prototype.$apm) {
transaction = Vue.prototype.$apm.startTransaction(location.pathname, 'app-lifecycle', {
managed: true,
canReuse: true
})
}
return Promise.resolve()
},
// qiankun 生命周期钩子 - 挂载后
afterMount: app => {
// 加载子应用后,进度条加载完成
// NProgress.done()
if (Vue.prototype.$apm) {
afterFrame(() => transaction && transaction.detectFinish())
}
return Promise.resolve()
}
})
// 当前环境-端口号
const currPort = window.location.port
// 当前环境-主机名
const hostName = `//${window.location.hostname}`
// 微应用端口
const PORT_ENUM = {
publish: {
dev: '8081',
porduct: currPort
},
}
/**
* 子应用信息
* // {
* // name: 'publishMicroApp', // 微应用名称 - 具有唯一性
* // entry: '//localhost:8081/publish-website', // 微应用入口 - 通过该地址加载微应用
* // container: '#frame', // 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
* // activeRule: '/portal/publish-website' // 微应用触发的路由规则 - 触发路由规则后将加载该微应用
* // props: msg
* // }
*/
Object.keys(PORT_ENUM).forEach(appName => {
const prot = isDev ? appName.dev : appName.publish
apps.push({
name: `${appName}MicroApp`,
entry: prot
? `${hostName}:${prot}/${mcube}${appName}-website/`
: `${hostName}/${mcube}${appName}-website/`,
container: '#frame',
activeRule: `/${process.env.VUE_APP_STATIC_BASE}/${global.MAIN_APP_NAME}/${appName}-website`,
props: msg
})
})
export default apps
2. 改造子应用
- 1). 改造子应用入口文件main.js的加载逻辑,通过
window.__POWER_BY_QIANKUN__
判断是否独立运行或qiankun下运行,改在微应用实例挂载逻辑 - 2). 暴露三个微应用的钩子函数:
- 2.1). bootstrap: 首次调用时执行
- 2.2). mount: 每次进入微应用时执行
- 2.3). unmount: 每次切除该子应用时调用
async function run() {
// 加载部分静态资源(如: jq/iconfont)
if (!window.__POWERED_BY_QIANKUN__) {
await load()
}
// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
}
run()
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('supportMicroApp bootstrap')
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
console.log('supportMicroApp mount', props)
render(props)
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
console.log('supportMicroApp unmount')
instance.$destroy()
instance.$el.innerHTML = ''
instance = null
router = null
}
3. 改造webpack打包配置
为了让主应用识别子应用暴露出来的信息
output.library
: 名称需要与主应用中注册的子应用名称一致output.libraryTarget
: 包在所有模式下均可运行output.JosnpFunction/output.chunkloadingGlobal
: 按需加载配置
configureWebpack: config => {
config.output = Object.assign({}, config.output, {
// 微应用的包名,这里与主应用中注册的微应用名称一致
library: 'supportMicroApp',
// 将你的 library 暴露为所有的模块定义下都可运行的方式
libraryTarget: 'umd',
// 按需加载相关,设置为 webpackJsonp_supportMicroApp 即可
jsonpFunction: `webpackJsonp_supportMicroApp`
})
return {
output: {
// 打包编译后的 文件名称
// 【模块名称.分支名称-commit哈希前五-打包时间.js】
filename: `js/[name].${version}.js`,
chunkFilename: `js/[name].${version}.js`
}
}
}
4. 新增运行时说明文件
webpack工程的src下需要新增 public-path.js
,用于修改运行时的publicPath,并将其放到入口文件的首行
/**
* 动态设置 webpack publicPath 防止资源加载出错
*/
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
5. 其它
- 5.1 强建议history
- 5.2 本地需要配置跨域(子应用)
devServer: {
headers: {
'Access-Control-Allow-Origin': '*'
},
}
- 5.3 关闭主机检查:disableHostCheck: true,使子应用可以被fetch
devServer: {
disableHostCheck: true,
}