~ Express 路由和中间件

Express 基于 Node.js,是一个简洁、灵活的 web 开发框架。

它是对 Node.js 中 http 模块的一层抽象,这使得开发者可以无需注意细节,而直接上手进行页面和业务逻辑的开发:

  • 路由:定义路由表执行不同的 http 请求动作
  • 中间件:设置中间件来响应 http 请求
  • 模板引擎:通过向模板传递参数动态渲染 HTML 页面

其核心概念是「 路由和中间件 」,从本质上来说,一个 Express 应用就是在调用各种中间件。


路由

路由,通过特定的 http 请求方法和请求路径( URL ),确定应用程序如何响应客户端对特定端点的请求。

每个路由可以有一个或多个处理函数,这些函数在路由匹配的时候被执行。


「 基本语法 」app.METHOD(path,handler)


[1] app:一个 express 实例。          // app 方法

// 安装并导入 express
   let express = require('express');
   const app = express();    // 实例化

[2] METHOD:请求方法 - express 支持对应 http 的请求方法,如 get、post ... 以及一个特殊的路由方法 app.all()

app.get('/',(req,res)=>{
	res.end('get request');
})
 
app.post('/',(req,res)=>{
	res.end('post request')
})
  • app.all()  该方法用于为所有请求方法的路径提供加载中间件功能
app.all('/secret',(req,res,next)=>{
	console.log('app-all');
	next();  // pass control to the next handler
});

[3] path:路由路径                               // 请求方法和路由路径,共同确定了对特定端点的请求

  • 基于字符串的路由路径
  // 此路由路径将匹配请求到根路由,/
  app.get('/', function (req, res) {
    res.send('root')
  })
  // 此路由路径将匹配请求/about
  app.get('/about', function (req, res) {
    res.send('about')
  })
  // 此路由路径将匹配请求/random.text
  app.get('/random.text', function (req, res) {
    res.send('random.text')
  })
  • 基于字符串模式的路由路径
  // 如果要在路径字符串中使用字符($),需将其包含在([])之内,如 /data/([\$])book 表示:/data/$book
  // 此路由路径将匹配acd并abcd
  app.get('/ab?cd', function (req, res) {
    res.send('ab?cd')
  })
  // 此路由路径将会匹配abcd,abbcd,abbbcd等等
  app.get('/ab+cd', function (req, res) {
    res.send('ab+cd')
  })
  // 此路由路径匹配abcd,abxcd,abRANDOMcd,ab123cd等
  app.get('/ab*cd', function (req, res) {
    res.send('ab*cd')
  })
  // 此路由路径将匹配/abe并/abcde
  app.get('/ab(cd)?e', function (req, res) {
    res.send('ab(cd)?e')
  })
  • 基于正则表达式的路由路径
  // 此路由路径将与路由名称中的 “a” 匹配
  app.get(/a/, function (req, res) {
    res.send('/a/')
  })
  // 此路由路径将匹配butterfly和dragonfly,但不butterflyman,dragonflyman等
  app.get(/.*fly$/, function (req, res) {
    res.send('/.*fly$/')
  })
  • 路由参数:带参数的路由路径,用于捕获 URL 中指定位置对应的值

命名:路由参数的名称必须由“单词字符”( A-Za-z0-9_ )组成

// 由于连字符(-)和点(.)是字面解释的,因此,它们可以与路由参数一起使用以达到有用的目的
   Route path: /flights/:from-:to
   Request URL: http://localhost:3000/flights/LAX-SFO
   req.params: { "from": "LAX", "to": "SFO" }
 
   Route path: /plantae/:genus.:species
   Request URL: http://localhost:3000/plantae/Prunus.persica
   req.params: { "genus": "Prunus", "species": "persica" }

// 要更好地控制可以通过路由参数匹配的确切字符串,可以在括号(())中追加正则表达式
   Route path: /user/:userId(\d+)
   Request URL: http://localhost:3000/user/42
   req.params: {"userId": "42"}

使用:捕获到的值,将填充到 req.params 对象中,而路径中指定的路由参数的名称,则作为其各自的键。

Request URL: http://localhost:3000/users/34/books/8989
 
//  代码示例:要使用路由参数定义路由,只需在路由路径中指定路由参数
app.get('/users/:userId/books/:bookId', function (req, res) {
  res.send(req.params)    // { "userId": "34", "bookId": "8989" }
})

[4] handler:路由处理程序                                                        // 请求对象和响应方法

路由处理程序,也可以称之为路由句柄,本质上是一个或多个回调函数,这些函数的行为与中间件相似,可以处理请求。

唯一的例外是,这些回调可能会调用 next('route') 绕过剩余的路由回调。


路由处理程序,可以使用单个 handler、多个 handler、handler 数组或混合的形式来处理请求

  • 一个 handler,可以处理一个路由
   app.get('/', (req, res) => {
	   res.writeHeader(200, {
		   'content-Type': 'text/plain;charset=utf8'
	   });
	
	   res.end('一个handler可以处理一个路由');
   });
  • 多个 handler,可以处理一个路由
   app.get('/about',(req,res,next)=>{
	   res.writeHeader(200, {
		   'content-Type': 'text/plain;charset=utf8'
	   });
	
	   res.write('多个handler可以处理一个路由。');
	   next();
   },(req,res)=>{
	   res.end('但,需要指定next对象');
   });
  • 可以使用 handler 数组,来处理一个请求
   var cb0 = function (req, res, next) {
     console.log('CB0')
     next()
   }
   var cb1 = function (req, res, next) {
     console.log('CB1')
     next()
   }
   var cb2 = function (req, res) {
     res.send('Hello from C!')
   }
   app.get('/example/c', [cb0, cb1, cb2])
  • 可以使用 handler、handler 数组混合的形式,来处理函数
   var cb0 = function (req, res, next) {
     console.log('CB0')
     next()
   }
   var cb1 = function (req, res, next) {
     console.log('CB1')
     next()
   }
   app.get('/example/d', [cb0, cb1], function (req, res, next) {
     console.log('the response will be sent by the next function ...')
     next()
   }, function (req, res) {
     res.send('Hello from D!')
   })

也可以是,可链接的路由处理程序

其优势在于:路径在单个位置指定的,所以对创建模块化路由很有帮助,因为这可以减少冗余和拼写错误

app.route('/book')
  .get(function (req, res) {
    res.send('Get a random book')
  })
  .post(function (req, res) {
    res.send('Add a book')
  })
  .put(function (req, res) {
    res.send('Update the book')
  })

中间件

express 实例( app )在运行过程中,会调用一系列的中间件,其最大的特点是:一个中间件处理完,会传递给下一个中间件。

简单来说,中间件( middleware ),就是在收到请求后、发送响应之前这个阶段,所执行的一些函数。


[1] 应用程序级中间件

其绑定的对象是 express 实例:app 对象,然后,使用 app.use() 或 app.METHOD() 方法进行加载

  • app.use 的用法和路由基本相同,都可以使用单个 handler、多个 handler、handler 数组等形式
  // 引入 express,并实例化
  const express = require('express');
  const app = express();

  app.use([path],[callback1[,callback2]])
  // path 默认为 /,会匹配以 path 开头的路径,不接受正则

「 拓展 」app.all 和 app.use 的区别在于:
  • app.all()  指的是路由中的所有请求方式,如:app.all('/a'),可以同时覆盖:app.get('/a')app.post('/a'...
  •   app.use()  接受一个或多个回调函数作为中间件。中间件,通常不处理请求和响应(技术上可以),往往处理输入数据,然后传递请求给下一个中间件,如处理 headercookiessessions等;执行顺序按照书写顺序,必须写在 app[method之前,否则不执行。

[2] 路由器级中间件

路由器级中间件,其工作方式与应用程序级中间件类似,但又有不同;

区别在于:它绑定的对象是 express.Router() 的实例:router 对象,同时,可以使用 router.use 或 router.METHOD 来加载


「 应用场景 - 路由的模块化 」定义一些路由,把这些路由模块安装在应用程序级路由上,并加载中间件功能

  • 编写路由器级中间件文件
//  /routes/bird.js

const express = require('express');
const router = express.Router();    // 安装并引入路由器级中间件对象

router.use((req,res,next)=>{
	console.log('Time',Date.now());
	next()
});
router.get('/',(req,res)=>{
	res.send('birds homepage');
});
router.get('/about',(req,res)=>{
	res.send('birds aboutpage');
});
 
module.exports = router;
  • 在应用程序中,加载路由器级中间件模块
// app.js

const express = require('express');
const app = express();
 
// 引入路由器级中间件文件 bird.js
const birdRouter  = require('./routes/bird.js');
 
app.use('/bird',birdRouter);
 
app.listen(3000,()=>{
   console.log('app is running');
});

[3] 错误处理中间件

错误处理中间件,始终需要 4 个参数来对其进行标识:即使不需要使用next对象,也必须指定它以维护签名;

否则,该next对象会被解释为常规中间件,并且将无法处理错误。

// 实际开发中,错误处理通常放在入口文件的最后

app.use(function(err, req, res, next) {
	res.locals.message = err.message;
	// ? 开发环境 : 生产环境错误提示
	res.locals.error = req.app.get('env') === 'development' ? err : {};
	res.status(err.status || 500);
	res.render('error');
});

[4] express 内置中间件

express.staitc 是 Express 唯一内置的中间件,负责 Express 应用中的静态资源托管

express.static(root,[option]);    // root,指的是提供静态资源的根目录;

// 通常,不需要特殊设置,只需要将静态资源所在目录(假设为:public)传递给
// express.static 中间件,就可以访问 public 目录下的资源了
   app.use(express.staitc('public'));  
   // localhost:3000/images/logo.png 
 
// 如果静态资源放在多个目录下面,可以多次调用 express.static 中间件
   app.use(express.static('public'));
   app.use(express.static('files'));  // 中间件会根据目录添加的顺序查找所需的文件
 
// 如果希望所有通过该中间件访问的文件存放在一个'虚拟'目录(实际不存在)中,
// 可以通过为静态资源目录指定一个加载路径的方式来实现
   app.use('/static',express.static('public'));
   // 这样就可以通过带有‘/static’前缀的地址来访问public目录下的资源了
   // localhost:3000/static/images/logo.png

[5] 第三方中间件

第三方插件,即为所需要的功能安装 Node.js 模块,然后将其加载到 app 的应用程序级别或路由器级别。