Webpack Loader源碼導讀之babel-loader

原文地址:http://hiihl.com/articles/2018/1/15/babelloader.md

webpack應該是當下最主流的前端構建工具之一,但是由于webpack本身糟糕的文檔,使得使用者多是會用但不知其所以然,出現問題時難以入手;為此,我想根據自身的理解,詳細講解一些常用loader、plugin源碼,深入各個loader及其配置所帶來的影響;本系列將以babel-loader開篇,但只講述babel-loader所做的事情,對于babel部份的內容,將在未來開篇詳談。

先看下源碼src目錄下的整體結構

|src
|——utils
|  |——exists.js  
|  |——read.js
|  |——relative.js
|——fs-cache.js
|——index.js
|——resolve-rc.js

配置說明

首先,我們進入index.js,找到module.exports = function(source, inputSourceMap),這里就是babel-loader的入口了。
每個webpack loader返回的都是一個function,這個function有兩個參數,第一個是待處理的代碼,第二個參數是上一loader處理后的sourcemap,如果有的話。
進入loader以后,首先是獲取文件名,再通過loaderUtils.getOptions獲取loader的配置,即query部份,其中babel-loader支持的配置有:

  • babelrc 配置文件.babelrc的位置,值false時不使用配置文件,配置為具體路徑且文件存在時直接使用該文件,否則按默認處理(從當前文件位置開始向上查找
    .babelrc.babelrc.js或使用package.json中的babel配置,具體見resolve-rc.js)
  • cacheDirectory 是否緩存目錄,默認false;設置為true時使用默認緩存目錄node_modules/.cache/babel-loader或者系統默認臨時文件目錄os.tmpdir();
    也可以設置具體的文件夾路徑
  • cacheIdentifier 緩存標識符;默認包括babel-core、babel-loader的版本號,.babelrc的內容以及BABEL_ENV(沒有時會取NODE_ENV)的值
  • sourceMap 是否輸出源碼,默認值與webpack配置devtool一致;配置此值時,將無視devtool的配置。
  • forceEnv Babel編譯的環境變量,默認不配置,環境變量先取BABEL_ENV再取NODE_ENV。當配置此值時,該值將覆蓋BABEL_ENV和NODE_ENV。
  • metadataSubscribers 訂閱元數據。這個配置在文檔中并沒有寫出來,但是是允許配置的。主要作用是訂閱一些編譯過程中的一些元數據,訂閱以后這些元數據將會被添加
    到webpack的上下文中。通常我們是用不上的,估計在某些babel-plugin中可能會使用到。數據大概是這樣的,記錄一些module導入導出的信息:
{
  "usedHelpers": [
    "createClass",
    "classCallCheck"
  ],
  "marked": [],
  "modules": {
    "imports": [],
    "exports": {
      "exported": [
        "Test"
      ],
      "specifiers": [
        {
          "kind": "local",
          "local": "Test",
          "exported": "default"
        }
      ]
    }
  }
}

cache詳解

真正的編譯過程都是在babel-core中執行的,babel-loader的主要作用時babel-core所需配置的一些初始化,以及編譯結果的緩存,現在我們主要講下緩存。
我們先修改下babel-loader的配置:

{
  test: /\.jsx?$/,
  loader: 'babel-loader',
  include: [path.resolve(process.cwd(), 'src')],
  exclude: [path.resolve(process.cwd(), 'node_modules')],
  query: {
    cacheDirectory: path.resolve(process.cwd(), 'tmp') // 配置緩存目錄到當前項目tmp下,方便等下查看緩存文件
  }
}

cacheIdentifier的默認值如下,如果我們配置了此值,將覆蓋默認值而非合并,所以暫時先不設置該值。

JSON.stringify({
  "babel-loader": pkg.version,
  "babel-core": babel.version,
  babelrc: babelrcPath ? read(fileSystem, babelrcPath) : null,
  env:
    loaderOptions.forceEnv ||
    process.env.BABEL_ENV ||
    process.env.NODE_ENV ||
    "development",
})

當我們配置了cacheDirectory時,loader會先查找緩存文件是否存在,文件名是通過下列方法計算得出

const filename = function(source, identifier, options) {
  const hash = crypto.createHash("SHA1");
  const contents = JSON.stringify({
    source: source,
    options: options,
    identifier: identifier,
  });

  hash.end(contents);

  return hash.read().toString("hex") + ".json.gz";
};

可以看到,文件名是以源碼source、loader選項options以及loader標識符identifier三個值的json字符串經過SHA1編碼得到的,所以當這三個值任意一個
發生變化時,都會導致文件名發生變化。
當緩存文件不存在,或者以上三個值發生變化導致緩存文件名變成一個不存在的文件時,會調用babel-coretransform方法進行編譯,編譯結果包含code
mapmetadata三個,其中map即與源碼的一些映射關系,這三個內容將保存在緩存文件中;
緩存文件是一個經過壓縮的JSON內容長這樣:

|tmp
|  |-- 9d08ce6a6158ff5416a96e2290c7243607f9f5c8.json.gz
|  |-- cceb4d9049dfb84308e4cdd7eeedbdadc98c7c09.json.gz

緩存的內容示例:

{
  "code": "'use strict';\n\nvar _test = require('./test');\n\nvar _test2 = _interopRequireDefault(_test);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar test = new _test2.default();\n\nconsole.log(test.toString());",
  "map": {
    "version": 3,
    "sources": [
      "src/index.js"
    ],
    "names": [
      "test",
      "console",
      "log",
      "toString"
    ],
    "mappings": ";;AAAA;;;;;;AAEA,IAAMA,OAAO,oBAAb;;AAEAC,QAAQC,GAAR,CAAYF,KAAKG,QAAL,EAAZ",
    "file": "index.js",
    "sourceRoot": "/Users/yzf/webpack-tuition/loaders/babel",
    "sourcesContent": [
      "import Test from './test';\n\nconst test = new Test();\n\nconsole.log(test.toString());\n"
    ]
  },
  "metadata": {
    "usedHelpers": [
      "interopRequireDefault"
    ],
    "marked": [],
    "modules": {
      "imports": [
        {
          "source": "./test",
          "imported": [
            "default"
          ],
          "specifiers": [
            {
              "kind": "named",
              "imported": "default",
              "local": "Test"
            }
          ]
        }
      ],
      "exports": {
        "exported": [],
        "specifiers": []
      }
    }
  }
}

當緩存文件存在時,則將緩存中的編譯結果read直接使用。

小結

babel-loader做的事情其實比較簡單,本文只是作為一個引子,開啟webpack常用loader的揭秘之路,對于babel整個編譯過程,未來可能會單獨開篇深入講解,
如有興趣歡迎關注我的博客hiihl.com

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容