您好,登錄后才能下訂單哦!
這篇文章主要講解了“webpack-dev-server的核心概念以及熱加載”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“webpack-dev-server的核心概念以及熱加載”吧!
webpack-dev-server 會(huì)使用當(dāng)前的路徑作為請(qǐng)求的資源路徑(所謂
當(dāng)前的路徑
就是運(yùn)行 webpack-dev-server 這個(gè)命令的路徑,如果對(duì) webpack-dev-server 進(jìn)行了包裝,比如 wcf,那么當(dāng)前路徑指的就是運(yùn)行 wcf 命令的路徑,一般是項(xiàng)目的根路徑),但是讀者可以通過指定 content-base 來(lái)修改這個(gè)默認(rèn)行為:
webpack-dev-server --content-base build/
這樣 webpack-dev-server 就會(huì)使用 build 目錄下的資源來(lái)處理靜態(tài)資源的請(qǐng)求,如 css/ 圖片等。content-base 一般不要和 publicPath、output.path 混淆掉。其中 content-base 表示靜態(tài)資源的路徑是什么,比如下面的例子:
<!DOCTYPE html> <html> <head> <title></title> <link rel="stylesheet" type="text/css" href="index.css" rel="external nofollow" > </head> <body> <div id="react-content">這里要插入 js 內(nèi)容</div> </body> </html>
在作為 html-webpack-plugin 的 template 以后,那么上面的 index.css 路徑到底是什么?是相對(duì)于誰(shuí)來(lái)說?上面已經(jīng)強(qiáng)調(diào)了:如果在沒有指定 content-base 的情況下就是相對(duì)于當(dāng)前路徑來(lái)說的,所謂的當(dāng)前路徑就是在運(yùn)行 webpack-dev-server 目錄來(lái)說的,所以假如在項(xiàng)目根路徑運(yùn)行了這個(gè)命令,那么就要保證在項(xiàng)目根路徑下存在該 index.css 資源,否則就會(huì)存在 html-webpack-plugin 的 404 報(bào)錯(cuò)。當(dāng)然,為了解決這個(gè)問題,可以將 content-base 修改為和 html-webpack-plugin的html 模板一樣的目錄。
上面講到 content-base 只是和靜態(tài)資源的請(qǐng)求有關(guān),那么我們將其 publicPath 和 output.path 做一個(gè)區(qū)分。
首先:假如將 output.path 設(shè)置為build(這里的 build 和 content-base 的 build 沒有任何關(guān)系,請(qǐng)不要混淆),要知道 webpack-dev-server 實(shí)際上并沒有將這些打包好的 bundle 寫到這個(gè)目錄下,而是存在于內(nèi)存中的,但是我們可以假設(shè)(注意這里是假設(shè))其是寫到這個(gè)目錄下的。
然后:這些打包好的 bundle 在被請(qǐng)求的時(shí)候,其路徑是相對(duì)于配置的publicPath來(lái)說的,publicPath 相當(dāng)于虛擬路徑,其映射于指定的output.path。假如指定的 publicPath 為 "/assets/",而且 output.path 為 "build",那么相當(dāng)于虛擬路徑 "/assets/" 對(duì)應(yīng)于 "build"(前者和后者指向的是同一個(gè)位置),而如果 build 下有一個(gè) "index.css",那么通過虛擬路徑訪問就是/assets/index.css。
最后:如果某一個(gè)內(nèi)存路徑(文件寫在內(nèi)存中)已經(jīng)存在特定的 bundle,而且編譯后內(nèi)存中有新的資源,那么我們也會(huì)使用新的內(nèi)存中的資源來(lái)處理該請(qǐng)求,而不是使用舊的 bundle!比如有一個(gè)如下的配置:
module.exports = { entry: { app: ["./app/main.js"] }, output: { path: path.resolve(__dirname, "build"), publicPath: "/assets/", //此時(shí)相當(dāng)于/assets/路徑對(duì)應(yīng)于 build 目錄,是一個(gè)映射的關(guān)系 filename: "bundle.js" } }
那么我們要訪問編譯后的資源可以通過 localhost:8080/assets/bundle.js 來(lái)訪問。如果在 build 目錄下有一個(gè) html 文件,那么可以使用下面的方式來(lái)訪問 js 資源:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <script src="assets/bundle.js"></script> </body> </html>
此時(shí)會(huì)看到控制臺(tái)輸出如下內(nèi)容:
enter image description here
主要關(guān)注下面兩句輸出:
Webpack result is served from /assets/
Content is served from /users/…./build
之所以是這樣的輸出結(jié)果是因?yàn)樵O(shè)置了 contentBase 為 build,因?yàn)檫\(yùn)行的命令為webpack-dev-server --content-base build/
。所以,一般情況下:如果在 html 模板中不存在對(duì)外部相對(duì)資源的引用,我們并不需要指定 content-base,但是如果存在對(duì)外部相對(duì)資源 css/ 圖片的引用,可以通過指定 content-base 來(lái)設(shè)置默認(rèn)靜態(tài)資源加載的路徑,除非所有的靜態(tài)資源全部在當(dāng)前目錄下。
為 webpack-dev-server 開啟 HMR 模式只需要在命令行中添加--hot,它會(huì)將 HotModuleReplacementPlugin 這個(gè)插件添加到 webpack 的配置中去,所以開啟 HotModuleReplacementPlugin 最簡(jiǎn)單的方式就是使用 inline 模式。在 inline 模式下,只需要在命令行中添加--inline --hot就可以自動(dòng)實(shí)現(xiàn)。
這時(shí)候 webpack-dev-server 就會(huì)自動(dòng)添加 webpack/hot/dev-server 入口文件到配置中,只是需要訪問下面的路徑就可以了 http://?host?:?port?/?path?。在控制臺(tái)中可以看到如下的內(nèi)容
其中以 [HMR] 開頭的部分來(lái)自于 webpack/hot/dev-server 模塊,而以[WDS]開頭的部分來(lái)自于 webpack-dev-server 的客戶端。下面的部分來(lái)自于 webpack-dev-server/client/index.js 內(nèi)容,其中的 log 都是以 [WDS] 開頭的:
function reloadApp() { if(hot) { log("info", "[WDS] App hot update..."); window.postMessage("webpackHotUpdate" + currentHash, "*"); } else { log("info", "[WDS] App updated. Reloading..."); window.location.reload(); } }
而在 webpack/hot/dev-server 中的 log 都是以 [HMR] 開頭的(它是來(lái)自于 Webpack 本身的一個(gè) plugin):
if(!updatedModules) { console.warn("[HMR] Cannot find update. Need to do a full reload!"); console.warn("[HMR] (Probably because of restarting the webpack-dev-server)"); window.location.reload(); return; }
那么如何在 nodejs 中使用 HMR 功能呢?此時(shí)需要修改三處配置文件:
1.添加一個(gè) Webpack 的入口點(diǎn),也就是 webpack/hot/dev-server
2.添加一個(gè) new webpack.HotModuleReplacementPlugin() 到 webpack 的配置中
3.添加 hot:true 到 webpack-dev-server 配置中,從而在服務(wù)端啟動(dòng) HMR(可以在 cli 中使用 webpack-dev-server --hot)
比如下面的代碼就展示了 webpack-dev-server 為了實(shí)現(xiàn) HMR 是如何處理入口文件的:
if(options.inline) { var devClient = [require.resolve("../client/") + "?" + protocol + "://" + (options.public || (options.host + ":" + options.port))]; //將 webpack-dev-server 的客戶端入口添加到的 bundle 中,從而達(dá)到自動(dòng)刷新 if(options.hot) devClient.push("webpack/hot/dev-server"); //這里是 webpack-dev-server 中對(duì) hot 配置的處理 [].concat(wpOpt).forEach(function(wpOpt) { if(typeof wpOpt.entry === "object" && !Array.isArray(wpOpt.entry)) { Object.keys(wpOpt.entry).forEach(function(key) { wpOpt.entry[key] = devClient.concat(wpOpt.entry[key]); }); } else { wpOpt.entry = devClient.concat(wpOpt.entry); } }); }
滿足上面三個(gè)條件的 nodejs 使用方式如下:
var config = require("./webpack.config.js"); config.entry.app.unshift("webpack-dev-server/client?http://localhost:8080/", "webpack/hot/dev-server"); //條件一(添加了 webpack-dev-server 的客戶端和 HMR 的服務(wù)端) var compiler = webpack(config); var server = new webpackDevServer(compiler, { hot: true //條件二(--hot 配置,webpack-dev-server 會(huì)自動(dòng)添加 HotModuleReplacementPlugin) ... }); server.listen(8080);
webpack-dev-server 使用
http-proxy-middleware
去把請(qǐng)求代理到一個(gè)外部的服務(wù)器,配置的樣例如下:
proxy: { '/api': { target: 'https://other-server.example.com', secure: false } } // In webpack.config.js { devServer: { proxy: { '/api': { target: 'https://other-server.example.com', secure: false } } } } // Multiple entry proxy: [ { context: ['/api-v1/**', '/api-v2/**'], target: 'https://other-server.example.com', secure: false } ]
這種代理在很多情況下是很重要的,比如可以把一些靜態(tài)文件通過本地的服務(wù)器加載,而一些 API 請(qǐng)求全部通過一個(gè)遠(yuǎn)程的服務(wù)器來(lái)完成。還有一個(gè)情景就是在兩個(gè)獨(dú)立的服務(wù)器之間進(jìn)行請(qǐng)求分割,如一個(gè)服務(wù)器負(fù)責(zé)授權(quán)而另外一個(gè)服務(wù)器負(fù)責(zé)應(yīng)用本身。下面給出日常開發(fā)中遇到的一個(gè)例子:
(1)有一個(gè)請(qǐng)求是通過相對(duì)路徑來(lái)完成的,比如地址是 "/msg/show.htm"。但是,在日常和生產(chǎn)環(huán)境下前面會(huì)加上不同的域名,如日常是 you.test.com 而生產(chǎn)環(huán)境是 you.inc.com。
(2)那么比如現(xiàn)在想在本地啟動(dòng)一個(gè) webpack-dev-server,然后通過 webpack-dev-server 來(lái)訪問日常的服務(wù)器,而且日常的服務(wù)器地址是 11.160.119.131,所以會(huì)通過如下的配置來(lái)完成:
devServer: { port: 8000, proxy: { "/msg/show.htm": { target: "http://11.160.119.131/", secure: false } } }
此時(shí)當(dāng)請(qǐng)求 "/msg/show.htm" 的時(shí)候,其實(shí)請(qǐng)求的真實(shí) URL 地址為 "http//11.160.119.131/msg/show.htm"。
(3)在開發(fā)環(huán)境中遇到一個(gè)問題,那就是:如果本地的 devServer 啟動(dòng)的地址為: "http://30.11.160.255:8000/" 或者常見的 "http://0.0.0.0:8000/" ,那么真實(shí)的服務(wù)器會(huì)返回一個(gè) URL 要求登錄,但是,將本地 devServer 啟動(dòng)到 localhost 上就不存在這個(gè)問題了(一個(gè)可能的原因在于 localhost 種上了后端需要的 cookie,而其他的域名沒有種上 cookie,導(dǎo)致代理服務(wù)器訪問日常服務(wù)器的時(shí)候沒有相應(yīng)的 cookie,從而要求權(quán)限驗(yàn)證)。其中指定 localhost 的方式可以通過
wcf
來(lái)完成,因?yàn)?wcf 默認(rèn)可以支持 IP 或者 localhost 方式來(lái)訪問。當(dāng)然也可以通過添加下面的代碼來(lái)完成:
devServer: { port: 8000, host:'localhost', proxy: { "/msg/show.htm": { target: "http://11.160.119.131/", secure: false } } }
(4)關(guān)于 webpack-dev-server 的原理,讀者可以查看“反向代理為何叫反向代理”等資料來(lái)了解,其實(shí)正向代理和反向代理用一句話來(lái)概括就是:“正向代理隱藏了真實(shí)的客戶端,而反向代理隱藏了真實(shí)的服務(wù)器”。而 webpack-dev-server 其實(shí)扮演了一個(gè)代理服務(wù)器的角色,服務(wù)器之間通信不會(huì)存在前端常見的同源策略,這樣當(dāng)請(qǐng)求 webpack-dev-server 的時(shí)候,它會(huì)從真實(shí)的服務(wù)器中請(qǐng)求數(shù)據(jù),然后將數(shù)據(jù)發(fā)送給你的瀏覽器。
browser => localhost:8080(webpack-dev-server無(wú)代理) => http://you.test.com browser => localhost:8080(webpack-dev-server有代理) => http://you.test.com
上面的第一種情況就是沒有代理的情況,在 localhost:8080 的頁(yè)面通過前端策略去訪問 http://you.test.com 會(huì)存在同源策略,即第二步是通過前端策略去訪問另外一個(gè)地址的。但是對(duì)于第二種情況,第二步其實(shí)是通過代理去完成的,即服務(wù)器之間的通信,不存在同源策略問題。而我們變成了直接訪問代理服務(wù)器,代理服務(wù)器返回一個(gè)頁(yè)面,對(duì)于頁(yè)面中某些滿足特定條件前端請(qǐng)求(proxy、rewrite配置)全部由代理服務(wù)器來(lái)完成,這樣同源問題就通過代理服務(wù)器的方式得到了解決。
(5)上面講述的是 target 是 IP 的情況,如果 target 要指定為域名的方式,可能需要綁定 host。比如下面綁定的 host:
11.160.119.131 youku.min.com
那么下面的 proxy 配置就可以采用域名了:
devServer: { port: 8000, proxy: { "/msg/show.htm": { target: "http://youku.min.com/", secure: false } } }
這和 target 綁定為 IP 地址的效果是完全一致的??偨Y(jié)一句話:“target 指定了滿足特定 URL 的請(qǐng)求應(yīng)該對(duì)應(yīng)到哪臺(tái)主機(jī)上,即代理服務(wù)器應(yīng)該訪問的真實(shí)主機(jī)地址”。
其實(shí) proxy 還可以通過配置一個(gè) bypass() 函數(shù)的返回值視情況繞開一個(gè)代理。這個(gè)函數(shù)可以查看 HTTP 請(qǐng)求和響應(yīng)及一些代理的選項(xiàng)。它返回要么是 false 要么是一個(gè) URL 的 path,這個(gè) path 將會(huì)用于處理請(qǐng)求而不是使用原來(lái)代理的方式完成。下面例子的配置將會(huì)忽略來(lái)自于瀏覽器的 HTTP 請(qǐng)求,它和 historyApiFallback 配置類似。瀏覽器請(qǐng)求可以像往常一樣接收到 html 文件,但是 API 請(qǐng)求將會(huì)被代理到另外的服務(wù)器:
proxy: { '/some/path': { target: 'https://other-server.example.com', secure: false, bypass: function(req, res, proxyOptions) { if (req.headers.accept.indexOf('html') !== -1) { console.log('Skipping proxy for browser request.'); return '/index.html'; } } } }
對(duì)于代理的請(qǐng)求也可以通過提供一個(gè)函數(shù)來(lái)重寫,這個(gè)函數(shù)可以查看或者改變 HTTP 請(qǐng)求。下面的例子就會(huì)重寫 HTTP 請(qǐng)求,其主要作用就是移除 URL 前面的 /api 部分。
proxy: { '/api': { target: 'https://other-server.example.com', pathRewrite: {'^/api' : ''} } }
其中 pathRewrite 配置來(lái)自于 http-proxy-middleware。更多配置可以查看
http-proxy-middleware 官方文檔。
當(dāng)使用 HTML 5 的 history API 的時(shí)候,當(dāng) 404 出現(xiàn)的時(shí)候可能希望使用 index.html 來(lái)作為請(qǐng)求的資源,這時(shí)候可以使用這個(gè)配置 :historyApiFallback:true。然而,如果修改了 output.publicPath,就需要指定重定向的 URL,可以使用 historyApiFallback.index 選項(xiàng)。
// output.publicPath: '/foo-app/' historyApiFallback: { index: '/foo-app/' }
使用 rewrite 選項(xiàng)可以重新設(shè)置靜態(tài)資源
historyApiFallback: { rewrites: [ // shows views/landing.html as the landing page { from: /^\/$/, to: '/views/landing.html' }, // shows views/subpage.html for all routes starting with /subpage { from: /^\/subpage/, to: '/views/subpage.html' }, // shows views/404.html on all other pages { from: /./, to: '/views/404.html' }, ], },
使用 disableDotRule 來(lái)滿足一個(gè)需求,即如果一個(gè)資源請(qǐng)求包含一個(gè).
符號(hào),那么表示是對(duì)某一個(gè)特定資源的請(qǐng)求,也就滿足 dotRule。我們看看
connect-history-api-fallback 內(nèi)部是如何處理的:
if (parsedUrl.pathname.indexOf('.') !== -1 && options.disableDotRule !== true) { logger( 'Not rewriting', req.method, req.url, 'because the path includes a dot (.) character.' ); return next(); } rewriteTarget = options.index || '/index.html'; logger('Rewriting', req.method, req.url, 'to', rewriteTarget); req.url = rewriteTarget; next(); };
也就是說,如果是對(duì)絕對(duì)資源的請(qǐng)求,也就是滿足 dotRule,但是 disableDotRule(disable dot rule file request)為 false,表示我們會(huì)自己對(duì)滿足 dotRule 的資源進(jìn)行處理,所以不用定向到 index.html 中!如果 disableDotRule 為 true 表示不會(huì)對(duì)滿足 dotRule 的資源進(jìn)行處理,所以直接定向到 index.html!
history({ disableDotRule: true })
var server = new WebpackDevServer(compiler, { contentBase: "/path/to/directory", //content-base 配置 hot: true, //開啟 HMR,由 webpack-dev-server 發(fā)送 "webpackHotUpdate" 消息到客戶端代碼 historyApiFallback: false, //單頁(yè)應(yīng)用 404 轉(zhuǎn)向 index.html compress: true, //開啟資源的 gzip 壓縮 proxy: { "**": "http://localhost:9090" }, //代理配置,來(lái)源于 http-proxy-middleware setup: function(app) { //webpack-dev-server 本身是 Express 服務(wù)器可以添加自己的路由 // app.get('/some/path', function(req, res) { // res.json({ custom: 'response' }); // }); }, //為 Express 服務(wù)器的 express.static 方法配置參數(shù) http://expressjs.com/en/4x/api.html#express.static staticOptions: { }, //在 inline 模式下用于控制在瀏覽器中打印的 log 級(jí)別,如`error`, `warning`, `info` or `none`. clientLogLevel: "info", //不在控制臺(tái)打印任何 log quiet: false, //不輸出啟動(dòng) log noInfo: false, //webpack 不監(jiān)聽文件的變化,每次請(qǐng)求來(lái)的時(shí)候重新編譯 lazy: true, //文件名稱 filename: "bundle.js", //webpack 的 watch 配置,每隔多少秒檢查文件的變化 watchOptions: { aggregateTimeout: 300, poll: 1000 }, //output.path 的虛擬路徑映射 publicPath: "/assets/", //設(shè)置自定義 http 頭 headers: { "X-Custom-Header": "yes" }, //打包狀態(tài)信息輸出配置 stats: { colors: true }, //配置 https 需要的證書等 https: { cert: fs.readFileSync("path-to-cert-file.pem"), key: fs.readFileSync("path-to-key-file.pem"), cacert: fs.readFileSync("path-to-cacert-file.pem") } }); server.listen(8080, "localhost", function() {}); // server.close();
上面其他配置中,除了 filename 和 lazy 外都是容易理解的,那么下面繼續(xù)分析下 lazy 和 filename 的具體使用場(chǎng)景。我們知道,在 lazy 階段 webpack-dev-server 不是調(diào)用 compiler.watch 方法,而是等待請(qǐng)求到來(lái)的時(shí)候才會(huì)編譯。源代碼如下:
startWatch: function() { var options = context.options; var compiler = context.compiler; // start watching if(!options.lazy) { var watching = compiler.watch(options.watchOptions, share.handleCompilerCallback); context.watching = watching; //context.watching 得到原樣返回的 Watching 對(duì)象 } else { //如果是 lazy,表示我們不是 watching 監(jiān)聽,而是請(qǐng)求的時(shí)候才編譯 context.state = true; } }
調(diào)用 rebuild 的時(shí)候會(huì)判斷 context.state。每次重新編譯后在 compiler.done 中會(huì)將 context.state 重置為 true!
rebuild: function rebuild() { //如果沒有通過 compiler.done 產(chǎn)生過 Stats 對(duì)象,那么設(shè)置 forceRebuild 為 true //如果已經(jīng)有 Stats 表明以前 build 過,那么調(diào)用 run 方法 if(context.state) { context.state = false; //lazy 狀態(tài)下 context.state 為 true,重新 rebuild context.compiler.run(share.handleCompilerCallback); } else { context.forceRebuild = true; } },
下面是當(dāng)請(qǐng)求到來(lái)的時(shí)候我們調(diào)用上面的 rebuild 繼續(xù)重新編譯:
handleRequest: function(filename, processRequest, req) { // in lazy mode, rebuild on bundle request if(context.options.lazy && (!context.options.filename || context.options.filename.test(filename))) share.rebuild(); //如果 filename 里面有 hash,那么通過 fs 從內(nèi)存中讀取文件名,同時(shí)回調(diào)就是直接發(fā)送消息到客戶端!!! if(HASH_REGEXP.test(filename)) { try { if(context.fs.statSync(filename).isFile()) { processRequest(); return; } } catch(e) { } } share.ready(processRequest, req); //回調(diào)函數(shù)將文件結(jié)果發(fā)送到客戶端 },
其中 processRequest 就是直接把編譯好的資源發(fā)送到客戶端:
function processRequest() { try { var stat = context.fs.statSync(filename); //獲取文件名 if(!stat.isFile()) { if(stat.isDirectory()) { filename = pathJoin(filename, context.options.index || "index.html"); //文件名 stat = context.fs.statSync(filename); if(!stat.isFile()) throw "next"; } else { throw "next"; } } } catch(e) { return goNext(); } // server content // 直接訪問的是文件那么讀取,如果是文件夾那么要訪問文件夾 var content = context.fs.readFileSync(filename); content = shared.handleRangeHeaders(content, req, res); res.setHeader("Access-Control-Allow-Origin", "*"); // To support XHR, etc. res.setHeader("Content-Type", mime.lookup(filename) + "; charset=UTF-8"); res.setHeader("Content-Length", content.length); if(context.options.headers) { for(var name in context.options.headers) { res.setHeader(name, context.options.headers[name]); } } // Express automatically sets the statusCode to 200, but not all servers do (Koa). res.statusCode = res.statusCode || 200; if(res.send) res.send(content); else res.end(content); } }
所以,在 lazy 模式下如果我們沒有指定文件名 filename,即每次請(qǐng)求的是那個(gè) Webpack 輸出文件(chunk),那么每次都是會(huì)重新 rebuild 的!但是如果指定了文件名,那么只有訪問該文件名的時(shí)候才會(huì) rebuild!
感謝各位的閱讀,以上就是“webpack-dev-server的核心概念以及熱加載”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)webpack-dev-server的核心概念以及熱加載這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。