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