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

Webpack Encore: Lessons learned 1 year later

Avatar for weaverryan 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.

Avatar for weaverryan

weaverryan

December 07, 2018
Tweet

More Decks by weaverryan

Other Decks in Technology

Transcript

  1. > 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. @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. @weaverryan AND A totally new (and modern) way of thinking

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

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

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

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

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

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

    {
 font-weight: bold;
 } Sure… that technically works great…
  12. // 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;
 }
  13. Best Practice: Each module is its own, unique snowflake: design

    each to import its own dependencies @weaverryan
  14. /* 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';
 }
  15. // assets/js/checkout.js
 import '../css/checkout.css'
 
 import CheckoutApp from './checkout/CheckoutApp';
 


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

  17. {# 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 %}
  18. > yarn add jquery --dev // assets/js/app.js
 
 // ...


    import $ from 'jquery';
 
 $('a.external').on('click', function() {
 // ...
 });
 Loads from node_modules/
  19. What’s the difference between: 
 import $ from 'jquery';
 


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

  22. > 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
  23. // 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
  24. > 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
  25. // jquery.tagsinput.js
 jQuery.fn.tagsInput = function() {
 // ...
 } …

    some jQuery plugins assume jQuery will always be available globally
  26. // 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
  27. Brilliant! We fixed the bad code! … now WE can

    write bad code too! @weaverryan
  28. // 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
  29. <script>
 // in a template
 // still does NOT work


    $('a.external').on('click', function() {
 
 });
 </script>
  30. <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') }}">
  31. 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.
  32. 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
  33. Yo! Don’t worry, I’ll build and wire up the final

    code correctly for you. It’s going to be awesome!
  34. // 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
  35. // 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.
  36. @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
  37. @weaverryan Runtime Chunk If you have more than 1 entry,

    you should probably use a runtime chunk.
  38. // 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
  39. // 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 :)
  40. // 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.
  41. // 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()
  42. {% 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?
  43. 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 %}

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


    
 {{ encore_entry_link_tags('checkout') }}
 {% endblock %}
 
 {% block javascripts %}
 {{ parent() }}
 
 {{ encore_entry_script_tags('checkout') }}
 {% endblock %}
  45. {
 "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"
 ]
 }
 }
  46. // 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!)
  47. Best Practice: Each module is its own, unique snowflake: design

    each to import its own dependencies @weaverryan
  48. Best Practice: Require jquery like any other module (even if

    autoProvidejQuery allows you to cheat) @weaverryan