赞
踩
single-spa 是一个实现微前端架构的框架。
在 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
是应用入口文件注意:在整个微前端项目中只有一个模板文件,也就是说,其它微应用是不包含模板文件的。
// 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 })
以下拆分并解析主要内容。
引入 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 存放微应用。
依赖的版本号同上面创建的容器应用中对应依赖一样:
{ "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
安装依赖。
// 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 } }) }
入口文件的命名规则为 <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('应用正在卸载') }
当前还不会触发卸载,因为还没有路由功能,添加路由功能后,从当前应用跳转到另一个应用的时候就会触发卸载。
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>
<% } %>
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 微应用了。
使用 create-single-spa 可以创建基于 React、Vue、Angular 框架的微应用。
在 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
修改 package.json 启动脚本:
"scripts": {
"start": "webpack serve --port 9002",
...
}
npm start
启动微应用。
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>
<% } %>
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>
现在访问 localhost:9000/todos
看到页面中显示 @study/todos is mounted!
就表示该应用已经注册好了。
todos 中的源代码文件主要是两个:
// 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. // 可以编写 jsx return 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>
}
默认微应用的根组件会挂载在 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>
再次查看页面:
// 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
注意:这里使用的是 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> ) }
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-dom externals: ['react-router-dom'] }); };
修改配置需要重新启动 todos 应用 npm start
创建 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)
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 模块解析器被引入。
修改 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
// container\src\study-root-config.js
registerApplication({
name: '@study/realworld',
app: () => System.import('@study/realworld'),
activeWhen: ['/realworld']
})
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
查看效果
现在虽然 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-Policy
的 connect-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
注释掉。
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 是用来创建跨框架、跨应用的公共 UI 的。
Parcel 可以使用任意 single-spa 支持的框架(如 React、Vue 等),它也是单独的应用,需要单独启动,但是它不关联路由。
Parcel 应用的模块访问地址也需要被添加到 import-map 中,其它微应用通过 System.import
方法加载该模块。
下面创建一个公共的导航模块,并在其它微应用中使用它。
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
// 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> ) }
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 object externals: ['react-router-dom'] }) }
修改启动端口:
"scripts": {
"start": "webpack serve --port 9004",
...
}
npm start
启用应用
当前应用不和路由进行关联,所以不需要在容器应用中注册应用,只需要配置引入地址:
<!-- 引入容器应用模块 -->
<!-- 开发环境指定本地地址 -->
<% 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>
<% } %>
在 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
查看效果。
在 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 应用。
utility modules 用于放置跨应用共享的 JavaScript 逻辑,它也是独立的应用,需要单独构建,单独启动。
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')
}
// 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
添加一个按钮,点击后调用 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.System parceConfig: window.System.import('@study/navbar'), mountParcel: mountRootParcel } }, methods: { async handleClick() { // 注意这里要使用 window.System const toolsModule = await window.System.import('@study/tools') toolsModule.sayHello('@study/realworld') } } } </script> <style></style>
在微前端框架中,一般通过发布订阅模式实现应用间的通信和状态共享。
实现跨应用通信可以借助这两个工具:
<!-- 引入公共模块地址 -->
<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>
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()
注意:订阅消息的同时也要考虑取消订阅。
// 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 = null if (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
<!-- 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.System parceConfig: window.System.import('@study/navbar'), mountParcel: mountRootParcel } }, methods: { async handleClick() { // 注意这里要使用 window.System const 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 广播了历史消息,其它动态加载的应用也可以在加载后收到广播的历史消息,从而可以实现跨应用通信和状态共享。
布局引擎允许使用组件的方式声明顶层路由,即访问什么地址激活什么应用,这种方式类似 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,即表示成功。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。