简介

Express 是一基于Node的一个框架,用来快速创建Web服务的一个工具,为什么要使用Express呢,因为创建Web服务如果从Node开始有很多繁琐的工作要做,而Express为你解放了很多工作,从而让你更加关注于逻辑业务开发。

举个例子,创建一个很简单的网站:

  1. 使用Node来开发:
  1. var http = require('http');
  2. http.createServer(function(req, res) {
  3. res.writeHead(200, {
  4. 'Content-Type': 'text/plain'
  5. });
  6. res.end('Hello World')
  7. }).listen(3000)

这是一个简单的 Hello World,但实际上真正的网站要比这个复杂很多,主要有:

(1) 多个页面的路由功能

(2) 对请求的逻辑处理

那么使用node原生写法就要进行以下处理

  1. var http = require('http')
  2. var url = require('url')
  3. var app = http.createServer(function (req, rep) {
  4. //req.url: 访问路径
  5. var urlObj = url.parse(req.url);
  6. switch (urlObj.pathname) {
  7. case '/':
  8. //首页
  9. rep.writeHead(200, {
  10. 'content-type': 'text/html;charset=utf-8'
  11. })
  12. rep.end('<h1>这是首页</h1>');
  13. break;
  14. case '/user':
  15. //个人中心
  16. rep.writeHead(200, {
  17. 'content-type': 'text/html;charset=utf-8'
  18. })
  19. rep.end('<h1>这是个人中心</h1>');
  20. break;
  21. default:
  22. //处理其他情况
  23. rep.writeHead(404, {
  24. 'content-type': 'text/html;charset=utf-8'
  25. })
  26. rep.end('<h1>页面不见了</h1>');
  27. break;
  28. }
  29. })
  30. app.listen(3000, 'localhost')

代码里在createServer函数里传递一个回调函数用来处理http请求并返回结果,在这个函数里有两个工作要做:

(1)路由分析,对于不同的路径需要进行分别处理

(2)逻辑处理和返回,对某个路径进行特别的逻辑处理

如果一个大型网站拥有海量的页面,每个页面的处理逻辑也是交错复杂,那这里的写法会非常混乱,没法维护,为了解决这个问题,TJ提出了Connect的概念,把Java里面的中间件概念第一次进入到JS的世界,Web请求将一个一个经过中间件,并通过其中一个中间件返回,大大提高了代码的可维护性和开发效率。

  1. // 引入connect模块
  2. var connect = require("connect");
  3. var http = require("http");
  4. // 建立app
  5. var app = connect();
  6. // 添加中间件
  7. app.use(function(request, response) {
  8. response.writeHead(200, { "Content-Type": "text/plain" });
  9. response.end("Hello world!");
  10. });
  11. 启动应用 http.createServer(app).listen(3000);

但是TJ认为还应该更好一点,于是Express诞生了,通过Express开发以上的例子:

  1. 使用Express来开发:
  1. var express = require('express');
  2. var app = express();
  3. app.get('/', function (req, res) {
  4. res.send('Hello World!');
  5. });
  6. app.get('/about', function (req, res) {
  7. res.send('About');
  8. });
  9. var server = app.listen(3000, function () {
  10. var host = server.address().address;
  11. var port = server.address().port;
  12. console.log('Example app listening at http://%s:%s', host, port);
  13. });

从Express例子可以看出,使用Express大大减少了代码,而且逻辑更为简洁,所以使用Express可以提高开发效率并降低项目维护成本。

安装使用

1.手动安装

  1. npm init
  2. npm install express || yarn add express
  1. var express = require('express');
  2. var app = express();
  3. app.get('/', function (req, res) {
  4. res.send('Hello World!');
  5. });
  6. var server = app.listen(3000, function () {
  7. var host = server.address().address;
  8. var port = server.address().port;
  9. console.log('Example app listening at http://%s:%s', host, port);
  10. });

2.通过express-generator生成express项目

  1. npm install express-generator -g
  2. express --ejs express_demo || . // .代表在当前目录生成项目
  3. cd express_demo
  4. npm install || yarn
  5. #运行项目
  6. npm start || node ./bin/www

源码结构

在 Express4.x 的版本中,已经移除了connect模块,Express内部自己实现了connect的模块,并进行了一些增强处理。这里对Express 源码进行一些简单的说明。

首先我们看一下Express的源码结构:

Express - 图1

  1. middleware: 中间件
  2. init.js 初始化requestresponse
  3. query.js 格式化url,将url中的rquest参数剥离, 储存到req.query
  4. router: 路由
  5. index.js Router类,用于存储中间件数组
  6. layer.js 中间件实体类
  7. route.js Route类,用于处理不同Method
  8. application.js 对外API
  9. express.js 入口
  10. request.js 请求增强
  11. response.js 返回增强
  12. utils.js 工具函数
  13. view.js 模版相关

Express中的中间件和connect中不太一样,因为Express有两种中间件,普通中间件、路由中间件

Express - 图2

app初始化时,会push两个中间件(init,query)进router.stack里。我们可以通过app.use往app添加非路由中间件,也可以通过app[METHOD]添加路由中间件。

普通中间件:

使用app.use方法的时候,会通过lazyrouter()方法实例化一个Router对象,在整个app中只有一个Router对象。最终调用router.use()方法,把 Layer 添加到 Router stack 中,且这个 Layer 的 route属性为undefined。

路由中间件:

使用app[METHOD]方法的时候,同样会把这个中间件添加到 Router对象的 stack 中, 但是这个 Layer 的 route属性会指向一个实例化的 Route 对象, 在Route里也有一个实例化的 Layer,且放在stack里,与Router的 Layer不同的是,Route的没有layer.route 且 layer.method 存放了http方法。

总结:

  • express 中添加中间件方法有 app.use 和 app[METHOD] ,当然还有内置的 Router 类,app.use 用来添加非路由中间件,app[METHOD] 用来添加路由中间件。
  • Layer 类封装中间的 path 和 handle (处理函数)
  • Router 和 Route都有对应的 stack,但是 Route 在整个 app 中只有一个,而 Route 可以有多个。放在Router
    stack 里的路由中间件,通过Layer.route 指向 Route,与 Route stack 相关联起来。

运行原理

Express - 图3

基本使用

示例代码: https://gitee.com/gongyz/blog_express/tree/study/

路由

路由定义了应用程序如何响应客户端的请求,这些请求是由一个 URI + HTTP请求方法 组成,每个路由都可以指定一个或者多个处理函数,处理函数会在匹配路由时执行。

路由的定义采用以下的结构:app.METHOD(PATH, HANDLER)

1、基本使用

  1. // 默认路由,基础路径为 '/'
  2. app.get('/', function (req,rep,next) {
  3. rep.send('Hello World!')
  4. })
  5. app.get('/user', function (req,rep,next) {
  6. rep.send('user')
  7. })
  8. // 也可以指定多个处理函数
  9. app.get('/user',fn1, fn2, ... function (req,rep,next) {
  10. rep.send('user')
  11. })
  12. app.get('/user/:name/:group', function (req,rep,next) {
  13. console.log(rep.params)
  14. next()
  15. })
  16. // 创建子路由,并且对子路由进行配置
  17. var router = express.Router({
  18. mergeParams: true,
  19. caseSensitive: true,
  20. strict: true
  21. });
  22. // 注册子路由,基础路径为 /user/:name/:group
  23. app.use('/user/:name/:group', router)
  24. // /user:name:group 和 /user:name:group/ 都可以匹配
  25. router.get('/', function (req, rep, next) {
  26. rep.send(req.params)
  27. })
  28. // /user:name:group/test
  29. router.get('/test', function (req, rep, next) {
  30. rep.send('router test')
  31. })

2、路由路径

路由路径可以是字符串、字符串匹配模式或正则表达式,详细的可以查看官方文档关于路由 这一节。

  1. /users/:userId/books/:bookId
  2. /abc?d 0次或1
  3. /abc+d 1次或多次
  4. /abc\*d c~d之间任意字符
  5. /a(bc)?d
  6. /a(bc)+d
  7. /\/ab[1,2]\/cd/ 正则匹配
  8. [/abc?d, /a(bc)?d] // 多个匹配
  9. app.get(/\/ab[1,2]\/cd/,function (req,res,next) {
  10. res.send('finish')
  11. })

3、app.all()

  1. // router.all() 所有匹配到该路由路径的HTTP方法都会执行处理函数
  2. router.all(function(req, res, next) {
  3. // runs for all HTTP verbs first
  4. // think of it as route specific middleware!
  5. next();
  6. })

4、app.param()

  1. // 普通写法对请求参数进行处理
  2. app.get('/user/:id', function (req, res, next) {
  3. if (req.params.id !== '1') {
  4. res.send(404)
  5. } else {
  6. res.send('success')
  7. }
  8. })
  9. // 使用app.param添加一个拦截器,对请求参数进行处理,下面的路由用来处理正确的请求
  10. app.param('id', function (req, res, next, id) {
  11. if (req.params.id !== '1') {
  12. res.send(404)
  13. } else {
  14. next()
  15. }
  16. })
  17. app.get('/user/:id', function (req, res, next) {
  18. res.send('success')
  19. })
  20. // 多个参数时的写法
  21. // 回调函数会执行两次,等同于下面的写法,建议采用下面的写法分开写
  22. app.param(['id', 'name'], function (req, res, next, value) {
  23. console.log(value)
  24. next()
  25. })
  26. // 第一次
  27. app.param('id', function (req, res, next, id) {
  28. console.log(id)
  29. next()
  30. })
  31. // 第二次
  32. app.param('name', function (req, res, next, name) {
  33. console.log(name)
  34. next()
  35. })
  36. app.get('/user/:id/:name', function (req, res, next) {
  37. res.send('success')
  38. })
  39. // router.param 用法和 app.param一样,不同的是router.param不支持['id', 'name']接收参数

5、app.route()

  1. // 使用 router.route() 方法避免对同一个路径重复命名
  2. router.route('/users/:user_id')
  3. .all(function(req, res, next) {
  4. // runs for all HTTP verbs first
  5. // think of it as route specific middleware!
  6. next();
  7. })
  8. .get(function(req, res, next) {
  9. res.json(req.user);
  10. })
  11. .post(function(req, res, next) {
  12. next(new Error('not implemented'));
  13. })
  14. .put(function(req, res, next) {
  15. // just an example of maybe updating the user
  16. req.user.name = req.params.name;
  17. // save user ... etc
  18. res.json(req.user);
  19. })
  20. .delete(function(req, res, next) {
  21. next(new Error('not implemented'));
  22. });

静态资源访问

express内部引用了 serve-static 这个库,并且挂载到了express的static方法上。在后面的响应部分也介绍了在不使用 express.static 方法情况下如何实现静态资源访问。

  1. // 基本用法
  2. app.use(express.static(path.join(__dirname, 'public')));
  3. // 自定义配置
  4. app.use(express.static('public', {
  5. index: 'index.html', // ['index.html', 'index.htm'] 指定默认的首页
  6. dotfiles: 'allow', // 是否.XXX
  7. extensions:['html', 'htm'] // 配置扩展名
  8. }))

获取客户端请求数据

1、获取URL中的数据

  1. app.get('/index/:id', function (req,res,next) {
  2. res.send(`
  3. <ul>
  4. <li>req.methed = ${req.method}</li>
  5. <li>req.hostnam = ${req.hostname}</li>
  6. <li>req.originalUrl = ${req.originalUrl}</li>
  7. <li>req.path = ${req.path}</li>
  8. <li>req.protocol = ${req.protocol}</li>
  9. <li>req.query = ${JSON.stringify(req.query)}</li>
  10. <li>req.params= ${JSON.stringify(req.params)}</li>
  11. </ul>
  12. `)
  13. })

2、获取 headers 中的数据

  1. app.get('/index', function (req,res) {
  2. res.send(req.headers)
  3. })

3、获取 body 中的数据

  1. 表单提交编码方式常用的有三种
  2. application/x-www-form-urlencoded 默认
  3. text/plain
  4. multipart/form-data // 该类型需要使用 Multer 中间件处理,下面会介绍
  5. // 引入 body-parse 模块,express支持 json、urlencoded、text 方法
  6. // express内部已经引入了 body-parse 模块,并且把两个常用的方法json、urlencoded方法挂载到了express实例上
  7. // 处理 Content-Type 为 application/x-www-form-urlencoded 类型的请求
  8. app.use(bodyParser.urlencoded());
  9. // 处理 Content-Type 为 text/plain 类型的请求
  10. app.use(bodyParser.text());
  11. // 处理 Content-Type 为 application/json 类型的请求
  12. app.use(bodyParser.json());
  13. // 使用 Postman测试
  14. app.post('/test', function (req,res) {
  15. res.send(req.body)
  16. })

4、获取上传文件的数据

这里需要用到 Multer 中间件,用于处理 multipart/form-data 类型的表单数据,它主要用于上传文件。

注意: Multer 不会处理任何非 multipart/form-data 类型的表单数据。

安装

  1. $ npm install --save multer

使用

Multer 会添加一个 body 对象 以及 filefiles 对象 到 express 的 request 对象中。 body 对象包含表单的文本域信息,filefiles 对象包含对象表单上传的文件信息。

警告: 确保你总是处理了用户的文件上传。永远不要将 multer 作为全局中间件使用,因为恶意用户可以上传文件到一个你没有预料到的路由,应该只在你需要处理上传文件的路由上使用。

基本使用方法:

  1. /*
  2. Multer 接受一个 options 对象,其中最基本的是 dest 属性,
  3. 这将告诉 Multer 将上传文件保存在哪。如果你省略 options对象,
  4. 这些文件将保存在内存中,永远不会写入磁盘。
  5. */
  6. // var upload = multer({ dest: 'uploads/' })
  7. // 内存存储
  8. // var storage = multer.memoryStorage()
  9. // 磁盘存储
  10. var storage = multer.diskStorage({
  11. destination: function (req, file, cb) {
  12. cb(null, 'uploads')
  13. },
  14. filename: function (req, file, cb) {
  15. cb(null, file.originalname + '-' + Date.now())
  16. }
  17. })
  18. // 创建上传中间件对请求进行拦截,处理上传的文件
  19. // 设置一个函数来控制什么文件可以上传以及什么文件应该跳过
  20. function fileFilter (req, file, cb) {
  21. // 这个函数应该调用 cb 用 boolean 值来指示是否应接受该文件
  22. if (file.mimetype === 'image/png') {
  23. // 接受这个文件,使用`true`
  24. cb(null, true)
  25. } else {
  26. // 拒绝这个文件,使用false
  27. cb(null, false)
  28. // 拒绝同时抛出错误给express处理
  29. cb(new Error('file type illegal'),false)
  30. }
  31. }
  32. var upload = multer({ storage: storage, fileFilter: fileFilter })
  33. // 只接受文本域
  34. app.post('/upload', upload.none(), function (req,res) {
  35. res.send(req.body)
  36. })
  37. // 接受文本域和一切上传的文件,文件数组将保存在 req.files
  38. app.post('/upload', upload.any(), function (req,res) {
  39. console.log('req.body', req.body)
  40. console.log('req.file', req.file)
  41. console.log('req.files', req.files)
  42. res.send('请求成功')
  43. })
  44. // 处理单个以 fieldname 命名的文件,fieldname 由表单指定, 文件的信息保存在 req.file
  45. app.post('/upload', upload.single('file'), function (req,res) {
  46. console.log('req.body', req.body)
  47. console.log('req.file', req.file)
  48. console.log('req.files', req.files)
  49. res.send('请求成功')
  50. })
  51. // 处理多个以 fieldname 命名的文件,文件 fieldname 相同, 可以配置 maxCount 来限制上传的最大数量,文件的信息保存在 req.files
  52. app.post('/upload', upload.array('file', 3), function (req,res) {
  53. console.log('req.body', req.body)
  54. console.log('req.file', req.file)
  55. console.log('req.files', req.files)
  56. res.send('请求成功')
  57. })
  58. // 处理不同 fieldname 命名的文件,文件的信息保存在 req.files
  59. fields 应该是一个对象数组,具有 name 和可选的 maxCount 属性
  60. let fields = [
  61. { name: 'file', maxCount: 1 },
  62. { name: 'file2', maxCount: 2 }
  63. ]
  64. app.post('/upload', upload.fields(fields), function (req,res) {
  65. console.log('req.body', req.body)
  66. console.log('req.file', req.file)
  67. console.log('req.files', req.files)
  68. res.send('请求成功')
  69. })

响应

1、基本方式的响应

  1. app.get('/txt',function (req,res) {
  2. res.send('my name is gongyz');
  3. })
  4. app.get('/json',function (req,res) {
  5. res.send({name: 'gongyz', age: 23});
  6. })
  7. app.get('/html', function(req,res) {
  8. res.send('<p style="color: red">Hello World</p>')
  9. })
  10. app.get('/download',function (req,res) {
  11. res.download('public/download.txt');
  12. })
  13. app.get('/redirect',function (req,res) {
  14. res.redirect('http://www.baidu.com')
  15. })
  16. // 静态资源访问
  17. app.get('/file/:name', function (req, res, next) {
  18. var options = {
  19. root: __dirname + '/public/',
  20. dotfiles: 'deny',
  21. headers: {
  22. 'x-timestamp': Date.now(),
  23. 'x-sent': true
  24. }
  25. }
  26. var fileName = req.params.name;
  27. res.sendFile(fileName, options, function (err) {
  28. if (err) {
  29. next(err)
  30. } else {
  31. console.log('Sent:', fileName)
  32. }
  33. })
  34. })

2、动态页面渲染

  1. // view engine setup
  2. app.set('views', path.join(__dirname, 'views'));
  3. app.set('view engine', 'ejs');
  4. var indexRouter = require('./routes/index');
  5. var usersRouter = require('./routes/users');
  6. app.use('/' ,indexRouter)
  7. app.use('/user' ,usersRouter)
  8. /* GET home page. */
  9. // router.get('/', function(req, res, next) {
  10. // var arr =[1,2,3,4,5,6,7,8]
  11. // res.render('index', { title: 'index', arr });
  12. // });
  13. // 实际项目中的做法
  14. var db = {
  15. getData (req,res,next) {
  16. var arr =[1,2,3,4,5,6,7,8]
  17. res.locals = {title: 'index', arr}
  18. next()
  19. }
  20. }
  21. router.get('/',db.getData, function(req, res, next) {
  22. res.render('index');
  23. });