3

在Canvas中使用React Hooks

 2 years ago
source link: https://www.joynop.com/p/434.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在Canvas中使用React Hooks

在本文中,我将使用React Hooks创建一个html canvas 画图网站,我将使用create-react-app脚手架从零开始构建项目。最后这个应用程序有诸如清除、撤销和使用localStorage基本功能。

本文我将向您展示任何构建自定义Hooks和在普通的Hooks中重用有状态逻辑。

我们首先使用create-react-app创建一个新的React应用程序。

$ npx create-react-app canvas-and-hooks
$ <span>cd</span> canvas-and-hooks/
$ yarn start
<span class="copy-code-btn">复制代码</span>

您的浏览器会打开 http://localhost:3000/,然后您会看到一个旋转的React logo图片,那么,您现在可以开始了...

第一个hook:useRef

用您喜欢的编辑器打开 src/App.js文件📃,然后替换成以下内容:

import React from <span>'react'</span>
<span>function</span> <span><span>App</span></span>() {
  <span>return</span> (
    <canvas width="{window.innerWidth}" height="{window.innerHeight}" onclick="{e" => {
        alert(e.clientX)
      }}
    />
  )
}
<span>export</span> default App
<span class="copy-code-btn">复制代码</span></canvas>
JavaScript

在浏览器窗口中点击任意一处,如果会弹出一个弹出框:显示您鼠标🖱️点击的x坐标,很好!应用程序跑起来了。

现在,我们真正的画一些东西。这样的话我们就需要canvas 元素的ref,所以,开始使用今天的第一个hook useRef吧:

import React from <span>'react'</span>
<span>function</span> <span><span>App</span></span>() {
  const canvasRef = React.useRef(null)
  <span>return</span> (
    <canvas ref="{canvasRef}" width="{window.innerWidth}" height="{window.innerHeight}" onclick="{e" => {
         const canvas = canvasRef.current
         const ctx = canvas.getContext(<span>'2d'</span>)
         // implement draw on ctx here
      }}
    />
  )
}
<span>export</span> default App
<span class="copy-code-btn">复制代码</span></canvas>
JavaScript

通常,在React中你不需要一个ref来做更新的操作。但是canvas不像其它的DOM元素。大多数DOM元素都有一个属性,比如说:value,你可以直接更新它。在canvas中允许✅您使用context(本🌰:ctx)来画一些东西。为此,我们不得不使用ref,它是对实际canvas DOM元素的引用。

现在我们有了canvas上下文,是时候画一些东西了。为此,粘贴复制以下代码绘制一个SVG hook。它与hooks无关,如果您不理解它也不需要担心😓。

import React from <span>'react'</span>
const HOOK_SVG =  <span>'m129.03125 63.3125c0-34.914062-28.941406-63.3125-64.519531-63.3125-35.574219 0-64.511719 28.398438-64.511719 63.3125 0 29.488281 20.671875 54.246094 48.511719 61.261719v162.898437c0 53.222656 44.222656 96.527344 98.585937 96.527344h10.316406c54.363282 0 98.585938-43.304688 98.585938-96.527344v-95.640625c0-7.070312-4.640625-13.304687-11.414062-15.328125-6.769532-2.015625-14.082032.625-17.960938 6.535156l-42.328125 64.425782c-4.847656 7.390625-2.800781 17.3125 4.582031 22.167968 7.386719 4.832032 17.304688 2.792969 22.160156-4.585937l12.960938-19.71875v42.144531c0 35.582032-29.863281 64.527344-66.585938 64.527344h-10.316406c-36.714844 0-66.585937-28.945312-66.585937-64.527344v-162.898437c27.847656-7.015625 48.519531-31.773438 48.519531-61.261719zm-97.03125 0c0-17.265625 14.585938-31.3125 32.511719-31.3125 17.929687 0 32.511719 14.046875 32.511719 31.3125 0 17.261719-14.582032 31.3125-32.511719 31.3125-17.925781 0-32.511719-14.050781-32.511719-31.3125zm0 0'</span>
const HOOK_PATH = new Path2D(HOOK_SVG)
const SCALE = 0.3
const OFFSET = 80
<span>function</span> draw(ctx, location) {
  ctx.fillStyle = <span>'deepskyblue'</span>
  ctx.shadowColor = <span>'dodgerblue'</span>
  ctx.shadowBlur = 20  ctx.save()
  ctx.scale(SCALE, SCALE)  ctx.translate(location.x / SCALE - OFFSET, location.y / SCALE - OFFSET)
  ctx.fill(HOOK_PATH)
  ctx.restore()
}
<span>function</span> <span><span>App</span></span>() {
  const canvasRef = React.useRef(null)
  <span>return</span> (
    <canvas ref="{canvasRef}" width="{window.innerWidth}" height="{window.innerHeight}" onclick="{e" => {
        const canvas = canvasRef.current
        const ctx = canvas.getContext(<span>'2d'</span>)
        draw(ctx, { x: e.clientX, y: e.clientY })
      }}
    />
  )
}
<span>export</span> default App
<span class="copy-code-btn">复制代码</span></canvas>
JavaScript

上面的代码是为了在坐标(x,y)绘制一个SVG形状(一个鱼钩)。

试一试,看看它是否起作用。

第二个hook:useState

我们要添加的下一个功能是Clean和Undo按钮🔘。为此,我们将使用useState hook来跟踪用户交互。

import React from <span>'react'</span>
// ...

// canvas draw <span>function</span>
// ...

<span>function</span> <span><span>App</span></span>() {
  const [locations, <span>set</span>Locations] = React.useState([])
  const canvasRef = React.useRef(null)
  <span>return</span> (
    <canvas ref="{canvasRef}" width="{window.innerWidth}" height="{window.innerHeight}" onclick="{e" => {
        const canvas = canvasRef.current
        const ctx = canvas.getContext(<span>'2d'</span>)
        const newLocation = { x: e.clientX, y: e.clientY }
        <span>set</span>Locations([...locations, newLocation])
        draw(ctx, newLocation)
      }}
    />
  )
}
<span>export</span> default App
<span class="copy-code-btn">复制代码</span></canvas>
JavaScript

所以,我们为app添加了state。您可以在return语句上面添加 console.log(locations)来验证一下。随着用户点击,您会看到打印的数组。

第三个hook:useEffect

目前,我们对state没有任何操作。我们还是像以前一样绘制了hooks。我们来看看用useEffect hook如何修复这个问题。

import React from <span>'react'</span>
// ...

// canvas draw <span>function</span>
// ...

<span>function</span> <span><span>App</span></span>() {
  const [locations, <span>set</span>Locations] = React.useState([])
  const canvasRef = React.useRef(null)
  React.useEffect(() => {
    const canvas = canvasRef.current
    const ctx = canvas.getContext(<span>'2d'</span>)
    ctx.clearRect(0, 0, window.innerHeight, window.innerWidth)
    locations.forEach(location => draw(ctx, location))
  })
  <span>return</span> (
    <canvas ref="{canvasRef}" width="{window.innerWidth}" height="{window.innerHeight}" onclick="{e" => {
        const newLocation = { x: e.clientX, y: e.clientY }
        <span>set</span>Locations([...locations, newLocation])
      }}
    />
  )
}
<span>export</span> default App
<span class="copy-code-btn">复制代码</span></canvas>
JavaScript

这里做了很多事情我们来一一拆解一下。我们把onClick事件处理函数的绘制函数移动到useEffect回掉里。这很重要,因为在画布上绘制由app的状态决定,这是个副作用。后面我们会使用localStorage来保持持久化,在state更新的时候这也会是个副作用。

我也对canvas本身的实际绘制做了一些更改,在当前实现中,每次render渲染先清除canvas然后再绘制所有位置,我们可以做的比这聪明一点。但为了保持简单,就留给读者去优化吧。

我们已经完成了所有最难的部分,现在添加新功能应该很简单了。我们来创建清除按钮吧。

import React from <span>'react'</span>
// ...

// canvas draw <span>function</span>
// ...

<span>function</span> <span><span>App</span></span>() {
  const [locations, <span>set</span>Locations] = React.useState([])
  const canvasRef = React.useRef(null)
  React.useEffect(() => {
    const canvas = canvasRef.current
    const ctx = canvas.getContext(<span>'2d'</span>)
    ctx.clearRect(0, 0, window.innerHeight, window.innerWidth)
    locations.forEach(location => draw(ctx, location))
  })
  <span>function</span> handleCanvasClick(e) {
    const newLocation = { x: e.clientX, y: e.clientY }
    <span>set</span>Locations([...locations, newLocation])
  }
  <span>function</span> <span><span>handleClear</span></span>() {
    <span>set</span>Locations([])
  }
  <span>return</span> (
    <>
      <button onclick="{handleClear}">Clear</button>
      <canvas ref="{canvasRef}" width="{window.innerWidth}" height="{window.innerHeight}" onclick="{handleCanvasClick}">
    </>
  )
}
<span>export</span> default App
<span class="copy-code-btn">复制代码</span></canvas>
JavaScript

清除功能只是一个简单的state更新:我们通过设置它为一个空数组来清除state,这很简单,对吗?

进一步,我也把canvas onClick事件处理移动到一个单独的函数里。

我们来添加另外一个功能:撤销。同样的原则,即使这种状态更新有点棘手。

import React from <span>'react'</span>
// ...

// canvas draw <span>function</span>
// ...

<span>function</span> <span><span>App</span></span>() {
  const [locations, <span>set</span>Locations] = React.useState([])
  const canvasRef = React.useRef(null)
  React.useEffect(() => {
    const canvas = canvasRef.current
    const ctx = canvas.getContext(<span>'2d'</span>)
    ctx.clearRect(0, 0, window.innerHeight, window.innerWidth)
    locations.forEach(location => draw(ctx, location))
  })
  <span>function</span> handleCanvasClick(e) {
    const newLocation = { x: e.clientX, y: e.clientY }
    <span>set</span>Locations([...locations, newLocation])
  }
  <span>function</span> <span><span>handleClear</span></span>() {
    <span>set</span>Locations([])
  }
  <span>function</span> <span><span>handleUndo</span></span>() {
    <span>set</span>Locations(locations.slice(0, -1))
  }
  <span>return</span> (
    <>
      <button onclick="{handleClear}">Clear</button>
      <button onclick="{handleUndo}">Undo</button>
      <canvas ref="{canvasRef}" width="{window.innerWidth}" height="{window.innerHeight}" onclick="{handleCanvasClick}">
    </>
  )
}
<span>export</span> default App
<span class="copy-code-btn">复制代码</span></canvas>
JavaScript

因为React中任何state更新都必须是不可变的,所以我们不能使用像 locations.pop()来清除数组中最近的一项。我们的操作不能改变原始的locations数组。方法是使用slice,复制所有项直到最后一个。你可以使用 locations.slice(0, locations.length - 1),但是slice有个更聪明的操作数组最后一位的-1。

在我们开始之前,我们整理一下html,然后添加一个css样式文件。在buttons按钮外面添加如下的div。

import React from <span>'react'</span>
import <span>'./App.css'</span>
// ...

// canvas draw <span>function</span>
// ...

<span>function</span> <span><span>App</span></span>() {
  // ...

  <span>return</span> (
    <>
      <div classname="<span">"controls">
        <button onclick="{handleClear}">Clear</button>
        <button onclick="{handleUndo}">Undo</button>
      </div>
      <canvas ref="{canvasRef}" width="{window.innerWidth}" height="{window.innerHeight}" onclick="{handleCanvasClick}">
    </>
  )
}
<span>export</span> default App
<span class="copy-code-btn">复制代码</span></canvas>
JavaScript

css样式如下:

*,
*:before,
*:after {
  box-sizing: border-box;
}
body {
  background-color: black;
}
.controls {
  position: absolute;
  top: 0;
  left: 0;
}
button {
  height: 3em;
  width: 6em;
  margin: 1em;
  font-weight: bold;
  font-size: 0.5em;
  text-transform: uppercase;
  cursor: pointer;
  color: white;
  border: 1px solid white;
  background-color: black;
}
button:hover {
  color: black;
  background-color: <span>#00baff;</span>
}
button:focus {
  border: 1px solid <span>#00baff;</span>
}
button:active {
  background-color: <span>#1f1f1f;</span>
  color: white;
}

<span class="copy-code-btn">复制代码</span>

看起来不错,我们来看看下一个功能:持久化。

添加localStorage

我们之前提过,我们也想要我们的绘制保存在localStroage中,这也是另外一个副作用,我们将添加另外一个useEffect。

import React from <span>'react'</span>
import <span>'./App.css'</span>
// ...draw <span>function</span>
<span>function</span> <span><span>App</span></span>() {
  const [locations, <span>set</span>Locations] = React.useState(
    JSON.parse(<span>local</span>Storage.getItem(<span>'draw-app'</span>)) || []  )
  const canvasRef = React.useRef(null)
  React.useEffect(() => {
    const canvas = canvasRef.current
    const ctx = canvas.getContext(<span>'2d'</span>)
    ctx.clearRect(0, 0, window.innerHeight, window.innerWidth)
    locations.forEach(location => draw(ctx, location))
  })
  React.useEffect(() => {
    <span>local</span>Storage.setItem(<span>'draw-app'</span>, JSON.stringify(locations))
  })
  <span>function</span> handleCanvasClick(e) {
    const newLocation = { x: e.clientX, y: e.clientY }
    <span>set</span>Locations([...locations, newLocation])
  }
  <span>function</span> <span><span>handleClear</span></span>() {
    <span>set</span>Locations([])
  }
  <span>function</span> <span><span>handleUndo</span></span>() {
    <span>set</span>Locations(locations.slice(0, -1))
  }
  <span>return</span> (
    <>
      <div classname="<span">"controls">
        <button onclick="{handleClear}">Clear</button>
        <button onclick="{handleUndo}">Undo</button>
      </div>
      <canvas ref="{canvasRef}" width="{window.innerWidth}" height="{window.innerHeight}" onclick="{handleCanvasClick}">
    </>
  )
}
<span>export</span> default App
<span class="copy-code-btn">复制代码</span></canvas>
JavaScript

现在我们已经完成了我们要构建的所有功能,但还不够。 关于books最酷的一件事是您可以使用现有的hooks来组建新的自定义hooks。我创建一个自定义的usePersistentState hook来展示这一点。

第一个自定义hook:usePersistentState

import React from <span>'react'</span>
import <span>'./App.css'</span>
// ...draw <span>function</span>
// our first custom hook!

<span>function</span> usePersistentState(init) {
  const [value, <span>set</span>Value] = React.useState(
    JSON.parse(<span>local</span>Storage.getItem(<span>'draw-app'</span>)) || init
  )
  React.useEffect(() => {
    <span>local</span>Storage.setItem(<span>'draw-app'</span>, JSON.stringify(value))
  })
  <span>return</span> [value, <span>set</span>Value]}
<span>function</span> <span><span>App</span></span>() {
  const [locations, <span>set</span>Locations] = usePersistentState([])
  const canvasRef = React.useRef(null)
  React.useEffect(() => {
    const canvas = canvasRef.current
    const ctx = canvas.getContext(<span>'2d'</span>)
    ctx.clearRect(0, 0, window.innerHeight, window.innerWidth)
    locations.forEach(location => draw(ctx, location))
  })
  <span>function</span> handleCanvasClick(e) {
    const newLocation = { x: e.clientX, y: e.clientY }
    <span>set</span>Locations([...locations, newLocation])
  }
  <span>function</span> <span><span>handleClear</span></span>() {
    <span>set</span>Locations([])
  }
  <span>function</span> <span><span>handleUndo</span></span>() {
    <span>set</span>Locations(locations.slice(0, -1))
  }
  <span>return</span> (
    // ...

  )
}
<span>export</span> default App
<span class="copy-code-btn">复制代码</span>
JavaScript

这里,我们创建了第一个自定义hook并且从App组件中提取了与从localStorage保存和获取状态相关的所有逻辑。我们这样做的方式是usePersistentState hook可以被其它组件重用。这里没有任何特定于此组件的内容。

我们重复这个技巧来操作canvas相关的逻辑。

第二个自定义hook:usePersistentCanvas

import React from <span>'react'</span>
import <span>'./App.css'</span>
// ...draw <span>function</span>
// our first custom hook
<span>function</span> usePersistentState(init) {
  const [value, <span>set</span>Value] = React.useState(
    JSON.parse(<span>local</span>Storage.getItem(<span>'draw-app'</span>)) || init
  )
  React.useEffect(() => {
    <span>local</span>Storage.setItem(<span>'draw-app'</span>, JSON.stringify(value))
  })
  <span>return</span> [value, <span>set</span>Value]
}
// our second custom hook: a composition of the first custom hook // and React<span>'s useEffect + useRef
function usePersistentCanvas() {
  const [locations, setLocations] = usePersistentState([])

  const canvasRef = React.useRef(null)

  React.useEffect(() => {
    const canvas = canvasRef.current
    const ctx = canvas.getContext('</span>2d<span>')
    ctx.clearRect(0, 0, window.innerWidth, window.innerHeight)
    locations.forEach(location => draw(ctx, location))
  })
  return [locations, setLocations, canvasRef]
}
function App() {
  const [locations, setLocations, canvasRef] = usePersistentCanvas()
  function handleCanvasClick(e) {
    const newLocation = { x: e.clientX, y: e.clientY }
    setLocations([...locations, newLocation])
  }
  function handleClear() {
    setLocations([])
  }
  function handleUndo() {
    setLocations(locations.slice(0, -1))
  }
  return (
    <>
      <div classname="controls">
        <button onclick="{handleClear}">Clear</button>
        <button onclick="{handleUndo}">Undo</button>
      </div>
      <canvas ref="{canvasRef}" width="{window.innerWidth}" height="{window.innerHeight}" onclick="{handleCanvasClick}">
    </>
  )
}
export default App
</canvas></span><span class="copy-code-btn">复制代码</span>
JavaScript

正如您所看到的,我们的App组件变得非常小。 在localStorage中存储状态和在canvas上绘图相关的所有逻辑都被提取到自定义hooks。 您可以通过将hooks移动到hooks文件中来进一步清理此文件。 这样,其他组件可以重用这种逻辑,例如构成更好的hooks。

如果将hooks与生命周期方法(如componentDidMount,componentDidUpdate)进行比较,是什么让hooks如此特别? 看看上面的例子:

  • hooks允许你在不同的组件 重用生命周期钩子逻辑
  • 你可以 合成hooks来构建更丰富的自定义hooks,就像你可以合成更丰富的UI组件一样。
  • hooks更小更简洁,不再臃肿,生命周期方法有时很困惑。

现在判断hooks是否真的要解决所有这些问题还为时尚早 - 以及可能会出现什么新的不良做法 - 但看看上面我对React的未来感到非常兴奋和乐观!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK