Express
Express是一个快速,简单,极简的Node.js web应用开发框架。通过它,可以轻松的构建各种web应用。例如:
- 接口服务
- 传统的web网站
- 开发工具集成等
- ...
Express本身是极简的,仅仅提供了web开发的基础功能,但是它通过中间件的方式集成了许许多多的外部插件来处理HTTP请求
- body-parser:解析HTTP请求体
- compression:压缩HTTP响应
- cookie-parser:解析cookie数据
- cors:处理跨域资源请求
- morgan:HTTP请求日志记录
- ...
Express中间件的特性固然强大,但是它提供的灵活性是一把双刃剑。它让Express本身变得更加灵活和简单。缺点在于虽然有一些中间件包可以解决几乎所有问题或需求,但是挑选合适的包有时也会称为一个挑战。
Express不对Node.js已有的特性进行二次抽象,只是在它之上扩展了web应用所需的基本功能。
- 内部使用的还是http模块
- 请求对象继承自http.IncomingMessage
- 响应对象继承自http.ServerResponse
- ...
有很多流行框架是基于Express构建出来的。
- LoopBack:高度可扩展的开源Node.js框架,用于快速创建动态的端到端REST API。
- Sails:用于Node.js的MVC框架, 用于构建实用的,可用于生产的应用程序。
- NestJs:一个渐进式的Node.js框架,用于在TypeScript和JavaScript(ES6,ES7,ES8)之上构建高效,可扩展的企业级服务器端应用程序。
- ...
Express的开发作者是知名的开源项目创建者和协作者TJ Holowaychuk。
- Github:https://github.com/tj
- Express、commander、ejs、co、koa...
Express特性
- 简单易学
- 丰富的基础API支持,以及常见的HTTP辅助陈旭,例如重定向、缓存等
- 强大的路由功能
- 灵活的中间件
- 高性能
- 非常稳定(它的源代码几乎百分百的测试覆盖率)
- 视图系统支持14个以上的主流模板引擎
- ...
Express发展历史
- Express.js由TJ Holowaychuk创立。首次发型的版本0.12.0,依据Express.js的Github仓库,是在2010年5月22日
- 在2014年6月,StrongLoop获得了项目的管理权。StrongLoop在2015年9月被IBM并购。在2016年1月,IBM宣布将Express.js置于Node.js基金会孵化器的管理之下。
Express应用场景
- 传统的Web网站,例如Ghost博客系统
- 接口服务
- 服务端渲染中间层
- 开发工具:JSON Server,webpack-dev-server
Express相关链接
- Express官网
- Express Github仓库
- Express 中文文档(非官方)
- Awesome Express
基本使用
创建一个工程,安装express
mkdir myapp
cd myapp
npm init
npm install express
touch app.js
在app.js中编写应用代码
const express = require('express')
const app = express()
const port = 3000
app.get('/',(req,res)=>{
res.send('Hello World!');
})
app.listen(port,()=>{
console.log('Server running at http://localhost:3000/');
})
然后执行node app.js即可启动
路由基础
路由时指确定应用程序如何响应客户端对特定端点的请求,该特定端点是URI(或路径)和特定的HTTP请求方法(GET,POST)等。每个路由可以具有一个或多个处理程序函数,这些函数在匹配该路由时执行。
路由定义采用以下结构:
app.METHOD(PATH,HANDLER)
- app是Express实例
- METHOD是小写的HTTP请求方法
- PATH是服务器上的路径
- HANDLER是当路由匹配时执行的功能
例如在根路由响应POST请求:
app.post('/',function(req,res){
res.send('Got a POST request')
});
响应对/user路径的PUT请求
app.put('/user',function(req,res){
res.send('Got a PUT request')
});
请求和响应
Express应用使用路由回调函数的参数:request和response对象来处理请求和响应的数据。
app.get('/',function(req,res){
});
- 内部使用的还是http模块
- 请求对象继承自http.IncomingMessage
- 响应对象继承自http.ServerResponse
- ...
Express扩展了HTTP模块中的请求和响应对象。
req对象代表HTTP请求,并具有请求查询字符串,参数,正文,HTTP标头等的属性。按照约定,该对象始终称为req(HTTP响应为res),但其实际名称由您正在使用的回调函数的参数确定。
res对象表示Express应用在收到HTTP请求时发送的HTTP响应,按照约定,该对象始终称为res(并且HTTP请求为req),但其实际名称由您正在使用的回调函数的参数确定。
动态路径
在路由路径设计中可以在路径中添加:id
的这种方式作为动态路径,例如
app.get('/todos/:id',(req,res)=>{
res.send(`get /todos/${req.params.id}`);
});
例如访问/todos/2
就能访问到上述路由,然后通过req.params来获取动态路径的值。
解析请求体
在express中使用req.body来获取请求体是获取不到的,需要应用一些中间件来进行解析。
- 解析application/json
app.use(express.json())
- 解析application/x-www.form-urlencoded
app.us(express.urlencoded())
中间件
Express的最大特色,也是最重要的一个设计,就是中间件。一个Express应用,就是由许许多多的中间件来完成的。
Express中间件和AOP面向切面编程就是一个意思,就是都需要经过的一些步骤,不去修改自己的代码,以此来扩展或处理一些功能。
AOP(Aspect Oriented Programming)面向切面编程:
- 将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
- 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率和可维护性。
就是在现有代码程序中,在程序生命周期或者横向流程中加入/减去一个或多个功能,不影响原有功能。
中间件函数
在Express中,中间件就是一个可以访问请求对象、响应对象和调用next方法的一个函数。
在中间件函数中可以执行一下任何任务:
- 执行任何代码
- 修改request或者response响应对象
- 结束请求响应周期
- 调用下一个中间件
中间件分类
在Express中应用程序可以使用以下类型的中间件:
- 应用程序级别中间件
- 路由级别中间件
- 错误处理中间件
- 内置中间件
- 第三那方中间件
应用程序级别中间件
指的是由express.app.use挂载的中间件。
- 不关心请求路径:
app.use(function(req,res,next){
console.log('Time:',Date.now());
next();
})
- 限定请求路径:只有请求路径符合规则才会进入
app.use('/user/:id',function(req,res,next){
console.log('Request Type',req.method);
next();
})
- 限定请求方法+请求路径:
app.get('/user/:id',function(req,res,next){
res.send('USER')
})
- 多个处理函数:
app.use('/user/:id',function(req,res,next){
console.log('Request URL',req.originalUrl);
next();
},function(req,res,next){
console.log('Request Type',req.method);
next();
})
- 为同一个路径定义多个处理中间件:
app.get('/user/:id',function(req,res,next){
console.log('ID',req.params.id);
next();
},function(req,res,next){
res.send('User Info')
})
要从路由器中间件堆栈中跳过其余中间件功能,请调用next('route')将控制权传递给下一条路由。
TIP
next('route')仅在使用app.METHOD()或route.METHOD()函数加载的中间件函数中有效。
例如显示了一个中间件子堆栈,该自堆栈处理对/user/:id路径的GET请求。
app.get('/user/:id',function(req,res,next){
if(req.params.id==='0') next('route')
else next();
},function(req,res,next){
res.send('User Info')
})
中间件可以在数组中声明为可重用。例如显示了一个带有中间件子堆栈的数组,该子堆栈处理对/user/:id路径的GET请求。
function logOriginalUrl(req,res,next){
console.log('Request URL',req.originalUrl);
next()
}
function logMethod(req,res,next){
console.log('Request Type',req.method);
next()
}
var logStuff = [logOriginalUrl,logMethod];
app.get('/user/:id',logStuff,function(req,res,next){
res.send('User Info')
});
路由器级别中间件
路由器级中间件与应用程序级中间件的工作方式相同,只不过它绑定到的实例express.Router()
。
var router = express.Router()
使用router.use()和router.METHOD()函数加载路由器级中间件。我们写一个简单的示例:
我们先创建一个router.js文件并编写router
//router.js
//路由模块
const express = require('express')
//1. 创建路由实例
//路由实例相当于一个mini Express实例
const router = express.Router()
//2. 配置路由
router.get('/foo',(req,res)=>{
res.send('get /foo')
})
//3. 导出路由实例
module.exports = router
//4. 将路由集成到Express实例应用中
然后在app中引入,并应用中间件
const express = require('express')
const app = express()
//app.js
const router = require('./router')
//挂载路由
app.use(router)
app.listen(3000,()=>{
console.log('Server running at http://localhost:3000/');
})
还可以给路由添加访问前缀,例如
app.use('/abc',router)
这样就在router路由下统一加上/abc
访问前缀。
错误处理中间件
以与其他中间件函数相同的方式定义错误处理中间件函数,除了使用四个参数而不是三个参数(特别是使用签名(err,req,res,next))之外:
app.use(function(err,req,res,next){
console.log(err.stack)
res.status(500).send('something broke')
})
错误处理中间件始终带有四个参数。您必须提供四个参数以将其标识为错误处理中间件函数。即使不需要使用该next对象,也必须指定它以维护签名。否则,该next对象将被解释为常规中间件,并且将无法处理错误。
如果将任何内容传递给该next()函数(字符串除外'route'),Express都会将当前请求视为错误,并且将跳过所有剩余的非错误处理路由和中间件函数。
而且要在路由配置中用next将err传递。
router.get('/',async(req,res,next)=>{
try{
...
}catch(err){
next(err)
}
})
注意,我们要在所有的中间件处理函数之后挂载错误处理中间件。
处理404
处理404就是处理没有匹配路由的请求,通常会在所有的路由之后配置处理404的内容
app.use(()=>{
res.status(404).send('404 Not Found.')
})
内置中间件
Express具有以下内置中间件函数:
- express.json(),解析Content-Type为application/json格式的请求体
- express.urlencoded(),解析Content-Type为application/x-www.form-urlencoded格式的请求体
- express.raw(),解析Content-Type为applicaiton/octet-stream格式的请求体
- express.text(),解析Content-Type为text/plain格式的请求体
- express.static(),托管静态资源文件
第三方中间件
早期的Express内置了很多中间件。后来Express在4.x之后移除了这些内置中间件,官方把这些功能性中间件以包的形式单独提供出来。这样做的目的是为了保持Express本身极简灵活的特性,开发人员可以根据自己的需要去灵活的使用。
有关Express常用的第三方中间件功能的部分列表,请参阅:http://expressjs.com/en/resources/middleware.html
Express路由
路由是指应用程序的端点(URI)如何响应客户端请求。你可以使用app与HTTP方法相对应的Express对象的方法来定义路由。例如app.get()处理GET请求和app.post()处理POST请求。你还可以使用app.all()处理所有HTTP方法,并使用app.use()将中间件指定为回调函数。
这些路由方法指定在应用程序收到对指定路由(端点)和HTTP方法的请求时调用的回调函数(有时称为处理函数)。换句话说,应用程序侦听与指定的路由和方法匹配的请求,并且当它检测到匹配项时,它将调用指定的回调函数。
实际上,路由方法可以具有多个回调函数作为参数。对于多个回调函数,重要的是提供next回调函数的参数,然后next()在函数体内调用以将控制权移交给下一个回调。
路由路径
路由路径与请求方法结合,定义了可以进行请求的端点。路由路径可以是字符串,字符串模式或正则表达式。字符?,+,*,
和()
是他们的正则表达式的对应的子集。连字符(-)
和点(.)
由基于字符串的路径按字面意义进行解释。
TIP
Express使用path-to-regexp来匹配路由路径。
例如字符串模式的路由路径。此路由路径将与acd和匹配abcd
app.get('/ab?cd',function(req,res){
res.send('ab?cd')
})
匹配abcd,abxcd,abxxxcd等
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')
})
基于正则表达式的路由路径示例
app.get(/a/,function(req,res){
res.send('/a/')
})
路径参数
路由参数被命名为URL段,用于捕获URL中在其位置处指定的值。捕获的值将填充到req.params对象中,并将路径中指定的route参数的名称作为其各自的键。
要使用路由参数定义路由,只需在路由路径中指定路由参数,如下所示。
app.get('/users/:userId/books/:bookId',function(req,res){
res.send(req.params)
})
路由处理程序
你可以提供行为类似于中间件的多个回调函数来处理请求。唯一的例外是这些回调可能会调用next('route')
以绕过其余的路由回调。你可以使用此机制在路由上施加先决条件,然后在没有理由继续使用当前路由的情况下将控制权传递给后续路由。
路由处理程序可以采用函数,函数数组或二者组合的形式,如以下示例所示。单个回调函数可以处理路由。例如:
app.get('/example/a',function(req,res){
res.send('hello rom a')
})
多个回调函数可以处理一条路由(请确保指定了next对象)。例如:
app.get('/example/a',function(req,res,next){
next()
},function(req,res){
res.send('hello rom a')
})
回调函数数组可以处理路由。例如:
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])
独立功能和功能数组的组合可以处理路由,例如:
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){
next()
},function(req,res){
res.send('hello from d')
})
应对方法
res下表中响应方法()上的方法可以将响应发送到客户端,并终止请求-响应周期。如果没有从路由处理程序调用这些方法,则客户端请求将被挂起。
方法 | 描述 |
---|---|
res.download() | 提示要下载的文件 |
res.end() | 结束响应过程 |
res.json() | 发送JSON响应 |
res.jsonp() | 发送带有JSONP支持的JSON数据响应 |
res.redirect() | 重定向请求 |
res.render() | 渲染视图模板 |
res.send() | 发送各种类型的响应 |
res.sendFile() | 将文件作为八位字节流发送 |
res.sendStatus() | 设置响应状态代码,并将其字符串表示形式发送为响应正文 |
app.route()
可以使用来为路由路径创建可链接的路由处理陈旭app.route()。由于路径是在单个位置指定的,因此创建模块化路由非常有帮助,减少冗余和错别字也很有帮助。
使用定义的链式路由处理程序的示例。
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.Router该类创建模块化的,可安装的路由处理程序。一个Router实例是一个完整的中间件和路由系统;因此,它通常被称为迷你应用程序。
以下示例将路由器创建为模块,在其中加载中间件功能,定义一些路由,并将路由器模块安装在主应用程序的路径上。
//router.js
//路由模块
const express = require('express')
//1. 创建路由实例
//路由实例相当于一个mini Express实例
const router = express.Router()
//2. 配置路由
router.get('/foo',(req,res)=>{
res.send('get /foo')
})
//3. 导出路由实例
module.exports = router
然后,在应用程序中加载路由器模块
const express = require('express')
const app = express()
//app.js
const router = require('./router')
//挂载路由
app.use(router)
app.listen(3000,()=>{
console.log('Server running at http://localhost:3000/');
})
把路由进行了一个模块化的处理,帮助我们组织路由模块。
Express实现原理
express简单的示例如下:
const express = require('express')
const app = express()
router.get('/foo',(req,res)=>{
res.send('get /foo')
})
app.listen(3000,()=>{
console.log('Server running at http://localhost:3000/');
})
源码结构
首先我们看一下express包的源码目录: 打开index.js可以看到它导出了lib下的express模块
/*!
* express
* Copyright(c) 2009-2013 TJ Holowaychuk
* Copyright(c) 2013 Roman Shtylman
* Copyright(c) 2014-2015 Douglas Christopher Wilson
* MIT Licensed
*/
'use strict';
module.exports = require('./lib/express');
然后打开lib/express模块,可以看到里面有一个模块createApplication,顾名思义它是用来创建一个应用的。
...
/**
* Expose `createApplication()`.
*/
exports = module.exports = createApplication;
/**
* Create an express application.
*
* @return {Function}
* @api public
*/
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
// expose the prototype that will get set on requests
app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
// expose the prototype that will get set on responses
app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
app.init();
return app;
}
...
当我们require('express')
时,它返回给我们createApplication函数,然后我们再调用该函数获取app。在createApplication中可以看到定义了一个app函数并将其返回回来,而且通过mixin方法给该函数扩展了其他的一些成员。
最主要的是扩展了proto,proto是引入了application模块,在该代码中导出了一个对象,并给该对象赋予了很多属性,express的很多成员方法都是来自application模块,像listen监听方法、。
var proto = require('./application');
...
mixin(app, proto, false);
...
前面提到express的req和res是扩展了http.IncomingMessage和http.ServerResponse,所以在request.js和response.js中分别对这两个模块进行了扩展。
utils.js中提供了express的工具函数。
view.js中提供了express的模板引擎。
router中存放了express对于路由的处理功能
middleware中存放了express的内置中间件
app.listen
这个方法在express中application模块下定义的,它直接使用http模块创建一个服务并监听。
//application.js
...
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
...
route监听
在express中使用methods
这个依赖库,里面导出了请求常用的方法。
type Method =
| 'ACL'
| 'BIND'
| 'CHECKOUT'
| 'CONNECT'
| 'COPY'
| 'DELETE'
| 'GET'
| 'HEAD'
| 'LINK'
| 'LOCK'
| 'M-SEARCH'
| 'MERGE'
| 'MKACTIVITY'
| 'MKCALENDAR'
| 'MKCOL'
| 'MOVE'
| 'NOTIFY'
| 'OPTIONS'
| 'PATCH'
| 'POST'
| 'PROPFIND'
| 'PROPPATCH'
| 'PURGE'
| 'PUT'
| 'REBIND'
| 'REPORT'
| 'SEARCH'
| 'SOURCE'
| 'SUBSCRIBE'
| 'TRACE'
| 'UNBIND'
| 'UNLINK'
| 'UNLOCK'
| 'UNSUBSCRIBE'
| 'acl'
| 'bind'
| 'checkout'
| 'connect'
| 'copy'
| 'delete'
| 'get'
| 'head'
| 'link'
| 'lock'
| 'm-search'
| 'merge'
| 'mkactivity'
| 'mkcalendar'
| 'mkcol'
| 'move'
| 'notify'
| 'options'
| 'patch'
| 'post'
| 'propfind'
| 'proppatch'
| 'purge'
| 'put'
| 'rebind'
| 'report'
| 'search'
| 'source'
| 'subscribe'
| 'trace'
| 'unbind'
| 'unlink'
| 'unlock'
| 'unsubscribe';
declare const methods: Method[];
export = methods;
然后在application模块中遍历循环将请求方法添加到app中,然后在该方法内部转发给route来进行处理。
/**
* Delegate `.VERB(...)` calls to `router.VERB(...)`.
*/
methods.forEach(function(method){
app[method] = function(path){
if (method === 'get' && arguments.length === 1) {
// app.get(setting)
return this.set(path);
}
this.lazyrouter();
var route = this._router.route(path);
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});