SystemJS: 如何注册全局脚本
有如下可以正常运行的代码,功能很简单,加载 React
, ReactDOM
,然后在 app
中渲染 React 元素:
<div id="app"></div>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.7.1/system.js"></script>
<script>
(async () => {
const React = await System.import(
"//cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.development.js"
);
const ReactDOM = await System.import(
"https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.development.min.js"
);
const { createElement: h } = React;
const container = document.getElementById("app");
ReactDOM.render(h("p", {}, "Hello world."), container);
})();
</script>
一切看上去很正常,ちょっと待って,引入的两个模块都是 UMD 的,为什么 SystemJS 可以正常注册模块呢?
从上图我们可以看出在 React 模块初始化时,exports
, module
, define
都是不存在的,React 直接被添加到了全局对象 window
上。
通过断点,我找到了这段代码(剔除其中无关代码):
/*
* SystemJS global script loading support
* Extra for the s.js build only
* (Included by default in system.js build)
*/
(function (global) {
var systemJSPrototype = global.System.constructor.prototype;
// safari unpredictably lists some new globals first or second in object order
var firstGlobalProp, secondGlobalProp, lastGlobalProp;
function getGlobalProp() {
var cnt = 0;
var lastProp;
for (var p in global) {
// do not check frames cause it could be removed during import
if (shouldSkipProperty(p)) continue;
if (
(cnt === 0 && p !== firstGlobalProp) ||
(cnt === 1 && p !== secondGlobalProp)
)
return p;
cnt++;
lastProp = p;
}
if (lastProp !== lastGlobalProp) return lastProp;
}
function noteGlobalProps() {
// alternatively Object.keys(global).pop()
// but this may be faster (pending benchmarks)
firstGlobalProp = secondGlobalProp = undefined;
for (var p in global) {
// do not check frames cause it could be removed during import
if (shouldSkipProperty(p)) continue;
if (!firstGlobalProp) firstGlobalProp = p;
else if (!secondGlobalProp) secondGlobalProp = p;
lastGlobalProp = p;
}
return lastGlobalProp;
}
var impt = systemJSPrototype.import;
systemJSPrototype.import = function (id, parentUrl) {
noteGlobalProps();
return impt.call(this, id, parentUrl);
};
// balabala
var isIE11 =
typeof navigator !== "undefined" &&
navigator.userAgent.indexOf("Trident") !== -1;
function shouldSkipProperty(p) {
return (
!global.hasOwnProperty(p) ||
(!isNaN(p) && p < global.length) ||
(isIE11 &&
global[p] &&
typeof window !== "undefined" &&
global[p].parent === window)
);
}
})(typeof self !== "undefined" ? self : global);
可以看到,每次 SystemJS 在 import 新的模块前,会调用 noteGlobalProps
检查当前全局的属性(亦可使用 Object.keys
),记录下最后一条属性。当模块加载完成后,再调用 getGlobalProp
获得当前全局对象中的最后一条属性,若此属性和加载前的得到的最后一条属性不同,则认为此属性是新加载的模块。需要注意的 Safari 有可能将新的属性放在第一或第二位,所以要特殊处理。
综上,我们可以知晓, SystemJS 是通过检查全局对象中新增的属性来注册全局的脚本的。
那么就有了一个新的问题,如果我们在模块加载过程中不断的向全局对象新增属性,是否会影响模块的注册呢?
通过在 getGlobalProp
断点,观察调用栈我们可以发现在 JavaScript 脚本 load
完成后,从 getRegister
到 getGlobalProp
的过程都是同步的 Task,所以模块加载过程中动态向全局对象增加属性不会影响模块的注册。