# 2.自定义 webpack-loader

# 目的

  1. 访问到 JS 配置文件中相对路径的图片

# 实现方案

  1. 因为在编译阶段,目录 img 下的图片,并不会被依赖,webpack 不会进行打包。所以在初始化的,将 img 强行进行依赖,打入包中。
  2. webpack-loader 对 JS 文件进行处理,将相对路径的图片,替换为绝对路径带有 MD5 后缀的图片。

# 自定义 webpack-loader 注意事项

# 先知

  1. webpack 只认识 JS 和 JSON,不认识 HTML、CSS、Ts、Less、图片,所以需要 loader 进行翻译
  2. loader 的处理顺序,是和书写顺序是相反的,即:less-loader->css-loader->style-loader,链式调用
  3. loader 的标准格式是,暴露一个 Node.js 模块,导出一个函数,对源内容进行处理后,返回处理后的内容。
module.exports = {
  module: {
    rules: [
      {
        // 增加对 less 文件的支持
        test: /\.less/,
        // less 文件的处理顺序为先 less-loader 再 css-loader 再 style-loader
        use: [
          "style-loader",
          {
            loader: "css-loader",
            // 给 css-loader 传入配置项
            options: {
              minimize: true,
            },
          },
          "less-loader",
        ],
      },
    ],
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# loader 的两种模版

# return source(返回转化后的内容)

module.exports = (source) => {
  const content = source.replace("hello", "哈哈哈");
  return content;
};
this.callback();
module.exports = (source) => {
  // 处理 source
  const content = source.replace("hello", "哈哈");
  // 使用 this.callback 返回内容
  this.callback(null, content);
  // 使用 this.callback 返回内容时,该 loader 必须返回 undefined,
  // 以让 Webpack 知道该 loader 返回的结果在 this.callback 中,而不是 return 中
};
1
2
3
4
5
6
7
8
9
10
11
12
13

# callback 详细参数

this.callback(
    // 当无法转换源内容时,给 Webpack 返回一个 Error
    err: Error | null,
    // 源内容转换后的内容
    content: string | Buffer,
    // 用于把转换后的内容得出原内容的 Source Map,方便调试
    sourceMap?: SourceMap,
    // 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,
    // 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能
    abstractSyntaxTree?: AST
);
1
2
3
4
5
6
7
8
9
10
11

# loader 的同步和异步

同步:上面两种方式,没有加 async 就是同步代码 异步:适用于需要依赖网络请求的 loader

module.exports = function (source) {
  // 调用 this.async() API,告诉 webpack本次转换是异步的,loader 会在 callback 中返回结果
  const callback = this.async();
  // 使用 setTimeout 模拟异步过程
  setTimeout(() => {
    const content = source.replace("hello", "哈哈");
    // 通过 callback 返回执行异步后的结果
    callback(null, content);
  }, 3000);
};
1
2
3
4
5
6
7
8
9
10

# 实战

  1. 将所以 img 目录下文件通过 require,引入(如果没有这一步,虽然图片路径对了,但是 webpack 打包里面会不存在该图片)
const exports = {};
const jsonReq = require.context(
  // 其组件目录的相对路径
  ".",
  // 是否查询其子目录
  true,
  // 匹配基础组件文件名的正则表达式
  /\.(json|js)$/
);
const regExp = /(10\d{3}).*\.(json|js)$/;
jsonReq.keys().forEach((fileName) => {
  const matches = fileName.match(regExp);
  if (matches) {
    const name = `config_${matches[1]}`;
    const ext = matches[2];
    // 同名情况下 JS 优先级更高
    if (ext === "js" || !exports[name]) {
      exports[name] = jsonReq(fileName);
    }
  }
});

// 引入所有的图片
const imgReq = require.context(".", true, /\.(png|jpe?g|svga)$/);

imgReq.keys().forEach((fileName) => {
  imgReq(fileName);
});

export default exports;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  1. 正则匹配所有 images,进行 loader 转化
// 1. 通过正则,匹配文件,svga普通loder,如果是
      {
        test: /\.svga$/,
        use: "file-loader"
      },
			{
        test: /nuwa\/schemas\/\d{5}.*\.js$/,
        use: "./src/banners/nuwa/build/loader.js"
      }
// 2. 将所有图片进行转化
		config.module
      .rule("images") // -> Default configuration
      .test(/\.(png|jpe?g|gif|webp)(\?.*)?$/)
      .use("url-loader")
      .loader("url-loader")
      .options({
        limit: 1,
        fallback: {
          loader: "file-loader",
          options: { name: "img/[name].[hash:8].[ext]" }
        }
      })
      .end();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  1. 如果文件是 svga,转为[hash].svga 格式,如果是其他格式,则在文件后面加入[hash.slice(0, 8)],这么做是因为文件通过 file-loader,转为上述格式的文件
const md5File = require("md5-file");
const path = require("path");
const { extractImageFromJSON, replaceJSON } = require("./utils");
const nodeEval = require("node-eval");

module.exports = function loader(content, map, meta) {
  let callback = this.async();
  const filePath = this.resourcePath;
  const imageList = extractImageFromJSON(filePath);

  const changedImageList = imageList.map((image) => {
    // 文件 md5
    const hash = md5File.sync(image);
    const baseName = path.basename(image);
    // 文件后缀和文件名
    const ext = baseName.split(".").pop();
    const name = baseName.replace(`.${ext}`, "");
    // 具体的格式,参考 build/vue.common.config.js 中 file-loader 的配置
    // svga 的处理逻辑 build/vue.common.config.js 中 file-loader
    const url =
      ext === "svga"
        ? `${hash}.${ext}`
        : `./img/${name}.${hash.slice(0, 8)}.${ext}`;
    return {
      url,
      file: image,
    };
  });

  // require 的方式会导致 source 内容缓存,需要用 vm.runInContext 执行 content 的内容
  // const source = require(filePath);
  const source = nodeEval(content);
  const parsedContent = replaceJSON(changedImageList, {
    content: JSON.stringify(source),
    dirname: path.dirname(filePath),
  });

  return callback(null, `module.exports = ${parsedContent}`, map, meta);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Last Updated: 6/3/2024, 1:08:34 AM