这是专门探索 JavaScript 及其所构建的组件的系列文章的第 20 篇。
如果你错过了前面的章节,可以在这里找到它们:
- JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述!
- JavaScript 是如何工作的:深入V8引擎&编写优化代码的5个技巧!
- JavaScript 是如何工作的:内存管理+如何处理4个常见的内存泄漏!
- JavaScript 是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式!
- JavaScript 是如何工作的:深入探索 websocket 和HTTP/2与SSE +如何选择正确的路径!
- JavaScript 是如何工作的:与 WebAssembly比较 及其使用场景!
- JavaScript 是如何工作的:Web Workers的构建块+ 5个使用他们的场景!
- JavaScript 是如何工作的:Service Worker 的生命周期及使用场景!
- JavaScript 是如何工作的:Web 推送通知的机制!
- JavaScript 是如何工作的:使用 MutationObserver 跟踪 DOM 的变化!
- JavaScript 是如何工作的:渲染引擎和优化其性能的技巧!
- JavaScript 是如何工作的:深入网络层 + 如何优化性能和安全!
- JavaScript 是如何工作的:CSS 和 JS 动画底层原理及如何优化它们的性能!
- JavaScript 是如何工作的:解析、抽象语法树(AST)+ 提升编译速度5个技巧!
- JavaScript 是如何工作的:深入类和继承内部原理+Babel和 TypeScript 之间转换!
- JavaScript 是如何工作的:存储引擎+如何选择合适的存储API!
- JavaScript 是如何工作的:Shadow DOM 的内部结构+如何编写独立的组件!
- JavaScript 是如何工作的:WebRTC 和对等网络的机制!
- JavaScript 是如何工作的:编写自己的 Web 开发框架 + React 及其虚拟 DOM 原理!
如果你是 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 的源代码 以
更加详细的介绍也可以在 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://medium.freecodecamp.o...
https://medium.freecodecamp.o...
你的点赞是我持续分享好东西的动力,欢迎点赞!
一个笨笨的码农,我的世界只能终身学习!
更多内容请关注公众号《大迁世界》!