Webpack ALL The Assets!!
With the release of Rails 6, Webpack was introduced as the default JavaScript bundler by using the Webpacker gem . We tend to think about Webpack only as a tool to handle JavaScript files, but it can be used to handle all kinds of asset files. This article shows how to create a Rails app that uses only Webpack to handle all the assets, including images, fonts, styles and videos.
Init the Rails App
rails new NoSprocketsRails --skip-sprockets
You can use
-S
instead of--skip-sprockets
, both are aliases. Userails new -h
to see the available options.
Cleaning the New App
Interestingly, there’s some Sprockets-related files and code that are still getting created so we need to remove them. The app/assets
folder contains: config/manifest.js
(a configuration file for Sprockets) and stylesheets/application.css
contains the comments and the require
statements from Sprockets.
We can delete the app/assets
folder for now. We are going to use app/assets
to store all our assets instead of app/javascript
, but we’ll do that in the next section.
Rails depends on the sprockets-rails
gem, so some dependencies will appear in the Gemfile.lock
file, but if we check inside config/application.rb
we can see that the app is not requiring sprockets/railtie
.
Another change we have to make is in the application.html.erb
layout file, it’s using stylesheet_link_tag
but with Webpacker we want to use the stylesheet_pack_tag
helper.
The
stylesheet_link_tag
is part of ActionView, so it still partially works even withoutsprockets-rails
.
app/javascript to app/assets
Since we are going to handle all asset types and not only JavaScript files with Webpack, we are going to use a more descriptive name for the folder. Instead of app/javascript
, we are renaming the folder to app/assets
.
We also have to tell Webpacker that we changed that path. We do that by setting the new path for the source_path
option in the config/webpacker.yml
file.
Handling CSS
We have two options here:
Using packs/application.css
We can create a CSS (or SASS) file at app/assets/packs/application.scss
(or .sass/css), Webpack will emit an application.css
pack/bundle as the compiled version.
This option feels more similar to what we would do with Sprockets, but there seems to be an issue when combined with image handling .
Importing CSS Inside JS
Another option is to import the css file inside our application.js
pack. In this case, Webpack will compile the imported CSS and will emit a .css
file with the same name as the pack, so, if the pack is called admin.js
, the imported CSS will be emitted as admin.css
even if the imported file is named differently.
We will use this option for the rest of the article.
Files Structure
We are storing the CSS files at app/assets/stylesheets
and we will import the needed files in our pack. The files will then look like this:
// app/assets/packs/application.js
import "../stylesheets/application.scss"; // this is added, the rest is the default
import Rails from "@rails/ujs";
// ...
// app/assets/stylesheets/application.scss
body {
background-color: blue;
}
JavaScript Code
A common mistake when starting with Webpack is to put all the JS files inside app/assets/pack
. The problem with this is that Webpack will emit one bundle for each of the files that it finds at that directory and it’s something we don’t want to do in general.
Files that are used by the pack that we don’t want Webpack to emit, should be in a different folder. We are going to use a structure similar to Sprockets, so any extra custom JS file will be at app/assets/javascript
.
// app/assets/packs/application.js
import "../stylesheets/application.scss";
import Rails from "@rails/ujs";
// ...
ActiveStorage.start();
import "../javascript/debugging"; // we import a file from outside `packs` with `..`
// app/assets/javascript/debugging.js
console.log("Hello world");
Now we should see Hello world
in the browser’s console using the DevTools.
Debugging Emitted Assets
In the previous section we talked about an issue when adding all the asset files inside /pack
and how Webpack would compile and emit any file found there. You can check your logs to see what Webpack is emitting:
[Webpacker] Hash: e4fc711e2adb6a0437c7 Version: webpack 4.46.0 Time: 697ms Built at: 02/07/2021 11:23:59 Asset Size Chunks Chunk Names js/application-f9a3471a7f250c8b35d8.js 138 KiB application [emitted] [immutable] application js/application-f9a3471a7f250c8b35d8.js.map 152 KiB application [emitted] [dev] application manifest.json 364 bytes [emitted]
It is only emitting a JS file (and the source map) during development because it doesn’t extract the CSS from JS files as a separated file by default. During development, the CSS is still part of the JS code and it’s injected in the head of the HTML by Webpack.
We can change this behavior with the extract_css
option for the development
environment inside the config/webpacker.yml
file. When set to true
, we will see it emits the CSS file too (and the source map):
[Webpacker] Hash: 1fae6519a03d7182ac2b Version: webpack 4.46.0 Time: 893ms Built at: 02/07/2021 11:35:49 Asset Size Chunks Chunk Names css/application-cf683911.css 80 bytes application [emitted] [immutable] application css/application-cf683911.css.map 188 bytes application [emitted] [dev] application js/application-fae78801cd0a245d8eac.js 126 KiB application [emitted] [immutable] application js/application-fae78801cd0a245d8eac.js.map 139 KiB application [emitted] [dev] application manifest.json 640 bytes [emitted]
Also, if we compile the assets manually for production using RAILS_ENV=production rails assets:precompile
, we can see it not only emits the CSS bundle but it adds compressed versions using Brotli and Gzip to optimize the size when the browser downloads the assets:
Hash: eee1e7c3239c83d7ca88 Version: webpack 4.46.0 Time: 2852ms Built at: 02/07/2021 11:28:53 Asset Size Chunks Chunk Names css/application-00551f1a.css 20 bytes 0 [emitted] [immutable] application js/application-91581966b20673bf924a.js 69.5 KiB 0 [emitted] [immutable] application js/application-91581966b20673bf924a.js.br 15.4 KiB [emitted] js/application-91581966b20673bf924a.js.gz 17.8 KiB [emitted] js/application-91581966b20673bf924a.js.map 205 KiB 0 [emitted] [dev] application js/application-91581966b20673bf924a.js.map.br 43.9 KiB [emitted] js/application-91581966b20673bf924a.js.map.gz 51 KiB [emitted] manifest.json 494 bytes [emitted] manifest.json.br 157 bytes [emitted] manifest.json.gz 170 bytes [emitted]
This comes handy when trying to debug issues in production.
Webpacker, by default, is configured to behave differently in different environments so keep that in mind if you can’t find your assets after a deploy.
Handling Images
To handle images with Webpack we need to also import them inside our application.js
pack. We can import each image manually or we can tell Webpack to import all the images inside a directory.
// app/assets/packs/application.js
import "../stylesheets/application.scss";
// import '../images/cat.jpg' // we could do this for each image
require.context("../images", true); // or this to import all the images at once
import Rails from "@rails/ujs";
// ...
And we store our cat.jpg
image at app/assets/images/cat.jpg
.
Now we can see it’s also emitting the image files:
Webpacker] Hash: 92afbc5eddcd2ffc50b0 Version: webpack 4.46.0 Time: 637ms Built at: 02/07/2021 11:54:48 Asset Size Chunks Chunk Names css/application-cf683911.css 80 bytes application [emitted] [immutable] application css/application-cf683911.css.map 188 bytes application [emitted] [dev] application js/application-8afdd2859d6b40d4b0c4.js 128 KiB application [emitted] [immutable] application js/application-8afdd2859d6b40d4b0c4.js.map 140 KiB application [emitted] [dev] application manifest.json 730 bytes [emitted] media/images/cat-106e150392be77f57358d16ef3678a35.jpg 732 KiB [emitted]
You can see all the file extensions that Webpack will process in the
config/webpacker.yml
file. You can add more image formats if you need, likewebp
for example.
Using the Image
Like Sprockets, Webpacker is configured to add a digest by default for the emitted files. To render an img
tag with the Rails’ helpers, we have two options:
image_tag
+ asset_pack_path
If we want to use the common image_tag
helper, we can’t reference the image name like we do with Sprockets, we need to use a Webpacker helper to get the correct path of the file:
image_tag asset_pack_path('media/images/cat.jpg')
media
is the directory where non-CSS/JS assets are emitted, you can check that with the console output.
image_pack_tag
Since the previous option requires a lot of extra code, we can use a new helper provided by Webpacker:
image_pack_tag 'cat.jpg'
This way, the usage is more similar to what we are used to, Webpack will take care of the path and the digest.
Handling Fonts
Webpack can already handle all the common font formats we may need (otf, ttf, woff, woff2, svg, etc). We will put all the font files at app/assets/fonts
.
For fonts, we don’t need to import them in our JS file, we can simply reference the files in a SCSS file and Webpack will compile them.
// app/assets/stylesheets/fonts.scss
@font-face {
font-family: "custom-font";
src: url("../fonts/custom-font.eot");
src: url("../fonts/custom-font.eot?#iefix") format("embedded-opentype"), url("../fonts/custom-font.ttf")
format("truetype"), url("../fonts/custom-font.woff") format("woff"), url("../fonts/custom-font.svg#custom-font")
format("svg");
font-weight: normal;
font-style: normal;
}
Then we need to import the fonts CSS in our application.scss
file:
// app/assets/stylesheets/application.scss
@import "./fonts.scss";
body {
background-color: blue;
}
Now we can check the logs to be sure fonts were emitted:
[Webpacker] Hash: 00125bacb545dee275b9 Version: webpack 4.46.0 Time: 653ms Built at: 02/07/2021 12:50:33 Asset Size Chunks Chunk Names css/application-9035a31a.css 718 bytes application [emitted] [immutable] application css/application-9035a31a.css.map 753 bytes application [emitted] [dev] application js/application-cae07e31f98e2d40328d.js 128 KiB application [emitted] [immutable] application js/application-cae07e31f98e2d40328d.js.map 141 KiB application [emitted] [dev] application manifest.json 1.22 KiB [emitted] media/fonts/custom-font-3053c3c3bde7275a0023f883df3c257e.eot 19.8 KiB [emitted] media/fonts/custom-font-62822c524e9c9ff8e89893ed331a2527.svg 61.4 KiB [emitted] media/fonts/custom-font-cfe66074982da095bcf581f8e37b7c85.woff 22.7 KiB [emitted] media/fonts/custom-font-e8c2d03a186a219dd552978db4371ccc.ttf 40.3 KiB [emitted] media/images/cat-106e150392be77f57358d16ef3678a35.jpg 732 KiB [emitted]
Handling Videos
Handling videos is similar to handling images, though Webpacker does not provide a shorter video_pack_tag
to replace video_tag
like it does for images.
In order to handle videos, we need to:
- add the videos at
app/assets/videos
(for example, let’s call itvideo.mp4
) - add the extensions we want in the
config/webpacker.yml
file - import a directory that contains the videos in the
app/assets/packs/application.js
file (like we did for images) - use the
video_tag
combined withasset_pack_path
to get the correct file name
// app/assets/packs/application.js
require.context("../videos", true);
video_tag asset_pack_path('media/videos/video.mp4')
Live Reloading
Now that all our assets are handled by Webpack, the live reloading feature is more powerful: it will also reload our page when it detects CSS or images changes for example.
To enable live reloading, we need to start the webpack-dev-server
along with the Rails server. We have to run ./bin/webpack-dev-server
in one terminal and rails s
in another. After reloading the page, if you change any asset, your page will be reloaded automatically.
Conclusion
With these changes, we can use Webpack to handle all the assets and, at the same time, keep a familiar file structure with assets organized by type inside app/assets
with the only difference of using packs
to tell Webpack which files to emit instead of an initializer to configure Sprockets.
We can now use Node modules that provide different assets easily without having to adapt them to Sprockets’ conventions (most packages will give you instructions to use them with Webpack).
It allows us to use a JavaScript library like ReactJS or Vue.js and import assets inside the components when needed and also use them with Rails helpers.
We also have access to Webpack plugins to add more processing during compilation. Some examples:
- the ImageMinimizer plugin to optimize the compression of images
- if we want to use Tailwind.css, we have the
purge-css
plugin to remove unused CSS - the Stylelint plugin to have a linter for our CSS
Finally, the live reload feature can speed up your development if you need to do many asset changes.
Should you migrate your app to handle all the assets using Webpack? As always, it depends on your requirements and how familiar and comfortable your team is with Webpack. You can read more in the previous blog posts to see a comparison with Sprockets and some tips to help you do the transition .
You can check a sample Rails application applying this guide here .