原文鏈接https://eggjs.org/zh-cn/intro/quickstart.html
服務(Service)
簡單來說该窗,Service 就是在復雜業(yè)務場景下用于做業(yè)務邏輯封裝的一個抽象層,提供這個抽象有以下幾個好處:
- 保持 Controller 中的邏輯更加簡潔翼馆。
- 保持業(yè)務邏輯的獨立性东且,抽象出來的 Service 可以被多個 Controller 重復調用启具。
- 將邏輯和展現(xiàn)分離,更容易編寫測試用例珊泳,測試用例的編寫具體可以查看這里鲁冯。
使用場景
- 復雜數(shù)據(jù)的處理,比如要展現(xiàn)的信息需要從數(shù)據(jù)庫獲取色查,還要經(jīng)過一定的規(guī)則計算薯演,才能返回用戶顯示⊙砹耍或者計算完成后跨扮,更新到數(shù)據(jù)庫。
- 第三方服務的調用,比如 GitHub 信息獲取等好港。
定義 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;">// app/service/user.js
const Service = require('egg').Service;
class UserService extends Service {
async find(uid) {
const user = await this.ctx.db.query('select * from user where uid = ?', uid);
return user;
}
}
module.exports = UserService;</pre>
注意事項
- Service 文件必須放在
app/service
目錄愉镰,可以支持多級目錄,訪問的時候可以通過目錄名級聯(lián)訪問钧汹。
<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/service/biz/user.js => ctx.service.biz.user
app/service/sync_user.js => ctx.service.syncUser
app/service/HackerNews.js => ctx.service.hackerNews
</pre>
一個 Service 文件只能包含一個類丈探, 這個類需要通過
module.exports
的方式返回。Service 需要通過 Class 的方式定義拔莱,父類必須是
egg.Service
碗降。Service 不是單例,是 請求級別 的對象塘秦,框架在每次請求中首次訪問
ctx.service.xx
時延遲實例化讼渊,所以 Service 中可以通過 this.ctx 獲取到當前請求的上下文。
使用 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;">// app/router.js
module.exports = app => {
app.router.get('/user/:id', app.controller.user.info);
};
// app/controller/user.js
const Controller = require('egg').Controller;
class UserController extends Controller {
async info() {
const { ctx } = this;
const userId = ctx.params.id;
const userInfo = await ctx.service.user.find(userId);
ctx.body = userInfo;
}
}
module.exports = UserController;
// app/service/user.js
const Service = require('egg').Service;
class UserService extends Service {
// 默認不需要提供構造函數(shù)。
// constructor(ctx) {
// super(ctx); 如果需要在構造函數(shù)做一些處理须误,一定要有這句話挨稿,才能保證后面 `this.ctx`的使用。
// // 就可以直接通過 this.ctx 獲取 ctx 了
// // 還可以直接通過 this.app 獲取 app 了
// }
async find(uid) {
// 假如 我們拿到用戶 id 從數(shù)據(jù)庫獲取用戶詳細信息
const user = await this.ctx.db.query('select * from user where uid = ?', uid);
// 假定這里還有一些復雜的計算京痢,然后返回需要的信息奶甘。
const picture = await this.getPicture(uid);
return {
name: user.user_name,
age: user.age,
picture,
};
}
async getPicture(uid) {
const result = await this.ctx.curl(`http://photoserver/uid=${uid}`, { dataType: 'json' });
return result.data;
}
}
module.exports = UserService;
// curl http://127.0.0.1:7001/user/1234</pre>
Cookie
HTTP 請求都是無狀態(tài)的,但是我們的 Web 應用通常都需要知道發(fā)起請求的人是誰祭椰。為了解決這個問題臭家,HTTP 協(xié)議設計了一個特殊的請求頭:Cookie。服務端可以通過響應頭(set-cookie)將少量數(shù)據(jù)響應給客戶端方淤,瀏覽器會遵循協(xié)議將數(shù)據(jù)保存钉赁,并在下次請求同一個服務的時候帶上(瀏覽器也會遵循協(xié)議,只在訪問符合 Cookie 指定規(guī)則的網(wǎng)站時帶上對應的 Cookie 來保證安全性)携茂。
通過 ctx.cookies
橄霉,我們可以在 controller 中便捷、安全的設置和讀取 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 HomeController extends Controller {
async add() {
const ctx = this.ctx;
let count = ctx.cookies.get('count');
count = count ? Number(count) : 0;
ctx.cookies.set('count', ++count);
ctx.body = count;
}
async remove() {
const ctx = this.ctx;
ctx.cookies.set('count', null);
ctx.status = 204;
}
}
</pre>
ctx.cookies.set(key, value, options)
設置 Cookie 其實是通過在 HTTP 響應中設置 set-cookie 頭完成的,每一個 set-cookie 都會讓瀏覽器在 Cookie 中存一個鍵值對按厘。在設置 Cookie 值的同時医吊,協(xié)議還支持許多參數(shù)來配置這個 Cookie 的傳輸、存儲和權限逮京。
-
{Number} maxAge
: 設置這個鍵值對在瀏覽器的最長保存時間卿堂。是一個從服務器當前時刻開始的毫秒數(shù)。 -
{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 個參數(shù)的支持:
-
{Boolean} overwrite
:設置 key 相同的鍵值對如何處理法焰,如果設置為 true,則后設置的值會覆蓋前面設置的鲤孵,否則將會發(fā)送兩個 set-cookie 響應頭壶栋。 -
{Boolean} signed
:設置是否對 Cookie 進行簽名,如果設置為 true普监,則設置鍵值對的時候會同時對這個鍵值對的值進行簽名贵试,后面取的時候做校驗,可以防止前端對這個值進行篡改凯正。默認為 true毙玻。 -
{Boolean} encrypt
:設置是否對 Cookie 進行加密,如果設置為 true廊散,則在發(fā)送 Cookie 前會對這個鍵值對的值進行加密桑滩,客戶端無法讀取到 Cookie 的明文值。默認為 false允睹。
在設置 Cookie 時我們需要思考清楚這個 Cookie 的作用运准,它需要被瀏覽器保存多久?是否可以被 js 獲取到缭受?是否可以被前端修改胁澳?
默認的配置下,Cookie 是加簽不加密的米者,瀏覽器可以看到明文韭畸,js 不能訪問宇智,不能被客戶端(手工)篡改。
- 如果想要 Cookie 在瀏覽器端可以被 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;">ctx.cookies.set(key, value, {
httpOnly: false,
signed: false,
});
</pre>
- 如果想要 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;">ctx.cookies.set(key, value, {
httpOnly: true, // 默認就是 true
encrypt: true, // 加密傳輸
});
</pre>
注意:
- 由于瀏覽器和其他客戶端實現(xiàn)的不確定性随橘,為了保證 Cookie 可以寫入成功,建議 value 通過 base64 編碼或者其他形式 encode 之后再寫入锦庸。
- 由于瀏覽器對 Cookie 有長度限制限制机蔗,所以盡量不要設置太長的 Cookie。一般來說不要超過 4093 bytes酸员。當設置的 Cookie value 大于這個值時蜒车,框架會打印一條警告日志。
ctx.cookies.get(key, options)
由于 HTTP 請求中的 Cookie 是在一個 header 中傳輸過來的幔嗦,通過框架提供的這個方法可以快速的從整段 Cookie 中獲取對應的鍵值對的值酿愧。上面在設置 Cookie 的時候,我們可以設置 options.signed
和 options.encrypt
來對 Cookie 進行簽名或加密邀泉,因此對應的在獲取 Cookie 的時候也要傳相匹配的選項嬉挡。
- 如果設置的時候指定為 signed,獲取時未指定汇恤,則不會在獲取時對取到的值做驗簽庞钢,導致可能被客戶端篡改。
- 如果設置的時候指定為 encrypt因谎,獲取時未指定基括,則無法獲取到真實的值,而是加密過后的密文财岔。
如果要獲取前端或者其他系統(tǒng)設置的 cookie风皿,需要指定參數(shù) signed
為 false
,避免對它做驗簽導致獲取不到 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;">ctx.cookies.get('frontend-cookie', {
signed: false,
});
</pre>
Cookie 秘鑰
由于我們在 Cookie 中需要用到加解密和驗簽桐款,所以需要配置一個秘鑰供加密使用。在 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 = {
keys: 'key1,key2',
};
</pre>
keys 配置成一個字符串夷恍,可以按照逗號分隔配置多個 key魔眨。Cookie 在使用這個配置進行加解密時:
- 加密和加簽時只會使用第一個秘鑰。
- 解密和驗簽時會遍歷 keys 進行解密酿雪。
如果我們想要更新 Cookie 的秘鑰遏暴,但是又不希望之前設置到用戶瀏覽器上的 Cookie 失效,可以將新的秘鑰配置到 keys 最前面指黎,等過一段時間之后再刪去不需要的秘鑰即可朋凉。
Session
Cookie 在 Web 應用中經(jīng)常承擔標識請求方身份的功能,所以 Web 應用在 Cookie 的基礎上封裝了 Session 的概念袋励,專門用做用戶身份識別。
框架內置了 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 HomeController 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) : 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;">ctx.session = null;
</pre>
需要 特別注意 的是:設置 session 屬性時需要避免以下幾種情況(會造成字段丟失赁炎,詳見 koa-session 源碼)
- 不要以
_
開頭 - 不能為
isNew
<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;">// ? 錯誤的用法
ctx.session._visited = 1; // --> 該字段會在下一次請求時丟失
ctx.session.isNew = 'HeHe'; // --> 為內部關鍵字, 不應該去更改
// ?? 正確的用法
ctx.session.visited = 1; // --> 此處沒有問題
</pre>
Session 的實現(xiàn)是基于 Cookie 的,默認配置下钾腺,用戶 Session 的內容加密后直接存儲在 Cookie 中的一個字段中徙垫,用戶每次請求我們網(wǎng)站的時候都會帶上這個 Cookie,我們在服務端解密后使用放棒。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;">exports.session = {
key: 'EGG_SESS',
maxAge: 24 * 3600 * 1000, // 1 天
httpOnly: true,
encrypt: true,
};
</pre>
可以看到這些參數(shù)除了 key
都是 Cookie 的參數(shù)姻报,key
代表了存儲 Session 的 Cookie 鍵值對的 key 是什么。在默認的配置下间螟,存放 Session 的 Cookie 將會加密存儲吴旋、不可被前端 js 訪問,這樣可以保證用戶的 Session 是安全的厢破。
擴展存儲
Session 默認存放在 Cookie 中荣瑟,但是如果我們的 Session 對象過于龐大,就會帶來一些額外的問題:
- 前面提到摩泪,瀏覽器通常都有限制最大的 Cookie 長度笆焰,當設置的 Session 過大時,瀏覽器可能拒絕保存见坑。
- Cookie 在每次請求時都會帶上嚷掠,當 Session 過大時,每次請求都要額外帶上龐大的 Cookie 信息鳄梅。
框架提供了將 Session 存儲到除了 Cookie 之外的其他存儲的擴展方案叠国,我們只需要設置 app.sessionStore
即可將 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;">// app.js
module.exports = app => {
app.sessionStore = {
// support promise / async
async get (key) {
// return value;
},
async set (key, value, maxAge) {
// set key to store
},
async destroy (key) {
// destroy key
},
};
};
</pre>
sessionStore 的實現(xiàn)我們也可以封裝到插件中戴尸,例如 egg-session-redis 就提供了將 Session 存儲到 redis 中的能力粟焊,在應用層,我們只需要引入 egg-redis 和 egg-session-redis 插件即可孙蒙。
<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;">// plugin.js
exports.redis = {
enable: true,
package: 'egg-redis',
};
exports.sessionRedis = {
enable: true,
package: 'egg-session-redis',
};
</pre>
注意:一旦選擇了將 Session 存入到外部存儲中项棠,就意味著系統(tǒng)將強依賴于這個外部存儲,當它掛了的時候挎峦,我們就完全無法使用 Session 相關的功能了香追。因此我們更推薦大家只將必要的信息存儲在 Session 中,保持 Session 的精簡并使用默認的 Cookie 存儲坦胶,用戶級別的緩存不要存儲在 Session 中透典。
Session 實踐
修改用戶 Session 失效時間
雖然在 Session 的配置中有一項是 maxAge晴楔,但是它只能全局設置 Session 的有效期,我們經(jīng)城椭洌可以在一些網(wǎng)站的登陸頁上看到有 記住我 的選項框税弃,勾選之后可以讓登陸用戶的 Session 有效期更長。這種針對特定用戶的 Session 有效時間設置我們可以通過 ctx.session.maxAge=
來實現(xiàn)凑队。
<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 ms = require('ms');
class UserController extends Controller {
async login() {
const ctx = this.ctx;
const { username, password, rememberMe } = ctx.request.body;
const user = await ctx.loginAndGetUser(username, password);
// 設置 Session
ctx.session.user = user;
// 如果用戶勾選了 `記住我`则果,設置 30 天的過期時間
if (rememberMe) ctx.session.maxAge = ms('30d');
}
}
</pre>
延長用戶 Session 有效期
默認情況下,當用戶請求沒有導致 Session 被修改時漩氨,框架都不會延長 Session 的有效期西壮,但是在有些場景下,我們希望用戶如果長時間都在訪問我們的站點叫惊,則延長他們的 Session 有效期款青,不讓用戶退出登錄態(tài)「撤茫框架提供了一個 renew
配置項用于實現(xiàn)此功能可都,它會在發(fā)現(xiàn)當用戶 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;">// config/config.default.js
module.exports = {
session: {
renew: true,
},
};</pre>