一直都很想嘗試用node來寫點東西,學習了一番以後依葫蘆畫瓢用koa框架加上sequelize ORM從零開始用MVC模式編寫了個簡單的後臺項目,故在此作一下記錄。javascript
mkdir node-koa-demo # 建立項目 cd node-koa-demo # 進入目錄 cnpm init -y # 生成package.json cnpm install koa koa-body koa-router koa-static koa2-cors path -S # 安裝koa插件 touch app.js # 生成入口文件 複製代碼
// package.json
{
"name": "node-koa-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"koa": "^2.11.0",
"koa-body": "^4.1.1",
"koa-router": "^7.4.0",
"koa-static": "^5.0.0",
"koa2-cors": "^2.0.6",
"path": "^0.12.7"
}
}
複製代碼
// app.js const Koa = require('koa') const app = new Koa() const config = require('./config') const path = require('path') const koaBody = require('koa-body')({ // // 解析body的中間件 multipart: true, // 支持文件上傳 encoding:'gzip', formLimit: '5mb', // 限制表單請求體的大小 jsonLimit: '5mb', // JSON 數據體的大小限制 textLimit: '5mb', // 限制 text body 的大小 formidable:{ uploadDir: path.join(__dirname, '/public/upload'), // 設置文件上傳目錄 keepExtensions: true, // 保持文件的後綴 maxFieldsSize: 200 * 1024 * 1024, // 設置上傳文件大小最大限制,默認2M onFileBegin: (name, file) => { // 文件上傳前的設置 console.log(`name: ${name}`) console.log(file) } } }) const static = require('koa-static') // 解析body的中間件 app.use(koaBody) app.use(static(path.join(__dirname))) app.listen(config.service.port, () => { console.log('server is running') }) 複製代碼
.env文件:java
// .env SERVE_PORT=[項目端口] SERVE_ENVIROMENT=[項目所處環境] 複製代碼
安裝dotenv插件用來在項目中引用env文件node
cnpm install dotenv -S # 用來引入.env配置環境變量 複製代碼
入口文件引入插件:mysql
const dotenv = require('dotenv') // 引入配置文件 dotenv.config() 複製代碼
config.js:sql
module.exports = { service: { port: process.env['SERVE_PORT'], enviroment: process.env['SERVE_ENVIROMENT'] || 'dev' } } 複製代碼
入口文件引入config.js:數據庫
const config = require('./utils/config') global.config = config 複製代碼
啓動項目:
cnpm run start
npm
安裝插件json
cnpm install nodemon -S
複製代碼
// package.json
"start:dev": "nodemon node app.js"
複製代碼
cnpm install mysql2 sequelize -S
複製代碼
mkdir db
touch db/index.js
複製代碼
// .env DB_DATABASE=[數據庫名稱] DB_USER=[數據庫用戶名] DB_PSW=[數據庫鏈接密碼] DB_HOST=[數據庫端口] 複製代碼
// db/index.js const Sequelize = require('sequelize') const sequelize = new Sequelize( process.env['DB_DATABASE'], process.env['DB_USER'], process.env['DB_PSW'], { host: process.env['DB_HOST'], // 數據庫地址 dialect: 'mysql', // 數據庫類型 dialectOptions: { // 字符集 charset:'utf8mb4', collate:'utf8mb4_unicode_ci', supportBigNumbers: true, bigNumberStrings: true }, pool: { max: 5, // 鏈接池最大連接數量 min: 0, // 最小鏈接數量 idle: 10000 // 若是一個線程10秒內沒有被使用的花,就釋放鏈接池 }, timezone: '+08:00', // 東八時區 logging: (log) => { console.log('dbLog: ', log) return false } // 執行過程會打印一些sql的log,設爲false就不會顯示 } ) module.exports = sequelize 複製代碼
mkdir model
touch model/User.js
複製代碼
const Sequelize = require('sequelize') const sequelize = require('../db') const User = sequelize.define('user', { id: { type: Sequelize.INTEGER, allowNull: false, // 設置爲false時,會給添加NOT NULL(非空)約束,數據保存時會進行非空驗證 comment: 'ID', // 字段描述(自1.7+後,此描述再也不添加到數據庫中 autoIncrement: true, // 是否自增 primaryKey: true, // 指定是不是主鍵 unique: true, // 設置爲true時,會爲列添加惟一約束 }, password: { type: Sequelize.STRING(20), validate: {}, // 模型每次保存時調用的驗證對象。但是validator.js中的驗證函數(參見 DAOValidator)、或自定義的驗證函數 allowNull: false, // 設置爲false時,會給添加NOT NULL(非空)約束,數據保存時會進行非空驗證 comment: '密碼' // 字段描述(自1.7+後,此描述再也不添加到數據庫中) }, name: { type: Sequelize.STRING(20), validate: { notEmpty: true }, // 模型每次保存時調用的驗證對象。但是validator.js中的驗證函數(參見 DAOValidator)、或自定義的驗證函數 allowNull: false, // 設置爲false時,會給添加NOT NULL(非空)約束,數據保存時會進行非空驗證 comment: '用戶名稱' // 字段描述(自1.7+後,此描述再也不添加到數據庫中) }, email: { type: Sequelize.STRING(20), validate: { isEmail: true }, // 模型每次保存時調用的驗證對象。但是validator.js中的驗證函數(參見 DAOValidator)、或自定義的驗證函數 allowNull: false, // 設置爲false時,會給添加NOT NULL(非空)約束,數據保存時會進行非空驗證 comment: 'email' // 字段描述(自1.7+後,此描述再也不添加到數據庫中) }, phone: { type: Sequelize.STRING(11), allowNull: false, // 設置爲false時,會給添加NOT NULL(非空)約束,數據保存時會進行非空驗證 comment: '手機號碼' // 字段描述(自1.7+後,此描述再也不添加到數據庫中) }, birth: { type: Sequelize.DATE, validate: { isDate: true }, // 模型每次保存時調用的驗證對象。但是validator.js中的驗證函數(參見 DAOValidator)、或自定義的驗證函數 allowNull: false, // 設置爲false時,會給添加NOT NULL(非空)約束,數據保存時會進行非空驗證 defaultValue: new Date(), // 字面默認值, JavaScript函數, 或一個 SQL 函數 comment: '生日' // 字段描述(自1.7+後,此描述再也不添加到數據庫中) }, sex: { type: Sequelize.INTEGER, validate: { isInt: true, len: 1 }, // 模型每次保存時調用的驗證對象。但是validator.js中的驗證函數(參見 DAOValidator)、或自定義的驗證函數 allowNull: false, // 設置爲false時,會給添加NOT NULL(非空)約束,數據保存時會進行非空驗證 defaultValue: 0, // 字面默認值, JavaScript函數, 或一個 SQL 函數 comment: '性別,0-男 1-女' // 字段描述(自1.7+後,此描述再也不添加到數據庫中) }, }, { freezeTableName: true, // 設置爲true時,sequelize不會改變表名,不然可能會按其規則有所調整 timestamps: true, // 爲模型添加 createdAt 和 updatedAt 兩個時間戳字段 }) //建立表,默認是false,true則是刪除原有表,再建立 User.sync({ force: false, }) module.exports = User 複製代碼
// model/index.js /* 掃描全部的model模型 */ const fs = require('fs') const files = fs.readFileSync(__dirname + '/model') // 遍歷目錄 const jsFiles = files.filter(item => { return item.endsWith('.js') }, files) module.exports = {} for (const file of jsFiles) { console.log(`import model from file ${file}`) const name = file.substring(0, file.length - 3) module.exports[name] = require(__dirname + '/model/' + file) } 複製代碼
mkdir router
touch router/index.js
複製代碼
// router/index.js const router = require('koa-router')({ prefix: '/api' }) router.get('/', async(ctx, next) => { ctx.body = 'Hello World~' }) module.exports = router 複製代碼
在入口文件app.js引入api
// 路由中間件 const router = require('./router') // 開始服務並生成路由 app.use(router.routes()).use(router.allowedMethods()) // 開始服務並生成路由 複製代碼
mkdir middleware
複製代碼
touch middleware/exception.js # 中間件文件 touch utils/http-exception.js # 定義已知異常類 複製代碼
明確已知異常仍是未知異常bash
// utils/http-exception.js /** * 默認的異常 */ class HttpException extends Error { constructor(msg = '錯誤請求', errorCode = 10000, code = 400) { super() this.errorCode = errorCode this.code = code this.msg = msg } } class ParameterException extends HttpException { constructor(msg, errorCode) { super() this.code = 400 this.msg = msg || '參數錯誤' this.errorCode = errorCode || 10000 } } class AuthFailed extends HttpException { constructor(msg, errorCode) { super() this.code = 401 this.mag = msg || '受權失敗' this.errorCode = errorCode || 10004 } } class NotFound extends HttpException { constructor(msg, errorCode) { super() this.code = 404 this.msg = msg || '未找到該資源' this.errorCode = errorCode || 10005 } } class Forbidden extends HttpException { constructor(msg, errorCode) { super() this.code = 403 this.msg = msg || '禁止訪問' this.errorCode = errorCode || 10006 } } class Oversize extends HttpException { constructor(msg, errorCode) { super() this.code = 413 this.msg = msg || '上傳文件過大' this.errorCode = errorCode || 10007 } } class InternalServerError extends HttpException { constructor(msg, errorCode) { super() this.code = 500 this.msg = msg || '服務器出錯' this.errorCode = errorCode || 10008 } } module.exports = { HttpException, ParameterException, AuthFailed, NotFound, Forbidden, Oversize, InternalServerError } 複製代碼
// middleware/exception.js const { HttpException } = require('../utils/http-exception') // 全局異常監聽 const catchError = async(ctx, next) => { try { await next() } catch(error) { // 已知異常 const isHttpException = error instanceof HttpException // 開發環境 const isDev = global.config.service.enviroment === 'dev' // 在控制檯顯示未知異常信息:開發環境下,不是HttpException 拋出異常 if (isDev && !isHttpException) { throw error } /** * 是已知錯誤,仍是未知錯誤 * 返回: * msg 錯誤信息 * error_code 錯誤碼 */ if (isHttpException) { ctx.body = { msg: error.msg, error_code: error.errorCode } ctx.response.status = error.code } else { ctx.body = { msg: '未知錯誤', error_code: 9999 } ctx.response.status = 500 } } } module.exports = catchError 複製代碼
// 加載全局異常 const errors = require('./utils/http-exception') global.errs = errors const app = new Koa() // 全局異常中間件監聽、處理,放在全部中間件的最前面 const catchError = require('./middleware/exception') app.use(catchError) 複製代碼
// utils/resJson.js const ResultJson = { success: (params) => { return { data: params.data || null, // 返回的數據 msg: params.msg || '操做成功', // 返回的提示信息 code: 1 // 返回的接口調用狀態碼,0-失敗,1-成功 } }, fail: (params) => { return { data: params.data || null, msg: params.msg || '操做失敗', code: 0, error_code: params.errorCode // 返回接口異常信息碼 } } } module.exports = ResultJson 複製代碼
修改上文提到的異常處理中間件
/* 錯誤處理中間件 */ const { HttpException } = require('../utils/http-exception') const resJson = require('../utils/resJson') // ...省略上文 if (isHttpException) { ctx.body = resJson.fail(error) ctx.response.status = error.code } else { ctx.body = resJson.fail({ msg: '未知錯誤', error_code: 9999 }) ctx.response.status = 500 } // ...省略下文 複製代碼
mkdir controller
touch controller/User.js
複製代碼
const User = require('../model/User.js') const resJson = require('../utils/resJson') module.exports = { selectAll: async (ctx, next) => { await User.findAll({ raw: true, attributes: { // 不返回password字段 exclude: ['password'] } }).then((res) => { // 成功返回 ctx.body = resJson.success({data: res}) }).catch((err) => { // 失敗,捕獲異常並輸出 ctx.body = resJson.fail(err) }) } } 複製代碼
// router/index.js const router = require('koa-router')({ prefix: '/api' }) // User控制器 const User = require('../controller/user') // 獲取所有用戶 router.get('/user/list', User.selectAll) module.exports = router 複製代碼
訪問接口地址:http://localhost:3002/api/user/list
訪問結果:
{ "data": [ { "id": 1, "name": "cai", "email": "cai@qq.com", "phone": "13234323453", "birth": "2019-12-13T01:23:17.000Z", "sex": 1, "createdAt": "2019-12-13T01:23:42.000Z", "updatedAt": "2019-12-13T01:23:42.000Z" } ], "msg": "操做成功", "code": 1 } 複製代碼