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-core
的transform
方法進行編譯,編譯結果包含code
、
map
、metadata
三個,其中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。