26 JavaScript模块

3 使用 ES6 之前的模块加载器

在 ES6 原生支持模块之前,使用模块的 JavaScript 代码本质上是希望使用默认没有的语言特性。因此,必须按照符合某种规范的模块语法来编写代码,另外还需要单独的模块工具把这些模块语法与JavaScript 运行时连接起来。这里的模块语法和连接方式有不同的表现形式,通常需要在浏览器中额外加载库或者在构建时完成预处理。

3.1 CommonJS

CommonJS 规范概述了同步声明依赖的模块定义。这个规范主要用于在服务器端实现模块化代码组织,但也可用于定义在浏览器中使用的模块依赖。CommonJS 模块语法不能在浏览器中直接运行。

注意: 一般认为,Node.js的模块系统使用了CommonJS规范,实际上并不完全正确。Node.js 使用了轻微修改版本的 CommonJS,因为 Node.js 主要在服务器环境下使用,所以不需要考虑网络延迟问题。考虑到一致性,本节使用 Node.js 风格的模块定义语法。

CommonJS 模块定义需要使用 require()指定依赖,使用 exports 对象定义自己的公共 API。下面的代码展示了简单的模块定义:

var moduleB = require('./moduleB');
 
module.exports = { 
 	stuff: moduleB.doStuff(); 
}; 

moduleA 通过使用模块定义的相对路径来指定自己对 moduleB 的依赖。什么是“模块定义”,以及如何将字符串解析为模块,完全取决于模块系统的实现。比如在 Node.js 中,模块标识符可能指向文件,也可能指向包含 index.js 文件的目录。

请求模块会加载相应模块,而把模块赋值给变量也非常常见,但赋值给变量不是必需的。调用require()意味着模块会原封不动地加载进来:

console.log('moduleA'); 
require('./moduleA'); // "moduleA" 

无论一个模块在 require()中被引用多少次,模块永远是单例。在下面的例子中,moduleA 只会被打印一次。这是因为无论请求多少次,moduleA 只会被加载一次。

console.log('moduleA'); 
var a1 = require('./moduleA'); 
var a2 = require('./moduleA'); 

console.log(a1 === a2); // true 

模块第一次加载后会被缓存,后续加载会取得缓存的模块(如下代码所示)。模块加载顺序由依赖图决定。

console.log('moduleA'); 
require('./moduleA'); 
require('./moduleB'); // "moduleA" 
require('./moduleA'); 

在 CommonJS 中,模块加载是模块系统执行的同步操作。因此 require()可以像下面这样以编程方式嵌入在模块中:

console.log('moduleA'); 

if (loadCondition) { 
 	require('./moduleA'); 
} 

这里,moduleA 只会在 loadCondition 求值为 true 时才会加载。这个加载是同步的,因此 if() 块之前的任何代码都会在加载 moduleA 之前执行,而 if()块之后的任何代码都会在加载 moduleA 之后执行。同样,加载顺序规则也会适用。因此,如果 moduleA 已经在前面某个地方加载过了,这个条件 require()就意味着只暴露 moduleA 这个命名空间而已。

在上面的例子中,模块系统是 Node.js 实现的,因此./moduleB 是相对路径,指向与当前模块位于同一目录中的模块目标。Node.js 会使用 require()调用中的模块标识符字符串去解析模块引用。在 Node.js 中可以使用绝对或相对路径,也可以使用安装在 node_modules 目录中依赖的模块标识符。我们并不关心这些细节,重要的是知道在不同的 CommonJS 实现中模块字符串引用的含义可能不同。不过,所有 CommonJS 风格的实现共同之处是模块不会指定自己的标识符,它们的标识符由其在模块文件层级中的位置决定。

指向模块定义的路径可能引用一个目录,也可能是一个 JavaScript 文件。无论是什么,这与本地模块实现无关,而 moduleB 被加载到本地变量中。moduleA 在 module.exports 对象上定义自己的公共接口,即 foo 属性。

如果有模块想使用这个接口,可以像下面这样导入它:

var moduleA = require('./moduleA'); 

console.log(moduleA.stuff); 

注意,此模块不导出任何内容。即使它没有公共接口,如果应用程序请求了这个模块,那也会在加载时执行这个模块体。

module.exports 对象非常灵活,有多种使用方式。如果只想导出一个实体,可以直接给 module.exports 赋值:

module.exports = 'foo';

这样,整个模块就导出一个字符串,可以像下面这样使用:

var moduleA = require('./moduleB'); 

console.log(moduleB); 	// 'foo'

导出多个值也很常见,可以使用对象字面量赋值或每个属性赋一次值来实现:

// 等价操作:

module.exports = { 
 	a: 'A', 
 	b: 'B' 
}; 

module.exports.a = 'A'; 
module.exports.b = 'B'; 

模块的一个主要用途是托管类定义(这里使用 ES6 风格的类定义,不过 ES5 风格也兼容):

class A {} 

module.exports = A; 
var A = require('./moduleA'); 

var a = new A();

也可以将类实例作为导出值:

class A {} 

module.exports = new A(); 

此外,CommonJS 也支持动态依赖:

if (condition) { 
 	var A = require('./moduleA'); 
} 

CommonJS 依赖几个全局属性如 require 和 module.exports。如果想在浏览器中使用 CommonJS模块,就需要与其非原生的模块语法之间构筑“桥梁”。模块级代码与浏览器运行时之间也需要某种“屏障”,因为没有封装的 CommonJS 代码在浏览器中执行会创建全局变量。这显然与模块模式的初衷相悖。

常见的解决方案是提前把模块文件打包好,把全局属性转换为原生 JavaScript 结构,将模块代码封装在函数闭包中,最终只提供一个文件。为了以正确的顺序打包模块,需要事先生成全面的依赖图。

3.2 Asynchronous Module Definition
3.3 Universal Module Definition

In an attempt to unify the CommonJS and AMD ecosystems, the Universal Module Definition (UMD) convention was introduced to create module code that could be used by both systems. Essentially, the pattern defines modules in a way that detects which module system is being used upon startup, configures it as appropriate, and wraps the whole thing in an immediately invoked function expression. It
is an imperfect combination, but for the purposes of combining the two ecosystems it is suitable in a
surprisingly large number of scenarios.

An example module with a single dependency (based on the UMD repository on GitHub) is as follows:

(function (root, factory) {
 if (typeof define === 'function' && define.amd) {
 // AMD. Register as an anonymous module.
 define(['moduleB'], factory);
 } else if (typeof module === 'object' && module.exports) {
 // Node. Does not work with strict CommonJS, but
 // only CommonJS-like environments that support module.exports,
 // like Node.
 module.exports = factory(require(' moduleB '));
 } else {
 // Browser globals (root is window)
 root.returnExports = factory(root. moduleB);
 }
}(this, function (moduleB) {
 //use moduleB in some fashion.
 // Just return a value to define the module export.
 // This example returns an object, but the module
 // can return a function as the exported value.
 return {};
}));

There are variations on this pattern that enable support for strict CommonJS and browser globals.
You should never be expected to be authoring this exact wrapper by hand—it should be automatically generated by a build tool. Your goal is to be concerned with the content of the modules, not the
boilerplate that connects each of them.

3.4 Module Loader Deprecation

Ultimately, the patterns shown in this section will become increasingly obsolete as support broadens for the ECMAScript 6 module specification. That being so, it still is quite useful to know what the ES6 module specification grew out of in order to learn why design decisions were chosen. The intense conflict between CommonJS and AMD cultivated the ECMAScript 6 module specification that we
now enjoy.

4.4 Module Exports

The public exports system for ES6 modules is very similar to CommonJS. The exports keyword is used to control what parts of a module are visible to external modules. There are two types of exports in ES6 modules: named exports and default exports. Different types of exports means they are imported differently—this is covered in the following section.

The export keyword is used to declare a value as a named export. Exports must occur in the toplevel of the module; they cannot be nested inside blocks:

// Allowed
export ... 

// Disallowed
if (condition) { 
 	export ... 
} 

Exporting a value has no direct effect on JavaScript execution inside a module, so there is no restriction on the locality of the export statement relative to what is being exported, or what order the export keyword must appear in the module. An export may even precede the declaration of the value it is exporting:

// Allowed
const foo = 'foo'; 
export { foo }; 

// Allowed
export const foo = 'foo'; 

// Allowed, but avoid
export { foo }; 
const foo = 'foo'; 

A named export behaves as if the module is a container for exported values. Inline named exports, as the name suggests, can be performed in the same line as variable declaration. In the following example, a variable declaration is paired with an inline export. An external module could import this module, and the value foo would be available inside it as a property of that module:

export const foo = 'foo';

Declaration does not need to occur in the same line as the export; you can perform the declaration and export the identifier elsewhere in the module inside an export clause:

const foo = 'foo'; 
export { foo };

It is also possible to provide an alias when exporting. An alias must occur inside the export clause bracket syntax; therefore, declaring a value, exporting it, and providing an alias cannot all be done in the same line. In the following example, an external module would access this value by importing this module and using the myFoo export:

const foo = 'foo'; 
export { foo as myFoo }; 

Because ES6 named exports allow you to treat the module as a container, you can declare multiple named exports inside a single module. Values can be declared inside the export statement, or they can be declared prior to specifying it as an export:

export const foo = 'foo'; 
export const bar = 'bar'; 
export const baz = 'baz';

Because exporting multiple values is common behavior, grouping export declarations is supported, as is aliasing some or all of those exports:

const foo = 'foo'; 
const bar = 'bar'; 
const baz = 'baz'; 
export { foo, bar as myBar, baz };

A default export behaves as if the module is the same entity as the exported value. The default keyword modifier is used to declare a value as a default export, and there can only ever be a single default export. Attempting to specify duplicate default exports will result in a SyntaxError.

In the following example, an external module could import this module, and the module itself would be the value of foo:

const foo = 'foo'; 
export default foo;

Alternately, the ES6 module system will recognize the default keyword when provided as an alias, and will apply the default export to the value even though it uses the named export syntax:

const foo = 'foo'; 

// 等同于 export default foo; 
export { foo as default }; 

Because there are no incompatibilities between named exports and default exports, ES6 allows you to use both in the same module:

const foo = 'foo'; 
const bar = 'bar'; 

export { bar }; 
export default foo; 

The two export statements can be combined into the same line:

const foo = 'foo'; 
const bar = 'bar'; 

export { foo as default, bar };

The ES6 specification restricts what can and cannot be done inside various forms of an export statement. Some forms allow declaration and assignment, some forms allow only expressions, and some forms only allow simple identifiers. Note that some forms utilize semicolons, and some do not.

// Named inline exports 
export const baz = 'baz'; 
export const foo = 'foo', bar = 'bar'; 
export function foo() {} 
export function* foo() {} 
export class Foo {} 

// Named clause exports
export { foo }; 
export { foo, bar }; 
export { foo as myFoo, bar }; 

// Default exports 
export default 'foo'; 
export default 123; 
export default /[a-z]*/; 
export default { foo: 'foo' }; 
export { foo, bar as default }; 
export default foo 
export default function() {} 
export default function foo() {} 
export default function*() {} 
export default class {} 

// Various disallowed forms that will cause errors:

// Variable declarations cannot occur inside inline default exports
export default const foo = 'bar'; 

// Only identifiers can appear in export clauses
export { 123 as foo } 

// Aliasing only can occur in export clauses
export const foo = 'foo' as myFoo;
4.5 Module Imports

Modules can use exports from other modules using the import keyword. Like export, import must appear in the top level of a module:

// Allowed
import ... 

// Disallowed
if (condition) { 
 	import ... 
} 

import statements are hoisted to the top of the module. Therefore, like the export keyword, the order in which import statements appear relative to the use of the imported values is unimportant. It is recommended, however, that imports be kept at the top of the module.

// Allowed
import { foo } from './fooModule.js'; 
console.log(foo); // 'foo' 

// Allowed, but avoid
console.log(foo); // 'foo' 
import { foo } from './fooModule.js'; 

A module identifier can be either the relative path to that module file from the current module or the absolute path to that module file from the base path. It must be a plain string; the identifier cannot be dynamically computed, for example, concatenating strings.

If the modules are being natively loaded in the browser via the path in their module identifier, a .js extension is required for the correct file to be referenced. However, if the ES6 modules are being bundled or interpreted by a build tool or third-party module loader, you may not need to include the file extension of the module in its identifier.

// Resolves to /components/bar.js
import ... from './bar.js';

// Resolves to /bar.js
import ... from '../bar.js'; 

// Resolves to /bar.js
import ... from '/bar.js'; 

Modules do not need to be imported via their exported members. If you do not need specific exported bindings from a module, but you still seed to load and execute the module for its side effects, you can load it with only its path:

import './foo.js'; 

Imports are treated as read-only views to the module, effectively the same as const-declared variables. When performing a bulk import using *, the aliased collection of named exports behaves as if it were treated with Object.freeze(). Direct manipulation of exported values is impossible, although modifying properties of an exported object is still possible. Adding or removing exported properties of the exported collection is also disallowed. Mutation of exported values must occur using exported methods that have access to internal variables and properties.

import foo, * as Foo './foo.js'; 

foo = 'foo'; 		// Error

Foo.foo = 'foo'; 	// Error

foo.bar = 'bar'; 	// Allowed

The distinction between named exports and default exports is mirrored in the way they are imported. Named exports can be retrieved in bulk without specifying their exact identifier using * and providing an identifier for the collection of exports:

const foo = 'foo', bar = 'bar', baz = 'baz'; 
export { foo, bar, baz } 
import * as Foo from './foo.js'; 

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

To perform explicit imports, the identifiers can be placed inside an import clause. Using an import clause also allows you to specify aliases for the imports:

import { foo, bar, baz as myBaz } from './foo.js'; 

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

Default exports behave as if the module target is the exported value. They can be imported using the default keyword and providing an alias; alternately, they can be imported without the use of curly braces, and the identifier you specify is effectively the alias for the default export:

// Equivalent
import { default as foo } from './foo.js'; 
import foo from './foo.js'; 

If a module exports both named exports and default exports, it’s possible to retrieve them in the same import statement. This retrieval can be performed by enumerating specific exports, or using *:

import foo, { bar, baz } from './foo.js'; 

import { default as foo, bar, baz } from './foo.js'; 

import foo, * as Foo from './foo.js'; 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值