nodejs+koa+Sequelize+pkg後端服務實踐

西安小哥發表於2023-02-15

nodejs+koa+Sequelize實踐

  • Node.js® 是一個開源、跨平臺的 JavaScript 執行時環境。
  • Koa 是一個新的 web 框架,由 Express 幕後的原班人馬打造, 致力於成為 web 應用和 API 開發領域中的一個更小、更富有表現力、更健壯的基石。 透過利用 async 函式,Koa 幫你丟棄回撥函式,並有力地增強錯誤處理。 Koa 並沒有捆綁任何中介軟體, 而是提供了一套優雅的方法,幫助您快速而愉快地編寫服務端應用程式。
  • Sequelize是一個基於 promise 的 Node.js ORM, 目前支援 Postgres, MySQL, MariaDB, SQLite 以及 Microsoft SQL Server. 它具有強大的事務支援, 關聯關係, 預讀和延遲載入,讀取複製等功能。
  • pkg可以將 Node.js 專案打包為可執行檔案,甚至可以在未安裝 Node.js 的裝置上執行。

基本架構

image.png

model層:資料持久化,並提共資料處理,持久化操作介面
control層:業務模組流程控制,呼叫service層介面
service層:業務操作實現類,呼叫model層介面**

中介軟體

中介軟體作用特點
koa-static靜態資源路徑
koa-logger日誌打點中介軟體
koa2-cors跨域處理
koa-jwt主要提供路有許可權控制的功能,它會對需要限制的資源請求進行檢查
koa-body是一個可以幫助解析 http 中 body 的部分的中介軟體,包括 json、表單、文字、檔案等。
SequelizeSequelize 是一個基於 promise 的 Node.js ORM, 目前支援 Postgres, MySQL, MariaDB, SQLite 以及 Microsoft SQL Server. 它具有強大的事務支援, 關聯關係, 預讀和延遲載入,讀取複製等功能。
koa-routerkoa 的一個路由中介軟體,它可以將請求的URL和方法(如:GET 、 POST 、 PUT 、 DELETE 等) 匹配到對應的響應程式或頁面

資料庫相關

  1. 資料庫配置

    const config = {
     database: "data_base", // 資料庫名稱
     username: "root", // 使用者名稱
     password: "xxxx", // 密碼
     host: "8.16.23.x", // 主機地址
     port: "3306", // 埠號
     dialect: "mysql", //資料庫型別,支援: 'mysql', 'sqlite', 'postgres', 'mssql'
     // logging: true, // 是否啟用日誌
    }
  2. 資料庫連結

    const config = require('./dbconfig') // 引入資料庫配置資訊
    const { Sequelize, DataTypes ,Op} = require("sequelize") // 引入sequelize依賴
    
    const sequelize = new Sequelize(
     config.database,
     config.username,
     config.password,
     {
       dialect: config.dialect,
       dialectOptions: {
         dateStrings: true,
         typeCast: true
       },
       host: config.host,
       port: config.port,
       logging: config.logging,
       pool: { // 連線池配置
         min: 0, // 最小連線數
         max: 5, // 最大連結數
         idle: 30000,
         acquire: 60000,
       },
       define: {
         // 欄位以下劃線(_)來分割(預設是駝峰命名風格)
         underscored: true
       },
       timezone: '+08:00'
     }
    )

    3、定義表結構

    const { sequelize, DataTypes } = require("../config/connect")
    const alliances = sequelize.define(
     "aw_table",
     {
       id: {
         type: DataTypes.INTEGER(11),
         allowNull: false, // 是否允許為空
         autoIncrement: true,
         primaryKey: true, // 是否主鍵
       },
       title: {
         type: DataTypes.STRING,
         allowNull: false,
         comment: '名稱'
       },
     }, {
       timestamps: true,
       
       // 不想要 createdAt
       createdAt: 'create_time',
       
       // 想要 updatedAt 但是希望名稱叫做 updateTimestamp
       updatedAt: 'update_time'
     }
    )
    // alliances.sync({force:true}) // 是否自動建立表

服務層

1、業務操作實現類,呼叫model層介面

const { daoModel } = require("../model/index");

class Service {
    async getList(params) {
        const { pagenum = 1, pagesize = 10 } = params;
        return daoModel.findAndCountAll({
            limit: parseInt(pagesize),
            // 跳過例項數目
            offset: (pagenum - 1) * parseInt(pagesize),
        });
    }
    async getDetail(id) {
        return daoModel.findOne({
            where: {
                id: id,
            },
        });
    }
}

module.exports = new Service();

控制層

1、control層:業務模組流程控制,呼叫service層介面

const Service = require("../services/newsService");
module.exports = {
    getList: async (ctx) => {
        const params = ctx.query;
        const { count, rows } = await Service.getList(params);
        if (count > 0) {
            const list = [];
            for (let i = 0; i < rows.length; i++) {
                const data = rows[i].dataValues;
                list.push(data);
            }
            ctx.body = {
                status: 200,
                msg: "獲取成功",
                data: {
                    list: list,
                    total: count,
                    limit: 1,
                },
            };
        } else {
            ctx.body = {
                status: "error",
                msg: "獲取失敗",
                data: null,
            };
        }
    },
    getDetail: async (ctx) => {
        const matchArr = ctx.url.split("/");
        const id = matchArr[matchArr.length - 1];
        // 擷取出文章詳情ID
        const result = await Service.getDetail(id);
        if (result) {
            ctx.body = {
                status: 200,
                msg: "獲取成功",
                data: {
                    detail: result.dataValues,
                },
            };
        }
    },
};

介面路由

1、服務模組化

 const Router = require('koa-router')
 const router = new Router()
 const Controller = require('../../controllers/admin/fileController')
 const routers = router
   .post('/alliance', Controller.add)
   .get('/alliance', Controller.getList)
   .get('/alliance/:id', Controller.getDetail)
   .delete('/alliance/:id', Controller.delete)
   .put('/alliance/:id', Controller.update)
 module.exports = routers

2、服務路由入庫

const Router = require('koa-router')
const router = new Router()

const Routes = require('./routes')
const upload = require('./upload')
const article = require('./article')

router.use(article.routes(), article.allowedMethods())
router.use(article.routes(), article.allowedMethods())

module.exports = router

許可權處理

const { varifyToken } = require('../utils/utils')
const RolesService = require('../services/admin/role/rolesService')
module.exports = function () {
  return async (ctx, next) => {
    const url = ctx.path
    // 對前端展示、登入、註冊等路由進行放行
    if (url.substring(0, 11) === '/api/v1/web'
      || url === '/api/v1/admin/login'
      || url === '/api/v1/admin/register'
      || url === '/api/v1/admin/logout') {
      await next()
    } else {
      // 判斷headers 中是否存在 authorization
      if (ctx.headers && ctx.headers.authorization === undefined) {
        ctx.status = 401
        ctx.body = {
          status: 401,
          msg: '無效token,沒有訪問許可權'
        }
      } else {
        try {
          // 若存在,驗證 token 是否等於當前登入使用者的使用者名稱,等於的話,再判斷此使用者的角色表中的 permission 欄位
          // 是否存在 ctx.url ,是的話 next(),否則未授權
          // 在else中再深入判斷它是否能夠訪問該介面的許可權就是啦{驗證token,判斷使用者是否有許可權能訪問此介面路徑}
          const token = ctx.headers.authorization
          // 解密token
          const payload = await varifyToken(token)
          const userInfo = payload.userInfo
          // roles:['管理員'],轉為字串
          const roleName = userInfo.roles.toString()
          const result = await RolesService.getRolePermission(roleName)
          const permissionApi = []
          for (let i = 0; i < result.length; i++) {
            const tmp = {
              // 拼接api路徑
              path: '/api/v1/admin' + result[i].path,
              method: result[i].method
            }
            permissionApi.push(tmp)
          }
          // console.log(permissionApi)
          const res = permissionApi.filter(item => {
            const index = item.path.indexOf(':')
            if (index !== -1) {
              // console.log(index)
              // 根據 :id等 動態拼接api路徑
              item.path = item.path.substring(0, index) + `${ctx.url.substring(index)}`
              // console.log('index: '+index+' '+item.path)
            }
            // 過濾出當前訪問的api介面
            return new RegExp(item.path, 'g').test(ctx.url) && item.method.toUpperCase() === ctx.request.method.toUpperCase()
          })
          // 返回當前訪問的api介面列表
          // console.log(res)
          if (res.length === 0) {
            ctx.status = 401
            ctx.body = {
              code: 401,
              msg: '您的使用者沒有該訪問許可權!'
            }
          } else {
            await next()
          }
        } catch (err) {
          // 捕獲 jwt 的異常資訊
          if (err.message === 'jwt expired') {
            ctx.status = 50014
            ctx.body = {
              code: 50014,
              msg: 'token 過期'
            }
          } else if (err.message === 'jwt malformed') {
            ctx.status = 50008
            ctx.body = {
              code: 50008,
              msg: 'token 無效'
            }
          } else {
            ctx.status = 500
            ctx.body = {
              code: 500,
              msg: err.message
            }
          }
        }
      }
    }
  }
}

pkg打包

1、相關配置

 pkg [options] <input>

 Options:

   -h, --help           output usage information
   -v, --version        output pkg version
   -t, --targets        comma-separated list of targets (see examples)
   -c, --config         package.json or any json file with top-level config
   --options            bake v8 options into executable to run with them on
   -o, --output         output file name or template for several files
   --out-path           path to save output one or more executables
   -d, --debug          show more information during packaging process [off]
   -b, --build          donot download prebuilt base binaries, build them
   --public             speed up and disclose the sources of top-level project
   --public-packages    force specified packages to be considered public
   --no-bytecode        skip bytecode generation and include source files as plain js
   -C, --compress       [default=None] compression algorithm = Brotli or GZip

 Examples:

 – Makes executables for Linux, macOS and Windows
   $ pkg index.js
 – Takes package.json from cwd and follows 'bin' entry
   $ pkg .
 – Makes executable for particular target machine
   $ pkg -t node14-win-arm64 index.js
 – Makes executables for target machines of your choice
   $ pkg -t node12-linux,node14-linux,node14-win index.js
 – Bakes '--expose-gc' and '--max-heap-size=34' into executable
   $ pkg --options "expose-gc,max-heap-size=34" index.js
 – Consider packageA and packageB to be public
   $ pkg --public-packages "packageA,packageB" index.js
 – Consider all packages to be public
   $ pkg --public-packages "*" index.js
 – Bakes '--expose-gc' into executable
   $ pkg --options expose-gc index.js
 – reduce size of the data packed inside the executable with GZip
   $ pkg --compress GZip index.js

1、 pkg .,意思就是它會尋找指定目錄下的package.json檔案,然後再尋找bin欄位作為入口檔案。
2、-t 用來指定打包的目標平臺和Node版本,如-t node12-win-x64,node12-linux-x64,node12-macos-x64,可以同時打包3個平臺的可執行程式;
3、--out-path 用來指定輸出的目錄地址;後面的"=dist/"就是指定的目錄地址,也可以這樣寫"--out-path dist/",用空格替代"="

相關配置

"bin": "./app.js",
    "pkg": {
        "assets": [
            "static/**/*"
        ],
        "targets": [
            "node16"
        ],
        "outputPath": "dist"
    }

測試pkg

image.png

image.png

相關文章