Egg学习记录(1)

原文链接https://eggjs.org/zh-cn/intro/quickstart.html

Application

Application 是全局应用对象,在一个应用中,只会实例化一个,在它上面我们可以挂载一些全局的方法和对象

事件

  • server: 该事件一个 worker 进程只会触发一次,在 HTTP 服务完成启动后,会将 HTTP server 通过这个事件暴露出来给开发者。
  • error: 运行时有任何的异常被 onerror 插件捕获后,都会触发 error 事件,将错误对象和关联的上下文(如果有)暴露给开发者,可以进行自定义的日志记录上报等处理。
  • request 和 response: 应用收到请求和响应请求时,分别会触发 request 和 response 事件,并将当前请求上下文暴露出来,开发者可以监听这两个事件来进行日志记录。

获取方式

Application 对象几乎可以在编写应用时的任何一个地方获取到,下面介绍几个经常用到的获取方式:
几乎所有被框架 Loader加载的文件(Controller,Service,Schedule 等),都可以 export 一个函数,这个函数会被 Loader 调用,并使用 app 作为参数:
module.exports = app => {
app.cache = new Cache();
};

Context

Context 是一个请求级别的对象,继承自 Koa.Context。在每一次收到用户请求时,框架会实例化一个 Context 对象,这个对象封装了这次用户请求的信息,并提供了许多便捷的方法来获取请求参数或者设置响应信息。框架会将所有的 Service 挂载到 Context 实例上,一些插件也会将一些其他的方法和对象挂载到它上面(egg-sequelize 会将所有的 model 挂载在 Context 上)。

获取方式

最常见的 Context 实例获取方式是在 Middleware, Controller 以及 Service 中。Controller 中的获取方式在上面的例子中已经展示过了,在 Service 中获取和 Controller 中获取的方式一样,在 Middleware 中获取 Context 实例则和 Koa 框架在中间件中获取 Context 对象的方式一致。

框架的 Middleware 同时支持 Koa v1 和 Koa v2 两种不同的中间件写法,根据不同的写法,获取 Context 实例的方式也稍有不同:

// Koa v1
function* middleware(next) {
 // this is instance of Context
 console.log(this.query);
 yield next;
}
// Koa v2
async function middleware(ctx, next) {
 // ctx is instance of Context
 console.log(ctx.query);
}

除了在请求时可以获取 Context 实例之外, 在有些非用户请求的场景下我们需要访问 service / model 等 Context 实例上的对象,我们可以通过 Application.createAnonymousContext() 方法创建一个匿名 Context 实例:

// app.js
module.exports = app => {
  app.beforeStart(async () => {
    const ctx = app.createAnonymousContext();
    // preload before app start
    await ctx.service.posts.load();
  });
}

在定时任务中的每一个 task 都接受一个 Context 实例作为参数,以便我们更方便的执行一些定时的业务逻辑:


// app/schedule/refresh.js
exports.task = async ctx => {
 await ctx.service.posts.refresh();
};

Request & Response

Request 是一个请求级别的对象,继承自 Koa.Request。封装了 Node.js 原生的 HTTP Request 对象,提供了一系列辅助方法获取 HTTP 请求常用参数。

Response 是一个请求级别的对象,继承自 Koa.Response。封装了 Node.js 原生的 HTTP Response 对象,提供了一系列辅助方法设置 HTTP 响应。

获取方式

可以在 Context 的实例上获取到当前请求的 Request(ctx.request) 和 Response(ctx.response) 实例。


// app/controller/user.js
class UserController extends Controller {
 async fetch() {
 const { app, ctx } = this;
 const id = ctx.request.query.id;
 ctx.response.body = app.cache.get(id);
 }
}
  • Koa 会在 Context 上代理一部分 Request 和 Response 上的方法和属性,参见 Koa.Context。
  • 如上面例子中的 ctx.request.query.idctx.query.id 是等价的,ctx.response.body=ctx.body= 是等价的。
  • 需要注意的是,获取 POST 的 body 应该使用 ctx.request.body,而不是 ctx.body

Controller

框架提供了一个 Controller 基类,并推荐所有的 Controller 都继承于该基类实现。这个 Controller 基类有下列属性:

  • ctx - 当前请求的 Context 实例。
  • app - 应用的 Application 实例。
  • config - 应用的配置。
  • service - 应用所有的 service。
  • logger - 为当前 controller 封装的 logger 对象。

在 Controller 文件中,可以通过两种方式来引用 Controller 基类:


// app/controller/user.js

// 从 egg 上获取(推荐)
const Controller = require('egg').Controller;
class UserController extends Controller {
 // implement
}
module.exports = UserController;

// 从 app 实例上获取
module.exports = app => {
 return class UserController extends app.Controller {
 // implement
 };
};

Middleware

Koa 的中间件和 Express 不同,Koa 选择了洋葱圈模型。

  • 中间件洋葱图:

Egg学习记录(1)_第1张图片
image

在 前面的章节中,我们介绍了 Egg 是基于 Koa 实现的,所以 Egg 的中间件形式和 Koa 的中间件形式是一样的,都是基于 洋葱圈模型。每次我们编写一个中间件,就相当于在洋葱外面包了一层。
Egg学习记录(1)_第2张图片
image

配置

一般来说中间件也会有自己的配置。在框架中,一个完整的中间件是包含了配置处理的。我们约定一个中间件是一个放置在 app/middleware 目录下的单独文件,它需要 exports 一个普通的 function,接受两个参数:

options: 中间件的配置项,框架会将 app.config[${middlewareName}] 传递进来。
app: 当前应用 Application 的实例。

在应用中使用中间件

在应用中,我们可以完全通过配置来加载自定义的中间件,并决定它们的顺序。

如果我们需要加载上面的 gzip 中间件,在 config.default.js 中加入下面的配置就完成了中间件的开启和配置:

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

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

router 中使用中间件

以上两种方式配置的中间件是全局的,会处理每一次请求。 如果你只想针对单个路由生效,可以直接在 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',
 },
};

通用配置

无论是应用层加载的中间件还是框架自带中间件,都支持几个通用的配置项:

  • enable:控制中间件是否开启。
  • match:设置只有符合某些规则的请求才会经过这个中间件。
  • ignore:设置符合某些规则的请求不经过这个中间件。

enable

如果我们的应用并不需要默认的 bodyParser 中间件来进行请求体的解析,此时我们可以通过配置 enable 为 false 来关闭它


module.exports = {
 bodyParser: {
 enable: false,
 },
};

match 和 ignore

match 和 ignore 支持的参数都一样,只是作用完全相反,match 和 ignore 不允许同时配置。

如果我们想让 gzip 只针对 /static 前缀开头的 url 请求开启,我们可以配置 match 选项


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

match 和 ignore 支持多种类型的配置方式

  1. 字符串:当参数为字符串类型时,配置的是一个 url 的路径前缀,所有以配置的字符串作为前缀的 url 都会匹配上。 当然,你也可以直接使用字符串数组。
  2. 正则:当参数为正则时,直接匹配满足正则验证的 url 的路径。
  3. 函数:当参数为一个函数时,会将请求上下文传递给这个函数,最终取函数返回的结果(true/false)来判断是否匹配。

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

Router

Router 主要用来描述请求 URL 和具体承担执行动作的 Controller 的对应关系, 框架约定了 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 目录下面实现 Controller
// app/controller/user.js
class UserController extends Controller {
  async info() {
    const { ctx } = this;
    ctx.body = {
      name: `hello ${ctx.params.id}`,
    };
  }
}

RESTful 风格的 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 路径结构,对应的 Controller 为 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。

参数获取

Query String 方式

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

参数命名方式


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

你可能感兴趣的:(Egg学习记录(1))