Symfony 4: using ParcelJs with Twig templates
Webpack's flexibility and power comes at the price of a fairly steep learning curve, however, and whilst the time required to learn its intricacies turned out to be a sound investment for my more complicated bundling requirements, it all started to feel a bit unnecessary when I just wanted to spin up something straight forward.
ParcelJs brings something slightly different to the table, and whilst it won't replace Webpack for complicated bundling, it more than makes up for it with speed and increased simplicity.
I've also created a Github repo for anyone that wants to dive straight into the code.
By the end of this article, you should be able to:
- set up a skeleton Symfony 4 project
- display the bundled assets using Symfony's Twig templates
- display certain assets inline
- employ cache-busting and code-splitting techniques
- OSX Mojave
- A local installation of the Composer dependency manager
- Basic knowledge of PHP and the Symfony framework
- A local installation of the latest stable NodeJs release (12.9.1 at the time of writing)
- Basic knowledge of asset bundling
Create a Symfony 4 skeleton project
If you're unsure about this process then please read my article that explains how to create a new Symfony project from scratch.
Create a new Controller
We'll need a controller to test out the results of our asset bundling.
The controller we've made is about as basic as it gets really. I've used the route annotations so that we don't have to be bothered with a separate routing file, but that's about it.
Create a Twig template
At the moment, our controller method will attempt to render a template that doesn't exist, so let's create a simple twig template to fix that.
Before we move on though, let's try to understand a bit more about what's going on here.
If you view the source of our page in your web browser, you should see something similar to the following:
Lines 8 - 10 contain the markup that we put in our template, but everything else comes from the base template.
If you take another look at our template code, you'll see that we actually extended base.html.twig, and added a block named body.
This tells the Twig templating system to first load the base.html.twig template, and then inject the markup that it finds between block and endblock tags (in index.html.twig in this case) into the corresponding references in base.html.twig.
Here we've named our block body, and if we open up the templates/base.html.twig file, we can see that it contains, amongst other things, a body block too (line 9):
We'll need to deal with some of those other blocks shortly, so let's get on with creating our assets.
Adding styles and scripts
Our assets aren't much good to us in their current location, but as we'll be using Parcel for our pipeline, it's time to get that installed.
Creating the asset bundles
At this point, we can see Parcel in action.
The above command tells Parcel to take the code in the file scripts/homePage/index.js, and bundle it along with the code contained in any of its associated import statements into a single file in Symfony's default public directory (public/assets/homePage.bundle.js). The code in the resulting file will be minified, as mentioned previously, and no source maps will be created thanks to the --no-source-maps flag.
We can now do the same for our css files:
Using the bundles
Isn't that beautiful. An inspiration for designers everywhere.
Well, maybe not, but it illustrates very clearly that our bundled styles have been successfully rendered.
Bundling assets during development
So far, we've seen how to create minified bundles using scripts that were run directly on the command line. This is all well and good, but we can make the process a little bit neater and set things up so that the bundles will be created automatically when changes are made to the associated source files.
What we've actually done here is run two commands that will start two simultaneous processes: one that watches for changes in .js files, and another that watches for changes in .css files.
The watch flag tells Parcel to watch for changes in the source files, and then the relevant bundles are recreated whenever a change is detected.
This is really handy during the development cycle, and it's far easier having the process triggered automatically rather than having to mess around in the terminal every time you make a change.
The only other thing to note is the --public-url flag. This accepts a value that specifies the location that the assets are being served from, and is used to provide a reference in the bundle to its associated source map file.
If you open up homePage.bundle.css, for example, you should see the following reference.
It's all very messy running big long commands in the terminal, so it would be nice if we could tidy things up a bit.
The development build / watch process can now be started by running the following command in the terminal:
The production build can now be started by running the following command in the terminal:
You may have noticed that there's a bit of additional jiggery-pokery at the beginning of the build_assets_dev and build_assets_prod scripts:
All this does is remove any pre-existing bundle files (the ones that have .bundle. in their name) so that the production build isn't affected by any artifacts that might remain from a previously run development build, or vice versa.
First, we find all the files in the public/assets directory:
We then pipe the resulting files names through to the grep command, which returns only the files that have .bundle. in their name:
Finally, we pipe the filtered results into xargs, which passes the identified file names on to the rm command.
At this point, our development process still involves having two terminal windows open. This might be fine, or even desirable for some people, but nevertheless we can simplify our process further using composer.json.
Now you should be able to start the Symfony project and start watching both .css and .js files from composer. Try stopping the development server, along with any watch processes, and run:
If you want to do the same thing with a production build (without watchers), you can run the following command:
The main reason to bundle assets is to reduce the number of HTTP requests required to render a web page. If you have two files referenced in the initial HTML response from a web server, for example, then a browser would need to make two additional requests to retrieve this files. Assuming that the files are small enough, combining them into a single file would then only require one additional HTTP request to retrieve it.
A potential downside to requiring additional files, however, is that those files will be downloaded after the initial HTML has been rendered in the browser, which can often cause the page to flash or jiggle while the browser updates everything.
Nowadays, it's popular practice to embed, or "inline", certain assets within the initial response from the web server in order to give the user the perception of a "visually ready" page as soon as possible, despite the resulting increase in the size of the initial response.
Typically these inlined assets (styles, scripts, images, fonts, etc) are essential to the page's fundamental structure, and provide the user with everything required for a pleasant experience, whilst less important functionality and styling is loaded in later on by the bundles.
The example I'm using in this tutorial is far too simple to be realistic, but we can still use it to take a look at how we might go about inlining assets that have been processed by Parcel.
Let's create some "essential" assets that we can inline.
The above code is pretty simple. All we're really doing is using PHP's file_get_contents function to return the contents of the specified files rather than the entire files themselves.
Here, we're taking the asset file contents that were returned from our helper methods, and making them available to our Twig templates in the essential_styles and essential_scripts variables.
Those variables aren't yet referenced in our Twig template, so let's fix that.
There are a couple of things to pay attention to here:
In both of those code snippets, we're referencing a variable and using the raw filter to ensure that the variable's content isn't HTML encoded before it's rendered. By default, Symfony provides automatic escaping, but we want the content of our asset files to appear in the source exactly as it appears in the files themselves. As you can see, the asset contents are injected between style and script tags respectively.
The checks on lines 7 and 16 are just defensive programming. We make sure that the essential_styles and essential_scripts variables exist before we try to use them, and also that they contain content. There's really no point in rendering an empty pair of tags.
You should see that the new Comic Sans MS font has been applied:
The page source should show that our essential scripts and styles have been added directly into the HTML response. If you started the project with composer run-local-dev, then the inline assets will not be minified, and will be automatically updated when you make changes to the source, whereas the composer run-local-prod should have minified the inline assets.
As I mentioned in the introduction, I've updated this article to provide a solution for cache-busting, and also to take advantage of Parcel's inbuilt support for code-splitting. It's debatable whether or not this workaround is sufficiently complex to warrant a move away from Parcel (until a Twig plugin is available at least), but I'll describe the steps I took to deal with the problem in any case.
Changes to the directory structure
The first obstacle I faced was Parcel's refusal to accept more than one out-file. Initially, I was adding custom suffixes to my various asset files (*.inline.css, *.inline.js, *.bundle.css and *.bundle.js), which was achieved by simply passing the asset name to Parcel's --out-file argument.
I wanted to use the built-in cache busting and code splitting functionality, so I needed to allow Parcel to create the names for the asset files itself.
New development scripts
There's quite a lot going on in the above example, but notice just for now that I've removed the --out-file arguments from the parcel build and parcel watch commands, and that I'm instead relying on Parcel to create the names and paths for the files inside the public/assets directory specified on the --out-dir argument. I've also relaxed some of the glob patterns as they felt a bit unnecessary.
New bundling methods
As you can see, we've removed the custom suffixes from the asset filenames in the getEssentialCssInline and getEssentialJsInline methods, and we've also added a couple of new methods that will deal with bundling.
The getBundlePath() method accepts the directory name ($bundleDirName) and filename ($bundleFileName) of a bundle that we want to reference. The task of actually locating the bundle and returning its path relative to our public asset directory (/assets/ in this case) is then handed off to the private getExternalBundlePath() method.
Once we have these paths, we can render them into the Twig templates so that they can be used in the front-end of our project.
New Twig templates
Notice the new sections on lines 6 to 8 and 17 to 19. They're there to render the paths that we returned from the calls to the getBundlePath methods that we made in src/Controller/HomeController.php.
New production scripts
That's all well and good, but it doesn't give us much more than we had before. Let's add some changes to the scripts for our production build so that we can see cache-busting in action.
The main change we've made here is the addition of the add_asset_hash script. This is used to call some code that will append MD5 hashes to the names of asset bundles. The MD5 hash is created from the contents of the file itself; the idea being that if the file changes then so will the MD5 hash, and therefore the name of the file, which will help to prevent browsers and intermediary caches storing stale asset content. After all, any awesome new front-end features that we create won't be appreciated if people are using out-of-date copies of our bundles ;)
Cache-busting in action
If you alter the contents of one of the bundled files and the re-run the composer run-local-prod command, you should see that the MD5 hash that is appended to the corresponding bundle name has been altered too.
Parcel also allows for the dynamic loading of Js modules. This feature is particularly useful when you'd rather load client-side features only where specific conditions are met, or when a certain event occurs (e.g. a button is clicked). In this way, it's possible to keep the initial asset bundle size to a minimum, downloading additional pieces of script if and when they are required.
Finally, we need to include the components of our code-splitting example into our Parcel build. Here's where I noticed something that further complicates our process though (if we want to use sourcemaps, that is).
I initially wanted to watch the development assets using the same script and Parcel process, using something similar to below:
Unfortunately, this only seems to create the correct sourcemap reference for the asset that is passed as the first argument (styles/homePage/*.css in this case).
Our changes in action
After this command has run, look in the public/assets directory, and you should see that the asset files have once again been moved into the corresponding homePage and codeSplitting directories, and that they have MD5 hashes embedded in their names. You should see the essential asset files in the root as before, and a main.xxxxxxxx.js file that Parcel uses for code-splitting.
By this stage, you should hopefully have a functional Symfony 4 project containing some useful examples of the Parcel.js functionality.
In conclusion, I think that you're probably better off using Webpack, or at least waiting for a Twig plugin to be released for Parcel, as opposed to using my workaround, but hopefully this article has been useful nonetheless. Please share a link around if you liked it.
Be sure to do your own research around all the topics discussed here, as I'm fairly new to PHP myself and this is quite an early iteration of my Symfony / Parcel / Twig solution.