白板:React Hooks
钩子
使用状态
使用效果
建造什么?
计划是什么?
木板
鼠标按下
鼠标抬起
处理鼠标移动
控制
颜色
橡皮
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
我们之前在《文本录制器:React 状态、事件处理和条件渲染》一文中讨论过状态以及如何设置和处理状态的变化。当时使用的是类组件,但当然,我们不必使用类组件也能获得所有优势,对吧?
让我们来看看如何对函数组件执行相同的操作!
钩子
Hooks 允许我们在函数组件中使用状态和生命周期方法。它们并非一直存在,而是最近在 React 16.8 中引入的。
它们是 JavaScript 函数,但不能在循环、条件语句或嵌套函数中调用。它们必须始终在 React 函数的顶层调用。
我们将讨论两个主要的钩子:
- 使用状态
- 使用效果
使用状态
要在类组件中设置状态,我们可以this.state在构造函数或this.setState()其他任何地方使用它。我们的代码可能如下所示:
this.setState({
dummyState: "dum dum"
});
要使用 Hooks 重写上述代码,我们需要用到 useState 函数。它接受一个参数,用于设置状态的初始值,并返回一个数组,数组的第一个元素是当前状态的值,第二个元素是一个函数,该函数稍后将用于设置状态的值。
const [dummyState, setDummyState]= useState("dum dum");
当然,我们可以随意命名它们,但惯例如上所述。此外,通常使用数组解构方法来轻松访问返回值。
要稍后更新状态值,我们只需使用更新后的值调用返回的函数即可。
setDummyState("dum dum dum");
使用效果
我们之前在《Woof Vs. Meow:数据获取和 React 组件生命周期》一文中学习了 componentDidMount、componentDidUpdate 和 componentWillUnmount 。我们的 useEffect Hook 可以等效地实现所有这些方法的功能。是不是很酷?
useEffect 接受一个函数作为参数,还可以接受一个可选的数组。让我们把下面的代码翻译成 Hooks 代码,以便更好地理解!
两个都
componentDidMount(){
functionThatFetchesSomeData();
}
和
componentDidUpdate(){
functionThatFetchesSomeData();
}
可以通过 useEffect Hook 实现相同的效果。
useEffect(()=>{
functionThatFetchesSomeData();
});
如前所述,useEffect Hook 的作用类似于 componentDidUpdate。它会在每次更新发生时重新运行。有时我们希望控制 useEffect 的运行时机,这就是第二个数组参数的作用所在。通过将特定的状态传递给这个数组,我们可以指示 Hook 将当前值与其先前的值进行比较,只有当它们不同时,我们的代码才会执行。
useEffect(()=>{
functionThatFetchesSomeData();
},[userId]);
我们可以设置多个 useEffect Hooks,每个 Hooks 都可以有自己的过滤器和代码。
如果我们只想在组件挂载时获取数据,并且不想在更新时重新运行代码,我们可以欺骗 Hook,并向其提供空数组作为第二个参数,这样它就永远不会检测到数组中的任何更改,并且只会运行一次。
我们最后要讨论的方法是 componentWillUnmount,它通常用作清理方法。为了让 Hook 知道要清理什么,我们只需返回一个包含清理指令的函数即可。
useEffect(()=>{
functionThatOpensAnImaginaryConnection();
return ()=>{
functionThatClosesAnImaginaryConnection();
}
});
这足以让我们开始动手做点什么了!我已经彻底上瘾了!
建造什么?
你知道有时候在解释某个概念的时候,你会很想用一张歪歪扭扭的手绘图表来佐证自己的理论吗?或者在解决问题的时候,你需要记下一些笔记才能更好地理解它?
今天,我们要自己动手制作一块白板,在上面画出我们想要的各种奇形怪状的图案和涂鸦!
在这里做一些小实验
计划是什么?
我们想要一大片空白区域用于绘画,所以我们的第一个组件就叫它“画板”吧!此外,我们还需要一些控件来改变颜色和擦除内容,因此我们的应用程序将再增加三个组件:一个用于控件,一个用于颜色,还有一个用于橡皮擦。
出发!
木板
现在,我们应该能够闭着眼睛安装 create-react-app 并创建文件夹结构,所以我就不赘述了。
在 Board 组件中,我们首先需要一个 canvas 元素。通常,为了给 canvas 添加 2D 上下文并使其可绘制,我们会使用 id 来选择它,但在 React 中,id 和 class 都不支持选择。因此,我们将使用 refs 来实现这一点。
我们之前讨论过如何在类组件中处理引用,函数组件中的处理方式也大同小异。让我们来看看它们具体是什么样的!
import React from "react";
import "./Board.css";
function Board() {
const canvasRef = React.useRef(null);
return (
<div className="board">
<canvas ref={canvasRef} />
</div>
);
}
export default Board;
让我们把看板添加到我们的应用程序中,以便像往常一样查看更改!
import React from "react";
import "./App.css";
import Board from "./components/Board/Board";
function App() {
return (
<div className="app">
<Board />
</div>
);
}
export default App;
现在我们要开始使用 Hooks 了。让我们导入 useState,然后首先添加上下文!
import React,{useState} from "react";
import "./Board.css";
function Board() {
const canvasRef = React.useRef(null);
const [ctx, setCtx] = useState({});
return (
<div className="board">
<canvas ref={canvasRef} />
</div>
);
}
export default Board;
首先,我们需要为画布设置上下文。在类组件中,我们会使用 componentDidMount,但正如我们之前约定的,在我们的例子中,它将被 useEffect Hook 所取代。所以,让我们导入 useEffect Hook 并设置上下文吧!
import React, { useState, useEffect } from "react";
import "./Board.css";
function Board() {
const canvasRef = React.useRef(null);
const [ctx, setCtx] = useState({});
useEffect(() => {
let canv = canvasRef.current;
let canvCtx = canv.getContext("2d");
canvCtx.lineJoin = "round";
canvCtx.lineCap = "round";
canvCtx.lineWidth = 5;
setCtx(canvCtx);
}, [ctx]);
return (
<div className="board">
<canvas ref={canvasRef} />
</div>
);
}
export default Board;
我给上下文添加了一些基本设置,ctx并将 useEffect 的第二个参数添加进去,以便仅在更改时触发它ctx,避免进入设置其值的无限循环。
太好了!现在我们需要处理一下我们将要使用的事件。
我们需要处理三件大事:
- onMouseDown 事件触发时,点击鼠标开始绘制。
- onMouseMove 事件会在绘制过程中移动鼠标时触发。
- onMouseUp 事件会在鼠标离开时停止绘制。
让我们把这些事件添加到画布元素中。
<canvas
ref={canvasRef}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
/>
鼠标按下
对于此事件,我们需要一个标志来跟踪绘图过程是否已开始,并为其赋予一个初始状态。false
const [drawing, setDrawing] = useState(false);
在我们的函数中,我们只需将其设置为 true 即可。
function handleMouseDown() {
setDrawing(true);
}
鼠标抬起
在这个函数中,我们将执行与 handleMouseDown 函数中完全相反的操作。
function handleMouseUp() {
setDrawing(false);
}
处理鼠标移动
这是我们处理绘图的主要函数。我们需要移动到上次检测到的鼠标位置,并从该点绘制一条线到当前鼠标位置。
首先,我们要记录前一个位置,起始值为 (0,0)。
const [position, setPosition] = useState({ x: 0, y: 0 });
我们还需要记录画布的偏移量。在本例中,画布位于窗口的左上角,但之后我们可能需要添加其他元素或一些 CSS 代码来改变它的位置。
const [canvasOffset, setCanvasOffset] = useState({ x: 0, y: 0 });
为了保证鼠标位置能够带来预期的结果,我们在设置上下文时会记录画布的左侧和顶部偏移量。
useEffect(() => {
let canv = canvasRef.current;
let canvCtx = canv.getContext("2d");
canvCtx.lineJoin = "round";
canvCtx.lineCap = "round";
canvCtx.lineWidth = 5;
setCtx(canvCtx);
let offset = canv.getBoundingClientRect();
setCanvasOffset({ x: parseInt(offset.left), y: parseInt(offset.top) });
}, [ctx]);
之后,我们可以通过从鼠标位置减去该偏移量来轻松检测位置。现在,我们有了先前的位置和当前的位置。在开始绘制路径之前,我们只需要检查绘制标志以确保绘制过程正在进行,完成后,我们将设置下一个笔画的位置。
function handleMouseMove(e) {
let mousex = e.clientX - canvasOffset.x;
let mousey = e.clientY - canvasOffset.y;
if (drawing) {
ctx.strokeStyle = "#000000";
ctx.beginPath();
ctx.moveTo(position.x, position.y);
ctx.lineTo(mousex, mousey);
ctx.stroke();
}
setPosition({ x: mousex, y: mousey });
}
此外,我们需要在鼠标点击后设置位置,以便在下一次笔画绘制时移动到该位置,因此我们需要修改 handleMouseDown 函数。
function handleMouseDown(e) {
setDrawing(true);
setPosition({
x: parseInt(e.clientX - canvasOffset.x),
y: parseInt(e.clientY - canvasOffset.y),
});
}
太棒了!现在,让我们给 App.css 添加一些 CSS 代码。
* {
box-sizing: border-box;
}
html,
body,
#root {
width: 100%;
height: 100%;
}
.app {
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
}
还有我们的 Board.css 文件。
.board {
background-color: white;
cursor: crosshair;
margin: 0 auto;
position: relative;
width: 100%;
overflow: hidden;
flex: auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
这一切都很棒,我可以在白板上画画,但还有一个问题一直困扰着我。我不常使用 Canvas,所以花了不少时间琢磨为什么线条看起来像素化,也让我意识到自己有多么热爱后端开发。我发现这是因为我用 CSS 设置了 Canvas 的高度,不知怎么的,这导致了问题。我应该直接将窗口的内部宽度和内部高度动态地赋值给 Canvas 的宽度和高度属性,或者赋值给父元素的偏移宽度和偏移高度。
为了实现这一点,让我们为 canvas 父元素添加一个新的引用,以便能够访问其偏移宽度和高度!
const parentRef = React.useRef(null);
我们还应该将其添加到父元素中。
return (
<div className="board" ref={parentRef}>
<canvas
ref={canvasRef}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
/>
</div>
);
我们可以在设置上下文之前直接指定宽度和高度。
useEffect(() => {
let canv = canvasRef.current;
canv.width = parentRef.current.offsetWidth;
canv.height = parentRef.current.offsetHeight;
let canvCtx = canv.getContext("2d");
canvCtx.lineJoin = "round";
canvCtx.lineCap = "round";
canvCtx.lineWidth = 5;
setCtx(canvCtx);
let offset = canv.getBoundingClientRect();
setCanvasOffset({ x: parseInt(offset.left), y: parseInt(offset.top) });
}, [ctx]);
太棒了!现在我们可以在白板上自由绘画了!
控制
是时候让我们的白板更进一步,添加控件组件了。它只需要几个按钮,所以我把它设计成置于画布之上。
在 Controls 组件中,我们只需添加一个简单的结构来容纳我们的按钮。
import React from "react";
import "./Controls.css";
function Controls() {
return <div className="controls"></div>;
}
export default Controls;
然后在 Controls.css 文件中添加一些 CSS 代码,将其定位到画布上。
.controls {
position: absolute;
top: 0;
display: flex;
justify-content: center;
width: auto;
}
颜色
接下来我们来看颜色组件!我们需要一个颜色选择器。我选择了react-color包,可以通过运行以下命令安装:
npm install react-color --save
顺便一提,我还想给控件添加图标,所以我们可以通过运行以下命令来安装react-fontawesome包:
npm i --save @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/react-fontawesome
我们先导入 Font Awesome 并添加颜色图标!
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPalette } from "@fortawesome/free-solid-svg-icons";
function Color() {
return (
<div className="color">
<FontAwesomeIcon
title="choose color"
className="fa-icon"
icon={faPalette}
/>
</div>
);
}
export default Color;
现在,我们需要添加颜色选择器。我喜欢 ChromePicker 的外观,所以我会导入它。
我只希望在点击调色板图标后才弹出选择器,因此我需要添加一个标志来检测它是否被点击,一些自定义 CSS 并处理点击事件。
import React, { useState } from "react";
import { ChromePicker } from "react-color";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPalette } from "@fortawesome/free-solid-svg-icons";
function Color(props) {
const popover = {
position: "absolute",
zIndex: "2",
};
const cover = {
position: "fixed",
top: "0px",
right: "0px",
bottom: "0px",
left: "0px",
};
const [displayed, setDisplayed] = useState(false);
function handleClick() {
setDisplayed(true);
}
function handleClose() {
setDisplayed(false);
}
return (
<div className="color">
<FontAwesomeIcon
onClick={handleClick}
title="choose color"
className="fa-icon"
icon={faPalette}
/>
{displayed ? (
<div style={popover}>
<div style={cover} onClick={handleClose} />
<ChromePicker />
</div>
) : null}
</div>
);
}
export default Color;
很好!现在让我们把颜色组件添加到控件组件中。
import React from "react";
import "./Controls.css";
import Color from "../Color/Color";
function Controls() {
return <div className="controls">
<Color />
</div>;
}
export default Controls;
我们将控制组件与电路板组件进行交互,以查看我们取得了多大的进展。
return (
<div className="board" ref={parentRef}>
<Controls />
<canvas
ref={canvasRef}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
/>
</div>
);
好了,现在我们需要在 Board 组件中添加另一个功能。我们还没有处理从颜色选择器中选择的颜色如何在画板上体现出来。
让我们使用 Hooks 来跟踪颜色值,并将其默认值设置为黑色。
const [color, setColor] = useState("#000000");
现在让我们修改 handleMouseMove 函数,将 strokeStyle 设置为颜色状态!
function handleMouseMove(e) {
let mousex = e.clientX - canvasOffset.x;
let mousey = e.clientY - canvasOffset.y;
if (drawing) {
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(position.x, position.y);
ctx.lineTo(mousex, mousey);
ctx.stroke();
}
setPosition({ x: mousex, y: mousey });
}
还有一件事,我们希望颜色选择器更改时颜色状态也能更新,所以我们将添加另一个函数来处理这种情况,并将其作为 prop 发送到我们的 Controls 组件,然后再从那里将其作为 prop 发送到 Color 组件。
function handleColor(color) {
setColor(color);
}
return (
<div className="board" ref={parentRef}>
<Controls handleColor={handleColor} />
<canvas
ref={canvasRef}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
/>
</div>
);
在 Controls 组件中,让我们将该属性传递给 Color 组件!
function Controls(props) {
return <div className="controls">
<Color handleColor={props.handleColor} />
</div>;
}
现在,让我们回到 Color 组件,并添加一个状态来跟踪颜色变化!
const [color, setColor] = useState("#000000");
之后,我们可以使用属性来处理颜色选择器的变化。我们需要的是传递给 handleChange 函数的参数中包含的颜色十六进制值。
function handleChange(pickerColor) {
setColor(pickerColor.hex);
props.handleColor(pickerColor.hex);
}
我们还希望使用选定的颜色更新颜色选择器本身。
<ChromePicker color={color} onChange={handleChange} />
完美!现在,颜色显示正常了!让我们在 Controls.css 文件中添加一些 CSS 样式,让按钮看起来更漂亮。
.controls .fa-icon {
cursor: pointer;
font-size: 3rem;
margin: 0.5rem;
padding: 0.5rem;
border-radius: 30%;
box-shadow: 0 0 6px black;
z-index: 2;
color: #071a54;
background: linear-gradient(
90deg,
rgba(174, 238, 237, 1) 0%,
rgba(181, 23, 23, 1) 100%
);
}
橡皮
我们的工作快完成了,现在只需要能用橡皮擦了。我打算作弊一下,直接把颜色改成白色。我们当然可以用原ctx.globalCompositeOperation = 'destination-out';方法,但改成白色就能解决问题。
我们的组件看起来会是这样。
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faEraser } from "@fortawesome/free-solid-svg-icons";
function Eraser(props) {
function handleEraser(e) {
e.preventDefault();
props.handleColor("#ffffff");
}
return (
<div className="eraser">
<FontAwesomeIcon
title="erase"
icon={faEraser}
className="fa-icon"
onClick={handleEraser}
/>
</div>
);
}
export default Eraser;
在我们的 Controls 组件中,我们将传递与传递给 Color 组件相同的 prop,以便在绘制时使其反映在我们的面板上。
import React from "react";
import "./Controls.css";
import Color from "../Color/Color";
import Eraser from "../Eraser/Eraser";
function Controls(props) {
return (
<div className="controls">
<Color handleColor={props.handleColor} />
<Eraser handleColor={props.handleColor} />
</div>
);
}
export default Controls;
这就是我们的功能齐全的白板!
代码可以在这里找到
借着这块迷你白板,我将结束我迈向 React 伟大之路的第五小步,期待我们下次再见。
任何反馈或建议都非常欢迎。可以通过这里、推特、其他任何方式联系我!
文章来源:https://dev.to/ranaemad/whiteboard-react-hooks-2pc9