3种常见的路由方式
- 格式:pdf
- 大小:168.82 KB
- 文档页数:6
3种常见的路由⽅式
本⽂将会介绍⽂件路径、MVC、RESTful三种常见的路由⽅式
--以下内容出⾃《深⼊浅出node.js》
1. ⽂件路径型
1.1 静态⽂件
这种⽅式的路由在路径解析的部分有过简单描述,其让⼈舒服的地⽅在于URL的路径与⽹站⽬录的路径⼀致,⽆须转换,⾮常直观。这
种路由的处理⽅式也⼗分简单,将请求路径对应的⽂件发送给客户端即可。如:
// 原⽣实现
http.createServer((req, res) => {
if (req.url === '/home') {
// 假设本地服务器将html静态⽂件放在根⽬录下的view⽂件夹
fs.readFile('/view/' + req.url + '.html', (err, data) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(data)
})
}
}).listen()
// Express
app.get('/home', (req, res) => {
fs.readFile('/view/' + req.url + '.html', (err, data) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.status(200).send(data)
})
})
1.2. 动态⽂件
在MVC模式流⾏起来之前,根据⽂件路径执⾏动态脚本也是基本的路由⽅式,它的处理原理是Web服务器根据URL路径找到对应的⽂
件,如/index.asp或/index.php。Web服务器根据⽂件名后缀去寻找脚本的解析器,并传⼊HTTP请求的上下⽂。
以下是Apache中配置PHP⽀持的⽅式:
AddType application/x-httpd-php .php
解析器执⾏脚本,并输出响应报⽂,达到完成服务的⽬的。现今⼤多数的服务器都能很智能 地根据后缀同时服务动态和静态⽂件。这种
⽅式在Node中不太常见,主要原因是⽂件的后缀都是.js,分不清是后端脚本,还是前端脚本,这可不是什么好的设计。⽽且Node中Web服
务器与应⽤业务脚本是⼀体的,⽆须按这种⽅式实现。
2. MVC
在MVC流⾏之前,主流的处理⽅式都是通过⽂件路径进⾏处理的,甚⾄以为是常态。直到 有⼀天开发者发现⽤户请求的URL路径原来可以跟具体脚本所在
的路径没有任何关系。
MVC模型的主要思想是将业务逻辑按职责分离,主要分为以下⼏种。
控制器(Controller),⼀组⾏为的集合。
模型(Model),数据相关的操作和封装。
视图(View),视图的渲染。
这是⽬前最为经典的分层模式,⼤致⽽⾔,它的⼯作模式如下说明。
路由解析,根据URL寻找到对应的控制器和⾏为。
⾏为调⽤相关的模型,进⾏数据操作。
数据操作结束后,调⽤视图和相关数据进⾏页⾯渲染,输出到客户端。
2.1 ⼿⼯映射
⼿⼯映射除了需要⼿⼯配置路由外较为原始外,它对URL的要求⼗分灵活,⼏乎没有格式上的限制。如下的URL格式都能⾃由映射: '/user/setting' , '/setting/user'
这⾥假设已经拥有了⼀个处理设置⽤户信息的控制器,如下所⽰:
exports.setting = (req, res) => {
// TODO
}
再添加⼀个映射的⽅法(路由注册)就⾏,为了⽅便后续的⾏⽂,这个⽅法名叫use(),如下所⽰:
const routes = []
const use = (path, action) => {
routes.push([path, action]);
}
我们在⼊⼝程序中判断URL,然后执⾏对应的逻辑,于是就完成了基本的路由映射过程,如下所⽰:
(req, res) => {
const pathname = url.parse(req.url).pathname
for (let i = 0; i < routes.length; i++) {
let route = routes[i];
if (pathname === route[0]) {
let action = route[1]
action(req, res)
return
}
}
// 处理404请求
handle404(req, res)
}
⼿⼯映射⼗分⽅便,由于它对URL⼗分灵活,所以我们可以将两个路径都映射到相同的业务 逻辑,如下所⽰:
use('/user/setting', exports.setting);
use('/setting/user', exports.setting);
// 甚⾄
use('/setting/user/jacksontian', exports.setting);
2.1.1 正则匹配
对于简单的路径,采⽤上述的硬匹配⽅式即可,但是如下的路径请求就完全⽆法满⾜需求了:
'/profile/jacksontian' , '/profile/hoover'
这些请求需要根据不同的⽤户显⽰不同的内容,这⾥只有两个⽤户,假如系统中存在成千上 万个⽤户,我们就不太可能去⼿⼯维护所有
⽤户的路由请求,因此正则匹配应运⽽⽣,我们期望通过以下的⽅式就可以匹配到任意⽤户:
use('/profile/:username', (req, res) => {
// TODO
});
于是我们改进我们的匹配⽅式,在通过use注册路由时需要将路径转换为⼀个正则表达式, 然后通过它来进⾏匹配,如下所⽰:
const pathRegexp = (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 + '$');
}
上述正则表达式⼗分复杂,总体⽽⾔,它能实现如下的匹配:/profile/:username => /profile/jacksontian, /profile/hoover
/user.:ext => /user.xml, /user.json
现在我们重新改进注册部分:
const use = (path, action) => {
routes.push([pathRegexp(path), action]);
}
以及匹配部分:
(req, res) => {
const pathname = url.parse(req.url).pathname;
for (let i = 0; i < routes.length; i++) {
let route = routes[i];
// 正则匹配
if (route[0].exec(pathname)) {
let action = route[1];
action(req, res);
return;
}
}
// 处理404请求
handle404(req, res);
}
现在我们的路由功能就能够实现正则匹配了,⽆须再为⼤量的⽤户进⾏⼿⼯路由映射了。
2.1.2 参数解析
尽管完成了正则匹配,可以实现相似URL的匹配,但是:username到底匹配了啥,还没有解决。为此我们还需要进⼀步将匹配到的内容
抽取出来,希望在业务中能如下这样调⽤:
use('/profile/:username', function (req, res) {
var username = req.params.username;
// TODO
});
这⾥的⽬标是将抽取的内容设置到req.params处。那么第⼀步就是将键值抽取出来,如下所⽰:
const pathRegexp = function (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 + '$')
};
}
我们将根据抽取的键值和实际的URL得到键值匹配到的实际值,并设置到req.params处,如 下所⽰:
(req, res) => {
const pathname = url.parse(req.url).pathname;
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];