Webpack Encore: JavaScript Greatness
Tip
The recipe now adds these 2 files in a slightly different location:
assets/app.js
assets/styles/app.css
But the purpose of each file is exactly the same.
Okay: here's how this whole thing works. The recipe added a new assets/
directory with a couple of example CSS and JS files. The app.js
file basically just console.log()
's something:
/* | |
* Welcome to your app's main JavaScript file! | |
* | |
* We recommend including the built version of this JavaScript file | |
* (and its CSS file) in your base layout (base.html.twig). | |
*/ | |
// any CSS you import will output into a single css file (app.css in this case) | |
import '../css/app.css'; | |
// Need jQuery? Install it with "yarn add jquery", then uncomment to import it. | |
// import $ from 'jquery'; | |
console.log('Hello Webpack Encore! Edit me in assets/js/app.js'); |
The app.css
changes the background color to light gray:
body { | |
background-color: lightgray; | |
} |
Webpack Encore is entirely configured by one file: webpack.config.js
:
var Encore = require('@symfony/webpack-encore'); | |
// Manually configure the runtime environment if not already configured yet by the "encore" command. | |
// It's useful when you use tools that rely on webpack.config.js file. | |
if (!Encore.isRuntimeEnvironmentConfigured()) { | |
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev'); | |
} | |
Encore | |
// directory where compiled assets will be stored | |
.setOutputPath('public/build/') | |
// public path used by the web server to access the output path | |
.setPublicPath('/build') | |
// only needed for CDN's or sub-directory deploy | |
//.setManifestKeyPrefix('build/') | |
/* | |
* ENTRY CONFIG | |
* | |
* Add 1 entry for each "page" of your app | |
* (including one that's included on every page - e.g. "app") | |
* | |
* Each entry will result in one JavaScript file (e.g. app.js) | |
* and one CSS file (e.g. app.css) if your JavaScript imports CSS. | |
*/ | |
.addEntry('app', './assets/js/app.js') | |
//.addEntry('page1', './assets/js/page1.js') | |
//.addEntry('page2', './assets/js/page2.js') | |
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization. | |
.splitEntryChunks() | |
// will require an extra script tag for runtime.js | |
// but, you probably want this, unless you're building a single-page app | |
.enableSingleRuntimeChunk() | |
/* | |
* FEATURE CONFIG | |
* | |
* Enable & configure other features below. For a full | |
* list of features, see: | |
* https://symfony.com/doc/current/frontend.html#adding-more-features | |
*/ | |
.cleanupOutputBeforeBuild() | |
.enableBuildNotifications() | |
.enableSourceMaps(!Encore.isProduction()) | |
// enables hashed filenames (e.g. app.abc123.css) | |
.enableVersioning(Encore.isProduction()) | |
// enables @babel/preset-env polyfills | |
.configureBabelPresetEnv((config) => { | |
config.useBuiltIns = 'usage'; | |
config.corejs = 3; | |
}) | |
// enables Sass/SCSS support | |
//.enableSassLoader() | |
// uncomment if you use TypeScript | |
//.enableTypeScriptLoader() | |
// uncomment to get integrity="..." attributes on your script & link tags | |
// requires WebpackEncoreBundle 1.4 or higher | |
//.enableIntegrityHashes(Encore.isProduction()) | |
// uncomment if you're having problems with a jQuery plugin | |
//.autoProvidejQuery() | |
// uncomment if you use API Platform Admin (composer req api-admin) | |
//.enableReactPreset() | |
//.addEntry('admin', './assets/js/admin.js') | |
; | |
module.exports = Encore.getWebpackConfig(); |
We won't talk much about this file - we'll save that for the Encore tutorial - but it's already configured to point at the app.js
and app.css
files: it knows that it needs to process them.
Running Encore
To execute Encore, find your terminal and run:
yarn watch
This is a shortcut for running yarn run encore dev --watch
. What does this do? It reads those 2 files in assets/
, does some processing on them, and outputs a built version of each inside a new public/build/
directory. Here is the "built" app.css
file... and the built app.js
file. If we ran Encore in production mode - which is just a different command - it would minimize the contents of each file.
Including the Built CSS and JS Files
There's a lot more cool stuff going on, but this is the basic idea: we code in the assets/
directory, but point to the built files in our templates.
For example, in base.html.twig
, instead of pointing at the old app.css
file, we want to point at the one in the build/
directory. That's simple enough, but the WebpackEncoreBundle has a shortcut to make it even easier: {{ encore_entry_link_tags() }}
and pass this app
, because that's the name of the source files - called an "entry" in Webpack land.
// ... line 1 | |
<html> | |
<head> | |
// ... lines 4 - 5 | |
{% block stylesheets %} | |
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> | |
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Spartan&display=swap"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/css/all.min.css" integrity="sha256-mmgLkCYLUQbXn0B1SRqzHar6dCnv9oZFPEC1g1cwlkk=" crossorigin="anonymous" /> | |
{{ encore_entry_link_tags('app') }} | |
{% endblock %} | |
</head> | |
// ... lines 13 - 33 | |
</html> |
At the bottom, render the script tag with {{ encore_entry_script_tags('app') }}
.
// ... line 1 | |
<html> | |
// ... lines 3 - 12 | |
<body> | |
// ... lines 14 - 25 | |
{% block javascripts %} | |
<script | |
src="https://code.jquery.com/jquery-3.4.1.min.js" | |
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" | |
crossorigin="anonymous"></script> | |
{{ encore_entry_script_tags('app') }} | |
{% endblock %} | |
</body> | |
</html> |
Let's try it! Move over and refresh. Did it work? It did! The background color is gray... and if I bring up the console, there's the log:
Hello Webpack Encore!
If you look at the HTML source, there's nothing special going on: we have a normal link tag pointing to /build/app.css
.
Moving our Code into Encore
Now that this is working, let's move our CSS into the new system. Open public/css/app.css
, copy all of this, then right click and delete the file. Now open the new app.css
inside assets/
and paste.
body { | |
font-family: spartan; | |
color: #444; | |
} | |
.jumbotron-img { | |
background: rgb(237,116,88); | |
background: linear-gradient(302deg, rgba(237,116,88,1) 16%, rgba(51,61,81,1) 97%); | |
color: #fff; | |
} | |
.q-container { | |
border-top-right-radius: .25rem; | |
border-top-left-radius: .25rem; | |
background-color: #efefee; | |
} | |
.q-container-show { | |
border-top-right-radius: .25rem; | |
border-top-left-radius: .25rem; | |
background-color: #ED7458 ; | |
} | |
.q-container img, .q-container-show img { | |
border: 2px solid #fff; | |
border-radius: 50%; | |
} | |
.q-display { | |
background: #fff; | |
border-radius: .25rem; | |
} | |
.q-title-show { | |
text-transform: uppercase; | |
font-size: 1.3rem; | |
color: #fff; | |
} | |
.q-title { | |
text-transform: uppercase; | |
color: #444; | |
} | |
.q-title:hover { | |
color: #2B2B2B; | |
} | |
.q-title h2 { | |
font-size: 1.3rem; | |
} | |
.q-display-response { | |
background: #333D51; | |
color: #fff; | |
} | |
.answer-link:hover .magic-wand { | |
transform: rotate(20deg); | |
} | |
.vote-arrows { | |
font-size: 1.5rem; | |
} | |
.vote-arrows span { | |
font-size: 1rem; | |
} | |
.vote-arrows a { | |
color: #444; | |
} | |
.vote-up:hover { | |
color: #3D9970; | |
} | |
.vote-down:hover { | |
color: #FF4136; | |
} | |
.btn-question { | |
color: #FFFFFF; | |
background-color: #ED7458; | |
border-color: #D45B3F; | |
} | |
.btn-question:hover, | |
.btn-question:focus, | |
.btn-question:active, | |
.btn-question.active, | |
.open .dropdown-toggle.btn-question { | |
color: #FFFFFF; | |
background-color: #D45B3F; | |
border-color: #D45B3F; | |
} | |
.btn-question:active, | |
.btn-question.active, | |
.open .dropdown-toggle.btn-question { | |
background-image: none; | |
} | |
.btn-question.disabled, | |
.btn-question[disabled], | |
fieldset[disabled] .btn-question, | |
.btn-question.disabled:hover, | |
.btn-question[disabled]:hover, | |
fieldset[disabled] .btn-question:hover, | |
.btn-question.disabled:focus, | |
.btn-question[disabled]:focus, | |
fieldset[disabled] .btn-question:focus, | |
.btn-question.disabled:active, | |
.btn-question[disabled]:active, | |
fieldset[disabled] .btn-question:active, | |
.btn-question.disabled.active, | |
.btn-question[disabled].active, | |
fieldset[disabled] .btn-question.active { | |
background-color: #ED7458; | |
border-color: #D45B3F; | |
} | |
.btn-question .badge { | |
color: #ED7458; | |
background-color: #FFFFFF; | |
} | |
footer { | |
background-color: #efefee; | |
} |
As soon as I do that, when I refresh... it works! Our CSS is back! The reason is that - if you check your terminal - yarn watch
is watching our files for changes. As soon as we modified the app.css
file, it re-read that file and dumped a new version into the public/build
directory. That's why we keep this running in the background.
Let's do the same thing for our custom JavaScript. Open question_show.js
and, instead of having a page-specific JavaScript file - where we only include this on our "show" page - to keep things simple, I'm going to put this into the new app.js
, which is loaded on every page.
// ... lines 1 - 13 | |
/** | |
* Simple (ugly) code to handle the comment vote up/down | |
*/ | |
var $container = $('.js-vote-arrows'); | |
$container.find('a').on('click', function(e) { | |
e.preventDefault(); | |
var $link = $(e.currentTarget); | |
$.ajax({ | |
url: '/comments/10/vote/'+$link.data('direction'), | |
method: 'POST' | |
}).then(function(data) { | |
$container.find('.js-vote-total').text(data.votes); | |
}); | |
}); |
Then go delete the public/js/
directory entirely... and public/css/
. Also open up templates/question/show.html.twig
and, at the bottom, remove the old script tag.
{% extends 'base.html.twig' %} | |
{% block title %}Question: {{ question }}{% endblock %} | |
{% block body %} | |
// ... lines 6 - 57 | |
{% endblock %} |
With any luck, Encore already rebuilt my app.js
. So if we click to view a question - I'll refresh just to be safe - and... click the vote icons. Yes! This still works.
Installing & Importing Outside Libraries (jQuery)
Now that we're using Encore, there are some really cool things we can do. Here's one: instead of linking to a CDN or downloading jQuery directly into our project and committing it, we can require jQuery and install it into our node_modules/
directory... which is exactly how we're used to doing things in PHP: we install third-party libraries into vendor/
instead of downloading them manually.
To do that, open a new terminal tab and run:
yarn add jquery --dev
This is equivalent to the composer require
command: it adds jquery
to the package.json
file and downloads it into node_modules/
. The --dev
part is not important.
Next, inside base.html.twig
, remove jQuery entirely from the layout.
// ... line 1 | |
<html> | |
// ... lines 3 - 12 | |
<body> | |
// ... lines 14 - 25 | |
{% block javascripts %} | |
{{ encore_entry_script_tags('app') }} | |
{% endblock %} | |
</body> | |
</html> |
If you go back to your browser and refresh the page now... it's totally broken:
$ is not defined
...coming from app.js
. That makes sense: we did just download jQuery into our node_modules/
directory - you can find a directory here called jquery
- but we're not using it yet.
How do we use it? Inside app.js
, uncomment this import line: import $ from 'jquery'
.
// ... lines 1 - 9 | |
// Need jQuery? Install it with "yarn add jquery", then uncomment to import it. | |
import $ from 'jquery'; | |
// ... lines 14 - 29 |
This "loads" the jquery
package we installed and assigns it to a $
variable. All these $
variables below are referencing the value we imported.
Here's the really cool part: without making any other changes, when we refresh, it works! Webpack noticed that we're importing jquery
and automatically packaged it inside of the built app.js
file. We import the stuff we need, and Webpack takes care of... packaging it all together.
Tip
Actually, Webpack splits the final files into multiple for efficiency. jQuery actually lives inside a different file in public/build/, though that doesn't matter!
Importing the Bootstrap CSS
We can do the same thing for the Bootstrap CSS. On the top of base.html.twig
, delete the link
tag for Bootstrap:
// ... line 1 | |
<html> | |
<head> | |
// ... lines 4 - 5 | |
{% block stylesheets %} | |
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Spartan&display=swap"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/css/all.min.css" integrity="sha256-mmgLkCYLUQbXn0B1SRqzHar6dCnv9oZFPEC1g1cwlkk=" crossorigin="anonymous" /> | |
{{ encore_entry_link_tags('app') }} | |
{% endblock %} | |
</head> | |
// ... lines 12 - 28 | |
</html> |
No surprise, when we refresh, our site looks terrible.
To fix it, find your terminal and run:
yarn add bootstrap --dev
This downloads the bootstrap
package into node_modules/
. This package contains both JavaScript and CSS. We want to bring in the CSS.
To do that, open app.css
and, at the top, use the good-old-fashioned @import
syntax. Inside double quotes, say ~bootstrap
:
@import "~bootstrap"; | |
// ... lines 3 - 129 |
In CSS, this ~
is a special way to say that you want to load the CSS from a bootstrap
package inside node_modules/
.
Move over, refresh and... we are back! Webpack saw this import, grabbed the CSS from the bootstrap package, and included it in the final app.css
file. How cool is that?
What Else can Encore Do?
This is just the start of what Webpack Encore can do. It can also minimize your files for production, supports Sass or LESS compiling, comes with React and Vue.js support, has automatic filename versioning and more. To go further, check out our free Webpack Encore tutorial.
And... that's it for this tutorial! Congratulations for sticking with me to the end! You already understand the most important parts of Symfony. In the next tutorial, we're going to unlock even more of your Symfony potential by uncovering the secrets of services. You'll be unstoppable.
As always, if you have questions, problems or have a really funny story - especially if it involves your cat - we would love to hear from you in the comments.
Alright friends - seeya next time!
hey there how are things going. it's been a while, happy new year :D
man, i've been trying wp and im getting this controllers.json not found error at every turn.
` btx main ≢ -2 | +1 ~3 yarn watch
yarn run v1.22.17
$ encore dev --watch
Running webpack ...
ERROR Failed to compile with 1 errors 1:22:00 PM
error in ./node_modules/@symfony/stimulus-bridge/controllers.json 1:22:00 PM
Module build failed (from ./node_modules/@symfony/stimulus-bridge/dist/webpack/loader.js):
Error: Your controllers.json file was not found. Be sure to add a Webpack alias from "@symfony/stimulus-bridge/controllers.json" to your controllers.json file.
Entrypoint app [big] 1.36 MiB = runtime.js 14.5 KiB vendors-node_modules_symfony_stimulus-bridge_dist_index_js-node_modules_core-js_modules_es_ob-3d222b.css 878 KiB vendors-node_modules_symfony_stimulus-bridge_dist_index_js-node_modules_core-js_modules_es_ob-3d222b.js 482 KiB app.css 426 bytes app.js 17.9 KiB
webpack compiled with 1 error
`