背景

自从实现了客户端和治理后盾我的项目后,一个残缺的web利用前端方面的我的项目算是搭建实现了。最初还须要有一个提供API服务的后端我的项目服务前端利用运行,通过一个月的开发,当初基本上曾经全副实现,通过部署后,实现了最初的上线运行。

我的项目应用技术栈

本我的项目是基于nodejs次要应用koa+mongodb为外围开发的轻量级服务端利用。接口是依照RESTful格调进行设计

应用次要中间件有
koa-compress,
koa-parameter,koa-connect-history-api-fallback,koa-static,koa-mount。具体应用形式请在koa官网仓库查看

数据库操作:mongoose

接口权限验证:jsonwebtoken

用户明码加密:bcryptjs

上传资源存储:koa-multer

路由散发:koa-router

接口参数解析: koa-bodyparser

接口应用文档: https://konglingwen94.github....

开发过程

数据库设计

本项目选择应用mongodb作为数据存储的数据库,因为其对于前端开发者有着人造的有好性,应用容易上手。
因为自己涉猎服务端畛域尚处于初级阶段,在数据库设计方面教训无限,在此分享进去仅供参考应用。

mongodb应用bson类型作为数据存储格局。因为跟前端js的json类型能够相互转换应用。所以这就减小了入门者设计数据库表字段的难度,咱们能够参照前端页面须要展现的数据进行设计。
而后通过应用mongoose这个库作为疾速操作数据库的模型,咱们能够用代码的模式设计mongodb表字段的模型,通过mongoose的编译能够存储到实在的数据库表中。

拿本我的项目中的商品表来说,这是一个申明好的mongooseSchema

 {    name: String,    price: Number,    oldPrice: Number,    description: String,    sellCount: Number,    rating: Number,    info: String,    menuID: ObjectId,    image: String,    online: { type: Boolean, default: true },  },

通过mongoose模型的编译办法后存储到数据库中的字段是这个样子

数据库存储字段

FieldTypeDescription
menuIDObjectId商品分类 ID
nameString商品题目
infoString商品信息
descriptionString商品简介
imageString商品封面
onlineBoolean是否公布
oldPriceNumber商品原价
priceNumber商品售价
sellCountNumber售卖个数
查看残缺的模型文件点这里

接口搭建

一个残缺的API接口从接管申请到响应数据实现,两头这个过程就是服务端解决各种代码逻辑的。这其中次要包含裸露接口地址,接口权限验证申请参数验证查询数据库返回响应信息这几个阶段。为了合乎服务端业务逻辑分层设计的模式,每一个解决阶段都能够抽离到一个独自的模块,最初再把各种相关联的模块组装起来打包成一个残缺的我的项目,这样的模块化设计能够很大的加强我的项目的维护性可读性。用目录构造的形式展示就是这个样子的

├── model  // 数据库模型│   ├── administrator.js│   ├── seller.js│   ├── rating.js│   ├── category.js│   └── food.js├── helper│   ├── validatorRules.json  // 参数验证规定│   ├── mongoose.js  // mongoose连贯脚本│   ├── middleware.js // 我的项目中间件│   └── util.js  // 工具函数├── controller  // 控制器│   ├── administrator.js│   ├── seller.js│   ├── rating.js│   ├── category.js│   └── food.js├── config│   └── config.default.json  // 我的项目配置文件├── router│   └── index.js  // 路由配置

model文件夹用来放数据库表模型,数据库存储了哪些字段在这个文件目录查看高深莫测。helper目录寄存了一些辅助的我的项目文件和一些脚本,其中middleware.js这个文件寄存了整个我的项目的所有中间件,依照模式分层的准则,我把服务端接口的一些解决逻辑都抽离到了中间件里,其中包含接口权限验证申请参数验证这两个次要的代码解决逻辑。controller目录则是寄存接口业务逻辑的中央,咱们也把他叫做控制器查询数据库返回响应信息也是在这个模块外面实现的。最初就是对立散发路由接口,router目录是我的项目所有接口散发的中央,在这里能够把不同的控制器散发到一个或多个路由接口地址上,这样能够实现控制器文件的复用,不须要写反复的业务代码。

权限验证和登录(蕴含注册性能)

面向多用户服务的后端我的项目,权限验证是不可或缺的。本我的项目应用了authorization申请头验证的形式判断每一个申请的权限。为了不便解决,我把这一块的代码逻辑抽离到了一个中间件里。这样对每一个接口是否验证权限也容易治理和浏览。本我的项目的权限验证应用jsonwebtoken这个第三方插件作为生成秘钥token的工具,用户在登录的时候服务端会生成一个token响应到前端,前端依据运行环境把它存储下来,之后的每一个申请依据业务须要携带这个token传递到服务端,服务端依据设置好的验证规定返回不同的验证后果,这就是本我的项目接口权限验证整体运行过程。

通过[控制器中的用户登录接口]()剖析其中的业务逻辑时怎么解决的

// 这里仅展现业务逻辑代码  async login(ctx) {    const { username, password } = ctx.request.body;    let result = await AdministratorModel.findOne({ username });    //如果没有后果则 创立新用户    if (!result) {      // 加密明码      const hashPass = await bcrypt.hash(password, 10);      const newUser = await AdministratorModel.create({ password: hashPass, username });      const token = jwt.sign({ username, role: newUser.role, level: newUser.level }, secretKey, {        expiresIn,      });      return (ctx.body = { admin: omit(newUser.toObject(), ["password"]), token });    }    if (!bcrypt.compareSync(password, result.password)) {      ctx.status = 400;      return (ctx.body = { message: "明码谬误" });    }    const user = result.toObject();    const token = jwt.sign(user, secretKey, { expiresIn });         ctx.body = { admin: omit(user, ["password"]), token };  },

为了反对治理先天首次登陆即注册的性能,本登录代码接口也蕴含了用户注册的业务逻辑。通过参数解析和校验的过程后(代码局部以中间件解决的形式在其余模块),通过解构即取到了前端传递的无效参数。依据数据库查问的后果解决不同的业务逻辑,在取到创立后的用户信息后需通过jsonwebtoken的签名生成一个token,此token也是其余接口在验证用户登录状态时惟一的验证信息。

通过登录接口生成token后咱们就能够对其余须要增加拜访权限的接口进行鉴权验证了。上面是通过验证token是否无效判断用户登录状态的逻辑代码中间件

// 对立抽离到一个中间件中,这里省略了引入其余模块的过程module.exports={ adminRequired() {    return async (ctx, next) => {      let token = ctx.headers["authorization"];      if (!token) {        ctx.status = 400;        return (ctx.body = { message: "没有传递token" });      }      token = token.split(" ")[1];      try {        var decodeToken = jwt.verify(token, secretKey, { expiresIn });      } catch (error) {        ctx.status = 403;        if (error.name === "TokenExpiredError") {          return (ctx.body = { message: "过期的token" });        }        return (ctx.body = { message: "有效的token" });      }      ctx.state.adminInfo = decodeToken;      await next();    };  }, }

申请进入到这里后,通过认证申请头先提取到token变量,当token取到具体值后,再应用jsonwebtoken外部提供的验证函数校验,依据不同的验证后果响应不同的状态码和错误信息。具体的验证后果谬误类型自行到插件仓库查看,这里不做具体介绍。当验证通过后会解析出token的签名内容,如果鉴权接口其余中央的业务逻辑须要用到此信息的话,咱们能够把它挂载到koa提供的特定命名空间字段上,这样不便部分的逻辑代码获取。

备注:token应用的认证类型须要依据前后端开发人员的约定应用,本我的项目应用Bearer ${token}的格局作为令牌拜访头

为了合乎koa中间件导出格局的设计准则,这个文件的中间件是以闭包的模式导出的,理论利用到接口上的是这个闭包函数,这样设计的益处是咱们在调用中间件函数的时能够传递参数进去,外部理论失效的中间件能够依据内部传递的参数做逻辑上的解决。在路由配置表外面对立应用这个中间件的形式是这个样子的

//局部代码省略const Router = require("koa-router");const router = new Router({ prefix: "/api" });const middleware = require("../helper/middleware");router.post("/admin/foods", middleware.adminRequired(),FoodController.createOne);

数据库分页查问性能

对于大多数前端我的项目,分页显示数据在一个十分常见的性能,对应到服务端的代码逻辑就是数据库的过滤查问。应用mongoose提供的过滤查问操作API能够很容易实现这个需要,当咱们用到的中央比拟多的时候,问题就呈现了。对于前端申请的接口门路个别是这个样子的/api/foods?page=1&size=20,咱们须要对传递的querystirng做进一步的判断和解析能力利用到数据库参数的查问上。问题是很多个接口都须要这个性能,应用起来比拟繁琐,那不如咱们把这个解析查问参数的过程抽离成一个模块,这样更不便咱们应用和保护。当初让咱们看一下封装好的全副代码吧!

module.exports = {  resolvePagination(pagination = {}) {    const defaults = { page: 1, size: 10 };    pagination.page = parseInt(pagination.page, 10);    pagination.size = parseInt(pagination.size, 10);    if (Number.isNaN(pagination.page) || pagination.page <= 0) {      pagination.page = defaults.page;    }    if (Number.isNaN(pagination.size) || pagination.size <= 0) {      pagination.size = defaults.size;    }    const { page, size } = pagination;    return {      page,      size,    };  },  resolveFilterOptions(filter = {}) {    let sort = {      createdAt: -1,    };    sort = defaults({}, filter.sort, sort);    const { page, pageSize } = resolvePagination({      page: filter.page,      size: filter.size,    });    return {      limit: size,      skip: (page - 1) * size,      sort,    };  },};

首先通过resolvePagination这个函数咱们能够解析出无效的query参数,在通过resolveFilterOptions这个函数解析进去合乎mongoose数据筛选操作的查问选项。通过模块化引入的操作形式,利用到理论的数据库查问过程中如下

// 代码片段来自我的项目`controller`目录const {  resolveFilterOptions, resolvePagination } = require("../helper/utils");module.exports={ async queryListByOpts(ctx) {    const { page, size } = resolvePagination({ page: ctx.query.page, size: ctx.query.size });    const { skip, limit, sort } = resolveFilterOptions({ page, size });    const total = await FoodModel.countDocuments();    var results = await FoodModel.find().populate("category").sort(sort).skip(skip).limit(limit);    ctx.body = {      data: results,      total,      pagination: {        page,        size,      },    };  },}

从代码中能够看到以获取到前端传递的query类型参数为解析值,resolvePagination函数负责解析无效的数据分页查问选项,resolveFilterOptions函数解析进去了mongoose特定查问语句格局的参数,咱们通过拆散业务代码和逻辑代码的形式无效加强了代码的模块化构造,也减少了代码的复用性,进步了我的项目的开发效率。

利用的部署和运行

本我的项目应用github-actions的继续集成性能主动部署到云服务器,有了继续集成的服务,就省去了我的项目手动构建,测试,公布这一系列流程,而且升高了手动操作程序出错的危险,具体的配置文件如下

name: Deploy fileson: [push]jobs:  build:    name: Build    runs-on: ubuntu-latest    steps:    - uses: actions/checkout@master    - name: copy file via ssh key      uses: appleboy/scp-action@master      with:        host: ${{ secrets.SERVER_HOST }}        username: ${{ secrets.SERVER_USERNAME }}        key: ${{ secrets.SERVER_SSH_KEY }}        port: ${{ secrets.SERVER_PORT }}        source: "*"        target: "/var/www/elm-seller-server"    - name: executing remote ssh commands using ssh key      uses: appleboy/ssh-action@master      with:        host: ${{ secrets.SERVER_HOST }}        username: ${{ secrets.SERVER_USERNAME }}        key: ${{ secrets.SERVER_SSH_KEY }}        port: "22"        script: |          cd /var/www/elm-seller-server          npm install                    npm start          

从配置文件中看出,服务器公布的环境变量都采纳了加密的形式传递,比方${{secrets.SERVER_HOST}}这个环境变量,实在的存储值须要咱们在github仓库的设置面板里的secret选项配置的,当本地应用git治理的仓库推送到近程仓库的时候就会触发github-actions的主动部署操作,同时咱们还能够在workflows文件夹上面配置多个以.yml结尾的配置文件,一个配置文件对应一个actions部署工作,本我的项目我就应用了两个继续集成的工作,因为我的项目对应的阐明文档也须要及时的更新公布。至于部署文件模板怎么抉择须要依据集体的需要本人抉择设置,github-actions官网市场提供了罕用的集成工作模板供咱们抉择

公布到云服务器的利用我抉择应用PM2治理利用,利用启动的配置文件点这里。pm2是一个面对node利用的管理工具,咱们能够不便的查看,重启,删除,进行,启动利用

API文档编写

文档的撰写是一个后端我的项目不可或缺的一部分内容,学会写文档能够回顾我的项目从设计到开发的过程,发现有问题的中央能够第一工夫发现,及时的修复bug。我的项目文档是应用markdown语法编写的REAEME文件,所有文件均在我的项目的docs目录内。文档应用vuepress作为构建工具预览和公布。具体应用形式自行查看官网文档,不做具体介绍

文档公布地址:https://konglingwen94.github....

工具和环境

vscode mac node mongodb git github postman ssh

总结心得

从我的项目的需要布局,到数据库表设计,api接口逻辑关注点的拆散,最初胜利的部署运行以及文档的撰写实现,本人初步把握了服务端我的项目残缺的开发流程,并积攒了一些开发教训能够在这分享。

作为编程开发人员,在我的项目开发过程中遇到困难是很失常的,尤其是在调试代码的时候各种各样的错误信息看的"目迷五色",尤其是服务端node的环境没有浏览器客户端调试不便。遇到代码出错不要怕,咱们须要一步步排查出错的起因,如果错误信息看起来不直观咱们能够借助第三方工具调试,本我的项目我应用的是nodemon这个工具,他能够热加载利用,也能够开启debug的命令关上一个相似浏览器开发者工具的调试面板,咱们能够在控制台面板查看程序抛出的错误信息,在source面板查看出错的代码堆栈,借助这些工具的剖析,只有有急躁,一点点思考呈现谬误的问题,最终咱们肯定能够解决它。

反对

感激所有点赞和关注的小伙伴们,对本我的项目有趣味的同学能够一块和我交换,欢送在上面留言!

如果您对本我的项目由好的倡议或者发现bug能够到我的项目仓库提issues,也欢迎您的珍藏和关注,谢谢!

仓库地址:https://github.com/konglingwe...

文档地址:https://konglingwen94.github....