微前端 single-spa

single-spa 是什麼

首先,必須先了解什麼是微前端架構。html

微前端架構是一種相似於微服務的架構,它將微服務的理念應用於瀏覽器端,即將 Web 應用由單一的單體應用轉變爲多個小型前端應用聚合爲一的應用。 --- phodal前端

微前端概念原文地址在這裏,推薦一下啊 微前端vue

single-spa 就是其中一種實現微前端架構的方式,或者說是一門框架。react

single-spa 能作什麼

single-spa 是一個讓你能在一個前端項目裏面兼容多個框架或者項目的框架。webpack

  • 同一頁面使用多個框架而無需刷新頁面。
  • 獨立部署微內容。
  • 使用新框架編寫代碼,而無需重寫現有應用程序。
  • 延遲加載代碼,用於改善初始加載時間。

single-spa 構建

分紅四個項目來講,主項目 single-spa,三個子項目 nav-spa(vue), vue-spa(vue), react-spa(react)。nginx

主項目 single-spa

先看目錄結構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

  • 項目名稱是惟一的,能夠本身設置
  • 項目地址因爲是線上地址,因此咱們必須用 system.js 獲取。
  • 匹配的路由根據你項目須要配置,但要結合子項目中的路由配置。
  • 初始化傳值能夠用在權限配置,因爲我這裏只是 demo 就不展開討論。

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;
複製代碼
  • 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會消失。
相關文章
相關標籤/搜索