手摸手,實(shí)現(xiàn)一個專屬于你的babel-loader
戳藍(lán)字「前端技術(shù)優(yōu)選」關(guān)注我們哦!
寫一個簡單的 babel-loader
這里所有的代碼都在github上,地址 https://github.com/lihongxun945/my-babel-loader。
這里以 babel-loader 為例,看我們?nèi)绾螌懸粋€自己的loader。首先,我們參考這篇官方教程,雖然寫的很粗略,但是我們可以學(xué)會寫一個簡單的loader。
最簡單的loader是一個什么都不做,原樣返回JS代碼的 loader,像這樣:
module.exports?=?function?(source)?{
??return?source
}
但是我們的 babel-loader 顯然需要調(diào)用 babel 來編譯代碼,我們查一下 babel-core 文檔,可以調(diào)用 babel.transform API來編譯代碼。再加上一些 presets 的設(shè)置,我們可以把上面的代碼做一下改造如下:
var?babel?=?require("babel-core")
module.exports?=?function?(source)?{
??var?babelOptions?=?{
????presets:?['env']
??}
??var?result?=?babel.transform(source,?babelOptions)
??return?result.code
}
關(guān)于 Babel 的API用法不在這里詳細(xì)解釋,有興趣的可以直接去看官方的文檔。我們這里做了一個很簡單的轉(zhuǎn)換,就是把 接收到的 source 源碼,用 babel 編譯一下,然后返回編譯后的代碼。
那么問題來了,我們要如何指定使用自己的loader呢,可以參考下面這種寫法,直接在 webpack config 文件里面加一個 resolveLoader 的配置即可,我們這里把 bable-loader 指定為自己寫的。
??resolveLoader:?{
????alias:?{
??????"babel-loader":?resolve('./build/babel-loader.js')
????}
??},
然后我們就可以運(yùn)行 npm run dev 編譯自己的JS代碼,比如我寫了這么幾行代碼:
class?People?{
??constructor?(name)?{
????this.name?=?name
??}
??sayName?()?{
????console.log(`Hello?there,?I'm?${this.name}`)
??}
}
const?lily?=?new?People('Lily')
lily.sayName()
經(jīng)過我們的 babel-loader 編譯后,最終輸出的代碼是這樣的:
var?People?=?function?()?{
??function?People(name)?{
????_classCallCheck(this,?People);
????this.name?=?name;
??}
??_createClass(People,?[{
????key:?'sayName',
????value:?function?sayName()?{
??????console.log('Hello?there,?I\'m?'?+?this.name);
????}
??}]);
??return?People;
}();
var?lily?=?new?People('Lily');
lily.sayName();
可以看到其中 class , 字符串模板,const 等都被 babel 編譯過。
添加sourcemap
上面的簡單代碼并沒有實(shí)現(xiàn) sourcemap 功能,如果需要支持 sourcemap,顯然需要把 babel-core 產(chǎn)生的 sourcemap 傳給 webpack。之前因?yàn)橹环祷鼐幾g后的代碼,所以我們直接返回了字符串,如果需要同時返回編譯的代碼和sourcemap,我們需要這個接口 this.callback,官方文檔上是這么說的:
this.callback(
??err:?Error?|?null,
??content:?string?|?Buffer,
??sourceMap?:?SourceMap,
??meta?:?any
);
那么我們查一下babel的文檔之后,很簡單就能獲取 sourcemap 了,其實(shí)就是 result.map,改動后的代碼如下:
var?babel?=?require("babel-core")
module.exports?=?function?(source,?inputSourceMap)?{
??var?babelOptions?=?{
????presets:?['env'],
????inputSourceMap:?inputSourceMap,
????sourceMaps:?true
??}
??var?result?=?babel.transform(source,?babelOptions)
??this.callback(null,?result.code,?result.map)
}
這里 sourceMaps: true 是告訴 babel 要生成 sourcemap,因?yàn)槟J(rèn)情況下它是不會生成的。然后刷新頁面應(yīng)該就能看到sourcemap了。
然而,根據(jù)你的webpack配置不同,很可能并看不到~~~這是因?yàn)?sourcemap 其實(shí)是要交給 webpack 來管理的,我們只是把 sourcemap 傳給了 webpack,而它在最終編譯出的代碼中到底要不要顯示是 webpack 自己的配置決定的。那么我們在 webpack.config.js 中添加一行代碼打開sourcemap功能即可:
devtool:?'eval-source-map'
這樣再刷新頁面就能看到sourcemap了,確實(shí)可以,然而 sourcemap 的文件名怎么是 unknown?我們在創(chuàng)建 sourcemap的時候需要指定一下文件名,不然確實(shí)會出現(xiàn) unknown 的問題。
那么問題又來了,怎么獲取文件名呢?
可以通過 this.request 來獲取當(dāng)前的文件名,比如這個示例中的 ?this.request 是:
~/github/my-webpack-loader/build/babel-loader.js!~/github/my-webpack-loader/src/main.js
其中 ~ 是被我省略的絕對路徑
可以看出 this.request 就是一個加載文件的請求,包含了兩部分,通過 ! 分割,前一部分是 對應(yīng)的 loader,后一部分是文件的路徑。我們一行代碼就可以把文件名提取出來:
this.request.split('!')[1].split('/').pop()
然后在 babel.transform 的配置中增加一行:
filename:?this.request.split('!')[1].split('/').pop()
再刷新頁面就可以看到正常的 sourcemap 了。
模塊
我們現(xiàn)在已經(jīng)支持 編譯代碼 和 sourcemap,下面我們要支持 modules,我們把 main.js 代碼拆成兩部分:
people.js
export?default?class?People?{
??constructor?(name)?{
????this.name?=?name
??}
??sayName?()?{
????console.log(`Hello?there,?I'm?${this.name}`)
??}
}
main.js
import?People?from?'./people'
const?lily?=?new?People('Lily')
lily.sayName()
那么我們需要怎么做處理呢?對JS文件來說,我們最好的方式是不作任何特殊處理。上面的代碼其實(shí)已經(jīng)可以正常打包模塊了。那么是怎么做到的呢?
因?yàn)镴S在webpack中是一等公民,webpack把所有的資源都當(dāng)做JS來加載。webpack默認(rèn)支持常見的AMD,CMD,ES6,nodejs 等常見的模塊加載方式,并且會自動做 bundle(打包)和 tree-shaking。反而,babel 只是把 ES6 模塊編譯成了 nodejs 模塊,它并不會做bundle。
比如我們的 people.js,經(jīng)過 babel-loader 編譯出來的代碼是這樣的:
//?省略幾個工具方法。。。
var?People?=?function?()?{
??function?People(name)?{
????_classCallCheck(this,?People);
????this.name?=?name;
??}
??_createClass(People,?[{
????key:?"sayName",
????value:?function?sayName()?{
??????console.log("Hello?there,?I'm?"?+?this.name);
????}
??}]);
??return?People;
}();
exports.default?=?People;
webpack 會識別 exports.default 語法,并且最終把兩個文件合并一個大的文件。反而,如果我們在 babel-loader 中做了打包,會導(dǎo)致 webpack 無法做 tree-shaking 優(yōu)化。
這也是webpack 的各種 JS loader,如 vue-loader, jsx-loader 等的共同做法,即把 modules bundle 交給 webpack 處理,讓webpack做最大程度的優(yōu)化。
當(dāng)然,這種做法僅僅對 JS 有效,因?yàn)镴S是webpack的一等公民,webpack內(nèi)置了對JS 模塊的完整支持,而其他的文件,比如 css, html 等,我們都需要把他們轉(zhuǎn)成JS然后交給webpack。這也是為什么 webpack 中有 style-loader, url-loader 卻沒有一個 js-loader 的原因。
我們的 babel-loader 代碼就已經(jīng)寫完了。其實(shí)總共就10行代碼,最終完整代碼如下所示:
var?babel?=?require("babel-core")
module.exports?=?function?(source,?inputSourceMap)?{
??var?babelOptions?=?{
????presets:?['env'],
????inputSourceMap:?inputSourceMap,
????filename:?this.request.split('!')[1].split('/').pop(),
????sourceMaps:?true
??}
??var?result?=?babel.transform(source,?babelOptions)
??this.callback(null,?result.code,?result.map)
}
那么我們?nèi)タ匆幌鹿俜降?babel-loader 是如何實(shí)現(xiàn)的。顯然他的代碼比我們的代碼多很多,他寫了那么多,其實(shí)主要是增加了 Cache,以及增加了對異常的處理。有興趣的可以自己去研究一下。
關(guān)于webpack的模塊加載機(jī)制,我們后續(xù)再詳細(xì)解讀。
如何編譯 JSX
如果我們是使用React寫的組件,那么同樣可以通過 babel-loader 來編譯。關(guān)于如何編譯JSX,babel官網(wǎng)這里做了很詳細(xì)的文檔 transform-react-jsx
簡單來說,就是 babel-core 本身雖然不支持 jsx,會報語法錯誤,但是我們可以通過加載一個插件就能支持 jsx用法如下:
require("babel-core").transform("code",?{
??plugins:?["transform-react-jsx"]
});
那么我們只要在 上面我們寫的 bable-loader 的代碼中加入一行 plugins: ["transform-react-jsx"] 就可以支持 jsx 的編譯了,是不是很簡單。
react 因?yàn)橛玫腏SX,而jsx 因?yàn)槿烤幾g成了JS 所以它的loader很簡單。但是 Vue 的組件并不是可以直接就全部編譯成JS,而是包括了 html,JS,CSS三部分,所以 vue-loader 相對來說就復(fù)雜很多了。
如果你喜歡探討技術(shù),或者對本文有任何的意見或建議,非常歡迎加魚頭微信好友一起探討,當(dāng)然,魚頭也非常希望能跟你一起聊生活,聊愛好,談天說地。魚頭的微信號是:krisChans95 也可以掃碼關(guān)注公眾號,訂閱更多精彩內(nèi)容。


