Вооружите наш проект koa2 элегантным кодом

Node.js

Как мы все знаем, koa2 — это очень легкий серверный фреймворк, основанный на nodejs, а его простые и легкие в использовании функции значительно сокращают затраты разработчиков переднего плана, разрабатывающих серверные API. Несмотря на то, что многие функции могут быть реализованы, как грамотный разработчик, необходимо тщательно учитывать уровень кода и возможность последующего сопровождения.

Честно говоря, судя по официальной документации koa, наш код некрасив.

Здесь нам нужно иметь очень четкое представление перед кодированием: как организовать наш код? Насколько стратифицированный? Как повторно использовать?

После ряда размышлений и практики некоторых проектов я обобщил некоторые навыки разработки с помощью koa, которые могут значительно улучшить качество кода проекта, и больше не нужно позволять сверстникам смеяться над плохим кодом!

1. Автоматическая загрузка маршрутов

Раньше наши маршруты всегда прописывались вручную? Вероятно, что-то вроде этого:

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

const user = require('./app/api/user');
const store = require('./app/api/store');

app.use(user.routes());
app.use(classic.routes());

Знаком ли этот код всем, кто писал проект koa? На самом деле хорошо, что файлов роутинга всего два, но на самом деле количество таких файлов в определенной степени огромно, и вводить и переиспользовать так будет громоздко и долго. Есть ли способ сделать так, чтобы эти файлы автоматически импортировались и использовались автоматически?

немного. Теперь давайте установим очень полезный пакет:

npm install require-directory --save

Теперь просто сделайте это:

//...
const Router = require('koa-router'); 
const requireDirectory = require('require-directory');
//module为固定参数,'./api'为路由文件所在的路径(支持嵌套目录下的文件),第三个参数中的visit为回调函数
const modules = requireDirectory(module, './app/api', {
    visit: whenLoadModule
});
function whenLoadModule(obj) {
    if(obj instanceof Router) {
        app.use(obj.routes());
    }
}

Видно, что хороший код может повысить эффективность.Такой автоматический маршрут загрузки экономит много усилий при регистрации и настройке.Разве это не круто?

2. Используйте менеджер для извлечения содержимого входного файла

Полагаю, многие так и делают: код регистрации роутинга прописывается в файле входа app.js, и в этом же файле прописывается импорт соответствующего мидлвара. Но для входного файла мы не хотим, чтобы он сильно раздувался, поэтому мы можем правильно извлечь некоторые операции.

Создайте папку 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() {
        //注意这里的路径是依赖于当前文件所在位置的
        //最好写成绝对路径
        const apiDirectory = `${process.cwd()}/app/api`
        const modules = requireDirectory(module, apiDirectory, {
            visit: whenLoadModule
        });
        function whenLoadModule(obj) {
            if(obj instanceof Router) {
                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;
}

Теперь вы можете получить текущую среду через глобальную глобальную переменную.

4. Промежуточное ПО глобальной обработки исключений

1. Яма асинхронной обработки исключений

В процессе написания серверного API очень важной частью является обработка исключений, потому что невозможно, чтобы каждая функция возвращала желаемый результат. Будь то синтаксическая ошибка или ошибка бизнес-логики, необходимо генерировать исключения, чтобы проблему можно было выявить наиболее интуитивно понятным способом, а не игнорировать напрямую. Что касается стиля кодирования, «Энциклопедия кода» также подчеркнула, что когда функция сталкивается с исключением, лучший способ — не возвращать false/null напрямую, а напрямую вызывать исключение.

В JS много раз мы пишем асинхронный код, такой как таймеры, промисы и т. д., что вызовет проблему.Если используется try/catch, мы не можем отлавливать ошибки в таком асинхронном коде. Например:

function func1() {
  try {
    func2();
  } catch (error) {
    console.log('error');
  }
}

function func2() {
  setTimeout(() => {
    throw new Error('error')
  }, 1000)
}

func1();

Реализация кода, который вы найдете в течение одной секунды после того, как программа была передана напрямую, console.log ('ошибка') не выполнялась, то есть не было захвачено исключение func1 func2. Эта проблема асинхронная.

Так как решить эту яму?

Самый простой способ — использовать async-await.

async function func1() {
  try {
    await func2();
  } catch (error) {
    console.log('error');
  }
}

function func2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject()
    }, 1000)
  })
}

func1();

Асинхронная функция здесь инкапсулирована Promise, а затем reject запускает перехват в func1, который перехватывает исключение в func2. К счастью, для асинхронного кода, такого как func2, часто используемые библиотеки (такие как axios, sequenceize) уже инкапсулировали для нас объект Promise, и нам не нужно инкапсулировать его самим, просто перейдите к try/catch через async-await. .

Совет: Таким образом, пока код является асинхронным, ожидание должно быть добавлено перед выполнением, и об ошибке отклонения необработанного обещания не будет сообщено. Уроки крови!

2. Разработка промежуточного программного обеспечения для обработки исключений

//middlewares/exception.js
//这里的工作是捕获异常生成返回的接口
const catchError = async (ctx, next) => {
  try {
    await next();
  } catch (error) {
    if(error.errorCode) {
      ctx.body = {
        msg: error.msg,
        error_code: error.errorCode,
        request: `${ctx.method} ${ctx.path}`
      };
    } else {
      //对于未知的异常,采用特别处理
      ctx.body = {
        msg: 'we made a mistake',
      };
    }
  }
}
module.exports = catchError;

Для входа в файл используйте это промежуточное программное обеспечение.

//app.js
const catchError = require('./middlewares/exception');
app.use(catchError)

Далее, давайте возьмем HttpException в качестве примера для создания исключения определенного типа.

//core/http-exception.js
class HttpException extends Error {
  //msg为异常信息,errorCode为错误码(开发人员内部约定),code为HTTP状态码
  constructor(msg='服务器异常', errorCode=10000, code=400) {
    super()
    this.errorCode = errorCode
    this.code = code
    this.msg = msg
  }
}

module.exports = {
  HttpException
}
//app/api/user.js
const Router = require('koa-router')
const router = new Router()
const { HttpException } = require('../../core/http-exception')

router.post('/user', (ctx, next) => {
    if(true){
        const error = new HttpException('网络请求错误', 10001, 400)
        throw error
  }
})
module.exports = router;

Возвращаемый интерфейс выглядит следующим образом:

Это вызывает определенный тип ошибки. Но типы ошибок в бизнесе очень сложны.Теперь я поделюсь некоторыми классами исключений, которые я написал для вашей справки:

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

class ParameterException extends HttpException{
  constructor(msg, errorCode){
    super(400, msg='参数错误', errorCode=10000);
  }
}

class NotFound extends HttpException{
  constructor(msg, errorCode) {
    super(404, msg='资源未找到', errorCode=10001);
  }
}

class AuthFailed extends HttpException{
  constructor(msg, errorCode) {
    super(404, msg='授权失败', errorCode=10002);
  }
}

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

module.exports = {
  HttpException,
  ParameterException,
  Success,
  NotFound,
  AuthFailed,
  Forbidden
}

Для такого рода кода обработки ошибок, который необходимо часто вызывать, необходимо размещать его глобально, а не импортировать каждый раз.

Текущий init.js выглядит так:

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

class InitManager {
  static initCore(app) {
    //入口方法
    InitManager.app = app;
    InitManager.initLoadRouters();
    InitManager.loadConfig();
    InitManager.loadHttpException();//加入全局的Exception
  }
  static initLoadRouters() {
    // path config
    const apiDirectory = `${process.cwd()}/app/api/v1`;
    requireDirectory(module, apiDirectory, {
      visit: whenLoadModule
    });

    function whenLoadModule(obj) {
      if (obj instanceof Router) {
        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.errs = errors;
  }
}

module.exports = InitManager;

5. Используйте JWT для завершения аутентификации и авторизации

JWT (т. е. Json Web Token) в настоящее время является одним из самых популярных решений для междоменной аутентификации. Его рабочий процесс выглядит следующим образом:

1. Передняя часть передает имя пользователя и пароль задней части

2. После успешной проверки имени пользователя и пароля в бэкенде токен (или сохраненный в файле cookie) возвращается во внешний интерфейс.

3. Внешний интерфейс получает токен и сохраняет его

4. Когда внешний интерфейс обращается к внутреннему интерфейсу, сначала выполняется аутентификация по токену, а доступ к интерфейсу возможен только после прохождения аутентификации.

Итак, что нам нужно сделать в коа?

На этапе генерации токена: сначала проверяется учетная запись, затем генерируется токен токена и передается во внешний интерфейс.

На этапе токена аутентификации: после завершения написания промежуточного программного обеспечения аутентификации выполняется уровень перехвата на внешнем доступе, и доступ к последующему интерфейсу возможен только после аутентификации токена.

1. Сгенерировать токен

Сначала установите два пакета:

npm install jsonwebtoken basic-auth --save
//config.js
module.exports = {
  environment: 'dev',
  database: {
    dbName: 'island',
    host: 'localhost',
    port: 3306,
    user: 'root',
    password: 'fjfj'
  },
  security: {
    secretKey: 'lajsdflsdjfljsdljfls',//用来生成token的key值
    expiresIn: 60 * 60//过期时间
  }
}

//utils.js 
//生成token令牌函数,uid为用户id,scope为权限等级(类型为数字,内部约定)
const generateToken = function(uid, scope){
    const { secretKey, expiresIn } = global.config.security
    //第一个参数为用户信息的js对象,第二个为用来生成token的key值,第三个为配置项
    const token = jwt.sign({
        uid,
        scope
    },secretKey,{
        expiresIn
    })
    return token
}

2. Промежуточное ПО аутентификации реализует перехват

//前端传token方式
//在请求头中加上Authorization:`Basic ${base64(token+":")}`即可
//其中base64为第三方库js-base64导出的一个方法

//middlewares/auth.js
const basicAuth = require('basic-auth');
const jwt = require('jsonwebtoken');

class Auth {
  constructor(level) {
    Auth.USER = 8;
    Auth.ADMIN = 16;
    this.level = level || 1;
  }
  //注意这里的m是一个属性
  get m() {
    return async (ctx, next) => {
      const userToken = basicAuth(ctx.req);
      let errMsg = 'token不合法';

      if(!userToken || !userToken.name) {
        throw new global.errs.Forbidden();
      }
      try {
        //将前端传过来的token值进行认证,如果成功会返回一个decode对象,包含uid和scope
        var decode = jwt.verify(userToken.name, global.config.security.secretKey);
      } catch (error) {
        // token不合法
        // 或token过期
        // 抛异常
        errMsg = '//根据情况定义'
        throw new global.errs.Forbidden(errMsg);
      }
      //将uid和scope挂载ctx中
      ctx.auth = {
        uid: decode.uid,
        scope: decode.scope
      };
      //现在走到这里token认证通过
      await next();
    }
  }
}
module.exports = Auth;

Напишите в соответствующем файле маршрута следующее:

//中间件先行,如果中间件中认证未通过,则不会走到路由处理逻辑这里来
router.post('/xxx', new Auth().m , async (ctx, next) => {
    //......
})

Шесть, требуется псевдоним пути

В процессе разработки, когда каталог проекта становится все более и более сложным, ссылочный путь пакета становится все более и более хлопотным. Раньше был такой путь импорта:

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

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

Используйте псевдоним модуля для псевдонима пути.

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

Затем импортируйте эту библиотеку в app.js:

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

Теперь код импорта выглядит так:

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

Гораздо проще и понятнее, и проще в обслуживании.

Семь, используйте транзакцию сиквелизации, чтобы решить проблему несогласованности данных.

Когда бизнесу необходимо выполнить несколько операций с базой данных, возьмем в качестве примера функцию like.Сначала вам нужно добавить запись в таблицу подобных записей, а затем вам нужно увеличить количество лайков соответствующего объекта на 1. Эти две операции: Это должно быть выполнено вместе Если одна операция будет успешной, а другая завершится ошибкой, это приведет к несогласованности данных, что является очень серьезной проблемой безопасности.

Мы хотим откатить непосредственно в состояние перед операцией, если что-то пойдет не так. В это время рекомендуется использовать операции транзакций базы данных. Использование транзакции SEXELIZE может быть завершена, вставьте код бизнес-части:

async like(art_id, uid) {
    //查找是否有重复的
    const favor = await Favor.findOne({
      where: { art_id, uid }
      }
    );
    //有重复则抛异常
    if (favor) {
      throw new global.errs.LikeError('你已经点过赞了');
    }
    //db为sequelize的实例
    //下面是事务的操作
    return db.transaction(async t => {
      //1.创建点赞记录
      await Favor.create({ art_id, uid }, { transaction: t });
      //2.增加点赞数
      const art = await Art.getData(art_id, type);//拿到被点赞的对象
      await art.increment('fav_nums', { by: 1, transaction: t });//加1操作
    });
  }

Транзакция в sequenceize, вероятно, делается так, официальный документ — это путь promise, который выглядит слишком неприглядно, гораздо лучше будет изменить его на async/await, но не забудьте написать await.

Что касается оптимизации кода koa2, я сначала поделюсь здесь, продолжение следует, и оно будет добавляться в будущем. Лайки и комментарии приветствуются!