Read more

Webpack(er): A primer

Tobias Kraze
July 05, 2019Software engineer at makandra GmbH

webpack Show archive.org snapshot is a very powerful asset bundler written in node.js to bundle (ES6) JavaScript modules, stylesheets, images, and other assets for consumption in browsers.

Illustration online protection

Rails Long Term Support

Rails LTS provides security patches for old versions of Ruby on Rails (2.3, 3.2, 4.2 and 5.2)

  • Prevents you from data breaches and liability risks
  • Upgrade at your own pace
  • Works with modern Rubies
Read more Show archive.org snapshot

Webpacker Show archive.org snapshot is a wrapper around webpack that handles integration with Rails.

This is a short introduction.

Installation

If you haven't already, you need to install node.js and Yarn.

Then, put

gem 'webpacker', '~> 4.x' # check if 4.x is still current!

in your Gemfile. Run

bundle install

Finally, run

bundle exec rails webpacker:install

Alternatively, you can add webpacker from the start when creating a new Rails app:

rails new myapp --webpack

We tend to put our assets in app/webpack instead of the default app/javascripts, since we not only bundle JavaScript, but stylesheets as well. To do this, change default.source_path to app/webpack in config/webpacker.yml.

Since we will use webpack for all assets, you should also disable the (Sprockets) Asset pipeline by adding

config.assets.enabled = false

to your application.rb.

Packs

Now we need to define our packs. A pack is a collection of assets that to be bundled into a single output JavaScript file, one single CSS file, plus separate files for images, fonts etc. Most applications will use a single pack (we often call it main), some applications might for example use separate frontend and backend packs.

To add a pack named main, add a file app/webpack/packs/main.js. This will be your entry point. All assets belonging to that pack need to be imported here.

Since webpack is configured to use Babel Show archive.org snapshot , you can write modern ES6 syntax in all JavaScript files. Babel will transpile it to work in most browsers. You can optionally configure which browsers to support.

Adding your own JavaScript

Add a

= javascript_pack_tag 'main'

at the bottom of your applications layout.

Now, all JavaScript you put into your packs/main.js will run in the browser. However, you should not put any application code directly there. Instead

  • make a directory app/webpack/javascripts

  • add some file app/webpack/javascripts/my_component.js

  • import it in packs/main.js with

    import '../javascripts/my_component.js'   // import always uses relative paths
    

And of course, your own code can (and should) use its own import statements to load any dependencies it might have.

Since this quickly becomes cumbersome, we usually add the following to simply import all our JavaScript files at once:

// in packs/main.js

let webpackContext = require.context('../javascripts', true, /\.js$/)
for(let key of webpackContext.keys()) { webpackContext(key) }

webpack will automatically run all JavaScript through Babel. It will also compress and minify in production. The generated output will be fingerprinted, so it can be properly cached.

Using external libraries

To use an external library, install it with yarn, e.g.

yarn add unpoly

Then simply import it in your main.js (or anywhere else), by adding

import 'unpoly'

If you don't specify a relative path, webpack will look in your node_modules directory, which is exactly where Yarn puts all the libraries.

Yarn is very similar to Bundler:

  • Code dependencies are managed within package.json
  • It has a lockfile (yarn.lock) with exact version numbers.
  • The lockfile should be checked into Git.
  • You never touch the lockfile by hand. You change it with a yarn command instead:
yarn install
yarn add [package]
yarn upgrade [package]
yarn upgrade [package]@[version]
yarn remove [package]

Under the hood, Yarn calls npm for you. Do not use the npm command yourself. Do not manipulate package.json yourself. Yarn will do this for you.

The dev server

If might have noticed that adding webpack made you development server pretty slow. To increase performance, open a separate terminal, and run bin/webpack-dev-server. The dev server will watch all your changes and compile everything automatically in the background. It will also auto-reload your code in the browser.

If your JavaScript code has syntax errors, those will be shown here as well.

Adding stylesheets

webpack's way of handling assets that are not JavaScript is to simply import them, too.

So to add a stylesheet, put a .sass (or .scss or .css file) in app/webpack/stylesheets, then import it

// in packs/main.js

import '../stylesheets/application.sass'

At first this looks weird. We're now importing something that is not JavaScript from within our own JavaScript code. But this is simply how bundling non-JavaScript assets works in webpack. This will not actually add any useful code to our JavaScript, but it will let webpack know, it has to put the compiled application.sass into our main.css.

Of course, we have to tell the browser to actually load the stylesheet, by adding a

= stylesheet_pack_tag 'main'

into your layout's <head> tag.

You can import stylesheets from external libraries the same way. To load Unpoly's default CSS for example, add

// packs/main.js

import 'unpoly/dist/unpoly.css'

As above, we usually want to load all CSS from some directories. Do this with

// packs/main.js

require.context('../stylesheets/blocks', true, /\.s[ac]ss$/)

Adding Images

Same deal. To emit an image, import it from your JavaScript. To import all images from app/webpack/images add

// packs/main.js

require.context('../images', true, /\.(?:png|jpg|gif|ico|svg)$/)

If you want to reference images in your CSS (for example for a background-image), you can do so by using a relative path:

.logo
  background-image: url(../images/logo.png)

Webpack will replace the relative path with the final path to the emitted image (including fingerprint).

To reference an image directly in your Ruby views, use

= image_tag(asset_pack_path('images/logo.png'))

With Webpack 4 other assets than stylesheets or javascripts are stored in a media folder (in this example the pack is application.js):

= image_pack_tag('media/application/images/logo.png')

Deployment

Follow Configuring Webpacker deployments with Capistrano.

Tests

In your cucumber test, you will want to regenerate your assets before each test suite run. Since this is a bit slow, especially when using multiple processes with parallel_test, read this card.

jQuery

If you still require jQuery, add it to your webpacker project by:

  • installing via

    yarn add jquery
    
  • requiring and exposing it in your packs/main.js:

    import jQuery from 'jquery'
    window.$ = jQuery
    window.jQuery = jQuery
    

If you use any other jQuery plugins that expect the $ or jQuery object to be globally accessible, you will have to use webpack's ProvidePlugin:

// in config/environment.js

environment.plugins.prepend(
  'Provide', new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery'
  })
)

This plugin will check if any file uses a global object called $ or jQuery, then load it from the given path (just jquery meaning node_modules/jquery in this case) and inject it.

A word about loaders

As we've seen above, to bundle files that are not JavaScript, we can simply import them. But how does webpack know what to do with each file? To emit images as their own files, to add stylesheets to a common CSS file? How does it know about SASS?

The answer is: You can define so called "loaders".

For example, the configuration for SASS files looks like this:

{ 
  test: /\.(scss|sass)$/i,
  use: [
    { loader: 'style-loader', options: { ... } },
    { loader: 'css-loader', options: { ... } },
    { loader: 'postcss-loader', options: { ... } },
    { loader: 'sass-loader', options: { ... }] }
  ],
}

When you import a file that matches the test regexp (i.e. any .scss / .sass file), all loaders (which are usually npm modules) are run on it in reverse order.

  • First, the sass-loader converts SASS to regular CSS.
  • Then, the postcss-loader uses PostCSS to postprocess the CSS. What it does exactly is configured in postcss.config.js. By default it mostly does auto-prefixing.
  • Next, the CSS loader searches the CSS for @import statements, to find any dependencies.
  • Lastly, the style-loader takes the CSS and emits it appropriately. By default, it would actually create some JavaScript code that programatically adds a <style> tag to your page. The job of actually emitting a separate main.css file is performed by a webpack plugin called MiniCssExtract.

Luckily, Webpacker configures all this for us and we usually don't have to touch it.

A word about plugins

Webpack also allows to add plugins that hook more deeply into its workflow. The MiniCssExtract plugin above is such an example. These plugins are also usually provided by npm modules.

Plugins can do a lot of things, but we don't use them too often. Two examples of plugins we do use are

Further reading

You might want to check out

Tobias Kraze
July 05, 2019Software engineer at makandra GmbH
Posted by Tobias Kraze to makandra dev (2019-07-05 11:21)