二、JavaScript 模块化编程

工程一般都是将大任务分解成小任务,并在系统中组合这些任务的解决方案。在软件工程中,我们遵循低耦合和高内聚的原则,将代码库分解为模块。在本章中,我们将讨论使用 JavaScript 创建模块的方法,包括以下主题:

  • 如何使用模块化 JavaScript 摆脱混乱
  • 如何在浏览器中使用异步模块
  • 如何在服务器上使用同步模块
  • JavaScript 内置模块系统
  • 传输 CommonJS 以便在浏览器中使用

如何使用模块化 JavaScript 摆脱混乱

你有多少张数码照片,可能有几千张,甚至更多?想象一下,如果你的图像查看器没有分类的能力。没有专辑,没有书,没有分类,什么都没有。那没有多大用处,是吗?现在让我们假设您在一个文件中有一个 JavaScript 应用,并且它会增长。当它接近上千行或上千行代码时,无论您的代码设计多么优秀,从可维护性的角度来看,它仍然会变成一堆无用的代码,就像那些庞大的未分类照片列表一样。我们不需要构建一个单一的应用,而是需要编写几个独立的模块,这些模块组合在一起形成一个应用。因此,我们将一个复杂的问题分解为简单的任务。

模块

那么,什么是模块?模块封装用于特定功能的代码。模块还提供一个接口,声明模块公开和需要哪些元素。模块通常打包在单个文件中,这使得定位和部署变得容易。一个设计良好的模块意味着低耦合(模块之间的相互依赖程度)和高内聚(模块的元素属于一起的程度)。

模块在 JavaScript 中给我们带来的优势是什么?

清洁全球范围

您知道,在 JavaScript 中,我们在任何函数范围外进行的任何赋值都会生成全局范围的新成员(浏览器中的内置对象窗口或 Node.js/Io.js 中的全局)。因此,我们总是面临意外重写已定义属性的风险。相反,模块中声明的任何内容都会留在这里,除非我们显式地导出它。

将代码打包成文件

在服务器端语言中,应用由许多文件组成。这里的最佳实践之一是,一个文件可能只包含一个类,并且只有一个职责。此外,完全限定的类名必须反映其文件位置。因此,当我们在一个对象上遇到问题时,我们可以很容易地推断在哪里可以找到它的源代码。我们可以将 JavaScript 应用代码划分为单独的脚本,但这些脚本将共享相同的范围,并且不会给我们任何封装。此外,当脚本异步加载时,必须解决内部依赖关系,这不容易做到。但是,如果我们使用模块,每个模块都有一个专用文件,并且有自己的作用域。模块加载器负责异步依赖关系。

重复使用

想象一下,在处理一个项目时,您编写了一个代码来解决一个任务,比如说它提供了一个方便的 API 来管理 cookie。当切换到另一个项目时,您意识到您的 cookie 管理器将在那里就位。对于意大利面代码,您必须提取组件代码,将其解耦,并将其绑定到新位置。如果您将组件编写为一个设计合理的模块,只需将其插入即可。

模块模式

嗯,我们知道模块有帮助,我们想使用它们。我们如何用 JavaScript 实现一个模块?首先,我们需要从全局范围中分离模块代码。我们只能通过使用函数包装模块代码来实现这一点。这里常见的做法是使用立即调用的函数表达式IIFE):

IIFE
(function () {
  "use strict";
   // variable defined inside this scope cannot be accessed from outside
}());

模块还必须具有与周围环境的接入点。与我们通常处理函数的方式相同,我们可以将对象引用作为参数传递给 IIFE。

Import
(function ( $, Backbone ) {
   "use strict";
  // module body
}( jQuery, Backbone ));

您可能还看到了一种模式,其中使用参数传递全局对象(窗口)。这样,我们不直接访问全局对象,而是通过引用访问。有一种观点认为,本地引用的访问速度更快。这并不完全正确。我在准备了一个带有测试的密码笔 http://codepen.io/dsheiko/pen/yNjEar 。它告诉我,在 Chrome(v45)中,本地引用的速度实际上要快 20%;然而,在 Firefox(v39)中,这并没有什么显著的区别。

您还可以在参数列表中使用undefined运行模式变体。未随参数提供的参数具有undefined值。所以,我们使用这个技巧来确保即使覆盖了全局undefined对象,我们也能在作用域中获得真实的undefined对象。

Local References
(function ( window, undefined ) {
   "use strict";
  // module body
}( window ));

为了在其作用域之外公开模块元素,我们可以简单地返回一个对象。函数调用的结果可以分配给外部变量,如下所示:

Export
/** @module foo */
var foo = (function () {
  "use strict";
       /**
        * @private
        * @type String
        */
   var bar = "bar",
       /**
        * @type {Object}
        */
       foo = {
         /**
          * @public
          * @type {String}
          */
         baz: "baz",
         /**
          * @public
          * @returns {String}
          */
         qux: function() {
           return "qux";
         }
       };
   return foo;
}());

console.log( foo.baz ); // baz
console.log( foo.qux() ); // qux

增强

有时我们需要在一个模块中把事情混在一起。例如,我们有一个提供核心功能的模块,我们希望根据使用的上下文来扩展插件。比方说,我有一个基于伪类声明创建对象的模块。

基本上,在实例化过程中,它会自动从指定对象继承并调用构造函数方法。在一个特定的应用中,我希望它也能根据给定的规范验证对象接口。因此,我将这个扩展插件插入基本模块。怎么做的?我们将基本模块的引用传递给插件。原始链接将被保留,因此我们可以在插件的范围内对其进行修改:

/** @module foo */
var foo = (function () {
      "use strict";
           /**
            * @type {Object}
            */
         var foo = {
             /**
              * @public
              * @type {String}
              */
             baz: "baz"
           };
       return foo;
    }()),
    /** @module bar */
    bar = (function( foo ){
      "use strict";
      foo.qux = "qux";
    }( foo || {} ));

console.log( foo.baz ); // baz
console.log( foo.qux ); // qux

模块标准

我们刚刚回顾了一些实现模块的方法。然而,在实践中,我们宁愿遵循标准化的 API。这些已经被一个巨大的社区所证明,被现实世界的项目所采用,并且被其他开发者所认可。我们需要记住的两个最重要的标准是AMDCommonJS 1.1,现在我们更愿意看看 ES6 模块 API,这将是下一个大事件。

CommonJS 1.1 同步加载模块。模块主体在第一次加载期间执行一次,并缓存导出的对象。它是为服务器端 JavaScript 设计的,主要用于 Node.js/Io.js。

AMD 异步加载模块。模块主体在第一次加载后执行一次,导出的对象也被缓存。这是为在浏览器中使用而设计的。AMD 需要一个脚本加载器。最流行的是 RequireJS、curl、lsjs 和 Dojo。

很快,我们就可以期望脚本引擎获得对 JavaScript 内置模块的本机支持。ES6 模块充分利用了这两个世界。与 CommonJS 类似,它们具有紧凑的语法并支持循环依赖,与 AMD 类似,模块异步加载,并且加载是可配置的。

如何在浏览器中使用异步模块

为了掌握 AMD,我们将举几个例子。我们需要脚本加载程序 RequireJS(http://requirejs.org/docs/download.html 。因此,您可以下载它,然后在 HTML 中对本地版本进行寻址,或者给它一个到 CDN 的外部链接。

首先,让我们看看如何创建一个模块并请求它。我们将模块放在foo.js文件中。我们使用define()调用来声明模块范围。如果我们将一个对象传递给该对象,该对象将被导出:

foo.js

define({
  bar: "bar",
  baz: "baz"
});

传递函数时,将调用该函数并导出其返回值:

foo.js

define(function () {
  "use strict";
  // Construction
  return {
    bar: "bar",
    baz: "baz"
  };
});

foo.js旁边,我们放置main.js。这段代码可以描述如下:当提供给第一个参数的所有模块(这里只有foo,这意味着./foo.js已加载且可用时,调用给定的回调。

main.js

require( [ "foo" ], function( foo ) {
  "use strict";
  document.writeln( foo.bar );
  document.writeln( foo.baz );
});

从 HTML(index.html中,首先加载RequireJS,然后加载main.js

index.html

<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.18/require.min.js"></script>
<script src="main.js" ></script>

当我们有一个加载程序时,同步加载脚本感觉不对劲。但是,我们可以使用唯一的脚本元素来实现这一点,此外,还可以强制异步加载该脚本元素:

index.html

<script data-main="./main" async 
  src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.18/require.min.js"></script>

通过data-main属性,我们告诉加载程序在模块准备就绪时首先加载哪个模块。当我们启动index.html时,我们将看到我们在main.js中导入的foo模块属性的值。

index.html输出异步加载模块的导出:

How to use asynchronous modules in the browser

现在我们处理更多的依赖关系。所以我们创建了bar.jsbaz.js模块:

bar.js

define({
  value: "bar"
});

baz.js

define({
  value: "baz"
});

我们必须修改foo.js才能访问这些模块:

foo.js

define([ "./bar", "./baz" ], function ( bar, baz ) {
  "use strict";
  // Construction
  return {
    bar: bar.value,
    baz: baz.value
  };
});

您可能已经注意到,require/define依赖项列表由模块标识符组成。在我们的例子中,所有模块和 HTML 都位于同一目录中。否则,我们需要基于相对路径构建标识符(可以省略.js文件扩展名)。如果您弄乱了路径,并且 RequireJS 无法解决依赖关系,它将触发Error: Script error for:<module-id>。没什么帮助,是吗?您可以自己改进错误处理。传递到模块作用域回调旁边的函数表达式接收异常对象作为参数。此对象具有特殊属性,如requireType(包含错误类型的字符串,如timeoutnodefinescripterror)和requireModules(受错误影响的模块 ID 数组)。

require([ "unexisting-path/foo" ], function ( foo ) {
  "use strict";
  console.log( foo.bar );
  console.log( foo.baz );
}, function (err) {
  console.log( err.requireType );
  console.log( err.requireModules );
});

在细粒度设计中,模块数量众多,并分配给目录树。为了避免每次都进行相对路径计算,可以配置一次脚本加载程序。因此,加载程序将知道通过指定别名在何处查找依赖项文件:

main.js

require.config({
    paths: {
        foo: "../../module/foo"
    }
});
require( [ "foo" ], function( foo ) {
  "use strict";
  console.log( foo.bar );
  console.log( foo.baz );
});

这给了我们额外的奖励。现在,如果我们决定更改一个模块文件名,我们不需要修改其他需要它的模块。我们只需要更改配置:

main.js

require.config({
  paths: {
    foo: "../../module/foo-v0_1_1"
  }
});
require( [ "foo" ], function( foo ) {
  "use strict";
  console.log( foo.bar );
  console.log( foo.baz );
});

通过配置,我们还可以寻址远程模块。例如,这里我们提到 jQuery,但 RequireJS 从配置中知道模块端点,因此从 CDN 加载模块:

require.config({

  paths: {
    jquery: "https://code.jquery.com/jquery-2.1.4.min.js"
  }
});

require([ "jquery" ], function ( $ ) {
  // use jQuery
});

利与弊

AMD 方法的主要优点是模块异步加载。这还意味着在部署时,我们不必上传整个代码库,只需更改一个模块即可。由于浏览器可以同时处理多个 HTTP 请求,因此我们可以通过这种方式提高性能。然而,这里有一个巨大的陷阱。在几个独立的部分中并行加载代码非常快。但现实世界的项目有更多的模块。对于目前仍然占主导地位的 HTTP/1.1 协议,加载所有这些协议将花费不可接受的长时间。与新标准 SPDY 和 HTTP/2 不同,HTTP/1.1 不能很好地处理页面下载期间的并发性,如果队列很长,则会导致行首阻塞(https://http2.github.io/faq/ )。RequreJS 提供了一个工具(http://requirejs.org/docs/optimization.html 组合一组模块。这样,我们不需要加载每个模块,只需要加载几个包。打包在一起的依赖项是同步解析的。因此,有人可能会说,我们在一定程度上放弃了 AMD 异步加载的主要好处。同时,我们还必须加载一个通常很重的脚本加载程序,并用define()回调包装每个模块。

根据我的经验,我宁愿建议您与编译成能够在浏览器中使用的包的常见 JS 模块同步。

如何–在服务器上使用同步模块

以下示例需要 Node.js。使用上提供的预构建安装程序安装 Node.js 只需几分钟时间 https://nodejs.org/download/ 或通过的软件包管理器更快 https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager

我们将首先在模块中加入一个简单的逻辑:

foo.js

console.log( "I'm running" );

现在我们可以调用该模块:

main.js

require( "./foo" );

为了运行该示例,我们将打开控制台(在 Windows 下,您可以简单地运行CMD.EXE,但我建议在上提供一个增强的工具,如 CMDERhttp://cmder.net/ )。在控制台中,我们键入以下内容:

node main.js

How to – use synchronous modules on the server

按下回车键后,控制台立即输出我正在运行。因此,当一个模块被请求时,它的主体代码被调用。但是,如果我们多次请求该模块,该怎么办?

main.js

require( "./foo" );
require( "./foo" );
require( "./foo" );

结果是一样的。它只输出我正在运行一次。这是因为最初请求模块时,模块主体代码只执行一次。导出的对象(可能由主体代码生成)被缓存,其行为类似于单例:

foo.js

var foo = new Date();

main.js

var first = require( "./foo" ),
    second = require( "./foo" );

console.log( first === second ); // true

您可能会注意到,与 AMD 不同,我们在模块中不需要任何包装。但它仍然与全球范围隔离吗?

foo.js

var foo = "foo";

main.js

require( "./foo" );
console.log( typeof foo ); // undefined

在模块范围内定义的任何变量在范围外都不可用。但是,如果确实希望在公开接口后面的模块变量之间共享任何内容,可以通过全局对象(Node.js 类似于浏览器中的 Windows 对象)来实现。

那么出口呢?CommonJS 优先选择单一导出。我们为module.exports分配一个对类型或值的引用,这将是所需函数的缓存返回。如果需要多次导出,只需导出一个对象:

foo.js

// module logic
module.exports = {
  bar: "bar",
  baz: "baz"
};

main.js

var foo = require("./foo");
console.log( foo.bar ); // bar
console.log( foo.baz ); // baz

以下是 Node.js 中导出对象构造函数的最常见情况:

foo.js

var Foo = function(){
  this.bar = "bar";
}

module.exports = Foo;

因此,通过一个必需的调用,我们将收到带有原型的构造函数,并可以创建实例:

main.js

var Foo = require("./foo"),
    foo = new Foo();

console.log( foo.bar ); // bar

与我们从main请求foo模块的方式相同,我们也可以从其他模块请求:

bar.js

// module logic
module.exports = "bar";

baz.js

// module logic
module.exports = "baz";

foo.js

// module logic
module.exports = {
  bar: require( "./bar" ),
  baz: require( "./baz" )
};

main.js

var foo = require( "./foo" );
console.log( foo.bar ); // bar
console.log( foo.baz ); // baz

但是,如果 Node.js 运行到循环依赖中会怎样呢?如果我们从被调用的模块请求回调用方呢?没有戏剧性的事情发生。您可能还记得,模块代码只执行一次。因此,如果我们在执行了main.js之后从foo.js请求main.js,其主体代码将不再被调用:

foo.js

console.log("Runnnig foo.js");
require("./main");

main.js

console.log("Runnnig main.js");
require("./foo");

当我们使用 Node.js 运行main.js时,我们得到以下输出:

Runnnig main.js
Runnnig foo.js

利与弊

CommonJS 有一个简洁且富有表现力的语法。它很容易使用。单元测试通常编写为在命令行中运行,最好是连续集成的一部分。一个设计良好的 CommonJS 模块构成了一个完美的测试单元,您可以从 Node.js 驱动的测试框架(例如 Mocha)直接访问它,远离应用上下文。然而,CommonJS 意味着同步加载,这不适合在浏览器中使用。如果我们想绕过这一限制,我们必须将模块源文件传输到单个脚本中,该脚本在不加载的情况下解析内部模块依赖关系(请参见“在浏览器中使用 Traspiling CommonJS”)。

UMD

如果您希望您的模块在 AMD 浏览器和 CommonJS 服务器上都能被接受,那么有一个技巧(https://github.com/umdjs/umd )。通过添加包装函数,您可以根据运行时环境以所需格式动态构建导出。

JavaScript 的内置模块系统

嗯,AMD 和 CommonJS 都是社区标准,不是语言规范的一部分。然而,在 EcmaScript 第 6 版中,JavaScript 获得了自己的模块系统。目前,还没有浏览器支持此功能,因此我们必须安装 Babel.js transpiler 来处理示例。

由于 Node.js 已经随 NPM(Node.js 包管理器)一起发布,现在我们可以运行以下命令:

npm install babel -g

指定出口

现在我们可以编写一个模块,如下所示:

foo.es6

export let bar = "bar";
export let baz = "baz";

在 ES6 中,我们可以导出多个元素。任何以关键字 export 为前缀的声明都可用于导入:

main.es6

import { bar, baz } from "./foo";
console.log( bar ); // bar
console.log( baz ); // baz

由于浏览器中还不支持 ES6 模块,我们将把它们转换成 CommonJS 或 AMD。这里 Babel.js 帮助我们:

babel --modules common *.es6 --out-dir .

通过这个命令,我们让 Babel.js 将当前目录的所有*.es6文件翻译成 CommonJS 模块。因此,我们可以使用 Node.js 运行派生的main.js模块:

node main.js

Named exports

同样,我们将 ES6 模块转换为 AMD:

babel --modules amd *.es6 --out-dir .

index.html

<script data-main="./main" 
  src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.18/require.min.js"></script>

在上一个示例中,我们在 import 语句中登记了命名的导出。我们还可以导入整个模块,并将命名的导出引用为属性:

main.es6

import * as foo from "./foo"; 
console.log( foo.bar ); // bar
console.log( foo.baz ); // baz

默认导出

此外,我们还可以进行默认导出。这是在 Node.js 中导出的通常方式:

foo.es6

export default function foo(){ return "foo"; }

main.es6

import foo from "./foo";
console.log( foo() ); // foo

我们导出了一个函数并附带了导入。这也可以是类或对象。

在 AMD 中,我们接收作为回调参数的导出,在 CommonJS 中接收作为局部变量的导出。虽然 ES6 不导出值,但它导出了不可变的所谓绑定(引用)。您可以读取它们的值,但如果尝试更改它们,则会出现类型错误。Babel.js 在编译期间触发此错误:

foo.es6

export let bar = "bar";
export function setBar( val ) {
   bar = val;
};

main.es6

import { bar, setBar } from "./foo";
console.log( bar ); // bar
setBar( "baz" );
console.log( bar ); // baz
bar = "qux"; // TypeError

模块加载程序 API

除了单独规范(中的声明性语法之外 https://github.com/whatwg/loader/ ),ES6 为我们提供了一个编程 API。它允许我们以编程方式使用模块并配置模块加载:

System.import( "./foo" ).then( foo => {
  console.log( foo );
})
.catch( err => {
  console.error( err );
});

与 Node.js 不同,ES6 模块由于其声明性质,需要在顶层进行导入和导出。因此,这不能是有条件的。但是,使用实用加载程序 API,我们可以执行以下操作:

Promise.all([ "foo", "bar", "baz" ]
    .map( mod => System.import( mod ) )
  )
  .then(([ foo, bar, baz ]) => {
     console.log( foo, bar, baz );
  });

在这里,我们定义了一个回调,该回调仅在加载所有三个指定模块时调用。

结论

AMD 和 CommonJS 均为临时标准。一旦 JavaScript 内置模块系统在脚本引擎中得到更广泛的支持,我们就不再需要它们了。ES6 模块异步加载,加载可以配置为与 AMD 类似。它们还具有紧凑且表达性强的语法,并支持类似于 CommonJS 的循环依赖关系。此外,ES 还为静态模块结构提供声明性语法。这种结构可以进行静态分析(静态检查、脱毛、优化等)。ES6 还提供了一个编程加载程序 API。因此,您可以配置如何加载模块以及有条件地加载模块。此外,ES6 模块可以通过宏和静态类型进行扩展。

虽然一切看起来都很平静,但美中不足。ES6 模块可以同步预加载(使用<script type="module"></script>),但通常存在异步加载,这会使我们陷入与 AMD 相同的陷阱。HTTP/1.1 上的大量请求会对用户响应时间造成有害影响(https://developer.yahoo.com/performance/rules.html 。另一方面,允许每个 TCP 连接多个请求的 SPDY 和 HTTP/2 得到了更广泛的支持,并最终将取代可疑的 HTTP/1.x。此外,W3C 还致力于一个名为“Web 上的打包”的标准 https://w3ctag.github.io/packaging-on-the-web/ 描述如何从 URL(散列)接受存档文件(脚本)。因此,我们将能够将整个目录和模块捆绑到一个归档文件中,并以与目录中相同的方式对其进行部署和寻址。

传输 CommonJS 供浏览器使用

虽然 HTTP/2 和 Web 上的打包仍在过程中,但我们需要快速模块化应用。正如前面提到的,我们可以将应用代码划分为 CommonJS 模块,并将它们传输到浏览器中使用。最常见的 JS transpiler 肯定是 Browserify(http://browserify.org 。该工具的最初任务是使 Node.js 模块可重用。他们在这方面相当成功。这可能感觉很神奇,但您确实可以在客户端上使用EventEmitter和其他一些 Node.js 核心模块。但是,由于主要关注 Node.js 兼容性,该工具为 CommonJS 编译提供的选项太少。例如,如果需要依赖项配置,则必须使用插件。在实际项目中,您可能会得到多个插件,每个插件都有特定的配置语法。因此,设置通常会变得复杂。相反,我们将在这里研究另一个名为 CommonJS 编译器(的工具 https://github.com/dsheiko/cjsc 。这是一个相当小的实用程序,旨在将 CommonJS 模块引入浏览器。该工具非常容易配置和使用,这使得它成为说明该概念的一个很好的选择。

首先,我们安装cjsc

npm install cjsc -g

现在我们可以从如何同步服务器部分的模块并将其传输到浏览器中使用的示例:

bar.js

// module logic
module.exports = "bar";

foo.js

// module logic
module.exports = {
  bar: require( "./bar" )};

main.js

var foo = require( "./foo" );
document.writeln( foo.bar ); // bar

起点是main.js。因此,我们告诉cjsc将此模块与所有必需的依赖项递归绑定到bundle.js

cjsc main.js -o bundle.js

Transpiling CommonJS for in-browser use

让我们来看看生成的文件。cjsc将所有 require 调用替换为custom _require并将其放入开始的_require函数定义中。这个小技巧允许您在 Node.js/Io.js 友好的环境中运行编译的代码,例如 NW.js,在 NW.js 环境中,本地包仍然需要require函数。每个模块都包装在一个功能范围内,该功能范围随模块相关对象(导出和模块)以及全局对象一起提供,全局对象是对全局对象的引用(window

Compiled Code
_require.def( "main.js", function( _require, exports, module, global )
{
  var foo = _require( "foo.js" );
  console.log( foo.bar ); // bar
  console.log( foo.baz ); // baz
    return module;
  });

生成的代码是一个通用 JavaScript,我们可以从 HTML 中确定它的地址:

index.html

<script src="bundle.js"></script>

我们的源代码仍然在 CommonJS 模块中。这意味着我们可以直接从基于 Node.js 的框架中访问它们以进行单元测试。Mocha.js 测试的官方网站为http://mochajs.org/

var expect = require( "chai" ).expect;
describe( "Foo module", function(){
  it( "should bypass the export of bar", function(){
      var foo = require( "./foo" );
      expect( foo ).to.have.property( "bar" );
      expect( foo.bar ).to.eql( "bar" );
  });
});

cjsc有很多选择。但在实际项目中,在每个构建中键入一个长的命令行既烦人又没有效率:

cjsc main-module.js -o build.js  --source-map=build/*.map \
 --source-map-root=../src -M --banner="/*! pkg v.0.0.1 */"

这就是为什么我们使用任务运行程序,例如GruntGulpCakeBroccoliGrunthttp://gruntjs.com 目前是最受欢迎的任务运行程序,并且有大量可用的插件(参见上的 Grunt vs Gulp 信息图表)http://sixrevisions.com/web-development/grunt-vs-gulp/ 。因此,我们在全球范围内安装grunt命令行界面:

npm install -g grunt-cli

为了建立一个Grunt项目,我们需要两个配置文件,package.jsonhttps://docs.npmjs.com/files/package.jsonGruntfile.js文件。第一个包含关于运行Grunt任务所需的 NPM 包的元数据。第二个是定义和配置任务。

在这里,我们可以从一个非常简约的package.json开始,它只有一个任意的项目名称及其在 semver(中的版本 http://semver.org/ 格式:

包,json

{
  "name": "project-name",
  "version": "0.0.1"
}

现在我们可以安装所需的 NPM 软件包:

npm install --save-dev grunt
npm install --save-dev grunt-cjsc

因此,我们得到了一个本地 Grunt 和一个用于 CommonJs 编译器的 Grunt 插件。--save-dev特殊选项在package.json部分创建devDependencies(如果不存在),并用已安装的依赖项填充它。例如,当我们从版本控制系统中提取项目源代码时,我们可以通过简单地运行npm install来恢复所有依赖项。

Gruntfile.js中,我们必须加载已经安装的grunt-cjsc插件,并配置一个名为cjsc的任务。实际上,我们将需要至少两个为该任务提供不同配置的目标。第一个cjsc:debug运行cjsc生成未压缩的代码,并提供源代码映射。第二个cjsc:build用于准备部署资产。所以我们在bundle.js中得到了简化的代码:

gruntile.js

module.exports = function( grunt ) {
  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON( "package.json" ),
    cjsc: {
      // A target to generate uncompressed code with sources maps
      debug: {
        options: {
          sourceMap: "js/*.map",
          sourceMapRoot: "src/",
          minify: false
        },
        files: { "js/bundle.js": "js/src/main.js" }
      },
      // A target to build project for production
      build: {
        options: {
          minify: true,
          banner: "/*! <%= pkg.name %> - v<%= pkg.version %> - " +
          "<%= grunt.template.today(\"yyyy-mm-dd\") %> */"
        },
        files: { "js/bundle.js": "js/src/main.js" }
      }
    }
  });

  // Load the plugin that provides the task.
  grunt.loadNpmTasks( "grunt-cjsc" );

  // Make it default task
  grunt.registerTask( "default", [ "cjsc:build" ] );

};

您可以从配置中看到,cjsc的目的是到transpile js/src/main.jsjs/bundle.js。因此,我们可以将前面示例中的模块复制到./js/src中。

现在,当我们准备好所有东西后,我们将运行一个任务。例如,请参见以下内容:

grunt cjsc:debug

Transpiling CommonJS for in-browser use

正如前面提到的一样,我们可以用cjsc配置依赖关系映射。我们只需要用一个对象文本来描述依赖关系,该文本可以在命令行界面中作为 JSON 文件提供给cjsc或注入到 Grunt 配置中:

{
  "jquery": {
    "path": "./vendors/jQuery/jquery.js"
  },
  "underscore": {
    "globalProperty": "_"
  },
  "foo": {
    "path": "./vendors/3rdpartyLib/not-a-module.js",
    "exports": [ "notAModule" ],
    "imports": [ "jquery" ]
  }
}

这里我们为位于./vendors/jQuery/jqueiry.js中的模块声明jquery别名(快捷方式)。我们还声明,必须将全局公开的"_"(下划线.js)库视为一个模块。最后,我们为第三方组件指定路径、导出和导入。因此,我们在应用中将其作为一个模块(不干预其代码),尽管它不是一个模块:

cjsc main.js -o bundle.js --config=cjsc-conig.json

或者,我们可以使用以下 Grunt 配置:

 grunt.initConfig({
cjsc main.js -o bundle.js --config=cjsc-conig.json
Grunt configuration
 grunt.initConfig({
    cjsc: {
      build: {
        options: {
          minify: true,
          config: require( "fs" ).readFileSync( "./cjsc-conig.json" )
        }
      },
        files: { "js/bundle.js": "js/src/main.js" }
      }
  });

捆绑 ES6 模块同步加载

好的,正如我们在JavaScript 内置模块系统一节中提到的,ES6 模块将取代 AMD 和 CommonJS 标准。此外,我们现在已经可以编写 ES6 代码并将其传输到 ES5 中。只要跨脚本代理对 ES6 的支持足够好,理论上我们就可以按原样使用代码。但是,性能如何?事实上,我们可以在 CommonJS 中编译 ES6 模块,然后将它们与cjsc捆绑在一起,以便在浏览器中使用:

foo.es6

export let bar = "bar";
export let baz = "baz";

main.es6

import { bar, baz } from "./foo";
document.writeln( bar ); // bar
document.writeln( baz ); // baz

首先,我们将 ES6 编译成 CommonJS 模块:

babel --modules common *.es6 --out-dir .

然后,我们将 CommonJS 模块捆绑到适合在浏览器中使用的脚本中:

cjsc main.js -o bundle.js -M

总结

模块化编程是一个与 OOP 密切相关的概念,它鼓励我们构造代码以获得更好的可维护性。特别是,JavaScript 模块保护全局范围不受污染,将应用代码划分为多个文件,并允许重用应用组件。

目前最常用的两个模块 API 标准是 AMD 和 CommonJS。第一个是为在浏览器中使用而设计的,它假设异步加载。第二个是同步的,用于服务器端 JavaScript。然而,你应该知道 AMD 有一个实质性的缺陷。在 HTTP/1.1 上使用大量模块的细粒度应用设计可能会导致应用性能方面的灾难。这就是为什么最近在浏览器中传输 CommonJS 模块的做法正在兴起的主要原因。

这两种 API 都应被视为临时标准,因为即将发布的 ES6 模块标准将取代它们。目前,没有支持此功能的脚本引擎,但是有 Transpiler(例如,Babel.js)允许将 ES6 模块转换为 CommonJs 或 AMD。