Scroll down to the script below, click on any sentence (including terminal blocks!) to jump to that spot in the video!
SymfonyCon 2018 Presentation by Ryan Weaver
In this talk, we'll quickly learn the basics of Webpack Encore, then, turn to the lessons we've learned over the past year: answer popular questions and explore common problems people run into when moving to Encore. We'll also dive into a host of lesser-known best practices that you can follow to make sure your frontend coding is as streamlined as possible. A modern frontend build system: all in the time of one talk!
How many people have seen the SymfonyCasts videos? I was going to explain that this is really my voice and the miracles of editing. It's really incredible what we can with that audio. Okay. I am Ryan. Hello nice to meet you. I'm the the lead of the documentation team. Also, a writer for KnpUniversity... wait SymfonyCasts.
Changing a domain is harder than you think. I'm basically, if you've been around Symfony for a while, you probably know me, I'm a Symfony evangelist / fanboy. The husband of the much more talented and much more popular at the Symfony conferences, Leanna. How many people have met Leanna? Okay, yeah, okay. That's so great, because that means, there's many of you have such a great opportunity today to meet Leanna, give her a jumping high-five, like this style.
Leanna, can you raise your hand, just so everyone sees, right here? Yeah, so Leanna's right here, and you can find her afterwards. She's going to come up afterwards and wait for her high-fives. And the father to my much more handsome, charming son Beckett. So now, I have a kid, so now I get to show you pictures of my kid in the beginning. Whether you want to or not, so that's my son, Beckett.
Okay, so let's talk about Webpack and Encore. We are going to do just a little bit of explanation in the beginning, then talk about some lessons that we've learned over the past year and a half. And also, some new features, because a new version of Encore came out, only one month ago, and there are actually some really exciting changes that we've made in that version.
require statements, so you can have one file
app.js, require some other files, sort of like we used to do in PHP, require some other files, and that's it. It's just going to turn all of those into one
app.js, and one
app.css file. That's kind of cool.
So, Encore is just a wrapper around Webpack. It's actually a Webpack configuration generator. It's a Node library, but we recommend installing it via a Composer package. I'll explain why later. There is a little bit of PHP in Encore, but it's mostly Node. Of course, it has a nice recipe when you install it:
composer require encore.
Oh, actually see, things changed. That's slightly out of date because we should not have the
––dev there anymore. I'll explain why later. There is a small bit of code that you do want to run at run-time now.
composer require encore, okay, it downloads a few things. This is what it gives you. It gives you a
So, here is what the
package.json looks like. It just requires two packages: Encore and the
webpack-notifier is just cool because it makes cool desktop notifications when your build finishes. It's like ding, your build is done! It just makes it more fun. That's it.
I'm saying go read that
./assets/js/app.js file. The first key
app? That could be anything: that will ultimately control the name of your output file. If that first key, that first argument was
foo, we would end up with a
But we're pointing it at
yarn install. Yarn is, there are two package managers in Node. Yarn is one of them. This is basically me running
For some reason, yarn lets you be so incredibly lazy that you can just say,
yarn and it installs for you. This is me running
yarn install and the end result is... boom! We end up with tons of files in a
node_modules/ directory, which is the
vendor/ directory. At this point, we have basically just required two packages from Node, and installed them. That's it.
To actually run Encore, you're going to run
yarn encore dev. There is also a
yarn encore production you'll see later: that's the mode that minifies all of your files and does other optimizations. Of course, there is a
--watch so you don't have to run every time. Every time you modify file, this is going to re-output your files.
The end result of this is that you get two files. Actually, you guys are probably good at counting, you get four files. The only important ones yet are the
app.css. What happened was: we told the Webpack to look at our
app.js, and one
app.css file. Then we just need to include those in our template, completely like normal.
script tag, or you need to go into some build system and remember to say, oh, now concatenate this file too, make sure they are in the correct order, all that kind of thing. It just didn't make sense. It was difficult to split your files.
With Webpack, you can actually program correctly. You don't think about the packaging of your files, you just think about: if I'm in one file, and I'd like to use another file, I require it. Very much like, again, I don't mean this to make it sound like the require statement is old, but in PHP, very much like we used to do in PHP. If you needed something, you required that file instead of just expecting it to be globally available.
app.js file, and that
app.js file is included in our
app.js on top, and then I'm just going to require another file and use its function.
The key thing is, in the second file, is the
Notice there is no
.js on the filename, but you don't need to. That's why you see it not there.
Or, to make things a little bit more complicated, interesting. You see the
require statement a lot, but the
require statement is not cool anymore. You're supposed to now use this
import keyword, and this
export keyword. The reason I'm telling you this is so that you know that they are exactly the same. They are just two syntaxes that do the exact same thing. The
export is actually more of a standard, so that's the one you should use. It has a little bit more flexibility, but really, they do the same thing. I'll typically use
export. It's all the same thing.
Here's a slightly more complex example. This is a file that's going to just call a function, but it's requiring this
displayRandomWord. Okay, let's look at
displayRandomWord. That's just another function. Sorry, that exports a function, but it imports another file, and that last file is actually another function which actually gives us the random word. Just an example of the type of thing we would do in PHP typically: isolate these things into small pieces.
Ah, but there's a problem. We've already talked about this. CSS dependencies. Actually, let me go back real quick. This works great, but now I'm going to re-factor my code so that the
displayRandomWord() function is now going to output HTML, and that HTML is going to have some classes on it. And dang it, I need to bring in the CSS file to style those classes. What we could do, because, remember our
app.js file requires
app.css, we could just go into
app.css and add our class there. That would make it into the final file.
app.js, require a CSS file. That doesn't mean that okay, great, let's put all of our CSS into that one file. No, we can actually break things down. We can have some very, very deep module - you don't even know what page it's going to be used on - but if that one module needs some CSS, like this, then require the CSS file. Then, no matter who uses this, you don't even care what page it's going to be used on, the CSS for that module is going to be included in the final output CSS.
Again, it's programming correctly. That's all it is. You're actually defining all of your requirements and dependencies in each little file.
The way you need to think about it is that each module, each file, is its own unique snowflake. Its own unique environment. When you look at a file, guys, I'm going to keep saying this, it's just like we program in PHP. If you went into it a file in PHP, you would never make any assumptions about what variables are available to you, or anything like that. If you need something inside of a class in PHP, you would make sure that you add a use statement, or use dependency injection, or something. It's the same thing with these modules. If you need something in a module, require it. Don't think: oh, it's okay, that CSS has already been required by this other module, which will be on the same page. That's bad. Put everything you need inside that one file.
In your CSS files you can use the
@import syntax. In the top of some CSS file, you could say
@import another CSS file. That's going to be read and parsed in exactly the same way. You could be in a CSS file and
@import a SASS file, and that's going to go get processed just fine, and get processed through SASS.
Alright, so cool story. But, so far, we've only been talking about one file, one
checkout.js next to
The result of this, since we added an entry called checkout, is that we end up with a
checkout.css with all the CSS we need. Then we just need to make sure that we put that on the checkout page. Inside of our checkout template, we're going to add a
script tag for the one file, and a CSS
link tag for the one CSS file.
checkout.js as its own app, which means that you need to, once again, require everything you need. If you need CSS require it, if you need some external library, require it. It's its own isolated environment. It's different than before when we think about: I'll include some script tags, and then these script tags will refer to some stuff that these script tags did. They're really two isolated environments.
Alright, so I want to talk a little bit about jQuery in case you're using it. Because jQuery is actually where things get weird. Actually, jQuery is also, because things get weird with jQuery, it's also a great example to understand how Webpack is doing some things internally.
Okay, so you want to use jQuery. This is one of the best things about the new way of developing. If you want jQuery, you just add it:
yarn add jquery –– dev. Just add it: just like we
composer require stuff. We don't need to download, go download something. We don't need to find a CDN. You just add it. Then you just require it or import it. That's it.
This is really great. Outside libraries are just actually very, very easy to use. This is not the problem with jQuery. This would work just fine. Anytime you import a module that does not start with a
./, it's importing it from the
node_modules directory. If you are importing a local file, you've noticed I'm probably using
./ before: that means a file next to me. If there's no
./, it's coming from the
jQuery is just like any other module, or React, or Vue. These are just modules: and you just require them, and they work like normal. Here's a key thing though, what's the difference between importing
$ from jQuery and a
script tag that you put in your layout for jQuery? The answer is ... everything. Even if that is literally pointing to the same file! Because, inside jQuery, and this is common to many, many modules, this is not a jQuery-specific thing, inside jQuery, it detects how it's being used and changes its behavior.
In Webpack, there are no global variables. Okay, just like PHP you can cheat and make global variables. But if you're programming things correctly, there are no global variables. When we include a script tag on the page, what does that do? It gives us a global
jQuery variable. If you require it, it detects that, and it uses the
A few minutes ago I talked about how you want your entry files, your application and your
checkouts.js to function like two separate applications. This is interesting. If we
import $ from 'jquery' in
app.js, and its script tag is included first, and then in
checkout.js we don't import it, but we just start using it, is that going to work? No. It's not going to work. Beautifully. That's going to be an "undefined variable $ in checkout.js": it's an uninitialized variable. In PHP, we would never expect that to work. It's not going to work. That is excellent.
This actually is little bit more interesting. Well actually, it's the same thing. Is this going to work? What I changed here is: okay, can I use the
$ in maybe a template? I just need, hey, I'm getting lazy, and I need to put a script tag in my template. Is that going to work? Nope. Same thing, there's no global variable, so that's not going to work either.
The weird thing about jQuery plug-ins is that they don't return anything. They modify jQuery and add something to it. They work fine with Webpack, but they're clearly from a universe that existed before webpack. Because it's just weird. It's weird to just import bootstrap and suddenly we have a
tooltip() function. It doesn't look obvious.
Internally, how does it do that? jQuery plug-ins, again, behave differently based on their environment. This is very important. If you think about the normal script tags, if you have a script tag for jQuery, then a script tag for bootstrap, how does bootstrap know where to find
jQuery so that it can modify it? It only has one option, expect it to be a global variable. It just looks for
jQuery as a global variable and it modifies it.
Fortunately, most modern libraries, most modern jQuery plug-ins now have code that looks like this. Where again, they detect their environment and they go: okay, I'm in a Webpack environment. What do they do? They actually require jQuery! One of the properties of the module system is that if two files require the same file, they get the same thing. It's a bit like Symfony's container. They'll get the same one instance of jQuery. It will actually modify jQuery.
Now, not all plug-ins are written correctly though. Here's a example. This is just a silly jQuery plug-in I found, tagsinput. The cool thing is I can say
yarn add, so I can just add this to my project by saying
node_modules/that directory/ the path of the CSS file. Sometimes you have to do a little digging to be like, what's the path to the CSS file? Once you find the path, you just require it. Done.
This doesn't work. We did the exact same thing as we did a second ago with bootstrap, but it doesn't work. So you get
jQuery is not defined. The reason is that, inside that module, it basically has code that looks like this. It just goes, jQuery. It just, hey, jQuery. It just expects it to be a global variable. And it's not anymore.
This is still written the old way. It has no code in it to make itself work in a module environment. It's broken. This is probably the most common and consistent question we get about Encore. It's not even an Encore thing, it's just a Webpack thing. It's important to understand how these jQuery plug-ins work, how they load, so that you can debug what's going on.
Ok so how do we fix that? Magic. We go back to our webpack config file, and we add one new line called
jQuery variable that has not been initialized, it replaces it with
require('jquery') and puts that in the output.
Yeah, right? It's incredible. This fixes it, and all of a sudden you can use these old things. Brilliant, we fixed somebody else's bad code! Be careful, because now we can write bad code too. This is now possible, so this is the downside of
.autoProvidejQuery(). So, don't turn it on if you don't need it. It now means you can go into any file and just start saying
jQuery. You can instantly start programming the old way with undefined variables. And Yay, Encore will also fix our bad code. If you turn it on, try to not do this, even though you can.
This is important, if I'm in a Twig template, and I put an inline script tag, that still does not work. Because
.autoProvidejQuery() does not give you a global
jQuery variable. It rewrites any code that's processed by Webpack. If you just have some random Twig template, there is still no
$ variable. This is just a repeat from earlier, even if you can cheat with the
autoProvidejQuery() plug-in, remember that each file is its own environment, so remember to require it.
This is another thing, so a couple of lessons from the past year that we've seen people do. How many people have had applications that look like this? Yeah, yeah, me too, yes. Because this is how we used to program. Require shared, and that creates these five global variables, and these 10 global functions, and then vendor.
Okay, now we have a
jQuery global variable. We just build on top of each other by adding more and more global variables. Then we have some script on the bottom that actually uses these. I have seen people basically take their old setup and just move it directly into Webpack. I understand, every file that I have in my old application is just an entry file, right? Because this is going to basically result in the same output file. Yeah, first one requires a file,
vendor is going to go actually require
react. Okay, good, this works, right? This does not work at all. Because what happens with that vendor entry, that vendor entry is going to require
jquery. Good for you, you just basically gave yourself a jQuery variable and never used it.
Another thing, another feature that we've had forever in Encore is the
app.js, we require the
Okay, now this is where things get really interesting and we're going to talk about some of the new features of Encore. I realize I'm running a little low on time, but I also realize there is a break afterwards. So sorry if I keep you for a few extra minutes.
Alright, so Webpack says:
hey, it's cool, just require stuff, I'll handle all the details, don't think about it. Really, it's like don't think about it. You just write code correctly, I'll take care of the rest. I'll build your files perfectly.
This is not the way we fix it anymore. I wanted to show you this because you'll see this. This is the way that we fixed this problem for the first year and half in Encore. Because this is the way that you fixed it in Webpack. Again, we're always leveraging Webpack features. Webpack 4 came out earlier this year and changed a lot of things. This is one of the features that is still available in Encore, but is not the recommended way to do it. Yeah, I'm not even going to talk about it, but this is a feature that existed before. It still exists, but don't use it.
Now what? What we do now? I said don't use that feature. Okay, so introducing Webpack Encore 0.21.0. Wow, what a great version number for a big release! Wow, congratulations! This brought a lot of new stuff. Webpack 4 support. I'm going to talk about a few of these. I'll show you a couple of these, so I'm not going to mention all of them here.
browserslist is a really good one. That allows you to put in your
This is a function that you can call now in your Webpack config file. Okay, cool, what does that do? It actually results in one additional file being output. A
runtime.js. It now means that you actually need two script tags when you enable this. Why do we do that? There's two reasons. One is actually a little bit for performance. I don't want to get into the weeds too much. The
runtime.js file contains some code that helps Webpack work. That code tends to change more often than your code, so by isolating it into its own file, your other files will change less often, and therefore can be cached longer by your users. It also has a side effect that when you have the runtime chunk, if you required jQuery from somewhere in
app.js, and somewhere in
checkout.js, they get the same object. Whereas, if you don't have that, your entries are so isolated that when you require jQuery, it's actually two separate objects. That may or may not be important to you.
Oh yeah, I actually have an example of that. Will this work? I require bootstrap in
app.js. Down in
checkout.js, is there a tooltip function? We required bootstrap further up, right? Right? It depends. With
enableSingleRuntimeChunk(), yes, because they're actually using the same jQuery object. With
disableSingleRuntimeChunk(), no, they would be more isolated.
It's not... what's wrong... enable is more convenient. Disable is actually a little more pure, because I said keep them isolated. It's up to you, I just want you to be aware of the decision there.
Okay, cool. Back to optimizing our build. We're not going to do this
createSharedEntry() thing anymore. We're going to go back to just, we have 2 entry files, or maybe 10 entry files. Cool. Now we're going to say
.splitEntryChunks(). This is all you need to do to optimize your build. You don't need to think about anything. Just call that function.
script tags. I know, yeah, I just got some looks like hmm?
What happens with
It's really incredible how it does this. It takes into account your file size. If you have some shared code, but it's maybe like five kilobytes, it's not worth the extra web request to isolate that into a separate file. You can configure all this, so I'm just telling you the defaults. If it's above 30 kilobytes, I think that's the threshold that it decides that this is better in a separate file. It also distinguishes between your code and vendor code, things in
node_modules, because it assumes that vendor code will change less often, so it tries to isolate the vendor code into its own file. Because that will change less often. Your users will, by isolating it, your users will be able to cache that longer because it's not going to change all the time.
Of course, the problem is how do we know what script tags to include? Also, including four script tags, that seems like a real pain. Also, as you can tell, these are going to change. If you start doing something different, it's going to split it in a different way.
So, introducing the newest bundle in the Symfony family, WebpackEncoreBundle. Amazing! Amazing! Wow. Probably the smallest bundle in all of the Symfony ecosystem. It gives you two Twig functions and that's it. It's super cool. Now you just say,
encore_entry_link_tags(), you give it the name of your entry, and it's just going to include all of the link tags, because the CSS is also split. Then same thing with script tags. It will include all of the script tags you need for that. When they change, will just get output.
How does that work? Magic? No. It's actually really boring. Oh, and same thing: checkout, include the checkout. It works because Encore dumps a file called
entrypoints.json that looks like this. Just dumps exactly which files are needed for every entry point. That bundle just reads those and it works. This means that you just enable
.splitEntryChunks() and you don't care. Things get split into pieces. It re-optimizes itself as you do different, keep coding and make more builds. Your code just works.
Also, I don't talk about here, but if you want versioning on your assets, so like when you build they have hashes in their filename, you get that out of the box. In the Encore recipe, we have a line in Webpack config that says
enableVersioning(). All of your files output with a hash in the filename. And you don't even know or care, because those hashes end up in this file. And so as long as you use the Twig functions, then you get free versioning. If you change a file, then it's going to change the hash in the filename. You don't even think about versioning anymore. You just get it for free.
One last thing I want to talk about, this is another feature that's not technically new with the new version of Encore, but we made it work without any configuration, is dynamic code splitting. It looks like this. Normally, we have the
import function, the
import call on top, the top of our file, we
import all the things we need. Sometimes you have a situation where you have a large piece of code, and you only need that code under certain situations.
The perfect example is if you have a React or Vue router, and it's like when you go to this URL, render this component, go to this URL, render that component, if you go to this URL, render that component. If you just code that, normally, that means all of the code from all of your components will get packaged into one single file. Maybe not that big of a deal. It depends on how big your application is, how much you care. Instead, what you can do is you can use
import like a function. It basically looks like an AJAX call, because it is an AJAX call. Import like a function, and then you give it the name of the module that you want to import, and then you say
.then(), that's a promise,
.then() and you pass it a callback. The argument passed your callback is that module. And then you use it.
In this case, since we're importing external linker, that code will not be included in the
Okay, and I forgot this one: require jQuery like anything else, even if you are allowed to cheat because you use
autoProvidejQuery(). And, use the new
splitEntryChunks(), because that's going to make your life much easier. By the way, I showed most of the presentation, I showed using normal script tags and normal link tags. Then we re-factored to the bundle. In reality, you just use the bundle. When you use Encore, you use those functions, then you don't have to think about versioning, or
splitEntryChunks(), or anything. It makes sense to turn that on.
All right, thank you, guys! Thank you for letting me go a little longer. Questions? I think I get to throw this. Cool. I was like, there has to be at least one question, because I haven't been able to throw this yet.
Yeah, that was amazing. That was on me. That was a bad throw. That was a good job over there.
Why are you wearing no shoes?
Why am I wearing no shoes? Well, now it's like my thing. A long time ago when I was presenting, I had shoes with heels, and I got on the stage, and it was like clunk, clunk, clunk, clunk. So, now I wear no shoes so I can be silent and slide around the stage. If it's summer, I wear no socks also.
Question over there? Or was it the same question?
Yeah, yeah, that's a really good question. Why when I'm doing the
yarn add, like the
composer require, why was I adding
–-dev? It doesn't matter. Technically, it's just a technical detail. Technically, if you use Node on your application for Webpack, but you also actually had a little bit of Node that you actually ran in production. I don't know, your code actually went through and used some Node code, then in theory, by adding Encore and those things as Dev dependencies, when you're deploying, you would actually, that's, you would run Encore, and your dev dependencies would be there, and you'd get that final built
public/build file. But then when you finally got to production, and you needed to install your Node dependencies for whatever weird thing you're doing at runtime, you could actually ask yarn to only install your not dev dependencies, which would be whatever libraries you're using for your actual runtime code. But you don't actually need Encore on production. It's technically correct as a dev dependency because that's not something that you actually need on production to execute your code. In reality, it doesn't matter because none of us, almost none of us are going to want to go to production and install our not Dev dependencies of Node because we have Node running alongside our PHP application. It's a weird edge case. Yeah, good question.
Nice, oh good, you guys are responsible for throwing it now.
Hi, thank you for presentation. I actually have two questions. First you said that
import are equally the same, but they're not. You showed that there is asynchronous fetching in
import. It's a big difference between the require also model splitting you can include only this part you need. The second question is how this jQuery fixing tool affects building time.
Interesting. Yeah, first part, yeah. You guys saw, I said require and import were basically the same, but import is a little bit better, but I didn't really say why. One of the reasons, as you said, is the import has this asynchronous, the import function thing. You can actually do that with require also, but the import is the cooler way to do it. The other thing which we didn't talk about is import and export, you have the ability to export multiple things from a module. With the require and the module.exports, it's always module.exports equals, the one thing that you want to export. But there's technically, with the export syntax you can export like 10 functions from the same module. Sorry, the other question was?
About jQuery fixing pack, or making them modules.
And changes imports to require, yeah.
Yeah, I don't think it affects much. Your builds can get big. It has to do with the size of your project. If you're doing things like... this alone won't make your builds slow, but if you were doing, if you were using SASS for CSS, and you wanted to import bootstrap as SASS, that alone is not going to make your build slow. But, if you start doing lots of those types of things, all of a sudden you're saying, hey SASS, go parse this giant SASS file. That's the kind of stuff that's going to make your build slower.
It's good you guys all sat together with the questions. It's very convenient.
package.json already changes. I don't know why.
Yeah, I don't know about that. As he changes branches, the
package.json file is suddenly modified. I'm just going to say that in case somebody else knows what weird thing that happens, but that might be something specific to your project.
Oh good, he can tell. Good.
Okay, so basically depends on the environment, so if some of your team, then you can have some differences in output. You can find a stackoverflow on basically how to fix it. You can disable it.
You better fix it in your project. So then you have no changes between operational systems. In our company we have fixed it recently, and it's pretty easy, you can just find it on stackoverflow.
Yep. All right, think we're past time. If you guys have more questions, come up and say hi. Thank you, guys.