التلاؤم

يمكن لمصرّف webpack فهم الوحدات المكتوبة بصيغة ES2015 modules أو CommonJS أو AMD. ومع ذلك، قد تتوقع بعض المكتبات الخارجية اعتماديات عامة (مثل $ لـ jQuery). وقد تنشئ هذه المكتبات أيضًا متغيرات عامة يجب تصديرها. هذه "الوحدات المكسورة" هي إحدى الحالات التي يظهر فيها دور shimming.

هناك حالة أخرى يمكن أن يكون فيها shimming مفيدًا، وهي عندما تريد polyfill لوظائف المتصفح من أجل دعم عدد أكبر من المستخدمين. في هذه الحالة، قد ترغب في إرسال تلك polyfills فقط إلى المتصفحات التي تحتاج إلى ترقيع (أي تحميلها عند الطلب).

ستعرض المقالة التالية هاتين الحالتين.

تلاؤم المتغيرات العامة

لنبدأ بحالة الاستخدام الأولى، وهي shimming للمتغيرات العامة. قبل أن نفعل أي شيء، دعنا نلقي نظرة أخرى على مشروعنا:

project

webpack-demo
 ├── package.json
 ├── package-lock.json
 ├── webpack.config.js
 ├── /dist
 │   └── index.html
 ├── /src
 │   └── index.js
 └── /node_modules

تتذكر حزمة lodash التي كنا نستخدمها؟ لأغراض العرض، لنفترض أننا أردنا بدلًا من ذلك توفيرها كمتغير عام في كامل تطبيقنا. للقيام بذلك، يمكننا استخدام ProvidePlugin.

يجعل ProvidePlugin حزمةً ما متاحة كمتغير في كل وحدة يتم تجميعها عبر webpack. إذا رأى webpack استخدام ذلك المتغير، فسيُضمّن الحزمة المحددة في الحزمة النهائية. دعنا نمضي قدمًا بإزالة تعليمة import الخاصة بـ lodash وتوفيرها بدلًا من ذلك عبر الملحق:

src/index.js

-import _ from 'lodash';
-
 function component() {
   const element = document.createElement('div');

-  // Lodash, now imported by this script
   element.innerHTML = _.join(['Hello', 'webpack'], ' ');

   return element;
 }

 document.body.appendChild(component());

webpack.config.js

 import path from "node:path";
 import { fileURLToPath } from 'url';
+import webpack from "webpack";



const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   output: {
     filename: 'main.js',
     path: path.resolve(__dirname, 'dist'),
   },
+  plugins: [
+    new webpack.ProvidePlugin({
+      _: 'lodash',
+    }),
+  ],
 };

ما فعلناه فعليًا هنا هو إخبار webpack بما يلي...

إذا صادفت أي استخدام للمتغير _، فضمّن حزمة lodash ووفّرها للوحدات التي تحتاج إليها.

إذا شغّلنا البناء، فيجب أن نرى الناتج نفسه:

$ npm run build

..

[webpack-cli] Compilation finished
asset main.js 69.1 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 344 bytes 2 modules
cacheable modules 530 KiB
  ./src/index.js 191 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.x.x compiled successfully in 2910 ms

يمكننا أيضًا استخدام ProvidePlugin لكشف تصدير واحد من وحدة عبر إعداده باستخدام "مسار مصفوفة" (مثل [module, child, ...children?]). لنتخيل أننا نريد فقط توفير دالة join من lodash أينما تم استدعاؤها:

src/index.js

 function component() {
   const element = document.createElement('div');

-  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+  element.innerHTML = join(['Hello', 'webpack'], ' ');

   return element;
 }

 document.body.appendChild(component());

webpack.config.js

 import path from "node:path";
 import webpack from "webpack";
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   output: {
     filename: 'main.js',
     path: path.resolve(__dirname, 'dist'),
   },
   plugins: [
     new webpack.ProvidePlugin({
-      _: 'lodash',
+      join: ['lodash', 'join'],
     }),
   ],
 };

سيتوافق هذا جيدًا مع Tree Shaking، إذ ينبغي إسقاط بقية مكتبة lodash.

Shimming دقيق

تعتمد بعض الوحدات القديمة على أن تكون قيمة this هي كائن window. دعنا نحدّث index.js لدينا بحيث تكون هذه هي الحالة:

 function component() {
   const element = document.createElement('div');

   element.innerHTML = join(['Hello', 'webpack'], ' ');

+  // افترض أننا داخل سياق `window`
+  this.alert("Hmmm, this probably isn't a great idea...");
+
   return element;
 }

 document.body.appendChild(component());

يصبح هذا مشكلة عندما يتم تنفيذ الوحدة في سياق CommonJS حيث تكون قيمة this مساوية لـ module.exports. في هذه الحالة يمكنك تجاوز this باستخدام imports-loader:

webpack.config.js

 import path from "node:path";
 import webpack from "webpack";
 import { fileURLToPath } from 'url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 exports default {
   entry: './src/index.js',
   output: {
     filename: 'main.js',
     path: path.resolve(__dirname, 'dist'),
   },
+  module: {
+    rules: [
+      {
+        test: import.meta.resolve('./src/index.js'),
+        use: 'imports-loader?wrapper=window',
+      },
+    ],
+  },
   plugins: [
     new webpack.ProvidePlugin({
       join: ['lodash', 'join'],
     }),
   ],
 };

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

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

project

  webpack-demo
  ├── package.json
  ├── package-lock.json
  ├── webpack.config.js
  ├── /dist
  ├── /src
  │   ├── index.js
+ │   ├── globals.js
  └── /node_modules

src/globals.js

const file = "blah.txt";
const helpers = {
  test() {
    console.log("test something");
  },
  parse() {
    console.log("parse something");
  },
};

الآن، رغم أنك غالبًا لن تفعل هذا أبدًا في كود المصدر الخاص بك، فقد تصادف مكتبة قديمة تريد استخدامها وتحتوي على كود مشابه لما هو موضح أعلاه. في هذه الحالة، يمكننا استخدام exports-loader لتصدير ذلك المتغير العام كتصدير وحدة عادي. على سبيل المثال، لتصدير file باسم file و helpers.parse باسم parse:

webpack.config.js

 import path from "node:path";
 import webpack from "webpack";
 import { fileURLToPath } from 'url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   output: {
     filename: 'main.js',
     path: path.resolve(__dirname, 'dist'),
   },
   module: {
     rules: [
       {
         test: import.meta.resolve('./src/index.js'),
         use: 'imports-loader?wrapper=window',
       },
+      {
+        test: import.meta.resolve('./src/globals.js'),
+        use:
+          'exports-loader?type=commonjs&exports=file,multiple|helpers.parse|parse',
+      },
     ],
   },
   plugins: [
     new webpack.ProvidePlugin({
       join: ['lodash', 'join'],
     }),
   ],
 };

الآن، من داخل سكربت الإدخال لدينا (أي src/index.js)، يمكننا استخدام const { file, parse } = require('./globals.js'); ويجب أن يعمل كل شيء بسلاسة.

تحميل Polyfills

كل ما ناقشناه تقريبًا حتى الآن كان متعلقًا بالتعامل مع الحزم القديمة. دعنا ننتقل إلى موضوعنا الثاني: polyfills.

توجد طرق كثيرة لتحميل polyfills. على سبيل المثال، لتضمين babel-polyfill قد نقوم بما يلي:

npm install --save babel-polyfill

ونقوم بـ import له لتضمينه في الحزمة الرئيسية:

src/index.js

+import 'babel-polyfill';
+
 function component() {
   const element = document.createElement('div');

   element.innerHTML = join(['Hello', 'webpack'], ' ');

   // افترض أننا داخل سياق `window`
   this.alert("Hmmm, this probably isn't a great idea...");

   return element;
 }

 document.body.appendChild(component());

لاحظ أن هذا النهج يعطي الأولوية للصحة على حساب حجم الحزمة. لكي تكون polyfills/shims آمنة ومتينة، يجب أن تعمل قبل كل الكود الآخر، ولذلك تحتاج إما إلى التحميل بشكل متزامن، أو يجب تحميل كل كود التطبيق بعد اكتمال تحميل جميع polyfills/shims. توجد أيضًا مفاهيم خاطئة كثيرة في المجتمع مفادها أن المتصفحات الحديثة "لا تحتاج" إلى polyfills، أو أن polyfills/shims لا تفعل سوى إضافة ميزات مفقودة؛ في الواقع، كثيرًا ما تقوم بإصلاح تنفيذات معطوبة، حتى في أحدث المتصفحات. لذلك تبقى أفضل ممارسة هي تحميل كل polyfills/shims بلا شروط وبشكل متزامن، رغم تكلفة حجم الحزمة التي يسببها ذلك.

إذا كنت ترى أنك خففت هذه المخاوف وترغب في تحمل خطر التعطل، فإليك طريقة يمكن أن تفعل بها ذلك: دعنا ننقل import إلى ملف جديد ونضيف polyfill whatwg-fetch:

npm install --save whatwg-fetch

src/index.js

-import 'babel-polyfill';
-
 function component() {
   const element = document.createElement('div');

   element.innerHTML = join(['Hello', 'webpack'], ' ');

   // افترض أننا داخل سياق `window`
   this.alert("Hmmm, this probably isn't a great idea...");

   return element;
 }

 document.body.appendChild(component());

project

  webpack-demo
  ├── package.json
  ├── package-lock.json
  ├── webpack.config.js
  ├── /dist
  ├── /src
  │   ├── index.js
  │   ├── globals.js
+ │   └── polyfills.js
  └── /node_modules

src/polyfills.js

import "babel-polyfill";
import "whatwg-fetch";

webpack.config.js

 import path from "node:path";
 import webpack from "webpack";
 import { fileURLToPath } from 'url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
-  entry: './src/index.js',
+  entry: {
+    polyfills: './src/polyfills',
+    index: './src/index.js',
+  },
   output: {
-    filename: 'main.js',
+    filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
   module: {
     rules: [
       {
         test: fileURLToPath(import.meta.resolve('./src/index.js')),
         use: 'imports-loader?wrapper=window',
       },
       {
         test: fileURLToPath(import.meta.resolve('./src/globals.js')),
         use:
           'exports-loader?type=commonjs&exports[]=file&exports[]=multiple|helpers.parse|parse',
       },
     ],
   },
   plugins: [
     new webpack.ProvidePlugin({
       join: ['lodash', 'join'],
     }),
   ],
 };

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

dist/index.html

 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8" />
     <title>Getting Started</title>
+    <script>
+      const modernBrowser = 'fetch' in window && 'assign' in Object;
+
+      if (!modernBrowser) {
+        const scriptElement = document.createElement('script');
+
+        scriptElement.async = false;
+        scriptElement.src = '/polyfills.bundle.js';
+        document.head.appendChild(scriptElement);
+      }
+    </script>
   </head>
   <body>
-    <script src="main.js"></script>
+    <script src="index.bundle.js"></script>
   </body>
 </html>

الآن يمكننا تنفيذ fetch لبعض البيانات داخل سكربت الإدخال:

src/index.js

 function component() {
   const element = document.createElement('div');

   element.innerHTML = join(['Hello', 'webpack'], ' ');

   // افترض أننا داخل سياق `window`
   this.alert("Hmmm, this probably isn't a great idea...");

   return element;
 }

 document.body.appendChild(component());
+
+fetch('https://jsonplaceholder.typicode.com/users')
+  .then((response) => response.json())
+  .then((json) => {
+    console.log(
+      "We retrieved some data! AND we're confident it will work on a variety of browser distributions."
+    );
+    console.log(json);
+  })
+  .catch((error) =>
+    console.error('Something went wrong when fetching this data: ', error)
+  );

إذا شغّلنا البناء، فسيتم إصدار ملف polyfills.bundle.js آخر ويجب أن يستمر كل شيء في العمل بسلاسة في المتصفح. لاحظ أن هذا الإعداد يمكن غالبًا تحسينه، لكنه يجب أن يعطيك فكرة جيدة عن كيفية توفير polyfills فقط للمستخدمين الذين يحتاجون إليها فعلًا.

تحسينات إضافية

تستخدم حزمة babel-preset-env مكتبة browserslist لتصيير ما لا تدعمه مصفوفة المتصفحات لديك فقط. يأتي هذا preset مع خيار useBuiltIns، وقيمته الافتراضية false، والذي يحوّل استيراد babel-polyfill العام لديك إلى نمط import أكثر دقة، ميزةً بميزة:

import "core-js/modules/es7.string.pad-start";
import "core-js/modules/es7.string.pad-end";
import "core-js/modules/web.timers";
import "core-js/modules/web.immediate";
import "core-js/modules/web.dom.iterable";

راجع توثيق babel-preset-env لمزيد من المعلومات.

المدمجات الخاصة بـ Node

يمكن عمل polyfill للمدمجات الخاصة بـ Node، مثل process، مباشرةً من ملف الإعدادات دون استخدام أي loaders أو plugins خاصة. راجع صفحة إعدادات node لمزيد من المعلومات والأمثلة.

أدوات أخرى

توجد بعض الأدوات الأخرى التي يمكن أن تساعد عند التعامل مع الوحدات القديمة.

عندما لا توجد نسخة AMD/CommonJS من الوحدة وتريد تضمين dist، يمكنك تعليم هذه الوحدة في noParse. سيؤدي ذلك إلى جعل webpack يضمّن الوحدة دون تحليلها أو حل تعليمات import و require(). تُستخدم هذه الممارسة أيضًا لتحسين أداء البناء.

أخيرًا، توجد بعض الوحدات التي تدعم عدة أنماط للوحدات، مثل مزيج من AMD و CommonJS والنمط القديم. في معظم هذه الحالات، تتحقق أولًا من define ثم تستخدم كودًا غريبًا بعض الشيء لتصدير الخصائص. في هذه الحالات، قد يساعد إجبار مسار CommonJS عبر تعيين additionalCode=var%20define%20=%20false; بواسطة imports-loader.

Edit this page·

1 Contributor

arabpolice