【W(wǎng)ebpack】devServer 實驗報告
Choosing a Development Tool
Webpack 的使用目前已經(jīng)是前端開發(fā)工程師必備技能之一。若是想在本地環(huán)境啟動一個開發(fā)服務(wù)快速開發(fā)我們的應(yīng)用(而不是每次 coding 完,手動執(zhí)行 run build,全量打包),大家只需在 Webpack 的配置中,增加 devServer 的配置即可。它的作用主要是用來伺服資源文件。webpack-dev-server(以下簡稱 wds) 已經(jīng)為我們封裝好了全面、豐富且可配置化的功能,配置工程師們只需通過 webpack.config 和 命令行參數(shù) 即可滿足開發(fā)所需。 然而配置工程師們,發(fā)現(xiàn) wds 的 hot、live reload 實際上相當于啟用了一個 express 的 Http 服務(wù)器 + webpack-dev-middleware(以下簡稱 wdm) 等中間件。這個 Http 服務(wù)器 和 用戶訪問服務(wù)的 client 可以通過 websocket 通訊協(xié)議建立長連接,在 webpack 'watch' 到原始文件作出改動后,wds 會使用 webpack 的實時編譯,再用 wdm 將 webpack 編譯后文件會輸出到內(nèi)存中。每當應(yīng)用程序請求一個文件時,wdm 匹配到了就把內(nèi)存中緩存的對應(yīng)結(jié)果以文件的格式返回給 client ,反之則進入到下一個中間件。 如果想要使用更多 wds 提供的配置功能,比如 proxy、static、open 等, 在 server 端增加中間件即可,這樣配置工程師搖身一變,配置開發(fā)工程師! 項目中的 devServer 我們更多是使用 webpack + express + webpack-dev-middleware + webpack-hot-middleware 的組合來完成 HMR。在系列文章中,有更加具體詳細的學習分享來介紹這些,這里收縮一下,我們只關(guān)注 webpack-dev-server。
wds 在宏觀世界的部分特性
Use webpack with a development server that provides live reloading. This should be used for development only. It uses webpack-dev-middleware under the hood, which provides fast in-memory access to the webpack assets.
webpack 配合一個開發(fā)服務(wù)器,可以提供熱重載功能。但只用于開發(fā)模式下。 wds 的底層,集成了 wdm,可以提供快速內(nèi)存訪問打包資源的功能。
以上是 wds 對自己的一個簡短自我介紹,我們來搞清楚它這么概括的點:
1. webpack 配合一個開發(fā)服務(wù)器,可以提供熱重載功能。
webpack 可以通過 watch mode 的方式啟動,指示 webpack 'watch' 依賴圖中所有文件的更改,并且自動打包。但是每次打包后的結(jié)果將會存儲到本地硬盤中,而 IO 操作是非常耗資源時間的,無法滿足本地開發(fā)調(diào)試需求。 wds 則可以提供一個開發(fā)服務(wù)器,并且提供 live reloading(實時重載)功能,在打包完成后通知客戶端,刷新頁面重新加載資源。
2. 快速內(nèi)存訪問打包資源
wdm 可以將 webpack 編譯后的資源輸出到內(nèi)存中,當應(yīng)用程序請求資源時,可以直接從內(nèi)存中進行響應(yīng)。 開發(fā)中,我們還會注意到,在編譯期間,客戶端的請求會被 delay 到最新的編譯結(jié)果完成之后才會去響應(yīng)。
- 「wdm」: wait until bundle finished: /myapp/
3. HMR 模塊熱替換
日常開發(fā)修改完代碼后,你有沒有傻乎乎地,手動刷新調(diào)試頁面,來驗證現(xiàn)在的 bug 還是不是之前的那個 bug? 其實 HMR 會在應(yīng)用程序運行過程中,替換、添加或刪除模塊,而無需重新加載整個頁面。 可以通過以下幾種方式,來顯著提速開發(fā)效率:
- 保留在完全重新加載頁面期間丟失的應(yīng)用程序狀態(tài)
- 只更新變更內(nèi)容,以節(jié)省寶貴的開發(fā)時間
- 在源代碼中對 CSS/JS 進行修改,會立刻在瀏覽器中進行更新,這幾乎相當于在瀏覽器 devtools 直接更改樣式
4. 服務(wù)啟動后自動啟動瀏覽器 - open
有時候服務(wù)啟動后,會打開 localhost:8080,或者打開瀏覽器的多個頁簽
5. 提供 history api 降級方案 - historyApiFallback
前端項目中,借助 history api,可以做到改變視圖而不向后端發(fā)出請求。如果你手動刷新一個路由中匹配不到的頁面,同時你的項目中沒有配置 404 頁面兜底邏輯,那就真的 404 Not Found 了。 wds 配置中有一項配置 historyApiFallback,可以配置一個頁面代替所有的 404 響應(yīng)。
- 「wds」: 404s will fallback to /index.html
6. 提供代理功能 - proxy
wds 的 proxy配置使用方法,可以詳見 Webpack-dev-server 的 proxy 用法。
- 如果你有一個單獨的后臺 API 服務(wù),你可以通過代理,把前端項目域名下發(fā)起的 API 請求,代理到后臺的域名。
- 解決開發(fā)環(huán)境中的跨域問題
- 通過代理你還可以定制返回的 html 頁面,比如同一個項目中你想要提供 PC、H5 兩端的產(chǎn)物,通過 UA 判斷,返回不同的打包產(chǎn)物 index.html or index.mobile.html
7. 代碼打包編譯出現(xiàn)警告和錯誤時,會在在頁面上顯示錯誤信息 - overlay
控制代碼打包編譯時出現(xiàn)警告和錯誤時,是否在頁面上顯示錯誤信息
8. output.path、output.publicPath、devServer.publicPath、devServer.contentBase
- output.path 打包輸出產(chǎn)物的絕對路徑
- output.publicPath 它會為所有打包后的資源指定一個基礎(chǔ)路徑,多用于靜態(tài)資源服務(wù)器或者 CDN 托管靜態(tài)資源
- devServer.publicPath 掛載到服務(wù)器中間件的可訪問虛擬地址
- devServer.contentBase 加載這個配置下(文件夾下)的靜態(tài)資源到服務(wù)器
- output.path 打包產(chǎn)物的絕對路徑,沒有什么疑問。對于 output.publicPath、devServer.publicPath,還是有點迷惑不解吧? 假如有一個域名 example.com,但是你們應(yīng)用部署在 example.com/myapp/ 。沒有指定 output.publicPath,默認為 '/',這時 index.html 引用其他模塊的 url 會是 /bundle.xxxhashxxx.js,這時這個資源的 url 就變成了 example.com/bundle.xxxhashxxx.js,毫無疑問,這個資源會 404 Not Found。如果指定 output.publicPath: '/myapp/',那么 index.html 中資源的 url 就變成了 '/myapp/bundle.xxxhashxxx.js'。 同理 wds 中,指定 devServer.publicPath: '/myapp/',devServer 就會在 http://localhost:8080/myapp/ 下伺服資源訪問。模擬生產(chǎn)環(huán)境下的運維配置。
- 「wds」: webpack output is served from /myapp/
contentBase 呢?它只作用于 wds,只有你想要伺服靜態(tài)資源文件的時候使用。換句話說,wds 會加載這個文件夾下的靜態(tài)資源到服務(wù)器,而不需要 bundle 這個文件夾。 假如,你的 app 中需要加載一些 mp4 文件,這些文件基本不會被改動,所以你不必把這些資源打包到 /dist 文件下,可以把這些文件維護在 /src、/dist 的同級目錄下的 /static。然后設(shè)置 contentBase: path.join(__dirname, 'static'),然后就可以在代碼中這樣引用靜態(tài)資源了。
- 「wds」: Content not from webpack is served from /Volumes/bomb/git/webpack-learning/webpack-demo/static
9. more
構(gòu)建滿足這些特性的自洽模型
為了驗證我們構(gòu)建的自洽模型,能夠自洽,我們需要一個參照物來進行修正。 我們使用 devServer 官方配置,來伺服資源文件。 為了不影響體驗,自洽模型 和 參考物 的代碼都維護在第四節(jié)的參考,有興趣的可以自己 debugger 一下。
1. 模擬 http 服務(wù)器
首先我們使用 express 啟動一個本地 server,讓瀏覽器可以訪問本地的靜態(tài)資源。
- // wds.server.js
- const app = express();
- const listeningApp = http.createServer(app);
- listeningApp.listen('8888', '127.0.0.1', (err) => {
- createSocketServer();
- });
這里創(chuàng)建 http 服務(wù)器,沒有使用 app.listen('8888', callback),而是使用 http.createServer(app) 的原因有兩點:
在創(chuàng)建 websocket server(代碼片段中的 createSocketServer)時,需要復用 http 服務(wù)器實例 listeningApp,在下一小節(jié)會介紹 wss
express 只返回 http 服務(wù)器實例,而 devServer 是支持配置 https 的,所以可以直接用 https.createServer(app),更加方便
2. 模擬監(jiān)聽代碼文件更新
wds 調(diào)用 webpack api 對文件系統(tǒng)進行 'watch',當文件發(fā)生改變后,webpack 會重新對文件進行編譯打包,然后保存到內(nèi)存中。 這一系列操作,主要有兩點:1、watch 文件更改;2、內(nèi)存響應(yīng)。所幸,wdm 完成了這部分功能,我們在自洽模型中直接引用 wdm 。
- // wds.server.js
- const webpack = require('webpack');
- const wdm = require('webpack-dev-middleware');
- const config = require('./webpack.config.js');
- const compiler = webpack(config); // 將 webpack.config.js 配置文件作為基礎(chǔ)配置
- const app = express();
- app.use(wdm(compiler)) // 告知 express 使用 webpack-dev-middleware
這里不難看出,wdm(compiler) 的執(zhí)行結(jié)果返回的是一個中間件,它將 webpack 編譯后的文件存儲到內(nèi)存中,然后在用戶訪問 express 服務(wù)時,將內(nèi)存中對應(yīng)的資源輸出返回。 那么 wdm 內(nèi)部是如何實現(xiàn)的呢? wdm 的源碼并不多,其核心只有 /index.js,/lib/middleware。
- // webpack-dev-middleware/index.js
- // 在 compiler 的 invalid、run、done、watchRun 這 4 個編譯生命周期上,注冊對應(yīng)的處理方法。
- // 通過 tapable 來調(diào)用插件功能,主要是 report 編譯的狀態(tài)信息以及執(zhí)行 context.callbacks 回調(diào)函數(shù)
- const context = createContext(compiler, options);
- ...
- // 以監(jiān)控的方式啟動 webpack,調(diào)用 compiler 的 watch 方法,之后 webpack 便會監(jiān)聽文件變更,一旦檢測到文件變更,就會重新執(zhí)行編譯。
- context.watching = compiler.watch(options.watchOptions, (err) => { ... });
- ...
- // 使用 memory-fs,將 webpack 的編譯內(nèi)容,輸出至內(nèi)存中
- setFs(context, compiler);
- // webpack-dev-middleware/lib/middleware
- // 核心邏輯是:針對 request 請求,根據(jù)各種條件判斷,最終返回對應(yīng)的文件
- module.exports = function wrapper(context) {
- // 返回 express 中間件函數(shù)的包裝函數(shù)
- return function middleware(req, res, next) {
- // 如果不是 SSR,直接 next,流轉(zhuǎn)到下一個中間件
- // 如果是 SSR,調(diào)用 util/ready,根據(jù) state 判斷執(zhí)行回調(diào) fn,還是將 fn 存儲到 callbacks 隊列中
- // ready 也是“在編譯期間,停止提供舊版的 bundle 并且將請求延遲到最新的編譯結(jié)果完成之后”的實現(xiàn)
- function goNext() { ... }
- // 根據(jù)請求的 req.url 地址,在 compiler 的內(nèi)存文件系統(tǒng)中查找對應(yīng)的文件,若查找不到,則直接調(diào)用 goNext() 方法處理請求
- let filename = getFilenameFromUrl( ... )
- if (filename === false) {
- return goNext();
- }
- // 根據(jù)上文找到的 filename 路徑獲取到對應(yīng)的文件內(nèi)容,并構(gòu)造 response 對象返回
- // 最后也是調(diào)用 ready
- return new Promise((resolve) => {
- handleRequest(context, filename, processRequest, req);
- function processRequest() {
- ...
- }
- })
- }
- }
3. 模擬 server 端:server 端 通知 client 端 文件發(fā)生改變
使用 HMR 的過程中,通過 network 我們知道 client 端 是通過 websocket 和 server 端 進行通信的。client 端 和 server 端 之間建立一個 websocket 長連接,將 webpack 編譯打包的各個階段的狀態(tài)信息告知 client 端。最關(guān)鍵的還是 wds 注冊 compiler hooks(compile、done、watchRun 等),當進入 webpack compile 生命周期時調(diào)用 hooks 回調(diào)注冊方法。 compilation done 時,server 端 傳遞的最主要的信息是 'stats.hash' 和 'ok',然后 client 端 根據(jù) hash 進行模塊熱更新。
- // wds.server.js
- let connect = null // 長連接實例
- // 調(diào)用 webpack api 監(jiān)聽 compile 的 done 事件
- // 注冊 compiler hooks -- done
- const { done } = compiler.hooks
- done.tap('myappPligins', (stats) => {
- if (connect) {
- let _stats = stats.toJson({...})
- // 將編譯打包后的新模塊 hash 值發(fā)送到 client 端
- connect.write(JSON.stringify({
- "type": "hash",
- "data": _stats.hash
- }))
- // 通知 client 端編譯完成,可以進行 reloadApp 操作
- connect.write(JSON.stringify({
- "type": "ok"
- }))
- }
- });
- // 創(chuàng)建 websocket server(wss)
- // 目前 webpack-dev-server@4.X 使用 sockjs 會出錯,webpack-dev-server@3.X 使用 ws 會報錯
- function createSocketServer () {
- let socket = sockjs.createServer({
- sockjs_url: './sockjs-client'
- });
- // 復用 http 服務(wù)器實例 listeningApp
- socket.installHandlers(listeningApp, {
- prefix: '/sockjs-node',
- });
- socket.on('connection', (connection) => {
- connect = connection
- ...
- });
- }
4. client 端接收 wss 消息并觸發(fā)響應(yīng)
我們在業(yè)務(wù)代碼中并沒有添加接收 wss 消息的代碼,那 client 端 的邏輯怎么實現(xiàn)的呢? 其實 wds 修改了 webpack.config.js 的基礎(chǔ)配置,它會往 chunk 中偷偷塞入兩個文件 webpack-dev-server/lib/client/index.js 和 webpack/hot/dev-server。 我們在自洽模型中也這么操作,這樣這兩段代碼就植入到 client 端了。
- // wds.server.js
- config.entry.app = [require.resolve('webpack/hot/dev-server'), './wds.client.js', config.entry.app]
- // HMR 作為一個 Webpack 內(nèi)置的功能,可以通過 HotModuleReplacementPlugin 開啟
- config.plugins.push(
- new webpack.HotModuleReplacementPlugin()
- )
client 端 通過 websocket 接收 server 端 最新編輯后的模塊 hash 值,這個值會被存起來(currentHash),在接收到 ok 后才會 reloadApp。 如果配置了 hot,開啟 HMR,會把程序控制權(quán)交給 webpack 的客戶端代碼進行 HMR。如果沒有開啟,就直接調(diào)用 location.reload() 刷新頁面。
- // wds.client.js
- const SockJS = require('./sockjs-client')
- const socketUrl = 'http://127.0.0.1:8888/sockjs-node'
- let currentHash = '' // 最新代碼模塊的 hash 值
- function reloadApp () {
- if (options.hot) {
- let hotEmitter = require('webpack/hot/emitter');
- // webpackHotUpdate 是 webpack 在 webpack/hot/dev-server.js 定義的一個事件,事件回調(diào)是獲取此次編譯的最新代碼
- hotEmitter.emit('webpackHotUpdate', currentHash);
- } else if (options.liveReload) { // 沒有配置 hmr,就直接 live reload 刷新頁面
- location.reload();
- }
- }
- // 處理 wss 通知
- const onSocketMessage = {
- ...
- hash: function hash(_hash) {
- currentHash = _hash; // wss 端 通知 client 端 最新編輯后的模塊 hash 值,這個值會被存起來(currentHash),在接收到 ok 后才會 reloadApp
- },
- ok: function ok() {
- reloadApp();
- }
- };
- const socket = (url, handlers) => {
- client = new SockJS(url)
- ...
- client.onmessage = function (data) { // 接收 wss 通知
- var msg = JSON.parse(data.data);
- if (handlers[msg.type]) {
- handlers[msg.type](msg.data);
- }
- }
- ...
- }
- socket(socketUrl, onSocketMessage)
5. 模擬 HMR or live reload當 client 端 收到 ok 的通知后,開啟 hot 的 wds,會執(zhí)行 reload 方法,然后調(diào)用 webpackHotUpdate
- // wds.client.js
- let hotEmitter = require('webpack/hot/emitter');
- hotEmitter.emit('webpackHotUpdate', currentHash);
然后程序被 client 端 的 webpack 接管(第四步中我們注入到 plugins 中的 webpack.HotModuleReplacementPlugin 就派上用場了),webpack 監(jiān)聽到 webpackHotUpdate 事件,并獲取到最新的 hash 值,然后開始檢查更新。
- // webpack/hot/dev-server.js
- var hotEmitter = require("./emitter");
- hotEmitter.on("webpackHotUpdate", function (currentHash) {
- lastHash = currentHash;
- ...
- check();
- });
- // 檢查更新
- var check = function check() {
- module.hot.check(true)
- .then(updatedModules => {
- ...
- })
- }
源碼中,追蹤到 module.hot.check,就不知道路該怎么走了,hot.check 是哪里來的? 系列文章中有單獨介紹 HMR 的一章,這里我們就偷個懶,粗線條的勾勒一下大致過程。 hot.check 來自于 /webpack/lib/hmr/HotModuleReplacement.runtime.js。
利用上一次保存的hash值,調(diào)用 hotDownloadManifest 發(fā)送 xxx.hash.hot-update.json 的 ajax 請求
請求結(jié)果獲取熱更新模塊相關(guān)信息,并進入熱更新準備階段。
- c: chunkIds m: removedChunks r: removedModules
通過 JSONP 的方式,調(diào)用 loadUpdateChunk 在 document.head 添加 script 標簽,發(fā)送 chunkId.hash.hot-update.js 請求。
下面就是拿到的 hot-update.js 的內(nèi)容。
JSONP 返回的 js 文件立即執(zhí)行,會調(diào)用 window.webpackHotUpdatewebpack_demo 方法。此方法會把更新的模塊 moreModules (圖中入?yún)⒌牡诙€參數(shù)對象)賦值給全局全量 currentUpdate。
最后會調(diào)用 HMR runtime 的 hotApply 進行熱更新模塊替換
6. 模擬 history api 降級方案
- // wds.server.js
- // 添加中間件 connect-history-api-fallback,解決 history api 降級
- app.use(historyApiFallback({
- htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], // 只對這些類型的請求進行 rewrite
- rewrites: [
- { from: /./, to: '/myapp/index.html' }
- ]
- }))
7. 模擬 proxy 代理
proxy 配置,我們經(jīng)常使用,那 node 是如何代理請求的呢?
在 wds 中,借助創(chuàng)建的 http 服務(wù)器,其 proxy 功能的實現(xiàn)就是解析配置項,并掛載 http-proxy-middleware 中間件到 http 服務(wù)上。結(jié)合 proxy 的用法,wds/lib/Server.js 中的代碼顯得一目了然。 http-proxy-middleware 則借助于 node-http-proxy,用于將 node 服務(wù)端接收到的請求,轉(zhuǎn)發(fā)到目標服務(wù)器,實現(xiàn)代理服務(wù)器的功能。 可以預見,整個流程的大致實現(xiàn)思路就是,通過配置項注冊全局的請求轉(zhuǎn)發(fā)規(guī)則,在中間件中攔截客戶端的 request 請求匹配轉(zhuǎn)發(fā)規(guī)則,然后調(diào)用 node-http-proxy 的 .web、.ws 方法進行轉(zhuǎn)發(fā)請求。 http-proxy-middleware 將轉(zhuǎn)發(fā)規(guī)則分為兩大類進行配置,context 和 options。
- // Proxy middleware configuration.
- var proxy = require('http-proxy-middleware');
- var apiProxy = proxy('/api', { target: 'http://www.example.org' });
- // \____/ \_____________________________/
- // | |
- // context options
- // 'apiProxy' is now ready to be used as middleware in a server.
context 用于匹配需要進行轉(zhuǎn)發(fā)的客戶端請求,默認值是 '/',客戶端發(fā)起的所有請求都會被轉(zhuǎn)發(fā);也可以字符串 url、字符串 url 數(shù)組、通配符或者個性化方法,來決定哪些請求會被代理轉(zhuǎn)發(fā)。http-proxy-middleware 使用 options 中
- target 用于設(shè)置要轉(zhuǎn)發(fā)到的目標服務(wù)器的 host;
- pathRewrite: object/function,用于改寫要轉(zhuǎn)發(fā)到的目標服務(wù)器的 url path;
- router: object/function,根據(jù)配置,將匹配的客戶端請求,改寫這次請求的 host。
- // rewrite path
- pathRewrite: {'^/old/api' : '/new/api'}
- // remove path
- pathRewrite: {'^/remove/api' : ''}
- // add base path
- pathRewrite: {'^/' : '/basepath/'}
- // custom rewriting
- pathRewrite: function (path, req) {}
- router: {
- 'integration.localhost:3000' : 'http://localhost:8001', // host only
- 'staging.localhost:3000' : 'http://localhost:8002', // host only
- 'localhost:3000/api' : 'http://localhost:8003', // host + path
- '/rest' : 'http://localhost:8004' // path only
- }
- // http-proxy-middleware/lib/index.js
- function HttpProxyMiddleware(context, opts) {
- ...
- var config = configFactory.createConfig(context, opts) // 解析獲取 context、options
- ...
- var proxy = httpProxy.createProxyServer({}) // 創(chuàng)建代理服務(wù)器,由這個服務(wù)器進行轉(zhuǎn)發(fā)請求
- ...
- var pathRewriter = PathRewriter.create(proxyOptions.pathRewrite) // 將客戶端請求路徑轉(zhuǎn)化為目標服務(wù)器的路徑(pathname 部分),既可以是 key-value,也可以函數(shù)。
- ...
- function shouldProxy(context, req) { // 判斷請求是否需要轉(zhuǎn)發(fā)
- var path = req.originalUrl || req.url
- return contextMatcher.match(context, path, req) // 通過多種匹配方法校驗客戶端 req 是否需要轉(zhuǎn)發(fā)
- }
- function prepareProxyRequest(req) {
- req.url = req.originalUrl || req.url
- var originalPath = req.url
- var newProxyOptions = _.assign({}, proxyOptions)
- __applyRouter(req, newProxyOptions) // 遍歷 options.router,校驗是否匹配客戶端 req,匹配的話就改寫這次請求的 host
- __applyPathRewrite(req, pathRewriter) // 如果有 pathRewriter,就匹配當前請求,匹配的話就將設(shè)置的目標服務(wù)器路徑寫入 req.url
- return newProxyOptions
- }
- ...
- function middleware(req, res, next) { // 真正的代理中間件
- if (shouldProxy(config.context, req)) {
- var activeProxyOptions = prepareProxyRequest(req)
- proxy.web(req, res, activeProxyOptions) // node-http-proxy 進行代理轉(zhuǎn)發(fā)
- } else {
- next()
- }
- }
- ...
- return middleware
- }
這樣看來,http-proxy-middleware 主要做的是解析轉(zhuǎn)發(fā)規(guī)則、最終把代理轉(zhuǎn)發(fā)的事情交給了 node-http-proxy,同時配置了相關(guān)的 Logger、綁定事件。
解析獲取 context、options,并配置 Logger 實例
通過 node-http-proxy 創(chuàng)建代理服務(wù)器,并 attach proxy-events
根據(jù) options.pathRewrite 生成路徑轉(zhuǎn)化器
匹配客戶端請求,通過代理服務(wù)器轉(zhuǎn)發(fā) http, https, websocket 請求
自洽實是自娛自樂
本文從開發(fā)過程中遇到的痛點出發(fā),梳理了現(xiàn)代打包工具對我們?nèi)粘i_發(fā)的幫助和提效,并自娛自樂的結(jié)合表現(xiàn)和源碼,照虎畫貓完成了所謂的自洽模型。其實自洽模型畫的遠不如貓,最多就是一個四支腿生物的簡筆畫了。權(quán)當梳理了一遍 wds 的工作流程,更加細節(jié)的東西,還需要大家一起動手才能挖掘出來,希望能對你的理解過程起到一定的幫助作用。
參考
參考一:社區(qū)文章
- webpack 官網(wǎng)
- publicPath、contentBase
- 十分鐘搞懂 webpack
- 輕松理解 webpack 熱更新原理
- 再不怕被問到 HMR
- wdm 源碼解讀
- Webpack-dev-server 的 proxy 用法
- 知乎 HMR 原理解析
- 關(guān)于 tapable 你需要知道這些
- 相關(guān)包的版本:
"webpack": "5.24.0", "webpack-cli": "4.5.0", "webpack-dev-server": "^3.11.2"
參考二:自洽模型的參照物
- // webpack.config.js 使用通用的 Vue 項目配置
- module.exports = {
- mode: 'development',
- entry: {
- app: './src/app.js'
- },
- output: {
- filename: '[name].bundle.js',
- path: path.resolve(__dirname, 'dist'),
- publicPath: '/myapp/'
- },
- devtool: 'inline-source-map',
- plugins: [
- ...some plugins
- ],
- module: {
- rules: [ ...some loaders ]
- }
- };
然后把 devServer 的配置單獨維護在 webpack.dev.js
- // webpack.dev.js devServer 配置
- module.exports = merge([
- base, // webpack.config.js
- {
- mode: 'development',
- devServer: {
- host: '127.0.0.1', // 服務(wù)器 host,默認為 localhost
- port: 7777, // 服務(wù)器端口號,默認為 8080
- open: true, // string | boolean,啟動后是否打開瀏覽器,當為字符串時,打開指定瀏覽器
- openPage: 'myapp/', // string | Array<string>, ['', 'index.html'], 'index.html', 打開瀏覽器后默認打開的頁面,Array 打開多個頁面
- compress: true,
- hot: true, // 是否啟動熱更新(HMR),熱更新使用的是 webpack 中 HotModuleReplacementPlugin
- http2: false, // 是否設(shè)置 HTTP/2 服務(wù)器,為 true,則默認使用 https 作為服務(wù)
- // https: {
- // key: '',//fs.readFileSync('/path/to/server.key'),
- // cert: '',//fs.readFileSync('/path/to/server.crt'),
- // ca: '',//fs.readFileSync('/path/to/ca.pem')
- // },
- proxy: {
- '/api': {
- target: 'http://localhost:7777',
- pathRewrite: { '^/api': '' },
- secure: false // HTTPS 設(shè)置為無效證書
- }
- },
- // 靜態(tài)文件屬性
- publicPath: '/myapp/', // 掛載到服務(wù)器中間件的可訪問虛擬地址
- contentBase: path.join(__dirname, 'static'), // devServer 伺服這個文件夾下的靜態(tài)資源。換句話說會加載本地 /static 目錄下的靜態(tài)文件到服務(wù)器
- stats: 'minimal',
- // 設(shè)置編譯出錯或警告后,頁面是否會直接顯示信息, boolean | {}
- // 默認為 false,當失敗后會顯示空白頁
- // 設(shè)置為 true 后,編譯失敗會顯示錯誤/警告的覆蓋層,也可以設(shè)置為 object,顯示多種類型信息
- overlay: {
- warnings: true,
- errors: true
- },
- injectClient: true, // 是否要注入 WebSocket 客戶端,將此屬性設(shè)置為 false,那么 hot、overlay 等功能都會失效
- injectHot: true, // 是否注入 HMR, 這個屬性是 injectClient 的子集。只影響熱更新
- liveReload: false, // 是否開啟自動刷新瀏覽器功能,優(yōu)先級低于 hot
- // 是否將所有 404 頁面都跳轉(zhuǎn)到 index.html,當此屬性設(shè)置為 true 或為 object 時并且使用 history API 時所有 404 頁面會跳轉(zhuǎn)到 index.html 或指定的頁面
- historyApiFallback: {
- rewrites: [
- { from: /./, to: '/myapp/index.html' },
- ]
- },
- // 設(shè)置 WebSocket,設(shè)置使用的 WebSocket 庫,內(nèi)置為 sockjs 或 ws
- transportMode: {
- // 目前 webpack-dev-server@4.X 使用 sockjs 會出錯,webpack-dev-server@3.X 使用 ws 會報錯
- server: 'sockjs'
- }
- }
- }
- ])
package.json
- // package.json
- "scripts": {
- "start": "webpack serve --config webpack.dev.js",
- "start:dev": "node wds.server.js",
- }
參考三:自洽模型
- // wds.server.js
- const express = require('express');
- const webpack = require('webpack');
- const http = require('http');
- const webpackDevMiddleware = require('webpack-dev-middleware');
- const historyApiFallback = require('connect-history-api-fallback');
- const sockjs = require('sockjs');
- const app = express();
- const config = require('./webpack.config.js');
- config.entry.app = [require.resolve('webpack/hot/dev-server'), './wds.client.js', config.entry.app]
- config.plugins.push(
- new webpack.HotModuleReplacementPlugin()
- )
- // 告知 express 使用 webpack-dev-middleware,
- // 以及將 webpack.config.js 配置文件作為基礎(chǔ)配置。
- const compiler = webpack(config)
- // historyApiFallback
- app.use(historyApiFallback({
- htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
- rewrites: [
- { from: /./, to: '/myapp/index.html' }
- ]
- }))
- app.use(
- webpackDevMiddleware(compiler, {
- publicPath: config.output.publicPath,
- })
- )
- let connect = null
- const { done } = compiler.hooks
- done.tap('myappPligins', (stats) => {
- if (connect) {
- let _stats = stats.toJson({
- all: false,
- hash: true,
- assets: true,
- warnings: true,
- errors: true,
- errorDetails: false,
- })
- connect.write(JSON.stringify({
- "type": "hash",
- "data": _stats.hash
- }))
- connect.write(JSON.stringify({
- "type": "ok"
- }))
- }
- });
- const listeningApp = http.createServer(app);
- function createSocketServer () {
- let socket = sockjs.createServer({
- sockjs_url: './sockjs-client'
- });
- socket.installHandlers(listeningApp, {
- prefix: '/sockjs-node',
- });
- socket.on('connection', (connection) => {
- if (!connection) {
- return;
- }
- connect = connection
- // 通知 client enable 了哪些功能
- connection.write(JSON.stringify({
- "type": "hot"
- }))
- });
- }
- listeningApp.listen('8888', '127.0.0.1', (err) => {
- console.log('Example app listening on port 8888!\n');
- createSocketServer();
- });
- listeningApp.on('error', (err) => {
- console.error(err);
- });
- // wds.client.js
- console.log('this is from client.')
- const SockJS = require('./sockjs-client')
- const socketUrl = 'http://127.0.0.1:8888/sockjs-node'
- const options = {
- hot: true,
- hotReload: true,
- liveReload: false,
- initial: true,
- useWarningOverlay: false,
- useErrorOverlay: false,
- useProgress: false
- }
- let currentHash = ''
- function reloadApp () {
- if (options.hot) {
- console.log('[WDS] App hot update...');
- let hotEmitter = require('webpack/hot/emitter');
- hotEmitter.emit('webpackHotUpdate', currentHash);
- // broadcast update to window
- window.postMessage("webpackHotUpdate".concat(currentHash), '*');
- } else if (options.liveReload) {
- location.reload();
- }
- }
- const onSocketMessage = {
- hot: function hot() {
- options.hot = true;
- console.info('[WDS] Hot Module Replacement enabled.')
- },
- liveReload: function liveReload() {
- options.liveReload = true;
- console.info('[WDS] Live Reloading enabled.')
- },
- invalid: function invalid() {
- console.info('[WDS] App updated. Recompiling...')
- },
- hash: function hash(_hash) {
- currentHash = _hash;
- },
- ok: function ok() {
- reloadApp();
- },
- close: function close() {
- console.error('[WDS] Disconnected!');
- }
- };
- let retries = 0
- let client = null
- const socket = (url, handlers) => {
- client = new SockJS(url)
- client.onopen = function () {
- retries = 0
- }
- client.onmessage = function (data) {
- var msg = JSON.parse(data.data);
- if (handlers[msg.type]) {
- handlers[msg.type](msg.data);
- }
- }
- client.onclose = function () {
- if (retries === 0) {
- handlers.close();
- } // Try to reconnect.
- client = null; // After 10 retries stop trying, to prevent logspam.
- if (retries <= 10) {
- var retryInMs = 1000 * Math.pow(2, retries) + Math.random() * 100;
- retries += 1;
- setTimeout(function () {
- socket(url, handlers);
- }, retryInMs);
- }
- }
- }
- socket(socketUrl, onSocketMessage)
【編輯推薦】