一、为什么使用web框架
前面我们已经学习了使用http内置模块来搭建Web服务器,为什么还要使用框架?
- 原生http在进行很多处理时,会较为复杂;
- 有URL判断、Method判断、参数处理、逻辑代码处理等,都需要我们自己来处理和封装;
- 并且所有的内容都放在一起,会非常的混乱;
目前在Node中比较流行的Web服务器框架是express、koa;
- express早于koa出现,并且在Node社区中迅速流行起来:
- 我们可以基于express快速、方便的开发自己的Web服务器;
- 并且可以通过一些实用工具和中间件来扩展自己功能;
Express整个框架的核心就是中间件,理解了中间件其他一切都非常简单!
二、Express安装
express的使用过程有两种方式:
- 方式一:通过express提供的脚手架,直接创建一个应用的骨架;
- 方式二:从零搭建自己的express应用结构;
方式一:安装express-generator
安装脚手架
- npm install -g express-generator
创建项目
安装依赖
启动项目
方式二:从零搭建自己的express应用结构;
- npm init -y
- npm install express
三、Express初体验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const express = require('express');
const app = express();
app.get('/', (req, res, next) => { res.end('Hello Experss'); }); app.post('/', (req, res, next) => { res.end('Hello Post Experss'); }); app.get('/login', (req, res, next) => { res.end('Hello'); });
app.listen(8000, () => { console.log('express服务器启动成功'); });
|
四、中间件
Express是一个路由和中间件的Web框架,它本身的功能非常少:
Express应用程序本质上是一系列中间件函数的调用;
中间件的本质是传递给express的一个回调函数;
这个回调函数接受三个参数:
- 请求对象(request对象);
- 响应对象(response对象);
- next函数(在express中定义的用于执行下一个中间件的函数);
中间件中可以执行哪些任务呢?
- 执行任何代码;
- 更改请求(request)和响应(response)对象;
- 结束请求-响应周期(返回数据);
- 调用栈中的下一个中间件;
如果当前中间件功能没有结束请求-响应周期,则必须调用next()将控制权传递给下一个中间件功能,否则,请求将被挂起。

如何将一个中间件应用到我们的应用程序中呢?
- express主要提供了两种方式:app/router.use和app/router.methods;
- 可以是 app,也可以是router,router我们后续再学习:
- methods指的是常用的请求方式,比如: app.get或app.post等;
- 我们先来学习use的用法,因为methods的方式本质是use的特殊情况;
案例一:最普通的中间件
1 2 3 4 5 6 7 8 9 10 11
| const express = require('express');
const app = express();
app.use((req, res, next) => { res.end('Hello Middleware'); }); app.listen(8000, () => { console.log('express服务器启动成功'); });
|
这样访问8000端口就可以了,无论加不加路径参数等
如果我们创建多个中间件,那么之后响应第一个中间件,如果想响应多个,需要next
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const express = require('express');
const app = express();
app.use((req, res, next) => { console.log('第一个中间件响应'); next() }); app.use((req, res, next) => { console.log('第二个中间件响应'); next() }); app.use((req, res, next) => { console.log('第三个中间件响应'); res.end('Hello Middleware'); }); app.listen(8000, () => { console.log('express服务器启动成功'); });
|
如果res.end(),后续不能再res.end(),否则会报错。
案例二:path匹配中间件
1 2 3 4 5 6 7 8 9 10 11 12 13
| const express = require('express');
const app = express();
app.use(('/home',(req, res, next) => { console.log('第一个路径匹配中间件响应'); res.end('Hello Home Middleware') }))
app.listen(8000, () => { console.log('express服务器启动成功'); });
|
中间件的响应永远是从第一个开始,不加next()就会停止,调用了next()就会继续向下执行
且只能调用一次res.end()
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 30
| const express = require('express');
const app = express(); app.use((req, res, next) => { console.log('第一个普通匹配中间件响应'); next(); }); app.use( ('/home', (req, res, next) => { console.log('第一个路径匹配中间件响应'); next(); }) ); app.use( ('/home', (req, res, next) => { console.log('第二个路径匹配中间件响应'); res.end('Hello Home Middleware'); }) );
app.listen(8000, () => { console.log('express服务器启动成功'); });
|
案例三:path和method匹配中间件
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
| const express = require('express');
const app = express(); app.use((req, res, next) => { console.log('第一个普通匹配中间件响应'); next(); }); app.get('/home',(req,res) => { console.log('get,home匹配中间件'); res.end('Hello') }) app.post( ('/login', (req, res, next) => { console.log('login,post匹配中间件响应'); res.end('Hello Login Middleware'); }) ); app.listen(8000, () => { console.log('express服务器启动成功'); });
|
案例四:注册多个中间件
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 30 31
| const express = require('express');
const app = express();
app.get( '/home', (req, res,next) => { console.log('get,home匹配中间件1'); next(); }, (req, res,next) => { console.log('get,home匹配中间件2'); next(); }, (req, res,next) => { console.log('get,home匹配中间件3'); next(); }, (req, res,next) => { console.log('get,home匹配中间件4'); res.end('Hello'); } );
app.listen(8000, () => { console.log('express服务器启动成功'); });
|
中间件的应用
如果我们想登录提交表单,可以使用中间件来简化操作:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const express = require('express'); const app = express(); app.use((req, res, next) => { req.on('data', (data) => { const info = JSON.parse(data.toString()); req.body = info; }); req.on('end', () => { next(); }); }); app.post('/login',(req,res,next) => { console.log(req.body); res.end('登录成功') }) app.listen(8000, () => { console.log('服务器启动成功'); });
|
但是我们可以使用expres内置的中间件或者使用body-parser来完成
1 2 3 4 5 6 7 8 9 10 11
| const express = require('express'); const app = express(); app.use(express.json()) app.post('/login',(req,res,next) => { console.log(req.body); res.end('登录成功') }) app.listen(8000, () => { console.log('服务器启动成功'); });
|
so easy
当然我们这里post采用的方式是application/json格式的
如果换成x-www-form-urlencoded:

1 2 3 4 5 6 7 8 9 10
| app.use(express.urlencoded({extended:true})) app.post('/login',(req,res,next) => { { name: 'yyy', age: '20' } console.log(req.body); res.end('登录成功') }) app.listen(8000, () => { console.log('服务器启动成功'); });
|
此时学到这里的我感觉很爽。中间件很强大
注意:
1
| express.urlencoded({extended:true})
|
true:那么对urlencoded进行解析时,它使用的是第三方库 qs
false:那么对urlencoded进行解析时,它使用的是Node内置模块:querystring
解析form-data数据需要使用到第三方库multer
如果解析的是普通数据非图片,那么可以使用any()
npm i multer

1 2 3 4 5 6 7 8 9 10 11 12
| const express = require('express'); const multer = require('multer'); const app = express(); const upload = multer(); app.use(upload.any()); app.post('/login', (req, res, next) => { console.log(req.body); }); app.listen(8000, () => { console.log('服务器启动成功'); });
|
multer解析图片上传

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const express = require('express'); const multer = require('multer'); const app = express(); const upload = multer({ dest: './uploads', });
app.use(upload.any()); app.post('/login', (req, res, next) => { console.log(req.body); });
app.post('/upload', upload.single('file'), (req, res, next) => { console.log(req.files); res.end('文件上传成功'); }); app.listen(8000, () => { console.log('服务器启动成功'); });
|
这样在当前目录的upload目录下就有了文件,但是文件默认没有后缀名:

如何自定义文件名?
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 30 31 32
| const path = require('path'); const express = require('express'); const multer = require('multer'); const app = express();
const storage = multer.diskStorage({ destination: (req, res, callback) => { callback(null, './uploads'); }, filename: (req, file, callback) => { callback(null, Date.now() + path.extname(file.originalname)); }, });
const upload = multer({ storage, });
app.use(upload.any()); app.post('/login', (req, res, next) => { console.log(req.body); }); app.post('/upload', upload.single('file'), (req, res, next) => { console.log(req.files); res.end('文件上传成功'); }); app.listen(8000, () => { console.log('服务器启动成功'); });
|
这样文件就有了后缀名,就可以正常显示了

注意upload.any() 和 req.files
上传单个文件可以使用req.file来获取信息
但此时upload.any 和 req.file冲突,官方也提示不要把upload.any()写在全局:
应该写到单独的路由里
1 2 3
| app.post('/login', upload.any(), (req, res, next) => { console.log(req.body); });
|
这样req.file就可以正常获取图片信息了:
1 2 3 4
| app.post('/upload', upload.single('file'), (req, res, next) => { console.log(req.file); res.end('文件上传成功'); });
|
1 2 3 4 5 6 7 8 9 10
| { fieldname: 'file', originalname: 'wallhaven-9m9jl1.jpg', encoding: '7bit', mimetype: 'image/jpeg', destination: './uploads', filename: '1637291417318.jpg', path: 'uploads\\1637291417318.jpg', size: 421334 }
|
如果使用upload.array() 要使用req.files
1 2 3 4
| app.post('/upload', upload.array('file'), (req, res, next) => { console.log(req.files); res.end('文件上传成功'); });
|
日志打印
将请求日志记录下来,那么可以使用express官网开发的第三方库:morgan
npm i morgan
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
| const fs = require('fs'); const express = require('express'); const morgan = require('morgan');
const writerStream = fs.createWriteStream('./logs/access.log', { flags: 'a+', });
const app = express();
app.get( '/home', morgan('combined', { stream: writerStream }), (req, res, next) => { res.end('Welcome'); } ); app.post( '/login', morgan('combined', { stream: writerStream }), (req, res, next) => { res.end('登录成功'); } ); app.listen(8000, () => { console.log('服务器启动成功'); });
|
1 2
| ::1 - - [19/Nov/2021:03:26:59 +0000] "GET /home HTTP/1.1" 200 - "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36" ::1 - - [19/Nov/2021:03:27:16 +0000] "POST /login HTTP/1.1" 200 - "-" "PostmanRuntime/7.6.0"
|
客户端发送请求的方式
客户端传递到服务器参数的方法常见的是5种:
- 方式一:通过get请求中的URL的params;
- 方式二:通过get请求中的URL的query;
- 方式三:通过post请求中的body的json格式(中间件中已经使用过);
- 方式四:通过post请求中的body的x-www-form-urlencoded格式(中间件使用过);
- 方式五:通过post请求中的form-data格式(中间件中使用过);
params
请求url为http://localhost:8000/products/data/xxxx
1 2 3 4 5 6 7 8 9 10 11 12 13
| const express = require('express');
const app = express(); app.get('/products/:id/:name',(req,res,next) => { console.log(req.params); res.end('返回数据成功') })
app.listen(8000, () => { console.log('express服务器启动成功'); });
|
query
请求url:http://localhost:8000/login?name=yyy&age=20
1 2 3 4 5 6 7 8 9 10 11 12 13
| const express = require('express');
const app = express(); app.get('/login',(req,res,next) => { console.log(req.query); res.end('返回数据成功') })
app.listen(8000, () => { console.log('express服务器启动成功'); });
|
响应数据
end方法
- 类似于http中的response.end方法,用法是一致的 ,想要传递对象等用法较复杂
1 2 3 4 5 6 7 8 9 10 11 12 13
| const express = require('express');
const app = express(); app.get('/login',(req,res,next) => { console.log(req.query); res.end({name:'xxx'}) })
app.listen(8000, () => { console.log('express服务器启动成功'); });
|
会报错:
1
| TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Object
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| const express = require('express');
const app = express(); app.get('/login', (req, res, next) => { console.log(req.query); res.end(JSON.stringify({ name: 'xxx' })); });
app.listen(8000, () => { console.log('express服务器启动成功'); });
|
这样打印的是一个对象不是json数据
1 2
| res.type('application/json'); res.end(JSON.stringify({ name: 'xxx' }));
|
这样返回的才是json数据
写起来很麻烦
json方法
- json方法中可以传入很多的类型:object、array、string、boolean、number、null等,它们会被转换成
json格式返回;
1
| res.json({ name: 'xxx', age: 20 });
|
返回的直接就是json数据格式
1 2 3 4
| { "name": "xxx", "age": 20 }
|
status方法
五、Express路由
如果我们将所有的代码逻辑都写在app中,那么app会变得越来越复杂:
- 一方面完整的Web服务器包含非常多的处理逻辑;
- 另一方面有些处理逻辑其实是一个整体,我们应该将它们放在一起:比如对users相关的处理
- 获取用户列表;
- 获取某一个用户信息;
- 创建一个新的用户;
- 删除一个用户;
- 更新一个用户;
我们可以使用 express.Router来创建一个路由处理程序:
- 一个Router实例拥有完整的中间件和路由系统;
- 因此,它也被称为 迷你应用程序(mini-app);
新建roters目录并新建路由文件如users.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const express = require('express'); const router = express.Router();
router.get('/', (req, res, next) => { res.json(['why', 'kobe', 'lilei']); });
router.get('/:id', (req, res, next) => { res.json(`${req.params.id}用户信息`); });
router.post('/', (req, res, next) => { res.json('create user success'); }); module.exports = router;
|
app中导入:
1 2 3 4 5 6 7
| const express = require('express'); const app = express(); const userRouter = require('./routers/users'); app.use('/users', userRouter); app.listen(8000, () => { console.log('服务器启动成功'); });
|
六、静态资源服务器
部署静态资源我们可以选择很多方式:
- Node也可以作为静态资源服务器,并且express给我们提供了方便部署静态资源的方法;
1 2 3 4 5 6 7 8 9
| const express = require('express'); const app = express();
app.use(express.static('静态文件根目录'))
app.listen(8000, () => { console.log('服务器启动成功'); });
|
七、服务端的错误处理
每次请求服务可能因为某些原因而发生错误,比如用户登录密码错误,用户注册用户名已经存在,如果单独的在路由里处理就会很臃肿,所以最好进行统一的错误处理。
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| const express = require('express'); const app = express();
const USERNAME_DOES_NOT_EXTISITES = 'USERNAME_DOES_NOT_EXTISITES'; const USERNAME_ALREADY_EXISTS = 'USERNAME_ALREADY_EXISTS';
app.post('/login', (req, res, next) => { const isLogin = false; if (isLogin) { res.json('user login success'); } else { next(new Error(USERNAME_DOES_NOT_EXTISITES)); } }); app.post('/regeister', (req, res, next) => { const isLogin = false; if (isLogin) { res.json('user regeister success'); } else { next(new Error(USERNAME_ALREADY_EXISTS)); } }); app.use((err, req, res, next) => { let status = 400; let message = ''; switch (err.message) { case USERNAME_DOES_NOT_EXTISITES: message = 'USERNAME_DOES_NOT_EXTISITES'; break; case USERNAME_ALREADY_EXISTS: message = 'USERNAME_ALREADY_EXISTS'; break; default: message = 'NOT FOUND'; } res.status(status) res.json({ errCode: status, errMessage: message, }); });
app.listen(8000, () => { console.log('服务器启动成功'); });
|
next传递的参数就是错误信息
当next传递参数时,app.use()第一个参数就是err,收集错误信息。
next带参数不会执行下一个中间件了,而是执行错误。