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;


目录
相关文章
|
前端开发
在Webpack配置文件中,如何配置loader以处理其他类型的文件,如CSS或图片
在Webpack配置文件中,通过设置`module.rules`来配置loader处理不同类型的文件。例如,使用`css-loader`和`style-loader`处理CSS文件,使用`file-loader`或`url-loader`处理图片等资源文件。配置示例:在`rules`数组中添加对应规则,指定`test`匹配文件类型,`use`指定使用的loader。
|
JavaScript 前端开发
Webpack中loader的使用场景
Webpack中的Loader用于处理和转换模块文件,如将TypeScript转为JavaScript、CSS预处理等,通过配置不同的Loader,可以灵活地支持多种文件类型和语言,实现模块化开发与构建优化。
|
前端开发 JavaScript
webpack 中 loader 和 plugin 的区别
在 webpack 中,loader 用于转换模块的源代码,如将 TypeScript 转为 JavaScript;而 plugin 则扩展了 webpack 的功能,可以执行更复杂的任务,如优化打包文件、注入环境变量等。两者共同作用于构建流程的不同阶段。
|
前端开发 UED
Webpack 中处理 CSS 和图片资源的多 Loader 配置
【10月更文挑战第12天】 处理 CSS 和图片资源是 Webpack 配置中的重要部分。通过合理选择和配置多个 Loader,可以实现对这些资源的精细处理和优化,提升项目的性能和用户体验。在实际应用中,需要不断探索和实践,根据项目的具体情况进行灵活调整和优化,以达到最佳的处理效果。通过对 Webpack 中多 Loader 处理 CSS 和图片资源的深入了解和掌握,你将能够更好地应对各种复杂的资源处理需求,为项目的成功构建和运行提供坚实的基础。
406 58
|
前端开发 JavaScript
Webpack 中多个 Loader 的配置
【10月更文挑战第12天】使用多个 Loader 进行配置是 Webpack 中常见的操作,可以实现对各种资源的精细处理和优化。在配置时,需要根据具体需求合理选择和排列 Loader,并注意它们之间的顺序和交互关系。同时,不断了解和掌握新的 Loader 以及它们的特性,有助于更好地发挥 Webpack 的强大功能,提升项目的开发效率和质量。通过深入理解和熟练运用多个 Loader 的配置方法,你将能够更加灵活地处理各种资源,满足项目的多样化需求。
356 58
|
前端开发 JavaScript
Webpack 常用 Loader 和 Plugin
【10月更文挑战第12天】Webpack 是一个强大的模块打包工具,能够将各种资源模块进行打包和处理。Loader 用于转换模块的源代码,如 `babel-loader` 将 ES6+ 代码转换为 ES5,`css-loader` 处理 CSS 文件等。Plugin 扩展 Webpack 功能,如 `HtmlWebpackPlugin` 自动生成 HTML 文件,`UglifyJsPlugin` 压缩 JavaScript 代码。通过合理配置和使用 Loader 和 Plugin,可以构建高效、优化的项目。
336 58
|
缓存 前端开发 JavaScript
深入了解Webpack:模块打包的革命
【10月更文挑战第11天】深入了解Webpack:模块打包的革命
|
缓存 前端开发 JavaScript
Webpack技术深度解析:模块打包与性能优化
【10月更文挑战第13天】Webpack技术深度解析:模块打包与性能优化
|
存储 JavaScript 前端开发
全栈开发实战|Vue进阶——使用静态模块打包工具webpack
全栈开发实战|Vue进阶——使用静态模块打包工具webpack
583 0
全栈开发实战|Vue进阶——使用静态模块打包工具webpack
|
JavaScript 前端开发
模块打包工具 webpack
webpack 支持 AMD 和 CommonJS 类型,通过 loader 机制也可以使用 ES6 的模块格式,通过一个 conf 文件,还能提供更加丰富的功能,支持多种静态文件,还有强大的 code spliting(代码拆分和异步加载) 对 conf 文件的解释:可以配置不同profile,如:可以实现一个webpack.
776 0