开发一个自己的 Webpack Loader

·  阅读 357

Webpack loader

一、什么是loader?

​ loader 是一个文件加载器,能够加载不同的资源,并对这些文件进行操作,如编译,压缩,最终打包到指定的文件。

  • 处理一个文件可以使用多个loader进行解析,loader的加载顺序是相反的,会从最后一个loader向上执行
  • 每个loader文件都提供一个函数,并且参数是解析文件的内容,当执行多个loader时,每个loader的参数是上个loader的执行结果
二、手写一个loader

​ 需求:删除Javascript代码中所有的console.log

​ 目录结构

 |-- package-lock.json
 |-- package.json
 |-- README.md
 |-- webpack.config.js      // webpack 配置
 |-- loaders
 |   |-- del-log-loader.js  // 删除console.log的loader
 |   |-- tranform-async.js  // 处理异步loader
 |-- src
     |-- index.js           // 目标文件
复制代码
1. 实现删除代码中的console.log
1.1 webpack.config.js 配置导出如下
const path = require('path')
module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js',
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.js'
  },
  module: {
    rules: [{
      test: /\.js$/,
      use: [
        {
          loader: path.resolve(__dirname, 'loaders/del-log-loader.js')
        }
      ]
    }],
  },
};   
复制代码
1.2 src/index.js 源码 如下
class Animation {
  run(a, b) {
    console.log('running...');
    return a + b;
  }
}
const an = new Animation();
an.run(1, 2);
复制代码
1.3 编写del-log-loader,该loader主要功能接收目标js,将代码中的console.log去掉,并返回处理后的结果。
module.exports = function (source) {
  const reg = /console.log\([\s\S]*?\);/g;
  source = source.replace(reg, '');
  return source;
}
复制代码
1.4 在package.json文件中添加"build": "webpack" 指令
"scripts": {
+   "build": "webpack",
    "test": "echo \"Error: no test specified\" && exit 1"
 }
复制代码
1.5 执行npm run build命令,得到处理后的结果
eval("\r\nclass Animation {\r\n  run(a, b) {\r\n    \r\n    return a + b;\r\n  }\r\n}\r\nconst an = new Animation()\r\nan.run(1, 2)\n\n//# sourceURL=webpack://demo1/./src/index.js?");
复制代码
2. loader 返回多个参数

通常写loader时,有时需要给下一个loader传额外参数,由于return返回值局限性,然幸运的是loader中提供了一个callback 会掉函数接受多个参数(注意:前三个参数必须是error,source和map)

2.1 修改del-log-loader
module.exports = function (source) {
  const reg = /console.log\([\s\S]*?\);/g;
+ const error = null
+ const map = {
+   name: '张三'
+ }
  source = source.replace(reg, '');
+ this.callback(error, source, map)
}
复制代码
2.2 添加另一个 loader ,检验结果
module.exports = function (source, map) {
  console.log(map)
  return source
}
复制代码
3.校验loader中的options参数

在webpack的配置中,通常会给loader传入options以便在loader中使用,为了获取并校验options,官方提供了两个工具包loader-utilsscheme-utils。继续完善del-log-loader,为了让使用者更加灵活,可以通过传参的方式决定是否删除console.log

3.1 webpack.config.js 修改配置
 rules: [{
      test: /\.js$/,
      use: [
        {
          loader: path.resolve(__dirname, 'loaders/tranform-async.js')
        },
        {
          loader: path.resolve(__dirname, 'loaders/del-log-loader.js'),
+          options: {
+            isDelLog: process.env.NODE_ENV === "production"
+          }
        }
      ]
    }],
复制代码
3.2 修改del-log-loader.js,增加获取和校验options的代码
+ const schemaUtils = require('schema-utils'); // 校验options
+ const loaderUtils = require('loader-utils'); // 获取options
+ const schema = {
+  type: 'object',
+  properties: {
+    isDelLog: { type: 'boolean' }
+  },
+  additionalProperties: false
+};
module.exports = function (source) {
+  const options = loaderUtils.getOptions(this);
+  schemaUtils.validate(schema, options, 'del-log-loader');
+  const { isDelLog } = options;
+  if (isDelLog) {
+    const reg = /console.log\([\s\S]*?\);/g;
+    source = source.replace(reg, '');
+  }
  const error = null
  const map = {
    name: '张三'
  }
  this.callback(error, source, map)
}
复制代码
4. transform-loader开发

transform-loader 是一个异步loader,主要作用是将es6语法转为es5,并将转换后的代码返回。异步loader和同步loader的开发基本一致,区别就是异步loader必须在返回前调用loader的上下文中的async方法。

继续修改webpack.config.js,在rules中增加transform-loader,把transform-loader放在del-log-loader前面,因为普通loader的执行顺序是从后往前,并依次传入返回的值。

use: [
+    {
+        loader: path.resolve(__dirname, 'loaders/tranform-async.js')
+    },
    {
        loader: path.resolve(__dirname, 'loaders/del-log-loader.js'),
        options: {
            isDelLog: process.env.NODE_ENV === "production"
        }
    }
]
复制代码

编辑transform-loader.js,接受del-log-loader返回的source和map进行转换后返回。

const babel = require('@babel/core');  // 使用babel进行转换
module.exports = function (source) {
  const callback = this.async();
  babel.transform(source, {
    presets: ['@babel/preset-env']
  }, (err, result) => {
    const { code } = result;
    callback(err, code, map);
  });
}
复制代码

得到结果

var Animation = /*#__PURE__*/function () {
  function Animation() {
    _classCallCheck(this, Animation);
  }
  _createClass(Animation, [{
    key: "run",
    value: function run(a, b) {
      console.log('running...');
      return a + b;
    }
  }]);
  return Animation;
}();

var an = new Animation();
an.run(1, 2);
复制代码
总结

从一个loader的基本实现,参数的传递,以及异步loader开发,在平日开发中基本能够应付自定义一个loader的所需的一些知识,更加详细可以看webpack的官网。

分类:
前端
标签: