本文主要講述我在做項目中使用裝飾器(decorator)來動態加載koa-router的路由的一個基礎架構。
目前JavaScript 對decorator 是不支持,但是可以用babel 來編譯
既然是koa2結合decorator 使用,首先是要起一個koa2 項目。
環境要求: node >7.6
1.建立文件夾名為koa-decorator ,在該目錄下運行 npm init 初始化一個項目(直接默認回車)
npm init
2.安裝koa的基本依賴包,koa,koa-router
npm install koa,koa-router;
3.構建基本項目目錄
├── dist----------------------------------- 編譯后的 ├── src ----------------------------------- 項目的所有代碼 │ ├──config ----------------------------- 配置文件 │ ├──controller ------------------------- 控制器 │ ├──lib -------------------------------- 一些項目的核心文件(如路由的裝飾器文件就在這里) │ ├──logic ------------------------------ 一些數據校驗 │ ├──middleware ------------------------- 中間件 │ ├──models------------------------------ 操作數據表相關邏輯代碼(根據項目復雜度可以再分Service層) │ ├──util-------------------------------- 相關的工具文件 │ ├──index.js---------------------------- 項目的入口文件 ├── theme --------------------------------- 一些靜態文件(上傳的圖片) ├── .babelrc ------------------------------ babelrc 的相關配置 ├── .gitignore ---------------------------- git 的忽略配置文件 ├── dev.js -------------------------------- 開發環境的啟動文件 ├── production.js ------------------------- 生產環境的啟動文件
4.安裝babel ,與裝飾器的編譯依賴(只需要要開發環境安裝) babel-cli,babel-core,babel-register,babel-plugin-transform-decorators-legacy
npm install babel-cli,babel-core,babel-register,babel-plugin-transform-decorators-legacy --save-dev;
5.配置 .babelrc 文件讓 其能使用裝飾器
{
"presets": [],
"plugins": [
"transform-decorators-legacy"
]
}
6. 編寫開發環境dev.js和 生產環境的production.js 的啟動文件
1. dev.js
require("babel-register");
process.env.NODE_ENV = "development";
require("./src");
2. production.js
process.env.NODE_ENV = "production";
require("./dist");
你會發現這兩個文件很簡單,主要是區別用來開發運行和生產打包編譯的,生產環境運行的打包后的dist 目錄的代碼
7.配置package.json 使項目能修改后自動重啟熱加載,這里開發環境我使用 supervisor,有人使用nodenom ,生產環境用pm2
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "babel src --out-dir dist",
"dev": "set NODE_ENV=development && supervisor --watch src dev.js",
"start": "npm run build && set NODE_ENV=production && supervisor --watch dist production.js",
"pm2": "pm2 start production.js --name 'wx-node' --env NODE_ENV='production' --output ./logs/logs-out.log --error ./logs/logs-error.log --watch dist"
},
7.1 運行 npm run build : 是用babel 直接將src 目錄編譯在dist 目錄
7.2 運行 npm run dev : 是設置環境變量為development 並且監聽src目錄,啟動dev.js 運行,為開發環境
7.3 運行 npm run start : 是 運行第一個命令npm run build 並且設置環境變量為production 監聽dist 目錄,啟動production.js運行,為生產或者測試環境
7.4 運行npm run pm2: 這是使用pm2來守護項目進程,並且設置環境變量和日志記錄
8.編寫入口文件index.js 讓服務跑起來
src/index.js
const koa = require("koa");
const http = require("http");
const App = new koa();
// 定義端口常量
const port = 3000;
App.use(async (ctx,next)=>{
ctx.body = await "this is koa"
await next();
})
// 啟動服務
var httpApp = http.createServer(App.callback()).listen(port,'0.0.0.0');//獲取ip 為ip4 格式(192.168.5.109),默認是ip6 格式(::ffff:192.168.5.109);
httpApp.on("listening",()=>{
console.log(`http server start runing in port ${port}...`)
})
App.on("error",(err,ctx)=>{
console.log("server error: "+err.stack);
ctx.throw(500, 'server error')
})
9.重點:編寫裝飾器的路由文件,本文核心內容就是在這里
9.1 引入相關依賴包 和定義所有的請求方法
src/lib/decoratorRouter/index.js
const koaRouter = require("koa-router");
const router = new koaRouter();
const routerPrefix ="/api" //定義接口前綴
//聲明所有接口的方式的映射,下面會用到
const RequestMethod = {
GET: 'get',
POST: 'post',
PUT: 'put',
DELETE: 'delete',
ALL: "all"
}
9.2 編寫裝飾類class 的函數,主要作用是對類的攔截,然后實例化該類,並獲取和調用該類下所有實例方法,由於es6 的class的方法是不迭代的,所以使用了Object.getOwnPropertyDescriptors(object.prototype)
src/lib/decoratorRouter/index.js
//定義controller 的函數,這是裝飾類class 的函數,接受一個參數(和路由前綴並接一起)
function Controller(prefix) {
router.prefixed =routerPrefix+(prefix ? prefix.replace(/\/+$/g, "") : '');
//對 類 class 進行攔截操作,返回一個函數,該函數實際接受三個參數(攔截目標targer,目標的key,key 的描述)
return (target) => {
//把路由router 掛載在攔截目標,作為靜態屬性
target.router = router;
//實例化該類 class
let obj = new target;
// 獲取該實例下的所有實例方法,進行 迭代調用,除了構造函數 和一個前置函數(后面會說得如何實現和作用)
let actionList = Object.getOwnPropertyDescriptors(target.prototype);
for (let key in actionList) {
if (key !== "constructor") {
var fn = actionList[key].value;
if (typeof fn == "function" && fn.name != "__before") {
fn.call(obj, router, obj);//保證在類中能正確訪問this,調用該方法是用call,還有兩個參數是 router 和 obj 實例
}
}
}
}
}
9.3 編寫裝飾 實例方法的函數,當我們對類class 進行裝飾的時候,其實例方法會全部自動被調用,這時候繼續對實例方法進行攔截,攔截的目的就是給該實例方法與路由結合一起
/src/lib/decoratorRouter/index.js
//該裝飾函數接受兩個參數,請求url 和請求方式
function Request(option = {url, method}) {
//攔截該實例方法,參數三個
return function (target, value, dec) {
//聲明fn 緩存原來的 函數體 dev.value
let fn = dec.value;
//然后重寫該函數,參數兩個,在 controller 裝飾類的時候自動調用轉入的兩個參數
dec.value = (routers, targets) => {
//這里,才是真正調用koa-router 路由的時候
routers[option.method](routers.prefixed + option.url, async (ctx, next) => {
//這里寫了一個前置函數,判斷前置函數存在
if (target.__before && typeof target.__before == "function") {
// 如果class 有__before 前置函數,//再默認裝飾一次
var beforeRes = await target.__before.call(target,ctx, next, target);
//前置函數如果沒有返回內容,繼續執行實例方法,否則直接響應 body,不執行實例方法
if (!beforeRes) {
return await fn.call(target, ctx, next, target)
}else{
return ctx.body = await beforeRes
}
} else {
// 沒有前置函數,直接調回原來的實例函數執行,使用call ,傳入的參數就有ctx,next,實例targe
await fn.call(target, ctx, next, target)
}
})
}
}
}
9.4 整合所有的請求方法並導出接口
/src/lib/decoratorRouter/index.js
// post 請求
function POST(url) {
return Request({url, method: RequestMethod.POST})
}
//get 請求
function GET(url) {
return Request({url, method: RequestMethod.GET})
}
//PUT 請求
function PUT(url) {
return Request({url, method: RequestMethod.PUT})
}
//DEL請求
function DEL(url) {
return Request({url, method: RequestMethod.DELETE})
}
//ALL 請求
function ALL(url) {
return Request({url, method: RequestMethod.ALL})
}
module.exports = {
Controller,POST,GET,PUT,DEL,ALL
}
10 .裝飾koa-router 的核心內容寫完了,那么如何做到自動加載呢,按照項目目錄架構,controller 目錄是處理接口目錄,使用內置的文件系統模塊fs 處理文件自動載入
/src/lib/loadRouter/index.js
const fs = require("fs");
const {resolve} = require("path")
//這里很重要,區別環境變量,確定調用是 dist/controller (編譯后),還是調用 src/controller (開發)
let entryPath = process.env.NODE_ENV==="development"?"src":"dist";
console.log(process.env.NODE_ENV+"環境:執行目錄"+entryPath)//這是controller 的入口根目錄
let controllerPath = resolve(entryPath,'controller');
//對外導出一個函數,並接收app 實例作為參數,
module.exports = (App)=>{
let loadCtroller = (rootPaths)=>{
try {
var allfile = fs.readdirSync(rootPaths); //加載目錄下的所有文件進行遍歷
allfile.forEach((file)=>{
var filePath = resolve(rootPaths,file)// 獲取遍歷文件的路徑
if(fs.lstatSync(filePath).isDirectory()){ //判斷該文件是否是文件夾,如果是遞歸繼續遍歷讀取文件
loadCtroller(filePath)
}else{
//如果是文件就使用require 導入,(controller下文件都是對外導出的class),在使用 @controller 裝飾函數的時候,將koa-router 的實例作為裝飾對象class 的靜態屬性
let r = require(filePath);
if(r&&r.router&&r.router.routes){ //如果有koa-routr 的實例說明裝飾成功,直接調用app.use()
try {
App
.use(r.router.routes())
} catch (error) {
console.log(filePath)
}
}else{
// console.log("miss routes:--filename:"+filePath)
}
}
})
} catch (error) {
console.log(error)
console.log("no such file or dir :---- "+rootPaths)
}
}
//調用自動加載路由
loadCtroller(controllerPath);
}
11. 在index.js 入口文件載入 /src/lib/loadRouter/index.js 文件
const koa = require("koa");
const http = require("http");
const App = new koa();
// 定義端口常量
const port = 3000;
require("./lib/loadRouter/index")(App) // 載入自動加載路由文件
// 啟動服務
var httpApp = http.createServer(App.callback()).listen(port,'0.0.0.0');//獲取ip 為ip4 格式(192.168.5.109),默認是ip6 格式(::ffff:192.168.5.109);
httpApp.on("listening",()=>{
console.log(`http server start runing in port ${port}...`)
})
App.on("error",(err,ctx)=>{
console.log("server error: "+err.stack);
ctx.throw(500, 'server error')
})
12.然后編寫controller 下的文件,新建index.js
/src/controller/index.js
const {Controller,GET,POST} = require("../lib/decoratorRouter")
//訪問路徑 :路由前綴 + controller 參數 + 請求方式的參數 => 域名:端口/api/index/add
@Controller("/index")
class index{
@GET("/")
async index(ctx,next){
ctx.body = await "this is index"
}
@POST("/add")
async add(ctx,next){
ctx.body = await "this is add"
}
}
module.exports = index;
運行: http://127.0.01:3000/api/index/ 成功訪問顯示 this is index ,到此基本完畢 了
源碼git 地址: https://github.com/1119879311/npm_module/tree/master/node-decorator
對於要多層繼續裝飾,做攔截,class繼承,還有前置函數的使用
可以參考該項目的用法:https://github.com/1119879311/koa2-decorator
在此,完畢,篇幅內容有點多,看不懂可以留言,謝謝大家
