webpack中编写自定义loader和plugin,及如何调试(debug)webpack,vuecli等脚手架

debug(调试)webpack:

因为plugin和loader的编写需要nodejs环境,需要追踪一些参数,这时候调试就显得很重要了,但调试webpack不像在浏览器中debug那么轻松,需要一些配置:

首先在package.json中加入如下代码: 改命令表示调试webpack并停在第一行

 "scripts": {
    
    
    "start": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
  }

调试分为两种:

  1. 浏览器中调试
  2. vscode中调试

浏览器中debug:

命令行直接执行npm run start
在这里插入图片描述
出现上述调试,说明启动成功,打开浏览器,打开开发者工具,可以看到控制台多了一个选项
在这里插入图片描述
在这里插入图片描述
到这里说明调试成功了

vscode中调试(我更喜欢的方式):

更改package.json如下:

 "scripts": {
    
    
    "start": "node --inspect-brk=5858 ./node_modules/webpack/bin/webpack.js"
  },

点击vscode中如下按键创建launch.json文件:
在这里插入图片描述
在这里插入图片描述

更改launch.json文件如下:端口好要对上

{
    
    
  // 使用 IntelliSense 了解相关属性。
  // 悬停以查看现有属性的描述。
  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
    
    
      "type": "node",
      "request": "launch",
      "name": "启动程序",
      "skipFiles": ["<node_internals>/**"],
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run", "start"],
      "port": 5858
    }
  ]
}

此时按F5或者点击此处即可开始debug在这里插入图片描述
效果如下:
在这里插入图片描述

同理,如果你是vue或者react的脚手架创建出来的项目,只需要把script中调试的js文件换成cli中的核心起始js文件即可,以vuecli为例,package.jsonscript换成如下:

 "scripts": {
    
    
        "debug": "node --inspect-brk=9229 ./node_modules/@vue/cli-service/bin/vue-cli-service.js serve"
  }



loader:

loader本质上是一个函数

在一个loader.js文件中编写如下代码:

module.exports = function (content, map, meta) {
    
    
   console.log(content);
	return content;
}

webpack.config.js配置如下:

module.exports = {
    
    
  module: {
    
    
    rules: [
      {
    
    
        test: /\.js$/,
        loader: 'babelLoader',
      },
    ]
  },
}

那么当执行webpack的时候,所有js文件都会经过我们编写的loader,content中的内容即是源码,我们可以通过一些正则,方法把源码转换后打包,这就是loader的原理了

loader的分类:

同步:
写法一:

//同步loader
module.exports = function (content, map, meta) {
    
    
  return content;
}

写法二:

module.exports = function (content, map, meta) {
    
    
  this.callback(null, content, map, meta);
}

异步:

// 异步loader
module.exports = function (content, map, meta) {
    
    

  const callback = this.async();

  setTimeout(() => {
    
    
    callback(null, content);
  }, 1000)
}

一般推荐的写法是异步loader,因为不会对打包造成阻塞,性能较好

loader中一些工具方法:

  1. getOptions
  2. validate

loader.js代码如下:

const {
    
     getOptions } = require('loader-utils');
const {
    
     , } = require('schema-utils');

const schema = require('./schema');

module.exports = function (content, map, meta) {
    
    
  // 获取options
  const options = getOptions(this);


  // 校验options是否合法
  validate(schema, options, {
    
    
    name: 'loader'  //填一个loader的名字,控制台会提示哪个loader的配置校验失败
  })

  return content;
}

webpack.config.js配置如下:

module.exports = {
    
    
  module: {
    
    
    rules: [
      {
    
    
        test: /\.js$/,
        loader: './loader.js',
        options: {
    
    
          name:"zhangsan"
        }
      },
    ]
  },
}

./schema.json如下:

{
    
    
  "type": "object",
  "properties": {
    
    
    "name": {
    
    
      "type": "string",
      "description": "名称~"
    }
  },
  "additionalProperties": false   //是否允许options中有额外的属性
}

./schema.json是从来配置option传入的规则,不符合规则,则会报错,getOptions是用来取到传入的loaderoption配置,
validate则是配合./schema.json中的规则来校验拿到的options配置是否符合预期

实际应用:

使用@babel/core编译js文件:

编写loader.js如下:

const {
    
     getOptions } = require('loader-utils');
const {
    
     validate } = require('schema-utils');
const babel = require('@babel/core');
const util = require('util');

const babelSchema = require('./babelSchema.json');

// babel.transform用来编译代码的方法
// 是一个普通异步方法
// util.promisify将普通异步方法转化成基于promise的异步方法
const transform = util.promisify(babel.transform);

module.exports = function (content, map, meta) {
    
    
  // 获取loader的options配置
  const options = getOptions(this) || {
    
    };
  // 校验babel的options的配置
  validate(babelSchema, options, {
    
    
    name: 'Babel Loader'
  });

  // 创建异步
  const callback = this.async();

  // 使用babel编译代码
  transform(content, options)
    .then(({
    
    code, map}) => callback(null, code, map, meta))
    .catch((e) => callback(e))
}

plugin:

plugin要比loader强大很多,不仅能影响源码,还能影响文件

webpack.config.js编写如下:

/const Plugin1 = require('./plugins/Plugin')

module.exports = {
    
    
  plugins: [
     new Plugin1()
  ]
}

Plugin.js编写如下:

class Plugin1 {
    
    
  apply(complier) {
    
    
    complier.hooks.emit.tap('Plugin1', (compilation) => {
    
    
      console.log('emit.tap 111');
    })
  }
}
module.exports = Plugin1;

apply是一种固定的写法,可以参考官网,apply中我们可以注册自己的钩子,那么在webpack执行的时候,执行到emit阶段,就会触发我们的钩子,对文件进行操作

实际应用:

编写一个插件,增加一个a.txt文件,查看文档可知,可在additionalAssets这个钩子中增加资源
在这里插入图片描述
plugin.js代码如下:

class Plugin2 {
    
    

  apply(compiler) {
    
    
    // 初始化compilation钩子
    compiler.hooks.thisCompilation.tap('Plugin2', (compilation) => {
    
    
      compilation.hooks.additionalAssets.tapAsync('Plugin2', async (cb) => {
    
    
        const content = 'hello plugin2';
        // 往要输出资源中,添加一个a.txt
        compilation.assets['a.txt'] = {
    
    
          // 文件大小
          size() {
    
    
            return content.length;
          },
          // 文件内容
          source() {
    
    
            return content;
          }
        }
        cb();
      })
    })
  }
}
module.exports = Plugin2;

webpack.config.js中使用:

// const Plugin2 = require('./plugins/Plugin2')

module.exports = {
    
    
  plugins: [
     new Plugin2()
  ]
}

那么打包后,dist文件中就会新增一个a.txt的资源文件了
在这里插入图片描述
这种添加资源的方式挺麻烦的内容和大小都要自己填写,有没有自动计算的方式呢?

利用webpack提供的一些工具函数,可以更加方便的生成和操作文件
改写plugin.js如下:

const fs = require('fs');
const util = require('util');
const path = require('path');
const webpack = require('webpack');
const {
    
     RawSource } = webpack.sources;
// 将fs。readFile方法变成基于promise风格的异步方法
const readFile = util.promisify(fs.readFile);
class Plugin2 {
    
    
  apply(compiler) {
    
    
    compiler.hooks.thisCompilation.tap('Plugin2', (compilation) => {
    
    
      compilation.hooks.additionalAssets.tapAsync('Plugin2', async (cb) => {
    
    
					//读取b.txt文件,读取后是个buffer 
         const data = await readFile(path.resolve(__dirname, 'b.txt'));
         compilation.assets['a.txt'] = new RawSource(data);
        cb();
      })
    })
  }
}
module.exports = Plugin2;

以上介绍了一些常用的工具方法,更多方法还得参考官网

实际应用:

编写一个copy plugin:用于复制资源,复制public中的资源到dist目录下
在这里插入图片描述

plugin.js代码如下:

const path = require('path');
const fs = require('fs');
const {
    
    promisify} = require('util')
const {
    
     validate } = require('schema-utils');
//可以匹配文件列表,过滤一些文件
const globby = require('globby');
const webpack = require('webpack');
const schema = require('./schema.json');
const readFile = promisify(fs.readFile);
const {
    
    RawSource} = webpack.sources

class CopyWebpackPlugin {
    
    
  constructor(options = {
     
     }) {
    
    
    // 验证options是否符合规范
    validate(schema, options, {
    
    
      name: 'CopyWebpackPlugin'
    })
    this.options = options;
  }

  apply(compiler) {
    
    
    compiler.hooks.thisCompilation.tap('CopyWebpackPlugin', (compilation) => {
    
    
      compilation.hooks.additionalAssets.tapAsync('CopyWebpackPlugin', async (cb) => {
    
    
        // 将from中的资源复制到to中,输出出去
        const {
    
     from, ignore } = this.options;
        const to = this.options.to ? this.options.to : '.';
        
        // context就是webpack配置
        // 运行指令的目录
        const context = compiler.options.context; // process.cwd()
        // 将输入路径变成绝对路径
        const absoluteFrom = path.isAbsolute(from) ? from : path.resolve(context, from);

        // 1. 过滤掉ignore的文件
        // globby(要处理的文件夹,options)
        const paths = await globby(absoluteFrom, {
    
     ignore });

        console.log(paths); // 所有要加载的文件路径数组

        // 2. 读取paths中所有资源
        const files = await Promise.all(
          paths.map(async (absolutePath) => {
    
    
            // 读取文件
            const data = await readFile(absolutePath);
            // basename得到最后的文件名称
            const relativePath = path.basename(absolutePath);
            // 和to属性结合
            // 没有to --> reset.css
            // 有to --> css/reset.css
            const filename = path.join(to, relativePath);

            return {
    
    
              // 文件数据
              data,
              // 文件名称
              filename
            }
          })
        )

        // 3. 生成webpack格式的资源
        const assets = files.map((file) => {
    
    
          const source = new RawSource(file.data);
          return {
    
    
            source,
            filename: file.filename
          }
        })
        
        // 4. 添加compilation中,输出出去
        assets.forEach((asset) => {
    
    
          compilation.emitAsset(asset.filename, asset.source);
        })

        cb();
      })
    })
  }
}
module.exports = CopyWebpackPlugin;

schema.json验证规则如下:

{
    
    
  "type": "object",
  "properties": {
    
    
    "from": {
    
    
      "type": "string"
    },
    "to": {
    
    
      "type": "string"
    },
    "ignore": {
    
    
      "type": "array"
    }
  },
  "additionalProperties": false
}

webpack.config.js代码如下:

const CopyWebpackPlugin = require('./plugins/CopyWebpackPlugin')
module.exports = {
    
    
  plugins: [
    new CopyWebpackPlugin({
    
    
      from: 'public',
      to: 'css',
      ignore: ['**/index.html']
    })
  ]
}

首先我的public目录如下:
在这里插入图片描述

执行npx webpack命令输出如下:
在这里插入图片描述
在这里插入图片描述
插件是成功运行的

plugin和loader编写的思路大致就是这样了,更加深入的使用,无非就是熟读文档和参与到更多的实践中去

猜你喜欢

转载自blog.csdn.net/fesfsefgs/article/details/119983556