Web项目实践@树洞(接口篇)

原创
2021/07/13 17:21
阅读数 419

总算把《树洞》这个项目相对完整地撸出来了,陆陆续续花了一个月左右的时间。这个项目在很久以前就准备搞一搞,现在也算了了一桩心事。

先简单说一说后端接口服务(毕竟不是专职做服务端,太深的东西也讲不了)。

技术选型

开发语言我选择的是Nodejs,当然选择其他语言慢慢也能磨出来,不过有点磨洋工的意思。另外一个重要的原因是Nodejs毕竟和前端的关联要多,多熟悉一下对于深度学习前端总是没错。框架选择采用Koa2,一来确保ECMAScript2015+能流畅地应用,二来之前用过稍微熟悉一点。关于Koa2的Generator有很多,其中不乏针对API开发的,我用的是koa2-generator。从代码上看,它大概是修改的express-generator,我又从中做了一些修改以满足我的需求。

数据库最开始我准备使用Mongodb或者Sqlite,怕麻烦的我最终还是选择了MySQL。为了避免去写一些SQL执行脚本,我又勉为其难地使用ORM框架sequelize。如此一来,只需要去MySQL创建一个Table,让程序跑起来就行了。

项目运行环境:Node版本14.46.0,MySQL版本8.0.25.0,Sequelize版本6.6.4。(必须要提一下我的项目运行环境,它直接影响到相关API的使用,比如Node版本之间API差异、Sequelize V6在使用上也有一定的变化。)

生成项目

npm i -g koa2-generator
koa2 tree-hole
npm i

我项目中用到的中间件稍微有点差异,所以app.js也有些不同,具体可以查看我的源码

项目生成以后,创建以下几个目录:

  • sequelize:存放配置数据库配置文件和sequelize初始化脚本。
  • schema:存放sequelize表结构定义文件。
  • model:存放sequelize数据增删改查处理模型文件。
  • controller:存放接口数据交互文件。
  • common:存放公共接口交互文件,这里我只有一个图片上传处理。
  • routes:存放路由文件。
  • utils:存放工具文件,这里我只有一个超级管理员创建脚本。

这些目录下的文件工作流程大概是:运行package.json中的命令bin/www启动Node服务器;服务器执行app.js中的代码到路由;路由执行controller;controller执行model;model执行schema触发数据库操作,发现表不存在则创建表。有两个比较特殊的处理:common中的图片上传,我是直接上传到Node服务器,它不会进行数据库操作;utils中的超级管理员创建可以不启动Node服务器直接执行controller,它可能会执行失败当后台管理员的表未被创建的时候,这个时候需要再执行一次。

数据库

安装MySQL,创建一个名为tree-hole的Table备用。其中使用的字符集之类的我使用默认,到目前为止还没有出问题,就将就如此了。

在sequelize目录下创建config.js用于sequelize的初始化,比如数据库名、账号、密码等等。一般在系统不会只在一个环境运行,所以数据库的配置也会有一些差别,比如host。这个时候config文件就非常有用,我只是在进行开发环境进行实验性操作,因此只做了dev。如果需要完善一些,就会需要用到dotenv之类的工具,或者直接修改Node的环境变量,然后通过process.env.NODE_ENV这样的变量对配置进行处理。

新建index.js文件,初始化sequelize,将初始化的实例暴露出去以供后续使用。

// 初始化之后,可以使用authenticate方法进行连接测试
sequelize.authenticate().then(() => {
	console.log('Connection has been established successfully.')
}).catch(err => {
	console.error('Unable to connect to the database: ', err)
})

数据库连接成功,下面开始建表。以letterlog举例,在schema下新建一个letterlog.js。

const { DataTypes } = require('sequelize')
const sequelize = require('../sequelize')

module.exports = sequelize.define('letterlog', {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    allowNull: false,
    autoIncrement: true
  },
  letterId: {
    type: DataTypes.INTEGER,
    allowNull: false,
    field: 'letterId',
    comment: '信笺id'
  },
  action: {
    type: DataTypes.INTEGER,
    allowNull: false,
    field: 'action',
    comment: '行为 0-否定 1-赞同 2-分享'
  },
  sender: {
    type: DataTypes.INTEGER,
    allowNull: false,
    field: 'sender',
    comment: '发送者编码'
  },
  receiver: {
    type: DataTypes.INTEGER,
    allowNull: false,
    field: 'receiver',
    comment: '接收者编码'
  },
}, {
  freezeTableName: true,
  timestamps: true
})

表建好后进行数据库操作,在model下新建一个letterlog.js。

const letterlog = require('../schema/letterlog')

class LetterlogModel {
  static async createLetterlog(data) {
    return await letterlog.create({
      letterId: data.letterId,
      action: data.action,
      sender: data.sender,
      receiver: data.receiver
    })
  }
}

module.exports = LetterlogModel

这样一个letterlog表增加数据的模型就建好了,在controller新建一个letterlog.js进行交互处理。

const LetterlogModel = require('../model/letterlog')

class LetterlogController {
  static async create(ctx) {
    const req = ctx.request.body
    if (req.letterId && req.action && req.sender && req.receiver) {
      try {
        await LetterlogModel.createLetterlog(req)
        ctx.body = {
          code: 200,
          message: '创建成功',
          data: null
        }
      } catch(err) {
        ctx.body = {
          code: 412,
          message: '创建失败',
          data: null
        }
      }
    } else {
      ctx.body = {
        code: 416,
        message: '缺少必要参数',
        data: null
      }
    }
  }
}

module.exports = LetterlogController

接着在路由中引入controller,只有引入进路由后sequelize才会在服务器启动的时候进行表的初始化操作。值得注意的是controller返回的必须是Promise,否则会出现错误。

const Router = require('koa-router')
const LetterlogController = require('../controller/website/letterlog')

// 我这里给接口加了一个/api的前缀
const router = new Router({
  prefix: '/api'
})

router.post('/letterlog/create', LetterlogController.create)

module.exports = router

最后在app.js中引入路由文件。

const router = require('./routes')

app.use(router.routes())

我个人觉得sequelize有时候也不那么好用,比如进行一些复杂的查询。我有这么一个表设计:letter表存了两种存在父子关系类型的数据,我需要先查到父级数据然后根据父级id对子级数据进行查询,到目前为止我还不知道怎么用sequelize进行处理。当然,我承认我的表也存在设计上的问题,不过我觉得用SQL直接写我可以搞出来。

关于sequelize的建表、字段类型以及后面查询等请自行查看Sequelize ORM官方文档

业务逻辑

业务逻辑上没什么可以介绍,项目只是做了简单的增、改、查,还有一个上传图片的操作。需要注意的是Node的静态文件操作,我用的是koa-static中间件,上传的文件放到uploads的目录下。

const koaStatic = require('koa-static')

app.use(koaStatic (path.join(__dirname, './uploads')))

这个配置没毛病,于是我返回图片上传后的链接http://xxx/uploads/images/xxx.jpg,结果不能访问。后来我才发现,koa-static处理后静态路径后就变成了域名部分,不需要再加这个路径,http://xxx/images/xxx.jpg就能访问了。

业务逻辑处理得差不多了,总不能随随便便就让用户访问到自己的接口吧。因此,我简简单单地做了个登录(通常密码不可能做这么简单),再简简单单做个jwt验证。

/**
 * user.js
 * 登录成功,生成token,返给用户
 */
const jsonwebtoken = require('jsonwebtoken')
static async login(ctx) {
  const req = ctx.request.body
  try {
    const res = await CuserModel.getUser(req)
    if (res) {
      /* 登录成功后,生成token */
      const token = jsonwebtoken.sign({
        id: res.id,
        account: req.account,
        password: req.password
      }, 'tree-hole', {
        expiresIn: '7d'
      })
      /* 成功处理 */
    } else {
      /* 失败处理 */
    }
  } catch(err) {
    /* 错误处理 */
  }
}

/**
 * app.js
 * jwt校验token
 */
const jwt = require('koa-jwt')
app.use(jwt({
  secret: 'tree-hole'
}).unless({
  path: /^((?!token).)*$/    // 路由以token开头的路径需要进行校验
}))

最后

项目是东拉西扯的搞完了,只能算是初做尝试,勉强为前端服务提供了API。革命尚未成功,同志仍需努力。

## 代码仓库 ##

前后端所有代码都在一个仓库不同的分支,代码拉下来切换分支即可。

仓库地址:GiteeGithub

##@树洞系列文章##

Web项目实践@树洞(接口篇)

Web项目实践@树洞(前端篇vue3)

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
0 收藏
0
分享
返回顶部
顶部