webpack 最出色的功能之一就是,除了JavaScript
,還能夠經過loader
引入 任何其餘類型的文件。
Entry
(入口):Webpack 執行構建的第一步將從 Entry 開始,可抽象成輸入。Output
(出口):指示 webpack 如何去輸出、以及在哪裏輸出Module
(模塊):在 Webpack 裏一切皆模塊,一個模塊對應着一個文件。Webpack 會從配置的 Entry 開始遞歸找出全部依賴的模塊。Chunk
(代碼塊):一個 Chunk 由多個模塊組合而成,用於代碼合併與分割。Loader
(模塊轉換器):用於把模塊原內容按照需求轉換成新內容。Plugin
(擴展插件):在 Webpack 構建流程中的特定時機會廣播出對應的事件,插件能夠監聽這些事件,並改變輸出結果entry: { a: "./app/entry-a", b: ["./app/entry-b1", "./app/entry-b2"] },
多入口能夠經過 HtmlWebpackPlugin
分開注入css
plugins: [ new HtmlWebpackPlugin({ chunks: ['a'], filename: 'test.html', template: 'src/assets/test.html' }) ]
修改路徑相關html
publicPath
:並不會對生成文件的目錄形成影響,主要是對你的頁面裏面引入的資源的路徑作對應的補全filename
:能修改文件名,也能更改文件目錄導出庫相關前端
library
: 導出庫的名稱libraryTarget
: 通用模板定義方式webpack 一切皆模塊,配置項 Module,定義模塊的各類操做,vue
Module 主要配置:node
loader
: 各類模塊轉換器extensions
:使用的擴展名alias
:別名、例如:vue-cli 經常使用的 @
出自此處plugins
: 插件列表devServer
:開發環境相關配置,譬如 proxy
externals
:打包排除模塊target
:包應該運行的環境,默認 web
webpack從啓動到結束會依次執行如下流程:jquery
Compiler
實例apply
方法,給插件傳入compiler
實例的引用,插件經過compiler調用Webpack提供的API,讓插件能夠監聽後續的全部事件節點。loader
將文件解析成抽象語法樹 AST
chunk
ps:因爲 webpack 是根據依賴圖動態加載全部的依賴項,因此,每一個模塊均可以明確表述自身的依賴,能夠避免打包未使用的模塊。webpack
Babel 是一個工具鏈,主要用於將 ECMAScript 2015+ 版本的代碼轉換爲向後兼容的 JavaScript
語法,以便可以運行在當前和舊版本的瀏覽器或其餘環境中:git
Babel 內部所使用的語法解析器是 Babylon
主要功能github
Polyfill
方式在目標環境中添加缺失的特性 (經過 @babel/polyfill
模塊)codemods
)主要模塊web
@babel/parser
:負責將代碼解析爲抽象語法樹@babel/traverse
:遍歷抽象語法樹的工具,咱們能夠在語法樹中解析特定的節點,而後作一些操做@babel/core
:代碼轉換,如ES6的代碼轉爲ES5的模式在使用 webpack 構建的典型應用程序或站點中,有三種主要的代碼類型:
library
或 "vendor
" 代碼。webpack
的 runtime
使用 manifest
管理全部模塊的交互。runtime
:在模塊交互時,鏈接模塊所需的加載和解析邏輯。包括瀏覽器中的已加載模塊的鏈接,以及懶加載模塊的執行邏輯。
manifest
:當編譯器(compiler)開始執行、解析和映射應用程序時,它會保留全部模塊的詳細要點。這個數據集合稱爲 "Manifest",
當完成打包併發送到瀏覽器時,會在運行時經過 Manifest 來解析和加載模塊。不管你選擇哪一種模塊語法,那些 import 或 require 語句如今都已經轉換爲 webpack_require 方法,此方法指向模塊標識符(module identifier)。經過使用 manifest 中的數據,runtime 將可以查詢模塊標識符,檢索出背後對應的模塊。
其中:
import
或 require
語句會轉換爲 __webpack_require__
require.ensure
(在Webpack 4 中會使用 Promise 封裝)gulp
是任務執行器(task runner):就是用來自動化處理常見的開發任務,例如項目的檢查(lint)、構建(build)、測試(test)webpack
是打包器(bundler):幫助你取得準備用於部署的 JavaScript 和樣式表,將它們轉換爲適合瀏覽器的可用格式。例如,JavaScript 能夠壓縮、拆分 chunk 和懶加載,loader
就是一個js文件,它導出了一個返回了一個 buffer
或者 string
的函數;
譬如:
// log-loader.js module.exports = function (source) { console.log('test...', source) return source }
在 use 時,若是 log-loader
並無在 node_modules
中,那麼可使用路徑導入。
plugin: 是一個含有 apply
方法的 類
。
譬如:
class DemoWebpackPlugin { constructor () { console.log('初始化 插件') } apply (compiler) { } } module.exports = DemoWebpackPlugin
apply 方法中接收一個 compiler
參數,也就是 webpack實例。因爲該參數的存在 plugin 能夠很好的運用 webpack 的生命週期鉤子,在不一樣的時間節點作一些操做。
Webpack 加快打包速度的方法
include
或 exclude
加快文件查找速度HappyPack
開啓多進程 Loader
轉換ParallelUglifyPlugin
開啓多進程 JS 壓縮使用 DllPlugin
+ DllReferencePlugin
分離打包
庫
和 項目代碼
分離打包cache-loader
)Webpack 加快代碼運行速度方法
prefetch
) || 預加載(preload
)webpack-bundle-analyzer
代碼分析動態導入
import(/* webpackChunkName: "lodash" */ 'lodash') // 註釋中的使用webpackChunkName。 // 這將致使咱們單獨的包被命名,lodash.bundle.js // 而不是just [id].bundle.js。
預取(prefetch
):未來可能須要一些導航資源
chunk
加載完成,webpack
就會添加 prefetch
import(/* webpackPrefetch: true */ 'LoginModal'); // 將<link rel="prefetch" href="login-modal-chunk.js">其附加在頁面的開頭
預加載(preload
):當前導航期間可能須要資源
preload
chunk 會在父 chunk 加載時,以並行方式開始加載webpackPreload
會有損性能,import(/* webpackPreload: true */ 'ChartingLibrary'); // 在加載父 chunk 的同時 // 還會經過 <link rel="preload"> 請求 charting-library-chunk
爲了極大減小構建時間,進行分離打包。
DllReferencePlugin 和 DLL插件DllPlugin 都是在_另外_的 webpack 設置中使用的。
DllPlugin
這個插件是在一個額外的獨立的 webpack 設置中建立一個只有 dll 的 bundle(dll-only-bundle)。 這個插件會生成一個名爲 manifest.json 的文件,這個文件是用來讓 DLLReferencePlugin
映射到相關的依賴上去的。
webpack.vendor.config.js
new webpack.DllPlugin({ context: __dirname, name: "[name]_[hash]", path: path.join(__dirname, "manifest.json"), })
webpack.app.config.js
new webpack.DllReferencePlugin({ context: __dirname, manifest: require("./manifest.json"), name: "./my-dll.js", scope: "xyz", sourceType: "commonjs2" })
經過將公共模塊拆出來,最終合成的文件可以在最開始的時候加載一次,便存到緩存中供後續使用。這個帶來速度上的提高,由於瀏覽器會迅速將公共的代碼從緩存中取出來,而不是每次訪問一個新頁面時,再去加載一個更大的文件。
若是把公共文件提取出一個文件,那麼當用戶訪問了一個網頁,加載了這個公共文件,再訪問其餘依賴公共文件的網頁時,就直接使用文件在瀏覽器的緩存,這樣公共文件就只用被傳輸一次。
entry: { vendor: ["jquery", "other-lib"], // 明確第三方庫 app: "./entry" }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: "vendor", // filename: "vendor.js" // (給 chunk 一個不一樣的名字) minChunks: Infinity, // (隨着 entry chunk 愈來愈多, // 這個配置保證沒其它的模塊會打包進 vendor chunk) }) ] // 打包後的文件 <script src="vendor.js" charset="utf-8"></script> <script src="app.js" charset="utf-8"></script>
基本上腳手架都包含了該插件,該插件會分析JS代碼語法樹,理解代碼的含義,從而作到去掉無效代碼、去掉日誌輸入代碼、縮短變量名等優化。
const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin'); //... plugins: [ new UglifyJSPlugin({ compress: { warnings: false, //刪除無用代碼時不輸出警告 drop_console: true, //刪除全部console語句,能夠兼容IE collapse_vars: true, //內嵌已定義但只使用一次的變量 reduce_vars: true, //提取使用屢次但沒定義的靜態值到變量 }, output: { beautify: false, //最緊湊的輸出,不保留空格和製表符 comments: false, //刪除全部註釋 } }) ]
ExtractTextPlugin 從 bundle 中提取文本(CSS)到單獨的文件,PurifyCSSPlugin純化CSS(其實用處沒多大)
module.exports = { module: { rules: [ { test: /\.css$/, loader: ExtractTextPlugin.extract({ fallback: 'style-loader', use: [ { loader: 'css-loader', options: { localIdentName: 'purify_[hash:base64:5]', modules: true } } ] }) } ] }, plugins: [ ..., new PurifyCSSPlugin({ purifyOptions: { whitelist: ['*purify*'] } }) ] };
DefinePlugin可以自動檢測環境變化,效率高效。
在前端開發中,在不一樣的應用環境中,須要不一樣的配置。如:開發環境的API Mocker、測試流程中的數據僞造、打印調試信息。若是使用人工處理這些配置信息,不只麻煩,並且容易出錯。
使用DefinePlugin
配置的全局常量
注意,由於這個插件直接執行文本替換,給定的值必須包含字符串自己內的實際引號。一般,有兩種方式來達到這個效果,使用 ' "production" '
, 或者使用 JSON.stringify('production')
。
new webpack.DefinePlugin({ // 固然,在運行node服務器的時候就應該按環境來配置文件 // 下面模擬的測試環境運行配置 'process.env':JSON.stringify('dev'), WP_CONF: JSON.stringify('dev'), }),
測試DefinePlugin
:編寫
if (WP_CONF === 'dev') { console.log('This is dev'); } else { console.log('This is prod'); }
打包後WP_CONF === 'dev'
會編譯爲false
if (false) { console.log('This is dev'); } else { console.log('This is prod'); }
當使用了DefinePlugin
插件後,打包後的代碼會有不少冗餘。能夠經過UglifyJsPlugin
清除不可達代碼。
[ new UglifyJsPlugin({ uglifyOptions: { compress: { warnings: false, // 去除warning警告 dead_code: true, // 去除不可達代碼 }, warnings: false } }) ]
最後的打包打包代碼會變成console.log('This is prod')
附Uglify文檔:https://github.com/mishoo/Ugl...
使用DefinePlugin區分環境 + UglifyJsPlugin清除不可達代碼,以減輕打包代碼體積
HappyPack能夠開啓多進程Loader轉換,將任務分解給多個子進程,最後將結果發給主進程。
使用
exports.plugins = [ new HappyPack({ id: 'jsx', threads: 4, loaders: [ 'babel-loader' ] }), new HappyPack({ id: 'styles', threads: 2, loaders: [ 'style-loader', 'css-loader', 'less-loader' ] }) ]; exports.module.rules = [ { test: /\.js$/, use: 'happypack/loader?id=jsx' }, { test: /\.less$/, use: 'happypack/loader?id=styles' }, ]
ParallelUglifyPlugin能夠開啓多進程壓縮JS文件
import ParallelUglifyPlugin from 'webpack-parallel-uglify-plugin'; module.exports = { plugins: [ new ParallelUglifyPlugin({ test, include, exclude, cacheDir, workerCount, sourceMap, uglifyJS: { }, uglifyES: { } }), ], };
webpack打包結果分析插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports = { plugins: [ new BundleAnalyzerPlugin() ] }
減少文件搜索範圍,從而提高速度
示例
{ test: /\.css$/, include: [ path.resolve(__dirname, "app/styles"), path.resolve(__dirname, "vendor/styles") ] }
這玩意不是插件,是wenpack的配置選項
externals 配置選項提供了「從輸出的 bundle 中排除依賴」的方法。相反,所建立的 bundle 依賴於那些存在於用戶環境(consumer's environment)中的依賴。此功能一般對 library 開發人員來講是最有用的,然而也會有各類各樣的應用程序用到它。
entry: { entry: './src/main.js', vendor: ['vue', 'vue-router', 'vuex'] }, externals: { // 從輸出的 bundle 中排除 echarts 依賴 echarts: 'echarts', }
Hot Module Replacement(簡稱 HMR)
包含如下內容:
webpack-dev-middleware 調用 webpack 的 api 對文件系統 watch,當文件發生改變後,webpack 從新對文件進行編譯打包,而後保存到內存中。
webpack 將 bundle.js 文件打包到了內存中,不生成文件的緣由就在於訪問內存中的代碼比訪問文件系統中的文件更快,並且也減小了代碼寫入文件的開銷。
這一切都歸功於memory-fs,memory-fs 是 webpack-dev-middleware 的一個依賴庫,webpack-dev-middleware 將 webpack 本來的 outputFileSystem 替換成了MemoryFileSystem 實例,這樣代碼就將輸出到內存中。
webpack-dev-middleware 中該部分源碼以下:
// compiler // webpack-dev-middleware/lib/Shared.js var isMemoryFs = !compiler.compilers && compiler.outputFileSystem instanceof MemoryFileSystem; if(isMemoryFs) { fs = compiler.outputFileSystem; } else { fs = compiler.outputFileSystem = new MemoryFileSystem(); }
在啓動 devServer 的時候,sockjs) 在服務端和瀏覽器端創建了一個 webSocket 長鏈接,以便將 webpack 編譯和打包的各個階段狀態告知瀏覽器,最關鍵的步驟仍是 webpack-dev-server 調用 webpack api 監聽 compile的 done 事件,當compile 完成後,webpack-dev-server經過 _sendStatus 方法將編譯打包後的新模塊 hash 值發送到瀏覽器端。
// webpack-dev-server/lib/Server.js compiler.plugin('done', (stats) => { // stats.hash 是最新打包文件的 hash 值 this._sendStats(this.sockets, stats.toJson(clientStats)); this._stats = stats; }); ... Server.prototype._sendStats = function (sockets, stats, force) { if (!force && stats && (!stats.errors || stats.errors.length === 0) && stats.assets && stats.assets.every(asset => !asset.emitted) ) { return this.sockWrite(sockets, 'still-ok'); } // 調用 sockWrite 方法將 hash 值經過 websocket 發送到瀏覽器端 this.sockWrite(sockets, 'hash', stats.hash); if (stats.errors.length > 0) { this.sockWrite(sockets, 'errors', stats.errors); } else if (stats.warnings.length > 0) { this.sockWrite(sockets, 'warnings', stats.warnings); } else { this.sockWrite(sockets, 'ok'); } };
webpack-dev-server 修改了webpack 配置中的 entry 屬性,在裏面添加了 webpack-dev-client 的代碼,這樣在最後的 bundle.js 文件中就會接收 websocket 消息的代碼了。
webpack-dev-server/client 當接收到 type 爲 hash 消息後會將 hash 值暫存起來,當接收到 type 爲 ok 的消息後對應用執行 reload 操做。
在 reload 操做中,webpack-dev-server/client 會根據 hot 配置決定是刷新瀏覽器仍是對代碼進行熱更新(HMR)。代碼以下:
// webpack-dev-server/client/index.js hash: function msgHash(hash) { currentHash = hash; }, ok: function msgOk() { // ... reloadApp(); }, // ... function reloadApp() { // ... if (hot) { log.info('[WDS] App hot update...'); const hotEmitter = require('webpack/hot/emitter'); hotEmitter.emit('webpackHotUpdate', currentHash); // ... } else { log.info('[WDS] App updated. Reloading...'); self.location.reload(); } }
首先 webpack/hot/dev-server(如下簡稱 dev-server) 監聽第三步 webpack-dev-server/client 發送的 webpackHotUpdate
消息,調用 webpack/lib/HotModuleReplacement.runtime(簡稱 HMR runtime)中的 check 方法,檢測是否有新的更新。
在 check 過程當中會利用 webpack/lib/JsonpMainTemplate.runtime(簡稱 jsonp runtime)中的兩個方法 hotDownloadManifest 和 hotDownloadUpdateChunk。
hotDownloadManifest 是調用 AJAX 向服務端請求是否有更新的文件,若是有將發更新的文件列表返回瀏覽器端。該方法返回的是最新的 hash 值。
hotDownloadUpdateChunk 是經過 jsonp 請求最新的模塊代碼,而後將代碼返回給 HMR runtime,HMR runtime 會根據返回的新模塊代碼作進一步處理,多是刷新頁面,也多是對模塊進行熱更新。該 方法返回的就是最新 hash 值對應的代碼塊。
最後將新的代碼塊返回給 HMR runtime,進行模塊熱更新。
附:爲何更新模塊的代碼不直接在第三步經過 websocket 發送到瀏覽器端,而是經過 jsonp 來獲取呢?
個人理解是,功能塊的解耦,各個模塊各司其職,dev-server/client 只負責消息的傳遞而不負責新模塊的獲取,而這些工做應該有 HMR runtime 來完成,HMR runtime 才應該是獲取新代碼的地方。再就是由於不使用 webpack-dev-server 的前提,使用 webpack-hot-middleware 和 webpack 配合也能夠完成模塊熱更新流程,在使用 webpack-hot-middleware 中有件有意思的事,它沒有使用 websocket,而是使用的 EventSource。綜上所述,HMR 的工做流中,不該該把新模塊代碼放在 websocket 消息中。
這一步是整個模塊熱更新(HMR)的關鍵步驟,並且模塊熱更新都是發生在HMR runtime 中的 hotApply 方法中
// webpack/lib/HotModuleReplacement.runtime function hotApply() { // ... var idx; var queue = outdatedModules.slice(); while(queue.length > 0) { moduleId = queue.pop(); module = installedModules[moduleId]; // ... // remove module from cache delete installedModules[moduleId]; // when disposing there is no need to call dispose handler delete outdatedDependencies[moduleId]; // remove "parents" references from all children for(j = 0; j < module.children.length; j++) { var child = installedModules[module.children[j]]; if(!child) continue; idx = child.parents.indexOf(moduleId); if(idx >= 0) { child.parents.splice(idx, 1); } } } // ... // insert new code for(moduleId in appliedUpdate) { if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) { modules[moduleId] = appliedUpdate[moduleId]; } } // ... }
模塊熱更新的錯誤處理,若是在熱更新過程當中出現錯誤,熱更新將回退到刷新瀏覽器,這部分代碼在 dev-server 代碼中,簡要代碼以下:
module.hot.check(true).then(function(updatedModules) { if(!updatedModules) { return window.location.reload(); } // ... }).catch(function(err) { var status = module.hot.status(); if(["abort", "fail"].indexOf(status) >= 0) { window.location.reload(); } });
當用新的模塊代碼替換老的模塊後,可是咱們的業務代碼並不能知道代碼已經發生變化,也就是說,當 hello.js 文件修改後,咱們須要在 index.js 文件中調用 HMR 的 accept 方法,添加模塊更新後的處理函數,及時將 hello 方法的返回值插入到頁面中。代碼以下
// index.js if(module.hot) { module.hot.accept('./hello.js', function() { div.innerHTML = hello() }) }