【微前端】single-spa 到底是个什么鬼(下)

简介: 说起微前端框架,很多人第一反应就是 single-spa。但是再问深入一点:它是干嘛的,它有什么用,可能就回答不出来了。 一方面没多少人研究和使用微前端。可能还没来得及用微前端扩展项目,公司就已经倒闭了。 另一方面是中文博客对微前端的研究少之又少,很多文章只是简单翻译一下官方文档,读几个API,放个官方的 Demo 就完事了。很少有深入研究到底 single-spa 是怎么一回事的。这篇文章将不会聊怎么搭建一个 Demo,而是会从 “Why” 和 “How” 的角度来聊一下官方文档的都讲了哪些内容,相信看完这篇文章就能看懂 官方的 Demo 了。

image.png

改造子应用


上面说的都是主应用的事情,现在我们来关心一下子应用。

子应用最关键的一步就是导出 bootstrap, mount, unmount 三个生命周期钩子。

import SubApp from './index.tsx'
export const bootstrap = () => {}
export const mount = () => {
  // 使用 React 来渲染子应用的根组件
  ReactDOM.render(<SubApp/>, document.getElementById('root'));
}
export const unmount = () => {}
复制代码


single-spa-react, single-spa-vue, single-spa-angular, single-spa-xxx, ...


emmmm,怎么说的呢,上面三个 export 不太好看,能不能有一种更直接的方法就实现 3 个生命周期的导出呢?


single-spa 说:可以啊,搞!所以有了 single-spa-react:

import React from 'react';
import ReactDOM from 'react-dom';
import SubApp from './index.tsx';
import singleSpaReact, {SingleSpaContext} from 'single-spa-react';
const reactLifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: SubApp,
  errorBoundary(err, info, props) {
    return (
      <div>出错啦!</div>
    );
  },
});
export const bootstrap = reactLifecycles.bootstrap;
export const mount = reactLifecycles.mount;
export const unmount = reactLifecycles.unmount;
复制代码


single-spa 说:我不能单给 react 搞啊,别的框架也要给它们整上一个,一碗水端平,所以有这了这些牛鬼蛇神:

image.png

不禁感慨:这些小轮子是真能造啊。


导入子应用的 CSS


不知道你有没有注意到,在刚刚的子应用注册里我们仅仅用 System.import 导入了一个 JS 文件,那 CSS 样式文件怎么搞呢?可能可以 System.import('xxx.css') 来导入。


但是,这又有问题了:在切换了应用时,unmount 的时候要怎么把已有的 CSS 给删掉呢?官方说可以这样:

const style = document.createElement('style');
style.textContent = `.settings {color: blue;}`;
export const mount = [
  async () => {
    document.head.appendChild(styleElement);
  },
  reactLifecycles.mount,
]
export const unmount = [
  reactLifecycles.unmount,
  async () => {
    styleElement.remove();
  }
]
复制代码


我:single-spa,求求你做个人吧,搭个 Demo,还要我来处理 CSS?single-spa 说:好,等我再去造一个轮子。于是,就有了 single-spa-css。用法如下:

import singleSpaCss from 'single-spa-css';
const cssLifecycles = singleSpaCss({
  // 这里放你导出的 CSS,如果 webpackExtractedCss 为 true,可以不指定
  cssUrls: ['https://example.com/main.css'],
  // 是否要使用从 Webpack 导出的 CSS,默认为 false
  webpackExtractedCss: false,
  // 是否 unmount 后被移除,默认为 true
  shouldUnmount: true,
  // 超时,不废话了,都懂的
  timeout: 5000
})
const reactLifecycles = singleSpaReact({...})
// 加入到子应用的 bootstrap 里
export const bootstrap = [
  cssLifecycles.bootstrap,
  reactLifecycles.bootstrap
]
export const mount = [
  // 加入到子应用的 mount 里,一定要在前面,不然 mount 后会有样式闪一下的问题
  cssLifecycles.mount,
  reactLifecycles.mount
]
export const unmount = [
  // 和 mount 同理
  reactLifecycles.unmount,
  cssLifecycles.unmount
]
复制代码

这里要注意一下,上面的 example.com/main.css 并没有看起来那么简单易用。


假如你用了 Webpack 来打包,很有可能会用分包或者 content hash 来给 CSS 文件命名,比如 filename: "[name].[contenthash].css"。那请问 cssUrls 要怎么写呀,每次都要改 cssUrls 参数么?太麻烦了吧。


single-spa-css 说:我可以通过 Webpack 导出的 __webpack_require__.cssAssetFileName 获取导出之后的真实 CSS 文件名。ExposeRuntimeCssAssetsPlugin 这个插件正好可以解决这个问题。这么一来 cssUrls 就可以不用指定了,直接把 Webpack 导出的真实 CSS 名放到 cssUrls 里了。

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({
      // The filename here must match the filename for the MiniCssExtractPlugin
      filename: "[name].css",
    }),
  ],
};
复制代码


子应用 CSS 样式隔离


虽然 single-spa-css 解决了子应用的 CSS 引入和移除问题,但是又带来了另一个问题:怎么保证各个子应用的样式不互相干扰呢?官方给出的建议是:


第一种方法:使用 Scoped CSS,也即在子应用的 CSS 选择器上加前缀就好了嘛,像这样:

.app1__settings-67f89dd87sf89ds {
  color: blue;
}
复制代码


要是嫌麻烦,可以在 Webpack 使用 PostCSS Prefix Selector 给样式自动加前缀:

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

另一种方法是在加载子应用的函数里,将子应用挂载到 Shadow DOM 上,可以实现完美的样式隔离。Shadow DOM 是什么,怎么玩可见 MDN这里


公共 CSS 样式怎么处理


上面说的都是子应用自己的 CSS 样式,那如果子应用之间要共享 CSS 怎么办呢?比如有两个子应用都用了 antd,那都要 import 两次 antd.min.css 了。


这个问题和上面提到的处理“公共依赖”的问题是差不多的。官方给出两个建议:

  1. 将公共的 CSS 放到 importmap 里,也可以理解为在 index.html 里直接加个 link 获取 antd 的 CSS 完事
  2. 将所有的公共的 UI 库都 import 到 utility 里,将 antd 所有内容都 export,再把 utility 包放到 importmap 里,然后 import { Button } from '@your-org-name/utility'; 去引入里面的组件


其实上面两个方法都大同小异,思路都是在主应用一波引入,只是一个统一引入CSS,另一个统一引入 UI 库。


子应用的 JS 隔离


我们来想想应用的 JS 隔离本质是什么,本质其实就是在 B 子应用里使用 window 全局对象里的变量时,不要被 A 子应用给污染了。


一个简单的解决思路就是:在 mount A 子应用时,正常添加全局变量,比如 jQuery 的 $, lodash 的 _。在 unmount A 子应用时,用一个对象记录之前给 window 添加的全局变量,并把 A 应用里添加 window 的变量都删掉。下一次再 mount A 应用时,把记录的全局变量重新加回来就好了。


single-spa 再次站出来:这个不用你自己手动记录 window 的变更了。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,
]
复制代码

但是,这个库的局限性在于:每个 url 只能加一个子 app,如果多个子 app 之间还是会访问同一个 window 对象,也因此会互相干扰,并不能做到完美的 JS 沙箱。

比如:一个页面里,导航栏用 3.0 的 jQuery,而页面主体用 5.0 的 jQuery,那就会有冲突了。


所以这个库的场景也仅限于:首页用 3.0 的 jQuery,订单详情页使用 5.0 的 jQuery 这样的场景。


子应用的分类


上面我们说到了,当 url 匹配 activeWhen 参数时,就会执行对应子应用的生命周期。那这样就相当于子应用和 url 绑定在了一起了。


我们再来看 single-spa-leaked-globals,single-spa-css 这些库,虽然它们也导出了生命周期,但这些生命周期与页面渲染、url 变化没有多大关系。


它们与普通的 application 唯一不同的地方就是:普通 application 的生命周期是通过 single-spa 来自动调度的,而这些库是要通过手动调度的。只不过我们一般选择在子应用里的生命周期里手动调用它们而已。


这种与 url 无关的 “app” 在微前端也有着非常重要的作用,一般是在子应用的生命周期里提供一些功能,像 single-spa-css 就是在 mount 时添加 <link/> 标签。single-spa 将这样的 “类子 app” 称为 Parcel。


同时,single-spa 还分出另一个类:Utility Modules。很多子应用都用 antd, dayjs, axios 的,那么就可以搞一个 utility 集合这些公共库,然后统一做 export,然后在 importmap 里统一导入。子应用就可以不需要在自己的 package.json 里添加 antd, dayjs, axios 的依赖了。


总结一下,single-spa 将微前端分为三大类:

分类 功能 导出 是否与 url 有关
Application 子应用 bootstrap, mount, unmount
Parcel 功能组件,比如子应用的生命周期打一些补丁 bootstrap, mount, unmount, update
Utility Module 公共资源 所有公共资源


create-single-spa


上面介绍了一堆的与子应用相关的库,如果自己要从 0 开始慢慢地配置子应用就比较麻烦。所以,single-spa 说:不麻烦,有脚手架工具,一行命令生成子应用,都给您配好了。

npm install --global create-single-spa
# 或者
yarn global add create-single-spa
复制代码


然后

create-single-spa
复制代码

注意!这里的 create-single-spa 指的是创建子应用!


总结


以上就是 singles-spa 文档里的所有内容了(除了 SSR 和 Dev Tools,前者用的不多,后者自己看一下就会了,不多废话)。由于本文是通过发现问题到解决问题来讲述文档内容的,所以从头看到尾还是有点乱,这里就做一下总结:


微前端概念

特点:


  • 技术栈无关
  • 独立开发、独立部署
  • 增量升级
  • 独立运行时


single-spa

只做两件事:


  • 提供生命周期概念,并负责调度子应用的生命周期
  • 挟持 url 变化事件和函数,url 变化时匹配对应子应用,并执行生命周期流程


三大分类:

  • Application:子应用,和 url 强相关,交由 single-spa 调用生命周期
  • Parcel:组件,和 url 无关,手动调用生命周期
  • Utility Module:统一将公共资源导出的模块


“重要”概念

  • Root Config:指主应用的 index.html + main.js。HTML 负责声明资源路径,JS 负责注册子应用和启动主应用
  • Application:要暴露 bootstrap, mount, umount 三个生命周期,一般在 mount 开始渲染子 SPA 应用
  • Parcel:也要暴露 bootstrap, mount, unmount 三个生命周期,可以再暴露 update 生命周期。Parcel 可大到一个 Application,也可以小到一个功能组件。与 Application 不同的是 Parcel 需要开发都手动调用生命周期


SystemJS

可以在浏览器使用 ES6 的 import/export 语法,通过 importmap 指定依赖库的地址。

和 single-spa 没有关系,只是 in-browser import/export 和 single-spa 倡导的 in-browser run time 相符合,所以 single-spa 将其作为主要的导入导出工具。

用 Webpack 动态引入可不可以,可以,甚至可能比 SystemJS 好用,并无好坏之分。


single-spa-layout

和 Vue Router 差不多,主要功能是可以在 index.html 指定在哪里渲染哪个子应用。


single-spa-react, single-spa-xxx....

给子应用快速生成 bootstrap, mount, unmount 的生命周期函数的工具库。


single-spa-css

隔离前后两个子应用的 CSS 样式。

在子应用 mount 时添加子应用的 CSS,在 unmount 时删除子应用的 CSS。子应用使用  Webpack 导出 CSS 文件时,要配合 ExposeRuntimeCssAssetsPlugin 插件来获取最终导出的 CSS 文件名。

算实现了一半的 CSS 沙箱。


如果要在多个子应用进行样式隔离,可以有两种方法:

  • Shadow DOM,样式隔离比较好的方法,但是穿透比较麻烦
  • Scoped CSS,在子应用的 CSS 选择器上添加前缀做区分,可以使用 postcss-prefix-selector 这个包来快速添加前缀


single-spa-leaked-globals

在子应用 mount 时给 window 对象恢复/添加一些全局变量,如 jQuery 的 $ 或者 lodash 的 _,在 unmount 时把 window 对象的变量删掉。

实现了“如果主应用一个url只有一个页面”情况下的 JS 沙箱。


公共依赖

有两种方法处理:


  • 造一个  Utility Module 包,在这个包导出所有公共资源内容,并用 SystemJS 的 importmap 在主应用的 index.html 里声明
  • 使用 Webpack 5 Module Federation 特性实现公共依赖的导入

哪个更推荐?都可以。


最后


single-spa 文档就这些了嘛?没错,就这些了。文档好像给了很多“最佳实践”,但真正将所有“最佳实践”结合起来并落地的又没多少。


比如文档说用 Shadow CSS 来做子应用之间的样式隔离,但是 single-spa-leaked-globals 又不让别人在一个 url 上挂载多个子应用。感觉很不靠谱:这里行了,那里又不行了。


再说回 Shadow CSS 来做样式隔离,但是也没有详细说明要具体要怎么做。像这样的例子还有很多:文档往往只告诉了一条路,怎么走还要看开发者自己。这就你给人一种 “把问题只解决一半” 的感觉。


如果真的想用 single-spa 来玩小 Demo 的,用上面提到的小库来搭建微前端是可以的,但是要用到生产环境真的没那么容易。


所以,为了填平 single-spa 遗留下来的坑,阿里基于 single-spa 造出了 qiankun 微前端框架,真正实现了微前端的所有特性,不过这又是另外一个故事了。




相关文章
|
9天前
|
前端开发 JavaScript
轻松上手:基于single-spa构建qiankun微前端项目完整教程
轻松上手:基于single-spa构建qiankun微前端项目完整教程
22 0
|
5月前
|
资源调度 前端开发 JavaScript
第十章(应用场景篇) Single-SPA微前端架构深度解析与实践教程
第十章(应用场景篇) Single-SPA微前端架构深度解析与实践教程
212 0
|
前端开发 JavaScript 持续交付
Single-Spa构建第一个微前端项目
微前端是前端web开发的趋势,微服务允许你将后端分解成更小的部分,受此启发,微前端允许你独立构建、测试和部署前端应用。根据你选择的微前端框架,你甚至可以让多个微前端应用——用React、Angular、Vue或其他工具编写的——在同一个大应用中无扰共存!之前在这里《认识微前端:一种用于前端 Web 开发的微服务》大概介绍了一下。
431 0
Single-Spa构建第一个微前端项目
|
Web App开发 前端开发 JavaScript
【微前端】single-spa 到底是个什么鬼(上)
说起微前端框架,很多人第一反应就是 single-spa。但是再问深入一点:它是干嘛的,它有什么用,可能就回答不出来了。 一方面没多少人研究和使用微前端。可能还没来得及用微前端扩展项目,公司就已经倒闭了。 另一方面是中文博客对微前端的研究少之又少,很多文章只是简单翻译一下官方文档,读几个API,放个官方的 Demo 就完事了。很少有深入研究到底 single-spa 是怎么一回事的。这篇文章将不会聊怎么搭建一个 Demo,而是会从 “Why” 和 “How” 的角度来聊一下官方文档的都讲了哪些内容,相信看完这篇文章就能看懂 官方的 Demo 了。
【微前端】single-spa 到底是个什么鬼(上)
|
Web App开发 JSON JavaScript
一个经常被忽略的 single-spa 微前端实践
大家好,我是海怪。了解过微前端的同学应该对 single-spa 这个框架都不陌生,但是我翻看了中文整个社区,发现太少文章是讲 single-spa Demo 实践的。 还有一些文章讲了,但是都是以晒代码为主,只讲是什么,不讲为什么。这对读者来说并不是一个很好的体验。那今天就跟大家深入分析一下 single-spa 的 React 版 Demo 吧。让读者知其然,也能知其所然。
一个经常被忽略的 single-spa 微前端实践
|
5天前
|
存储 人工智能 前端开发
前端大模型应用笔记(三):Vue3+Antdv+transformers+本地模型实现浏览器端侧增强搜索
本文介绍了一个纯前端实现的增强列表搜索应用,通过使用Transformer模型,实现了更智能的搜索功能,如使用“番茄”可以搜索到“西红柿”。项目基于Vue3和Ant Design Vue,使用了Xenova的bge-base-zh-v1.5模型。文章详细介绍了从环境搭建、数据准备到具体实现的全过程,并展示了实际效果和待改进点。
|
5天前
|
JavaScript 前端开发 程序员
前端学习笔记——node.js
前端学习笔记——node.js
17 0
|
5天前
|
人工智能 自然语言处理 运维
前端大模型应用笔记(一):两个指令反过来说大模型就理解不了啦?或许该让第三者插足啦 -通过引入中间LLM预处理用户输入以提高多任务处理能力
本文探讨了在多任务处理场景下,自然语言指令解析的困境及解决方案。通过增加一个LLM解析层,将复杂的指令拆解为多个明确的步骤,明确操作类型与对象识别,处理任务依赖关系,并将自然语言转化为具体的工具命令,从而提高指令解析的准确性和执行效率。
|
5天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
5天前
|
机器学习/深度学习 弹性计算 自然语言处理
前端大模型应用笔记(二):最新llama3.2小参数版本1B的古董机测试 - 支持128K上下文,表现优异,和移动端更配
llama3.1支持128K上下文,6万字+输入,适用于多种场景。模型能力超出预期,但处理中文时需加中英翻译。测试显示,其英文支持较好,中文则需改进。llama3.2 1B参数量小,适合移动端和资源受限环境,可在阿里云2vCPU和4G ECS上运行。