精读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这些前后端分离的框架来进行页面渲染。

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

个人目前能力有限,并没有自主构建一个社区的能力,如有任何问题或想法与我沟通,请通过上述某个平台联系我,谢谢!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值