偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

【W(wǎng)ebpack】devServer 實(shí)驗(yàn)報(bào)告

開發(fā) 前端
本文從開發(fā)過程中遇到的痛點(diǎn)出發(fā),梳理了現(xiàn)代打包工具對(duì)我們?nèi)粘i_發(fā)的幫助和提效,并自娛自樂的結(jié)合表現(xiàn)和源碼,照虎畫貓完成了所謂的自洽模型。

 [[404559]]

Choosing a Development Tool

Webpack 的使用目前已經(jīng)是前端開發(fā)工程師必備技能之一。若是想在本地環(huán)境啟動(dòng)一個(gè)開發(fā)服務(wù)快速開發(fā)我們的應(yīng)用(而不是每次 coding 完,手動(dòng)執(zhí)行 run build,全量打包),大家只需在 Webpack 的配置中,增加 devServer 的配置即可。它的作用主要是用來伺服資源文件。webpack-dev-server(以下簡(jiǎn)稱 wds) 已經(jīng)為我們封裝好了全面、豐富且可配置化的功能,配置工程師們只需通過 webpack.config 和 命令行參數(shù) 即可滿足開發(fā)所需。 然而配置工程師們,發(fā)現(xiàn) wds 的 hot、live reload 實(shí)際上相當(dāng)于啟用了一個(gè) express 的 Http 服務(wù)器 + webpack-dev-middleware(以下簡(jiǎn)稱 wdm) 等中間件。這個(gè) Http 服務(wù)器 和 用戶訪問服務(wù)的 client 可以通過 websocket 通訊協(xié)議建立長連接,在 webpack 'watch' 到原始文件作出改動(dòng)后,wds 會(huì)使用 webpack 的實(shí)時(shí)編譯,再用 wdm 將 webpack 編譯后文件會(huì)輸出到內(nèi)存中。每當(dāng)應(yīng)用程序請(qǐng)求一個(gè)文件時(shí),wdm 匹配到了就把內(nèi)存中緩存的對(duì)應(yīng)結(jié)果以文件的格式返回給 client ,反之則進(jìn)入到下一個(gè)中間件。 如果想要使用更多 wds 提供的配置功能,比如 proxy、static、open 等, 在 server 端增加中間件即可,這樣配置工程師搖身一變,配置開發(fā)工程師! 項(xiàng)目中的 devServer 我們更多是使用 webpack + express + webpack-dev-middleware + webpack-hot-middleware 的組合來完成 HMR。在系列文章中,有更加具體詳細(xì)的學(xué)習(xí)分享來介紹這些,這里收縮一下,我們只關(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 配合一個(gè)開發(fā)服務(wù)器,可以提供熱重載功能。但只用于開發(fā)模式下。 wds 的底層,集成了 wdm,可以提供快速內(nèi)存訪問打包資源的功能。

以上是 wds 對(duì)自己的一個(gè)簡(jiǎn)短自我介紹,我們來搞清楚它這么概括的點(diǎn):

1. webpack 配合一個(gè)開發(fā)服務(wù)器,可以提供熱重載功能。

webpack 可以通過 watch mode 的方式啟動(dòng),指示 webpack 'watch' 依賴圖中所有文件的更改,并且自動(dòng)打包。但是每次打包后的結(jié)果將會(huì)存儲(chǔ)到本地硬盤中,而 IO 操作是非常耗資源時(shí)間的,無法滿足本地開發(fā)調(diào)試需求。 wds 則可以提供一個(gè)開發(fā)服務(wù)器,并且提供 live reloading(實(shí)時(shí)重載)功能,在打包完成后通知客戶端,刷新頁面重新加載資源。

2. 快速內(nèi)存訪問打包資源

wdm 可以將 webpack 編譯后的資源輸出到內(nèi)存中,當(dāng)應(yīng)用程序請(qǐng)求資源時(shí),可以直接從內(nèi)存中進(jìn)行響應(yīng)。 開發(fā)中,我們還會(huì)注意到,在編譯期間,客戶端的請(qǐng)求會(huì)被 delay 到最新的編譯結(jié)果完成之后才會(huì)去響應(yīng)。

  1. 「wdm」: wait until bundle finished: /myapp/ 

3. HMR 模塊熱替換

日常開發(fā)修改完代碼后,你有沒有傻乎乎地,手動(dòng)刷新調(diào)試頁面,來驗(yàn)證現(xiàn)在的 bug 還是不是之前的那個(gè) bug? 其實(shí) HMR 會(huì)在應(yīng)用程序運(yùn)行過程中,替換、添加或刪除模塊,而無需重新加載整個(gè)頁面。 可以通過以下幾種方式,來顯著提速開發(fā)效率:

  • 保留在完全重新加載頁面期間丟失的應(yīng)用程序狀態(tài)
  • 只更新變更內(nèi)容,以節(jié)省寶貴的開發(fā)時(shí)間
  • 在源代碼中對(duì) CSS/JS 進(jìn)行修改,會(huì)立刻在瀏覽器中進(jìn)行更新,這幾乎相當(dāng)于在瀏覽器 devtools 直接更改樣式

4. 服務(wù)啟動(dòng)后自動(dòng)啟動(dòng)瀏覽器 - open

有時(shí)候服務(wù)啟動(dòng)后,會(huì)打開 localhost:8080,或者打開瀏覽器的多個(gè)頁簽

5. 提供 history api 降級(jí)方案 - historyApiFallback

前端項(xiàng)目中,借助 history api,可以做到改變視圖而不向后端發(fā)出請(qǐng)求。如果你手動(dòng)刷新一個(gè)路由中匹配不到的頁面,同時(shí)你的項(xiàng)目中沒有配置 404 頁面兜底邏輯,那就真的 404 Not Found 了。 wds 配置中有一項(xiàng)配置 historyApiFallback,可以配置一個(gè)頁面代替所有的 404 響應(yīng)。

  1. 「wds」: 404s will fallback to /index.html 

6. 提供代理功能 - proxy

wds 的 proxy配置使用方法,可以詳見 Webpack-dev-server 的 proxy 用法。

  • 如果你有一個(gè)單獨(dú)的后臺(tái) API 服務(wù),你可以通過代理,把前端項(xiàng)目域名下發(fā)起的 API 請(qǐng)求,代理到后臺(tái)的域名。
  • 解決開發(fā)環(huán)境中的跨域問題
  • 通過代理你還可以定制返回的 html 頁面,比如同一個(gè)項(xiàng)目中你想要提供 PC、H5 兩端的產(chǎn)物,通過 UA 判斷,返回不同的打包產(chǎn)物 index.html or index.mobile.html

7. 代碼打包編譯出現(xiàn)警告和錯(cuò)誤時(shí),會(huì)在在頁面上顯示錯(cuò)誤信息 - overlay

控制代碼打包編譯時(shí)出現(xiàn)警告和錯(cuò)誤時(shí),是否在頁面上顯示錯(cuò)誤信息

8. output.path、output.publicPath、devServer.publicPath、devServer.contentBase

  • output.path 打包輸出產(chǎn)物的絕對(duì)路徑
  • output.publicPath 它會(huì)為所有打包后的資源指定一個(gè)基礎(chǔ)路徑,多用于靜態(tài)資源服務(wù)器或者 CDN 托管靜態(tài)資源
  • devServer.publicPath 掛載到服務(wù)器中間件的可訪問虛擬地址
  • devServer.contentBase 加載這個(gè)配置下(文件夾下)的靜態(tài)資源到服務(wù)器
  • output.path 打包產(chǎn)物的絕對(duì)路徑,沒有什么疑問。對(duì)于 output.publicPath、devServer.publicPath,還是有點(diǎn)迷惑不解吧? 假如有一個(gè)域名 example.com,但是你們應(yīng)用部署在 example.com/myapp/ 。沒有指定 output.publicPath,默認(rèn)為 '/',這時(shí) index.html 引用其他模塊的 url 會(huì)是 /bundle.xxxhashxxx.js,這時(shí)這個(gè)資源的 url 就變成了 example.com/bundle.xxxhashxxx.js,毫無疑問,這個(gè)資源會(huì) 404 Not Found。如果指定 output.publicPath: '/myapp/',那么 index.html 中資源的 url 就變成了 '/myapp/bundle.xxxhashxxx.js'。 同理 wds 中,指定 devServer.publicPath: '/myapp/',devServer 就會(huì)在 http://localhost:8080/myapp/ 下伺服資源訪問。模擬生產(chǎn)環(huán)境下的運(yùn)維配置。
  1. 「wds」: webpack output is served from /myapp/ 

contentBase 呢?它只作用于 wds,只有你想要伺服靜態(tài)資源文件的時(shí)候使用。換句話說,wds 會(huì)加載這個(gè)文件夾下的靜態(tài)資源到服務(wù)器,而不需要 bundle 這個(gè)文件夾。 假如,你的 app 中需要加載一些 mp4 文件,這些文件基本不會(huì)被改動(dòng),所以你不必把這些資源打包到 /dist 文件下,可以把這些文件維護(hù)在 /src、/dist 的同級(jí)目錄下的 /static。然后設(shè)置 contentBase: path.join(__dirname, 'static'),然后就可以在代碼中這樣引用靜態(tài)資源了。

  1. 「wds」: Content not from webpack is served from /Volumes/bomb/git/webpack-learning/webpack-demo/static 

9. more

構(gòu)建滿足這些特性的自洽模型

為了驗(yàn)證我們構(gòu)建的自洽模型,能夠自洽,我們需要一個(gè)參照物來進(jìn)行修正。 我們使用 devServer 官方配置,來伺服資源文件。 為了不影響體驗(yàn),自洽模型 和 參考物 的代碼都維護(hù)在第四節(jié)的參考,有興趣的可以自己 debugger 一下。

1. 模擬 http 服務(wù)器

首先我們使用 express 啟動(dòng)一個(gè)本地 server,讓瀏覽器可以訪問本地的靜態(tài)資源。

  1. // wds.server.js 
  2. const app = express(); 
  3. const listeningApp = http.createServer(app); 
  4. listeningApp.listen('8888''127.0.0.1', (err) => { 
  5.   createSocketServer(); 
  6. }); 

這里創(chuàng)建 http 服務(wù)器,沒有使用 app.listen('8888', callback),而是使用 http.createServer(app) 的原因有兩點(diǎn):

在創(chuàng)建 websocket server(代碼片段中的 createSocketServer)時(shí),需要復(fù)用 http 服務(wù)器實(shí)例 listeningApp,在下一小節(jié)會(huì)介紹 wss

express 只返回 http 服務(wù)器實(shí)例,而 devServer 是支持配置 https 的,所以可以直接用 https.createServer(app),更加方便

2. 模擬監(jiān)聽代碼文件更新

wds 調(diào)用 webpack api 對(duì)文件系統(tǒng)進(jìn)行 'watch',當(dāng)文件發(fā)生改變后,webpack 會(huì)重新對(duì)文件進(jìn)行編譯打包,然后保存到內(nèi)存中。 這一系列操作,主要有兩點(diǎn):1、watch 文件更改;2、內(nèi)存響應(yīng)。所幸,wdm 完成了這部分功能,我們?cè)谧郧⒛P椭兄苯右?wdm 。

  1. // wds.server.js 
  2. const webpack = require('webpack'); 
  3. const wdm = require('webpack-dev-middleware'); 
  4. const config = require('./webpack.config.js'); 
  5.  
  6. const compiler = webpack(config); // 將 webpack.config.js 配置文件作為基礎(chǔ)配置 
  7. const app = express(); 
  8. app.use(wdm(compiler)) // 告知 express 使用 webpack-dev-middleware 

這里不難看出,wdm(compiler) 的執(zhí)行結(jié)果返回的是一個(gè)中間件,它將 webpack 編譯后的文件存儲(chǔ)到內(nèi)存中,然后在用戶訪問 express 服務(wù)時(shí),將內(nèi)存中對(duì)應(yīng)的資源輸出返回。 那么 wdm 內(nèi)部是如何實(shí)現(xiàn)的呢? wdm 的源碼并不多,其核心只有 /index.js,/lib/middleware。

  1. // webpack-dev-middleware/index.js 
  2. // 在 compiler 的 invalid、run、done、watchRun 這 4 個(gè)編譯生命周期上,注冊(cè)對(duì)應(yīng)的處理方法。 
  3. // 通過 tapable 來調(diào)用插件功能,主要是 report 編譯的狀態(tài)信息以及執(zhí)行 context.callbacks 回調(diào)函數(shù) 
  4. const context = createContext(compiler, options); 
  5. ... 
  6. // 以監(jiān)控的方式啟動(dòng) webpack,調(diào)用 compiler 的 watch 方法,之后 webpack 便會(huì)監(jiān)聽文件變更,一旦檢測(cè)到文件變更,就會(huì)重新執(zhí)行編譯。 
  7. context.watching = compiler.watch(options.watchOptions, (err) => { ... }); 
  8. ... 
  9. // 使用 memory-fs,將 webpack 的編譯內(nèi)容,輸出至內(nèi)存中 
  10. setFs(context, compiler); 

  1. // webpack-dev-middleware/lib/middleware 
  2. // 核心邏輯是:針對(duì) request 請(qǐng)求,根據(jù)各種條件判斷,最終返回對(duì)應(yīng)的文件 
  3. module.exports = function wrapper(context) { 
  4.   // 返回 express 中間件函數(shù)的包裝函數(shù) 
  5.   return function middleware(req, res, next) { 
  6.     // 如果不是 SSR,直接 next,流轉(zhuǎn)到下一個(gè)中間件 
  7.     // 如果是 SSR,調(diào)用 util/ready,根據(jù) state 判斷執(zhí)行回調(diào) fn,還是將 fn 存儲(chǔ)到 callbacks 隊(duì)列中 
  8.     // ready 也是“在編譯期間,停止提供舊版的 bundle 并且將請(qǐng)求延遲到最新的編譯結(jié)果完成之后”的實(shí)現(xiàn) 
  9.    function goNext() { ... } 
  10.     // 根據(jù)請(qǐng)求的 req.url 地址,在 compiler 的內(nèi)存文件系統(tǒng)中查找對(duì)應(yīng)的文件,若查找不到,則直接調(diào)用 goNext() 方法處理請(qǐng)求 
  11.     let filename = getFilenameFromUrl( ... ) 
  12.     if (filename === false) { 
  13.       return goNext(); 
  14.     } 
  15.     // 根據(jù)上文找到的 filename 路徑獲取到對(duì)應(yīng)的文件內(nèi)容,并構(gòu)造 response 對(duì)象返回 
  16.     // 最后也是調(diào)用 ready 
  17.     return new Promise((resolve) => { 
  18.      handleRequest(context, filename, processRequest, req); 
  19.       function processRequest() { 
  20.        ... 
  21.       } 
  22.     }) 
  23.   } 

3. 模擬 server 端:server 端 通知 client 端 文件發(fā)生改變

使用 HMR 的過程中,通過 network 我們知道 client 端 是通過 websocket 和 server 端 進(jìn)行通信的。client 端 和 server 端 之間建立一個(gè) websocket 長連接,將 webpack 編譯打包的各個(gè)階段的狀態(tài)信息告知 client 端。最關(guān)鍵的還是 wds 注冊(cè) compiler hooks(compile、done、watchRun 等),當(dāng)進(jìn)入 webpack compile 生命周期時(shí)調(diào)用 hooks 回調(diào)注冊(cè)方法。 compilation done 時(shí),server 端 傳遞的最主要的信息是 'stats.hash' 和 'ok',然后 client 端 根據(jù) hash 進(jìn)行模塊熱更新。

  1. // wds.server.js 
  2. let connect = null // 長連接實(shí)例 
  3. // 調(diào)用 webpack api 監(jiān)聽 compile 的 done 事件 
  4. // 注冊(cè) compiler hooks -- done 
  5. const { done } = compiler.hooks 
  6. done.tap('myappPligins', (stats) => { 
  7.   if (connect) { 
  8.     let _stats = stats.toJson({...}) 
  9.     // 將編譯打包后的新模塊 hash 值發(fā)送到 client 端 
  10.     connect.write(JSON.stringify({ 
  11.       "type""hash"
  12.       "data": _stats.hash 
  13.     })) 
  14.     // 通知 client 端編譯完成,可以進(jìn)行 reloadApp 操作 
  15.     connect.write(JSON.stringify({ 
  16.       "type""ok" 
  17.     })) 
  18.   } 
  19. }); 
  20. // 創(chuàng)建 websocket server(wss) 
  21. // 目前 webpack-dev-server@4.X 使用 sockjs 會(huì)出錯(cuò),webpack-dev-server@3.X 使用 ws 會(huì)報(bào)錯(cuò) 
  22. function createSocketServer () { 
  23.   let socket = sockjs.createServer({ 
  24.     sockjs_url: './sockjs-client' 
  25.   }); 
  26.   // 復(fù)用 http 服務(wù)器實(shí)例 listeningApp 
  27.   socket.installHandlers(listeningApp, { 
  28.     prefix: '/sockjs-node'
  29.   }); 
  30.   socket.on('connection', (connection) => { 
  31.     connect = connection 
  32.     ... 
  33.   }); 

4. client 端接收 wss 消息并觸發(fā)響應(yīng)

我們?cè)跇I(yè)務(wù)代碼中并沒有添加接收 wss 消息的代碼,那 client 端 的邏輯怎么實(shí)現(xiàn)的呢? 其實(shí) wds 修改了 webpack.config.js 的基礎(chǔ)配置,它會(huì)往 chunk 中偷偷塞入兩個(gè)文件 webpack-dev-server/lib/client/index.js 和 webpack/hot/dev-server。 我們?cè)谧郧⒛P椭幸策@么操作,這樣這兩段代碼就植入到 client 端了。

  1. // wds.server.js 
  2. config.entry.app = [require.resolve('webpack/hot/dev-server'), './wds.client.js', config.entry.app] 
  3. // HMR 作為一個(gè) Webpack 內(nèi)置的功能,可以通過 HotModuleReplacementPlugin 開啟 
  4. config.plugins.push( 
  5.   new webpack.HotModuleReplacementPlugin() 

client 端 通過 websocket 接收 server 端 最新編輯后的模塊 hash 值,這個(gè)值會(huì)被存起來(currentHash),在接收到 ok 后才會(huì) reloadApp。 如果配置了 hot,開啟 HMR,會(huì)把程序控制權(quán)交給 webpack 的客戶端代碼進(jìn)行 HMR。如果沒有開啟,就直接調(diào)用 location.reload() 刷新頁面。

  1. // wds.client.js 
  2. const SockJS = require('./sockjs-client'
  3. const socketUrl = 'http://127.0.0.1:8888/sockjs-node' 
  4. let currentHash = '' // 最新代碼模塊的 hash 值 
  5.  
  6. function reloadApp () { 
  7.   if (options.hot) { 
  8.     let hotEmitter = require('webpack/hot/emitter'); 
  9.     // webpackHotUpdate 是 webpack 在 webpack/hot/dev-server.js 定義的一個(gè)事件,事件回調(diào)是獲取此次編譯的最新代碼 
  10.     hotEmitter.emit('webpackHotUpdate', currentHash); 
  11.   } else if (options.liveReload) { // 沒有配置 hmr,就直接 live reload 刷新頁面 
  12.     location.reload(); 
  13.   } 
  14. // 處理 wss 通知 
  15. const onSocketMessage = { 
  16.   ... 
  17.   hash: function hash(_hash) { 
  18.     currentHash = _hash; // wss 端 通知 client 端 最新編輯后的模塊 hash 值,這個(gè)值會(huì)被存起來(currentHash),在接收到 ok 后才會(huì) reloadApp 
  19.   }, 
  20.   ok: function ok() { 
  21.     reloadApp(); 
  22.   } 
  23. }; 
  24.  
  25. const socket = (url, handlers) => { 
  26.   client = new SockJS(url) 
  27.  ... 
  28.   client.onmessage = function (data) { // 接收 wss 通知 
  29.     var msg = JSON.parse(data.data); 
  30.     if (handlers[msg.type]) { 
  31.       handlers[msg.type](msg.data); 
  32.     } 
  33.   } 
  34.   ... 
  35. socket(socketUrl, onSocketMessage) 

5. 模擬 HMR or live reload當(dāng) client 端 收到 ok 的通知后,開啟 hot 的 wds,會(huì)執(zhí)行 reload 方法,然后調(diào)用 webpackHotUpdate

  1. // wds.client.js 
  2. let hotEmitter = require('webpack/hot/emitter'); 
  3. hotEmitter.emit('webpackHotUpdate', currentHash); 

然后程序被 client 端 的 webpack 接管(第四步中我們注入到 plugins 中的 webpack.HotModuleReplacementPlugin 就派上用場(chǎng)了),webpack 監(jiān)聽到 webpackHotUpdate 事件,并獲取到最新的 hash 值,然后開始檢查更新。

  1. // webpack/hot/dev-server.js 
  2. var hotEmitter = require("./emitter"); 
  3. hotEmitter.on("webpackHotUpdate"function (currentHash) { 
  4.   lastHash = currentHash; 
  5.   ... 
  6.   check(); 
  7. }); 
  8. // 檢查更新 
  9. var check = function check() { 
  10.   module.hot.check(true
  11.     .then(updatedModules => { 
  12.       ... 
  13.     }) 

源碼中,追蹤到 module.hot.check,就不知道路該怎么走了,hot.check 是哪里來的? 系列文章中有單獨(dú)介紹 HMR 的一章,這里我們就偷個(gè)懶,粗線條的勾勒一下大致過程。 hot.check 來自于 /webpack/lib/hmr/HotModuleReplacement.runtime.js。

利用上一次保存的hash值,調(diào)用 hotDownloadManifest 發(fā)送 xxx.hash.hot-update.json 的 ajax 請(qǐng)求

請(qǐng)求結(jié)果獲取熱更新模塊相關(guān)信息,并進(jìn)入熱更新準(zhǔn)備階段。

  • c: chunkIds m: removedChunks r: removedModules

通過 JSONP 的方式,調(diào)用 loadUpdateChunk 在 document.head 添加 script 標(biāo)簽,發(fā)送 chunkId.hash.hot-update.js 請(qǐng)求。

下面就是拿到的 hot-update.js 的內(nèi)容。

JSONP 返回的 js 文件立即執(zhí)行,會(huì)調(diào)用 window.webpackHotUpdatewebpack_demo 方法。此方法會(huì)把更新的模塊 moreModules (圖中入?yún)⒌牡诙€(gè)參數(shù)對(duì)象)賦值給全局全量 currentUpdate。

最后會(huì)調(diào)用 HMR runtime 的 hotApply 進(jìn)行熱更新模塊替換

6. 模擬 history api 降級(jí)方案

  1. // wds.server.js 
  2. // 添加中間件 connect-history-api-fallback,解決 history api 降級(jí) 
  3. app.use(historyApiFallback({ 
  4.   htmlAcceptHeaders: ['text/html''application/xhtml+xml'], // 只對(duì)這些類型的請(qǐng)求進(jìn)行 rewrite 
  5.   rewrites: [ 
  6.     { from: /./, to'/myapp/index.html' } 
  7.   ] 
  8. })) 

7. 模擬 proxy 代理

proxy 配置,我們經(jīng)常使用,那 node 是如何代理請(qǐng)求的呢?

在 wds 中,借助創(chuàng)建的 http 服務(wù)器,其 proxy 功能的實(shí)現(xiàn)就是解析配置項(xiàng),并掛載 http-proxy-middleware 中間件到 http 服務(wù)上。結(jié)合 proxy 的用法,wds/lib/Server.js 中的代碼顯得一目了然。 http-proxy-middleware 則借助于 node-http-proxy,用于將 node 服務(wù)端接收到的請(qǐng)求,轉(zhuǎn)發(fā)到目標(biāo)服務(wù)器,實(shí)現(xiàn)代理服務(wù)器的功能。 可以預(yù)見,整個(gè)流程的大致實(shí)現(xiàn)思路就是,通過配置項(xiàng)注冊(cè)全局的請(qǐng)求轉(zhuǎn)發(fā)規(guī)則,在中間件中攔截客戶端的 request 請(qǐng)求匹配轉(zhuǎn)發(fā)規(guī)則,然后調(diào)用 node-http-proxy 的 .web、.ws 方法進(jìn)行轉(zhuǎn)發(fā)請(qǐng)求。 http-proxy-middleware 將轉(zhuǎn)發(fā)規(guī)則分為兩大類進(jìn)行配置,context 和 options。

  1. // Proxy middleware configuration. 
  2. var proxy = require('http-proxy-middleware'); 
  3.  
  4. var apiProxy = proxy('/api', { target: 'http://www.example.org' }); 
  5. //                   \____/   \_____________________________/ 
  6. //                     |                    | 
  7. //                   context             options 
  8.  
  9. // 'apiProxy' is now ready to be used as middleware in a server. 

context 用于匹配需要進(jìn)行轉(zhuǎn)發(fā)的客戶端請(qǐng)求,默認(rèn)值是 '/',客戶端發(fā)起的所有請(qǐng)求都會(huì)被轉(zhuǎn)發(fā);也可以字符串 url、字符串 url 數(shù)組、通配符或者個(gè)性化方法,來決定哪些請(qǐng)求會(huì)被代理轉(zhuǎn)發(fā)。http-proxy-middleware 使用 options 中

  • target 用于設(shè)置要轉(zhuǎn)發(fā)到的目標(biāo)服務(wù)器的 host;
  • pathRewrite: object/function,用于改寫要轉(zhuǎn)發(fā)到的目標(biāo)服務(wù)器的 url path;
  • router: object/function,根據(jù)配置,將匹配的客戶端請(qǐng)求,改寫這次請(qǐng)求的 host。
  1. // rewrite path 
  2. pathRewrite: {'^/old/api' : '/new/api'
  3. // remove path 
  4. pathRewrite: {'^/remove/api' : ''
  5. // add base path 
  6. pathRewrite: {'^/' : '/basepath/'
  7. // custom rewriting 
  8. pathRewrite: function (path, req) {} 
  9.  
  10. router: { 
  11.     'integration.localhost:3000' : 'http://localhost:8001',  // host only 
  12.     'staging.localhost:3000'     : 'http://localhost:8002',  // host only 
  13.     'localhost:3000/api'         : 'http://localhost:8003',  // host + path 
  14.     '/rest'                      : 'http://localhost:8004'   // path only 

  1. // http-proxy-middleware/lib/index.js 
  2. function HttpProxyMiddleware(context, opts) { 
  3.  ... 
  4.   var config = configFactory.createConfig(context, opts) // 解析獲取 context、options 
  5.   ... 
  6.   var proxy = httpProxy.createProxyServer({}) // 創(chuàng)建代理服務(wù)器,由這個(gè)服務(wù)器進(jìn)行轉(zhuǎn)發(fā)請(qǐng)求 
  7.   ... 
  8.   var pathRewriter = PathRewriter.create(proxyOptions.pathRewrite) // 將客戶端請(qǐng)求路徑轉(zhuǎn)化為目標(biāo)服務(wù)器的路徑(pathname 部分),既可以是 key-value,也可以函數(shù)。 
  9.   ... 
  10.   function shouldProxy(context, req) { // 判斷請(qǐng)求是否需要轉(zhuǎn)發(fā) 
  11.     var path = req.originalUrl || req.url 
  12.     return contextMatcher.match(context, path, req) // 通過多種匹配方法校驗(yàn)客戶端 req 是否需要轉(zhuǎn)發(fā) 
  13.   } 
  14.   function prepareProxyRequest(req) { 
  15.     req.url = req.originalUrl || req.url 
  16.     var originalPath = req.url 
  17.     var newProxyOptions = _.assign({}, proxyOptions) 
  18.     __applyRouter(req, newProxyOptions) // 遍歷 options.router,校驗(yàn)是否匹配客戶端 req,匹配的話就改寫這次請(qǐng)求的 host 
  19.     __applyPathRewrite(req, pathRewriter) // 如果有 pathRewriter,就匹配當(dāng)前請(qǐng)求,匹配的話就將設(shè)置的目標(biāo)服務(wù)器路徑寫入 req.url 
  20.     return newProxyOptions 
  21.   } 
  22.   ... 
  23.   function middleware(req, res, next) { // 真正的代理中間件 
  24.     if (shouldProxy(config.context, req)) { 
  25.       var activeProxyOptions = prepareProxyRequest(req) 
  26.       proxy.web(req, res, activeProxyOptions) // node-http-proxy 進(jìn)行代理轉(zhuǎn)發(fā) 
  27.     } else { 
  28.       next() 
  29.     } 
  30.   } 
  31.   ... 
  32.   return middleware 

這樣看來,http-proxy-middleware 主要做的是解析轉(zhuǎn)發(fā)規(guī)則、最終把代理轉(zhuǎn)發(fā)的事情交給了 node-http-proxy,同時(shí)配置了相關(guān)的 Logger、綁定事件。

解析獲取 context、options,并配置 Logger 實(shí)例

通過 node-http-proxy 創(chuàng)建代理服務(wù)器,并 attach proxy-events

根據(jù) options.pathRewrite 生成路徑轉(zhuǎn)化器

匹配客戶端請(qǐng)求,通過代理服務(wù)器轉(zhuǎn)發(fā) http, https, websocket 請(qǐng)求

自洽實(shí)是自娛自樂

本文從開發(fā)過程中遇到的痛點(diǎn)出發(fā),梳理了現(xiàn)代打包工具對(duì)我們?nèi)粘i_發(fā)的幫助和提效,并自娛自樂的結(jié)合表現(xiàn)和源碼,照虎畫貓完成了所謂的自洽模型。其實(shí)自洽模型畫的遠(yuǎn)不如貓,最多就是一個(gè)四支腿生物的簡(jiǎn)筆畫了。權(quán)當(dāng)梳理了一遍 wds 的工作流程,更加細(xì)節(jié)的東西,還需要大家一起動(dòng)手才能挖掘出來,希望能對(duì)你的理解過程起到一定的幫助作用。

參考

參考一:社區(qū)文章

  1. webpack 官網(wǎng)
  2. publicPath、contentBase
  3. 十分鐘搞懂 webpack
  4. 輕松理解 webpack 熱更新原理
  5. 再不怕被問到 HMR
  6. wdm 源碼解讀
  7. Webpack-dev-server 的 proxy 用法
  8. 知乎 HMR 原理解析
  9. 關(guān)于 tapable 你需要知道這些
  10. 相關(guān)包的版本:

"webpack": "5.24.0", "webpack-cli": "4.5.0", "webpack-dev-server": "^3.11.2"

參考二:自洽模型的參照物

  1. // webpack.config.js 使用通用的 Vue 項(xiàng)目配置 
  2. module.exports = { 
  3.   mode: 'development'
  4.   entry: { 
  5.     app: './src/app.js' 
  6.   }, 
  7.   output: { 
  8.     filename: '[name].bundle.js'
  9.     path: path.resolve(__dirname, 'dist'), 
  10.     publicPath: '/myapp/' 
  11.   }, 
  12.   devtool: 'inline-source-map'
  13.   plugins: [ 
  14.   ...some plugins 
  15.   ], 
  16.   module: { 
  17.     rules: [ ...some loaders ] 
  18.   } 
  19. }; 

然后把 devServer 的配置單獨(dú)維護(hù)在 webpack.dev.js

  1. // webpack.dev.js devServer 配置 
  2. module.exports = merge([ 
  3.   base, // webpack.config.js 
  4.   { 
  5.     mode: 'development'
  6.     devServer: { 
  7.       host: '127.0.0.1', // 服務(wù)器 host,默認(rèn)為 localhost 
  8.       port: 7777, // 服務(wù)器端口號(hào),默認(rèn)為 8080 
  9.       opentrue, // string | boolean,啟動(dòng)后是否打開瀏覽器,當(dāng)為字符串時(shí),打開指定瀏覽器 
  10.       openPage: 'myapp/', // string | Array<string>, ['''index.html'], 'index.html', 打開瀏覽器后默認(rèn)打開的頁面,Array 打開多個(gè)頁面 
  11.       compress: true
  12.       hot: true, // 是否啟動(dòng)熱更新(HMR),熱更新使用的是 webpack 中 HotModuleReplacementPlugin 
  13.       http2: false, // 是否設(shè)置 HTTP/2 服務(wù)器,為 true,則默認(rèn)使用 https 作為服務(wù) 
  14.       // https: { 
  15.       //   key'',//fs.readFileSync('/path/to/server.key'), 
  16.       //   cert: '',//fs.readFileSync('/path/to/server.crt'), 
  17.       //   ca: '',//fs.readFileSync('/path/to/ca.pem'
  18.       // }, 
  19.       proxy: { 
  20.         '/api': { 
  21.           target: 'http://localhost:7777'
  22.           pathRewrite: { '^/api''' }, 
  23.           secure: false // HTTPS 設(shè)置為無效證書 
  24.         } 
  25.       }, 
  26.       // 靜態(tài)文件屬性 
  27.       publicPath: '/myapp/', // 掛載到服務(wù)器中間件的可訪問虛擬地址 
  28.       contentBase: path.join(__dirname, 'static'), // devServer 伺服這個(gè)文件夾下的靜態(tài)資源。換句話說會(huì)加載本地 /static 目錄下的靜態(tài)文件到服務(wù)器 
  29.       stats: 'minimal'
  30.       // 設(shè)置編譯出錯(cuò)或警告后,頁面是否會(huì)直接顯示信息, boolean | {} 
  31.       // 默認(rèn)為 false,當(dāng)失敗后會(huì)顯示空白頁 
  32.       // 設(shè)置為 true 后,編譯失敗會(huì)顯示錯(cuò)誤/警告的覆蓋層,也可以設(shè)置為 object,顯示多種類型信息 
  33.       overlay: { 
  34.         warnings: true
  35.         errors: true 
  36.       }, 
  37.       injectClient: true, // 是否要注入 WebSocket 客戶端,將此屬性設(shè)置為 false,那么 hot、overlay 等功能都會(huì)失效 
  38.       injectHot: true, // 是否注入 HMR, 這個(gè)屬性是 injectClient 的子集。只影響熱更新 
  39.       liveReload: false, // 是否開啟自動(dòng)刷新瀏覽器功能,優(yōu)先級(jí)低于 hot 
  40.       // 是否將所有 404 頁面都跳轉(zhuǎn)到 index.html,當(dāng)此屬性設(shè)置為 true 或?yàn)?nbsp;object 時(shí)并且使用 history API 時(shí)所有 404 頁面會(huì)跳轉(zhuǎn)到 index.html 或指定的頁面 
  41.       historyApiFallback: { 
  42.         rewrites: [ 
  43.           { from: /./, to'/myapp/index.html' }, 
  44.         ] 
  45.       }, 
  46.       //  設(shè)置 WebSocket,設(shè)置使用的 WebSocket 庫,內(nèi)置為 sockjs 或 ws 
  47.       transportMode: { 
  48.         //  目前 webpack-dev-server@4.X 使用 sockjs 會(huì)出錯(cuò),webpack-dev-server@3.X 使用 ws 會(huì)報(bào)錯(cuò) 
  49.         server: 'sockjs' 
  50.       } 
  51.     } 
  52.   } 
  53. ]) 

package.json

  1. // package.json 
  2. "scripts": { 
  3.   "start""webpack serve --config webpack.dev.js"
  4.    "start:dev""node wds.server.js"

參考三:自洽模型

  1. // wds.server.js 
  2. const express = require('express'); 
  3. const webpack = require('webpack'); 
  4. const http = require('http'); 
  5. const webpackDevMiddleware = require('webpack-dev-middleware'); 
  6. const historyApiFallback = require('connect-history-api-fallback'); 
  7. const sockjs = require('sockjs'); 
  8.  
  9. const app = express(); 
  10. const config = require('./webpack.config.js'); 
  11.  
  12. config.entry.app = [require.resolve('webpack/hot/dev-server'), './wds.client.js', config.entry.app] 
  13. config.plugins.push( 
  14.   new webpack.HotModuleReplacementPlugin() 
  15.  
  16. // 告知 express 使用 webpack-dev-middleware, 
  17. // 以及將 webpack.config.js 配置文件作為基礎(chǔ)配置。 
  18. const compiler = webpack(config) 
  19.  
  20. // historyApiFallback 
  21. app.use(historyApiFallback({ 
  22.   htmlAcceptHeaders: ['text/html''application/xhtml+xml'], 
  23.   rewrites: [ 
  24.     { from: /./, to'/myapp/index.html' } 
  25.   ] 
  26. })) 
  27.  
  28. app.use( 
  29.   webpackDevMiddleware(compiler, { 
  30.     publicPath: config.output.publicPath, 
  31.   }) 
  32.  
  33. let connect = null 
  34.  
  35. const { done } = compiler.hooks 
  36. done.tap('myappPligins', (stats) => { 
  37.   if (connect) { 
  38.     let _stats = stats.toJson({ 
  39.       allfalse
  40.       hash: true
  41.       assets: true
  42.       warnings: true
  43.       errors: true
  44.       errorDetails: false
  45.     }) 
  46.     connect.write(JSON.stringify({ 
  47.       "type""hash"
  48.       "data": _stats.hash 
  49.     })) 
  50.     connect.write(JSON.stringify({ 
  51.       "type""ok" 
  52.     })) 
  53.   } 
  54. }); 
  55.  
  56.  
  57. const listeningApp = http.createServer(app); 
  58.  
  59. function createSocketServer () { 
  60.   let socket = sockjs.createServer({ 
  61.     sockjs_url: './sockjs-client' 
  62.   }); 
  63.   socket.installHandlers(listeningApp, { 
  64.     prefix: '/sockjs-node'
  65.   }); 
  66.   socket.on('connection', (connection) => { 
  67.     if (!connection) { 
  68.       return
  69.     } 
  70.  
  71.     connect = connection 
  72.  
  73.     // 通知 client enable 了哪些功能 
  74.     connection.write(JSON.stringify({ 
  75.       "type""hot" 
  76.     })) 
  77.   }); 
  78.  
  79. listeningApp.listen('8888''127.0.0.1', (err) => { 
  80.   console.log('Example app listening on port 8888!\n'); 
  81.   createSocketServer(); 
  82. }); 
  83.  
  84. listeningApp.on('error', (err) => { 
  85.   console.error(err); 
  86. }); 

  1. // wds.client.js 
  2. console.log('this is from client.'
  3. const SockJS = require('./sockjs-client'
  4. const socketUrl = 'http://127.0.0.1:8888/sockjs-node' 
  5. const options = { 
  6.   hot: true
  7.   hotReload: true
  8.   liveReload: false
  9.   initial: true
  10.   useWarningOverlay: false
  11.   useErrorOverlay: false
  12.   useProgress: false 
  13. let currentHash = '' 
  14.  
  15. function reloadApp () { 
  16.   if (options.hot) { 
  17.     console.log('[WDS] App hot update...'); 
  18.     let hotEmitter = require('webpack/hot/emitter'); 
  19.     hotEmitter.emit('webpackHotUpdate', currentHash); 
  20.     // broadcast update to window 
  21.     window.postMessage("webpackHotUpdate".concat(currentHash), '*'); 
  22.   } else if (options.liveReload) { 
  23.     location.reload(); 
  24.   } 
  25.  
  26. const onSocketMessage = { 
  27.   hot: function hot() { 
  28.     options.hot = true
  29.     console.info('[WDS] Hot Module Replacement enabled.'
  30.   }, 
  31.   liveReload: function liveReload() { 
  32.     options.liveReload = true
  33.     console.info('[WDS] Live Reloading enabled.'
  34.   }, 
  35.   invalid: function invalid() { 
  36.     console.info('[WDS] App updated. Recompiling...'
  37.   }, 
  38.   hash: function hash(_hash) { 
  39.     currentHash = _hash; 
  40.   }, 
  41.   ok: function ok() { 
  42.     reloadApp(); 
  43.   }, 
  44.   closefunction close() { 
  45.     console.error('[WDS] Disconnected!'); 
  46.   } 
  47. }; 
  48.  
  49. let retries = 0 
  50. let client = null 
  51.  
  52. const socket = (url, handlers) => { 
  53.   client = new SockJS(url) 
  54.   client.onopen = function () { 
  55.     retries = 0 
  56.   } 
  57.  
  58.   client.onmessage = function (data) { 
  59.     var msg = JSON.parse(data.data); 
  60.  
  61.     if (handlers[msg.type]) { 
  62.       handlers[msg.type](msg.data); 
  63.     } 
  64.   } 
  65.  
  66.   client.onclose = function () { 
  67.     if (retries === 0) { 
  68.       handlers.close(); 
  69.     } // Try to reconnect. 
  70.  
  71.     client = null; // After 10 retries stop trying, to prevent logspam. 
  72.  
  73.     if (retries <= 10) { 
  74.       var retryInMs = 1000 * Math.pow(2, retries) + Math.random() * 100; 
  75.       retries += 1; 
  76.       setTimeout(function () { 
  77.         socket(url, handlers); 
  78.       }, retryInMs); 
  79.     } 
  80.   } 
  81.  
  82. socket(socketUrl, onSocketMessage) 

 【編輯推薦】

 

責(zé)任編輯:姜華 來源: 微醫(yī)大前端技術(shù)
相關(guān)推薦

2021-12-21 14:00:25

WebpackDevServer的開發(fā)

2011-05-25 17:51:40

2011-04-12 11:01:48

LinuxUnix桌面

2022-03-08 09:16:20

webpack前端開發(fā)

2022-10-09 08:43:47

H5Webpack代碼

2020-08-05 08:21:41

Webpack

2024-05-27 00:00:01

2020-09-19 21:26:56

webpack

2020-11-17 08:09:01

webpack配置項(xiàng)腳手架

2011-04-22 10:30:11

VMwareWindowsFTP

2024-09-27 11:46:51

2010-07-28 20:47:31

2012-11-08 20:22:18

2021-12-15 23:42:56

Webpack原理實(shí)踐

2021-09-06 06:45:06

WebpackMindMasterEntry

2017-07-11 15:50:11

前端webpack2優(yōu)化

2017-03-24 10:56:21

Webpack技巧建議

2021-12-16 22:02:28

webpack原理模塊化

2018-02-01 17:03:50

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)