• 现代 React Web 开发实战——kanban实现卡片拖拽


    前提摘要:
    学习宋一玮 React 新版本 + 函数组件 &Hooks 优先
    开篇就是函数组件+Hooks
    实现的效果如下:
    学到第11篇了 照葫芦画瓢,不过老师在讲解的过程中没有考虑拖拽目标项边界问题,我稍微处理了下这样就实现拖拽流畅了

    在这里插入图片描述

    下面就是主要的代码了,实现拖拽(src/App.js):
    核心在于标记当前项,来源项,目标项,并且在拖拽完成时对数据处理,更新每一组数据(useState);

    /** @jsxImportSource @emotion/react */
    // 上面代码是使用emotion的关键CSS-in-JS
    import React, { useEffect, useState, useRef } from "react";
    import { css } from "@emotion/react";
    import "./App.css";
    const MINUTE = 60 * 1000;
    const HOUR = 60 * MINUTE;
    const DAY = 24 * HOUR;
    const UPDATE_INTERVAL = MINUTE;
    // const ongoingList = [{ title: "进行任务", status: "2022-11-09 15:29" }];
    // const doneList = [{ title: "完成任务", status: "2022-11-09 15:59" }];
    const KanbanBoard = ({ children }) => (
      <main
        css={css`
          flex: 10;
          display: flex;
          flex-direction: row;
          gap: 1rem;
          margin: 0 1rem 1rem;
        `}
      >
        {children}
      </main>
    );
    const KanbanColumn = ({
      children,
      className,
      title,
      setIsDragSource = () => { },
      setIsDragTarget = () => { },
      onDrop,
    }) => {
      const combinedClassName = `kanban-column ${className}`;
      return (
        <section
          className={combinedClassName}
          onDragStart={() => setIsDragSource(true)}
          onDragOver={(evt) => {
            evt.preventDefault();
            evt.dataTransfer.dropEffect = "move";
            setIsDragTarget(true);
          }}
          onDragLeave={(evt) => {
            evt.preventDefault();
            evt.dataTransfer.dropEffect = "none";
            setIsDragTarget(false);
          }}
          onDrop={(evt) => {
            evt.preventDefault();
            onDrop && onDrop(evt);
          }}
          onDragEnd={(evt) => {
            evt.preventDefault();
            setIsDragSource(false);
            setIsDragTarget(false);
          }}
        >
          <h2>{title}</h2>
          <ul>{children}</ul>
        </section>
      );
    };
    const KanbanCard = ({ title, status, onDragStart }) => {
      const [displayTime, setDisplayTime] = useState(status);
      useEffect(() => {
        const updateDisplayTime = () => {
          const timePassed = new Date() - new Date(status);
          let relativeTime = "刚刚";
          if (MINUTE <= timePassed && timePassed < HOUR) {
            relativeTime = `${Math.ceil(timePassed / MINUTE)} 分钟前`;
          } else if (HOUR <= timePassed && timePassed < DAY) {
            relativeTime = `${Math.ceil(timePassed / HOUR)} 小时前`;
          } else if (DAY <= timePassed) {
            relativeTime = `${Math.ceil(timePassed / DAY)} 天前`;
          }
    
          setDisplayTime(relativeTime);
        };
        const intervalId = setInterval(updateDisplayTime, UPDATE_INTERVAL);
        updateDisplayTime();
        return function cleanup() {
          clearInterval(intervalId);
        };
      }, [status]);
      const handleDragStart = (evt) => {
        evt.dataTransfer.effectAllowed = "move";
        evt.dataTransfer.setData("text/plain", title);
        onDragStart && onDragStart(evt);
      };
      return (
        <li className="kanban-card" draggable onDragStart={handleDragStart}>
          <div className="card-title">{title}</div>
          <div className="card-status">{displayTime}</div>
        </li>
      );
    };
    const AddKanbanCard = ({ onSubmit }) => {
      const [title, setTitle] = useState("");
      const handleChange = (evt) => {
        setTitle(evt.target.value);
      };
      const handleKeyDown = (evt) => {
        if (evt.key === "Enter") onSubmit(title);
      };
      const inputElem = useRef(null);
      useEffect(() => {
        inputElem.current.focus();
      });
      return (
        <li className="kanban-card">
          <h4>添加新卡片</h4>
          <div className="card-title">
            <input
              ref={inputElem}
              type="text"
              value={title}
              onChange={handleChange}
              onKeyDown={handleKeyDown}
            ></input>
          </div>
        </li>
      );
    };
    const DATE_STORE_KEY = "kanban_data_store";
    const COLUMN_KEY_TODO = "todo";
    const COLUMN_KEY_ONGONING = "ongoing";
    const COLUMN_KEY_DONE = "done";
    function App() {
      const [todoList, setTodoList] = useState([
        { title: "开发任务-1", status: "2022-05-22 18:15" },
      ]);
      const [ongoingList, setOngoingList] = useState([
        { title: "进行任务-1", status: "2022-08-22 18:15" },
      ]);
      const [doneList, setDoneList] = useState([
        { title: "完成任务-1", status: "2022-10-22 18:15" },
      ]);
      const [showAdd, setShowAdd] = useState(false);
      const handleAdd = (evt) => {
        setShowAdd(true);
      };
      const handleSubmit = (title) => {
        // todoList.unshift({title,status:new Date().toDateString()});
        setTodoList((current) => [{ title, status: new Date() + " " }, ...current]);
        setShowAdd(false);
      };
      const handleSaveAll = () => {
        const data = JSON.stringify({
          todoList,
          ongoingList,
          doneList,
        });
        window.localStorage.setItem(DATE_STORE_KEY, data);
      };
      useEffect(() => {
        const data = window.localStorage.getItem(DATE_STORE_KEY);
        setTimeout(() => {
          if (data) {
            const kanbanColumnData = JSON.parse(data);
            setTodoList(kanbanColumnData.todoList);
          }
        }, 1000);
      });
      const [draggedItem, setDraggedItem] = useState(null);
      const [dragSource, setDragSource] = useState(null);
      const [dragTarget, setDragTarget] = useState(null);
      const handleDrop = (evt) => {
        if (!draggedItem || !dragSource || !dragTarget || dragSource === dragTarget) { return; }
        const updaters = {
          [COLUMN_KEY_TODO]: setTodoList,
          [COLUMN_KEY_ONGONING]: setOngoingList,
          [COLUMN_KEY_DONE]: setDoneList
        };
        if (dragSource) {
          updaters[dragSource]((currentStat) => {
            return currentStat.filter((item) => !Object.is(item, draggedItem));
          });
        }
        if (dragTarget) {
          updaters[dragTarget]((currentStat) => {
            if (currentStat.length > 0) {
              return [draggedItem, ...currentStat]
            } else {
              return [draggedItem]
            }
          })
        }
      };
      return (
        <div className="App">
          <header className="App-header">
            <h1>
              我的看板<button onClick={handleSaveAll}>保存所有卡片</button>{" "}
            </h1>
          </header>
          <KanbanBoard>
            <KanbanColumn
              className="column-todo"
              title={
                <>
                  待处理
                  <button disabled={showAdd} onClick={handleAdd}>
                    &#8853;添加新卡片
                  </button>{" "}
                </>
              }
              setIsDragSource={(isSrc) =>
                setDragSource(isSrc ? COLUMN_KEY_TODO : null)
              }
              setIsDragTarget={(isTarget) =>
                setDragTarget(isTarget ? COLUMN_KEY_TODO : null)
              }
              onDrop={handleDrop}
            >
              {/* 

    待处理 {" "}

    */
    } {/*
      */} {showAdd && <AddKanbanCard onSubmit={handleSubmit} />} {todoList && todoList.map((item) => ( <KanbanCard {...item} key={item.title} onDragStart={() => setDraggedItem(item)} /> ))} {/*
    */
    } </KanbanColumn> <KanbanColumn className="column-ongoing" title={"进行中"} setIsDragSource={(isSrc) => setDragSource(isSrc ? COLUMN_KEY_ONGONING : null) } setIsDragTarget={(isTarget) => setDragTarget(isTarget ? COLUMN_KEY_ONGONING : null) } onDrop={handleDrop}> {/*

    进行中

      */} {ongoingList && ongoingList.map((item) => ( <KanbanCard {...item} key={item.title} onDragStart={() => setDraggedItem(item)} /> ))} {/*
    */
    } </KanbanColumn> <KanbanColumn className="column-done" title={"已处理"} setIsDragSource={(isSrc) => setDragSource(isSrc ? COLUMN_KEY_DONE : null) } setIsDragTarget={(isTarget) => setDragTarget(isTarget ? COLUMN_KEY_DONE : null) } onDrop={handleDrop}> {/*

    已处理

      */} {doneList && doneList.map((item) => ( <KanbanCard {...item} key={item.title} onDragStart={() => setDraggedItem(item)} /> ))} {/*
    */
    } </KanbanColumn> </KanbanBoard> </div> ); } export default App;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274

    这时拖拽基本完成,此时有一个bug,就是待处理添加新卡片的时候,拖拽之后的数据出现混乱!!如下所示:
    在这里插入图片描述
    首先问题定位,移动的来源项出现了问题,看代码之后发现拖拽处理来源项没有问题,那一定是那块调用更新todaList出现了问题,问题定位在useEffect,使用时如果useEffect的第二个参数不传就在组件所有更新都执行(即任何时候),传个空数组仅在挂载和卸载的时候执行,或者传个你想要去进行更新时候去执行(默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执行。)
    更改代码如下:

      useEffect(() => {
        const data = window.localStorage.getItem(DATE_STORE_KEY);
        setTimeout(() => {
          if (data) {
            const kanbanColumnData = JSON.parse(data);
            setTodoList(kanbanColumnData.todoList);
          }
        }, 1000);
      },[]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    useEffect新增空数组效果展示:
    在这里插入图片描述

    学习一个新的框架总是会进行对比,
    一:React的单项数据流和Vue中的双向绑定有什么区别?
    在我看了Vue 双向绑定其实是语法糖罢了,其原理其实是Object.defineProperty()对数据进行劫持,监听到变化就去对数据进行更改;
    而React 中的单项数据流做到了对原有数据的保护,你不能去直接去对Props进行更改,而是在需要赋值给State,然后再SetState中去进行更改,当然Hooks中提供了useState方法,使得开发者更加方便的去对数据进行处理和更改(我可太喜欢Hooks了很省事!!)
    二:JSX 是什么?
    学习React的时候,写组件的时候写页面元素是用JSX来写的,即Render里面是用JSX来实现的,渲染之后其实质是React.createElement,他只是语法糖 实现React组件的一部分而已,对比Vue中Template,(我更喜欢Vue的实现,更符合开发者)不过Vue也可用JSX来实现;
    三:函数式组件(Hooks)与类组件(Class)优缺点?
    宋一玮老师的数据表明函数式比类组件使用更多,并且函数组件基本上涵盖了类组件的功能点,除了(只有类组件才能成为错误边界)
    从React官网中开局也是用类组件来领进门的,我是看完了官网的基础才来学习课程的才觉得学习没有那么吃力反而能加深理解;(老师的反其道而行可能不太适合初学者)
    四:CSS可否想JS一样应用在组件中?
    可以的,使用emotion来应用到JSX中(主要代码中有使用),不过CSS中传入JS数据确实很方便但是在运行emotion时会创建大量的