“人如其字”,代码亦是如此。优雅的代码往往体现出一位Coder高深的逼格
背景
你是一位刚入职的新人前端开发者,入职第一天PM带着PRD跑到你面前,说到:我需要你实现一个页面,这个页面中需要展示一些列表数据。你回答道:okok
import React, { useState, useEffect } from 'react';
export const Page: React.FC = () => {
const [list, setList] = useState([]);
useEffect(() => {
fetch('xxx').then(res => setList(res.data));
}, []);
return (
<div>
{list.map(item => (
<div key={item}>{item}</div>
))}
</div>
);
};
写好后部署上线,PM看后说道:我这网好差,为了提升用户体验,我还需要你写一个骨架屏。你心想这也不难,看我的!
import React, { useState, useEffect } from 'react';
export const Page: React.FC = () => {
const [loading, setLoading] = useState(true);
const [list, setList] = useState([]);
useEffect(() => {
fetch('xxx').then(res => {
setLoading(false);
setList(res.data);
});
}, []);
if (loading) {
return <div>这是骨架屏</div>;
}
return (
<div>
{list.map(item => (
<div key={item}>{item}</div>
))}
</div>
);
};
写完后,你就提测了。提测中,QA同学给你提了一个bug:后端接口出现问题时,页面白屏。你一想,确实,自己没有做任何的错误处理,急急忙忙修了一版:
import React, { useState, useEffect } from 'react';
export const Page: React.FC = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [list, setList] = useState([]);
useEffect(() => {
fetch('xxx').then(res => {
setLoading(false);
setList(res.data);
}).catch(err => setError(err));
}, []);
if (loading) {
return <div>这是骨架屏</div>;
}
if (error) {
return <div>出错了...</div>;
}
return (
<div>
{list.map(item => (
<div key={item}>{item}</div>
))}
</div>
);
};
修改完这个问题后,QA验收也通过了,但组内CR的时候,又遇到了问题。你这个我好像也用到了,要不你抽离出来,避免之后写重复的代码。于是你又开始了修改:
import React, { useState, useEffect } from 'react';
const useFetch = url => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then(res => {
setLoading(false);
setData(res.data);
})
.catch(err => setError(err));
}, [url]);
return { data, loading, error };
};
export const Page: React.FC = () => {
const { data, loading, error } = useFetch('xxx');
if (loading) {
return <div>这是骨架屏</div>;
}
if (error) {
return <div>出错了...</div>;
}
return (
<div>
{data.map(item => (
<div key={item}>{item}</div>
))}
</div>
);
};
完成CR后,你成功上线!但第二个优化需求如约而至,PM发给你一个优化PRD说到,这一期我们要增加一个Tabs,用来切换列表数据。你按照需求写的时候发现现在抽离出来的hooks不能满足你这的需求,在连续切换Tabs的时候,很容易就出现了显示错误,后来你看了React官网后发现原来是没有在useEffect中去清除你的副作用导致的,经历了一系列改动后,这个内容你的开发也步入了尾声
const useFetch = url => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
useEffect(() => {
let needSetData = false;
setLoading(true);
fetch(url)
.then(res => {
if (!needSetData) {
setData(res.data);
setLoading(false);
}
})
.catch(err => {
setError(err);
setLoading(false);
});
return () => {
needSetData = true;
setLoading(false);
};
}, [url]);
return { data, loading, error };
};
通过js闭包的能力,切换tabs导致数据不一致得到了解决,但你的写法又被CR的时候提出了问题。这个来回切换要一直请求数据,是不是性能不太好,这块可以优化一下。于是你又开始了修改,避免连续请求那我就存起来吧
const cacheMap = new Map();
const useFetch = url => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
useEffect(() => {
let needSetData = false;
setLoading(true);
if (cacheMap.has(url)) {
setData(cacheMap.get(url));
setLoading(false);
} else {
fetch(url)
.then(res => {
if (!needSetData) {
cacheMap.set(url, data);
setData(res.data);
setLoading(false);
}
})
.catch(err => {
setError(err);
setLoading(false);
});
}
return () => {
needSetData = true;
setLoading(false);
};
}, [url]);
return { data, loading, error };
};
在书写完成后,新的优化需求如约而至,这次要将列表作为实时数据,所以要支持下拉更新。由于列表是一个可变的。你最初的想法是暴露一个函数出去刷新,但后来想想对于这个解决方案,你自己觉得还是有一些优化空间,例如可以让页面在用户点到后自动刷新
const cacheMap = new Map();
const useFetch = url => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
const handleFetch = useCallback(() => {
fetch(url)
.then(res => {
cacheMap.set(url, data);
setData(res.data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [url]);
const remove = useCallback(() => {
cacheMap.delete(url);
}, [url]);
const mutate = useCallback(() => {
remove();
handleFetch();
}, [url]);
useEffect(() => {
const onFocus = () => {
mutate();
};
window.addEventListener('focus', onFocus);
return () => {
window.removeEventListener('focus', onFocus);
};
}, []);
useEffect(() => {
let needSetData = false;
setLoading(true);
if (cacheMap.has(url)) {
setData(cacheMap.get(url));
setLoading(false);
} else {
if (!needSetData) {
handleFetch();
}
}
return () => {
needSetData = true;
setLoading(false);
};
}, [url]);
return { data, loading, error, mutate };
};
但随着之后的需求越来越庞大,内容越来越多,你发现自己写的useFetch缺少了很多内容,例如:
-
完善的重试机制:在数据加载出问题的时候,要进行有条件的重试(如仅 5xx 时重试,403、404 时放弃重试)
-
完善的错误日志机制:包括日志上报,报警等
-
完善的预加载技术
-
完善的 SSR、SSG 支持能力
-
完善的分页请求缓存能力
-
等等......
想到这些内容你就头痛,你心想这些东西我一个初级前端工程师怎么可能完成呢!!!带着怨气,你想从百度看看有没有参考的方向,应差阳错的打开了一个叫做SWR的网站,然后你发现你进入了新世界的大门(嘻嘻嘻)
正文
正如官网所说的,SWR是一个用于数据请求的 React Hooks 库,但不要只把它当作一个useFetch这么简单!!!
如果需要在自己的业务代码中用,官方推荐的写法是这样
function useUser(id) {
const { data, error } = useSWR(`/api/user/${id}`, fetcher)
return {
user: data,
isLoading: !error && !data,
isError: error,
}
}
function Avatar({ id }) {
const { user, isLoading, isError } = useUser(id)
if (isLoading) return <Spinner />
if (isError) return <Error />
return <img src={user.avatar} />
}
并且值得注意的是,多个组件引用swr的数据,也会实时更新,这样业务便不再关心UI视图为什么多处展示不一致,类似于下方的Content和Avatar两个组件,当页面自动刷新或者手动触发更新时,视图会一起发生改变,再也不用自己手动维护state状态
function Content() {
const { user, isLoading } = useUser()
if (isLoading) return <Spinner />
return <h1>Welcome back, {user.name}</h1>
}
function Avatar() {
const { user, isLoading } = useUser()
if (isLoading) return <Spinner />
return <img src={user.avatar} alt={user.name} />
}
这里只是简单介绍下SWR的使用方法,更加详细的例如刷新、分页等可以去他的官方文档中查看(API 选项 – SWR)这里就不再赘述。顺便也就简单从源码中解析了解下SWR的工作流程及它核心的逻辑部分。
通过config(包含所有入参),如果存在key对应的cache,则获取到对应数据,否则去执行对应的useSWRHandler函数,其实SWR v1.3.0暴露出来了更多的内容(相比于之前版本更新size、getSize、isLoading)
const useSwr = (
getKey: SWRInfiniteKeyLoader,
fn: BareFetcher<Data> | null,
config: Omit<typeof SWRConfig.defaultValue, 'fetcher'>
& Omit<SWRInfiniteConfiguration<Data, Error>, 'fetcher'>
) => {
return {
size: resolvePageSize(),
setSize,
mutate,
get data() {
return swr.data
},
get error() {
return swr.error
},
get isValidating() {
return swr.isValidating
},
get isLoading() {
return swr.isLoading
}
}
}
getKey可以是字符串、任意数组或者 null,也可以是一个返回上述类型的函数。通过阅读源码可知,key参数会被serialize格式化处理,作为缓存的唯一依据,并且通过key作为唯一值调用swr的核心代码函数revalidate,在no-error并且visible online时候去获取数据。
function execute()
if (
!getCache().error &&
(refreshWhenHidden || getConfig().isVisible()) &&
(refreshWhenOffline || getConfig().isOnline())
) {
revalidate(WITH_DEDUPE).then(next)
} else {
// Schedule the next interval to check again.
next()
}
}
// revalidate核心逻辑
const revalidate = useCallback(() => {
try {
if (shouldStartNewRequest) {
setCache(initialState)
// If no cache is being rendered currently (it shows a blank page),
// we trigger the loading slow event.
if (config.loadingTimeout && isUndefined(getCache().data)) {
setTimeout(() => {
if (loading && callbackSafeguard()) {
getConfig().onLoadingSlow(key, config)
}
}, config.loadingTimeout)}
// Start the request and save the timestamp.
// Key must be truthy if entering here.
FETCH[key] = [currentFetcher(fnArg as DefinitelyTruthy<Key>),getTimestamp()]
}
// Wait until the ongoing request is done. Deduplication is also
// considered here.
;[newData, startAt] = FETCH[key]
newData = await newData
if (shouldStartNewRequest) {
// If the request isn't interrupted, clean it up after the
// deduplication interval.
setTimeout(cleanupState, config.dedupingInterval)
}
if (!FETCH[key] || FETCH[key][1] !== startAt) {
if (shouldStartNewRequest) {
if (callbackSafeguard()) {
getConfig().onDiscarded(key)
}
}
return false
}
// Clear error.
finalState.error = UNDEFINED
/**
case 1:
req------------------>res
mutate------>end
case 2:
req------------>res
mutate------>end
case 3:
req------------------>res
mutate-------...---------->
*/
const mutationInfo = MUTATION[key]
if (
!isUndefined(mutationInfo) &&
// case 1
(startAt <= mutationInfo[0] ||
// case 2
startAt <= mutationInfo[1] ||
// case 3
mutationInfo[1] === 0)
) {
finishRequestAndUpdateState()
if (shouldStartNewRequest) {
if (callbackSafeguard()) {
getConfig().onDiscarded(key)
}
}
return false
}
const cacheData = getCache().data
finalState.data = compare(cacheData, newData) ? cacheData : newData
// Trigger the successful callback if it's the original request.
if (shouldStartNewRequest) {
if (callbackSafeguard()) {
getConfig().onSuccess(newData, key, config)
}
}
return true
}
})
那对应的mutate主动刷新方法也就更好理解,在能够调用revalidate后,完成cache的刷新即可
// 暴露的mutate方法
// Default to true.
const shouldRevalidate = options.revalidate !== false
// It is possible that the key is still falsy.
if (!infiniteKey) return EMPTY_PROMISE
if (shouldRevalidate) {
if (!isUndefined(data)) {
// We only revalidate the pages that are changed
const originalData = dataRef.current
set({ _i: [false, originalData] })
} else {
// Calling `mutate()`, we revalidate all pages
set({ _i: [true] })
}
}
// 实际的mutateByKey方法
const [key] = serialize(_k)
if (!key) return
const [get, set] = createCacheHelper<Data, MutateState<Data>>(cache, key)
const [EVENT_REVALIDATORS, MUTATION, FETCH] = SWRGlobalState.get(cache) as GlobalState
const revalidators = EVENT_REVALIDATORS[key]
const startRevalidate = () => {
if (revalidate) {
// Invalidate the key by deleting the concurrent request markers so new
// requests will not be deduped.
delete FETCH[key]
if (revalidators && revalidators[0]) {
return revalidators[0](revalidateEvents.MUTATE_EVENT).then(
() => get().data
)}
}
return get().data
}