如果你是 JavaScript 的新手,一些像 “module bundlers vs module loaders”、“Webpack vs Browserify” 和 “AMD vs.CommonJS” 这样的术语,很快让你不堪重负。
JavaScript 模块系统可能令人生畏,但理解它对 Web 开拓职员至关主要。
在这篇文章中,我将以大略的言语(以及一些代码示例)为你阐明这些术语。 希望这对你有会有帮助!
什么是模块?
好作者能将他们的书分成章节,精良的程序员将他们的程序划分为模块。
就像书中的章节一样,模块只是笔墨片段(或代码,视情形而定)的集群。然而,好的模块是高内聚低松耦的,具有不同的功能,许可在必要时对它们进行更换、删除或添加,而不会扰乱整体功能。
为什么利用模块?利用模块有利于扩展、相互依赖的代码库,这有很多好处。在我看来,最主要的是:
1)可掩护性: 根据定义,模块是高内聚的。一个设计良好的模块旨在尽可能减少对代码库部分的依赖,这样它就可以独立地增强和改进,当模块与其他代码片段解耦时,更新单个模块要随意马虎得多。
回到我们的书的例子,如果你想要更新你书中的一个章节,如果对一个章节的小改动须要你调度每一个章节,那将是一场噩梦。相反,你希望以这样一种办法编写每一章,即可以在不影响其他章节的情形下进行改进。
2)命名空间: 在 JavaScript 中,顶级函数范围之外的变量是全局的(这意味着每个人都可以访问它们)。因此,“名称空间污染”很常见,完备不干系的代码共享全局变量。
在不干系的代码之间共享全局变量在开拓中是一个大禁忌。正如我们将在本文后面看到的,通过为变量创建私有空间,模块许可我们避免名称空间污染。
3)可重用性:坦白地说:我们将前写过的代码复制到新项目中。 例如,假设你从之前项目编写的一些实用程序方法复制到当前项目中。
这统统都很好,但如果你找到一个更好的方法来编写代码的某些部分,那么你必须记得回去在曾经利用过的其他项目更新它。
这显然是在摧残浪费蹂躏韶光。如果有一个我们可以一遍又一各处重复利用的模块,不是更随意马虎吗?
如何创建模块?有多种方法来创建模块,来看几个:
模块模式模块模式用于仿照类的观点(由于 JavaScript 本身不支持类),因此我们可以在单个工具中存储公共和私有方法和变量——类似于在 Java 或 Python 等其他编程措辞中利用类的办法。这许可我们为想要公开的方法创建一个面向公共的 API,同时仍旧将私有变量和方法封装在闭包范围中。
有几种方法可以实现模块模式。在第一个示例中,将利用匿名闭包,将所有代码放在匿名函数中来帮助我们实现目标。(记住:在 JavaScript 中,函数是创建新浸染域的唯一方法。)
例一:匿名闭包
(function () { // 将这些变量放在闭包范围内实现私有化 var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item}, 0); return '均匀分 ' + total / myGrades.length + '.'; } var failing = function(){ var failingGrades = myGrades.filter(function(item) { return item < 70;}); return '挂机科了 ' + failingGrades.length + ' 次。'; } console.log(failing()); // 挂机科了次}());
利用这个构造,匿名函数就有了自己的实行环境或“闭包”,然后我们立即实行。这让我们可以从父(全局)命名空间隐蔽变量。
这种方法的优点是,你可以在这个函数中利用局部变量,而不会意外地覆盖现有的全局变量,但仍旧可以访问全局变量,就像这样:
var global = '你好,我是一个全局变量。)'; (function () { // 将这些变量放在闭包范围内实现私有化 var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item}, 0); return '均匀分 ' + total / myGrades.length + '.'; } var failing = function(){ var failingGrades = myGrades.filter(function(item) { return item < 70;}); return '挂机科了 ' + failingGrades.length + ' 次。'; } console.log(failing()); // 挂机科了次 onsole.log(global); // 你好,我是一个全局变量。 }());
把稳,匿名函数的圆括号是必需的,由于以关键字 function 开头的语句常日被认为是函数声明(请记住,JavaScript 中不能利用不决名的函数声明)。因此,周围的括号将创建一个函数表达式,并立即实行这个函数,这还有另一种叫法 立即实行函数(IIFE)。如果你对这感兴趣,可以在这里理解到更多。
例二:全局导入
jQuery 等库利用的另一种盛行方法是全局导入。它类似于我们刚才看到的匿名闭包,只是现在我们作为参数传入全局变量:
(function (globalVariable) { // 在这个闭包范围内保持变量的私有化 var privateFunction = function() { console.log('Shhhh, this is private!'); } // 通过 globalVariable 接口公开下面的方法 // 同时将方法的实现隐蔽在 function() 块中 globalVariable.each = function(collection, iterator) { if (Array.isArray(collection)) { for (var i = 0; i < collection.length; i++) { iterator(collection[i], i, collection); } } else { for (var key in collection) { iterator(collection[key], key, collection); } } }; globalVariable.filter = function(collection, test) { var filtered = []; globalVariable.each(collection, function(item) { if (test(item)) { filtered.push(item); } }); return filtered; }; globalVariable.map = function(collection, iterator) { var mapped = []; globalUtils.each(collection, function(value, key, collection) { mapped.push(iterator(value)); }); return mapped; }; globalVariable.reduce = function(collection, iterator, accumulator) { var startingValueMissing = accumulator === undefined; globalVariable.each(collection, function(item) { if(startingValueMissing) { accumulator = item; startingValueMissing = false; } else { accumulator = iterator(accumulator, item); } }); return accumulator; }; }(globalVariable));
在这个例子中,globalVariable 是唯一的全局变量。与匿名闭包比较,这种方法的好处是可以预先声明全局变量,使得别人更随意马虎阅读代码。
例三:工具接口
另一种方法是利用立即实行函数接口工具创建模块,如下所示:
var myGradesCalculate = (function () { // 将这些变量放在闭包范围内实现私有化 var myGrades = [93, 95, 88, 0, 55, 91]; // 通过接口公开这些函数,同时将模块的实现隐蔽在function()块中 return { average: function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item; }, 0); return'均匀分 ' + total / myGrades.length + '.'; }, failing: function() { var failingGrades = myGrades.filter(function(item) { return item < 70; }); return '挂科了' + failingGrades.length + ' 次.'; } }})();myGradesCalculate.failing(); // '挂科了 2 次.' myGradesCalculate.average(); // '均匀分 70.33333333333333.'
正如您所看到的,这种方法许可我们通过将它们放在 return 语句中(例如算均匀分和挂科数方法)来决定我们想要保留的变量/方法(例如 myGrades)以及我们想要公开的变量/方法。
例四:显式模块模式
这与上面的方法非常相似,只是它确保所有方法和变量在显式公开之前都是私有的:
var myGradesCalculate = (function () { // 将这些变量放在闭包范围内实现私有化 var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item; }, 0); return'均匀分 ' + total / myGrades.length + '.'; }; var failing = function() { var failingGrades = myGrades.filter(function(item) { return item < 70; }); return '挂科了' + failingGrades.length + ' 次.'; }; // Explicitly reveal public pointers to the private functions // that we want to reveal publicly return { average: average, failing: failing }})();myGradesCalculate.failing(); // '挂科了 2 次.' myGradesCalculate.average(); // '均匀分 70.33333333333333.'
这可能看起来很多,但它只是模块模式的冰山一角。 以下是我在自己的探索中创造有用的一些资源:
Learning JavaScript Design Patterns:作者是 Addy Osmani,一本简洁又令人印象深刻的书本,蕴藏着许多宝藏。Adequately Good by Ben Cherry:包含模块模式的高等用法示例。Blog of Carl Danley:模块模式概览,也是 JavaScript 许多设计模式的资源库。CommonJS 和 AMD所有这些方法都有一个共同点:利用单个全局变量将其代码包装在函数中,从而利用闭包浸染域为自己创建一个私有名称空间。
虽然每种方法都有效且都有各自特点,但却都有缺点。
首先,作为开拓职员,你须要知道加载文件的精确依赖顺序。例如,假设你在项目中利用 Backbone,因此你可以将 Backbone 的源代码 以<script> 脚本标签的形式引入到文件中。
但是,由于 Backbone 对 Underscore.js 有很强的依赖性,因此 Backbone 文件的脚本标记不能放在Underscore.js 文件之前。
作为一名开拓职员,管理依赖关系并精确处理这些事情有时会令人头痛。
另一个缺陷是它们仍旧会导致名称空间冲突。例如,如果两个模块具有相同的名称怎么办?或者,如果有一个模块的两个版本,并且两者都须要,该怎么办?
幸运的是,答案是肯定的。
有两种盛行且实用的方法:CommonJS 和 AMD。
CommonJSCommonJS 是一个志愿者事情组,卖力设计和实现用于声明模块的 JavaScript API。
CommonJS 模块实质上是一个可重用的 JavaScript,它导出特定的工具,使其可供其程序中须要的其他模块利用。 如果你已经利用 Node.js 编程,那么你该当非常熟习这种格式。
利用 CommonJS,每个 JavaScript 文件都将模块存储在自己独立的模块高下文中(就像将其封装在闭包中一样)。 在此范围内,我们利用 module.exports 导出模块,或利用 require 来导入模块。
在定义 CommonJS 模块时,它可能是这样的:
function myModule() { this.hello = function() { return 'hello!'; } this.goodbye = function() { return 'goodbye!'; }}module.exports = myModule;
我们利用分外的工具模块,并将函数的引用放入 module.exports 中。这让 CommonJS 模块系统知道我们想要公开什么,以便其他文件可以利用它。
如果想利用 myModule,只须要利用 require 方法就可以,如下:
var myModule = require('myModule');var myModuleInstance = new myModule();myModuleInstance.hello(); // 'hello!'myModuleInstance.goodbye(); // 'goodbye!'
与前面谈论的模块模式比较,这种方法有两个明显的好处:
避免全局命名空间污染依赖关系更加明确其余须要把稳的是,CommonJS 采取做事器优先方法并同步加载模块。 这很主要,由于如果我们须要三个其他模块,它将逐个加载它们。
现在,它在做事器上运行良好,但遗憾的是,在为浏览器编写 JavaScript 时利用起来更加困难。 可以这么说,从网上读取模块比从磁盘读取须要更长的韶光。 只要加载模块的脚本正在运行,它就会阻挡浏览器运行其他任何内容,直到完成加载,这是由于 JavaScript 是单线程且 CommonJS 是同步加载的。
AMDCommonJS统统都很好,但是如果我们想要异步加载模块呢? 答案是 异步模块定义,简称 AMD。
利用 AMD 的加载模块如下:
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) { console.log(myModule.hello());});
define 函数的第一个参数是一个数组,数组中是依赖的各种模块。这些依赖模块在后台(以非壅塞的办法)加载进来,一旦加载完毕,define 函数就会调用第二个参数,即回调函数实行操作。
接下来,回调函数吸收参数,即依赖模块 - 示例中便是 myModule 和 myOtherModule - 许可函数利用这些依赖项, 末了,所依赖的模块本身也必须利用 define 关键字来定义。例如,myModule如下所示:
define([], function() { return { hello: function() { console.log('hello'); }, goodbye: function() { console.log('goodbye'); } };});
因此,与 CommonJS 不同,AMD 采取浏览器优先的方法和异步辇儿动来完成事情。 (把稳,有很多人坚信在开始运行代码时动态加载文件是不利的,我们将不才一节关于模块构建的内容中磋商更多内容)。
除了异步性,AMD 的另一个好处是模块可以是工具,函数,布局函数,字符串,JSON 和许多其他类型,而CommonJS 只支持工具作为模块。
也便是说,和CommonJS比较,AMD不兼容io、文件系统或者其他做事器真个功能特性,而且函数包装语法与大略的require 语句比较有点冗长。
UMD对付同时支持 AMD 和 CommonJS 特性的项目,还有另一种格式:通用模块定义(Universal Module Definition, UMD)。
UMD 实质上创造了一种利用两者之一的方法,同时也支持全局变量定义。因此,UMD 模块能够同时在客户端和做事端同时事情。
大略看一下 UMD 是若何事情的:
(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD define(['myModule', 'myOtherModule'], factory); } else if (typeof exports === 'object') { // CommonJS module.exports = factory(require('myModule'), require('myOtherModule')); } else { // Browser globals (Note: root is window) root.returnExports = factory(root.myModule, root.myOtherModule); }}(this, function (myModule, myOtherModule) { // Methods function notHelloOrGoodbye(){}; // A private method function hello(){}; // A public method because it's returned (see below) function goodbye(){}; // A public method because it's returned (see below) // Exposed public methods return { hello: hello, goodbye: goodbye }}));
Github 上 enlightening repo 里有更多关于 UMD 的例子。
Native JS你可能已经把稳到,上面的模块都不是 JavaScript 原生的。相反,我们已经创建了通过利用模块模式、CommonJS 或 AMD 来仿照模块系统的方法。
幸运的是,TC39(定义 ECMAScript 的语法和语义的标准组织)一帮聪明的人已经引入了ECMAScript 6(ES6)的内置模块。
ES6 为导入导出模块供应了很多不同的可能性,已经有许多其他人花韶光阐明这些,下面是一些有用的资源:
jsmodules.ioexploringjs.com与 CommonJS 或 AMD 比较,ES6 模块最大的优点在于它能够同时供应两方面的上风:简明的声明式语法和异步加载,以及对循环依赖项的更好支持。
大概我个人最喜好的 ES6 模块功能是它的导入模块是导出时模块的实时只读视图。(比较起 CommonJS,导入的是导出模块的拷贝副本,因此也不是实时的)。
下面是一个例子:
// lib/counter.jsvar counter = 1;function increment() { counter++;}function decrement() { counter--;}module.exports = { counter: counter, increment: increment, decrement: decrement};// src/main.jsvar counter = require('../../lib/counter');counter.increment();console.log(counter.counter); // 1
在这个例子中,我们基本上创建了两个模块的工具:一个用于导出它,一个在我们须要的时候引入。
此外,在 main.js 中的工具目前是与原始模块是相互独立的,这便是为什么纵然我们实行 increment 方法,它仍旧返回 1,由于引入的变量和最初导入的变量是毫无关联的。须要改变你引入的工具唯一的办法是手动实行增加:
counter.counter++;console.log(counter.counter); // 2
另一方面,ES6创建了我们导入的模块的实时只读视图:
// lib/counter.jsexport let counter = 1;export function increment() { counter++;}export function decrement() { counter--;}// src/main.jsimport as counter from '../../counter';console.log(counter.counter); // 1counter.increment();
console.log(counter.counter); // 2
超酷?我创造这一点是由于ES6许可你可以把你定义的模块拆分成更小的模块而不用删减功能,然后你还能反过来把它们合成到一起, 完备没问题。
什么是模块打包?总体上看,模块打包只是将一组模块(及其依赖项)以精确的顺序拼接到一个文件(或一组文件)中的过程。正如 Web开拓的其它方方面面,棘手的问题总是潜藏在详细的细节里。
为什么须要打包?将程序划分为模块时,常日会将这些模块组织到不同的文件和文件夹中。 有可能,你还有一组用于正在利用的库的模块,如 Underscore 或 React。
因此,每个文件都必须以一个 <script> 标签引入到主 HTML 文件中,然后当用户访问你的主页时由浏览器加载进来。 每个文件利用 <script> 标签引入,意味着浏览器不得不分别逐个的加载它们。
这对付页面加载韶光来说切实其实是噩梦。
为理解决这个问题,我们将所有文件打包或“拼接”到一个大文件(或视情形而定的几个文件),以减少要求的数量。 当你听到开拓职员评论辩论“构建步骤”或“构建过程”时,这便是他们所评论辩论的内容。
另一种加速构建操作的常用方法是“缩减”打包代码。 缩减是从源代码中移除不必要的字符(例如,空格,注释,换行符等)的过程,以便在不改变代码功能的情形下减少内容的整体大小。
较少的数据意味着浏览器处理韶光会更快,从而减少了下载文件所需的韶光。 如果你见过具有 “min” 扩展名的文件,如 “underscore-min.js” ,可能会把稳到与完全版比较,缩小版本非常小(不过很难阅读)。
除了捆绑和/或加载模块之外,模块捆绑器还供应了许多其他功能,例如在进行变动时天生自动重新编译代码或天生用于调试的源映射。
构建工具(如 Gulp 和 Grunt)能为开拓者直接进行拼接和缩减,确保为开拓职员供应可读代码,同时有利于浏览器实行的代码。
打包模块有哪些不同的方法?当你利用一种标准模块模式(上部分谈论过)来定义模块时,拼接和缩减文件非常有用。 你真正在做的便是将一堆普通的 JavaScript 代码捆绑在一起。
但是,如果你坚持利用浏览器无法解析的非原生模块系统(如 CommonJS 或 AMD(乃至是原生 ES6模块格式)),则须要利用专门工具将模块转换为排列精确、浏览器可解析的代码。 这便是 Browserify,RequireJS,Webpack 和其他“模块打包工具”或“模块加载工具”的用武之地。
除了打包和/或加载模块之外,模块打包器还供应了许多其他功能,例如在进行变动时天生自动重新编译代码或天生用于调试的源映射。
下面是一些常见的模块打经办法:
打包 CommonJS正如前面所知道的,CommonJS以同步办法加载模块,这没有什么问题,只是它对浏览器不实用。我提到过有一个办理方案——个中一个是一个名为 Browserify 的模块打包工具。Browserify 是一个为浏览器编译 CommonJS模块的工具。
例如,有个 main.js 文件,它导入一个模块来打算一组数字的均匀值:
var myDependency = require(‘myDependency’);var myGrades = [93, 95, 88, 0, 91];var myAverageGrade = myDependency.average(myGrades);
在这种情形下,我们有一个依赖项(myDependency),利用下面的命令,Browserify 以 main.js 为入口把所有依赖的模块递归打包成一个文件:
browserify main.js -o bundle.js
Browserify 通过跳入文件剖析每一个依赖的 抽象语法树(AST),以便遍历项目的全体依赖关系图。一旦确定了依赖项的构造,就把它们按精确的顺序打包到一个文件中。然后,在 html 里插入一个用于引入 “bundle.js” 的 <script> 标签,从而确保你的源代码在一个 HTTP 要求中完成下载。
类似地,如果有多个文件且有多个依赖时,只需见告 Browserify 的入口文件路径即可。末了打包后的文件可以通过 Minify-JS 之类的工具压缩打包后的代码。
打包 AMD如果你正在利用 AMD,你须要利用像 RequireJS 或者 Curl 这样的 AMD 加载器。模块加载器(与模块打包工具不同)会动态加载程序须要运行的模块。
提醒一下,AMD 与 CommonJS 的紧张差异之一是它以异步办法加载模块。 从这个意义上说,对付 AMD,从技能上讲,实际上并不须要构建步骤,由于异步加载模块意味着在运行过程中逐步下载那些程序所须要的文件,而不是用户刚进入页面就一下把所有文件都下载下来。
但实际上,对付每个用户操作而言,随着韶光的推移,大容量要求的开销在生产中没有多大意义。 大多数 Web 开拓职员仍旧利用构建工具打包和压缩 AMD 模块以得到最佳性能,例如利用 RequireJS 优化器,r.js 等工具。
总的来说,AMD 和 CommonJS 在打包方面的差异在于:在开拓期间,AMD 可以省去任何构建过程。当然,在代码上线前,要利用优化工具(如 r.js)进行优化。
Webpack就打包工具而言,Webpack 是一个新事物。它被设计成与你利用的模块系统无关,许可开拓职员在适当的情形下利用 CommonJS、AMD 或 ES6。
你可能想知道,为什么我们须要 Webpack,而我们已经有了其他打包工具了,比如 Browserify 和 RequireJS,它们可以完成事情,并且做得非常好。首先,Webpack 供应了一些有用的特性,比如 “代码分割”(code splitting) —— 一种将代码库分割为“块(chunks)”的办法,从而能实现按需加载。
例如,如果你的 Web 运用程序,个中只须要某些代码,那么将全体代码库都打包进一个大文件就不是很高效。 在这种情形下,可以利用代码分割,将须要的部分代码抽离在"打包块",在实行按需加载,从而避免在最开始就碰着大量负载的麻烦。
代码分割只是 Webpack 供应的浩瀚引人瞩目的特性之一,网上有很多关于 “Webpack 与 Browserify 谁更好” 的激烈谈论。以下是一些客不雅观镇静的谈论,帮助我轻微理清了头绪:
https://gist.github.com/subst...点击预览http://mattdesl.svbtle.com/br...http://blog.namangoel.com/bro...ES6 模块当前 JS 模块规范(CommonJS, AMD) 与 ES6 模块之间最主要的差异是 ES6 模块的设计考虑到了静态剖析。这意味着当你导入模块时,导入的模块在编译阶段也便是代码开始运行之前就被解析了。这许可我们在运行程序之前移,移除那些在导出模块中不被其它模块利用的部分。移除不被利用的模块能节省空间,且有效地减少浏览器的压力。
一个常见的问题,利用一些工具,如 Uglify.js ,缩减代码时,有一个去世码删除的处理,它和 ES6 移除没用的模块又有什么不同呢?只能说 “视情形而定”。
去世码肃清(Dead codeelimination)是一种编译器事理中编译最优化技能,它的用场是移除对程序运行结果没有任何影响的代码。移除这类的代码有两种优点,不但可以减少程序的大小,还可以避免程序在运行中进行不干系的运算行为,减少它运行的韶光。不会被运行到的代码(unreachable code)以及只会影响到无关程序运行结果的变量(Dead Variables),都是去世码(Dead code)的范畴。
有时,在 UglifyJS 和 ES6 模块之间去世码肃清的事情办法完备相同,有时则不然。如果你想验证一下, Rollup’s wiki 里有个很好的示例。
ES6 模块的不同之处在于去世码肃清的不同方法,称为“tree shaking”。“tree shaking” 实质上是去世码肃清反过程。它只包含包须要运行的代码,而非打消不须要的代码。来看个例子:
假设有一个带有多个函数的 utils.js 文件,每个函数都用 ES6 的语法导出:
export function each(collection, iterator) { if (Array.isArray(collection)) { for (var i = 0; i < collection.length; i++) { iterator(collection[i], i, collection); } } else { for (var key in collection) { iterator(collection[key], key, collection); } } }export function filter(collection, test) { var filtered = []; each(collection, function(item) { if (test(item)) { filtered.push(item); } }); return filtered;}export function map(collection, iterator) { var mapped = []; each(collection, function(value, key, collection) { mapped.push(iterator(value)); }); return mapped;}export function reduce(collection, iterator, accumulator) { var startingValueMissing = accumulator === undefined; each(collection, function(item) { if(startingValueMissing) { accumulator = item; startingValueMissing = false; } else { accumulator = iterator(accumulator, item); } }); return accumulator;}
接着,假设我们不知道要在程序中利用什么 utils.js 中的哪个函数,以是我们将上述的所有模块导入main.js中,如下所示:
import as Utils from ‘./utils.js’;
终极,我们只用到的 each 方法:
import as Utils from ‘./utils.js’;Utils.each([1, 2, 3], function(x) { console.log(x) });
“tree shaken” 版本的 main.js 看起来如下(一旦模块被加载后):
function each(collection, iterator) { if (Array.isArray(collection)) { for (var i = 0; i < collection.length; i++) { iterator(collection[i], i, collection); } } else { for (var key in collection) { iterator(collection[key], key, collection); } } };each([1, 2, 3], function(x) { console.log(x) });
把稳:只导出我们利用的 each 函数。
同时,如果决定利用 filte r函数而不是每个函数,终极会看到如下的结果:
import as Utils from ‘./utils.js’;Utils.filter([1, 2, 3], function(x) { return x === 2 });
tree shaken 版本如下:
function each(collection, iterator) { if (Array.isArray(collection)) { for (var i = 0; i < collection.length; i++) { iterator(collection[i], i, collection); } } else { for (var key in collection) { iterator(collection[key], key, collection); } } };function filter(collection, test) { var filtered = []; each(collection, function(item) { if (test(item)) { filtered.push(item); } }); return filtered;};filter([1, 2, 3], function(x) { return x === 2 });
此时,each 和 filter 函数都被包含进来。这是由于 filter 在定义时利用了 each。因此也须要导出该函数模块以担保程序正常运行。
构建 ES6 模块我们知道 ES6 模块的加载办法与其他模块格式不同,但我们仍旧没有谈论利用 ES6 模块时的构建步骤。
遗憾的是,由于浏览器对 ES6模 块的原生支持还不足完善,以是现阶段还须要我们做一些补充事情。
下面是几个在浏览器中 构建/转换 ES6 模块的方法,个中第一个是目前最常用的方法:
利用转换器(例如 Babel 或 Traceur)以 CommonJS、AMD 或 UMD 格式将 ES6 代码转换为 ES5 代码,然后再通过 Browserify 或 Webpack 一类的构建工具来进行构建。利用 Rollup.js,这实在和上面差不多,只是 Rollup 捎带 ES6 模块的功能,在打包之前静态剖析ES6 代码和依赖项。 它利用 “tree shaking” 技能来优化你的代码。 总言,当您利用ES6模块时,Rollup.js 相对付 Browserify 或 Webpack 的紧张好处是 tree shaking 能让打包文件更小。 须要把稳的是,Rollup提 供了几种格式来的打包代码,包括 ES6,CommonJS,AMD,UMD 或 IIFE。 IIFE 和 UMD 捆绑包可以直接在浏览器中事情,但如果你选择打包 AMD,CommonJS 或 ES6,需须要探求能将代码转成浏览器能理解运行的代码的方法(例如,利用 Browserify, Webpack,RequireJS等)。小心踩坑作为 web 开拓职员,我们必须经历很多困难。转换语法优雅的ES6代码以便在浏览器里运行并不总是随意马虎的。
问题是,什么时候 ES6 模块可以在浏览器中运行而不须要这些开销?
答案是:“尽快”。
ECMAScript 目前有一个办理方案的规范,称为 ECMAScript 6 module loader API。简而言之,这是一个纲领性的、基于 Promise 的 API,它支持动态加载模块并缓存它们,以便后续导入不会重新加载模块的新版本。
它看起来如下:
// myModule.jsexport class myModule { constructor() { console.log('Hello, I am a module'); } hello() { console.log('hello!'); } goodbye() { console.log('goodbye!'); }}
// main.jsSystem.import(‘myModule’).then(function(myModule) { new myModule.hello();});// ‘hello!’
你亦可直接对 script 标签指定 “type=module” 来定义模块,如:
<script type="module"> // loads the 'myModule' export from 'mymodule.js' import { hello } from 'mymodule'; new Hello(); // 'Hello, I am a module!'</script>
更加详细的先容也可以在 Github 上查看:es-module-loader
此外,如果您想测试这种方法,请查看 SystemJS,它建立在 ES6 Module Loader polyfill 之上。 SystemJS 在浏览器和 Node 中动态加载任何模块格式(ES6模块,AMD,CommonJS 或 全局脚本)。
它跟踪“模块注册表”中所有已加载的模块,以避免重新加载先前已加载过的模块。 更不用说它还会自动转换ES6模块(如果只是设置一个选项)并且能够从任何其他类型加载任何模块类型!
对付日益遍及的 ES6 模块,下面有一些有趣的不雅观点:
HTTP/2 会让模块打包过期吗?对付 HTTP/1,每个TCP连接只许可一个要求。这便是为什么加载多个资源须要多个要求。有了 HTTP/2,统统都变了。HTTP/2 是完备多路复用的,这意味着多个要乞降相应可以并行发生。因此,我们可以在一个连接上同时处理多个要求。
由于每个 HTTP 要求的本钱明显低于HTTP/1,因此从长远来看,加载一组模块不会造成很大的性能问题。一些人认为这意味着模块打包不再是必要的,这当然有可能,但这要详细情形详细剖析了。
例如,模块打包还有 HTTP/2 没有好处,比如移除冗余的导出模块以节省空间。 如果你正在构建一个性能至关主要的网站,那么从长远来看,打包可能会为你带来增量上风。 也便是说,如果你的性能需求不是那么极度,那么通过完备跳过构建步骤,可以以最小的本钱节省韶光。
总的来说,绝大多数网站都用上 HTTP/2 的那个时候离我们现在还很远。我预测构建过程将会保留,至少在近期内。
CommonJS、AMD 与 UMD 会被淘汰吗?一旦 ES6 成为模块标准,我们还须要其他非原生模块规范吗?
我以为还有。
Web 开拓遵守一个标准方法进行导入和导出模块,而不须要中间构建步骤——网页开拓长期受益于此。但 ES6 成为模块规范须要多永劫光呢?
机会是有,但得等一段韶光 。
再者,众口难调,以是“一个标准的方法”可能永久不会成为现实。
总结希望这篇文章能帮你理清一些开拓者口中的模块和模块打包的干系观点,共进步。