koa2 项目实战

本文所有代码已上传:github

项目的搭建

目录结构的划分

  • 按照功能模块划分:例如:控制器 (controller) 的放在一起;操作数据库 (service) 的放在一起……
  • 按照业务模块划分:一个完整的小功能放在一起;

本文将用功能模块划分

js
1
2
3
4
5
6
7
-src
-main.js //入口文件
-app //全局文件夹
-controller //所有的控制器
-service //数据库操作相关
-router //路由相关
-utils //工具

安装依赖

js
1
2
3
4
//在src目录下
npm init -y
npm i koa
npm i nodemon -D //-D为开发依赖

设置快捷启动

js
1
2
3
4
5
// 在package.json 配置
"scripts": {
"start": "nodemon ./src/main.js"
}
// 之后可以直接通过 npm run start 方式启动项目,不需要找目录结构了

入口文件

main.js 键入如下代码:使用 nodemon main.js 即可启动服务;

js
1
2
3
4
5
6
const Koa = require("koa");
const app = new Koa();

app.listen(3000,()=>{
console.log("服务器启动成功~~");
})

但是入口文件中,代码越简洁越好,现在所有 app 的操作都在入口文件,不是很妥;

入口文件拆分

app 文件中,新建 index.js,用来放 app 的相关操作,然后在入口文件中,将其引入即可;

js
1
2
3
4
5
//app文件下的 index.js
const Koa = require("koa");
const app = new Koa();

module.exports=app;
js
1
2
3
4
5
6
//入口文件 main.js
const app = require("./app/index.js");

app.listen(3000, () => {
console.log("服务器启动成功~~");
})

配置文件

端口号是直接在代码中写死的,不妥,需要抽离出来,写在配置文件

根目录(与 package.json 同级目录)下新建一个.env(environment)做配置文件用来存放所有抽离出来的环境变量,比如端口号;那么入口文件之如何读取到.env 文件呢?

  • .env 配置文件中,写入 APP_PORT=3000

  • 下载 dotenv 插件:

    npm i dotenv

    • 因为.env 文件是需要全局加载的,所以在 app 目录下新建 config.js 文件
js
1
2
3
4
5
6
7
8
//  app/config.js
const dotenv = require("dotenv");
dotenv.config(); //将.env中的变量挂载到process.env中
// console.log(process.env.APP_PORT);

module.exports = {
APP_PORT
} = process.env;
js
1
2
3
4
5
6
7
// main.js
const app= require("./app/index.js");
const config = require("./app/config.js");

app.listen(config.APP_PORT,()=>{
console.log(`server is running port is ${config.APP_PORT}`);
})

用户注册接口

首先考虑: 请求路径与中间件处理的映射,但是 koa 不支持 app.post 等写法,只能用 app.use,需要手动处理;所以只能借助路由第三方库 koa-router

  • 下载路由第三方库:npm i koa-router
  • 查询数据需要获取到用户传递的参数,下载第三方库 npm i koa-bodyparser
  • 使用 postman 进行测试;

架构基础写法

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// app文件下的 index.js
const Koa = require("koa");
const bodyParser= require("koa-bodyparser");
const Router = require("koa-router");
const app = new Koa();
const userRouter = new Router({prefix:"/users"});

userRouter.post("/",(ctx,next)=>{
ctx.body="创建用户成功~";
})

// 注册body-parser
app.use(bodyParser());
// 注册路由
app.use(userRouter.routes());
app.use(userRouter.allowedMethods());


module.exports=app;

所有的路由相关的接口如果都写在 app文件下的 index.js 里面的话,代码会显得很臃肿,而且也不容易进行后期维护;

架构进阶写法

路由拆分

router 文件夹下新建 user.router.js,用来存储用户的路由信息;只负责注册接口,中间件处理逻辑不写在这里 (见下文);

router 路由抽离出去;

js
1
2
3
4
5
6
7
8
9
// router/user.router.js
const Router = require("koa-router");
const userRouter = new Router({prefix:"/users"});

userRouter.post("/",(ctx,next)=>{
ctx.body="创建用户成功~";
})

module.exports = userRouter;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app/index.js
const Koa = require("koa");
const bodyParser= require("koa-bodyparser");
const userRouter = require("../router/user.router.js")
const app = new Koa();


// 注册body-parser
app.use(bodyParser());
// 注册路由
app.use(userRouter.routes());
app.use(userRouter.allowedMethods());


module.exports=app;

其实接口请求的中间件的逻辑 (见下面代码) 也是很多的,所以也需求抽取;

js
1
2
3
4
5
6
7
8
9
10
11
// 接口请求的中间件的逻辑
userRouter.post("/",(ctx,next)=>{
ctx.body="创建用户成功~";
})
//中的
(ctx,next)=>{
ctx.body="创建用户成功~";
}

//只负责注册接口,中间件处理逻辑不写在这里
userRouter.post("/",具体的处理逻辑);

路由中间件拆分

controller 文件夹新建 user.controller.js,用来存放 中间件处理逻辑;

将路由中间件抽离出去;

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//  controller/user.controller.js
class UserController{
// 因为操作数据库是异步操作的,所以直接用 async 函数; async函数为对象的方法,下面直接module导出了一个对象;
async create(ctx,next){
// 获取用户请求传递的参数

// 查询数据

// 返回数据
ctx.body="controller success~"
}
}

module.exports= new UserController(); //导出的是一个对象(类的实例)
js
1
2
3
4
5
6
7
8
9
// router/user.router.js
const Router = require("koa-router");
// user.controller.js直接导出了一个实例对象,调用其方法;
const {create} = require("../controller/user.controller.js")

const userRouter = new Router({prefix:"/users"});
userRouter.post("/",create)

module.exports = userRouter;

不难发现,即使抽离出来了中间件,user.controller.js 异步方法里需要进行获取用户请求传递的参数,查询数据,返回数据的操作,依旧很繁琐;

故将查询数据的代码逻辑抽取出来,放到 service 的文件里;

路由中间件操作数据库拆分

service 文件夹新建 user.service.js,用来存放 查询数据 逻辑;

将操作数据库的逻辑抽离出去;

js
1
2
3
4
5
6
7
8
9
10
//  service/user.service.js
class UserService {
async create(user) {
console.log("user.controller传入的实参为", user);
// 将user存储到数据库中
return "service success"
}
}

module.exports = new UserService();
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// controller/user.controller.js
const service = require("../service/user.service.js");
class UserController{
// 因为操作数据库是异步操作的,所以直接用 async 函数; async函数为对象的方法,下面直接module导出了一个对象;
async create(ctx,next){
// 获取用户请求传递的参数
const user = ctx.request.body;
// 查询数据
const result = await service.create(user);
// 返回数据
ctx.body= result;
}
}

module.exports= new UserController(); //导出的是一个对象(类的实例)
js
1
2
3
4
5
6
7
8
9
// router/user.router.js
const Router = require("koa-router");
// user.controller.js直接导出了一个实例对象,调用其方法;
const {create} = require("../controller/user.controller.js")

const userRouter = new Router({prefix:"/users"});
userRouter.post("/",create)

module.exports = userRouter;

userSignin1

至此,架构已经大致划分完毕:一个接口划分了三层:router 层、controller 层、service 层

连接数据库

安装依赖

  • 安装 mysql2npm i mysql2

创建连接

  • 创建连接池app 文件夹下新建 database.js

连接数据库的一些变量也属于配置文件,需要写到配置文件中;

js
1
2
3
4
5
6
7
8
// .env
APP_PORT=3000

MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DATABASE=nodeHub
MYSQL_USER=root
MYSQL_PASSWORD=123456
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//  app/config.js
const dotenv = require("dotenv");
dotenv.config(); //将.env中的变量挂载到process.env中
// console.log(process.env.APP_PORT);

module.exports = {
APP_PORT,
MYSQL_HOST,
MYSQL_PORT,
MYSQL_DATABASE,
MYSQL_USER,
MYSQL_PASSWORD,

} = process.env;
js
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
// app/database.js
const mysql = require("mysql2");

const config = require("./config.js");
// 1.创建连接池
const connectionPool = mysql.createPool({
host: config.MYSQL_HOST,
port: config.MYSQL_PORT,
database: config.MYSQL_DATABASE,
user: config.MYSQL_USER,
password: config.MYSQL_PASSWORD,
});

// 判断是否连接成功
connectionPool.getConnection((err, connection) => {
if (err) {
console.log("获取连接失败", err);
return;
}
connection.connect((err) => {
if (err) {
console.log("数据库交互失败" + err);
} else {
console.log("数据库连接成功!");
}
});
});

// connections有promise()方法,可以直接调用;
module.exports = connectionPool.promise();

插入数据

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//  service/user.service.js
const connections = require("../app/database.js");

class UserService {
async create(user) {
const {name,password} = user;
const statement = `INSERT INTO users(name,password) VALUES(?,?)`;
// database.js 导出的是connections.promise();
const result = await connections.execute(statement,[name,password]);
// 将user存储到数据库中
return result;
}
}

module.exports = new UserService();
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// controller/user.controller.js
const service = require("../service/user.service.js");
class UserController{
// 因为操作数据库是异步操作的,所以直接用 async 函数; async函数为对象的方法,下面直接module导出了一个对象;
async create(ctx,next){
// 获取用户请求传递的参数
const user = ctx.request.body;
// 查询数据
const result = await service.create(user);
// 返回数据
ctx.body= result;
}
}

module.exports= new UserController(); //导出的是一个对象(类的实例)

userSign2

database1

以上代码基本实现了用户注册的逻辑,但是如果用户没有传参,或者传入的参数不对,没有做处理;即没有做错误处理;

错误处理

那错误处理要写在什么地方呢?答案是以中间件的形式写在路由 router 中,并且在插入数据库中间件之前;

PS:Koa 的 router 路由可以连续注册中间件,验证的中间件写在操作数据库的中间件之前就可以了;

  • 在根目录 (package.json 同级) 下新建 middleware 文件夹,用来存放中间件;
  • middleware 文件夹下新建 user.middleware.js 用来存放验证用户的中间件函数;
  • 中间件为一个个单独的函数,比类使用起来会更灵活一点;所以这里不使用类;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//  middleware/user.middleware.js
const verifyUser = async (ctx, next) => {

/**
* next之后才会匹配下一个符合条件的中间件;
* koa的next返回的是个promise;
* 因为下个中间件有异步操作,所以这里需要加await;
*/
await next();
}

module.exports = {
verifyUser
}
js
1
2
3
4
5
6
7
8
9
10
11
12
// router/user.router.js
const Router = require("koa-router");
// user.controller.js直接导出了一个实例对象,调用其方法;
const {create} = require("../controller/user.controller.js");

// 中间件
const {verifyUser} = require("../middleware/user.middleware.js");

const userRouter = new Router({prefix:"/users"});
userRouter.post("/",verifyUser,create);

module.exports = userRouter;
  • middleware/user.middleware.js 添加逻辑处理

middleware/user.middleware.js 中将错误弹出去;

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// middleware/user.middleware.js
const verifyUser = async (ctx, next) => {
// 获取用户名密码
const {name,password} = ctx.request.body;
// 判断用户名密码不能为空
if(!name||!password){
const userError = new Error("用户名或密码不能为空");
// return 结束当前函数执行
return ctx.app.emit("error",userError,ctx);
}
// 查看用户是否已经注册(未写)

await next();
}

module.exports = {
verifyUser
}

app/index.js 监听错误

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Koa = require("koa");
const bodyParser= require("koa-bodyparser");
const userRouter = require("../router/user.router.js");
const errorHandler = require("./errorHandler.js");
const app = new Koa();


// 注册body-parser
app.use(bodyParser());
// 注册路由
app.use(userRouter.routes());
app.use(userRouter.allowedMethods());

// 监听错误
app.on("error",errorHandler);


module.exports=app;

app 文件夹下新建了一个 errorHandler.js 来做错误处理;

js
1
2
3
4
5
6
7
8
9
const errorHandler = (error,ctx) => {
//通过 message接收传过来的错误
console.log(error.message);

ctx.status = 404;
ctx.body = "发生了错误";
}

module.exports = errorHandler;

userSign3

但是错误提示语句有很多,最好 new Error(xx) 里放的是变量,到时候改一个就好了;

  • 在根目录 (packagejson 同级目录) 下新建 constants 文件夹,里面在新建 error-types.js 用来存放错误常量;
js
1
2
3
4
5
6
// constants/error-types.js
const NAME_OR_PASSWORD_IS_REQUIRED = "name_or_password_is_required";

module.exports={
NAME_OR_PASSWORD_IS_REQUIRED,
}
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// middleware/user.middleware.js
const {NAME_OR_PASSWORD_IS_REQUIRED} = require("../constants/error-types.js");
const verifyUser = async (ctx, next) => {
// 获取用户名密码
const {name,password} = ctx.request.body;
// 判断用户名密码不能为空
if(!name||!password){
const userError = new Error(NAME_OR_PASSWORD_IS_REQUIRED);
// return 结束当前函数执行
return ctx.app.emit("error",userError,ctx);
}
// 查看用户是否已经注册(未写)

await next();
}

module.exports = {
verifyUser
}
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// app/errorHandler.js
const errorType = require("../constants/error-types.js");
const errorHandler = (error, ctx) => {
// console.log(error.message);
let status, message;
switch (error.message) {
case errorType.NAME_OR_PASSWORD_IS_REQUIRED:
status = 400; //Bad Request
message = "用户名或密码不能为空";
break;
default:
status = 404;
message = "发生错误了~";
break;
}
ctx.status = status;
ctx.body = message;
}

module.exports = errorHandler;

查看用户是否已经注册的逻辑还没写,即查看用户是否存在;需要查询数据库;

查看用户是否已经注册

service/user.service 查看数据库

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// service/user.service
const connections = require("../app/database.js");

class UserService {
async create(user) {
const {name,password} = user;
const statement = `INSERT INTO users(name,password) VALUES(?,?)`;
// database.js 导出的是connections.promise();
const result = await connections.execute(statement,[name,password]);
// 将user存储到数据库中
return result;
}
async getUserByName(name){
const statement = `SELECT * FROM users WHERE name=?`;
// database.js 导出的是connections.promise();
const result = await connections.execute(statement,[name]);
return result[0]; //返回result的话,又有很多其他信息,只有0才是我们想要的
}
}

module.exports = new UserService();

middleware/user.middleware 下调用查看数据库的方法;

js
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
// middleware/user.middleware
const errorType = require("../constants/error-types.js");
const {getUserByName} = require("../service/user.service.js");
const verifyUser = async (ctx, next) => {
// 获取用户名密码
const {name,password} = ctx.request.body;

// 判断用户名密码不能为空
if(!name||!password){
const userError = new Error(errorType.NAME_OR_PASSWORD_IS_REQUIRED);
// return 结束当前函数执行
return ctx.app.emit("error",userError,ctx);
}

// 判断用户不能重复
const result = await getUserByName(name);
// console.log(result);
if(result.length>0){
const userError = new Error(errorType.USER_IS_EXIT);
// return 结束当前函数执行
return ctx.app.emit("error",userError,ctx);
}
await next();
}

module.exports = {
verifyUser
}

constants/error-types 下新添加错误变量

js
1
2
3
4
5
6
7
8
// constants/error-types
const NAME_OR_PASSWORD_IS_REQUIRED = "name_or_password_is_required";
const USER_IS_EXIT="user_is_exit";

module.exports={
NAME_OR_PASSWORD_IS_REQUIRED,
USER_IS_EXIT,
}

app/errorHandler.js 对新添加的变量做 错误处理;

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// app/errorHandler.js
const errorType = require("../constants/error-types.js");
const errorHandler = (error, ctx) => {
// console.log(error.message);
let status, message;
switch (error.message) {
case errorType.NAME_OR_PASSWORD_IS_REQUIRED:
status = 400; //Bad Request
message = "用户名或密码不能为空";
break;
case errorType.USER_IS_EXIT:
status = 409; //conflict
message = "用户名已存在~";
break;
default:
status = 404;
message = "发生错误了~";
break;
}
ctx.status = status;
ctx.body = message;
}

module.exports = errorHandler;

此时,数据库保存的密码是明文的,不安全,需要进行加密处理,这里采用中间件进行密码的加密;

密码加密

新建一个中间件,在 验证用户是否注册 和 插入 之间进行密码的加密;

  • nodejs 中自带了 ctypto 库,使用这个进行加密,但是加密的方法还需要我们自己写,不是直接调;在 utils 文件夹新建 password-handle 封装加密方法;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// nodejs中自带了 ctypto库,使用这个进行加密

const crypto = require("crypto");

const md5Password = (password) => {
// crypto.createHash('加密方式')
const md5 = crypto.createHash("md5"); //返回是是一个对象
// md5.update(加密的数据).digest()==>获取二进制加密的Buffer
// 进postman测试,md5.update(加密的数据)这个数据必须是string,不能为number;

const result = md5.update(JSON.stringify(password)).digest("hex"); //获取16进制加密的字符串
return result;
};

module.exports = md5Password;
  • middlewareuser.middleware 下封装一个处理密码加密的中间件;
js
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
const errorType = require("../constants/error-types.js");
const {getUserByName} = require("../service/user.service.js");
const md5Password = require("../utils/password-handle.js");
const verifyUser = async (ctx, next) => {
// 获取用户名密码
const {name,password} = ctx.request.body;

// 判断用户名密码不能为空
if(!name||!password){
const userError = new Error(errorType.NAME_OR_PASSWORD_IS_REQUIRED);
// return 结束当前函数执行
return ctx.app.emit("error",userError,ctx);
}

// 判断用户不能重复
const result = await getUserByName(name);
// console.log(result);
if(result.length>0){
const userError = new Error(errorType.USER_IS_EXIT);
// return 结束当前函数执行
return ctx.app.emit("error",userError,ctx);
}
await next();
}

// 密码加密
const handlePassword = async (ctx,next)=>{
let {password} = ctx.request.body; //获取到未加密的
ctx.request.body.password= md5Password(password); //将ctx.body上的密码进行加密,然后插入到数据库

await next();
}

module.exports = {
verifyUser,
handlePassword,
}
  • router 下的 user.router.js 中进行密码加密中间件的导入与使用;、
js
1
2
3
4
5
6
7
8
9
10
11
const Router = require("koa-router");
// user.controller.js直接导出了一个实例对象,调用其方法;
const {create} = require("../controller/user.controller.js");

// 中间件
const {verifyUser,handlePassword} = require("../middleware/user.middleware.js");

const userRouter = new Router({prefix:"/users"});
userRouter.post("/",verifyUser,handlePassword,create);

module.exports = userRouter;

注!!!这里在 postman 测试的时候,密码为数字的时候,会报错。可能是 postman 的原因,key 和 value 都需要双引号包裹;

userSing4

用户登录接口

写接口的思想是:一开始没有任何限制,然后一点点的往里面添加中间件,来进行限制。这样才好写,而不是一下子就能想那么多;

登录逻辑结构搭建

  • router 文件夹下新建 auth.router.js,用来存放登录的路由逻辑;
js
1
2
3
4
5
6
const Router = require("koa-router");
const {login} =require("../controller/auth.controller.js");
const authRouter = new Router({prefix:"/login"});
authRouter.post("/",login);

module.exports=authRouter;
  • controller 文件夹下新建 auth.controller.js,用来存放中间件的逻辑处理;
js
1
2
3
4
5
6
7
8
class AuthController {
async login(ctx, next) {
const {name} = ctx.request.body;
ctx.body = `欢迎${name}回来~`;
}
}

module.exports = new AuthController();
  • app 文件夹下的 index.js 中,将路由进行引入与注册;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Koa = require("koa");
const bodyParser= require("koa-bodyparser");
const userRouter = require("../router/user.router.js");
const authRouter = require("../router/auth.router.js");
const errorHandler = require("./errorHandler.js");
const app = new Koa();


// 注册body-parser
app.use(bodyParser());
// 注册路由
app.use(userRouter.routes());
app.use(userRouter.allowedMethods());

app.use(authRouter.routes());
app.use(authRouter.allowedMethods());

// 监听错误
app.on("error",errorHandler);

module.exports=app;
  • postman 进行测试

userLogin1

壳子套好了,但是现在是个用户就能登录。需要添加登录条件,登录的时候判断用户是否存在,判断用户和密码输入的是否正确。。。

登录条件

通过中间件的方式来写入限制条件

  • middleware 下新建 auth.middleware.js,用来做登录的限制;
js
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 errorType = require("../constants/error-types.js");
const {getUserByName} = require("../service/user.service.js");
const md5Password = require("../utils/password-handle.js");
const vertifyLogin = async(ctx,next)=>{
// 1.获取用户名和密码
const {name,password} = ctx.request.body;
// 2.判断用户名或密码是否为空
if(!name||!password){
const loginError = new Error(errorType.NAME_OR_PASSWORD_IS_REQUIRED);
return ctx.app.emit("error",loginError,ctx);
}
// 3.判断用户名是否存在
const result = await getUserByName(name);
let user = result[0];
console.log(user);
if(!user){
const loginError = new Error(errorType.USER_IS_NOT_EXIT);
return ctx.app.emit("error",loginError,ctx);
}
// 4.验证密码是否和数据库中的密码一致(加密)
if(md5Password(password)!==user.password){
const loginError = new Error(errorType.PASSWORD_IS_INCORRENT);
return ctx.app.emit("error",loginError,ctx);
}
await next();
}

module.exports={
vertifyLogin,
}
  • routerauth.router.js 引入中间件;
js
1
2
3
4
5
6
7
const Router = require("koa-router");
const {login} =require("../controller/auth.controller.js");
const {vertifyLogin} = require("../middleware/auth.middleware.js");
const authRouter = new Router({prefix:"/login"});
authRouter.post("/",vertifyLogin,login);

module.exports=authRouter;
  • 中间件错误变量,放在在错误变量 constants 文件夹下的 error-types.js 里;(app/index.js 监听错误处理,这里登录接口写了,不需要更改);
js
1
2
3
4
5
6
7
8
9
10
11
const NAME_OR_PASSWORD_IS_REQUIRED = "name_or_password_is_required";
const USER_IS_EXIT="user_is_exit";
const USER_IS_NOT_EXIT="user_is_not_exit";
const PASSWORD_IS_INCORRENT="password_id_incorrent";

module.exports={
NAME_OR_PASSWORD_IS_REQUIRED,
USER_IS_EXIT,
USER_IS_NOT_EXIT,
PASSWORD_IS_INCORRENT,
}
  • app 文件夹下的 errorHandler.js 下做变量的错误处理;
js
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 errorType = require("../constants/error-types.js");
const errorHandler = (error, ctx) => {
// console.log(error.message);
let status, message;
switch (error.message) {
case errorType.NAME_OR_PASSWORD_IS_REQUIRED:
status = 400; //Bad Request
message = "用户名或密码不能为空";
break;
case errorType.USER_IS_EXIT:
status = 409; //conflict
message = "用户名已存在~";
break;
case errorType.USER_IS_NOT_EXIT:
status = 400; //请求参数错误
message = "用户不存在";
break;
case errorType.PASSWORD_IS_INCORRENT:
status = 400; //请求参数错误
message = "用户名或密码错误";
break;
default:
status = 404;
message = "发生错误了~";
break;
}
ctx.status = status;
ctx.body = message;
}

module.exports = errorHandler;
  • postman 进行测试~

简化路由注册

现在注册了两个路由,app/index.js 代码为:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const Koa = require("koa");
const bodyParser= require("koa-bodyparser");
const userRouter = require("../router/user.router.js");
const authRouter = require("../router/auth.router.js");
const errorHandler = require("./errorHandler.js");
const app = new Koa();


// 注册body-parser
app.use(bodyParser());
// 注册路由
app.use(userRouter.routes());
app.use(userRouter.allowedMethods());

app.use(authRouter.routes());
app.use(authRouter.allowedMethods());

// 监听错误
app.on("error",errorHandler);


module.exports=app;
  • router 文件夹下新建 index.js
js
1
2
3
4
5
6
7
8
9
10
11
12
13
const fs = require("fs");

const useRoutes = (app) => {
// fs.readdirSync返回的是个数组;
fs.readdirSync(__dirname).forEach(file => {
if (file == "index.js") return;
const router = require(`./${file}`);
app.use(router.routes());
app.use(router.allowedMethods());
})
}

module.exports=useRoutes;
  • app 文件夹下 index.js 中进行引入;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Koa = require("koa");
const bodyParser= require("koa-bodyparser");
// const userRouter = require("../router/user.router.js");
// const authRouter = require("../router/auth.router.js");
const useRoutes = require("../router/index.js");
const errorHandler = require("./errorHandler.js");
const app = new Koa();


// 注册body-parser
app.use(bodyParser());
// 注册路由
useRoutes(app); //传参app参数

// 监听错误
app.on("error",errorHandler);

module.exports=app;

登录凭证

登录成功返回凭证:cookie+session 或者是 Token令牌;现在基本都是 Token令牌作为登录凭证;

为什么需要登录凭证呢?

web 开发中,我们使用最多的协议是 http,但是 http 是一个无状态的协议。那什么叫做无状态协议呢?

  • 举个例子:
    • 我们登录了一个网站 www.fsllala.top;
    • 登录的时候我们需要输入用户名和密码:比如用户名 forward,密码:Forward666;
    • 登录成功之后,我们要以 forward 的身份去访问其他的数据和资源,还是通过 http 请求去访问。
      • fsllala 的服务器会问:你谁呀?
      • forward 说:我是 forward 呀,刚刚登录过呀;
      • fsllala:怎么证明你刚刚登录过呀?
      • forward 说:这。。。,http 没有告诉你吗?
      • fsllala:http 的每次请求对我来说都是一个单独的请求,和之前请求过什么没有关系。

看到了吧?这就是 http 的无状态,也就是服务器不知道你上一步做了什么,我们必须得有一个办法可以证明我们登录过;

  • 那如何证明刚才就是我登录的啊?
    • 登陆成功的时候服务器给我们发过来一个登录凭证,访问其他资源的时候,通过这个登录凭证来证明刚才就是我登录的,而且我能访问哪些资源,不能访问哪些资源,都可以通过登录凭证来决定;

  • 目前登录凭证有两种:
    • cookie+session(逐渐淘汰)
    • Token 令牌

详细文章,参考 NodeJs 登录凭证

JWT 颁发签名

  • 我们在 vertifyLogin 中间件中查询到了用户数据,我们需要将用户的 idname生成的token 返回给用户;所以可以在 vertifyLogin 中间件中,将用户的数据绑定在 ctx 中 (类似于原型);最后在 login 中间件即 controller 里面获取用户数据,进行 token生成和数据的返回;
  • 采用非对称加密,将生成的私钥公钥放到 app 文件夹下 (公共的数据);
  1. middleware 文件夹下,将从数据库中获取到的 user 绑定在 ctx 对象上,用于 controller 获取用户数据;
js
1
2
将user绑定在ctx对象上,用于controller获取用户数据
ctx.user=user;

middleware/auth.middleware.js 完整代码:

js
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 errorType = require("../constants/error-types.js");
const {getUserByName} = require("../service/user.service.js");
const md5Password = require("../utils/password-handle.js");
const vertifyLogin = async(ctx,next)=>{
// 1.获取用户名和密码
const {name,password} = ctx.request.body;
// 2.判断用户名或密码是否为空
if(!name||!password){
const loginError = new Error(errorType.NAME_OR_PASSWORD_IS_REQUIRED);
return ctx.app.emit("error",loginError,ctx);
}
// 3.判断用户名是否存在
const result = await getUserByName(name);
let user = result[0];
// console.log(user);
if(!user){
const loginError = new Error(errorType.USER_IS_NOT_EXIT);
return ctx.app.emit("error",loginError,ctx);
}
// 4.验证密码是否和数据库中的密码一致(加密)
if(md5Password(password)!==user.password){
const loginError = new Error(errorType.PASSWORD_IS_INCORRENT);
return ctx.app.emit("error",loginError,ctx);
}
// 5.将user绑定在ctx对象上,用于controller获取用户数据
ctx.user=user;
await next();
}

module.exports={
vertifyLogin,
}
  1. app 下的 config.js 读取 key 并导出;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const dotenv = require("dotenv");
dotenv.config(); //将.env中的变量挂载到process.env中
// console.log(process.env.APP_PORT);
const fs = require("fs");
const path = require("path");

const PRIVATE_KEY = fs.readFileSync(path.resolve(__dirname, "./keys/private.key"));
const PUBLIC_KEY = fs.readFileSync(path.resolve(__dirname, "./keys/public.key"));

module.exports = {
APP_PORT,
MYSQL_HOST,
MYSQL_PORT,
MYSQL_DATABASE,
MYSQL_USER,
MYSQL_PASSWORD,

} = process.env;

// 这里不能写在module.exports上面,原因可参考模块化规范一文;
module.exports.PRIVATE_KEY = PRIVATE_KEY;
module.exports.PUBLIC_KEY = PUBLIC_KEY;
  1. 安装 jsonwebtoken
js
1
npm i jsonwebtoken
  1. controller 下的 auth.controller.js 中做 token 的返回;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const jwt = require("jsonwebtoken");
const {PRIVATE_KEY} = require("../app/config.js");
class AuthController {
async login(ctx, next) {
// const {name} = ctx.request.body;
// ctx.body = `欢迎${name}回来~`;
const {id,name}=ctx.user;
const token = jwt.sign({id,name},PRIVATE_KEY,{
expiresIn:60*60*24,
algorithm:"RS256"
})
//返回对象形式的数据(ES6简写)
ctx.body={
id,
name,
token
}
}
}

module.exports = new AuthController();
  1. 使用 postman 进行测试;(填入数据库存在的用户);

  1. 设计个简单的接口,验证上面写的登录接口;(这一步实际项目不用做,做的话看下一步骤 7)
js
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
//在controller下的auth.controller.js下
const jwt = require("jsonwebtoken");
const { PRIVATE_KEY, PUBLIC_KEY } = require("../app/config.js");
const errorType = require("../constants/error-types.js");
class AuthController {
async login(ctx, next) {
}

// 验证token
async vertifyToken(ctx, next) {
const authorization = ctx.headers.authorization;
// 此时的token多一个"Bearer ",去掉;
const token = authorization.replace("Bearer ", "");
// 认证token 通过 jwt.verify(token,key); 使用公钥进行解密,并告诉其加密算法;
try {
const result = jwt.verify(token, PUBLIC_KEY, {
algorithms: ["RS256"], //这里algorithms是个复数,所以需要传入数组;
});
ctx.body = result;
} catch (error) {
const tokenError = new Error(errorType.UNAUTHORIZATION);
return ctx.app.emit("error", tokenError, ctx);
}
}
}

module.exports = new AuthController();
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在constants下的error-types.js下
const NAME_OR_PASSWORD_IS_REQUIRED = "name_or_password_is_required";
const USER_IS_EXIT = "user_is_exit";
const USER_IS_NOT_EXIT = "user_is_not_exit";
const PASSWORD_IS_INCORRENT = "password_is_incorrent";
const UNAUTHORIZATION = "unauthorization";

module.exports = {
NAME_OR_PASSWORD_IS_REQUIRED,
USER_IS_EXIT,
USER_IS_NOT_EXIT,
PASSWORD_IS_INCORRENT,
UNAUTHORIZATION,
};
js
1
2
3
4
5
// 在app下的errorHandler.js下
case errorType.UNAUTHORIZATION:
status = 401;
message = "无效的token~";
break;
js
1
2
3
4
5
6
7
8
9
//在router下的auth.router.js下
const Router = require("koa-router");
const { vertifyLogin } = require("../middleware/auth.middleware.js");
const { login,vertifyToken } = require("../controller/auth.controller.js");
const authRouter = new Router({ prefix: "/login" });
authRouter.post("/", vertifyLogin, login);
authRouter.get("/", vertifyToken);

module.exports = authRouter;

使用 postman,填入 token,进行测试 {{baseUrl}}/login

  1. 实际项目中,几乎每个接口都需要验证 token,所以需要将 6 中的验证 token 的逻辑提取到中间件中;以后别的接口需要验证 token 的话,直接引入这个验证 token 的中间件就行。
js
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
//在middleware的auth.middleware.js下
const errorType = require("../constants/error-types.js");
const { getUserByName } = require("../service/user.service.js");
const md5Password = require("../utils/password-handle.js");
const jwt = require("jsonwebtoken");
const { PUBLIC_KEY } = require("../app/config.js");
const vertifyLogin = async (ctx, next) => {
};

// 验证token是否有效
const vertifyToken = async (ctx, next) => {
const authorization = ctx.headers.authorization;
// 验证是否携带TOKEN
if(!authorization){
const tokenError = new Error(errorType.UNAUTHORIZATION);
return ctx.app.emit("error", tokenError, ctx);
}
// 此时的token多一个"Bearer ",去掉;
const token = authorization.replace("Bearer ", "");
// 认证token 通过 jwt.verify(token,key); 使用公钥进行解密,并告诉其加密算法;
try {
// 获取到token中信息(id.name...)
const result = jwt.verify(token, PUBLIC_KEY, {
algorithms: ["RS256"], //这里algorithms是个复数,所以需要传入数组;
});
// 将token信息保留下来,以便后续使用
ctx.userInfo = result;

// 执行下一个中间件;
await next();
} catch (error) {
const tokenError = new Error(errorType.UNAUTHORIZATION);
return ctx.app.emit("error", tokenError, ctx);
}
};

module.exports = {
vertifyLogin,
vertifyToken
};
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在constants下的error-types.js下
const NAME_OR_PASSWORD_IS_REQUIRED = "name_or_password_is_required";
const USER_IS_EXIT = "user_is_exit";
const USER_IS_NOT_EXIT = "user_is_not_exit";
const PASSWORD_IS_INCORRENT = "password_is_incorrent";
const UNAUTHORIZATION = "unauthorization";

module.exports = {
NAME_OR_PASSWORD_IS_REQUIRED,
USER_IS_EXIT,
USER_IS_NOT_EXIT,
PASSWORD_IS_INCORRENT,
UNAUTHORIZATION,
};
js
1
2
3
4
5
// 在app下的errorHandler.js下
case errorType.UNAUTHORIZATION:
status = 401;
message = "无效的token~";
break;
js
1
2
3
4
5
6
7
8
9
//在router下的auth.router.js下
const Router = require("koa-router");
const { vertifyLogin,vertifyToken} = require("../middleware/auth.middleware.js");
const { login } = require("../controller/auth.controller.js");
const authRouter = new Router({ prefix: "/login" });
authRouter.post("/", vertifyLogin, login);
authRouter.get("/", vertifyToken);

module.exports = authRouter;

使用 postman,填入 token,进行测试 {{baseUrl}}/login

动态信息接口 (一对多)

设计表字段思路:谁,发表了什么动态信息;

一条动态 (含唯一 id),只能是一个用户发布的;一个用户,可以发表多条动态信息 (含唯一 id);

属于” 一对多” 关系型数据库表;

数据库表

sql
1
2
3
4
5
6
7
8
CREATE TABLE IF NOT EXISTS `moment`(
id INT PRIMARY KEY AUTO_INCREMENT,
content VARCHAR(1000) NOT NULL,
user_id INT NOT NULL,
ctrateTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updateTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES user(id)
)

发表动态

  • router 文件夹下新建 moment.router.js,用来存放用户动态的路由逻辑;
js
1
2
3
4
5
6
7
8
9
10
//在简化路由注册中,实现了路由的自动引入;因此,不需要手动引入路由;
const Router = require("koa-router");
const { vertifyToken } = require("../middleware/auth.middleware");
const { postUpdates } = require("../controller/moment.controller");
const momentRouter = new Router({ prefix: "/moment" });

// 发表动态(登录之后才能,引入验证token的中间件)
momentRouter.post("/", vertifyToken, postUpdates);

module.exports = momentRouter;
  • controller 文件夹下新建 moment.controller.js,用来存放中间件的逻辑处理;
js
1
2
3
4
5
6
7
8
9
// 啥逻辑也不写,进行简单的接口验证
class MomentController {
// 发表动态
async postUpdates(ctx, next) {
ctx.body="发表动态成功~"
}
}

module.exports = new MomentController();
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 进行逻辑的处理
class MomentController {
// 发表动态
async postUpdates(ctx, next) {
// 1.获取动态的内容;
const {content} = ctx.request.body;
// 2.动态是谁发布的(谁登录就是谁发布的,登录的时候再ctx上绑定了user属性);
const {id} = ctx.user;

// 3.将动态存储到数据库中;(需要放到service层)
}
}

module.exports = new MomentController();
  • service 文件夹下新建 moment.service.js,用来存放中间件的逻辑处理;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 可能插入失败,需要修改数据表content字符集为 utf8;
const connections = require("../app/database.js");
class UserService {
// 数据库插入数据
async create(contents,userId) {
const statement = `INSERT INTO moment(content,user_id) VALUES(?,?)`;
// 返回的是一个[{}]格式的数据,lenth为1;采用数组结构,获取[]中的{};
const [serviceResult] = await connections.execute(statement,[contents,userId]);

return serviceResult;
}
}

module.exports = new UserService();

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// moment.controller.js
const momentService = require("../service/moment.service");
class MomentController {
// 发表动态
async postUpdates(ctx, next) {
// 1.获取动态的内容;
const { content } = ctx.request.body;
// 2.动态是谁发布的(谁登录就是谁发布的,验证token的时候再ctx上绑定了userInfo属性);

const { id } = ctx.userInfo;

// 3.将动态存储到数据库中;(需要放到service层)
const result = await momentService.create(content, id);
// 4.返回数据
ctx.body = {
code: 0,
message: "发布动态成功",
data: result,
};
}
}

module.exports = new MomentController();

查询动态列表

查询一般涉及到了分页的处理。

  • router 文件夹下的 moment.router.js,新增查询用户动态的路由逻辑;
js
1
2
3
4
5
6
7
8
9
10
11
const Router = require("koa-router");
const { vertifyToken } = require("../middleware/auth.middleware");
const { postUpdates,list } = require("../controller/moment.controller");
const momentRouter = new Router({ prefix: "/moment" });

// 发表动态(登录之后才能,引入验证token的中间件)
momentRouter.post("/", vertifyToken, postUpdates);
// 查询动态(这个不登录也能看动态的)
momentRouter.get("/list", list);

module.exports = momentRouter;
  • controller 文件夹下的 moment.controller.js,新增查询中间件的逻辑处理;
js
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
const momentService = require("../service/moment.service");
class MomentController {
// 发表动态
async postUpdates(ctx, next) {
// 1.获取动态的内容;
const { content } = ctx.request.body;
// 2.动态是谁发布的(谁登录就是谁发布的,验证token的时候再ctx上绑定了userInfo属性);

const { id } = ctx.userInfo;

// 3.将动态存储到数据库中;(需要放到service层)
const result = await momentService.create(content, id);
// 4.返回数据
ctx.body = {
code: 0,
message: "发布动态成功",
data: result,
};
}


// 查询动态
async list(ctx, next) {
// 获取 size与offset
const { size,offset } = ctx.query;
const result = await momentService.queryList(size,offset);
ctx.body = {
code: 0,
message: "查询动态成功",
data: result,
};
}
}

module.exports = new MomentController();
  • service 文件夹下的 moment.service.js,新增查询中间件的逻辑处理;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const connections = require("../app/database.js");
// 专门做数据库操作的;
class UserService {
// 数据库插入数据
async create(contents,userId) {
const statement = `INSERT INTO moment(content,user_id) VALUES(?,?)`;
// 返回的是一个[{}]格式的数据,lenth为1;采用数组结构,获取[]中的{};
const [serviceResult] = await connections.execute(statement,[contents,userId]);

return serviceResult;
}

// 查询数据列表
async queryList(size,offset){
const statement = `SELECT * FROM moment LIMIT ? OFFSET ?`;
// 这里如果写成String()的话,如果用户不传size,offset,接口也能通;如果是Number的话,会报错;
const [serviceResult] = await connections.execute(statement,[String(size),String(offset)]);

return serviceResult;
}
}

module.exports = new UserService();

访问 moment/list?size=10&offset=0,进行动态列表数据的分页查询。

但是现在返回的数据,其实仅仅是当前的 moment 数据表中的数据,不能知道是谁评论的,需要用外键 user_iduser 表关联起来;

  • 多表查询
sql
1
2
3
4
// 基础写法(没有数据格式处理,字段没有过滤)
SELECT * FROM moment
LEFT JOIN `user` ON user.id=moment.user_id
LIMIT 10 OFFSET 0
sql
1
2
3
4
5
6
7
// 进阶
SELECT
m.id as id,m.content content,m.ctrateTime ctrateTime,m.updateTime updateTime,
JSON_OBJECT('id',u.id,'name',u.name) as userInfo
FROM moment as m
LEFT JOIN `user` as u ON u.id=m.user_id
LIMIT 10 OFFSET 0

  • service 文件夹下的 moment.service.js,修改查询 SQL 语句;
js
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
const connections = require("../app/database.js");
// 专门做数据库操作的;
class UserService {
// 数据库插入数据
async create(contents,userId) {
const statement = `INSERT INTO moment(content,user_id) VALUES(?,?)`;
// 返回的是一个[{}]格式的数据,lenth为1;采用数组结构,获取[]中的{};
const [serviceResult] = await connections.execute(statement,[contents,userId]);

return serviceResult;
}

// 查询数据列表
async queryList(size,offset){
const statement = `SELECT
m.id as id,m.content content,m.ctrateTime ctrateTime,m.updateTime updateTime,
JSON_OBJECT('id',u.id,'name',u.name) as userInfo
FROM moment as m
LEFT JOIN user as u ON u.id=m.user_id
LIMIT ? OFFSET ?`;
// 这里如果写成String()的话,如果用户不传size,offset,接口也能通;如果是Number的话,会报错;
const [serviceResult] = await connections.execute(statement,[String(size),String(offset)]);

return serviceResult;
}
}

module.exports = new UserService();

访问 moment/list?size=10&offset=0,进行动态列表数据的分页查询,查询结果如下图所示

查询动态详情

获取列表某一条数据的详情。

  • router 文件夹下的 moment.router.js,新增查询用户动态详情接口;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
const Router = require("koa-router");
const { vertifyToken } = require("../middleware/auth.middleware");
const { postUpdates,list,detail} = require("../controller/moment.controller");
const momentRouter = new Router({ prefix: "/moment" });

// 发表动态(登录之后才能,引入验证token的中间件)
momentRouter.post("/", vertifyToken, postUpdates);
// 查询动态(这个不登录也能看动态的)
momentRouter.get("/list", list);
// 查询动态详情
momentRouter.get("/:momentId", detail);

module.exports = momentRouter;
  • controller 文件夹下的 moment.controller.js,新增查询中间件的逻辑处理;
js
1
2
3
4
5
6
7
8
9
10
11
// 查询动态详情
async detail(ctx, next) {
const { momentId } = ctx.params;
// console.log(momentId);
const result = await momentService.queryDetailById(momentId);
ctx.body = {
code: 0,
message: "查询动态详情成功",
data: result,
};
}
  • service 文件夹下的 moment.service.js,新增查询中间件的逻辑处理;
js
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
const connections = require("../app/database.js");
// 专门做数据库操作的;
class UserService {
// 数据库插入数据
async create(contents,userId) {
const statement = `INSERT INTO moment(content,user_id) VALUES(?,?)`;
// 返回的是一个[{}]格式的数据,lenth为1;采用数组结构,获取[]中的{};
const [serviceResult] = await connections.execute(statement,[contents,userId]);

return serviceResult;
}

// 查询数据列表
async queryList(size,offset){
const statement = `SELECT
m.id as id,m.content content,m.ctrateTime ctrateTime,m.updateTime updateTime,
JSON_OBJECT('id',u.id,'name',u.name) as userInfo
FROM moment as m
LEFT JOIN user as u ON u.id=m.user_id
LIMIT ? OFFSET ?`;
// 这里如果写成String()的话,如果用户不传size,offset,接口也能通;如果是Number的话,会报错;
const [serviceResult] = await connections.execute(statement,[String(size),String(offset)]);

return serviceResult;
}

// 查询动态详情(将'查询数据列表'的LIMIT...ON...改为WHERE...即可)
async queryDetailById(momentId){
const statement = `SELECT
m.id as id,m.content content,m.ctrateTime ctrateTime,m.updateTime updateTime,
JSON_OBJECT('id',u.id,'name',u.name) as userInfo
FROM moment as m
LEFT JOIN user as u ON u.id=m.user_id
WHERE m.id=?`;
const [serviceResult] = await connections.execute(statement,[momentId]);
return serviceResult;
}
}

module.exports = new UserService();

通过 /moment/1(GET) 获取详情数据。

修改动态

修改列表某一条数据的详情。

  • router 文件夹下的 moment.router.js,新增修改用户动态接口;
js
1
momentRouter.patch("/:momentId", vertifyToken, update);
  • controller 文件夹下的 moment.controller.js,新增修改用户的逻辑处理;
js
1
2
3
4
5
6
7
8
9
10
11
// 修改动态
async update(ctx, next) {
const { momentId } = ctx.params;
const { content } = ctx.request.body;
const result = await momentService.updateById(content, momentId);
ctx.body = {
code: 0,
message: "修改动态成功",
data: result,
};
}
  • service 文件夹下的 moment.service.js,新增修改用户的 SQL 操作;
js
1
2
3
4
5
6
// 修改动态
async updateById(content,momentId){
const statement = `UPDATE moment SET content=? WHERE id=?`;
const [serviceResult] = await connections.execute(statement,[content,momentId]);
return serviceResult;
}

通过 /moment/1(PATCH) 修改详情数据。

但是现在只要 token 有效,所有人的动态信息都能修改;正常来说,只能修改私人的动态,即:用户登录的 id 和数据库表中发表此动态的 id 一致可以修改,否则不能修改;

  • 增加验证修改权限的中间件
sql
1
2
//SQL语句包含了查询的ID与用户的ID;如果筛选条件都满足的话返回一条数据,否则为空;
SELECT * FROM moment WHERE id=1 AND user_id =1
  • router 文件夹下的 moment.router.js,新增修改用户动态权限的验证;
js
1
2
3
4
5
6
7
8
9
const Router = require("koa-router");
const { vertifyToken } = require("../middleware/auth.middleware");
const {vertifyMomentPermission}= require("../middleware/permission.middleware");
const { postUpdates,list,detail,update} = require("../controller/moment.controller");
const momentRouter = new Router({ prefix: "/moment" });
// 修改动态(登录之后才能,引入验证token的中间件)
// 用户登录的id和数据库表中发表此动态的id一致可以修改,否则不能修改
momentRouter.patch("/:momentId", vertifyToken, vertifyMomentPermission,update);
module.exports = momentRouter;
  • middleware 文件夹下新建 permission.middleware.js,验证登录的 id 是否有权限更改相关数据;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const errorType = require("../constants/error-types");
const permissionService = require("../service/permission.service");
// 验证登录的id是否有权限更改相关数据
const vertifyMomentPermission = async (ctx, next) => {
// 上个验证token的中间件在ctx上绑定了userInfo,即登录的用户信息;
const { id } = ctx.userInfo;
const { momentId } = ctx.params;
const result = await permissionService.checkMoment(id, momentId);
if (result.length === 0) {
const permissionError = new Error(errorType.NO_PERMISSION);
return ctx.app.emit("error", permissionError, ctx);
} else {
await next();
}
};

module.exports = {
vertifyMomentPermission,
};
  • service 文件夹下新建 permission.service.js,用户查询数据库;
js
1
2
3
4
5
6
7
8
9
10
11
const connections = require("../app/database.js");
class PermissionService {
// 数据库查看是否存在相应权限
async checkMoment(userId, momentId) {
const statement = `SELECT * FROM moment WHERE id=? AND user_id=?`;
const [result] = await connections.execute(statement, [momentId, userId]);
return result;
}
}

module.exports = new PermissionService();
  • constants 文件夹下 error-types.js,新增无权限的常量;
js
1
2
3
4
const NO_PERMISSION = "no_permission";
module.exports = {
NO_PERMISSION
};
  • app 文件夹下 errorHandler.js,新增无权限的判断;
js
1
2
3
4
case errorType.NO_PERMISSION:
status = 403;
message = "权限不足~";
break;

删除动态

  • router 文件夹下的 moment.router.js,新增删除用户动态接口;
js
1
2
3
4
5
6
7
const Router = require("koa-router");
const { vertifyToken } = require("../middleware/auth.middleware");
const {vertifyMomentPermission}= require("../middleware/permission.middleware");
const { postUpdates,list,detail,update,remove} = require("../controller/moment.controller");
const momentRouter = new Router({ prefix: "/moment" });
// 删除动态
momentRouter.delete("/:momentId", vertifyToken, vertifyMomentPermission,remove);
  • controller 文件夹下的 moment.controller.js,新增删除用户的逻辑处理;
js
1
2
3
4
5
6
7
8
9
// 删除动态
async remove(ctx, next) {
const { momentId } = ctx.params;
const result = await momentService.removeById(momentId);
ctx.body = {
code: 0,
message: "删除动态成功",
data: result,
}
  • service 文件夹下的 moment.service.js,新增删除用户的 SQL 操作;
js
1
2
3
4
5
6
// 删除动态
async removeById(momentId){
const statement = `DELETE FROM moment WHERE id=?`;
const [serviceResult] = await connections.execute(statement,[momentId]);
return serviceResult;
}

通过 /moment/1(DELETE) 删除详情数据。

评论动态接口 (一对多)

设计表字段思路:谁,发表了什么评论,该评论评论了哪条动态;

一条评论 (含唯一 id) 只能是一个用户发的,一个用户可以发多条评论 (含唯一 id);

属于” 一对多” 关系型数据库表;

数据库表

sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
--  创建comment 评论表
CREATE TABLE IF NOT EXISTS `comment`(
id INT PRIMARY KEY AUTO_INCREMENT,
-- 评论的内容
content VARCHAR(1000) NOT NULL,
-- 在哪条动态下评论的
moment_id INT NOT NULL,
-- 谁评论的
user_id INT NOT NULL,
-- 此条动态有多个评论,给每条评论一个id;就可以实现 评论 动态下面的某一条具体评论的效果(回复评论);
comment_id INT DEFAULT NULL,
ctrateTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updateTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY(moment_id) REFERENCES moment(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE,
-- comment_id对应自身表中的id
FOREIGN KEY(comment_id) REFERENCES comment(id) ON DELETE CASCADE ON UPDATE CASCADE
)

发表评论

需要知道:谁,在哪个动态下,评论了什么内容;

  • router 文件夹下新建 comment.router.js,用来存放发表评论的路由逻辑;
js
1
2
3
4
5
6
7
8
9
10
const Router = require("koa-router");
const { vertifyToken } = require("../middleware/auth.middleware");
const { create } = require("../controller/comment.controller");

const commentRouter = new Router({ prefix: "/comment" });

// 增:新增评论
commentRouter.post("/", vertifyToken, create);

module.exports = commentRouter;
  • controller 文件夹下新建 comment.controller.js,用来存放发表评论的逻辑处理;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const commentService = require("../service/comment.service");
class CommentController {
// 发表动态
async create(ctx, next) {
// 获取动态的内容,评论动态的Id
const { content, momentId } = ctx.request.body;
// 获取动态是谁发布的(谁登录就是谁发布的,验证token的时候再ctx上绑定了userInfo属性);
const { id } = ctx.userInfo;
// 将动态存储到数据库中;(需要放到service层)
const result = await commentService.create(content, id, momentId);
ctx.body = {
code: 0,
message: "发表评论成功",
data: result,
};
}
}

module.exports = new CommentController();
  • service 文件夹下新建 comment.service.js,用来存放发表评论的 SQL 处理;
js
1
2
3
4
5
6
7
8
9
10
11
const connections = require("../app/database.js");
class CommentService {
// 数据库插入数据
async create(content, userId, momentId) {
const statement = `INSERT INTO comment (content,user_id,moment_id) VALUES (?,?,?)`;
const [result] = await connections.execute(statement, [content,userId,momentId]);
return result;
}
}

module.exports = new CommentService();

通过 /comment(POST) 发表评论;例:body 为

{ "content": "kunkun打球好帅", "momentId": 3 }

注:其中 momentId 的 value 值,必须是 moment 表中存在的,因为设置了外键约束。

回复评论

相比较于发表评论,回复评论需要多一个参数,即:comment_id

  • router 文件夹下的 comment.router.js,用来存放回复评论的路由逻辑;
js
1
2
3
4
5
6
7
8
9
10
11
12
const Router = require("koa-router");
const { vertifyToken } = require("../middleware/auth.middleware");
const { create,reply } = require("../controller/comment.controller");

const commentRouter = new Router({ prefix: "/comment" });

// 增:新增评论
commentRouter.post("/", vertifyToken, create);

// 增:回复评论
commentRouter.post("/reply", vertifyToken, reply);
module.exports = commentRouter;
  • controller 文件夹下的 comment.controller.js,用来存回复的逻辑处理;
js
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
const commentService = require("../service/comment.service");
class CommentController {
// 发表动态
async create(ctx, next) {
// 获取动态的内容,评论动态的Id
const { content, momentId } = ctx.request.body;
// 获取动态是谁发布的(谁登录就是谁发布的,验证token的时候再ctx上绑定了userInfo属性);
const { id } = ctx.userInfo;
// 将动态存储到数据库中;(需要放到service层)
const result = await commentService.create(content, id, momentId);
ctx.body = {
code: 0,
message: "发表评论成功",
data: result,
};
}
// 回复动态
async reply(ctx, next) {
// 获取动态的内容,评论动态的Id,回复的Id
const { content, momentId,commentId } = ctx.request.body;
// 获取动态是谁发布的(谁登录就是谁发布的,验证token的时候再ctx上绑定了userInfo属性);
const { id } = ctx.userInfo;
// 将动态存储到数据库中;(需要放到service层)
const result = await commentService.reply(content, id, momentId,commentId);
ctx.body = {
code: 0,
message: "回复评论成功",
data: result,
};
}
}

module.exports = new CommentController();
  • service 文件夹下的 comment.service.js,用来存放回复评论的 SQL 处理;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const connections = require("../app/database.js");
class CommentService {
// 数据库插入数据
async create(content, userId, momentId) {
const statement = `INSERT INTO comment (content,user_id,moment_id) VALUES (?,?,?)`;
const [result] = await connections.execute(statement, [content,userId,momentId]);
return result;
}
async reply(content, userId, momentId,commentId) {
const statement = `INSERT INTO comment (content,user_id,moment_id,comment_id) VALUES (?,?,?,?)`;
const [result] = await connections.execute(statement, [content,userId,momentId,commentId]);
return result;
}
}

module.exports = new CommentService();

通过 /comment/reply(POST) 发表评论;例:body 为

{ "content": "确实帅,我家鸽鸽太有实力了", "momentId": 3, "commentId":6 }

注:其中 momentId/commentId 的 value 值,必须是 moment/comment 表中存在的,因为设置了外键约束。

动态信息与评论接口 (一对多)

一般来说,查询动态信息列表及详情的时候,也需要将评论一并查询出来。现需求如下:

  • 查询动态列表时,显示评论的个数;
  • 查询动态详情时,显示评论的列表;

查询动态列表

sql
1
2
3
4
5
6
7
-- 查询动态列表SQL
SELECT
m.id as id,m.content content,m.ctrateTime ctrateTime,m.updateTime updateTime,
JSON_OBJECT('id',u.id,'name',u.name) as userInfo
FROM moment as m
LEFT JOIN `user` as u ON u.id=m.user_id
LIMIT 10 OFFSET 0
sql
1
2
-- 查询评论个数SQL
SELECT COUNT(*) FROM `comment` WHERE moment_id=3
sql
1
2
3
4
5
6
7
8
-- 两者结合(SQL子查询)
SELECT
m.id as id,m.content content,m.ctrateTime ctrateTime,m.updateTime updateTime,
JSON_OBJECT('id',u.id,'name',u.name) as userInfo,
(SELECT COUNT(*) FROM comment WHERE comment.moment_id=m.id) commentCount
FROM moment as m
LEFT JOIN `user` as u ON u.id=m.user_id
LIMIT 10 OFFSET 0

查询动态详情

sql
1
2
3
4
5
6
7
-- 查询动态详情SQL
SELECT
m.id as id,m.content content,m.ctrateTime ctrateTime,m.updateTime updateTime,
JSON_OBJECT('id',u.id,'name',u.name) as userInfo
FROM moment as m
LEFT JOIN user as u ON u.id=m.user_id
WHERE m.id=3
sql
1
2
3
-- 查询评论列表SQL
SELECT * FROM `comment` WHERE `comment`.moment_id=3
-- 可以将两者查询出来,然后使用JS进行拼接处理;

标签动态接口 (多对多)

设计表字段思路:标签对应了哪条动态;没人在意标签是谁建的;

一个标签可以对应多条动态,一条动态可以有个标签;

属于” 多对多” 关系型数据库表;

数据库表

  • 标签表
sql
1
2
3
4
5
6
CREATE TABLE IF NOT EXISTS label(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(10) NOT NULL UNIQUE,
ctrateTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updateTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
  • 关系表 (记录标签,动态的关系)
sql
1
2
3
4
5
6
7
8
9
10
CREATE TABLE IF NOT EXISTS moment_label(
-- id INT PRIMARY KEY AUTO_INCREMENT, -- 这里使用联合主键,当然也可以用id
moment_id INT NOT NULL,
label_id INT NOT NULL,
PRIMARY KEY(moment_id,label_id),-- 联合主键
ctrateTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updateTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY(moment_id) REFERENCES moment(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY(label_id) REFERENCES label(id) ON DELETE CASCADE ON UPDATE CASCADE
)
  • 多对多,三张表,关系表加外键;
  • 添加数据时,先添加父表记录 (moment,label),再添加子表 (moment_label) 记录;
  • 删除数据时,先删除子表记录 (moment_label),再删除父表记录 (moment,label);
  • 详细代码:可参考上文接口;仅是 sql 语句更变了;

文件处理接口

上传头像

  • 安装处理文件的库

npm i koa-multer 的时候给出警告: Please use @koa/multer instead;

通过查阅 github 发现:koa-multer 在 2018 年就不再维护,但是 koa 官方自己研发了相关 multer 库:@koa/multer,这里使用官方推荐的;

相对于 koa-multer,koa/multer 仅仅在获取文件的方式从 ctx.req.file–>ctx.request.file;别的用法和 koa-multer 一致;

bash
1
npm install --save @koa/multer multer

基本逻辑

  • router 文件夹下新建 file.router.js,用来存放上传头像的路由逻辑;
js
1
2
3
4
5
6
7
8
9
const Router = require("koa-router");
const { vertifyToken } = require("../middleware/auth.middleware.js");
const { handleAvatar } = require("../middleware/handleAvatar.middleware.js");
const { uploadAvatar } = require("../controller/file.controller.js");
const fileRouter = new Router({ prefix: "/file" });

fileRouter.post("/avatar", vertifyToken, handleAvatar, uploadAvatar);

module.exports = fileRouter;
  • middleware 文件夹下新建 handleAvatar.middleware.js,用来存放上传头像中间件的逻辑处理;
js
1
2
3
4
5
6
7
8
9
10
11
12
const multer = require("@koa/multer");

const uploadAvatars = multer({
// 注意这里的./是和程序的启动目录有关系的,不是当前目录
dest: "./uploads/",
});

const handleAvatar = uploadAvatars.single("avatar");

module.exports = {
handleAvatar,
};
  • controller 文件夹下新建 file.controller.js,用来存放上传头像的逻辑处理;
js
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
class FileController {
async uploadAvatar(ctx, next) {
const files = ctx.request.file;
console.log(files);
ctx.body = {
code: 0,
message: "上传成功",
};
}
}

module.exports = new FileController();

/**console.log(files)的打印:
{
fieldname: 'avatar',
originalname: 'default_cover_7.webp',
encoding: '7bit',
mimetype: 'image/webp',
destination: './uploads/',
filename: '7a30342648914762d1cc040219d2a6e1',
path: 'uploads\\7a30342648914762d1cc040219d2a6e1',
size: 382920
}
*/

使用 postman 请求 /file/avatar(POST),form-data 选择 File,key 为 avatar,value 为上传的头像;发现项目自动创建了 uploads 文件夹,里面包含了上传的头像。

数据库表

根据 file 信息,挑选几个有用的字段进行保存,还需要将上传头像的用户 ID 进行保存,后续为了给这个人设置头像;

sql
1
2
3
4
5
6
7
8
9
10
CREATE TABLE IF NOT EXISTS `avatar`(
id INT PRIMARY KEY AUTO_INCREMENT,
filename VARCHAR(255) NOT NULL UNIQUE,
mimetype VARCHAR(30),
size INT,
user_id INT,
ctrateTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updateTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE
)

插入数据

  • 在基础逻辑上,在 controller 文件夹下 file.controller.js 中,优化存放上传头像的逻辑处理;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const serviceFile = require("../service/file.service");
class FileController {
async uploadAvatar(ctx, next) {
const files = ctx.request.file;
// console.log(files);
// 1.获取相应的信息
const {filename,mimetype,size } = ctx.request.file;
const {id} = ctx.userInfo;
// 2.保存到数据库
const result = await serviceFile.uploadAvatar(filename,mimetype,size,id);
// 3.返回
ctx.body = {
code: 0,
message: "上传成功",
data: result,
};
}
}

module.exports = new FileController();
  • service 文件夹下新建 file.service.js,用来存放上传头像的 SQL 逻辑处理;
js
1
2
3
4
5
6
7
8
9
10
11
const connections = require("../app/database.js");

class FileService {
async uploadAvatar(filename,mimetype,size,userId){
const statement = `INSERT INTO avatar (filename,mimetype,size,user_id) VALUES (?,?,?,?)`;
const [result] = await connections.execute(statement,[filename,mimetype,size,userId]);
return result;
}
}

module.exports = new FileService();

浏览头像

现在头像是没办法浏览的;现在提供一个浏览头像的接口;

  • 因为头像是 user 的,所以这里选择在 user 下提供浏览头像的接口;router 文件夹下的 user.router.js,用来提供一个浏览头像的接口;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
const Router = require("koa-router");
const userRouter = new Router({ prefix: "/users" });
const {verifyUser,handlePassword} = require("../middleware/user.middleware.js");

const {create,showAvatarImage} = require("../controller/user.controller.js")

// 注册用户
userRouter.post("/",verifyUser,handlePassword, create);

// 为用户提供头像展示(看图片不需要验证权限)
userRouter.get("/avatar/:userId",showAvatarImage)

module.exports = userRouter;
  • controller 文件夹下的 user.controller.js,用来处理用户头像展示的逻辑处理;
js
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 fs= require("fs");
const service = require("../service/user.service.js");
const fileService = require("../service/file.service.js");
class UserController {
// 因为操作数据库是异步操作的,所以直接用 async 函数; async函数为对象的方法,下面直接module导出了一个对象;
async create(ctx, next) {
// 获取用户请求传递的参数
const user = ctx.request.body;
// 查询数据
const result = await service.create(user);
// 返回数据
ctx.body = result;
}

// 用户头像展示
async showAvatarImage(ctx, next) {
// 1.获取用户的id
const { userId } = ctx.params;
// 2.获取userID对应的头像信息
const avatarInfo = await fileService.showAvatarImageById(userId);
console.log(avatarInfo)
// 3.读取头像所在的文件
const { filename, mimetype } = avatarInfo;
// 创建一个可读流
ctx.body = fs.createReadStream(`./uploads/${filename}`);
// 设置响应头信息
ctx.set("Content-Type", mimetype);
}
}

module.exports = new UserController(); //导出的是一个对象(类的实例)
  • service 文件夹下的 file.service.js,用来做用户头像展示的 SQL 逻辑处理;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const connections = require("../app/database.js");

class FileService {
async uploadAvatar(filename,mimetype,size,userId){
const statement = `INSERT INTO avatar (filename,mimetype,size,user_id) VALUES (?,?,?,?)`;
const [result] = await connections.execute(statement,[filename,mimetype,size,userId]);
return result;
}

async showAvatarImageById(userId){
const statement = `SELECT * FROM avatar WHERE user_id=?`;
const [result] = await connections.execute(statement,[userId]);
// 用户可能上传多张头像,所以result数组里面展示最后一个对象;
return result[result.length-1];
}
}

module.exports = new FileService();

浏览器通过 http://127.0.0.1:3000/users/avatar/1;或者 postman 通过 /users/avatar/1 即可查阅图片;

修改 user 表

在 user 表中,增加一个 avatar_url 字段;

  • controller 文件夹下的 file.controller.js,用来处理上传用户头像存储的逻辑处理;
js
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
const serviceFile = require("../service/file.service");
const userService = require("../service/user.service");
const {SERVER_HOST,APP_PORT} = require("../app/config");
class FileController {
async uploadAvatar(ctx, next) {
const files = ctx.request.file;
// console.log(files);
// 1.获取相应的信息
const {filename,mimetype,size } = ctx.request.file;
const {id} = ctx.userInfo;
// 2.保存到数据库
const result = await serviceFile.uploadAvatar(filename,mimetype,size,id);

// 3.将头像地址信息在user表中也存储一份;
const avatar_url = `${SERVER_HOST}:${APP_PORT}/users/avatar/${id}`;
const userResult = await userService.updateUserAvatar(avatar_url,id);
// 3.返回
ctx.body = {
code: 0,
message: "上传成功",
data: avatar_url,
};
}
}

module.exports = new FileController();
  • service 文件夹下的 user.service.js,用来做更新用户头像地址的 SQL 逻辑处理;
js
1
2
3
4
5
6
7
8
9
10
11
12
const connections = require("../app/database.js");
// 专门做数据库操作的;
class UserService {
// 更新用户头像地址
async updateUserAvatar(avatarUrl,userId){
const statement = `UPDATE user SET avatar_url=? WHERE id=?`;
const [result] = await connections.execute(statement,[avatarUrl,userId]);
return result;
}
}

module.exports = new UserService();
  • controller 文件夹下的 file.controller.js,返回头像地址需要写到配置文件里
js
1
2
3
# .env
APP_PORT=3000
SERVER_HOST=http://192.168.11.242
  • app 文件夹下的 config.js 将配置文件中的变量暴露出去;
js
1
2
3
4
module.exports = {
APP_PORT
SERVER_HOST
} = process.env;

请求 /file/avatar(POST) 上传文件,如果成功,返回结果如下:

js
1
2
3
4
5
6
{
"code": 0,
"message": "上传成功",
"data": "http://192.168.11.242:3000/users/avatar/1"
}
// 通过访问http://192.168.11.242:3000/users/avatar/1(接口),访问图片。

但是吧,这个跟我们平时见到的图片地址不一样,我们平时见到的图片地址是 xx.jpg/png 结尾的,而不是上面这种。下面讲述一下如何展示图片地址是 xx.jpg/png 结尾的。

上传头像 2

参考文献:koa-multer 实现图片上传Koa 实现图片上传功能koa 处理上传图片

常见图片后缀的展示,需要用到 node 自带的 path 模块和 static 模块和第三方包 koa/multer;

在 avatar 表中,现在的 filename 是存储的类似于 e91b35e24586c1cb208a544f6acb5961 的图片信息,需要改为存储有后缀名的 filename。

这里问了公司后端:filename 一般都是设置为 UNIQUE,且不会保留前端传过来的本身的文件名;因为如果传过来两个一样的文件名,后端不知道返回哪个;除非已经做好命名规范,可不设置为 UNIQUE;

即:上传的文件名一般不保存,除非约定好了上传的文件名唯一;

  • 安装所需依赖
sql
1
npm install koa-static
  • router 文件夹下 file.router.js,新写一个上传头像的路由逻辑;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Router = require("koa-router");

const { vertifyToken } = require("../middleware/auth.middleware.js");
const { handleAvatar,handleAvatar2 } = require("../middleware/handleAvatar.middleware.js");
const { uploadAvatar,uploadAvatar2 } = require("../controller/file.controller.js");
const fileRouter = new Router({ prefix: "/file" });

// 上传头像,没有常见的后缀名,即通过接口访问图片地址,不需要后缀
fileRouter.post("/avatar", vertifyToken, handleAvatar, uploadAvatar);

// 上传头像,有常见的后缀名
fileRouter.post("/avatar2", vertifyToken, handleAvatar2, uploadAvatar2);

module.exports = fileRouter;
  • middleware 文件夹下的 handleAvatar.middleware.js,新写一个存放上传头像中间件的逻辑处理;
js
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 multer = require("@koa/multer");
const path = require("path");
const uploadAvatars = multer({
// 注意这里的./是和程序的启动目录有关系的,不是当前目录
dest: "./uploads/",
});

const handleAvatar = uploadAvatars.single("avatar");

// 上传头像,有常见的后缀名
const storage = multer.diskStorage({
destination: (req, file, cb) => {
// 可以用 path.join(__dirname ,'xxx')拼接,就不用考虑相对路径的问题了;
cb(null, "./uploads/");
},

// 这里一般不会保留原来的,否则会导致后端不知道返回啥;除非约定好了
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname));
},
});
const uploadAvatars2 = multer({
storage,
});
const handleAvatar2 = uploadAvatars2.single("avatar2");


module.exports = {
handleAvatar,
handleAvatar2,
};
  • controller 文件夹下的 file.controller.js,新写一个存放上传头像的逻辑处理;
js
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
const serviceFile = require("../service/file.service");
const userService = require("../service/user.service");
const { SERVER_HOST, APP_PORT } = require("../app/config");
class FileController {
async uploadAvatar(ctx, next) {
const files = ctx.request.file;
// console.log(files);
// 1.获取相应的信息
const { filename, mimetype, size } = ctx.request.file;
const { id } = ctx.userInfo;
// 2.保存到数据库
const result = await serviceFile.uploadAvatar(filename, mimetype, size, id);

// 3.将头像地址信息在user表中也存储一份;
const avatar_url = `${SERVER_HOST}:${APP_PORT}/users/avatar/${id}`;
await userService.updateUserAvatar(avatar_url, id);
// 3.返回
ctx.body = {
code: 0,
message: "上传成功",
data: avatar_url,
};
}

// 有后缀名的头像
async uploadAvatar2(ctx, next) {
const files = ctx.request.file;
// console.log(files);
// 1.获取相应的信息
const { filename, mimetype, size } = ctx.request.file;
const { id } = ctx.userInfo;
// 2.保存到数据库
const result = await serviceFile.uploadAvatar(filename, mimetype, size, id);

// 3.将头像地址信息在user表中也存储一份;
const avatar_url = `${SERVER_HOST}:${APP_PORT}/${filename}`;
await userService.updateUserAvatar(avatar_url, id);
// 3.返回
ctx.body = {
code: 0,
message: "上传成功",
data: avatar_url,
};
}
}

module.exports = new FileController();
  • service 文件夹下的 user.service.js,用来做更新用户头像地址的 SQL 逻辑处理;(这个没有变);相对于通过接口显示图片,有后缀的是通过 koa-static 展示的,所以可以不用专门写一个接口浏览头像;
js
1
2
3
4
5
6
    // 更新用户头像地址
async updateUserAvatar(avatarUrl,userId){
const statement = `UPDATE user SET avatar_url=? WHERE id=?`;
const [result] = await connections.execute(statement,[avatarUrl,userId]);
return result;
}
  • koa-static 静态服务:在 app 文件夹下的 index.js
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const Koa = require("koa");
const path =require("path");
const static = require("koa-static");
const useRoutes = require("../router/index.js");
const bodyParser= require("koa-bodyparser");
const errorHandler = require("./errorHandler.js");

const app = new Koa();



app.use(bodyParser());
// 注册路由
useRoutes(app); //传参app参数


// 静态资源服务
const filePath = path.resolve(__dirname,"../../uploads");
app.use(static(filePath));

// 错误处理
app.on("error",errorHandler);
module.exports = app;

请求 /file/avatar2(POST),文件格式选:form-data (File),key 为 avatar2,value 为文件;返回结果为:

js
1
2
3
4
5
6
{
"code": 0,
"message": "上传成功",
"data": "http://192.168.11.242:3000/1698373200499.webp"
}
// 浏览器访问图片地址,能直接访问,则成功;