Hooks + TS 搭建一个任务管理系统(二)-- 项目列表展示

x33g5p2x  于9个月前 转载在 其他  
字(7.8k)|赞(0)|评价(0)|浏览(78)

📢 大家好,我是小丞同学,一名大二的前端爱好者

📢 这个系列文章是实战 jira 任务管理系统的一个学习总结

📢 非常感谢你的阅读,不对的地方欢迎指正 🙏

📢 愿你忠于自己,热爱生活

在我们写好登录注册界面后,我们需要开始解决登录后的项目列表展示页,这也是我们在自动登录后显示的页面

💡 知识点抢先看

这篇文章将讲到以下几个知识点

  • antd 组件库渲染项目列表
  • ...更多按钮的实现
  • 通过 URL 进行状态管理
  • 封装项目列表中的 url 操作

一、antd 组件库渲染项目列表

首先我们先来讲讲页面中最重要的列表,这里采用的是 Antd 组件库中的 Table 组件为基础架构,我们在它的基础上重新创建了一个 List 组件表示我们的项目列表

大概的结构如下

export const List = ({ users, ...props }: ListProps) => {
    return <Table rowKey={"id"} pagination={false} columns={[
        // 此处省略6个 {} 结构 
    ]} {...props} />
}

我们需要向 columns 中注入数据,在这里我们的 List 组件接收了需要使用的数据,用户数据以及相关配置项

这里利用的是一个类型的继承

interface ListProps extends TableProps<Project> {
    users: User[];
    refresh?: () => void;
}

我们通过这个接口继承了 Table 组件原先的所有 props 参数的类型的基础上,又添加了几个类型,这样我们的数据既能符合需求,也能顺利的穿透Table 组件中。同时我们需要给 Table 组件指定数据源 dataSource ,在这样处理后,我们直接可以使用 {...props} 即可

在这里我们使用的 Project 泛型,其实也指定了 dataSource 的类型,也是 columns 中的获取数据类型

根据我们 UI 图,这里一共需要有6个数据:收藏情况、名称、部门、负责人、创建时间、更多按钮

这里将从三个问题来讲解如何渲染数据

  1. 如何分列渲染数据?

我们通过 Table 组件的 columns 属性添加对象的方式来实现 List 中的每一列,简单的说就是组件自带的属性,直接配置就好,这里的 title 也就是用来设置列头的标题

{
    title: '名称',
    //其他配置
},
// 其他5列

不用标题的话可以不设置 title 属性

  1. 如何显示数据呢?

我们可以使用 dataIndex 以及 render 来实现

首先dataIndex 这个是 columns 中的一个 API ,我们可以通过它来指定列数据的来源
dataIndex : 列数据在数据项中对应的路径,支持通过数组查询嵌套路径

对于部门的数据展示

{
    title: '部门',
    dataIndex: 'organization',
    sorter: (a, b) => a.name.localeCompare(b.name)
}

它可以指定从哪里来获取这些数据,这里就是指定从 project 内直接获取数据

我们这里采用的就是这种方法,这样就能直接的对数据进行列渲染

同时我们还可以采用 render 方法
生成复杂数据的渲染函数,参数分别为当前行的值,当前行数据,行索引

一般用来处理一些比较难的逻辑,比如 名称

我们采用的就是 render 来渲染

{
    title: '名称',
    sorter: (a, b) => a.name.localeCompare(b.name),
    render(value, project) {
        return <Link to={String(project.id)}>{project.name}</Link>
    }
}

首先值得注意的是,这里的 render 和其他的 render 不同,这里的 render 更像是一个函数,我们通过传递参数,然后返回结构,就能渲染在页面上

function(text, record, index) {}

它接收三个参数,都是可选的,分别是当前行的值,当前行数据,行索引

这里特别注意的是当前行数据,我们可以直接使用 props 中的数据,这里我们传入的是 project ,最后返回一个 Link 元素,这样渲染到页面上的就是一个 Link 标签

  1. 如何实现列排序呢?

columns 中有一个 sorter API,我们可以通过它来实现排序

sorter: (a, b) => a.name.localeCompare(b.name)

通过名字大小写来排序

其实这里讲的都是 Table 组件的用法而已,查看文档也能实现

在这里有一些列中渲染的是一个组件,在后面会讲到

二、更多按钮的实现

Table 列表的 columns 属性中我们的最后一列(更多),采用的是一个封装的组件,这样可以减少我们 Table 组件的代码,同时实现组件复用(这次没有用到)

更多按钮的实现也是利用了一个 Antd 库中的 DropdownMenu 组件,实现一个下拉框的效果

<Dropdown overlay={<Menu>
    <Menu.Item onClick={editProject(project.id)} key={'edit'}>
        <ButtonNoPadding type={'link'}>编辑</ButtonNoPadding>
    </Menu.Item>
    <Menu.Item onClick={() => confirmDeleteProject(project.id)} key={'delete'}>
        <ButtonNoPadding type={'link'}>删除</ButtonNoPadding>
    </Menu.Item>
</Menu>}>
    <ButtonNoPadding type={"link"}>
        ...
    </ButtonNoPadding>
</Dropdown>

利用 overlay 配置一个 Menu 组件,在 Menu 中配置下拉显示的内容 ,Dropdown 中直接配置 当前显示的内容

这个就是实现的效果,这里封装了一个 ButtonNoPadding 组件,是一个 Antd 中去除 paddingButton 组件
关于删改的实现后面会讲解

关于布局就涉及这么多,接下来才是重头戏

三、通过 URL 进行状态管理

这里有很多的问题!!!

在这里我们就讲几个 custom hook

  • useUrlQueryParam
  • useSetUrlSearchParam

这两个 hook 分别是用与返回页面 url 中的 query 和设置当前的 URL 地址的

知道了它们的作用,我们来一步步实现它

首先在这里有人可能会有疑惑我们为什么不将这两个 hook 写成一个呢?
这里一开始实现的时候是写的一个 hook ,但是到后面逻辑复杂了之后,就会出现无限循环的情况,同时造成 url 的重复跳转,难以实现我们的逻辑,因此我们将两个逻辑分离开来,让它的功能更加具体化

这里我们先来写 useSetUrlSearchParam ,因为在我们的查看逻辑中使用了这部分的代码

1. useSetUrlSearchParam

首先我们使用 react-router-dom 中的 useSearchParams 这个 hook ,它返回一个 searchParamssetSearchParams,从用法上来看有点像 useState ,通过这个 hook 可以来处理我们的查询字符串

在这里我们接收一个参数 params ,也就是查询字符串,用来设置我们的 url,例如我们的编辑页面的 url

是通过拼接了一个 editingProjectId=id 实现的,转化成代码的话就是我们这里的 params ,在传递的时候是以对象键值对的方式来传递的,因此在这里我们对 params 的类型的定义应该符合这个规则

params: { [key in string]: unknown }

对于初学 TS 的来说,如何理解这样的类型定义呢?

我们指定 params 的类型是一个对象 {} ,它的 : 左侧也就是 key 被指定为 string ,右侧 unknown 指定 value 的类型

在我们成功接收到这个 params 时,我们将这个数据解构出来,与原先 url 中存在的 query 一同经过清理之后,将得到的对象传递给 setSearchParams 来设置当前的 url

// 通过这个单独得 hook 来 set search param
// 把输入框的内容映射到url地址上
export const useSetUrlSearchParam = () => {
    const [searchParams, setSearchParams] = useSearchParams()
    return (params: { [key in string]: unknown }) => {
        const o = cleanObject({
            ...Object.fromEntries(searchParams),
            ...params
        }) as URLSearchParamsInit
        return setSearchParams(o)
    }
}

讲讲我自己对这里的理解吧

由于我们这部分采用的是 SPA

一方面我们需要实现打开网址时,显示对应的页面,另一方面我们需要实现我们的跳转

我们在这里采用的这样的方式:在我们点击创建或者编辑时,我们将当前的项目列表组件切换成编辑组件,同时我们通过我们封装的 custom hook手动的更改当前的 url,从而实现了 url与数据与页面相匹配

2. useUrlQueryParam

首先再次明确我们这个 hook 的功能:返回页面 url 中的 query ,同时利用 useSetUrlSearchParam 返回的方法来设置 url

我们先来明确以下这个 hook接收的参数和返回的值
接收一个 keys 的数组,也就是 query 中的键名的数组,返回一个数组,第一个元素是一个对象保存着 key-value ,第二个元素是一个方法,也就是修改 url 的方法

接下来我们再来确定以下接收参数的类型

这里我们接收一个泛型 K 的数组,同时由于这是 key ,这个 K 应当继承 string

<K extends string>(keys: K[])

接下来我们来引入一些我们需要用到的方法,查询和设置

// 定义了一些实用的方法来处理 URL 的查询字符串
const [searchParams] = useSearchParams()
const setSearchParams = useSetUrlSearchParam() // 引入这个自定义的方法,不使用原生自带的

我们再来研究以下如何返回当前 urlquery 对象

useMemo(
    () => keys.reduce((prev, key) => {
        // 解决当get 的值是null 时的默认值
        return { ...prev, [key]: searchParams.get(key) || '' }
        // 传入的是一个 key 类型在 K 中值为 string 的对象
    }, {} as { [key in K]: string }),
    [keys, searchParams]
)

首先我们通过 reduce遍历传入的 keys 数组,每一次遍历都将使用 searchParams 方法去查找对应的value 值,遍历完成后会返回整个对象,利用 reduce 将每次的 key-value 添加到 {} 中,最后全部返回

这里我们给 reduce 传入了第二个参数,指定了我们传入的函数的初始值

同时在这里我们采用了 useMemo 这个 hook 来优化我们的代码,只有在依赖项改变的时候才会重新计算,这样可以解决无限循环的问题(todo: 关于无限循环的问题之后出一篇文)

接下来我们来研究返回数组的第二个值

// 键值限定在我们设置的范围之内
(params: Partial<{ [key in K]: unknown }>) => {
    // 把 fromEntries 转化为一个对象
    return setSearchParams(params)
}

这个很简单,直接将传入的 params 传递给 setSearchParams 中添加就可以了~

在这里我们采用了一个 Partial 方法,它是 TS 联合类型中的一个点,它可以把指定的泛型中的类型都变成可选

底层实现

type Partial<T> = {
    [P in keyof T]?: T[P];
};

最后一个非常重要的点是 as const ,这个也是 TS 中比较高级的用法,也叫做 const 断言,否则会错乱

关于 const 断言,做个简单的解释,如果没有使用 as const 的话,会默认的进行类型推断,return 返回的是一个函数类型的数组,但是它完全忘记了有两个元素,因此会丢失返回数组中元素的类型,采用 const 断言,就能指示使表达式的字面类型不被扩展

未采用 const 断言

采用 const 断言

能明显感受出来它们的不同

以下是 return 的完整代码

return [
    useMemo(
        () => keys.reduce((prev, key) => {
            return { ...prev, [key]: searchParams.get(key) || '' }
        }, {} as { [key in K]: string }),
        [keys, searchParams]
    ),
    (params: Partial<{ [key in K]: unknown }>) => {
        return setSearchParams(params)
    }
] as const

为了给下一篇文章搭建好梯子,接下来我们写一下我们这两个 custom hookproject 列表中的应用

四、封装项目列表中的 url 操作

由于我们在 project 列表中会大量使用到 url 操作,为了能将我们的代码更加简洁,我们利用 useUrlQueryParam 这个轮子来造车,在这个基础上将 project 的特定 keys 传入即可,这样我们在 project 中使用时,就可以直接调用对应的 searchParams 方法

这里我们讲 3 个 custom hook

  • useProjectsSearchParams
  • useProjectsQueryKey
  • useProjectModel

1. useProjectsSearchParams

这一个 hook 就是 useUrlQueryParam 的作用,只是将它具体到了 project 中使用

返回的是一个数组,第一个元素是查找的数据,第二个是修改的方法

export const useProjectsSearchParams = () => {
    // 要搜索的数据
    // 返回的是一个新的对象,造成地址不断改变,不断的渲染
    // 用这个方法来设置路由地址跟随输入框变化
    // 服务器返回的都是 string 类型
    const [param, setParam] = useUrlQueryParam(['name', 'personId'])
    return [
        // 采用 useMemo 解决 重复调用的问题
        useMemo(() => ({ ...param, personId: Number(param.personId) || undefined }), [param]),
        setParam
    ] as const
}

我们在使用这个 hook 的时候,直接调用即可,因为我们已经指定了它的 keys 数组为 ['name', 'personId'],这个是在 搜索模块 中使用的 hook

2. useProjectsQueryKey

这个 hook 用来返回 query 的键值对,返回的是 {name: '', personId: undefined} 样式

export const useProjectsQueryKey = () => {
    const [params] = useProjectsSearchParams()
    // {name: '', personId: undefined}
    return ['projects', params]
}

我们在使用的时候也是直接调用即可返回数据

3. useProjectModel

我们通过这个 hook 来判断当前的状态是不是在创建、编辑,如果是的话我们就显示出我们对应的页面

首先我们先从利用 useUrlQueryParams 来获取到页面的 query 对象

通过对象解构的方式,解构出对应的数据,例如这里我们解构出 query 中的 projectCreate 字段

那第一个来说就是利用 useUrlQueryParam 传入 projectCreate 来在 url 中查找有没有这个字段,返回查找的结果,同时返回一个可以修改它的函数 setProjectCreate ,这就是我们的 url custom hook 发挥的作用了

const [{ projectCreate }, setProjectCreate] = useUrlQueryParam([
    'projectCreate'
])
// 判断当前是不是在编辑,解构出当前编辑项目的 id
const [{ editingProjectId }, setEditingProjectId] = useUrlQueryParam([
    'editingProjectId'
])

在接下来的代码中就是封装一些更改它们的方法,暴露出去给外部直接调用,例如控制 modal 页面的开关openclose 方法,控制编辑页面开启的 startEdit 方法

代码逻辑非常简单,我们只需要调用对应的 set... 方法来改变 url 中的对应键值对的值就可以了

const open = () => setProjectCreate({ projectCreate: true })
const startEdit = (id: number) => setEditingProjectId({ editingProjectId: id })
const close = () => setUrlParams({
    editingProjectId: undefined, projectCreate: undefined
})

例如 open 我们通过 setProjectCreate({ projectCreate: true })projectCreate 改成 true 表示当前正在创建的页面

关于这个 editingProjectId 我们可以通过 useProject 这个 custom hook 来获取(或许在下一篇会讲到,这里不展开),采用的是 react-query , 它返回的是一个 data 数 据

最后我们暴露这些方法

return {
    // 采用 id才是最佳选择,这样不用等待数据返回就能打开编辑框
    projectModelOpen: projectCreate === 'true' || Boolean(editingProjectId),
    open,
    close,
    startEdit,
    editingProject,
    isLoading
}

这样我们的 project 列表下的 url 控制操作 hook 就全部完成了

那么这篇文章就到这里结束了,在接下来的文章中,会利用这些封装好的 hook去实现项目列表的增删改查以及乐观更新等功能

📌 总结

  1. 在这篇文章中我们写了大量的 custom hook ,也更加的熟练了它的写法和好处
  2. const 断言有了一定的了解
  3. 学会了如何使用 TableDropdown 等组件
  4. 大致的认识了 useMemo 的用法
  5. useSearchParams 有了一定的了解
  6. TS 中的联合类型有了更深的理解
    最后,可能在很多地方讲诉的不够清晰,请见谅

💌 如果文章有什么错误的地方,或者有什么疑问,欢迎留言,也欢迎私信交流

相关文章