SystemJS 解析

570 阅读4分钟

介绍

SystemJS 是由 Guy Bedford 创建的一个模块加载器,它诞生于 2014 年左右,正值 JavaScript 模块化标准尚未完全统一,开发者需要在 AMD、CommonJS、ES6 模块等多种模块格式之间切换的时期。SystemJS 的目标是提供一个高度灵活的模块加载解决方案,能够无痛地支持多种模块格式,并且能适应从旧的浏览器环境到最新的 JavaScript 标准的过渡。

使用示例

在 HTML 中使用 systemjs-importmap 导入依赖

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>SystemJS AMD Modules from CDN Example</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <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="../dist/system.js"></script>

</head>
<body>
  <div id="react-root"></div>
  <script type="systemjs-module" src="../dist/hello-world.js"></script>
</body>
</html>

查看更多示例

核心 API

System.import(id [, parentURL]) -> Promise(Module)

<script type="systemjs-module" src="/js/main.js"></script>

// System.import('/js/main.js');

System.addImportMap(map [, base])

<script type="systemjs-importmap">
{
  "imports": {
    "lodash": "https://unpkg.com/lodash@4.17.10/lodash.js"
  }
}
</script>

<!-- <script type="systemjs-importmap" src="path/to/map.json" crossorigin="anonymous"</script>-->

  // System.addImportMap({
  //   imports: {
  //     lodash: 'https://unpkg.com/lodash@4.17.10/lodash.js',
  //   },
  // });

System.set(id, module) -> Module

System.set('http://site.com/normalized/module/name.js');

System.register 一般结构如下

System.register([...deps...], function (_export, _context) {
  return {
    setters: [...setters...],
    execute: function () {

    }
  };
}, [...metas...],);
  • deps: String[]: 依赖项
  • setters: Function[]: 每当依赖项更新时要调用的函数数组,其索引顺序与 deps 相同。对于没有导出的依赖项,setter 函数可以未定义。
  • execute: Function | AsyncFunction: 这是在代码执行的精确点调用的,而外部包装器则在早期调用,从而允许包装器在执行之前导出函数声明;如果使用异步函数进行执行,则按照规范的变体提供顶级等待执行支持语义。
  • _export: ({ [name: String]: any } | (name: String, value: any)) => value: 可以使用导出函数的直接形式来导出绑定 _export('exportName', value) 它返回设置的值,以便在表达式中使用;还可以批量导出函数允许设置导出对象。通过减少使用时的设置函数调用来提高性能,因此在实现中应尽可能使用批量导出函数,而不是直接导出。
  • _context.meta: Object import.meta:这是一个表示模块执行值的对象。默认情况下,它将import.meta.url存在于 SystemJS 中。
  • _context.import: (id: String) => Promise<Module>:这是可供模块替代的上下文动态导入功能import()
  • metas: Object[]:附加到模块依赖项的元数据,按与 相同的顺序进行索引deps。这是一个可选参数。

hello-world.js

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(React.createElement("button", null, "A button created by React"), document.getElementById('react-root'));
    }
  };
});

加载流程

加载流程

Untitled Diagram.png

...
envGlobal.System = new SystemJS();
...
 if (hasDocument) {
    window.addEventListener('DOMContentLoaded', processScripts);
  }
...
  function processScripts () {
    [].forEach.call(document.querySelectorAll('script'), function (script) {
    ...
      if (script.type === 'systemjs-module') {
      ...
        System.import(script.src.slice(0, 7) === 'import:' ? script.src.slice(7) : resolveUrl(script.src, baseUrl));
        ...
      }
      else if (script.type === 'systemjs-importmap') {
      ...
        var fetchPromise = script.src ? (System.fetch || fetch)(script.src) : script.innerHTML;
        ...
        importMapPromise = importMapPromise.then(function () {
          return fetchPromise;
        }).then(function (text) {
          extendImportMap(importMap, text, script.src || baseUrl);
        });
      }
    });
  }

image.png

import 机制

  systemJSPrototype.import = function (id, parentUrl, meta) {
    var loader = this;
    (parentUrl && typeof parentUrl === 'object') && (meta = parentUrl, parentUrl = undefined);
    return Promise.resolve(loader.prepareImport())
    .then(function() {
      return loader.resolve(id, parentUrl, meta);
    })
    .then(function (id) {
      var load = getOrCreateLoad(loader, id, undefined, meta);
      return load.C || topLevelLoad(loader, load);
    });
  };

Untitled Diagram.png

systemJSPrototype.import 方法是 SystemJS 中用于异步导入模块的核心函数。它接受三个参数:id(模块标识符),parentUrl(可选的父模块URL,用于确定模块的相对路径),以及meta(元数据,如模块的类型或其他信息)

  • 参数处理:如果传入的 parentUrl 实际上是一个对象,那么它的值被赋给 meta,而 parentUrl 被重置为 undefined。这允许你以一个对象的形式传递 meta 数据,其中可能包括 parentUrl
  • 准备导入:使用当前的加载器实例(loader)调用 prepareImport 方法,这是一个异步操作,内部调用 processScripts方法做开始导入前的再次确认。结果是一个 Promise,当 prepareImport 完成时会被解析。
  • 解析模块路径:一旦 prepareImport 成功完成,下一个 then 方法会调用 loader.resolve 来解析模块标识符 id 到一个绝对的模块路径。resolve 方法会考虑 parentUrl 和 meta 来正确解析模块路径。
  • 获取或创建加载实例:解析成功后,得到的模块路径被用来调用 getOrCreateLoad 方法。这个方法会查找或创建一个代表模块加载过程的 load 对象。如果模块已经被加载过,load 将直接返回先前的加载实例;否则,会创建一个新的加载实例。
  • 执行加载:最后,如果 load 实例已经有一个表示完成状态的 C 属性(即模块已经完成加载),那么直接返回这个 C 值。如果没有,调用 topLevelLoad 函数,这个函数负责实际执行模块的加载和执行流程。

image.png

总结

按上述的思路,那么微前端是不是也是一个远程模块?

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