浅析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: <br />

告诉loader-runner这个loader将会异步地回调,并返回一个callback方法,供返回数据时调用。

> this.request: <br />

被解析出来的request 字符串。

> this.query: <br />

> 1)如果loader配置了options ,this.query 就指向这个 option 对象。 <br />

> 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 <br />

第二个参数是:转换后的内容为string 或者 Buffer。 <br />

第三个参数可选的:是一个可以被这个模块解析的 source map。将会传递给下一个loader或者Webpack,怎么获取sourceMap,后面讲。 <br />

第四个选项可选的:可以是任何数据,只在loader间传递共享, 最终不会传给webpack。 <br />

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

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,321评论 6 543
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,559评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,442评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,835评论 1 317
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,581评论 6 412
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,922评论 1 328
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,931评论 3 447
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,096评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,639评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,374评论 3 358
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,591评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,104评论 5 364
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,789评论 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,196评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,524评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,322评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,554评论 2 379