single-spa

一、创建容器应用

1.全局安装single-spa脚手架工具:npm i create-single-spa -g

2.新建workspace文件夹,切换到文件夹下,使用create-single-spa 创建:

# 应用是否创建到当前目录,因为首先创建的是一个容器应用,所以新建到container文件夹中
? Directory for new project container 
# 创建什么应用,选择root config,也就是容器应用
#   1. single-spa-application / parcel:微前端架构中的微应用,可以使用 vue、react、angular 等框架。
#   2. single-spa root config:创建微前端容器应用。
#   3. utility modules:公共模块应用,非渲染组件,用于跨应用共享 javascript 逻辑的微应用。
? Select type to generate single-spa root config
# 用什么管理  选择npm
? Which package manager do you want to use? npm
# 使用ts吗,暂不使用
? Will this project use Typescript? No
# 生成默认布局文件吗,暂不使用
? Would you like to use single-spa Layout Engine No
# 组织名称,可以理解为团队名称
? Organization name (can use letters, numbers, dash or underscore) jichen

3.启动应用: cd container npm start

4.访问应用:localhost:9000,可以看到如下页面:

容器是没有页面的,之所以能看到这个页面,是因为脚手架默认注册了一个微应用。

3.默认代码解析:

container/src/jichen-root-config.js

import { registerApplication, start } from "single-spa";

// 注册微前端应用:
//   1.name: 字符串类型,微前端应用名称"@组织名称/应用名称"
//   2.app: 函数类型,返回Promise,通过systemjs引用打包好的微前端应用模块代码(umd)
//   3.activeWhen: 路由匹配时激活应用
registerApplication({
  name: "@single-spa/welcome",
  app: () =>
    System.import(
      "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
    ),
  activeWhen: ["/"],
});

// registerApplication({
//   name: "@jichen/navbar",
//   app: () => System.import("@jichen/navbar"),
//   activeWhen: ["/"]
// });

// start 方法必须在single spa的配置文件中调用
// 调用 start 之前,应用会被加载,但不会初始化,挂载或卸载
start({
  // 是否可以通过history.pushState() 和history.replaceState()更改触发single-spa路由,默认为false, true:不允许
  urlRerouteOnly: true,
});

container/src/index.ejs

<!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>Root Config</title>
  <!--
    Remove this if you only support browsers that support async/await.
    This is needed by babel to share largeish helper code for compiling async/await in older
    browsers. More information at https://github.com/single-spa/create-single-spa/issues/112
  -->
  <script src="https://cdn.jsdelivr.net/npm/regenerator-runtime@0.13.7/runtime.min.js"></script>
  <!--
    This CSP allows any SSL-enabled host and for arbitrary eval(), but you should limit these directives further to increase your app's security.
    Learn more about CSP policies at https://content-security-policy.com/#directive
  -->
  <meta http-equiv="Content-Security-Policy" content="default-src 'self' https: localhost:*; script-src 'unsafe-inline' 'unsafe-eval' https: localhost:*; connect-src https: localhost:* ws://localhost:*; style-src 'unsafe-inline' https:; object-src 'none';">
  <meta name="importmap-type" content="systemjs-importmap" />
  <!-- JavaScript 模块下载地址,此处可放置微前端项目中的公共模块 -->
  <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">

  <!-- Add your organization's prod import map URL to this script's src  -->
  <!-- <script type="systemjs-importmap" src="/importmap.json"></script> -->

  <% if (isLocal) { %>
  <script type="systemjs-importmap">
    {
      "imports": {
        "@jichen/root-config": "//localhost:9000/jichen-root-config.js"
      }
    }
  </script>
  <% } %>

  <!-- 用于支持 Angular 应用 -->
  <!-- <script src="https://cdn.jsdelivr.net/npm/zone.js@0.11.3/dist/zone.min.js"></script> -->
  <!-- 用于覆盖通过 import-map 设置的 JavaScript 模块下载地址 -->
  <script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script>
  <% if (isLocal) { %>
  <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.js"></script>
  <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>
  <!-- systemjs用来解析 AMD 模块的插件 -->
  <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.min.js"></script>
  <% } %>
</head>
<body>
  <noscript>
    You need to enable JavaScript to run this app.
  </noscript>
  <main></main>
  <!-- 导入微前端容器应用 -->
  <script>
    System.import('@jichen/root-config');
  </script>
  <!-- import-map-overrides 可以覆盖导入映射,当前项目中用于配合single-spa Inspector调试工具使用。可以手动覆盖项目中的JS模块加载地址,用于调试 -->
  <import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>

二、创建不基于框架的微应用

1.应用初始化,在workspace文件夹下创建qing文件夹,切换到该文件夹下,用于存放改微应用的代码,使用以下package.json文件安装所需依赖:

{
  "name": "qing",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "start": "webpack serve"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/core": "^7.12.10",
    "single-spa": "^5.9.0",
    "webpack": "^5.8.0",
    "webpack-cli": "^4.2.0",
    "webpack-config-single-spa": "^2.0.0",
    "webpack-dev-server": "^4.0.0-beta.0",
    "webpack-merge": "^5.4.0"
  }
}

2.安装完依赖后,新建webpack.config.js配置webpack

const singleSpaDefaults = require('webpack-config-single-spa')
const { merge } = require("webpack-merge")

module.exports = () =>{
  const defaultConfig = singleSpaDefaults({
	// 组织名称
    orgName: "jichen",
	// 项目名称
    projectName: "qing"
  })
  // 使用merge方法配置微应用默认端口
  return merge(defaultConfig, {
    devServer: {
      port: 9001
    }
  })
}

3.在qing文件夹下新建src目录,然后再sr目录下新建微应用的入口文件,文件名格式是”组织名称-微应用名称.js”,即jichen-qing.js,框架要求入口文件中必须导出微前端应用所需的声明周期函数,生命周期函数必须返回Promise:

// 定义一个变量,方便卸载时移除
let qingContainer = null
// 使用async 默认返回的就是Promise对象
export async function bootstrap() {
  console.log("应用正在启动。。。");
}

export async function mount() {
  console.log("应用正在挂载。。。");
  qingContainer = document.createElement('div')
  qingContainer.id = "qingContainer"
  qingContainer.innerHTML = 'qing wahaha'
  document.body.appendChild(qingContainer)
}

export async function unmount() {
  console.log("应用正在卸载");
  document.body.removeChild(qingContainer)
}

4.在前端容器应用中注册微前端应用,在container/src/jichen-root-config.js文件中添加如下代码:

registerApplication({
  name: "@jichen/qing",
  app: () => System.import("@jichen/qing"),
  activeWhen: ["/qing"]
});

5.在模板文件container/src/index.ejs中指定模板访问地址:

<script type="systemjs-importmap">
    {
      "imports": {
        "@jichen/root-config": "//localhost:9000/jichen-root-config.js",
		// 添加访问地址
        "@jichen/qing": "//localhost:9001/jichen-qing.js"
      }
    }
  </script>

6.终端中切换到qing目录下,执行npm start,启动微应用 ,然后访问容器应用地址 localhost:9000/qing ,会发现同时存在了两个微应用(qing和脚手架默认注册的微应用)的内容,这是因为localhost:9000/qing会匹配activeWhen: ["/"] 规则:

修改默认应用代码container/src/jichen-root-config.js:

registerApplication({
  name: "@single-spa/welcome",
  app: () =>
    System.import(
      "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
    ),
  activeWhen: ["/"],
});

改为如下配置:

registerApplication("@single-spa/welcome", () =>
    System.import(
      "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
    ),
  location => location.pathname === '/',
);

这时访问localhost:9000/qing和localhost:9000就只显示各自微应用的内容了。

三、创建基于React的微应用

1.创建应用:create-single-spa

? Directory for new project reactApp
? 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) jichen
? Project name (can use letters, numbers, dash or underscore) reactApp

2.修改应用启动端口reactApp/package.json:

{
	"scripts": {
	    "start": "webpack serve --port 9002",
    },
}

3.切换到reactApp目录下,执行npm start启动应用

4.注册微应用到容器应用container/src/jichen-root-config.js:

registerApplication({
  name: "@jichen/reactApp",
  app: () => System.import("@jichen/reactApp"),
  activeWhen: ["/reactApp"]
});

5.指定微前端应用模块的引用地址:

<script type="systemjs-importmap">
    {
      "imports": {
        "@jichen/root-config": "//localhost:9000/jichen-root-config.js",
        "@jichen/qing": "//localhost:9001/jichen-qing.js",
		// 添加地址
        "@jichen/reactApp": "//localhost:9002/jichen-reactApp.js",
      }
    }
  </script>

6.指定公共库的访问地址,因为single-spa认为react和react-dom属于公共库,不应该单独被webpack打包:

<script type="systemjs-importmap">
    {
      "imports": {
        "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js",
		// 增加公共库地址
		""https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.production.min.js

      }
    }
  </script>

7.微前端React应用入口文件代码:

// 引用的是容器应用index.ejs 文件中import-map指定的版本
import React from "react";
import ReactDOM from "react-dom";
// single-spa-react 用于创建使用React框架实现的微前端应用
import singleSpaReact from "single-spa-react";
// 用于渲染在页面中的根组件
import Root from "./root.component";

// 指定根组件渲染位置,必须在容器应用index.ejs 文件中定义一个id为root的div
const domElementGetter = () => document.getElementById('root')

const lifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: Root,
  // 错误边界函数,微应用发生错误时显示
  errorBoundary(err, info, props) {
    // Customize the root error boundary for your microfrontend here.
    return null;
  },
});
// 暴露必须的声明周期函数
export const { bootstrap, mount, unmount } = lifecycles;

8.访问localhost:9000/reactApp,可以看到如下页面,说明创建的react微应用启动成功了:

9.配置路由:

// reactApp/src/root.component.js
import React from "react";
import {BrowserRouter, Switch, Route, Redirect, Link} from "react-router-dom"

import Home from './pages/Home'
import About from './pages/About'

export default function Root(props) {
  return (
    <BrowserRouter basename="/reactApp">
      <div>{props.name}</div>
      <div>
        <Link to="home">Home</Link>
        <Link to="about">About</Link>
      </div>
      <Switch>
        <Route path="/home">
          <Home></Home>
        </Route>
        <Route path="/about">
          <About></About>
        </Route>
        <Route path="/">
          <Redirect to="/home" />
        </Route>
        
      </Switch>
    </BrowserRouter>
  );
}
// reactApp/src/pages/Home.js
import React from 'react'

const Home = () => {
  return (
    <div>Home Works</div>
  )
}

export default Home
// reactApp/src/pages/About.js
import React from 'react'

const About = () => {
  return (
    <div>About Works</div>
  )
}

export default About

10.修改webpack配置reactApp/webpack.config.js:

const { merge } = require("webpack-merge");
const singleSpaDefaults = require("webpack-config-single-spa-react");

module.exports = (webpackConfigEnv, argv) => {
  const defaultConfig = singleSpaDefaults({
    orgName: "jichen",
    projectName: "reactApp",
    webpackConfigEnv,
    argv,
  });

  return merge(defaultConfig, {
    // modify the webpack config however you'd like to by adding to this object
	// 编译时不打包react-router-dom框架
    externals: ["react-router-dom"]
  });
};

可以看到如下效果:

四、创建基于Vue的微应用

1.创建应用

2.在vueapp目录下新建vue.config.js配置webpack:

module.exports = {
  chainwebpack: config => {
	// 不打包vue和vue-router,将他们作为公共库在容器模板中引入
    config.externals(["vue", "vue-router"])
  }
}

3.容器应用的模板文件index.ejs引入vue和vue-router:

<script type="systemjs-importmap">
    {
      "imports": {
		// 新增
        "vue": "https://cdn.jsdelivr.net/npm/vue/dist/vue.js",
        "vue-route": "https://cdn.staticfile.org/vue-router/2.7.0/vue-router.min.js"
      }
    }
</script>

4.修改启动命令vueapp/package.json:

{
  "scripts": {
    "start": "vue-cli-service serve --port 9003"
  },
}

5.npm start启动微应用

6.注册微应用到容器

// container/src/jichen-root-config.js
registerApplication({
  name: "@jichen/vueapp",
  app: () => System.import("@jichen/vueapp"),
  activeWhen: ["/vueapp"]
});

7.指定微前端应用模块的引用地址:

访问http://localhost:9003/,提示不是正确的访问路径,也可以看到应该配置的入口是:http://localhost:9003/js/app.js

// container/src/index.ejs
 <script type="systemjs-importmap">
    {
      "imports": {
		// 增加
        "@jichen/vueapp": "//localhost:9003/js/app.js"
      }
    }
  </script>

8.访问__http://localhost:9000/vueapp__,看到如下页面就表示成功了,但是有个报错

是个类似跨域安全的报错,需要将container/src/index.ejs文件中的以下代码注释,就不报错了

<meta http-equiv="Content-Security-Policy" content="default-src 'self' https: localhost:*; script-src 'unsafe-inline' 'unsafe-eval' https: localhost:*; connect-src https: localhost:* ws://localhost:*; style-src 'unsafe-inline' https:; object-src 'none';">

9.配置路由:

// vueapp/src/main.js
import Vue from 'vue';
import VueRouter from 'vue-router'
import singleSpaVue from 'single-spa-vue';

import App from './App.vue';
Vue.use(VueRouter)
Vue.config.productionTip = false;

// 路由组件
const Home = { template: "<div>Home</div>" }
const About = { template: "<div>About</div>" }

// 路由规则
const routes = [
  {path: '/home', component: Home},
  {path: '/about', component: About}
]

// 路由实例
const router = new VueRouter({routes, mode: "history", base: "vueapp"})

const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    // 路由
    router,
    //渲染组件
    render(h) {
      return h(App, {
        props: {
          // single-spa props are available on the "this" object. Forward them to your component as needed.
          // https://single-spa.js.org/docs/building-applications#lifecycle-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;
// vueapp/src/App.vue
<template>
  <div id="app">
    <h1>{{ name }}</h1>
    <p>
      <router-link to="/home">Home</router-link>
      <router-link to="/about">About</router-link>
    </p>
    <router-view></router-view>
  </div>
</template>

<script>

export default {
  name: 'App',
  props: ["name"]
}
</script>

<style>
</style>

访问http://localhost:9000/vueapp/home可以看到如下效果

五、创建Parcel应用

Parcel用来创建公共UI,涉及跨框架共享UI时,需要使用Parcel。

Parcel的定义可以使用任何single-spa支持的框架,它也是单独的应用,需要单独启动,但它不关联路由。

Paecel应用的模块访问地址也需要被添加到import-map中,其它微应用通过System.import方法进行引用。

需求:使用vue创建一个的Parcel应用navbar。

1.使用create-single-spa

2.在navbar目录下新建vue.config.js配置webpack:

module.exports = {
  chainWebpack: config => {
	// 不打包vue和vue-router,将他们作为公共库在容器模板中引入
    config.externals(["vue", "vue-router"])
  }
}

3.修改启动命令navbar/package.json

{
  "scripts": {
    "start": "vue-cli-service serve --port 9004",
  },
}

4.在容器模板文件container/src/index.ejs中指定应用模块地址:

{
    "imports": {
        "@jichen/root-config": "//localhost:9000/jichen-root-config.js",
        "@jichen/qing": "//localhost:9001/jichen-qing.js",
        "@jichen/reactApp": "//localhost:9002/jichen-reactApp.js",
        "@jichen/vueapp": "//localhost:9003/js/app.js",
		// 新增
        "@jichen/navbar": "//localhost:9004/js/app.js"
    }
}

5.实现navbar功能navbar/src/App.vue:

<template>
  <div id="app">
    <p>
      <router-link to="/">@single-spa/welcome</router-link>&nbsp;
      <router-link to="/qing">@jichen/qing</router-link>&nbsp;
      <router-link to="/vueapp">@jichen/vueapp</router-link>&nbsp;
      <router-link to="/reactapp">@jichen/reactapp</router-link>
    </p>
    <router-view></router-view>
  </div>
</template>

<script>

export default {
  name: 'App',
  components: {
  }
}
</script>

<style>
</style>

6.在reactapp中使用navbar(reactApp/src/pages/Home.js):

import React from 'react'
import Parcel from "single-spa-react/parcel"

const Home = () => {
  return (
    <div>
      <Parcel config={System.import("@jichen/navbar")} />
    </div>
  )
}

export default Home

7.在vueapp中使用navbar:

<!-- vueapp/src/App.vue -->
<template>
  <div id="app">
    <h1>{{ name }}</h1>
    <p>
      <router-link to="/home">Home</router-link>
      <router-link to="/about">About</router-link>
    </p>
    <p>
      <Parcel :config="parcelConfig" :mountParcel="mountParcel" />
    </p>
    <router-view></router-view>
  </div>
</template>

<script>
import Parcel from 'single-spa-vue/dist/esm/parcel';
import { mountRootParcel } from 'single-spa';
export default {
  components: {
    Parcel,
  },
  data() {
    return {
      parcelConfig: window.System.import("@jichen/navbar"),
      mountParcel: mountRootParcel
    }
  },
  name: 'App',
  props: ["name"]
}
</script>

<style>
</style>
// navbar/src/main.js
import Vue from 'vue';
import VueRouter from 'vue-router'
import singleSpaVue from 'single-spa-vue';

import App from './App.vue';

Vue.config.productionTip = false;
Vue.use(VueRouter)

const routes = []
const router = new VueRouter({routes, mode: "history", base: "/"})

const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    router,
    render(h) {
      return h(App, {
        props: {
          // single-spa props are available on the "this" object. Forward them to your component as needed.
          // https://single-spa.js.org/docs/building-applications#lifecycle-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;

8.访问http://localhost:9000/vueapp,可以看到如下,可以在vueapp和reactapp中使用navbar的功能:

六、创建utility modules

1.用于放置跨应用共享的JavaScript逻辑,它也是独立的应用,需要单独构建,单独启动。

2.使用create-single-spa创建:

3.修改启动端口tools/package.json

{
  "name": "@jichen/tools",
  "scripts": {
    "start": "webpack serve --port 9005",
  }
}

4.导出共享的方法tools/src/jichen-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")
}

5.容器应用模板中声明模块访问地址container/src/index.ejs

{
      "imports": {
        "@jichen/root-config": "//localhost:9000/jichen-root-config.js",
        "@jichen/qing": "//localhost:9001/jichen-qing.js",
        "@jichen/reactApp": "//localhost:9002/jichen-reactApp.js",
        "@jichen/vueapp": "//localhost:9003/js/app.js",
        "@jichen/navbar": "//localhost:9004/js/app.js",
		// 新增
        "@jichen/tools": "//localhost:9005/jichen-tools.js"
      }
}

6.终端切换到tools目录下,运行npm start启动应用。

7.在reactapp中使用sayHello方法reactApp/src/pages/About.js

import React, {useEffect, useState} from 'react'

function useToolsModule() {
  const [toolsModule, setToolsModule] = useState()
  useEffect(() => {
    System.import("@jichen/tools").then(setToolsModule)
  }, [])
  return toolsModule
}

const About = () => {
  const toolsModule = useToolsModule()
  if(toolsModule) toolsModule.sayHello('react')
  return (
    <div>About Works</div>
  )
}

export default About

访问http://localhost:9000/reactApp/about 可以看到以下结果:

8.vueapp使用sayHello方法vueapp/src/App.vue

<template>
  <div id="app">
    <h1>{{ name }}</h1>
    <p>
      <router-link to="/home">Home</router-link>
      <router-link to="/about">About</router-link>
    </p>
    <p>
      <Parcel :config="parcelConfig" :mountParcel="mountParcel" />
    </p>
    <button @click="handleClick">sayHello</button>
    <router-view></router-view>
  </div>
</template>

<script>
import Parcel from 'single-spa-vue/dist/esm/parcel';
import { mountRootParcel } from 'single-spa';
export default {
  components: {
    Parcel,
  },
  data() {
    return {
      parcelConfig: window.System.import("@jichen/navbar"),
      mountParcel: mountRootParcel
    }
  },
  name: 'App',
  props: ["name"],
  methods: {
    async handleClick() {
      let toolsModule = await window.System.import("@jichen/tools")
      toolsModule.sayHello('vue')
    }
  },
}
</script>

<style>
</style>


访问 http://localhost:9000/vueapp 点击sayHello按钮,可以看到以下结果:

9.使用RxJS跨应用通信,因为它无关框架,可以在任何框架中使用。

9-1. 在容器应用的模板文件中引入RxJS库,container/src/index.ejs

{
      "imports": {
        "rxjs": "https://unpkg.com/rxjs/bundles/Rx.min.js"
      }
}

9-2. 在tools中导出一个ReplaySubject,可以广播历史消息,就算是动态加载进来的应用也可以接收到数据tools/src/jichen-tools.js

import { ReplaySubject } from 'rxjs'
// Anything exported from this file is importable by other in-browser modules.
export function sayHello(who) {
  console.log(`%c${who} sayHello`, "color:skyblue")
}

export const sharedSubject = new ReplaySubject()

9-3. 在reactapp中订阅reactApp/src/pages/About.js

import React, {useEffect, useState} from 'react'

function useToolsModule() {
  const [toolsModule, setToolsModule] = useState()
  useEffect(() => {
    System.import("@jichen/tools").then(setToolsModule)
  }, [])
  return toolsModule
}

const About = () => {
  const toolsModule = useToolsModule()
  useEffect(() => {
    let subjection = null
    if(toolsModule) {
      toolsModule.sayHello('react')
	  // 订阅
      subjection = toolsModule.sharedSubject.subscribe(console.log)
    }
	// 取消订阅
    return () => subjection.unsubscribe
  })
  return (
    <div>About Works</div>
  )
}

export default About

9-4. 在vueapp中订阅vueapp/src/App.vue

<template>
  <div id="app">
    <h1>{{ name }}</h1>
    <p>
      <router-link to="/home">Home</router-link>
      <router-link to="/about">About</router-link>
    </p>
    <p>
      <Parcel :config="parcelConfig" :mountParcel="mountParcel" />
    </p>
    <button @click="handleClick">sayHello</button>
    <router-view></router-view>
  </div>
</template>

<script>
import Parcel from 'single-spa-vue/dist/esm/parcel';
import { mountRootParcel } from 'single-spa';
export default {
  components: {
    Parcel,
  },
  data() {
    return {
      parcelConfig: window.System.import("@jichen/navbar"),
      mountParcel: mountRootParcel
    }
  },
  name: 'App',
  props: ["name"],
  methods: {
    async handleClick() {
      let toolsModule = await window.System.import("@jichen/tools")
      toolsModule.sayHello('vue')
    }
  },
  async mounted () {
    let toolsModule = await window.System.import("@jichen/tools")
    toolsModule.sharedSubject.subscribe(console.log)
  },
}
</script>

<style>
</style>

9-5. reactapp中定义发起广播的按钮:reactApp/src/pages/About.js

import React, {useEffect, useState} from 'react'

function useToolsModule() {
  const [toolsModule, setToolsModule] = useState()
  useEffect(() => {
    System.import("@jichen/tools").then(setToolsModule)
  }, [])
  return toolsModule
}

const About = () => {
  const toolsModule = useToolsModule()
  useEffect(() => {
    let subjection = null
    if(toolsModule) {
      toolsModule.sayHello('react')
      subjection = toolsModule.sharedSubject.subscribe(console.log)
    }
    return () => subjection.unsubscribe
  }, [toolsModule])
  return (
    <div>
      About Works
      <button onClick={() => subjection.sharedSubject.next('请注意')}>广播</button>
    </div>
  )
}

export default About

9-6. 访问http://localhost:9000/reactApp/about ,点击广播按钮,可以看到输出,并且切换到vueapp也可以看到历史消息:

9-7. 在vueapp中定义倒车按钮,vueapp/src/App.vue

<template>
  <div id="app">
    <h1>{{ name }}</h1>
    <p>
      <router-link to="/home">Home</router-link>
      <router-link to="/about">About</router-link>
    </p>
    <p>
      <Parcel :config="parcelConfig" :mountParcel="mountParcel" />
    </p>
    <button @click="handleClick">sayHello</button>
    <button @click="boardCast">倒车</button>
    <router-view></router-view>
  </div>
</template>

<script>
import Parcel from 'single-spa-vue/dist/esm/parcel';
import { mountRootParcel } from 'single-spa';
export default {
  components: {
    Parcel,
  },
  data() {
    return {
      parcelConfig: window.System.import("@jichen/navbar"),
      mountParcel: mountRootParcel
    }
  },
  name: 'App',
  props: ["name"],
  methods: {
    async handleClick() {
      let toolsModule = await window.System.import("@jichen/tools")
      toolsModule.sayHello('vue')
    },
    async boardCast () {
      let toolsModule = await window.System.import("@jichen/tools")
      toolsModule.sharedSubject.next("倒车")
    },
  },
  async mounted () {
    let toolsModule = await window.System.import("@jichen/tools")
    toolsModule.sharedSubject.subscribe(console.log)
  },
}
</script>

<style>
</style>

9-8. 访问http://localhost:9000/vueapp ,点击倒车按钮,切换到reactapp的About页面,可以看到历史消息:

七、布局引擎的使用方式

1.允许使用组件的方式声明顶层路由,并且提供了更加便捷的路由API用来注册应用。

2.切换到container下安装布局引擎npm i single-spa-layout

3.在容器应用的模板文件中构建路由(主要是template中的代码),还有之前创建容器时默认的@single-spa/welcome也需要声明一下,container/src/index.ejs

<!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>Root Config</title>
  <!--
    Remove this if you only support browsers that support async/await.
    This is needed by babel to share largeish helper code for compiling async/await in older
    browsers. More information at https://github.com/single-spa/create-single-spa/issues/112
  -->
  <script src="https://cdn.jsdelivr.net/npm/regenerator-runtime@0.13.7/runtime.min.js"></script>
  <!--
    This CSP allows any SSL-enabled host and for arbitrary eval(), but you should limit these directives further to increase your app's security.
    Learn more about CSP policies at https://content-security-policy.com/#directive
  -->
  <!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self' https: localhost:*; script-src 'unsafe-inline' 'unsafe-eval' https: localhost:*; connect-src https: localhost:* ws://localhost:*; style-src 'unsafe-inline' https:; object-src 'none';"> -->
  <meta name="importmap-type" content="systemjs-importmap" />
  <!-- If you wish to turn off import-map-overrides for specific environments (prod), uncomment the line below -->
  <!-- More info at https://github.com/joeldenning/import-map-overrides/blob/master/docs/configuration.md#domain-list -->
  <!-- <meta name="import-map-overrides-domains" content="denylist:prod.example.com" /> -->

  <!-- Shared dependencies go into this import map. Your shared dependencies must be of one of the following formats:

    1. System.register (preferred when possible) - https://github.com/systemjs/systemjs/blob/master/docs/system-register.md
    2. UMD - https://github.com/umdjs/umd
    3. Global variable

    More information about shared dependencies can be found at https://single-spa.js.org/docs/recommended-setup#sharing-with-import-maps.
  -->
  <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://unpkg.com/react@18/umd/react.production.min.js",
        "react-dom": "https://unpkg.com/react-dom@18/umd/react-dom.production.min.js",
        "react-router-dom": "https://unpkg.com/react-router-dom@5.0.0/umd/react-router-dom.min.js",
        "vue": "https://unpkg.com/vue@2.6.10/dist/vue.js",
        "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js",
        "rxjs": "https://unpkg.com/rxjs/bundles/Rx.min.js"
      }
    }
  </script>
  <link rel="preload" href="https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js" as="script">

  <!-- Add your organization's prod import map URL to this script's src  -->
  <!-- <script type="systemjs-importmap" src="/importmap.json"></script> -->

  <% if (isLocal) { %>
  <script type="systemjs-importmap">
    {
      "imports": {
        "@jichen/root-config": "//localhost:9000/jichen-root-config.js",
        "@jichen/qing": "//localhost:9001/jichen-qing.js",
        "@jichen/reactApp": "//localhost:9002/jichen-reactApp.js",
        "@jichen/vueapp": "//localhost:9003/js/app.js",
        "@jichen/navbar": "//localhost:9004/js/app.js",
        "@jichen/tools": "//localhost:9005/jichen-tools.js",
        "@single-spa/welcome": "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
      }
    }
  </script>
  <% } %>

  <!--
    If you need to support Angular applications, uncomment the script tag below to ensure only one instance of ZoneJS is loaded
    Learn more about why at https://single-spa.js.org/docs/ecosystem-angular/#zonejs
  -->
  <!-- <script src="https://cdn.jsdelivr.net/npm/zone.js@0.11.3/dist/zone.min.js"></script> -->

  <script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script>
  <% if (isLocal) { %>
  <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.js"></script>
  <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>
  <% } %>
</head>
<body>
  <noscript>
    You need to enable JavaScript to run this app.
  </noscript>
  <template id="single-spa-layout">
    <single-spa-router>
      <application name="@jichen/navbar"></application>
      <route default>
        <application name="@single-spa/welcome"></application>
      </route>
      <route path="qing">
        <application name="@jichen/qing"></application>
      </route>
      <route path="reactApp">
        <application name="@jichen/reactApp"></application>
      </route>
      <route path="vueapp">
        <application name="@jichen/vueapp"></application>
      </route>
    </single-spa-router>
  </template>
  <main></main>
  <div id="root"></div>
  <script>
    System.import('@jichen/root-config');
  </script>
  <import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>

4.获取路由信息并注册应用container/src/jichen-root-config.js

import { registerApplication, start } from "single-spa";
import { constructApplications, constructRoutes } from "single-spa-layout"

// 获取路由配置对象
const routes = constructRoutes(document.querySelector("#single-spa-layout"))

// 获取路由信息数组
const applications = constructApplications({
  routes,
  loadApp({ name }) {
    return System.import(name)
  }
})

// 遍历路由信息注册应用
applications.forEach(registerApplication)


// registerApplication({
//   name: "@single-spa/welcome",
//   app: () =>
//     System.import(
//       "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
//     ),
//   activeWhen: ["/"],
// });

// registerApplication("@single-spa/welcome", () =>
//     System.import(
//       "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
//     ),
//   location => location.pathname === '/',
// );

// registerApplication({
//   name: "@jichen/qing",
//   app: () => System.import("@jichen/qing"),
//   activeWhen: ["/qing"]
// });

// registerApplication({
//   name: "@jichen/reactApp",
//   app: () => System.import("@jichen/reactApp"),
//   activeWhen: ["/reactApp"]
// });

// registerApplication({
//   name: "@jichen/vueapp",
//   app: () => System.import("@jichen/vueapp"),
//   activeWhen: ["/vueapp"]
// });

start({
  urlRerouteOnly: true,
});

5.访问http://localhost:9000/ 效果如下,每个微应用都添加了navbar,reactapp和vueapp有两个navbar是因为我们之前手动加过一次。

源码参考:https://gitee.com/caicai521/workspace

posted @ 2022-10-08 09:52  菜菜123521  阅读(388)  评论(0编辑  收藏  举报