$30 off During Our Annual Pro Sale. View Details »

Webpack Encore: Lessons learned 1 year later

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.

weaverryan

December 07, 2018
Tweet

More Decks by weaverryan

Other Decks in Technology

Transcript

  1. Webpack Encore
    lessons learned
    1 year later

    View Slide

  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

    View Slide

  3. @weaverryan
    PART 1
    Webpack & Encore
    Powerful and Easy!
    (until they’re not)

    View Slide

  4. What *is* Webpack?
    @weaverryan

    View Slide

  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

    View Slide

  6. @weaverryan
    AND
    A totally new (and modern) way of
    thinking about your assets.
    Hint: Unlearn everything you know
    about JavaScript and global variables

    View Slide

  7. @weaverryan
    Encore is a Node library
    … but we recommend installing
    the “encore” Composer package
    (it comes with a nice recipe)

    View Slide

  8. composer require encore --dev

    View Slide

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

    View Slide

  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

    View Slide

  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();


    View Slide

  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;

    }


    View Slide

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

    View Slide

  14. View Slide



  15. {% block stylesheets %}

    href="{{ asset('build/app.css') }}">

    {% endblock %}



    {% block body %}{% endblock %}

    {% block javascripts %}


    {% endblock %}



    View Slide

  16. Best Practice:
    Make your CSS a Dependency
    of your JavaScript “app”
    @weaverryan

    View Slide

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

    View Slide

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

    View Slide

  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)];

    }


    View Slide

  20. Or…
    @weaverryan

    View Slide

  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)];

    }


    View Slide

  22. Best Practice:
    Organize your code into
    reusable “modules”. Your
    entry file is your “controller”
    @weaverryan

    View Slide

  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)];

    }


    View Slide

  24. Problem:
    My displayRandomWord()
    function outputs an HTML
    element that needs some CSS
    @weaverryan

    View Slide

  25. /* assets/css/app.css */

    body {

    background-color: lightgray;

    }


    .lucky-word {

    font-weight: bold;

    }
    Sure… that technically works great…

    View Slide

  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

    ${randomWord}`;

    }

    /* assets/css/lucky_word.css */

    .lucky-word {

    font-weight: bold;

    }

    View Slide

  27. Best Practice:
    Each module is its own, unique
    snowflake: design each to
    import its own dependencies
    @weaverryan

    View Slide

  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';

    }

    View Slide

  29. > yarn encore dev --watch

    View Slide

  30. Cool Story:
    But I’m not building an SPA:
    I have multiple pages
    @weaverryan

    View Slide

  31. // assets/js/checkout.js

    import '../css/checkout.css'


    import CheckoutApp from './checkout/CheckoutApp';


    const checkoutApp = new CheckoutApp();

    checkoutApp.render();

    Page-Specific JS & CSS

    View Slide

  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();


    View Slide

  33. > yarn encore dev --watch

    View Slide

  34. {# templates/default/checkout.html.twig #}

    {% extends 'base.html.twig' %}


    {% block body %}

    Checkout!

    {% endblock %}


    {% block stylesheets %}

    {{ parent() }}


    href="{{ asset('build/checkout.css') }}">

    {% endblock %}


    {% block javascripts %}

    {{ parent() }}



    {% endblock %}

    View Slide

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

    View Slide

  36. @weaverryan
    PART 2
    jQuery
    Where things fall apart

    View Slide

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

    View Slide

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


    // ...

    import $ from 'jquery';


    $('a.external').on('click', function() {

    // ...

    });

    Loads from node_modules/

    View Slide

  39. Best Practice:
    Require jquery like any other
    module
    @weaverryan

    View Slide

  40. What’s the difference between:

    import $ from 'jquery';


    src="https://code.jquery.com/jquery.js">

    and…
    Everything.

    View Slide

  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

    View Slide

  42. @weaverryan
    All (most) JavaScript libraries
    change their behavior based
    *how* they are included

    View Slide



  43. // assets/js/app.js

    import $ from 'jquery';

    // …

    // assets/js/checkout.js

    $('a.external').on('click', function() {

    // ...

    });


    View Slide



  44. 
<br/>$('a.external').on('click', function() {
<br/>
<br/>}); 
<br/>
    // assets/js/app.js

    import $ from 'jquery';

    // …


    View Slide

  45. jQuery Plugins?
    Things get weirder…
    @weaverryan

    View Slide

  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

    View Slide

  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

    View Slide

  48. Some jQuery plugins
    are *not* written correctly
    @weaverryan

    View Slide

  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

    View Slide

  50. // jquery.tagsinput.js

    jQuery.fn.tagsInput = function() {

    // ...

    }
    … some jQuery plugins assume jQuery
    will always be available globally

    View Slide

  51. @weaverryan
    Fix the bad library:
    Encore.autoProvidejQuery()

    View Slide

  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

    View Slide

  53. Brilliant! We fixed the bad code!
    … now WE can write bad code too!
    @weaverryan

    View Slide

  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

    View Slide

  55. 
<br/>// in a template
<br/>// still does NOT work
<br/>$('a.external').on('click', function() {
<br/>
<br/>});
<br/>

    View Slide

  56. Best Practice:
    (duplicate form earlier!)
    Treat each entry, each “page” a
    standalone JavaScript app
    @weaverryan

    View Slide










  57. View Slide

  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.

    View Slide

  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

    View Slide

  60. @weaverryan
    PART 3
    Creating an Efficient Build
    Code Splitting

    View Slide

  61. Yo! Don’t worry, I’ll
    build and wire up the
    final code correctly for
    you. It’s going to be
    awesome!

    View Slide

  62. > yarn encore production
    } Woh! So many jQueries!

    View Slide

  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

    View Slide

  64. @weaverryan
    Webpack 4
    The shared entry is dead!

    View Slide

  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.

    View Slide

  66. @weaverryan
    So now what?

    View Slide

  67. @weaverryan
    Introducing…
    Webpack Encore 0.21.0

    View Slide

  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

    View Slide

  69. @weaverryan
    Runtime Chunk
    If you have more than 1 entry, you should
    probably use a runtime chunk.

    View Slide

  70. // webpack.config.js

    Encore

    // ...

    .enableSingleRuntimeChunk()

    ;


    module.exports = Encore.getWebpackConfig();

    {# templates/base.html.twig #}

    {% block javascripts %}



    {% endblock %}

    With a runtime chunk, if multiple entry
    files require the same module,
    they receive the same object

    View Slide

  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 :)

    View Slide

  72. Ok cool,
    what about optimizing my build?
    @weaverryan

    View Slide

  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.

    View Slide

  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()

    View Slide

  75. > yarn encore production

    View Slide

  76. {% block javascripts %}





    {% 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?

    View Slide

  77. @weaverryan
    Introducing…
    WebpackEncoreBundle

    View Slide

  78. View Slide

  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 %}


    View Slide

  80. {# checkout.html.twig #}

    {% block stylesheets %}

    {{ parent() }}


    {{ encore_entry_link_tags('checkout') }}

    {% endblock %}


    {% block javascripts %}

    {{ parent() }}


    {{ encore_entry_script_tags('checkout') }}

    {% endblock %}

    View Slide

  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"

    ]

    }

    }

    View Slide

  82. Dynamic Code Splitting
    @weaverryan

    View Slide

  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!)

    View Slide

  84. @weaverryan
    Putting it all together

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  88. Best Practice:
    Each module is its own, unique
    snowflake: design each to
    import its own dependencies
    @weaverryan

    View Slide

  89. Best Practice:
    Make your CSS a Dependency
    of your JavaScript “app”
    @weaverryan

    View Slide

  90. Best Practice:
    Require jquery like any other
    module
    (even if autoProvidejQuery
    allows you to cheat)
    @weaverryan

    View Slide

  91. Best Practice:
    Use the new splitEntryChunks()
    to make your multi-entry
    build performant
    @weaverryan

    View Slide

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

    View Slide