快速构建express后端模版从qiya-cli开始

方焱
2023-12-01

写在前面的话

对于经常使用node的开发人员来说,每次搭建后台服务,都需要考虑如何建立一个更好的文件结构,而大部分的工作都是重复的,有时候会直接拷贝以前的项目文件,但是需要删除或修改很多东西,而且有很多都不需要的文件,这就很烦恼。

想到像vue-cli那样的脚手架一键生成基础项目模版,那我何不做多个属于自己的项目模版。使用的时候只需要一行命令就可以省去很多劳动力,不仅省时省事,而且可以定制自己想要的项目模版。说干就干,做脚手架之前先把模版做好,根据之前做小程序时搭建的express后端服务,这里做了一个基于express的纯后端模版。

使用模版

目前已经发布了脚手架工具qiya-cli,可以使用此模版快速生成后端项目,使用方法如下:

npm install qiya-cli -g
qiya init
复制代码

关于脚手架工具qiya-cli的更多功能可以参看qiya-cli,另外关于这个脚手架的搭建过程与发布,我会再写一篇文章详细介绍。

主要功能

注册与登录接口

支持JWT验证

Joi参数校验

支持mysql的orm框架sequelize

apidoc接口文档自动生成

全局参数配置

自动重启

redis支持

自动化测试

模版目录结构

.
├── README.md  // 说明文档
├── app.js  // express实例化文件
├── bin 
│   └── www // 主入口文件
├── config
│   ├── config.js // 数据库配置
│   └── index.js // 全局参数配置
├── control // Controller层目录
│   └── userControl.js
├── helper // 自定义API Error拋出错误信息
│   └── AppError.js
├── joi-rule // Joi 参数验证规则
│   └── user-validation.js
├── models // sequelize需要的数据库models
│   ├── index.js // 处理当前目录的所有model
│   └── user.js // user表的model
├── package-lock.json
├── package.json
├── public
│   └── apidoc // 自动生成的apidoc文档
├── routes // 路由目录
│   └── user.js
├── service // service层目录
│   └── user.js
└── until 
    └── token.js// token下发与验证
复制代码

主要文件说明

package.json

由于我的脚手架工qiya-cli使用了Metalsmith和Handlebars 修改模板文件,所以可以看到在项目名称、项目介绍、作者处使用模版语法。 此模版主要有三个scripts命令,npm run dev开发环境使用, npm run start生产环境使用, npm run apidoc自动生成apidoc文档(默认启动后访问 http://localhost:4000/apidoc即可看到api文档和进行接口测试),

    {
    "name": "{{project}}",
    "version": "1.0.0",
    "description": "{{description}}",
    "main": "index.js",
    "scripts": {
        "dev": "NODE_ENV=development nodemon ./bin/www",
        "start": "NODE_ENV=production node ./bin/www",
        "apidoc": "apidoc -i ./routes/ -o ./public/apidoc/",
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "{{author}}",
    "license": "ISC",
    "dependencies": {
        "body-parser": "^1.18.3",
        "cookie-parser": "^1.4.4",
        "debug": "^4.1.1",
        "env2": "^2.2.2",
        "express": "^4.16.4",
        "express-validation": "^1.0.2",
        "http-status": "^1.3.1",
        "ioredis": "^4.6.2",
        "joi": "^14.3.1",
        "jsonwebtoken": "^8.5.1",
        "mysql2": "^1.6.5",
        "sequelize": "^5.1.0",
        "sequelize-cli": "^5.4.0",
        "uuid": "^3.3.2"
    },
    "apidoc": {
        "name": "{{project}}",
        "version": "1.0.0",
        "description": "{{project}}项目API文档",
        "title": "{{project}} API",
        "url": "http://localhost:4000",
        "forceLanguage": "zh-cn"
    },
    "devDependencies": {
        "nodemon": "^1.18.10"
    }
}
复制代码

.env.example

为了防止敏感数据被放到git仓库中,我引入一个被 .gitignore 的 .env 的文件,以 key-value 的方式,记录系统中所需要的可配置环境参数。并同时配套一个.env.example 的示例配置文件用来放置占位,.env.example 可以放心地进入 git 版本仓库。在实际使用过程中,只需拷贝一份此文件重命名为.env,然后修改为真实的配置信息即可

# 服务的启动名字和端口,但也可以缺省不填值,默认值的填写只是一定程度减少起始数据配置工作
HOST = 127.0.0.1
PORT = 4000

# MySQL 数据库链接配置
MYSQL_HOST = localhost
MYSQL_PORT = 3306
MYSQL_DB_NAME = 数据库名
MYSQL_USERNAME = 数据库用户名
MYSQL_PASSWORD = 数据库密码

#jwt secret秘钥(自己设置一个复杂的)
JWTSECRET = JWTSECRET
复制代码

routes > user.js

由于引入了apidoc,所以在写路由的时候注意按照以下格式书写api说明。如果是个人项目,可能会感觉麻烦,不需要这种方式,但是如果是协同工作或者项目比较大的时候,有一个良好的api文档就显的非常重要了。所以建议日常项目中养成写api文档的习惯,对以后的工作将会有很大的帮助。更详细的apidoc文档配置可以参考【ApiDoc】官方文档(翻译)官网

const express = require('express')
const router = express.Router()
const control = require('../control/userControl')
const validate = require('express-validation')
const paramValidation = require('../joi-rule/user-validation')

/**
* @api {post} /v1/user/login 用户登录 
* @apiDescription 用户登录
* @apiName login
* @apiGroup user
* @apiParam {string} [username]  用户名
* @apiParam {string} password 密码
* @apiSuccess {sting} token token
* @apiSuccessExample {json} Success-Response:
* {head:{'code':0,'msg':'ok'},data:{'token':''}
* @apiSampleRequest http://127.0.0.1:4000/v1/user/login
* @apiError (Error 400) {String} EMPTY_ERROR [没有传入用户名或密码]
* @apiErrorExample {json} 参数为空
* {"message": ["\"password\" is not allowed to be empty"],"code": 400,"stack": {}}
* @apiVersion 1.0.0
*/
router.route('/user/login')
    .post(validate(paramValidation.userLogin), control.login)

/**
* @api {post} /v1/user/sign 用户注册 
* @apiDescription 用户注册
* @apiName sign
* @apiGroup user
* @apiParam {string} username  用户名
* @apiParam {string} password 密码
* @apiParam {string} gender 性别
* @apiParam {string} avatar_url 头像
* @apiSuccess {string} result result
* @apiSuccessExample {json} Success-Response:
* {head:{'code':0,'msg':'ok'},data:{'result':'注册成功'}
* @apiSampleRequest http://127.0.0.1:4000/v1/user/sign
* @apiError (Error 400) {String} EMPTY_ERROR [没有传入用户名或密码]
* @apiErrorExample {json} 参数为空
* {"message": ["\"password\" is not allowed to be empty"],"code": 400,"stack": {}}
* @apiError (Error 200) {String} EXIST_USER_ERROR [用户名已存在]
* @apiErrorExample {json} 用户已存在
* {head:{'code':0,'msg':'ok'},data:{'result':'用户名已存在'}
* @apiVersion 1.0.0
*/
router.route('/user/sign')
    .post(validate(paramValidation.userSign), control.sign)

module.exports = router
复制代码

joi-rule > user-validation.js

Joi参数校验规则

const Joi = require('joi')

module.exports = {
    //POST /v1/user/login 
    userLogin: {
        body: {
            username: Joi.string().required(),
            password: Joi.string().required(),
        }
    },
    // POST /v1/user/sign
    userSign: {
        body: {
            username: Joi.string().required(),
            password: Joi.string().regex(/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[\(\)])+$)([^(0-9a-zA-Z)]|[\(\)]|[a-z]|[A-Z]|[0-9]){6,}$/).min(8).required(),
            gender: Joi.any().allow('1', '2', '0'),
            avtar_url: Joi.string().regex(/^((ht|f)tps?):\/\/([\w\-]+(\.[\w\-]+)*\/)*[\w\-]+(\.[\w\-]+)*\/?(\?([\w\-\.,@?^=%&:\/~\+#]*)+)?/)
        }
    }
}
复制代码

untils > token.js

jwt验证的实现逻辑,包括token的下发和校验

const jwt = require('jsonwebtoken')

// 下发token
function createJwt(opt) {
    const payload = {
        user_id: opt.user_id,
        name: opt.name,
        exp: Math.floor(new Date().getTime() / 1000) + 60 * 60,
    }
    return jwt.sign(payload, process.env.JWTSECRET)
}

// 解析token
function parse(token) {
    if (token) {
        try {
            return jwt.verify(token, process.env.JWTSECRET)
        } catch (err) {
            return null
        }
    }
    return null
}

// 验证token
function verifyToken(token, user_id) {
    if (token) {
        jwt.verify(token, process.env.JWTSECRET, (error, decode) => {
            if (error) {
                console.log('token 验证错误信息', error)
                return false
            }
            if (decode.user_id) {
                return user_id == decode.user_id
            } else {
                return false
            }
        })
    } else {
        return false
    }
}

module.exports = {
    createJwt,
    parse,
    verifyToken
}
复制代码

models > index.js

sequelize实例化,以及映射数据库表

const fs = require('fs')
const path = require('path')
const Sequelize = require('sequelize')
const configs = require('../config/config')
const basename = path.basename(__filename)
const env = process.env.NODE_ENV || 'development'
const config = {
    ...configs[env],
    define: {
        underscored: true
    }
}
const db = {}
let sequelize = null

if (config.use_env_variable) { // 连线字串的方式连线
    sequelize = new Sequelize(process.env[config.use_env_variable], config)
} else {
    sequelize = new Sequelize(config.database, config.username, config.password, config)
}

// require 将相同目录底下的 .js 以 model.name 当索引值放到 db 物件中。
// 执行每一个 model 的 「define」将 资料表与 js 对应上
fs
    .readdirSync(__dirname)
    .filter(file => {
        return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js')
    })
    .forEach(file => {
        var model = sequelize['import'](path.join(__dirname, file))
        db[model.name] = model
    })
    // 来执行 db 物件裡的每一个 .associate method
    // 执行每一个 「model 关联」 的设定,也就是关联式资料库的 foreign key 的设定与 js 对应上。
Object.keys(db).forEach(modelName => {
    if (db[modelName].associate) {
        db[modelName].associate(db)
    }
})

// 将全域的物件与类别,也放进 db 物件中。
// 将所有关于 MVC 的 M 都收敛在 db 里
db.sequelize = sequelize
db.Sequelize = Sequelize
module.exports = db
复制代码

相关工具介绍

sequelize

Sequelize是一款基于Nodejs功能强大的异步ORM框架。同时支持PostgreSQL, MySQL, SQLite and MSSQL多种数据库,很适合作为Nodejs后端数据库的存储接口,为快速开发Nodejs应用奠定扎实、安全的基础。

相关文档:

apidoc

apidoc是一款可以由源代码中的注释直接自动生成api接口文档的工具,它几乎支持目前主流的所有风格的注释。例如: Javadoc风格注释(可以在C#, Go, Dart, Java, JavaScript, PHP, TypeScript等语言中使用)

相关文档:

JOI

joi就好比是一个验证器,你可以自己规范schema来限制资料格式,有点像是正规表示法,这边来举个例子好了,利如PORT只允许输入数字若输入字串就会被阻挡PORT: Joi.number(),这样有好处万一有使用者不按照规范输入数值他会在middleware抛出一个错误告诉你这边有问题要你马上修正。

相关文档:

JWT

JWT是JSON Web Token的缩写,通常用来解决身份认证的问题,JWT是一个很长的base64字串在这个字串中分为三个部分别用点号来分隔,第一个部分为Header,里面分别储存型态和加密方法,通常系统是预设HS256杂凑演算法来加密,官方也提供许多演算法加密也可以手动更改加密的演算法,第二部分为有效载荷,它和会话一样,可以把一些自的定义数据存储在Payload里例如像是用户资料,第三个部分为Signature,做为检查码是为了预防前两部分被中间人伪照修改或利用的机制。

Header(标头):用来指定哈希算法(预设为HMAC SHA256) Payload(内容):可以放一些自己要传递的资料 Signature(签名):为签名检查码用,会有一个serect string来做一个字串签署 把上面三个用「。」接起来就是一个完整的JWT了!

使用流程: 使用者登入-> 产生API Token -> 进行API 路径存取时先JWT 验证-> 验证成功才允许访问该API

相关文档:

更多模版

目前此模版已存在qiya-cli中,后续将会在qiya-cli中添加更多的模版,方便日常开发,减少重复工作。

目前计划中的项目模版

  • 基于mpvue的前端项目模版
  • 适用于小程序的后端项目模版,开箱即用(包含jwt验证)
  • 基于koa的纯后端项目模版
  • 基于vue2.x + Element的后台管理系统模版
  • ...

合作计划

如果你有更好的项目模版,无论是前端还是后端,都欢迎提交PR到github.com/gengchen528…,或者在下方留言。

当然你的模版必须要有一份详细的功能说明及核心文件详解,并且在package.json中把项目名称使用{{project}},项目说明使用description,作者使用author替换。

我会把好的项目模版添加到qiya-cli中,并且注明模版提供者与提供者git链接。希望qiya-cli能够成为一个全面的项目模版工具,只要你想要的模版都能在qiya-cli中快速找到。

项目GIT链接

脚手架

qiya-cli

模版:

qiya-cli-express-template(此模版)

联系我

如果你有好的项目模版,欢迎联系我

 类似资料: