使用 jQuery Mobile 與 HTML5 開發 Web App —— jQuery Mobile 頁面事件與 deferred

在系列的上一篇文章《使用 jQuery Mobile 與 HTML5 開發 Web App —— jQuery Mobile 事件詳解》中,Kayo 介紹了除頁面事件外的其餘 jQuery Mobile 事件,而頁面事件因爲事件數較多,而且涉及 jQuery 中一個比較複雜的對象 deferred ,所以在本文中單獨說明。jQuery Mobile 頁面事件使用分爲頁面加載事件 (Page load events),頁面跳轉事件 (Page change events),頁面顯示/隱藏事件 (Page show/hide events),頁面初始化事件 (Page initialization events),頁面移除事件 (Page remove events) 五種。本文除了會對以上五種事件做出詳細說明外,還會對 jQuery Mobile 的頁面加載流程做出詳細講解。html

一.頁面加載事件 (Page load events)

當一個外部頁面加載到 DOM 時,會觸發兩個事件 —— 第一個事件是 pagebeforeload,第二個是 pageload 或 pageloadfailed。jQuery Mobile 提供了這些 API ,可使開發者能夠方便地在頁面加載先後對頁面數據進行處理。html5

注意這裏是外部頁面加載到 DOM 的過程 ,即加載的頁面不在當前頁面的文檔中,而一個文檔中的多個 "page" 是原本就存在於 DOM 中,所以在同一文檔中的不一樣 "page" 的跳轉不會觸發 pagebeforeload 事件。關於 jQuery Mobile 中「page」的理解,能夠閱讀《使用 jQuery Mobile 與 HTML5 開發 Web App —— jQuery Mobile 頁面與對話框》jquery

pagebeforeload

pagebeforeload 事件會在頁面加載前被觸發,這個事件的最經常使用實例是 —— 爲綁定該事件的回調函數調用 preventDefault() ,阻止這個事件的默認行爲,這樣代表由事件的回調函數進行自定義處理頁面,這時開發者必須使用 deferred 對象調用 resolve() 或者 reject() 在自定義處理結束後恢復頁面請求。web

在說明爲何須要用 resove() 或 reject() 恢復頁面請求前,首先要詳細介紹 deferred 對象。ajax

deferred 對象從 jQuery 1.5.0 引入,jQuery Mobile 是基於 jQuery 庫的,固然也可使用 deferred 對象。在舊版本的 jQuery 中,回調函數功能很單一,就事件機制爲例,一般只能是事件觸發後指定調用一個函數(即爲事件綁定的回調函數),而後執行該函數。而引入 deferred 後,jQuery 的回調函數功能則強大不少了,deferred 從字面上來講是「延時」的意思,但 deferred 對象的功能不止如此,它除了延時操做外(解決耗時操做問題),還進行了統一封裝,爲回調函數的相關處理提供統一編程接口。編程

deferred 對象是在 $.ajax() 內部實現的,因此能夠在調用 ajax 時建立 deferred 對象並調用。jQuery Mobile 是基於 ajax 的,包括它的頁面加載,而且頁面加載的過程在內部實現時已經建立了相應的 deferred 對象,因此在調用頁面事件的回調函數時能夠調用 deferred 對象。promise

所以,咱們只需指定 ajax 請求成功和失敗時的回調函數列表便可方便的進行回調,這兩種回調能夠分別寫在 done() 和 fail() 方法中,而上面介紹的 resolve() 或 rejected() 方法,則分別能夠手動執行 done() 和 fail() 方法。這時候 deferred 的做用大體已經說明了。瀏覽器

這裏再引入一個概念,deferred 狀態。deferred 有三種狀態:初始化(unresolved),成功(resolved),失敗(rejected)。正如上面所說, deferred 執行哪些回調函數是依賴於狀態。app

簡而言之,deferred 的用途是根據不一樣的請求狀態,調用相應的回調隊列,使到開發者能夠方便地建立一系列的回調,甚至是鏈式調用,而不須要像傳統方法那樣只能爲一個動做綁定一 個回調函數,另外它還有 resolve() 和 rejected() 等方法,使到這個回調處理能夠更加靈活。異步

接下來再回到 pagebeforeload 事件上,上面只是說明了 deferred 的主要做用,但並無說明 deferred 在頁面加載流程中的具體功能。在這以前,咱們應該瞭解新頁面載入的流程是怎樣的?

當咱們觸發了一個跳轉到新頁面的連接後,jQuery Mobile 會調用 $.mobile.changePage() 方法,這是 jQuery Mobile 用於加載新頁面的方法,往後會另做介紹,調用這個方法後,加載頁面的流程正式開始,爲了進一步說明這個流程,下面 Kayo 節選 $.mobile.changePage() 的源碼並註釋。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 若 toPage 參數爲字符串,即第一個參數格式正確,則在 $.mobile.changePage() 內調用 $.mobile.loadPage 方法
if ( typeof toPage === "string" ) {
     $.mobile.loadPage( toPage, settings )
         // 指定 done() 回調操做隊列
         .done( function ( url, options, newPage, dupCachedPage ) {
             isPageTransitioning = false ;
             options.duplicateCachedPage = dupCachedPage;
             $.mobile.changePage( newPage, options );
         })
         // 指定 fail() 回調操做隊列,並鏈式調用
         .fail( function ( url, options ) {
             isPageTransitioning = false ;
 
             // 清除用戶點擊的按鈕的激活狀態
             removeActiveLinkClass( true );
 
             // 釋放 transition
             releasePageTransitionLock();
             settings.pageContainer.trigger( "pagechangefailed" , triggerData );
         });
     return ;
}

從上面的源碼中能夠看出,實際上加載頁面內容是由另外一個方法 $.mobile.loadPage() 負責,$.mobile.changePage() 的責任是指定 $.mobile.loadPage() 請求成功和請求失敗時的回調隊列,即請求成功或失敗後分別須要作些什麼。所以不難想象,實際利用 deferred 對象的也是 $.mobile.loadPage() 。

這裏 Kayo 須要指出兩點:一是這裏 done() 和 fail() 是鏈式寫法,即調用 done() 後會繼續調用 fail() ,這是由於不管頁面請求成功與否,有一些操做(像清除用戶點擊的按鈕的激活狀態)都是必須的,所以 jQuery Mobile 採用鏈式寫法,把這部分操做寫在 fail() 中,若頁面請求失敗,則直接調用 fail() ,若請求成功則調用 done() 後鏈式調用 fail() 。二是以上兩個方法的實際做用,因爲篇幅有限,這裏沒有列出完整源碼,沒法看出以上兩個方法更深刻的做用,實際上 $.mobile.loadPage() 的做用是把外部頁面的元素插入到當前 DOM 中,而 $.mobile.changePage() 只是把新頁面顯示(激活一個頁面),所以外部頁面才須要首先調用 $.mobile.loadPage() 把元素插入當前 DOM 中。

下面再對 $.mobile.loadPage() 的源碼進行分析,這裏會爲整個 $.mobile.loadPage() 方法的源碼進行註釋,但爲了方便閱讀,不會列出所有的源碼實體,以源碼註釋代替完整源碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$.mobile.loadPage = function ( url, options ) {
 
         // 建立一個 deferred 對象,用於告知調用者 $.mobile.changePage() 頁面請求成功或是出現錯誤
         // 也就是讓 $.mobile.changePage() 知道須要調用 done() 或是 fail() 回調隊列
         var deferred = $.Deferred(),
 
             // … 根據參數處理頁面數據 …
 
         // 觸發一個 pagebeforeload 事件
         var mpc = settings.pageContainer,
             pblEvent = new $.Event( "pagebeforeload" ),
             // 保存頁面選項在 data 參數中
             triggerData = { url: url, absUrl: absUrl, dataUrl: dataUrl, deferred: deferred, options: settings };
 
         // 讓監聽器知道正準備加載一個新頁面
         mpc.trigger( pblEvent, triggerData );
 
         // 若是開發者阻止了默認行爲,本函數立刻結束,並返回 deferred 對象的 promise() 方法
         if ( pblEvent.isDefaultPrevented() ) {
             return deferred.promise();
         }
 
         // 提示正在加載頁面
         // 使用 ajax 把頁面插入 DOM ,而後根據 ajax 請求成功仍是失敗做出相應的處理
 
         // 請求成功
         /*
             根據實際狀況和傳遞的參數調整頁面內容(調整 jqm header, jqm title, 移除 loading 提示等)
             觸發 pageload 事件告知監聽者頁面請求成功
         */
         // 調用 resolve() 方法使 deferred 狀態爲成功
         deferred.resolve( absUrl, options, page, dupCachedPage );
 
         // 請求失敗
         /*
             報錯並對頁面做出一個調整(移除 loading 提示等)
             觸發 pageloadfail 事件告知監聽者頁面請求失敗
         */
         // 調用 rejected() 方法使 deferred 狀態爲失敗
         deferred.reject( absUrl, options );
 
         // 返回一個 deferred 對象的 promise() 方法
         return deferred.promise();
};

看了上面的代碼和註釋後,相信你們對頁面加載的流程以及頁面事件的觸發時間已經有了完整的理解,deferred 對象起到了控制使用哪一種回調的做用,這些回調的具體內容是在 $.mobile.changePage() 中分別以 done() 和 fail() 指定的。

若用戶沒有阻止事件的默認行爲,則根據請求成功或失敗,會調用 resolve() 或 rejected() 方法改變 deferred 的狀態,而後根據狀態判斷是調用 done() ,或是直接調用 fail() 。

若用戶阻止事件的默認行爲,jQuery Mobile 會立刻終止加載頁面,並返回一個 deferred 的 promise() 方法,promise() 方法返回的是 deferred 的只讀版本,即返回一個可讀不可寫的 deferred 對象,除此以外沒有任何處理了。可是上面的源碼註釋中已經說明,$.mobile.loadPage 會把一系列的屬性保存在 data 參數中,其中包括了 deferred 對象,咱們知道,開發者能夠爲事件綁定一個回調函數,這個 data 對象會被分配到 pagebeforepage 事件的回調函數的第二個參數中(第一個參數爲事件自己),所以咱們能夠調用這個 data.deferred 對象的 resolve() 或 rejected() 方法恢復請求,實際上這裏的恢復請求是告知調用者 $.mobile.changePage() 此次 ajax 請求是成功仍是失敗,使到 $.mobile.changePage() 方法知道須要調用 done() 回調隊列仍是直接調用 fail() 回調隊列。

resolve() 或 rejected() 中填寫的參數實際上也就是 done() 或 fail() 中相應的參數。

固然,若開發者沒有阻止默認行爲,這個 $.mobile.loadPage() 在最後也會觸發 resolve() 或 rejected() 方法,所以實際上有沒有利用阻止默認行爲進行自定義處理的區別是 —— 是否進行頁面處理和觸發 loadpage 或 loadpagefailed 事件,即自定義處理不會有上面的源碼註釋中使用 /* */ 註釋的部分,或者說,自定義處理須要作的,就是自行設計一些處理,來代替 /* */ 註釋中的默認處理。

這就意味着,如有須要的話,阻止默認行爲並進行自定義處理頁面(調整 jqm header, jqm title 移除 loading)後還必須手動觸發 loadpage 事件或 loadpagefailed 事件。

在說明整個加載頁面流程後,這裏再介紹一下上面所說的傳遞給 pagebeforeload 事件的回調函數的第二個參數 —— data 的各項屬性。

  • url (string) 經過回調函數傳遞絕對或者相對的 url 到 $.mobile.loadPage()
  • absUrl (string) 絕對 url
  • dataUrl (string) 當識別頁面或者更新瀏覽器地址的時候使用過濾過的絕對 url
  • deferred (object) 回調函數中調用 preventDefault() ,必須使用 resolve() 或 rejected() 恢復 changePage() 請求
  • options (object) 這個對象包含了須要傳遞給 $.mobile.loadPage() 的選項

最後引用 jQuery Mobile 官方的例子來講明如何阻止默認行爲並進行自定義處理,爲了進一步說明,Kayo 會修改一下這些例子。

調用 resolve()

1
2
3
4
5
6
7
8
9
10
11
12
$( document ).bind( "pagebeforeload" , function ( event, data ){
 
     // 阻止默認行爲,告知瀏覽器本次事件由事件的回調函數(即本函數)處理
     event.preventDefault();
 
     // 自定義處理,如簡單的彈出一個提示
     alert( '本次事件由開發者做出一些處理' );
     // 在本函數或者其它異步方法中調用 resolve()
     // 告知 $.mobile.changePage() 方法繼續頁面請求並執行 done() 回調函數隊列
     data.deferred.resolve( data.absUrl, data.options, page );
 
});

調用 reject()

1
2
3
4
5
6
7
8
9
10
11
12
$( document ).bind( "pagebeforeload" , function ( event, data ){
 
     // 阻止默認行爲,告知瀏覽器本次事件由事件的回調函數(即本函數)處理
     event.preventDefault();
 
     // 自定義處理,如簡單的彈出一個提示
     alert( '本次事件由開發者做出一些處理' );
     // 在本函數或者其它異步方法中調用 rejected()
     // 告知 $.mobile.changePage() 方法繼續頁面請求並直接執行 fail() 回調函數隊列
     data.deferred.rejected( data.absUrl, data.options );
 
});

pageload

pageload 事件的觸發流程相對 pagebeforeload 來講則較爲簡單,當頁面成功加載並插入到 DOM 後會觸發 pageload 事件,這個事件也會傳遞一個 data 參數做爲事件回調函數的第二個參數,但這個 data 參數與 pagebeforeload 的屬性有些不一樣,下面列出完整的屬性。

  • url (string) 經過回調函數傳遞絕對或者相對的 url 到 $.mobile.loadPage()
  • absUrl (string) 絕對 url
  • dataUrl (string) 當識別頁面或者更新瀏覽器地址的時候使用過濾過的絕對 url
  • options (object) 這個對象包含了須要傳遞給 $.mobile.loadPage() 的選項
  • xhr (object) 當嘗試加載頁面時,會使用 jQuery XMLHttpRequest 對象,這個也是 $.ajax() 成功回調函數的第三個參數
  • textStatus (null or string) 根據 jQuery 核心文檔,請求時會以一個字符串描述狀態,這也是 $.ajax() 失敗回調函數的第二個參數

能夠看出,jQuery Mobile 沒有爲 pageload 提供 deferred 對象屬性,這說明頁面請求成功後,只能按默認狀況執行 done() 隊列,不能利用 deferred 對象直接執行 fail() 隊列。

pageloadfailed

pageloadfailed 事件是頁面請求失敗時觸發的,參數與 pageload 類似,但這裏再次引入 deferred 對象,這代表 jQuery Mobile 容許開發者即便頁面請求失敗,仍可利用 deferred 對象選擇執行 done() 隊列或 fail() 隊列 。具體的 data 屬性以下。

  • url (string) 經過回調函數傳遞絕對或者相對的 url 到 $.mobile.loadPage()
  • absUrl (string) 絕對 url
  • dataUrl (string) 當識別頁面或者更新瀏覽器地址的時候使用過濾過的絕對 url
  • deferred (object) 回調函數中調用 preventDefault() ,必須使用 resolve() 或 reject() 恢復 changePage() 請求
  • options (object) 這個對象包含了須要傳遞給 $.mobile.loadPage() 的選項
  • xhr (object) 當嘗試加載頁面時,會使用 jQuery XMLHttpRequest 對象,這個也是 $.ajax() 成功回調函數的第三個參數
  • textStatus (null or string) 根據 jQuery 核心文檔,請求時會以一個字符串描述狀態,這也是 $.ajax() 失敗回調函數的第二個參數
  • errorThrown (null, string, object) 根據 jQuery 核心文檔,這個屬性可能會被用做 HTTP 狀態的一部分,這也是 $.ajax() 失敗回調函數的第三個參數。

上面介紹 pagebeforeload 事件時,已經介紹了使用 preventDefault() 阻止事件默認行爲,而後進行自定義的處理,而且從新使用 deferred 對象的 resolve() 或 rejected() 方法恢復頁面請求。而在 pageloadfailed 事件中也有相似的作法,這裏的自定義處理一般是在頁面請求失敗後加載另外一個頁面,下面舉例說明。

調用 resolve()

1
2
3
4
5
6
7
8
9
10
11
12
13
$( document ).bind( "pageloadfailed" , function ( event, data ){
 
     // 阻止默認行爲,告知瀏覽器本次事件由事件的回調函數(即本函數)處理
 
     event.preventDefault();
 
     // 自定義處理,嘗試加載另外一個頁面
 
     // 在本函數或者其它異步方法中調用 resolve()
     // 告知 $.mobile.changePage() 方法繼續頁面請求並執行 done() 回調函數隊列
     data.deferred.resolve( data.absUrl, data.options, page );
 
});

調用 rejected()

1
2
3
4
5
6
7
8
9
10
11
12
13
$( document ).bind( "pageloadfailed" , function ( event, data ){
 
     // 阻止默認行爲,告知瀏覽器本次事件由事件的回調函數(即本函數)處理
 
     event.preventDefault();
 
     // 自定義處理,嘗試加載另外一個頁面
 
     // 在即本函數或者其它異步方法中調用 rejected()
     // 告知 $.mobile.changePage() 方法繼續頁面請求並直接執行 fail() 回調函數隊列
     data.deferred.reject( data.absUrl, data.options );
 
});

下面給出一個完整例子來驗證自定義處理的方法中須要代替的是頁面處理和觸發 pageload 或 pageloadfailed 事件,本例子中,Kayo 阻止了 pagebeforeload 的默認行爲,注意,Kayo 在例子中綁定了 pageload 事件,並在其回調函數中添加彈出提示,但由於進行自定義處理,不會產生 pageload 事件,所以不會彈出加載 pageload 事件的提示,只有「自定義處理」的彈出提示。另外,讀者能夠嘗試在 Demo 中的兩個頁面中來回點擊幾回,會發現只有第一次點擊會彈出「自定義處理」提示,這是由於第一次加載另外一個頁面後,該頁面已經存在於 DOM 中,再次加載不會觸發 pagebeforeload 事件。讀者能夠在 Demo 中驗證以上兩點。

頁面加載事件 Demo(建議使用 PC 上的 Firefox、Chrome 等現代瀏覽器和 IE9+ 或 Android , iPhone/iPad 的系統瀏覽器瀏覽,下同)

二.頁面跳轉事件 (Page change events)

頁面跳轉事件與頁面加載事件相似,但由於沒有直接使用 deferred 對象,所以這系列的事件會較爲簡單。

頁面跳轉事件由 $.mobile.changePage() 產生,第一個爲 pagebeforechange ,第二個是 pagechange 或 pagechangefailed 。

$.mobile.changePage() 方法調用後會對頁面參數進行一些處理,而後觸發 pagebeforechange 事件,若頁面請求成功,即上面的 $.mobile.loadPage() 調用了 done() 隊列,而且頁面外部處理(history 等)完成後,會觸發 pagechange ,若請求失敗,即上面的 $.mobile.loadPage() 直接調用了 fail() 隊列,則觸發 pagechangefailed 事件。

在上面 Kayo 已經說明過,實質處理頁面加載的是 $.mobile.loadPage() 事件,所以須要進行頁面加載先後的自定義處理仍是用阻止 pagebeforeload , pageload, pageloadfail 等事件並進行自定義處理比較合適,儘管這樣,阻止 pagebeforechange 仍具備意義 —— 它能夠直接阻止一個頁面加載,在某些狀況下,可能你會須要這樣作,就像有些狀況下須要阻止 click 的默認跳轉行爲同樣。

這三個事件的回調函數的第二個參數 data 有以下兩個屬性:

  • toPage (object or string) 這個屬性值應該爲一個但願被激活的頁面(即須要進入的頁面),實際取值能夠爲一個包含頁面 DOM 對象的 jQuery 對象或者一個外部頁面的絕對或相對連接,這個值同時也是 $.mobile.changePage() 方法的第一個參數。
  • options (object) 這個值是一個包含頁面選項的對象,同時也會用於 $.mobile.changePage() ,做爲第二個參數。

利用上面兩個屬性,能夠在事件回調函數中做出一些處理。

值得注意的是,經過了解加載頁面的流程,能夠知道,一次正確的頁面跳轉中,事件產生的順序應該是,pagebeforechange , pagebeforeload , pageload , pagechange ,在開發時須要注意這些事件的觸發順序。

三.頁面顯示/隱藏事件 (Page show/hide events)

在頁面轉場先後,會觸發一些事件,舉一個例子:

B 頁面中有一個 id 爲 page-B 的 page 組件 ,而後在 A 頁面的 head 中做出以下的事件綁定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$( '#page-B' ).live( 'pagebeforeshow' , function (event, ui){
     alert( 'pagebeforeshow' );
});
 
$( '#page-B' ).live( 'pagebeforehide' , function (event, ui){
     alert( 'pagebeforehide' );
});
$( '#page-B' ).live( 'pageshow' , function (event, ui){
     alert( 'pageshow' );
});
 
$( '#page-B' ).live( 'pagehide' , function (event, ui){
     alert( 'pagehide' );
});

從 A 頁面點擊連接跳轉到 B 頁面,而後從 B 頁面跳轉回 A 頁面,則觸發事件的狀況以下,

  • pagebeforeshow:從 A 跳轉到 B,B 頁面被顯示時(轉場開始)觸發
  • pagebeforehide:從 A 跳轉到 B,A 頁面被隱藏時(轉場結束)觸發
  • pageshow:從 B 跳轉到 A,A 頁面被顯示時(轉場開始)觸發
  • pagehide:從 B 跳轉到 A,B 頁面被隱藏時(轉場結束)觸發

所以,觸發哪一種頁面顯示/隱藏事件,要視乎頁面是被顯示仍是被隱藏。

這些事件會由轉場實現函數控制觸發,它們的回調函數的第二個參數 data 只有一個屬性,分別爲:

  • pagebeforeshow:prevPage (object) 這個屬性值應爲包含正在離開的頁面 DOM 對象的 jQuery 對象,即上面的 $(A)
  • pagebeforehide: nextPage (object) 這個屬性值應爲包含正在進入的頁面 DOM 對象的 jQuery 對象,即上面的 $(B)
  • pageshow: prevPage (object) 這個屬性值應爲包含正在離開的頁面 DOM 對象的 jQuery 對象,即上面的 $(B)
  • pagehide: nextPage (object) 這個屬性值應爲包含正在進入的頁面 DOM 對象的 jQuery 對象,即上面的 $(A)

下面給出一個 Demo ,演示上面 A 與 B 頁面的例子

頁面顯示/隱藏事件 Demo

四.頁面初始化事件 (Page initialization events)

頁面初始化中的初始化指的是頁面加強,即 jQuery Mobile 對頁面組件的樣式、功能和交互進行豐富並加強的過程,在這個過程當中也會觸發一系列事件。

pagebeforecreate

在頁面開始初始化以前觸發,這時 DOM 已被 jQuery Mobile 得到,但仍未進行 jQuery Mobile 加強,所以綁定這個事件並在回調函數中做出處理能夠在頁面被加強前進行自定義處理。

另外若阻止事件的默認行爲,會禁止頁面組件初始化。

是否以爲這個流程與 pagebeforeload 的流程類似,的確,在 jQuery Mobile 中,不少事件的產生函數(如 pagebeforeload 的產生函數 pagebeforeload)內部都有阻止默認行爲的處理,即用判斷語句判斷觸發阻止默認行爲後自動 return ,阻止後面的處理髮生,瞭解這個流程對於瞭解 jQuery Mobile 的工做原理頗有幫助。

pagecreate

在頁面初始化時,即 DOM 剛開始被建立,而且須要進行 jQuery Mobile 的元素已經準備好(即加入待加強列表),但仍未實際開始進行 jQuery Mobile 加強前觸發。

pageinit

當頁面初始化後觸發,jQuery Mobile 建議使用這個事件的綁定代替 jQuery 中經常使用的 DOM ready() ,由於觸發這個事件意味着頁面已經直接或者經過 Ajax 加載好並加強,更適合用在 jQuery Mobile 開發中。

五.頁面移除事件 (Page remove events)

頁面移除事件只有一個 —— pageremove ,當 jQuery Mobile 準備從 DOM 中移除一個外部頁面時會觸發這個事件,阻止這個事件的默認行爲能夠阻止一個頁面被移除。

六.頁面事件觸發順序

上面的說明中,基本說明了頁面加載的完整流程,同時對不一樣事件的觸發順序做出了間接的說明,下面再給出一個例子,來直接說明在一次頁面跳轉中各個頁面事件的觸發順序。

頁面事件觸發順序 Demo

相關文章
相關標籤/搜索