Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to split code in Webpack with require.ensure for production?

I'm using Webpack and I would like to split my client code into several pieces and load them once user needs them.

Here's my file structure:

app.js       <-- Entry point as introduced to Webpack
Module.js    <-- To be loaded dynamically

There's no direct connection between app.js and Module.js, instead the second file is loaded by the first like this:

require.ensure([], (require) => {
    let path = "Module";
    let module = require("./" + path).default;
});

I used "./" + path just to make sure Webpack won't go smart on me and try to resolve the path statically. Anyways, this code works in development mode with webpack-dev-server. By that I mean the Module.js is not downloaded until I trigger the event to run the above code. And after that, it is loaded and executed perfectly.

But when I pack the project for production, it stops working. It gives out the following error (in browser when I trigger the download event) without even trying to send the request:

Uncaught Error: Cannot find module './Module'.

Furthermore, when I compose the path dynamically (like the above code), building process gives out the following warning:

WARNING in ./src/app/app.js Critical dependencies: 74:34-47 the request of a dependency is an expression

What's the right way to configure Webpack for the production so it supports code splitting to be downloaded dynamically?


I've tested the solution given by @wollnyst and here are the results. When I implement it like this, it works:

require.ensure(["./Module"], (require) => {
    let path = "Module";
    let module = require("./" + path).default;
});

But that's not how I want it, I want it like this:

let path = "Module";
require.ensure(["./" + path], (require) => {
    let module = require("./" + path).default;
});

Now it gives out a runtime error in browser:

Uncaught TypeError: webpack_require(...).ensure is not a function

Still no luck!

like image 580
Mehran Avatar asked May 14 '16 04:05

Mehran


2 Answers

Putting the path you want to require dynamically in the require.ensure array first argument is wrong and should not be done. This array is intended to be used for the dependencies of the module you want to load dynamically and that are needed for the async code in your callback to run safely.

Critical part in understanding how webpack can handle the code splitting is that you can't do fully dynamic statements, as webpack needs some location information to resolve the module you want to require, thus the dependency is an expression warning. Even if you can go smart like you did by preprending the ./, it is preferable to actually repeat the full ensure statement with the static module path string even if you have a lot of modules to dynamically load and it's a bit obnoxious, so that you won't hit any issue.

Another way to do it would be to create a folder dedicated to that purpose, splits for example, and resolve all your modules from here. But that would mean that every module you require in this directory would be in the same split point, not really what would anyone want except for specific use-cases.


Regarding the configs, you will need to use the namedModulesPlugins and the CommonsChunkPlugin one. What I personally do is having a main.js that contains all the common sources, a vendor.js, containing all the common node_modules, and a runtime.js (required by webpack). Then, the chunks are separated and if one depends on a specific vendor dependency, it will be added to this specific chunk rather than the common vendor.js.

plugins: [
  new webpack.NamedModulesPlugin(),
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks: module => module.context && module.context.indexOf('node_modules'),
  }),
  new webpack.optimize.CommonsChunkPlugin({
    name: 'runtime',
  }),
],

You might also make sure to have something like the following output.filename:

output: {
  filename: '[name]-[chunkhash].js',
}

Otherwise the hash of your vendor.js will change everytime your sources change, even if you didn't modified your deps, which is bad if you're concerned about caching.

The recent webpack visualizer is a very good tool to inspect what your bundle and chunks look like and to verify things looks okay or detect dependencies that could be chunked better. You might need to change a bit your config if using the StatsWriterPlugin to collect the stats so that it handles your chunks:

new StatsWriterPlugin({
  fields: null,
  transform: (data, opts) => {
    const stats = opts.compiler.getStats().toJson({ chunkModules: true })
    return JSON.stringify(stats, null, 2)
  },
}),

One thing to note too, is that require.ensure is specific to webpack and is being superseded by the import() that it now also handles. Since this question is from 2016 and it might not have the same elements we have now with webpack 2, I'm gonna expand a bit on it.

Now, the dynamic import proposal is on its way into ES, and you can make use of it in webpack. You'll require a Promise polyfill and something like babel syntax-dynamic-import plugin (and that should now be in the stage-3 preset) or the dynamic-import-webpack if that doesn't work for you yet (which basically transform import() to ensures).

You'll still need to specify the full path like with ensure if you want to get a chunk per module, but that's a better syntax and more oriented in the future.

There's a lot of other resources you could look into on the new webpack documentation page about code splitting.

like image 176
Preview Avatar answered Nov 09 '22 23:11

Preview


You need to pass the module you want to require in the first argument of require.ensure:

require.ensure(['./Module'], function(require) {
    const module = require('./Module');
});
like image 34
wollnyst Avatar answered Nov 10 '22 00:11

wollnyst