单实例微前端设计思想
- 拿到子应用构建后的资源清单,一般项目中都会生成一个
asset-manifest.json
文件,用来记录各个子应用的入口资源url
信息,以便在切换不同子应用时使用模块加载器去远程加载。因为每次子应用更新后入口资源的hash
通常会变化,所以需要服务端定时去更新该配置表,以便框架能及时加载子应用最新的资源; - 同样,子应用之间的一些公共依赖通过配置文件记录;
- 主应用监听路由按需加载子应用模块;
- 主应用请求获取到
JS
模块后,可以通过eval、new Function、SystemJS
方式执行JS
模块,拿到子应用生命周期钩子,挂载到DOM
。这一过程为了避免全局变量的命名冲突,应该做到JS
沙箱隔离; - 主应用请求获取到
CSS
模块后,拼接CSS
字符串,使用style
标签动态注入到页面中,为了避免子应用之间样式冲突,在unmount
阶段将注入的style
标签删除;
single-spa
Single-spa
是一个将多个单页面应用聚合为一个整体应用的 JavaScript 微前端框架。 使用 single-spa
进行前端架构设计可以带来很多好处,例如:
- 在同一页面上使用多个前端框架 而不用刷新页面 (React, AngularJS, Angular, Ember, 你正在使用的框架)
- 独立部署每一个单页面应用
- 新功能使用新框架,旧的单页应用不用重写可以共存
- 改善初始加载时间,迟加载代码
single-spa
借鉴了组件生命周期的思想,它为应用设置了针对路由的生命周期。当应用匹配路由处于激活状态时,应用会把自身的内容挂载到页面上;反之则卸载。
该框架核心提供两个功能,我这里称之为加载器和包装器。加载器用来调度子应用,决定何时展示哪一个子应用,可以把它理解为电源。包装器可以把子应用进行包装,给子应用提供生命周期钩子,并将其导出,使得加载器可以使用它们,它相当于电源适配器。
single-spa-react
是针对react
项目的包装器
import React from 'react'
import ReactDOM from 'react-dom'
import singleSpaReact from 'single-spa-react';
const domElementGetter = () => {
let el = document.getElementById("micro-content");
if (!el) {
el = document.createElement('div');
el.id = 'micro-content';
document.body.appendChild(el);
}
return el;
}
export const singleSpaPacker = (rootComponent: React.FC<any>) => {
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent,
domElementGetter,
})
const bootstrap = (props: any) => {
return reactLifecycles.bootstrap(props);
}
const mount = (props: any) => {
return reactLifecycles.mount(props);
}
const unmount = (props: any) => {
return reactLifecycles.unmount(props);
}
return { bootstrap, mount, unmount };
}
import React from 'react';
import { HashRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
import routes from './config/routes';
import { Provider } from 'mobx-react';
import stores from './stores';
const App = () => {
return (
<HashRouter>
<Provider {...stores}>{renderRoutes(routes)}</Provider>
</HashRouter>
)
}
export default App;
import ReactDOM from 'react-dom';
import React from 'react';
import { singleSpaPacker } from './utils';
import App from './App';
if (process.env.NODE_ENV === 'development') {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
}
const { bootstrap, mount, unmount } = singleSpaPacker(App);
export { bootstrap, mount, unmount };
registerApplication
是single-spa
提供的加载器,包含四个参数:
appName
: 注册的应用名称;applicationOrLoadingFn
:应用入口文件(必须是一个函数,返回一个函数或者一个promise
);activityFn
:控制应用是否激活的函数(必须是一个纯函数,接受window.location
作为参数,返回一个boolean
);customProps
:在包装器生命周期函数中传递给子应用的props
(应该是一个对象,可选)。
import { LifeCycles, registerApplication, start } from 'single-spa';
/** 匹配路由 */
const pathPrefix = (prefix: string) => {
return (location: Location) => location.hash.startsWith(prefix);
}
/** 按需加载子应用 */
const applicationOrLoadingFn = async (url: string, appName: string): Promise<LifeCycles> => {
// coding
}
registerApplication(appName, ({ name }) => applicationOrLoadingFn(url, name), pathPrefix(hash));
start();
子应用资源清单
上面single-spa
方案流程中提到子应用构建生成的资源清单应该在部署时写入资源配置表中,例子中我准备直接请求子应用的资源清单。
构建配置 stats-webpack-plugin
插件,生成一个资源清单manifest.json
文件,create-react-app
搭建的react
项目中webpack
默认使用webpack-manifest-plugin
生成资源清单。
子应用模块加载
动态加载子应用主要是让子应用自己将内容渲染到某个 DOM
节点,因而动态加载的目的主要是执行子应用的代码,另外是需要拿到子工程声明的一些生命周期钩子。
由于子应用通常又有集成部署、独立部署两种模式同时支持的需求,使得我们只能选择 umd
这种兼容性的模块格式打包我们的子应用。
// src/config-overrides.js
const pkg = require('./package.json');
const path = require('path')
module.exports = function override(config, env) {
config.entry = path.resolve(__dirname, 'src/index.tsx');
config.output.library = pkg.name;
config.output.libraryTarget = 'window';
delete config.optimization;
return config;
};
我在使用create-react-app
脚手架搭建React微前端项目时,选择umd
模块格式,但是执行子应用脚本时生命周期钩子没有赋值给window[pkg.name]
,因为主应用全局环境下存在define
函数属性,导致webpackUniversalModuleDefinition
在做环境判断时采用的是amd
模块格式,而主应用中并没有引入RequestJS
,因此无法拿到钩子
因此,我将libraryTarget
设置成 'window'
,但是window[pkg.name]
拿到的值却是数值1,查看main.chunk.js
发现window[pkg.name]
得到的是数组push
后的返回值
后面我将删除掉webpack
中optimization
配置后,window[pkg.name]
能拿到钩子
JS加载
dynamic script
/** 获取构建后生成的资源清单asset-manifest.json */
const fetchAssets = (url: string) => {
return new Promise<string[]>(async (resolve) => {
const mainfest = 'asset-manifest.json';
const { data } = await axios.get(`${url}/${mainfest}?version=${Date.now()}`);
const { files } = data;
resolve(Object.values(files as string[]).filter(s => /\^*.(js|css)$/.test(s)));
});
}
/** dynamic script */
const insertScript = (src: string) => {
return new Promise<void>((resolve, reject) => {
const script = document.createElement('script');
script.charset = 'utf-8';
script.async = true;
script.crossOrigin = 'anonymous';
script.onload = () => {
document.head.removeChild(script);
resolve();
};
script.onerror = reject;
script.src = src;
document.head.appendChild(script);
})
}
/** 按需加载子应用 */
const applicationOrLoadingFn = async (url: string, appName: string): Promise<LifeCycles> => {
/** 获取mainfest */
const files = await fetchAssets(url);
const JSList = files.filter(s => /\^*.js$/.test(s));
/** 动态执行JS模块 */
await Promise.all(JSList.map((file: string) => insertScript(`${url}${file}`)))
/** 返回子应用生命周期钩子 */
return (window as any)[appName];
}
启动项目访问子应用时,发现window[appName]
并没有拿到钩子,但是动态脚本全部写入到页面中,请求也发起了
我很怀疑上面的现场,为了验证这个问题,我写了一个html
,直接注入script
,然后使用http-server -p 8888 --cors
启动
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="http://localhost:8234/static/js/main.00f2ddab.js?version=1626774357341"></script>
<script src="http://localhost:8234/static/js/1.5ba77f0d.chunk.js?version=1626774357341"></script>
<script src="http://localhost:8234/static/js/2.023d622e.chunk.js?version=1626774357352"></script>
<script src="http://localhost:8234/static/js/3.92fd6021.chunk.js?version=1626774357374"></script>
</body>
</html>
发现例子通过window[appName]
可以拿到声明周期钩子
那么为什么动态注入script
没有执行呢?因为React
从设计层面上就具备了很好的防御 XSS
的能力,所以虽然你在"检查元素"时看到了<script>
,但HTML里的代码实际是\<script\>
;,注入行为没成功,代码没执行
React
官方中提到了React DOM
在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串,因此恶意代码无法成功注入,从而有效地防止了XSS
攻击。详情可以参考浅谈 React 中的 XSS 攻击
eval/new Function
new Function()
方案:用 fetch
或者其他请求库直接拿到子应用内容,然后用 new Function
将子模块作为 function
的函数体来执行,将输出挂载到 window
,跟 eval
是类似的,但使用起来会清晰一些。
eval()
方案:用 fetch
或者其他请求库直接拿到子应用内容,直接 eval
执行,将输出挂载到 window
。
/** 根据路由加载子应用 */
const applicationOrLoadingFn = async (url: string, appName: string): Promise<LifeCycles> => {
/** 获取mainfest */
const files = await fetchAssets(url);
const JSList = files.filter(s => /\^*.js$/.test(s));
await Promise.all(JSList.map((file: string) => axios.get(`${url}${file}?version=${Date.now()}`))).then(res => {
res.forEach(r => {
// new Function(r.data)()
// eval(r.data)会报错
window.eval(r.data) // 或者eval.call(null, r.data)
})
})
return (window as any)[appName];
}
注意:使用eval
执行子应用模块时需要指定eval
执行函数体内的this
指针,不然控制台会报Cannot read property 'webpackJsonpicec-cloud-inquiry-mall-react' of undefined
因为eval
在松散模式下运行代码会在当前的作用域中创建局部变量,不会把webpackJsonpicec-cloud-inquiry-mall-react
挂载到全局环境中,因此拿不到,所以需要在全局环境中调用eval
。参考以 eval() 和 new Function() 执行JavaScript代码
window.eval(r.data) // 或者eval.call(null, r.data)
SystemJS
SystemJS
是一个插件化的、通用的模块加载器,它能在浏览器或者 NodeJS
上动态加载模块,并且支持 CommonJS、AMD
、全局模块对象和 ES6
模块,也是Angular2
官推的加载器。通过使用插件,它不仅可以加载 JavaScript
,还可以加载 CoffeeScript
和 TypeScript
。虽然Chrome
浏览器其实已经支持js
代码中的import、export
有一段时间了,但是其他浏览器还不支持,要想使用import
动态加载只能引入polyfill
。
通常它支持创建的插件种类有:
CSS
System.import('my/file.css!')
Image
System.import('some/image.png!image')
JSON
System.import('some/data.json!').then(function(json){})
Markdown
System.import('app/some/project/README.md!').then(function(html) {})
Text
System.import('some/text.txt!text').then(function(text) {})
WebFont
System.import('google Port Lligat Slab, Droid Sans !font')
SystemJS
原理核心思想:动态创建script
标签执行模块,将执行结果模块导出
SystemJS
是在页面完成加载后解析importmap
,然后加载执行模块,但是我的importmap
并不是在页面加载前注入的,而是在加载完后动态注入的,因此不会被解析执行。那怎么办呢?好在SystemJS@6.4.0
之后开始支持动态注入importmap
,需要在主应用html
中引入dynamic-import-maps.js
// dynamic-import-maps.js
(function(){/*
* Support for live DOM updating import maps
*/
new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
var mutation = mutations[i];
if (mutation.type === 'childList')
for (var j = 0; j < mutation.addedNodes.length; j++) {
var addedNode = mutation.addedNodes[j];
if (addedNode.tagName === 'SCRIPT' && addedNode.type === 'systemjs-importmap' && !addedNode.sp) {
System.prepareImport(true);
break;
}
}
}
}).observe(document, { childList: true, subtree: true });}());
因此,我们可以在加载函数中动态注入importmap
,然后执行返回子应用声明周期钩子
interface IImports {
imports: {
[key: string]: string;
}
}
/** 动态注入importMap */
const insertNewImportMap = (newMapJSON: IImports) => {
return new Promise<void>(resolve => {
const newScript = document.createElement('script');
newScript.type = 'systemjs-importmap';
newScript.text = JSON.stringify(newMapJSON);
/** 删除上次注入的importmap */
[].filter.call(document.querySelectorAll('script'), (script) => script.type === 'systemjs-importmap').forEach(s => document.head.removeChild(s));
document.head.appendChild(newScript);
resolve();
})
}
/** 根据路由加载子应用 */
const applicationOrLoadingFn = async (url: string, appName: string): Promise<LifeCycles> => {
/** 获取mainfest */
const files = await fetchAssets(url);
/** 动态importmap */
const maps: IImports = { imports: {} } as IImports;
const JSList = files.filter(s => /\^*.js$/.test(s));
JSList.forEach(async (file: string) => {
maps.imports[`@${appName}${file}`] = `${url}${file}?version=${Date.now()}`
});
await insertNewImportMap(maps);
await Promise.all(Object.keys(maps.imports).map((key: string) => (window as any).System.import(key)))
return (window as any)[appName];
}
疑问:SystemJS
也是动态创建script
加载执行模块,跟直接创建动态script
加载执行模块有什么区别吗?为什么SystemJS
动态script
加载执行模块时不会遇到XSS
防御呢?
CSS加载
dynamic style
获取资源清单中的所有CSS
,拼接成字符串,使用<style>
注入页面中
/** 根据路由加载子应用 */
const applicationOrLoadingFn = async (url: string, appName: string): Promise<LifeCycles> => {
/** 获取mainfest */
const files = await fetchAssets(url);
/** 动态importmap */
const maps: IImports = { imports: {} } as IImports;
const JSList = files.filter(s => /\^*.js$/.test(s));
const styleText: string = '';
const CSSList = files.filter(s => /\^*.css$/.test(s));
/** 注入importmap */
JSList.forEach(async (file: string) => {
maps.imports[`@${appName}${file}`] = `${url}${file}?version=${Date.now()}`
});
await insertNewImportMap(maps);
/** 注入style */
await Promise.all(CSSList.map((file: string) => axios.get(`${url}${file}?version=${Date.now()}`))).then(res => {
styleText = res.map(r => r.data).join('\n');
const style = document.createElement('style');
style.id = appName;
style.appendChild(document.createTextNode(styleText));
document.head.appendChild(style);
})
await Promise.all(Object.keys(maps.imports).map((key: string) => (window as any).System.import(key)))
return (window as any)[appName];
}
沙箱隔离
JS沙箱
针对 JS
隔离的问题,我们独创了一个运行时的 JS
沙箱。简单画了个架构图:
即在应用的 bootstrap
及 mount
两个生命周期开始之前分别给全局状态打下快照,然后当应用切出/卸载时,将状态回滚至 bootstrap
开始之前的阶段,确保应用对全局状态的污染全部清零。而当应用二次进入时则再恢复至 mount
前的状态的,从而确保应用在 remount
时拥有跟第一次 mount
时一致的全局上下文。
class Sandbox {
constructor(name) {
this.name = name;
this.modifyMap = {}; // 存放修改的属性
this.windowSnapshot = {};
}
active() {
// 缓存active状态的沙箱
this.windowSnapshot = {};
for (const item in window) {
this.windowSnapshot[item] = window[item];
}
Object.keys(this.modifyMap).forEach(p => {
window[p] = this.modifyMap[p];
})
}
inactive() {
for (const item in window) {
if (this.windowSnapshot[item] !== window[item]) {
// 记录变更
this.modifyMap[item] = window[item];
// 还原window
window[item] = this.windowSnapshot[item];
}
}
}
}
/** single-spa应用包装器 */
export const singleSpaPacker = (rootComponent: React.FC<any>) => {
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent,
domElementGetter,
})
const sandbox = new Sandbox('');
const bootstrap = (props: any) => {
diffSandbox.active();
return reactLifecycles.bootstrap(props);
}
const mount = (props: any) => {
return reactLifecycles.mount(props);
}
const unmount = (props: any) => {
diffSandbox.inactive();
return reactLifecycles.unmount(props);
}
return { bootstrap, mount, unmount };
}
CSS隔离
在使用create-react-app
创建的项目中,如果照下面方式引入样式是不会进行css module
和postcss
处理
import 'index.css';
// 或者import 'index.scss';
const Demo = () => (
<span className='text'/>
)
// 构建后注入页面的样式
<style>
.text {}
</style>
一般我们在项目中使用第三方组件库时会这样引入,比如引入antd
样式
import 'antd/dist/antd.css';
假如两个子应用中都是用了antd
,但是如果引入的antd
版本不一样,那么就可能会产生样式冲突。
附上react-scripts
构建时对样式处理的关键代码,不难看出对import 'index.css'
或者import 'index.scss'
这两种引入方式不会进行css module
和postcss
处理
// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
// common function to get style loaders
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
// css is located in `static/css`, use '../../' to locate index.html folder
// in production `paths.publicUrlOrPath` can be a relative path
options: paths.publicUrlOrPath.startsWith('.')
? { publicPath: '../../' }
: {},
},
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
{
// Options for PostCSS as we reference these options twice
// Adds vendor prefixing based on your specified browser support in
// package.json
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebook/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
}),
// Adds PostCSS Normalize as the reset css with default options,
// so that it honors browserslist config in package.json
// which in turn let's users customize the target behavior as per their needs.
postcssNormalize(),
],
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
},
},
].filter(Boolean);
if (preProcessor) {
loaders.push(
{
loader: require.resolve('resolve-url-loader'),
options: {
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
root: paths.appSrc,
},
},
{
loader: require.resolve(preProcessor),
options: {
sourceMap: true,
},
}
);
}
return loaders;
};
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader turns CSS into JS modules that inject <style> tags.
// In production, we use MiniCSSExtractPlugin to extract that CSS
// to a file, but in development "style" loader enables hot editing
// of CSS.
// By default we support CSS Modules with the extension .module.css
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
},
// Opt-in support for SASS (using .scss or .sass extensions).
// By default we support SASS Modules with the
// extensions .module.scss or .module.sass
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
},
'sass-loader'
),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
},
'sass-loader'
),
},
针对上述不会进行CSS module
和postcss
处理的引入方式场景需要做样式隔离。在加载子应用时给动态注入的<style>
打上id
标识,并将被加载的子应用名通过customProps
传递给钩子,在子应用卸载时删除动态注入的<style>
registerApplication(appName, ({ name }) => applicationOrLoadingFn(url, name), (name, location) => ({
appName: name,
}))
/** single-spa应用包装器 */
export const singleSpaPacker = (rootComponent: React.FC<any>) => {
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent,
domElementGetter,
})
const sandbox = new Sandbox('');
const bootstrap = (props: any) => {
diffSandbox.active();
return reactLifecycles.bootstrap(props);
}
const mount = (props: any) => {
return reactLifecycles.mount(props);
}
const unmount = (props: any) => {
/** 还原环境 */
diffSandbox.inactive();
/** 删除注入的样式 */
document.getElementById(props.appName)?.remove();
return reactLifecycles.unmount(props);
}
return { bootstrap, mount, unmount };
}
从子应用A
切到子应用B
然后再切回子应用A
时发现卸载后的<style>
不会重新注入页面,这是因为single-spa
只会在首次加载子应用才会执行加载函数,其余情况走的都是缓存,因此上述做法不行。
然后我想了下能不能在mount
阶段对相应的<style>
加上display: block;
,在unmount
阶段对相应的<style>
加上display: none;
,控制样式生效呢?但是实际上发现这并不能控制<style>
生效或者失效,加上display: none;
也会生效。
const mount = (props: any) => {
const styleLabel = document.getElementById(props.appName);
if (styleLabel) {
styleLabel.style.display = 'block';
}
return reactLifecycles.mount(props);
}
const unmount = (props: any) => {
const styleLabel = document.getElementById(props.appName);
if (styleLabel) {
styleLabel.style.display = 'none';
}
diffSandbox.inactive();
return reactLifecycles.unmount(props);
}
那我将两次初次加载注入页面的<style>
记录到全局对象中,然后通过customProps
传递给子应用,在mount阶段注入页面,在unmount
阶段删除
let styleTag: any = {};
/** 根据路由加载子应用 */
const applicationOrLoadingFn = async (url: string, appName: string): Promise<LifeCycles> => {
/** 获取mainfest */
const files = await fetchAssets(url);
/** 获取JS脚本,并出入到importsmap中,后续通过systemjs获取 */
// const maps: IImports = { imports: {} } as IImports;
let styleText: string = '';
const JSList = files.filter(s => /\^*.js$/.test(s));
await Promise.all(JSList.map((file: string) => axios.get(`${url}${file}?version=${Date.now()}`))).then(res => {
res.forEach(r => {
// new Function(r.data)();
eval.call(null, r.data);
})
})
// SystemJS加载方式
// JSList.forEach(async (file: string) => {
// maps.imports[`@${appName}${file}`] = `${url}${file}?version=${Date.now()}`
// });
// await insertNewImportMap(maps);
// await Promise.all(Object.keys(maps.imports).map((key: string) => (window as any).System.import(key)))
/** 注入style */
const CSSList = files.filter(s => /\^*.css$/.test(s));
await Promise.all(CSSList.map((file: string) => axios.get(`${url}${file}?version=${Date.now()}`))).then(res => {
styleText = res.map(r => r.data).join('\n');
const style = document.createElement('style');
style.id = appName;
style.appendChild(document.createTextNode(styleText));
// 记录子应用style
styleTag[appName] = style;
})
return (window as any)[appName];
}
/** 注册single-spa应用 */
routerConfig.forEach((item) => {
const { appName, hash, url } = item;
registerApplication(appName, ({ name }) => applicationOrLoadingFn(url, name), pathPrefix(hash), (name) => ({
appName: name,
styleTag // 将子应用style传递给钩子
}));
})
子应用钩子注入、删除<style>
/** single-spa应用包装器 */
export const singleSpaPacker = (rootComponent: React.FC<any>) => {
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent,
domElementGetter,
})
const diffSandbox = new DiffSandbox('');
const bootstrap = (props: any) => {
diffSandbox.active();
return reactLifecycles.bootstrap(props);
}
const mount = (props: any) => {
document.head.appendChild(props.styleTag[props.name]);
return reactLifecycles.mount(props);
}
const unmount = (props: any) => {
props.styleTag[props.appName].remove();
diffSandbox.inactive();
return reactLifecycles.unmount(props);
}
return { bootstrap, mount, unmount };
}
github
传送门:https://github.com/Revelation...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。