1. 程式人生 > >koa面向物件式程式設計

koa面向物件式程式設計

koa面向物件式程式設計

最近由於在用egg.js,所以十分羨慕它裡面面向物件的邏輯,

自己參照著簡單封裝了一個面向物件的koa2-node的 寫法

封裝思想,將一些基礎的方法(list, insert, update, delete)封裝至一個父類(基類),子類繼承它便可以獲得對應的方法

  • route層

    定義了一堆頁面路由

    • 使用者請求對應的路由

    • 進入對應的server.action方法

    • 檔案目錄

    • admin/index.js

    const model = require('../model') // 獲取基本路由
    // 模組匯出
    module.exports = {
      ...model,
    }
    ​
    • model/index.js

    ​
    // enum http function: post、get
    const methods = require('../methods')
    ​
    module.exports = {
      // search
      'list': { method: methods.get },
      // add
      'insert': { method: methods.post },
      // update
      'update': { method: methods.post },
      // delete
      'delete': { method: methods.post },
    }
    • method.js

    module.exports = {
      get: 'get',
      post: 'post',
    }
    
     
    • index.js
    // 使用koa-router middleware
    const router = require('koa-router')()
    ​
    // 獲取對應的service層
    const services = {
      admin: require('../services/AdminService'),
    }
    // 配置所有的routes檔案
    const routes = (config => {
        return config.reduce((copy, name) => {
        const obj = require(`./${name}`) // 獲取對應個單個檔案  admin.js etc..
        const newArr = Object.keys(obj).reduce((total, each) => {
          // 配置path,method etc..
          // {  path: '/api/admin/list', method: post, action: list, server: 'admin' }
          let item = { path: `/api/${name.toLowerCase()}/${each}`, method: obj[each].method, action: each, service: name }
          total.push(item)
          return total
        }, [])
        copy = copy.concat(newArr)
          return copy
        }, [])
    })(Object.keys(services))
    ​
    // 配置最終的路由,形式為
    // router.get(url, service.action)
    ​
    routes.forEach(item => {
      const { method, path, service: serviceName, action } = item
      const service = services[serviceName]
      router[method](path, service[action])
    })
    module.exports = router
     
  • service層

    對請求引數的操作層

    對返回資料的加工層

    • 拿到request.body裡面的引數

    • 呼叫對應的controller

    • 拿到返回的資料,加工返回

    • 檔案目錄

    • BaseService.js

      const { pojo,filterUnderLine,} = require('../helper') // 獲取輔助類裡面的一些方法
      const { success, failed, successWithCode } = pojo // 獲取訊息集裡的一些輔助方法
      ​
      // 需要繫結this的方法
      const funcs = [
        'list',
        'insert',
        'update',
        'delete',
      ]
      class BaseService {
        constructor() {
          this.controller = null;
          // 迴圈遍歷繫結this
          funcs.forEach(item => {
            this[item] = this[item].bind(this)
          })
      ​
        }
      ​
        // 查詢方法
        async list(ctx) {
          // controller返回的是一個物件,success(成功為true, 失敗為false), data(成功則有此資料), err(失敗則有此物件)
          const { success: flag, data, err } = await this.controller.list()
          if (flag) {
            // success 為pojo訊息集的成功返回
            ctx.body = success(
              data.map(item => 
                //  篩選下劃線屬性,返回駝峰
                filterUnderLine(item)
              )
            )
          } else {
            // failed 為pojo訊息集的失敗返回,下同
            ctx.body = failed(err)
          }
        }
        // 插入方法
        async insert(ctx) {
          const { row } = ctx.request.body
          const { success, err } = await this.controller.insert(row)
          if (success) {
            // successWithCode 為沒有資料返回時的成功返回
            ctx.body = successWithCode('新增成功') // 沒有資料則返回
          } else {
            // 同上
            ctx.body = failed(err)
          }
        }
        // 更新方法
        // 同上
        async update(ctx) {
          const { row } = ctx.request.body
          const { success, err } = await this.controller.update(row)
          if (success) {
            ctx.body = successWithCode('新增成功')
          } else {
            ctx.body = failed(err)
          }
        }
        // 刪除方法
        // 同上
        async delete(ctx) {
          const { row } = ctx.request.body
          const { success, err } = await this.controller.delete(row)
          if (success) {
            ctx.body = successWithCode('新增成功')
          } else {
            ctx.body = failed(err)
          }
        }
      }
      ​
      module.exports = BaseService
    • AdminService.js

      // 匯入基類
      const BaseService = require('./BaseService')
      // 匯入對應的controller
      const Controller = require('../controller/AdminController')
      // 生成一次controller
      const AdminController = new Controller()
      class AdminService extends BaseService {
        constructor() {
          super()
          // 繫結對應的controller
          this.controller = AdminController;
        }
      }
      module.exports = new AdminService()
      ​
      

       

  • controller層

    對資料庫的操作層

    • 接收service層傳來的引數

    • 對引數進行處理

    • 執行sql語句,返回資料庫結果

    • 檔案目錄

    • BaseController.js

      const pool = require('../lib/mysql') // 匯入封裝好的mysql庫
      const { query } = pool // 匯入query方法
      const { STATUS } = require('../enum') // 匯入列舉型別STATUS
      const { NtNUpdate,filterCamel, } = require('../helper') // 匯入helper內相關方法
      ​
      class BaseController {
        constructor() {
          this.table = ''
        }
        // 查詢表內所有資料(非刪除)
        async list() {
          const sql = `select * from ${this.table} where status != ?`
          return await query(sql, [STATUS.DELED])
        }
        // 插入新資料
        async insert(row) {
          const {
            keys,
            vals
          } = filterCamel(row) // 將駝峰命令轉換為下劃線
          const names = keys.join(',') // 對應的引數
          const questions = keys.map(item => '?').join(',') // 對應的引數佔位符
          // 補全sql語句 insert into table (x, xx) values(x, xx)
          const sql = `insert into ${this.table}(${names},create_time,status) values(${questions},now(),?)`
          return await query(sql, [...vals, STATUS.NORMAL])
        }
      ​
        async update(row) {
          const {
            id,
          } = row; // 獲取資料內的id
          // 刪除id
          delete row.id
          // 啟始sql
          let _sql = `update ${this.table} set `
          const {
            sql,
            args
          } = NtNUpdate(row, _sql)// 獲取物件內非空值,加工sql 語句  update table set name=?, val= ?
          // 補全sql語句 update table set name = ?, val = ? where id = ?
          _sql = sql + 'where id = ?'
          return await query(_sql, [...args, id])
        }
      ​
        async delete(row) {
          const {
            id
          } = row // 獲取資料內的id
          // 補全sql update table set status = ? where id = ?
          const sql = `update ${this.table} set status = ? where id = ?`
          return await query(sql, [STATUS.DELED, id])
        }
      ​
        excute(sql, vals) {
          // 執行方法
          return await = query(sql, vals)
        }
        // log 方法
        log({func, err}) {
          console.log(`excute function[${func}] occured error : ${err.message || err}`)
        }
      ​
      }
      ​
      module.exports = BaseController

       

    • AdminController

      const BaseController = require('./BaseController') // 獲得基類
      ​
      class AdminController extends BaseController {
        constructor() {
          super();
          this.table = 'tour_admin' // 賦值table
        }
      }
      module.exports = AdminController

       

  • helper類

    • helper/index.js

      const pojo = require('./pojo')
      ​
      /**
       * 
       * @param {Object} params  引數物件
       * @param {String} sql sql語句
       * @description 根據引數物件去改變sql語句,最後返回對應的sql語句
       * @return 返回處理後的sql語句
       */
      const update = (params, sql) =>  {
        let keys = Object.keys(params)
        let arr = []
        keys.forEach((key) => {
          if (key) {
            sql = sql + `${key} = ? ,`
            arr.push(params[key])
          }
        })
        sql = sql.substring(0, sql.length - 1)
        return {
          args: arr,
          sql,
        }
      }
      /**
       * 
       * @param {String} val  原下劃線值
       * @param {String} char 要替換的字元
       * @description 根據原key去替換下劃線後轉為駝峰
       * @return 返回處理後的key
       */
      const replaceUnderLine = (val, char = '_') => {
        const arr = val.split('')
        const index = arr.indexOf(char)
        arr.splice(index, 2, arr[index+1].toUpperCase())
        val = arr.join('')
        return val
      }
      ​
      /**
       * 
       * @param {String} val  原下劃線值
       * @description 下劃線轉駝峰
       * @return 返回處理後的key
       */
      const underline2Camel = (val) => {
        return val.replace(/\_(\w)/g, (all, letter) => {
          return letter.toUpperCase()
        })
      }
      ​
      /**
       * 
       * @param {String} val  原key
       * @param {String} char  要替換的字元
       * @description 駝峰轉下劃線
       * @return 返回處理後的key
       */
      const camel2UnderLine = (val, char = '_') => {
        return val.replace(/([A-Z])/g,`${char}$1`).toLowerCase();
      }
      ​
      /**
       * 
       * @param {Object} obj  原物件
       * @param {String} char  要替換的字元
       * @description 物件駝峰轉下劃線
       * @return 返回處理後的keys 和 對應的vals { aboutExample: 'example' }, ['about_example'], ['example']
       * @return { Array } keys 
       * @return { Array } vals 
       */
      const fileterCamel = (obj, char = '_') => {
        const keys = Object.keys(obj)
        return keys.reduce((init, item) => {
          const str = item
          if (~item.indexOf(char)) {
            str = camel2UnderLine(item)
          }
          init.keys.push(str)
          init.vals.push(obj[item])
          return init
        }, {
          keys: [],
          vals: [],
        })
      }
      ​
      /**
       * 
       * @param {Object} obj  原物件
       * @param {String} char  要替換的字元
       * @description 物件下劃線轉駝峰
       * @return 返回處理後的物件 { about_example: 'example' } { aboutExample: 'example' }
       * @return { Object } obj  
       */
      const  filterUnderLine = (obj, char = '_') => {
        const arr =  Object.keys(obj).filter(item => ~item.indexOf(char))
        arr.forEach(item => {
          const val = obj[item]
          const key = underline2Camel(item)
          obj[key] = val
          delete obj[item]
        })
        return obj
      }
      ​
      module.exports = {
        NtNUpdate: update,
        filterUnderLine,
        replaceUnderLine,
        replaceCamel: fileterCamel,
        pojo,
      }
      ​

       

    • helper/pojo.js

      // 成功返回
      const success = (result) => {
        return {
          retCode: 200,
          retValue: result
        }
      }
      // 成功沒資料返回
      const successWithCode = msg => {
        return {
          retCode: 200,
          msg,
        }
      }
      ​
      // 失敗返回
      const failed = (error) => {
        console.log(error)
        return {
          retCode: 500,
          msg: error.message || error || '伺服器異常'
        }
      }
      ​
      ​
      ​
      module.exports = {
        success,
        failed,
        successWithCode,
      }
      ​

       

  • enum列舉

    • enum/index.js

      const status = require('./status') // 獲取狀態
      const type = require('./type')// 獲取型別
      ​
      // 匯出
      module.exports = {
        STATUS: status,
        TYPES: type,
      }
      ​

       

    • enum/status.js

      module.exports = {
        DELED: 404, // 刪除狀態
        NORMAL: 0, // 普通狀態
      }

       

    • enum/type.js

      module.exports = {
        MANAGER: 999, // 管理員
        USER: 0, // 使用者
      }
      
       
  • lib/mysql.js
  • const mysql = require('mysql');
    const config = require('../config/default.js')
    ​
    // 連線池
    const pool  = mysql.createPool({ 
      host     : config.database.HOST, // 資料庫連線埠
      user     : config.database.USERNAME, // 資料庫使用者名稱
      password : config.database.PASSWORD, // 資料庫密碼
      database : config.database.DATABASE // 資料庫名稱
    });
    ​
    /**
     * 
     * @param sql 接收的sql語句
     * @param {Array} values sql語句引數
     * @return { Object } { success: boolean, err || data  }
     */
    const query = function( sql, values ) {
      return new Promise(( resolve, reject ) => {
        pool.getConnection(function(err, connection) {
          if (err) {
            resolve( {
              success: false,
              err,
            } )
          } else {
            connection.query(sql, values, ( err, rows) => {
              if ( err ) {
                resolve({
                  success: false,
                  err,
                } )
              } else {
                resolve( {
                  success: true,
                  data: rows,
                } )
              }
              connection.release()
            })
          }
        })
      })
    }
    module.exports={
      query,
    }
  • config

  • config/default.js
    
    const config = {
      // 啟動埠
      port: 1200,
      // 資料庫配置
      database: {
        DATABASE: 'xxx',
        USERNAME: 'root',
        PASSWORD: 'xxx',
        PORT: 'xxx',
        HOST: 'xxx'
      }
    }
    ​
    module.exports = config

  • src/index.js

      

var Koa=require('koa');
var path=require('path')
var bodyParser = require('koa-bodyparser');
var session = require('koa-session-minimal');
var MysqlStore = require('koa-mysql-session');
var config = require('./config/default.js');
var koaStatic = require('koa-static')
var app=new Koa()
const routers = require('./routes/index')



// session儲存配置
const sessionMysqlConfig= {
  user: config.database.USERNAME,
  password: config.database.PASSWORD,
  database: config.database.DATABASE,
  host: config.database.HOST,
}

// 配置session中介軟體
app.use(session({
  key: 'USER_SID',
  store: new MysqlStore(sessionMysqlConfig)
}))

// 配置跨域
app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Headers', 'Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With')
  ctx.set('Access-Control-Allow-Origin', 'http://localhost:9000');
  ctx.set('Access-Control-Allow-Methods', 'PUT,DELETE,POST,GET');
  ctx.set('Access-Control-Allow-Credentials', true);
  ctx.set('Access-Control-Max-Age', 3600 * 24);
  await next();
});
// 配置靜態資源載入中介軟體
app.use(koaStatic(
  path.join(__dirname , './public')
))

// 使用表單解析中介軟體
app.use(bodyParser())

// 使用新建的路由檔案
app.use(routers.routes()).use(routers.allowedMethods())

// 監聽在1200
app.listen(config.port)

console.log(`listening on port ${config.port}`)

 

到這裡專案就可以完整跑起來了,

最後奉上專案地址

喜歡就Star一下吧~