Dieser Artikel ist ein Versuch, die derzeit verfügbaren Tools zusammenzuführen und herauszufinden, ob es möglich ist, produktionsbereite Anwendungen auf React ohne vorherige Kompilierung durch Sammler wie Webpack zu erstellen oder zumindest eine solche Kompilierung zu minimieren.
Alles, was beschrieben wird, ist sehr experimentell und ich habe absichtlich Ecken abgeschnitten. In keinem Fall empfehle ich, so etwas in der realen Produktion zu machen.
Die Möglichkeit, ECMAScript-Module ( <script type="module"/>
mit Importen des Formulars import Foo from './foo';
und import('./Foo')
) direkt in den Browser zu verwenden, ist lange Zeit nicht neu, es ist eine gut unterstützte Funktionalität: https: //caniuse.com/#feat=es6-module .
In Wirklichkeit importieren wir aber nicht nur unsere Module, sondern auch Bibliotheken. Zu diesem Thema gibt es einen ausgezeichneten Artikel: https://salomvary.com/es6-modules-in-browsers.html . Ein weiterer ebenso guter und erwähnenswerter Artikel ist https://github.com/stken2050/esm-bundlerless .
Unter den anderen wichtigen Dingen aus diesen Artikeln sind diese Punkte für die Erstellung einer React-Anwendung am wichtigsten:
- Unterstützung für Paketspezifizierer-Importe (oder Import-Maps): Wenn wir
import React from 'react'
schreiben, sollten wir tatsächlich so etwas importieren: https://cdn.com/react/react.production.js
- Unterstützung für UMD: React wird weiterhin als UMD vertrieben, und die Autoren haben sich derzeit noch nicht darauf geeinigt, wie die Bibliothek als Modul vertrieben werden soll
- Jsx
- CSS-Import
Lassen Sie uns nacheinander alle Punkte durchgehen.
Projektstruktur
Zunächst bestimmen wir die Projektstruktur:
node_modules
natürlich die Abhängigkeiten abgelegtsrc
Verzeichnis mit index*.html
und Dienstskripten
app
direkt Anwendungscode auf React
Unterstützung für Paketspezifiziererimporte
So verwenden Sie Reagieren durch import React from 'react';
wir müssen dem Browser sagen, wo er nach der wirklichen Quelle suchen soll, weil react
ist keine echte Datei, sondern ein Zeiger auf eine Bibliothek. Es gibt einen Stub für diese https://github.com/guybedford/es-module-shims .
Lassen Sie uns den Stub einrichten und reagieren:
$ npm i es-module-shims react react-dom --save
Wir starten die Anwendung aus der Datei 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>
Wo src/app/index.jsx
aussieht:
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); })();
Und src/app/Button.jsx
gefällt das:
import React from 'react'; export const Button = ({children}) => <button>{children}</button>;
Wird das funktionieren? Natürlich nicht. Auch wenn bei Bedarf alles erfolgreich importiert wird.
Fahren wir mit dem nächsten Problem fort.
UMD-Unterstützung
Dynamischer Weg
Aufgrund der Tatsache, dass React als UMD vertrieben wird, kann es nicht direkt importiert werden, auch nicht über einen Stub (wenn das Ticket als repariert geschlossen wurde, können Sie den Schritt überspringen). Wir müssen die Quelle irgendwie patchen, damit sie kompatibel wird.
Die obigen Artikel haben mich dazu veranlasst, hierfür Service Worker zu verwenden, die Netzwerkanforderungen und -antworten abfangen und ändern können. Erstellen wir den Haupteinstiegspunkt src/index.js
, wo wir SW und die App konfigurieren und verwenden, anstatt die Anwendung direkt aufzurufen ( 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); } })();
Erstellen Sie einen Service Worker ( 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' }) }) ) ) } });
Um zusammenzufassen, was getan wurde:
- Wir haben eine Export-Map erstellt, die einen Paketnamen mit einer globalen Variablen verknüpft
- Erstellen Sie im
head
ein Skript-Tag mit dem Inhalt des in UMD eingeschlossenen Skripts - Exportierte globale Variable als Standardexport
Für eine Demo ist ein derart brutaler Patch ausreichend, dies funktioniert jedoch möglicherweise nicht mit allen UMD-Wrappern. Im Gegenzug kann etwas Zuverlässigeres verwendet werden.
Ändern Sie nun src/index-dev.html
, um das Konfigurationsskript zu verwenden:
<!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>
Jetzt können wir React und React DOM importieren.
Statischer Pfad
Es sollte beachtet werden, dass es einen anderen Weg gibt. Es gibt einen inoffiziellen ES-Build von React:
npm install esm-react --save
Die Importkarte sieht folgendermaßen aus:
{ "imports": { "react": "../node_modules/esm-react/src/react.js", "react-dom": "../node_modules/esm-react/src/react-dom.js" } }
Leider ist das Projekt sehr weit zurück, die neueste Version ist 16.8.3
während React bereits 16.10.2
.
Jsx
Es gibt zwei Möglichkeiten, JSX zu kompilieren. Wir können entweder das traditionelle Babel von der Konsole aus vormontieren oder dies kann in der Browser-Laufzeit erfolgen. Für die eigentliche Produktion ist eine Vorkompilierung vorzuziehen, im Entwicklungsmodus ist dies jedoch zur Laufzeit möglich. Da wir bereits einen Servicemitarbeiter haben, werden wir ihn verwenden.
Installiere ein spezielles Paket mit Babel:
$ npm install @babel/standalone --save-dev
src/sw.js
Service Worker ( src/sw.js
) nun Folgendes 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' }) }) ) ) } });
Hier haben wir den gleichen Ansatz verwendet, um Netzwerkanforderungen abzufangen und neu zu schreiben. Wir haben Babel verwendet, um den ursprünglichen Quellcode zu transformieren. Bitte beachten Sie, dass das Plugin für dynamische Importe syntax-dynamic-import
, nicht wie üblich @babel/plugin-syntax-dynamic-import
da es sich um eine eigenständige Version handelt.
CSS
In dem erwähnten Artikel hat der Autor die Texttransformation verwendet. Wir werden etwas weiter gehen und CSS auf der Seite einbetten. Dazu verwenden wir wieder den 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! Wenn wir jetzt src/index-dev.html
in einem Browser öffnen, werden Schaltflächen angezeigt. Stellen Sie sicher, dass der erforderliche Service Worker installiert ist und mit nichts in Konflikt steht. Wenn Sie sich nicht sicher sind, können Sie für alle Fälle Dev Tools öffnen, dort unter Service Workers
zur Application
Unregister
für alle registrierten Worker die Registrierung aufheben und dann die Seite neu laden.
Produktion
Der obige Code funktioniert im Entwicklungsmodus wie gewünscht, aber wir möchten Site-Benutzer nicht dazu zwingen, Code in ihren Browsern zu kompilieren. Dies ist völlig unpraktisch. Machen wir eine Art minimalistischen Produktionsmodus.
Erstellen Sie einen separaten Einstiegspunkt für src/index.html
:
<!DOCTYPE html> <html> <body> <div id="root"></div> <script type="module" src="index.js"></script> </body> </html>
Wie Sie sehen können, gibt es hier keine Stubs. Wir werden eine andere Methode zum Umschreiben von Paketnamen verwenden. Da Babel JSX erneut kompilieren muss, werden wir es verwenden, importMap.json
Pfade importMap.json
zu importMap.json
anstatt importMap.json
zu importMap.json
. Installieren Sie die erforderlichen Pakete:
$ npm install @babel/cli @babel/core @babel/preset-react @babel/plugin-syntax-dynamic-import babel-plugin-module-resolver --save-dev
package.json
einen Abschnitt mit Skripten zu package.json
:
{ "scripts": { "start": "npm run build -- --watch", "build": "babel src/app --out-dir build/app --source-maps --copy-files" } }
Fügen Sie die Datei .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('../../', '../') } ] ] }
Es ist zu beachten, dass diese Datei nur für die Produktion verwendet wird. Im Entwicklungsmodus konfigurieren wir Babel in Service Worker.
Hinzufügen des Kampfmodus zum Servicearbeiter:
// 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'); }
Fügen Sie die Bedingungen zu 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');
Ersetzen
// src/sw.js if (!ext && !url.endsWith('/')) { url = url + '.jsx' with }
Auf
// src/sw.js if (!ext && !url.endsWith('/')) { url = url + '.' + (production ? 'js' : 'jsx'); }
Lassen Sie uns ein kleines Konsolenskript build.sh
( build.sh
mit Windows können dasselbe für Windows im Abbild und in der Abbildung erstellen), das alles, was Sie benötigen, in das build
einsammelt:
# 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
Wir gehen diesen Weg, damit das Verzeichnis node_modules
bei der Produktion nicht aufgrund von Abhängigkeiten node_modules
die nur in der Erstellungsphase und im Entwicklungsmodus benötigt werden.
Endgültiges Repository: http://github.com/kirill-konshin/pure-react-with-dynamic-imports
Wenn wir jetzt build/index.html
öffnen build/index.html
wir die gleiche Ausgabe wie in src/index-dev.html
aber diesmal sammelt der Browser nichts, sondern verwendet die zuvor von Babel gesammelten Dateien.
Wie Sie sehen können, enthält die Lösung Duplikate: importMap.json
, alias
Abschnitt der Datei .babelrc.js
und eine Liste der Dateien, die in build.sh
kopiert werden build.sh
. Es reicht für eine Demo, sollte aber im Allgemeinen irgendwie automatisiert sein.
Die Assembly ist verfügbar unter: https://kirill-konshin.imtqy.com/pure-react-with-dynamic-imports/index.html
Fazit
Im Allgemeinen wurde ein vollständig lebensfähiges Produkt erhalten, obwohl es sehr roh war.
HTTP2 soll sich um eine Reihe kleiner Dateien kümmern, die über das Netzwerk gesendet werden.
Repository, in dem Sie den Code sehen können