【实战】十一、看板页面及任务组页面开发(四) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(二十六)

文章目录


学习内容来源:React + React Hook + TS 最佳实践-慕课网


相对原教程,我在学习开始时(2023.03)采用的是当前最新版本:

版本
react & react-dom ^18.2.0
react-router & react-router-dom ^6.11.2
antd ^4.24.8
@commitlint/cli & @commitlint/config-conventional ^17.4.4
eslint-config-prettier ^8.6.0
husky ^8.0.3
lint-staged ^13.1.2
prettier 2.8.4
json-server 0.17.2
craco-less ^2.0.0
@craco/craco ^7.1.0
qs ^6.11.0
dayjs ^1.11.7
react-helmet ^6.1.0
@types/react-helmet ^6.1.6
react-query ^6.1.0
@welldone-software/why-did-you-render ^7.0.1
@emotion/react & @emotion/styled ^11.10.6

具体配置、操作和内容会有差异,"坑"也会有所不同。。。


一、项目起航:项目初始化与配置

二、React 与 Hook 应用:实现项目列表

三、TS 应用:JS神助攻 - 强类型

四、JWT、用户认证与异步请求


五、CSS 其实很简单 - 用 CSS-in-JS 添加样式


六、用户体验优化 - 加载中和错误状态处理



七、Hook,路由,与 URL 状态管理



八、用户选择器与项目编辑功能


九、深入React 状态管理与Redux机制





十、用 react-query 获取数据,管理缓存


十一、看板页面及任务组页面开发

1~3

4~6

7&8

9.拖拽实现

接下来的内容将会是整个课程最难的部分,相关知识也不是很常用

  • 安装拖拽功能库 react-beautiful-dnd 及相关类型文件库
bash 复制代码
npm i react-beautiful-dnd ## --force
npm i @types/react-beautiful-dnd -D ## --force 

接下来在原功能库的基础上自行二次封装

新建 src\components\grag-and-drop.tsx

js 复制代码
import React, { ReactNode } from "react"
import { Draggable, DraggableProps, Droppable, DroppableProps, DroppableProvided, DroppableProvidedProps } from "react-beautiful-dnd"

// 删除原有 "函数类型" children,使用 ReactNode 类型的 children
type DropProps = Omit<DroppableProps, 'children'> & { children: ReactNode }

export const Drop = ({ children, ...props }: DropProps) => {
  return (
    <Droppable {...props}>
      {(provided) => {
        if (React.isValidElement(children)) {
          return React.cloneElement(children, {
            ...provided.droppableProps,
            ref: provided.innerRef,
            provided,
          });
        }
        return <div />;
      }}
    </Droppable>
  );
};

type DropChildProps =
  Partial<{ provided: DroppableProvided } & DroppableProvidedProps> &
    React.HTMLAttributes<HTMLDivElement>

export const DropChild = React.forwardRef<HTMLDivElement, DropChildProps>(
  ({children, ...props}, ref) => <div ref={ref} {...props} >
    {children}
    {props.provided?.placeholder}
  </div>
);

type DragProps = Omit<DraggableProps, 'children'> & { children: ReactNode }
export const Drag = ({children, ...props}: DragProps) => {
  return <Draggable {...props}>
    {
      (provided => {
        if(React.isValidElement(children)) {
          return React.cloneElement(children, {
            ...provided.draggableProps,
            ...provided.dragHandleProps,
            ref: provided.innerRef
          })
        }
        return <div/>
      })
    }
  </Draggable>
}

forwardRef 是用作转发的,经过包裹后的组件可以传入 ref 属性

这步报错:ref: provided.innerRef

bash 复制代码
Argument of type '{ ref: (element: HTMLElement | null) => void; 'data-rbd-drag-handle-draggable-id'?: string | undefined; 'data-rbd-drag-handle-context-id'?: string | undefined; 'aria-describedby'?: string | undefined; ... 7 more ...; onTransitionEnd?: React.TransitionEventHandler<...> | undefined; }' is not assignable to parameter of type 'Partial<unknown> & Attributes'.
      Object literal may only specify known properties, and 'ref' does not exist in type 'Partial<unknown> & Attributes'.

接下来使用这个组件

编辑 src\screens\ViewBoard\index.tsx

js 复制代码
...
import { DragDropContext } from "react-beautiful-dnd";
import { Drag, Drop, DropChild } from "components/grag-and-drop";

export const ViewBoard = () => {
  useDocumentTitle("看板列表");

  const { data: currentProject } = useProjectInUrl();
  const { data: viewboards, isLoading: viewBoardIsLoading } = useViewboards(
    useViewBoardSearchParams()
  );
  const { isLoading: taskIsLoading } = useTasks(useTasksSearchParams());
  const isLoading = taskIsLoading || viewBoardIsLoading;

  return (
    <DragDropContext onDragEnd={() => {}}>
      <ViewContainer>
        <h1>{currentProject?.name}看板</h1>
        <SearchPanel />
        {isLoading ? (
          <Spin />
        ) : (
          <Drop type='COLUMN' direction='horizontal' droppableId="viewboard">
            <ColumnsContainer>
              {viewboards?.map((vbd, index) => (
                <Drag key={vbd.id} draggableId={'viewboard' + vbd.id} index={index}>
                  <ViewboardColumn viewboard={vbd} key={vbd.id} />
                </Drag>
              ))}
              <CreateViewBoard />
            </ColumnsContainer>
          </Drop>
        )}
        <TaskModal />
      </ViewContainer>
    </DragDropContext>
  );
};

export const ColumnsContainer = styled(DropChild)`
  display: flex;
  overflow-x: scroll;
  flex: 1;
`;

编辑 src\screens\ViewBoard\components\ViewboardCloumn.tsx 使组件可以透传 props 以及通过 forwardRef 转发 传入 ref:

js 复制代码
...
export const ViewboardColumn = React.forwardRef<HTMLDivElement, { viewboard: Viewboard }>(({ viewboard, ...props }, ref) => {
  const { data: allTasks } = useTasks(useTasksSearchParams());
  const tasks = allTasks?.filter((task) => task.kanbanId === viewboard.id);
  return (
    <Container {...props} ref={ref}>
      <Row>
        <h3>{viewboard.name}</h3>
        <More viewboard={viewboard} key={viewboard.id}/>
      </Row>
      <TasksContainer>
        {tasks?.map((task) => (
          <TaskCard key={task.id} task={task} />
        ))}
        <CreateTask kanbanId={viewboard.id} />
      </TasksContainer>
    </Container>
  );
});

10.拖拽持久化

拖拽的时候 看板之间的间隔应该是不变的

编辑 src\screens\ViewBoard\index.tsx(调整组件层级并显式使用 DropChild):

js 复制代码
...
export const ViewBoard = () => {
  ...
  return (
    <DragDropContext onDragEnd={() => {}}>
      <ViewContainer>
        <h1>{currentProject?.name}看板</h1>
        <SearchPanel />
        {isLoading ? (
          <Spin />
        ) : (
          <ColumnsContainer>
            <Drop type="COLUMN" direction="horizontal" droppableId="viewboard">
              <DropChild style={{display: 'flex'}}>
                {viewboards?.map((vbd, index) => (
                  <Drag
                    key={vbd.id}
                    draggableId={"viewboard" + vbd.id}
                    index={index}
                  >
                    <ViewboardColumn viewboard={vbd} key={vbd.id} />
                  </Drag>
                ))}
              </DropChild>
            </Drop>
            <CreateViewBoard />
          </ColumnsContainer>
        )}
        <TaskModal />
      </ViewContainer>
    </DragDropContext>
  );
};

export const ColumnsContainer = styled.div`
  display: flex;
  overflow-x: scroll;
  flex: 1;
`;

接下来做 任务拖拽排序

编辑 src\screens\ViewBoard\components\ViewboardCloumn.tsx:

js 复制代码
...
import { Drag, Drop, DropChild } from "components/grag-and-drop";
...
export const ViewboardColumn = React.forwardRef<
  HTMLDivElement,
  { viewboard: Viewboard }
>(({ viewboard, ...props }, ref) => {
  const { data: allTasks } = useTasks(useTasksSearchParams());
  const tasks = allTasks?.filter((task) => task.kanbanId === viewboard.id);
  return (
    <Container {...props} ref={ref}>
      <Row>
        <h3>{viewboard.name}</h3>
        <More viewboard={viewboard} key={viewboard.id} />
      </Row>
      <TasksContainer>
        <Drop type="Row" direction="vertical" droppableId={'task' + viewboard.id}>
          <DropChild>
            {tasks?.map((task, taskIndex) => (
              <Drag
                key={task.id}
                draggableId={"task" + task.id}
                index={taskIndex}>
                <TaskCard key={task.id} task={task} />
              </Drag>
            ))}
          </DropChild>
        </Drop>
        <CreateTask kanbanId={viewboard.id} />
      </TasksContainer>
    </Container>
  );
});

拖拽功能好了,接下来将拖拽结果持久化到数据库中

编辑 src\utils\use-optimistic-options.ts(看板和任务排序 获取URL参数,为后续乐观更新做准备):

js 复制代码
...
export const useReorderViewboardConfig = (queryKey: QueryKey) =>
  useConfig(queryKey, (target, old) => old ? [old, ...target] : []);

export const useReorderTaskConfig = (queryKey: QueryKey) =>
  useConfig(queryKey, (target, old) => old || []);

编辑 src\utils\viewboard.ts(新增看板排序接口的 Custom Hook, SortPropsuseReorderTask 共用):

js 复制代码
...
export interface SortProps {
  // 要重新排序的 item
  fromId: number;
  // 目标 item
  referenceId: number;
  // 放在目标 Item 的前还是后
  type: 'before' | 'after';
  fromKanbanId?: number;
  toKanbanId?: number;
}

export const useReorderViewboard = () => {
  const client = useHttp();
  return useMutation((params: SortProps) => {
    return client("kanbans/reorder", {
      data: params,
      method: "POST",
    });
  }, useReorderViewboardConfig(queryKey));
};

编辑 src\utils\task.ts(新增看板排序接口的 Custom Hook):

js 复制代码
...
export const useReorderTask = (queryKey: QueryKey) => {
  const client = useHttp();
  return useMutation((params: SortProps) => {
    return client("tasks/reorder", {
      data: params,
      method: "POST",
    });
  }, useReorderTaskConfig(queryKey));
};

编辑 src\screens\ViewBoard\index.tsx(完善之前预留的 onDragEnd):

js 复制代码
...
import { useReorderViewboard, useViewboards } from "utils/viewboard";
import {
  useProjectInUrl,
  useTasksQueryKey,
  useTasksSearchParams,
  useViewBoardQueryKey,
  useViewBoardSearchParams,
} from "./utils";
...
import { useReorderTask, useTasks } from "utils/task";
...
import { useCallback } from "react";

export const ViewBoard = () => {
  ...

  const onDragEnd = useDragEnd();
  return (
    <DragDropContext onDragEnd={onDragEnd}>
      ...
    </DragDropContext>
  );
};


export const useDragEnd = () => {
  const { data: viewboards } = useViewboards(useViewBoardSearchParams());
  const { mutate: reorderViewBoard } = useReorderViewboard(useViewBoardQueryKey());
  const { mutate: reorderTask } = useReorderTask(useTasksQueryKey());
  const { data: allTasks = [] } = useTasks(useTasksSearchParams());
  return useCallback(
    ({ source, destination, type }: DropResult) => {
      if (!destination) {
        return;
      }
      // 看板排序
      if (type === "COLUMN") {
        const fromId = viewboards?.[source.index].id;
        const toId = viewboards?.[destination.index].id;
        if (!fromId || !toId || fromId === toId) {
          return;
        }
        const type = destination.index > source.index ? "after" : "before";
        reorderViewBoard({ fromId, referenceId: toId, type });
      }
      if (type === "ROW") {
        const fromKanbanId = +source.droppableId;
        const toKanbanId = +destination.droppableId;
        if (fromKanbanId === toKanbanId) {
          return;
        }
        const fromTask = allTasks.filter((task) => task.kanbanId === fromKanbanId)[source.index];
        const toTask = allTasks.filter((task) => task.kanbanId === toKanbanId)[destination.index];
        if (fromTask?.id === toTask?.id) {
          return;
        }
        reorderTask({
          fromId: fromTask?.id,
          referenceId: toTask?.id,
          fromKanbanId,
          toKanbanId,
          type:
            fromKanbanId === toKanbanId && destination.index > source.index
              ? "after"
              : "before",
        });
      }
    },
    [viewboards, reorderViewBoard, allTasks, reorderTask]
  );
};
...

至此,拖拽持久化完成,查看效果验证


部分引用笔记还在草稿阶段,敬请期待。。。

相关推荐
我要洋人死41 分钟前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人1 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人1 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR1 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596931 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9151 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼2 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
小牛itbull3 小时前
ReactPress:重塑内容管理的未来
react.js·github·reactpress