手把手带你学webpack(3)-- Webpack中加载图片及其他资源

810 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

本篇文章源码已传到 github: github.com/Plasticine-…

前两章我们讲了加载css资源相关的内容,这篇文章我们来探讨一下在webpack中加载图片、字体等资源,也就是说我们需要在js中直接import图片等资源并在html中使用它们

1. 加载图片案例场景

以加载图片为例,先编写入口js文件

// src/index.js
import './css/index.css';

// ESModule 或者 CommonJS 方式将图片作为模块使用
import img1 from './img/img1.jpg';
// const img1 = require('./img/img.jpg').default;

(() => {
  const init = () => {
    const el = document.createElement('div');

    // 1. 使用图片 -- 通过 img 标签的 src 属性
    const fooImg = new Image();
    fooImg.src = img1;
    fooImg.style.width = '300px';
    fooImg.style.height = '300px';
    fooImg.style.objectFit = 'cover';

    // 2. 使用图片 -- 通过 css 设置 background-image 属性
    const barEl = document.createElement('div');
    barEl.style.width = '300px';
    barEl.style.height = '300px';
    barEl.className = 'bg-img';

    el.appendChild(fooImg);
    el.appendChild(barEl);

    document.body.appendChild(el);
  };

  init();
})();
/* src/css/index.css */
.bg-img {
  background-image: url('../img/img2.jpg');
  background-size: cover;
}

这里使用了两种方式使用图片:

  1. img标签的src属性
  2. css中设置background-image属性,通过url()函数引用图片

第一种方式中可以使用import xxx from 'img/xxx.jpg'这样的形式或者require('img/xxx.jpg')这种形式,也就是ES ModuleCommon JS都支持,但是要注意:

  • file-loader版本如果是4.x的话,require()加载图片资源直接使用即可
  • file-loader版本在5.0以上,require()加载的图片资源要使用default属性,即require('img/xxx.jpg').default

直接打包会遇到如下错误 image.png 这是因为入口文件index.js中加载了img.jpg文件,因此img.jpg会加入到webpack的依赖图中,当加载.jpg文件的时候,会到配置文件中寻找配置的相关loader去处理它,但是由于并没有配置loader去处理jpg文件,因而会报错


2. 使用 file-loader 处理图片资源

这里我们需要一个叫file-loaderloader去处理图片文件

pnpm i file-loader -D

file-loader的作用就是帮助我们处理import/require引入的文件资源,并将这些资源放到输出的dist目录中,如果不配置输出目录则默认是直接放到dist目录下,这个是可以配置的,比如想要配置输出到dist/img目录也是可以的

先简单用一下吧,直接将file-loader写进webpack.config.js

/**
 * @type { import('webpack').Configuration }
 */
module.exports = {
  module: {
    rules: [
      {
        test: /\.css/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.(jpe?g|png|gif)/,
        use: ['file-loader'],
      },
    ],
  },
};

打包后的结果 image.png 可以看到,被引用的图片会被直接复制到dist目录下,并且用一串hash值作为文件名


2.1 使用 file-loader 的 placeholder 配置文件名

打包出来的图片文件名是可以配置的,使用file-loader提供的placeholder占位符来进行配置

v4.webpack.js.org/loaders/fil…

根据官方文档的说明,我们做出如下配置

/**
 * @type { import('webpack').Configuration }
 */
module.exports = {
  module: {
    rules: [
      {
        test: /\.css/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.(jpe?g|png|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name]-[sha256:hash:8]-[emoji].[ext]',
            },
          },
        ],
      },
    ],
  },
};

接下来打包看看生成的文件名:img-3a8d7299-🏗️.jpg,生成了这样一个文件名,和我们配置的规则符合


2.2 配置文件存放路径

根据file-loader的官方文档,我们还可以使用outputPath配置存放路径

/**
 * @type { import('webpack').Configuration }
 */
module.exports = {
  module: {
    rules: [
      {
        test: /\.css/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.(jpe?g|png|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name]-[sha256:hash:8]-[emoji].[ext]',
              outputPath: 'img',
            },
          },
        ],
      },
    ],
  },
};

注意:每次重新打包都需要将dist目录删除,否则旧的打包文件仍然会保留而不是被替代,只有文件名相同时才会替代,这点之后会通过一个**plugin**解决


2.3 file-loader 在 webpack5 中的问题

心细的读者可能发现了,file-loader的那个文档还是webpack4时候的,事实上到了webpack5开始,官方已经不推荐使用file-loader了,而是使用Asset Modules进行替代

If you have rules defined for loading assets using raw-loader, url-loader, or file-loader, please use Asset Modules instead as they're going to be deprecated in near future.

所以一步步跟着做到现在时会发现,打包的结果当中会发现打包结果当中,对于直接在js中导入的图片资源是没有啥问题的,但是css文件中通过url导入的图片是无法正常显示的,并且打包结果中多出了一个多余的图片文件 image.png 并且该图片文件的内容根本不是图片二进制数据,而是一串文本 image.png 如果使用webpack4则上述配置不需要进行任何更改即可正常工作,但是如果实在是想在webpack5中使用的话,就需要对配置作出如下修改,或者使用Asset Modules,这个后面会讲 image.png 这是webpack5官方文档中说的,要想在webpack5中使用file-loader,就需要将type配置为javascript/auto

/**
 * @type { import('webpack').Configuration }
 */
module.exports = {
  module: {
    rules: [
      {
        test: /\.css/,
        use: ['style-loader', 'css-loader'],
      },
      // 使用 file-loader
      {
        test: /\.(jpe?g|png|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name]-[sha256:hash:8]-[emoji].[ext]',
              outputPath: 'img',
              esModule: false,
            },
          },
        ],
        type: 'javascript/auto',
      },
    ],
  },
};

还做出的修改是file-loaderoptions中添加一项esModule: false

这样修改的原因是css-loader会将url()的资源解析为url(require('xxx')),在遇到图片文件时,触发了配置好的.jpg规则,使用file-loader进行解析

file-loader在解析的时候默认使用ESModule的方式解析的,使用ESModule的方式去解析require那肯定是无效的,所以需要先将esModule置为false,让其使用CommonJS的方式去解析模块

现在重新打包就可以看到效果啦 image.png 可以看到两种方式加载的图片资源都成功被打包


3. 使用 url-loader 处理图片资源

前面我们使用file-loaderwebpack能够处理在js中直接导入图片的行为,以及在css中使用url()加载的图片

现在我们思考一个问题,如果项目当中有很多图片都需要打包的话,全部都直接复制到打包结果中,那会导致打包结果十分巨大,大部分都是图片资源,那么有没有一种能够将图片打包成base64编码的方式,而不是直接复制图片呢?

当然也不能够将任何图片都直接用base64编码处理,我们希望最理想的结果是在某一个指定阈值内,使用base64编码图片,而超出该阈值的时候,就还是将其打包成图片文件的方式

3.1 配置 url-loader

这时候就要用到url-loader了,首先安装url-loader

pnpm i url-loader -D

然后进行配置,根据url-loader的官方文档(当然,仍然是webpack4下的,因为webpack5中已经弃用)它具有如下配置项

v4.webpack.js.org/loaders/url…

NameTypeDefaultDescription
limit{Boolean\|Number\|String}trueSpecifying the maximum size of a file in bytes.
mimetype{Boolean\|String}based from mime-typesSets the MIME type for the file to be transformed.
encoding{Boolean\|String}base64Specify the encoding which the file will be inlined with.
generator{Function}() => type/subtype;encoding,base64_dataYou can create you own custom implementation for encoding data.
fallback{String}file-loaderSpecifies an alternative loader to use when a target file's size exceeds the limit.
esModule{Boolean}trueUse ES modules

我们可以使用limit配置项去配置前面说到的阈值,我使用的两张图片一张是2.94MB一张是4.13MB的,那就将阈值设置为3MB吧,这样的话最终打包结果应当是2.94MB的图片使用base64编码处理,而4.13MB的图片以文件的方式被打包

// 使用 url-loader
{
  test: /\.(jpe?g|png|gif)$/,
  use: [
    {
      loader: 'url-loader',
      options: {
        limit: 3 * 1024 * 1024, // 3MB
      },
    },
  ],
  type: 'javascript/auto',
},

我们还希望大于3MB的图片被打包成文件,但是配置项中貌似并没有看到能让我们配置文件打包相关的配置项,其实在超出limit时,默认会使用file-loader

If the file size is equal or greater than the limit file-loader will be used (by default) and all query parameters are passed to it. Using an alternative to file-loader is enabled via the fallback option.

根据官方文档的说明,当解析超出limit限制的文件时,我们在options中配置的配置项会被传递到file-loader中,当然这是默认会交给file-loader处理,我们也可以在fallback配置项中指定使用别的loader

既然如此,那我们直接在options中填写file-loader的配置项即可

{
  test: /\.(jpe?g|png|gif)$/,
  use: [
    {
      loader: 'url-loader',
      options: {
        limit: 3 * 1024 * 1024, // 3MB
        // 超出 limit 阈值时默认会使用 file-loader 处理 并且配置项会被传递给 file-loader
        name: '[name]-[sha256:hash:8]-[emoji].[ext]',
        outputPath: 'img',
        esModule: false,
      },
    },
  ],
  type: 'javascript/auto',
},

当然,和前面配置file-loader一样,因为file-loaderurl-loader都是已经在webpack5中被废弃了,因此我们还需要添加上type: 'javascript/auto'options.esModule: false

虽然是麻烦了点,但是由于讲解的是loader的使用,因此还是要这样做,如果嫌麻烦也可以安装webpack4,不过后面会讲解webpack5官方推荐的方式,不会再弄这么多不必要的配置了!

3.2 不要盲目使用 base64 编码图片

现在我们来打包看看结果吧 image.png 可以看到打包结果中确实只有一张图片了,和我们的预期一样!再打开浏览器看看另外一张图片是如何被处理的 image.png image.png 可以看到,2.94MB的那张图片(也就是第一张图片)是被进行base64编码处理了,不过实际使用中我们只会对头像这种比较小的图片进行base64编码处理,如果对大图片也是用base64处理的话,实际上编码后的结果并没有比图片本身小多少 image.png 可以看到,我们单个js文件就已经接近4MB了,就是因为在js中存放了图片的base64编码结果才这样的

因此千万不要无脑将所有图片都使用base64编码处理哦,我们使用base64编码的主要目的还是为了减少**http**请求

对于那些数量多,但是体积小的图片文件,如果以文件的方式打包的话会产生多条http请求,而如果把它们全都编码成base64存在js文件中,那http请求只会有一次,这样才是合理使用url-loader的场景。


4. 使用 Asset Modules 处理图片资源

前面使用file-loaderurl-loader处理图片资源是webpack4时期的主流方式,现在到了webpack5了,我们需要使用新的方式去处理图片资源

Asset Modules的官方文档

webpack.js.org/guides/asse…

image.png 从官方文档中可以直到:

  • asset/resource替代了原来的file-loader
  • asset/inline替代了原来的url-loader
  • asset/source替代了原来的raw-loader(一种用于将文件加载成字符串的loader
  • asset替代了原来的url-loaderlimit配置

那么我们就来用一下吧,首先是最基本的加载图片

// 使用 asset/resource 加载图片资源
{
  test: /\.(jpe?g|png|gif)$/,
  type: 'asset/resource',
},

直接打包,结果如下 image.png 打包成功,并且浏览器中可以正常加载图片,但是如何和之前一样想要配置打包的文件名以及存放路径怎么办呢?

有两种方式,我的理解就是全局配置和局部配置

4.1 打包资源文件名配置

4.1.1 全局配置

image.pngoutput.assetModuleFilename中配置文件名和存放路径

module.exports = {
  output: {
    // 全局配置资源文件打包路径和文件名
    assetModuleFilename: 'img/[name]-[hash:8][ext]',
  },
};

注意:**[ext]**占位符已经包括了**.**,因此不需要再额外配置一个**.**上去,即不需要写成img/[name]-[hash:8].[ext]

并且不支持[sha256:hash]这样指定hash算法,以及不能使用[emoji]生成可爱的文件名了,要想使用这样的文件名占位符,还是需要使用file-loader

打包结果如下 image.png


4.1.2 局部配置

image.png 就是在具体的rules项中去配置,通过generator去配置

// 使用 asset/resource 加载图片资源
{
  test: /\.(jpe?g|png|gif)$/,
  type: 'asset/resource',
  generator: {
    filename: 'img/[name]-[hash:8][ext]',
  },
},

4.2 实现 url-loader 的 limit 效果

You can change this condition by setting a Rule.parser.dataUrlCondition.maxSize option on the module rule level of your webpack configuration

  1. type改为asset
  2. parser.dataUrlCondition.maxSize中配置阈值
// 使用 asset/resource 加载图片资源
{
  test: /\.(jpe?g|png|gif)$/,
  type: 'asset',
  generator: {
    filename: 'img/[name]-[hash:8][ext]',
  },
  parser: {
    dataUrlCondition: {
      maxSize: 3 * 1024 * 1024, // 3MB
    },
  },
},

5. 加载字体文件

项目中除了使用图片资源外,还经常会使用到字体或者字体图标等资源,那么它们又该如何被webpack加载呢?

首先先准备一下案例环境,我们去阿里的iconfont图标库网站生成一份字体图标代码

  1. 随便找一个图标,下载其svg文件

image.png

  1. 上传图标

image.png

  1. 点击右上角的购物车按钮

image.png

  1. 下载代码

image.png

5.1 font class 方式使用字体图标

解压后能够看到源码,并且有一个html文件打开后可以学习如何使用字体图标,我们使用font class的方式使用字体图标,即导入css文件,并添加相应类名即可使用

// 使用 iconfont 字体图标
import './font/iconfont.css';

const el = document.createElement('div');

// 使用字体图标
const iconEl = document.createElement('i');
iconEl.style.fontSize = '128px';
iconEl.className = 'iconfont icon-shopping';

el.appendChild(iconEl);

使用asset/resource配置字体文件的存放位置

{
  test: /\.ttf$/,
  type: 'asset/resource',
  generator: {
    filename: 'font/[name]-[hash:8][ext]',
  },
},

打包后的效果如下 image.png


5.2 Symbol 方式使用字体图标

可以看到字体图标生效了,但是为啥和一开始在网站上下载的图标不一样呢?好像没有颜色诶,我们打开解压后的源码中的html文档看一看 image.png image.png 原来使用这种方式的字体图标是不支持多色显示的,要想使用多色显示,得使用Symbol方式 image.png image.png 那我们就试试这种方式吧

// src/index.js

// Symbol 方式使用 iconfont 字体图标
import './font/iconfont';

// Symbol 方式使用字体图标
const symbolIconEl = document.createElement('svg');
const useEl = document.createElement('use');
useEl.setAttribute('xlink:href', '#icon-shopping');
symbolIconEl.className = 'icon';
symbolIconEl.ariaHidden = true;
symbolIconEl.appendChild(useEl);
/* src/css/index.css */
.icon {
  width: 10em;
  height: 10em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}

现在就能看到彩色图标了 image.png


通过对图片资源和字体资源在webpack中的使用,不难发现,对于其他任何类型的资源处理也是类似的

只要是想要以文件的方式直接放到打包结果中的话,先配置一条rule,匹配想要处理的文件类型,然后指明type,就是用asset/resource即可,更详细的内容建议阅读webpack5官方文档,本篇文章只是一个开篇,让你能够快速了解webpack的一些概念,如果想要深入了解还是建议阅读官方文档,官方文档才是最好的学习途径!