前言
回顾一下, 断更了有快两个月了,最近属实是忙过头了,又是找工作准备面试,又是接了个大需求,做的痛不欲生的。在这种节奏下,更文就被落下了。
好了,回归正题,今天我们来聊一聊SystemJs。
SystemJs
对于SystemJs,或许很多人并没有听说过,甚至很多人其实有在使用它,但却没见过它。
SystemJs是一个极其灵活的模块加载器
,也可以认为它是一种规范
,因为使用它去加载模块,需要符合一定的规范。
它的主要用途就是加载模块,在微前端领域中,single-spa就是依赖于它的,而我们都知道,qiunkun是基于single-spa的,那么上面说的很多人正在使用SystemJs却没见过它,指的就是很多qiankun用户啦,毕竟SystemJs只是默默地在底层工作。
ok,我们继续聊聊它的使用,以及它是如何加载模块的。
基本使用
我们使用webpack起一个很简单的项目,项目主要文件内容如下。
// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = (env) => {
return {
mode: "development",
output: {
filename: "index.js",
path: path.resolve(__dirname, "dist"),
// 注意,这个libraryTarget是必须的
libraryTarget: env.production ? "system" : "",
},
module: {
rules: [
{
test: /.js$/,
use: { loader: "babel-loader" },
exclude: /node_modules/,
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
})
],
// 这里不为空就行,如若是vue,改为[vue]即可
externals: env.production ? ["react", "react-dom"] : [],
};
};
这个配置文件中,其他配置都比较随意,但libraryTarget是必须的,且webpack版本必须为4.30以上,指定为system之后,它会打包出一个符合SystemJs要求的产物。
ok。这个项目主要就是这份配置文件,其他文件,各位随便写点就好。笔者这边就仅写一个简单的入口文件。
const App = () => <div><h1>Hello World!</h1></div>;
export default App;
然后我们添加一行打包命令。
// package.json
"scripts": {
"build": "webpack --env production"
},
执行build命令,看下我们打包后的产物。
如果和笔者这边的配置一样的,dist文件夹中会有两个文件,一个index.js,另一个是index.html
我们先来看index.js
System.register(
["react-dom", "react"],
function (__WEBPACK_DYNAMIC_EXPORT__, __system_context__) {
var __WEBPACK_EXTERNAL_MODULE_react_dom__ = {};
var __WEBPACK_EXTERNAL_MODULE_react__ = {};
Object.defineProperty(__WEBPACK_EXTERNAL_MODULE_react_dom__, "__esModule", {
value: true,
});
Object.defineProperty(__WEBPACK_EXTERNAL_MODULE_react__, "__esModule", {
value: true,
});
return {
setters: [
function (module) {
Object.keys(module).forEach(function (key) {
__WEBPACK_EXTERNAL_MODULE_react_dom__[key] = module[key];
});
},
function (module) {
Object.keys(module).forEach(function (key) {
__WEBPACK_EXTERNAL_MODULE_react__[key] = module[key];
});
},
],
execute: function() { 这里是项目的打包后可执行的代码 },
}
},
);
经过很轻微地简化,就是如上代码了,这个文件就一个register函数,这就是SystemJs的要求,这个脚本的内容改为被一个register函数包裹,然后第一个参数就是不被打包进来但通过特定方式进行加载(下文会讲到这个特定的加载方式),第二个参数一个函数,主要用来返回我们的项目代码,其中__WEBPACK_EXTERNAL_MODULE_react_dom__和__WEBPACK_EXTERNAL_MODULE_react__则分别对应第一个参数中的需要特定方式加载回来的模块。
看到此处,相信各位还是比较懵的,没关系,我们继续往下走。
我们来看index.html
内容非常简单,一个div盒子,加上一个script标签,这个script加载的就是上面的index.js,我们直接用浏览器去打开这个index.html文件,自然会报错的,因为在index.js中需要调用的System.register这个方法,但我们并没有System更别谈register了。
所以我们对index.html改造下
- 先添加上SystemJs
<script src="https://cdn.bootcdn.net/ajax/libs/systemjs/6.10.1/system.min.js"></script>
- 提供React和React-dom的加载链接
// 这是systeJs规定的写法
<script type="systemjs-importmap">
{
"imports": {
"react": "https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.development.js",
"react-dom": "https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"
}
}
</script>
- 然后删掉加载index.js的那行script,并采用SystemJs的import方法。
<script>
System.import('./index.js');
</script>
整个index.html改造后的样子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
主应用 - 基座 - 用来加载子应用的
<script type="systemjs-importmap">
{
"imports": {
"react": "https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.development.js",
"react-dom": "https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/umd/react-dom.development.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');
</script>
</body>
最后我们使用浏览器打开这个html文件。
页面中很成功的出现了hello world。
结合改造后的html文件和js文件,现在大家应该比较清晰了,整个流程可以总结为以下这几步。
- 打包生成System.register(依赖列表, 回调函数返回值有一个setters和execute)
- react,react-dom 加载后调用setters 将对应的结果赋予给webpack
- 调用execute,执行页面渲染
再往细了的讲,我们调用setters,其实就是将需要特定方式加载的包的内容保存到一个execute能调用到的地方。
我们把这种加载机制往微前端方向去思考下,我们将html文件和excute类比为基座,将额外加载的react和react-dom比作子应用,excute调用react/react-dom类比做渲染子应用的内容,是不是觉得微前端通了?事实上,single-spa正是这么做的。
不过本文不讲single-spa,所以发散到此就ok了,下面我们来简单写一下SystemJs的register和import这两个核心方法。
简单实现
代码不多且比较简单,各处都标了注释,这里就直接贴全代码了。
const newMapUrl = {};
// 解析script标签,获取模块加载连接
function processScripts() {
Array.from(document.querySelectorAll('script')).forEach((script) => {
if (script.type === 'systemjs-importmap') {
const imports = JSON.parse(script.innerHTML).imports;
Object.entries(imports).forEach(([key, value]) => {
newMapUrl[key] = value;
});
}
})
}
let set = new Set();
// 先保存window上的属性 给window拍照
function saveGlobalProperty() {
for (let k in window) {
set.add(k);
}
}
saveGlobalProperty();
// 看下window上新增的属性
function getLastGlobalProperty() {
for (let k in window) {
if (set.has(k)) continue;
set.add(k);
return window[k];
}
}
let lastRegister;
function load(id) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
// 支持cdn
script.src = newMapUrl[id] || id;
script.async = true;
document.head.appendChild(script);
script.addEventListener('load', () => {
const _lastRegister = lastRegister;
lastRegister = null;
resolve(_lastRegister);
});
})
}
// 模块规范 用来加载System模块的
class SystemJS {
// id,模块路径,原则上可以是第三方路径 cdn
import(id) {
return Promise.resolve(processScripts()).then(() => {
// 1 去当前路径查找对应的资源 index.js
const lastSepIndex = location.href.lastIndexOf('/');
const baseURL = location.href.slice(0, lastSepIndex + 1);
if (id.startsWith('./')) {
return baseURL + id.slice(2);
}
}).then((id) => {
let execute;
// 根据路径加载资源
return load(id).then((register) => {
const { setters, execute: exe } = register[1](() => {});
execute = exe;
return [register[0], setters];
}).then(([registration, setters]) => {
return Promise.all(registration.map((dep, i) => {
return load(dep).then(() => {
// setters[i]拿到的是函数,加载资源后将加载后的模块传递给这个setter
// 加载完毕后,会在window上增添属性 例如React就会在window.React上,ReactDOM会在window.ReactDOM上
// window新增的属性
const property = getLastGlobalProperty();
setters[i](property);
});
}));
}).then(() => {
execute();
});
})
}
register(deps, declare) {
// 将回调的结果保存起来
lastRegister = [deps, declare];
}
}
const System = new SystemJS();
System.import('./index.js').then(() => {
console.log('模块加载完毕');
});
以上代码的主要内容大概为这些:
- 解析script标签,获得模块加载的链接
- 加载模块并放到window上
- 获取到setters和execute
- 执行setters
- 执行execute
至于细节部分,大家可复制上面的代码进行调试~
最后我们看下结果:
结尾
本文主要讲了systemJs是什么,怎么用,以及如何在微前端中发挥作用,最后还简单实现了下SystemJs两个核心方法import和register,希望本文对各位读者有所帮助。
最后,觉得本文不错的话,请不要吝啬手中的赞哦🌹🌹🌹
原文链接:https://juejin.cn/post/7347958786051538995 作者:猪头切图仔