plugin page htmlwebpackplugin body app javascript webpack

javascript - page - Cree paquetes SPA individuales con Webpack



webpack wikipedia (3)

¿Cómo uso Webpack para crear paquetes SPA independientes que pueden o no cargarse sobre la marcha mientras mi usuario navega en mi SPA?

Tengo un módulo de contactos y un módulo de tareas. Ambos tienen dos dependencias. Quiero que WebPack cree paquetes para cada uno que se carguen cuando (y si) es necesario.

El código está abajo. El problema parece ser que cada una de estas entradas se ve como puntos de entrada de la aplicación, y también se está insertando el código de arranque de la carpeta web.

He visto varios ejemplos con CommonsChunkPlugin pero no puedo encontrar una referencia / documentación de API para él, y por lo que puedo suponer, eso no es lo que quiero de todos modos.

Editar: encontré esos documentos here y agregué un intento con ese complemento a continuación en mi edición.

Configuración actual

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

EDITAR

Aquí está mi intento número 2 con el CommonsChunkPlugin. Creé una aplicación ficticia.js

app.js

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

Luego moví todos mis contactos y archivos de tareas a una carpeta de componentes, pero los dejé en paz. Mi nueva configuración:

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, ahora app-bundle.js parece no tener ningún código de arranque de Webpack

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

contacts-components-bundle.js ahora solo tiene esto

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

y tasks-components-bundle.js parece tener todo el código de arranque de mi 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 = ""; /******/ }) /************************************************************************/ /******/ ([]);

Una vez más, estoy tratando de usar Webpack para poner en marcha una prueba de concepto de SPA, con algún tipo de punto de entrada root app.js, y luego una cantidad arbitraria de módulos / componentes que se cargan a pedido. Esto es trivialmente fácil con requirejs, así que tengo que imaginarme que me falta algo clave aquí, especialmente con todos los artículos que he visto hablando acerca de lo fantástico que es el paquete web para SPA.

EDIT 2

A continuación, la respuesta de Per bebraw probé lo siguiente:

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

Y luego en mi carpeta de compilación me queda app-bundle.js que tiene todo mi código de arranque, y mi código app.js, y luego 1.1-bundle.js que tiene todos mis códigos de tareas y contactos.

También intenté esto

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

Lo cual produce lo mismo que el anterior, pero ahora también tiene tasks-component-bundle.js y contacts-component-bundle.js, ambos tienen solo un código de arranque de webpack; las tareas y el código de los contactos están todavía en 1.1-bundle.

De nuevo, simplemente quiero poder decirle a Webpack, de una forma u otra, que agrupen los módulos individuales y sus dependencias para su posterior carga asincrónica, cuando sea necesario.

La respuesta final fue dada por el creador de Tobias-Webpack-below, que pondré aquí para la posteridad.

Verdaderamente dinámico no es posible. webpack (en constracto a require.js) compila su aplicación antes de ejecutarla y no tiene acceso a la información de tiempo de ejecución. Dynamic requiere una inmersión en la carpeta web en todas las carpetas posibles, siempre que su expresión dinámica no contenga ... Incluso debería poder configurarla para usar mod + ''/'' + mod con ContextReplacementPlugin y un poco de magia RegExp (use referencias atrás en RegExp). Por defecto, incluiría demasiados módulos.



He trabajado un poco en esto y quería publicar mi trabajo aquí para el beneficio de los demás.

La premisa es una aplicación web que consta de una sola página, con ciertas utilidades de infraestructura cargadas inicialmente, con todas las secciones posteriores de la aplicación cargadas a pedido a medida que el usuario navega y cambia el hash de la url.

La prueba de concepto del marco / punto de entrada de app.js se parece a esto

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

Obviamente, el punto de interés es framework.loadModule(mod, moduleLoaded, invalidModule); . Como dijo Tobias, debe haber declaraciones requeridas por separado de estilo AMD (creo que hay una alternativa de CommonJS, pero no he explorado eso) para CADA posibilidad. Obviamente nadie querría escribir cada posibilidad para una aplicación grande, así que mi presunción es que algún tipo de tarea de nodo simple existiría como parte del proceso de compilación para navegar por la estructura de la aplicación, y generar automáticamente todas estas requieren declaraciones para cada módulo para ti En este caso, la suposición es que cada carpeta en modules contiene un módulo, el código principal y html para los cuales están en archivos nombrados por el mismo nombre. Por ejemplo, para los contactos, la definición del módulo sería en modules / contacts / contacts.js y html en modules / contacts / contacts.htm.

Simplemente escribí este archivo manualmente porque tener carpetas y estructuras de archivos de Nodo, y generar nuevos archivos es trivialmente fácil.

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

Con el resto de los archivos:

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

Y el archivo html principal

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>

El siguiente paso es agregar dependencias ad hoc a estos módulos. Lamentablemente, agregar un require([''dep1'', ''dep2''], function(){ no funciona del todo como yo esperaba; eso persigue ansiosamente todas las dependencias de la lista y las agrupa con el módulo en cuestión, en lugar de cargarlos a pedido. Esto significa que si tanto los contactos como los módulos de tareas requieren la misma dependencia (ya que están a punto) ambos módulos tienen toda esa dependencia incluida, lo que hace que se cargue y vuelva a cargar a medida que el usuario navega a contactos y luego a tareas.

La solución es el cargador de paquetes npm install bundle-loader --save . Esto nos permite hacer require(''bundle!../../libs/alt'') que devuelve una función que cuando se llama recupera nuestra dependencia. La función toma como argumento una devolución de llamada que acepta nuestra dependencia recién cargada. Obviamente, cargar dependencias de N de esta manera requerirá un código desagradable para disputar las N devoluciones de llamada, así que construiré el soporte Promise en solo un momento. Pero primero para actualizar la estructura del módulo para admitir la especificación de dependencia.

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

Ahora cada módulo devuelve un objeto literal con el módulo mismo y las dependencias que necesita. Obviamente, hubiera sido agradable simplemente escribir una lista de cadenas, pero necesitamos el require(''bundle! Calls allí mismo para que Webpack pueda ver lo que necesitamos.

Ahora, para incorporar compatibilidad con Promise a nuestra aplicación principal.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''); } };

Esto hace que se creen archivos separados de paquetes individuales para contactos, tareas, alt y makeFinalStore. Primero, la carga de tareas muestra el paquete con el módulo de tareas y el paquete con carga alternativa en la pestaña de red; cargando contactos después de eso muestra el paquete de contactos cargando junto con el paquete makeFinalStore. La carga de contactos primero muestra contactos, alt y makeFinalStore bundles cargando; las tareas de carga después de eso muestran solo la carga del paquete de tareas.

Por último, quería ampliar el módulo de contactos para que admitiera su propia carga dinámica ad hoc. En la vida real, un módulo de contactos puede cargar sobre la marcha la información de facturación del contacto, información de contacto, información de suscripción, etc. Obviamente, esta prueba de concepto será más simple, rayana en tonterías.

Debajo de la carpeta de contactos creé una carpeta contactDynamic, con los siguientes archivos

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>

El código actualizado para contacts.js está a continuación. Algunas cosas para notar Estamos construyendo contextos dinámicos antes de tiempo para que podamos excluir los archivos de forma adecuada. Si no lo hacemos, ¡entonces nuestra dinámica requiere bundle! fallará cuando llegue a los archivos html; nuestro contexto limita los archivos a *.js . También creamos un contexto para los archivos .htm: ¡tenga en cuenta que estamos usando ambos bundle! y html! cargadores juntos. También tenga en cuenta que el orden importa - bundle!html! funciona pero html!bundle! hace que estos paquetes no se construyan, y espero que alguien pueda comentar por qué. Pero como es, se crean paquetes separados para cada archivo .js y .htm individual, y se cargan a pedido solo cuando es necesario. ¡Y por supuesto estoy envolviendo el bundle! llamadas en Promesas como antes.

Además, entiendo que el ContextReplacementPlugin se puede usar en lugar de estos contextos, y espero que alguien me muestre cómo: ¿se ContextReplacementPlugin pasado una instancia de ContextReplacementPlugin a una ContextReplacementPlugin dinámica?

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>

Por último, el último 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>


webpack crea un punto de división por declaración de requerimiento asincrónico ( require.ensure o AMD require([]) ). Por lo tanto, debe escribir un require([]) por cada parte de su aplicación cargada por latencia.

Su SPA solo tiene un único punto de entrada: el enrutador (del lado del cliente). Vamos a llamarlo app.js Las páginas se cargan a pedido y no son puntos de entrada.

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

Alternativa: usar un "contexto".

Cuando se usa una dependencia dinámica i. e require("./pages/" + mod) no puede escribir un punto de división por archivo. Para este caso, hay un cargador que cierra un archivo en un bloque require.ensure :

app.js

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

Esto es específico de la carpeta web. No te olvides de npm install bundle-loader --save . Verifique la carcasa correcta, distingue entre mayúsculas y minúsculas.