webpack 核心模块 —— loader & plugins(下)

简介: webpack 核心模块 —— loader & plugins

对 loader 中的 options 进行合法校验

为什么需要校验合法性?

向外提供了 options 配置是为了让自定义 loader 具有更高的灵活性和可配置性,但是这样的灵活性如果没有得到约束,那么 options 配置可能就变得没有意义。试想一下,外部使用时传递了一堆 loader 中根本用不到的配置,除了让配置看起来更复杂之外,也会让 loader 内部的各种判断逻辑进行无用的执行。基于以上种种原因,对 options 的合法校验显得尤为重要,只有在校验通过之后再去执行 loader 中的其他处理程序。

获取 loader 中的 options 配置

要对 options 进行合法校验,首先就得获取 options,获取方式有 2 种:

  • 通过 const options = this.getOptions()的方式获取
  • 通过调用 loader-utils 库中的 getOptions(this) 方法获取

校验合法性

可以通过 schema-utils 库中的 validate() 方法进行校验.

通过一个例子进行直观的理解,首先在 webpack.config.js 中修改配置,也就是给 loader1 传入 options 配置,然后对 loader1.js 中的内容进行改写,如下:

// webpack.config.js
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'loader1',
            options: {
              name: 'this is a name!'
            }
          },
          'loader2',
        ]
      },
    ]
  }
// loader1.js
const { validate } = require('schema-utils');
// schema 意为模式,定义校验规则
const loader1_schema = {
  type: "object",
  properties: {
    name: {
      type: 'string',
    },
  },
  // additionalProperties 代表是否可以追加属性
  additionalProperties: true
};
module.exports = function (content, map, meta) {
  console.log('loader1 ...');
  // 获取 options
  const options = this.getOptions();
  console.log('loader1 options = ',options);
  // 校验 options 是否合法
  validate(loader1_schema, options,{
    name: 'loader1',
    baseDataPath: 'options',
  });
  this.callback(null, content, map, meta);
}
module.exports.pitch = function () {
  console.log('loader1 pitch...');
}
复制代码

在 webpack.config.js 中进行合法配置:

{
            loader: 'loader1',
            options: {
              name: 'this is a name!'
            }
          }
复制代码

image.png

在 webpack.config.js 中进行非法配置:

{
            loader: 'loader1',
            options: {
              name: false
            }
          }
复制代码

image.png

实现自定义 loader —— vueLoader

功能描述

针对 .vue 文件中的 <template> 、<script>、<style> 三部分进行拆分,并且重组到一个 .html 文件中.

webapck.config.js

const { resolve } = require('path');
module.exports = {
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: {
          loader: 'vueLoader',
          options: {
            template: {
              path: resolve(__dirname, 'src/index.html'),
              fileName: 'app',
            },
            name: 'app',
            title: 'Home Page',
            reset: true
          }
        }
      },
    ]
  },
  resolveLoader: {
    modules: [
      resolve(__dirname, 'loaders'),
      'node_modules'
    ],
  }
}
复制代码

vueLoader.js

const { validate } = require('schema-utils');
const fs = require('fs');
const { resolve } = require('path');
const vueLoader_schema = {
  type: "object",
  properties: {
    template: {
      type: 'object',
      properties: {
        path: { type: 'string' },
        fileName: { type: 'string' }
      },
      additionalProperties: false
    },
    name: {
      type: 'string',
    },
    title: {
      type: 'string',
    },
    reset: {
      type: 'boolean',
    }
  },
  additionalProperties: false
};
module.exports = function (content, map, meta) {
  const options = this.getOptions();
  const regExp = {
    template: /<template>([\s\S]+)<\/template>/,
    script: /<script>([\s\S]+)<\/script>/,
    style: /<style.+>([\s\S]+)<\/style>/,
  };
  validate(vueLoader_schema, options, {
    name: 'vueLoader',
    baseDataPath: 'options',
  });
  let template = '';
  let script = '';
  let style = '';
  if (content.match(regExp.template)) {
    template = RegExp.$1;
  }
  if (content.match(regExp.script)) {
    let match = RegExp.$1;
    let name = match.match(/name:(.+),?/)[1].replace(/("|')+/g,'');
    script = match.replace(/export default/, `const ${name} = `);
  }
  if (content.match(regExp.style)) {
    style = RegExp.$1;
  }
  let { path, fileName } = options.template;
  fileName = fileName || path.substring(path.lastIndexOf('\\') + 1, path.lastIndexOf('.html'));
  fs.readFile(path, 'utf8', function (error, data) {
    if (error) {
      console.log(error);
      return false;
    }
    const innerRegExp = {
      headEnd: /<\/head>/,
      bodyEnd: /<\/body>/,
    };
    content = data
      .replace(innerRegExp.headEnd, (match, p1, index, origin) => {
        let resetCss = "";
        if (options.reset) {
          resetCss = fs.readFileSync(resolve(__dirname, 'css/reset.css'), 'utf-8')
        }
        let rs = `<style>${resetCss} ${style}</style></head>`;
        return rs;
      })
      .replace(innerRegExp.bodyEnd, (match, p1, index, origin) => {
        let rs = `${template}<script>${script}</script></body>`;
        return rs;
      });
    if (options.title) {
      content = content.replace(/<title>([\s\S]+)<\/title>/, () => {
        return `<title>${options.title}</title>`
      });
    }
    fs.writeFile(`dist/${fileName}.html`, content, 'utf8', function (error) {
      if (error) {
        console.log(error);
        return false;
      }
      console.log('Write successfully!!!');
    });
  });
  return "";
}
复制代码

plugins

在 webpack 中 plugin 是什么?

webpack 中的 plugin 由以下组成:

  • 一个 JavaScript 命名函数JavaScript 类
  • 在插件函数的 prototype 上定义一个 apply() 方法
  • 指定一个绑定到 命名函数 自身的 事件钩子
  • 处理 webpack 内部实例的特定数据
  • 功能完成后调用 webpack 提供的回调

下面是一个 plugin 的基本结构:

apply 中的 tap() 方法来绑定同步操作,但有些 plugin 需要进行是异步操作,这时候可以使用 tapAsync()tapPromise() 这两个异步方法来绑定。当使用 tapAsync 方式时,回调参数会多一个 callback 用于指明异步处理是否结束;当时用 tapPromise 方式时,要在其内部返回一个 Promise 对象,通过改变 Promise 状态来指明异步处理的结果。

class TestWebpackPlugin {
  apply(compiler) {
    compiler.hooks.emit.tap('TestWebpackPlugin', (compilation) => {
      console.log('tap callBack ...');
      // 返回 true 以输出 output 结果,否则返回 false
      return true;
    });
    compiler.hooks.emit.tapAsync('TestWebpackPlugin', (compilation, callback) => {
      setTimeout(() => {
        console.log('tapAsync callBack ...');
        callback();
      }, 2000);
    });
    compiler.hooks.emit.tapPromise('TestWebpackPlugin', (compilation) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('tapPromise callBack ...');
          resolve();
        }, 1000);
      });
    });
  }
}
module.exports = TestWebpackPlugin;
// 输出顺序:
//         1. tap callBack ...  
//         2. tapAsync callBack ...(等待前面的 tap 执行完毕,2s 后输出)  
//         3. tapPromise callBack ...(等待前面的 tapAsync 执行完毕,1s 后输出)
复制代码

plugin 中的执行顺序

从上面的例子中,可以看出其执行顺序为:

  • 不同 hooks 的执行时机可以参考 生命周期钩子函数,执行时机决定了执行顺序
  • 同一个 plugin 中的同一个 hooks 中注册的回调,会按串行顺序执行,即便其中包含了 异步操作

对 plugin 中的 options 进行合法校验

这一点和 loader 中的校验一样,都需要使用 schema-utils 中的 validate() 方法进行校验。和 loader 中不一样的就是,plugin 中的 options 不需要通过 this.getOptions() 的方式获取,因为 plugin 是一个 class 或者是 构造函数,因此可以直接在 constructor 中直接进行获取。

实现自定义 plugin —— CopyWebpackPlugin

功能描述

指定目录 下的所有文件复制到 目标目录,支持忽略某些文件.

webpack.config.js

const CopyWebpackPlugin = require('./plugins/CopyWebpackPlugin');
module.exports = {
  mode:'none',
  plugins: [
    new CopyWebpackPlugin({
      from: './public',
      to: 'dist',
      ignores: ['notCopy.txt']
    })
  ]
};
复制代码

CopyWebpackPlugin.js

const { validate } = require('schema-utils');
const { join, resolve, isAbsolute, basename } = require('path');
const { promisify } = require('util');
const fs = require('fs');
const webapck = require('webpack');
const { RawSource } = webapck.sources;
const readdir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);
const schema = {
  type: 'object',
  properties: {
    from: {
      type: 'string',
    },
    to: {
      type: 'string',
    },
    ignores: {
      type: 'array',
    },
  },
  additionalProperties: false,
}
class CopyWebpackPlugin {
  constructor(options = {}) {
    this.options = options;
    // 校验 options 合法性
    validate(schema, options);
  }
  apply(compiler) {
    compiler.hooks.emit.tapAsync('CopyWebpackPlugin', async (compilation, callback) => {
      let { from, to = '.', ignores = [] } = this.options;
      // 运行指令的目录
      let dir = process.cwd() || compilation.options.context;
      // 判断传入的路径是否为绝对路径
      from = isAbsolute(from) ? from : resolve(dir, from);
      // 1. 获取 form 目录下所以文件或文件夹名称
      let dirFiles = await readdir(from, 'utf-8');
      // 2. 通过 ignores 进行过滤文件或文件夹名称
      dirFiles = dirFiles.filter(name => !ignores.includes(name));
      // 3. 读取 form 目录下所有文件
      const files = await Promise.all(dirFiles.map(async (name) => {
        const fullPath = join(from, name);
        const data = await readFile(fullPath);
        const filename = join(to, basename(fullPath));
        return {
          data,// 文件内容数据
          filename,// 文件名
        };
      }));
      // 4. 生成 webpack 格式的资源
      const assets = files.map(file => {
        const source = new RawSource(file.data);
        return {
          source,
          filename: file.filename,
        };
      });
      // 5. 添加到 compilation 中,向外输出
      assets.forEach((asset) => {
        compilation.emitAsset(asset.filename, asset.source);
      });
      // 6. 通过 callback 指明当前处理完成
      callback();
    });
  }
}
module.exports = CopyWebpackPlugin;


目录
相关文章
|
3月前
|
Web App开发 JSON 前端开发
Webpack【搭建Webpack环境、Webpack增加配置文件、Webpack中使用Loader、Webpack分离CSS文件 】(一)-全面详解(学习总结---从入门到深化)
Webpack【搭建Webpack环境、Webpack增加配置文件、Webpack中使用Loader、Webpack分离CSS文件 】(一)-全面详解(学习总结---从入门到深化)
53 0
|
4月前
|
JSON 前端开发 JavaScript
Webpack【搭建Webpack环境、Webpack增加配置文件、Webpack中使用Loader、Webpack分离CSS文件 】(一)-全面详解(学习总结---从入门到深化)(上)
Webpack【搭建Webpack环境、Webpack增加配置文件、Webpack中使用Loader、Webpack分离CSS文件 】(一)-全面详解(学习总结---从入门到深化)
56 0
|
4天前
|
前端开发 JavaScript 开发者
深入了解Webpack:前端模块打包工具
深入了解Webpack:前端模块打包工具
8 1
|
4月前
|
前端开发 JavaScript
webpack 核心武器:loader 和 plugin 的使用指南(下)
webpack 核心武器:loader 和 plugin 的使用指南(下)
webpack 核心武器:loader 和 plugin 的使用指南(下)
|
4月前
|
JSON 前端开发 JavaScript
webpack 核心武器:loader 和 plugin 的使用指南(上)
webpack 核心武器:loader 和 plugin 的使用指南(上)
webpack 核心武器:loader 和 plugin 的使用指南(上)
|
4月前
|
XML JSON 前端开发
说说webpack中常见的loader?解决了什么问题?
在Webpack中,Loader是用于处理各种文件类型的模块加载器,它们用于对文件进行转换、处理和加载。常见的Loader解决了以下问题:
19 0
|
4月前
|
Web App开发 前端开发 JavaScript
Webpack【搭建Webpack环境、Webpack增加配置文件、Webpack中使用Loader、Webpack分离CSS文件 】(一)-全面详解(学习总结---从入门到深化)(下)
Webpack【搭建Webpack环境、Webpack增加配置文件、Webpack中使用Loader、Webpack分离CSS文件 】(一)-全面详解(学习总结---从入门到深化)
28 0
|
5月前
|
JavaScript 前端开发
Webpack ECMAScript 模块
Webpack ECMAScript 模块
27 0
|
2月前
|
JavaScript 前端开发
webpack成长指北第9章---webpack如何对icon字体进行打包
webpack成长指北第9章---webpack如何对icon字体进行打包
33 1
|
2月前
|
前端开发 JavaScript
webpack成长指北第7章---webpack的css\less\scss样式打包
webpack成长指北第7章---webpack的css\less\scss样式打包
42 0