single-spa 是什么
首先,必须先了解什么是微前端架构。
微前端架构是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。 --- phodal
微前端概念原文地址在这里,推荐一下啊 微前端 。
single-spa 就是其中一种实现微前端架构的方式,或者说是一门框架。
single-spa 能做什么
single-spa 是一个让你能在一个前端项目里面兼容多个框架或者项目的框架。
- 同一页面使用多个框架而无需刷新页面。
- 独立部署微内容。
- 使用新框架编写代码,而无需重写现有应用程序。
- 延迟加载代码,用于改善初始加载时间。
single-spa 构建
分成四个项目来说,主项目 single-spa,三个子项目 nav-spa(vue), vue-spa(vue), react-spa(react)。
主项目 single-spa
先看目录结构

single-spa.config.js
import {registerApplication, start} from 'single-spa'
import Publisher from './Publisher.js';
import {initPublisher} from './lib/initPublisher.js';
window.Publisher = new Publisher();
registerApplication(
// Name of our single-spa application
initPublisher('nav'),
// Our loading function
() => {
return window.System.import('@portal/nav')
},
// Our activity function
() => {
return location.pathname.startsWith('/')
}
);
...
start()
registerApplication 用于注册我们的子项目,第一个参数为项目名称,第二个参数为项目地址,第三个参数为匹配的路由,第四参数为初始化传值。
- 项目名称是唯一的,可以自己设置
- 项目地址由于是线上地址,所以我们必须用 system.js 获取。
- 匹配的路由根据你项目需要配置,但要结合子项目中的路由配置。
- 初始化传值可以用在权限配置,由于我这里只是 demo 就不展开讨论。
start 函数开启我们的项目。
注册 Publisher 挂在 window 上,让所有项目都能够获取。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="nav"></div>
<div id="home"></div>
<div id="vue-spa"></div>
<script src='https://unpkg.com/systemjs@4.1.0/dist/system.js'></script>
<script src='https://unpkg.com/systemjs@4.1.0/dist/extras/amd.js'></script>
<script src='https://unpkg.com/systemjs@4.1.0/dist/extras/named-exports.js'></script>
<script src='https://unpkg.com/systemjs@4.1.0/dist/extras/use-default.js'></script>
<script type="systemjs-importmap">
{
"imports": {
"@portal/nav": "http://ip:port",
"@portal/vue": "http://ip:port",
"@portal/react":"http://ip:port",
}
}
</script>
<script src="/dist/single-spa.config.js"></script>
</body>
</html>
cdn 应用system.js用来获取我们的三个子项目,命名可以自己配置。
三个定义好 id 的 div,分别对应三个子项目中创建 dom 的 id。
Publisher.js
import {getMountedApps} from 'single-spa'
class Publisher {
constructor() {
this.handlers = new Map();
this.fnArr = {};
}
on (eventType) {
// 创建自定义事件
const event = document.createEvent("HTMLEvents");
// 初始化testEvent事件
event.initEvent(eventType, false, true);
this.handlers.set(eventType, event);
// 注册
if (!this.fnArr[eventType]) {
this.fnArr[eventType] = []
}
}
saveEvent(eventType, event) {
this.fnArr[eventType].push(event)
}
getEvent(eventType) {
for (let i = 0; i < this.fnArr[eventType].length; i++) {
window.dispatchEvent(this.fnArr[eventType][i]);
}
this.fnArr[eventType] = []
}
// 触发事件
emit(eventType, obj) {
if (!this.handlers.has(eventType)) return this;
let event = this.handlers.get(eventType);
event.data = obj;
const apps = getMountedApps()
if (apps.find(i => i === eventType)) {
window.dispatchEvent(event);
} else {
this.saveEvent(eventType, event);
}
}
}
export default Publisher;
- on 函数用于注册当前的订阅者并且创建自定义事件类型,使其可以在其他项目中被派发事件。
- emit 函数用于派发事件,先判断当前需要被派发的app是否被挂载,如果还没注册先存放事件,等待app注册后再派发。
- getEvent 获取事件。
- saveEvent 存放派发事件。
/lib/initPublisher.js
import Publisher from '../Publisher.js';
export const initPublisher = (name) => {
if (!window.Publisher) {
window.Publisher = new Publisher();
}
window.Publisher.on(name);
return name;
}
在获取项目名称时注册当前订阅者。
nav-spa
目录结构

入口文件index.js
import Vue from 'vue';
import App from './App.vue';
import routes from './router'
import 'es6-promise/auto'
import store from './store/index'
import singleSpaVue from 'single-spa-vue';
const vueLifecycles = singleSpaVue({
Vue,
appOptions: {
el: '#nav',
router:routes,
store,
render: h => h(App)
}
});
export const bootstrap = [
vueLifecycles.bootstrap,
];
export const mount = [
vueLifecycles.mount,
];
export const unmount = [
vueLifecycles.unmount,
];
- singleSpaVue 是 single-spa 结合 vue 的方法,第一个参数传入 vue,第二个参数 appOptions 就是我们平时传入的vue配置。
- bootstrap 生命周期,只会在挂载的时候执行一遍。
- mount 生命周期,每次进入app都会执行。
- unmount 生命周期,卸载的时候执行。
router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import nav from './Components/nav/nav.vue';
Vue.use(VueRouter)
const routes = [
{ path: '/*', component: nav },
]
const router = new VueRouter({
mode: 'history',
routes,
})
export default router;
由于所有的路径下都应该有nav,所以要结合主项目中的路由匹配填写/*。
webpack.prod.js
const config = require('./webpack.config.js');
const webpack = require('webpack');
const path = require('path');
config.entry = path.resolve(__dirname, 'src/index.js')
config.output = {
filename: 'navSpa.js',
library: 'navSpa',
libraryTarget: 'amd',
path: path.resolve(__dirname, 'build/navSpa'),
},
config.plugins.push(new webpack.NamedModulesPlugin());
config.plugins.push(new webpack.HotModuleReplacementPlugin());
config.mode = 'production'
module.exports = config;
这里的线上打包模式为amd模式,主要为了可以让system.js引用。
其他文件和普通的 vue 文件一致,由于本文不是vue教程就不一一展开了,详细的文件信息可以在本文最后访问仓库。
react-spa
目录结构

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import App from './App.jsx';
function domElementGetter() {
return document.getElementById("home")
}
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
domElementGetter,
})
export const bootstrap = [
reactLifecycles.bootstrap,
];
export const mount = [
reactLifecycles.mount,
];
export const unmount = [
reactLifecycles.unmount,
];
其实无论是 vue 还是 react 配置基本是一致的,都是需要返回一些生命周期。而其他文件和普通的 react 文件没有区别。
vue-spa
这里的目录结构也和nav的一致,但这里主要说的是事件派发和自身状态管理器的结合,实现两个系统之间的通信。
index.js
import Vue from 'vue';
import App from './App.vue';
import routes from './router'
import 'es6-promise/auto'
import store from './store/index'
import singleSpaVue from 'single-spa-vue';
const vueLifecycles = singleSpaVue({
Vue,
appOptions: {
el: '#vue-spa',
router:routes,
store,
render: h => h(App)
}
});
export const bootstrap = [
() => {
return new Promise((resolve, reject) => {
// 注册事件
window.addEventListener('vue-spa', obj => {
store.commit('all/setAll', obj.data)
})
resolve();
});
},
vueLifecycles.bootstrap,
];
export const mount = [
() => {
return new Promise((resolve, reject) => {
//获取订阅事件
window.Publisher.getEvent('vue-spa')
resolve();
});
},
vueLifecycles.mount,
]
export const unmount = [
vueLifecycles.unmount,
];
在 bootstrap 的生命周期上注册了 vue-spa 事件,与在主项目中初始化的事件名称一致。可用于事件广播出发 commit 更改自身的 store。
在 mount 的生命周期获取订阅的事件并且派发。
其他文件与vue文件一致。
构建完成
主项目和三个子项目完成后,通过构建和system引入就可以达到微前端的效果了。详细的仓库地址如下
坑点
- 在配置systemJs引用时会有跨域问题,这时候可以配置nginx的返回头进行解决,详情仓库见。
- 在构建vue项目时,App.vue文件的主div id必须为你项目构建的id,因为第一次构建后你的html上的div会消失。