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. 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
  2. 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
  3. 6.

    @weaverryan AND A totally new (and modern) way of thinking

    about your assets. Hint: Unlearn everything you know about JavaScript and global variables
  4. 7.

    @weaverryan Encore is a Node library … but we recommend

    installing the “encore” Composer package (it comes with a nice recipe)
  5. 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
  6. 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();

  7. 14.
  8. 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>

  9. 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)];
 }

  10. 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)];
 }

  11. 22.
  12. 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)];
 }

  13. 25.

    /* assets/css/app.css */
 body {
 background-color: lightgray;
 }
 
 .lucky-word

    {
 font-weight: bold;
 } Sure… that technically works great…
  14. 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;
 }
  15. 27.

    Best Practice: Each module is its own, unique snowflake: design

    each to import its own dependencies @weaverryan
  16. 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';
 }
  17. 30.
  18. 31.

    // assets/js/checkout.js
 import '../css/checkout.css'
 
 import CheckoutApp from './checkout/CheckoutApp';
 


    const checkoutApp = new CheckoutApp();
 checkoutApp.render();
 Page-Specific JS & CSS
  19. 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();

  20. 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 %}
  21. 38.

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


    import $ from 'jquery';
 
 $('a.external').on('click', function() {
 // ...
 });
 Loads from node_modules/
  22. 40.

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


    <script src="https://code.jquery.com/jquery.js"> </script> and… Everything.
  23. 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
  24. 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() {
 // ...
 });

  25. 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
  26. 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
  27. 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
  28. 50.

    // jquery.tagsinput.js
 jQuery.fn.tagsInput = function() {
 // ...
 } …

    some jQuery plugins assume jQuery will always be available globally
  29. 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
  30. 53.

    Brilliant! We fixed the bad code! … now WE can

    write bad code too! @weaverryan
  31. 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
  32. 55.

    <script>
 // in a template
 // still does NOT work


    $('a.external').on('click', function() {
 
 });
 </script>
  33. 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') }}">
  34. 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.
  35. 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
  36. 61.

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

    code correctly for you. It’s going to be awesome!
  37. 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
  38. 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.
  39. 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
  40. 69.

    @weaverryan Runtime Chunk If you have more than 1 entry,

    you should probably use a runtime chunk.
  41. 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
  42. 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 :)
  43. 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.
  44. 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()
  45. 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?
  46. 78.
  47. 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 %}

  48. 80.

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


    
 {{ encore_entry_link_tags('checkout') }}
 {% endblock %}
 
 {% block javascripts %}
 {{ parent() }}
 
 {{ encore_entry_script_tags('checkout') }}
 {% endblock %}
  49. 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"
 ]
 }
 }
  50. 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!)
  51. 87.
  52. 88.

    Best Practice: Each module is its own, unique snowflake: design

    each to import its own dependencies @weaverryan
  53. 90.

    Best Practice: Require jquery like any other module (even if

    autoProvidejQuery allows you to cheat) @weaverryan