bholtbholt
bholtbholt

Reputation: 13383

How to share the same instance of jQuery across multiple Webpack packs?

I'm working in a Rails app, migrating from Sprockets to Webpack. Our current JavaScript relies on libraries globally assigned to window. In order to migrate over, I'd like to maintain global assignment until we address it at a later date. I'd like to swap from Sprockets to Webpack without any JavaScript changes.

We have several JavaScript bundles, which have been manually split up for performance:

All the bundles rely on jQuery, and application.js relies on vendor.js. That's fine for Sprockets, but is an issue with Webpack.

How can I keep jQuery in core.js, but exclude it from vendor.js and application.js in Webpack? or another way to ask this is How do I share the same instance of jQuery across multiple webpack bundles? I need to use the same instance because application.js relies on jQuery plugins defined in vendor.js

My config file includes both expose-loader and ProvidePlugin, but these include jQuery in the bundle itself, which is not the same instance. In other words, I'm getting jQuery bundled multiple times (which is fixed with splitChunks()), but I can't guarantee which instance I'm using, therefore can't guarantee the plugin is available.

// Webpacker environment.js config

const { environment } = require('@rails/webpacker');
const path = require('path');
const webpack = require('webpack');

environment.loaders.append('expose', {
  test: require.resolve('jquery'),
  use: [
    {
      loader: 'expose-loader',
      options: 'jQuery'
    },
    {
      loader: 'expose-loader',
      options: '$'
    }
  ]
});

environment.plugins.append(
  'Provide',
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery'
  })
);


module.exports = environment;

Upvotes: 4

Views: 3092

Answers (4)

rossta
rossta

Reputation: 11504

I have had success with splitChunks forcing a single instance for jquery combined with the expose-loader config like you have described.

Here's the key part of the Webpack config I'm using:

environment.splitChunks((config) => {
  return Object.assign({}, config, {
    optimization: {
      splitChunks: {
        chunks: 'all',
        name: true,
        cacheGroups: {
          vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10,
          },
          default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true,
          },
        },
      },
    },
  })
})

The key here is to understand that Webpack splitChunks replaces the need for manual code splitting. In other words, let go of the need to explicitly create core.js and vendor.js packs; consolidate all your imports as part of the application.js dependency tree. Let Webpack do the code-splitting for you.

Another caveat to mention is that Webpacker's implementation of splitChunks means placing a single javascript_packs_with_chunks_tag in the view so that the shared "runtime" chunk will ensure that modules, like jQuery, aren't duplicated.

I used to split my bundles manually like you in my previous projects with Sprockets. When I had to do my own migration to Webpack, it took awhile for this new approach to sink in. I gave a talk about it which will shed some more light on my answer here: https://youtu.be/fKOq5_2qj54?t=185

Upvotes: 4

Herz3h
Herz3h

Reputation: 712

The only two options that were working in my case were:

1) Adding jquery to externals like this:

module.exports = {
    ...
    externals: {
    'jquery': 'jQuery'
    },
};

putting a script tag in a file that you include everywhere (in my case was base.html.twig, and everything else extends from this).

<script src="/path/to/jquery/jquery-2.2.3.min.js"></script>

2) Using ProvidePlugin + runtimeChunk: single:

module.exports = {
    ...
    plugins: [
        new webpack.ProvidePlugin({
            $: 'jquery',
            jQuery: 'jquery',
            "window.jQuery": 'jquery',
            "window.$": 'jquery',
        })
    ],
    optimization: {
        runtimeChunk: 'single',
    }
}

runtimeChunk single will generate a new file that you have to include in a page that's included everywhere (basically wherever you have an entrypoint you include this runtime chunk !)

Upvotes: 2

bholtbholt
bholtbholt

Reputation: 13383

While I don't have a solution yet, I've found setting jQuery as an external library seems to force a single global instance.

// Webpacker environment.js config

const { environment } = require('@rails/webpacker');
const path = require('path');
const webpack = require('webpack');

environment.config.set('externals', {
  $: 'jquery',
  jquery: 'jQuery'
});

module.exports = environment;

I've been able to test this by using my Sprockets core.js bundle and Webpack for the everything else. It all works as expected.

I've posted a follow-up question to try to conditionally set externals and use Webpack for everything: Conditionally set externals for a specific Webpack bundle

Upvotes: 1

Dijkie85
Dijkie85

Reputation: 1106

Not really sure if this is will work, but maybe you could try:

# vendor.js

import JQuery from 'jquery' 
window.$ = window.JQuery = JQuery

// rest of vendor.js code


#application.js
import './vendor.js'

//rest of application.js code

Upvotes: 0

Related Questions