Webpack:手写自定义loader

1.loader执行顺序

1.1 loader的分类

  • pre: 前置 loader
  • normal: 普通 loader
  • inline: 内联 loader
  • post: 后置 loader

1.2 loader的执行顺序

  • 4类 loader 的执行优级为:pre > normal > inline > post
  • 相同优先级的 loader 执行顺序为:从右到左,从下到上

下面是例子:

// 此时loader执行顺序:loader3 - loader2 - loader1
module: {
  rules: [
    {
      test: /\.js$/,
      loader: "loader1",
    },
    {
      test: /\.js$/,
      loader: "loader2",
    },
    {
      test: /\.js$/,
      loader: "loader3",
    },
  ],
},
// 此时loader执行顺序:loader1 - loader2 - loader3
module: {
  rules: [
    {
      enforce: "pre",
      test: /\.js$/,
      loader: "loader1",
    },
    {
      // 没有enforce就是normal
      test: /\.js$/,
      loader: "loader2",
    },
    {
      enforce: "post",
      test: /\.js$/,
      loader: "loader3",
    },
  ],
},

1.3 使用loader的方式

  • 配置方式:在 webpack.config.js 文件中指定 loader。(pre、normal、post loader)
  • 内联方式:在每个 import 语句中显式指定 loader。(inline loader)

1.4 inline loader

用法:import Styles from 'style-loader!css-loader?modules!./styles.css';

含义:

  • 使用 css-loaderstyle-loader 处理 styles.css 文件
  • 通过 ! 将资源中的 loader 分开
    inline loader 可以通过添加不同前缀,跳过其他类型 loader。
  • ! 跳过 normal loader。
    import Styles from '!style-loader!css-loader?modules!./styles.css';
  • -! 跳过 pre 和 normal loader。
    import Styles from '-!style-loader!css-loader?modules!./styles.css';
  • !! 跳过 pre、 normal 和 post loader。
    import Styles from '!!style-loader!css-loader?modules!./styles.css';

2.开发一个最简单的loader

2.1 项目准备

项目目录结构如下:
在这里插入图片描述
webpack.config.js内容如下:

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

module.exports = {
    entry:"./src/main.js",
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'js/[name].js',
        clean:true
    },
    module:{
        rules:[

        ]
    },
    plugins:[
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname,"public/index.html")
        }),
    ],
    //暂时指定为开发模式,因为开发模式不会压缩便于观察打包后的代码信息
    mode:"development",
}

src/main.js内容如下:

console.log("hello main!!!")

安装一下依赖:

npm install webpack webpack-cli html-webpack-plugin -D

安装好依赖后多出两个包管理文件:
在这里插入图片描述
通过npx webpack打包:
在这里插入图片描述

2.2 最简单的loader

新建一个loader文件:
在这里插入图片描述

//`loaders/test-loader.js`:

/*
  loader就是一个函数
  当webpack解析资源时,会调用相应的loader去处理
  loader接受到文件内容作为参数,返回内容出去
    content 文件内容
    map SourceMap
    meta 别的loader传递的数据
*/
module.exports = function loader1(content) {
    console.log("hello loader");
    return content.toUpperCase();
};

配置webpack.config.js

 module:{
        rules:[
            {
                test:/\.js$/,
                loader: "./loaders/test-loader.js"
            }
        ]
 },

然后打包npx webpack
在这里插入图片描述
查看dist/js下的js文件:
在这里插入图片描述

2.3 loader接受的参数

  • content 源文件的内容
  • map SourceMap 数据
  • meta 数据,可以是任何内容

3.loader分类

3.1 同步loader

//如果当前资源只配置了这一个loader,可以用这种方式
module.exports = function (content, map, meta) {
  return content;
};

this.callback 方法则更灵活,因为它允许传递多个参数,而不仅仅是 content

module.exports = function (content, map, meta) {
  /*
    第一个参数:err 代表是否有错误
    第二个参数:content 处理后的内容
    第三个参数:source-map 继续传递source-map
    第四个参数:meta 给下一个loader传递参数
  */
  this.callback(null, content, map, meta);
  // 同步loader中不能进行异步操作
  // setTimeout(() => {
  //   console.log("test1");
  //   this.callback(null, content, map, meta);
  // }, 1000);
};

3.2 异步loader

module.exports = function (content, map, meta) {
  const callback = this.async();
  // 进行异步操作
  setTimeout(() => {
    callback(null, result, map, meta);
  }, 1000);
};

由于同步计算过于耗时,在 Node.js 这样的单线程环境下进行此操作并不是好的方案,我们建议尽可能地使你的 loader 异步化。但如果计算量很小,同步 loader 也是可以的。

3.3 Raw Loader

默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 raw 为 true,loader 可以接收原始的 Buffer。

loaders/raw-loader.js
module.exports = function (content) {
  // content是一个Buffer数据
  console.log(content)
  return content;
};
module.exports.raw = true; // 开启 Raw Loader

配置一下webpack配置文件:

{
    test:/\.js$/,
    loader: "./loaders/raw-loader.js"
}

打包项目:
在这里插入图片描述

3.4 Pitching Loader

module.exports = function (content) {
  return content;
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  console.log("do somethings");
};

webpack 会先从左到右执行 loader 链中的每个 loader 上的 pitch 方法(如果有),然后再从右到左执行 loader 链中的每个 loader 上的普通 loader 方法。

下面来举个例子:
在这里插入图片描述
test-loaders/test1.js:

module.exports = function (content) {
    console.log('normal loader 1');
    return content;
};

module.exports.pitch = function () {
    console.log("pitch loader 1");
};

test-loaders/test2.js:

module.exports = function (content) {
    console.log('normal loader 2');
    return content;
};

module.exports.pitch = function () {
    console.log("pitch loader 2");
};

test-loaders/test3.js:

module.exports = function (content) {
    console.log('normal loader 3');
    return content;
};

module.exports.pitch = function () {
    console.log("pitch loader 3");
};

配置webpack文件:

  {
                test:/\.js$/,
                use: ["./test-loaders/test1.js","./test-loaders/test2.js","./test-loaders/test3.js"]
  }

打包项目:
在这里插入图片描述

在这个过程中如果任何 pitch 有返回值,则 loader 链被阻断。webpack 会跳过后面所有的的 pitch 和 loader,直接进入上一个 loader 。

假如我们在test2.js中的pitch 加一个返回值:

module.exports = function (content) {
    console.log('normal loader 2');
    return content;
};

module.exports.pitch = function () {
    console.log("pitch loader 2");
    return "result";
};

打包项目:
在这里插入图片描述

4.loader API

方法名含义用法
this.async异步回调 loader。返回 this.callbackconst callback = this.async()
this.callback可以同步或者异步调用的并返回多个结果的函数this.callback(err, content, sourceMap?, meta?)
this.getOptions(schema)获取 loader 的 optionsthis.getOptions(schema)
this.emitFile产生一个文件this.emitFile(name, content, sourceMap)
this.utils.contextify返回一个相对路径this.utils.contextify(context, request)
this.utils.absolutify返回一个绝对路径this.utils.absolutify(context, request)

其它方法详细讲官方文档。

5.开发自定义loader

5.1 手写clean-log-loader

作用:用来清理 js 代码中的console.log

//clean-log-loader/clean-log-loader.js
module.exports = function cleanLogLoader(content) {
  // 将console.log替换为空
  //`\`为转义符
  //`?`为可选
  //g表示全局
  return content.replace(/console\.log\(.*\);?/g, "");
};

配置webpack文件:

  {
                test:/\.js$/,
                loader: "./clean-log-loader/clean-log-loader.js"
  }

打包项目:
在这里插入图片描述

5.2 手写banner-loader

作用:给 js 代码添加文本注释

//banner-loader/index.js
const schema = require("./schema.json");

module.exports = function (content) {
  // 获取loader的options,同时对options内容进行校验
  // schema是options的校验规则(符合 JSON schema 规则)
  const options = this.getOptions(schema);

  const prefix = `
    /*
    * Author: ${options.author}
    */
  `;
  return `${prefix} \n ${content}`;
};
//banner-loader/schema.json
{
  "type": "object",
  "properties": {
    "author": {
      "type": "string"
    }
  },
  //是否允许追加属性
  "additionalProperties": false
}

配置webpack文件:

 {
              test: /\.js$/,
              loader: "./banner-loader/index.js",
              options: {
                author: "老王",
                //age: 18, // 不能新增字段,不然会报错
              },
 },

打包项目:
在这里插入图片描述

5.3 手写babel-loader

作用:编译js代码,将 ES6+语法编译成 ES5-语法。

在此之前我们需要了解babel是什么,可以看我写的文章https://blog.csdn.net/fageaaa/article/details/146023275。另外还可以点击这儿查看中文说明文档。在这里面可以查看相关工具的用法。如下:
在这里插入图片描述

先要下载依赖:

npm i @babel/core @babel/preset-env -D
//babel-loader/index.js
const schema = require("./schema.json");
const babel = require("@babel/core");

module.exports = function (content) {
  const options = this.getOptions(schema);
  // 使用异步loader
  const callback = this.async();
  // 使用babel对js代码进行编译
  babel.transform(content, options, function (err, result) {
    callback(err, result.code);
  });
};
//banner-loader/schema.json
{
  "type": "object",
  "properties": {
    "presets": {
      "type": "array"
    }
  },
  "additionalProperties": true
}

配置webpack

 {
                test: /\.js$/,
                loader: "./babel-loader/index.js",
                options: {
                    presets: ["@babel/preset-env"],
                },
 },

修改一下src/main.js文件:

console.log("hello main!!!")
let a=11

打包项目:
在这里插入图片描述

5.4 手写file-loader

作用:将文件(如图片、字体)原封不动输出出去。在webpack中通过type:asset/resource实现。

file-loader可以让打包文件如下:
在这里插入图片描述
总结:

  • file-loader可以根据文件内容生成一个hash值的文件名称
  • 把该文件名输出到地址目录
  • return一个module.exports=“路径/hash值”

安装一个工具:

npm i loader-utils -D
//file-loader/index.js
const loaderUtils = require("loader-utils");

module.exports = function (content) {
  // 1. 根据文件内容生成带hash值文件名
  let interpolatedName = loaderUtils.interpolateName(this, "[hash].[ext][query]", {
    content,
  });
  interpolatedName = `images/${interpolatedName}`
  // console.log(interpolatedName);
  // 2. 将文件输出出去
  this.emitFile(interpolatedName, content);
  // 3. 返回:module.exports = "文件路径(文件名)"
  return `module.exports = "${interpolatedName}"`;
};

// 需要处理图片、字体等文件。它们都是buffer数据
// 需要使用raw loader才能处理
module.exports.raw = true;

配置webpack文件:

   {
                test: /\.(png|jpe?g|gif)$/,
                loader: "./file-loader/index.js",
                //这个地方如果不设置,webpack默认会用type:asset/resource来处理非js的资源
                //也就是会多生成3张图片
                type: "javascript/auto", // 阻止webpack默认处理图片资源,只使用file-loader处理
   },

然后我们需要在项目中用到图片,准备一下图片:
在这里插入图片描述
新建index.css

//src/css/index.css
.box1 {
  width: 100px;
  height: 100px;
  background-image: url("../images/1.jpeg");
}

.box2 {
  width: 100px;
  height: 100px;
  background-image: url("../images/2.png");
}

.box3 {
  width: 100px;
  height: 100px;
  background-image: url("../images/3.gif");
}

在主文件引入样式:

import "./css/index.css";

css的打包加载需要用到css-loaderstyle-loader

npm i css-loader style-loader -D

css提交规则:

 {
                test: /\.css$/,
                use: ["style-loader", "css-loader"],
},

然后给项目添加一些html结构:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>webpack</title>
</head>
<body>
  <div class="box1"></div>
  <div class="box2"></div>
  <div class="box3"></div>
</body>
</html>

打包项目:
在这里插入图片描述
在这里插入图片描述
通过vscode打开项目:
在这里插入图片描述

5.5 手写style-loader

作用:动态创建style标签,插入js中的样式代码,使样式生效。

关于style-loader的详细解释参见我写的文章

//style-loader/index.js
module.exports = function (content) {
  /*
    1. 直接使用style-loader,只能处理样式
      不能处理样式中引入的其他资源
      use: ["./loaders/style-loader"],
  */
  const script = `
     const styleEl = document.createElement('style');
     styleEl.innerHTML = ${JSON.stringify(content)};
     document.head.appendChild(styleEl);
  `;
  return script;
};

然后只添加style-loader规则:

   {
                test: /\.css$/,
                // use: ["style-loader", "css-loader"],
                use: ["./style-loader/index.js"],

   },

打包项目运行打包文件index.html发现界面空白:
在这里插入图片描述
原因是直接使用style-loader,只能处理样式不能处理样式中引入的其他资源。

css-loader添加进去:

 {
                test: /\.css$/,
                // use: ["style-loader", "css-loader"],
                // use: ["./style-loader/index.js"],
                use: ["./style-loader/index.js","css-loader"],
 },

再次打包项目运行打包文件index.html发现界面还是空白:
在这里插入图片描述
出现的问题是css-loader暴露了一段js代码,style-loader需要执行js代码,得到返回值,再动态创建style标签,插入到页面上

我们需要使用pitch loader用法:

//style-loader/index.js
module.exports = function (content) {

};
module.exports.pitch = function (remainingRequest) {
  // remainingRequest 剩下还需要处理的loader
  // console.log(remainingRequest);
  //C:\前端\webpack5\手写loader\my-loader\node_modules\css-loader\dist\cjs.js!C:\前端\webpack5\手写loader\my-loader\src\css\index.css

  // 1. 将 remainingRequest 中绝对路径改成相对路径(因为后面只能使用相对路径操作)
  const relativePath = remainingRequest
    .split("!")
    .map((absolutePath) => {
      // 返回相对路径
      return this.utils.contextify(this.context, absolutePath);
    })
    .join("!");

  //console.log(relativePath); //  ../../node_modules/css-loader/dist/cjs.js!./index.css

  // 2. 引入css-loader处理后的资源
  // 3. 创建style,将内容插入页面中生效
  //这里有两个`!`,`!!`会跳过 pre、 normal 和 post loader。
  const script = `
    import style from "!!${relativePath}";
    const styleEl = document.createElement('style');
    styleEl.innerHTML = style;
    document.head.appendChild(styleEl);
  `;

  // 中止后面loader执行
  return script;
};

我们打包运行,这次是正常运行。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值