A Webpack Survival Guide for Rails Developers

A Webpack Survival Guide for Rails Developers

Moving from Webpack basics to using it in production means understanding how it works. We'll demystify how Webpack bundles assets for the browser, how it differs from the Rails asset pipeline, and highlight common challenges that may occur coming from Sprockets.

This is the talk we would have wanted to see before recently adopting Webpack in our own Rails app.

B0169a78f851962058d63337ad0147d6?s=128

Ross Kaffenberger

April 10, 2018
Tweet

Transcript

  1. 2.
  2. 4.
  3. 5.
  4. 6.
  5. 10.
  6. 12.
  7. 13.
  8. 14.
  9. 15.
  10. 17.
  11. 18.
  12. 22.
  13. 28.
  14. 34.
  15. 35.
  16. 36.
  17. 37.
  18. 40.
  19. 41.

    Webpack tutorials SurviveJS Webpack Book - Juho Vepsäläinen survivejs.com/webpack Webpack

    Learning Academy - Sean Thomas Larkin webpack.academy Webpack from Nothing - David Bryant Copeland what-problem-does-it-solve.com/webpack/index.html
  20. 48.
  21. 51.

    sprockets Organizing code app/ assets/ javascripts/ application.js admin.js my_function.js my_module.js

    components/ component_a.js component_b.js public/ assets/ javascripts/ application-abc123...js admin-def456...jss
  22. 52.

    sprockets Organizing code app/ assets/ javascripts/ application.js admin.js my_function.js my_module.js

    components/ component_a.js component_b.js public/ assets/ javascripts/ application-abc123...js admin-def456...jss Rails.application.configure do config.assets.precompile += %w(admin.js) end
  23. 56.

    webpacker+ app/ webpacker+ entry assets/ javascripts/ my_function.js my_module.js components/ component_a.js

    component_b.js application.js admin.js ... javascript/ packs/ Organizing code
  24. 57.

    webpacker+ app/ public/ assets/ javascripts/ ... packs/ application-abc123...js admin-def456...js webpacker+

    entry assets/ javascripts/ my_function.js my_module.js components/ component_a.js component_b.js application.js admin.js ... javascript/ packs/ Organizing code
  25. 58.

    webpacker+ app/ public/ assets/ javascripts/ ... packs/ application-abc123...js admin-def456...js webpacker+

    output entry assets/ javascripts/ my_function.js my_module.js components/ component_a.js component_b.js application.js admin.js ... javascript/ packs/ Organizing code
  26. 59.

    webpacker+ app/ public/ assets/ javascripts/ ... packs/ application-abc123...js admin-def456...js webpacker+

    output entry “packs” auto-configured assets/ javascripts/ my_function.js my_module.js components/ component_a.js component_b.js application.js admin.js ... javascript/ packs/ Organizing code
  27. 73.

    webpacker+ app/ assets/ javascripts/ ... javascript/ my_function.js my_module.js components/ component_a.js

    component_b.js packs/ application.js admin.js public/ assets/ javascripts/ ... packs/ application-abc123...js admin-def456...js Organizing code webpacker+ ✅
  28. 82.

    webpacker+ require.context Organizing code // app/javascript/some_directory/index.js const requireModule = require.context('.',

    true, /\.js$/) // app/javascript/packs/application.js import 'some_directory' requireModule.keys() // ['./module_a.js', ‘./module_b.js', ...] .filter(filename => filename !== './index.js') .forEach(requireModule)
  29. 84.

    webpacker+ Organizing code // app/javascript/some_directory/index.js import camelCase from 'lodash/camelCase' const

    requireModule = require.context('.', false, /\.js$/) const modules = {} requireModule.keys().forEach(fileName => { if (fileName === './index.js') return const moduleName = camelCase( fileName.replace(/(\.\/|\.js)/g, '') ) modules[moduleName] = requireModule(fileName) }) export default modules
  30. 85.

    webpacker+ Organizing code // app/javascript/some_directory/index.js import camelCase from 'lodash/camelCase' const

    requireModule = require.context('.', false, /\.js$/) const modules = {} requireModule.keys().forEach(fileName => { if (fileName === './index.js') return const moduleName = camelCase( fileName.replace(/(\.\/|\.js)/g, '') ) modules[moduleName] = requireModule(fileName) }) export default modules
  31. 86.

    webpacker+ Organizing code // app/javascript/some_directory/index.js import camelCase from 'lodash/camelCase' const

    requireModule = require.context('.', false, /\.js$/) const modules = {} requireModule.keys().forEach(fileName => { if (fileName === './index.js') return const moduleName = camelCase( fileName.replace(/(\.\/|\.js)/g, '') ) modules[moduleName] = requireModule(fileName) }) export default modules
  32. 87.

    webpacker+ Organizing code // app/javascript/some_directory/index.js import camelCase from 'lodash/camelCase' const

    requireModule = require.context('.', false, /\.js$/) const modules = {} requireModule.keys().forEach(fileName => { if (fileName === './index.js') return const moduleName = camelCase( fileName.replace(/(\.\/|\.js)/g, '') ) modules[moduleName] = requireModule(fileName) }) export default modules
  33. 88.

    webpacker+ Organizing code // app/javascript/some_directory/index.js import camelCase from 'lodash/camelCase' const

    requireModule = require.context('.', false, /\.js$/) const modules = {} requireModule.keys().forEach(fileName => { if (fileName === './index.js') return const moduleName = camelCase( fileName.replace(/(\.\/|\.js)/g, '') ) modules[moduleName] = requireModule(fileName) }) export default modules
  34. 89.
  35. 90.
  36. 95.

    sprockets Taming dependencies app/ assets/ javascripts/ application.js vendor.js sprockets //

    app/views/layouts/application.html.erb <%= javascript_include_tag "vendor" %> <%= javascript_include_tag "application" %>
  37. 96.

    sprockets Taming dependencies app/ assets/ javascripts/ application.js vendor.js sprockets //

    app/assets/javascripts/vendor.js //= require jquery-rails // app/views/layouts/application.html.erb <%= javascript_include_tag "vendor" %> <%= javascript_include_tag "application" %>
  38. 97.

    sprockets Taming dependencies app/ assets/ javascripts/ application.js vendor.js sprockets //

    app/assets/javascripts/vendor.js //= require jquery-rails // app/assets/javascripts/application.js //= require chosen-js //= require slick-carousel // app/views/layouts/application.html.erb <%= javascript_include_tag "vendor" %> <%= javascript_include_tag "application" %>
  39. 98.

    sprockets Taming dependencies app/ assets/ javascripts/ application.js vendor.js sprockets //

    app/assets/javascripts/vendor.js //= require jquery-rails // app/assets/javascripts/application.js //= require chosen-js //= require slick-carousel manual code splitting // app/views/layouts/application.html.erb <%= javascript_include_tag "vendor" %> <%= javascript_include_tag "application" %>
  40. 100.
  41. 101.

    webpacker+ Taming dependencies app/ javascript/ packs/ application.js vendor.js // app/javascript/packs/vendor.js

    import 'jquery' // app/views/layouts/application.html.erb <%= javascript_pack_tag "vendor" %> <%= javascript_pack_tag "application" %>
  42. 102.

    webpacker+ Taming dependencies app/ javascript/ packs/ application.js vendor.js // app/javascript/packs/vendor.js

    import 'jquery' // app/javascript/packs/application.js import 'chosen-js' import 'slick-carousel' // app/views/layouts/application.html.erb <%= javascript_pack_tag "vendor" %> <%= javascript_pack_tag "application" %>
  43. 103.

    webpacker+ Taming dependencies app/ javascript/ packs/ application.js vendor.js // app/javascript/packs/vendor.js

    import 'jquery' // app/javascript/packs/application.js import 'chosen-js' import 'slick-carousel' // app/views/layouts/application.html.erb <%= javascript_pack_tag "vendor" %> <%= javascript_pack_tag "application" %> We added “$” to global scope
  44. 107.

    webpacker+ > typeof $.fn.chosen “function" Taming dependencies // app/javascript/packs/application.js //

    app/javascript/packs/application.js import 'chosen-js' // app/javascript/packs/vendor.js import 'jquery'
  45. 108.

    webpacker+ > typeof $.fn.chosen “function" Taming dependencies // app/javascript/packs/application.js //

    app/javascript/packs/application.js import 'chosen-js' // app/javascript/packs/application.js import 'chosen-js' import 'slick-carousel' // app/javascript/packs/vendor.js import 'jquery'
  46. 109.

    webpacker+ > typeof $.fn.chosen “function" > typeof $.fn.chosen “function" >

    typeof $.fn.slick "function" Taming dependencies // app/javascript/packs/application.js // app/javascript/packs/application.js import 'chosen-js' // app/javascript/packs/application.js import 'chosen-js' import 'slick-carousel' // app/javascript/packs/vendor.js import 'jquery'
  47. 110.

    webpacker+ > typeof $.fn.chosen “function" > typeof $.fn.chosen “function" >

    typeof $.fn.slick "function" Taming dependencies // app/javascript/packs/application.js // app/javascript/packs/application.js import 'chosen-js' // app/javascript/packs/application.js import 'chosen-js' import 'slick-carousel' > typeof $.fn.chosen "function" > typeof $.fn.slick "function" > typeof $.fn.chosen "undefined" // app/javascript/packs/vendor.js import 'jquery'
  48. 111.
  49. 112.
  50. 114.

    webpacker+ Taming dependencies // config/webpack/development.js const environment = require('./environment') const

    BundleAnalyzerPlugin = require(‘webpack-bundle-analyzer').BundleAnalyzerPlugin environment.plugins.append( 'BundleAnalyzerPlugin', new BundleAnalyzerPlugin() ) $ yarn add webpack-bundle-analyzer
  51. 121.

    webpacker+ (function() { // ... $ = jQuery $.fn.extend({ chosen:

    function(options) { // ... } }) // ... }).call(this) Taming dependencies import 'chosen-js'
  52. 122.

    webpacker+ (function() { // ... $ = jQuery $.fn.extend({ chosen:

    function(options) { // ... } }) // ... }).call(this) Taming dependencies import 'chosen-js' assumes “jQuery” in global scope
  53. 124.

    webpacker+ Taming dependencies (function(factory) { if (typeof exports !== 'undefined')

    { module.exports = factory(require('jquery')) } else if (typeof define === 'function' && define.amd) { define(['jquery'], factory) } else { factory(jQuery) } })(function($) { // library code }) import 'slick-carousel'
  54. 125.

    webpacker+ Taming dependencies (function(factory) { if (typeof exports !== 'undefined')

    { module.exports = factory(require('jquery')) } else if (typeof define === 'function' && define.amd) { define(['jquery'], factory) } else { factory(jQuery) } })(function($) { // library code }) import 'slick-carousel' “Universal Module Definition”
  55. 126.

    webpacker+ Taming dependencies (function(factory) { if (typeof exports !== 'undefined')

    { module.exports = factory(require('jquery')) } else if (typeof define === 'function' && define.amd) { define(['jquery'], factory) } else { factory(jQuery) } })(function($) { // library code }) import 'slick-carousel' “Universal Module Definition” adds jQuery as a dependency
  56. 129.

    webpacker+ Taming dependencies // config/webpack/environment.js const webpack = require('webpack') const

    { environment } = require('@rails/webpacker') environment.plugins.append( 'CommonsChunkVendor', new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', }) )
  57. 130.

    webpacker+ Taming dependencies // config/webpack/environment.js const webpack = require('webpack') const

    { environment } = require('@rails/webpacker') environment.plugins.append( 'CommonsChunkVendor', new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', }) ) Extracts common modules into one bundle
  58. 131.

    webpacker+ Taming dependencies // config/webpack/environment.js const webpack = require('webpack') const

    { environment } = require('@rails/webpacker') environment.plugins.append( 'CommonsChunkVendor', new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', }) ) Extracts common modules into one bundle Changes in Webpack 4
  59. 139.

    webpacker+ Taming dependencies // config/webpack/environment.js const { environment } =

    require('@rails/webpacker') environment.loaders.append('jquery', { test: require.resolve('jquery'), use: [{ loader: 'expose-loader', options: '$', }], }) $ yarn add expose-loader
  60. 140.

    webpacker+ Taming dependencies // config/webpack/environment.js const { environment } =

    require('@rails/webpacker') environment.loaders.append('jquery', { test: require.resolve('jquery'), use: [{ loader: 'expose-loader', options: '$', }], }) $ yarn add expose-loader
  61. 141.

    webpacker+ Taming dependencies // config/webpack/environment.js const { environment } =

    require('@rails/webpacker') environment.loaders.append('jquery', { test: require.resolve('jquery'), use: [{ loader: 'expose-loader', options: '$', }], }) $ yarn add expose-loader exposes “$” to global scope
  62. 142.

    webpacker+ (function() { // ... $ = jQuery $.fn.extend({ chosen:

    function(options) { // ... } }) // ... }).call(this) Taming dependencies import 'chosen-js'
  63. 143.

    webpacker+ (function() { // ... $ = jQuery $.fn.extend({ chosen:

    function(options) { // ... } }) // ... }).call(this) Taming dependencies import 'chosen-js'
  64. 145.

    webpacker+ // config/webpack/environment.js const { environment } = require('@rails/webpacker') environment.loaders.append('chosen-js',

    { test: require.resolve('chosen-js'), use: [{ loader: 'imports-loader', options: 'jQuery=jquery,$=jquery,this=>window', }] }) Taming dependencies $ yarn add imports-loader
  65. 146.

    webpacker+ // config/webpack/environment.js const { environment } = require('@rails/webpacker') environment.loaders.append('chosen-js',

    { test: require.resolve('chosen-js'), use: [{ loader: 'imports-loader', options: 'jQuery=jquery,$=jquery,this=>window', }] }) Taming dependencies $ yarn add imports-loader
  66. 147.

    webpacker+ // config/webpack/environment.js const { environment } = require('@rails/webpacker') environment.loaders.append('chosen-js',

    { test: require.resolve('chosen-js'), use: [{ loader: 'imports-loader', options: 'jQuery=jquery,$=jquery,this=>window', }] }) Taming dependencies inject dependencies into legacy modules $ yarn add imports-loader
  67. 155.

    webpacker+ new dependency Predictable caching // app/javascript/packs/application.js import myFunction from

    'my_function' import myModule from 'my_module' myFunction() console.log('MyModule', myModule)
  68. 156.

    webpacker+ Predictable caching // app/javascript/packs/application.js import myFunction from 'my_function' import

    myModule from 'my_module' myFunction() console.log('MyModule', myModule) // app/javascript/my_module.js export default { name: 'MyModule', render() { return '<div>MyModule</div>' }, }
  69. 157.
  70. 158.
  71. 163.
  72. 164.
  73. 175.
  74. 176.
  75. 178.

    webpacker+ // config/webpack/environment.js const webpack = require('webpack') const { environment

    } = require('@rails/webpacker') environment.plugins.append( 'CommonsChunkVendor', new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', }) ) Predictable caching
  76. 180.

    webpacker+ // config/webpack/environment.js … continued Predictable caching // config/webpack/environment.js …

    continued environment.plugins.append( 'CommonsChunkManifest', new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, }), ) extract the runtime into its own bundle
  77. 181.

    webpacker+ // config/webpack/environment.js … continued Predictable caching // config/webpack/environment.js …

    continued environment.plugins.append( 'CommonsChunkManifest', new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, }), ) use consistent module ids for runtime extract the runtime into its own bundle environment.plugins.append( 'HashedModuleIds', new webpack.HashedModuleIdsPlugin() )
  78. 183.

    webpacker+ vendor.js FA C1 3D A6 33 application.js 2F EC

    4B D8 Anatomy of a bundle runtime.js
  79. 185.
  80. 186.
  81. 188.

    webpacker+ import $ from 'jquery' $('.pdf-canvas').each(() => { const url

    = $(this).data('url') Predictable caching PDFJS.getDocument(url).then(pdf => { // ... render pdf data to canvas }) }) import PDFJS from 'pdfjs/webpack'
  82. 189.

    webpacker+ import $ from 'jquery' $('.pdf-canvas').each(() => { const url

    = $(this).data('url') Predictable caching PDFJS.getDocument(url).then(pdf => { // ... render pdf data to canvas }) }) import PDFJS from 'pdfjs/webpack'
  83. 190.

    webpacker+ Predictable caching import $ from 'jquery' $('.pdf-canvas').each(() => {

    const url = $(this).data('url') PDFJS.getDocument(url).then(pdf => { // ... render pdf data to canvas }) import('pdfjs/webpack').then((PDFJS) => { }) })
  84. 191.

    webpacker+ Predictable caching import $ from 'jquery' $('.pdf-canvas').each(() => {

    const url = $(this).data('url') PDFJS.getDocument(url).then(pdf => { // ... render pdf data to canvas }) import('pdfjs/webpack').then((PDFJS) => { }) })
  85. 192.

    webpacker+ Predictable caching import $ from 'jquery' $('.pdf-canvas').each(() => {

    const url = $(this).data('url') PDFJS.getDocument(url).then(pdf => { // ... render pdf data to canvas }) import('pdfjs/webpack').then((PDFJS) => { }) }) Webpack’s sweet spot!
  86. 195.

    webpacker+ Predictable caching import( Use a “chunk name” for consistent

    bundle caching 'pdfjs/webpack').then(…) /* webpackChunkName: "pdfjs" */
  87. 196.

    webpacker+ Predictable caching import( Use a “chunk name” for consistent

    bundle caching 'pdfjs/webpack').then(…) /* webpackChunkName: "pdfjs" */
  88. 197.

    webpacker+ Predictable caching import( Use a “chunk name” for consistent

    bundle caching 'pdfjs/webpack').then(…) /* webpackChunkName: "pdfjs" */
  89. 201.