h5数据预取方案

背景

在页面的性能优化中,资源加载是决定页面展示时间的重中之重。为了减少资源的请求,有两个常见的解决方案:

1.一个是SSR服务端渲染页面,这样可以减少页面的初始化数据请求,页面在服务端接受到请求时进行数据请求与渲染页面,最终返回给客户端一个可以直接展示的html页面(直出)。但这个方案需要服务器的支持。
2.另一方是SSG服务端生成页面。与SSR的区别是,SSG在构建时就会生成html文件,后续的数据获取需要客户端加载了页面资源后再进行。SSG的优势是首屏加载速度快,可以直接分发资源到CDN,减少服务器压力。更进一步可以使用离线包方案,客户端使用本地资源打开页面,最大化减少网络的影响。

目前我们采用了第二种方案:SSG与离线包相结合。但在动态页面中,页面快速展示后,可以看到明显的页面卡顿,这是由于页面需要从服务端获取动态数据。本文便是为了解决这一问题。

期望效果

期望能结合目前的离线包方案在页面展示时,没有卡顿,能直接获取到动态数据并展示。

注意事项:

1.目前项目中存在离线包与在线包两种环境的页面,离线包方式是通过客户端代理页面的方式实现,两个包的域名不同,所以在获取动态数据时,需要注意不同包的数据获取。
2.部分页面的数据与当前时间状态深度绑定,这种情况下应避免得页面预取数据与页面展示相隔太久。

解决方案

1.例如百度(CloudHybrid)、腾讯(VasSonic)等大厂都在公司内部研发了相应的框架,特点是客户端在请求页面时,提前缓存页面与请求服务端获取动态数据,再根据获取到的数据与缓存页面进行水合。但该方案需要全端配合,复杂度较高。
2.最简单的方案就是在客户端初始化时,后台加载一个预取数据页面,这个页面会将数据存储起来,后续前端使用时直接调用。这个方案大幅前置了数据预取的过程,所以在页面首屏加载后,仍需要再次请求接口更新数据,防止期间数据变动,且发生数据变动时会导致页面数据闪动。

由于人力与成本考虑,项目使用第二种方案,下面介绍详细流程:

1.客户端在用户打开APP,初始化应用时,在后台加载预取数据页面。
2.预取数据页面从服务端获取动态数据。
3.将数据存储至本地缓存。这里建议使用localforage库,该库会自动选择IndexedDB、WebSQL、localStorage中的一种存储方式,且自动进行类型转换。
4.进入应用后,用户访问功能页面。
5.页面先使用本地缓存数据展示页面。这里注意由于页面使用ssg的方案,如果展示加载的骨架屏,依然会出现数据从无到有的闪动出现。所以需要直接将页面首屏置空,等js加载完成获取到本地数据后再显示页面(使用useEffect触发)。
6.由于动态数据可能更新,所以在页面加载后,需要再次请求服务端接口,更新页面(如数据变动,会造成页面闪动)。
7.最后将更新后的数据再次存入本地存储。

附录

// 结合localforage的hooks
import { useCallback, useEffect, useRef, useState } from 'react';
import localforage from 'localforage';

let EventMap = new Map<String, Function[]>();
class EventEmitter {static on<T>(key: string, callback: (value: T) => void) {if (EventMap.has(key)) {EventMap.get(key)?.push(callback);} else {EventMap.set(key, [callback]);}return () => {const funcList = EventMap.get(key);EventMap.set(key,funcList!.filter((func) => func !== callback),);};}static emit<T>(key: string, value: T) {if (EventMap.has(key)) {EventMap.get(key)?.forEach((func) => {func(value);});}}
}

export function useLocalStorage<T>(key: string,defaultValue: T,pathname?: string,
) {const refKey = useRef(key);const refInit = useRef(false);function getStoredValue() {return new Promise<T>((resolve) => {localforage.getItem(refKey.current).then((raw) => {if (typeof raw !== 'undefined' && raw !== null) {resolve(raw as T);} else {resolve(defaultValue);}}).catch((e) => {console.error(e);resolve(defaultValue);});});}const [state, setState] = useState<T>(defaultValue);const [initSetList, setInitSetList] = useState<Function[]>([]);useEffect(() => {const path = pathname || location.pathname.replace(/\//g, '_');refKey.current = `${path}-${key}`;getStoredValue().then((value) => {setState(value);if (initSetList.length) {initSetList.forEach((func) => func());}});refInit.current = true;}, [key]);useEffect(() => {const handleEventEmitter = (eventValue: T) => {if (JSON.stringify(eventValue) !== JSON.stringify(state)) {updateState(eventValue, true);}};const removeHandler = EventEmitter.on<T>(key, handleEventEmitter);return () => {removeHandler();};}, [state]);const updateState = useCallback((value: T, isEmit?: boolean) => {function updateForageState(currentState: T) {setState(currentState);if (typeof currentState === 'undefined') {localforage.removeItem(refKey.current);} else {localforage.setItem(refKey.current, currentState).then(() => {if (!isEmit) {console.log('emit');EventEmitter.emit(key, currentState);}}).catch((e) => {console.error(e);});}}if (!refInit) {setInitSetList((list) => [...list,updateForageState.bind(useLocalStorage, value),]);} else {updateForageState(value);}},[refKey, refInit],);return [state, updateState] as const;
} 

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

猜你喜欢

转载自blog.csdn.net/web220507/article/details/129755121