zoukankan      html  css  js  c++  java
  • koa2使用es7 的装饰器decorator

    本文主要讲述我在做项目中使用装饰器(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 

    在此,完毕,篇幅内容有点多,看不懂可以留言,谢谢大家

      

  • 相关阅读:
    gitlab+gerrit+jenkins持续集成框架
    多线程自动翻页爬虫
    爬虫超级简单入门
    完整开发流程管理提升与系统需求分析过程 随堂笔记(day 1) 【2019/10/14】
    Logistic Regression
    cmdb项目-2
    cmdb项目-3
    cmdb全总结
    crm-1
    crm-2
  • 原文地址:https://www.cnblogs.com/beyonds/p/11190359.html
Copyright © 2011-2022 走看看