Skip to content

子应用集成

现有系统集成这套微前端方案,并不需要大刀阔斧的重构,更多的是做'减法'。很多东西都不需要了。

注意

注意。下文大部分改造步骤都是可选的,或者只是最佳实践。微前端基座并没有严格要求子应用必须调用它提供的 API 或者组件,这些完全是可选的。

最小化的改造只会涉及菜单隐藏、子应用注册等步骤

1. 统一使用 vue-cli 5+

vue-cli 是目前使用构建工具。第一步请确保你的项目使用 vue-cli, 并升级到 vue-cli 5+。这并不是说,我们只能用 vue-cli。其他构建工具也可以,只不过我们提供了 vue-cli 插件,可以更方便地让子应用集成进来, 开箱即用。

DANGER

未来可能支持 vite,但是目前 vite 在 qiankun 微前端框架下并不能完美使用。

2. 子应用描述信息

子应用需要在基座中注册一些关键的信息,基座才知道如何加载和渲染子应用。

字段名描述默认值
🔴 name: string应用名称, 必须唯一。 如果集成的是 @mfejs/vue-cli-plugin-mfapp-child 在控制台可以看到生成的应用名称必填
🔴 entry: string子应用的 HTML 入口, 例如 /__apps__/vue3, 指向微应用的静态资源的目录即可。本地开发时可以指向本地路径,例如 //localhost:5173必填
🔴 activeRule: string | string[]激活的路由, 路由前缀,以 / 开始,例如 /mfe, 当路由以 /mfe 前缀时,基座会启动并渲染对应的子应用。注意不需要包含基座 base,会自动添加。必填
container?: string | HTMLElement挂载的 DOM 节点, 默认为 #root。这个通常由基座提供挂载点,保持默认就行了#root
props?: Record<string, any>传递给子应用的参数, 可以在这里放置一些元数据
version?: string子应用版本号。描述信息,不会影响实际渲染
description?: string应用描述。描述信息,不会影响实际渲染
independent?: boolean独立模式。 一般情况下微应用都是挂载在由基座的提供的页面框架内,某些特殊场景,微应用要完全接管一个页面,比如登录。这时候就开启false
routeMode?: 'hash''history'路由模式
alias?: string用于多业态应用,绑定到同一个身份上

其中最关键的只有三个参数: name、entry、activeRule。这三个参数的意义请开发者务必要清楚。

注意

注意,如果 activeRule 为数组, 那么 openByAppName 接口调用可能出现歧义,这时会默认打开第一个

3. 引入子应用集成插件

安装 @mfejs/vue-cli-plugin-mfapp-child 插件:

shell
$ pnpm add @mfejs/vue-cli-plugin-mfapp-child -D

并在 vue.config.js 中配置:

js
const { defineConfig } = require('@vue/cli-service');
const { defineMFappChild } = require('@mfejs/vue-cli-plugin-mfapp-child');

module.exports = defineConfig({
  transpileDependencies: false,
  pluginOptions: {
    ...defineMFAppChild({
      mapp: {
        activeRule: '/dsp.html',
      }
    }),
  },
  lintOnSave: false,
});

defineMFAppChild 支持以下参数:

名称描述默认值
name?: string微应用名称,默认从 package.json name 读取,并转换为驼峰形式camelCase(package.name)
mapp?: MappSingle | MappMultiple[]微应用描述信息
constants?: Record<string, string>自定义变量

高级参数, ⚠️建议保持默认值:

名称描述默认值
terminalMode?: boolean是否在微前端运行容器下运行。微前端运行容器支持模板true
CDNDomain?: stringCDN 域名,如果静态资源需要由 CDN 分发,则需要配置此项, 如果开启了 terminalMode, CDNDomain 默认为 [%= cdnDomain ? '//' + cdnDomain : '' %]
baseUrl?: string主应用基础路径,默认为 '/' , 建议和主应用配置一致。如果开启了 terminalMode, baseUrl 默认为 [%= removeTrailingSlash(base) %]
publicPath?: string微应用 publicPath,默认为 auto, 即 <CDNDomain><base>/__apps__/<name>/auto
shared?: SharedDeclaration[]从基座中共享的依赖,必须精确匹配。

在大部分的场景下,你只需要配置 activeRule、name 等关键信息即可。其他信息保持默认配置。

3.1 单页应用配置示例

js
const { defineConfig } = require('@vue/cli-service');
const { defineMFAppChild } = require('@mfejs/vue-cli-plugin-mfapp-child');

module.exports = defineConfig({
  transpileDependencies: false,
  pluginOptions: {
    ...defineMFAppChild({
      // 默认从 package.json 中读取
      name: 'dsp',
      mapp: {
        // 子应用的激活路由,默认为 /<name>
        activeRule: '/dsp.html',
        // 独立模式,默认为 false
        // independent: false,
        // 应用别名, 详见多业态应用文档
        alias: 'dsp';
      }
    }),
  },
  lintOnSave: false,
});

3.2 多页应用配置示例

js
const { defineConfig } = require('@vue/cli-service');
const { defineMFAppChild } = require('@mfejs/vue-cli-plugin-mfapp-child');

module.exports = defineConfig({
  // 多页应用
  pages: {
    index: 'src/main.ts',
    another: 'src/another.ts',
  },
  pluginOptions: {
    // 微前端集成配置
    ...defineMFAppChild({
      mapp: [
        {
          // 默认为 <packageName> + upperFirst(<entry>)
          // name: 'playgroundVue3',

          // entry 必须为上面 pages 中定义的 key
          entry: 'index',

          // 默认为 /<name>
          // activeRule: '/playgroundVue3',
        },
        {
          // entry 必须为上面 pages 中定义的 key
          entry: 'another',
          activeRule: '/playgroundVue3Another',

          // 独立模式
          independent: true,
        },
      ],
    }),
  },
});

4. 支持 Web Component 识别

基座暴露了一些 Web Component 给子应用使用。为了避免 Vue 将这些组件识别为 Vue 自定义组件,需要一些额外的配置。

Vue 3 配置如下

shell
$ pnpm add @mfejs/vue-cli-plugin-ce -D

然后在 vue.config.js 中配置:

js
const { defineConfig } = require('@vue/cli-service');
const { defineCE } = require('@mfejs/vue-cli-plugin-ce');

module.exports = defineConfig({
  transpileDependencies: true,
  parallel: false,
  pluginOptions: {
    ...defineCE({ customElement: /mfe-/ }),
    // ... 其他配置
  },
});

TIP

当然你也可以手动配置

js
// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => ({
        ...options,
        compilerOptions: {
          // treat any tag that starts with ion- as custom elements
          isCustomElement: tag => tag.startsWith('mfe-'),
        },
      }));
  },
};

Vue2 配置如下:

js
import Bay from '@mfejs/mfapp-bay';

// Bay 会自动配置 Vue.config.ignoredElements
Vue.use(Bay);

TIP

当然你也可以手动配置

js
Vue.config.ignoredElements = [/^mfe-/];

5. 移除组件库样式

在布局与主题 中提到: element-ui、element-plus 的样式都外置到主题包中。 因此我们建议将 element-ui/element-plus 相关的样式移除。

6. 挂载应用

6.1 Vue 2 示例

原始代码:

js
import Vue from 'vue';

import App from './App.vue';
import router from './router';
import store from './store';

Vue.config.productionTip = false;

(new Vue({
    router,
    store,
    render: h => h(App),
  })).$mount(#)

调整为:

js
import Vue from 'vue';
import Bay from '@mfejs/mfapp-bay';

import App from './App.vue';
import router from './router';
import store from './store';

Vue.config.productionTip = false;

Vue.use(Bay);

let instance: Vue;

Bay.createMicroApp({
  /**
   * 启动时被调用。全局只会调用一次。
   */
  async bootstrap(props) {
    console.log('bootstrap', props);
  },

  /**
   * 挂载, 会调用多次。
   */
  async mount(container, props) {
    console.log('mount', props);

    instance = new Vue({
      router,
      store,
      render: h => h(App),
    });

    instance.$mount(container?.querySelector('#app') || '#app');
  },

  /**
   *  卸载
   */
  async unmount(props: any) {
    console.log('unmount', props);

    if (instance) {
      instance.$destroy();
    }
  },

  /**
   * props 更新,可选
   */
  async update(props: any) {
    console.log('update', props);
  },
});

首先使用 Vue.use(Bay) 注册插件, 接着使用 Bay.createMicroApp 创建微应用。 这里重点关注 mountunmount 两个生命周期。 后面基座就知道如何接管子应用的渲染了。

6.2 Vue 3 示例

原始代码:

js
const router = createRouter({
  // 基座路由必须使用 hash 模式
  history: createWebHashHistory(),
  routes,
});

const app = createApp(App).use(store).use(router);

app.mount('#app');

调整为:

js
import { createApp, App as TApp } from 'vue';
import { createRouter, createWebHashHistory } from 'vue-router';
import Bay from '@mfejs/bay';

import App from './App.vue';
import { routes } from './router';
import store from './store';

let app: TApp;

Bay.createMicroApp({
  async bootstrap() {
    console.log('bootstrap vue3');
  },
  async mount(container, props) {
    console.log('mount vue3', props);

    const router = createRouter({
      history: createWebHashHistory(),
      routes,
    });

    app = createApp(App).use(store).use(router).use(Bay);

    app.mount(container?.querySelector('#app') ?? '#app');
  },

  async unmount() {
    console.log('unmount vue3');

    app.unmount();
  },

  async update() {
    console.log('update vue3');
  },
});

注意

你也可以使用 qiankun 原生的导出模块挂载方式, 这种方式需要额外配置 webpack 的 output 配置。

7. 调用基座 API

基座暴露了一些组件和 API 供子应用调用。

其中, 组件以 Web Component 形式暴露,全局可以使用,不需要额外导入,直接就可能使用:

vue
<template>
  <div class="my-page">
    <mfc-header>
      <!-- content -->
    </mfc-header>
  </div>
</template>

API 可以通过 @mfejs/mfapp-bay 调用:

js
import bay from '@mfejs/mfapp-bay';

// 退出登录
bay.logout();

8. 减法

基座负责了整个页面框架的渲染,比如侧边栏、顶部菜单栏、权限、路由管理、会话管理等等。所以子应用就没必要再处理这些事情了:

  • 移除顶部菜单栏
  • 移除侧边栏
  • 移除权限判断相关代码
  • 移除 401 跳转到登录页面(基座会全局拦截)
  • 移除 检查登录状态相关代码
  • 页面布局使用基座提供的组件
  • 移除组件库样式包
  • 移除接口代理配置
  • 迁移 i18n 初始化代码到 @mfejs/i18n 或 @mfejs/i18n-legacy
  • 移除不必要 polyfill 代码
  • ...

9. 避免硬编码配置信息

我们应该避免在代码中硬编码配置信息,比如域名信息、企业文案、服务器链接。而是预留模板, 在部署时通过运行容器来配置。

  1. 首先是文案信息,可以使用 assets 库或者语言包, 详见 主题包。

  2. 对于配置信息,应该从 process.env.* 中获取,比如 publicPath 在 vue-cli 下可以通过 process.env.BASE_URL 引用:

js
-// ❌ 硬编码域名信息
-const defaultImage = 'https://example.com/__apps__/myapp/test.png'
+// ✅ 通过编译常量获取
+const defaultImage = `${process.env.BASE_URL}test.png`

不过,对于应用内的静态资源,不应该通过组装 URL 的形式来访问,而是通过模块导入:

js
-// ❌ 通过 URL 访问应用独占的静态资源
-const defaultImage = 'https://example.com/__apps__/myapp/test.png'
+// ✅ 使用模块导入
+import defaultImage from './test.png'

注意

@mfejs/vue-cli-plugin-mfapp-child 下,默认的 publicPath 是子应用的部署目录:

  • 开发环境:${devServer.https ? 'https:' : 'http:'}//localhost:${devServer.port}/
  • 生产环境: [%= cdnDomain ? '//' + cdnDomain : '' %][%= removeTrailingSlash(base) %]/__apps__/${name}/

按照 webpack 的约定, publicPath 应该以 '/' 结束

vue-cli 中,只有 NODE_ENV 和 BASE_URL 两个内置变量,大多数场景你还是需要自定义变量。 自定义变量有两种方式:

  • 使用 环境变量
  • (推荐) 使用 @mfejs/vue-cli-plugin-mfapp-child 的 constants 参数

假设我们要创建一个 CDN_ORIGIN 变量, CDN_ORIGIN 表示 CDN 源 URL,形式如 https://cdn.xxx.com:port(结尾没有 '/')

js
// vue.config.js
const { defineConfig } = require('@vue/cli-service');
const { defineMFAppChild } = require('@mfejs/vue-cli-plugin-mfapp-child');

module.exports = defineConfig({
  transpileDependencies: false,
  pluginOptions: {
    ...defineMFAppChild({
      // ... 其他配置
      constants: {
        CDN_ORIGIN:
          process.env.NODE_ENV === 'development'
            ? // 开发环境, 随意,可以直接用线上地址
              'https://cdn.xxxx.com'
            : // 生产环境使用模板, 通过运行容器注入
              `[%= cdnDomain ? '//' + cdnDomain : '' %]`,
      },
    }),
  },
  lintOnSave: false,
});

代码使用示例:

js
-const PREVIEW_URL = 'https://cdn.xxxx.com/resources/preview.html'
+const PREVIEW_URL = process.env.CDN_ORIGIN +  '/resources/preview.html'

在 html 模板中、SCSS、LESS 文件中,我们也可以使用这些变量:

html
<html>
  <head>
    <!-- ... -->
    <!-- html-webpack-plugin 默认使用的是 lodash template 语法 -->
    <script src="<%= CDN_ORIGIN %>/resources/lib/wxapi.js %>"></script>
  </head>
</html>

SCSS 文件示例:

css
.demo {
  background-image: url($CDN_ORIGIN + '/resources/hello.png');
}

LESS 文件示例

css
.hello {
  background: url('@{CDN_ORIGIN}foo.img');
}

10. 本地调试

Ok, 一切准备就绪, 开始本地调试。

我们不需要在本地启动基座、登录等页面。只需要启动子应用本身即可:

子应用启动后,会在控制台输出应用描述信息。

11. 部署

最后就是部署上线了。

12. 注册子应用

手动注册。手动将描述信息注册到运行容器的配置文件中

13. 其他

独立模式

将 independent 开启后,基座会为该子应用创建一个独立的路由,这个页面由该子应用独占渲染。换句话说,就是子应用不会在菜单框架下渲染。

适用于登录、应用切换这类不在主页面下运行的页面。

TIP

和全屏模式 或无菜单模式 的区别:

  • 全屏和无菜单模式都是在主页面下渲染的,只不过是菜单隐藏起来了而已
  • 主页面会加载菜单、检查登录状态。independent 不会执行这些逻辑

菜单多语言

基座不会维护子应用的菜单多语言包,这个由子应用自行注册

菜单图标

同样,基座提供了内置的图标,如果基座想要自定义图标也可以通过 registerAsset 接口注册

Released under the MIT License.