微前端基础

470 阅读9分钟

微前端概述

什么是微前端?

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

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

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

image.png

微前端的价值

1、增量迁移

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

2、独立发布

在目前的单页应用架构中,使用组件构建用户界面,应用中的每个组件或功能开发完成或者bug修复完成后,每次都需要对整个产品重新进行构建和发布,任务耗时操作上也比较繁琐。 在使用了微前端架构后,可以将不同的功能模块拆分成独立的应用,此时功能模块就可以单独构建单独发布了,构建时间也会变得非常快,应用发布后不需要更改其他内容应用就会自动更新,这意味着你可以进行频繁的构建发布操作了。

3、允许单个团队做出技术决策

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

image.png

微前端使用场景

1、拆分巨石应用,使应用变得更加可维护

2、兼容历史应用,实现增量开发

和微前端架构相关的四个问题

1、多个微应用如何进行组合?

在微前端架构中,除了存在多个微应用以外,还存在一个容器应用,每个微应用都需要注册到容器应用中。

微前端中的每个应用在浏览器中都是一个独立的JS模块,通过模块化的方式被容器应用启动和运行。

使用模块化的方式运行应用可以防止不同的微应用在同时运行时发生冲突。

2、在微应用中如何实现路由?

在微前端架构中,当路由发生变化时,容器应用首先会拦截路由的变化,根据路由匹配微前端应用,当匹配到微应用以后,再启动微应用路由,匹配具体的页面组件。

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

在微应用中可以通过发布订阅模式实现状态共享,比如使用RxJS.

4、微应用与微应用之间如何实现框架和库的共享?

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

systemjs模块化解决方案

systemjs

概述

在微前端架构中,微应用被打包为模块,但浏览器不支持模块化,需要使用systemjs实现浏览器中的模块化。

systemjs是一个用于实现模块化的JS库,有属于自己的模块化规范

在开发阶段使用ES模块规范,然后使用webpack将其转换为systemjs支持的模块。

体验

案例:通过 webpack 将 react 应用打包为 systemjs 模块,在通过 systemjs 在浏览器中加载模块.

npm install webpack@5.17.0 webpack-cli@4.4.0 webpack-dev-server@3.11.2 html-webpack-plugin@4.5.1 @babel/core@7.12.10 @babel/cli@7.12.10 @babel/preset-env@7.12.11 @babel/preset-react@7.12.10 babel-loader@8.2.2

image.png

image.png

image.png

微前端框架 single-spa

概述

single-spa是一个实现微前端架构的框架。

在single-spa框架中有三种类型的微前端应用:

  • single-spa-application / parcel : 微前端架构中的微应用,可以使用Vue、react等框架

  • single-spa root config: 创建微前端容器应用。

  • utility modules: 公共模块应用,非渲染组件,用于跨应用共享JS逻辑的微应用。

创建容器应用

1、安装single-spa脚手架工具

npm install create-single-spa@2.0.3 -g

2、创建微前端应用目录: mkdir workspace && cd "$_"

3、 创建微前端容器应用: create-single-spa

  • 应用文件夹填写 container
  • 应用选择 single-spa root config
  • 组织名称填写 study

组织名称可以理解为团队名称,微前端架构允许多团队共同开发应用,组织名称可以标识应用 由哪个团队开发。

应用名称的命名规则为 @组织名称/应用名称 ,比如 @study/todos

4、启动应用: npm start

:【[webpack-cli] Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.】 升级 webpack-dev-server 5、默认代码解析

  • Root-config.js[// workspace/container/src/study-root-config.js]
import { registerApplication, start } from "single-spa";

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


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

start({
  urlRerouteOnly: true,
});
  • index.ejs
<script>
    //导入 微前端容器应用
    System.import('@study/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>
<!-- 用于覆盖通过 import-map 设置的JS模块下载地址 -->
  <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>

  <% } 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>
  <!-- single-spa预加载 -->
  <link rel="preload" href="https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js" as="script">
 <!-- js模块下载地址 此处可放置微前端项目中的公共模块  -->
  <script type="systemjs-importmap">
    {
      "imports": {
        "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js"
      }
    }
  </script>

创建不基于框架的微应用

1、应用初始化

mkdir common && cd "$_"

2、配置 webpack [webpack.config.js]

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

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

3、在 package.json 文件中添加应用启动命令

"scripts": {
    "start": "webpack serve"
  },

4、在应用入口文件中导出微前端应用所需的生命周期函数,生命周期函数必须返回 Promise[src/study-common.js 命名方式为orgName-projectName]

let container = null

export const bootstrap = async function (){
  console.log("一般应用启动中。。");
}

export const mount = async function(){
  console.log("一般应用挂载中。。");
  container = document.createElement("div")
  container.innerHTML = "这个是没有使用框架的一般应用"
  container.id = "container"
  document.body.appendChild(container)
}
export const unmount = async function(){
  console.log("一般应用卸载中");
  document.body.removeChild(container)
}

5、在微前端容器应用中注册微前端应用

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

6、在模板文件中指定模块访问地址

// index.ejs
<script type="systemjs-importmap">
    {
      "imports": {
        "@study/root-config": "//localhost:9000/study-root-config.js",
        "@study/common":"//localhost:9001/study-common.js"
      }
    }
  </script>

7、修改默认应用代码

// 注意: 参数的传递方式发生了变化, 原来是传递了一个对象, 对象中有三项配置, 现在是传递了三
个参数
registerApplication("@single-spa/welcome", () =>
  System.import(
    "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
  ),

  location => location.pathname === '/')

创建基于react的微应用

1、创建应用 create-single-spa

  • 应用目录输入 todos
  • 框架选择react

2、修改应用端口

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

3、注册应用,将 React 项目的入口文件注册到基座应用中

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

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

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

5、指定公共库的访问地址

默认情况下,应用中的 react 和 react-dom 没有被 webpack 打包, single-spa 认为它是公共库, 不应该单独打包

 <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"
      }
    }
  </script>

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

// 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 Root from "./root.component";

// 指定根组件的渲染位置
// const domElementGetter = () => document.getElementById("todosContainer")


// 错误边界函数
const errorBoundary = () => <div>发生错误时此处内容将会被渲染</div>

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

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

7、路由配置

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


export default function Root(props) {
  return  <BrowserRouter basename="/todos">
    <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"></Redirect>
      </Route>
    </Switch>
  </BrowserRouter>
 
}

8、修改 webpack 配置

// webpack.config.js   react-router-dom 不需要打包
return merge(defaultConfig, {
    externals: ["react-router-dom"]
  });
};

创建基于Vue的微应用

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

  • 项目文件夹 realworld
  • 框架 Vue2.0

2、 提取 vue && vue-router

// vue.config.js
module.exports = {
  chainWebpack:config =>{
    config.externals(["vue","vue-router"])
  }
}
<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>

3、修改启动命令

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

4、注册应用

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

5、指定微前端应用模块的引用地址【地址跟react的不同哦】

  <script type="systemjs-importmap">
    {
      "imports": {
        "@study/realworld":"//localhost:9003/js/app.js"
      }
    }
  </script>

6、Vue 应用配置路由

// 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)

// 路由组件
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'
})

Vue.config.productionTip = false;

const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    router,
    render(h) {
      return h(App, {
        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;

App.vue

<template>
  <div id="app">
    <h1>{{ name }}</h1>
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
    <router-view></router-view>
  </div>
</template>

<script>

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

创建Parcel应用

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

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

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

需求:创建 Parcel ,在不同的应用中使用它。

1、使用 React 创建 Parcel 应用 create-single-spa [新的]

import {BrowserRouter,Link} from 'react-router-dom'

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

2、在 webpack 配置文件中去除 react-router-dom[新的]
externals: ["react-router-dom"]
3、指定端口[新的]

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

4、在模板文件中指定应用模块地址[index.ejs]

{
"imports": {
    "@study/navbar": "//localhost:9004/study-navbar.js"
}

5、在 React 应用中使用它[老的]

import Parcel from "single-spa-react/parcel"
<Parcel config={System.import("@study/navbar")} />

6、在 Vue 应用中使用它

<Parcel :config="parcelConfig" :mountParcel="mountParcel" />
<script>
import Parcel from "single-spa-vue/dist/esm/parcel"
// 这里注意 需要在Vue项目中排除single-spa  config.externals(["vue","vue-router","single-spa"])
import { mountRootParcel } from "single-spa"

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

创建utility modules

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

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

  • 文件夹填写 tools
  • 应用选择 in-browser utility module

2、修改端口

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

3、应用中导出方法

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

4、在模板文件中声明应用模块访问地址

<script type="systemjs-importmap">
{
"imports": {
    "@study/tools": "//localhost:9005/study-tools.js"
  }
}
</script>

5、 在 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

6、在 Vue 应用中使用该方法

<h1 @click="handleClick">{{ name }}</h1>

async handleClick() {
  let toolsModule = await window.System.import("@study/tools")
  toolsModule.sayHello("realworld")

实现跨应用通信

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

  1. 在 index.ejs 文件中添加 rxjs 的 import-map
{
"imports": {
  "rxjs":
    "https://cdn.jsdelivr.net/npm/rxjs@6.6.3/bundles/rxjs.umd.min.js"
  }
}
  1. 在 utility modules 中导出一个 ReplaySubject,它可以广播历史消息,就算应用是动态加载进来 的,也可以接收到数据。
// tools
import { ReplaySubject } from "rxjs"
export const sharedSubject = new ReplaySubject()
  1. 在 React 应用中订阅它
useEffect(() => {
  let subjection = null
  if (toolsModule) {
    subjection = toolsModule.sharedSubject.subscribe(console.log)
  }
  return () => subjection && subjection.unsubscribe()
}, [toolsModule]
  
// 发布消息
          
 <button onClick={() => toolsModule.sharedSubject.next('fa fa fa')}>send</button>
  1. 在 Vue 应用中订阅它
async mounted() {
  let toolsModule = await window.System.import("@study/tools")
  toolsModule.sharedSubject.subscribe(console.log)
}

布局引擎(Layout Engine)的使用

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

  1. 下载布局引擎 npm install single-spa-layout@1.3.1

container 文件夹下

  1. 构建路由
 <script type="systemjs-importmap">
    {
      "imports": {
          "@single-spa/welcome": "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
      }
    }
  </script>
<template id="single-spa-layout">
    <single-spa-router>
      <application name="@study/parcel"></application>
     <route default>
        <application name="@single-spa/welcome"></application>
     </route>
     <route path="common">
        <application name="@study/common"></application>
      </route>
      <route path="todos">
       <application name="@study/todos"></application>
     </route>
     <route path="realworld">
        <application name="@study/realworld"></application>
      </route>
    </single-spa-router>
  </template>
  1. 获取路由信息 && 注册应用
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
})

示例代码: github.com/demong89/mi…