babel-loader实现

自定义 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。
成就感满满啊,有木有!本人后续将持续更新其他文章,希望能对大家有所帮助。😊