深入浅出Koa(1):生成器和Thunk函数
Koa是个小而美的Node.js web框架,它由Express的原班人马打造的, 致力于以一种现代化开发的方式构建web应用。 通过这个系列,你将能够理解Koa的基本原理,并且学习如何正确有效的使用Koa编写Web应用程序。本文主要向你介绍 Koa的一些基础知识,包括生成器,thunks等。
为什么要用Koa?
Koa提供的一些关键特性能够让你尽可能避免回调函数的情况下简单快捷的编写web应用。Koa使用了ES6中的一些新的语言 特性来让Node应用的控制流管理变得更加简单。
Koa本身非常的小。与当下其他流行的web框架(例如Express)不同的是,Koa从一出生开始就采用了一种极致模块化的方案, 意味着每个模块做且只做一件事情。请牢记这一点,让我们开始吧。
Hello Koa
var koa = require('koa');
var app = koa();
app.use(function *() {
this.body = 'Hello World';
});
app.listen(3000);
在开始之前,为了能够用node运行本文中的例子或你自己写的ES6代码 (如果你使用的是node v4以下版本,则需要加上--harmony,建议你升级到Node的最新版本)。
正如你从上面的例子中看到的那样,使用Koa来编写一个web服务器并没有什么特别的地方,除了那个奇怪的跟在function 关键字后的*符号。好吧,如果你了解ES6的话,我想你应该知道这个符号意味着该函数现在是生成器函数了。
生成器 Genderators
想象这样的一个场景,当你执行一个函数的时候,你可以在某个点暂停函数的执行,做一些其它工作,计算一些其他值, 然后返回到函数,甚至是携带一些新的值继续函数的执行。
可以认为生成器就是某种迭代器(类似于循环)。当然,这也是生成器函数最擅长做的事,生成器是ES6的新特性。因此我们可以自由的使用它。
我们来试着使用一下生成器!首先,你得创建一个生成器函数,这与我们创建普通函数没有太大的差别,唯一的区别在于, 在function关键字后面需要添加*符号。例如:
function *foo() { }
如上,现在我们就创建了一个生成器函数。当我们调用这个函数时,它会返回一个迭代器对象。因此与一般函数不同的是, 当我们调用一个生成器时,代码并不会立即执行,正如我们前面所讨论的,我们需要手动的迭代这个迭代器对象。
// 生成器函数
function *foo(arg) {
}
var bar = foo(123); // bar为迭代器对象
使用这个返回对象bar,我们可以用来遍历函数中的中断点。可以通过调用迭代器对象bar的next()方法来完成遍历操作。 当next()被调用的时候,函数会从之前暂停的地方执行到下一个暂停点。
但是在继续执行之前,迭代器会返回一个对象,这个对象包含了生成器的的状态信息(即每次调用next()方法时会返回一个对象)。 这个对象包含两个属性,分别是value和done。value表示当前迭代值,done则表示的是迭代是否结束。
function *foo(args) {
return args;
}
var bar = foo(123);
bar.next(); // {value: 123, done: true}
正如我们所看到的那样,这个生成器函数中并没有任何的中断点(没有yield关键字), 因此首次调用next()方法时返回对象的done值为true。这意味着,如果你在生成器函数中指明了return值, 它将会被作为最后一个迭代器对象被返回(也就是done为true)。现在,我们唯一需要所做的是能够暂停迭代器。 正如前面说的那样,在迭代遍历生成器函数时,每次迭代会产出一个值(在暂停点)。 因此,ES6使用yield关键字用于暂停函数。
yield
yield [[expression]]
我们知道,我们可以通过next()方法来启动一个生成器,每当遇到一个yield关键字时,它会暂停执行。 然后返回一个包括value和done属性的对象。此时value值即为yield后的表达式结果。 当然,这个表达式可以是任何表达式。例如:
function *foo() {
var index = 0;
while (index < 2) {
yield index++;
}
}
var bar = foo();
console.log(bar.next()); // { value: 0, done: false }
console.log(bar.next()); // { value: 1, done: false }
console.log(bar.next()); // { value: undefined, done: true }
再次调用next()方法,当前的yield值会被返回,然后继续执行。当然,如果你要在next()方法中接收一个值, 例如next(val)这样,也是可以的,接收的值会在生成器继续执行时被返回。
function *foo() {
var val = yield 'A';
console.log(val); // 'B'
}
var bar = foo();
console.log(bar.next()); // {value: 'A', done: false}
console.log(bar.next('B'); // {value: undefined, done: true}
再次解释一下:第一次调用bar.next()方法时,生成器对象执行到yield 'A',然后返回{value: 'A', done: false}, value的值即为yield后表达式的值,done表示是否结束。再次调用bar.next('B'),此时已经没有了yield语句, 或者return语句,因此返回对象的value值为undefined。但是因为next()方法接收了一个值B, 因此该值会赋值给val。
错误处理
如果你发现迭代器对象的值发生了错误的话,你可以使用throw()方法在生成器中捕获错误。 使用这种方法,你可以轻松的在生成器完成错误处理的任务。
function *foo() {
try {
x = yield 'asd B'; // Error will be thrown
} catch(err) {
throw err;
}
}
var bar = foo();
if (bar.next().value == 'B') {
bar.throw(new Error("it's B!"));
}
for…of
在ES6中有一个新的循环类型,可以被用来迭代生成器对象,它就是for...of循环。 使用for...of循环时,迭代会一直执行到done为false时。值得注意的是,如果你使用了这种循环类型, 那么你将不能在next()方法中传值,并且循环会舍弃返回值。
function *foo() {
yield 1;
yield 2;
yield 3;
}
for (v of foo()) {
console.log(v);
}
yield *
前面说过,你可以yield几乎任何东西,甚至是生成器。如果你需要yield一个生成器的话,那么需要使用yield *。 这被称为生成器委托(delegation)。这意味着你正在委托给另一个生成器,因此, 你可以使用一个迭代器对象迭代遍历多重嵌套的生成器。
function *bar () {
yield 'b';
}
function *foo () {
yield 'a';
yield *bar(); // bar()返回一个生成器(迭代器)
yield 'c';
}
for (v of foo()) {
console.log(v);
}
Thunks
如果要彻底理解Koa,Thunk是另一个需要搞懂的重要概念。Thunk函数是一个偏函数,执行它会得到一个新的只带一个回调参数的函数。 某种程度上,我们可以将它与lazy evaluation联系在一起。 我们来看一下例子:
var read = function(file) {
return function(cb) {
require('fs').readFile(file, cb);
}
}
read('package.json')(function (err, str){});
上面的示例代码中,read函数时一个典型的Thunk函数,执行read('package.json')后我们可以获得一个只有回调参数的新函数。
Thunkify
我们可以利用一个叫thunkify小模块, 将普通的node函数转换为thunk函数。你可能会问,我们为什么需要这么做?因为我们在使用生成器函数, 事实证明,它可以让你轻松的在生成器函数中避免使用回调函数。
为了能够在生成器函数中试用thunk函数,我们首先需要做的是将原先的含回调的普通node函数转为thunk函数。 如果不这样做,我们就必须使用回调来进行处理。当我们调用next()方法时,此刻返回的value值是一个函数, 该函数的参数是一个被thunk化的函数的回调。在回调函数中,我们可以检查错误(使用throw关键字), 或者调用next()方法获取接收到的数据。
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile); // 1
// 2
function *bar() {
try {
var x = yield read('input.txt');
} catch(err) {
throw err;
}
console.log(x);
}
var gen = bar(); // 3
gen.next().value(function (err, data) { // 4
if (err) gen.throw(err);
gen.next(data.toString());
});
解释一下上面的执行流程:
- 我们利用thunkify模块将原生方法fs.readFile转为thunk化后的read()函数。 注意!read()函数是个thunk函数。
- 定义一个生成器函数,yield后的是一个thunk化的read()函数。
- 调用这个生成器函数,将返回的迭代器对象赋值给gen。
- 调用next()方法,返回的value值是一个函数,该函数接收一个callback,这个callback其实就是被thunk化的 read()函数的callback。我们可以利用这个callback来执行处理。
你需要花点时间来理解上面代码中的每一个部分,因为这对于理解Koa非常的关键。希望你能更多的关注到代码中的生成器部分。 它拥有同步代码的简洁性,使用了合理的错误处理,但是需要注意的是,它仍然是异步代码。
本文是Koa系列的第一篇,后面还会继续介绍Koa。
Statement
原文地址: https://blog.risingstack.com/introduction-to-koa-generators/
References
- https://blog.risingstack.com/introduction-to-koa-generators/
- http://purplebamboo.github.io/2014/05/24/koa-source-analytics-1/
深入浅出Koa(2):co的实现和使用
在之前的文章中, 我们介绍了Koa的基础知识,主要涉及了ES6生成器的相关知识,借助于生成器,我们可以使用同步风格的代码编写异步代码, 这么做可以让异步编程变得更加简单,也让代码更可读,本文我们将要介绍的是co,它是Koa的实现基础。
简单回顾
// First part
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
// Second part
function *bar () {
try {
var x = yield read('input.txt');
} catch (err) {
console.log(err);
}
console.log(x);
}
// Third part
var gen = bar();
gen.next().value(function (err, data) {
if (err) {
gen.throw(err);
}
gen.next(data.toString());
});
这是前一篇文章中的最后一个例子,正如你看到的那样,我们可以将程序分为三个重要部分。 首先,我们需要创建了一个被thunk化的函数,我们将在生成器中使用到这个thunk函数。 然后,我们可以编写我们的生成器函数,并在函数中使用前面创建的thunk函数。 最后,我们调用并遍历生成器,并进行错误处理等工作。如果你仔细的考虑了这个过程, 你会发现最后一个部分和整个程序的核心内容并没有太大的关联。它只不过是用来让我们运行生成器而已。 因此,我们可以利用一个工具来简化这个过程,幸运的是,正好有一个这样的模块,他就是co。
co
Co是一个基于生成器的Node流程控制模块。下面的例子所做的工作与前一个例子完全一样, 但是我们没有直接编写调用生成器的代码(上面代码的第三部分)。取而代之的是,我们将生成器直接传递给了co函数, 然后立即调用这个函数,任务就这么神奇的完成了。其实,也没什么神奇的,co只不过是帮你完成了生成器调用代码而已, 因此我们没必要担心下层的工作。
var co = require('co');
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
co(function *bar() {
try {
var x = yield read('input.txt','utf-8');
} catch (err) {
console.log(err);
}
console.log(x);
}).catch(onerror);
function onerror(err) {
console.error(err.stack);
}
正如你已经知道的那样,你可以在yield后面跟上任何你需要计算(用于获取某些值、执行某些任务)的表达式。因此, 不仅仅是thunk函数可以跟在yield后面。由于co主要用来创建更简单的控制流,因此它只能用来yield一些特定类型。 目前主要可以在co中被yield的类型有:
- thunks(函数)
- 数组(并行执行)
- 对象(并行执行)
- 生成器(代理)
- 生成器函数(代理)
- Promises
前面我们已经讨论了thunks是如何工作的,因此下面我们来讨论一些其他内容。
co的简单实现
co的异步解决方案主要建立在Thunk函数的基础上。使用co时,yield后的表达式经常是Thunk函数。 前面的文章中,我们已经介绍过,你可以借助诸如thunkify这样的模块来包裹原来的函数。 有了co后,我们可以使用这样的方式来进行异步编程:
let co = require('co'); // 这里使用的是4.0之前的版本,4.0后开始使用promise风格
let thunkify = require('thunkify');
let fs = require('fs');
let size = thunkify(fs.stat);
co(function *() {
let a = yield size('file1.txt');
let b = yield size('file2.txt');
console.log(a);
console.log(b);
return [a, b];
})(function (err, values){
if (err) throw err;
console.log("----------");
console.log(values);
});
程序的输出结果大概是这样的:
12
33
----------
[ 12, 33 ]
在上面的代码中,我们使用yield来直接获取异步函数size()的值,但代码简直和同步代码没什么两样。 这样做的好处是,让我们不再关心回调函数可能引发的回调地狱问题。实际上,co本质上也可以看成是一个Thunk函数, 它接收了一个生成器函数作为参数,并生成了一个实际操作函数,而这个函数正是通过接收回调的方式来传入最后的返回值。
下面我们来看下如何实现一个最简单的co:
let co = function (callback) {
return function (done) {
let ctx = this;
let gen = callback.call(ctx);
let it = null;
function _next(err, res) {
it = gen.next(res);
if (it.done) {
done.call(ctx, err, it.value);
} esle {
it.value(_next);
}
}
_next();
}
}
我们说过,co本质上也是一个Thunk函数,它需要传入一个生成器函数,它能够帮你不停的调用所传入生成器的next函数, 如果done微true,则代表生成器函数已经迭代完成,并将值传给回调函数。这里需要注意_next函数的实现, 它实际上会成为前面的yield的函数的回调函数。
比如之前的代码中的size('file1.txt')会返回一个带回调的函数a。这里有个约定, 就是Thunk函数的回调一般都是function (err,res) { }的格式,实际上这也是node实际的规范。
上面我们实现了一个简单的co函数,已经可以支持最基本的同步调用了,但很多时候,我们还需要在yield后面处理其他的值, 比如一个数组或者对象,我推荐你阅读这篇博文。 当然,最重要的还是阅读co的源码。
co v4使用指南
下面看完了co的源码,我们来看看如何使用co,它的最新版本是v4,这个版本主要依赖于promise。
并行执行
co(function *() {
// 3 concurrent reads
var reads = yield [
read('input.txt', 'utf-8'),
read('input.txt', 'utf-8'),
read('input.txt', 'utf-8')
];
console.log(reads);
// 2 concurrent reads
reads = yield {a: read('input.txt', 'utf-8'), b: read('input.txt', 'utf-8')};
console.log(reads);
}).catch(onerror);
如果你yield了一个数组或对象,它会并行的计算数组或对象中的内容。当然,如果集合中的内容为thunks或生成器, 它也能同样进行处理。并且你也可以进行嵌套,它能够便利你的数组或对象,然后并行的执行工作函数。需要记住的是: yield后的生成的结果并不是扁平的,而是保持和原来相同的结构。
co(function *() {
var a = [read('cinq.txt', 'utf-8'), read('deux.txt', 'utf-8')];
var b = [read('six.txt', 'utf-8'), read('un.txt', 'utf-8')];
var files = yield [a, b];
console.log(files); // [ [ 'cinq', 'deux' ], [ 'six', 'un' ] ]
}).catch(onerror);
你也可以在thunk调用后进行yield,同样能达到并行的目的。
co(function *() {
var a = read('input.txt', 'utf-8');
var b = read('input.txt', 'utf-8');
// 2 concurrent reads
console.log([yield a, yield b]);
// or
// 2 concurrent reads
console.log(yield [a, b]);
}).catch(onerror);
委托
前面说过,你也可以在yield后面跟上生成器。注意你可以不必使用yield *;
var stat = thunkify(fs.stat);
function *size(file) {
var s = yield stat(file);
return s.size;
}
co(function *() {
var f = yield size('input.txt'); // hello world
console.log(f); // 11
}).catch(onerror);
我们大致尝试用co处理了每一种可以被yield的类型。下面我们来看最后一个例子,算作总结。
var co = require('co');
var fs = require('fs');
function size(file) {
return function (fn) {
fs.stat(file, function (err, stat) {
if (err) return fn(err);
fn(null, stat.size);
});
}
}
function *foo() {
var a = yield size('un.txt');
var b = yield size('deux.txt');
var c = yield size('trois.txt');
return [a, b, c];
}
function *bar() {
var a = yield size('quatre.txt');
var b = yield size('cinq.txt');
var c = yield size('six.txt');
return [a, b, c];
}
co(function *() {
var results = yield [foo(), bar()];
console.log(results);
}).catch(onerror);
function onerror(err) {
console.error(err.stack);
}
我想目前为止你应该能够对生成器有了一个较为清晰的认识了,并且能够借用co很好的处理异步控制流。关于co本身, 如果你想了解的更深入些,可以阅读它的源代码。现在,我们将重点移到Koa本身上来。
小结
本文为你介绍了Koa的实现基础co,它能够让你的node应用避免回调地狱。后面我们会继续介绍Koa的使用与实现。
Statement
原文地址:https://blog.risingstack.com/getting-started-with-koa-part-2/
References
深入浅出Koa(3):Koa的入门和实现
Nov 25, 2015
前面我们了解Koa的两大基础:生成器和co,Koa是基于这两者之上的新一代的Node中间件框架。 为了能够让你更透彻的理解Koa,本文将要介绍的是Koa的实现。
Koa入门
对于Koa而言,你需要知道的并没有很多。如果你阅读了它的源代码的话,你甚至会发现总共才4个文件,每个才300行左右。 Koa遵循了每个程序只做一件事并将其做的更好的原则。你会发现,每个优秀的Koa模块都非常的紧凑,并且只做一件事, 并且在其他模块的基础上进行构建。你应该记住这一点,并使用这个方法来开发你自己的Koa模块。这将会有益于所有人, 也有助于你和其他人阅读源代码。先记住这一点后,然后我们来看看Koa的一些核心特性。
应用 Application
var koa = require('koa');
var app = koa();
创建一个Koa应用只不过是在调用一个相关的模块函数而已。它会提供给你一个对象,这个对象包含一个生成器数组 (一组中间件),对收到的每个请求,它会使用一种堆栈式的方法执行。
级联 Cascading
当你使用Koa时,一个非常重要的术语是中间件。现在让我们来搞清楚这个概念。
Koa中的中间件是一组用于处理请求的函数。使用Koa创建的服务器,可以有一组堆栈结构的中间件与它关联。
级联在Koa中意味着:控制流会流经一组中间件。在web开发中这个概念非常的有用,你可以将复杂的行为借助于这个手段变得简单。 Koa通过生成器函数来实现这一点(中间件级联),并且更具创新性和简洁性。 它能够yiled下游中间件,之后控制流再返回到上游中间件。 将生成器加入到控制流中非常简单,只要在调用use()方法时使用生成器即可。 猜猜下面的代码为什么每次接收到请求时会输出的是A, B, C, D, E。
下面的代码演示了一个服务器,因此listen方法用于监听一个具体的端口:
var koa = require('koa');
var app = koa();
// 1
app.use(function *(next) {
console.log('A');
yield next;
console.log('E');
});
// 2
app.use(function *(next) {
console.log('B');
yield next;
console.log('D');
});
// 3
app.use(function *(next) {
console.log('C');
});
app.listen(3000);
当一个新的请求进来的时候,它会流经一系列的中间件(即按照use方法调用的顺序1-2-3)。因此在上面的示例代码中, 请求首先经过第一个中间件,它会首先输出A,然后它遇到了yield next语句,这会使它转入下一个中间件计算相应的结果, 只有在处理完后才回到离开的地方。因此,接下来会转入到下一个中间件打印B,再次碰到yield next, 转入下一个中间件,打印C。现在已经没有更多的中间件了,因此执行完第三个中间件就会回流,首先回到第二个中间件, 继续执行打印出D,中间件2的代码执行完毕,再回流到第一个中间件,打印E。
到目前为止,Koa模块本身并没有什么复杂的地方,因此没有必要再追溯那些你可以在文档中就能知道的信息。 我推荐你直接去阅读Koa的文档去了解关于Koa的详细使用说明,其实也没有什么复杂的内容,所以这里也就不再多介绍了。 这里列出相关文档的链接:
让我们再来看一个例子(这个例子来源于Koa的官网),它利用了一些HTTP特性。第一个中间件计算了响应的时间。 你会发现获取响应开始和结束的时间非常简单。并且在Koa中你可以优雅的将这些功能模块分离开。
app.use(function *(next) {
var start = new Date;
yield next;
var ms = new Date - start;
this.set('X-Response-Time', ms + 'ms');
});
app.use(function *(next) {
var start = new Date;
yield next;
var ms = new Date - start;
console.log('%s %s - %s', this.method, this.url, ms);
});
app.use(function *() {
this.body = 'Hello World';
});
app.listen(3000);
Koa的实现
下面我们来看看koa的源代码,为了简单起见,我们解读的是v1.x的源码。 本部分的内容主要参考了阮一峰的koa介绍的博文。 koa项目的入口文件时lib/application.js,代码的框架大致如下:
function Application() {
if (!(this instanceof Application)) return new Application;
this.env = process.env.NODE_ENV || 'development';
this.subdomainOffset = 2;
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
var app = Application.prototype;
exports = module.exports = Application;
app.use()用于注册中间件,即将生成器函数放入中间件数组:
app.use = function(fn){
if (!this.experimental) {
// es7 async functions are allowed
assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
};
app.listen()就是将http.createServer(app.callback()).listen(...)封装了一层:
app.listen = function(){
debug('listen');
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
app.callback = function(){
// 将respond函数放入this.middleware
var mw = [respond].concat(this.middleware);
var fn = this.experimental
? compose_es7(mw)
: co.wrap(compose(mw)); // 将中间件数组转为一个层层调用的生成器函数
var self = this;
if (!this.listeners('error').length) this.on('error', this.onerror);
return function(req, res){
res.statusCode = 404;
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).catch(ctx.onerror);
}
};
在上面的代码中,app.callback()会返回一个函数,用来处理HTTP请求。它的第一行代码用于将respond函数放入this.middleware, 现在mw数组就包括了[respond, s1, s2, s3]。
compose(mw)用于将中间件数组转为一个层层调用的生成器函数。我们来看它的源码:
function compose(middleware){
return function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
yield *next;
}
}
function *noop(){}
在上面的代码中,下一个生成器函数总是上一个生成器函数的参数,从而保证了层层调用。 var fn = co.wrap(gen)则是将生成器函数包装成自动执行的函数,并且返回一个Promise。
co.wrap = function (fn) {
return function () {
return co.call(this, fn.apply(this, arguments));
}
}
将所有的上下文变量都放进context对象中:
app.createContext = function(req, res){
var context = Object.create(this.context);
var request = context.request = Object.create(this.request);
var response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.onerror = context.onerror.bind(context);
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, this.keys);
context.accept = request.accept = accepts(req);
context.state = {};
return context;
};
而真正处理HTTP请求的是下面这个生成器函数:
function *respond(next) {
yield *next;
// allow bypassing koa
if (false === this.respond) return;
var res = this.res;
if (res.headersSent || !this.writable) return;
var body = this.body;
var code = this.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
this.body = null;
return res.end();
}
if ('HEAD' == this.method) {
if (isJSON(body)) this.length = Buffer.byteLength(JSON.stringify(body));
return res.end();
}
// status body
if (null == body) {
this.type = 'text';
body = this.message || String(code);
this.length = Buffer.byteLength(body);
return res.end(body);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
this.length = Buffer.byteLength(body);
res.end(body);
}
总结
现在你已经熟悉了Koa的核心内容, 虽然使用旧的框架也能够完成相关的任务,但是现在你可以尝试Koa这个新的框架来解决以前的问题。因为, 在旧的框架中可能有非常多的功能是你从来都用不到的,或者某些并不是按照你的设想工作的。 现在,以Koa为代表的现代Node框架能够为你带来这些改变。你可以使用更轻量级的核心, 然后通过npm引入你需要的模块到你的app中,这种方式下你能够完全的控制哪些模块是你需要用的。
模块
阅读: 4197
在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。
为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在Node环境中,一个.js文件就称之为一个模块(module)。
使用模块有什么好处?
最大的好处是大大提高了代码的可维护性。其次,编写代码不必从零开始。当一个模块编写完毕,就可以被其他地方引用。我们在编写程序的时候,也经常引用其他模块,包括Node内置的模块和来自第三方的模块。
使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。
在上一节,我们编写了一个hello.js
文件,这个hello.js
文件就是一个模块,模块的名字就是文件名(去掉.js
后缀),所以hello.js
文件就是名为hello
的模块。
我们把hello.js
改造一下,创建一个函数,这样我们就可以在其他地方调用这个函数:
'use strict';
vars =
'Hello';
functiongreet(name) {
console.log(s +
', '+ name +
'!');
}
module.exports = greet;
函数greet()
是我们在hello
模块中定义的,你可能注意到最后一行是一个奇怪的赋值语句,它的意思是,把函数greet
作为模块的输出暴露出去,这样其他模块就可以使用greet
函数了。
问题是其他模块怎么使用hello
模块的这个greet
函数呢?我们再编写一个main.js
文件,调用hello
模块的greet
函数:
'use strict';
// 引入hello模块:
vargreet =
require(
'./hello');
vars =
'Michael';
greet(s);
// Hello, Michael!
注意到引入hello
模块用Node提供的require
函数:
vargreet =
require(
'./hello');
引入的模块作为变量保存在greet
变量中,那greet
变量到底是什么东西?其实变量greet
就是在hello.js
中我们用module.exports = greet;
输出的greet
函数。所以,main.js
就成功地引用了hello.js
模块中定义的greet()
函数,接下来就可以直接使用它了。
在使用require()
引入模块的时候,请注意模块的相对路径。因为main.js
和hello.js
位于同一个目录,所以我们用了当前目录.
:
vargreet =
require(
'./hello');
// 不要忘了写相对目录!
如果只写模块名:
vargreet =
require(
'hello');
则Node会依次在内置模块、全局模块和当前模块下查找hello.js
,你很可能会得到一个错误:
module.js
throw err;
^
Error: Cannot find module 'hello'
at Function.Module._resolveFilename
at Function.Module._
load
...
at Function.Module._load
at Function.Module.runMain
遇到这个错误,你要检查:
·模块名是否写对了;
·模块文件是否存在;
·相对路径是否写对了。
CommonJS规范
这种模块加载机制被称为CommonJS规范。在这个规范下,每个.js
文件都是一个模块,它们内部各自使用的变量名和函数名都互不冲突,例如,hello.js
和main.js
都申明了全局变量var s = 'xxx'
,但互不影响。
一个模块想要对外暴露变量(函数也是变量),可以用module.exports = variable;
,一个模块要引用其他模块暴露的变量,用var ref = require('module_name');
就拿到了引用模块的变量。
结论
要在模块中对外输出变量,用:
module.exports = variable;
输出的变量可以是任意对象、函数、数组等等。
要引入其他模块输出的对象,用:
varfoo =
require(
'other_module');
引入的对象具体是什么,取决于引入模块输出的对象。
深入了解模块原理
如果你想详细地了解CommonJS的模块实现原理,请继续往下阅读。如果不想了解,请直接跳到最后做练习。
当我们编写JavaScript代码时,我们可以申明全局变量:
vars =
'global';
在浏览器中,大量使用全局变量可不好。如果你在a.js
中使用了全局变量s
,那么,在b.js
中也使用全局变量s
,将造成冲突,b.js
中对s
赋值会改变a.js
的运行逻辑。
也就是说,JavaScript语言本身并没有一种模块机制来保证不同模块可以使用相同的变量名。
那Node.js是如何实现这一点的?
其实要实现“模块”这个功能,并不需要语法层面的支持。Node.js也并不会增加任何JavaScript语法。实现“模块”功能的奥妙就在于JavaScript是一种函数式编程语言,它支持闭包。如果我们把一段JavaScript代码用一个函数包装起来,这段代码的所有“全局”变量就变成了函数内部的局部变量。
请注意我们编写的hello.js
代码是这样的:
vars =
'Hello';
varname =
'world';
console.log(s +
' '+ name +
'!');
Node.js加载了hello.js
后,它可以把代码包装一下,变成这样执行:
(
function() {
// 读取的hello.js代码:
var
s =
'Hello';
var
name =
'world';
console.log(s +
' '+ name +
'!');
// hello.js代码结束
})();
这样一来,原来的全局变量s
现在变成了匿名函数内部的局部变量。如果Node.js继续加载其他模块,这些模块中定义的“全局”变量s
也互不干扰。
所以,Node利用JavaScript的函数式编程的特性,轻而易举地实现了模块的隔离。
但是,模块的输出module.exports
怎么实现?
这个也很容易实现,Node可以先准备一个对象module
:
// 准备module对象:
var module = {
id:
'hello',
exports: {}
};
varload =
function(module) {
// 读取的hello.js代码:
functiongreet(name) {
console.log(
'Hello, '+ name +
'!');
}
module.exports = greet;
// hello.js代码结束
return
module.exports;
};
var exported = load(module);
// 保存module:
save(module, exported);
可见,变量module
是Node在加载js文件前准备的一个变量,并将其传入加载函数,我们在hello.js
中可以直接使用变量module
原因就在于它实际上是函数的一个参数:
module.exports = greet;
通过把参数module
传递给load()
函数,hello.js
就顺利地把一个变量传递给了Node执行环境,Node会把module
变量保存到某个地方。
由于Node保存了所有导入的module
,当我们用require()
获取module时,Node找到对应的module
,把这个module
的exports
变量返回,这样,另一个模块就顺利拿到了模块的输出:
vargreet =
require(
'./hello');
以上是Node实现JavaScript模块的一个简单的原理介绍。
module.exports vs exports
很多时候,你会看到,在Node环境中,有两种方法可以在一个模块中输出变量:
方法一:对module.exports赋值:
// hello.js
functionhello() {
console.log(
'Hello, world!');
}
functiongreet(name) {
console.log(
'Hello, '+ name +
'!');
}
functionhello() {
console.log(
'Hello, world!');
}
module.exports = {
hello: hello,
greet: greet
};
方法二:直接使用exports:
// hello.js
functionhello() {
console.log(
'Hello, world!');
}
functiongreet(name) {
console.log(
'Hello, '+ name +
'!');
}
functionhello() {
console.log(
'Hello, world!');
}
exports.hello = hello;
exports.greet = greet;
但是你不可以直接对exports
赋值:
// 代码可以执行,但是模块并没有输出任何变量:
exports = {
hello: hello,
greet: greet
};
如果你对上面的写法感到十分困惑,不要着急,我们来分析Node的加载机制:
首先,Node会把整个待加载的hello.js
文件放入一个包装函数load
中执行。在执行这个load()
函数前,Node准备好了module变量:
var
module = {
id:
'hello'
,
exports:
{}
};
load()
函数最终返回module.exports
:
var
load = function (exports, module) {
// hello.js的文件内容
...
// load函数返回:
return module.exports;
};
var exported =
load(module.exports, module);
也就是说,默认情况下,Node准备的exports
变量和module.exports
变量实际上是同一个变量,并且初始化为空对象{}
,于是,我们可以写:
exports.foo =
function() {return
'foo'
; };
exports.bar =
function() {return
'bar'
; };
也可以写:
module.exports.foo =
function() {return
'foo'
; };
module.exports.bar =
function() {return
'bar'
; };
换句话说,Node默认给你准备了一个空对象{}
,这样你可以直接往里面加东西。
但是,如果我们要输出的是一个函数或数组,那么,只能给module.exports
赋值:
module.exports =
function() {return
'foo'
; };
给exports
赋值是无效的,因为赋值后,module.exports
仍然是空对象{}
。
结论
如果要输出一个键值对象{}
,可以利用exports
这个已存在的空对象{}
,并继续在上面添加新的键值;
如果要输出一个函数或数组,必须直接对module.exports
对象赋值。
所以我们可以得出结论:直接对module.exports
赋值,可以应对任何情况:
module.exports = {
foo:
function() {return
'foo'
; }
};
或者:
module.exports =
function() {return
'foo'
; };
最终,我们强烈建议使用module.exports = xxx
的方式来输出模块变量,这样,你只需要记忆一种方法。
练习
编写hello.js
,输出一个或多个函数;
编写main.js
,引入hello
模块,调用其函数。
参考源码