Чтобы изучить egg.js, достаточно прочесть эту статью!

Node.js Egg.js

введение яиц

что такое яйцо?

Egg — это серверная веб-инфраструктура node.js, созданная Alibaba, основанная на пакете koa и имеющая некоторые соглашения.

Почему его называют яйцом?

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

Какие продукты разработаны с использованием яиц?

язык птицаОн разработан с помощью egg.Схема архитектуры выглядит следующим образом:

语雀架构图

Какие компании используют яйца?

Hema, Zhuanzhuan подержанные автомобили, PingWest, Xiaomi, 58.com и т. д. (ссылка для выбора стека технологийСвязь)

Поддерживает ли яйцо Typescript?

Хотя само яйцо написано на JavaScript, приложение яйца можно написать на Typescript, просто используйте следующую команду для создания проекта (см.Связь):

$ npx egg-init --type=ts showcase

用ts写egg案例

Будет ли intellisense при написании яиц на JavaScript?

Да, если в package.json будет добавлено следующее объявление, каталог typings будет динамически генерироваться в корневом каталоге проекта, который содержит объявления типов для различных моделей (см.Связь):

"egg": {
  "declarations": true
}

Какая связь между яйцом и коа?

коа является базовой структурой яйца, а яйцо является усовершенствованием коа.

Нужен ли коа, чтобы выучить яйца?

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

Создать проект

Мы используем базовый шаблон и выбираем домашнее зеркало для создания проекта яйца:

$ npm init egg --type=simple --registry=china
# 或者 yarn create egg --type=simple --registry=china

объяснятьnpm init eggЭтот синтаксис:

Представлена ​​версия npm@6npm-init <initializer>синтаксис, эквивалентныйnpx create-<initializer>команда, покаnpxкоманда пойдет$PATHпуть иnode_modules/.binНайдите путь с именемcreate-<initializer>Исполняемый файл, если он найден, то будет выполнен, а если не найден, то будет установлен.

То есть,npm init eggпойду найду или скачаюcreate-eggисполняемый, покаcreate-eggпакетegg-initПсевдоним пакета, который эквивалентен вызовуegg-init.

После создания структура каталогов выглядит следующим образом (игнорируйте файл README и тестовый каталог):

├── app
│   ├── controller
│   │   └── home.js
│   └── router.js
├── config
│   ├── config.default.js
│   └── plugin.js
├── package.json

Это минимальный проект яйца, использующийnpm iилиyarnПосле установки зависимостей выполните команду запуска:

$ npm run dev

[master] node version v14.15.1
[master] egg version 2.29.1
[master] agent_worker#1:23135 started (842ms)
[master] egg started on http://127.0.0.1:7001 (1690ms)

Открытьhttp://127.0.0.1:7001/будет отображаться на веб-страницеhi, egg.

соглашение о каталогах

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

egg-project
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app/
|   ├── router.js # 用于配置 URL 路由规则
│   ├── controller/ # 用于存放控制器(解析用户的输入、加工处理、返回结果)
│   ├── model/ (可选) # 用于存放数据库模型
│   ├── service/ (可选) # 用于编写业务逻辑层
│   ├── middleware/ (可选) # 用于编写中间件
│   ├── schedule/ (可选) # 用于设置定时任务
│   ├── public/ (可选) # 用于放置静态资源
│   ├── view/ (可选) # 用于放置模板文件
│   └── extend/ (可选) # 用于框架的扩展
│       ├── helper.js (可选)
│       ├── request.js (可选)
│       ├── response.js (可选)
│       ├── context.js (可选)
│       ├── application.js (可选)
│       └── agent.js (可选)
├── config/
|   ├── plugin.js # 用于配置需要加载的插件
|   ├── config.{env}.js # 用于编写配置文件(env 可以是 default,prod,test,local,unittest)

Это согласовано с каркасом яйца или встроенными плагинами и является передовой практикой, подытоженной Али.Хотя каркас также предоставляет пользователям возможность настраивать структуру каталогов, по-прежнему рекомендуется использовать это решение из Али. В следующих главах функции вышеупомянутых условных каталогов и файлов будут объяснены один за другим.

Маршрутизатор

маршрут определенПуть запроса (URL)а такжеКонтроллерОтношение сопоставления между ними, то есть какой контроллер должен обрабатывать URL-адрес, к которому обращается пользователь. мы открытыapp/router.jsпосмотри:

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

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

router.verb('path-match', controllerAction)

вverbОбычно в нижнем регистре HTTP-глаголы, например:

  • HEAD - router.head
  • OPTIONS - router.options
  • GET - router.get
  • PUT - router.put
  • POST - router.post
  • PATCH - router.patch
  • DELETE - router.deleteилиrouter.del

Кроме этого, есть специальный глаголrouter.redirectУказывает на перенаправление.

а такжеcontrollerActionопределяется синтаксисом точки (·)controllerКонкретная функция в файле в каталоге, например:

controller.home.index // 映射到 controller/home.js 文件的 index 方法
controller.v1.user.create // controller/v1/user.js 文件的 create 方法

Вот несколько примеров и их пояснения:

module.exports = app => {
  const { router, controller } = app
  // 当用户访问 news 会交由 controller/news.js 的 index 方法进行处理
  router.get('/news', controller.news.index)
  // 通过冒号 `:x` 来捕获 URL 中的命名参数 x,放入 ctx.params.x
  router.get('/user/:id/:name', controller.user.info)
  // 通过自定义正则来捕获 URL 中的分组参数,放入 ctx.params 中
  router.get(/^\/package\/([\w-.]+\/[\w-.]+)$/, controller.package.detail)
}

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

// 对 posts 按照 RESTful 风格映射到控制器 controller/posts.js 中
router.resources('posts', '/posts', controller.posts)

Автоматически генерируются следующие маршруты:

HTTP-метод путь запроса название маршрута функция контроллера
GET /posts posts app.controller.posts.index
GET /posts/new new_post app.controller.posts.new
GET /posts/:id post app.controller.posts.show
GET /posts/:id/edit edit_post app.controller.posts.edit
POST /posts posts app.controller.posts.create
PATCH /posts/:id post app.controller.posts.update
DELETE /posts/:id post app.controller.posts.destroy
Вам нужно только реализовать соответствующий метод в контроллере.

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

  1. Ручной импорт, то есть запись файла маршрутизации вapp/routerкаталог, а затемapp/router.jsимпортировать эти файлы. Образец кода:

    // app/router.js
    module.exports = app => {
      require('./router/news')(app)
      require('./router/admin')(app)
    };
    
    // app/router/news.js
    module.exports = app => {
      app.router.get('/news/list', app.controller.news.list)
      app.router.get('/news/detail', app.controller.news.detail)
    };
    
    // app/router/admin.js
    module.exports = app => {
      app.router.get('/admin/user', app.controller.admin.user)
      app.router.get('/admin/log', app.controller.admin.log)
    };
    
  2. использоватьegg-router-plusПлагины автоматически импортируютсяapp/router/**/*.jsи предоставляет функцию пространства имен:

    // app/router.js
    module.exports = app => {
      const subRouter = app.router.namespace('/sub')
      subRouter.get('/test', app.controller.sub.test) // 最终路径为 /sub/test
    }
    

В дополнение к HTTP-глаголу маршрутизатор также предоставляет метод перенаправления для внутреннего перенаправления, например:

module.exports = app => {
  app.router.get('index', '/home/index', app.controller.home.index)
  app.router.redirect('/', '/home/index', 302)
}

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

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

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

мы создаем новыйmiddleware/slow.jsПромежуточное ПО медленных запросов, когда время запроса превышает указанный нами порог, печатает журнал, код такой:

module.exports = (options, app) => {
  return async function (ctx, next) {
    const startTime = Date.now()
    await next()
    const consume = Date.now() - startTime
    const { threshold = 0 } = options || {}
    if (consume > threshold) {
      console.log(`${ctx.url}请求耗时${consume}毫秒`)
    }
  }
}

затем вconfig.default.jsиспользуется в:

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

Сконфигурированное здесь промежуточное ПО включено глобально, если вы просто хотите использовать промежуточное ПО на указанном маршруте, например, только для/apiЕсли запрос URL, начинающийся с префикса, использует определенное промежуточное ПО, есть два способа:

  1. существуетconfig.default.jsУстановите атрибут совпадения или игнорирования в конфигурации:

    module.exports = {
      middleware: [ 'slow' ],
      slow: {
        threshold: 1,
        match: '/api'
      },
    };
    
  2. в файле маршрутизацииrouter.jsвведен в

    module.exports = app => {
      const { router, controller } = app
      // 在 controller 处理之前添加任意中间件
      router.get('/api/home', app.middleware.slow({ threshold: 1 }), controller.home.index)
    }
    

egg делит промежуточное ПО на промежуточное ПО, определяемое прикладным уровнем (app.config.appMiddleware) и промежуточное ПО по умолчанию (app.config.coreMiddleware), давайте распечатаем это:

module.exports = app => {
  const { router, controller } = app
  console.log(app.config.appMiddleware)
  console.log(app.config.coreMiddleware)
  router.get('/api/home', app.middleware.slow({ threshold: 1 }), controller.home.index)
}

оказаться:

// appMiddleware
[ 'slow' ] 
// coreMiddleware
[
  'meta',
  'siteFile',
  'notfound',
  'static',
  'bodyParser',
  'overrideMethod',
  'session',
  'securities',
  'i18n',
  'eggLoaderTrace'
]

Среди них coreMiddleware — промежуточное программное обеспечение, созданное для нас egg.Оно включено по умолчанию.Если вы не хотите его использовать, вы можете отключить его, настроив:

module.exports = {
  i18n: {
    enable: false
  }
}

Контроллер

Контролер несет ответственностьПроанализируйте ввод пользователя и верните соответствующий результат после обработки., простейший пример helloworld:

const { Controller } = require('egg');
class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    ctx.body = 'hi, egg';
  }
}
module.exports = HomeController;

Конечно, код в нашем реальном проекте будет не таким простым, обычно в контроллере делаются следующие вещи:

  • Получение, проверка и обработка параметров HTTP-запроса
  • Вызовите службу (Service) для обработки дела
  • Отвечать пользователю по HTTP

Реальный случай таков:

const { Controller } = require('egg');
class PostController extends Controller {
  async create() {
    const { ctx, service } = this;
    const createRule = {
      title: { type: 'string' },
      content: { type: 'string' },
    };
    // 校验和组装参数
    ctx.validate(createRule);
    const data = Object.assign(ctx.request.body, { author: ctx.session.userId });
    // 调用 Service 进行业务处理
    const res = await service.post.create(data);
    // 响应客户端数据
    ctx.body = { id: res.id };
    ctx.status = 201;
  }
}
module.exports = PostController;

Поскольку Controller — это класс, общие методы могут быть инкапсулированы путем настройки базового класса, например:

// 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) {
    this.ctx.throw(404, msg || 'not found');
  }
}
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);
  }
}

Пройти в контроллерthis.ctxВы можете получить объект контекста, который удобен для получения и установки связанных параметров, например:

  • ctx.query: параметры запроса в URL (игнорировать повторяющиеся ключи)
  • ctx.quries: параметры запроса в URL (повторяющиеся ключи помещаются в массив)
  • ctx.params: Именованные параметры на маршрутизаторе
  • ctx.request.body: содержимое тела HTTP-запроса.
  • ctx.request.files: Файловый объект, загруженный внешним интерфейсом
  • ctx.getFileStream(): получить поток загруженных файлов
  • ctx.multipart():Получатьmultipart/form-dataданные
  • ctx.cookies: читать и устанавливать куки
  • ctx.session: прочитать и установить сеанс
  • ctx.service.xxx: получить экземпляр указанного объекта службы (отложенная загрузка).
  • ctx.status: установить код состояния
  • ctx.body: установить тело ответа
  • ctx.set: установить заголовок ответа
  • ctx.redirect(url): перенаправление
  • ctx.render(template): шаблон рендеринга

this.ctxОбъект контекста является наиболее важным объектом в каркасе яйца и каркасе коа.Нам нужно выяснить функцию этого объекта, но следует отметить, что некоторые свойства не связаны напрямую сapp.ctxНа объекте, но проксируя свойства объекта запроса или ответа, мы можем использоватьObject.keys(ctx)посмотри:

[
  'request', 'response', 'app', 'req', 'res', 'onerror', 'originalUrl', 'starttime', 'matched',
  '_matchedRoute', '_matchedRouteName', 'captures', 'params', 'routerName', 'routerPath'
]

Оказание услуг

Служба — это реализация определенной бизнес-логики.Инкапсулированная служба может вызываться несколькими контроллерами, а контроллер также может вызывать несколько служб.Хотя бизнес-логика также может быть написана в контроллере, делать это не рекомендуется.Логика контроллера должны быть простыми и играть только роль «моста».

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

Обычно в Сервисе выполняются следующие действия:

  • Работа со сложной бизнес-логикой
  • Базы данных вызовов или сторонние сервисы (например, сбор информации GitHub и т. д.)

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

// app/service/user.js
const { Service } = require('egg').Service;

class UserService extends Service {
  async find(uid) {
    const user = await this.ctx.db.query('select * from user where uid = ?', uid);
    return user;
  }
}

module.exports = UserService;

Его можно вызвать прямо в контроллере:

class UserController extends Controller {
  async info() {
    const { ctx } = this;
    const userId = ctx.params.id;
    const userInfo = await ctx.service.user.find(userId);
    ctx.body = userInfo;
  }
}

Обратите внимание, что файл службы должен быть помещен вapp/serviceКаталог, поддерживает многоуровневые каталоги и может быть доступен через каскад имен каталогов при доступе:

app/service/biz/user.js => ctx.service.biz.user
app/service/sync_user.js => ctx.service.syncUser
app/service/HackerNews.js => ctx.service.hackerNews

Функции в Сервисе можно понимать как наименьшую единицу конкретной бизнес-логики.В Сервисе также можно вызывать другие сервисы.Стоит отметить, что Сервис не является синглтоном, этоуровень запросаобъект, к которому фреймворк обращается впервые в каждом запросеctx.service.xxЗадержка создания экземпляра, чтобы служба могла получить контекст текущего запроса через this.ctx.

Рендеринг шаблона

Каркас яйца имеет встроенныйegg-viewВ качестве шаблонного решения он поддерживает различные шаблоны рендеринга, такие как ejs, handlebars, nunjunks и другие механизмы шаблонов.Каждый механизм шаблонов представлен как плагин.По умолчанию все плагины найдутapp/viewфайлы в каталоге, а затем в соответствии сconfig\config.default.jsсопоставление суффиксов, определенное в для выбора другого механизма шаблонов:

config.view = {
  defaultExtension: '.nj',
  defaultViewEngine: 'nunjucks',
  mapping: {
    '.nj': 'nunjucks',
    '.hbs': 'handlebars',
    '.ejs': 'ejs',
  },
}

Приведенная выше конфигурация указывает, что когда файл:

  • суффикс.njПри использовании механизма шаблонов nunjunks
  • суффикс.hbsПри использовании шаблонизатора руля
  • суффикс.ejsПри использовании механизма шаблонов ejs
  • По умолчанию, когда суффикс не указан.html
  • По умолчанию используется nunjunks, если не указан механизм шаблонов.

Далее мы устанавливаем плагин шаблонизатора:

$ npm i egg-view-nunjucks egg-view-ejs egg-view-handlebars --save
# 或者 yarn add egg-view-nunjucks egg-view-ejs egg-view-handlebars

затем вconfig/plugin.jsВключите плагин в:

exports.nunjucks = {
  enable: true,
  package: 'egg-view-nunjucks',
}
exports.handlebars = {
  enable: true,
  package: 'egg-view-handlebars',
}
exports.ejs = {
  enable: true,
  package: 'egg-view-ejs',
}

затем добавьтеapp/viewкаталог, добавьте в него несколько файлов:

app/view
├── ejs.ejs
├── handlebars.hbs
└── nunjunks.nj

Коды:

<!-- ejs.ejs 文件代码 -->
<h1>ejs</h1>
<ul>
  <% items.forEach(function(item){ %>
    <li><%= item.title %></li>
  <% }); %>
</ul>
      
<!-- handlebars.hbs 文件代码 -->
<h1>handlebars</h1>
{{#each items}}
  <li>{{title}}</li>
{{~/each}}
    
<!-- nunjunks.nj 文件代码 -->
<h1>nunjunks</h1>
<ul>
{% for item in items %}
  <li>{{ item.title }}</li>
{% endfor %}
</ul>

Затем настройте маршрутизацию в Router:

module.exports = app => {
  const { router, controller } = app
  router.get('/ejs', controller.home.ejs)
  router.get('/handlebars', controller.home.handlebars)
  router.get('/nunjunks', controller.home.nunjunks)
}

Далее реализуем логику контроллера:

const Controller = require('egg').Controller

class HomeController extends Controller {
  async ejs() {
    const { ctx } = this
    const items = await ctx.service.view.getItems()
    await ctx.render('ejs.ejs', {items})
  }

  async handlebars() {
    const { ctx } = this
    const items = await ctx.service.view.getItems()
    await ctx.render('handlebars.hbs', {items})
  }

  async nunjunks() {
    const { ctx } = this
    const items = await ctx.service.view.getItems()
    await ctx.render('nunjunks.nj', {items})
  }
}

module.exports = HomeController

Заносим данные в Сервис:

const { Service } = require('egg')

class ViewService extends Service {
  getItems() {
    return [
      { title: 'foo', id: 1 },
      { title: 'bar', id: 2 },
    ]
  }
}

module.exports = ViewService

Посетите следующий адрес, чтобы увидеть результаты, обработанные различными механизмами шаблонов:

GET http://localhost:7001/nunjunks
GET http://localhost:7001/handlebars
GET http://localhost:7001/ejs

Вы спросите, а откуда взялся метод ctx.render? Правильно, это обеспечивается расширением контекста egg-view, которое добавляет к объекту контекста ctxrender,renderViewа такжеrenderStringТри метода, код выглядит следующим образом:

const ContextView = require('../../lib/context_view')
const VIEW = Symbol('Context#view')

module.exports = {
  render(...args) {
    return this.renderView(...args).then(body => {
      this.body = body;
    })
  },

  renderView(...args) {
    return this.view.render(...args);
  },

  renderString(...args) {
    return this.view.renderString(...args);
  },

  get view() {
    if (this[VIEW]) return this[VIEW]
    return this[VIEW] = new ContextView(this)
  }
}

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

плагин

Когда мы объясняли рендеринг шаблонов на прошлом уроке, мы уже знали, как использовать плагины, то есть нужно только быть в приложении или фреймворке.config/plugin.jsЗаявление в:

exports.myPlugin = {
  enable: true, // 是否开启
  package: 'egg-myPlugin', // 从 node_modules 中引入
  path: path.join(__dirname, '../lib/plugin/egg-mysql'), // 从本地目录中引入
  env: ['local', 'unittest', 'prod'], // 只有在指定运行环境才能开启
}

После включения плагина вы можете использовать функции, предоставляемые плагином:

app.myPlugin.xxx()

Если плагин содержит конфигурацию, которую должен настроить пользователь, ее можноconfig.default.jsУкажите, например:

exports.myPlugin = {
  hello: 'world'
}

Плагин на самом деле представляет собой «мини-приложение», которое содержитService,промежуточное ПО,настроить,удлинение рамыд., но не независимыйRouterа такжеController, и вы не можете определить свой собственныйplugin.js.

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

Интегрировать MongoDB

Сначала убедитесь, что база данных MongoDB установлена ​​и запущена на компьютере.Если это компьютер Mac, вы можете быстро установить и запустить его с помощью следующих команд:

$ brew install mongodb-community
$ brew services start mongodb/brew/mongodb-community # 后台启动
# 或者使用 mongod --config /usr/local/etc/mongod.conf 前台启动

затем установитеegg-mongooseПлагин:

$ npm i egg-mongoose
# 或者 yarn add egg-mongoose

существуетconfig/plugin.jsОткройте плагин в:

exports.mongoose = {
  enable: true,
  package: 'egg-mongoose',
}

существуетconfig/config.default.jsОпределите параметры подключения в:

config.mongoose = {
  client: {
    url: 'mongodb://127.0.0.1/example',
    options: {}
  }
}

затем вmodel/user.jsОпределите модель в:

module.exports = app => {
  const mongoose = app.mongoose
  const UserSchema = new mongoose.Schema(
    {
      username: {type: String, required: true, unique: true}, // 用户名
      password: {type: String, required: true}, // 密码
    },
    { timestamps: true } // 自动生成 createdAt 和 updatedAt 时间戳
  )
  return mongoose.model('user', UserSchema)
}

Вызовите метод мангуста в контроллере:

const {Controller} = require('egg')

class UserController extends Controller {
  // 用户列表 GET /users
  async index() {
    const {ctx} = this
    ctx.body = await ctx.model.User.find({})
  }

  // 用户详情 GET /users/:id
  async show() {
    const {ctx} = this
    ctx.body = await ctx.model.User.findById(ctx.params.id)
  }

  // 创建用户 POST /users
  async create() {
    const {ctx} = this
    ctx.body = await ctx.model.User.create(ctx.request.body)
  }

  // 更新用户 PUT /users/:id
  async update() {
    const {ctx} = this
    ctx.body = await ctx.model.User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
  }

  // 删除用户 DELETE /users/:id
  async destroy() {
    const {ctx} = this
    ctx.body = await ctx.model.User.findByIdAndRemove(ctx.params.id)
  }
}

module.exports = UserController

Наконец, настройте карту маршрутов RESTful:

module.exports = app => {
  const {router, controller} = app
  router.resources('users', '/users', controller.user)
}

Интегрировать MySQL

Сначала убедитесь, что база данных MySQL установлена ​​на вашем компьютере.Если это компьютер Mac, вы можете быстро установить и запустить его с помощью следующих команд:

$ brew install mysql
$ brew services start mysql # 后台启动
# 或者 mysql.server start 前台启动
$ mysql_secure_installation # 设置密码

есть официальныйegg-mysqlПлагин, вы можете подключиться к базе данных MySQL, использование очень простое:

$ npm i egg-mysql
# 或者 yarn add egg-mysql

существуетconfig/plugin.js

exports.mysql = {
  enable: true,
  package: 'egg-mysql',
}

config/config.default.js

config.mysql = {
  client: {
    host: 'localhost',
    port: '3306',
    user: 'root',
    password: 'root',
    database: 'cms',
  }
}

app.mysql

class UserService extends Service {
  async find(uid) {
    const user = await this.app.mysql.get('users', { id: 11 });
    return { user }
  }
}

ERROR 5954 nodejs.ER_NOT_SUPPORTED_AUTH_MODEError: ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server; consider upgrading MySQL client

ali-rdsmysql

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'
flush privileges

sequelizesequelize

npm install egg-sequelize mysql2 --save 
yarn add egg-sequelize mysql2

config/plugin.js

exports.sequelize = {
  enable: true,
  package: 'egg-sequelize',
}

config/config.default.js

config.sequelize = {
  dialect: 'mysql',
  host: '127.0.0.1',
  port: 3306,
  database: 'example',
}

CREATE TABLE `books` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key',
  `name` varchar(30) DEFAULT NULL COMMENT 'book name',
  `created_at` datetime DEFAULT NULL COMMENT 'created time',
  `updated_at` datetime DEFAULT NULL COMMENT 'updated time',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='book';

model/book.js

module.exports = app => {
  const { STRING, INTEGER } = app.Sequelize
  const Book = app.model.define('book', {
    id: { type: INTEGER, primaryKey: true, autoIncrement: true },
    name: STRING(30),
  })
  return Book
}

controller/book.js

const Controller = require('egg').Controller

class BookController extends Controller {
  async index() {
    const ctx = this.ctx
    ctx.body = await ctx.model.Book.findAll({})
  }

  async show() {
    const ctx = this.ctx
    ctx.body = await ctx.model.Book.findByPk(+ctx.params.id)
  }

  async create() {
    const ctx = this.ctx
    ctx.body = await ctx.model.Book.create(ctx.request.body)
  }

  async update() {
    const ctx = this.ctx
    const book = await ctx.model.Book.findByPk(+ctx.params.id)
    if (!book) return (ctx.status = 404)
    await book.update(ctx.request.body)
    ctx.body = book
  }

  async destroy() {
    const ctx = this.ctx
    const book = await ctx.model.Book.findByPk(+ctx.params.id)
    if (!book) return (ctx.status = 404)
    await book.destroy()
    ctx.body = book
  }
}

module.exports = BookController

module.exports = app => {
  const {router, controller} = app
  router.resources('books', '/books', controller.book)
}

npm init egg --type=plugin
# 或者 yarn create egg --type=plugin

├── config
│   └── config.default.js
├── package.json

package.jsoneggPlugin

{
  "eggPlugin": {
    "name": "myPlugin",
    "dependencies": [ "registry" ],
    "optionalDependencies": [ "vip" ],
    "env": [ "local", "test", "unittest", "prod" ]
  }
}

  • name
  • dependencies
  • optionalDependencies
  • env

  • app/extend/request.jsresponse.js

    egg-bcryptextend.js

    egg-bcrypt

    ctx.genHash(plainText)ctx.compare(plainText, hash)

  • app/middlewareapp.js

    egg-corscors.jskoa-cors

    egg-cors

    config/config.default.js

    exports.cors = {
      origin: '*',
      allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'
    }
    
  • app.js

    egg-elasticsearch

    egg-elasticsearch

    beforeStartbeforeStart

  • app/schedule/

app/scheduleupdate_cache.js

const Subscription = require('egg').Subscription

class UpdateCache extends Subscription {
  // 通过 schedule 属性来设置定时任务的执行间隔等配置
  static get schedule() {
    return {
      interval: '1m', // 1 分钟间隔
      type: 'all', // 指定所有的 worker 都需要执行
    }
  }

  // subscribe 是真正定时任务执行时被运行的函数
  async subscribe() {
    const res = await this.ctx.curl('http://www.api.com/cache', {
      dataType: 'json',
    })
    this.ctx.app.cache = res.data
  }
}

module.exports = UpdateCache

  • 5000ms5s

  • cron-parser

    *    *    *    *    *    *
    ┬    ┬    ┬    ┬    ┬    ┬
    │    │    │    │    │    |
    │    │    │    │    │    └ day of week (0 - 7) (0 or 7 is Sun)
    │    │    │    │    └───── month (1 - 12)
    │    │    │    └────────── day of month (1 - 31)
    │    │    └─────────────── hour (0 - 23)
    │    └──────────────────── minute (0 - 59)
    └───────────────────────── second (0 - 59, optional)
    

  • worker
  • all

app.runSchedule(schedulePath)app.runScheduleapp/scheduleapp.js

module.exports = app => {
  app.beforeStart(async () => {
    // 程序启动前确保缓存已更新
    await app.runSchedule('update_cache')
  })
}

model.User

egg-error

Internal Server Error, real status: 500

config/plugin.js

module.exports = {
  onerror: {
    accepts: () => 'json',
  },
};

{
    "message": "Cannot read property 'find' of undefined",
    "stack": "TypeError: Cannot read property 'find' of undefined\n    at UserController.index (/Users/keliq/code/egg-project/app/controller/user.js:7:37)",
    "name": "TypeError",
    "status": 500
}

content-type

module.exports = {
  onerror: {
      accepts: (ctx) => {
        if (ctx.get('content-type') === 'application/json') return 'json';
        return 'html';
      }
  },
};

config/config.default.js

module.exports = {
  onerror: {
    errorPageUrl: '/public/error.html',
  },
};

?real_status=500egg-onerror

module.exports = {
  onerror: {
    all(err, ctx) {
      // 在此处定义针对所有响应类型的错误处理方法
      // 注意,定义了 config.all 之后,其他错误处理方法不会再生效
      ctx.body = 'error'
      ctx.status = 500
    },
    html(err, ctx) { // 处理 html hander
      ctx.body = '<h3>error</h3>'
      ctx.status = 500
    },
    json(err, ctx) { // json hander
      ctx.body = {message: 'error'}
      ctx.status = 500
    },
  },
}

  • { "message": "Not Found" }

  • <h1>404 Not Found</h1>

config/config.default.js

module.exports = {
  notfound: {
    pageUrl: '/404.html',
  }
}

middleware/notfound_handler.js

module.exports = () => {
  return async function (ctx, next) {
    await next()
    if (ctx.status === 404 && !ctx.body) {
      ctx.body = ctx.acceptJSON ? { error: 'Not Found' } : '<h1>Page Not Found</h1>'
    }
  }
}

config/config.default.js

config.middleware = ['notfoundHandler']

  • configWillLoad
  • configDidLoad
  • didLoad
  • willReady
  • didReady
  • serverDidReady
  • beforeClose

app.js

class AppBootHook {
  constructor(app) {
    this.app = app
  }

  configWillLoad() {
    // config 文件已经被读取并合并,但是还并未生效,这是应用层修改配置的最后时机
    // 注意:此函数只支持同步调用
  }

  configDidLoad() {
    // 所有的配置已经加载完毕,可以用来加载应用自定义的文件,启动自定义的服务
  }

  async didLoad() {
    // 所有的配置已经加载完毕,可以用来加载应用自定义的文件,启动自定义的服务
  }

  async willReady() {
    // 所有的插件都已启动完毕,但是应用整体还未 ready
    // 可以做一些数据初始化等操作,这些操作成功才会启动应用
  }

  async didReady() {
    // 应用已经启动完毕
  }

  async serverDidReady() {
    // http / https server 已启动,开始接受外部请求
    // 此时可以从 app.server 拿到 server 的实例
  }

  async beforeClose() {
    // 应用即将关闭
  }
}

module.exports = AppBootHook

egg-lifecycle

const BAR = Symbol('bar') 

module.exports = {
  foo(param) {}, // 扩展方法
  get bar() { // 扩展属性
    if (!this[BAR]) {
      this[BAR] = this.get('x-bar')
    }
    return this[BAR]
  },
}

this

  • app/extend/application.jsappctx.app.xxx
  • app/extend/context.js
  • app/extend/<request|response>.jsrequestresponserequestresponse
  • app/extend/helper.jshelperhelper

npm init egg --type=framework --registry=china
# 或者 yarn create egg --type=framework --registry=china

├── app
│   ├── extend
│   │   ├── application.js
│   │   └── context.js
│   └── service
│       └── test.js
├── config
│   ├── config.default.js
│   └── plugin.js
├── index.js
├── lib
│   └── framework.js
├── package.json

lib/framework.js

const path = require('path')
const egg = require('egg')
const EGG_PATH = Symbol.for('egg#eggPath')

class Application extends egg.Application {
  get [EGG_PATH]() {
    return path.dirname(__dirname)
  }
}

class Agent extends egg.Agent {
  get [EGG_PATH]() {
    return path.dirname(__dirname)
  }
}

module.exports = Object.assign(egg, {
  Application,
  Agent,
})

Symbol.for('egg#eggPath')path.dirname(__dirname)

npm link # 或者 yarn link

npm link my-framework

"egg": {
  "framework": "my-framework"
},

const Application = require('egg').Application
// 继承 egg 的 Application
class Enterprise extends Application {
  get [EGG_PATH]() {
    return '/path/to/enterprise'
  }
}

const Application = require('enterprise').Application
// 继承 enterprise 的 Application
class Department extends Application {
  get [EGG_PATH]() {
    return '/path/to/department'
  }
}

package.json
config/plugin.{env}.js
config/config.{env}.js
app/extend/application.js
app/extend/request.js
app/extend/response.js
app/extend/context.js
app/extend/helper.js
agent.js
app.js
app/service
app/middleware
app/controller
app/router.js

Symbol.for('egg#eggPath')Symbol.for('egg#loader')

const path = require('path')
const egg = require('egg')
const EGG_PATH = Symbol.for('egg#eggPath')
const EGG_LOADER = Symbol.for('egg#loader')

class MyAppWorkerLoader extends egg.AppWorkerLoader {
  // 自定义的 AppWorkerLoader
}

class Application extends egg.Application {
  get [EGG_PATH]() {
    return path.dirname(__dirname)
  }

  get [EGG_LOADER]() {
    return MyAppWorkerLoader
  }
}

egg-core

  • loadPlugin()
  • loadConfig()
  • loadAgentExtend()
  • loadApplicationExtend()
  • loadRequestExtend()
  • loadResponseExtend()
  • loadContextExtend()
  • loadHelperExtend()
  • loadCustomAgent()
  • loadCustomApp()
  • loadService()
  • loadMiddleware()
  • loadController()
  • loadRouter()

const {AppWorkerLoader} = require('egg')
const {EggLoader} = require('egg-core')

// 如果需要改变加载顺序,则需要继承 EggLoader,否则可以继承 AppWorkerLoader
class MyAppWorkerLoader extends AppWorkerLoader {
  constructor(options) {
    super(options)
  }

  load() {
    super.load()
    console.log('自定义load逻辑')
  }

  loadPlugin() {
    super.loadPlugin()
    console.log('自定义plugin加载逻辑')
  }

  loadConfig() {
    super.loadConfig()
    console.log('自定义config加载逻辑')
  }

  loadAgentExtend() {
    super.loadAgentExtend()
    console.log('自定义agent extend加载逻辑')
  }

  loadApplicationExtend() {
    super.loadApplicationExtend()
    console.log('自定义application extend加载逻辑')
  }

  loadRequestExtend() {
    super.loadRequestExtend()
    console.log('自定义request extend加载逻辑')
  }

  loadResponseExtend() {
    super.loadResponseExtend()
    console.log('自定义response extend加载逻辑')
  }

  loadContextExtend() {
    super.loadContextExtend()
    console.log('自定义context extend加载逻辑')
  }

  loadHelperExtend() {
    super.loadHelperExtend()
    console.log('自定义helper extend加载逻辑')
  }

  loadCustomAgent() {
    super.loadCustomAgent()
    console.log('自定义custom agent加载逻辑')
  }

  loadCustomApp() {
    super.loadCustomApp()
    console.log('自定义custom app加载逻辑')
  }

  loadService() {
    super.loadService()
    console.log('自定义service加载逻辑')
  }

  loadMiddleware() {
    super.loadMiddleware()
    console.log('自定义middleware加载逻辑')
  }

  loadController() {
    super.loadController()
    console.log('自定义controller加载逻辑')
  }

  loadRouter() {
    super.loadRouter()
    console.log('自定义router加载逻辑')
  }
}

自定义plugin加载逻辑
自定义config加载逻辑
自定义application extend加载逻辑
自定义request extend加载逻辑
自定义response extend加载逻辑
自定义context extend加载逻辑
自定义helper extend加载逻辑
自定义custom app加载逻辑
自定义service加载逻辑
自定义middleware加载逻辑
自定义controller加载逻辑
自定义router加载逻辑
自定义load逻辑