【微前端】single-spa 到底是个什么鬼
前言
说起微前端框架,很多人第一反应就是 single-spa。但是再问深入一点:它是干嘛的,它有什么用,可能就回答不出来了。
一方面没多少人研究和使用微前端。可能还没来得及用微前端扩展项目,公司就已经倒闭了。
另一方面是中文博客对微前端的研究少之又少,很多文章只是简单翻译一下官方文档,读几个API,放个官方的 Demo 就完事了。很少有深入研究到底 single-spa 是怎么一回事的。
还有一方面是 single-spa 的文档非常难看懂,和 Redux 文档一样喜欢造概念。讲一个东西的时候,总是把别的库拉进来一起讲,把一个简单的东西变得非常复杂。最令人吐槽的一点就是官方的 sample code 都是只言片语,完全拼凑不出来一个 Demo,而 Github 的 Demo 还贼复杂,没解释,光看完都要 clone 好几个 repo。
最后,求人不如求己,刚完源码再刚一下文档。
这篇文章将不会会聊怎么搭建一个 Demo,而是会从 “Why” 和 “How” 的角度来聊一下官方文档的都讲了哪些内容,相信看完这篇文章就能看懂 官方的 Demo 了。
一个需求
让我们从一个最小的需求开始说起。有一天产品经理突然说:我们要做一个 A 页面,我看到隔壁组已经做过这个 A 页面了,你把它放到我们项目里吧,应该不是很难吧?明天上线吧。
此时,产品经理想的是:应该就填一个 URL 就好吧?再不行,复制粘贴也很快吧。而程序员想的却是:又要看屎山了。又要重构了。又要联调了。测试数据有没有啊?等一下,联调的后端是谁啊?
估计这是做大项目时经常遇到的需求了:搬运一个现有的页面。我想大多数人都会选择在自己项目里复制粘贴别人的代码,然后稍微重构一下,再测试环境联调,最后上线。
但是,这样就又多了一份代码了,如果别人的页面改了,那么自己项目又要跟着同步修改,再联调,再上线,非常麻烦。
所以程序员就想能不能我填一个 url,然后这个页面就到项目里来了呢?所以,<iframe/> 就出场了。
iframe 的弊端
iframe 就相当于页面里再开个窗口加载别的页面,但是它有很多弊端:
- 每次进来都要加载,状态不能保留
- DOM 结构不共享。比如子应用里有一个 Modal,显示的时候只能在那一小块地方展示,不能全屏展示
- 无法跟随浏览器前进后退
- 天生的硬隔离,无法与主应用进行资源共享,交流也很困难
而 SPA 正好可以解决上面的问题:
- 切换路由就是切换页面组件,组件的挂载和卸载非常快
- 单页应用肯定共享 DOM
- 前端控制路由,想前就前,想后就后
- React 通信有 Redux,Vue 通信有 Vuex,可与 App 组件进行资源共享,交流很爽
这就给我们一个启发:能不能有这么一个巨型 SPA 框架,把现有的 SPA 当成 Page Component 来组装成一个新的 SPA 呢?这就是微前端的由来。
微前端是什么
微前端应该有如下特点:
- 技术栈无关,主框架不限制接入应用的技术栈,微应用具备完全自主权
- 独立开发、独立部署,微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
- 增量升级,在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
- 独立运行时,每个微应用之间状态隔离,运行时状态不共享
等一下等一下,说了一堆,到底啥是 single-spa 啊。
嘿嘿,single-spa 框架并没有实现上面任何特点,对的,一个都没有,Just Zero。

single-spa 到底是干嘛的
single-spa 仅仅是一个子应用生命周期的调度者。single-spa 为应用定义了 boostrap, load, mount, unmount 四个生命周期回调:

只要写过 SPA 的人都能理解,无非就是生、老、病、死。不过有几个点需要注意一下:
- Register 不是生命周期,指的是调用
registerApplication函数这一步 - Load 是开始加载子应用,怎么加载由开发者自己实现(等会会说到)
- Unload 钩子只能通过调用
unloadApplication函数才会被调用
OK,上面 4 个生命周期的回调顺序是 single-spa 可以控制的,我能理解,那什么时候应该开始这一套生命周期呢?应该是有一个契机来开始整套流程的,或者某几个流程的。
契机就是当 window.location.href 匹配到 url 时,开始走对应子 App 的这一套生命周期嘛。所以,single-spa 还要监听 url 的变化,然后执行子 app 的生命周期流程。
到此,我们就有了 single-spa 的大致框架了,无非就两件事:
- 实现一套生命周期,在 load 时加载子 app,由开发者自己玩,别的生命周期里要干嘛的,还是由开发者造的子应用自己玩
- 监听 url 的变化,url 变化时,会使得某个子 app 变成 active 状态,然后走整套生命周期
画个草图如下:

是不是感觉 single-spa 很鸡贼?虽然 single-spa 说自己是微前端框架,但是一个微前端的特性都没有实现,都是需要开发者在加载自己子 App 的时候实现的,要不就是通过一些第三方工具实现。
注册子应用
有了上面的了解之后,我们再来看 single-spa 里最重要的 API:registerApplication,表示注册一个子应用。使用如下:
singleSpa.registerApplication({name: 'taobao', // 子应用名app: () => System.import('taobao'), // 如何加载你的子应用activeWhen: '/appName', // url 匹配规则,表示啥时候开始走这个子应用的生命周期customProps: { // 自定义 props,从子应用的 bootstrap, mount, unmount 回调可以拿到authToken: 'xc67f6as87f7s9d'}
})singleSpa.start() // 启动主应用
上面注册了一个子应用 'taobao'。我们自己实现了加载子应用的方法,通过 activeWhen 告诉 single-spa 什么时候要挂载子应用,好像就可以上手开撸代码喽。
可以个鬼!请告诉我 System.import 是个什么鬼。哦,是 SystemJS,诶,SystemJS 听说过,它是个啥?为啥要用 SystemJS?凭啥要用 SystemJS?
SystemJS
相信很多人看过一些微前端的博客,它们都会说 single-spa 是基于 SystemJS 的。错!single-spa 和 SystemJS 一点关系都没有!这里先放个主应用和子应用的关系图:

single-spa 的理念是希望主应用可以做到非常非常简单的和轻量,简单到只要一个 index.html + 一个 main.js 就可以完成微前端工程,连 Webpack 都不需要,直接在浏览器里执行 singleSpa.registerApplication 就收工了,这种执行方式也就是 in-browser 执行。
但是,浏览器里执行 JS,别说实现 import xxx from 'https://taobao.com' 了,我要是在浏览器里实现 ES6 的 import/export 都不行啊: import axios from 'axios'。

其实,也不是不行,只需要在 <script> 标签加上 type="module",也是可以实现的,例如:
<script type="module" src="module.js"></script>
<script type="module">// or an inline scriptimport {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": {"vue": "https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js"}}
</script><div id="container">我是:{{name}}</div><script type="module">import Vue from 'vue'new Vue({el: '#container',data: {name: 'Jack'}})
</script>
importmap 的功能就是告诉 'vue' 这个玩意要从 "https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js" 这里来的。不过,importmap 现在只有 Chrome 是支持的。
所以,SystemJS 就将这一块补齐了。当然,除了 importmap,它还有很多的功能,比如获取当前加载的所有模块、当前模块的 URL、可以 import html, import css,import wasm。
等等,这在 Webpack 不也可以做到么?Webpack 还能 import less, import scss 呢?这不比 SystemJS 牛逼?对的,如果不是因为要在浏览器使用 import/export,没人会用 SystemJS。SystemJS 的好处和优势有且仅有一点:那就是在浏览器里使用 ES6 的 import/export。
而正因为 SystemJS 可以在浏览器里可以使用 ES6 的 import/export 并支持动态引入,正好符合 single-spa 所提倡的 in-browser 执行思路,所以 single-spa 文档里才反复出现 SystemJS 的身影,而且 Github Demo 里依然是使用 SystemJS 的 importmap 机制来引入不同模块:
<script type="systemjs-importmap">{"imports": {"@react-mf/root-config": "//localhost:9000/react-mf-root-config.js"}}
</script><script>singleSpa.registerApplication({name: 'taobao', // 子应用名app: () => System.import('@react-mf/root-config'), // 如何加载你的子应用activeWhen: '/appName', // url 匹配规则,表示啥时候开始走这个子应用的生命周期customProps: { // 自定义 props,从子应用的 bootstrap, mount, unmount 回调可以拿到authToken: 'xc67f6as87f7s9d'}})
</script>
公共依赖
SystemJS 另一个好处就是可以通过 importmap 引入公共依赖。
假如,我们有三个子应用,它们都有公共依赖项 antd,那每个子应用打包出来都会有一份 antd 的代码,就显示很冗余。

一个解决方法就是在主应用里,通过 importmap 直接把 antd 代码引入进来,子应用在 Webpack 设置 external 把 antd 打包时排除掉。子应用打包就不会把 antd 打包进去了,体积也变小了。

有人会说了:我用 CDN 引入不行嘛?不行啊,因为子应用的代码都是 import {Button} from 'antd' 的,浏览器要怎么直接识别 ES6 的 import/export 呢?那还不得 SystemJS 嘛。
难道 Webpack 就没有办法可以实现 importmap 的效果了么?Webpack 5 提出的 Module Federation 模块联邦就可以很好地做的 importmap 的效果。这是 Webpack 5 的新特性,使用的效果和 importmap 差不多。关于模块联邦是个啥,可以参考 这篇文章。
至于用 importmap 还是 Webpack 的 Module Federation,singles-spa 是推荐使用 importmap 的,但是,文档也没有反对使用 Webpack 的 Module Federation 的理由。能用就OK。
SystemJS vs Webpack ES
有人可能会想:都 1202 年了,怎么还要在浏览器环境写 JS 呢?不上个 Webpack 都不好意思说自己是前端开发了。
没错,Webpack 是非常强大的,而且可以利用 Webpack 很多能力,让主应用变得更加灵活。比如,写 less,scss,Webpack 的 prefetch 等等等等。然后在注册子应用时,完全可以利用 Webpack 的动态引入:
singleSpa.registerApplication({name: 'taobao', // 子应用名app: () => import('taobao'), // 如何加载你的子应用activeWhen: '/appName', // url 匹配规则,表示啥时候开始走这个子应用的生命周期customProps: { // 自定义 props,从子应用的 bootstrap, mount, unmount 回调可以拿到authToken: 'xc67f6as87f7s9d'}
})
那为什么 single-spa 还要推荐 SystemJS 呢?个人猜测是因为 single-spa 希望主应用应该就一个空壳子,只需要管内容要放在哪个地方,所有的功能、交互都应该交由 index.html 来统一管理。
当然,这仅仅是一种理念,可以完全不遵循它。像我个人还是喜欢用 Webpack 多一点,SystemJS 还是有点多余,而且觉得有点奥特曼了。不过,为了跟着文档的节奏来,这里假设就用 SystemJS 来实现主应用。
Root Config
由于 single-spa 非常强调 in-browser 的方式来实现主应用,所以 index.html 就充当了静态资源、子应用的路径声明的角色。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Polyglot Microfrontends</title><meta name="importmap-type" content="systemjs-importmap" /><script type="systemjs-importmap" src="https://storage.googleapis.com/polyglot.microfrontends.app/importmap.json"></script><% if (isLocal) { %><script type="systemjs-importmap">{"imports": {"@polyglot-mf/root-config": "//localhost:9000/polyglot-mf-root-config.js"}}</script><% } %><script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script><script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.min.js"></script><script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.min.js"></script>
</head>
<body><script>System.import('@polyglot-mf/root-config');System.import('@polyglot-mf/styleguide');</script><import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>
而 main.js 则实现子应用注册、主应用启动。
import { registerApplication, start } from "single-spa";registerApplication({name: "@polyglot-mf/navbar",app: () => System.import("@polyglot-mf/navbar"),activeWhen: "/",
});registerApplication({name: "@polyglot-mf/clients",app: () => System.import("@polyglot-mf/clients"),activeWhen: "/clients",
});registerApplication({name: "@polyglot-mf/account-settings",app: () => loadWithoutAmd("@polyglot-mf/account-settings"),activeWhen: "/settings",
});start();// A lot of angularjs libs are compiled to UMD, and if you don't process them with webpack
// the UMD calls to window.define() can be problematic.
function loadWithoutAmd(name) {return Promise.resolve().then(() => {let globalDefine = window.define;delete window.define;return System.import(name).then((module) => {window.define = globalDefine;return module;});});
}
像这样的资源声明 + 主子应用加载的组件,single-spa 称之为 Root Config。 它不是什么新概念,就只有两个东西: 一个主应用的 index.html 一个执行 registerApplication 函数的 JS 文件
single-spa-layout
虽然一个 index.html 是完美的轻量微前端主应用,但是就算再压缩主应用的交互,那总得告诉子应用放置的位置吧,那不还得 DOM API 一把梭?一样麻烦?
为了解决这个问题,single-spa 说:没事,我帮你搞,然后造了 single-spa-layout。具体使用请看代码:
<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>
不能说和 Vue Router 很像,只能说一模一样吧。当然上面这么写很直观,但是浏览器并不认识这些元素,所以 single-spa-layout 把识别这些元素的逻辑都封装成了函数,并暴露给开发者,开发者只要调用一下就能识别出 appName 等信息了:
import { registerApplication, start } from 'single-spa';
import {constructApplications,constructRoutes,constructLayoutEngine,
} from 'single-spa-layout';// 获取 routes
const routes = constructRoutes(document.querySelector('#single-spa-layout'));// 获取所有的子应用
const applications = constructApplications({routes,loadApp({ name }) {return System.import(name); // SystemJS 引入入口 JS},
});// 生成 layoutEngine
const layoutEngine = constructLayoutEngine({ routes, applications });// 批量注册子应用
applications.forEach(registerApplication);// 启动主应用
start();
没什么好说的,constrcutRoutes, constructApplication 和 constructLayoutEngine 本质上就是识别 single-spa-layout 定义的元素标签,然后获取里面的属性,再通过 registerApplication 函数一个个注册就完事了。
改造子应用
上面说的都是主应用的事情,现在我们来关心一下子应用。
子应用最关键的一步就是导出 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 搞啊,别的框架也要给它们整上一个,一碗水端平,所以有这了这些牛鬼蛇神:

不禁感慨:这些小轮子是真能造啊。
导入子应用的 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,默认为 falsewebpackExtractedCss: false,// 是否 unmount 后被移除,默认为 trueshouldUnmount: 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
]
这里要注意一下,上面的 https://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 MiniCssExtractPluginfilename: "[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 了。
这个问题和上面提到的处理“公共依赖”的问题是差不多的。官方给出两个建议:
- 将公共的 CSS 放到 importmap 里,也可以理解为在 index.html 里直接加个 link 获取 antd 的 CSS 完事
- 将所有的公共的 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 微前端框架,真正实现了微前端的所有特性,不过这又是另外一个故事了。
相关文章:
【微前端】single-spa 到底是个什么鬼
前言 说起微前端框架,很多人第一反应就是 single-spa。但是再问深入一点:它是干嘛的,它有什么用,可能就回答不出来了。 一方面没多少人研究和使用微前端。可能还没来得及用微前端扩展项目,公司就已经倒闭了。 另一方…...
log4j2同步日志引发的性能问题 | 京东物流技术团队
1 问题回顾 1.1 问题描述 在项目的性能测试中,相关的接口的随着并发数增加,接口的响应时间变长,接口吞吐不再增长,应用的CPU使用率较高。 1.2 分析思路 谁导致的CPU较高,阻塞接口TPS的增长?接口的响应时…...
vs studio Ctrl+D 快捷键失效(无法复制行)
打开 调试/选项/环境/键盘,然后设置如下 快去试试吧...
数据结构题型18-哈夫曼树和哈夫曼编码
文章目录 1 哈夫曼树定义2 哈夫曼树构造3 哈夫曼编码4 并查集 1 哈夫曼树定义 2 哈夫曼树构造 3 哈夫曼编码 4 并查集 暂不做补充。...
【广州华锐互动】VR模拟电力生产事故,切身感受危险发生
随着科技的不断发展,虚拟现实(VR)技术已经在各个领域中得到了广泛的应用。其中,VR技术在电力安全事故还原中的应用,不仅可以帮助我们更好地理解和预防事故的发生,还可以为事故调查提供更为准确和直观的证据…...
kafka安装和使用的入门教程
这篇文章简单介绍如何在ubuntu上安装kafka,并使用kafka完成消息的发送和接收。 一、安装kafka 访问kafka官网Apache Kafka,然后点击快速开始 紧接着,点击Download 最后点击下载链接下载安装包 如果下载缓慢,博主已经把安装包上传…...
享搭低代码平台:加速企业应用开发,轻松搭建表单和报表
在当今快节奏的商业环境中,企业需要快速响应市场需求并提供高效的解决方案。然而,传统的应用开发过程繁琐、耗时,并且需要专业的编程技能。为了解决这些问题,享搭低代码平台应运而生。本文将详细介绍享搭低代码平台的特点和优势&a…...
华为云应用中间件DCS系列—Redis实现(社交APP)实时评论
云服务、API、SDK,调试,查看,我都行 阅读短文您可以学习到:应用中间件系列之Redis实现(社交APP)实时评论 1 什么是DEVKIT 华为云开发者插件(Huawei Cloud Toolkit)࿰…...
01-spring源码概述
文章目录 1. Spring两大主要功能2. Bean的生命周期(部分生命周期,不包括销毁)2.1 两个重要接口及Aware接口2.2 创建对象的过程2.3 Bean的scope作用域2.4 Bean的类型2.5 获得反射对象的三种方式 3. 涉及的接口汇总4. 涉及设计模式 1. Spring两…...
datax 同步本地csv到mysql
csv 文件 /root/tempdata/us_population.csv NY,New York,8143197 CA,Los Angeles,3844829 IL,Chicago,2842518 TX,Houston,2016582 PA,Philadelphia,1463281 AZ,Phoenix,1461575 TX,San Antonio,1256509 CA,San Diego,1255540 TX,Dallas,1213825 CA,San Jose,912332csv2mysq…...
国内原汁原味的免费sd训练工具--哩布哩布AI
作者简介:一名云计算网络运维人员、每天分享网络与运维的技术与干货。 公众号:网络豆云计算学堂 座右铭:低头赶路,敬事如仪 个人主页: 网络豆的主页 目录 写在前面 一.体验与操作 1.注册 2.为何可…...
组合数(1) 用Vector实现获取所有组合数列表的QT实现
1.工程文件 QT coreCONFIG c17 cmdline# You can make your code fail to compile if it uses deprecated APIs. # In order to do so, uncomment the following line. #DEFINES QT_DISABLE_DEPRECATED_BEFORE0x060000 # disables all the APIs deprecated before Qt 6.…...
Ultra-Fast-Lane-Detection-v2 裁剪数据增强
目录 标注拆分为独立文件加载并数据增强 Ultra-Fast-Lane-Detection-v2 裁剪数据增强 main方法是调用示例...
从零开始学习调用百度地图网页API:三、鼠标点击绘图功能
目录 代码功能问题注意addEventListenerplot_line 代码 <!DOCTYPE html> <html> <head><meta http-equiv"Content-Type" content"text/html; charsetutf-8" /><meta name"viewport" content"initial-scale1.0,…...
强化学习案例复现(1)--- MountainCar基于Q-learning
1 搭建环境 1.1 gym自带 import gym# Create environment env gym.make("MountainCar-v0")eposides 10 for eq in range(eposides):obs env.reset()done Falserewards 0while not done:action env.action_space.sample()obs, reward, done, action, info env.…...
BUUCTF学习(6): 命令执行ip
1、介绍 2、hackbar安装 BUUCTF学习(四): 文件包含tips-CSDN博客 ?ip127.0.0.1;ag;cat$IFS$9fla$a.php 空格过滤 $IFS$9 检查源代码 结束...
javaweb:mybatis:mapper(sql映射+代理开发+配置文件之设置别名、多环境配置、顺序+注解开发)
1.0版本 sql映射文件实现 流程 首先程序进入启动类MyBatisDemo.java中,读取配置文件mybatis-config.xml 再由mybatis-config的mappers属性 <mappers><mapper resource"UserMapper.xml"></mapper></mappers>找到sql映射文件Use…...
JavaScript基础知识——练习巩固(2)
写一个程序,要求如下 需求1:让用户输入五个有效年龄(0-100之间),放入数组中 必须输入五个有效年龄年龄,如果是无效年龄,则不能放入数组中 需求2:打印出所有成年人的年龄 (数组筛选)…...
FutureTask的测试使用和方法执行分析
FutureTask类图如下 java.util.concurrent.FutureTask#run run方法执行逻辑如下 public void run() {if (state ! NEW ||!RUNNER.compareAndSet(this, null, Thread.currentThread()))return;try {Callable<V> c callable;if (c ! null && state NEW) {V res…...
SpringMVC的请求处理
目录 请求映射路径的配置 请求数据的接收 接收Restful风格的数据 什么是Restful风格? 接收上传文件 获取headers头信息和cookie信息 JavaWeb常用对象获取 请求静态资源 注解驱动标签 请求映射路径的配置 请求映射路径的配置主要是通过RequestMapping注解实现…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
RocketMQ延迟消息机制
两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数,对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后…...
[ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?
论文网址:pdf 英文是纯手打的!论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误,若有发现欢迎评论指正!文章偏向于笔记,谨慎食用 目录 1. 心得 2. 论文逐段精读 2.1. Abstract 2…...
工程地质软件市场:发展现状、趋势与策略建议
一、引言 在工程建设领域,准确把握地质条件是确保项目顺利推进和安全运营的关键。工程地质软件作为处理、分析、模拟和展示工程地质数据的重要工具,正发挥着日益重要的作用。它凭借强大的数据处理能力、三维建模功能、空间分析工具和可视化展示手段&…...
《通信之道——从微积分到 5G》读书总结
第1章 绪 论 1.1 这是一本什么样的书 通信技术,说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号(调制) 把信息从信号中抽取出来&am…...
零基础设计模式——行为型模式 - 责任链模式
第四部分:行为型模式 - 责任链模式 (Chain of Responsibility Pattern) 欢迎来到行为型模式的学习!行为型模式关注对象之间的职责分配、算法封装和对象间的交互。我们将学习的第一个行为型模式是责任链模式。 核心思想:使多个对象都有机会处…...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...
ios苹果系统,js 滑动屏幕、锚定无效
现象:window.addEventListener监听touch无效,划不动屏幕,但是代码逻辑都有执行到。 scrollIntoView也无效。 原因:这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作,从而会影响…...
搭建DNS域名解析服务器(正向解析资源文件)
正向解析资源文件 1)准备工作 服务端及客户端都关闭安全软件 [rootlocalhost ~]# systemctl stop firewalld [rootlocalhost ~]# setenforce 0 2)服务端安装软件:bind 1.配置yum源 [rootlocalhost ~]# cat /etc/yum.repos.d/base.repo [Base…...
MacOS下Homebrew国内镜像加速指南(2025最新国内镜像加速)
macos brew国内镜像加速方法 brew install 加速formula.jws.json下载慢加速 🍺 最新版brew安装慢到怀疑人生?别怕,教你轻松起飞! 最近Homebrew更新至最新版,每次执行 brew 命令时都会自动从官方地址 https://formulae.…...
