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

在React項(xiàng)目中使用Webpack進(jìn)行打包構(gòu)建時(shí),需要在Webpack的配置文件配置Babel-loader,來(lái)將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是怎么做到這些的呢偎血?在解答這一問(wèn)題之前,我們先來(lái)看看Webpack的loader的相關(guān)知識(shí)着绷。

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

loader用于對(duì)模塊的源代碼進(jìn)行轉(zhuǎn)換赖舟, 可以使你在 import或"加載"模塊時(shí)預(yù)處理文件蓬戚,將文件從不同的語(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)換讲衫,若同類(lèi)型文件用戶配置了多了個(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,只是它什么也沒(méi)做:

``` javaScript

module.exports = function (source: string) {

? return source;

}

```

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

### 3渴肉、loader上下文

開(kāi)發(fā)loader的過(guò)程中需要使用相關(guān)上下文來(lái)獲取代碼文件的相關(guān)信息以及和Webpack交互等,所謂的loader上下文指的是在loader內(nèi)使用this可以訪問(wèn)的一些方法或?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 />

被解析出來(lái)的request 字符串乌奇。

> this.query: <br />

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

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

### 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)通過(guò)調(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)過(guò)異步處理才得到的最終轉(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);

? ? });

}

```

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

### 5征绸、loader工具庫(kù)

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方法來(lái)進(jìn)行代碼轉(zhuǎn)換的祝拯。先來(lái)看看

``` javaScript

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

```

參數(shù)說(shuō)明:

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)。其中需要說(shuō)明一下佳头,當(dāng)Webpack配置文件中將devtool設(shè)置為 'eval-source-map'時(shí)鹰贵,最終Webpack編譯出的代碼中才會(huì)顯示sourcemap。

3)callback:

``` javaScript

/**

* result:{

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

*? map, 資源映射sourceMap

*? ast? ast語(yǔ)法樹(shù)

* }

**/

callback(err, result)

```

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

講了那么多背景知識(shí)碉输,終于開(kāi)始編寫(xiě)代碼了,等等亭珍!在開(kāi)始對(duì)于編寫(xiě)loader之前敷钾,還需要了解以下開(kāi)發(fā)loader的準(zhǔn)則,比如:模塊化的輸出肄梨、確保無(wú)狀態(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)寫(xiě)為:xxxx,以自定義插件balel-plugin-noconsole為例主慰,Webpack配置如下:

``` javaScript

{

? plugins: [

? ? "noconsole",

? ? {

? ? ? ? // 這些屬性可以隨意嚣州,最后可以在opts里面訪問(wèn)得到

? ? ? ? "key": "value"

? ? }

? ]

}

```

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

``` javaScript

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

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

var babel-plugin-noconsole = {

? ? visitor: { // 訪問(wèn)器,名稱(chēng)必須是visitor

? ? ? ? ExpressionStatement: function(path){

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

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

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

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

? ? ? ? ? ? }

? ? ? ? }

? ? }

}

module.exports = babel-plugin-noconsole;

```

當(dāng)Babel.transfrom轉(zhuǎn)換為代碼時(shí)藐不,經(jīng)過(guò)詞法分析匀哄、語(yǔ)法分析得到代碼對(duì)應(yīng)的AST語(yǔ)法樹(shù),若此時(shí)設(shè)置了Babel插件的話雏蛮,會(huì)對(duì)AST語(yǔ)法樹(shù)進(jìn)行遍歷涎嚼,在遍歷過(guò)程中根據(jù)插件中編寫(xiě)的訪問(wèn)器獲取到對(duì)應(yīng)類(lèi)型的節(jié)點(diǎn),然后插件就可以對(duì)該節(jié)點(diǎn)進(jìn)行處理挑秉。上面的代碼中通過(guò)訪問(wèn)器查詢到ExpressionStatement類(lèi)型的結(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)單通過(guò)在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開(kāi)發(fā)為單獨(dú)的npm包走触,可以通過(guò)npm link來(lái)將其關(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)目中就可以通過(guò)require等方式引入自定義loader了

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末惫皱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子尤莺,更是在濱河造成了極大的恐慌旅敷,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,948評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件颤霎,死亡現(xiàn)場(chǎng)離奇詭異媳谁,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)友酱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)晴音,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人缔杉,你說(shuō)我怎么就攤上這事锤躁。” “怎么了或详?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,490評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵系羞,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我霸琴,道長(zhǎng)椒振,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,521評(píng)論 1 284
  • 正文 為了忘掉前任沈贝,我火速辦了婚禮杠人,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己嗡善,他們只是感情好辑莫,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著罩引,像睡著了一般各吨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上袁铐,一...
    開(kāi)封第一講書(shū)人閱讀 49,842評(píng)論 1 290
  • 那天揭蜒,我揣著相機(jī)與錄音,去河邊找鬼剔桨。 笑死屉更,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的洒缀。 我是一名探鬼主播瑰谜,決...
    沈念sama閱讀 38,997評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼树绩!你這毒婦竟也來(lái)了萨脑?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,741評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤饺饭,失蹤者是張志新(化名)和其女友劉穎渤早,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體瘫俊,經(jīng)...
    沈念sama閱讀 44,203評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鹊杖,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了扛芽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仅淑。...
    茶點(diǎn)故事閱讀 38,673評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖胸哥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赡鲜,我是刑警寧澤空厌,帶...
    沈念sama閱讀 34,339評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站银酬,受9級(jí)特大地震影響嘲更,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜揩瞪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評(píng)論 3 313
  • 文/蒙蒙 一赋朦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦宠哄、人聲如沸壹将。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,770評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)诽俯。三九已至,卻和暖如春承粤,著一層夾襖步出監(jiān)牢的瞬間暴区,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,000評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工辛臊, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留仙粱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,394評(píng)論 2 360
  • 正文 我出身青樓彻舰,卻偏偏與公主長(zhǎng)得像伐割,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子淹遵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評(píng)論 2 349