React, JSX, importación de módulos ES (incluido dinámico) en un navegador sin Webpack

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:


  1. Creamos un mapa de exportación que asocia un nombre de paquete con una variable global
  2. Cree una etiqueta de script en la head con el contenido del script envuelto en UMD
  3. 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> <!--  app/index.jsx  index.js --> <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

Source: https://habr.com/ru/post/474672/


All Articles