背景
在货拉拉小程序中有这样一个场景:从订单列表点击进入订单详情,由于订单详情是一个分包,所以可能会出现两次 loading,第一次是小程序分包加载,第二次是进入页面后接口请求。
而订单详情页面的静态资源加载时间主要分布在 200ms ~ 600ms 之间,接口的平均耗时在 800ms 左右。在订单列表点击的时候已经知道了要查看的订单,何不在点击的时候预请求订单详情接口,节省一个出接口加载的时间呢?
如何实现?
首先想到的,把数据放到全局状态里面。
列表页的代码:
js
navigateToOrderDetail({ order_uuid });
// 在跳转的同时请求详情数据
const orderDetail = await getOrderDetail({ order_uuid });
store.commit('setOrderDetail', orderDetail);
在订单详情页的代将原本的请求换成从 store 里面取数据。
js
// const orderDetail = await getOrderDetail({ order_uuid })
const orderDetail = store.state.orderDetail;
上机测试,问题来了:
- 我们知道大部分用户用户页面加载都小于 600ms,此时接口 (800ms) 还未请求完,store 里面还没有值。
- 有些场景是从外部直接进入订单详情,此时去读 store 也没有值。
而且此方案不够易用,如果要改造其他页面,那岂不是每个页面数据都要在 store 放一份?
目标是什么?
测试过后发现问题所在:在写代码之前都没有考虑下各种场景,这怎么能写出想要的代码呢?
赶紧先罗列一下我们的目标:
- 首先,对现有代码改动不能太大,最好就是两个函数,
preload
和usePreload
,将现有请求进行包裹。 - 在
preload
正在请求时,usePreload
要等待结果。 - 如果没有经过
preload
,那usePreload
需要正常发起请求。 usePreload
只有第一次会使用preload
的缓存,后面请求需要正常发起 (不然页面就没法刷新啦)。
有了目标,我们开始写代码。
由于是跨页面使用,我们需要一个 key 将 preload
和 usePreload
关联起来。
js
// utils/preload.js
const preloadMap = {};
export function preload(key, request) {
preloadMap[key] = request();
}
export function usePreload(key, request) {
const cache = preloadMap[key];
if (cache) {
preloadMap[key] = null;
return cache;
}
return request();
}
js
import { preload, usePreload } from '@/utils/preload';
navigateToOrderDetail({ order_uuid });
preload('orderDetail', () => getOrderDetail({ order_uuid }));
const orderDetail = await usePreload('orderDetail', () =>
getOrderDetail({ order_uuid }),
);
等一下,代码就这么简单?是的,原因在于所有 api 请求都已经使用 Promise
进行封装。
从目标 2 (在 preload
正在请求时,usePreload
要等待结果) 可以猜出,要实现这个能力,非 Promise
莫属。
Promise
大家都很熟悉了,俗话说,没手写过 Promise
的前端都不是好厨师。
我们这里用到它的一个特性:preload
和 usePreload
返回同一个 Promise
,如果页面先加载完而接口尚未返回,此时 Promise
在 pending
中,页面还是会正常展示请求 loading,但是请求时间减少为 800 - 页面加载时间
;如果接口先返回,页面加载完后 Promise
已经是 fulfilled
状态,可以直接读取结果进行展示,此时接口请求时间为 0。
上机测试,完美实现 4 个小目标,收工!
不过在上线使用后,我们遇到了一些问题,然后又有了下面这几个新的目标:
- 有些页面数据只要参数不变,返回数据是相同的,能不能在
usePreload
的时候判断一下如果参数不变,就不移除缓存呢?这样不仅页面快了,请求量还少了。 - 如果实现了第 5 点,那想在参数不变的时候强制刷新该怎么做呢?
- 虽然参数没变,但是缓存的数据是无效的,因为接口请求失败了,该怎么处理呢?
- 如果传入函数返回的不是一个
Promise
,而在使用的时候.then
读取不就会报错吗?
要实现缓存,我们需要知道请求参数,只能增加第三个参数传入了:
js
// 将参数依赖传入 usePreload,判断依赖相同则返回缓存,否则发起新的请求
usePreload('driverInfo', () => getDriverInfo({ driverId, userId }), [
driverId,
userId,
]);
需要修改的点有:
- 修改
preloadMap
的结构,能够将每个请求对应的参数保存起来。 - 返回一定要是个
Promise
,所以我们需要用Promise
对传入的request
进行包裹。
js
// utils/preload.js
const preloadMap = {};
export function preload(key, request, deps) {
preloadMap[key] = {
promise: Promise.resolve(request()),
deps,
};
}
export function usePreload(key, request, deps) {
const cache = preloadMap[key];
if (cache) {
const isDepsSame = deps.every((item, index) =>
Object.is(item, cache.deps[index]),
);
if (isDepsSame) {
return cache.promise;
}
preloadMap[key] = null;
return cache;
}
return request();
}
用 Promise.resolve(request())
是可以将返回变成 Promise
,但这样不就永远都返回 fulfilled
状态了吗?
嘿嘿,小伙子,看来 Promise
基本功还不够扎实啊~(我不会说我又补过了才这么写的)
简单来说,不管嵌套多少层 Promise
,只要中间没有 catch
拦截,在外面都是可以被 catch
到的。
另外,还有 6 和 7 两个目标还没实现呢,但是没关系,上面只是展示一下它的诞生过程,现在你只需要安装它然后使用。
@huolala-tech/preload-js
它的功能如上面所介绍,代码也开源了:github.com/HuolalaTech...
bash
npm i @huolala-tech/preload-js
- 预请求:
js
import { preload } from '@huolala-tech/preload-js';
// pageA
preload('pageBData', pageBRequest);
navigateTo('pageB');
// pageB
const data = await usePreload('pageBData', pageBRequest);
- 缓存:
js
import { usePreload } from '@huolala-tech/preload-js';
const params = {
data_id: 123,
user_id: 456,
};
const data = usePreload(
'someData',
() => requestData(params),
Object.values(params),
);
- 强制刷新
js
import { usePreload, useDep } from '@huolala-tech/preload-js';
const [dep, refreshDep] = useDep();
const userId = store.state.userId;
const userInfo = await usePreload('userInfo', () => getUserInfo({ userId }), [
userId,
dep, // 同依赖参数一样传入
]);
function updateUserInfo() {
refreshDep();
}
- Promise.catch 自动清除缓存
js
import { setConfig } from '@huolala-tech/preload-js';
// 默认 removeOnCatch 为 true
setConfig({ removeOnCatch: false }); // 此设置为全局生效
js
import { usePreload, removeOnCatch } from '@huolala-tech/preload-js';
// 单独设置请求
const data = await usePreload('userInfo', () => getUserInfo({ userId }), [
useId,
removeOnCatch(false), // 和参数依赖一样传入
]);
新的目标
能否实现持久化本地缓存?即使在断网情况下打开,页面依然能渲染上一次的数据?
本文对 preload-js 的实现原理做了简单的介绍,完整的代码点击 HuolalaTech/preload-js 查看。如果你有使用上的问题,欢迎到 Github 上讨论。