1. 概述

微前端是一种软件架构,可以将前端应用拆解成一些更小的能够独立开发部署的微型应用,然后再将这些微应用进行组合使其成为整体应用的架构模式。微前端架构类似于组件架构,但不同的是,组件不能独立构建和发布,但是微前端中的应用是可以的。微前端架构与框架无关,每个微应用都可以使用不同的框架。

1. 微前端的价值

1.增量迁移

迁移是一项非常耗时且艰难的任务,比如有一个管理系统使用AngularJS开发维护已经有三年时 间,但是随时间的推移和团队成员的变更,无论从开发成本还是用人需求上,AngularJS已经不能 满足要求,于是团队想要更新技术栈,想在其他框架中实现新的需求,但是现有项目怎么办?直接 迁移是不可能的,在新的框架中完全重写也不太现实。

使用微前端架构就可以解决问题,在保留原有项目的同时,可以完全使用新的框架开发新的需求,然后再使用微前端架构将旧的项目和新的项目进行整合。这样既可以使产品得到更好的用户体验,也可以使团队成员在技术上得到进步,产品开发成本也降到的最低。

2.独立发布

在目前的单页应用架构中,使用组件构建用户界面,应用中的每个组件或功能开发完成或者bug修复完成后,每次都需要对整个产品重新进行构建和发布,任务耗时操作上也比较繁琐。

在使用了微前端架构后,可以将不能的功能模块拆分成独立的应用,此时功能模块就可以单独构建单独发布了,构建时间也会变得非常快,应用发布后不需要更改其他内容应用就会自动更新,这意味着你可以进行频繁的构建发布操作了。

3.允许单个团队做出技术决策

因为微前端构架与框架无关,当一个应用由多个团队进行开发时,每个团队都可以使用自己擅长的技术栈进行开发,也就是它允许适当的让团队决策使用哪种技术,从而使团队协作变得不再僵硬。

微前端一般的使用场景包括拆分矩形应用,使应用变得更加可维护,兼容历史应用,实现增量开发。

2. 如何实现微前端

1.多个微应用如何进行组合

在微前端架构中,除了存在多个微应用以外,还存在一个容器应用,每个微应用都需要被注册到容器应用中。微前端中的每个应用在浏览器中都是一个独立的JavaScript模块,通过模块化的方式被容器应用启 动和运行。使用模块化的方式运行应用可以防止不同的微应用在同时运行时发生冲突。

2.在微应用中如何实现路由

在微前端架构中,当路由发生变化时,容器应用首先会拦截路由的变化,根据路由匹配微前端应用,当匹配到微应用以后,再启动微应用路由,匹配具体的页面组件。

3.微应用与微应用之间如何实现状态共享

在微应用中可以通过发布订阅模式实现状态共享,比如使用RxJS

4.微应用与微应用之间如何实现框架和库的共享

通过import-mapwebpack中的externals属性。

2. Systemjs

Systemjs是一个动态模块加载器,在微前端架构中每一个微应用都会被打包成模块,需要在浏览器中加载他并且运行他,但浏览器并不支持模块化,需要使用systemjs来实现浏览器中的模块化。systemjs是一个用于实现模块化的js库,有属于自己的模块化规范,在开发阶段可以使用ES模块规范,然后使用webpack将其转换为systemjs支持的模块。

比如可以通过webpackreact应用打包为systemjs模块,再通过systemjs在浏览器中加载模块。

npm install webpack@5.17.0 webpack-cli@4.4.0 webpack-dev-server@3.11.2 html-webpack-
plugin@4.5.1 @babel/core@7.12.10 @babel/cli@7.12.10 @babel/preset-env@7.12.11
@babel/preset-react@7.12.10 babel-loader@8.2.2
// package.json
{
    "name": "systemjs-react",
        "scripts": {
        "start": "webpack serve"
    },
    "dependencies": {
        "@babel/cli": "^7.12.10",
        "@babel/core": "^7.12.10",
        "@babel/preset-env": "^7.12.11",
        "@babel/preset-react": "^7.12.10",
        "babel-loader": "^8.2.2",
        "html-webpack-plugin": "^4.5.1",
        "webpack": "^5.17.0",
        "webpack-cli": "^4.4.0",
        "webpack-dev-server": "^3.11.2"
    }
}

首先需要在webpack配置的输出位置,指定打包使用system模块。还要使用externals将公共模块排除。因为微前端中公共模块都是公共的比如reactreact-dom

// webpack.config.js
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")
module.exports = {
    mode: "development",
    entry: "./src/index.js",
    output: {
        path: path.join(__dirname, "build"),
        filename: "index.js",
        libraryTarget: "system"
    },
    devtool: "source-map",
    devServer: {
        port: 9000,
        contentBase: path.join(__dirname, "build"),
        historyApiFallback: true
    },
        module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: ["@babel/preset-env", "@babel/react"]
                    }
                }
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            inject: false,
            template: "./src/index.html"
        })
    ],
    externals: ["react", "react-dom", "react-router-dom"]
}

在页面中通过script的方式将system引入进来。然后就可以使用system动态引入reactreact-dom以及react-router-dom,使用方式就是添加一个type值为systemjs-importmapsystem模块,systemjs-importmapsystem提供的,不是浏览器原生的。在script中添加的对象就会自动添加到system系统中。

还有就是不能通过webpack将打包后的js注入到页面中,应该使用system加载进来。在body中添加script标签里面写上System.import导入模块。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>systemjs-react</title>
    <script type="systemjs-importmap">
      {
        "imports": {
          "react": "https://cdn.jsdelivr.net/npm/react/umd/react.production.min.js",
          "react-dom": "https://cdn.jsdelivr.net/npm/react-dom/umd/react-dom.production.min.js",
          "react-router-dom": "https://cdn.jsdelivr.net/npm/react-router-dom@5.2.0/umd/react-router-dom.min.js"
                }
            }
        </script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.10.3/dist/system.min.js"></script>
    </head>
  <body>
    <div id="root"></div>
    <script>
      System.import("./index.js")
    </script>
  </body>
</html>

这就是system.js的基本使用。

3. Single-Spa

single-spa是一个实现微前端架构的框架,在single-spa框架中有三种类型的微前端应用。

1.single-spa-application/parcel微前端架构中的微应用,可以使用vuereactangular等框架。single-spa-application和路由相关联,single-spa-parcel不和路由相关联,主要用于跨应用共享UI组件的。

2.single-spa root config是微前端架构中的容器应用,主要管理微应用的,用于创建微前端容器应用。

3.utility modules是公共模块应用,不是用于渲染组件的,是用于跨应用共享javascript逻辑的微应用。

1. 创建容器应用

首先需要安装single-spa脚手架工具。

npm install create-single-spa@2.0.3 -g

# 创建微前端应用目录
mkdir workspace
# 进入目录
cd workspace
# 创建微前端容器应用
create-single-spa

应用文件夹填写container,应用选择single-spa root config,接着选择npm安装依赖,不使用TS就输入N,同样不适应布局引擎,输入N,组织名称填写yindong,组织名称可以理解为团队名称,微前端架构允许多团队共同开发应用,组织名称可以标识应用由哪个团队开发。应用名称的命名规则为@组织名称/应用名称,比如@yindong/todos。初始化之后就可以启动应用了。

# 启动应用
npm run start

接着可以看下创建好的容器应用相关代码,src是开发的目录默认会有index.ejsstudy-root-cofig.js两个文件,index.ejs是模板文件并且再整个应用中只有这一个模板文件,其他微应用是不包含模板文件的,study-root-config.js是容器应用的入口文件。

首先可以看下study-root-config.js文件,这里引入了registerApplicationstart两个方法,registerApplication是用来注册微应用的,start是用来启动应用的。这里默认通过registerApplication注册的应用就是启动后看到的那个页面。

// workspace/container/src/study-root-config.js
import { registerApplication, start } from "single-spa"
/*
注册微前端应用
1. name: 字符串类型, 微前端应用名称 "@组织名称/应用名称"
2. app: 函数类型, 返回 Promise, 通过 systemjs 引用打包好的微前端应用模块代码
(umd)
3. activeWhen: 路由匹配时激活应用
*/
registerApplication({
  name: "@single-spa/welcome",
  app: () => System.import("https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"),
  activeWhen: ["/"]
})
// start 方法必须在 single spa 的配置文件中调用
// 在调用 start 之前, 应用会被加载, 但不会初始化, 挂载或卸载. 
start({
// 是否可以通过 history.pushState() 和 history.replaceState() 更改触发 single-spa 路由
// true 不允许 false 允许
  urlRerouteOnly: true
})

index.ejs

<!-- 导入微前端容器应用 -->
<script>
  System.import("@study/root-config")
</script>
<!--
import-map-overrides 可以覆盖导入映射 当前项目中用于配合 single-spa Inspector 调试工具使用. 可以手动覆盖项目中的 JavaScript 模块加载地址, 用于调试.
-->
<import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
<!-- 模块加载器 -->
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.0/dist/system.min.js"></script>
<!-- systemjs 用来解析 AMD 模块的插件 -->
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.0/dist/extras/amd.min.js"></script>
<!-- 用于覆盖通过 import-map 设置的 JavaScript 模块下载地址 -->
<script src="https://cdn.jsdelivr.net/npm/import-map- overrides@2.2.0/dist/import-map-overrides.js"></script>
<!-- 用于支持 Angular 应用 -->
<script src="https://cdn.jsdelivr.net/npm/zone.js@0.10.3/dist/zone.min.js"></script>
<!-- single-spa 预加载 -->
<link
  rel="preload"
  href="https://cdn.jsdelivr.net/npm/single-spa@5.8.3/lib/system/single-spa.min.js"
  as="script"
/>
<!-- JavaScript 模块下载地址 此处可放置微前端项目中的公共模块 -->
<script type="systemjs-importmap">
  {
    "imports": {
      "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.8.3/lib/system/single-spa.min.js"
        } 
    }
</script>

2. 创建微应用

创建一个不基于任何框架的微应用。

# 应用初始化
mkdir yindong && cd "$_"

初始化依赖。

{
    "name": "yindong",
    "version": "1.0.0",
    "description": "",
    "main": "webpack.config.js",
    "scripts": {
        "start": "webpack serve"
    },
    "dependencies": {
        "@babel/core": "^7.12.10",
        "single-spa": "^5.9.0",
        "webpack": "^5.8.0",
        "webpack-cli": "^4.2.0",
        "webpack-config-single-spa": "^2.0.0",
        "webpack-dev-server": "^4.0.0-bate.0",
        "webpack-merge": "^5.4.0"
    }
}

配置webpack

const { merge } = require("webpack-merge");
const singleSpaDefaults = require("webpack-config-single-spa");

module.exports = () => {
  const defaultConfig = singleSpaDefaults({
        // 组织名称
        orgName: "study", 
        // 项目名称 
        projectName: "yindong"
    })
  return merge(defaultConfig, {
    devServer: {
            port: 9001
        }
    })
}

创建入口文件,注意入口文件名字必须是组织名称-项目名称,也就是study-yindong.js。在应用入口文件中导出微前端应用所需的3个生命周期函数,生命周期函数必须返回Promise。这3个生命周期函数都是应用的生命周期函数,启动、挂载、卸载。

let yindongContainer = null

export const bootstrap = async function () { 
    console.log("应用正在启动")
}

export const mount = async function () {
    console.log("应用正在挂载")
    yindongContainer = document.createElement("div");
    yindongContainer.innerHTML = "Hello Yindong";
    yindongContainer.id = "yindongContainer";
    document.body.appendChild(yindongContainer)
}

export const unmount = async function () {
    console.log("应用正在卸载")
  document.body.removeChild(yindongContainer)
}

主应用中将微前端容器应用中注册微前端应用。

registerApplication({
  name: "@study/yindong",
  app: () => System.import("@study/yindong"),
  activeWhen: ["/yindong"]
})

主应用的index.ejs中在模板文件中指定模块访问地址。

<script type="systemjs-importmap">
{
  "imports": {
     "@study/yindong": "//localhost:9001/study-yindong.js"
    }
}
</script>

registerApplication可以换一种注册方式。

// 注意: 参数的传递方式发生了变化, 原来是传递了一个对象, 对象中有三项配置, 现在是传递了三 个参数
registerApplication(
  "@single-spa/welcome",
  () =>
    System.import(
      "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
    ),
  location => location.pathname === "/"
)

3. 创建基于 React 的微应用

同样需要使用create-single-spa命令进行创建。应用目录输入todos

# 创建应用
create-single-spa
# 应用目录输入 todos
# 框架选择 react

找到package.json文件修改应用端口和启动应用。修改之后就可以启动应用了。

{
  "scripts": {
     "start": "webpack serve --port 9002",
  }
}

在主应用中将React项目的入口文件注册到基座应用中。container/src/study-root-config.js

registerApplication({
  name: "@study/todos",
  app: () => System.import("@study/todos"),
  activeWhen: ["/todos"]
})

在主应用中需要指定微前端应用模块的引用地址。

<!--
在注册应用时 systemjs 引用了 @study/todos 模块, 所以需要配置该模块的引用地址
-->
<script type="systemjs-importmap">
{
  "imports": {
     "@study/root-config": "//localhost:9000/study-root-config.js",
     "@study/todos": "//localhost:9002/study-todos.js"
    }
}
</script>

默认情况下,应用中的reactreact-dom没有被webpack打包,single-spa认为它是公共库,不应该单独打包。

<script type="systemjs-importmap">
{
  "imports": {
     "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.8.3/lib/system/single-spa.min.js",
     "react": "https://cdn.jsdelivr.net/npm/react@17.0.1/umd/react.production.min.js",
     "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@17.0.1/umd/react-dom.production.min.js",
     "react-router-dom": "https://cdn.jsdelivr.net/npm/react-router-dom@5.2.0/umd/react-router-dom.min.js"
  }
}
</script>

微前端React应用入口文件代码解析study-todos.js

// react、react-dom 的引用是 index.ejs 文件中 import-map 中指定的版本 import React from "react"
import ReactDOM from "react-dom"
// single-spa-react 用于创建使用 React 框架实现的微前端应用
import singleSpaReact from "single-spa-react" // 用于渲染在页面中的根组件
import rootComponent from "./root.component"
// 指定根组件的渲染位置
const domElementGetter = () => document.getElementById("todosContainer") // 错误边界函数
const errorBoundary = () => <div>发生错误时此处内容将会被渲染</div>
// 创建基于 React 框架的微前端应用, 返回生命周期函数对象 const lifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent,
  domElementGetter,
  errorBoundary
})
// 暴露必要的生命周期函数,不急于框架的微应用需要寿佛那个创建生命周期,基于框架的微应用自动创建好了生命周期。
export const { bootstrap, mount, unmount } = lifecycles;

路由配置,在todos/src目录创建两个组件HomeAbout,然后在root.component.js根组件中引入这两个组件并且使用。

import React from "react";
import {BrowserRouter, Switch, Route, Redirect, Link} from "react-router-dom";
import Home from "./pages/Home";
import About from "./pages/About";

export default function Root(props) {
  return (
    <BrowserRouter basename="/todos">
      <div>{props.name}</div>
      <div>
        <Link to="/home">Home</Link>
        <Link to="/about">About</Link>
      </div>
      <Switch>
        <Route path="/home">
          <Home />
        </Route>
        <Route path="/about">
          <About />
        </Route>
        <Route path="/">
          <Redirect to="/home" />
        </Route>
      </Switch>
    </BrowserRouter>
    )
}

子应用中修改webpack配置,告诉webpack不要打包react-router-dom

const { merge } = require("webpack-merge")
const singleSpaDefaults = require("webpack-config-single-spa-react")
module.exports = (webpackConfigEnv, argv) => {
  const defaultConfig = singleSpaDefaults({
    orgName: "study",
    projectName: "todos",
    webpackConfigEnv,
    argv
  })
  return merge(defaultConfig, {
    externals: ["react-router-dom"]
  })
}

4. 创建基于 Vue 的微应用

通过create-single-spa命令创建以个叫做realword的微应用。

# 创建应用
create-single-spa
# 项目文件夹填写 realworld 
# 框架选择 Vue
# 生成 Vue 2 项目

vue框架应该是一个公共框架,修改webpack不要打包vuevue-router,新建vue.config.js文件,加入externals

// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.externals(["vue", "vue-router", "single-spa"])
  }
}

ejs文件里面配置vuevue-router

<script type="systemjs-importmap">
{
  "imports": {
     "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js",
     "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js"
  }
}
</script>

修改启动命令,端口设置为9003,然后启动项目。

"scripts": {
  "start": "vue-cli-service serve --port 9003",
}

在主应用的study-root-config.js文件中引入上面的应用。

registerApplication({
  name: "@study/realworld",
  app: () => System.import("@study/realworld"),
  activeWhen: ["/realworld"]
})

还需要在模板文件中配置这个应用。

<script type="systemjs-importmap">
{
  "imports": {
     "@study/root-config": "//localhost:9000/study-root-config.js",
     "@study/realworld": "//localhost:9003/study-realworld.js"
    }
}
</script>

vuevue-routeramd模块,需要使用amd加载器。

<!-- systemjs 用来解析 AMD 模块的插件 -->
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.0/dist/extras/amd.min.js"></script>

Vue应用配置路由,在配置路由的时候有一点不一样,需要配置base: "/realworld",否则无法正常进行工作。

import Vue from "vue"
import VueRouter from "vue-router"
import singleSpaVue from "single-spa-vue"
import App from "./App.vue"
Vue.use(VueRouter)
Vue.config.productionTip = false;
// 路由组件
const Foo = { template: "<div>foo</div>" } const Bar = { template: "<div>bar</div>" }
// 路由规则
const routes = [
  { path: "/foo", component: Foo },
  { path: "/bar", component: Bar }
]
// 路由实例
const router = new VueRouter({ routes, mode: "history", base: "/realworld"});
const vueLifecycles = singleSpaVue({
  Vue,
    // 应用配置
    appOptions: {
        // 路由
        router,
        // 渲染组件
        render(h) {
            return h(App, {
            // 向组件中传递的数据 
                props: {
                    name: this.name,
                    mountParcel: this.mountParcel,
                    singleSpa: this.singleSpa
                } 
            })
        }
    }
})

// 导出生命周期函数
export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;
<template>
  <div id="app">
    <h1>{{ name }}</h1>
    <p>
      <router-link to="/foo">Go to Foo</router-link>
      <router-link to="/bar">Go to Bar</router-link>
    </p>
    <router-view></router-view>
  </div>
</template>
<script>
    export default {
        name: "App",
        props: ["name"]
    }
</script>

5. 创建 Parcel 应用

Parcel用来创建公共UI,涉及到跨框架共享UI时需要使用ParcelParcel的定义可以使用任何single-spa支持的框架,可以使用react或者vue创建Parcel,当然使用react创建的Parcel也可以在vue中使用,因为它也是单独的应用,需要单独启动,但是它不关联路由。Parcel应用的模块访问地址需要被添加到import-map中,对于其他微应用通过System.import方法进行引用。

这里通过创建navbar parcel,在不同的应用中使用它来演示Parcel

# 使用 React 创建 Parcel 应用
create-single-spa
# 输入 navbar
# 选择Parcel
# 选择react

navbar/src/root-component.js

import React from "react"
import { BrowserRouter, Link } from "react-router-dom"

export default function Root(props) {
  return (
    <BrowserRouter>
      <div>
        <Link to="/">@single-spa/welcome</Link>{" "}
        <Link to="/yindong">@study/yindong</Link>{" "}
        <Link to="/todos">@study/todos</Link>{" "}
        <Link to="/realworld">@study/realworld</Link>
      </div>
    </BrowserRouter>
  ) 
}

navbar/src/study-navbar.js

import React from "react"
import ReactDOM from "react-dom"
import singleSpaReact from "single-spa-react"
import Root from "./root.component"
const lifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: Root,
  errorBoundary(err, info, props) {
    // Customize the root error boundary for your microfrontend here.
return null }
})
export const { bootstrap, mount, unmount } = lifecycles

webpack配置文件中去除react-router-dom

externals: ["react-router-dom"]

指定端口9004启动应用。

"scripts": {
   "start": "webpack serve --port 9004",
}

在模板文件中指定应用模块地址。

{
  "imports": {
    "@study/navbar": "//localhost:9004/study-navbar.js"
  }
}

React应用中使用它。

import Parcel from "single-spa-react/parcel"

<Parcel config={System.import("@study/navbar")} />

Vue应用中使用它。

<Parcel :config="parcelConfig" :mountParcel="mountParcel" />

<script>

import Parcel from "single-spa-vue/dist/esm/parcel";
import { mountRootParcel } from "single-spa";

export default {
  components: {
    Parcel
  }, 
  data() {
    return {
      parcelConfig: window.System.import("@study/navbar"),
      mountParcel: mountRootParcel
    }
  }
}
</script>

6. 创建 utility modules

也是一个单独的微应用,用于放置跨应用共享的JavaScript逻辑,它也是独立的应用,需要单独构建单独启动。

# 创建应用
create-single-spa
# 文件夹填写 tools
# 应用选择 in-browser utility module (styleguide, api cache, etc)

修改端口为9005启动应用。

"scripts": {
   "start": "webpack serve --port 9005",
}

应用中导出方法。

export function sayHello(who) {
  console.log(`%c${who} Say Hello`, "color: skyblue")
}

在模板文件中声明应用模块访问地址。

<script type="systemjs-importmap">
{
  "imports": {
    "@study/tools": "//localhost:9005/study-tools.js"
} }
</script>

React应用中使用该方法。

import React, { useEffect, useState } from "react";

function useToolsModule() {
  const [toolsModule, setToolsModule] = useState()
  useEffect(() => {
    System.import("@study/tools").then(setToolsModule)
  }, [])
  return toolsModule
}
const Home = () => {
  const toolsModule = useToolsModule()
  if (toolsModule) toolsModule.sayHello("todos")
  return <div>Todos home works</div>
}
export default Home;

Vue应用中使用该方法。

<button @click="handleClick">button</button>
async handleClick() {
  let toolsModule = await window.System.import("@study/tools")
  toolsModule.sayHello("realworld")
}

7. 跨应用通信

跨应用通信可以使用RxJS,它无关于框架,就是可以在任何其他框架中使用。在微前端中是使用发布订阅方式实现微前端通信和状态共享。RxJS就是这样的设计模式。

首先需要在容器的模板index.ejs文件中添加rxjs

{
  "imports": {
    "rxjs": "https://cdn.jsdelivr.net/npm/rxjs@6.6.3/bundles/rxjs.umd.min.js"
  } 
}

utility modules中导出一个ReplaySubject,也就是tools应用中的study-tools.js添加,它可以广播历史消息,就算应用是动态加载进来的,也可以接收到数据。

import { ReplaySubject } from "rxjs"
export const sharedSubject = new ReplaySubject()

export function sayHello(who) {
  console.log(`${who} sayHello`)
}

然后需要在子应用中订阅,在React应用中订阅它,在组件卸载的时候记得取消订阅。

useEffect(() => {
    let subjection = null
    if (toolsModule) {
      // 订阅
      subjection = toolsModule.sharedSubject.subscribe(console.log)
    }
    // 记得取消订阅
    return () => subjection.unsubscribe()
  }, [toolsModule])

Vue应用中订阅它,同样别忘了取消订阅。

async mounted() {
  let toolsModule = await window.System.import("@study/tools")
  toolsModule.sharedSubject.subscribe(console.log)
}

React中触发事件。

<button onClick={() => { toolsModule.sharedSubject.next("yindong"); }}></button>

跨组件通信会接收历史广播,比如React中发送的广播,切换到Vue应用的时候Vue才会收到,这是一种非常明智的设计。

8. 布局引擎

允许使用组件的方式声明顶层路由,提供了更加便捷的路由API用来注册应用从而改变注册应用方式,使用方式类似于React组件。

# 下载布局引擎
npm install single-spa-layout@1.3.1

在模板index.ejs中添加template标记,把路由配置放置在这个标记当中,给template添加id属性,这个id是要在js中获取的。在template通过single-spa-router标记表示配置路由。

single-spa-router中的每一个route就是一个路由,route中的path属性指向的就是当前访问的地址,application标签里面的name就是要展示的应用。通过default属性来表示默认应用。

导航栏可以使用单独的application标签写在route上面。

<template id="single-spa-layout">
  <single-spa-router>
    <application name="@study/navbar"></application>
    <route default>
      <application name="@single-spa/welcome"></application>
    </route>
    <route path="yindong">
      <application name="@study/yindong"></application>
    </route>
    <route path="todos">
      <application name="@study/todos"></application>
    </route>
    <route path="realworld">
      <application name="@study/realworld"></application>
    </route>
  </single-spa-router>
</template>
<script type="systemjs-importmap">
{
  "imports": {
     "@single-spa/welcome": "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
  }
}
</script>

还需要使用registerApplication进行注册,将上面的标记转换为数组,数组里面的每个对象都是一个有效的registerApplication参数。

import { registerApplication, start } from "single-spa"
import { constructApplications, constructRoutes } from "single-spa-layout"
// 获取路由配置对象
const routes = constructRoutes(document.querySelector("#single-spa-layout")) // 获取路由信息数组
const applications = constructApplications({
  routes,
  loadApp({ name }) {
    return System.import(name)
  }
});

// 遍历路由信息注册应用
applications.forEach(registerApplication)
start({
  urlRerouteOnly: true
});

转载须知

如转载必须标明文章出处文章名称文章作者,格式如下:

转自:【致前端 - zhiqianduan.com】 微前端初实践  "隐冬"
请输入评论...