如何将 Fabric.js 与 React 一起使用?

Posted

技术标签:

【中文标题】如何将 Fabric.js 与 React 一起使用?【英文标题】:How can I use Fabric.js with 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 等包一起使用。 【参考方案1】:

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

我找不到在 *** 代码编辑器中工作的示例,但 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>
    )
  

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

【讨论】:

你能帮我解决这个问题吗?我无法在画布中设置背景图片***.com/questions/41581511/… 看来你对 reactjs 和 fabricjs 很有经验。我需要你的帮助。我可以设置背景图像,但无法设置背景颜色。 ***.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
        
        >
          <Text
            text="Hello World!"
            left=300
            top=300
            fill="#000000"
            fontFamily="Arial"
          />
      </Canvas>
    );
  
);

export default HelloFabric;

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

【讨论】:

另请参阅我对连接 fabric.js 和 redux 的问题的回答,我使用普通的 fabricjs 并将其内容保存到 redux 商店:***.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;
;

注意:

我们应该使用 React.useCallback(fn, []) 到 prevent double calling 的 ref 和 null。如果element为null,则表示componentWillUnmount,所以我们必须在这里dispose fabric。 请使用// eslint-disable-next-line react-hooks/exhaustive-deps 到surpress warnings,用于空数组deps 的useCallback。 fabricCanvas(fabric.Canvas)类似于useFabirc的instance variable,也是Uncontrolled Components,也就是fabric.Canvas对象,可以在你的所有应用中使用。

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

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

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

Codepen

按上下文共享 Fabric Canvas 对象

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

首先,我们定义上下文,它以一个 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

【讨论】:

以上是关于如何将 Fabric.js 与 React 一起使用?的主要内容,如果未能解决你的问题,请参考以下文章

如何将 React Context API 与 useReducer 一起使用以遵循与 Redux 类似的风格 [关闭]

如何将 react-datepicker 与 redux 表单一起使用?

如何在 React.js 中从 fabric.js 渲染 svg 字符串?

如何在fabric js中使用image.centerObject()使对象居中后获取图像坐标

如何将 Autodesk Forge 扩展与 React.js 一起使用?

如何使 React 光滑的滑块与图标一起使用?