Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Webpack with two common chunks: one exported, one local

I would like to use Webpack in a multi-page application in such a way that some pre-determined dependencies are bundled into a "vendor" chunk and the rest of the dependencies is bundled into a "commons" chunk.

For example, assuming two entry points (effectively representing a different page each), pageA.js and pageB.js contain both this code (in EC6, processed via Babel), followed by their own code:

import $ from 'jquery';
require('bootstrap/dist/css/bootstrap.css');
import angular from 'angular';
import uitree from 'angular-ui-tree';

I'd like jQuery and Bootstrap to be bundled into a "vendor" chunk, and the rest (whatever that is) to be bundled into a "commons" chunk.

The objectives are:

  • I would like to be able to have another separate build that would be able to rely on that same vendor chunk, without it needing to re-include the vendor libraries (I would explicitly declare that set of vendor libraries, to make it available to any sub-build that needs it).
  • I would also like not to have to re-process the vendor chunk every time I make a change to a page's script.

Here is the configuration I've tried:

module.exports = {
    entry : {
        "vendor" : [ "jquery", "bootstrap" ],
        "pageA" : "./src/pageA.js",
        "pageB" : "./src/pageB.js"
    },
    output : {
        path : path.join(__dirname, "./dest"),
        filename : "[name].chunk.js"
    },
    module : {
        loaders : [ {
            test : /bootstrap\/js\//,
            loader : 'imports?jQuery=jquery'
        },
        /* ... Some modules for angular and CSS processing ... */

        {
            test : /\.js?$/,
            include : [ path.resolve(__dirname, "./src") ],
            loader : 'babel',
            query : {
                presets : [ 'es2015' ]
            }
        }
        /* ... Some other settings for the fonts ... */ ]
    },
    plugins : [
        new webpack.ProvidePlugin({
            $ : "jquery",
            jQuery : "jquery"
        }),
        new webpack.optimize.UglifyJsPlugin({
            sourceMap : false,
            mangle : false,
            compress : false
        }),
        new CommonsChunkPlugin({
            name : "vendor",
            minChunks : Infinity
        }),
        new CommonsChunkPlugin({
            name : "commons",
            minChunks : 2
        })
    ]
};

With the configuration above, I get jQuery and Bootstrap in vendor.chunk.js, as required, but the commons.chunk.js file is almost empty, all the rest of what's commonly used by pageA.js and pageB.js is then put into pageA.chunk.js and pageB.chunk.js (effectively duplicating that code).

If I swap the order of the last two plugins, commons.chunk.js now contains almost everything (unless what's actually specific to pageA.js and pageB.js), and vendor.chunk.js is almost empty:

plugins : [
    // ...,
    new CommonsChunkPlugin({
        name : "commons",
        minChunks : 2
    }),
    new CommonsChunkPlugin({
        name : "vendor",
        minChunks : Infinity
    })
]

Is there a way to bundle a pre-defined list of libraries (e.g. [ "jquery", "jquery-ui", "bootstrap" ] into one particular chunk (in such a way that it can be used by completely independent scripts) and also have another common chunk for whatever else is in commonly used between the entry points?

The aim of all this would be to be able to build a completely separate piece of code for another page later, and tell it it doesn't need to re-bundle those pre-defined libraries.

Here is a diagram representing what I'm trying to achieve:

Dependencies Diagram

I would then use the generated scripts as follows on page A:

<script src="vendor.js"></script>
<script src="common.js"></script>
<script src="pageA.chunk.js"></script>

And on page C (built completely independently from pages A and B):

<script src="vendor.js"></script>
<script src="common2.js"></script>
<script src="pageC.chunk.js"></script>

(I am using Webpack 1.12.14.)


I have tried the solution suggested in the only answer so far. While this makes it indeed possible to separate the vendor chunk from the commons chunk, the vendor chunks (with the same definition) made from two separate builds generally cannot be swapped between each other. This does not make it possible to use only one of those vendor chunks across two builds (even though they are virtually the same).

like image 368
Bruno Avatar asked Feb 23 '16 18:02

Bruno


People also ask

How does webpack code splitting work?

In React, code splitting involves applying the dynamic import() syntax to your components so that webpack automatically split the imported component as a separate bundle.

Can webpack split code into separate files?

Code splitting is one of the most compelling features of webpack. This feature allows you to split your code into various bundles which can then be loaded on demand or in parallel.

What is split chunk?

splitChunks.If the current chunk contains modules already split out from the main bundle, it will be reused instead of a new one being generated. This can affect the resulting file name of the chunk. webpack.config.js module.

What is bundling webpack?

Webpack is an aggressive and powerful module bundler for JavaScript applications. It packages all the modules in your application into one or more bundles (often, just one) and serves it to the browser.


2 Answers

I was working on something similar. I observed the behaviour you desire by having this configuration:

const entryPath = path.resolve(__dirname, "src", "modules"),
    entryPoints = {
        vendor: ["babel-polyfill", "react", "react-dom", "react-pure-render", "react-simpletabs", "react-redux", "redux", "redux-thunk"],
        a: entryPath + "/a.js",
        b: entryPath + "/b.js",
        c: entryPath + "/c.js",
        d: entryPath + "/d.js",
        ...
    }, plugins = [
        new CommonsChunkPlugin({
            name: "commons",
            filename: "commons.bundle.js",
            minChunks: 2,
            chunks: Object.keys(entryPoints).filter(key => key !== "vendor")
        }),
        new CommonsChunkPlugin("vendor", "vendor.bundle.js"),
        new ExtractTextPlugin("[name].bundle.css", {
            //allChunks: true
        })
    ];

so this code above left in b.bundle.js still some import of React and other React libraries. after I added "fixed-data-table" to the vendor list, that disappeared and the only react source code there was was in vendor.bundle.js I assume that is what you were looking for? (in the end each vendor not listed in vendor list ended up in each own module.bundle.js or in commons.bundle.js if it was re-used in multiple bundles)

Regards Jonas

like image 197
jonas.hartwig Avatar answered Oct 20 '22 13:10

jonas.hartwig


As far as your request to be able to use the vendor chunk created in one webpack build with bundles from another build, the only way I have found to do this is through a combination of expose-loader and externals.

In my case, I successfully used this to bundle jQuery (and Bootstrap) into a "common.js" in one build, and then use that jQuery in modules belonging to another build, as follows:

1. Expose jQuery from build A chunk

module: {
  loaders: [
    {
      // export jQuery globals for other modules to use as externals
      test: require.resolve('jquery'),
      loader: 'expose?$!expose?jQuery'
    }
  ]
}

2. Consume jQuery in build B modules

externals: {
  'jquery': 'jQuery'
}

Of course, the drawback to this is that you're bridging between the two sets of bundles by using manually-managed global variables, which you probably started using webpack to avoid :-)

However, I don't know of any other way at this point, given that every webpack build creates its own namespace of "private" IDs internally to reference modules, with no guarantee of stability or global unique-ness.

like image 2
ryan_effectiveui Avatar answered Oct 20 '22 13:10

ryan_effectiveui