在了解微前端技术原理中咱们介绍了微前端的概念和核心技术原理。本篇咱们联合目前业内支流的微前端实现 single-spa 来阐明在生产实践中是如何实现微前端的。

single-spa 的文档略显凌乱,概念也比拟多,首次接触它的同学容易抓不住重点。明天咱们尝试整顿出一条清晰的脉络,让感兴趣的同学可能疾速了解它。

在 single-spa 的架构设计中,有两种次要角色,主利用和子利用,如下图。

主利用力求足够简略,只负责子利用的调度,业务逻辑都由子利用来承当。

外围能力

其实总结来说,single-spa 的外围就是定义了一套协定。通过这套协定,主利用能够不便的晓得在什么状况下激活哪个子利用。而这套协定次要蕴含两个局部:主利用的配置信息和子利用的生命周期函数

主利用的配置信息

在 single-spa 中,这个配置信息叫 Root Config。

上面的样例展现了配置信息的构造:

{  name: "subApp1",  app: () => System.import("/path/to/subApp1/code"),  activeWhen: "/subApp1",}

name 就是子利用的名称,app 函数通知主利用如何加载子利用的代码,activeWhen 通知主利用何时激活子利用,也能够为一个返回布尔值的函数。

通过 registerApplication 将子利用的信息注册到主利用中。

样例如下:

singleSpa.registerApplication({    name: 'appName',    app: () => System.import('appName'),    activeWhen: '/appName',    customProps: {        authToken: 'xc67f6as87f7s9d'    }})

子利用的生命周期函数

主利用在治理子利用的时候,通过子利用裸露的生命周期函数来实现子利用的启动和卸载。

次要有如下几个生命周期函数。

  • bootstrap

    这个生命周期函数会在利用第一次挂载前执行一次。就是说在子利用的代码加载实现当前,页面渲染之前执行。函数模式如下:

    export function bootstrap(props) {  return Promise    .resolve()    .then(() => {      // 能够在这里部署只执行一次的初始化代码      console.log('bootstrapped!')    });}
  • mount

    当主利用断定须要激活这个子利用时会调用这个生命周期函数。在这个函数中实现子利用的挂载、页面渲染等逻辑。这个函数也只会执行一次。咱们能够简略的了解为 ReactDOM.render 操作。函数模式如下:

    export function mount(props) {  return Promise.resolve().then(() => {    // 页面渲染逻辑    console.log('mounted!');  });}
  • unmount

    当主利用断定须要卸载这个子利用时会调用这个生命周期函数。在这个函数中实现组件卸载、清理事件监听等逻辑。咱们能够简略的了解为 ReactDOM.unmountComponentAtNode 操作。函数模式如下:

    export function unmount(props) {  return Promise.resolve().then(() => {    // 页面卸载逻辑    console.log('unmounted!');  });}

察看每个生命周期函数的签名咱们能够发现,每个函数都有一个 props 参数,主利用能够通过这个参数向子利用传递一些额定信息,前面会做阐明。

为了不便各种技术栈的子利用能不便的接入,single-spa 提供了很多工具,能够在这里查到官网保护的工具列表。

其余概念

子利用的分类

single-spa 依据职能的不同,把子利用划分成三类:

  • Application
    示意一般的子利用,须要实现下面提到的生命周期函数;
  • Parcel
    能够了解为能够跨子利用复用的业务单元,须要实现与之对应的生命周期函数;
  • Utility
    示意一段可复用的逻辑,比方一个函数等,不做页面渲染。

不难看出,Parcel 和 Utility 都是为了共享和复用,也算是 single-spa 在框架层面给出的一种复用计划。

Layout Engine

尽管 single-spa 的理念是让主利用尽可能的简略,然而在实践中,主利用通常会负责通用的顶部、底部通栏的渲染。这个时候,如何确定子利用的渲染地位就成了一个问题。

single-spa 提供了 Layout Engine的计划。样例代码如下,与 Vue 颇为类似,具体的能够查看文档,这里不做过多叙述。

<html>  <head>    <template id="single-spa-layout">      <single-spa-router>        <nav class="topnav">          <application name="@organization/nav"></application>        </nav>        <div class="main-content">          <route path="settings">            <application name="@organization/settings"></application>          </route>          <route path="clients">            <application name="@organization/clients"></application>          </route>        </div>        <footer>          <application name="@organization/footer"></application>        </footer>      </single-spa-router>    </template>  </head></html>

对于 SystemJS

很多人在提到 single-spa 的时候都会提到 SystemJS,认为 SystemJS 是 single-spa 的外围之一。其实这是一个误区, SystemJS 并不是 single-spa 所必须的。

后面说到,子利用要实现生命周期函数,而后导出给主利用应用。要害就是这个“导出”的实现,这就波及到 JavaScript 的模块化问题。

在一些古代浏览器中,咱们能够通过在 <script> 标签上增加 type="module" 来实现导入导出。

<script type="module" src="module.js"></script><script type="module">  // or an inline script  import {helperMethod} from './providesHelperMethod.js';  helperMethod();</script>// providesHelperMethod.jsexport function helperMethod() {  console.info(`I'm helping!`);}

然而如果咱们想要实现 import axios from 'axios' 还须要借助于 importmap

<script type="importmap">    {       "imports": {          "axios": "https://cdn.jsdelivr.net/npm/axios@0.20.0/dist/axios.min.js"       }    }</script><script type="module">  import axios from 'axios'</script>

在低版本浏览器中,咱们就须要借助于一些 “Polyfill” 来实现模块化了。SystemJS 就是解决这个问题的。所以 single-spa 的样例中大量采纳了 SystemJS 来加载利用。

其实也能够不必 SystemJS,webpack 也能够实现相似的能力,然而会加深主利用与子利用间的工程耦合。

隔离

在了解微前端技术原理中,咱们花了很长的篇幅来阐明子利用隔离的思路。那么,single-spa 中是如何来实现隔离的呢?

款式隔离

single-spa 中的款式隔离能够分为两块来说。

首先是子利用款式的加载和卸载。single-spa 提供了 single-spa-css 这个工具来实现。

import singleSpaCss from 'single-spa-css';const cssLifecycles = singleSpaCss({  // 须要加载的 css 列表  cssUrls: ['https://example.com/main.css'],  // 是否是 webpack 导出的 css,如果是要做额定解决(webpack 导出的文件名通常会有 hash)  webpackExtractedCss: false,  // 当子利用 unmount 的时候,css 是否须要一并删除  shouldUnmount: true,});const reactLifecycles = singleSpaReact({...})// 退出到子利用的 bootstrap 里export const bootstrap = [  cssLifecycles.bootstrap,  reactLifecycles.bootstrap]export const mount = [  // 退出到子利用的 mount 里,css 放后面,不然 mount 后会有款式闪动(FOUC)的问题  cssLifecycles.mount,  reactLifecycles.mount]export const unmount = [  // 后卸载 css,避免款式闪动  reactLifecycles.unmount,  cssLifecycles.unmount]

如果款式是 webpack 导出的,则每次构建后都要更新款式文件列表。single-spa 贴心的筹备了一个插件来解决这个问题。只有在 webpack 的配置文件中增加如下插件即可。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");const ExposeRuntimeCssAssetsPlugin = require("single-spa-css/ExposeRuntimeCssAssetsPlugin.cjs");module.exports = {  plugins: [    new MiniCssExtractPlugin({      filename: "[name].css",    }),    new ExposeRuntimeCssAssetsPlugin({      // filename 必须与 MiniCssExtractPlugin 中的 filename 一一对应      filename: "[name].css",    }),  ],};

解决了子利用款式加载和卸载问题当前,咱们再来看子利用款式隔离的问题。

single-spa 给出了一些倡议,比方应用 Scoped CSS,每个子利用都有一个固定前缀,相似于上面这样:

/*<div class="app1__settings-67f89dd87sf89ds"></div>*/.app1__settings-67f89dd87sf89ds {  color: blue;}/*<div data-df65s76dfs class="settings"></div>*/.settings[data-df65s76dfs] {  color: blue;}/*<div id="single-spa-application:@org-name/project-name">    <div class="settings"></div>  </div>*/#single-spa-application\:\@org-name\/project-name .settings {  color: blue;}

有很多工具能够实现 Scoped CSS,比方 CSS Modules 等。

最初一种形式咱们能够通过 webpack 自动化的实现。

const prefixer = require('postcss-prefix-selector');module.exports = {  plugins: [    prefixer({      prefix: "#single-spa-application\\:\\@org-name\\/project-name"    })  ]}

single-spa 也提到了 Shadow DOM,咱们在上一篇文章中曾经剖析过,这里不再赘述了。

JS 隔离

single-spa 采纳了相似于快照模式的隔离机制,通过 single-spa-leaked-globals 来实现。

用法如下:

import singleSpaLeakedGlobals from 'single-spa-leaked-globals';// 其它 single-spa-xxx 提供的生命周期函数const frameworkLifecycles = ...// 新增加的全局变量const leakedGlobalsLifecycles = singleSpaLeakedGlobals({  globalVariableNames: ['$', 'jQuery', '_'],})export const bootstrap = [  leakedGlobalsLifecycles.bootstrap, // 放在第一位  frameworkLifecycles.bootstrap,]export const mount = [  leakedGlobalsLifecycles.mount, // mount 时增加全局变量,如果之前有记录在案的,间接复原  frameworkLifecycles.mount,]export const unmount = [  leakedGlobalsLifecycles.unmount, // 删掉新增加的全局变量  frameworkLifecycles.unmount,]
后面曾经说过,快照模式的一个毛病是无奈保障多个子利用同时运行时的无效隔离。

小结

总体来说,single-spa 算是根本实现了一个微前端框架须要具备的各种性能,然而又实现的不够彻底,遗留了很多问题须要解决。尽管官网提供了很多样例和最佳实际,然而总显得过于薄弱,总给人一种“问题解决了,然而又没有齐全解决”的感觉。

qiankun 基于 single-spa 开发,肯定水平上解决了很多 single-spa 没有解决的问题。咱们下篇具体阐明。

常见面试知识点、技术计划剖析、教程,都能够扫码关注公众号“众里千寻”获取,或者来这里 https://everfind.github.io/po... 。