Pro Yearly is on sale from $80 to $50! »

Webpack Encore: Lessons learned 1 year later

F5dfeeef276fcfd4751f4063487a5a3f?s=47 weaverryan
December 07, 2018

Webpack Encore: Lessons learned 1 year later

Webpack Encore has been around for more than 1 year, and it's been a huge success! In this presentation, we'll learn some lessons from the past year and get a look at the brand new features supported in the latest version of Encore.

F5dfeeef276fcfd4751f4063487a5a3f?s=128

weaverryan

December 07, 2018
Tweet

Transcript

  1. Webpack Encore lessons learned 1 year later

  2. > Lead of the Symfony documentation team
 > Writer for

    SymfonyCasts.com > Symfony evangelist… Fanboy > Husband of the much more talented @leannapelham symfonycasts.com twitter.com/weaverryan Yo! I’m Ryan! > Father to my much more charming son, Beckett
  3. @weaverryan PART 1 Webpack & Encore Powerful and Easy! (until

    they’re not)
  4. What *is* Webpack? @weaverryan

  5. @weaverryan 
 // app.js
 
 require('jquery');
 require('./another_file');
 
 require('./styles.css');
 


    
 console.log('Webpack will package all this up');
 console.log('And output 2 files: app.js and app.css');
 A tool that “packages” your app up into one CSS and one JS file
  6. @weaverryan AND A totally new (and modern) way of thinking

    about your assets. Hint: Unlearn everything you know about JavaScript and global variables
  7. @weaverryan Encore is a Node library … but we recommend

    installing the “encore” Composer package (it comes with a nice recipe)
  8. composer require encore --dev

  9. dependencies Webpack config a few files to get you started

    {
  10. {
 "devDependencies": {
 "@symfony/webpack-encore": "^0.20.0",
 "webpack-notifier": "^1.6.0"
 },
 "scripts": {


    "dev-server": "encore dev-server",
 "dev": "encore dev",
 "watch": "encore dev --watch",
 "build": "encore production"
 }
 }
 package.json
  11. // webpack.config.js
 var Encore = require('@symfony/webpack-encore');
 
 Encore
 .setOutputPath('public/build/')
 .setPublicPath('/build')


    
 .addEntry('app', './assets/js/app.js')
 ;
 
 module.exports = Encore.getWebpackConfig();

  12. assets/js/app.js require('../css/app.css');
 
 console.log('Edit me in assets/js/app.js');
 assets/css/app.css body {


    background-color: pink;
 }

  13. A new node_modules/ directory is now FULL of dependencies

  14. None
  15. <html>
 <head>
 {% block stylesheets %}
 <link rel="stylesheet" href="{{ asset('build/app.css')

    }}">
 {% endblock %}
 </head>
 <body>
 {% block body %}{% endblock %}
 {% block javascripts %}
 <script src="{{ asset('build/app.js') }}"></script>
 {% endblock %}
 </body>
 </html>

  16. Best Practice: Make your CSS a Dependency of your JavaScript

    “app” @weaverryan
  17. Best Practice: Create one “entry” that is included in your

    layout @weaverryan
  18. @weaverryan Game-Changing Feature: Modules & imports

  19. // assets/js/app.js
 const getRandomWord = require('./common/random_word');
 
 console.log('The word of

    the day is: '+getRandomWord())
 // assets/js/common/random_word.js
 module.exports = function() {
 const words = ['foo', 'bar', 'baz'];
 
 return words[Math.floor(Math.random() * words.length)];
 }

  20. Or… @weaverryan

  21. // assets/js/app.js
 - const getRandomWord = require(‘./common/random_word'); + import getRandomWord

    from './common/random_word';
 
 console.log('The word of the day is: '+getRandomWord())
 // assets/js/common/random_word.js
 - module.exports = function() { + export default function() {
 const words = ['foo', 'bar', 'baz'];
 
 return words[Math.floor(Math.random() * words.length)];
 }

  22. Best Practice: Organize your code into reusable “modules”. Your entry

    file is your “controller” @weaverryan
  23. // assets/js/app.js
 import displayRandomWord from './common/display_random_word';
 
 const el =

    document.getElementById('lucky-word');
 displayRandomWord(el);
 // assets/js/common/display_random_word.js
 import getRandomWord from './random_word';
 
 export default function displayRandomWord(el)
 {
 const randomWord = getRandomWord();
 el.innerHTML = 'The lucky word is '+randomWord;
 }
 // assets/js/common/random_word.js
 export default function() {
 const words = ['foo', 'bar', 'baz'];
 
 return words[Math.floor(Math.random() * words.length)];
 }

  24. Problem: My displayRandomWord() function outputs an HTML element that needs

    some CSS @weaverryan
  25. /* assets/css/app.css */
 body {
 background-color: lightgray;
 }
 
 .lucky-word

    {
 font-weight: bold;
 } Sure… that technically works great…
  26. // assets/js/common/display_random_word.js
 import getRandomWord from './random_word';
 import '../../css/lucky_word.css';
 
 export

    default function displayRandomWord(el)
 {
 const randomWord = getRandomWord();
 el.innerHTML = `The lucky word is
 <span class="lucky-word">${randomWord}</span>`;
 }
 /* assets/css/lucky_word.css */
 .lucky-word {
 font-weight: bold;
 }
  27. Best Practice: Each module is its own, unique snowflake: design

    each to import its own dependencies @weaverryan
  28. /* assets/css/lucky_word.css */
 @font-face {
 font-family: 'Sweet Pea';
 src: url('../fonts/Sweet

    Pea.ttf');
 }
 
 .lucky-word {
 font-weight: bold;
 font-family: 'Sweet Pea';
 }
  29. > yarn encore dev --watch

  30. Cool Story: But I’m not building an SPA: I have

    multiple pages @weaverryan
  31. // assets/js/checkout.js
 import '../css/checkout.css'
 
 import CheckoutApp from './checkout/CheckoutApp';
 


    const checkoutApp = new CheckoutApp();
 checkoutApp.render();
 Page-Specific JS & CSS
  32. // webpack.config.js
 var Encore = require('@symfony/webpack-encore');
 
 Encore
 .setOutputPath('public/build/')
 .setPublicPath('/build')


    
 .addEntry('app', './assets/js/app.js')
 .addEntry('checkout', './assets/js/checkout.js')
 
 // ...
 ;
 
 module.exports = Encore.getWebpackConfig();

  33. > yarn encore dev --watch

  34. {# templates/default/checkout.html.twig #}
 {% extends 'base.html.twig' %}
 
 {% block

    body %}
 <h1>Checkout!</h1>
 {% endblock %}
 
 {% block stylesheets %}
 {{ parent() }}
 
 <link rel=“stylesheet" href="{{ asset('build/checkout.css') }}">
 {% endblock %}
 
 {% block javascripts %}
 {{ parent() }}
 
 <script src="{{ asset('build/checkout.js') }}"></script>
 {% endblock %}
  35. Best Practice: Treat each entry, each “page” a standalone JavaScript

    app @weaverryan
  36. @weaverryan PART 2 jQuery Where things fall apart

  37. So you want jQuery? No problem! @weaverryan

  38. > yarn add jquery --dev // assets/js/app.js
 
 // ...


    import $ from 'jquery';
 
 $('a.external').on('click', function() {
 // ...
 });
 Loads from node_modules/
  39. Best Practice: Require jquery like any other module @weaverryan

  40. What’s the difference between: 
 import $ from 'jquery';
 


    <script src="https://code.jquery.com/jquery.js"> </script> and… Everything.
  41. // jquery.js
 
 jQuery = // a bunch of code

    to create the jquery object
 
 if ( typeof module.exports === "object" ) {
 module.exports = jQuery;
 } else {
 window.jQuery = jQuery;
 window.$ = jQuery;
 } In Webpack? NO GLOBAL VARIABLES
  42. @weaverryan All (most) JavaScript libraries change their behavior based *how*

    they are included
  43. <script src="/build/app.js"></script>
 <script src="/build/checkout.js"></script> // assets/js/app.js
 import $ from 'jquery';


    // …
 // assets/js/checkout.js
 $('a.external').on('click', function() {
 // ...
 });

  44. <script src="/build/app.js"></script>
 <script src=“/build/checkout.js"></script> <script>
 $('a.external').on('click', function() {
 
 });

    
 </script> // assets/js/app.js
 import $ from 'jquery';
 // …

  45. jQuery Plugins? Things get weirder… @weaverryan

  46. > yarn add bootstrap --dev // assets/js/app.js
 import $ from

    'jquery';
 import 'bootstrap';
 
 $('.add-tooltip').tooltip();
 jQuery plugins do not export anything … they modify the jQuery object & add things
  47. // bootstrap.js
 if(typeof module !== 'undefined') {
 var jQuery =

    require('jquery');
 } else {
 var jQuery = window.jQuery;
 }
 
 // bootstrap modifies jQuery many jQuery plugins correctly require the jquery module
  48. Some jQuery plugins are *not* written correctly @weaverryan

  49. > yarn add jquery-tags-input --dev // app.js
 import $ from

    'jquery';
 import 'jquery-tags-input';
 import 'jquery-tags-input/dist/jquery.tagsinput.css';
 
 $('#tags').tagsInput(); you can require specific files inside node_modules/jquery-tags-input
  50. // jquery.tagsinput.js
 jQuery.fn.tagsInput = function() {
 // ...
 } …

    some jQuery plugins assume jQuery will always be available globally
  51. @weaverryan Fix the bad library: Encore.autoProvidejQuery()

  52. // webpack.config.js
 var Encore = require('@symfony/webpack-encore');
 
 Encore
 // ...


    .autoProvidejQuery()
 ;
 
 module.exports = Encore.getWebpackConfig();
 // jquery.tagsinput.js
 + require('jquery').fn.tagsInput = function() {
 - jQuery.fn.tagsInput = function() {
 // ...
 } Webpack rewrites the bad code
  53. Brilliant! We fixed the bad code! … now WE can

    write bad code too! @weaverryan
  54. // assets/js/checkout.js
 
 // jQuery was never required
 // "$"

    should be an undefined variable // but this WILL work
 $('a.external').on('click', function() {
 
 }); Ryan says: BOOOOOOOOO
  55. <script>
 // in a template
 // still does NOT work


    $('a.external').on('click', function() {
 
 });
 </script>
  56. Best Practice: (duplicate form earlier!) Treat each entry, each “page”

    a standalone JavaScript app @weaverryan
  57. <script src="{{ asset('build/shared.js') }}"></script>
 <script src="{{ asset('build/vendor.js') }}"></script>
 <script src="{{

    asset('build/jquery.tag.js') }}"></script>
 <script src="{{ asset('build/bootstrap.js') }}"></script>
 <script src="{{ asset('build/custom_modal.js') }}"></script>
 <script src="{{ asset('build/top_nav.js') }}"></script>
 
 <link rel="{{ asset('build/layout.css') }}">
 <link rel="{{ asset('build/header.css') }}">
  58. var Encore = require('@symfony/webpack-encore');
 
 Encore
 .addEntry('shared', './assets/js/shared.js')
 .addEntry('vendor', ['jquery',

    'react'])
 .addEntry('jquery.tags', 'jquery-tags-input')
 .addEntry('bootstrap', 'bootstrap')
 .addEntry('custom_modal', './assets/custom_modal.js')
 .addEntry('top_nav', './assets/js/top_nav.js')
 .addStyleEntry('layout', './assets/css/layout.css')
 .addStyleEntry('header', './assets/css/header.css')
 ;
 
 module.exports = Encore.getWebpackConfig();
 Nope! 1 entry per page. Each entry works independently and requires what it needs.
  59. var Encore = require('@symfony/webpack-encore');
 
 Encore
 .addEntry('shared', './assets/js/shared.js')
 .addEntry('vendor', ['jquery',

    'react'])
 .addEntry('jquery.tags', 'jquery-tags-input')
 .addEntry('bootstrap', 'bootstrap')
 .addEntry('custom_modal', './assets/custom_modal.js')
 .addEntry('top_nav', './assets/js/top_nav.js')
 .addStyleEntry('layout', './assets/css/layout.css')
 .addStyleEntry('header', './assets/css/header.css')
 ;
 
 module.exports = Encore.getWebpackConfig();
 addStyleEntry() is a hack
  60. @weaverryan PART 3 Creating an Efficient Build Code Splitting

  61. Yo! Don’t worry, I’ll build and wire up the final

    code correctly for you. It’s going to be awesome!
  62. > yarn encore production } Woh! So many jQueries!

  63. // webpack.config.js
 Encore
 // ...
 - .addEntry('app', './assets/js/app.js')
 + .createSharedEntry('app',

    './assets/js/app.js')
 .addEntry('checkout', './assets/js/checkout.js')
 ; Anything in app.js will not be repackaged in any other entry files
  64. @weaverryan Webpack 4 The shared entry is dead!

  65. // webpack.config.js
 Encore
 // ...
 - .addEntry('app', './assets/js/app.js')
 + .createSharedEntry('app',

    './assets/js/app.js')
 .addEntry('checkout', './assets/js/checkout.js')
 ; This will still work in Encore, but only because we hack it to work.
  66. @weaverryan So now what?

  67. @weaverryan Introducing… Webpack Encore 0.21.0

  68. @weaverryan > Webpack 4 support > Runtime chunk > ”Split

    Chunks” support > browserlist support in package.json > Dynamic imports (code splitting) syntax support > Smarter version checking system > Babel 7 out of the box Webpack Encore 0.21.0
  69. @weaverryan Runtime Chunk If you have more than 1 entry,

    you should probably use a runtime chunk.
  70. // webpack.config.js
 Encore
 // ...
 .enableSingleRuntimeChunk()
 ;
 
 module.exports =

    Encore.getWebpackConfig();
 {# templates/base.html.twig #}
 {% block javascripts %}
 <script src="{{ asset('build/runtime.js') }}"></script>
 <script src="{{ asset('build/app.js') }}"></script>
 {% endblock %}
 With a runtime chunk, if multiple entry files require the same module, they receive the same object
  71. // app.js
 var $ = require('jquery');
 require('bootstrap');
 
 // checkout.js


    var $ = require('jquery');
 $('.item').tooltip(); Will this work? Does the jquery module have the tooltip in checkout.js? enableSingleRuntimeChunk() Yes :D disableSingleRuntimeChunk() No :)
  72. Ok cool, what about optimizing my build? @weaverryan

  73. // webpack.config.js
 Encore
 // ...
 + .addEntry('app', './assets/js/app.js')
 - .createSharedEntry('app',

    './assets/js/app.js')
 .addEntry('checkout', './assets/js/checkout.js')
 ; Go back to the boring, original setup.
  74. // webpack.config.js
 Encore
 // ...
 + .addEntry('app', './assets/js/app.js')
 - .createSharedEntry('app',

    './assets/js/app.js')
 .addEntry('checkout', ‘./assets/js/checkout.js') .splitEntryChunks()
 ; But now enable splitEntryChunks()
  75. > yarn encore production

  76. {% block javascripts %}
 <script src="{{ asset('build/runtime.js') }}"></script>
 <script src="{{

    asset('build/vendor~a33029de.js') }}"></script>
 <script src="{{ asset('build/vendor~ea1103fa.js') }}"></script>
 <script src="{{ asset('build/app.js') }}"></script>
 {% endblock %} All of these files are needed to for the “app” entry to function How are we supposed to know what script & link tags we need?
  77. @weaverryan Introducing… WebpackEncoreBundle

  78. None
  79. composer require symfony/webpack-encore-bundle (will be included in the “encore” pack

    soon) {# base.html.twig #}
 {% block stylesheets %}
 {{ encore_entry_link_tags('app') }}
 {% endblock %}
 
 {% block javascripts %}
 {{ encore_entry_script_tags('app') }}
 {% endblock %}

  80. {# checkout.html.twig #}
 {% block stylesheets %}
 {{ parent() }}


    
 {{ encore_entry_link_tags('checkout') }}
 {% endblock %}
 
 {% block javascripts %}
 {{ parent() }}
 
 {{ encore_entry_script_tags('checkout') }}
 {% endblock %}
  81. {
 "app": {
 "js": [
 "build/runtime.js",
 "build/vendors~a33029de.js",
 "build/vendors~ea1103fa.js",
 "build/app.js"
 ],


    "css": [
 "build/vendors~ea1103fa.css",
 "build/app.css"
 ]
 },
 "checkout": {
 "js": [
 "build/runtime.js",
 "build/vendors~a33029de.js",
 "build/vendors~11e1498d.js",
 "build/checkout.js"
 ],
 "css": [
 "build/checkout.css"
 ]
 }
 }
  82. Dynamic Code Splitting @weaverryan

  83. // app.js
 $('a.external').on('click', function(e) {
 import('./common/external_linker').then(linker => {
 linker.default(e.currentTarget);
 });


    });
 // assets/js/common/external_linker.js
 import $ from 'jquery';
 import '../../css/external_link.css';
 
 export default function(linkEl) {
 $(linkEl).addClass('clicked');
 } The code from external_linker.js will not be included in the app.js It will be loaded via AJAX when needed (including the CSS file!)
  84. @weaverryan Putting it all together

  85. Best Practice: Create one “entry” that is included in your

    layout @weaverryan
  86. Best Practice: Treat each entry, each “page” a standalone JavaScript

    app @weaverryan
  87. Best Practice: Organize your code into reusable “modules”. Your entry

    file is your “controller” @weaverryan
  88. Best Practice: Each module is its own, unique snowflake: design

    each to import its own dependencies @weaverryan
  89. Best Practice: Make your CSS a Dependency of your JavaScript

    “app” @weaverryan
  90. Best Practice: Require jquery like any other module (even if

    autoProvidejQuery allows you to cheat) @weaverryan
  91. Best Practice: Use the new splitEntryChunks() to make your multi-entry

    build performant @weaverryan
  92. Thank You! Ryan Weaver @weaverryan https://symfonycasts.com/encore