Improving Your AEM Front-End Development Workflow - ClientLibs Hot Reloading
The ability to quickly prototype, iterate, and refine your application is crucial for productivity in front-end development. With Adobe Experience Manager (AEM), this is somewhat achievable, but there's always room for improvement.
In this article, I will walk you through a basic configuration setup of ClientLibs hot reloading – a tool that transformed my AEM front-end workflow.
Access the related repository here. It’s an example project generated using aem-project-archetype, extended with hot reloading capacity explained in this article. It takes around one hour to integrate this setup into the archetype project. Follow the steps outlined below to apply this methodology to your own development process.
But first, let’s explore why attempting to streamline your workflow is worth it - in 3 key points.
The Key Ingredients of a Great Front-End Development Experience
Support for Modern Technologies: In today's fast-paced development landscape, staying up-to-date with modern technologies is crucial. This includes using tools like TypeScript, ESNext, SCSS, or Webpack to leverage the latest advancements in front-end development. By embracing these technologies, developers can write cleaner, more maintainable code and take advantage of modern features and patterns.
Fast Development Feedback: The ability to see changes instantly during the development and testing phases is a game-changer. Rapid feedback loops enable developers to iterate quickly, catch bugs early, and fine-tune their code for optimal performance. Whether it's immediate browser refreshes or real-time updates, quick feedback greatly enhances productivity and fosters a more enjoyable development experience.
Backend Independence in UI: Collaboration between front-end and back-end developers is critical for successful web application development. However, it’s important to define optimal points of contact in this collaboration. In parts of applications that change frequently and relate to a specific specialization, the points of contact should be limited in order to provide optimal workflow. Having backend independence in developing UIs allows front-end developers to work independently, which facilitates quicker iterations, lessens communication overhead, and promotes more efficient teamwork.
Support for Modern Technologies in AEM Front-End
Fortunately, we have this part covered out of the box thanks to ClientLibs and the Webpack setup of ui-frontend module in the AEM archetype. We can set up build tools to understand whatever technologies and syntaxes we want and produce ClientLibs that AEM understands.
The Challenge of Fast Development Feedback In AEM
AEM documentation outlines two possible workflows for Front-End development: using Webpack to serve static files or utilizing tools like Storybook. The first one is configured out of the box in the AEM project archetype. While these approaches offer a rapid feedback loop, they can compromise backend independence in UI development. Given that components and their markup are defined in AEM and typically handled by back-end developers, any UI modifications impacting the markup must be performed there. This requires developers to write mock-up code on the front end, which then needs to be replicated on the back end. This duplication of work not only increases development time but also introduces the potential for inconsistencies and communication overhead between front-end and back-end teams.
To address these issues, we need to introduce the concept of client libraries synchronization and make front-end team work on the markup. This way we can streamline the development process, reduce duplication of work, and enhance collaboration between front-end and back-end teams.
Client libraries synchronization with AEM instance - Backend Independence
Project generated with AEM project archetype comes with configured aemsync, a tool that synchronizes filesystem changes with AEM instance.
The npm configuration looks as follows:
{
"start": "webpack-dev-server --open --config ./webpack.dev.js",
"chokidar": "chokidar -c \"clientlib\" ./dist",
"aemsyncro": "aemsync -w ../ui.apps/src/main/content",
"watch": "npm-run-all --parallel start chokidar aemsyncro",
}
The start script will launch the webpack-dev-server observing the code changes and producing a bundle.
Then, the chokidar script is responsible for observing changes in the bundle and launching aem-clientlib-generator, which transforms the bundle into a clientlib structure and puts it in ui-apps module.
Finally, aemsyncro script starts aemsync in watch mode, observing changes in ui-apps and synchronizing them with the running AEM instance.
Developers can run watch script to launch described tools in parallel. Doing so will cover a whole process from introducing a change to the codebase to synchronizing it to AEM.
This approach allows us to detach front-end and back-end development, moving the responsibility of managing markup files (.html) to FE developers but leaving the markup source of truth in ui-apps.
However this method, while fixing one issue, introduces another - the feedback loop gets significantly slow. Code change triggers bundling, and only when it’s finished, clientlib generation is triggered. Following this, AEM sync will detect changes and put them on the instance. After all that, a manual browser refresh is required to see the changes in front-end code.
It is possible to set up live reloading (e.g. using aemfed), but it requires additional work and adds another step to this flow. Also, following this approach, useful Webpack features like Hot Reloading or in-cache bundling are not supported and disabled in the provided configuration.
To improve upon this, we need to reduce the number of steps required in the workflow by moving Webpack closer to running AEM instance. Continue reading to learn how to accomplish this.
Giving Webpack the power over ClientLibs
Note: The configuration was tested on MacOS.
We will achieve the following: webpack-dev-server will observe the source code as before, this time producing the output to memory instead of dist directory. This output will be used instead of clientlibs. The content and other assets will be served directly from AEM.
First, let’s get rid of aemsync. We can configure webpack-dev-server to serve markup from the running AEM instance but serve the clientlibs directly from the bundling output.
Let’s take a look at the example changes to the initial configuration generated by AEM project archetype:
Below '-' means 'deleted line'; '+' means 'added line'.
module.exports = env => {
...
// cleanup
- plugins: [
- new HtmlWebpackPlugin({
- template: path.resolve(__dirname, SOURCE_ROOT + '/static/index.html')
- })
- ],
devServer: {
// cleanup
- proxy: [{
- context: ['/published', '/etc.clientlibs'],
- target: 'http://localhost:4502',
- }],
+ static: {
+ directory: path.join(__dirname, 'dist/'),
+ },
+ port: 8080,
+ proxy: [
// proxy application code directly to webpack dist/
+ {
+ context: '/etc.clientlibs/wknd',
+ target: 'http://localhost:8080',
+ },
// the rest of requests proxy to AEM
+ {
+ context: () => true,
+ target: 'http://localhost:4502',
+ }
+ ],
...
},
...
}
This configuration sets up the Webpack Dev Server to serve static files directly from dist/ (bundling output directory). It also reconfigures the proxy to serve requests for clientlibs from the dev server while handling other requests from the AEM instance.
Unfortunately, there is a flaw in this configuration: for every /etc.clientlibs/wknd/* request that’s proxied back to Webpack Dev Server we will get an infinite loop and a timeout because the redirected request will get redirected again to the proxy.
In order to solve this problem, we need to bypass requests that have already been proxied:
Below '-' means 'deleted line'; '+' means 'added line'.
+// Bypass requests that have already been proxied once (avoid infinite loop)
+const bypassAlreadyProxiedRequests = {
+ xfwd: true,
+ bypass: (req) => {
+ if (req.headers['x-forwarded-for'] !== undefined) {
+ return req.path;
+ }
+ },
+};
module.exports = env => {
...
devServer: {
...
proxy: [
{
context: '/etc.clientlibs/wknd',
target: 'http://localhost:8080',
+ ...bypassAlreadyProxiedRequests
},
{
context: () => true,
target: 'http://localhost:4502',
+ ...bypassAlreadyProxiedRequests
}
],
...
},
...
}
With this configuration, we don’t need aemsync anymore, because requests for files that we provide are proxied back to Webpack Dev Server.
The next step would be to eliminate the aem-clientlib-generator. As mentioned earlier, webpack output is not compatible with AEM, it needs to be transformed to clientlib format, but this step is sequential and takes additional time.
By analyzing how clientlibs are loaded in AEM, we can configure proxy in a way that works correctly with clientlibs requests without a need to actually transform bundles.
Below '-' means 'deleted line'; '+' means 'added line'.
module.exports = env => {
...
devServer: {
...
proxy: [
{
context: '/etc.clientlibs/wknd',
target: 'http://localhost:8080',
+ pathRewrite: {
// mapping of asset files e.g.:
// `/etc.clientlibs/wknd/clientlib-site.min.14aee73c91ea79ebbdd94db033c6a6ab.js`
// to
// `/dist/clientlib-site/site.js`
// Groups:
// 1: ([a-zA-Z0-9-]+) - clientlib name
// 2: (\\.min)? - optional, dependent on `minify` option in AEM configuration of com.adobe.granite.ui.clientlibs.impl.HtmlLibraryManagerImpl
// 3: (\\.[a-z0-9]+)? - optional, hash added by AEM
// 4: ([a-z]+) - file extension
// 5: (.*) - optional, additional path parameters
+ '/etc\\.clientlibs/wknd/clientlibs/clientlib-([a-zA-Z0-9-]+)(\\.min)?(\\.[a-z0-9]+)?\\.([a-z]+)(.*)': '/clientlib-$1/$1.$4$5',
// mapping needed to access *.hot-update.(js|json) files generated by webpack HMR
+ '/etc\\.clientlibs/wknd/(.*hot-update.*)': '/$1'
},
...
],
...
}
This configuration will detect actual requests made for ClientLibs and proxy them back to Webpack Dev Server. It takes into account possible AEM configurations adding minifications or hash to clientlibs requests.
Now, we can replace npm scripts with the following:
Below '-' means 'deleted line'; '+' means 'added line'.
{
- "start": "webpack-dev-server --open --config ./webpack.dev.js",
- "chokidar": "chokidar -c \"clientlib\" ./dist",
- "aemsyncro":"aemsync -w ../ui.apps/src/main/content",
- "watch": "npm-run-all --parallel start chokidar aemsyncro",
+ "watch": "webpack-dev-server --config ./webpack.dev.js"
}
The Power of Hot Reloading
Hot Reloading (a.k.a. Hot Module Replacement) allows developers to see immediate changes in their code without losing the application's state. When a modification is made in the code, the hot reloading mechanism intelligently updates the affected components in real-time, ensuring a seamless and uninterrupted development flow. This eliminates the need for manual or automatic browser refreshes or rebuilding of the entire project, saving precious time and effort.
By leveraging the built-in hot reloading functionality of the Webpack Dev Server,we can achieve a dynamic and efficient workflow. The server acts as a bridge between AEM and the front-end code, facilitating the seamless transfer of changes to the browser. In order to enable Hot Reloading we need to add the following options to Webpack Dev Server configuration:
Below '-' means 'deleted line'; '+' means 'added line'.
module.exports = env => {
...
devServer: {
...
+ hot: 'only',
+ liveReload: false,
...
}
...
}
It will work out of the box for CSS modules, because of native support of hot reloading in style-loader. For JS code, it’s necessary to include following code in bundle entry:
if (module['hot']) {
module['hot'].accept();
}
If you want to configure Webpack hot reloading for UI frameworks like React or Vue, see the documentation.
It’s also possible to disable hot reloading, and use live reload instead.
In summary
The instant feedback from hot reloading feedback not only boosts productivity but also enhances the overall development experience. It empowers developers to experiment, iterate, and innovate with confidence, resulting in faster delivery of high-quality applications.