Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create individual SPA bundles with Webpack

How do I use Webpack to create independent SPA bundles that may or may not be loaded on the fly as my user navigates my SPA?

I have a contacts module, and a tasks module. Both have two dependencies. I want WebPack to create bundles for each that are loaded when (and if) needed.

The code is below. The problem appears to be that each of these entries are being seen as application entry points, and so are getting webpack bootstrap code inserted therein.

I've seen various examples with CommonsChunkPlugin but I can't find an API reference / documentation for it, and from what I can surmise, that's not what I want anyway.

Edit - found those docs here, and added an attempt with that plugin below in my edit.


Current configuration

module.exports = {     entry: {         contacts: './contacts',         tasks: './tasks'     },     output: {         path: path.resolve(__dirname, 'build'),         filename: '[name]-bundle.js'     } }; 

Contacts.js

define(['./ca', './cb'], function(ca, cb){     var name = 'Contacts';     alert(ca + ' ' + cb); }); 

Tasks.js

define(['./ta', './tb'], function(ta, tb){     var name = 'TASKS Main';     alert(ta + ' ' + tb); }); 

tasks-bundle.js

/******/ (function(modules) { // webpackBootstrap /******/    // The module cache /******/    var installedModules = {};  /******/    // The require function /******/    function __webpack_require__(moduleId) {  /******/        // Check if module is in cache /******/        if(installedModules[moduleId]) /******/            return installedModules[moduleId].exports;  /******/        // Create a new module (and put it into the cache) /******/        var module = installedModules[moduleId] = { /******/            exports: {}, /******/            id: moduleId, /******/            loaded: false /******/        };  /******/        // Execute the module function /******/        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);  /******/        // Flag the module as loaded /******/        module.loaded = true;  /******/        // Return the exports of the module /******/        return module.exports; /******/    }   /******/    // expose the modules object (__webpack_modules__) /******/    __webpack_require__.m = modules;  /******/    // expose the module cache /******/    __webpack_require__.c = installedModules;  /******/    // __webpack_public_path__ /******/    __webpack_require__.p = "";  /******/    // Load entry module and return exports /******/    return __webpack_require__(0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ function(module, exports, __webpack_require__) {      var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(3), __webpack_require__(4)], __WEBPACK_AMD_DEFINE_RESULT__ = function(ta, tb){         var name = 'TASKS Main';         alert(ta + ' ' + tb);     }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));  /***/ }, /* 1 */, /* 2 */, /* 3 */ /***/ function(module, exports, __webpack_require__) {      var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_RESULT__ = function(){         var name = 'TASKS - A';         alert('ta');     }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));  /***/ }, /* 4 */ /***/ function(module, exports, __webpack_require__) {      var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_RESULT__ = function(){         var name = 'TASKS - B';         alert('tb');     }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));  /***/ } /******/ ]); 

EDIT

Here's my attempt number 2 with the CommonsChunkPlugin. I created a dummy app.js

app.js

var module = window.location.hash.split('/')[0]; alert(module); 

Then I moved all of my contacts and tasks files under a components folder, but otherwise left them alone. My new configuration:

module.exports = {     entry: {         app: './app'     },     output: {         path: path.resolve(__dirname, 'build'),         filename: '[name]-bundle.js'     },     plugins: [         new webpack.optimize.CommonsChunkPlugin({             name: './components/contacts',             filename: 'contacts-component-bundle.js'         }),         new webpack.optimize.CommonsChunkPlugin({             name: './components/tasks',             filename: 'tasks-component-bundle.js'         })     ] }; 

Bizarely, now app-bundle.js appears to not have any Webpack bootstrap code

webpackJsonp([0,1,2],[ /* 0 */ /***/ function(module, exports) {      var module = window.location.hash.split('/')[0];     alert(module);  /***/ } ]); 

contacts-components-bundle.js now just has this

webpackJsonp([1,2],[]); 

and tasks-components-bundle.js appears to have all of my webpack bootstrap code

/******/ (function(modules) { // webpackBootstrap /******/    // install a JSONP callback for chunk loading /******/    var parentJsonpFunction = window["webpackJsonp"]; /******/    window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) { /******/        // add "moreModules" to the modules object, /******/        // then flag all "chunkIds" as loaded and fire callback /******/        var moduleId, chunkId, i = 0, callbacks = []; /******/        for(;i < chunkIds.length; i++) { /******/            chunkId = chunkIds[i]; /******/            if(installedChunks[chunkId]) /******/                callbacks.push.apply(callbacks, installedChunks[chunkId]); /******/            installedChunks[chunkId] = 0; /******/        } /******/        for(moduleId in moreModules) { /******/            modules[moduleId] = moreModules[moduleId]; /******/        } /******/        if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules); /******/        while(callbacks.length) /******/            callbacks.shift().call(null, __webpack_require__); /******/        if(moreModules[0]) { /******/            installedModules[0] = 0; /******/            return __webpack_require__(0); /******/        } /******/    };  /******/    // The module cache /******/    var installedModules = {};  /******/    // object to store loaded and loading chunks /******/    // "0" means "already loaded" /******/    // Array means "loading", array contains callbacks /******/    var installedChunks = { /******/        2:0, /******/        1:0 /******/    };  /******/    // The require function /******/    function __webpack_require__(moduleId) {  /******/        // Check if module is in cache /******/        if(installedModules[moduleId]) /******/            return installedModules[moduleId].exports;  /******/        // Create a new module (and put it into the cache) /******/        var module = installedModules[moduleId] = { /******/            exports: {}, /******/            id: moduleId, /******/            loaded: false /******/        };  /******/        // Execute the module function /******/        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);  /******/        // Flag the module as loaded /******/        module.loaded = true;  /******/        // Return the exports of the module /******/        return module.exports; /******/    }  /******/    // This file contains only the entry chunk. /******/    // The chunk loading function for additional chunks /******/    __webpack_require__.e = function requireEnsure(chunkId, callback) { /******/        // "0" is the signal for "already loaded" /******/        if(installedChunks[chunkId] === 0) /******/            return callback.call(null, __webpack_require__);  /******/        // an array means "currently loading". /******/        if(installedChunks[chunkId] !== undefined) { /******/            installedChunks[chunkId].push(callback); /******/        } else { /******/            // start chunk loading /******/            installedChunks[chunkId] = [callback]; /******/            var head = document.getElementsByTagName('head')[0]; /******/            var script = document.createElement('script'); /******/            script.type = 'text/javascript'; /******/            script.charset = 'utf-8'; /******/            script.async = true;  /******/            script.src = __webpack_require__.p + "" + chunkId + "." + ({"0":"app","1":"./components/contacts"}[chunkId]||chunkId) + "-bundle.js"; /******/            head.appendChild(script); /******/        } /******/    };  /******/    // expose the modules object (__webpack_modules__) /******/    __webpack_require__.m = modules;  /******/    // expose the module cache /******/    __webpack_require__.c = installedModules;  /******/    // __webpack_public_path__ /******/    __webpack_require__.p = ""; /******/ }) /************************************************************************/ /******/ ([]); 

Again, I'm just trying to use Webpack to get an SPA proof of concept up and running, with some sort of root app.js entry point, and then some arbitrary number of modules / components which are loaded on demand. This is trivially easy with requirejs, so I have to imagine I'm missing something key here, especially with all the articles I've seen talking about how great Webpack is for SPAs.


EDIT 2

Per bebraw's answer below I tried the following:

app.js

var mod = window.location.hash.split('/')[0]; alert(mod);  require.ensure([], function() {     require('./components/' + mod).show(); }); 

webpack.config.js

var path = require('path'); var webpack = require('webpack');  module.exports = {     entry: {         app: './app'     },     output: {         path: path.resolve(__dirname, 'build'),         filename: '[name]-bundle.js'     } }; 

And then in my build folder I'm left with app-bundle.js which has all my bootstrap code, and my app.js code, and then 1.1-bundle.js which has all of my tasks and contacts code.

I've also tried this

module.exports = {     entry: {         app: './app'     },     output: {         path: path.resolve(__dirname, 'build'),         filename: '[name]-bundle.js'     },     plugins: [         new webpack.optimize.CommonsChunkPlugin({             name: './components/contacts',             filename: 'contacts-component-bundle.js',             children: true         }),         new webpack.optimize.CommonsChunkPlugin({             name: './components/tasks',             filename: 'tasks-component-bundle.js',             children: true         })     ] }; 

Which yields the same as the above, but now also has tasks-component-bundle.js and contacts-component-bundle.js, both of which have only some webpack bootstrap code; the tasks and contacts code are all still in 1.1-bundle.

Again, I simply want to be able to tell Webpack, one way or another, to bundle up individual modules and their dependencies for subsequent lazy, async loading when needed.

The final answer was given by Tobias—Webpack creator—below, which I'll put here for posterity.

Truly dynamic is not possible. webpack (in constract to require.js) compiles your app before executing it, and don't have access to runtime information. Dynamic requires in webpack dive in every possible folder as long your dynamic expression don't contain ... You should even be able to configure it to use mod + '/' + mod with the ContextReplacementPlugin and a little RegExp magic (use backreferences in the RegExp). By default it would include too many modules.

like image 830
Adam Rackis Avatar asked Jul 24 '15 19:07

Adam Rackis


People also ask

How do I enable standalone single-spa webpack plugin?

To use the plugin, add it to your webpack config. Then when you run webpack-dev-server, an HTML file will be generated that loads and mounts your microfrontend as a single-spa application or parcel.

Can webpack have multiple entry points?

Webpack is a powerful tool to build an SPA, because: It manages modules and their dependencies and transforms static assets to build bundles. It creates the same build bundles in development and production for consistency. It controls multiple application entry points or sections, and provides common bundles among them ...


2 Answers

webpack creates a split point per async require statement (require.ensure or AMD require([])). So you need to write a require([]) per lazy-loaded part of your app.

Your SPA only has a single entry point: the (client-side) router. Let's call it app.js. The pages are loaded on demand and ain't entry points.

webpack.config.js:

module.exports = {     entry: {         app: './app'     },     output: {         path: path.resolve(__dirname, 'build'),         filename: '[name]-bundle.js'     } } 

app.js:

var mod = window.location.hash.split('/')[0].toLowerCase(); alert(mod);  switch(mod) {     case "contacts":         require(["./pages/contacts"], function(page) {             // do something with "page"         });         break;     case "tasks":         require(["./pages/tasks"], function(page) {             // do something with "page"         });         break; } 

Alternative: Using a "context".

When using a dynamic dependency i. e require("./pages/" + mod) you can't write a split point per file. For this case there is a loader that wrapps a file in a require.ensure block:

app.js

var mod = window.location.hash.split('/')[0].toLowerCase(); alert(mod);  require("bundle!./pages/" + mod)(function(page) {     // do something with "page" }); 

This is webpack-specific. Don't forget to npm install bundle-loader --save. Check the correct casing, it's case-sensitive.

like image 81
Tobias K. Avatar answered Oct 02 '22 20:10

Tobias K.


I've worked through a bit of this and wanted to post my work here for the benefit of others.

The premise is a web app consisting of a single page, with certain framework utilities loaded initially, with all subsequent sections of the app loaded on demand as the user navigates and changes the url hash.

The proof of concept app.js framework/entry point looks like this

app.js

var framework = require('./framework/frameworkLoader');  window.onhashchange = hashChanged; hashChanged(); //handle initial hash  function hashChanged() {     var mod = window.location.hash.split('/')[0].replace('#', '');      if (!mod) return;      framework.loadModule(mod, moduleLoaded, invalidModule);      function moduleLoaded(moduleClass, moduleHtml){         //empty to remove handlers previously added         $('#mainContent').empty();          $('#mainContent').html(moduleHtml);          var inst = new moduleClass();         inst.initialize();     }      function invalidModule(){         alert('Yo - invalid module');     } }; 

Obviously the point of interest is framework.loadModule(mod, moduleLoaded, invalidModule);. As Tobias said, there must be separate, stand-alone AMD-style require statements (I believe there's a CommonJS alternative, but I haven't explored that) for EACH possibility. Obviously nobody would want to write out each possibility for a large application, so my presumption is that some sort of simple node task would exist as part of the build process to navigate the app's structure, and auto-generate all of these require statements for each module for you. In this case the assumption is that each folder in modules contains a module, the main code and html for which are in eponymously-named files. For example, for contacts the module definition would be in modules/contacts/contacts.js and the html in modules/contacts/contacts.htm.

I just manually wrote out this file since having Node navigate folders and file structures, and output new files is trivially easy.

frameworkLoader.js

//************** in real life this file would be auto-generated*******************  function loadModule(modName, cb, onErr){     if (modName == 'contacts') require(['../modules/contacts/contacts', 'html!../modules/contacts/contacts.htm'], cb);     else if (modName == 'tasks') require(['../modules/tasks/tasks', 'html!../modules/tasks/tasks.htm'], cb);     else onErr(); }  module.exports = {     loadModule: loadModule }; 

With the rest of the files:

webpack.config.js

var path = require('path'); var webpack = require('webpack');  module.exports = {     entry: {         app: './app'     },     output: {         path: path.resolve(__dirname, 'build'),         filename: '[name]-bundle.js',         publicPath: '/build/',     } }; 

And the main html file

default.htm

<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml">     <head>         <title></title>          <script type="text/javascript" src="http://code.jquery.com/jquery-1.11.3.min.js"></script>         <script type="text/javascript" src="build/app-bundle.js"></script>     </head>     <body>         <h1>Hello there!</h1>         <h2>Sub heading</h2>          <h3>Module content below</h3>         <div id="mainContent"></div>     </body> </html> 

The next step is adding ad hoc dependencies to these modules. Unfortunately adding a require(['dep1', 'dep2'], function(){ doesn't quite work as I would have hoped; that eagerly chases down all dependencies in the list, and bundles them all with the module in question, rather than loading them on demand. This means that if both the contacts and tasks modules require the same dependency (as they're about to) both modules with have that entire dependency bundled in, causing it to be loaded and reloaded as the user browses to contacts and then tasks.

The solution is the bundle loader npm install bundle-loader --save. This allows us to do require('bundle!../../libs/alt') which returns a function that when called fetches our dependency. The function takes as argument a callback which accepts our newly loaded dependency. Obviously loading N dependencies like this will require unpleasant code to wrangle the N callbacks, so I'll build in Promise support in just a moment. But first to update the module structure to support dependency specification.

contacts.js

function ContactsModule(){     this.initialize = function(alt, makeFinalStore){         //use module     }; }  module.exports = {     module: ContactsModule,     deps: [require('bundle!../../libs/alt'), require('bundle!alt/utils/makeFinalStore')] }; 

tasks.js

function TasksModule(){     this.initialize = function(alt){         //use module     }; }  module.exports = {     module: TasksModule,     deps: [require('bundle!../../libs/alt')] }; 

Now each module returns an object literal with the module itself, and the dependencies it needs. Obviously it would have been nice to just write out a list of strings, but we need the require('bundle! calls right there so Webpack can see what we need.

Now to build in Promise support to our main app.js

app.js

var framework = require('./framework/frameworkLoader');  window.onhashchange = hashChanged; hashChanged(); //handle initial hash  function hashChanged() {     var mod = window.location.hash.split('/')[0].replace('#', '');      if (!mod) return;      framework.loadModule(mod, moduleLoaded, invalidModule);      function moduleLoaded(modulePacket, moduleHtml){         var ModuleClass = modulePacket.module,             moduleDeps = modulePacket.deps;          //empty to remove handlers previous module may have added         $('#mainContent').empty();          $('#mainContent').html(moduleHtml);          Promise.all(moduleDeps.map(projectBundleToPromise)).then(function(deps){             var inst = new ModuleClass();             inst.initialize.apply(inst, deps);         });          function projectBundleToPromise(bundle){             return new Promise(function(resolve){ bundle(resolve); });         }     }      function invalidModule(){         alert('Yo - invalid module');     } }; 

This causes separate individual bundle files to be created for contacts, tasks, alt, and makeFinalStore. Loading tasks first shows the bundle with the tasks module, and the bundle with alt loading in the network tab; loading contacts after that shows the contacts bundle loading along with the with the makeFinalStore bundle. Loading contacts first shows contacts, alt, and makeFinalStore bundles loading; loading tasks after that shows only tasks bundle loading.


Lastly, I wanted to extend the contacts module so that it would support its own ad hoc dynamic loading. In real life a contacts module might load on the fly the contact's billing information, contact information, subscription information, and so on. Obviously this proof of concept will be more simple, bordering on silly.

Under the contacts folder I created a contactDynamic folder, with the following files

contentA.js contentA.htm contentB.js contentB.htm contentC.js contentC.htm 

contentA.js

module.exports = {     selector: '.aSel',     onClick: function(){ alert('Hello from A') } }; 

contentA.htm

<h1>Content A</h1>  <a class="aSel">Click me for a message</a> 

contentB.js

module.exports = {     selector: '.bSel',     onClick: function(){ alert('Hello from B') } }; 

contentB.htm

<h1>Content B</h1>  <a class="bSel">Click me for a message</a> 

contentC.js

module.exports = {     selector: '.cSel',     onClick: function(){ alert('Hello from C') } }; 

contentC.htm

<h1>Content C</h1>  <a class="cSel">Click me for a message</a> 

The updated code for contacts.js is below. Some things to note. We're building dynamic contexts ahead of time so we can exclude files appropriately. If we don't, then our dynamic require with bundle! will fail when it gets to the html files; our context limits files to *.js. We also create a context for .htm files—note that we're using both the bundle! and html! loaders together. Also note that order matters - bundle!html! works but html!bundle! causes these bundles to not get built, and I'm hoping someone can comment as to why. But as is, separate bundles are created for each individual .js, and .htm file, and are loaded on demand only when needed. And of course I'm wrapping the bundle! calls in Promises as before.

Also, I understand that the ContextReplacementPlugin can be used instead of these contexts, and I'm hoping someone can show me how: is an instance of ContextReplacementPlugin passed into a dynamic require?

contacts.js

function ContactsModule(){     this.initialize = function(alt, makeFinalStore){         $('#contacts-content-loader').on('click', '.loader', function(){             loadDynamicContactContent($(this).data('name'));         });     }; }  function loadDynamicContactContent(name){     var reqJs = require.context('bundle!./contactDynamic', false, /.js$/);     var reqHtml = require.context('bundle!html!./contactDynamic', false, /.htm$/);      var deps = [reqJs('./' + name + '.js'), reqHtml('./' + name + '.htm')];      Promise.all(deps.map(projectBundleToPromise)).then(function(deps){         var code = deps[0],             html = deps[1];          $('#dynamicPane').empty().html(html);         $('#dynamicPane').off().on('click', code.selector, function(){             code.onClick();         });     }); }  function projectBundleToPromise(bundle){     return new Promise(function(resolve){ bundle(resolve); }); }  module.exports = {     module: ContactsModule,     deps: [require('bundle!../../libs/alt'), require('bundle!alt/utils/makeFinalStore')] }; 

contacts.htm

<h1>CONTACTS MODULE</h1>  <div id="contacts-content-loader">     <a class="loader" data-name="contentA">Load A</a>     <a class="loader" data-name="contentB">Load B</a>     <a class="loader" data-name="contentC">Load C</a> </div>  <div id="dynamicPane">     Nothing loaded yet </div> 

Lastly, the final default.htm

default.htm

<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml">     <head>         <title></title>          <script type="text/javascript" src="http://code.jquery.com/jquery-1.11.3.min.js"></script>         <script type="text/javascript" src="build/app-bundle.js"></script>     </head>     <body>         <h1>Hello there!</h1>         <h2>Sub heading</h2>          <a href="#contacts">Load contacts</a>         <br><br>         <a href="#tasks">Load tasks</a>          <h3>Module content below</h3>         <div id="mainContent"></div>     </body> </html> 
like image 32
Adam Rackis Avatar answered Oct 02 '22 21:10

Adam Rackis