Как я могу использовать Fabric.js с React?
У меня есть приложение, использующее в основном HTML5 canvas через Fabric.js. Приложение написано поверх Angular 1.x, и я планирую перенести его в React. Мое приложение позволяет писать текст и рисовать линии, прямоугольники и эллипсы. Также можно перемещать, увеличивать, уменьшать, выбирать, вырезать, копировать и вставлять один или несколько таких объектов. Также можно увеличивать и панорамировать холст с помощью различных ярлыков. Короче говоря, мое приложение в полной мере использует Fabric.js.
Я не смог найти много информации о том, как использовать Fabric.js вместе с React, поэтому меня беспокоит то, что: 1. возможно ли это без значительных изменений, и 2. имеет ли это смысл или я должен вместо этого использовать какую-то другую обширную библиотеку canvas, которая имеет лучшую поддержку React?
Единственным примером React+Fabric.js, который я смог найти, был Reaction-Komik, который, однако, намного проще, чем мое приложение. Моя главная задача - обработка событий и манипулирование DOM в Fabric.js, а также их влияние на React.
Кажется, есть и библиотека холста для React, называемая реагирующим холстом, но, по-видимому, ей не хватает многих функций по сравнению с Fabric.js.
Что я должен учитывать (в отношении манипулирования DOM, обработки событий и т. Д.) При использовании Fabric.js в приложении React?
8 ответов
У нас была такая же проблема в нашем приложении, как использовать Fabric.js внутри реакции. Я рекомендую рассматривать ткань как неконтролируемый компонент. Имейте экземпляр фабрики, с которым может разговаривать все ваше приложение и делать звонки фабрики, а затем, когда что-то изменяется .toObject()
позвоните, чтобы поместить все состояние матрицы в ваш магазин Redux. Тогда ваше приложение React может читать состояние фабрики из вашего глобального состояния Redux, как если бы вы это делали в любом обычном приложении React.
Я не могу получить пример работы в редакторе кода Stackru, но вот пример JSFiddle, который реализует шаблон, который я рекомендую.
Я использовал 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>
)
}
}
Размещение конструктора ткани в componentDidMount
гарантирует, что он не потерпит неудачу, потому что к моменту выполнения этого метода DOM готов. (но реквизит иногда нет, на случай, если вы используете Redux)
Используйте ссылки для расчета фактической ширины и высоты
Ссылки являются ссылками на фактические элементы DOM. С помощью ссылок вы можете сделать то же, что и с элементами DOM, используя DOM API: выбрать дочерних элементов, найти родителя, назначить свойства стиля, рассчитать innerHeight
а также innerWidth
, Последнее именно то, что вам нужно:
componentDidMount() {
const canvas = new fabric.Canvas('c', {
width: this.refs.canvas.clientWidth,
height: this.refs.canvas.clientHeight
});
// do some stuff with it
}
Не забудьте определить refs
собственностью this
, Для этого вам понадобится конструктор. Все это будет выглядеть
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
, Просто полагаясь на 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>
)
}
}
Просто замените рендеринг изображения на нужный вам код, который зависит от состояния нового компонента или реквизита. Не забудьте очистить холст, прежде чем рендерить новые объекты на нем!
В response 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>
}
Я последовал ответу StefanHayden , и вот тестовый код.
Создать объект холста ткани
Создайте собственный хук для возврата FabricRef(обратных вызовов 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, [])
чтобы предотвратить двойной вызов ref с нулевым значением. Если element имеет значение null, это означает, что componentWillUnmount , поэтому здесь мы должны разместить ткань. - Пожалуйста, используйте
// eslint-disable-next-line react-hooks/exhaustive-deps
чтобы подавить предупреждения , для useCallback с пустым массивом deps. - FabricCanvas ( ) похож на useFabirc, это также неконтролируемый компонент , который является объектом fabric.CanvasFabric.Canvas , который можно использовать во всех ваших приложениях.
Наконец, создайте холст и передайте ссылку:
function App() {
const fabricRef = useFabric();
return <canvas ref={fabricRef} width={640} height={360}/>;
}
Но мы не можем использовать FabricCanvas в другом месте, я объясню в следующей главе.
Совместное использование объекта Fabric Canvas по контексту
По контексту , в котором хранится переменную экземпляраизменяемое значение Refs , мы можем использовать объект холста ткани где угодно.
Во-первых, мы определяем контекст, который принимает ссылку как значение:
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>;
}
На протяжении всего жизненного цикла приложения теперь можно использовать объект холста ткани в любом месте.
Поделиться объектом Canvas Canvas от реквизита
Если вы не хотите делиться контекстом, например глобальными переменными, мы также можем поделиться с родителем с помощью реквизита.
Поделитесь объектом Fabric Canvas от forwardRef
Если вы не хотите делиться по контексту, как глобальные переменные, мы также можем поделиться с родителем по forwardRef.
Я не вижу здесь никаких ответов с использованием функциональных компонентов React, и я не мог заставить работать хук от @jantimon. Вот мой подход. У меня есть размеры холста, взятые из макета страницы, а затем есть "backgroundObject" в точке (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;
Существует также пакет response-fabricjs, который позволяет использовать тканевые объекты в качестве реагирующих компонентов. На самом деле, ответ Ришата включает в себя этот пакет, но я не понимаю, как он должен работать, так как нет никакого объекта "fabric" в реагирующем устройстве (он, вероятно, имел в виду пакет "фабричный материал"). Пример простого компонента "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;
Даже если вы не хотите использовать этот пакет, изучение его кода может помочь вам понять, как реализовать Fabric.js в React самостоятельно.
Я создал редактор формы реакции для аналогичного варианта использования, в котором Fabric.js имел все необходимые мне функции, но было больно поддерживать синхронизацию с моим кодом React/Redux. У него не так много функций, как у Fabric, но он может быть хорошей заменой для некоторых случаев использования.
Упс!
Если вы пришли сюда в ожидании ответов о Microsoft Fabric, вы попали не туда. Вы должны искать офис-UI-ткань.
(Нерелевантный и слегка смущающий оригинальный ответ удален.)