总算把《树洞》这个项目相对完整地撸出来了,陆陆续续花了一个月左右的时间。这个项目在很久以前就准备搞一搞,现在也算了了一桩心事。
先简单说一说后端接口服务(毕竟不是专职做服务端,太深的东西也讲不了)。
技术选型
开发语言我选择的是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。革命尚未成功,同志仍需努力。
## 代码仓库 ##
前后端所有代码都在一个仓库不同的分支,代码拉下来切换分支即可。
##@树洞系列文章##
Web项目实践@树洞(接口篇)