在本文的第一部分中,讨论了什么是模块,开发人员为何使用它们以及将它们合并到程序中的各种方法。
在第二部分中,将解决“捆绑”模块的确切含义:我们为什么捆绑模块,捆绑方式不同以及网络开发中模块的未来。
什么是模块打包?
总体上看,模块打包只是将一组模块(及其依赖项)以正确的顺序拼接到一个文件(或一组文件)中的过程。正如 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/substack/68f8d502be42d5cd4942
- http://mattdesl.svbtle.com/browserify-vs-webpack
- http://blog.namangoel.com/browserify-vs-webpack-js-drama
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.js export class myModule { constructor() { console.log('Hello, I am a module'); } hello() { console.log('hello!'); } goodbye() { console.log('goodbye!'); } }
// main.js System.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 模块后,还需要模块打包吗?
对于日益普及的 ES6 模块,下面有一些有趣的观点:
HTTP/2 会让模块打包过时吗?
对于 HTTP/1,每个TCP连接只允许一个请求。这就是为什么加载多个资源需要多个请求。有了 HTTP/2,一切都变了。HTTP/2 是完全多路复用的,这意味着多个请求和响应可以并行发生。因此,我们可以在一个连接上同时处理多个请求。
由于每个 HTTP 请求的成本明显低于HTTP/1,因此从长远来看,加载一组模块不会造成很大的性能问题。一些人认为这意味着模块打包不再是必要的,这当然有可能,但这要具体情况具体分析了。
例如,模块打包还有 HTTP/2 没有好处,比如移除冗余的导出模块以节省空间。 如果你正在构建一个性能至关重要的网站,那么从长远来看,打包可能会为你带来增量优势。 也就是说,如果你的性能需求不是那么极端,那么通过完全跳过构建步骤,可以以最小的成本节省时间。
总的来说,绝大多数网站都用上 HTTP/2 的那个时候离我们现在还很远。我预测构建过程将会保留,至少在近期内。
CommonJS、AMD 与 UMD 会被淘汰吗?
一旦 ES6 成为模块标准,我们还需要其他非原生模块规范吗?
我觉得还有。
Web 开发遵守一个标准方法进行导入和导出模块,而不需要中间构建步骤——网页开发长期受益于此。但 ES6 成为模块规范需要多长时间呢?
机会是有,但得等一段时间 。
再者,众口难调,所以“一个标准的方法”可能永远不会成为现实。
总结
希望这篇文章能帮你理清一些开发者口中的模块和模块打包的相关概念,共进步。
原文地址:https://www.freecodecamp.org/news/javascript-modules-part-2-module-bundling-5020383cf306/
为了保证的可读性,本文采用意译而非直译。
以上就是JavaScript模块第2部分:模块捆绑的详细内容,更多请关注0133技术站其它相关文章!