不使用微前端:如何实现主应用和子模块动态管理与通信实现

51 阅读7分钟

架构概述

moyu 采用 主应用 + 子模块 的微前端架构模式,通过 MoyuConfig 全局配置中心实现模块间的解耦和通信。整个架构支持动态模块加载、按需启用、跨模块组件调用等功能。

exported_image.png

项目结构

web_master/
├── src/                    # 主应用代码
├── modules/                # 子模块目录
│   ├── moyu-systemset-page/    # 系统设置模块
│   ├── moyu-assetmanage-page/  # 资产管理模块
│   └── ...                 # 其他子模块
├── static/js/config.js     # 全局配置中心
├── childModule.json        # 模块配置清单
├── src/development_extension.js # 开发环境模块导入文件
└── build/                  # 构建配置

1. 全局注册中心(moyuConfig)

位置static/js/config.js

moyuConfig 充当主项目和子模块之间的通信总线,提供以下核心能力:

方法功能说明
pushComponent()子模块注册组件、路由、Store
getComponentByName()跨模块获取组件
getModuleByName()获取指定模块
getChildStores()获取所有子模块 Store
getRootRouters()获取根路由配置
getChildRouters()获取子路由配置

2. 模块配置开关

位置childModule.json

json
{
  "moyu-assetmanage-page": {
    "open": true,
    "desc": "资产管理中心"
  },
  "moyu-logaudit-page": {
    "open": true,
    "desc": "日志审计中心"
  }
}
  • ✅ open: true - 启用该模块
  • ❌ open: false - 禁用该模块
  • 📝 desc - 模块描述信息

3. 构建配置别名

位置webpack.dev.conf.js / vite.config.js

构建工具读取 childModule.json,动态生成模块别名:

javascript
// webpack.dev.conf.js
function getModuleAlias() {
  const moduleAlias = {}
  Object.keys(allModules).forEach(moduleName => {
    moduleAlias[moduleName] = path.resolve(__dirname, `../modules/${moduleName}/src/index.js`)
  });
  return moduleAlias;
}
javascript
// vite.config.js
function getModuleAlias() {
  Object.keys(allModules).forEach(moduleName => {
    alias[moduleName] = path.resolve(__dirname, `./modules/${moduleName}/src/index.js`)
  });
}

🔄 工作流程

节点类型作用说明
A[子模块 index.js]运行时入口每个业务子模块的 JS 入口文件,通过 pushComponent(...) 方法向 moyuConfig 注册自身能力(如组件、路由、Store 模块等)。
B[moyuConfig]全局配置收集器一个单例对象,用于在应用启动前收集所有已加载子模块的注册信息。
C[childModule.json]配置文件存放于子模块目录下,包含 enabled: true/false 等字段,控制该模块是否参与构建和运行。
D[构建工具]构建阶段插件在 Vite 或 Webpack 构建过程中读取所有 childModule.json,根据开关决定是否打包该模块,并生成对应的路径别名(如 @/modules/user → 实际路径)。
E[Webpack / Vite]构建系统最终使用的打包工具,通过 alias 支持模块按需引入,实现物理隔离与逻辑聚合。
F[全局中心 MoyuCore]运行时核心枢纽聚合 moyuConfig 中的所有注册项,对外提供统一接口(如 getChildStores()initRouter())。
G[main.js]应用主入口Vue 项目主文件,在启动时调用全局中心的方法,完成 Store、路由等的动态初始化。
H[Vuex Store]状态管理实例通过 store.registerModule() 动态注册子模块的 Vuex 模块,实现状态按需加载。

📦 子模块结构示例

位置modules/moyu-assetmanage-page/src/index.js

import { pushComponent } from 'MoyuConfig'
import router from './router.js'

const components = {
  AssetStatistics: () => import('./pages/assetsmanage/Assets_AssetList/AssetList.vue'),
  addAsset: () => import('./pages/assetsmanage/Assets_AssetList/AssetAdd/AssetAdd.vue'),
  // ... 更多组件
};

pushComponent({
  key: 'assetmanage',
  components: components,
  router: router
});

export default {
  key: 'assetmanage',
  components: components,
  router: router
}

关键特性

特性实现方式
懒加载使用 () => import() 异步加载组件
模块标识通过 key 字段唯一标识模块
路由独立每个模块拥有独立的 router.js
按需注册通过 pushComponent 注册到全局

🎯 主项目集成

位置src/main.js

import {
  setComponentsPath,
  getChildStores,
  setI18n
} from 'moyuConfig';

// 初始化路由
const { router, asyncRouter, asyncRouterConfigCenter } = initRouter();

// 初始化 Vuex Store
const store = new Vuex.Store(rootStore);

// 动态注册子模块 Store
const childStores = getChildStores();
for (let i = 0; i < childStores.length; i++) {
  store.registerModule(childStores[i].key, childStores[i].value);
}

// 创建 Vue 实例
const app = new Vue({
  el: '#app',
  i18n,
  router,
  store,
  template: '<App/>',
  components: { App }
});

🔌 跨模块组件调用

javascript
// 方式一:通过 MoyuConfig 获取
import { getComponentByName } from 'MoyuConfig'
const AssetList = getComponentByName('assetmanage', 'AssetStatistics');

// 方式二:直接通过别名导入(构建时解析)
import AssetList from 'moyu-assetmanage-page';

📊 架构优势总结

优势说明
🧩 高度解耦子模块独立开发,无需修改主项目代码
⚡ 按需加载通过 childModule.json 控制模块启用
🔄 动态注册运行时自动聚合路由、组件、Store
📦 独立打包支持 UMD 格式独立打包(webpack.package.conf.js
🛠️ 双构建支持同时支持 Webpack 和 Vite 构建
🌐 跨模块通信通过全局注册中心实现模块间组件调用

📁 关键文件清单

文件路径作用
static/js/config.js全局注册中心 MoyuConfig
childModule.json模块启用配置
src/main.js主项目入口,聚合子模块
modules/*/src/index.js子模块入口,注册组件
build/webpack.dev.conf.jsWebpack 开发配置
vite.config.jsVite 构建配置
build/changeModules/webpack.package.conf.js子模块独立打包配置

🎯 打包构建流程

开发环境构建

  1. 启动构建脚本: npm run dev
  2. 生成模块导入文件: prepare.createFile()
  3. 监听配置变化: prepare.watchConfig()
  4. Webpack 别名配置: 模块名映射到实际路径
  5. 热重载: 修改代码自动刷新

生产环境构建

  1. 读取 childModule.json: 获取所有启用的模块
  2. 静态分析依赖: Webpack 分析模块依赖关系
  3. 代码分割: 按模块进行代码分割
  4. 生成最终包: 包含主应用和所有启用的子模块

关键优势

特性说明
模块解耦子模块独立开发、测试、部署
按需加载通过配置控制模块启用状态
跨模块通信统一的组件调用和 Store 访问机制
开发友好配置驱动,无需手动维护导入语句
版本同步自动下载对应分支的子模块
错误处理组件缺失时提供友好的错误提示

🎓 总结

整个架构像一个 插件系统

  1. 主框架main.js)提供运行环境和全局插座(MoyurConfig
  2. 子模块modules/*)是插件,启动时自动插入插座注册自己
  3. 配置文件childModule.json)是开关,决定哪些插件生效
  4. 构建工具Webpack/Vite)负责连线,通过别名让代码能找到彼此

这种设计使得新增功能模块时,只需在 modules 目录添加代码并修改 childModule.json,无需大量修改主项目代码,实现了高度的解耦和可扩展性。

1. 核心通信机制:全局注册中心 项目定义了一个全局对象

moyuConfig(位于 static/js/config.js),它充当了主项目和子模块之间的“通信总线”。

  • 注册:子模块(如 modules/moyu-assetmanage-page/src/index.js)加载时,调用 MoyuConfig.pushComponent 方法,将自己的组件、路由、Store 注册到全局中心。
  • 获取:主项目或其他模块需要通过 MoyuConfig.getComponentByNameMoyuConfig.getModuleByName 来获取已注册的组件,实现跨模块调用,避免直接引用代码。

2. 模块解耦与动态启用 模块的启用与否由配置文件

childModule.json 控制,实现了按需加载。

  • 配置开关childModule.json 中定义了所有子模块(如 moyu-assetmanage-page),通过 open: true 字段控制是否启用。
  • 构建别名:构建工具(webpack.dev.conf.jsvite.config.js)读取该 JSON 文件,动态生成 webpack/vite 的 alias 配置。这使得代码中可以通过模块名直接引用子模块入口,而无需关心物理路径。
  • 物理隔离:子模块代码存放在 modules/ 目录下,拥有独立的 src/index.js 入口,逻辑上相互独立。

3. 路由与状态管理聚合

主项目启动时,动态收集子模块的路由和状态,实现统一化管理。

  • 路由聚合:主项目 src/main.js 中调用 initRouter,底层会读取 MoyurConfig 中收集的路由配置(getRootRouters 等),将子模块路由动态挂载到主路由实例。
  • 状态共享:主项目通过 MoyuConfig.getChildStores 获取子模块的 Store 配置,并在 Vuex 初始化时通过 store.registerModule 动态注册,实现全局状态共享。

4. 组件按需加载

为了优化性能,子模块内部采用了懒加载策略。

  • 异步组件:在子模块的 index.js 中,组件定义使用箭头函数配合 import()(如 AssetStatistics: () => import(...)),确保只有当组件真正被使用时才会加载代码。
  • 独立打包潜力build/changeModules/webpack.package.conf.js 显示子模块支持配置为 umd 格式独立打包,这意味着未来支持通过 CDN 或微前端方式独立部署子模块。