PWA輔助工具sw-precache、sw-toolbox簡易教程

簡要說明

sw-precache 用來處理預(yù)緩存
sw-toolbox 用來處理運行時緩存
sw-precache 默認集成了 sw-toolbox

如果你是太長不想看悔醋,下面是使用sw-precache的配置說明

先看一下開發(fā)目錄

Working
├─ app
│  ├─ css
│  ├─ images
│  ├─ js
│  ├─ index.html
│  ├─ manifest.json
│  ├─ serviceworker.js
│  ├─ sync.js
│  └─ config.js
├─ node_modules
│  └─ ....
└─ gulpfile.js

gulp的配置

'use strict';

var gulp = require('gulp');
var path = require('path');
var swPrecache = require('sw-precache');

gulp.task('make-service-worker', function(callback) {
    var rootDir = 'app';  // 開發(fā)文件和工程文件隔離
    swPrecache.write(path.join(rootDir, 'serviceworker.js'), {
        staticFileGlobs: [rootDir + '/**/*.{html,css,png,jpg,gif}',
                          rootDir + '/js/*.js'],  // 避免serviceworker被緩存
        // staticFileGlobs: 需預(yù)緩存的靜態(tài)資源摩窃,路徑是相對于gulpfile的路徑

        stripPrefix: rootDir,  
        // stripPrefix: 跳過的前綴,不加的話生成的serviceworker中尋找緩存資源路徑中都會帶上'app'
        // 因為gulpfile和最終生成的serviceworker不在一個路徑下,gulpfile中尋找資源的路徑必然不能與生成的serviceworker中一致

        importScripts: ['config.js', 'sync.js'],  
        // importScripts: 在servicerworker中引入直接js的文件

        navigateFallback: 'message.html',
        // navigateFallback: 在尋找資源網(wǎng)絡(luò)訪問失敗時默認回退到的url(測試不可用)


        /*以上都是預(yù)緩存的內(nèi)容猾愿,下面runtimeCaching是運行時緩存鹦聪,由sw-toolbox控制的*/
        /*urlPattern: 支持以正則的形式捕獲http請求*/
        /*handler: 處理請求的策略,共有五種:cacheOnly, networkOnly, cacheFirst, networkFirst,  Fastest*/
        /*options: 可選參數(shù)蒂秘,這里我們給每一類緩存用不同的緩存名稱存儲泽本,方便查找*/        
        runtimeCaching: [
        {
            urlPattern: /https:\/\/www\.reddit\.com\/api\/subreddits_by_topic.json?query=javascript/,
            handler: 'cacheOnly',
            options: {
                cache: {
                    name: 'subreddits'
                }
            }
        },
        {
            urlPattern: /https:\/\/www\.reddit\.com\/r\/\w{1,255}\.json/,
            handler: 'networkFirst',
            options: {
                cache: {
                    name: 'titles'
                }
            }
        },
        {
            urlPattern: /https:\/\/www\.reddit\.com\/r\/[javascript|node|reactnative|reactjs|web_design]\/comments\/\w{6}\/[\w]{0,255}\.json/,
            handler: 'cacheFirst',
            options: {
                cache: {
                    name: 'articles'
                }
              }
        }],
        verbose: true  // 為每個緩存打出日志
    }, callback);
});

<br />

好吧詳細點來說

一、總覽

本教程將展示如何使用兩個google的pwa輔助庫sw-precachesw-toolbox來幫助更加快速和簡單的創(chuàng)建service worker姻僧。這兩個庫可以分開使用也可以一起使用规丽,本教程將使用gulp task來利用這兩個庫創(chuàng)建service woreker。
本教程使用了一個小型的pwa應(yīng)用 Redder——Redder是Reddit的客戶端段化,用來讀JavaScript相關(guān)的文章嘁捷。Redder在app中讀取Reddit的文章,在新網(wǎng)頁中讀取其他網(wǎng)站的文章显熏。

二雄嚣、初始化工程
  1. 下載工程
    https://github.com/NowhereToRun/PWA_caching-with-libraries/archive/master.zip
  2. 設(shè)置工作區(qū)
$ cd caching-with-libraries
$ mkdir work
$ cp -r step-02/* work
$ cd work
  1. 安裝&運行web server
    Chrome Web Server(需翻墻訪問谷歌商店)( 或者別的HTTP Server,自行啟動)
    點擊CHOOSE FOLDER喘蟆,選擇路徑work/app缓升,勾選上Automatically show index.html
    打開對應(yīng)的URL,即可看到基本的網(wǎng)頁蕴轨。
  2. 安裝依賴庫
$ cd work
$ npm init
$ npm install --save-dev sw-precache

本工程使用gulp來構(gòu)建港谊,如果沒有安裝gulp,還需以下命令進行安裝

npm install gulp-cli -g
npm install gulp --save-dev
三橙弱、相關(guān)背景

在創(chuàng)建工程之前歧寺,需要明確幾個問題:

  • 我們需要緩存什么資源
  • 什么時候需要進行緩存
  • 怎么緩存

看一下網(wǎng)頁的結(jié)構(gòu)


網(wǎng)頁布局

所有可以緩存的資源可能有以下這些:

  • 基礎(chǔ)的資源,特別是HTML棘脐、CSS斜筐、Images、可能還需要JS
  • 與JS相關(guān)的子目錄列表
  • 文章鏈接和標題
  • 文章內(nèi)容
緩存類型

預(yù)緩存 Precaching
我們需要預(yù)緩存APP需要立即使用的資源蛀缝,并且隨著版本更新而更新. 這是 sw-precache 的主要功能顷链。
運行時緩存 Runtime caching
這是我們緩存所有的其他資源的方法。即運行時緩存包括以下五種類型屈梁,
sw-toolbox 都已提供 —— network first, cache first, fastest, cache only, network only. 如果你已經(jīng)閱讀過 Jake Archibald的 The Offline Cookbook 你將會很熟悉這些內(nèi)容嗤练。
本例子將使用到帶星號的這些策略

  • 網(wǎng)絡(luò)優(yōu)先 Network first *

    • 我們假設(shè)讀者希望讀到最新的文章。對于文章的標題在讶,我們總是網(wǎng)絡(luò)優(yōu)先煞抬,優(yōu)先去請求最新的資源。
  • 緩存優(yōu)先 Cache first *

    • 你對Reddit文章的第一印象會是我們總是想要從網(wǎng)絡(luò)上加載它构哺。然而Service worker的代碼可以在 app啟動時子目錄被選中時 后臺加載這些文章此疹。因為文章可能在我們創(chuàng)建后并沒有改變,我們選擇使用緩存優(yōu)先去瀏覽這些文章。
  • 最快 Fastest

    • 即使本例中沒有使用這個策略蝗碎,我們?nèi)钥梢允褂眠@個策略用來緩存文章。在這個策略中旗扑,同步請求緩存和網(wǎng)絡(luò)蹦骑。哪個先返回先使用哪一個。
  • 只用緩存 Cache Only *

    • 因為我們改變頻率很低臀防,子目錄subreddits將會在應(yīng)用第一次加載時獲取眠菇,之后都將會從緩存中讀取。在其他情況下袱衷,我們可以升級service worker時更新子目錄subreddit的名稱捎废。
  • 只用網(wǎng)絡(luò) Network Only

    • 只用網(wǎng)絡(luò)即不使用任何緩存,因為你不想緩存的資源可能被其他的策略所緩存致燥,Network Only給你了一個用來排除指定的路徑登疗,防止被緩存的明確的策略。
四嫌蚤、Gulp配置

work中的gulpfile.js辐益。目前他應(yīng)該包含這些代碼

'use strict';
var gulp = require('gulp');
var path = require('path');
// Gulp commands go here.
  1. 引入sw-precache庫
var swPrecache = require('sw-precache');
  1. 添加空的gulp task
gulp.task('make-service-worker', function(callback) {
});
  1. 你會注意到work下有app文件夾,包含著web app實際的文件脱吱。這樣我們的開發(fā)文件(例如gulp file)和應(yīng)用文件時隔離的智政。讓我們在變量中標記應(yīng)用文件的位置,稍后使用箱蝠。
gulp.task('make-service-worker', function(callback) {
    var rootDir = 'app';
});
  1. 調(diào)用 swPrecache.write()
    sw-precache庫的方法write()续捂,可以用來在指定位置創(chuàng)建service worker。在task中添加此方法宦搬。
gulp.task('make-service-worker', function(callback) {
    var rootDir = 'app';
    swPrecache.write(path.join(rootDir, 'serviceworker.js'), {
        
    }, callback);
});

write()方法有三個參數(shù)
filePath牙瓢,生成service worker的路徑
options,配置service worker的對象床三,包含前面提到過的兩種緩存策略precaching和runtime caching一罩。目前為空
callback,我們必須添加gulp callback到sw-precache中
剩下的代碼將會被添加到options對象中∑膊荆現(xiàn)在聂渊,可以執(zhí)行g(shù)ulp task

$ gulp make-service-worker
五、預(yù)緩存 Precaching

讓我們開始關(guān)注業(yè)務(wù)四瘫,我們需要讓service worker做一些事情汉嗽。

告訴sw-precache需要緩存的資源
首先我們需要precache Redder的app shell。
在options中使用staticFileGlobs字段找蜜,它的值為字符串數(shù)組饼暑。 例如

{staticFileGlobs: [rootDir + '/index.html',
                   rootDir + 'css/styles.css',
                   rootDir + 'images/dog.png'
                  ...], // contents excerpted
}

然而我們并不想把每個文件單獨列出,這樣可能會有漏掉的文件,當文件過多時弓叛,代碼也將變得很長彰居。所幸staticFileGlobs使用node glob,所以可以使用以下的形式

{staticFileGlobs: [rootDir + '/**/*.{html,css,png,jpg,gif}',
                        rootDir + '/js/*.js']
}

這樣會拿到app shell的所有文件撰筷,service worker可以把他們?nèi)烤彺嬖跒g覽器中陈惰。
staticFileGlobs屬性告訴sw-precache到哪里去尋找文件,而不是告訴生成的service worker在哪里去獲取這些資源(這句話的意思是毕籽,尋找文件的路徑上可能會帶上不需要的路徑抬闯,例如app/,在服務(wù)啟動時我們是直接在app/路徑下啟動的关筒,所以資源上帶有app會造成瀏覽器在獲取資源時出錯)溶握,所以使用stripPrefix來截取資源的前綴。

{staticFileGlobs: [rootDir + '/**/*.{html,css,png,jpg,gif}',
                        rootDir + '/js/*.js'],
 stripPrefix: rootDir
}



為什么JS文件要單獨列出來
如果我們在第一行引入蒸播,precaching會把service worker和他import的文件全部緩存睡榆,這樣是不對的。我們在更新應(yīng)用時廉赔,使用了舊版的Service worker會帶來很多困擾肉微。
因為service worker存在rootDir中,我們可以跳過rootDir蜡塌,來緩存其他的js文件碉纳。

生成service worker

$ gulp make-service-worker

service worker生成在work/app/路徑下

驗證預(yù)緩存precaching

六、運行時緩存 Runtime caching

通過給write()的options對象添加參數(shù)馏艾,可以配置運行時緩存的策略劳曹。運行時緩存必須的兩個參數(shù)是urlPatternhandler,有些緩存策略可能會需要更多琅摩。參數(shù)配置類似下面這種形式铁孵。其中urlPattern支持正則匹配。

runtimeCaching: [
{
        urlPattern: /some regex/,
        handler: 'cachingStrategy'
},
{
        urlPattern: /some regex/,
        handler: 'cachingStrategy'
}
// Repeat as needed.
],

緩存文章標題
如果你偷看final/中的gulpfile.js房资,你可以發(fā)現(xiàn)為三類內(nèi)容使用了三類緩存策略蜕劝。
我們首先來看文章標題的緩存。
因為標題變化頻率高轰异,使用網(wǎng)絡(luò)優(yōu)先緩存策略岖沛。
swPrecache.write()stripPrefix字段后添加runtimeCaching屬性。
子目錄的標題名稱由以下url返回
http://www.reddit.com/r/subredit_name.json
正則形式為 https:\/\/www\.reddit\.com\/r\/\w{1,255}\.json
所以配置如下:

runtimeCaching: [
{
        urlPattern: /https:\/\/www\.reddit\.com\/r\/\w{1,255}\.json/,
        handler: 'networkFirst'
}],

使用正確的緩存
因為我們使用了三種不同的緩存策略(自行把final下的三種緩存的代碼拷過來吧...)搭独,存儲了標題婴削、文章、子目錄牙肝。我們需要給cache特定的名稱來區(qū)別他們唉俗,給runtimeCaching數(shù)組中的對象添加帶有cache屬性的第三個參數(shù)options嗤朴,配置緩存名稱。

runtimeCaching: [
{
        urlPattern: /https:\/\/www\.reddit\.com\/r\/\w{1,255}\.json/,
                handler: 'networkFirst',
                options: {
                        cache: {
                                name: 'titles'
                        }
                }
}],

再次執(zhí)行命令虫溜,刷新網(wǎng)頁查看效果

$ gulp make-service-worker

后臺同步運行時緩存
Redder有一個額外的技巧雹姊,使用后臺同步來預(yù)填充運行時緩存。對子目錄衡楞,標題和文章都會執(zhí)行容为。它是怎么工作的和怎么觸發(fā)的并不是本次教程的重點,但是后面會介紹到寺酪。
給write方法添加importScript參數(shù),可以在service worker中import js文件替劈。

importScripts: ['sync.js']
七寄雀、Debugging 緩存

開啟debugging
sw-toolbox庫有debug開關(guān),打開后sw-precache可以輸出信息到DevTools的console中陨献。
我們可以在service worker中添加toolbox.options.debug = true;來開啟debug
但是這樣會每次生成service worker都需要手動輸入
于是我們把這段代碼寫在config.js中盒犹,如果需要開啟debug模式,在importScript中引入config.js即可眨业。
打開console可以看到

注意到輸出信息
[sw-toolbox] preCache List: (none)
這并不是個錯誤急膀。sw-toolbox庫可以與sw-precache分割使用,擁有自己的precaching能力龄捡,因為我們沒有使用這個特征卓嫂,我們才看到了這段message。
模擬離線和低延時環(huán)境
選擇不同的子標題聘殖,我們會看到下面的輸出

sw-toolbox輸出了對應(yīng)url的緩存策略晨雳。
在network中選擇offline,再點擊之前點擊過的子列表奸腺,可看到以下輸出


可看到已切換到緩存餐禁,頁面也可正常展示。

添加導(dǎo)航回退
在offline模式下點擊沒有點擊過得子列表突照,必然獲取不到相關(guān)數(shù)據(jù)帮非。為此,我們希望創(chuàng)建一個回退頁面讹蘑,以顯示所請求的資源不可用末盔。添加以下配置:

navigateFallback: 'message.html'

為了能啟用message.html必須precache
對于這個功能,測試失敗衔肢,還沒搞懂為什么庄岖,查看了一下生成的service worker

self.addEventListener('fetch', function(event) {
  if (event.request.method === 'GET') {
    // Should we call event.respondWith() inside this fetch event handler?
    // This needs to be determined synchronously, which will give other fetch
    // handlers a chance to handle the request if need be.
    var shouldRespond;

    // First, remove all the ignored parameters and hash fragment, and see if we
    // have that URL in our cache. If so, great! shouldRespond will be true.
    var url = stripIgnoredUrlParameters(event.request.url, ignoreUrlParametersMatching);
    shouldRespond = urlsToCacheKeys.has(url); 
    // If shouldRespond is false, check again, this time with 'index.html'
    // (or whatever the directoryIndex option is set to) at the end.
    var directoryIndex = 'index.html';
    if (!shouldRespond && directoryIndex) {
      url = addDirectoryIndex(url, directoryIndex);
      shouldRespond = urlsToCacheKeys.has(url);
    }

    // If shouldRespond is still false, check to see if this is a navigation
    // request, and if so, whether the URL matches navigateFallbackWhitelist.
    var navigateFallback = 'message.html';
    if (!shouldRespond &&
        navigateFallback &&
        (event.request.mode === 'navigate') &&
        isPathWhitelisted([], event.request.url)) {
      url = new URL(navigateFallback, self.location).toString();
      shouldRespond = urlsToCacheKeys.has(url);
    }

    // If shouldRespond was set to true at any point, then call
    // event.respondWith(), using the appropriate cache key.
    if (shouldRespond) {
      event.respondWith(
        caches.open(cacheName).then(function(cache) {
          return cache.match(urlsToCacheKeys.get(url)).then(function(response) {
            if (response) {
              return response;
            }
            throw Error('The cached response that was expected is missing.');
          });
        }).catch(function(e) {
          // Fall back to just fetch()ing the request if some unexpected error
          // prevented the cached response from being valid.
          console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e);
          return fetch(event.request);
        })
      );
    }
  }
});

shouldRespond:service worker會檢測多次是否需要使用緩存響應(yīng),前面的不命中才會執(zhí)行后面的檢測角骤。
對回退的檢測放在最后

    var navigateFallback = 'message.html';
    if (!shouldRespond &&
        navigateFallback &&
        (event.request.mode === 'navigate') &&
        isPathWhitelisted([], event.request.url)) {
      url = new URL(navigateFallback, self.location).toString();
      shouldRespond = urlsToCacheKeys.has(url);
    }

針對于回退頁面的檢測的四個條件
!shouldRespond: true
navigateFallback: true
isPathWhitelisted(): 對于沒有白名單的(第一個參數(shù)隅忿,數(shù)組為空)心剥,默認返回true
event.request.mode === 'navigate'這個不知道什么情況下會觸發(fā)
留著這個問題,以后再來

來看一下后臺同步緩存

頁面在腳本加載完畢后會調(diào)用redder.js中的getReddit方法

function getReddit() {
  fetchSubreddits();
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.ready.then(reg => {
      return reg.sync.register('subreddits');
    });
  }
  var anchorLocation = window.location.href.indexOf('#');
  if (anchorLocation != -1) {
    fetchTopics(window.location.href.slice(anchorLocation + 1));
  }
}

第五行 reg.sync.register('subreddits');觸發(fā)后臺同步
sync.js中監(jiān)聽同步事件背桐,并發(fā)出對應(yīng)請求优烧。
針對于本應(yīng)用
在頁面初始化時會同步請求子目錄
在點擊子目錄展示本目錄內(nèi)的文章時,會同步請求所有符合規(guī)則的文章內(nèi)容链峭,相當于做到了預(yù)加載畦娄。

self.addEventListener('sync', function (event) {
    if (event.tag == 'articles') {
        console.log('in sync articles');
        syncArticles();
    } else if (event.tag == 'subreddits') {
        console.log('in sync subreddits');
        syncSubreddits();
    }
});

Web應(yīng)用程序通常在不可靠網(wǎng)絡(luò)的環(huán)境中運行(eg:手機)和未知的生命周期(瀏覽器可能關(guān)閉或用戶點擊跳轉(zhuǎn)了)。這使得很難同步web app客戶端與服務(wù)端的數(shù)據(jù)(如照片上傳弊仪,文檔變更熙卡,或電子郵件)。如果在同步完成之前瀏覽器關(guān)閉或用戶跳轉(zhuǎn)励饵,數(shù)據(jù)同步將會中斷驳癌,直到用戶再次使用這個頁面并再次嘗試。此規(guī)范提供了一個新的serviceworker事件onsync役听,即使在數(shù)據(jù)最初請求時網(wǎng)絡(luò)情況不佳颓鲜,仍可以在后臺進行同步操作。這個API是為了減少內(nèi)容創(chuàng)建和與服務(wù)端內(nèi)容同步的時間典予。

同步請求會在觸發(fā)時立刻執(zhí)行甜滨,如果網(wǎng)絡(luò)狀況不好,會run the event at the soonest convenience瘤袖。
當已不再會執(zhí)行更多的請求時衣摩,event.lastChance置為true,用戶可自行決定如何提示孽椰。

參考資料:

codelab昭娩,源碼的文章標題顯示邏輯有問題,稍作了修改
sync API
詳細文檔黍匾,It is not a W3C Standard nor is it on the W3C Standards Track.
sw-precache => gulp
sw-precache => webpack

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末栏渺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子锐涯,更是在濱河造成了極大的恐慌磕诊,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纹腌,死亡現(xiàn)場離奇詭異霎终,居然都是意外死亡,警方通過查閱死者的電腦和手機升薯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門莱褒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人涎劈,你說我怎么就攤上這事广凸≡牟瑁” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵谅海,是天一觀的道長脸哀。 經(jīng)常有香客問我,道長扭吁,這世上最難降的妖魔是什么撞蜂? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮侥袜,結(jié)果婚禮上蝌诡,老公的妹妹穿的比我還像新娘。我一直安慰自己枫吧,他們只是感情好送漠,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著由蘑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪代兵。 梳的紋絲不亂的頭發(fā)上尼酿,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機與錄音植影,去河邊找鬼裳擎。 笑死,一個胖子當著我的面吹牛思币,可吹牛的內(nèi)容都是我干的鹿响。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼谷饿,長吁一口氣:“原來是場噩夢啊……” “哼惶我!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起博投,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤绸贡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后毅哗,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體听怕,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年虑绵,在試婚紗的時候發(fā)現(xiàn)自己被綠了尿瞭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡翅睛,死狀恐怖声搁,靈堂內(nèi)的尸體忽然破棺而出红符,到底是詐尸還是另有隱情劲腿,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站揭璃,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏榛臼。R本人自食惡果不足惜峻厚,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望骤铃。 院中可真熱鬧拉岁,春花似錦、人聲如沸惰爬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撕瞧。三九已至陵叽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間丛版,已是汗流浹背巩掺。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留页畦,地道東北人胖替。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像豫缨,于是被迫代替她去往敵國和親独令。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355

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