前言
在对新项目进行技术选型的时候,针对数据流做了一些思考记录。
大量的文章分析了redux、mobx等数据流方案的优缺点,在我看来,方案的选择离不开业务。
而在react hooks 之后,又涌现了很多优秀的解决方案如:SWR等。
作为一个开发者而言,始终面临着一个问题,究竟当前数据是否应该放入状态管理库或者仅仅只在组件中使用? 而我们始终秉承着思想:
- 当数据不需要共享,那它就应该只属于某个组件,保持它的独立性,不需要使用状态去管理它,用完抛弃了就好。
- 拒绝样板代码,太多的冗余代码增加了应用的复杂度
在全面拥抱react hooks之后,redux和上面的思想相悖。我们需要一个更简单、精简与视图更加紧密的请求器(本文默认大家已经对SWR有一定的了解)
首先,我们来看下在react function component
中,如何去获取数据:
function Posts() {
const [posts, setPosts] = useState();
const [params, setParams] = useState({id: 1});
useEffect(() => {
fetchPosts(params).then(data => setPosts(data));
}, [params]);
if (!posts) { return <div>Loading posts...</div>; }
return (
<ul>{posts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul>
);
}
可以看到,在hooks的写法下,请求数据需要额外的逻辑来保存数据流的状态,这种方式又带来了更多的冗余,缺少了部分逻辑扩展。
鉴于此我们可以想到,那可以把上面请求逻辑封装成自定义hook简化逻辑(伪代码):
const useRequest = (api, params) => {
const [posts, setPosts] = React.useState()
const [loading, setLoading] = React.useState(false)
const fetcher = (params) => {
setLoading(true)
const realParmas = JSON.parse(params)
api(realParmas).then(data => {
setPosts(data)
setLoading(false)
})
}
React.useEffect(() => {
fetcher(params)
}, [params])
return {
data: posts,
loading
}
}
function Posts() {
const { data, loading } = useRequest('/user', JSON.stringify({params: 123}))
return (
<ul> {data.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul>
);
}
通过封装,我们把业务简化,减少了冗余代码,逻辑更清晰。
上面的封装带来了几个特性, - 依赖请求 。useRequest的params更新时,useRequest会重新请求。 - 请求状态管理。loading 状态
但是一个商业化的产品,需要的特性不仅仅如此,我们可能需要对数据进行缓存、对api进行统一管理、错误重试。面对typescript项目还需要支持对api参数的推导,带来更友好的代码提示。
而SWR覆盖了大部分我们所需要的特性,下面我们基于SWR进行封装,保留SWR的特性下,增加SWR所缺少的: - api进行统一管理 - typescript类型推导 - 支持动态依赖请求
Api进行统一管理
我们在管理api的时候,需要写很多的样板代码来定义api,所以需要一个生成函数来简化它。
import { AxiosRequestConfig } from 'axios'
export type ApiConfig<Params = any, Data = any> = AxiosRequestConfig & {
url: string
method?: Method
params?: Params
data?: Params
_response?: Data
[x: string]: any
}
export type Service<Params = any, Data = any> = (_params?: any) => ApiConfig<Params, Data>
export const createGetApi = <Params = any, Data = any>(apiConfig: ApiConfig) => {
return (params: Params): Service<Params, Data> => (_params = {}) => {
const nextParams = { ...params, ..._params }
return {
...apiConfig,
params: nextParams,
method: 'get'
}
}
}
export const createPostApi = <Params = any, Data = any>(apiConfig: ApiConfig) => {
return (data: Params): Service<Params, Data> => {
return () => ({
...apiConfig,
data,
method: 'post'
})
}
}
我们基于axios
定义了两个Api
生成器。生成器所做的工作就是定义api的基本配置和参数及返回值的类型定义,接下来我们来使用它:
interface GetUserParmas {
id: number
}
interface GetUserResponse {
id: number
userName: string
}
const userModule = {
getUser: createGetApi<GetUserParmas, GetUserResponse>({url: ‘/user’})
}
可以看到,这样就可以很方便的定义api,我们在用个custom hook来统一管理api
const useApiSelector = () => {
return {
userModule
}
}
好了我们完成了第一步统一管理Api,接下来我们要对swr进行封装,让它支持上诉所需要的特性:
基于SWR封装新特性
function useAsync<Params = any, Data = any>(
# api生成器生成的api
service: ServiceCombin<Params, Data>,
# swr的配置
config: ConfigInterface<Data>
): BaseResult<Params, Data>
function useAsync<Params = any, Data = any>(service: ServiceCombin<Params, Data>, config) {
// swr全局配置
const globalConfig = React.useContext(Provider)
const requestConfig = Object.assign({}, DEFAULT_CONFIG, globalConfig as any, config || {})
//传入参数fetcher 保证与swr 一致
if (requestConfig.fetcher) {
requestConfig['fetcher'] = fetcherWarpper(requestConfig.fetcher)
}
const configRef = React.useRef(requestConfig)
configRef.current = requestConfig
// 取出api的配置
const serviceConfig = genarateServiceConfig(service)
const serviceRef = React.useRef(serviceConfig)
serviceRef.current = serviceConfig
// 将api的参数转化成swr的 key
const serializeKey = React.useMemo(() => {
if (!serviceRef.current) return null
try {
return JSON.stringify(serviceRef.current)
} catch (error) {
return new Error(error)
}
}, [serviceRef.current])
if (serializeKey instanceof Error) {
invariant(false, '[serializeKey] service must be object')
}
const response = useSWR<Response<Data>>(serializeKey, configRef.current)
const getParams = React.useMemo(() => {
if (!serviceRef.current) return undefined
return (serviceRef.current?.params || serviceRef.current?.data || {}) as Params
}, [serviceRef.current])
// 对返回值进行验证,可以忽略
const disasterRecoveryData = React.useMemo(() => {
return getDisasterRecoveryData<Data>(response?.data)
}, [response.data])
return {
...response,
...disasterRecoveryData,
params: getParams
}
}
好了,我们完成对SWR的封装,根据传入的api定义类型推导出response。来和之前对比一下目前方案:
const App = () => {
const apis = useApiSelector()
// 在typescirpt 应用中,就可以自动推导出getUser需要的参数类型
const { data, isValidating } = useRequest(api.getUser({id: 1}))
return (
<div>{data}</div>
)
}
总结:
以上,主要是把自己对SWR与业务结合的一些思考阐述出来。选择一个数据流管理方案的时候,并不总是选择一个大家都在用,而且需要一个更贴切自身业务的。
上方的代码只是作为思路的引导,完整的解决方案已经上传到github,大家可以到仓库查看完整的代码,完整的代码包含了更多的特性,包括支持antd的分页等特性。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!