Egg學習記錄(2)

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

控制器(Controller)

簡單的說 Controller 負責解析用戶的輸入孽惰,處理后返回相應的結(jié)果怕享,例如

  • RESTful 接口中募壕,Controller 接受用戶的參數(shù)芭商,從數(shù)據(jù)庫中查找內(nèi)容返回給用戶或者將用戶的請求更新到數(shù)據(jù)庫中。
  • 在 HTML 頁面請求中嗽测,Controller 根據(jù)用戶訪問不同的 URL绪励,渲染不同的模板得到 HTML 返回給用戶。
  • 在代理服務(wù)器中唠粥,Controller 將用戶的請求轉(zhuǎn)發(fā)到其他服務(wù)器上,并將其他服務(wù)器的處理結(jié)果返回給用戶停做。

框架推薦 Controller 層主要對用戶的請求參數(shù)進行處理(校驗晤愧、轉(zhuǎn)換),然后調(diào)用對應的 service 方法處理業(yè)務(wù)蛉腌,得到業(yè)務(wù)結(jié)果后封裝并返回:

  1. 獲取用戶通過 HTTP 傳遞過來的請求參數(shù)官份。
  2. 校驗只厘、組裝參數(shù)。
  3. 調(diào)用 Service 進行業(yè)務(wù)處理舅巷,必要時處理轉(zhuǎn)換 Service 的返回結(jié)果羔味,讓它適應用戶的需求。
  4. 通過 HTTP 將結(jié)果響應給用戶钠右。

如何編寫 Controller

所有的 Controller 文件都必須放在 app/controller 目錄下赋元,可以支持多級目錄,訪問的時候可以通過目錄名級聯(lián)訪問飒房。Controller 支持多種形式進行編寫搁凸,可以根據(jù)不同的項目場景和開發(fā)習慣來選擇。

Controller 類(推薦)

我們可以通過定義 Controller 類的方式來編寫代碼:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// app/controller/post.js
const Controller = require('egg').Controller;
class PostController extends Controller {
 async create() {
 const { ctx, service } = this;
 const createRule = {
 title: { type: 'string' },
 content: { type: 'string' },
 };
 // 校驗參數(shù)
 ctx.validate(createRule);
 // 組裝參數(shù)
 const author = ctx.session.userId;
 const req = Object.assign(ctx.request.body, { author });
 // 調(diào)用 Service 進行業(yè)務(wù)處理
 const res = await service.post.create(req);
 // 設(shè)置響應內(nèi)容和響應狀態(tài)碼
 ctx.body = { id: res.id };
 ctx.status = 201;
 }
}
module.exports = PostController;
</pre>

我們通過上面的代碼定義了一個 PostController 的類狠毯,類里面的每一個方法都可以作為一個 Controller 在 Router 中引用到护糖,我們可以從 app.controller 根據(jù)文件名和方法名定位到它。


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// app/router.js
module.exports = app => {
 const { router, controller } = app;
 router.post('createPost', '/api/posts', controller.post.create);
}
</pre>

Controller 支持多級目錄嚼松,例如如果我們將上面的 Controller 代碼放到 app/controller/sub/post.js 中嫡良,則可以在 router 中這樣使用:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// app/router.js
module.exports = app => {
 app.router.post('createPost', '/api/posts', app.controller.sub.post.create);
}
</pre>

定義的 Controller 類,會在每一個請求訪問到 server 時實例化一個全新的對象献酗,而項目中的 Controller 類繼承于 egg.Controller寝受,會有下面幾個屬性掛在 this 上。

  • this.ctx: 當前請求的上下文 Context 對象的實例凌摄,通過它我們可以拿到框架封裝好的處理當前請求的各種便捷屬性和方法羡蛾。
  • this.app: 當前應用 Application 對象的實例,通過它我們可以拿到框架提供的全局對象和方法锨亏。
  • this.service:應用定義的 Service痴怨,通過它我們可以訪問到抽象出的業(yè)務(wù)層,等價于 this.ctx.service 器予。
  • this.config:應用運行時的配置項浪藻。
  • this.logger:logger 對象,上面有四個方法(debug乾翔,info爱葵,warnerror)反浓,分別代表打印四個不同級別的日志萌丈,使用方法和效果與 context logger 中介紹的一樣,但是通過這個 logger 對象記錄的日志雷则,在日志前面會加上打印該日志的文件路徑辆雾,以便快速定位日志打印位置。

自定義 Controller 基類

按照類的方式編寫 Controller月劈,不僅可以讓我們更好的對 Controller 層代碼進行抽象(例如將一些統(tǒng)一的處理抽象成一些私有方法)度迂,還可以通過自定義 Controller 基類的方式封裝應用中常用的方法藤乙。


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// app/core/base_controller.js
const { Controller } = require('egg');
class BaseController extends Controller {
 get user() {
 return this.ctx.session.user;
 }

 success(data) {
 this.ctx.body = {
 success: true,
 data,
 };
 }

 notFound(msg) {
 msg = msg || 'not found';
 this.ctx.throw(404, msg);
 }
}
module.exports = BaseController;
</pre>

此時在編寫應用的 Controller 時,可以繼承 BaseController惭墓,直接使用基類上的方法:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">//app/controller/post.js
const Controller = require('../core/base_controller');
class PostController extends Controller {
 async list() {
 const posts = await this.service.listByUser(this.user);
 this.success(posts);
 }
}
</pre>

Controller 方法(不推薦使用坛梁,只是為了兼容)

每一個 Controller 都是一個 async function,它的入?yún)檎埱蟮纳舷挛?Context 對象的實例腊凶,通過它我們可以拿到框架封裝好的各種便捷屬性和方法划咐。

例如我們寫一個對應到 POST /api/posts 接口的 Controller,我們會在 app/controller 目錄下創(chuàng)建一個 post.js 文件

<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// app/controller/post.js
exports.create = async ctx => {
 const createRule = {
 title: { type: 'string' },
 content: { type: 'string' },
 };
 // 校驗參數(shù)
 ctx.validate(createRule);
 // 組裝參數(shù)
 const author = ctx.session.userId;
 const req = Object.assign(ctx.request.body, { author });
 // 調(diào)用 service 進行業(yè)務(wù)處理
 const res = await ctx.service.post.create(req);
 // 設(shè)置響應內(nèi)容和響應狀態(tài)碼
 ctx.body = { id: res.id };
 ctx.status = 201;
};</pre>

HTTP 基礎(chǔ)

由于 Controller 基本上是業(yè)務(wù)開發(fā)中唯一和 HTTP 協(xié)議打交道的地方吭狡,在繼續(xù)往下了解之前尖殃,我們首先簡單的看一下 HTTP 協(xié)議是怎樣的。

如果我們發(fā)起一個 HTTP 請求來訪問前面例子中提到的 Controller:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">curl -X POST http://localhost:3000/api/posts --data '{"title":"controller", "content": "what is controller"}' --header 'Content-Type:application/json; charset=UTF-8'
</pre>

通過 curl 發(fā)出的 HTTP 請求的內(nèi)容就會是下面這樣的:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">POST /api/posts HTTP/1.1
Host: localhost:3000
Content-Type: application/json; charset=UTF-8

{"title": "controller", "content": "what is controller"}
</pre>

請求的第一行包含了三個信息划煮,我們比較常用的是前面兩個:

  • method:這個請求中 method 的值是 POST送丰。
  • path:值為 /api/posts,如果用戶的請求中包含 query弛秋,也會在這里出現(xiàn)

從第二行開始直到遇到的第一個空行位置器躏,都是請求的 Headers 部分,這一部分中有許多常用的屬性蟹略,包括這里看到的 Host登失,Content-Type,還有 Cookie挖炬,User-Agent 等等揽浙。在這個請求中有兩個頭:

  • Host:我們在瀏覽器發(fā)起請求的時候,域名會用來通過 DNS 解析找到服務(wù)的 IP 地址意敛,但是瀏覽器也會將域名和端口號放在 Host 頭中一并發(fā)送給服務(wù)端馅巷。
  • Content-Type:當我們的請求有 body 的時候,都會有 Content-Type 來標明我們的請求體是什么格式的草姻。

之后的內(nèi)容全部都是請求的 body钓猬,當請求是 POST, PUT, DELETE 等方法的時候,可以帶上請求體撩独,服務(wù)端會根據(jù) Content-Type 來解析請求體敞曹。

在服務(wù)端處理完這個請求后,會發(fā)送一個 HTTP 響應給客戶端


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Content-Length: 8
Date: Mon, 09 Jan 2017 08:40:28 GMT
Connection: keep-alive

{"id": 1}
</pre>

第一行中也包含了三段综膀,其中我們常用的主要是響應狀態(tài)碼澳迫,這個例子中它的值是 201,它的含義是在服務(wù)端成功創(chuàng)建了一條資源剧劝。

和請求一樣纲刀,從第二行開始到下一個空行之間都是響應頭,這里的 Content-Type, Content-Length 表示這個響應的格式是 JSON担平,長度為 8 個字節(jié)示绊。

最后剩下的部分就是這次響應真正的內(nèi)容。

獲取 HTTP 請求參數(shù)

從上面的 HTTP 請求示例中可以看到暂论,有好多地方可以放用戶的請求數(shù)據(jù)面褐,框架通過在 Controller 上綁定的 Context 實例,提供了許多便捷方法和屬性獲取用戶通過 HTTP 請求發(fā)送過來的參數(shù)取胎。

query

在 URL 中 ? 后面的部分是一個 Query String展哭,這一部分經(jīng)常用于 GET 類型的請求中傳遞參數(shù)。例如 GET /posts?category=egg&language=nodecategory=egg&language=node 就是用戶傳遞過來的參數(shù)闻蛀。我們可以通過 ctx.query 拿到解析過后的這個參數(shù)體


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">class PostController extends Controller {
 async listPosts() {
 const query = this.ctx.query;
 // {
 //   category: 'egg',
 //   language: 'node',
 // }
 }
}
</pre>

當 Query String 中的 key 重復時匪傍,ctx.query 只取 key 第一次出現(xiàn)時的值,后面再出現(xiàn)的都會被忽略觉痛。GET /posts?category=egg&category=koa 通過 ctx.query 拿到的值是 { category: 'egg' }役衡。

這樣處理的原因是為了保持統(tǒng)一性,由于通常情況下我們都不會設(shè)計讓用戶傳遞 key 相同的 Query String薪棒,所以我們經(jīng)常會寫類似下面的代碼:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">const key = ctx.query.key || '';
if (key.startsWith('egg')) {
 // do something
}
</pre>

而如果有人故意發(fā)起請求在 Query String 中帶上重復的 key 來請求時就會引發(fā)系統(tǒng)異常手蝎。因此框架保證了從 ctx.query 上獲取的參數(shù)一旦存在,一定是字符串類型俐芯。

queries

有時候我們的系統(tǒng)會設(shè)計成讓用戶傳遞相同的 key棵介,例如 GET /posts?category=egg&id=1&id=2&id=3。針對此類情況吧史,框架提供了 ctx.queries 對象邮辽,這個對象也解析了 Query String,但是它不會丟棄任何一個重復的數(shù)據(jù)贸营,而是將他們都放到一個數(shù)組中:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// GET /posts?category=egg&id=1&id=2&id=3
class PostController extends Controller {
 async listPosts() {
 console.log(this.ctx.queries);
 // {
 //   category: [ 'egg' ],
 //   id: [ '1', '2', '3' ],
 // }
 }
}
</pre>

ctx.queries 上所有的 key 如果有值吨述,也一定會是數(shù)組類型。

Router params

Router 中莽使,我們介紹了 Router 上也可以申明參數(shù)锐极,這些參數(shù)都可以通過 ctx.params 獲取到。


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// app.get('/projects/:projectId/app/:appId', 'app.listApp');
// GET /projects/1/app/2
class AppController extends Controller {
 async listApp() {
 assert.equal(this.ctx.params.projectId, '1');
 assert.equal(this.ctx.params.appId, '2');
 }
}</pre>

body

雖然我們可以通過 URL 傳遞參數(shù)芳肌,但是還是有諸多限制:

  • 瀏覽器中會對 URL 的長度有所限制灵再,如果需要傳遞的參數(shù)過多就會無法傳遞。
  • 服務(wù)端經(jīng)常會將訪問的完整 URL 記錄到日志文件中亿笤,有一些敏感數(shù)據(jù)通過 URL 傳遞會不安全翎迁。

在前面的 HTTP 請求報文示例中,我們看到在 header 之后還有一個 body 部分净薛,我們通常會在這個部分傳遞 POST汪榔、PUT 和 DELETE 等方法的參數(shù)。一般請求中有 body 的時候肃拜,客戶端(瀏覽器)會同時發(fā)送 Content-Type 告訴服務(wù)端這次請求的 body 是什么格式的痴腌。Web 開發(fā)中數(shù)據(jù)傳遞最常用的兩類格式分別是 JSON 和 Form雌团。

框架內(nèi)置了 bodyParser 中間件來對這兩類格式的請求 body 解析成 object 掛載到 ctx.request.body 上。HTTP 協(xié)議中并不建議在通過 GET士聪、HEAD 方法訪問時傳遞 body锦援,所以我們無法在 GET、HEAD 方法中按照此方法獲取到內(nèi)容剥悟。


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// POST /api/posts HTTP/1.1
// Host: localhost:3000
// Content-Type: application/json; charset=UTF-8
//
// {"title": "controller", "content": "what is controller"}
class PostController extends Controller {
 async listPosts() {
 assert.equal(this.ctx.request.body.title, 'controller');
 assert.equal(this.ctx.request.body.content, 'what is controller');
 }
}
</pre>

框架對 bodyParser 設(shè)置了一些默認參數(shù)灵寺,配置好之后擁有以下特性:

  • 當請求的 Content-Type 為 application/jsonapplication/json-patch+json区岗,application/vnd.api+jsonapplication/csp-report 時略板,會按照 json 格式對請求 body 進行解析,并限制 body 最大長度為 100kb慈缔。
  • 當請求的 Content-Type 為 application/x-www-form-urlencoded 時叮称,會按照 form 格式對請求 body 進行解析,并限制 body 最大長度為 100kb胀糜。
  • 如果解析成功颅拦,body 一定會是一個 Object(可能是一個數(shù)組)。

一般來說我們最經(jīng)常調(diào)整的配置項就是變更解析時允許的最大長度教藻,可以在 config/config.default.js 中覆蓋框架的默認值距帅。


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">module.exports = {
 bodyParser: {
 jsonLimit: '1mb',
 formLimit: '1mb',
 },
};
</pre>

如果用戶的請求 body 超過了我們配置的解析最大長度,會拋出一個狀態(tài)碼為 413 的異常括堤,如果用戶請求的 body 解析失斅到铡(錯誤的 JSON),會拋出一個狀態(tài)碼為 400 的異常悄窃。

注意:在調(diào)整 bodyParser 支持的 body 長度時讥电,如果我們應用前面還有一層反向代理(Nginx),可能也需要調(diào)整它的配置轧抗,確保反向代理也支持同樣長度的請求 body恩敌。

一個常見的錯誤是把 ctx.request.bodyctx.body 混淆,后者其實是 ctx.response.body 的簡寫横媚。

獲取上傳的文件

請求 body 除了可以帶參數(shù)之外纠炮,還可以發(fā)送文件,一般來說灯蝴,瀏覽器上都是通過 Multipart/form-data 格式發(fā)送文件的恢口,框架通過內(nèi)置 Multipart 插件來支持獲取用戶上傳的文件,我們?yōu)槟闾峁┝藘煞N方式:

  • File 模式:

如果你完全不知道 Nodejs 中的 Stream 用法穷躁,那么 File 模式非常合適你:

1)在 config 文件中啟用 file 模式:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// config/config.default.js
exports.multipart = {
 mode: 'file',
};
</pre>

2)上傳 / 接收文件:

  1. 上傳 / 接收單個文件:

你的前端靜態(tài)頁面代碼應該看上去如下樣子:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;"><form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
 title: <input name="title" />
 file: <input name="file" type="file" />
 <button type="submit">Upload</button>
</form>
</pre>

對應的后端代碼如下:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// app/controller/upload.js
const Controller = require('egg').Controller;
const fs = require('mz/fs');

module.exports = class extends Controller {
 async upload() {
 const { ctx } = this;
 const file = ctx.request.files[0];
 const name = 'egg-multipart-test/' + path.basename(file.filename);
 let result;
 try {
 // 處理文件耕肩,比如上傳到云端
 result = await ctx.oss.put(name, file.filepath);
 } finally {
 // 需要刪除臨時文件
 await fs.unlink(file.filepath);
 }

 ctx.body = {
 url: result.url,
 // 獲取所有的字段值
 requestBody: ctx.request.body,
 };
 }
};
</pre>

  1. 上傳 / 接收多個文件:

對于多個文件,我們借助 ctx.request.files 屬性進行遍歷,然后分別進行處理:

你的前端靜態(tài)頁面代碼應該看上去如下樣子:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;"><form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
 title: <input name="title" />
 file1: <input name="file1" type="file" />
 file2: <input name="file2" type="file" />
 <button type="submit">Upload</button>
</form>
</pre>

對應的后端代碼:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// app/controller/upload.js
const Controller = require('egg').Controller;
const fs = require('mz/fs');

module.exports = class extends Controller {
 async upload() {
 const { ctx } = this;
 console.log(ctx.request.body);
 console.log('got %d files', ctx.request.files.length);
 for (const file of ctx.request.files) {
 console.log('field: ' + file.fieldname);
 console.log('filename: ' + file.filename);
 console.log('encoding: ' + file.encoding);
 console.log('mime: ' + file.mime);
 console.log('tmp filepath: ' + file.filepath);
 let result;
 try {
 // 處理文件猿诸,比如上傳到云端
 result = await ctx.oss.put('egg-multipart-test/' + file.filename, file.filepath);
 } finally {
 // 需要刪除臨時文件
 await fs.unlink(file.filepath);
 }
 console.log(result);
 }
 }
};
</pre>

  • Stream 模式:

如果你對于 Node 中的 Stream 模式非常熟悉婚被,那么你可以選擇此模式。在 Controller 中两芳,我們可以通過 ctx.getFileStream() 接口能獲取到上傳的文件流摔寨。

  1. 上傳 / 接受單個文件:
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;"><form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
 title: <input name="title" />
 file: <input name="file" type="file" />
 <button type="submit">Upload</button>
</form>
</pre>

<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">const path = require('path');
const sendToWormhole = require('stream-wormhole');
const Controller = require('egg').Controller;

class UploaderController extends Controller {
 async upload() {
 const ctx = this.ctx;
 const stream = await ctx.getFileStream();
 const name = 'egg-multipart-test/' + path.basename(stream.filename);
 // 文件處理,上傳到云存儲等等
 let result;
 try {
 result = await ctx.oss.put(name, stream);
 } catch (err) {
 // 必須將上傳的文件流消費掉怖辆,要不然瀏覽器響應會卡死
 await sendToWormhole(stream);
 throw err;
 }

 ctx.body = {
 url: result.url,
 // 所有表單字段都能通過 `stream.fields` 獲取到
 fields: stream.fields,
 };
 }
}

module.exports = UploaderController;
</pre>

要通過 ctx.getFileStream 便捷的獲取到用戶上傳的文件,需要滿足兩個條件:

  • 只支持上傳一個文件删顶。
  • 上傳文件必須在所有其他的 fields 后面竖螃,否則在拿到文件流時可能還獲取不到 fields。
  1. 上傳 / 接受多個文件:

如果要獲取同時上傳的多個文件逗余,不能通過 ctx.getFileStream() 來獲取特咆,只能通過下面這種方式:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">const sendToWormhole = require('stream-wormhole');
const Controller = require('egg').Controller;

class UploaderController extends Controller {
 async upload() {
 const ctx = this.ctx;
 const parts = ctx.multipart();
 let part;
 // parts() 返回 promise 對象
 while ((part = await parts()) != null) {
 if (part.length) {
 // 這是 busboy 的字段
 console.log('field: ' + part[0]);
 console.log('value: ' + part[1]);
 console.log('valueTruncated: ' + part[2]);
 console.log('fieldnameTruncated: ' + part[3]);
 } else {
 if (!part.filename) {
 // 這時是用戶沒有選擇文件就點擊了上傳(part 是 file stream,但是 part.filename 為空)
 // 需要做出處理录粱,例如給出錯誤提示消息
 return;
 }
 // part 是上傳的文件流
 console.log('field: ' + part.fieldname);
 console.log('filename: ' + part.filename);
 console.log('encoding: ' + part.encoding);
 console.log('mime: ' + part.mime);
 // 文件處理腻格,上傳到云存儲等等
 let result;
 try {
 result = await ctx.oss.put('egg-multipart-test/' + part.filename, part);
 } catch (err) {
 // 必須將上傳的文件流消費掉,要不然瀏覽器響應會卡死
 await sendToWormhole(part);
 throw err;
 }
 console.log(result);
 }
 }
 console.log('and we are done parsing the form!');
 }
}

module.exports = UploaderController;
</pre>

為了保證文件上傳的安全啥繁,框架限制了支持的的文件格式菜职,框架默認支持白名單如下:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// images
'.jpg', '.jpeg', // image/jpeg
'.png', // image/png, image/x-png
'.gif', // image/gif
'.bmp', // image/bmp
'.wbmp', // image/vnd.wap.wbmp
'.webp',
'.tif',
'.psd',
// text
'.svg',
'.js', '.jsx',
'.json',
'.css', '.less',
'.html', '.htm',
'.xml',
// tar
'.zip',
'.gz', '.tgz', '.gzip',
// video
'.mp3',
'.mp4',
'.avi',
</pre>

用戶可以通過在 config/config.default.js 中配置來新增支持的文件擴展名,或者重寫整個白名單

  • 新增支持的文件擴展名

<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">module.exports = {
 multipart: {
 fileExtensions: [ '.apk' ] // 增加對 apk 擴展名的文件支持
 },
};
</pre>

  • 覆蓋整個白名單

<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">module.exports = {
 multipart: {
 whitelist: [ '.png' ], // 覆蓋整個白名單旗闽,只允許上傳 '.png' 格式
 },
};
</pre>

注意:當重寫了 whitelist 時酬核,fileExtensions 不生效。

Cookie

HTTP 請求都是無狀態(tài)的适室,但是我們的 Web 應用通常都需要知道發(fā)起請求的人是誰嫡意。為了解決這個問題,HTTP 協(xié)議設(shè)計了一個特殊的請求頭:Cookie捣辆。服務(wù)端可以通過響應頭(set-cookie)將少量數(shù)據(jù)響應給客戶端蔬螟,瀏覽器會遵循協(xié)議將數(shù)據(jù)保存,并在下次請求同一個服務(wù)的時候帶上(瀏覽器也會遵循協(xié)議汽畴,只在訪問符合 Cookie 指定規(guī)則的網(wǎng)站時帶上對應的 Cookie 來保證安全性)旧巾。

通過 ctx.cookies,我們可以在 Controller 中便捷整袁、安全的設(shè)置和讀取 Cookie菠齿。


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">class CookieController extends Controller {
 async add() {
 const ctx = this.ctx;
 const count = ctx.cookies.get('count');
 count = count ? Number(count) : 0;
 ctx.cookies.set('count', ++count);
 ctx.body = count;
 }

 async remove() {
 const ctx = this.ctx;
 const count = ctx.cookies.set('count', null);
 ctx.status = 204;
 }
}
</pre>

Cookie 雖然在 HTTP 中只是一個頭,但是通過 foo=bar;foo1=bar1; 的格式可以設(shè)置多個鍵值對坐昙。

Cookie 在 Web 應用中經(jīng)常承擔了傳遞客戶端身份信息的作用绳匀,因此有許多安全相關(guān)的配置,不可忽視,Cookie 文檔中詳細介紹了 Cookie 的用法和安全相關(guān)的配置項疾棵,可以深入閱讀了解戈钢。

Session

通過 Cookie,我們可以給每一個用戶設(shè)置一個 Session是尔,用來存儲用戶身份相關(guān)的信息殉了,這份信息會加密后存儲在 Cookie 中,實現(xiàn)跨請求的用戶身份保持拟枚。

框架內(nèi)置了 Session 插件薪铜,給我們提供了 ctx.session 來訪問或者修改當前用戶 Session 。

<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">class PostController extends Controller {
 async fetchPosts() {
 const ctx = this.ctx;
 // 獲取 Session 上的內(nèi)容
 const userId = ctx.session.userId;
 const posts = await ctx.service.post.fetch(userId);
 // 修改 Session 的值
 ctx.session.visited = ctx.session.visited ? ++ctx.session.visited : 1;
 ctx.body = {
 success: true,
 posts,
 };
 }
}
</pre>

Session 的使用方法非常直觀恩溅,直接讀取它或者修改它就可以了隔箍,如果要刪除它,直接將它賦值為 null


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">class SessionController extends Controller {
 async deleteSession() {
 this.ctx.session = null;
 }
};
</pre>

和 Cookie 一樣脚乡,Session 也有許多安全等選項和功能蜒滩,在使用之前也最好閱讀 Session 文檔深入了解。

配置

對于 Session 來說奶稠,主要有下面幾個屬性可以在 config.default.js 中進行配置:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">module.exports = {
 key: 'EGG_SESS', // 承載 Session 的 Cookie 鍵值對名字
 maxAge: 86400000, // Session 的最大有效時間
};
</pre>

參數(shù)校驗

在獲取到用戶請求的參數(shù)后俯艰,不可避免的要對參數(shù)進行一些校驗。

借助 Validate 插件提供便捷的參數(shù)校驗機制锌订,幫助我們完成各種復雜的參數(shù)校驗竹握。


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// config/plugin.js
exports.validate = {
 enable: true,
 package: 'egg-validate',
};
</pre>

通過 ctx.validate(rule, [body]) 直接對參數(shù)進行校驗:


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">class PostController extends Controller {
 async create() {
 // 校驗參數(shù)
 // 如果不傳第二個參數(shù)會自動校驗 `ctx.request.body`
 this.ctx.validate({
 title: { type: 'string' },
 content: { type: 'string' },
 });
 }
}
</pre>

當校驗異常時,會直接拋出一個異常瀑志,異常的狀態(tài)碼為 422涩搓,errors 字段包含了詳細的驗證不通過信息。如果想要自己處理檢查的異常劈猪,可以通過 try catch 來自行捕獲昧甘。


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">class PostController extends Controller {
 async create() {
 const ctx = this.ctx;
 try {
 ctx.validate(createRule);
 } catch (err) {
 ctx.logger.warn(err.errors);
 ctx.body = { success: false };
 return;
 }
 }
};
</pre>

校驗規(guī)則

參數(shù)校驗通過 Parameter 完成,支持的校驗規(guī)則可以在該模塊的文檔中查閱到战得。

自定義校驗規(guī)則

除了上一節(jié)介紹的內(nèi)置檢驗類型外充边,有時候我們希望自定義一些校驗規(guī)則,讓開發(fā)時更便捷常侦,此時可以通過 app.validator.addRule(type, check) 的方式新增自定義規(guī)則浇冰。


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// app.js
app.validator.addRule('json', (rule, value) => {
 try {
 JSON.parse(value);
 } catch (err) {
 return 'must be json string';
 }
});
</pre>

添加完自定義規(guī)則之后,就可以在 Controller 中直接使用這條規(guī)則來進行參數(shù)校驗了


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">class PostController extends Controller {
 async handler() {
 const ctx = this.ctx;
 // query.test 字段必須是 json 字符串
 const rule = { test: 'json' };
 ctx.validate(rule, ctx.query);
 }
};</pre>

調(diào)用 Service

我們并不想在 Controller 中實現(xiàn)太多業(yè)務(wù)邏輯聋亡,所以提供了一個 Service 層進行業(yè)務(wù)邏輯的封裝肘习,這不僅能提高代碼的復用性,同時可以讓我們的業(yè)務(wù)邏輯更好測試坡倔。

在 Controller 中可以調(diào)用任何一個 Service 上的任何方法漂佩,同時 Service 是懶加載的脖含,只有當訪問到它的時候框架才會去實例化它。


<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, &quot;Liberation Mono&quot;, Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">class PostController extends Controller {
 async create() {
 const ctx = this.ctx;
 const author = ctx.session.userId;
 const req = Object.assign(ctx.request.body, { author });
 // 調(diào)用 service 進行業(yè)務(wù)處理
 const res = await ctx.service.post.create(req);
 ctx.body = { id: res.id };
 ctx.status = 201;
 }
}</pre>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末投蝉,一起剝皮案震驚了整個濱河市养葵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瘩缆,老刑警劉巖关拒,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異庸娱,居然都是意外死亡着绊,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門涌韩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來畔柔,“玉大人,你說我怎么就攤上這事臣樱。” “怎么了腮考?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵雇毫,是天一觀的道長。 經(jīng)常有香客問我踩蔚,道長棚放,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任馅闽,我火速辦了婚禮飘蚯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘福也。我一直安慰自己局骤,他們只是感情好,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布暴凑。 她就那樣靜靜地躺著峦甩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪现喳。 梳的紋絲不亂的頭發(fā)上凯傲,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機與錄音嗦篱,去河邊找鬼冰单。 笑死,一個胖子當著我的面吹牛灸促,可吹牛的內(nèi)容都是我干的诫欠。 我是一名探鬼主播涵卵,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼呕诉!你這毒婦竟也來了缘厢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤甩挫,失蹤者是張志新(化名)和其女友劉穎贴硫,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體伊者,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡英遭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了亦渗。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挖诸。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖法精,靈堂內(nèi)的尸體忽然破棺而出多律,到底是詐尸還是另有隱情,我是刑警寧澤搂蜓,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布狼荞,位于F島的核電站,受9級特大地震影響帮碰,放射性物質(zhì)發(fā)生泄漏相味。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一殉挽、第九天 我趴在偏房一處隱蔽的房頂上張望丰涉。 院中可真熱鬧,春花似錦斯碌、人聲如沸一死。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽摘符。三九已至,卻和暖如春策吠,著一層夾襖步出監(jiān)牢的瞬間逛裤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工猴抹, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留带族,地道東北人。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓蟀给,卻偏偏與公主長得像蝙砌,于是被迫代替她去往敵國和親阳堕。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,111評論 1 32
  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5择克? 答:HTML5是最新的HTML標準恬总。 注意:講述HT...
    kismetajun閱讀 27,518評論 1 45
  • 原文鏈接https://eggjs.org/zh-cn/intro/quickstart.html 服務(wù)(Serv...
    龍哈哈_4b89閱讀 632評論 0 1
  • 一侣诺、基本用法 1.1 架設(shè) HTTP 服務(wù) // demos/01.jsconst Koa = require('...
    majun00閱讀 1,364評論 0 5
  • 1.編碼規(guī)范 1.1 編碼格式與語法 項目默認編碼格式統(tǒng)一為UTF-8格式幽七,語法采用ES6+語法 1.2 代碼注釋...
    ZZES_ZCDC閱讀 4,638評論 6 19