DEV Community

Cover image for How to build a webpack assets loader script
Giannis Koutsaftakis
Giannis Koutsaftakis

Posted on • Edited on

How to build a webpack assets loader script

Sometimes we need to load our webpack bundled JavaScript app into a host application that we don't have access to the HTML page's <head> or the capability to provide our own index.html. Scripts and CSS are usually loaded dynamically and injected into the page by the host application at runtime. We only get a location to upload the assets of our app, a place to define the JavaScript files that need loading, and a div id in which our app will mount.

Now, every time our project is built for production, webpack generates new chunk ids for the output files. In a common scenario, usually, these are inserted automatically into the index.html template file with HtmlWebpackPlugin. In this given scenario though since we don't have any access to the index.html, we have to manually define in the host application the JS and CSS files needed for our app to load. Things get more complicated if our bundle is split into multiple initial chunks (which it should).

Let's see how we can create a webpack assets loader script that will automate the process of loading all JS and CSS assets with their correct chunk ids and display a nice progress bar while doing so. We'll use webpack 5 but the setup is similar for webpack 4 too.

Part 1 - the loader

First, let's start by creating a folder named loader into our project's src folder that will contain all the necessary files. We're gonna need a JavaScript loader that we'll actually fetch the JS and CSS using XHR and inject them into the page. There are a lot of solutions floating around the web but we are going to create a super simple one using this excellent post by David Walsh as a base.
src/loader/assets-loader.js

const filetypeToTag = {
  css: 'link',
  js: 'script'
}

export function load (url) {
  const filetype = url.split('.').pop()
  const tag = filetypeToTag[filetype]
  return new Promise((resolve, reject) => {
    const element = document.createElement(tag)
    let parent = 'body'
    let attr = 'src'
    element.onload = () => {
      resolve(url)
    }
    element.onerror = () => {
      reject(url)
    }
    switch (tag) {
      case 'script':
        element.async = false
        break
      case 'link':
        element.type = 'text/css'
        element.rel = 'stylesheet'
        attr = 'href'
        parent = 'head'
    }
    element[attr] = url
    document.head.appendChild(element)
  })
}
Enter fullscreen mode Exit fullscreen mode

We can then simply load a JS or CSS asset with:

load('example.js').then(() => {
  // asset is now loaded
})
Enter fullscreen mode Exit fullscreen mode

Part 2 - the progressbar

Next, we are going to create our HTML template for the loading animation that will display while the assets are downloading. This will consist of an image (that could be the branding image of our website) and an animating progress bar.
src/loader/loader.html

<style type="text/css">
  #loader {
    position: fixed;
    background-color: #fff;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 9999;
    display: flex;
    justify-content: center;
  }
  #loader .container {
    top: 30%;
    position: relative;
  }
  #loader .image {
    background:url('{{logoLoader}}');
    width: 130px;
    background-size: 100%;
    height: 141px;
    margin: 0px auto 40px auto;
    opacity: 0;
    animation: loader-fade 0.25s ease-in-out forwards;
  }
  #loader .progress {
    width: 300px;
    height: 8px;
    overflow: hidden;
    background-color: #ddd;
    margin: 0 auto 0 auto;
    text-align: center;
    border-radius: 10px;
    opacity: 0;
    animation: loader-fade 0.7s ease-in-out forwards;
  }
  #loader .progress .progress-bar {
    width: 0%;
    max-width: 100%;
    height: 8px;
    overflow: hidden;
    position: relative;
    background-color: #1c78c0;
    animation-name: loader-progress;
    animation-duration:  20s;
    animation-timing-function: cubic-bezier(0.024, 0.9, 0.024, 0.9);
    animation-delay: 0.5s;
    animation-iteration-count: 1;
    animation-fill-mode: forwards;
    border-radius: 10px;
  }
  #loader .progress .progress-bar .shimmer  {
    position: absolute;
    left: -400px;
    top: 0px;
    animation-name: loader-shimmer;
    animation-duration: 1.1s;
    animation-timing-function: linear;
    animation-delay: 0.65s;
    animation-iteration-count: infinite;
    z-index: 10;
  }
  #loader .progress .progress-bar .shimmer > div {
    position: absolute;
    width: 400px;
    height: 8px;    
    background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.35) 45%, rgba(255,255,255,0.45) 60%, rgba(255,255,255,0.55) 75%, rgba(255,255,255,0) 100%);
  }
  @keyframes loader-progress {
    to {
      width: 95%
    }
  }
  @keyframes loader-shimmer {
    0% {
      transform: translateX(-50px);
      animation-timing-function: cubic-bezier(0.85, 0, 0.64, 1);
    }
    98.36% {
      transform: translateX(600px);
      animation-timing-function: linear;
    }
    100% {
      transform: translateX(600px);
    }
  }
  @keyframes loader-fade {
    to {
      opacity: 1;
    }
  }
</style>

<div id="loader">
  <div class="container">
    <div class="image"></div>
    <div class="progress">
      <div class="progress-bar">
        <div class="shimmer">
          <div></div>
        </div>
      </div>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

We are later going to replace {{logoLoader}} with a base64 string of our image. You can view the final result of the progress bar animation in this codesandbox here.

Part 3 - webpack configuration

Now for the tricky part. We need to get all the built files emitted by webpack in order to feed them into our loader. HtmlWebpackPlugin to the rescue! Although usually HtmlWebpackPlugin is used to generate an HTML file, we're going to use HtmlWebpackPlugin's template option to generate a JavaScript file instead. That will export the list of webpack build files as an Array.
If not already present inside your webpack build config, you can follow the instructions here to get it installed.

Ok, now for some changes in our webpack.config.js. We' are going to create a separate webpack config for our loader so we'll need to alter the project's webpack config to incorporate multiple configs. So basically now you'll have your webpack config as an array of objects rather than a single object.

Inside our existing app config, we're going to add a section for HtmlWebpackPlugin. In our loader config we'll have src/loader/loader.js as an entry point.

webpack.config.js

...
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = [
  // App config here...
  {
    entry: ' ... ',
    output { ... },
    plugins: [ 
    ...
      new HtmlWebpackPlugin({
        template: 'src/loader/files-template.js',
        filename: '../src/loader/files.js',
        inject: false,
        publicPath: ''
      })
    ],
    ...
  },
  // Loader config here...
  {
    entry: './src/loader/loader.js',
    output: {
      filename: 'loader.js',
      path: path.resolve(__dirname, 'dist')
    },
    module: {
      rules: [
        {
          test: /\.html$/i,
          loader: 'html-loader'
        },
        {
          test: /\.png/,
          type: 'asset/inline'
        }
      ]
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

The html-loader plugin is used so that we can import our progress bar template HTML file as a string. We are using the assets module feature of webpack 5 to have our logo image imported as a base64 string. You can achieve the same result in webpack 4 using url-loader.

Let's now create the HtmlWebpackPlugin template file. Create a new file files-template.js and put the following inside
src/loader/files-template.js

module.exports = templateParams => {
  const files = templateParams.htmlWebpackPlugin.files
  const assets = files.js.concat(files.css)
  return `export default ${JSON.stringify(assets)}`
}
Enter fullscreen mode Exit fullscreen mode

This will generate a file (/src/loader/files.js as defined previously in our config) that will export all JS and CSS assets of the webpack build as an Array.

Part 4 - Putting it all together

Finally, let's create our loader entry point that will be used to inject the loading animation into the page (into an #app div) and load the webpack assets.
src/loader/files-template.js

import files from './files.js'
import { load } from './assets-loader.js'
import logoLoader from './logo-loader.png'
import template from './loader.html'

document.addEventListener('DOMContentLoaded', () => {
  document.querySelector('#app').insertAdjacentHTML('beforeend', template.replace('{{logoLoader}}', logoLoader))

  Promise.all(files.map(o => load(o)))
})
Enter fullscreen mode Exit fullscreen mode

That's it! Now when we run our build (usually with npm run build) a new file loader.js will be generated inside our dist directory. That's the only script we need to load our app!
loader.js can even be injected dynamically into the page and our app will load.

You can find an example GitHub repo with all the source code as well as a live demo here.
Have fun loading!

Top comments (0)