前言

微前端将web应用由单一的单体应用转变为多个小型前端应用聚合为一的应用,每个前端应用还可以独立运行、独立开发、独立部署。同时,他们也可以在共享组件的同时进行并行开发–这些组件可以通过Git Tag等来管理。

使用微前端框架对旧有的服务运行,而使用新框架构建新服务是最好的尝试。

微前端构建的应用是前后端分离的单页面应用,在此基础上微前端才有意义。

分类

微前端架构一般分为以下路线:

  • 使用HTTP服务器的路由来重定向多个应用
  • 自制框架:在不同的框架上设计通讯、加载机制、如Mooa
  • 通过组合多个独立应组件来构建一个单体应用
  • 使用iFrame及自定义消息传递机制
  • 使用纯 Web Components构建应用
  • 结合 Web Components 构建

在形式上说,单体前端框架的路由和单体后端应用没有太大区别:依据不同的路由,来返回不同页面的模版

原先单页面应用的路由配置:

const appRoutes: Routes = [
  { path: 'index', component: IndexComponent },
  { path: 'detail/:id', component: DetailComponent },
];

当我们将之微服务后,应用A的路由:

const appRoutes: Routes = [
  { path: 'index', component: IndexComponent },
];

以及应用B的路由:

const appRoutes: Routes = [
  { path: 'detail/:id', component: DetailComponent },
];

路由分发式微前端

使用HTTP服务器的反向代理或应用框架自带的路由,通过路由将不同的业务分发到不同的、独立前端应用上。

这种方式每跳转到一个应用,相当于刷新一次页面

如下是一个基于路由分发的Nginx配置:

http {
  server {
    listen       80;
    server_name  www.phodal.com;
    location /api/ {
      proxy_pass http://http://172.31.25.15:8000/api;
    }
    location /web/admin {
      proxy_pass http://172.31.25.29/web/admin;
    }
    location /web/notifications {
      proxy_pass http://172.31.25.27/web/notifications;
    }
    location / {
      proxy_pass /;
    }
  }
}

使用iFrame构建容器

采用iFrame可以创建一个全新的独立的宿主环境,有效地将另一个HTML页面嵌入到当前页面中

采用iFrame有两个重要的前提:

  • 网站不需要SEO支持
  • 拥有相应的应用管理机制

自制框架兼容应用

不论是基于 Web Components 的 Angular,或者是 VirtualDOM 的 React 等,现有的前端框架都离不开基本的 HTML 元素 DOM

那么,我们只需要:

  1. 在页面合适的地方引入或创建DOM
  2. 用户操作时,加载对应的应用(触发应用的启动),并卸载应用

第2个问题关键在移除DOM和相应应用的监听。

尽管如react等Single-Page框架已经拥有启动和卸载处理,但是它仍然不适应生产用途,需要重写一个框架,如Mooa

组合式集成:将应用微件化

组合式集成,即通过软件工程的方式在构建前、构建时、构建后等步骤中,对应用进行一步的拆分,并重新组合。

从这种定义上来看,它可能算不上并不是一种微前端——它可以满足了微前端的三个要素,即:独立运行、独立开发、独立部署。但是,配合上前端框架的组件 Lazyload 功能——即在需要的时候,才加载对应的业务组件或应用,它看上去就是一个微前端应用。

但是,首先它有一个严重的限制:必须使用同一个框架。对于多数团队来说,这并不是问题。采用微服务的团队里,也不会因为微服务这一个前端,来使用不同的语言和技术来开发。当然了,如果要使用别的框架,也不是问题,我们只需要结合上一步中的自制框架兼容应用就可以满足我们的需求。

采用这种方式由一些限制,就是规范,我们需要:

  • 统一依赖。统一这些依赖的版本,引入新的依赖时需要一一加入。
  • 规范应用的组件及路由。避免不同应用之间组件名称冲突
  • 构建复杂。在有些方案中,我们需要修改构建系统,有些方案里则需要复杂的构建脚本。
  • 共享通用代码。
  • 制定代码规范

纯 Web Components 技术构建

Web Components 是一套不同的技术,允许构建可重用的定制元素(它们封装在您的代码之外)并且在您的web应用中使用它们,它主要由四项技术组件:

  • Custom elements,允许开发者创建自定义的元素
  • Shadow DOM,即影子DOM,通常是将Shadow DOM附加到主文档DOM中,并可以控制其关联的功能。而这个Shadow DOM则是不能直接用其他主文档DOM来控制。
  • HTML templates,即<template><slot>元素,用于编写不在页面中显示的标记模版。
  • HTML Imports,用于引入自定义组件

每个组件由link标签引入:

<link rel="import" href="components/di-li.html">
<link rel="import" href="components/d-header.html">

随后,在各自的HTML文件中,构建相应的组件元素,编写相应的组件逻辑。

qiankun微前端

基于qiankun,先在主应用中创建微应用的承载容器,这个容器规定了微应用的显示区域,微应用将在该容器内渲染并显示。

以Vue为基座,路由文件规定了主应用自身的路由匹配规则:

// micro-app-main/src/routes/index.ts
import Home from "@/pages/home/index.vue";

const routes = [
  {
    /**
     * path: 路径为 / 时触发该路由规则
     * name: 路由的 name 为 Home
     * component: 触发路由时加载 `Home` 组件
     */
    path: "/",
    name: "Home",
    component: Home,
  },
];

export default routes;

// micro-app-main/src/main.ts
//...
import Vue from "vue";
import VueRouter from "vue-router";

import routes from "./routes";

/**
 * 注册路由实例
 * 即将开始监听 location 变化,触发路由规则
 */
const router = new VueRouter({
  mode: "history",
  routes,
});

// 创建 Vue 实例
// 该实例将挂载/渲染在 id 为 main-app 的节点上
new Vue({
  router,
  render: (h) => h(App),
}).$mount("#main-app");

构建好主框架后,使用qiankun的registerMicroApps方法注册微应用,如下:

// micro-app-main/src/micro/apps.ts
// 此时我们还没有微应用,所以 apps 为空
const apps = [];

export default apps;

// micro-app-main/src/micro/index.ts
// 一个进度条插件
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import { message } from "ant-design-vue";
import {
  registerMicroApps,
  addGlobalUncaughtErrorHandler,
  start,
} from "qiankun";

// 微应用注册信息
import apps from "./apps";

/**
 * 注册微应用
 * 第一个参数 - 微应用的注册信息
 * 第二个参数 - 全局生命周期钩子
 */
registerMicroApps(apps, {
  // qiankun 生命周期钩子 - 微应用加载前
  beforeLoad: (app: any) => {
    // 加载微应用前,加载进度条
    NProgress.start();
    console.log("before load", app.name);
    return Promise.resolve();
  },
  // qiankun 生命周期钩子 - 微应用挂载后
  afterMount: (app: any) => {
    // 加载微应用前,进度条加载完成
    NProgress.done();
    console.log("after mount", app.name);
    return Promise.resolve();
  },
});

/**
 * 添加全局的未捕获异常处理器
 */
addGlobalUncaughtErrorHandler((event: Event | string) => {
  console.error(event);
  const { message: msg } = event as any;
  // 加载失败时提示
  if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
    message.error("微应用加载失败,请检查应用是否可运行");
  }
});

// 导出 qiankun 的启动函数
export default start;

微应用注册信息在apps数组中,然后使用registerMicroApps方法注册微应用,最后导出start函数

注册React微应用

以react为例子,注册微应用

首先,在主应用中注册微应用的信息,如下:

// micro-app-main/src/micro/apps.ts
const apps = [
  /**
   * name: 微应用名称 - 具有唯一性
   * entry: 微应用入口 - 通过该地址加载微应用
   * container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
   * activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
   */
  {
    name: "ReactMicroApp",
    entry: "//localhost:10100",
    container: "#frame",
    activeRule: "/react",
  },
];

export default apps;

在主应用中注册了我们的react微应用,进入/react路由时加载我们的react微应用

在菜单配置处也加入react微应用的快捷入口,如中后台中子菜单选项启动微应用

// micro-app-main/src/App.vue
//...
export default class App extends Vue {
  /**
   * 菜单列表
   * key: 唯一 Key 值
   * title: 菜单标题
   * path: 菜单对应的路径
   */
  menus = [
    {
      key: "Home",
      title: "主页",
      path: "/",
    },
    {
      key: "ReactMicroApp",
      title: "React 主页",
      path: "/react",
    },
    {
      key: "ReactMicroAppList",
      title: "React 列表页",
      path: "/react/list",
    },
  ];
}

随后,在react的入口文件index.js中,导出qiankun主应用所需的三个生命周期钩子函数:

// micro-app-react/src/public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  // 动态设置 webpack publicPath,防止资源加载出错
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

// micro-app-react/src/index.js
import React from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";

import "./public-path";
import App from "./App.jsx";

/**
 * 渲染函数
 * 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
 */
function render() {
  ReactDOM.render(<App />, document.getElementById("root"));
}

// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log("ReactMicroApp bootstraped");
}

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  console.log("ReactMicroApp mount", props);
  render(props);
}

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  console.log("ReactMicroApp unmount");
  ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}

最后,新建config.overrides.js文件来配置webpack

const path = require("path");

module.exports = {
  webpack: (config) => {
    // 微应用的包名,这里与主应用中注册的微应用名称一致
    config.output.library = `ReactMicroApp`;
    // 将你的 library 暴露为所有的模块定义下都可运行的方式
    config.output.libraryTarget = "umd";
    // 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
    config.output.jsonpFunction = `webpackJsonp_ReactMicroApp`;

    config.resolve.alias = {
      ...config.resolve.alias,
      "@": path.resolve(__dirname, "src"),
    };
    return config;
  },

  devServer: function (configFunction) {
    return function (proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);
      // 关闭主机检查,使微应用可以被 fetch
      config.disableHostCheck = true;
      // 配置跨域请求头,解决开发环境的跨域问题
      config.headers = {
        "Access-Control-Allow-Origin": "*",
      };
      // 配置 history 模式
      config.historyApiFallback = true;

      return config;
    };
  },
};