React, JSX, importation de modules ES (y compris dynamiques) dans un navigateur sans Webpack

Cet article est une tentative de rassembler les outils actuellement disponibles et de découvrir s'il est possible de créer des applications prêtes pour la production sur React sans compilation préalable par des collectionneurs comme Webpack, ou au moins de minimiser une telle compilation.


Tout ce qui est décrit est très expérimental et j'ai délibérément coupé les coins par endroits. En aucun cas, je ne recommande de faire quelque chose comme ça sur une production réelle.


La possibilité d'utiliser les modules ECMAScript ( <script type="module"/> avec les importations du formulaire import Foo from './foo'; et import('./Foo') ) directement dans le navigateur n'est pas nouvelle depuis longtemps, c'est une fonctionnalité bien prise en charge: https: //caniuse.com/#feat=es6-module .


Mais en réalité, nous importons non seulement nos modules, mais aussi des bibliothèques. Il existe un excellent article sur ce sujet: https://salomvary.com/es6-modules-in-browsers.html . Et un autre article tout aussi bon à mentionner est https://github.com/stken2050/esm-bundlerless .


Parmi les autres éléments importants de ces articles, ces points sont les plus importants pour créer une application React:


  • Prise en charge des importations de spécificateurs de packages (ou des cartes d'importation): lorsque nous écrivons import React from 'react' nous devons réellement importer quelque chose comme ceci https://cdn.com/react/react.production.js
  • Prise en charge d'UMD: React est toujours distribué en tant qu'UMD et pour le moment, les auteurs ne se sont pas encore entendus sur la façon de distribuer la bibliothèque en tant que module
  • Jsx
  • Importation CSS

Passons en revue tous les points tour à tour.


Structure du projet


Tout d'abord, nous déterminerons la structure du projet:


  • node_modules évidemment c'est là que les dépendances seront mises
  • répertoire src avec index*.html et scripts de service
    • app directement le code d'application sur React

Spécification de package: prise en charge des importations


Pour utiliser React via l' import React from 'react'; nous devons dire au navigateur où chercher la vraie source, car react n'est pas un vrai fichier, mais un pointeur vers une bibliothèque. Il existe un talon pour ce https://github.com/guybedford/es-module-shims .


Configurons le stub et React:


 $ npm i es-module-shims react react-dom --save 

Nous allons démarrer l'application à partir du fichier 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> 

src/app/index.jsx ressemble à ceci:


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

Et src/app/Button.jsx comme ceci:


 import React from 'react'; export const Button = ({children}) => <button>{children}</button>; 

Est-ce que cela fonctionnera? Bien sûr que non. Même si tout est importé avec succès là où c'est nécessaire.


Passons au problème suivant.


Prise en charge UMD


Manière dynamique


Étant donné que React est distribué en tant qu'UMD, il ne peut pas être importé directement, même via un talon (si le ticket a été fermé comme réparé, vous pouvez ignorer l'étape). Nous devons en quelque sorte patcher la source afin qu'elle devienne compatible.


Les articles ci-dessus m'ont incité à utiliser Service Workers pour cela, qui peut intercepter et modifier les demandes et les réponses du réseau. Créons le point d'entrée principal src/index.js , où nous allons configurer SW et l'application et l'utiliser au lieu d'invoquer directement l'application ( 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); } })(); 

Créez un src/sw.js service ( 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' }) }) ) ) } }); 

Pour résumer ce qui a été fait:


  1. Nous avons créé une mappe d'exportation qui associe un nom de package à une variable globale
  2. Créer une balise de script en head avec le contenu du script enveloppé dans UMD
  3. Variable globale exportée comme exportation par défaut

Pour une démo, un correctif aussi brutal sera suffisant, mais cela peut ne pas fonctionner avec tous les wrappers UMD. Quelque chose de plus fiable peut être utilisé en retour.


Maintenant, changez src/index-dev.html pour utiliser le script de configuration:


 <!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> 

Nous pouvons maintenant importer React et React DOM.


Chemin statique


Il convient de noter qu'il existe un autre moyen. Il existe une version ES non officielle de React:


 npm install esm-react --save 

La carte d'importation ressemblera à ceci:


 { "imports": { "react": "../node_modules/esm-react/src/react.js", "react-dom": "../node_modules/esm-react/src/react-dom.js" } } 

Mais malheureusement, le projet est très en retard, la dernière version est 16.8.3 alors que React est déjà 16.10.2 .


Jsx


Il existe deux façons de compiler JSX. Nous pouvons pré-assembler le Babel traditionnel à partir de la console, ou cela peut être fait dans le runtime du navigateur. Pour la production elle-même, la pré-compilation est préférable, mais en mode développement, elle est possible en runtime. Puisque nous avons déjà un technicien de service, nous allons l'utiliser.


Installez un package spécial avec Babel:


 $ npm install @babel/standalone --save-dev 

Ajoutez maintenant ce qui suit à 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' }) }) ) ) } }); 

Ici, nous avons utilisé la même approche pour intercepter les requêtes réseau et les réécrire, nous avons utilisé Babel pour transformer le code source d'origine. Veuillez noter que le plugin pour les importations dynamiques s'appelle syntax-dynamic-import , pas comme d'habitude @babel/plugin-syntax-dynamic-import car il s'agit d'une version autonome.


CSS


Dans l'article mentionné, l'auteur a utilisé la transformation de texte, nous irons un peu plus loin et intégrerons CSS sur la page. Pour ce faire, nous utiliserons à nouveau le 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 nous ouvrons maintenant src/index-dev.html dans un navigateur, nous verrons des boutons. Assurez-vous que le Service Worker requis est installé et n'est en conflit avec rien. Si vous n'êtes pas sûr, alors juste au cas où, vous pouvez ouvrir Dev Tools, aller dans Application , là dans Service Workers , et cliquer sur Unregister pour tous les travailleurs enregistrés, puis recharger la page.


La production


Le code ci-dessus fonctionne comme il se doit en mode de développement, mais en soi, nous ne voulons pas forcer les utilisateurs du site à compiler du code dans leurs navigateurs, cela est complètement impossible. Faisons une sorte de mode de production minimaliste.


Créez un point d'entrée src/index.html distinct:


 <!DOCTYPE html> <html> <body> <div id="root"></div> <script type="module" src="index.js"></script> </body> </html> 

Comme vous pouvez le voir, il n'y a pas de stubs ici, nous utiliserons une autre méthode pour réécrire les noms des packages. Comme nous avons besoin de Babel pour recompiler JSX, nous l'utiliserons pour importMap.json chemins au lieu d' importMap.json pour le stub. Installez les packages nécessaires:


 $ npm install @babel/cli @babel/core @babel/preset-react @babel/plugin-syntax-dynamic-import babel-plugin-module-resolver --save-dev 

Ajoutez une section avec des scripts à package.json :


 { "scripts": { "start": "npm run build -- --watch", "build": "babel src/app --out-dir build/app --source-maps --copy-files" } } 

Ajoutez le fichier .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('../../', '../') } ] ] } 

Il convient de garder à l'esprit que ce fichier sera utilisé uniquement pour la production, en mode développement, nous configurons Babel dans Service Worker.


Ajoutez le mode combat à Service Worker:


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

Ajoutez les conditions à 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'); 

Remplacer


 // src/sw.js if (!ext && !url.endsWith('/')) { url = url + '.jsx' with } 

Sur


 // src/sw.js if (!ext && !url.endsWith('/')) { url = url + '.' + (production ? 'js' : 'jsx'); } 

Créons un petit script de console build.sh (les gens avec Windows peuvent créer le même pour Windows dans l'image et la ressemblance) qui collectera tout ce dont vous avez besoin dans le répertoire 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 

Nous allons de cette façon pour que le répertoire node_modules gonfle pas en production à partir de dépendances nécessaires uniquement dans la phase de construction et en mode de développement.


Dépôt final: http://github.com/kirill-konshin/pure-react-with-dynamic-imports


Si nous ouvrons maintenant build/index.html nous verrons la même sortie que dans src/index-dev.html mais cette fois le navigateur ne collectera rien, il utilisera les fichiers précédemment collectés par Babel.


Comme vous pouvez le voir, il y a duplication dans la solution: importMap.json , section alias du fichier .babelrc.js et une liste de fichiers à copier dans build.sh . Cela fera l'affaire pour une démo, mais en général, il devrait être en quelque sorte automatisé.


L'assemblage est disponible sur: https://kirill-konshin.imtqy.com/pure-react-with-dynamic-imports/index.html


Conclusion


En général, un produit complètement viable a été obtenu, bien que très brut.


HTTP2 est censé prendre en charge un tas de petits fichiers envoyés sur le réseau.


Référentiel où vous pouvez voir le code

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


All Articles