多人协作开发项目时最容易出现的问题如下
- 主页面和详情页非同一人所写、内外页面数据不同步。例如发现页视频评论数为28,进入详情页后发布评论、详情页内评论数为29。返回后主页面评论数依然为28。
- 同一个Query接口、不同主页次页所传参数数量不一致、导致返回数据不同。本身需求是应该取出完全相同的数据、所传参数数量应当相同。
- 重复编写 Query 查询接口代码、严重的情况是使用同一接口不同按钮因为样式不同都各自写了一遍Query代码。
- 例如粉丝、关注两个页面,同为列表,都有下拉刷新、上啦加载更多。仅仅查询的GQL不一样。两个页面都各自写一遍查询逻辑、刷新、加载逻辑。
- 还有更多场景这里不一一介绍
HOC (高阶组件)
本质: 接收一个组件、返回一个组件。其本身是一个function (函数)
可以返回class组件、也可以返回function组件、并无严格规定。
基本结构如下:
function withMovieList(WrapperedComponent){
return function MovieListComponent(props){
// ... logic code here
// 🅿️1️⃣
return <WrapperedComponent {...props}/>
}
}
基本用法如下:
const MovieListComponentView = (props) => {
//... own logic code here
return (
<YourUIComponents></YourUIComponents >
)
}
const MovieListComponent = withMovieList(MovieListComponentView);
const HomePage = (props) => {
return (
<View>
<MovieListComponent enableRefresh={true}/>
</View>
)
}
注意, 在 🅿️1️⃣ 处、就可以把公共逻辑写在那里、并将相应的回调、数据通过props传递给 WrapperedComponent
在 HomePage 传递给 MovieListComponent 的 enableRefresh 属性,会直接出现在
withMovieList返回的函数组件props中。
下面将通过一个中低等复杂度例子来说明高阶组件的实际用法,类似的会在新视频工厂App模板项目中大量出现。
一 . 粉丝、关注列表
粉丝列表Query GQL :UserFollowers
关注列表Query GQL :UserFollowings
接收参数: { user_id: number; count: number; page: number }
高阶组件业务说明、需要为其返回的组件提供用户列表数据data ,提供 refetch 和 fetchMore 方法,提供能够获取到当前查询的页码的方法或直接属性。
高阶组件代码如下:
interface UserRelationListComponentProps {
count?: number;
initialPage?: number;
user_id?: number;
}
interface UserRelationListWrapperedComponentProps {
loadRelationList:any;
relationListQueryPage:number;
updateRelationListQueryPage:(page:number) => void;
isRelationListQueryCalled: boolean;
isRelationListQueryLoading: boolean;
relationListQueryError: any;
relationListQueryData: any;
relationListQueryRefetch: () => void;
relationListQueryFetchMore: () => void;
}
const withUserRelationsList = (
WrapperedComponent,
GQLDocument: any,
options?: { fetchImmediately: boolean; }
) => {
const fetchImmediately = options.fetchImmediately ?? false;
return function RelationListComponent(props) {
const count = props.count ?? 15;
const page = useRef(props.initialPage ?? 1);
const user_id = props.user_id ?? -1;
const injectedProps = useMemo(() => {
let _propsWithoutHocProps = { ...(props) };
for (let del of ['count', 'initialPage', 'user_id']) {
delete _propsWithoutHocProps[del];
}
return _propsWithoutHocProps;
}, [props])
const [loadRelationList, { called, loading, error, data, refetch, fetchMore }] = useLazyQuery(
GQLDocument,
{ variables: { page, count, user_id }, fetchPolicy: 'network-only' }
);
const relationListQueryFetchMore = () => {
page.current += 1;
return fetchMore({ variables: { page: page.current } })
}
const relationListQueryRefetch = () => {
page.current = 1;
return refetch({ variables: page.current });
}
useEffect(() => {
if (fetchImmediately && user_id !== -1) loadRelationList();
}, [])
return <WrapperedComponent
{...injectedProps}
loadRelationList={loadRelationList}
relationListQueryPage={page.current}
updateRelationListQueryPage={(p: number) => { page.current = p; }}
isRelationListQueryCalled={called}
isRelationListQueryLoading={loading}
relationListQueryError={error}
relationListQueryData={data}
relationListQueryRefetch={relationListQueryRefetch}
relationListQueryFetchMore={relationListQueryFetchMore}
/>
}
}
从上面代码可以看出几乎和GQL相关的数据都通过props传给了高阶函数的第一个参数的这个组件。
下面是 关注用户列表 页面的相关代码
- 关注用户列表组件
下面UI部分组件并没有进一步进行解耦、实际上应该将FlatList 和 renderItem 部分再抽象出来,并编写相关数据转换代码,将这里拿到的数据转换成UI组件可展示的数据相关属性。
//此为UI部分组件
const FollowListComponent = (props:UserRelationListWrapperedComponentProps) => {
const { relationListQueryData,relationListQueryFetchMore } = props;
const onEndReached = () => {
relationListQueryFetchMore();
}
return (
<FlatList
data={relationListQueryData}
renderItem={({ item, index }) => <View />}
onEndReached={onEndReached}
/>
)
}
var UserFollowers: any; //这里替换为引入的 GQL
interface FollowersListProps extends UserRelationListComponentProps {}
//该FollowersList是包含了UI、Data的关注列表组件
const FollowersList: (props:FollowersListProps) => any = withUserRelationsList(
FollowersListComponent,
UserFollowers,
{ fetchImmediately: true }
);
下面是关注列表页面
const FollowersPage = (props) => {
const user_id = props.router.params.user_id;
return (
<View style={{ flex: 1 }}>
<FollowersList user_id={user_id}/>
</View>
)
}
- 粉丝列表组件
//此为UI部分组件
var UserFollowings: any; //这里替换为引入的 GQL
interface FollowingsListProps extends UserRelationListComponentProps {}
//该FollowersList是包含了UI、Data的关注列表组件
const FollowingsList: (props:FollowingsListProps) => any = withUserRelationsList(
FollowingsListComponent,
UserFollowings,
{ fetchImmediately: false }
);
这里注意到, FollowListComponent 为 粉丝和关注列表公用组件、因为两个列表在实际业务中几乎仅 renderItem 渲染的组件有细微区别、因此这里可以完全公用,实际项目中将renderitem 渲染的 item 通过 props传给FollowListComponent即可。
粉丝页面
const FollowingsPage = (props) => {
const user_id = props.router.params.user_id;
return (
<View style={{ flex: 1 }}>
<FollowingsList user_id={user_id}/>
</View>
)
}
从上面HOC的用法可以看出、项目中通过将GQL 和 HOC 层紧密结合、UI组件就可以完全解放,无需关心后端的GQL结构是什么。在使用HOC包裹的组件时加一层 转换 函数即可,如下
// GQL返回结构
{
data: {
id: 1,
username: 'lily',
age: 12
}
}
纯UI组件如下
const PlainListItemOne = (props:{
title?:string;
subtitle?:string;
}) => {
const title = props.title ?? '';
const subtitle = props.subtitle ?? '';
return (
<View>
<Text>{title}<Text/>
<Text>{subtitle}</Text>
</View>
)
}
在编写使用HOC包裹的组件时用到该 PlainListItemOne 时需要的转换函数
function convert(value){
return ({
title: value.data.username,
subtitle: value.data.age
})
}