اذهب إلى المحتوى

العمل مع أداة التحزيم webpack في تطبيقات React


ابراهيم الخضور

لقد نال تطوير المواقع باستخدام React سمعة سيئة نظرًا لحاجته إلى أدوات كثيرة كان من الصعب تهيئتها بالشكل المناسب. أما في أيامنا هذه فقد أصبح استخدام React مريحًا بفضل البرنامج create-react-app. ويمكن القول أنه لم توجد قبله أية آليات مهمة لتطوير المواقع باستخدام JavaScript من جانب المتصفح.

لكن لا يمكن الاعتماد إلى الأبد على الحلول السحرية التي يقدمها هذا البرنامج، وعلينا أن نحاول الاطلاع على ما يجري خلف الكواليس. إنّ أحد اللاعبين الأساسيين في جعل تطبيقات React جاهزة للعمل هو برنامج تجميع يدعى webpack.

تجميع وحزم الملفات

لقد طورنا تطبيقاتنا بتقسيمها إلى وحدات منفصلة، ثم أدرجنا هذه الوحدات في الأماكن التي تتطلبها. لكن ليس لدى المتصفح أية فكرة عن كيفية التعامل مع الشيفرة المجزأة ضمن وحدات، حتى ضمن وحدات ES6 التي عُرّفت في المعيار ECSMAScript.

ولهذا السبب، يجب إعادة تجميع الوحدات لكي تتعامل معها المتصفحات، أي يجب تحويل جميع ملفات الشيفرة إلى ملف واحد يحتوي على شيفرة التطبيق بالكامل. فعندما نشرنا نسخة الإنتاج من تطبيق React للواجهة الأمامية في القسم 3، نفذنا عملية تجميع للتطبيق باستخدام الأمر npm run build. حيث جمّع سكربت npm الشيفرة المصدرية باستخدام المجمّع webpack تحت الستار، ونتج عنها مجموعة من الملفات ضمن المجلد "build".

├── asset-manifest.json
├── favicon.ico
├── index.html
├── manifest.json
├── precache-manifest.8082e70dbf004a0fe961fc1f317b2683.js
├── service-worker.js
└── static
    ├── css
    │   ├── main.f9a47af2.chunk.css
    │   └── main.f9a47af2.chunk.css.map
    └── js
        ├── 1.578f4ea1.chunk.js
        ├── 1.578f4ea1.chunk.js.map
        ├── main.8209a8f2.chunk.js
        ├── main.8209a8f2.chunk.js.map
        ├── runtime~main.229c360f.js
        └── runtime~main.229c360f.js.map

حيث يمثل الملف "index.html" الموجود في جذر المجلد "build" الملف الرئيسي للتطبيق، فهو الذي يحمّل ملفات JavaScript المجمّعة داخل المعرّف <script> (يوجد في الواقع ملفي JavaScript مجمّعين).

<!doctype html><html lang="en">
<head>
  <meta charset="utf-8"/>
  <title>React App</title>
  <link href="/static/css/main.f9a47af2.chunk.css" rel="stylesheet"></head>
<body>
  <div id="root"></div>
  <script src="/static/js/1.578f4ea1.chunk.js"></script>
  <script src="/static/js/main.8209a8f2.chunk.js"></script>
</body>
</html>

فيمكن أن نرى من خلال المثال الذي أنشأناه باستخدام create-react-app، أنّ سكربت بناء التطبيق يجمّع أيضًا ملفات تنسيق CSS في ملف واحد باسم "static/css/main.f9a47af2.chunk.css/".

تجري عملية التجميع في واقع الأمر لتعريف نقطة دخول إلى التطبيق، وهي عادة الملف "index.js". فعندما يجمّع webpack الشيفرة فإنه يضم كل الشيفرات التي ستدرجها نقطة الدخول والشيفرة التي ستدرج تلك المُدرجات، وهكذا.

وطالما أنّ بعض الملفات التي تُدرج ستكون على شكل حزم مثل React وRedux وAxios، فسيضم ملف JavaScript المُجمّع محتوى كل مكتبة من تلك المكتبات.

اقتباس

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

سننشئ تاليًا يدويًا ومن الصفر ملف تهيئة webpack يناسب تطبيق React. لننشئ أولًا مجلدًا جديدًا لمشروعنا يحوي على المجلدين الفرعيين "build" و"src" وداخلهما الملفات التالية:

├── build
├── package.json
├── src
│   └── index.js
└── webpack.config.js

ستكون محتويات الملف "package.json" كالتالي:

{
  "name": "webpack-part7",
  "version": "0.0.1",
  "description": "practising webpack",
  "scripts": {},
  "license": "MIT"
}

لنثبت webpack كالتالي:

npm install --save-dev webpack webpack-cli

نحدد عمل webpack من خلال الملف "webpack.config.js" والذي نعطيه قيمًا أولية كالتالي:

const path = require('path')

const config = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'main.js'
  }
}
module.exports = config

سنعرف بعدها سكربت npm يُدعى "build" سينفذ عملية التجميع مع webpack:

// ...
"scripts": {
  "build": "webpack --mode=development"
},
// ...

سنضع بعض الشيفرة في الملف "src/index.js"

const hello = name => {
  console.log(`hello ${name}`)
}

عندما ننفذ الأمر npm run build، سيجمّع webpack شيفرة تطبيقنا وسينتج عنه ملف جديد "main.js" ستجده ضمن المجلد "build":

bundling_by_npm_webpack_01.png

يحتوي الملف على العديد من النقاط المهمة. كما يمكننا أن نرى الشيفرة التي كتبناها سابقًا في آخر الملف:

main_content_02.png

سنضيف الآن الملف "App.js" إلى المجلد src، وسيحتوي الشيفرة التالية:

const App = () => {
  return null
}

export default App

لندرج الوحدة App ضمن الملف "index.js":

import App from './App';

const hello = name => {
  console.log(`hello ${name}`)
}

App()

عندما نجمع التطبيق من جديد بالأمر npm run build، سنجد أن webpack قد ميّز ملفين:

webpack_bundle_two_files_03.png

ستجد شيفرة التطبيق في نهاية ملف التجميع وبطريقة مبهمة نوعًا ما:

/***/ "./src/App.js":
/*!********************!*\
  !*** ./src/App.js ***!
  \********************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\nconst App = () => {\n  return null\n}\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (App);\n\n//# sourceURL=webpack:///./src/App.js?");

/***/ }),

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _App__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./App */ \"./src/App.js\");\n\n\nconst hello = name => {\n  console.log(`hello ${name}`)\n};\n\nObject(_App__WEBPACK_IMPORTED_MODULE_0__[\"default\"])()\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

ملف التهيئة

لنلق نظرة على الملف "webpack.config.js":

const path = require('path')

const config = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'main.js'
  }
}

module.exports = config

كُتب ملف التهيئة باستخدام JavaScript، وصُدّر كائن التهيئة باستخدام عبارة وحدة Node. ستشرح عبارات التهيئة البسيطة التي كتبناها نفسها بنفسها. حيث تحدد الخاصية entry لكائن التهيئة الملف الذي سيستخدم كنقطة دخول إلى التطبيق المُجمّع. بينما تحدد الخاصية output المكان الذي ستُخزّن فيه الشيفرة المجمعة.يجب تعريف المسار الهدف على شكل مسار مطلق، ويسهل عمل ذلك باستخدام التابع path.resolve. كما سنستخدم المتغير العام ‎__dirname في Node والذي سيخزّن مسار الوصول إلى المجلد الحالي.

تجميع تطبيق React

لنحوّل تطبيقنا إلى تطبيق React بسيط.

سنثبّت أولًا المكتبات الضرورية:

npm install react react-dom

ثم سنحول تطبيقنا إلى تطبيق React بإضافة التعريفات التالية إلى الملف "index.js":

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(<App />, document.getElementById('root'))

كما سنجري التغييرات التالية على الملف "App.js":

import React from 'react'

const App = () => (
  <div>hello webpack</div>
)

export default App

سنحتاج أيضًا إلى الملف "build/index.html" والذي سيشكل الصفحة الرئيسية للتطبيق والتي ستحمل بدورها شيفرة JavaScript المجمعة لتطبيقنا باستخدام المعرّف script:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/javascript" src="./main.js"></script>
  </body>
</html>

ستواجهنا المشكلة التالية عند تجميع التطبيق:

bundle_react_problem_04.png

المُحمّلات

تنص رسالة الخطأ السابقة الناتجة عن webpack، على أننا قد نحتاج إلى مُحمّل مناسب لتجميع الملف App.js بالشكل الصحيح. لا يعرف webpack افتراضيًا سوى تجميع شيفرة JavaScript الأساسية. وربما أغفلنا فكرة أننا نستخدم عمليًا JSX لتصيير واجهات العرض في React، ولتوضيح ذلك، سنعرض الشيفرة التالية والتي لا تمثل شيفرة JavaScript نظامية:

const App = () => {
  return <div>hello webpack</div>
}

إنّ الشيفرة السابقة مكتوبة باستخدام JSX التي تؤمن طريقة بديلة لتعريف عناصر React ضمن المعرف <div> للغة HTML.

يمكن استخدام المُحمّلات لإبلاغ webpack عن الملفات التي ينبغي معالجتها قبل تجميعها. لنهيئ مُحملًا لتطبيقنا، يحوّل شيفرة JSX إلى شيفرة JavaScript نظامية:

const config = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'main.js',
  },
  module: {    
      rules: [      
          {        
              test: /\.js$/,        
              loader: 'babel-loader',        
              options: {          
                  presets: ['@babel/preset-react'],        
              },      
          },    
      ],  
  },}

عُرّف المُحمّل ضمن الخاصية module في المصفوفة rules.

يتألف تعريف المُحمّل من ثلاثة أقسام:

{
  test: /\.js$/,
  loader: 'babel-loader',
  options: {
    presets: ['@babel/preset-react']
  }
}

تحدد الخاصية test بأن المُحمِّل مخصص للملفات التي تنتهي باللاحقة "js"، بينما تحدد الخاصية loader أن معالجة هذه الملفات ستجري باستخدام babel-loader، أما الخاصية options فتستخدم لتحديد معاملات تهيئ وظيفة المحمّل.

لنثبت المُحمِّل والحزم التي يحتاجها كاعتمادية تطوير:

npm install @babel/core babel-loader @babel/preset-react --save-dev

سينجح الآن تجميع التطبيق.

لو عدّلنا قليلًا في شيفرة المكوّن APP، ثم ألقينا نظرة على الشيفرة المجمّعة، سنلاحظ أنّ النسخة المجمّعة من المكوّن ستكون على النحو التالي:

const App = () =>
  react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(
    'div',
    null,
    'hello webpack'
  )

يمكن أن نلاحظ من خلال هذا المثال، أنّ عناصر React قد كُتبت بعد التجميع بشيفرة JavaScript نظامية بدلًا من عبارات JSX، وذلك باستخدام الدالة createElement التي تقدمها React.

يمكننا اختبار التطبيق المجمّع بتشغيل الملف "build/index.html" من خلال المتصفح:

testing_bundled_app_05.png

تجدر الإشارة إلى أن استخدام عبارة awit/async ضمن شيفرة التطبيق، سيمنع بعض المتصفحات من إظهار أي شيء. يقودنا البحث عن رسالة الخطأ الظاهرة على الطرفية إلى المشكلة. علينا تثبيت اعتمادية مفقودة أو أكثر. في حالتنا نجد أن الاعتمادية ‎@babel/polyfill هي العنصر المفقود:

npm install @babel/polyfill

لنجري التعديلات التالية على الخاصية entry لكائن تهيئة webpack، وذلك ضمن الملف "webpack.config.js":

  entry: ['@babel/polyfill', './src/index.js']

يحتوي ملف التهيئة الآن كل ما يلزم لتطوير تطبيقات React.

نواقل الشيفرة

تُعرّف عملة نقل الشيفرة (Transpilling) بأنها عملية تحويل شيفرة JavaScript من شكل إلى آخر. ويشير المصطلح بشكل عام، إلى عملية ترجمة الشيفرة المصدرية بتحويلها من لغة إلى أخرى.

عند استخدام تعليمات التهيئة التي شرحناها سابقًا، فإننا نقلنا عمليًا شيفرة JSX إلى شيفرة JavaScript نظامية بمساعدة babel والذي يمثل حاليًا الأداة الأكثر شعبية لهذا الغرض.

وكما ذكرنا في القسم 1، أنّ معظم المتصفحات لا تدعم آخر الميزات التي قدمتها ES6 وES7، ولهذا تُنقل الشيفرة عادةً إلى نسخة JavaScript التي تتوافق مع معايير ES5.

تُعرّف عملية النقل التي ينفذها Babel كإضافة. إذ يستخدم المطورين في الواقع مُهيَّئات جاهزة (presets) وهي مجموعة من الإضافات المهيئة مسبقًا.

سنستخدم حاليًا المُهيَّئة ‎@babel/preset-react لنقل الشيفرة المصدرية لتطبيقنا:

{
  test: /\.js$/,
  loader: 'babel-loader',
  options: {
    presets: ['@babel/preset-react']  }
}

سنضيف المهيَّئة ‎@babel/preset-env التي تحتوي على كل شيئ قد نحتاجه في نقل الشيفرة التي تستخدم آخر ميزات اللغة إلى معيار ES5.

{
  test: /\.js$/,
  loader: 'babel-loader',
  options: {
    presets: ['@babel/preset-env', '@babel/preset-react']  }
}

لنثبت المهيِّئة كالتالي:

npm install @babel/preset-env --save-dev

ستتحول الشيفرة بعد نقلها إلى أسلوب JavaScript القديم. فسيصبح تعريف المكوّن App كالتالي:

var App = function App() {
  return _react2.default.createElement('div', null, 'hello webpack')
};

لاحظ أن المتغيرات قد عرفت باستخدام التعليمة var لأن المعيار ES5 لا يفهم التعليمة const. وكذلك لم تُستعمل الدوال السهمية، بل استخدمت التعليمة function.

التنسيق باستخدام CSS

لنضف بعض تنسيقات CSS إلى التطبيق، وذلك بإنشاء الملف "src/index.css":

.container {
  margin: 10;
  background-color: #dee8e4;
}

لننسق المكوّن App:

const App = () => {
  return (
    <div className="container">
      hello webpack
    </div>
  )
}

ثم سندرج ملف التنسيق ضمن الملف "index.js":

import './index.css'

سيسبب ذلك انهيار عملية نقل الشيفرة:

transpill_break_06.png

إذ علينا عند استخدام CSS أن نستخدم المُحمّلين css وstyle:

{
  rules: [
    {
      test: /\.js$/,
      loader: 'babel-loader',
      options: {
        presets: ['@babel/preset-react', '@babel/preset-env'],
      },
    },
    {      
       test: /\.css$/,      
       use: ['style-loader', 'css-loader'],    
    },  
  ];
}

تقتقضي مهمة المحمّل css loader تحميل ملف CSS بينما مهمة style loader هي توليد وإضافة عنصر التنسيق الذي يحتوي على كل التنسيقات التي يستخدمها التطبيق.

وهكذا ستُعرّف تنسيقات CSS ضمن الملف الرئيسي للتطبيق "index.html". وبالتالي لا حاجة لإدراج التنسيقات ضمنه. ويمكن عند الحاجة توليد تنسيقات CSS ثم وضعها في ملف منفصل باستخدام الإضافة mini-css-extract-plugin.

عند تثبيت المحمَّلين كالتالي:

npm install style-loader css-loader --save-dev

ستنجح عملية التجميع وسيحمل التطبيق تنسيقًا جديدًا.

المكتبة Webpack-dev-server

يمكن تطوير التطبيقات باستخدام طرائق التهيئة السابقة لكنها في الواقع عملية مرهقة (مقارنة بسير نفس العمليات في Java). فعلينا في كل مرة نجري فيها تعديلًا، أن نجمّّّع التطبيق من جديد ونحدّث المتصفح لنختبر ما فعلناه.

تقدم المكتبة webpack-dev-server حلًا لمشكلتنا. سنثبتها الآن:

npm install --save-dev webpack-dev-server

لنعرف سكربت npm لتشغيل خادم التطوير (dev-server):

{
  // ...
  "scripts": {
    "build": "webpack --mode=development",
    "start": "webpack serve --mode=development"  },
  //...
}

لنضف أيضًا الخاصية devServer إلى كائن التهيئة في الملف "webpack.config.js":

const config = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'main.js',
  },
  devServer: {    
      contentBase: path.resolve(__dirname, 'build'),    
      compress: true,    
      port: 3000,  
  },  // ...
};

سيشغل الأمر خادم التطوير على المنفذ 3000، وبالتالي يمكننا الوصول إلى تطبيقنا على العنوان http://localhost:3000 من خلال المتصفح. وعندما نغيّر في الشيفرة سيُحدّث المتصفح الصفحة تلقائيًا.

تتم عملية تحديث الشيفرة بسرعة. فعند استخدام خادم التطوير، لن تجمع الشيفرة بالطريقة المعتادة ضمن الملف "main.js"، بل ستبقى فقط في الذاكرة.

لنوسع الشيفرة بتغيير تعريف المكوّن App كالتالي:

import React, {useState} from 'react'

const App = () => {
  const [counter, setCounter] = useState(0)

  return (
    <div className="container">
      hello webpack {counter} clicks
      <button onClick={() => setCounter(counter + 1)}>
        press
      </button>
    </div>
  )
}

export default App

تجدر الملاحظة أنّ رسائل الخطأ لن تظهر بنفس الطريقة التي ظهرت بها عند استخدام create-react-app في تطوير التطبيق. لهذا السبب يجب أن ننتبه أكثر لما تعرضه الطرفية:

error_message_changed_07.png

سيعمل التطبيق الآن بشكل جيد وستجري العمليات بسلاسة.

الدلالات المصدرية

لننقل معالج حدث النقر إلى دالة خاصة به، ولنخزّن القيمة السابقة للعداد في حالة مخصصة له تدعى values:

const App = () => {
  const [counter, setCounter] = useState(0)
  const [values, setValues] = useState()
  const handleClick = () => {
    setCounter(counter + 1)
    setValues(values.concat(counter))  }

  return (
    <div className="container">
      hello webpack {counter} clicks
      <button onClick={handleClick}>
        press
      </button>
    </div>
  )
}

لن يعمل التطبيق الآن، وستعرض الطرفية رسالة الخطأ التالية:

onclick_error_message_08.png

نعلم أن الخطأ موجود في التابع onClick، لكن في حال كان التطبيق أضخم، سنجد صعوبة بالغة في تقفي أثر الخطأ:

App.js:27 Uncaught TypeError: Cannot read property 'concat' of undefined
    at handleClick (App.js:27)

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

diffrent_source_code_location_09.png

نحتاج بالطبع إلى رؤية شيفرتنا المصدرية الأساسية عند النقر على رسالة الخطأ. ولحسن الحظ فإصلاح رسالة الخطأ مع ذلك عملية سهلة. سنطلب من webpack أن يولد ما يسمى دلالة مصدرية (source map) للمُجمّع، بحيث يغدو ممكنًا الدلالة على الخطأ الذي يحدث عند التجميع ضمن الشيفرة المصدرية الأساسية.

يمكن توليد الدلالة المصدرية بإضافة الخاصية devtool إلى كائن التهيئة وإسناد القيمة source-map لها:

const config = {
  entry: './src/index.js',
  output: {
    // ...
  },
  devServer: {
    // ...
  },
  devtool: 'source-map',  // ..
};

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

ستصبح رسالة الخطأ الآن أوضح بكثير:

evident_error_message_10.png

طالما أنها تشير إلى الشيفرة التي كتبناها:

map_error_to_source_11.png

يجعل توليد الدلالة المصدرية من استخدام منقح Chrome ممكنًا:

chrome_debugging_possible_12.png

سنصلح الثغرة الآن بتهيئة القيمة الأولية للحالة values لتكون مصفوفة فارغة:

const App = () => {
  const [counter, setCounter] = useState(0)
  const [values, setValues] = useState([])
  // ...
}

تصغير الشيفرة

عندما سننشر التطبيق في مرحلة الإنتاج، سنستخدم الملف المُجمّع "main.js" الذي ولده webpack. إنّ حجم هذا الملف حوالي 974473 بايت على الرغم من أنّ شيفرة تطبيقنا لا تتجاوز عدة أسطر. إنّ هذا الحجم الكبير للملف يعود في الواقع إلى أنّ الملف المُجمَّع سيحتوي الشيفرة المرجعية لكامل مكتبة React. سيؤثر حجم الملف المجمّع لأن المتصفح سيحمّل الملف كاملًا عند تشغيل التطبيق للمرة الأولى. لن تكون هناك مشكلة بالحجم السابق إن كانت سرعة الاتصال بالإنترنت عالية، لكن الاستمرار في إضافة الاعتماديات سيخلق مشكلة في سرعة التحميل وخاصة لدى مستخدمي الهواتف النقالة.

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

تدعى عملية استمثال ملفات JavaScript بالتصغير (minification). وتعتبر الأداة UglifyJS من أفضل الأدوات المعدة لهذا الغرض.

لم تعد إضافات التصغير في webpack ابتداء من الإصدار 4 بحاجة إلى تهيئة إضافية. عليك فقط تعديل سكربت npm في الملف "package.json" لتحدد أنّ webpack سينفذ عملية التجميع في وضع الإنتاج:

{
  "name": "webpack-part7",
  "version": "0.0.1",
  "description": "practising webpack",
  "scripts": {
    "build": "webpack --mode=production",    
    "start": "webpack serve --mode=development"
  },
  "license": "MIT",
  "dependencies": {
    // ...
  },
  "devDependencies": {
    // ...
  }
}

عندما نجمّع التطبيق ثانيةً، سيقل حجم الملف "main.js" بشكل واضح.

$ ls -l build/main.js
-rw-r--r--  1 mluukkai  984178727  132299 Feb 16 11:33 build/main.js

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

function h(){if(!d){var e=u(p);d=!0;for(var t=c.length;t;){for(s=c,c=[];++f<t;)s&&s[f].run();f=-1,t=c.length}s=null,d=!1,function(e){if(o===clearTimeout)return clearTimeout(e);if((o===l||!o)&&clearTimeout)return o=clearTimeout,clearTimeout(e);try{o(e)}catch(t){try{return o.call(null,e)}catch(t){return o.call(this,e)}}}(e)}}a.nextTick=function(e){var t=new Array(arguments.length-1);if(arguments.length>1)

تهيئة نسختي التطوير الإنتاج

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

سنخزّن المحتوى التالي ضمن الملف "db.json":

{
  "notes": [
    {
      "important": true,
      "content": "HTML is easy",
      "id": "5a3b8481bb01f9cb00ccb4a9"
    },
    {
      "important": false,
      "content": "Mongo can save js objects",
      "id": "5a3b920a61e8c8d3f484bdd0"
    }
  ]
}

هدفنا هنا هو تهيئة webpack بحيث يستخدم التطبيق خادم JSON على المنفذ 3001 كواجهة خلفية، إن تم استعمال webpack محليًا.

سيهيّأ الملف المجمّع عندها لاستخدام الواجهة الخلفية الموجودة على العنوان https://blooming-atoll-75500.herokuapp.com/api/notes.

سنثبت المكتبة axios ونشغل خادم JSON ومن ثم سنجري التعديلات اللازمة على التطبيق. تتطلب عملية التغيير إحضار الملاحظات من الواجهة الخلفية باستخدام خطاف مخصص يدعى useNotes:

import React, { useState, useEffect } from 'react'
import axios from 'axios'

const useNotes = (url) => {  
    const [notes, setNotes] = useState([])  useEffect(() => {       
        axios.get(url).then(response => {      
        setNotes(response.data)
     })  
    },[url])  
    return notes}
const App = () => {
  const [counter, setCounter] = useState(0)
  const [values, setValues] = useState([])
  const url = 'https://blooming-atoll-75500.herokuapp.com/api/notes'
  const notes = useNotes(url)
  const handleClick = () => {
    setCounter(counter + 1)
    setValues(values.concat(counter))
  }

  return (
    <div className="container">
      hello webpack {counter} clicks
      <button onClick={handleClick} >press</button>
      <div>{notes.length} notes on server {url}</div>    
</div>
  )
}

export default App

استخدمنا عنوانًا مكتوبًا لخادم الواجهة الخلفية في التطبيق. لكن كيف يمكننا تغيير العنوان بحيث نصبح قادرين على تغييره لكي يدل على خادم الواجهة الخلفية عندما تُجمّع الشيفرة في مرحلة الإنتاج؟

سنغيّر كائن التهيئة في الملف "webpack.config.js" ليصبح دالة بدلًا من كونه تابعًا:

const path = require('path');

const config = (env, argv) => {
  return {
    entry: './src/index.js',
    output: {
      // ...
    },
    devServer: {
      // ...
    },
    devtool: 'source-map',
    module: {
      // ...
    },
    plugins: [
      // ...
    ],
  }
}

module.exports = config

سيبقى التعريف نفسه تقريبًا ماعدا أن كائن التهيئة سيعاد من قبل الدالة. تتلقى الدالة معاملين هما env وargv. يمكن استخدام المعامل الثاني للوصول إلى الخاصية mode المعرفة في سكربت npm.

يمكن استخدام الإضافة DefinePlugin في برنامج webpack لتعريف متغيرات عامة افتراضية يمكن استخدامها ضمن الشيفرة المجمّعة. لنعرف إذًا المتغير العام BACKEND_URL الذي يأخذ قيمًا مختلفة بناء على البيئة التي تجري فيها عملية تجميع الشيفرة:

const path = require('path')
const webpack = require('webpack')
const config = (env, argv) => {
  console.log('argv', argv.mode)

  const backend_url = argv.mode === 'production'    
  ? 'https://blooming-atoll-75500.herokuapp.com/api/notes'   
  : 'http://localhost:3001/api/notes'
  return {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'build'),
      filename: 'main.js'
    },
    devServer: {
      contentBase: path.resolve(__dirname, 'build'),
      compress: true,
      port: 3000,
    },
    devtool: 'source-map',
    module: {
      // ...
    },
    plugins: [      
        new webpack.DefinePlugin({        
            BACKEND_URL: JSON.stringify(backend_url)
        })
    ]
  }
}

module.exports = config

يستخدم المتغير العام ضمن الشيفرة بالطريقة التالية:

const App = () => {
  const [counter, setCounter] = useState(0)
  const [values, setValues] = useState([])
  const notes = useNotes(BACKEND_URL)
  // ...
  return (
    <div className="container">
      hello webpack {counter} clicks
      <button onClick={handleClick} >press</button>
      <div>{notes.length} notes on server {BACKEND_URL}</div>    
    </div>
  )
}

إن كانت هناك اختلافات واسعة في تهيئة نسختي الإنتاج والتطوير. فمن الأفضل فصل تهيئة كل نسخة ضمن ملف خاص بها.

يمكن تحري نسخة الإنتاج المجمّمعة من التطبيق محليًا وذلك بتنفيذ الأمر التالي:

npx static-server

ستكون هذه النسخة متاحة بشكل افتراضي على العنوان http://localhost:9080.

استخدام شيفرة Polyfill

انتهى تطبيقنا الآن وأصبح جاهزًا للعمل مع مختلف المتصفحات الحديثة الموجودة حاليًا ماعدا Internet Explorer. وذلك لأن الوعود التي تستخدمها axios غير مدعومة من قبل IE.

IE_axios_not_supported_13.png

هنالك العديد من الأمور التي لا يدعمها IE. وبعضها مؤذٍ كالتابع find الذي يستخدم للتعامل مع مصفوفات إذ يفوق قدرة هذا المتصفح.

find_method_in_IE_14.png

فلا يكفي في هذه الحالة نقل الشيفرة من نسخة JavaScript حديثة إلى قديمة مدعومة على نطاق أوسع من قبل المتصفحات. يفهم المتصفح IE الوعود قواعديًا، لكنه ببساطة لا يمتلك البنية الوظيفية لتنفيذها. فالخاصية find العائدة للمصفوفات غير معرفة على سبيل المثال.

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

يمكن إضافة تلك الشيفرات بمساعدة webpack and Babel أو بتثبيت إحدى مكتباتها. فشيفرة polyfill التي تقدمها المكتبة promise-polyfill سهلة الاستخدام، وليس علينا سوى إضافة ما يلي إلى شيفرتنا:

import PromisePolyfill from 'promise-polyfill'

if (!window.Promise) {
  window.Promise = PromisePolyfill
}

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

يمكنك إيجاد قائمة بمكتبات polyfill بزيارة الموقع github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills، كما يمكنك الاطلاع على توافق المتصفحات مع الواجهات البرمجية المختلفة من خلال زيارة الموقع https://caniuse.com، أو من خلال زيارة موقع Mozilla.

إلغاء التهيئة الافتراضية (تحرير المشروع)

تستخدم الأداة create-react-app برنامج webpack خلف الستار. فإن لم تجد أنّ التهيئة الافتراضية كافية، يمكنك إلغاءها بعملية تسمى تحرير المشروع (eject)، والتي يتم التخلص فيها من العمليات خلف الكواليس، كما تُخزّن التهيئة الافتراضية في المجلد config وفي ملف package.json معدّل.

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

كما ننصحك بقراءة ملفات التهيئة للمشاريع المحررة بعناية، فهي ذات قيمة تعليمية كبيرة.

ترجمة -وبتصرف- للفصل webpack من سلسلة Deep Dive Into Modern Web Development


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...