single-spa 概述

single-spa 是一个实现微前端架构的框架。

在 single-spa 框架中有三种类型的微前端应用:

  1. single-spa application / parcel:微前端架构中的微应用,可以使用 vue、react、angular 等框架。

    1. single-spa Application 和路由相关联的,根据路由决定访问哪个微应用。
    2. single-spa Parcel 的使用方式和前者一样,区别是这种类型的微应用不和路由进行关联,它主要是用于跨应用共享 UI 组件的
  2. single-spa root config:创建微前端容器应用,通过容器应用加载和管理普通的微应用。
  3. utility modules:公共模块应用,非渲染组件,用于跨应用共享 javascript 逻辑的微应用。

使用 create-single-spa 脚手架创建容器应用

初始化项目

# 创建工作目录,存放每个微应用(实际开发中每个微应用一般都是放在不同的开发人员的电脑中)
mkdir workspace
cd workspace# 全局安装脚手架
npm i create-single-spa -g
# 脚手架创建微应用
create-single-spa# 如果不想将脚手架安装到全局,可以使用 npx 运行脚手架
# npx create-single-spa
? Directory for new project container # 创建项目的文件夹(默认 ./)
? Select type to generate single-spa root config # 创建什么类型的应用
? Which package manager do you want to use? npm # 使用什么工具安装 package
? Will this project use Typescript? No # 是否使用 TS
? Would you like to use single-spa Layout Engine No # 是否使用 single-spa 布局引擎
? Organization name (can use letters, numbers, dash or underscore) study # 组织名称

组织名称可以理解为团队名称,微前端架构允许多团队共同开发应用,组织名称可以标识应用由哪个团队开发。

应用名称的命名规则为 @组织名称/项目名称,比如 @study/todos

安装完后启动应用:

cd container
npm start

访问:http://localhost:9000/,看到 Welcome 欢迎页面即表示成功。

容器应用默认代码解析

容器应用默认应该不包含任何页面,但是在 single-spa 的容器应用启动后显示了 Welcome 欢迎页面,这是因为在 single-spa 的容器应用中默认注册了一个微应用,名为 @single-spa/welcome,下面解析一下 single-spa 容器应用默认代码。

src 目录用于存放源代码文件:

  • index.ejs 是模板文件
  • study-root-config.js 是应用入口文件

注意:在整个微前端项目中只有一个模板文件,也就是说,其它微应用是不包含模板文件的。

xxx-root-config.js

// container\src\study-root-config.js
// 引入两个方法:
// - registerApplication: 用于注册微应用
// - start: 用来启动微前端应用
import { registerApplication, start } from 'single-spa'/*** 注册一个微应用(默认的 Welcome 欢迎页面)* name {String} - 微应用名称 `@组织名称/项目名称`* app {() => <Function | Promise>} - 一个返回加载的模块或 Promise 的函数* activeWhen - 指定微应用在什么条件下激活*/
registerApplication({// welcome 微应用名称name: '@single-spa/welcome',// 通过 systemjs 引用打包好的微应用模块代码app: () => System.import('https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js'),// 使用数组,指定首页路由下激活activeWhen: ['/']
})// registerApplication({//   name: "@study/navbar",
//   app: () => System.import("@study/navbar"),
//   activeWhen: ["/"]
// });// 启动当前应用
// start 方法必须在 single-spa 的配置文件中调用
// 调用 start 之前,应用会被加载,但不会初始化、挂载或卸载
start({// 是否可以通过 history.pushState() 和 history.replaceState() 更改触发 single-spa 路由// true: 不允许; false: 允许// 默认是 false// 在某些情况下,将此设置为true可以提高性能urlRerouteOnly: true
})

index.ejs

以下拆分并解析主要内容。

引入 single-spa 和配置预加载:

  <!-- 引入公共模块地址 --><script type="systemjs-importmap">{"imports": {"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js"}}</script><!-- 预加载 single-spa --><link rel="preload" href="https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js" as="script">

引入 systemjs 模块加载器(区分了开发环境,为了引入压缩版本):

  <!-- 引入模块加载器 --><!-- isLocal(Boolean) 表示是否是本地开发环境 --><% if (isLocal) { %><!-- 开发环境 引入未压缩版本 --><!-- systemjs 模块加载器 --><script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.js"></script><!-- systemjs 用来解析 AMD (浏览器优先)模块的插件(如果不使用 AMD 模块可以不引入) --><script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.js"></script><% } else { %><!-- 其它环境 引入压缩版本 --><script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.min.js"></script><script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.min.js"></script><% } %>

引入 root-config 容器应用模块,并通过 system.import() 加载:

  <!-- 引入容器应用模块 --><!-- 开发环境指定本地地址 --><% if (isLocal) { %><script type="systemjs-importmap">{"imports": {"@study/root-config": "//localhost:9000/study-root-config.js"}}</script><% } %><!-- 加载容器应用模块 --><script>System.import('@study/root-config');</script>

或者不使用 import-map,直接引入:

  <!-- 加载容器应用模块 --><script>System.import('./study-root-config.js');</script>

引入浏览器调试工具(single-spa)并使用(需安装浏览器插件):

官方介绍:
single-spa-inspector | single-spa
single-spa-inspector | single-spa

  <!-- 调试工具:用于覆盖通过 import-map 设置的 JavaScript 模块地址 --><script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script><!-- 调试工具。可以通过浏览器调试工具(single-spa-Inspector)更改注册的微应用模块的地址 --><!-- 例如,将线上环境的模块地址更改为开发环境的模块地址 --><import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>

创建不基于框架的微应用

创建一个不基于框架(如 React、Vue)的微应用,只能手动创建,无法用 create-single-spa 脚手架创建。

在 workspace 目录下创建文件夹 foo 存放微应用。

1、添加 package.json

依赖的版本号同上面创建的容器应用中对应依赖一样:

{"name": "foo","version": "1.0.0","description": "","main": "webpack.config.js","scripts": {"start": "webpack serve"},"keywords": [],"author": "","license": "ISC","dependencies": {"@babel/core": "^7.15.0","single-spa": "^5.9.3","webpack": "^5.51.0","webpack-cli": "^4.8.0","webpack-config-single-spa": "^5.0.0","webpack-dev-server": "^4.0.0","webpack-merge": "^5.8.0"}
}

npm install 安装依赖。

2、添加 webpack.config.js 配置文件

// foo\webpack.config.js
const { merge } = require('webpack-merge')// 引入 single-spa 的默认 webpack 配置
const singleSpaDefaults = require('webpack-config-single-spa')module.exports = () => {const defaultConfig = singleSpaDefaults({// 微应用组织名称orgName: 'study',// 微应用项目名称projectName: 'foo'})return merge(defaultConfig, {devServer: {port: 9001}})
}

3、添加入口文件

入口文件的命名规则为 <orgName>-<projectName>.js,当前为 src/study-foo.js

single-spa 框架要求在每个微应用的入口文件中必须导出 3 个返回 Promise 的生命周期函数。

这三个周期函数都是应用级别的,分别为启动、挂载和卸载,容器应用要通过微应用提供的三个周期函数执行微应用的启动、挂载和卸载。

// foo\src\study-foo.js
let fooContainer = null// 启动
export async function bootstrap() {console.log('应用正在启动')
}// 挂载
export async function mount() {console.log('应用正在挂载')fooContainer = document.createElement('div')fooContainer.id = 'fooContainer'fooContainer.innerHTML = 'Hello Foo'
}// 卸载
export async function unmount() {console.log('应用正在卸载')
}

当前还不会触发卸载,因为还没有路由功能,添加路由功能后,从当前应用跳转到另一个应用的时候就会触发卸载。

4、在容器应用中注册微应用

container/src/study-root-config.js 添加代码:

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

container/src/index.ejs 中配置 foo 微应用引入地址:

  <!-- 引入容器应用模块 --><!-- 开发环境指定本地地址 --><% if (isLocal) { %><script type="systemjs-importmap">{"imports": {"@study/root-config": "//localhost:9000/study-root-config.js","@study/foo": "//localhost:9001/study-foo.js"}}</script><% } %>

5、启动微应用

npm start 启动微应用,访问容器应用页面 http://localhost:9000/foo,页面中显示了 Hello Foo 内容。

但是 Welcome 微应用也在当前路由显示了,这是因为 /foo 同样匹配到了 /,修改注册配置以完全匹配 / 地址:

registerApplication({name: '@single-spa/welcome',app: () => System.import('https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js'),// 使用数组,指定首页路由下激活// activeWhen: ['/']// 使用一个返回 Boolean 的函数指定激活条件activeWhen: location => location.pathname === '/'
})

再次访问 /foo 就不会显示 Welcome 微应用了。

创建基于 React 框架的微应用

使用 create-single-spa 可以创建基于 React、Vue、Angular 框架的微应用。

1、创建微应用

在 workspace 目录下打开命令行工具:

create-single-spa
? Directory for new project todos
? Select type to generate single-spa application / parcel
? Which framework do you want to use? react
? Which package manager do you want to use? npm
? Will this project use Typescript? No
? Organization name (can use letters, numbers, dash or underscore) study
? Project name (can use letters, numbers, dash or underscore) todos

2、修改启动端口

修改 package.json 启动脚本:

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

npm start 启动微应用。

3、在容器应用中注册微应用

container/src/study-root-config.js 添加代码:

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

container/src/index.ejs 中配置 foo 微应用引入地址:

  <!-- 引入容器应用模块 --><!-- 开发环境指定本地地址 --><% if (isLocal) { %><script type="systemjs-importmap">{"imports": {"@study/root-config": "//localhost:9000/study-root-config.js","@study/foo": "//localhost:9001/study-foo.js","@study/todos": "//localhost:9002/study-todos.js"}}</script><% } %>

4、配置 react 相关模块引入地址

single-spa 创建的 React 微应用默认不会打包 react 和 react-dom 模块,它认为这两个应该是公共模块,所以需要在容器应用中手动指定这两个模块的引入地址。

  <!-- 引入公共模块地址 --><script type="systemjs-importmap">{"imports": {"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js","react": "https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js","react-dom": "https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js"}}</script>

5、查看效果

现在访问 localhost:9000/todos 看到页面中显示 @study/todos is mounted! 就表示该应用已经注册好了。

6、代码解析

todos 中的源代码文件主要是两个:

  • study-todos.js 入口文件
  • root.component.js 根组件文件
// todos\src\study-todos.js
import React from 'react'
import ReactDOM from 'react-dom'
import singleSpaReact from 'single-spa-react'
import Root from './root.component'// 用于创建基于 React 的微应用
const lifecycles = singleSpaReact({// 传递 react 相关模块React,ReactDOM,// 传递根组件rootComponent: Root,// 错误边界处理errorBoundary(err, info, props) {// Customize the root error boundary for your microfrontend here.// 可以编写 jsxreturn null}
})// singleSpaReact 方法返回的对象包含管理应用的三个周期函数
export const { bootstrap, mount, unmount } = lifecycles
// todos\src\root.component.js
export default function Root(props) {// props.name 即注册微应用时指定的微应用名称(name)return <section>{props.name} is mounted!</section>
}

7、挂载位置

默认微应用的根组件会挂载在 body 目录下 id 为 single-spa-application:<微应用名称> 的节点上:

要想自定义挂载位置可以在创建微应用的时候指定挂载节点:

// todos\src\study-todos.js// 用于创建基于 React 的微应用
const lifecycles = singleSpaReact({React,ReactDOM,rootComponent: Root,// 指定根组件的挂载节点: 一个返回 DOM 的函数domElementGetter: () => document.getElementById('root'),errorBoundary(err, info, props) {return <div>发生错误时此处内容将会被渲染</div>}
})

还要在模板文件 container\src\index.ejs 中添加挂载节点:

<div id="root"></div>

再次查看页面:

基于 React 框架的微前端应用配置路由

1、创建组件

// todos\src\Home.js
const Home = () => {return <div>Home works</div>
}export default Home
// todos\src\About.js
const About = () => {return <div>About works</div>
}export default About

2、修改根组件

注意:这里使用的是 v5 版本的 api

// todos\src\root.component.js
import React from 'react'
import { BrowserRouter, Route, Link, Redirect, Switch } from 'react-router-dom'
import Home from './Home'
import About from './About'export default function Root(props) {return (<BrowserRouter basename="/todos"><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>)
}

3、配置公共模块

react-router-dom 应该作为公共模块被引入,修改 container\src\index.ejs

  <!-- 引入公共模块地址 --><script type="systemjs-importmap">{"imports": {"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js","react": "https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js","react-dom": "https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js","react-router-dom": "https://cdn.jsdelivr.net/npm/react-router-dom@5.3.0/umd/react-router-dom.min.js"}}</script>

webpack 配置禁止打包 react-router-dom:

// todos\webpack.config.js
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, {// 禁止打包 react-router-domexternals: ['react-router-dom']});
};

4、重新启动

修改配置需要重新启动 todos 应用 npm start

创建基于 Vue 框架的微应用

1、创建微应用

创建 Vue 框架微应用的过程中,在输入组织名称后,create-single-spa 会下载 vue-cli 工具,下载完成后使用 vue-cli 继续创建 Vue 项目,创建完成后回到 create-single-spa 创建应用流程中继续填写项目名称:

create-single-spa
? Directory for new project todos
? Select type to generate single-spa application / parcel
? Which framework do you want to use? react
? Which package manager do you want to use? npm
? Will this project use Typescript? No
? Organization name (can use letters, numbers, dash or underscore) study
# 下载 vue-cli 后继续创建 vue 项目,只提示选择使用的 vue 版本,本例选择 vue2.x
# > Default ([Vue 2] babel, eslint)

2、配置 vue 相关公共模块

single-spa 在创建 Vue 微应用的时候并未将 vue 和 vue-router 作为公共模块配置,需要手动修改配置。

首先配置 webpack 不打包 vue 和 vue-router,在 realworld 目录下添加文件 vue-config-js

// realworld\vue.config.js
module.exports = {chainWebpack: config => {// 禁止打包的模块config.externals(['vue', 'vue-router'])}
}

配置公共模块引入地址:

<!-- 引入公共模块地址 --><script type="systemjs-importmap">{"imports": {"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js","react": "https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js","react-dom": "https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js","react-router-dom": "https://cdn.jsdelivr.net/npm/react-router-dom@5.3.0/umd/react-router-dom.min.js","vue": "https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js","vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.5.3/dist/vue-router.min.js"}}</script>

注意:该 CDN 地址加载的 vue 和 vue-router 模块是 AMD 类型的,所以请确保 systemjs 的 AMD 模块解析器被引入。

3、启动项目

修改 realworld/package.json

  "scripts": {"start": "vue-cli-service serve --port 9003","build": "vue-cli-service build","lint": "vue-cli-service lint","serve:standalone": "vue-cli-service serve --mode standalone"},

启动项目 npm start

4、注册微应用

// container\src\study-root-config.js
registerApplication({name: '@study/realworld',app: () => System.import('@study/realworld'),activeWhen: ['/realworld']
})

5、引入应用

Vue 项目下并没有 src/study-realword.js 文件,它的入口源文件是 src/main.js,项目打包后仍会生成名为 js/app.js 的入口文件。

要想知道微应用的入口文件,也可以访问启动应用后的根目录,single-spa 会提示入口文件:

在容器应用中注册微应用:

  <!-- 引入容器应用模块 --><!-- 开发环境指定本地地址 --><% if (isLocal) { %><script type="systemjs-importmap">{"imports": {"@study/root-config": "//localhost:9000/study-root-config.js","@study/foo": "//localhost:9001/study-foo.js","@study/todos": "//localhost:9002/study-todos.js","@study/realworld": "//localhost:9003/js/app.js"}}</script><% } %>

访问 http://localhost:9000/realworld 查看效果

6、屏蔽报错

现在虽然 Vue 微应用已经运行到容器中,不过控制台还是报错:

Refused to connect to 'http://xxx.xxx.xxx.xxx:9003/sockjs-node/info?t=1642400465303' because it violates the following Content Security Policy directive: "connect-src https: localhost:* ws://localhost:*".

这是因为模板文件中设置了 Content-Security-Policyconnect-src,它限制了能通过脚本接口加载的 URL。

而 vue-cli 创建的项目默认会向本地 ip(而不是 localhost)发送 websocket 请求,地址如下:

  • http://xxx.xxx.xxx.xxx:9003/sockjs-node/info?t=1642400465303
  • ws://xxx.xxx.xxx.xxx:9003/sockjs-node/938/ddz3z0b0/websocket

可以将本地地址添加进去:http://xxx.xxx.xxx.xxx:* ws://xxx.xxx.xxx.xxx:*

也可以直接将 Content-Security-Policy 注释掉。

基于 Vue 框架的微前端应用配置路由

realworld/src/main.js 是基于 Vue 框架的微应用的入口文件:

// realworld\src\main.js
import Vue from 'vue'
import singleSpaVue from 'single-spa-vue'
import VueRouter from 'vue-router'import App from './App.vue'Vue.use(VueRouter)Vue.config.productionTip = false// 路由组件
const Bar = { template: '<div>Bar works</div>' }
const Baz = { template: '<div>Baz works</div>' }// 路由规则
const routes = [{ path: '/bar', component: Bar },{ path: '/baz', component: Baz }
]// 路由实例
const router = new VueRouter({routes,mode: 'history',base: '/realworld'
})// 创建基于 Vue 的微应用
const vueLifecycles = singleSpaVue({// 传入 vue 模块Vue,// 应用配置appOptions: {// 路由router,// 渲染组件render(h) {return h(App, {// 向组件中传递的数据props: {name: this.name// single-spa props are available on the "this" object. Forward them to your component as needed.// https://single-spa.js.org/docs/building-applications#lifecyle-props// if you uncomment these, remember to add matching prop definitions for them in your App.vue file./*name: this.name,mountParcel: this.mountParcel,singleSpa: this.singleSpa,*/}})}}
})// 导出必要的生命周期函数
export const bootstrap = vueLifecycles.bootstrap
export const mount = vueLifecycles.mount
export const unmount = vueLifecycles.unmount

修改 App.vue 组件:

<template><div id="app"><h1>{{ name }}</h1><div><router-link to="/bar">Bar</router-link><router-link to="/baz">Baz</router-link></div><router-view /></div>
</template><script>
export default {name: 'App',props: ['name']
}
</script><style></style>

创建 Parcel 应用

Parcel 是用来创建跨框架、跨应用的公共 UI 的。

Parcel 可以使用任意 single-spa 支持的框架(如 React、Vue 等),它也是单独的应用,需要单独启动,但是它不关联路由。

Parcel 应用的模块访问地址也需要被添加到 import-map 中,其它微应用通过 System.import 方法加载该模块。

下面创建一个公共的导航模块,并在其它微应用中使用它。

1、创建基于 React 框架的 Parcel 微应用

create-single-spa
? Directory for new project navbar
? Select type to generate single-spa application / parcel
? Which framework do you want to use? react
? Which package manager do you want to use? npm
? Will this project use Typescript? No
? Organization name (can use letters, numbers, dash or underscore) study
? Project name (can use letters, numbers, dash or underscore) navbar

2、编写导航组件

// navbar\src\root.component.js
import { BrowserRouter, Link } from 'react-router-dom'export default function Root(props) {return (<BrowserRouter><div><Link to="/">@single-spa/welcome</Link>{' | '}<Link to="/foo">@study/foo</Link>{' | '}<Link to="/todos">@study/todos</Link>{' | '}<Link to="/realworld">@study/realworld</Link></div></BrowserRouter>)
}

3、禁止打包 react-router-dom

// navbar\webpack.config.js
const { merge } = require('webpack-merge')
const singleSpaDefaults = require('webpack-config-single-spa-react')module.exports = (webpackConfigEnv, argv) => {const defaultConfig = singleSpaDefaults({orgName: 'study',projectName: 'navbar',webpackConfigEnv,argv})return merge(defaultConfig, {// modify the webpack config however you'd like to by adding to this objectexternals: ['react-router-dom']})
}

4、修改脚本&启动应用

修改启动端口:

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

npm start 启用应用

5、容器应用中引入模块

当前应用不和路由进行关联,所以不需要在容器应用中注册应用,只需要配置引入地址:

  <!-- 引入容器应用模块 --><!-- 开发环境指定本地地址 --><% if (isLocal) { %><script type="systemjs-importmap">{"imports": {"@study/root-config": "//localhost:9000/study-root-config.js","@study/foo": "//localhost:9001/study-foo.js","@study/todos": "//localhost:9002/study-todos.js","@study/realworld": "//localhost:9003/js/app.js","@study/navbar": "//localhost:9004/study-navbar.js"}}</script><% } %>

6、在 React 应用中使用

在 React 应用中使用 Parcel 应用,需要使用 single-spa 提供的组件,将 System.import 加载的模块对象传递给组件的 config 属性:

import Parcel from 'single-spa-react/parcel'
<Parcel config={System.import('@study/navbar')} />

修改代码:

// todos\src\root.component.js
import React from 'react'
import Parcel from 'single-spa-react/parcel'
import { BrowserRouter, Route, Link, Redirect, Switch } from 'react-router-dom'
import Home from './Home'
import About from './About'export default function Root(props) {return (<BrowserRouter basename="/todos"><Parcel config={System.import('@study/navbar')} /><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>)
}

访问 http://localhost:9000/todos 查看效果。

7、在 Vue 应用中使用

在 Vue 应用中使用 Parcel 应用,也是使用 single-spa 提供的组件,将加载模块的对象传递给组件,还需要向组件从传递一个方法 mountRootParcel,该方法用于挂载 Parcel。

<!-- realworld\src\App.vue -->
<template><div id="app"><Parcel :config="parceConfig" :mountParcel="mountParcel" /><h1>{{ name }}</h1><div><router-link to="/bar">Bar</router-link><router-link to="/baz">Baz</router-link></div><router-view /></div>
</template><script>
import Parcel from 'single-spa-vue/dist/esm/parcel'
import { mountRootParcel } from 'single-spa'export default {name: 'App',components: { Parcel },props: ['name'],data() {return {parceConfig: window.System.import('@study/navbar'),mountParcel: mountRootParcel}}
}
</script><style></style>

**注意1:**在 Vue 微应用中使用 System 要使用 window.System

**注意2:**这里加载了模块 single-spa 的 mountRootParcel 方法,打包时会将 single-spa 一起打包。而容器应用已经将 single-spa 作为公共模块引入,所以 Vue 应用需要配置不打包 single-spa。

// realworld\vue.config.js
module.exports = {chainWebpack: config => {// 禁止打包的模块config.externals(['vue', 'vue-router', 'single-spa'])}
}

修改配置后需要重新启动 Vue 应用。

创建跨框架共享的 JavaScript 逻辑

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

创建 utility modules 微应用

create-single-spa
? Directory for new project tools
? Select type to generate in-browser utility module (styleguide, api cache, etc)
? Which framework do you want to use? none
? Which package manager do you want to use? npm
? Will this project use Typescript? No
? Organization name (can use letters, numbers, dash or underscore) study
? Project name (can use letters, numbers, dash or underscore) tools

修改启动命令

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

容器应用中引入模块

utility modules 同 Parcel 一样,是在微应用中使用,不需要在容器应用中注册,只需要在容器应用中引入即可。

  <!-- 引入容器应用模块 --><!-- 开发环境指定本地地址 --><% if (isLocal) { %><script type="systemjs-importmap">{"imports": {"@study/root-config": "//localhost:9000/study-root-config.js","@study/foo": "//localhost:9001/study-foo.js","@study/todos": "//localhost:9002/study-todos.js","@study/realworld": "//localhost:9003/js/app.js","@study/navbar": "//localhost:9004/study-navbar.js","@study/tools": "//localhost:9005/study-tools.js"}}</script><% } %>

在应用中导出方法

在 utility modules 应用入口文件中导出的方法,可以被其它应用使用。

// tools\src\study-tools.js
// Anything exported from this file is importable by other in-browser modules.
export function sayHello(who) {console.log(`%c${who} sayHello`, 'color:skyblue')
}

在 React 应用中使用

// todos\src\Home.js
import { useEffect, useState } from 'react'// 创建一个自定义工具函数用于加载 tools
function useToolsModule() {const [toolsModule, setToolsModule] = useState()useEffect(() => {System.import('@study/tools').then(setToolsModule)})return toolsModule
}const Home = () => {const toolsModule = useToolsModule()if (toolsModule) {// 调用 tools 应用导出的方法toolsModule.sayHello('@study/todos')}return <div>Home works</div>
}export default Home

在 Vue 应用中使用

添加一个按钮,点击后调用 tools 中的方法。

<!-- realworld\src\App.vue -->
<template><div id="app"><Parcel :config="parceConfig" :mountParcel="mountParcel" /><h1>{{ name }}</h1><div><router-link to="/bar">Bar</router-link><router-link to="/baz">Baz</router-link><button @click="handleClick">button</button></div><router-view /></div>
</template><script>
import Parcel from 'single-spa-vue/dist/esm/parcel'
import { mountRootParcel } from 'single-spa'export default {name: 'App',components: { Parcel },props: ['name'],data() {return {// 注意这里要使用 window.SystemparceConfig: window.System.import('@study/navbar'),mountParcel: mountRootParcel}},methods: {async handleClick() {// 注意这里要使用 window.Systemconst toolsModule = await window.System.import('@study/tools')toolsModule.sayHello('@study/realworld')}}
}
</script><style></style>

实现跨应用通信

在微前端框架中,一般通过发布订阅模式实现应用间的通信和状态共享。

实现跨应用通信可以借助这两个工具:

  • utility modules:实现跨应用共享 JS 逻辑
  • RxJS:提供发布订阅模式的功能,它是单独的库,与框架无关,可以在任何框架中使用。

在容器应用中引入 RxJS

  <!-- 引入公共模块地址 --><script type="systemjs-importmap">{"imports": {"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js","react": "https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js","react-dom": "https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js","react-router-dom": "https://cdn.jsdelivr.net/npm/react-router-dom@5.3.0/umd/react-router-dom.min.js","vue": "https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js","vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.5.3/dist/vue-router.min.js","rxjs": "https://cdn.jsdelivr.net/npm/rxjs@7.5.2/dist/bundles/rxjs.umd.min.js"}}</script>

在 utilty modules 中导出方法

RxJS 的 ReplaySubject 方法可以广播历史消息

当一个应用发布消息时,另一个应用可能还未加载,发布历史消息,可以保证应用动态加载后仍可以接受到消息。

// tools\src\study-tools.js
// Anything exported from this file is importable by other in-browser modules.import { ReplaySubject } from 'rxjs'export function sayHello(who) {console.log(`%c${who} sayHello`, 'color:skyblue')
}// 实例化一个 ReplaySubject 对象
// 它缓存一组值,除了向现有订阅者发送新值外,还会立即向新订阅者发送缓存的值
export const sharedSubject = new ReplaySubject()

在 React 应用中订阅/发布消息

注意:订阅消息的同时也要考虑取消订阅。

// todos\src\Home.js
import { useEffect, useState } from 'react'// 创建一个自定义工具函数用于加载 tools
function useToolsModule() {const [toolsModule, setToolsModule] = useState()useEffect(() => {System.import('@study/tools').then(setToolsModule)})return toolsModule
}const Home = () => {const toolsModule = useToolsModule()// 组件挂载完成后调用useEffect(() => {let subjection = nullif (toolsModule) {// 调用 tools 应用导出的方法toolsModule.sayHello('@study/todos')// 订阅消息subjection = toolsModule.sharedSubject.subscribe(console.log)}// 返回清理函数,用于在组件卸载时取消订阅return () => subjection && subjection.unsubscribe()}, [toolsModule])return (<div>Home works<button onClick={() => toolsModule.sharedSubject.next('Hello')}>发布消息</button></div>)
}export default Home

在 Vue 应用中订阅/发布消息

<!-- realworld\src\App.vue -->
<template><div id="app"><Parcel :config="parceConfig" :mountParcel="mountParcel" /><h1>{{ name }}</h1><div><router-link to="/bar">Bar</router-link><router-link to="/baz">Baz</router-link><button @click="handleClick">button</button></div><router-view /></div>
</template><script>
import Parcel from 'single-spa-vue/dist/esm/parcel'
import { mountRootParcel } from 'single-spa'export default {name: 'App',components: { Parcel },props: ['name'],data() {return {subjection: null,// 注意这里要使用 window.SystemparceConfig: window.System.import('@study/navbar'),mountParcel: mountRootParcel}},methods: {async handleClick() {// 注意这里要使用 window.Systemconst toolsModule = await window.System.import('@study/tools')toolsModule.sayHello('@study/realworld')}},async mounted() {const toolsModule = await window.System.import('@study/tools')// 订阅消息this.subjection = toolsModule.sharedSubject.subscribe(console.log)},beforeDestroy() {// 取消订阅this.subjection.unsubscribe()},
}
</script><style></style>

测试

访问 React 的 todos 应用页面,点击**“发送消息”**按钮,控制台打印了消息内容,这是 todos 应用订阅的处理事件。

然后再切换到 Vue 的 realworld 应用页面,组件挂载后,控制台也会打印消息内容,这是 realworld 应用定于的处理事件。

说明 ReplaySubject 广播了历史消息,其它动态加载的应用也可以在加载后收到广播的历史消息,从而可以实现跨应用通信和状态共享。

布局引擎 Layout Engine

布局引擎允许使用组件的方式声明顶层路由,即访问什么地址激活什么应用,这种方式类似 React 中配置路由的方式。

布局引擎还提供了更加便捷的路由 API 来注册应用,从而改变现在通过多次调用 registerApplication 注册应用的方式。

安装布局引擎

在 workspace/container 容器应用下安装:

# 本例安装的版本是 single-spa-layout@2.0.1
npm install single-spa-layout

构建路由

在模板文件中添加一个 <template> 元素,在里面配置路由,之后在 js 文件中需要获取这个 <template> 元素。

<body><template id="single-spa-layout"><single-spa-router><!-- 不包含在 route 组件下的应用是公共模块,会在每个页面都显示 --><application name="@study/navbar"></application><!-- 包含在 route 组件下的应用会在指定路由下显示 --><!-- 应用会在 path 指定的路由下显示 --><!-- default 是默认路由(/) --><route default><application name="@single-spa/welcome"></application></route><route path="foo"><application name="@study/foo"></application></route><route path="todos"><application name="@study/todos"></application></route><route path="realworld"><application name="@study/realworld"></application></route></single-spa-router></template><main></main><div id="root"></div><!-- 加载容器应用模块 --><script>System.import('@study/root-config')// System.import('./study-root-config.js');</script><!-- 调试工具。可以通过浏览器调试工具(single-spa-Inspector)更改注册的微应用模块的地址 --><!-- 例如,将线上环境的模块地址更改为开发环境的模块地址 --><import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>

@single-spa/welcome 应用当前是在注册时指定的引入地址,要在 import-map 中添加一下:

    <!-- 引入容器应用模块 --><!-- 开发环境指定本地地址 --><% if (isLocal) { %><script type="systemjs-importmap">{"imports": {"@study/root-config": "//localhost:9000/study-root-config.js","@study/foo": "//localhost:9001/study-foo.js","@study/todos": "//localhost:9002/study-todos.js","@study/realworld": "//localhost:9003/js/app.js","@study/navbar": "//localhost:9004/study-navbar.js","@study/tools": "//localhost:9005/study-tools.js","@single-spa/welcome": "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"}}</script><% } %>

获取路由信息 && 注册应用

现在已经通过组件的方式配置了路由,可以将它理解为一种语法糖,可以通过它获取一个数组,数组中包含的每个对象都是一个有效的 registerApplication 方法的参数,这样就可以在 js 文件中使用了:

// container\src\study-root-config.js
import { registerApplication, start } from 'single-spa'
import { constructRoutes, constructApplications } from 'single-spa-layout'// 获取路由配置对象
// constructRoutes 会将路由配置解析成一个对象,其中包含用于注册应用的数组
const routes = constructRoutes(document.getElementById('single-spa-layout'))// 获取路由信息数组
// constructApplications 会解析 routes,返回一个数组,数组的每个元素都是 registerApplication 方法接受的参数对象 `{name,app,activeWhen}`
const applications = constructApplications({routes,loadApp({ name }) {return System.import(name)}
})// 遍历路由信息注册应用
applications.forEach(registerApplication)start({urlRerouteOnly: true
})

现在访问 todos 和 realworld 可以看到两个 navbar,即表示成功。

微前端解决方案初探 02 微前端框架 single-spa相关推荐

  1. 微前端解决方案初探 01 微前端介绍、价值、如何实现、systemjs 模块化方案

    什么是微前端 微前端是一种软件架构,可以将前端应用拆解成一些更小的能够独立开发部署的微型应用,然后再将这些微应用进行组合使其成为整体应用的架构模式. 微前端架构类似于组件架构,但不同的是,组件不能独立 ...

  2. 微前端解决方案初探 03 模块联邦(概述、案例介绍、应用初始化)

    模块联邦概述 模块联邦(Module Federation)是 Webpack 5 中新增的一项功能,可以实现跨应用共享模块. 以下图为例: 在 A 应用中有一个 sayHelloFromA 方法,在 ...

  3. 乾坤 微前端_微前端架构初探以及我的前端技术盘点

    前言 最近几年微前端一直是前端界的热门议题, 它类似于微服务架构, 主要面向于浏览器端,能将一个复杂而庞大的单体应用拆分为多个功能模块清晰且独立的子应用,且共同服于务同一个主应用.各个子应用可以独立运 ...

  4. 【前端微服务化】使用飞冰搭建前端微服务化框架

    框架简介 icestark 是一个面向大型系统的微前端解决方案,适用于以下业务场景: 后台比较分散,体验差别大,因为要频繁跳转导致操作效率低,希望能统一收口的一个系统内; 单页面应用非常庞大,多人协作 ...

  5. 前端微服务化解决方案

    作者:Alili前端大暴炸的前端微服务化解决方案系列 链接:https://www.jianshu.com/u/2aa7a9ad33ad 来源:简书 著作权归作者所有.商业转载请联系作者获得授权,非商 ...

  6. registermodule 微前端_麦饭:可能是你见过最酷最简单的微前端解决方案

    目前国内最火的微前端框架是来自阿里的qiankun,以及它底层的single-spa,但是,这是最终形态吗?在我看来并不是.无论是single-spa,还是qiankun,都有比较强的协议规定,对原有 ...

  7. 网易严选企业级微前端解决方案与落地实践

    本文作者:张浩 (网易严选技术团队) 张浩,网易资深前端开发工程师,严选数据产品前端负责人.先后负责过网易企业邮箱.网易有钱.网易严选等大型项目的前端架构设计及开发.当前致力于大前端与通用能力建设.工 ...

  8. 已开源! icestark 助力业务快速落地微前端解决方案

    作者|刘雄(大果) 出品|阿里巴巴新零售淘系技术部 2017 年中旬,飞冰(ICE)团队接到一个叫做「阿里创作平台」的项目,这个产品为创作者提供了入驻.帐号管理.内容管理.内容发布.粉丝运营.数据分析 ...

  9. 目标是最完善的微前端解决方案 - qiankun 2.0

    距  qiankun 开源已过去了 11 个月,距上次官方  发声 已过去 8 个月. Announcing qiankun@2.0 2019 年 6 月,微前端框架 qiankun 正式发布了 1. ...

最新文章

  1. Xshell远程登录Ubuntu
  2. 在线英汉词典 智能纠错的设计
  3. 【效率】再见 Typora,这款 Markdown 编辑器开源又免费!
  4. [Android] 底部菜单布局+PopupWindows实现弹出菜单功能(初级篇)
  5. 《Python数据挖掘:概念、方法与实践》——1.5节小结
  6. 简述本地组策略中用户和计算机配置的差异,组策略编辑器中的计算机配置和用户配置有什么区别吗?...
  7. 扫描仪 pfu_行业案例:扫描仪高效加持下的艺术工作室
  8. 2017.5.11 道路修建 思考记录
  9. [iOS] 使用xib做为应用程序入口 with Code
  10. 如何使用Bootbox ?
  11. 瑞星企业防毒2010网络版(一)管理中心与客户端部署
  12. 转:H5 页面36种漂亮的CSS3网页按钮Button样式
  13. c语言求数组交集并集差集,求两个数组的交集、并集和差集算法分析与实现
  14. 在linux中运行安卓程序
  15. 服务器接上显示器操作,服务器接上显示器
  16. OpenStack云平台搭建(5) | 部署Nova
  17. 常用的第三方模块-Pillow
  18. 说一下数据库有哪些索引类型,有什么优缺点?
  19. LED背光源运用在温控设备上
  20. I'm up to my ears

热门文章

  1. wxwork和wx.qy判断企业微信小程序编译运行环境
  2. syncthing搭建自己的同步云
  3. 着手社区建设掌握的两个概念
  4. u盘与计算机识别不正常,U盘识别不了的原因及解决方法
  5. mysql带where的join加索引_MySQL索引分析和优化+JOIN的分类(转)
  6. Java语言基础小结
  7. mysql的check语言_check在SQL语句中的意思是什么?
  8. Retrofit 通过刷新头部Token解决token过期
  9. 斧子演示:如何取消导出高清视频的限制
  10. SpringBoot配置Mybatis多数据源