babel-loader实现

629 阅读3分钟

自定义 babel-loader 前,先了解一些前置知识吧。

1. 准备工作

首先,创建一个项目,并初始化 package.json 文件。

mkdir loaders-demo

cd loaders-demo && npm init -y

根目录下创建 src/index.js 文件,作为入口文件。

mkdir src 

cd src && touch index.js

根目录下创建 loaders 目录,作为自定义 loader 的目录, 在其下分别创建 loader-one.js,loader-two.js,loader-three.js 文件。

mkdir loaders 

cd loaders && touch loader-one.js && touch loader-two.js && touch loader-three.js

安装 webpack 依赖。

npm i webpack webpack-cli --save-dev

根目录下创建 webpack.config.js 文件,并编写文件入口和出口。

touch webpack.config.js
/**
 * @file webapck配置文件
 * @module webpack.config.js
 * @version 0.1.0
 * @author yueluo <yueluo.yang@qq.com>
 * @time 2020-06-24
 */

/**
 * @requires node_modules/path node内置模块
 */
const path = require('path');

// 导出webpack配置
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
}

根目录下执行 npx webpack 命令,如果出现 dist 目录和 bundle.js 文件,说明基础配置已经完成。

w1.png

2. 如何使用自定义loader

基础环境已经配置完毕,那么如何使用已经编写好的 loader ?

首先编写 loader-one.js 文件,

/**
 * @file loader-one
 * @module loaders/loader-one
 * @version 0.1.0
 * @author yueluo <yueluo.yang@qq.com>
 * @time 2020-06-24
 */

/**
 * @description 自定义loader
 * @param {string} sourceCode - 源代码 
 * @return {string}
 */
function loader (sourceCode) {
  console.log('loader one.');
  return sourceCode;
}

// 导出自定义loader
module.exports = loader;

使用自定义 loader 有以下3种方式。

(1)绝对路径引用

webpack.config.js 配置如下。

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: path.resolve(__dirname, 'loaders', 'loader-one')
      }
    ]
  }
}

我们可以使用 path.resolve 方法编写绝对路径进行引入。

(2)别名配置

使用 resolveLoader 中的 alias 配置。

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    alias: {
      'loader-one': path.resolve(__dirname, 'loaders', 'loader-one')
    }
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'loader-one'
      }
    ]
  }
}

(3) 配置查找范围

使用 resolveLoader 中的 modules 配置。

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')]
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'loader-one'
      }
    ]
  }
}

使用以上任意一种方式都可以实现效果,个人推荐第三种。

3. 如何使用多个自定义loader

首先将 loader-one.js 文件里的代码复制到 loader-two.js 文件和 loader-three.js 文件。

loader-two.js

/**
 * @file loader-two
 * @module loaders/loader-two
 * @version 0.1.0
 * @author yueluo <yueluo.yang@qq.com>
 * @time 2020-06-24
 */

/**
 * @description 自定义loader
 * @param {string} sourceCode - 源代码 
 * @return {string}
 */
function loader (sourceCode) {
  console.log('loader two.');
  return sourceCode;
}

// 导出自定义loader
module.exports = loader;

loader-three.js

/**
 * @file loader-three
 * @module loaders/loader-three
 * @version 0.1.0
 * @author yueluo <yueluo.yang@qq.com>
 * @time 2020-06-24
 */

/**
 * @description 自定义loader
 * @param {string} sourceCode - 源代码 
 * @return {string}
 */
function loader (sourceCode) {
  console.log('loader three.');
  return sourceCode;
}

// 导出自定义loader
module.exports = loader;

使用多个 loader,有两种形式。

(1)字符串的方式

/**
 * @file webapck配置文件
 * @module webpack.config.js
 * @version 0.1.0
 * @author yueluo <yueluo.yang@qq.com>
 * @time 2020-06-24
 */

/**
 * @requires node_modules/path node内置模块
 */
const path = require('path');

// 导出webpack配置
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')]
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          'loader-three',
          'loader-two',
          'loader-one'
        ]
      }
    ]
  }
}

数组中的 loader 的执行顺序是自右向左的,所以 loader-one 应该放在最后一位。

执行 npx webpack 测试如下。

w2.png

(2)对象的方式

第一种方式

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')]
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'loader-three',
          },
          {
            loader: 'loader-two',
          },
          {
            loader: 'loader-one',
          }
        ]
      }
    ]
  }
}

第二种方式

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')]
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'loader-three'
      },
      {
        test: /\.js$/,
        use: 'loader-two'
      },
      {
        test: /\.js$/,
        use: 'loader-one'
      }
    ]
  }
}

在 rules 中的解析顺序是自下向上执行的。

4. 自定义loader的执行顺序

webpack 的 loader 是存在种类划分的,可以划分为 pre、normal、inline、post。

我们可以使用 pre、post 来定义 loader 的执行顺序。

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')]
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'loader-one',
        enforce: 'pre'
      },
      {
        test: /\.js$/,
        use: 'loader-two'
      },
      {
        test: /\.js$/,
        use: 'loader-three',
        enforce: 'post'
      }
    ]
  }
}

通过定义 enforce,可以让数组中的 loader-three 最后执行,让 loader-one 优先执行。

关于上面介绍的类型,normal 就是未定义时的状态,下面我们再说一下 inline-loader。

5. 关于inline-loader

inline-loader 可以理解为在JS文件中引用的 loader。

首先在 loaders 目录下创建并编写测试文件 inline-loader.js。

cd loaders && touch inline-loader.js
/**
 * @file inline-loader
 * @module loaders/inline-loader
 * @version 0.1.0
 * @author yueluo <yueluo.yang@qq.com>
 * @time 2020-06-24
 */

/**
 * @description 自定义loader
 * @param {string} sourceCode - 源代码 
 * @return {string}
 */
function loader (sourceCode) {
  console.log('inline loader.');
  return sourceCode;
}

// 导出自定义loader
module.exports = loader;

然后在 src 目录下创建并编写 a.js 。

cd src && touch a.js
/**
 * @file 行内loader测试文件
 * @module src/a
 * @version 0.1.0
 * @author yueluo <yueluo.yang@qq.com>
 * @time 2020-06-24
 */

const str = 'yueluo';

// 导出定义的字符串
module.exports = str;

我们可以在 index.js 文件中使用 inline-loader。

/**
 * @file 入口文件
 * @module src/index
 * @version 0.1.0
 * @author yueluo <yueluo.yang@qq.com>
 * @time 2020-06-24
 */

console.log('hello yueluo.');

/**
 * @constant {string} 测试的字符串常量
 */
const str = require('inline-loader!./a.js');

console.log(str);

运行 npx webpack 命令命令如下。

w3.png

inline loader 的3种使用语法

(1)loader前添加 !

添加 ! 后,所有的 normal loader 都不会执行。

/**
 * @constant {string} 测试的字符串常量
 */
const str = require('!inline-loader!./a.js');

测试如下。

w4.png

可以看到,loader two 并没有执行。

(2)loader前添加 !!

添加 !! 后,所有的 pre、normal、post loader 都不会执行。

/**
 * @constant {string} 测试的字符串常量
 */
const str = require('!!inline-loader!./a.js');

测试如下。

w5.png

可以看到,编译 a.js 文件时,只执行 inline loader。

(3)loader前添加 -!

添加 -! 后,所有的 pre、normal loader 都不会执行。

/**
 * @constant {string} 测试的字符串常量
 */
const str = require('-!inline-loader!./a.js');

测试如下。

w6.png

可以看到,编译时只执行了 inline 和 post loader。

6. loader的执行阶段

loader 的执行分为两个阶段:pitching 和 normal 阶段。

举个例子,假如配置文件中已经使用3个loader。

use: [
  'a-loader',
  'b-loader',
  'c-loader'
]

pitch 阶段时,会依次执行 a b c 三个 loader。

a-loader -> b-loader -> c-loader

pitch 阶段后,文件作为模块开始被处理,进入 normal 阶段。 normal 阶段,会依次执行 c b a 三个 loader。

c-loader -> b-loader -> a-loader

当然,这样执行的前提是 loader 在pitch时不存在返回值。

打个比方,如果 b-loader 存在返回值,就不再执行 c-loader 的 pitch 阶段,可以起到阻断作用。 执行时,也只会执行 c-loader,其他的 loader 因为阻断,就无法被执行。

口说无凭,下面使用代码测试下。

分别为之前的 loader-one、loader-two、loader-three 添加 pitch 方法。

/**
 * @file loader-one
 * @module loaders/loader-one
 * @version 0.1.0
 * @author yueluo <yueluo.yang@qq.com>
 * @time 2020-06-24
 */

/**
 * @description 自定义loader
 * @param {string} sourceCode - 源代码 
 * @return {string}
 */
function loader (sourceCode) {
  console.log('loader one.');
  return sourceCode;
}

loader.pitch = function () {
  console.log('loader one pitch phase.');
}

// 导出自定义loader
module.exports = loader;
/**
 * @file loader-two
 * @module loaders/loader-two
 * @version 0.1.0
 * @author yueluo <yueluo.yang@qq.com>
 * @time 2020-06-24
 */

/**
 * @description 自定义loader
 * @param {string} sourceCode - 源代码 
 * @return {string}
 */
function loader (sourceCode) {
  console.log('loader two.');
  return sourceCode;
}

loader.pitch = function () {
  console.log('loader two pitch phase.');
}

// 导出自定义loader
module.exports = loader;
/**
 * @file loader-three
 * @module loaders/loader-three
 * @version 0.1.0
 * @author yueluo <yueluo.yang@qq.com>
 * @time 2020-06-24
 */

/**
 * @description 自定义loader
 * @param {string} sourceCode - 源代码 
 * @return {string}
 */
function loader (sourceCode) {
  console.log('loader three.');
  return sourceCode;
}

loader.pitch = function () {
  console.log('loader three pitch phase.');
}

// 导出自定义loader
module.exports = loader;

首先看一下正常情况,运行 npx webpack 命令。

w7.png

可以看到,loader 首先经过 pitch 阶段,然后再进入 normal 阶段。

下面我们为 loader-two 设置返回值。

/**
 * @file loader-two
 * @module loaders/loader-two
 * @version 0.1.0
 * @author yueluo <yueluo.yang@qq.com>
 * @time 2020-06-24
 */

/**
 * @description 自定义loader
 * @param {string} sourceCode - 源代码 
 * @return {string}
 */
function loader (sourceCode) {
  console.log('loader two.');
  return sourceCode;
}

loader.pitch = function () {
  console.log('loader two pitch phase.');
  return 'yueluo';
}

// 导出自定义loader
module.exports = loader;

再次运行 npx webpack 命令。

w8.png

可以看到,在 loader-two 的时候执行被阻断,normal 时只执行了 loader-three。

7. 编写loader时,需要注意的地方

1. 每一个loader都应该只完成一个任务,有利于更好组合,实现链式调用;
2. loader应该是一个单独的模块;
3. loader应该是无状态的,应该保证代码每次执行都是可预测的;

8. 实现babel-loader

哈哈哈 😀 ,弯弯绕绕终于到了正题。下面开始实现一个自己的 babel-loader。

(1)准备工作

因为需要自己实现 babel-loader,所以只安装 babel-loader 的依赖模块。

npm i @babel/core @babel/preset-env --save-dev

此外还需要使用一个 webpack 的工具函数,loader-utils。

npm i loader-utils --save-dev

在 loaders 目录下创建 babel-loader.js 文件

cd loaders && touch babel-loader.js

在 index.js 中编写测试代码

/**
 * @file 入口文件
 * @module src/index
 * @version 0.1.0
 * @author yueluo <yueluo.yang@qq.com>
 * @time 2020-06-24
 */

console.log('hello yueluo.');

/**
 * @class Person
 * @description 用户类
 */
class Person {
  constructor () {
    this.name = 'yueluo';
  }

  /**
   * @description 用户说方法
   * @return {string}
   */
  say () {
    return `Hello ${this.name}`;
  }
}

const person = new Person();
console.log(person.say());

(2)编写webpack.config.js文件

/**
 * @file webapck配置文件
 * @module webpack.config.js
 * @version 0.1.0
 * @author yueluo <yueluo.yang@qq.com>
 * @time 2020-06-24
 */

/**
 * @requires node_modules/path node内置模块
 */
const path = require('path');

// 导出webpack配置
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')]
  },
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [ '@babel/preset-env' ]
          }
        }
      }
    ]
  }
}

webpack.config.js 文件定义使用 babel-loader 以及配置项,并且开启源码调试。

(3)编写babel-loader核心代码

/**
 * @file babel-loader
 * @module loaders/babel-loader
 * @version 0.1.0
 * @author yueluo <yueluo.yang@qq.com>
 * @time 2020-06-24
 */

/**
 * @requires node_modules/@babel/core babel核心代码
 * @requires node_modules/loader-utils webpack工具
 */
const babel = require('@babel/core'),
      loaderUtils = require('loader-utils');

/**
 * @description 自定义loader
 * @param {string} sourceCode - 源代码 
 * @return {string}
 */
function loader (sourceCode) {
  const options = loaderUtils.getOptions(this),
        callback = this.async();

  /**
   * @description 转换代码
   * @property {object} options - 配置文件中的配置项
   * @property {boolen} sourceMap - 是否开启源码调试
   * @property {string} filename - 源码文件的名称
   */
  babel.transform(sourceCode, {
    ...options,
    sourceMap: true,
    filename: this.resourcePath.split('/').pop()
  }, (err, result) => {
    if (err) {
      return callback(err);
    }

    callback(null, result.code, result.map);
  });

  return sourceCode;
}

// 导出自定义loader
module.exports = loader;

需要注意的一点是,转换代码时,是异步操作,需要用 callback 的方式返回处理后的值。

ok,大功告成,下面运行 npx webpack 命令进行测试。

w9.png

查看 bundle.js 文件内容如下。

w10.png

9. 总结

本篇文章介绍了编写 loader 需要注意的要点,并且实现了自己的 babel-loader。 成就感满满啊,有木有!本人后续将持续更新其他文章,希望能对大家有所帮助。😊

另外,感兴趣的同学也可以关注下我的小程序(开发中),月落攻城狮。


minprogram.png