微前端框架 single-spa 技术分析
在理解微前端技术原理中我们介绍了微前端的概念和核心技术原理。本篇我们结合目前业内主流的微前端实现 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 技术分析的更多相关文章
- 微前端框架 qiankun 技术分析
我们在single-spa 技术分析 基本实现了一个微前端框架需要具备的各种功能,但是又实现的不够彻底,遗留了很多问题需要解决.虽然官方提供了很多样例和最佳实践,但是总显得过于单薄,总给人一种&quo ...
- 微前端框架 之 qiankun 从入门到源码分析
封面 简介 从 single-spa 的缺陷讲起 -> qiankun 是如何从框架层面解决 single-spa 存在的问题 -> qiankun 源码解读,带你全方位刨析 qianku ...
- 极致简洁的微前端框架-京东MicroApp开源了
前言 MicroApp是一款基于类WebComponent进行渲染的微前端框架,不同于目前流行的开源框架,它从组件化的思维实现微前端,旨在降低上手难度.提升工作效率.它是目前市面上接入微前端成本最低的 ...
- 微前端框架single-spa初探
前言 最近入职的一家公司采用single-spa这个微前端框架,所以自学了此框架. single-spa这个微前端框架虽然有中文文档,但是有些零散和晦涩. 所以我想在学习之余,写篇博客拉平一下这个学习 ...
- 基于 iframe 的微前端框架 —— 擎天
vivo 互联网前端团队- Jiang Zuohan 一.背景 VAPD是一款专为团队协作办公场景设计的项目管理工具,实践敏捷开发与持续交付,以「项目」为核心,融合需求.任务.缺陷等应用,使用敏捷迭代 ...
- 微前端框架 single-spa
单体应用对比前端微服务化 普通的前端单体应用 微前端架构 1.基本概念 实现一套微前端架构,可以把其分成四部分(参考:https://alili.tech/archive/11052bf4/) 加载器 ...
- 微前端大赏二-singlespa实践
微前端大赏二-singlespa实践 微前端大赏二-singlespa实践 序 介绍singleSpa singleSpa核心逻辑 搭建环境 vue main react child 生命周期 结论 ...
- 基于微前端qiankun的多页签缓存方案实践
作者:vivo 互联网前端团队- Tang Xiao 本文梳理了基于阿里开源微前端框架qiankun,实现多页签及子应用缓存的方案,同时还类比了多个不同方案之间的区别及优劣势,为使用微前端进行多页签开 ...
- 微前端 & 微前端实践 & 微前端教程
微前端 & 微前端实践 & 微前端教程 微前端 micro frontends https://micro-frontends.org/ https://github.com/neul ...
随机推荐
- 联想SR658安装显卡驱动【NVIDIA Tesla V100】
1. 安装基础依赖环境 yum -y install gcc kernel-devel kernel-headers 2.查看内核和源码版本是否一致 查看内核版本: ls /boot | grep v ...
- js_数据类型转换
转整数----parseInt(string,radix) 1)类似于从左往右匹配数字,直到匹配到非数字结束,并返回匹配到的数字.同parseFloat(). parseInt("123&q ...
- STM32定时器学习---基本定时器
STM32F1系列的产品,除了互联网产品外,工作8个,3种定时器,其中一种就是基本定时器.那么STM32单片机的基本定时器如何操作以及编程呢? 下面我们就来详细的了解一下 STM32F1系列的产品,除 ...
- spring cloud Alibaba --sentinel组件的使用
sentinel组件 对于sentinel的前置知识这里就不多说了: 直接上代码: Release v1.8.1 · alibaba/Sentinel · GitHub 下载地址 springclo ...
- hdu 5083 Instruction (稍比较复杂的模拟题)
题意: 二进制指令转汇编指令,汇编指令转二进制指令. 思路: 额,条理分好,想全,思维不能乱. 代码: int findyu(char yu[50],char c){ int l=strlen(yu) ...
- mysql查看数据库大小
要想知道每个数据库的大小的话,步骤如下: 1.进入information_schema 数据库(存放了其他的数据库的信息) use information_schema; 2.查询所有数据的大小: s ...
- VSCode PHP 开发环境配置 详细教程
VSCode PHP 开发环境配置 详细教程 这篇文章主要介绍了VScode+PHPstudy配置PHP开发环境的步骤,整理了官方以及优秀第三方的内容,对于学习和工作有一定借鉴意义. 配置过程 第一步 ...
- [python]RobotFramework自定义库实现UI自动化
1.安装教程 环境搭建不多说,网上资料一大堆,可参考https://www.cnblogs.com/puresoul/p/3854963.html,写的比较详细,值得推荐.目前python3是不支持r ...
- 设计模式学习-使用go实现适配器模式
适配器模式 定义 代码实现 优点 缺点 适用范围 代理.桥接.装饰器.适配器4种设计模式的区别 参考 适配器模式 定义 适配器模式的英文翻译是Adapter Design Pattern.顾名思义,这 ...
- Part 23 to 26 Routing in Angular
Part 23 AngularJS routing tutorial In general, as the application becomes complex you will have more ...