商业技术解决方案与高阶技术专题 - 微前端基础

1. 什么是微前端

微前端是一种软件架构,可以将前端应用拆分成一些更小的能够独立开发部署的微型应用。然后将这些微应用进行组合使其成为整体的应用的架构模式。

微前端架构类似于架构,但不同的是,组件不能独立构建和发布,但是微前端中的应用是可以的。

微前端架构与框架无关,每个微应用都可以使用不同的框架。

2.微前端架构带来的价值

  1. 允许增量迁移和增量开发。

项目从一个框架迁移到另一个框架是非常花费时间和艰巨的任务。例如:一个使用AngularJS开发的项目,在后续迭代过程中由于人力,或者是主流框架变了,想换成react,或者vue?这个时候怎么办呢?是继续使用AngularJS还是用react/vue呢?直接迁移是不可能的,在新的框架中完全重写也不太现实。使用微前端架构就可以解决问题,在保留原有项目的同时,可以完全使用新的框架开发新的需求,然后再使用微前端架构将旧的项目和新的项目进行整合。这样既可以使产品得到更好的用户体验,也可以使团队成员在技术上得到进步,产品开发成本也降到的最低。

迁移是一项非常耗时且艰难的任务,例如:有一个管理系统使用 AngularJS 开发维护已经有三年时间,但是随时间的推移和团队成员的变更,无论从开发成本还是用人需求上,AngularJS 已经不能满足要求,于是团队想要更新技术栈,AngularJS不再是主流的框架,想换成react,或者vue开发,想在其他框架中实现新的需求,但是现有项目怎么办?直接迁移是不可能的,在新的框架中完全重写也不太现实。使用微前端架构就可以解决问题,在保留原有项目的同时,可以完全使用新的框架开发新的需求,然后再使用微前端架构将旧的项目和新的项目进行整合。这样既可以使产品得到更好的用户体验,也可以使团队成员在技术上得到进步,产品开发成本也降到的最低。

  1. 允许应用中的小块区域独立发布和构建
    当应用足够庞大时,应用的发布和构建就会变的非常耗时,有的应用仅构建就需要花费10多分钟,构建完成,还需要发布,这也需要时间,但是使用了微前端架构之后,一个大的应用,可以被拆分成成一个一个小的应用,可以独立构建,独立发布的应用。哪个部分发生了变化,就独立修改,独立构建发布,而不是将整个应用构建发布。

  2. 允许不同的团队选择自己擅长的技术栈
    因为微前端架构与框架无关,当一个应用由多个团队进行开发时,每个团队都可以使用自己擅长的技术栈进行开发,也就是它允许适当的让团队决策使用哪种技术,从而使团队协作变得不再僵硬。

微前端的使用场景:

  1. 拆分巨型应用,使应用变得更加可维护
  2. 兼容历史应用,实现增量开发

3.如何实现微前端

  1. 多个微应用如何进行组合 ?
    实际上微前端架构中的每一个应用,最终都会被打包成模块,在浏览器当中通过加载这些模块,来运行不同的微应用。通过模块化的方式运行不同的微应用,可以方防止微应用之间的js,css的发生冲突。在微前端架构中,除了多个微应用以外,还存在一个容器应用,我们要将每个微应用都被注册到容器应用中。通过容器应用管理微应用的加载,运行以及卸载。

  2. 如何防止微应用与微应用之间发生路由冲突?
    在微前端架构中,当路由发生变化时,容器应用首先会拦截路由的变化,根据路由匹配微应用,当匹配到微应用以后,再启动微应用当中的路由,这样就可以防止微应用与微应用之间路由发生冲突。

  3. 微应用与微应用之间如何实现状态共享 ?

    在微应用中可以通过【发布订阅模式】实现状态共享,例如:使用 RxJS库。 RxJS不仅仅可以在ng里面使用,RxJS可以在任何框架中使用。

通过【发布订阅模式】可以轻松的实现微应用与微应用之间状态共享 。

  1. 微应用与微应用之间【如何实现框架和库的共享】?

例如:react框架本身,vue框架本身。因为,多个微应用之间很多时候需要使用同一个框架,或者是库。在微前端的架构中我们要使用模块化的方式去加载应用,在最新的模块化规范当中,新增了 import-map 特性,这个特性可以允许我们使用网络模块,而不是一定将模块下载到本地,我们只需要提前配置好模块名字与对应的模块地址就可以了。这样每个微应用在引用公共模块时,都可以引用提前配置好的公共模块。这样就可以解决多个微应用之间框架共享的问题了。当然后,还需要微应用本身的webpack配置,通过webpack中的externals属性告诉webpack在打包应用时,哪些模块是不需要被打包的。

通过 import-map 和 webpack 中的 externals 属性。

4.Systemjs的基本使用

1.Systemjs是什么?
Systemjs是一个动态模块加载器。因为在微前端架构中每一个应用都会被打包成一个模块,我们在浏览器🀄️加载他,运行它,但是到目前为止,浏览器还不支持模块化,或者说支持成都还不够好,这个时候我们就需要用到一个库,通过这个库,让浏览器可以加载动态模块。

在开发阶段我们可以使用 ES 模块规范,难道我们要使用systemjs库了之后,就要使用systemjs规范么?不是这样的,在开发阶段我们就是用es规范就行了。我们的应用通常都会使用webpack打包,在使用webpack打包的时候我们可以告诉webpack,我们要将es模块规范转换为systemjs能够支持的模块规范就可以了。

在微前端架构中,微应用被打包为模块,但浏览器不支持模块化,需要使用 systemjs 实现浏览器中的模块化。
systemjs 是一个用于实现模块化的 JavaScript 库,有属于自己的模块化规范。
在开发阶段我们可以使用 ES 模块规范,然后使用 webpack 将其转换为 systemjs 支持的模块。

案例:

  1. mkdir system-react
  2. cd system-react
  3. npm install
  4. npm start

package.json

{
  "name": "systemjs-react",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack serve"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/cli": "^7.12.10",
    "@babel/core": "^7.12.10",
    "@babel/preset-env": "^7.12.11",
    "@babel/preset-react": "^7.12.10",
    "babel-loader": "^8.2.2",
    "html-webpack-plugin": "^4.5.1",
    "webpack": "^5.17.0",
    "webpack-cli": "^4.4.0",
    "webpack-dev-server": "^3.11.2"
  }
}

webpack.config.js

const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    filename: "index.js",
    path: path.join(__dirname, "build"),
   
   	// 打包的的时候转换为system支持的模式 (一定要写,否则包react的错)
    libraryTarget: "system"
  },
  devtool: "source-map",
  devServer: {
    port: 9000,
    contentBase: path.join(__dirname, "build"),
    historyApiFallback: true
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env", "@babel/preset-react"]
          }
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      inject: false // 
    })
  ],
  // 哪些模块不需要打包
  externals: ["react", "react-dom", "react-router-dom"]
}

src/App.js

import React from "react"

export default function App() {
  return <div>App works</div>
}

src/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>systemjs-react</title>
    <!-- index.js文件中引入的React,react-dom,react-router-dom就是来自这个地方配置的。 -->
    <script type="systemjs-importmap">
      {
        "imports": {
          "react": "https://cdn.jsdelivr.net/npm/react/umd/react.production.min.js",
          "react-dom": "https://cdn.jsdelivr.net/npm/react-dom/umd/react-dom.production.min.js",
          "react-router-dom": "https://cdn.jsdelivr.net/npm/react-router-dom@5.2.0/umd/react-router-dom.min.js"
        }
      }
    </script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.0/dist/system.min.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <!-- System.import:引入模块。当前文件夹下面的index.js -->
    <script>
      System.import("./index.js")
    </script>
  </body>
</html>

src/index.js

import React from "react"
import ReactDOM from "react-dom"
import App from "./App.js"

ReactDOM.render(<App />, document.getElementById("root"))

5.single-spa概述

single-spa 是一个实现微前端架构的框架。
在 single-spa 框架中为我们提供了三种类型的微前端应用

  1. single-spa-application / parcel:普通的微应用。 微前端架构中的微应用,可以使用 vue、react、angular 等框
    架进行开发。这种微应用是和路由相关联的。例如:当我们访问“/a”地址的时候,加载哪一个微应用,当访问“/b”的时候,加载哪一个微应用。
    parcel这种微应用不和路由进行关联。主要用于跨应用共享UI组件。

  2. single-spa root config:创建微架构中的容器应用。我们通过容器应用管理普通的微应用。

  3. utility modules:公共模块应用,不是渲染UI组件的,他是用于跨应用共享 javascript 逻辑的微应用

6. 使用create-single-spa脚手架工具创建容器应用

  1. 安装 single-spa 脚手架工具: npm install create-single-spa@2.0.3 -g
  2. 查看脚手架信息:npm info create-single-spa
  3. 创建微前端应用目录: mkdir workspace && cd “$_”
  4. 创建微前端容器应用: create-single-spa
    1.应用文件夹填写 container
    2.应用选择 single-spa root config
    3.组织名称填写 study
    组织名称可以理解为团队名称,微前端架构允许多团队共同开发应用,组织名称可以标识应用
    由哪个团队开发。
    应用名称的命名规则为 @组织名称/应用名称 ,比如 @study/todos
  5. 启动应用: npm start
  6. 访问应用: localhost:9000
  7. 默认代码解析

请添加图片描述

Root-config.js

index.ejs: 是模版文件。只有这一个模版文件,其他的应用当中是没有这个文件的。
study-root-config.js : 容器应用的入口文件。

workspace/container/src/study-root-config.js

import { registerApplication, start } from "single-spa" 
/*
	registerApplication:注册微前端应用 
	1. name: 字符串类型, 当前注册的微应用名称是什么。
		结构如: "@组织名称/应用名称" 。例如:@single-spa/welcome  single-spa是组织名称,welcome是应用名称
	3. app: 函数类型, 返回的必须是Promise, 通过 systemjs 引用打包好的微前端应用模块代码 (umd),是一个线上地址
	4. activeWhen: 路由匹配时激活应用 
*/
registerApplication({ 
	name: "@single-spa/welcome", 
	
	app: () => System.import( "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js" ),
	
	// 当前这个微应用什么时候激活,例如:当浏览器地址栏中访问“/”时激活
	activeWhen: ["/"] 
})
// registerApplication({
//   name: "@study/navbar",
//   app: () => System.import("@study/navbar"),
//   activeWhen: ["/"]
// });

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

index.ejs

<!-- 导入微前端容器应用 --> 
<script> System.import("@study/root-config") </script> 
<!--
	import-map-overrides 可以覆盖导入映射 当前项目中用于配合 single-spa Inspector 调试工具使用. 可以手动覆盖项目中的 JavaScript 模块加载地址, 用于调试. 
-->
<import-map-overrides-full show-when-local-storage="devtools" dev-libs> </import-map-overrides-full>


<!-- 模块加载器 --> 
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.0/dist/system.min.js"> </script> 
<!-- systemjs 用来解析 AMD 模块的插件 --> 
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.0/dist/extras/amd.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> 
<!-- 用于支持 Angular 应用 --> 
<script src="https://cdn.jsdelivr.net/npm/zone.js@0.10.3/dist/zone.min.js"> </script>


<!-- single-spa 预加载 --> 
<link rel="preload" href="https://cdn.jsdelivr.net/npm/single-spa@5.8.3/lib/system/single- spa.min.js" as="script" />

<!-- JavaScript 模块下载地址 此处可放置微前端项目中的公共模块 --> 
<script type="systemjs-importmap"> 
{ "imports": { "single-spa": "https://cdn.jsdelivr.net/npm/single- spa@5.8.3/lib/system/single-spa.min.js" } } 
</script>

请添加图片描述

文件目录结构:
请添加图片描述

7.创建不基于框架的微应用

  1. 创建lagou项目的微应用
    mkdir container lagou && cd container lagou
    package.json
{
  "name": "lagou",
  "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"
  }
}

webpack.config.js

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

module.exports = () => {
  const defaultConfig = singleSpaDefaults({
    orgName: "study",
    projectName: "lagou"
  })
  return merge(defaultConfig, {
    devServer: {
      port: 9001
    }
  })
}

在应用入口文件中导出微应用所需的生命周期函数,生命周期函数必须返回promise,所以每个函数时用async 修饰返回promise
src/study-lagou.js

let lagouContainer = null

// 启动
export async function bootstrap() {
  console.log("应用正在启动")
}

// 挂载
export async function mount() {
  console.log("应用正在挂载")
  lagouContainer = document.createElement("div")
  lagouContainer.id = "lagouContainer"
  lagouContainer.innerHTML = "Hello Lagou"
  document.body.appendChild(lagouContainer)
}

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

请添加图片描述

请添加图片描述
2. 在微前端容器应用中注册lagou微前端应用

container/src/study-root-config.js

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

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

// 第2种方式书写
 registerApplication(
  "@single-spa/welcome",
  () =>
    System.import(
      "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
    ),
  location => location.pathname === "/" // 当访问/lago的时候会出现welcome应用的页面,那么需要设置location
)

// 注册lago应用
+ registerApplication({
+  name: "@study/lagou", // 微应用名
+  app: () => System.import("@study/lagou"),
+  activeWhen: ["/lagou"] // 当前这个微应用在什么时候激活
})

// 启动微应用
start({
  // 配置对象
  urlRerouteOnly: true 
})

  1. 在模板文件中指定模块访问地址

container/src/index.ejs

<script type="systemjs-importmap">
      {
        "imports": {
          "@study/root-config": "//localhost:9000/study-root-config.js",
 +        "@study/lagou": "//localhost:9001/study-lagou.js",
        }
      }
    </script>
  1. 修改默认应用代码(当访问/lago的时候会出现welcome应用的页面,那么需要设置location)
    如下视图:
    请添加图片描述
// 注意: 参数的传递方式发生了变化, 原来是传递了一个对象, 对象中有三项配置, 现在是传递了三 个参数 
registerApplication(
	 "@single-spa/welcome", 
	 () => System.import( "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js" ),
	 location => location.pathname === "/" // 当访问/lago的时候会出现welcome应用的页面,那么需要设置location
)

启动lagou:npm start
访问:localhost://9000/lago

8.创建基于react框架的微应用

1.基础create-single-spa创建react项目
请添加图片描述
目录结构:

2.修改应用端口 && 启动应用
todos/package.json

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

3.注册todos应用,将 React 项目的入口文件注册到基座应用中.
container/src/study-root-config.js

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

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

container/src/index.ejs

<!--在注册应用时 systemjs 引用了 @study/todos 模块, 所以需要配置该模块的引用地址 --> 
<script type="systemjs-importmap"> 
{ 
"imports": { 
"@study/root-config": "//localhost:9000/study-root-config.js", 			   
"@study/todos": "//localhost:9002/study-todos.js"
} 
}</script>

5.指定公共库的访问地址
默认情况下,应用中的 react 和 react-dom 没有被 webpack 打包, single-spa 认为它是公共库,
不应该单独打包。

container/src/index.ejs

<script type="systemjs-importmap"> 
{ "imports": 
	{ 
	"single-spa": "https://cdn.jsdelivr.net/npm/single- spa@5.8.3/lib/system/single-spa.min.js", 
	"react": "https://cdn.jsdelivr.net/npm/react@17.0.1/umd/react.production.min.js",  "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@17.0.1/umd/react- dom.production.min.js",
	 "react-router-dom": "https://cdn.jsdelivr.net/npm/react-router- dom@5.2.0/umd/react-router-dom.min.js" 
	 }
}
</script>

6.微前端 React 应用入口文件代码解析

todos/src/study-todos.js

// react、react-dom 的引用是 index.ejs 文件中 import-map 中指定的版本 
import React from "react" 
import ReactDOM from "react-dom"
// single-spa-react 用于创建使用 React 框架实现的微前端应用 
import singleSpaReact from "single-spa-react" 
// 用于渲染在页面中的根组件 
import rootComponent from "./root.component" 
// 指定根组件的渲染位置 
const domElementGetter = () => document.getElementById("todosContainer") 
// 错误边界函数 
const errorBoundary = () => <div>发生错误时此处内容将会被渲染</div> 

// 创建基于 React 框架的微前端应用, 返回生命周期函数对象
const lifecycles = singleSpaReact({ React, ReactDOM, rootComponent, domElementGetter, errorBoundary })

// 暴露必要的生命周期函数
export const { bootstrap, mount, unmount } = lifecycles

7.路由配置
todos/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="/todos"> 
		<div>{props.name}</div>
		<div> 
			<Link to="/home">Home</Link>
			<Link to="/about">About</Link>
		 </div>
		 <Switch>
		 	<Route path="/home"> <Home /> 
		 	</Route> <Route path="/about"> <About /> </Route> 
		 	<Route path="/"> <Redirect to="/home" /> </Route> 
		 </Switch>
   </BrowserRouter>
  ) }

todos/src/pages/About.js

import React from "react"

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

export default About

todos/src/pages/Hone.js

import React from "react"

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

export default About

8.修改 webpack 配置

const { merge } = require("webpack-merge") 
const singleSpaDefaults = require("webpack-config-single-spa-react") 
module.exports = (webpackConfigEnv, argv) => { 
const defaultConfig = singleSpaDefaults({ 
	orgName: "study", 
	projectName: "todos",
	webpackConfigEnv, argv
	})
return merge(
	 defaultConfig, 
+    { externals: ["react-router-dom"] })
}

9.创建基于Vue框架的微应用

  1. 创建vue微应用realworld:
    请添加图片描述
    创建应用: create-single-spa
    1.项目文件夹填写 realworld
    2.框架选择 Vue
    3.生成 Vue 2 项目

  2. 提取 vue && vue-router

vue.config.js

module.exports = { chainWebpack: config => { config.externals(["vue", "vue-router"]) } }

container/index.ejs

<script type="systemjs-importmap"> { "imports": { "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js", "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue- router.min.js" } }</script>
  1. 修改启动命令 && 启动应用

realworld/package.json

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

npm start

  1. 在containe中注册vue的微应用

container/src/study-root-config.js

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

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

container/src/index.ejs

 <script type="systemjs-importmap">
      {
        "imports": {
          "@study/root-config": "//localhost:9000/study-root-config.js",
          "@study/lagou": "//localhost:9001/study-lagou.js",
          "@study/todos": "//localhost:9002/study-todos.js",
+          "@study/realworld": "//localhost:9003/js/app.js",
        }
      }
    </script>

请添加图片描述
解决上面的报错添加amd.min.js库
container/src/index.ejs

 <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.0/dist/extras/amd.min.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.0/dist/extras/amd.min.js"></script>
    <% } %>

请添加图片描述
解决上面的报错注释meta: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';"
    /> -->
  1. 配置路由

realword/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 Foo = { template: "<div>Foo</div>" }
const Bar = { template: "<div>Bar</div>" }

const routes = [
  { path: "/foo", component: Foo },
  { path: "/bar", component: Bar }
]

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

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#lifecyle-props
          name: this.name,
          mountParcel: this.mountParcel,
          singleSpa: this.singleSpa
        }
      })
    }
  }
})

export const bootstrap = vueLifecycles.bootstrap
export const mount = vueLifecycles.mount
export const unmount = vueLifecycles.unmount

realword/App.js

<template>
  <div id="app">
    <div>
      <router-link to="/foo">foo</router-link>
      <router-link to="/bar">bar</router-link>
      <button @click="handleClick">button</button>
    </div>
    <router-view></router-view>
  </div>
</template>

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

<style></style>

9.创建Parcel应用

Parcel 用来创建公共 UI,这个公共UI并不是应用内部的公共UI,指的是跨框架跨应用的。
涉及到跨框架共享 UI 时需要使用Parcel。
Parcel 的定义可以使用任何 single-spa 支持的框架,它也是单独的应用,需要单独启动,但是它不关联
路由。
Parcel 应用的模块访问地址也需要被添加到 import-map 中,其他微应用通过 System.import 方法进行
引用。

需求:创建 navbar parcel,在不同的框架应用中使用它。

1.使用 React 创建 Parcel 应用 create-single-spa
请添加图片描述
navbar/study-navbar.js

import React from "react"
import ReactDOM from "react-dom"
import singleSpaReact from "single-spa-react"
import Root from "./root.component"

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

navbar/root.component.js

import React from "react"
import { BrowserRouter, Link } from "react-router-dom"

export default function Root(props) {
  return (
    <BrowserRouter>
      <div>
        <Link to="/">@single-spa/welcome</Link>{" "}
        <Link to="/lagou">@study/lagou</Link>{" "}
        <Link to="/todos">@study/todos</Link>{" "}
        <Link to="/realworld">@study/realworld</Link>
      </div>
    </BrowserRouter>
  )
}

  1. 在 webpack 配置文件中去除 react-router-dom

navbar/webpack.config.js

externals: ["react-router-dom"]
  1. 指定端口,启动应用

navbar/package.json

"scripts": { "start": "webpack serve --port 9004", }
  1. 在模板文件中指定应用模块地址

container/index.ejs

{ "imports": { "@study/navbar": "//localhost:9004/study-navbar.js" } }
  1. 在 React 应用中使用它

todos/src/root-component.js

import React from "react"
+ import Parcel from "single-spa-react/parcel"
import {
  BrowserRouter,
  Route,
  Link,
  Redirect,
  Switch
} from "react-router-dom"
import Home from "./Home"
import About from "./About"

export default function Root(props) {
  return (
    <BrowserRouter basename="/todos">
 +     <Parcel config={System.import("@study/navbar")} />
      <div>
        <Link to="/home">Home</Link>
        <Link to="/about">About</Link>
      </div>
      <Switch>
        <Route path="/home">
          <Home />
        </Route>
        <Route path="/about">
          <About />
        </Route>
        <Route path="/">
          <Redirect to="/home" />
        </Route>
      </Switch>
    </BrowserRouter>
  )
}

  1. 在 Vue 应用中使用它
    realword/src/App.vue
<template>
  <div id="app">
    <div>
      <Parcel :config="parcelConfig" :mountParcel="mountParcel" />
      <router-link to="/foo">foo</router-link>
      <router-link to="/bar">bar</router-link>
      <button @click="handleClick">button</button>
    </div>
    <router-view></router-view>
  </div>
</template>

<script>
+ import Parcel from "single-spa-vue/dist/esm/parcel"
+ import { mountRootParcel } from "single-spa"

export default {
  name: "App",
  components: {
  +  Parcel
  },
  data() {
    return {
 +     parcelConfig: window.System.import("@study/navbar"),
 +     mountParcel: mountRootParcel
    }
  },
  methods: {
}
</script>

<style></style>

realword/vue.config.js

module.exports = {
  chainWebpack: config => {
+    config.externals(["vue", "vue-router", "single-spa"])
  }
}

10. 创建跨框架共享的JavaScript逻辑

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

  1. 创建tools应用: create-single-spa
    1 文件夹填写 tools
    2.应用选择 in-browser utility module (styleguide, api cache, etc)
    请添加图片描述

  2. 修改端口,启动应用

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

3.在模板文件中声明应用模块访问地址

container/index.ejs

 <script type="systemjs-importmap">
      {
        "imports": {
          "@study/root-config": "//localhost:9000/study-root-config.js",
          "@study/lagou": "//localhost:9001/study-lagou.js",
          "@study/todos": "//localhost:9002/study-todos.js",
          "@study/realworld": "//localhost:9003/js/app.js",
          "@study/navbar": "//localhost:9004/study-navbar.js",
+          "@study/tools": "//localhost:9005/study-tools.js",
        }
      }
    </script>
  1. 应用中导出方法
    tools/src/study-tools.js
export function sayHello(who) {
  console.log(`%c${who} sayHello`, "color:skyblue")
}
  1. 在react项目中引用
    todos/src/Home.js
import React, { useState, useEffect } from "react"

// 加载公共模块
+ function useToolsModule() {
  const [toolsModule, setToolsModule] = useState()
  useEffect(() => {
    System.import("@study/tools").then(setToolsModule)
  }, [])
  return toolsModule
}

const Home = () => { 
+ const toolsModule = useToolsModule() 
+ if (toolsModule) toolsModule.sayHello("todos") 
return <div>Todos home works</div> }

export default Home

  1. 在 Vue 应用中使用该方法
<template>
  <div id="app">
    <div>
      <Parcel :config="parcelConfig" :mountParcel="mountParcel" />
      <router-link to="/foo">foo</router-link>
      <router-link to="/bar">bar</router-link>
  +    <button @click="handleClick">button</button>
    </div>
    <router-view></router-view>
  </div>
</template>

<script>
import Parcel from "single-spa-vue/dist/esm/parcel"
import { mountRootParcel } from "single-spa"

export default {
  name: "App",
  components: {
    Parcel
  },
  data() {
    return {
      parcelConfig: window.System.import("@study/navbar"),
      mountParcel: mountRootParcel
    }
  },
  methods: {
+    async handleClick() {
      const toolsModule = await window.System.import("@study/tools")
      toolsModule.sayHello("@study/realworld")
    }
  },
}
</script>

<style></style>

11.实现跨应用通讯 - 基于发布订阅模式

跨应用通信可以使用 RxJS,因为它无关于框架,也就是可以在任何其他框架中使用。

  1. 在 index.ejs 文件中添加 rxjs 的 import-map
 <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://cdn.jsdelivr.net/npm/react@17.0.1/umd/react.production.min.js",
          "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@17.0.1/umd/react-dom.production.min.js",
          "react-router-dom": "https://cdn.jsdelivr.net/npm/react-router-dom@5.2.0/umd/react-router-dom.min.js",
          "vue": "https://cdn.jsdelivr.net/npm/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://cdn.jsdelivr.net/npm/rxjs@6.6.3/bundles/rxjs.umd.min.js"
        }
      }
    </script>
  1. 在 tools 中导出一个 ReplaySubject,它可以广播历史消息,就算应用是动态加载进来
    的,也可以接收到数据。

tools/src/study-tools.js

+ import { ReplaySubject } from "rxjs"

export function sayHello(who) {
  console.log(`%c${who} sayHello`, "color:skyblue")
}

+ export const sharedSubject = new ReplaySubject()
  1. 在 React 应用中订阅它

// 订阅
useEffect(() => { let subjection = null 
if (toolsModule) { 
	subjection = toolsModule.sharedSubject.subscribe(console.log) }
	return () => subjection.unsubscribe() 
}, [toolsModule])

// 发送广播
<button onClick={() => toolsModule.sharedSubject.next("Hello Hello Hello")}>
 button
</button>
  1. 在 Vue 应用中订阅它
// 订阅
async mounted() { 
	let toolsModule = await window.System.import("@study/tools") toolsModule.sharedSubject.subscribe(console.log) 
}

12.布局引擎的使用方式

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

  1. 下载布局引擎 npm install single-spa-layout@1.3.1
  2. 构建路由
<body>
  +  <template id="single-spa-layout">
      <single-spa-router>
        <application name="@study/navbar""></application>
        <route default>
          <application name="@single-spa/welcome"></application>
        </route>
        <!-- 布局引擎的使用方式:path: 当前要访问的地址 -->
        <route path="lagou">
        // name: 应用名称
          <application name="@study/lagou"></application>
        </route>
        <route path="todos">
          <application name="@study/todos"></application>
        </route>
        <route path="realworld">
          <application name="@study/realworld"></application>
        </route>
      </single-spa-router>
    </template>
    <main></main>
    <script>
      System.import("@study/root-config")
    </script>
    <div id="root"></div>
    <import-map-overrides-full
      show-when-local-storage="devtools"
      dev-libs
    ></import-map-overrides-full>
  </body>
+ <script type="systemjs-importmap"> { "imports": { "@single-spa/welcome": "https://unpkg.com/single-spa- welcome/dist/single-spa-welcome.js" } }</script>
  1. 获取路由信息 && 注册应用
    container/study-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) 
start({ urlRerouteOnly: true })
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值