深入浅出微前端

深入浅出微前端

长文警告⚠️,目的是通过从使用到实现,一层层剖析微前端

文章首发于@careteen/micro-fe,转载请注明来源即可。

背景

在微前端出现之前,一个系统的前端开发模式基本都是单仓库,包含了所有的功能、代码...

很多企业也基本在物理上进行了应用代码隔离,实行单个应用单个库,闭环部署更新测试环境和正式环境。

比如我们公司的权限管理后台,首页中罗列了各个系统的入口,每个系统由单独仓库管理,点击具体系统,打开新窗口进行访问。

由于多个应用一级域名一致,使用不同二级域名区分。cookie存放在一级域名下,所以各应用可以借此实现用户信息的一致性。但是对于头部、左侧菜单通用的模块,以及多个应用之间如何实现资源共享?

我们尝试采用npm包形式头部、左侧菜单抽离成npm包的形式进行管理和使用。但是却带来了发布效率低下的问题;

如果需要迭代npm包内的逻辑业务,需要先发布npm包之后,再每个使用了该npm包的应用都更新一次npm包版本,再各自构建发布一次,过程繁琐。如果涉及到的应用更多的话,花费的人力和精力就更多了。

不仅如此,我们可能还有下面几个诉求:

  • 不同团队间开发同一个应用技术栈不同怎么办?
  • 希望每个团队都可以独立开发,独立部署怎么办?(上述方式虽然可以解决,但是体验不好)
  • 项目中还需要老的应用代码怎么办?

什么是微前端

在2016年,微前端的概念诞生。micro-frontends中定义Techniques, strategies and recipes for building a modern web App with multiple teams that can ship features independently.翻译成中文为用来构建能够让 多个团队 独立交付项目代码的 现代web App 技术,策略以及实践方法

前端也是借鉴后端微服务的思想。微前端就是将不同的功能按照不同的纬度拆分成多个子应用。通过主应用来加载这些子应用

前端的核心在于先拆后合

前端优势

  • 同步更新
  • 增量升级
  • 简单、解耦的代码
  • 独立开发、部署

前端解决方案

  • 基座模式:通过搭建基座、配置中心来管理子应用。如基于single spaqiankun方案。
  • 自组织模式:通过约定进行互相调用,但会遇到处理第三方依赖的问题。
  • 去中心模式:脱离基座模式,每个应用之间都可以批次分享资源。如基于webpack5 module federation实现的EMP微前端方案,可以实现多个应用彼此共享资源。

为什么不是TA

为什么不是 iframe

qiankun技术圆桌中有一篇关于微前端Why Not Iframe的思考,下面贴一下iframe的优缺点

  • iframe 提供了浏览器原生的硬隔离方案,不论是样式隔离、 JS 隔离这类问题统统都能被完美解决。
  • url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
  • UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
  • 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
  • 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

因为这些原因,最终大家都舍弃了 iframe 方案。

为什么不是 Web Component

MDN Web Components由三项主要技术组成,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。

  • Custom elements(自定义元素):一组JavaScript API,允许您定义custom elements及其行为,然后可以在您的用户界面中按照需要使用它们。
  • Shadow DOM(影子DOM):一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
  • HTML templates(HTML模板)<template> 和 <slot> 元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。

官方提供的示例web-components-examples

但是兼容性很差,查看can i use WebComponents

为什么不是ESM

ESMES Module,是一种前端模块化手段。他能做到微前端的几个核心点

  • 无技术栈限制: ESM加载的只是JS内容,无论哪个框架,最终都要编译成JS,因此,无论哪种框架,ESM都能加载。
  • 应用单独开发: ESM只是JS的一种规范,不会影响应用的开发模式。
  • 应用整合: 只要将微应用以ESM的方式暴露出来,就能正常加载。
  • 远程加载模块: ESM能够直接请求cdn资源,这是它与生俱来的能力。

但是可惜的是兼容性不好,查看can i use import

SingleSpa

查看single-spa配置文件JS#L44" target="_blank" rel="nofollow noopener noreferrer">rollup.config.JS可得知,使用了rollup做打包工具,并采用的system模块规范做输出。

感兴趣可查看对@careteen/rollup的简易实现。

那我们就很有必要先介绍下SystemJS的相关知识。

JS%E4%BD%BF%E7%94%A8">SystemJS使用

SystemJS 是一个通用的模块加载器,它能在浏览器上动态加载模块。微前端的核心就是加载微应用,我们将应用打包成模块,在浏览器中通过 SystemJS 来加载模块。

下方示例存放在JS" target="_blank" rel="nofollow noopener noreferrer">@careteen/micro-fe/system.JS,感兴趣可以前往调试。

新建项目并配置

安装依赖

$ mkdir system.JS
$ yarn init
$ yarn add webpack webpack-cli webpack-dev-server babel-loader @babel/core @babel/preset-env @babel/preset-React html-webpack-plugin -D
$ yarn add React React-dom
复制

配置webpack.config.JS文件,采用system.JS模块规范作为output.libraryTarget,并不打包React/React-dom

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = (env) => {
  return {
    mode: "development",
    output: {
      filename: "index.JS",
      path: path.resolve(__dirname, "dest"),
      libraryTarget: env.production ? "system" : "",
    },
    module: {
      rules: [
        {
          test: /.JS$/,
          use: { loader: "babel-loader" },
          exclude: /node_modules/,
        },
      ],
    },
    plugins: [
      !env.production &&
        new HtmlWebpackPlugin({
          template: "./public/index.html",
        }),
    ].filter(Boolean),
    externals: env.production ? ["React", "React-dom"] : [],
  };
};
复制

配置.babelrc文件

{
  "presets":[
    "@babel/preset-env",
    "@babel/preset-React"
  ]
}
复制

配置package.JSon文件

"scripts": {
  "dev": "webpack serve",
  "build": "webpack --env production"
},
复制
JS%E3%80%81html%E4%BB%A3%E7%A0%81">编写JS、html代码

新建src/index.JS入口文件

import React from 'React';
import ReactDOM from 'React-dom';

ReactDOM.render(
<h1>hello system.JS</h1>,
document.getElementById(‘root’)
)复制

新建public/index.html文件,以cdn的形式引入system.JS,并且将React/React-dom作为前置依赖配置到systemJS-importmap中。

<!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>system.JS demo</title>
</head>

<body>
<script type=“systemJS-importmap”>
{
“imports”: {
React”: “https://cdn.bootcdn.net/ajax/libs/React/17.0.2/umd/React.production.min.JS”,
React-dom”: “https://cdn.bootcdn.net/ajax/libs/React-dom/17.0.2/umd/React-dom.production.min.JS
}
}
</script>
<div id=“root”></div>
<script src=“https://cdn.bootcdn.net/ajax/libs/systemJS/6.10.1/system.min.JS”></script>
<script>
System.import(“./index.JS”).then(() => {});
</script>
</body>
</html>复制

然后命令行运行

$ npm run dev # or build
复制

打开浏览器访问,可正常显示文本。

查看dest目录

观察dest/index.JS文件,可发现通过system.JS打包后会根据webpack配置而先register预加载React/React-dom然后返回execute执行函数。

System.register([“React”,“React-dom”], function(WEBPACK_DYNAMIC_EXPORT, system_context) {
return {
setters: [
// …
],
execute: function() {
// …
}
};
});
复制

并且我们在使用时是通过System.import(“./index.JS”).then(() => {});这个形式。

基于上述观察,我们了解到system.JS两个核心api

  • System.import :加载入口文件
  • System.register :预加载

下面将做个简易实现。

JS%E5%8E%9F%E7%90%86">SystemJS原理

下方实现原理代码存放在JS/dest/index.html" target="_blank" rel="nofollow noopener noreferrer">@careteen/micro-fe/system.JS/dest/index.html,感兴趣可以前往调试。

首先提供构造函数,并将window的属性存一份,目的是查找对window属性进行的修改。

function SystemJS() {}
let set = new Set();
const saveGlobalPro = () => {
for (let p in window) {
set.add§;
}
};
const getGlobalLastPro = () => {
let result;
for (let p in window) {
if (set.has§) continue;
result = window[p];
result.default = result;
}
return result;
};

saveGlobalPro();复制

核心方法-register

实现register方法,主要是对前置依赖做存储,方便后面加载文件时取值加载。

let lastRegister;
SystemJS.prototype.register = function (deps, declare) {
// 将本次注册的依赖和声明 暴露到外部
lastRegister = [deps, declare];
};
复制

使用JSONP提供load创建script脚本函数。

function load(id) {
return new Promise((resolve, reject) => {
const script = document.createElement(“script”);
script.src = id;
script.async = true;
document.head.AppendChild(script);
script.addEventListener(“load”, function () {
// 加载后会拿到 依赖 和 回调
let _lastRegister = lastRegister;
lastRegister = undefined;
  if (!_lastRegister) {
    resolve([[], function () {}]); // 表示没有其他依赖了
  }
  resolve(_lastRegister);
});

});
}复制

核心方法-import

实现import方法,传参为id即入口文件,加载入口文件后,解析查看dest目录中的setters和execute

由于ReactReact-dom 会给全局增添属性 window.React,window.ReactDOM属性,所以可以通过getGlobalLastPro获取到这些新增的依赖库。

SystemJS.prototype.import = function (id) {
return new Promise((resolve, reject) => {
const lastSepIndex = window.location.href.lastIndexOf(“/”);
const baseURL = location.href.slice(0, lastSepIndex + 1);
if (id.startsWith(“./”)) {
resolve(baseURL + id.slice(2));
}
}).then((id) => {
let exec;
// 可以实现system模块递归加载
return load(id)
.then((registerition) => {
let declared = registerition[1](() => {});
// 加载 ReactReact-dom 加载完毕后调用setters
// 调用执行函数
exec = declared.execute;
return [registerition[0], declared.setters];
// {setters:[],execute:function(){}}
})
.then((info) => {
return Promise.all(
info[0].map((dep, i) => {
var setter = info[1][i];
// ReactReact-dom 会给全局增添属性 window.React,window.ReactDOM
return load(dep).then(® => {
// console.log®;
let p = getGlobalLastPro();
// 这里如何获取 ReactReact-dom?
setter§; // 传入加载后的文件
});
})
);
})
.then(() => {
exec();
});
});
};
复制

上述简单实现了system.JS的核心方法,可注释掉cdn引入形式,使用自己实现的进行测试,可正常展示。

let System = new SystemJS();
System.import(“./index.JS”).then(() => {});
复制

SingleSpa使用

下方示例代码存放在@careteen/micro-fe/single-spa,感兴趣可以前往调试。

安装脚手架,方便快速创建应用

$ npm i -g create-single-spa
复制
创建基座
$ create-single-spa base
复制

src/careteen-root-config.JS文件中新增下面子应用配置

registerApplication({
name: “@careteen/Vue”, // 应用名字
App: () => System.import(“@careteen/Vue”), // 加载的应用
activeWhen: [“/Vue”], // 路径匹配
customProps: {
name: ‘single-spa-base’,
},
});

registerApplication({
name: “@careteen/React”,
App: () => System.import(“@careteen/React”),
activeWhen: [“/React”],
customProps: {
name: ‘single-spa-base’,
},
});
start({
urlRerouteOnly: true, // 全部使用SingleSpa中的reroute管理路由
});复制

提供registerApplication方法注册并加载应用start方法启动应用

查看src/index.eJS文件

<script type=“systemJS-importmap”>
{
“imports”: {
“single-spa”: “https://cdn.JSdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.JS
}
}
</script>
<link rel=“preload” href=“https://cdn.JSdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.JS” as=“script”>

<script>
System.import(‘@careteen/root-config’);
</script>复制

可得知需要single-spa作为前置依赖,并且实现preload预加载,最后加载基座应用System.import(‘@careteen/root-config’);

下面继续使用脚手架创建子应用

Vue%E9%A1%B9%E7%9B%AE">创建Vue项目
$ create-single-spa slave-Vue
复制

此处选择Vue3.x版本。新建Vue.config.JS配置文件,配置开发端口号为3000

module.exports = {
devServer: {
port: 3000,
},
}
复制

还需要修改src/router/index.JS

const router = createRouter({
history: createWebHistory(‘/Vue’),
routes,
});
复制

在基座中配置

<script type=“systemJS-importmap”>
{
“imports”: {
“@careteen/root-config”: “//localhost:9000/careteen-root-config.JS”,
“@careteen/slave-Vue”: “//localhost:3000/JS/App.JS
}
}
</script>
复制
React%E9%A1%B9%E7%9B%AE">创建React项目
$ create-single-spa slave-React
复制

修改开发端口号为4000

“scripts”: {
“start”: “webpack serve --port 4000”,
}
复制

创建下面路由

import { BrowserRouter as Router, Route, Link, Switch, Redirect } from ‘React-router-dom’
import Home from ‘./components/Home.JS
import About from ‘./components/About.JS

export default function Root(props) {
return <Router basename=“/React”>
<div>
<Link to=“/”>Home React</Link>
<Link to=“/about”>About React</Link>
</div>
<Switch>
<Route path=“/” exact={true} component={Home}></Route>
<Route path=“/about” component={About}></Route>
<Redirect to=“/”></Redirect>
</Switch>
</Router>
}复制

在基座中配置React/React-dom以及@careteen/React

<script type=“systemJS-importmap”>
{
“imports”: {
“single-spa”: “https://cdn.JSdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.JS”,
React”:“https://cdn.bootcdn.net/ajax/libs/React/17.0.2/umd/React.production.min.JS”,
React-dom”:“https://cdn.bootcdn.net/ajax/libs/React-dom/17.0.2/umd/React-dom.production.min.JS
}
}
</script>
<script type=“systemJS-importmap”>
{
“imports”: {
“@careteen/root-config”: “//localhost:9000/careteen-root-config.JS”,
“@careteen/slave-Vue”: “//localhost:3000/JS/App.JS”,
“@careteen/React”: “//localhost:4000/careteen-React.JS
}
}
</script>
复制
启动项目
$ cd base && yarn start
$ cd …/slave-Vue && yarn start
$ cd …/slave-React && yarn start
复制

浏览器打开 http://localhost:9000/

手动输入 Vue/" target="_blank" rel="nofollow noopener noreferrer">http://localhost:9000/Vue/ 并可以切换路由

手动输入 React/" target="_blank" rel="nofollow noopener noreferrer">http://localhost:9000/React/ 并可以切换路由

SingleSpa原理

下方原理实现代码存放在@careteen/micro-fe/single-spa/single-spa,感兴趣可以前往调试。

single spa使用中,可以发现主要是两个方法registerApplicationstart

先新建single-spa/example/index.html文件,使用cdn的形式使用single-spa

原生Demo
<!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>my single spa demo</title>
<script src=“https://cdn.bootcdn.net/ajax/libs/single-
spa/5.9.3/umd/single-spa.min.JS”></script>
</head>

<body>
<!-- 切换导航加载不同的应用 -->
<a href=“#/a”>a应用</a>
<a href=“#/b”>b应用</a>
<!-- 源码中single-spa 是用rollup打包的 -->
<script type=“module”>
const { registerApplication, start } = singleSpa;
// 接入协议
let App1 = {
bootstrap: [
// 这东西只执行一次 ,加载完应用,不需要每次都重复加载
async (customProps) => {
// koa中的中间件 VueRouter4 中间件
console.log(“App1 启动~1”, customProps);
},
async () => {
console.log(“App1 启动~2”);
},
],
mount: async (customProps) => {
console.log(“App1 mount”);
},
unmount: async (customProps) => {
console.log(“App1 unmount”);
},
};
let App2 = {
bootstrap: [
async () => {
console.log(“App2 启动~1”);
},
async () => {
console.log(“App2 启动~2”);
},
],
mount: async () => {
console.log(“App2 mount”);
},
unmount: async () => {
console.log(“App2 unmount”);
},
};

  const customProps = { name: "single spa" };
  // 注册微<a href="/tag/79A2B8BB.shtml" title="应用">应用</a>
  register<a href="/tag/79A2BEDA.shtml" title="App">App</a>lication(
    "<a href="/tag/79A2BEDA.shtml" title="App">App</a>1", // 这个名字可以用于过滤防止加载重复的<a href="/tag/79A2B8BB.shtml" title="应用">应用</a>
    <a href="/tag/79A2D3AE.shtml" title="async">async</a> () =&gt; {
      return <a href="/tag/79A2BEDA.shtml" title="App">App</a>1;
    },
    (location) =&gt; location.hash == "#/a",
    customProps
  );
  register<a href="/tag/79A2BEDA.shtml" title="App">App</a>lication(
    "<a href="/tag/79A2BEDA.shtml" title="App">App</a>2", // 这个名字可以用于过滤防止加载重复的<a href="/tag/79A2B8BB.shtml" title="应用">应用</a>
    <a href="/tag/79A2D3AE.shtml" title="async">async</a> () =&gt; {
      return <a href="/tag/79A2BEDA.shtml" title="App">App</a>2;
    },
    (location) =&gt; location.hash == "#/b",
    customProps
  );

  start();
&lt;/script&gt;

</body>
</html>复制

package.JSon做如下配置

“scripts”: {
“dev”: “http-server -p 5000”
}
复制

然后运行

$ cd single-spa
$ yarn
$ yarn dev
复制

打开 http://127.0.0.1:5000/example 点击切换a b应用查看打印结果

Application">核心方法-registerApplication

接着去实现核心方法

新建single-spa/src/single-spa.JS

export { registerApplication } from ‘./Applications/Apps.JS’;
export { start } from ‘./start.JS’;
复制

新建single-spa/src/Applications/App.JS

import { reroute } from “…/navigation/reroute.JS”;
import { NOT_LOADED } from “./App.helpers.JS”;

export const Apps = [];
export function registerApplication(AppName, loadApp, activeWhen, customProps) {
const registeration = {
name: AppName,
loadApp,
activeWhen,
customProps,
status: NOT_LOADED,
};
Apps.push(registeration);
reroute();
}复制

维护数组Apps存放所有的子应用,每个子应用需要的传参如下

  • AppName: 应用名称
  • loadApp: 应用的加载函数 此函数会返回 bootstrap mount unmount
  • activeWhen: 当前什么时候激活 location => location.hash == ‘#/a’
  • customProps: 用户的自定义参数
  • status: 应用状态

将子应用保存到Apps中,后续可以在数组里晒选需要的App是加载 还是 卸载 还是挂载

还需要调用reroute,重写路径, 后续切换路由要再次做这些事 ,这也是single-spa的核心。

状态机

NOT_LOADED(未加载)应用的默认状态,那应用还存在哪些状态呢?

新建single-spa/src/Applications/App.helpers.JS存放所有状态

export const NOT_LOADED = “NOT_LOADED”; // 应用默认状态是未加载状态
export const LOADING_SOURCE_CODE = “LOADING_SOURCE_CODE”; // 正在加载文件资源
export const NOT_BOOTSTRAppED = “NOT_BOOTSTRAppED”; // 此时没有调用bootstrap
export const BOOTSTRAppING = “BOOTSTRAppING”; // 正在启动中,此时bootstrap调用完毕后,需要表示成没有挂载
export const NOT_MOUNTED = “NOT_MOUNTED”; // 调用了mount方法
export const MOUNTED = “MOUNTED”; // 表示挂载成功
export const UNMOUNTING = “UNMOUNTING”; // 卸载中, 卸载后回到NOT_MOUNTED

// 当前应用是否被挂载了 状态是不是MOUNTED
export function isActive(App) {
return App.status == MOUNTED;
}

// 路径匹配到才会加载应用
export function shouldBeActive(App) {
// 如果返回的是true 就要进行加载
return App.activeWhen(window.location);
}复制

于此同时还是提供几个方法判断当前应用所处状态。

然后再提供根据App状态对所有注册的App进行分类

// single-spa/src/<a href="/tag/79A2BEDA.shtml" title="App">App</a>lications/<a href="/tag/79A2BEDA.shtml" title="App">App</a>.helpers.<a href="/tag/79A2CC4D.shtml" title="JS">JS</a>
export function getAppChanges() {
// 拿不到所有App的?
const AppsToLoad = []; // 需要加载的列表
const AppsToMount = []; // 需要挂载的列表
const AppsToUnmount = []; // 需要移除的列表
Apps.forEach((App) => {
const AppShouldBeActive = shouldBeActive(App); // 看一下这个App是否要加载
switch (App.status) {
case NOT_LOADED:
case LOADING_SOURCE_CODE:
if (AppShouldBeActive) {
AppsToLoad.push(App); // 没有被加载就是要去加载的App,如果正在加载资源 说明也没有加载过
}
break;
case NOT_BOOTSTRAppED:
case NOT_MOUNTED:
if (AppShouldBeActive) {
AppsToMount.push(App); // 没启动柜, 并且没挂载过 说明等会要挂载他
}
break;
case MOUNTED:
if (!AppShouldBeActive) {
AppsToUnmount.push(App); // 正在挂载中但是路径不匹配了 就是要卸载的
}
default:
break;
}
});
return { AppsToLoad, AppsToMount, AppsToUnmount };
}
复制

然后开始实现single-spa/src/navigation/reroute.JS的核心方法

import {
getAppChanges,
} from “…/Applications/App.helpers.JS”;
export function reroute() {
// 所有的核心逻辑都在这里
const { AppsToLoad, AppsToMount, AppsToUnmount } = getAppChanges();
return loadApps();
function loadApps() {
// 获取所有需要加载的App,调用加载逻辑
const loadPromises = AppsToLoad.map(toLoadPromise); // 调用加载逻辑
return Promise.all(loadPromises)
}
}
复制

于此同时再提供工具方法,方便处理传参进来的生命周期钩子是数组的场景

function flattenFnArray(fns) {
fns = Array.isArray(fns) ? fns : [fns];
return function (customProps) {
return fns.reduce(
(resultPromise, fn) => resultPromise.then(() => fn(customProps),
Promise.resolve()
);
};
}
复制

实现原理类似于koa中的中间件,将多个promise组合成一个promise链。

再提供toLoadPromise, 只有当子应用NOT_LOADED 的时候才需要加载,并使用flattenFnArray将各个生命周期进行处理

function toLoadPromise(App) {
return Promise.resolve().then(() => {
if (App.status !== NOT_LOADED) {
return App;
}
App.status = LOADING_SOURCE_CODE;
return App.loadApp().then((val) => {
let { bootstrap, mount, unmount } = val; // 获取应用的接入协议,子应用暴露的方法
App.status = NOT_BOOTSTRAppED;
App.bootstrap = flattenFnArray(bootstrap);
App.mount = flattenFnArray(mount);
App.unmount = flattenFnArray(unmount);
  return <a href="/tag/79A2BEDA.shtml" title="App">App</a>;
});

});
}复制

核心方法-start

然后实现single-spa/src/start.JS

import { reroute } from “./navigation/reroute.JS”;
export let started = false;
export function start() {
started = true; // 开始启动了
reroute();
}
复制
核心逻辑-reroute

接着需要对reroute方法进行完善,将不需要的组件全部卸载,将需要加载的组件去加载-> 启动 -> 挂载,如果已经加载完毕,那么直接启动和挂载。

export function reroute() {
const { AppsToLoad, AppsToMount, AppsToUnmount } = getAppChanges();
if (started) { // 启动应用
return performAppChanges();
}
function performAppChanges() {
AppsToUnmount.map(toUnmountPromise);
AppsToLoad.map(App => toLoadPromise(App).then((App) => tryBootstrapAndMount(App)))
AppsToMount.map(AppToMount => tryBootstrapAndMount(AppToMount))
}
}
复制

其核心就是卸载需要卸载的应用-> 加载应用 -> 启动应用 -> 挂载应用

然后提供toUnmountPromise,标记成正在卸载,调用卸载逻辑 , 并且标记成 未挂载。

function toUnmountPromise(App) {
return Promise.resolve().then(() => {
// 如果不是挂载状态 直接跳出
if (App.status !== MOUNTED) {
return App;
}
App.status = UNMOUNTING;
return App.unmount(App.customProps).then(() => {
App.status = NOT_MOUNTED;
});
});
}
复制

以及tryBootstrapAndMount,提供a/b应用的切换

// a -> b b->a a->b
function tryBootstrapAndMount(App, unmountPromises) {
return Promise.resolve().then(() => {
if (shouldBeActive(App)) {
return toBootStrAppromise(App).then((App) =>
unmountPromises.then(() => {
capturedEventListeners.hashchange.forEach((item) => item());
return toMountPromise(App);
})
);
}
});
}
复制

实现toBootStrAppromise启动应用

function toBootStrAppromise(App) {
return Promise.resolve().then(() => {
if (App.status !== NOT_BOOTSTRAppED) {
return App;
}
App.status = BOOTSTRAppING;
return App.bootstrap(App.customProps).then(() => {
App.status = NOT_MOUNTED;
return App;
});
});
}
复制

实现toMountPromise加载应用

function toMountPromise(App) {
return Promise.resolve().then(() => {
if (App.status !== NOT_MOUNTED) {
return App;
}
return App.mount(App.customProps).then(() => {
App.status = MOUNTED;
return App;
});
});
}
复制

上述实现了子应用各个状态的切换逻辑,下面还需要将路由进行重写。

新建single-spa/src/navigation/navigation-events.JS,监听hashchange和popstate,路径变化时重新初始化应用

import { reroute } from “./reroute.JS”;

function urlRoute() {
reroute();
}
window.addEventListener(“hashchange”, urlRoute);
window.addEventListener(“popstate”, urlRoute);复制

需要对浏览器的事件进行拦截,其实现方式和Vue-router类似,使用AOP的思想实现的。

因为子应用里面也可能会有路由系统,需要先加载父应用的事件,再去调用子应用

const routerEventsListeningTo = [“hashchange”, “popstate”];
export const capturedEventListeners = {
hashchange: [],
popstate: [],
};
const originalAddEventListener = window.addEventListener;
const originalRemoveEventLister = window.removeEventListener;

window.addEventListener = function (eventName, fn) {
if (
routerEventsListeningTo.includes(eventName) &&
!capturedEventListeners[eventName].some((l) => fn == l)
) {
return capturedEventListeners[eventName].push(fn);
}
return originalAddEventListener.Apply(this, arguments);
};

window.removeEventListener = function (eventName, fn) {
if (routerEventsListeningTo.includes(eventName)) {
return (capturedEventListeners[eventName] = capturedEventListeners[
eventName
].filter((l) => fn != l));
}
return originalRemoveEventLister.Apply(this, arguments);
};复制

需要对跳转方法进行拦截,例如 Vue-router内部会通过pushState() 不改路径改状态,所以还是要处理下。如果路径不一样,也需要重启应用

function patchedUpdateState(updateState, methodName) {
return function() {
const urlBefore = window.location.href;
const result = updateState.Apply(this, arguments);
const urlAfter = window.location.href;
if (urlBefore !== urlAfter) {
window.dispatchEvent(new PopStateEvent(“popstate”));
}
return result;
}
}
window.history.pushState = patchedUpdateState(window.history.pushState, ‘pushState’);
window.history.replaceState = patchedUpdateState(window.history.replaceState, ‘replaceState’)
复制

提供触发事件的方法

export function callCapturedEventListeners(eventArguments) { // 触发捕获的事件
if (eventArguments) {
const eventType = eventArguments[0].type;
// 触发缓存中的方法
if (routingEventsListeningTo.includes(eventType)) {
capturedEventListeners[eventType].forEach(listener => {
listener.Apply(this, eventArguments);
})
}
}
}
复制
完善核心逻辑-reroute

改动reroute逻辑,启动完成需要调用callAllEventListeners应用卸载完毕也需要调用callAllEventListeners

export function reroute() {
const { AppsToLoad, AppsToMount, AppsToUnmount } = getAppChanges();
if (started) {
return performAppChanges();
}
return loadApps();

function loadApps() {
const loadPromises = AppsToLoad.map(toLoadPromise);
return Promise.all(loadPromises).then(callAllEventListeners); // ++
}
function performAppChanges() {
let unmountPromises = Promise.all(AppsToUnmount.map(toUnmountPromise)).then(callAllEventListeners); // ++

<a href="/tag/79A2BEDA.shtml" title="App">App</a>sToLoad.map((<a href="/tag/79A2BEDA.shtml" title="App">App</a>) =&gt;
  toLoadPromise(<a href="/tag/79A2BEDA.shtml" title="App">App</a>).then((<a href="/tag/79A2BEDA.shtml" title="App">App</a>) =&gt;
    tryBootstrapAndMount(<a href="/tag/79A2BEDA.shtml" title="App">App</a>, unmountPromises)
  )
);
<a href="/tag/79A2BEDA.shtml" title="App">App</a>sToMount.map((<a href="/tag/79A2BEDA.shtml" title="App">App</a>) =&gt; tryBootstrapAndMount(<a href="/tag/79A2BEDA.shtml" title="App">App</a>, unmountPromises));

}
}复制

上述代码已经实现了基本功能

$ cd single-spa
$ yarn
$ yarn dev
复制

打开 http://127.0.0.1:5000/example 点击切换a b应用查看打印结果,表现同原生Demo的结果。

SingleSpa小结

single-spa提供了主应用作为基座,通过路由匹配加载不同子应用的模式。具备如下优点

  • 技术栈无关: 独立开发、独立部署、增量升级、独立运行时
  • 提供生命周期概念:负责调度子应用的生命周期, 挟持 url 变化事件和函数,url 变化时匹配对应子应用,并执行生命周期流程

但是仍然存在一些问题

  • 样式隔离:子应用样式可能影响主应用,需要通过类似于BEM约定式方案解决。
  • JS隔离:主子应用共用DOM、BOMAPI,例如在window上赋值同一个同名变量,将互相影响,也需要有隔离方案。

qiankun

JS/qiankun" target="_blank" rel="nofollow noopener noreferrer">qiankun的灵感来自并基于single-spa,有以下几个特点。

  • 简单: 任意 JS 框架均可使用。微应用接入像使用接入一个 iframe 系统一样简单, 但实际不是 iframe 。
  • 完备: 几乎包含所有构建微前端系统时所需要的基本能力,如 样式隔离、 JS 沙箱、 预加载等。
  • 生产可用: 已在蚂蚁内外经受过足够大量的线上系统的考验及打磨,健壮性值得信 赖。

single-spa的基础上,qiankun还实现了如下特性

  • 使用import-html-entry取代system.JS加载子应用
  • 提供多种样式隔离方案
  • 提供多种JS隔离方案

qiankun使用

下方示例代码存放在@careteen/micro-fe/qiankun,感兴趣可以前往调试。

下面实例采用React作为基座,并提供一个Vue应用和一个React应用

提供基座
$ create-React-App base
$ yarn add React-router-dom qiankun
复制

提供/Vue和/React路由

import { BrowserRouter as Router, Link } from “React-router-dom”;
function App() {
return (
<div className=“App”>
<Router>
<Link to=“/Vue”>Vue应用</Link>
<Link to=“/React”>React应用</Link>
</Router>
<div id=“container”></div>
</div>
);
}
export default App;
复制

src/registerApps.JS中配置两个子应用入口

import { registerMicroApps, start } from “qiankun”;

const loader = (loading) => {
console.log(loading);
};
registerMicroApps(
[
{
name: “slave-Vue”,
entry: “//localhost:20000”,
container: “#container”,
activeRule: “/Vue”,
loader,
},
{
name: “slave-React”,
entry: “//localhost:30000”,
container: “#container”,
activeRule: “/React”,
loader,
},
],
{
beforeLoad: () => {
console.log(“加载前”);
},
beforeMount: () => {
console.log(“挂载前”);
},
afterMount: () => {
console.log(“挂载后”);
},
beforeUnmount: () => {
console.log(“销毁前”);
},
afterUnmount: () => {
console.log(“销毁后”);
},
}
);
start({
sandbox: {
// experimentalStyleIsolation:true
strictStyleIsolation: true,
},
});复制

运行命令,打开 http://localhost:3000/ 访问,下面将继续

yarn start
复制
Vue%E5%AD%90%E5%BA%94%E7%94%A8">提供Vue应用
$ Vue create slave-Vue
复制

新建Vue.config.JS配置文件,设置publicPath保证子应用静态资源都是像20000端口上发送的,设置headers跨域保证父应用可以访问到。

qiankun没有使用single-spa所使用system.JS模块规范,而打包成umd形式,在qiankun内部使用了fetch去加载子应用文件内容。

module.exports = {
publicPath: ‘//localhost:20000’,
devServer: {
port: 20000,
headers:{
‘Access-Control-Allow-Origin’: ‘*’
}
},
configureWebpack: {
output: {
libraryTarget: ‘umd’,
library: ‘slave-Vue
}
}
}
复制

使用qiankunsingle-spa类似,需要在入口文件按照约定导出特定的生命周期函数bootstrap、mount、unmount

并且提供独立访问接入到主应用两种场景。主要是借助window.POWERED_BY_QIANKUN字段判断是否在qiankun主应用下。

import { createApp } from ‘Vue’;
import { createRouter, createWebHistory } from ‘Vue-router’;
import App from ‘./App.Vue’;
import routes from ‘./router’;

let history;
let router;
let App;
function render(props = {}) {
history = createWebHistory(‘/Vue’);
router = createRouter({
history,
routes
});
App = createApp(App);
let { container } = props;
App.use(router).mount(container ? container.querySelector(‘#App’) : ‘#App’)
}

if (!window.POWERED_BY_QIANKUN) { // 独立运行自己
render();
}

export async function bootstrap() {
console.log(‘Vue3 App bootstraped’);
}

export async function mount(props) {
console.log(‘Vue3 App mount’,);
render(props)
}
export async function unmount() {
console.log(‘Vue3 App unmount’);
history = null;
App = null;
router = null;
}复制

运行命令,打开 http://localhost:20000/ 可独立访问

$ yarn serve
复制
React%E5%AD%90%E5%BA%94%E7%94%A8">提供React应用
$ create-React-App slave-React
$ yarn add @rescripts/cli -D
复制

借助@rescripts/cliReact的配置.rescriptsrc.JS

输出和Vue项目一样也采用umd模块规范。

module.exports = {
webpack:(config)=>{
config.output.library = ‘slave-React’;
config.output.libraryTarget = ‘umd’;
config.output.publicPath = ‘//localhost:30000/’;
return config;
},
devServer:(config)=>{
config.headers = {
‘Access-Control-Allow-Origin’: ‘*’
};
return config;
}
}
复制

然后在.env中将端口号进行修改

PORT=30000
WDS_SOCKET_PORT=30000
复制

Vue应用配置

import React from ‘React’;
import ReactDOM from ‘React-dom’;
import ‘./index.css’;
import App from ‘./App’;

function render(props = {}) {
let { container } = props;
ReactDOM.render(<App />,
container ? container.querySelector(‘#root’) : document.getElementById(‘root’)
);
}
if (!window.POWERED_BY_QIANKUN) {
render();
}
export async function bootstrap() {

}
export async function mount(props) {
render(props)
}

export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(container ? container.querySelector(‘#root’) : document.getElementById(‘root’))
}复制

scripts脚本需要做修改

“scripts”: {
“start”: “rescripts start”,
“build”: “rescripts build”,
“test”: “rescripts test”,
“eject”: “rescripts eject”
},
复制

运行命令,打开 http://localhost:30000/ 可独立访问

$ yarn start
复制
查看最终效果

在主应用中配置样式隔离

start({
sandbox: {
// experimentalStyleIsolation:true
strictStyleIsolation: true,
},
});
复制

浏览器打开 http://localhost:3000/ 点击Vue应用

点击React应用,可观察父子应用样式互不影响。

qiankun原理

通过使用qiankun可观察到其APIsingle-spa差不多。下面将大致了解下qiankun的实现原理。

分析代码@careteen/qiankun,里面有大量注释。

Apps">registerMicroApps

从入口注册方法registerMicroApps开始。

export function registerMicroApps<T extends ObjectType>(
Apps: Array<RegistrableApp<T>>, // 需要注册的应用
lifeCycles?: FrameworkLifeCycles<T>, // 对应的生命周期
) {
// 过滤注册重复的应用
const unregisteredApps = Apps.filter((App) => !microApps.some((registeredApp) => registeredApp.name === App.name));

microApps = […microApps, …unregisteredApps];

// 将需要注册的新应用,循环依次注册
unregisteredApps.forEach((App) => {
const { name, activeRule, loader = noop, props, …AppConfig } = App;

// 实际还是调用 single-spa 的注册函数
register<a href="/tag/79A2BEDA.shtml" title="App">App</a>lication({
  name,
  <a href="/tag/79A2BEDA.shtml" title="App">App</a>: <a href="/tag/79A2D3AE.shtml" title="async">async</a> () =&gt; {
    loader(true); // 设置 loading
    await frameworkStartedDefer.promise; // 等待 start 方法被调用

    const { mount, ...otherMicro<a href="/tag/79A2BEDA.shtml" title="App">App</a>Configs } = (
      // 加载<a href="/tag/79A2B8BB.shtml" title="应用">应用</a>,获取生命周期钩子
      await load<a href="/tag/79A2BEDA.shtml" title="App">App</a>({ name, props, ...<a href="/tag/79A2BEDA.shtml" title="App">App</a>Config }, frameworkConfiguration, lifeCycles)
    )();

    // 调用 mount 
    return {
      mount: [<a href="/tag/79A2D3AE.shtml" title="async">async</a> () =&gt; loader(true), ...toArray(mount), <a href="/tag/79A2D3AE.shtml" title="async">async</a> () =&gt; loader(false)],
      ...otherMicro<a href="/tag/79A2BEDA.shtml" title="App">App</a>Configs,
    };
  },
  activeWhen: activeRule,
  customProps: props,
});

});
}复制

实际还是调用single-spa的注册函数registerApplication,只不过多做了过滤注册重复的应用

start
export function start(opts: FrameworkConfiguration = {}) {
// prefetch 是否支持预加载
// singular 是否支持单例模式
// sandbox 是否支持沙箱
frameworkConfiguration = { prefetch: true, singular: true, sandbox: true, …opts };
const {
prefetch,
sandbox,
singular,
urlRerouteOnly = defaultUrlRerouteOnly,
…importEntryOpts
} = frameworkConfiguration;

if (prefetch) { // 预加载策略
doPrefetchStrategy(microApps, prefetch, importEntryOpts);
}

// 开启沙箱
if (sandbox) {
// 如果不支持 Proxy 则降级到快照沙箱 loose 表示使用快照沙箱
if (!window.Proxy) {
console.warn(‘[qiankun] Miss window.Proxy, proxySandbox will degenerate into snapshotSandbox’);
frameworkConfiguration.sandbox = typeof sandbox === ‘object’ ? { …sandbox, loose: true } : { loose: true };
// Proxy 下若为非单例模式 则会报错
if (!singular) {
console.warn(
‘[qiankun] Setting singular as false may cause unexpected behavior while your browser not support window.Proxy’,
);
}
}
}

// 启动应用,最终实际调用 single spa 的 start 方法
startSingleSpa({ urlRerouteOnly });
started = true;

// 启动后,将 promise 状态改为成功态
frameworkStartedDefer.resolve();
}复制

qiankun提供预加载、单例模式、开启沙箱配置。在开启沙箱时,会优先使用Proxy代理沙箱,如果浏览器不支持,则降级使用Snapshot快照沙箱。

在使用代理沙箱时,如果浏览器不支持Proxy且开启了单例模式,则会报错,因为在快照沙箱下使用单例模式会存在问题。具体下面会提到

prefetch
export function doPrefetchStrategy(
Apps: AppMetadata[],
prefetchStrategy: PrefetchStrategy,
importEntryOpts?: ImportEntryOpts,
) {
const AppsName2Apps = (names: string[]): AppMetadata[] => Apps.filter((App) => names.includes(App.name));

if (Array.isArray(prefetchStrategy)) {
// 加载第一个应用
prefetchAfterFirstMounted(AppsName2Apps(prefetchStrategy as string[]), importEntryOpts);
}
// …
}

function prefetchAfterFirstMounted(Apps: AppMetadata[], opts?: ImportEntryOpts): void {
// 监听第一个应用
window.addEventListener(‘single-spa:first-mount’, function listener() {
// 过滤所有没加载的 App
const notLoadedApps = Apps.filter((App) => getAppStatus(App.name) === NOT_LOADED);

if (process.env.NODE_ENV === 'development') {
  const mounted<a href="/tag/79A2BEDA.shtml" title="App">App</a>s = getMounted<a href="/tag/79A2BEDA.shtml" title="App">App</a>s();
  console.log(`[qiankun] prefetch starting after ${mounted<a href="/tag/79A2BEDA.shtml" title="App">App</a>s} mounted...`, notLoaded<a href="/tag/79A2BEDA.shtml" title="App">App</a>s);
}
// 没加载的 <a href="/tag/79A2BEDA.shtml" title="App">App</a> 全部需要预加载
notLoaded<a href="/tag/79A2BEDA.shtml" title="App">App</a>s.forEach(({ entry }) =&gt; prefetch(entry, opts));
// 移除监听的事件
window.removeEventListener('single-spa:first-mount', listener);

});
}
function prefetch(entry: Entry, opts?: ImportEntryOpts): void {
if (!navigator.onLine || isSlowNetwork) {
// Don’t prefetch if in a slow network or offline
return;
}
// 使用 requestIdleCallback 在浏览器空闲时间进行预加载
requestIdleCallback(async () => {
// 使用 import-html-entry 进行加载资源
// 其内部实现 是通过 fetch 去加载资源
const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
requestIdleCallback(getExternalStyleSheets);
requestIdleCallback(getExternalScripts);
});
}复制

监听第一个加载的应用:过滤所有没加载的 App,将其预加载。

使用 requestIdleCallback浏览器空闲时间进行预加载;使用 import-html-entry 进行加载资源,其内部实现 是通过 fetch 去加载资源,取代single-spa采用的system.JS模块规范加载资源。

requestIdleCallbackReact fiber 架构中有使用到,感兴趣的可前往React/tree/master/packages/fiber#%E6%B5%8F%E8%A7%88%E5%99%A8%E4%BB%BB%E5%8A%A1%E8%B0%83%E5%BA%A6%E7%AD%96%E7%95%A5%E5%92%8C%E6%B8%B2%E6%9F%93%E6%B5%81%E7%A8%8B" target="_blank" rel="nofollow noopener noreferrer">浏览器任务调度策略和渲染流程查看。

App">loadApp

当执行start方法后,会去执行registerApplication中的loadApp加载子应用

其实现代码较多,可以前往qiankun/loader.ts/loadApp查看实现,有注释表明大概流程。总结下来主要做了如下几件事

  • 通过 importEntry 方法拉取子应用
  • 在拉取的模板外面包一层 div ,增加 css 样式隔离,提供shadowdomscopedCSS两种方式
  • 将模板进行挂载
  • 创建 JS 沙箱 ,获得沙箱开启和沙箱关闭方法
  • 合并出 beforeUnmountafterUnmountafterMountbeforeMountbeforeLoad 方法。增加 qiankun 标识
  • 依次调用 beforeLoad 方法
  • 在沙箱中执行脚本, 获取子应用的生命周期 bootstrapmountunmount 、update
  • 格式化子应用mount 方法和 unmount 方法。
    • mount执行前挂载沙箱、依次执行 beforeMount ,之后调用mount方法,将 全局通信方法传入。mount方法执行完毕后执行 afterMount
    • unmount方法会优先执行 beforeUnmount 钩子,之后开始卸载
  • 增添一个 update 方法
createSandboxContainer

接下来是如何实现创建沙箱

创建沙箱会先判断浏览器是否支持Proxy,如果支持并不是useLooseSandbox模式,则使用代理沙箱实现,如果不支持则采用快照沙箱

Proxy Sandbox
class ProxySandbox {
constructor() {
const rawWindow = window
const fakeWindow = {}
const proxy = new Proxy(fakeWindow, {
set(target, p, value) {
target[p] = value
return true
},
get(target, p) {
return target[p] || rawWindow[p]
},
})
this.proxy = proxy
}
}

let sandbox1 = new ProxySandbox()
let sandbox2 = new ProxySandbox()

window.name = ‘搜狐焦点’
((window) => {
window.name = ‘智能话机’
console.log(window.name)
})(sandbox1.proxy)

((window) => {
window.name = ‘识客宝’
console.log(window.name)
})(sandbox2.proxy)复制

其原理主要是代理原生window,在取值时优先从proxy window上获取,如果没有值再从真实 window上获取;在赋值时只改动proxy window,进而达到和主应用隔离。这只是简易实现,qiankunProxySandbox实现

Snapshot Sandbox

源码实现代码

function iter(obj: typeof window, callbackFn: (prop: any) => void) {
// eslint-disable-next-line guard-for-in, no-restricted-syntax
for (const prop in obj) {
// patch for clearInterval for compatible reason, see #1490
if (obj.hasOwnProperty(prop) || prop === ‘clearInterval’) {
callbackFn(prop);
}
}
}
// …
active() {
// 记录当前快照
this.windowSnapshot = {} as Window;
iter(window, (prop) => {
this.windowSnapshot[prop] = window[prop];
});

// 恢复之前的变更
Object.keys(this.modifyPropsMap).forEach((p: any) => {
window[p] = this.modifyPropsMap[p];
});

this.sandboxRunning = true;
}复制

主要是对window的所有属性进行了一个拍照。存在的问题就是多实例的情况会混乱,所以在浏览器不支持Proxy且设置非单例的情况下,qiankun会报错。

Style Shadow Dom Sandbox

源码实现代码

当设置strictStyleIsolation=true时,会开启Shadow Dom样式沙箱。表现如下,会包裹一层shadow dom,做到真正意义上的样式隔离,但缺点就是子应用想要复用父应用的样式时做不到。

Style Scope Sandbox

Append/common.ts#L196" target="_blank" rel="nofollow noopener noreferrer">源码实现代码

qiankun也提供设置experimentalStyleIsolation=true开启scope样式隔离,表现如下,使用div包裹子应用,并将子应用的顶级样式加上应用名称前缀进行样式隔离。其中还将标签选择器加上[data-qainkun]=“slave-name”

父子应用通信方式

源码实现代码

基于发布订阅实现。

  • setGlobalState:更新 store 数据
    • 对输入 state 的第一层属性做校验,只有初始化时声明过的第一层(bucket)属性才会被更改
    • 修改 store 并触发全局监听
  • onGlobalStateChange:全局依赖监听
    • 收集 setState 时所需要触发的依赖
  • offGlobalStateChange:注销该应用下的依赖
export function getMicroAppStateActions(id: string, isMaster?: boolean): MicroAppStateActions {
return {
onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) {
if (!(callback instanceof Function)) {
console.error(‘[qiankun] callback must be function!’);
return;
}
if (deps[id]) {
console.warn([qiankun] '${id}' global listener already exists before this, new listener will overwrite it.);
}
deps[id] = callback;
if (fireImmediately) {
const cloneState = cloneDeep(globalState);
callback(cloneState, cloneState);
}
},
setGlobalState(state: Record<string, any> = {}) {
if (state === globalState) {
console.warn(‘[qiankun] state has not changed!’);
return false;
}
const changeKeys: string[] = [];
const prevGlobalState = cloneDeep(globalState);
globalState = cloneDeep(
Object.keys(state).reduce((_globalState, changeKey) => {
if (isMaster || _globalState.hasOwnProperty(changeKey)) {
changeKeys.push(changeKey);
return Object.assign(_globalState, { [changeKey]: state[changeKey] });
}
console.warn([qiankun] '${changeKey}' not declared when init state!);
return _globalState;
}, globalState),
);
if (changeKeys.length === 0) {
console.warn(‘[qiankun] state has not changed!’);
return false;
}
emitGlobal(globalState, prevGlobalState);
return true;
},
offGlobalStateChange() {
delete deps[id];
return true;
},
};
}
复制

qiankun小结

  • 基于 single spa的上层封装
  • 提供shadow domscope样式隔离方案
  • 解决proxy sandboxsnapshot sanboxJS隔离方案
  • 基于发布订阅更好的服务于React setState
  • 还提供JS.org/zh-CN/plugins/plugin-qiankun" target="_blank" rel="nofollow noopener noreferrer">@umiJS/plugin-qiankun插件能在umi应用下更好的接入

总结

除了single-spa这种基于底座的微前端解决方案, webpack5 module federationwebpack5的联邦模块也能实现,YY团队的EMP基于此实现了去中心模式,脱离基座模式,每个应用之间都可以批次分享资源。可以通过这篇文章尝尝鲜,后面再继续研究。

            </div>
                            <div class="share" id="down"><img src="">
                <div class="share-text">
                     <p>本文地址:<a href="/Show/27/<%ID%>.shtml" target="_blank">  深入浅出微前端  </a></p>
                </div>
            </div>
            <div class="clear"></div>
            
        </div>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值