前言
本文是笔者所总结的有关 Nodejs Express 系列之一,
本文为作者原创作品,转载请注明出处;
概述
Nodejs 使得 javascript 通过异步的方式来构建服务器成为可能;最常见的 Server 就是 Web Server,我们可以使用原生的 Nodejs 来构建我们的 Web Server,但是需要自己去实现一系列的 Servlet 的规范,解析 HTTP Request 里面的 Header、Body 生成相应的 Response 以及实现 HTTP 规范等,这些都是繁琐且琐碎的事情;Nodejs 急需一款像 Tomcat、Jetty 这样的 Web Server 来实现 Http 和 Servlet 的规范,于是,Express 便呼之欲出;
由此,我们要知道的是,Express 的地位就好比 Tomcat、Jetty 之于 Java Web Server 的地位;不过 Express 的架构非常灵活,通过其灵活的 Router 的设计,可以非常容易的嵌入其它第三方的模块和应用,所以说,如果单纯的将其视为 Web Server 是不太合适的,它同时具备 Application Server 的特性,类似 Java 的 Spring 容器的特性,区别是,Spring 利用的是 IoC 的方式来构建应用服务器,而 Express 更多的是依赖于其 Router 的设计,一种类似于 Web Filter 的方式来构建其应用服务器,不具备 IoC 的特性,不过于笔者而言,Web Filter 反倒返璞归真,构建大多数的应用服务器往往就足够了;也因此,Express 反倒显得特别的轻巧;
本文不打算对 Node.js 的内容照着官网上照本宣科,相关的基础介绍和基础教程请参考官网 http://expressjs.com/ ,笔者撰写本文的重点是对其特性和内在逻辑进行抽象,以便在笔者的脑海里能够有一幅对 Express 的全景图;
三大核心组件
Express 的核心组件笔者用如下的一张思维导图总结了,
其实归纳起来,Express 真实太简单了,它最核心的就是这样三大组件,Router、Middleware 和 App,通过这三大组件构建了其 Servlet 的所有特性;我们知道,Servlet 最关键的部分就是如何将请求的 URI 路由到对应的处理方法既 handler 上,通过 handler 对 URI 请求做相应的处理,并生成对应的 Response;这一部分是 Router 组件的主要职责了,它定义了 URI 到 Middleware 之间的路由关系,注意一个 Middleware 可以由多个 handlers 构成,也就是说一个 URI 可以被多个 handlers 处理;不过同样需要注意的是,同一个 URI 也可能被多个 Router 进行处理;下面,我们来看看各个组件的主要作用,
- Middleware
由一个或者多个 function 构成;它是构成 Router 实例的一部分; - Router
Router 是 URI 和 Middleware 构成;一个 Middleware 由多个 handlers 既 functions 构成;注意两个特性- 一个 URI 可以交给一个或者多个 functions 处理,要注意的是,functions 是按照注册的顺序依次执行的,通过 function 的最后一个参数 next 方法 next() 来控制其流转,直到整个 request -> response 生命周期结束;
- 一个 URI 可以交给一个或者多个 routers 处理,调用过程中,使用 next(‘router’) 自动的将处理交由下一个 router 进行处理;
- App
app 是一个 Express 实例,用来作为 http 模块构建 http server 的唯一参数,http server 是单例的,所以 app 在同一个 http server 实例的情况下也是单例的,而通过 app 实际上将 http server 转换成了 web server;所以,我们可以简单的认为,一个 app 就是一个 web server,对应了唯一的服务端口,而 router 构建了 uri 和 middleware (既 handlers) 之间的关系,那么剩下的就是,如何将 router 绑定到某个 app 上,在官方文档将该行为称之为 mount;
所以总结起来,Express 的核心逻辑便是,通过 Router 定义了 URI 和 Middlewares 既 functions 之间的路由关系,通过 app 将 Router 绑定(mount)到某个 web server 上;核心的逻辑便是如此了,只是 Express 的使用方式非常的灵活,该部分笔者将会在后续内容概括性的进行描述;
Middleware
Middleware 的定义
Middleware 由一个或者多个 handlers 所构成,作为某请求的回调函数;
单个 handler 的场景,
1
2
3
4
5
6
7
8const express = require('express')
const app = express()
app.get('/', function (req, res) {
res.send('hello world')
})
app.listen(3000, () => console.log('Example app listening on port 3000!'))上面的代码 4 - 6 行便使用了一个 middleware 来处理 '/' 请求,可知这个 middleware 其实就是一个 function,
1
2
3function (req, res) {
res.send('hello world')
}多个 handlers 的场景,
1
2
3
4
5
6app.get('/user/:id', function (req, res, next) {
console.log('ID:', req.params.id)
next()
}, function (req, res, next) {
res.send('User Info')
})这时,该 middleware 由多个 handlers 构成;
注意
next() 的方法调用,表示跳转到下一个 handler 继续执行,如果不使用该方法,则不会跳转到下一个 handler 执行,
不过使用 Middleware 的场景往往比这个更复杂,这部分内容笔者将会在后续内容举例说明;
next()
在上一小节中我们看到了
在 request reponse cycle 的执行流程没有结束的情况下,将会顺序跳转到下一个 handler,并且如果当前 router 的 middleware 执行完成,但并没有 response,那么 next() 将会进入下一个 router 继续执行,看一个例子,
1 | app.get('/user/:id', function (req, res, next) { |
这样,当第一个 GET ‘/user/:id’ router 执行完成以后,便会执行下一个 GET ‘/user/:id’ router 的 middleware;但是,如果,response end1
2
3
4
5
6
7
8
9
10app.get('/user/:id', function (req, res, next) {
console.log('ID:', req.params.id)
next()
}, function (req, res, next) {
res.end('User Info')
})
app.get('/user/:id', function (req, res, next) {
response.end('end')
})
或者,不再使用 next()1
2
3
4
5
6
7
8
9
10app.get('/user/:id', function (req, res, next) {
console.log('ID:', req.params.id)
next()
}, function (req, res, next) {
res.send('User Info')
})
app.get('/user/:id', function (req, res, next) {
response.end('end')
})
那么第二个 GET ‘/user/:id’ router 便永远不会再执行了
可配置的 Middleware
有些时候,我们期望通过参数传递的方式来自定义 Middleware,
my-middleware.js
1 | module.exports = function(options) { |
自定义 middleware,
1 | var mw = require('./my-middleware.js') |
Router
Router 的定义
有关 Router 的定义,官网的 API http://expressjs.com/en/4x/api.html#router 定义得非常的清楚,
A router object is an isolated instance of middleware and routes.
一个 router 对象由两部分组成,routes 和 middleware,
- routes
就是 request URI; - middleware
一个或者多个 functions;
概括而言,Router 就是将 middleware 和相应的 uri 绑定 (mount) 起来,作用就是将某个 uri 路由到相应的 middleware,让 middleware 来处理请求;就像 MVC 中的 Servlet Dispatch 的功能;不过这里要注意的是,这里只是定义了 URI 与 Middleware 之间的映射关系,它并没有明确的指定该 Router 应该使用到哪个 App 实例上,如果要将该 Router 应用到某个 App 上,那么需要将 Router 作为调用参数绑定到某个 App 实例上;这样做的好处就是,Router 和 App 之间解耦了,同一个 Router 实例可以用在多个不同的 App 实例上,如何绑定参考下一章节的内容;
Router 的初始化方式和调用方式
如何定义一个 Router?两种方式
通过定义 Application-level Middleware 的方式定义,像这样,
1
2
3app.get('/', function (req, res) {
res.send('hello world')
})通过定义 Router-level Middleware 的方式定义,见后续章节内容
注意,如果要想调用流程在多个 Router 中进行流转,使用 next(‘router’),见下一小节内容,
next(‘router’)
如果需要提前的跳转出当前的 Middleware function stack,使用 next(‘route’),见下面这个例子,
1 | app.get('/user/:id', function (req, res, next) { |
代码第 3 行,如果 req.params.id === ‘0’,便会直接跳出当前的 middleware stack 并跳转至第二个 router;
App
源码分析
App 就是一个 Express 实例,
1 | var express = require('express'); |
用来创建 Http Server 使用的,1
app.listen(3000, () => console.log('Example app listening on port 3000!'))
我们便开启了一个监听在 3000 端口上的 Web Server 用来接收请求,app 其实正是用来构建 Node.js 的 http server 的必要参数,分析其源码可知,1
2
3
4app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
从源码可知,http.createServer(this) 使用的正是 app 实例来初始化构建 http server 的,而 app 是 Express 的一个实例,Express 通过 exports 将其导出,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30exports = 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;
}
可以看到 app 正好是一个 function(req, res, next) 对象,用来作为构建 http server 的必要参数,而 http server 是单例的,所以 app 在一个 web server 中同样是单例的;不过,当笔者分析到这里的时候,突然想到,其实通过构建多个 app 实例,可以非常容易的构建出多个 web server,通过 router 可以将一些公共的 uri <-> request/response 进行抽象,比如说,用户的身份认证、授权等操作,可以通过 Router 抽象出来,并供多个 app 实例使用,将来在架构设计的时候可以充分的考虑这种模式;->
与 Router
本小节简单的介绍如何将 Router 作用到 App 实例上;
直接使用 app 方法,比如通过
1
2
3app.get('/', function (req, res) {
res.send('Hello World!')
})这样,就相当于将一个 '/' 请求的 Router 实例直接作用到了 app 上;这种方式在 Express 中有专门的命名,叫做 Application-level Middleware;
将 Router 实例注入 app 实例,看如下的代码片段
1
2
3
4
5
6
7
8
9
10var router = express.Router()
// handler for the /user/:id path, which renders a special page
router.get('/user/:id', function (req, res, next) {
console.log(req.params.id)
res.render('special')
})
// mount the router on the app
app.use('/', router)这样,我们将 router 实例注入了 app,当访问请求 /user/:id 的时候,便会调用由 Router 实例所映射的 middleware,
1
2
3
4function (req, res, next) {
console.log(req.params.id)
res.render('special')
}这种方式在 Express 中有专门的命名,叫做 Router-level Middleware;
如果是专属于某个 app 的 Router,笔者推荐使用 Application-level Middleware 的方式直接将某个 Router 作用到该 app 上即可,但如果是某个通用的 Router,比如用户登录、认证和授权流程,笔者推荐使用 Router-level Middleware 的方式;这两部分的详细分析内容参看后续 Router 的章节;
实战
本章节并非是教大家如何写一个简单的 Express 用例,Express 的入门实例可以参考官网的 Hello World 实例,由于官网上的内容过于的零散,所以笔者本章节主要通过实例的方式,将 Express 核心组件之间的细节给串清楚,以至于能够使笔者的脑海中能够有一个清晰的概念图景;
Route Path
Route Paths
常规匹配
匹配根路径
1
2
3app.get('/', function (req, res) {
res.send('root')
})匹配 /bout
1
2
3app.get('/about', function (req, res) {
res.send('about')
})匹配 /random.text
1
2
3app.get('/random.text', function (req, res) {
res.send('random.text')
})
Express 特有的 String Pattern
String Pattern 是 Express 特有的匹配模式,
匹配 acd 和 abcd.
1
2
3app.get('/ab?cd', function (req, res) {
res.send('ab?cd')
})表示 b 可以出现 0 或者 1 次;
匹配 abcd, abbcd, abbbcd …
1
2
3app.get('/ab+cd', function (req, res) {
res.send('ab+cd')
})表示 b 最少出现一次;
匹配 abcd, abxcd, abRANDOMcd, ab123cd…
1
2
3app.get('/ab*cd', function (req, res) {
res.send('ab*cd')
})* 号表示可以匹配任意字符;
匹配 /abe 和 /abcde.
1
2
3app.get('/ab(cd)?e', function (req, res) {
res.send('ab(cd)?e')
})表示 cd 出现 0 次或者 1次;
常规正则表达式
只要路径中出现了“a”,便会匹配
1
2
3app.get(/a/, function (req, res) {
res.send('/a/')
})注意写法,表达式不使用引号;
- 匹配 butterfly、dragonfly…
1
2
3app.get(/.*fly$/, function (req, res) {
res.send('/.*fly$/')
})
Route Parameters
Route 参数表示的就是 URI 参数,按照 URI 参数的标准,URI 参数既是 URL 路径的一部分,比如,1
2
3Route path: /users/:userId/books/:bookId
Request URL: http://localhost:3000/users/34/books/8989
req.params: { "userId": "34", "bookId": "8989" }
- Request URL
访问的完整路径 - Route path: /users/:userId/books/:bookId
要特别注意,Route path 在 Express 中表示的就是除开 Domain 以外的其它部分,其中的 :userId 和 :bookId 就表示 URI 参数; - req.params
Express 会自动将 Route 参数进行解析成 json 对象并存储在 req.params 对象中;
Router
有前面的描述可知,Router 主要分为两种类型,一种是 Application-level,另外一种是 Router-level;注意,这两种并非是两种不同类型的 Router,相反,它们都是 Routers,
Application-level
将 Router 直接应用到 app 实例上,这种使用的方式叫做 Application-level;先来看看在 app 上是如何定义 router 的,来看一个简单的例子,1
2
3app.get('/', function (req, res) {
res.send('GET request to the homepage')
})
这样便在 application-level 上定义了一个 router;比如,我们继续定义,
1 | app.get('/', function (req, res) { |
这样是,实际上,我们针对请求路径 ‘/‘ 定义了两个 router,但第二个 router 永远不会被执行到;更详细的分析参考后续章节,
基础用法
使用常规的 http methods 来定义,来看一个最简单的例子,
1
2
3
4// GET method route
app.get('/', function (req, res) {
res.send('GET request to the homepage')
})上面这个例子做了两件事情,
- 创建一个 Router 实例,定义了如下的两件事情,
路由
是由访问路径 ‘/‘ 到 function(req, res) 的映射过程;- http
method
是 get
- 将 Router 实例绑定到 app 实例上,这里直接使用 app.get() 方法定义了 Router 并实现了绑定;这种方式虽然便捷,但是 Router 就只能使用在该 app 上了;
同样,我们可以定义一个 post method Router
1
2
3
4// POST method route
app.post('/', function (req, res) {
res.send('POST request to the homepage')
})通过下面的这种方式 app.all() 可以对所有 http methods 进行定义,
1
2
3
4app.all('/secret', function (req, res, next) {
console.log('Accessing the secret section ...')
next() // pass control to the next handler
})- 创建一个 Router 实例,定义了如下的两件事情,
使用 app.use 的方式,
不 mount path 同时也不 mount method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var express = require('express')
var app = express()
var myLogger = function (req, res, next) {
console.log('LOGGED')
next()
}
app.use(myLogger)
app.get('/', function (req, res) {
res.send('Hello World!')
})
app.listen(3000)上面这个例子表示,在后续发生任何 app router 调用之前,都会调用 myLogger 输出 ‘LOGGED’,注意,这里的 app.use 并没有 mount path 和 method,所以,myLogger 中间件将会在任意的 URI 和任意的 Request Method 的情况下调用;
mount path
1
2
3
4app.use('/user/:id', function (req, res, next) {
console.log('Request Type:', req.method)
next()
})这种方式等同于使用
1
2
3app.all('/user/:id', function (req, res, next){
...
});集成第三方中间件,比如集成 cookie-parser
1
2
3
4
5
6var express = require('express')
var app = express()
var cookieParser = require('cookie-parser')
// load the cookie-parsing middleware
app.use(cookieParser())
uri -> multi-handlers
常规的例子
一个 middleware 可以由多个 handlers 构成,
1 | app.get('/example/b', function (req, res, next) { |
上面的 next() 表示 request 流程并没有结束,将会继续执行下一个 handler 既下一个 callback function,注意,如果上面的 next() 方法漏掉了,将不会触发后续的 handler 并且也没有任何的 response;上述的定义方式可以简化为使用数组的方式,
1 | var cb0 = function (req, res, next) { |
还可以使用数组和方法混用的方式,
1 | var cb0 = function (req, res, next) { |
无效的 router1
2
3
4
5
6
7
8
9
10
11app.get('/user/:id', function (req, res, next) {
console.log('ID:', req.params.id)
next()
}, function (req, res, next) {
res.send('User Info')
})
// handler for the /user/:id path, which prints the user ID
app.get('/user/:id', function (req, res, next) {
res.end(req.params.id)
})
这里要注意的是,因为第一个 router 执行完以后,没有继续调用 next() 或者 next(‘route’),因此第二个 router 永远不会被执行;
uri -> multi-routers
针对同一个 http request 的 URI 请求可以定义多个 multi-router
1 | app.get('/user/:id', function (req, res, next) { |
链式 Routers
可以通过 app.route() 方法将某个 URI 相关的 http 方法定义在一起,
1 | app.route('/book') |
Router-level
1 | var router = express.Router() |
通过 express.Router() 初始化一个 router 实例,然后为其填充相应的 URI 和 handlers,
直接定义 router 并注入 app
1 | var app = express() |
- 该 router 的 URI 将会绑定到相对路径 ‘/‘ 上,因此,当 request URI === ‘/user/:id’,便会调用该 router;
- router.use(uri, handlers)
为任何 http method 的 uri 注册 handlers; - router.get(uri, handlers)
为 GET http method 的 uri 注册 handlers;
将 router 模块化
假设 app.js 的路径为 /app/app.js,birds.js 的路径为 /app/birds.js,在同一个包路径下,
bird.js
1 | var express = require('express') |
app.js1
2
3
4
5var birds = require('./birds')
// ...
app.use('/birds', birds)
通过 app.use() 指定了 URI 的相对路径 ‘/birds’ 来引用 birds 模块,针对 app 实例的 web server,通过 URI /birds 和 /birds/about 可以访问到 birds 模块中所暴露的 routers;
所以,这里要特别特别注意的是,也是在开发过程中特别容易出错的地方,需要记住的是,app 中定义的访问路径,/birds
1
app.use('/birds', birds)
与 Router 模块中所定义的访问路径,'/'
、'/about'
1
2
3
4
5
6
7
8// define the home page route
router.get('/', function (req, res) {
res.send('Birds home page')
})
...
router.get('/about', function (req, res) {
res.send('About birds')
})
之间是拼接
的关系;
- 如果期望由 app 来全权控制访问路径,那么在 Router 中可以不指定访问路径,既是使用
'/'
即可; 同样,如果不期望由 app 来指定令,而是全权由 Router 自己来控制访问路径,那么 app 可以使用如下方式定义,既不指定访问路径,
1
app.use(birds)
然后,由 router 来制定访问路径,
1
2
3
4
5
6
7router.get('/birds/', function (req, res) {
res.send('Birds home page')
})
...
router.get('/birds/about', function (req, res) {
res.send('About birds')
})
注意流转顺序
默认情况下,URI 是按照 Router 所配置的顺序依次执行的,比如,我们定义有下面这样的 Routers,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20app.use('/', index);
app.use('/users', users);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
此时,若我们想添加一个新的 URI /manager,如果像这样添加,将它添加到末尾,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25app.use('/', index);
app.use('/users', users);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
app.get("/manager", function(req, res, next) {
console.log("get index");
res.render('index', { title: 'Express' });
});
当访问 /manager 则会得到 404 Resource Not Found 的错误,原因是 Router 是按照上述代码的顺序进行匹配的,当匹配完了 / 和 /users 以后,便会匹配 404 的错误反馈,因此,虽然这里添加了 /manager URI 的 Router 但是却执行不到这里;因此,正确的添加方式是,将它添加到 /users 请求之后,像这样1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24app.use('/', index);
app.use('/users', users);
app.get("/manager", function(req, res, next) {
console.log("get index");
res.render('index', { title: 'Express' });
});
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
实现类似切面的方法
TODO,我要监控每一个方法执行的时间;
注意事项
本章节笔者就自己开发过程中遇到的问题进行汇总,
Route Parameter
模块化 Router 的参数设置
如果使用模块化的 Router 的方式来定义,那么 Route Parameter 必须设置在 Router 模块上,否则不生效,如下,
app.js1
2
3
4...
var product = require('./routes/product')
...
app.use('/product/:productId', product);
product.js1
2
3
4router.get('/', function(req, res, next) {
logger.debug('the params: %s', Object.keys(req.params).length)
res.render('product', { title: 'Product' });
});
访问 localhost:3000/product/111,结果这个时候,req.params 中没有任何参数输出,也就是说,上面的这种方式,Express 不会解析任何参数化 URI 的值;经过测试,发现必须使用如下的方式中的一种,才可以顺利的解析出相关的 URI 参数,
将 URI 路径全部由 Router 自己来配置
app.js1
app.use(product);
product.js
1
2
3
4router.get('/product/:productId', function(req, res, next) {
logger.debug('the params: %s', Object.keys(req.params).length)
res.render('product', { title: 'Product' });
});或者参数部分由 Router 来配置
app.js1
app.use('/product', product);
product.js
1
2
3
4router.get('/:productId', function(req, res, next) {
logger.debug('the params: %s', Object.keys(req.params).length)
res.render('product', { title: 'Product' });
});
参数后可直接接后缀
有时候我们需要给参数化的 URI 加上后缀,且该后缀必须直接添加到参数的后面,比如想下面这个链接那样,1
/product/111.html
那么我们是否可以通过参数化的 URI 来进行配置呢?比如像这样,1
/product/:productId.html
答案是可行的,可以顺利的通过 req.params 解析到结果;
References
expressjs.com: http://expressjs.com/en/guide/writing-middleware.html
webpack: https://zhuanlan.zhihu.com/p/20782320