erzki
erzki

Reputation: 311

Webpack with small initial script and async loading of all other scripts

I have started using Webpack when developing usual web sites consisting of a number pages and of different pages types. I'm used to the RequireJs script loader that loads all dependencies on demand when needed. Just a small piece of javascript is downloaded when page loads.

What I want to achieve is this:

I have tried many configurations to achieve this but with no success.

entry: {
    main: 'main.js', //Used on all pages, e.g. mobile menu
    'standard-page': 'pages/standard-page.js',
    'start-page': 'pages/start-page.js',
    'vendor': ['jquery']
},
alias: {
    jquery: 'jquery/dist/jquery.js'
},
plugins: [
    new webpack.optimize.CommonsChunkPlugin("vendor", "vendor.js"),
    new webpack.optimize.CommonsChunkPlugin('common.js')
]

In the html I want to load the javascripts like this:

<script src="/Static/js/dist/common.js"></script>
<script src="/Static/js/dist/main.js" async></script>

And on a specific page type (start page)

<script src="/Static/js/dist/start-page.js" async></script>

common.js should be a tiny file for fast loading of the page. main.js loads async and require('jquery') inside.

The output from Webpack looks promising but I can't get the vendors bundle to load asynchronously. Other dependencies (my own modules and domReady) is loaded in ther autogenerated chunks, but not jquery.

I can find plenty of examples that does almost this but not the important part of loading vendors asynchronously.

Output from webpack build:

                  Asset       Size  Chunks             Chunk Names
            main.js.map  570 bytes    0, 7  [emitted]  main
                main.js  399 bytes    0, 7  [emitted]  main
       standard-page.js  355 bytes    2, 7  [emitted]  standard-page
c6ff6378688eba5a294f.js  348 bytes    3, 7  [emitted]
          start-page.js  361 bytes    4, 7  [emitted]  start-page
8986b3741c0dddb9c762.js  387 bytes    5, 7  [emitted]
              vendor.js     257 kB    6, 7  [emitted]  vendor
              common.js    3.86 kB       7  [emitted]  common.js
2876de041eaa501e23a2.js     1.3 kB    1, 7  [emitted]  

Upvotes: 16

Views: 18505

Answers (4)

Grzegorz T.
Grzegorz T.

Reputation: 4113

Some time ago I made such a small "Proof of concept" to check how importlazy will work in IE11. I have to admit it works :) After clicking the button, the code responsible for changing the background color of the page is loaded - full example

Js:

// polyfils for IE11
import 'core-js/modules/es.array.iterator';

const button = document.getElementById('background');

button.addEventListener('click', async (event) => {
  event.preventDefault();
  try {
    const background = await import(/* webpackChunkName: "background" */ `./${button.dataset.module}.js`);
    background.default();
  } catch (error) {
    console.log(error);
  }
})

Html:

<button id="background" class="button-primary" data-module="background">change the background</button>

Upvotes: 1

Ted Fitzpatrick
Ted Fitzpatrick

Reputation: 928

I've recently travelled this same road, I'm working on optimizing my Webpack output since I think bundles are too big, HTTP2 can load js files in parallel and caching will be better with separate files, I was getting some dependencies duplicated in bundles, etc. While I got a solution working with Webpack 4 SplitChunksPlugin configuration, I'm currently moving towards using mostly Webpack's dynamic import() syntax since just that syntax will cause Webpack to automatically bundle dynamically imported bundles in their own file which I can name via a "magic comment":

import(/* webpackChunkName: "mymodule" */ "mymodule"); // I added an resolve.alias.mymodule entry in Webpack.config

Upvotes: 1

mpen
mpen

Reputation: 282825

Here's the solution I came up with.

First, export these two functions to window.* -- you'll want them in the browser.

export function requireAsync(module) {
    return new Promise((resolve, reject) => require(`bundle!./pages/${module}`)(resolve));
}

export function runAsync(moduleName, data={}) {
    return requireAsync(moduleName).then(module => {
        if(module.__esModule) {
            // if it's an es6 module, then the default function should be exported as module.default
            if(_.isFunction(module.default)) {
                return module.default(data);
            }
        } else if(_.isFunction(module)) {
            // if it's not an es6 module, then the module itself should be the function
            return module(data);
        }
    })
}

Then, when you want to include one of your scripts on a page, just add this to your HTML:

<script>requireAsync('script_name.js')</script>

Now everything in the pages/ directory will be pre-compiled into a separate chunk that can be asynchronously loaded at run time, only when needed.

Furthermore, using the functions above, you now have a convenient way of passing server-side data into your client-side scripts:

<script>runAsync('script_that_needs_data', {my:'data',wow:'much excite'})</script>

And now you can access it:

// script_that_needs_data.js
export default function({my,wow}) {
    console.log(my,wow);
}

Upvotes: 1

Jamund Ferguson
Jamund Ferguson

Reputation: 17014

The solution to this problem is two-fold:

  1. First you need to understand how code-splitting works in webpack
  2. Secondly, you need to use something like the CommonsChunkPlugin to generate that shared bundle.

Code Splitting

Before you start using webpack you need to unlearn to be dependent on configuration. Require.js was all about configuration files. This mindset made it difficult for me to transition into webpack which is modeled more closely after CommonJS in node.js, which relies on no configuration.

With that in mind consider the following. If you have an app and you want it to asynchronously load some other parts of javascript you need to use one of the following paradigms.

Require.ensure

require.ensure is one way that you can create a "split point" in your application. Again, you may have thought you'd need to do this with configuration, but that is not the case. In the example when I hit require.ensure in my file webpack will automatically create a second bundle and load it on-demand. Any code executed inside of that split-point will be bundled together in a separate file.

require.ensure(['jquery'], function() {
    var $ = require('jquery');
    /* ... */
});

Require([])

You can also achieve the same thing with the AMD-version of require(), the one that takes an array of dependencies. This will also create the same split point:

require(['jquery'], function($) {
    /* ... */
});

Shared Bundles

In your example above you use entry to create a vendor bundle which has jQuery. You don't need to manually specify these dependency bundles. Instead, using the split points above you webpack will generate this automatically.

Use entry only for separate <script> tags you want in your pages.

Now that you've done all of that you can use the CommonsChunkPlugin to additional optimize your chunks, but again most of the magic is done for you and outside of specifying which dependencies should be shared you won't need to do anything else. webpack will pull in the shared chunks automatically without the need for additional <script> tags or entry configuration.

Conclusion

The scenario you describe (multiple <script> tags) may not actually be what you want. With webpack all of the dependencies and bundles can be managed automatically starting with only a single <script> tag. Having gone through several iterations of re-factoring from require.js to webpack, I've found that's usually the simplest and best way to manage your dependencies.

All the best!

Upvotes: 14

Related Questions