模块讲解,的一次更新说明
分类:前端技术

关于 Node.js 里 ES6 Modules 的一次更新说明

2017/04/27 · JavaScript · es6

原文出处: James M Snell   译文出处:凹凸实验室   

几个月前,我写了一篇文章来描述 Node.js 现存的 CommonJS 模块和新的 ES6 模块系统的许多不同,也说明了在 Node.js 内核中实现这个新模型的内在的一些挑战。现在,我想分享一下关于这件事情的进展情况。

【转】

模块通常是指编程语言所提供的代码组织机制,利用此机制可将程序拆解为独立且通用的代码单元。所谓模块化主要是解决代码分割、作用域隔离、模块之间的依赖管理以及发布到生产环境时的自动化打包与处理等多个方面。

明白你什么时候该知道你需要知道的东西

在这之前,如果你还没准备好,你可以花一点时间来看一下我之前的描述这两个模块架构上存在许多根本区别的文章。总结来说就是:CommonJS 与 ES6 Modules 之间的关键不同在于代码什么时候知道一个模块的结构和使用它。

举个栗子,假如我现在有一个简单的 ComminJS 模块(模块名叫'foobar'):

JavaScript

function foo() { return 'bar'; } function bar() { return 'foo'; } module.exports.foo = foo; module.exports.bar = bar;

1
2
3
4
5
6
7
8
function foo() {
  return 'bar';
}
function bar() {
  return 'foo';
}
module.exports.foo = foo;
module.exports.bar = bar;

现在我们在一个叫 app.js 的 JS 文件中引用它

JavaScript

const {foo, bar} = require('foobar'); console.log(foo(), bar());

1
2
const {foo, bar} = require('foobar');
console.log(foo(), bar());

当我执行 $node app.js 的时候,Node.js 已二进制的形式加载 app.js 文件,解析它,并且开始执行里面的代码。在执行过程中,里面的 require() 方法被调用,然后它会同步的去加载 foobar.js 的内容进内存,同步的解析编译里面的 JavaScript 代码,同步的执行里面的代码,然后返回 module.exports 的值当做 app.js 里的 require('foobar') 的返回值。当 app.js 里的 require() 方法返回的时候,foobar 模块的结构就已经知道了,并且可以被使用。所有的这些事情都发生在 Node.js 进程事件循环的同一个周期里。

要理解 CommonJS 与 ES6 Modules 之间的不同至关重要的是,一个 CommonJS 的模块在没有被执行完之前,它的结构(API)是不可知的 — 即使在它被执行完以后,它的结构也可以随时被其他代码修改。

现在我们用 ES6 的写法来写同样的模块:

JavaScript

export function foo() { return 'bar'; } export function bar() { return 'foo'; }

1
2
3
4
5
6
export function foo() {
  return 'bar';
}
export function bar() {
  return 'foo';
}

并且在代码中引用它:

JavaScript

import {foo, bar} from 'foobar'; console.log(foo()); console.log(bar());

1
2
3
import {foo, bar} from 'foobar';
console.log(foo());
console.log(bar());

从 ECMAScript 统一的标准来看,ES6 Modules 的步骤与 CommonJS 里已经实现的有很大的不同。第一步从硬盘上加载文件内容大致上是相同的,但是可能是异步的。当内容加载完成后,会解析它。在解析的同时,模块里被 export 声明定义的结构会在组件内容被执行之前就探知出来。一旦结构被探知出来,组件的代码就会被执行。这里重要的是记住所有的 import 和 export 语句都会在代码执行之前被解析出来。另一点是在 ES6 中是允许这个解析的步骤异步执行的。这就意味着,在 Node.js 的机制中,加载脚本内容、解析模块的 import 和 export 、执行模块代码将发生在多个事件循环里。

遵循的模块化规范不一样

模块的优点

时机很重要

在评估 ES6 Modules 的可实现性之前,我们关注的重点是怎么样无缝衔接的实现它。比如我们希望它可以可以实现同时对两种模块的支持,这样可以很大程度上对用户是透明的。

可惜,事情并不是这么简单…

尤其是 ES6 Modules 的加载、解析和执行都是异步的,这就导致不能通过 require() 来引用一个 ES6 模块。原因是 require() 是一个完全同步的函数。如果我们去修改 require() 的语义让它可以进行异步加载的话,那对于现有的生态系统将会产生巨大的破坏。所以我们有考虑在 ES6 的 import() 函数提议(详情)通过之后建模实现一个 require.import() 函数。这个函数会返回一个 Promise 在 ES6 模块加载完成后标记完成。这不是最好的方案,但是它可以让你在现有的 Node.js 里以 CommonJS 的格式来使用。

有一点好消息是在 ES6 模块里可以很方便地使用 import 来引用一个 CommonJS 模块。因为在 ES6 模块里异步加载不是必须的。ECMAScript 规范进行一些小修改就可以更好地支持这种方式。但是所有这些工作过后,还有一个重要的事情…

模块化规范:即为 JavaScript 提供一种模块编写、模块依赖和模块运行的方案。谁让最初的 JavaScript 是那么的裸奔呢——全局变量就是它的模块化规范。

可维护性。因为模块是独立的,一个设计良好的模块会让外面的代码对自己的依赖越少越好,这样自己就可以独立去更新和改进。

命名引用

命名引用是 ES6 Modules 里的一个基本的特性。举个例子:

JavaScript

import {foo, bar} from 'foobar';

1
import {foo, bar} from 'foobar';

变量 foobar 在解析阶段就从 foobar 中被引用进来 —— 在所有代码被执行之前。因为 ES6 Modules 的结构是之前就可以被探知到的。

另一方面,在 CommonJS 里模块结构在代码没有执行之前是不能被探知的。也就是说,如果不对 ECMAScript 规范做重大更改的话,在 CommonJS 模块里是不能使用命名引用的。开发者会引用到 ES6 Modules 里面的名为 “default” 的导出。比如,上面的例子在 CommonJS 里是这样的:

JavaScript

import foobar from 'foobar'; console.log(foobar.foo(), foobar.bar());

1
2
import foobar from 'foobar';
console.log(foobar.foo(), foobar.bar());

区别很小但是很重要。所以当你想使用 import 来引用一个 CommonJS 模块的时候,下面这种写法是根本行不通的:

JavaScript

import {foo, bar} from 'foobar';

1
import {foo, bar} from 'foobar';

这里的 foobar 不会直接被解析成 CommonJS 模块里导出的 foo()bar() 方法。

require/exports 出生在野生规范当中,什么叫做野生规范?即这些规范是 JavaScript 社区中的开发者自己草拟的规则,得到了大家的承认或者广泛的应用。比如 CommonJS、AMD、CMD 等等。import/export 则是名门正派。TC39 制定的新的 ECMAScript 版本,即 ES6(ES2015)中包含进来。

命名空间。在 JavaScript 里面,如果一个变量在最顶级的函数之外声明,它就直接变成全局可用。因此,常常不小心出现命名冲突的情况。使用模块化开发来封装变量,可以避免污染全局环境。

但是在 Babel 里可以!

使用过像 Babel 这种的 ES6 Modules 语法转换工具的人应该很熟悉命名引用。Babel 的工作原理是把 ES6 的写法转换成可以在 Node.js 里运行的 CommonJS 的形式。虽然语法看起来很像 ES6,但是实际上并不是。这一点很重要,Babel 里的 ES6 命名引用与完全按照规范实现的 ES6 命名引用有本质的不同。

出现的时间不同

重用代码。我们有时候会喜欢从之前写过的项目中拷贝代码到新的项目,这没有问题,但是更好的方法是,通过模块引用的方式,来避免重复的代码库。

Michael Jackson Script

实际上CommonJS 和 ES6 Modules 之间还有另外一个重要的不同就是,ECMAScript 编译器必须提前知道它加载的代码是 CommonJS 的还是 ES6 Modules 的。原因是之前说的 ES6 Modules 必须在代码执行前就解析出模块中的 importexport 声明。

这就意味着 Node.js 需要某些机制来预先识别它在加载那种类型的文件。在探索了很多方案以后,我们回归到了以前最糟糕的方案,就是引入一个新的 *.mjs 文件后缀来表示一个 ES6 Modules 的 JavaScript 文件。(之前我们亲切的叫它 “Michael Jackson Script”)

require/exports 相关的规范由于野生性质,在 2010 年前后出生。AMD、CMD 相对命比较短,到 2014 年基本上就摇摇欲坠了。一开始大家还比较喜欢在浏览器上采用这种异步小模块的加载方式,但并不是银弹。随着 Node.js 流行和 Browsersify 的兴起,运行时异步加载逐渐被构建时模块合并分块所替代。Wrapper 函数再也不需要了。 2014 年 Webpack 还是新玩意,现在已经是前端必备神器了。

CommonJS

时间线

在目前的时间点上,在 Node.js 可以开始处理支持实现 ES6 Modules 之前,还有很多关于规范现实的问题和虚拟机方面的问题。相关工作还在进行,但是需要一些时间 —— 我们目前估计至少需要一年左右。

1 赞 收藏 评论

图片 1

Browsersify、Webpack 一开始的目的就是打包 CommonJS 模块。

CommonJS 最开始是 Mozilla 的工程师于 2009 年开始的一个项目,它的目的是让浏览器之外的 JavaScript (比如服务器端或者桌面端)能够通过模块化的方式来开发和协作。

CommonJS 作为 Node.js 的规范,一直沿用至今。由于 npm 上 CommonJS 的类库众多,以及 CommonJS 和 ES6 之间的差异,Node.js(这里不太准确) 无法直接兼容 ES6。所以现阶段 require/exports 仍然是必要且必须的。出自 ES6 的  import/export 相对就晚了许多。被大家所熟知和使用也是 2015 年之后的事了。 这其实要感谢 babel(原来项目名叫做 6to5,后更名为 babel) 这个神一般的项目。由于有了 babel 将还未被宿主环境(各浏览器、Node.js)直接支持的 ES6 Module 编译为 ES5 的 CommonJS —— 也就是 require/exports 这种写法 —— Webpack 插上 babel-loader 这个翅膀才开始高飞,大家也才可以称 " 我在使用 ES6! "

在 CommonJS 的规范中,每个 JavaScript 文件就是一个独立的模块上下文(module context),在这个上下文中默认创建的属性都是私有的。也就是说,在一个文件定义的变量(还包括函数和类),都是私有的,对其他文件是不可见的。

这也就是为什么前面说 require/exports 是必要且必须的。因为事实是,目前你编写的 import/export 最终都是编译为 require/exports 来执行的。

需要注意的是,CommonJS 规范的主要适用场景是服务器端编程,所以采用同步加载模块的策略。如果我们依赖3个模块,代码会一个一个依次加载它们。

require/exports 和 import/export 形式不一样

该模块实现方案主要包含 require 与 module 这两个关键字,其允许某个模块对外暴露部分接口并且由其他模块导入使用。

require/exports 的用法只有以下三种简单的写法:

//sayModule.js

const fs = require('fs')exports.fs = fsmodule.exports = fs

function SayModule () {

而 import/export 的写法就多种多样:

   this.hello = function () {

import fs from 'fs'import {default as fs} from 'fs'import * as fs from 'fs'import {readFile} from 'fs'import {readFile as read} from 'fs'import fs, {readFile} from 'fs'export default fsexport const fsexport function readFileexport {readFile, read}export * from 'fs'

       console.log('hello');

require/exports 和 import/export 本质上的差别

   };

形式上看起来五花八门,但本质上:

   this.goodbye = function () {

CommonJS 还是 ES6 Module 输出都可以看成是一个具备多个属性或者方法的对象;

       console.log('goodbye');

default 是 ES6 Module 所独有的关键字,export default fs 输出默认的接口对象,import fs from 'fs' 可直接导入这个对象;

   };

ES6 Module 中导入模块的属性或者方法是强绑定的,包括基础类型;而 CommonJS 则是普通的值传递或者引用传递。

}

1、2 相对比较好理解,3 需要看个例子:

module.exports = SayModule;

// counter.jsexports.count=0setTimeout(function(){console.log('increase count to', exports.count,'in counter.js after 500ms')},500)// commonjs.jsconst{count}=require('./counter')setTimeout(function(){console.log('read count after 1000ms in commonjs is',count)},1000)//es6.jsimport{count}from'./counter'setTimeout(function(){console.log('read count after 1000ms in es6 is',count)},1000)

//main.js 引入sayModule.js

分别运行 commonjs.js 和 es6.js:

var Say = require('./sayModule.js');

➜testnode commonjs.jsincrease count to1in counter.js after 500msreadcount after 1000ms in commonjs is 0➜testbabel-node es6.jsincrease count to1in counter.js after 500msreadcount after 1000ms in es6 is 1

var sayer = new Say();

作者:寸志

sayer.hello(); //hello

链接:

作为一个服务器端的解决方案,CommonJS 需要一个兼容的脚本加载器作为前提条件。该脚本加载器必须支持名为 require 和 module.exports 的函数,它们将模块相互导入导出。

来源:知乎

Node.js

【转】commonjs模块与es6模块的区别

Node 从 CommonJS 的一些创意中,创造出自己的模块化实现。由于Node 在服务端的流行,Node 的模块形式被(不正确地)称为 CommonJS。

到目前为止,已经实习了3个月的时间了。最近在面试,在面试题里面有题目涉及到模块循环加载的知识。趁着这个机会,将commonjs模块与es6模块之间一些重要的的区别做个总结。语法上有什么区别就不具体说了,主要谈谈引用的区别。

Node.js模块可以分为两大类,一类是核心模块,另一类是文件模块。

转载请注明出处:commonjs模块与es6模块的区别

核心模块

commonjs

就是Node.js标准的API中提供的模块,如fs、http、net等,这些都是由Node.js官方提供的模块,编译成了二进制代码,可以直接通过require获取核心模块,例如require('fs'),核心模块拥有最高的加载优先级,如果有模块与核心模块命名冲突,Node.js总是会加载核心模块。

对于基本数据类型,属于复制。即会被模块缓存。同时,在另一个模块可以对该模块输出的变量重新赋值。

文件模块

对于复杂数据类型,属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。

是存储为单独的文件(或文件夹)的模块,可能是JavaScript代码、JSON或编译好的C/C 代码。在不显式指定文件模块扩展名的时候,Node.js会分别试图加上.js、.json、.node(编译好的C/C 代码)。

当使用require命令加载某个模块时,就会运行整个模块的代码。

加载方式

当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,commonjs模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。

按路径加载模块

循环加载时,属于加载时执行。即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

如果require参数一"/"开头,那么就以绝对路径的方式查找模块名称,如果参数一"./"、"../"开头,那么则是以相对路径的方式来查找模块。

ES6模块

通过查找node_modules目录加载模块

es6模块中的值属于【动态只读引用】。

如果require参数不以"/"、"./"、"../"开头,而该模块又不是核心模块,那么就要通过查找node_modules加载模块了。我们使用的npm获取的包通常就是以这种方式加载的。

对于只读来说,即不允许修改引入变量的值,import的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

加载缓存

对于动态来说,原始值发生变化,import加载的值也会发生变化。不论是基本数据类型还是复杂数据类型。

Node.js模块不会被重复加载,这是因为Node.js通过文件名缓存所有加载过的文件模块,所以以后再访问到时就不会重新加载了。

循环加载时,ES6模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行。

注意:Node.js是根据实际文件名缓存的,而不是require()提供的参数缓存的,也就是说即使你分别通过require('express')和require('./node_modules/express')加载两次,也不会重复加载,因为尽管两次参数不同,解析到的文件却是同一个。

上面说了一些重要区别。现在举一些例子来说明每一点吧

Node.js 中的模块在加载之后是以单例化运行,并且遵循值传递原则:如果是一个对象,就相当于这个对象的引用。

commonjs

模块载入过程

对于基本数据类型,属于复制。即会被模块缓存。同时,在另一个模块可以对该模块输出的变量重新赋值。

加载文件模块的工作,主要由原生模块module来实现和完成,该原生模块在启动时已经被加载,进程直接调用到runMain静态方法。

// b.js

例如运行: node app.js

let count = 1

Module.runMain = function () {

let plusCount = () => {

   // Load the main module--the command line argument.

count

   Module._load(process.argv[1], null, true);

}

};

setTimeout(() => {

//_load静态方法在分析文件名之后执行

console.log('b.js-1', count)

var module = new Module(id, parent);

}, 1000)

//并根据文件路径缓存当前模块对象,该模块实例对象则根据文件名加载。

module.exports = {

module.load(filename);

count,

具体说一下上文提到了文件模块的三类模块,这三类文件模块以后缀来区分,Node.js会根据后缀名来决定加载方法,具体的加载方法在下文 require.extensions中会介绍。

plusCount

.js通过fs模块同步读取js文件并编译执行。

}

.node通过C/C 进行编写的Addon。通过dlopen方法进行加载。

// a.js

.json读取文件,调用JSON.parse解析加载。

let mod = require('./b.js')

接下来详细描述js后缀的编译过程。Node.js在编译js文件的过程中实际完成的步骤有对js文件内容进行头尾包装。以app.js为例,包装之后的app.js将会变成以下形式:

console.log('a.js-1', mod.count)

//circle.js

mod.plusCount()

var PI = Math.PI;

console.log('a.js-2', mod.count)

exports.area = function (r) {

setTimeout(() => {

   return PI * r * r;

mod.count = 3

};

console.log('a.js-3', mod.count)

exports.circumference = function (r) {

}, 2000)

   return 2 * PI * r;

node a.js

};

a.js-1 1

//app.js

a.js-2 1

var circle = require('./circle.js');

b.js-1 2  // 1秒后

console.log( 'The area of a circle of radius 4 is ' circle.area(4));

a.js-3 3  // 2秒后

//app包装后

以上代码可以看出,b模块export的count变量,是一个复制行为。在plusCount方法调用之后,a模块中的count不受影响。同时,可以在b模块中更改a模块中的值。如果希望能够同步代码,可以export出去一个getter。

(function (exports, require, module, __filename, __dirname) {

// 其他代码相同

   var circle = require('./circle.js');

module.exports = {

   console.log('The area of a circle of radius 4 is ' circle.area(4));

get count () {

});

return count

//这段代码会通过vm原生模块的runInThisContext方法执行(类似eval,只是具有明确上下文,不污染全局),返回为一个具体的function对象。最后传入module对象的exports,require方法,module,文件名,目录名作为实参并执行。

},

这就是为什么require并没有定义在app.js 文件中,但是这个方法却存在的原因。从Node.js的API文档中可以看到还有 __filename、 __dirname、 module、 exports几个没有定义但是却存在的变量。其中 __filename和 __dirname在查找文件路径的过程中分析得到后传入的。 module变量是这个模块对象自身, exports是在module的构造函数中初始化的一个空对象({},而不是null)。

plusCount

在这个主文件中,可以通过require方法去引入其余的模块。而其实这个require方法实际调用的就是module._load方法。

}

load方法在载入、编译、缓存了module后,返回module的exports对象。这就是circle.js文件中只有定义在exports对象上的方法才能被外部调用的原因。

node a.js

以上所描述的模块载入机制均定义在lib/module.js中。

a.js-1 1

require 函数

a.js-2 1

require 引入的对象主要是函数。当 Node 调用 require() 函数,并且传递一个文件路径给它的时候,Node 会经历如下几个步骤:

b.js-1 2  // 1秒后

Resolving:找到文件的绝对路径;

a.js-3 2  // 2秒后, 由于没有定义setter,因此无法对值进行设置。所以还是返回2

Loading:判断文件内容类型;

对于复杂数据类型,属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。

Wrapping:打包,给这个文件赋予一个私有作用范围。这是使 require 和 module 模块在本地引用的一种方法;

// b.js

Evaluating:VM 对加载的代码进行处理的地方;

let obj = {

Caching:当再次需要用这个文件的时候,不需要重复一遍上面步骤。

count: 1

require.extensions 来查看对三种文件的支持情况:

}

可以清晰地看到 Node 对每种扩展名所使用的函数及其操作:对 .js 文件使用 module._compile;对 .json 文件使用 JSON.parse;对 .node 文件使用 process.dlopen。

let plusCount = () => {

文件查找策略

obj.count

从文件模块缓存中加载

}

尽管原生模块与文件模块的优先级不同,但是优先级最高的是从文件模块的缓存中加载已经存在的模块。

setTimeout(() => {

从原生模块加载

console.log('b.js-1', obj.count)

原生模块的优先级仅次于文件模块缓存的优先级。require方法在解析文件名之后,优先检查模块是否在原生模块列表中。以http模块为例,尽管在目录下存在一个 http、 http.js、 http.node、 http.json文件, require(“http”)都不会从这些文件中加载,而是从原生模块中加载。

}, 1000)

原生模块也有一个缓存区,同样也是优先从缓存区加载。如果缓存区没有被加载过,则调用原生模块的加载方式进行加载和执行。

setTimeout(() => {

从文件加载

console.log('b.js-2', obj.count)

当文件模块缓存中不存在,而且不是原生模块的时候,Node.js会解析require方法传入的参数,并从文件系统中加载实际的文件,加载过程中的包装和编译细节在前面说过是调用load方法。

}, 3000)

当 Node 遇到 require(X) 时,按下面的顺序处理。

module.exports = {

1、如果 X 是内置模块(比如 require('http'))

obj,

a. 返回该模块。

plusCount

b. 不再继续执行。

}

2、如果 X 以 "./" 或者 "/" 或者 "../" 开头

// a.js

a. 根据 X 所在的父模块,确定 X 的绝对路径。

var mod = require('./b.js')

b. 将 X 当成文件,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。

console.log('a.js-1', mod.obj.count)

X

mod.plusCount()

X.js

console.log('a.js-2', mod.obj.count)

X.json

setTimeout(() => {

X.node

mod.obj.count = 3

c. 将 X 当成目录,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。

console.log('a.js-3', mod.obj.count)

X/package.json(main字段)

}, 2000)

X/index.js

node a.js

X/index.json

a.js-1 1

X/index.node

a.js-2 2

3、如果 X 不带路径

b.js-1 2

a. 根据 X 所在的父模块,确定 X 可能的安装目录。

a.js-3 3

b. 依次在每个目录中,将 X 当成文件名或目录名加载。

b.js-2 3

4、抛出 "not found"

以上代码可以看出,对于对象来说属于浅拷贝。当执行a模块时,首先打印obj.count的值为1,然后通过plusCount方法,再次打印时为2。接着在a模块修改count的值为3,此时在b模块的值也为3。

模块循环依赖

3.当使用require命令加载某个模块时,就会运行整个模块的代码。

//创建两个文件,module1.js 和 module2.js,并且让它们相互引用

4.当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,commonjs模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。

   // module1.js

5.循环加载时,属于加载时执行。即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

   exports.a = 1;

3, 4, 5可以使用同一个例子说明

   require('./module2');

// b.js

   exports.b = 2;

exports.done = false

   exports.c = 3;

let a = require('./a.js')

   // module2.js

console.log('b.js-1', a.done)

   const Module1 = require('./module1');

exports.done = true

   console.log('Module1 is partially loaded here', Module1);

console.log('b.js-2', '执行完毕')

在 module1 完全加载之前需要先加载 module2,而 module2 的加载又需要 module1。这种状态下,我们从 exports 对象中能得到的就是在发生循环依赖之前的这部分。上面代码中,只有 a 属性被引入,因为 b 和 c 都需要在引入 module2 之后才能加载进来。

// a.js

Node 使这个问题简单化,在一个模块加载期间开始创建 exports 对象。如果它需要引入其他模块,并且有循环依赖,那么只能部分引入,也就是只能引入发生循环依赖之前所定义的这部分。

exports.done = false

AMD

let b = require('./b.js')

AMD 是 Asynchronous Module Definition 的简称,即“异步模块定义”,是从 CommonJS 讨论中诞生的。AMD 优先照顾浏览器的模块加载场景,使用了异步加载和回调的方式。

console.log('a.js-1', b.done)

AMD 和 CommonJS 一样需要脚本加载器,尽管 AMD 只需要对 define 方法的支持。define 方法需要三个参数:模块名称,模块运行的依赖数组,所有依赖都可用之后执行的函数(该函数按照依赖声明的顺序,接收依赖作为参数)。只有函数参数是必须的。define 既是一种引用模块的方式,也是定义模块的方式。

exports.done = true

// file lib/sayModule.js

console.log('a.js-2', '执行完毕')

define(function (){

// c.js

   return {

let a = require('./a.js')

       sayHello: function () {

let b = require('./b.js')

           console.log('hello');

console.log('c.js-1', '执行完毕', a.done, b.done)

       }

node c.js

   };

b.js-1 false

});

b.js-2 执行完毕

//file main.js

a.js-1 true

define(['./lib/sayModule'], function (say){

a.js-2 执行完毕

   say.sayHello(); //hello

c.js-1 执行完毕 true true

})

仔细说明一下整个过程。

main.js 作为整个应用的入口模块,我们使用 define 关键字声明了该模块以及外部依赖(没有生命模块名称);当我们执行该模块代码时,也就是执行 define 函数的第二个参数中定义的函数功能,其会在框架将所有的其他依赖模块加载完毕后被执行。这种延迟代码执行的技术也就保证了依赖的并发加载。

在Node.js中执行c模块。此时遇到require关键字,执行a.js中所有代码。

RequireJS

在a模块中exports之后,通过require引入了b模块,执行b模块的代码。

RequireJS 是一个前端的模块化管理的工具库,遵循AMD规范,通过一个函数来将所有所需要的或者说所依赖的模块实现装载进来,然后返回一个新的函数(模块),我们所有的关于新模块的业务代码都在这个函数内部操作,其内部也可无限制的使用已经加载进来的以来的模块。

在b模块中exports之后,又require引入了a模块,此时执行a模块的代码。

//scripts下的main.js则是指定的主代码脚本文件,所有的依赖模块代码文件都将从该文件开始异步加载进入执行。

a模块只执行exports.done = false这条语句。

defined用于定义模块,RequireJS要求每个模块均放在独立的文件之中。按照是否有依赖其他模块的情况分为独立模块和非独立模块。

回到b模块,打印b.js-1, exports, b.js-2。b模块执行完毕。

1、独立模块 不依赖其他模块。直接定义:

回到a模块,接着打印a.js-1, exports, b.js-2。a模块执行完毕

define({

回到c模块,接着执行require,需要引入b模块。由于在a模块中已经引入过了,所以直接就可以输出值了。

   methodOne: function (){},

结束。

   methodTwo: function (){}

从以上结果和分析过程可以看出,当遇到require命令时,会执行对应的模块代码。当循环引用时,有可能只输出某模块代码的一部分。当引用同一个模块时,不会再次加载,而是获取缓存。

});

ES6模块

//等价于

es6模块中的值属于【动态只读引用】。只说明一下复杂数据类型。

define(function (){

对于只读来说,即不允许修改引入变量的值,import的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

   return {

对于动态来说,原始值发生变化,import加载的值也会发生变化。不论是基本数据类型还是复杂数据类型。

       methodOne: function (){},

// b.js

       methodTwo: function (){}

export let counter = {

   };

count: 1

});

}

2、非独立模块,对其他模块有依赖。

setTimeout(() => {

define([ 'moduleOne', 'moduleTwo' ], function(mOne, mTwo){

console.log('b.js-1', counter.count)

   ...

}, 1000)

});

// a.js

//或者

import { counter } from './b.js'

define( function( require ){

counter = {}

   var mOne = require( 'moduleOne' ),

console.log('a.js-1', counter)

       mTwo = require( 'moduleTwo' );

// Syntax Error: "counter" is read-only

   ...

虽然不能将counter重新赋值一个新的对象,但是可以给对象添加属性和方法。此时不会报错。这种行为类型与关键字const的用法。

});

// a.js

如上代码, define中有依赖模块数组的 和 没有依赖模块数组用require加载 这两种定义模块,调用模块的方法合称为AMD模式,定义模块清晰,不会污染全局变量,清楚的显示依赖关系。AMD模式可以用于浏览器环境并且允许非同步加载模块,也可以按需动态加载模块。

import { counter } from './b.js'

CMD

counter.count

CMD(Common Module Definition),在CMD中,一个模块就是一个文件。

console.log(counter)

全局函数define,用来定义模块。

// 2

参数 factory 可以是一个函数,也可以为对象或者字符串。

循环加载时,ES6模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行。

当 factory 为对象、字符串时,表示模块的接口就是该对象、字符串。

// b.js

定义JSON数据模块:

import {foo} from './a.js';

define({ "foo": "bar" });

export function bar() {

factory 为函数的时候,表示模块的构造方法,执行构造方法便可以得到模块向外提供的接口。

console.log('bar');

define( function(require, exports, module) {

if (Math.random() > 0.5) {

   // 模块代码

foo();

});

}

SeaJS

}

sea.js 核心特征:

// a.js

遵循CMD规范,与NodeJS般的书写模块代码。

import {bar} from './b.js';

依赖自动加载,配置清晰简洁。

export function foo() {

seajs.use用来在页面中加载一个或者多个模块。

console.log('foo');

// 加载一个模块

bar();

seajs.use('./a');

console.log('执行完毕');

// 加载模块,加载完成时执行回调

}

seajs.use('./a',function(a){

foo();

   a.doSomething();

babel-node a.js

});

foo

// 加载多个模块执行回调

bar

seajs.use(['./a','./b'],function(a , b){

执行完毕

   a.doSomething();

// 执行结果也有可能是

   b.doSomething();

foo

});

bar

AMD和CMD最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同。

foo

很多人说requireJS是异步加载模块,SeaJS是同步加载模块,这么理解实际上是不准确的,其实加载模块都是异步的,只不过AMD依赖前置,js可以方便知道依赖模块是谁,立即加载,而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。

bar

为什么说是执行时机处理不同?

执行完毕

同样都是异步加载模块,AMD在加载模块完成后就会执行该模块,所有模块都加载执行完后会进入回调函数,执行主逻辑,这样的效果就是依赖模块的执行顺序和书写顺序不一定一致,看网络速度,哪个先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执行。

执行完毕

CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的。

由于在两个模块之间都存在引用。因此能够正常执行。

UMD

以上以上。对es6 module和commonjs module有不了解的同学可以参考一下以下的文章

统一模块定义(UMD:Universal Module Definition )就是将 AMD 和 CommonJS 合在一起的一种尝试,常见的做法是将CommonJS 语法包裹在兼容 AMD 的代码中。

ES6 module

(function(define) {

module的语法

   define(function () {

module的加载实现

       return {

来源:

           sayHello: function () {

               console.log('hello');

           }

       };

   });

}(

   typeof module === 'object' && module.exports && typeof define !== 'function' ?

   function (factory) { module.exports = factory(); } :

   define

));

该模式的核心思想在于所谓的 IIFE(Immediately Invoked Function Expression),该函数会根据环境来判断需要的参数类别。

ES6模块(module)

严格模式 

ES6 的模块自动采用严格模式,不管有没有在模块头部加上"use strict";。

严格模式主要有以下限制。

变量必须声明后再使用

函数的参数不能有同名属性,否则报错

不能使用with语句

不能对只读属性赋值,否则报错

不能使用前缀0表示八进制数,否则报错

不能删除不可删除的属性,否则报错

不能删除变量delete prop,会报错,只能删除属性delete global[prop]

eval不会在它的外层作用域引入变量

eval和arguments不能被重新赋值

arguments不会自动反映函数参数的变化

不能使用arguments.callee

不能使用arguments.caller

禁止this指向全局对象

不能使用fn.caller和fn.arguments获取函数调用的堆栈

增加了保留字(比如protected、static和interface)

模块Module

一个模块,就是一个对其他模块暴露自己的属性或者方法的文件。

导出Export

作为一个模块,它可以选择性地给其他模块暴露(提供)自己的属性和方法,供其他模块使用。

// profile.js

export var firstName = 'qiqi';

export var lastName = 'haobenben';

export var year = 1992;

//等价于

var firstName = 'qiqi';

var lastName = 'haobenben';

var year = 1992;

export {firstName, lastName, year}

1、 通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

function v1() { ... }

function v2() { ... }

export {

 v1 as streamV1,

 v2 as streamV2,

 v2 as streamLatestVersion

};

//上面代码使用as关键字,重命名了函数v1和v2的对外接口。重命名后,v2可以用不同的名字输出两次。

2、 需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

// 报错

export 1;

// 报错

var m = 1;

export m;

//上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出1,第二种写法通过变量m,还是直接输出1。1只是一个值,不是接口。

/ 写法一

export var m = 1;

// 写法二

var m = 1;

export {m};

// 写法三

var n = 1;

export {n as m};

//上面三种写法都是正确的,规定了对外的接口m。其他脚本可以通过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。

3、最后,export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,接下来说的import命令也是如此。

function foo() {

 export default 'bar' // SyntaxError

}

foo()

导入import

作为一个模块,可以根据需要,引入其他模块的提供的属性或者方法,供自己模块使用。

1、 import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

import { lastName as surename } from './profile';

2、import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js路径可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。

3、注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。

foo();

import { foo } from 'my_module';

//上面的代码不会报错,因为import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。

4、由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

/ 报错

import { 'f' 'oo' } from 'my_module';

// 报错

let module = 'my_module';

import { foo } from module;

// 报错

if (x === 1) {

 import { foo } from 'module1';

} else {

 import { foo } from 'module2';

}

5、最后,import语句会执行所加载的模块,因此可以有下面的写法。

import 'lodash';

//上面代码仅仅执行lodash模块,但是不输入任何值。

默认导出(export default)

每个模块支持我们导出 一个没有名字的变量,使用关键语句export default来实现。

export default function(){

           console.log("I am default Fn");

       }

//使用export default关键字对外导出一个匿名函数,导入这个模块的时候,可以为这个匿名函数取任意的名字

//取任意名字均可

import sayDefault from "./module-B.js";

sayDefault();

//结果:I am default Fn

1、默认输出和正常输出的比较

// 第一组

export default function diff() { // 输出

 // ...

}

import diff from 'diff'; // 输入

// 第二组

export function diff() { // 输出

 // ...

};

import {diff} from 'diff'; // 输入

//上面代码的两组写法,第一组是使用export default时,对应的import语句不需要使用大括号;第二组是不使用export default时,对应的import语句需要使用大括号。

export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能对应一个方法。

2、因为export default本质是将该命令后面的值,赋给default变量以后再默认,所以直接将一个值写在export default之后。

/ 正确

export default 42;

// 报错

export 42;

//上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定外对接口为default。

3、如果想在一条import语句中,同时输入默认方法和其他变量,可以写成下面这样。

import _, { each } from 'lodash';

//对应上面代码的export语句如下

export default function (){

   //...

}

export function each (obj, iterator, context){

   //...

}

export 与 import 的复合写法

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。

export { foo, bar } from 'my_module';

// 等同于

import { foo, bar } from 'my_module';

export { foo, bar };

/ 接口改名

export { foo as myFoo } from 'my_module';

// 整体输出

export * from 'my_module';

注意事项

声明的变量,对外都是只读的。但是导出的是对象类型的值,就可修改。

导入不存在的变量,值为undefined。

ES6 中的循环引用

ES6 中,imports 是 exprts 的只读视图,直白一点就是,imports 都指向 exports 原本的数据,比如:

//------ lib.js ------

export let counter = 3;

export function incCounter() {

   counter ;

}

//------ main.js ------

import { counter, incCounter } from './lib';

// The imported value `counter` is live

console.log(counter); // 3

incCounter();

console.log(counter); // 4

// The imported value can’t be changed

counter ; // TypeError

因此在 ES6 中处理循环引用特别简单,看下面这段代码:

//------ a.js ------

import {bar} from 'b'; // (1)

export function foo() {

 bar(); // (2)

}

//------ b.js ------

import {foo} from 'a'; // (3)

export function bar() {

 if (Math.random()) {

   foo(); // (4)

 }

}

假设先加载模块 a,在模块 a 加载完成之后,bar 间接性地指向的是模块 b 中的 bar。无论是加载完成的 imports 还是未完成的 imports,imports 和 exports 之间都有一个间接的联系,所以总是可以正常工作。

实例

//---module-B.js文件---

//导出变量:name

export var name = "cfangxu";

moduleA模块代码:

//导入 模块B的属性 name    

import { name } from "./module-B.js";   

console.log(name)

//打印结果:cfangxu

批量导出:

//属性name

var name = "cfangxu";

//属性age

var age  = 26;

//方法 say

var say = function(){

           console.log("say hello");

        }

//批量导出

export {name,age,say}

批量导入:

//导入 模块B的属性

import { name,age,say } from "./module-B.js";

console.log(name)

//打印结果:cfangxu

console.log(age)

//打印结果:26

say()

//打印结果:say hello

重命名导入变量:

import {name as myName} from './module-B.js';

console.log(myName) //cfangxu

整体导入:

/使用*实现整体导入

import * as obj from "./module-B.js";

console.log(obj.name)

//结果:"cfangxu"

console.log(obj.age)

//结果:26

obj.say();

//结果:say hello

本文由pc28.am发布于前端技术,转载请注明出处:模块讲解,的一次更新说明

上一篇:没有了 下一篇:没有了
猜你喜欢
热门排行
精彩图文
  • 遇见未知的,web开采连忙入门
    遇见未知的,web开采连忙入门
    CSS 框架 Bulma 教程 2017/10/26 · CSS ·Bulma 原文出处:阮一峰    网页样式需要大量时间开发,最省事的方法就是使用 CSS 框架。 Bootstrap 是最著名的 CSS框架,
  • 追踪客户,读书笔记
    追踪客户,读书笔记
    使用 CSS 追踪用户 2018/01/20 · CSS · 1评论 ·追踪 原文出处:jbtronics   译文出处:枫上雾棋    除了使用 JS 追踪用户,现在有人提出了还可以使用 CSS 进行
  • pusle雷达动漫完结,推荐8款CSS3兑现的动态特效
    pusle雷达动漫完结,推荐8款CSS3兑现的动态特效
    CSS技巧:逐帧动漫抖动实施方案 2017/08/16 · CSS ·动画 原来的书文出处:坑坑洼洼实验室    我所在的前端共青团和少先队首要从事活动端的H5页面开荒,而
  • 跟随我在oracle学习php,HTML中form表单的用法
    跟随我在oracle学习php,HTML中form表单的用法
    表单元素之搭车系 2016/01/28 · HTML5 ·表单 原文出处:司徒正美(@司徒正美)    对于表单元素, 除了reset元素,只要有name与value都能提交 因为在我们印象
  • Codecademy为编程初学者新增HTML和CSS两门课程,可以
    Codecademy为编程初学者新增HTML和CSS两门课程,可以
    Codecademy为编制程序初读书人新添HTML和CSS两门学科 2012/04/03 · CSS · 来源:伯乐在线     ·CSS 葡萄牙语原来的文章:Mashable  编译:伯乐在线– 黄利民 乐