Node实现接口

8/8/2022

根据视频(opens new window)练习 Node,基本按照视频步骤来,但也不是完全按照视频来

#初始化项目

  1. 生成package.json
  2. 安装express
  3. 生成tsconfig.json,如果生成不了,先全局安装 typescript,npm install typescript -g
  4. 安装typescript以及其他所需要的依赖包
  5. 创建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
  1. 修改package.json启动函数
{
  "scripts": {
    "dev": "nodemon --exec ts-node ./index.ts"
  }
}
1
2
3
4
5
  1. 在 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
  1. 执行命令pnpm dev,到浏览器查看3000端口

#初始化路由

  1. 新建router文件夹,然后新建user.ts文件
  2. 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
// index.ts
// ...
import router from './router/user';

// 注册路由
app.use('/api', router);
1
2
3
4
5
6

#开发登录注册接口

  1. 在 mysql 中新建表名为users同时定义 6 个字段,分别为idusernamepasswordnick_nameemailuser_pic
  2. 安装mysql2
pnpm add mysql2
1
  1. 新建db文件夹并在文件夹下新建index.ts
  2. 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

#实现注册接口

  1. 新建controller并下文件夹下新建user.ts
  2. router/uesr.ts中引入路由处理的逻辑函数
  3. 安装bcrypt包用于密码加密
  4. 书写逻辑代码
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

#优化返回值

  1. 新增types文件夹并创建module.d.ts文件,并写下
declare namespace Express {
  export interface Response {
    cc: (err: string | Error, status?: number) => void;
  }
}
1
2
3
4
5
  1. tsconfig.json中配置,其中ts-node的配置特别重要
{
  "ts-node": {
    "files": true
  },
  "compilerOptions": {
    "typeRoots": ["node_modules/@types", "./types"]
  }
}
1
2
3
4
5
6
7
8
  1. 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
  1. 使用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

#表单校验

对比了一下 express 表单校验的相关包,选择了express-validator,没有选择视频中的表单验证包

  1. 安装pnpm add express-validator
  2. 创建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
  1. 创建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
  1. 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
  1. controller/uesr.ts中就可以去掉判断用户名密码的格式问题

#实现登录接口

  1. 和注册一样需要加表单验证
router.post('/login', registerSchema, validateRequestSchema, login);
1
  1. 查询是否存在用户名,存在的情况下密码是否一致
  2. 创建一个config.ts文件,加入属性配置项,生成token值,需要安装jsonwebtoken,执行pnpm add jsonwebtokenpnpm add @types/jsonwebtoken -D
// config.ts
const config = {
    secretKey: '^_^', // token加密密钥
    expiresIn: '1h' // 过期时间
}

export default config;
1
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
  1. 统一入口判断用户除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

#个人信息

#获取用户信息

  1. 新建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
  1. 新建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
  1. index.ts中引入
import userinfoRouter from './router/userinfo';
app.use('/my', userinfoRouter);
1
2

#更新用户信息

  1. 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
  1. 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
  1. 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

#更新密码

  1. 设置路由
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
  1. 设置校验规则
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
  1. 设置路由处理函数
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

#更新头像

  1. 更新路由
router.put('/update/avatar', avatarSchema, validateRequestSchema, avatarHandle);
1
  1. 设置校验函数
const avatar = body('avatar').isDataURI().withMessage('头像为base64的字符串');

export const avatarSchema = [avatar];
1
2
3
  1. 设置处理函数
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

#文章类别

  1. 创建一个artical_category的数据表,字段分别为idnamealiasis_delete

#获取文章分类

  1. 创建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
  1. index.ts中导入路由
import articalRouter from './router/artical';

// ...
app.use('/my/artial', articalRouter);
1
2
3
4
  1. 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

#创建文章分类

  1. 创建路由
router.post('/category', artialSchema, validateRequestSchema, postCategoryHandle);
1
  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
  1. 新增路由处理函数
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

#删除文章分类

  1. 创建路由
router.delete('/category/:id', delArticalSchema, validateRequestSchema, delCategoryHandle);
1
  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
  1. 新增处理函数
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

#根据id获取文章分类

  1. 设置路由
router.get('/category/:id', delArticalSchema, validateRequestSchema, getCategoryByIdHandle);
1
  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

#更新

  1. 设置路由
router.put('/category', updateArticalSchema, validateRequestSchema, updateCategoryByIdHandle);
1
  1. 设置校验函数
import { id as articalId } from './user';
export const updateArticalSchema = [articalId, ...artialSchema];
1
2
  1. 设置路由处理函数
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
Last Updated:5/25/2024, 2:23:06 AM