Este artículo es un intento de reunir las herramientas disponibles actualmente y descubrir si es posible crear aplicaciones listas para producción en React sin la compilación previa de coleccionistas como Webpack, o al menos minimizar dicha compilación.
Todo lo descrito es muy experimental y deliberadamente corto esquinas en algunos lugares. En ningún caso recomiendo hacer algo como esto en la producción real.
La capacidad de usar módulos ECMAScript ( <script type="module"/>
con importaciones del formulario import Foo from './foo';
e import('./Foo')
) directamente en el navegador no es nueva por mucho tiempo, es una funcionalidad bien soportada: https: //caniuse.com/#feat=es6-module .
Pero en realidad, importamos no solo nuestros módulos, sino también bibliotecas. Hay un excelente artículo sobre este tema: https://salomvary.com/es6-modules-in-browsers.html . Y otro artículo igualmente bueno que vale la pena mencionar es https://github.com/stken2050/esm-bundlerless .
Entre las otras cosas importantes de estos artículos, estos puntos son los más importantes para crear una aplicación React:
- Soporte para importaciones de especificador de paquetes (o mapas de importación): cuando escribimos
import React from 'react'
de hecho, deberíamos importar algo como esto https://cdn.com/react/react.production.js
- Soporte para UMD: React todavía se distribuye como UMD y, por el momento, los autores aún no han acordado cómo distribuir la biblioteca como módulo
- Jsx
- Importar CSS
Repasemos todos los puntos por turno.
Estructura del proyecto
En primer lugar, determinaremos la estructura del proyecto:
node_modules
obviamente aquí es donde se colocarán las dependencias- directorio
src
con index*.html
y scripts de servicio
app
directamente código de aplicación en React
El especificador de paquete importa soporte
Para usar React a través de import React from 'react';
debemos decirle al navegador dónde buscar la fuente real, porque react
no es un archivo real, sino un puntero a una biblioteca. Hay un trozo para este https://github.com/guybedford/es-module-shims .
Vamos a configurar el trozo y reaccionar:
$ npm i es-module-shims react react-dom --save
public/index-dev.html
la aplicación desde el archivo public/index-dev.html
:
<!DOCTYPE html> <html> <body> <div id="root"></div> <script defer src="../node_modules/es-module-shims/dist/es-module-shims.js"></script> <script type="importmap-shim"> { "imports": { "react": "../node_modules/react/umd/react.development.js", "react-dom": "../node_modules/react-dom/umd/react-dom.development.js" } } </script> <script type="module-shim"> import './app/index.jsx'; </script> </body> </html>
Donde src/app/index.jsx
ve así:
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; (async () => { const {Button} = await import('./Button.jsx'); const root = document.getElementById('root'); ReactDOM.render(( <div> <Button>Direct</Button> </div> ), root); })();
Y src/app/Button.jsx
así:
import React from 'react'; export const Button = ({children}) => <button>{children}</button>;
¿Funcionará esto? Por supuesto que no. Incluso a pesar del hecho de que todo se importa con éxito desde donde es necesario.
Pasemos al siguiente problema.
Soporte UMD
Forma dinámica
Según el hecho de que React se distribuye como UMD, no se puede importar directamente, incluso a través de un código auxiliar (si el ticket se cerró como reparado, puede omitir el paso). Necesitamos parchear de alguna manera la fuente para que sea compatible.
Los artículos anteriores me impulsaron a utilizar Service Workers para esto, que puede interceptar y modificar las solicitudes y respuestas de la red. Creemos el punto de entrada principal src/index.js
, donde configuraremos SW y la aplicación y lo usaremos en lugar de invocar directamente la aplicación ( src/app/index.jsx
):
(async () => { try { const registration = await navigator.serviceWorker.register('sw.js'); await navigator.serviceWorker.ready; const launch = async () => import("./app/index.jsx"); // SW // https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#clientsclaim if (navigator.serviceWorker.controller) { await launch(); } else { navigator.serviceWorker.addEventListener('controllerchange', launch); } } catch (error) { console.error('Service worker registration failed', error); } })();
Crear un trabajador de servicio ( src/sw.js
):
// //@see https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#clientsclaim self.addEventListener('activate', event => event.waitUntil(clients.claim())); const globalMap = { 'react': 'React', 'react-dom': 'ReactDOM' }; const getGlobalByUrl = (url) => Object.keys(globalMap).reduce((res, key) => { if (res) return res; if (matchUrl(url, key)) return globalMap[key]; return res; }, null); const matchUrl = (url, key) => url.includes(`/${key}/`); self.addEventListener('fetch', (event) => { const {request: {url}} = event; console.log('Req', url); const fileName = url.split('/').pop(); const ext = fileName.includes('.') ? url.split('.').pop() : ''; if (!ext && !url.endsWith('/')) { url = url + '.jsx'; } if (globalMap && Object.keys(globalMap).some(key => matchUrl(url, key))) { event.respondWith( fetch(url) .then(response => response.text()) .then(body => new Response(` const head = document.getElementsByTagName('head')[0]; const script = document.createElement('script'); script.setAttribute('type', 'text/javascript'); script.appendChild(document.createTextNode( ${JSON.stringify(body)} )); head.appendChild(script); export default window.${getGlobalByUrl(url)}; `, { headers: new Headers({ 'Content-Type': 'application/javascript' }) }) ) ) } else if (url.endsWith('.js')) { // rewrite for import('./Panel') with no extension event.respondWith( fetch(url) .then(response => response.text()) .then(body => new Response( body, { headers: new Headers({ 'Content-Type': 'application/javascript' }) }) ) ) } });
Para resumir lo que se ha hecho:
- Creamos un mapa de exportación que asocia un nombre de paquete con una variable global
- Cree una etiqueta de script en la
head
con el contenido del script envuelto en UMD - Variable global exportada como exportación predeterminada
Para una demostración, un parche tan brutal será suficiente, pero esto puede no funcionar con todos los contenedores UMD. Algo más confiable puede usarse a cambio.
Ahora cambie src/index-dev.html
para usar el script de configuración:
<!DOCTYPE html> <html> <body> <div id="root"></div> <script defer src="../node_modules/es-module-shims/dist/es-module-shims.js"></script> <script type="importmap-shim">... </script> <script type="module-shim" src="index.js"></script> </body> </html>
Ahora podemos importar React y React DOM.
Camino estático
Cabe señalar que hay otra manera. Hay una versión no oficial de ES de React:
npm install esm-react --save
El mapa de importación se verá así:
{ "imports": { "react": "../node_modules/esm-react/src/react.js", "react-dom": "../node_modules/esm-react/src/react-dom.js" } }
Pero desafortunadamente el proyecto está muy atrasado, la última versión es 16.8.3
mientras que React ya es 16.10.2
.
Jsx
Hay dos formas de compilar JSX. Podemos premontar el Babel tradicional desde la consola, o esto se puede hacer en el tiempo de ejecución del navegador. Para la producción en sí, es preferible la compilación previa, pero en modo de desarrollo es posible en tiempo de ejecución. Como ya tenemos un Service Serviceer, lo utilizaremos.
Instale un paquete especial con Babel:
$ npm install @babel/standalone --save-dev
Ahora agregue lo siguiente a Service Worker ( src/sw.js
):
# src/sw.js // importScripts('../node_modules/@babel/standalone/babel.js'); // self.addEventListener('fetch', (event) => { // } else if (url.endsWith('.jsx')) { event.respondWith( fetch(url) .then(response => response.text()) .then(body => new Response( //TODO Babel.transform(body, { presets: [ 'react', ], plugins: [ 'syntax-dynamic-import' ], sourceMaps: true }).code, { headers: new Headers({ 'Content-Type': 'application/javascript' }) }) ) ) } });
Aquí utilizamos el mismo enfoque para interceptar las solicitudes de red y reescribirlas, utilizamos Babel para transformar el código fuente original. Tenga en cuenta que el complemento para las importaciones dinámicas se llama syntax-dynamic-import
, no como el habitual @babel/plugin-syntax-dynamic-import
porque es una versión independiente.
CSS
En el artículo mencionado, el autor usó la transformación de texto, iremos un poco más allá e insertaremos CSS en la página. Para hacer esto, nuevamente usaremos el Service Worker ( src/sw.js
):
// self.addEventListener('fetch', (event) => { // + } else if (url.endsWith('.css')) { event.respondWith( fetch(url) .then(response => response.text()) .then(body => new Response( ` const head = document.getElementsByTagName('head')[0]; const style = document.createElement('style'); style.setAttribute('type', 'text/css'); style.appendChild(document.createTextNode( ${JSON.stringify(body)} )); head.appendChild(style); export default null; `, { headers: new Headers({ 'Content-Type': 'application/javascript' }) }) ) ); } });
Voila! Si ahora abrimos src/index-dev.html
en un navegador, veremos botones. Asegúrese de que el Service Worker requerido esté instalado y no esté en conflicto con nada. Si no está seguro, entonces, por si acaso, puede abrir Dev Tools, ir a Application
, allí en Service Workers
, y hacer clic en Unregister
de Unregister
para todos los trabajadores registrados, y luego volver a cargar la página.
Producción
El código anterior funciona como debería en el modo de desarrollo, pero por sí solo no queremos obligar a los usuarios del sitio a compilar código en sus navegadores, esto es completamente poco práctico. Hagamos algún tipo de modo de producción minimalista.
Cree un punto de entrada src/index.html
separado:
<!DOCTYPE html> <html> <body> <div id="root"></div> <script type="module" src="index.js"></script> </body> </html>
Como puede ver, no hay apéndices aquí, utilizaremos otro método para reescribir los nombres de los paquetes. Como necesitamos que Babel vuelva a compilar JSX, lo usaremos para importMap.json
rutas en lugar de importMap.json
para el código auxiliar. Instale los paquetes necesarios:
$ npm install @babel/cli @babel/core @babel/preset-react @babel/plugin-syntax-dynamic-import babel-plugin-module-resolver --save-dev
Agregue una sección con scripts a package.json
:
{ "scripts": { "start": "npm run build -- --watch", "build": "babel src/app --out-dir build/app --source-maps --copy-files" } }
Agregue el archivo .babelrc.js
:
module.exports = { presets: [ '@babel/preset-react' ], plugins: [ '@babel/plugin-syntax-dynamic-import', [ 'babel-plugin-module-resolver', { alias: { 'react': './node_modules/react/umd/react.development.js', 'react-dom': './node_modules/react-dom/umd/react-dom.development.js' }, // build resolvePath: (sourcePath, currentFile, opts) => resolvePath(sourcePath, currentFile, opts).replace('../../', '../') } ] ] }
Debe tenerse en cuenta que este archivo se usará solo para producción, en modo de desarrollo configuramos Babel en Service Worker.
Agregue el modo de combate al Trabajador de servicio:
// src/index.js if ('serviceWorker' in navigator) { (async () => { try { // const production = !window.location.toString().includes('index-dev.html'); const config = { globalMap: { 'react': 'React', 'react-dom': 'ReactDOM' }, production }; const registration = await navigator.serviceWorker.register('sw.js?' + JSON.stringify(config)); await navigator.serviceWorker.ready; const launch = async () => { if (production) { await import("./app/index.js"); } else { await import("./app/index.jsx"); } }; // https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#clientsclaim if (navigator.serviceWorker.controller) { await launch(); } else { navigator.serviceWorker.addEventListener('controllerchange', launch); } } catch (error) { console.error('Service worker registration failed', error); } })(); } else { alert('Service Worker is not supported'); }
Agregue las condiciones a src/sw.js
:
// src/sw.js const {globalMap, production} = JSON.parse((decodeURIComponent(self.location.search) || '?{}').substr(1)); if (!production) importScripts('../node_modules/@babel/standalone/babel.js');
Reemplazar
// src/sw.js if (!ext && !url.endsWith('/')) { url = url + '.jsx' with }
En
// src/sw.js if (!ext && !url.endsWith('/')) { url = url + '.' + (production ? 'js' : 'jsx'); }
build.sh
un pequeño script de consola build.sh
(las personas con Windows pueden crear lo mismo para Windows en la imagen y semejanza) que recopilará todo lo que necesita en el directorio de build
:
# rm -rf build # mkdir -p build/scripts mkdir -p build/node_modules # cp -r ./node_modules/react ./build/node_modules/react cp -r ./node_modules/react-dom ./build/node_modules/react-dom # , cp ./src/*.js ./build cp ./src/index.html ./build/index.html # npm run build
Vamos de esta manera para que el directorio node_modules
se hinche en la producción de dependencias necesarias solo en la fase de construcción y en el modo de desarrollo.
Repositorio final: http://github.com/kirill-konshin/pure-react-with-dynamic-imports
Si ahora abrimos build/index.html
veremos el mismo resultado que en src/index-dev.html
pero esta vez el navegador no recopilará nada, utilizará los archivos recopilados previamente por Babel.
Como puede ver, hay una duplicación en la solución: importMap.json
, sección de alias
del archivo .babelrc.js
y una lista de archivos para copiar en build.sh
. Lo hará para una demostración, pero en general debería estar automatizado de alguna manera.
El ensamblaje está disponible en: https://kirill-konshin.imtqy.com/pure-react-with-dynamic-imports/index.html
Conclusión
En general, se obtuvo un producto completamente viable, aunque muy crudo.
Se supone que HTTP2 se encarga de un montón de pequeños archivos enviados a través de la red.
Repositorio donde puedes ver el código