koa2项目实战

本文所有代码已上传:github

项目的搭建

目录结构的划分

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

本文将用功能模块划分

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

安装依赖

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

设置快捷启动

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

入口文件

main.js键入如下代码:使用nodemon main.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的相关操作,然后在入口文件中,将其引入即可;

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

module.exports=app;
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文件
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;
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进行测试;

架构基础写法

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路由抽离出去;

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;
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;

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

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,用来存放 中间件处理逻辑;

将路由中间件抽离出去;

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(); //导出的是一个对象(类的实例)
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,用来存放 查询数据 逻辑;

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

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();
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(); //导出的是一个对象(类的实例)
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

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

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
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;
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();

插入数据

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();
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用来存放验证用户的中间件函数;
  • 中间件为一个个单独的函数,比类使用起来会更灵活一点;所以这里不使用类;
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
}
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中将错误弹出去;

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监听错误

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来做错误处理;

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用来存放错误常量;
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,
}
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
}
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查看数据库

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下调用查看数据库的方法;

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下新添加错误变量

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对新添加的变量做 错误处理;

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封装加密方法;
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下封装一个处理密码加密的中间件;
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中进行密码加密中间件的导入与使用;、
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,用来存放登录的路由逻辑;
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,用来存放中间件的逻辑处理;
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中,将路由进行引入与注册;
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,用来做登录的限制;
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引入中间件;
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监听错误处理,这里登录接口写了,不需要更改);
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下做变量的错误处理;
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代码为:

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
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中进行引入;
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获取用户数据;
1
2
将user绑定在ctx对象上,用于controller获取用户数据
ctx.user=user;

middleware/auth.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
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并导出;
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
1
npm i jsonwebtoken
  1. controller下的auth.controller.js中做token的返回;
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)
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();
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,
};
1
2
3
4
5
// 在app下的errorHandler.js下
case errorType.UNAUTHORIZATION:
status = 401;
message = "无效的token~";
break;
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的中间件就行。
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
};
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,
};
1
2
3
4
5
// 在app下的errorHandler.js下
case errorType.UNAUTHORIZATION:
status = 401;
message = "无效的token~";
break;
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);

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

数据库表

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,用来存放用户动态的路由逻辑;
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,用来存放中间件的逻辑处理;
1
2
3
4
5
6
7
8
9
// 啥逻辑也不写,进行简单的接口验证
class MomentController {
// 发表动态
async postUpdates(ctx, next) {
ctx.body="发表动态成功~"
}
}

module.exports = new MomentController();
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,用来存放中间件的逻辑处理;
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();

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,新增查询用户动态的路由逻辑;
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,新增查询中间件的逻辑处理;
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,新增查询中间件的逻辑处理;
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表关联起来;

  • 多表查询
1
2
3
4
// 基础写法(没有数据格式处理,字段没有过滤)
SELECT * FROM moment
LEFT JOIN `user` ON user.id=moment.user_id
LIMIT 10 OFFSET 0
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语句;
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,新增查询用户动态详情接口;
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,新增查询中间件的逻辑处理;
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,新增查询中间件的逻辑处理;
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,新增修改用户动态接口;
1
momentRouter.patch("/:momentId", vertifyToken, update);
  • controller文件夹下的moment.controller.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操作;
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一致可以修改,否则不能修改;

  • 增加验证修改权限的中间件
1
2
//SQL语句包含了查询的ID与用户的ID;如果筛选条件都满足的话返回一条数据,否则为空;
SELECT * FROM moment WHERE id=1 AND user_id =1
  • router文件夹下的moment.router.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是否有权限更改相关数据;
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,用户查询数据库;
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,新增无权限的常量;
1
2
3
4
const NO_PERMISSION = "no_permission";
module.exports = {
NO_PERMISSION
};
  • app文件夹下errorHandler.js,新增无权限的判断;
1
2
3
4
case errorType.NO_PERMISSION:
status = 403;
message = "权限不足~";
break;

删除动态

  • router文件夹下的moment.router.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,新增删除用户的逻辑处理;
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操作;
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);

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

数据库表

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,用来存放发表评论的路由逻辑;
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,用来存放发表评论的逻辑处理;
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处理;
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,用来存放回复评论的路由逻辑;
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,用来存回复的逻辑处理;
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处理;
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表中存在的,因为设置了外键约束。

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

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

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

查询动态列表

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
1
2
-- 查询评论个数SQL
SELECT COUNT(*) FROM `comment` WHERE moment_id=3
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

查询动态详情

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
1
2
3
-- 查询评论列表SQL
SELECT * FROM `comment` WHERE `comment`.moment_id=3
-- 可以将两者查询出来,然后使用JS进行拼接处理;

标签动态接口(多对多)

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

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

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

数据库表

  • 标签表
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
)
  • 关系表(记录标签,动态的关系)
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一致;

1
npm install --save @koa/multer multer

基本逻辑

  • router文件夹下新建file.router.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,用来存放上传头像中间件的逻辑处理;
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,用来存放上传头像的逻辑处理;
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进行保存,后续为了给这个人设置头像;

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中,优化存放上传头像的逻辑处理;
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逻辑处理;
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,用来提供一个浏览头像的接口;
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,用来处理用户头像展示的逻辑处理;
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逻辑处理;
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,用来处理上传用户头像存储的逻辑处理;
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逻辑处理;
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,返回头像地址需要写到配置文件里
1
2
3
# .env
APP_PORT=3000
SERVER_HOST=http://192.168.11.242
  • app文件夹下的config.js将配置文件中的变量暴露出去;
1
2
3
4
module.exports = {
APP_PORT
SERVER_HOST
} = process.env;

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

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;

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

  • 安装所需依赖
1
npm install koa-static
  • router文件夹下file.router.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,新写一个存放上传头像中间件的逻辑处理;
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,新写一个存放上传头像的逻辑处理;
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展示的,所以可以不用专门写一个接口浏览头像;
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
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为文件;返回结果为:

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