和 loading 界面說 ByeBye
全棧框架 Remix 宣佈開源,於是懷着好奇心去看了下官網,發現了這個口號(Say goodbye to Spinnageddon):
活整的還挺好的,再往下看看實現的效果是怎麼樣的。
預加載效果展示
鼠標移到 Link 組件上面,就已經發起了三條請求,分別是 Link 跳轉的頁面渲染所需的數據 (Fetch/XHR 類型)、模塊資源(該頁面打包後的 JS 模塊)、CSS
然後在真正點擊 Link,跳轉頁面的時候,再發起的請求,就是從響應裏拿。
還是有點東西的呀,下面我們一起來看看這預加載是如何實現的。
知識前提
本文主題在預加載,故 Remix 的寫法等就不過多描述了,只概括一下理解預加載所需要的幾點,詳細的可以參考官方文檔的快速開始 Demo
1、Remix 是一個全棧框架,且一個路由的前後端的代碼寫在同一個文件裏。路由結構依賴於文件目錄結構。
2、在一個路由文件中,可以定義 loader 函數拉取組件渲染所需的數據(GET 請求),loader 在服務端執行,返回值傳遞給路由頁面組件。
3、路由的 CSS 需要通過 export 一個 links 函數去定義(其他 link 資源也可以通過 links 函數去定義)。
舉個例子:
// 路由admin.tsx
import { useLoaderData } from"remix";
import { getPosts, PostData } from"~/containers/posts";
import adminStyles from"~/styles/admin.css";
// 加載這個頁面的Links(CSS)
exportconst links = () => {
return [{ rel: "stylesheet", href: adminStyles }];
};
// loader
exportconst loader = async () => {
return getPosts(); // 後端操作,讀文件等,此處返回一個數組
};
// 渲染admin路由組件
exportdefaultfunction Admin() {
// 通過useLoaderData拿到loader返回的內容
const posts = useLoaderData<PostData[]>() || [];
return (
<div class>
...
</div>
);
}
4、Remix 提供的 PrefetchPageLinks 組件,會 return 指定頁面所需要的原生 <link>
標籤,並設置 prefetch 屬性(prefetch 是 link 的一個實驗性屬性,即告訴瀏覽器,這個資源需要預先獲取,瀏覽器就會在網絡請求空閒時,加載該資源)。
所以渲染 <PrefetchPageLinks page={xxx} />
組件就相當於預加載了 xxx 頁面。
拓展知識:
原生 link 標籤中 preload 與 prefetch 屬性的區別:
prefetch:資源優先級低,一般用於其他頁面可能會用到的資源,瀏覽器會在網絡請求空閒時再加載該資源,離開當前頁面時不會中斷請求。
preload:資源優先級高,一般用於當前頁面重要的資源(如用於優先加載頁面所需的字體資源),瀏覽器會優先加載這個資源,離開當前頁面時會中斷請求。
更詳細內容可以參考:使用 Preload/Prefetch 優化你的應用。
如何使用預加載:
<Link prefetch="intent" to="xxx">跳轉</Link>
用戶鼠標移到這個 Link 上面時,瀏覽器就會預加載對應的 xxx 頁面資源了。
預加載原理
簡單來說
就是在原來的事件處理(比如鼠標的 hover)基礎上,加入了修改某 state 變量的邏輯,而該 state 變量會控制 PrefetchPageLinks 組件的渲染,從而實現了某事件觸發,就開始預加載的效果。
流程圖(簡潔版):
詳細來說
Remix 實現的 Link 維護了兩個狀態值:maybePrefetch、shouldPrefetch(下面簡稱 maybe、should)。
shouldPrefetch:當這個值爲 true 的時候,就會渲染 PrefetchPageLinks 組件,返回下一個頁面所需要的 <link>
標籤,瀏覽器檢測到後就發起預加載的請求。
maybePrefetch:用於 “intent” 模式的預加載。
通過修改事件處理和 maybePrefetch 與 shouldPrefetch 變量的聯繫實現這個效果的:
1、首先修改 Link 組件的事件處理,比如在 onMouseEnter 事件觸發時,先執行原有的事件函數,然後把 maybePrefetch 設置爲 true。
對相應的事件處理:
composeEventHandlers 的實現:
2、使用 useEffect 判斷當 maybePrefetch 改變且爲 true 時,設置 shouldPrefetch 爲 true,從而將兩個狀態聯繫起來。
至於這裏爲什麼用 setTimeout,暫時還不清楚,我後續再研究一下,知道的同學可以在評論區說一下。
3、當 shouldPrefetch 爲 true 的時候,渲染出 PrefetchPageLinks
組件,引入原生 <link>
標籤,瀏覽器檢測到後就發起預加載請求了。
流程圖(詳細版):
現在我們已經清楚 Remix 的 Link
組件是如何控制預渲加載了,但是 PrefetchPageLinks
如何生成具有預加載能力的原生 link
標籤,這塊還是比較模糊的,下面我們繼續探究一下 PrefetchPageLinks
。
PrefetchPageLinks 實現預加載
流程概括
匹配需要預加載的頁面路由,路由中存儲了該頁面的資源信息(用戶寫的 loader,links,以及 js 文件的地址等),然後利用 PrefetchPageLinksImpl 組件,根據路由中的信息生成對應的原生 link 標籤,即可觸發瀏覽器進行預加載。
流程圖:
其中要注意,預加載不同類型的資源,生成原生 link 的屬性設置上也有些區別,這塊在 PrefetchPageLinksImpl
組件上做了些區分。
這裏概括一下:Remix 把預加載的資源分成了三類,分別是 data、module 和 link。
-
data 爲頁面渲染所必需的數據,如用戶名稱,表格數據等。
-
module 爲頁面 js 文件,其中還包括了其 import 進來的內容。
-
link 爲 CSS 資源與設置了 preload 屬性的資源。
有了這三個資源,一個完整的頁面就可以渲染出來了。
PrefetchPageLinksImpl
組件實現如下:
而細心的你可能會發現,這三個資源,都可以直接在用戶寫的路由文件裏拿到:
data 資源:即對應路由的 loader,進入路由前會請求 loader,獲得 loader 返回的數據後渲染路由。
module 資源:即對應路由的文件地址,以及其 import 的路徑。
link 資源:即對應路由文件中導出的 links。
開發者在開發頁面的時候就**已經指定好了該頁面所需要的所有資源,**Remix 就可以在預渲染的時候,知道下一個頁面需要的所有資源,準確地發起預渲染請求。
拓展:
moudle 類型的 rel 爲 modulepreload,指明預加載內容爲模塊,modulepreload 可以看做模塊類型的 preload,而且會在請求到資源後立即進行解析,然後在需要的時候就可以直接使用,而不是在需要的時候再去解析。
詳細可以參考這裏:Preloading modules。
嵌套路由帶來的優化
除此之外,因爲 Remix 還支持嵌套路由,即把路由從頁面級細化爲組件級。(所以上文中提到的 “頁面” 均可替換爲 “路由組件”)
這樣當傳遞一個 url 給服務器時,服務器就可以通過 url 去分析出頁面需要渲染的所有路由。
而 Remix 是可以知道一個路由渲染所需要的所有資源的,即可以實現通過一個 url,就能知道對應所有路由(即組件)所需的所有資源。
這樣就可以在服務端進行的並行加載,響應一個擁有完整數據的 HTML 文檔,可以直接渲染出頁面,而非瀑布流式的渲染(瀑布流式渲染:把組件所需的數據請求寫在組件裏,需要等組件加載、渲染完後才能再發起拉取數據的請求,組件再進入 loading 等待,如此反覆)。
官方對比示例如下:
瀑布流式渲染流程:
Remix 的並行加載式渲染:
並行加載與預加載配合起來,雖然不能完全達到 say goodbye to Spinnageddon 的效果,但可以大量減少加載態。極大地提升了用戶體驗,提高了網絡利用效率(但首屏渲染時間也會加長,因爲需要等服務端拉數據)。
看到這裏,Remix 的預加載實現邏輯是不是都十分清晰了呢。從根據事件判斷用戶意圖,到結合全棧框架的優勢,把資源定義交給用戶,實現提前獲得頁面的所有資源,整體優化的角度新奇,值得學習。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/2CZ6kbE3Q4W1tZt96rJ25A