九、错误处理和运行应用
好的 web 应用必须有信息丰富的错误消息来通知客户端请求失败的确切原因。错误可能是由客户端(例如,错误的输入数据)或服务器(例如,代码中的错误)引起的。
客户端可能是一个浏览器,在这种情况下,应用应该显示一个 HTML 页面。例如,当找不到请求的资源时,应该显示 404 页面。或者客户端可能是另一个通过 REST API 消耗我们资源的应用。在这种情况下,应用应该以 JSON 格式(或者 XML 或其他支持的格式)发送适当的 HTTP 状态代码和消息。由于这些原因,在开发重要的应用时,定制错误处理代码总是最佳实践。
在典型的 Express.js 应用中,错误处理程序遵循以下路线。错误处理值得在这本书里有自己的章节,因为它不同于其他中间件。在错误处理程序之后,我们将介绍 Express.js 应用方法和启动 Express.js 应用的方式。因此,本章的主要议题如下:
- 错误处理
- 运行应用
错误处理
由于 Node.js 和回调模式的异步特性,捕捉和记录错误发生的状态以备将来分析并不是一项简单的任务。在第十七章中,我们将介绍 Express.js 应用中域名的使用。在 Express.js 中使用域进行错误处理是一种更高级的技术,对于大多数开箱即用的实现来说,框架的内置错误处理可能已经足够了(加上定制的错误处理中间件)。
我们可以从我们的ch2/cli-app
例子中的基本开发错误处理程序开始。错误处理器抛出错误状态(500
,内部服务器错误)、堆栈跟踪和错误消息。当应用处于开发模式时,仅通过此代码启用:
if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
提示
app.get('env')
是process.env.NODE_ENV
的便捷方法;换句话说,前面一行可以用process.env.NODE_ENV === 'development'
改写。
这是有意义的,因为错误处理通常在整个应用中使用。因此,最好将其实现为中间件。
对于定制的错误处理程序实现,除了多了一个参数error
(或简称为err
):之外,中间件与其他任何中间件都是一样的
*// Main middleware*
app.use(function(err, req, res, next) {
*// Do logging and user-friendly error message display*
console.error(err);
res.status(500).send();
});
*// Routes*
我们可以使用res.status(500).end()
来获得类似的结果,因为我们没有发送任何数据(例如,错误消息)。建议至少发送一条简短的错误消息,因为这将有助于出现问题时的调试过程。事实上,响应可以是任何东西:JSON、文本、静态页面的重定向或其他东西。
对于大多数前端和其他 JSON 客户端,首选格式当然是 JSON:
app.use(function(err, req, res, next) {
*// Do logging and user-friendly error message display*
console.error(err);
res.status(500).send({status:500, message: 'internal error', type:'internal'});
})
注意开发者可以使用
req.xhr
属性或者检查Accept
请求头是否有application/json
值。
最简单的方法就是发一条短信:
app.use(function(err, req, res, next) {
*// Do logging and user-friendly error message display*
console.error(err);
res.status(500).send('internal server error');
})
或者,如果我们知道输出错误消息是安全的,我们可以使用下面的方法:
app.use(function(err, req, res, next) {
*// Do logging and user-friendly error message display*
console.error(err);
res.status(500).send('internal server error: ' + err);
})
为了简单地呈现一个名为500
(模板是文件500.jade
,引擎是 Jade)的静态错误页面,我们可以使用
app.use(function(err, req, res, next) {
*// Do logging and user-friendly error message display*
console.error(err);
*// Assuming that template engine is plugged in*
res.render('500');
})
或者,如果我们想要覆盖文件扩展名,我们可以使用以下内容作为完整的文件名500.html
:
app.use(function(err, req, res, next) {
*// Do logging and user-friendly error message display*
console.error(err);
*// Assuming that template engine is plugged in*
res.render('500.html');
})
我们也可以使用res.redirect()
:
app.use(function(err, req, res, next) {
*// Do logging and user-friendly error message display*
res.redirect('/public/500.html');
})
建议始终使用正确的 HTTP 响应状态,如401
、400
、500
等。快速参考参见表 9-1 。
表 9-1 。主要 HTTP 状态代码
|
密码
|
名字
|
意义
|
| — | — | — |
| Two hundred | 好 | 成功 HTTP 请求的标准响应 |
| Two hundred and one | 创造 | 请求已被满足。新资源已创建 |
| Two hundred and four | 没有内容 | 请求已处理。没有返回内容 |
| Three hundred and one | 永久移动 | 此请求和所有将来的请求都指向给定的 URI |
| Three hundred and four | 未修改 | 自上次请求后,资源未被修改 |
| four hundred | 错误的请求 | 由于语法错误,请求无法实现 |
| Four hundred and one | 未经授权的 | 身份验证是可能的,但是失败了 |
| Four hundred and three | 被禁止的 | 服务器拒绝响应请求 |
| Four hundred and four | 未发现 | 找不到请求的资源 |
| Five hundred | 内部服务器错误 | 服务器出现故障时的一般错误消息 |
| Five hundred and one | 未实施 | 服务器无法识别该方法或缺乏实现该方法的能力 |
| Five hundred and three | 服务不可用 | 服务器当前不可用 |
提示关于可用 HTTP 方法的完整列表,请参考位于
www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
的 RFC 2616。
这是我们发送状态500
(内部服务器错误)而不发回任何数据的方式:
app.use(function(err, req, res, next) {
*// Do logging and user-friendly error message display*
res.end(500);
})
要从请求处理程序和中间件内部触发错误,我们只需调用
app.get('/', function(req, res, next){
next(error);
});
或者,如果我们想要传递一个特定的错误消息,那么我们创建一个错误对象并将其传递给next()
:
app.get('/', function(req,res,next){
next(new Error('Something went wrong :-('));
});
使用return
关键字来处理多个容易出错的情况并结合前面的两种方法是一个好主意。例如,我们将数据库错误传递给next()
,但是一个空的查询结果不会导致数据库错误(即error
将是null
,所以我们用!users
: 检查这个条件
// A GET route for the user entity
app.get('/users', function(req, res, next) {
// A database query that will get us any users from the collection
db.get('users').find({}, function(error, users) {
if (error) return next(error);
if (!users) return next(new Error('No users found.'));
*// Do something, if fail the return next(error);*
res.send(users);
});
对于复杂的应用,最好使用多个错误处理程序。例如,一个用于 XHR/AJAX 请求,一个用于普通请求,一个用于通用的 catch-everything-else。使用命名函数(并将它们组织在模块中)而不是匿名函数也是一个好主意。
关于这种高级错误处理的例子,请参考第二十二章。
提示在管理错误处理方面有一个简单的方法,特别适合开发目的。它被称为
errorhandler
( https://www.npmjs.org/package/errorhandler
),它拥有 Express.js/Connect.js.的默认错误处理程序,更多关于errorhandler
的信息,请参考第四章。
运行应用
Express.js 类提供了一些应用范围内的对象和其对象上的方法,在我们的例子中是app
。推荐使用这些对象和方法,因为它们可以改进代码重用和维护。例如,不用到处硬编码数字3000
,我们只需用app.set('PORT', 3000);
分配它一次。然后,如果我们以后需要更新它,我们只有一个地方需要改变。因此,我们将在本节中介绍以下属性和方法:
app.locals
app.render()
app.mountpath
app.on('mount', callback)
app.path()
app.listen()
app.locals
app.locals
对象类似于res.locals
对象(在第八章的中讨论过),它将数据暴露给模板。然而,有一个主要的区别:app.locals
使它的属性在app
呈现的所有模板中可用,而res.locals
将它们限制为仅请求。因此,开发者需要小心不要通过app.locals
泄露任何敏感信息。这方面的最佳用例是应用范围的设置,如位置、URL、联系信息等。例如:
app.locals.lang = 'en';
app.locals.appName = 'HackHall';
app.locals
对象也可以像函数一样被调用:
app.locals([
author: 'Azat Mardan',
email: 'hi@azat.co',
website: 'http://proexpressjs.com'
]);
app.render()
app.render()
方法或者用视图名和回调调用,或者用视图名、数据和回调调用。例如,系统可能有一个用于“感谢您注册”消息的电子邮件模板和另一个用于“重置您的密码”的电子邮件模板:
var sendgrid = require('sendgrid')(api_user, api_key);
var sendThankYouEmail = function(userEmail) {
app.render('emails/thank-you', function(err, html){
if (err) return console.error(err);
sendgrid.send({
to: userEmail,
from: app.get('appEmail'),
subject: 'Thank you for signing up',
html: html // The html value is returned by the app.render
}, function(err, json) {
if (err) { return console.error(err); }
console.log(json);
});
});
};
var resetPasswordEmail = function(userEmail) {
app.render('emails/reset-password', {token: generateResetToken()}, function(err, html){
if (err) return console.error(err);
sendgrid.send({
to: userEmail,
from: app.get('appEmail'),
subject: 'Reset your password',
html: html
}, function(err, json) {
if (err) { return console.error(err); }
console.log(json);
});
});
};
注例子中使用的
sendgrid
模块在 NPM1 和 GitHub 都有。 2
app.mountpath
app.mountpath
属性用于挂载/订阅的应用。挂载的应用是子应用,可以用于更好的代码重用和组织。属性返回安装了app
的路径。
例如,在ch9/app-mountpath.js
中有两个子应用:post
和comment
。帖子挂载在 app 的/post
路径,评论挂载在帖子的/comment
。作为日志的结果,mountpath 返回值/post
和/comment
:
var express= require('express'),
app = express(),
post = express(),
comment = express();
app.use('/post', post);
post.use('/comment', comment);
console.log(app.mountpath); // ''
console.log(post.mountpath); // '/post'
console.log(comment.mountpath); // '/comment'
app.on('mount ',函数(父){…})
当子应用被装载到父/主应用的特定路径上时,装载被触发。例如,在ch9/app-on-mount.js
中,我们有两个带有装载事件监听器的子应用,它们打印父应用的装载路径。路径的值是post
的父级(app
)的/
和comment
的父级(post
)的/post
:
var express= require('express'),
app = express(),
post = express(),
comment = express();
post.on('mount', function(parent){
console.log(parent.mountpath); // '/'
})
comment.on('mount', function(parent){
console.log(parent.mountpath); // '/post'
})
app.use('/post', post);
post.use('/comment', comment);
app.path()
app.path()
方法将返回 Express.js 应用的规范路径。如果您正在使用安装到不同路径的多个 Express.js 应用(为了更好地组织代码),这将非常有用。
例如,通过在post
应用的/comment
路径上安装comment
应用,你可以获得帖子的评论资源(与评论相关的路径)。但是你仍然可以用comment.path()
(来自ch9/app-path.js
)获得“完整”路径:
var express= require('express'),
app = express(),
post = express(),
comment = express();
app.use('/post', post);
post.use('/comment', comment);
console.log(app.path()); // ''
console.log(post.path()); // '/post'
console.log(comment.path()); // '/post/comment'
app.listen()
Express.js app.listen(port, [hostname,] [backlog,] [callback])
方法类似于核心 Node.js http 模块中的server.listen()
3 。这个方法是启动 Express.js app 的方法之一。port
是服务器应该接受传入请求的端口号。hostname
是域名。当您将应用部署到云时,您可能需要设置它。backlog 是排队等待连接的最大数量。默认值为 511。而callback
是一个异步函数,在服务器启动时被调用。
要在特定端口上直接启动 Express.js 应用(3000):
var express = require('express');
var app = express();
*// ... Configuration*
*// ... Routes*
app.listen(3000);
这种方法是由 Express.js 生成器在第二章 Hello World 示例中的ch2/hello.js
和ch2/hell-name.js
示例中创建的。在这里,app.js
文件不启动服务器,但是它用
module.exports = app;
我们也不使用$ node app.js
运行app.js
文件。相反,我们用$ ./bin/www
启动了一个 shell 脚本www
。shell 脚本的第一行有这个特殊的字符串:
#!/usr/bin/env node
上面的代码行将 shell 脚本转换成 Node.js 程序。这个程序从app.js
文件中导入app
对象,设置端口,用listen()
和一个回调函数启动app
服务器
var debug = require('debug')('cli-app');
var app = require('../app');
app.set('port', process.env.PORT || 3000);
var server = app.listen(app.get('port'), function() {
debug('Express server listening on port ' + server.address().port);
});
当另一个过程需要您的服务器对象时,例如一个测试框架,将您的服务器对象导出为一个模块是必要的。在前面的例子中,主服务器文件(ch2/cli-app/app.js
)导出了对象,没有办法用$ node app
启动服务器。如果您不希望有一个单独的 shell 文件来启动服务器,但仍然希望在需要时导出服务器,您可以使用下面的技巧。这种方法的要点是检查模块是否是具有require.main === module
条件的依赖。如果是真的,那么我们启动应用。如果不是,那么我们公开方法和app
对象。
var server = http.createServer(app);
var boot = function () {
server.listen(app.get('port'), function(){
console.info('Express server listening on port ' + app.get('port'));
});
}
var shutdown = function() {
server.close();
}
if (require.main === module) {
boot();
} else {
console.info('Running app as a module');
exports.boot = boot;
exports.shutdown = shutdown;
exports.port = app.get('port');
}
除了app.listen()
之外的另一种启动服务器的方式是将 Express.js app 应用到核心 Node.js 服务器函数中。这对于生成具有相同代码库的 HTTP 服务器和 HTTPS 服务器非常有用:
var express = require('express');
var https = require('https');
var http = require('http');
var app = express();
var ops = require('conf/ops');
*//... Configuration*
*//... Routes*
http.createServer(app).listen(80);
https.createServer(ops, app).listen(443);
您可以创建一个自签名的 SSL 证书(例如,server.crt
文件),通过运行以下命令使用OpenSSL
在本地测试您的 HTTPS 服务器,以用于开发目的:
$ sudo ssh-keygen -f host.key
$ sudo openssl req -new -key host.key -out request.csr
$ sudo openssl x509 -req -days 365 -in request.csr -signkey host.key -out server.crt
OpenSSL 是安全套接字层(SSL)协议的开源实现,也是一个工具包。你可以在https://www.openssl.org
找到更多相关信息。当你使用 OpenSSL 时,Chrome 和许多其他浏览器会抱怨一个关于自签名证书的警告——无论如何你可以通过点击继续忽略它(见图 9-1 )。
图 9-1 。您可以忽略这个由自签名 SSL 证书引起的警告
?? 提示要在 Mac OS X 上安装
OpenSSL
,运行$ brew install OpenSSL
。在 Windows 上,从http://gnuwin32.sourceforge.net/packages/openssl.htm
下载安装程序。在 Ubuntu 上,运行apt-get install OpenSSL
。
在server.crt
准备好之后,像这样把它喂给https.createServer()
方法(ch9/app.js
文件):
var express = require('express');
var https = require('https');
var http = require('http');
var app = express();
var fs = require('fs');
var ops = {
key: fs.readFileSync('host.key'),
cert: fs.readFileSync('server.crt') ,
passphrase: 'your_secret_passphrase'
};
app.get('/', function(request, response){
response.send('ok');
});
http.createServer(app).listen(80);
https.createServer(ops, app).listen(443);
该密码是您在使用OpenSSL
创建证书时使用的密码。如果您没有输入任何密码,请忽略它。要启动这个过程,您可能必须使用 sudo,比如$ sudo node app
。
如果一切正常,您应该会看到如图图 9-2 所示的正常信息。
图 9-2 。使用自签名 SSL 证书进行本地开发
最后,如果您的应用执行大量的阻塞工作,您可能希望用cluster
模块启动多个进程。这个话题在第十三章中有所涉及。
摘要
本章介绍了实现错误处理程序的多种方法、app
对象接口以及启动 Express.js 服务器的方法。第二部分“深度 API 参考”到此结束希望您已经了解了 Express.js 框架对象的许多新属性和方法,例如响应、请求和应用本身。如果你对中间件有任何疑问,那么第四章消除了任何疑虑。最后但同样重要的是,我们讨论了路由、错误处理和模板利用主题。所有这些主题奠定了您的基础,因此您可以使用 Express.js 将这些知识应用于创建令人惊叹和激动人心的新应用
随着这本书的参考部分结束,我们正在进入更实际和复杂的主题,其中包括“如何使用 X”或“如何做 Y”的例子。继续第三部分,“解决常见和抽象的问题”
1
2
3
十、抽象
本章讨论代码组织。抽象通常意味着将整体逻辑分成几个部分。本章介绍的大多数技术可以应用于任何 Node.js 代码,而不仅仅是 Express.js 代码。
一个 Express.js app 通常有一个主文件(app.js
或者server.js
)。您应该尽量保持这个文件尽可能小,因为随着它变得越来越大,维护起来就越来越困难。最适合用来代替大型主文件的代码类型是中间件和路由。也可以抽象出配置语句,但是它们通常没有路由多,路由可能超过 200 到 300 条。
中间件
如第四章所述,中间件概念提供了灵活性。软件工程师可以使用匿名函数或命名函数作为中间件。使用匿名函数的方法如下所示:
app.use(function(request, response) {
// ...
});
app.get(function(request, response) {
//...
}, function(request, response) {
// ...
});
命名函数方法如下所示:
var middleware = function(request, response){
};
var middleware2 = function(request, response){
//...
};
var middleware3= function(request, response){
//...
};
app.use(middleware);
app.get(middleware2, middleware3);
出于代码重用的目的,命名函数方法更有吸引力。也就是说,如果命名了一个函数,就可以通过传递名称在多个路由中使用它。相反,使用匿名函数的方法只允许您在定义函数的地方使用中间件一次。
当使用命名函数时,随着应用变得越来越大,最佳实践是根据它们的功能将命名函数抽象到外部模块中,例如身份验证或数据库任务。
路线
假设我们有一个包含以下资源的 REST API:故事、元素和用户。我们可以相应地将请求处理程序分离到文件中,这样routes/stories.js
就有了
module.exports.findStories = function(req, res, next) {
// ... Query to find stories from the database
};
module.exports.createStory = function(req, res, next) {
// ... Query to create a story in the database
};
// ....
routes/users.js
文件保存用户实体的逻辑:
module.exports.findUser = function(req, res, next){
// ...
};
module.exports.updateUser = function(req, res, next){
// ...
};
module.exports.removeUser = function(req, res, next){
// ...
};
主服务器文件(app.js
或server.js
)可以以这种方式使用前面的模块:
// ...
var stories = require('./routes/stories');
var users = require('./routes/users');
// ...
app.get('/stories', stories.findStories);
app.post('/stories', stories.createStory);
app.get('/users/:user_id', users.findUser);
app.put('/users/:user_id', users.updateUser);
app.del('/users/:user_id', users.removeUser);
// ...
在带有var stories = require('./routes/stories');
的示例中,stories
是带有省略(可选).js
扩展名的文件stories.js
。
提示
'./routes/stories'
中的句号(.
)表示路径从当前文件夹开始。在 Windows 上,路径使用\
而不是/
,所以更好的方法是编写require(path.join('routes', 'stories'));
,因为它对跨平台更友好。
请注意,代码在每一行/每一个模块中不断重复;也就是说,开发人员必须通过导入相同的模块(例如users
)来复制代码。想象一下,我们需要在每个文件中包含这三个模块!为了避免这种重复,有一个聪明的方法来包含多个文件:将index.js
文件放在stories
文件夹中,并让该文件包含所有的路线。这是一个很好的做法,因为如果你以后想添加更多的文件到routes
文件夹或者改变现有文件的名称,你不需要从app.js
请求一个新文件。你只需要修改routes/index.js
代码。
例如,在app.js
中,我们通过只传递文件夹名来导入index.js
(如果我们传递文件夹名,require
会寻找index.js
):
app.get('/stories', require('./routes').stories.findStories);
或者,我们可以多次使用路线:
var routes = ('./routes');
app.get('/stories', routes.stories.findStories);
app.post('/stories', routes.stories. createStory);
这段代码将访问index.js
,这将公开stories
对象(和其他对象),该对象从routes
文件夹中的stories.js
文件导入:
module.exports.stories = require('./stories');
module.exports.users = require('./users);
前面的代码可以重写如下:
exports.stories = require('./stories');
exports.users = require('./users);
或者如下,这是我个人最喜欢的,因为它的口才:
module.exports = {
stories: require('./stories'),
users: require('./users)
};
最后,./routes/index.js
代码读取的stories.js
文件有
exports.findStories = function(req, res, next) {
// ...
};
exports.createStory = function(req, res, next) {
// ...
};
// ...
提示为了更好地组织路线,你可以使用
Router
类,在第六章中有所介绍。
功能的每一部分都可以分割成一个单独的文件。例如,findStories
方法进入ch10/routes-exports/find-stories.js
,内容如下:
exports.findStories = function(ops){
ops=ops || '';
console.log ('findStories module logic ' + ops);
};
In the index.js we simply import the find-stories.js:
exports.stories = require('./find-stories.js');
举个工作例子,你可以从expressjsguide
文件夹运行node -e "require('./routes-exports').stories.findStories();"
来查看console.log
从模块输出的字符串,如图图 10-1 所示。
图 10-1 。通过文件夹和 index.js 文件导入模块
结合中间件和路由
为了说明代码重用的另一种方法,假设有一个应用具有路线/admin
和/api/stories
:
app.get('/admin', function(req, res, next) {
if (!req.query._token) return next(new Error('No token was provided.'));
}, function(req, res, next) {
res.render('admin');
});
*// Middleware that applied to all /api/* calls*
app.use('/api/*', function(req, res, next) {
if (!req.query.api_key) return next(new Error('No API key was provided.'));
});
app.get('/api/stories', findStory, function(req, res){
res.json(req.story):
});
在这两个示例中,我们使用以下代码行检查查询字符串参数:
if (!req.query._token) return next(new Error('no token provided'));
和
if (!req.query.api_key) return next(new Error('No API key was provided.'));
但是,参数是不同的,所以我们不能将两个语句抽象成一个函数。不用重复我们自己不是很聪明吗?
为了避免重复,我们可以实现一个返回函数的函数,如下所示:
var requiredParam = function (param) {
*// Do something with the param, e.g.,*
*// Create a private attribute paramName based on the value of param variable*
var paramName = '';
if (param === '_token')
paramName = 'token';
else if (param === 'api_key')
paramName = 'API key'
return function (req,res, next) {
*// Use paramName, e.g.,*
*// If query has no such parameter, proceed next() with error using paramName*
if (!req.query[param]) return next(new Error('No ' + paramName +' was provided.'));
next();
});
}
app.get('/admin', requiredParam('_token'), function(req, res, next) {
res.render('admin');
});
*// Middleware that applied to all /api/* calls*
app.use('/api/*', requiredParam('api_key'));
从某种意义上说,这种“返回函数的函数”模式是一种开关,它根据传递的参数改变模式。
提示前面提到的“返回函数的函数”模式类似于状态单子(
http://en.wikipedia.org/wiki/Monad_(functional_programming)#State_monads
)。这可以作为面试时讨论的一个很好的话题。
前面的例子非常简单,在大多数情况下,开发人员不需要担心正确的错误文本消息的映射。然而,您可以将这种模式用于许多目的,例如限制输出、管理权限以及在不同的块之间切换。
注意
__dirname
全局变量提供了使用它的文件的绝对路径,而./
返回当前工作目录,这可能会根据我们执行 Node.js 脚本的位置而有所不同(例如,$ node ~/code/app/index.js
与$ node index.js
)。./
规则的一个例外是当它被用在require()
函数中时,比如conf = require('./config.json');
,在这种情况下它充当__dirname
。
正如您所看到的,中间件/请求处理程序的使用是保持代码有组织的一个强大的概念。最佳实践是通过将所有逻辑移入相应的外部模块/文件来保持路由精简。这样,当您需要时,重要的服务器配置参数都会整齐地放在一个地方。
全局变量module.exports
和exports
在给它们分配新值时并不完全相同。在前面的例子中,module.exports = function(){...}
运行良好,完全有道理,但是exports = function() {...}
甚至exports = someObject;
将悲惨地失败。原因是 JavaScript 基础知识:对象的属性可以被替换,而不会丢失对该对象的引用,但当我们替换整个对象(即exports = ...,
)时,我们会丢失与暴露我们函数的外部世界的链接。
这种行为也被称为对象是可变的和原始的(字符串、数字和布尔值在 JavaScript 中是不可变的)。因此,exports
只能通过创建属性并为其赋值来工作,比如exports.method = function() {...};
。例如,我们可以在ch10
文件夹中运行下面的代码:
$ node -e "require('./routes-module-exports').FindStories('databases');"
结果,你可以在图 10-2 中看到,我们的嵌套结构减少了一级。
图 10-2 。使用 module.exports 的结果
如需更多示例,请参考 Karl sweep(http://openmymind.net/2012/2/3/Node-Require-and-Exports
)的文章“Node.js,Require and Exports”。对于本书中的一个工作示例,请查看第二十二章中的 HackHall 示例。
摘要
养成良好的代码组织习惯和实践是极其重要的。没有他们,项目将很难维持。这在大型项目中更是如此。
在下一章中,我们将继续这个主题,但是将它应用于数据库。我们还将探索如何处理键(以及您不希望受到攻击的其他敏感信息),并在应用于 Express.js 应用时使用流。
十一、数据库、密钥和流提示
本章延续了上一章的主题,即在 Express.js 应用中更好地组织代码。本章提供了关于如何从另一个模块连接到数据库、访问应该保密的密钥和密码以及数据流的技巧。
在模块中使用数据库
本节讨论代码组织模式。这不是一个关于数据库和 Express.js 的详细教程。关于这方面的内容,请参考第二十章到第二十二章。
在我向您展示如何从另一个模块访问数据库连接之前,如果您将路由抽象到单独的app.js
/ server.js
文件中,可能会用到这个模块,让我们先复习一些基础知识。
对于本机 Node.js MongoDB 驱动程序,Express.js 服务器需要等待连接被创建,然后才能使用数据库:
// ... Modules importing
var routes = require('routes');
var mongodb = require('mongodb');
var Db = mongodb.Db;
var db = new Db('test', new Server(dbHost, dbPort, {}));
// ... Instantiation
db.open(function(error, dbConnection){
var app = express();
app.get('/', routes.home);
// ... Other routes
app.listen(3000);
});
我们可以通过将路由和配置移到数据库回调之外来做得更好:
// ... Modules importing
var routes = require('routes');
var mongodb = require('mongodb');
var Db = mongodb.Db;
var db = new Db('test', new Server(dbHost, dbPort, {}));
// ... Instantiation
var app = express();
app.get('/', routes.home);
// ... Other routes
db.open(function(error, dbConnection){
app.listen(3000);
});
但是在数据库连接建立之后,仍然需要调用app.listen()
调用。
多亏了更高级的库,比如 Mongoskin、Monk 和 Mongoose,它们可以缓冲数据库请求,我们不需要将app.listen()
调用放在db
回调中。开发人员的任务可以像这样简单:
var express = require('express'),
mongoskin = require('mongoskin'),
bodyParser = require('body-parser');
var app = express();
app.use(bodyParser.json());
// ... Configurations and middleware
var db = mongoskin.db('localhost:27017/test', {safe:true});
app.get('/', function(req, res) {
res.send('please select a collection, e.g., /collections/messages')
});
// ... Routes
app.listen(3000);
如果路由需要访问数据库对象,例如连接、模型等等,但是这些路由不在主服务器文件中,那么我们需要做的就是使用定制中间件将所需的对象附加到请求(即req
)上:
app.use(function(req, res, next) {
req.db = db;
next();
});
这样在刚刚展示的自定义中间件之后声明的所有中间件和路由都会有req.db
对象;req
对象是Request
对象的同一个实例。
或者,在导入或导出模块时,我们可以将需要的变量传递或接受到构造函数中。在 JavaScript/Node.js 中,对象是通过引用传递的,所以我们将在模块中处理原始对象。routes.js
模块的例子如下:
module.exports = function(app){
console.log(app.get('db')); *// app has everything we need!*
// ... Initialize other objects as needed
return {
findUsers: function(req, res) {
// ...
},
createUser: function(req, res) {
// ...
}
} // for "return"
}
这是主文件:
var app = express();
// ... Configuration
app.set('db', db);
routes = require('./routes.js')(app);
// ... Middleware
app.get('/users', routes.findUser);
// ... Routes
或者,我们可以在变量中重构:
var app = express();
// ... Configuration
app.set('db', db);
Routes = require('./routes.js');
routes = Routes(app);
// ... Middleware
app.get('/users', routes.findUser);
// ... Routes
您可以尝试使用proexpressjs/ch11
文件夹中的示例将数据传递给模块本身。为此,只需运行以下命令:
$ node -e "require('./routes-module-exports').FindStories('databases');"
从proexpressjs/ch11
文件夹运行它。该命令只是导入模块并从中调用一个方法。结果,您将看到在终端中打印出单词“databases”(或者您传递给FindStories
的任何其他字符串)。
关于通过一个req
对象将一个 Mongoose 数据库对象传递给 routes 的真实例子,请看一下第二十二章。
密钥和密码
对于数据库,典型的 web 服务可能需要通过用户名和密码连接到其他服务,对于第三方 API,则需要 API 密钥和秘密/令牌。正如您可能猜到的,将这些敏感数据存储在源代码中并不是一个好主意!解决这个问题的两种最普遍的方法是
- JSON 文件
- 环境变量
注意这一节我们说的是 Node.js,不是浏览器 JavaScript。通常,您不希望在前端暴露您的密码和 API 密钥。
JSON 文件
JSON 文件方法 听起来很简单。我们只需要一个 JSON 文件。例如,假设我们在conf/keys.json
中有一个本地数据库和两个外部服务,比如 HackHall 和 Twitter:
{
"db": {
"host": "http://localhost",
"port": 27017,
"username": "azat",
"password": "CE0E08FE-E486-4AE0-8441-2193DF8D5ED0"
},
"hackhall": {
"api_key": "C7C211A6-D8A7-4E41-99E6-DA0EB95CD864"
},
"twitter": {
"consumer_key": "668C68E1-B947-492E-90C7-F69F5D32B42E",
"consumer_secret": "4B5EE783-E6BB-4F4E-8B05-2A746056BEE1"
}
}
Node.js 的最新版本允许开发者用require()
函数导入 JSON 文件。为没有乱搞fs
模块而欢呼!因此,主应用文件可能会使用这些语句:
var configurations = require('/conf/keys.json');
var twitterConsumerKey = configurations.twitter.consumer_key;
或者,我们可以用fs
模块手动读取文件,并将流解析成 JavaScript 对象。自己试试这个。
至于对configurations
的访问,如果我们可以使用app.set(name, value)
全局共享这个配置对象就更好了:
app.set('configurations', configurations);
或者,使用中间件,传播到以下每个请求:
app.use(function(req, res, next) {
req.configurations = configurations;
});
将conf/keys.json
添加到。gitignore
防止跟踪和暴露文件。要添加它,只需创建一个新的系统文件.gitignore
,并添加这一行:
conf/keys.json
如果您将您的密钥提交给 Git 一次,那么即使您删除了该文件,它们也会保留在历史记录中。从 Git 历史中删除敏感数据的解决方案很棘手。最好重新生成密钥以避免暴露。
将 JSON 配置文件传送到服务器时,问题仍然存在。这可以通过 SSH 和scp
(安全复制)命令来完成:
$ scp [options] username1@source_host:directory1/filename1 username2@destination_host:directory2/filename2
比如$ scp ./keys.json azat@webapplog:/var/www/conf/keys.json
。
或者,您可以使用rsync
,因为它只传输增量。例如:
$ rsync -avz ./keys.json azat@webapplog:var/www/conf
环境变量
第二种方法涉及到环境变量 (env vars)的使用。说明 env 变量最简单的方法是用前缀key=value
开始脚本,例如$ NODE_ENV=test node app
。这将填充process.env.NODE_ENV
。试试这个脚本,它会将NODE_ENV
打印出来:
$ NODE_ENV=test node -e 'console.log(process.env.NODE_ENV)'
为了将这些 var 交付/部署到远程服务器中,我们可以使用 Ubuntu 的/etc/init/nodeprogram.conf
。凯文·范·松内维尔德的这篇简洁的教程提供了更多的细节:“在 Ubuntu 上运行 Node.js 作为服务”1
此外,还有一个 Nodejitsu 工具(http://www.nodejitsu.com
)可以永远守护 Node 流程(http://npmjs.org/forever
);GitHub: https://github.com/nodejitsu/forever
。
对于 Heroku 来说,将 env 变量与云同步的过程甚至更简单:在本地,我们将变量放入。env
为工头 2 (自带 Heroku toolbelt)在项目文件夹中归档,然后用heroku-config
( https://github.com/ddollar/heroku-config
推送到云端。更多信息在 Heroku 发展中心。 3
对于一个工作示例(显然没有敏感信息),看一看第二十二章。
流
Express.js 请求和响应对象分别是可读和可写的 Node.js 流。Streams 是在特定进程(读取、接收、写入、发送)实际结束之前处理大块数据的强大工具。这使得流在处理大量数据(如音频或视频)时非常有用。流的另一个例子是在执行大型数据库迁移时。
提示关于如何使用 streams 的更多信息,substack 的 James Halliday (
http://substack.net/
)提供了一些惊人的资源:stream-handbook ( https://github.com/substack/stream-handbook
)和 stream-adventure ( https://npmjs.org/package/stream-adventure
)。
下面是一个从proexpressjs/ch11/streams-http-res.js
到普通响应的管道流的例子:
var http = require('http');
var fs = require('fs');
var server = http.createServer(function (req, res) {
fs.createReadStream('users.csv').pipe(res);
});
server.listen(3000);
来自终端的带有 CURL 的 GET 请求如下所示:
$ curl http://localhost:3000
前面一行将导致服务器输出文件users.csv
的内容;例如:
...
Stanton Botsford,Raina@clinton.name,619
Dolly Feeney,Aiden_Schaefer@carmel.tv,670
Oma Beahan,Mariano@paula.tv,250
Darrion Johnson,Miracle@liliana.com,255
Garth Huels V,Patience@leda.co.uk,835
Chet Hills II,Donna.Lesch@daniela.co.uk,951
Margarette Littel,Brenda.Prosacco@heber.biz,781
Alexandrine Schiller,Brown.Kling@jason.name,779
Concepcion Emmerich,Leda.Hudson@cara.biz,518
Mrs. Johnpaul Brown,Conrad.Cremin@tavares.tv,315
Aniyah Barrows,Alexane@daniela.tv,193
Okey Kohler PhD,Cordell@toy.biz,831
Bill Murray,Tamia_Reichert@zella.com,962
Allen O'Reilly,Jesus@joey.name,448
Ms. Bud Hoeger,Ila@freda.us,997
Kathryn Hettinger,Colleen@vincenza.name,566
...
结果也显示在图 11-1 中。
图 11-1 。从 users.csv 文件运行流响应的结果
如果想创建自己的测试文件如users.csv
,可以安装 faker . js(https://npmjs.org/package/Faker
;GitHub: https://github.com/marak/Faker.js/
)并重新运行seed-users.js
文件:
$ npm install Faker@0.7.2
$ node seed-users.js
Express.js 实现在proexpressjs/ch11/stream-express-res.js
中惊人地相似:
var fs = require('fs');
var express = require('express');
var app = express();
app.get('*', function (req, res) {
fs.createReadStream('users.csv').pipe(res);
});
app.listen(3000);
请记住,请求是一个可读的流,而响应是一个可写的流,我们可以实现一个服务器,将 POST 请求保存到一个文件中。下面是proexpressjs/ch11/stream-http-req.js
的内容:
var http = require('http');
var fs = require('fs');
var server = http.createServer(function (req, res) {
if (req.method === 'POST') {
req.pipe(fs.createWriteStream('ips.txt'));
}
res.end('\n');
});
server.listen(3000);
我们调用 Faker.js 来生成由名称、域、IP 地址、纬度和经度组成的测试数据。这一次,我们不会将数据保存到文件中,而是通过管道将它发送到 CURL。
下面是 Faker.js 脚本的一部分,它从proexpressjs/ch11/seed-ips.js
向 stdout 输出 1000 条记录的 JSON 对象:
var Faker = require('Faker');
var body = [];
for (var i = 0; i < 1000; i++) {
body.push({
'name': Faker.Name.findName(),
'domain': Faker.Internet.domainName(),
'ip': Faker.Internet.ip(),
'latitude': Faker.Address.latitude(),
'longitude': Faker.Address.longitude()
});
}
process.stdout.write(JSON.stringify(body));
为了测试我们的stream-http-req.js
,让我们跑吧
$ node seed-ips.js | curl -d@- http://localhost:3000.
结果是一个 IP 数组,如图图 11-2 所示。
图 11-2 。Node.js 服务器编写的文件的开头
让我们再一次将这个例子转换成一个 Express.js 应用:
var http = require('http');
var express = require('express');
var app = express();
app.post('*', function (req, res) {
req.pipe(fs.createWriteStream('ips.txt'));
res.end('\n');
});
app.listen(3000);
提示在某些情况下,拥有不消耗太多资源的直通逻辑是很好的。为此,请通过(
https://npmjs.org/package/through
)查看模块;GitHub: https://github.com/dominictarr/through
)。另一个有用的模块是 concat-stream(https://npmjs.org/package/concat-stream
;GitHub: https://github.com/maxogden/node-concat-stream
)。它允许流的连接。
摘要
到目前为止,我们已经介绍了从其他非app.js
/ server.js
文件实现数据库连接的方法。我们还对我们的源库隐藏敏感信息,并流式传输数据。这些概念和方法在处理第十九章到第二十二章中的例子时会派上用场。
下一章将给出一些使用 Redis 和实现认证模式的 Express.js 技巧。
1
2
3
十二、Redis 和认证模式
本章讨论两个 Express.js 主题:Redis 和认证模式。Redis 是一个快速数据库,通常用于存储 Express.js 会话。
使用心得
Redis ( http://redis.io
)经常在 Express.js 应用中用于会话持久性,因为将会话存储在物理存储中可以防止应用在系统重启或重新部署时丢失用户数据。它还支持使用多个 RESTful 服务器,因为它们可以连接到同一个 Redis 服务器。此外,Redis 可用于队列和调度任务(例如,电子邮件作业)。
Redis 本身是一项独立的服务。因此,要在 Express.js 中使用 Redis,我们需要两样东西:
- Redis 服务器:可以监听特定端口并通过 Redis 控制台或应用访问的数据库服务器
- Connect-redis :一个 NPM 模块(
https://www.npmjs.org/package/connect-redis
);GitHub:https://github.com/tj/connect-redis
),使 Express.js 能够使用 redis 存储,并包含 Redis 模块(https://www.npmjs.org/package/redis
;GitHub:https://github.com/mranney/node_redis
)
要下载 Redis 2.6.7,请输入以下简单命令:
$ wget http://download.redis.io/releases/redis-2.6.7.tar.gz
$ tar xzf redis-2.6.7.tar.gz
$ cd redis-2.6.7
$ make
更多 Redis 说明,可以访问http://redis.io/download
。
要开始重定向,请按 enter 键
$ src/redis-server
要停止重复,只需按 Ctrl+C 即可。
要访问 Redis 命令行界面,请输入
$ src/redis-cli
以下是如何使用 Redis 管理 Express.js 会话的简单示例。
首先,要访问 Redis,使用connect-redis
驱动程序。您可以使用ch12/package.json
中熟悉的依赖项键/值对来实现这一点:
{
"name": "redis-example",
"dependencies": {
"express": "4.8.1",
"connect-redis": "2.1.0",
"cookie-parser": "1.3.2",
"express-session": "1.7.6"
}
}
要使用 Redis 作为 Express.js 服务器中的会话存储(ch12/app.js
),请输入以下内容:
var express = require('express');
var app = express();
var cookieParser = require('cookie-parser');
var session = require('express-session');
var RedisStore = require('connect-redis')(session);
app.use(cookieParser());
app.use(session({
resave: true,
saveUninitialized: true,
store: new RedisStore({
host: 'localhost',
port: 6379
}),
secret: '0FFD9D8D-78F1-4A30-9A4E-0940ADE81645',
cookie: { path: '/', maxAge: 3600000 }
}));
接下来,定义“/
”路由,该路由将递增每个唯一会话的计数器。换句话说,如果我们关闭浏览器,停止 Express.js 服务器,等待一段时间,重新启动服务器,然后重新打开浏览器,只要带有会话 ID 的 cookie 没有过期或被删除,该值就会被保存并递增。此外,输出计数器和会话 ID:
app.get('/', function(request, response){
console.log('Session ID: ', request.sessionID);
if (request.session.counter) {
request.session.counter = request.session.counter +1;
} else {
request.session.counter = 1;
}
response.send('Counter: ' + request.session.counter);
});
app.listen(3000);
现在,当你启动服务器时,它应该向你显示Counter: 1
并且connect.sid
cookie 应该有一个类似于下面的值(见图 12-1 ):
s%3AA3l_jSr25tbWWjRHot9sEUM5OApCn21R.qxxe7TLSaZBwuCGKmSfvI9jpVcnLyUrKMEkXxXMvAzM
图 12-1 。Redis store Express.js 会话示例的输出
您的会话 ID 会有所不同,但格式和长度是相同的。让我们在浏览器或 Node.js 控制台中用decodeURIComponent()
方法解码这个值(见图 12-2 ):
decodeURIComponent('s%3AA3l_jSr25tbWWjRHot9sEUM5OApCn21R.qxxe7TLSaZBwuCGKmSfvI9jpVcnLyUrKMEkXxXMvAzM')
图 12-2 。解码 Express.js 会话 ID
s:
和.
之间的值是会话 ID。将它与服务器日志中打印的值进行比较。它们应该匹配。
要再次检查计数器值是否确实存储在 Redis 中,而不是存储在其他地方,请复制会话 ID,并在新的终端窗口中打开 Redis 控制台
$ redis-cli
然后,键入以下命令来获取会话值:get sess:SESSION_ID
,其中SESSION_ID
是您的会话 ID;例如:
> get sess:A3l_jSr25tbWWjRHot9sEUM5OApCn21R
您应该看到带有会话值计数器的 JSON 对象的字符串,以及一些选项。例如,我的输出是
"{\"cookie\":{\"originalMaxAge\":3600000,\"expires\":\"2014-09-03T19:03:55.007Z\",\"httpOnly\":true,\"path\":\"/\"},\"counter\":1}"
您还可以使用> keys
命令来获取存储的会话密钥列表(前缀为sess
):
> keys sess*
正如本章开始时提到的,connect-redis 模块由 redis 模块提供支持。有了这个模块,Redis 可以作为一个平面的、独立的数据库使用。有趣的是,Redis 支持四种类型的数据:字符串、列表、集合和散列。
提示要深入研究 Redis,可以在
http://try.redis.io
找到一个互动教程。
身份验证模式
最常见的认证类型是要求用户名和密码的组合。我们可以对照数据库检查匹配,然后在会话中存储一个authenticated=true
标志。Express.js 为该代理发出的每个其他请求自动存储的会话数据:
app.use(function(req, res, next) {
if (req.session && req.session.authenticated)
return next();
else {
return res.redirect('/login');
}
}
如果我们需要额外的用户信息,它也可以存储在会话中:
app.post('/login', function(req, res) {
*// Check the database for the username and password combination*
*// In a real-world scenario you would salt the password*
db.findOne({username: req.body.username,
password: req.body.password},
function(error, user) {
if (error) return next();
if (!user) return next(new Error('Bad username/password');
req.session.user = user;
res.redirect ('/protected_area');
}
);
});
对于 salt 密码的工具,看一下bcryptjs
( https://www.npmjs.org/package/bcryptjs
),它与bcrypt
兼容,但不需要编译,因为它完全在 JavaScript/Node.js 上运行。与其他库或您自己的哈希/salt 实现相比,bcryptjs
(和 bcrypt)的另一个很酷的事情是,salt 是密码的一部分,所以您不需要在数据库中为每个用户存储额外的 salt 字段。例如:
var bcrypt = require('bcryptjs');
bcrypt.hash('pr0expressr0cks!, 8, function(err, hash) {
// ... Store the hash, which is a password and salt together
});
使用异步 bcryptjs 方法,因为它们会非常慢(越慢越好保护!).
与第三方的认证通常通过 OAuth 来完成。对于oauth
模块(https://www.npmjs.org/package/oauth
)的工作示例;GitHub:https://github.com/ciaranj/node-oauth
)——作者提供了一些文档——查看第二十二章中的 HackHall 示例。
OAuth 1.0/2.0 要求回调路由,以便用户重定向回我们的站点。使用 Express.js,这是毫不费力的。此外,还有完全自动化的解决方案来处理一切(数据库、签名、路由等)。):Everyauth ( https://npmjs.org/package/everyauth
)和 Passport ( https://npmjs.org/package/passport
)。
提示要想快速了解适用于 Node.js 的 OAuth,可以考虑阅读我的书,《用 Node.js 介绍 OAuth:Twitter API OAuth 1.0、OAuth2.0、OAuth Echo、Everyauth 和 OAuth 2.0 服务器示例 (webapplog.com,2014)。
摘要
在这一章中,我们安装并使用了 Redis,这样会话信息就可以持久,也就是说,如果服务器停机,它也不会丢失。Redis(或任何其他持久性存储)允许我们在多个服务器之间共享会话。然后我们讲述了用中间件实现认证。
在下一章中,我们将讨论集群多线程,继续让您的 Express.js 应用产品化的主题。
十三、集群和多线程
有很多批评者反对使用 Node.js,他们的许多观点都源于一个神话,即基于 Node.js 的系统有是单线程的。事实远非如此——使用cluster
模块,我们可以毫不费力地派生一个 Node.js 流程来创建多个流程。是的,每个进程仍将是单线程的,并且会被不合适的同步代码或一些费力的进程(如密码散列)阻塞。然而,现在由几个进程组成的整个系统不会被阻塞。
在 web 应用的情况下,进程可以监听同一个端口,从而确保如果第一个进程繁忙,请求将由第二个(或第三个或第四个)进程处理。通常情况下,我们会根据机器上的 CPU 数量生成尽可能多的进程,这样我们就可以利用机器上的所有 CPU。
多线程的例子
下面是一个在四个进程上运行的 Express.js 应用的工作示例。其中一个是所谓的师傅工序,另外三个是工人工序。master 负责管理和监控 worker,而 worker 本身就是一个独立的 Express.js app。master 和 worker 的代码包含在同一个文件中。
在文件的开头,我们导入依赖关系:
var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;
var express = require('express');
模块有一个属性告诉我们这个进程是主进程还是子进程。我们使用该属性生成四个工人(默认工人将使用相同的文件,但这可以用setupMaster()
方法覆盖)。 1 除此之外,我们还可以附加事件监听器,接收来自工作器的消息(例如'kill'
)。
if (cluster.isMaster) {
console.log('Fork %s worker(s) from master', numCPUs)
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
};
cluster.on('online', function(worker) {
console.log('Worker is running on %s pid', worker.process.pid)
});
cluster.on('exit', function(worker, code, signal) {
console.log('Worker with %s is closed', worker.process.pid );
});
worker 代码只是一个稍加改动的 Express.js 应用。我们正在获取进程 ID ,即 PID(下面的代码延续了前面的代码片段):
} else if (cluster.isWorker) {
现在我们编写一个端口为 3000 的 Express.js 服务器代码:
var port = 3000;
console.log('Worker (%s) is now listening to http://localhost:%s',
cluster.worker.process.pid, port);
var app = express();
服务器有一个包罗万象的路由,它将打印 PID:
app.get('*', function(req, res) {
res.send(200, 'cluster '
+ cluster.worker.process.pid
+ ' responded \n');
})
app.listen(port);
}
请在您的项目中随意使用ch13/cluster.js
的完整源代码:
var cluster = require('cluster');
var numCPUs = require('os').cpus().length;
var express = require('express');
if (cluster.isMaster) {
console.log('Fork %s worker(s) from master', numCPUs);
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('online', function(worker) {
console.log('Worker is running on %s pid', worker.process.pid);
});
cluster.on('exit', function(worker, code, signal) {
console.log('Worker with %s is closed', worker.process.pid );
});
} else if (cluster.isWorker) {
var port = 3000;
console.log('Worker (%s) is now listening to http://localhost:%s',
cluster.worker.process.pid, port);
var app = express();
app.get('*', function(req, res) {
res.send(200, 'cluster ' + cluster.worker.process.pid + ' responded \n');
});
app.listen(port);
}
像往常一样,要启动一个应用,运行$ node cluster
。应该有四个(或者两个,取决于你机器的架构)进程,日志可能看起来像这样(见图 13-1 ):
worker is running on 15279 pid
worker is running on 15277 pid
...
图 13-1 。用集群启动四个进程
有不同的进程监听同一个端口并响应我们。例如,响应可能如下(见图 13-2 ):
cluster 15278 responded
cluster 15280 responded
图 13-2 。服务器响应由不同的进程呈现
提示如果你更喜欢现成的解决方案而不是低级库(比如
cluster
),那就去看看易贝创建并使用的真实世界的生产库:cluster2
(https://www.npmjs.org/package/cluster2
;GitHub: https://github.com/ql-io/cluster2
)。
再次查看ch13/cluster.js
,注意我们可以将实际的 Express.js 应用抽象到一个单独的文件中。这是一件好事,因为它保持了两个不同逻辑单元、集群和 Express.js 应用之间的分离。例如,前面列出的集群文件可以重构为:
var cluster = require('cluster');
var numCPUs = require('os').cpus().length;
var app = require('./app'); // <- THIS FILE!
if (cluster.isMaster) {
console.log('Fork %s worker(s) from master', numCPUs);
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('online', function(worker) {
console.log('Worker is running on %s pid', worker.process.pid);
});
cluster.on('exit', function(worker, code, signal) {
console.log('Worker with %s is closed', worker.process.pid );
});
} else if (cluster.isWorker) {
var port = 3000;
console.log('Worker (%s) is now listening to http://localhost:%s',
cluster.worker.process.pid, port);
app.listen(port);
}
摘要
在这一章中,我们探索了一种实现系统的方法,该系统有一个主 Node 和多个工作 Node,它们是在同一个端口上监听的 Express.js 应用。这是一个很有价值的技巧,你可以用它来减少你的服务器负载,让你用更少的机器提供更多的流量,这反过来又会节省你的钱。
在下一章,我们将介绍如何将 Stylus、Less 和 Sass CSS 库应用于 Express.js 服务器。
1
十四、应用 Stylus、Less 和 Sass
对于任何 web 项目来说,使用级联样式表(CCS)都是必须的,但是在复杂的项目中,CSS 样式表的编写和管理非常繁琐。这主要是因为 CSS 不是真正的编程语言。CSS 没有继承、变量或函数。对于普通的 CSS 资源来说,代码重用和可维护性是一个棘手的问题。对于大型项目或旨在在不同项目间共享的 CSS 库来说尤其如此。
解决使用普通 CSS 的痛苦的方法是使用另一种更好的语言,这种语言在构建时(用于生产)或在运行中(用于开发)被编译到 CSS 中。对于后者,Express.js 使用中间件,所以每次网页请求 CSS 资产时,框架都会将更好的 CSS 代码转换为普通 CSS。一些更好的 CSS 库包括 Stylus(我最喜欢的)、Less(Twitter Bootstrap 团队最喜欢的)和 Sass。
因此,Stylus、Less 和 Sass 带来了急需的可重用性(混合、扩展、变量等)。)转换为样式表,这样我们作为开发人员就可以更高效、更容易地重用 CSS 代码。让我们看看当我们使用 Express.js 时,如何利用这一令人惊叹的资源。
关于这些库及其特性的详细介绍超出了本章的范围,因为这些特性非常多,并且将来可能会发生变化(所以最好参考官方文档,本章末尾提供了相关链接)。本章教你如何将库插入 Express.js 应用。这些库本身(在很大程度上)是向后兼容普通 CSS 的;因此,他们有一个宽容、平坦的学习曲线。
提示 Express Generator v4.2.0 支持 Less 和 Stylus 库,但不支持 Sass。
唱针
Stylus 是 Express.js 的兄弟,是最常用的 CSS 框架。 1 我最喜欢它的原因和我喜欢 Jade 的原因一样:它通过最小化工作所需的字符数来实现雄辩。我必须键入的字符越少,我出错的机会就越少,我就有更多的时间来解决实际问题。
要安装 Stylus,请键入并运行
$ npm install stylus@0.42.3 --save.
使用带有app.use
和文件夹名称的 Stylus。然后,要应用static
中间件,将它包含在您的服务器文件中:
//... Import dependencies and instantiate app
app.use(require('stylus').middleware(__dirname + '/public'));
app.use(express.static(path.join(__dirname, 'public')));
//... Routes and app.listen()
将*.styl
文件放入我们公开的文件夹中(例如public
或public/css
,并使用*.css
扩展名将它们包含在 Jade(或任何其他)模板中。对,没错!文件是*.styl
,但是在 Jade 或 HTML 代码中,我们要求的是*.css
文件。Express.js 会变魔术的!例如,我们在public/stylesheets/
中有style.styl
,所以我们在 template/HTML 中使用/stylesheets/style.css
:
//...
head
link(rel='stylesheet', href='/stylesheets/style.css')
//...
或者,在您选择的任何其他模板或纯 HTML 文件中,输入以下内容:
<link rel="stylesheet" href="/stylesheets/style.css"/>
对于从头开始创建的项目,您可以使用生成器命令:
$ express -c stylus express-app-name command
您可以在ch14/stylus
文件夹和 GitHub ( https://github.com/azat-co/proexpressjs
)上找到支持 Stylus 的项目。
较少的
要在 Express.js 中少用,我们需要less-middleware
,它是一个外部 NPM 模块(
https://www.npmjs.org/package/less-middleware
)
:
$ npm install less-middleware@1.0.0 --save
然后,我们需要在static
和路由(ch14/less/app.js
)前添加less-middleware
:
//... Import dependencies and instantiate app
app.use(require('less-middleware')(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'public')));
//... Routes and app.listen()
假设较少的文件在public/css
中,我们可以链接*.css
文件,剩下的会自动处理;例如,一个 Jade 模板可能会这样使用:
//...
head
link(rel='stylesheet', href='/stylesheets/style.css')
//...
或者,在您选择的任何其他模板中,或者在纯 HTML 文件中,使用以下内容:
<link rel="stylesheet" href="/stylesheets/style.css"/>
对于从头开始创建的项目,您可以使用$ express -c less express-app-name
命令。
你可以在ch14/less
文件夹和 GitHub ( https://github.com/azat-co/proexpressjs
)上找到这个功能较少的项目。
厚颜无耻
要在 Express.js 中使用 Sass ,我们需要 node-sass,它是一个外部 NPM 模块(https://npmjs.org/package/node-sass
;GitHub: https://github.com/sass/node-sass
):
$ npm install node-sass@0.6.7 --save
这是我们用于 Sass ( ch14/sass/app.js
)的 Express.js 插件:
// ... Import dependencies and instantiate app
app.use(require('node-sass').middleware({
src: __dirname + '/public',
dest: __dirname + '/public',
debug: true,
outputStyle: 'compressed'
}));
app.use(express.static(path.join(__dirname, 'public')));
// ... Routes and app.listen()
Jade 模板还导入了*.css
文件:
link(rel='stylesheet', href='/stylesheets/style.css')
支持 Sass 的项目在ch14/sass
文件夹中,也在 GitHub ( https://github.com/azat-co/proexpressjs
)上。
摘要
我们在本章中提到的 CSS 框架的大部分代码都与普通 CSS 兼容。换句话说,普通的 CSS 代码在 Stylus、Sass 或更少的软件中也能很好地工作。因此,在您的项目中包含这样一个 CSS 框架没有任何害处。
使用这种框架的好处很多,包括混合、变量、继承等的可用性。这些特性相当广泛,这也是它们没有被包括在本书中的原因。有关完整信息,请参考 Stylus(http://learnboost.github.io/stylus
)、Sass ( http://sass-lang.com
)和 Less ( http://lesscss.org
)的官方在线文档。
下一章提供了一些关于 Express.js 和安全性的重要提示。
1
十五、安全提示
本章中的一组技巧涉及 Express.js 应用的安全性。安全性通常是一个被忽视的话题,直到发布前的最后一刻才被提及。显然,这种事后才考虑安全性的方法容易给攻击者留下漏洞。更好的方法是从头开始考虑和实现安全问题。
浏览器 JavaScript 因安全漏洞而声名狼藉,所以我们需要尽可能保证 Node.js 应用的安全!通过本章介绍的简单修改和中间件,您可以毫不费力地解决一些基本的安全问题。
本章涵盖以下主题:
- 跨站点请求伪造(CSRF)
- 流程权限
- HTTP 安全标头
- 输入验证
跨站请求伪造
CSRF 和csurf
中间件在第四章中有简要介绍。有关 CSRF 的定义和解释,请参考该章。
csurf
中间件完成了匹配来自请求的传入值的大部分工作。然而,我们仍然需要公开响应中的值,并在模板(或 JavaScript XHRs)中将它们传递回服务器。首先,我们像安装其他依赖项一样安装csurf
模块
$ npm install csurf@1.6.0
然后,我们将csurf
与var csrf = require('csurf'); app.use()
一起应用,如第四章所述:
app.use(csrf());
csrf
必须在之前cookie-parser
和express-session
,因为它依赖于这些中间件(即安装、导入和应用必要的模块)。
实现验证的方法之一是使用定制的中间件将 CSRF 令牌传递给所有使用response.local
的模板。这个定制中间件必须在路由之前(大多数中间件语句都是这种情况):
app.use(function (request, response, next) {
response.locals.csrftoken = request.csrfToken();
next();
});
换句话说,我们手动促进了令牌在主体(如本例所示)、查询或头中的出现。(根据你的偏好或客户之间的合同,你可以使用查询或标题。)
因此,要将模板中的值呈现为隐藏的表单值,我们可以使用
input(type="hidden", name="_csrf", value="#{csrftoken}")
该hidden
输入字段将把令牌值添加到提交的表单数据中,便于将 CSRF 令牌与其他字段(如email
和password
)一起发送到/login
路线。
以下是文件ch15/index.jade
中完整的 Jade 语言内容:
doctype html
html
head
title= title
link(rel='stylesheet', href='/css/style.css')
body
if errors
each error in errors
p.error= error.msg
form(method="post", action="/login")
input(type="hidden", name="_csrf", value="#{csrftoken}")
input(type="text", name="email", placeholder="hi@webapplog.com")
input(type="password", name="password", placeholder="Password")
button(type="submit") Login
p
include lorem-ipsum
要在ch15/app.js
中观看 CSRF 的演示,像通常使用$ node app
一样启动服务器。然后导航到位于http://localhost:3000
的主页。你应该在表单的hidden
字段看到令牌,如图图 15-1 所示。请记住,您的令牌值会有所不同,但其格式是相同的。
图 15-1 。将来自csurf
模块的 CSRF 令牌插入表单,稍后发送到/登录路由
对于对主页(/
)或页面刷新的每个请求,您将获得一个新令牌。但是,如果您增加令牌来模拟攻击(您可以在 Chrome 开发人员工具中直接进行),您将得到以下错误:
403 Error: invalid csrf token
at verifytoken...
流程权限
显然,以 root 用户身份运行 web 服务通常不是一个好主意。运营开发人员可以利用 Ubuntu 的authbind
1 绑定到特权端口(例如,HTTP 的 80 和 HTTPS 的 443),而无需授予 root 访问权限。
或者,也可以在绑定到端口后取消特权。这里的想法是,我们将 GID(组 ID)和 UID(用户 ID)的值传递给 Node.js 应用,并使用解析后的值来设置流程的组标识和用户标识。这在 Windows 上不起作用,所以你可能想使用if
/ else
和process.platform
或NODE_ENV
来使你的代码跨平台。
下面是一个通过使用来自process.env.GID
和process.evn.UID
环境变量的属性设置 GID 和 UID2来删除特权的示例:
// ... Importing modules
var app = express();
// ... Configurations, middleware and routes
http.createServer(app).listen(app.get('port'), function(){
console.log("Express server listening on port "
+ app.get('port'));
process.setgid(parseInt(process.env.GID, 10));
process.setuid(parseInt(process.env.UID, 10));
});
HTTP 安全标头
叫做头盔 ( https://www.npmjs.org/package/helmet
)的 Express.js 中间件;GitHub: https://github.com/helmetjs/helmet
)是一个安全相关中间件的集合,它提供了 Recx 文章《免费提高 Web 应用安全性的七个 Web 服务器 HTTP Headers 中描述的大部分安全头。 3 撰写本文时,头盔的版本是 0.4.1,包括以下中间件:
crossdomain
:用于/crossdomain.xml
防止 Flash 加载某些不想要的内容(见http://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html
)csp
:增加内容安全策略,允许载入内容白名单(见http://content-security-policy.com
和http://www.html5rocks.com/en/tutorials/security/content-security-policy
)hidePoweredBy
:删除X-Powered-By
以防止暴露您正在使用 Node.js 和 Express.jshsts
:增加 HTTP 严格传输安全,防止您的网站被 HTTP(而不是 HTTPS)查看ienoopen
:设置 IE 8+的X-Download-Options
头,防止 IE 浏览器加载不可信的 HTML(见http://blogs.msdn.com/b/ie/archive/2008/07/02/ie8-security-part-v-comprehensive-protection.aspx
)nocache
:Cache-Control
和Pragma
头停止缓存(有助于清除用户浏览器的旧错误)nosniff
:设置适当的X-Content-Type-Options
头以减少 MIME 类型嗅探(见http://msdn.microsoft.com/en-us/library/gg622941%28v=vs.85%29.aspx
)xframe
:将X-Frame-Options
标题设置为DENY
,防止你的资源被放入框架进行点击劫持攻击(见https://en.wikipedia.org/wiki/Clickjacking
)xssFilter
:设置 IE8+和 Chrome 的X-XSS-Protection
头,防止 XSS 攻击(见http://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx
)
要安装helmet
,只需运行
$ npm install helmet@0.4.1
像往常一样导入模块:
var helmet = require('helmet');
然后在路由之前应用中间件*。默认用法如下(ch15/app.js
)😗
app.use(helmet());
图 15-2 显示了helmet
v0.4.1 HTTP 响应在使用默认选项时的样子:
图 15-2 。头盔 v0.4.1 HTTP 响应与默认选项一起使用
输入验证
当您使用 body-parser 或 query 作为输入数据时,Express.js 不执行任何用户/客户端输入清理或验证。众所周知,我们永远不应该相信输入。恶意代码可以插入(XSS 或 SQL 注入)到您的系统中。例如,当您在页面上打印该字符串时,您视为良性字符串的浏览器 JavaScript 代码可能会变成攻击(特别是如果您的模板引擎没有自动转义特殊字符的话!).
第一道防线是在接受外部数据的路由上用正则表达式手动检查数据。额外的“防御”可以添加在对象关系映射层上,比如 Mongoose 模式(见第二十二章)。请记住,前端/浏览器验证的执行仅仅是为了可用性的目的(也就是说,它更加用户友好)——它并不能保护你的网站免受任何攻击。
例如,在ch15/app.js
中,我们可以实现在email
字段、if-else
语句和test()
方法上使用 RegExp 模式的验证,将错误消息附加到errors
数组,如下所示:
app.post('/login-custom', function(request, response){
var errors = [];
var emailRegExp = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (!request.body.password) errors.push({msg: 'Password is required'});
if (!request.body.email || !emailRegExp.test(request.body.email) ) errors.push({msg: 'A valid email is required'});
if (errors)
response.render('index', {errors: errors});
else
response.render('login', {email: request.email});
});
随着您添加更多要验证的路径和输入字段,您将会得到更多的 RegExp 模式和if
/ else
语句。虽然这比没有验证要好,但是推荐的方法是编写自己的模块或者使用express-validator
??。
要安装express-validator
,请运行:
$ npm install express-validator@2.4.0
在ch15/app.js
中导入express-validator
:
var validator = require('express-validator');
然后在 body-parser
后应用express-validator
:
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(validator());
现在,在请求处理程序中,我们可以访问request.assert
和request.validationErrors()
:
app.post('/login', function(request, response){
request.assert('password', 'Password is required').notEmpty();
request.assert('email', 'A valid email is required').notEmpty().isEmail();
var errors = request.validationErrors();
if (errors)
response.render('index', {errors: errors});
else
response.render('login', {email: request.email});
});
index.jade
文件简单地打印来自数组的错误,如果有的话:
if errors
each error in errors
p.error= error.msg
并且login.jade
模板打印电子邮件。仅当验证成功时,才会呈现该模板。
p= email
要进行演示,请转到主页并尝试输入一些数据。如果有错误,会显示错误首页,如图图 15-3 所示。双重“需要有效的电子邮件”消息来自于这样一个事实,即我们对email
字段有两个断言(notEmpty
和isEmail
)并且当email
字段为空时都失败。
图 15-3 。使用 express-validator 断言表单值的错误消息
摘要
安全是最重要的,但经常被忽视。在发展的早期阶段尤其如此。典型的思考过程是这样的:让我们专注于提供更多的功能,然后当我们即将发布时,我们会考虑安全性。这个决定通常是善意的,但很少按计划进行。结果,系统的安全性受到损害。
有了中间件库,比如csurf
、helmet
和express-validator
,我们可以在不增加太多开发周期的情况下获得良好的基本安全性。
在下一章,我们将改变思路,介绍一些使用 Express.js 的方法。用于反应式(即实时更新)视图的 IO 库。
1
2
3
十六、SocketIO 和 Express.js
SocketIO ( http://socket.io
)是一个库,它提供了在客户机和服务器之间实时建立双向通信的能力。这种双向通信是由 WebSocket 技术驱动的。
WebSocket 技术在大多数现代浏览器中都可以使用,最简单的概念化方法是想象浏览器和服务器之间的持续连接,而不是传统的零星 HTTP 请求。WebSocket 和传统 HTTP 请求的另一个区别是,前者是一个双向通道,这意味着服务器可以发起数据传输。这对于实时更新页面非常有用。
WebSocket 并不是实时类系统的唯一选择。您可以使用轮询实现接近实时的系统,这是指浏览器代码在很短的时间间隔内(例如,100 毫秒)发出大量 HTTP 请求。和 SocketIO 不是唯一可以用来实现 WebSocket 的库。在 Practical Node.js (Apress,2014)中,我展示了一个使用ws
作为服务器库和一个本地(根本没有库)浏览器 API 的例子。但是,使用 Socket 的好处。IO 在于它具有广泛的、跨平台的、跨浏览器的支持;WebSocket 不可用时回退到轮询;并使用事件作为它的主要实现模式。
插座全覆盖。IO 图书馆值得拥有自己的书。然而,用 Express.js 开始使用是如此的酷和容易,以至于我包含了这一章来向你展示一个基本的例子。
使用插座。超正析象管(Image Orthicon)
该示例将实时反向回显(浏览器到服务器,然后返回)我们的输入。这意味着我们将构建一个 Express.js 服务器,其中包含一个包含表单输入字段的 web 页面。该网页还将有一些前端 JavaScript 代码,在我们键入时将输入字段字符实时传输到服务器。服务器将反转字符串并将其发送回浏览器,我们的前端 JavaScript 代码将最终字符串打印到浏览器控制台。我们将使用插座。所有这些功能的 IO 方法和事件监听器。
在最终产品中,如果您键入“!stekcoS,”应用会将其转换为“插座!”如图图 16-1 所示。并且将显示浏览器控制台输出
!
sending ! to server
received !
!
sending !
received !
!s
sending !s to server
received s!
...
图 16-1 。的投入!stekcoS 出品插座!
可以从一个$ express socket
创建的 fresh Express.js app 开始,也可以从ch16/socket
文件夹下载源代码。然后,用$ cd socket && npm install
安装依赖项。
提示如果需要 Express.js 3.x 的例子,可以参考这个 app 为 Express.js 3.3.5 编写的版本。源代码在 GitHub repo:
https://github.com/azat-co/proexpressjs/tree/master/ch16/socket-express3
。
要包含 Socket.io,我们可以使用:
$ npm install socket.io@1.1.0 --save
或者,我们可以使用package.json
:
{
"name": "socket-app",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node app.js"
},
"dependencies": {
"errorhandler": "1.1.1",
"express": "4.8.1",
"jade": "1.5.0",
"morgan": "1.2.2",
"serve-favicon": "2.0.1",
"socket.io": "1.1.0"
}
}
插座。从某种意义上来说,IO 可以被认为是另一个服务器。我们可以通过将 Express.js app
对象传递给createServer()
方法,然后调用套接字来重构自动生成的 Express.js 代码。木卫一listen()
方法上server
物体:
var server = http.createServer(app);
var io = require('socket.io').listen(server);
//...
server.listen(app.get('port'), function(){
console.log('Express server listening on port '
+ app.get('port'));
});
在io
对象被实例化后,下面的代码可以用来建立一个连接:
io.sockets.on('connection', function (socket) {
一旦建立了连接——我们知道这是因为我们在回调中——我们附加了messageChange
事件监听器,它将由浏览器上的用户操作触发:
socket.on('messageChange', function (data) {
console.log(data);
一旦消息被更改(例如,用户输入了一些内容),我们可以反转字符串并以带有socket.emit():
的receive
事件的形式将其发送回来
socket.emit('receive',
data.message.split('').reverse().join('') );
})
});
下面是ch16/socket/app.js
的全部内容:
var http = require('http'),
express = require('express'),
path = require('path'),
logger = require('morgan'),
favicon = require('serve-favicon'),
errorhandler = require('errorhandler'),
bodyParser = require('body-parser');
var app = express();
app.set('view engine', 'jade');
app.set('port', process.env.PORT || 3000);
app.use(logger('combined'));
app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(express.static('public'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.get('/', function(request, response){
response.render('index');
});
app.use(errorhandler());
var server = http.createServer(app);
var io = require('socket.io').listen(server);
io.sockets.on('connection', function (socket) {
socket.on('messageChange', function (data) {
console.log(data);
socket.emit('receive', data.message.split('').reverse().join('') );
});
});
server.listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
最后,我们的应用在index.jade
中需要一些前端程序,以输入框和 JavaScript 代码的形式发送和接收用户输入。输入框可以这样实现:
input(type='text', class='message', placeholder='what is on your mind?', onkeyup='send(this)')
在每个按键事件中,它都调用send()
函数,但是在我们编写这个函数之前,让我们包含这个套接字。IO 库:
script(src="/socket.io/socket.io.js")
socket.io.js
文件不是您需要下载的真实文件,例如 jQuery 文件。Node.js 套接字。IO 服务器将“自动”提供这个文件
现在,我们可以连接到 socket 服务器并附加receive
事件监听器:
var socket = io.connect('http://localhost');
socket.on('receive', function (message) {
console.log('received %s', message);
document.querySelector('.received-message').innerText = message;
});
document.querySelector
只是现代浏览器对 jQuery 选择器的模拟。它只是给了我们一个 HTML 元素,这样我们就不需要依赖 jQuery 了。我们使用innerText
属性在页面上显示新的文本。
send
函数发出将消息传递给服务器的事件messageChange
。这种情况在每次击键时都会发生,因此变化会实时出现。
var send = function(input) {
console.log(input.value)
var value = input.value;
console.log('sending %s to server', value);
socket.emit('messageChange', {message: value});
}
以下是ch16/socket/index.jade
的完整源代码:
extends layout
block content
h1= title
p Welcome to
span.received-message #{title}
input(type='text', class='message', placeholder='what is on your mind?', onkeyup='send(this)')
script(src="/socket.io/socket.io.js")
script.
var socket = io.connect('http://localhost');
socket.on('receive', function (message) {
console.log('received %s', message);
document.querySelector('.received-message').innerText = message;
});
var send = function(input) {
console.log(input.value)
var value = input.value;
console.log('sending %s to server', value);
socket.emit('messageChange', {message: value});
}
运行应用
现在一切都应该准备好了,你可以启动应用了($ node app
)。转到主页(http://localhost:3000
)输入“!stekcoS”,应用会将其转换为“插座!”如前图 16-1 所示。
当您查看浏览器输出时,您可能会认为代码只是在客户端进行了转换,而没有服务器的参与。事实并非如此,证据在服务器日志中,你会看到转换实际上发生在服务器上(见图 16-2 )。
图 16-2 。Express.js 服务器实时捕捉和处理输入
更多 SocketIO 示例,转到http://socket.io/#how-to-use
。
摘要
同样,您已经看到了 Express.js 与另一个库的无缝集成。在这个双通道通信的简短示例中,我们使用了来自套接字的事件侦听器。IO 库。该库兼容 Node.js 和浏览器 JavaScript,这使得它更易于使用。SocketIO 为实时应用提供了良好的跨浏览器支持。它与 Express.js 堆栈无缝集成,与使用 WebSocket 的原生浏览器 API 相比具有许多优势。
在下一章,我们将讨论如何使用域来更好地处理异步错误。