首先,必須先了解什麼是微前端架構。html
微前端架構是一種相似於微服務的架構,它將微服務的理念應用於瀏覽器端,即將 Web 應用由單一的單體應用轉變爲多個小型前端應用聚合爲一的應用。 --- phodal前端
微前端概念原文地址在這裏,推薦一下啊 微前端 。vue
single-spa 就是其中一種實現微前端架構的方式,或者說是一門框架。react
single-spa 是一個讓你能在一個前端項目裏面兼容多個框架或者項目的框架。webpack
分紅四個項目來講,主項目 single-spa,三個子項目 nav-spa(vue), vue-spa(vue), react-spa(react)。nginx
先看目錄結構git
single-spa.config.jses6
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 用於註冊咱們的子項目,第一個參數爲項目名稱,第二個參數爲項目地址,第三個參數爲匹配的路由,第四參數爲初始化傳值。github
start 函數開啓咱們的項目。web
註冊 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;
複製代碼
/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;
}
複製代碼
在獲取項目名稱時註冊當前訂閱者。
目錄結構
入口文件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,
];
複製代碼
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教程就不一一展開了,詳細的文件信息能夠在本文最後訪問倉庫。
目錄結構
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 文件沒有區別。
這裏的目錄結構也和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引入就能夠達到微前端的效果了。詳細的倉庫地址以下