背景
由于 qiankun 官方方案不支持子项目热更新,导致在多项目联调的时候,十分的不方便。
- 比如我启动了3000,3001,3002 三个端口的服务,其中3000作为主应用,3001,3002 作为微应用的时候。
- 如果我需要在主应用看效果的时候,那么两个微应用就不能支持热更新,只能通过子项目 reload 的方式将资源加载进来,导致 react 状态丢失。
- 如果单独启动子项目,配置成热更新模式,qiankun 主应用又不能及时更新 ui,如果要切换到主应用查看,那么还需要关闭当前服务,通过命令行传参数来实现热更新的启停,真的是一根筋两头堵,开发体验十分的不佳。
我的方案
qiankun 只有主应用支持热更新,这是事实,那么只能优化一些开发体验,针对微应用,我的想法是在页面注入一个按钮,通过点击按钮来控制热更新的启停,这样不用离开浏览器,进入 vscode,切换 cli 启停热更新。
rsbuild 自定义 plugin
import path from 'path'
import fs from 'fs'
import { RsbuildPlugin } from '@rsbuild/core'
export const alterHrmPlugin = (props: { hmr: boolean, single: boolean }): RsbuildPlugin => ({
name: 'alter-hmr-plugin',
apply: (congig, context) => {
return context.action === 'dev' && !props.single
},
setup(api) {
api.modifyRsbuildConfig((config, { mergeRsbuildConfig }) => {
const hmr = props.hmr
return mergeRsbuildConfig(config, {
dev: {
hmr,
},
})
})
api.onBeforeStartDevServer(({ server }) => {
// 拦截页面请求
server.middlewares.use((req, res, next) => {
if (req.originalUrl?.startsWith('/alter/hmr')) {
// 通过改变配置文件,从而更新配置,达到重启服务的目的
const url = new URLSearchParams(req.originalUrl.split('?')[1])
fs.writeFileSync(
path.resolve(__dirname, '.env.local'),
['hmr', url.get('state')].join(' = ')
)
res.writeHead(200, {
'Content-Type': 'application/json',
})
res.end(JSON.stringify({ done: true }), 'utf-8')
} else {
next()
}
})
})
// 注入 button 到页面中
api.modifyHTMLTags(({ headTags, bodyTags }) => {
const config = api.getRsbuildConfig()
bodyTags.push({
tag: 'button',
attrs: {
type: 'button',
'data-hmr-href': `/alter/hmr`,
title: '开启关闭热更新',
onclick: `{
const { hmrStatus, hmrHref } = this.dataset
const newHmrStatus = hmrStatus === 'on' ? 'off' : 'on';
this.dataset.hmrStatus = newHmrStatus;
let count = 0;
setInterval(() => {
this.innerText = newHmrStatus + '.'.repeat(count++ % 4)
}, 200)
this.style.background = newHmrStatus === 'on' ? 'forestgreen' : 'grey'
this.disabled = true
fetch(this.dataset.hmrHref + '?state=' + newHmrStatus).then(res => res.json()).then(res => {
console.warn('热更新模式切换成功')
}).catch(err => {
console.error('热更新模式切换失败')
})
}`,
'data-hmr-status': config.dev?.hmr === false ? 'off' : 'on',
style: `position: fixed; width: 100px; line-height: 48px; right: -32px; bottom: -8px; transform: rotate(-45deg); background: ${
config.dev?.hmr ? 'forestgreen' : 'grey'
}; color: #fff`,
},
children: config.dev?.hmr === false ? 'off' : 'on',
})
return { headTags, bodyTags }
})
},
})
使用方法
import path from 'path'
import { defineConfig, loadEnv } from '@rsbuild/core'
import { pluginReact } from '@rsbuild/plugin-react'
import { pluginSass } from '@rsbuild/plugin-sass'
import { dropWhile, get } from 'lodash-es'
import { pluginSvgr } from '@rsbuild/plugin-svgr'
import { name } from './package.json'
import { externals } from '@dnt/config/cdn'
import { PUBLIC_URL } from '@dnt/config/webpack.config'
import CompressionPlugin from "compression-webpack-plugin"
import { pluginEslint } from '@rsbuild/plugin-eslint'
import { alterHrmPlugin } from './alter-hmr-plugin'
const { publicVars, rawPublicVars, parsed } = loadEnv({ prefixes: ['PUBLIC_'] })
const argv = require('minimist')(
dropWhile(process.argv, (v) => v !== '--').slice(1)
)
/**
*
* 单体运行 single true\
* 父级单体运行 single true\
* 父级 qiankun 运行 undefined false\
*/
const single = get<boolean>(argv, 'single', false)
export default defineConfig({
plugins: [
pluginReact(),
pluginSass(),
// 可将 svg 直接导入到 react 组件中使用
pluginSvgr({ mixedImport: true }),
// TODO: 有空放开修改不合理的地方
// pluginEslint(),
alterHrmPlugin({
hmr: parsed.hmr === 'on' ? true : false,
single
}),
],
html: {
template: './public/index.html',
},
source: {
define: {
...publicVars,
'process.env': JSON.stringify(rawPublicVars),
},
},
output: {
// 兼容 ie 11
// polyfill: 'usage',
assetPrefix: [PUBLIC_URL, 'child', name, ''].join('/'),
distPath: {
root: 'build',
},
},
resolve: {
alias: {
'&': path.resolve(__dirname, 'src/'),
},
},
server: {
strictPort: true,
open: single === true,
port: parseInt(parsed.PORT),
proxy: [
{
context: ['/rhino/v1', '/auth/v1'],
target: parsed.HTTPS_PROXY_TARGET,
secure: false,
changeOrigin: true,
},
],
headers: {
'Access-Control-Allow-Origin': '*',
},
historyApiFallback: true,
},
dev: {
lazyCompilation: single, // 注意这个配置在主应用的时候必须关闭,否则会报错
client: {
overlay: false,
port: parseInt(parsed.PORT),
},
hmr: single === true,
},
tools: {
rspack(config, { env }) {
config.output.library = {
name: `${name}-[name]`,
type: 'umd',
}
config.output.chunkLoadingGlobal = `webpackJsonp_${name}`
config.output.globalObject = 'window'
if (env === 'production') {
// config.externals = externals
config.plugins.push(
new CompressionPlugin({
algorithm: 'gzip',
})
)
} else {
config.output.publicPath = 'auto'
}
return config
},
},
performance: {
removeConsole: true,
chunkSplit: {
strategy: 'split-by-module'
}
}
})
效果展示
右下角生成一个 button
点击之后
总结
核心就是利用了 rsbuild 会 watch env 文件更新,从而重启服务,这样我通过命令行启动 3 个服务以后,如果想在主应用看效果,那可以关闭热更新,如果想在微应用开发,就可以开放热更新。