koa2项目实战 本文所有代码已上传:github
项目的搭建 目录结构的划分
按照功能模块划分:例如:控制器(controller)的放在一起;操作数据库(service)的放在一起……
按照业务模块划分:一个完整的小功能放在一起;
本文将用功能模块划分
1 2 3 4 5 6 7 -src -main.js -app -controller -service -router -utils
安装依赖 1 2 3 4 npm init -y npm i koa npm i nodemon -D
设置快捷启动 1 2 3 4 5 "scripts" : { "start" : "nodemon ./src/main.js" }
入口文件
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 const Koa = require ("koa" );const app = new Koa ();module .exports =app;
1 2 3 4 5 6 const app = require ("./app/index.js" );app.listen (3000 , () => { console .log ("服务器启动成功~~" ); })
配置文件
端口号是直接在代码中写死的,不妥,需要抽离 出来,写在配置文件 中
在根目录 (与package.json
同级目录)下新建一个.env
(environment)做配置文件 用来存放所有抽离出来的环境变量 ,比如端口号;那么入口文件 之如何读取到.env
文件呢?
1 2 3 4 5 6 7 8 const dotenv = require ("dotenv" );dotenv.config (); module .exports = { APP_PORT } = process.env ;
1 2 3 4 5 6 7 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 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 ="创建用户成功~" ; }) 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 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 const Koa = require ("koa" );const bodyParser= require ("koa-bodyparser" );const userRouter = require ("../router/user.router.js" )const app = new Koa ();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 class UserController { async create (ctx,next ){ ctx.body ="controller success~" } } module .exports = new UserController ();
1 2 3 4 5 6 7 8 9 const Router = require ("koa-router" );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 class UserService { async create (user ) { console .log ("user.controller传入的实参为" , user); return "service success" } } module .exports = new UserService ();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const service = require ("../service/user.service.js" );class UserController { 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 const Router = require ("koa-router" );const {create} = require ("../controller/user.controller.js" )const userRouter = new Router ({prefix :"/users" });userRouter.post ("/" ,create) module .exports = userRouter;
至此,架构已经大致划分完毕:一个接口划分了三层:router层、controller层、service层 ;
连接数据库 安装依赖
创建连接
创建连接池 :app
文件夹下新建database.js
;
连接数据库的一些变量也属于配置文件 ,需要写到配置文件中;
1 2 3 4 5 6 7 8 APP_PORT =3000 MYSQL_HOST =localhostMYSQL_PORT =3306 MYSQL_DATABASE =nodeHubMYSQL_USER =rootMYSQL_PASSWORD =123456
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const dotenv = require ("dotenv" );dotenv.config (); 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 const mysql = require ("mysql2" );const config = require ("./config.js" );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 ("数据库连接成功!" ); } }); }); module .exports = connectionPool.promise ();
插入数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const connections = require ("../app/database.js" );class UserService { async create (user ) { const {name,password} = user; const statement = `INSERT INTO users(name,password) VALUES(?,?)` ; const result = await connections.execute (statement,[name,password]); return result; } } module .exports = new UserService ();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const service = require ("../service/user.service.js" );class UserController { async create (ctx,next ){ const user = ctx.request .body ; const result = await service.create (user); ctx.body = result; } } module .exports = new UserController ();
以上代码基本实现了用户注册的逻辑,但是如果用户没有传参,或者传入的参数不对,没有做处理;即没有做错误处理;
错误处理
那错误处理要写在什么地方呢?答案是以中间件的形式写在路由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 const verifyUser = async (ctx, next ) => { await next (); } module .exports = { verifyUser }
1 2 3 4 5 6 7 8 9 10 11 12 const Router = require ("koa-router" );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 const verifyUser = async (ctx, next ) => { const {name,password} = ctx.request .body ; if (!name||!password){ const userError = new Error ("用户名或密码不能为空" ); 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 ();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 ) => { console .log (error.message ); ctx.status = 404 ; ctx.body = "发生了错误" ; } module .exports = errorHandler;
但是错误提示语句有很多,最好new Error(xx)
里放的是变量,到时候改一个就好了;
在根目录(packagejson
同级目录)下新建constants
文件夹,里面在新建error-types.js
用来存放错误常量;
1 2 3 4 5 6 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 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 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 const errorType = require ("../constants/error-types.js" );const errorHandler = (error, ctx ) => { let status, message; switch (error.message ) { case errorType.NAME_OR_PASSWORD_IS_REQUIRED : status = 400 ; 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 const connections = require ("../app/database.js" );class UserService { async create (user ) { const {name,password} = user; const statement = `INSERT INTO users(name,password) VALUES(?,?)` ; const result = await connections.execute (statement,[name,password]); return result; } async getUserByName (name ){ const statement = `SELECT * FROM users WHERE name=?` ; const result = await connections.execute (statement,[name]); return 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 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 ctx.app .emit ("error" ,userError,ctx); } const result = await getUserByName (name); if (result.length >0 ){ const userError = new Error (errorType.USER_IS_EXIT ); return ctx.app .emit ("error" ,userError,ctx); } await next (); } module .exports = { verifyUser }
constants/error-types
下新添加错误变量
1 2 3 4 5 6 7 8 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 const errorType = require ("../constants/error-types.js" );const errorHandler = (error, ctx ) => { let status, message; switch (error.message ) { case errorType.NAME_OR_PASSWORD_IS_REQUIRED : status = 400 ; message = "用户名或密码不能为空" ; break ; case errorType.USER_IS_EXIT : status = 409 ; 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 const crypto = require ("crypto" );const md5Password = (password ) => { const md5 = crypto.createHash ("md5" ); const result = md5.update (JSON .stringify (password)).digest ("hex" ); return result; }; module .exports = md5Password;
在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 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 ctx.app .emit ("error" ,userError,ctx); } const result = await getUserByName (name); if (result.length >0 ){ const userError = new Error (errorType.USER_IS_EXIT ); 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); 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" );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都需要双引号包裹;
用户登录接口
写接口的思想是:一开始没有任何限制,然后一点点的往里面添加中间件,来进行限制。这样才好写,而不是一下子就能想那么多;
登录逻辑结构搭建
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 ();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;
壳子套好了,但是现在是个用户就能登录。需要添加登录条件,登录的时候判断用户是否存在,判断用户和密码输入的是否正确。。。
登录条件
通过中间件的方式来写入限制条件
在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 )=>{ const {name,password} = ctx.request .body ; if (!name||!password){ const loginError = new Error (errorType.NAME_OR_PASSWORD_IS_REQUIRED ); return ctx.app .emit ("error" ,loginError,ctx); } 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); } 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, }
router
的auth.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 ) => { let status, message; switch (error.message ) { case errorType.NAME_OR_PASSWORD_IS_REQUIRED : status = 400 ; message = "用户名或密码不能为空" ; break ; case errorType.USER_IS_EXIT : status = 409 ; 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;
简化路由注册
现在注册了两个路由,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 ();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;
1 2 3 4 5 6 7 8 9 10 11 12 13 const fs = require ("fs" );const useRoutes = (app ) => { 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;
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 useRoutes = require ("../router/index.js" );const errorHandler = require ("./errorHandler.js" );const app = new Koa ();app.use (bodyParser ()); useRoutes (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
中间件中查询到了用户数据,我们需要将用户的id
、name
和生成的token
返回给用户;所以可以在vertifyLogin
中间件中,将用户的数据绑定在ctx
中(类似于原型);最后在login
中间件即controller
里面获取用户数据,进行token生成
和数据的返回;
采用非对称加密
,将生成的私钥
和公钥
放到app
文件夹下(公共的数据);
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 )=>{ const {name,password} = ctx.request .body ; if (!name||!password){ const loginError = new Error (errorType.NAME_OR_PASSWORD_IS_REQUIRED ); return ctx.app .emit ("error" ,loginError,ctx); } const result = await getUserByName (name); let user = result[0 ]; if (!user){ const loginError = new Error (errorType.USER_IS_NOT_EXIT ); return ctx.app .emit ("error" ,loginError,ctx); } if (md5Password (password)!==user.password ){ const loginError = new Error (errorType.PASSWORD_IS_INCORRENT ); return ctx.app .emit ("error" ,loginError,ctx); } ctx.user =user; await next (); } module .exports ={ vertifyLogin, }
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 (); 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 .PRIVATE_KEY = PRIVATE_KEY ;module .exports .PUBLIC_KEY = PUBLIC_KEY ;
安装jsonwebtoken
在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 {id,name}=ctx.user ; const token = jwt.sign ({id,name},PRIVATE_KEY ,{ expiresIn :60 *60 *24 , algorithm :"RS256" }) ctx.body ={ id, name, token } } } module .exports = new AuthController ();
使用postman
进行测试;(填入数据库存在的用户);
设计个简单的接口,验证上面写的登录接口;(这一步实际项目不用做,做的话看下一步骤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 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 ) { } async vertifyToken (ctx, next ) { const authorization = ctx.headers .authorization ; const token = authorization.replace ("Bearer " , "" ); try { const result = jwt.verify (token, PUBLIC_KEY , { algorithms : ["RS256" ], }); 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 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 case errorType.UNAUTHORIZATION : status = 401 ; message = "无效的token~" ; break ;
1 2 3 4 5 6 7 8 9 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
;
实际项目中,几乎每个接口都需要验证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 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 ) => {}; const vertifyToken = async (ctx, next ) => { const authorization = ctx.headers .authorization ; if (!authorization){ const tokenError = new Error (errorType.UNAUTHORIZATION ); return ctx.app .emit ("error" , tokenError, ctx); } const token = authorization.replace ("Bearer " , "" ); try { const result = jwt.verify (token, PUBLIC_KEY , { algorithms : ["RS256" ], }); 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 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 case errorType.UNAUTHORIZATION : status = 401 ; message = "无效的token~" ; break ;
1 2 3 4 5 6 7 8 9 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" });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 ) { const {content} = ctx.request .body ; const {id} = ctx.user ; } } module .exports = new MomentController ();
service
文件夹下新建moment.service.js
,用来存放中间件的逻辑处理;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const connections = require ("../app/database.js" );class UserService { async create (contents,userId ) { const statement = `INSERT INTO moment(content,user_id) VALUES(?,?)` ; 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 const momentService = require ("../service/moment.service" );class MomentController { async postUpdates (ctx, next ) { const { content } = ctx.request .body ; const { id } = ctx.userInfo ; const result = await momentService.create (content, id); 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" });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 ) { const { content } = ctx.request .body ; const { id } = ctx.userInfo ; const result = await momentService.create (content, id); ctx.body = { code : 0 , message : "发布动态成功" , data : result, }; } async list (ctx, next ) { 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(?,?)` ; const [serviceResult] = await connections.execute (statement,[contents,userId]); return serviceResult; } async queryList (size,offset ){ const statement = `SELECT * FROM moment LIMIT ? OFFSET ?` ; const [serviceResult] = await connections.execute (statement,[String (size),String (offset)]); return serviceResult; } } module .exports = new UserService ();
访问moment/list?size=10&offset=0
,进行动态列表数据的分页查询。
但是现在返回的数据,其实仅仅是当前的moment
数据表中的数据,不能知道是谁评论的,需要用外键user_id
与user
表关联起来;
1 2 3 4 / / 基础写法(没有数据格式处理,字段没有过滤)SELECT * FROM moment LEFT JOIN `user ` ON user.id= moment.user_idLIMIT 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 userInfoFROM moment as mLEFT JOIN `user ` as u ON u.id= m.user_idLIMIT 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(?,?)` ; 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 ?` ; 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" });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 ; 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(?,?)` ; 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 ?` ; const [serviceResult] = await connections.execute (statement,[String (size),String (offset)]); return serviceResult; } 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" });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" );const vertifyMomentPermission = async (ctx, next ) => { 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 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 , 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, 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 ) { const { content, momentId } = ctx.request .body ; const { id } = ctx.userInfo ; 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 ) { const { content, momentId } = ctx.request .body ; const { id } = ctx.userInfo ; const result = await commentService.create (content, id, momentId); ctx.body = { code : 0 , message : "发表评论成功" , data : result, }; } async reply (ctx, next ) { const { content, momentId,commentId } = ctx.request .body ; const { id } = ctx.userInfo ; 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 SELECT m.id as id,m.content content,m.ctrateTime ctrateTime,m.updateTime updateTime, JSON_OBJECT ('id' ,u.id,'name' ,u.name) as userInfoFROM moment as mLEFT JOIN `user ` as u ON u.id= m.user_idLIMIT 10 OFFSET 0
1 2 SELECT COUNT (* ) FROM `comment` WHERE moment_id= 3
1 2 3 4 5 6 7 8 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 mLEFT JOIN `user ` as u ON u.id= m.user_idLIMIT 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 WHERE m.id= 3
1 2 3 SELECT * FROM `comment` WHERE `comment`.moment_id= 3
标签动态接口(多对多)
设计表字段思路:标签对应了哪条动态;没人在意标签是谁建的;
一个标签可以对应多条动态,一条动态可以有个标签;
属于”多对多”关系型数据库表;
数据库表
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(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 ();
使用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 ; const {filename,mimetype,size } = ctx.request .file ; const {id} = ctx.userInfo ; const result = await serviceFile.uploadAvatar (filename,mimetype,size,id); 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 create (ctx, next ) { const user = ctx.request .body ; const result = await service.create (user); ctx.body = result; } async showAvatarImage (ctx, next ) { const { userId } = ctx.params ; const avatarInfo = await fileService.showAvatarImageById (userId); console .log (avatarInfo) 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]); 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 ; const {filename,mimetype,size } = ctx.request .file ; const {id} = ctx.userInfo ; const result = await serviceFile.uploadAvatar (filename,mimetype,size,id); const avatar_url = `${SERVER_HOST} :${APP_PORT} /users/avatar/${id} ` ; const userResult = await userService.updateUserAvatar (avatar_url,id); 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 :
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" }
但是吧,这个跟我们平时见到的图片地址不一样,我们平时见到的图片地址是xx.jpg/png结尾的,而不是上面这种。下面讲述一下如何展示图片地址是xx.jpg/png结尾的。
上传头像2 参考文献:koa-multer实现图片上传 ;Koa实现图片上传功能 ;koa处理上传图片 ;
常见图片后缀的展示,需要用到node自带的path模块和static模块和第三方包koa/multer;
在avatar表中,现在的filename是存储的类似于e91b35e24586c1cb208a544f6acb5961
的图片信息,需要改为存储有后缀名的filename。
这里问了公司后端:filename一般都是设置为UNIQUE,且不会保留前端传过来的本身的文件名;因为如果传过来两个一样的文件名,后端不知道返回哪个;除非已经做好命名规范,可不设置为UNIQUE;
即:上传的文件名一般不保存,除非约定好了上传的文件名唯一;
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 ) => { 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 ; const { filename, mimetype, size } = ctx.request .file ; const { id } = ctx.userInfo ; const result = await serviceFile.uploadAvatar (filename, mimetype, size, id); const avatar_url = `${SERVER_HOST} :${APP_PORT} /users/avatar/${id} ` ; await userService.updateUserAvatar (avatar_url, id); ctx.body = { code : 0 , message : "上传成功" , data : avatar_url, }; } async uploadAvatar2 (ctx, next ) { const files = ctx.request .file ; const { filename, mimetype, size } = ctx.request .file ; const { id } = ctx.userInfo ; const result = await serviceFile.uploadAvatar (filename, mimetype, size, id); const avatar_url = `${SERVER_HOST} :${APP_PORT} /${filename} ` ; await userService.updateUserAvatar (avatar_url, id); 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); 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" }