在React项目中使用Webpack进行打包构建时,需要在Webpack的配置文件配置Babel-loader,来将es6转换为es5以及将jsx转换为js文件:
```javeScript
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
"plugins": [
["@babel/plugin-transform-react-jsx", { pragma: 'h'}]
],
"presets": [
'@babel/preset-env'
]
}
}
}
```
那么Babel-loader是怎么做到这些的呢?在解答这一问题之前,我们先来看看Webpack的loader的相关知识。
## 一、开发Webpack的loader
loader用于对模块的源代码进行转换, 可以使你在 import或"加载"模块时预处理文件,将文件从不同的语言(如TypeScript)转换为 JavaScript,或将内联图像转换为 data URL,拓展了webpack的功能。
### 1、loader的调用顺序
Webpack根据用户配置的入口路径,查找读取文件内容并根据文件的扩展名,调用配置文件中,用户设置的loader对文件内容进行转换,若同类型文件用户配置了多了个loader,Webpack会反序调用loader,先调用最后一个loader,最后才会调用第一个loader,Webpack的compiler得到最后一个loader产生的处理结果。
### 2、loaedr的定义
loader其实就是一个node模块,它会导出一个函数。如下示例,就是个最简单的loader,只是它什么也没做:
``` javaScript
module.exports = function (source: string) {
return source;
}
```
形参source为文件内容或者上一个loader转换后的内容。
### 3、loader上下文
开发loader的过程中需要使用相关上下文来获取代码文件的相关信息以及和Webpack交互等,所谓的loader上下文指的是在loader内使用this可以访问的一些方法或属性。
Webpack官网中介绍了很多this可以获取到上下文属性,本文只介绍将会用到的两个:
> this.async:
告诉loader-runner这个loader将会异步地回调,并返回一个callback方法,供返回数据时调用。
> this.request:
被解析出来的request 字符串。
> this.query:
> 1)如果loader配置了options ,this.query 就指向这个 option 对象。
> 2)如果 loader 中没有 options,而是以 query 字符串作为参数调用时,this.query 就是一个以 ? 开头的字符串。
### 4、同步、异步loader
根据loader本身的特性,loader分为同步、异步的。
(1)同步loader:当loader转换文件内容时是同步的得到最终转换结果的。
``` javaScript
module.exports = function (source: string) {
return source.replace(/clog/g,'console.log');
}
```
同步loader的返回方式有两种:
1)直接使用return返回一个值,也就是转换后的文件内容。
2)通过调用this.callback方法返回多个值,callback方法的参数如下:
``` javaScript
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
```
第一个参数是:Error或者null
第二个参数是:转换后的内容为string 或者 Buffer。
第三个参数可选的:是一个可以被这个模块解析的 source map。将会传递给下一个loader或者Webpack,怎么获取sourceMap,后面讲。
第四个选项可选的:可以是任何数据,只在loader间传递共享, 最终不会传给webpack。
``` javaScript
module.exports = function(content, map, meta) {
this.callback(null, someSyncOperation(content), map, meta);
return; // 当调用 callback() 时总是返回 undefined
};
```
(2)异步loader:当loader转换文件内容时是经过异步处理才得到的最终转换结果的。如下示例:
``` javaScript
module.exports = function (source: string) {
// 使用this.async()获取callback方法,以便于异步操作完成后,调用callback把结果返回给Webpack
var callback = this.async();
var headerPath = path.resolve('header.js');
fs.readFile(headerPath, 'utf-8', function(err, header) {
if(err) return callback(err);
// 异步操作完成,调用callback把结果返回给Webpack
callback(null, header + "\n" + source);
});
}
```
上面的示例代码中,通过this.async()返回this.callback()回调函数,并来指示 loader runner等待异步结果,在异步读取文件成功后执行callback返回转换后的内容。
### 5、loader工具库
loader-utils包提供了许多有用的工具,常用api
有:getOptions获取传递给loader的选项。schema-utils包可以校验获取到的options与我们设置的JSON Schema结构是否一致,用于保证用户设置的loader选项格式与要求的一致。
## 二、Babel-loader的实现
### 1、babel.transform的使用
在Babel-loader中es6转换为es5实际上是使用Babel-core的transform方法来进行代码转换的。先来看看
``` javaScript
babel.transform(code: string, options?: Object, callback: Function)
```
参数说明:
1)code:为要转换的代码
2)options:为传入的选项操作
```
{
filename, // 文件名
plugins, // 转码时需要的插件
presets, // 编译环境
sourceMaps, // 是否需要sourceMap
inputSourceMap // 调用时传入的sourceMap
}
```
本文只介绍文中需要使用的几个属性,详细的介绍可以查看[Babel options官网](https://babeljs.io/docs/en/options)。其中需要说明一下,当Webpack配置文件中将devtool设置为 'eval-source-map'时,最终Webpack编译出的代码中才会显示sourcemap。
3)callback:
``` javaScript
/**
* result:{
* code, 转换后的代码
* map, 资源映射sourceMap
* ast ast语法树
* }
**/
callback(err, result)
```
### 2、实现代码啦~
讲了那么多背景知识,终于开始编写代码了,等等!在开始对于编写loader之前,还需要了解以下开发loader的准则,比如:模块化的输出、确保无状态等。这些大家就自己去Webpack官网上查看吧,本文不在详细讲解,看代码啦:
``` javaScript
var babel = require("babel-core");
import { getOptions } from 'loader-utils';
import validateOptions from 'schema-utils';
var schema = {
"type": "object",
"properties": {
"cacheDirectory": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "string"
}
],
"default": false
},
"cacheIdentifier": {
"type": "string"
},
"cacheCompression": {
"type": "boolean",
"default": true
},
"customize": {
"type": "string",
"default": null
}
},
"additionalProperties": true
}
module.exports = function (source, inputSourceMap) {
// 异步loader 使用this.async获取callback
var callback = this.async();
// 使用loader-utils的getOptions获取用户配置
var babelOptions = getOptions(this) || {
presets: ['@babel/preset-env'],
inputSourceMap: inputSourceMap,
filename: this.request.split('!')[1].split('/').pop(),
sourceMaps: true
};
// 使用schema-utils检验optins结构
validateOptions(schema, babelOptions, {
name: "Babel loader",
});
// 调用babel.transform进行转码
babel.transform(source, babelOptions, function(err, result) {
// 将结果返回给Webpack
callback(null, result.code, result.map)
})
}
```
本文只是实现了Babel-loader很简单的功能,相较于[Babel-loader](https://github.com/babel/babel-loader)的源码少了很多,异常处理、options选项兼容处理、缓存处理等。
## 三、编译JSX
### 1、babel插件
babel插件也就是在调用transform方法时在options中设置的plugins,插件的命名格式为:babel-plugin-xxxx,在Webpack中配置时可以简写为:xxxx,以自定义插件balel-plugin-noconsole为例,Webpack配置如下:
``` javaScript
{
plugins: [
"noconsole",
{
// 这些属性可以随意,最后可以在opts里面访问得到
"key": "value"
}
]
}
```
babel插件实际上是一个对象,它包括一个属性visitor(属性名不能改),visitor是AST语法树的访问器的。
``` javaScript
// @babel/types 工具类,主要用途是在创建AST的过程中判断各种语法的类型
const types = require("@babel/types")
var babel-plugin-noconsole = {
visitor: { // 访问器,名称必须是visitor
ExpressionStatement: function(path){
// 获取到expression节点
var expression = path.node.expression;
if(types.isCallExpression(expression)) {
// 对词类型节点进行处理...
}
}
}
}
module.exports = babel-plugin-noconsole;
```
当Babel.transfrom转换为代码时,经过词法分析、语法分析得到代码对应的AST语法树,若此时设置了Babel插件的话,会对AST语法树进行遍历,在遍历过程中根据插件中编写的访问器获取到对应类型的节点,然后插件就可以对该节点进行处理。上面的代码中通过访问器查询到ExpressionStatement类型的结点,进行一系列处理。
那么编译JSX也就是在调用Babel.transfrom进行转码时设置options的插件为transform-react-jsx:
``` javaScript
var babelOptions = {
presets: ['@babel/preset-env'],
plugins: ["@babel/plugin-transform-react-jsx"]
};
// 调用babel.transform进行转码
babel.transform(source, babelOptions, function(err, result) {
// 将结果返回给Webpack
callback(null, result.code, result.map)
})
```
原理同上面所讲述的。
## 五、本地loader的使用
我们要如何指定使用自己的loader呢?官网中介绍了以下三种方式:
1、匹配(test)单个 loader,你可以简单通过在Webpack配置文件的 rule对象设置path.resolve指向这个本地文件或者使用resolveLoader:
```
// webpack.config.js
module: {
rules: [
{
test: /\.js$/
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {/* ... */}
}
]
}
]
}
// 或者使用resolveLoader
resolveLoader: {
alias: {
"babel-loader": resolve('./build/babel-loader.js')
}
}
```
2、匹配(test)多个 loaders,你可以使用resolveLoader.modules配置,webpack 将会从这些目录中搜索这些loaders例。如,如果你的项目中有一个 /loaders本地目录:
``` javaScript
resolveLoader: {
modules: [
'node_modules',
path.resolve(__dirname, 'loaders')
]
}
```
3、如果loader开发为单独的npm包,可以通过npm link来将其关联到你要测试的项目。
1)在自定义的loader包的package.json中进行配置。
2)在自定义的loader包目录下,执行npm link,将loader链接到全局
3)在测试项目目录中执行npm link loadername,这样在测试项目中就可以通过require等方式引入自定义loader了