回顧一下上篇講到的內容,上篇講了:css
一個 Web 應用自己應該是無狀態的,並擁有根據運行環境設置自身的能力。html
config/env
文件指定,該文件的內容就是運行環境,如 prod
EGG_SERVER_ENV
環境變量指定方式 2 比較經常使用,由於經過 EGG_SERVER_ENV
環境變量指定運行環境更加方便,好比在生產環境啓動應用:node
EGG_SERVER_ENV=prod npm start
複製代碼
使用 app.config.env
獲取ios
框架默認支持的運行環境及映射關係(若是未指定 EGG_SERVER_ENV
會根據 NODE_ENV
來匹配)git
NODE_ENV | EGG_SERVER_ENV | 說明 |
---|---|---|
local | 本地開發環境 | |
test | unittest | 單元測試 |
production | prod | 生產環境 |
當 NODE_ENV
爲 production
而 EGG_SERVER_ENV
未指定時,框架會將 EGG_SERVER_ENV
設置成 prod
。github
常規開發流程可能不只僅只有以上幾種環境,Egg 支持自定義環境來適應本身的開發流程。web
好比,要爲開發流程增長集成測試環境 SIT。將 EGG_SERVER_ENV
設置成 sit
(並建議設置 NODE_ENV = production
),啓動時會加載 config/config.sit.js
,運行環境變量 app.config.env
會被設置成 sit
。npm
在 Koa 中咱們經過 app.env
來進行環境判斷,app.env
默認的值是 process.env.NODE_ENV
。json
在 Egg(和基於 Egg 的框架)中,配置統一都放置在 app.config
上,因此咱們須要經過 app.config.env
來區分環境,app.env
再也不使用。後端
框架提供了強大且可擴展的配置功能,能夠自動合併應用、插件、框架的配置,按順序覆蓋,且能夠根據環境維護不一樣的配置。合併後的配置可直接從 app.config
獲取。
Egg 選擇了配置即代碼,配置的變動也應該通過 review 後才能發佈。應用包自己是能夠部署在多個環境的,只須要指定運行環境便可。
框架支持根據環境來加載配置,定義多個環境的配置文件
config
|- config.default.js
|- config.prod.js
|- config.unittest.js
|- config.local.js
複製代碼
config.default.js
爲默認的配置文件,全部環境都會加載這個配置文件,通常也會做爲開發環境的默認配置文件。
當指定 env 時會同時加載對應的配置文件,並覆蓋默認配置文件的同名配置。如 prod 環境會加載 config.prod.js
和 config.default.js
文件,config.prod.js
會覆蓋 config.default.js
的同名配置。
配置文件返回的是一個 object 對象,能夠覆蓋框架的一些配置,應用也能夠將本身業務的配置放到這裏方便管理,獲取時直接經過 app.config
。
導出對象式寫法
// 配置 logger 文件的目錄,logger 默認配置由框架提供
module.exports = {
logger: {
dir: '/home/admin/logs/demoapp',
},
};
複製代碼
配置文件也能夠返回一個 function,能夠接受 appInfo
參數
// 將 logger 目錄放到代碼目錄下
const path = require('path');
module.exports = appInfo => {
return {
logger: {
dir: path.join(appInfo.baseDir, 'logs'),
},
};
};
複製代碼
內置的 appInfo 有:
appInfo | 說明 |
---|---|
pkg | package.json |
name | 應用名,同 pkg.name |
baseDir | 應用代碼的目錄 |
HOME | 用戶目錄,如 admin 帳戶爲 /home/admin |
root | 應用根目錄,只有在 local 和 unittest 環境下爲 baseDir,其餘都爲 HOME。 |
appInfo.root
是一個優雅的適配,好比在服務器環境咱們會使用 /home/admin/logs
做爲日誌目錄,而本地開發時又不想污染用戶目錄,這樣的適配就很好解決這個問題。
配置的合併使用 extend2 模塊進行深度拷貝
const a = {
arr: [ 1, 2 ],
};
const b = {
arr: [ 3 ],
};
extend(true, a, b);
// => { arr: [ 3 ] }
// 框架直接覆蓋數組而不是進行合併。
複製代碼
框架在啓動時會把合併後的最終配置 dump 到 run/application_config.json
(worker 進程)和 run/agent_config.json
(agent 進程)中,能夠用來分析問題。
Egg 是基於 Koa 實現的,因此 Egg 的中間件形式和 Koa 的中間件形式是同樣的,都是基於洋蔥圈模型。每次咱們編寫一箇中間件,就至關於在洋蔥外面包了一層。
中間件內容的寫法
// app/middleware/gzip.js
const isJSON = require('koa-is-json');
const zlib = require('zlib');
async function gzip(ctx, next) {
await next();
// 後續中間件執行完成後將響應體轉換成 gzip
let body = ctx.body;
if (!body) return;
if (isJSON(body)) body = JSON.stringify(body);
// 設置 gzip body,修正響應頭
const stream = zlib.createGzip();
stream.end(body);
ctx.body = stream;
ctx.set('Content-Encoding', 'gzip');
}
複製代碼
框架的中間件和 Koa 的中間件寫法是如出一轍的,因此任何 Koa 的中間件均可以直接被框架使用。
通常來講中間件也會有本身的配置。
咱們約定一箇中間件是一個放置在 app/middleware 目錄下的單獨文件,它須要 exports 一個普通的 function,接受兩個參數:
app.config[${middlewareName}]
傳遞進來,因此能夠直接獲取到配置文件中的中間件配置將上面的 gzip 中間件作一個簡單的優化,讓它支持指定只有當 body 大於配置的 threshold 時才進行 gzip 壓縮,咱們要在 app/middleware
目錄下新建一個文件 gzip.js
// app/middleware/gzip.js
const isJSON = require('koa-is-json');
const zlib = require('zlib');
module.exports = options => {
return async function gzip(ctx, next) {
await next();
// 後續中間件執行完成後將響應體轉換成 gzip
let body = ctx.body;
if (!body) return;
// 支持 options.threshold
if (options.threshold && ctx.length < options.threshold) return;
if (isJSON(body)) body = JSON.stringify(body);
// 設置 gzip body,修正響應頭
const stream = zlib.createGzip();
stream.end(body);
ctx.body = stream;
ctx.set('Content-Encoding', 'gzip');
};
};
複製代碼
中間件編寫完成後,咱們還須要手動掛載
在 config.default.js
中加入下面的配置就完成了中間件的開啓和配置
module.exports = {
// 配置須要的中間件,數組順序即爲中間件的加載順序
middleware: [ 'gzip' ],
// 配置 gzip 中間件的配置
gzip: {
threshold: 1024, // 小於 1k 的響應體不壓縮
},
// options.gzip.threshold
};
複製代碼
該配置最終將在啓動時合併到 app.config.appMiddleware
框架和插件不支持在 config.default.js
中匹配 middleware
,須要經過如下方式:
// app.js
module.exports = app => {
// 在中間件最前面統計請求時間
app.config.coreMiddleware.unshift('report');
};
// app/middleware/report.js
module.exports = () => {
return async function (ctx, next) {
const startTime = Date.now();
await next();
// 上報請求時間
reportTime(Date.now() - startTime);
}
};
複製代碼
應用層定義的中間件(app.config.appMiddleware
)和框架默認中間件(app.config.coreMiddleware
)都會被加載器加載,並掛載到 app.middleware
上。
以上兩種方式配置的中間件是全局的,會處理每一次請求
若是你只想針對單個路由生效,能夠直接在 app/router.js
中實例化和掛載,以下:
module.exports = app => {
const gzip = app.middleware.gzip({ threshold: 1024 });
app.router.get('/needgzip', gzip, app.controller.handler);
};
複製代碼
除了應用層加載中間件以外,框架自身和其餘的插件也會加載許多中間件。
全部的這些自帶中間件的配置項都經過在配置中修改中間件同名配置項進行修改。
框架自帶的中間件中有一個 bodyParser 中間件(框架的加載器會將文件名中的各類分隔符都修改爲駝峯形式的變量名),咱們想要修改 bodyParser 的配置,只須要在 config/config.default.js
中編寫
module.exports = {
bodyParser: {
jsonLimit: '10mb',
},
};
複製代碼
在框架裏面能夠很是容易的引入 Koa 中間件生態。
以 koa-compress 爲例,在 Koa 中使用時:
const koa = require('koa');
const compress = require('koa-compress');
const app = koa();
const options = { threshold: 2048 };
app.use(compress(options));
複製代碼
Egg
// app/middleware/compress.js
// koa-compress 暴露的接口(`(options) => middleware`)和框架對中間件要求一致
module.exports = require('koa-compress');
複製代碼
配置中間件
// config/config.default.js
module.exports = {
middleware: [ 'compress' ],
compress: {
threshold: 2048,
},
}
複製代碼
match 和 ignore 支持的參數都同樣,只是做用徹底相反,match 和 ignore 不容許同時配置。
module.exports = {
gzip: {
match: '/static',
},
};
module.exports = {
gzip: {
match(ctx) {
// 只有 ios 設備纔開啓
const reg = /iphone|ipad|ipod/i;
return reg.test(ctx.get('user-agent'));
},
},
};
複製代碼
匹配規則,摘自 egg-path-matching
const pathMatching = require('egg-path-matching');
const options = {
ignore: '/api', // string will use parsed by path-to-regexp
// support regexp
ignore: /^\/api/,
// support function
ignore: ctx => ctx.path.startsWith('/api'),
// support Array
ignore: [ ctx => ctx.path.startsWith('/api'), /^\/foo$/, '/bar'],
// support match or ignore
match: '/api',
};
複製代碼
Router 主要用來描述請求 URL 和具體承擔執行動做的 Controller 的對應關係, 框架約定了 app/router.js
文件用於統一全部路由規則。
app/router.js
裏面定義 URL 路由規則// app/router.js
module.exports = app => {
const { router, controller } = app;
router.get('/user/:id', controller.user.info);
};
複製代碼
app/controller
目錄下面實現 Controller// app/controller/user.js
class UserController extends Controller {
async info() {
const { ctx } = this;
ctx.body = {
name: `hello ${ctx.params.id}`,
};
}
}
複製代碼
下面是路由的完整定義,參數能夠根據場景的不一樣,自由選擇:
router.verb('path-match', app.controller.action);
router.verb('router-name', 'path-match', app.controller.action);
router.verb('path-match', middleware1, ..., middlewareN, app.controller.action);
router.verb('router-name', 'path-match', middleware1, ..., middlewareN, app.controller.action);
複製代碼
路由完整定義主要包括5個主要部分:
router.head
- HEADrouter.options
- OPTIONSrouter.get
- GETrouter.put
- PUTrouter.post
- POSTrouter.patch
- PATCHrouter.delete
- DELETErouter.del
- 因爲 delete 是一個保留字,因此提供了一個 delete 方法的別名。router.redirect
- 能夠對 URL 進行重定向處理,好比咱們最常用的能夠把用戶訪問的根目錄路由到某個主頁。app.controller.user.fetch
- 直接指定一個具體的 controller'user.fetch'
- 能夠簡寫爲字符串形式${fileName}.${functionName}
的方式指定對應的 Controller。${directoryName}.${fileName}.${functionName}
的方式制定對應的 Controller。demo
// app/router.js
module.exports = app => {
const { router, controller } = app;
router.get('/home', controller.home);
router.get('/user/:id', controller.user.page);
router.post('/admin', isAdmin, controller.admin);
router.post('/user', isLoginUser, hasAdminPermission, controller.user.create);
router.post('/api/v1/comments', controller.v1.comments.create); // app/controller/v1/comments.js
};
複製代碼
提供了 app.resources('routerName', 'pathMatch', controller)
快速在一個路徑上生成 CRUD 路由結構。
// app/router.js
module.exports = app => {
const { router, controller } = app;
router.resources('posts', '/api/posts', controller.posts);
router.resources('users', '/api/v1/users', controller.v1.users); // app/controller/v1/users.js
};
複製代碼
上面代碼就在 /posts
路徑上部署了一組 CRUD 路徑結構,對應的 Controller 爲 app/controller/posts.js
接下來, 你只須要在 posts.js
裏面實現對應的函數就能夠了。
Method | Path | Route Name | Controller.Action |
---|---|---|---|
GET | /posts | posts | app.controllers.posts.index |
GET | /posts/new | new_post | app.controllers.posts.new |
GET | /posts/:id | post | app.controllers.posts.show |
GET | /posts/:id/edit | edit_post | app.controllers.posts.edit |
POST | /posts | posts | app.controllers.posts.create |
PUT | /posts/:id | post | app.controllers.posts.update |
DELETE | /posts/:id | post | app.controllers.posts.destroy |
// app/controller/posts.js
exports.index = async () => {};
exports.new = async () => {};
exports.create = async () => {};
exports.show = async () => {};
exports.edit = async () => {};
exports.update = async () => {};
exports.destroy = async () => {};
複製代碼
若是咱們不須要其中的某幾個方法,能夠不用在 posts.js 裏面實現,這樣對應 URL 路徑也不會註冊到 Router。
Query String 方式
ctx.query.xxx
// app/router.js
module.exports = app => {
app.router.get('/search', app.controller.search.index);
};
// app/controller/search.js
exports.index = async ctx => {
ctx.body = `search: ${ctx.query.name}`;
};
// curl http://127.0.0.1:7001/search?name=egg
複製代碼
參數命名方式
/user/:id/:name
=> ctx.params.xxx
// app/router.js
module.exports = app => {
app.router.get('/user/:id/:name', app.controller.user.info);
};
// app/controller/user.js
exports.info = async ctx => {
ctx.body = `user: ${ctx.params.id}, ${ctx.params.name}`;
};
// curl http://127.0.0.1:7001/user/123/xiaoming
複製代碼
複雜參數的獲取
路由裏面也支持定義正則,能夠更加靈活的獲取參數:
// app/router.js
module.exports = app => {
app.router.get(/^\/package\/([\w-.]+\/[\w-.]+)$/, app.controller.package.detail);
};
// app/controller/package.js
exports.detail = async ctx => {
// 若是請求 URL 被正則匹配, 能夠按照捕獲分組的順序,從 ctx.params 中獲取。
// 按照下面的用戶請求,`ctx.params[0]` 的 內容就是 `egg/1.0.0`
ctx.body = `package:${ctx.params[0]}`;
};
// curl http://127.0.0.1:7001/package/egg/1.0.0
複製代碼
// app/router.js
module.exports = app => {
app.router.post('/form', app.controller.form.post);
};
// app/controller/form.js
exports.post = async ctx => {
ctx.body = `body: ${JSON.stringify(ctx.request.body)}`;
};
複製代碼
這裏直接發起 POST 請求會報錯。
上面的校驗是由於框架中內置了安全插件 egg-security,提供了一些默認的安全實踐,而且框架的安全插件是默認開啓的,若是須要關閉其中一些安全防範,直接設置該項的 enable 屬性爲 false 便可。
暫時關閉 csrf
exports.security = {
csrf: false
};
複製代碼
npm i -S egg-validate
複製代碼
// config/plugin.js
module.exports = {
// had enabled by egg
// static: {
// enable: true,
// }
validate: {
enable: true,
package: 'egg-validate',
},
};
複製代碼
// app/router.js
module.exports = app => {
app.router.resources('/user', app.controller.user);
};
// app/controller/user.js
const createRule = {
username: {
type: 'email',
},
password: {
type: 'password',
compare: 're-password',
},
};
exports.create = async ctx => {
// 若是校驗報錯,會拋出異常
ctx.validate(createRule);
ctx.body = ctx.request.body;
};
複製代碼
內部重定向
// app/router.js
module.exports = app => {
app.router.get('index', '/home/index', app.controller.home.index);
app.router.redirect('/', '/home/index', 303); // 訪問 / 自動重定向到 /home/index
};
// app/controller/home.js
exports.index = async ctx => {
ctx.body = 'hello controller';
};
複製代碼
外部重定向
exports.index = async ctx => {
const type = ctx.query.type;
const q = ctx.query.q || 'nodejs';
if (type === 'bing') {
ctx.redirect(`http://cn.bing.com/search?q=${q}`);
} else {
ctx.redirect(`https://www.google.co.kr/search?q=${q}`);
}
};
複製代碼
Controller 負責解析用戶的輸入,處理後返回相應的結果。
框架推薦 Controller 層主要對用戶的請求參數進行處理(校驗、轉換),而後調用對應的 service 方法處理業務,獲得業務結果後封裝並返回:
全部的 Controller 文件都必須放在 app/controller 目錄下,能夠支持多級目錄,訪問的時候能夠經過目錄名級聯訪問
// app/controller/post.js
const Controller = require('egg').Controller;
class PostController extends Controller {
async create() {
const { ctx } = this;
ctx.body = 'PostController';
ctx.status = 201;
}
}
module.exports = PostController;
複製代碼
上面定義的方法,能夠在路由中經過 app.controller 根據文件名和方法名定位到它
// app/router.js
module.exports = app => {
const { router, controller } = app;
router.post('createPost', '/api/posts', controller.post.create);
}
複製代碼
Controller 支持多級目錄,例如若是咱們將上面的 Controller 代碼放到 app/controller/sub/post.js
中,則
app.router.post('createPost', '/api/posts', app.controller.sub.post.create);
複製代碼
屬性介紹 定義的 Controller 類,會在每個請求訪問到 server 時實例化一個全新的對象,而項目中的 Controller 類繼承於 egg.Controller
,會有下面幾個屬性掛在 this
上。
this.ctx
: 當前請求的上下文 Context 對象的實例,經過它咱們能夠拿到框架封裝好的處理當前請求的各類便捷屬性和方法。this.app
: 當前應用 Application 對象的實例,經過它咱們能夠拿到框架提供的全局對象和方法。this.service
:應用定義的 Service,經過它咱們能夠訪問到抽象出的業務層,等價於 this.ctx.service 。this.config
:應用運行時的配置項。this.logger
:logger 對象,上面有四個方法(debug
,info
,warn
,error
),分別表明打印四個不一樣級別的日誌,使用方法和效果與 context logger 中介紹的同樣,可是經過這個 logger 對象記錄的日誌,在日誌前面會加上打印該日誌的文件路徑,以便快速定位日誌打印位置。自定義 Controller 基類
// 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;
複製代碼
此時在編寫應用的 Controller 時,能夠繼承 BaseController,直接使用基類上的方法:
//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); // 使用基類的方法
}
}
複製代碼
每個 Controller 都是一個 async function,它的入參爲請求的上下文 Context 對象的實例,經過它咱們能夠拿到框架封裝好的各類便捷屬性和方法。
// app/controller/post.js
exports.create = async ctx => {
const createRule = {
title: { type: 'string' },
content: { type: 'string' },
};
// 校驗參數
ctx.validate(createRule);
// 組裝參數
const author = ctx.session.userId;
const req = Object.assign(ctx.request.body, { author });
// 調用 service 進行業務處理
const res = await ctx.service.post.create(req);
// 設置響應內容和響應狀態碼
ctx.body = { id: res.id };
ctx.status = 201;
};
複製代碼
class PostController extends Controller {
async listPosts() {
const query = this.ctx.query;
// {
// category: 'egg',
// language: 'node',
// }
}
}
複製代碼
當 Query String 中的 key 重複時,ctx.query
只取 key 第一次出現時的值,後面再出現的都會被忽略。
有時候咱們的系統會設計成讓用戶傳遞相同的 key,例如 GET /posts?category=egg&id=1&id=2&id=3
。
架提供了 ctx.queries
對象,這個對象也解析了 Query String,可是它不會丟棄任何一個重複的數據,而是將他們都放到一個數組中。
// 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' ],
// }
}
}
複製代碼
Router 上也能夠申明參數,這些參數均可以經過 ctx.params
獲取到。
// 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');
}
}
複製代碼
框架內置了 bodyParser 中間件來對這兩類格式的請求 body 解析成 object 掛載到 ctx.request.body
上。
通常來講咱們最常常調整的配置項就是變動解析時容許的最大長度,能夠在 config/config.default.js
中覆蓋框架的默認值。
module.exports = {
bodyParser: {
jsonLimit: '1mb',
formLimit: '1mb',
},
};
複製代碼
若是用戶的請求 body 超過了咱們配置的解析最大長度,會拋出一個狀態碼爲 413
(Request Entity Too Large) 的異常,若是用戶請求的 body 解析失敗(錯誤的 JSON),會拋出一個狀態碼爲 400
(Bad Request) 的異常。
一個常見的錯誤是把 ctx.request.body
和 ctx.body
混淆,後者實際上是 ctx.response.body
的簡寫。
通常來講,瀏覽器上都是經過 Multipart/form-data
格式發送文件的,框架經過內置 Multipart 插件來支持獲取用戶上傳的文件。
在 config 文件中啓用 file 模式:
// config/config.default.js
exports.multipart = {
mode: 'file',
};
複製代碼
上傳 / 接收單個文件:
<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>
複製代碼
對應的後端代碼以下:
// 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,
};
}
};
複製代碼
上傳 / 接收多個文件:
對於多個文件,咱們藉助 ctx.request.files
屬性進行遍歷,而後分別進行處理:
<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>
複製代碼
對應的後端代碼:
// 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);
}
}
};
複製代碼
爲了保證文件上傳的安全,框架限制了支持的的文件格式,框架默認支持白名單以下:
// 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',
複製代碼
用戶能夠經過在 config/config.default.js
中配置來新增支持的文件擴展名,或者重寫整個白名單。
module.exports = {
multipart: {
fileExtensions: [ '.apk' ] // 增長對 apk 擴展名的文件支持
},
};
複製代碼
module.exports = {
multipart: {
whitelist: [ '.png' ], // 覆蓋整個白名單,只容許上傳 '.png' 格式
},
};
複製代碼
除了從 URL 和請求 body 上獲取參數以外,還有許多參數是經過請求 header 傳遞的。
ctx.headers
,ctx.header
,ctx.request.headers
,ctx.request.header
:這幾個方法是等價的,都是獲取整個 header 對象。ctx.get(name)
,ctx.request.get(name)
:獲取請求 header 中的一個字段的值,若是這個字段不存在,會返回空字符串。ctx.get(name)
而不是 ctx.headers['name']
,由於前者會自動處理大小寫。經過 ctx.cookies
,咱們能夠在 Controller 中便捷、安全的設置和讀取 Cookie。
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;
}
}
複製代碼
經過 Cookie,咱們能夠給每個用戶設置一個 Session,用來存儲用戶身份相關的信息,這份信息會加密後存儲在 Cookie 中,實現跨請求的用戶身份保持。
框架內置了 Session 插件,給咱們提供了 ctx.session 來訪問或者修改當前用戶 Session 。
class PostController extends Controller {
async fetchPosts() {
const ctx = this.ctx;
// 獲取 Session 上的內容
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,
};
}
}
複製代碼
Session 的使用方法很是直觀,直接讀取它或者修改它就能夠了,若是要刪除它,直接將它賦值爲 null
:
class SessionController extends Controller {
async deleteSession() {
this.ctx.session = null;
}
};
複製代碼
配置
module.exports = {
key: 'EGG_SESS', // 承載 Session 的 Cookie 鍵值對名字
maxAge: 86400000, // Session 的最大有效時間
};
複製代碼
藉助 Validate 插件提供便捷的參數校驗機制,幫助咱們完成各類複雜的參數校驗。
由於是插件,因此要完成插件的註冊:
// config/plugin.js
exports.validate = {
enable: true,
package: 'egg-validate',
};
複製代碼
經過 ctx.validate(rule, [body])
直接對參數進行校驗:
this.ctx.validate({
title: { type: 'string' },
content: { type: 'string' },
});
複製代碼
當校驗異常時,會直接拋出一個異常,異常的狀態碼爲422
(Unprocessable Entity),errors 字段包含了詳細的驗證不經過信息。若是想要本身處理檢查的異常,能夠經過 try catch
來自行捕獲。
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;
}
}
};
複製代碼
參數校驗經過 Parameter 完成,支持的校驗規則能夠在該模塊的文檔中查閱到。
能夠經過 app.validator.addRule(type, check)
的方式新增自定義規則。
// app.js
app.validator.addRule('json', (rule, value) => {
try {
JSON.parse(value);
} catch (err) {
return 'must be json string';
}
});
複製代碼
在 Controller 中能夠調用任何一個 Service 上的任何方法,同時 Service 是懶加載的,只有當訪問到它的時候框架纔會去實例化它。
ctx.status = 201;
複製代碼
絕大多數的數據都是經過 body 發送給請求方的,和請求中的 body 同樣,在響應中發送的 body,也須要有配套的 Content-Type 告知客戶端如何對數據進行解析。
ctx.body
是 ctx.response.body
的簡寫,不要和 ctx.request.body
混淆了。
class ViewController extends Controller {
async show() {
this.ctx.body = {
name: 'egg',
category: 'framework',
language: 'Node.js',
};
}
async page() {
this.ctx.body = '<html><h1>Hello</h1></html>';
}
}
複製代碼
因爲 Node.js 的流式特性,咱們還有不少場景須要經過 Stream 返回響應,例如返回一個大文件,代理服務器直接返回上游的內容,框架也支持直接將 body 設置成一個 Stream,並會同時處理好這個 Stream 上的錯誤事件。
class ProxyController extends Controller {
async proxy() {
const ctx = this.ctx;
const result = await ctx.curl(url, {
streaming: true,
});
ctx.set(result.header);
// result.res 是一個 stream
ctx.body = result.res;
}
};
複製代碼
經過 ctx.set(key, value)
方法能夠設置一個響應頭,ctx.set(headers)
設置多個 Header。
框架經過 security 插件覆蓋了 koa 原生的 ctx.redirect
實現,以提供更加安全的重定向。
ctx.redirect(url)
若是不在配置的白名單域名內,則禁止跳轉。ctx.unsafeRedirect(url)
不判斷域名,直接跳轉,通常不建議使用,明確瞭解可能帶來的風險後使用。用戶若是使用 ctx.redirect
方法,須要在應用的配置文件中作以下配置:
// config/config.default.js
exports.security = {
domainWhiteList:['.domain.com'], // 安全白名單,以 . 開頭
};
複製代碼
若用戶沒有配置 domainWhiteList
或者 domainWhiteList
數組內爲空,則默認會對全部跳轉請求放行,即等同於 ctx.unsafeRedirect(url)