全栈框架 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 className="admin">
...
</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
。
匹配需要预加载的页面路由,路由中存储了该页面的资源信息(用户写的 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 的预加载实现逻辑是不是都十分清晰了呢。从根据事件判断用户意图,到结合全栈框架的优势,把资源定义交给用户,实现提前获得页面的所有资源,整体优化的角度新奇,值得学习。