原文鏈接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é)果后封裝并返回:
- 獲取用戶通過 HTTP 傳遞過來的請求參數(shù)官份。
- 校驗只厘、組裝參數(shù)。
- 調(diào)用 Service 進行業(yè)務(wù)處理舅巷,必要時處理轉(zhuǎn)換 Service 的返回結(jié)果羔味,讓它適應用戶的需求。
- 通過 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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
爱葵,warn
,error
)反浓,分別代表打印四個不同級別的日志萌丈,使用方法和效果與 context logger 中介紹的一樣,但是通過這個 logger 對象記錄的日志雷则,在日志前面會加上打印該日志的文件路徑辆雾,以便快速定位日志打印位置。
自定義 Controller 基類
按照類的方式編寫 Controller月劈,不僅可以讓我們更好的對 Controller 層代碼進行抽象(例如將一些統(tǒng)一的處理抽象成一些私有方法)度迂,還可以通過自定義 Controller 基類的方式封裝應用中常用的方法藤乙。
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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=node
中 category=egg&language=node
就是用戶傳遞過來的參數(shù)闻蛀。我們可以通過 ctx.query
拿到解析過后的這個參數(shù)體
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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/json
,application/json-patch+json
区岗,application/vnd.api+json
和application/csp-report
時略板,會按照 json 格式對請求 body 進行解析,并限制 body 最大長度為100kb
慈缔。 - 當請求的 Content-Type 為
application/x-www-form-urlencoded
時叮称,會按照 form 格式對請求 body 進行解析,并限制 body 最大長度為100kb
胀糜。 - 如果解析成功颅拦,body 一定會是一個 Object(可能是一個數(shù)組)。
一般來說我們最經(jīng)常調(diào)整的配置項就是變更解析時允許的最大長度教藻,可以在 config/config.default.js
中覆蓋框架的默認值距帅。
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", 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.body
和 ctx.body
混淆,后者其實是 ctx.response.body
的簡寫横媚。
獲取上傳的文件
請求 body 除了可以帶參數(shù)之外纠炮,還可以發(fā)送文件,一般來說灯蝴,瀏覽器上都是通過 Multipart/form-data
格式發(fā)送文件的恢口,框架通過內(nèi)置 Multipart 插件來支持獲取用戶上傳的文件,我們?yōu)槟闾峁┝藘煞N方式:
如果你完全不知道 Nodejs 中的 Stream 用法穷躁,那么 File 模式非常合適你:
1)在 config 文件中啟用 file
模式:
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", 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)上傳 / 接收文件:
- 上傳 / 接收單個文件:
你的前端靜態(tài)頁面代碼應該看上去如下樣子:
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", 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, "Liberation Mono", 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>
- 上傳 / 接收多個文件:
對于多個文件,我們借助 ctx.request.files
屬性進行遍歷,然后分別進行處理:
你的前端靜態(tài)頁面代碼應該看上去如下樣子:
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", 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, "Liberation Mono", 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>
如果你對于 Node 中的 Stream 模式非常熟悉婚被,那么你可以選擇此模式。在 Controller 中两芳,我們可以通過 ctx.getFileStream()
接口能獲取到上傳的文件流摔寨。
- 上傳 / 接受單個文件:
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", 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, "Liberation Mono", 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。
- 上傳 / 接受多個文件:
如果要獲取同時上傳的多個文件逗余,不能通過 ctx.getFileStream()
來獲取特咆,只能通過下面這種方式:
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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, "Liberation Mono", 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>