webpack 開發體驗
webpack 增強開發體驗
原始開發方式:
- 編寫源代碼
- webpack打包
- 運行應用
- 刷新瀏覽器
設想理想的開發環境:
- 以HTTP Server運行
- 不是以文件形式預覽
- 接近生產環境的狀態
- 類似ajax這類api,不支持文件訪問形式
- 自動編譯 + 自動刷新
- 提供Source Map支持
- 調試錯誤時快速定位
實現自動編譯
watch監聽模式:監聽文件變化,自動重新運行打包任務。(類似其他構建工具的watch)
命令行使用方法:--watch
參數啓動監聽模式
例如:yarn webpack --watch
實現編譯後自動刷新瀏覽器
使用browser-sync模塊的--files
參數監聽文件變化,觸發瀏覽器刷新。
例如:browser-sync dist --files "**/*"
上面實現兩個開發體驗的方式的缺點:
- 需要打開兩個終端去執行命令,操作較麻煩。
- webpack頻繁將編譯後的文件寫入磁盤,browser-sync從磁盤中讀取文件,效率上降低了。
Webpack Dev Server 實現自動編譯 + 自動刷新
webpack-dev-server 是 webpack官方的開發工具。
- 它提供用於開發的 HTTP Server 服務器
- 集成了「自動編譯」和「自動刷新瀏覽器」等功能
安裝yarn add webpack-dev-server --dev
它提供一個webpack-dev-server
的命令。
webpack-dev-server爲了提高開發效率,並沒有將打包結果寫入到磁盤當中。
它將打包結果,暫時存放在內存中。
內部的HTTP Server從內存中讀取這些文件,發送給瀏覽器。
這樣減少很多不必要的磁盤讀寫操作,從而大大提高構建效率。
可以添加--open
參數,使啓動服務後立即從瀏覽器打開。
Webpack Dev Server 靜態資源訪問
Dev Server 默認會將構建結果輸出的文件,全部作爲開發服務器的資源文件(即默認只會serve打包輸出的文件)
也就是說,只要是webpack輸出的文件,都可以直接被訪問。
但是還有一些沒有參與構建的靜態資源也需要serve,就需要額外的告訴Webpack Dev Server
webpack配置的devServer.contentBase
屬性,可以額外的爲開發服務器指定查找資源目錄。
它可以接收表示目錄的字符串或數組。
配置contentBase替代copy插件
由於webpack打包任務可能使用copy(copy-webpack-plugin)插件將靜態資源文件拷貝到輸出目錄(Dev Server是將拷貝的內容存儲在內存中),所以運行HTTP可以訪問到這些靜態資源。
但是,由於開發階段修改代碼會頻繁重複的執行webpack打包任務。
如果拷貝的文件比較多或比較大,每次執行copy任務,打包的開銷就比較大,並且會降低速度。
所以拷貝任務一般會配置在打包發佈版本的階段執行,而開發階段使用配置額外資源的查找路徑devServer.contentBase
的方式去訪問。
Webpack Dev Server 代理 API 服務
由於Dev Server啓動了一個本地的開發服務器,默認http://localhost:8080
。
當請求後端發佈到線上的API時,會因爲跨域而請求失敗。
雖然可以通過配置CORS時,API支持跨域。
但這需要後端和服務器配合,而且並不是任何情況下API都應該支持CORS。
例如:前後端同源部署,即發佈後,前後端在同一個域名、協議、端口下,就沒有必要開啓CORS。
所以解決 「開發階段接口跨域問題」 的最好的辦法就是在開發服務器當中配置**「代理服務」**。
也就是將接口服務,代理到本地的開發服務地址。
Webpack Dev Server 支持通過配置(devServer.proxy
)的方式,添加代理服務。
實現:將GitHub API 代理到開發服務器
目標:將API(https://api.github.com/
)代理到本地開發服務器。
github接口的Endpoint一般都是在根目錄下。
例如 https://api.github.com/users
Endpoint 可以理解爲 接口端點/入口
webpack通過devServer.proxy
對象配置代理服務。
對象中的每個屬性,都是一個代理規則的配置。
- 屬性的名稱(
key
)就是需要代理的請求路徑的前綴,例如'/api'
。 - 屬性的值(
value
)是爲這個前綴匹配的代理規則配置。target
:代理目標,即訪問key
相當於訪問target/key
,他會將key
添加到後面,可通過pathRewrite
實現代理路徑的重寫。pathRewrite
:重寫代理路徑。它接收一個對象,key是正則匹配的路徑字符串,value是要替換的內容。- 它修改的是path路徑(參考location.pathname),例如
https://api.github.com/api/users
修改的是/api/users
。
- 它修改的是path路徑(參考location.pathname),例如
changeOrigin
:設置爲true
。
Host 和 changeOrigin
HTTP請求頭(Request Headers)中必須包含一個 「host」 頭字段
「host」 請求頭指明瞭 服務器的域名 和 以及(可選的)端口號。(也有說是 指明瞭主機名 和 端口號)
如果沒有給定 端口號,會自動使用被請求服務的默認端口。
例如:請求https://api.github.com/api/users
時,請求頭的 「host」 爲api.github.com
(默認80端口)
「host」的意義:一般情況下,服務器會配置多個網站,服務器端需要根據 「host」 判斷當前請求是哪個網站,從而把這個請求指派到對應的網站。
Webpack Dev Server 在客戶端對代理後的地址發起請求時,請求的地址是http://localhost:8080/api/users
,所以請求頭的 「host」 爲localhost:8080
。
代理背後又去請求被代理的地址https://api.github.com/users
,請求的過程中同樣會帶一個 「host」,而代理服務默認使用用戶在客戶端發起請求的 「host」,即localhost:8080
。
而localhost:8080
並不是GitHub配置的網站。請求頭應爲實際請求地址的「host」,即api.github.com
。
配置changeOrigin
爲true
,就會以實際發生代理請求的「host」(api.github.com
)作爲發起請求的「host」。
這樣就不用關心,最終會把它代理成了什麼樣。
Source Map
通過構建編譯,可以將開發環境的源代碼轉化爲能在生產環境運行的代碼。
這使得 運行代碼 完全不同於 源代碼。
由於調試和報錯都是基於運行代碼。如果需要調試應用,或運行應用時報出了錯誤,就無法定位。
Source Map(源代碼地圖) 就是解決這類問題最好的辦法。
它用來映射 轉換後的代碼(compiled) 與 源代碼(source) 之間的關係。
轉換後的代碼,通過轉換過程中生成的 Source Map 解析,就可以逆向得到源代碼。
Source Map 文件
目前很多第三方的庫在打包後都會生成一個.map
後綴的Source Map文件。
它是一個 json 格式的文件,主要包含以下屬性:
- version:表示當前文件所使用source map標準的版本
- sources:記錄轉換之前源文件的名稱
- 可能是多個文件合併轉換成一個文件,所以它是數組形式
- names:記錄源代碼中使用的成員名稱
- 壓縮代碼時,會將開發階段編寫的有意義的變量名替換爲簡短的字符,從而去壓縮整體代碼的體積
- names記錄的就是原始對應的名稱
- mappings:記錄轉換後的代碼當中的字符,與轉換前所對應的映射關係。
- 它是整個source map的核心屬性。
- 他是一個 Base64 VLQ 編碼的字符串
{
"version": 3。
"sources": ["jquery.js"],
"names": [...],
"mappings": "Base64 VLQ編碼字符串"
}
Source Map 文件使用
可以在轉換後的文件中通過添加註釋的方式引入source map文件。例如:
// jquery.min.js
// ...轉換後的代碼
//# sourceMappingURL=jquery.min.map
引入後,如果在瀏覽器中打開開發人員工具,開發人員工具在加載到這個js文件時發現有這個註釋,它就會自動去請求這個source map文件。
然後根據這個文件的內容,逆向解析對應的源代碼,以便於調試。(在開發人員工具的sources面板就會多出一個解析後的源文件)
同時因爲有了映射的關係,如果源代碼中出現了錯誤,也能很容易定位到源代碼中對應的位置。
source map文件主要用於調試和定位錯誤,所以它對生產環境沒有太大的意義,所以生產環境一般不需要生成source map文件。
Source Map 總結
解決了在前端方向引入了構建編譯之類的概念之後,導致前端編寫的代碼與運行的代碼之間不一樣所產生的調試的問題。
webpack 配置 Source Map
webpack支持對打包後的結果生成對應的source map文件。
可通過devtool
屬性配置指定一個生成方式。
例如:devtool: 'source-map'
webpack 基於對source map不同風格的支持,提供了12種不同的模式(實現方式)。
每種方式的 效率 和 效果 各不相同。
簡單表現爲:效果越少的,生成速度越快。
webpack官方文檔 提供了一個 devtool
不 同模式對比表。
分別從 初次構建(打包)速度「build」、監視模式重新打包速度「rebuild」、是否適合在生產環境中使用「production」 以及 所生成的 source map 的質量「quality」4個維度對比了不同方式之間的差異。
webpack期望設置devtool時,使用特定的順序(eval (none)除外):
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
webpack的配置文件,一般返回一個配置對象,也可以返回由多個配置對象組成的數組,從而實現一次構建執行多個配置任務。
以下使用這種方式查看不同devtool模式的差異。
source-map
會生成對應的source map文件,並以常規方式在打包文件最後添加sourceMappingsURL註釋
eval
eval即js當中的eval函數。
eval('console.log(123)')
會講js代碼默認運行在一個虛擬機環境中,在開發者環境中執行這條語句,可以看到它的來源指向VM**
,點擊可跳轉到sources面板查看它的源代碼,tab名即虛擬機環境名稱VM**
。
可以通過 sourceURL 修改它的運行環境的 名稱/所屬文件路徑 。
它修改的只是個標識而已,代碼依然在虛擬機上運行。
執行eval('console.log(123)' //# sourceURL=./foo/bar.js)
,它的來源就會指向./foo/bar.js
。
使用 eval 模式 ,會在打包文件中將要執行的代碼放到eval()方法中執行,並且在eval函數執行的字符串最後,通過sourceURL去說明所對應的模塊文件路徑。
eval 模式 只指明瞭對應模塊的文件路徑,並沒有指定source map路徑(實際上也沒有生成source map)。
如此,瀏覽器在通過eval執行這段代碼時,就知道所對應的源代碼文件。查看源代碼時,只能看到對應的模塊打包後的代碼。
- eval模式只能正確定位到代碼所屬的模塊文件(路徑)
- 這種模式不會生成 source map,也就是和 source map 沒有太大關係。
- 構建速度最快:不需要生成 source.map
- 效果最差:只能定位源代碼文件的路徑,而不知道具體的行列信息
eval-source-map
與eval模式類似,但它查看的代碼內容,是編譯前的內容,所以它能定位到具體的行和列的信息。
原因是它生成了一個 Data URLs 地址的 source map。
// eval執行的字符串
// ...執行代碼
//# sourceURL=[module]
//# sourceMappingURL=data:application/json;charset=utf-8;base64,[base64內容]
eval-cheap-source-map
cheap 表示會生成 廉價(閹割版) 的source-map。
效果:
- 查看的源碼是經過loader轉換後的代碼(如果配置了對應的loader),導致定位到的行與實際源代碼不一致。
- 無法定位到列。
- 表現爲通過開發人員工具跳轉到源代碼時,光標只會定位到代碼的行,不會定位到代碼的列。
由於少了一些效果,所以生成速度比 eval-source-map快很多。
eval-cheap-module-source-map
與eval-cheap-source-map的區別是,查看的源碼與實際源文件一樣(loader轉換前)。
但同樣無法定位列。
devtool 總結
devtool是將幾種配置拼接在一起使用,webpack期望設置devtool時,使用特定的順序(eval (none)除外):
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
拆解介紹:
-
eval
:是否使用eval執行模塊代碼。 -
inline
:指定source map以Data URLs方式嵌入到打包文件。 -
hidden
:指定不會在打包文件中,通過註釋引入source map文件。- 一般用於開發第三方包時使用。
-
nosources
:在開發人員工具中會看到行列信息,但無法看到源碼(報錯:Could not load content for xxxx)。- 用於在生產環境避免其他人看到源碼的同時,定位錯誤。
- 可能是通過未定義
sourcesContent
實現。
-
source-map
:表示會生成source map,eval/inline模式會以 Data URLs 形式嵌入到打包文件中,其他模式以物理文件(.map
)形式生成 -
cheap
:source map是否包含行信息。- 會解析生成閹割版的source map,即經過loader加工後的代碼,並且無法定位到列。
-
module
:解析loader處理之前的源代碼。- 會解析完整的source map,即沒有經過loader加工的與源代碼一致的代碼,因它需要配置在
cheap-
後,所以同樣無法定位到列。
- 會解析完整的source map,即沒有經過loader加工的與源代碼一致的代碼,因它需要配置在
根據它們的定義可以理解以下規則:
inline/hidden/nosources/cheap
需要與source-map
一起使用module
需要與cheap
一起使用
使用建議:
[eval/inline]-source-map
會將source map以Data URLs方式嵌入到打包文件中,會使文件變大很多,一般不建議使用。
Source Map 模式選擇建議
- 開發環境:eval-cheap-module-source-map
- cheap:每行代碼不會太長,只需要定位到行位置即可。
- module:項目中一般都使用了loader,需要查看加工前的代碼。
- 通過官方對比表可以看到,這個模式首次啓動打包速度慢,但是重寫打包速度快,開發中一般使用dev server實現自動編譯,所以首次啓動打包速度慢無所謂。
- 生產環境:none
- source map會暴露源代碼
- 調試是開發階段的事情
- 如果沒有信息預防生產環境報錯的情況,建議使用 nosources-source-map,以定位位置又不至於暴露源代碼內容。
webpack 自動刷新
webpack dev server 主要爲使用webpack構建的項目,提供友好的開發環境,和一個用於調試的開發服務器。
它可以監視到代碼的變化,自動打包,最後通過 自動刷新頁面 的方式同步到瀏覽器以便於即時預覽。
缺點: 自動刷新瀏覽器 會導致頁面狀態丟失。
期望:頁面不刷新的前提下,模塊也可以及時更新。
webpack HMR 熱替換
HMR(Hot Module Replacement):模塊熱替換 / 模塊熱更新
計算機行業常見名詞「熱拔插」:在一個正在運行的機器上隨時插拔設備。
- 機器的運行狀態不會受插拔設備的影響。
- 插上的設備可以立即開始工作。
例如電腦上的USB端口就是可以熱拔插的。
「模塊熱替換」 中的「熱」與「熱拔插」中的「熱」是一個道理,它們都是在運行過程中的即時變化。
模塊熱替換 就是 應用運行過程中實時替換某個模塊,應用運行狀態不受影響。
相對於自動刷新頁面丟失頁面狀態,熱替換隻將修改的模塊實時替換至應用中,不必完全刷新應用。
HMR可以實時更新包括CSS、JS 以及 靜態資源的所有模塊。
HMR是webpack中最強大、最受歡迎的功能之一。它極大程度的提高了開發者的工作效率。
開啓HMR
webpack-dev-server 已經集成了 HMR。
webpack 或 webpack-dev-server 可以通過在運行命令時添加--hot
參數去開啓這個特性。
也可以通過在配置文件中配置devServer.hot
爲true
開啓。
注意:
- 如果通過配置文件啓用,則需要配合webpack內置的熱替換插件
HotModuleReplacementPlugin
才能完全啓用HMR - 如果通過命令行參數
--hot
啓用,則會自動添加此插件,而不需要將其添加到webpack.config.js。
HMR 疑問
通過上述啓用HMR後發現,修改css文件確實實現了熱替換,而修改js文件依然會刷新頁面。
這是由於webpack中的HMR並不像其他特性一樣開箱即用。
它還需要進行一些額外的操作,才能正常工作。
webpack中的HMR需要通過代碼手動處理 模塊熱替換邏輯 ( 當模塊更新後,如何把更新過的模塊替換到運行頁面中 )。
如果沒有手動處理,就會觸發自動刷新頁面,反之就不會觸發自動刷新頁面。
Q1. 爲什麼樣式文件的熱更新開箱即用?
因爲樣式文件是通過loader處理的,上例(代碼目錄08-hmr)中樣式文件在style-loader中就已經自動處理了樣式文件的熱更新。
可通過在開發這工具中查看樣式文件的source map,其中使用了處理熱替換邏輯的代碼:
if (module.hot) {
// ...
module.hot.accept(/*...*/)
// ...
}
Q2. 爲什麼樣式文件可以自動處理,而腳本文件需要手動處理?
因爲樣式文件變更後,只需要將樣式文件的內容替換到頁面中,就可以實現樣式的即時更新。
而Javascript模塊是沒有任何規律的:模塊可能導出的是一個對象,一個字符串,或者一個函數。
開發中對這些導出的使用方式也是不同的。
所以webpack面對這些毫無規律的JS模塊,不知道如何處理當前更新後的模塊。也就沒有辦法實現一個可以通用所有情況的模塊替換方案。
Q3. 使用vue-cli或create-react-app創建的項目,沒有手動處理,JS照樣可以熱替換
這是因爲項目使用了框架,框架提供了統一的規則,框架下的開發,每種文件都是有規律的。
例如在react中要求每個文件必須導出一個函數或一個類。
有了規律,就可能有一個通用的替換方案。
例如如果每個文件都導出一個函數,就把這個函數拿過來再次執行一次,實現熱替換。
另一方面,通過腳手架創建的項目內部已經集成並使用了通用的HMR方案,所以不需要手動處理。
HMR APIs
HotModuleReplacementPlugin 爲JS提供了一套用於處理HMR的API。
開發者需要在自己的代碼中使用這套API,以處理當某個模塊更新後,應該如何替換到當前正在運行的頁面中。
module.hot
是HMR API的核心對象。
module.hot.accept(arg1, arg2)
用於註冊,當某個模塊更新後的處理函數。
arg1
接收一個依賴模塊的路徑。
arg2
就是依賴模塊更新後的處理函數。
if (module.hot) {
module.hot.accept('./editor', () => {
console.log('editor 模塊更新了,需要這裏手動處理熱替換邏輯')
})
}
HMR 注意事項
- 手動處理HMR時,如果處理邏輯的代碼中報錯導致失敗,就會回退到自動刷新頁面的方式實現替換。由於自動刷新,處理邏輯代碼中的報錯信息就不會展示。
- 解決辦法:配置
devServer.hotOnly:true
啓用不刷新頁面的熱模塊替換,代替devServer.hot:true
。 - 命令行使用:
--hot-only
- 解決辦法:配置
- 項目中使用了HMR APIs(
module.hot.accept
),但是並沒有配置完全啓用HMR。執行時就會報錯:Cannot read property 'accept' of undefined
- 這是由於module.hot是內置插件HotModuleReplacementPlugin提供的,未啓用HMR(也就是未使用這個插件),
module.hot
就是undefined
- 解決辦法:在使用API前先確認下hot是否開啓,使用
if (module.hot)
- 這是由於module.hot是內置插件HotModuleReplacementPlugin提供的,未啓用HMR(也就是未使用這個插件),
- 代碼中寫了很多與業務無關的代碼(處理熱替換的邏輯代碼)
- 解決辦法:由於生產環境不需要啓用HMR,並且在調用HMR APIs前進行了
if(module.hot)
確認,所以生產環境打包後,處理熱替換的代碼就會編譯爲if (false) {}
。代碼全部清空。
- 解決辦法:由於生產環境不需要啓用HMR,並且在調用HMR APIs前進行了