Разбираемся в Eggjs в одной статье — исчерпывающее объяснение основ (посередине)

Egg.js
Разбираемся в Eggjs в одной статье — исчерпывающее объяснение основ (посередине)

Оглядываясь назад на то, что было сказано в предыдущем посте,Часть 1сказал:

Рабочая среда

Само веб-приложение не должно иметь состояния и иметь возможность устанавливать себя в соответствии со средой выполнения.

Укажите операционную среду

  • пройти черезconfig/envСпецификация файла, содержимое файла - это рабочая среда, такая какprod
  • пройти черезEGG_SERVER_ENVспецификация переменной окружения

Метод 2 используется чаще, посколькуEGG_SERVER_ENVПеременные среды более удобны для указания рабочей среды, например запуска приложения в производственной среде:

EGG_SERVER_ENV=prod npm start

Получите среду выполнения в приложении

использоватьapp.config.envПолучать

Отличие от NODE_ENV

Среда выполнения и взаимосвязь сопоставления, поддерживаемые платформой по умолчанию (если не указано иное).EGG_SERVER_ENVбудет основываться наNODE_ENVсоответствовать)

NODE_ENV EGG_SERVER_ENV иллюстрировать
local локальная среда разработки
test unittest модульный тест
production prod Производственная среда

когдаNODE_ENVдляproductionа такжеEGG_SERVER_ENVЕсли не указано, фреймворк будетEGG_SERVER_ENVустановлен вprod.

пользовательская среда

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

Например, чтобы добавить в процесс разработки интегрированную тестовую среду SIT. БудуEGG_SERVER_ENVустановлен вsit(и рекомендуем установитьNODE_ENV = production), который загружается при запускеconfig/config.sit.js, запустить переменную средыapp.config.envбудет установлен наsit.

Отличие от Коа

В Коа мы проходимapp.envвыносить экологические суждения,app.envЗначение по умолчаниюprocess.env.NODE_ENV.

В яйцеклетках (на основе яиц и кадр) единая конфигурация размещена вapp.config, поэтому нам нужно пройтиapp.config.envразличать окружение,app.envБольше не использую.

Конфигурация конфигурации

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

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

Конфигурация с несколькими средами

Платформа поддерживает загрузку конфигурации в соответствии со средой и определяет файлы конфигурации для нескольких сред.

config
|- config.default.js
|- config.prod.js
|- config.unittest.js
|- config.local.js

config.default.jsВ качестве файла конфигурации по умолчанию все среды будут загружать этот файл конфигурации, как правило, в качестве файла конфигурации по умолчанию для среды разработки.

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

Написание конфигурации

Файл конфигурации возвращает объектный объект, который может переопределить некоторые конфигурации фреймворка.Приложение также может поместить сюда свою собственную бизнес-конфигурацию для удобства управления.app.config.

экспортировать нотацию объекта

// 配置 logger 文件的目录,logger 默认配置由框架提供
module.exports = {
  logger: {
    dir: '/home/admin/logs/demoapp',
  },
};

Файл конфигурации также может возвращать функцию, которая принимаетappInfoпараметр

// 将 logger 目录放到代码目录下
const path = require('path');
module.exports = appInfo => {
  return {
    logger: {
      dir: path.join(appInfo.baseDir, 'logs'),
    },
  };
};

Встроенные appInfos:

appInfo иллюстрировать
pkg package.json
name Имя приложения, такое же, как pkg.name
baseDir каталог приложений кода
HOME Каталог пользователя, например учетная запись администратора, /home/admin
root Корневой каталог приложения — это baseDir только в локальных средах и средах unittest и HOME для других.

appInfo.rootЭто элегантная адаптация, например, в серверной среде, которую мы будем использовать./home/admin/logsКак каталог журнала, и вы не хотите загрязнять каталог пользователя во время локальной разработки, эта адаптация является хорошим решением этой проблемы.

Объединить правила

Объединенное использование конфигурацииextend2модуль для глубокого копирования

const a = {
  arr: [ 1, 2 ],
};
const b = {
  arr: [ 3 ],
};
extend(true, a, b);
// => { arr: [ 3 ] }
// 框架直接覆盖数组而不是进行合并。

Результат конфигурации

Фреймворк выгружает окончательную объединенную конфигурацию вrun/application_config.json(рабочий процесс) иrun/agent_config.json(процесс агента), можно использовать для анализа проблемы.

ПО промежуточного слоя

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

написать промежуточное ПО

написание

Написание содержимого промежуточного программного обеспечения

// app/middleware/gzip.js
const isJSON = require('koa-is-json');
const zlib = require('zlib');

async function gzip(ctx, next) {
  await next();

  // 后续中间件执行完成后将响应体转换成 gzip
  let body = ctx.body;
  if (!body) return;
  if (isJSON(body)) body = JSON.stringify(body);

  // 设置 gzip body,修正响应头
  const stream = zlib.createGzip();
  stream.end(body);
  ctx.body = stream;
  ctx.set('Content-Encoding', 'gzip');
}

Промежуточное ПО фреймворка написано точно так же, как промежуточное ПО Коа, поэтому любое промежуточное ПО Коа может напрямую использоваться фреймворком.

настроить

Вообще говоря, промежуточное ПО также будет иметь свою собственную конфигурацию.

Условимся, что middleware — это отдельный файл, помещаемый в директорию app/middleware, он должен экспортировать обычную функцию, которая принимает два параметра:

  • варианты: элементы конфигурации промежуточного программного обеспечения, фреймворк будетapp.config[${middlewareName}]Передано, поэтому вы можете напрямую получить конфигурацию промежуточного программного обеспечения в файле конфигурации.
  • app: экземпляр текущего приложения

Сделайте простую оптимизацию вышеуказанного промежуточного программного обеспечения gzip, чтобы оно поддерживало указание сжатия gzip только тогда, когда размер тела превышает настроенный порог.app/middlewareСоздайте новый файл в каталогеgzip.js

// app/middleware/gzip.js
const isJSON = require('koa-is-json');
const zlib = require('zlib');

module.exports = options => {
  return async function gzip(ctx, next) {
    await next();

    // 后续中间件执行完成后将响应体转换成 gzip
    let body = ctx.body;
    if (!body) return;

    // 支持 options.threshold
    if (options.threshold && ctx.length < options.threshold) return;

    if (isJSON(body)) body = JSON.stringify(body);

    // 设置 gzip body,修正响应头
    const stream = zlib.createGzip();
    stream.end(body);
    ctx.body = stream;
    ctx.set('Content-Encoding', 'gzip');
  };
};

Использовать промежуточное ПО

После того, как промежуточное программное обеспечение написано, нам также нужно установить его вручную

Используйте промежуточное ПО в приложении

существуетconfig.default.jsДобавьте следующую конфигурацию, чтобы завершить открытие и настройку промежуточного программного обеспечения.

module.exports = {
  // 配置需要的中间件,数组顺序即为中间件的加载顺序
  middleware: [ 'gzip' ],

  // 配置 gzip 中间件的配置
  gzip: {
    threshold: 1024, // 小于 1k 的响应体不压缩
  },
  // options.gzip.threshold
};

Эта конфигурация в конечном итоге будет объединена вapp.config.appMiddleware

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

Фреймворки и плагины не поддерживаются вconfig.default.jsсреднее соответствиеmiddleware, вам необходимо сделать следующее:

// app.js
module.exports = app => {
  // 在中间件最前面统计请求时间
  app.config.coreMiddleware.unshift('report');
};

// app/middleware/report.js
module.exports = () => {
  return async function (ctx, next) {
    const startTime = Date.now();
    await next();
    // 上报请求时间
    reportTime(Date.now() - startTime);
  }
};

ПО промежуточного слоя, определяемое прикладным уровнем (app.config.appMiddleware) и промежуточное ПО по умолчанию (app.config.coreMiddleware) будет загружен загрузчиком и смонтирован вapp.middlewareначальство.

Использовать промежуточное ПО в маршрутизаторе

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

Если вы хотите применить эффект только для одного маршрута, вы можете напрямуюapp/router.jsсоздается и монтируется следующим образом:

module.exports = app => {
 const gzip = app.middleware.gzip({ threshold: 1024 });
 app.router.get('/needgzip', gzip, app.controller.handler);
};

Промежуточное ПО по умолчанию для фреймворка

В дополнение к промежуточному ПО, загружаемому прикладным уровнем, сам фреймворк и другие плагины также загружают много промежуточного ПО.

Все эти элементы конфигурации со встроенным промежуточным ПО модифицируются путем изменения одноименного элемента конфигурации промежуточного ПО в конфигурации.

В промежуточном программном обеспечении, которое поставляется с фреймворком, есть промежуточное программное обеспечение bodyParser (загрузчик фреймворка изменит различные разделители в имени файла на имена переменных в случае верблюда), мы хотим изменить конфигурацию bodyParser, просто вconfig/config.default.jsнаписано на

module.exports = {
  bodyParser: {
    jsonLimit: '10mb',
  },
};

Промежуточное ПО с использованием Koa

Экосистему промежуточного программного обеспечения Koa можно легко внедрить во фреймворк.

Возьмите koa-compress в качестве примера при использовании в Koa:

const koa = require('koa');
const compress = require('koa-compress');

const app = koa();

const options = { threshold: 2048 };
app.use(compress(options));

Egg

// app/middleware/compress.js
// koa-compress 暴露的接口(`(options) => middleware`)和框架对中间件要求一致
module.exports = require('koa-compress');

настроить промежуточное ПО

// config/config.default.js
module.exports = {
  middleware: [ 'compress' ],
  compress: {
    threshold: 2048,
  },
}

Общая конфигурация

  • enable: контролирует, включено ли промежуточное ПО.
  • match: установите, что только запросы, соответствующие определенным правилам, будут проходить через это промежуточное ПО.
  • игнорировать: настроить запросы, соответствующие определенным правилам, чтобы они не проходили через это промежуточное ПО.

соответствовать и игнорировать

Параметры, поддерживаемые match и ignore, одинаковы, но функции полностью противоположны, Match и ignore не могут быть настроены одновременно.

module.exports = {
  gzip: {
    match: '/static',
  },
};

module.exports = {
  gzip: {
    match(ctx) {
      // 只有 ios 设备才开启
      const reg = /iphone|ipad|ipod/i;
      return reg.test(ctx.get('user-agent'));
    },
  },
};

правила соответствия, взятые изegg-path-matching

const pathMatching = require('egg-path-matching');
const options = {
  ignore: '/api', // string will use parsed by path-to-regexp
  // support regexp
  ignore: /^\/api/,
  // support function
  ignore: ctx => ctx.path.startsWith('/api'),
  // support Array
  ignore: [ ctx => ctx.path.startsWith('/api'), /^\/foo$/, '/bar'],
  // support match or ignore
  match: '/api',
};

маршрутизация

Маршрутизатор в основном используется для описания соответствующих отношений между URL-адресом запроса и контроллером, который отвечает за выполнение действия.app/router.jsфайл используется для унификации всех правил маршрутизации.

Как определить маршрутизатор

  • app/router.jsОпределите правила маршрутизации URL внутри
// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.get('/user/:id', controller.user.info);
};
  • app/controllerРеализовать контроллер в каталоге
// app/controller/user.js
class UserController extends Controller {
  async info() {
    const { ctx } = this;
    ctx.body = {
      name: `hello ${ctx.params.id}`,
    };
  }
}

Подробное описание маршрутизатора

Ниже приводится полное определение маршрутизации, параметры могут свободно выбираться в соответствии с различными сценариями:

router.verb('path-match', app.controller.action);
router.verb('router-name', 'path-match', app.controller.action);
router.verb('path-match', middleware1, ..., middlewareN, app.controller.action);
router.verb('router-name', 'path-match', middleware1, ..., middlewareN, app.controller.action);

Полное определение маршрутизации в основном включает 5 основных частей:

  • verb — действие, инициируемое пользователем, поддерживает все методы HTTP, такие как get, post и т. д., которые будут подробно объяснены с примерами позже.
    • router.head - HEAD
    • router.options - OPTIONS
    • router.get - GET
    • router.put - PUT
    • router.post - POST
    • router.patch - PATCH
    • router.delete - DELETE
    • router.del- Поскольку удаление является зарезервированным словом, предоставляется псевдоним для метода удаления.
    • router.redirect- URL-адрес может быть перенаправлен, например, корневой каталог, который мы используем чаще всего, может направить корневой каталог, к которому обращается пользователь, на домашнюю страницу.
  • router-name устанавливает псевдоним для маршрута и может генерировать URL-адреса с помощью вспомогательных функций pathFor и urlFor, предоставляемых Helper. (необязательный)
  • path-match - маршрутизировать пути URL
  • middleware1...middlewareN — в маршрутизаторе можно настроить несколько промежуточных программ. (Необязательно) укажите, что URL-адрес обрабатывается только этим ПО промежуточного слоя.
  • controller — указывает конкретный контроллер, на который сопоставляется маршрут.Контроллер можно записать двумя способами:
    • app.controller.user.fetch- Укажите конкретный контроллер напрямую
    • 'user.fetch'- может быть сокращено до строковой формы

Меры предосторожности

  • В определении маршрутизатора он может поддерживать несколько тандемных исполнений промежуточного программного обеспечения.
  • Контроллер должен быть определен в каталоге app/controller.
  • Файл также может содержать несколько определений контроллера.При определении маршрута вы можете передать${fileName}.${functionName}способ указать соответствующий контроллер.
  • Контроллер поддерживает подкаталоги, при определении маршрутов можно передать${directoryName}.${fileName}.${functionName}способ сформулировать соответствующий Контроллер.

demo

// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.get('/home', controller.home);
  router.get('/user/:id', controller.user.page);
  router.post('/admin', isAdmin, controller.admin);
  router.post('/user', isLoginUser, hasAdminPermission, controller.user.create);
  router.post('/api/v1/comments', controller.v1.comments.create); // app/controller/v1/comments.js
};

Определение URL-адреса RESTful

при условииapp.resources('routerName', 'pathMatch', controller)Быстро создавайте структуры маршрутизации CRUD на пути.

// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.resources('posts', '/api/posts', controller.posts);
  router.resources('users', '/api/v1/users', controller.v1.users); // app/controller/v1/users.js
};

Приведенный выше код/postsНабор структур пути CRUD развертывается на пути, и соответствующий контроллерapp/controller/posts.jsДалее нужно простоposts.jsДостаточно реализовать внутри соответствующую функцию.

Method Path Route Name Controller.Action
GET /posts posts app.controllers.posts.index
GET /posts/new new_post app.controllers.posts.new
GET /posts/:id post app.controllers.posts.show
GET /posts/:id/edit edit_post app.controllers.posts.edit
POST /posts posts app.controllers.posts.create
PUT /posts/:id post app.controllers.posts.update
DELETE /posts/:id post app.controllers.posts.destroy
// app/controller/posts.js
exports.index = async () => {};

exports.new = async () => {};

exports.create = async () => {};

exports.show = async () => {};

exports.edit = async () => {};

exports.update = async () => {};

exports.destroy = async () => {};

Если какие-то из этих методов нам не нужны, их не нужно реализовывать в posts.js, чтобы соответствующий URL-путь не регистрировался в Router.

роутер в действии

сбор параметров

Метод строки запроса

ctx.query.xxx

// app/router.js
module.exports = app => {
  app.router.get('/search', app.controller.search.index);
};

// app/controller/search.js
exports.index = async ctx => {
  ctx.body = `search: ${ctx.query.name}`;
};

// curl http://127.0.0.1:7001/search?name=egg

наименование параметра

/user/:id/:name => ctx.params.xxx

// app/router.js
module.exports = app => {
  app.router.get('/user/:id/:name', app.controller.user.info);
};

// app/controller/user.js
exports.info = async ctx => {
  ctx.body = `user: ${ctx.params.id}, ${ctx.params.name}`;
};

// curl http://127.0.0.1:7001/user/123/xiaoming

Получить сложные параметры

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

// app/router.js
module.exports = app => {
  app.router.get(/^\/package\/([\w-.]+\/[\w-.]+)$/, app.controller.package.detail);
};

// app/controller/package.js
exports.detail = async ctx => {
  // 如果请求 URL 被正则匹配, 可以按照捕获分组的顺序,从 ctx.params 中获取。
  // 按照下面的用户请求,`ctx.params[0]` 的 内容就是 `egg/1.0.0`
  ctx.body = `package:${ctx.params[0]}`;
};

// curl http://127.0.0.1:7001/package/egg/1.0.0

Получить содержимое формы

// app/router.js
module.exports = app => {
  app.router.post('/form', app.controller.form.post);
};

// app/controller/form.js
exports.post = async ctx => {
  ctx.body = `body: ${JSON.stringify(ctx.request.body)}`;
};

Инициирование запроса POST непосредственно здесь приведет к сообщению об ошибке.

Вышеупомянутая проверка связана с тем, что подключаемый модуль безопасности egg-security встроен в платформу, которая обеспечивает некоторые методы обеспечения безопасности по умолчанию, а подключаемый модуль безопасности платформы включен по умолчанию. меры предосторожности, можно напрямую установить для атрибута enable этого элемента значение false.

временно отключить csrf

exports.security = {
  csrf: false
};

проверка формы

npm i -S egg-validate
// config/plugin.js
module.exports = {
  // had enabled by egg
  // static: {
  //   enable: true,
  // }
  validate: {
    enable: true,
    package: 'egg-validate',
  },
};
// app/router.js
module.exports = app => {
  app.router.resources('/user', app.controller.user);
};

// app/controller/user.js
const createRule = {
  username: {
    type: 'email',
  },
  password: {
    type: 'password',
    compare: 're-password',
  },
};

exports.create = async ctx => {
  // 如果校验报错,会抛出异常
  ctx.validate(createRule);
  ctx.body = ctx.request.body;
};

перенаправить

Внутреннее перенаправление

// app/router.js
module.exports = app => {
  app.router.get('index', '/home/index', app.controller.home.index);
  app.router.redirect('/', '/home/index', 303); // 访问 / 自动重定向到 /home/index
};

// app/controller/home.js
exports.index = async ctx => {
  ctx.body = 'hello controller';
};

Внешнее перенаправление

exports.index = async ctx => {
  const type = ctx.query.type;
  const q = ctx.query.q || 'nodejs';

  if (type === 'bing') {
    ctx.redirect(`http://cn.bing.com/search?q=${q}`);
  } else {
    ctx.redirect(`https://www.google.co.kr/search?q=${q}`);
  }
};

Контроллер

Контроллер отвечает за анализ ввода пользователя и возврат соответствующего результата после обработки.

Framework рекомендует основной уровень контроллераОбработать (проверить, преобразовать) параметры запроса пользователя, а затем вызвать соответствующий сервисный метод для обработки бизнес-процесса, инкапсулировать и вернуть бизнес-результат после получения бизнес-результата:

  • Получить параметры запроса, переданные пользователем через HTTP
  • Калибровка и параметры сборки
  • Вызвать Сервис для бизнес-обработки и при необходимости обработать и преобразовать возвращенный результат Сервиса, чтобы адаптировать его к потребностям пользователей.
  • Отвечать пользователю по HTTP

Как написать контроллер

Все файлы контроллера должны быть помещены в каталог app/controller, который может поддерживать многоуровневые каталоги и может быть доступен через каскадное имя каталога при доступе.

Класс контроллера (рекомендуется)

// app/controller/post.js
const Controller = require('egg').Controller;
class PostController extends Controller {
  async create() {
    const { ctx } = this;
    
    ctx.body = 'PostController';
    ctx.status = 201;
  }
}
module.exports = PostController;

Определенный выше метод может быть расположен в маршруте через app.controller на основе имени файла и имени метода.

// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.post('createPost', '/api/posts', controller.post.create);
}

Контроллер поддерживает многоуровневые каталоги, например, если мы поместим приведенный выше код контроллера вapp/controller/sub/post.jsв то

app.router.post('createPost', '/api/posts', app.controller.sub.post.create);

Введение атрибутаОпределенный класс Controller будет создавать экземпляр совершенно нового объекта, когда каждый запрос обращается к серверу, а класс Controller в проекте наследуется отegg.Controller, будут висеть следующие свойстваthisначальство.

  • this.ctx: Экземпляр объекта контекста текущего запроса, через который мы можем получить различные удобные свойства и методы, инкапсулированные фреймворком для обработки текущего запроса.
  • this.app: Экземпляр текущего объекта приложения Application, через который мы можем получить глобальные объекты и методы, предоставляемые фреймворком.
  • this.service: Служба, определяемая приложением, через которую мы можем получить доступ к абстрактному бизнес-уровню, который эквивалентен this.ctx.service .
  • this.config: элементы конфигурации для среды выполнения приложения.
  • this.logger: объект логгера с четырьмя методами (debug,info,warn,error), Representing four different levels of print logs, and use the same effects as described in the context of the logger, but by the logger object record log, the log will be added in front of the print log file path to quickly locate log print позиция.

Базовый класс пользовательского контроллера

// app/core/base_controller.js
const { Controller } = require('egg');
class BaseController extends Controller {
  get user() {
    return this.ctx.session.user;
  }

  success(data) {
    this.ctx.body = {
      success: true,
      data,
    };
  }

  notFound(msg) {
    msg = msg || 'not found';
    this.ctx.throw(404, msg);
  }
}
module.exports = BaseController;

На этом этапе при написании контроллера приложения вы можете наследовать BaseController и напрямую использовать методы базового класса:

//app/controller/post.js
const Controller = require('../core/base_controller');
class PostController extends Controller {
  async list() {
    const posts = await this.service.listByUser(this.user); // 使用基类的方法
    this.success(posts); // 使用基类的方法
  }
}

Метод контроллера

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

// app/controller/post.js
exports.create = async ctx => {
  const createRule = {
    title: { type: 'string' },
    content: { type: 'string' },
  };
  // 校验参数
  ctx.validate(createRule);
  // 组装参数
  const author = ctx.session.userId;
  const req = Object.assign(ctx.request.body, { author });
  // 调用 service 进行业务处理
  const res = await ctx.service.post.create(req);
  // 设置响应内容和响应状态码
  ctx.body = { id: res.id };
  ctx.status = 201;
};

Получить параметры HTTP-запроса

query

class PostController extends Controller {
  async listPosts() {
    const query = this.ctx.query;
    // {
    //   category: 'egg',
    //   language: 'node',
    // }
  }
}

Когда ключ в строке запроса повторяется,ctx.queryБерется только значение первого вхождения ключа, а последующие вхождения будут игнорироваться.

queries

Иногда наша система позволяет пользователям передавать один и тот же ключ, напримерGET /posts?category=egg&id=1&id=2&id=3.

стойка предоставляетсяctx.queriesобъект, который также анализирует строку запроса, но вместо того, чтобы отбрасывать любые дубликаты, он помещает их все в массив.

// GET /posts?category=egg&id=1&id=2&id=3
class PostController extends Controller {
  async listPosts() {
    console.log(this.ctx.queries);
    // {
    //   category: [ 'egg' ],
    //   id: [ '1', '2', '3' ],
    // }
  }
}

Router params

Параметры также могут быть объявлены на маршрутизаторе, и эти параметры могут быть переданы черезctx.paramsполученный.

// app.get('/projects/:projectId/app/:appId', 'app.listApp');
// GET /projects/1/app/2
class AppController extends Controller {
  async listApp() {
    assert.equal(this.ctx.params.projectId, '1');
    assert.equal(this.ctx.params.appId, '2');
  }
}

body

рама встроеннаяbodyParserПромежуточное ПО для анализа тела запроса этих двух форматов в объект и его монтирования вctx.request.bodyначальство.

Вообще говоря, элемент конфигурации, который мы чаще всего настраиваем, заключается в изменении максимальной длины, разрешенной во время синтаксического анализа, которую можно установить вconfig/config.default.jsПереопределите значение по умолчанию для фреймворка в .

module.exports = {
  bodyParser: {
    jsonLimit: '1mb',
    formLimit: '1mb',
  },
};

Если тело запроса пользователя превышает максимальную длину синтаксического анализа, которую мы настроили, будет выдан код состояния.413Исключение (Request Entity Too Large), если тело запроса пользователя не может быть проанализировано (неправильный JSON), будет выдан код состояния.400Исключение (неверный запрос).

Распространенная ошибка — ставитьctx.request.bodyа такжеctx.bodyпутаница, последнее на самом делеctx.response.bodyсокращение для .

Получить загруженные файлы

Обычно браузеры используютMultipart/form-dataДля отправки файлов в формате фреймворк поддерживает получение файлов, загруженных пользователями, через встроенный плагин Multipart.

Режим файла:

Включите файловый режим в файле конфигурации:

// config/config.default.js
exports.multipart = {
  mode: 'file',
};
Чтобы загрузить/получить файлы:

Чтобы загрузить/получить один файл:

<form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
  title: <input name="title" />
  file: <input name="file" type="file" />
  <button type="submit">Upload</button>
</form>

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

// app/controller/upload.js
const Controller = require('egg').Controller;
const fs = require('mz/fs');

module.exports = class extends Controller {
  async upload() {
    const { ctx } = this;
    const file = ctx.request.files[0];
    const name = 'egg-multipart-test/' + path.basename(file.filename);
    let result;
    try {
      // 处理文件,比如上传到云端
      result = await ctx.oss.put(name, file.filepath);
    } finally {
      // 需要删除临时文件
      await fs.unlink(file.filepath);
    }

    ctx.body = {
      url: result.url,
      // 获取所有的字段值
      requestBody: ctx.request.body,
    };
  }
};

Загрузить/получить несколько файлов:

Для нескольких файлов мы используемctx.request.filesСвойства просматриваются и затем обрабатываются отдельно:

<form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
  title: <input name="title" />
  file1: <input name="file1" type="file" />
  file2: <input name="file2" type="file" />
  <button type="submit">Upload</button>
</form>

Соответствующий внутренний код:

// app/controller/upload.js
const Controller = require('egg').Controller;
const fs = require('mz/fs');

module.exports = class extends Controller {
  async upload() {
    const { ctx } = this;
    console.log(ctx.request.body);
    console.log('got %d files', ctx.request.files.length);
    for (const file of ctx.request.files) {
      console.log('field: ' + file.fieldname);
      console.log('filename: ' + file.filename);
      console.log('encoding: ' + file.encoding);
      console.log('mime: ' + file.mime);
      console.log('tmp filepath: ' + file.filepath);
      let result;
      try {
        // 处理文件,比如上传到云端
        result = await ctx.oss.put('egg-multipart-test/' + file.filename, file.filepath);
      } finally {
        // 需要删除临时文件
        await fs.unlink(file.filepath);
      }
      console.log(result);
    }
  }
};

Чтобы обеспечить безопасность загрузки файлов, платформа ограничивает поддерживаемые форматы файлов.По умолчанию платформа поддерживает белые списки следующим образом:

// images
'.jpg', '.jpeg', // image/jpeg
'.png', // image/png, image/x-png
'.gif', // image/gif
'.bmp', // image/bmp
'.wbmp', // image/vnd.wap.wbmp
'.webp',
'.tif',
'.psd',
// text
'.svg',
'.js', '.jsx',
'.json',
'.css', '.less',
'.html', '.htm',
'.xml',
// tar
'.zip',
'.gz', '.tgz', '.gzip',
// video
'.mp3',
'.mp4',
'.avi',

Пользователи могут использоватьconfig/config.default.jsconfig, чтобы добавить поддерживаемые расширения файлов или переопределить весь белый список.

  • Добавлены поддерживаемые расширения файлов
module.exports = {
  multipart: {
    fileExtensions: [ '.apk' ] // 增加对 apk 扩展名的文件支持
  },
};
  • Переопределить весь белый список
module.exports = {
  multipart: {
    whitelist: [ '.png' ], // 覆盖整个白名单,只允许上传 '.png' 格式
  },
};

смотрите подробностиEgg-Multipart

header

Помимо получения параметров из URL-адреса и тела запроса, многие параметры передаются через заголовки запроса.

  • ctx.headers,ctx.header,ctx.request.headers,ctx.request.header: Эти методы эквивалентны, все они полученывесь объект заголовка.
  • ctx.get(name),ctx.request.get(name): Получить значение поля в заголовке запроса. Если поле не существует, будет возвращена пустая строка.
  • Мы рекомендуем использоватьctx.get(name)вместоctx.headers['name'], потому что первый обрабатывает регистр автоматически.

Cookie

пройти черезctx.cookiesМы можем сделать удобные, безопасные настройки и прочитать файлы cookie в Контроллере.

class CookieController extends Controller {
  async add() {
    const ctx = this.ctx;
    const count = ctx.cookies.get('count');
    count = count ? Number(count) : 0;
    ctx.cookies.set('count', ++count);
    ctx.body = count;
  }

  async remove() {
    const ctx = this.ctx;
    const count = ctx.cookies.set('count', null);
    ctx.status = 204;
  }
}

Session

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

рама встроеннаяSessionПлагин предоставляет нам ctx.session для доступа или изменения Session текущего пользователя.

class PostController extends Controller {
  async fetchPosts() {
    const ctx = this.ctx;
    // 获取 Session 上的内容
    const userId = ctx.session.userId;
    const posts = await ctx.service.post.fetch(userId);
    // 修改 Session 的值
    ctx.session.visited = ctx.session.visited ? ++ctx.session.visited : 1;
    ctx.body = {
      success: true,
      posts,
    };
  }
}

Использование сеанса очень интуитивно понятно. Вы можете напрямую прочитать или изменить его. Если вы хотите удалить его, вы можете напрямую назначить его какnull:

class SessionController extends Controller {
  async deleteSession() {
    this.ctx.session = null;
  }
};

настроить

module.exports = {
  key: 'EGG_SESS', // 承载 Session 的 Cookie 键值对名字
  maxAge: 86400000, // Session 的最大有效时间
};

проверка параметров

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

Поскольку это подключаемый модуль, необходимо выполнить регистрацию подключаемого модуля:

// config/plugin.js
exports.validate = {
  enable: true,
  package: 'egg-validate',
};

пройти черезctx.validate(rule, [body])Проверьте параметры напрямую:

this.ctx.validate({
  title: { type: 'string' },
  content: { type: 'string' },
});

Когда проверка ненормальна, исключение будет выдано напрямую, а код состояния исключения422(Необрабатываемый объект), поле ошибок содержит подробную информацию об ошибке проверки. Если вы хотите самостоятельно обрабатывать проверенные исключения, вы можете передатьtry catchСними сам.

class PostController extends Controller {
  async create() {
    const ctx = this.ctx;
    try {
      ctx.validate(createRule);
    } catch (err) {
      ctx.logger.warn(err.errors);
      ctx.body = { success: false };
      return;
    }
  }
};

Проверить правила

Проверка параметров пройденаParameterГотово, поддерживаемые правила проверки можно найти в документации этого модуля.

в состоянии пройтиapp.validator.addRule(type, check)способ добавления пользовательских правил.

// app.js
app.validator.addRule('json', (rule, value) => {
  try {
    JSON.parse(value);
  } catch (err) {
    return 'must be json string';
  }
});

позвоните в сервис

Любой метод любой службы может быть вызван в контроллере, а служба загружается отложенно, и инфраструктура создает ее экземпляр только при доступе к ней.

Отправить HTTP-ответ

установить статус

ctx.status = 201;

установить тело

Большая часть данных отправляется инициатору запроса через тело.Как и тело в запросе, тело, отправленное в ответе, также должно иметь соответствующий Content-Type, чтобы сообщить клиенту, как анализировать данные.

ctx.bodyдаctx.response.bodyсокращение от , неctx.request.bodyСмущенный.

class ViewController extends Controller {
  async show() {
    this.ctx.body = {
      name: 'egg',
      category: 'framework',
      language: 'Node.js',
    };
  }

  async page() {
    this.ctx.body = '<html><h1>Hello</h1></html>';
  }
}

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

class ProxyController extends Controller {
  async proxy() {
    const ctx = this.ctx;
    const result = await ctx.curl(url, {
      streaming: true,
    });
    ctx.set(result.header);
    // result.res 是一个 stream
    ctx.body = result.res;
  }
};

установить заголовок

пройти черезctx.set(key, value)метод может установить заголовок ответа,ctx.set(headers)Установите несколько заголовков.

перенаправить

Фреймворк переопределяет родной koa с помощью плагина безопасности.ctx.redirectРеализовано для обеспечения более безопасного перенаправления.

  • ctx.redirect(url)Если его нет в настроенном белом списке доменного имени, перенаправление запрещено.
  • ctx.unsafeRedirect(url)Перейти напрямую, не оценивая доменное имя. Как правило, его использовать не рекомендуется. Используйте его после четкого понимания возможных рисков.

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

// config/config.default.js
exports.security = {
  domainWhiteList:['.domain.com'],  // 安全白名单,以 . 开头
};

Если пользователь не настраиваетdomainWhiteListилиdomainWhiteListЕсли массив пуст, все запросы на переход будут выпущены по умолчанию, что эквивалентноctx.unsafeRedirect(url)