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 文件,说明基础配置已经完成。
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 测试如下。
(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 命令命令如下。
inline loader 的3种使用语法
(1)loader前添加 !
添加 ! 后,所有的 normal loader 都不会执行。
/**
* @constant {string} 测试的字符串常量
*/
const str = require('!inline-loader!./a.js');
测试如下。
可以看到,loader two 并没有执行。
(2)loader前添加 !!
添加 !! 后,所有的 pre、normal、post loader 都不会执行。
/**
* @constant {string} 测试的字符串常量
*/
const str = require('!!inline-loader!./a.js');
测试如下。
可以看到,编译 a.js 文件时,只执行 inline loader。
(3)loader前添加 -!
添加 -! 后,所有的 pre、normal loader 都不会执行。
/**
* @constant {string} 测试的字符串常量
*/
const str = require('-!inline-loader!./a.js');
测试如下。
可以看到,编译时只执行了 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 命令。
可以看到,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 命令。
可以看到,在 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 命令进行测试。
查看 bundle.js 文件内容如下。
9. 总结
本篇文章介绍了编写 loader 需要注意的要点,并且实现了自己的 babel-loader。
成就感满满啊,有木有!本人后续将持续更新其他文章,希望能对大家有所帮助。😊