diff --git a/site/docs/advanced/cluster-client.zh-CN.md b/site/docs/advanced/cluster-client.zh-CN.md index 77410d64da..9e6ba1a33a 100644 --- a/site/docs/advanced/cluster-client.zh-CN.md +++ b/site/docs/advanced/cluster-client.zh-CN.md @@ -3,7 +3,7 @@ title: 多进程研发模式增强 order: 4 --- -在前面的[多进程模型章节](../core/cluster-and-ipc.md)中,我们详细讲述了框架的多进程模型,其中适合使用 Agent 进程的有一类常见的场景:一些中间件客户端需要和服务器建立长连接,理论上一台服务器最好只建立一个长连接,但多进程模型会导致 n 倍(n = Worker 进程数)连接被创建。 +在前面的 [多进程模型章节](../core/cluster-and-ipc.md) 中,我们详细讲述了框架的多进程模型。适合使用 Agent 进程的,有一类常见的场景:一些中间件客户端需要和服务器建立长连接。理论上,一台服务器最好只建立一个长连接,但多进程模型会导致 n 倍(n = Worker 进程数)的连接被创建。 ```bash +--------+ +--------+ @@ -18,23 +18,22 @@ order: 4 +--------+ +--------+ ``` -为了尽可能的复用长连接(因为它们对于服务端来说是非常宝贵的资源),我们会把它放到 Agent 进程里维护,然后通过 messenger 将数据传递给各个 Worker。这种做法是可行的,但是往往需要写大量代码去封装接口和实现数据的传递,非常麻烦。 +为了尽可能地复用长连接(因为它们对于服务端来说是非常宝贵的资源),我们会把它放到 Agent 进程里维护,然后通过 messenger 将数据传递给各个 Worker。这种做法是可行的,但往往需要写大量代码去封装接口和实现数据的传递,非常麻烦。 -另外,通过 messenger 传递数据效率是比较低的,因为它会通过 Master 来做中转;万一 IPC 通道出现问题还可能将 Master 进程搞挂。 - -那么有没有更好的方法呢?答案是肯定的,我们提供一种新的模式来降低这类客户端封装的复杂度。通过建立 Agent 和 Worker 的 socket 直连跳过 Master 的中转。Agent 作为对外的门面维持多个 Worker 进程的共享连接。 +另外,通过 messenger 传递数据效率较低,因为它会通过 Master 来做中转;万一 IPC 通道出现问题,还可能把 Master 进程弄挂。 +那么有没有更好的方法呢?答案是肯定的,我们提供了一种新的模式来降低这类客户端封装的复杂度。通过建立 Agent 和 Worker 的 socket 直连,跳过 Master 的中转,Agent 作为对外的门面,维持多个 Worker 进程的共享连接。 ## 核心思想 - 受到 [Leader/Follower](https://www.dre.vanderbilt.edu/~schmidt/PDF/lf.pdf) 模式的启发。 - 客户端会被区分为两种角色: - - Leader: 负责和远程服务端维持连接,对于同一类的客户端只有一个 Leader。 - - Follower: 会将具体的操作委托给 Leader,常见的是订阅模型(让 Leader 和远程服务端交互,并等待其返回)。 + - Leader:负责和远程服务端维持连接,对于同一类的客户端只有一个 Leader。 + - Follower:会将具体的操作委托给 Leader,常见的是订阅模型(让 Leader 和远程服务端交互,并等待其返回)。 - 如何确定谁是 Leader,谁是 Follower 呢?有两种模式: - - 自由竞争模式:客户端启动的时候通过本地端口的争夺来确定 Leader。例如:大家都尝试监听 7777 端口,最后只会有一个实例抢占到,那它就变成 Leader,其余的都是 Follower。 + - 自由竞争模式:客户端启动时通过本地端口的争夺来确定 Leader。例如:大家都尝试监听 7777 端口,最后只有一个实例抢占到,那它就变成了 Leader,其余的都是 Follower。 - 强制指定模式:框架指定某一个 Leader,其余的就是 Follower。 -- 框架里面我们采用的是强制指定模式,Leader 只能在 Agent 里面创建,这也符合我们对 Agent 的定位 -- 框架启动的时候 Master 会随机选择一个可用的端口作为 Cluster Client 监听的通讯端口,并将它通过参数传递给 Agent 和 App Worker。 +- 框架里面我们采用的是强制指定模式,Leader 只能在 Agent 里面创建,这也符合我们对 Agent 的定位。 +- 框架启动时,Master 会随机选择一个可用的端口作为 Cluster Client 监听的通讯端口,并将它通过参数传递给 Agent 和 App Worker。 - Leader 和 Follower 之间通过 socket 直连(通过通讯端口),不再需要 Master 中转。 新的模式下,客户端的通信方式如下: @@ -45,27 +44,27 @@ order: 4 +---+---+ | +--------+---------+ - __| port competition |__ + __| 端口竞争 |__ win / +------------------+ \ lose / \ -+---------------+ tcp conn +-------------------+ ++---------------+ tcp 连接 +-------------------+ | Leader(Agent) |<---------------->| Follower(Worker1) | +---------------+ +-------------------+ - | \ tcp conn + | \ tcp 连接 | \ +--------+ +-------------------+ | Client | | Follower(Worker2) | +--------+ +-------------------+ ``` - ## 客户端接口类型抽象 -我们将客户端接口抽象为下面两大类,这也是对客户端接口的一个规范,对于符合规范的客户端,我们可以自动将其包装为 Leader/Follower 模式。 +我们将客户端接口抽象为以下两大类,这也是对客户端接口的一个规范,对于符合规范的客户端,我们可以自动将其包装为 Leader/Follower 模式。 - 订阅、发布类(subscribe / publish): - `subscribe(info, listener)` 接口包含两个参数,第一个是订阅的信息,第二个是订阅的回调函数。 - - `publish(info)` 接口包含一个参数,就是订阅的信息。 -- 调用类 (invoke),支持 callback, promise 和 generator function 三种风格的接口,但是推荐使用 generator function。 + - `publish(info)` 接口包含一个参数,即订阅的信息。 + +- 调用类(invoke),支持 `callback`,`Promise` 和 `async function` 三种风格的接口,但是推荐使用 `async function`。 客户端示例 @@ -75,15 +74,15 @@ const Base = require('sdk-base'); class Client extends Base { constructor(options) { super(options); - // 在初始化成功以后记得 ready + // 在初始化成功后,记得要调用 ready this.ready(true); } /** * 订阅 * - * @param {Object} info - 订阅的信息(一个 JSON 对象,注意尽量不要包含 Function, Buffer, Date 这类属性) - * @param {Function} listener - 监听的回调函数,接收一个参数就是监听到的结果对象 + * @param {Object} info - 订阅的信息(一个 JSON 对象,注意尽量不包含 Function、Buffer、Date 这类属性) + * @param {Function} listener - 监听的回调函数,它接收一个参数,就是监听到的结果对象。 */ subscribe(info, listener) { // ... @@ -92,29 +91,27 @@ class Client extends Base { /** * 发布 * - * @param {Object} info - 发布的信息,和上面 subscribe 的 info 类似 + * @param {Object} info - 发布的信息,与 subscribe 方法中的 info 类似。 */ publish(info) { // ... } /** - * 获取数据 (invoke) + * 获取数据(invoke) * - * @param {String} id - id - * @return {Object} result + * @param {String} id - ID + * @return {Object} - 结果对象 */ async getData(id) { // ... } } ``` - ## 异常处理 -- Leader 如果“死掉”会触发新一轮的端口争夺,争夺到端口的那个实例被推选为新的 Leader。 -- 为保证 Leader 和 Follower 之间的通道健康,需要引入定时心跳检查机制,如果 Follower 在固定时间内没有发送心跳包,那么 Leader 会将 Follower 主动断开,从而触发 Follower 的重新初始化。 - +- 如果 Leader 实例“死掉”,将触发新一轮的端口争夺。争夺到端口的实例将被推举为新的 Leader。 +- 为了保证 Leader 和 Follower 之间通道的健康,需要引入定时的心跳检查机制。如果 Follower 在固定时间内未发送心跳包,Leader 会将其主动断开,以触发 Follower 的重新初始化。 ## 协议和调用时序 Leader 和 Follower 通过下面的协议进行数据交换: @@ -122,9 +119,9 @@ Leader 和 Follower 通过下面的协议进行数据交换: ```js 0 1 2 4 12 +-------+-------+---------------+---------------------------------------------------------------+ - |version|req/res| reserved | request id | + | version | req/res | reserved | request id | +-------------------------------+-------------------------------+-------------------------------+ - | timeout | connection object length | application object length | + | timeout | connection object length | application object length | +-------------------------------+---------------------------------------------------------------+ | conn object (JSON format) ... | app object | +-----------------------------------------------------------+ | @@ -132,12 +129,12 @@ Leader 和 Follower 通过下面的协议进行数据交换: +-----------------------------------------------------------------------------------------------+ ``` -1. 在通讯端口上 Leader 启动一个 Local Server,所有的 Leader/Follower 通讯都经过 Local Server。 -2. Follower 连接上 Local Server 后,首先发送一个 register channel 的 packet(引入 channel 的概念是为了区别不同类型的客户端)。 +1. 在通讯端口上,Leader 启动一个 Local Server,所有的 Leader/Follower 通讯都经过 Local Server。 +2. Follower 连接上 Local Server 后,首先发送一个 register channel 的 packet(引入 channel 的概念是为了区分不同类型的客户端)。 3. Local Server 会将 Follower 分配给指定的 Leader(根据客户端类型进行配对)。 4. Follower 向 Leader 发送订阅、发布请求。 -5. Leader 在订阅数据变更时通过 subscribe result packet 通知 Follower。 -6. Follower 向 Leader 发送调用请求,Leader 收到后执行相应操作后返回结果。 +5. Leader 在订阅数据变更时,通过 subscribe result packet 通知 Follower。 +6. Follower 向 Leader 发送调用请求,Leader 收到后执行相应操作并返回结果。 ```js +----------+ +---------------+ +---------+ @@ -146,21 +143,20 @@ Leader 和 Follower 通过下面的协议进行数据交换: | register channel | assign to | + -----------------------> | --------------------> | | | | - | subscribe | + | subscribe | + ------------------------------------------------> | - | publish | + | publish | + ------------------------------------------------> | | | | subscribe result | | <------------------------------------------------ + | | - | invoke | + | invoke | + ------------------------------------------------> | | invoke result | | <------------------------------------------------ + | | ``` - ## 具体的使用方法 下面我用一个简单的例子,介绍在框架里面如何让一个客户端支持 Leader/Follower 模式: @@ -191,7 +187,7 @@ class RegistryClient extends Base { /** * 获取配置 - * @param {String} dataId - the dataId + * @param {String} dataId - 数据 ID * @return {Object} 配置 */ async getConfig(dataId) { @@ -201,8 +197,8 @@ class RegistryClient extends Base { /** * 订阅 * @param {Object} reg - * - {String} dataId - the dataId - * @param {Function} listener - the listener + * - {String} dataId - 数据 ID + * @param {Function} listener - 监听器函数 */ subscribe(reg, listener) { const key = reg.dataId; @@ -217,8 +213,8 @@ class RegistryClient extends Base { /** * 发布 * @param {Object} reg - * - {String} dataId - the dataId - * - {String} publishData - the publish data + * - {String} dataId - 数据 ID + * - {String} publishData - 要发布的数据 */ publish(reg) { const key = reg.dataId; @@ -237,7 +233,7 @@ class RegistryClient extends Base { if (changed) { this.emit( key, - this._registered.get(key).map((url) => URL.parse(url, true)), + this._registered.get(key).map(url => URL.parse(url, true)), ); } } @@ -250,9 +246,9 @@ module.exports = RegistryClient; ```js // agent.js -const RegistryClient = require('registry_client'); +const RegistryClient = require('./registry_client'); -module.exports = (agent) => { +module.exports = agent => { // 对 RegistryClient 进行封装和实例化 agent.registryClient = agent .cluster(RegistryClient) @@ -261,7 +257,7 @@ module.exports = (agent) => { agent.beforeStart(async () => { await agent.registryClient.ready(); - agent.coreLogger.info('registry client is ready'); + agent.coreLogger.info('注册客户端已就绪'); }); }; ``` @@ -270,20 +266,20 @@ module.exports = (agent) => { ```js // app.js -const RegistryClient = require('registry_client'); +const RegistryClient = require('./registry_client'); -module.exports = (app) => { +module.exports = app => { app.registryClient = app.cluster(RegistryClient).create({}); app.beforeStart(async () => { await app.registryClient.ready(); - app.coreLogger.info('registry client is ready'); + app.coreLogger.info('注册客户端已就绪'); // 调用 subscribe 进行订阅 app.registryClient.subscribe( { dataId: 'demo.DemoService', }, - (val) => { + val => { // ... }, ); @@ -294,7 +290,7 @@ module.exports = (app) => { publishData: 'xxx', }); - // 调用 getConfig 接口 + // 调用 getConfig 获取配置 const res = await app.registryClient.getConfig('demo.DemoService'); console.log(res); }); @@ -303,7 +299,7 @@ module.exports = (app) => { 是不是很简单? -当然,如果你的客户端不是那么『标准』,那你可能需要用到其他一些 API,比如,你的订阅函数不叫 `subscribe` 而是叫 `sub`: +当然,如果你的客户端不是那么“标准”,那你可能需要用到其他一些 API,比如你的订阅函数不叫 `subscribe` 而是叫 `sub`: ```js class MockClient extends Base { @@ -320,7 +316,7 @@ class MockClient extends Base { } sub(info, listener) { - const key = reg.dataId; + const key = info.dataId; this.on(key, listener); const data = this._registered.get(key); @@ -329,18 +325,18 @@ class MockClient extends Base { } } - ... + // ... } ``` -你需要通过 `delegate`(API 代理)手动设置此委托: +你需要通过 `delegate`(API 代理)手动设置这个委托: ```js // agent.js -module.exports = (agent) => { +module.exports = agent => { agent.mockClient = agent .cluster(MockClient) - // 将 sub 代理到 subscribe 逻辑上 + // 将 sub 代理到 subscribe .delegate('sub', 'subscribe') .create(); @@ -352,37 +348,37 @@ module.exports = (agent) => { ```js // app.js -module.exports = (app) => { +module.exports = app => { app.mockClient = app .cluster(MockClient) - // 将 sub 代理到 subscribe 逻辑上 + // 将 sub 代理到 subscribe .delegate('sub', 'subscribe') .create(); app.beforeStart(async () => { await app.mockClient.ready(); - app.sub({ id: 'test-id' }, (val) => { - // put your code here + app.mockClient.sub({ id: 'test-id' }, val => { + // 请把你的代码放在这里 }); }); }; ``` -我们已经理解,通过 `cluster-client` 可以让我们在不理解多进程模型的情况下开发『纯粹』的 `RegistryClient`,只负责和服务端进行交互,然后使用 `cluster-client` 进行简单的封装就可以得到一个支持多进程模型的 `ClusterClient`。这里的 `RegistryClient` 实际上是一个专门负责和远程服务通信进行数据通信的 `DataClient`。 +我们已经理解,通过 `cluster-client` 可以让我们在不理解多进程模型的情况下开发“纯粹”的 `RegistryClient`,只负责和服务端进行交互,然后使用 `cluster-client` 进行简单的封装就可以得到一个支持多进程模型的 `ClusterClient`。这里的 `RegistryClient` 实际上是一个专门负责和远程服务通信进行数据通信的 `DataClient`。 大家可能已经发现,`ClusterClient` 同时带来了一些约束,如果想在各进程暴露同样的方法,那么 `RegistryClient` 上只能支持 sub/pub 模式以及异步的 API 调用。因为在多进程模型中所有的交互都必须经过 socket 通信,势必带来了这一约束。 假设我们要实现一个同步的 get 方法,订阅过的数据直接放入内存,使用 get 方法时直接返回。要怎么实现呢?而真实情况可能比这更复杂。 -在这里,我们引入一个 `APIClient` 的最佳实践。对于有读取缓存数据等同步 API 需求的模块,在 `RegistryClient` 基础上再封装一个 `APIClient` 来实现这些与远程服务端交互无关的 API,暴露给用户使用到的是这个 `APIClient` 的实例。 +在这里,我们引入一个 `APIClient` 的最佳实践。对于有读取缓存数据等同步 API 需求的模块,在 `RegistryClient` 基础上再封装一个 `APIClient` 来实现这些与远程服务端交互无关的 API,暴露给用户使用的是这个 `APIClient` 的实例。 -在 APIClient 内部实现上: +在 `APIClient` 内部实现上: - 异步数据获取,通过调用基于 `ClusterClient` 的 `RegistryClient` 的 API 实现。 - 同步调用等与服务端无关的接口在 `APIClient` 上实现。由于 `ClusterClient` 的 API 已经抹平了多进程差异,所以在开发 `APIClient` 调用到 `RegistryClient` 时也无需关心多进程模型。 -例如在模块的 `APIClient` 中增加带缓存的 get 同步方法: +例如,在模块的 `APIClient` 中增加带缓存的 get 同步方法: ```js // some-client/index.js @@ -393,13 +389,13 @@ class APIClient extends Base { constructor(options) { super(options); - // options.cluster 用于给 Egg 的插件传递 app.cluster 进来 + // options.cluster 用于给 Egg 的插件传递 app.cluster this._client = (options.cluster || cluster)(RegistryClient).create(options); this._client.ready(() => this.ready(true)); this._cache = {}; - // subMap: + // subMap 的例子: // { // foo: reg1, // bar: reg2, @@ -407,7 +403,7 @@ class APIClient extends Base { const subMap = options.subMap; for (const key in subMap) { - this.subscribe(subMap[key], (value) => { + this.subscribe(subMap[key], value => { this._cache[key] = value; }); } @@ -426,18 +422,18 @@ class APIClient extends Base { } } -// 最终模块向外暴露这个 APIClient +// 最终模块向外暴露这个 `APIClient` module.exports = APIClient; ``` -那么我们就可以这么使用该模块: +那么,我们就可以这样使用这个模块: ```js -// app.js || agent.js -const APIClient = require('some-client'); // 上面那个模块 +// app.js 或 agent.js +const APIClient = require('some-client'); // 上文中的模块 module.exports = app => { const config = app.config.apiClient; - app.apiClient = new APIClient(Object.assign({}, config, { cluster: app.cluster }); + app.apiClient = new APIClient(Object.assign({}, config, { cluster: app.cluster })); app.beforeStart(async () => { await app.apiClient.ready(); }); @@ -454,7 +450,7 @@ exports.apiClient = { }; ``` -为了方便你封装 `APIClient`,在 [cluster-client](https://www.npmjs.com/package/cluster-client) 模块中提供了一个 `APIClientBase` 基类,那么上面的 `APIClient` 可以改写为: +为了方便你封装 `APIClient`,在 `cluster-client` 模块中提供了一个 `APIClientBase` 基类,那么上文中的 `APIClient` 可以改写为: ```js const APIClientBase = require('cluster-client').APIClientBase; @@ -489,7 +485,7 @@ class APIClient extends APIClientBase { 总结一下: -```bash +```plaintext +------------------------------------------------+ | APIClient | | +----------------------------------------| @@ -499,46 +495,45 @@ class APIClient extends APIClientBase { +------------------------------------------------+ ``` -- RegistryClient - 负责和远端服务通讯,实现数据的存取,只支持异步 API,不关心多进程模型。 -- ClusterClient - 通过 `cluster-client` 模块进行简单 wrap 得到的 client 实例,负责自动抹平多进程模型的差异。 -- APIClient - 内部调用 `ClusterClient` 做数据同步,无需关心多进程模型,用户最终使用的模块。API 都通过此处暴露,支持同步和异步。 - -有兴趣的同学可以看一下[增强多进程研发模式](https://github.com/eggjs/egg/issues/322) 讨论过程。 +- `RegistryClient` - 负责和远端服务通讯,实现数据的存取,只支持异步 API,不关心多进程模型。 +- `ClusterClient` - 通过 `cluster-client` 模块进行简单包装得到的客户端实例,负责自动抹平多进程模型的差异。 +- `APIClient` - 内部调用 `ClusterClient` 做数据同步,无需关心多进程模型,用户最终使用的模块。API 通过此处暴露,支持同步和异步。 +有兴趣的同学可以查看《增强多进程研发模式》讨论过程。 ## 在框架里面 cluster-client 相关的配置项 ```js /** - * @property {Number} responseTimeout - response timeout, default is 60000 + * @property {Number} responseTimeout - 响应超时,默认值为 60000 * @property {Transcode} [transcode] - * - {Function} encode - custom serialize method - * - {Function} decode - custom deserialize method + * - {Function} encode - 自定义序列化方法 + * - {Function} decode - 自定义反序列化方法 */ config.clusterClient = { responseTimeout: 60000, }; ``` -| 配置项 | 类型 | 默认值 | 描述 | -| --------------- | -------- | ---------------- | ------------------------------------------------------------------------------------------------------------------- | -| responseTimeout | number | 60000 (一分钟) | 全局的进程间通讯的超时时长,不能设置的太短,因为代理的接口本身也有超时设置 | -| transcode | function | N/A | 进程间通讯的序列化方式,默认采用 [serialize-json](https://www.npmjs.com/package/serialize-json)(建议不要自行设置) | +| 配置项 | 类型 | 默认值 | 描述 | +| --------------- | -------- | ------------------ | ------------------------------------------------------------------ | +| responseTimeout | number | 60000(一分钟) | 全局的进程间通讯的超时时长,因为代理接口本身也有超时设置,所以不宜设置太短 | +| transcode | function | 未设置(N/A) | 进程间通讯的序列化方式,默认使用 [serialize-json](https://www.npmjs.com/package/serialize-json),建议不要自行设置 | -上面是全局的配置方式。如果,你想对一个客户端单独做设置 +上述表格为全局配置方式。如果你想为特定客户端单独设置,可以使用以下方法: -- 可以通过 `app/agent.cluster(ClientClass, options)` 的第二个参数 `options` 进行覆盖 +- 可以通过 `app/agent.cluster(ClientClass, options)` 的第二个参数 `options` 进行覆盖。 ```js app.registryClient = app .cluster(RegistryClient, { - responseTimeout: 120 * 1000, // 这里传入的是和 cluster-client 相关的参数 + responseTimeout: 120 * 1000, // 这里传入的是与 cluster-client 相关的参数 }) .create({ // 这里传入的是 RegistryClient 需要的参数 }); ``` -- 也可以通过覆盖 `APIClientBase` 的 `clusterOptions` 这个 `getter` 属性 +- 也可以通过覆盖 `APIClientBase` 的 `clusterOptions` 这个 `getter` 属性。 ```js const APIClientBase = require('cluster-client').APIClientBase; @@ -559,4 +554,4 @@ class APIClient extends APIClientBase { } module.exports = APIClient; -``` +``` \ No newline at end of file diff --git a/site/docs/advanced/framework.zh-CN.md b/site/docs/advanced/framework.zh-CN.md index bc2adc41e7..99dc71efac 100644 --- a/site/docs/advanced/framework.zh-CN.md +++ b/site/docs/advanced/framework.zh-CN.md @@ -5,7 +5,7 @@ order: 3 如果你的团队遇到过: -- 维护很多个项目,每个项目都需要复制拷贝诸如 `gulpfile.js` / `webpack.config.js` 之类的文件。 +- 维护很多个项目,每个项目都需要复制拷贝诸如 `gulpfile.js`、`webpack.config.js` 之类的文件。 - 每个项目都需要使用一些相同的类库,相同的配置。 - 在新项目中对上面的配置做了一个优化后,如何同步到其他项目? @@ -13,22 +13,22 @@ order: 3 - 统一的技术选型,比如数据库、模板、前端框架及各种中间件设施都需要选型,而框架封装后保证应用使用一套架构。 - 统一的默认配置,开源社区的配置可能不适用于公司,而又不希望应用去配置。 -- 统一的部署方案,通过框架和平台的双向控制,应用只需要关注自己的代码,具体查看[应用部署](../core/deployment.md) -- 统一的代码风格,框架不仅仅解决代码重用问题,还可以对应用做一定约束,作为企业框架是很必要的。Egg 在 Koa 基础上做了很多约定,框架可以使用 [Loader](./loader.md) 自己定义代码规则。 +- 统一的部署方案,通过框架和平台的双向控制,应用只需要关注自己的代码,具体查看[应用部署](../core/deployment.md)。 +- 统一的代码风格,框架不仅仅解决代码重用问题,还可以对应用做一定约束,作为企业框架是很必要的。Egg 在 Koa 的基础上做了很多约定,框架可以使用 [Loader](./loader.md) 自己定义代码规则。 为此,Egg 为团队架构师和技术负责人提供 `框架定制` 的能力,框架是一层抽象,可以基于 Egg 去封装上层框架,并且 Egg 支持多层继承。 -这样,整个团队就可以遵循统一的方案,并且在项目中可以根据业务场景自行使用插件做差异化,当后者验证为最佳实践后,就能下沉到框架中,其他项目仅需简单的升级下框架的版本即可享受到。 +这样,整个团队就可以遵循统一的方案,并且在项目中可以根据业务场景自行使用插件做差异化;当后者验证为最佳实践后,就能下沉到框架中,其他项目仅需简单的升级框架版本即可享受到。 具体可以参见[渐进式开发](../intro/progressive.md)。 ## 框架与多进程 -框架的扩展是和多进程模型有关的,我们已经知道[多进程模型](../core/cluster-and-ipc.md),也知道 Agent Worker 和 App Worker 的区别,所以我们需要扩展的类也有两个 Agent 和 Application,而这两个类的 API 不一定相同。 +框架的扩展与多进程模型有关,我们已经了解了[多进程模型](../core/cluster-and-ipc.md),也知道 `Agent Worker` 和 `App Worker` 的区别。因此,我们需要扩展的类也有两个:`Agent` 和 `Application`,这两个类的 API 不一定相同。 -在 Agent Worker 启动的时候会实例化 Agent,而在 App Worker 启动时会实例化 Application,这两个类又同时继承 [EggCore](https://github.com/eggjs/egg-core)。 +在 `Agent Worker` 启动的时候会实例化 `Agent`,而在 `App Worker` 启动时会实例化 `Application`。这两个类又同时继承自 [EggCore](https://github.com/eggjs/egg-core)。 -EggCore 可以看做 Koa Application 的升级版,默认内置 [Loader](./loader.md)、[Router](../basics/router.md) 及应用异步启动等功能,可以看做是支持 Loader 的 Koa。 +EggCore 可以看做是 Koa `Application` 的升级版,默认内置了 [Loader](./loader.md)、[Router](../basics/router.md) 及应用异步启动等功能,可以看作是支持 `Loader` 的 Koa。 ``` Koa Application @@ -41,10 +41,9 @@ EggCore 可以看做 Koa Application 的升级版,默认内置 [Loader](./load ^ ^ agent worker app worker ``` - ## 如何定制一个框架 -你可以直接通过 [egg-boilerplate-framework](https://github.com/eggjs/egg-boilerplate-framework) 脚手架来快速上手。 +你可以直接通过 [egg-boilerplate-framework](https://github.com/eggjs/egg-boilerplate-framework) 脚手架快速上手。 ```bash $ mkdir yadan && cd yadan @@ -53,46 +52,43 @@ $ npm i $ npm test ``` -但同样,为了让大家了解细节,接下来我们还是手把手来定制一个框架,具体代码可以查看[示例](https://github.com/eggjs/examples/tree/master/framework) +但同样,为了让大家了解细节,接下来我们会手把手地来定制一个框架,具体代码可以查看[示例](https://github.com/eggjs/examples/tree/master/framework)。 ### 框架 API -Egg 框架提供了一些 API,所有继承的框架都需要提供,只增不减。这些 API 基本都有 Agent 和 Application 两份。 +Egg 框架提供了一些 API,所有继承的框架都需要提供,只增不减。这些 API 基本都存在于 Agent 和 Application 两份实现中。 #### `egg.startCluster` -Egg 的多进程启动器,由这个方法来启动 Master,主要的功能实现在 [egg-cluster](https://github.com/eggjs/egg-cluster) 上。所以直接使用 EggCore 还是单进程的方式,而 Egg 实现了多进程。 +Egg 的多进程启动器,通过这个方法来启动 Master,主要的功能实现在 [egg-cluster](https://github.com/eggjs/egg-cluster) 上。因此,直接使用 EggCore 是单进程方式启动的,而 Egg 实现了多进程模式。 ```js const startCluster = require('egg').startCluster; -startCluster( - { +startCluster({ // 应用的代码目录 baseDir: '/path/to/app', // 需要通过这个参数来指定框架目录 framework: '/path/to/framework', - }, - () => { +}, () => { console.log('app started'); - }, -); +}); ``` -所有参数可以查看 [egg-cluster](https://github.com/eggjs/egg-cluster#options) +所有参数可以查看 [egg-cluster](https://github.com/eggjs/egg-cluster#options)。 #### `egg.Application` 和 `egg.Agent` -进程中的唯一单例,但 Application 和 Agent 存在一定差异。如果框架继承于 Egg,会定制这两个类,那 framework 应该 export 这两个类。 +它们是进程中的唯一实例,但 Application 和 Agent 存在一定差异。如果框架继承自 Egg,会定制这两个类,那么框架应该导出(export)这两个类。 #### `egg.AppWorkerLoader` 和 `egg.AgentWorkerLoader` -框架也存在定制 Loader 的场景,覆盖原方法或者新加载目录都需要提供自己的 Loader,而且必须要继承 Egg 的 Loader。 +框架也可能会有定制 Loader 的场景,如覆盖原方法或新加载目录,都需要提供自己的 Loader。而且必须继承自 Egg 的 Loader。 ### 框架继承 -框架支持继承关系,可以把框架比作一个类,那么基类就是 Egg 框架,如果想对 Egg 做扩展就继承。 +框架支持继承关系,可以把框架类比于一个类,那么基类就是 Egg 框架。如果你想对 Egg 进行扩展,那么可以继承它。 -首先定义一个框架需要实现 Egg 所有的 API +首先,定义一个框架需要实现 Egg 所有的 API: ```js // package.json @@ -113,18 +109,18 @@ const EGG_PATH = Symbol.for('egg#eggPath'); class Application extends egg.Application { get [EGG_PATH]() { - // 返回 framework 路径 + // 返回框架路径 return path.dirname(__dirname); } } -// 覆盖了 Egg 的 Application module.exports = Object.assign(egg, { Application, + // 可能还会有其他扩展 }); ``` -应用启动时需要指定框架名(在 `package.json` 指定 `egg.framework`,默认为 egg),Loader 将从 `node_modules` 找指定模块作为框架,并加载其 export 的 Application。 +应用启动时需要指定框架名(在 `package.json` 中指定 `egg.framework`,默认值是 `egg`),Loader 会从 `node_modules` 中寻找指定模块作为框架,并载入其导出的 Application。 ```json { @@ -137,47 +133,48 @@ module.exports = Object.assign(egg, { } ``` -现在 yadan 框架目录已经是一个 loadUnit,那么相应目录和文件(如 `app` 和 `config`)都会被加载,查看[框架被加载的文件](./loader.md)。 +现在,yadan 框架的目录已经成为了一个加载单元(loadUnit),因此相应的目录和文件(如 `app` 和 `config`)都会被加载。详情可以查看[框架被加载的文件](./loader.md)。 ### 框架继承原理 -使用 `Symbol.for('egg#eggPath')` 来指定当前框架的路径,目的是让 Loader 能探测到框架的路径。为什么这样实现呢?其实最简单的方式是将框架的路径传递给 Loader,但我们需要实现多级框架继承,每一层框架都要提供自己的当前路径,并且需要继承存在先后顺序。 +使用 `Symbol.for('egg#eggPath')` 来指定当前框架的路径,目的是让 Loader 能探测到框架路径。为什么采取这种实现方式?本可以将框架路径直接传给 Loader,但为了实现多级框架继承,每一层框架都要提供自己的路径,且继承有其顺序。 -现在的实现方案是基于类继承的,每一层框架都必须继承上一层框架并且指定 eggPath,然后遍历原型链就能获取每一层的框架路径了。 +现在的实现方案是基于类继承的。每一层框架都必须继承上一层框架,并且指定 eggPath,之后遍历原型链,就可以获取到每一层框架的路径了。 比如有三层框架:部门框架(department)> 企业框架(enterprise)> Egg ```js // enterprise const Application = require('egg').Application; -class Enterprise extends Application { +class EnterpriseApplication extends Application { get [EGG_PATH]() { return '/path/to/enterprise'; } } -// 自定义模块 Application -exports.Application = Enterprise; +// 自定义模块的 Application +exports.Application = EnterpriseApplication; // department -const Application = require('enterprise').Application; -// 继承 enterprise 的 Application -class department extends Application { +const EnterpriseApplication = require('enterprise').Application; +// 继承自 enterprise 的 Application +class DepartmentApplication extends EnterpriseApplication { get [EGG_PATH]() { return '/path/to/department'; } } -// 启动需要传入 department 的框架路径才能获取 Application -const Application = require('department').Application; -const app = new Application(); +// 启动时需要传入 department 的框架路径 +const DepartmentApplication = require('department').Application; +const app = new DepartmentApplication(); app.ready(); ``` -以上均是伪代码,为了详细说明框架路径的加载过程,不过 Egg 已经在[本地开发](../core/development.md)和[应用部署](../core/deployment.md)提供了很好的工具,不需要自己实现。 +以上都是示例代码,用于解释框架路径加载过程。实际上,Egg 已经提供了[本地开发](../core/development.md)和[应用部署](../core/deployment.md)的优秀工具,不需要我们自行实现。 +下面是根据《优秀技术文档的写作标准》修改后的全文内容: ### 自定义 Agent -上面的例子自定义了 Application,因为 Egg 是多进程模型,所以还需要定义 Agent,原理是一样的。 +上面的例子自定义了 Application。由于 Egg 是多进程模型,因此还需要定义 Agent。原理是一样的。 ```js // lib/framework.js @@ -205,13 +202,13 @@ module.exports = Object.assign(egg, { }); ``` -**但因为 Agent 和 Application 是两个实例,所以 API 有可能不一致。** +**但因为 Agent 和 Application 是两个实例,API 有可能不一致。** ### 自定义 Loader -Loader 应用启动的核心,使用它还能规范应用代码,我们可以基于这个类扩展更多功能,比如加载数据代码。扩展 Loader 还能覆盖默认的实现,或调整现有的加载顺序等。 +Loader 是应用启动的核心。利用它,我们不仅能规范应用代码,还能基于这个类扩展更多功能,比如加载数据模型。扩展 Loader 还可以覆盖默认的实现,或调整现有的加载顺序等。 -自定义 Loader 也是用 `Symbol.for('egg#loader')` 的方式,主要的原因还是使用原型链,上层框架可覆盖底层 Loader,在上面例子的基础上 +我们使用 `Symbol.for('egg#loader')` 来自定义 Loader,主要原因还是为了使用原型链。这样,上层框架可以覆盖底层 Loader。在上面的例子基础上: ```js // lib/framework.js @@ -222,7 +219,7 @@ const EGG_PATH = Symbol.for('egg#eggPath'); class YadanAppWorkerLoader extends egg.AppWorkerLoader { load() { super.load(); - // 自己扩展 + // 进行自己的扩展 } } @@ -231,7 +228,7 @@ class Application extends egg.Application { // 返回 framework 路径 return path.dirname(__dirname); } - // 覆盖 Egg 的 Loader,启动时使用这个 Loader + // 覆盖 Egg 的 Loader,启动时将使用这个 Loader get [EGG_LOADER]() { return YadanAppWorkerLoader; } @@ -240,36 +237,35 @@ class Application extends egg.Application { // 覆盖了 Egg 的 Application module.exports = Object.assign(egg, { Application, - // 自定义的 Loader 也需要 export,上层框架需要基于这个扩展 + // 自定义的 Loader 也需要导出,以便上层框架进行扩展 AppWorkerLoader: YadanAppWorkerLoader, }); ``` -AgentWorkerLoader 扩展也类似,这里不再举例。AgentWorkerLoader 加载的文件可以于 AppWorkerLoader 不同,比如:默认加载时,Egg 的 AppWorkerLoader 会加载 `app.js` 而 AgentWorkerLoader 加载的是 `agent.js`。 +AgentWorkerLoader 的扩展也类似,这里不再赘述。AgentWorkerLoader 加载的文件可以与 AppWorkerLoader 不同。比如,默认加载时,Egg 的 AppWorkerLoader 会加载 `app.js`,而 AgentWorkerLoader 加载的是 `agent.js`。 ## 框架启动原理 -框架启动在[多进程模型](../core/cluster-and-ipc.md)、[Loader](./loader.md)、[插件](./plugin.md)中或多或少都提过,这里系统的梳理下启动顺序。 - -- startCluster 启动传入 `baseDir` 和 `framework`,Master 进程启动 -- Master 先 fork Agent Worker - - 根据 framework 找到框架目录,实例化该框架的 Agent 类 - - Agent 找到定义的 AgentWorkerLoader,开始进行加载 - - AgentWorkerLoader,开始进行加载 整个加载过程是同步的,按 plugin > config > extend > `agent.js` > 其他文件顺序加载 - - `agent.js` 可自定义初始化,支持异步启动,如果定义了 beforeStart 会等待执行完成之后通知 Master 启动完成。 -- Master 得到 Agent Worker 启动成功的消息,使用 cluster fork App Worker - - App Worker 有多个进程,所以这几个进程是并行启动的,但执行逻辑是一致的 - - 单个 App Worker 和 Agent 类似,通过 framework 找到框架目录,实例化该框架的 Application 类 - - Application 找到 AppWorkerLoader,开始进行加载,顺序也是类似的,会异步等待,完成后通知 Master 启动完成 -- Master 等待多个 App Worker 的成功消息后启动完成,能对外提供服务。 - +框架启动过程在[多进程模型](../core/cluster-and-ipc.md)、[Loader](./loader.md)、[插件](./plugin.md)中或多或少都有提及。这里我们系统地梳理一下启动顺序: + +1. `startCluster` 启动时传入 `baseDir` 和 `framework`,从而启动 Master 进程。 +2. Master 首先 fork Agent Worker: + - 根据 framework 找到框架目录,实例化该框架的 Agent 类。 + - Agent 根据定义的 AgentWorkerLoader 开始加载。 + - 整个 AgentWorkerLoader 的加载过程是同步的,按照 plugin > config > extend > `agent.js` > 其他文件的顺序进行。 + - 如果 `agent.js` 中定义了自定义初始化并支持异步启动,当执行完成后,会告知 Master 启动已完成。 +3. Master 在接到 Agent Worker 启动成功的消息后,会 fork App Worker: + - App Worker 由多个进程组成,这些进程会并行启动,但执行逻辑是一致的。 + - 单个 App Worker 通过 framework 找到框架目录,实例化该框架的 Application 类。 + - Application 根据 AppWorkerLoader 开始加载,加载顺序类似,会异步等待完成后通知 Master 启动完成。 +4. Master 在等到所有 App Worker 发来的启动成功消息后,完成启动,开始对外提供服务。 ## 框架测试 -在看下文之前请先查看[单元测试章节](../core/unittest.md),框架测试的大部分使用场景和应用类似。 +在看下文之前,请先查看[单元测试章节](../core/unittest.md)。框架测试的大部分使用场景和应用类似。 ### 初始化 -框架的初始化方式有一定差异 +框架的初始化方式有一定差异。 ```js const mock = require('egg-mock'); @@ -280,7 +276,7 @@ describe('test/index.test.js', () => { // 转换成 test/fixtures/apps/example baseDir: 'apps/example', // 重要:配置 framework - framework: true, + framework: true }); return app.ready(); }); @@ -296,13 +292,13 @@ describe('test/index.test.js', () => { - 框架和应用不同,应用测试当前代码,而框架是测试框架代码,所以会频繁更换 baseDir 达到测试各种应用的目的。 - baseDir 有潜规则,我们一般会把测试的应用代码放到 `test/fixtures` 下,所以自动补全,也可以传入绝对路径。 -- 必须指定 `framework: true`,告知当前路径为框架路径,也可以传入绝对路径。 -- app 应用需要在 before 等待 ready,不然在 testcase 里无法获取部分 API -- 框架在测试完毕后需要使用 `app.close()` 关闭,不然会有遗留问题,比如日志写文件未关闭导致 fd 不够。 +- 必须指定 `framework: true`,告知当前路径为框架路径;也可以传入绝对路径。 +- app 应用需要在 before 等待 ready,否则在 testcase 里无法获取部分 API。 +- 框架在测试完毕后,需要使用 `app.close()` 关闭,否则会有遗留问题,例如日志写文件未关闭导致 fd 不够。 ### 缓存 -在测试多环境场景需要使用到 cache 参数,因为 `mm.app` 默认有缓存,当第一次加载过后再次加载会直接读取缓存,那么设置的环境也不会生效。 +在测试多环境场景需要使用到 cache 参数,因为 `mock.app` 默认有缓存,当第一次加载后再次加载会直接读取缓存,那么设置的环境也不会生效。 ```js const mock = require('egg-mock'); @@ -315,7 +311,7 @@ describe('/test/index.test.js', () => { app = mock.app({ baseDir: 'apps/example', framework: true, - cache: false, + cache: false }); return app.ready(); }); @@ -324,7 +320,7 @@ describe('/test/index.test.js', () => { app = mock.app({ baseDir: 'apps/example', framework: true, - cache: false, + cache: false }); return app.ready(); }); @@ -333,9 +329,9 @@ describe('/test/index.test.js', () => { ### 多进程测试 -很少场景会使用多进程测试,因为多进程无法进行 API 级别的 mock 导致测试成本很高,而进程在有覆盖率的场景启动很慢,测试会超时。但多进程测试是验证多进程模型最好的方式,还可以测试 stdout 和 stderr。 +很少场景会使用多进程测试,因为多进程无法进行 API 级别的 mock,导致测试成本很高。而进程在有覆盖率的场景启动很慢,测试会超时。但多进程测试是验证多进程模型最好的方式。还可以测试 stdout 和 stderr。 -多进程测试和 `mm.app` 参数一致,但 app 的 API 完全不同,不过 SuperTest 依然可用。 +多进程测试和 `mock.app` 参数一致,但 app 的 API 完全不同。不过,SuperTest 依然可用。 ```js const mock = require('egg-mock'); @@ -344,7 +340,7 @@ describe('/test/index.test.js', () => { before(() => { app = mock.cluster({ baseDir: 'apps/example', - framework: true, + framework: true }); return app.ready(); }); @@ -356,7 +352,7 @@ describe('/test/index.test.js', () => { }); ``` -多进程测试还可以测试 stdout/stderr,因为 `mm.cluster` 是基于 [coffee](https://github.com/popomore/coffee) 扩展的,可进行进程测试。 +多进程测试还可以测试 stdout/stderr,因为 `mock.cluster` 是基于 [coffee](https://github.com/popomore/coffee) 扩展的,可进行进程测试。 ```js const mock = require('egg-mock'); @@ -365,7 +361,7 @@ describe('/test/index.test.js', () => { before(() => { app = mock.cluster({ baseDir: 'apps/example', - framework: true, + framework: true }); return app.ready(); }); diff --git a/site/docs/advanced/loader-update.zh-CN.md b/site/docs/advanced/loader-update.zh-CN.md index 3e1a48e798..e933ee7757 100644 --- a/site/docs/advanced/loader-update.zh-CN.md +++ b/site/docs/advanced/loader-update.zh-CN.md @@ -10,7 +10,7 @@ order: 6 ## beforeStart 函数替代 -我们通常在 app.js 中通过 `module.export` 中传入的 `app` 参数进行此函数的操作,一个典型的例子: +我们通常在 `app.js` 中通过 `module.exports` 中传入的 `app` 参数进行此函数的操作,一个典型的例子: ```js module.exports = (app) => { @@ -20,7 +20,7 @@ module.exports = (app) => { }; ``` -现在升级之后的写法略有改变 —— 我们可以直接在 `app.js` 中用类方法的形式体现出来:对于应用开发而言,我们应该写在 `willReady` 方法中;对于插件则写在 `didLoad` 中。形式如下: +现在升级之后的写法略有改变 —— 我们可以直接在 `app.js` 中用类方法的形式体现出来。对于应用开发而言,应该写在 `willReady` 方法中;对于插件则写在 `didLoad` 中。形式如下: ```js // app.js 或 agent.js 文件: @@ -30,17 +30,18 @@ class AppBootHook { } async didLoad() { - // 请将你的插件项目中 app.beforeStart 中的代码置于此处。 + // 请将你的插件项目中 app.beforeStart 中的代码置于此处 } async willReady() { - // 请将你的应用项目中 app.beforeStart 中的代码置于此处。 + // 请将你的应用项目中 app.beforeStart 中的代码置于此处 } } module.exports = AppBootHook; ``` + ## ready 函数替代 同样地,我们之前在 `app.ready` 中处理我们的逻辑: @@ -63,13 +64,14 @@ class AppBootHook { } async didReady() { - // 请将您的 app.ready 中的代码置于此处。 + // 请将你的 app.ready 中的代码置于此处 } } module.exports = AppBootHook; ``` + ## beforeClose 函数替代 原先的 `app.beforeClose` 如以下形式: @@ -92,11 +94,12 @@ class AppBootHook { } async beforeClose() { - // 请将您的 app.beforeClose 中的代码置于此处。 + // 请将你的 app.beforeClose 中的代码置于此处 } } +module.exports = AppBootHook; ``` ## 其它说明 -本教程只是一对一地讲了替换方法,便于开发者们快速上手进行替换;若想要具体了解整个 Loader 原理以及生命周期的完整函数版本,请参考[加载器](./loader.md)和[启动自定义](../basics/app-start.md)两篇文章。 +本教程只是一对一地讲了替换方法,便于开发者们快速上手进行替换。若想要具体了解整个 Loader 原理以及生命周期的完整函数版本,请参考《[加载器](./loader.md)》和《[启动自定义](../basics/app-start.md)》两篇文章。 diff --git a/site/docs/advanced/loader.zh-CN.md b/site/docs/advanced/loader.zh-CN.md index deb5a4e23f..c6e8d3903b 100644 --- a/site/docs/advanced/loader.zh-CN.md +++ b/site/docs/advanced/loader.zh-CN.md @@ -3,11 +3,11 @@ title: 加载器(Loader) order: 1 --- -Egg 在 Koa 的基础上进行增强最重要的就是基于一定的约定,根据功能差异将代码放到不同的目录下管理,对整体团队的开发成本提升有着明显的效果。Loader 实现了这套约定,并抽象了很多底层 API 可以进一步扩展。 +Egg 在 Koa 的基础上进行增强,最重要的就是基于一定的约定,将功能不同的代码分类放置到不同的目录下管理,这对整体团队的开发成本提升有着明显的效果。Loader 实现了这套约定,并且抽象了很多底层 API,以便于进一步扩展。 ## 应用、框架和插件 -Egg 是一个底层框架,应用可以直接使用,但 Egg 本身的插件比较少,应用需要自己配置插件增加各种特性,比如 MySQL。 +Egg 是一个底层框架,应用可以直接使用,但 Egg 本身的插件比较少。因此,应用需要自己配置插件来增加各种特性,比如 MySQL。 ```js // 应用配置 @@ -23,12 +23,12 @@ Egg 是一个底层框架,应用可以直接使用,但 Egg 本身的插件 module.exports = { mysql: { enable: true, - package: 'egg-mysql', - }, + package: 'egg-mysql' + } } ``` -当应用达到一定数量,我们会发现大部分应用的配置都是类似的,这时可以基于 Egg 扩展出一个框架,应用的配置就会简化很多。 +当应用数量达到一定规模时,会发现大部分应用的配置都相似。这时,可以基于 Egg 扩展出一个框架,进而简化应用的配置。 ```js // 框架配置 @@ -46,11 +46,11 @@ module.exports = { module.exports = { mysql: { enable: false, - package: 'egg-mysql', + package: 'egg-mysql' }, view: { enable: false, - package: 'egg-view-nunjucks', + package: 'egg-view-nunjucks' } } @@ -58,7 +58,7 @@ module.exports = { // package.json { "dependencies": { - "framework1": "^1.0.0", + "framework1": "^1.0.0" } } @@ -66,16 +66,16 @@ module.exports = { module.exports = { // 开启插件 mysql: true, - view: true, + view: true } ``` -从上面的使用场景可以看到应用、插件和框架三者之间的关系。 +从上面的使用场景可以看出应用、插件和框架三者之间的关系。 -- 我们在应用中完成业务,需要指定一个框架才能运行起来,当需要某个特性场景的功能时可以配置插件(比如 MySQL)。 -- 插件只完成特定功能,当两个独立的功能有互相依赖时,还是分开两个插件,但需要配置依赖。 -- 框架是一个启动器(默认就是 Egg),必须有它才能运行起来。框架还是一个封装器,将插件的功能聚合起来统一提供,框架也可以配置插件。 -- 在框架的基础上还可以扩展出新的框架,也就是说**框架是可以无限级继承的**,有点像类的继承。 +- 在应用中完成业务,需要指定框架才能运行。当应用需要某一特定功能时,可以通过配置插件来获得,例如 MySQL。 +- 插件专注于完成特定的功能。如果两个独立功能之间存在依赖,可以分成两个插件,但需要相互配置依赖。 +- 框架是一个启动器(默认是 Egg),有了框架应用才能运行。框架还起到封装器的作用,将多个插件的功能聚合起来统一提供,并且框架也可以配置插件。 +- 在框架的基础上还可以扩展新的框架,也就是说,框架可以无限级继承,这有点类似于类的继承。 ``` +-----------------------------------+--------+ @@ -90,10 +90,9 @@ module.exports = { | Koa | +-----------------------------------+--------+ ``` - ## 加载单元(loadUnit) -Egg 将应用、框架和插件都称为加载单元(loadUnit),因为在代码结构上几乎没有什么差异,下面是目录结构 +Egg 将应用、框架和插件都称为加载单元(loadUnit),因为在代码结构上几乎没有什么差异。下面是一种典型的目录结构: ``` loadUnit @@ -102,12 +101,12 @@ loadUnit ├── agent.js ├── app │ ├── extend -│ | ├── helper.js -│ | ├── request.js -│ | ├── response.js -│ | ├── context.js -│ | ├── application.js -│ | └── agent.js +│ │ ├── helper.js +│ │ ├── request.js +│ │ ├── response.js +│ │ ├── context.js +│ │ ├── application.js +│ │ └── agent.js │ ├── service │ ├── middleware │ └── router.js @@ -119,45 +118,45 @@ loadUnit └── config.unittest.js ``` -不过还存在着一些差异 +不过,还存在一些差异,如下表所示: | 文件 | 应用 | 框架 | 插件 | | ------------------------- | ---- | ---- | ---- | -| package.json | ✔︎ | ✔︎ | ✔︎ | -| config/plugin.{env}.js | ✔︎ | ✔︎ | | -| config/config.{env}.js | ✔︎ | ✔︎ | ✔︎ | -| app/extend/application.js | ✔︎ | ✔︎ | ✔︎ | -| app/extend/request.js | ✔︎ | ✔︎ | ✔︎ | -| app/extend/response.js | ✔︎ | ✔︎ | ✔︎ | -| app/extend/context.js | ✔︎ | ✔︎ | ✔︎ | -| app/extend/helper.js | ✔︎ | ✔︎ | ✔︎ | -| agent.js | ✔︎ | ✔︎ | ✔︎ | -| app.js | ✔︎ | ✔︎ | ✔︎ | -| app/service | ✔︎ | ✔︎ | ✔︎ | -| app/middleware | ✔︎ | ✔︎ | ✔︎ | -| app/controller | ✔︎ | | | -| app/router.js | ✔︎ | | | - -文件按表格内的顺序自上而下加载 - -在加载过程中,Egg 会遍历所有的 loadUnit 加载上述的文件(应用、框架、插件各有不同),加载时有一定的优先级 - -- 按插件 => 框架 => 应用依次加载 -- 插件之间的顺序由依赖关系决定,被依赖方先加载,无依赖按 object key 配置顺序加载,具体可以查看[插件章节](./plugin.md) +| package.json | ✔ | ✔ | ✔ | +| config/plugin.{env}.js | ✔ | ✔ | | +| config/config.{env}.js | ✔ | ✔ | ✔ | +| app/extend/application.js | ✔ | ✔ | ✔ | +| app/extend/request.js | ✔ | ✔ | ✔ | +| app/extend/response.js | ✔ | ✔ | ✔ | +| app/extend/context.js | ✔ | ✔ | ✔ | +| app/extend/helper.js | ✔ | ✔ | ✔ | +| agent.js | ✔ | ✔ | ✔ | +| app.js | ✔ | ✔ | ✔ | +| app/service | ✔ | ✔ | ✔ | +| app/middleware | ✔ | ✔ | ✔ | +| app/controller | ✔ | | | +| app/router.js | ✔ | | | + +文件按表格内的顺序从上到下加载。 + +在加载过程中,Egg 会遍历所有的 loadUnit 加载上述的文件(应用、框架、插件各有不同),加载时有一定的优先级: + +- 按插件 => 框架 => 应用的顺序依次加载。 +- 插件之间的顺序由依赖关系决定,被依赖方先加载,无依赖者按 object key 的配置顺序加载。具体可以查看[插件章节](./plugin.md)。 - 框架按继承顺序加载,越底层越先加载。 -比如有这样一个应用配置了如下依赖 +例如,有这样一个应用配置了如下依赖: ``` app -| ├── plugin2 (依赖 plugin3) +| ├── plugin2 (依赖 plugin3) | └── plugin3 └── framework1 | └── plugin1 └── egg ``` -最终的加载顺序为 +最终的加载顺序为: ``` => plugin1 @@ -168,143 +167,141 @@ app => app ``` -plugin1 为 framework1 依赖的插件,配置合并后 object key 的顺序会优先于 plugin2/plugin3。因为 plugin2 和 plugin3 的依赖关系,所以交换了位置。framework1 继承了 egg,顺序会晚于 egg。应用最后加载。 +plugin1 是 framework1 依赖的插件。由于 plugin2 和 plugin3 的依赖关系,因此交换了它们的位置。由于 framework1 继承了 egg,因此它的加载顺序会晚于 egg。应用将最后加载。 -请查看 [Loader.getLoadUnits](https://github.com/eggjs/egg-core/blob/65ea778a4f2156a9cebd3951dac12c4f9455e636/lib/loader/egg_loader.js#L233) 方法 +更多信息请查看 [Loader.getLoadUnits](https://github.com/eggjs/egg-core/blob/65ea778a4f2156a9cebd3951dac12c4f9455e636/lib/loader/egg_loader.js#L233) 方法。 ### 文件顺序 -上面已经列出了默认会加载的文件,Egg 会按如下文件顺序加载,每个文件或目录再根据 loadUnit 的顺序去加载(应用、框架、插件各有不同)。 +上文已经列出了默认会加载的文件。Egg 会按照如下文件顺序进行加载,每个文件或目录再根据 loadUnit 的顺序去加载(应用、框架、插件各有不同): -- 加载 [plugin](./plugin.md),找到应用和框架,加载 `config/plugin.js` -- 加载 [config](../basics/config.md),遍历 loadUnit 加载 `config/config.{env}.js` -- 加载 [extend](../basics/extend.md),遍历 loadUnit 加载 `app/extend/xx.js` -- [自定义初始化](../basics/app-start.md),遍历 loadUnit 加载 `app.js` 和 `agent.js` -- 加载 [service](../basics/service.md),遍历 loadUnit 加载 `app/service` 目录 -- 加载 [middleware](../basics/middleware.md),遍历 loadUnit 加载 `app/middleware` 目录 -- 加载 [controller](../basics/controller.md),加载应用的 `app/controller` 目录 -- 加载 [router](../basics/router.md),加载应用的 `app/router.js` +1. 加载 [plugin](./plugin.md),找到应用和框架,加载 `config/plugin.js`。 +2. 加载 [config](../basics/config.md),遍历 loadUnit 加载 `config/config.{env}.js`。 +3. 加载 [extend](../basics/extend.md),遍历 loadUnit 加载 `app/extend/xx.js`。 +4. [自定义初始化](../basics/app-start.md),遍历 loadUnit 加载 `app.js` 和 `agent.js`。 +5. 加载 [service](../basics/service.md),遍历 loadUnit 加载 `app/service` 目录。 +6. 加载 [middleware](../basics/middleware.md),遍历 loadUnit 加载 `app/middleware` 目录。 +7. 加载 [controller](../basics/controller.md),加载应用的 `app/controller` 目录。 +8. 加载 [router](../basics/router.md),加载应用的 `app/router.js`。 -注意: - -- 加载时如果遇到同名的会覆盖,比如想要覆盖 `ctx.ip` 可以直接在应用的 `app/extend/context.js` 定义 ip 就可以了。 -- 应用完整启动顺序查看[框架开发](./framework.md) +请注意: +- 加载时如果遇到同名文件将会被覆盖。比如,如果想要覆盖 `ctx.ip`,可以在应用的 `app/extend/context.js` 中直接定义 `ip`。 +- 应用完整启动顺序请查看[框架开发](./framework.md)。 ### 生命周期 -框架提供了这些生命周期函数供开发人员处理: +框架提供了以下生命周期函数供开发者使用: -- 配置文件即将加载,这是最后动态修改配置的时机(`configWillLoad`) -- 配置文件加载完成(`configDidLoad`) -- 文件加载完成(`didLoad`) +- 配置文件即将加载,为修改配置的最后机会(`configWillLoad`) +- 配置文件已加载完成(`configDidLoad`) +- 文件已加载完成(`didLoad`) - 插件启动完毕(`willReady`) - worker 准备就绪(`didReady`) - 应用启动完成(`serverDidReady`) - 应用即将关闭(`beforeClose`) -定义如下: +定义方法如下: ```js -// app.js or agent.js +// app.js 或 agent.js class AppBootHook { constructor(app) { this.app = app; } configWillLoad() { - // Ready to call configDidLoad, - // Config, plugin files are referred, - // this is the last chance to modify the config. + // 准备调用 configDidLoad, + // 配置文件和插件文件将被引用, + // 这是修改配置的最后机会。 } configDidLoad() { - // Config, plugin files have been loaded. + // 配置文件和插件文件已被加载。 } async didLoad() { - // All files have loaded, start plugin here. + // 所有文件已加载,这里开始启动插件。 } async willReady() { - // All plugins have started, can do some thing before app ready + // 所有插件已启动,在应用准备就绪前可执行一些操作。 } async didReady() { - // Worker is ready, can do some things - // don't need to block the app boot. + // worker 已准备就绪,在这里可以执行一些操作, + // 这些操作不会阻塞应用启动。 } async serverDidReady() { - // Server is listening. + // 服务器已开始监听。 } async beforeClose() { - // Do some thing before app close. + // 应用关闭前执行一些操作。 } } module.exports = AppBootHook; ``` -开发者使用类的方式定义 `app.js` 和 `agent.js` 之后, 框架会自动加载并实例化这个类, 并且在各个生命周期阶段调用对应的方法。 +开发者使用类的方式定义 `app.js` 和 `agent.js` 后,框架将自动加载并实例化这个类,并在各个生命周期阶段调用相应的方法。 -启动过程如图所示: +启动过程如图所示: ![](https://user-images.githubusercontent.com/40081831/47344271-a688d500-d6da-11e8-96e9-663fa9f45108.png) -**使用 `beforeClose` 的时候需要注意,在框架的进程关闭处理中是有超时时间的,如果 worker 进程在接收到进程退出信号之后,没有在所规定的时间内退出,将会被强制关闭。** +**在使用 `beforeClose` 时,需要注意:框架在处理关闭进程时设有超时限制。如果 worker 进程在收到退出信号后,未能在规定时间内退出,则会被强制终止。** -如果需要调整超时时间的话,查看[此处文档](https://github.com/eggjs/egg-cluster)。 +如需调整超时时间,请查阅[相关文档](https://github.com/eggjs/egg-cluster)。 -弃用的方法: +弃用的方法: ## beforeStart -`beforeStart` 方法在 loading 过程中调用, 所有的方法并行执行。 一般用来执行一些异步方法, 例如检查连接状态等, 比如 [`egg-mysql`](https://github.com/eggjs/egg-mysql/blob/master/lib/mysql.js) 就用 `beforeStart` 来检查与 mysql 的连接状态。所有的 `beforeStart` 任务结束后, 状态将会进入 `ready` 。不建议执行一些耗时较长的方法, 可能会导致应用启动超时。插件开发者应使用 `didLoad` 替换。应用开发者应使用 `willReady` 替换。 +`beforeStart` 方法在加载过程中调用,所有方法并行执行。通常用于执行一些异步任务,例如检查连接状态等。例如,[`egg-mysql`](https://github.com/eggjs/egg-mysql/blob/master/lib/mysql.js) 使用 `beforeStart` 来检查 MySQL 的连接状态。所有 `beforeStart` 任务结束后,应用将进入 `ready` 状态。不建议执行耗时长的方法,可能导致应用启动超时。插件开发者应使用 `didLoad` 替代,应用开发者应使用 `willReady` 替代。 ## ready -`ready` 方法注册的任务在 load 结束并且所有的 `beforeStart` 方法执行结束后顺序执行, HTTP server 监听也是在这个时候开始, 此时代表所有的插件已经加载完毕并且准备工作已经完成, 一般用来执行一些启动的后置任务。开发者应使用 `didReady` 替换。 +注册到 `ready` 方法的任务将在加载结束后,所有 `beforeStart` 方法执行完毕后顺序执行,HTTP 服务器监听也在此时开始。此时代表所有插件已加载完成且准备工作已完成,通常用于执行一些启动后置任务。开发者应使用 `didReady` 替代。 ## beforeClose -`beforeClose` 注册方法在 app/agent 实例的 `close` 方法被调用后, 按注册的逆序执行。一般用于资源的释放操作, 例如 [`egg`](https://github.com/eggjs/egg/blob/master/lib/egg.js) 用来关闭 logger、删除监听方法等。开发者不应该直接使用 `app.beforeClose`, 而是定义类的形式, 实现 `beforeClose` 方法。 +`beforeClose` 注册方法在 app/agent 实例的 `close` 方法调用后,按注册的逆序执行。通常用于资源释放操作,例如 [`egg`](https://github.com/eggjs/egg/blob/master/lib/egg.js) 用于关闭日志、移除监听器等。开发者不应直接使用 `app.beforeClose`,而是通过定义类的形式,实现 `beforeClose` 方法。 -**这个方法不建议在生产环境使用, 可能遇到未执行完就结束进程的问题。** +**此方法不建议在生产环境使用,因可能会出现未完全执行结束就结束进程的情况。** -此外,我们可以使用 [`egg-development`](https://github.com/eggjs/egg-development#loader-trace) 来查看加载过程。 +另外,我们可以使用 [`egg-development`](https://github.com/eggjs/egg-development#loader-trace) 来查看加载过程。 ### 文件加载规则 -框架在加载文件时会进行转换,因为文件命名风格和 API 风格存在差异。我们推荐文件使用下划线,而 API 使用驼峰。比如 `app/service/user_info.js` 会转换成 `app.service.userInfo`。 +框架在加载文件时会进行转换,因为文件命名风格与 API 风格有所差异。我们推荐文件使用下划线命名,而 API 使用驼峰命名。例如 `app/service/user_info.js` 会转换为 `app.service.userInfo`。 -框架也支持连字符和驼峰的方式 +框架也支持其它风格命名的文件;连字符和驼峰方式命名的文件同样支持: - `app/service/user-info.js` => `app.service.userInfo` - `app/service/userInfo.js` => `app.service.userInfo` -Loader 还提供了 [caseStyle](#caseStyle-string) 强制指定首字母大小写,比如加载 model 时 API 首字母大写,`app/model/user.js` => `app.model.User`,就可以指定 `caseStyle: 'upper'`。 - +Loader 也提供了 [caseStyle](#caseStyle-string) 设置来强制指定命名方式,如将 model 加载时的 API 首字母大写,`app/model/user.js` => `app.model.User`,可指定 `caseStyle: 'upper'`。 ## 扩展 Loader -[Loader] 是一个基类,并根据文件加载的规则提供了一些内置的方法,它本身并不会去调用这些方法,而是由继承类调用。 - -- loadPlugin() -- loadConfig() -- loadAgentExtend() -- loadApplicationExtend() -- loadRequestExtend() -- loadResponseExtend() -- loadContextExtend() -- loadHelperExtend() -- loadCustomAgent() -- loadCustomApp() -- loadService() -- loadMiddleware() -- loadController() -- loadRouter() - -Egg 基于 Loader 实现了 [AppWorkerLoader] 和 [AgentWorkerLoader],上层框架基于这两个类来扩展,**Loader 的扩展只能在框架进行**。 +`Loader` 是一个基类,并根据文件加载的规则提供了一些内置的方法。它本身并不会去调用这些方法,而是由继承类调用。 + +- `loadPlugin()` +- `loadConfig()` +- `loadAgentExtend()` +- `loadApplicationExtend()` +- `loadRequestExtend()` +- `loadResponseExtend()` +- `loadContextExtend()` +- `loadHelperExtend()` +- `loadCustomAgent()` +- `loadCustomApp()` +- `loadService()` +- `loadMiddleware()` +- `loadController()` +- `loadRouter()` + +`Egg` 基于 `Loader` 实现了 `AppWorkerLoader` 和 `AgentWorkerLoader`,上层框架基于这两个类来扩展。**Loader 的扩展只能在框架进行**。 ```js // 自定义 AppWorkerLoader @@ -348,17 +345,16 @@ module.exports = Object.assign(egg, { }); ``` -通过 Loader 提供的这些 API,可以很方便的定制团队的自定义加载,如 `this.model.xx`,`app/extend/filter.js` 等等。 - -以上只是说明 Loader 的写法,具体可以查看[框架开发](./framework.md)。 +通过 `Loader` 提供的这些 API,可以很方便地定制团队的自定义加载,例如 `this.model.xx`,`app/extend/filter.js` 等等。 +以上只是说明 `Loader` 的写法,具体可以查看[框架开发](./framework.md)。 ## 加载器函数(Loader API) -Loader 还提供一些底层的 API,在扩展时可以简化代码,[点击此处](https://github.com/eggjs/egg-core#eggloader)查看所有相关 API。 +Loader 提供了一些基础 API,方便在扩展时简化代码。想了解所有相关 API,请[点击此处](https://github.com/eggjs/egg-core#eggloader)。 ### loadFile -用于加载一个文件,比如加载 `app/xx.js` 就是使用这个方法。 +此函数用来加载文件,例如加载 `app/xx.js` 就会用到它。 ```js // app/xx.js @@ -367,18 +363,18 @@ module.exports = (app) => { }; // app.js -// 以 app/xx.js 为例,我们可以在 app.js 加载这个文件 +// 以 app/xx.js 为例子,在 app.js 中加载此文件: const path = require('path'); module.exports = (app) => { app.loader.loadFile(path.join(app.config.baseDir, 'app/xx.js')); }; ``` -如果文件 export 一个函数会被调用,并将 app 作为参数,否则直接使用这个值。 +如果文件导出了一个函数,这个函数会被调用,`app` 作为参数传入;如果不是函数,则直接使用文件导出的值。 ### loadToApp -用于加载一个目录下的文件到 app,比如 `app/controller/home.js` 会加载到 `app.controller.home`。 +此函数用来将一个目录下的文件加载到 app 对象上,例如 `app/controller/home.js` 会被加载到 `app.controller.home`。 ```js // app.js @@ -389,17 +385,17 @@ module.exports = (app) => { }; ``` -一共有三个参数 `loadToApp(directory, property, LoaderOptions)` +`loadToApp` 有三个参数:`loadToApp(directory, property, LoaderOptions)` -1. directory 可以为 String 或 Array,Loader 会从这些目录加载文件 -1. property 为 app 的属性 -1. [LoaderOptions](#LoaderOptions) 为一些配置 +1. `directory` 可以是字符串或数组。Loader 会从这些目录中加载文件。 +2. `property` 是 app 的属性名。 +3. [`LoaderOptions`](#LoaderOptions) 包含了一些配置选项。 ### loadToContext -与 loadToApp 有一点差异,loadToContext 是加载到 ctx 上而非 app,而且是懒加载。加载时会将文件都放到一个临时对象上,在调用 ctx API 时才实例化对象。 +`loadToContext` 与 `loadToApp` 略有不同,它是将文件加载到 `ctx` 上,而不是 `app`,并且支持懒加载。加载操作会将文件放到一个临时对象中,在调用 `ctx` API 时才去实例化。 -比如 service 的加载就是使用这种模式 +例如,加载 service 文件的方式就用到了这种模式: ```js // 以下为示例,请使用 loadService @@ -415,33 +411,31 @@ const servicePaths = app.loader .map((unit) => path.join(unit.path, 'app/service')); app.loader.loadToContext(servicePaths, 'service', { - // service 需要继承 app.Service,所以要拿到 app 参数 - // 设置 call 在加载时会调用函数返回 UserService + // service 需要继承 app.Service,因此需要 app 参数 + // 设置 call 为 true,会在加载时调用函数,并返回 UserService call: true, // 将文件加载到 app.serviceClasses fieldClass: 'serviceClasses', }); ``` -文件加载后 `app.serviceClasses.user` 就是 UserService,当调用 `ctx.service.user` 时会实例化 UserService, -所以这个类只有每次请求中首次访问时才会实例化,实例化后会被缓存,同一个请求多次调用也只会实例化一次。 - +文件加载完成后,`app.serviceClasses.user` 就代表 UserService 类。当调用 `ctx.service.user` 时,会实例化 UserService 类。因此,这个类只有在每次请求中首次被访问时才会实例化。实例化后,对象会被缓存,同一个请求中多次调用也只实例化一次。 ### LoaderOptions #### ignore [String] -`ignore` 可以忽略一些文件,支持 glob,默认为空 +`ignore` 可用于忽略某些文件,支持 glob 匹配模式,默认值为空。 ```js app.loader.loadToApp(directory, 'controller', { - // 忽略 app/controller/util 下的文件 + // 忽略 app/controller/util 目录下的文件 ignore: 'util/**', }); ``` #### initializer [Function] -对每个文件 export 出来的值进行处理,默认为空 +对每个文件 export 的值进行处理,此项默认为空。 ```js // app/model/user.js @@ -449,12 +443,12 @@ module.exports = class User { constructor(app, path) {} }; -// 从 app/model 目录加载,加载时可做一些初始化处理 +// 从 app/model 目录加载,且可以在加载时进行一些初始化处理 const directory = path.join(app.config.baseDir, 'app/model'); app.loader.loadToApp(directory, 'model', { initializer(model, opt) { // 第一个参数为 export 的对象 - // 第二个参数为一个对象,只包含当前文件的路径 + // 第二个参数为一个对象,里面包含当前文件的路径 return new model(app, opt.path); }, }); @@ -462,53 +456,55 @@ app.loader.loadToApp(directory, 'model', { #### caseStyle [String] -文件的转换规则,可选为 `camel`,`upper`,`lower`,默认为 `camel`。 +设置文件命名的转换规则,可选项为 `camel`、`upper` 或 `lower`,默认值为 `camel`。 -三者都会将文件名转换成驼峰,但是对于首字母的处理有所不同。 +这些选项都会将文件名转换为驼峰命名,但是首字符的大小写处理不同: +- `camel`:首字母保持不变。 +- `upper`:首字母转为大写。 +- `lower`:首字母转为小写。 -- `camel`:首字母不变。 -- `upper`:首字母大写。 -- `lower`:首字母小写。 +根据不同文件类型设置相应的转换规则,如下表所示: -在加载不同文件时配置不同 - -| 文件 | 配置 | -| -------------- | ----- | -| app/controller | lower | -| app/middleware | lower | -| app/service | lower | +| 文件类型 | `caseStyle` 配置 | +| ------------- | -------------- | +| app/controller | lower | +| app/middleware | lower | +| app/service | lower | #### override [Boolean] -遇到已经存在的文件时是直接覆盖还是抛出异常,默认为 false +当存在同名文件时,是否覆盖原有文件,或抛出异常。默认值为 `false`。 -比如同时加载应用和插件的 `app/service/user.js` 文件,如果为 true 应用会覆盖插件的,否则加载应用的文件时会报错。 +例如,当同时加载应用和插件中的 `app/service/user.js` 文件时: +- 若 `override` 设为 `true`,则应用中的文件会覆盖插件中的同名文件。 +- 若设为 `false`,则在尝试加载应用中的文件时会报错。 -在加载不同文件时配置不同 +根据不同文件类型设置 `override` 的配置值,如下表所示: -| 文件 | 配置 | -| -------------- | ----- | -| app/controller | true | -| app/middleware | false | -| app/service | false | +| 文件类型 | `override` 配置 | +| ------------- | --------------- | +| app/controller | true | +| app/middleware | false | +| app/service | false | #### call [Boolean] -当 export 的对象为函数时则调用,并获取返回值,默认为 true +若 export 出的对象是函数,则可以调用此函数并获取其返回值,默认值为 `true`。 + +根据不同文件类型设置 `call` 的配置值,如下表所示: -在加载不同文件时配置不同 +| 文件类型 | `call` 配置 | +| ------------- | ------------- | +| app/controller | true | +| app/middleware | false | +| app/service | true | -| 文件 | 配置 | -| -------------- | ----- | -| app/controller | true | -| app/middleware | false | -| app/service | true | ## CustomLoader -`loadToContext` 和 `loadToApp` 可被 `customLoader` 配置替代。 +`loadToContext` 和 `loadToApp` 方法可以通过 `customLoader` 的配置来替代。 -如使用 `loadToApp` 加载的代码如下 +以下是用 `loadToApp` 方法加载代码的示例: ```js // app.js @@ -518,26 +514,27 @@ module.exports = (app) => { }; ``` -换成 `customLoader` 后变为 +改为使用 `customLoader` 后的写法是: ```js // config/config.default.js module.exports = { customLoader: { - // 定义在 app 上的属性名 app.adapter + // 在 app 对象上定义的属性名为 app.adapter adapter: { - // 相对于 app.config.baseDir + // 路径相对于 app.config.baseDir directory: 'app/adapter', - // 如果是 ctx 则使用 loadToContext + // 如果用于 ctx,则应该使用 loadToContext 方法 inject: 'app', // 是否加载框架和插件的目录 loadunit: false, - // 还可以定义其他 LoaderOptions + // 也可以定义其他 LoaderOptions }, }, }; ``` -[loader]: https://github.com/eggjs/egg-core/blob/master/lib/loader/egg_loader.js -[appworkerloader]: https://github.com/eggjs/egg/blob/master/lib/loader/app_worker_loader.js -[agentworkerloader]: https://github.com/eggjs/egg/blob/master/lib/loader/agent_worker_loader.js +参考链接: +- [loader]: https://github.com/eggjs/egg-core/blob/master/lib/loader/egg_loader.js +- [appworkerloader]: https://github.com/eggjs/egg/blob/master/lib/loader/app_worker_loader.js +- [agentworkerloader]: https://github.com/eggjs/egg/blob/master/lib/loader/agent_worker_loader.js \ No newline at end of file diff --git a/site/docs/advanced/plugin.zh-CN.md b/site/docs/advanced/plugin.zh-CN.md index 7f846b1a70..3372cf9775 100644 --- a/site/docs/advanced/plugin.zh-CN.md +++ b/site/docs/advanced/plugin.zh-CN.md @@ -3,15 +3,14 @@ title: 插件开发 order: 2 --- -插件机制是我们框架的一大特色。它不但可以保证框架核心的足够精简、稳定、高效,还可以促进业务逻辑的复用,生态圈的形成。有人可能会问了: +插件机制是我们框架的一大特色。它不但可以保证框架核心的足够精简、稳定、高效,还可以促进业务逻辑的复用,生态圈的形成。有人可能会问: - Koa 已经有了中间件的机制,为啥还要插件呢? - 中间件、插件、应用它们之间是什么关系,有什么区别? - 我该怎么使用一个插件? - 如何编写一个插件? -- ... -在[使用插件](../basics/plugin.md)章节我们已经讨论过前几点,接下来我们来看看如何开发一个插件。 +在 [使用插件](../basics/plugin.md) 章节我们已经讨论过前几点,接下来我们来看看如何开发一个插件。 ## 插件开发 @@ -28,30 +27,30 @@ $ npm test ## 插件的目录结构 -一个插件其实就是一个『迷你的应用』,下面展示的是一个插件的目录结构,和应用(app)几乎一样。 +一个插件其实就是一个“迷你的应用”,下面展示的是一个插件的目录结构,和应用(app)几乎一样。 -```js -. egg-hello +```plaintext +.egg-hello ├── package.json -├── app.js (可选) -├── agent.js (可选) +├── app.js(可选) +├── agent.js(可选) ├── app -│ ├── extend (可选) -│ | ├── helper.js (可选) -│ | ├── request.js (可选) -│ | ├── response.js (可选) -│ | ├── context.js (可选) -│ | ├── application.js (可选) -│ | └── agent.js (可选) -│ ├── service (可选) -│ └── middleware (可选) +│ ├── extend(可选) +│ │ ├── helper.js(可选) +│ │ ├── request.js(可选) +│ │ ├── response.js(可选) +│ │ ├── context.js(可选) +│ │ ├── application.js(可选) +│ │ └── agent.js(可选) +│ ├── service(可选) +│ └── middleware(可选) │ └── mw.js ├── config -| ├── config.default.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 └── mw.test.js @@ -62,7 +61,7 @@ $ npm test 1. 插件没有独立的 router 和 controller。这主要出于几点考虑: - 路由一般和应用强绑定的,不具备通用性。 - - 一个应用可能依赖很多个插件,如果插件支持路由可能导致路由冲突。 + - 一个应用可能依赖很多个插件,如果插件支持路由可能会导致路由冲突。 - 如果确实有统一路由的需求,可以考虑在插件里通过中间件来实现。 2. 插件需要在 `package.json` 中的 `eggPlugin` 节点指定插件特有的信息: @@ -70,7 +69,7 @@ $ npm test - `{String} name` - 插件名(必须配置),具有唯一性,配置依赖关系时会指定依赖插件的 name。 - `{Array} dependencies` - 当前插件强依赖的插件列表(如果依赖的插件没找到,应用启动失败)。 - `{Array} optionalDependencies` - 当前插件的可选依赖插件列表(如果依赖的插件未开启,只会 warning,不会影响应用启动)。 - - `{Array} env` - 只有在指定运行环境才能开启,具体有哪些环境可以参考[运行环境](../basics/env.md)。此配置是可选的,一般情况下都不需要配置。 + - `{Array} env` - 只有在指定运行环境才能开启,具体有哪些环境可以参考 [运行环境](../basics/env.md)。此配置是可选的,一般情况下都不需要配置。 ```json { @@ -87,11 +86,11 @@ $ npm test 3. 插件没有 `plugin.js`: - `eggPlugin.dependencies` 只是用于声明依赖关系,而不是引入插件或开启插件。 - - 如果期望统一管理多个插件的开启和配置,可以在[上层框架](./framework.md)处理。 + - 如果期望统一管理多个插件的开启和配置,可以在 [上层框架](./framework.md) 处理。 ## 插件的依赖管理 -和中间件不同,插件是自己管理依赖的。应用在加载所有插件前会预先从它们的 `package.json` 中读取 `eggPlugin > dependencies` 和 `eggPlugin > optionalDependencies` 节点,然后根据依赖关系计算出加载顺序,举个例子,下面三个插件的加载顺序就应该是 `c => b => a` +和中间件不同,插件是自己管理依赖的。应用在加载所有插件前会预先从它们的 `package.json` 中读取 `eggPlugin > dependencies` 和 `eggPlugin > optionalDependencies` 节点,然后根据依赖关系计算出加载顺序,举个例子,下面三个插件的加载顺序就应该是 `c => b => a`。 ```json // plugin a @@ -99,7 +98,7 @@ $ npm test "name": "egg-plugin-a", "eggPlugin": { "name": "a", - "dependencies": [ "b" ] + "dependencies": ["b"] } } @@ -108,7 +107,7 @@ $ npm test "name": "egg-plugin-b", "eggPlugin": { "name": "b", - "optionalDependencies": [ "c" ] + "optionalDependencies": ["c"] } } @@ -123,31 +122,30 @@ $ npm test **注意:`dependencies` 和 `optionalDependencies` 的取值是另一个插件的 `eggPlugin.name`,而不是 `package name`。** -`dependencies` 和 `optionalDependencies` 是从 `npm` 借鉴来的概念,大多数情况下我们都使用 `dependencies`,这也是我们最推荐的依赖方式。那什么时候可以用 `optionalDependencies` 呢?大致就两种: - -- 只在某些环境下才依赖,比如:一个鉴权插件,只在开发环境依赖一个 mock 数据的插件 -- 弱依赖,比如:A 依赖 B,但是如果没有 B,A 有相应的降级方案 +`dependencies` 和 `optionalDependencies` 是从 npm 借鉴来的概念,大多数情况下我们都使用 `dependencies`,这也是我们最推荐的依赖方式。那什么时候可以用 `optionalDependencies` 呢?大致就两种: -需要特别强调的是:如果采用 `optionalDependencies` 那么框架不会校验依赖的插件是否开启,它的作用仅仅是计算加载顺序。所以,这时候依赖方需要通过『接口探测』等方式来决定相应的处理逻辑。 +- 只在某些环境下才依赖,比如:一个鉴权插件,只在开发环境依赖一个 mock 数据的插件。 +- 弱依赖,比如:A 依赖 B,但是如果没有 B,A 有相应的降级方案。 +需要特别强调的是:如果采用 `optionalDependencies`,那么框架不会校验依赖的插件是否开启,它的作用仅仅是计算加载顺序。所以,这时候依赖方需要通过“接口探测”等方式来决定相应的处理逻辑。 ## 插件能做什么? 上面给出了插件的定义,那插件到底能做什么? ### 扩展内置对象的接口 -在插件相应的文件内对框架内置对象进行扩展,和应用一样 +在插件相应的文件内对框架内置对象进行扩展,和应用一样: - `app/extend/request.js` - 扩展 Koa#Request 类 - `app/extend/response.js` - 扩展 Koa#Response 类 - `app/extend/context.js` - 扩展 Koa#Context 类 -- `app/extend/helper.js ` - 扩展 Helper 类 +- `app/extend/helper.js` - 扩展 Helper 类 - `app/extend/application.js` - 扩展 Application 类 - `app/extend/agent.js` - 扩展 Agent 类 ### 插入自定义中间件 -1. 首先在 `app/middleware` 目录下定义好中间件实现 +1. 首先在 `app/middleware` 目录下定义好中间件实现: ```js 'use strict'; @@ -163,7 +161,7 @@ $ npm test 'Must set `app.config.static.dir` when static plugin enable', ); - // ensure directory exists + // 确保目录存在 mkdirp.sync(options.dir); app.loggers.coreLogger.info( @@ -176,7 +174,7 @@ $ npm test }; ``` -2. 在 `app.js` 中将中间件插入到合适的位置(例如:下面将 static 中间件放到 bodyParser 之前) +2. 在 `app.js` 中将中间件插入到合适的位置(例如:下面将 static 中间件放到 bodyParser 之前): ```js const assert = require('assert'); @@ -192,7 +190,7 @@ $ npm test ### 在应用启动时做一些初始化工作 -- 我在启动前想读取一些本地配置 +- 我在启动前想读取一些本地配置: ```js // ${plugin_root}/app.js @@ -202,11 +200,11 @@ $ npm test module.exports = (app) => { app.customData = fs.readFileSync(path.join(app.config.baseDir, 'data.bin')); - app.coreLogger.info('read data ok'); + app.coreLogger.info('Data read successfully'); }; ``` -- 如果有异步启动逻辑,可以使用 `app.beforeStart` API +- 如果有异步启动逻辑,可以使用 `app.beforeStart` API: ```js // ${plugin_root}/app.js @@ -219,12 +217,12 @@ $ npm test }); app.beforeStart(async () => { await app.myClient.ready(); - app.coreLogger.info('my client is ready'); + app.coreLogger.info('My client is ready'); }); }; ``` -- 也可以添加 agent 启动逻辑,使用 `agent.beforeStart` API +- 也可以添加 agent 启动逻辑,使用 `agent.beforeStart` API: ```js // ${plugin_root}/agent.js @@ -237,11 +235,10 @@ $ npm test }); agent.beforeStart(async () => { await agent.myClient.ready(); - agent.coreLogger.info('my client is ready'); + agent.coreLogger.info('My client is ready'); }); }; ``` - ### 设置定时任务 1. 在 `package.json` 里设置依赖 schedule 插件 @@ -256,45 +253,43 @@ $ npm test } ``` -2. 在 `${plugin_root}/app/schedule/` 目录下新建文件,编写你的定时任务 +2. 在 `${plugin_root}/app/schedule/` 目录下新建文件,编写你的定时任务。 ```js exports.schedule = { type: 'worker', cron: '0 0 3 * * *', // interval: '1h', - // immediate: true, + // immediate: true }; exports.task = async (ctx) => { - // your logic code + // 你的逻辑代码 }; ``` ### 全局实例插件的最佳实践 -许多插件的目的都是将一些已有的服务引入到框架中,如 [egg-mysql], [egg-oss]。他们都需要在 app 上创建对应的实例。而在开发这一类的插件时,我们发现存在一些普遍性的问题: +许多插件的目的都是将一些已有的服务引入到框架中,如`egg-mysql`、`egg-oss`。它们都需要在 app 上创建对应的实例。而在开发这一类插件时,我们发现存在一些普遍性的问题: - 在一个应用中同时使用同一个服务的不同实例(连接到两个不同的 MySQL 数据库)。 - 从其他服务获取配置后动态初始化连接(从配置中心获取到 MySQL 服务地址后再建立连接)。 -如果让插件各自实现,可能会出现各种奇怪的配置方式和初始化方式,所以框架提供了 `app.addSingleton(name, creator)` 方法来统一这一类服务的创建。需要注意的是在使用 `app.addSingleton(name, creator)` 方法时,配置文件中一定要有 `client` 或者 `clients` 为 key 的配置作为传入 `creator` 函数 的 `config`。 +如果让插件各自实现,可能会出现各种奇怪的配置方式和初始化方式,所以框架提供了 `app.addSingleton(name, creator)` 方法来统一这类服务的创建。需要注意的是,在使用 `app.addSingleton(name, creator)` 方法时,配置文件中一定要有 `client` 或者 `clients` 为 key 的配置。 #### 插件写法 -我们将 [egg-mysql] 的实现简化之后来看看如何编写此类插件: +以下代码展示了如何编写这类插件,它是对 `egg-mysql` 插件实现的简化: ```js // egg-mysql/app.js -module.exports = (app) => { - // 第一个参数 mysql 指定了挂载到 app 上的字段,我们可以通过 `app.mysql` 访问到 MySQL singleton 实例 - // 第二个参数 createMysql 接受两个参数(config, app),并返回一个 MySQL 的实例 +module.exports = app => { app.addSingleton('mysql', createMysql); }; /** - * @param {Object} config 框架处理之后的配置项,如果应用配置了多个 MySQL 实例,会将每一个配置项分别传入并调用多次 createMysql - * @param {Application} app 当前的应用 + * @param {Object} config 框架处理后的配置项,如应用配置了多个 MySQL 实例,每个配置项会分别传入并多次调用 createMysql + * @param {Application} app 当前应用 * @return {Object} 返回创建的 MySQL 实例 */ function createMysql(config, app) { @@ -302,45 +297,35 @@ function createMysql(config, app) { // 创建实例 const client = new Mysql(config); - // 做启动应用前的检查 + // 应用启动前检查 app.beforeStart(async () => { const rows = await client.query('select now() as currentTime;'); - app.coreLogger.info( - `[egg-mysql] init instance success, rds currentTime: ${rows[0].currentTime}`, - ); + app.coreLogger.info(`[egg-mysql] init instance success, rds currentTime: ${rows[0].currentTime}`); }); return client; } ``` -初始化方法也支持 `Async function`,便于有些特殊的插件需要异步化获取一些配置文件: +初始化方法也支持 `async function`,便于有些需要异步获取配置文件的特殊插件: ```js async function createMysql(config, app) { // 异步获取 mysql 配置 const mysqlConfig = await app.configManager.getMysqlConfig(config.mysql); - assert( - mysqlConfig.host && - mysqlConfig.port && - mysqlConfig.user && - mysqlConfig.database, - ); + assert(mysqlConfig.host && mysqlConfig.port && mysqlConfig.user && mysqlConfig.database); // 创建实例 const client = new Mysql(mysqlConfig); - // 做启动应用前的检查 + // 应用启动前检查 const rows = await client.query('select now() as currentTime;'); - app.coreLogger.info( - `[egg-mysql] init instance success, rds currentTime: ${rows[0].currentTime}`, - ); + app.coreLogger.info(`[egg-mysql] init instance success, rds currentTime: ${rows[0].currentTime}`); return client; } ``` -可以看到,插件中我们只需要提供要挂载的字段以及对应服务的初始化方法,所有的配置管理、实例获取方式都由框架封装并统一提供了。 - +可以看到,插件中我们只需要提供要挂载的字段和服务的初始化方法,所有配置管理、实例获取方式由框架封装并统一提供。 #### 应用层使用方案 ##### 单实例 @@ -369,19 +354,19 @@ async function createMysql(config, app) { class PostController extends Controller { async list() { const posts = await this.app.mysql.query(sql, values); - }, + } } ``` ##### 多实例 -1. 同样需要在配置文件中声明 MySQL 的配置,不过和单实例时不同,配置项中需要有一个 `clients` 字段,分别申明不同实例的配置,同时可以通过 `default` 字段来配置多个实例中共享的配置(如 host 和 port)。需要注意的是在这种情况下要用 `get` 方法指定相应的实例。(例如:使用 `app.mysql.get('db1').query()`,而不是直接使用 `app.mysql.query()` 得到一个 `undefined`)。 +1. 同样需要在配置文件中声明 MySQL 的配置,不过和单实例时不同,配置项中需要有一个 `clients` 字段,分别声明不同实例的配置。同时,可以通过 `default` 字段配置多个实例中共享的配置(如 host 和 port)。需要注意的是,在这种情况下要用 `get` 方法指定相应的实例。(例如:使用 `app.mysql.get('db1').query()`,而不是直接使用 `app.mysql.query()`,否则可能得到一个 `undefined` )。 ```js // config/config.default.js exports.mysql = { clients: { - // clientId, access the client instance by app.mysql.get('clientId') + // clientId,可通过 app.mysql.get('clientId') 访问客户端实例 db1: { user: 'user1', password: 'upassword1', @@ -393,7 +378,7 @@ async function createMysql(config, app) { database: 'db2', }, }, - // default configuration for all databases + // 所有数据库的默认配置 default: { host: 'mysql.com', port: '3306', @@ -401,26 +386,26 @@ async function createMysql(config, app) { }; ``` -2. 通过 `app.mysql.get('db1')` 来获取对应的实例并使用。 +2. 通过 `app.mysql.get('db1')` 获取对应的实例并使用。 ```js // app/controller/post.js class PostController extends Controller { async list() { const posts = await this.app.mysql.get('db1').query(sql, values); - }, + } } ``` ##### 动态创建实例 -我们可以不需要将配置提前申明在配置文件中,而是在应用运行时动态的初始化一个实例。 +我们可以不需要将配置提前声明在配置文件中,而是在应用运行时动态初始化一个实例。 ```js // app.js -module.exports = (app) => { +module.exports = app => { app.beforeStart(async () => { - // 从配置中心获取 MySQL 的配置 { host, post, password, ... } + // 从配置中心获取 MySQL 配置 { host, port, password, ... } const mysqlConfig = await app.configCenter.fetch('mysql'); // 动态创建 MySQL 实例 app.database = await app.mysql.createInstanceAsync(mysqlConfig); @@ -428,72 +413,72 @@ module.exports = (app) => { }; ``` -通过 `app.database` 来使用这个实例。 +通过 `app.database` 使用这个实例。 ```js // app/controller/post.js class PostController extends Controller { async list() { const posts = await this.app.database.query(sql, values); - }, + } } ``` -**注意,在动态创建实例的时候,框架也会读取配置中 `default` 字段内的配置项作为默认配置。** +**注意,在动态创建实例时,框架还会读取配置中 `default` 字段的配置作为默认配置项。** ### 插件的寻址规则 -框架在加载插件的时候,遵循下面的寻址规则: - -- 如果配置了 path,直接按照 path 加载。 -- 没有 path 根据 package 名去查找,查找的顺序依次是: +框架加载插件时,遵循以下寻址规则: +- 如果配置了 `path`,直接按照 `path` 加载。 +- 没有 `path` 时,根据包名(package name)查找,查找顺序依次是: 1. 应用根目录下的 `node_modules` 2. 应用依赖框架路径下的 `node_modules` - 3. 当前路径下的 `node_modules` (主要是兼容单元测试场景) - + 3. 当前路径下的 `node_modules`(主要是兼容单元测试场景) ### 插件规范 -我们非常欢迎您贡献新的插件,同时也希望您遵守下面一些规范: +我们非常欢迎你贡献新的插件,同时也希望你遵守下面一些规范: - 命名规范 - - `npm` 包名以 `egg-` 开头,且为全小写,例如:`egg-xx`。比较长的词组用中划线:`egg-foo-bar` - - 对应的插件名使用小驼峰,小驼峰转换规则以 `npm` 包名的中划线为准 `egg-foo-bar` => `fooBar` - - 对于可以中划线也可以不用的情况,不做强制约定,例如:userservice(egg-userservice) 还是 user-service(egg-user-service) 都可以 + - `npm` 包名应以 `egg-` 开头,且应为全小写,例如:`egg-xx`。比较长的词组应使用中划线:`egg-foo-bar`。 + - 对应的插件名应使用小驼峰式命名。小驼峰式的转换规则以 `npm` 包名中的中划线为准,例如 `egg-foo-bar` => `fooBar`。 + - 对于既可以加中划线也可以不加的情况,不做强制约定,例如:`userservice`(`egg-userservice`)或 `user-service`(`egg-user-service`)都可。 + - `package.json` 书写规范 - - 按照上面的文档添加 `eggPlugin` 节点 - - 在 `keywords` 里加上 `egg`、`egg-plugin`、`eggPlugin` 等关键字,便于索引 - - ```json - { - "name": "egg-view-nunjucks", - "version": "1.0.0", - "description": "view plugin for egg", - "eggPlugin": { - "name": "nunjucks", - "dep": ["security"] - }, - "keywords": [ - "egg", - "egg-plugin", - "eggPlugin", - "egg-plugin-view", - "egg-view", - "nunjucks" - ] - } - ``` + - 按照上面的文档添加 `eggPlugin` 节点。 + - 在 `keywords` 里添加 `egg`、`egg-plugin`、`eggPlugin` 等关键字,便于索引。 + +```json +{ + "name": "egg-view-nunjucks", + "version": "1.0.0", + "description": "view plugin for egg", + "eggPlugin": { + "name": "nunjucks", + "dep": ["security"] + }, + "keywords": [ + "egg", + "egg-plugin", + "eggPlugin", + "egg-plugin-view", + "egg-view", + "nunjucks" + ] +} +``` ## 为何不使用 npm 包名来做插件名? -Egg 是通过 `eggPlugin.name` 来定义插件名的,只在应用或框架具备唯一性,也就是说**多个 npm 包可能有相同的插件名**,为什么这么设计呢? +Egg 通过 `eggPlugin.name` 来定义插件名,只需应用或框架具备唯一性,也就是说**多个 npm 包可能有相同的插件名**。为什么这么设计呢? + +首先,Egg 插件不仅支持 npm 包,还支持通过目录来寻找插件。在[渐进式开发](../intro/progressive.md)章节提到了如何使用这两个配置进行代码演进。目录对单元测试也更为友好。所以,Egg 无法通过 npm 包名来确保唯一性。 -首先 Egg 插件不仅仅支持 npm 包,还支持通过目录来找插件。在[渐进式开发](../intro/progressive.md)章节提到如何使用这两个配置来进行代码演进。目录对单元测试也比较友好。所以 Egg 无法通过 npm 的包名来做唯一性。 +更重要的是,Egg 通过这种特性来做适配器。例如,[模板开发规范](./view-plugin.md#插件命名规范)定义的插件名为 `view`,存在 `egg-view-nunjucks`、`egg-view-react` 等插件,使用者只需要更换插件和修改模板,无需修改 Controller,因为所有的模板插件都实现了相同的 API。 -更重要的是 Egg 可以使用这种特性来做适配器。比如[模板开发规范](./view-plugin.md#插件命名规范)定义的插件名为 view,而存在 `egg-view-nunjucks`,`egg-view-react` 等插件,使用者只需要更换插件和修改模板,不需要动 Controller, 因为所有的模板插件都实现了相同的 API。 +**将相同功能的插件赋予相同的插件名,以及提供相同的 API,可以快速进行切换**。这种做法在模板、数据库等领域非常适用。 -**将相同功能的插件赋予相同的插件名,具备相同的 API,可以快速切换**。这在模板、数据库等领域非常适用。 [egg-boilerplate-plugin]: https://github.com/eggjs/egg-boilerplate-plugin [egg-mysql]: https://github.com/eggjs/egg-mysql diff --git a/site/docs/advanced/view-plugin.zh-CN.md b/site/docs/advanced/view-plugin.zh-CN.md index 2b4ff3e5c1..1317f2f316 100644 --- a/site/docs/advanced/view-plugin.zh-CN.md +++ b/site/docs/advanced/view-plugin.zh-CN.md @@ -3,12 +3,11 @@ title: View 插件开发 order: 5 --- -绝大多数情况,我们都需要读取数据后渲染模板,然后呈现给用户,而框架并不强制使用某种模板引擎,由开发者来自行选型,具体参见[模板渲染](../core/view.md)。 +绝大多数情况下,我们都需要读取数据后渲染模板,然后呈现给用户。框架并不强制使用某种模板引擎,由开发者自行选型,具体参见[模板渲染](../core/view.md)。 -本文将阐述框架对 View 插件的规范约束, 我们可以依此来封装对应的模板引擎插件。以下以 [egg-view-ejs] 为例。 +本文将阐述框架对 View 插件的规范约束。我们可以依此来封装对应的模板引擎插件。以下以 [egg-view-ejs](https://github.com/eggjs/egg-view-ejs) 为例。 ## 插件目录结构 - ```bash egg-view-ejs ├── config @@ -25,45 +24,43 @@ egg-view-ejs ## 插件命名规范 -- 遵循[插件开发规范](./plugin.md) -- 插件命名约定以 `egg-view-` 开头 -- `package.json` 配置如下,插件名以模板引擎命名,比如 ejs - - ```json - { - "name": "egg-view-ejs", - "eggPlugin": { - "name": "ejs" - }, - "keywords": ["egg", "egg-plugin", "egg-view", "ejs"] - } - ``` +- 遵循[插件开发规范](./plugin.md)。 +- 插件命名约定以 `egg-view-` 开头。 +- `package.json` 的配置如下,插件名以模板引擎命名,例如 ejs: -- 配置项也以模板引擎命名 +```json +{ + "name": "egg-view-ejs", + "eggPlugin": { + "name": "ejs" + }, + "keywords": ["egg", "egg-plugin", "egg-view", "ejs"] +} +``` - ```js - // config/config.default.js - module.exports = { - ejs: {}, - }; - ``` +- 配置项也以模板引擎命名: + +```js +// config/config.default.js +exports.ejs = {}; +``` ## View 基类 -接下来需提供一个 View 基类,这个类会在每次请求实例化。 +接下来需提供一个 View 基类,这个类会在每次请求时实例化。 -View 基类需提供 `render` 和 `renderString` 两个方法,支持 generator function 和 async function(也可以是函数返回一个 Promise)。`render` 方法用于渲染文件,而 `renderString` 方法用于渲染模板字符串。 +View 基类需要提供 `render` 和 `renderString` 两个方法,支持 generator function 和 async function(也可以是函数返回一个 Promise)。`render` 方法用于渲染文件,而 `renderString` 方法用于渲染模板字符串。 -以下为简化代码,可直接[查看源码](https://github.com/eggjs/egg-view-ejs/blob/master/lib/view.js) +以下为简化代码,您可以直接[查看源码](https://github.com/eggjs/egg-view-ejs/blob/master/lib/view.js): ```js const ejs = require('ejs'); -module.exports = class EjsView { +class EjsView { render(filename, locals) { return new Promise((resolve, reject) => { // 异步调用 API - ejs.renderFile(filename, locals, (err, result) => { + ejs.renderFile(filename, locals, function(err, result) { if (err) { reject(err); } else { @@ -81,60 +78,61 @@ module.exports = class EjsView { return Promise.reject(err); } } -}; +} + +module.exports = EjsView; ``` ### 参数 -`render` 方法的三个参数 - -- filename: 是完整的文件的路径,框架查找文件时已确认文件是否存在,这里不需要处理 -- locals: 渲染所需的数据,数据来自 `app.locals`,`ctx.locals` 和调用 `render` 方法传入的。框架还内置了 `ctx`,`request`, `ctx.helper` 这几个对象。 -- viewOptions: 用户传入的配置,可覆盖模板引擎的默认配置,这个可根据模板引擎的特征考虑是否支持。比如默认开启了缓存,而某个页面不需要缓存。 +- `render` 方法的参数: + - `filename`:是完整文件路径,框架查找文件时已确认文件是否存在,因此这里不需要处理。 + - `locals`:渲染所需数据,来源包括 `app.locals`、`ctx.locals` 以及调用 `render` 方法传入的数据。框架还内置了 `ctx`、`request` 和 `ctx.helper` 这几个对象。 + - `viewOptions`:用户传入的配置,可以覆盖模板引擎的默认配置。这个可根据模板引擎的特征考虑是否支持。例如,默认开启了缓存,而某个页面不需要缓存。 -`renderString` 方法的三个参数 +- `renderString` 方法的三个参数 -- tpl: 模板字符串,没有文件路径。 -- locals: 同 `render`。 -- viewOptions: 同 `render`。 + - `tpl`: 模板字符串,没有文件路径。 + - `locals`: 同 `render`。 + - `viewOptions`: 同 `render`。 ## 插件配置 -根据上面的命名约定,配置名一般为模板引擎的名字,比如 ejs。 +根据上述的命名约定,配置名通常为模板引擎的名称,例如 ejs。 -插件的配置主要来自模板引擎的配置,可根据具体情况定义配置项,如 [ejs 的配置](https://github.com/mde/ejs#options)。 +插件的配置主要来源于模板引擎的配置,可根据具体情况定义配置项目,如 [ejs 的配置](https://github.com/mde/ejs#options)。 ```js // config/config.default.js module.exports = { ejs: { - cache: true, - }, + cache: true + } }; ``` ### helper -框架本身提供了 `ctx.helper` 供开发者使用,但有些情况下,我们希望对 helper 方法进行覆盖,仅在模板渲染时生效。 +框架本身提供了 `ctx.helper` 供开发者使用。但在某些情况下,我们希望覆盖 helper 方法,使其仅在模板渲染时生效。 -在模板渲染中,我们经常会需要输出用户提供的 html 片段,通常需要使用 `egg-security` 插件提供的 `helper.shtml` 清洗下 +在模板渲染中,我们经常需要输出用户提供的 HTML 片段,这通常需要使用 `egg-security` 插件提供的 `helper.shtml` 方法进行清洗: ```html
{{ helper.shtml(data.content) | safe }}
``` -但如上代码所示,我们需要加上 ` | safe` 来告知模板引擎,该 html 是安全的,无需再次 `escape`,直接渲染。 +但如上所示,我们需要加上 `| safe` 来告知模板引擎,该 HTML 是安全的,无需再次 `escape`,可以直接渲染。 -而这样用起来比较麻烦,而且容易遗忘,所以我们可以封装下: +这样使用起来比较繁琐,而且容易忘记。所以,我们可以进行封装: -- 先提供一个 helper 子类: +- 首先提供一个 helper 子类: ```js // {plugin_root}/lib/helper.js module.exports = (app) => { return class ViewHelper extends app.Helper { - // safe 由 [egg-view-nunjucks] 注入,在渲染时不会转义, - // 否则在模板调用 shtml 会被转义 + // `safe` 是由 [egg-view-nunjucks] 注入的,在渲染时不会进行转义。 + // 否则在模板调用 `shtml` 时,内容会被转义。 shtml(str) { return this.safe(super.shtml(str)); } @@ -142,7 +140,7 @@ module.exports = (app) => { }; ``` -- 在渲染时使用自定义的 helper +- 在渲染时使用我们自定义的 helper: ```js // {plugin_root}/lib/view.js @@ -152,16 +150,16 @@ module.exports = class MyCustomView { render(filename, locals) { locals.helper = new ViewHelper(this.ctx); - // 调用 Nunjucks render + // 调用 Nunjucks 的 render 方法 } }; ``` -具体代码可[查看](https://github.com/eggjs/egg-view-nunjucks/blob/2ee5ee992cfd95bc0bb5b822fbd72a6778edb118/lib/view.js#L11) +具体代码可以在[这里](https://github.com/eggjs/egg-view-nunjucks/blob/2ee5ee992cfd95bc0bb5b822fbd72a6778edb118/lib/view.js#L11)查看。 ### 安全相关 -模板和安全息息相关,[egg-security] 也给模板提供了一些方法,模板引擎可以根据需求使用。 +模板与安全密不可分。[egg-security] 也为模板提供了一些方法。模板引擎可以根据需求使用这些方法。 首先声明对 [egg-security] 的依赖: @@ -175,11 +173,11 @@ module.exports = class MyCustomView { } ``` -此外,框架提供了 [app.injectCsrf](../core/security.md#appinjectcsrfstr) 和 [app.injectNonce](../core/security.md#appinjectnoncestr),更多可查看[安全章节](../core/security.md)。 +除此之外,框架还提供了 [app.injectCsrf](../core/security.md#appinjectcsrfstr) 与 [app.injectNonce](../core/security.md#appinjectnoncestr) 方法。更多内容可查看[安全章节](../core/security.md)。 ### 单元测试 -作为一个高质量的插件,完善的单元测试是必不可少的,我们也提供了很多辅助工具使插件开发者可以无痛的编写测试,具体参见[单元测试](../core/unittest.md)和[插件](./plugin.md)中的相关内容。 +为了确保插件的高质量,完善的单元测试是不可或缺的。我们也提供了很多辅助工具,以帮助插件开发者毫无障碍地编写测试。具体内容请参见[单元测试](../core/unittest.md)与[插件](./plugin.md)相关章节。 [egg-security]: https://github.com/eggjs/egg-security [egg-view-nunjucks]: https://github.com/eggjs/egg-view-nunjucks diff --git a/site/docs/community/CONTRIBUTING.zh-CN.md b/site/docs/community/CONTRIBUTING.zh-CN.md index 5e66e18754..e3b7a83500 100644 --- a/site/docs/community/CONTRIBUTING.zh-CN.md +++ b/site/docs/community/CONTRIBUTING.zh-CN.md @@ -1,52 +1,51 @@ --- + title: 代码贡献规范 + --- -有任何疑问,欢迎提交 [issue](https://github.com/eggjs/egg/issues), -或者直接修改提交 [PR](https://github.com/eggjs/egg/pulls)! +有任何疑问,欢迎提交 [issue](https://github.com/eggjs/egg/issues),或者直接修改提交 [PR](https://github.com/eggjs/egg/pulls)! ## 提交 issue - 请确定 issue 的类型。 - 请避免提交重复的 issue,在提交之前搜索现有的 issue。 -- 在标签(分类参考**标签分类**), 标题 或者内容中体现明确的意图。 +- 在标签(分类参考 **标签分类**)、标题或者内容中体现明确的意图。 -随后 egg 负责人会确认 issue 意图,更新合适的标签,关联 milestone,指派开发者。 +随后,Egg 负责人会确认 issue 意图,更新合适的标签,关联 milestone,指派开发者。 -标签可分为两类,type 和 scope +标签可分为两类:type 和 scope。 -- type: issue 的类型,如 `feature`, `bug`, `documentation`, `performance`, `support` ... -- scope: 修改文件的范围,如 `core: xx`,`plugin: xx`,`deps: xx` +- type:issue 的类型,如 `feature`、`bug`、`documentation`、`performance`、`support` 等。 +- scope:修改文件的范围,如 `core: xx`、`plugin: xx`、`deps: xx` 等。 ### 常用标签说明 -- `support`: issue 提出的问题需要开发者协作排查,咨询,调试等等日常技术支持。 -- `bug`: 一旦发现可能是 bug 的问题,请打上 `bug`,然后等待确认,一旦确认是 bug,此 issue 会被再打上 `confirmed`。 - - 此时 issue 会被非常高的优先级进行处理。 - - 如果此 bug 是正在影响线上应用正常运行,会再打上 `critical`,代表是最高优先级,需要马上立刻处理! - - bug 会在最低需要修复的版本进行修复,如是在 `0.9.x` 要修复的,而当前最新版本是 `1.1.x`, - 那么此 issue 还会被打上 `0.9`,`0.10`,`1.0`,`1.1`,代表需要修复到这些版本。 -- `core: xx`: 代表 issue 跟 core 内核相关,如 `core: antx` 代表跟 `antx` 配置相关。 -- `plugin: xx`: 代表 issue 跟插件相关,如 `deps: session` 代表跟 `session` 插件相关。 -- `deps: xx`: 代表 issue 跟 `dependencies` 模块相关,如 `deps: egg-cors` 代表跟 `egg-cors` 模块相关。 -- `chore: documentation`: 代表发现了文档相关问题,需要修复文档说明。 -- `cbd`: 代表跟服务器部署相关 +- `support`:issue 提出的问题需要开发者协作排查、咨询、调试等日常技术支持。 +- `bug`:一旦发现可能是 bug 的问题,请打上 `bug`,然后等待确认。一旦确认是 bug,此 issue 会被再打上 `confirmed`。 + - 此时,issue 会被非常高的优先级进行处理。 + - 如果此 bug 正在影响线上应用正常运行,会再打上 `critical`,代表是最高优先级,需要马上处理! + - bug 会在最低需要修复的版本进行修复,如在 `0.9.x` 版本需要修复,而当前最新版本是 `1.1.x`,那么此 issue 还会被打上 `0.9`、`0.10`、`1.0`、`1.1` 标签,代表需要修复到这些版本。 +- `core: xx`:代表 issue 跟 core 内核相关,如 `core: antx` 代表跟 `antx` 配置相关。 +- `plugin: xx`:代表 issue 跟插件相关,如 `deps: session` 代表跟 `session` 插件相关。 +- `deps: xx`:代表 issue 跟 `dependencies` 模块相关,如 `deps: egg-cors` 代表跟 `egg-cors` 模块相关。 +- `chore: documentation`:代表发现了文档相关问题,需要修复文档说明。 +- `cbd`:代表跟服务器部署相关。 ## 编写文档 -所有功能点必须提交配套文档,文档须满足以下要求 +所有功能点必须提交配套文档。文档须满足以下要求: -- 必须说清楚问题的几个方面:what(是什么),why(为什么),how(怎么做),可根据问题的特性有所侧重。 -- how 部分必须包含详尽完整的操作步骤,必要时附上 **足够简单,可运行** 的范例代码, - 所有范例代码放在 [eggjs/examples](https://github.com/eggjs/examples) 库中。 -- 提供必要的链接,如申请流程,术语解释和参考文档等。 +- 必须说清楚问题的几个方面:what(是什么)、why(为什么)、how(怎么做),可根据问题的特性有所侧重。 +- how 部分必须包含详尽完整的操作步骤,必要时附上 **足够简单,可运行** 的范例代码。所有范例代码放在 [eggjs/examples](https://github.com/eggjs/examples) 库中。 +- 提供必要的链接,如申请流程、术语解释和参考文档等。 - 同步修改中英文文档,或者在 PR 里面说明。 ## 提交代码 ### 提交 Pull Request -如果你有仓库的开发者权限,而且希望贡献代码,那么你可以创建分支修改代码提交 PR,egg 开发团队会 review 代码合并到主干。 +如果你有仓库的开发者权限,而且希望贡献代码,那么你可以创建分支修改代码提交 PR。Egg 开发团队会 review 代码合并到主干。 ```bash # 先创建开发分支开发,分支名应该有含义,避免使用 update、tmp 之类的 @@ -54,16 +53,16 @@ $ git checkout -b branch-name # 开发完成后跑下测试是否通过,必要时需要新增或修改测试用例 $ npm test - +``` # 测试通过后,提交代码,message 见下面的规范 -$ git add . # git add -u 删除文件 -$ git commit -m "fix(role): role.use must xxx" +``` +$ git add . +$ git commit -m "fix(role): role.use 必须 xxx" $ git push origin branch-name ``` 由于谁也无法保证过了多久之后还记得多少,为了后期回溯历史的方便,请在提交 MR 时确保提供了以下信息。 - 1. 需求点(一般关联 issue 或者注释都算) 2. 升级原因(不同于 issue,可以简要描述下为什么要处理) 3. 框架测试点(可以关联到测试文件,不用详细描述,关键点即可) @@ -71,11 +70,11 @@ $ git push origin branch-name ### 代码风格 -你的代码风格必须通过 eslint,你可以运行 `$ npm run lint` 本地测试。 +你的代码风格必须通过 eslint,你可以运行 `$ npm run lint` 进行本地测试。 ### Commit 提交规范 -根据 [angular 规范](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit-message-format)提交 commit, +根据 [Angular 规范](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit-message-format)提交 commit, 这样 history 看起来更加清晰,还可以自动生成 changelog。 ```xml @@ -88,25 +87,25 @@ $ git push origin branch-name (1)type -提交 commit 的类型,包括以下几种 +提交 commit 的类型,包括以下几种: -- feat: 新功能 -- fix: 修复问题 -- docs: 修改文档 -- style: 修改代码格式,不影响代码逻辑 -- refactor: 重构代码,理论上不影响现有功能 -- perf: 提升性能 -- test: 增加修改测试用例 -- chore: 修改工具相关(包括但不限于文档、代码生成等) -- deps: 升级依赖 +- feat:新功能 +- fix:修复问题 +- docs:修改文档 +- style:修改代码格式,不影响代码逻辑 +- refactor:重构代码,理论上不影响现有功能 +- perf:提升性能 +- test:增加修改测试用例 +- chore:修改工具相关(包括但不限于文档、代码生成等) +- deps:升级依赖 (2)scope -修改文件的范围(包括但不限于 doc, middleware, core, config, plugin) +修改文件的范围(包括但不限于 doc,middleware,core,config,plugin) (3)subject -用一句话清楚的描述这次提交做了什么 +用一句话清楚的描述这次提交做了什么。 (4)body @@ -114,18 +113,18 @@ $ git push origin branch-name (5)footer -- **当有非兼容修改(Breaking Change)时必须在这里描述清楚** +- **当有非兼容修改(Breaking Change)时必须在这里描述清楚** - 关联相关 issue,如 `Closes #1, Closes #2, #3` -- 如果功能点有新增或修改的,还需要关联文档 `doc` 和 `egg-core` 的 PR,如 `eggjs/egg-core#123` +- 如果功能点有新增或修改的,还需要关联文档 `doc` 和 `egg-core` 的 PR,如 `eggjs/egg-core#123` 示例 ``` -fix($compile): [BREAKING_CHANGE] couple of unit tests for IE9 +fix($compile): couple of unit tests for IE9 -Older IEs serialize html uppercased, but IE9 does not... -Would be better to expect case insensitive, unfortunately jasmine does -not allow to user regexps for throw expectations. +Older IEs serialize HTML uppercased, but IE9 does not... +Would be better to expect case insensitive, unfortunately Jasmine does +not allow to use regexps for throw expectations. Document change on eggjs/egg#123 @@ -133,7 +132,7 @@ Closes #392 BREAKING CHANGE: - Breaks foo.bar api, foo.baz should be used instead + Breaks foo.bar API, use foo.baz instead. ``` 详情请查看具体[文档](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit)。 @@ -142,42 +141,41 @@ BREAKING CHANGE: 英语正文按照一般英语语法规律书写即可,但标题比较特殊,应该按照以下规范进行书写: -- 名词、动词、代词、形容词、副词等首字母大写,介词、冠词、连词、感叹词和助词首字母小写,_标题第一个单词、最后一个单词无论词性首字母应该大写_。 +- 名词、动词、代词、形容词、副词等首字母大写,介词、冠词、连词、感叹词和助词首字母小写;标题第一个单词、最后一个单词无论词性首字母应该大写。 - 专有名词(如直接引用某个变量,或者某个插件名称等),必须使用反单引号(键盘上 Esc 正下方)进行引用,并保持原来大小写。 - 超过 5 个字母的介词首字母应该大写,否则一律小写。 -- 如果是重要提示性标题,或者是专有名称标题(例如 Http 请求方法:GET,POST,PUT,DELETE),可以全部字母都用大写(慎重考虑)。 +- 如果是重要提示性标题,或者是专有名称标题(例如 HTTP 请求方法:GET,POST,PUT,DELETE),可以全部字母都用大写(慎重考虑)。 - 如果标题属于“动宾”性质的短语(如“配置管理”),尽量翻译成“宾+动词名词”的形式(Configuration Management),或者是“动名词+宾语”的形式(Managing Configuration)。 -- 如果标题被当做一个完整的英语句子,请按照英语句子的语法格式大小写(如:常见问题 FAQ 中每一个标题都是一个英语句子)。 +- 如果标题被当做一个完整的英语句子,请按照英语句子的语法格式大小写(例如:常见问题 FAQ 中每一个标题都是一个英语句子)。 有关详情,可以参考[英语标题大小写]。 - ## 发布管理 -egg 基于 [semver] 语义化版本号进行发布。 +Egg 基于 [semver](语义化版本号)进行发布。 ### 分支策略 `master` 分支为当前稳定发布的版本,`next` 分支为下一个开发中的大版本。 -- 只维护两个版本,除非有安全问题,否则修复只会 patch 到 `master` 和 `next` 分支,其他更新推动上层框架升级到稳定大版本的最新版本。 -- 所有 API 的废弃都需要在当前的稳定版本上 `deprecate` 提示,并保证在当前的稳定版本上一直兼容到新版本的发布。 -- `master` 分支不设置 publish tag,上层框架基于 semver 依赖稳定版本。 -- `next` 分支设置 tag 为 `next`,上层框架可以通过 `egg@next` 引用开发中的版本进行测试。 -- egg 持续维护的版本以 Milestone 为准,只要是开着的版本都会进行修复。 +- 只维护两个版本。除非有安全问题,否则修复只会合并到 `master` 和 `next` 分支。我们鼓励上层框架升级到稳定大版本的最新版本。 +- 所有 API 的废弃都需要在当前的稳定版本上添加 `deprecate` 提示,并保证兼容到新版本发布。 +- `master` 分支不设置 publish tag。上层框架基于 semver 依赖稳定版本。 +- `next` 分支设置 tag 为 `next`。上层框架可以通过 `egg@next` 引用开发中的版本进行测试。 +- Egg 持续维护的版本以 Milestone 为准。只要是开着的版本,都会进行修复。 -### 发布策略 +### 发布策略 -每个大版本都有一个发布经理管理(PM),他/她要做的事情 +每个大版本都有一个发布经理(PM)负责管理。他/她的工作内容包括: #### 准备工作: -- 建立 milestone,确认需求关联 milestone,指派和更新 issues,如 [1.x milestone]。 +- 建立 Milestone,确认需求关联到 Milestone,分配和更新 Issues。如 [1.x Milestone]。 - 从 `master` 分支新建 `next` 分支,并设置 tag 为 `next`。 #### 发布前: -- 确认当前 Milestone 所有的 issue 都已关闭或可延期,完成性能测试。 -- 发起一个新的 [Release Proposal MR],按照 [node CHANGELOG] 进行 `History` 的编写,修正文档中与版本相关的内容,commits 可以自动生成。 +- 确认当前 Milestone 所有的 Issue 都已关闭,或可以延期。并完成性能测试。 +- 发起一个新的 [Release Proposal MR],按照 [Node CHANGELOG] 进行 `History` 的编写。修正文档中与版本相关的内容,commits 可通过以下命令自动生成: ```bash $ npm run commits ``` @@ -185,10 +183,10 @@ egg 基于 [semver] 语义化版本号进行发布。 #### 发布时: -- 将老的稳定版本(master)备份到以当前大版本为名字的分支上(例如 `1.x`),并设置 tag 为 `release-{v}.x`( v 为当前版本,例如 `release-1.x`)。 -- 将 `next` 分支推送到 `master`,成为新的稳定版本分支,并去除 `next` tag,修改 README 中与分支相关的内容。 +- 将老的稳定版本(`master`)备份到以当前大版本为名的分支上(例如 `1.x`),并设置 tag 为 `release-{v}.x`(v 为当前版本,例如 `release-1.x`)。 +- 将 `next` 分支推送到 `master`,成为新的稳定版本分支。去除 `next` tag,并修改 README 中与分支相关的内容。 - 发布新的稳定版本到 [npm],并通知上层框架进行更新。 -- `npm publish` 之前,请先阅读 [我是如何发布一个 npm 包的]。 +- 在执行 `npm publish` 之前,请先阅读 [我是如何发布一个 npm 包的]。 上述描述中所有的设置 tag 都是指在 `package.json` 中设置 npm 的 tag。 @@ -198,10 +196,10 @@ egg 基于 [semver] 语义化版本号进行发布。 } ``` -[semver]: https://semver.org/lang/zh-CN/ -[release proposal mr]: https://github.com/nodejs/node/pull/4181 -[node changelog]: https://github.com/nodejs/node/blob/master/CHANGELOG.md -[1.x milestone]: https://github.com/eggjs/egg/milestone/1 -[npm]: http://npmjs.com/ -[我是如何发布一个 npm 包的]: https://fengmk2.com/blog/2016/how-i-publish-a-npm-package -[英语标题大小写]: https://headlinecapitalization.com/ +[semver]: https://semver.org/lang/zh-CN/ +[Release Proposal MR]: https://github.com/nodejs/node/pull/4181 +[Node CHANGELOG]: https://github.com/nodejs/node/blob/master/CHANGELOG.md +[1.x Milestone]: https://github.com/eggjs/egg/milestone/1 +[npm]: http://npmjs.com/ +[我是如何发布一个 npm 包的]: https://fengmk2.com/blog/2016/how-i-publish-a-npm-package +[英语标题大小写]: https://headlinecapitalization.com/ \ No newline at end of file diff --git a/site/docs/community/faq.zh-CN.md b/site/docs/community/faq.zh-CN.md index b837c1be78..199122f1af 100644 --- a/site/docs/community/faq.zh-CN.md +++ b/site/docs/community/faq.zh-CN.md @@ -7,23 +7,23 @@ order: 3 ## 如何高效地反馈问题? -感谢您向我们反馈问题。 +感谢你向我们反馈问题。 -1. 我们推荐如果是小问题(错别字修改,小的 bug fix)直接提交 PR。 +1. 我们推荐如果是小问题(错别字修改、小的 bug 修复)直接提交 PR。 2. 如果是一个新需求,请提供:详细需求描述,最好是有伪代码示意。 -3. 如果是一个 BUG,请提供:复现步骤,错误日志以及相关配置,并尽量填写下面的模板中的条目。 +3. 如果是一个 BUG,请提供:复现步骤、错误日志以及相关配置,并尽量填写下面的模板中的条目。 4. **如果可以,尽可能使用 `npm init egg --type=simple bug` 提供一个最小可复现的代码仓库,方便我们排查问题。** -5. 不要挤牙膏似的交流,扩展阅读:[如何向开源项目提交无法解答的问题](https://zhuanlan.zhihu.com/p/25795393) +5. 不要挤牙膏似的交流,扩展阅读:[如何向开源项目提交无法解答的问题](https://zhuanlan.zhihu.com/p/25795393)。 -最重要的是,请明白一件事:开源项目的用户和维护者之间并不是甲方和乙方的关系,issue 也不是客服工单。在开 issue 的时候,请抱着一种『一起合作来解决这个问题』的心态,不要期待我们单方面地为你服务。 +最重要的是,请明白一件事:开源项目的用户和维护者之间并不是甲方和乙方的关系;issue 也不是客服工单。在开 issue 的时候,请抱着一种『我们一起合作来解决这个问题』的心态,不要期待我们单方面地为你服务。 ## 为什么我的配置不生效? -框架的配置功能比较强大,有不同环境变量,又有框架、插件、应用等很多地方配置。 +框架的配置功能比较强大,有不同环境变量,还有框架、插件、应用等多个配置。 -如果你分析问题时,想知道当前运行时使用的最终配置,可以查看下 `${root}/run/application_config.json`(worker 进程配置) 和 `${root}/run/agent_config.json`(agent 进程配置) 这两个文件。(`root` 为应用根目录,只有在 local 和 unittest 环境下为项目所在目录,其他环境下都为 HOME 目录) +如果你在分析问题时想知道当前运行时使用的最终配置,可以查看下 `${root}/run/application_config.json`(worker 进程配置)和 `${root}/run/agent_config.json`(agent 进程配置)这两个文件。(`root` 为应用根目录,只有在 local 和 unittest 环境下为项目所在目录,其他环境下为 HOME 目录) -也可参见[配置文件](../basics/config.md#配置结果)。 +也可以参见[配置文件](../basics/config.md#配置结果)。 PS:请确保没有写出以下代码: @@ -39,19 +39,19 @@ module.exports = (appInfo) => { ## 线上的日志打印去哪里了? -默认配置下,本地开发环境的日志都会打印在应用根目录的 `logs` 文件夹下(`${baseDir}/logs`) ,但是在非开发期的环境(非 local 和 unittest 环境),所有的日志都会打印到 `$HOME/logs` 文件夹下(例如 `/home/admin/logs`)。这样可以让本地开发时应用日志互不影响,服务器运行时又有统一的日志输出目录。 +默认配置下,本地开发环境的日志都会打印在应用根目录的 `logs` 文件夹下 (`${baseDir}/logs`),但在非开发期的环境(非 local 和 unittest 环境)中,所有日志都会打印到 `$HOME/logs` 文件夹下(例如 `/home/admin/logs`)。这样可以让本地开发时应用日志互不影响,服务器运行时又有统一的日志输出目录。 -## 进程管理为什么没有选型 PM2 ? +## 进程管理为什么没有选型 PM2? 1. PM2 模块本身复杂度很高,出了问题很难排查。我们认为框架使用的工具复杂度不应该过高,而 PM2 自身的复杂度超越了大部分应用本身。 2. 没法做非常深的优化。 -3. 切实的需求问题,一个进程里跑 leader,其他进程代理到 leader 这种模式([多进程模型](../core/cluster-and-ipc.md)),在企业级开发中对于减少远端连接,降低数据通信压力等都是切实的需求。特别当应用规模大到一定程度,这就会是刚需。egg 本身起源于蚂蚁金服和阿里,我们对标的起点就是大规模企业应用的构建,所以要非常全面。这些特性通过 PM2 很难做到。 +3. 切实需要解决如:多个进程中有一个充当 leader,其他进程则通过代理的方式将请求转发给 leader 这种模式([多进程模型](../core/cluster-and-ipc.md)),这在企业级开发中可以减少远端连接,降低数据通信压力。特别是当应用规模大到一定程度时,这就会是必需。egg 起源于蚂蚁金服和阿里,我们的起点就是大规模企业应用的构建,所以需要非常全面。这些特性通过 PM2 很难实现。 -进程模型非常重要,会影响到开发模式,运行期间的深度优化等,我们认为可能由框架来控制比较合适。 +进程模型非常重要,会影响开发模式以及运行期间的深度优化等,我们认为可能由框架来控制比较合适。 **如何使用 PM2 启动应用?** -尽管我们不推荐使用 PM2 启动,但仍然是可以做到的。 +尽管我们不推荐使用 PM2 启动,你仍然可以这样做。 首先,在项目根目录定义启动文件: @@ -66,7 +66,7 @@ egg.startCluster({ }); ``` -这样,我们就可以通过 PM2 进行启动了: +接着,就可以通过 PM2 启动: ```bash pm2 start server.js @@ -79,16 +79,16 @@ pm2 start server.js - `missing csrf token` - `invalid csrf token` -Egg 内置的 [egg-security](https://github.com/eggjs/egg-security/) 插件默认对所有『非安全』的方法,例如 `POST`,`PUT`,`DELETE` 都进行 CSRF 校验。 +Egg 内置的 [egg-security](https://github.com/eggjs/egg-security/) 插件默认对所有“非安全”的方法,例如 `POST`、`PUT`、`DELETE`,都进行 CSRF 校验。 -请求遇到 csrf 报错通常是因为没有加正确的 csrf token 导致,具体实现方式,请阅读[安全威胁 CSRF 的防范](../core/security.md#安全威胁csrf的防范)。 +遇到 csrf 报错通常是因为没有加正确的 csrf token 导致的,具体实现方式,请阅读[安全威胁 CSRF 的防范](../core/security.md#安全威胁csrf的防范)。 ## 本地开发时,修改代码后为什么 worker 进程没有自动重启? -没有自动重启的情况一般是在使用 Jetbrains 旗下软件(IntelliJ IDEA, WebStorm..),并且开启了 Safe Write 选项。 +worker 进程没有自动重启的情形通常发生在使用 Jetbrains 旗下软件(例如 IntelliJ IDEA、WebStorm 等),并且开启了 Safe Write 选项时。 -Jetbrains [Safe Write 文档](https://www.jetbrains.com/help/webstorm/2016.3/system-settings.html)中有提到(翻译如下): +Jetbrains [Safe Write 文档](https://www.jetbrains.com/help/webstorm/2016.3/system-settings.html) 中有提到(翻译如下): -> 如果此复选框打钩,变更的文件将首先被存储在一个临时文件中。如果成功保存了该文件,那么就会被这个文件所取代(从技术上来说,原文件被删除,临时文件被重命名)。 +>“如果此复选框打钩,变更的文件将首先被存储在一个临时文件中。如果文件保存成功,则临时文件会替换原文件(从技术上讲,原文件被删除,临时文件被重命名)。” -由于使用了重命名导致文件监听的失效。解决办法是关掉 Safe Write 选项。(Settings | Appearance & Behavior | System Settings | Use "safe write" 路径可能根据版本有所不同) +由于使用了重命名,文件监听失效。解决方法是关闭 Safe Write 选项。(Settings | Appearance & Behavior | System Settings | Use "safe write",路径可能因版本不同有所差异) diff --git a/site/docs/community/index.zh-CN.md b/site/docs/community/index.zh-CN.md index 9b21c2eb09..025c0b6677 100644 --- a/site/docs/community/index.zh-CN.md +++ b/site/docs/community/index.zh-CN.md @@ -21,7 +21,7 @@ nav: - 其他 - [awesome-egg](https://github.com/eggjs/awesome-egg) - 文章 - - [如何评价阿里开源的企业级 Node.js 框架 Egg?](https://www.zhihu.com/question/50526101/answer/144952130) by [@天猪](https://github.com/atian25) + - [如何评价阿里开源的企业级 Node.js 框架 Egg?](https://www.zhihu.com/question/50526101/answer/144952130) 由 [@天猪](https://github.com/atian25) 提供 - 你也可以到[知乎专栏](https://zhuanlan.zhihu.com/eggjs)看我们的文章 ## 项目赞助 diff --git a/site/docs/community/style-guide.zh-CN.md b/site/docs/community/style-guide.zh-CN.md index 52d3d93bd7..ba168aa0c9 100644 --- a/site/docs/community/style-guide.zh-CN.md +++ b/site/docs/community/style-guide.zh-CN.md @@ -31,12 +31,12 @@ class UserService extends Service { module.exports = UserService; ``` -同时,`框架开发者`需要改变写法如下,否则`应用开发者`自定义 Service 等基类会有问题: +同时,框架开发者需要改变写法如下,否则应用开发者自定义 Service 等基类会有问题: ```js const egg = require('egg'); -module.export = Object.assign(egg, { +module.exports = Object.assign(egg, { Application: class MyApplication extends egg.Application { // ... }, @@ -47,8 +47,8 @@ module.export = Object.assign(egg, { ## 私有属性与慢初始化 - 私有属性用 `Symbol` 来挂载。 -- Symbol 的描述遵循 jsdoc 的规则,描述映射后的类名+属性名。 -- 延迟初始化。 +- `Symbol` 的描述遵循 jsdoc 的规则, 描述映射后的类名 + 属性名。 +- 实现延迟初始化。 ```js // app/extend/application.js diff --git a/site/docs/core/cluster-and-ipc.zh-CN.md b/site/docs/core/cluster-and-ipc.zh-CN.md index 2e74f1a238..602aa23a19 100644 --- a/site/docs/core/cluster-and-ipc.zh-CN.md +++ b/site/docs/core/cluster-and-ipc.zh-CN.md @@ -3,29 +3,29 @@ title: 多进程模型和进程间通讯 order: 7 --- -我们知道 JavaScript 代码是运行在单线程上的,换句话说一个 Node.js 进程只能运行在一个 CPU 上。那么如果用 Node.js 来做 Web Server,就无法享受到多核运算的好处。作为企业级的解决方案,我们要解决的一个问题就是: +我们知道 JavaScript 代码是运行在单线程上的,换句话说,一个 Node.js 进程只能运行在一个 CPU 上。那么如果用 Node.js 来做 Web Server,就无法享受到多核运算的好处。作为企业级的解决方案,我们要解决的一个问题就是: > 如何榨干服务器资源,利用上多核 CPU 的并发优势? -而 Node.js 官方提供的解决方案是 [Cluster 模块](https://nodejs.org/api/cluster.html),其中包含一段简介: +Node.js 官方提供的解决方案是 [Cluster 模块](https://nodejs.org/api/cluster.html)。其中包含一段简介: > 单个 Node.js 实例在单线程环境下运行。为了更好地利用多核环境,用户有时希望启动一批 Node.js 进程用于加载。 > > 集群化模块使得你很方便地创建子进程,以便于在服务端口之间共享。 -## Cluster 是什么呢? +## Cluster 是什么? -简单的说, +简单地说,Cluster 是: - 在服务器上同时启动多个进程。 -- 每个进程里都跑的是同一份源代码(好比把以前一个进程的工作分给多个进程去做)。 -- 更神奇的是,这些进程可以同时监听一个端口(具体原理推荐阅读 @DavidCai1993 这篇 [Cluster 实现原理](https://cnodejs.org/topic/56e84480833b7c8a0492e20c))。 +- 每个进程里都运行着同一份源代码(好比把以前一个进程的工作分给多个进程去做)。 +- 更神奇的是,这些进程可以同时监听一个端口(具体原理推荐阅读 @DavidCai1993 这篇关于 [Cluster 实现原理](https://cnodejs.org/topic/56e84480833b7c8a0492e20c) 的文章)。 其中: -- 负责启动其他进程的叫做 Master 进程,他好比是个『包工头』,不做具体的工作,只负责启动其他进程。 -- 其他被启动的叫 Worker 进程,顾名思义就是干活的『工人』。它们接收请求,对外提供服务。 -- Worker 进程的数量一般根据服务器的 CPU 核数来定,这样就可以完美利用多核资源。 +- 负责启动其他进程的叫做 Master 进程,好比是一个“包工头”,不做具体的工作,只负责启动其他进程。 +- 其他被启动的进程叫 Worker 进程,顾名思义就是干活的“工人”。它们接收请求,对外提供服务。 +- Worker 进程的数量一般根据服务器的 CPU 核数来定,这样可以完美利用多核资源。 ```js const cluster = require('cluster'); @@ -38,43 +38,42 @@ if (cluster.isMaster) { cluster.fork(); } - cluster.on('exit', function (worker, code, signal) { - console.log('worker ' + worker.process.pid + ' died'); + cluster.on('exit', (worker, code, signal) => { + console.log(`worker ${worker.process.pid} died`); }); } else { - // Workers can share any TCP connection - // In this case it is an HTTP server + // Workers can share any TCP connection. + // In this case it is an HTTP server. http - .createServer(function (req, res) { + .createServer((req, res) => { res.writeHead(200); res.end('hello world\n'); }) .listen(8000); } ``` - ## 框架的多进程模型 -上面的示例是不是很简单,但是作为企业级的解决方案,要考虑的东西还有很多。 +上面的示例是否很简单呢?但作为企业级应用解决方案,我们需要考虑的问题还有很多。 -- Worker 进程异常退出以后该如何处理? -- 多个 Worker 进程之间如何共享资源? -- 多个 Worker 进程之间如何调度? -- ... +- Worker 进程异常退出后,我们应如何处理? +- 多个 Worker 进程之间,如何共享资源? +- 多个 Worker 进程之间,又该如何调度? +- ...(此处省略号不需要修改为中文省略号,因为它在用列表符号表示内容,并不是句子的结束) ### 进程守护 -健壮性(又叫鲁棒性)是企业级应用必须考虑的问题,除了程序本身代码质量要保证,框架层面也需要提供相应的『兜底』机制保证极端情况下应用的可用性。 +健壮性(又称鲁棒性)是企业级应用必须要考虑的问题。除了程序代码质量要保证外,框架层面也需要提供相应的“兜底”机制,以保证极端情况下应用的可用性。 一般来说,Node.js 进程退出可以分为两类: #### 未捕获异常 -当代码抛出了异常没有被捕获到时,进程将会退出,此时 Node.js 提供了 `process.on('uncaughtException', handler)` 接口来捕获它,但是当一个 Worker 进程遇到 [未捕获的异常](https://nodejs.org/dist/latest-v6.x/docs/api/process.html#process_event_uncaughtexception) 时,它已经处于一个不确定状态,此时我们应该让这个进程优雅退出: +当代码抛出未被捕获的异常时,进程将会退出。这时 Node.js 提供了 `process.on('uncaughtException', handler)` 接口来捕获它。但是,当一个 Worker 进程遇到[未捕获的异常](https://nodejs.org/dist/latest-v6.x/docs/api/process.html#process_event_uncaughtexception)时,它已经处于不确定状态,此时我们应该让这个进程优雅退出: -1. 关闭异常 Worker 进程所有的 TCP Server(将已有的连接快速断开,且不再接收新的连接),断开和 Master 的 IPC 通道,不再接受新的用户请求。 -2. Master 立刻 fork 一个新的 Worker 进程,保证在线的『工人』总数不变。 -3. 异常 Worker 等待一段时间,处理完已经接受的请求后退出。 +1. 关闭异常 Worker 进程的所有 TCP Server(将已有连接快速断开,且不再接收新的连接),断开与 Master 的 IPC 通道,不再接受新的用户请求。 +2. Master 立即 fork 一个新的 Worker 进程,以确保在线的“工人”总数保持不变。 +3. 异常 Worker 等待一段时间,在处理完已接收的请求后退出。 ```bash +---------+ +---------+ @@ -98,18 +97,18 @@ if (cluster.isMaster) { #### OOM、系统异常 -而当一个进程出现异常导致 crash 或者 OOM 被系统杀死时,不像未捕获异常发生时我们还有机会让进程继续执行,只能够让当前进程直接退出,Master 立刻 fork 一个新的 Worker。 +当进程由于异常导致 crash 或者因 OOM 被系统杀死时,不同于未捕获异常发生时我们还有让进程继续执行的机会,只能让当前进程直接退出。Master 立即 fork 一个新的 Worker。 -在框架里,我们采用 [graceful] 和 [egg-cluster] 两个模块配合实现上面的逻辑。这套方案已在阿里巴巴和蚂蚁金服的生产环境广泛部署,且经受过『双 11』大促的考验,所以是相对稳定和靠谱的。 +在框架里,我们采用 [graceful] 和 [egg-cluster] 两个模块配合,实现上述逻辑。这套方案已在阿里巴巴和蚂蚁金服的生产环境中广泛部署,并经历过“双 11”大促的考验。因此,它相对稳定可靠。 ### Agent 机制 -说到这里,Node.js 多进程方案貌似已经成型,这也是我们早期线上使用的方案。但后来我们发现有些工作其实不需要每个 Worker 都去做,如果都做,一来是浪费资源,更重要的是可能会导致多进程间资源访问冲突。举个例子:生产环境的日志文件我们一般会按照日期进行归档,在单进程模型下这再简单不过了: +Node.js 的多进程方案现已成型,这也是我们早期线上使用的方案。但后来我们发现,有些工作不需要每个 Worker 都去做。如果都去做,既是资源浪费,更重要的是,可能会导致多进程间资源访问冲突。举个例子,在生产环境中我们通常按日期归档日志文件,在单进程模型下这非常简单: -> 1. 每天凌晨 0 点,将当前日志文件按照日期进行重命名 -> 2. 销毁以前的文件句柄,并创建新的日志文件继续写入 +> 1. 每天凌晨 0 点,将当前日志文件按日期重命名。 +> 2. 销毁之前的文件句柄,并创建新的日志文件继续写入。 -试想如果现在是 4 个进程来做同样的事情,是不是就乱套了。所以,对于这一类后台运行的逻辑,我们希望将它们放到一个单独的进程上去执行,这个进程就叫 Agent Worker,简称 Agent。Agent 好比是 Master 给其他 Worker 请的一个『秘书』,它不对外提供服务,只给 App Worker 打工,专门处理一些公共事务。现在我们的多进程模型就变成下面这个样子了 +试想,如果现在有 4 个进程同时做同样事情,就会混乱。因此,对于这类后台任务,我们希望它们在一个单独的进程中执行,该进程就叫 Agent Worker,简称 Agent。Agent 类似于 Master 为其他 Worker 雇佣的“秘书”,不对外提供服务,只服务于 App Worker,专门处理公共事务。现在我们的多进程模型就成了下面这个样子: ```bash +--------+ +-------+ @@ -125,7 +124,7 @@ if (cluster.isMaster) { +----------+ +----------+ +----------+ ``` -那我们框架的启动时序如下: +框架的启动时序如下: ```bash +---------+ +---------+ +---------+ @@ -145,30 +144,30 @@ if (cluster.isMaster) { +----------------------------------------->| ``` -1. Master 启动后先 fork Agent 进程 -2. Agent 初始化成功后,通过 IPC 通道通知 Master -3. Master 再 fork 多个 App Worker -4. App Worker 初始化成功,通知 Master -5. 所有的进程初始化成功后,Master 通知 Agent 和 Worker 应用启动成功 +1. Master 启动后先 fork Agent 进程。 +2. Agent 初始化成功后,通过 IPC 通道通知 Master。 +3. Master 接着 fork 多个 App Worker。 +4. App Worker 初始化成功后,通知 Master。 +5. 所有进程初始化成功后,Master 通知 Agent 和 Worker,应用启动成功。 -另外,关于 Agent Worker 还有几点需要注意的是: +关于 Agent Worker,还有几点需要注意: -1. 由于 App Worker 依赖于 Agent,所以必须等 Agent 初始化完成后才能 fork App Worker -2. Agent 虽然是 App Worker 的『小秘』,但是业务相关的工作不应该放到 Agent 上去做,不然把她累垮了就不好了 -3. 由于 Agent 的特殊定位,**我们应该保证它相对稳定**。当它发生未捕获异常,框架不会像 App Worker 一样让他退出重启,而是记录异常日志、报警等待人工处理 -4. Agent 和普通 App Worker 挂载的 API 不完全一样,如何识别差异可查看[框架文档](../advanced/framework.md) +1. 因为 App Worker 依赖于 Agent,所以必须要等 Agent 初始化完成后才能 fork App Worker。 +2. Agent 是 App Worker 的“小秘”,但不应该安排业务相关的工作,以免过于繁忙。 +3. 由于 Agent 的特殊定位,我们应该确保它相对稳定。遇到未捕获异常时,框架不会像 App Worker 那样重启,而是记录异常日志、报警等待人工处理。 +4. Agent 和普通 App Worker 提供的 API 不完全相同。想知道具体差异,请查看[框架文档](../advanced/framework.md)。 ### Agent 的用法 -你可以在应用或插件根目录下的 `agent.js` 中实现你自己的逻辑(和[启动自定义](../basics/app-start.md) 用法类似,只是入口参数是 agent 对象) +你可以在应用或插件的根目录下的 `agent.js` 文件中实现你自己的逻辑(使用方法类似于[启动自定义](../basics/app-start.md),只是入口参数为 agent 对象)。 ```js // agent.js module.exports = agent => { - // 在这里写你的初始化逻辑 + // 在这里写你的初始化逻辑。 - // 也可以通过 messenger 对象发送消息给 App Worker - // 但需要等待 App Worker 启动成功后才能发送,不然很可能丢失 + // 你还可以通过 messenger 对象发送消息给 App Worker。 + // 但是,需要等 App Worker 启动成功后才能发送,否则可能丢失消息。 agent.messenger.on('egg-ready', () => { const data = { ... }; agent.messenger.sendToApp('xxx_action', data); @@ -178,30 +177,29 @@ module.exports = agent => { ```js // app.js -module.exports = (app) => { - app.messenger.on('xxx_action', (data) => { +module.exports = app => { + app.messenger.on('xxx_action', data => { // ... }); }; ``` -这个例子中,`agent.js` 的代码会执行在 agent 进程上,`app.js` 的代码会执行在 Worker 进程上,他们通过框架封装的 `messenger` 对象进行进程间通讯(IPC),后面的章节会对框架的 IPC 做详细的讲解。 - +这个例子中,`agent.js` 的代码将在 Agent 进程上执行,`app.js` 的代码则在 Worker 进程上执行。它们通过框架封装的 `messenger` 对象进行进程间通信(IPC)。后续章节会对框架的 IPC 进行详细讲解。 ### Master VS Agent VS Worker -当一个应用启动时,会同时启动这三类进程。 +应用启动时,会同时创建三类进程。下表概述了每种进程的数量、作用、稳定性以及是否运行业务代码: -| 类型 | 进程数量 | 作用 | 稳定性 | 是否运行业务代码 | -| ------ | ------------------- | ---------------------------- | ------ | ---------------- | -| Master | 1 | 进程管理,进程间消息转发 | 非常高 | 否 | +| 类型 | 进程数量 | 作用 | 稳定性 | 是否运行业务代码 | +| ------ | ------------------ | -------------------------- | ------ | ---------------- | +| Master | 1 | 进程管理,进程间消息转发 | 非常高 | 否 | | Agent | 1 | 后台运行工作(长连接客户端) | 高 | 少量 | -| Worker | 一般设置为 CPU 核数 | 执行业务代码 | 一般 | 是 | +| Worker | 通常设置为 CPU 核数 | 执行业务代码 | 一般 | 是 | #### Master -在这个模型下,Master 进程承担了进程管理的工作(类似 [pm2]),不运行任何业务代码,我们只需要运行起一个 Master 进程它就会帮我们搞定所有的 Worker、Agent 进程的初始化以及重启等工作了。 +在此模型中,Master 进程负责进程管理,类似 [pm2],它不运行业务代码。我们仅需启动一个 Master 进程,它就会自动管理 Worker、Agent 进程的初始化及重启。 -Master 进程的稳定性是极高的,线上运行时我们只需要通过 [egg-scripts] 后台运行通过 `egg.startCluster` 启动的 Master 进程就可以了,不再需要使用 [pm2] 等进程守护模块。 +Master 进程非常稳定,在线上环境中,我们通过 [egg-scripts] 后台运行 Master 进程即可,不需额外使用 [pm2] 等进程守护模块。 ```bash $ egg-scripts start --daemon @@ -209,19 +207,19 @@ $ egg-scripts start --daemon #### Agent -在大部分情况下,我们在写业务代码的时候完全不用考虑 Agent 进程的存在,但是当我们遇到一些场景,只想让代码运行在一个进程上的时候,Agent 进程就到了发挥作用的时候了。 +在绝大多数情况下,编写业务代码时可以忽略 Agent 进程。但若需要代码仅运行在单个进程上,Agent 进程便显得尤为重要。 -由于 Agent 只有一个,而且会负责许多维持连接的脏活累活,因此它不能轻易挂掉和重启,所以 Agent 进程在监听到未捕获异常时不会退出,但是会打印出错误日志,**我们需要对日志中的未捕获异常提高警惕**。 +Agent 进程因只有一个实例,且承担多种任务,因此不能轻易重启。遇到未捕获的异常时,Agent 进程会记录错误日志,**我们应对日志中的未捕获异常保持警惕**。 #### Worker -Worker 进程负责处理真正的用户请求和[定时任务](../basics/schedule.md)的处理。而 Egg 的定时任务也提供了只让一个 Worker 进程运行的能力,**所以能够通过定时任务解决的问题就不要放到 Agent 上执行**。 +Worker 进程处理用户请求和[定时任务](../basics/schedule.md)。Egg 的定时任务可控制仅由一个 Worker 进程执行,**因此可被解决的问题不应放在 Agent 上**。 -Worker 运行的是业务代码,相对会比 Agent 和 Master 进程上运行的代码复杂度更高,稳定性也低一点,**当 Worker 进程异常退出时,Master 进程会重启一个 Worker 进程。** +Worker 进程因运行复杂的业务代码,稳定性相对较低。一旦 Worker 异常退出,Master 将重新启动一个新进程。 ## 进程间通讯(IPC) -虽然每个 Worker 进程是相对独立的,但是它们之间始终还是需要通讯的,叫进程间通讯(IPC)。下面是 Node.js 官方提供的一段示例代码 +尽管 Worker 进程相对独立,它们间仍需通讯。以下是 Node.js 官方的 IPC 示例代码: ```js 'use strict'; @@ -240,13 +238,13 @@ if (cluster.isMaster) { } ``` -细心的你可能已经发现 cluster 的 IPC 通道只存在于 Master 和 Worker/Agent 之间,Worker 与 Agent 进程互相间是没有的。那么 Worker 之间想通讯该怎么办呢?是的,通过 Master 来转发。 +注意到 cluster 的 IPC 仅在 Master 和 Worker/Agent 之间有效,Worker 与 Agent 间无法直接通讯。Worker 之间的通讯需要经由 Master 转发。 ```bash -广播消息: agent => all workers - +--------+ +-------+ - | Master |<---------| Agent | - +--------+ +-------+ +广播消息:Agent => 所有 Worker + +--------+ +-------+ + | Master |<-----------| Agent | + +--------+ +-------+ / | \ / | \ / | \ @@ -256,40 +254,39 @@ if (cluster.isMaster) { | Worker 1 | | Worker 2 | | Worker 3 | +----------+ +----------+ +----------+ -指定接收方: one worker => another worker - +--------+ +-------+ - | Master |----------| Agent | - +--------+ +-------+ +指定接收方:一个 Worker => 另一个 Worker + +--------+ +-------+ + | Master |------------| Agent | + +--------+ +-------+ ^ | - send to / | - worker 2 / | - / | - / v + | | 发送至 Worker 2 + | | + | v +----------+ +----------+ +----------+ | Worker 1 | | Worker 2 | | Worker 3 | +----------+ +----------+ +----------+ ``` -为了方便调用,我们封装了一个 messenger 对象挂在 app / agent 实例上,提供一系列友好的 API。 +为简化调用,我们在 app 和 agent 实例上封装了 messenger 对象,并提供了友好的 API: ### 发送 -- `app.messenger.broadcast(action, data)`:发送给所有的 agent / app 进程(包括自己) -- `app.messenger.sendToApp(action, data)`: 发送给所有的 app 进程 - - 在 app 上调用该方法会发送给自己和其他的 app 进程 - - 在 agent 上调用该方法会发送给所有的 app 进程 -- `app.messenger.sendToAgent(action, data)`: 发送给 agent 进程 - - 在 app 上调用该方法会发送给 agent 进程 - - 在 agent 上调用该方法会发送给 agent 自己 -- `agent.messenger.sendRandom(action, data)`: - - app 上没有该方法(现在 Egg 的实现是等同于 sentToAgent) - - agent 会随机发送消息给一个 app 进程(由 master 来控制发送给谁) -- `app.messenger.sendTo(pid, action, data)`: 发送给指定进程 +- `app.messenger.broadcast(action, data)`: 向所有的 agent / app 进程发送消息(包括自己)。 +- `app.messenger.sendToApp(action, data)`: 发送至所有的 app 进程。 + - app 上调用即发送至自己与其他 app + - agent 上调用则发送至所有 app 进程。 +- `app.messenger.sendToAgent(action, data)`: 发送消息至 agent 进程。 + - app 上调用即发送至 agent + - agent 上调用即发送至自己。 +- `agent.messenger.sendRandom(action, data)`: + - app 上无此方法(Egg 实现与 sentToAgent 类似) + - agent 随机向某 app 进程发送消息(由 master 控制)。 +- `app.messenger.sendTo(pid, action, data)`: 向指定进程发送消息。 ```js // app.js module.exports = (app) => { - // 注意,只有在 egg-ready 事件拿到之后才能发送消息 + // 只有在 egg-ready 事件后才能发送消息 app.messenger.once('egg-ready', () => { app.messenger.sendToAgent('agent-event', { foo: 'bar' }); app.messenger.sendToApp('app-event', { foo: 'bar' }); @@ -297,44 +294,47 @@ module.exports = (app) => { }; ``` -_上面所有 `app.messenger` 上的方法都可以在 `agent.messenger` 上使用。_ +所有 `app.messenger` 方法也可在 `agent.messenger` 上调用。 #### egg-ready -上面的示例中提到,需要等 `egg-ready` 消息之后才能发送消息。只有在 Master 确认所有的 Agent 进程和 Worker 进程都已经成功启动(并 ready)之后,才会通过 messenger 发送 `egg-ready` 消息给所有的 Agent 和 Worker,告知一切准备就绪,IPC 通道可以开始使用了。 +如上所述,在接到 `egg-ready` 消息后方可发送消息。只有当 Master 确认所有 Agent 和 Worker 启动成功并就绪后,才会通过 messenger 发送 `egg-ready` 通知每个 Agent 和 Worker,表示一切就绪,可使用 IPC。 ### 接收 -在 messenger 上监听对应的 action 事件,就可以收到其他进程发送来的信息了。 +监听 messenger 上的相应 action 事件可收到其他进程发送的消息。 ```js app.messenger.on(action, (data) => { - // process data + // 处理数据 }); app.messenger.once(action, (data) => { - // process data + // 处理数据 }); ``` -_agent 上的 messenger 接收消息的用法和 app 上一致。_ +_agent 上收消息的方式与 app 相同。_ +现在我将开始根据《优秀技术文档的写作标准》修改全文。 + +--- ## IPC 实战 -我们通过一个简单的例子来感受一下在框架的多进程模型下如何使用 IPC 解决实际问题。 +我们通过一个简单的例子,来感受一下在框架的多进程模型下,如何使用 IPC 解决实际问题。 ### 需求 -我们有一个接口需要从远程数据源中读取一些数据,对外部提供 API,但是这个数据源的数据很少变化,因此我们希望将数据缓存到内存中以提升服务能力,降低 RT。此时就需要有一个更新内存缓存的机制。 +我们有一个接口,需要从远程数据源中读取数据,并对外提供 API。但这个数据源的数据很少变化,因此我们希望将数据缓存到内存中以提升服务能力,降低 RT。此时就需要一个更新内存缓存的机制。 -1. 定时从远程数据源获取数据,更新内存缓存,为了降低对数据源压力,更新的间隔时间会设置的比较长。 -2. 远程数据源提供一个检查是否有数据更新的接口,我们的服务可以更频繁的调用检查接口,当有数据更新时才去重新拉取数据。 -3. 远程数据源通过消息中间件推送数据更新的消息,我们的服务监听消息来更新数据。 +1. 定时从远程数据源获取数据,更新内存缓存。为了降低对数据源的压力,我们会把更新间隔时间设置得较长。 +2. 远程数据源提供一个检查是否有数据更新的接口。我们的服务可以更频繁地调用此接口。当有数据更新时,才去重新拉取数据。 +3. 远程数据源通过消息中间件推送数据更新的消息。我们的服务监听此消息来更新数据。 -在实际项目中,我们可以采用方案一用于兜底,结合方案三或者方案二的一种用于提升数据更新的实时性。而在这个示例中,我们会通过 IPC + [定时任务](../basics/schedule.md)来同时实现这三种缓存更新方案。 +在实际项目中,我们可以采用方案一作为基础。结合方案三或方案二中的一种,以提升数据更新的实时性。而在这个示例中,我们会使用 IPC + [定时任务](../basics/schedule.md),同时实现这三种更新方案。 ### 实现 -我们将所有的与远程数据源交互的逻辑封装在一个 Service 中,并提供 `get` 方法给 Controller 调用。 +我们把所有与远程数据源交互的逻辑封装在一个 Service 中。并提供 `get` 方法给 Controller 调用。 ```js // app/service/source.js @@ -360,13 +360,13 @@ class SourceService extends Service { } ``` -编写定时任务,实现方案一,每 10 分钟定时从远程数据源获取数据更新缓存做兜底。 +编写定时任务,实现方案一。每 10 分钟定时从远程数据源获取数据并更新缓存做兜底。 ```js // app/schedule/force_refresh.js exports.schedule = { interval: '10m', - type: 'all', // run in all workers + type: 'all' // 在所有的 workers 中运行 }; exports.task = async (ctx) => { @@ -375,13 +375,13 @@ exports.task = async (ctx) => { }; ``` -再编写一个定时任务来实现方案二的检查逻辑,每 10s 让一个 worker 调用检查接口,当发现数据有变化时,通过 messenger 提供的方法通知所有的 Worker。 +再编写一个定时任务,来实现方案二的检查逻辑。让一个 worker 每 10 秒调用一次检查接口。发现数据有变化时,通过 messenger 通知所有的 Worker。 ```js // app/schedule/pull_refresh.js exports.schedule = { interval: '10s', - type: 'worker', // only run in one worker + type: 'worker' // 只在一个 worker 中运行 }; exports.task = async (ctx) => { @@ -393,14 +393,14 @@ exports.task = async (ctx) => { }; ``` -在启动自定义文件中监听 `refresh` 事件,并更新数据,所有的 Worker 进程都能收到这个消息,并触发更新,此时我们的方案二也已经大功告成了。 +在自定义启动文件中,监听 `refresh` 事件并更新数据。所有的 Worker 进程都能收到这个消息,并触发更新。此时,我们的方案二也已经完成。 ```js // app.js module.exports = (app) => { app.messenger.on('refresh', (by) => { app.logger.info('start update by %s', by); - // create an anonymous context to access service + // 创建一个匿名 context 来访问 service const ctx = app.createAnonymousContext(); ctx.runInBackground(async () => { await ctx.service.source.update(); @@ -410,7 +410,7 @@ module.exports = (app) => { }; ``` -现在我们来看看如何实现第三个方案。我们需要有一个消息中间件的客户端,它会和服务端保持长连接,这一类的长连接维持比较适合在 Agent 进程上做,可以有效降低连接数,减少两端的消耗。所以我们在 Agent 进程上来开启消息监听。 +现在我们来看看如何实现第三个方案。我们需要一个消息中间件的客户端。它会和服务端维持长连接,适合在 Agent 进程上运行。这可以有效降低连接数,减少两端的消耗。所以我们在 Agent 进程上开启消息监听。 ```js // agent.js @@ -419,18 +419,13 @@ const Subscriber = require('./lib/subscriber'); module.exports = (agent) => { const subscriber = new Subscriber(); - // listen changed event, broadcast to all workers + // 监听变更事件,广播到所有 workers subscriber.on('changed', () => agent.messenger.sendToApp('refresh', 'push')); }; ``` -通过合理使用 Agent 进程、定时任务和 IPC,我们可以轻松搞定类似的需求并降低对数据源的压力。具体的示例代码可以查看 [examples/ipc](https://github.com/eggjs/examples/tree/master/ipc)。 +通过合理使用 Agent 进程、定时任务和 IPC,我们可以轻松搞定类似的需求。同时也可降低对数据源的压力。具体的示例代码可以查看 [examples/ipc](https://github.com/eggjs/examples/tree/master/ipc)。 ## 更复杂的场景 -上面的例子中,我们在 Agent 进程上运行了一个 subscriber,来监听消息中间件的消息,如果 Worker 进程也需要监听一些消息怎么办?如何通过 Agent 进程建立连接再转发给 Worker 进程呢?这些问题可以在[多进程研发模式增强](../advanced/cluster-client.md)中找到答案。 - -[pm2]: https://github.com/Unitech/pm2 -[egg-cluster]: https://github.com/eggjs/egg-cluster -[egg-scripts]: https://github.com/eggjs/egg-scripts -[graceful]: https://github.com/node-modules/graceful +在上面的例子中,我们在 Agent 进程上运行了一个 subscriber,来监听消息中间件的消息。如果 Worker 进程也需要监听一些消息怎么办?如何通过 Agent 进程建立连接,再转发给 Worker 进程呢?这些问题可以在 [多进程模型增强](../advanced/cluster-client.md) 文档中找到答案。 diff --git a/site/docs/core/cookie-and-session.zh-CN.md b/site/docs/core/cookie-and-session.zh-CN.md index 1ecc0b43dd..6ed1278a47 100644 --- a/site/docs/core/cookie-and-session.zh-CN.md +++ b/site/docs/core/cookie-and-session.zh-CN.md @@ -7,7 +7,7 @@ order: 6 HTTP 请求都是无状态的,但是我们的 Web 应用通常都需要知道发起请求的人是谁。为了解决这个问题,HTTP 协议设计了一个特殊的请求头:[Cookie](https://en.wikipedia.org/wiki/HTTP_cookie)。服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性)。 -通过 `ctx.cookies`,我们可以在 controller 中便捷、安全的设置和读取 Cookie。 +通过 `ctx.cookies`,我们可以在 controller 中便捷、安全地设置和读取 Cookie。 ```js class HomeController extends Controller { @@ -30,29 +30,29 @@ class HomeController extends Controller { 设置 Cookie 其实是通过在 HTTP 响应中设置 set-cookie 头完成的,每一个 set-cookie 都会让浏览器在 Cookie 中存一个键值对。在设置 Cookie 值的同时,协议还支持许多参数来配置这个 Cookie 的传输、存储和权限。 -- `{Number} maxAge`: 设置这个键值对在浏览器的最长保存时间。是一个从服务器当前时刻开始的毫秒数。 -- `{Date} expires`: 设置这个键值对的失效时间,如果设置了 maxAge,expires 将会被覆盖。如果 maxAge 和 expires 都没设置,Cookie 将会在浏览器的会话失效(一般是关闭浏览器时)的时候失效。 -- `{String} path`: 设置键值对生效的 URL 路径,默认设置在根路径上(`/`),也就是当前域名下的所有 URL 都可以访问这个 Cookie。 -- `{String} domain`: 设置键值对生效的域名,默认没有配置,可以配置成只在指定域名才能访问。 -- `{Boolean} httpOnly`: 设置键值对是否可以被 js 访问,默认为 true,不允许被 js 访问。 -- `{Boolean} secure`: 设置键值对[只在 HTTPS 连接上传输](http://stackoverflow.com/questions/13729749/how-does-cookie-secure-flag-work),框架会帮我们判断当前是否在 HTTPS 连接上自动设置 secure 的值。 +- `{Number} maxAge`:设置这个键值对在浏览器的最长保存时间。是一个从服务器当前时刻开始的毫秒数。 +- `{Date} expires`:设置这个键值对的失效时间。如果设置了 maxAge,expires 将会被覆盖。如果 maxAge 和 expires 都没设置,Cookie 将会在浏览器的会话失效(一般是关闭浏览器时)的时候失效。 +- `{String} path`:设置键值对生效的 URL 路径,默认设置在根路径上(`/`),也就是当前域名下的所有 URL 都可以访问这个 Cookie。 +- `{String} domain`:设置键值对生效的域名,默认没有配置,可以配置成只在指定域名才能访问。 +- `{Boolean} httpOnly`:设置键值对是否可以被 js 访问,默认为 true,不允许被 js 访问。 +- `{Boolean} secure`:设置键值对只在 HTTPS 连接上传输,框架会帮我们判断当前是否在 HTTPS 连接上自动设置 secure 的值。 -除了这些属性之外,框架另外扩展了 3 个参数的支持: +除了这些属性之外,框架另外扩展了三个参数的支持: -- `{Boolean} overwrite`:设置 key 相同的键值对如何处理,如果设置为 true,则后设置的值会覆盖前面设置的,否则将会发送两个 set-cookie 响应头。 -- `{Boolean} signed`:设置是否对 Cookie 进行签名,如果设置为 true,则设置键值对的时候会同时对这个键值对的值进行签名,后面取的时候做校验,可以防止前端对这个值进行篡改。默认为 true。 -- `{Boolean} encrypt`:设置是否对 Cookie 进行加密,如果设置为 true,则在发送 Cookie 前会对这个键值对的值进行加密,客户端无法读取到 Cookie 的明文值。默认为 false。 +- `{Boolean} overwrite`:设置 key 相同的键值对如何处理。如果设置为 true,则后设置的值会覆盖前面设置的;否则将会发送两个 set-cookie 响应头。 +- `{Boolean} signed`:设置是否对 Cookie 进行签名。如果设置为 true,则设置键值对的时候会同时对这个键值对的值进行签名,后面取的时候做校验,可以防止前端对这个值进行篡改。默认为 true。 +- `{Boolean} encrypt`:设置是否对 Cookie 进行加密。如果设置为 true,则在发送 Cookie 前会对这个键值对的值进行加密,客户端无法读取到 Cookie 的明文值。默认为 false。 在设置 Cookie 时我们需要思考清楚这个 Cookie 的作用,它需要被浏览器保存多久?是否可以被 js 获取到?是否可以被前端修改? -**默认的配置下,Cookie 是加签不加密的,浏览器可以看到明文,js 不能访问,不能被客户端(手工)篡改。** +默认的配置下,Cookie 是加签不加密的,浏览器可以看到明文,js 不能访问,不能被客户端(手工)篡改。 -- 如果想要 Cookie 在浏览器端可以被 js 访问并修改: +- 如果想要 Cookie 在浏览器端可以被 js 访问并修改: ```js ctx.cookies.set(key, value, { httpOnly: false, - signed: false, + signed: false }); ``` @@ -61,37 +61,37 @@ ctx.cookies.set(key, value, { ```js ctx.cookies.set(key, value, { httpOnly: true, // 默认就是 true - encrypt: true, // 加密传输 + encrypt: true // 加密传输 }); ``` 注意: 1. 由于[浏览器和其他客户端实现的不确定性](http://stackoverflow.com/questions/7567154/can-i-use-unicode-characters-in-http-headers),为了保证 Cookie 可以写入成功,建议 value 通过 base64 编码或者其他形式 encode 之后再写入。 -2. 由于[浏览器对 Cookie 有长度限制限制](http://stackoverflow.com/questions/640938/what-is-the-maximum-size-of-a-web-browsers-cookies-key),所以尽量不要设置太长的 Cookie。一般来说不要超过 4093 bytes。当设置的 Cookie value 大于这个值时,框架会打印一条警告日志。 +2. 由于[浏览器对 Cookie 有长度限制](http://stackoverflow.com/questions/640938/what-is-the-maximum-size-of-a-web-browsers-cookies-key),所以尽量不要设置太长的 Cookie。一般来说不要超过 4093 bytes。当设置的 Cookie value 大于这个值时,框架会打印一条警告日志。 #### `ctx.cookies.get(key, options)` -由于 HTTP 请求中的 Cookie 是在一个 header 中传输过来的,通过框架提供的这个方法可以快速的从整段 Cookie 中获取对应的键值对的值。上面在设置 Cookie 的时候,我们可以设置 `options.signed` 和 `options.encrypt` 来对 Cookie 进行签名或加密,因此对应的在获取 Cookie 的时候也要传相匹配的选项。 +由于 HTTP 请求中的 Cookie 是在一个 header 中传输过来的,通过框架提供的这个方法可以快速地从整段 Cookie 中获取对应的键值对的值。上面在设置 Cookie 的时候,我们可以设置 `options.signed` 和 `options.encrypt` 来对 Cookie 进行签名或加密,因此对应的在获取 Cookie 的时候也要传递相匹配的选项。 -- 如果设置的时候指定为 signed,获取时未指定,则不会在获取时对取到的值做验签,导致可能被客户端篡改。 +- 如果设置的时候指定为 signed,获取时未指定,则不会在获取时对取到的值做验签,可能导致被客户端篡改。 - 如果设置的时候指定为 encrypt,获取时未指定,则无法获取到真实的值,而是加密过后的密文。 -如果要获取前端或者其他系统设置的 cookie,需要指定参数 `signed` 为 `false`,避免对它做验签导致获取不到 cookie 的值。 +如果要获取前端或者其他系统设置的 cookie,需要指定参数 `signed` 为 `false`,避免验签导致获取不到 cookie 的值。 ```js ctx.cookies.get('frontend-cookie', { - signed: false, + signed: false }); ``` ### Cookie 秘钥 -由于我们在 Cookie 中需要用到加解密和验签,所以需要配置一个秘钥供加密使用。在 `config/config.default.js` 中 +由于我们在 Cookie 中需要用到加解密和验签,所以需要配置一个秘钥供加密使用。在 `config/config.default.js` 中: ```js module.exports = { - keys: 'key1,key2', + keys: 'key1,key2' }; ``` @@ -100,13 +100,12 @@ keys 配置成一个字符串,可以按照逗号分隔配置多个 key。Cooki - 加密和加签时只会使用第一个秘钥。 - 解密和验签时会遍历 keys 进行解密。 -如果我们想要更新 Cookie 的秘钥,但是又不希望之前设置到用户浏览器上的 Cookie 失效,可以将新的秘钥配置到 keys 最前面,等过一段时间之后再删去不需要的秘钥即可。 - +如果我们想要更新 Cookie 的秘钥,但是又不希望之前设置到用户浏览器上的 Cookie 失效,可以将新的秘钥配置到 keys 最前面,等过一段时间之后再删除不需要的秘钥即可。 ## Session -Cookie 在 Web 应用中经常承担标识请求方身份的功能,所以 Web 应用在 Cookie 的基础上封装了 Session 的概念,专门用做用户身份识别。 +Cookie 通常用作 Web 应用中标识请求方身份的功能,基于此,Web 应用封装了 Session 概念,专用于用户身份识别。 -框架内置了 [Session](https://github.com/eggjs/egg-session) 插件,给我们提供了 `ctx.session` 来访问或者修改当前用户 Session 。 +框架内置了 [Session](https://github.com/eggjs/egg-session) 插件,提供了 `ctx.session` 用于访问或修改当前用户的 Session。 ```js class HomeController extends Controller { @@ -125,27 +124,27 @@ class HomeController extends Controller { } ``` -Session 的使用方法非常直观,直接读取它或者修改它就可以了,如果要删除它,直接将它赋值为 null: +Session 的使用方法非常直观,直接读取或修改即可。若需删除 Session,将其赋值为 null: ```js ctx.session = null; ``` -需要 **特别注意** 的是:设置 session 属性时需要避免以下几种情况(会造成字段丢失,详见 [koa-session](https://github.com/koajs/session/blob/master/lib/session.js#L37-L47) 源码) +需要 **特别注意** 的是,设置 session 属性时要避免以下情况,否则可能导致字段丢失(详见 [koa-session](https://github.com/koajs/session/blob/master/lib/session.js#L37-L47) 源码): -- 不要以 `_` 开头 -- 不能为 `isNew` +- 不以 `_` 开头 +- 不使用 `isNew` ```js // ❌ 错误的用法 -ctx.session._visited = 1; // --> 该字段会在下一次请求时丢失 -ctx.session.isNew = 'HeHe'; // --> 为内部关键字, 不应该去更改 +ctx.session._visited = 1; // 该字段会在下一次请求时丢失 +ctx.session.isNew = 'HeHe'; // 为内部关键字,不应更改 // ✔️ 正确的用法 -ctx.session.visited = 1; // --> 此处没有问题 +ctx.session.visited = 1; // 无问题 ``` -Session 的实现是基于 Cookie 的,默认配置下,用户 Session 的内容加密后直接存储在 Cookie 中的一个字段中,用户每次请求我们网站的时候都会带上这个 Cookie,我们在服务端解密后使用。Session 的默认配置如下: +Session 默认基于 Cookie 实现,内容加密后直接存储在 Cookie 的字段中。每次请求带上这个 Cookie,服务端解密后使用。默认配置如下: ```js exports.session = { @@ -156,36 +155,32 @@ exports.session = { }; ``` -可以看到这些参数除了 `key` 都是 Cookie 的参数,`key` 代表了存储 Session 的 Cookie 键值对的 key 是什么。在默认的配置下,存放 Session 的 Cookie 将会加密存储、不可被前端 js 访问,这样可以保证用户的 Session 是安全的。 +上述参数除 `key` 外均为 Cookie 的参数,`key` 代表存储 Session 的 Cookie 键值对的 key。默认配置下,存放 Session 的 Cookie 将加密存储、前端 js 不可访问,保障用户 Session 安全。 ### 扩展存储 -Session 默认存放在 Cookie 中,但是如果我们的 Session 对象过于庞大,就会带来一些额外的问题: - -- 前面提到,浏览器通常都有限制最大的 Cookie 长度,当设置的 Session 过大时,浏览器可能拒绝保存。 -- Cookie 在每次请求时都会带上,当 Session 过大时,每次请求都要额外带上庞大的 Cookie 信息。 +Session 默认存放在 Cookie 中可能出现问题:浏览器有最大 Cookie 长度限制,过大的 Session 可能被拒绝保存,且每次请求Session 会增加额外传输负担。 -框架提供了将 Session 存储到除了 Cookie 之外的其他存储的扩展方案,我们只需要设置 `app.sessionStore` 即可将 Session 存储到指定的存储中。 +框架允许将 Session 存储到 Cookie 之外的存储,只需设置 `app.sessionStore` 即可: ```js // app.js -module.exports = (app) => { +module.exports = app => { app.sessionStore = { - // support promise / async async get(key) { - // return value; + // 返回值 }, async set(key, value, maxAge) { - // set key to store + // 设置 key 到存储 }, async destroy(key) { - // destroy key + // 销毁 key }, }; }; ``` -sessionStore 的实现我们也可以封装到插件中,例如 [egg-session-redis] 就提供了将 Session 存储到 redis 中的能力,在应用层,我们只需要引入 [egg-redis] 和 [egg-session-redis] 插件即可。 +例如,通过引入 [egg-redis](https://github.com/eggjs/egg-redis) 和 [egg-session-redis](https://github.com/eggjs/egg-session-redis) 插件,可以将 Session 存储到 redis 中。 ```js // plugin.js @@ -199,13 +194,13 @@ exports.sessionRedis = { }; ``` -**注意:一旦选择了将 Session 存入到外部存储中,就意味着系统将强依赖于这个外部存储,当它挂了的时候,我们就完全无法使用 Session 相关的功能了。因此我们更推荐大家只将必要的信息存储在 Session 中,保持 Session 的精简并使用默认的 Cookie 存储,用户级别的缓存不要存储在 Session 中。** +**注意**:将 Session 存入外部存储意味着系统强依赖该存储。建议仅将必要信息存于 Session,并保持其精简使用默认的 Cookie 存储,不将用户级别的缓存存于 Session。 ### Session 实践 #### 修改用户 Session 失效时间 -虽然在 Session 的配置中有一项是 maxAge,但是它只能全局设置 Session 的有效期,我们经常可以在一些网站的登录页上看到有 **记住我** 的选项框,勾选之后可以让登录用户的 Session 有效期更长。这种针对特定用户的 Session 有效时间设置我们可以通过 `ctx.session.maxAge=` 来实现。 +Session 配置中的 `maxAge` 可全局设置有效期。**记住我** 功能中,可针对特定用户的 Session 设置不同有效时间,通过 `ctx.session.maxAge=` 实现: ```js const ms = require('ms'); @@ -217,7 +212,7 @@ class UserController extends Controller { // 设置 Session ctx.session.user = user; - // 如果用户勾选了 `记住我`,设置 30 天的过期时间 + // 勾选 `记住我` 时,设置 30 天过期时间 if (rememberMe) ctx.session.maxAge = ms('30d'); } } @@ -225,14 +220,12 @@ class UserController extends Controller { #### 延长用户 Session 有效期 -默认情况下,当用户请求没有导致 Session 被修改时,框架都不会延长 Session 的有效期,但是在有些场景下,我们希望用户如果长时间都在访问我们的站点,则延长他们的 Session 有效期,不让用户退出登录态。框架提供了一个 `renew` 配置项用于实现此功能,它会在发现当用户 Session 的有效期仅剩下最大有效期一半的时候,重置 Session 的有效期。 +默认情况下,未修改 Session 的请求不延长有效期。某些场景下希望用户长期访问站点时延长 Session 有效期,`renew` 配置项可实现此功能,如 Session 剩余有效期少于半时,重置有效期: ```js // config/config.default.js -module.exports = { - session: { - renew: true, - }, +exports.session = { + renew: true, }; ``` diff --git a/site/docs/core/deployment.zh-CN.md b/site/docs/core/deployment.zh-CN.md index f38e854368..a5c7534acb 100644 --- a/site/docs/core/deployment.zh-CN.md +++ b/site/docs/core/deployment.zh-CN.md @@ -3,15 +3,15 @@ title: 应用部署 order: 3 --- -在[本地开发](./development.md)时,我们使用 `egg-bin dev` 来启动服务,但是在部署应用的时候不可以这样使用。因为 `egg-bin dev` 会针对本地开发做很多处理,而生产运行需要一个更加简单稳定的方式。所以本章主要讲解如何部署你的应用。 +在[本地开发](./development.md)时,我们使用 `egg-bin dev` 来启动服务,但在部署应用时不可以这样使用。因为 `egg-bin dev` 会针对本地开发做很多处理,而生产环境需要一个更加简单稳定的方式。本章主要讲解如何部署你的应用。 -一般从源码代码到真正运行,我们会拆分成构建和部署两步,可以做到**一次构建多次部署**。 +一般从源码到运行,会分为构建和部署两步,实现**一次构建、多次部署**。 ## 构建 -JavaScript 语言本身不需要编译的,构建过程主要是下载依赖。但如果使用 TypeScript 或者 Babel 支持 ES6 以上的特性,那就必须要这一步了。 +JavaScript 语言本身不需要编译,构建过程主要是下载依赖。如果使用 TypeScript 或 Babel 支持 ES6 及以上特性,则必须构建。 -一般安装依赖会指定 `NODE_ENV=production` 或 `npm install --production` 只安装 dependencies 的依赖。因为 devDependencies 中的模块过大而且在生产环境不会使用,安装后也可能遇到未知问题。 +一般安装依赖时,会指定 `NODE_ENV=production` 或 `npm install --production` 仅安装核心依赖。因为开发依赖包体积大,在生产环境不必要,且可能导致问题。 ```bash $ cd baseDir @@ -19,28 +19,28 @@ $ npm install --production $ tar -zcvf ../release.tgz . ``` -构建完成后打包成 tgz 文件,部署的时候解压启动就可以了。 +构建后,将其打包为 tgz 文件。部署时解压启动即可。 -增加构建环节才能做到真正的**一次构建多次部署**,理论上代码没有改动的时候是不需要再次构建的,可以用原来的包进行部署,这有着不少好处: +增加构建环节,能实现真正的**一次构建、多次部署**。理论上,代码未变更时,无需重构,可用原包部署,带来诸多好处: -- 构建依赖的环境和运行时是有差异的,所以不要污染运行时环境。 -- 可以减少发布的时间,而且易回滚,只需要把原来的包重新启动即可。 +- 构建环境与运行环境差异,避免污染运行环境。 +- 缩短发布时间,便于回滚,只需重启原包即可。 ## 部署 -服务器需要预装 Node.js,框架支持的 Node 版本为 `>= 14.20.0`。 +服务器需要预装 Node.js,框架支持 Node 版本 `>= 14.20.0`。 -框架内置了 [egg-cluster] 来启动 [Master 进程](./cluster-and-ipc.md#master),Master 有足够的稳定性,不再需要使用 [pm2] 等进程守护模块。 +框架内置 [egg-cluster] 启动 [Master 进程](./cluster-and-ipc.md#master),Master 稳定,不需 [pm2] 等进程守护模块。 -同时,框架也提供了 [egg-scripts] 来支持线上环境的运行和停止。 +框架同时提供 [egg-scripts] 支持线上运行和停止。 -首先,我们需要把 `egg-scripts` 模块作为 `dependencies` 引入: +首先,将 `egg-scripts` 模块引入 `dependencies`: ```bash $ npm i egg-scripts --save ``` -添加 `npm scripts` 到 `package.json`: +`package.json` 添加 `npm scripts`: ```json { @@ -51,9 +51,9 @@ $ npm i egg-scripts --save } ``` -这样我们就可以通过 `npm start` 和 `npm stop` 命令启动或停止应用。 +现在,可以通过 `npm start` 和 `npm stop` 启停应用。 -> 注意:`egg-scripts` 对 Windows 系统的支持有限,参见 [#22](https://github.com/eggjs/egg-scripts/pull/22)。 +> 注意:Windows 系统下 `egg-scripts` 支持有限,详见 [#22](https://github.com/eggjs/egg-scripts/pull/22)。 ### 启动命令 @@ -61,27 +61,27 @@ $ npm i egg-scripts --save $ egg-scripts start --port=7001 --daemon --title=egg-server-showcase ``` -如上示例,支持以下参数: +示例支持参数如下: -- `--port=7001` 端口号,默认会读取环境变量 `process.env.PORT`,如未传递将使用框架内置端口 `7001`。 -- `--daemon` 是否允许在后台模式,无需 `nohup`。若使用 Docker 建议直接前台运行。 -- `--env=prod` 框架运行环境,默认会读取环境变量 `process.env.EGG_SERVER_ENV`, 如未传递将使用框架内置环境 `prod`。 -- `--workers=2` 框架 worker 线程数,默认会创建和 CPU 核数相当的 app worker 数,可以充分的利用 CPU 资源。 -- `--title=egg-server-showcase` 用于方便 ps 进程时 grep 用,默认为 `egg-server-${appname}`。 -- `--framework=yadan` 如果应用使用了[自定义框架](../advanced/framework.md),可以配置 `package.json` 的 `egg.framework` 或指定该参数。 -- `--ignore-stderr` 忽略启动期的报错。 -- `--https.key` 指定 HTTPS 所需密钥文件的完整路径。 -- `--https.cert` 指定 HTTPS 所需证书文件的完整路径。 +- `--port=7001`:端口号,默认读取 `process.env.PORT`,未传递则使用内置端口 `7001`。 +- `--daemon`:启用后台模式,不需 `nohup`,使用 Docker 时建议前台运行。 +- `--env=prod`:运行环境,默认读取 `process.env.EGG_SERVER_ENV`,未传递则使用内置 `prod`。 +- `--workers=2`:worker 数,默认创建与 CPU 核数等量的 app worker,利用 CPU 资源。 +- `--title=egg-server-showcase`:便于 ps 进程时 grep,未设置默认为 `egg-server-${appname}`。 +- `--framework=yadan`:使用[自定义框架](../advanced/framework.md)时,配置 `package.json` 的 `egg.framework` 或指定该参数。 +- `--ignore-stderr`:忽略启动期错误。 +- `--https.key`:HTTPS 密钥路径。 +- `--https.cert`:HTTPS 证书路径。 -- 所有 [egg-cluster] 的 Options 都支持透传,如 `--port` 等。 +[egg-cluster] 的所有 Options 支持透传,如 `--port` 等。 -更多参数可查看 [egg-scripts] 和 [egg-cluster] 文档。 +更多参数见 [egg-scripts] 和 [egg-cluster] 文档。 -> 注意:`--workers` 默认使用 `process.env.EGG_WORKERS`,或者 `os.cpus().length` 值进行设置,但在 docker 中 `os.cpus().length` 不一定等于分配的核数,获得的值可能较大,导致启动失败,需要手动设置下 `--workers`,参见 [#1431](https://github.com/eggjs/egg/issues/1431#issuecomment-573989059)。 +> 注意:`--workers` 默认由 `process.env.EGG_WORKERS` 或 `os.cpus().length` 设置,Docker 中 `os.cpus().length` 可能大于核数,值较大可能导致失败,需手动设置 `--workers`,详见 [#1431](https://github.com/eggjs/egg/issues/1431#issuecomment-573989059)。 #### 启动配置项 -你也可以在 `config.{env}.js` 中配置指定启动配置。 +`config.{env}.js` 中可指定启动配置。 ```js // config/config.default.js @@ -89,13 +89,13 @@ $ egg-scripts start --port=7001 --daemon --title=egg-server-showcase exports.cluster = { listen: { port: 7001, - hostname: '127.0.0.1', // 不建议设置 hostname 为 '0.0.0.0',它将允许来自外部网络和来源的连接,请在知晓风险的情况下使用 + hostname: '127.0.0.1', // 不建议设置为 '0.0.0.0',可能导致外部连接风险,请了解后使用 // path: '/var/run/egg.sock', }, }; ``` -`path`,`port`,`hostname` 均为 [server.listen](https://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback) 的参数,`egg-scripts` 和 `egg.startCluster` 方法传入的 port 优先级高于此配置。 +`path`、`port`、`hostname` 见 [server.listen](https://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback) 参数。`egg-scripts` 和 `egg.startCluster` 传入的 port 优先级高于此配置。 ### 停止命令 @@ -103,32 +103,31 @@ exports.cluster = { $ egg-scripts stop [--title=egg-server] ``` -该命令将杀死 master 进程,并通知 worker 和 agent 优雅退出。 +该命令杀死 master 进程,并优雅退出 worker 和 agent。 -支持以下参数: +支持参数: -- `--title=egg-server` 用于杀死指定的 egg 应用,未传递则会终止所有的 Egg 应用。 - -你也可以直接通过 `ps -eo "pid,command" | grep -- "--title=egg-server"` 来找到 master 进程,并 `kill` 掉,无需 `kill -9`。 +- `--title=egg-server`:杀死指定 Egg 应用,未设置则终止所有 Egg 应用。 +也可通过 `ps -eo "pid,command" | grep -- "--title=egg-server"` 查找 master 进程,并 `kill` 掉,不需 `kill -9`。 ## 监控 -我们还需要对服务进行性能监控,内存泄露分析,故障排除等。 +我们还需要对服务进行性能监控、内存泄露分析、故障排除等。 业界常用的有: -- [Node.js 性能平台(alinode)](https://www.aliyun.com/product/nodejs) +- [Node.js 性能平台(Alinode)](https://www.aliyun.com/product/nodejs) - [NSolid](https://nodesource.com/products/nsolid/) -### Node.js 性能平台(alinode) +### Node.js 性能平台(Alinode) -**注意:** Node.js 性能平台 (alinode) 目前仅支持 macOS 和 Linux,不支持 Windows。 +**注意**:Node.js 性能平台(Alinode)目前仅支持 macOS 和 Linux,不支持 Windows。 -[Node.js 性能平台](https://www.aliyun.com/product/nodejs) 是面向所有 Node.js 应用提供 `性能监控、安全提醒、故障排查、性能优化` 等服务的整体性解决方案,提供完善的工具链和服务,协助开发者快速发现和定位线上问题。 +[Node.js 性能平台](https://www.aliyun.com/product/nodejs)是面向所有 Node.js 应用提供性能监控、安全提醒、故障排查、性能优化等服务的整体性解决方案。它提供完善的工具链和服务,协助开发者快速发现和定位线上问题。 #### 安装 Runtime -AliNode Runtime 可以直接替换掉 Node.js Runtime,对应版本参见[文档](https://help.aliyun.com/knowledge_detail/60811.html)。 +Alinode Runtime 可以直接替换掉 Node.js Runtime,对应版本参见[文档](https://help.aliyun.com/knowledge_detail/60811.html)。 全局安装方式参见[文档](https://help.aliyun.com/document_detail/60338.html)。 @@ -139,21 +138,21 @@ $ npm i nodeinstall -g $ nodeinstall --install-alinode ^3 ``` -[nodeinstall] 会把对应版本的 `alinode` 安装到项目的 `node_modules` 目录下。 +[nodeinstall]会把对应版本的 `alinode` 安装到项目的 `node_modules` 目录下。 > 注意:打包机的操作系统和线上系统需保持一致,否则对应的 Runtime 不一定能正常运行。 #### 安装及配置 -我们提供了 [egg-alinode] 来快速接入,无需安装 `agenthub` 等额外的常驻服务。 +我们提供了[egg-alinode]来快速接入,无需安装 `agenthub` 等额外的常驻服务。 -**安装依赖:** +**安装依赖**: ```bash $ npm i egg-alinode --save ``` -**开启插件:** +**开启插件**: ```js // config/plugin.js @@ -163,7 +162,7 @@ exports.alinode = { }; ``` -**配置:** +**配置**: ```js // config/config.default.js @@ -189,7 +188,7 @@ $ [Tue Aug 06 2019 15:54:25 GMT+0800 (China Standard Time)] Connecting to wss:// $ [Tue Aug 06 2019 15:54:26 GMT+0800 (China Standard Time)] agent register ok. ``` -其中 `agent register ok.` 表示配置的 `egg-alinode` 正确连接上了 Node.js 性能平台服务器。 +其中`agent register ok.`表示配置的 `egg-alinode` 正确连接上了 Node.js 性能平台服务器。 #### 访问控制台 diff --git a/site/docs/core/development.zh-CN.md b/site/docs/core/development.zh-CN.md index 7bb8a847a1..4b53e175ca 100644 --- a/site/docs/core/development.zh-CN.md +++ b/site/docs/core/development.zh-CN.md @@ -19,7 +19,7 @@ $ npm i egg-bin --save-dev ### 添加命令 -添加 `npm scripts` 到 `package.json`: +向 `package.json` 中添加 `npm scripts`: ```json { @@ -33,34 +33,34 @@ $ npm i egg-bin --save-dev ### 环境配置 -本地启动的应用是以 `env: local` 启动的,读取的配置也是 `config.default.js` 和 `config.local.js` 合并的结果。 +本地启动的应用是以 `env: local` 启动的,读取的配置是 `config.default.js` 和 `config.local.js` 合并的结果。 -> 注意:本地开发环境依赖 `egg-development` 插件,默认开启,其他环境下关闭,配置参考 [config/config.default.js](https://github.com/eggjs/egg-development/blob/master/config/config.default.js) 。 +> 注意:本地开发环境依赖 `egg-development` 插件,该插件默认开启,而在其他环境下关闭。配置参考 [config/config.default.js](https://github.com/eggjs/egg-development/blob/master/config/config.default.js)。 ### 关于 `Reload` 功能 -以下目录(含子目录)下默认会监听开发环境下的文件变化,触发一次Egg开发环境服务器重载: +以下目录(包括子目录)在开发环境下默认会监听文件变化,一旦发生变化,将触发 Egg 服务器重载: -- ${app_root}/app -- ${app_root}/config -- ${app_root}/mocks -- ${app_root}/mocks_proxy -- ${app_root}/app.js +- `${app_root}/app` +- `${app_root}/config` +- `${app_root}/mocks` +- `${app_root}/mocks_proxy` +- `${app_root}/app.js` -> 设置 `config.development.overrideDefault` 为 `true` 将跳过默认合并. +> 若设置 `config.development.overrideDefault` 为 `true`,则会跳过默认合并。 -以下目录下(包括子目录)默认忽略开发环境下的文件改动: +以下目录(包括子目录)在开发环境下默认忽略文件改动: -- ${app_root}/app/view -- ${app_root}/app/assets -- ${app_root}/app/public -- ${app_root}/app/web +- `${app_root}/app/view` +- `${app_root}/app/assets` +- `${app_root}/app/public` +- `${app_root}/app/web` -> 设置 `config.development.overrideIgnore` 为 `true` 将跳过默认合并. +> 若设置 `config.development.overrideIgnore` 为 `true`,则会跳过默认合并。 ### 指定端口 -本地启动应用默认监听 7001 端口,可指定其他端口,例如: +本地启动应用默认监听 7001 端口,你也可以指定其他端口,例如: ```json { @@ -69,14 +69,13 @@ $ npm i egg-bin --save-dev } } ``` - ## 单元测试 这里主要讲解工具部分的使用,更多关于单元测试的内容请参考[这里](./unittest.md)。 ### 添加命令 -添加 `npm scripts` 到 `package.json`: +在 `package.json` 中添加 `npm scripts`: ```json { @@ -86,29 +85,29 @@ $ npm i egg-bin --save-dev } ``` -这样我们就可以通过 `npm test` 命令运行单元测试。 +我们可以通过执行 `npm test` 命令来运行单元测试。 ### 环境配置 -测试用例执行时,应用是以 `env: unittest` 启动的,读取的配置也是 `config.default.js` 和 `config.unittest.js` 合并的结果。 +测试用例执行时,应用以 `env: unittest` 启动,读取的配置是 `config.default.js` 和 `config.unittest.js` 合并的结果。 ### 运行特定用例文件 -运行 `npm test` 时会自动执行 test 目录下的以 `.test.js` 结尾的文件(默认 [glob] 匹配规则 `test/**/*.test.js` )。 +执行 `npm test` 会自动运行 test 目录下的以 `.test.js` 结尾的文件(默认 glob 匹配规则 `test/**/*.test.js`)。 -我们在编写用例时往往想单独执行正在编写的用例,可以通过以下方式指定特定用例文件: +如果只想执行正在编写的用例,可以通过下列方式指定特定用例文件: ```bash $ TESTS=test/x.test.js npm test ``` -支持 [glob] 规则。 +该方式支持 glob 规则。 ### 指定 reporter -Mocha 支持多种形式的 reporter,默认使用 `spec` reporter。 +Mocha 支持多种形式的 reporter,默认是 `spec` reporter。 -可以手动设置 `TEST_REPORTER` 环境变量来指定 reporter,例如使用 `dot`: +通过设置 `TEST_REPORTER` 环境变量来指定 reporter,例如 `dot`: ```bash $ TEST_REPORTER=dot npm test @@ -118,7 +117,7 @@ $ TEST_REPORTER=dot npm test ### 指定用例超时时间 -默认执行超时时间为 30 秒。我们也可以手动指定超时时间(单位毫秒),例如设置为 5 秒: +用例默认超时时间是 30 秒。也可以手动设置超时时间(单位毫秒),例如设为 5 秒: ```bash $ TEST_TIMEOUT=5000 npm test @@ -126,27 +125,28 @@ $ TEST_TIMEOUT=5000 npm test ### 通过 argv 方式传参 -`egg-bin test` 除了环境变量方式,也支持直接传参,支持 mocha 的所有参数,参见:[mocha usage](https://mochajs.org/#usage) 。 +`egg-bin test` 不仅支持环境变量传参,还支持直接传参,包括所有 mocha 参数,详情见:[mocha usage](https://mochajs.org/#usage) 。 ```bash -$ # npm 传递参数需额外加一个 `--`,参见 https://docs.npmjs.com/cli/run-script +$ # npm 传参需额外加 `--`,详情见 https://docs.npmjs.com/cli/run-script $ npm test -- --help $ -$ # 等同于 `TESTS=test/**/test.js npm test`,受限于 bash,最好加上双引号 +$ # 相当于 `TESTS=test/**/test.js npm test`,但加双引号更佳,避免 bash 限制 $ npm test "test/**/test.js" $ -$ # 等同于 `TEST_REPORTER=dot npm test` +$ # 相当于 `TEST_REPORTER=dot npm test` $ npm test -- --reporter=dot $ -$ # 支持 mocha 的参数,如 grep / require 等 +$ # 支持 mocha 参数,如 grep、require 等 $ npm test -- -t 30000 --grep="should GET" ``` + ## 代码覆盖率 -egg-bin 已经内置了 [nyc](https://github.com/istanbuljs/nyc) 来支持单元测试自动生成代码覆盖率报告。 +egg-bin 已内置 [nyc](https://github.com/istanbuljs/nyc) 支持单元测试生成代码覆盖率报告。 -添加 `npm scripts` 到 `package.json`: +在 `package.json` 中添加 `npm scripts`: ```json { @@ -156,7 +156,7 @@ egg-bin 已经内置了 [nyc](https://github.com/istanbuljs/nyc) 来支持单元 } ``` -这样我们就可以通过 `npm run cov` 命令运行单元测试覆盖率。 +我们可以通过 `npm run cov` 命令运行测试覆盖率。 ```bash $ egg-bin cov @@ -179,31 +179,30 @@ Lines : 100% ( 41/41 ) ================================================================================ ``` -还可以通过 `open coverage/lcov-report/index.html` 打开完整的 HTML 覆盖率报告。 +还可以通过 `open coverage/lcov-report/index.html` 命令来查看完整 HTML 覆盖率报告。 ![image](https://cloud.githubusercontent.com/assets/156269/21845201/a9a85ab6-d82c-11e6-8c24-5e85f352be4a.png) ### 环境配置 -和 `test` 命令一样,`cov` 命令执行时,应用也是以 `env: unittest` 启动的,读取的配置也是 `config.default.js` 和 `config.unittest.js` 合并的结果。 +与 `test` 命令类似,执行 `cov` 命令时,应用以 `env: unittest` 启动,并读取 `config.default.js` 和 `config.unittest.js` 合并的配置结果。 ### 忽略指定文件 -对于某些不需要跑测试覆盖率的文件,可以通过 `COV_EXCLUDES` 环境变量指定: +对于不需要计算覆盖率的文件,可通过 `COV_EXCLUDES` 环境变量来指定: ```bash $ COV_EXCLUDES=app/plugins/c* npm run cov -$ # 或者传参方式 +$ # 或者使用传参方式 $ npm run cov -- --x=app/plugins/c* ``` - ## 调试 ### 日志输出 ### 使用 logger 模块 -框架内置了[日志](./logger.md) 功能,使用 `logger.debug()` 输出调试信息,**推荐在应用代码中使用它。** +框架内置了 [日志](./logger.md) 功能,使用 `logger.debug()` 输出调试信息,**推荐在应用代码中使用它。** ```js // controller @@ -220,11 +219,11 @@ app.logger.debug('app init'); ### 使用 debug 模块 -[debug](https://www.npmjs.com/package/debug) 模块是 Node.js 社区广泛使用的 debug 工具,很多模块都使用它模块打印调试信息,Egg 社区也广泛采用这一机制打印 debug 信息,**推荐在框架和插件开发中使用它。** +[debug](https://www.npmjs.com/package/debug) 模块是 Node.js 社区广泛使用的 debug 工具,很多模块都使用这个模块来打印调试信息,Egg 社区也广泛采用这一机制来打印 debug 信息,**推荐在框架和插件开发中使用它。** 我们可以通过 `DEBUG` 环境变量选择开启指定的调试代码,方便观测执行过程。 -(调试模块和日志模块不要混淆,而且日志模块也有很多功能,这里所说的日志都是调试信息。) +(调试模块和日志模块不要混淆;此外,日志模块还具备很多其他功能。这里所说的日志全部指调试信息。) 开启所有模块的日志: @@ -238,13 +237,12 @@ $ DEBUG=* npm run dev $ DEBUG=egg* npm run dev ``` -单元测试也可以用 `DEBUG=* npm test` 来查看测试用例运行的详细日志。 - +单元测试也可以使用 `DEBUG=* npm test` 来查看测试用例运行的详细日志。 ### 使用 egg-bin 调试 #### 添加命令 -添加 `npm scripts` 到 `package.json`: +在 `package.json` 中添加 `npm scripts`: ```json { @@ -254,28 +252,28 @@ $ DEBUG=egg* npm run dev } ``` -这样我们就可以通过 `npm run debug` 命令来断点调试应用。 +现在,我们可以通过 `npm run debug` 命令来断点调试应用。 -`egg-bin` 会智能选择调试协议,在 8.x 之后版本使用 [Inspector Protocol] 协议,低版本使用 [Legacy Protocol]。 +`egg-bin` 会智能选择调试协议。在 Node.js 8.x 之后的版本中,使用 [Inspector Protocol] 协议,低版本使用 [Legacy Protocol]。 -同时也支持自定义调试参数: +也支持自定义调试参数: ```bash -$ egg-bin debug --inpsect=9229 +$ egg-bin debug --inspect=9229 ``` -- `master` 调试端口为 9229 或 5858(旧协议) -- `agent` 调试端口固定为 5800,可以传递 `process.env.EGG_AGENT_DEBUG_PORT` 来自定义。 +- `master` 调试端口为 9229 或 5858(旧协议)。 +- `agent` 调试端口固定为 5800,可以通过传递 `process.env.EGG_AGENT_DEBUG_PORT` 来自定义。 - `worker` 调试端口为 `master` 调试端口递增。 -- 开发阶段 worker 在代码修改后会热重启,导致调试端口会自增,参见下文的 IDE 配置以便自动重连。 +- 开发阶段,worker 的代码修改后会热重启,导致调试端口自增。参见后文的 IDE 配置,可以实现自动重连。 #### 环境配置 -执行 `debug` 命令时,应用也是以 `env: local` 启动的,读取的配置是 `config.default.js` 和 `config.local.js` 合并的结果。 +执行 `debug` 命令时,应用也是以 `env: local` 启动的。读取的配置是 `config.default.js` 和 `config.local.js` 合并的结果。 #### 使用 [DevTools] 进行调试 -最新的 DevTools 只支持 [Inspector Protocol] 协议,故你需要使用 Node.js 8.x+ 的版本方能使用。 +最新的 DevTools 只支持 [Inspector Protocol] 协议,因此你需要使用 Node.js 8.x 及以上版本。 执行 `npm run debug` 启动: @@ -298,10 +296,10 @@ Debug Proxy online, now you could attach to 9999 without worry about reload. DevTools → chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9999/__ws_proxy__ ``` -然后选择以下一种方式即可: +接下来选择以下其中一种方式: -- 直接访问控制台最后输出的 `DevTools` 地址,该地址是代理后的 worker,无需担心重启问题。 -- 访问 `chrome://inspect`,配置对应的端口,然后点击 `Open dedicated DevTools for Node` 即可打开调试控制台。 +- 直接访问控制台最后输出的 `DevTools` 地址,该地址代理了 worker。不必担心重启问题。 +- 访问 `chrome://inspect` 后,配置相应的端口。点击 `Open dedicated DevTools for Node` 打开调试控制台。 ![DevTools](https://user-images.githubusercontent.com/227713/30419047-a54ac592-9967-11e7-8a05-5dbb82088487.png) @@ -309,19 +307,19 @@ DevTools → chrome-devtools://devtools/bundled/inspector.html?experiments=true& `egg-bin` 会自动读取 WebStorm 调试模式下设置的环境变量 `$NODE_DEBUG_OPTION`。 -使用 WebStorm 的 npm 调试启动即可: +直接使用 WebStorm 的 npm 调试功能启动: ![WebStorm](https://user-images.githubusercontent.com/227713/30423086-5dd32ac6-9974-11e7-840f-904e49a97694.png) #### 使用 [VSCode] 进行调试 -可以通过 2 个方式: +有以下两种方式: -方式一:开启 VSCode 配置 `Debug: Toggle Auto Attach`,然后在 Terminal 执行 `npm run debug` 即可。 +方式一:开启 VSCode 的 `Debug: Toggle Auto Attach`。然后在 Terminal 中执行 `npm run debug`。 -方式二:配置 VSCode 的 `.vscode/launch.json`,然后 F5 一键启动即可。(注意,需要关闭方式一中的配置) +方式二:配置 VSCode 的 `.vscode/launch.json`,然后使用 F5 启动。注意,要关闭方式一中的配置。 -```js +```json // .vscode/launch.json { "version": "0.2.0", @@ -344,15 +342,15 @@ DevTools → chrome-devtools://devtools/bundled/inspector.html?experiments=true& } ``` -我们也提供了一个 [vscode-eggjs] 扩展来自动生成配置。 +我们还提供了 [vscode-eggjs] 扩展,可以自动生成配置。 ![VSCode](https://user-images.githubusercontent.com/227713/35954428-7f8768ee-0cc4-11e8-90b2-67e623594fa1.png) -更多 VSCode Debug 用法可以参见文档: [Node.js Debugging in VS Code](https://code.visualstudio.com/docs/nodejs/nodejs-debugging) +更多 VSCode 调试用法请参见文档:[Node.js Debugging in VS Code](https://code.visualstudio.com/docs/nodejs/nodejs-debugging)。 ## 更多 -如果想了解更多本地开发相关的内容,例如为你的团队定制一个本地开发工具,请参考 [egg-bin]。 +想要了解更多有关本地开发的内容,例如如何为你的团队定制本地开发工具,请参阅 [egg-bin]。 [glob]: https://www.npmjs.com/package/glob [egg-bin]: https://github.com/eggjs/egg-bin diff --git a/site/docs/core/error-handling.zh-CN.md b/site/docs/core/error-handling.zh-CN.md index e1fcb852f2..512af4c547 100644 --- a/site/docs/core/error-handling.zh-CN.md +++ b/site/docs/core/error-handling.zh-CN.md @@ -11,7 +11,7 @@ order: 9 // app/service/test.js try { const res = await this.ctx.curl('http://eggjs.com/api/echo', { - dataType: 'json', + dataType: 'json' }); if (res.status !== 200) throw new Error('response status is not 200'); return res.data; @@ -21,145 +21,140 @@ try { } ``` -按照正常代码写法,所有的异常都可以用这个方式进行捕获并处理,但是一定要注意一些特殊的写法可能带来的问题。打一个不太正式的比方,我们的代码全部都在一个异步调用链上,所有的异步操作都通过 await 串接起来了,但是只要有一个地方跳出了异步调用链,异常就捕获不到了。 +按照正常代码写法,所有的异常都可以用这种方式进行捕获并处理,但是一定要注意一些特殊的写法可能带来的问题。举个不太正式的比方,我们的代码全部都在一个异步调用链上,所有的异步操作都通过 `await` 串接起来了。但是,只要有一个地方跳出了异步调用链,异常就无法被捕获。 ```js // app/controller/home.js class HomeController extends Controller { async buy() { const request = {}; - const config = await ctx.service.trade.buy(request); + const config = await this.ctx.service.trade.buy(request); // 下单后需要进行一次核对,且不阻塞当前请求 setImmediate(() => { - ctx.service.trade.check(request).catch((err) => ctx.logger.error(err)); + this.ctx.service.trade.check(request).catch(err => this.ctx.logger.error(err)); }); } } ``` -在这个场景中,如果 `service.trade.check` 方法中代码有问题,导致执行时抛出了异常,尽管框架会在最外层通过 `try catch` 统一捕获错误,但是由于 `setImmediate` 中的代码『跳出』了异步链,它里面的错误就无法被捕捉到了。因此在编写类似代码的时候一定要注意。 +在这个场景下,如果 `service.trade.check` 的代码出现问题,导致执行时抛出异常,框架虽可以在最外层通过 `try catch` 统一捕获错误,但由于 `setImmediate` 中代码『跳出』了异步链,错误就无法被捉到了。所以开发时需要特别注意。 -当然,框架也考虑到了这类场景,提供了 `ctx.runInBackground(scope)` 辅助方法,通过它又包装了一个异步链,所有在这个 scope 里面的错误都会统一捕获。 +幸运的是,框架针对类似场景提供了 `ctx.runInBackground(scope)` 辅助方法,通过它封装了另一个异步链,所有在这个 `scope` 内的错误都会被捕获。 ```js class HomeController extends Controller { async buy() { const request = {}; - const config = await ctx.service.trade.buy(request); + const config = await this.ctx.service.trade.buy(request); // 下单后需要进行一次核对,且不阻塞当前请求 - ctx.runInBackground(async () => { - // 这里面的异常都会统统被 Backgroud 捕获掉,并打印错误日志 - await ctx.service.trade.check(request); + this.ctx.runInBackground(async () => { + // 这里的异常都会被 Background 捕获,并打印错误日志 + await this.ctx.service.trade.check(request); }); } } ``` -**为了保证异常可追踪,必须保证所有抛出的异常都是 Error 类型,因为只有 Error 类型才会带上堆栈信息,定位到问题。** +**为确保异常可追踪,所有抛出的异常必须是 `Error` 类型,因为只有 `Error` 类型才具备堆栈信息,便于问题定位。** ## 框架层统一异常处理 -框架通过 [onerror](https://github.com/eggjs/egg-onerror) 插件提供了统一的错误处理机制。对一个请求的所有处理方法(Middleware、Controller、Service)中抛出的任何异常都会被它捕获,并自动根据请求想要获取的类型返回不同类型的错误(基于 [Content Negotiation](https://tools.ietf.org/html/rfc7231#section-5.3.2))。 +框架通过 [onerror](https://github.com/eggjs/egg-onerror) 插件提供统一的错误处理机制。此机制将捕获所有处理方法(Middleware、Controller、Service)中抛出的任何异常,并根据请求预期的响应类型返回不同的错误内容。 -| 请求需求的格式 | 环境 | errorPageUrl 是否配置 | 返回内容 | -| -------------- | ---------------- | --------------------- | ---------------------------------------------------- | -| HTML & TEXT | local & unittest | - | onerror 自带的错误页面,展示详细的错误信息 | -| HTML & TEXT | 其他 | 是 | 重定向到 errorPageUrl | -| HTML & TEXT | 其他 | 否 | onerror 自带的没有错误信息的简单错误页(不推荐) | -| JSON & JSONP | local & unittest | - | JSON 对象或对应的 JSONP 格式响应,带详细的错误信息 | -| JSON & JSONP | 其他 | - | JSON 对象或对应的 JSONP 格式响应,不带详细的错误信息 | +| 请求格式需求 | 环境 | `errorPageUrl` 配置 | 返回内容 | +| ------------ | ---- | ------------------- | -------- | +| HTML & TEXT | local & unittest | - | onerror 提供的详细错误页面 | +| HTML & TEXT | 其他 | 是 | 重定向至 `errorPageUrl` | +| HTML & TEXT | 其他 | 否 | 简易错误页(不含错误信息) | +| JSON & JSONP | local & unittest | - | 详细错误信息的 JSON 或 JSONP 响应 | +| JSON & JSONP | 其他 | - | 不含详细错误信息的 JSON 或 JSONP 响应 | ### errorPageUrl -onerror 插件的配置中支持 errorPageUrl 属性,当配置了 errorPageUrl 时,一旦用户请求线上应用的 HTML 页面异常,就会重定向到这个地址。 - -在 `config/config.default.js` 中 +配置了 `errorPageUrl` 后,线上应用 HTML 页面异常时将重定向至该地址。 ```js // config/config.default.js module.exports = { onerror: { - // 线上页面发生异常时,重定向到这个页面上 - errorPageUrl: '/50x.html', - }, + // 线上发生异常时,重定向到此页面 + errorPageUrl: '/50x.html' + } }; ``` ## 自定义统一异常处理 -尽管框架提供了默认的统一异常处理机制,但是应用开发中经常需要对异常时的响应做自定义,特别是在做一些接口开发的时候。框架自带的 onerror 插件支持自定义配置错误处理方法,可以覆盖默认的错误处理方法。 +虽然框架提供了默认的异常处理机制,但应用开发中往往需自定义异常响应,特别是接口开发。onerror 插件支持自定义配置错误处理方法,允许覆盖默认方法。 ```js // config/config.default.js module.exports = { onerror: { all(err, ctx) { - // 在此处定义针对所有响应类型的错误处理方法 - // 注意,定义了 config.all 之后,其他错误处理方法不会再生效 + // 定义所有响应类型的错误处理方法 + // 定义了 config.all 后,其他错误处理不再生效 ctx.body = 'error'; ctx.status = 500; }, html(err, ctx) { - // html handler + // HTML 错误处理 ctx.body = '

error

'; ctx.status = 500; }, json(err, ctx) { - // json handler + // JSON 错误处理 ctx.body = { message: 'error' }; ctx.status = 500; }, jsonp(err, ctx) { - // 一般来说,不需要特殊针对 jsonp 进行错误定义,jsonp 的错误处理会自动调用 json 错误处理,并包装成 jsonp 的响应格式 - }, - }, + // JSONP 错误一般不需特殊处理,自动调用 JSON 方法 + } + } }; ``` - -## 404 - 框架并不会将服务端返回的 404 状态当做异常来处理,但是框架提供了当响应为 404 且没有返回 body 时的默认响应。 - 当请求被框架判定为需要 JSON 格式的响应时,会返回一段 JSON: - ```json - { "message": "Not Found" } - ``` +```json +{ "message": "Not Found" } +``` - 当请求被框架判定为需要 HTML 格式的响应时,会返回一段 HTML: - ```html -

404 Not Found

- ``` +```html +

404 Not Found

+``` 框架支持通过配置,将默认的 HTML 请求的 404 响应重定向到指定的页面。 ```js // config/config.default.js module.exports = { - notfound: { - pageUrl: '/404.html', - }, + notfound: { + pageUrl: '/404.html', + }, }; ``` ### 自定义 404 响应 -在一些场景下,我们需要自定义服务器 404 时的响应,和自定义异常处理一样,我们也只需要加入一个中间件即可对 404 做统一处理: +在一些场景下,我们需要自定义服务器 404 时的响应。和自定义异常处理一样,我们也只需要加入一个中间件即可对 404 做统一处理: ```js // app/middleware/notfound_handler.js module.exports = () => { - return async function notFoundHandler(ctx, next) { - await next(); - if (ctx.status === 404 && !ctx.body) { - if (ctx.acceptJSON) { - ctx.body = { error: 'Not Found' }; - } else { - ctx.body = '

Page Not Found

'; - } - } - }; + return async function notFoundHandler(ctx, next) { + await next(); + if (ctx.status === 404 && !ctx.body) { + if (ctx.acceptJSON) { + ctx.body = { error: 'Not Found' }; + } else { + ctx.body = '

Page Not Found

'; + } + } + }; }; ``` @@ -168,6 +163,6 @@ module.exports = () => { ```js // config/config.default.js module.exports = { - middleware: ['notfoundHandler'], + middleware: ['notfoundHandler'], }; ``` diff --git a/site/docs/core/httpclient.zh-CN.md b/site/docs/core/httpclient.zh-CN.md index 3fa05c13d1..5276a96a08 100644 --- a/site/docs/core/httpclient.zh-CN.md +++ b/site/docs/core/httpclient.zh-CN.md @@ -5,32 +5,30 @@ order: 5 互联网时代,无数服务是基于 HTTP 协议进行通信的,Web 应用调用后端 HTTP 服务是一种非常常见的应用场景。 -为此框架基于 [urllib] 内置实现了一个 [HttpClient],应用可以非常便捷地完成任何 HTTP 请求。 +为此,框架基于 [urllib] 内置实现了一个 [HttpClient],应用可以非常便捷地完成任何 HTTP 请求。 ## 通过 `app` 使用 HttpClient -框架在应用初始化的时候,会自动将 [HttpClient] 初始化到 `app.httpclient`。 -同时增加了一个 `app.curl(url, options)` 方法,它等价于 `app.httpclient.request(url, options)`。 +框架在应用初始化的时候,会自动将 [HttpClient] 初始化到 `app.httpclient`。同时增加了一个 `app.curl(url, options)` 方法,它等价于 `app.httpclient.request(url, options)`。 这样就可以非常方便地使用 `app.curl` 方法完成一次 HTTP 请求。 ```js // app.js -module.exports = (app) => { +module.exports = app => { app.beforeStart(async () => { - // 示例:启动的时候去读取 https://registry.npmmirror.com/egg/latest 的版本信息 + // 示例:启动时去读取 https://registry.npmmirror.com/egg/latest 的版本信息 const result = await app.curl('https://registry.npmmirror.com/egg/latest', { dataType: 'json', }); - app.logger.info('Egg latest version: %s', result.data.version); + app.logger.info('Egg 最新版本:%s', result.data.version); }); }; ``` ## 通过 `ctx` 使用 HttpClient -框架在 Context 中同样提供了 `ctx.curl(url, options)` 和 `ctx.httpclient`,保持跟 app 下的使用体验一致。 -这样就可以在有 Context 的地方(如在 controller 中)非常方便地使用 `ctx.curl()` 方法完成一次 HTTP 请求。 +框架在 Context 中同样提供了 `ctx.curl(url, options)` 和 `ctx.httpclient`,以保持与 app 下的使用体验一致。这样,在有 Context 的地方(如在 controller 中)非常方便地使用 `ctx.curl()` 方法完成一次 HTTP 请求。 ```js // app/controller/npm.js @@ -40,7 +38,7 @@ class NpmController extends Controller { // 示例:请求一个 npm 模块信息 const result = await ctx.curl('https://registry.npmmirror.com/egg/latest', { - // 自动解析 JSON response + // 自动解析 JSON 响应 dataType: 'json', // 3 秒超时 timeout: 3000, @@ -54,17 +52,15 @@ class NpmController extends Controller { } } ``` - ## 基本 HTTP 请求 -HTTP 已经被广泛大量使用,尽管 HTTP 有多种请求方式,但是万变不离其宗,我们先以基本的 4 个请求方法为例子, -逐步讲解一下更多的复杂应用场景。 +HTTP 已经被广泛大量使用。尽管 HTTP 有多种请求方式,但是万变不离其宗。我们先以基本的四个请求方法为例子,逐步讲解一下更多的复杂应用场景。 -以下例子都会在 controller 代码中对 https://httpbin.org 发起请求来完成。 +以下例子都会在 controller 代码中对 `https://httpbin.org` 发起请求来完成。 ### GET -读取数据几乎都是使用 GET 请求,它是 HTTP 世界最常见的一种,也是最广泛的一种,它的请求参数也是最容易构造的。 +读取数据几乎都是使用 GET 请求。它是 HTTP 世界最常见的一种,也是最广泛的一种。它的请求参数也是最容易构造的。 ```js // app/controller/npm.js @@ -77,20 +73,21 @@ class NpmController extends Controller { ctx.body = result.data; } } + ``` -- GET 请求可以不用设置 `options.method` 参数,HttpClient 的默认 method 会设置为 `GET`。 -- 返回值 `result` 会包含 3 个属性:`status`, `headers` 和 `data` - - `status`: 响应状态码,如 `200`, `302`, `404`, `500` 等等 - - `headers`: 响应头,类似 `{ 'content-type': 'text/html', ... }` - - `data`: 响应 body,默认 HttpClient 不会做任何处理,会直接返回 Buffer 类型数据。 - 一旦设置了 `options.dataType`,HttpClient 将会根据此参数对 `data` 进行相应的处理。 +- GET 请求可以不用设置 `options.method` 参数,`HttpClient` 的默认 `method` 会设置为 `GET`。 +- 返回值 `result` 会包含三个属性:`status`,`headers` 和 `data`。 + - `status`:响应状态码,如 `200`,`302`,`404`,`500` 等等。 + - `headers`:响应头,类似 `{ 'content-type': 'text/html', ... }`。 + - `data`:响应 body,默认 `HttpClient` 不会进行任何处理,会直接返回 `Buffer` 类型数据。 + 一旦设置了 `options.dataType`,`HttpClient` 将会根据此参数对 `data` 进行相应的处理。 完整的请求参数 `options` 和返回值 `result` 的说明请看下文的 [options 参数详解](#options-参数详解) 章节。 ### POST -创建数据的场景一般来说都会使用 POST 请求,它相对于 GET 来说多了请求 body 这个参数。 +创建数据的场景一般来说都会使用 POST 请求,它相对于 GET 来说,多了请求 body 这个参数。 以发送 JSON body 的场景举例: @@ -114,14 +111,15 @@ class NpmController extends Controller { ctx.body = result.data; } } + ``` -下文还会详细讲解以 POST 实现 Form 表单提交和文件上传的功能。 +下文还会详细讲解以 POST 实现表单提交和文件上传的功能。 ### PUT PUT 与 POST 类似,它更加适合更新数据和替换数据的语义。 -除了 method 参数需要设置为 `PUT`,其他参数几乎跟 POST 一模一样。 +除了 method 参数需要设置为 `PUT`,其他参数几乎与 POST 完全一样。 ```js // app/controller/npm.js @@ -142,11 +140,12 @@ class NpmController extends Controller { ctx.body = result.data; } } + ``` ### DELETE -删除数据会选择 DELETE 请求,它通常可以不需要加请求 body,但是 HttpClient 不会限制。 +删除数据会选择 DELETE 请求。它通常可以不需要增加请求 body,但是 `HttpClient` 不会对此进行限制。 ```js // app/controller/npm.js @@ -162,16 +161,15 @@ class NpmController extends Controller { ctx.body = result.data; } } -``` +``` ## 高级 HTTP 请求 -在真实的应用场景下,还是会包含一些较为复杂的 HTTP 请求。 +在真实的应用场景下,还会包含一些较为复杂的 HTTP 请求。 ### Form 表单提交 -面向浏览器设计的 Form 表单(不包含文件)提交接口,通常都要求以 `content-type: application/x-www-form-urlencoded` -的格式提交请求数据。 +面向浏览器设计的 Form 表单(不包含文件)提交接口,通常都要求以 `content-type: application/x-www-form-urlencoded` 的格式提交请求数据。 ```js // app/controller/npm.js @@ -201,8 +199,7 @@ class NpmController extends Controller { ### 以 Multipart 方式上传文件 -当一个 Form 表单提交包含文件的时候,请求数据格式就必须以 [multipart/form-data](http://tools.ietf.org/html/rfc2388) -进行提交了。 +当一个 Form 表单提交包含文件时,请求数据格式就必须以 [multipart/form-data](http://tools.ietf.org/html/rfc2388) 进行提交了。 [urllib] 内置了 [formstream] 模块来帮助我们生成可以被消费的 `form` 对象。 @@ -218,10 +215,10 @@ class HttpController extends Controller { data: { foo: 'bar', }, - + // 单文件上传 files: __filename, - + // 多文件上传 // files: { // file1: __filename, @@ -233,7 +230,7 @@ class HttpController extends Controller { ctx.body = result.data.files; // 响应最终会是类似以下的结果: // { - // "file": "'use strict';\n\nconst For...." + // "file": "use strict; const For...." // } } } @@ -241,9 +238,7 @@ class HttpController extends Controller { ### 以 Stream 方式上传文件 -其实,在 Node.js 的世界里面,Stream 才是主流。 -如果服务端支持流式上传,最友好的方式还是直接发送 Stream。 -Stream 实际会以 `Transfer-Encoding: chunked` 传输编码格式发送,这个转换是 [HTTP] 模块自动实现的。 +其实,在 Node.js 的世界里面,Stream 才是主流。如果服务端支持流式上传,最友好的方式还是直接发送 Stream。Stream 实际会以 `Transfer-Encoding: chunked` 传输编码格式发送,这个转换是 [HTTP] 模块自动实现的。 ```js // app/controller/npm.js @@ -270,19 +265,18 @@ class NpmController extends Controller { } } ``` - ## options 参数详解 由于 HTTP 请求的复杂性,导致 `httpclient.request(url, options)` 的 options 参数会非常多。 -接下来将会以参数说明和代码配合一起讲解每个可选参数的实际用途。 +接下来将以参数说明和代码配合一起讲解每个可选参数的实际用途。 ### HttpClient 默认全局配置 -```js +```javascript // config/config.default.js exports.httpclient = { // 是否开启本地 DNS 缓存,默认关闭,开启后有两个特性 - // 1. 所有的 DNS 查询都会默认优先使用缓存的,即使 DNS 查询错误也不影响应用 + // 1. 所有 DNS 查询都会默认优先使用缓存的,即使 DNS 查询错误也不影响应用 // 2. 对同一个域名,在 dnsCacheLookupInterval 的间隔内(默认 10s)只会查询一次 enableDNSCache: false, // 对同一个域名进行 DNS 查询的最小间隔时间 @@ -292,7 +286,7 @@ exports.httpclient = { request: { // 默认 request 超时时间 - timeout: 3000, + timeout: 3000 }, httpAgent: { @@ -305,7 +299,7 @@ exports.httpclient = { // 允许创建的最大 socket 数 maxSockets: Number.MAX_SAFE_INTEGER, // 最大空闲 socket 数 - maxFreeSockets: 256, + maxFreeSockets: 256 }, httpsAgent: { @@ -318,8 +312,8 @@ exports.httpclient = { // 允许创建的最大 socket 数 maxSockets: Number.MAX_SAFE_INTEGER, // 最大空闲 socket 数 - maxFreeSockets: 256, - }, + maxFreeSockets: 256 + } }; ``` @@ -329,68 +323,67 @@ exports.httpclient = { 需要发送的请求数据,根据 `method` 自动选择正确的数据处理方式。 -- GET,HEAD:通过 `querystring.stringify(data)` 处理后拼接到 url 的 query 参数上。 -- POST,PUT 和 DELETE 等:需要根据 `contentType` 做进一步判断处理。 +- GET、HEAD:通过 `querystring.stringify(data)` 处理后拼接到 url 的 query 参数上。 +- POST、PUT 和 DELETE 等:需要根据 `contentType` 做进一步判断处理。 - `contentType = json`:通过 `JSON.stringify(data)` 处理,并设置为 body 发送。 - 其他:通过 `querystring.stringify(data)` 处理,并设置为 body 发送。 -```js +```javascript // GET + data ctx.curl(url, { - data: { foo: 'bar' }, + data: { foo: 'bar' } }); // POST + data ctx.curl(url, { method: 'POST', - data: { foo: 'bar' }, + data: { foo: 'bar' } }); // POST + JSON + data ctx.curl(url, { method: 'POST', contentType: 'json', - data: { foo: 'bar' }, + data: { foo: 'bar' } }); ``` ### `dataAsQueryString: Boolean` -如果设置了 `dataAsQueryString=true`,那么即使在 POST 情况下, -也会强制将 `options.data` 以 `querystring.stringify` 处理之后拼接到 `url` 的 query 参数上。 +如果设置了 `dataAsQueryString=true`,即使在 POST 请求下, +也会将 `options.data` 经 `querystring.stringify` 处理后拼接到 `url` 的 query 参数上。 -可以很好地解决以 `stream` 发送数据,且额外的请求参数以 `url` query 形式传递的应用场景: +此设置适用于需要以 `stream` 发送数据,并且附带额外的请求参数以 `url` query 形式传递的场景: -```js +```javascript ctx.curl(url, { method: 'POST', dataAsQueryString: true, data: { - // 一般来说都是 access token 之类的权限验证参数 - accessToken: 'some access token value', + // 通常是权限验证参数,如 access token + accessToken: 'some access token value' }, - stream: myFileStream, + stream: myFileStream }); ``` ### `content: String|Buffer` -发送请求正文,如果设置了此参数,那么会直接忽略 `data` 参数。 +发送请求正文。若设置此参数,将直接忽略 `data` 参数。 -```js +```javascript ctx.curl(url, { method: 'POST', - // 直接发送原始 xml 数据,不需要 HttpClient 做特殊处理 + // 直接发送原始 XML 数据,不需 HttpClient 经行特殊处理 content: 'world', headers: { - 'content-type': 'text/html', - }, + 'content-type': 'text/html' + } }); ``` - ### `files: Mixed` -文件上传,支持格式: `String | ReadStream | Buffer | Array | Object`。 +文件上传,支持以下格式:`String | ReadStream | Buffer | Array | Object`。 ```js ctx.curl(url, { @@ -420,8 +413,7 @@ ctx.curl(url, { ### `stream: ReadStream` -设置发送请求正文的可读数据流,默认是 `null`。 -一旦设置了此参数,HttpClient 将会忽略 `data` 和 `content`。 +设置发送请求正文的可读数据流,默认值为 `null`。一旦设置了此参数,`HttpClient` 将忽略 `data` 和 `content`。 ```js ctx.curl(url, { @@ -432,9 +424,7 @@ ctx.curl(url, { ### `writeStream: WriteStream` -设置接受响应数据的可写数据流,默认是 `null`。 -一旦设置此参数,那么返回值 `result.data` 将会被设置为 `null`, -因为数据已经全部写入到 `writeStream` 中了。 +设置接收响应数据的可写数据流,默认值为 `null`。一旦设置此参数,返回值 `result.data` 将被设置为 `null`,因数据已写入 `writeStream`。 ```js ctx.curl(url, { @@ -444,21 +434,17 @@ ctx.curl(url, { ### `consumeWriteStream: Boolean` -是否等待 `writeStream` 完全写完才算响应全部接收完毕,默认是 `true`。 -此参数不建议修改默认值,除非我们明确知道它的副作用是可接受的, -否则很可能会导致 `writeStream` 数据不完整。 +是否等待 `writeStream` 完全写完才算响应接收完毕,默认为 `true`。此参数建议保留默认值,除非你明确知道其可能的副作用。 ### `method: String` -设置请求方法,默认是 `GET`。 -支持 `GET、POST、PUT、DELETE、PATCH` 等[所有 HTTP 方法](https://nodejs.org/api/http.html#http_http_methods)。 +设置请求方法,默认为 `GET`。支持 `GET`、`POST`、`PUT`、`DELETE`、`PATCH` 等 [所有 HTTP 方法](https://nodejs.org/api/http.html#http_http_methods)。 ### `contentType: String` -设置请求数据格式,默认是 `undefined`,HttpClient 会自动根据 `data` 和 `content` 参数自动设置。 -`data` 是 object 的时候默认设置的是 `form`。支持 `json` 格式。 +设置请求数据格式,默认为 `undefined`。`HttpClient` 会根据 `data` 和 `content` 自动设置。`data` 为 object 时,默认设为 `form`。支持 `json` 格式。 -如需要以 JSON 格式发送 `data`: +例如,以 JSON 格式发送 `data`: ```js ctx.curl(url, { @@ -473,10 +459,9 @@ ctx.curl(url, { ### `dataType: String` -设置响应数据格式,默认不对响应数据做任何处理,直接返回原始的 buffer 格式数据。 -支持 `text` 和 `json` 两种格式。 +设置响应数据格式,默认不处理,直接返回 buffer。支持 `text` 和 `json`。 -**注意:设置成 `json` 时,如果响应数据解析失败会抛 `JSONResponseFormatError` 异常。** +**注意:若设为 `json`,解析失败则抛出 `JSONResponseFormatError` 异常。** ```js const jsonResult = await ctx.curl(url, { @@ -492,8 +477,7 @@ console.log(htmlResult.data); ### `fixJSONCtlChars: Boolean` -是否自动过滤响应数据中的特殊控制字符 (U+0000 ~ U+001F),默认是 `false`。 -通常一些 CGI 系统返回的 JSON 数据会包含这些特殊控制字符,通过此参数可以自动过滤掉它们。 +是否自动过滤特殊控制字符(U+0000~U+001F),默认为 `false`。某些 CGI 系统返回的 JSON 可能含有这些字符。 ```js ctx.curl(url, { @@ -513,20 +497,19 @@ ctx.curl(url, { }, }); ``` - ### `timeout: Number|Array` -请求超时时间,默认是 `[ 5000, 5000 ]`,即创建连接超时是 5 秒,接收响应超时是 5 秒。 +请求超时时间,默认是 `[5000, 5000]`,即创建连接超时是 5 秒,接收响应超时是 5 秒。 ```js ctx.curl(url, { // 创建连接超时 3 秒,接收响应超时 3 秒 - timeout: 3000, + timeout: 3000 }); ctx.curl(url, { // 创建连接超时 1 秒,接收响应超时 30 秒,用于响应比较大的场景 - timeout: [1000, 30000], + timeout: [1000, 30000] }); ``` @@ -536,7 +519,7 @@ ctx.curl(url, { ```js ctx.curl(url, { - agent: false, + agent: false }); ``` @@ -546,7 +529,7 @@ ctx.curl(url, { ```js ctx.curl(url, { - httpsAgent: false, + httpsAgent: false }); ``` @@ -557,19 +540,18 @@ ctx.curl(url, { ```js ctx.curl(url, { // 参数必须按照 `user:password` 格式设置 - auth: 'foo:bar', + auth: 'foo:bar' }); ``` ### `digestAuth: String` -摘要登录授权(Digest Authentication)参数,设置此参数会自动对 401 响应尝试生成 `Authorization` 请求头, -尝试以授权方式请求一次。 +摘要登录授权(Digest Authentication)参数,设置此参数会自动对 401 响应尝试生成 `Authorization` 请求头,尝试以授权方式请求一次。 ```js ctx.curl(url, { // 参数必须按照 `user:password` 格式设置 - digestAuth: 'foo:bar', + digestAuth: 'foo:bar' }); ``` @@ -579,36 +561,35 @@ ctx.curl(url, { ```js ctx.curl(url, { - followRedirect: true, + followRedirect: true }); ``` ### `maxRedirects: Number` -设置最大自动跳转次数,避免循环跳转无法终止,默认是 10 次。 -此参数不宜设置过大,它只在 `followRedirect=true` 情况下才会生效。 +设置最大自动跳转次数,避免循环跳转无法终止,默认是 10 次。此参数不宜设置过大,它只在 `followRedirect=True` 情况下才会生效。 ```js ctx.curl(url, { followRedirect: true, - // 最大只允许自动跳转 5 次。 - maxRedirects: 5, + // 最多自动跳转 5 次 + maxRedirects: 5 }); ``` ### `formatRedirectUrl: Function(from, to)` -允许我们通过 `formatRedirectUrl` 自定义实现 302、301 等跳转 url 拼接, 默认是 `url.resolve(from, to)`。 +允许通过 `formatRedirectUrl` 自定义实现 302、301 等跳转 URL 的拼接,默认是 `url.resolve(from, to)`。 ```js ctx.curl(url, { formatRedirectUrl: (from, to) => { - // 例如可在这里修正跳转不正确的 url + // 比如可以在这里修正跳转不正确的 URL if (to === '//foo/') { to = '/foo'; } return url.resolve(from, to); - }, + } }); ``` @@ -619,21 +600,19 @@ HttpClient 在请求正式发送之前,会尝试调用 `beforeRequest` 钩子 ```js ctx.curl(url, { beforeRequest: (options) => { - // 例如我们可以设置全局请求 id,方便日志跟踪 + // 比如可以在这里设置全局请求 ID,便于日志跟踪 options.headers['x-request-id'] = uuid.v1(); - }, + } }); ``` ### `streaming: Boolean` -是否直接返回响应流,默认为 `false`。 -开启 streaming 之后,HttpClient 会在拿到响应对象 res 之后马上返回, -此时 `result.headers` 和 `result.status` 已经可以读取到,只是没有读取 data 数据而已。 +是否直接返回响应流,默认为 `false`。一旦启用 `streaming`,HttpClient 会在拿到响应对象 res 之后立即返回,此时 `result.headers` 和 `result.status` 已可读取,只是没有读取数据 `data`。 ```js const result = await ctx.curl(url, { - streaming: true, + streaming: true }); console.log(result.status, result.data); @@ -641,13 +620,10 @@ console.log(result.status, result.data); ctx.body = result.res; ``` -**注意:如果 res 不是直接传递给 body,那么我们必须消费这个 stream,并且要做好 error 事件处理。** - +**注意**:如果 res 不是直接传递给 body,那么我们必须消费这个 stream 并且做好 `error` 事件的处理。 ### `gzip: Boolean` -是否支持 gzip 响应格式,默认为 `false`。 -开启 gzip 之后,HttpClient 将自动设置 `Accept-Encoding: gzip` 请求头, -并且会自动解压带 `Content-Encoding: gzip` 响应头的数据。 +是否支持 gzip 响应格式,默认为 `false`。开启 gzip 之后,HttpClient 将自动设置 `Accept-Encoding: gzip` 请求头,并且会自动解压带有 `Content-Encoding: gzip` 响应头的数据。 ```js ctx.curl(url, { @@ -657,17 +633,14 @@ ctx.curl(url, { ### `timing: Boolean` -是否开启请求各阶段的时间测量,默认为 `false`。 -开启 timing 之后,可以通过 `result.res.timing` 拿到这次 HTTP 请求各阶段的时间测量值(单位是毫秒), -通过这些测量值,我们可以非常方便地定位到这次请求最慢的环境发生在那个阶段,效果如同 Chrome network timing 的作用。 +是否开启请求各阶段的时间测量,默认为 `false`。开启 timing 之后,可以通过 `result.res.timing` 拿到这次 HTTP 请求各阶段的时间测量值(单位是毫秒)。通过这些测量值,我们可以非常方便地定位到这次请求最慢的环节发生在哪个阶段。效果类似于Chrome network timing。 timing 各阶段测量值解析: - -- queuing:分配 socket 耗时 +- queuing:分配 socket 的耗时 - dnslookup:DNS 查询耗时 - connected:socket 三次握手连接成功耗时 -- requestSent:请求数据完整发送完毕耗时 -- waiting:收到第一个字节的响应数据耗时 +- requestSent:请求数据完整发送结束耗时 +- waiting:收到第一个字节响应数据耗时 - contentDownload:全部响应数据接收完毕耗时 ```js @@ -676,18 +649,18 @@ const result = await ctx.curl(url, { }); console.log(result.res.timing); // { -// "queuing":29, -// "dnslookup":37, -// "connected":370, -// "requestSent":1001, -// "waiting":1833, -// "contentDownload":3416 +// "queuing": 29, +// "dnslookup": 37, +// "connected": 370, +// "requestSent": 1001, +// "waiting": 1833, +// "contentDownload": 3416 // } ``` -### `ca,rejectUnauthorized,pfx,key,cert,passphrase,ciphers,secureProtocol` +### `ca`、`rejectUnauthorized`、`pfx`、`key`、`cert`、`passphrase`、`ciphers` 和 `secureProtocol` -这几个都是透传给 [HTTPS] 模块的参数,具体请查看 [`https.request(options, callback)`](https://nodejs.org/api/https.html#https_https_request_options_callback)。 +这几个参数都是透传给 [HTTPS] 模块的参数,具体可查看 [`https.request(options, callback)`](https://nodejs.org/api/https.html#https_https_request_options_callback)。 ## 调试辅助 @@ -713,60 +686,51 @@ module.exports = () => { }; ``` -然后启动你的抓包工具,如 [charles] 或 [fiddler]。 - -最后通过以下指令启动应用: +然后启动抓包工具,如 [Charles] 或 [Fiddler]。通过以下指令启动应用: ```bash $ http_proxy=http://127.0.0.1:8888 npm run dev ``` -然后就可以正常操作了,所有经过 HttpClient 的请求,都可以你的抓包工具中查看到。 +操作完成后,所有通过 HttpClient 发出的请求都可以在抓包工具中查看。 ## 常见错误 ### 创建连接超时 - - 异常名称:`ConnectionTimeoutError` -- 出现场景:通常是 DNS 查询比较慢,或者客户端与服务端之间的网络速度比较慢导致的。 -- 排查建议:请适当增大 `timeout` 参数。 +- 出现场景:通常是 DNS 查询较慢或者客户端与服务端网络较慢导致。 +- 排查建议:适当增大 `timeout` 参数。 ### 服务响应超时 - - 异常名称:`ResponseTimeoutError` -- 出现场景:通常是客户端与服务端之间网络速度比较慢,并且响应数据比较大的情况下会发生。 -- 排查建议:请适当增大 `timeout` 参数。 +- 出现场景:客户端与服务端网络较慢,响应数据较大时发生。 +- 排查建议:适当增大 `timeout` 参数。 ### 服务主动断开连接 - - 异常名称:`ResponseError, code: ECONNRESET` -- 出现场景:通常是服务端主动断开 socket 连接,导致 HTTP 请求链路异常。 -- 排查建议:请检查当时服务端是否发生网络异常。 +- 出现场景:服务端主动断开 socket 连接,导致 HTTP 请求链路异常。 +- 排查建议:检查服务端是否发生网络异常。 ### 服务不可达 - - 异常名称:`RequestError, code: ECONNREFUSED, status: -1` -- 出现场景:通常是因为请求的 url 所属 IP 或者端口无法连接成功。 -- 排查建议:请确保 IP 或者端口设置正确。 +- 出现场景:请求的 URL 所属 IP 或端口无法连接。 +- 排查建议:确保 IP 或端口设置正确。 ### 域名不存在 - - 异常名称:`RequestError, code: ENOTFOUND, status: -1` -- 出现场景:通常是因为请求的 url 所在的域名无法通过 DNS 解析成功。 -- 排查建议:请确保域名存在,也需要排查一下 DNS 服务是否配置正确。 +- 出现场景:请求的 URL 域名无法通过 DNS 解析。 +- 排查建议:确保域名存在,检查 DNS 服务配置。 ### JSON 响应数据格式错误 - - 异常名称:`JSONResponseFormatError` -- 出现场景:设置了 `dataType=json` 并且响应数据不符合 JSON 格式,就会抛出此异常。 -- 排查建议:确保服务端无论在什么情况下都要正确返回 JSON 格式的数据。 - +- 出现场景:设置 `dataType=json` 但响应数据不是 JSON 格式时抛出。 +- 排查建议:确保服务端返回正确的 JSON 格式数据。 ## 全局 `request` 和 `response` 事件 -在企业应用场景,常常会有统一 tracer 日志的需求。 -为了方便在 app 层面统一监听 HttpClient 的请求和响应,我们约定了全局 `request` 和 `response` 来暴露这两个事件。 +在企业应用场景中,常常会有统一 tracer 日志的需求。 +为了方便在 app 层面统一监听 HttpClient 的请求和响应,我们约定了全局 `request` 和 `response` 事件来暴露这两个事件。 -```bash +``` init options | V @@ -788,8 +752,8 @@ $ http_proxy=http://127.0.0.1:8888 npm run dev ```js app.httpclient.on('request', (req) => { - req.url; //请求 url - req.ctx; //是发起这次请求的当前上下文 + req.url; // 请求 URL + req.ctx; // 发起这次请求的当前上下文 // 可以在这里设置一些 trace headers,方便全链路跟踪 }); @@ -797,13 +761,13 @@ app.httpclient.on('request', (req) => { ### `response` 事件:发生在网络操作结束之后 -请求结束之后会触发一个 `response` 事件,这样外部就可以订阅这个事件打印日志。 +请求结束之后会触发一个 `response` 事件,这样外部就可以订阅这个事件来打印日志。 ```js app.httpclient.on('response', (result) => { - result.res.status; - result.ctx; //是发起这次请求的当前上下文 - result.req; //对应的 req 对象,即 request 事件里面那个 req + result.res.status; // 响应状态码 + result.ctx; // 发起这次请求的当前上下文 + result.req; // 对应的 req 对象,即 request 事件里的那个 req }); ``` @@ -811,10 +775,11 @@ app.httpclient.on('response', (result) => { 完整示例代码可以在 [eggjs/examples/httpclient](https://github.com/eggjs/examples/blob/master/httpclient) 找到。 -[urllib]: https://github.com/node-modules/urllib -[httpclient]: https://github.com/eggjs/egg/blob/master/lib/core/httpclient.js -[formstream]: https://github.com/node-modules/formstream -[http]: https://nodejs.org/api/http.html -[https]: https://nodejs.org/api/https.html -[charles]: https://www.charlesproxy.com/ -[fiddler]: http://www.telerik.com/fiddler +其他参考链接: +- [urllib]: https://github.com/node-modules/urllib +- [httpclient]: https://github.com/eggjs/egg/blob/master/lib/core/httpclient.js +- [formstream]: https://github.com/node-modules/formstream +- [http]: https://nodejs.org/api/http.html +- [https]: https://nodejs.org/api/https.html +- [charles]: https://www.charlesproxy.com/ +- [fiddler]: http://www.telerik.com/fiddler diff --git a/site/docs/core/i18n.zh-CN.md b/site/docs/core/i18n.zh-CN.md index 6a044b8237..e99cee3fc8 100644 --- a/site/docs/core/i18n.zh-CN.md +++ b/site/docs/core/i18n.zh-CN.md @@ -7,7 +7,7 @@ order: 11 ## 默认语言 -默认语言是 `en-US`。假设我们想修改默认语言为简体中文: +默认语言是 `en-US`。如果我们想修改默认语言为简体中文,可以进行以下设置: ```js // config/config.default.js @@ -18,29 +18,27 @@ exports.i18n = { ## 切换语言 -我们可以通过下面几种方式修改应用的当前语言(修改后会记录到 `locale` 这个 Cookie),下次请求直接用设定好的语言。 - -优先级从高到低: +我们可以通过下面几种方式修改应用的当前语言(修改后会记录到 `locale` 这个 Cookie),下次请求会直接使用设定好的语言。优先级从高到低依次是: 1. query: `/?locale=en-US` 2. cookie: `locale=zh-TW` 3. header: `Accept-Language: zh-CN,zh;q=0.5` -如果想修改 query 或者 Cookie 参数名称: +如果需要修改 query 或 Cookie 参数名称,可以按照如下方式配置: ```js // config/config.default.js exports.i18n = { queryField: 'locale', cookieField: 'locale', - // Cookie 默认一年后过期, 如果设置为 Number,则单位为 ms + // Cookie 默认一年后过期, 如果设置为 Number,则单位为 ms cookieMaxAge: '1y', }; ``` ## 编写 I18n 多语言文件 -多种语言的配置是独立的,统一存放在 `config/locale/*.js` 下。 +不同语言的配置文件是独立存放的,统一放置在 `config/locale/*.js` 目录下。例如: ``` - config/locale/ @@ -49,11 +47,9 @@ exports.i18n = { - zh-TW.js ``` -不仅对于应用目录生效,在框架,插件的 `config/locale` 目录下同样生效。 - -**注意单词拼写,是 locale 不是 locals。** +无论是在应用目录、框架还是插件的 `config/locale` 目录下,设置都是同样生效的。注意单词的拼写应该是 locale,而不是 locals。 -例如: +例如,可以这样配置中文语言文件: ```js // config/locale/zh-CN.js @@ -62,7 +58,7 @@ module.exports = { }; ``` -或者也可以用 JSON 格式的文件: +也可以使用 JSON 格式的语言文件: ```json // config/locale/zh-CN.json @@ -70,12 +66,11 @@ module.exports = { "Email": "邮箱" } ``` - ## 获取多语言文本 -我们可以使用 `__` (Alias: `gettext`) 函数获取 locale 文件夹下面的多语言文本。 +我们可以使用 `__`(别名:`gettext`)函数获取 locale 文件夹下面的多语言文本。 -**注意: `__` 是两个下划线** +**注意:`__` 是两个下划线。** 以上面配置过的多语言为例: @@ -85,16 +80,16 @@ ctx.__('Email'); // en-US => Email ``` -如果文本中含有 `%s`,`%j` 等 format 函数,可以按照 [`util.format()`](https://nodejs.org/api/util.html#util_util_format_format_args) 类似的方式调用: +如果文本中含有 `%s`、`%j` 等 format 函数,可以按照 [`util.format()`](https://nodejs.org/api/util.html#util_util_format_format_args) 类似的方式调用: ```js // config/locale/zh-CN.js module.exports = { - 'Welcome back, %s!': '欢迎回来,%s!', + 'Welcome back, %s!': '欢迎回来,%s!', }; ctx.__('Welcome back, %s!', 'Shawn'); -// zh-CN => 欢迎回来,Shawn! +// zh-CN => 欢迎回来,Shawn! // en-US => Welcome back, Shawn! ``` @@ -103,7 +98,7 @@ ctx.__('Welcome back, %s!', 'Shawn'); ```js // config/locale/zh-CN.js module.exports = { - 'Hello {0}! My name is {1}.': '你好 {0}! 我的名字叫 {1}。', + 'Hello {0}! My name is {1}.': '你好 {0}!我的名字叫 {1}。', }; ctx.__('Hello {0}! My name is {1}.', ['foo', 'bar']); @@ -118,10 +113,10 @@ class HomeController extends Controller { async index() { const ctx = this.ctx; ctx.body = { - message: ctx.__('Welcome back, %s!', ctx.user.name) - // 或者使用 gettext,gettext 是 __ 函数的 alias + message: ctx.__('Welcome back, %s!', ctx.user.name), + // 或者使用 gettext,gettext 是 `__` 函数的别名 // message: ctx.gettext('Welcome back', ctx.user.name) - user: ctx.user, + user: ctx.user }; } } @@ -129,10 +124,10 @@ class HomeController extends Controller { ### View 中使用 -假设我们使用的模板引擎是 [Nunjucks](https://github.com/eggjs/egg-view-nunjucks) +假设我们使用的模板引擎是 [Nunjucks](https://github.com/eggjs/egg-view-nunjucks)。 ```html -
  • {{ __('Email') }}: {{ user.email }}
  • +
  • {{ __('Email') }}:{{ user.email }}
  • {{ __('Welcome back, %s!', user.name) }}
  • {{ __('Hello {0}! My name is {1}.', ['foo', 'bar']) }}
  • ``` diff --git a/site/docs/core/logger.zh-CN.md b/site/docs/core/logger.zh-CN.md index f41ddb00b0..ecd68e4084 100644 --- a/site/docs/core/logger.zh-CN.md +++ b/site/docs/core/logger.zh-CN.md @@ -7,20 +7,20 @@ order: 4 框架内置了强大的企业级日志支持,由 [egg-logger](https://github.com/eggjs/egg-logger) 模块提供。 -主要特性: +主要特性包括: -- 日志分级 -- 统一错误日志,所有 logger 中使用 `.error()` 打印的 `ERROR` 级别日志都会打印到统一的错误日志文件中,便于追踪 -- 启动日志和运行日志分离 -- 自定义日志 -- 多进程日志 -- 自动切割日志 -- 高性能 +- 日志分级。 +- 统一错误日志:所有 logger 中使用 `.error()` 打印的 `ERROR` 级别日志都会打印到统一的错误日志文件中,便于追踪。 +- 启动日志和运行日志分离。 +- 自定义日志。 +- 多进程日志。 +- 自动切割日志。 +- 高性能。 ## 日志路径 - 所有日志文件默认都放在 `${appInfo.root}/logs/${appInfo.name}` 路径下,例如 `/home/admin/logs/example-app`。 -- 在本地开发环境 (env: local) 和单元测试环境 (env: unittest),为了避免冲突以及集中管理,日志会打印在项目目录下的 logs 目录,例如 `/path/to/example-app/logs/example-app`。 +- 在本地开发环境(env: local)和单元测试环境(env: unittest)中,为避免冲突和集中管理,日志会打印在项目目录下的 logs 目录,例如 `/path/to/example-app/logs/example-app`。 如果想自定义日志路径: @@ -35,16 +35,16 @@ exports.logger = { 框架内置了几种日志,分别在不同的场景下使用: -- appLogger `${appInfo.name}-web.log`,例如 `example-app-web.log`,应用相关日志,供应用开发者使用的日志。我们在绝大数情况下都在使用它。 -- coreLogger `egg-web.log` 框架内核、插件日志。 -- errorLogger `common-error.log` 实际一般不会直接使用它,任何 logger 的 `.error()` 调用输出的日志都会重定向到这里,重点通过查看此日志定位异常。 -- agentLogger `egg-agent.log` agent 进程日志,框架和使用到 agent 进程执行任务的插件会打印一些日志到这里。 +- appLogger `${appInfo.name}-web.log`,例如 `example-app-web.log`,应用相关日志,供应用开发者使用的日志。我们在绝大多数情况下都在使用它。 +- coreLogger `egg-web.log`:框架内核、插件日志。 +- errorLogger `common-error.log`:实际上一般不会直接使用它,任何 logger 的 `.error()` 调用输出的日志都会重定向到这里,重点通过查看此日志定位异常。 +- agentLogger `egg-agent.log`:agent 进程日志,框架和使用到 agent 进程执行任务的插件会打印一些日志到这里。 如果想自定义以上日志文件名称,可以在 config 文件中覆盖默认值: ```js // config/config.${env}.js -module.exports = (appInfo) => { +module.exports = appInfo => { return { logger: { appLogName: `${appInfo.name}-web.log`, @@ -60,24 +60,23 @@ module.exports = (appInfo) => { ### Context Logger -如果我们在处理请求时需要打印日志,这时候使用 Context Logger,用于记录 Web 行为相关的日志。 +如果我们在处理请求时需要打印日志,这时候使用 Context Logger 用于记录 Web 行为相关的日志。 -每行日志会自动记录上当前请求的一些基本信息, -如 `[$userId/$ip/$traceId/${cost}ms $method $url]`。 +每行日志会自动记录当前请求的一些基本信息,如 `[$userId/$ip/$traceId/${cost}ms $method $url]`。 ```js ctx.logger.debug('debug info'); ctx.logger.info('some request data: %j', ctx.request.body); -ctx.logger.warn('WARNNING!!!!'); +ctx.logger.warn('警告!'); -// 错误日志记录,直接会将错误日志完整堆栈信息记录下来,并且输出到 errorLog 中 +// 错误日志记录,直接会将错误日志的完整堆栈信息记录下来,并且输出到 errorLog 中 // 为了保证异常可追踪,必须保证所有抛出的异常都是 Error 类型,因为只有 Error 类型才会带上堆栈信息,定位到问题。 ctx.logger.error(new Error('whoops')); ``` -对于框架开发者和插件开发者会使用到的 Context Logger 还有 `ctx.coreLogger`。 +对于框架开发者和插件开发者,还可以使用 `ctx.coreLogger`。 -例如 +例如: ```js ctx.coreLogger.info('info'); @@ -89,20 +88,20 @@ ctx.coreLogger.info('info'); ```js // app.js -module.exports = (app) => { +module.exports = app => { app.logger.debug('debug info'); app.logger.info('启动耗时 %d ms', Date.now() - start); - app.logger.warn('warning!'); + app.logger.warn('警告!'); app.logger.error(someErrorObj); }; ``` -对于框架和插件开发者会使用到的 App Logger 还有 `app.coreLogger`。 +对于框架和插件开发者,还可以使用 `app.coreLogger`。 ```js // app.js -module.exports = (app) => { +module.exports = app => { app.coreLogger.info('启动耗时 %d ms', Date.now() - start); }; ``` @@ -113,20 +112,19 @@ module.exports = (app) => { ```js // agent.js -module.exports = (agent) => { +module.exports = agent => { agent.logger.debug('debug info'); agent.logger.info('启动耗时 %d ms', Date.now() - start); - agent.logger.warn('warning!'); + agent.logger.warn('警告!'); agent.logger.error(someErrorObj); }; ``` 如需详细了解 Agent 进程,请参考[多进程模型](./cluster-and-ipc.md)。 - ## 日志文件编码 -默认编码为 `utf-8`,可通过如下方式覆盖: +默认编码为 `utf-8`,可通过下面的方式进行覆盖: ```js // config/config.${env}.js @@ -137,7 +135,7 @@ exports.logger = { ## 日志文件格式 -设置输出格式为 JSON,方便日志监控系统分析 +设置输出格式为 JSON,方便日志监控系统分析。 ```js // config/config.${env}.js @@ -148,17 +146,17 @@ exports.logger = { ## 日志级别 -日志分为 `NONE`,`DEBUG`,`INFO`,`WARN` 和 `ERROR` 5 个级别。 +日志分为 `NONE`、`DEBUG`、`INFO`、`WARN` 和 `ERROR` 5 个级别。 日志打印到文件中的同时,为了方便开发,也会同时打印到终端中。 ### 文件日志级别 -默认只会输出 `INFO` 及以上(`WARN` 和 `ERROR`)的日志到文件中。 +默认只会输出 `INFO` 及以上(即 `WARN` 和 `ERROR`)的日志到文件中。 -可通过如下方式配置输出到文件日志的级别: +可以通过下面的方式配置输出到文件中的日志级别: -打印所有级别日志到文件中: +- 打印所有级别的日志到文件中: ```js // config/config.${env}.js @@ -167,7 +165,7 @@ exports.logger = { }; ``` -关闭所有打印到文件的日志: +- 关闭所有输出到文件的日志: ```js // config/config.${env}.js @@ -176,9 +174,9 @@ exports.logger = { }; ``` -#### 生产环境打印 debug 日志 +#### 生产环境下打印 debug 日志 -为了避免一些插件的调试日志在生产环境打印导致性能问题, 生产环境默认禁止打印 DEBUG 级别的日志,如果确实有需求在生产环境打印 DEBUG 日志进行调试,需要打开 `allowDebugAtProd` 配置项。 +为了避免一些插件的调试日志在生产环境中打印导致性能问题,生产环境默认禁止打印 `DEBUG` 级别的日志。如果确实有需求在生产环境中打印 `DEBUG` 日志进行调试,需要打开 `allowDebugAtProd` 配置项。 ```js // config/config.prod.js @@ -190,13 +188,11 @@ exports.logger = { ### 终端日志级别 -默认只会输出 `INFO` 及以上(`WARN` 和 `ERROR`)的日志到终端中。(注意:这些日志默认只在 local 和 unittest 环境下会打印到终端) +默认只会输出 `INFO` 及以上(即 `WARN` 和 `ERROR`)的日志到终端中。这些日志默认只在 `local` 和 `unittest` 环境下打印到终端。 -- `logger.consoleLevel`: 输出到终端日志的级别。默认为 `INFO`,在 local 和 unittest 环境下默认为 `WARN`。 +可以通过下面的方式配置输出到终端的日志级别: -可通过如下方式配置输出到终端日志的级别: - -打印所有级别日志到终端: +- 打印所有级别的日志到终端: ```js // config/config.${env}.js @@ -205,7 +201,7 @@ exports.logger = { }; ``` -关闭所有打印到终端的日志: +- 关闭所有输出到终端的日志: ```js // config/config.${env}.js @@ -214,7 +210,7 @@ exports.logger = { }; ``` -- 基于性能的考虑,在正式环境下,默认会关闭终端日志输出。如有需要,你可以通过下面的配置开启。(**不推荐**) +- 基于性能考虑,在正式环境下,默认会关闭终端日志输出。如有需要,可以通过下面的配置进行开启(**不推荐**): ```js // config/config.${env}.js @@ -222,31 +218,30 @@ exports.logger = { disableConsoleAfterReady: false, }; ``` - ## 自定义日志 ### 增加自定义日志 -**一般应用无需配置自定义日志**,因为日志打太多或太分散都会导致关注度分散,反而难以管理和难以排查发现问题。 +一般应用无需配置自定义日志,因为日志打太多或太分散都会导致关注度分散,反而难以管理和难以排查发现问题。 -如果实在有需求可以如下配置: +如果确实有这样的需求,可以参照下面的配置: ```js // config/config.${env}.js const path = require('path'); -module.exports = (appInfo) => { +module.exports = appInfo => { return { customLogger: { xxLogger: { - file: path.join(appInfo.root, 'logs/xx.log'), - }, - }, + file: path.join(appInfo.root, 'logs/xx.log') + } + } }; }; ``` -可通过 `app.getLogger('xxLogger')` / `ctx.getLogger('xxLogger')` 获取,最终的打印结果和 coreLogger 类似。 +你可以通过 `app.getLogger('xxLogger')` 或 `ctx.getLogger('xxLogger')` 获取自定义日志对象。最终打印的日志格式和 coreLogger 类似。 ### 自定义日志格式 @@ -254,7 +249,7 @@ module.exports = (appInfo) => { // config/config.${env}.js const path = require('path'); -module.exports = (appInfo) => { +module.exports = appInfo => { return { customLogger: { xxLogger: { @@ -262,34 +257,27 @@ module.exports = (appInfo) => { formatter(meta) { return `[${meta.date}] ${meta.message}`; }, - // ctx logger contextFormatter(meta) { return `[${meta.date}] [${meta.ctx.method} ${meta.ctx.url}] ${meta.message}`; - }, - }, - }, + } + } + } }; }; ``` ### 高级自定义日志 -日志默认是打印到日志文件中,当本地开发时同时会打印到终端。 -但是,有时候我们会有需求把日志打印到其他媒介上,这时候我们就需要自定义日志的 transport。 +日志默认是打印到日志文件中,同时在本地开发时也会打印到终端。但有时,我们需要将日志打印到其他媒介,比如需要将错误日志打印到 `common-error.log` 的同时,上报给第三方服务。 -Transport 是一种传输通道,一个 logger 可包含多个传输通道。比如默认的 logger 就有 fileTransport 和 consoleTransport 两个通道, -分别负责打印到文件和终端。 - -举个例子,我们不仅需要把错误日志打印到 `common-error.log`,还需要上报给第三方服务。 - -首先我们定义一个日志的 transport,代表第三方日志服务。 +首先,我们定义一个日志的传输通道(transport),该通道代表第三方日志服务。 ```js const util = require('util'); const Transport = require('egg-logger').Transport; class RemoteErrorTransport extends Transport { - // 定义 log 方法,在此方法中把日志上报给远端服务 + // 定义 log 方法。在此方法中,将日志上报给远端服务。 log(level, args) { let log; if (args[0] instanceof Error) { @@ -299,29 +287,25 @@ class RemoteErrorTransport extends Transport { err.name, err.message, err.stack, - process.pid, + process.pid ); } else { log = util.format(...args); } - this.options.app - .curl('http://url/to/remote/error/log/service/logs', { - data: log, - method: 'POST', - }) - .catch(console.error); + this.options.app.curl('http://url/to/remote/error/log/service/logs', { + data: log, + method: 'POST' + }) + .catch(console.error); } } -// app.js 中给 errorLogger 添加 transport,这样每条日志就会同时打印到这个 transport 了 -app - .getLogger('errorLogger') - .set('remote', new RemoteErrorTransport({ level: 'ERROR', app })); +// 在 app.js 中给 errorLogger 添加 transport,这样每条日志就会同时打印到这个 transport。 +app.getLogger('errorLogger').set('remote', new RemoteErrorTransport({ level: 'ERROR', app })); ``` -上面的例子比较简单,实际情况中我们需要考虑性能,很可能采取先打印到内存,再定时上传的策略,以提高性能。 - +上述代码示例中,虽然比较简单,但是在实际使用时需要考虑性能问题。通常采取先暂存至内存,再定时上传的策略,以此优化性能。 ## 日志切割 企业级日志一个最常见的需求之一是对日志进行自动切割,以方便管理。框架对日志切割的支持由 [egg-logrotator](https://github.com/eggjs/egg-logrotator) 插件提供。 @@ -330,13 +314,13 @@ app 这是框架的默认日志切割方式,在每日 `00:00` 按照 `.log.YYYY-MM-DD` 文件名进行切割。 -以 appLog 为例,当前写入的日志为 `example-app-web.log`,当凌晨 `00:00` 时,会对日志进行切割,把过去一天的日志按 `example-app-web.log.YYYY-MM-DD` 的形式切割为单独的文件。 +以 appLog 为例,当前写入的日志为 `example-app-web.log`,当凌晨 `00:00` 时,会对日志进行切割,把过去一天的日志按 `example-app-web.log.YYYY-MM-DD` 的格式切割为单独的文件。 -### 按照文件大小切割 +### 按文件大小切割 -我们也可以按照文件大小进行切割。例如,当文件超过 2G 时进行切割。 +我们也可以选择按照文件大小进行切割。例如,当文件超过 2G 时进行切割。 -例如,我们需要把 `egg-web.log` 按照大小进行切割: +举个例子,我们需要把 `egg-web.log` 按照大小进行切割: ```js // config/config.${env}.js @@ -354,13 +338,13 @@ module.exports = (appInfo) => { }; ``` -添加到 `filesRotateBySize` 的日志文件不再按天进行切割。 +添加到 `filesRotateBySize` 的日志文件将不再按天进行切割。 -### 按照小时切割 +### 按小时切割 -我们也可以选择按照小时进行切割,这和默认的按天切割非常类似,只是时间缩短到每小时。 +我们还可以选择按小时切割,这和默认的按天切割很相似,只是频率变成了每小时。 -例如,我们需要把 `common-error.log` 按照小时进行切割: +比如,我们希望把 `common-error.log` 按小时进行切割: ```js // config/config.${env}.js @@ -377,12 +361,12 @@ module.exports = (appInfo) => { }; ``` -添加到 `filesRotateByHour` 的日志文件不再被按天进行切割。 +添加到 `filesRotateByHour` 的日志文件也将不再按天切割。 ## 性能 -通常 Web 访问是高频访问,每次打印日志都写磁盘会造成频繁磁盘 IO,为了提高性能,我们采用的文件日志写入策略是: +通常,Web 访问是高频访问,每次输出日志直接写磁盘会导致频繁的磁盘 IO 操作。为了提升性能,我们采取的文件日志写入策略是: -> 日志同步写入内存,异步每隔一段时间(默认 1 秒)刷盘 +> 日志同步写入内存,异步每隔一段时间(默认 1 秒)进行刷盘。 -更多详细请参考 [egg-logger](https://github.com/eggjs/egg-logger) 和 [egg-logrotator](https://github.com/eggjs/egg-logrotator)。 +更多细节,请参考 [egg-logger](https://github.com/eggjs/egg-logger) 和 [egg-logrotator](https://github.com/eggjs/egg-logrotator)。 diff --git a/site/docs/core/security.zh-CN.md b/site/docs/core/security.zh-CN.md index dfed8d4f2c..c66d65b37e 100644 --- a/site/docs/core/security.zh-CN.md +++ b/site/docs/core/security.zh-CN.md @@ -5,30 +5,30 @@ order: 10 ## Web 安全概念 -Web 应用中存在很多安全风险,这些风险会被黑客利用,轻则篡改网页内容,重则窃取网站内部数据,更为严重的则是在网页中植入恶意代码,使得用户受到侵害。常见的安全漏洞如下: +Web 应用中存在很多安全风险,这些风险可能会被黑客利用。轻则篡改网页内容,重则窃取网站内部数据。更为严重的,则是在网页中植入恶意代码,使用户受到侵害。常见的安全漏洞包括: - XSS 攻击:对 Web 页面注入脚本,使用 JavaScript 窃取用户信息,诱导用户操作。 -- CSRF 攻击:伪造用户请求向网站发起恶意请求。 +- CSRF 攻击:伪造用户请求,向网站发起恶意请求。 - 钓鱼攻击:利用网站的跳转链接或者图片制造钓鱼陷阱。 -- HTTP 参数污染:利用对参数格式验证的不完善,对服务器进行参数注入攻击。 -- 远程代码执行:用户通过浏览器提交执行命令,由于服务器端没有针对执行函数做过滤,导致在没有指定绝对路径的情况下就执行命令。 +- HTTP 参数污染:利用对参数格式验证不完善,对服务器进行参数注入攻击。 +- 远程代码执行:用户通过浏览器提交执行命令。由于服务器端没有对执行函数做过滤,导致在没有指定绝对路径下执行命令。 -而框架本身针对 Web 端常见的安全风险内置了丰富的解决方案: +框架本身针对 Web 端常见的安全风险,内置了丰富的解决方案: -- 利用 [extend](https://github.com/eggjs/egg/blob/master/docs/source/zh-cn/basics/extend.md) 机制扩展了 Helper API, 提供了各种模板过滤函数,防止钓鱼或 XSS 攻击。 +- 利用 [extend](https://github.com/eggjs/egg/blob/master/docs/source/zh-cn/basics/extend.md) 机制,扩展了 Helper API,提供了各种模板过滤函数,防止钓鱼或 XSS 攻击。 - 常见 Web 安全头的支持。 - CSRF 的防御方案。 -- 灵活的安全配置,可以匹配不同的请求 url 。 +- 灵活的安全配置,可以对不同的请求 url 进行匹配。 - 可定制的白名单,用于安全跳转和 url 过滤。 - 各种模板相关的工具函数做预处理。 -在框架中内置了安全插件 [egg-security](https://github.com/eggjs/egg-security), 提供了默认的安全实践。 +框架内置了安全插件 [egg-security](https://github.com/eggjs/egg-security),提供了默认的安全实践。 ### 开启与关闭配置 注意:除非清楚地确认后果,否则不建议擅自关闭安全插件提供的功能。 -框架的安全插件是默认开启的,如果我们想关闭其中一些安全防范,直接设置该项的 `enable` 属性为 false 即可。例如关闭 xframe 防范: +框架的安全插件默认是开启的。如果想关闭其中一些安全防范,直接设置该项的 `enable` 属性为 false 即可。例如关闭 xframe 防范: ```js exports.security = { @@ -38,52 +38,53 @@ exports.security = { }; ``` -### match 和 ignore +### Match 和 Ignore -match 和 ignore 使用方法和格式与[中间件通用配置](../basics/middleware.md#match和ignore)一致。 +`match` 和 `ignore` 方法和格式,与 [中间件通用配置](../basics/middleware.md#match和ignore) 一致。 -如果只想开启针对某一路径,则配置 match 选项,例如只针对 `/example` 开启 CSP: +如果只想针对某个路径开启,可以配置 `match` 选项。例如,只对 `/example` 开启 CSP: ```js exports.security = { csp: { match: '/example', policy: { - //... + // ... }, }, }; ``` -如果需要针对某一路径忽略某安全选项,则配置 ignore 选项,例如针对 `/example` 关闭 xframe,以便合作商户能够嵌入我们的页面: +如果需要针对某个路径忽略某安全选项,则配置 `ignore` 选项。比如,为了合作商户能够嵌入我们的页面,针对 `/example` 关闭 xframe: ```js exports.security = { csp: { ignore: '/example', xframe: { - //... + // ... }, }, }; ``` -如果要针对内部 ip 关闭部分安全防范: +如果要针对内部 IP 关闭部分安全防范: ```js exports.security = { csrf: { - // 判断是否需要 ignore 的方法,请求上下文 context 作为第一个参数 + // 判断是否需要 ignore 的方法,请求上下文 `context` 作为第一个参数 ignore: (ctx) => isInnerIp(ctx.ip), }, }; ``` -下面我们会针对具体的场景,来讲解如何使用框架提供的安全方案进行 Web 安全防范。 +下面将针对具体的场景,来讲解如何使用框架提供的安全方案进行 Web 安全防范。 -## 安全威胁`XSS`的防范 +--- +## 安全威胁 XSS 的防范 -[XSS]()(cross-site scripting 跨域脚本攻击)攻击是最常见的 Web 攻击,其重点是『跨域』和『客户端执行』。 +[XSS](https://www.owasp.org/index.php/Cross-site_Scripting_(XSS))(Cross-Site Scripting,跨站脚本攻击)攻击是最常见的 Web 攻击,其重点是“跨域”和“客户端执行”。 XSS 攻击一般分为两类: @@ -92,26 +93,25 @@ XSS 攻击一般分为两类: ### Reflected XSS -反射型的 XSS 攻击,主要是由于服务端接收到客户端的不安全输入,在客户端触发执行从而发起 Web 攻击。比如: +反射型的 XSS 攻击,主要是由服务端接收到客户端的不安全输入,在客户端触发执行从而发起 Web 攻击。比如: -在某购物网站搜索物品,搜索结果会显示搜索的关键词。搜索关键词填入``, 点击搜索。页面没有对关键词进行过滤,这段代码就会直接在页面上执行,弹出 alert。 +在某购物网站搜索物品,搜索结果会显示搜索的关键词。搜索关键词填入 ``,点击搜索。页面没有对关键词进行过滤,这段代码就会直接在页面上执行,弹出 alert。 #### 防范方式 框架提供了 `helper.escape()` 方法对字符串进行 XSS 过滤。 ```js -const str = '><'; +const str = '><'; console.log(ctx.helper.escape(str)); // => ><script>alert("abc") </script>< ``` -当网站需要直接输出用户输入的结果时,请务必使用 `helper.escape()` 包裹起来,如在 [egg-view-nunjucks] 里面就覆盖掉了内置的 `escape`。 +当网站需要直接输出用户输入的结果时,请务必使用 `helper.escape()` 包裹起来,如在 [egg-view-nunjucks](https://github.com/eggjs/egg-view-nunjucks) 里面就覆盖掉了内置的 `escape`。 另外一种情况,网站输出的内容会提供给 JavaScript 来使用。这个时候需要使用 `helper.sjs()` 来进行过滤。 -`helper.sjs()` 用于在 JavaScript(包括 onload 等 event)中输出变量,会对变量中字符进行 JavaScript ENCODE, -将所有非白名单字符转义为 `\x` 形式,防止 XSS 攻击,也确保在 js 中输出的正确性。使用实例: +`helper.sjs()` 用于在 JavaScript(包括 onload 等 event)中输出变量,会对变量中字符进行 JavaScript ENCODE,将所有非白名单字符转义为 `\x` 形式,防止 XSS 攻击,也确保在 js 中输出的正确性。使用实例: ```js const foo = '"hello"'; @@ -121,16 +121,16 @@ console.log(`var foo = "${foo}";`); // => var foo = ""hello""; // 使用 sjs -console.log(`var foo = "${this.helper.sjs(foo)}";`); +console.log(`var foo = "${ctx.helper.sjs(foo)}";`); // => var foo = "\\x22hello\\x22"; ``` -还有一种情况,有时候我们需要在 JavaScript 中输出 json ,若未做转义,易被利用为 XSS 漏洞。框架提供了 `helper.sjson()` 宏做 json encode,会遍历 json 中的 key ,将 value 的值中,所有非白名单字符转义为 `\x` 形式,防止 XSS 攻击。同时保持 json 结构不变。 +还有一种情况,有时候我们需要在 JavaScript 中输出 json,若未做转义,易被利用为 XSS 漏洞。框架提供了 `helper.sjson()` 宏做 json encode,会遍历 json 中的 key,将 value 的值中,所有非白名单字符转义为 `\x` 形式,防止 XSS 攻击。同时保持 json 结构不变。 若存在模板中输出一个 JSON 字符串给 JavaScript 使用的场景,请使用 `helper.sjson(变量名)` 进行转义。 **处理过程较复杂,性能损耗较大,请仅在必要时使用。** -实例: +实例: ```html - ``` **注意:这个路径生成规则是有映射的,如 `index.js` -> `http://127.0.0.1:8000/index.js`。如果本地开发工具不支持这层映射,比如自定义了 entry 配置,可以使用其他模板引擎。** #### 全局自定义 html 模板 -一般默认的 html 无法满足需求,可以指定模板路径和模板引擎。 +一般默认的 HTML 无法满足需求,可以指定模板路径和模板引擎: ```js // config/config.default.js -module.exports = (appInfo) => ({ +module.exports = appInfo => ({ assets: { templatePath: path.join(appInfo.baseDir, 'app/view/template.html'), templateViewEngine: 'nunjucks', @@ -87,7 +85,7 @@ module.exports = (appInfo) => ({ }); ``` -添加模板文件 +添加模板文件: ```html @@ -106,7 +104,7 @@ module.exports = (appInfo) => ({ #### 页面自定义 html 模板 -支持根据不同页面指定模板,可以在 `render` 方法传参 +支持根据不同页面指定模板,可以在 `render` 方法传参: ```js // app/controller/home.js @@ -118,10 +116,10 @@ module.exports = class HomeController extends Controller { { templatePath: path.join( this.app.config.baseDir, - 'app/view/template.html', + 'app/view/template.html' ), templateViewEngine: 'nunjucks', - }, + } ); } }; @@ -129,21 +127,20 @@ module.exports = class HomeController extends Controller { #### 修改静态资源目录 -以上例子是将静态资源放到 `app/view` 目录下,但大部分情况希望放到独立目录,如 `app/assets`。因为 assets 模板引擎使用 `egg-view` 的加载器,所以直接修改其配置 +以上例子是将静态资源放到 `app/view` 目录下,但大部分情况希望将它们放到独立目录,如 `app/assets`。因为 assets 模板引擎使用 `egg-view` 的加载器,所以直接修改其配置: ```js // config/config.default.js -module.exports = (appInfo) => ({ +module.exports = appInfo => ({ view: { // 如果还有其他模板引擎,需要合并多个目录 root: path.join(appInfo.baseDir, 'app/assets'), }, }); ``` - ### 使用其他模板引擎 -如果无法满足[文件映射](#映射关系),可以配合其他模板引擎使用,这时不需要配置 assets 模板引擎,查看[使用 umi 的例子](https://github.com/eggjs/examples/tree/master/assets-with-umi)。 +如果默认的 assets 模板引擎无法满足需求,你可以考虑结合其他模板引擎使用。这种情况下不需要配置 assets 模板引擎,你可以参考 [使用 umi 的例子](https://github.com/eggjs/examples/tree/master/assets-with-umi)。 ```js // config/config.default.js @@ -165,7 +162,7 @@ module.exports = class HomeController extends Controller { }; ``` -添加模板文件(简化了 umi 的模板) +添加模板文件(这里简化了 umi 的模板) ```html @@ -180,13 +177,13 @@ module.exports = class HomeController extends Controller { ``` -**在其他模板中必须添加参数生成需要的静态资源路径** +**在其他模板中,必须添加参数以生成所需的静态资源路径。** ### 上下文数据 -有时候前端需要获取服务端数据,所以在渲染页面时会向 window 全局对象设置数据。 +有时前端需要获取服务端数据。因此,在渲染页面时,我们通常会向 `window` 全局对象设置数据。 -assets 模板引擎可直接传入参数,默认前端代码可以从 `window.context` 获取数据。 +如果使用 assets 模板引擎,默认的前端代码可以从 `window.context` 获取数据。 ```js // app/controller/home.js @@ -197,7 +194,7 @@ module.exports = class HomeController extends Controller { }; ``` -其他模板引擎需要调用 `helper.assets.getContext(__context__)` 并传入上下文的参数 +使用其他模板引擎时,你需要调用 `helper.assets.getContext(__context__)` 函数,并传入相应的上下文参数。 ```js // app/controller/home.js @@ -210,29 +207,26 @@ module.exports = class HomeController extends Controller { }; ``` -默认属性为 `context`,这个可以通过配置修改 +默认的属性名为 `context`,但你可以通过以下配置来修改它: ```js exports.assets = { contextKey: '__context__', }; ``` - ## 构建工具 这种模式最重要的是和构建工具整合,保证本地开发体验及自动部署,所以构建工具和框架需要有一层约定。 -下面以 [roadhog] 为例 +下面以 roadhog 为例。 ### 映射关系 -构建工具的 entry 配置决定了映射关系,如基于 [webpack] 封装的 [roadhog]、[umi] 等工具内置了映射关系,如果单独使用 [webpack] 需要根据这层映射来选择用哪种方式。 +构建工具的 entry 配置决定了映射关系,如基于 webpack 封装的 roadhog、umi 等工具内置了映射关系。如果单独使用 webpack,则需要根据这层映射来选择使用哪种方式: -- 文件源码 `app/assets/index.js`,对应的 entry 为 `index.js` - -- 本地静态服务接收以此为 entry,如请求 `http://127.0.0.1:8000/index.js` - -- 构建生成的文件需要有这层映射关系,如生成 index.{hash}.js 并生成 manifest 文件描述关系如 +- 文件源码 `app/assets/index.js`,对应的 entry 为 `index.js`。 +- 本地静态服务接收以此为 entry,如请求 `http://127.0.0.1:8000/index.js`。 +- 构建生成的文件需要有这层映射关系,如生成 `index.{hash}.js` 并生成 manifest 文件描述关系,例如: ```json { @@ -240,26 +234,26 @@ exports.assets = { } ``` -[roadhog] 完全满足这个映射关系使用 [assets 模板引擎](#使用-assets-模板引擎)。而 [umi] 不满足文件映射,因为他只有一个入口 `umi.js` 文件,所以选择[其他模板引擎](#使用其他模板引擎)的方案。 +roadhog 完全满足这个映射关系,可使用 [assets 模板引擎](#使用-assets-模板引擎)。而 umi 不满足文件映射,因为它只有一个入口文件 `umi.js`。因此,可以选择[其他模板引擎](#使用其他模板引擎)的方案。 -**其他构建工具的接入需要满足这层映射关系。** +**其他构建工具接入需要满足这层映射关系。** ### 本地开发 -查看[示例配置](https://github.com/eggjs/examples/blob/master/assets-with-roadhog/config/config.default.js),本地服务配置成 `roadhog dev`,配置 `port` 来检查服务是否启动完成,因为 roadhog 默认启动端口为 8000,所以这里配置成 8000。 +查看[示例配置](https://github.com/eggjs/examples/blob/master/assets-with-roadhog/config/config.default.js),本地服务配置为 `roadhog dev`,配置 `port` 来检查服务是否启动完成。因为 roadhog 默认启动端口为 8000,所以这里配置为 8000: ```js exports.assets = { devServer: { command: 'roadhog dev', - port: 8000, - }, + port: 8000 + } }; ``` ### 部署 -静态资源部署之前需要构建,配置 `roadhog build` 命令,并执行 `npm run build` +静态资源部署之前需要构建,配置 `roadhog build` 命令,并执行 `npm run build`: ```json { @@ -269,34 +263,34 @@ exports.assets = { } ``` -**注意:这里添加了 `SET_PUBLIC_PATH` 变量是因为 roadhog 这样才能开启 publicPath** +**注意**:此处添加了 `SET_PUBLIC_PATH` 变量,因为 roadhog 这样才能开启 publicPath。 -构建的结果根据 `.webpackrc` 配置的 output 决定,示例是放到 `app/public` 目录下,由 `egg-static` 提供服务。 +构建的结果根据 `.webpackrc` 配置的 output 决定,示例中是放到 `app/public` 目录下,由 `egg-static` 提供服务。 -同时根据 `.webpackrc` 配置的 manifest 生成一个 `manifest.json` 文件到 `config` 目录下(egg 需要读取这个文件作为映射关系)。 +同时根据 `.webpackrc` 配置的 manifest 生成一个 `manifest.json` 文件到 `config` 目录下(Egg 读取此文件作为映射关系)。 #### 应用提供服务 -现在应用启动后可以通过 `http://127.0.0.1:7001/public/index.{hash}.js` 访问静态资源,发现这里多了一层 public 的路径,所以需要添加 publicPath 配置。 +现在,应用启动后可以通过 `http://127.0.0.1:7001/public/index.{hash}.js` 访问静态资源。这里多了一层 public 路径,所以需要添加 publicPath 配置: ```js // config/config.prod.js exports.assets = { - publicPath: '/public/', + publicPath: '/public/' }; ``` #### 使用 CDN -一般静态资源都会发到 CDN,所以在构建完成后需要平台将构建产物发布到 CDN 上,如 `https://cdn/myapp/index.{hash}.js`。 +通常情况下,静态资源会部署到 CDN 上。因此,在构建完成后,平台需要将构建产物发布到 CDN,例如 `https://cdn/myapp/index.{hash}.js`。 -现在除了 publichPath 还需要修改静态资源地址 +此时,除了 publicPath 还需修改静态资源地址: ```js // config/config.prod.js exports.assets = { url: 'https://cdn', - publicPath: '/myapp/', + publicPath: '/myapp/' }; ``` diff --git a/site/docs/tutorials/index.zh-CN.md b/site/docs/tutorials/index.zh-CN.md index 1f7a9e76ad..b2bad980fe 100644 --- a/site/docs/tutorials/index.zh-CN.md +++ b/site/docs/tutorials/index.zh-CN.md @@ -11,7 +11,7 @@ nav: ## 骨架类型说明 -你可以使用骨架类型,像下面这样: +你可以使用骨架类型,像下面这样: ```bash $ npm init egg --type=simple @@ -23,14 +23,14 @@ $ npm init egg --type=simple | :-------: | --------------------: | | simple | 简单 egg 应用程序骨架 | | empty | 空的 egg 应用程序骨架 | -| plugin | egg plugin 骨架 | -| framework | egg framework 骨架 | +| plugin | egg 插件骨架 | +| framework | egg 框架骨架 | ## 模板引擎 -框架内置 [egg-view] 作为模板解决方案,并支持多模板渲染,每个模板引擎都以插件的方式引入,但保持渲染的 API 一致。查看[如何使用模板](./core/view.md),如果想更深入的了解,可以查看[模板插件开发](./advanced/view-plugin.md)。 +框架内置 [egg-view] 作为模板解决方案,并支持多模板渲染。每个模板引擎都以插件的形式引入,但保持渲染的 API 一致。查看[如何使用模板](./core/view.md);如果想更深入地了解,可以查看[模板插件开发](./advanced/view-plugin.md)。 -可使用以下模板引擎,更多[查看](https://github.com/search?utf8=%E2%9C%93&q=topic%3Aegg-view&type=Repositories&ref=searchresults) +可使用以下模板引擎,更多请[查看](https://github.com/search?utf8=%E2%9C%93&q=topic%3Aegg-view&type=Repositories&ref=searchresults): - [egg-view-nunjucks] - [egg-view-react] @@ -42,7 +42,7 @@ $ npm init egg --type=simple ## 数据库 -官方维护的 ORM 模型是基于 [Leoric] 实现的 [egg-orm],目前可用的数据库插件: +官方维护的 ORM 模型是基于 [Leoric] 实现的 [egg-orm]。目前可用的数据库插件包括: - [egg-orm] - [egg-sequelize] diff --git a/site/docs/tutorials/mysql.zh-CN.md b/site/docs/tutorials/mysql.zh-CN.md index 9f8f363ad0..7d0d77e394 100644 --- a/site/docs/tutorials/mysql.zh-CN.md +++ b/site/docs/tutorials/mysql.zh-CN.md @@ -2,7 +2,7 @@ title: MySQL --- -在 Web 应用方面 MySQL 是最常见,最好的关系型数据库之一。非常多网站都选择 MySQL 作为网站数据库。 +在 Web 应用方面,MySQL 是最常见且最优秀的关系型数据库之一。许多网站选择 MySQL 作为网站数据库。 ## egg-mysql @@ -10,7 +10,7 @@ title: MySQL ### 安装与配置 -安装对应的插件 [egg-mysql] : +安装对应的插件 [egg-mysql]: ```bash $ npm i --save egg-mysql @@ -22,15 +22,15 @@ $ npm i --save egg-mysql // config/plugin.js exports.mysql = { enable: true, - package: 'egg-mysql', + package: 'egg-mysql' }; ``` -在 `config/config.${env}.js` 配置各个环境的数据库连接信息。 +在 `config/config.${env}.js` 中配置各个环境的数据库连接信息。 #### 单数据源 -如果我们的应用只需要访问一个 MySQL 数据库实例,可以如下配置: +如果我们的应用只需要访问一个 MySQL 数据库实例,可以按以下方式配置: ```js // config/config.${env}.js @@ -46,12 +46,12 @@ exports.mysql = { // 密码 password: 'test_password', // 数据库名 - database: 'test', + database: 'test' }, // 是否加载到 app 上,默认开启 app: true, // 是否加载到 agent 上,默认关闭 - agent: false, + agent: false }; ``` @@ -68,31 +68,31 @@ await app.mysql.query(sql, values); // 单实例可以直接通过 app.mysql 访 ```js exports.mysql = { clients: { - // clientId, 获取client实例,需要通过 app.mysql.get('clientId') 获取 + // clientId, 获取 client 实例,需通过 app.mysql.get('clientId') 获取 db1: { // host host: 'mysql.com', // 端口号 port: '3306', // 用户名 - user: 'test_user', + user: 'test_user', // 密码 - password: 'test_password', + password: 'test_password', // 数据库名 - database: 'test', + database: 'test' }, db2: { // host - host: 'mysql2.com', + host: 'mysql2.com', // 端口号 - port: '3307', + port: '3307', // 用户名 - user: 'test_user', + user: 'test_user', // 密码 - password: 'test_password', + password: 'test_password', // 数据库名 - database: 'test', - }, + database: 'test' + } // ... }, // 所有数据库配置的默认值 @@ -101,7 +101,7 @@ exports.mysql = { // 是否加载到 app 上,默认开启 app: true, // 是否加载到 agent 上,默认关闭 - agent: false, + agent: false }; ``` @@ -117,11 +117,11 @@ await client2.query(sql, values); #### 动态创建 -我们可以不需要将配置提前申明在配置文件中,而是在应用运行时动态的从配置中心获取实际的参数,再来初始化一个实例。 +我们也可以不在配置文件中预先声明配置,而是在应用运行时动态地从配置中心获取实际参数,然后初始化一个实例。 ```js // {app_root}/app.js -module.exports = (app) => { +module.exports = app => { app.beforeStart(async () => { // 从配置中心获取 MySQL 的配置 // { host: 'mysql.com', port: '3306', user: 'test_user', password: 'test_password', database: 'test' } @@ -131,20 +131,21 @@ module.exports = (app) => { }; ``` +[egg-mysql]: https://github.com/eggjs/egg-mysql "egg-mysql" ## Service 层 由于对 MySQL 数据库的访问操作属于 Web 层中的数据处理层,因此我们强烈建议将这部分代码放在 Service 层中维护。 下面是一个 Service 中访问 MySQL 数据库的例子。 -更多 Service 层的介绍,可以参考 [Service](../basics/service.md) +更多 Service 层的介绍,可以参考 [Service](../basics/service.md)。 ```js // app/service/user.js class UserService extends Service { async find(uid) { - // 假如 我们拿到用户 id 从数据库获取用户详细信息 - const user = await this.app.mysql.get('users', { id: 11 }); + // 假如我们拿到用户 id,从数据库获取用户详细信息 + const user = await this.app.mysql.get('users', { id: uid }); return { user }; } } @@ -162,11 +163,12 @@ class UserController extends Controller { ctx.body = user; } } -``` +``` +在上述代码中,我们首先在 Service 层中定义了一个名为 `UserService` 的类,该类继承自 Service 基类。在 `UserService` 类中,我们定义了一个异步方法 `find`,该方法通过调用 `this.app.mysql.get` 方法从 `users` 表中获取到了 id 等于 uid 参数的用户数据,在获取数据后将用户信息以对象的形式返回。在 Controller 层,我们定义了一个名为 `UserController` 的类,该类继承自 Controller 基类。在 `UserController` 类中,我们定义了一个异步方法 `info`,该方法从上下文 `ctx` 中获取到了用户 ID,然后通过调用 `ctx.service.user.find` 方法获取到了用户信息,并最终将这个用户信息赋值给响应体 `ctx.body`。通过这种方式,我们就可以在 Controller 层中获取 Service 层提供的数据,从而实现层与层之间的数据传递和业务逻辑的分离。 ## 如何编写 CRUD 语句 -下面的语句若没有特殊注明,默认都书写在 `app/service` 下。 +下面的语句,若没有特殊注明,默认都书写在 `app/service` 下。 ### Create @@ -174,22 +176,23 @@ class UserController extends Controller { ```js // 插入 -const result = await this.app.mysql.insert('posts', { title: 'Hello World' }); // 在 post 表中,插入 title 为 Hello World 的记录 +const result = await this.app.mysql.insert('posts', { title: 'Hello World' }); // 在 posts 表中,插入 title 为 Hello World 的记录 -=> INSERT INTO `posts`(`title`) VALUES('Hello World'); +// SQL 语句相当于 +// INSERT INTO `posts`(`title`) VALUES('Hello World'); console.log(result); -=> -{ - fieldCount: 0, - affectedRows: 1, - insertId: 3710, - serverStatus: 2, - warningCount: 2, - message: '', - protocol41: true, - changedRows: 0 -} +// 输出为 +// { +// fieldCount: 0, +// affectedRows: 1, +// insertId: 3710, +// serverStatus: 2, +// warningCount: 2, +// message: '', +// protocol41: true, +// changedRows: 0 +// } // 判断插入成功 const insertSuccess = result.affectedRows === 1; @@ -197,7 +200,7 @@ const insertSuccess = result.affectedRows === 1; ### Read -可以直接使用 `get` 方法或 `select` 方法获取一条或多条记录。`select` 方法支持条件查询与结果的定制。 +可以直接使用 `get` 方法或 `select` 方法获取一条或多条记录。`select` 方法支持条件查询与结果定制。 可以使用 `count` 方法对查询结果的所有行进行计数。 - 查询一条记录 @@ -205,7 +208,8 @@ const insertSuccess = result.affectedRows === 1; ```js const post = await this.app.mysql.get('posts', { id: 12 }); -=> SELECT * FROM `posts` WHERE `id` = 12 LIMIT 0, 1; +// SQL 语句相当于 +// SELECT * FROM `posts` WHERE `id` = 12 LIMIT 0, 1; ``` - 查询全表 @@ -213,31 +217,34 @@ const post = await this.app.mysql.get('posts', { id: 12 }); ```js const results = await this.app.mysql.select('posts'); -=> SELECT * FROM `posts`; +// SQL 语句相当于 +// SELECT * FROM `posts`; ``` - 条件查询和结果定制 ```js -const results = await this.app.mysql.select('posts', { // 搜索 post 表 +const results = await this.app.mysql.select('posts', { // 搜索 posts 表 where: { status: 'draft', author: ['author1', 'author2'] }, // WHERE 条件 - columns: ['author', 'title'], // 要查询的表字段 + columns: ['author', 'title'], // 要查询的字段 orders: [['created_at','desc'], ['id','desc']], // 排序方式 limit: 10, // 返回数据量 offset: 0, // 数据偏移量 }); -=> SELECT `author`, `title` FROM `posts` - WHERE `status` = 'draft' AND `author` IN('author1','author2') - ORDER BY `created_at` DESC, `id` DESC LIMIT 0, 10; +// SQL 语句相当于 +// SELECT `author`, `title` FROM `posts` +// WHERE `status` = 'draft' AND `author` IN('author1','author2') +// ORDER BY `created_at` DESC, `id` DESC LIMIT 0, 10; ``` - 统计查询结果的行数 ```js -const total = await this.app.mysql.count('posts', { status: 'published' }); // 统计 posts 表中 status 为 published 的结果行数 +const total = await this.app.mysql.count('posts', { status: 'published' }); // 统计 posts 表中 status 为 published 的行数 -=> SELECT COUNT(*) FROM `posts` WHERE `status` = 'published' +// SQL 语句相当于 +// SELECT COUNT(*) FROM `posts` WHERE `status` = 'published' ``` ### Update @@ -245,25 +252,26 @@ const total = await this.app.mysql.count('posts', { status: 'published' }); // 可以直接使用 `update` 方法更新数据库记录。 ```js -// 修改数据,将会根据主键 ID 查找,并更新 +// 修改数据 const row = { id: 123, name: 'fengmk2', - otherField: 'other field value', // any other fields u want to update - modifiedAt: this.app.mysql.literals.now, // `now()` on db server + otherField: 'other field value', // 其他想要更新的字段 + modifiedAt: this.app.mysql.literals.now, // 数据库服务器上的当前时间 }; const result = await this.app.mysql.update('posts', row); // 更新 posts 表中的记录 -=> UPDATE `posts` SET `name` = 'fengmk2', `modifiedAt` = NOW() WHERE id = 123 ; +// SQL 语句相当于 +// UPDATE `posts` SET `name` = 'fengmk2', `modifiedAt` = NOW() WHERE `id` = 123; // 判断更新成功 const updateSuccess = result.affectedRows === 1; -// 如果主键是自定义的 ID 名称,如 custom_id,则需要在 `where` 里面配置 -const row = { +// 如果主键是自定义的 ID 名称,如 custom_id,则需要在 `where` 里配置 +const row2 = { name: 'fengmk2', - otherField: 'other field value', // any other fields u want to update - modifiedAt: this.app.mysql.literals.now, // `now()` on db server + otherField: 'other field value', // 其他想要更新的字段 + modifiedAt: this.app.mysql.literals.now, // 数据库服务器上的当前时间 }; const options = { @@ -271,12 +279,13 @@ const options = { custom_id: 456 } }; -const result = await this.app.mysql.update('posts', row, options); // 更新 posts 表中的记录 +const result2 = await this.app.mysql.update('posts', row2, options); // 更新 posts 表中的记录 -=> UPDATE `posts` SET `name` = 'fengmk2', `modifiedAt` = NOW() WHERE custom_id = 456 ; +// SQL 语句相当于 +// UPDATE `posts` SET `name` = 'fengmk2', `modifiedAt` = NOW() WHERE `custom_id` = 456 ; // 判断更新成功 -const updateSuccess = result.affectedRows === 1; +const updateSuccess2 = result2.affectedRows === 1; ``` ### Delete @@ -288,46 +297,45 @@ const result = await this.app.mysql.delete('posts', { author: 'fengmk2', }); -=> DELETE FROM `posts` WHERE `author` = 'fengmk2'; +// SQL 语句相当于 +// DELETE FROM `posts` WHERE `author` = 'fengmk2'; ``` +## 直接执行 SQL 语句 -## 直接执行 sql 语句 - -插件本身也支持拼接与直接执行 sql 语句。使用 `query` 可以执行合法的 sql 语句。 +插件本身也支持拼接与直接执行 SQL 语句。使用 `query` 方法可以执行合法的 SQL 语句。 -**注意!!我们极其不建议开发者拼接 sql 语句,这样很容易引起 sql 注入!!** +**注意!!我们极其不建议开发者拼接 SQL 语句,这样很容易引起 SQL 注入!!** -如果必须要自己拼接 sql 语句,请使用 `mysql.escape` 方法。 +如果必须要自己拼接 SQL 语句,请使用 `mysql.escape` 方法。 -参考 [preventing-sql-injection-in-node-js](http://stackoverflow.com/questions/15778572/preventing-sql-injection-in-node-js) +参考 [preventing-sql-injection-in-node-js](http://stackoverflow.com/questions/15778572/preventing-sql-injection-in-node-js)。 ```js const postId = 1; const results = await this.app.mysql.query('update posts set hits = (hits + ?) where id = ?', [1, postId]); -=> update posts set hits = (hits + 1) where id = 1; +// => update posts set hits = (hits + 1) where id = 1; ``` ## 使用事务 -MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说,在人员管理系统中,你删除一个人员,你既需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等。这时候使用事务处理可以方便管理这一组操作。 -一个事务将一组连续的数据库操作,放在一个单一的工作单元来执行。该组内的每个单独的操作是成功,事务才能成功。如果事务中的任何操作失败,则整个事务将失败。 +MySQL 事务主要用于处理操作量大,复杂度高的数据。例如,在人员管理系统中,当你删除一个人员,你既需要删除人员的基本资料,也要删除与该人员相关的信息,如信箱、文章等等。这时候使用事务处理可以方便管理这一组操作。一个事务将一组连续的数据库操作,放在一个单一的工作单元来执行。只有该组内的每个单独的操作都成功,事务才能成功。如果事务中的任何操作失败,则整个事务将失败。 -一般来说,事务是必须满足 4 个条件(ACID): Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durability(可靠性) +一般来说,事务必须满足 4 个条件(ACID):Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durability(可靠性)。 - 原子性:确保事务内的所有操作都成功完成,否则事务将被中止在故障点,以前的操作将回滚到以前的状态。 -- 一致性:对于数据库的修改是一致的。 -- 隔离性:事务是彼此独立的,不互相影响 +- 一致性:对数据库的修改是一致的。 +- 隔离性:事务是彼此独立的,不互相影响。 - 持久性:确保提交事务后,事务产生的结果可以永久存在。 -因此,对于一个事务来讲,一定伴随着 beginTransaction、commit 或 rollback,分别代表事务的开始,成功和失败回滚。 +因此,对于一个事务来说,一定伴随着 `beginTransaction`、`commit` 或 `rollback`,分别代表事务的开始、成功和失败回滚。 egg-mysql 提供了两种类型的事务。 ### 手动控制 -- 优点:`beginTransaction`, `commit` 或 `rollback` 都由开发者来完全控制,可以做到非常细粒度的控制。 -- 缺点:手写代码比较多,不是每个人都能写好。忘记了捕获异常和 cleanup 都会导致严重 bug。 +- 优点:`beginTransaction`、`commit` 或 `rollback` 都由开发者完全控制,可以做到非常细粒度的控制。 +- 缺点:代码量比较多,不是每个人都能写好,忽视捕获异常和 cleanup 都会导致严重的 bug。 ```js const conn = await app.mysql.beginTransaction(); // 初始化事务 @@ -337,7 +345,7 @@ try { await conn.update(table, row2); // 第二步操作 await conn.commit(); // 提交事务 } catch (err) { - // error, rollback + // 错误,回滚 await conn.rollback(); // 一定记得捕获异常后回滚事务!! throw err; } @@ -346,9 +354,9 @@ try { ### 自动控制:Transaction with scope - API:`beginTransactionScope(scope, ctx)` - - `scope`: 一个 generatorFunction,在这个函数里面执行这次事务的所有 sql 语句。 - - `ctx`: 当前请求的上下文对象,传入 ctx 可以保证即便在出现事务嵌套的情况下,一次请求中同时只有一个激活状态的事务。 -- 优点:使用简单,不容易犯错,就感觉事务不存在的样子。 + - `scope`:一个 `generatorFunction`,在这个函数里执行这次事务的所有 SQL 语句。 + - `ctx`:当前请求的上下文对象,传入 `ctx` 可以保证即使在出现事务嵌套的情况下,一次请求中同时只有一个激活状态的事务。 +- 优点:使用简单,容易操作,感觉事务不存在。 - 缺点:整个事务要么成功,要么失败,无法做细粒度控制。 ```js @@ -357,11 +365,11 @@ const result = await app.mysql.beginTransactionScope(async (conn) => { await conn.insert(table, row1); await conn.update(table, row2); return { success: true }; -}, ctx); // ctx 是当前请求的上下文,如果是在 service 文件中,可以从 `this.ctx` 获取到 -// if error throw on scope, will auto rollback +}, ctx); // `ctx` 是当前请求的上下文,如果是在 service 文件中,可以从 `this.ctx` 获取到 +// 如果在 scope 中抛出错误,将自动回滚 ``` -## 表达式(Literal) +## 表达式(Literal) 如果需要调用 MySQL 内置的函数(或表达式),可以使用 `Literal`。 @@ -374,12 +382,12 @@ await this.app.mysql.insert(table, { create_time: this.app.mysql.literals.now, }); -=> INSERT INTO `$table`(`create_time`) VALUES(NOW()) +// => INSERT INTO `$table` (`create_time`) VALUES (NOW()) ``` ### 自定义表达式 -下例展示了如何调用 MySQL 内置的 `CONCAT(s1, ...sn)` 函数,做字符串拼接。 +以下示例展示如何调用 MySQL 内置的 `CONCAT(s1, ...sn)` 函数,进行字符串拼接。 ```js const Literal = this.app.mysql.literals.Literal; @@ -390,7 +398,7 @@ await this.app.mysql.insert(table, { fullname: new Literal(`CONCAT("${first}", "${last}")`), }); -=> INSERT INTO `$table`(`id`, `fullname`) VALUES(123, CONCAT("James", "Bond")) +// => INSERT INTO `$table` (`id`, `fullname`) VALUES (123, CONCAT("James", "Bond")) ``` [egg-mysql]: https://github.com/eggjs/egg-mysql diff --git a/site/docs/tutorials/passport.zh-CN.md b/site/docs/tutorials/passport.zh-CN.md index f2e3f17e64..dcbc5080dd 100644 --- a/site/docs/tutorials/passport.zh-CN.md +++ b/site/docs/tutorials/passport.zh-CN.md @@ -4,22 +4,21 @@ title: Passport **『登录鉴权』** 是一个常见的业务场景,包括『账号密码登录方式』和『第三方统一登录』。 -其中,后者我们经常使用到,如 Google, GitHub,QQ 统一登录,它们都是基于 [OAuth](https://oauth.net/2/) 规范。 +其中,后者我们经常使用到,如 Google、GitHub、QQ 统一登录,它们都是基于 [OAuth](https://oauth.net/2/) 规范的。 -[Passport] 是一个扩展性很强的认证中间件,支持 `Github`,`Twitter`,`Facebook` 等知名服务厂商的 `Strategy`,同时也支持通过账号密码的方式进行登录授权校验。 +[Passport] 是一个扩展性很强的认证中间件,支持 `Github`、`Twitter`、`Facebook` 等知名服务厂商的 `Strategy`,同时也支持通过账号密码的方式进行登录授权校验。 -Egg 在它之上提供了 [egg-passport] 插件,把初始化、鉴权成功后的回调处理等通用逻辑封装掉,使得开发者仅需调用几个 API 即可方便的使用 Passport 。 +Egg 在它之上提供了 [egg-passport] 插件,把初始化、鉴权成功后的回调处理等通用逻辑封装掉,使得开发者仅需调用几个 API,即可方便地使用 Passport。 [Passport] 的执行时序如下: -- 用户访问页面 -- 检查 Session -- 拦截跳鉴权登录页面 -- Strategy 鉴权 -- 校验和存储用户信息 -- 序列化用户信息到 Session -- 跳转到指定页面 - +- 用户访问页面; +- 检查 Session; +- 拦截并跳转到鉴权登录页面; +- Strategy 进行鉴权; +- 校验并存储用户信息; +- 序列化用户信息到 Session; +- 跳转到指定页面。 ## 使用 egg-passport 下面,我们将以 GitHub 登录为例,来演示下如何使用。 @@ -31,7 +30,7 @@ $ npm i --save egg-passport $ npm i --save egg-passport-github ``` -更多插件参见 [GitHub Topic - egg-passport](https://github.com/topics/egg-passport) 。 +更多插件参见 [GitHub Topic - egg-passport](https://github.com/topics/egg-passport)。 ### 配置 @@ -39,24 +38,24 @@ $ npm i --save egg-passport-github ```js // config/plugin.js -module.exports.passport = { +exports.passport = { enable: true, package: 'egg-passport', }; -module.exports.passportGithub = { +exports.passportGithub = { enable: true, package: 'egg-passport-github', }; ``` -**配置:** +**配置:** -注意:[egg-passport] 标准化了配置字段,统一为 `key` 和 `secret` 。 +注意:[egg-passport] 标准化了配置字段,统一为 `key` 和 `secret`。 ```js // config/default.js -config.passportGithub = { +exports.passportGithub = { key: 'your_clientID', secret: 'your_clientSecret', // callbackURL: '/passport/github/callback', @@ -67,19 +66,19 @@ config.passportGithub = { **注意:** - 创建一个 [GitHub OAuth Apps](https://github.com/settings/applications/new),得到 `clientID` 和 `clientSecret` 信息。 -- 填写 `callbackURL`,如 `http://127.0.0.1:7001/passport/github/callback` - - 线上部署时需要更新为对应的域名 - - 路径为配置的 `options.callbackURL`,默认为 `/passport/${strategy}/callback` -- 如应用部署在 Nginx/HAProxy 之后,需设置插件 `proxy` 选项为 `true`, 并检查以下配置: - - 代理附加 HTTP 头字段:`x-forwarded-proto` 与 `x-forwarded-host` - - 配置中 `config.proxy` 应设置为 `true` +- 填写 `callbackURL`,如 `http://127.0.0.1:7001/passport/github/callback`。 + - 线上部署时需要更新为对应的域名。 + - 路径为配置的 `options.callbackURL`,默认为 `/passport/${strategy}/callback`。 +- 如应用部署在 Nginx/HAProxy 之后,需设置插件 `proxy` 选项为 `true`,并检查以下配置: + - 代理附加 HTTP 头字段:`x-forwarded-proto` 与 `x-forwarded-host`。 + - 配置中 `config.proxy` 应设置为 `true`。 ### 挂载路由 ```js // app/router.js -module.exports = (app) => { - const { router, controller } = app; +module.exports = app => { + const { router } = app; // 挂载鉴权路由 app.passport.mount('github'); @@ -95,12 +94,12 @@ module.exports = (app) => { 接着,我们还需要: -- 首次登录时,一般需要把用户信息进行入库,并记录 Session 。 +- 首次登录时,一般需要把用户信息入库,并记录 Session。 - 二次登录时,从 OAuth 或 Session 拿到的用户信息,读取数据库拿到完整的用户信息。 ```js // app.js -module.exports = (app) => { +module.exports = app => { app.passport.verify(async (ctx, user) => { // 检查用户 assert(user.provider, 'user.provider should exists'); @@ -110,7 +109,7 @@ module.exports = (app) => { // // Authorization Table // column | desc - // --- | -- + // --- | --- // provider | provider name, like github, twitter, facebook, weibo and so on // uid | provider unique id // user_id | current application user id @@ -134,7 +133,7 @@ module.exports = (app) => { // return user; }); - // 反序列化后把用户信息从 session 中取出来,反查数据库拿到完整信息 + // 反序列化后从 session 中取出用户信息,反查数据库拿到完整信息 app.passport.deserializeUser(async (ctx, user) => { // 处理 user // ... @@ -143,38 +142,35 @@ module.exports = (app) => { }; ``` -至此,我们就完成了所有的配置,完整的示例可以参见:[eggjs/examples/passport] - +至此,我们就完成了所有的配置。完整的示例可以参见:[eggjs/examples/passport](https://github.com/topics/egg-passport)。 ### API -[egg-passport] 提供了以下扩展: +`egg-passport` 提供了以下扩展: -- `ctx.user` - 获取当前已登录的用户信息 -- `ctx.isAuthenticated()` - 检查该请求是否已授权 -- `ctx.login(user, [options])` - 为用户启动一个登录的 session -- `ctx.logout()` - 退出,将用户信息从 session 中清除 -- `ctx.session.returnTo=` - 在跳转验证前设置,可以指定成功后的 redirect 地址 +- `ctx.user`:获取当前已登录的用户信息。 +- `ctx.isAuthenticated()`:检查该请求是否已授权。 +- `ctx.login(user, [options])`:为用户启动一个登录的 session。 +- `ctx.logout()`:退出,将用户信息从 session 中清除。 +- `ctx.session.returnTo`:在跳转验证前设置,可以指定成功后的 redirect 地址。 还提供了 API: -- `app.passport.verify(async (ctx, user) => {})` - 校验用户 -- `app.passport.serializeUser(async (ctx, user) => {})` - 序列化用户信息后存储进 session -- `app.passport.deserializeUser(async (ctx, user) => {})` - 反序列化后取出用户信息 -- `app.passport.authenticate(strategy, options)` - 生成指定的鉴权中间件 - - `options.successRedirect` - 指定鉴权成功后的 redirect 地址 - - `options.loginURL` - 跳转登录地址,默认为 `/passport/${strategy}` - - `options.callbackURL` - 授权后回调地址,默认为 `/passport/${strategy}/callback` -- `app.passport.mount(strategy, options)` - 语法糖,方便开发者配置路由 +- `app.passport.verify(async (ctx, user) => {})`:校验用户。 +- `app.passport.serializeUser(async (ctx, user) => {})`:序列化用户信息后存储进 session。 +- `app.passport.deserializeUser(async (ctx, user) => {})`:反序列化后取出用户信息。 +- `app.passport.authenticate(strategy, options)`:生成指定的鉴权中间件。 + - `options.successRedirect`:指定鉴权成功后的 redirect 地址。 + - `options.loginURL`:跳转登录地址,默认为 `/passport/${strategy}`。 + - `options.callbackURL`:授权后回调地址,默认为 `/passport/${strategy}/callback`。 +- `app.passport.mount(strategy, options)`:语法糖,方便开发者配置路由。 **注意:** -- `app.passport.authenticate` 中,未设置 `options.successRedirect` 或者 `options.successReturnToOrRedirect` 将默认跳转 `/` +- 在 `app.passport.authenticate` 中,如果未设置 `options.successRedirect` 或者 `options.successReturnToOrRedirect`,将默认跳转至 `/`。 ## 使用 Passport 生态 -[Passport] 的中间件很多,不可能都进行二次封装。 -接下来,我们来看看如何在框架中直接使用 Passport 中间件。 -以『账号密码登录方式』的 [passport-local] 为例: +`Passport` 的中间件很多,不可能都进行二次封装。接下来,我们来看看如何在框架中直接使用 `Passport` 中间件。以“账号密码登录方式”为例,采用 `passport-local` 中间件: ### 安装 @@ -184,29 +180,26 @@ $ npm i --save passport-local ### 配置 -```js +```javascript // app.js const LocalStrategy = require('passport-local').Strategy; module.exports = (app) => { // 挂载 strategy - app.passport.use( - new LocalStrategy( - { - passReqToCallback: true, - }, - (req, username, password, done) => { - // format user - const user = { - provider: 'local', - username, - password, - }; - debug('%s %s get user: %j', req.method, req.url, user); - app.passport.doVerify(req, user, done); - }, - ), - ); + app.passport.use(new LocalStrategy( + { + passReqToCallback: true, + }, + (req, username, password, done) => { + // 格式化 user + const user = { + provider: 'local', + username, + password, + }; + app.passport.doVerify(req, user, done); + }, + )); // 处理用户信息 app.passport.verify(async (ctx, user) => {}); @@ -217,7 +210,7 @@ module.exports = (app) => { ### 挂载路由 -```js +```javascript // app/router.js module.exports = (app) => { const { router, controller } = app; @@ -229,13 +222,9 @@ module.exports = (app) => { // 渲染登录页面,用户输入账号密码 router.get('/login', controller.home.login); // 登录校验 - router.post( - '/login', - app.passport.authenticate('local', { successRedirect: '/authCallback' }), - ); + router.post('/login', app.passport.authenticate('local', { successRedirect: '/authCallback' })); }; ``` - ## 如何开发一个 egg-passport 插件 在上一节中,我们学会了如何在框架中使用 Passport 中间件,我们可以进一步把它封装成插件,回馈社区。 @@ -246,7 +235,7 @@ module.exports = (app) => { $ npm init egg --type=plugin egg-passport-local ``` -在 `package.json` 中**配置依赖:** +在 `package.json` 中配置依赖: ```json { @@ -265,35 +254,33 @@ $ npm init egg --type=plugin egg-passport-local **配置:** ```js -// {plugin_root}/config/config.default.js +// plugin_root/config/config.default.js // https://github.com/jaredhanson/passport-local exports.passportLocal = {}; ``` -注意:[egg-passport] 标准化了配置字段,统一为 `key` 和 `secret`,故若对应的 Passport 中间件属性名不一致时,开发者应该进行转换。 +注意:`egg-passport` 标准化了配置字段,统一为 `key` 和 `secret`。因此,如果对应的 Passport 中间件属性名不一致时,开发者应该进行转换。 **注册 passport 中间件:** ```js -// {plugin_root}/app.js +// plugin_root/app.js const LocalStrategy = require('passport-local').Strategy; module.exports = (app) => { const config = app.config.passportLocal; config.passReqToCallback = true; - app.passport.use( - new LocalStrategy(config, (req, username, password, done) => { - // 把 Passport 插件返回的数据进行清洗处理,返回 User 对象 - const user = { - provider: 'local', - username, - password, - }; - // 这里不处理应用层逻辑,传给 app.passport.verify 统一处理 - app.passport.doVerify(req, user, done); - }), - ); + app.passport.use(new LocalStrategy(config, (req, username, password, done) => { + // 把 Passport 插件返回的数据进行清洗处理,返回 User 对象 + const user = { + provider: 'local', + username, + password + }; + // 这里不处理应用层逻辑,传给 app.passport.verify 统一处理 + app.passport.doVerify(req, user, done); + })); }; ``` diff --git a/site/docs/tutorials/proxy.zh-CN.md b/site/docs/tutorials/proxy.zh-CN.md index d59eee6deb..ac17d4e939 100644 --- a/site/docs/tutorials/proxy.zh-CN.md +++ b/site/docs/tutorials/proxy.zh-CN.md @@ -2,13 +2,13 @@ title: 前置代理模式 --- -一般来说我们的服务都不会直接接受外部的请求,而会将服务部署在接入层之后,从而实现多台机器的负载均衡和服务的平滑发布,保证高可用。 +一般来说,我们的服务都不会直接接受外部的请求,而会将服务部署在接入层之后。这样做可以实现多台机器的负载均衡和服务的平滑发布,保证高可用。 -在这个场景下,我们无法直接获取到真实用户请求的连接,从而无法确认用户的真实 IP,请求协议,甚至请求的域名。为了解决这个问题,框架默认提供了一系列配置项来让开发者配置,以便基于和接入层的约定(事实标准)来让应用层获取到真实的用户请求信息。 +在这个场景下,我们无法直接获取到真实用户请求的连接,从而无法确认用户的真实 IP、请求协议,甚至请求的域名。为了解决这个问题,框架默认提供了一系列配置项,以便开发者基于与接入层的约定(事实标准)来使应用层获取真实的用户请求信息。 ## 开启前置代理模式 -通过 `config.proxy = true`,可以打开前置代理模式: +通过设置 `config.proxy = true` 可以开启前置代理模式: ```js // config/config.default.js @@ -16,11 +16,11 @@ title: 前置代理模式 exports.proxy = true; ``` -注意,开启此模式后,应用就默认自己处于反向代理之后,会支持通过解析约定的请求头来获取用户真实的 IP,协议和域名。如果你的服务未部署在反向代理之后,请不要开启此配置,以防被恶意用户伪造请求 IP 等信息。 +注意,开启此模式后,应用就默认处于反向代理之后。它会支持通过解析约定的请求头来获取用户真实的 IP、协议和域名。如果你的服务未部署在反向代理之后,请不要开启此配置,以防被恶意用户伪造请求 IP 等信息。 ### `config.ipHeaders` -开启 proxy 配置后,应用会解析 [X-Forwarded-For](https://en.wikipedia.org/wiki/X-Forwarded-For) 请求头来获取客户端的真实 IP。如果你的前置代理通过其他的请求头来传递该信息,可以通过 `config.ipHeaders` 来配置,这个配置项支持配置多个头(逗号分开)。 +开启 proxy 配置后,应用会解析 [X-Forwarded-For](https://en.wikipedia.org/wiki/X-Forwarded-For) 请求头来获取客户端的真实 IP。如果你的前置代理通过其他请求头传递该信息,可以通过 `config.ipHeaders` 来配置。此配置项支持配置多个头(逗号分开)。 ```js // config/config.default.js @@ -30,19 +30,19 @@ exports.ipHeaders = 'X-Real-Ip, X-Forwarded-For'; ### `config.maxIpsCount` -`X-Forwarded-For` 等传递 IP 的头,通用的格式是: +`X-Forwarded-For` 等传递 IP 的头,通常格式是: ``` X-Forwarded-For: client, proxy1, proxy2 ``` -我们可以拿第一个作为请求的真实 IP,但是如果有恶意用户在请求中传递了 `X-Forwarded-For` 参数来伪造其在反向代理之后,就会导致 `X-Forwarded-For` 拿到的值不准确了,可以被用来伪造请求 IP 地址,突破应用层的一些 IP 限制。 +通常我们可以取第一个作为请求的真实 IP。但是,如果有恶意用户在请求中传递了 `X-Forwarded-For` 参数,以伪造其位置在反向代理之后,将会导致获取的 `X-Forwarded-For` 值变得不准确。这可能被用来伪造请求 IP 地址,绕过应用层的某些 IP 限制。 ``` X-Forwarded-For: fake, client, proxy1, proxy2 ``` -为了避免此问题,我们可以通过 `config.maxIpsCount` 来配置前置的反向代理数量,这样在获取请求真实 IP 地址时,就会忽略掉用户多传递的伪造 IP 地址了。例如我们将应用部署在一个统一的接入层之后(例如阿里云 SLB),我们可以将此参数配置为 `1`,这样用户就无法通过 `X-Forwarded-For` 请求头来伪造 IP 地址了。 +为避免此问题,我们可以通过 `config.maxIpsCount` 来限制前置代理的数量。这样在获取请求真实 IP 地址时,会忽略掉用户所传递的伪造 IP 地址。例如,如果我们将应用部署在一个统一的接入层后(如阿里云 SLB),可以将此参数配置为 `1`。这样用户就无法通过 `X-Forwarded-For` 请求头伪造 IP 地址了。 ```js // config/config.default.js @@ -54,7 +54,7 @@ exports.maxIpsCount = 1; ### `config.protocolHeaders` -开启 proxy 配置后,应用会解析 [X-Forwarded-Proto](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) 请求头来获取客户端的真实访问协议。如果你的前置代理通过其他的请求头来传递该信息,可以通过 `config.protocolHeaders` 来配置,这个配置项支持配置多个头(逗号分开)。 +开启 proxy 配置后,应用会解析 [X-Forwarded-Proto](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) 请求头来获取客户端的真实访问协议。如果你的前置代理通过其他请求头传递该信息,可以通过 `config.protocolHeaders` 来配置。此配置项支持配置多个头(逗号分开)。 ```js // config/config.default.js @@ -64,7 +64,7 @@ exports.protocolHeaders = 'X-Real-Proto, X-Forwarded-Proto'; ### `config.hostHeaders` -开启 proxy 配置后,应用仍然还是直接读取 `host` 来获取请求的域名,绝大部分反向代理并不会修改这个值。但是也许有些反向代理会通过 [X-Forwarded-Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host) 来传递客户端的真实访问域名,可以通过在 `config.hostHeaders` 中配置,这个配置项支持配置多个头(逗号分开)。 +开启 proxy 配置后,应用通常会直接读取 `host` 来获取请求的域名,因为大多数反向代理不会修改这个值。但有时,一些反向代理会通过 [X-Forwarded-Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host) 传递客户端的真实访问域名。你可以在 `config.hostHeaders` 中配置此信息,配置项支持多个头(逗号分开)。 ```js // config/config.default.js diff --git a/site/docs/tutorials/restful.zh-CN.md b/site/docs/tutorials/restful.zh-CN.md index 90b2cfe941..293a720f74 100644 --- a/site/docs/tutorials/restful.zh-CN.md +++ b/site/docs/tutorials/restful.zh-CN.md @@ -2,13 +2,13 @@ title: 实现 RESTful API --- -通过 Web 技术开发服务给客户端提供接口,可能是各个 Web 框架最广泛的应用之一。这篇文章我们拿 [CNode 社区](https://cnodejs.org/) 的接口来看一看通过 Egg 如何实现 [RESTful](https://zh.wikipedia.org/wiki/REST) API 给客户端调用。 +通过 Web 技术开发服务,为客户端提供接口,可能是各个 Web 框架最广泛的应用之一。这篇文章我们拿 [CNode 社区](https://cnodejs.org/) 的接口来看一看,通过 Egg 如何实现 [RESTful](https://zh.wikipedia.org/wiki/REST) API,供客户端调用。 -CNode 社区现在 v1 版本的接口不是完全符合 RESTful 语义,在这篇文章中,我们将基于 CNode V1 的接口,封装一个更符合 RESTful 语义的 V2 版本 API。 +CNode 社区现在的 v1 版本接口不是完全符合 RESTful 语义。在这篇文章中,我们将基于 CNode v1 的接口,封装一个更符合 RESTful 语义的 v2 版本 API。 ## 设计响应格式 -在 RESTful 风格的设计中,我们会通过响应状态码来标识响应的状态,保持响应的 body 简洁,只返回接口数据。以 `topics` 资源为例: +在 RESTful 风格的设计中,我们通过响应状态码标识响应的状态,保持响应的 body 简洁,只返回接口数据。以 `topics` 资源为例: ### 获取主题列表 @@ -17,8 +17,7 @@ CNode 社区现在 v1 版本的接口不是完全符合 RESTful 语义,在这 - 响应体: ```json -[ - { +[{ "id": "57ea257b3670ca3f44c5beb6", "author_id": "541bf9b9ad60405c1f151a03", "tab": "share", @@ -29,8 +28,8 @@ CNode 社区现在 v1 版本的接口不是完全符合 RESTful 语义,在这 "reply_count": 155, "visit_count": 28176, "create_at": "2016-09-27T07:53:31.872Z" - }, - { +}, +{ "id": "57ea257b3670ca3f44c5beb6", "author_id": "541bf9b9ad60405c1f151a03", "tab": "share", @@ -41,8 +40,7 @@ CNode 社区现在 v1 版本的接口不是完全符合 RESTful 语义,在这 "top": true, "reply_count": 193, "visit_count": 47633 - } -] +}] ``` ### 获取单个主题 @@ -53,16 +51,16 @@ CNode 社区现在 v1 版本的接口不是完全符合 RESTful 语义,在这 ```json { - "id": "57ea257b3670ca3f44c5beb6", - "author_id": "541bf9b9ad60405c1f151a03", - "tab": "share", - "content": "content", - "title": "《一起学 Node.js》彻底重写完毕", - "last_reply_at": "2017-01-11T10:20:56.496Z", - "good": false, - "top": true, - "reply_count": 193, - "visit_count": 47633 + "id": "57ea257b3670ca3f44c5beb6", + "author_id": "541bf9b9ad60405c1f151a03", + "tab": "share", + "content": "content", + "title": "《一起学 Node.js》彻底重写完毕", + "last_reply_at": "2017-01-11T10:20:56.496Z", + "good": false, + "top": true, + "reply_count": 193, + "visit_count": 47633 } ``` @@ -72,9 +70,9 @@ CNode 社区现在 v1 版本的接口不是完全符合 RESTful 语义,在这 - 响应状态码:201 - 响应体: -``` +```json { - "topic_id": "57ea257b3670ca3f44c5beb6" + "topic_id": "57ea257b3670ca3f44c5beb6" } ``` @@ -86,26 +84,27 @@ CNode 社区现在 v1 版本的接口不是完全符合 RESTful 语义,在这 ### 错误处理 -在接口处理发生错误的时候,如果是客户端请求参数导致的错误,我们会返回 4xx 状态码,如果是服务端自身的处理逻辑错误,我们会返回 5xx 状态码。所有的异常对象都是对这个异常状态的描述,其中 error 字段是错误的描述,detail 字段(可选)是导致错误的详细原因。 +在接口处理发生错误时,如果是客户端请求参数导致的错误,我们返回 4xx 状态码;如果是服务端自身的处理逻辑错误,我们返回 5xx 状态码。所有异常对象都是对这个异常状态的描述,其中 `error` 字段是错误的描述,`detail` 字段(可选)是导致错误的详细原因。 例如,当客户端传递的参数异常时,我们可能返回一个响应,状态码为 422,返回响应体为: ```json { - "error": "Validation Failed", - "detail": [ - { "message": "required", "field": "title", "code": "missing_field" } - ] + "error": "Validation Failed", + "detail": [{ + "message": "required", + "field": "title", + "code": "missing_field" + }] } ``` - ## 实现 在约定好接口之后,我们可以开始动手实现了。 ### 初始化项目 -还是通过[快速入门](../intro/quickstart.md)章节介绍的 `npm` 来初始化我们的应用 +还是通过[快速入门](../intro/quickstart.md)章节介绍的 `npm` 来初始化我们的应用: ```bash $ mkdir cnode-api && cd cnode-api @@ -115,7 +114,7 @@ $ npm i ### 开启 validate 插件 -我们选择 [egg-validate](https://github.com/eggjs/egg-validate) 作为 validate 插件的示例。 +我们选择 [egg-validate](https://github.com/eggjs/egg-validate) 作为验证插件的示例。在 `config/plugin.js` 文件中开启它: ```js // config/plugin.js @@ -127,20 +126,20 @@ exports.validate = { ### 注册路由 -首先,我们先按照前面的设计来注册[路由](../basics/router.md),框架提供了一个便捷的方式来创建 RESTful 风格的路由,并将一个资源的接口映射到对应的 controller 文件。在 `app/router.js` 中: +首先,我们先按照前面的设计来注册[路由](../basics/router.md),框架提供了一个便捷的方式来创建 RESTful 风格的路由,并将一个资源的接口映射到对应的 controller 文件。在 `app/router.js` 中编写: ```js // app/router.js -module.exports = (app) => { +module.exports = app => { app.router.resources('topics', '/api/v2/topics', app.controller.topics); }; ``` -通过 `app.resources` 方法,我们将 topics 这个资源的增删改查接口映射到了 `app/controller/topics.js` 文件。 +通过 `app.resources` 方法,我们将 `topics` 这个资源的增删改查接口映射到了 `app/controller/topics.js` 文件。 ### controller 开发 -在 [controller](../basics/controller.md) 中,我们只需要实现 `app.resources` 约定的 [RESTful 风格的 URL 定义](../basics/router.md#restful-风格的-url-定义) 中我们需要提供的接口即可。例如我们来实现创建一个 topics 的接口: +在 [controller](../basics/controller.md) 中,我们只需要实现 `app.resources` 约定的 [RESTful 风格的 URL 定义](../basics/router.md#restful-风格的-url-定义) 中我们需要提供的接口即可。例如我们来实现创建一个 `topics` 的接口: ```js // app/controller/topics.js @@ -180,7 +179,7 @@ module.exports = TopicController; ### service 开发 -在 [service](../basics/service.md) 中,我们可以更加专注的编写实际生效的业务逻辑。 +在 [service](../basics/service.md) 中,我们可以更加专注地编写实际生效的业务逻辑。 ```js // app/service/topics.js @@ -209,10 +208,7 @@ class TopicService extends Service { // 封装统一的调用检查函数,可以在查询、创建和更新等 Service 中复用 checkSuccess(result) { if (result.status !== 200) { - const errorMsg = - result.data && result.data.error_msg - ? result.data.error_msg - : 'unknown error'; + const errorMsg = result.data && result.data.error_msg ? result.data.error_msg : 'unknown error'; this.ctx.throw(result.status, errorMsg); } if (!result.data.success) { @@ -225,19 +221,17 @@ class TopicService extends Service { module.exports = TopicService; ``` -在创建 topic 的 Service 开发完成之后,我们就从上往下的完成了一个接口的开发。 +在创建 `topic` 的 `Service` 开发完成之后,我们就从上往下的完成了一个接口### 统一错误处理 -### 统一错误处理 +正常的业务逻辑已经完成,但是异常我们还没有进行处理。在前面编写的代码中,Controller 和 Service 都可能抛出异常,这是我们推荐的编码方式,当发现客户端参数错误或调用后端服务异常时,通过抛出异常的方式来中断操作。 -正常的业务逻辑已经正常完成了,但是异常我们还没有进行处理。在前面编写的代码中,Controller 和 Service 都有可能抛出异常,这也是我们推荐的编码方式,当发现客户端参数传递错误或者调用后端服务异常时,通过抛出异常的方式来进行中断。 +- Controller 中通过 `this.ctx.validate()` 进行参数校验,校验失败时抛出异常。 +- Service 中调用了 `this.ctx.curl()` 方法访问 CNode 服务,可能会因网络问题等情况抛出服务端异常。 +- Service 中拿到 CNode 服务端返回的结果后,也可能会收到请求调用失败的返回结果,此时同样会抛出异常。 -- Controller 中 `this.ctx.validate()` 进行参数校验,失败抛出异常。 -- Service 中调用 `this.ctx.curl()` 方法访问 CNode 服务,可能由于网络问题等原因抛出服务端异常。 -- Service 中拿到 CNode 服务端返回的结果后,可能会收到请求调用失败的返回结果,此时也会抛出异常。 +尽管框架提供了默认的异常处理方式,但可能与我们之前接口的约定不一致,因此需要自己实现一个统一错误处理的中间件来处理异常。 -框架虽然提供了默认的异常处理,但是可能和我们在前面的接口约定不一致,因此我们需要自己实现一个统一错误处理的中间件来对错误进行处理。 - -在 `app/middleware` 目录下新建一个 `error_handler.js` 的文件来新建一个 [middleware](../basics/middleware.md) +在 `app/middleware` 目录下新建一个 `error_handler.js` 文件,创建一个中间件: ```js // app/middleware/error_handler.js @@ -246,17 +240,17 @@ module.exports = () => { try { await next(); } catch (err) { - // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 + // 所有的异常都会触发 app 上的一个 error 事件,框架会记录一条错误日志 ctx.app.emit('error', err, ctx); const status = err.status || 500; - // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息 + // 在生产环境中,500 错误的详细内容不返回给客户端,因为可能含有敏感信息 const error = status === 500 && ctx.app.config.env === 'prod' ? 'Internal Server Error' : err.message; - // 从 error 对象上读出各个属性,设置到响应中 + // 从 error 对象读出各属性,设置到响应中 ctx.body = { error }; if (status === 422) { ctx.body.detail = err.errors; @@ -267,27 +261,26 @@ module.exports = () => { }; ``` -通过这个中间件,我们可以捕获所有异常,并按照我们想要的格式封装了响应。将这个中间件通过配置文件(`config/config.default.js`)加载进来: +通过这个中间件,可以捕获所有异常,并以我们想要的格式组织响应内容。接下来需在配置文件 (`config/config.default.js`) 中加载这个中间件: ```js // config/config.default.js module.exports = { // 加载 errorHandler 中间件 middleware: ['errorHandler'], - // 只对 /api 前缀的 url 路径生效 + // 只对以 /api 为前缀的 URL 路径生效 errorHandler: { match: '/api', }, }; ``` - ## 测试 代码完成只是第一步,我们还需要给代码加上[单元测试](../core/unittest.md)。 ### Controller 测试 -我们先来编写 Controller 代码的单元测试。在写 Controller 单测的时候,我们可以适时的模拟 Service 层的实现,因为对 Controller 的单元测试而言,最重要的部分是测试自身的逻辑,而 Service 层按照约定的接口 mock 掉,Service 自身的逻辑可以让 Service 的单元测试来覆盖,这样我们开发的时候也可以分层进行开发测试。 +我们先来编写 Controller 代码的单元测试。在写 Controller 单测的时候,我们可以适时地模拟 Service 层的实现,因为对 Controller 的单元测试而言,最重要的部分是测试自身的逻辑,而 Service 层按照约定的接口模拟(mock)掉,Service 自身的逻辑可以让 Service 的单元测试来覆盖,这样我们开发的时候也可以分层进行开发测试。 ```js const { app, mock, assert } = require('egg-mock/bootstrap'); @@ -367,7 +360,7 @@ describe('test/app/service/topics.test.js', () => { it('should create success', async () => { // 不影响 CNode 的正常运行,我们可以将对 CNode 的调用按照接口约定模拟掉 - // app.mockHttpclient 方法可以便捷的对应用发起的 http 请求进行模拟 + // app.mockHttpclient 方法可以便捷地对应用发起的 http 请求进行模拟 app.mockHttpclient(`${ctx.service.topics.root}/topics`, 'POST', { data: { success: true, @@ -386,7 +379,7 @@ describe('test/app/service/topics.test.js', () => { }); ``` -上面对 Service 层的测试中,我们通过 egg-mock 提供的 `app.createContext()` 方法创建了一个 Context 对象,并直接调用 Context 上的 Service 方法进行测试,测试时可以通过 `app.mockHttpclient()` 方法模拟 HTTP 调用的响应,让我们剥离环境的影响而专注于 Service 自身逻辑的测试上。 +上面对 Service 层的测试中,我们通过 egg-mock 提供的 `app.mockContext()` 方法创建了一个 Context 对象,并直接调用 Context 上的 Service 方法进行测试,测试时可以通过 `app.mockHttpclient()` 方法模拟 HTTP 调用的响应,让我们剥离环境的影响而专注于 Service 自身逻辑的测试上。 --- diff --git a/site/docs/tutorials/sequelize.zh-CN.md b/site/docs/tutorials/sequelize.zh-CN.md index 3c3d4bca7d..180ada56b1 100644 --- a/site/docs/tutorials/sequelize.zh-CN.md +++ b/site/docs/tutorials/sequelize.zh-CN.md @@ -2,13 +2,13 @@ title: Sequelize --- -[前面的章节中](./mysql.md),我们介绍了如何在框架中通过 [egg-mysql] 插件来访问数据库。而在一些较为复杂的应用中,我们可能会需要一个 ORM 框架来帮助我们管理数据层的代码。而在 Node.js 社区中,[sequelize] 是一个广泛使用的 ORM 框架,它支持 MySQL、PostgreSQL、SQLite 和 MSSQL 等多个数据源。 +在前面的章节中,我们介绍了如何在框架中通过 [egg-mysql] 插件来访问数据库。在一些较为复杂的应用中,我们可能会需要一个 ORM 框架来帮助我们管理数据层的代码。在 Node.js 社区中,[sequelize] 是一个广泛使用的 ORM 框架,它支持 MySQL、PostgreSQL、SQLite 和 MSSQL 等多个数据源。 -本章节我们会通过开发一个对 MySQL 中 `users` 表的数据做 CURD 的例子来一步步介绍如何在 egg 项目中使用 sequelize。 +本章节,我们将通过开发一个对 MySQL 中 `users` 表的数据做 CURD 操作的例子,一步步介绍如何在 egg 项目中使用 sequelize。 ## 准备工作 -在这个例子中,我们会使用 sequelize 连接到 MySQL 数据源,因此在开始编写代码之前,我们需要先在本机上安装好 MySQL,如果是 MacOS,可以通过 homebrew 快速安装: +在这个例子中,我们将使用 sequelize 连接到 MySQL 数据源。因此,在开始编写代码之前,我们需要先在本机上安装好 MySQL。如果是 MacOS,可以通过 homebrew 快速安装: ```bash brew install mysql @@ -17,7 +17,7 @@ brew services start mysql ## 初始化项目 -通过 `npm` 初始化一个项目: +通过 `npm` 初始化一个项目: ```bash $ mkdir sequelize-project && cd sequelize-project @@ -53,7 +53,7 @@ exports.sequelize = { }; ``` -我们可以在不同的环境配置中配置不同的数据源地址,用于区分不同环境使用的数据库,例如我们可以新建一个 `config/config.unittest.js` 配置文件,写入如下配置,将单测时连接的数据库指向 `egg-sequelize-doc-unittest`。 +我们可以在不同的环境配置中配置不同的数据源地址,以区分不同环境使用的数据库。例如,我们可以新建一个 `config/config.unittest.js` 配置文件,写入以下配置,将单元测试时连接的数据库指向 `egg-sequelize-doc-unittest`。 ```js exports.sequelize = { @@ -64,11 +64,10 @@ exports.sequelize = { }; ``` -完成上面的配置之后,一个使用 sequelize 的项目就初始化完成了。[egg-sequelize] 和 [sequelize] 还支持更多的配置项,可以在他们的文档中找到。 - +完成上述配置之后,一个使用 sequelize 的项目就初始化完成了。[egg-sequelize] 和 [sequelize] 还支持更多的配置项,你可以在他们的文档中找到。 ## 初始化数据库和 Migrations -接下来我们先暂时离开 egg 项目的代码,设计和初始化一下我们的数据库。首先我们通过 mysql 命令在本地快速创建开发和测试要用到的两个 database: +接下来我们先暂时离开 egg 项目的代码,设计和初始化一下我们的数据库。首先我们通过 MySQL 命令在本地快速创建开发和测试要用到的两个数据库: ```bash mysql -u root -e 'CREATE DATABASE IF NOT EXISTS `egg-sequelize-doc-default`;' @@ -79,18 +78,18 @@ mysql -u root -e 'CREATE DATABASE IF NOT EXISTS `egg-sequelize-doc-unittest`;' ```sql CREATE TABLE `users` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key', - `name` varchar(30) DEFAULT NULL COMMENT 'user name', - `age` int(11) DEFAULT NULL COMMENT 'user age', - `created_at` datetime DEFAULT NULL COMMENT 'created time', - `updated_at` datetime DEFAULT NULL COMMENT 'updated time', + `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key', + `name` VARCHAR(30) DEFAULT NULL COMMENT 'user name', + `age` INT(11) DEFAULT NULL COMMENT 'user age', + `created_at` DATETIME DEFAULT NULL COMMENT 'created time', + `updated_at` DATETIME DEFAULT NULL COMMENT 'updated time', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='user'; ``` -我们可以直接通过 mysql 命令将表直接建好,但是这并不是一个对多人协作非常友好的开发模式。在项目的演进过程中,每一个迭代都有可能对数据库数据结构做变更,怎样跟踪每一个迭代的数据变更,并在不同的环境(开发、测试、CI)和迭代切换中,快速变更数据结构呢?这时候我们就需要 [Migrations] 来帮我们管理数据结构的变更了。 +我们可以直接通过 MySQL 命令将表直接建好,但是这并不是一个对多人协作非常友好的开发模式。在项目的演进过程中,每一个迭代都有可能对数据库数据结构做变更,怎样跟踪每一个迭代的数据变更,并在不同的环境(开发、测试、CI)和迭代切换中,快速变更数据结构呢?这时候我们就需要 `Migrations` 来帮我们管理数据结构的变更了。 -sequelize 提供了 [sequelize-cli] 工具来实现 [Migrations],我们也可以在 egg 项目中引入 sequelize-cli。 +sequelize 提供了 `sequelize-cli` 工具来实现 `Migrations`,我们也可以在 egg 项目中引入 sequelize-cli。 - 安装 sequelize-cli @@ -106,10 +105,10 @@ npm install --save-dev sequelize-cli const path = require('path'); module.exports = { - config: path.join(__dirname, 'database/config.json'), - 'migrations-path': path.join(__dirname, 'database/migrations'), - 'seeders-path': path.join(__dirname, 'database/seeders'), - 'models-path': path.join(__dirname, 'app/model'), + "config": path.join(__dirname, 'database/config.json'), + "migrations-path": path.join(__dirname, 'database/migrations'), + "seeders-path": path.join(__dirname, 'database/seeders'), + "models-path": path.join(__dirname, 'app/model'), }; ``` @@ -122,7 +121,7 @@ npx sequelize init:migrations 执行完后会生成 `database/config.json` 文件和 `database/migrations` 目录,我们修改一下 `database/config.json` 中的内容,将其改成我们项目中使用的数据库配置: -``` +```json { "development": { "username": "root", @@ -141,13 +140,13 @@ npx sequelize init:migrations } ``` -此时 sequelize-cli 和相关的配置也都初始化好了,我们可以开始编写项目的第一个 Migration 文件来创建我们的一个 users 表了。 +此时 sequelize-cli 和相关的配置也都初始化好了,我们可以开始编写项目的第一个 Migration 文件来创建我们的一个 `users` 表了。 ```bash npx sequelize migration:generate --name=init-users ``` -执行完后会在 `database/migrations` 目录下生成一个 migration 文件(`${timestamp}-init-users.js`),我们修改它来处理初始化 `users` 表: +执行完后会在 `database/migrations` 目录下生成一个 migration 文件(`${timestamp}-init-users.js`),我们修改它来处理初始化 `users` 表: ```js 'use strict'; @@ -176,22 +175,25 @@ module.exports = { ```bash # 升级数据库 npx sequelize db:migrate + # 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更 + # npx sequelize db:migrate:undo + # 可以通过 `db:migrate:undo:all` 回退到初始状态 -# npx sequelize db:migrate:undo:all ``` +# NPX Sequelize DB:Migrate:Undo:All 执行之后,我们的数据库初始化就完成了。 ## 编写代码 -现在终于可以开始编写代码实现业务逻辑了,首先我们来在 `app/model/` 目录下编写 user 这个 Model: +现在终于可以开始编写代码实现业务逻辑了。首先我们来在 `app/model/` 目录下编写 `user` 这个 Model: ```js 'use strict'; -module.exports = (app) => { +module.exports = app => { const { STRING, INTEGER, DATE } = app.Sequelize; const User = app.model.define('user', { @@ -206,7 +208,7 @@ module.exports = (app) => { }; ``` -这个 Model 就可以在 Controller 和 Service 中通过 `app.model.User` 或者 `ctx.model.User` 访问到了,例如我们编写 `app/controller/users.js`: +这个 Model 就可以在 Controller 和 Service 中通过 `app.model.User` 或者 `ctx.model.User` 访问到了。例如,我们编写 `app/controller/users.js`: ```js // app/controller/users.js @@ -272,18 +274,17 @@ class UserController extends Controller { module.exports = UserController; ``` -最后我们将这个 controller 挂载到路由上: +最后,我们将这个 controller 挂载到路由上: ```js // app/router.js -module.exports = (app) => { +module.exports = app => { const { router, controller } = app; router.resources('users', '/users', controller.users); }; ``` -针对 `users` 表的 CURD 操作的接口就开发完了,为了验证代码逻辑是否正确,我们接下来需要编写单元测试来验证。 - +针对 `users` 表的 CURD 操作的接口就开发完了。为了验证代码逻辑是否正确,我们接下来需要编写单元测试来验证。 ## 单元测试 在编写测试之前,由于在前面的 egg 配置中,我们将单元测试环境和开发环境指向了不同的数据库,因此需要通过 Migrations 来初始化测试数据库的数据结构: @@ -296,42 +297,44 @@ NODE_ENV=test npx sequelize db:migrate:up - 安装 `factory-girl` 依赖 -```bash -npm install --save-dev factory-girl -``` + ```bash + npm install --save-dev factory-girl + ``` -- 定义 factory-girl 的数据模型到 `test/factories.js` 中 +- 定义 `factory-girl` 的数据模型到 `test/factories.js` 中 -```js -// test/factories.js -'use strict'; + ```js + // test/factories.js + 'use strict'; -const { factory } = require('factory-girl'); + const { factory } = require('factory-girl'); -module.exports = (app) => { - // 可以通过 app.factory 访问 factory 实例 - app.factory = factory; + module.exports = (app) => { + // 可以通过 app.factory 访问 factory 实例 + app.factory = factory; - // 定义 user 和默认数据 - factory.define('user', app.model.User, { - name: factory.sequence('User.name', (n) => `name_${n}`), - age: 18, - }); -}; -``` + // 定义 user 模型和默认数据 + factory.define('user', app.model.User, { + name: factory.sequence('User.name', (n) => `name_${n}`), + age: 18, + }); + }; + ``` - 初始化文件 `test/.setup.js`,引入 factory,并确保测试执行完后清理数据,避免被影响。 -```js -const { app } = require('egg-mock/bootstrap'); -const factories = require('./factories'); + ```js + const { app } = require('egg-mock/bootstrap'); + const factories = require('./factories'); -before(() => factories(app)); -afterEach(async () => { - // clear database after each test case - await Promise.all([app.model.User.destroy({ truncate: true, force: true })]); -}); -``` + before(() => factories(app)); + afterEach(async () => { + // 在每个测试案例执行完后清理数据库 + await Promise.all([ + app.model.User.destroy({ truncate: true, force: true }), + ]); + }); + ``` 接下来我们就可以开始编写真正的测试用例了: @@ -342,7 +345,7 @@ const { assert, app } = require('egg-mock/bootstrap'); describe('test/app/controller/users.test.js', () => { describe('GET /users', () => { it('should work', async () => { - // 通过 factory-girl 快速创建 user 对象到数据库中 + // 通过 factory-girl 快速创建用户对象到数据库中 await app.factory.createMany('user', 3); const res = await app.httpRequest().get('/users?limit=2'); assert(res.status === 200); @@ -391,7 +394,7 @@ describe('test/app/controller/users.test.js', () => { 最后,如果我们需要在 CI 中运行单元测试,需要确保在执行测试代码之前,执行一次 migrate 确保数据结构更新,例如我们在 `package.json` 中声明 `scripts.ci` 来在 CI 环境下执行单元测试: -```js +```json { "scripts": { "ci": "eslint . && NODE_ENV=test npx sequelize db:migrate && egg-bin cov" @@ -405,7 +408,7 @@ describe('test/app/controller/users.test.js', () => { ## 脚手架 -我们也提供了 sequelize 的脚手架,集成了文档中提供的 [egg-sequelize], [sequelize-cli] 与 [factory-girl] 等模块。可以通过 `npm init egg --type=sequelize` 来基于它快速初始化一个新的应用。 +我们也提供了 sequelize 的脚手架,集成了文档中提供的 [egg-sequelize]、[sequelize-cli] 与 [factory-girl] 等模块。你可以通过 `npm init egg --type=sequelize` 来基于它快速初始化一个新的应用。 [mysql2]: https://github.com/sidorares/node-mysql2 [sequelize]: http://docs.sequelizejs.com/ diff --git a/site/docs/tutorials/socketio.zh-CN.md b/site/docs/tutorials/socketio.zh-CN.md index ff412a0ca5..ae02aba33c 100644 --- a/site/docs/tutorials/socketio.zh-CN.md +++ b/site/docs/tutorials/socketio.zh-CN.md @@ -2,16 +2,16 @@ title: Socket.IO --- -**Socket.IO** 是一个基于 Node.js 的实时应用程序框架,在即时通讯、通知与消息推送,实时分析等场景中有较为广泛的应用。 +**Socket.IO** 是一个基于 Node.js 的实时应用程序框架。在即时通讯、通知与消息推送,实时分析等场景中有较为广泛的应用。 -WebSocket 的产生源于 Web 开发中日益增长的实时通信需求,对比基于 http 的轮询方式,它大大节省了网络带宽,同时也降低了服务器的性能消耗; [socket.io] 支持 websocket、polling 两种数据传输方式以兼容浏览器不支持 WebSocket 场景下的通信需求。 +WebSocket 的产生源于 Web 开发中日益增长的实时通信需求。对比传统的基于 http 的轮询方式,它大大节约了网络带宽,同时也降低了服务器的性能消耗。`socket.io` 支持 websocket 和 polling 两种数据传输方式,以兼容不支持 WebSocket 的浏览器。 -框架提供了 [egg-socket.io] 插件,增加了以下开发规约: +框架提供了 `egg-socket.io` 插件,增加了以下开发规约: -- namespace: 通过配置的方式定义 namespace(命名空间) -- middleware: 对每一次 socket 连接的建立/断开、每一次消息/数据传递进行预处理 -- controller: 响应 socket.io 的 event 事件 -- router: 统一了 socket.io 的 event 与 框架路由的处理配置方式 +- namespace:通过配置的方式定义 namespace(命名空间)。 +- middleware:对每一次 socket 连接的建立/断开、每一次消息/数据传递进行预处理。 +- controller:响应 `socket.io` 的 event 事件。 +- router:统一了 `socket.io` 的 event 与框架路由的处理配置方式。 ## 安装 egg-socket.io @@ -23,154 +23,153 @@ $ npm i egg-socket.io --save **开启插件:** -```js +```javascript // {app_root}/config/plugin.js exports.io = { - enable: true, - package: 'egg-socket.io', + enable: true, + package: 'egg-socket.io', }; ``` ### 配置 -```js +```javascript // {app_root}/config/config.${env}.js exports.io = { - init: {}, // passed to engine.io - namespace: { - '/': { - connectionMiddleware: [], - packetMiddleware: [], - }, - '/example': { - connectionMiddleware: [], - packetMiddleware: [], + init: {}, // 传递给 engine.io + namespace: { + '/': { + connectionMiddleware: [], + packetMiddleware: [], + }, + '/example': { + connectionMiddleware: [], + packetMiddleware: [], + }, }, - }, }; ``` -> 命名空间为 `/` 与 `/example`, 不是 `example` +> 命名空间为 `/` 与 `/example`,而不是 `example`。 #### uws -**Egg Socket 内部默认使用 `ws` 引擎,[uws](https://www.npmjs.com/package/uws) 因为[某些原因](https://github.com/socketio/socket.io/issues/3319)被废止。** +**Egg Socket 内部默认使用 `ws` 引擎。`uws` 因为[某些原因](https://github.com/socketio/socket.io/issues/3319)被废止了。** -如坚持需要使用,请按照以下配置即可: +如果坚持要使用 `uws`,请按照以下配置: -```js +```javascript // {app_root}/config/config.${env}.js exports.io = { - init: { wsEngine: 'uws' }, // default: ws + init: { wsEngine: 'uws' }, // 默认是 ws }; ``` #### redis -[egg-socket.io] 内置了 `socket.io-redis`,在 cluster 模式下,使用 redis 可以较为简单的实现 clients/rooms 等信息共享 +`egg-socket.io` 内置了 `socket.io-redis`。在 cluster 模式下,使用 redis 可以简单地实现 clients/rooms 等信息共享。 -```js +```javascript // {app_root}/config/config.${env}.js exports.io = { - redis: { - host: { redis server host }, - port: { redis server port }, - auth_pass: { redis server password }, - db: 0, - }, + redis: { + host: { redis server host }, + port: { redis server port }, + auth_pass: { redis server password }, + db: 0, + }, }; ``` -> 开启 `redis` 后,程序在启动时会尝试连接到 redis 服务器 -> 此处 `redis` 仅用于存储连接实例信息,参见 [#server.adapter](https://socket.io/docs/server-api/#server-adapter-value) +> 开启 `redis` 后,程序在启动时会尝试连接到 redis 服务器。此处的 `redis` 仅用于存储连接实例信息,详见 [#server.adapter](https://socket.io/docs/server-api/#server-adapter-value)。 **注意:** -如果项目中同时使用了 `egg-redis`, 请单独配置,不可共用。 +如果项目中同时使用了 `egg-redis`,请分别配置,不可共用。 ### 部署 -框架是以 Cluster 方式启动的,而 socket.io 协议实现需要 sticky 特性支持,否则在多进程模式下无法正常工作。 +由于框架是以 Cluster 方式启动的,而 `socket.io` 协议实现需要 sticky 特性支持,在多进程模式下才能正常工作。 -由于 [socket.io] 的设计,在多进程中服务器必须在 `sticky` 模式下工作,故需要给 startCluster 传递 sticky 参数。 +由于 `socket.io` 的设计,多进程服务器必须在 `sticky` 模式下工作。因此,需要给 startCluster 传递 sticky 参数。 -修改 `package.json` 中 `npm scripts` 脚本: +修改 `package.json` 中的 npm scripts 脚本: -``` +```json { - "scripts": { - "dev": "egg-bin dev --sticky", - "start": "egg-scripts start --sticky" - } + "scripts": { + "dev": "egg-bin dev --sticky", + "start": "egg-scripts start --sticky" + } } ``` **Nginx 配置** -``` +```nginx location / { - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $host; - proxy_pass http://127.0.0.1:7001; - - # http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_bind - # proxy_bind $remote_addr transparent; -} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_pass http://127.0.0.1:7001; + + # http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_bind +``` +# proxy_bind $remote_addr transparent; ``` -## 使用 egg-socket.io +## 使用 `egg-socket.io` 开启 [egg-socket.io] 的项目目录结构如下: ``` chat ├── app -│   ├── extend -│   │   └── helper.js -│   ├── io -│   │   ├── controller -│   │   │   └── default.js -│   │   └── middleware -│   │   ├── connection.js -│   │   └── packet.js -│   └── router.js +│ ├── extend +│ │ └── helper.js +│ ├── io +│ │ ├── controller +│ │ │ └── default.js +│ │ └── middleware +│ │ ├── connection.js +│ │ └── packet.js +│ └── router.js ├── config └── package.json ``` -> 注意:对应的文件都在 app/io 目录下 +> 注意:对应的文件都在 `app/io` 目录下。 ### Middleware -中间件有如下两种场景: +中间件有以下两种场景: - Connection - Packet -其配置于各个命名空间下,根据上述两种场景分别发生作用。 +它们的配置位于各个命名空间下,根据上述两种场景分别起作用。 **注意:** -如果我们启用了框架中间件,则会发现项目中有以下目录: +如果启用了框架中间件,会在项目中发现以下目录: -- `app/middleware`:框架中间件 -- `app/io/middleware`:插件中间件 +- `app/middleware`:框架中间件。 +- `app/io/middleware`:插件中间件。 -区别: +两者的区别: -- 框架中间件基于 http 模型设计,处理 http 请求。 -- 插件中间件基于 socket 模型设计,处理 socket.io 请求。 +- 框架中间件基于 HTTP 模型设计,处理 HTTP 请求。 +- 插件中间件基于 socket 模型设计,处理 `socket.io` 请求。 -虽然框架通过插件尽量统一了它们的风格,但务必注意,它们的使用场景是不一样的。详情参见 issue:[#1416](https://github.com/eggjs/egg/issues/1416) +虽然框架通过插件尽量统一了它们的风格,但必须注意,它们的使用场景是不同的。详情请参见 issue [#1416](https://github.com/eggjs/egg/issues/1416)。 #### Connection -在每一个客户端连接或者退出时发生作用,故而我们通常在这一步进行授权认证,对认证失败的客户端做出相应的处理 +每个客户端连接或退出时起作用。因此,通常在这一步进行授权认证,并对认证失败的客户端进行处理。 ```js // {app_root}/app/io/middleware/connection.js -module.exports = (app) => { +module.exports = app => { return async (ctx, next) => { ctx.socket.emit('res', 'connected!'); await next(); @@ -186,17 +185,17 @@ module.exports = (app) => { const tick = (id, msg) => { logger.debug('#tick', id, msg); socket.emit(id, msg); - app.io.of('/').adapter.remoteDisconnect(id, true, (err) => { + app.io.of('/').adapter.remoteDisconnect(id, true, err => { logger.error(err); }); }; ``` -同时,针对当前的连接也可以简单处理: +针对当前连接的简单处理示例: ```js // {app_root}/app/io/middleware/connection.js -module.exports = (app) => { +module.exports = app => { return async (ctx, next) => { if (true) { ctx.socket.disconnect(); @@ -210,11 +209,11 @@ module.exports = (app) => { #### Packet -作用于每一个数据包(每一条消息);在生产环境中,通常用于对消息做预处理,又或者是对加密消息的解密等操作 +每条数据包(消息)都会执行该中间件。在生产环境中,通常用于对消息做预处理,或者对加密消息进行解密等操作。 ```js // {app_root}/app/io/middleware/packet.js -module.exports = (app) => { +module.exports = app => { return async (ctx, next) => { ctx.socket.emit('res', 'packet received!'); console.log('packet:', ctx.packet); @@ -222,18 +221,17 @@ module.exports = (app) => { }; }; ``` - ### Controller -Controller 对客户端发送的 event 进行处理;由于其继承于 `egg.Contoller`, 拥有如下成员对象: +Controller 对客户端发送的 event 进行处理;由于其继承自 `egg.Controller`,拥有以下成员对象: -- ctx -- app -- service -- config -- logger +- `ctx` +- `app` +- `service` +- `config` +- `logger` -> 详情参考 [Controller](../basics/controller.md) 文档 +详情参考 [Controller](../basics/controller.md) 文档。 ```js // {app_root}/app/io/controller/default.js @@ -243,35 +241,28 @@ const Controller = require('egg').Controller; class DefaultController extends Controller { async ping() { - const { ctx, app } = this; + const { ctx } = this; const message = ctx.args[0]; await ctx.socket.emit('res', `Hi! I've got your message: ${message}`); } } module.exports = DefaultController; - -// or async functions - -exports.ping = async function () { - const message = this.args[0]; - await this.socket.emit('res', `Hi! I've got your message: ${message}`); -}; ``` ### Router -路由负责将 socket 连接的不同 events 分发到对应的 controller,框架统一了其使用方式 +路由负责将 socket 连接的不同 events 分发到对应的 controller,框架统一了其使用方式。 ```js // {app_root}/app/router.js -module.exports = (app) => { +module.exports = app => { const { router, controller, io } = app; - + // default router.get('/', controller.home.index); - + // socket.io io.of('/').route('server', io.controller.home.server); }; @@ -279,24 +270,24 @@ module.exports = (app) => { **注意:** -nsp 有如下的系统事件: +`nsp` 有如下的系统事件: -- `disconnecting` doing the disconnect -- `disconnect` connection has disconnected. -- `error` Error occurred +- `disconnecting`:正在断开连接。 +- `disconnect`:连接已断开。 +- `error`:发生错误。 ### Namespace/Room #### Namespace (nsp) -namespace 通常意味分配到不同的接入点或者路径,如果客户端没有指定 nsp,则默认分配到 "/" 这个默认的命名空间。 +`namespace` 通常意味着分配到不同的接入点或者路径,如果客户端没有指定 `nsp`,则默认分配到 "/" 这个默认的命名空间。 -在 socket.io 中我们通过 `of` 来划分命名空间;鉴于 nsp 通常是预定义且相对固定的存在,框架将其进行了封装,采用配置的方式来划分不同的命名空间。 +在 socket.io 中我们通过 `of` 来划分命名空间;鉴于 `nsp` 通常是预定义且相对固定的存在,框架将其进行了封装,采用配置的方式来划分不同的命名空间。 ```js // socket.io -var nsp = io.of('/my-namespace'); -nsp.on('connection', function (socket) { +const nsp = io.of('/my-namespace'); +nsp.on('connection', socket => { console.log('someone connected'); }); nsp.emit('hi', 'everyone!'); @@ -314,12 +305,12 @@ exports.io = { #### Room -room 存在于 nsp 中,通过 join/leave 方法来加入或者离开; 框架中使用方法相同; +`room` 存在于 `nsp` 中,通过 `join`/`leave` 方法来加入或者离开;框架中使用方法相同。 ```js const room = 'default_room'; -module.exports = (app) => { +module.exports = app => { return async (ctx, next) => { ctx.socket.join(room); ctx.app.io @@ -332,93 +323,91 @@ module.exports = (app) => { }; ``` -**注意:** 每一个 socket 连接都会拥有一个随机且不可预测的唯一 id `Socket#id`,并且会自动加入到以这个 `id` 命名的 room 中 - +**注意:** 每一个 socket 连接都会拥有一个随机且不可预测的唯一 id `Socket#id`,并且会自动加入到以这个 `id` 命名的 `room` 中。 ## 实例 -这里我们使用 [egg-socket.io] 来做一个支持 p2p 聊天的小例子 +这里我们使用 [egg-socket.io](https://github.com/eggjs/egg-socket.io) 来做一个支持 P2P 聊天的小例子。 -### client +### 客户端 -UI 相关的内容不重复写了,通过 window.socket 调用即可 +UI 相关的内容不重复编写,通过 `window.socket` 调用即可。 ```js -// browser +// 浏览器 const log = console.log; window.onload = function () { - // init - const socket = io('/', { - // 实际使用中可以在这里传递参数 - query: { - room: 'demo', - userId: `client_${Math.random()}`, - }, - - transports: ['websocket'], - }); + // 初始化 + const socket = io('/', { + // 实际使用中可以在这里传递参数 + query: { + room: 'demo', + userId: `client_${Math.random()}`, // 传递了 room 和 userId 两个参数 + }, + + transports: ['websocket'], + }); - socket.on('connect', () => { - const id = socket.id; + socket.on('connect', () => { + const id = socket.id; - log('#connect,', id, socket); + log('#connect,', id, socket); - // 监听自身 id 以实现 p2p 通讯 - socket.on(id, (msg) => { - log('#receive,', msg); + // 监听自身 id,以实现 P2P 通讯 + socket.on(id, (msg) => { + log('#receive,', msg); + }); }); - }); - // 接收在线用户信息 - socket.on('online', (msg) => { - log('#online,', msg); - }); + // 接收在线用户信息 + socket.on('online', (msg) => { + log('#online,', msg); + }); - // 系统事件 - socket.on('disconnect', (msg) => { - log('#disconnect', msg); - }); + // 系统事件 + socket.on('disconnect', (msg) => { + log('#disconnect', msg); + }); - socket.on('disconnecting', () => { - log('#disconnecting'); - }); + socket.on('disconnecting', () => { + log('#disconnecting'); + }); - socket.on('error', () => { - log('#error'); - }); + socket.on('error', () => { + log('#error'); + }); - window.socket = socket; + window.socket = socket; }; ``` #### 微信小程序 -微信小程序提供的 API 为 WebSocket ,而 socket.io 是 Websocket 的上层封装,故我们无法直接用小程序的 API 连接,可以使用类似 [weapp.socket.io](https://github.com/wxsocketio/weapp.socket.io) 的库来适配。 +微信小程序提供的 API 为 `WebSocket` ,因为 `socket.io` 是 `WebSocket` 的上层封装,所以我们无法直接使用小程序的 API 连接。可以使用类似 [weapp.socket.io](https://github.com/wxsocketio/weapp.socket.io) 的库适配。 示例代码如下: ```js // 小程序端示例代码 -const io = require('./yout_path/weapp.socket.io.js'); +const io = require('./your_path/weapp.socket.io.js'); // 请替换成实际路径 const socket = io('http://localhost:8000'); socket.on('connect', function () { - console.log('connected'); + console.log('connected'); }); socket.on('news', (d) => { - console.log('received news: ', d); + console.log('received news:', d); }); socket.emit('news', { - title: 'this is a news', + title: 'this is a news', }); ``` - ### server -以下是 demo 的部分代码并解释了各个方法的作用 +以下是 `demo` 的部分代码,并解释了各个方法的作用。 #### config @@ -428,15 +417,15 @@ exports.io = { namespace: { '/': { connectionMiddleware: ['auth'], - packetMiddleware: [], // 针对消息的处理暂时不实现 - }, + packetMiddleware: [] // 针对消息的处理暂时不实现 + } }, // cluster 模式下,通过 redis 实现数据共享 redis: { host: '127.0.0.1', - port: 6379, - }, + port: 6379 + } }; // 可选 @@ -445,36 +434,30 @@ exports.redis = { port: 6379, host: '127.0.0.1', password: '', - db: 0, - }, + db: 0 + } }; ``` #### helper -框架扩展用于封装数据格式 +框架扩展用于封装数据格式。 ```js // {app_root}/app/extend/helper.js module.exports = { parseMsg(action, payload = {}, metadata = {}) { - const meta = Object.assign( - {}, - { - timestamp: Date.now(), - }, - metadata, - ); + const meta = Object.assign({}, { timestamp: Date.now() }, metadata); return { meta, data: { action, - payload, - }, + payload + } }; - }, + } }; ``` @@ -483,20 +466,19 @@ Format: ```js { data: { - action: 'exchange', // 'deny' || 'exchange' || 'broadcast' - payload: {}, + action: 'exchange', // 'deny' || 'exchange' || 'broadcast' + payload: {} }, - meta:{ + meta: { timestamp: 1512116201597, client: 'nNx88r1c5WuHf9XuAAAB', target: 'nNx88r1c5WuHf9XuAAAB' - }, + } } ``` +#### 中间件 -#### middleware - -[egg-socket.io] 中间件负责 socket 连接的处理 +[egg-socket.io] 中间件负责处理 socket 连接。 ```js // {app_root}/app/io/middleware/auth.js @@ -519,34 +501,35 @@ module.exports = () => { const tick = (id, msg) => { logger.debug('#tick', id, msg); - // 踢出用户前发送消息 + // 踢出用户前发送信息 socket.emit(id, helper.parseMsg('deny', msg)); - // 调用 adapter 方法踢出用户,客户端触发 disconnect 事件 + // 调用 adapter 方法踢出用户,客户端会触发 disconnect 事件 nsp.adapter.remoteDisconnect(id, true, (err) => { logger.error(err); }); }; // 检查房间是否存在,不存在则踢出用户 - // 备注:此处 app.redis 与插件无关,可用其他存储代替 + // 注:此处 app.redis 与插件无关,可用其他存储替代 const hasRoom = await app.redis.get(`${PREFIX}:${room}`); logger.debug('#has_exist', hasRoom); + // 若房间不存在 if (!hasRoom) { tick(id, { type: 'deleted', - message: 'deleted, room has been deleted.', + message: 'deleted, room has been deleted.' }); return; } - // 用户加入 + // 用户加入房间 logger.debug('#join', room); socket.join(room); - // 在线列表 + // 获取在线列表 nsp.adapter.clients(rooms, (err, clients) => { logger.debug('#online_join', clients); @@ -555,42 +538,34 @@ module.exports = () => { clients, action: 'join', target: 'participator', - message: `User(${id}) joined.`, + message: `User(${id}) joined.` }); }); await next(); - // 用户离开 + // 用户离开房间 logger.debug('#leave', room); - // 在线列表 + // 获取在线列表 nsp.adapter.clients(rooms, (err, clients) => { logger.debug('#online_leave', clients); - // 获取 client 信息 - // const clientsDetail = {}; - // clients.forEach(client => { - // const _client = app.io.sockets.sockets[client]; - // const _query = _client.handshake.query; - // clientsDetail[client] = _query; - // }); - // 更新在线用户列表 nsp.to(room).emit('online', { clients, action: 'leave', target: 'participator', - message: `User(${id}) leaved.`, + message: `User(${id}) leaved.` }); }); }; }; ``` -#### controller +#### 控制器 -P2P 通信,通过 exchange 进行数据交换 +P2P 通信,通过 exchange 方法实现数据交换。 ```js // {app_root}/app/io/controller/nsp.js @@ -617,7 +592,6 @@ class NspController extends Controller { module.exports = NspController; ``` - #### router ```js @@ -631,7 +605,7 @@ module.exports = (app) => { }; ``` -开两个 tab 页面,并调出控制台: +打开两个 tab 页面,并调出控制台: ```js socket.emit('exchange', { @@ -644,13 +618,15 @@ socket.emit('exchange', { ![](https://raw.githubusercontent.com/eggjs/egg/master/docs/assets/socketio-console.png) + ## 参考链接 -- [socket.io] -- [egg-socket.io] -- [egg-socket.io example](https://github.com/eggjs/egg-socket.io/tree/master/example) -- [egg-socket.io demo](https://github.com/eggjs-community/demo-egg-socket.io) -- [nginx proxy_bind](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_bind) +- [socket.io](https://socket.io) +- [egg-socket.io](https://github.com/eggjs/egg-socket.io) +- [egg-socket.io 示例](https://github.com/eggjs/egg-socket.io/tree/master/example)(egg-socket.io example) +- [egg-socket.io 演示](https://github.com/eggjs-community/demo-egg-socket.io)(egg-socket.io demo) +- [nginx 代理绑定](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_bind)(nginx proxy_bind) + [socket.io]: https://socket.io [egg-socket.io]: https://github.com/eggjs/egg-socket.io diff --git a/site/docs/tutorials/typescript.zh-CN.md b/site/docs/tutorials/typescript.zh-CN.md index fa3be83df4..27d39dae1f 100644 --- a/site/docs/tutorials/typescript.zh-CN.md +++ b/site/docs/tutorials/typescript.zh-CN.md @@ -2,29 +2,29 @@ title: TypeScript --- -> [TypeScript](https://www.typescriptlang.org/) 是 JavaScript 类型的超集,它可以编译成纯 JavaScript。 +> [TypeScript](https://www.typescriptlang.org/) 是 JavaScript 的一个类型超集,它可以被编译成纯 JavaScript。 -TypeScript 的静态类型检查,智能提示,IDE 友好性等特性,对于大规模企业级应用,是非常的有价值的。详见:[TypeScript 体系调研报告](https://juejin.im/post/59c46bc86fb9a00a4636f939) 。 +TypeScript 提供的静态类型检查、智能提示和 IDE 友好性等特性,对于大规模企业级应用来说,具有极高的价值。有关详细信息,请参见:[TypeScript 体系调研报告](https://juejin.im/post/59c46bc86fb9a00a4636f939)。 -然而,此前使用 TypeScript 开发 Egg ,会遇到一些影响 **开发者体验** 问题: +然而,在之前使用 TypeScript 开发 Egg 应用时,会遇到一些影响**开发者体验**的问题: -- Egg 最精髓的 Loader 自动加载机制,导致 TS 无法静态分析出部分依赖。 -- Config 自动合并机制下,如何在 `config.{env}.js` 里面修改插件提供的配置时,能校验并智能提示? -- 开发期需要独立开一个 `tsc -w` 独立进程来构建代码,带来临时文件位置纠结以及 `npm scripts` 复杂化。 -- 单元测试,覆盖率测试,线上错误堆栈如何指向 TS 源文件,而不是编译后的 js 文件。 +- Egg 特有的 Loader 动态加载机制,使得 TypeScript 无法进行某些依赖的静态分析。 +- 在自动合并配置的机制中,如何在 `config.{env}.js` 中修改插件提供的配置,同时能够进行校验并智能提示? +- 开发期间需要启动一个单独的 `tsc -w` 进程来构建代码,这将导致临时文件位置的不确定性以及 `npm scripts` 的复杂性。 +- 单元测试、覆盖率测试以及线上错误的堆栈如何指向 TypeScript 源文件,而非编译后的 JavaScript 文件。 -本文主要阐述: +本文主要介绍: -- **应用层 TS 开发规范** -- **我们在工具链方面的支持,是如何来解决上述问题,让开发者几乎无感知并保持一致性。** +- **应用层 TypeScript 开发规范** +- **我们在工具链方面的支持,以解决上述问题,让开发者基本无感知的同时,也保持了一致性的体验。** -具体的折腾过程参见:[[RFC] TypeScript tool support](https://github.com/eggjs/egg/issues/2272) +关于具体开发过程的详细信息,请参见:[[RFC] TypeScript tool support](https://github.com/eggjs/egg/issues/2272)。 --- ## 快速入门 -通过骨架快速初始化: +通过骨架快速初始化一个项目: ```bash $ mkdir showcase && cd showcase @@ -33,7 +33,7 @@ $ npm i $ npm run dev ``` -上述骨架会生成一个极简版的示例,更完整的示例参见:[eggjs/examples/hackernews-async-ts](https://github.com/eggjs/examples/tree/master/hackernews-async-ts) +上面的骨架会生成一个极简版的示例,更完整的示例请参见:[eggjs/examples/hackernews-async-ts](https://github.com/eggjs/examples/tree/master/hackernews-async-ts) ![tegg.gif](https://user-images.githubusercontent.com/227713/38358019-bf7890fa-38f6-11e8-8955-ea072ac6dc8c.gif) @@ -41,17 +41,17 @@ $ npm run dev ## 目录规范 -**一些约束:** +**约束条件:** -- Egg 目前没有计划使用 TS 重写。 -- Egg 以及它对应的插件,会提供对应的 `index.d.ts` 文件方便开发者使用。 -- TypeScript 只是其中一种社区实践,我们通过工具链给予一定程度的支持。 -- TypeScript 最低要求:版本 2.8。 +- Egg 目前没有打算采用 TypeScript 进行重写。 +- Egg 及相关插件会提供 `index.d.ts` 文件以方便开发者使用。 +- TypeScript 是社区的一种实践方式,我们通过工具链提供一定程度的支持。 +- TypeScript 要求版本至少为 2.8。 -整体目录结构上跟 Egg 普通项目没啥区别: +整体的目录结构与一般的 Egg 项目没有太大差异: -- `typescript` 代码风格,后缀名为 `ts` -- `typings` 目录用于放置 `d.ts` 文件(大部分会自动生成) +- 采用 `typescript` 代码风格,文件后缀名为 `.ts`。 +- `typings` 目录用于存放 `d.ts` 文件(大部分文件可以自动生成)。 ```bash showcase @@ -121,18 +121,17 @@ export interface NewsItem { title: string; } ``` - ### 中间件(Middleware) ```typescript import { Context } from 'egg'; -// 这里是你自定义的中间件 +// 这是你自定义的中间件 export default function fooMiddleware(): any { return async (ctx: Context, next: () => Promise) => { // 你可以获取 config 的配置: // const config = ctx.app.config; - // config.xxx.... + // config.xxx... await next(); }; } @@ -140,7 +139,7 @@ export default function fooMiddleware(): any { 当某个 Middleware 文件的名称与 config 中某个属性名一致时,Middleware 会自动把这个属性下的所有配置读取过来。 -我们假定你有一个 Middleware,名称是 uuid,其 config.default.js 中配置如下: +假设你有一个 Middleware,名称是 `uuid`,在 `config.default.js` 中的配置如下: ```javascript 'use strict'; @@ -176,10 +175,9 @@ export default function(appInfo: EggAppConfig) { ...bizConfig, }; } - ``` -在对应的 uuid 中间件中: +在对应的 `uuid` 中间件中: ```typescript // app/middleware/uuid.ts @@ -191,14 +189,14 @@ export default function uuidMiddleWare( app: Application, ): any { return async (ctx: Context, next: () => Promise) => { - // name 就是 config.default.js 中 uuid 下的属性 + // name 就是 `config.default.js` 中 `uuid` 下的属性 console.info(options.name); await next(); }; } ``` -**注意:Middleware 目前返回值必须都是 `any`,否则使用 route.get/all 等方法的时候因为 Koa 的 `IRouteContext` 和 Egg 自身的 `Context` 不兼容导致编译报错。** +**注意:目前中间件的返回值必须是 `any` 类型。这是因为,如果使用 Koa 的 `IRouteContext` 类和 Egg 的 `Context` 类时,它们不兼容,将导致编译报错。** ### 扩展(Extend) @@ -219,13 +217,12 @@ export default (app) => { }); }; ``` - ### 配置(Config) -`Config` 这块稍微有点复杂,因为要支持: +`Config` 这部分稍微有点复杂,因为要支持: - 在 Controller,Service 那边使用配置,需支持多级提示,并自动关联。 -- Config 内部, `config.view = {}` 的写法,也应该支持提示。 +- Config 内部,`config.view = {}` 的写法,也应该支持提示。 - 在 `config.{env}.ts` 里可以用到 `config.default.ts` 自定义配置的提示。 ```typescript @@ -241,7 +238,7 @@ export default (appInfo: EggAppInfo) => { defaultViewEngine: 'nunjucks', mapping: { '.tpl': 'nunjucks', - }, + } }; // 应用本身的配置 @@ -250,19 +247,19 @@ export default (appInfo: EggAppInfo) => { pageSize: 30, serverUrl: 'https://hacker-news.firebaseio.com/v0', }; - + // 目的是将业务配置属性合并到 EggAppConfig 中返回 return { - // 如果直接返回 config ,将该类型合并到 EggAppConfig 的时候可能会出现 circulate type 错误。 + // 如果直接返回 config ,则将该类型合并到 EggAppConfig 的时候可能会出现 circulate type 错误。 ...(config as {}), - ...bizConfig, + ...bizConfig }; }; ``` -**注意,上面这种写法,将 config.default.ts 中返回的配置类型合并到 egg 的 EggAppConfig 类型中需要 egg-ts-helper 的配合。** +**注意,上述写法将 `config.default.ts` 中返回的配置类型合并到 egg 的 `EggAppConfig` 类型中时需要 egg-ts-helper 的配合。** -当 EggAppConfig 合并 config.default.ts 的类型后,在其他 config.{env}.ts 中这么写就也可以获得在 config.default.ts 定义的自定义配置的智能提示: +当 `EggAppConfig` 合并 `config.default.ts` 的类型后,在其他 `config.{env}.ts` 中这么写就也可以获得在 `config.default.ts` 定义的自定义配置的智能提示: ```typescript // app/config/config.local.ts @@ -281,7 +278,7 @@ export default () => { 备注: - TS 的 `Conditional Types` 是我们能完美解决 Config 提示的关键。 -- 有兴趣的可以看下 [egg/index.d.ts](https://github.com/eggjs/egg/blob/master/index.d.ts) 里面的 `PowerPartial` 实现。 +- 有兴趣的可以浏览 `egg/index.d.ts` 里面 `PowerPartial` 的实现。 ```typescript // {egg}/index.d.ts @@ -321,44 +318,42 @@ export default class FooBoot implements IBoot { } configWillLoad() { - // Ready to call configDidLoad, - // Config, plugin files are referred, - // this is the last chance to modify the config. + // 预备调用 configDidLoad, + // Config 和 plugin 文件已被引用, + // 这是修改配置的最后机会。 } configDidLoad() { - // Config, plugin files have loaded. + // Config 和 plugin 文件已加载。 } async didLoad() { - // All files have loaded, start plugin here. + // 所有文件已加载,此时可以启动插件。 } async willReady() { - // All plugins have started, can do some thing before app ready. + // 所有插件已启动,这里可以执行一些在应用准备好之前的操作。 } async didReady() { - // Worker is ready, can do some things - // don't need to block the app boot. + // Worker 已准备好,可以执行一些不会阻塞应用启动的操作。 } async serverDidReady() { - // Server is listening. + // 服务器已监听。 } async beforeClose() { - // Do some thing before app close. + // 应用关闭前执行的操作。 } } ``` - ### TS 类型定义(Typings) 该目录为 TS 的规范,在里面的 `**/*.d.ts` 文件将被自动识别。 - 开发者需要手写的建议放在 `typings/index.d.ts` 中。 -- 工具会自动生成 `typings/{app,config}/**.d.ts` ,请勿自行修改,避免被覆盖。(见下文) +- 工具会自动生成 `typings/{app,config}/**.d.ts`,请勿自行修改,避免被覆盖(见下文)。 --- @@ -366,11 +361,11 @@ export default class FooBoot implements IBoot { ### ts-node -`egg-bin` 已经内建了 [ts-node](https://github.com/TypeStrong/ts-node) ,`egg loader` 在开发期会自动加载 `*.ts` 并内存编译。 +`egg-bin` 已经内建了 [ts-node](https://github.com/TypeStrong/ts-node),`egg loader` 在开发期会自动加载 `*.ts` 并内存编译。 -目前已支持 `dev` / `debug` / `test` / `cov` 。 +目前已支持 `dev` / `debug` / `test` / `cov`。 -开发者仅需简单配置下 `package.json` : +开发者仅需简单配置下 `package.json`: ```json { @@ -383,11 +378,9 @@ export default class FooBoot implements IBoot { ### egg-ts-helper -由于 Egg 的自动加载机制,导致 TS 无法静态分析依赖,关联提示。 +由于 Egg 的自动加载机制,导致 TS 无法静态分析依赖,关联提示。幸运的是,TS 黑魔法比较多,我们可以通过 TS 的 [Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) 编写 `d.ts` 来辅助。 -幸亏 TS 黑魔法比较多,我们可以通过 TS 的 [Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) 编写 `d.ts` 来辅助。 - -譬如 `app/service/news.ts` 会自动挂载为 `ctx.service.news` ,通过如下写法即识别到: +例如,`app/service/news.ts` 会自动挂载为 `ctx.service.news`,通过如下写法即可识别到: ```typescript // typings/app/service/index.d.ts @@ -400,9 +393,9 @@ declare module 'egg' { } ``` -手动写这些文件,未免有点繁琐,因此我们提供了 [egg-ts-helper](https://github.com/whxaxes/egg-ts-helper) 工具来自动分析源码生成对应的 `d.ts` 文件。 +手动编写这些文件,未免有点繁琐,因此我们提供了 [egg-ts-helper](https://github.com/whxaxes/egg-ts-helper) 工具来自动分析源代码生成对应的 `d.ts` 文件。 -只需配置下 `package.json` : +只需配置下 `package.json`: ```json { @@ -417,11 +410,11 @@ declare module 'egg' { } ``` -开发期将自动生成对应的 `d.ts` 到 `typings/{app,config}/` 下,**请勿自行修改,避免被覆盖**。 +开发期将自动生成对应的 `d.ts` 到 `typings/{app,config}/` 下,请勿自行修改,避免被覆盖。 目前该工具已经能支持 ts 以及 js 的 egg 项目,均能获得相应的智能提示。 -### 单元测试和覆盖率(Unit Test and Cov) +### 单元测试和覆盖率(Unit Test and Coverage) 单元测试当然少不了: @@ -445,7 +438,7 @@ describe('test/app/service/news.test.js', () => { }); ``` -运行命令也跟之前一样,并内置了 `错误堆栈和覆盖率` 的支持: +运行命令也跟之前一样,并内置了错误堆栈和覆盖率的支持: ```json { @@ -465,7 +458,7 @@ describe('test/app/service/news.test.js', () => { ### 调试(Debug) -断点调试跟之前也没啥区别,会自动通过 `sourcemap` 断点到正确的位置。 +断点调试与之前没有什么区别,会自动通过 `sourcemap` 命中正确的位置。 ```json { @@ -481,18 +474,17 @@ describe('test/app/service/news.test.js', () => { } ``` -- [使用 VSCode 进行调试](https://eggjs.org/zh-cn/core/development.html#%E4%BD%BF%E7%94%A8-vscode-%E8%BF%9B%E8%A1%8C%E8%B0%83%E8%AF%95) +- [使用 VSCode 进行调试](https://eggjs.org/zh-cn/core/development.html#使用-vscode-进行调试) - [VSCode 调试 Egg 完美版 - 进化史](https://github.com/atian25/blog/issues/25) --- - ## 部署(Deploy) ### 构建(Build) -- 正式环境下,我们更倾向于把 ts 构建为 js ,建议在 `ci` 上构建并打包。 +- 正式环境下,我们更倾向于把 `ts` 构建为 `js`,建议在 `ci` 上构建并打包。 -配置 `package.json` : +配置 `package.json` : ```json { @@ -513,7 +505,7 @@ describe('test/app/service/news.test.js', () => { } ``` -对应的 `tsconfig.json` : +对应的 `tsconfig.json` : ```json { @@ -522,20 +514,19 @@ describe('test/app/service/news.test.js', () => { } ``` -**注意:当有同名的 ts 和 js 文件时,egg 会优先加载 js 文件。因此在开发期,`egg-ts-helper` 会自动调用清除同名的 `js` 文件,也可 `npm run clean` 手动清除。** +**注意:** 当有同名的 `ts` 和 `js` 文件时,egg 会优先加载 `js` 文件。因此在开发期,`egg-ts-helper` 会自动调用清除同名的 `js` 文件,也可通过 `npm run clean` 手动清除。 ### 错误堆栈(Error Stack) -线上服务的代码是经过编译后的 js,而我们期望看到的错误堆栈是指向 TS 源码。 +线上服务的代码是经过编译后的 `js`,而我们期望看到的错误堆栈是指向 `TS` 源码。 因此: -- 在构建的时候,需配置 `inlineSourceMap: true` 在 js 底部插入 sourcemap 信息。 +- 在构建的时候,需配置 `inlineSourceMap: true` 在 `js` 底部插入 `sourcemap` 信息。 - 在 `egg-scripts` 内建了处理,会自动纠正为正确的错误堆栈,应用开发者无需担心。 -具体内幕参见: - -- [https://zhuanlan.zhihu.com/p/26267678](https://zhuanlan.zhihu.com/p/26267678) -- [https://github.com/eggjs/egg-scripts/pull/19](https://github.com/eggjs/egg-scripts/pull/19) +具体内幕参见以下链接: +- [知乎专栏](https://zhuanlan.zhihu.com/p/26267678) +- [GitHub PR](https://github.com/eggjs/egg-scripts/pull/19) --- @@ -543,14 +534,14 @@ describe('test/app/service/news.test.js', () => { **指导原则:** -- 不建议使用 TS 直接开发插件/框架,发布到 npm 的插件应该是 js 形式。 -- 当你开发了一个插件/框架后,需要提供对应的 `index.d.ts` 。 -- 通过 [Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) 将插件/框架的功能注入到 Egg 中。 -- 都挂载到 `egg` 这个 module,不要用上层框架。 +- 不建议使用 `TS` 直接开发插件/框架,发布到 `npm` 的插件应该是 `js` 形式。 +- 当你开发了一个插件/框架后,需要提供对应的 `index.d.ts`。 +- 通过 [Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) 将插件/框架的功能注入到 `Egg` 中。 +- 都挂载到 `egg` 这个模块,不要用上层框架。 ### 插件 -可以参考 `egg-ts-helper` 自动生成的格式 +可以参考 `egg-ts-helper` 自动生成的格式: ```typescript // {plugin_root}/index.d.ts @@ -591,10 +582,10 @@ import * as Egg from 'egg'; import 'my-plugin'; declare module 'egg' { - // 跟插件一样拓展 egg ... + // 跟插件一样扩展 egg ... } -// 将 Egg 整个 export 出去 +// 将 `Egg` 整个 export 出去 export = Egg; ``` @@ -603,7 +594,7 @@ export = Egg; ```typescript // app/service/news.ts -// 开发者引入你的框架,也可以使用到提示到所有 Egg 的提示 +// 开发者引入你的框架,也可以使用到提示到所有 `Egg` 的提示 import { Service } from 'duck-egg'; export default class NewsService extends Service { @@ -612,30 +603,29 @@ export default class NewsService extends Service { } } ``` - ## 常见问题 -汇集一些有不少人提过的 issue 问题并统一解答。 +汇集了一些人们频繁提问的 `issue` 问题,并给出了统一的解答。 -### 运行 npm start 不会加载 ts +### 运行 `npm start` 不会加载 `ts` -npm start 运行的是 `egg-scripts start`,而我们只在 egg-bin 中集成了 ts-node,也就是只有在使用 egg-bin 的时候才允许直接运行 ts 。 +运行 `npm start` 实际上是执行了 `egg-scripts start` 命令,而 `ts-node` 只在 `egg-bin` 中被集成,只有使用 `egg-bin` 的时候,才允许直接运行 `ts` 文件。 -egg-scripts 是用于在生产环境下运行 egg 的 cli ,在生产环境下我们建议将 ts 编译成 js 之后再运行,毕竟在线上是需要考虑应用的健壮性和性能的,因此不建议在线上环境使用 ts-node 来运行应用。 +`egg-scripts` 是在生产环境下运行 `egg` 的 `CLI` 工具。在生产环境中我们建议先将 `ts` 编译成 `js`,然后再执行,因为在线上环境中,需要考虑应用的健壮性和性能,所以不建议使用 `ts-node`。 -而在开发期 ts-node 能降低 tsc 编译产生的文件带来的管理成本,并且 ts-node 带来的性能损耗在开发期几乎可以忽略,所以我们在 egg-bin 集成了 ts-node。 +而在开发环境中,`ts-node` 能减少 `tsc` 编译产生的文件管理成本,且在开发环境中带来的性能损耗几乎可以忽略,因此 `egg-bin` 中集成了 `ts-node`。 -**总结:如果项目需要在线上运行,请先使用 tsc 将 ts 编译成 js ( `npm run tsc` )再运行 `npm start`。** +**总结:** 如果项目需要在线上环境运行,请先使用 `tsc` 将 `ts` 编译成 `js`(`npm run tsc`),然后再运行 `npm start`。 -### 使用了 egg 插件后发现没有对应插件挂载的对象 +### 使用了 `egg` 插件后发现没有对应插件挂载的对象 -遇到该问题,一般是两种原因: +出现这个问题通常有两个原因: -**1. 该 egg 插件未定义 d.ts 。** +**1. 该 `egg` 插件未定义 `d.ts`。** -如果要在插件中将某个对象挂载到 egg 的类型中,需要按照上面写的 `插件 / 框架开发指南` 补充声明文件到对应插件中。 +如果在插件中想要将某个对象挂载到 `egg` 的类型中,需要按照分节“插件 / 框架开发指南”补充声明文件到相应插件中。 -如果需要上线想快速解决这个问题,可以直接在项目下新建个声明文件来解决。比如我使用了 `egg-dashboard` 这个插件,这个插件在 egg 的 app 中挂载了个 dashboard 对象,但是这个插件没有声明,直接使用 `app.dashboard` 又会有类型错误,我又急着解决该问题,就可以在项目下的 typings 目录下新建个 `index.d.ts` ,并且写入以下内容 +如果想要快速上线解决这个问题,可以直接在项目下新建一个声明文件。比如使用了 `egg-dashboard` 插件,该插件在 `egg` 的 `app` 对象中挂载了 `dashboard` 对象,但插件没有提供声明,直接使用 `app.dashboard` 会导致类型错误。此时可以在项目下的 `typings` 目录中新建 `index.d.ts` 文件,并写入以下内容: ```typescript // typings/index.d.ts @@ -649,13 +639,13 @@ declare module 'egg' { } ``` -即可解决,当然,我们更期望你能给缺少声明的插件提 PR 补声明,方便你我他。 +这样即可暂时解决问题,但我们更希望您能为缺少声明的插件提供 PR,以补充声明帮助更多人。 -**2. egg 插件定义了 d.ts ,但是没有引入。** +**2. `egg` 插件定义了 `d.ts` ,但未被引入。** -如果 egg 插件中正确无误定义了 d.ts ,也需要在应用或者框架层显式 import 之后 ts 才能加载到对应类型。 +即使 `egg` 插件正确地定义了 `d.ts`,也需要在应用或框架层明确地引入它,`ts` 才能加载对应类型。 -如果使用了 egg-ts-helper ,egg-ts-helper 会自动根据应用中开启了什么插件从而生成显式 import 插件的声明。如果未使用,就需要开发者自行在 `d.ts` 中显式 import 对应插件。 +如果使用了 `egg-ts-helper`,它会自动根据应用中启用的插件生成显式 `import` 插件声明。如果未使用,就需要开发者在 `d.ts` 中自行显式 `import` 对应插件。 ```typescript // typings/index.d.ts @@ -663,79 +653,76 @@ declare module 'egg' { import 'egg-dashboard'; ``` -**注意:必须在 d.ts 中 import,因为 egg 插件大部分没有入口文件,如果在 ts 中 import 的话运行会出问题。** +**注意:** 必须在 `d.ts` 中 `import`。由于 `egg` 插件大部分没有入口文件,如果在 `ts` 文件中 `import`,运行时可能出现问题。 -### 在 tsconfig.json 中配置了 paths 无效 +### 在 `tsconfig.json` 中配置了 `paths` 无效 -这个严格来说不属于 egg 的问题,但是问的人不少,因此也在此解答一下。原因是 tsc 将 ts 编译成 js 的时候,并不会去转换 import 的模块路径,因此当你在 tsconfig.json 中配置了 paths 之后,如果你在 ts 中使用 paths 并 import 了对应模块,编译成 js 的时候就有大概率出现模块找不到的情况了。 +此问题严格来说并非 `egg` 特有,但常见,故在此解答。原因是当 `tsc` 将 `ts` 编译成 `js` 时,并不转换 `import` 的模块路径。因此,若您在 `tsconfig.json` 中配置了 `paths` 后,在 `ts` 中使用 `paths` 导入对应模块,编译成 `js` 后可能出现模块找不到的问题。 -解决办法是,要么不用 paths ,要么使用 paths 的时候只用来 import 一些声明而非具体值,再要么就可以使用 [tsconfig-paths](https://github.com/dividab/tsconfig-paths) 来 hook 掉 node 中的模块路径解析逻辑,从而支持 tsconfig.json 中的 paths。 +解决方法:不使用 `paths`;或使用 `paths` 时只导入声明,不导入具体值;或使用 [`tsconfig-paths`](https://github.com/dividab/tsconfig-paths) 动态处理。 -使用 tsconfig-paths 可以直接在 config/plugin.ts 中引入,因为 plugin.ts 不管在 App 中还是在 Agent 中都是第一个加载的,因此在这个代码中引入 tsconfig-paths 即可。 +使用 `tsconfig-paths` 时,可以直接在 `config/plugin.ts` 中引入,因为它总是最先加载的。在代码中引入该模块,见下例: ```typescript // config/plugin.ts import 'tsconfig-paths/register'; -... +// 其他代码 ``` -### 给 egg 插件提交声明的时候如何编写单测? +### 如何为 `egg` 插件编写声明单测? -由于有不少开发者在给 egg 插件提交声明的时候,不知道如何编写单测来测试声明的准确性,因此也在这里说明一下。 +许多开发者在给 `egg` 插件提交声明时,不了解如何编写单元测试来验证声明的准确性。以下是解决方法。 -当给一个 egg 插件编写好声明之后,就可以在 `test/fixures` 下创建个使用 ts 写的 egg 应用,参考 ( https://github.com/eggjs/egg-view/tree/master/test/fixtures/apps/ts ),记得在 tsconfig.json 中加入 paths 的配置从而方便在 fixture 中 import ,比如 egg-view 中的 +在编写完 `egg` 插件的声明后,可以在 `test/fixtures` 中创建一个使用 `ts` 编写的 `egg` 应用,类似于 [https://github.com/eggjs/egg-view/tree/master/test/fixtures/apps/ts](https://github.com/eggjs/egg-view/tree/master/test/fixtures/apps/ts) 的样本,并在 `tsconfig.json` 中加入 `paths` 配置,便于在单元测试中 `import` 模块。如 `egg-view` 中配置: ```json - "paths": { - "egg-view": ["../../../../"] - } +"paths": { + "egg-view": ["../../../../"] +} ``` -同时记住不要在 tsconfig.json 中配置 `"skipLibCheck": true` ,如果配置了该属性为 true ,tsc 编译的时候会忽略 d.ts 中的类型校验,这样单测就无意义了。 +同时请勿在 `tsconfig.json` 中设置 `"skipLibCheck": true`。如果设置为 `true`,`tsc` 编译时会忽略 `d.ts` 文件中的类型检查,使单元测试失去意义。 -然后再添加一个用例用来验证插件的声明使用是否正确即可,还是拿 egg-view 来做示例。 +接着添加用例验证插件声明的正确性,参考 `egg-view`: ```js describe('typescript', () => { it('should compile ts without error', () => { - return ( - coffee - .fork(require.resolve('typescript/bin/tsc'), [ - '-p', - path.resolve(__dirname, './fixtures/apps/ts/tsconfig.json'), - '--noEmit', - ]) - // .debug() - .expect('code', 0) - .end() - ); + return coffee + .fork(require.resolve('typescript/bin/tsc'), [ + '-p', + path.resolve(__dirname, './fixtures/apps/ts/tsconfig.json'), + '--noEmit', + ]) + // .debug() + .expect('code', 0) + .end(); }); }); ``` -可参考单测的项目: +以下几个项目可作为单元测试参考: - [https://github.com/eggjs/egg](https://github.com/eggjs/egg) - [https://github.com/eggjs/egg-view](https://github.com/eggjs/egg-view) - [https://github.com/eggjs/egg-logger](https://github.com/eggjs/egg-logger) - ### 编译速度慢? -根据我们的实践,ts-node 是目前相对较优的解决方案,既不用另起终端执行 tsc ,也能获得还能接受的启动速度( 仅限于 ts-node@7 ,新的版本由于把文件缓存去掉了,导致特别慢( [#754](https://github.com/TypeStrong/ts-node/issues/754) ),因此未升级 )。 +根据我们的实践,`ts-node` 是目前相对较优的解决方案,既不用另起终端执行 `tsc`,也能获得还能接受的启动速度(仅限于 `ts-node@7`,新的版本由于把文件缓存去掉了,导致特别慢([#754](https://github.com/TypeStrong/ts-node/issues/754)),因此未升级)。 -但是如果项目特别庞大,ts-node 的性能也会吃紧,我们提供了以下优化方案供参考: +但如果项目特别庞大,`ts-node` 的性能也会吃紧,我们提供了以下优化方案供参考: #### 关闭类型检查 -编译耗时大头是在类型检查,如果关闭也能带来一定的性能提升,可以在启动应用的时候带上 `TS_NODE_TRANSPILE_ONLY=true` 环境变量,比如 +编译耗时大头在类型检查。如果关闭,也能带来一定的性能提升。可以在启动应用时带上 `TS_NODE_TRANSPILE_ONLY=true` 环境变量,比如 ```bash $ TS_NODE_TRANSPILE_ONLY=true egg-bin dev ``` -或者配置 tscompiler 为 ts-node 提供的仅编译的注册器。 +或者在 `package.json` 中配置 `tscompiler` 为 `ts-node` 提供的仅编译的注册器。 ```json // package.json @@ -751,13 +738,13 @@ $ TS_NODE_TRANSPILE_ONLY=true egg-bin dev #### 更换高性能 compiler -除了 ts-node 之外,业界也有不少支持编译 ts 的项目,比如 esbuild ,可以先安装 [esbuild-register](https://github.com/egoist/esbuild-register) +除了 `ts-node` 之外,业界也有不少支持编译 ts 的项目,比如 `esbuild`。可以先安装 [esbuild-register](https://github.com/egoist/esbuild-register) ```bash $ npm install esbuild-register --save-dev ``` -再在 package.json 中配置 `tscompiler` +再在 `package.json` 中配置 `tscompiler` ```json // package.json @@ -771,22 +758,22 @@ $ npm install esbuild-register --save-dev } ``` -即可使用 esbuild-register 来编译( 注意,esbuild-register 不具备 typecheck 功能 )。 +即可使用 `esbuild-register` 来编译(注意,`esbuild-register` 不具备 typecheck 功能)。 -> 如果想用 swc 也一样,安装一下 [@swc-node/register](https://github.com/Brooooooklyn/swc-node#swc-noderegister) ,然后一样配置到 tscompiler 即可 +> 如果想用 `swc` 也一样,安装 [@swc-node/register](https://github.com/Brooooooklyn/swc-node#swc-noderegister),然后配置到 `tscompiler` 即可。 -#### 使用 tsc +#### 使用 `tsc` -如果还是觉得这种在运行时动态编译的速度实在无法忍受,也可以直接使用 tsc ,即不需要在 package.json 中配置 typescript 为 true ,在开发期间单独起个终端执行 tsc +如果还是觉得这种在运行时动态编译的速度实在无法忍受,也可以直接使用 `tsc`。即不需要在 `package.json` 中配置 `typescript` 为 `true`,在开发期间单独起个终端执行 `tsc`。 ```bash $ tsc -w ``` -然后再正常启动 egg 应用即可 +然后再正常启动 `egg` 应用即可。 ```bash $ egg-bin dev ``` -建议在 .gitignore 中加上对 `**/*.js` 的配置,避免将生成的 js 代码也提交到了远端。 +建议在 `.gitignore` 中加上对 `**/*.js` 的配置,避免将生成的 js 代码也提交到了远端。