淺析Babel-loader實(shí)現(xiàn)原理

在React項(xiàng)目中使用Webpack進(jìn)行打包構(gòu)建時(shí),需要在Webpack的配置文件配置Babel-loader,來將es6轉(zhuǎn)換為es5以及將jsx轉(zhuǎn)換為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的相關(guān)知識(shí)。

## 一、開發(fā)Webpack的loader

loader用于對(duì)模塊的源代碼進(jìn)行轉(zhuǎn)換, 可以使你在 import或"加載"模塊時(shí)預(yù)處理文件,將文件從不同的語言(如TypeScript)轉(zhuǎn)換為 JavaScript,或?qū)?nèi)聯(lián)圖像轉(zhuǎn)換為 data URL,拓展了webpack的功能。

### 1、loader的調(diào)用順序

Webpack根據(jù)用戶配置的入口路徑,查找讀取文件內(nèi)容并根據(jù)文件的擴(kuò)展名,調(diào)用配置文件中,用戶設(shè)置的loader對(duì)文件內(nèi)容進(jìn)行轉(zhuǎn)換,若同類型文件用戶配置了多了個(gè)loader,Webpack會(huì)反序調(diào)用loader,先調(diào)用最后一個(gè)loader,最后才會(huì)調(diào)用第一個(gè)loader,Webpack的compiler得到最后一個(gè)loader產(chǎn)生的處理結(jié)果。

### 2、loaedr的定義

loader其實(shí)就是一個(gè)node模塊,它會(huì)導(dǎo)出一個(gè)函數(shù)。如下示例,就是個(gè)最簡(jiǎn)單的loader,只是它什么也沒做:

``` javaScript

module.exports = function (source: string) {

? return source;

}

```

形參source為文件內(nèi)容或者上一個(gè)loader轉(zhuǎn)換后的內(nèi)容。

### 3、loader上下文

開發(fā)loader的過程中需要使用相關(guān)上下文來獲取代碼文件的相關(guān)信息以及和Webpack交互等,所謂的loader上下文指的是在loader內(nèi)使用this可以訪問的一些方法或?qū)傩浴?/p>

Webpack官網(wǎng)中介紹了很多this可以獲取到上下文屬性,本文只介紹將會(huì)用到的兩個(gè):

> this.async: <br />

告訴loader-runner這個(gè)loader將會(huì)異步地回調(diào),并返回一個(gè)callback方法,供返回?cái)?shù)據(jù)時(shí)調(diào)用。

> this.request: <br />

被解析出來的request 字符串。

> this.query: <br />

> 1)如果loader配置了options ,this.query 就指向這個(gè) option 對(duì)象。 <br />

> 2)如果 loader 中沒有 options,而是以 query 字符串作為參數(shù)調(diào)用時(shí),this.query 就是一個(gè)以 ? 開頭的字符串。

### 4、同步、異步loader

根據(jù)loader本身的特性,loader分為同步、異步的。

(1)同步loader:當(dāng)loader轉(zhuǎn)換文件內(nèi)容時(shí)是同步的得到最終轉(zhuǎn)換結(jié)果的。

``` javaScript

module.exports = function (source: string) {


? ? return source.replace(/clog/g,'console.log');

}

```

同步loader的返回方式有兩種:

1)直接使用return返回一個(gè)值,也就是轉(zhuǎn)換后的文件內(nèi)容。

2)通過調(diào)用this.callback方法返回多個(gè)值,callback方法的參數(shù)如下:

``` javaScript

this.callback(

? err: Error | null,

? content: string | Buffer,

? sourceMap?: SourceMap,

? meta?: any

);

```

第一個(gè)參數(shù)是:Error或者null <br />

第二個(gè)參數(shù)是:轉(zhuǎn)換后的內(nèi)容為string 或者 Buffer。 <br />

第三個(gè)參數(shù)可選的:是一個(gè)可以被這個(gè)模塊解析的 source map。將會(huì)傳遞給下一個(gè)loader或者Webpack,怎么獲取sourceMap,后面講。 <br />

第四個(gè)選項(xiàng)可選的:可以是任何數(shù)據(jù),只在loader間傳遞共享, 最終不會(huì)傳給webpack。 <br />

```? javaScript

module.exports = function(content, map, meta) {

? this.callback(null, someSyncOperation(content), map, meta);

? return; // 當(dāng)調(diào)用 callback() 時(shí)總是返回 undefined

};

```

(2)異步loader:當(dāng)loader轉(zhuǎn)換文件內(nèi)容時(shí)是經(jīng)過異步處理才得到的最終轉(zhuǎn)換結(jié)果的。如下示例:

``` javaScript

module.exports = function (source: string) {

? ? // 使用this.async()獲取callback方法,以便于異步操作完成后,調(diào)用callback把結(jié)果返回給Webpack

? ? var callback = this.async();

? ? var headerPath = path.resolve('header.js');


? ? fs.readFile(headerPath, 'utf-8', function(err, header) {

? ? ? ? if(err) return callback(err);


? ? ? ? // 異步操作完成,調(diào)用callback把結(jié)果返回給Webpack

? ? ? ? callback(null, header + "\n" + source);

? ? });

}

```

上面的示例代碼中,通過this.async()返回this.callback()回調(diào)函數(shù),并來指示 loader runner等待異步結(jié)果,在異步讀取文件成功后執(zhí)行callback返回轉(zhuǎn)換后的內(nèi)容。

### 5、loader工具庫

loader-utils包提供了許多有用的工具,常用api

有:getOptions獲取傳遞給loader的選項(xiàng)。schema-utils包可以校驗(yàn)獲取到的options與我們?cè)O(shè)置的JSON Schema結(jié)構(gòu)是否一致,用于保證用戶設(shè)置的loader選項(xiàng)格式與要求的一致。

## 二、Babel-loader的實(shí)現(xiàn)

### 1、babel.transform的使用

在Babel-loader中es6轉(zhuǎn)換為es5實(shí)際上是使用Babel-core的transform方法來進(jìn)行代碼轉(zhuǎn)換的。先來看看

``` javaScript

babel.transform(code: string, options?: Object, callback: Function)

```

參數(shù)說明:

1)code:為要轉(zhuǎn)換的代碼

2)options:為傳入的選項(xiàng)操作

```

{

? ? filename, // 文件名

? ? plugins, // 轉(zhuǎn)碼時(shí)需要的插件

? ? presets, // 編譯環(huán)境

? ? sourceMaps, // 是否需要sourceMap

? ? inputSourceMap // 調(diào)用時(shí)傳入的sourceMap

}

```

本文只介紹文中需要使用的幾個(gè)屬性,詳細(xì)的介紹可以查看[Babel options官網(wǎng)](https://babeljs.io/docs/en/options)。其中需要說明一下,當(dāng)Webpack配置文件中將devtool設(shè)置為 'eval-source-map'時(shí),最終Webpack編譯出的代碼中才會(huì)顯示sourcemap。

3)callback:

``` javaScript

/**

* result:{

*? code, 轉(zhuǎn)換后的代碼

*? map, 資源映射sourceMap

*? ast? ast語法樹

* }

**/

callback(err, result)

```

### 2、實(shí)現(xiàn)代碼啦~

講了那么多背景知識(shí),終于開始編寫代碼了,等等!在開始對(duì)于編寫loader之前,還需要了解以下開發(fā)loader的準(zhǔn)則,比如:模塊化的輸出、確保無狀態(tài)等。這些大家就自己去Webpack官網(wǎng)上查看吧,本文不在詳細(xì)講解,看代碼啦:

``` 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檢驗(yàn)optins結(jié)構(gòu)

? ? validateOptions(schema, babelOptions, {

? ? ? ? name: "Babel loader",

? ? });


? ? // 調(diào)用babel.transform進(jìn)行轉(zhuǎn)碼

? ? babel.transform(source, babelOptions, function(err, result) {

? ? ? ? // 將結(jié)果返回給Webpack

? ? ? ? callback(null, result.code, result.map)

? ? })

}

```

本文只是實(shí)現(xiàn)了Babel-loader很簡(jiǎn)單的功能,相較于[Babel-loader](https://github.com/babel/babel-loader)的源碼少了很多,異常處理、options選項(xiàng)兼容處理、緩存處理等。

## 三、編譯JSX

### 1、babel插件

babel插件也就是在調(diào)用transform方法時(shí)在options中設(shè)置的plugins,插件的命名格式為:babel-plugin-xxxx,在Webpack中配置時(shí)可以簡(jiǎn)寫為:xxxx,以自定義插件balel-plugin-noconsole為例,Webpack配置如下:

``` javaScript

{

? plugins: [

? ? "noconsole",

? ? {

? ? ? ? // 這些屬性可以隨意,最后可以在opts里面訪問得到

? ? ? ? "key": "value"

? ? }

? ]

}

```

babel插件實(shí)際上是一個(gè)對(duì)象,它包括一個(gè)屬性visitor(屬性名不能改),visitor是AST語法樹的訪問器的。

``` javaScript

// @babel/types 工具類,主要用途是在創(chuàng)建AST的過程中判斷各種語法的類型

const types = require("@babel/types")

var babel-plugin-noconsole = {

? ? visitor: { // 訪問器,名稱必須是visitor

? ? ? ? ExpressionStatement: function(path){

? ? ? ? ? ? // 獲取到expression節(jié)點(diǎn)

? ? ? ? ? ? var expression = path.node.expression;

? ? ? ? ? ? if(types.isCallExpression(expression)) {

? ? ? ? ? ? ? ? // 對(duì)詞類型節(jié)點(diǎn)進(jìn)行處理...

? ? ? ? ? ? }

? ? ? ? }

? ? }

}

module.exports = babel-plugin-noconsole;

```

當(dāng)Babel.transfrom轉(zhuǎn)換為代碼時(shí),經(jīng)過詞法分析、語法分析得到代碼對(duì)應(yīng)的AST語法樹,若此時(shí)設(shè)置了Babel插件的話,會(huì)對(duì)AST語法樹進(jìn)行遍歷,在遍歷過程中根據(jù)插件中編寫的訪問器獲取到對(duì)應(yīng)類型的節(jié)點(diǎn),然后插件就可以對(duì)該節(jié)點(diǎn)進(jìn)行處理。上面的代碼中通過訪問器查詢到ExpressionStatement類型的結(jié)點(diǎn),進(jìn)行一系列處理。

那么編譯JSX也就是在調(diào)用Babel.transfrom進(jìn)行轉(zhuǎn)碼時(shí)設(shè)置options的插件為transform-react-jsx:

``` javaScript

var babelOptions = {

? ? presets: ['@babel/preset-env'],

? ? plugins: ["@babel/plugin-transform-react-jsx"]

};


// 調(diào)用babel.transform進(jìn)行轉(zhuǎn)碼

babel.transform(source, babelOptions, function(err, result) {

? ? // 將結(jié)果返回給Webpack

? ? callback(null, result.code, result.map)

})

```

原理同上面所講述的。

## 五、本地loader的使用

我們要如何指定使用自己的loader呢?官網(wǎng)中介紹了以下三種方式:

1、匹配(test)單個(gè) loader,你可以簡(jiǎn)單通過在Webpack配置文件的 rule對(duì)象設(shè)置path.resolve指向這個(gè)本地文件或者使用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)多個(gè) loaders,你可以使用resolveLoader.modules配置,webpack 將會(huì)從這些目錄中搜索這些loaders例。如,如果你的項(xiàng)目中有一個(gè) /loaders本地目錄:


``` javaScript

resolveLoader: {

? modules: [

? ? 'node_modules',

? ? path.resolve(__dirname, 'loaders')

? ]

}

```

3、如果loader開發(fā)為單獨(dú)的npm包,可以通過npm link來將其關(guān)聯(lián)到你要測(cè)試的項(xiàng)目。

1)在自定義的loader包的package.json中進(jìn)行配置。

2)在自定義的loader包目錄下,執(zhí)行npm link,將loader鏈接到全局

3)在測(cè)試項(xiàng)目目錄中執(zhí)行npm link loadername,這樣在測(cè)試項(xiàng)目中就可以通過require等方式引入自定義loader了

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容