Slide 1

Slide 1 text

Webpack Encore lessons learned 1 year later

Slide 2

Slide 2 text

> 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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

What *is* Webpack? @weaverryan

Slide 5

Slide 5 text

@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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

composer require encore --dev

Slide 9

Slide 9 text

dependencies Webpack config a few files to get you started {

Slide 10

Slide 10 text

{
 "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

Slide 11

Slide 11 text

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


Slide 12

Slide 12 text

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


Slide 13

Slide 13 text

A new node_modules/ directory is now FULL of dependencies

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text


 
 {% block stylesheets %}
 
 {% endblock %}
 
 
 {% block body %}{% endblock %}
 {% block javascripts %}
 
 {% endblock %}
 
 


Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

@weaverryan Game-Changing Feature: Modules & imports

Slide 19

Slide 19 text

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


Slide 20

Slide 20 text

Or… @weaverryan

Slide 21

Slide 21 text

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


Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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


Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

/* assets/css/app.css */
 body {
 background-color: lightgray;
 }
 
 .lucky-word {
 font-weight: bold;
 } Sure… that technically works great…

Slide 26

Slide 26 text

// 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;
 }

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

/* 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';
 }

Slide 29

Slide 29 text

> yarn encore dev --watch

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

// assets/js/checkout.js
 import '../css/checkout.css'
 
 import CheckoutApp from './checkout/CheckoutApp';
 
 const checkoutApp = new CheckoutApp();
 checkoutApp.render();
 Page-Specific JS & CSS

Slide 32

Slide 32 text

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


Slide 33

Slide 33 text

> yarn encore dev --watch

Slide 34

Slide 34 text

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


Checkout!


 {% endblock %}
 
 {% block stylesheets %}
 {{ parent() }}
 
 
 {% endblock %}
 
 {% block javascripts %}
 {{ parent() }}
 
 
 {% endblock %}

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

@weaverryan PART 2 jQuery Where things fall apart

Slide 37

Slide 37 text

So you want jQuery? No problem! @weaverryan

Slide 38

Slide 38 text

> yarn add jquery --dev // assets/js/app.js
 
 // ...
 import $ from 'jquery';
 
 $('a.external').on('click', function() {
 // ...
 });
 Loads from node_modules/

Slide 39

Slide 39 text

Best Practice: Require jquery like any other module @weaverryan

Slide 40

Slide 40 text

What’s the difference between: 
 import $ from 'jquery';
 
 and… Everything.

Slide 41

Slide 41 text

// 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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text


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


Slide 44

Slide 44 text


 
 $('a.external').on('click', function() {
 
 }); 
 // assets/js/app.js
 import $ from 'jquery';
 // …


Slide 45

Slide 45 text

jQuery Plugins? Things get weirder… @weaverryan

Slide 46

Slide 46 text

> 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

Slide 47

Slide 47 text

// 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

Slide 48

Slide 48 text

Some jQuery plugins are *not* written correctly @weaverryan

Slide 49

Slide 49 text

> 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

Slide 50

Slide 50 text

// jquery.tagsinput.js
 jQuery.fn.tagsInput = function() {
 // ...
 } … some jQuery plugins assume jQuery will always be available globally

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

// 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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

// 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

Slide 55

Slide 55 text


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


Slide 56

Slide 56 text

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

Slide 57

Slide 57 text


 
 
 
 
 
 
 


Slide 58

Slide 58 text

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.

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

@weaverryan PART 3 Creating an Efficient Build Code Splitting

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

> yarn encore production } Woh! So many jQueries!

Slide 63

Slide 63 text

// 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

Slide 64

Slide 64 text

@weaverryan Webpack 4 The shared entry is dead!

Slide 65

Slide 65 text

// 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.

Slide 66

Slide 66 text

@weaverryan So now what?

Slide 67

Slide 67 text

@weaverryan Introducing… Webpack Encore 0.21.0

Slide 68

Slide 68 text

@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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

// 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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

Ok cool, what about optimizing my build? @weaverryan

Slide 73

Slide 73 text

// 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.

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

> yarn encore production

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

@weaverryan Introducing… WebpackEncoreBundle

Slide 78

Slide 78 text

No content

Slide 79

Slide 79 text

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


Slide 80

Slide 80 text

{# checkout.html.twig #}
 {% block stylesheets %}
 {{ parent() }}
 
 {{ encore_entry_link_tags('checkout') }}
 {% endblock %}
 
 {% block javascripts %}
 {{ parent() }}
 
 {{ encore_entry_script_tags('checkout') }}
 {% endblock %}

Slide 81

Slide 81 text

{
 "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"
 ]
 }
 }

Slide 82

Slide 82 text

Dynamic Code Splitting @weaverryan

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

@weaverryan Putting it all together

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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