# 頁面路由
在小程序中,所有頁面的創(chuàng)建、銷毀及狀態(tài)轉(zhuǎn)換都由頁面路由來表達和進行控制。以下內(nèi)容會簡單介紹小程序的頁面路由相關(guān)邏輯。
# 路由的時機
路由會以事件形式表示,由微信客戶端下發(fā)給小程序基礎(chǔ)庫,下發(fā)后客戶端和基礎(chǔ)庫將分別同時處理這一次路由事件。路由事件的發(fā)起可以大致分為以下兩類:
通過用戶的操作(如按下返回按鈕)發(fā)起。通過這種方式發(fā)起時,路由事件將直接由客戶端下發(fā)到基礎(chǔ)庫執(zhí)行;
由開發(fā)者通過 API(如
wx.navigateTo)或者組件(如<navigator>)發(fā)起。通過這種方式發(fā)起時,基礎(chǔ)庫將首先向客戶端發(fā)起路由請求,客戶端確認路由可以被執(zhí)行后,再將路由事件下發(fā)到基礎(chǔ)庫。其中,如果路由被確定執(zhí)行,API 的success回調(diào)函數(shù)或組件的success事件將被觸發(fā),否則將觸發(fā)fail。
當一次路由被確定執(zhí)行(API 或組件通知 success)時,沒有操作可以取消這一次路由。
當多次路由被連續(xù)發(fā)起時,如果當前的路由事件還未處理完畢,后續(xù)的路由事件將等待當前路由處理,并排隊依次執(zhí)行,直到所有待處理的路由都被執(zhí)行完畢。
一個簡單的例子:用戶點擊返回按鈕觸發(fā)了
navigateBack,小程序在頁面棧當前棧頂頁的onUnload中調(diào)用wx.redirectTo,并不能 將當前正在被銷毀的頁面重定向為一個新頁面,而是會先完成頁面返回,再將頁面返回后的新棧頂頁重定向到新的頁面。
# 頁面棧
目前,小程序的頁面會被組織為一個頁面棧加若干不在棧中的懸垂頁面的組合形式。其中,頁面棧按順序存放了通過跳轉(zhuǎn)依次打開的頁面,而當前已經(jīng)創(chuàng)建但非活躍的 tabBar 頁面及處于畫中畫模式(如 video、live-player 等)中的頁面將以懸垂頁面的形式存在。
全局接口 getCurrentPages 可以用來獲取當前頁面棧。
小程序冷啟動完成后,在整個小程序存活過程中(除去某次路由執(zhí)行到一半的中間狀態(tài)外),頁面棧中都將存在至少一個頁面。
頁面棧的具體行為可以參見下面具體路由行為中的詳細描述。
# 路由的監(jiān)聽及響應(yīng)
# 頁面生命周期函數(shù)
每個小程序頁面都有若干生命周期函數(shù),如 onLoad, onShow, onRouteDone, onHide, onUnload 等。它們可以在頁面注冊時定義,并會在相應(yīng)的時機觸發(fā)。所有生命周期函數(shù)及它們各自的含義和觸發(fā)時機可以參見 Page 接口,下面的內(nèi)容也將詳細說明每個路由將如何觸發(fā)頁面的生命周期函數(shù)。
# 頁面路由監(jiān)聽
從基礎(chǔ)庫版本 3.5.5 開始,基礎(chǔ)庫提供了一組針對路由事件的監(jiān)聽函數(shù)。相比頁面生命周期函數(shù),它們能更好地針對某次路由進行響應(yīng)。詳見 頁面路由監(jiān)聽。
# 路由類型
小程序目前的路由類型可以大致分為以下七種:
# 1. 小程序啟動
- openType:
appLaunch
小程序啟動路由 appLaunch 表示一個新的小程序啟動,并加載第一個頁面。appLaunch 在每個小程序?qū)嵗袝覂H會出現(xiàn)一次,且每個小程序?qū)嵗龁訒r的第一個路由事件必定為 appLaunch。
觸發(fā)方式
appLaunch 僅能由小程序冷啟動被動觸發(fā),不能由開發(fā)者主動觸發(fā),啟動后也不能通過其他用戶操作觸發(fā)。
頁面棧及生命周期處理
由于 appLaunch 必定是啟動時的第一個路由,而路由前沒有任何頁面存在,此時頁面棧必定為空。appLaunch 會創(chuàng)建路由事件指定的頁面,并將其推入頁面棧作為棧中唯一的頁面。在這個過程中,這個頁面的 onLoad, onShow 兩個生命周期將依次被觸發(fā)。
# 2. 打開新頁面
- openType:
navigateTo
打開新頁面路由 navigateTo 表示打開一個新的頁面,并將其推入頁面棧。
觸發(fā)方式
- 調(diào)用 API
wx.navigateTo,Router.navigateTo - 使用組件
<navigator open-type="navigateTo"/> - 用戶點擊一個視頻小窗(如
video)
navigateTo 的目標必須為非 tabBar 頁面。
頁面棧及生命周期處理
navigateTo 事件發(fā)生時,頁面棧當前的棧頂頁面將首先被隱藏,觸發(fā) onHide 生命周期;之后框架將創(chuàng)建路由事件指定的頁面,并將其推入頁面棧作為新的棧頂。在這個過程中,這個新頁面的 onLoad, onShow 兩個生命周期將依次被觸發(fā)。
作為一種特殊情況,如果 navigateTo 事件發(fā)生時,頁面棧當前的棧頂頁面滿足小窗模式邏輯,或事件由用戶點擊視頻小窗發(fā)起,那么頁面棧及生命周期的的處理會有所不同。
# 3. 頁面重定向
- openType:
redirectTo
頁面重定向路由 redirectTo 表示將頁面棧當前的棧頂頁面替換為一個新的頁面。
觸發(fā)方式
- 調(diào)用 API
wx.redirectTo,Router.redirectTo - 使用組件
<navigator open-type="redirectTo"/>
redirectTo 的目標必須為非 tabBar 頁面。
頁面棧及生命周期處理
redirectTo 事件發(fā)生時,頁面棧當前的棧頂頁面將首先被彈出并銷毀,在此過程中,這個棧頂頁面的 onUnload 生命周期將被觸發(fā);之后框架將創(chuàng)建路由事件指定的頁面,并將其推入頁面棧作為新的棧頂。在這個過程中,這個新頁面的 onLoad, onShow 兩個生命周期將依次被觸發(fā)。
# 4. 頁面返回
- openType:
navigateBack
頁面返回路由 navigateBack 表示將頁面棧當前的棧頂?shù)娜舾蓚€頁面依次彈出并銷毀。
觸發(fā)方式
- 調(diào)用 API
wx.navigateBack,Router.navigateBack - 使用組件
<navigator open-type="navigateBack"/> - 用戶按左上角返回按鈕,或觸發(fā)操作系統(tǒng)返回的動作(如按下系統(tǒng)返回鍵、屏幕邊緣向內(nèi)滑動等)
- 用戶點擊一個視頻小窗(如
video)
如果頁面棧中當前只有一個頁面,navigateBack 調(diào)用請求將失?。o論指定的 delta 是多少);
如果頁面棧中當前的頁面數(shù)量少于調(diào)用時指定的 delta + 1(即調(diào)用后頁面數(shù)量將少于一個),navigateBack 將彈出到只剩頁面棧當前的頁面棧底的頁面為止(即至少保留一個頁面)。
頁面棧及生命周期處理
navigateBack 事件發(fā)生時,頁面棧當前的棧頂頁面將被彈出并銷毀,并觸發(fā)這個頁面的 onUnload 生命周期;以上操作將被重復(fù)執(zhí)行多次,直到彈出的頁面數(shù)量等于指定的頁面數(shù)量,或當前頁面棧中只剩下一個頁面。之后,頁面棧新的棧頂頁面的 onShow 生命周期將被觸發(fā)。
一種特殊情況是,如果 navigateBack 發(fā)生時,頁面棧當前的棧頂頁面滿足小窗模式邏輯,或事件由用戶點擊視頻小窗發(fā)起,那么頁面棧及生命周期的的處理會有所不同。
# 5. Tab 切換
- openType:
switchTab
Tab 切換路由 switchTab 表示切換到指定的 tab 頁面。
觸發(fā)方式
- 調(diào)用 API
wx.switchTab,Router.switchTab - 使用組件
<navigator open-type="switchTab"/> - 用戶點擊 Tab Bar 中的 Tab 按鈕
switchTab 的目標必須為 tabBar 頁面。
頁面棧及生命周期處理
由于 navigateTo 和 redirectTo 不能指定 tabBar 頁面作為目標,因此當一個 tabBar 頁面出現(xiàn)在頁面棧中時,它必定為頁面棧的第一個頁面(即棧底頁面);同時,框架會保證任一 tabBar 頁面在小程序中最多同時存在一個頁面實例。switchTab 的行為主要基于這兩點進行。
switchTab 事件發(fā)生時,如果當前頁面棧中存在多于一個頁面,頁面棧當前的棧頂頁面將被彈出并銷毀,并觸發(fā)這個頁面的 onUnload 生命周期;以上操作將被重復(fù)執(zhí)行多次,直到頁面棧中只剩下一個頁面。之后,根據(jù)頁面棧中僅剩的頁面進行不同的處理:
-
如果這個頁面即為目標 tabBar 頁面:
- 如果路由事件開始時頁面棧中存在多于一個頁面(即目標 tabBar 頁面不是棧頂頁面),觸發(fā)目標 tabBar 頁面的
onShow生命周期; - 否則(路由事件開始時目標 tabBar 頁面是棧頂頁面),不觸發(fā)任何生命周期,直接結(jié)束;
- 如果路由事件開始時頁面棧中存在多于一個頁面(即目標 tabBar 頁面不是棧頂頁面),觸發(fā)目標 tabBar 頁面的
-
否則(該頁面不為目標 tabBar 頁面時):
- 將這個頁面從頁面棧中彈出;
-
如果這個頁面為其他 tabBar 頁面,該頁面成為懸垂頁面,并:
- 如果路由事件開始時頁面棧中只有一個頁面(即該 tabBar 頁面是棧頂頁面),觸發(fā)它的
onHide生命周期; - 否則(路由事件開始時該 tabBar 頁面不是棧頂頁面),不觸發(fā)它的任何生命周期;
- 如果路由事件開始時頁面棧中只有一個頁面(即該 tabBar 頁面是棧頂頁面),觸發(fā)它的
- 否則(這個頁面為非 tabBar 頁面時),銷毀該頁面,觸發(fā)
onUnload生命周期;
-
如果這個頁面為其他 tabBar 頁面,該頁面成為懸垂頁面,并:
- 如果目標 tabBar 頁之前已經(jīng)被創(chuàng)建過(現(xiàn)在是一個懸垂頁面),將其推入頁面棧,觸發(fā)
onShow生命周期; - 否則(目標 tabBar 頁不存在實例),創(chuàng)建目標 tabBar 頁并推入頁面棧,依次觸發(fā)
onLoad,onShow生命周期。
- 如果目標 tabBar 頁之前已經(jīng)被創(chuàng)建過(現(xiàn)在是一個懸垂頁面),將其推入頁面棧,觸發(fā)
# 6. 重加載
- openType:
reLaunch,autoReLaunch
重加載路由 reLaunch 或 autoReLaunch 表示銷毀當前所有的頁面,并載入一個新頁面。
重加載路由的兩種 openType 的區(qū)別主要為是否由開發(fā)者主動觸發(fā)(或是由用戶觸發(fā)),這兩種 openType 的路由邏輯基本一致。
觸發(fā)方式
- (
reLaunch)調(diào)用 APIwx.reLaunch,Router.reLaunch - (
reLaunch)使用組件<navigator open-type="reLaunch"/> - (
autoReLaunch)小程序處于后臺時,用戶從掃碼或分享等場景重新進入小程序
reLaunch 可以指定任意頁面作為目標頁面,無論它是否是小程序的首頁或是否 tabBar 頁。
請注意:reLaunch 及 autoReLaunch 僅代表一種路由,并不等于小程序重啟,小程序會在當前的 AppService 上繼續(xù)運行,既不會重新啟動 AppService 的 JavaScript 運行環(huán)境,也不會重新注入小程序代碼或觸發(fā) App.onLaunch 生命周期,各種 JS 的全局變量或全局狀態(tài)也不會被重置。
頁面棧及生命周期處理
reLaunch 或 autoReLaunch 事件發(fā)生時,頁面棧中的所有頁面將由頂至底依次被彈出并銷毀,觸發(fā) onUnload 生命周期;之后所有懸垂頁面將以不確定的順序逐個被銷毀,觸發(fā) onUnload 生命周期。所有頁面都被銷毀后,目標頁面將被創(chuàng)建,并推入頁面棧成為棧中唯一的頁面,依次觸發(fā) onLoad 和 onShow 兩個生命周期。
# 7. 關(guān)閉小窗頁面
- openType:
dismissPip
關(guān)閉小窗頁面路由 dismissPip 表示關(guān)閉一個正處于小窗模式的頁面。
# 附注
switchTab 事件的處理邏輯較為復(fù)雜,下面的表格用以展示在各種情況下進行 switchTab 時生命周期的觸發(fā)情況,作為輔助說明。在這個表格中,我們假設(shè):
tabA,tabB為 tabBar 頁面C是一個非 tabBar 頁面,并且我們只會從tabA頁面打開它D是一個非 tabBar 頁面,并且我們只會從tabB頁面打開它
| 當前頁面 | switchTab 目標頁面 | 觸發(fā)的生命周期(按順序) |
|---|---|---|
tabA | tabA | Nothing happened |
tabA | tabB | tabA.onHide(), tabB.onLoad(), tabB.onShow() |
tabA | tabB(再次打開) | tabA.onHide(), tabB.onShow() |
C | tabA | C.onUnload(), tabA.onShow() |
C | tabB | C.onUnload(), tabB.onLoad(), tabB.onShow() |
D | tabB | D.onUnload(), C.onUnload(), tabB.onLoad(), tabB.onShow() |
D(從轉(zhuǎn)發(fā)進入) | tabA | D.onUnload(), tabA.onLoad(), tabA.onShow() |
D(從轉(zhuǎn)發(fā)進入) | tabB | D.onUnload(), tabB.onLoad(), tabB.onShow() |