使用Webpack创建单独的SPA包

时间:2015-07-24 19:02:57

标签: javascript webpack

当我的用户导航我的SPA时,如何使用Webpack创建可能会或可能不会立即加载的独立SPA套件?

我有一个联系人模块和一个任务模块。两者都有两个依赖关系。我希望WebPack为每个(如果需要)加载时加载的包创建包。

代码如下。问题似乎是这些条目中的每一个都被视为应用程序入口点,因此将webpack引导代码插入其中。

我已经看过CommonsChunkPlugin的各种示例,但我无法找到它的API参考/文档,而且我可以推测,这不是我想要的。

修改 - 找到了这些文档here,并在我的编辑中添加了下面的插件尝试。

当前配置

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);
});

任务-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__));

/***/ }
/******/ ]);

修改

这是我使用CommonsChunkPlugin尝试的第2号。我创建了一个虚拟的app.js

app.js

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

然后我将所有联系人和任务文件移动到组件文件夹下,否则将它们单独留下。我的新配置:

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'
        })
    ]
};

很奇怪,现在 app-bundle.js 似乎没有任何Webpack引导代码

webpackJsonp([0,1,2],[
/* 0 */
/***/ function(module, exports) {

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

/***/ }
]);

contacts-components-bundle.js 现在只有这个

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

tasks-components-bundle.js 似乎包含了我的所有webpack引导代码

/******/ (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 = "";
/******/ })
/************************************************************************/
/******/ ([]);

同样,我只是尝试使用Webpack来获取SPA概念证明并运行,使用某种根app.js入口点,然后加载一些任意数量的模块/组件需求。这对于requirejs来说非常简单,所以我不得不想象我在这里遗漏了一些关键词,尤其是我所见过的所有文章都在谈论Webpack对于SPA的好处。

编辑2

根据下面的回答,我尝试了以下方法:

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'
    }
};

然后在我的构建文件夹中,我留下了app-bundle.js,其中包含我的所有引导代码和我的app.js代码,然后是1.1-bundle.js,其中全部我的任务和联系人代码。

试过这个

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
        })
    ]
};

其结果与上述相同,但现在有tasks-component-bundle.js和contacts-component-bundle.js,两者都只有 一些webpack引导代码;任务和联系人代码仍然是1.1-bundle。

同样,我只是希望能够以某种方式告诉Webpack将各个模块及其依赖项捆绑在一起,以便在需要时进行后续的延迟异步加载。

最终的答案由Tobias-Webpack创建者提供,我将在这里为后人提供。

  

真正的动态是不可能的。 webpack(在require.js中的构造)在执行之前编译您的应用程序,并且无法访问运行时信息。只要您的动态表达不包含,动态就需要webpack潜入每个可能的文件夹......您甚至应该能够将其配置为使用mod +&#39; /&#39; + mod使用ContextReplacementPlugin和一点RegExp魔术(在RegExp中使用反向引用)。默认情况下,它将包含太多模块。

3 个答案:

答案 0 :(得分:13)

webpack为每个异步require语句(require.ensure或AMD require([]))创建一个拆分点。因此,您需要为应用中的每个延迟加载部分编写require([])

您的SPA只有一个入口点:(客户端)路由器。我们称之为app.js。这些页面是按需加载的,也是一个入口点。

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;
}

替代方案:使用&#34;上下文&#34;。

使用动态依赖时i。 e require("./pages/" + mod)您无法为每个文件写一个分割点。对于这种情况,有一个加载器包装require.ensure块中的文件:

app.js

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

require("bundle!./pages/" + mod)(function(page) {
    // do something with "page"
});

这是特定于webpack的。别忘了npm install bundle-loader --save。检查正确的外壳,它区分大小写。

答案 1 :(得分:4)

我已经完成了一些工作,并希望在这里发布我的工作,以造福他人。

前提是一个由单个页面组成的Web应用程序,最初加载了某些框架实用程序,当用户导航并更改url哈希时,应用程序的所有后续部分都会按需加载。

概念验证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(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');
    }
};

显然,兴趣点是framework.loadModule(mod, moduleLoaded, invalidModule);。正如Tobias所说,必须有单独的,独立的AMD风格的需求声明(我相信它有一个CommonJS替代品,但我还没有探索过),因为它有可能。显然没有人会想要为大型应用程序写出每种可能性,所以我的假设是,某些简单的节点任务将作为构建过程的一部分存在,以导航应用程序的结构,并自动生成所有这些需要为每个模块声明。在这种情况下,假设modules中的每个文件夹都包含一个模块,主要代码和html是以同名文件命名的。例如,对于联系人,模块定义将位于modules / contacts / contacts.js中,html位于modules / contacts / contacts.htm中。

我只是手动写出了这个文件,因为有Node导航文件夹和文件结构,输出新文件非常简单。

<强> 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
};

使用其余文件:

<强> 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/',
    }
};

主要的html文件

<强>的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>

下一步是向这些模块添加临时依赖项。不幸的是,添加require(['dep1', 'dep2'], function(){并不像我希望的那样完成工作;急切地追查列表中的所有依赖项,并将它们全部捆绑在有问题的模块中,而不是按需加载它们。这意味着如果联系人和任务模块都需要相同的依赖关系(因为他们将要执行)两个模块都捆绑了整个依赖关系,导致它在用户浏览联系人然后任务时加载和重新加载

解决方案是捆绑加载器npm install bundle-loader --save。这允许我们执行require('bundle!../../libs/alt'),它返回函数,在调用时获取我们的依赖项。该函数将一个回调作为参数,该回调接受我们新加载的依赖项。显然加载这样的N个依赖项将需要令人不快的代码来纠缠N个回调,因此我将在片刻内构建Promise支持。但首先要更新模块结构以支持依赖项规范。

<强> 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')]
};

现在每个模块都返回一个包含模块本身的对象文字,以及它所需的依赖项。显然,只写出一个字符串列表会很好,但是我们需要require('bundle!调用,所以Webpack可以看到我们需要的东西。

现在为我们的主app.js构建Promise支持

<强> 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');
    }
};

这会导致为contacts,tasks,alt和makeFinalStore创建单独的单个bundle文件。加载任务首先显示包含任务模块的包,并在网络选项卡中显示带有alt加载的包;之后加载联系人显示联系人捆绑加载与makeFinalStore捆绑。加载联系人首先显示联系人,alt和makeFinalStore包加载;之后加载任务只显示任务捆绑加载。

最后,我想扩展contacts模块,以便它支持自己的ad hoc动态加载。在现实生活中,联系人模块可以即时加载联系人的账单信息,联系信息,订阅信息等。显然,这种概念证明将更加简单,接近愚蠢。

在contacts文件夹下,我创建了一个contactDynamic文件夹,其中包含以下文件

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>

contacts.js的更新代码如下。有些事情需要注意。我们提前构建动态上下文,以便我们可以适当地排除文件。如果我们不这样做,那么bundle!的动态需求会在到达html文件时失败;我们的上下文将文件限制为*.js。我们还为.htm文件创建了一个上下文 - 请注意我们同时使用bundle!html!个加载器。另请注意,订单重要 - bundle!html!有效但html!bundle!导致这些捆绑包无法构建,我希望有人可以就其原因发表评论。但实际上,为每个单独的.js和.htm文件创建了单独的包,并且仅在需要时按需加载。当然,我像以前一样在Promises中包装bundle!次调用。

此外,我了解可以使用ContextReplacementPlugin代替这些上下文,我希望有人可以告诉我如何:ContextReplacementPlugin的实例传递给动态{{ 1}}?

<强> contacts.js

require

<强> contacts.htm

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')]
};

最后,最后的default.htm

<强>的default.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>

答案 2 :(得分:0)

我认为require.ensure可能是关键所在。这将允许您设置动态加载的分割点。您可以通过某种方式将其与SPA的路由器连接起来。以下是Pete Hunt的基本想法:https://github.com/petehunt/webpack-howto#9-async-loading