根据视频(opens new window)练习 Node,基本按照视频步骤来,但也不是完全按照视频来
#初始化项目
- 生成
package.json
- 安装
express
- 生成
tsconfig.json
,如果生成不了,先全局安装 typescript,npm install typescript -g
- 安装
typescript
以及其他所需要的依赖包 - 创建
index.ts
文件
执行命令:
pnpm init -y
pnpm add express
tsc --init
pnpm add @types/node @types/express nodemon ts-node typescript -D
touch index.ts
1
2
3
4
5
2
3
4
5
- 修改
package.json
启动函数
{
"scripts": {
"dev": "nodemon --exec ts-node ./index.ts"
}
}
1
2
3
4
5
2
3
4
5
- 在 index.ts 中写如下初始代码
import type { Request, Response } from 'express';
import express from 'express';
const app = express();
// 解析x-www-form-urlencoded
app.use(express.urlencoded({ extended: false }));
app.get('/', (req: Request, res: Response) => {
res.send('Hello World');
});
app.listen(3000, () => {
console.log('端口在3000端口已启动');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 执行命令
pnpm dev
,到浏览器查看3000
端口
#初始化路由
- 新建
router
文件夹,然后新建user.ts
文件 - 写
user.ts
文件并在index.ts
中导入
// user.ts
import express from 'express';
const router = express.Router();
router.post('/register', (req, res) => {
res.send('注册路由');
});
router.post('/login', (req, res) => {
res.send('登录路由');
});
export default router;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
// index.ts
// ...
import router from './router/user';
// 注册路由
app.use('/api', router);
1
2
3
4
5
6
2
3
4
5
6
#开发登录注册接口
- 在 mysql 中新建表名为
users
同时定义 6 个字段,分别为id
、username
、password
、nick_name
、email
、user_pic
- 安装
mysql2
pnpm add mysql2
1
- 新建
db
文件夹并在文件夹下新建index.ts
- 在
index.ts
下连接数据库
import mysql2 from 'mysql2';
const db = mysql2.createPool({
host: 'localhost',
user: 'root',
database: 'mydb',
password: 'admin123',
});
export default db;
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
#实现注册接口
- 新建
controller
并下文件夹下新建user.ts
- 在
router/uesr.ts
中引入路由处理的逻辑函数 - 安装
bcrypt
包用于密码加密 - 书写逻辑代码
import type { Request, Response } from 'express';
import type { RowDataPacket } from 'mysql2';
import db from '../db';
interface UserTable extends RowDataPacket {
id: number;
username: string;
password: string;
email?: string;
nick_name?: string;
user_pic?: string;
}
export const register = (req: Request, res: Response) => {
if (!req.body.username || !req.body.password) {
return res.send({ message: '账号或密码不能为空' });
}
const sql = `select * from users where username=?`;
db.query<UserTable[]>(sql, req.body.username, (err, results) => {
if (err) {
res.send({ message: err.message });
return;
}
if (results.length > 0) {
res.send({ message: '用户名已存在' });
return;
}
req.body.password = bcrypt.hashSync(req.body.password, 10);
// 插入语句,注意如果密码长度设置太小,那么有可能出现插入失败的情况,因此需要修改密码的可允许输入长度
// 例如alter table users modify password varchar(100)
const sql = `insert into users set ?`;
db.query<OkPacket>(
sql,
{ username: req.body.username, password: req.body.password },
(err, results) => {
if (err) {
return res.send({ message: err.message });
}
if (results.affectedRows !== 1) {
return res.send({ message: '注册失败' });
}
return res.send({ message: '注册成功' });
}
);
});
};
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
47
48
49
50
51
52
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
47
48
49
50
51
52
#优化返回值
- 新增
types
文件夹并创建module.d.ts
文件,并写下
declare namespace Express {
export interface Response {
cc: (err: string | Error, status?: number) => void;
}
}
1
2
3
4
5
2
3
4
5
- 在
tsconfig.json
中配置,其中ts-node
的配置特别重要
{
"ts-node": {
"files": true
},
"compilerOptions": {
"typeRoots": ["node_modules/@types", "./types"]
}
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- 在
index.ts
中写入中间处理函数,注意在路由之前
app.use((req, res, next) => {
res.cc = (err: string | Error, status = 1) => {
res.send({
status,
err: err instanceof Error ? err.message : err,
});
};
next();
});
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- 使用
res.cc
返回,例如:
export const register = (req: Request, res: Response) => {
if (!req.body.username || !req.body.password) {
return res.cc('账号或密码不能为空');
}
// ...
};
1
2
3
4
5
6
7
2
3
4
5
6
7
#表单校验
对比了一下 express 表单校验的相关包,选择了express-validator
,没有选择视频中的表单验证包
- 安装
pnpm add express-validator
- 创建
schema
文件夹并新建user.ts
文件,写下登录校验
import { body } from 'express-validator';
const username = body('username').notEmpty().withMessage('用户名不为空').isLength({ min: 1, max: 10 }).withMessage('用户名最大10个字符');
const password = body('password').matches(/^\S{6,12}$/).withMessage('密码为6到12位的非空字符串');
export const registerSchema = [username, password];
1
2
3
4
5
6
2
3
4
5
6
- 创建
middleware
文件夹并创建validator.ts
,统一拦截
import type { Request, Response, NextFunction } from 'express';
import { validationResult } from 'express-validator';
export function validateRequestSchema(
req: Request,
res: Response,
next: NextFunction
) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.cc(errors.array());
}
next();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 在
router/user.ts
中写下如下代码
import express from 'express';
import { register, login } from '../controller/user';
import { validateRequestSchema } from '../middleware/validator';
import { registerSchema } from '../schema/user';
const router = express.Router();
router.post('/register', registerSchema, validateRequestSchema, login);
export default router;
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- 在
controller/uesr.ts
中就可以去掉判断用户名密码的格式问题
#实现登录接口
- 和注册一样需要加表单验证
router.post('/login', registerSchema, validateRequestSchema, login);
1
- 查询是否存在用户名,存在的情况下密码是否一致
- 创建一个config.ts文件,加入属性配置项,生成token值,需要安装
jsonwebtoken
,执行pnpm add jsonwebtoken
和pnpm add @types/jsonwebtoken -D
// config.ts
const config = {
secretKey: '^_^', // token加密密钥
expiresIn: '1h' // 过期时间
}
export default config;
1
2
3
4
5
6
7
2
3
4
5
6
7
// controller/uesr.ts
import JsWebToken from 'jsonwebtoken';
import config from '../config';
// ...
export const login = (req: Request, res: Response) => {
const sql = `select * from users where username=?`;
db.query<UserTable[]>(sql, req.body.username, (err, results) => {
if (err) {
return res.cc(err);
}
if (results.length !== 1) {
return res.cc('登录失败');
}
const isSamePassword = bcrypt.compareSync(req.body.password, results[0].password);
if (!isSamePassword) {
return res.cc('密码错误');
}
const user: UserTable = { ...results[0], password: '', email: '', nick_name: '' };
const token = JsWebToken.sign(user, config.secretKey, { expiresIn: config.expiresIn });
res.send({
status: 0,
token: `Bearer ${token}`,
message: '登录成功'
});
});
}
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
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
- 统一入口判断用户除API接口外是否存在token,执行
pnpm add express-jwt
// ...
import { expressjwt } from 'express-jwt';
import config from './config';
// 路由之前
app.use(
expressjwt({ secret: config.secretKey, algorithms: ['HS256'] }).unless({
path: /^\/api/,
})
);
// 路由之后
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err.name === 'UnauthorizedError') {
return res.cc('没有权限');
}
res.cc(err);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#个人信息
#获取用户信息
- 新建
router/userinfo.ts
import express from 'express';
import { userInfoHandle } from '../controller/userinfo';
const router = express.Router();
router.get('/userinfo', userInfoHandle);
export default router;
1
2
3
4
5
6
7
2
3
4
5
6
7
- 新建
controller/userinfo.ts
import type { Response } from 'express';
import type { UserTable } from './user';
import type { Request } from 'express-jwt';
import db from '../db';
export const userInfoHandle = (req: Request, res: Response) => {
const sql = `select id, username, email, nick_name, user_pic from users where id=?`;
db.query<UserTable[]>(sql, req.auth!.id, (err, results) => {
if (err) {
return res.cc(err);
}
if (results.length !== 1) {
return res.cc('查询用户失败');
}
res.send({
status: 0,
data: results[0]
});
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 在
index.ts
中引入
import userinfoRouter from './router/userinfo';
app.use('/my', userinfoRouter);
1
2
2
#更新用户信息
router/userinfo.ts
新增路由
import { userInfoUpdate } from '../controller/userinfo';
import { validateRequestSchema } from '../middleware/validator';
import { userInfoSchema } from '../schema/user';
router.post('/userinfo', userInfoSchema, validateRequestSchema, userInfoUpdate)
1
2
3
4
5
2
3
4
5
schema/user.ts
新增用户校验函数
const id = body('id').notEmpty().withMessage('id不为空').isInt().withMessage('id为整数');
const nickName = body('nick_name').notEmpty().withMessage('名称不为空');
const email = body('email').notEmpty().withMessage('邮箱不为空').isEmail().withMessage('邮箱格式错误');
export const userInfoSchema = [id, nickName, email];
1
2
3
4
5
2
3
4
5
controller/userinfo.ts
新增路由处理函数
export const userInfoUpdate = (req: Request, res: Response) => {
const sql = `update users set nick_name=?,email=? where id=?`;
db.query<OkPacket>(sql, [req.body.nick_name, req.body.email, req.body.id], (err, results) => {
if (err) {
return res.cc(err);
}
if (results.affectedRows !== 1) {
return res.cc('更新信息失败');
}
return res.cc('更新信息成功', 0);
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
#更新密码
- 设置路由
import { pwdHandle } from '../controller/userinfo';
import { validateRequestSchema } from '../middleware/validator';
import { pwdSchema } from '../schema/user';
router.put('/update/pwd', pwdSchema, validateRequestSchema, pwdHandle);
export default router;
1
2
3
4
5
6
7
2
3
4
5
6
7
- 设置校验规则
const oldPwd = body('old_password').matches(/^\S{6,12}$/).withMessage('旧密码错误');
const newPwd = body('new_password').matches(/^\S{6,12}$/).withMessage('新密码为6到12位的非空字符串').custom((password, { req }) => {
if (password === req.body.old_password) {
throw new Error('新旧密码一致');
}
return password;
});
export const pwdSchema = [oldPwd, newPwd];
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- 设置路由处理函数
export const pwdHandle = (req: Request, res: Response) => {
const sql = `select * from users where id=?`;
db.query<UserTable[]>(sql, req.auth!.id, (err, results) => {
if (err) {
return res.cc(err);
}
if (results.length !== 1) {
return res.cc('更新密码失败');
}
const oldPwd = req.body.old_password;
const isSamePassword = bcrypt.compareSync(oldPwd, results[0].password);
if (!isSamePassword) {
return res.cc('旧密码错误');
}
const sql = `update users set password=? where id=?`;
const newPwd = bcrypt.hashSync(req.body.new_password, 10);
db.query<OkPacket>(sql, [newPwd, req.auth!.id], (err, results) => {
if (err) {
return res.cc(err);
}
if (results.affectedRows !== 1) {
return res.cc('更新密码失败');
}
return res.cc('更新密码成功', 0);
});
});
}
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
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
#更新头像
- 更新路由
router.put('/update/avatar', avatarSchema, validateRequestSchema, avatarHandle);
1
- 设置校验函数
const avatar = body('avatar').isDataURI().withMessage('头像为base64的字符串');
export const avatarSchema = [avatar];
1
2
3
2
3
- 设置处理函数
export const avatarHandle = (req: Request, res: Response) => {
const sql = `update users set user_pic=? where id=?`;
db.query<OkPacket>(sql, [req.body.avatar, req.auth!.id], (err, results) => {
if (err) {
return res.cc(err);
}
if (results.affectedRows !== 1) {
return res.cc('更新头像失败');
}
return res.cc('更新头像成功', 0);
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#文章类别
- 创建一个
artical_category
的数据表,字段分别为id
、name
、alias
、is_delete
#获取文章分类
- 创建
router/artical
路由表
import express from 'express';
import { getCategoryHandle } from '../controller/artical';
const router = express.Router();
router.get('/category', getCategoryHandle);
export default router;
1
2
3
4
5
6
7
2
3
4
5
6
7
- 在
index.ts
中导入路由
import articalRouter from './router/artical';
// ...
app.use('/my/artial', articalRouter);
1
2
3
4
2
3
4
- 在
controller
新建artical.ts
文件,新增路由处理功能
import type { Response } from 'express';
import type { Request } from 'express-jwt';
import type { RowDataPacket } from 'mysql2';
import db from '../db';
export interface ArticalCategory extends RowDataPacket {
id: number;
name: string;
alias: string;
is_delete: 0 | 1;
}
export const getCategoryHandle = (req: Request, res: Response) => {
const sql = `select * from artical_category where is_delete=0 order by id asc`;
db.query<ArticalCategory[]>(sql, (err, results) => {
if (err) {
return res.cc(err);
}
res.send({
status: 0,
data: results
});
});
}
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
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
#创建文章分类
- 创建路由
router.post('/category', artialSchema, validateRequestSchema, postCategoryHandle);
1
- 新增校验函数,新增
schema/artial.ts
文件
import { body } from 'express-validator';
const name = body('name').notEmpty().withMessage('文章名称不为空');
const alias = body('alias').notEmpty().withMessage('文章别名不为空');
export const artialSchema = [name, alias];
1
2
3
4
5
6
2
3
4
5
6
- 新增路由处理函数
export const postCategoryHandle = (req: Request, res: Response) => {
const sql = `select * from artical_category where name=? or alias=?`;
const { name, alias } = req.body;
db.query<ArticalCategory[]>(sql, [name, alias], (err, results) => {
if (err) {
return res.cc(err);
}
if (results.length === 2) {
return res.cc('名称和别名都重复');
}
if (results.length === 1) {
if (results[0].name === name && results[0].alias === alias) {
return res.cc('名称和别名都重复');
}
return results[0].name === name ? res.cc('文章名重复') : res.cc('文章别名重复')
}
const sql = `insert artical_category set ?`;
db.query<OkPacket>(sql, req.body, (err, results) => {
if (err) {
return res.cc(err);
}
if (results.affectedRows !== 1) {
return res.cc('插入失败');
}
res.send({
status: 0
});
});
});
}
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
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
#删除文章分类
- 创建路由
router.delete('/category/:id', delArticalSchema, validateRequestSchema, delCategoryHandle);
1
- 新增接口校验
import { param } from 'express-validator';
const id = param('id').notEmpty().withMessage('id不为空').isInt().withMessage('id为整数');
export const delArticalSchema = [id];
1
2
3
4
5
2
3
4
5
- 新增处理函数
export const delCategoryHandle = (req: Request, res: Response) => {
const sql = `update artical_category set is_delete=1 where id=?`;
db.query<OkPacket>(sql, req.params.id, (err, results) => {
if (err) {
return res.cc(err);
}
if (results.affectedRows !== 1) {
return res.cc('删除失败');
}
res.cc('删除成功', 0);
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
#根据id获取文章分类
- 设置路由
router.get('/category/:id', delArticalSchema, validateRequestSchema, getCategoryByIdHandle);
1
- 设置路由处理函数
export const getCategoryByIdHandle = (req: Request, res: Response) => {
const sql = `select * from artical_category where id=? and is_delete=0`;
db.query<ArticalCategory[]>(sql, req.params.id, (err, results) => {
if (err) {
return res.cc(err);
}
res.send({
status: 0,
data: results[0]
});
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
#更新
- 设置路由
router.put('/category', updateArticalSchema, validateRequestSchema, updateCategoryByIdHandle);
1
- 设置校验函数
import { id as articalId } from './user';
export const updateArticalSchema = [articalId, ...artialSchema];
1
2
2
- 设置路由处理函数
export const updateCategoryByIdHandle = (req: Request, res: Response) => {
const sql = `select * from artical_category where id!=? and is_delete=0 and (name=? or alias=?)`;
const { name, alias, id } = req.body;
db.query<ArticalCategory[]>(sql, [id, name, alias], (err, results) => {
if (err) {
return res.cc(err);
}
if (results.length === 2) {
return res.cc('名称和别名都重复');
}
if (results.length === 1) {
if (results[0].name === name && results[0].alias === alias) {
return res.cc('名称和别名都重复');
}
return results[0].name === name ? res.cc('文章名重复') : res.cc('文章别名重复')
}
const sql = `update artical_category set ? where id=?`;
db.query<OkPacket>(sql, [{ name, alias }, id], (err, results) => {
if (err) {
return res.cc(err);
}
if (results.affectedRows !== 1) {
return res.cc('更新类别失败');
}
res.cc('更新成功', 0);
});
});
}
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
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