最近几年微前端的兴起,带火了single-spa,很多微前端的技术方案都是基于它去实现的。但是single-spa自身并没有实现模块加载的能力,官方在The Recommended Setup中推荐了使用SystemJS来进行微前端应用的加载。
在SystemJS的GitHub主页上,关于自己是这么介绍的:
Dynamic ES module loader.
也就是说,它对自己的定位是一个动态的ES Module加载器。SystemJS主要实现了:
- 兼容ES modules的模块格式,能够在ES5的环境中执行
- 基于import-maps的模块映射
所以,在开始分析SystemJS之前,我们先来介绍一下ES modules和Import-Maps.
ES Modules
在ES6(ES2015)中提出的模块标准——ES Modules(ESM)已经逐渐在前端开发中普及开来。它在设计思想上是尽可能的静态化;在语言标准的层面上,实现了模块功能,正在逐渐替代CommonJS
和AMD
两种规范,成为浏览器端和服务器端通用的模块解决方案。
最近在构建工具领域内很火的vite和snowpack, 也是基于这种特性去实现的。关于ES Modules的更多知识,可以参考阮一峰的教程——Module的语法。
1. 模块的加载
在支持ES Modules的浏览器中,我们可以对script
标签指定type="module"
来直接加载ESM格式的JavaScript代码,例如:
// hello.js
export default function sayHello() {
const result = 'Hello, ES modules!'
console.log(result)
return result
}
function welcome(user) {
const result = 'Welcome to es modules, ' + user
console.log(result)
return result
}
export {
welcome
}
在HTML中,我们可以通过以下方式来引入这段js代码:
<script type="module">
// 直接引入hello.js
import sayHello, { welcome } from './hello.js'
// 动态引入hello.js
import('./hello.js').then(module => {
const { default: sayHello, welcome } = module
sayHello()
welcome('alex')
})
</script>
2. import.meta
import.meta
对象属于ECMAScript标准的一部分,是一个给JavaScript模块暴露特定上下文的元数据属性的对象,它带有一个null
的原型对象。
import.meta包含了这个模块的信息,比如说url
. 同时,这个对象是可以扩展的,并且它的属性都是可写,可配置和可枚举的。
获取url:
<script type="module" src="my-module.mjs"></script>
console.log(import.meta.url); // { url: "file:///home/user/my-module.mjs" }
在Node.js的文档中,还提到了一个实验性的特性——import.meta.resolve
:
const dependencyAsset = await import.meta.resolve('component-lib/asset.css');
await import.meta.resolve('./dep', import.meta.url);
3. 支持情况
浏览器端
具体的浏览器支持情况,可以参见JavaScript modules via script tag.
服务器端
从8.5版本开始,我们已经可以通过--experimental-modules
标签获得对ESM的实验性支持;关于Node.js的最新支持情况,具体可以参见Modules:ECMA modules.
受限于浏览器对于ES module的兼容性,即使是vite/snowpack在build的时候,也是基于rollup/webpack去实现的;而SystemJS,则提供了一种在ES5环境下兼容ES modules的格式——System Register Format,方便我们来进行模块的加载。
Import Maps
import maps是[WICG]((www.w3.org/community/w…
Deno已经内置了对于import-maps的支持,类似如下:
// import_map.json
{
"imports": {
"fmt/": "https://deno.land/std@0.106.0/fmt/"
}
}
// color.ts
import { red } from "fmt/colors.ts";
console.log(red("hello world"));
通过import maps的配置,我们就可以直接去根据模块名称来引入具体的模块,而不用关心它是存在于node_modules中还是在CDN上。
在浏览器中,可以声明如下的配置:
<script type="importmap">
{
"imports": {
"moment": "/node_modules/moment/src/moment.js",
"lodash": "/node_modules/lodash-es/lodash.js"
}
}
</script>
如果我们引入以上模块:
import moment from "moment";
import { partition } from "lodash";
就等同于:
import moment from "/node_modules/moment/src/moment.js";
import { partition } from "/node_modules/lodash-es/lodash.js";
SystemJS中内置了对于Import Maps的支持;对于SystemJS,我们需要设置script的标签为:
<script type="systemjs-importmap">
Import Maps的规范中更多的细节,可以参考import-map的GitHub和wicg的import-maps草案。
SystemJS
SystemJS是一个基于标准的、插件化的模块加载器。它可以做到:
- 将原生的ES module的JavaScript代码转换成System.register模块格式,以在不能支持ES module的旧浏览器(最低是IE11)中运行我们的代码
- 借助于ImportMap标准,实现了module名称和module的url的映射,以及通过名称来加载模块
System.register模块格式
system.register提供了在ES5环境中对于ES6的语法支持,具体如下:
- dynamic import()
- import.meta
- top-level await
- live binding
- circular reference include function hositing
下面是一个大致的system.register模块格式的结构:
System.register([...deps...], function (_export, _context) {
return {
setters: [...setters...],
execute: function () {
}
};
});
其中:
- deps:模块的依赖
- setters:模块依赖的回调,和deps一一对应;当依赖更新时调用
- _export:
(name: String, value: any) => value
,用来导出绑定的函数 - _context:
- _context.meta 语义上等同于ESM中的
import.meta
- _context.import 语义用等同于esm中的import函数,用来支持动态引入
- _context.meta 语义上等同于ESM中的
如下为一个更为具体的例子:
<script type="system-importmap">
{
"imports": {
"react": "https://unpkg.com/react@17/umd/react.production.min.js",
"react-dom": "https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"
}
}
</script>
System.register(["react","react-dom"], function(__WEBPACK_DYNAMIC_EXPORT__) {
var __WEBPACK_EXTERNAL_MODULE_react__, __WEBPACK_EXTERNAL_MODULE_react_dom__;
return {
setters: [
function(module) {
__WEBPACK_EXTERNAL_MODULE_react__ = module;
},
function(module) {
__WEBPACK_EXTERNAL_MODULE_react_dom__ = module;
}
],
execute: function() {
__WEBPACK_DYNAMIC_EXPORT__(
//...
/***/ "react-dom":
/*!****************************!*\
!*** external "react-dom" ***!
\****************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = __WEBPACK_EXTERNAL_MODULE_react_dom__;
/***/ })
//...
)
}
};
});
模块的使用
1. script标签
我们可以在html中直接通过script标签,设置type="systemjs-module"
进行引入:
// 引入systemjs
<script src="system.js"></script>
// 用type="systemjs-module"去标识引入的JavaScript代码是System.register格式
<script type="systemjs-module" src="/js/main.js"></script>
<script type="systemjs-module" src="import:name-of-module"></script>
// 引入systemjs
<script src="system.js"></script>
// 用type="systemjs-module"去标识引入的JavaScript代码是System.register格式
<script type="systemjs-module" src="/js/main.js"></script>
<script type="systemjs-module" src="import:name-of-module"></script>
2. 动态引入
如果需要动态引入的话,我们可以使用System.import
:
// 必须是一个System.register格式的模块
System.import('/aSystemRegisterModule.js');
与构建工具的整合
相对于直接使用ESM格式的JavaScript,我们需要直接把我们的应用编译成System.register format,这样才能直接被SystemJS识别。
在我们的构建工具中,我们也需要配置对应的输出格式:
- 在webpack中,设置
libraryTarget
为"system"
- 在rollup中,设置
format
为"system"