1

I am trying to write shared code (that runs on both server and client) that uses an HTML canvas.

On the client, this should work perfectly fine. On the server, Node doesn't have a canvas (or a DOM), so I'd like to use the node-canvas plugin: https://github.com/Automattic/node-canvas.

However, I can't work out a way to access it that doesn't make webpack try to bundle node-canvas into my client-side code (which crashes webpack). Is there any way of loading node-canvas in such a way that I can reference it with the same code I'll use in the browser and without making webpack crash horribly?


My current effort, which did not work:

canvas.server.js

import Canvas from 'canvas';

const createCanvas = (width, height) => new Canvas(width, height);

export default createCanvas;

canvas.client.js

const createCanvas = (width, height) => {
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;

  return canvas;
};

export default createCanvas;

canvas.js

let createCanvas;

if (typeof document === 'undefined') {
  // SERVER/node
  createCanvas = require('./canvas.server.js');
} else {
  // BROWSER
  createCanvas = require('./canvas.client.js');
}

export default createCanvas;

in use:

import createCanvas from './canvas';

const canvasElement = createCanvas(width, height);
const ctx = canvasElement.getContext('2d');

Unfortunately, webpack still bundles in node-canvas.

futuraprime
  • 5,468
  • 7
  • 38
  • 58
  • In case someone's wondering: on both server and client, the canvas dumps its contents into a data URL which is rendered on the page as an inside an SVG. This is all *extremely* hacky and weird and probably won't work anyway, but I'd like to try it and this is the step I'm stuck on. – futuraprime May 05 '17 at 23:50
  • I can not understand why you need two identical copies of the same canvas rendered. You are better off keeping an abstracted copy of the canvas at the server, then when the image is needed (other clients), send the abstract and let each client render to the image. All the node.js canvas APIs are software rendering and SLOW! you will bog down your server very quickly shuffling pixels that clients handle effortlessly via the GPU. – Blindman67 May 06 '17 at 02:11
  • I have no server process. The canvas is rendered once only, at build time when the entire app is reduced to a flat file, and then recreated on the client when the user moves away from the initial view. – futuraprime May 06 '17 at 07:42

1 Answers1

0

Did you try requiring node-canvas only when the code is running in node?

If you do not actually call the require in front-end code, webpack will not bundle it. This means calling the actual require inside aforementioned conditional statement and not at the top of your file. This is important. Also, verify that you did not put node-canvas as an entry point in webpack.

Example:

// let's assume there is `isNode` variable as described in linked answer
let canvas;
if (isNode) {
  const Canvas = require('canvas');
  canvas = new Canvas(x, y);
else {
  canvas = document.getElementById('canvas');
}
// get canvas context and draw on it

Edit after OP provided code example:

I've reproduced the exact structure in this WebpackBin to prove this should be working as explained above. After all, this is a feature of common.js (and other module loaders) and is therefore supported in webpack.

My changes to the code:

  1. Replaced the code in canvas.client.js and canvas.server.js with console.log of what would be called at that line
  2. Fixed wrong use of require with export default (should be require(...).default). Irrelevant, but had to do it to make the code work.
  3. Removed the canvasElement.getContex call. Would not work in the example code and is irrelevant anyway, so there is no point of mocking it.
Community
  • 1
  • 1
Marko Gresak
  • 7,950
  • 5
  • 40
  • 46
  • @futuraprime I just updated my answer and added an example. Did you do it like this? Also, note the part with verifying that `node-canvas` is not an entry point in webpack, e.g. to be bundled with vendor files. I'm certain that if you do it this way, webpack should not bundle it. If it does, you're not doing something the way I suggested. – Marko Gresak May 06 '17 at 00:00
  • I've added my current attempt to the question. I think that's a more roundabout way of doing the same thing (it was based on a few other answers I'd seen on here, though most people said they did not work either). My webpack config has only two entry points, my index.js and react-hot-loader. – futuraprime May 06 '17 at 00:02
  • @futuraprime You must have a problem somewhere else. I quickly hacked together [this example on WebpackBin](https://www.webpackbin.com/bins/-KjQ61RTI-3KCDWPxK7Y). It randomly picks file a.js or b.js, to test it out, open console and hit the save button (top left or ctrl/cmd+s), then observe what was logged. It's working as I've said it should. A side note to your code, if you're using `export default`, you'll have to use `require(...).default` to access the exported value. – Marko Gresak May 06 '17 at 00:17
  • A suggestion: try the exact code I linked for determining `isNode`. It could be that something sets `document` value even in your node code and `typeof document === 'undefined'` is not true anymore. The fact is that this part of code must execute or you must be accessing `node-canvas` from somewhere else in your code. This feature is default in all common.js environments, do not parse the code that was never required and webpack respects that from the very beginning. It should not be possible that in your case it does it anyway. – Marko Gresak May 06 '17 at 00:26
  • To prove it, I've reproduced your exact case in another WebpackBin. See updated answer for link and explanation. – Marko Gresak May 06 '17 at 00:46
  • Ah, I think I've figured out where we're diverging. You are correct that webpack doesn't eventually bundle the file it does not want. However, it still tries to work out its dependency tree, and that's where it's crashing. I added `require('fs')` to your canvas.server.js on webpackbin and the bundler crashes, even though it's in a file it won't ultimately bundle. (node-canvas uses fs; this is where webpack crashes.) – futuraprime May 06 '17 at 07:28
  • @futuraprime interesting, I never noticed that before, I guess that's part of imports optimization. A hack, but I guess you could use [resolve.alias](https://webpack.js.org/configuration/resolve/) to mock canvas with webpack. – Marko Gresak May 06 '17 at 11:24