認識Webpack
網上已經有不少Webpack教程入門教程了。 本文記錄了我以我的方式方法、思路認識瞭解Webpack。從官方的Tutorial入手,不斷提出問題、解決,一步一步認識Webpack。
從早期的自己寫指令碼,到現在的各種構建工具,前端工程化已經發展到新的階段了。
早先在百度地圖的時候,地圖程式碼用PHP進行簡單粗放的處理。這個階段算是最原始的自己寫指令碼處理。後來我用Ruby寫了一套集合了開發、動態合併、mock資料、一鍵build的工具。這算是更進了一步。
現在基於Nodejs的任務管理工具Grunt、Glup都提供了程式碼合併、壓縮、各種JS Transpiler、CSS預處理、各種前端模板的處理。
在Grunt、Gulp中是通過第三方庫進行編譯的。 在Webpack中也是類似的,只不過是增加了Loader的概念。通過一系列“Loader”完成處理。 處理之後統一輸出為JS程式碼。
初步認識
在深入之前,你需要先照著官方教程實踐一下。有個感性的認識。 完成教程的getting started部分後,你可以初步得出以下結論:
- Webpack是一個用來打包js工程的工具。官方定義為
Module bundler
- Webpack命令列引數可以配置到名為
webpack.config.js
(或webpackfile
)的配置檔案中 - Webpack提供一個可以檢測檔案變化並編譯然後重新整理瀏覽器的
webpack-dev-server
那麼問題來了: 對於如Grunt、Webpack這種通過配置工作的工具來說,有哪些配置可用,配置的行為、配置的可選值,需要完備的文件才好會用。 幸好Webpack官網提供了詳盡的文件
下面是主要配置項的簡要說明
context 工程目錄,必須是絕對路徑
entry 打包生成的bundle。可以是多個
output 生成的檔案配置選項
output.filename 生成的檔名模板,比如 "[name].bundle.js"
output.path 生成的檔案目錄,絕對路徑
output.publicPath 線上靜態資源目錄
output.chunkFilename 程式碼塊檔名模板
output.sourceMapFilename source-map檔名模板。預設是[file].map
output.devtoolModuleFilenameTemplate
output.devtoolFallbackModuleFilenameTemplate
output.devtoolLineToLine
output.hotUpdateChunkFilename
output.hotUpdateMainFilename
output.jsonpFunction JSONP非同步載入程式碼塊(chunk)時JSONP函式名,預設是webpackJsonp
output.hotUpdateFunction JSONP非同步熱更新程式碼塊時JSONP函式名,預設是webpackHotUpdate
output.pathinfo 是否以註釋形式在require中增加模組path資訊
output.library bundle作為庫輸出,值為庫名
output.libraryTarget 輸出庫的格式。比如可選amd,umd,commonjs等
output.umdNamedDefine
output.sourcePrefix
output.crossOriginLoading
module
module.loaders Loader配置
module.preLoaders, module.postLoaders preLoader和postLoader配置
module.noParse 不需要loader編譯的檔案
resolve 模組決議配置
resolve.alias 模組別名
resolve.root 模組根目錄,絕對路徑
resolve.modulesDirectories 模組目錄,工作方式類似node_modules。預設值是["web_modules", "node_modules"]
resolve.fallback 如果在root和modulesDirectories都找不到,會在這裡搜尋
resolve.extensions 用於模組查詢的副檔名。
resolve.packageMains
resolve.packageAlias
resolve.unsafeCache
resolveLoader 與resolve類似,不過是給loader模組決議使用的配置
resolveLoader.moduleTemplates
externals
target 目標環境,程式碼是用於web還是node還是electron環境等等
bail
profile 每個模組的時間打點資訊
cache 是否開啟編譯快取以提高效能。watch模式預設開啟
debug 設定loaders為debug模式
devtool 用於方便除錯的開發工具選項。比如source-map方便除錯混淆後的程式碼
devServer 傳給webpack-dev-server的引數
node 傳遞給node作為polyfills和mocks的引數
amd require.mad和define.amd對應的值。比如{jQuery:true}
loader 提供給loader的額外資訊
recordsPath, recordsInputPath, recordsOutputPath
plugins 外掛配置
Loaders
我們最關心的是有哪些loader可以用呢? 通過在 https://github.com/webpack 搜尋專案名中包含-loader
。我找到了這些官方提供的loader:
#裸資料
raw-loader
#指令碼程式碼
coffee-loader
script-loader
#樣式相關
css-loader
style-loader
less-loader
#html相關
html-loader
jade-loader
#json相關
json-loader
json5-loader
#其他
worker-loader-loader imports-loader exports-loader source-map-loader coffee-redux-loader multi-loader react-proxy-loader expose-loader url-loader node-loader bundle-loader val-loader transform-loader jshint-loader null-loader coverjs-loader
咦,為什麼有一個css-loader還有一個style-loader?css-loader是用來載入css檔案的 style-loader是用來應用已經載入的css中的樣式的。
在配置檔案中,配置需要使用的loader。test
用來對檔名進行匹配測試,匹配成功的檔案會用對應的loader處理。
module: {
loaders: [
{ test: /\.coffee$/, loader: "coffee-loader" },
{ test: /\.js$/, loader: "jsx-loader" }
]
},
每個loader都有自己獨特的配置,需要參考對應文件。 所有loader都可以配置一下專案:
test 用來對檔名進行匹配測試
exclude 被排除的檔名
include 包含的檔名
loader 歎號分割的loaders
loaders loader陣列
比如babel的配置就有query、cacheDirectory配置項。
可以想象,loader要做的工作無非就是拿到原始碼,根據引數配置進行變換,返回變換後的結果。看一下less-loder
的原始碼:
/**
* 簡化後的虛擬碼
*/
var less = require("less");
var loaderUtils = require("loader-utils");
module.exports = function(source){
//解析loader的query string
var query = loaderUtils.parseQuery(this.query);
//預設less編譯配置
var config = {
filename: this.resource,
compress: !!this.minimize
};
//將query中的配置merge到預設配置中
Object.keys(query).forEach(function(attr){
config[attr] = query[attr]
});
//編譯less
var cb = this.callback;
less.render(source, config, function(e, result){
cb(null, result.css, result.map);
});
};
基本上就是從query讀取配置,呼叫less編譯器編譯原始碼。
官網編寫loader的教程驗證了上述想法。同時也指出了編寫loader時要注意的一些問題。
參考:官方給出的已有loader列表
Plugins
有哪些plugin呢? 通過在 https://github.com/webpack 搜尋專案名中包含-plugin
我找到了這些官方提供的plugin:
extract-text-webpack-plugin
compression-webpack-plugin
i18n-webpack-plugin
component-webpack-plugin
感覺不對啊,那個很多教程中常見的UglifyJsPlugin
都沒有看到啊!那麼只有一個可能,這些plugin都是內建的。在原始碼中一定能找到。clone下來webpack的程式碼。開啟lib,滿眼都是XXXPlugin。在optimize目錄下可以找到UglifyJsPlugin
。大致看一下這些程式碼可以發現,每個Plugin的原型上都有一個apply函式:
/**
* 從UglifyJsPlugin.js簡化而來的虛擬碼
*/
...
var uglify = require("uglify-js");
...
UglifyJsPlugin.prototype.apply = function(compiler) {
...
compiler.plugin("compilation", function (module) {
...
var input = asset.source();
var ast = uglify.parse(input);
//壓縮
if (options.compress !== false) {
var compress = uglify.Compressor(options.comrpess);
ast = ast.transform(compress);
}
//混淆
if (options.mangle !== false) {
ast.mangle_names();
uglify.mangle_properties(ast);
}
//重新從ast生成程式碼
var result = uglify.OutputStream();
ast.print(result);
});
};
可以想象,webpack會根據配置檔案中plugins陣列中的外掛例項,呼叫其apply函式。 在apply函式中,外掛對感興趣的事件(官方叫做stage)註冊處理函式(plugin
)。比如UglifyJsPlugin
就是在compilation
事件觸發時,對原始碼進行壓縮混淆。
通過官網閱讀how-to-write-a-plugin可以驗證了上面的想法。
既然有compilation
事件,那肯定還有其他事件嘍。在lib目錄下搜尋原始碼中的compile.plugin
呼叫
$ ack -Q compiler.plugin( | grep plugin|gawk "{print($2)}"|sort|uniq
compiler.plugin("additional-pass",
compiler.plugin("after-compile",
compiler.plugin("after-environment",
compiler.plugin("after-resolvers",
compiler.plugin("compilation",
compiler.plugin("compile",
compiler.plugin("context-module-factory",
compiler.plugin("done",
compiler.plugin("emit",
compiler.plugin("entry-option",
compiler.plugin("environment",
compiler.plugin("invalid",
compiler.plugin("make",
compiler.plugin("normal-module-factory",
compiler.plugin("run",
compiler.plugin("should-emit",
compiler.plugin("this-compilation",
compiler.plugin("watch-run",
一共有18個事件。官方教程中只介紹了done,compilation,emit
三個。 用同樣的方法,我們還可以查出compilation
支援的事件:
$ ack -Q compilation.plugin( | grep plugin|gawk "{print($2,$3)}"|sort|uniq
compilation.plugin("additional-assets", function(callback)
compilation.plugin("additional-chunk-assets", function()
compilation.plugin("after-hash", function()
compilation.plugin("after-optimize-chunk-assets", function(chunks)
compilation.plugin("after-optimize-tree", function(chunks,
compilation.plugin("before-module-ids", function(modules)
compilation.plugin("build-module", function(module)
compilation.plugin("chunk-hash", function(chunk,
compilation.plugin("failed-module", moduleDone);
compilation.plugin("need-additional-pass", function()
compilation.plugin("normal-module-loader", function(context,
compilation.plugin("normal-module-loader", function(loaderContext)
compilation.plugin("normal-module-loader", function(loaderContext,
compilation.plugin("optimize-assets", function(assets,
compilation.plugin("optimize-chunk-assets", function(chunks,
compilation.plugin("optimize-chunk-ids", function(chunks)
compilation.plugin("optimize-chunk-order", function(chunks)
compilation.plugin("optimize-chunks-advanced", function(chunks)
compilation.plugin("optimize-chunks-basic", function(chunks)
compilation.plugin("optimize-module-order", function(modules)
compilation.plugin("optimize-modules-advanced", function(modules)
compilation.plugin("optimize-tree", function(chunks,
compilation.plugin("record", function(compilation,
compilation.plugin("record-chunks", function(chunks,
compilation.plugin("record-modules", function(modules,
compilation.plugin("revive-chunks", function(chunks,
compilation.plugin("revive-modules", function(modules,
compilation.plugin("seal", function()
compilation.plugin("should-generate-chunk-assets", function()
compilation.plugin("should-record", function()
compilation.plugin("succeed-module", moduleDone);
compilation.plugin(["optimize-chunks", "optimize-extracted-chunks"],
compilation.plugin(["optimize-chunks-basic", "optimize-extracted-chunks-basic"],
有了這兩個列表,在自己編寫外掛就可以有的放矢地參考原始碼了。
參考:官方給出的已有plugin的列表
Webpack-dev-server
Webpack提供一個小巧的基於express的開發伺服器。支援自動重新整理、模組熱替換。還有代理。具體如何配置在這裡。
代理(proxy
)在開發是還是很有用的。你可以將動態請求對映到後端的開發機,方便聯調。
總結
現在照著官方教程你已經可以簡單地使用Webpack了。下一步要做的是
- 瞭解webpack.config.js中如何配置,有哪些要注意的(比如路徑)
- 實踐常用的Loader和Plugin
- 實踐webpack的眾多配置項
- 實踐使用webpack-dev-server進行開發
需要時可以更進一步:
- 學習如何編寫Loader和Plugin
- 閱讀已有Loader和Plugin的原始碼
- 在原始碼中瞭解上面列出的stages的含義
更新:Webpack、Browserify和Gulp三者之間到底是怎樣的關係?
下面是我在知乎的回答:
Task Runner
Gulp、Grunt和Make(常見於c/cpp)、Ant、Maven、Gradle(Java/Android)、Rake、Thor(Ruby)一樣,都是是Task Runner。用來將一些繁瑣的task自動化並處理任務的依賴關係。 其中有些是基於配置描述的,描述邏輯比較費勁,比如Ant基於xml。還有些就是程式碼,比較靈活,個人偏好這種。比如Rake、Thor、Gulp、Gradle。對於Gradle來說也有些蛋疼。因為它本身是Groovy的DSL。如果要深入使用,你還得學一下Groovy語言。其他就好多了Rake、Thor就是寫Ruby;Gulp就是JavaScript。相對門檻低很多。
模組化解決方案
Browserify It provides a way to bundle CommonJS modules together, adheres to the Unix philosophy(小工具協作), is in fact a good alternative to Webpack. Webpack takes a more monolithic(整體解決、大而全) approach than Browserify... is relies on configuration.
上面這些工具在功能上有交集:程式碼的Minify、Concat;資源預處理等;
其實每個工具的官網上都有對工具的設計思想、要解決的問題、與其他工具的對比。自己摘抄下來,做個表格對比一下。高亮出每個工具獨特的特性。這樣你就知道什麼時候需要用哪個工具了。 比如,你的工程模組依賴很簡單,不需要把js或各種資源打包,只需要簡單的合併、壓縮,在頁面中引用就好了。那就不需要Browserify、Webpack。Gulp就夠用了。
反過來,如果你的工程龐大,頁面中使用了很多庫(SPA很容易出現這種情況),那就可以選擇某種模組化方案。至於是用Browserify還是Webpack就需要根據其他因素來判斷了。比如團隊已經在使用了某種方案,大家都比較熟悉了。再比如,你喜歡Unix小工具協作的方式,那就Browserify。
充分了解各種工具、方案,選擇合適的和自己需要的。沒有絕對的好。優點換了場景也會變成缺點。
UPDATE
下面是閒耘™用Makefile管理前端工程任務的例子: https://github.com/hotoo/pinyin/blob/master/Makefile
更多資料: