-
Notifications
You must be signed in to change notification settings - Fork 2
一步一步实现最小化的AMD加载器
原文链接:https://curiosity-driven.org/minimal-loader?utm_source=echojs 翻译:jnotnull
AMD简化了JS的模块化应用,但是因为它还不是浏览器端内置的机制,因此还是需要一些引导过程。好在写了一个加载器插件,它在优化后不到850个字符。 这个加载器要实现如下功能:
- 可以运行在当前任何流行的浏览器。浏览器可以没有实现ES6的规范,例如Promises,
- 提供最基本的AMD方法:define和require,
- 被Uglify.js压缩后,应该尽可能的小,但是可读性要强,
- 能够被其他新的加载插件扩展,
- 和require.js要保持兼容,
为了验证加载器运行是否正常,设计了如下的单元测试用例:
define('framework', ['component', 'library'], function(cmp, lib) {
return { init: 'initialized:\ncomponent: ' + cmp.description +
'\nand library: ' + lib.version};
});
require(['framework'], function(framework) {
assert(framework.init === 'initialized:\ncomponent: uses library version: 0.0.1\nand library: 0.0.1');
});
define('library', [], function() {
return { version: '0.0.1' };
});
define('component', ['library'], function(lib) {
return { description: 'uses library version: ' + lib.version };
});
实现分为两个部分:内部函数和外部函数
内部函数负责依赖定义和监听器实现
- addLoadListener 用于当定义依赖的时候,注册一个可以执行的回调方法。如果之前已经定义过依赖的话,则可以立即执行改回调方法。
- resolve 通过传递依赖对象的名称,然后通知所有监听器去执行回调。
外部函数实现是相对于内部函数而言的,它负责把方法暴露给全局的window对象
- define:当依赖的模块都可用时,定义一个模块。
- require:当依赖都加载后,执行回调。
(function() {
var registry = {
listeners: { },
resolves: { }
};
function addLoadListener(name, listener) {
if (name in registry.resolves) {
// value is already loaded, call listener immediately
listener(name, registry.resolves[name]);
} else if (registry.listeners[name]) {
registry.listeners[name].push(listener);
} else {
registry.listeners[name] = [ listener ];
}
}
function resolve(name, value) {
registry.resolves[name] = value;
var libListeners = registry.listeners[name];
if (libListeners) {
libListeners.forEach(function(listener) {
listener(name, value);
});
delete registry.listeners[name];
}
}
window.require = function(deps, definition) {
if (deps.length === 0) {
// no dependencies, run definition now
definition();
} else {
// we need to wait for all dependencies to load
var values = [], loaded = 0;
function dependencyLoaded(name, value) {
values[deps.indexOf(name)] = value;
if (++loaded >= deps.length) {
definition.apply(null, values);
}
}
deps.forEach(function(dep) {
addLoadListener(dep, dependencyLoaded);
});
}
}
window.define = function(name, deps, definition) {
if (!definition) {
// just two arguments - bind name to value (deps) now
resolve(name, deps);
} else {
// asynchronous define with dependencies
require(deps, function() {
resolve(name, definition.apply(null, arguments));
});
}
}
}());
单元测试结果: passed.
使用Uglify.js可以压缩到524字符.
!function(){function e(e,n){e in i.resolves?n(e,i.resolves[e]):i.listeners[e]?i.listeners[e].push(n):i.listeners[e]=[n]}function n(e,n){i.resolves[e]=n;var s=i.listeners[e];s&&(s.forEach(function(i){i(e,n)}),delete i.listeners[e])}var i={listeners:{},resolves:{}};window.require=function(n,i){function s(e,s){l[n.indexOf(e)]=s,++r>=n.length&&i.apply(null,l)}if(0===n.length)i();else{var l=[],r=0;n.forEach(function(n){e(n,s)})}},window.define=function(e,i,s){s?require(i,function(){n(e,s.apply(null,arguments))}):n(e,i)}}();
虽然原始实现可以正常运行,而且已经很小了。然而通过重组,Uglify.js可以让他变的更小。
通过分析压缩后的结果,可以看到有几个名字并没有被缩短:listeners和resolves。Uglify没有去改变他们是因为把它当成变量registry的一个属性了。如果这个变量被外部代码引用,就会造成名称不对应,引起无法预知的错误。Uglify不知道它是一个内部变量,因此不会对其进行压缩。一个解决的方法就是针对内部对象使用手工去缩小它的名字。
另一个解决方法是扁平化对象的结构。我们不再使用把listeners和resolves注册到对象的方法,我们直接定义变量。 使用扁平化后的注册对象后的实现(只修改了addLoadListener和resolve)
(function() {
var listeners = { }, resolves = { };
function addLoadListener(name, listener) {
if (name in resolves) {
// value is already loaded, call listener immediately
listener(name, resolves[name]);
} else if (listeners[name]) {
listeners[name].push(listener);
} else {
listeners[name] = [ listener ];
}
}
function resolve(name, value) {
resolves[name] = value;
var libListeners = listeners[name];
if (libListeners) {
libListeners.forEach(function(listener) {
listener(name, value);
});
delete listeners[name];
}
}
window.require = function(deps, definition) {
if (deps.length === 0) {
// no dependencies, run definition now
definition();
} else {
// we need to wait for all dependencies to load
var values = [], loaded = 0;
function dependencyLoaded(name, value) {
values[deps.indexOf(name)] = value;
if (++loaded >= deps.length) {
definition.apply(null, values);
}
}
deps.forEach(function(dep) {
addLoadListener(dep, dependencyLoaded);
});
}
}
window.define = function(name, deps, definition) {
if (!definition) {
// just two arguments - bind name to value (deps) now
resolve(name, deps);
} else {
// asynchronous define with dependencies
require(deps, function() {
resolve(name, definition.apply(null, arguments));
});
}
}
}());
单元测试结果: passed.
使用Uglify.js压缩到428字符.
!function(){function n(n,e){n in t?e(n,t[n]):i[n]?i[n].push(e):i[n]=[e]}function e(n,e){t[n]=e;var l=i[n];l&&(l.forEach(function(i){i(n,e)}),delete i[n])}var i={},t={};window.require=function(e,i){function t(n,t){l[e.indexOf(n)]=t,++r>=e.length&&i.apply(null,l)}if(0===e.length)i();else{var l=[],r=0;e.forEach(function(e){n(e,t)})}},window.define=function(n,i,t){t?require(i,function(){e(n,t.apply(null,arguments))}):e(n,i)}}();
(译者注:关于函数的不完全调用,可以参考正在翻译的 JavaScript中的不完全函数调用 ) 一些匿名函数可以通过使用bind生成不完全调用函数,一个例子就是上面的window.require里面的匿名函数。 不幸的是,因为bind采取的按照参数的左最多参数原则-首先是listener,然后是name 原有代码:
deps.forEach(function(dep) {
addLoadListener(dep, dependencyLoaded);
});
修改参数顺序后代码:
deps.forEach(function(dep) {
addLoadListener(dependencyLoaded, dep);
});
通过使用不完全调用函数,我们就可以删除掉之前的匿名函数了
deps.forEach(addLoadListener.bind(null, dependencyLoaded));
bind方法返回一个函数,他会使用两个参数dependencyLoaded和value去调用addLoadListener方法 因为dependencyLoaded没有在其他地方被引用,所以我们修改下它的位置
deps.forEach(addLoadListener.bind(null, function(name, value) {
values[deps.indexOf(name)] = value;
if (++loaded >= length) {
definition.apply(null, values);
}
}));
通过观察优化后的测试结果可以发现,require函数的dependencies变量的length属性重复了两次。length属性不能够被优化,但是通过使用一个变量去存储和读取,可以节省一些字节。 The comparison of length with 0 can be minimized to !length as 0 is falsy leading to this code: 因为长度为0的话代表false,所以我们可以通过比较长度和0的关系来优化如下代码
window.require = function(deps, definition) {
var length = deps.length;
if (!length) {
// no dependencies, run definition now
definition();
} else {
// we need to wait for all dependencies to load
var values = [], loaded = 0;
deps.forEach(addLoadListener.bind(null, function(name, value) {
values[deps.indexOf(name)] = value;
if (++loaded >= length) {
definition.apply(null, values);
}
}));
}
}
这些不走可以减少压缩的字节,但是可能降低代码的可读性 移除对window对象的赋值-在非严格模式下,赋值给未定义的变量会导致赋值给全局对象。 替换apply和bind方法中的null为0-因为参数在这些函数中并没有被用到,这样的替换可以为每个参数节省3个字符。 修改delete listeners[name]为listeners[name] = 0-这样可以节省3个字符,同时也不影响垃圾回收。 命名require方法名为req,同时在define中也使用这个名字-这个内部的名字可以被优化,这样可以节省3个字符。 最终优化后代码:
(function() {
var listeners = { }, resolves = { };
function addLoadListener(listener, name) {
if (name in resolves) {
// value is already loaded, call listener immediately
listener(name, resolves[name]);
} else if (listeners[name]) {
listeners[name].push(listener);
} else {
listeners[name] = [ listener ];
}
}
function resolve(name, value) {
resolves[name] = value;
var libListeners = listeners[name];
if (libListeners) {
libListeners.forEach(function(listener) {
listener(name, value);
});
// remove listeners (delete listeners[name] is longer)
listeners[name] = 0;
}
}
function req(deps, definition) {
var length = deps.length;
if (!length) {
// no dependencies, run definition now
definition();
} else {
// we need to wait for all dependencies to load
var values = [], loaded = 0;
deps.forEach(addLoadListener.bind(0, function(name, value) {
values[deps.indexOf(name)] = value;
if (++loaded >= length) {
definition.apply(0, values);
}
}));
}
}
/** @export */
require = req;
/** @export */
define = function(name, deps, definition) {
if (!definition) {
// just two arguments - bind name to value (deps) now
resolve(name, deps);
} else {
// asynchronous define with dependencies
req(deps, function() {
resolve(name, definition.apply(0, arguments));
});
}
}
}());
使用Uglify.js可以压缩到386字符.
!function(){function n(n,e){e in u?n(e,u[e]):t[e]?t[e].push(n):t[e]=[n]}function e(n,e){u[n]=e;var i=t[n];i&&(i.forEach(function(i){i(n,e)}),t[n]=0)}function i(e,i){var t=e.length;if(t){var u=[],f=0;e.forEach(n.bind(0,function(n,o){u[e.indexOf(n)]=o,++f>=t&&i.apply(0,u)}))}else i()}var t={},u={};require=i,define=function(n,t,u){u?i(t,function(){e(n,u.apply(0,arguments))}):e(n,t)}}();
加载插件不仅仅可以被用来管理脚本依赖,也可以用于其他方面的加载需求。例如CodeMirror需要样式在内置编辑器之前呈现出来。 依赖名可以包含感叹号(!),用来表示哪个插件在工作-
css!codemirror/styles.css
function addLoadListener(listener, name) {
if (name in resolves) {
// value is already loaded, call listener immediately
listener(name, resolves[name]);
} else if (listeners[name]) {
listeners[name].push(listener);
} else {
listeners[name] = [ listener ];
// first time this dependency is requested
// get the loader name from string before ! character
req([ 'js!' + name.split('!')[0] ], function (loader) {
loader(name);
});
}
}
下面的代码定义了两个插件: js-通过添加脚本标签,用来加载外部js模块.这些模块会包含define方法,因此这个加载器很简单。 css-用于样式。在返回这个插件之前,会校验是否所有样式已经生效。全部生效后,会返回。
(function(document, define, setTimeout) {
function addElement(name, properties) {
var element = document.createElement(name);
for (var item in properties) {
element[item] = properties[item];
}
document.head.appendChild(element);
}
define('js!js', function(name) {
var fileName = name.split('!')[1];
addElement('SCRIPT', {
src: fileName
});
});
define('js!css', function(name) {
var fileName = name.split('!')[1];
addElement('LINK', {
href: fileName,
rel: 'stylesheet',
onload: function check() {
for (var i = 0, sheet; sheet = document.styleSheets[i]; i++) {
if (sheet.href && (sheet.href.indexOf(fileName) > -1)) {
return define(name);
}
}
// style is loaded but not being applied yet
setTimeout(check, 50);
}
});
});
// require dependencies specified in
使用Uglify.js压缩到463个字符.
!function(n,e,i){function t(e,i){var t=n.createElement(e);for(var r in i)t[r]=i[r];n.head.appendChild(t)}e("js!js",function(n){var e=n.split("!")[1];t("SCRIPT",{src:e})}),e("js!css",function(r){var f=r.split("!")[1];t("LINK",{href:f,rel:"stylesheet",onload:function o(){for(var t,u=0;t=n.styleSheets[u];u++)if(t.href&&t.href.indexOf(f)>-1)return e(r);i(o,50)}})}),i(require.bind(0,n.body.getAttribute("data-load").split(" "),Date),0)}(document,define,setTimeout);
这个代码片段使用了几个新的技术去减少了输出字节
被使用超过一次的全局变量都被拷贝到函数的参数,因此可以节省30个字符
(function(document, define, setTimeout) {
// document, define and setTimeout are used more than once here
// as they are arguments Uglify.js will minimize each usage
}(document, define, setTimeout));
require函数需要两个参数-dependencies和一个回调函数,这个回调函数用于在依赖都加载后的执行。因为依赖. As dependencies are not used here instead of using empty function (function(){}) a side-effect free Date function is used saving 8 characters.
require(document.body.getAttribute('data-load').split(' '), Date);
通过settimeout可以实现require方法的延时执行。这就意味这个浏览器可以使用内联样式去先渲染初始化页面布局,然后再去加载剩下的部分。交互脚本和新增的样式都可以从标签路径中移除了. 为了防止settimeout有匿名函数,我们在require里面创建了一个新的函数不完全调用。这个心的函数参数最少,调用的时候会传递依赖的长度和回调函数Date。
setTimeout(require.bind(0,
document.body.getAttribute('data-load').split(' '), Date), 0);
压缩后的loader只有849个字符,但是如果你有更好的方法去减少字符,那就赶紧提交你的建议吧。 当前这个最小化的loader已经用在了当前blog中(查看本页最后的html代码)