SystemJS 探秘

背景

SystemJS 诞生于 2015 年,那个时候 ES Module 还未成为标准,在浏览器端只能通过 requirejs、seajs 等方案实现模块加载,随着 npm 在前端界的流行,一个项目中可能存在多种模块规范,所以我认为 SystemJS 最初诞生的目的是为了做一个通用的模块加载器,在浏览器端实现对 CommonJS、AMD、UMD 等各种模块的加载。

后来,随着 ES6 的普及,越来越多的浏览器开始支持 ES Module, SystemJS 是现阶段下(浏览器尚未正式支持importMap)原生 ES Module 的替代品,ES Module 被编译成 System.register 格式之后能够跑在旧版本的浏览器当中。 ​

特性

Import Maps

<script type="module">
    import moment from "moment";
    import { partition } from "lodash";
</script>

这样写会报错,原因是在浏览器中,import 必须给出相对或绝对的 URL 路径。没有任何路径的模块被称为“裸(bare)”模块。在 import 中不允许这种模块。 某些环境,像 Node.js 或者打包工具允许没有任何路径的裸模块,因为它们有自己查找模块的方法。但是浏览器尚不支持裸模块。

但是如果有了 Import Maps

<script type="importmap">
{
  "imports": {
    "moment": "/node_modules/moment/src/moment.js",
    "lodash": "/node_modules/lodash-es/lodash.js"
  }
}
</script>

上面的写法就能被解析为:

<script type="module">
    import moment from "/node_modules/moment/src/moment.js";
    import { partition } from "/node_modules/lodash-es/lodash.js";
</script>

Import maps 在 Chrome 74 中可以通过实验性质开启,本质上是一个配置文件,可以让开发者将模块标识符映射到一到多个其他标识符的机制,这个机制非常强大,它赋予了开发者在运行时针对指定的模块动态修改浏览器实际获取模块的能力。该配置文件描述了依赖的解析方式,某种程度上,Import maps 给浏览器端带来了包管理,但是目前支持 Import Maps 的浏览器还很少

如果想要使用这个特性的话,需要引入 SystemJS

<script type="systemjs-importmap">
  {
    "imports": {
      "moment": "https://cdn.jsdelivr.net/npm/moment/dist/moment.js",
      "lodash": "https://cdn.jsdelivr.net/npm/lodash/dist/lodash.js"
    }
  }
</script>
<script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.js"></script>

应用

single-spa

single-spa 是一个将多个单页面应用聚合为一个整体应用的 JavaScript 微前端框架

single-spa 的使用过程中,我们需要用 import-map 在根项目中引入所有的模块文件和子项目,从而在其余项目中可以进行模块的引用,就像上面说的那样,可以把 moment 想象成一个子项目。

<script type="systemjs-importmap">
  {
    "imports": {
      "module": "https://[cdn-link].js",
    }
  }
</script>
<script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.js"></script>

import-map 里的文件放到一个 json 当中,就变成了

<script type="systemjs-importmap" src="https://[bucket]/import-map.json"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.js"></script>

我们开发者需要做的,就是把模块文件打包成单一的 cdn 文件,然后引入 import-map.json,实现子模块的引入。

Bundless

ESModule

使用 type="module" 开启 ESModule

<script type="importmap">
  {
    "imports": {
      "react": "https://airpack.alibaba-inc.com/react",
      "react-dom": "https://airpack.alibaba-inc.com/react-dom"
    }
  }
</script>

<div id="app"></div>

<script type="module">
  import React from 'react'
  import ReactDOM from 'react-dom'

  ReactDOM.render('Hello React', document.getElementById('root'))
</script>

这种方式的缺点是需要所有模块都导出成 ESModule,当前社区当中的很多模块都没有导出成 ESModule,有些模块甚至没有经过编译,所以目前使用 ESModule 实现 bundless 仍然有一定困难。

SystemJS

StackBlitz 是社区非常著名的一个在线 WebIDE,它就是基于 SystemJS 和 Unpkg 实现 bundless。

Our architecture is based entirely on SystemJS & Unpkg and uses no code from CodeSandbox or the Webpack bundling service ya’ll made. From the start, our goal was to port VS Code, NPM, and Webpack loaders to run _entirely in your browser and still work offline. _With StackBlitz, the browser is installing, bundling, and serving everything — our servers don’t do any of that. More info on this can be found here.

下面是使用 SystemJS 实现类似 ESModule 的示例,区别是不需要模块导出成 ESModule,可以使用 UMD 模块。

<script type="systemjs-importmap">
  {
    "imports": {
      "react": "https://cdn.jsdelivr.net/npm/react/umd/react.production.min.js",
      "react-dom": "https://cdn.jsdelivr.net/npm/react-dom/umd/react-dom.production.min.js"
    }
  }
</script>
<script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.min.js"></script>

<div id="app"></div>

<script type="systemjs-module">
  System.register(["react", "react-dom"], function (_export, _context) {
    "use strict";

    var React, ReactDOM;
    return {
      setters: [function (_react) {
        React = _react.default;
      }, function (_reactDom) {
        ReactDOM = _reactDom.default;
      }],
      execute: function () {
        ReactDOM.render('Hello React', document.getElementById('root'));
      }
    };
  });
</script>

总结

SystemJS 是时代的产物,现阶段下也仍然是原生 ESModule 的替代品,但是不可否认,未来一定是 ESModule 的世界。

发布于 2021-08-21 17:23