从一套配置到八个子应用:一次微前端工程化改造的记录

介绍

多子应用工作区的目录拆分、共享构建配置、qiankun 生命周期与部署踩坑。

背景:一个后台,拆成了八个

平台起步是一个 Vue 2 + Element UI 的单体后台,设备接入、规则编排、边缘管理页面全在一个工程里。业务域越铺越宽之后,单体出现三个具体问题:构建一次要十几分钟;改一个页面也要全量回归;某个模块要独立发版时,只能整包发。

后来我们先用 qiankun 把「壳」和业务子应用分开,再往后干脆把子应用拆成 apps/ 工作区,每个应用可以单独开发、单独构建、单独部署。这篇文章讲的就是 apps/ 这一层的搭法,以及改造过程中真实踩到的坑。

整体架构:目录即分工

apps/ 下目前这些应用:

目录职责关键依赖
dmp-general门户首页、总览大屏、基础监控echarts 4
dmp-access设备接入、台账、物模型、号卡@antv/x6、echarts
dmp-edge边缘节点、云边协同、应用管理element-ui
dmp-forward告警规则、规则编排、消息队列vuedraggable
dmp-extend数字孪生、任务、视频服务、指标@antv/g6、echarts5、xlsx
dmp-protocol-center工业协议中心element-ui
dmp-endpoint-management端点、固件、场景管理leaflet、xterm
process-app流程编排编辑器LogicFlow 1.2.8

另有两个特殊成员:common/ 是被所有应用引用的公共源码包;dmp-uni-account/ 是一个独立的 npm workspaces(基座 + 系统管理子应用 + @dmp/shared),属于账号中心那条线,和上面这套构建方式并行存在。

目录大致是:

apps/
├── common/                   # 公共源码:组件、utils、构建配置、生命周期
│   └── app/
│       ├── vue.config.shared.js
│       ├── lifecycle.js
│       └── setup.js
├── dmp-access/
│   ├── .env                  # 应用名、端口、部署前缀
│   ├── vue.config.js         # 只有一行工厂调用
│   └── src/
├── dmp-general/
├── dmp-forward/
└── ...

每个应用的 vue.config.js 都短到只有两行:

const createVueConfig = require('common/app/vue.config.shared')
module.exports = createVueConfig({ appDir: __dirname })

这是改造里最划算的一步:把原来复制粘贴到每个应用里的几百行 webpack 配置收进一个工厂函数,应用只声明自己的 appDir。以后改构建策略,只改一处。

微前端:接入方式与通信

先说一个容易被误解的点:apps/ 下的应用是独立部署的,不是被老基座用 qiankun 注册的。老基座(application/home)的 registerMicroApps 仍只注册老一代应用,规则按 hash 激活:

const getActiveRule = (hash) => (location) => location.hash.startsWith(hash)
{
  name: 'dmp_engine',
  entry: process.env.NODE_ENV === 'production' ? `${microFixObj.engine}/` : '//localhost:9889',
  container: '#appContainer',
  activeRule: getActiveRule(`#${microFixObj.engine}/`),
}

新应用改用同源路径部署:生产环境 publicPath 是 /dmp-html/dmp-access/,路由是 history 模式,base 直接取应用前缀:

new Router({
  mode: 'history',
  base: process.env.VUE_APP_MICRO_PREFIX, // /dmp-access
  routes: [...constantRoutes],
})

应用之间的跳转用 window.location.href,不再依赖基座路由;登录态则通过 Cookie 共享(common/utils/auth 统一读写 token)。换句话说,微前端框架在这里退化成了「可选的挂载方式」,拆分和独立部署才是目的。

公共生命周期仍留在 common/app/lifecycle.js,让同一个入口既能独立跑,也能被 qiankun 挂载:

export function createMicroApp({ Vue, App, router, store }) {
  let instance = null
  const render = () => {
    instance = new Vue({ router, store, render: (h) => h(App) }).$mount('#devApp')
  }
  if (!window.__POWERED_BY_QIANKUN__) render()

  return {
    async bootstrap() {},
    async mount(props) {
      props.onGlobalStateChange((state) => {
        store.commit('user/SET_PERMISSIONS', state.permissionList)
      }, true)
      await store.dispatch('user/getInfo')
      render()
    },
    async unmount() {
      instance && instance.$destroy()
      instance = null
    },
  }
}

主应用侧用 initGlobalState 广播权限,用 props 把 store、router 传下去;子应用只在 mount 时接管。独立运行时这些都不执行,避免把「必须依赖基座」写死。

构建与部署:环境变量只留四个

每个应用只有一个 .env:

VUE_APP_MICRO_DIRECTORY='/dmp-html'
VUE_APP_MICRO_PREFIX='/dmp-access'
VUE_APP_MICRO_NAME='dmp-access'
VUE_APP_MICRO_PORT='9010'

共享工厂据此推出 publicPath、outputDir、UMD library 名和 devServer 端口:

const name = process.env.VUE_APP_MICRO_NAME
const port = process.env.VUE_APP_MICRO_PORT
return {
  publicPath: process.env.NODE_ENV === 'production'
    ? `${process.env.VUE_APP_MICRO_DIRECTORY}${process.env.VUE_APP_MICRO_PREFIX}/`
    : `//localhost:${port}`,
  outputDir: `../dist${process.env.VUE_APP_MICRO_DIRECTORY}${process.env.VUE_APP_MICRO_PREFIX}`,
  devServer: {
    port,
    headers: { 'Access-Control-Allow-Origin': '*' },
  },
  configureWebpack: {
    output: {
      library: name,
      libraryTarget: 'umd',
      chunkLoadingGlobal: `webpackJsonp_${name}`,
    },
  },
}

Access-Control-Allow-Origin 是 qiankun 用 fetch 拉子应用 HTML 的必要条件;library 和 chunkLoadingGlobal 按应用名区分,避免多个应用在同一页面加载时互相覆盖。

版本形态也在构建期决定。npm run dev --light 这样的参数会被 npm 转成 npm_config_light=true,共享配置再把它映射成 VUE_APP_EDITION:

const variants = ['mini', 'light', 'light_plus']
const edition = variants.find((v) => process.env[`npm_config_${v}`] === 'true') || 'full'
process.env.VUE_APP_EDITION = edition

默认 full(不带参数);极简、轻量、轻量增值版共用同一套代码,只靠环境变量控制功能开关,不用为每个版本维护一条分支。

工程规范同样收在 common/app/lint:各应用的 .eslintrc.js 只有一行 module.exports = require('common/app/lint/eslintrc'),再配合 f2elint 打开 stylelint、markdownlint、prettier。规范不再靠口头约定。

踩过的坑

1. 端口漂移。 开发时端口被占用,Vue CLI 默认会自动 +1,但基座里写死的子应用地址不会跟着变,结果就是「子应用明明启动了,基座一直白屏」。共享配置里加了 onListening 检查,端口不一致直接退出,比事后排查便宜得多。

2. 生产前缀和路由 base 必须一致。 publicPath 是 /dmp-html/dmp-access/,路由 base 是 /dmp-access,两者不是同一个字符串。history 模式下服务器还需要把子路径回退到对应 index.html;仓库里没有 nginx 配置,这一层由部署环境负责,我无法在代码里验证。

3. 公共包是源码共享,不是运行时共享。 "common": "file:../common" 安装后只是一个软链接,webpack 仍会把 common 里的代码分别打进每个应用;Vue、Element UI 也是各打各的。它解决的是「改一处、所有应用生效」,但不解决运行时单例和包体积。真要共享运行时,得走 externals 或模块联邦,我们没走到那一步。

4. 注释和实现会骗人。 共享配置里写着「生产环境去除 console.log」,实际 terser 配置是 drop_console = false,只删了 debugger。线上排查时如果默认日志已经被清掉,就会白找很久;配置里的注释也要当代码 review。

5. 历史架构并存。 dmp-uni-account 用的是另一套 npm workspaces + qiankun 2.10.16 的组合,路由按 hash 匹配,和 apps/ 的路径部署不是一套规则。它能独立工作,但两套心智模型并存,新人第一次看很容易混。这部分是否会统一,代码里没有答案。

小结

这次改造真正解决的不是「要不要上微前端」,而是三件事:构建配置从复制粘贴变成一处工厂;应用边界按业务域切清楚,能独立发版;公共能力和规范沉到 common,不再靠人肉同步。qiankun 只是保留了一条兼容路径。如果重来一次,我会更早做共享配置和端口/前缀校验——这两件小事省下的排查时间,比多写几个组件多得多。