From d73046bb9165c74473b6f28842de23c880e78a87 Mon Sep 17 00:00:00 2001 From: Otto Mao Date: Wed, 24 Jan 2024 13:59:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E8=A1=A8=E8=BE=BE=20(#5290)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: xiaofeng.mxf --- site/docs/basics/app-start.zh-CN.md | 53 ++- site/docs/basics/config.zh-CN.md | 65 ++-- site/docs/basics/controller.zh-CN.md | 526 +++++++++++++-------------- site/docs/basics/env.zh-CN.md | 31 +- site/docs/basics/extend.zh-CN.md | 76 ++-- site/docs/basics/middleware.zh-CN.md | 66 ++-- site/docs/basics/objects.zh-CN.md | 146 ++++---- site/docs/basics/plugin.zh-CN.md | 63 ++-- site/docs/basics/router.zh-CN.md | 94 +++-- site/docs/basics/schedule.zh-CN.md | 66 ++-- site/docs/basics/service.zh-CN.md | 75 ++-- site/docs/basics/structure.zh-CN.md | 71 ++-- 12 files changed, 641 insertions(+), 691 deletions(-) diff --git a/site/docs/basics/app-start.zh-CN.md b/site/docs/basics/app-start.zh-CN.md index aef0fe6512..34123fb508 100644 --- a/site/docs/basics/app-start.zh-CN.md +++ b/site/docs/basics/app-start.zh-CN.md @@ -3,21 +3,20 @@ title: 启动自定义 order: 12 --- -我们常常需要在应用启动期间进行一些初始化工作,等初始化完成后应用才可以启动成功,并开始对外提供服务。 +我们常常需要在应用启动期间进行一些初始化工作,待初始化完成后,应用才可以启动成功,并开始对外提供服务。 -框架提供了统一的入口文件(`app.js`)进行启动过程自定义,这个文件返回一个 Boot 类,我们可以通过定义 Boot 类中的生命周期方法来执行启动应用过程中的初始化工作。 +框架提供了统一的入口文件(`app.js`)进行启动过程自定义。这个文件需要返回一个 Boot 类。我们可以通过定义 Boot 类中的生命周期方法来执行启动应用过程中的初始化工作。 -框架提供了这些 [生命周期函数](../advanced/loader.md#life-cycles)供开发人员处理: +框架提供了以下 [生命周期函数](../advanced/loader.md#life-cycles) 供开发人员处理: +- 配置文件即将加载,这是最后动态修改配置的时机(`configWillLoad`); +- 配置文件加载完成(`configDidLoad`); +- 文件加载完成(`didLoad`); +- 插件启动完毕(`willReady`); +- worker 准备就绪(`didReady`); +- 应用启动完成(`serverDidReady`); +- 应用即将关闭(`beforeClose`)。 -- 配置文件即将加载,这是最后动态修改配置的时机(`configWillLoad`) -- 配置文件加载完成(`configDidLoad`) -- 文件加载完成(`didLoad`) -- 插件启动完毕(`willReady`) -- worker 准备就绪(`didReady`) -- 应用启动完成(`serverDidReady`) -- 应用即将关闭(`beforeClose`) - -我们可以在 `app.js` 中定义这个 Boot 类,下面我们抽取几个在应用开发中常用的生命周期函数来举例: +我们可以在 `app.js` 中定义这个 Boot 类。下面我们抽取几个在应用开发中常用的生命周期函数为例: ```js // app.js @@ -27,8 +26,8 @@ class AppBootHook { } configWillLoad() { - // 此时 config 文件已经被读取并合并,但是还并未生效 - // 这是应用层修改配置的最后时机 + // 此时 config 文件已经被读取并合并,但还并未生效 + // 这是应用层修改配置的最后机会 // 注意:此函数只支持同步调用 // 例如:参数中的密码是加密的,在此处进行解密 @@ -39,40 +38,40 @@ class AppBootHook { } async didLoad() { - // 所有的配置已经加载完毕 - // 可以用来加载应用自定义的文件,启动自定义的服务 + // 所有配置已经加载完毕 + // 可以用来加载应用自定义的文件,启动自定义服务 - // 例如:创建自定义应用的示例 + // 例如:创建自定义应用的实例 this.app.queue = new Queue(this.app.config.queue); await this.app.queue.init(); - // 例如:加载自定义的目录 + // 例如:加载自定义目录 this.app.loader.loadToContext(path.join(__dirname, 'app/tasks'), 'tasks', { fieldClass: 'tasksClasses', }); } async willReady() { - // 所有的插件都已启动完毕,但是应用整体还未 ready - // 可以做一些数据初始化等操作,这些操作成功才会启动应用 + // 所有插件已启动完毕,但应用整体尚未 ready + // 可进行数据初始化等操作,这些操作成功后才启动应用 // 例如:从数据库加载数据到内存缓存 this.app.cacheData = await this.app.model.query(QUERY_CACHE_SQL); } async didReady() { - // 应用已经启动完毕 + // 应用已启动完毕 const ctx = await this.app.createAnonymousContext(); await ctx.service.Biz.request(); } async serverDidReady() { - // http / https server 已启动,开始接受外部请求 - // 此时可以从 app.server 拿到 server 的实例 + // http/https 服务器已启动,开始接收外部请求 + // 此时可以从 app.server 获取 server 实例 - this.app.server.on('timeout', (socket) => { - // handle socket timeout + this.app.server.on('timeout', socket => { + // 处理 socket 超时 }); } } @@ -80,6 +79,6 @@ class AppBootHook { module.exports = AppBootHook; ``` -**注意:在自定义生命周期函数中不建议做太耗时的操作,框架会有启动的超时检测。** +**注意:在自定义生命周期函数中,不建议进行耗时的操作,因为框架会有启动的超时检测。** -如果你的 Egg 框架的生命周期函数是旧版本的,建议你升级到类方法模式;详情请查看[升级你的生命周期事件函数](../advanced/loader-update.md)。 +如果你的 Egg 框架的生命周期函数是旧版本的,建议你将其升级到类方法模式;详情请查看[升级你的生命周期事件函数](../advanced/loader-update.md)。 \ No newline at end of file diff --git a/site/docs/basics/config.zh-CN.md b/site/docs/basics/config.zh-CN.md index 4270d0a633..300911e9b3 100644 --- a/site/docs/basics/config.zh-CN.md +++ b/site/docs/basics/config.zh-CN.md @@ -5,17 +5,16 @@ order: 4 框架提供了强大且可扩展的配置功能,可以自动合并应用、插件、框架的配置,按顺序覆盖,且可以根据环境维护不同的配置。合并后的配置可直接从 `app.config` 获取。 -配置的管理有多种方案,以下列一些常见的方案 +配置的管理有多种方案,以下列举一些常见的方案: -1. 使用平台管理配置,应用构建时将当前环境的配置放入包内,启动时指定该配置。但应用就无法一次构建多次部署,而且本地开发环境想使用配置会变的很麻烦。 -1. 使用平台管理配置,在启动时将当前环境的配置通过环境变量传入,这是比较优雅的方式,但框架对运维的要求会比较高,需要部署平台支持,同时开发环境也有相同痛点。 -1. 使用代码管理配置,在代码中添加多个环境的配置,在启动时传入当前环境的参数即可。但无法全局配置,必须修改代码。 - -我们选择了最后一种配置方案,**配置即代码**,配置的变更也应该经过 review 后才能发布。应用包本身是可以部署在多个环境的,只需要指定运行环境即可。 +1. 使用平台管理配置,应用构建时将当前环境的配置放入包内,启动时指定该配置。但应用就无法一次构建多次部署,而且本地开发环境想使用配置会变得很麻烦。 +2. 使用平台管理配置,在启动时将当前环境的配置通过环境变量传入,这是比较优雅的方式,但框架对运维的要求会比较高,需要部署平台支持,同时开发环境也有相同的痛点。 +3. 使用代码管理配置,在代码中添加多个环境的配置,在启动时传入当前环境的参数即可。但无法全局配置,必须修改代码。 +我们选择了最后一种配置方案,**配置即代码**,配置的变更也应该经过审核后才能发布。应用包本身是可以部署在多个环境的,只需要指定运行环境即可。 ### 多环境配置 -框架支持根据环境来加载配置,定义多个环境的配置文件,具体环境请查看[运行环境配置](./env.md) +框架支持根据环境来加载配置,定义多个环境的配置文件,具体环境请查看[运行环境配置](./env.md)。 ``` config @@ -27,11 +26,10 @@ config `config.default.js` 为默认的配置文件,所有环境都会加载这个配置文件,一般也会作为开发环境的默认配置文件。 -当指定 env 时会同时加载默认配置和对应的配置(具名配置)文件,具名配置和默认配置将合并(使用[extend2](https://www.npmjs.com/package/extend2)深拷贝)成最终配置,具名配置项会覆盖默认配置文件的同名配置。如 `prod` 环境会加载 `config.prod.js` 和 `config.default.js` 文件,`config.prod.js` 会覆盖 `config.default.js` 的同名配置。 - +当指定 `env` 时,会同时加载默认配置和对应的配置(具名配置)文件。具名配置和默认配置将合并(使用 [extend2](https://www.npmjs.com/package/extend2) 深拷贝)成最终配置,具名配置项会覆盖默认配置文件的同名配置。例如,`prod` 环境会加载 `config.prod.js` 和 `config.default.js` 文件,`config.prod.js` 会覆盖 `config.default.js` 的同名配置。 ### 配置写法 -配置文件返回的是一个 object 对象,可以覆盖框架的一些配置,应用也可以将自己业务的配置放到这里方便管理。 +配置文件返回的是一个对象,可以覆盖框架的一些配置,应用也可以将自己业务的配置放到这里方便管理。 ```js // 配置 logger 文件的目录,logger 默认配置由框架提供 @@ -42,7 +40,7 @@ module.exports = { }; ``` -配置文件也可以简化的写成 `exports.key = value` 形式 +配置文件也可以简化地写成 `exports.key = value` 形式: ```js exports.keys = 'my-cookie-secret-key'; @@ -51,7 +49,7 @@ exports.logger = { }; ``` -配置文件也可以返回一个 function,可以接受 appInfo 参数 +配置文件也可以返回一个函数,该函数可以接受 `appInfo` 参数: ```js // 将 logger 目录放到代码目录下 @@ -65,22 +63,22 @@ module.exports = (appInfo) => { }; ``` -内置的 appInfo 有 +内置的 `appInfo` 属性包括: -| appInfo | 说明 | -| ------- | ---------------------------------------------------------------------- | -| pkg | package.json | -| name | 应用名,同 pkg.name | -| baseDir | 应用代码的目录 | -| HOME | 用户目录,如 admin 账户为 /home/admin | -| root | 应用根目录,只有在 local 和 unittest 环境下为 baseDir,其他都为 HOME。 | +| appInfo | 说明 | +| ------- | ------------------------------------------------------------ | +| pkg | `package.json` 文件 | +| name | 应用名称,同 `pkg.name` | +| baseDir | 应用代码的目录 | +| HOME | 用户目录,如 admin 账户为 `/home/admin` | +| root | 应用根目录,在 `local` 和 `unittest` 环境下为 `baseDir`,其他都为 `HOME`。 | -`appInfo.root` 是一个优雅的适配,比如在服务器环境我们会使用 `/home/admin/logs` 作为日志目录,而本地开发时又不想污染用户目录,这样的适配就很好解决这个问题。 +`appInfo.root` 是一个优雅的适配方案。例如,在服务器环境我们通常使用 `/home/admin/logs` 作为日志目录,而在本地开发时为了避免污染用户目录,我们需要一种优雅的适配方案,`appInfo.root` 正好解决了这个问题。 -请根据具体场合选择合适的写法,但请确保没有写出以下代码: +请根据具体场合选择合适的写法。但请确保没有完成以下代码: ```js -// config/config.default.js +// 配置文件 config/config.default.js exports.someKeys = 'abc'; module.exports = (appInfo) => { const config = {}; @@ -91,9 +89,9 @@ module.exports = (appInfo) => { ### 配置加载顺序 -应用、插件、框架都可以定义这些配置,而且目录结构都是一致的,但存在优先级(应用 > 框架 > 插件),相对于此运行环境的优先级会更高。 +应用、插件、框架都可以定义这些配置,且目录结构都是一致的,但存在优先级(应用 > 框架 > 插件),相对于此运行环境的优先级会更高。 -比如在 prod 环境加载一个配置的加载顺序如下,后加载的会覆盖前面的同名配置。 +比如在 prod 环境中加载一个配置的加载顺序如下,后加载的会覆盖前面的同名配置。 ``` -> 插件 config.default.js @@ -104,11 +102,10 @@ module.exports = (appInfo) => { -> 应用 config.prod.js ``` -**注意:插件之间也会有加载顺序,但大致顺序类似,具体逻辑可[查看加载器](../advanced/loader.md)。** - +**注意**:插件之间也会有加载顺序,但大致顺序类似。具体逻辑可[查看加载器](../advanced/loader.md)。 ### 合并规则 -配置的合并使用 [extend2] 模块进行深度拷贝,[extend2] fork 自 [extend],处理数组时会存在差异。 +配置的合并使用 `extend2` 模块进行深度拷贝,`extend2` 来源于 `extend`,但是在处理数组时的表现会有所不同。 ```js const a = { @@ -125,14 +122,14 @@ extend(true, a, b); ### 配置结果 -框架在启动时会把合并后的最终配置 dump 到 `run/application_config.json`(worker 进程)和 `run/agent_config.json`(agent 进程)中,可以用来分析问题。 +框架在启动时会把合并后的最终配置输出到 `run/application_config.json`(worker 进程)和 `run/agent_config.json`(agent 进程)中,以供问题分析。 -配置文件中会隐藏一些字段,主要包括两类: +配置文件中会隐藏以下两类字段: -- 如密码、密钥等安全字段,这里可以通过 `config.dump.ignore` 配置,必须是 [Set] 类型,查看[默认配置](https://github.com/eggjs/egg/blob/master/config/config.default.js)。 -- 如函数、Buffer 等类型,`JSON.stringify` 后的内容特别大 +1. 安全字段,如密码、密钥等。这些字段通过 `config.dump.ignore` 属性进行配置,其类型必须是 [Set]。可参见[默认配置](https://github.com/eggjs/egg/blob/master/config/config.default.js)。 +2. 非字符串化字段,如函数、Buffer 等。这些字段在 `JSON.stringify` 后所生成的内容容量很大。 -还会生成 `run/application_config_meta.json`(worker 进程)和 `run/agent_config_meta.json`(agent 进程)文件,用来排查属性的来源,如 +此外,框架还会生成 `run/application_config_meta.json`(worker 进程)和 `run/agent_config_meta.json`(agent 进程)文件。这些文件用于排查配置属性的来源,例如: ```json { @@ -142,6 +139,6 @@ extend(true, a, b); } ``` -[set]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set +[Set]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set [extend]: https://github.com/justmoon/node-extend [extend2]: https://github.com/eggjs/extend2 diff --git a/site/docs/basics/controller.zh-CN.md b/site/docs/basics/controller.zh-CN.md index c9610a8565..d68544ba05 100644 --- a/site/docs/basics/controller.zh-CN.md +++ b/site/docs/basics/controller.zh-CN.md @@ -5,21 +5,20 @@ order: 7 ## 什么是 Controller -[前面章节](./router.md)写到,我们通过 Router 将用户的请求基于 method 和 URL 分发到了对应的 Controller 上,那 Controller 负责做什么? +[前面章节](./router.md) 提到,我们通过 Router 将用户的请求基于 method 和 URL 分发到了对应的 Controller,那么 Controller 主要有什么职责呢? -简单的说 Controller 负责**解析用户的输入,处理后返回相应的结果**,例如 +简单地说,Controller 负责**解析用户的输入,处理后返回相应的结果**。例如: -- 在 [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) 接口中,Controller 接受用户的参数,从数据库中查找内容返回给用户或者将用户的请求更新到数据库中。 -- 在 HTML 页面请求中,Controller 根据用户访问不同的 URL,渲染不同的模板得到 HTML 返回给用户。 -- 在代理服务器中,Controller 将用户的请求转发到其他服务器上,并将其他服务器的处理结果返回给用户。 +- 在 [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) 接口中,Controller 接受用户的参数,从数据库中查找内容返回给用户,或将用户的请求更新到数据库中。 +- 在 HTML 页面请求中,Controller 根据用户访问不同的 URL,渲染不同的模板得到 HTML,后返回给用户。 +- 在代理服务器中,Controller 将用户的请求转发到其他服务器,之后将那些服务器的处理结果返回给用户。 -框架推荐 Controller 层主要对用户的请求参数进行处理(校验、转换),然后调用对应的 [service](./service.md) 方法处理业务,得到业务结果后封装并返回: +框架推荐的 Controller 层主要流程是:首先对用户通过 HTTP 传递过来的请求参数进行处理(校验、转换),然后调用对应的 [service](./service.md) 方法处理业务,在必要时把 Service 的返回结果处理转换,使之满足用户需求,最后通过 HTTP 将结果响应给用户。具体步骤如下: 1. 获取用户通过 HTTP 传递过来的请求参数。 -1. 校验、组装参数。 -1. 调用 Service 进行业务处理,必要时处理转换 Service 的返回结果,让它适应用户的需求。 -1. 通过 HTTP 将结果响应给用户。 - +2. 校验、组装参数。 +3. 调用 Service 进行业务处理,必要时处理转换 Service 的返回结果,让它适应用户的需求。 +4. 通过 HTTP 将结果响应给用户。 ## 如何编写 Controller 所有的 Controller 文件都必须放在 `app/controller` 目录下,可以支持多级目录,访问的时候可以通过目录名级联访问。Controller 支持多种形式进行编写,可以根据不同的项目场景和开发习惯来选择。 @@ -28,7 +27,7 @@ order: 7 我们可以通过定义 Controller 类的方式来编写代码: -```js +```javascript // app/controller/post.js const Controller = require('egg').Controller; class PostController extends Controller { @@ -53,9 +52,9 @@ class PostController extends Controller { module.exports = PostController; ``` -我们通过上面的代码定义了一个 `PostController` 的类,类里面的每一个方法都可以作为一个 Controller 在 Router 中引用到,我们可以从 `app.controller` 根据文件名和方法名定位到它。 +我们通过上述代码定义了一个 `PostController` 的类,类里面的每一个方法都可以作为一个 Controller 在 Router 中引用。下面是如何在 `app.router` 中根据文件名和方法名定位到它的示例: -```js +```javascript // app/router.js module.exports = (app) => { const { router, controller } = app; @@ -63,28 +62,28 @@ module.exports = (app) => { }; ``` -Controller 支持多级目录,例如如果我们将上面的 Controller 代码放到 `app/controller/sub/post.js` 中,则可以在 router 中这样使用: +Controller 支持多级目录。例如,如果我们将上面的 Controller 代码放到 `app/controller/sub/post.js` 中,那么可以在 router 中这样使用: -```js +```javascript // app/router.js -module.exports = (app) => { +module.exports = app => { app.router.post('createPost', '/api/posts', app.controller.sub.post.create); }; ``` -定义的 Controller 类,会在每一个请求访问到 server 时实例化一个全新的对象,而项目中的 Controller 类继承于 `egg.Controller`,会有下面几个属性挂在 `this` 上。 +定义的 Controller 类在每一个请求访问到服务器时实例化一个全新的对象,而项目中的 Controller 类继承于 `egg.Controller`,会有以下几个属性挂在 `this` 上: -- `this.ctx`: 当前请求的上下文 [Context](./extend.md#context) 对象的实例,通过它我们可以拿到框架封装好的处理当前请求的各种便捷属性和方法。 -- `this.app`: 当前应用 [Application](./extend.md#application) 对象的实例,通过它我们可以拿到框架提供的全局对象和方法。 -- `this.service`:应用定义的 [Service](./service.md),通过它我们可以访问到抽象出的业务层,等价于 `this.ctx.service` 。 +- `this.ctx`:当前请求的上下文 [Context](./extend.md#context) 对象的实例,通过它我们可以拿到框架封装好的处理当前请求的各种便捷属性和方法。 +- `this.app`:当前应用 [Application](./extend.md#application) 对象的实例,通过它我们可以拿到框架提供的全局对象和方法。 +- `this.service`:应用定义的 [Service](./service.md),通过它我们可以访问抽象出的业务层,等价于 `this.ctx.service`。 - `this.config`:应用运行时的[配置项](./config.md)。 -- `this.logger`:logger 对象,上面有四个方法(`debug`,`info`,`warn`,`error`),分别代表打印四个不同级别的日志,使用方法和效果与 [context logger](../core/logger.md#context-logger) 中介绍的一样,但是通过这个 logger 对象记录的日志,在日志前面会加上打印该日志的文件路径,以便快速定位日志打印位置。 +- `this.logger`:logger 对象,上面有四个方法(`debug`、`info`、`warn`、`error`),分别代表打印四个不同级别的日志。使用方法和效果与 [context logger](../core/logger.md#context-logger) 中介绍的相同,但是通过这个 logger 对象记录的日志,在日志前面会加上打印该日志的文件路径,以便快速定位日志打印位置。 #### 自定义 Controller 基类 -按照类的方式编写 Controller,不仅可以让我们更好的对 Controller 层代码进行抽象(例如将一些统一的处理抽象成一些私有方法),还可以通过自定义 Controller 基类的方式封装应用中常用的方法。 +按照类的方式编写 Controller,不仅可以让我们更好地对 Controller 层代码进行抽象(例如,将一些统一的处理抽象为一些私有方法),还可以通过自定义 Controller 基类的方式封装应用中常用的方法。 -```js +```javascript // app/core/base_controller.js const { Controller } = require('egg'); class BaseController extends Controller { @@ -107,10 +106,10 @@ class BaseController extends Controller { module.exports = BaseController; ``` -此时在编写应用的 Controller 时,可以继承 BaseController,直接使用基类上的方法: +在编写应用的 Controller 时,可以继承 BaseController,直接使用基类上的方法: -```js -//app/controller/post.js +```javascript +// app/controller/post.js const Controller = require('../core/base_controller'); class PostController extends Controller { async list() { @@ -122,13 +121,13 @@ class PostController extends Controller { ### Controller 方法(不推荐使用,只是为了兼容) -每一个 Controller 都是一个 async function,它的入参为请求的上下文 [Context](./extend.md#context) 对象的实例,通过它我们可以拿到框架封装好的各种便捷属性和方法。 +每一个 Controller 都是一个 `async function`,其入参为请求的上下文 [Context](./extend.md#context) 对象的实例。通过它,我们可以拿到框架封装好的各种便捷属性和方法。 -例如我们写一个对应到 `POST /api/posts` 接口的 Controller,我们会在 `app/controller` 目录下创建一个 `post.js` 文件 +例如,我们编写一个对应到 `POST /api/posts` 接口的 Controller,我们需要在 `app/controller` 目录下创建一个 `post.js` 文件: -```js +```javascript // app/controller/post.js -exports.create = async (ctx) => { +exports.create = async ctx => { const createRule = { title: { type: 'string' }, content: { type: 'string' }, @@ -138,7 +137,7 @@ exports.create = async (ctx) => { // 组装参数 const author = ctx.session.userId; const req = Object.assign(ctx.request.body, { author }); - // 调用 service 进行业务处理 + // 调用 Service 进行业务处理 const res = await ctx.service.post.create(req); // 设置响应内容和响应状态码 ctx.body = { id: res.id }; @@ -146,19 +145,18 @@ exports.create = async (ctx) => { }; ``` -在上面的例子中我们引入了许多新的概念,但还是比较直观,容易理解的,我们会在下面对它们进行更详细的介绍。 - +以上是一个简单直观的例子,我们引入了一些新的概念,但它们都是易于理解的。我们将在后面对它们进行更详细的介绍。 ## HTTP 基础 -由于 Controller 基本上是业务开发中唯一和 HTTP 协议打交道的地方,在继续往下了解之前,我们首先简单的看一下 HTTP 协议是怎样的。 +由于控制器(Controller)基本上是业务开发中唯一与 HTTP 协议打交道的地方,在继续深入了解之前,我们首先要简单了解一下 HTTP 协议本身。 -如果我们发起一个 HTTP 请求来访问前面例子中提到的 Controller: +假设我们发起一个 HTTP 请求来访问前面例子中提及的 Controller: ``` curl -X POST http://localhost:3000/api/posts --data '{"title":"controller", "content": "what is controller"}' --header 'Content-Type:application/json; charset=UTF-8' ``` -通过 curl 发出的 HTTP 请求的内容就会是下面这样的: +通过 curl 发出的 HTTP 请求内容就如下: ``` POST /api/posts HTTP/1.1 @@ -168,19 +166,19 @@ Content-Type: application/json; charset=UTF-8 {"title": "controller", "content": "what is controller"} ``` -请求的第一行包含了三个信息,我们比较常用的是前面两个: +请求的第一行包含三个信息,我们较为常用的是前两个: -- method:这个请求中 method 的值是 `POST`。 -- path:值为 `/api/posts`,如果用户的请求中包含 query,也会在这里出现 +- 方法(method):这个请求中 method 的值是 `POST`。 +- 路径(path):值为 `/api/posts`,如果用户请求中含 query,则也会在此出现。 -从第二行开始直到遇到的第一个空行位置,都是请求的 Headers 部分,这一部分中有许多常用的属性,包括这里看到的 Host,Content-Type,还有 `Cookie`,`User-Agent` 等等。在这个请求中有两个头: +从第二行开始至第一个空行之前,都是请求的头部(Headers)部分。这里有众多常用属性,如 Host、Content-Type,以及 `Cookie`、`User-Agent` 等。在本次请求中有两个头信息: -- `Host`:我们在浏览器发起请求的时候,域名会用来通过 DNS 解析找到服务的 IP 地址,但是浏览器也会将域名和端口号放在 Host 头中一并发送给服务端。 -- `Content-Type`:当我们的请求有 body 的时候,都会有 Content-Type 来标明我们的请求体是什么格式的。 +- `Host`:浏览器发起请求时,会使用域名通过 DNS 解析找到服务器的 IP 地址,浏览器还会将域名和端口号放进 Host 头内发送给服务器。 +- `Content-Type`:请求中如有请求体(body),通常伴随 Content-Type,标明请求体格式。 -之后的内容全部都是请求的 body,当请求是 POST, PUT, DELETE 等方法的时候,可以带上请求体,服务端会根据 Content-Type 来解析请求体。 +之后内容为请求体,POST、PUT、DELETE 等方法可附带请求体,服务端根据 Content-Type 解析请求体。 -在服务端处理完这个请求后,会发送一个 HTTP 响应给客户端 +服务器处理请求后,会发送一个 HTTP 响应给客户端: ``` HTTP/1.1 201 Created @@ -192,67 +190,68 @@ Connection: keep-alive {"id": 1} ``` -第一行中也包含了三段,其中我们常用的主要是[响应状态码](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes),这个例子中它的值是 201,它的含义是在服务端成功创建了一条资源。 +响应的首行也包括三部分,其中常用的主要是[响应状态码](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes)。本例中为 201,意味服务端成功创建了资源。 -和请求一样,从第二行开始到下一个空行之间都是响应头,这里的 Content-Type, Content-Length 表示这个响应的格式是 JSON,长度为 8 个字节。 - -最后剩下的部分就是这次响应真正的内容。 +响应头从第二行至下一个空行,这里的 Content-Type 和 Content-Length 表明响应格式为 JSON,长度 8 字节。 +最后部分即响应实际内容。 ## 获取 HTTP 请求参数 -从上面的 HTTP 请求示例中可以看到,有好多地方可以放用户的请求数据,框架通过在 Controller 上绑定的 Context 实例,提供了许多便捷方法和属性获取用户通过 HTTP 请求发送过来的参数。 +从上述 HTTP 请求示例中, 我们可以看到, 多个位置可以放置用户的请求数据。框架通过在 Controller 上绑定的 Context 实例, 提供了多种便捷方法和属性, 以获取用户通过 HTTP 请求发送过来的参数。 -### query +### Query -在 URL 中 `?` 后面的部分是一个 Query String,这一部分经常用于 GET 类型的请求中传递参数。例如 `GET /posts?category=egg&language=node` 中 `category=egg&language=node` 就是用户传递过来的参数。我们可以通过 `ctx.query` 拿到解析过后的这个参数体 +在 URL 中 `?` 后的部分是 Query String。这部分经常用于 GET 类型请求中传递参数。例如,`GET /posts?category=egg&language=node` 中的 `category=egg&language=node` 就是用户传递的参数。我们可以通过 `ctx.query` 获取解析后的这个参数对象。 ```js class PostController extends Controller { async listPosts() { const query = this.ctx.query; + // 输出: // { // category: 'egg', - // language: 'node', + // language: 'node' // } } } ``` -当 Query String 中的 key 重复时,`ctx.query` 只取 key 第一次出现时的值,后面再出现的都会被忽略。`GET /posts?category=egg&category=koa` 通过 `ctx.query` 拿到的值是 `{ category: 'egg' }`。 +当 Query String 中的 key 重复时,`ctx.query` 只取第一次出现的值,后续的都会被忽略。例如 `GET /posts?category=egg&category=koa`,通过 `ctx.query` 获取的值将是 `{ category: 'egg' }`。 -这样处理的原因是为了保持统一性,由于通常情况下我们都不会设计让用户传递 key 相同的 Query String,所以我们经常会写类似下面的代码: +之所以这样处理是为了保持一致性。一般我们不会设计让用户传递相同 key 的 Query String,所以经常编写如下代码: ```js const key = ctx.query.key || ''; if (key.startsWith('egg')) { - // do something + // 执行相应操作 } ``` -而如果有人故意发起请求在 Query String 中带上重复的 key 来请求时就会引发系统异常。因此框架保证了从 `ctx.query` 上获取的参数一旦存在,一定是字符串类型。 +如果有人故意在 Query String 中带上重复的 key 请求,就会引发系统异常。因此框架确保从 `ctx.query` 获取的参数一旦存在,一定是字符串类型。 -#### queries +#### Queries -有时候我们的系统会设计成让用户传递相同的 key,例如 `GET /posts?category=egg&id=1&id=2&id=3`。针对此类情况,框架提供了 `ctx.queries` 对象,这个对象也解析了 Query String,但是它不会丢弃任何一个重复的数据,而是将他们都放到一个数组中: +有些系统会设计为让用户传递相同的 key,例如 `GET /posts?category=egg&id=1&id=2&id=3`。框架提供了 `ctx.queries` 对象,它同样解析了 Query String,但不会丢弃任何重复数据,而是将它们放进一个数组。 ```js // 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' ], + // id: [ '1', '2', '3' ] // } } } ``` -`ctx.queries` 上所有的 key 如果有值,也一定会是数组类型。 +`ctx.queries` 中所有 key 的值,如果存在, 必然是数组类型。 -### Router params +### Router Params -在 [Router](./router.md) 中,我们介绍了 Router 上也可以申明参数,这些参数都可以通过 `ctx.params` 获取到。 +在 [Router](./router.md) 文档中,我们介绍了可以在 Router 上声明参数,所有参数可通过 `ctx.params` 获取。 ```js // app.get('/projects/:projectId/app/:appId', 'app.listApp'); @@ -265,16 +264,16 @@ class AppController extends Controller { } ``` -### body +### Body -虽然我们可以通过 URL 传递参数,但是还是有诸多限制: +我们也可以通过 URL 传参,但存在一些限制: -- [浏览器中会对 URL 的长度有所限制](http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers),如果需要传递的参数过多就会无法传递。 -- 服务端经常会将访问的完整 URL 记录到日志文件中,有一些敏感数据通过 URL 传递会不安全。 +- [浏览器对 URL 长度有限制](http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers);若参数过多可能无法传递。 +- 服务端通常会将完整 URL 记录到日志中;敏感数据如果通过 URL 传递可能不安全。 -在前面的 HTTP 请求报文示例中,我们看到在 header 之后还有一个 body 部分,我们通常会在这个部分传递 POST、PUT 和 DELETE 等方法的参数。一般请求中有 body 的时候,客户端(浏览器)会同时发送 `Content-Type` 告诉服务端这次请求的 body 是什么格式的。Web 开发中数据传递最常用的两类格式分别是 JSON 和 Form。 +在 HTTP 请求报文中,Header 之后有一个 Body 部分。POST、PUT 和 DELETE 等方法常在此传递参数。请求中若有 Body,则客户端(浏览器)会发送 `Content-Type` 告知服务端该请求 body 的格式。WEB 应用数据传输中最常见的格式有 JSON 和 Form。 -框架内置了 [bodyParser](https://github.com/koajs/bodyparser) 中间件来对这两类格式的请求 body 解析成 object 挂载到 `ctx.request.body` 上。HTTP 协议中并不建议在通过 GET、HEAD 方法访问时传递 body,所以我们无法在 GET、HEAD 方法中按照此方法获取到内容。 +框架内置了 [bodyParser](https://github.com/koajs/bodyparser) 中间件,可解析这两种格式请求的 body 并挂载到 `ctx.request.body` 上。`GET`、`HEAD` 方法不建议传递 body,因此无法按此方法获取内容。 ```js // POST /api/posts HTTP/1.1 @@ -290,13 +289,13 @@ class PostController extends Controller { } ``` -框架对 bodyParser 设置了一些默认参数,配置好之后拥有以下特性: +框架为 bodyParser 中间件配置了默认参数。配置后具备以下特性: -- 当请求的 Content-Type 为 `application/json`,`application/json-patch+json`,`application/vnd.api+json` 和 `application/csp-report` 时,会按照 json 格式对请求 body 进行解析,并限制 body 最大长度为 `100kb`。 -- 当请求的 Content-Type 为 `application/x-www-form-urlencoded` 时,会按照 form 格式对请求 body 进行解析,并限制 body 最大长度为 `100kb`。 -- 如果解析成功,body 一定会是一个 Object(可能是一个数组)。 +- Content-Type 为 `application/json`,`application/json-patch+json`,`application/vnd.api+json` 和 `application/csp-report` 时,按 json 格式解析请求 body,限制最大长度 `100kb`。 +- Content-Type 为 `application/x-www-form-urlencoded` 时,按 form 格式解析请求 body,限制最大长度 `100kb`。 +- 若解析成功,body 必然是 Object(Array)。 -一般来说我们最经常调整的配置项就是变更解析时允许的最大长度,可以在 `config/config.default.js` 中覆盖框架的默认值。 +通常我们会调整配置项以变更解析时允许的最大长度,可在 `config/config.default.js` 中修改默认值。 ```js module.exports = { @@ -307,140 +306,136 @@ module.exports = { }; ``` -如果用户的请求 body 超过了我们配置的解析最大长度,会抛出一个状态码为 `413` 的异常,如果用户请求的 body 解析失败(错误的 JSON),会抛出一个状态码为 `400` 的异常。 - -**注意:在调整 bodyParser 支持的 body 长度时,如果我们应用前面还有一层反向代理(Nginx),可能也需要调整它的配置,确保反向代理也支持同样长度的请求 body。** +如果请求 body 超过配置的最大长度,会抛出状态码 `413` 的异常;body 解析失败(如错误 JSON)会抛出状态码 `400` 的异常。 -**一个常见的错误是把 `ctx.request.body` 和 `ctx.body` 混淆,后者其实是 `ctx.response.body` 的简写。** +**注意:调整 bodyParser 支持的 body 长度时,如果应用之前有一层反向代理(如 Nginx),同样需要调整配置确保支持相等长度的请求 body。** +**常见错误:将 `ctx.request.body` 与 `ctx.body` 混淆,后者实际上是 `ctx.response.body` 的简写。** ### 获取上传的文件 -请求 body 除了可以带参数之外,还可以发送文件,一般来说,浏览器上都是通过 `Multipart/form-data` 格式发送文件的,框架通过内置 [Multipart](https://github.com/eggjs/egg-multipart) 插件来支持获取用户上传的文件,我们为你提供了两种方式: +请求体除了可以带参数之外,还可以发送文件。通常情况下,浏览器会通过 `Multipart/form-data` 格式发送文件。通过内置的 [Multipart](https://github.com/eggjs/egg-multipart) 插件,框架支持获取用户上传的文件。我们为你提供了两种方式: + +#### File 模式 -- #### File 模式: - 如果你完全不知道 Nodejs 中的 Stream 用法,那么 File 模式非常合适你: +如果你不熟悉 Node.js 中的 Stream 用法,那么 File 模式非常适合你: 1)在 config 文件中启用 `file` 模式: -```js +```javascript // config/config.default.js exports.multipart = { mode: 'file', }; ``` -2)上传 / 接收文件: +2)上传/接收文件: -1. 上传 / 接收单个文件: +1. 上传/接收单个文件: -你的前端静态页面代码应该看上去如下样子: +你的前端静态页面代码可能如下所示: ```html -
- title: file: - + + title: + file: +
``` 对应的后端代码如下: -```js +```javascript // app/controller/upload.js const Controller = require('egg').Controller; const fs = require('fs/promises'); +const path = require('path'); // 补上缺失的 path 模块 -module.exports = class extends Controller { +class UploadController 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, }; } -}; -``` +} -2. 上传 / 接收多个文件: +module.exports = UploadController; +``` -对于多个文件,我们借助 `ctx.request.files` 属性进行遍历,然后分别进行处理: +2. 上传/接收多个文件: -你的前端静态页面代码应该看上去如下样子: +对于多个文件,可以使用 `ctx.request.files` 数组进行遍历,然后分别处理每个文件。以下是你的前端静态页面的代码: ```html -
- title: file1: file2: - - + + title: + file1: + file2: +
``` -对应的后端代码: +对应的后端代码如下: -```js +```javascript // app/controller/upload.js const Controller = require('egg').Controller; const fs = require('fs/promises'); +const path = require('path'); // 补上缺失的 path 模块 -module.exports = class extends Controller { +class UploadController extends Controller { async upload() { const { ctx } = this; console.log(ctx.request.body); - console.log('got %d files', ctx.request.files.length); + console.log(`共收到 ${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); + console.log(`字段名: ${file.fieldname}`); + console.log(`文件名: ${file.filename}`); + console.log(`编码: ${file.encoding}`); + console.log(`MIME 类型: ${file.mime}`); + console.log(`临时文件路径: ${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); } } -}; +} + +module.exports = UploadController; ``` -- #### Stream 模式: - 如果你对于 Node 中的 Stream 模式非常熟悉,那么你可以选择此模式。在 Controller 中,我们可以通过 `ctx.getFileStream()` 接口能获取到上传的文件流。 +以上代码包涵了前端的表单代码以及后端处理上传文件的代码。在服务器端,我们首先获取上传文件的信息,然后将文件上传到指定的储存系统,例如云储存。随后,我们确保了临时文件被删除,防止占用服务器空间。 +#### Stream 模式 + +如果你对 Node 中的 Stream 模式非常熟悉,那么你可以选择此模式。在 Controller 中,我们可以通过 `ctx.getFileStream()` 接口获取到上传的文件流。 -1. 上传 / 接受单个文件: +1. 上传/接受单个文件: ```html -
- title: file: + + title: file:
``` @@ -466,9 +461,9 @@ class UploaderController extends Controller { } ctx.body = { - url: result.url, + url:result.url, // 所有表单字段都能通过 `stream.fields` 获取到 - fields: stream.fields, + fields:stream.fields }; } } @@ -476,12 +471,12 @@ class UploaderController extends Controller { module.exports = UploaderController; ``` -要通过 `ctx.getFileStream` 便捷的获取到用户上传的文件,需要满足两个条件: +要通过 `ctx.getFileStream` 便捷地获取到用户上传的文件,需要满足两个条件: - 只支持上传一个文件。 - 上传文件必须在所有其他的 fields 后面,否则在拿到文件流时可能还获取不到 fields。 -2. 上传 / 接受多个文件: +2. 上传/接受多个文件: 如果要获取同时上传的多个文件,不能通过 `ctx.getFileStream()` 来获取,只能通过下面这种方式: @@ -498,28 +493,25 @@ class UploaderController extends Controller { while ((part = await parts()) != null) { if (part.length) { // 这是 busboy 的字段 - console.log('field: ' + part[0]); - console.log('value: ' + part[1]); - console.log('valueTruncated: ' + part[2]); - console.log('fieldnameTruncated: ' + part[3]); + console.log('field:' + part[0]); + console.log('value:' + part[1]); + console.log('valueTruncated:' + part[2]); + console.log('fieldnameTruncated:' + part[3]); } else { if (!part.filename) { - // 这时是用户没有选择文件就点击了上传(part 是 file stream,但是 part.filename 为空) + // 这时是用户没有选择文件就点击了上传(part 是 file stream,但是 part.filename 为空) // 需要做出处理,例如给出错误提示消息 return; } // part 是上传的文件流 - console.log('field: ' + part.fieldname); - console.log('filename: ' + part.filename); - console.log('encoding: ' + part.encoding); - console.log('mime: ' + part.mime); + console.log('field:' + part.fieldname); + console.log('filename:' + part.filename); + console.log('encoding:' + part.encoding); + console.log('mime:' + part.mime); // 文件处理,上传到云存储等等 let result; try { - result = await ctx.oss.put( - 'egg-multipart-test/' + part.filename, - part, - ); + result = await ctx.oss.put('egg-multipart-test/' + part.filename, part); } catch (err) { // 必须将上传的文件流消费掉,要不然浏览器响应会卡死 await sendToWormhole(part); @@ -535,12 +527,12 @@ class UploaderController extends Controller { module.exports = UploaderController; ``` -为了保证文件上传的安全,框架限制了支持的的文件格式,框架默认支持白名单如下: +为了保证文件上传的安全,框架限制了支持的文件格式。框架默认支持的白名单如下: ```js // images '.jpg', '.jpeg', // image/jpeg -'.png', // image/png, image/x-png +'.png', // image/png,image/x-png '.gif', // image/gif '.bmp', // image/bmp '.wbmp', // image/vnd.wap.wbmp @@ -560,78 +552,77 @@ module.exports = UploaderController; // video '.mp3', '.mp4', -'.avi', +'.avi' ``` -用户可以通过在 `config/config.default.js` 中配置来新增支持的文件扩展名,或者重写整个白名单 +用户可以通过在 `config/config.default.js` 中的配置来新增支持的文件扩展名,或者重写整个白名单。 -- 新增支持的文件扩展名 +- 新增支持的文件扩展名: ```js module.exports = { multipart: { - fileExtensions: ['.apk'], // 增加对 apk 扩展名的文件支持 - }, + fileExtensions: ['.apk'] // 增加对 '.apk' 扩展名的文件支持 + } }; ``` -- 覆盖整个白名单 +- 覆盖整个白名单: ```js module.exports = { multipart: { - whitelist: ['.png'], // 覆盖整个白名单,只允许上传 '.png' 格式 - }, + whitelist: ['.png'] // 覆盖整个白名单,只允许上传 '.png' 格式 + } }; ``` **注意:当重写了 whitelist 时,fileExtensions 不生效。** -欲了解更多相关此技术细节和详情,请参阅 [Egg-Multipart](https://github.com/eggjs/egg-multipart)。 +欲了解更多有关的技术细节和信息,请参阅 [Egg-Multipart](https://github.com/eggjs/egg-multipart)。 +### Header -### header +除了从 URL 和请求 body 上获取参数之外,还有许多参数是通过请求 header 传递的。框架提供了一些辅助属性和方法来获取: -除了从 URL 和请求 body 上获取参数之外,还有许多参数是通过请求 header 传递的。框架提供了一些辅助属性和方法来获取。 +- `ctx.headers`、`ctx.header`、`ctx.request.headers`、`ctx.request.header`:这几个方法是等价的,都用于获取整个 header 对象。 +- `ctx.get(name)`、`ctx.request.get(name)`:用于获取请求 header 中的一个字段的值。如果这个字段不存在,会返回空字符串。 +- 我们建议使用 `ctx.get(name)` 而不是 `ctx.headers['name']`,因为前者会自动处理字段名大小写。 -- `ctx.headers`,`ctx.header`,`ctx.request.headers`,`ctx.request.header`:这几个方法是等价的,都是获取整个 header 对象。 -- `ctx.get(name)`,`ctx.request.get(name)`:获取请求 header 中的一个字段的值,如果这个字段不存在,会返回空字符串。 -- 我们建议用 `ctx.get(name)` 而不是 `ctx.headers['name']`,因为前者会自动处理大小写。 +由于 header 的特殊性,某些字段如 `Content-Type`、`Accept` 等有明确的 HTTP 协议含义,有些如 `X-Forwarded-For` 则由反向代理设定。框架对这些字段提供了便捷的 getter,详细信息参见 [API](https://eggjs.org/api/) 文档。 -由于 header 比较特殊,有一些是 `HTTP` 协议规定了具体含义的(例如 `Content-Type`,`Accept`),有些是反向代理设置的,已经约定俗成(X-Forwarded-For),框架也会对他们增加一些便捷的 getter,详细的 getter 可以查看 [API](https://eggjs.org/api/) 文档。 - -特别是如果我们通过 `config.proxy = true` 设置了应用部署在反向代理(Nginx)之后,有一些 Getter 的内部处理会发生改变。 +特别地,若通过 `config.proxy = true` 设定了应用部署在反向代理(如 Nginx)之后,某些 Getter 的内部处理将发生改变。 #### `ctx.host` -优先读通过 `config.hostHeaders` 中配置的 header 的值,读不到时再尝试获取 host 这个 header 的值,如果都获取不到,返回空字符串。 +此 Getter 优先读取 `config.hostHeaders` 中配置的 header 值。若无法获取,则尝试读取 `host` 这个 header 的值。若仍旧获取不到,则返回空字符串。 -`config.hostHeaders` 默认配置为 `x-forwarded-host`。 +`config.hostHeaders` 的默认配置为 `x-forwarded-host`。 #### `ctx.protocol` -通过这个 Getter 获取 protocol 时,首先会判断当前连接是否是加密连接,如果是加密连接,返回 https。 +通过此 Getter 获取协议类型时,首先判断当前连接是否为加密连接(即使用 HTTPS)。若是加密连接,返回 `https`。 -如果处于非加密连接时,优先读通过 `config.protocolHeaders` 中配置的 header 的值来判断是 HTTP 还是 https,如果读取不到,我们可以在配置中通过 `config.protocol` 来设置兜底值,默认为 HTTP。 +对于非加密连接,首先尝试从 `config.protocolHeaders` 中读取 header 值以判断是 HTTP 还是 HTTPS。若读不到值,则可通过 `config.protocol` 设置默认值,默认为 `http`。 -`config.protocolHeaders` 默认配置为 `x-forwarded-proto`。 +`config.protocolHeaders` 的默认配置为 `x-forwarded-proto`。 #### `ctx.ips` -通过 `ctx.ips` 获取请求经过所有的中间设备 IP 地址列表,只有在 `config.proxy = true` 时,才会通过读取 `config.ipHeaders` 中配置的 header 的值来获取,获取不到时为空数组。 +通过 `ctx.ips` 获取请求经过的所有中间设备的 IP 地址列表。只在 `config.proxy = true` 时,才会从 `config.ipHeaders` 中读取 header 值。若获取不到,则为空数组。 -`config.ipHeaders` 默认配置为 `x-forwarded-for`。 +`config.ipHeaders` 的默认配置为 `x-forwarded-for`。 #### `ctx.ip` -通过 `ctx.ip` 获取请求发起方的 IP 地址,优先从 `ctx.ips` 中获取,`ctx.ips` 为空时使用连接上发起方的 IP 地址。 +`ctx.ip` 用于获取请求发起方的 IP 地址。优先从 `ctx.ips` 中获取,若 `ctx.ips` 为空,则使用连接上的 IP 地址。 -**注意:`ip` 和 `ips` 不同,`ip` 当 `config.proxy = false` 时会返回当前连接发起者的 `ip` 地址,`ips` 此时会为空数组。** +**注:`ip` 与 `ips` 存在区别。当 `config.proxy = false` 时,`ip` 会返回当前连接发起者的 IP 地址,而 `ips` 会为空数组。** ### Cookie -HTTP 请求都是无状态的,但是我们的 Web 应用通常都需要知道发起请求的人是谁。为了解决这个问题,HTTP 协议设计了一个特殊的请求头:[Cookie](https://en.wikipedia.org/wiki/HTTP_cookie)。服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性)。 +HTTP 请求本质上是无状态的,但 Web 应用通常需要知道请求者的身份。为此,HTTP 协议设计了 Cookie([Cookie](https://en.wikipedia.org/wiki/HTTP_cookie)),允许服务端通过响应头(set-cookie)向客户端发送数据。浏览器则会将数据保存,并在下次请求同一服务时发送,以确保安全性。 -通过 `ctx.cookies`,我们可以在 Controller 中便捷、安全的设置和读取 Cookie。 +通过 `ctx.cookies`,可在 Controller 中安全地设置和读取 Cookie。 ```js class CookieController extends Controller { @@ -645,19 +636,17 @@ class CookieController extends Controller { async remove() { const ctx = this.ctx; - const count = ctx.cookies.set('count', null); + ctx.cookies.set('count', null); ctx.status = 204; } } ``` -Cookie 虽然在 HTTP 中只是一个头,但是通过 `foo=bar;foo1=bar1;` 的格式可以设置多个键值对。 - -Cookie 在 Web 应用中经常承担了传递客户端身份信息的作用,因此有许多安全相关的配置,不可忽视,[Cookie](../core/cookie-and-session.md#cookie) 文档中详细介绍了 Cookie 的用法和安全相关的配置项,可以深入阅读了解。 +Cookie 通常用于传递客户端身份信息,因此包含众多安全设置。详细用法和安全选项详见 [Cookie 文档](../core/cookie-and-session.md#cookie)。 #### 配置 -对于 Cookie 来说,主要有下面几个属性可以在 `config.default.js` 中进行配置: +Cookie 相关配置位于 `config.default.js`: ```js module.exports = { @@ -668,7 +657,7 @@ module.exports = { }; ``` -举例: 配置应用级别的 Cookie [SameSite](https://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html) 属性等于 `Lax`。 +例如,配置 Cookie [SameSite](https://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html) 属性为 `lax`: ```js module.exports = { @@ -680,18 +669,18 @@ module.exports = { ### Session -通过 Cookie,我们可以给每一个用户设置一个 Session,用来存储用户身份相关的信息,这份信息会加密后存储在 Cookie 中,实现跨请求的用户身份保持。 +Cookie 可以存储每个用户的 Session 来保持跨请求的用户身份。这些信息加密后存储在 Cookie 中。 -框架内置了 [Session](https://github.com/eggjs/egg-session) 插件,给我们提供了 `ctx.session` 来访问或者修改当前用户 Session 。 +框架内置了 [Session](https://github.com/eggjs/egg-session) 插件,通过 `ctx.session` 访问或修改用户 Session: ```js class PostController extends Controller { async fetchPosts() { const ctx = this.ctx; - // 获取 Session 上的内容 + // 读取 Session const userId = ctx.session.userId; const posts = await ctx.service.post.fetch(userId); - // 修改 Session 的值 + // 修改 Session ctx.session.visited = ctx.session.visited ? ++ctx.session.visited : 1; ctx.body = { success: true, @@ -701,7 +690,7 @@ class PostController extends Controller { } ``` -Session 的使用方法非常直观,直接读取它或者修改它就可以了,如果要删除它,直接将它赋值为 `null`: +Session 的操作直观:直接读取、修改或将其赋值为 `null` 删除: ```js class SessionController extends Controller { @@ -711,24 +700,23 @@ class SessionController extends Controller { } ``` -和 Cookie 一样,Session 也有许多安全等选项和功能,在使用之前也最好阅读 [Session](../core/cookie-and-session.md#session) 文档深入了解。 +Session 的安全选项和用法详见 [Session 文档](../core/cookie-and-session.md#session)。 #### 配置 -对于 Session 来说,主要有下面几个属性可以在 `config.default.js` 中进行配置: +Session 相关配置也位于 `config.default.js`: ```js module.exports = { - key: 'EGG_SESS', // 承载 Session 的 Cookie 键值对名字 - maxAge: 86400000, // Session 的最大有效时间 + key: 'EGG_SESS', // Session Cookie 名称 + maxAge: 86400000, // Session 最长有效期 }; ``` - ## 参数校验 -在获取到用户请求的参数后,不可避免的要对参数进行一些校验。 +在获取用户请求的参数后,不可避免要进行一些校验。 -借助 [Validate](https://github.com/eggjs/egg-validate) 插件提供便捷的参数校验机制,帮助我们完成各种复杂的参数校验。 +借助 [Validate](https://github.com/eggjs/egg-validate) 插件,提供便捷的参数校验机制,帮助我们完成各种复杂的参数校验。 ```js // config/plugin.js @@ -738,22 +726,22 @@ exports.validate = { }; ``` -通过 `ctx.validate(rule, [body])` 直接对参数进行校验: +通过 `ctx.validate(rule, [body])` 直接对参数校验: ```js class PostController extends Controller { async create() { // 校验参数 - // 如果不传第二个参数会自动校验 `ctx.request.body` + // 如果不传第二个参数,会自动校验 `ctx.request.body` this.ctx.validate({ title: { type: 'string' }, - content: { type: 'string' }, + content: { type: 'string' } }); } } ``` -当校验异常时,会直接抛出一个异常,异常的状态码为 422,errors 字段包含了详细的验证不通过信息。如果想要自己处理检查的异常,可以通过 `try catch` 来自行捕获。 +校验异常时,会直接抛出异常,异常状态码为 422,`errors` 字段包含了详细的验证不通过信息。想要自行处理检查异常,可以通过 `try catch` 捕获。 ```js class PostController extends Controller { @@ -772,11 +760,11 @@ class PostController extends Controller { ### 校验规则 -参数校验通过 [Parameter](https://github.com/node-modules/parameter#rule) 完成,支持的校验规则可以在该模块的文档中查阅到。 +参数校验通过 [Parameter](https://github.com/node-modules/parameter#rule) 完成,支持的校验规则在模块文档中查询。 #### 自定义校验规则 -除了上一节介绍的内置检验类型外,有时候我们希望自定义一些校验规则,让开发时更便捷,此时可以通过 `app.validator.addRule(type, check)` 的方式新增自定义规则。 +除了上一节介绍的内置校验类型,有时需自定义校验规则,可以通过 `app.validator.addRule(type, check)` 新增自定义规则。 ```js // app.js @@ -784,18 +772,18 @@ app.validator.addRule('json', (rule, value) => { try { JSON.parse(value); } catch (err) { - return 'must be json string'; + return '必须是 JSON 字符串'; } }); ``` -添加完自定义规则之后,就可以在 Controller 中直接使用这条规则来进行参数校验了 +添加完自定义规则后,可在 Controller 中用这条规则进行参数校验。 ```js class PostController extends Controller { async handler() { const ctx = this.ctx; - // query.test 字段必须是 json 字符串 + // query.test 字段必须是 JSON 字符串 const rule = { test: 'json' }; ctx.validate(rule, ctx.query); } @@ -804,9 +792,9 @@ class PostController extends Controller { ## 调用 Service -我们并不想在 Controller 中实现太多业务逻辑,所以提供了一个 [Service](./service.md) 层进行业务逻辑的封装,这不仅能提高代码的复用性,同时可以让我们的业务逻辑更好测试。 +我们希望 Controller 中业务逻辑不太复杂,提供了 [Service](./service.md) 层,封装业务逻辑,提高代码复用性,便于测试。 -在 Controller 中可以调用任何一个 Service 上的任何方法,同时 Service 是懒加载的,只有当访问到它的时候框架才会去实例化它。 +Controller 可调用任何 Service 上的任何方法,Service 是懒加载的,只有使用时框架才实例化。 ```js class PostController extends Controller { @@ -814,7 +802,7 @@ class PostController extends Controller { const ctx = this.ctx; const author = ctx.session.userId; const req = Object.assign(ctx.request.body, { author }); - // 调用 service 进行业务处理 + // 调用 service 处理业务 const res = await ctx.service.post.create(req); ctx.body = { id: res.id }; ctx.status = 201; @@ -822,8 +810,7 @@ class PostController extends Controller { } ``` -Service 的具体写法,请查看 [Service](./service.md) 章节。 - +Service 具体写法,查看 [Service](./service.md) 章节。 ## 发送 HTTP 响应 当业务逻辑完成之后,Controller 的最后一个职责就是将业务逻辑的处理结果通过 HTTP 响应发送给用户。 @@ -832,7 +819,7 @@ Service 的具体写法,请查看 [Service](./service.md) 章节。 HTTP 设计了非常多的[状态码](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes),每一个状态码都代表了一个特定的含义,通过设置正确的状态码,可以让响应更符合语义。 -框架提供了一个便捷的 Setter 来进行状态码的设置 +框架提供了一个便捷的 Setter 来进行状态码的设置。 ```js class PostController extends Controller { @@ -847,12 +834,12 @@ class PostController extends Controller { ### 设置 body -绝大多数的数据都是通过 body 发送给请求方的,和请求中的 body 一样,在响应中发送的 body,也需要有配套的 Content-Type 告知客户端如何对数据进行解析。 +绝大部分的数据都是通过 body 发送给请求方的,同请求中的 body 一样,响应中发送的 body 也需要有配套的 Content-Type 告知客户端如何对数据进行解析。 -- 作为一个 RESTful 的 API 接口 controller,我们通常会返回 Content-Type 为 `application/json` 格式的 body,内容是一个 JSON 字符串。 -- 作为一个 html 页面的 controller,我们通常会返回 Content-Type 为 `text/html` 格式的 body,内容是 html 代码段。 +- 作为一个 RESTful 的 API 接口 controller 我们通常会返回 Content-Type 为 `application/json` 格式的 body,内容是一个 JSON 字符串。 +- 作为一个 html 页面的 controller 我们通常会返回 Content-Type 为 `text/html` 格式的 body,内容是 html 代码段。 -**注意:`ctx.body` 是 `ctx.response.body` 的简写,不要和 `ctx.request.body` 混淆了。** +**注意:`ctx.body` 是 `ctx.response.body` 的简写,不要与 `ctx.request.body` 混淆。** ```js class ViewController extends Controller { @@ -860,7 +847,7 @@ class ViewController extends Controller { this.ctx.body = { name: 'egg', category: 'framework', - language: 'Node.js', + language: 'Node.js' }; } @@ -870,14 +857,14 @@ class ViewController extends Controller { } ``` -由于 Node.js 的流式特性,我们还有很多场景需要通过 Stream 返回响应,例如返回一个大文件,代理服务器直接返回上游的内容,框架也支持直接将 body 设置成一个 Stream,并会同时处理好这个 Stream 上的错误事件。 +由于 Node.js 的流式特性,我们还有很多场景需要通过 Stream 返回响应,例如返回一个大文件,代理服务器直接返回上游的内容。框架也支持直接将 body 设置成一个 Stream,并会同时处理好这个 Stream 上的错误事件。 ```js class ProxyController extends Controller { async proxy() { const ctx = this.ctx; const result = await ctx.curl(url, { - streaming: true, + streaming: true }); ctx.set(result.header); // result.res 是一个 stream @@ -888,8 +875,7 @@ class ProxyController extends Controller { #### 渲染模板 -通常来说,我们不会手写 HTML 页面,而是会通过模板引擎进行生成。 -框架自身没有集成任何一个模板引擎,但是约定了 [View 插件的规范](../advanced/view-plugin.md),通过接入的模板引擎,可以直接使用 `ctx.render(template)` 来渲染模板生成 html。 +通常来说,我们不会手写 HTML 页面,而是通过模板引擎进行生成。框架自身没有集成任何一个模板引擎,但是约定了 [View 插件的规范](../advanced/view-plugin.md),通过接入的模板引擎,可以直接使用 `ctx.render(template)` 来渲染模板生成 html。 ```js class HomeController extends Controller { @@ -909,18 +895,18 @@ class HomeController extends Controller { 由于 JSONP 如果使用不当会导致非常多的安全问题,所以框架中提供了便捷的响应 JSONP 格式数据的方法,封装了 [JSONP XSS 相关的安全防范](../core/security.md#jsonp-xss),并支持进行 CSRF 校验和 referrer 校验。 -- 通过 `app.jsonp()` 提供的中间件来让一个 controller 支持响应 JSONP 格式的数据。在路由中,我们给需要支持 jsonp 的路由加上这个中间件: +- 通过 `app.jsonp()` 提供的中间件来让一个 controller 支持响应 JSONP 格式的数据。在路由中,我们给需要支持 JSONP 的路由加上这个中间件: ```js // app/router.js -module.exports = (app) => { +module.exports = app => { const jsonp = app.jsonp(); app.router.get('/api/posts/:id', jsonp, app.controller.posts.show); app.router.get('/api/posts', jsonp, app.controller.posts.list); }; ``` -- 在 Controller 中,只需要正常编写即可: +- 在 Controller 中,只需要按常规编写即可: ```js // app/controller/posts.js @@ -929,38 +915,38 @@ class PostController extends Controller { this.ctx.body = { name: 'egg', category: 'framework', - language: 'Node.js', + language: 'Node.js' }; } } ``` -用户请求对应的 URL 访问到这个 controller 的时候,如果 query 中有 `_callback=fn` 参数,将会返回 JSONP 格式的数据,否则返回 JSON 格式的数据。 +用户请求对应的 URL 访问到这个 controller 的时候,如果 query 中有 `_callback=fn` 参数,将会返回 JSONP 格式的数据;否则返回 JSON 格式的数据。 ##### JSONP 配置 -框架默认通过 query 中的 `_callback` 参数作为识别是否返回 JSONP 格式数据的依据,并且 `_callback` 中设置的方法名长度最多只允许 50 个字符。应用可以在 `config/config.default.js` 全局覆盖默认的配置: +框架默认通过 query 中的 `_callback` 参数作为识别是否返回 JSONP 格式数据的依据,并且 `_callback` 中设置的方法名长度最多只允许 50 个字符。应用可以在 `config/config.default.js` 中全局覆盖默认的配置: ```js // config/config.default.js exports.jsonp = { callback: 'callback', // 识别 query 中的 `callback` 参数 - limit: 100, // 函数名最长为 100 个字符 + limit: 100 // 函数名最长为 100 个字符 }; ``` -通过上面的方式配置之后,如果用户请求 `/api/posts/1?callback=fn`,响应为 JSONP 格式,如果用户请求 `/api/posts/1`,响应格式为 JSON。 +通过上述方式配置之后,如果用户请求 `/api/posts/1?callback=fn`,响应为 JSONP 格式;如果用户请求 `/api/posts/1`,响应格式为 JSON。 -我们同样可以在 `app.jsonp()` 创建中间件时覆盖默认的配置,以达到不同路由使用不同配置的目的: +我们同样可以在 `app.jsonp()` 创建中间件时覆盖默认的配置,以实现不同路由使用不同配置的目的: ```js // app/router.js -module.exports = (app) => { +module.exports = app => { const { router, controller, jsonp } = app; router.get( '/api/posts/:id', jsonp({ callback: 'callback' }), - controller.posts.show, + controller.posts.show ); router.get('/api/posts', jsonp({ callback: 'cb' }), controller.posts.list); }; @@ -968,104 +954,104 @@ module.exports = (app) => { ##### 跨站防御配置 -默认配置下,响应 JSONP 时不会进行任何跨站攻击的防范,在某些情况下,这是很危险的。我们初略将 JSONP 接口分为三种类型: +默认配置下,响应 JSONP 时不会进行任何跨站攻击的防范。在某些情况下,这是很危险的。我们初步将 JSONP 接口分为三种类型: 1. 查询非敏感数据,例如获取一个论坛的公开文章列表。 2. 查询敏感数据,例如获取一个用户的交易记录。 -3. 提交数据并修改数据库,例如给某一个用户创建一笔订单。 +3. 提交数据并修改数据库,例如为某一个用户创建一笔订单。 -如果我们的 JSONP 接口提供下面两类服务,在不做任何跨站防御的情况下,可能泄露用户敏感数据甚至导致用户被钓鱼。因此框架给 JSONP 默认提供了 CSRF 校验支持和 referrer 校验支持。 +如果我们的 JSONP 接口提供下面两类服务,在不做任何跨站防御的情况下,可能泄露用户敏感数据甚至导致用户被钓鱼。因此框架默认为 JSONP 提供了 CSRF 校验支持和 referrer 校验支持。 ###### CSRF -在 JSONP 配置中,我们只需要打开 `csrf: true`,即可对 JSONP 接口开启 CSRF 校验。 +在 JSONP 配置中,我们只需打开 `csrf: true`,即可对 JSONP 接口开启 CSRF 校验。 ```js // config/config.default.js module.exports = { jsonp: { - csrf: true, - }, + csrf: true + } }; ``` **注意,CSRF 校验依赖于 [security](../core/security.md) 插件提供的基于 Cookie 的 CSRF 校验。** -在开启 CSRF 校验时,客户端在发起 JSONP 请求时,也要带上 CSRF token,如果发起 JSONP 的请求方所在的页面和我们的服务在同一个主域名之下的话,可以读取到 Cookie 中的 CSRF token(在 CSRF token 缺失时也可以自行设置 CSRF token 到 Cookie 中),并在请求时带上该 token。 +在开启 CSRF 校验时,客户端在发起 JSONP 请求时,也应带上 CSRF token。如果发起 JSONP 的请求方所在的页面和我们的服务在同一个主域名之下,可以读取到 Cookie 中的 CSRF token(在 CSRF token 缺失时,也可以自行设置 CSRF token 到 Cookie 中),并在请求时携带该 token。 -##### referrer 校验 +##### Referrer 校验 -如果在同一个主域之下,可以通过开启 CSRF 的方式来校验 JSONP 请求的来源,而如果想对其他域名的网页提供 JSONP 服务,我们可以通过配置 referrer 白名单的方式来限制 JSONP 的请求方在可控范围之内。 +如果在同一个主域之下,可以通过开启 CSRF 的方式来校验 JSONP 请求的来源。而如果想对其他域名的网页提供 JSONP 服务,我们可以通过配置 referrer 白名单的方式来限制 JSONP 的请求方在可控范围之内。 -```js -//config/config.default.js +```javascript +// config/config.default.js exports.jsonp = { - whiteList: /^https?:\/\/test.com\//, - // whiteList: '.test.com', - // whiteList: 'sub.test.com', - // whiteList: [ 'sub.test.com', 'sub2.test.com' ], + whiteList: /^https?:\/\/test.com\// + // whiteList: '.test.com' + // whiteList: 'sub.test.com' + // whiteList: ['sub.test.com', 'sub2.test.com'] }; ``` `whiteList` 可以配置为正则表达式、字符串或者数组: -- 正则表达式:此时只有请求的 Referrer 匹配该正则时才允许访问 JSONP 接口。在设置正则表达式的时候,注意开头的 `^` 以及结尾的 `\/`,保证匹配到完整的域名。 +- 正则表达式:此时只有请求的 referrer 匹配该正则时才允许访问 JSONP 接口。在设置正则表达式的时候,注意开头的 `^` 和结尾的 `\/`,保证匹配到完整的域名。 -```js +```javascript exports.jsonp = { - whiteList: /^https?:\/\/test.com\//, + whiteList: /^https?:\/\/test.com\// }; -// matches referrer: +// Matches referrer: // https://test.com/hello // http://test.com/ ``` -- 字符串:设置字符串形式的白名单时分为两种,当字符串以 `.` 开头,例如 `.test.com` 时,代表 referrer 白名单为 `test.com` 的所有子域名,包括 `test.com` 自身。当字符串不以 `.` 开头,例如 `sub.test.com`,代表 referrer 白名单为 `sub.test.com` 这一个域名。(同时支持 HTTP 和 HTTPS)。 +- 字符串:设置字符串形式的白名单时分为两种。当字符串以 `.` 开头,例如 `.test.com` 时,代表 referrer 白名单为 `test.com` 的所有子域名,包括 `test.com` 自身。当字符串不以 `.` 开头,例如 `sub.test.com`,则代表 referrer 白名单为 `sub.test.com` 这一个域名。(同时支持 HTTP 和 HTTPS) -```js +```javascript exports.jsonp = { - whiteList: '.test.com', + whiteList: '.test.com' }; -// matches domain test.com: +// Matches domain test.com: // https://test.com/hello // http://test.com/ -// matches subdomain +// Matches subdomain // https://sub.test.com/hello // http://sub.sub.test.com/ exports.jsonp = { - whiteList: 'sub.test.com', + whiteList: 'sub.test.com' }; -// only matches domain sub.test.com: +// Only matches domain sub.test.com: // https://sub.test.com/hello // http://sub.test.com/ ``` - 数组:当设置的白名单为数组时,代表只要满足数组中任意一个元素的条件即可通过 referrer 校验。 -```js +```javascript exports.jsonp = { - whiteList: ['sub.test.com', 'sub2.test.com'], + whiteList: ['sub.test.com', 'sub2.test.com'] }; -// matches domain sub.test.com and sub2.test.com: +// Matches domain sub.test.com and sub2.test.com: // https://sub.test.com/hello // http://sub2.test.com/ ``` -**当 CSRF 和 referrer 校验同时开启时,请求发起方只需要满足任意一个条件即可通过 JSONP 的安全校验。** +**当 CSRF 和 referrer 校验同时开启时,请求发起方只需满足任意一个条件即可通过 JSONP 的安全校验。** ### 设置 Header -我们通过状态码标识请求成功与否、状态如何,在 body 中设置响应的内容。而通过响应的 Header,还可以设置一些扩展信息。 +我们通过状态码标识请求是否成功及其状态,而响应体(body)中则设置响应的内容。通过响应头(Header),我们还可以设置一些扩展信息。 -通过 `ctx.set(key, value)` 方法可以设置一个响应头,`ctx.set(headers)` 设置多个 Header。 +通过 `ctx.set(key, value)` 方法可以设置一个响应头,使用 `ctx.set(headers)` 设置多个 Header。 -```js +```javascript // app/controller/api.js class ProxyController extends Controller { async show() { - const ctx = this.ctx; + const { ctx } = this; const start = Date.now(); ctx.body = await ctx.service.post.get(); const used = Date.now() - start; @@ -1077,18 +1063,18 @@ class ProxyController extends Controller { ### 重定向 -框架通过 security 插件覆盖了 koa 原生的 `ctx.redirect` 实现,以提供更加安全的重定向。 +框架通过安全插件(security)覆盖了 Koa 原生的 `ctx.redirect` 实现,以增加重定向的安全性。 - `ctx.redirect(url)` 如果不在配置的白名单域名内,则禁止跳转。 -- `ctx.unsafeRedirect(url)` 不判断域名,直接跳转,一般不建议使用,明确了解可能带来的风险后使用。 +- `ctx.unsafeRedirect(url)` 不判断域名,直接跳转。不建议使用,除非已明确了解可能带来的风险。 -用户如果使用`ctx.redirect`方法,需要在应用的配置文件中做如下配置: +如果使用 `ctx.redirect` 方法,需要在应用的配置文件中进行如下配置: -```js +```javascript // config/config.default.js exports.security = { - domainWhiteList: ['.domain.com'], // 安全白名单,以 . 开头 + domainWhiteList: ['.domain.com'] // 安全白名单,以 "." 开头 }; ``` -若用户没有配置 `domainWhiteList` 或者 `domainWhiteList`数组内为空,则默认会对所有跳转请求放行,即等同于`ctx.unsafeRedirect(url)` +如果没有配置 `domainWhiteList` 或 `domainWhiteList` 数组为空,则默认允许所有跳转请求,等同于使用 `ctx.unsafeRedirect(url)`。 \ No newline at end of file diff --git a/site/docs/basics/env.zh-CN.md b/site/docs/basics/env.zh-CN.md index 634b46a130..f67b41b205 100644 --- a/site/docs/basics/env.zh-CN.md +++ b/site/docs/basics/env.zh-CN.md @@ -9,9 +9,9 @@ order: 3 框架有两种方式指定运行环境: -1. 通过  `config/env` 文件指定,该文件的内容就是运行环境,如 `prod`。一般通过构建工具来生成这个文件。 +1. 通过 `config/env` 文件指定,该文件的内容就是运行环境,如 `prod`。一般通过构建工具来生成这个文件。 -``` +```plaintext // config/env prod ``` @@ -24,32 +24,29 @@ EGG_SERVER_ENV=prod npm start ## 应用内获取运行环境 -框架提供了变量 `app.config.env` 来表示应用当前的运行环境。 - +框架提供了变量 `app.config.env`,来表示应用当前的运行环境。 ## 运行环境相关配置 不同的运行环境会对应不同的配置,具体请阅读 [Config 配置](./config.md)。 +## 与 `NODE_ENV` 的区别 -## 与 NODE_ENV 的区别 +很多 Node.js 应用会使用 `NODE_ENV` 来区分运行环境,但 `EGG_SERVER_ENV` 区分得更加精细。一般的项目开发流程包括本地开发环境、测试环境、生产环境等,除了本地开发环境和测试环境外,其他环境可统称为**服务器环境**。服务器环境的 `NODE_ENV` 应该为 `production`。而且 npm 也会使用这个变量,在应用部署时,一般不会安装 devDependencies,所以这个值也应该为 `production`。 -很多 Node.js 应用会使用 `NODE_ENV` 来区分运行环境,但 `EGG_SERVER_ENV` 区分得更加精细。一般的项目开发流程包括本地开发环境、测试环境、生产环境等,除了本地开发环境和测试环境外,其他环境可统称为**服务器环境**,服务器环境的 `NODE_ENV` 应该为 `production`。而且 npm 也会使用这个变量,在应用部署的时候一般不会安装 devDependencies,所以这个值也应该为 `production`。 +框架默认支持的运行环境及映射关系(如果未指定 `EGG_SERVER_ENV`,会根据 `NODE_ENV` 来匹配)如下表所示: -框架默认支持的运行环境及映射关系(如果未指定 `EGG_SERVER_ENV` 会根据 `NODE_ENV` 来匹配) - -| NODE_ENV | EGG_SERVER_ENV | 说明 | -| ---------- | -------------- | ------------ | -| | local | 本地开发环境 | -| test | unittest | 单元测试 | -| production | prod | 生产环境 | +| `NODE_ENV` | `EGG_SERVER_ENV` | 说明 | +|--------------|------------------|--------------| +| (不设置) | local | 本地开发环境 | +| test | unittest | 单元测试 | +| production | prod | 生产环境 | 例如,当 `NODE_ENV` 为 `production` 而 `EGG_SERVER_ENV` 未指定时,框架会将 `EGG_SERVER_ENV` 设置成 `prod`。 - ## 自定义环境 -常规开发流程可能不仅仅只有以上几种环境,Egg 支持自定义环境来适应自己的开发流程。 +Egg 框架支持开发者根据实际需要自定义开发环境。 -比如,要为开发流程增加集成测试环境 SIT。将 `EGG_SERVER_ENV` 设置成 `sit`(并建议设置 `NODE_ENV = production`),启动时会加载 `config/config.sit.js`,运行环境变量 `app.config.env` 会被设置成 `sit`。 +假如你需要在开发流程中加入 SIT 集成测试环境,只需将环境变量 `EGG_SERVER_ENV` 设为 `sit`。同时,建议设置 `NODE_ENV` 为 `production`,这样在启动项目时,Egg 会加载 `config/config.sit.js` 配置文件,并将运行时环境的 `app.config.env` 设为 `sit`。 ## 与 Koa 的区别 -在 Koa 中我们通过 `app.env` 来进行环境判断,`app.env` 默认的值是 `process.env.NODE_ENV`。但是在 Egg(和基于 Egg 的框架)中,配置统一都放置在 `app.config` 上,所以我们需要通过 `app.config.env` 来区分环境,`app.env` 不再使用。 +在 Koa 中,我们通过 `app.env` 来判断运行环境,其默认值为 `process.env.NODE_ENV`。然而在 Egg(及基于 Egg 的框架)中,配置统一放置于 `app.config`,因此需要通过 `app.config.env` 来区分环境。不再使用 `app.env`。 diff --git a/site/docs/basics/extend.zh-CN.md b/site/docs/basics/extend.zh-CN.md index 08c147b587..6ba5acf684 100644 --- a/site/docs/basics/extend.zh-CN.md +++ b/site/docs/basics/extend.zh-CN.md @@ -3,7 +3,7 @@ title: 框架扩展 order: 11 --- -框架提供了多种扩展点扩展自身的功能: +框架提供了多种扩展点,以扩展自身的功能: - Application - Context @@ -11,7 +11,7 @@ order: 11 - Response - Helper -在开发中,我们既可以使用已有的扩展 API 来方便开发,也可以对以上对象进行自定义扩展,进一步加强框架的功能。 +在开发中,我们既可以使用已有的扩展 API 来方便开发,也可以对以上对象进行自定义扩展,以进一步加强框架的功能。 ## Application @@ -20,19 +20,23 @@ order: 11 ### 访问方式 - `ctx.app` -- Controller,Middleware,Helper,Service 中都可以通过 `this.app` 访问到 Application 对象,例如 `this.app.config` 访问配置对象。 -- 在 `app.js` 中 `app` 对象会作为第一个参数注入到入口函数中 + + `ctx.app` 提供了一种访问全局 `app` 对象的方式。 - ```js - // app.js - module.exports = (app) => { - // 使用 app 对象 - }; - ``` +- Controller,Middleware,Helper,Service 中都可以通过 `this.app` 访问到 Application 对象。例如,通过 `this.app.config` 可以访问配置对象。 + +- 在 `app.js` 中,`app` 对象会作为第一个参数注入到入口函数中。 + +```js +// app.js +module.exports = app => { + // 使用 app 对象 +}; +``` ### 扩展方式 -框架会把 `app/extend/application.js` 中定义的对象与 Koa Application 的 prototype 对象进行合并,在应用启动时会基于扩展后的 prototype 生成 `app` 对象。 +框架会将 `app/extend/application.js` 中定义的对象与 Koa Application 的 prototype 对象进行合并。在应用启动时会基于扩展后的 prototype 生成 `app` 对象。 #### 方法扩展 @@ -49,11 +53,11 @@ module.exports = { #### 属性扩展 -一般来说属性的计算只需要进行一次,那么一定要实现缓存,否则在多次访问属性时会计算多次,这样会降低应用性能。 +通常,属性的计算只需执行一次。因此,需要实现缓存以免多次访问属性时重复计算,这会降低应用性能。 -推荐的方式是使用 Symbol + Getter 的模式。 +推荐使用 Symbol 加 Getter 的模式实现属性缓存。 -例如,增加一个 `app.bar` 属性 Getter: +例如,我们增加一个 `app.bar` 属性的 Getter: ```js // app/extend/application.js @@ -61,19 +65,18 @@ const BAR = Symbol('Application#bar'); module.exports = { get bar() { - // this 就是 app 对象,在其中可以调用 app 上的其他方法,或访问属性 + // this 是 app 对象,在其中可以调用 app 上的其他方法,或访问属性 if (!this[BAR]) { - // 实际情况肯定更复杂 + // 实际情况比这更复杂 this[BAR] = this.config.xx + this.config.yy; } return this[BAR]; }, }; ``` - ## Context -Context 指的是 Koa 的请求上下文,这是 **请求级别** 的对象,每次请求生成一个 Context 实例,通常我们也简写成 `ctx`。在所有的文档中,Context 和 `ctx` 都是指 Koa 的上下文对象。 +Context 指的是 Koa 的请求上下文,这是请求级别的对象,每次请求生成一个 Context 实例,通常我们也简写成 `ctx`。在所有的文档中,Context 和 `ctx` 都是指 Koa 的上下文对象。 ### 访问方式 @@ -83,7 +86,7 @@ Context 指的是 Koa 的请求上下文,这是 **请求级别** 的对象, ### 扩展方式 -框架会把 `app/extend/context.js` 中定义的对象与 Koa Context 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 ctx 对象。 +框架会将 `app/extend/context.js` 中定义的对象与 Koa Context 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 ctx 对象。 #### 方法扩展 @@ -94,13 +97,13 @@ Context 指的是 Koa 的请求上下文,这是 **请求级别** 的对象, module.exports = { foo(param) { // this 就是 ctx 对象,在其中可以调用 ctx 上的其他方法,或访问属性 - }, + } }; ``` #### 属性扩展 -一般来说属性的计算在同一次请求中只需要进行一次,那么一定要实现缓存,否则在同一次请求中多次访问属性时会计算多次,这样会降低应用性能。 +一般来说,属性的计算在同一次请求中只需要进行一次,那么一定要实现缓存,否则在同一次请求中多次访问属性时会计算多次,这样会降低应用性能。 推荐的方式是使用 Symbol + Getter 的模式。 @@ -118,11 +121,10 @@ module.exports = { this[BAR] = this.get('x-bar'); } return this[BAR]; - }, + } }; ``` - -## Request +## Request 对象 Request 对象和 Koa 的 Request 对象相同,是 **请求级别** 的对象,它提供了大量请求相关的属性和方法供使用。 @@ -132,13 +134,13 @@ Request 对象和 Koa 的 Request 对象相同,是 **请求级别** 的对象 ctx.request; ``` -`ctx` 上的很多属性和方法都被代理到 `request` 对象上,对于这些属性和方法使用 `ctx` 和使用 `request` 去访问它们是等价的,例如 `ctx.url === ctx.request.url`。 +`ctx` 上的很多属性和方法都被代理到 `request` 对象上,对于这些属性和方法使用 `ctx` 和使用 `request` 访问它们是等价的,例如 `ctx.url === ctx.request.url`。 -Koa 内置的代理 `request` 的属性和方法列表:[Koa - Request aliases](http://koajs.com/#request-aliases) +Koa 内置的代理 `request` 的属性和方法列表可参阅:[Koa - Request aliases](http://koajs.com/#request-aliases)。 ### 扩展方式 -框架会把 `app/extend/request.js` 中定义的对象与内置 `request` 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 `request` 对象。 +框架会将 `app/extend/request.js` 中定义的对象与内置 `request` 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 `request` 对象。 例如,增加一个 `request.foo` 属性 Getter: @@ -153,7 +155,7 @@ module.exports = { ## Response -Response 对象和 Koa 的 Response 对象相同,是 **请求级别** 的对象,它提供了大量响应相关的属性和方法供使用。 +Response 对象和 Koa 的 Response 对象相同,是 **请求级别** 的对象,提供了众多响应相关的属性和方法供使用。 ### 访问方式 @@ -161,15 +163,15 @@ Response 对象和 Koa 的 Response 对象相同,是 **请求级别** 的对 ctx.response; ``` -ctx 上的很多属性和方法都被代理到 `response` 对象上,对于这些属性和方法使用 `ctx` 和使用 `response` 去访问它们是等价的,例如 `ctx.status = 404` 和 `ctx.response.status = 404` 是等价的。 +`ctx` 上的许多属性和方法都代理到了 `response` 对象上,因此直接通过 `ctx` 访问这些属性和方法与通过 `response` 访问是等价的。例如,`ctx.status = 404` 和 `ctx.response.status = 404` 是等价的。 -Koa 内置的代理 `response` 的属性和方法列表:[Koa Response aliases](http://koajs.com/#response-aliases) +参考 Koa 官方文档中列出的内置代理 `response` 的属性和方法列表:[Koa Response aliases](http://koajs.com/#response-aliases)。 ### 扩展方式 -框架会把 `app/extend/response.js` 中定义的对象与内置 `response` 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 `response` 对象。 +框架会将 `app/extend/response.js` 中定义的对象与内置 `response` 的 prototype 对象合并,在处理请求时基于扩展后的 prototype 生成 `response` 对象。 -例如,增加一个 `response.foo` 属性 setter: +例如,要增加一个 `response.foo` 属性的 setter: ```js // app/extend/response.js @@ -180,13 +182,13 @@ module.exports = { }; ``` -就可以这样使用啦:`this.response.foo = 'bar';` +现在可以这样使用:`this.response.foo = 'bar';`。 ## Helper Helper 函数用来提供一些实用的 utility 函数。 -它的作用在于我们可以将一些常用的动作抽离在 helper.js 里面成为一个独立的函数,这样可以用 JavaScript 来写复杂的逻辑,避免逻辑分散各处。另外还有一个好处是 Helper 这样一个简单的函数,可以让我们更容易编写测试用例。 +它的作用在于我们可以将一些常用的动作抽离在 `helper.js` 里面成为一个独立的函数,这样可以用 JavaScript 来写复杂的逻辑,避免逻辑分散各处。另外还有一个好处是 Helper 这样一个简单的函数,可以让我们更容易编写测试用例。 框架内置了一些常用的 Helper 函数。我们也可以编写自定义的 Helper 函数。 @@ -222,7 +224,7 @@ module.exports = { ## 按照环境进行扩展 -另外,还可以根据环境进行有选择的扩展,例如,只在 unittest 环境中提供 `mockXX()` 方法以便进行 mock 方便测试。 +在 `unittest` 环境中,你可以选择性地扩展应用程序。例如,只在 `unittest` 现场提供 `mockXX()` 方法,便于进行 mock 测试。 ```js // app/extend/application.unittest.js @@ -231,6 +233,4 @@ module.exports = { }; ``` -这个文件只会在 unittest 环境加载。 - -同理,对于 Application,Context,Request,Response,Helper 都可以使用这种方式针对某个环境进行扩展,更多参见[运行环境](./env.md)。 +这个文件只会在 `unittest` 环境加载。同理,Application、Context、Request、Response 和 Helper 都可以使用这种方式针对某个特定环境进行扩展。更多信息,请参阅[运行环境](./env.md)。 diff --git a/site/docs/basics/middleware.zh-CN.md b/site/docs/basics/middleware.zh-CN.md index 78a981d0d7..ac53170237 100644 --- a/site/docs/basics/middleware.zh-CN.md +++ b/site/docs/basics/middleware.zh-CN.md @@ -9,7 +9,7 @@ order: 5 ### 写法 -我们先来通过编写一个简单的 gzip 中间件,来看看中间件的写法。 +我们首先来通过编写一个简单的 gzip 中间件,了解中间件的写法。 ```js // app/middleware/gzip.js @@ -19,7 +19,7 @@ const zlib = require('zlib'); async function gzip(ctx, next) { await next(); - // 后续中间件执行完成后将响应体转换成 gzip + // 后续中间件执行完成后,将响应体转换成 gzip let body = ctx.body; if (!body) return; if (isJSON(body)) body = JSON.stringify(body); @@ -36,12 +36,12 @@ async function gzip(ctx, next) { ### 配置 -一般来说中间件也会有自己的配置。在框架中,一个完整的中间件是包含了配置处理的。我们约定一个中间件是一个放置在 `app/middleware` 目录下的单独文件,它需要 exports 一个普通的 function,接受两个参数: +一般来说,中间件也会有自己的配置。在框架中,一个完整的中间件包含了配置处理。我们约定一个中间件是一个放置于 `app/middleware` 目录下的单独文件。它需要 `exports` 一个普通的函数,接受两个参数: -- options: 中间件的配置项,框架会将 `app.config[${middlewareName}]` 传递进来。 -- app: 当前应用 Application 的实例。 +- `options`:中间件的配置项,框架会将 `app.config[${middlewareName}]` 传递进来。 +- `app`:当前应用 `Application` 的实例。 -我们将上面的 gzip 中间件做一个简单的优化,让它支持指定只有当 body 大于配置的 threshold 时才进行 gzip 压缩,我们要在 `app/middleware` 目录下新建一个文件 `gzip.js` +下面我们对上文中的 gzip 中间件做一个简单的优化,使其支持指定只有当体积大于配置的 `threshold` 时才进行 gzip 压缩。我们在 `app/middleware` 目录下新建 `gzip.js` 文件。 ```js // app/middleware/gzip.js @@ -52,7 +52,7 @@ module.exports = (options) => { return async function gzip(ctx, next) { await next(); - // 后续中间件执行完成后将响应体转换成 gzip + // 后续中间件执行完成后,将响应体转换成 gzip let body = ctx.body; if (!body) return; @@ -69,7 +69,6 @@ module.exports = (options) => { }; }; ``` - ## 使用中间件 中间件编写完成后,我们还需要手动挂载,支持以下方式: @@ -87,8 +86,8 @@ module.exports = { // 配置 gzip 中间件的配置 gzip: { - threshold: 1024, // 小于 1k 的响应体不压缩 - }, + threshold: 1024 // 小于 1k 的响应体不压缩 + } }; ``` @@ -100,14 +99,14 @@ module.exports = { ```js // app.js -module.exports = (app) => { +module.exports = app => { // 在中间件最前面统计请求时间 app.config.coreMiddleware.unshift('report'); }; // app/middleware/report.js module.exports = () => { - return async function (ctx, next) { + return async function(ctx, next) { const startTime = Date.now(); await next(); // 上报请求时间 @@ -118,21 +117,20 @@ module.exports = () => { 应用层定义的中间件(`app.config.appMiddleware`)和框架默认中间件(`app.config.coreMiddleware`)都会被加载器加载,并挂载到 `app.middleware` 上。 -### router 中使用中间件 +### 在 router 中使用中间件 以上两种方式配置的中间件是全局的,会处理每一次请求。 如果你只想针对单个路由生效,可以直接在 `app/router.js` 中实例化和挂载,如下: ```js -module.exports = (app) => { +module.exports = app => { const gzip = app.middleware.gzip({ threshold: 1024 }); app.router.get('/needgzip', gzip, app.controller.handler); }; ``` - ## 框架默认中间件 -除了应用层加载中间件之外,框架自身和其他的插件也会加载许多中间件。所有的这些自带中间件的配置项都通过在配置中修改中间件同名配置项进行修改,例如[框架自带的中间件](https://github.com/eggjs/egg/tree/master/app/middleware)中有一个 bodyParser 中间件(框架的加载器会将文件名中的各种分隔符都修改成驼峰形式的变量名),我们想要修改 bodyParser 的配置,只需要在 `config/config.default.js` 中编写 +除了应用层加载中间件之外,框架自身和其他插件也会加载许多中间件。所有这些自带中间件的配置项都可以通过修改配置文件中的同名配置项来进行更改。例如,框架自带的中间件列表中有一个名为 `bodyParser` 的中间件(框架的加载器会将文件名中的分隔符都转换为驼峰形式的变量名)。如果我们想要修改 `bodyParser` 的配置,只需要在 `config/config.default.js` 中编写如下内容: ```js module.exports = { @@ -142,19 +140,18 @@ module.exports = { }; ``` -**注意:框架和插件加载的中间件会在应用层配置的中间件之前,框架默认中间件不能被应用层中间件覆盖,如果应用层有自定义同名中间件,在启动时会报错。** - +**注意:框架和插件加载的中间件会在应用层配置的中间件之前被加载。框架默认中间件不能被应用层中间件覆盖。如果应用层有自定义同名中间件,启动时将会报错。** ## 使用 Koa 的中间件 -在框架里面可以非常容易的引入 Koa 中间件生态。 +在框架里面可以非常容易地引入 Koa 中间件生态。 -以 [koa-compress](https://github.com/koajs/compress) 为例,在 Koa 中使用时: +以 [`koa-compress`](https://github.com/koajs/compress) 为例,在 Koa 中使用时: ```js const koa = require('koa'); const compress = require('koa-compress'); -const app = koa(); +const app = new koa(); const options = { threshold: 2048 }; app.use(compress(options)); @@ -164,7 +161,7 @@ app.use(compress(options)); ```js // app/middleware/compress.js -// koa-compress 暴露的接口(`(options) => middleware`)和框架对中间件要求一致 +// koa-compress 暴露的接口(`(options) => middleware`)和框架对中间件要求一致 module.exports = require('koa-compress'); ``` @@ -196,18 +193,17 @@ module.exports = (options, app) => { return webpackMiddleware(options.compiler, options.others); }; ``` - ## 通用配置 无论是应用层加载的中间件还是框架自带中间件,都支持几个通用的配置项: -- enable:控制中间件是否开启。 -- match:设置只有符合某些规则的请求才会经过这个中间件。 -- ignore:设置符合某些规则的请求不经过这个中间件。 +- `enable`:控制中间件是否开启。 +- `match`:设置只有符合某些规则的请求才会经过这个中间件。 +- `ignore`:设置符合某些规则的请求不经过这个中间件。 ### enable -如果我们的应用并不需要默认的 bodyParser 中间件来进行请求体的解析,此时我们可以通过配置 enable 为 false 来关闭它 +如果我们的应用并不需要默认的 `bodyParser` 中间件来进行请求体的解析,此时我们可以通过配置 `enable` 为 `false` 来关闭它。 ```js module.exports = { @@ -219,9 +215,9 @@ module.exports = { ### match 和 ignore -match 和 ignore 支持的参数都一样,只是作用完全相反,match 和 ignore 不允许同时配置。 +`match` 和 `ignore` 支持的参数都一样,只是作用完全相反,`match` 和 `ignore` 不允许同时配置。 -如果我们想让 gzip 只针对 `/static` 前缀开头的 url 请求开启,我们可以配置 match 选项 +如果我们想让 `gzip` 只针对 `/static` 前缀开头的 url 请求开启,我们可以配置 `match` 选项。 ```js module.exports = { @@ -231,18 +227,17 @@ module.exports = { }; ``` -match 和 ignore 支持多种类型的配置方式 +`match` 和 `ignore` 支持多种类型的配置方式: -1. 字符串:当参数为字符串类型时,配置的是一个 url 的路径前缀,所有以配置的字符串作为前缀的 url 都会匹配上。 - 当然,你也可以直接使用字符串数组。 +1. 字符串:当参数为字符串类型时,配置的是一个 url 的路径前缀,所有以配置的字符串作为前缀的 url 都会匹配上。当然,你也可以直接使用字符串数组。 2. 正则:当参数为正则时,直接匹配满足正则验证的 url 的路径。 -3. 函数:当参数为一个函数时,会将请求上下文传递给这个函数,最终取函数返回的结果(true/false)来判断是否匹配。 +3. 函数:当参数为一个函数时,会将请求上下文传递给这个函数,最终取函数返回的结果(`true`/`false`)来判断是否匹配。 ```js module.exports = { gzip: { match(ctx) { - // 只有 ios 设备才开启 + // 只有 iOS 设备才开启 const reg = /iphone|ipad|ipod/i; return reg.test(ctx.get('user-agent')); }, @@ -250,5 +245,4 @@ module.exports = { }; ``` -有关更多的 match 和 ignore 配置情况,详见 -[egg-path-matching](https://github.com/eggjs/egg-path-matching). +有关更多的 `match` 和 `ignore` 配置情况,详见 [egg-path-matching](https://github.com/eggjs/egg-path-matching)。 diff --git a/site/docs/basics/objects.zh-CN.md b/site/docs/basics/objects.zh-CN.md index 0e557a6660..5b6ac0a4d7 100644 --- a/site/docs/basics/objects.zh-CN.md +++ b/site/docs/basics/objects.zh-CN.md @@ -3,46 +3,46 @@ title: 框架内置基础对象 order: 2 --- -在本章,我们会初步介绍一下框架中内置的一些基础对象,包括从 [Koa] 继承而来的 4 个对象(Application, Context, Request, Response) 以及框架扩展的一些对象(Controller, Service, Helper, Config, Logger),在后续的文档阅读中我们会经常遇到它们。 +在本章中,我们将初步了解框架内部内置的一些基础对象。这些对象包括从 [Koa] 继承而来的 4 个对象(`Application`,`Context`,`Request`,`Response`)以及框架扩展的其他一些对象(`Controller`,`Service`,`Helper`,`Config`,`Logger`)。在后续的文档中,我们会经常遇到它们。 ## Application -Application 是全局应用对象。在一个应用中,每个进程只会实例化一个 Application 实例。它继承自 [Koa.Application],在它上面我们可以挂载一些全局的方法和对象。我们可以轻松的在插件或者应用中[扩展 Application 对象](./extend.md#Application)。 +Application 是全局应用对象。在一个应用中,每个进程只会实例化一个 Application 实例。它继承自 `Koa.Application`,在其上我们可以挂载一些全局的方法和对象。我们可以轻易地在插件或应用中[扩展 Application 对象](./extend.md#Application)。 ### 事件 -在框架运行时,会在 Application 实例上触发一些事件,应用开发者或者插件开发者可以监听这些事件做一些操作。作为应用开发者,我们一般会在[启动自定义脚本](./app-start.md)中进行监听。 +在框架运行时,会在 Application 实例上触发一些事件,应用开发者或插件开发者可以监听这些事件做一些操作。作为应用开发者,我们一般会在[启动自定义脚本](./app-start.md)中进行监听: -- `server`: 该事件一个 worker 进程只会触发一次,在 HTTP 服务完成启动后,会将 HTTP server 通过这个事件暴露出来给开发者。 -- `error`: 运行时有任何的异常被 onerror 插件捕获后,都会触发 `error` 事件,将错误对象和关联的上下文(如果有)暴露给开发者,可以进行自定义的日志记录上报等处理。 -- `request` 和 `response`: 应用收到请求和响应请求时,分别会触发 `request` 和 `response` 事件,并将当前请求上下文暴露出来,开发者可以监听这两个事件来进行日志记录。 +- `server`:该事件在一个 worker 进程中只会触发一次,在 HTTP 服务完成启动后,会通过这个事件将 HTTP server 暴露出来给开发者。 +- `error`:运行时捕获到任何异常后,都会触发 `error` 事件,将错误对象和关联的上下文(如果有)暴露出来,开发者可以对其进行自定义的日志记录、上报等处理。 +- `request` 和 `response`:应用收到请求和响应请求时,分别会触发 `request` 和 `response` 事件,并将当前请求的上下文暴露出来,开发者可以监听这两个事件进行日志记录。 ```js // app.js module.exports = (app) => { app.once('server', (server) => { - // websocket + // websocket 相关操作 }); app.on('error', (err, ctx) => { - // report error + // 上报错误 }); app.on('request', (ctx) => { - // log receive request + // 记录收到的请求 }); app.on('response', (ctx) => { - // ctx.starttime is set by framework + // ctx.starttime 是由框架设置的 const used = Date.now() - ctx.starttime; - // log total cost + // 记录请求总耗时 }); }; ``` ### 获取方式 -Application 对象几乎可以在编写应用时的任何一个地方获取到,下面介绍几个经常用到的获取方式: +Application 对象在编写应用时几乎任何场合都能获取到,下面介绍几个常用的获取方式: -几乎所有被框架 [Loader](../advanced/loader.md) 加载的文件(Controller,Service,Schedule 等),都可以 export 一个函数,这个函数会被 Loader 调用,并使用 app 作为参数: +几乎所有由框架 `Loader` 加载的文件(Controller、Service、Schedule 等),都可以通过导出一个函数来获取 Application 实例,该函数会被 `Loader` 调用,并将 app 作为参数: - [启动自定义脚本](./app-start.md) @@ -64,7 +64,7 @@ Application 对象几乎可以在编写应用时的任何一个地方获取到 } ``` -和 [Koa] 一样,在 Context 对象上,可以通过 `ctx.app` 访问到 Application 对象。以上面的 Controller 文件举例: +与 `Koa` 一样,在 Context 对象上,可以通过 `ctx.app` 访问到 Application 对象。以 Controller 文件为例: ```js // app/controller/user.js @@ -75,7 +75,7 @@ class UserController extends Controller { } ``` -在继承于 Controller, Service 基类的实例中,可以通过 `this.app` 访问到 Application 对象。 +在继承自 Controller、Service 基类的实例中,可以通过 `this.app` 访问到 Application 对象。 ```js // app/controller/user.js @@ -85,46 +85,45 @@ class UserController extends Controller { } } ``` - ## Context -Context 是一个**请求级别的对象**,继承自 [Koa.Context]。在每一次收到用户请求时,框架会实例化一个 Context 对象,这个对象封装了这次用户请求的信息,并提供了许多便捷的方法来获取请求参数或者设置响应信息。框架会将所有的 [Service] 挂载到 Context 实例上,一些插件也会将一些其他的方法和对象挂载到它上面([egg-sequelize] 会将所有的 model 挂载在 Context 上)。 +Context 是一个**请求级别的对象**,继承自 [Koa.Context]。在每次收到用户请求时,框架都会实例化一个 Context 对象。这个对象封装了这次用户请求的信息,并提供了许多便捷的方法来获取请求参数或者设置响应信息。框架会将所有的 [Service] 挂载到 Context 实例上。部分插件也会将其他方法和对象挂载至其上(如 [egg-sequelize] 会将所有的 model 挂到 Context 上)。 ### 获取方式 -最常见的 Context 实例获取方式是在 [Middleware], [Controller] 以及 [Service] 中。Controller 中的获取方式在上面的例子中已经展示过了,在 Service 中获取和 Controller 中获取的方式一样,在 Middleware 中获取 Context 实例则和 [Koa] 框架在中间件中获取 Context 对象的方式一致。 +获取 Context 实例的最常见方式是在 [Middleware]、[Controller] 以及 [Service] 中。我们已经在 Controller 的示例中看到了相应的获取方式。在 Service 中获取 Context 的方法与 Controller 中相同,在 Middleware 中获取 Context 实例的方法则与 [Koa] 框架的中间件中使用方法一致。 -框架的 [Middleware] 同时支持 Koa v1 和 Koa v2 两种不同的中间件写法,根据不同的写法,获取 Context 实例的方式也稍有不同: +框架的 [Middleware] 支持 Koa v1 和 Koa v2 两种中间件的写法。根据不同的写法,获取 Context 实例的方式略有区别: ```js // Koa v1 function* middleware(next) { - // this is instance of Context + // this 为 Context 的实例 console.log(this.query); yield next; } // Koa v2 async function middleware(ctx, next) { - // ctx is instance of Context + // ctx 为 Context 的实例 console.log(ctx.query); } ``` -除了在请求时可以获取 Context 实例之外, 在有些非用户请求的场景下我们需要访问 service / model 等 Context 实例上的对象,我们可以通过 `Application.createAnonymousContext()` 方法创建一个匿名 Context 实例: +除了在处理请求时可以获得 Context 实例外,还有一些非用户请求的场景下需要访问 service / model 等 Context 实例上的对象。这时我们可以通过 `Application.createAnonymousContext()` 方法创建一个匿名 Context 实例: ```js // app.js module.exports = (app) => { app.beforeStart(async () => { const ctx = app.createAnonymousContext(); - // preload before app start + // 应用启动前预加载 await ctx.service.posts.load(); }); }; ``` -在[定时任务](./schedule.md)中的每一个 task 都接受一个 Context 实例作为参数,以便我们更方便的执行一些定时的业务逻辑: +在[定时任务](./schedule.md)中,每个 task 都接收一个 Context 实例作为参数,这样我们可以更便利地执行一些定时的业务逻辑: ```js // app/schedule/refresh.js @@ -132,12 +131,11 @@ exports.task = async (ctx) => { await ctx.service.posts.refresh(); }; ``` - ## Request & Response -Request 是一个**请求级别的对象**,继承自 [Koa.Request]。封装了 Node.js 原生的 HTTP Request 对象,提供了一系列辅助方法获取 HTTP 请求常用参数。 +Request 是一个**请求级别的对象**,继承自 `[Koa.Request]`。封装了 Node.js 原生的 HTTP Request 对象,提供了一系列辅助方法获取 HTTP 请求常用参数。 -Response 是一个**请求级别的对象**,继承自 [Koa.Response]。封装了 Node.js 原生的 HTTP Response 对象,提供了一系列辅助方法设置 HTTP 响应。 +Response 是一个**请求级别的对象**,继承自 `[Koa.Response]`。封装了 Node.js 原生的 HTTP Response 对象,提供了一系列辅助方法设置 HTTP 响应。 ### 获取方式 @@ -148,27 +146,28 @@ Response 是一个**请求级别的对象**,继承自 [Koa.Response]。封装 class UserController extends Controller { async fetch() { const { app, ctx } = this; + // 获取请求中的 `id` 参数 const id = ctx.request.query.id; + // 设置响应体 ctx.response.body = app.cache.get(id); } } ``` -- [Koa] 会在 Context 上代理一部分 Request 和 Response 上的方法和属性,参见 [Koa.Context]。 -- 如上面例子中的 `ctx.request.query.id` 和 `ctx.query.id` 是等价的,`ctx.response.body=` 和 `ctx.body=` 是等价的。 +- `[Koa]` 会在 Context 上代理一部分 Request 和 Response 上的方法和属性,参见 `[Koa.Context]`。 +- 如上面例子中的 `ctx.request.query.id` 和 `ctx.query.id` 是等价的,`ctx.response.body =` 和 `ctx.body =` 也是等价的。 - 需要注意的是,获取 POST 的 body 应该使用 `ctx.request.body`,而不是 `ctx.body`。 - ## Controller -框架提供了一个 Controller 基类,并推荐所有的 [Controller] 都继承于该基类实现。这个 Controller 基类有下列属性: +框架提供了一个 Controller 基类,并推荐所有的 `Controller` 都继承于该基类实现。这个 Controller 基类有下列属性: -- `ctx` - 当前请求的 [Context](#context) 实例。 -- `app` - 应用的 [Application](#application) 实例。 +- `ctx` - 当前请求的 `Context` 实例。 +- `app` - 应用的 `Application` 实例。 - `config` - 应用的[配置](./config.md)。 -- `service` - 应用所有的 [service](./service.md)。 -- `logger` - 为当前 controller 封装的 logger 对象。 +- `service` - 应用的所有 [service](./service.md)。 +- `logger` - 为当前 `Controller` 封装的 `logger` 对象。 -在 Controller 文件中,可以通过两种方式来引用 Controller 基类: +在 `Controller` 文件中,可以通过两种方式来引用 Controller 基类: ```js // app/controller/user.js @@ -176,21 +175,20 @@ class UserController extends Controller { // 从 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 + // 实现 }; }; ``` - ## Service -框架提供了一个 Service 基类,并推荐所有的 [Service] 都继承于该基类实现。 +框架提供了一个 Service 基类,并推荐所有的 Service 都继承于该基类实现。 Service 基类的属性和 [Controller](#controller) 基类属性一致,访问方式也类似: @@ -211,16 +209,15 @@ module.exports = (app) => { }; }; ``` - ## Helper -Helper 用来提供一些实用的 utility 函数。它的作用在于我们可以将一些常用的动作抽离在 helper.js 里面成为一个独立的函数,这样可以用 JavaScript 来写复杂的逻辑,避免逻辑分散各处,同时可以更好的编写测试用例。 +Helper 用来提供一些实用的 utility 函数。它的作用在于我们可以将一些常用的动作抽离在 `helper.js` 里面成为一个独立的函数。这样可以利用 JavaScript 编写复杂的逻辑,避免逻辑分散于各个地方,同时便于更好地编写测试用例。 -Helper 自身是一个类,有和 [Controller](#controller) 基类一样的属性,它也会在每次请求时进行实例化,因此 Helper 上的所有函数也能获取到当前请求相关的上下文信息。 +Helper 本身是一个类,具有和 `Controller` 基类相同的属性,它也会在每次请求时进行实例化。因此,Helper 上的所有函数也能获取到当前请求相关的上下文信息。 ### 获取方式 -可以在 Context 的实例上获取到当前请求的 Helper(`ctx.helper`) 实例。 +可以在 Context 的实例上获取到当前请求的 Helper(`ctx.helper`)实例。 ```js // app/controller/user.js @@ -234,16 +231,16 @@ class UserController extends Controller { } ``` -除此之外,Helper 的实例还可以在模板中获取到,例如可以在模板中获取到 [security](../core/security.md) 插件提供的 `shtml` 方法。 +除此之外,Helper 的实例还可以在模板中获取。例如,可以在模板中使用 [security](../core/security.md) 插件提供的 `shtml` 方法。 -``` -// app/view/home.nj +```html + {{ helper.shtml(value) }} ``` -### 自定义 helper 方法 +### 自定义 Helper 方法 -应用开发中,我们可能经常要自定义一些 helper 方法,例如上面例子中的 `formatUser`,我们可以通过[框架扩展](./extend.md#helper)的形式来自定义 helper 方法。 +在应用开发中,我们可能经常需要自定义一些 Helper 方法。例如上面例子中的 `formatUser`,我们可以通过 [框架扩展](./extend.md#helper) 的形式来自定义 Helper 方法。 ```js // app/extend/helper.js @@ -253,51 +250,49 @@ module.exports = { }, }; ``` - ## Config -我们推荐应用开发遵循配置和代码分离的原则,将一些需要硬编码的业务配置都放到配置文件中,同时配置文件支持各个不同的运行环境使用不同的配置,使用起来也非常方便,所有框架、插件和应用级别的配置都可以通过 Config 对象获取到,关于框架的配置,可以详细阅读 [Config 配置](./config.md)章节。 +我们推荐应用开发遵循配置和代码分离的原则,将一些需要硬编码的业务配置都放到配置文件中。同时,配置文件支持各个不同的运行环境使用不同的配置,使用起来也非常方便。所有框架、插件和应用级别的配置都可以通过 `Config` 对象获取到。关于框架的配置,可以详细阅读[Config 配置](./config.md)章节。 ### 获取方式 -我们可以通过 `app.config` 从 Application 实例上获取到 config 对象,也可以在 Controller, Service, Helper 的实例上通过 `this.config` 获取到 config 对象。 - +我们可以通过 `app.config` 从 `Application` 实例上获取到 `config` 对象,也可以在 Controller、Service、Helper 的实例上通过 `this.config` 获取到 `config` 对象。 ## Logger -框架内置了功能强大的[日志功能](../core/logger.md),可以非常方便的打印各种级别的日志到对应的日志文件中,每一个 logger 对象都提供了 4 个级别的方法: +框架内置了功能强大的[日志功能](../core/logger.md),可以非常方便地打印各种级别的日志到对应的日志文件中,每一个 logger 对象都提供了 4 个级别的方法: - `logger.debug()` - `logger.info()` - `logger.warn()` - `logger.error()` -在框架中提供了多个 Logger 对象,下面我们简单的介绍一下各个 Logger 对象的获取方式和使用场景。 +在框架中提供了多个 Logger 对象,下面我们简单地介绍一下各个 Logger 对象的获取方式和使用场景。 ### App Logger -我们可以通过 `app.logger` 来获取到它,如果我们想做一些应用级别的日志记录,如记录启动阶段的一些数据信息,记录一些业务上与请求无关的信息,都可以通过 App Logger 来完成。 +我们可以通过 `app.logger` 来获取它。如果我们想做一些应用级别的日志记录,如记录启动阶段的一些数据信息,记录一些与请求无关的业务信息,都可以通过 App Logger 来完成。 ### App CoreLogger -我们可以通过 `app.coreLogger` 来获取到它,一般我们在开发应用时都不应该通过 CoreLogger 打印日志,而框架和插件则需要通过它来打印应用级别的日志,这样可以更清晰的区分应用和框架打印的日志,通过 CoreLogger 打印的日志会放到和 Logger 不同的文件中。 +我们可以通过 `app.coreLogger` 来获取它。一般在开发应用时,我们不应该通过 CoreLogger 打印日志。而框架和插件则需要通过它来打印应用级别的日志,这样可以更清晰地区分应用和框架打印的日志。通过 CoreLogger 打印的日志会被放到与 Logger 不同的文件中。 ### Context Logger -我们可以通过 `ctx.logger` 从 Context 实例上获取到它,从访问方式上我们可以看出来,Context Logger 一定是与请求相关的,它打印的日志都会在前面带上一些当前请求相关的信息(如 `[$userId/$ip/$traceId/${cost}ms $method $url]`),通过这些信息,我们可以从日志快速定位请求,并串联一次请求中的所有的日志。 +我们可以从 Context 实例上,通过 `ctx.logger` 获取它。从访问方式上我们可以看出,Context Logger 一定是与请求相关的。它打印的日志都会在前面带上一些当前请求相关的信息(如 `[$userId/$ip/$traceId/${cost}ms $method $url]`)。通过这些信息,我们可以从日志快速定位请求,并串联一次请求中的所有日志。 ### Context CoreLogger -我们可以通过 `ctx.coreLogger` 获取到它,和 Context Logger 的区别是一般只有插件和框架会通过它来记录日志。 +我们可以通过 `ctx.coreLogger` 获取它。它与 Context Logger 的区别在于,一般只有插件和框架会通过它来记录日志。 -### Controller Logger & Service Logger +### Controller Logger 和 Service Logger -我们可以在 Controller 和 Service 实例上通过 `this.logger` 获取到它们,它们本质上就是一个 Context Logger,不过在打印日志的时候还会额外的加上文件路径,方便定位日志的打印位置。 +我们可以在 Controller 和 Service 实例上,通过 `this.logger` 获取它们。它们实质上就是一个 Context Logger,不过在打印日志时,还会额外地加上文件路径,以方便定位日志的打印位置。 -## Subscription +## 订阅模型 -订阅模型是一种比较常见的开发模式,譬如消息中间件的消费者或调度任务。因此我们提供了 Subscription 基类来规范化这个模式。 +订阅模型是一种比较常见的开发模式,例如消息中间件的消费者或调度任务。因此,我们提供了 `Subscription` 基类来规范化这个模式。 -可以通过以下方式来引用 Subscription 基类: +你可以通过以下方式来引用 `Subscription` 基类: ```js const Subscription = require('egg').Subscription; @@ -309,14 +304,15 @@ class Schedule extends Subscription { } ``` -插件开发者可以根据自己的需求基于它定制订阅规范,如[定时任务](./schedule.md)就是使用这种规范实现的。 - -[koa]: http://koajs.com -[koa.application]: http://koajs.com/#application -[koa.context]: http://koajs.com/#context -[koa.request]: http://koajs.com/#request -[koa.response]: http://koajs.com/#response -[egg-sequelize]: https://github.com/eggjs/egg-sequelize -[middleware]: ./middleware.md -[controller]: ./controller.md -[service]: ./service.md +插件开发者可以根据自己的需求,基于它定制订阅规范,例如定时任务就是使用这种规范实现的。 + +相关链接: +- [koa](http://koajs.com) +- [koa.application](http://koajs.com/#application) +- [koa.context](http://koajs.com/#context) +- [koa.request](http://koajs.com/#request) +- [koa.response](http://koajs.com/#response) +- [egg-sequelize](https://github.com/eggjs/egg-sequelize) +- [中间件(middleware)](./middleware.md) +- [控制器(controller)](./controller.md) +- [服务(service)](./service.md) diff --git a/site/docs/basics/plugin.zh-CN.md b/site/docs/basics/plugin.zh-CN.md index fe59fabf67..d2aaa8b67d 100644 --- a/site/docs/basics/plugin.zh-CN.md +++ b/site/docs/basics/plugin.zh-CN.md @@ -3,16 +3,14 @@ title: 插件 order: 9 --- -插件机制是我们框架的一大特色。它不但可以保证框架核心的足够精简、稳定、高效,还可以促进业务逻辑的复用,生态圈的形成。有人可能会问了 +插件机制是我们框架的一大特色。它不但可以保证框架核心的足够精简、稳定、高效,还可以促进业务逻辑的复用,生态圈的形成。有人可能会问了: -- Koa 已经有了中间件的机制,为啥还要插件呢? -- 中间件、插件、应用它们之间是什么关系,有什么区别? -- 我该怎么使用一个插件? +- Koa 已经有了中间件的机制,为什么还要插件呢? +- 中间件、插件、应用之间是什么关系,它们之间有什么区别? +- 我该如何使用一个插件? - 如何编写一个插件? -- ... - -接下来我们就来逐一讨论 +接下来我们就来逐一讨论。 ## 为什么要插件 我们在使用 Koa 中间件过程中发现了下面一些问题: @@ -25,28 +23,27 @@ order: 9 ### 中间件、插件、应用的关系 -一个插件其实就是一个『迷你的应用』,和应用(app)几乎一样: +一个插件其实就是一个“迷你的应用”,和应用(app)几乎一样: - 它包含了 [Service](./service.md)、[中间件](./middleware.md)、[配置](./config.md)、[框架扩展](./extend.md)等等。 - 它没有独立的 [Router](./router.md) 和 [Controller](./controller.md)。 -- 它没有 `plugin.js`,只能声明跟其他插件的依赖,而**不能决定**其他插件的开启与否。 +- 它没有 `plugin.js`,只能声明和其他插件的依赖,而**不能决定**其他插件的开启与否。 他们的关系是: - 应用可以直接引入 Koa 的中间件。 -- 当遇到上一节提到的场景时,则应用需引入插件。 +- 当遇到上一节提到的场景时,应用需引入插件。 - 插件本身可以包含中间件。 - 多个插件可以包装为一个[上层框架](../advanced/framework.md)。 - ## 使用插件 -插件一般通过 npm 模块的方式进行复用: +插件通常通过 npm 模块的方式进行复用: ```bash $ npm i egg-mysql --save ``` -**注意:我们建议通过 `^` 的方式引入依赖,并且强烈不建议锁定版本。** +**注意:我们推荐通过 `^` 的方式引入依赖,并且强烈不建议锁定版本。** ```json { @@ -56,7 +53,7 @@ $ npm i egg-mysql --save } ``` -然后需要在应用或框架的 `config/plugin.js` 中声明: +接着,需要在应用或框架的 `config/plugin.js` 中声明: ```js // config/plugin.js @@ -67,7 +64,7 @@ exports.mysql = { }; ``` -就可以直接使用插件提供的功能: +这样就可以直接使用插件提供的功能: ```js app.mysql.query(sql, values); @@ -79,23 +76,23 @@ app.mysql.query(sql, values); - `{Boolean} enable` - 是否开启此插件,默认为 true - `{String} package` - `npm` 模块名称,通过 `npm` 模块形式引入插件 -- `{String} path` - 插件绝对路径,跟 package 配置互斥 -- `{Array} env` - 只有在指定运行环境才能开启,会覆盖插件自身 `package.json` 中的配置 +- `{String} path` - 插件绝对路径,与 package 配置互斥 +- `{Array} env` - 只有在指定运行环境才能开启,会覆盖该插件自身 `package.json` 中的配置 ### 开启和关闭 -在上层框架内部内置的插件,应用在使用时就不用配置 package 或者 path,只需要指定 enable 与否: +在上层框架内置的插件,应用在使用时,可以不配置 package 或者 path,只需指定 enable 即可: ```js -// 对于内置插件,可以用下面的简洁方式开启或关闭 +// 对于内置插件,可采用以下简洁方式开启或关闭 exports.onerror = false; ``` ### 根据环境配置 -同时,我们还支持 `plugin.{env}.js` 这种模式,会根据[运行环境](../basics/env.md)加载插件配置。 +同时,我们还支持 `plugin.{env}.js` 的模式,会根据[运行环境](../basics/env.md)加载插件配置。 -比如定义了一个开发环境使用的插件 `egg-dev`,只希望在本地环境加载,可以安装到 `devDependencies`。 +比如,如果定义了一个只在开发环境使用的插件 `egg-dev`,希望只在本地环境加载,可以将其安装到 `devDependencies` 中。 ```js // npm i egg-dev --save-dev @@ -107,7 +104,7 @@ exports.onerror = false; } ``` -然后在 `plugin.local.js` 中声明: +接下来,在 `plugin.local.js` 中声明: ```js // config/plugin.local.js @@ -117,18 +114,18 @@ exports.dev = { }; ``` -这样在生产环境可以 `npm i --production` 不需要下载 `egg-dev` 的包了。 +这样,在生产环境下执行 `npm i --production` 时,就不需要下载 `egg-dev` 包了。 **注意:** -- 不存在 `plugin.default.js` -- **只能在应用层使用,在框架层请勿使用。** +- `plugin.default.js` 不存在 +- **只能在应用层使用,框架层请勿使用。** ### package 和 path - `package` 是 `npm` 方式引入,也是最常见的引入方式 -- `path` 是绝对路径引入,如应用内部抽了一个插件,但还没达到开源发布独立 `npm` 的阶段,或者是应用自己覆盖了框架的一些插件 -- 关于这两种方式的使用场景,可以参见[渐进式开发](../intro/progressive.md)。 +- `path` 是绝对路径引入,例如应用内部提取了一个插件,但尚未发布至 npm;或者是应用自行改写了框架的某些插件 +- 关于这两种方式的使用场景,可参见[渐进式开发](../intro/progressive.md)文档。 ```js // config/plugin.js @@ -138,10 +135,9 @@ exports.mysql = { path: path.join(__dirname, '../lib/plugin/egg-mysql'), }; ``` - ## 插件配置 -插件一般会包含自己的默认配置,应用开发者可以在 `config.default.js` 覆盖对应的配置: +插件一般会包含自己的默认配置。应用开发者可以在 `config.default.js` 中覆盖对应的配置: ```js // config/config.default.js @@ -151,13 +147,12 @@ exports.mysql = { port: '3306', user: 'test_user', password: 'test_password', - database: 'test', - }, + database: 'test' + } }; ``` -具体合并规则可以参见[配置](./config.md)。 - +具体的合并规则可以参见[配置](./config.md)。 ## 插件列表 - 框架默认内置了企业级应用[常用的插件](https://eggjs.org/zh-cn/plugins/): @@ -173,7 +168,7 @@ exports.mysql = { - [static](https://github.com/eggjs/egg-static) 静态服务器 - [jsonp](https://github.com/eggjs/egg-jsonp) jsonp 支持 - [view](https://github.com/eggjs/egg-view) 模板引擎 -- 更多社区的插件可以 GitHub 搜索 [egg-plugin](https://github.com/topics/egg-plugin)。 +- 更多社区的插件可以在 GitHub 上搜索 [egg-plugin](https://github.com/topics/egg-plugin)。 ## 如何开发一个插件 diff --git a/site/docs/basics/router.zh-CN.md b/site/docs/basics/router.zh-CN.md index aa296ebc55..7f8101a330 100644 --- a/site/docs/basics/router.zh-CN.md +++ b/site/docs/basics/router.zh-CN.md @@ -3,11 +3,9 @@ title: 路由(Router) order: 6 --- -Router 主要用来描述请求 URL 和具体承担执行动作的 Controller 的对应关系, -框架约定了 `app/router.js` 文件用于统一所有路由规则。 - -通过统一的配置,我们可以避免路由规则逻辑散落在多个地方,从而出现未知的冲突,集中在一起我们可以更方便的来查看全局的路由规则。 +Router 主要用来描述请求 URL 和具体承担执行动作的 Controller 的对应关系,框架约定了 `app/router.js` 文件用于统一所有路由规则。 +通过统一的配置,我们可以避免路由规则逻辑散落在多个地方,从而出现未知的冲突。集中在一起,我们可以更方便地来查看全局的路由规则。 ## 如何定义 Router - `app/router.js` 里面定义 URL 路由规则 @@ -35,7 +33,6 @@ class UserController extends Controller { ``` 这样就完成了一个最简单的 Router 定义,当用户执行 `GET /user/123`,`user.js` 这个里面的 info 方法就会执行。 - ## Router 详细定义说明 下面是路由的完整定义,参数可以根据场景的不同,自由选择: @@ -47,37 +44,37 @@ router.verb('path-match', middleware1, ..., middlewareN, app.controller.action); router.verb('router-name', 'path-match', middleware1, ..., middlewareN, app.controller.action); ``` -路由完整定义主要包括 5 个主要部分: - -- verb - 用户触发动作,支持 get,post 等所有 HTTP 方法,后面会通过示例详细说明。 - - router.head - HEAD - - router.options - OPTIONS - - router.get - GET - - router.put - PUT - - router.post - POST - - router.patch - PATCH - - router.delete - DELETE - - router.del - 由于 delete 是一个保留字,所以提供了一个 delete 方法的别名。 - - router.redirect - 可以对 URL 进行重定向处理,比如我们最经常使用的可以把用户访问的根目录路由到某个主页。 -- router-name 给路由设定一个别名,可以通过 Helper 提供的辅助函数 `pathFor` 和 `urlFor` 来生成 URL。(可选) -- path-match - 路由 URL 路径。 -- middleware1 - 在 Router 里面可以配置多个 Middleware。(可选) -- controller - 指定路由映射到的具体的 controller 上,controller 可以有两种写法: - - `app.controller.user.fetch` - 直接指定一个具体的 controller - - `'user.fetch'` - 可以简写为字符串形式 +路由的完整定义主要包括以下五个主要部分: + +- `verb`:用户触发动作,支持 get、post 等所有 HTTP 方法,后面会通过示例详细说明。 + - `router.head`:HEAD + - `router.options`:OPTIONS + - `router.get`:GET + - `router.put`:PUT + - `router.post`:POST + - `router.patch`:PATCH + - `router.delete`:DELETE + - `router.del`:由于 delete 是一个保留字,所以提供了一个 delete 方法的别名。 + - `router.redirect`:可以对 URL 进行重定向处理,比如我们最经常使用的可以把用户访问的根目录路由到某个主页。 +- `router-name`:给路由设定一个别名,可以通过 Helper 提供的辅助函数 `pathFor` 和 `urlFor` 来生成 URL。(可选) +- `path-match`:路由 URL 路径。 +- `middlewareN`:在 Router 里面可以配置多个 Middleware。(可选) +- `controller`:指定路由映射到的具体 controller 上,controller 可以有两种写法: + - `app.controller.user.fetch`:直接指定一个具体的 controller。 + - `'user.fetch'`:可以简写为字符串形式。 ### 注意事项 -- 在 Router 定义中, 可以支持多个 Middleware 串联执行 +- 在 Router 定义中,可以支持多个 Middleware 串联执行。 - Controller 必须定义在 `app/controller` 目录中。 -- 一个文件里面也可以包含多个 Controller 定义,在定义路由的时候,可以通过 `${fileName}.${functionName}` 的方式指定对应的 Controller。 -- Controller 支持子目录,在定义路由的时候,可以通过 `${directoryName}.${fileName}.${functionName}` 的方式指定对应的 Controller。 +- 一个文件里面也可以包含多个 Controller 定义,在定义路由时,可以通过 `${fileName}.${functionName}` 的方式指定对应的 Controller。 +- Controller 支持子目录,在定义路由时,可以通过 `${directoryName}.${fileName}.${functionName}` 的方式指定对应的 Controller。 -下面是一些路由定义的方式: +以下是一些路由定义的方式: ```js // app/router.js -module.exports = (app) => { +module.exports = app => { const { router, controller } = app; router.get('/home', controller.home); router.get('/user/:id', controller.user.page); @@ -89,20 +86,18 @@ module.exports = (app) => { ### RESTful 风格的 URL 定义 -如果想通过 RESTful 的方式来定义路由, -我们提供了 `app.router.resources('routerName', 'pathMatch', controller)` 快速在一个路径上生成 [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) 路由结构。 +如果想用 RESTful 的方式定义路由,我们提供了 `app.router.resources('routerName', 'pathMatch', controller)` 方法,快速在一个路径上生成 [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) 路由结构。 ```js // app/router.js -module.exports = (app) => { +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` 里面实现对应的函数就可以了。 +上述代码就在 `/posts` 路径上部署了一组 CRUD 路径结构,对应的 Controller 为 `app/controller/posts.js`。接下来,只需在 `posts.js` 里面实现对应的函数即可。 | Method | Path | Route Name | Controller.Action | | ------ | --------------- | ---------- | ----------------------------- | @@ -131,11 +126,10 @@ exports.update = async () => {}; exports.destroy = async () => {}; ``` -如果我们不需要其中的某几个方法,可以不用在 `posts.js` 里面实现,这样对应 URL 路径也不会注册到 Router。 - +如果我们不需要其中的某些方法,可以省略在 `posts.js` 里面的实现,这样对应的 URL 路径也不会注册到 Router 中。 ## router 实战 -下面通过更多实际的例子,来说明 router 的用法。 +下面通过更多实际的例子,来说明 `router` 的用法。 ### 参数获取 @@ -173,7 +167,7 @@ exports.info = async (ctx) => { #### 复杂参数的获取 -路由里面也支持定义正则,可以更加灵活的获取参数: +路由里面也支持定义正则,可以更加灵活地获取参数: ```js // app/router.js @@ -186,8 +180,8 @@ module.exports = (app) => { // app/controller/package.js exports.detail = async (ctx) => { - // 如果请求 URL 被正则匹配, 可以按照捕获分组的顺序,从 ctx.params 中获取。 - // 按照下面的用户请求,`ctx.params[0]` 的 内容就是 `egg/1.0.0` + // 如果请求 URL 被正则匹配,可以按照捕获分组的顺序,从 ctx.params 中获取。 + // 按照下面的用户请求,`ctx.params[0]` 的内容就是 `egg/1.0.0` ctx.body = `package:${ctx.params[0]}`; }; @@ -213,17 +207,15 @@ exports.post = async (ctx) => { > 附: -> 这里直接发起 POST 请求会**报错**:'secret is missing'。错误信息来自 [koa-csrf/index.js#L69](https://github.com/koajs/csrf/blob/2.5.0/index.js#L69) 。 - -> **原因**:框架内部针对表单 POST 请求均会验证 CSRF 的值,因此我们在表单提交时,请带上 CSRF key 进行提交,可参考[安全威胁 csrf 的防范](https://eggjs.org/zh-cn/core/security.html#安全威胁csrf的防范) +> 这里直接发起 POST 请求会**报错**:'secret is missing'。错误信息来源于 [koa-csrf/index.js#L69](https://github.com/koajs/csrf/blob/2.5.0/index.js#L69)。 -> **注意**:上面的校验是因为框架中内置了安全插件 [egg-security](https://github.com/eggjs/egg-security),提供了一些默认的安全实践,并且框架的安全插件是默认开启的,如果需要关闭其中一些安全防范,直接设置该项的 enable 属性为 false 即可。 +> **原因**:框架内部针对表单 POST 请求均会验证 CSRF 的值,因此我们在表单提交时,需要带上 CSRF key 进行提交。具体可参考[安全威胁 CSRF 的防范](https://eggjs.org/zh-cn/core/security.html#安全威胁csrf的防范)。 -> 「除非清楚的确认后果,否则不建议擅自关闭安全插件提供的功能。」 +> **注意**:上述校验是因为框架中内置了安全插件 [egg-security](https://github.com/eggjs/egg-security),提供了一些默认的安全实践,并且框架的安全插件默认是开启的。如果需要关闭一些安全防范,直接设置相应选项的 `enable` 属性为 `false` 即可。 -> 这里在写例子的话可临时在 `config/config.default.js` 中设置 +> 虽然不推荐,但如果确实需要关闭某些安全功能,可以在 `config/config.default.js` 中设置以下代码: -``` +```javascript exports.security = { csrf: false }; @@ -303,7 +295,7 @@ exports.index = async (ctx) => { ### 中间件的使用 如果我们想把用户某一类请求的参数都大写,可以通过中间件来实现。 -这里我们只是简单说明下如何使用中间件,更多请查看 [中间件](./middleware.md)。 +这里我们仅简单说明如何使用中间件,更多信息请参考[中间件](./middleware.md)。 ```js // app/controller/search.js @@ -325,7 +317,7 @@ module.exports = (app) => { 's', '/search', app.middleware.uppercase(), - app.controller.search, + app.controller.search.index, ); }; @@ -334,9 +326,9 @@ module.exports = (app) => { ### 太多路由映射? -如上所述,我们并不建议把路由规则逻辑散落在多个地方,会给排查问题带来困扰。 +如上所述,我们不建议在多个地方分散路由规则,这可能会导致问题排查困难。 -若确实有需求,可以如下拆分: +如果确实存在需求,可以采用如下方法拆分: ```js // app/router.js @@ -358,4 +350,4 @@ module.exports = (app) => { }; ``` -也可直接使用 [egg-router-plus](https://github.com/eggjs/egg-router-plus)。 +如果需要更好的路由组织方式,也可以直接使用 [egg-router-plus](https://github.com/eggjs/egg-router-plus)。 diff --git a/site/docs/basics/schedule.zh-CN.md b/site/docs/basics/schedule.zh-CN.md index d841effd54..40f9946bd1 100644 --- a/site/docs/basics/schedule.zh-CN.md +++ b/site/docs/basics/schedule.zh-CN.md @@ -6,16 +6,15 @@ order: 10 虽然我们通过框架开发的 HTTP Server 是请求响应模型的,但是仍然还会有许多场景需要执行一些定时任务,例如: 1. 定时上报应用状态。 -1. 定时从远程接口更新本地缓存。 -1. 定时进行文件切割、临时文件删除。 +2. 定时从远程接口更新本地缓存。 +3. 定时进行文件切割、临时文件删除。 框架提供了一套机制来让定时任务的编写和维护更加优雅。 - ## 编写定时任务 所有的定时任务都统一存放在 `app/schedule` 目录下,每一个文件都是一个独立的定时任务,可以配置定时任务的属性和要执行的方法。 -一个简单的例子,我们定义一个更新远程数据到内存缓存的定时任务,就可以在 `app/schedule` 目录下创建一个 `update_cache.js` 文件 +一个简单的例子,我们定义一个更新远程数据到内存缓存的定时任务,就可以在 `app/schedule` 目录下创建一个 `update_cache.js` 文件。 ```js const Subscription = require('egg').Subscription; @@ -41,7 +40,7 @@ class UpdateCache extends Subscription { module.exports = UpdateCache; ``` -还可以简写为 +还可以简写为: ```js module.exports = { @@ -71,7 +70,7 @@ module.exports = { #### interval -通过 `schedule.interval` 参数来配置定时任务的执行时机,定时任务将会每间隔指定的时间执行一次。interval 可以配置成 +通过 `schedule.interval` 参数来配置定时任务的执行时机,定时任务将会每间隔指定的时间执行一次。interval 可以配置成: - 数字类型,单位为毫秒数,例如 `5000`。 - 字符类型,会通过 [ms](https://github.com/zeit/ms) 转换成毫秒数,例如 `5s`。 @@ -92,15 +91,15 @@ module.exports = { **注意:cron-parser 支持可选的秒(linux crontab 不支持)。** ```bash -* * * * * * -┬ ┬ ┬ ┬ ┬ ┬ -│ │ │ │ │ | -│ │ │ │ │ └ 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) +* * * * * * +┬ ┬ ┬ ┬ ┬ ┬ +│ │ │ │ │ │ +│ │ │ │ │ └─ 周日(0 - 7)(0 或 7 是周日) +│ │ │ │ └─── 月份(1 - 12) +│ │ │ └───── 日期(1 - 31) +│ │ └─────── 小时(0 - 23) +│ └───────── 分钟(0 - 59) +└─────────── 秒(0 - 59,可选) ``` ```js @@ -121,10 +120,10 @@ module.exports = { ### 其他参数 -除了刚才介绍到的几个参数之外,定时任务还支持这些参数: +除了上述介绍的几个参数,定时任务还支持以下参数: -- `cronOptions`: 配置 cron 的时区等,参见 [cron-parser](https://github.com/harrisiirak/cron-parser#options) 文档 -- `immediate`:配置了该参数为 true 时,这个定时任务会在应用启动并 ready 后立刻执行一次这个定时任务。 +- `cronOptions`:配置 cron 的时区等,参见 [cron-parser](https://github.com/harrisiirak/cron-parser#options) 文档。 +- `immediate`:配置该参数为 true 时,这个定时任务会在应用启动并 ready 后立即执行一次这个定时任务。 - `disable`:配置该参数为 true 时,这个定时任务不会被启动。 - `env`:数组,仅在指定的环境下才启动该定时任务。 @@ -141,10 +140,9 @@ config.customLogger = { }, }; ``` - ### 动态配置定时任务 -有时候我们需要配置定时任务的参数。定时任务还有支持另一种写法: +有时候,我们需要配置定时任务的参数。定时任务还可以支持另一种写法: ```js module.exports = (app) => { @@ -165,11 +163,11 @@ module.exports = (app) => { ## 手动执行定时任务 -我们可以通过 `app.runSchedule(schedulePath)` 来运行一个定时任务。`app.runSchedule` 接受一个定时任务文件路径(`app/schedule` 目录下的相对路径或者完整的绝对路径),执行对应的定时任务,返回一个 Promise。 +我们可以通过 `app.runSchedule(schedulePath)` 来运行一个定时任务。`app.runSchedule` 接受一个定时任务文件路径(位于 `app/schedule` 目录下的相对路径或者完整的绝对路径),执行对应的定时任务,并返回一个 Promise 对象。 -有一些场景我们可能需要手动的执行定时任务,例如 +在以下场景中,我们可能需要手动执行定时任务: -- 通过手动执行定时任务可以更优雅的编写对定时任务的单元测试。 +- 手动执行定时任务可以更优雅地编写定时任务的单元测试。 ```js const mm = require('egg-mock'); @@ -183,12 +181,12 @@ it('should schedule work fine', async () => { }); ``` -- 应用启动时,手动执行定时任务进行系统初始化,等初始化完毕后再启动应用。参见[应用启动自定义](./app-start.md)章节,我们可以在 `app.js` 中编写初始化逻辑。 +- 应用启动时,可以手动执行定时任务进行系统初始化。在初始化完毕后,再启动应用。具体可以参见[应用启动自定义](./app-start.md)章节。我们可以在 `app.js` 中编写初始化逻辑。 ```js module.exports = (app) => { app.beforeStart(async () => { - // 保证应用启动监听端口前数据已经准备好了 + // 保证应用启动监听端口前,数据已经准备好 // 后续数据的更新由定时任务自动触发 await app.runSchedule('update_cache'); }); @@ -197,18 +195,18 @@ module.exports = (app) => { ## 扩展定时任务类型 -默认框架提供的定时任务只支持每台机器的单个进程执行和全部进程执行,有些情况下,我们的服务并不是单机部署的,这时候可能有一个集群的某一个进程执行一个定时任务的需求。 +虽然默认的框架提供的定时任务只支持单个进程执行和全部进程执行,但是在某些情况下,比如服务非单机部署时,我们可能需要集群中的某一个进程执行定时任务。 -框架并没有直接提供此功能,但开发者可以在上层框架自行扩展新的定时任务类型。 +虽然框架没有直接提供此功能,开发者可在上层框架中自行扩展新的定时任务类型。 -在 `agent.js` 中继承 `agent.ScheduleStrategy`,然后通过 `agent.schedule.use()` 注册即可: +在 `agent.js` 中,继承 `agent.ScheduleStrategy`,然后通过 `agent.schedule.use()` 方法注册即可: ```js module.exports = (agent) => { class ClusterStrategy extends agent.ScheduleStrategy { start() { - // 订阅其他的分布式调度服务发送的消息,收到消息后让一个进程执行定时任务 - // 用户在定时任务的 schedule 配置中来配置分布式调度的场景(scene) + // 订阅其他分布式调度服务发送的消息,收到消息后让一个进程执行定时任务 + // 用户可以在定时任务的 schedule 属性中配置分布式调度的场景(scene) agent.mq.subscribe(this.schedule.scene, () => this.sendOne()); } } @@ -216,8 +214,8 @@ module.exports = (agent) => { }; ``` -`ScheduleStrategy` 基类提供了: +`ScheduleStrategy` 基类提供了以下方法: -- `this.schedule` - 定时任务的属性,`disable` 是默认统一支持的,其他配置可以自行解析。 -- `this.sendOne(...args)` - 随机通知一个 worker 执行 task,`args` 会传递给 `subscribe(...args)` 或 `task(ctx, ...args)`。 -- `this.sendAll(...args)` - 通知所有的 worker 执行 task。 +- `this.schedule` - 定时任务的属性,所有任务默认支持的 `disable` 属性,以及其他自定义配置的解析。 +- `this.sendOne(...args)` - 随机通知某个 worker 执行 task,`args` 会传递给 `subscribe(...args)` 或 `task(ctx, ...args)` 方法。 +- `this.sendAll(...args)` - 通知所有的 worker 执行 task。 \ No newline at end of file diff --git a/site/docs/basics/service.zh-CN.md b/site/docs/basics/service.zh-CN.md index 129b07c83d..330852ddc7 100644 --- a/site/docs/basics/service.zh-CN.md +++ b/site/docs/basics/service.zh-CN.md @@ -3,17 +3,15 @@ title: 服务(Service) order: 8 --- -简单来说,Service 就是在复杂业务场景下用于做业务逻辑封装的一个抽象层,提供这个抽象有以下几个好处: - -- 保持 Controller 中的逻辑更加简洁。 -- 保持业务逻辑的独立性,抽象出来的 Service 可以被多个 Controller 重复调用。 -- 将逻辑和展现分离,更容易编写测试用例,测试用例的编写具体可以查看[这里](../core/unittest.md)。 +简单来说,Service 就是在复杂业务场景下用于做业务逻辑封装的一个抽象层,它的提供具有以下几个优点: +- 保持 Controller 中的逻辑更简洁。 +- 保持业务逻辑的独立性,抽象出的 Service 可以被多个 Controller 重复使用。 +- 分离逻辑和展示,这样更便于编写测试用例。具体的测试用例编写方法,可以参见[这里](../core/unittest.md)。 ## 使用场景 -- 复杂数据的处理,比如要展现的信息需要从数据库获取,还要经过一定的规则计算,才能返回用户显示。或者计算完成后,更新到数据库。 -- 第三方服务的调用,比如 GitHub 信息获取等。 - +- 数据处理:当需要展示的信息须从数据库获取,并经规则计算后才能显示给用户,或计算后需更新数据库时。 +- 第三方服务调用:例如获取 GitHub 信息等。 ## 定义 Service ```js @@ -24,7 +22,7 @@ class UserService extends Service { async find(uid) { const user = await this.ctx.db.query( 'select * from user where uid = ?', - uid, + uid ); return user; } @@ -35,44 +33,41 @@ module.exports = UserService; ### 属性 -每一次用户请求,框架都会实例化对应的 Service 实例,由于它继承于 `egg.Service`,故拥有下列属性方便我们进行开发: +每一次用户请求,框架都会实例化对应的 Service 实例。因为它继承自 `egg.Service`,所以我们拥有以下属性便于开发: -- `this.ctx`: 当前请求的上下文 [Context](./extend.md#context) 对象的实例,通过它我们可以拿到框架封装好的处理当前请求的各种便捷属性和方法。 -- `this.app`: 当前应用 [Application](./extend.md#application) 对象的实例,通过它我们可以拿到框架提供的全局对象和方法。 -- `this.service`:应用定义的 [Service](./service.md),通过它我们可以访问到其他业务层,等价于 `this.ctx.service` 。 -- `this.config`:应用运行时的[配置项](./config.md)。 -- `this.logger`:logger 对象,上面有四个方法(`debug`,`info`,`warn`,`error`),分别代表打印四个不同级别的日志,使用方法和效果与 [context logger](../core/logger.md#context-logger) 中介绍的一样,但是通过这个 logger 对象记录的日志,在日志前面会加上打印该日志的文件路径,以便快速定位日志打印位置。 +- `this.ctx`:当前请求的上下文 [Context](./extend.md#context) 对象实例。通过它,我们可以获取框架封装的处理当前请求的各种便捷属性和方法。 +- `this.app`:当前应用 [Application](./extend.md#application) 对象实例。通过它,我们可以访问框架提供的全局对象和方法。 +- `this.service`:应用定义的 [Service](./service.md)。通过它,我们可以访问到其他业务层,等同于 `this.ctx.service`。 +- `this.config`:应用运行时的 [配置项](./config.md)。 +- `this.logger`:logger 对象。它有四个方法(`debug`,`info`,`warn`,`error`),分别代表不同级别的日志。使用方法和效果与 [context logger](../core/logger.md#context-logger) 所述一致。但通过这个 logger 记录的日志,在日志前会加上文件路径,方便定位日志位置。 ### Service ctx 详解 -为了可以获取用户请求的链路,我们在 Service 初始化中,注入了请求上下文, 用户在方法中可以直接通过 `this.ctx` 来获取上下文相关信息。关于上下文的具体详解可以参看 [Context](./extend.md#context), -有了 ctx 我们可以拿到框架给我们封装的各种便捷属性和方法。比如我们可以用: +为了能获取用户请求的链路,在 Service 初始化时,注入了请求上下文。用户可以通过 `this.ctx` 直接获取上下文相关信息。关于上下文的更多详细解释,请参考 [Context](./extend.md#context)。有了 `ctx`,我们可以: -- `this.ctx.curl` 发起网络调用。 -- `this.ctx.service.otherService` 调用其他 Service。 -- `this.ctx.db` 发起数据库调用等, db 可能是其他插件提前挂载到 app 上的模块。 +- 使用 `this.ctx.curl` 发起网络调用。 +- 通过 `this.ctx.service.otherService` 调用其他 Service。 +- 调用 `this.ctx.db` 发起数据库操作,`db` 可能是插件预挂载到 app 上的模块。 ### 注意事项 -- Service 文件必须放在 `app/service` 目录,可以支持多级目录,访问的时候可以通过目录名级联访问。 +- Service 文件必须放在 `app/service` 目录下,支持多级目录。可以通过目录名级联访问。 ```js + // app/service/biz/user.js 对应到 ctx.service.biz.user app/service/biz/user.js => ctx.service.biz.user + // app/service/sync_user.js 对应到 ctx.service.syncUser app/service/sync_user.js => ctx.service.syncUser + // app/service/HackerNews.js 对应到 ctx.service.hackerNews app/service/HackerNews.js => ctx.service.hackerNews ``` -- 一个 Service 文件只能包含一个类, 这个类需要通过 `module.exports` 的方式返回。 -- Service 需要通过 Class 的方式定义,父类必须是 `egg.Service`。 -- Service 不是单例,是 **请求级别** 的对象,框架在每次请求中首次访问 `ctx.service.xx` 时延迟实例化,所以 Service 中可以通过 this.ctx 获取到当前请求的上下文。 - -## 使用 Service - -下面就通过一个完整的例子,看看怎么使用 Service。 - +- 一个 Service 文件仅包含一个类,该类需通过 `module.exports` 导出。 +- Service 应通过 Class 形式定义,且继承自 `egg.Service`。 +- Service 不是单例,它是请求级别的对象。框架在每次请求中初次访问 `ctx.service.xx` 时才进行实例化。因此,Service 中可以通过 `this.ctx` 获取当前请求的上下文。 ```js // app/router.js -module.exports = (app) => { +module.exports = app => { app.router.get('/user/:id', app.controller.user.info); }; @@ -92,31 +87,31 @@ module.exports = UserController; const Service = require('egg').Service; class UserService extends Service { // 默认不需要提供构造函数。 - // constructor(ctx) { - // super(ctx); 如果需要在构造函数做一些处理,一定要有这句话,才能保证后面 `this.ctx`的使用。 - // // 就可以直接通过 this.ctx 获取 ctx 了 - // // 还可以直接通过 this.app 获取 app 了 - // } + /* constructor(ctx) { + super(ctx); // 如果需要在构造函数做一些处理,一定要有这句话,才能保证后面 `this.ctx` 的使用。 + // 就可以直接通过 this.ctx 获取 ctx 了 + // 还可以直接通过 this.app 获取 app 了 + } */ async find(uid) { - // 假如 我们拿到用户 id 从数据库获取用户详细信息 + // 假如我们拿到用户 id,从数据库获取用户详细信息 const user = await this.ctx.db.query( 'select * from user where uid = ?', - uid, + uid ); - // 假定这里还有一些复杂的计算,然后返回需要的信息。 + // 假定这里还有一些复杂的计算,然后返回需要的信息 const picture = await this.getPicture(uid); return { name: user.user_name, age: user.age, - picture, + picture }; } async getPicture(uid) { const result = await this.ctx.curl(`http://photoserver/uid=${uid}`, { - dataType: 'json', + dataType: 'json' }); return result.data; } diff --git a/site/docs/basics/structure.zh-CN.md b/site/docs/basics/structure.zh-CN.md index 9ebcfcfcfd..884426b154 100644 --- a/site/docs/basics/structure.zh-CN.md +++ b/site/docs/basics/structure.zh-CN.md @@ -8,36 +8,36 @@ order: 1 ```bash egg-project ├── package.json -├── app.js (可选) -├── agent.js (可选) +├── app.js(可选) +├── agent.js(可选) ├── app | ├── router.js │ ├── controller -│ | └── home.js -│ ├── service (可选) -│ | └── user.js -│ ├── middleware (可选) -│ | └── response_time.js -│ ├── schedule (可选) -│ | └── my_task.js -│ ├── public (可选) -│ | └── reset.css -│ ├── view (可选) -│ | └── home.tpl -│ └── extend (可选) -│ ├── helper.js (可选) -│ ├── request.js (可选) -│ ├── response.js (可选) -│ ├── context.js (可选) -│ ├── application.js (可选) -│ └── agent.js (可选) +│ │ └── home.js +│ ├── service(可选) +│ │ └── user.js +│ ├── middleware(可选) +│ │ └── response_time.js +│ ├── schedule(可选) +│ │ └── my_task.js +│ ├── public(可选) +│ │ └── reset.css +│ ├── view(可选) +│ │ └── home.tpl +│ └── extend(可选) +│ ├── helper.js(可选) +│ ├── request.js(可选) +│ ├── response.js(可选) +│ ├── context.js(可选) +│ ├── application.js(可选) +│ └── agent.js(可选) ├── config | ├── plugin.js | ├── config.default.js │ ├── config.prod.js -| ├── config.test.js (可选) -| ├── config.local.js (可选) -| └── config.unittest.js (可选) +| ├── config.test.js(可选) +| ├── config.local.js(可选) +| └── config.unittest.js(可选) └── test ├── middleware | └── response_time.test.js @@ -49,21 +49,22 @@ egg-project - `app/router.js` 用于配置 URL 路由规则,具体参见 [Router](./router.md)。 - `app/controller/**` 用于解析用户的输入,处理后返回相应的结果,具体参见 [Controller](./controller.md)。 -- `app/service/**` 用于编写业务逻辑层,可选,建议使用,具体参见 [Service](./service.md)。 -- `app/middleware/**` 用于编写中间件,可选,具体参见 [Middleware](./middleware.md)。 -- `app/public/**` 用于放置静态资源,可选,具体参见内置插件 [egg-static](https://github.com/eggjs/egg-static)。 -- `app/extend/**` 用于框架的扩展,可选,具体参见[框架扩展](./extend.md)。 -- `config/config.{env}.js` 用于编写配置文件,具体参见[配置](./config.md)。 -- `config/plugin.js` 用于配置需要加载的插件,具体参见[插件](./plugin.md)。 -- `test/**` 用于单元测试,具体参见[单元测试](../core/unittest.md)。 -- `app.js` 和 `agent.js` 用于自定义启动时的初始化工作,可选,具体参见[启动自定义](./app-start.md)。关于`agent.js`的作用参见[Agent 机制](../core/cluster-and-ipc.md#agent-机制)。 +- `app/service/**` 用于编写业务逻辑层,建议使用,具体参见 [Service](./service.md)。 +- `app/middleware/**` 用于编写中间件,具体参见 [Middleware](./middleware.md)。 +- `app/public/**` 用于放置静态资源,具体参见内置插件 [egg-static](https://github.com/eggjs/egg-static)。 +- `app/extend/**` 用于框架的扩展,具体参见 [框架扩展](./extend.md)。 +- `config/config.{env}.js` 用于编写配置文件,具体参见 [配置](./config.md)。 +- `config/plugin.js` 用于配置需要加载的插件,具体参见 [插件](./plugin.md)。 +- `test/**` 用于单元测试,具体参见 [单元测试](../core/unittest.md)。 +- `app.js` 和 `agent.js` 用于自定义启动时的初始化工作,具体参见 [启动自定义](./app-start.md)。关于 `agent.js` 的作用,参见 [Agent 机制](../core/cluster-and-ipc.md#agent-机制)。 + 由内置插件约定的目录: -- `app/public/**` 用于放置静态资源,可选,具体参见内置插件 [egg-static](https://github.com/eggjs/egg-static)。 -- `app/schedule/**` 用于定时任务,可选,具体参见[定时任务](./schedule.md)。 +- `app/public/**` 用于放置静态资源,具体参见内置插件 [egg-static](https://github.com/eggjs/egg-static)。 +- `app/schedule/**` 用于定时任务,具体参见 [定时任务](./schedule.md)。 **若需自定义自己的目录规范,参见 [Loader API](https://eggjs.org/zh-cn/advanced/loader.html)** -- `app/view/**` 用于放置模板文件,可选,由模板插件约定,具体参见[模板渲染](../core/view.md)。 -- `app/model/**` 用于放置领域模型,可选,由领域类相关插件约定,如 [egg-sequelize](https://github.com/eggjs/egg-sequelize)。 +- `app/view/**` 用于放置模板文件,具体参见 [模板渲染](../core/view.md)。 +- `app/model/**` 用于放置领域模型,如 [`egg-sequelize`](https://github.com/eggjs/egg-sequelize) 等领域类相关插件。