webpack 自定义loader

相关文章

本篇文章涉及到webpack loader 的一些原理、api,如果不清楚的可以查看下面的文章或者 webpack 官网查看


项目目录

让我们实现一些简易的loader,从大量的简易loader的实现过程中学习编写如何 webpack loader

├── loaders                     # loader目录
├── src                         # 业务代码
│   │── index.html
│   └── index.js										
├── .gitignore
├── package.json
├── package-lock.json
└── webpack.config.js			# webpack 配置文件

搭建项目

mkdir loaders
cd loaders
npm init -y
npm i -D webpack webpack-cli html-webpack-plugin webpack-dev-server loader-utils

webpack.config.js

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

module.exports = {
  mode: 'development',
  plugins: [
    new HtmlWebpackPlugin({
      title: '自定义 webpack loader',
      template: './src/index.html',
    }),
  ],
};

src/index.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>

</body>
</html>

src/index.js

import text from './happy-new-year.txt';

const textDom = document.createElement('p');
textDom.style.cssText = 'width: 200px;height: 200px;background-color: pink;';
textDom.innerText = text;
document.body.appendChild(textDom);

实现 my-raw-loader

src/happy-new-year.txt

🎉🎉🎆🎆🧨🧨
新年快乐!大吉大利!
🎉🎉🎆🎆🧨🧨

执行 npx webpack-dev-server,会发现编译报错了

在这里插入图片描述

那么下面我们就实现 my-raw-loader 来抛砖引玉!


webpack.config.js

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

module.exports = {
  mode: 'development',
  module: {
    rules: [
      {
        test: /.txt$/,
        use: [
          {
            loader: path.resolve(__dirname, 'loaders/my-raw-loader'),
            options: {
              esModule: true,
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: '自定义 webpack loader',
      template: './src/index.html',
    }),
  ],
};

loaders/my-raw-loader.js

function myRawLoader(source) {
  console.log('source', source);
}

module.exports = myRawLoader;

执行 npx webpack-dev-server 可以看到打印结果,这个参数是一个字符串

在这里插入图片描述


参数

修改 loaders/my-raw-loader.js

function myRawLoader(source) {
  // 提取给定的 loader 选项,
  // 从 webpack 5 开始,this.getOptions 可以获取到 loader 上下文对象。它用来替代来自 loader-utils 中的 getOptions 方法。
  const { esModule } = this.getOptions();
  console.log('esModule:', esModule);

  // 这里一定要返回字符串或者 buffer
  if (!esModule) {
    return `module.exports = ${JSON.stringify(source)}`;
  }
  return `export default ${JSON.stringify(source)}`;
}

module.exports = myRawLoader;

执行 npx webpack-dev-server可以看到通过 this.getOptions() 获取到了当前 loader 的配置,并且编译未报错,访问 http://localhost:8080/ 页面得偿所愿!成功读取并渲染了原始文本内容。

在这里插入图片描述

在这里插入图片描述


schema-utils

schema-utils 由webpack 官方提供, 它配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验

const { validate } = require('schema-utils');
const schema = {
  type: 'object',
  properties: {
    esModule: {
      type: 'boolean',
    }
  },
  "additionalProperties": false // 是否允许不存在的选项传入
};

function myRawLoader(source) {
  const options = this.getOptions();
  validate(schema, options, {
    name: 'my-raw-loader',
    baseDataPath: 'options',
  });

  // 提取给定的 loader 选项,
  // 从 webpack 5 开始,this.getOptions 可以获取到 loader 上下文对象。它用来替代来自 loader-utils 中的 getOptions 方法。
  console.log('esModule:', options.esModule);

  // 这里一定要返回字符串或者 buffer
  if (!options.esModule) {
    return `module.exports = ${JSON.stringify(source)}`;
  }
  return `export default ${JSON.stringify(source)}`;
}

module.exports = myRawLoader;

如果传入未定义的选项,则会发生编译报错

{
  test: /.txt$/,
  use: [
    {
      loader: path.resolve(__dirname, 'loaders/my-raw-loader'),
      options: {
        esModule2: true,
      },
    },
  ],
},

在这里插入图片描述


实现 tpl-loader

info.tpl

<div>
    <h1>{{ name }}</h1>
    <p>{{ age }}</p>
    <p>{{ sex }}</p>
</div>

index.js

import infoTpl from './info.tpl';

const info = {
  name: 'HuaJi',
  age: 29,
  sex: '男',
};

const textDom = document.createElement('p');
textDom.style.cssText = 'width: 200px;height: 200px;background-color: pink;';
textDom.innerHTML = infoTpl(info);
document.body.appendChild(textDom);

webpack.config.js

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

module.exports = {
  mode: 'development',
  output: {
    clean: true,
  },
  devServer: {
    port: 9000,
    hot: true,
    open: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, './src/index.html'),
    }),
  ],

  module: {
    rules: [
      {
        test: /\.tpl$/i,
        use: [
          {
            loader: path.resolve(__dirname, './loaders/tpl-loader.js'),
            options: {
              log: true,
            },
          },
        ],
      },
    ],
  },
};

loaders/tpl-loader.js

const schema = {
  type: 'object',
  properties: {
    log: {
      type: 'boolean',
    },
  },
  additionalProperties: false,
};

function tplReplace(template, replaceData) {
  // 正则匹配{{}}替换为值
  return template.replace(/\{\{(.*?)}}/g, (word, key) => replaceData[key]);
}

function tplLoader(content, map, meta) {
  const options = this.getOptions(schema);
  // 去除模板中的不可见字符
  const resource = content.replace(/\s+/g, '');
  const logStr = options.log ? `console.log('Compiled the file from ${this.resourcePath}')` : '';
  // 返回 esm export 及 函数
  // 函数接收外部参数,函数内部声明一个函数,然后调用
  // 调用时注意:第一个参数 `template` 应当是一个字符串,切记
  return `export default (data) => {
    ${tplReplace.toString()}
    ${logStr}
    return tplReplace('${resource}', data)
  }`;
}
module.exports = tplLoader;

结果:
在这里插入图片描述


实现 file-loader

  1. 指定输出文件的路径——即打包后文件的存储位置。
  2. 生成解析文件的路径——即打包后引用文件时的URL地址。

index.js

import sunflower from './sunflower.jpg';

const img = document.createElement('img');
img.style.cssText = 'width: 200px;height: auto;';
img.src = sunflower;
document.body.appendChild(img);

webpack.config.js

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

module.exports = {
  mode: 'development',
  output: {
    clean: true,
  },
  devServer: {
    port: 9000,
    hot: true,
    open: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, './src/index.html'),
    }),
  ],

  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif)$/i,
        loader: path.resolve(__dirname, './loaders/file-loader.js'),
      },
    ],
  },
};

loaders/file-loader.js

const loaderUtils = require('loader-utils');

function fileLoader(content) {
  // 1. 根据文件内容生成带 hash 值文件名
  const filename = loaderUtils.interpolateName(this, '', {
    content,
  });
  // 2. 将文件输出出去
  this.emitFile(filename, content);

  // 3. 返回 export default "[contenthash].[ext]"
  return `export default "${filename}"`;
}
// file-loader 需要处理图片、字体等文件,它们都是buffer数据,需要使用 raw loader 才能处理
fileLoader.raw = true;

module.exports = fileLoader;

结果:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


实现 style-loader

index.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div class="box">
    <div class="box1"></div>
    <div class="box2"></div>
    <div class="box3"></div>
</div>

</body>
</html>

index.js

import './index.less';

index.less

.box {
  .box1, .box2, .box3 {
    width: 200px;
    height: 200px;
  }

  .box1 {
    background-color: #9a6e3a;
  }
  .box2 {
    background-color: #55a532;
  }
  .box3 {
    background-color: #0077aa;
  }
}

webpack.config.js

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

module.exports = {
  mode: 'development',
  output: {
    clean: true,
  },
  devServer: {
    port: 9000,
    hot: true,
    open: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, './src/index.html'),
    }),
  ],

  module: {
    rules: [
      {
        test: /\.(css|less)$/i,
        use: [path.resolve(__dirname, './loaders/style-loader.js'),
          'css-loader',
          'less-loader',
        ],
      },
    ],
  },
};

loaders/style-loader.js

function styleLoader() {}

// Pitching Loader:https://blog.csdn.net/qq_41887214/article/details/128605233?spm=1001.2014.3001.5501#t12
styleLoader.pitch = function (remainingRequest) {
  // remainingRequest:剩下还需要处理的 loader
  console.log('remainingRequest:', remainingRequest);
  // remainingRequest:/Users/huaji/learning/code/webpack-demos/node_modules/css-loader/dist/cjs.js!/Users/huaji/learning/code/webpack-demos/proficient/step_6-loaders/node_modules/less-loader/dist/cjs.js!/Users/huaji/learning/code/webpack-demos/proficient/step_6-loaders/src/testStyleLoader/index.less

  // 1. 将`remainingRequest`中的绝对路径转换为相对路径
  // https://webpack.docschina.org/api/loaders/#thisutils
  const relativePath = remainingRequest.split('!').map((absolutePath) => this.utils.contextify(this.context, absolutePath)).join('!');
  console.log('relativePath:', relativePath);
  // relativePath:../../../../node_modules/css-loader/dist/cjs.js!../../node_modules/less-loader/dist/cjs.js!./index.less

  const script = `
    // 2. 引入 less-loader、css-loader 处理后的结果
    import style from "!!${relativePath}";
    // 3. 动态生成\`style\`标签插入到页面
    const styleDom = document.createElement('style');
    styleDom.innerHTML = style;
    document.head.appendChild(styleDom);
  `;

  return script;
};

module.exports = styleLoader;

原理

  1. relativePath…/…/…/…/node_modules/css-loader/dist/cjs.js!../…/node_modules/less-loader/dist/cjs.js!./index.less

    • relativePath 是 inline loader 用法
    • 代表./index.less 需要使用 less-loadercss-loader 处理
  2. import style from “!!${relativeRequest}”

    • !! 表示跳过 pre、 normal 和 post loader
    • 在这里是跳过后面再次执行 less-loadercss-loader
  3. inline loader

    • loader 的使用方式

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

    • 含义:

      • 使用 css-loader 和 style-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’;

结果:

在这里插入图片描述



源码:https://gitee.com/yanhuakang/webpack-demos/tree/master/proficient/step_6-loaders

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

__畫戟__

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值