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