## 精读express系列——基础篇(1)
yaodebian opened this issue · comments
精读express系列——基础篇(1)
foreword(前言)
由于前三周搬家、身体、心理等方面的一些因素,导致文章更新中断。目前已经明确了自己的一些规划,从本周开始,我将会以更加系统的视角深入各种知识,以更加稳定的节奏进行文章创作,希望能在这个过程中不断提高自己的技术水准、眼界视角、文章质量等。
express作为nodejs中非常经典的框架之一,我认为从学习使用它开始,并深入阅读并理解它的源码,会是一个非常有意思的过程,而且我觉得这本身也是一个非常好的学习nodejs的方式。
从本篇开始,我将从express的使用开始,到实际案例,再到源码解读,一步步全面理解express,并同时更加熟练地掌握nodejs。
express
express是什么?
Fast, unopinionated, minimalist web framework for Node.js. (一个基于Nodejs的、可自我灵活配置的、极简的web框架)
官方就是有点绕,我把它理解为:它是web框架,能够提供后端服务能力,并能以类似JSP模版的形式渲染web页面。
express有两个核心模块——路由和中间件,路由是对请求路径的匹配与处理,中间件则是请求完成之前的一个处理函数(handler),在请求响应客户端(response to client)之前,你可以对返回的数据进行结构化、过滤或者其他的一些操作。
除此之外,本篇文章还涉及到:
- express的模版引擎;
- express基于中间件的错误监听与处理;
- express设置trust proxy;
- express基于第三方库的数据库连接解决方案;
路由(router)
路由和操作系统中的文件检索类似,拿windows为例,在资源管理器的路径栏中输入我们要访问的文件夹路径,资源管理器就会将视图切换到相应的目录下, 路由中的路径就和我们访问的文件夹路径类似,路由中的handler就和资源管理器切换视图的操作类似。
一个最简单的路由案例是这样的:
var express = require('express')
var app = express()
// respond with "hello world" when a GET request is made to the homepage
app.get('/', function (req, res) {
res.send('hello world')
})
app.listen(8080, () => {
console.log('Server is running on http://localhost:8080')
})
服务启动后,只要访问“localhost:8080”,那么页面将会展示“hello world”的字样。
我们注意到上面的脚本中,调用了一个get方法作为“/”的监听,而我们访问的“localhost:8080”就是一个get请求,所以我们猜想get方法就是针对“/”路径的get请求的监听。确实也是这样的,除了get,express还支持其他的请求Methods的调用,如下:
- checkout
- copy
- delete
- get
- head
- lock
- merge
- mkactivity
- mkcol
- move
- m-search
- notify
- options
- patch
- post
- purge
- put
- report
- search
- subscribe
- trace
- unlock
- unsubscribe
当然我们一般只会用get和post这两个请求Method。
除了以上express对请求方法的封装,express还支持一个特殊的方法——app.all。这个方法的使用如下:
app.all('/secret', function (req, res, next) {
console.log('Accessing the secret section ...')
next() // pass control to the next handler
})
它的作用是仅仅对“/secret”这个请求路径做监听,无论请求的http method是get、post、head、put等,都会触发后面的handler方法。
与all类似,还有一个方法route(),它与all不同的是,route所监听的http method是由coder按照自己的需要配置的,而且不同的http method所对应的handler也是单独配置的,如下:
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')
})
这个方法的好处是对于几种数据请求我们都可以用同一个路径,不同的数据通过不同的method区分,这样我们只需要关注对应的几个handler了。不过我觉得这种方式还是不太妥,在请求的时候比较容易弄混,每次请求都要去查一下get请求到底是获取列表呢,还是新增数据呢,所以还是按照正常模式,每个数据库操作不同的请求路径,这样会舒服一些。
如果像上面那样,每一个接口都写在同一个文件中,这肯定是不切实际的。express有相应的模块机制,能够让我们很好地管理我们的路由:
// 首先通过express.router()构建一个mini的express实例,它拥有和express一样的请求监听方法。
bird.js:
var express = require('express')
var router = express.Router()
// middleware that is specific to this router
router.use(function timeLog (req, res, next) {
console.log('Time: ', Date.now())
next()
})
// define the home page route
router.get('/', function (req, res) {
res.send('Birds home page')
})
// define the about route
router.get('/about', function (req, res) {
res.send('About birds')
})
module.exports = router
var birds = require('./birds')
// ...
app.use('/birds', birds)
由于express.router()本身就是一个中间件,所以我们可以在运行脚本中通过app.use('/birds', birds)的方式将birds.js中所有的路由路径都加上/birds前缀,以作为最终的监听地址。简单来说,就是只要请求路径的前缀是/birds,那么nodejs将会进入birds.js中继续匹配路径,并执行对应的handler,比如我们请求/birds/about,那么请求将返回“About birds”。
通过这样的方式,可以将不同模块的接口放到单独一个js文件中,并配上不同的前缀,这样整个接口就变得非常清晰了。
中间件(middleware)
什么是中间件,中间件就是一个方法,这个方法接收request和response对象作为参数,还可以通过next()方法调用下一个中间件。(路由的handler其实就是一个中间件,只不过它是和某个请求路径绑定在一起的,只有请求这条路径才会触发这个中间件的执行)
一个简单的中间件例子如下:
var 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)
上面代码,如果请求localhost:3000,会先执行myLogger方法,然后再执行路由路径“/”对应的handler。
中间件有以下的特点:
- 按顺序执行;
- 可执行任何脚本;
- 可以对request对象和response对象进行overwrite;
- 可以响应请求以结束本次请求生命周期;
- 通过next方法执行下一个中间件;
中间件可以分为以下几类:
- Application-level middleware (应用层级的中间件)
- Router-level middleware(路由层级的中间件)
- Error-handling middleware (错误监听中间件)
- Built-in middleware (内置中间件)
- Third-party middleware(第三方中间件)
应用层级的中间件:只要是直接通过express实例绑定的中间件就是应用层级的中间件,如下:
var app = express()
app.use(function (req, res, next) {
console.log('Time:', Date.now())
next()
})
app.use('/method', 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')
})
路由层级的中间件: 与应用层级的中间件类似,路由层级中间件是由express.Router()实例绑定的中间件。
错误监听中间件:它比较特殊哦,必须有四个参数,如下所示:
app.use(function (err, req, res, next) {
console.error(err.stack)
res.status(500).send('Something broke!')
})
四个参数缺一不可,第一个参数用来接收error对象。
内置中间件目前有以下这几个:
- express.static
- express.json(Available with Express 4.16.0+)
- express.urlencoded(Available with Express 4.16.0+)
其中express.static是非常好用的一个中间件,通过简单一句的代码即可配置好静态资源的访问,如配置app.use('/static', express.static('public'))
,我们便可以基于/public前缀访问./public
文件夹下面的各种静态资源,如:
http://localhost:3000/static/images/kitten.jpg
http://localhost:3000/static/css/style.css
http://localhost:3000/static/js/app.js
http://localhost:3000/static/images/bg.png
http://localhost:3000/static/hello.html
第三方中间件则是第三方开发者封装的中间件,比较常用的是“body-parser”、“cookie-parser”、“morgan”等。
模板引擎(template engines)
模板引擎的作用和传统后端渲染模式JSP其实是类似的,以下我们以pug模板引擎为例。
首先,我们需要设置视图层文件夹路径:
app.set('views', './views')
接着我们设置模板引擎:
app.set('view engine', 'pug')
当然还得下载pug:npm install pug --save
这时我们可以在views文件夹中创建一个模板:index.pug
html
head
title= title
body
h1= message
配置相应的请求:
app.get('/', function (req, res) {
res.render('index', { title: 'Hey', message: 'Hello there!' })
})
这样当在浏览器中请求服务根路径时,就会看到index.pug渲染的页面(index.pug中的title和message变量就是我们render方法中传的{ title: 'Hey', message: 'Hello there!' }的title属性和message属性)。
错误监听(Error handler)
在中间件那一块,我们已经讲到了关于错误监听的一个中间件,这里我们先来实现一下。
app.get('/500', (req, res, next) => {
throw new Error('500')
})
app.use((err, req, res, next) => {
res.send({
code: 500,
success: false,
data: '',
msg: 'something went wrong'
})
})
当我们的访问/500时,会触发错误监听的中间件,并返回一个error对象。
其实默认情况下,express是有错误处理的,只是体验性比较差。假如我们把上面最后的中间件删除,则返回如下:
Error: 500
at /Users/yaodebian/demo_example/gift2017_backstage/server.js:8:9
at Layer.handle [as handle_request] (/Users/yaodebian/demo_example/gift2017_backstage/node_modules/express/lib/router/layer.js:95:5)
at next (/Users/yaodebian/demo_example/gift2017_backstage/node_modules/express/lib/router/route.js:137:13)
at Route.dispatch (/Users/yaodebian/demo_example/gift2017_backstage/node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (/Users/yaodebian/demo_example/gift2017_backstage/node_modules/express/lib/router/layer.js:95:5)
at /Users/yaodebian/demo_example/gift2017_backstage/node_modules/express/lib/router/index.js:281:22
at Function.process_params (/Users/yaodebian/demo_example/gift2017_backstage/node_modules/express/lib/router/index.js:335:12)
at next (/Users/yaodebian/demo_example/gift2017_backstage/node_modules/express/lib/router/index.js:275:10)
at /Users/yaodebian/demo_example/gift2017_backstage/node_modules/express/lib/router/index.js:635:15
at next (/Users/yaodebian/demo_example/gift2017_backstage/node_modules/express/lib/router/index.js:260:14)
返回的是一个错误调用栈。
当然我们可以不让它返回调用栈,只要运行如下:NODE_ENV=production node [你的脚本],这个时候会返回:Internal Server Error
。
最后我们再思考一下一个问题,如果请求的路径是不存在的,那么上面的错误监听类型的中间件能否被触发呢?
不能。
考虑到这种情况,我们必须在所有路由后面再加上这样几句脚本:
app.use('*', (req, res) => {
res.status(404).send(({
code: 500,
success: false,
data: '',
msg: 'the directory you request is not exist in the server')
})
}
所以,为了能兼顾服务器报错和404错误,我们需要加上这两句:
app.use('*', (req, res) => {
res.status(404).send(({
code: 500,
success: false,
data: '',
msg: 'the directory you request is not exist in the server')
})
}
app.use((err, req, res, next) => {
res.send({
code: 500,
success: false,
data: '',
msg: 'something went wrong'
})
})
设置trust proxy
当在代理服务器之后运行 Express 时,请将应用变量 trust proxy 设置(使用 app.set())为下述序列中的一项。
如果没有设置应用变量 trust proxy,应用将不会运行,除非 trust proxy 设置正确,否则应用会误将代理服务器的 IP
地址注册为客户端 IP 地址。
1.Boolean类型
如果为 true,客户端 IP 地址为 X-Forwarded-* 头最左边的项。
如果为 false, 应用直接面向互联网,客户端 IP 地址从 req.connection.remoteAddress 得来,这是默认的设置。
2.IP 地址
IP 地址、子网或 IP 地址数组和可信的子网。下面是预配置的子网列表。
- loopback - 127.0.0.1/8, ::1/128
- linklocal - 169.254.0.0/16, fe80::/10
- uniquelocal - 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7
使用如下方式设置 IP 地址:
app.set('trust proxy', 'loopback') // specify a single subnet(指定唯一子网)
app.set('trust proxy', 'loopback, 123.123.123.123') // specify a subnet and an address(指定子网和 IP 地址)
app.set('trust proxy', 'loopback, linklocal, uniquelocal') // specify multiple subnets as CSV(指定多个子网)
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']) // specify multiple subnets as an array(使用数组指定多个子网)
当指定地址时,IP
地址或子网从地址确定过程中被除去,离应用服务器最近的非受信 IP
地址被当作客户端 IP
地址。
3.Number类型
将代理服务器前第 n 跳当作客户端。
4.Function类型
定制实现,只有在您知道自己在干什么时才能这样做。
app.set('trust proxy', function (ip) {
if (ip === '127.0.0.1' || ip === '123.123.123.123') return true; // 受信的 IP 地址
else return false;
})
设置 trust proxy 为非假值会带来三个重要变化:
- req.hostname的值是从X-Forwarded-Host请求头中设置的值派生的,该值可以由客户端或代理设置。
- 无论是 HTTP、HTTPS 或者是无效的名称,都可以通过反向代理来设置 X-Forwarded-Proto通知应用程序。这个值是通过 req.protocol 来反应的。
- req.ip 和 req.ips 的值将会由X-Forwarded-For 中列出的 IP 地址构成。
trust proxy 设置由 proxy-addr 软件包实现,请参考其文档了解更多信息。
数据库操作(database integration)
关于数据库的连接,主要是针对不同类型的数据库采用相应的Node.js驱动程序。具体使用请参考:http://expressjs.com/en/guide/database-integration.html
总结
express确确实实是很经典的一个nodejs web框架,它的路由和中间件机制非常值得通过阅读源码一探究竟,我想这将会是非常有意思的过程。
express结合一些第三方库、中间件其实已经可以实现一个小型的后台服务了,通过body-parser解析请求参数对象,通过cookie-parser解析cookie,通过morgan打印日志,通过method-override兼容请求方法。。。同时通过mysql第三方npm包连接数据库。不过显然,其在实际运用当中并不多,目前比较主流还是koajs、egg.js等。并且,还需要提到的是express的模板引擎用的应该不多,毕竟目前基本都是angular、vuejs、react这些前后端分离的框架来进行页面渲染。
下一篇文章,我们将继续深入掌握express的使用。
last(最后)
非常感谢您能阅读完这篇文章,您的阅读是我不断前进的动力。对于上面所述,有什么新的观点或发现有什么错误,希望您能指出。
最后,附上个人常逛的社交平台:
知乎:https://www.zhihu.com/people/bi-an-yao-91/activities
csdn:https://blog.csdn.net/YaoDeBiAn
github: https://github.com/yaodebian
我的邮箱:2801540120@qq.com or yaodebian@gmail.com
个人目前能力有限,并没有自主构建一个社区的能力,如有任何问题或想法与我沟通,请通过上述某个平台联系我,谢谢!!!