Bereaksi, JSX, impor modul ES (termasuk dinamis) di browser tanpa Webpack

Artikel ini adalah upaya untuk menyatukan alat yang tersedia saat ini dan mencari tahu apakah mungkin untuk membuat aplikasi siap produksi di Bereaksi tanpa kompilasi awal oleh kolektor seperti Webpack, atau setidaknya meminimalkan kompilasi tersebut.


Semuanya dijelaskan sangat eksperimental dan saya sengaja mengambil jalan pintas. Dalam hal apapun saya tidak merekomendasikan melakukan sesuatu seperti ini pada produksi nyata.


Kemampuan untuk menggunakan modul ECMAScript ( <script type="module"/> dengan impor formulir import Foo from './foo'; dan import('./Foo') ) secara langsung di peramban bukan hal yang baru dalam waktu yang lama, fungsi yang didukung dengan baik: https: //caniuse.com/#feat=es6-module .


Namun pada kenyataannya, kami mengimpor tidak hanya modul kami, tetapi juga perpustakaan. Ada artikel yang bagus tentang topik ini: https://salomvary.com/es6-modules-in-browsers.html . Dan artikel lain yang juga layak disebut adalah https://github.com/stken2050/esm-bundlerless .


Di antara hal-hal penting lainnya dari artikel ini, poin-poin ini paling penting untuk membuat aplikasi Bereaksi:


  • Dukungan untuk impor penentu paket (atau impor peta): ketika kami menulis import React from 'react' sebenarnya kami harus mengimpor sesuatu seperti ini https://cdn.com/react/react.production.js
  • Dukungan untuk UMD: React masih didistribusikan sebagai UMD dan saat ini penulis belum sepakat tentang bagaimana mendistribusikan perpustakaan sebagai modul
  • Jsx
  • Impor CSS

Mari kita lihat semua poin pada gilirannya.


Struktur proyek


Pertama-tama, kami akan menentukan struktur proyek:


  • node_modules jelas di sinilah dependensi akan diletakkan
  • direktori src dengan index*.html dan skrip layanan
    • kode aplikasi langsung aplikasi pada Bereaksi

Dukungan impor specifier paket


Untuk menggunakan Bereaksi melalui import React from 'react'; kita harus memberi tahu browser tempat mencari sumber yang sebenarnya, karena react bukan file nyata, tetapi penunjuk ke perpustakaan. Ada tulisan rintisan untuk https://github.com/guybedford/es-module-shims ini.


Mari kita mengatur rintisan dan Bereaksi:


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

Kami akan memulai aplikasi dari file 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> 

Di mana src/app/index.jsx terlihat seperti ini:


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

Dan src/app/Button.jsx menyukai ini:


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

Akankah ini berhasil? Tentu saja tidak. Bahkan terlepas dari kenyataan bahwa semuanya berhasil diimpor dari mana diperlukan.


Mari kita beralih ke masalah selanjutnya.


Dukungan UMD


Cara yang dinamis


Berdasarkan fakta bahwa Bereaksi didistribusikan sebagai UMD, itu tidak dapat diimpor secara langsung, bahkan melalui rintisan (jika tiket ditutup karena diperbaiki, Anda dapat melewati langkah). Kita perlu menambal sumbernya agar menjadi kompatibel.


Artikel di atas mendorong saya untuk menggunakan Pekerja Layanan untuk ini, yang dapat mencegat dan memodifikasi permintaan dan respons jaringan. Mari kita buat entry point utama src/index.js , di mana kita akan mengkonfigurasi SW dan App dan menggunakannya daripada langsung memanggil aplikasi ( 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); } })(); 

Buat Pekerja Layanan ( 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' }) }) ) ) } }); 

Untuk meringkas apa yang telah dilakukan:


  1. Kami membuat peta ekspor yang mengaitkan nama paket dengan variabel global
  2. Buat tag skrip di head dengan isi skrip yang dibungkus dengan UMD
  3. Variabel global yang diekspor sebagai ekspor default

Untuk demo, tambalan brutal seperti itu sudah cukup, tetapi ini mungkin tidak berfungsi dengan semua pembungkus UMD. Sebagai gantinya, sesuatu yang lebih dapat diandalkan dapat digunakan.


Sekarang ubah src/index-dev.html untuk menggunakan skrip konfigurasi:


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

Sekarang kita dapat mengimpor Bereaksi dan Bereaksi DOM.


Jalur statis


Perlu dicatat bahwa ada cara lain. Ada membangun ES tidak resmi dari React:


 npm install esm-react --save 

Peta impor akan terlihat seperti ini:


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

Namun sayangnya proyek ini sangat jauh ketinggalan, versi terbaru adalah 16.8.3 sedangkan React sudah 16.10.2 .


Jsx


Ada dua cara untuk mengkompilasi JSX. Kita dapat pra-merakit Babel tradisional dari konsol, atau ini bisa dilakukan di browser runtime. Untuk produksi itu sendiri, pra-kompilasi lebih disukai, tetapi dalam mode pengembangan dimungkinkan dalam runtime. Karena kami sudah memiliki Pekerja Layanan, kami akan menggunakannya.


Instal paket khusus dengan Babel:


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

Sekarang tambahkan berikut ini ke Pekerja Layanan ( 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' }) }) ) ) } }); 

Di sini kami menggunakan pendekatan yang sama dengan memotong permintaan jaringan dan menulis ulang, kami menggunakan Babel untuk mengubah kode sumber asli. Harap dicatat bahwa plugin untuk impor dinamis disebut syntax-dynamic-import , tidak seperti biasa @babel/plugin-syntax-dynamic-import karena ini adalah versi Standalone.


CSS


Dalam artikel yang disebutkan, penulis menggunakan transformasi teks, kami akan melangkah lebih jauh dan menanamkan CSS pada halaman. Untuk melakukan ini, kami akan kembali menggunakan Pekerja Layanan ( 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! Jika sekarang kita membuka src/index-dev.html di browser, kita akan melihat tombol. Pastikan bahwa Pekerja Servis yang diperlukan telah diinstal dan tidak bertentangan dengan apa pun. Jika Anda tidak yakin, maka untuk berjaga-jaga, Anda dapat membuka Alat Dev, masuk ke Application , di sana di Service Workers , dan klik Unregister untuk semua pekerja terdaftar, lalu muat ulang halaman.


Produksi


Kode di atas berfungsi sebagaimana mestinya dalam mode pengembangan, tetapi dengan sendirinya kami tidak ingin memaksa pengguna situs untuk mengkompilasi kode di browser mereka, ini sama sekali tidak praktis. Mari kita membuat semacam mode produksi minimalis.


Buat titik masuk src/index.html terpisah:


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

Seperti yang Anda lihat, tidak ada stubs di sini, kami akan menggunakan metode lain untuk menulis ulang nama paket. Karena kita membutuhkan Babel untuk mengkompilasi JSX lagi, kita akan menggunakannya untuk importMap.json path daripada importMap.json untuk rintisan. Instal paket yang diperlukan:


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

Tambahkan bagian dengan skrip ke package.json :


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

Tambahkan file .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('../../', '../') } ] ] } 

Harus diingat bahwa file ini hanya akan digunakan untuk produksi, dalam mode pengembangan kami mengkonfigurasi Babel di Pekerja Layanan.


Tambahkan mode tempur ke Pekerja Servis:


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

Tambahkan ketentuan ke 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'); 

Ganti


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

Aktif


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

Mari kita membuat skrip konsol build.sh (orang-orang dengan Windows dapat membuat yang sama untuk Windows dalam gambar dan rupa) yang akan mengumpulkan semua yang Anda butuhkan ke direktori 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 

Kita pergi dengan cara ini sehingga direktori node_modules tidak membengkak pada produksi dari dependensi yang dibutuhkan hanya dalam fase pembangunan dan dalam mode pengembangan.


Gudang final: http://github.com/kirill-konshin/pure-react-with-dynamic-imports


Jika sekarang kita buka build/index.html maka kita akan melihat output yang sama seperti di src/index-dev.html tetapi kali ini browser tidak akan mengumpulkan apa-apa, itu akan menggunakan file yang sebelumnya dikumpulkan oleh Babel.


Seperti yang Anda lihat, ada duplikasi dalam solusinya: importMap.json , alias bagian dari file .babelrc.js dan daftar file yang akan disalin ke build.sh . Ini akan berfungsi untuk demo, tetapi secara umum itu harus otomatis.


Majelis tersedia di: https://kirill-konshin.imtqy.com/pure-react-with-dynamic-imports/index.html


Kesimpulan


Secara umum, produk yang benar-benar layak diperoleh, meskipun sangat mentah.


HTTP2 seharusnya menangani banyak file kecil yang dikirim melalui jaringan.


Repositori tempat Anda dapat melihat kode

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


All Articles