هذه المقالة محاولة لجمع الأدوات المتوفرة حاليًا ومعرفة ما إذا كان من الممكن إنشاء تطبيقات جاهزة للإنتاج على 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' }) }) ) ) } });
لتلخيص ما تم القيام به:
- لقد أنشأنا خريطة تصدير تربط اسم الحزمة مع متغير عمومي
- إنشاء علامة برنامج نصي في
head
مع محتويات البرنامج النصي ملفوفة في UMD - متغير عمومي تصدير كتصدير افتراضي
بالنسبة إلى العرض التوضيحي ، سيكون مثل هذا التصحيح الوحشي كافياً ، لكن هذا قد لا يعمل مع جميع الأغلفة 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> <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 لرعاية مجموعة من الملفات الصغيرة المرسلة عبر الشبكة.
مستودع حيث يمكنك رؤية الكود