理解微前端技术原理中我们介绍了微前端的概念和核心技术原理。本篇我们结合目前业内主流的微前端实现 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.js
export 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/posts/

微前端框架 single-spa 技术分析的更多相关文章

  1. 微前端框架 qiankun 技术分析

    我们在single-spa 技术分析 基本实现了一个微前端框架需要具备的各种功能,但是又实现的不够彻底,遗留了很多问题需要解决.虽然官方提供了很多样例和最佳实践,但是总显得过于单薄,总给人一种&quo ...

  2. 微前端框架 之 qiankun 从入门到源码分析

    封面 简介 从 single-spa 的缺陷讲起 -> qiankun 是如何从框架层面解决 single-spa 存在的问题 -> qiankun 源码解读,带你全方位刨析 qianku ...

  3. 极致简洁的微前端框架-京东MicroApp开源了

    前言 MicroApp是一款基于类WebComponent进行渲染的微前端框架,不同于目前流行的开源框架,它从组件化的思维实现微前端,旨在降低上手难度.提升工作效率.它是目前市面上接入微前端成本最低的 ...

  4. 微前端框架single-spa初探

    前言 最近入职的一家公司采用single-spa这个微前端框架,所以自学了此框架. single-spa这个微前端框架虽然有中文文档,但是有些零散和晦涩. 所以我想在学习之余,写篇博客拉平一下这个学习 ...

  5. 基于 iframe 的微前端框架 —— 擎天

    vivo 互联网前端团队- Jiang Zuohan 一.背景 VAPD是一款专为团队协作办公场景设计的项目管理工具,实践敏捷开发与持续交付,以「项目」为核心,融合需求.任务.缺陷等应用,使用敏捷迭代 ...

  6. 微前端框架 single-spa

    单体应用对比前端微服务化 普通的前端单体应用 微前端架构 1.基本概念 实现一套微前端架构,可以把其分成四部分(参考:https://alili.tech/archive/11052bf4/) 加载器 ...

  7. 微前端大赏二-singlespa实践

    微前端大赏二-singlespa实践 微前端大赏二-singlespa实践 序 介绍singleSpa singleSpa核心逻辑 搭建环境 vue main react child 生命周期 结论 ...

  8. 基于微前端qiankun的多页签缓存方案实践

    作者:vivo 互联网前端团队- Tang Xiao 本文梳理了基于阿里开源微前端框架qiankun,实现多页签及子应用缓存的方案,同时还类比了多个不同方案之间的区别及优劣势,为使用微前端进行多页签开 ...

  9. 微前端 & 微前端实践 & 微前端教程

    微前端 & 微前端实践 & 微前端教程 微前端 micro frontends https://micro-frontends.org/ https://github.com/neul ...

随机推荐

  1. 联想SR658安装显卡驱动【NVIDIA Tesla V100】

    1. 安装基础依赖环境 yum -y install gcc kernel-devel kernel-headers 2.查看内核和源码版本是否一致 查看内核版本: ls /boot | grep v ...

  2. js_数据类型转换

    转整数----parseInt(string,radix) 1)类似于从左往右匹配数字,直到匹配到非数字结束,并返回匹配到的数字.同parseFloat(). parseInt("123&q ...

  3. STM32定时器学习---基本定时器

    STM32F1系列的产品,除了互联网产品外,工作8个,3种定时器,其中一种就是基本定时器.那么STM32单片机的基本定时器如何操作以及编程呢? 下面我们就来详细的了解一下 STM32F1系列的产品,除 ...

  4. spring cloud Alibaba --sentinel组件的使用

    sentinel组件 对于sentinel的前置知识这里就不多说了: 直接上代码: Release v1.8.1 · alibaba/Sentinel · GitHub  下载地址 springclo ...

  5. hdu 5083 Instruction (稍比较复杂的模拟题)

    题意: 二进制指令转汇编指令,汇编指令转二进制指令. 思路: 额,条理分好,想全,思维不能乱. 代码: int findyu(char yu[50],char c){ int l=strlen(yu) ...

  6. mysql查看数据库大小

    要想知道每个数据库的大小的话,步骤如下: 1.进入information_schema 数据库(存放了其他的数据库的信息) use information_schema; 2.查询所有数据的大小: s ...

  7. VSCode PHP 开发环境配置 详细教程

    VSCode PHP 开发环境配置 详细教程 这篇文章主要介绍了VScode+PHPstudy配置PHP开发环境的步骤,整理了官方以及优秀第三方的内容,对于学习和工作有一定借鉴意义. 配置过程 第一步 ...

  8. [python]RobotFramework自定义库实现UI自动化

    1.安装教程 环境搭建不多说,网上资料一大堆,可参考https://www.cnblogs.com/puresoul/p/3854963.html,写的比较详细,值得推荐.目前python3是不支持r ...

  9. 设计模式学习-使用go实现适配器模式

    适配器模式 定义 代码实现 优点 缺点 适用范围 代理.桥接.装饰器.适配器4种设计模式的区别 参考 适配器模式 定义 适配器模式的英文翻译是Adapter Design Pattern.顾名思义,这 ...

  10. Part 23 to 26 Routing in Angular

    Part 23 AngularJS routing tutorial In general, as the application becomes complex you will have more ...