前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[译]JS 模块化历史简介

[译]JS 模块化历史简介

作者头像
savokiss
发布2019-11-06 19:07:30
2.2K0
发布2019-11-06 19:07:30
举报
文章被收录于专栏:码力全开

对于 JavaScript 来说,模块化是一个相对现代的概念,这篇文章会带你在 JavaScript 的世界里快速浏览模块化的历史进程~

Script 标签和闭包

在早些年间,JavaScript 就是直接写在 HTML 的 <script> 标签里的,最多也就是放在独立的文件里面,而它们也都共享一个全局作用域。

任何 JS 文件里面声明的变量都会被附加在全局的 window 对象上,并且还有可能意外覆盖掉第三方库中的变量。

随着 web 应用越来越复杂,共享全局作用域这种方式的弊端开始显现,于是 IIFE(立即调用函数表达式)就被发明了出来,并且广为使用。IIFE 就是将一整段代码包裹在一个函数中,然后立即执行这个函数。在 JavaScript 中,每个函数都有一个作用域,所以在函数中声明的变量就只在这个函数中可见。即使有变量提升,变量也不会污染到全局作用域中。

下面让我们看几个 IIFE 的写法,每个 IIFE 的作用域都是独立的,其中第一种写法比较常见:

代码语言:javascript
复制
(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 上绑定一个对象作为命名空间,这样就避免了污染全局作用域。看下面的代码,假如我们要建立一个 mathlib 工具,它有一个 sum 方法。假如这个工具有多个模块,也可以建立多个文件,每个文件里都是一个 IIFE,然后向 window.mathlib 对象中添加方法就可以了:

代码语言:javascript
复制
(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

IIFE 这种方式可以说是模块化的先河,它让开发者可以将模块放在单独的文件中,并且不污染全局作用域。

当然 IIFE 也有缺点,它并没有一个明确的依赖树,这使得开发者只能自己确保 JS 文件的加载顺序。

RequireJS, AngularJS 和依赖注入

RequireJS 和 AngularJS 的出现,让我们知道了依赖注入是什么,即需要用哪个模块,就注入哪个模块。

下面的例子我们先用 RequireJS 的 define 方法定义一个没有依赖的 mathlib/sum.js 模块:

代码语言:javascript
复制
define(function() {
  return sum

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

然后我们可以创建一个入口模块 mathlib.js 用来集合所有子模块。我们的例子中只有 mathlib/sum 一个子模块,但是你可以在 mathlib 文件夹中随意扩展。下面我们声明 mathlib 模块的依赖,并将依赖作为形参按顺序传入工厂方法,并返回 mathlib 模块对象:

代码语言:javascript
复制
define(['mathlib/sum'], function(sum) {
  return { sum }
})

好了我们已经定义了一个 mathlib 库,下面就可以用 require 引入并使用它:

代码语言:javascript
复制
require(['mathlib'], function(mathlib) {
  mathlib.sum(1, 2, 3)
  // <- 6
})

RequireJS 在内部维护了一个依赖树,让开发者不用关心依赖之间的顺序,只需要在需要的地方声明要加载的模块即可使用。

这种明确地声明依赖的写法让各个模块间的依赖都非常清晰,并且反过来促进了模块化的发展。

但是 RequireJS 并不是没有缺点。它的整个模式专注于解决异步加载模块,却忽略了在生产环境下,异步加载多个模块造成的网络请求过多等性能影响。如果依赖过多,开发者也将面临一个很长的依赖数组和回调里面的形参列表。同时它的 API 也不够直观,就拿声明一个含有依赖的模块来说,就有很多种不同的写法。

AngularJS 的依赖注入系统也面临同样的问题。有一个方法可以根据形参名字来解析模块,让开发者不用再写那个依赖数组,但是却对代码压缩工具不友好,因为压缩后变量名就变短了,也就找不到相应的依赖。

直到 AngularJS v1 之后,可以通过一种构建任务,将以下代码:

代码语言:javascript
复制
module.factory('calculator', function(mathlib) {
  // …
})

转换成可压缩的带依赖数组的代码:

代码语言:javascript
复制
module.factory('calculator', ['mathlib', function(mathlib) {
  // …
}])

然鹅不得不提的是,用工程师思维添加了这么一个构建步骤,解决了这个本不应该出现的问题,但是这本身性价比实在是不高,于是大部分开发者还是选择自己手写所有的依赖数组(我当年就是这样,哈哈)。

Node.js 和 CommonJS

CommonJS 模块系统是 Node.js 中众多革新的一个,也叫 CJS。得力于 Node.js 可以直接访问文件系统,CommonJS 规范更贴近的是传统的模块加载方式。在 CommonJS 中,每个文件都是一个模块,并具有自己独立的作用域。依赖的加载使用一个同步的 require 函数,这个函数可以在模块的任意地方调用:

代码语言:javascript
复制
const mathlib = require('./mathlib')

与 RequireJS 和 AngularJS 相似的是, CommonJS 依赖也是与文件路径相关联。但是与它们最大的区别,就是 CommonJS 完全抛弃了包装函数和依赖数组,并且require 函数可以像 JS 表达式一样,在模块的任何地方使用。

在 RequireJS 和 AngularJS 中,你可能有很多动态定义的模块,然而 CommonJS 中的文件和模块是一一对应的。与此同时,RequireJS 众多的模块定义方式,与 AngularJS 中的 factory、service、provider 都让人头大。与之相反的是,CommonJS 只有一种模块加载方式,一个 JS 文件就是一个模块,加载依赖只需要用 require,导出模块只需要将要导出的值赋给 module.exports。这些优点都让 CommonJS 模块系统更简洁和易于使用。

终于,Browserify 作为桥梁,打通了 CommonJS 在 Node.js 和浏览器端的鸿沟。它可以将众多模块打包成一个可在浏览器中运行的文件。而 npm 源的出现,作为 CommonJS 的杀手级功能,基本上确立了模块加载生态中的事实标准。

诚然,npm 主要服务于 CommonJS 模块和 JavaScript 包,由于简单的模块化语法和可复用性,大量 Node.js 和 web 浏览器的包出现在 npm 上,npm 也成为世界上最大的包管理器。

ES6, import, Babel, 和 Webpack

ES6 是在 2015 年被标准化,在此之前 Babel 一直承担着将 ES6 转换为 ES5 的角色,一场新的革命正在袭来。ES6 规范中包含了一个原生的模块化系统,一般称之为 ECMAScript Modules(ESM)。

ESM 受到 CommonJS 和先烈们的影响,提供了一个静态的声明式的 API 和一个基于 Promise 的动态加载的 API:

代码语言:javascript
复制
import mathlib from './mathlib'
import('./mathlib').then(mathlib => {
  // …
})

在 ESM 中,每个文件同样是一个模块,并且具有自己独立的作用域和执行环境。ESM 相对 CJS 来说有一个重要的优点:即 ESM 是静态加载依赖的。静态加载极大地提高了模块系统的自我检查能力,使得模块系统可以基于抽象语法树(AST)作静态分析,同时 ESM 限制了加载语句必须置于模块顶部,也大大简化了语法解析和语法检查。

在 Node.js v8.5.0 中,ESM 已经可以通过一个 flag 开启。大部分主流的浏览器也都可以支持 ESM。

Webpack 作为 Browserify 的继任者,由于功能强大,基本上坐稳了通用模块打包器老大的位置。像 Babel 支持转换 ES6 那样,Webpack 很早就支持了 ESM 的 importexport 语法以及 import() 动态加载函数。并且在 ESM 的基础上,添加了 code-splitting 功能,可以将应用程序代码分割成多个文件来提升首屏加载体验。

鉴于 ESM 是原生的模块加载规范,它一统江湖也指日可待了!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-06-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码力全开 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Script 标签和闭包
  • RequireJS, AngularJS 和依赖注入
  • Node.js 和 CommonJS
  • ES6, import, Babel, 和 Webpack
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档