3. 路由解析
前文提及了很多Web请求的预处理过程,对于不同的业务,我们还是期望有不同的处理方式,这带来了路由的选择问题。
(1)文件路径型
1. 静态文件
这种方式的路由在路径解析的部分有过简单的描述,其让人舒服的地方在于URL的路径与网站目录的路径一致,无需转换,非常直观。这种路由的处理方式也十分简单,将请求对应的文件发送给客户端即可。这在前文路劲解析部分有过介绍,不再重复。
2. 动态文件
在MVC模式流行起来之前,根据文件的路径执行动态脚本也是基本的路由方式,它的处理原则是Web服务器根据URL路径找到对应的文件, 比如/index.php或者/index.asp。Web服务器根据文件名后缀去寻找脚本的解析器,并传入HTTP请求的上下文。
解析器执行脚本,并输出响应报文,达到完成服务的目的。现在大多数的服务器都能很明智地根据后缀同时服务动态或静态文件,这种方式不太适用于node,主要原因就是文件的后缀名都是js ,分不清是后端脚本还是前端脚本,这可不是什么 好的设计,而且node中的Web服务器与对应业务脚本是一体的,无需按照这种方式实现。
(2)MVC(重点)
在MVC流行之前,主流的处理方式就是通过文件的路径进行处理,甚至以为是常态。直到有一天开发者发现用户请求的URL路径原来可以跟具体脚本所在的路径没有任何的关系。
MVC模型的主要思想是将业务逻辑按照职责分离,主要分为以下几种:
- 控制器(Controller)(C层),一组行为的集合。
- 模型(Model)(M层),数据相关的操作和封装。
- 试图(View)(V层),视图的渲染。
这是目前最为经典的分层模式,大致而言,它的工作模式就是:
- 路由的解析,根据URL寻找对应的控制器和行为。
- 行为调用相关的模型,进行数据操作。
- 数据操作结束后,调用试图与相关数据进行页面渲染,输出到客户端。
如何根据URL做路由映射,这里有两个分支实现。一、手工关联映射;二、自然关联映射。前者会有一个对应的路由文件将URL映射到对应的控制器,后者没有这样的文件。
路由的映射(接口的实现)
先来解释一下什么是路由映射:
1. 首先我们要理解MVC中路由的作用:url Routing的作用是将浏览器的URL请求映射到特定的MVC控制器动作。
2. 当我们访问http://localhost:8080/Home/Index 这个地址的时候,请求首先被UrlRoutingModule截获,截获请求后,从Routes中得到与当前请求URL相符合的RouteData对象,将RouteData对象和当前URL封装成一个RequestContext对象,然后从RequestContext封装的RouteData中得到 Controller名字,根据Controller的名字,通过反射创建控制器对象,这个时候控制器才真正被激活,最后去执行控制器里面对应的 Action。
路由处理流程:
(1)手动映射
- 手动映射的实现
具体代码:
var routes = []; // 生成的router 所保存的位置
/*****路由配置文件,(可以用单独的模块配置然后require),这就是所谓的接口配置*****/
var http = require('http');
var url = require('url');
var server = http.createServer();
/* 模拟路径,将url路径映射到对应的action中 */
var routes_config = [
{
path: "/user/get",
ctrl: (req, res) => res.end("path=user/get")
},
{
path: "/user/post",
ctrl: (req, res) => res.end("path=user/post")
}
];
// 设置路由方法
function setRoutes(path, action) {
routes.push([path, action])
}
routes_config.forEach((item, index) => {
setRoutes(item.path, item.ctrl);
})
server.on('request', function (req, res) {
var pathname = url.parse(req.url).pathname;
for (var i = 0; i < routes.length; i++) {
var route = routes[i];
if (route[0] == pathname) {
var action = route[1];
action(req, res);
return;
}
}
function res404(req, res) {
res.end("404");
}
// 错误路由处理
res404(req, res);
}).listen(8000,()=>{
console.log('server is create')
})
手工映射十分方便,由于它对URL十分灵活,按照业务需求可以在routes_config配置更多的路由,以更好地实现任务。
不过通常我们会将文件分开成两部分以便于操作:
router.js:
var routes = []; // 生成的router 所保存的位置
/*****路由配置文件,(可以用单独的模块配置然后require),这就是所谓的接口配置*****/
var routes_config = [
{
path: "/user/get",
ctrl: (req, res) => res.end("path=user/get")
},
{
path: "/user/post",
ctrl: (req, res) => res.end("path=user/post")
}
];
// 设置路由方法
function setRoutes(path, action) {
routes.push([path, action])
}
routes_config.forEach((item, index) => {
setRoutes(item.path, item.ctrl);
})
module.exports.routes = routes;
luyou.js:
var r = require('./router');
var http = require('http');
var url = require('url');
var server = http.createServer();
server.on('request', function (req, res) {
var pathname = url.parse(req.url).pathname;
for (var i = 0; i < r.routes.length; i++) {
var route = r.routes[i];
if (route[0] == pathname) {
var action = route[1];
action(req, res);
return;
}
}
function res404(req, res) {
res.end("404");
}
// 错误路由处理
res404(req, res);
}).listen(8000,()=>{
console.log('server is create')
})
对于简单的路径采用上面的方法是可以解决问题的,但是路径一旦复杂上述配置映射就显得和吃力。
所以采用正则来进行匹配
zzrouter.js:
var routes = []; // 生成的router 所保存的位置
/*****路由配置文件,(可以用单独的模块配置然后require),这就是所谓的接口配置*****/
var routes_config = [
{
path: "/user/:username/id/:id",
ctrl: (req, res) => {
console.log(req.url)
res.end()
// ...... ToDo
}
},
{
path: "/user.:ext",
ctrl: (req, res) => {
console.log(req.url)
res.end()
// ...... ToDo
}
}
];
var pathRegexp = function (path) {
let strict = path
path = path
.concat(strict ? '' : '/?')
.replace(/\/\(/g, '(?:/')
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?(\*)?/g, function (_, slash, format, key, capture,
optional, star) {
slash = slash || '';
return ''
+ (optional ? '' : slash)
+ '(?:'
+ (optional ? slash : '')
+ (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')'
+ (optional || '')
+ (star ? '(/*)?' : '');
})
.replace(/([\/.])/g, '\\$1')
.replace(/\*/g, '(.*)');
return new RegExp('^' + path + '$');
}
// 设置路由方法
function setRoutes(path, action) {
routes.push([path, action])
}
routes_config.forEach((item, index) => {
setRoutes(pathRegexp(item.path), item.ctrl);
})
module.exports.routes = routes;
zzluyou.js:
var r = require('./zzrouter');
var http = require('http');
var url = require('url');
var server = http.createServer();
server.on('request', function (req, res) {
var pathname = url.parse(req.url).pathname;
for (var i = 0; i < r.routes.length; i++) {
var route = r.routes[i];
if (route[0].exec(pathname)) {
var action = route[1];
action(req, res);
return;
}
}
function res404(req, res) {
res.end("404");
}
// 错误路由处理
res404(req, res);
}).listen(8000, () => {
console.log('server is create')
})
结果:
- 参数解析
尽管完成了正则匹配,可以实现相似URL的匹配,但是:username到底匹配了啥,还没有解决。为此我们还需要进一步将匹配到的内容抽取出来,希望在业务中能如下这样调用:
{
path: "/user/:username",
ctrl: (req, res) => {
var username = req.params.username;
console.log(username)
res.end()
// ...... ToDo
}
},
这里的目标是将抽取的内容设置到req.params处。那么第一步就是将键值抽取出来,如下所示:
修改 zzrouter.js:
module.exports.routes = routes;
var routes = []; // 生成的router 所保存的位置
/*****路由配置文件,(可以用单独的模块配置然后require),这就是所谓的接口配置*****/
var routes_config = [
{
path: "/user/:username/id/:id",
ctrl: (req, res) => {
var pa = req.params;
console.log(pa) //=> { username: 'username', id: '1' }
res.end()
// ...... ToDo
}
},
{
path: "/user.:ext",
ctrl: (req, res) => {
var username = req.params.username;
console.log(username)
res.end()
// ...... ToDo
}
}
];
var pathRegexp = function (path) {
let strict = path
const keys = [];
path = path
.concat(strict ? '' : '/?')
.replace(/\/\(/g, '(?:/')
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?(\*)?/g, function (_, slash, format, key, capture, optional, star) {
// 将匹配到的键值保存起来
keys.push(key);
slash = slash || '';
return ''
+ (optional ? '' : slash)
+ '(?:'
+ (optional ? slash : '')
+ (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')'
+ (optional || '')
+ (star ? '(/*)?' : '');
})
.replace(/([\/.])/g, '\\$1')
.replace(/\*/g, '(.*)');
return {
keys: keys,
regexp: new RegExp('^' + path + '$')
};
}
// 设置路由方法
function setRoutes(path, action) {
routes.push([path, action])
}
routes_config.forEach((item, index) => {
setRoutes(pathRegexp(item.path), item.ctrl);
})
module.exports.routes = routes;
修改 zzluyou.js:
var r = require('./zzrouter');
var http = require('http');
var url = require('url');
var server = http.createServer();
server.on('request', function (req, res) {
var pathname = url.parse(req.url).pathname;
for (var i = 0; i < r.routes.length; i++) {
var route = r.routes[i];
// 正则匹配
var reg = route[0].regexp;
var keys = route[0].keys;
var matched = reg.exec(pathname);
if (matched) {
// 抽取具体值
var params = {};
for (var i = 0, l = keys.length; i < l; i++) {
var value = matched[i + 1];
if (value) {
params[keys[i]] = value;
}
}
req.params = params;
var action = route[1];
action(req, res);
return;
}
}
// 处理404请求
function res404(req, res) {
res.end("404");
}
// 错误路由处理
res404(req, res);
}).listen(8000, () => {
console.log('server is create')
})
结果:
至此,我们除了从查询字符串(req.query)或提交数据(req.body)中取到值外,还能从路径的映射里取到值。
(2)自然映射
略...
(3)RESTful
MVC模式大行其道了很多年,直到RESTful的流行,大家才意识到URL也可以设计得很规范,请求方法也能作为逻辑分发的单元。
REST的全称是Representational State Transfer,中文含义为表现层状态转化。符合REST规范的设计,我们称为RESTful设计。它的设计哲学主要将服务器端提供的内容实体看作一个资源, 并表现在URL上。
比如一个用户的地址如下所示:
/users/Errrl
这个地址代表了一个资源,对这个资源的操作,主要体现在HTTP请求方法上,不是体现在URL上。过去我们对用户的增删改查或许是如下这样设计URL的:
POST /user/add?username=Errrl
GET /user/remove?username=Errrl
POST /user/update?username=Errrl
GET /user/get?username=Errrl
操作行为主要体现在行为上,主要使用的请求方法是POST和GET。在RESTful设计中,它是如下这样的:
POST /users/Errrl
DELETE /users/Errrl
PUT /users/Errrl
GET /users/Errrl
它将DELETE和PUT请求方法引入设计中,参与资源的操作和更改资源的状态。
对于这个资源的具体表现形态,也不再如过去一样表现在URL的文件后缀上。过去设计资源的格式与后缀有很大的关联,例如:
GET /users/test.json
GET /users/test.xml
在RESTful设计中,资源的具体格式由请求报头中的Accept字段和服务器端的支持情况来决定。如果客户端同时接受JSON和XML格式的响应,那么它的Accept字段值是如下这样的:
Accept: application/json,application/xml
靠谱的服务器端应该要顾及这个字段,然后根据自己能响应的格式做出响应。在响应报文中,通过Content-Type字段告知客户端是什么格式,如下所示:
Content-Type: application/json
具体格式,我们称之为具体的表现。所以REST的设计就是,通过URL设计资源、请求方法定义资源的操作,通过Accept决定资源的表现形式。
RESTful与MVC设计并不冲突,而且是更好的改进。相比MVC,RESTful只是将HTTP请求方法也加入了路由的过程,以及在URL路径上体现得更资源化。
请求方法的实现
为了让Node能够支持RESTful需求,我们改进了我们的设计。如果use是对所有请求方法的处理,那么在RESTful的场景下,我们需要区分请求方法设计。示例如下所示:
http.js:
var http = require('http');
// 获取请求
var restful = require('./RESTful');
var server = http.createServer();
server.listen(8000,function(err){
console.log('请在浏览器中打开127.0.0.1:8000');
});
restful.server(server);
RESTful.js (路由分发js):
var r = require('./rest')
var url = require('url');
module.exports.server = function (server) {
server.on('request', (req, res) => {
const match = (pathname, routes) => {
for (let i = 0; i < routes.length; i++) {
let route = routes[i];
// 正则匹配
let reg = route[0].regexp;
let keys = route[0].keys;
let matched = reg.exec(pathname);
if (matched) {
// 抽取具体值
const params = {};
for (let i = 0, l = keys.length; i < l; i++) {
let value = matched[i + 1];
if (value) {
params[keys[i]] = value;
}
}
req.params = params;
let action = route[1];
action(req, res);
return true;
}
}
return false;
};
let pathname = url.parse(req.url).pathname;
// 将请求方法变为小写
let method = req.method.toLowerCase();
if (r.routes.hasOwnProperty(method)) {
// 根据请求方法分发
if (match(pathname, r.routes[method])) {
return;
} else {
// 如果路径没有匹配成功,尝试让all()来处理
if (match(pathname, r.routes.all)) {
return;
}
}
} else {
// 直接让all()来处理
if (match(pathname, r.routes.all)) {
return;
}
}
// 处理404请求
function res404(req, res) {
res.end("404");
}
res404(req, res);
})
}
rest.js (路由js):
var yewu = require('./yewu')
var pathRegexp = function (path) {
let strict = path
const keys = [];
path = path
.concat(strict ? '' : '/?')
.replace(/\/\(/g, '(?:/')
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?(\*)?/g, function (_, slash, format, key, capture, optional, star) {
// 将匹配到的键值保存起来
keys.push(key);
slash = slash || '';
return ''
+ (optional ? '' : slash)
+ '(?:'
+ (optional ? slash : '')
+ (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')'
+ (optional || '')
+ (star ? '(/*)?' : '');
})
.replace(/([\/.])/g, '\\$1')
.replace(/\*/g, '(.*)');
return {
keys: keys,
regexp: new RegExp('^' + path + '$')
};
}
const routes = { 'all': [] };
const app = {};
app.use = function (path, action) {
routes.all.push([pathRegexp(path), action]);
};
['get', 'put', 'delete', 'post'].forEach(function (method) {
routes[method] = [];
app[method] = function (path, action) {
routes[method].push([pathRegexp(path), action]);
};
});
// 增加用户
app.post('/user/:username/id/:id', yewu.addUser);
// 删除用户
app.delete('/user/:username/id/:id', yewu.removeUser);
// 修改用户
app.put('/user/:username/id/:id', yewu.updateUser);
// 查询用户
app.get('/user/:username/id/:id', yewu.getUser);
module.exports.routes = routes;
yewu.js (action控制器js文件):
module.exports = {
getUser:(req,res)=>{
// ..... ToDo ..... //
res.end('1');
},
removeUser:(req,res)=>{
// ..... ToDo ..... //
res.end('2');
},
updateUser:(req,res)=>{
// ..... ToDo ..... //
res.end('3');
},
addUser:(req,res)=>{
// ..... ToDo ..... //
res.end('4');
}
}
结果:
- 参数解析同上,此处不再论述。
如此,我们完成了实现RESTful支持的必要条件。这里的实现过程采用了手工映射的方法完成,事实上通过自然映射也能完成RESTful的支持,但是根据Controller/Action的约定必须要转化为Resource/Method的约定,此处已经引出实现思路,不再详述。
目前RESTful应用已经开始广泛起来,随着业务逻辑前端化、客户端的多样化,RESTful模式以其轻量的设计,得到广大开发者的青睐。对于多数的应用而言,只需要构建一套RESTful服务接口,就能适应移动端、PC端的各种客户端应用。