Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Ross Kaffenberger

April 10, 2018
Tweet

More Decks by Ross Kaffenberger

Other Decks in Technology

Transcript

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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+ ✅
  9. 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)
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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" %>
  16. 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" %>
  17. 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" %>
  18. 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" %>
  19. 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" %>
  20. 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" %>
  21. 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
  22. 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'
  23. 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'
  24. 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'
  25. 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'
  26. 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
  27. webpacker+ (function() { // ... $ = jQuery $.fn.extend({ chosen:

    function(options) { // ... } }) // ... }).call(this) Taming dependencies import 'chosen-js'
  28. webpacker+ (function() { // ... $ = jQuery $.fn.extend({ chosen:

    function(options) { // ... } }) // ... }).call(this) Taming dependencies import 'chosen-js' assumes “jQuery” in global scope
  29. 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'
  30. 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”
  31. 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
  32. 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', }) )
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. webpacker+ (function() { // ... $ = jQuery $.fn.extend({ chosen:

    function(options) { // ... } }) // ... }).call(this) Taming dependencies import 'chosen-js'
  39. webpacker+ (function() { // ... $ = jQuery $.fn.extend({ chosen:

    function(options) { // ... } }) // ... }).call(this) Taming dependencies import 'chosen-js'
  40. 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
  41. 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
  42. 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
  43. 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)
  44. 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>' }, }
  45. 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
  46. 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
  47. 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() )
  48. webpacker+ vendor.js FA C1 3D A6 33 application.js 2F EC

    4B D8 Anatomy of a bundle runtime.js
  49. 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'
  50. 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'
  51. 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) => { }) })
  52. 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) => { }) })
  53. 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!
  54. webpacker+ Predictable caching import( Use a “chunk name” for consistent

    bundle caching 'pdfjs/webpack').then(…) /* webpackChunkName: "pdfjs" */
  55. webpacker+ Predictable caching import( Use a “chunk name” for consistent

    bundle caching 'pdfjs/webpack').then(…) /* webpackChunkName: "pdfjs" */
  56. webpacker+ Predictable caching import( Use a “chunk name” for consistent

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