如何在React中使用Fabric.js?

58

我有一个使用Fabric.js和HTML5画布紧密结合的应用程序。这个应用程序是基于Angular 1.x编写的,我计划将其迁移到React。我的应用程序允许编写文字和绘制直线、矩形和椭圆形。还可以移动、放大、缩小、选择、剪切、复制和粘贴一个或多个此类对象。还可以使用各种快捷键缩放和平移画布。简而言之,我的应用程序充分利用了Fabric.js。

我没有找到关于如何在React中使用Fabric.js的详细信息,因此我的担心是:1.是否需要进行重大修改才能实现,2.它是否可行,或者我应该使用其他更好支持React的广泛画布库?

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

还有一个针对React的画布库叫做react-canvas,但与Fabric.js相比,它似乎缺少很多功能。

当在React应用程序中使用Fabric.js时,我需要考虑哪些问题(例如DOM操作、事件处理等)?


1
你可以查看 https://github.com/lavrton/react-konva。我认为将其移植到 Fabric(而不是 Konva)应该非常容易。 - lavrton
这是一个有趣的库,但不是我所要求的。我已经在很大程度上使用了Fabric.js,因此我更喜欢避免切换到其他画布库或过多地干扰Fabric.js内部,因为这会使维护变得麻烦。 - Kitanotori
1
react-faux-dom可以与d3和fabric等包一起使用。 - vijayst
6个回答

61

我们的应用程序也遇到了如何在React中使用Fabric.js的同样问题。我的建议是将fabric视为不受控制的组件,创建一个fabric实例,使整个应用程序都可以访问并调用fabric方法,当任何更改发生时,使用 .toObject() 方法将整个fabric状态存储到Redux store中。然后,您的React应用程序可以从全局的Redux状态中读取fabric状态,就像在任何普通的React应用程序中一样。

我无法在StackOverflow代码编辑器中运行示例,但这里有一个JSFiddle示例 ,实现了我推荐的模式。


每次状态改变都调用 toObject 似乎有些过度了。这样做会让你的应用程序变得非常缓慢,不是吗? - Kitanotori
不,这是一个非常快速的调用。而且你不会在任何变化时调用它,只有在最后才会调用。因此,如果你正在拖动一个形状,你只需要在鼠标松开时调用toObject - StefanHayden
1
能否将您的示例添加到jsfiddle或其他任何地方,因为现在它返回404错误?谢谢。 - Radomír Laučík
谢谢提供示例。您能否详细说明在上述示例(或任何用于绘制的应用程序)中添加Redux store的优点?Store仅保存fabric状态的副本,其余应用程序可以从中读取,但所有更改/操作都在fabricCanvas上完成。(我是一个新手) - markussvensson

23

我曾在一个概念验证项目中使用过 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 已准备就绪。(但有时属性可能尚未准备好,如果您使用 Redux,则需要考虑这一点)

使用 refs 计算实际宽度和高度

Refs 是指向实际 DOM 元素的引用。您可以像使用 DOM API 一样使用 refs:选择子元素、查找父元素、分配样式属性、计算 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>
    )
  }
}

只需要用新组件状态或属性所需的代码替换图像渲染即可。在渲染新对象之前,不要忘记清理画布。


你能帮我解决一下吗?我无法在画布中设置背景图像。http://stackoverflow.com/questions/41581511/selected-image-is-not-shown-on-canvas - Serenity
看起来你对ReactJS和FabricJS很有经验。我需要你的帮助。我可以设置背景图片,但无法设置背景颜色。https://dev59.com/xVoU5IYBdhLWcg3wV14Z。你能看一下吗? - pythonBeginner
我有点困惑这是如何工作的。我无法看到 react-fabricjs 在任何地方公开 fabric 对象。所以这一行:import { fabric } from 'react-fabricjs'; 对我来说会产生错误。 - Alex
我认为我的答案已经过时了。如果您使用最新版本的react-fabricjs解决了问题,您介意编辑答案或编写一个新的吗? - rishat

10

使用 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]); return ref; }; - U Avalos
如果在第一次 useEffect 运行时引用为 null,那么这将无法工作,因为更改引用不会再次执行 useEffect - jantimon
正确,但是这样使用useEffect应该等同于componentDidMount。如果我没记错的话,那么引用(ref)在那时应该是存在的。 - U Avalos
不是因为你可以懒惰地呈现一个div,例如 return someCondition ? <div ref={ref}>...</div>: null - 官方文档中也显示了这一点: https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node - jantimon
我正在尝试使用这个,但我不够聪明:当我稍后尝试访问ref.current(来自上面的FabricDemo)时,它总是null。但画布在页面上显示正常。 - xaphod
谢谢jantimon,一个超级优雅的答案。我向你致敬。 :-) - Charlie Benger-Stevenson

7
我按照StefanHayden的回答进行了操作,下面是测试代码。

创建Fabric Canvas对象

创建一个自定义钩子来返回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;
};

请注意:

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

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

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

Codepen

通过上下文共享Fabric Canvas对象

通过存储Refs可变值上下文,我们可以在任何地方使用fabric canvas对象。

首先,我们定义上下文,它以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的整个生命周期中,现在可以在任何地方使用fabric画布对象。 Codepen

通过props共享Fabric Canvas Object

如果不想通过上下文(如全局变量)共享,我们也可以通过props与父组件共享。 CodePen

通过forwardRef共享Fabric Canvas Object

如果不想通过上下文(如全局变量)共享,我们也可以通过forwardRef与父组件共享。 CodePen

这个回答太棒了。 - kyun
你真的帮了我很多。非常感谢这些清晰易懂的指导。我在过去的两天里一直在纠结如何让这个在NextJS中正常运行。 - undefined

3

我在这里没有看到使用React函数式组件的任何答案,而且我也无法使来自@jantimon的钩子正常工作。这是我的方法。我让画布本身从页面布局中获取其尺寸,然后在(0,0)设置一个“背景对象”,该对象充当用户可绘制区域。

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;


1

还有react-fabricjs包,可以让您将fabric对象用作React组件。实际上,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 store中:https://dev59.com/Kpfga4cB1Zd3GeqPCeWE#40021187 - Radomír Laučík
看起来 react-fabricjs 项目已经无人维护了(GitHub 链接显示 404)。 - ncrypticus
1
这条评论来自2016年,看起来该项目自那以后并没有得到太多的维护,但仍然可以在此处找到:https://github.com/neverlan/react-fabricjs 无论如何,在2020年,我建议探索上面用户jantimon的答案。 - Radomír Laučík

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接