[ webpack ] webpack 的 loader 和 plugin 开发的方法

2021-06-12 16:26:02 浏览数 (1)

虽说不同构建工具在原理上是大同小异,但下一篇也应该写写 vite 了。

Loader 和 Plugins 的区别

loader 主要的是处理静态资源,而 plugins 是可以贯穿在整个 webpack 构建的周期中,他能做到 loader 做不到的事情。但是,loader 他可以用独立的运行环境,可以在本地使用一些库进行本地发发调制,而 plugins 不行,他必须编写好这个 plugin 之后在 webpack 构建中将 plugin 放在 plugins 的数组中执行。

loader 开发的常用的开发方法

webpack 文档 DEMO 结构分析

代码语言:txt复制
import { getOptions } from 'loader-utils';
import { validate } from 'schema-utils';
const schema = {
  type: 'object',
  properties: {
    test: {
      type: 'string',
    },
  },
};
export default function (source) {
  const options = getOptions(this);
  validate(schema, options, {
    name: 'Example Loader',
    baseDataPath: 'options',
  });
  // Apply some transformations to the source...
  return `export default ${JSON.stringify(source)}`;
}

loader 实际上就是一个 js 模块,提供一个方法对原文件进行逻辑操作,处理完毕之后返回回去的一个过程。顺带提一点就是,loader 的链式调用是从后往前。

同步 loader 参数获取

参数获取可以使用一个 叫 loader-utils 的 loader,使用其中的 getOptions 的方法就可以拿到传递的参数。

在 runLoaders 配置中 loaders 参照文档修改为带 options 的配置,举例加上一个对象:

代码语言:txt复制
runLoaders({
    loaders: [
        // path.join(__dirname, './loaders/raw-loader.js'),
        {
            loader: path.join(__dirname, './loaders/raw-loader.js'),
            options:{
                env: 'development',
                version: '1.0.0'
            },
        }
    ],
...})

之后使用 loader-utils 将传递的参数带出。注意,这里的 module.exports 不能使用箭头函数,否则,this 是指向了当前的作用域,就拿不到 runLoaders 里面的属性了。

代码语言:txt复制
// npm install loader-utils -save-dev
const loaderUtils = require('loader-utils');

module.exports = function (source) {
    console.log(this);
    const { env, version } = loaderUtils.getOptions(this);
    console.log(env);
    console.log(version);
    ...
};

// npmjs -> https://www.npmjs.com/package/loader-utils 

同步 loader 异常处理

同步执行的情况下,处理异常错误打印的方式有两种:

第一种:是直接使用 Error 输出错误,可以在信息内填写错误编号

代码语言:txt复制
module.exports = function (source) {
 	  ...
    return new Error('error:10001');
};

第二种:是直接使用 this.callback ,根据参数的不同,再执行下一步或者是输出报错信息。

代码语言:txt复制
module.exports = function (source) {
		...
    return this.callback(new Error('error:10001'), source);
};

this.callback 的第一个参数是 Error 时,表示异常直接报错,如果第一个参数是 null ,那就说明函数正常执行下一步返回数据, 同时也支持多个参数进行传递,代码如下:

代码语言:txt复制
module.exports = function (source) {
		...
		const params = { key: 1 }
    return this.callback(null, source, params);
};

异步 loader 处理

使用异步获取结果需要使用 this.async() 这个方法,作为一个 callback 使用,第一个参数是判断是否错误,第二个直接传递参数。

代码语言:txt复制
module.exports = function (source) {
		...
    const callback = this.async();
    fs.readFile(path.join(__dirname, '../src/number.txt'), 'utf-8', (err, data) => {
        if(err){
            callback(err,'error')
        }
        callback(null, data);
    });
};

loader 输出文件

使用 emitFile 进行输出。

代码语言:txt复制
const loaderUtils = require('loader-utils');
module.exports = function (source) {
    const url = loaderUtils.interpolateName(this, '[name].[ext]', source);
    this.emitFile(url, source)
    return source;
}

开发适用于活动的 CSS Sprites loader

首先,需要使用 spritesmith 这个依赖,将多张图片和合并到一起。

第一步:读取图片 URL

思路是先将 css 文件获取到,再使用正则匹配导出所有的图片地址

代码语言:txt复制
const loaderUtils = require('loader-utils');
module.exports = function (source) {
   const images = source.match(/url((S*)/g);
   const matchedImages = [];
   if (images && images.length > 0) {
		for (let i = 0; i < images.length; i  ) {
     const img = images[i].match(/url(..(S*)'/)[1];
     matchedImages.push(path.join(__dirname, img));
    }
   }
}

再把文件的目录提取出来,转换成一个绝对地址 push 到一个数组里面,打印出来的 matchedImages 如下:

代码语言:txt复制
[
  '/Users/../images/web-security-bg-1.jpeg',
  '/Users/../images/webpack-images-2.png'
]
第二步:使用 spritesmith 合并图片和 css 地址替换
代码语言:txt复制
Spritesmith.run({ src: matchedImages}, (err, result) => {
	fs.writeFileSync(path.join(process.cwd(), 'dist/sprite.png'), result.image);
	source = source.replace(/url((S*)/g, (match) =>{
		return `url("dist/sprite.jpg")`;
  });
  fs.writeFileSync(path.join(process.cwd(), 'dist/index.css'), source);
     callback(null,source); })

在 dist 的目录中,就会出现一个合并好的图片同时 dist 里面还有一个已替换了 sprite 图的 css 文件 ,当然这里只是说明了一个思路,如果要完全的实现图片和样式的替换还需要考虑到背景大小,定位或者是一些边界问题,这里就不再细说了。

webpack 的 Plugin 的常用的开发方法

webpack 文档 DEMO 结构分析

代码语言:txt复制
// webpack writting a plugin -> https://webpack.js.org/contribute/writing-a-plugin/

// A JavaScript class.
class HelloWorldPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('Hello World Plugin', (
      stats /* stats is passed as an argument when done hook is tapped.  */
    ) => {
      console.log('Hello World!');
    });
  }
}
module.exports = HelloWorldPlugin;

plugin 实际上一个一个类,在 webpack 使用 plugin 的时候都会有 new XXplugin() 的操作,就是新建一个 plugin 的类。apply 是 plugin 在 webpack 是每一次构建的时候都会运行。

hooks 是 compiler 对象的一个钩子,也可以说是可以监听在某个阶段做一些什么样的事情。会掉的最后就是这个 plugin 的逻辑代码。

开发一个压缩构建资源为 zip 包的 plugin

创建 zip 文件

首先,还是先使用一个 jszip 它可以将文件压缩成一个 zip 包,使用 compiler 对象的 hooks 的 emit 钩子,生成一个文件。

代码语言:txt复制
const JSZip = require('jszip');
const zip = new JSZip();
compiler.hooks.emit.tapAsync('ZipPlugin', (Compilation, callback) => {
    const folder = zip.folder(this.options.filename);
    for (let filename in Compilation.assets) {
        const source = Compilation.assets[filename].source();
        folder.file(filename, source)
    }
})

使用 zip.folder 先设置 zip 包的名称, 在触发到了emit 的 tapAsunc 异步钩子的时候,处理 compilation 的内容填充到 folder 里面去。 compilation.assist 是一个目录名,遍历出文件名之后使用 source 拿到 source 在放回 folder 中。

第二步将文件输出为 zip
代码语言:txt复制
zip.generateAsync({
  type:'nodebuffer'
  	}).then((content)=>{
  	const outputPath = path.join(Compilation.options.output.path 	,this.options.filename   '.zip');
  const outputRelativePath = path.relative(
    Compilation.options.output.path,
    outputPath
  )
  Compilation.assets[outputRelativePath] = new RawSource(content);
    callback()
    console.log(Compilation.options)
  })
})

使用 zip.generateAsync 返回的 content 是一个 buffer ,需要使用 RawSource 将这个 buffer 转换挂到 compilation 的 assist 中 ,最后执行 callback 。

0 人点赞