小声BB
本文在翻译过程中确保意思传达准确的前提下,会加入很多本人的个人解释(会在括号中写,像这样)和自己的一些吐槽
时隔半年,重新点开 React-dnd
官网来,是因为最近真的要用这个插件了,想到好像之前挖了个坑没有填。这次就趁着和产品撕逼成功后的需求整改期,来把翻译的坑填一下。
前景提要
人称问题::原文大量使用第一人称,翻译时候尝试使用第一人称复数进行翻译来拉近读者之间的距离但是失败!所以还是翻译成第一人称单数形式。
一些坑: demo代码里面引入了一些未定义的变量,如果有跟着敲demo出现问题的可以参考这个issue
来解决,还解决不了的可以在评论下面留言(笑死,说的好像你的文章真的有人看一样)~
文中的代码都是截图:代码都是截图的原因有两点:
- 这个主题显示的代码很丑;
- 望不小心点到这个文章的掘友可以自己手动敲代码跟着教程做,会有些收获。
国际象棋游戏规则:帮你们百度了掘友们
做出来的效果:
👍👍👍👍 一起来试试吧~,觉得有用的希望可以点赞支持一下 👍👍👍👍
译文
原文链接:React DnD官方文档-教程篇
教程
现在,你已经读过了概览部分,是时候开始冒险了!
在这里,我要使用 React
和 React-DND
来做一个象棋游戏!开个玩笑,做一个完整的象棋游戏就超出教程的目标,我要做的是一个棋盘和一个孤独的骑士(国际象棋中的马)。这个骑士可以按照国际象棋的规则去拖动。
如果你已经熟悉了 React
,你可以直接跳到添加拖放交互章节
(建议不要跳,棋盘从0到1的过程有些想法值得学习)。
我将用这个例子向你展示 react-dnd
的数据驱动思想。你会学习到如何创建一个可拖拽项 和 可放置容器,如何把它们和你的组件关联在一起,然后看看它们如何与拖拽事件进行交互。
让我开始吧!(搓手)
环境准备
在这个教程里,代码例子都是使用函数型组件和现代 JavaScript
语法(大概意思是用的js语法比较新)。建议你们使用构建工具把这些新语法特性进行转译来匹配你们开发环境。我建议直接使用create-react-app。
你可以先在这里预览要做的东西。
开始整!
定义组件
首先来创建一些组件,先把拖拽的事情放一放,如果想要把拖拽棋子功能做出来,会需要哪些组件呢?能想到的有下面这几个:
Knight
:孤独的骑士棋子;Square
:棋盘上的小格子;Board
: 有64个小格子的棋盘; 想一下它们的props
会有哪些。Knight
可能是一个无状态组件。它有一个位置
属性,但是Knight
本身不需要知道,可以通过Square
的位置来定位子元素Knight
的位置(即通过props
把position
传进来)。- 可以通过
props
把位置
传给Square
(然后传给Knight),但是没有必要。因为Square
唯一需要的信息就是它要渲染的背景色。可以把Square
的背景色设置成白色,然后添加一个可选的黑色作为可选的props
。当然,Square
可能会有一个子节点,即当前落在该格子内的棋子,棋子就选一个白色当做默认颜色来匹配浏览器的默认色。 Board
组件有点难搞。把Square
组件当做children
传给它没啥意义(就是说不需要把Board
当做容器组件,然后用{props.children}
去渲染Square
),因为Board
组件本身还能有啥?它只有Square
组件,同时它还需要包含Knight
,因为Knight
组件需要要放置在Square
组件里面。这意味着Board
组件需要知道Knight
当前的位置。在真正的国际象棋游戏中,Board
需要接收一个描述所有棋子的颜色和位置的数据集合。但是对这个例子来说,一个knightPosition
的prop
更方便。我使用一个长度为2
的数组作为棋子坐标,[0,0]
表示A8
坐标的格子,为什么[0,0]
表示A8
不表示A1
? 是为了匹配浏览器的坐标方向,而使用另外一种坐标匹配(指用[0,0]
去对应A1
)让我脑壳疼~(下面贴个图好理解A8和A1的位置)
那我要把 state
放哪里呢?我实在是不想把它们放在 Board
组件中。可能的话,在子组件中放一些 state
也是不错的选择。另外,Board
组件已经有一些排版的逻辑,所以我不想让它再去管理 state
。
好消息是,那个不是我现在考虑的事情。我只管先写而不去管state
放在哪里,先把东西正确渲染出来,然后再考虑管理state
的事情。
创建组件
我更喜欢自下而上去写,因为这样可以边做边看效果。如果从 Board
组件开始写,那么在写完 Square
之前页面上啥也没有。另外就是我可以在不考虑 Board
的情况下,直接边写边看到 Square
。我认为这种即时反馈是非常有必要的。
所以我从 Knight
开始写。它没有任何的 props
属性,很简单就能搞定:
♘是一个 Unicode
编码的棋子!十分的帅气。我本可以把颜色作为棋子的 props
属性,但是在这个例子里面不会有黑色的棋子,所以不需要。
这看起来可以跑起来,为了确保没有问题,立马把挂载的组件改一下把它渲染测试一下。
每次我写完一个组件都会做这件事,这样我每次都有东西可以渲染。在大型app开发的时候,我会使用类似cosmos组件库(用于开发和测试组件的沙盒环境),这样就不会闷着头写不知道组件写成啥样子了。
Knight
显示在屏幕上了,是时候去实现 Square
了,下面是我的第一次尝试。
接着我把挂载的组件换一下来看看Knight
在Square
里面是啥样子:
哦吼,页面没有成功渲染。我犯了几个小错误:
- 没有给
Square
任何尺寸。我不想让Square
是固定的尺寸,所以我给个width:'100%'
和height:'100%'
。 - 忘记在
Square
的div
里面加{children}
,所以Knight
被忽略了没有渲染出来。 这两个错误改完之后,棋格子是黑的,我仍然无法看到我的棋子。因为浏览器文本的默认值是黑色的,所以在黑色的棋格中看不到棋子。可以给Knight
一个color
的props
来解决这个问题,但是更简单的修复方法就是在设置backgroundcolor
的地方去设置文本颜色。这样Square
就可以在修复这些错误的同时兼容黑白两种颜色。
终于,要开始 Board
部分!首先我写一个简单版本-直接返回一个 Square
。
目前为止,我唯一要的就是它在页面中成功渲染出来,然后才可以开始下一步:
确实,现在可以看到一个棋格子。现在我需要加一堆棋格!但是从哪里开始呢?要在 render
方法中放些啥? for
循环?还是 map
循环个数组?
老实说,现在并不需要去想这些。我已经知道了如何去渲染不同状态的棋格。我也通过 knightPosition
知道了棋子的坐标位置。这意味这我可以写一个 renderSquare
方法去渲染 Square
,如何渲染 Board
这个事先放在一边。
第一次尝试写的 renderSquare
方法长这样:
可以试着将 Board
的渲染方法改成下面这样
到这里,我发现没有给我的棋格子们添加任何布局。我使用 flex
布局,添加一些样式到根 div
,然后把Square
用 div
包住,从而可以把 Square
排列一下。一般来说,尽管需要添加额外的 div
去包裹组件,但是把组件封装成一个不需要关注其内部布局的组件仍然是一个好方法。
看起来非常不错!我不知道如何约束 Board
以保持棋格的宽高比,但这应该很容易在之后加上。
回想一下,我从0
开始,到现在可以通过改变 knightPosition
来让 Knight
在 Board
上移动:
The declarativeness is fantastic! That's why people love working with React. (这里是一个React的彩虹屁,不想翻译!)
添加 Game
状态管理器
我希望可以拖动这个棋子。为了实现这点,我需要把 knightPosition
保存在 state
中存储,并且有一些方法可以去改变它。
因为设置 state
相关的事情需要费点脑子,我不会尝试同时去想如何实现拖动。 相反,我将从一个更简单的实现开始。当你点击一个棋格的时候,我就把棋子移动过去。但是需要遵守国际象棋的规则。实现这个逻辑需要我对管理状态有足够的了解,所以可以在处理完责这个逻辑之后再用拖放代替点击。
React
支持多种状态管理和数据流工具;你可以使用 Flux
,Redux
,Rx
或者~~Backbone~~
,avoid fat models and separate your reads from writes。
我不想为这个简单的例子安装或设置 Redux
,所以我将用一个更简单的模式。它没有 redux
那么全面,但我也不需要它那么全面。 我还没有决定我的状态管理器的 API
,但我先将把它称为 Game
,它肯定需要某种方式来向我的 React
代码发送数据更改信号。
知道这些之后,我可以重写我的 index.js
,在 index.js
中引入一个还不存在的 Game
。注意,此时我在瞎写代码,跑不起来,因为我还不清楚 Game
里面要导出什么 API
。
我导入的这个 observe
函数是啥?这是我能想到的订阅不断变化状态的最简单的方式。我本可以把他变成一个EventEmitter
(事件触发器),但是当我需要的只是一个单一的变化事件的时候,我为什么要那么做呢?我可以把Game
做成一个对象模型,但是我需要的只是一系列的简单值,我为什么需要那么做呢?
为了验证这个订阅 API
是否起作用,我将写一个返回随机位置的 Game
:
重新把项目跑起来的感觉针不戳!
这个现在肯定还用不了,如果我想要一些交互效果,我需要找到在 component
中修改 Game
中 state
的方法。现在,我尽可能简单的直接暴露一个 moveknight
方法来修改内部的 state
。在稍微复杂的应用中,用户的一个动作可能会引起多个不同的 state
变化,这个做法就行不通,但在我这个例子中就没有这个问题。
现在回到我的组件。当前的目标就是把棋子移动到我点击的棋格上。一种方法是在 Square
组件内调用 moveKnight
方法。但是,这样就需要我把位置传给 Square
。下面是一条好的经验法则:
如果一个组件在渲染的时候不需要某些数据,那么这个组件压根上就不需要这些数据。
Square
组件不需要知道棋子的位置去渲染。所以最好避免 moveknight
方法和 Square
组件耦合。替代方案就是,我添加一个 onClickhandler
方法在 Board
组件中包裹 Square
的 div
上。
我也可以向 Square
添加一个 onClickprop
来代替,但既然我稍后将删除点击处理事件以支持拖放界面,那么何必呢?
现在最后一部分是检查国际象棋规则。 棋子(马)不能随便移动到一个任意的方格,只允许做L-shaped moves。 我向 Game
中添加一个 canMoveKnight(toX, toY)
函数并将初始位置更改为 B1
以匹配国际象棋规则:
最后,我在 handleSquareClick
方法中添加了一个 canMoveKnightcheck
:
目前为止做的还不错!
添加拖拽交互
这是真正促使我编写本教程的部分。我们现在将看到使用 React DnD
向现有组件添加一些拖放交互是多么容易。
这部分中假定你已经对 overview
中提到的一些概念,像 backends
, collecting functions
, types
, items
, drag sources
, drop targets
,多少有了一些了解。如果你对这些东西一点都不了解也没有关系,在你开始编码之前给他们一个机会(去瞅瞅!)。
我们从安装 React DnD
和 HTML5 backend
开始:
npm install react-dnd react-dnd-html5-backend
将来,您可能想要探索替代的第三方 backends
,例如 touch backend,但这超出了本教程的范围。
设置拖拽上下文
我们需要在我们的应用程序中设置的第一件事是 DndProvider
。DndProvider
应该挂载在我们应用程序的顶部(在根节点APP的外层)来说明我们要用 HTML5Backend
。
定义拖拽类型
接下来,我要为可拖拽的 itemtypes
创建常量。在我们游戏中只有一个 itemtype
,就是这个孤独的骑士棋子。创建一个 Constants
对象并导出:
准备工作搞定,让我们给 Knight
加个 buff
,让它可以拖动起来!
让马儿跑起来
useDraghook
接受一个返回特定格式对象的记忆(memoization
)函数。 在这个特定格式对象中,item.type
被设置为我们刚刚定义的常量,现在,我们需要编写一个收集函数。
咱们细细品品:
-
useDrag
接受一个有特定格式的对象作为参数。item.type
的值是必需的,它指定了拖动item
的类型。我们还可以再这里加其他的信息来识别其他被拖动的棋子的type
。但是既然我们这是一个玩具应用,所以只需要定义这一个type
。 -
collect
定义了一个收集器函数:这个函数主要作用是把拖拽系统
中的state
转换成可以被组件使用的props
。 -
结果数组
包括:- 数组第一项是一个
props
对象-包含从拖拽系统
中收集到的所有属性。 - 数组第二项是一个
ref function
。这个方法用来让Dom元素
和react-dnd
建立联系。
- 数组第一项是一个
现在Knight组件加了 useDrag
调用和更新了 render
方法,让我们看看它现在长啥样子:
让Board Squares可放置
Knight
现在是一个可拖拽项,但是目前还没有可放置容器。我们现在就去让 Square
变成一个可放置容器。
这次我们无可避免的需要传递 位置
属性给 Square
。毕竟如果 Square
不知道自己的位置,它是无法知道如何放置被拖拽的棋子的。但反过来说,这样也不对,因为 Square
仍然是我们应用中没有变化过的整体,它以前只是一个简单的组件,为什么需要把它变复杂呢?当遇到这种进退两难的场景,是时候把有状态组件和无状态组件进行拆分了。
我即将要介绍一个新的组件,叫做 BoardSquare
。它既渲染了之前的 Square
,又知道它的位置。实际上,它封装了过去 Board
内部的 renderSquare
方法所做的一些逻辑。在合适的时机,通常可以从此类渲染方法中提取出 React
组件。
下面是我提取出来的组件:
我也修改了Board组件:
现在我们把 BoardSquare
组件用 useDrop
钩子包裹住。我准备写一个只处理 drop
事件的可放置容器:
看到了吗? drop
方法可以拿到 BoardSquare
的 props
属性,所以容器可以知道当棋子 drop
的时候,要把棋子移动到哪里。在一个真正的应用程序中,我可能还会使用 monitor.getItem()
来检索可拖拽项从 beginDrag
返回的 dragged item,但由于我们在整个应用程序中只有一个可拖动的东西,所以我不需要这样做。
在我的收集函数中,我将询问监听器鼠标是否在 BoardSquare 上,以便我决定是否高亮它。
在修改 render
方法去连接可放置容器并且显示高亮蒙板后,这是 BoardSquare
现在的样子:
这个开始变得很好了!距离完成教程还有一步之遥。我们想要在拖动棋子的时候高亮可移动的棋格,而且只在棋子落在有效的棋格上时才去处理
drop
事件。
值得庆幸的是,用 React DnD
真的很容易做到这一点。我只需要在我的可放置容器配置中定义一个 canDrop
方法就行。
我还在我的收集函数中加入了 monitor.canDrop()
,以及一些高亮蒙板渲染逻辑到组件中:
添加一个拖动预览图片
我想演示的最后一件事是拖动预览图的定制。当然,浏览器会对 DOM
节点进行截图,但如果我们想显示一个自定义的图片呢?
我们又很幸运,因为用 React DnD
很容易做到这一点。我们只需要使用 useDrag
钩子提供的 preview ref(不好翻译 直接加粗就好!)。
preview ref 让我们在渲染函数中可以连接一个 dragPreview
,就像我们用于定义可拖动项一样。 react-dnd
还提供了一个实用组件 DragPreviewImage
,它可以使用这个 ref
来展示一个图像作为拖动预览。
结语
本教程引导你创建React组件,对组件和应用 state 做出设计决策,最后添加drag and drop交互。本教程的目的是向你展示 React DnD
与 React
的理念非常契合,在潜心实现复杂的交互之前,你应该先考虑应用程序的架构。
Happy dragging and dropping.
译者结语
React-DnD
的教程部分文档,从 0
开始带读者实现一个小 demo
。在 demo
中穿插 React
组件设计思路和 state
的状态管理拆分。全文不仅仅是讲到了 React-DnD
的相关 API
,也为我们展现了一个由 React
构建的生动丰富的生态系统圈。
翻译讲究信达雅,自己慢慢在摸索。自知还没有到信这一个层级,尤其是技术方面的文章,有很多技术圈里的特殊含义,查字典有时候都查不出来,这种只能靠慢慢积累。
~peace~