快速構建express後端模版從qiya-cli開始

寫在前面的話

對於常用node的開發人員來講,每次搭建後臺服務,都須要考慮如何創建一個更好的文件結構,而大部分的工做都是重複的,有時候會直接拷貝之前的項目文件,可是須要刪除或修改不少東西,並且有不少都不須要的文件,這就很煩惱。html

想到像vue-cli那樣的腳手架一鍵生成基礎項目模版,那我何不作多個屬於本身的項目模版。使用的時候只須要一行命令就能夠省去不少勞動力,不只省時省事,並且能夠定製本身想要的項目模版。說幹就幹,作腳手架以前先把模版作好,根據以前作小程序時搭建的express後端服務,這裏作了一個基於express的純後端模版。前端

使用模版

目前已經發布了腳手架工具qiya-cli,可使用此模版快速生成後端項目,使用方法以下:vue

npm install qiya-cli -g
qiya init
複製代碼

關於腳手架工具qiya-cli的更多功能能夠參看qiya-cli,另外關於這個腳手架的搭建過程與發佈,我會再寫一篇文章詳細介紹。node

主要功能

註冊與登陸接口mysql

支持JWT驗證git

Joi參數校驗github

支持mysql的orm框架sequelizeweb

apidoc接口文檔自動生成redis

全局參數配置算法

自動重啓

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(此模版)

聯繫我

若是你有好的項目模版,歡迎聯繫我

相關文章
相關標籤/搜索