• בלוג
  • החלפת מודולים HMR

החלפת מודולים HMR

04/10/2019

אחת הפונקציות הכי אהובות ב Webpack במצב פיתוח נקראת HMR או בשמה המלא Hot Module Replacement. הרעיון הוא החלפת רכיבים במערכת תוך כדי שהמערכת פועלת ובלי לטעון מחדש את העמוד.

1. למה זה טוב

שרת הפיתוח של Webpack באופן אוטומטי בונה מחדש את הקבצים שלנו וטוען מחדש את העמוד בדפדפן אחרי כל שינוי. אבל מה קורה באפליקציות Single Page Applications, שם כל טעינה מחדש של הדף מחזירה אותנו למצב אחר או למסך פנימי אחר באפליקציה? במצב כזה יהיה קשה מאוד לפתח כי כל שינוי בקוד יחייב אותנו לבצע שוב את אותן פעולות שהביאו אותנו לאותו מסך פנימי עם בעיה.

פיצ'ר HMR מאפשר החלפה של מודולים תוך כדי שהאפליקציה רצה ובלי לטעון מחדש את העמוד. הפיצ'ר דורש טיפול מיוחד מצד היישום כדי לפעול ולכן אי אפשר להפעיל אותו על כל קוד JavaScript, אבל כשיש לכם פלאגין מתאים לפריימוורק אתם מקבלים חווית פיתוח משודרגת בצורה משמעותית.

בואו נראה שתי דוגמאות לשימוש ב HMR, הראשונה בפרויקט הכולל CSS והשניה בפרויקט React.

2. איך להפעיל HMR עבור CSS

ראינו באחד הפרקים הקודמים איך לבנות פרויקט Webpack שכולל גם קבצי CSS באמצעות css-loader. הקוד שכתבנו לקח קוד מקור מתיקיית src שם היה קובץ main.js וקובץ main.css ובנה מתוכם קבצי css ו js מתאימים בתיקיית dist. קובץ ההגדרות נראה כך:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');


module.exports = {
  entry: './src/main.js',
  output: {
    filename: 'app.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          { loader: 'css-loader', options: {} },
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css',
    }),
    new HtmlWebpackPlugin(),
  ],
};

עיקר העבודה של HMR מתרחשת בתוך הפלאגין MiniCssExtractPlugin - פלאגין זה יודע לזהות שיש שינוי בקבצי ה CSS ולטעון מחדש את הגדרות העיצוב ישירות לתוך הדף. בשביל להפעיל את ההתנהגות הזאת נעביר באוביקט ההגדרות שלו את האפשרות hmr באופן הבא:

          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              hmr: process.env.NODE_ENV === 'development',
            },
          },

ובנוסף נבקש משרת הפיתוח של וובפאק לעבוד במצב HMR:

  devServer: {
    hot: true
  }

סך הכל קובץ ההגדרות המלא נראה עכשיו כך:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');


module.exports = {
  entry: './src/main.js',
  output: {
    filename: 'app.js',
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    hot: true
  },  
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              hmr: true,
            },
          },
          { loader: 'css-loader', options: {} },
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css',
    }),
    new HtmlWebpackPlugin(),
  ],
};

ועץ התיקיות בפרויקט שלי נראה כך:

.
├── package-lock.json
├── package.json
├── src
│   ├── lib.css
│   ├── main.css
│   └── main.js
└── webpack.config.js

מוכנים לצאת לדרך? נכנס לקובץ src/main.js ונכתוב שם את הקוד הבא:

import $ from 'jquery';
import './main.css';

$(document.body).append('<button>0</button>');
$('button').on('click', function() {
  this.textContent ++;
});

נפעיל את השרת עם:

$ npx webpack-dev-server

ואפשר לפתוח את הדף בדפדפן ולראות כפתור, כאשר כל לחיצה על הכפתור מעלה את המספר שכתוב בו ב-1.

החלק המעניין יקרה כשנעדכן את קובץ ה CSS. באופן רגיל שינוי ב CSS היה מביא לטעינה מחדש של הדף ולכן המספר על הכפתור היה מתאפס. בזכות השימוש ב HMR אתם יכולים לשנות את ה CSS ולשמור, ותראו את העיצוב החדש בלי שהמספר על הכפתור ישתנה.

3. איך להפעיל HMR ב React

מנגנון HMR הוא להיט אמיתי כשמדובר בפיתוח באמצעות פריימוורק כמו ריאקט, שם כל מבנה העמוד נקבע ב JavaScript. לריאקט עצמה יש תמיכה מובנית ב HMR מה שאומר שכל פעם שוובפאק מזהה שינוי בקומפוננטות ריאקט, באופן אוטומטי הוא יטען את הקבצים החדשים ובאופן אוטומטי ריאקט תחליף את הקומפוננטות הישנות בחדשות תוך שמירת ה state של כל הקומפוננטות.

במילים אחרות זה אומר שאנחנו נראה על המסך את הקומפוננטה החדשה בלי טעינה מחדש של הדף ומאותו מצב בדיוק בו היינו. בואו נגדיר את זה על פרויקט כדי לקבל את אפקט ה-וואו.

בתיקיה חדשה נתקין תחילה את כל המודולים:

$ npm init -y
$ npm install --save-dev webpack webpack-cli webpack-dev-server react react-dom webpack-html-plugin  babel-loader @babel/core @babel/preset-react html-webpack-plugin react-hot-loader @hot-loader/react-dom

ניצור תיקיית src ובתוכה שני קבצים: הקובץ src/main.js יכיל את נקודת הכניסה למערכת והקובץ src/hello.js יכיל קומפוננטה אותה נשנה ונטען מחדש. תוכן הקובץ src/hello.js יהיה:

import { hot } from 'react-hot-loader/root';
import React, { useState } from 'react';

function App(props) {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>four</p>
      <button onClick={() => setCount(count+1)}>{count}</button>
    </div>
  )
}

export default hot(App);

והנה המקום הראשון שאנו צריכים לעדכן בשביל לקבל HMR והוא הקומפוננטה הראשית ביישום. במקום לייצר את App ישירות אנו עוטפים אותו בפונקציה hot של react-hot-loader. חשוב לשים לב שאת העטיפה הזאת צריך לשים רק על הקומפוננטה הראשית ביישום, וששורת ה import שטוענת את react-hot-loader חייבת להופיע לפני שטוענים את ריאקט עצמה.

נמשיך לנקודת הכניסה הקובץ src/main.js עם התוכן הבא:

import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import HelloWorld from './hello'

const root = document.createElement('div');
document.body.appendChild(root);

ReactDOM.render(<HelloWorld />, root);

הקובץ יוצר אלמנט DOM ומרנדר את היישום שלנו אליו. אין צורך בשום שינוי בהשוואה ליישום ריאקט קלאסי כדי לקבל את התמיכה ב HMR.

תחנה אחרונה היא קובץ ההגדרות webpack.config.js:

  1. נשתמש כאן ב Babel Loader כדי לטעון את קבצי ה JSX. בשביל שנוכל גם להחליף קומפוננטות בלי להוריד את היישום צריך להוסיף פלאגין של בייבל שנקרא react-hot-loader/babel. טעינת הפלאגין תבוצע מתוך אוביקט ההגדרות module.rules[0].use.options.

  2. המודול react-hot-loader כולל גם קוד שמחליף את react-dom. אני משתמש ב resolve.alias כדי להחליף את המודולים. זה טוב כי ככה במעבר ל Production אפשר לוותר על ההחלפה בלי לשנות כלל את קוד היישום.

  3. אנחנו צריכים להעביר hot: true לאוביקט ההגדרות של webpack-dev-server.

סך הכל קובץ ההגדרות המלא נראה כך:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: 'app.js',
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    hot: true,
  },
  resolve: {
    alias: {
      'react-dom': '@hot-loader/react-dom',
    },
  },
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react'],
            plugins: ["react-hot-loader/babel"]
          },
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin(),
  ],
};

ועכשיו אנו מוכנים ל-וואו. הפעילו את webpack-dev-server עם:

$ npx webpack-dev-server

כנסו עם הדפדפן ליישום שלכם ולחצו על הכפתור כדי להעלות את המספר. אחרי שהגעתם למספר טוב חזרו לקוד ועדכנו את קוד הפקד בקובץ src/hello.js. מיד אחרי השמירה תוכלו לראות שהפקד התעדכן על המסך, אבל ה State נשאר אותו דבר כך שהמספר בכפתור לא ישתנה.

חווית הפיתוח עם HMR דורשת קצת עבודה בהגדרה ונראית בהתחלה כמו קסם אבל האמת שאין פה קסמים יותר מדי גדולים: וובפאק מזהה את השינוי בקבצים, מדווח לקוד JavaScript שמותקן דרך המודול react-hot-loader, אותו קוד JavaScript בתגובה מחליף את הקומפוננטה ומרנדר את הקומפוננטה החדשה. הקסם היחיד הוא שמירת ה State בין הטעינות ואם אתם סקרנים לגבי המימוש תצטרכו להיכנס לקוד של react-hot-loader עצמו.