微前端 single-spa

12,824 阅读5分钟

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会消失。