webpack二刷之四、開發體驗

webpack 開發體驗

webpack 增強開發體驗

原始開發方式:

  1. 編寫源代碼
  2. webpack打包
  3. 運行應用
  4. 刷新瀏覽器

設想理想的開發環境:

  1. 以HTTP Server運行
    1. 不是以文件形式預覽
    2. 接近生產環境的狀態
    3. 類似ajax這類api,不支持文件訪問形式
  2. 自動編譯 + 自動刷新
  3. 提供Source Map支持
    1. 調試錯誤時快速定位

實現自動編譯

watch監聽模式:監聽文件變化,自動重新運行打包任務。(類似其他構建工具的watch)

命令行使用方法:--watch參數啓動監聽模式

例如:yarn webpack --watch


實現編譯後自動刷新瀏覽器

使用browser-sync模塊的--files參數監聽文件變化,觸發瀏覽器刷新。

例如:browser-sync dist --files "**/*"


上面實現兩個開發體驗的方式的缺點

  1. 需要打開兩個終端去執行命令,操作較麻煩。
  2. webpack頻繁將編譯後的文件寫入磁盤,browser-sync從磁盤中讀取文件,效率上降低了。

Webpack Dev Server 實現自動編譯 + 自動刷新

webpack-dev-server 是 webpack官方的開發工具。

  1. 它提供用於開發的 HTTP Server 服務器
  2. 集成了「自動編譯」和「自動刷新瀏覽器」等功能

安裝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對象配置代理服務。

對象中的每個屬性,都是一個代理規則的配置。

  1. 屬性的名稱(key)就是需要代理的請求路徑的前綴,例如'/api'
  2. 屬性的值(value)是爲這個前綴匹配的代理規則配置。
    1. target:代理目標,即訪問key相當於訪問target/key,他會將key添加到後面,可通過pathRewrite實現代理路徑的重寫。
    2. pathRewrite:重寫代理路徑。它接收一個對象,key是正則匹配的路徑字符串,value是要替換的內容。
      1. 它修改的是path路徑(參考location.pathname),例如https://api.github.com/api/users修改的是/api/users
    3. 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

配置changeOrigintrue,就會以實際發生代理請求的「host」(api.github.com)作爲發起請求的「host」。

這樣就不用關心,最終會把它代理成了什麼樣。

Source Map

通過構建編譯,可以將開發環境的源代碼轉化爲能在生產環境運行的代碼。

這使得 運行代碼 完全不同於 源代碼。

由於調試和報錯都是基於運行代碼。如果需要調試應用,或運行應用時報出了錯誤,就無法定位。

Source Map(源代碼地圖) 就是解決這類問題最好的辦法。

它用來映射 轉換後的代碼(compiled) 與 源代碼(source) 之間的關係。

轉換後的代碼,通過轉換過程中生成的 Source Map 解析,就可以逆向得到源代碼。

Source Map 文件

目前很多第三方的庫在打包後都會生成一個.map後綴的Source Map文件。

它是一個 json 格式的文件,主要包含以下屬性:

  1. version:表示當前文件所使用source map標準的版本
  2. sources:記錄轉換之前源文件的名稱
    1. 可能是多個文件合併轉換成一個文件,所以它是數組形式
  3. names:記錄源代碼中使用的成員名稱
    1. 壓縮代碼時,會將開發階段編寫的有意義的變量名替換爲簡短的字符,從而去壓縮整體代碼的體積
    2. names記錄的就是原始對應的名稱
  4. mappings:記錄轉換後的代碼當中的字符,與轉換前所對應的映射關係。
    1. 它是整個source map的核心屬性。
    2. 他是一個 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。

效果:

  1. 查看的源碼是經過loader轉換後的代碼(如果配置了對應的loader),導致定位到的行與實際源代碼不一致。
  2. 無法定位到列。
    1. 表現爲通過開發人員工具跳轉到源代碼時,光標只會定位到代碼的行,不會定位到代碼的列。

由於少了一些效果,所以生成速度比 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-後,所以同樣無法定位到列。

根據它們的定義可以理解以下規則:

  • 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.hottrue開啓。

注意:

  • 如果通過配置文件啓用,則需要配合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 注意事項

  1. 手動處理HMR時,如果處理邏輯的代碼中報錯導致失敗,就會回退到自動刷新頁面的方式實現替換。由於自動刷新,處理邏輯代碼中的報錯信息就不會展示。
    1. 解決辦法:配置devServer.hotOnly:true啓用不刷新頁面的熱模塊替換,代替devServer.hot:true
    2. 命令行使用:--hot-only
  2. 項目中使用了HMR APIs(module.hot.accept),但是並沒有配置完全啓用HMR。執行時就會報錯:Cannot read property 'accept' of undefined
    1. 這是由於module.hot是內置插件HotModuleReplacementPlugin提供的,未啓用HMR(也就是未使用這個插件),module.hot就是undefined
    2. 解決辦法:在使用API前先確認下hot是否開啓,使用if (module.hot)
  3. 代碼中寫了很多與業務無關的代碼(處理熱替換的邏輯代碼)
    1. 解決辦法:由於生產環境不需要啓用HMR,並且在調用HMR APIs前進行了if(module.hot)確認,所以生產環境打包後,處理熱替換的代碼就會編譯爲if (false) {}。代碼全部清空。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章