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];