写点什么

koa 实战

作者:coder2028
  • 2023-02-28
    浙江
  • 本文字数:7016 字

    阅读完需:约 23 分钟

项目初始化

// 初始化项目,生成package.jsonnpm init
复制代码


指定入口文件 main.js

项目的基础搭建

创建 src 工作目录

创建 main.js 主入口文件

在 main.js 中引入 koa


const koa = require('koa')const app = new Koa()
app.listen(3000, () => { // 监听成功的回调 console.log('server is running:http://localhost:3000')})
复制代码


node main.js 后即可通过访问 http://localhost:3000,访问到此项目

项目的基本优化

自动重启


// 热更新,只在开发模式下才会用的到npm install nodemon -D
复制代码


这时候我们安装的 nodemon 会在 package.json 中的 devDependencies 下

修改 script 选项


"scripts":{   "dev":"nodemon ./src/main.js"}
复制代码


使用 nodemon 启动,开发过程中的改动会自动重启


配置文件


我们开发的过程中还需要区分环境,开发、正式、测试等


// 安装dotenvnpm install dotenv -S
复制代码


在项目的根目录下创建.env文件

尽可能早的在项目中引入 dotenv 并进行环境变量的配置


const dotenv = require('dotenv')dotenv.config()// 经过了上面的配置,我们在.env文件中所配置的环境变量就已经被加载进process.env中了// 可以将环境变量导出,在需要用到的时候进行引入module.exports = process.env
复制代码


这样我们就在项目中配置了环境变量,配置环境变量还有另外一种方式,就是在 package.json 中的 script 中配置执行的命令,并指定环境变量,这样我们就不用新开一个文件在 js 文件中引用了

添加路由

// 这是一个构造函数const Router = require('koa-router')const router = new Router({ prefix: '/user' })
router.post('/register', (ctx, next) => {})
复制代码


通过引入 koa 的路由中间件 koa-router,我们可以设置项目的路由,通过在构造函数中传入prefix:'/user'可以设置路由的前缀,以作为不同功能模块的区分

目录结构的划分

我们在 main.js 中引入了 koa 启动了服务

又在 main.js 中引入了 koa-router 设置了项目的路由

但是随着功能的逐渐增多,项目变大,我们不能把所有的东西都写在 main.js 中,我们需要做功能模块的区分


抽离路由


在 src 目录下新建 router 文件夹,这个文件夹专门存放并管理项目中的路由。

  • 如果需要新增 user 的路由,就新建 user.route.js 文件

  • 如果需要新增 order 的路由,就新建 order.route.js 文件


新建好了各个功能模块的路由,我们需要一个index.js文件来作为路由的总入口文件,它负责引入各个功能模块的路由

const Router = require('koa-router')
const router = new Router()
const fs = require('fs')

// 需要使用nodejs的fs模块,来进行文件的读取和引入
fs.readdirSync(__dirname).forEach((file) => {
  // 读取当前目录下的文件['user.route.js','order.route.js']
  if (file !== 'index.js') {
    const currentFile = require('./' + file)
    // 注册路由
    router.use(currentFile.routes())
  }
})

module.exports = router


这样我们所有的路由都注册在了index.js中的总路由中,我们只需要在main.js中注册在 app 上,就可以实现路由的功能


const router = require('./router') // 引入index.js可以不用写
app.use(router.routes())// 这个是路由做的http允许的请求方法处理,如果不写这条语句,那么在使用别的httpMethod请求时,会抛出500的错误,加上了这一句,在请求方法不当的时候,会进行提示app.use(router.allowedMethods())
复制代码


抽离 app 服务


我们需要在 src 底下新建一个 app 文件夹专门管理我们的服务,因为有时候我们可能在一个项目中使用多个服务。可能是 express、可能是 koa、也可能是 node 中 http 模块

  • 在 src 下新建 app 目录

  • 在 app 目录下新建一个index.js文件,这个文件用于编写我们现在这个项目中主要用的服务,例如 koa,其余的服务可新开文件编写。


const koa = require('koa')const app = new Koa()const router = require('./router')
app.use(router.routes())app.use(router.allowedMethods())
// 注册号路由之后将app导出module.exports = app
复制代码


这样我们将服务抽离出来,在main.js中进行引入的时候,将 app 服务引入并监听即可


const app = require('./app/index.js')
app.listen(3000, () => { // 监听成功的回调 console.log('server is running:http://localhost:3000')})
复制代码


这样main.js就变的更加简洁了


抽离 controller


我们在 user.route.js 中写下了这样的代码


router.post('/register', (ctx, next) => {})// (ctx,next)=>{} 这个是用来处理register逻辑的函数,我们可以把它抽离成一个controller,专门用于处理各个访问的逻辑
复制代码


  • 在 src 目录下新建一个文件夹 controller

  • 在 controller 文件夹下新建一个文件,叫做 user.controller.js

  • 在 controller 文件夹下新建一个文件,叫做 order.controller.js


class UserController {  async registerUser() {    // 这里处理register的逻辑  }  async loginUser() {    // 这里处理login的逻辑  }}
module.exports = new UserController()
复制代码


抽离出来之后在路由文件中引入相应的 controller


抽离 service


我们在 controller 中要进行数据库的操作,我们把操作数据库的这一部分,抽离为 service

  • 在 src 文件夹下创建 service 文件夹

  • 在 service 文件夹下创建 user.service.js


参考 前端进阶面试题详细解答


class UserService {  // 处理创建用户的service  async createUser() {    // 这个内部封装了数据库的操作  }
// 处理更新用户的service async updateUser() {}}
复制代码


抽离数据库定义


sequelize 这个包专门用于项目中处理关系型数据库的操作,它是基于 promise 的


我们需要借助它来对数据库进行操作

  • npm install sequelize -S先安装

  • 在 src 下新建一个 db 目录用于管理此项目需要连接的数据库

  • 在 db 目录下新建一个 seq.js


const { Sequelize } = require('sequelize')const { HOST, PORT /*等等需要的配置*/ } = process.env
// 实例化sequelize对象const seq = new Sequelize( '要连接的数据库名称', '数据路的用户名', '数据库的密码', { // options host: '要连接的数据库的host', port: '要连接的数据路的端口', dialect: 'mysql', // 要操作的数据库类型 })
// 实例化过后就进行连接seq .authenticate() .then((res) => { console.log(res, '连接成功的回调') }) .catch((err) => { console.log(err, '连接失败的回调') })
module.exports = seq
复制代码


抽离 model


连接好了数据库之后,我们需要定义数据库表,这时候需要抽离一个 model 层,来定义数据库的表结构

  • 在 src 下新建一个 model 目录

  • 在 model 中新建一个 user.model.js,进行如下定义:


const seq = require('../db/seq')
// 创建User表,表名为user,user中有各项字段const User = seq.define('user', { { userName:{ type:DataTypes.String,// DataTypes是Sequelize中为我们提供的类型,需要引入 allowNull:false, // 是否允许空值,参考Sequelize文档 unique:true,// 是否允许唯一 comment:'字段注释', }, { password:{ type:DataTypes.String, allowNull:false, //... } } }})
module.exports = User
复制代码


当我们定义好了 User 的 model 之后,就可以在 user.service.js 中引入并使用它


const User = require('../model/user.model.js')
class UserService { // 处理创建用户的service async createUser(userName, password) { // 这个内部封装了数据库的操作,都是基于promise,需要进行try...catch错误捕获 const res = User.create({ userName, password }) return res.dataValues }
// 处理更新用户的service async updateUser() {}}
module.exports = new UserService()
复制代码


抽离中间件


当我们完成了这一系列的操作之后,就搭建起了一个接口编写的框架。我们可以在每一个 controller 中编写对应的业务处理。但是在我们编写接口的过程中,时常会碰到相同或相似的处理模块,这时候我们为了避免重复冗余的代码,需要把这些相同或相似的功能抽离成中间件。


  • 在 src 下新建一个文件夹叫做 middleware

  • 在 middleware 中新建一个文件叫做 user.middleware.js


const validateUserInfo = (ctx, next) => {  // 这里可以填充用户登录或注册时的校验方法}
const comparePassword = (ctx, next) => { // 这里可以填充修改密码时,两个密码进行对比的方法}
module.exports = { validateUserInfo, comparePassword,}
复制代码


抽离错误处理


Sequelize 是基于 promise 的数据库操作工具,我们在进行数据库操作或者日常代码编写的时候要进行错误处理,将错误处理的这一部分抽离出来,也会方便我们排查问题。


  • 在 src 下新建一个 constant 问价夹,专门用来存放代码中需要用到的常量

  • 在 constant 文件夹下新建一个 err.type.js 用来存储返回给前端的错误提示


module.exports = {  UserValidError = {    code:'10001',    message:'用户校验失败',    result:''  },  UserLoginError = {    code:'10002',    message:'用户登录失败',    result:''  }}
复制代码


将错误归集起来了之后,我们只需要在捕获到这个错误的时候使用它


// ctx中提供了当前的app,其中有一个emit的方法,可以传递一个事件,后面为该事件需要的参数ctx.app.emit('error', UserValidError, ctx)// 在app中使用on作为接收app.on('error', errHandler)
复制代码


// errHandlermodule.exports = (error, ctx) => {  // 这里的error就是UserValidError  // ctx 就是传递过来的ctx上下文  ctx.body = error // 把当前错误返回给前端}
复制代码


至此我们就完成了项目中的功能拆分,接下来就是在每个模块中填充相应的内容

注册接口的编写

注册的逻辑一般为用户提供用户名密码,传递给后端,后端拿到用户名和密码以后,首先要判断数据库中是否已经存在此用户,如果已经存在了这个用户,就返回提示码并告知前端,此用户已经注册。如果不存在,则对用户传递过来的密码进行加密,然后存储到数据库中。


这里只记录加密接口的步骤


// 使用bcryptjsconst bcrypt = require('bcrypt')
const cryptPassword = async (ctx, next) => { const { password } = ctx.request.body const salt = bcrypt.genSaltSync(10) // 加盐 const hash = bcrypt.hashSync(password, salt) ctx.request.body.password = hash await next()}
复制代码


经过此步骤加密之后,我们就可以往下处理注册是逻辑

  • 从 request.body 中取出加密之后的密码

  • 存储用户名和密码至数据库

  • 向用户返回结果

登录接口的编写

登录的逻辑一般为,用户输入用户名和密码进行登录。我们拿取到用户名和密码之后,要和数据库中的用户名和密码进行比较,如果比较失败,则返回用户失败的结果,否则登录成功,成功之后需要下发 token 以及 cookie 等。


这里只记录密码对比和 token 下发的步骤


// 密码对比bcrypt.compareSync('当前密码', '用户传递过来的密码') // 如果相同返回true,如果不同返回false
// 下发token 需要用到jsonwebtoken这个库// npm install jsonwebtoken -Sconst jwt = require('jsonwebtoken')// 从数据库中拿取出数据之后,除了密码以外,将其它的信息都用于token下发,也可以用作userInfo返回const { password, ...res } = await getUserInfo({ userName })// 那么这个res就是我们下发token需要用到const token = jwt.sign(res, '自己设置的加密串', { expiresIn: '1d' /*token的有效时间*/,})ctx.body = { code: 200, message: '登录成功', result: { token, },}
复制代码


这样我们就完成了登录的流程,将 token 下发给用户之后,用户以后的资源请求都需要将 token 携带过来,我们进行验证,如果验证成功,那么可以进行后续的操作,如果验证失败,那么用户就不能获取我们的真实资源。

验证中间件的编写

由于我们下发 token 之后的每一个接口都要通过验证之后才能向下进行,所以我们需要编写一个验证 token 的中间件

  • 在 middleware 这个文件加下创建 auth.middleware.js 文件


const jwt = require('jsonwebtoken')
const auth = (ctx, next) => { // 这里编写验证token的相关内容 const { authorization } = ctx.request.header const token = authorization.replace('Bearer ', '')
try { // 如果通过验证,会把我们先前用来生成token的信息返回,这里也就是除了password之外的其他用户信息 const user = jwt.verify(token, '我们先前设置的加密串') ctx.state.user = user // 我们把通过验证的用户信息放入state属性下的user中 } catch (error) { // 如果没有通过验证,那么有几种情况 // error.name === TokenExpiredError // error.name === JsonWebTokenError // 详情参考jsonwebtoken这个库的介绍 }}
复制代码


验证的中间件编写完成之后,我们的每一次需要验证 token 的请求,都会使用到它

数据上传

编写接口的同时我们要处理前端传递过来的数据,那么在 koa 中,数据上传需要用到一个中间件,就是koa-body

  • npm install koa-body -S 安装依赖

  • 在路由注册之前先注册koa-body


const KoaBody = require('koa-body')
app.use( KoaBody({ // ...options }))
// 注册路由app.use(router.routes())
复制代码


因为经过 koa-body 的处理,前端传递过来的请求数据会挂在ctx.request.body 上,我们在后续的路由处理中,在此处获取并处理即可


koa-body 有很多选项(比如是否支持文件上传等),具体参考手册

静态资源管理

如果想要前端通过浏览器的 uri 访问到本服务的静态资源,那么需要进行静态资源配置

需要使用到koa-static

  • npm install koa-static -S


const koaStatic = require('koa-static')
app.use(koaStatic('静态资源路径,最好借助path模块'))
复制代码


通过了这样的静态资源配置,前端就可以在浏览器上输入 uri 来访问到本服务的静态资源

sequelize 的基本理解

模型 model 时 sequelize 的本质,是数据库中表的抽象,在 sequelize 中是一个类


比如说,我们要创建一个用户表,那么首先需要定义一个 User 类,这个 User 类就是 sequelize 的模型。表中的每一条数据都是一个对象,每一个对象都是这个类的实例。而我们对 User 类的操作,或者是对实例(表中的每一条数据)的操作,都是类似操作 js 对象一样思想。有了这样的认识,可以帮助我们更好的理解 sequelize 的各项操作。


sequelize 文档

增删改查

做完前面的一些基础工作之后,最常见也是最经常写的就是 CRUD 了


####新增接口


第一步:定义路由,遵守 restfull 规范,定义为router.post('/order','中间件1','中间件2')


第二步:在controller中定义处理该路由的中间件


第三步:在service中定义写入数据库的方法,如果这一步需要用到新的 model,则先在model中定义好数据字段


//需要借助sequelize来进行数据库操作// 先把User模型给引进来const User = require('../model/user.model.js')// 新增操作需要在User表中新增一条数据,从类的角度来说,就是创建一个实例// 假设我们此时是在/src/service/user.service.jsclass UserService {  // 创建一个用于处理User model的类  async addUser({ id, userName }) {    // ID,userName是从controller中解析的    // 模型中有一个创建的方法    // 方法一:    const res = await User.create({ id, userName })    // 在没有错误的情况下,执行完毕这个操作,就会在user表中新增一条数据    // 方法二:    // sequelize的model为我们提供了创建实例的方法build    const res = User.build({ id, userName })    // 但是此时的build的方法,仅仅是创建出的一个对象,表示可以将这个对象映射到数据库中的数据,这个对象还并未真正的保存在数据库中,我们应该使用save方法,将其同步    return res.save()    // 执行完这一步才算是真正的同步至了数据库中    // 建议直接使用create方法,具体操作详见sequelize官方文档  }}module.exports = new UserService()
复制代码


第四步:注意错误捕获与错误处理

修改接口

第一步:定义路由,定义为router.put('/order/:id','中间件','中间件')


第二步:在controller中定义处理该路由的中间件


第三步:在 service 中定义修改数据库的方法


// 修改接口同新增接口// 假设我们此时在/src/service/user.service.jsclass UserService {// 新增用户的接口async addUser(){}// 更新用户的接口async updateUser({id,userName}){ // sequelize中为我们提供的更新方法也有两种 // 方法一: const res = await User.update({userName},{   where:{     id,     userName   } }) // 方法二: const res = await User.create({id,userName}) res.set({   userName:'xxx' }) return awaut res.save()}}
复制代码


第四步:注意错误捕获与错误处理

删除接口

删除首先要确定是使用硬删除,还是软删除。这二者的区别为硬删除为直接从数据库中的记录抹去,软删除为在数据库中增加一个标识字段,该字段标记了就代表删除了,但不是真正意义上的删除。


第一步:定义路由,定义为router.delete('/order/:id','中间件','中间件','中间件')


第二步:在controller中定义处理该路由的中间件


第三步:在service中定义删除该数据的方法,此时可以选择硬删除,或者是软删除,详见sequelize文档

查询接口

查询接口的思路同上


用户头像

coder2028

关注

还未添加个人签名 2022-09-08 加入

还未添加个人简介

评论

发布
暂无评论
koa实战_JavaScript_coder2028_InfoQ写作社区