likes
comments
collection
share

Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~

作者站长头像
站长
· 阅读数 13

纯前端不了解全栈开发但想学?赶紧看过来!本文通过node server实战分享,带你快速上手全栈开发,掌握后端开发实战技巧。通过koa2 + mongo实现服务端功能以完成前端发布平台的业务需求。小前端也有一个全栈梦!!!

本文是《实战前端发布平台,打开CICD黑盒》专栏的第二篇——实战前端发布平台后端。文章之间存在关联,对整个系列感兴趣的朋友可以关注专栏把其余文章也一起看看~

系列文章:

本文为第二篇「前端发布平台 node server 实战」的实战记录分享,主要内容是通过 Koa + mongoDb 实现 项目构建配置CRUD 功能,如项目的仓库信息、构建分支、打包命令等...为后续的前端自动化部署奠定基础

快速看源码

一、Koa

在上一篇文章中,已经用 koa2 + @koa/router 搭建了一个初始化的后端项目了,本文将在之前的基础上进行扩展和完善,let's go!

1. 路由模块

回顾上一篇文章,笔者已经用 @koa/router 实现了简单的路由,并且可以通过 postman 中发送 getpost 请求成功响应。接下来,我们需要把 route 模块进行系统化的整理和抽离,总不能都把路由信息写到入口文件吧。

// 入口js
const Koa = require('koa')
const Router = require('@koa/router')

const router = new Router()
const app = new Koa();

router.get('/test', (ctx, next) => {
  ctx.body = {
    code: 0,
    data: { name: '井柏然-get' }
  }
}) // 待抽离整理

router.post('/test', (ctx, next) => {
  ctx.body = {
    code: 0,
    data: { name: '井柏然-post' }
  }
}) // 待抽离整理

app.use(router.routes()).use(router.allowedMethods())

app.listen(3000);

整理思路:

  • 路由分类。按照功能模块进行路由分类,如案例的 test 模块,一会要用的存放配置的 config 模块。 Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~
  • 命名统一。清晰整个 mvc 链路,如请求 routestest 模块,其执行函数是在 controller 目录 中的 testKoa+mongoDb手把手实战「前端发布平台」后端!值得收藏~

接下来,开始整理,将原首页的内容放到 routes/test.js 文件中,包进执行函数 initTestRoute 中,并导出。

// test路由文件 routes/test.js
function initTestRoute (router) {
  router.get('/test', (ctx, next) => {
    ctx.body = {
      code: 0,
      data: { name: '井柏然-get' }
    }
  })

  router.post('/test', (ctx, next) => {
    ctx.body = {
      code: 0,
      data: { name: '井柏然-post' }
    }
  })
}

module.exports = {
  initTestRoute // 导出 test 模块的路由初始化函数
}

routes/index.js 中导入 每个模块的init函数,放在一个全局 init 的函数中统一执行,最后导出 全局路由init函数。(就是将每个模块的init统一管理调用,在routes的入口中汇集而已)

// 路由入口文件 routes/index.js
const { initTestRoute } = require('./test')
const { initConfigRoute } = require('./config')

function initGlobalRoute (router) {
  initTestRoute(router) // 调用 test 模块路由注册
  initConfigRoute(router) // 调用 config 模块路由注册
}

module.exports = {
  initGlobalRoute // 导出全局路由注册
}

最后,在 koa 入口中调用 initGlobalRoute 函数,如下代码:

const Koa = require('koa')
const Router = require('@koa/router')
const { initGlobalRoute } = require('./routes/index')

const router = new Router()
const app = new Koa();

initGlobalRoute(router) // 注册全局路由

app.use(router.routes()).use(router.allowedMethods())

app.listen(3000);

紧接着,我们再使用 postman 对 test 模块进行请求:

Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~

可以看到,postget 请求都有了正确的返回。接下来,我们把整个后端处理链路(每一层都按照这个模块划分规范)统一整理一波!

2. 整理controller+services+model

从路由层的“抛砖引玉”,我们把这种模块划分的整理思路套到每个分层中!

回顾一下上一篇中提到的后端分层。 Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~ 用户发起请求 -> route层 -> controller层 -> service层 -> db

我们依照这个分层,将上文中的按模块整理的思路放到每一个分层中。

首先是 controller 层(处理业务逻辑):

// test路由文件 routes/test.js
const controller = require('../controller/test')

function initTestRoute (router) {
  router.get('/test', controller.get)

  router.post('/test', (ctx, next) => {
    ctx.body = {
      code: 0,
      data: { name: '井柏然-post' }
    }
  })
}

module.exports = {
  initTestRoute
}

很简单,对比上文实现,笔者只是将原本处理 get 请求的函数抽离到了 controller/test 中,在 routes层 中去调用 controller层 的方法。

// controller层的实现 controller/test.js
function get (ctx, next) {
  ctx.body = {
    code: 0,
    data: { name: '井柏然-get(放在controller里啦!)' } // 这里改了文案跟上面形成对比
  }
  next()
}

module.exports = {
  get
}

这时候看 postman 的请求结果。跟预想中的返回一样: Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~

好,按照这样的思路,对整个后端的分层、模块进行整理。如 services 层,也是按照当前模块划分,进行相应的数据库数据查询操作; model 层按照模块划分,定义不同模块中的数据模型。因为思路都是类似的,笔者就不再展开赘述了,只要按照这个思路将各层按模块划分好即可。

3. 中间件

Koa中间件大家可能多多少少都有听过,但是可能没自己玩过!这里笔者借着实战的场景,把中间件也用上,通过场景更加加深大家对中间件的使用理解。

中间件使用场景:

  • 需要对服务器处理的每个请求的返回-response做一层拦截,以便后续拓展,如需要对一些错误信息进行统一收集等。
  • 每个进入的请求都要进行登陆校验权限校验,以统一进行相应的逻辑处理。

讲到Koa中间件,一定要讲一下洋葱模型: Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~

光看图可能很难 get 到要点,笔者这里根据官网的 解释 外加一个 demo 来跟大家一起体会一下这幅图的含义。首先看看官网的一段解释: Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~ 重点看笔者划线的部分:调用 next() 该函数暂停执行,控制传递给下一个中间件,并且没有其他中间件时,恢复执行。

这么说可能还是有点懵,直接上demo。官网的案例还是复杂了点,直接撸个简单的demo。笔者直接在上述的入口文件中添加如下代码,通过 console.log 来输出 1-4 的数字。

app.use(function fn1 (ctx, next) {
  console.log(1)
  next()
  console.log(2)
})
app.use(function fn2 (ctx, next) {
  console.log(3)
  next()
  console.log(4)
})

这里大家不妨先试着想一下输出结果,结合 Koa 官网对 next函数 的解释(当前暂停执行,控制传递下一中间件),应该都能想到答案。

Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~ 如上图蓝色圈,输出的结果:1-3-4-2。毫无意外是不是!细心的童鞋可能已经发现,笔者在传入中间件的 function 中都加了函数名 fn1fn2,为的就是把 next() 机制转换成伪代码方便大家理解。如下:

function fn1 () {
    console.log(1)
    fn2() // 把原本的 next 替换成 fn2
    console.log(2)
}
function fn2 () {
    console.log(3)
    next() // 如果还有 fn3 那就一直这样嵌套调用下去而已
    console.log(4)
}

换成这样的写法,是不是就很清晰了。其实 next 就是实现了一个函数嵌套调用。这样一来,再结合上文提到的洋葱模型的图片,应该就能很好的理解中间件的执行机制了。最后,笔者再撸个请求-响应流的图跟大家一起巩固一下洋葱模型!

Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~

紧接着,笔者通过实现一个中间件对所有的响应进行拦截,来完善整个请求-响应流的返回结果。根据 Koa 官网推荐的命名空间,笔者在这里对整个 controller层 的返回结果进行一个约定,约定请求处理的结果按照规定字段放在 ctx.state.apiResponse 中。

Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~

中间件代码实现如下:

import { RESPONSE_CODE } from '../constant'

export function handleResponse () {
  return async function (ctx, next) {
    await next()
    // controller 层的结果处理结果放置 ctx.state.apiResponse 中
    const { code, data, msg } = ctx.state.apiResponse
    ctx.body = getResult(code, data, msg)
  }
}

function getResult (code, data, msg) {
  const result = {
    code,
    data: null,
    msg: null
  }
  if (code === RESPONSE_CODE.SUC) {
    result.data = data // 响应成功
  }

  if (code === RESPONSE_CODE.ERR) {
    result.msg = msg // 响应失败的msg
  }

  return result
}

最后,我们需要在 入口app 中使用我们的中间件 handleResponse (权限的就不演示了,每个童鞋的场景都不一样,只要按照这个思路,自己整个也是没问题的~),这里需要注意一点就是,我们这个中间件是需要对所有的返回做拦截的,按照洋葱模型的请求流向,handleResponseuse 的位置应该在 koa-router 中间件的前面。代码如下:

// index 入口文件
const Koa = require('koa')
const Router = require('@koa/router')
const { initGlobalRoute } = require('./routes/index')
const { handleResponse } = require('./middleware')

const router = new Router()
const app = new Koa();

initGlobalRoute(router)

app.use(handleResponse()) // 中间件实现返回拦截

app.use(router.routes()).use(router.allowedMethods()) // 路由相关

app.listen(3000);

紧接着,我们对 test 模块的 get请求按照刚才的约定,进行一定的代码改造,再通过 postman 进行请求,看看返回的结果。

const { RESPONSE_CODE } = require('../constant')

function get (ctx, next) {
 // 按照约定,把返回的内容包裹在 ctx.state.apiResponse 中
  ctx.state.apiResponse = {
    code: RESPONSE_CODE.SUC, // 约定的字段 code
    data: { name: '井柏然-get(放在controller里啦!)' } // 约定的字段 data
  }
  next()
}

module.exports = {
  get
}

通过 get 请求可以得到我们期望的返回: Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~

ok,讲到这里,整个 Koa 的一些基础开发工作就算是差不多了,基本可以在这个基础上进行相应的业务开发了。紧接着我们进入到下一个阶段——数据库。上手完数据库~我们就能进行愉快的 crud 操作了!!!

二、mongoDb

到了这个阶段,我们首先要做的事情就是各种安装配置!配置的话是因为我们 devprod 环境中所需要连接的数据库地址不一样,所以趁着这次的配置,顺便在这里把项目的一些配置都给整一整。

1. nodemon

"scripts": {
  "dev": "node ./src/index.js" // 这样每次修改文件都需要重新启动
},

当前项目仅是通过 node 入口文件 的方式去执行的,所以每当代码修改,都需要重启服务。这时候你一定很想念开发前端项目时,打包工具给我们提供的 HMR 功能!这个时候不用我多说了, nodemon 就是 nodeHMR !修改代码后会自动重启服务进程。

接着我们安装、配置一下 nodemon 就ok啦。配置啥的就不展开往下说啦,毕竟不是这个章节的重点,想详细了解的童鞋可以看笔者的 github源码~

"scripts": {
  "dev": "nodemon ./src/index.js --watch server --exec babel-node" // 自动重启香
},

细心的伙伴可能发现启动命令后面带了一大段参数,作用是让我们能在 koa 中就可以用 es6 模块导入。哈哈哈,由于笔者之前都没写过 commonjs 模块化,所以这次 demo 特地写了一下 cjs,结果发现自己还是写习惯了 esm ,趁着这次机会,换回来换回来,不得不说啊,笔者真的喜欢折腾~

2. 安装、连接数据库

首先得安装个 mongoDb 。安装这一块就不演示了,毕竟大家系统不一样,安装的方式都不同,笔者这里自己撸一个~安装成功后,通过 mongod -version 能看到相关的信息。(当然也可以通过 docker 下载安装mongo,笔者是用 docker 下的) Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~ 这里说多一句,刚开始笔者下的是 mongodb 6.0+ 折腾了一会,发现遇到一些问题网上都没找到解决的办法,可能是版本比较新吧,毕竟是才发布没多久的,所以后来笔者久用回5版本的了~

现在安装好数据库,先把数据库服务进程给启起来,然后要为这次发布平台项目新建一个数据库,就命名为 cicd 吧。

启动的方式各异,笔者也不对这个点进行展开说了。可以用命令的,或者像笔者一样通过 docker 去启动也是可以的。在镜像中找到 mongodb ,然后运行。 Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~

首次运行完后,后续要启动数据库可以直接在 容器 中去启动,非常方便。 Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~

浏览器访问 http://localhost:27017/ 出现如下界面证明数据库启动成功了。需要注意的是,端口 27017 为默认端口,如果使用了自定义的端口记得要换过来。 Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~

接着通过 shell 命令,可以在命令行窗口执行 新建、切换数据库 等各种操作:

# 输入以下命令进入 docker root
docker exec -it cicd-mongo bash
# 输入 mongo 后进入交互式程序
mongo
# 查看数据库
show dbs 
# 创建、切换到 cicd 数据库
use cicd 

讲到这里,数据库的安装就算完了,此时我们还需要再装一个 mongoose 的包,配合着 Koa 项目使用爽得很!详细了解可以戳下他的github。笔者自己先安装了,大家自己动动手吧~

安装好之后,我们可以在项目层面进行数据库的连接了!回到项目代码中,开始进行简单的数据库连接。等连通成功后,再去接着搞一波环境配置。整个的代码非常简单,核心就是通过 mongoose.connect 去连接数据库,传入对应的 mongodb 地址即可,笔者直接贴出来代码:

import mongoose from 'mongoose'

const db = mongoose.connection

export const connect = function () {
  mongoose.connect('mongodb://localhost:27017/cicd') // 暂时写死数据库uri
  db.on('error', console.error.bind(console, 'mongodb connect error'))
  db.once('open', console.log.bind(console, 'mongodb connect success'))
}

现在,我们在入口中使用上面这段代码进行数据库连接,然后 pnpm dev 试试! Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~ ok,控制台完美输出,那接下来就到下一步搞个环境配置,完了就可以开始令人心动的实战了~

3. 环境变量配置

之前有提到,数据库配置在不同的环境下肯定也是不同的,所以我们这里还要顺带把数据库的各环境配置也给准备好,后续的开发、部署上线就能省点事了。

首先,在之前预留的 config 文件中添加几个 js 文件,如下图所示: Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~ 我们分别把需要针对环境进行不同配置的数据配置到 .env.js 文件中。

// 开发环境 development
export default {
  db: {
    uri: 'mongodb://localhost:27017/cicd'
  }
}

// 生产环境 production
export default {
  db: {
    uri: 'mongodb://xxx_xxx_xxx_prod/cicd'
  }
}

然后,我们只需要根据 process.env.NODE_ENV 去取当前环境的配置即可,直接给出需要改动的地方吧,详细的也不展开说了,这一块大家应该也比较熟悉了。

// packge.json 启动命令添加 环境变量
"scripts": {
  "dev": "export NODE_ENV=development && nodemon ./src/index.js --exec babel-node"
}

// 连接数据库中写死的地址改为变量
export const connect = function () {
  mongoose.connect(config.db.uri) // 将写死的 uri 该成变量,跟随环境变量变化
  db.on('error', console.error.bind(console, 'mongodb connect error'))
  db.once('open', console.log.bind(console, 'mongodb connect success'))
}

搞定了 mongodb 的基础工作之后,就可以进入业务阶段的开发了。接下来通过实战:构建配置crud 的方式跟大家一起熟悉整个后端的开发模式,并为后续实现 前端发布平台 + jenkins 实现自动化部署功能做一个基础的铺垫。

三、实战构建配置CRUD

进入实战阶段,首先需要一个明确的目标,当然也就是提需求阶段!有了需求,才好去落实。那么回顾上一篇文章中提到的一些构建需要的配置,笔者在此再进行一个罗列:

  1. 项目名称
  2. 项目源码(git仓库地址)
  3. 需要构建的分支
  4. 需要执行的打包命令
  5. 上传到文件服务器的目录

上述这5点,就是本次实战中需要实现的构建配置,我们需要实现配置的 保存 、 修改 、 删除 操作以满足我们发布平台的业务需求。

1. 定义Schema

Schema 大概就是定义一个数据的基本格式,是一个集合。笔者是这么理解的,好比一张表有一些字段,每个字段是什么类型的、如何定义它而已,就是一个静态的数据格式。比如,笔者就为上述的配置定义个 Schema

import mongoose from 'mongoose'

const configSchema = new mongoose.Schema({
  projectName: {
    type: String // 项目名称
  },
  gitUrl: {
    type: String // 项目源码(git仓库地址)
  },
  gitBranch: {
    type: String // 需要构建的分支
  },
  buildCommand: {
    type: String // 需要执行的打包命令
  },
  uploadPath: {
    type: String // 上传到文件服务器的目录
  }
})

通过定义个这么个 schema ,把笔者需要的配置信息的数据模型就已经定义好了,接下来就是通过 crud 去操作数据库的这张表了。

我们把表名命为 jobConfig ,并导出 mongoose.model(表名, Schema)

export default mongoose.model('jobConfig', configSchema)

对于 mongoose.Schemamongoose.model,笔者理解他们的区别就是 Schema 是定义数据结构的,而 model 是根据这个结构去操作数据的,我们的 crud 就是通过 model 这一层去实现的。

2. 实现配置保存

  1. routes 层。新建 jobConfig.js 文件,提供一个 post 的路由入口,调用 controller 层的 save 方法

    export function initConfigRoute (router) {
      router.post('/job/save', controller.save)
    }
    
  2. controller 层。也是新建 jobConfig 文件,实现保存相关的业务逻辑。

    这里补充说明一下,由于需要解析 post 请求的 request body,所以我们还需要安装个 koa-body 的包来帮助我们~ pnpm i koa-body 跑起来。安装完成后,在入口文件中使用该中间件。

    // 入口文件
    app.use(KoaBody({
      multipart: true
    }));
    

    那么,接下来就是 controller 层的逻辑了,我们直接看代码(每一步都有注释)

    export async function save (ctx, next) {
      // 首先拿到 request body
      const requestBody = ctx.request.body
    
      try {
        // 调用 services 层的 save 方法。这个下面会展开代码
        await services.save(requestBody)
        // 这里是保存成功后的处理
        ctx.state.apiResponse = {
          code: RESPONSE_CODE.SUC,
          data: null
        }
        // 还记得之前写返回拦截中间件时定义的 apiResponse 的返回规范吗?这里就这样用了
      } catch (e) {
        // 处理保存失败的返回
        ctx.state.apiResponse = {
          code: RESPONSE_CODE.ERR,
          msg: '配置数据保存失败'
        }
      }
      // koa-router 也是一种中间件模式,所以我们这里要加个next
      /** 
       * 比如路由是这样写的 router.get('/test', conttoller1, controller2)
       * 那 controller2 就需要 controller1 提供一个 next 才会执行到了
      **/
      next()
    }
    
  3. services 层。上面 controller 层调用了 services 层的 save,那我们就需要在 services 层提供一个 save 方法。save 的实现按照 mongosse 的用法来写即可,代码实现如下:

    import JobModel from '../model/jobConfig'
    
    /**
     * 这里导入的 JobModel 就是我们在 model 层定义 Schema 时,导出的 model
     * 没错,就是 export default mongoose.model('jobConfig', configSchema) 这句代码
    **/
    // params 就是保存的参数,在 request body 中获得的
    export function save (params) {
      // 这里就时 mongoose 保存数据,操作 model 的用法。
      return new JobModel(params).save()
    }
    

ok,以上就是所有我们要实现的 save 的业务代码了,很简单有没有。那这个时候,我们打开 postman 来验证一下,看看能不能把配置成功保存到数据库里吧。

这里贴出需要保存的配置参数:

{
    "projectName": "cicd-project",
    "gitUrl": "git://xxx",
    "gitBranch": "master",
    "buildCommand": "pnpm run build",
    "uploadPath": "/static"
}

话不多说,直接用 postman post 一下!看起来好像成功了 Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~

接着,我们通过命令去查看一下,是否真的把我们的配置信息存到数据库里面了。

# 切换数据库
use cicd
# 查看所有数据集合
show collections 
# 查看集合中的所有数据
db.jobConfigs.find()

结果如下(为了演示效果,笔者先通过 db.jobConfigs.drop()jobConfigs 的集合删除): Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~ 根据结果可以看到,我们通过 postman 发送的 post 请求,保存项目名称 projectName"cicd-project" 的配置信息已经成功保存到数据库中了!

3. 实现配置更新 & 删除

由于我们已经把保存功能实现了,配置的更新、删除也就是在数据库操作那一层会有点不一样而已,所以笔者这里不会详细的展开,只放上核心的实现代码。大家如果想详细了解的话,可以到 github 上去看整个后端的源码~废话不多说,我们接着撸起来,就快完工啦~

  1. routes 层。添加 updatedelete 的路由。
    router.post('/job/update', controller.update)
    router.post('/job/delete', controller.del)
    
  2. controller 层。实现 updatedelete 的业务逻辑,并调用 services 层实现数据库操作。这一层的实现基本跟 save 是一样的,所以不展开啦~
  3. services 层。这里稍有不同,就是我们需要调用的数据库操作的方法不一样。我们可以通过 mongoose 中的 findByIdAndUpdatefindByIdAndDelete 实现更新、删除。
    export function update (id, params) {
      // 这里需要2个参数:一个id(到时候前端是能拿到的),一个新的配置
      return JobModel.findByIdAndUpdate(id, params) 
    }
    
    export function deleteById (id) {
      // 通过 id 删除配置
      return JobModel.findByIdAndDelete(id)
    }
    

好了,代码都撸完了,捣鼓一下 postman 去。首先来试试 update 的: 参数除了笔者圈出来的都一致,当然还多了一个 id 的参数哈哈~ Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~ 看到 postman 的返回结果是成功的,接着我们去数据库那里查询一下。 Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~ 没有意外,重新查询后,库里的 projectName 的数据值已经更新了。删除的功能也是可以的,笔者就不再演示啦,结果都符合预期!

到这里,整个实战Koa + mongoDb后端的篇章就完结啦,接下来笔者会接着分享 node server 打通 jenkins 实现自动化部署核心流程的文章,主要是通过后端调 jenkins 的openapi 实现 freestyle job 的新建、替换、构建执行,完成整个前端发布平台的自动化部署核心功能。大家可以关注笔者或者关注下该专栏,笔者一定快马加鞭的撸文章给大家~

写在最后

有些时候技术这东西真的不是难不难的说,更多可能是机遇的问题。像笔者做了几年了都是纯前端,也就今年才开始接触 node server。工作实战中没有机会接触后端,也就只能一时想学 node server 就去学学,写个 koaexpress 案例 这样子。但是效果并不好,隔段时间不接触,又什么都不会了。所以,机遇还是很重要的,但是话说回来,有时候工作中真的没有机会搞node server,那就只能靠自己了,其实可以跟着这篇文章的思路走,自己给自己出个需求,做个发布平台啊啥的,一定是有实战意义的,然后需要从0-1去搭建一个后端,使用多种技术栈~最关键是自己提需求,自己实现需求。在实现需求的过程中,就跟实战场景很相似了,会遇到很多坑,要用各种方法、技术不断地解决问题。当通过折腾把问题解决了,印象就很深刻了,等过了这个阶段,你会发现自己已经具备了一定的 node server 开发能力。所以,还是得自己多动手去实现,实在没项目可做的,照着笔者的方向自己去做一个发布平台也是会有收获的,大家一起加油吧~

转载自:https://juejin.cn/post/7151939505883185183
评论
请登录