【问题标题】:How can I use Fabric.js with React?如何将 Fabric.js 与 React 一起使用?
【发布时间】:2016-09-30 14:35:17
【问题描述】:

我有一个通过Fabric.js 使用大量HTML5 画布的应用程序。该应用程序是在Angular 1.x 之上编写的,我计划将其迁移到React。我的应用程序允许编写文本和绘制线条、矩形和椭圆。也可以移动、放大、缩小、选择、剪切、复制和粘贴一个或多个这样的对象。还可以使用各种快捷方式缩放和平移画布。简而言之,我的应用充分利用了 Fabric.js。

我找不到太多关于如何将 Fabric.js 与 React 一起使用的信息,所以我担心的是 1. 是否可以不进行重大修改,以及 2. 是否有意义或者我应该改用其他一些广泛的对 React 支持更好的 canvas 库?

我能找到的唯一 React+Fabric.js 示例是 react-komik,但它比我的应用程序简单得多。我主要关心的是 Fabric.js 的事件处理和 DOM 操作,以及它们对 React 的影响。

似乎还有一个用于 React 的画布库,名为 react-canvas,但与 Fabric.js 相比,它似乎缺少很多功能。

在 React 应用程序中使用 Fabric.js 时,我必须考虑什么(关于 DOM 操作、事件处理等)?

【问题讨论】:

  • 您可以查看github.com/lavrton/react-konva。我认为移植到 Fabric(而不是 Konva)应该很容易。
  • 这是个有趣的库,但不是我问的。我已经在大范围使用 Fabric.js,所以我更愿意避免切换到其他画布库或过多地篡改 Fabric.js 内部,因为这会使维护变得很麻烦。
  • react-faux-dom 可以与 d3 和 fabric 等包一起使用。

标签: reactjs fabricjs react-canvas


【解决方案1】:

我们在我们的应用程序中遇到了同样的问题,即如何在 react 中使用 Fabric.js。我的建议是将织物视为不受控制的组件。拥有整个应用程序可以与之通信的结构实例并进行结构调用,然后当发生任何变化时,使用.toObject() 调用将整个结构状态放入您的 Redux 存储中。然后,您的 React 应用程序可以像在任何普通 React 应用程序中一样从全局 Redux 状态读取结构状态。

我找不到在 StackOverflow 代码编辑器中工作的示例,但 here is a JSFiddle example 实现了我推荐的模式。

【讨论】:

  • 在每次状态更改时调用 toObject 听起来有点矫枉过正。它不会让您的应用程序变得非常缓慢吗?
  • 没有。这是一个非常快的电话。此外,当 anything 最后发生变化时,您也不会调用它。因此,如果您正在拖动一个形状,您只需在鼠标向上时调用toObject
  • 现在是否可以在 jsfiddle 或其他任何地方添加您的示例,因为它现在返回 404?谢谢。
  • 感谢您的示例。您能否详细说明在上面的示例中添加 Redux 商店(或任何用于绘图的应用程序)的优点?商店只保存了结构状态的副本,应用程序的其余部分可以从中读取,但所有更改/操作都在 fabricCanvas 上完成。 (我是这方面的新手)
【解决方案2】:

我已将 Fabric 用于概念验证项目,总体思路与 D3 等相同。请记住,Fabric 对 DOM 元素进行操作,而 React 将数据呈现到 DOM 中,通常后者是延迟的。有两件事可以帮助您确保代码正常工作:

等到组件被挂载

为此,请将您的 Fabric 实例化放入 componentDidMount

import React, { Component } from 'react';
import { fabric } from 'react-fabricjs';
import styles from './MyComponent.css';

class MyComponent extends Component {
  componentWillMount() {
    // dispatch some actions if you use Redux
  }

  componentDidMount() {
    const canvas = new fabric.Canvas('c');

    // do some stuff with it
  }

  render() {
    return (
      <div className={styles.myComponent}>
        <canvas id="c" />
      </div>
    )
  }
}

将 Fabric 构造函数放入 componentDidMount 可确保它不会失败,因为在执行此方法时,DOM 已准备就绪。 (但 props 有时不是,以防万一你使用 Redux)

使用 refs 计算实际的宽高

Refs 是对实际 DOM 元素的引用。你可以使用 refs 来做你可以使用 DOM API 来处理 DOM 元素的事情:选择子元素、查找父元素、分配样式属性、计算 innerHeightinnerWidth。后者正是您所需要的:

componentDidMount() {
  const canvas = new fabric.Canvas('c', {
    width: this.refs.canvas.clientWidth,
    height: this.refs.canvas.clientHeight
  });

  // do some stuff with it
}

不要忘记定义thisrefs 属性。为此,您需要一个构造函数。整个事情看起来像

import React, { Component } from 'react';
import { fabric } from 'react-fabricjs';
import styles from './MyComponent.css';

class MyComponent extends Component {
  constructor() {
    super()
    this.refs = {
      canvas: {}
    };
  }

  componentWillMount() {
    // dispatch some actions if you use Redux
  }

  componentDidMount() {
    const canvas = new fabric.Canvas('c', {
      width: this.refs.canvas.clientWidth,
      height: this.refs.canvas.clientHeight
    });

    // do some stuff with it
  }

  render() {
    return (
      <div className={styles.myComponent}>
        <canvas
          id="c"
          ref={node => {
            this.refs.canvas = node;
          } />
      </div>
    )
  }
}

将 Fabric 与组件状态或道具混合

您可以让您的 Fabric 实例对任何组件道具或状态更新做出反应。要使其工作,只需在componentDidUpdate 上更新您的 Fabric 实例(如您所见,您可以将其存储为组件自身属性的一部分)。仅仅依靠render 函数调用并没有真正的帮助,因为渲染的任何元素都不会随着新的道具或新的状态而改变。像这样的:

import React, { Component } from 'react';
import { fabric } from 'react-fabricjs';
import styles from './MyComponent.css';

class MyComponent extends Component {
  constructor() {
    this.refs = {
      canvas: {}
    };
  }

  componentWillMount() {
    // dispatch some actions if you use Redux
  }

  componentDidMount() {
    const canvas = new fabric.Canvas('c', {
      width: this.refs.canvas.clientWidth,
      height: this.refs.canvas.clientHeight
    });

    this.fabric = canvas;

    // do some initial stuff with it
  }

  componentDidUpdate() {
    const {
      images = []
    } = this.props;
    const {
      fabric
    } = this;

    // do some stuff as new props or state have been received aka component did update
    images.map((image, index) => {
      fabric.Image.fromURL(image.url, {
        top: 0,
        left: index * 100 // place a new image left to right, every 100px
      });
    });
  }

  render() {
    return (
      <div className={styles.myComponent}>
        <canvas
          id="c"
          ref={node => {
            this.refs.canvas = node;
          } />
      </div>
    )
  }
}

只需用您需要的代码替换图像渲染,这取决于新的组件状态或道具。不要忘记在画布上渲染新对象之前清理它!

【讨论】:

  • 你能帮我解决这个问题吗?我无法在画布中设置背景图片stackoverflow.com/questions/41581511/…
  • 看来你对 reactjs 和 fabricjs 很有经验。我需要你的帮助。我可以设置背景图像,但无法设置背景颜色。 stackoverflow.com/questions/37565041/… 。你能看看这个吗?
  • 我有点困惑这是如何工作的。我看不到react-fabricjs 在任何地方暴露fabric 对象。所以这一行:import { fabric } from 'react-fabricjs'; 为我创建了一个错误。
  • 我认为我的回答已经过时了。如果你用最新版本的 react-fabricjs 解决了这个问题,你介意编辑答案还是写一个新答案?
【解决方案3】:

使用 react 16.8 或更高版本,您还可以创建自定义挂钩:

import React, { useRef, useCallback } from 'react';
const useFabric = (onChange) => {
    const fabricRef = useRef();
    const disposeRef = useRef();
    return useCallback((node) => {
        if (node) {
            fabricRef.current = new fabric.Canvas(node);
            if (onChange) {
                disposeRef.current = onChange(fabricRef.current);
            }
        }
        else if (fabricRef.current) {
            fabricRef.current.dispose();
            if (disposeRef.current) {
                disposeRef.current();
                disposeRef.current = undefined;
            }
        }
    }, []);
};

用法

const FabricDemo = () => {
  const ref = useFabric((fabricCanvas) => {
    console.log(fabricCanvas)
  });
  return <div style={{border: '1px solid red'}}>
    <canvas ref={ref} width={300} height={200} />
  </div>
}

【讨论】:

  • 你不能这样做--- ``` const useFabric = ( opts: { width, number }, onChange ) => { const ref = useRef(null); useEffect(() => { const fabricInst = new fabric.Canvas(ref.current); onChange(fabricInst); }, [onChange, opts.height, opts.width]);返回参考; }; ```
  • 如果第一个 useEffect 运行的引用是 null,这将不起作用,因为更改引用将不会再次执行 useEffect
  • 对,但是这样使用的useEffect应该相当于componentDidMount。如果我没记错的话,到那时 ref 保证存在
  • 否,因为您可以延迟渲染 div,例如return someCondition ? &lt;div ref={ref}&gt;...&lt;/div&gt;: null - 也显示在官方文档中:reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
  • 我正在尝试使用它,但我不够聪明:当我稍后尝试访问 ref.current(来自上面的 FabricDemo)时,它始终为空。但是画布在页面上显示正常
【解决方案4】:

我在这里没有看到使用 React 的功能组件的任何答案,而且我无法让 @jantimon 的钩子起作用。这是我的方法。我让画布本身从页面布局中获取尺寸,然后在 (0,0) 处有一个“backgroundObject”作为用户的可绘制区域。

import React, { useState, useRef, useEffect } from 'react';

const { fabric } = window;

// this component takes one prop, data, which is an object with data.sizePixels, an array that represents the width,height of the "backgroundObject"

const CanvasComponent = ({ data }) => {
  const canvasContainer = useRef();
  const canvasRef = useRef();
  const fabricRef = useRef();
  const [error, setError] = useState();

  const initCanvas = ({ canvas, size }) => {
    console.log('*** canvas init');
    console.log(canvas);

    let rect = new fabric.Rect({
      width: size[0],
      height: size[1],
      fill: 'white',
      left: 0,
      top: 0,
      selectable: false,
      excludeFromExport: true,
    });
    canvas.add(rect);

    rect = new fabric.Rect({
      width: 100,
      height: 100,
      fill: 'red',
      left: 100,
      top: 100,
    });
    canvas.add(rect);
  };

  // INIT
  useEffect(() => {
    if (!canvasRef.current) return;
    if (fabricRef.current) return;
    const canvas = new fabric.Canvas('canvas', {
      width: canvasContainer.current.clientWidth,
      height: canvasContainer.current.clientHeight,
      selection: false, // disables drag-to-select
      backgroundColor: 'lightGrey',
    });
    fabricRef.current = canvas;
    initCanvas({ canvas, size: data.sizePixels });
  });

  // INPUT CHECKING
  if (!data || !Array.isArray(data.sizePixels)) setError('Sorry, we could not open this item.');

  if (error) {
    return (
      <p>{error}</p>
    );
  }

  return (
    <>
      <div style={{
        display: 'flex',
        flexDirection: 'row',
        width: '100%',
        minHeight: '60vh',
      }}
      >
        <div style={{ flex: 3, backgroundColor: 'green' }}>
          Controls
        </div>
        <div ref={canvasContainer} style={{ flex: 7, backgroundColor: 'white' }}>
          <canvas id="canvas" ref={canvasRef} style={{ border: '1px solid red' }} />
        </div>
      </div>

      <div>
        <button
          type="button"
          onClick={() => {
            const json = fabricRef.current.toDatalessJSON();
            setDebugJSON(JSON.stringify(json, null, 2));
          }}
        >
          Make JSON
        </button>
      </div>
    </>
  );
};

export default CanvasComponent;

【讨论】:

    【解决方案5】:

    还有react-fabricjs 包,它允许您使用织物对象作为反应组件。实际上,Rishat 的答案包括这个包,但我不明白它应该如何工作,因为 react-fabricjs 中没有“fabric”对象(他可能是指“fabric-webpack”包)。 简单的“Hello world”组件示例:

    import React from 'react';
    import {Canvas, Text} from 'react-fabricjs';
    
    const HelloFabric = React.createClass({
      render: function() {
        return (
          <Canvas
            width="900"
            height="900">
              <Text
                text="Hello World!"
                left={300}
                top={300}
                fill="#000000"
                fontFamily="Arial"
              />
          </Canvas>
        );
      }
    });
    
    export default HelloFabric;

    即使你不想使用这个包,探索它的代码可能会帮助你了解如何自己在 React 中实现 Fabric.js。

    【讨论】:

    • 另请参阅我对连接 fabric.js 和 redux 的问题的回答,我使用普通的 fabricjs 并将其内容保存到 redux 商店:stackoverflow.com/questions/37742465/…
    • 看起来 react-fabricjs 项目已经死了(github 链接显示 404)。
    • 评论来自 2016 年,看起来该项目从那时起并没有太多维护,但仍然可以在这里找到:github.com/neverlan/react-fabricjs 不管怎样,在 2020 年,我建议从用户 jantimon。
    【解决方案6】:

    我关注了StefanHayden 的回答,这里是测试代码。

    创建织物画布对象

    创建一个custom hook 以返回一个fabricRef(Callback Refs):

    const useFabric = () => {
      const canvas = React.createRef(null);
      const fabricRef = React.useCallback((element) => {
        if (!element) return canvas.current?.dispose();
    
        canvas.current = new fabric.Canvas(element, {backgroundColor: '#eee'});
        canvas.current.add(new fabric.Rect(
          {top: 100, left: 100, width: 100, height: 100, fill: 'red'}
        ));
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, []);
      return fabricRef;
    };
    

    注意:

    最后,创建一个画布并传递 ref:

    function App() {
      const fabricRef = useFabric();
      return <canvas ref={fabricRef} width={640} height={360}/>;
    }
    

    但是我们不能在其他地方使用fabricCanvas,我会在下一章解释。

    Codepen

    按上下文共享 Fabric Canvas 对象

    通过存储Refs mutable valueContext,我们可以在任何地方使用织物画布对象。

    首先,我们定义上下文,它以一个 ref 为值:

    const FabricContext = React.createContext();
    
    function App() {
      return (
        <FabricContext.Provider value={React.createRef()}>
          <MyToolKit />
          <MyFabric />
        </FabricContext.Provider>
      );
    }
    

    然后,我们可以在自定义钩子中使用它。我们现在不创建引用,而是在上下文中使用引用:

    const useFabric = () => {
      const canvas = React.useContext(FabricContext);
      const fabricRef = React.useCallback((element) => {
        if (!element) return canvas.current?.dispose();
    
        canvas.current = new fabric.Canvas(element, {backgroundColor: '#eee'});
        canvas.current.add(new fabric.Rect(
          {top: 100, left: 100, width: 100, height: 100, fill: 'red'}
        ));
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, []);
      return fabricRef;
    };
    
    function MyFabric() {
      const fabricRef = useFabric();
      return <canvas ref={fabricRef} width={640} height={360} />;
    }
    

    我们也可以在任何地方使用它,只有当上下文可用时:

    function MyToolKit() {
      const canvas = React.useContext(FabricContext);
      const drawRect = () => {
        canvas.current?.add(new fabric.Rect(
          {top: 100, left: 100, width: 100, height: 100, fill: 'red'}
        ));
      };
      return <button onClick={drawRect}>Draw</button>;
    }
    

    在 App 的整个生命周期中,它现在可以在任何地方使用织物画布对象。

    Codepen

    通过道具分享 Fabric Canvas 对象

    如果不想通过 Context 共享,比如全局变量,我们也可以通过 props 与 parent 共享。

    CodePen

    通过 forwardRef 共享 Fabric Canvas 对象

    如果不想通过 Context 共享,比如全局变量,我们也可以通过 forwardRef 与 parent 共享。

    CodePen

    【讨论】:

      猜你喜欢
      • 2013-02-26
      • 2022-09-29
      • 1970-01-01
      • 2018-12-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多