JavaScript 模块简史(译)

2019-04-02217次阅读javascript

说到javascript,模块化是一个现代概念。在本文中,我们将快速回顾和总结Javascript世界中模块化如何发展的里程碑。无论如何,这并不意味着是一个全面的列表,而是用来说明JavaScript历史上的主要范式变化。

javascript标签和闭包

在早期,javascript被内联在HTML<script>标签中。最多,它被放在专用的脚本文件中,所有脚本文件都共享一个全局范围。

这些文件或内联脚本中声明的任何变量都将被打印在全局window对象上,从而在完全不相关的脚本之间创建泄漏,这些脚本可能会导致冲突甚至破坏经验,其中一个脚本中的变量可能无意中替换了另一个脚本中的全局变量。依赖全局。

最终,随着Web应用程序的规模和复杂性不断增加,范围界定的概念和依赖全局的危险性变得明显和广为人知。立即调用函数表达式(IIFE)被发明并成为当下的主流。IIFE通过将整个文件或文件的一部分包装在立即执行的函数中来工作。javascript中的每个函数都创建了一个新的作用域,这意味着var变量绑定将包含在IIFE中。即使变量声明被提升到其包含范围的顶部,它们也永远不会成为隐式全局变量,这要归功于IIFE包装器,从而抑制了隐式JavaScript全局变量的脆弱性。

在下一个示例代码段中可以找到几种IIFE版本。每个IIFE中的代码都是隔离的,只能通过显式语句转义到全局上下文中window.fromIIFE = true。

(function() {
  console.log('IIFE using parenthesis')
})()

~function() {
  console.log('IIFE using a bitwise operator')
}()

void function() {
  console.log('IIFE using the void operator')
}()

使用IIFE模式,库通常会通过在window对象上公开然后重用单个绑定来创建模块,从而避免全局命名空间污染。下一个片段展示了我们如何在这些基于IIFE的库中给mathlib组件添加sum模块方法。如果我们想向mathlib中添加更多的模块,我们可以将每个模块放置在一个单独的IIFE中,该IIFE将自己的方法添加到mathlib公共接口中,而其他任何模块都可以对定义新功能部分的组件保持私有。

void function() {
  window.mathlib = window.mathlib || {}
  window.mathlib.sum = sum

  function sum(...values) {
    return values.reduce((a, b) => a + b, 0)
  }
}()

mathlib.sum(1, 2, 3)
// <- 6

巧合的是,这个模式是对javascript工具的公开邀请,允许开发人员第一次将每个IIFE模块安全地连接到一个文件中,从而减轻网络压力。

IIFE方法的问题在于没有明确的依赖树。这意味着开发人员必须按照精确的顺序制作组件文件列表,以便在依赖于它们的任何模块之前加载依赖关系 - 递归。

RequireJS、AngularJS和依赖注入

自从像RequireJS这样的模块系统或AngularJS中的依赖注入机制出现以来,这是一个我们几乎不必考虑的问题,这两种机制都允许我们明确地命名每个模块的依赖关系。

下面的示例显示,我们可以使用RequireJS的define函数定义mathlib/sum.js库,该函数已添加到全局范围中。然后,define回调返回的值用作模块的公共接口。

define(function() {
  return sum

  function sum(...values) {
    return values.reduce((a, b) => a + b, 0)
  }
})

然后我们可以有一个mathlib.js模块,它聚合了我们希望在库中包含的所有功能。在我们的例子中,它只是mathlib/sum,但是我们可以以相同的方式列出我们想要的依赖项。我们将使用数组中的路径列出每个依赖项,并按照相同的顺序将它们的公共接口作为参数传递到回调中。

define(['mathlib/sum'], function(sum) {
  return { sum }
})

既然我们已经定义了一个库,我们就可以使用require来使用它。请注意下面的代码片段中如何为我们解析依赖链。

require(['mathlib'], function(mathlib) {
  mathlib.sum(1, 2, 3)
  // <- 6
})

这是RequireJS及其固有的依赖树的优点。无论我们的应用程序包含成百上千个模块,RequireJS都可以解析依赖树,而无需仔细维护列表。鉴于我们已经列出了依赖项的确切位置,我们已经消除了每个组件的长列表的必要性,以及它们彼此之间的关系,以及维护这样一个列表的容易出错的过程。消除如此大的复杂性只是一个副作用,而不是主要的好处。

依赖声明在模块级别上的这种明确性使组件与应用程序的其他部分之间的关系变得很明显。这种明确性反过来又促进了更大程度的模块化,这在以前是无效的,因为遵循依赖链是多么困难。

Requirejs并非没有问题。整个模式围绕其异步加载模块的能力展开,这对于生产部署是不明智的,因为它的性能非常差。使用异步加载机制,在执行大部分代码之前,您以瀑布式方式发出了数百个网络请求。必须使用不同的工具来优化生产的构建。然后是冗长的因素,最后是依赖项的长列表、RequireJS函数调用和模块的回调。在这一点上,有很多不同的RequireJS函数和调用这些函数的几种方法,使其使用复杂化。API不是最直观的,因为有很多方法可以做到相同的事情:声明一个具有依赖性的模块。

AngularJS中的依赖注入系统也遇到了许多相同的问题。当时,它是一个优雅的解决方案,依靠巧妙的字符串解析来避免依赖项数组,而是使用函数参数名来解析依赖项。这种机制与minifiers不兼容,minifiers将参数重命名为单个字符,从而破坏注入器。

在AngularJS v1的生命后期,引入了一个构建任务,该任务将转换如下代码:

module.factory('calculator', function(mathlib) {
  // …
})

进入下面的代码中的格式,这是简化安全的,因为它包含显式依赖列表。

module.factory('calculator', ['mathlib', function(mathlib) {
  // …
}])

不用说,延迟引入这个鲜为人知的构建工具,再加上有一个额外的构建步骤来断开不应该被破坏的东西的过度工程化的方面,不鼓励使用一个无论如何都具有如此微不足道的好处的模式。开发人员大多选择使用熟悉的需求,如硬编码依赖数组格式。

Node.js和CommonJS的出现

在node.js所推崇的众多创新中,一个是commonJS模块系统,简称CJS。利用node.js程序可以访问文件系统这一事实,CommonJS标准更符合传统的模块加载机制。在CommonJS中,每个文件都是一个具有自己的作用域和上下文的模块。依赖项是使用同步require函数加载的,该函数可以在模块生命周期的任何时候动态调用,如下面的代码片段所示。

const mathlib = require('./mathlib')

与RequireJS和AngularJS非常类似,CommonJS依赖项也由路径名引用。主要的区别在于样板函数和依赖数组现在都不存在了,模块的接口可以分配给变量绑定,或者可以在任何可以使用javascript表达式的地方使用。

与RequireJS或AngularJS不同,CommonJS相当严格。在RequireJS和AngularJS中,每个文件可以有许多动态定义的模块,而CommonJS在文件和模块之间有一对一的映射。同时,RequireJS有几种声明模块的方法,而AngularJS有几种工厂、服务、提供者等,除了依赖注入机制与AngularJS框架本身紧密耦合之外。相比之下,CommonJS只有一种声明模块的方法。任何javascript文件都是一个模块,调用require将加载依赖项,分配给module.exports的任何内容都是其接口。这使得更好的工具和代码内省变得更容易让工具学习CommonJS组件系统的层次结构。

最后,browserify被发明为弥补node.js服务器的commonjs模块和浏览器之间的差距的方法。使用browserify命令行界面程序并为其提供入口点模块的路径,可以将数量不可想象的模块组合成一个可供浏览器使用的包。CommonJS的杀手特性,即NPM包注册,在帮助它接管模块加载生态系统方面起着决定性的作用。

当然,NPM并不局限于CommonJS模块甚至是JavaScript包,但大体上,这是它的主要用例。在您的Web应用程序中,只要按几下指尖,就可以获得数千个包(现在超过50万个,而且还在稳步增长),再加上在node.js Web服务器和每个客户端的Web浏览器上重用系统的大部分功能,这对于其他系统来说是一个竞争优势。

ES6,importBabel和Webpack

随着ES6在2015年6月开始标准化,并且Babel在此之前很久就将ES6升级为ES5,一场新的革命正在迅速逼近。ES6规范包括JavaScript原生的模块系统,通常称为ECMAScript模块(ESM)。

ESM在很大程度上受CJS及其前辈的影响,提供静态声明性API和基于Promise的动态编程性API,如下所示。

import mathlib from './mathlib'
import('./mathlib').then(mathlib => {
  // …
})

在ESM中,每个文件都是一个有自己的作用域和上下文的模块。ESM相对于CJS的一个主要优势是,ESM拥有并鼓励使用静态导入依赖项的方式。静态导入极大地提高了模块系统的自省能力,因为它们可以静态分析并从系统中每个模块的抽象语法树(AST)中词汇提取。ESM中的静态导入被限制在模块的最顶层,进一步简化了解析和内省。

在node.js V8.5.0中,ESM模块支持是在标志后面引入的。大多数常青浏览器也支持标志后面的ESM模块。

Webpack是Browserify的继承者,由于具有更广泛的功能,它在很大程度上取代了通用模块捆绑器的角色。与Babel和ES6的情况一样,WebPack长期以来一直支持ESM与它的两个import和export语句以及动态import()功能。由于引入了“代码分割”机制,它能够将应用程序划分为不同的捆绑包以提高首次加载体验的性能,因此它在ESM中得到了特别富有成效的应用。

考虑到ESM是该语言的原生语言,与CJS不同,预计它将在几年内完全取代模块生态系统。

翻译自:https://ponyfoo.com/articles/brief-history-of-modularity

上一篇: 搬瓦工(Bandwagon)商家 VPS主机套餐信息  下一篇: javascript中如何将阿拉伯数字每三位一逗号分隔  

JavaScript 模块简史(译)相关文章