使用 Egg.js 开发 Node.js 项目

编辑于2018年10月11日

Egg.js 是由阿里开源的 Node.js 框架,为企业级框架和应用而生。Egg 选择了 Koa 作为其基础框架,在它的模型基础上,进一步对它进行了一些增强。Egg 是一个强约束框架,奉行『约定优于配置』,有效降低了团队协作成本,这也是它与 Koa 和 Express 最大的不同。

提供基于 Egg 定制上层框架的能力
高度可扩展的插件机制
内置多进程管理
基于 Koa 开发,性能优异
框架稳定,测试覆盖率高
渐进式开发

接下来,就介绍一下如何使用 Egg.js 进行项目开发,关于 Egg 更多、更详细的介绍见 Egg 官网,本文就不再赘述了。

初始化构建

Egg 最低要求 Node.js 版本为 8.x,推荐使用 LTS 版本

官方给我们提供了脚手架工具,我们只需要使用几条简单的指令,即可快速生成项目。当然,也可以自己一步步的构建,但是要记住,Egg 对目录结构作出了规定。

这里我们直接使用脚手架快速构建:

$ npm i egg-init -g  # 全局安装脚手架
$ egg-init <project-name> --type=simple  # 使用初始化项目
$ cd <project-name>  # 进入项目目录
$ npm i  # 安装依赖

其中执行 egg-init 命令可以通过 type 参数指定使用何种模版,egg 提供了一下模版:

  • simple - 简易的 Egg 应用 。
  • ts - 简易的 Egg 应用,提供 typescript 支持。
  • empty - 空的 Egg 应用。
  • plugin - Egg 插件。
  • framework - Egg 框架。

使用脚手架构建完成的项目结构如下图所示。

egg_project

关于项目的目录结构,官网上给出了更详细的说明。

开发调试

在介绍如何进行开发调试之前,我们先来了解一下运行环境

运行环境

一个 Web 应用本身应该是无状态的,并拥有根据运行环境设置自身的能力。

通常在 Node.js 应用中我们会使用 NODE_ENV 来区分运行环境,而在 Egg 中使用 EGG_SERVER_ENV 设置运行环境。框架默认支持的运行环境及映射关系(如果未指定 EGG_SERVER_ENV 会根据 NODE_ENV 来匹配)如下:

NODE_ENV EGG_SERVER_ENV 说明
local 本地开发环境
test unittest 单元测试
production prod 生产环境

NODE_ENVproductionEGG_SERVER_ENV 未指定时,框架会将 EGG_SERVER_ENV 设置成 prod。我们可以使用 app.config.env 来读取当前运行环境。

在不同的运行环境下,框架会加载不同的配置文件,根据配置文件后缀来匹配运行环境。如将 EGG_SERVER_ENV 设置成 sit(并建议设置 NODE_ENV = production),启动时会加载 config/config.sit.js

本地开发

使用 egg-bin 模块,可以很方便的进行开发、调试、单元测试等,如果使用脚手架初始化项目,则已经集成了相应模块,可以直接使用 npm 脚本启动: npm run dev

本地启动的应用是以 env: local 启动的,读取的配置也是 config.default.jsconfig.local.js 合并的结果。本地启动应用默认监听 7001 端口,可指定其他端口,例如:

{
  "scripts": {
    "dev": "egg-bin dev --port 7001"
  }
}

调试

如何调试在官网上也有详细的介绍,我这里使用 Webstorm 进行调试。我们只需要在 Webstrom 中添加 debug 配置即可。配置好以后,点击 debug 按钮启动项目,在相应的代码处打上端点就可以进行调试了。

定义路由

Egg 的路由统一定义在 app/router.js 中,格式如下:

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

控制器全部在 app/controller 目录下面实现,通过 app.${filename} 访问 controller。Controller 支持子目录,在定义路由的时候,可以通过 app.${directoryName}.${fileName}.${functionName} 的方式访问对应的 Controller。

这里比较简单,添加匹配规则,指定处理请求的 Controller 即可,详情见文档,示例代码可以参考颜多多

编写 Controller

所有的 Controller 文件都必须放在 app/controller 目录下,可以支持多级目录,访问的时候可以通过目录名级联访问。Controller 支持多种形式进行编写,可以根据不同的项目场景和开发习惯来选择,我这里使用 Controller 类的方式进行编写。

定义的 Controller 类,会在每一个请求访问到 server 时实例化一个全新的对象,而项目中的 Controller 类继承于 egg.Controller,会有下面几个属性挂在 this上。

  • this.ctx: 当前请求的上下文 Context 对象的实例,通过它我们可以拿到框架封装好的处理当前请求的各种便捷属性和方法。
  • this.app: 当前应用 Application 对象的实例,通过它我们可以拿到框架提供的全局对象和方法。
  • this.service:应用定义的 Service,通过它我们可以访问到抽象出的业务层,等价于 this.ctx.service
  • this.config:应用运行时的配置项。
  • this.logger:logger 对象,上面有四个方法(debuginfowarnerror),分别代表打印四个不同级别的日志,使用方法和效果与 context logger 中介绍的一样,但是通过这个 logger 对象记录的日志,在日志前面会加上打印该日志的文件路径,以便快速定位日志打印位置。

按照类的方式编写 Controller,我们可以自定义基类来封装应用中常用的方法,如:

'use strict';

const Controller = require('egg').Controller;

class Base_controller extends Controller {
  /**
   * 请求成功
   *
   * @param {string} [message] - 提示文字
   * @param {object} [data] - 数据
   */
  success(message, data) {
    this.ctx.body = {
      status: 'success',
      message: message || '成功',
      data,
    };
  }
}

module.exports = Base_controller;

此时在编写应用的 Controller 时,可以继承 BaseController,直接使用基类上的方法。

处理请求

请求处理过程,无非就是读取请求参数,根据参数返回相应的结果。在 Controller 中我们可以通过 ctx.query 读取 query ,ctx.request.body 读取 body 参数,通过 ctx.get() 读取请求头,具体见文档

读取到请求参数后,我们可以之后在 Controller 中处理简单的业务逻辑,并返回响应的结果。通过 ctx.status 可以设置响应状态吗,ctx.body 设置响应主体。

在获取到用户请求的参数后,不可避免的要对参数进行一些校验。借助 Validate 插件提供便捷的参数校验机制,帮助我们完成各种复杂的参数校验,插件如何使用见下文。

编写 Service

当在请求处理过程中,需要处理复杂的业务逻辑,比如要展现的信息需要从数据库获取,还要经过一定的规则计算,才能返回用户显示。或者,需要进行第三方服务的调用,比如 GitHub 信息获取等。这个时候,如果我们将代码都放在 Controller 中就不太适合了,Egg 提供了 Service 层用来处理这种场景。

Service 就是在复杂业务场景下用于做业务逻辑封装的一个抽象层,保持 Controller 中的逻辑更加简洁,保持业务逻辑的独立性,抽象出来的 Service 可以被多个 Controller 重复调用,便于进行编写测试用例。

Service 都定义在 app/services 目录下,继承于 egg.Controller

// 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;

调用时使用 ctx.service.${fileName} 进行调用。

中间件

Egg 是基于 Koa 实现的,所以 Egg 的中间件形式和 Koa 的中间件形式是一样的,都是基于洋葱圈模型。每次我们编写一个中间件,就相当于在洋葱外面包了一层。

中间件都定义在 app/middleware 目录下,写法和 Koa 中间件一摸一样。中间件编写完成后,我们还需要手动挂载,常用的使用方式如下:

  • 在应用中使用中间件:应用级中间件直接在配置文件中配置,会处理每一次请求。
  • 路由级中间件:直接在 app/router.js 中实例化和挂载,针对某一个路由。

除此上述之外,我们还可以使用 Koa 中间件、Egg 内置中间件等,具体件文档

插件的使用

在 Egg 中,一个插件其实就是一个『迷你的应用』,和应用(app)几乎一样:

  • 它包含了 Service、中间件、配置、框架扩展等等。
  • 它没有独立的 Router 和 Controller。

具体介绍

插件的使用非常简单,我们只需要安装对应的插件(如果没有则需要自己开发),然后添加必要的配置即可,这里以 Sequelize 为例进行详细介绍。

  1. 安装插件
$ npm i egg-sequelize
$ npm i mysql2

安装 sequelize 插件及对应的数据库驱动。

  1. config/plugin.js 声明插件。
// config/plugin.js
// 使用 sequelize 插件
exports.sequelize = {
  enable: true,
  package: 'egg-sequelize'
}
  1. 添加插件配置
// config/plugin.js
exports.sequelize = {
  dialect: 'mysql', // support: mysql, mariadb, postgres, mssql
  database: 'test',
  host: 'localhost',
  port: '3306',
  username: 'root',
  password: '',
};

更多配置见 Sequelize 文档

配置好之后,我们就可以使用该插件了,每个插件的用法都略有不同,但是安装配置上基本都是一样的,具体见各插件的说明文档。

egg-sequelize 插件要求所有的 Model 都定义在 app/model 目录下。我们可以通过 app.model 访问到 Sequelize 实例,然后通过该实例定义模型、同步数据表等操作。

模型定义好之后,我们可以通过 ctx.model.<name> 访问到对应的模型。具体示例,见 egg-sequelize 说明文档

总结

除了上述介绍的内容之外,Egg 还提供了丰富的功能,帮助我们快速构建应用,大家可以在官网上找到详细的说明,慢慢研究。

颜多多是一款颜值测试、颜值社交类 APP,由我和小伙伴一同开发,其服务端就是使用 Egg.js 构建,截至我写这篇博客,项目还没有完成,仍在开发过程中。代码会开源到 Github,仅供学习交流,欢迎 start🌟。