Подтвердить что ты не робот

Создавайте индивидуальные пакеты SPA с помощью Webpack

Как использовать Webpack для создания независимых пакетов SPA, которые могут или не могут быть загружены "на лету", когда мой пользователь переводит мой SPA?

У меня есть модуль контактов и модуль задач. Оба имеют две зависимости. Я хочу, чтобы WebPack создавал пакеты для каждого, которые загружаются при необходимости (и если).

Код ниже. Проблема заключается в том, что каждая из этих записей рассматривается как точки входа в приложение, и поэтому в нее вставлен код начальной загрузки webpack.

Я видел различные примеры с CommonsChunkPlugin, но я не могу найти для него ссылку на API/документацию, и из того, что я могу догадаться, это не то, что я хочу в любом случае.

Изменить - нашел те документы здесь и добавил попытку с этим плагином ниже в моем редактировании.


Текущая конфигурация

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

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

ИЗМЕНИТЬ

Здесь моя попытка № 2 с CommonsChunkPlugin. Я создал фиктивный 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'
        })
    ]
};

В Bizarely теперь 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, чтобы получить доказательство API о концепции и запуске, с некоторой точкой входа root.js, а затем некоторое количество модулей/компонентов, которые загружаются по требованию. Это просто тривиально с требованием, поэтому я должен представить себе, что у меня отсутствует что-то здесь, особенно со всеми статьями, которые я видел, говоря о том, как большой Webpack для SPA.


РЕДАКТИРОВАТЬ 2

В ответе на bebraw ниже я пробовал следующее:

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

Это дает то же самое, что и выше, но теперь также имеет task-component-bundle.js и contacts-component-bundle.js, оба из которых имеют только некоторый код начальной загрузки webpack; задачи и код контактов все еще находятся в 1.1-пакете.

Опять же, я просто хочу сказать Webpack, так или иначе, связать отдельные модули и их зависимости для последующей ленивой асинхронной загрузки, когда это необходимо.

Окончательный ответ дал создатель Тобиаса-Вебпака - ниже, который я положу сюда для потомков.

Истинная динамика невозможна. webpack (в constract to require.js) компилирует ваше приложение перед его исполнением и не имеет доступа к информации о времени выполнения. Dynamic требует погружения в webpack в каждую возможную папку, поскольку ваше динамическое выражение не содержит... Вы даже можете настроить его на использование mod + '/' + mod с помощью ContextReplacementPlugin и немного волшебства RegExp (используйте обратные ссылки в RegExp). По умолчанию в него будет включено слишком много модулей.

4b9b3361

Ответ 1

webpack создает точку разделения для инструкции aync 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;
}

Альтернатива: использование "контекста".

При использовании динамической зависимости 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. Проверьте правильность корпуса и чувствительность к регистру.

Ответ 2

Я немного поработал и хотел опубликовать свою работу здесь для других.

Предпосылка - это веб-приложение, состоящее из одной страницы с первоначально загруженными утилитами фреймов, со всеми последующими разделами приложения, загружаемыми по требованию, когда пользователь перемещается и изменяет хэш URL.

Доказательство концепции app.js framework/entry point выглядит так:

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);. Как сказал Тобиас, должны существовать отдельные автономные требования в стиле AMD (я считаю, что есть альтернатива CommonJS, но я не изучил это) для КАЖДОЙ возможности. Очевидно, никто не захочет выписывать каждую возможность для большого приложения, поэтому моя презумпция заключается в том, что какая-то простая задача node будет существовать как часть процесса сборки для навигации по структуре приложения и автоматического генерации всех этих запросов для каждого модуля для вас. В этом случае предполагается, что каждая папка в modules содержит модуль, основной код и html, для которых находятся файлы с одноименным именем. Например, для контактов определение модуля будет в модулях/контактах/контактах. Js и html в модулях/контактах/контактах .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>

Следующим шагом будет добавление зависимостей ad hoc к этим модулям. К сожалению, добавление 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 мог видеть, что нам нужно.

Теперь, чтобы создать поддержку Promise для нашего основного приложения 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');
    }
};

Это приводит к созданию отдельных отдельных файлов пакета для контактов, задач, alt и makeFinalStore. Сначала загрузочные задания показывают пакет с модулем задач и набор с загрузкой alt на вкладке сети; загрузка контактов после этого показывает загрузку контактов контактов вместе с комплектом makeFinalStore. Загрузка контактов сначала показывает контакты загрузки, alt и makeFinalStore; после загрузки задачи загрузки загружаются только задачи.


Наконец, я хотел расширить модуль контактов так, чтобы он поддерживал собственную динамическую загрузку. В реальной жизни модуль контактов может мгновенно загружать информацию о контактных биллингах, контактную информацию, информацию о подписке и т.д. Очевидно, это доказательство концепции будет более простым, граничащим с глупостью.

В папке контактов я создал папку 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>

Ниже приведен обновленный код для контактов. js. Некоторые вещи, чтобы отметить. Мы создаем динамические контексты заблаговременно, поэтому мы можем исключить файлы соответствующим образом. Если мы этого не сделаем, то наша динамическая потребность с bundle! не удастся, когда она попадет в html файлы; наш контекст ограничивает файлы до *.js. Мы также создаем контекст для файлов .htm - обратите внимание, что мы вместе используем загрузчики bundle! и html!. Также обратите внимание, что порядок имеет значение - bundle!html!, но html!bundle! заставляет эти пакеты не создавать, и я надеюсь, что кто-то может прокомментировать, почему. Но как есть, отдельные пакеты создаются для каждого отдельного файла .js и .htm и загружаются по требованию только тогда, когда это необходимо. И, конечно, я обматываю вызовы bundle! в Promises, как и раньше.

Кроме того, я понимаю, что вместо этих контекстов можно использовать ContextReplacementPlugin, и я надеюсь, что кто-то может показать мне, как: экземпляр ContextReplacementPlugin передается в динамический 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>

Наконец, окончательный 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>

Ответ 3

Я думаю, что require.ensure может быть ключевым здесь. Это позволит вам настроить точки разделения для динамической загрузки. Вы каким-то образом подключили бы его к маршрутизатору вашего SPA. Вот основная идея от Пете Ханта: https://github.com/petehunt/webpack-howto#9-async-loading.