在實踐中反應懸念
本文探討React Suspense 的工作機制、功能以及如何在實際Web 應用中集成。我們將學習如何將路由和數據加載與React 中的Suspense 集成。對於路由,我將使用原生JavaScript,並使用我自己的micro-graphql-react GraphQL 庫進行數據處理。
如果您正在考慮使用React Router,它看起來很棒,但我從未有機會使用它。我自己的個人項目有一個足夠簡單的路由方案,我一直都是手動完成的。此外,使用原生JavaScript 將使我們更好地了解Suspense 的工作原理。
簡要背景
讓我們來談談Suspense 本身。 Kingsley Silas 對其進行了全面的概述,但首先要注意的是,它仍然是一個實驗性API。這意味著——React 的文檔也這麼說——不要將其用於生產環境中的工作。在它完全完成之前,它隨時可能發生變化,所以請記住這一點。
也就是說,Suspense 的核心在於在異步依賴(例如延遲加載的React 組件、GraphQL 數據等)出現時保持一致的UI。 Suspense 提供低級別API,允許您在應用程序管理這些內容時輕鬆維護UI。
但在這種情況下,“一致”是什麼意思?這意味著不渲染部分完成的UI。這意味著,如果頁面上有三個數據源,其中一個已完成,我們不想要渲染該更新的狀態片段,以及旁邊過時的其他兩個狀態片段的加載指示器。
我們想要做的是向用戶指示數據正在加載,同時繼續顯示舊的UI,或者顯示一個替代的UI 來指示我們正在等待數據;Suspense 支持兩者,我稍後會詳細介紹。
Suspense 的確切功能
這比看起來要簡單得多。傳統上,在React 中,您會設置狀態,您的UI 會更新。生活很簡單。但它也導致了上述類型的不一致性。 Suspense 添加的功能是使組件能夠在渲染時通知React 它正在等待異步數據;這稱為掛起,它可以在組件樹中的任何位置發生,並且可以根據需要多次發生,直到樹準備好為止。當組件掛起時,React 將拒絕渲染掛起的狀態更新,直到所有掛起的依賴項都已滿足。
那麼,當組件掛起時會發生什麼? React 將向上查找樹,找到第一個<suspense></suspense>
組件,並渲染其回退內容。我將提供大量示例,但現在,請知道您可以提供以下內容:
<suspense fallback="{<Loading"></suspense> }>
……如果<suspense></suspense>
的任何子組件都掛起了,則將渲染<loading></loading>
組件。
但是,如果我們已經有了一個有效的、一致的UI,而用戶加載了新數據,導致組件掛起會怎樣?這將導致整個現有UI 取消渲染,並顯示回退內容。這仍然是一致的,但這並不是一個好的用戶體驗。我們更希望在加載新數據時,舊的UI 保持在屏幕上。
為了支持這一點,React 提供了第二個API, useTransition
,它有效地使狀態更改在內存中進行。換句話說,它允許您在內存中設置狀態,同時保持現有UI 在屏幕上;React 將在內存中字面地保留組件樹的第二個副本,並在該樹上設置狀態。組件可能會掛起,但只在內存中掛起,因此您的現有UI 將繼續顯示在屏幕上。當狀態更改完成並且所有掛起都已解決時,內存中的狀態更改將渲染到屏幕上。顯然,您希望在此過程中向用戶提供反饋,因此useTransition
提供了一個pending
布爾值,您可以使用它來顯示某種內聯“加載”通知,同時在內存中解決掛起。
當您考慮它時,您可能不希望在加載掛起時無限期地顯示現有UI。如果用戶嘗試執行某些操作,並且在完成之前經過很長時間,您可能應該考慮現有UI 已過時且無效。此時,您可能確實希望您的組件樹掛起,並顯示<suspense></suspense>
回退內容。
為了實現這一點, useTransition
採用timeoutMs
值。這表示您願意讓內存中狀態更改運行的時間量,然後再掛起。
const Component = props => { const [startTransition, isPending] = useTransition({ timeoutMs: 3000 }); // ..... };
在這裡, startTransition
是一個函數。當您想要“在內存中”運行狀態更改時,您可以調用startTransition
並傳遞一個執行狀態更改的lambda 表達式。
startTransition(() => { dispatch({ type: LOAD_DATA_OR_SOMETHING, value: 42 }); });
您可以在任何地方調用startTransition
。您可以將其傳遞給子組件等。當您調用它時,您執行的任何狀態更改都將在內存中發生。如果發生掛起, isPending
將變為true,您可以使用它來顯示某種內聯加載指示器。
就是這樣。這就是Suspense 的作用。
本文的其餘部分將介紹一些實際代碼來利用這些功能。
示例:導航
為了將導航與Suspense 關聯起來,您會很高興知道React 提供了一個原語來執行此操作: React.lazy
。它是一個函數,它接受一個返回Promise 的lambda 表達式,該Promise 解析為一個React 組件。此函數調用的結果將成為您的延遲加載組件。聽起來很複雜,但它看起來像這樣:
const SettingsComponent = lazy(() => import("./modules/settings/settings"));
SettingsComponent
現在是一個React 組件,當渲染時(但不是之前),它將調用我們傳入的函數,該函數將調用import()
並加載位於./modules/settings/settings
的JavaScript 模塊。
關鍵部分是:在import()
正在進行時,渲染SettingsComponent
的組件將掛起。看起來我們已經掌握了所有組件,所以讓我們將它們放在一起並構建一些基於Suspense 的導航。
導航輔助程序
但首先,為了提供上下文,我將簡要介紹此應用程序中導航狀態的管理方式,以便Suspense 代碼更有意義。
我將使用我的booklist 應用程序。它只是我自己的一個個人項目,我主要用來試驗前沿的Web 技術。它是我獨自編寫的,因此預計其中某些部分會有點粗糙(尤其是設計)。
該應用程序很小,用戶可以瀏覽大約八個不同的模塊,沒有任何更深層次的導航。模塊可能使用的任何搜索狀態都存儲在URL 的查詢字符串中。考慮到這一點,有一些方法可以從URL 中提取當前模塊名稱和搜索狀態。這段代碼使用npm 的query-string
和history
包,看起來有點像這樣(為了簡單起見,刪除了一些細節,例如身份驗證)。
import createHistory from "history/createBrowserHistory"; import queryString from "query-string"; export const history = createHistory(); export function getCurrentUrlState() { let location = history.location; let parsed = queryString.parse(location.search); return { pathname: location.pathname, searchState: parsed }; } export function getCurrentModuleFromUrl() { let location = history.location; return location.pathname.replace(/\//g, "").toLowerCase(); }
我有一個appSettings
reducer,它保存應用程序的當前模塊和searchState
值,並在需要時使用這些方法與URL 同步。
基於Suspense 的導航組件
讓我們開始進行一些Suspense 工作。首先,讓我們為我們的模塊創建延遲加載的組件。
const ActivateComponent = lazy(() => import("./modules/activate/activate")); const AuthenticateComponent = lazy(() => import("./modules/authenticate/authenticate")); const BooksComponent = lazy(() => import("./modules/books/books")); const HomeComponent = lazy(() => import("./modules/home/home")); const ScanComponent = lazy(() => import("./modules/scan/scan")); const SubjectsComponent = lazy(() => import("./modules/subjects/subjects")); const SettingsComponent = lazy(() => import("./modules/settings/settings")); const AdminComponent = lazy(() => import("./modules/admin/admin"));
現在我們需要一個根據當前模塊選擇正確組件的方法。如果我們使用React Router,我們將有一些不錯的<route></route>
組件。由於我們正在手動執行此操作,因此switch
語句將可以。
export const getModuleComponent = moduleToLoad => { if (moduleToLoad == null) { return null; } switch (moduleToLoad.toLowerCase()) { case "activate": return ActivateComponent; case "authenticate": return AuthenticateComponent; case "books": return BooksComponent; case "home": return HomeComponent; case "scan": return ScanComponent; case "subjects": return SubjectsComponent; case "settings": return SettingsComponent; case "admin": return AdminComponent; } return HomeComponent; };
將所有內容放在一起
在完成所有枯燥的設置之後,讓我們看看整個應用程序根目錄是什麼樣的。這裡有很多代碼,但我保證,其中相對較少的行與Suspense 相關,我將介紹所有這些內容。
const App = () => { const [startTransitionNewModule, isNewModulePending] = useTransition({ timeoutMs: 3000 }); const [startTransitionModuleUpdate, moduleUpdatePending] = useTransition({ timeoutMs: 3000 }); let appStatePacket = useAppState(); let [appState, _, dispatch] = appStatePacket; let Component = getModuleComponent(appState.module); useEffect(() => { startTransitionNewModule(() => { dispatch({ type: URL_SYNC }); }); }, []); useEffect(() => { return history.listen(location => { if (appState.module != getCurrentModuleFromUrl()) { startTransitionNewModule(() => { dispatch({ type: URL_SYNC }); }); } else { startTransitionModuleUpdate(() => { dispatch({ type: URL_SYNC }); }); } }); }, [appState.module]); return ( <appcontext.provider value="{appStatePacket}"> <moduleupdatecontext.provider value="{moduleUpdatePending}"> <div> <mainnavigationbar></mainnavigationbar> {isNewModulePending ?<loading></loading> : null} <suspense fallback="{<LongLoading"></suspense> }> <div style="{{" flex: overflowy:> {Component ?<component updating="{moduleUpdatePending}"></component> : null} </div> </div> </moduleupdatecontext.provider> </appcontext.provider> ); };
首先,我們對useTransition
進行了兩次不同的調用。我們將一個用於路由到新模塊,另一個用於更新當前模塊的搜索狀態。為什麼會有區別?好吧,當模塊的搜索狀態正在更新時,該模塊可能希望顯示內聯加載指示器。該更新狀態由moduleUpdatePending
變量保存,您將看到我將其放在上下文中,以便活動模塊可以獲取並根據需要使用:
<div> <mainnavigationbar></mainnavigationbar> {isNewModulePending ?<loading></loading> : null} <suspense fallback="{<LongLoading"></suspense> }> <div style="{{" flex: overflowy:> {Component ?<component updating="{moduleUpdatePending}"></component> : null} </div> </div>
appStatePacket
是上面討論的(但未顯示)應用程序狀態reducer 的結果。它包含各種應用程序狀態片段,這些片段很少更改(顏色主題、脫機狀態、當前模塊等)。
let appStatePacket = useAppState();
稍後,我將根據當前模塊名稱獲取任何處於活動狀態的組件。最初這將為null。
let Component = getModuleComponent(appState.module);
對useEffect
的第一次調用將告訴我們的appSettings
reducer 在啟動時與URL 同步。
useEffect(() => { startTransitionNewModule(() => { dispatch({ type: URL_SYNC }); }); }, []);
由於這是Web 應用程序導航到的初始模塊,因此我將其包裝在startTransitionNewModule
中以指示正在加載新的模塊。雖然將應用程序設置reducer 的初始模塊名稱作為其初始狀態可能很誘人,但這阻止我們調用startTransitionNewModule
回調,這意味著我們的Suspense 邊界將立即渲染回退內容,而不是在超時之後。
對useEffect
的下一次調用設置了一個歷史記錄訂閱。無論如何,當url 更改時,我們都會告訴我們的應用程序設置與URL 同步。唯一的區別是同一調用包裝在哪個startTransition
中。
useEffect(() => { return history.listen(location => { if (appState.module != getCurrentModuleFromUrl()) { startTransitionNewModule(() => { dispatch({ type: URL_SYNC }); }); } else { startTransitionModuleUpdate(() => { dispatch({ type: URL_SYNC }); }); } }); }, [appState.module]);
如果我們正在瀏覽到一個新的模塊,我們將調用startTransitionNewModule
。如果我們正在加載尚未加載的組件, React.lazy
將掛起,並且只有應用程序根目錄可見的掛起指示器將設置,這將在獲取和加載延遲加載的組件時在應用程序頂部顯示加載微調器。由於useTransition
的工作方式,當前屏幕將繼續顯示三秒鐘。如果超過該時間並且組件仍未準備好,我們的UI 將掛起,並且將渲染回退內容,這將顯示<longloading></longloading>
組件:
{isNewModulePending ?<loading></loading> : null} <suspense fallback="{<LongLoading"></suspense> }> <div style="{{" flex: overflowy:> {Component ?<component updating="{moduleUpdatePending}"></component> : null} </div>
如果我們沒有更改模塊,我們將調用startTransitionModuleUpdate
:
startTransitionModuleUpdate(() => { dispatch({ type: URL_SYNC }); });
如果更新導致掛起,我們將放在上下文中的掛起指示器將被觸發。活動組件可以檢測到這一點並顯示它想要的任何內聯加載指示器。和以前一樣,如果掛起時間超過三秒鐘,則將觸發相同的Suspense 邊界……除非,正如我們稍後將看到的,樹中較低的位置存在Suspense 邊界。
需要注意的一件重要事情是,這三秒鐘的超時不僅適用於組件加載,還適用於準備好顯示。如果組件在兩秒鐘內加載,並且在內存中渲染時(因為我們在startTransition
調用內)掛起,則useTransition
將繼續等待最多一秒鐘,然後再掛起。
在撰寫這篇博文時,我使用了Chrome 的慢速網絡模式來幫助強制加載變慢,以測試我的Suspense 邊界。設置位於Chrome 開發工具的“網絡”選項卡中。
讓我們打開我們的應用程序到設置模塊。這將被稱為:
dispatch({ type: URL_SYNC });
我們的appSettings
reducer 將與URL 同步,然後將模塊設置為“settings”。這將在startTransitionNewModule
內發生,以便當延遲加載的組件嘗試渲染時,它將掛起。由於我們在startTransitionNewModule
內,因此isNewModulePending
將切換到true,並且將渲染<loading></loading>
組件。
那麼,當我們瀏覽到新的地方時會發生什麼?基本上與之前相同,除了這個調用:
dispatch({ type: URL_SYNC });
……將來自useEffect
的第二個實例。讓我們瀏覽到書籍模塊並看看會發生什麼。首先,內聯微調器按預期顯示:
(此處應插入屏幕截圖)
搜索和更新
讓我們留在書籍模塊中,並更新URL 查詢字符串以啟動新的搜索。回想一下,我們在第二個useEffect
調用中檢測到相同的模塊並為此使用專用useTransition
調用。從那裡,我們將掛起指示器放在上下文中,以便任何對我們來說都可以獲取和使用的活動模塊。
讓我們看看一些實際使用它的代碼。這裡沒有太多與Suspense 相關的代碼。我從上下文中獲取值,如果為真,則在我的現有結果頂部渲染內聯微調器。回想一下,當useTransition
調用已開始並且應用程序在內存中掛起時,就會發生這種情況。在此期間,我們將繼續顯示現有UI,但帶有此加載指示器。
const BookResults = ({ books, uiView }) => { const isUpdating = useContext(ModuleUpdateContext); return ( <div> {!books.length ? ( <div classname="alert alert-warning" style="{{" margintop: marginright:> No books found </div> ) : null} {isUpdating ?<loading></loading> : null} {uiView.isGridView ? ( <gridview books="{books}"></gridview> ) : uiView.isBasicList ? ( <basiclistview books="{books}"></basiclistview> ) : uiView.isCoversList ? ( <coversview books="{books}"></coversview> ) : null} </div> ); };
讓我們設置一個搜索詞並看看會發生什麼。首先,內聯微調器顯示。
然後,如果useTransition
超時,我們將獲得Suspense 邊界的回退內容。書籍模塊定義了自己的Suspense 邊界,以便提供更精細的加載指示器,如下所示:
(此處應插入屏幕截圖)
這是一個關鍵點。在創建Suspense 邊界回退時,盡量不要顯示任何微調器和“正在加載”消息。這對於我們的頂級導航是有意義的,因為沒有其他事情可做。但是,當您在應用程序的特定部分時,請嘗試使回退內容重用許多相同的組件,並在數據所在的位置顯示某種加載指示器——但所有其他內容都已禁用。
這是我的書籍模塊的相關組件的樣子:
const RenderModule = () => { const uiView = useBookSearchUiView(); const [lastBookResults, setLastBookResults] = useState({ totalPages: 0, resultsCount: 0 }); return ( <div classname="standard-module-container margin-bottom-lg"> <suspense fallback="{<Fallback" uiview="{uiView}"></suspense> }> <maincontent setlastbookresults="{setLastBookResults}" uiview="{uiView}"></maincontent> </div> ); }; const Fallback = ({ uiView, totalPages, resultsCount }) => { return ( <div> <booksmenubardisabled resultscount="{resultsCount}" totalpages="{totalPages}"></booksmenubardisabled> {uiView.isGridView ? ( <gridviewshell></gridviewshell> ) : ( <h1> Books are loading<i classname="fas fa-cog fa-spin"></i> </h1> )} </div> ); };
關於一致性的快速說明
在我們繼續之前,我想指出前面屏幕截圖中的一件事。查看搜索掛起時顯示的內聯微調器,然後查看該搜索掛起時的屏幕,然後查看完成的結果:
(此處應插入屏幕截圖)
注意搜索窗格右側是否有“C ”標籤,以及是否有選項可以將其從搜索查詢中刪除?或者,注意該標籤僅在後兩張屏幕截圖上? URL 更新的那一刻,控制該標籤的應用程序狀態確實已更新;但是,該狀態最初不會顯示。最初,狀態更新在內存中掛起(因為我們使用了useTransition
),並且先前的UI 繼續顯示。
然後回退內容呈現。回退內容呈現該相同搜索欄的禁用版本,該版本確實顯示了當前搜索狀態(根據選擇)。我們現在已刪除了先前的UI(因為現在它已經相當舊且陳舊),並且正在等待禁用菜單欄中顯示的搜索。
這就是Suspense 免費為您提供的一致性類型。
您可以花時間製作精美的應用程序狀態,而React 會完成推斷事物是否已準備就緒的工作,而無需您處理Promise。
嵌套Suspense 邊界
假設我們的頂級導航需要一段時間才能將我們的書籍組件加載到我們的“仍然加載中,抱歉”微調器從Suspense 邊界呈現的程度。從那裡,書籍組件加載,並且書籍組件內的新的Suspense 邊界呈現。但是,然後,在渲染繼續時,我們的書籍搜索查詢觸發並掛起。會發生什麼?頂級Suspense 邊界是否會繼續顯示,直到一切準備就緒,或者書籍中的較低級別的Suspense 邊界是否會接管?
答案是後者。當新的Suspense 邊界在樹中較低的位置呈現時,它們的回退內容將替換已經顯示的任何先前Suspense 回退內容的回退內容。目前有一個不穩定的API 可以覆蓋此功能,但是如果您正在很好地製作回退內容,這可能是您想要的行為。您不希望“仍然加載中,抱歉”繼續顯示。相反,一旦書籍組件準備好,您絕對希望顯示帶有更具針對性的等待消息的shell。
現在,如果我們的書籍模塊在startTransition
微調器仍在顯示然後掛起時加載並開始渲染會怎樣?換句話說,假設我們的startTransition
超時時間為三秒鐘,書籍組件渲染,嵌套的Suspense 邊界在一秒鐘後位於組件樹中,並且搜索查詢掛起。在該新的嵌套Suspense 邊界呈現回退內容之前,剩餘的兩秒鐘是否會過去,或者回退內容是否會立即顯示?答案可能令人驚訝,默認情況下,新的Suspense 回退內容將立即顯示。這是因為最好盡快顯示新的有效UI,以便用戶可以看到事情正在發生並且正在進展。
數據如何融入
導航很好,但是數據加載如何融入這一切?
它完全透明地融入其中。數據加載與使用React.lazy
的導航一樣觸發掛起,並且它與所有相同的useTransition
和Suspense 邊界掛鉤。這就是Suspense 如此令人驚嘆的原因:所有異步依賴項都無縫地在這個相同的系統中工作。在Suspense 之前,手動管理這些各種異步請求以確保一致性是一場噩夢,這正是沒有人這樣做的原因。 Web 應用程序因級聯微調器而臭名昭著,這些微調器在不可預測的時間停止,產生僅部分完成的不一致UI。
好的,但是我們如何實際將數據加載與之關聯?在Suspense 中加載數據既更複雜,又很簡單。
我來解釋一下。
如果您正在等待數據,您將在讀取(或嘗試讀取)數據的組件中拋出一個Promise。 Promise 應基於數據請求保持一致。因此,對同一“C ”搜索查詢的四個重複請求應拋出相同且相同的Promise。這意味著某種緩存層來管理所有這些。您可能不會自己編寫這個。相反,您只會希望並等待您使用的數據庫更新自身以支持Suspense。
在我的micro-graphql-react 庫中已經完成了這項工作。您將使用useSuspenseQuery
掛鉤而不是useQuery
掛鉤,它具有相同的API,但在您等待數據時會拋出一個一致的Promise。
等等,預加載呢? !
閱讀其他關於Suspense 的內容,談論瀑布、渲染時獲取、預加載等,您的大腦是否已經變得糊塗了?別擔心。這就是這一切的含義。
假設您延遲加載書籍組件,該組件然後請求一些數據,這會導致新的Suspense。組件的網絡請求和數據的網絡請求將一個接一個地發生——以瀑布的方式。
但關鍵部分是:當您開始加載組件時,導致任何初始查詢運行的應用程序狀態已經可用(在這種情況下是URL)。那麼,為什麼不在您知道需要它時立即“啟動”查詢呢?一旦您瀏覽到/books,為什麼不在那時立即啟動當前搜索查詢,以便在組件加載時它已經在進行中?
micro-graphql-react 模塊確實有一個預加載方法,我建議您使用它。預加載數據是一種不錯的性能優化,但它與Suspense 無關。經典的React 應用程序可以(也應該)在知道需要數據時立即預加載數據。 Vue 應用程序應該在知道需要數據時立即預加載數據。 Svelte 應用程序應該……你明白了。
預加載數據與Suspense 正交,這是您可以使用任何框架執行的操作。這也是我們所有人本來就應該已經做的事情,儘管其他人沒有做。
但是說真的,您是如何預加載的?
這取決於您。至少,運行當前搜索的邏輯絕對需要完全分離到它自己的獨立模塊中。您應該從字面上確保此預加載函數位於單獨的文件中。不要依賴webpack 進行樹狀抖動;下次審核您的包時,您可能會面臨徹底的悲傷。
您在它自己的包中有一個preload()
方法,因此請調用它。當您知道要導航到該模塊時,請調用它。我假設React Router 有一些API 可以在導航更改時運行代碼。對於上面的原生路由代碼,我在之前的路由切換中調用該方法。為了簡潔起見,我省略了它,但是書籍條目實際上看起來像這樣:
switch (moduleToLoad.toLowerCase()) { case "activate": return ActivateComponent; case "authenticate": return AuthenticateComponent; case "books": // 預加載! ! ! booksPreload(); return BooksComponent;
就是這樣。這是一個可以試玩的實時演示:
(此處應插入鏈接)
要修改Suspense 超時值(默認為3000 毫秒),請導航到設置,然後查看其他選項卡。修改後,請務必刷新頁面。
總結
我對Web 開發生態系統中的任何東西都像對Suspense 那樣興奮過。它是一個極其雄心勃勃的系統,用於管理Web 開發中最棘手的問題之一:異步性。
以上是在實踐中反應懸念的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

您是否曾經在項目上需要一個倒計時計時器?對於這樣的東西,可以自然訪問插件,但實際上更多

關於Flex佈局中紫色斜線區域的疑問在使用Flex佈局時,你可能會遇到一些令人困惑的現象,比如在開發者工具(d...

在元素個數不固定的情況下如何通過CSS選擇第一個指定類名的子元素在處理HTML結構時,常常會遇到元素個數不�...
