浅析Babel-loader实现原理

在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了

你可能感兴趣的:(浅析Babel-loader实现原理)