深入分析single-spa——模块加载

856 阅读4分钟

最近几年微前端的兴起,带火了single-spa,很多微前端的技术方案都是基于它去实现的。但是single-spa自身并没有实现模块加载的能力,官方在The Recommended Setup中推荐了使用SystemJS来进行微前端应用的加载。

在SystemJS的GitHub主页上,关于自己是这么介绍的:

Dynamic ES module loader.

也就是说,它对自己的定位是一个动态的ES Module加载器。SystemJS主要实现了:

  1. 兼容ES modules的模块格式,能够在ES5的环境中执行
  2. 基于import-maps的模块映射

所以,在开始分析SystemJS之前,我们先来介绍一下ES modules和Import-Maps.

ES Modules

在ES6(ES2015)中提出的模块标准——ES Modules(ESM)已经逐渐在前端开发中普及开来。它在设计思想上是尽可能的静态化;在语言标准的层面上,实现了模块功能,正在逐渐替代CommonJSAMD两种规范,成为浏览器端和服务器端通用的模块解决方案。

最近在构建工具领域内很火的vitesnowpack, 也是基于这种特性去实现的。关于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.

1636775838(1).png

服务器端

从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函数,用来支持动态引入

如下为一个更为具体的例子:

<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"

参考文档:

systemjs

Module的语法