Posted 5 months ago. Visible to the public. Repeats.

Webpack(er): A primer

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

Webpacker 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

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

in your Gemfile. Run

Copy
bundle install

Finally, run

Copy
bundle exec rails webpacker:install

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

Copy
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

Copy
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, 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

Copy
= 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/javascript/my_component.js
  • import it in packs/main.js with

    Copy
    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:

Copy
// 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.

Copy
yarn add unpoly

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

Copy
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:

  • 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:
Copy
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

Copy
// 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

Copy
= 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

Copy
// packs/main.js import 'unpoly/dist/unpoly.css'

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

Copy
// 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

Copy
// 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:

Copy
.logo background-image: (../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

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

Deployment

Deploying works the same as with the asset pipeline. The usual bin/rails assets:precompile is overridden by Webpacker to run webpack.

Just make sure you have a

Copy
require 'capistrano/rails/assets'

in your Capfile.

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

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

    Copy
    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:

Copy
// 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:

Copy
{ 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

Once an application no longer requires constant development, it needs periodic maintenance for stable and secure operation. makandra offers monthly maintenance contracts that let you focus on your business while we make sure the lights stay on.

Owner of this card:

Avatar
Tobias Kraze
Last edit:
5 months ago
by Tobias Kraze
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Tobias Kraze to makandra dev
This website uses cookies to improve usability and analyze traffic.
Accept or learn more