Valerii Iatsko

Full Stack Developer

Hi! My name is Valerii Iatsko.

I'm a Full Stack Developer and the author of this blog.

→ Follow on Twitter

React for Beginners - Episode 3 - Adding assets to your isomorphic application (using webpack-isomorphic-tools)

1

In a previous article, we created an isomorphic application and we had a little homework part on adding webpack-isomorphic-tools to your project in order to be able to add css/images/other assets to your project.

In this article, we’ll try to solve this problem!

-> Most of the stuff here came up after I invested some time investigating https://github.com/erikras/react-redux-universal-hot-example works and how to build isomorphic app using the same approach step-by-step.

First of all, let’s understand what loaders we are going to use for our assets. I’d suggest to start with these here:

  • css-loader
  • style-loader
  • file-loader
  • url-loader
  • postcss-loader + autoprefixer plugin (so we won’t need to deal with vendor prefixes manually)

Hopefully, none of them will need explanation as these are pretty much typical loaders for the most of webpack setups.

Let’s install them:

npm install style-loader css-loader postcss-loader autoprefixer url-loader file-loader --save

Also, we’ll use a special package to make these loaders working both on server and client side, called webpack-isomorphic-tools:

npm install webpack-isomorphic-tools --save

What is webpack-isomorphic-tools? webpack-isomorphic-tools is a small helper module providing basic support for isomorphic (universal) rendering when using Webpack

We’ll need to add a special configuration file for it.

Let’s first create it and then I’ll explain what it does exactly:

const WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
module.exports = {
  assets: {
    images: {
      extensions: [
        'jpeg',
        'jpg',
        'png',
        'gif',
      ],
      parser: WebpackIsomorphicToolsPlugin.url_loader_parser,
    },
    fonts: {
      extensions: [
        'woff',
        'woff2',
        'ttf',
        'eot',
      ],
      parser: WebpackIsomorphicToolsPlugin.url_loader_parser,
    },
    svg: {
      extension: 'svg',
      parser: WebpackIsomorphicToolsPlugin.url_loader_parser,
    },
    style_modules: {
      extensions: ['css'],
      filter(module, regex, options, log) {
        if (options.development) {
          return WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log);
        }
        return regex.test(module.name);
      },
      path(module, options, log) {
        if (options.development) {
          return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log);
        }
        return module.name;
      },
      parser(module, options, log) {
        if (options.development) {
          return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log);
        }
        return module.source;
      },
    },
  },
};

Here we’re describing what type of assets we want to handle and how we are going to handle those in development and production. With images and svg files that’s pretty much clear — they are always existing on the disk, for styles we are using style-loader as we want to keep hot load support in development environment, and in production those will be simply extracted using webpack’s ExtractText plugin.

To learn more about webpack-isomorphic-tools configuration, if you need to, I’d recommend to read official documentation on module page https://github.com/halt-hammerzeit/webpack-isomorphic-tools.

Now, we need to add webpackIsomorphicTools as a plugin to each of webpack config’s:

I’ll simply add couple of require directives at the top of each config:

const webpackIsomorphicToolsConfig = require('./webpack-isomorphic-tools');
const WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
const webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(webpackIsomorphicToolsConfig);

And

webpackIsomorphicToolsPlugin.development(),

to plugins list in ./webpack/dev.config.js and simply

webpackIsomorphicToolsPlugin,

to production config.

If you want to check on what we have for this moment, here’s the revision with updated webpack configs, dependencies and webpack-isomorphic-tools configuration file inside: https://github.com/codingbox/isomorphic-react-redux-tutorial/tree/915069face442afef4aaf8931a92087cb9f0d611

Now, we need to add loaders to webpack config:

This is a configuration for pretty much typical set of the modules we’ve loaded (here are also ?v= postfixes used for the cache wiping purposes):

  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        exclude: /(node_modules|bower_components)/,
        loader: 'babel',
      },
      {
        test: /\.css$/,
        loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!postcss',
      },
      {
        test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url?limit=10000&mimetype=application/font-woff',
      },
      {
        test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url?limit=10000&mimetype=application/font-woff',
      },
      {
        test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url?limit=10000&mimetype=application/octet-stream',
      },
      {
        test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'file',
      },
      {
        test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url?limit=10000&mimetype=image/svg+xml',
      },
      {
        test: webpackIsomorphicToolsPlugin.regular_expression('images'),
        loader: 'url-loader?limit=10240',
      },
    ],
  },
  postcss() {
    return [autoprefixer];
  },

(make sure to require(‘autoprefixer’) in order to make postcss block working :P)

For production config we’ll need to add ExtractText plugin:

npm install extract-text-webpack-plugin --save

What does it do? It basically exports all the styles to external .css file. It’s needed so you won’t have to load css files from javascript which might negatively impact user experience in terms of page might be flickering for a while (of course, there’re some ways to avoid this, but all of them are coming at much higher cost than simply separating css from js).

Include it into your production config as well as autoprefixer:

const ExtractTextPlugin = require('extract-text-webpack-plugin');
const autoprefixer = require('autoprefixer');

The plugin set will be pretty much the same as for development, except that we need to wrap our css-loader in ExtractTextPlugin.extract directive:

      {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract(
                  'style',
                  'css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!postcss'
                ),
      },
      {
        test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url?limit=10000&mimetype=application/font-woff',
      },
      {
        test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url?limit=10000&mimetype=application/font-woff',
      },
      {
        test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url?limit=10000&mimetype=application/octet-stream',
      },
      {
        test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'file',
      },
      {
        test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url?limit=10000&mimetype=image/svg+xml',
      },
      {
        test: webpackIsomorphicToolsPlugin.regular_expression('images'),
        loader: 'url-loader?limit=10240',
      },
    ],
  },
  postcss() {
    return [autoprefixer];
  },

Also, we’ll need to add ExtractText to plugins list:

new ExtractTextPlugin('[name]-[chunkhash].css', { allChunks: true }),

Here’s the git revision to check if you did everything correctly with your configs https://github.com/codingbox/isomorphic-react-redux-tutorial/tree/690b8f89ee26fb0fd6b2271efdd7ac5682c0f23e

From now on, if you’ll run the project:

npm run dev

You’ll see the webpack-assets.json file appearing in your root directory with assets mapping.

That’s cool and actually we might now use it to properly map paths correctly to webpack bundles even if they’ll include hashes (and hashes are useful to invalidate cdn cache).

Let’s do this little change in our webpack configs:

In output sections, simply change:

filename: '[name].js',

to:

filename: '[name]-[hash].js',

Now as you’ll run npm run dev again, you’ll see that hash appeared in assets json:

$ cat webpack-assets.json                                                                                           
{
  "javascript": {
    "main": "http://localhost:3002/assets/main-eb4ddded33b24b21fb8b.js"
  },
  "styles": {},
  "assets": {}
}

Amazing! Isn’t it?

Now let’s hack our server a little bit in order to make it picking up webpack-isomorphic-tools properly.

Let’s go to ./bin/server.js and add a little trick right after:

app.use((req, res) => {

The trick would be this one:

  if (process.env.NODE_ENV === 'development') {
    webpackIsomorphicTools.refresh();
  }

Also we can now bring webpack-isomorphic-tools assets lists to components right away by adding assets property to every Default component:

<Default assets={webpackIsomorphicTools.assets()} />

Now let’s try to run our server again…

…and you’ll see that it doesn’t work, because we didn’t import webpackIsomorphicTools here.

But we can’t do it right away!

The problem is that webpack-isomorphic-tools should be hacking into nodejs require system in order to operate properly, so we’ll need to initialize it externally.

Let’s rename server.js -> _server.js. And create a simple loader in place of server.js:

/* eslint global-require: 0 */
const path = require('path');
const webpackIsomorphicToolsConfig = require('../webpack/webpack-isomorphic-tools');
const WebpackIsomorphicTools = require('webpack-isomorphic-tools');
const rootDir = path.resolve(__dirname, '..');
global.webpackIsomorphicTools = new WebpackIsomorphicTools(webpackIsomorphicToolsConfig)
  .development(process.env.NODE_ENV === 'development')
  .server(rootDir, () => {
    require('./_server');
  });

Now npm run dev will be working again.

Now, let’s create some styles, I’ll use the very simple example:

./src/containers/App.css:

body {
  margin: 0;
  padding: 0;
}
.app {
  font-size: 72px;
}

And I’ll make App using it as an inline CSS module:

/* eslint react/prefer-stateless-function: 0, react/forbid-prop-types: 0 */
import React from 'react';
import styles from './App.css';
export default class App extends React.Component {
  render() {
    return <div className={styles.app}>{this.props.children}</div>;
  }
}
App.propTypes = {
  children: React.PropTypes.any,
};

Looks good, now the last step, let’s change our layout file in order to make it loading our new assets :) You should pay attention at a part added between <head> & </head> tags as well as assets property (we added it to every Default component in ./bin/server.js earlier).

/* eslint react/prefer-stateless-function: 0, react/no-danger: 0, react/forbid-prop-types: 0 */
/* eslint no-underscore-dangle: 0, global-require: 0 */
import React from 'react';
import ReactDOM from 'react-dom/server';
import serialize from 'serialize-javascript';
import { webpackHost, webpackPort } from '../../config/env';
export default class Default extends React.Component {
  render() {
    const { assets, component, store } = this.props;
    const content = component ? ReactDOM.renderToString(component) : '';
return (
      <html lang="en">
        <head>
          <title>Hello, world!</title>
          {/* production */}
          {Object.keys(assets.styles).map((style, key) =>
            <link
              href={assets.styles[style]}
              key={key} media="screen, projection"
              rel="stylesheet" type="text/css" charSet="UTF-8"
            />
          )}
          {/* development */}
          {
            Object.keys(assets.styles).length === 0 ?
              <style dangerouslySetInnerHTML={{ __html: require('../containers/App.css')._style }} /> :
            null
          }
        </head>
        <body>
          <div id="root" dangerouslySetInnerHTML={{ __html: content }} />
          <script
            dangerouslySetInnerHTML={{ __html: `window.__data=${serialize(store.getState())};` }}
            charSet="UTF-8"
          />
          <script
            src={ assets.javascript.main }
            charSet="UTF-8"
         />
        </body>
      </html>
    );
  }
}
Default.propTypes = {
  assets: React.PropTypes.object,
  component: React.PropTypes.node,
  store: React.PropTypes.object,
};

If everything’s fine, now we will be getting a way bigger text on vising our app:

2

And in source code you’ll see our css:

3

Of course, in production it will be moved to a separate file, see:

4

That’s it! Now you have a fully isomorphic app with assets. :)

The code for this article can be found on a github https://github.com/codingbox/isomorphic-react-redux-tutorial

© 2017 Valerii Iatsko. Personal opinions.
EN | RU