Koa+mongoDb手把手实战「前端发布平台」后端!值得收藏~
纯前端不了解全栈开发但想学?赶紧看过来!本文通过node server实战分享,带你快速上手全栈开发,掌握后端开发实战技巧。通过koa2 + mongo实现服务端功能以完成前端发布平台的业务需求。小前端也有一个全栈梦!!!
本文是《实战前端发布平台,打开CICD黑盒》专栏的第二篇——实战前端发布平台后端。文章之间存在关联,对整个系列感兴趣的朋友可以关注专栏把其余文章也一起看看~
系列文章:
本文为第二篇「前端发布平台 node server
实战」的实战记录分享,主要内容是通过 Koa + mongoDb 实现 项目构建配置 的 CRUD
功能,如项目的仓库信息、构建分支、打包命令等...为后续的前端自动化部署奠定基础。
快速看源码
一、Koa
在上一篇文章中,已经用 koa2
+ @koa/router
搭建了一个初始化的后端项目了,本文将在之前的基础上进行扩展和完善,let's go!
1. 路由模块
回顾上一篇文章,笔者已经用 @koa/router
实现了简单的路由,并且可以通过 postman 中发送 get
、 post
请求成功响应。接下来,我们需要把 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 模块。
- 命名统一。清晰整个 mvc 链路,如请求
routes
的test
模块,其执行函数是在controller
目录 中的test
。
接下来,开始整理,将原首页的内容放到 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 模块进行请求:
![]() | ![]() |
---|
可以看到,post
、 get
请求都有了正确的返回。接下来,我们把整个后端处理链路(每一层都按照这个模块划分规范)统一整理一波!
2. 整理controller
+services
+model
层
从路由层的“抛砖引玉”,我们把这种模块划分的整理思路套到每个分层中!
回顾一下上一篇中提到的后端分层。
用户发起请求 -> 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 的请求结果。跟预想中的返回一样:
好,按照这样的思路,对整个后端的分层、模块进行整理。如 services
层,也是按照当前模块划分,进行相应的数据库数据查询操作; model
层按照模块划分,定义不同模块中的数据模型。因为思路都是类似的,笔者就不再展开赘述了,只要按照这个思路将各层按模块划分好即可。
3. 中间件
Koa中间件大家可能多多少少都有听过,但是可能没自己玩过!这里笔者借着实战的场景,把中间件也用上,通过场景更加加深大家对中间件的使用理解。
中间件使用场景:
- 需要对服务器处理的每个请求的返回-response做一层拦截,以便后续拓展,如需要对一些错误信息进行统一收集等。
- 对每个进入的请求都要进行登陆校验、权限校验,以统一进行相应的逻辑处理。
讲到Koa中间件,一定要讲一下洋葱模型:
光看图可能很难 get 到要点,笔者这里根据官网的 解释 外加一个 demo 来跟大家一起体会一下这幅图的含义。首先看看官网的一段解释:
重点看笔者划线的部分:调用
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函数
的解释(当前暂停执行,控制传递下一中间件),应该都能想到答案。
如上图蓝色圈,输出的结果:
1-3-4-2
。毫无意外是不是!细心的童鞋可能已经发现,笔者在传入中间件的 function
中都加了函数名 fn1
、 fn2
,为的就是把 next()
机制转换成伪代码方便大家理解。如下:
function fn1 () {
console.log(1)
fn2() // 把原本的 next 替换成 fn2
console.log(2)
}
function fn2 () {
console.log(3)
next() // 如果还有 fn3 那就一直这样嵌套调用下去而已
console.log(4)
}
换成这样的写法,是不是就很清晰了。其实 next
就是实现了一个函数嵌套调用。这样一来,再结合上文提到的洋葱模型的图片,应该就能很好的理解中间件的执行机制了。最后,笔者再撸个请求-响应流的图跟大家一起巩固一下洋葱模型!
紧接着,笔者通过实现一个中间件对所有的响应进行拦截,来完善整个请求-响应流的返回结果。根据 Koa 官网推荐的命名空间,笔者在这里对整个 controller层
的返回结果进行一个约定,约定请求处理的结果按照规定字段放在 ctx.state.apiResponse
中。
中间件代码实现如下:
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
(权限的就不演示了,每个童鞋的场景都不一样,只要按照这个思路,自己整个也是没问题的~),这里需要注意一点就是,我们这个中间件是需要对所有的返回做拦截的,按照洋葱模型的请求流向,handleResponse
的 use
的位置应该在 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 请求可以得到我们期望的返回:
ok,讲到这里,整个 Koa 的一些基础开发工作就算是差不多了,基本可以在这个基础上进行相应的业务开发了。紧接着我们进入到下一个阶段——数据库。上手完数据库~我们就能进行愉快的 crud
操作了!!!
二、mongoDb
到了这个阶段,我们首先要做的事情就是各种安装、配置!配置的话是因为我们 dev
、 prod
环境中所需要连接的数据库地址不一样,所以趁着这次的配置,顺便在这里把项目的一些配置都给整一整。
1. nodemon
"scripts": {
"dev": "node ./src/index.js" // 这样每次修改文件都需要重新启动
},
当前项目仅是通过 node 入口文件
的方式去执行的,所以每当代码修改,都需要重启服务。这时候你一定很想念开发前端项目时,打包工具给我们提供的 HMR
功能!这个时候不用我多说了, nodemon
就是 node
的 HMR
!修改代码后会自动重启服务进程。
接着我们安装、配置一下 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 下的)
这里说多一句,刚开始笔者下的是
mongodb 6.0+
折腾了一会,发现遇到一些问题网上都没找到解决的办法,可能是版本比较新吧,毕竟是才发布没多久的,所以后来笔者久用回5版本的了~
现在安装好数据库,先把数据库服务进程给启起来,然后要为这次发布平台项目新建一个数据库,就命名为 cicd 吧。
启动的方式各异,笔者也不对这个点进行展开说了。可以用命令的,或者像笔者一样通过 docker 去启动也是可以的。在镜像中找到 mongodb ,然后运行。
首次运行完后,后续要启动数据库可以直接在 容器 中去启动,非常方便。
浏览器访问 http://localhost:27017/
出现如下界面证明数据库启动成功了。需要注意的是,端口 27017
为默认端口,如果使用了自定义的端口记得要换过来。
接着通过 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 试试!
ok,控制台完美输出,那接下来就到下一步搞个环境配置,完了就可以开始令人心动的实战了~
3. 环境变量配置
之前有提到,数据库配置在不同的环境下肯定也是不同的,所以我们这里还要顺带把数据库的各环境配置也给准备好,后续的开发、部署上线就能省点事了。
首先,在之前预留的 config
文件中添加几个 js
文件,如下图所示:
我们分别把需要针对环境进行不同配置的数据配置到
.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
进入实战阶段,首先需要一个明确的目标,当然也就是提需求阶段!有了需求,才好去落实。那么回顾上一篇文章中提到的一些构建需要的配置,笔者在此再进行一个罗列:
- 项目名称
- 项目源码(git仓库地址)
- 需要构建的分支
- 需要执行的打包命令
- 上传到文件服务器的目录
上述这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.Schema
和 mongoose.model
,笔者理解他们的区别就是 Schema
是定义数据结构的,而 model
是根据这个结构去操作数据的,我们的 crud
就是通过 model
这一层去实现的。
2. 实现配置保存
-
routes 层。新建
jobConfig.js
文件,提供一个post
的路由入口,调用controller
层的save
方法export function initConfigRoute (router) { router.post('/job/save', controller.save) }
-
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() }
-
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
一下!看起来好像成功了
接着,我们通过命令去查看一下,是否真的把我们的配置信息存到数据库里面了。
# 切换数据库
use cicd
# 查看所有数据集合
show collections
# 查看集合中的所有数据
db.jobConfigs.find()
结果如下(为了演示效果,笔者先通过 db.jobConfigs.drop()
把 jobConfigs
的集合删除):
根据结果可以看到,我们通过 postman 发送的 post 请求,保存项目名称
projectName
为 "cicd-project"
的配置信息已经成功保存到数据库中了!
3. 实现配置更新 & 删除
由于我们已经把保存功能实现了,配置的更新、删除也就是在数据库操作那一层会有点不一样而已,所以笔者这里不会详细的展开,只放上核心的实现代码。大家如果想详细了解的话,可以到 github 上去看整个后端的源码~废话不多说,我们接着撸起来,就快完工啦~
- routes 层。添加
update
、delete
的路由。router.post('/job/update', controller.update) router.post('/job/delete', controller.del)
- controller 层。实现
update
、delete
的业务逻辑,并调用services
层实现数据库操作。这一层的实现基本跟save
是一样的,所以不展开啦~ - services 层。这里稍有不同,就是我们需要调用的数据库操作的方法不一样。我们可以通过 mongoose 中的
findByIdAndUpdate
、findByIdAndDelete
实现更新、删除。export function update (id, params) { // 这里需要2个参数:一个id(到时候前端是能拿到的),一个新的配置 return JobModel.findByIdAndUpdate(id, params) } export function deleteById (id) { // 通过 id 删除配置 return JobModel.findByIdAndDelete(id) }
好了,代码都撸完了,捣鼓一下 postman 去。首先来试试 update
的:
参数除了笔者圈出来的都一致,当然还多了一个 id
的参数哈哈~
看到 postman 的返回结果是成功的,接着我们去数据库那里查询一下。
没有意外,重新查询后,库里的
projectName
的数据值已经更新了。删除的功能也是可以的,笔者就不再演示啦,结果都符合预期!
到这里,整个实战Koa + mongoDb后端的篇章就完结啦,接下来笔者会接着分享 node server
打通 jenkins 实现自动化部署核心流程的文章,主要是通过后端调 jenkins 的openapi 实现 freestyle job
的新建、替换、构建执行,完成整个前端发布平台的自动化部署核心功能。大家可以关注笔者或者关注下该专栏,笔者一定快马加鞭的撸文章给大家~
写在最后
有些时候技术这东西真的不是难不难的说,更多可能是机遇的问题。像笔者做了几年了都是纯前端,也就今年才开始接触 node server
。工作实战中没有机会接触后端,也就只能一时想学 node server
就去学学,写个 koa
、express
案例 这样子。但是效果并不好,隔段时间不接触,又什么都不会了。所以,机遇还是很重要的,但是话说回来,有时候工作中真的没有机会搞node server
,那就只能靠自己了,其实可以跟着这篇文章的思路走,自己给自己出个需求,做个发布平台啊啥的,一定是有实战意义的,然后需要从0-1去搭建一个后端,使用多种技术栈~最关键是自己提需求,自己实现需求。在实现需求的过程中,就跟实战场景很相似了,会遇到很多坑,要用各种方法、技术不断地解决问题。当通过折腾把问题解决了,印象就很深刻了,等过了这个阶段,你会发现自己已经具备了一定的 node server
开发能力。所以,还是得自己多动手去实现,实在没项目可做的,照着笔者的方向自己去做一个发布平台也是会有收获的,大家一起加油吧~
转载自:https://juejin.cn/post/7151939505883185183