一、为什么使用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 express-demo
  • 安装依赖

    • npm install
  • 启动项目

    • node bin/www

方式二:从零搭建自己的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
//express本质上是一个函数
const express = require('express');

// 创建服务器 app本质上也是一个函数
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服务器启动成功');
});

//有普通中间件 还是普通先响应
//第一个普通匹配中间件响应
//get,home匹配中间件

案例四:注册多个中间件

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服务器启动成功');
});
//get,home匹配中间件1
//get,home匹配中间件2
//get,home匹配中间件3
//get,home匹配中间件4

中间件的应用

如果我们想登录提交表单,可以使用中间件来简化操作:

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) => {

//{ username: 'xxx', password: '123456' }
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) => {
//{ username: 'xxx', password: '123456' }
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.json())
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

multer解析form-data

解析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) => {
//[Object: null prototype] { username: 'xxx', password: '123456' }
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) => {
//[Object: null prototype] { username: 'xxx', password: '123456' }
console.log(req.body);
});
//upload.single()处理单个文件
//多个文件可以使用upload.array()
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,
// dest: './uploads',
});

app.use(upload.any());
app.post('/login', (req, res, next) => {
//[Object: null prototype] { username: 'xxx', password: '123456' }
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
app.use(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" //服务器请求 get
::1 - - [19/Nov/2021:03:27:16 +0000] "POST /login HTTP/1.1" 200 - "-" "PostmanRuntime/7.6.0" //postman请求 post

客户端发送请求的方式

客户端传递到服务器参数的方法常见的是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) => {
//{ id: 'data', name: 'xxxx' }
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) => {
//{ name: 'yyy', age: '20' }
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('返回数据成功')
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('返回数据成功')
res.end(JSON.stringify({ name: 'xxx' }));
});

app.listen(8000, () => {
console.log('express服务器启动成功');
});

这样打印的是一个对象不是json数据

1
{"name":"xxx"}
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方法

  • 用于设置状态码:
1
res.status(300)

五、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
//users.js
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带参数不会执行下一个中间件了,而是执行错误。