在上一篇《webpack從入門到上線》介紹了wepack的配置和相關的概念,這一篇介紹怎樣寫一個webpack loader. 通過寫一個js的html模板為例子。
上篇文章已提及,loader加載器就是對各種非正常資源的解析,轉化成瀏覽器可以識別的js/css文件等,甚至可以說loader就是一個小型的編譯器。例如sass loader:將sass格式編譯成css,在安裝sass的過程中你會發現,其實sass用的是C++,sass本身是面向對象的。但是本文不會介紹怎樣寫一個編譯器(我也不知道),只是簡單介紹下怎樣寫一個loader,拋磚引玉。
有什么樣的場景需要寫一個loader呢?
有一個就是在js里面寫html的模板,會有點不太方便,一個是語法不是高亮的,第二個是需要雙引號把每一行包起來,或者用數組push之類的,所以如果能夠require一個html文件,然后轉化成相應的js字符串,那就比較方便了。(如果你用react開發的,用jsx是比較自然的事情)。我在網上找了下,沒有找到能夠比較符合上面兩點需求的loader,所以就自己嘗試寫一個吧。
js模板通常有三種場景:
- 純html,一個文件就是一個模板,直接轉成字符串即可;
- 需要有變量,一個文件有幾個模板,每個模板對應一個變量
- 再復雜一點,模板是require了其它的工廠模塊生成的字符串
現在開始來寫loader
loader的基本框架
首先在工程的node_modules下面新建一個文件夾html-template-loader,在里面創建一個index.js做為這個loader的js文件。這樣就可以在webpack.config.js里面添加一個loader了:
1
2
3
4
|
{
test
:
/
\
.
tpl
\
.
html
$
/
,
loader
:
'html-template-loader'
}
|
以.tpl.html作為后綴名,也就是說在邏輯代碼里面引入一個.tpl.html結尾的文件,就歸這個loader處理
1
|
var
tpl
=
require
(
"./dialog.tpl.html"
)
;
|
重點就在於,這個loader怎么寫呢?
loader也需要寫成一個模塊,一個基本的loader的寫法:
1
2
3
4
5
6
7
8
|
/**
* source為原文件的字符串格式
*/
module
.
exports
=
function
(
source
,
map
)
{
//對source進行解析
var
exports
=
process
(
source
)
;
return
"module.exports = "
+
exports
;
}
|
是的,你沒看錯,一個loader就是這么簡單。關鍵在於理解loader的機制——其實loader最后會創建一個模塊,當我們require一個需要讓loader的解析的文件之后,通過上面第7行的return——return里面的內容就是自動創建的模塊的內容,跟平時自已寫的模塊的區別就在於這個要自己拼成一個js語法合法的字符串。
如上面生成的exports為:
1
2
|
var
exports
=
"var tpl = {email: '<div>1</div>', hello: '<p>2</p>'}"
;
return
"var tpl = "
+
tpl
+
";\n module.exports = tpl;"
;
|
那么webpack自動創建這樣一個模塊:
1
2
3
4
5
|
/***/
109
:
/***/
function
(
module
,
exports
)
{
var
tpl
=
{
email
:
'<div>1</div>'
,
hello
:
'<p>2</p>'
}
;
module
.
exports
=
tpl
;
/***/
}
|
在我這個工程,這個模塊的id為109,然后require的返回結果就是這個tpl的object。
另外一方面,初始化的時候可以拿到source,這個source就是webpack以utf-8讀的整個文件的內容,以字符串的形式作為一個參數傳進來。
所以現在目標就很明確了,我們需要處理這個source的字符串,然后再返回一個可以eval的字符串。需要定義loader的輸入和輸出的格式,這是作為loader開發者的權利,同時中間的處理過程對用戶屏蔽。
loader的具體實現
把source這整一個文件,拆成一行一行處理:
1
2
3
4
5
6
7
8
9
10
|
var
tpl
=
""
;
source
.
split
(
/
\
r
?
\
n
/
)
.
forEach
(
function
(
line
)
{
line
=
line
.
trim
(
)
;
if
(
!
line
.
length
)
{
return
;
}
//對line進行處理...
tpl
+=
process
(
line
)
;
}
return
"module.exports = ..."
;
|
難點就在於第8行的process函數怎么寫,怎樣拼成一個合法的tpl字符串。
如果是上面提到的第一種場景,則很簡單,直接加就好了;
如果是第二種場景需要用變量區分的,那么需要自定義一個語法,由於我們用的是html,因為有高亮的功能。所以可考慮用html的注釋的標簽:
1
2
3
4
5
6
7
8
|
<
!
--
%
email
%
--
>
<
div
>
<
p
>
hello
,
world
<
/
p
>
<
/
div
>
<
!
--
%
alert
%
--
>
<
p
>
hi
<
/
p
>
<
span
>
man
<
/
span
>
|
我們的語法就是<!–%變量名%–>,像上面定義了兩個變量email和alert,接下來在process函數里面就可以進行識別、拼一個object的字符串。
如果是第三種場景需要引入第三方模塊的,則需要在最后一行return的時候加上require這個函數的代碼,即:
1
2
|
return
"var widget = require(../widgetFactory"
)
;
\
n
" +
"
module
.
exports
=
widget
.
make
(
)"
;
|
同樣地,需要定義一個語法,require的代碼用script的方式:
1
|
<script
generate
>
var
SELECT
=
require
(
"js/select"
)
;
</script>
|
以一個generate的屬性標志,這塊是需要依賴的代碼,也就是說要把它拼在前面。
正常的調用就用一個script,不帶屬性:
1
2
3
4
5
6
7
|
<
!
--
%
email
%
--
>
<
div
>
<
p
>
hello
,
world
<
/
p
>
<
/
div
>
<script>
widget
.
make
(
)
</script>
|
最后把它轉成一個object.
經過合適的處理之后,最后生成的模塊是這樣的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/***/
110
:
/***/
function
(
module
,
exports
,
__webpack_require__
)
{
var
widget
=
__webpack_require__
(
111
)
;
var
tpl
=
{
email
:
'<div>'
+
'<p>\'tpl\'</p>'
+
'<p>hello, world</p>'
+
'</div>'
+
widget
.
make
(
)
+
'<p>good</p>'
+
''
,
alert
:
'<p>hi</p>'
+
'<span>man</span>'
+
''
}
module
.
exports
=
tpl
/***/
}
,
|
具體處理代碼略,詳見github
loader的高階話題
1. loader支持鏈式,上一個loader的處理結果可以給下一個loader,像sass的loader是這樣寫的:
1
|
require
(
"!style!css!sass!./file.scss"
)
;
|
或者是寫在配置文件里面:
1
2
3
4
|
{
test
:
/
\
.
scss
$
/
,
loaders
:
[
"style"
,
"css"
,
"sass"
]
}
|
sass處理完之后給css的loader,css的loader處理完后給style的loader,loader間的數據傳遞通過loader的定義的callback函數,例如上面的loader可以在return之前再加一行:
1
|
this
.
callback
(
null
,
source
,
map
)
;
|
這樣就可以傳給下一個loader,最后一個loader一定要有return的內容;
2. loader可以緩存,可以加快速度
1
|
this
.
cacheable
(
)
;
|
3. 其它:loader支持異步,loader的加載可以用異步的方式,loader可以傳參數等等,詳見官方文檔:HOW TO WRITE A LOADER,官方文檔比較繞,不是從0開始,也沒有給一個完整的demo,具體可以再另外查查。
寫loader關鍵在於怎么編譯這個文件,上面是用的把它拆成一行一行的方式,然后做了個簡單的處理,一般編譯是要用到語法樹。