網上已經有不少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
更多資料: