Webpack 之詳解

Webpack 之詳解

Webpack 之詳解

webpack是現代前端開發中最火的模塊打包工具,只需要通過簡單的配置,便可以完成模塊的加載和打包。那它是怎麼做到通過對一些插件的配置,便可以輕鬆實現對代碼的構建呢?

webpack的配置

const path =require('path');
module.exports =
{ entry
:

"./app/entry"
,

// string | object | array

// Webpack打包的入口
output
:

{

// 定義webpack如何輸出的選項
path
:
path
.
resolve
(
__dirname
,

"dist"
),

// string

// 所有輸出文件的目標路徑
filename
:


"[chunkhash].js"
,

// string

// 「入口(entry chunk)」文件命名模版
publicPath
:

"/assets/"
,

// string

// 構建文件的輸出目錄

/* 其它高級配置 */

},
module
:

{

// 模塊相關配置
rules
:

[

// 配置模塊loaders,解析規則

{
test
:

/\\.jsx?$/
,

// RegExp | string
include
:

[

// 和test一樣,必須匹配選項

path
.
resolve
(
__dirname
,

"app"
)

],
exclude
:

[

// 必不匹配選項(優先級高於test和include)
path
.
resolve
(
__dirname
,

"app/demo-files"
)

],
loader
:

"babel-loader"
,

// 模塊上下文解析
options
:

{

// loader的可選項
presets
:

[
"es2015"
]

},

},

},
resolve
:

{

// 解析模塊的可選項
modules
:

[

// 模塊的查找目錄

"node_modules"
,
path
.
resolve
(
__dirname
,

"app"
)

],
extensions
:

[
".js"
,

".json"
,

".jsx"
,

".css"
],

// 用到的文件的擴展

alias
:

{

// 模塊別名列表

"module"
:

"new-module"

},

},
devtool
:

"source-map"
,

// enum

// 為瀏覽器開發者工具添加元數據增強調試
plugins
:

[

// 附加插件列表

// ...

],
}

從上面我們可以看到,webpack配置中需要理解幾個核心的概念 Entry 、 Output、 Loaders 、 Plugins、 Chunk:

  • Entry:指定webpack開始構建的入口模塊,從該模塊開始構建並計算出直接或間接依賴的模塊或者庫
  • Output:告訴webpack如何命名輸出的文件以及輸出的目錄
  • Loaders:由於webpack只能處理javascript,所以我們需要對一些非js文件處理成webpack能夠處理的模塊,比如sass文件
  • Plugins: Loaders將各類型的文件處理成webpack能夠處理的模塊, plugins有著很強的能力。插件的範圍包括,從打包優化和壓縮,一直到重新定義環境中的變量。但也是最複雜的一個。比如對js文件進行壓縮優化的 UglifyJsPlugin插件
  • Chunk:coding split的產物,我們可以對一些代碼打包成一個單獨的chunk,比如某些公共模塊,去重,更好的利用緩存。或者按需加載某些功能模塊,優化加載時間。在webpack3及以前我們都利用 CommonsChunkPlugin將一些公共代碼分割成一個chunk,實現單獨加載。在webpack4 中 CommonsChunkPlugin被廢棄,使用 SplitChunksPlugin

webpack詳解

讀到這裡,或許你對webpack有一個大概的瞭解,那webpack 是怎麼運行的呢?我們都知道,webpack是高度複雜抽象的插件集合,理解webpack的運行機制,對於我們日常定位構建錯誤以及寫一些插件處理構建任務有很大的幫助。

不得不說的tapable

webpack本質上是一種事件流的機制,它的工作流程就是將各個插件串聯起來,而實現這一切的核心就是Tapable,webpack中最核心的負責編譯的 Compiler和負責創建bundles的 Compilation都是Tapable的實例。在Tapable1.0之前,也就是webpack3及其以前使用的Tapable,提供了包括:

  • plugin(name:string,handler:function)註冊插件到Tapable對象中
  • apply(…pluginInstances:(AnyPlugin|function)[])調用插件的定義,將事件監聽器註冊到Tapable實例註冊表中
  • applyPlugins*(name:string,…)多種策略細緻地控制事件的觸發,包括 applyPluginsAsync、 applyPluginsParallel等方法實現對事件觸發的控制,實現

(1)多個事件連續順序執行

(2)並行執行

(3)異步執行

(4)一個接一個地執行插件,前面的輸出是後一個插件的輸入的瀑布流執行順序

(5)在允許時停止執行插件,即某個插件返回了一個 undefined的值,即退出執行

我們可以看到,Tapable就像nodejs中 EventEmitter,提供對事件的註冊 on和觸發 emit,理解它很重要,看個栗子:比如我們來寫一個插件

function

CustomPlugin
()

{}
CustomPlugin
.
prototype
.

apply
=

function
(
compiler
)

{
compiler
.
plugin
(
'emit'
,
pluginFunction
);
}

在webpack的生命週期中會適時的執行:

this
.
apply
*(
"emit"
,
options
)

當然上面提到的Tapable都是1.0版本之前的,如果想深入學習,可以查看Tapable和事件流(https://segmentfault.com/a/1190000008060440)。

那1.0的Tapable又是什麼樣的呢?1.0版本發生了巨大的改變,不再是此前的通過 plugin註冊事件,通過 applyPlugins*觸發事件調用,那1.0的Tapable是什麼呢?

暴露出很多的鉤子,可以使用它們為插件創建鉤子函數

const

{

SyncHook
,

SyncBailHook
,

SyncWaterfallHook
,

SyncLoopHook
,

AsyncParallelHook
,

AsyncParallelBailHook
,

AsyncSeriesHook
,

AsyncSeriesBailHook
,

AsyncSeriesWaterfallHook

}

=
require
(
"tapable"
);

我們來看看怎麼使用。

class

Order

{
constructor
()

{

this
.
hooks
=

{

//hooks
goods
:

new

SyncHook
([
'goodsId'
,

'number'
]),
consumer
:

new

AsyncParallelHook
([
'userId'
,

'orderId'
])

}

}
queryGoods
(
goodsId
,
number
)

{

this
.
hooks

.
goods
.
call
(
goodsId
,
number
);

}
consumerInfoPromise
(
userId
,
orderId
)

{

this
.
hooks
.
consumer
.
promise
(
userId
,
orderId
).
then
(()

=>

{

//TODO

})

}
consumerInfoAsync
(
userId
,
orderId
)


{

this
.
hooks
.
consumer
.
callAsync
(
userId
,
orderId
,

(
err
,
data
)

=>

{

//TODO

})

}
}

對於所有的hook的構造函數均接受一個可選的string類型的數組。

const
hook
=

new

SyncHook
([
"arg1"
,

"arg2"

,

"arg3"
]);
// 調用tap方法註冊一個consument
order
.
hooks
.
goods
.
tap
(
'QueryPlugin'
,

(
goodsId
,
number
)

=>

{

return
fetchGoods
(
goodsId
,
number
);
})
// 再添加一個
order
.
hooks
.
goods
.
tap
(
'LoggerPlugin'
,

(
goodsId
,

number
)

=>

{
logger
(
goodsId
,
number
);
})
// 調用
order
.
queryGoods
(
'10000000'
,

1
)

對於一個 SyncHook,我們通過 tap來添加消費者,通過 call來觸發鉤子的順序執行。

對於一個非 sync*類型的鉤子,即 async*類型的鉤子,我們還可以通過其它方式註冊消費者和調用

// 註冊一個sync 鉤子
order
.
hooks
.
consumer
.
tap
(
'LoggerPlugin'
,

(
userId

,
orderId
)

=>

{
logger
(
userId
,
orderId
);
})
order
.
hooks
.
consumer
.
tapAsync
(
'LoginCheckPlugin'
,

(
userId
,
orderId
,
callback
)

=>

{

LoginCheck
(
userId
,
callback
);
})
order
.
hooks
.
consumer
.

tapPromise
(
'PayPlugin'
,

(
userId
,
orderId
)

=>

{

return

Promise
.
resolve
();
})
// 調用
// 返回Promise
order
.
consumerInfoPromise
(
'user007'
,

'1024'
);
//回調函數
order
.
consumerInfoAsync
(
'user007'
,

'1024'
)

通過上面的栗子,你可能已經大致瞭解了 Tapable的用法,它的用法:

  • 插件註冊數量
  • 插件註冊的類型(sync, async, promise)
  • 調用的方式(sync, async, promise)
  • 實例鉤子的時候參數數量
  • 是否使用了 interception

Tapable詳解

Webpack 之詳解

對於 Sync*類型的鉤子來說:

  • 註冊在該鉤子下面的插件的執行順序都是順序執行。
  • 只能使用 tap註冊,不能使用 tapPromise和 tapAsync註冊
// 所有的鉤子都繼承於Hook
class

Sync
*
extends
Hook

{

tapAsync
()

{

// Sync*類型的鉤子不支持tapAsync

throw

new

Error
(
"tapAsync is not supported on a Sync*"
);

}
tapPromise
()

{
// Sync*類型的鉤子不支持tapPromise

throw

new

Error

(
"tapPromise is not supported on a Sync*"
);

}
compile
(
options
)

{

// 編譯代碼來按照一定的策略執行Plugin
factory
.
setup
(
this
,
options
);

return
factory
.
create
(
options
);

}
}

對於 Async*類型鉤子:支持 tap、 tapPromise、 tapAsync註冊。

class

AsyncParallelHook
extends
Hook

{
constructor
(
args
)

{
super
(
args
);

this
.
call
=

this
.
_call
=

undefined
;

}
compile
(
options
)

{
factory
.
setup
(
this
,
options
);

return
factory
.
create
(
options
);

}
}
class

Hook

{

constructor
(
args
)

{

if
(!
Array
.
isArray
(
args
))
args
=

[];

this
.
_args
=
args
;

// 實例鉤子的時候的string類型的數組

this
.
taps
=

[];

// 消費者

this
.
interceptors
=

[];

// interceptors

this
.

call
=

this
.
_call
=

// 以sync類型方式來調用鉤子

this
.
_createCompileDelegate
(
"call"
,

"sync"
);

this
.
promise
=


this
.
_promise
=

// 以promise方式

this
.
_createCompileDelegate
(
"promise"
,

"promise"
);

this
.
callAsync
=


this
.
_callAsync
=

// 以async類型方式來調用

this
.
_createCompileDelegate
(
"callAsync"
,

"async"
);

this
.
_x
=

undefined
;

//

}
_createCall
(
type
)

{

return

this
.
compile
({
taps
:

this
.
taps
,
interceptors
:


this
.
interceptors
,
args
:

this
.
_args
,
type
:
type

});

}
_createCompileDelegate
(
name
,
type
)

{

const
lazyCompileHook
=

(...
args
)

=>

{

this
[
name
]

=

this
.
_createCall

(
type
);

return

this
[
name
](...
args
);

};

return
lazyCompileHook
;

}

// 調用tap 類型註冊
tap
(
options
,
fn
)

{

// ...
options
=

Object
.
assign
({
type
:

"sync"
,
fn
:
fn
},
options
);


// ...

this
.
_insert
(
options
);

// 添加到 this.taps中

}

// 註冊 async類型的鉤子
tapAsync
(
options
,
fn
)

{

// ...
options
=

Object
.
assign
({
type
:

"async"
,
fn
:
fn
},
options
);

// ...

this
.
_insert

(
options
);

// 添加到 this.taps中

}

註冊
promise
類型鉤子
tapPromise
(
options
,
fn
)

{

// ...
options
=

Object
.
assign
({
type
:

"promise"
,
fn
:
fn
},
options
);

// ...

this
.
_insert
(
options
);

// 添加到 this.taps中

}
}

每次都是調用 tap、 tapSync、 tapPromise註冊不同類型的插件鉤子,通過調用 call、 callAsync 、 promise方式調用。其實調用的時候為了按照一定的執行策略執行,調用 compile方法快速編譯出一個方法來執行這些插件。

const
factory
=

new

Sync
*
CodeFactory
();
class

Sync
*
extends
Hook

{


// ...
compile
(
options
)

{

// 編譯代碼來按照一定的策略執行Plugin
factory
.
setup
(

this
,
options
);

return
factory
.
create
(
options
);

}
}
class

Sync
*
CodeFactory
extends
HookCodeFactory

{
content
({
onError
,
onResult
,
onDone
,
rethrowIfPossible
})

{

return

this
.
callTapsSeries
({
onError
:

(
i
,
err

)

=>
onError
(
err
),
onDone
,
rethrowIfPossible

});

}
}

compile中調用 HookCodeFactory#create方法編譯生成執行代碼。

class

HookCodeFactory

{
constructor
(
config
)

{

this
.
config
=
config
;

this
.
options
=

undefined
;

}
create
(

options
)

{

this
.
init
(
options
);

switch
(
this
.
options
.
type
)

{

case

"sync"
:

// 編譯生成sync, 結果直接返回

return

new

Function
(
this
.
args
(),


"\\"use strict\\";\\n"

+

this
.
header

()

+

this
.
content
({

// ...
onResult
:
result
=>

`
return
$
{
result
};
\\n
`,

// ...

}));

case

"async"
:

// async類型, 異步執行,最後將調用插件執行結果來調用callback,

return

new

Function
(
this
.
args
({
after
:

"_callback"

}),

"\\"use strict\\";\\n"

+

this
.
header
()

+

this
.
content
({

// ...
onResult
:
result
=>

`
_callback
(
null
,
$
{
result
});
\\n
`,
onDone
:

()

=>

"_callback();\\n"

}));

case

"promise"
:

// 返回promise類型,將結果放在resolve中

// ...
code
+=

"return new Promise((_resolve, _reject) => {\\n"
;
code
+=

"var _sync = true;\\n"
;
code
+=

this
.
header
();
code
+=

this
.
content
({

// ...
onResult
:
result
=>

`
_resolve
(
$
{
result
});
\\n
`,
onDone
:

()

=>

"_resolve();\\n"

});

// ...

return

new

Function
(
this
.
args
(),
code
);

}

}

// callTap 就是執行一些插件,並將結果返回
callTap
(
tapIndex
,

{
onError
,
onResult
,
onDone
,
rethrowIfPossible
})

{
let code
=

""
;

let hasTapCached
=

false
;

// ...
code
+=

`
var
_fn$
{
tapIndex
}

=
$
{
this
.
getTapFn
(
tapIndex
)};
\\n
`;

const
tap
=

this
.
options
.
taps
[
tapIndex
];

switch
(
tap
.
type
)

{


case

"sync"
:

// ...

if
(
onResult
)

{
code
+=

`
var
_result$
{
tapIndex
}

=
_fn$
{
tapIndex
}(
$
{
this
.
args
({
before
:
tap
.
context
?

"_context"

:

undefined

})});
\\n

`;

}

else

{
code
+=

`
_fn$
{
tapIndex
}(
$
{
this
.
args
({
before
:
tap
.
context
?

"_context"

:

undefined

})});
\\n
`;

}

if
(
onResult
)

{

// 結果透傳
code
+=

onResult
(`
_result$
{
tapIndex
}`);

}

if
(
onDone
)

{

// 通知插件執行完畢,可以執行下一個插件
code
+=
onDone
();

}

break
;

case

"async"
:

//異步執行,插件運行完後再將結果通過執行callback透傳
let cbCode
=

""
;

if
(
onResult
)
cbCode
+=

`(

_err$
{
tapIndex
},
_result$
{
tapIndex
})

=>

{
\\n
`;

else
cbCode
+=

`
_err$
{
tapIndex
}

=>

{
\\n
`;
cbCode
+=

`
if
(
_err$
{
tapIndex
})

{
\\n
`;
cbCode
+=
onError
(`
_err$
{

tapIndex
}`);
cbCode
+=

"} else {\\n"
;

if
(
onResult
)

{
cbCode
+=
onResult
(`
_result$
{
tapIndex
}`);

}
cbCode
+=

"}\\n"
;
cbCode
+=

"}"
;
code
+=

`
_fn$
{
tapIndex
}(
$
{
this
.
args
({
before
:

tap
.
context
?

"_context"

:

undefined
,
after
:
cbCode
//cbCode將結果透傳

})});
\\n
`;

break
;

case

"promise"
:

// _fn${tapIndex} 就是第tapIndex 個插件,它必須是個Promise類型的插件
code
+=

`
var
_hasResult$
{
tapIndex
}

=

false
;
\\n
`;
code
+=

`
_fn$
{
tapIndex
}(
$
{
this
.
args
({
before
:
tap
.
context
?

"_context"

:

undefined

})}).
then
(
_result$
{
tapIndex
}

=>

{
\\n
`;
code
+=

`
_hasResult$
{
tapIndex
}

=

true
;

\\n
`;

if
(
onResult
)

{
code
+=
onResult
(`
_result$
{
tapIndex
}`);

}

// ...

break
;

}

return
code
;

}

// 按照插件的註冊順序,按照順序遞歸調用執行插件
callTapsSeries
({
onError
,
onResult
,
onDone
,
rethrowIfPossible
})

{

// ...


const
firstAsync
=

this
.
options
.
taps
.
findIndex
(
t
=>
t
.
type
!==

"sync"
);

const
next
=
i
=>

{

// ...

const
done
=

()

=>
next
(
i
+

1
);

// ...

return

this
.
callTap
(
i
,

{

// ...
onResult
:
onResult
&&

((
result
)

=>

{

return
onResult
(
i
,
result
,
done
,
doneBreak
);

}),

// ...

});

};

return
next
(
0
);


}
callTapsLooping
({
onError
,
onDone
,
rethrowIfPossible
})

{

const
syncOnly
=

this
.
options
.
taps
.
every
(
t
=>
t
.
type
===

"sync"
);
let code
=

""
;

if
(!
syncOnly
)

{
code
+=

"var _looper = () => {\\n"

;
code
+=

"var _loopAsync = false;\\n"
;

}
code
+=

"var _loop;\\n"
;
code
+=

"do {\\n"
;
code
+=

"_loop = false;\\n"
;

// ...
code
+=

this
.
callTapsSeries
({

// ...
onResult
:

(
i
,
result
,
next
,
doneBreak
)

=>

{


// 一旦某個插件返回不為undefined, 即一隻調用某個插件執行,如果為undefined,開始調用下一個
let code
=

""
;
code
+=

`
if
(
$
{
result
}

!==

undefined
)

{
\\n
`;
code
+=

"_loop = true;\\n"
;

if
(!
syncOnly
)
code
+=

"if(_loopAsync) _looper();\\n"
;
code
+=
doneBreak
(
true
);

code
+=

`}

else

{
\\n
`;
code
+=
next
();
code
+=

`}
\\n
`;

return
code
;

},

// ...

})
code
+=

"} while(_loop);\\n"
;

// ...

return
code
;

}

// 並行調用插件執行
callTapsParallel
({
onError
,
onResult

,
onDone
,
rethrowIfPossible
,
onTap
=

(
i
,
run
)

=>
run
()

})

{

// ...

// 遍歷註冊都所有插件,並調用

for
(
let i
=

0
;
i
<

this
.
options
.
taps
.
length
;
i
++)

{

// ...
code
+=

"if(_counter <= 0) break;\\n"
;
code
+=
onTap
(
i
,

()

=>

this
.
callTap
(
i
,

{

// ...
onResult
:
onResult
&&

((
result
)

=>

{
let code
=

""
;
code
+=

"if(_counter > 0) {\\n"
;
code

+=
onResult
(
i
,
result
,
done
,
doneBreak
);
code
+=

"}\\n"
;

return
code
;

}),

// ...

}),
done
,
doneBreak
);

}

// ...

return
code
;

}
}

在 HookCodeFactory#create中調用到 content方法,此方法將按照此鉤子的執行策略,調用不同的方法來執行編譯 生成最終的代碼。

SyncHook中調用 callTapsSeries編譯生成最終執行插件的函數, callTapsSeries做的就是將插件列表中插件按照註冊順序遍歷執行。

class

SyncHookCodeFactory
extends
HookCodeFactory

{
content
({
onError
,
onResult
,
onDone
,
rethrowIfPossible
})

{

return

this
.
callTapsSeries
({
onError
:

(
i
,
err
)

=>
onError
(
err
),
onDone
,
rethrowIfPossible

});

}
}

SyncBailHook中當一旦某個返回值結果不為 undefined便結束執行列表中的插件。

 
class

SyncBailHookCodeFactory
extends
HookCodeFactory

{
content
({
onError
,
onResult
,
onDone
,
rethrowIfPossible
})

{

return

this
.
callTapsSeries
({

// ...
onResult
:

(
i
,
result
,
next
)

=>

`
if
(

$
{
result
}

!==

undefined
)

{
\\n$
{
onResult
(
result
)};
\\n
}

else

{
\\n$
{
next
()}}
\\n
`,

// ...

});

}
}

SyncWaterfallHook中上一個插件執行結果當作下一個插件的入參。

class

SyncWaterfallHookCodeFactory
extends
HookCodeFactory

{
content

({
onError
,
onResult
,
onDone
,
rethrowIfPossible
})

{

return

this
.
callTapsSeries
({

// ...
onResult
:

(
i
,
result
,
next
)

=>

{
let code
=

""
;
code
+=

`
if
(
$
{
result
}

!==

undefined
)

{
\\n
`;
code
+=

`
$
{
this
.
_args
[
0
]}

=
$
{
result
};
\\n
`;
code
+=

`}
\\n
`;
code
+=
next
();

return
code
;

},
onDone
:

()

=>

onResult
(
this
.
_args
[
0
]),

});

}
}
  • AsyncParallelHook調用 callTapsParallel並行執行插件
class

AsyncParallelHookCodeFactory
extends
HookCodeFactory

{
content
({
onError
,
onDone
})

{

return

this
.
callTapsParallel
({
onError
:

(
i
,
err
,
done

,
doneBreak
)

=>
onError
(
err
)

+
doneBreak
(
true
),
onDone

});

}
}

webpack流程篇

本文關於webpack 的流程講解是基於webpack4的。

webpack 入口文件

從webpack項目的package.json文件中我們找到了入口執行函數,在函數中引入webpack,那麼入口將會是 lib/webpack.js,而如果在shell中執行,那麼將會走到 ./bin/webpack.js,我們就以 lib/webpack.js為入口開始吧!

{

"name"
:

"webpack"
,

"version"
:

"4.1.1"

,

...

"main"
:

"lib/webpack.js"
,

"web"
:

"lib/webpack.web.js"
,

"bin"
:

"./bin/webpack.js"
,

...

}

webpack入口

const
webpack
=

(
options
,
callback
)

=>

{

// ...

// 驗證options正確性

// 預處理options
options

=

new

WebpackOptionsDefaulter
().
process
(
options
);

// webpack4的默認配置
compiler
=

new

Compiler
(
options
.
context
);

// 實例Compiler

// ...

// 若options.watch === true && callback 則開啟watch線程
compiler
.
watch
(
watchOptions
,
callback
);
compiler
.
run
(
callback
);

return
compiler
;
};

webpack 的入口文件其實就實例了 Compiler並調用了 run方法開啟了編譯,webpack的編譯都按照下面的鉤子調用順序執行:

  • before-run 清除緩存
  • run 註冊緩存數據鉤子
  • before-compile
  • compile 開始編譯
  • make 從入口分析依賴以及間接依賴模塊,創建模塊對象
  • build-module 模塊構建
  • seal 構建結果封裝, 不可再更改
  • after-compile 完成構建,緩存數據
  • emit 輸出到dist目錄

編譯&構建流程

webpack中負責構建和編譯都是 Compilation。

class

Compilation
extends
Tapable

{
constructor
(
compiler
)


{
super
();

this
.
hooks
=

{

// hooks

};

// ...

this
.
compiler
=
compiler
;

// ...

// template

this
.
mainTemplate
=

new

MainTemplate
(
this
.
outputOptions
);

this
.
chunkTemplate
=

new

ChunkTemplate
(
this
.
outputOptions
);

this
.
hotUpdateChunkTemplate
=

new

HotUpdateChunkTemplate
(

this
.
outputOptions

);

this
.
runtimeTemplate
=

new

RuntimeTemplate
(

this
.
outputOptions
,

this
.
requestShortener

);

this
.
moduleTemplates
=

{

javascript
:

new

ModuleTemplate
(
this
.
runtimeTemplate
),
webassembly
:

new

ModuleTemplate
(
this
.
runtimeTemplate
)

};

// 構建生成的資源

this
.
chunks
=

[];

this
.
chunkGroups
=

[];

this
.
modules
=

[];

this
.

additionalChunkAssets
=

[];

this
.
assets
=

{};

this
.
children
=

[];

// ...

}

//
buildModule
(
module
,
optional
,
origin
,
dependencies
,
thisCallback
)

{

// ...

// 調用module.build方法進行編譯代碼,build中 其實是利用acorn編譯生成AST

this
.
hooks
.
buildModule

.
call
(
module
);
module
.
build
(
/**param*/
);

}

// 將模塊添加到列表中,並編譯模塊
_addModuleChain
(
context
,
dependency
,
onModule
,
callback
)

{

// ...

// moduleFactory.create創建模塊,這裡會先利用loader處理文件,然後生成模塊對象
moduleFactory
.
create
(

{
contextInfo
:

{
issuer
:

""
,
compiler

:

this
.
compiler
.
name

},
context
:
context
,
dependencies
:

[
dependency
]

},

(
err
,
module
)

=>

{

const
addModuleResult
=

this
.
addModule
(
module
);
module
=
addModuleResult
.
module
;
onModule
(

module
);
dependency
.
module
=
module
;

// ...

// 調用buildModule編譯模塊

this
.
buildModule
(
module
,

false
,

null
,

null
,
err
=>

{});

}

});

}

// 添加入口模塊,開始編譯&構建
addEntry
(
context
,
entry
,
name
,
callback

)

{

// ...

this
.
_addModuleChain
(

// 調用_addModuleChain添加模塊
context
,
entry
,
module
=>

{

this
.
entries
.
push
(
module
);

},

// ...

);

}
seal
(
callback
)

{

this
.
hooks
.
seal
.

call
();

// ...

const
chunk
=

this
.
addChunk
(
name
);

const
entrypoint
=

new

Entrypoint
(
name
);
entrypoint
.
setRuntimeChunk
(
chunk
);
entrypoint
.
addOrigin
(
null
,
name
,
preparedEntrypoint
.
request
);

this
.
namedChunkGroups
.
set

(
name
,
entrypoint
);

this
.
entrypoints
.
set
(
name
,
entrypoint
);

this
.
chunkGroups
.
push
(
entrypoint
);

GraphHelpers
.
connectChunkGroupAndChunk
(
entrypoint
,
chunk
);

GraphHelpers
.
connectChunkAndModule
(
chunk
,
module
);
chunk
.
entryModule
=
module
;
chunk

.
name
=
name
;

// ...

this
.
hooks
.
beforeHash
.
call
();

this
.
createHash
();

this
.
hooks
.
afterHash
.
call
();

this
.
hooks
.
beforeModuleAssets
.
call
();

this
.
createModuleAssets
();

if

(
this
.

hooks
.
shouldGenerateChunkAssets
.
call
()

!==

false
)

{

this
.
hooks
.
beforeChunkAssets
.
call
();

this
.
createChunkAssets
();

}

// ...

}
createHash
()

{

// ...

}

// 生成 assets 資源並 保存到 Compilation.assets 中 給webpack寫插件的時候會用到
createModuleAssets
()

{

for


(
let i
=

0
;
i
<

this
.
modules
.
length
;
i
++)

{

const
module
=

this
.
modules
[
i
];

if

(
module
.
buildInfo
.
assets
)

{

for

(
const
assetName of
Object

.
keys
(
module
.
buildInfo
.
assets
))

{

const
fileName
=

this
.
getPath
(
assetName
);

this
.
assets
[
fileName
]

=
module
.
buildInfo
.
assets
[
assetName
];


this
.
hooks
.
moduleAsset
.
call
(
module

,
fileName
);

}

}

}

}
createChunkAssets
()

{

// ...

}
}

在webpack make鉤子中, tapAsync註冊了一個 DllEntryPlugin, 就是將入口模塊通過調用 compilation.addEntry方法將所有的入口模塊添加到編譯構建隊列中,開啟編譯流程。

compiler
.
hooks
.
make
.
tapAsync
(
"DllEntryPlugin"
,

(
compilation
,
callback
)

=>

{
compilation
.

addEntry
(

this
.
context
,

new

DllEntryDependency
(

this
.
entries
.
map
((
e
,
idx
)

=>

{

const
dep
=

new

SingleEntryDependency
(
e
);
dep
.
loc
=

`
$
{
this
.
name
}:

$
{
idx
}`;

return
dep
;

}),

this
.
name

),

// ...

);

});

隨後在 addEntry 中調用 _addModuleChain開始編譯。在 _addModuleChain首先會生成模塊,最後構建。

class

NormalModuleFactory
extends
Tapable

{

// ...
create
(
data
,
callback
)

{

// ...

this
.

hooks
.
beforeResolve
.
callAsync
(

{
contextInfo
,
resolveOptions
,
context
,
request
,
dependencies

},

(
err
,
result
)

=>

{

if

(
err
)

return
callback
(
err
);

// Ignored

if

(!
result
)

return
callback
();

// factory 鉤子會觸發 resolver 鉤子執行,而resolver鉤子中會利用acorn 處理js生成AST,再利用acorn處理前,會使用loader加載文件

const
factory
=

this
.
hooks
.
factory
.
call
(
null
);
factory
(
result
,

(
err
,
module
)

=>

{

if

(
err
)

return
callback
(
err
);

if

(
module
&&

this
.
cachePredicate
(
module
))

{

for

(
const
d of dependencies
)

{
d
.
__NormalModuleFactoryCache
=
module
;

}

}
callback
(
null
,
module
);

});

}

);

}
}

在編譯完成後,調用 compilation.seal方法封閉,生成資源,這些資源保存在 compilation.assets, compilation.chunk, 在給webpack寫插件的時候會用到。

class

Compiler
extends
Tapable

{
constructor
(
context
)

{
super
();

this
.
hooks
=

{
beforeRun
:

new

AsyncSeriesHook
([
"compilation"
]),
run
:

new

AsyncSeriesHook
([
"compilation"
]),
emit
:

new

AsyncSeriesHook
([
"compilation"
]),
afterEmit
:

new

AsyncSeriesHook
([
"compilation"
]),
compilation
:

new

SyncHook
([
"compilation"
,

"params"
]),
beforeCompile
:

new

AsyncSeriesHook
([
"params"
]),
compile
:

new

SyncHook
([
"params"
]),
make
:

new

AsyncParallelHook
([

"compilation"
]),
afterCompile
:

new

AsyncSeriesHook
([
"compilation"
]),

// other hooks

};

// ...

}
run
(
callback
)

{

const
startTime
=

Date
.
now
();

const
onCompiled
=

(
err
,
compilation
)

=>

{

// ...


this
.
emitAssets
(
compilation
,
err
=>

{

if

(
err
)

return
callback
(
err
);

if

(
compilation
.
hooks
.
needAdditionalPass
.
call
())

{
compilation
.
needAdditionalPass
=

true
;

const
stats
=

new


Stats
(
compilation
);
stats
.
startTime
=
startTime
;
stats
.
endTime
=

Date
.
now
();

this
.
hooks
.
done
.
callAsync
(
stats
,
err
=>

{

if

(
err
)

return
callback
(
err
);

this
.

hooks
.
additionalPass
.
callAsync
(
err
=>

{

if

(
err
)

return
callback
(
err
);

this
.
compile
(
onCompiled
);

});

});

return
;

}

// ...

});

};

this
.
hooks
.
beforeRun

.
callAsync
(
this
,
err
=>

{

if

(
err
)

return
callback
(
err
);

this
.
hooks
.
run
.
callAsync
(
this
,
err
=>

{

if

(
err
)

return
callback
(
err
);

this

.
readRecords
(
err
=>

{

if

(
err
)

return
callback
(
err
);

this
.
compile
(
onCompiled
);

});

});

});

}

// 輸出文件到構建目錄
emitAssets
(
compilation
,
callback
)

{

// ...

this
.
hooks

.
emit
.
callAsync
(
compilation
,
err
=>

{

if

(
err
)

return
callback
(
err
);
outputPath
=
compilation
.
getPath
(
this
.
outputPath
);

this
.
outputFileSystem
.
mkdirp
(
outputPath
,
emitFiles
);

});

}
newCompilationParams
()


{

const
params
=

{
normalModuleFactory
:

this
.
createNormalModuleFactory
(),
contextModuleFactory
:

this
.
createContextModuleFactory
(),
compilationDependencies
:

new

Set
()

};

return
params
;

}
compile
(
callback
)

{

const
params
=

this
.

newCompilationParams
();

this
.
hooks
.
beforeCompile
.
callAsync
(
params
,
err
=>

{

if

(
err
)

return
callback
(
err
);

this
.
hooks
.
compile
.
call
(
params
);

const
compilation
=

this
.
newCompilation
(
params

);

this
.
hooks
.
make
.
callAsync
(
compilation
,
err
=>

{

if

(
err
)

return
callback
(
err
);
compilation
.
finish
();

// make 鉤子執行後,調用seal生成資源
compilation
.
seal
(
err
=>

{

if

(
err
)

return
callback
(
err
);

this
.
hooks
.
afterCompile
.
callAsync
(
compilation
,
err
=>

{

if

(
err
)

return
callback
(
err
);

// emit, 生成最終文件

return
callback
(
null
,
compilation
);

});

});

});

});


}
}

最後輸出

在 seal執行後,便會調用 emit鉤子,根據webpack config文件的output配置的path屬性,將文件輸出到指定的path.

Webpack 之詳解



分享到:


相關文章: