From 1b0a7ed8eae65c23bd7366fd054e29381c234f37 Mon Sep 17 00:00:00 2001
From: ascoders <576625322@qq.com>
Date: Mon, 24 Feb 2020 09:08:57 +0800
Subject: [PATCH] finish 140
---
...13\345\206\231 JSON Parser\343\200\213.md" | 2 +-
...\347\224\237 Drag Drop API\343\200\213.md" | 233 ++++++++++++++++++
2 files changed, 234 insertions(+), 1 deletion(-)
create mode 100644 "140.\347\262\276\350\257\273\343\200\212\347\273\223\345\220\210 React \344\275\277\347\224\250\345\216\237\347\224\237 Drag Drop API\343\200\213.md"
diff --git "a/139.\347\262\276\350\257\273\343\200\212\346\211\213\345\206\231 JSON Parser\343\200\213.md" "b/139.\347\262\276\350\257\273\343\200\212\346\211\213\345\206\231 JSON Parser\343\200\213.md"
index 594f7a2c..01fa8bee 100644
--- "a/139.\347\262\276\350\257\273\343\200\212\346\211\213\345\206\231 JSON Parser\343\200\213.md"
+++ "b/139.\347\262\276\350\257\273\343\200\212\346\211\213\345\206\231 JSON Parser\343\200\213.md"
@@ -54,7 +54,7 @@ function fakeParseJSON(str) {
}
```
-其中 `skipWhitespace` 表示匹配并跳过空格,所谓匹配意味着匹配成功,此时 `i` 下标可以继续后移,否则匹配失败。下一步则判断如果 `i` 不是结束标志 `}`,则按照 `parseString` 匹配字符串 → `skipWhitespace` 跳过空格 → `eatColon` 吃掉逗号 → `parseValue` 匹配值,这个链路循环。其中吃掉逗号表示 “匹配逗号但不会产生任何结果,所以就像吃掉了一样”,吃这个动作还可以用在其他场景,比如吃掉尾分号。
+其中 `skipWhitespace` 表示匹配并跳过空格,所谓匹配意味着匹配成功,此时 `i` 下标可以继续后移,否则匹配失败。下一步则判断如果 `i` 不是结束标志 `}`,则按照 `parseString` 匹配字符串 → `skipWhitespace` 跳过空格 → `eatColon` 吃掉冒号 → `parseValue` 匹配值,这个链路循环。其中吃掉冒号表示 “匹配冒号但不会产生任何结果,所以就像吃掉了一样”,吃这个动作还可以用在其他场景,比如吃掉尾分号。
> 对于看到这儿的小伙伴,笔者要友情提示一下,原文的思路是一种定制语法解析思路,无论是 `eatColon` 还是 `parseValue` 都仅具备解析 JSON 的通用性,但不具备解析任意语法的通用性。如果你想做一个具备解析任何通用语法的解析器,读入的内容应该是语法描述,处理方式必须更加通用,如果感兴趣可以阅读 [精读《手写 SQL 编译器 - 语法分析》](https://github.com/dt-fe/weekly/blob/v2/066.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md) 系列文章了解更多。
diff --git "a/140.\347\262\276\350\257\273\343\200\212\347\273\223\345\220\210 React \344\275\277\347\224\250\345\216\237\347\224\237 Drag Drop API\343\200\213.md" "b/140.\347\262\276\350\257\273\343\200\212\347\273\223\345\220\210 React \344\275\277\347\224\250\345\216\237\347\224\237 Drag Drop API\343\200\213.md"
new file mode 100644
index 00000000..3f717257
--- /dev/null
+++ "b/140.\347\262\276\350\257\273\343\200\212\347\273\223\345\220\210 React \344\275\277\347\224\250\345\216\237\347\224\237 Drag Drop API\343\200\213.md"
@@ -0,0 +1,233 @@
+## 1 引言
+
+拖拽是前端非常常见的交互操作,但显然拖拽是强 DOM 交互的,而 React 绕过了 DOM 这一层,那么基于 React 的拖拽方案就必定值得聊一聊。
+
+结合 [How To Use The HTML Drag-And-Drop API In React](https://www.smashingmagazine.com/2020/02/html-drag-drop-api-react/) 这篇文章,让我们谈谈 React 拖拽这些事。
+
+## 2 概述
+
+原文说的比较简单,笔者先快速介绍其中重点部分。
+
+首先拖拽主要的 API 有 4 个:`dragEnter` `dragLeave` `dragOver` `drop`,分别对应拖入、拖出、正在当前元素范围内拖拽、完成拖入动作。
+
+基于这些 API,我们可以利用 React 实现一个拖入区域:
+
+```jsx
+import React from "react";
+
+const DragAndDrop = props => {
+ const handleDragEnter = e => {
+ e.preventDefault();
+ e.stopPropagation();
+ };
+ const handleDragLeave = e => {
+ e.preventDefault();
+ e.stopPropagation();
+ };
+ const handleDragOver = e => {
+ e.preventDefault();
+ e.stopPropagation();
+ };
+ const handleDrop = e => {
+ e.preventDefault();
+ e.stopPropagation();
+ };
+ return (
+
handleDrop(e)}
+ onDragOver={e => handleDragOver(e)}
+ onDragEnter={e => handleDragEnter(e)}
+ onDragLeave={e => handleDragLeave(e)}
+ >
+
Drag files here to upload
+
+ );
+};
+export default DragAndDrop;
+```
+
+`preventDefault` 指的是阻止默认响应,这个响应可能是跳转页面之类的,`stopPropagation` 是阻止冒泡,这样同样监听了事件的父元素就不会收到响应,我们可以精准作用于嵌套的子元素。
+
+接下来是拖拽状态管理,提到了 `useReducer`,顺便复习一下用法:
+
+```jsx
+...
+const reducer = (state, action) => {
+ switch (action.type) {
+ case 'SET_DROP_DEPTH':
+ return { ...state, dropDepth: action.dropDepth }
+ case 'SET_IN_DROP_ZONE':
+ return { ...state, inDropZone: action.inDropZone };
+ case 'ADD_FILE_TO_LIST':
+ return { ...state, fileList: state.fileList.concat(action.files) };
+ default:
+ return state;
+ }
+};
+const [data, dispatch] = React.useReducer(
+ reducer, { dropDepth: 0, inDropZone: false, fileList: [] }
+)
+...
+```
+
+最后一个关键点在于拖入后的处理,利用 `dispatch` 增加拖入文件、设置拖入状态即可:
+
+```js
+const handleDrop = e => {
+ ...
+ let files = [...e.dataTransfer.files];
+
+ if (files && files.length > 0) {
+ const existingFiles = data.fileList.map(f => f.name)
+ files = files.filter(f => !existingFiles.includes(f.name))
+
+ dispatch({ type: 'ADD_FILE_TO_LIST', files });
+ e.dataTransfer.clearData();
+ dispatch({ type: 'SET_DROP_DEPTH', dropDepth: 0 });
+ dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: false });
+ }
+};
+```
+
+`e.dataTransfer.clearData` 函数用于清除拖拽过程中产生的临时变量,这些临时变量可以通过 `e.dataTransfer.xxx =` 的方式赋值,一般用于拖拽过程中值的传递。
+
+总结一下,利用 HTML5 的 API 将拖拽转化为状态,最终通过状态映射到 UI。
+
+原文内容还是比较简单的,笔者在精读部分再拓展一些更体系化的内容。
+
+## 3 精读
+
+现阶段拖拽主要分为两种,一种是 HTML5 原生规范的拖拽,这种方式在拖拽过程中不会影响 DOM 结构。另一种是完全所见即所得的拖拽方式,拖拽过程中 DOM 位置会随之变动,好处是可以立即反馈拖拽结果,当然缺点是华而不实,一旦用在生产环境,这种拖拽过程可能导致页面结构频繁跳动,反而看不清拖拽效果。
+
+由于本文也采用了第一种拖拽方案,因为笔者再重新整理一遍自己的封装思路。
+
+从使用角度反推,假设我们拥有一个拖拽库,那必定要拥有两个 API:
+
+```jsx
+import { DragContainer, DropContainer } from 'dnd'
+
+const DragItem = (
+
+ {({ dragProps }) => (
+
+ )}
+
+)
+
+const DropItem = (
+
+ {({ dropProps }) => (
+
+ )}
+
+)
+```
+
+`DragContainer` 包裹可以被拖拽的元素,`DragContainer` 包裹可以被拖入的元素,而至于 `dragProps` 与 `dropProps` 需要透传到子元素的 dom 节点,是为了利用 DOM API 控制拖拽效果,这也是拖拽唯一对 DOM 的要求,双方元素都需要有实体 DOM 承载。
+
+而上面例子中给出 `dragProps` 与 `dropProps` 的方式属于 RenderProps,我们可以将 `children` 当作函数执行以达到效果:
+
+```jsx
+const DragContainer = ({ children, componentId }) => {
+ const { dragProps } = useDnd(componentId)
+
+ return children({
+ dragProps
+ })
+}
+
+const DropContainer = ({ children, componentId }) => {
+ const { dropProps } = useDnd(componentId)
+
+ return children({
+ dropProps
+ })
+}
+```
+
+那么这里创建了一个自定义 Hook `useDnd` 接收 `dragProps` 与 `dropProps`,这个自定义 Hook 可以这么写:
+
+```jsx
+const useDnd = ({ componentId }) => {
+ const dragProps = {}
+ const dropProps = {}
+
+ return { dragProps, dropProps }
+}
+```
+
+接下来,我们就要分别实现 `drag` 与 `drop` 了。
+
+对 `drag` 来说,只要实现 `onDragStart` 与 `onDragEnd` 即可:
+
+```jsx
+const dragProps = {
+ onDragStart: ev => {
+ ev.stopPropagation()
+ ev.dataTransfer.setData('componentId', componentId)
+ },
+ onDragEnd: ev => {
+ // 做一些拖拽结束的清理工作
+ }
+}
+```
+
+`stopPropagation` 的作用在原文简介中已经介绍过了,`setData` 则是通知拖拽方,当前拖拽的组件 id 是什么,**这是由于拖拽由 `drag` 发起而由 `drop` 响应,因此必须有个数据传输过程,而 `dataTransfer` 就最适合做这件事。**
+
+对于 `drop` 来说,只要实现 `onDragOver` 与 `onDrop` 即可:
+
+```jsx
+const dropProps = {
+ onDropOver: ev => {
+ // 做一些样式处理,提示用户此时松手会将元素防止在何处
+ },
+ onDrop: ev => {
+ ev.stopPropagation()
+ const componentId = ev.dataTransfer.getData('componentId')
+ // 通过 componentId 修改数据,通过 React Rerender 刷新 UI
+ }
+}
+```
+
+重点在 `onDrop`,它是实现拖拽效果的 “真正执行处”,最终通过修改 UI 的方式更新数据。
+
+存在一种场景,一个容器既可以被拖动,也可以被拖入,这种情况一般这个组件是个容器,但这个容器可以被拖入到其他容器中,可以自由嵌套。
+
+实现这种场景的方式就是将 `DragContainer` 与 `DropContainer` 作用到一个组件上:
+
+```jsx
+const Box = (
+
+ {({ dragProps }) => (
+
+ {({ dropProps }) => {
+
+ }}
+
+ )}
+
+)
+```
+
+之所以能嵌套,在于 HTML5 的 API 允许一个元素同时拥有 `onDragStart`、`onDrop` 这两种属性,而上面的语法不过是同时将这两种属性传给组件 DOM。
+
+所以,动手实现一个拖拽库就是这么简单,只要活用 HTML5 的拖拽 API,结合 React 一些特殊语法便够了。
+
+## 4 总结
+
+最后留下一个思考题,许多具有拖拽功能的系统都具备 “拖拽 placeholder” 的功能,即拖拽元素的过程中,在其 “落点” 位置展示一条横线或竖线,引导出松手后元素位置落点,如图所示:
+
+
+
+那么这条辅助线是通过什么方式实现的呢?欢迎在评论区留言!如果你有辅助线实现方案解析的文章,欢迎分享,也可以期待笔者未来专门写一篇 “拖拽 placeholder” 实现剖析的精读。
+
+> 讨论地址是:[精读《手写 JSON Parser》 · Issue #233 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/233)
+
+**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
+
+> 关注 **前端精读微信公众号**
+
+
+
+> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))