JavaScript Modules
Inspect element on this page and head over to the browser console. Ah, we've got a console log that says it comes from assets/app.js
. And sure enough, if we spin over and open that file... there it is!
/* | |
* Welcome to your app's main JavaScript file! | |
* | |
* This file will be included onto the page via the importmap() Twig function, | |
* which should already be in your base.html.twig. | |
*/ | |
import './styles/app.css' | |
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉') |
But how is this file being loaded? To answer that, view the page source. There's some interesting stuff going on here, but I want to zoom in on one part: <script type="module">
, import 'app';
.
ECMAScript Modules
It turns out that all modern browsers - basically everything except for IE 11... and you should not be supporting IE 11 anymore - ahem all modern browsers support JavaScript modules, also known as ECMAScript modules or ESM. But they're nothing fancy: a JavaScript module is any JavaScript file that uses the import
or export
statements that you probably grew accustomed to in Webpack Encore.
The big news is that: browsers understand import
and export
all by themselves! No build step needed. If you open any HTML page and say <script type="module">
, the code inside is allowed to use import
and export
statements.
Importmaps
So... the second question is: what the heck is app
? How does app
ultimately refer to assets/app.js
? This is also a new trick of browsers called importmaps. And this has nothing to do with Symfony or AssetMapper. If, on your page, you have a <script type="importmap">
, this becomes a key value map that's used by your browser when it loads modules. So if we say import 'app'
, it looks inside of this list, sees app
and ultimately loads this file... which is served by AssetMapper. It's a nice bit of teamwork!
Importmaps are supported by all modern browsers... though it has slightly less support than JavaScript modules. Fortunately, there's a shim or polyfill so that if your user happens to use a browser that doesn't support importmaps, that shim will add it and everything will work.
The importmap() Function
The final question on my mind is: where the heck is this all coming from? To answer that, open templates/base.html.twig
. It's entirely coming from this one line right here: {{ importmap('app') }}
:
<html> | |
<head> | |
// ... lines 4 - 10 | |
{% block javascripts %} | |
{{ importmap('app') }} | |
{% endblock %} | |
</head> | |
// ... lines 15 - 19 | |
</html> |
Because we passed app
, this will generate a <script type="module">
with import 'app'
inside. But this also dumps the polyfill, some preloads - those are good for performance, but not required - and, of course, the importmap itself. The importmap is primarily, though not entirely (we'll get to that), generated from this importmap.php
file:
// ... lines 1 - 2 | |
/** | |
* Returns the import map for this application. | |
* | |
* - "path" is a path inside the asset mapper system. Use the | |
* "debug:asset-map" command to see the full list of paths. | |
* | |
* - "entrypoint" (JavaScript only) set to true for any module that will | |
* be used as an "entrypoint" (and passed to the importmap() Twig function). | |
* | |
* The "importmap:require" command can be used to add new entries to this file. | |
* | |
* This file has been auto-generated by the importmap commands. | |
*/ | |
return [ | |
'app' => [ | |
'path' => './assets/app.js', | |
'entrypoint' => true, | |
], | |
]; |
The importmap.php File
When we installed AssetMapper, its recipe gave us this file. And this is the reason that the importmap
in our HTML has an app
key that points to assets/app.js
.
Writing Some JavaScript Modules
So I want to play a bit with this new system. Inside the assets/
directory - we can organize this however we want - create a lib/
directory with an alien-greeting.js
file. Inside, I'm going to write some awesome, modern JavaScript: export default
a function, give it message
and inPeace
arguments... then I'll log a message using a template literal - the fancy backticks - and some emojis:
export default function (message, inPeace = false) { | |
console.log(`${message}! ${inPeace ? '👽' : '👾'}`); | |
} |
Cool! This new file lives inside assets/
so, technically, it's publicly available. But... nobody is using it yet.
Let's try something non-traditional, but fun to start. Go into the base layout and, anywhere, say <script type="module">
. Inside, import alienGreeting
... and I'll hit tab:
<html> | |
<head> | |
// ... lines 4 - 10 | |
{% block javascripts %} | |
{{ importmap('app') }} | |
<script type="module"> | |
import alienGreeting from '{{ asset('lib/alien-greeting.js') }}'; | |
// ... lines 16 - 17 | |
</script> | |
{% endblock %} | |
</head> | |
// ... lines 21 - 25 | |
</html> |
Hmm: PhpStorm used ../assets
for the path. That's not going to work. Instead, we can use the asset()
function and the logical path: lib/alien-greeting.js
. Then below, use that: alienGreeting()
, a message and we will not come in peace!
<html> | |
<head> | |
// ... lines 4 - 10 | |
{% block javascripts %} | |
// ... lines 12 - 13 | |
<script type="module"> | |
import alienGreeting from '{{ asset('lib/alien-greeting.js') }}'; | |
alienGreeting('Give us all your candy!', false); | |
</script> | |
{% endblock %} | |
</head> | |
// ... lines 21 - 25 | |
</html> |
Let's see if it works! Close that, and... it doesn't? I actually thought it would! We get a 404 for lib/alien-greeing.js
- with no "t"...! Boop!
// ... lines 1 - 14 | |
import alienGreeting from '{{ asset('lib/alien-greeting.js') }}'; | |
// ... lines 16 - 27 |
Now it works! No build, nice code, nothing special.
If you view the page source, we, of course, have this nice versioned filename in the import
. So you can import simple things like app
and rely on the importmap
to point to the true filename, or you can include full paths.
Importing from JS Files
As fun as it was to hack this into the HTML, in reality, we're not usually going to write in-line code like this. Copy this, get rid of the <script type="module">
:
<html> | |
<head> | |
// ... lines 4 - 10 | |
{% block javascripts %} | |
{{ importmap('app') }} | |
{% endblock %} | |
</head> | |
// ... lines 15 - 19 | |
</html> |
Then go into app.js
. Paste the code here:
// ... lines 1 - 6 | |
import './styles/app.css' | |
import alienGreeting from './lib/alien-greeting.js'; | |
alienGreeting('Give us all your candy!', false); |
And now that we're inside JavaScript, when we refer to a path, we can write it with normal, relative paths: ./alien-greeting.js
:
// ... lines 1 - 7 | |
import alienGreeting from './lib/alien-greeting.js'; | |
// ... lines 9 - 11 |
This is the exact code that we would have in Webpack Encore, with one small difference. In Webpack, you don't need to have the .js
on the end. It turns out that leaving off the extension is a Node-specific thing. In real JavaScript, you do need to have the extension. So you do need to add the .js
.
And... it works!
PhpStorm: Auto-add Extension
By the way, if you let PhpStorm auto-complete the path to the imported JavaScript file, by default, it will not include the .js
on the end. To fix that, open the settings... and search for "extensions". There we go: "Editor"=>"Code Style"=> "JavaScript". Right here, change this "use file extension" to "always".
Ok, day 3 is in the books! Tomorrow, we'll make our JavaScript set up much more powerful by learning how to install 3rd-party packages!
I am having a CSP issue with data:application/javascript:
Refused to load the script 'data:application/javascript,' because it violates the following Content Security Policy directive: "script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.
Any advice on how I should handle this?
Thanks :)!