React ، JSX ، استيراد وحدات ES (بما في ذلك الديناميكي) في مستعرض بدون Webpack

هذه المقالة محاولة لجمع الأدوات المتوفرة حاليًا ومعرفة ما إذا كان من الممكن إنشاء تطبيقات جاهزة للإنتاج على React دون تجميع أولي بواسطة جامعي مثل Webpack ، أو على الأقل تقليل هذا التجميع.


كل ما تم وصفه تجريبي للغاية وقمت بقص الزوايا في الأماكن عمداً. لا أوصي بأي حال من الأحوال بعمل مثل هذا على الإنتاج الحقيقي.


إن القدرة على استخدام وحدات ECMAScript ( <script type="module"/> مع استيراد نموذج import Foo from './foo'; import('./Foo') ) مباشرة في المتصفح ليست جديدة لفترة طويلة ، إنها وظيفة مدعومة جيدًا: https: //caniuse.com/#feat=es6-module .


لكن في الواقع ، نحن لا نستورد وحداتنا فقط ، بل نستوردها أيضًا. يوجد مقال ممتاز حول هذا الموضوع: https://salomvary.com/es6-modules-in-browsers.html . ومقال آخر جيد على حد سواء يستحق الذكر هو https://github.com/stken2050/esm-bundlerless .


من بين الأشياء المهمة الأخرى الواردة في هذه المقالات ، تعتبر هذه النقاط هي الأكثر أهمية لإنشاء تطبيق React:


  • دعم عمليات استيراد محددات الحزمة (أو استيراد الخرائط): عندما نكتب " import React from 'react' في الواقع ، يجب علينا استيراد شيء مثل هذا https://cdn.com/react/react.production.js
  • دعم UMD: لا يزال React موزعًا على UMD وفي الوقت الحالي لم يتفق المؤلفون بعد على كيفية توزيع المكتبة كوحدة نمطية
  • JSX
  • استيراد CSS

دعنا نذهب من خلال جميع النقاط في المقابل.


هيكل المشروع


بادئ ذي بدء ، سوف نحدد هيكل المشروع:


  • node_modules الواضح أن node_modules هذا هو المكان الذي سيتم فيه وضع التبعيات
  • دليل src مع index*.html والبرامج النصية للخدمة
    • app مباشرة رمز التطبيق على React

يستورد محدد الحزمة الدعم


لاستخدام React خلال import React from 'react'; يجب أن نقول للمتصفح أن نبحث عن المصدر الحقيقي ، لأن react ليس ملفًا حقيقيًا ، لكنه مؤشر إلى مكتبة. هناك كعب روتين لهذا https://github.com/guybedford/es-module-shims .


دعنا ننشئ كعب الردة ونتفاعل:


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

سنبدأ التطبيق من ملف 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 مثل هذا:


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

و src/app/Button.jsx مثل هذا:


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

هل سينجح هذا؟ بالطبع لا. على الرغم من حقيقة أن كل شيء يتم استيراده بنجاح من عند الضرورة.


دعنا ننتقل إلى المشكلة التالية.


دعم UMD


طريقة ديناميكية


استنادًا إلى حقيقة أن React يتم توزيعها على أنها UMD ، لا يمكن استيرادها مباشرةً ، حتى من خلال كعب الروتين (إذا تم إغلاق البطاقة كإصلاح ، فيمكنك تخطي الخطوة). نحتاج إلى تصحيح المصدر بطريقة ما بحيث يصبح متوافقًا.


دفعتني المقالات أعلاه إلى استخدام "عمال الخدمة" لهذا الأمر ، والذي يمكنه اعتراض وتعديل طلبات الشبكة والاستجابات. لنقم بإنشاء نقطة الدخول الرئيسية src/index.js ، حيث سنقوم بتكوين SW والتطبيق واستخدامه بدلاً من استدعاء التطبيق مباشرةً ( 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); } })(); 

إنشاء عامل خدمة ( 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' }) }) ) ) } }); 

لتلخيص ما تم القيام به:


  1. لقد أنشأنا خريطة تصدير تربط اسم الحزمة مع متغير عمومي
  2. إنشاء علامة برنامج نصي في head مع محتويات البرنامج النصي ملفوفة في UMD
  3. متغير عمومي تصدير كتصدير افتراضي

بالنسبة إلى العرض التوضيحي ، سيكون مثل هذا التصحيح الوحشي كافياً ، لكن هذا قد لا يعمل مع جميع الأغلفة UMD. شيء أكثر موثوقية يمكن استخدامها في المقابل.


الآن قم بتغيير src/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">...    </script> <!--  app/index.jsx  index.js --> <script type="module-shim" src="index.js"></script> </body> </html> 

الآن يمكننا استيراد React و React DOM.


مسار ثابت


تجدر الإشارة إلى أن هناك طريقة أخرى. هناك بنية ES غير رسمية لـ React:


 npm install esm-react --save 

ستبدو خريطة الاستيراد بالشكل التالي:


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

لكن لسوء الحظ ، فإن المشروع متأخر جدًا ، الإصدار الأخير هو 16.8.3 بينما 16.10.2 بالفعل 16.10.2 .


JSX


هناك طريقتان لتجميع JSX. يمكننا إما تجميع Babel التقليدي من وحدة التحكم ، أو يمكن القيام بذلك في وقت تشغيل المتصفح. بالنسبة للإنتاج نفسه ، يكون التجميع المسبق هو الأفضل ، ولكن في وضع التطوير يكون ذلك ممكنًا في وقت التشغيل. نظرًا لأن لدينا بالفعل عامل خدمة ، سنستخدمه.


تثبيت حزمة خاصة مع بابل:


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

أضف الآن ما يلي إلى 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' }) }) ) ) } }); 

استخدمنا هنا النهج نفسه في اعتراض طلبات الشبكة وإعادة كتابتها ، استخدمنا بابل لتحويل شفرة المصدر الأصلية. يرجى ملاحظة أن المكوّن الإضافي للواردات الديناميكية يسمى syntax-dynamic-import ، وليس كالمعتاد @babel/plugin-syntax-dynamic-import لأنه إصدار مستقل.


CSS


في المقالة المذكورة ، استخدم المؤلف تحويل النص ، سنذهب إلى أبعد من ذلك قليلاً ونضمّن CSS على الصفحة. للقيام بذلك ، سوف نستخدم "عامل الخدمة" ( 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' }) }) ) ); } }); 

فويلا! إذا src/index-dev.html الآن src/index-dev.html في المستعرض ، src/index-dev.html الأزرار. تأكد من تثبيت "عامل الخدمة" المطلوب وعدم تعارضه مع أي شيء. إذا لم تكن متأكدًا ، فقط في حالة إمكانية فتح أدوات Dev ، والانتقال إلى Application ، وهناك في Service Workers ، والنقر فوق Unregister التسجيل لجميع العمال المسجلين ، ثم إعادة تحميل الصفحة.


الإنتاج


تعمل الشفرة أعلاه كما ينبغي في وضع التطوير ، ولكن في حد ذاته لا نريد إجبار مستخدمي الموقع على ترجمة الشفرة في متصفحاتهم ، فهذا غير عملي تمامًا. دعونا نجعل نوعا من وضع الإنتاج في أضيق الحدود.


قم بإنشاء نقطة إدخال src/index.html منفصلة:


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

كما ترون ، لا توجد أجزاء صغيرة هنا ، سنستخدم طريقة أخرى لإعادة كتابة أسماء الحزمة. نظرًا لأننا نحتاج إلى Babel لتجميع JSX مرة أخرى ، importMap.json المسارات بدلاً من importMap.json . تثبيت الحزم اللازمة:


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

إضافة قسم مع البرامج النصية إلى package.json :


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

أضف ملف .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('../../', '../') } ] ] } ، 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('../../', '../') } ] ] } 

يجب أن يؤخذ في الاعتبار أن هذا الملف سوف يستخدم فقط للإنتاج ، في وضع التطوير نقوم بتكوين Babel في 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'); } 

أضف الشروط إلى 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'); 

استبدل


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

في


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

دعنا ننشئ برنامج إنشاء نص صغير لوحدة التحكم (يمكن للأشخاص الذين يستخدمون Windows إنشاء نفسه لـ Windows في الصورة ومثالها) والذي سيجمع كل ما تحتاجه في دليل الإنشاء:


 #  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 

نذهب بهذه الطريقة حتى node_modules يتضخم دليل node_modules على الإنتاج من التبعيات المطلوبة فقط في مرحلة node_modules وفي وضع التطوير.


المستودع النهائي: http://github.com/kirill-konshin/pure-react-with-dynamic-imports


إذا src/index-dev.html الآن build/index.html src/index-dev.html نفس الناتج كما هو الحال في src/index-dev.html ولكن هذه المرة لن يجمع المتصفح أي شيء ، src/index-dev.html الملفات التي تم جمعها مسبقًا بواسطة Babel.


كما ترون ، هناك ازدواجية في الحل: importMap.json ، قسم alias لملف .babelrc.js وقائمة الملفات المراد نسخها إلى build.sh . ستعمل من أجل العرض التوضيحي ، ولكن بشكل عام يجب أن تكون آلية بطريقة ما.


التجميع متاح في: https://kirill-konshin.imtqy.com/pure-react-with-dynamic-imports/index.html


استنتاج


بشكل عام ، تم الحصول على منتج قابل للحياة تمامًا ، رغم أنه خام للغاية.


من المفترض HTTP2 لرعاية مجموعة من الملفات الصغيرة المرسلة عبر الشبكة.


مستودع حيث يمكنك رؤية الكود

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


All Articles