3种常见的路由方式

  本文将会介绍文件路径、MVC、RESTful三种常见的路由方式 php

  --如下内容出自《深刻浅出node.js》html

 

  1. 文件路径型前端

  1.1 静态文件node

  这种方式的路由在路径解析的部分有过简单描述,其让人舒服的地方在于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. 动态文件 json

  在MVC模式流行起来以前,根据文件路径执行动态脚本也是基本的路由方式,它的处理原理是Web服务器根据URL路径找到对应的文件,如/index.asp或/index.php。Web服务器根据文件名后缀去寻找脚本的解析器,并传入HTTP请求的上下文。后端

  如下是Apache中配置PHP支持的方式:  缓存

AddType application/x-httpd-php .php

  解析器执行脚本,并输出响应报文,达到完成服务的目的。现今大多数的服务器都能很智能 地根据后缀同时服务动态和静态文件。这种方式在Node中不太常见,主要缘由是文件的后缀都是.js,分不清是后端脚本,仍是前端脚本,这可不是什么好的设计。并且Node中Web服务器与应用业务脚本是一体的,无须按这种方式实现。服务器

 

  2. MVCapp

  在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];
        if (value) {
          params[keys[i]] = value;
        }
      }
      req.params = params;
      let action = route[1];
      action(req, res);
      return;
    }
  }
  // 处理404请求
  handle404(req, res);
}

  至此,咱们除了从查询字符串(req.query)或提交数据(req.body)中取到值外,还能从路 径的映射里取到值。

  2.2. 天然映射

  手工映射的优势在于路径能够很灵活,可是若是项目较大,路由映射的数量也会不少。从前端路径到具体的控制器文件,须要进行查阅才能定位到实际代码的位置,为此有人提出,满是路由不如无路由。实际上并不是没有路由,而是路由按一种约定的方式天然而然地实现了路由,而无须去维护路由映射。

  上文的路径解析部分对这种天然映射的实现有稍许介绍,简单而言,它将以下路径进行了划分处理:

/controller/action/param1/param2/param3 

  以/user/setting/12/1987为例,它会按约定去找controllers目录下的user文件,将其require出来后,调用这个文件模块的setting()方法,而其他的值做为参数直接传递给这个方法。

(req, res) => {
  let pathname = url.parse(req.url).pathname;
  let paths = pathname.split('/');
  let controller = paths[1] || 'index';
  let action = paths[2] || 'index';
  let args = paths.slice(3);
  let module;

  try {
    // require的缓存机制使得只有第一次是阻塞的
    module = require('./controllers/' + controller);
  } catch (ex) {
    handle500(req, res);
    return;
  }
  let method = module[action]
  if (method) {
    method.apply(null, [req, res].concat(args));
  } else {
    handle500(req, res);
  }
}

  因为这种天然映射的方式没有指明参数的名称,因此没法采用req.params的方式提取,可是直接经过参数获取更简洁,以下所示:

exports.setting = (req, res, month, year) => {
  // 若是路径为/user/setting/12/1987,那么month为12,year为1987
  // TODO
}; 

  事实上手工映射也能将值做为参数进行传递,而不是经过req.params。可是这个观点见仁见智,这里不作比较和讨论。

  天然映射这种路由方式在PHP的MVC框架CodeIgniter中应用十分普遍,设计十分简洁,在Node中实现它也十分容易。与手工映射相比,若是URL变更,它的文件也须要发生变更,手工映射只须要改动路由映射便可。

  3. RESTful

  MVC模式大行其道了不少年,直到RESTful的流行,你们才意识到URL也能够设计得很规范,请求方法也能做为逻辑分发的单元。

REST的全称是Representational State Transfer,中文含义为表现层状态转化。符合REST规范的设计,咱们称为RESTful设计。它的设计哲学主要将服务器端提供的内容实体看做一个资源, 并表如今URL上。

  好比一个用户的地址以下所示:

/users/jacksontian 

 

  这个地址表明了一个资源,对这个资源的操做,主要体如今HTTP请求方法上,不是体如今URL上。过去咱们对用户的增删改查或许是以下这样设计URL的:

POST /user/add?username=jacksontian
GET /user/remove?username=jacksontian
POST /user/update?username=jacksontian
GET /user/get?username=jacksontian 

  操做行为主要体如今行为上,主要使用的请求方法是POST和GET。在RESTful设计中,它是以下这样的:

POST /user/jacksontian
DELETE /user/jacksontian
PUT /user/jacksontian
GET /user/jacksontian 

  它将DELETE和PUT请求方法引入设计中,参与资源的操做和更改资源的状态。

  对于这个资源的具体表现形态,也再也不如过去同样表如今URL的文件后缀上。过去设计资源的格式与后缀有很大的关联,例如:

GET /user/jacksontian.json
GET /user/jacksontian.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路径上体现得更资源化。

  3.1 请求方法

  为了让Node可以支持RESTful需求,咱们改进了咱们的设计。若是use是对全部请求方法的处理,那么在RESTful的场景下,咱们须要区分请求方法设计。示例以下所示:

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

  上面的代码添加了get()、put()、delete()、post()4个方法后,咱们但愿经过以下的方式完成路由映射:

// 增长用户
app.post('/user/:username', addUser);
// 删除用户
app.delete('/user/:username', removeUser);
// 修改用户
app.put('/user/:username', updateUser);
// 查询用户
app.get('/user/:username', getUser);

  这样的路由可以识别请求方法,并将业务进行分发。为了让分发部分更简洁,咱们先将匹配的部分抽取为match()方法,以下所示:

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

  而后改进咱们的分发部分,以下所示:

(req, res) => {
  let pathname = url.parse(req.url).pathname;
  // 将请求方法变为小写
  let method = req.method.toLowerCase();
  if (routes.hasOwnPerperty(method)) {
    // 根据请求方法分发
    if (match(pathname, routes[method])) {
      return;
    } else {
      // 若是路径没有匹配成功,尝试让all()来处理
      if (match(pathname, routes.all)) {
        return;
      }
    }
  } else {
    // 直接让all()来处理
    if (match(pathname, routes.all)) {
      return;
    }
  }
  // 处理404请求
  handle404(req, res);
} 

  如此,咱们完成了实现RESTful支持的必要条件。这里的实现过程采用了手工映射的方法完成,事实上经过天然映射也能完成RESTful的支持,可是根据Controller/Action的约定必需要转化为Resource/Method的约定,此处已经引出实现思路,再也不详述。

  目前RESTful应用已经开始普遍起来,随着业务逻辑前端化、客户端的多样化,RESTful模式以其轻量的设计,获得广大开发者的青睐。对于多数的应用而言,只须要构建一套RESTful服务接口,就能适应移动端、PC端的各类客户端应用。

相关文章
相关标签/搜索