Egg學習記錄(3)

原文鏈接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, &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/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, &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/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, &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.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, &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 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, &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;">ctx.cookies.set(key, value, {
 httpOnly: false,
 signed: false,
});
</pre>

  • 如果想要 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;">ctx.cookies.set(key, value, {
 httpOnly: true, // 默認就是 true
 encrypt: true, // 加密傳輸
});
</pre>

注意:

  1. 由于瀏覽器和其他客戶端實現(xiàn)的不確定性随橘,為了保證 Cookie 可以寫入成功,建議 value 通過 base64 編碼或者其他形式 encode 之后再寫入锦庸。
  2. 由于瀏覽器對 Cookie 有長度限制限制机蔗,所以盡量不要設置太長的 Cookie。一般來說不要超過 4093 bytes酸员。當設置的 Cookie value 大于這個值時蜒车,框架會打印一條警告日志。

ctx.cookies.get(key, options)

由于 HTTP 請求中的 Cookie 是在一個 header 中傳輸過來的幔嗦,通過框架提供的這個方法可以快速的從整段 Cookie 中獲取對應的鍵值對的值酿愧。上面在設置 Cookie 的時候,我們可以設置 options.signedoptions.encrypt 來對 Cookie 進行簽名或加密邀泉,因此對應的在獲取 Cookie 的時候也要傳相匹配的選項嬉挡。

  • 如果設置的時候指定為 signed,獲取時未指定汇恤,則不會在獲取時對取到的值做驗簽庞钢,導致可能被客戶端篡改。
  • 如果設置的時候指定為 encrypt因谎,獲取時未指定基括,則無法獲取到真實的值,而是加密過后的密文财岔。

如果要獲取前端或者其他系統(tǒng)設置的 cookie风皿,需要指定參數(shù) signedfalse,避免對它做驗簽導致獲取不到 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;">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, &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 = {
 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, &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 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, &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;">ctx.session = null;
</pre>

需要 特別注意 的是:設置 session 屬性時需要避免以下幾種情況(會造成字段丟失赁炎,詳見 koa-session 源碼)

  • 不要以 _ 開頭
  • 不能為 isNew

<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;">// ? 錯誤的用法
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, &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;">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, &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
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-redisegg-session-redis 插件即可孙蒙。


<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;">// 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, &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 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, &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
module.exports = {
 session: {
 renew: true,
 },
};</pre>

?著作權歸作者所有,轉載或內容合作請聯(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
  • 正文 為了忘掉前任杠茬,我火速辦了婚禮月褥,結果婚禮上弛随,老公的妹妹穿的比我還像新娘。我一直安慰自己宁赤,他們只是感情好频丘,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布光坝。 她就那樣靜靜地躺著娄涩,像睡著了一般榔幸。 火紅的嫁衣襯著肌膚如雪途蒋。 梳的紋絲不亂的頭發(fā)上瓷马,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天糠涛,我揣著相機與錄音社搅,去河邊找鬼厉斟。 笑死挚躯,一個胖子當著我的面吹牛,可吹牛的內容都是我干的擦秽。 我是一名探鬼主播码荔,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼感挥!你這毒婦竟也來了缩搅?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤触幼,失蹤者是張志新(化名)和其女友劉穎硼瓣,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體置谦,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡堂鲤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了媒峡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瘟栖。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖谅阿,靈堂內的尸體忽然破棺而出半哟,到底是詐尸還是另有隱情,我是刑警寧澤奔穿,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布镜沽,位于F島的核電站,受9級特大地震影響贱田,放射性物質發(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

推薦閱讀更多精彩內容