回顾一下,Express.js 应用的典型结构(通常是一个server.js或app.js文件)大致由这些部分组成,顺序如下:
- 依赖关系:导入依赖关系的一组语句
- 实例化:创建对象的一组语句
- 配置:配置系统和自定义设置的一组语句
- 中间件:为每个传入请求执行的一组语句
- Routes :定义服务器路由、端点和页面的一组语句
- Bootup :一组启动服务器并让它在特定端口监听传入请求的语句
本章包括第五类,路由和我们在路由中定义的 URL 参数。这些参数以及 app.param()中间件是必不可少的,因为它们允许应用访问 URL 中从客户端传递的信息(例如,books/proexpressjs)。这是 REST APIs 最常见的约定。例如,http://hackhall.com/api/posts/521eb002d00c970200000003路由将使用 521 EB 002d 00 c 9702000000003 的值作为帖子 ID。
参数是在请求的 URL 的查询字符串中传递的值。如果我们没有 Express.js 或类似的库,只能使用核心的 Node.js 模块,我们就必须通过某种require('querystring').parse(url)或require('url').parse(url, true)函数“诡计”从HTTP.request ( http://nodejs.org/api/http.html#http_http_request_options_callback)对象中提取参数
让我们仔细看看如何为特定的 URL 参数定义特定的规则或逻辑。
参数
从 URL 中提取参数的第一种方法是在请求处理程序(route)中编写一些代码。如果您需要在其他路由中重复这个片段,您可以抽象代码并手动将相同的逻辑应用到许多路由。(To abstract code 的意思是重构代码,以便可以在其他地方重用和/或更好地组织。这提高了代码的可维护性和可读性。)
例如,假设我们需要用户资料页面(/v1/users/azat定义为/v1/users/:username)和管理页面(/v1/admin/azat定义为/v1/admin/:username)上的用户信息。一种方法是定义一个查找用户信息的函数(findUserByUsername),并在每条路线中调用这个函数两次。这是我们实现它的方式(示例ch6/app.js):
var users = {
'azat': {
email: 'hi@azat.co',
website: 'http://azat.co',
blog: 'http://webapplog.com'
}
};
var findUserByUsername = function (username, callback) {
// Perform database query that calls callback when it's done
// This is our fake database
if (!users[username])
return callback(new Error(
'No user matching '
+ username
)
);
return callback(null, users[username]);
};
app.get('/v1/users/:username', function(request, response, next) {
var username = request.params.username;
findUserByUsername(username, function(error, user) {
if (error) return next(error);
return response.render('user', user);
});
});
app.get('/v1/admin/:username', function(request, response, next) {
var username = request.params.username;
findUserByUsername(username, function(error, user) {
if (error) return next(error);
return response.render('admin', user);
});
});您可以使用$ node app命令运行 ch6 文件夹中的应用。然后,打开一个新的终端选项卡/窗口,并使用以下内容来处理 GET 请求:
$ curl http://localhost:3000/v1/users/azatTo see this:
user profile</h2><p>http://azat.co</p><p>http://webapplog.com</p>并且随着
$ curl http://localhost:3000/v1/admin/azat要看这个:
admin: user profile</h2><p>hi@azat.co</p><p>http://azat.co</p><p>http://webapplog.com</p><div><Practical>Node.js is your step-by-step guide to learning how to build scalable real-world web applications, taking you from installing Express.js to writing full-stack web applications with powerful libraries such as Mongoskin, Everyauth, Mongoose, Socket.IO, Handlebars, and everything in between.</Practical></div>
注意 Windows 用户可以从http://curl.haxx.se/download.html .下载 CURL
或者,你可以在http://bit.ly/JGSQwr使用 Postman Chrome 扩展。或者,对于 GET 请求,您可以使用浏览器——只需转到 URL。浏览器不会发出上传或删除请求,只有当您提交表单时,它才会发出发布请求。
最后一种方法是使用 jQuery 发出 AJAX/XHR 请求,但是要注意跨源限制,这意味着在服务器上使用相同的域或 CORS 头。或者你可以在你的浏览器中简单地进入http://localhost:3000/v1/users/azat(见图 6-1 )和http://localhost:3000/v1/admin/azat(见图 6-2 )。
图 6-1 。用户名 URL 参数被解析并用于查找用户页面上显示的信息(例如 ch6)
图 6-2 。用户名 URL 参数被解析并用于查找显示在管理页面上的信息(例如 ch6)
admin.jade模板 ( 图 6-2 )与user.jade ( 图 6-1 )的内容略有不同,以帮助您区分这两个页面/路径,因此您可以确保它们都能正确解析和使用参数。
即使在将大部分代码抽象成findUserByUsername()函数 之后,我们仍然以笨拙的代码结束。如果我们使用中间件方法,代码会变得稍微好一点。想法是编写一个定制的中间件 findUserByUsernameMiddleware,并将其用于需要用户信息的每个路由。下面是如何重构相同的两条路由并使用/v2前缀(前缀通常用于区分 REST API 版本):
var findUserByUsername = function (username, callback) {
// Perform database query that calls callback when it's done
// This is our fake database!
if (!users[username])
return callback(new Error(
'No user matching '
+ username
)
);
return callback(null, users[username]);
};
var findUserByUsernameMiddleware = function(request, response, next){
if (request.params.username) {
console.log('Username param was detected: ', request.params.username)
findUserByUsername(request.params.username, function(error, user){
if (error) return next(error);
request.user = user;
return next();
})
} else {
return next();
}
}
// The v2 routes that use the custom middleware
app.get('/v2/users/:username',
findUserByUsernameMiddleware,
function(request, response, next){
return response.render('user', request.user);
});
app.get('/v2/admin/:username',
findUserByUsernameMiddleware,
function(request, response, next){
return response.render('admin', request.user);
});中间件 findUserByUsernameMiddleware 检查参数(request.params.username)是否存在,如果存在,则继续获取信息。这是一个更好的模式,因为它保持了路由的精简和逻辑的抽象。然而,Express.js 有一个更好的解决方案。它类似于中间件方法,但是它通过自动执行参数存在检查(即检查参数是否在请求中)使我们的生活变得更简单。遇见app.param()法!
app.param()
只要给定的字符串(例如,username)出现在路由的 URL 模式中,并且服务器接收到与该路由匹配的请求,就会触发对app.param()的回调。例如,使用app.param('username', function(req, res, next, username){...})和app.get('/users/:username', findUser)时,每次我们有一个请求/username/azat或/username/tjholowaychuk,就会执行app.param()中的关闭(在findUser之前)。
app.param()方法与app.use()非常相似,但是它提供值(在我们的例子中是username)作为函数的第四个,也是最后一个参数。在这个代码片段中,用户名将具有来自 URL 的值(例如,'azat'代表/users/azat):
app.param('username', function (request, response, next, username) {
*// ... Perform database query and*
*// ... Store the user object from the database in the req object*
req.user = user;
return next();
});不需要额外的代码行,因为我们有由app.param()填充的req.user对象:
app.get('/users/:username', function(request, response, next) {
*//... Do something with req.user*
return res.render(req.user);
});这条路线也不需要额外的代码。我们免费得到req.user,因为前面定义了app.param():
app.get('/admin/:username', function(request, response, next) {
*//... Same thing, req.user is available!*
return res.render(user);
});下面是我们如何将 param 中间件插入我们的应用的另一个例子:
app.param('id', function(request, response, next, id){
*// Do something with id*
*// Store id or other info in req object*
*// Call next when done*
next();
});
app.get('/api/v1/stories/:id', function(request, response){
*// Param middleware will be executed before and*
*// We expect req objects to already have needed info*
*// Output something*
res.send(data);
});
提示如果你有一个大型应用,有很多版本的 API 和 routes (v1、v2 等。),那么最好用Router类/对象来组织这些路由的代码。您创建一个Router对象,并将其挂载到一个路径上,比如/api或/api/v1。路由只是var app = express()对象的精简版。关于Router类的更多细节将在本章后面提供。
下面是一个将 param 中间件插入到一个应用中的例子,该应用在req.db中有一个 Mongoskin/Monk 类型的数据库连接:
app.param('id', function(request, response, next, id){
req.db.get('stories').findOne({_id: id}, function (error, story){
if (error) return next(error);
if (!story) return next(new Error('Nothing is found'));
req.story = story;
next();
});
});
app.get('/api/v1/stories/:id', function(request, response){
res.send(req.story);
});或者,我们可以使用多个请求处理程序,但概念保持不变:我们可以预期在执行这段代码之前会抛出一个req.story对象或错误,因此我们抽象出获取参数及其各自对象的公共代码/逻辑。这里有一个例子:
app.get('/api/v1/stories/:id', function(request, response, next) {
*//do authorization*
},
*//we have an object in req.story so no work is needed here*
function(request, response) {
*//output the result of the database search*
res.send(story);
});
注意授权和输入卫生是驻留在中间件中的很好的候选者。关于 OAuth 和 Express.js 的广泛示例,请参考实用 node . js1(Apress,2014)。
param()函数特别酷,因为我们可以在路线中组合不同的变量;例如:
app.param('storyId', function(request, response, next, storyId) {
*// Fetch the story by its ID (storyId) from a database*
*// Save the found story object into request object*
*request.story = story;*
});
app.param('elementId', function(request, response, next, elementId) {
*// Fetch the element by its ID (elementId) from a database*
*// Narrow down the search when request.story is provided*
*// Save the found element object into request object*
*request.element = element;*
});
app.get('/api/v1/stories/:storyId/elements/:elementId', function(request, response){
// Now we automatically get the story and element in the request object
res.send({ story: request.story, element: request.element});
});
app.post('/api/v1/stories/:storyId/elements', function(request, response){
// Now we automatically get the story in the request object
// We use story ID to create a new element for that story
res.send({ story: request.story, element: newElement});
});总之,通过定义 app.param 一次,它的逻辑将为具有匹配 URL 参数名称的每个路由触发。您可能想知道,“它与编写自己的函数并调用它,或者与编写自己的定制中间件有什么不同?”它们都可以正确地执行代码,但是 param 是一种更好的方法。我们可以重构我们之前的例子来展示不同之处。
让我们回到ch6项目。如果我们重构前面来自ch6/app.js的示例,并使用v3作为新的路由前缀,我们可能会得到如下优雅的代码:
app.param('v3Username', function(request, response, next, username){
console.log(
'Username param was is detected: ',
username
)
findUserByUsername(
username,
function(error, user){
if (error) return next(error);
request.user = user;
return next();
}
);
});
app.get('/v3/users/:v3Username',
function(request, response, next){
return response.render('user', request.user);
}
);
app.get('/v3/admin/:v3Username',
function(request, response, next){
return response.render('admin', request.user);
}
);因此,提取参数很重要,但定义路线更重要。定义路由也是使用app.param()从 URL 参数中提取值的一种替代方法——当一个参数只使用一次时,推荐使用这种方法。如果不止一次使用,param 是更好的模式。
在前五章中已经定义了许多路线。在下一节中,我们将更详细地探索如何定义各种 HTTP 方法,链中间件,抽象中间件代码,以及定义所有方法路由。
路由
Express.js 是一个 Node.js 框架,它提供了一种将路由组织成更小的子部分(路由—Router类/对象的实例)的方法。在 Express.js 3.x 和更早的版本中,定义路由的唯一方式是使用app.VERB()模式,我们将在接下来介绍。然而,从 Express.js v4.x 开始,使用新的Router类是推荐的通过router.route(path)定义路线的方式。我们将首先介绍传统方法。
app。动词()
每个路由都是通过一个应用对象上的方法调用定义的,第一个参数是 URL 模式(也支持正则表达式2);也就是app.METHOD(path, [callback...], callback)。
例如,要定义一个 GET /api/v1/stories端点:
app.get('/api/v1/stories/', function(request, response){
// ...
})或者,为 POST HTTP 方法和相同的路由定义一个端点:
app.post('/api/v1/stories', function(request, response){
// ...
})也支持 DELETE、PUT 和其他方法。更多信息,参见http://expressjs.com/api.html#app.VERB。
我们传递给get()或post()方法的回调被称为请求处理程序(在第 7 章中有详细介绍),因为它们接受请求(req),处理请求,并写入响应(res)对象。例如:
app.get('/about', function(request, response){
res.send('About Us: ...');
});我们可以在一个路由中有多个请求处理器。除了第一个和最后一个之外,它们都将处于流程的中间(它们被执行的顺序),因此得名中间件。它们接受第三个参数/函数next,当被调用时(next(),将执行流切换到下一个处理程序。例如,我们有三个执行授权、数据库搜索和输出的功能:
app.get('/api/v1/stories/:id', function(request, response, next) {
*// Do authorization*
*// If not authorized or there is an error*
*// Return next(error);*
*// If authorized and no errors*
return next();
}), function(request, response, next) {
*// Extract id and fetch the object from the database*
*// Assuming no errors, save story in the request object*
request.story = story;
return next();
}), function(request, response) {
*// Output the result of the database search*
res.send(response.story);
});名称next()是一个任意的约定,这意味着您可以使用任何您喜欢的名称来代替next()。Express.js 使用函数中参数的顺序来确定它们的含义。故事的 ID 是 URL 参数,我们需要它在数据库中查找匹配的条目。
现在,如果我们有另一条路线/admin呢?我们可以定义多个请求处理程序,它们执行资源的认证、验证和加载:
app.get('/admin',
function(request, response, next) {
*// Check active session, i.e.,*
*// Make sure the request has cookies associated with a valid user session*
*// Check if the user has administrator privileges*
return next();
}, function(request, response, next){
*// Load the information required for admin dashboard*
*// Such as user list, preferences, sensitive info*
return next();
}, function(request, response) {
*// Render the information with proper templates*
*// Finish response with a proper status*
res.end();
})但是如果/admin的一些代码,比如授权/认证,是从/stories复制过来的呢?下面的代码完成了同样的事情,但是通过使用命名函数,更加简洁:
var auth = function (request, response, next) {
// ... Authorization and authentication
return next();
}
var getStory = function (request, response, next) {
// ... Database request for story
return next();
}
var getUsers = function (request, response, next) {
// ... Database request for users
return next();
}
var renderPage = function (request, response) {
if (req.story) res.render('story', story);
else if (req.users) res.render('users', users);
else res.end();
}
app.get('/api/v1/stories/:id', auth, getStory, renderPage);
app.get('/admin', auth, getUsers, renderPage);另一个有用的技术是将回调作为数组的项来传递,这得益于arguments JavaScript 机制的内部工作方式: 3
var authAdmin = function (request, response, next) {
// ...
return next();
}
var getUsers = function (request, response, next) {
// ...
return next();
}
var renderUsers = function (request, response) {
// ...
res.end();
}
var admin = [authAdmin, getUsers, renderUsers];
app.get('/admin', admin);路由和中间件中的请求处理程序之间的一个明显区别是,我们可以通过调用next('route');来绕过链中的其余回调。如果在前面使用/admin路由的例子中,请求在第一次回调中认证失败,这可能会很方便,在这种情况下没有必要继续。如果有多条路线匹配同一个 URL,您还可以使用next()跳转到下一条路线。
请注意,如果我们传递给app.VERB()的第一个参数包含查询字符串(例如/?debug=true,Express.js 将忽略该信息。例如,app.get('/?debug=true', routes.index);将被完全视为app.get('/', routes.index);。
以下是最常用的表述性状态转移(REST) 服务器架构 HTTP 方法及其在 Express.js 中的对应方法以及简要含义:
- GET:
app.get()—检索实体或实体列表 - HEAD:
app.head()—与 GET 相同,只是没有主体 - 发布:
app.post()—提交新实体 - PUT:
app.put()—通过完全替换来更新实体 - 补丁:
app.patch()—部分更新实体 - 删除:
app.delete()和app.del()—删除现有实体 - 选项:
app.options()—检索服务器的功能
提示HTTP 方法是每个 HTTP(S)请求的特殊属性,类似于它的头或主体。在浏览器中打开 URL 是 GET 请求,提交表单是 POST 请求。其他类型的请求,如 PUT、DELETE、PATCH 和 OPTIONS,只能通过 CURL、Postman 或定制的应用(前端和后端)等特殊客户端获得。
有关 HTTP 方法的更多信息,请参考 RFC 2616 ( http://tools.ietf.org/html/rfc2616)及其“方法定义”部分(第 9 节)。
app.all()
app.all()方法允许在特定路径上执行指定的请求处理程序,而不管请求的 HTTP 方法是什么。在定义全局或名称空间逻辑时,这个过程可能是救命稻草,如下例所示:
app.all('*', userAuth);
...
app.all('/api/*', apiAuth);尾随斜线
默认情况下,结尾带有斜杠的路径被视为与正常路径相同。要关闭此功能,请使用app.enable('strict routing');或app.set('strict routing', true);。你可以在第三章中了解关于设置选项的更多信息。
路由类别
Router类是一个只有中间件和路由的 mini Express.js 应用。这对于根据它们执行的业务逻辑抽象某些模块很有用。例如,所有的/users/*路由可以在一个路由中定义,而所有的/posts/*路由可以在另一个路由中定义。好处是,在我们用router.path()在路由中定义了 URL 的一部分之后(见下一节),我们不需要一遍又一遍地重复它,就像使用app.VERB()方法一样。
以下是创建路由实例的示例:
var express = require('express');
var router = express.Router(options);
// ... Define routes
app.use('/blog', router);其中options是可以具有以下属性的对象:
caseSensitive: Boolean,表示是否将名称相同但字母大小写不同的路由视为不同,默认为false;例如,如果设置为false,那么/Users与/users相同。strict: Boolean,表示是否将名称相同但尾部有无斜杠的路由视为不同,默认为false;例如,如果设置为false,那么/users与/users/相同。
router.route(路径)
router.route(path)方法用于链接 HTTP 动词方法。例如,在一个创建、读取、更新和删除(CRUD) 服务器中,对于/posts/:id URL(例如/posts/53fb401dc96c1caa7b78bbdb)有 POST、GET、PUT 和 delete 端点,我们可以如下使用Router类:
var express = require('express');
var router = express.Router();
// ... Importations and configurations
router.param('postId', function(request, response, next) {
// Find post by ID
// Save post to request
request.post = {
name: 'PHP vs. Node.js',
url: 'http://webapplog.com/php-vs-node-js'
};
return next();
});
router
.route('/posts/:postId')
.all(function(request, response, next){
// This will be called for request with any HTTP method
})
.post(function(request, response, next){
})
.get(function(request, response, next){
response.json(request.post);
})
.put(function(request, response, next){
// ... Update the post
response.json(request.post);
})
.delete(function(request, response, next){
// ... Delete the post
response.json({'message': 'ok'});
})Router.route(path)方法提供了链接方法的便利,这是一种比为每条路线重新键入router更有吸引力的结构化代码的方式。
或者,我们可以使用router.VERB(path, [callback...], callback)来定义路线,就像我们使用app.VERB()一样。同样,router.use()和router.param()方法的工作原理与app.use()和app.param()相同。
回到我们的示例项目(在ch6文件夹中),我们可以用Router实现v4/users/:username和v4/admin/:username:
router.param('username', function(request, response, next, username){
console.log(
'Username param was detected: ',
username
)
findUserByUsername(
username,
function(error, user){
if (error) return next(error);
request.user = user;
return next();
}
);
})
router.get('/users/:username',
function(request, response, next){
return response.render('user', request.user);
}
);
router.get('/admin/:username',
function(request, response, next){
return response.render('admin', request.user);
}
);
app.use('/v4', router);如您所见,router.get()方法没有提到v4。通常,router.get()和router.param()方法被抽象成一个单独的文件。这样,主文件(在我们的例子中是app.js)保持精简,易于阅读和维护——这是一个很好的遵循原则!
请求处理程序
Express.js 中的请求处理程序与核心 Node.js http.createServer()方法中的回调惊人地相似,因为它们只是带有req和res参数的函数(匿名、命名或方法):
var ping = function(req, res) {
console.log('ping');
res.end(200);
};
app.get('/', ping);此外,我们可以利用第三个参数next()来控制流程。这与错误处理的主题密切相关,错误处理将在第 9 章的中介绍。下面是两个请求处理程序的简单例子,ping和pong,其中前者在打印一个单词 ping 后跳到后者:
var ping = function(req, res, next) {
console.log('ping');
return next();
};
var pong = function(req, res) {
console.log('pong');
res.end(200);
};
app.get('/', ping, pong);当请求出现在/路线上时,Express.js 调用ping(),在这种情况下它充当中间件(因为它在中间!).Ping 完成后,用res.end()调用 pong 完成响应。
return关键词也很重要。例如,如果在第一个中间件中认证失败,我们不想继续处理请求:
*// Instantiate app and configure error handling*
*// Authentication middleware*
var checkUserIsAdmin = function (req, res, next) {
if (req.session && req.session._admin !== true) {
return next (401);
}
return next();
};
*// Admin route that fetches users and calls render function*
var admin = {
main: function (req, res, next) {
req.db.get('users').find({}, function(e, users) {
if (e) return next(e);
if (!users) return next(new Error('No users to display.'));
res.render('admin/index.html', users);
});
}
};
*// Display list of users for admin dashboard*
app.get('/admin', checkUserIsAdmin, admin.main);关键字return是必不可少的,因为如果我们不在next(e)调用中使用它,即使有错误和/或我们没有任何用户,应用也会试图呈现(res.render())。例如,以下可能是一个坏主意,因为在我们调用next()之后,这将在错误处理程序中触发适当的错误,流程继续并试图呈现页面:
var admin = {
main: function (req, res, next) {
req.db.get('users').find({}, function(e, users) {
if (e) next(e);
if (!users) next(new Error('No users to display.'));
res.render('admin/index.html', users);
});
}
};我们应该使用这样的东西:
if (!users) return next(new Error('No users to display.'));
res.render('admin/index.html', users);或者类似这样的东西:
if (!users)
return next(new Error('No users to display.'));
else
res.render('admin/index.html', users);摘要
在本章中,我们介绍了 Express.js 应用典型结构的两个主要方面:定义路线和提取 URL 参数。我们探索了如何将它们从 URL 中取出并在请求处理程序中使用它们的三种不同方式(req.params、定制中间件和app.param())。您了解了如何为各种 HTTP 方法定义路由。最后,我们深入研究了充当 mini Express.js 应用的Router类,并使用Router类为示例项目实现了另一组路由。
每次我们定义路由(或中间件)时,我们都在回调中使用匿名函数定义或命名函数来定义请求处理程序。请求处理器通常有三个参数:request(或req)、response(或res)和next。在下一章中,您将了解更多关于这些对象的内容,以及在 Express.js 中,它们与核心 Node.js http模块的request和response有何不同。了解这些差异将为您提供更多的特性和功能!
1T0】
2T0】
3 参见https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments

