node从入门到放弃系列之(11)用优雅的代码武装我们的koa2项目

奉上代码: node服务demo代码=》koa2-server项目代码;vue前端demo代码=》 vue-client项目代码。
如果git登不上可以换gitee=》koa2-server项目代码;vue前端demo代码=》 vue-client项目代码

本文借鉴于:用优雅的代码武装我们的koa2项目;对其中部分做修改整合而成。

用优雅的代码武装我们的koa2项目

    • 路由的自动加载
    • 开发环境和生产环境的区分
    • 统一接口返回格式
    • 全局异常处理中间件
    • 使用JWT完成认证授权
    • nodejs require路径别名

路由的自动加载

之前我的路由总是手动注册的,大概是这样的:

//app.js
const Koa = require('koa');
const app = new Koa(); 

// 路由
const index = require('./routes/index');
const commom = require('./routes/commom'); // 通用服务

app.use(index.routes(), index.allowedMethods());
app.use(commom.routes(), commom.allowedMethods());

当时开发久了路由多了文件数量就庞大了,再像这样引入再use方式未免会显得繁琐拖沓。那有没有办法让这些文件自动被引入、自动被加载呢?

此时就需要一个好用的依赖了

npm install require-directory --save

在根目录下建一个文件夹core,以后一些公共的代码都存放在这里。

//core/init.js
const requireDirectory = require('require-directory');
const Router = require('koa-router'); 

class InitManager {
     
    static initCore(app) {
     
        // 把app.js中的koa实例传进来
        InitManager.app = app;
        InitManager.initLoadRouters();
    }
    static initLoadRouters() {
     
        // 注意这里的路径是依赖于当前文件所在位置的
        // module为固定参数,apiDirectory为路由文件所在的路径(支持嵌套目录下的文件),第三个参数中的visit为回调函数
        // 那么根据我的项目目录就是这样的
        const apiDirectory = `${
       process.cwd()}/routes`
        const modules = requireDirectory(module, apiDirectory, {
     
            visit: whenLoadModule
        });
        function whenLoadModule(obj) {
     
            if(obj instanceof Router) {
     
                // FIXME 路由黑名单
		        const blackList = ['/image'];
		        const prefix = obj.opts.prefix;
		        if (!blackList.includes(prefix)) {
     
		          InitManager.app.use(obj.routes());
		        }
            }
        }
    }
}
module.exports = InitManager;

// app.js
const Koa = require('koa');
const app = new Koa();

const InitManager = require('./core/init');
// ...中间件等其他操作
// 最后引入路由
InitManager.initCore(app);

可以说已经精简很多了,代码看起来是不是很爽,而且功能的实现照样没有问题。

开发环境和生产环境的区分

有时候,在两种不同的环境下,我们需要做不同的处理,这时候就需要我们提前在全局中注入相应的参数。

首先在项目根目录中,创建config文件夹:

// config/config.js
module.exports = {
     
  environment: 'dev'
}
//core/init.js的initManager类中增加如下内容
static loadConfig() {
     
    const configPath = process.cwd() + '/config/config.js';
    const config = require(configPath);
    global.config = config;
}

// app.js
const Koa = require('koa');
const app = new Koa();

const InitManager = require('./core/init');
InitManager.loadConfig(); // 全局配置, 靠前引入,这样才能在用的时候有数据
// ...中间件等其他操作
// 最后引入路由
InitManager.initCore(app);

// 使用的时候
global.config.environment=== 'dev' ? dev : pro;

这样就通过全局的global变量中就可以取到当前的环境啦。

但是!!! 这样有个问题,你部署生产环境的时候要手动修改config.js里的environment配置(从代码上猜测的,笔者还没部署到生产环境)

统一接口返回格式

统一的返回格式可以提高前端的处理效率,在组件化开发的今天,统一的返回格式也能让前端更好的封装组件。如果一个项目里都没有一个统一的返回格式,那这项目代码质量多半好不到哪里去。

先定义成功和失败的返回格式:

// middleware/response/response.js
/**
 * response
 * @param ctx
 * @param data 数据
 * @param code 错误码 || [错误码, 错误描述]
 * @param message 错误描述
 */
exports.response = (ctx, data, code, message) => {
     
  if (typeof code == 'object') {
     
    message = code[1];
    code = code[0];
  }
  ctx.body = {
     
    code,
    data,
    message
  };
};

/**
 * response 成功
 * @param ctx
 * @param data 数据
 * @param code 错误码 || [错误码, 错误描述]
 * @param message 错误描述
 */
exports.success = (ctx, data, code = 1, message = '操作成功') => {
     
  if (typeof code === 'string') {
     
    message = code;
  }
  this.response(ctx, data, code, message);
};

/**
 * response 异常
 * @param ctx
 * @param code 错误码 || [错误码, 错误描述]
 * @param message 错误描述
 */
exports.error = (ctx, code = 0, data = '', message = '操作失败') => {
     
  if (typeof code === 'object') {
     
    message = code[1];
    code = code[0];
  }
  ctx.errorLog(ctx, message, 0); // 记录异常日志
  this.response(ctx, data, code, message);
};

// 导出模块
// middleware/response/index.js
const {
      success, error } = require('./response');

module.exports = async (ctx, next) => {
     
  ctx.success = success.bind(null, ctx);
  ctx.error = error.bind(null, ctx);
  await next();
};

// app.js
// ... 使用中间件
const response = require('./middleware/response');
app.use(response); // 返回体中间件
// ...

这样返回的时候直接使用就行了,格式统一,看起来舒服

ctx.error([0, '错误提示']);
// or
ctx.success(xxx);

全局异常处理中间件

在服务端api编写的过程中,异常处理是非常重要的一环,因为不可能每个函数返回的结果都是我们想要的。无论是语法的错误,还是业务逻辑上的错误,都需要让异常抛出,让问题以最直观的方式暴露,而不是直接忽略。关于编码风格,《代码大全》里面也强调过,在一个函数遇到异常时,最好的方式不是直接return false/null,而是让异常直接抛出。如果让异常直接抛出到前端会让人感觉这个后端真的菜。所以一定要封装。

设计异常处理中间件

// middleware/errorHandler.js
// 这里的工作是捕获异常生成返回的接口
const catchError = async (ctx, next) => {
     
  try {
     
    await next();
  } catch (error) {
     
    // console.log(error);
    if (error.errno) {
     
      // FIXME sql报错处理
      ctx.error([0, 'sql查询报错'], error.sqlMessage);
    } else if (error.code) {
     
      ctx.error([error.code, error.msg]);
    } else {
     
      // 对于未知的异常,采用特别处理
      ctx.errorLog(ctx, error, 'we made a mistake'); // 记录异常日志
      ctx.error([-1, '未知的异常']);
    }
  }
};

module.exports = catchError;

到入口文件使用这个中间件

// app.js
// 统一错误异常处理
const errorHandler = require('./middleware/errorHandler');
app.use(errorHandler);

接着我们来以HttpException为例生成特定类型的异常:

// core/http-exception.js
class HttpException extends Error {
     
  constructor(msg = '服务器异常', code = 400) {
     
    super();
    this.code = code;
    this.msg = msg;
  }
}

class ParamError extends HttpException {
     
  constructor(msg) {
     
    super();
    this.code = 400;
    this.msg = msg || '参数错误';
  }
}

class NotFound extends HttpException {
     
  constructor(msg) {
     
    super();
    this.msg = msg || '资源未找到';
    this.code = 404;
  }
}

class AuthFailed extends HttpException {
     
  constructor(msg) {
     
    super();
    this.msg = msg || '授权失败';
    this.code = 404;
  }
}

class Forbidden extends HttpException {
     
  constructor(msg) {
     
    super();
    this.msg = msg || '禁止访问';
    this.code = 404;
  }
}

module.exports = {
     
  HttpException,
  ParamError,
  NotFound,
  AuthFailed,
  Forbidden
};

对于这种经常需要调用的错误处理的代码,有必要将它放到全局,不用每次都导入。

现在的init.js中是这样的:

const requireDirectory = require('require-directory');
const Router = require('koa-router');

class InitManager {
     
  static initCore(app) {
     
    // 把app.js中的koa实例传进来
    InitManager.app = app;
    InitManager.initLoadRouters();
    InitManager.loadHttpException(); // 加入全局的Exception
  }

  static initLoadRouters() {
     
    // 注意这里的路径是依赖于当前文件所在位置的
    // 最好写成绝对路径
    const apiDirectory = `${
       process.cwd()}/routes`;
    const modules = requireDirectory(module, apiDirectory, {
     
      visit: whenLoadModule
    });
    function whenLoadModule(obj) {
     
      if (obj instanceof Router) {
     
        // FIXME 路由黑名单
        const blackList = ['/image'];
        const prefix = obj.opts.prefix;
        if (!blackList.includes(prefix)) {
     
          InitManager.app.use(obj.routes());
        }
      }
    }
  }

  static loadConfig(path = '') {
     
    const configPath = path || process.cwd() + '/config/config.js';
    const config = require(configPath);
    global.config = config;
  }

  static loadHttpException() {
     
    const errors = require('./http-exception');
    global.err = errors;
  }
}

module.exports = InitManager;

这样就可以全局使用了

// 接口中
if (xxx) throw new global.err.ParamError(xxx);

但是!! 好像只能通过throw的方法来抛出错误,又因为是类,所以必须先实例化new下,后面看看能不能再优化下

使用JWT完成认证授权

这个我在之前系列(7)里专门写过了,这边就不写了,也可以按本文借鉴的那篇文章里介绍的那样。
不过还缺少了刷新token的使用,有空补下。

nodejs require路径别名

在开发的过程,当项目的目录越来越复杂的时候,包的引用路径也变得越来越麻烦。曾经就出现过这样的导入路径:

const Favor = require('../../../models/favor');

甚至还有比这个更加冗长的导入方式,作为一个有代码洁癖的程序员,实在让人看的非常不爽。其实通过绝对路径process.cwd()的方式也是可以解决这样一个问题的,但是当目录深到一定程度的时候,导入的代码也非常繁冗。那有没有更好的解决方式呢?

使用module-alias将路径别名就可以。

npm install module-alias --save
//package.json添加如下内容
  "_moduleAliases": {
    "@root": ".", // 根目录
    "@models": "app/models",
  },

重点: 然后在app.js引入这个库:

// 引入即可
require('module-alias/register');

现在引入代码就变成这样了:

const Favor = require('@models/favor');

不过这样用了别名之后在vscode里F12就找不到函数的定义位置了,有点小不足

上一篇:node从入门到放弃系列之(10)图形验证功能
下一篇:未完待续!!!

你可能感兴趣的:(node从入门到放弃,nodejs)