舍弃redux,基于swr的通用数据流方案

3,884 阅读4分钟

前言

在对新项目进行技术选型的时候,针对数据流做了一些思考记录。

大量的文章分析了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的分页等特性。