gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
Before we talk about anything related to Symfony, we're going to strip things down to the bare minimum and prove that we can code modern JavaScript, right in our browser.
Go directly into the public/
directory and create a new app.js
file. To start, just console.log()
a message.
This won't be processed by Symfony or anything. In templates/base.html.twig
, up here in the javascripts
block, though that doesn't make any difference, add a boring <script>
tag for this: <script src="{{ asset('app.js') }}">
. I am using the asset()
function... but that's not doing anything either.
... lines 1 - 2 | |
<head> | |
... lines 4 - 14 | |
{% block javascripts %} | |
<script src="{{ asset('app.js') }}"></script> | |
... line 17 | |
{% endblock %} | |
</head> | |
... lines 20 - 69 |
Ok, head to the browser, open up your Console and... refresh. There's the log! It's snooze-worthy, but working.
Time to make things interesting! Back in app.js
, copy the mix name. Let's create a class: class MixedVinyl
, with a constructor and some properties. This uses the class syntax introduced in ES6, or ECMAScript 6... basically version "6" of JavaScript. You'll hear ES6 a lot because most modern features you're used to came from this version - released way back in 2015.
class MixedVinyl { | |
constructor(title, year) { | |
this.title = title; | |
this.year = year; | |
} | |
describe() { | |
... line 8 | |
} | |
} | |
... lines 11 - 14 |
In the describe()
method, I'm leveraging string interpolation - another modern feature from ES6 - to return the string. Below, use this: const
- yet another ES6 feature - mix = new MixedVinyl()
and pass in the mix name and year. Finally, console.log(mix.describe())
.
class MixedVinyl { | |
... lines 2 - 6 | |
describe() { | |
return `${this.title} was released in ${this.year}`; | |
} | |
} | |
const mix = new MixedVinyl('Awesome Mix Vol. 1', 2014); | |
console.log(mix.describe()); |
Cool! This is the kind of code I like to write every day. Unfortunately, this is also the kind of code that browsers have historically choked on!
So, normally, we would have a build system like Encore that would read this modern code and rewrite it to old JavaScript... so it would work in our browser. But... tada! It already works in our browser! We don't need to do anything. And that's not just because I'm using a new browser. This is going to work in every browser.
If you're ever unsure, go to https://caniuse.com to check it out. Let's look up "ES6 class". Yup, it's basically supported by everything... except for IE 11, which is dead.
But what about the import
statement? Copy the class MixedVinyl
then create another file directly inside public/
called vinyl.js
. Paste this in and then export
it: export default class
.
export default class { | |
constructor(title, year) { | |
this.title = title; | |
this.year = year; | |
} | |
describe() { | |
return `${this.title} was released in ${this.year}`; | |
} | |
} | |
... lines 11 - 12 |
Back over in app.js
, import MixedVinyl from
and, just like we do in Encore, use the relative path: ./vinyl.js
.
import MixedVinyl from './vinyl.js'; | |
... lines 2 - 5 |
Though, notice that I am including the .js
file extension... which you can do in Encore, but it's not required. More on that later - but this was on purpose.
So... does my browser support the import
statement? Let's find out! Refresh. Booo:
Cannot use import statement outside a module
Ok, not a "code red" kind of boo, more like a "code orange". Head back to base.html.twig
. When you hear the word "module", it's referring to files that leverage export
and import
. And if you want your JavaScript to be able to use these, you need to load the original file "as a module". It's a simple change. Copy the asset()
function and now say <script type="module">
. Then, instead of src
, inside, we're going to write some JavaScript to import
our app.js
file.
... lines 1 - 2 | |
<head> | |
... lines 4 - 14 | |
{% block javascripts %} | |
<script type="module">import '{{ asset('app.js') }}';</script> | |
... line 17 | |
{% endblock %} | |
</head> | |
... lines 20 - 69 |
This may look nutty at first, but... we're simply importing the path to our app.js
file. By doing this, app.js
will execute exactly like it did before... but as a "module"... which just means that import
and export
statements "should" work.
Do they? They do! OMG, our browser supports the import
statement!
We can even import third-party packages. To find one, I'm going to use my favorite CDN: "jsDelivr". We'll be using this quite a bit throughout the tutorial. But you don't need to use jsDelivr's CDN in your final code. It's a mirror of every NPM package... and so it's a convenient place to find what we need.
Search for the popular "lodash" package. When we select it, it shows us a <script>
tag we could use. Click on "ESM", which is short for ECMAScript modules. When you're coding with imports and exports, you want the ESM version of a package: it's a version that properly "exports" modules.
Now check out that script
tag:
<script type="module">
import lodash from '[...]'
</script>
That looks very similar to the code we have over here! We won't use this exactly, but I am going to copy the URL. Now go back to app.js
. To use lodash
we can say import _ from
and paste that full URL.
... line 1 | |
import _ from 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/+esm'; | |
... lines 3 - 6 |
Yes, importing from a full URL is totally allowed. Or we could download this file locally: I'll talk more about that later. Below, let's say _.camelCase()
to call one of its methods.
Let's try it! Spin over, refresh, and... look at that!. There's no build system here: we're just playing with files inside the public/
directory. And yet, we're writing modern JavaScript, importing and exporting modules and using a third-party NPM package. That's pretty amazing.
However, there are two remaining problems. First, importing packages using the full URL is annoying. I want to be able to say import from 'lodash'
The second problem is asset versioning. To have a performant system, we need the final files downloaded by the browser to have version hashes in their filenames, like app.1234abcd.js
. We need this so that we can instruct browsers to perform long-term caching. And we can't get this by creating & serving files directly from public/
.
These are precisely the two things that Symfony's new AssetMapper component will help us solve. But I wanted to start with raw JavaScript so that we could see how... most of what we're doing is not solved by Symfony or AssetMapper or AI: it's solved by your browser and the modern web.
Ok, let's delete these two files so I don't get confused... and also remove the import
inside of base.html.twig
. Don't worry! We'll see all of that code in a different way soon.
Next: Let's install AssetMapper and get it rocking.
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"babdev/pagerfanta-bundle": "^4.0", // v4.2.0
"doctrine/doctrine-bundle": "^2.7", // 2.10.0
"doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.4
"doctrine/orm": "^2.12", // 2.15.2
"knplabs/knp-time-bundle": "^1.18", // v1.20.0
"pagerfanta/doctrine-orm-adapter": "^4.0", // v4.1.0
"pagerfanta/twig": "^4.0", // v4.1.0
"stof/doctrine-extensions-bundle": "^1.7", // v1.7.1
"symfony/asset": "6.3.*", // v6.3.0
"symfony/asset-mapper": "6.3.*", // v6.3.0
"symfony/console": "6.3.*", // v6.3.0
"symfony/dotenv": "6.3.*", // v6.3.0
"symfony/flex": "^2", // v2.3.1
"symfony/framework-bundle": "6.3.*", // v6.3.0
"symfony/http-client": "6.3.*", // v6.3.0
"symfony/monolog-bundle": "^3.0", // v3.8.0
"symfony/proxy-manager-bridge": "6.3.*", // v6.3.0
"symfony/runtime": "6.3.*", // v6.3.0
"symfony/stimulus-bundle": "^2.9", // v2.9.1
"symfony/twig-bundle": "6.3.*", // v6.3.0
"symfony/ux-turbo": "^2.9", // v2.9.1
"symfony/web-link": "6.3.*", // v6.3.0
"symfony/yaml": "6.3.*", // v6.3.0
"twig/extra-bundle": "^2.12|^3.0", // v3.6.1
"twig/twig": "^2.12|^3.0" // v3.6.1
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.4
"symfony/debug-bundle": "6.3.*", // v6.3.0
"symfony/maker-bundle": "^1.41", // v1.49.0
"symfony/stopwatch": "6.3.*", // v6.3.0
"symfony/web-profiler-bundle": "6.3.*", // v6.3.0
"zenstruck/foundry": "^1.21" // v1.33.0
}
}
Is there a reason why you chose to add an inline-script to the template instead of loading the JS with
<script type="module" src="{{ asset('app.js') }}"></script>
?