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"  }