الاستبدال الساخن للوحدات

يُعد الاستبدال الساخن للوحدات (Hot Module Replacement أو HMR) أحد أكثر الميزات فائدة التي يقدمها webpack. فهو يسمح بتحديث جميع أنواع الوحدات أثناء وقت التشغيل دون الحاجة إلى تحديث كامل للصفحة. تركز هذه الصفحة على التنفيذ، بينما تقدم صفحة المفاهيم تفاصيل أكثر حول طريقة عمله وسبب فائدته.

تفعيل HMR

هذه الميزة رائعة للإنتاجية. كل ما نحتاج إليه هو تحديث إعدادات webpack-dev-server، واستخدام ملحق HMR المدمج في webpack. سنزيل أيضًا نقطة الإدخال الخاصة بـ print.js لأنها ستُستهلك الآن من خلال وحدة index.js.

ابتداءً من webpack-dev-server v4.0.0، يكون Hot Module Replacement مفعّلًا افتراضيًا.

webpack.config.js

  import path from 'node:path';
  import { fileURLToPath } from 'node:url';
  import HtmlWebpackPlugin from 'html-webpack-plugin';

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

  export default {
    entry: {
       app: './src/index.js',
-      print: './src/print.js',
    },
    devtool: 'inline-source-map',
    devServer: {
      static: './dist',
+     hot: true,
    },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'Hot Module Replacement',
      }),
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
  };

يمكنك أيضًا توفير نقاط إدخال يدوية لـ HMR:

webpack.config.js

  import path from 'node:path';
  import { fileURLToPath } from 'node:url';
  import HtmlWebpackPlugin from 'html-webpack-plugin';
+ import webpack from 'webpack';

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

  export default {
    entry: {
       app: './src/index.js',
-      print: './src/print.js',
+      // كود وقت التشغيل للاستبدال الساخن للوحدات
+      hot: 'webpack/hot/dev-server.js',
+      // عميل خادم التطوير لنقل web socket ومنطق hot و live reload
+      client: 'webpack-dev-server/client/index.js?hot=true&live-reload=true',
    },
    devtool: 'inline-source-map',
    devServer: {
      static: './dist',
+     // عميل خادم التطوير لنقل web socket ومنطق hot و live reload
+     hot: false,
+     client: false,
    },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'Hot Module Replacement',
      }),
+     // ملحق الاستبدال الساخن للوحدات
+     new webpack.HotModuleReplacementPlugin(),
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
  };

الآن دعنا نحدّث ملف index.js بحيث عندما يتم اكتشاف تغيير داخل print.js نخبر webpack بقبول الوحدة المحدّثة.

index.js

  import _ from 'lodash';
  import printMe from './print.js';

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

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

    btn.innerHTML = 'Click me and check the console!';
    btn.onclick = printMe;

    element.appendChild(btn);

    return element;
  }

  document.body.appendChild(component());
+
+ if (module.hot) {
+   module.hot.accept('./print.js', function() {
+     console.log('Accepting the updated printMe module!');
+     printMe();
+   })
+ }

ابدأ بتغيير تعليمة console.log في print.js، ويجب أن ترى الناتج التالي في وحدة تحكم المتصفح (لا تقلق الآن بشأن ناتج button.onclick = printMe، فسنحدّث هذا الجزء لاحقًا أيضًا).

print.js

  export default function printMe() {
-   console.log('I get called from print.js!');
+   console.log('Updating print.js...');
  }

console

[HMR] Waiting for update signal from WDS...
main.js:4395 [WDS] Hot Module Replacement enabled.
+ 2main.js:4395 [WDS] App updated. Recompiling...
+ main.js:4395 [WDS] App hot update...
+ main.js:4330 [HMR] Checking for updates on the server...
+ main.js:10024 Accepting the updated printMe module!
+ 0.4b8ee77….hot-update.js:10 Updating print.js...
+ main.js:4330 [HMR] Updated modules:
+ main.js:4330 [HMR]  - 20

عبر واجهة Node.js API

عند استخدام Webpack Dev Server مع واجهة Node.js API، لا تضع خيارات خادم التطوير على كائن إعدادات webpack. بدلًا من ذلك، مررها كمعامل ثانٍ عند الإنشاء. على سبيل المثال:

new WebpackDevServer(options, compiler)

لتفعيل HMR، تحتاج أيضًا إلى تعديل كائن إعدادات webpack ليتضمن نقاط إدخال HMR. إليك مثالًا صغيرًا على الشكل المحتمل لذلك:

dev-server.js

import path from "node:path";
import { fileURLToPath } from "node:url";
import HtmlWebpackPlugin from "html-webpack-plugin";
import webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";

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

const config = {
  mode: "development",
  entry: [
    // كود وقت التشغيل للاستبدال الساخن للوحدات
    "webpack/hot/dev-server.js",
    // عميل خادم التطوير لنقل web socket ومنطق hot و live reload
    "webpack-dev-server/client/index.js?hot=true&live-reload=true",
    // نقطة الإدخال الخاصة بك
    "./src/index.js",
  ],
  devtool: "inline-source-map",
  plugins: [
    // ملحق الاستبدال الساخن للوحدات
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      title: "Hot Module Replacement",
    }),
  ],
  output: {
    filename: "[name].bundle.js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },
};
const compiler = webpack(config);

// تم تعطيل خياري `hot` و `client` لأننا أضفناهما يدويًا
const server = new WebpackDevServer({ hot: false, client: false }, compiler);

try {
  await server.start();
  console.log("dev server is running");
} catch (err) {
  throw new Error(`Failed to start dev server: ${err.message}`, { cause: err });
}

راجع التوثيق الكامل لواجهة Node.js API الخاصة بـ webpack-dev-server.

ملاحظات مهمة

قد يكون Hot Module Replacement مربكًا أحيانًا. لتوضيح ذلك، دعنا نعود إلى مثالنا العامل. إذا نقرت الزر في صفحة المثال، ستلاحظ أن وحدة التحكم تطبع دالة printMe القديمة.

يحدث ذلك لأن معالج حدث onclick الخاص بالزر لا يزال مرتبطًا بدالة printMe الأصلية.

لجعل هذا يعمل مع HMR، نحتاج إلى تحديث ذلك الربط إلى دالة printMe الجديدة باستخدام module.hot.accept:

index.js

  import _ from 'lodash';
  import printMe from './print.js';

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

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

    btn.innerHTML = 'Click me and check the console!';
    btn.onclick = printMe;  // حدث onclick مرتبط بدالة printMe الأصلية

    element.appendChild(btn);

    return element;
  }

- document.body.appendChild(component());
+ let element = component(); // خزّن العنصر لإعادة تصييره عند تغييرات print.js
+ document.body.appendChild(element);

  if (module.hot) {
    module.hot.accept('./print.js', function() {
      console.log('Accepting the updated printMe module!');
-     printMe();
+     document.body.removeChild(element);
+     element = component(); // أعد تصيير "component" لتحديث معالج النقر
+     document.body.appendChild(element);
    })
  }

هذا مثال واحد فقط، لكن توجد حالات أخرى كثيرة يمكن أن توقع المطورين بسهولة. لحسن الحظ، توجد العديد من loaders (بعضها مذكور أدناه) التي تجعل الاستبدال الساخن للوحدات أسهل بكثير.

HMR مع ملفات التنسيق

يُعد Hot Module Replacement مع CSS مباشرًا إلى حد كبير بمساعدة style-loader. يستخدم هذا loader module.hot.accept خلف الكواليس لتعديل وسوم <style> عند تحديث اعتماديات CSS.

أولًا، لنثبّت كلا الـ loaders بالأمر التالي:

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

الآن دعنا نحدّث ملف الإعدادات لاستخدام الـ loader.

webpack.config.js

  import path from 'node:path';
  import { fileURLToPath } from 'node:url';
  import HtmlWebpackPlugin from 'html-webpack-plugin';

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

  export default {
    entry: {
      app: './src/index.js',
    },
    devtool: 'inline-source-map',
    devServer: {
      static: './dist',
      hot: true,
    },
+   module: {
+     rules: [
+       {
+         test: /\.css$/,
+         use: ['style-loader', 'css-loader'],
+       },
+     ],
+   },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'Hot Module Replacement',
      }),
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
  };

يمكن تنفيذ التحميل الساخن لملفات التنسيق عبر استيرادها داخل وحدة:

project

  webpack-demo
   ├── package.json
   ├── webpack.config.js
   ├── /dist
   │   └── bundle.js
   └── /src
       ├── index.js
       ├── print.js
 +     └── styles.css

styles.css

body {
  background: blue;
}

index.js

  import _ from 'lodash';
  import printMe from './print.js';
+ import './styles.css';

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

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

    btn.innerHTML = 'Click me and check the console!';
    btn.onclick = printMe;  // حدث onclick مرتبط بدالة printMe الأصلية

    element.appendChild(btn);

    return element;
  }

  let element = component();
  document.body.appendChild(element);

  if (module.hot) {
    module.hot.accept('./print.js', function() {
      console.log('Accepting the updated printMe module!');
      document.body.removeChild(element);
      element = component(); // أعد تصيير "component" لتحديث معالج النقر
      document.body.appendChild(element);
    })
  }

غيّر التنسيق على body إلى background: red; ويجب أن ترى لون خلفية الصفحة يتغير فورًا دون تحديث كامل.

styles.css

  body {
-   background: blue;
+   background: red;
  }

كود وأطر عمل أخرى

توجد العديد من loaders والأمثلة الأخرى في المجتمع لجعل HMR يتفاعل بسلاسة مع أطر عمل ومكتبات متنوعة...

  • React Hot Loader: تعديل مكونات React في الوقت الحقيقي.
  • Vue Loader: يدعم هذا loader ميزة HMR لمكونات Vue مباشرةً.
  • Elm Hot webpack Loader: يدعم HMR للغة البرمجة Elm.
  • Angular HMR: لا حاجة إلى loader! دعم HMR مدمج في Angular CLI، أضف العلم --hmr إلى أمر ng serve لديك.
  • Svelte Loader: يدعم هذا loader ميزة HMR لمكونات Svelte مباشرةً.
Edit this page·

1 Contributor

arabpolice