Webpack 的 ContentBase vs publicPath vs output.pathcss
webpack-dev-server 會使用當前的路徑做爲請求的資源路徑(所謂html
當前的路徑前端
就是運行 webpack-dev-server 這個命令的路徑,若是對 webpack-dev-server 進行了包裝,好比 wcf,那麼當前路徑指的就是運行 wcf 命令的路徑,通常是項目的根路徑),可是讀者能夠經過指定 content-base 來修改這個默認行爲:node
webpack-dev-server --content-base build/
這樣 webpack-dev-server 就會使用 build 目錄下的資源來處理靜態資源的請求,如 css/ 圖片等。content-base 通常不要和 publicPath、output.path 混淆掉。其中 content-base 表示靜態資源的路徑是什麼,好比下面的例子:react
<!DOCTYPE html> <html> <head> <title></title> <link rel="stylesheet" type="text/css" href="index.css"> </head> <body> <div id="react-content">這裏要插入 js 內容</div> </body> </html>
在做爲 html-webpack-plugin 的 template 之後,那麼上面的 index.css 路徑究竟是什麼?是相對於誰來講?上面已經強調了:若是在沒有指定 content-base 的狀況下就是相對於當前路徑來講的,所謂的當前路徑就是在運行 webpack-dev-server 目錄來講的,因此假如在項目根路徑運行了這個命令,那麼就要保證在項目根路徑下存在該 index.css 資源,不然就會存在 html-webpack-plugin 的 404 報錯。固然,爲了解決這個問題,能夠將 content-base 修改成和 html-webpack-plugin的html 模板同樣的目錄。webpack
上面講到 content-base 只是和靜態資源的請求有關,那麼咱們將其 publicPath 和 output.path 作一個區分。
首先:假如將 output.path 設置爲build(這裏的 build 和 content-base 的 build 沒有任何關係,請不要混淆),要知道 webpack-dev-server 實際上並無將這些打包好的 bundle 寫到這個目錄下,而是存在於內存中的,可是咱們能夠假設(注意這裏是假設)其是寫到這個目錄下的。
而後:這些打包好的 bundle 在被請求的時候,其路徑是相對於配置的publicPath來講的,publicPath 至關於虛擬路徑,其映射於指定的output.path。假如指定的 publicPath 爲 "/assets/",並且 output.path 爲 "build",那麼至關於虛擬路徑 "/assets/" 對應於 "build"(前者和後者指向的是同一個位置),而若是 build 下有一個 "index.css",那麼經過虛擬路徑訪問就是/assets/index.css。
最後:若是某一個內存路徑(文件寫在內存中)已經存在特定的 bundle,並且編譯後內存中有新的資源,那麼咱們也會使用新的內存中的資源來處理該請求,而不是使用舊的 bundle!好比有一個以下的配置:git
module.exports = { entry: { app: ["./app/main.js"] }, output: { path: path.resolve(__dirname, "build"), publicPath: "/assets/", //此時至關於/assets/路徑對應於 build 目錄,是一個映射的關係 filename: "bundle.js" } }
那麼咱們要訪問編譯後的資源能夠經過 localhost:8080/assets/bundle.js 來訪問。若是在 build 目錄下有一個 html 文件,那麼可使用下面的方式來訪問 js 資源:github
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <script src="assets/bundle.js"></script> </body> </html>
此時會看到控制檯輸出以下內容:web
enter image description hereexpress
主要關注下面兩句輸出:
Webpack result is served from /assets/ Content is served from /users/…./build
之因此是這樣的輸出結果是由於設置了 contentBase 爲 build,由於運行的命令爲webpack-dev-server --content-base build/
。因此,通常狀況下:若是在 html 模板中不存在對外部相對資源的引用,咱們並不須要指定 content-base,可是若是存在對外部相對資源 css/ 圖片的引用,能夠經過指定 content-base 來設置默認靜態資源加載的路徑,除非全部的靜態資源所有在當前目錄下。
爲 webpack-dev-server 開啓 HMR 模式只須要在命令行中添加--hot,它會將 HotModuleReplacementPlugin 這個插件添加到 webpack 的配置中去,因此開啓 HotModuleReplacementPlugin 最簡單的方式就是使用 inline 模式。在 inline 模式下,只須要在命令行中添加--inline --hot就能夠自動實現。
這時候 webpack-dev-server 就會自動添加 webpack/hot/dev-server 入口文件到配置中,只是須要訪問下面的路徑就能夠了 http://«host»:«port»/«path»。在控制檯中能夠看到以下的內容
其中以 [HMR] 開頭的部分來自於 webpack/hot/dev-server 模塊,而以[WDS]開頭的部分來自於 webpack-dev-server 的客戶端。下面的部分來自於 webpack-dev-server/client/index.js 內容,其中的 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] 開頭的(它是來自於 Webpack 自己的一個 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 功能呢?此時須要修改三處配置文件:
1.添加一個 Webpack 的入口點,也就是 webpack/hot/dev-server
2.添加一個 new webpack.HotModuleReplacementPlugin() 到 webpack 的配置中
3.添加 hot:true 到 webpack-dev-server 配置中,從而在服務端啓動 HMR(能夠在 cli 中使用 webpack-dev-server --hot)
好比下面的代碼就展現了 webpack-dev-server 爲了實現 HMR 是如何處理入口文件的:
if(options.inline) { var devClient = [require.resolve("../client/") + "?" + protocol + "://" + (options.public || (options.host + ":" + options.port))]; //將 webpack-dev-server 的客戶端入口添加到的 bundle 中,從而達到自動刷新 if(options.hot) devClient.push("webpack/hot/dev-server"); //這裏是 webpack-dev-server 中對 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); } }); }
知足上面三個條件的 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 的服務端) var compiler = webpack(config); var server = new webpackDevServer(compiler, { hot: true //條件二(--hot 配置,webpack-dev-server 會自動添加 HotModuleReplacementPlugin) ... }); server.listen(8080);
webpack-dev-server 使用
去把請求代理到一個外部的服務器,配置的樣例以下:
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 } ]
這種代理在不少狀況下是很重要的,好比能夠把一些靜態文件經過本地的服務器加載,而一些 API 請求所有經過一個遠程的服務器來完成。還有一個情景就是在兩個獨立的服務器之間進行請求分割,如一個服務器負責受權而另一個服務器負責應用自己。下面給出平常開發中遇到的一個例子:
(1)有一個請求是經過相對路徑來完成的,好比地址是 "/msg/show.htm"。可是,在平常和生產環境下前面會加上不一樣的域名,如平常是 you.test.com 而生產環境是 you.inc.com。
(2)那麼好比如今想在本地啓動一個 webpack-dev-server,而後經過 webpack-dev-server 來訪問平常的服務器,並且平常的服務器地址是 11.160.119.131,因此會經過以下的配置來完成:
devServer: { port: 8000, proxy: { "/msg/show.htm": { target: "http://11.160.119.131/", secure: false } } }
此時當請求 "/msg/show.htm" 的時候,其實請求的真實 URL 地址爲 "http//11.160.119.131/msg/show.htm"。
(3)在開發環境中遇到一個問題,那就是:若是本地的 devServer 啓動的地址爲: "http://30.11.160.255:8000/" 或者常見的 "http://0.0.0.0:8000/" ,那麼真實的服務器會返回一個 URL 要求登陸,可是,將本地 devServer 啓動到 localhost 上就不存在這個問題了(一個可能的緣由在於 localhost 種上了後端須要的 cookie,而其餘的域名沒有種上 cookie,致使代理服務器訪問平常服務器的時候沒有相應的 cookie,從而要求權限驗證)。其中指定 localhost 的方式能夠經過
來完成,由於 wcf 默承認以支持 IP 或者 localhost 方式來訪問。固然也能夠經過添加下面的代碼來完成:
devServer: { port: 8000, host:'localhost', proxy: { "/msg/show.htm": { target: "http://11.160.119.131/", secure: false } } }
(4)關於 webpack-dev-server 的原理,讀者能夠查看「反向代理爲什麼叫反向代理」等資料來了解,其實正向代理和反向代理用一句話來歸納就是:「正向代理隱藏了真實的客戶端,而反向代理隱藏了真實的服務器」。而 webpack-dev-server 其實扮演了一個代理服務器的角色,服務器之間通訊不會存在前端常見的同源策略,這樣當請求 webpack-dev-server 的時候,它會從真實的服務器中請求數據,而後將數據發送給你的瀏覽器。
browser => localhost:8080(webpack-dev-server無代理) => http://you.test.com browser => localhost:8080(webpack-dev-server有代理) => http://you.test.com
上面的第一種狀況就是沒有代理的狀況,在 localhost:8080 的頁面經過前端策略去訪問 http://you.test.com 會存在同源策略,即第二步是經過前端策略去訪問另一個地址的。可是對於第二種狀況,第二步實際上是經過代理去完成的,即服務器之間的通訊,不存在同源策略問題。而咱們變成了直接訪問代理服務器,代理服務器返回一個頁面,對於頁面中某些知足特定條件前端請求(proxy、rewrite配置)所有由代理服務器來完成,這樣同源問題就經過代理服務器的方式獲得瞭解決。
(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 地址的效果是徹底一致的。總結一句話:「target 指定了知足特定 URL 的請求應該對應到哪臺主機上,即代理服務器應該訪問的真實主機地址」。
其實 proxy 還能夠經過配置一個 bypass() 函數的返回值視狀況繞開一個代理。這個函數能夠查看 HTTP 請求和響應及一些代理的選項。它返回要麼是 false 要麼是一個 URL 的 path,這個 path 將會用於處理請求而不是使用原來代理的方式完成。下面例子的配置將會忽略來自於瀏覽器的 HTTP 請求,它和 historyApiFallback 配置相似。瀏覽器請求能夠像往常同樣接收到 html 文件,可是 API 請求將會被代理到另外的服務器:
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'; } } } }
對於代理的請求也能夠經過提供一個函數來重寫,這個函數能夠查看或者改變 HTTP 請求。下面的例子就會重寫 HTTP 請求,其主要做用就是移除 URL 前面的 /api 部分。
proxy: { '/api': { target: 'https://other-server.example.com', pathRewrite: {'^/api' : ''} } }
其中 pathRewrite 配置來自於 http-proxy-middleware。更多配置能夠查看
當使用 HTML 5 的 history API 的時候,當 404 出現的時候可能但願使用 index.html 來做爲請求的資源,這時候可使用這個配置 :historyApiFallback:true。然而,若是修改了 output.publicPath,就須要指定重定向的 URL,可使用 historyApiFallback.index 選項。
// output.publicPath: '/foo-app/' historyApiFallback: { index: '/foo-app/' }
使用 rewrite 選項能夠從新設置靜態資源
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 來知足一個需求,即若是一個資源請求包含一個
.
符號,那麼表示是對某一個特定資源的請求,也就知足 dotRule。咱們看看
connect-history-api-fallback 內部是如何處理的:
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(); };
也就是說,若是是對絕對資源的請求,也就是知足 dotRule,可是 disableDotRule(disable dot rule file request)爲 false,表示咱們會本身對知足 dotRule 的資源進行處理,因此不用定向到 index.html 中!若是 disableDotRule 爲 true 表示不會對知足 dotRule 的資源進行處理,因此直接定向到 index.html!
history({ disableDotRule: true })
var server = new WebpackDevServer(compiler, { contentBase: "/path/to/directory", //content-base 配置 hot: true, //開啓 HMR,由 webpack-dev-server 發送 "webpackHotUpdate" 消息到客戶端代碼 historyApiFallback: false, //單頁應用 404 轉向 index.html compress: true, //開啓資源的 gzip 壓縮 proxy: { "**": "http://localhost:9090" }, //代理配置,來源於 http-proxy-middleware setup: function(app) { //webpack-dev-server 自己是 Express 服務器能夠添加本身的路由 // app.get('/some/path', function(req, res) { // res.json({ custom: 'response' }); // }); }, //爲 Express 服務器的 express.static 方法配置參數 http://expressjs.com/en/4x/api.html#express.static staticOptions: { }, //在 inline 模式下用於控制在瀏覽器中打印的 log 級別,如`error`, `warning`, `info` or `none`. clientLogLevel: "info", //不在控制檯打印任何 log quiet: false, //不輸出啓動 log noInfo: false, //webpack 不監聽文件的變化,每次請求來的時候從新編譯 lazy: true, //文件名稱 filename: "bundle.js", //webpack 的 watch 配置,每隔多少秒檢查文件的變化 watchOptions: { aggregateTimeout: 300, poll: 1000 }, //output.path 的虛擬路徑映射 publicPath: "/assets/", //設置自定義 http 頭 headers: { "X-Custom-Header": "yes" }, //打包狀態信息輸出配置 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 外都是容易理解的,那麼下面繼續分析下 lazy 和 filename 的具體使用場景。咱們知道,在 lazy 階段 webpack-dev-server 不是調用 compiler.watch 方法,而是等待請求到來的時候纔會編譯。源代碼以下:
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 對象 } else { //若是是 lazy,表示咱們不是 watching 監聽,而是請求的時候才編譯 context.state = true; } }
調用 rebuild 的時候會判斷 context.state。每次從新編譯後在 compiler.done 中會將 context.state 重置爲 true!
rebuild: function rebuild() { //若是沒有經過 compiler.done 產生過 Stats 對象,那麼設置 forceRebuild 爲 true //若是已經有 Stats 代表之前 build 過,那麼調用 run 方法 if(context.state) { context.state = false; //lazy 狀態下 context.state 爲 true,從新 rebuild context.compiler.run(share.handleCompilerCallback); } else { context.forceRebuild = true; } },
下面是當請求到來的時候咱們調用上面的 rebuild 繼續從新編譯:
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 從內存中讀取文件名,同時回調就是直接發送消息到客戶端!!! if(HASH_REGEXP.test(filename)) { try { if(context.fs.statSync(filename).isFile()) { processRequest(); return; } } catch(e) { } } share.ready(processRequest, req); //回調函數將文件結果發送到客戶端 },
其中 processRequest 就是直接把編譯好的資源發送到客戶端:
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,即每次請求的是那個 Webpack 輸出文件(chunk),那麼每次都是會從新 rebuild 的!可是若是指定了文件名,那麼只有訪問該文件名的時候纔會 rebuild!
做者:Dabao123 連接:https://www.jianshu.com/p/e547fb9747e0 來源:簡書 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。