טיפול בקישורים בקבצי ה CSS

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

1. תמונות ו CSS

זה די נוח ש Webpack מייצר תיקיה בשם dist ושם בה את כל קבצי ה HTML, JS ו CSS שאנחנו צריכים לפרויקט, אבל זה לא מספיק. פרויקט ווב צריך הרבה פעמים גם קבצים נוספים למשל תמונות, סרטי וידאו, פונטים ואולי גם קבצי PDF או קבצים סטטיים אחרים. באמצעות loader שנקרא file-loader אנחנו יכולים להוסיף לתיקיית הפלט כל סוג קובץ אחר שנרצה, ובתנאי שמישהו יטען את הקובץ מאיזשהו מקום ביישום.

נתחיל עם קבצי תמונות ו CSS. ב CSS אפשר לשלב תמונות באמצעות פקודת background-image ולהעביר לה url של תמונה. בואו נניח שיש לי בתיקיית src את הקובץ main.css עם התוכן הבא:

body {
  background: url('../img/puppy1.jpg');
}

ובהתאמה יש לי בתיקיית img את הקובץ puppy1.jpg, שם יש תמונה של כלבלב חמוד. אם נשאיר את הפקודה כמו שהיא בקובץ ה CSS נהיה בבעיה - הנתיב ../img/puppy1.jpg לא בטוח יצביע לתמונה הנכונה על השרת. במיוחד זה נכון אם אנחנו מתכננים תהליך אוטומטי שייקח את תיקיית dist ש Webpack מייצר ויעביר אותה לשרת כמו שהיא. התמונה כלל לא תופיע בתיקיית dist.

וכאן נכנס לפעולה file-loader. נתקין אותו עם:

$ npm install --save-dev file-loader

ונגדיר ל Webpack שישתמש ב file-loader כדי לטעון תמונות. ההגדרה הרלוונטית נראית כך:

      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              outputPath: 'images',
            }
          },
        ],
      },

וקובץ ההגדרות כולו (בשביל קופי-פייסט) נראה כך:

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: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              hmr: process.env.NODE_ENV === 'development',
            },
          },
          { loader: 'css-loader', options: {} },
        ],
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              outputPath: 'images',
            }
          },
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css',
    }),
    new HtmlWebpackPlugin(),
    new webpack.ProvidePlugin({
      'window.jQuery': 'jquery',
    })
  ],
};

האופציה outputPath לא הכרחית ומציינת לאן להעתיק בתוך תיקיית dist את התמונות. אני בחרתי images כך שאחרי הפעלת webpack אקבל תיקיית dist עם המבנה הבא:

$ npm run build
$ tree dist
dist
├── app.js
├── images
│   └── b96d6c6f4296f559ec55289fa67143c4.jpg
├── index.html
└── main.css

נוצרה תיקיה חדשה בשם images ובתוכה קובץ חדש עם שם שהוא hash של התמונה. כך נוכל להיעזר ב Browser Cache כדי לשמור את התמונה במצב ייצור, ואם התמונה תשתנה באופן אוטומטי שם הקובץ גם יהיה שונה.

אגב הקובץ main.css בתיקיית dist עודכן כדי לכלול את הנתיב החדש לתמונה והחלק הרלוונטי ממנו נראה עכשיו כך:

body {
  background: url(images/b96d6c6f4296f559ec55289fa67143c4.jpg);
}

בצורה כזאת Webpack יכול לנהל בשבילנו את התמונות שאנחנו טוענים מקבצי ה CSS ואנחנו מקבלים:

  1. כל התמונות שמשתמשים בהן עוברות לתיקיית dist ומשם יועתקו לשרת. תמונות ישנות שכבר לא משתמשים בהן יותר לא ייכנסו.

  2. שם קובץ ייחודי לכל תמונה כדי שנוכל לנצל את ה Browser Cache.

  3. וידוא שכל התמונות נמצאות במקום ואין שגיאת כתיב בציון קובץ התמונה כבר בעת בניית ה Bundle.

2. תמונות ו JavaScript

מרגע שהחלטנו שהתמונות ייטענו דרך Webpack אנחנו יכולים להשתמש בנתיבים לתמונות גם מתוך JavaScript ולקבל את אותו פיענוח נתיבים שהיה לנו ב CSS. נתבונן רגע בקוד JavaScript לדוגמא הבא:

import $ from 'jquery';
import puppyUrl from '../img/puppy2.jpg';

$('body').append(`<img src="${puppyUrl}" />`);

הקוד הזה יוצר אלמנט תמונה על העמוד. ה URL לתמונה נלקח אוטומטית מתוך Webpack באמצעות אותו file-loader עליו דיברנו. בעצם התהליך יהיה:

  1. וובפאק מזהה שצריך לטעון קובץ תמונה כשמגיע לטעון את קובץ ה JS.

  2. טעינת התמונה גורמת להעתקת התמונה לתיקיית dist והחזרת הנתיב לשם הקובץ לתוך המשתנה puppyUrl.

  3. המשך הקוד יכול להשתמש ב puppyUrl כדי ליצור אלמנט תמונה על העמוד שמתיחס לתמונה שטענו.

3. פונטים ו CSS

כמו שטענו קבצי תמונות אפשר להשתמש ב file-loader כדי לטעון כל סוג קובץ: קבצי PDF, קבצי וידאו, פונטים או כל קובץ סטטי אחר. אם נוסיף לדוגמא את הכלל הבא להגדרות ה Webpack שלנו:

{
  test: /\.(woff|woff2|eot|ttf|otf)$/,
  use: [
    'file-loader'
  ]
}

נגרום ל Webpack להעתיק את קבצי הפונטים לתיקיית הפלט בדיוק כמו שהוא העתיק את קבצי התמונות. עכשיו אפשר יהיה לכתוב ב CSS את הקוד הבא:

@font-face {
  font-family: 'MyFont';
  src:  url('./my-font.woff2') format('woff2'),
        url('./my-font.woff') format('woff');
  font-weight: 600;
  font-style: normal;
}

.hello {
  color: red;
  font-family: 'MyFont';
  background: url('./icon.png');
}

ובאופן אוטומטי הנתיבים לקבצי הפונטים יוחלפו בנתיבים האמיתיים והפונט הנכון ייטען.