资讯专栏INFORMATION COLUMN

手把手深入理解 webpack dev middleware 原理與相關 plugins

gitmilk / 2097人阅读

摘要:的架構設計促使第三方開發者讓核心發揮出無限的潛力。當然建置比起開發是較進階的議題,因為我們必須要理解內部的一些事件。這個編譯結果包含的訊息包含模組的狀態,編譯後的資源檔,發生異動的檔案,被觀察的相依套件等。

本文將對 webpack 周邊的 middleware 與 plugin 套件等作些介紹,若您對於 webpack 還不了解可以參考這篇彙整的翻譯。

webpack dev server 是什麼?

webpack dev server 是一個開發伺服器,內建 webpack 使用的 live reloading 功能。

那 webpack dev middleware 是啥?

它就是一個用來組織包裝 webpack 使其可以變成中介軟體,或稱中間件的容器。回想一下 express 你大概可以明白關於 middleware 的用途,就是在輸入到輸出的過程中 加工 的一種手段。單純說 middleware 的話我們可以想成一系列任務, 動作(actions stack),不只 express 有,在 Ruby 中的 rake 也具備這種機制。

先看看web dev server的說明

The webpack-dev-server is a little node.js Express server, which uses the webpack-dev-middleware to serve a webpack bundle.

從頭說起的話就是 webpack 本身只負責打包編譯的功能 bundle, webpack-dev-server 當然就是協助我們開發的伺服器,這個伺服器底層是靠 express 來實作的,接著思考一下我們要如何更新(live reload)呢? 當然是需要取得 webpack 編好的資料啊,於是就需要在從 requestresponse 的過程中透過 express 的 middleware 取得資料,而方法就是透過 webpack-dev-middleware 。

比起直接編譯成檔案,webpack-dev-middleware 這個套件還多了一些好處:

不需要一直寫入磁碟,所有產生的結果會直接存在記憶體

在監視模式(watch mode)下如果檔案發生異動,middleware 會馬上停止提供舊版的 bundle 並且會延遲請求的回應直到編譯完成,如此一來我們就不需要去觀察編譯是否結束了

webpack hot middleware 是什麼?

我們都知道 webpack dev server 有提供一種Hot Module Replacement/Hot Reloading 熱替換的功能。在一般 webpack-dev-server 的時候我們會在 webpack.config.js 加入 new webpack.HotModuleReplacementPlugin() 或設定來啟用。

webpack hot middleware 是給 webpack-dev-middleware 用的。就是讓我們在一般的 server 上加上熱替換的功能,總結來說就是 webpack-dev-middleware + webpack-hot-middleware 即可讓我們用 express 客製一個有熱替換功能的 webpack 開發伺服器。

使用 webpack-dev-server 當中介軟體

webpack 提供了 express 的 middleware 讓我們可以處理一些靜態資源檔而不是使用 express.static。要達成這項功能,我們需要安裝 webpack-dev-middlewarewebpack-hot-middleware

$ npm i webpack express webpack-dev-middleware webpack-hot-middleware -D

安裝完成套件之後,首先我們需要設定一個 webpack.dev.config.js 檔案,並且在 entry 中加上 webpack/hot/dev-serverwebpack-hot-middleware/client

entry: [
  "webpack/hot/dev-server",
  "webpack-hot-middleware/client",
  "client/index.js"
]

這個 webpack.config 主要是給開發伺服器用的,由於這時的匯出都會存在記憶體中,因此 path 可以直接設為根

output: {
  path: "/",
  publicPath: "http://localhost:8080/scripts/",
  filename: "bundle.js"
}

最後補上任何您所需要的 loaders,最重要的是記得。

plugins: [
  new webpack.HotModuleReplacementPlugin()
]

接著下來我們開始來撰寫這個開發環境的設定檔和 express 程式。

我們會匯入 webpack,webpack-dev-middleware, webpack-hot-middleware 和 express。

若需要搭配樣板引擎請自行安裝 ejs 或 jade

var express = require("express")
var webpack = require("webpack")
var WebpackDevMiddleware = require("webpack-dev-middleware")
var WebpackHotMiddleware = require("webpack-hot-middleware")

載入套件之後,使用 express 建立一個 http 應用程式與路由

app = express()
router = express.Router()

router.get("/", MainController)
app.use(router)

上面只是一個一般的 Server 應用,為了達成 webpack 的神奇黑魔法我們需要匯入 webpack 的設定

var config = require("./webpack.dev.config")

webpack 的角色就是我們的編譯器,透過下面的程式碼建立編譯器的 instance

var compiler = webpack(config)

重點來了,我們有了伺服器 express,有了編譯核心 webpack,接著我們需要 wrapper 來打包 webpack 將其合進 express 的 middleware stack 中。

app.use(WebpackDevMiddleware(compiler, {
  publicPath: config.output.publicPath,
  stats: { colors: true }
}))

publicPath 就是我們想要存取前端 bundle 的網址,路徑,位置。然後我們要再加上 webpack-hot-middleware 使其具備熱替換的功能。

app.use(WebpackHotMiddleware(compiler, {
  log: console.log
}))

最後則是 express 的監聽事件

app.listen(8080, function () {
  console.log("Listening on 8080")
})

完整的 server 程式碼如下

var express = require("express")
var webpack = require("webpack")
var WebpackDevMiddleware = require("webpack-dev-middleware")
var WebpackHotMiddleware = require("webpack-hot-middleware")
var config = require("./config/webpack.dev.config")
var compiler = webpack(config)

app = express()
app.set("views", "./views")
app.set("view engine", "ejs")
app.use(express.static("public"));

app.use(WebpackDevMiddleware(compiler, {
  publicPath: config.output.publicPath,
  stats: { colors: true }
}))
app.use(WebpackHotMiddleware(compiler))

var router = express.Router()
router.get("/", function (req, res, next) {
  res.render("index", { message: "Hey there!"});
})
app.use(router)

app.listen(8080, function () {
  console.log("Listening on 8080")
})
換個思路

假設我們並不是要實作一個全站 SPA 的站,實務上我們的確會遇到需要拆分為許多 view .html 的狀況,這種情況下我們會希望自己客製的這個 server 就像 webpack-dev-server 一樣,當然,這邊只是要指出做法,如果一樣您當然就直接用 webpack-dev-server 就好了。

根據上面這個需求最簡單的方式就是透過 express.static(__dirname) 讓 express 直接 return raw 檔案。

html-webpack-plugin

小弟認為在學習的過程中,最重要的就是搞懂動機,而這個 html-webpack-plugin 插件,其用途就是簡化建立 html 的過程。

先回頭看看上一小節,很直覺的,我們會依據需求建立不同的頁面(.html),因為在開發過程中很多時候前端只需要注重那些互動介面的邏輯樣式樣板標籤結構 ,那我們的重點只有 client 端的 html, js, css 就不在話下了吧!再如果我們又以元件為思路中心來設計實踐的話,那麼 html 裡面大部分的東西都會往元件的 template 搬。依據 SPA 的思路,html 的責任就只是把我們的 bundle 載入並掛載 root component

如果照著這樣的想法,不斷的新增 html 結果大部分的內容都是重複的那就不太靠譜啦。我們就需要一種簡化工作的方式。

這個套件如上面所說就是簡化建立載入 bundle 的 html的步驟,用在 webpack 打包的檔案包含每次編譯都會更新的 hash 時特方便。

我們可以讓套件幫我們產生 html 或者搭配 loaders 與其他樣版引擎。

基本的用法

第一種最簡單的用途就是為我們的 bundle 包上一層 html

plugins: [
  new HtmlWebpackPlugin({
    filename: "i_love_this_file.html"
  })
]

如果我們有多個 entry 進入點,那麼所有的 bundle 都會被加進這個自動產生的 HTML 中。
如果我們透過 webpack 匯出了 css 資源檔(例如 extract-text-plugin) 那麼這些檔案也會透過 被加入 HTML 中。

html-webpack-plugin 的設定

當然這個套件也有一些參數,讓我們可以透過設定提供其他的功能。

title: 設定該 html 的 </b> 標籤</p></p> <p><p><b>filename</b>: html 檔名,也當作路徑存取。預設是 <b>index.html</b></p></p> <p><p><b>template</b>: 樣板的路徑,也就是說我們可以先組織 HTML 在載入讓 <b>html-webpack-plugin</b> 幫我們注入(inject) bundle。此部分要注意相對路徑是從 server 程式檔案出發。</p></p> <p> <p><b>inject</b>: 將所有的資源檔注入 <b>template</b> 或 <b>templateContent</b>,當值是 <b>true</b>, <b>"body"</b> 的時候所有的 js 資源檔都會被注入 <b></body></b> 之前,<b>"head"</b> 則是 <b><head></b> 之間,<b>false</b> 自然就是關閉</p> <p><p>true: Boolean</p></p> <p><p>false: Boolean</p></p> <p><p>head: String</p></p> <p><p>body: String</p></p> </p> <p><p><b>favicon</b>: 替 HTML 加上 favicon 路徑</p></p> <p> <p><b>minify</b>: 傳入 html-minifier 參數物件,壓縮輸出。</p> <p><p>options: Object</p></p> <p><p>false: Boolean</p></p> </p> <p> <p><b>hash</b>: <b>true</b> 時替 webpack 編譯的檔案或結果路徑結尾補上 hash,這麼做的用意是在開發時期當檔案有異動時可以避免瀏覽器快取</p> <p><p>true: Boolean</p></p> <p><p>false: Boolean</p></p> </p> <p> <p><b>cache</b>: 預設是 <b>true</b> 快取檔案,除非檔案有異動</p> <p><p>true: Boolean</p></p> <p><p>false: Boolean</p></p> </p> <p> <p><b>showErrors</b>: 預設 <b>true</b> 例外或錯誤資訊會寫入 html 頁面</p> <p><p>true: Boolean</p></p> <p><p>false: Boolean</p></p> </p> <p><p><b>chunks</b>: 允許我們加入一些程式碼片段,例如單元測試</p></p> <p> <p><b>chunksSortMode</b>: 控制 chunks 排序</p> <p><p>none: String</p></p> <p><p>auto: String</p></p> <p><p>dependency: String</p></p> <p><p>{}: Function</p></p> </p> <p><p><b>excludeChunks</b>: 略過部分 chunk 程式碼片段</p></p> <p> <p><b>xhtml</b>: 設定為 <b>true</b> 的話 <b>link</b> 標籤會是 self-closing ,預設是 <b>false</b></p> <p><p>true: Boolean</p></p> <p><p>false: Boolean</p></p> </p> <b>腦力激盪 - 如果要多個頁面搭配各自的 bundle?</b> <p>webapck 難就難在其靈活之中伴隨著複雜,不同的思路有著不同的做法。這一小節目的是為了不讓我們對 webpack 使用上僵化而提出的一個小題目。</p> <p>要達成這個需求,我們可以先使用 webpack.config 中 <b>[name]</b> 的功能拆分我們的 bundle</p> <pre>{ entry: { a: "./path/src/a", b: "./path/src/b", c: "./path/src/c" }, output: { filename: "[name].bundle.js" } }</pre> <p>接著透過 <b>html-webpack-plugin</b> 的參數,把 <b>inject: false</b> 然後 <b>template</b> 在各自的 template 中使用 bundle。</p> <b> html-webpack-template - 更牛的方式</b> <p>照著上面的方式你可能又跟我抱怨,那不是又要產一堆 HTML 了嗎? 對啊!原本這個架構就是針對 SPA 設計的嘛。不過透過這樣來來回回的思考動機與流程我相信對於您日後使用 webpack 與閱讀設定有很大的幫助。現在的問題是 - 你覺得產一大堆 HTML 不是很靠譜,於是我們就有了 <b>html-webpack-template</b> 的產生啦。</p> <p>這個東西大略的用法就是</p> <pre>plugins: [ new HtmlWebpackPlugin({ title: "Sample", filename: "sample.html" }), new HtmlWebpackPlugin({ inject: false, // 必須 template: require("html-webpack-template"), // 必須 filename: "sp.html", // 存取的路徑 // 只需要特定 bundle 可以這樣設定 chunks: ["vender"], title: "OH My Gosh", // 可以參考 html-webpack-template 的參數設定 // 下面為提供 GA googleAnalytics: { trackingId: "UA-XXXX-XX", pageViewOnLoad: true } }) ]</pre> <b>html-webpack-plugin 事件</b> <p>特地介紹此套件的事件也是因為挺有可能會需要一些時間點對 html 動些手腳,有了事件的機制我們就可以讓<b>其他套件</b>修改產生的 html</p> <p>非同步事件:</p> <p><p><b>html-webpack-plugin-before-html-generation</b></p></p> <p><p><b>html-webpack-plugin-before-html-processing</b></p></p> <p><p><b>html-webpack-plugin-after-html-processing</b></p></p> <p><p><b>html-webpack-plugin-after-emit</b></p></p> <p>同步事件:</p> <p><p><b>html-webpack-plugin-alter-chunks</b></p></p> <p>大略的用法就是在透過 hook event 綁定的事件做些處理</p> <pre>compiler.plugin("compilation", function(compilation) { console.log("The compiler is starting a new compilation..."); compilation.plugin("html-webpack-plugin-before-html-processing", function(htmlPluginData, callback) { htmlPluginData.html += "The magic footer"; callback(null, htmlPluginData); }); });</pre> <b>webpack-hot-middleware</b> <p><b>webpack-hot-middleware</b> 這個套件只能搭配 <b>webpack-dev-middleware</b> 使用,其實就是把熱替換的功能加到一般 server 應用。</p> <p>這個模組只專注在處理 webpack 和瀏覽器溝通的機制。這個中介軟體會去訂閱監聽開發伺服器,當更新或異動發生的時候它就透過 webpack 的 HMR API 來更新。實際上讓您的程式能無縫的使用熱替換已超過本文範圍,在這部分通常會靠其他模組來處理。</p> <p>安裝完套件與在伺服器 app 中套用之外,要記得 webpack.config 的 plugin 也要加上 <b>HotModuleReplacementPlugin</b></p> <pre>plugins: [ // Webpack 1.0 new webpack.optimize.OccurenceOrderPlugin(), // Webpack 2.0 fixed this mispelling // new webpack.optimize.OccurrenceOrderPlugin(), new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ]</pre> <p>簡短地介紹一下 <b>OccurrenceOrderPlugin</b> 部份,您應該知道 webapck 會給編譯好的程式碼片段一個 id 以用來辨別。</p> <p>透過上面的這個 plugin 可以讓 webpack 在 id 的分派上優化並保持一致性。</p> <p>接著要在 entry point 加上 <b>webpack-hot-middleware/client</b> 這隻檔案會連到 server 目的是當 server 重新編譯好檔案時收到通知然後更新 client 的檔案。</p> <b>如何撰寫 plugin</b> <p>為什麼要了解怎麼寫 plugin 呢? 因為某些 plugin 可以擴展支援其他 plugin 相互傳遞資料或需要客製後續任務,所以稍微明白 plugin 的寫法可以讓我們對於 plugin 的設定更加清楚。</p> <p>plugin 的架構設計促使第三方開發者讓 webpack 核心發揮出無限的潛力。在不同建置階段執行 callback ,開發者可以自訂出特有的行為。</p> <p>當然建置 plugin 比起開發 loader 是較進階的議題,因為我們必須要理解 webpack 內部的一些 hook 事件。</p> <h5>編譯器與編譯結果</h5> <p>要開發 plugin 第一步就是先了解其中最重要的兩個角色 <b>compiler</b> 和 <b>compilation</b> 物件</p> <p><p><b>compiler</b> 編譯器物件代表一個完整設定的 webpack 環境。這個物件在 webpack 發動之後就會被建置,而且只會建置一次。然後它會配置所有可以操作的設定包含 <b>loaders</b>, <b>plugins</b>。當我們套用一個 plugin 這個 plugin 會收到 <b>compiler</b> 的參考透過存取這個<b>參考 reference</b>就可以取得 webpack 環境</p></p> <p><p><b>compilation</b> 編譯成果這個物件代表的是<b>某個版本的編譯後的資源檔</b>,在運行 webpack dev middleware 期間每當檔案發生異動就會產生一個新的 <b>compilation</b> 也就是產生新的編譯結果。這個<b>編譯結果</b>包含的訊息包含 module 模組的狀態,編譯後的資源檔,發生異動的檔案,被觀察的相依套件等。這個編譯結果物件也提供一些執行 callback 的機會讓我們可以在過程中客製一些自己想要的行為。</p></p> <p>任何 webpack plugin 都必須依靠這兩者來完成,所以有需要對其原始碼有些大概的了解</p> <p><p>Compiler Source</p></p> <p><p>Compilation Source</p></p> <b>基本 plugin 架構</b> <p>本質上來說 plugin 只是一個物件實例具有 apply 方法,這個 <b>apply</b> 會在安裝時期被 <b>webpack compiler</b> 執行一次。</p> <p>透過這一次的執行呢我們就可以繫結許多事件,直接來看看程式碼您就明白了。</p> <pre>function MyPlugin(options) { // 設定參數 this.options = options } MyPlugin.prototype.apply = function(compiler) { compiler.plugin("done", function() { // 當 plugin 安裝完成就會... console.log("Hello World!"); }) // 我們自然需要拿到編譯的結果 compiler.plugin("compilation", function(compilation) { console.log(compilation.assets) compilation.plugin("optimize", function() { console.log("Assets are being optimized."); }); }); }</pre> <p>OK! 我們現在並不是要開發套件所以點到這邊我想就足夠了,剩下的您可以自行參考相關文件。</p> <p><p>詳細 plugin API</p></p> <b>extract-text-webpack-plugin</b> <p>顧名思義這個 plugin 的用途就是把 text 類型的結果匯出成一個檔案,先說這不是非常精確的描述,但概念來說 text 類型指的就是<b>不會</b>輸出成 <b>module.exports</b> 或 <b>json</b> 的資料。而像是 CSS 這類的資源檔 webpack 其實最終就是在 JS 中幫我們建個 style tag 的 dom 然後整包放進去。<b>file-loader</b>, <b>raw-loader</b> 等等這類內容大略就屬於 text 類型。查閱各種 loaders 回傳資料類型</p> <p>於是乎以 entry point 為單位過程中解析的 text 內容就會被抽出來匯出成一個檔案。最常見的用法就是把 css 抽出來:</p> <pre>var ExtractTextPlugin = require("extract-text-webpack-plugin") module.exports = { module: { loaders: [ { test: /.css$/, loader: ExtractTextPlugin.extract("style", "css") } ] }, plugins: [ // 注意: 這邊的副檔名如果亂下是會造成瀏覽器行為不符合預期的,例如不給副檔名那瀏覽器就會當作 binary 下載 new ExtractTextPlugin("styles.css") ] }</pre> <p>如果想要拆分多個檔案,那麼就先初始化 instance</p> <pre>let ExtractTextPlugin = require("extract-text-webpack-plugin"); // multiple extract instances let cssExtractor = new ExtractTextPlugin("stylesheets/[name].css"); let lessExtractor = new ExtractTextPlugin("stylesheets/[name].less"); module.exports = { module: { loaders: [ {test: /.scss$/i, loader: cssExtractor.extract(["css","sass"])}, {test: /.less$/i, loader: lessExtractor.extract(["css","less"])}, ... ] }, plugins: [ cssExtractor, lessExtractor ] }</pre> <b>HMR 熱替換</b> <p>Hot Module Replacement (HRM) 又稱熱替換,功能就是在程式運行中交換,移除,增加模組且不會使頁面重新載入。這跟我們伺服器的熱插拔差不多概念。</p> <h5>它是怎麼運作的?</h5> <p>webpack 在 bundle 中即我們的 js 裡加入了一個小型的 HMR 執行環境,在編譯過程中這個 runtime 會在我們的 app 中運行。</p> <p>當建置完成時 webpack 也不會消失反而會持續存在,繼續監控原始碼檔案是否發生修改。一旦 webpack 發現程式有改變他就會去重新編譯那些有修改的模組,不全部重建。根據設定要嘛就是 webpack 把訊號丟給 HRM runtime 要嘛就是 HRM 自己更新異動資訊。不管哪種方式反正重點就是 HRM runtime 會取得修改的模組,接著就試著在運行的狀態下更新模組。首先會先檢查更新的模組是否能 <b>self-accept</b>。</p> <p>關於 <b>self-accept</b> 先看看範例和原始碼,意思是要<b>支援熱替換的模組或說編譯結果</b>基本上是應該要實作 <b>module.hot.accept</b> 和遵循其他熱替換的規則。</p> <p>如果沒有辦法自己確認自己可以直接被更新,那就往上傳,通知那些 require 匯入使用自己的模組更新,就這樣層層往上。直到有人可以 accept 或到頂,不過一旦到根就表示熱替換失敗。</p> <p>讀到這邊你可能通了,為什麼當我們要讓 React 支援 Hot Mode 的時候需要一個 <b>react-hot-loader</b>。以及因為要和 HRM 執行環境溝通的關係我們需要在 bundle 的 entry point 加上 <b>webpack/hot/dev-server</b>, <b>webpack-hot-middleware/client</b> 之類的東西。</p> <h5>從 App 的角度</h5> <p><script type="text/javascript">showImg("https://segmentfault.com/img/remote/1460000006763873");</script></p> <p>當 App 程式開始執行(就是載入 bundle) HMR runtime 執行環境就會啟用,接下來程式就會要求 HMR runtime 幫我們檢查是否需要更新。HMR 會幫我們下載更新然後通知 App 程式有哪些更新可用。</p> <h5>從編譯器(webpack/compiler)的角度</h5> <p>除了一般的資源檔像是圖片,css,編譯器還需要觸發<b>更新事件</b>讓程式碼可以完成新舊替換。這個"更新"包含兩個部分</p> <p><p>更新的 Manifest 支援配置文件(json)</p></p> <p><p>一或多個更新的<b>chunks</b>程式片段(js)</p></p> <p>支援配置文件包含更新後編譯結果的 hash 和新的 chunks 程式碼片段的列表。而新的 chunks 則包含更新後模組的程式碼或 <b>flag</b>。</p> <p>編譯器同時也會確保模組和片段 ID 是一致的,透過一個 <b>records</b> 的 json 檔案來儲存相關資訊。</p> <h5>從模組角度</h5> <p>HMR 是選擇性的功能,所以只有在模組包含 HRM 程式碼才會被影響作用。也就是在模組中使用文件有提供的 API。一般來說模組的開發者 handler 會在模組相依的部分更新時被執行。當然也可以寫一個 handler 在這個模組更新時被呼叫。</p> <p>在大部分的情況並不需要為每一個模組都撰寫<b>支援 HMR 的程式碼</b>,當一個模組沒有遵循處理規則時就會往上層傳遞事件,意味著只有上方有一個 handler 可以處理就好,但不要讓這個冒泡事件一路冒到頂喔。</p> <h5>從 HMR runtime 角度</h5> <p>模組系統的執行環境其實是額外加入的程式,用來追蹤模組之間的父子關係。</p> <pre>if(module.hot) { ... }</pre> <p>從管理的角度,這個執行環境 runtime 支援 <b>check</b> 和 <b>apply</b> 兩個方法。</p> <p><b>check</b> 的功能是發出 HTTP request 用來取得上面提到的 Manifest,當 request 失敗時就等於沒有任何更新。否則就會依照得到的<b>更新列表</b>去比對 chunks。</p> <p>對每個已載入的 chunk 都會有對應更新的程式碼要被下載。所有模組更新會被存在 runtime 中準備拿來更新。當執行環境切換成 <b>ready</b> 狀態就表示更新的程式碼都被下載完成了隨時可以套用。</p> <p>接著 <b>apply</b> 方法會將所有已更新的模組的 <b>flag</b> 標記為 <b>invalid</b> 無效,然後無效的模組需要 update 的 handler 處理函式,這個 handler 會在模組中或者父節點上。只要沒有這個 handler 就會持續往上曾傳遞並標註為 <b>invalid</b>,一旦冒泡機制冒到頂端即 <b>entry point</b> 就表示熱替換失敗。</p> <p>所有被標記為無效的模組都會透過 <b>module.hot.dispose</b> 卸載,然後更新 hash,再來所有 <b>module.hot.accept</b> 的 handlers 會被調用。</p> <p>執行環境切回 <b>idle</b> 狀態表示所有更新都完成了。</p> <p>講這麼多其實簡單來說就是我們的模組要補一些 hot mode 的邏輯</p> <pre>var app = require("./app"); // 模擬每 5 秒更新一次 setInterval(function() { console.log(app(new Date())); }, 5000); if(module.hot) { module.hot.accept("./app", function() { app = require("./app"); }); }</pre> <h5>檔案的更新流程</h5> <p>左邊表示初始化時編譯器產生的結構,右邊則是當模組 4 和 9 更新時的流程。<br>方塊表示從 Entry 開始,webpack 幫我們編譯產生的部份從 Entry 然後轉換成 Chunk 0 - 4</p> <p><script type="text/javascript">showImg("https://segmentfault.com/img/remote/1460000004840685");</script></p> <b>資源參考</b> <p><p>html-webpack-plugin</p></p> <p><p>webpack dev middleware 說明</p></p> <b>備註</b> <p>部分內容可能理解不夠精確若有錯誤歡迎指教留言</p> </div> <div class="mt-64 tags-seach" > <div class="tags-info"> <a style="width:120px;" title="云服务器" href="https://www.ucloud.cn/site/active/kuaijiesale.html?ytag=seo">云服务器</a> <a style="width:120px;" title="GPU云服务器" href="https://www.ucloud.cn/site/product/gpu.html">GPU云服务器</a> <a style="width:120px;" title="與相關" href="https://www.ucloud.cn/yun/tag/??xiang??/">與相關</a> <a style="width:120px;" title="webpack-dev-server" href="https://www.ucloud.cn/yun/tag/webpack-dev-server/">webpack-dev-server</a> <a style="width:120px;" title="深入浅出webpack" href="https://www.ucloud.cn/yun/tag/shenruqianchuwebpack/">深入浅出webpack</a> <a style="width:120px;" title="深入理解系列" href="https://www.ucloud.cn/yun/tag/shenrulijiexilie/">深入理解系列</a> </div> </div> <div class="entry-copyright mb-30"> <p class="mb-15"> 文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。</p> <p>转载请注明本文地址:https://www.ucloud.cn/yun/79623.html</p> </div> <ul class="pre-next-page"> <li class="ellipsis"><a class="hpf" href="https://www.ucloud.cn/yun/79622.html">上一篇:React开发 获取不到当前触发事件的元素</a></li> <li class="ellipsis"><a class="hpf" href="https://www.ucloud.cn/yun/79624.html">下一篇:zepto.cutphoto 头像裁剪小工具</a></li> </ul> </div> <div class="about_topicone-mid"> <h3 class="top-com-title mb-0"><span data-id="0">相关文章</span></h3> <ul class="com_white-left-mid atricle-list-box"> <li> <div class="atricle-list-right"> <h2 class="ellipsis2"><a class="hpf" href="https://www.ucloud.cn/yun/107860.html"><b>【敲黑板】<em>手<em>把手</em></em>教你vue-cli单页到多页应用</b></a></h2> <p class="ellipsis2 good">摘要:到多页应用前言我有一个创建的项目,但是我想做成多页应用,怎么办,废话不多说,直接开撸约定新增代码部分在和中间删除注释代码部分在和中间,很多东西都写在注释里第一步一个项目新建一个项目官网默认使用的服务,这个服务是做不了单页的,需要手动建一 vue-cli到多页应用 前言:我有一个cli创建的vue项目,但是我想做成多页应用,怎么办,废话不多说,直接开撸~ 约定:新增代码部分在//add和...</p> <div class="com_white-left-info"> <div class="com_white-left-infol"> <a href="https://www.ucloud.cn/yun/u-215.html"><img src="https://www.ucloud.cn/yun/data/avatar/000/00/02/small_000000215.jpg" alt=""><span class="layui-hide64">DC_er</span></a> <time datetime="">2019-08-26 11:56</time> <span><i class="fa fa-commenting"></i>评论0</span> <span><i class="fa fa-star"></i>收藏0</span> </div> </div> </div> </li> <li> <div class="atricle-list-right"> <h2 class="ellipsis2"><a class="hpf" href="https://www.ucloud.cn/yun/90923.html"><b>前端临床手札——<em>webpack</em>构建逐步解构(上)</b></a></h2> <p class="ellipsis2 good">摘要:前言由于博主最近又闲下来了,之前觉得的官方文档比较难啃一直放到现在。文章会逐步分析每个处理的用意当然是博主自己的理解,不足之处欢迎指出沟通交流。后续将会补上构建生产的配置分析,案例参考。前端临床手札构建逐步解构下 前言 由于博主最近又闲下来了,之前觉得webpack的官方文档比较难啃一直放到现在。细心阅读多个webpack配置案例后觉得还是得自己写个手脚架,当然这个案例是基于vue的,...</p> <div class="com_white-left-info"> <div class="com_white-left-infol"> <a href="https://www.ucloud.cn/yun/u-1459.html"><img src="https://www.ucloud.cn/yun/data/avatar/000/00/14/small_000001459.jpg" alt=""><span class="layui-hide64">lowett</span></a> <time datetime="">2019-08-22 10:36</time> <span><i class="fa fa-commenting"></i>评论0</span> <span><i class="fa fa-star"></i>收藏0</span> </div> </div> </div> </li> <li> <div class="atricle-list-right"> <h2 class="ellipsis2"><a class="hpf" href="https://www.ucloud.cn/yun/99190.html"><b><em>webpack</em>4配置详解之逐行分析</b></a></h2> <p class="ellipsis2 good">摘要:今天就尝试着一起来聊聊吧,旨在帮大家加深理解新手更容易上路,都能从到搭建配置自定属于自己的脚手架,或对已封装好的脚手架有进一步的巩固,接下来苏南会详细讲解中的每一个配置字段的作用部分为新增。 showImg(https://segmentfault.com/img/bVbjmMV?w=1008&h=298); 前言   经常会有群友问起webpack、react、redux、甚至cre...</p> <div class="com_white-left-info"> <div class="com_white-left-infol"> <a href="https://www.ucloud.cn/yun/u-1481.html"><img src="https://www.ucloud.cn/yun/data/avatar/000/00/14/small_000001481.jpg" alt=""><span class="layui-hide64">dkzwm</span></a> <time datetime="">2019-08-23 13:04</time> <span><i class="fa fa-commenting"></i>评论0</span> <span><i class="fa fa-star"></i>收藏0</span> </div> </div> </div> </li> <li> <div class="atricle-list-right"> <h2 class="ellipsis2"><a class="hpf" href="https://www.ucloud.cn/yun/102835.html"><b><em>webpack</em>-<em>dev</em>-<em>middleware</em>@1.12.2 源码解读</b></a></h2> <p class="ellipsis2 good">摘要:如果此时我们不想把文件输出到内存里,可以通过修改的源代码来实现。服务启动成功。。。根据请求的,拼接出 ​ webpack-dev-middleware 是express的一个中间件,它的主要作用是以监听模式启动webpack,将webpack编译后的文件输出到内存里,然后将内存的文件输出到epxress服务器上;下面通过一张图片来看一下它的工作原理: showImg(https:...</p> <div class="com_white-left-info"> <div class="com_white-left-infol"> <a href="https://www.ucloud.cn/yun/u-1200.html"><img src="https://www.ucloud.cn/yun/data/avatar/000/00/12/small_000001200.jpg" alt=""><span class="layui-hide64">yearsj</span></a> <time datetime="">2019-08-23 16:15</time> <span><i class="fa fa-commenting"></i>评论0</span> <span><i class="fa fa-star"></i>收藏0</span> </div> </div> </div> </li> <li> <div class="atricle-list-right"> <h2 class="ellipsis2"><a class="hpf" href="https://www.ucloud.cn/yun/96221.html"><b><em>webpack</em>使用记录</b></a></h2> <p class="ellipsis2 good">容易混淆概念解析 读这篇文章理清下面概念 webpack 中那些最易混淆的 5 个知识点 1.module,chunk 和 bundle 的区别是什么?2.filename 和 chunkFilename 的区别 版本区别 webpack 2x entry output loaders file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 url-lo...</p> <div class="com_white-left-info"> <div class="com_white-left-infol"> <a href="https://www.ucloud.cn/yun/u-730.html"><img src="https://www.ucloud.cn/yun/data/avatar/000/00/07/small_000000730.jpg" alt=""><span class="layui-hide64">Sike</span></a> <time datetime="">2019-08-22 18:48</time> <span><i class="fa fa-commenting"></i>评论0</span> <span><i class="fa fa-star"></i>收藏0</span> </div> </div> </div> </li> </ul> </div> <div class="topicone-box-wangeditor"> <h3 class="top-com-title mb-64"><span>发表评论</span></h3> <div class="xcp-publish-main flex_box_zd"> <div class="unlogin-pinglun-box"> <a href="javascript:login()" class="grad">登陆后可评论</a> </div> </div> </div> <div class="site-box-content"> <div class="site-content-title"> <h3 class="top-com-title mb-64"><span>0条评论</span></h3> </div> <div class="pages"></ul></div> </div> </div> <div class="layui-col-md4 layui-col-lg3 com_white-right site-wrap-right"> <div class=""> <div class="com_layuiright-box user-msgbox"> <a href="https://www.ucloud.cn/yun/u-380.html"><img src="https://www.ucloud.cn/yun/data/avatar/000/00/03/small_000000380.jpg" alt=""></a> <h3><a href="https://www.ucloud.cn/yun/u-380.html" rel="nofollow">gitmilk</a></h3> <h6>男<span>|</span>高级讲师</h6> <div class="flex_box_zd user-msgbox-atten"> <a href="javascript:attentto_user(380)" id="attenttouser_380" class="grad follow-btn notfollow attention">我要关注</a> <a href="javascript:login()" title="发私信" >我要私信</a> </div> <div class="user-msgbox-list flex_box_zd"> <h3 class="hpf">TA的文章</h3> <a href="https://www.ucloud.cn/yun/ut-380.html" class="box_hxjz">阅读更多</a> </div> <ul class="user-msgbox-ul"> <li><h3 class="ellipsis"><a href="https://www.ucloud.cn/yun/121948.html">基于javaweb+jsp的医院信息管理系统</a></h3> <p>阅读 2017<span>·</span>2021-10-09 09:41</p></li> <li><h3 class="ellipsis"><a href="https://www.ucloud.cn/yun/121406.html">大龄业余程序员要搞个django小程序--09--小程序登陆状态维护之个人中心如何做数据加载</a></h3> <p>阅读 1596<span>·</span>2021-09-28 09:36</p></li> <li><h3 class="ellipsis"><a href="https://www.ucloud.cn/yun/121126.html">程序员的算法趣题Q39: 反复排序</a></h3> <p>阅读 1099<span>·</span>2021-09-26 09:55</p></li> <li><h3 class="ellipsis"><a href="https://www.ucloud.cn/yun/119841.html">DediPath:美国特价独立服务器,洛杉矶独服月付$39起,全场VPS一律4折优惠</a></h3> <p>阅读 1284<span>·</span>2021-09-10 11:17</p></li> <li><h3 class="ellipsis"><a href="https://www.ucloud.cn/yun/118753.html">GigsGigsCloud:洛杉磯CN2+聯通9929線路VPS限時2折</a></h3> <p>阅读 1139<span>·</span>2021-09-02 09:56</p></li> <li><h3 class="ellipsis"><a href="https://www.ucloud.cn/yun/115209.html">CSS3魔法堂:说说Multi-column Layout</a></h3> <p>阅读 2755<span>·</span>2019-08-30 12:58</p></li> <li><h3 class="ellipsis"><a href="https://www.ucloud.cn/yun/112339.html">js实现鼠标拖拽多选功能</a></h3> <p>阅读 2926<span>·</span>2019-08-29 13:03</p></li> <li><h3 class="ellipsis"><a href="https://www.ucloud.cn/yun/109278.html">摸倚天鱼文章推荐系列 - 19/03/31</a></h3> <p>阅读 1846<span>·</span>2019-08-26 13:40</p></li> </ul> </div> <!-- 文章详情右侧广告--> <div class="com_layuiright-box"> <h6 class="top-com-title"><span>最新活动</span></h6> <div class="com_adbox"> <div class="layui-carousel" id="right-item"> <div carousel-item> <div> <a href="https://www.ucloud.cn/site/active/kuaijiesale.html?ytag=seo" rel="nofollow"> <img src="https://www.ucloud.cn/yun/data/attach/240625/2rTjEHmi.png" alt="云服务器"> </a> </div> <div> <a href="https://www.ucloud.cn/site/product/gpu.html" rel="nofollow"> <img src="https://www.ucloud.cn/yun/data/attach/240807/7NjZjdrd.png" alt="GPU云服务器"> </a> </div> </div> </div> </div> <!-- banner结束 --> <div class="adhtml"> </div> <script> $(function(){ $.ajax({ type: "GET", url:"https://www.ucloud.cn/yun/ad/getad/1.html", cache: false, success: function(text){ $(".adhtml").html(text); } }); }) </script> </div> </div> </div> </div> </div> </section> <!-- wap拉出按钮 --> <div class="site-tree-mobile layui-hide"> <i class="layui-icon layui-icon-spread-left"></i> </div> <!-- wap遮罩层 --> <div class="site-mobile-shade"></div> <!--付费阅读 --> <div id="payread"> <div class="layui-form-item">阅读需要支付1元查看</div> <div class="layui-form-item"><button class="btn-right">支付并查看</button></div> </div> <script> var prei=0; $(".site-seo-depict pre").each(function(){ var html=$(this).html().replace("<code>","").replace("</code>","").replace('<code class="javascript hljs" codemark="1">',''); $(this).attr('data-clipboard-text',html).attr("id","pre"+prei); $(this).html("").append("<code>"+html+"</code>"); prei++; }) $(".site-seo-depict img").each(function(){ if($(this).attr("src").indexOf('data:image/svg+xml')!= -1){ $(this).remove(); } }) $("LINK[href*='style-49037e4d27.css']").remove(); $("LINK[href*='markdown_views-d7a94ec6ab.css']").remove(); layui.use(['jquery', 'layer','code'], function(){ $("pre").attr("class","layui-code"); $("pre").attr("lay-title",""); $("pre").attr("lay-skin",""); layui.code(); $(".layui-code-h3 a").attr("class","copycode").html("复制代码 ").attr("onclick","copycode(this)"); }); function copycode(target){ var id=$(target).parent().parent().attr("id"); var clipboard = new ClipboardJS("#"+id); clipboard.on('success', function(e) { e.clearSelection(); alert("复制成功") }); clipboard.on('error', function(e) { alert("复制失败") }); } //$(".site-seo-depict").html($(".site-seo-depict").html().slice(0, -5)); </script> <link rel="stylesheet" type="text/css" href="https://www.ucloud.cn/yun/static/js/neweditor/code/styles/tomorrow-night-eighties.css"> <script src="https://www.ucloud.cn/yun/static/js/neweditor/code/highlight.pack.js" type="text/javascript"></script> <script src="https://www.ucloud.cn/yun/static/js/clipboard.js"></script> <script>hljs.initHighlightingOnLoad();</script> <script> function setcode(){ var _html=''; document.querySelectorAll('pre code').forEach((block) => { var _tmptext=$.trim($(block).text()); if(_tmptext!=''){ _html=_html+_tmptext; console.log(_html); } }); } </script> <script> function payread(){ layer.open({ type: 1, title:"付费阅读", shadeClose: true, content: $('#payread') }); } // 举报 function jupao_tip(){ layer.open({ type: 1, title:false, shadeClose: true, content: $('#jubao') }); } $(".getcommentlist").click(function(){ var _id=$(this).attr("dataid"); var _tid=$(this).attr("datatid"); $("#articlecommentlist"+_id).toggleClass("hide"); var flag=$("#articlecommentlist"+_id).attr("dataflag"); if(flag==1){ flag=0; }else{ flag=1; //加载评论 loadarticlecommentlist(_id,_tid); } $("#articlecommentlist"+_id).attr("dataflag",flag); }) $(".add-comment-btn").click(function(){ var _id=$(this).attr("dataid"); $(".formcomment"+_id).toggleClass("hide"); }) $(".btn-sendartcomment").click(function(){ var _aid=$(this).attr("dataid"); var _tid=$(this).attr("datatid"); var _content=$.trim($(".commenttext"+_aid).val()); if(_content==''){ alert("评论内容不能为空"); return false; } var touid=$("#btnsendcomment"+_aid).attr("touid"); if(touid==null){ touid=0; } addarticlecomment(_tid,_aid,_content,touid); }) $(".button_agree").click(function(){ var supportobj = $(this); var tid = $(this).attr("id"); $.ajax({ type: "GET", url:"https://www.ucloud.cn/yun/index.php?topic/ajaxhassupport/" + tid, cache: false, success: function(hassupport){ if (hassupport != '1'){ $.ajax({ type: "GET", cache:false, url: "https://www.ucloud.cn/yun/index.php?topic/ajaxaddsupport/" + tid, success: function(comments) { supportobj.find("span").html(comments+"人赞"); } }); }else{ alert("您已经赞过"); } } }); }); function attenquestion(_tid,_rs){ $.ajax({ //提交数据的类型 POST GET type:"POST", //提交的网址 url:"https://www.ucloud.cn/yun/favorite/topicadd.html", //提交的数据 data:{tid:_tid,rs:_rs}, //返回数据的格式 datatype: "json",//"xml", "html", "script", "json", "jsonp", "text". //在请求之前调用的函数 beforeSend:function(){}, //成功返回之后调用的函数 success:function(data){ var data=eval("("+data+")"); console.log(data) if(data.code==2000){ layer.msg(data.msg,function(){ if(data.rs==1){ //取消收藏 $(".layui-layer-tips").attr("data-tips","收藏文章"); $(".layui-layer-tips").html('<i class="fa fa-heart-o"></i>'); } if(data.rs==0){ //收藏成功 $(".layui-layer-tips").attr("data-tips","已收藏文章"); $(".layui-layer-tips").html('<i class="fa fa-heart"></i>') } }) }else{ layer.msg(data.msg) } } , //调用执行后调用的函数 complete: function(XMLHttpRequest, textStatus){ postadopt=true; }, //调用出错执行的函数 error: function(){ //请求出错处理 postadopt=false; } }); } </script> <footer> <div class="layui-container"> <div class="flex_box_zd"> <div class="left-footer"> <h6><a href="https://www.ucloud.cn/"><img src="https://www.ucloud.cn/yun/static/theme/ukd//images/logo.png" alt="UCloud (优刻得科技股份有限公司)"></a></h6> <p>UCloud (优刻得科技股份有限公司)是中立、安全的云计算服务平台,坚持中立,不涉足客户业务领域。公司自主研发IaaS、PaaS、大数据流通平台、AI服务平台等一系列云计算产品,并深入了解互联网、传统企业在不同场景下的业务需求,提供公有云、混合云、私有云、专有云在内的综合性行业解决方案。</p> </div> <div class="right-footer layui-hidemd"> <ul class="flex_box_zd"> <li> <h6>UCloud与云服务</h6> <p><a href="https://www.ucloud.cn/site/about/intro/">公司介绍</a></p> <p><a href="https://zhaopin.ucloud.cn/" >加入我们</a></p> <p><a href="https://www.ucloud.cn/site/ucan/onlineclass/">UCan线上公开课</a></p> <p><a href="https://www.ucloud.cn/site/solutions.html" >行业解决方案</a></p> <p><a href="https://www.ucloud.cn/site/pro-notice/">产品动态</a></p> </li> <li> <h6>友情链接</h6> <p><a href="https://www.compshare.cn/?ytag=seo">GPU算力平台</a></p> <p><a href="https://www.ucloudstack.com/?ytag=seo">UCloud私有云</a></p> <p><a href="https://www.surfercloud.com/">SurferCloud</a></p> <p><a href="https://www.uwin-link.com/">工厂仿真软件</a></p> <p><a href="https://pinex.it/">Pinex</a></p> <p><a href="https://www.picpik.ai/zh">AI绘画</a></p> </li> <li> <h6>社区栏目</h6> <p><a href="https://www.ucloud.cn/yun/column/index.html">专栏文章</a></p> <p><a href="https://www.ucloud.cn/yun/udata/">专题地图</a></p> </li> <li> <h6>常见问题</h6> <p><a href="https://www.ucloud.cn/site/ucsafe/notice.html" >安全中心</a></p> <p><a href="https://www.ucloud.cn/site/about/news/recent/" >新闻动态</a></p> <p><a href="https://www.ucloud.cn/site/about/news/report/">媒体动态</a></p> <p><a href="https://www.ucloud.cn/site/cases.html">客户案例</a></p> <p><a href="https://www.ucloud.cn/site/notice/">公告</a></p> </li> <li> <span><img src="https://static.ucloud.cn/7a4b6983f4b94bcb97380adc5d073865.png" alt="优刻得"></span> <p>扫扫了解更多</p></div> </div> <div class="copyright">Copyright © 2012-2023 UCloud 优刻得科技股份有限公司<i>|</i><a rel="nofollow" href="http://beian.miit.gov.cn/">沪公网安备 31011002000058号</a><i>|</i><a rel="nofollow" href="http://beian.miit.gov.cn/"></a> 沪ICP备12020087号-3</a><i>|</i> <script type="text/javascript" src="https://gyfk12.kuaishang.cn/bs/ks.j?cI=197688&fI=125915" charset="utf-8"></script> <script> var _hmt = _hmt || []; (function() { var hm = document.createElement("script"); hm.src = "https://hm.baidu.com/hm.js?290c2650b305fc9fff0dbdcafe48b59d"; var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(hm, s); })(); </script> <!-- Global site tag (gtag.js) - Google Analytics --> <script async src="https://www.googletagmanager.com/gtag/js?id=G-DZSMXQ3P9N"></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-DZSMXQ3P9N'); </script> <script> (function(){ var el = document.createElement("script"); el.src = "https://lf1-cdn-tos.bytegoofy.com/goofy/ttzz/push.js?99f50ea166557aed914eb4a66a7a70a4709cbb98a54ecb576877d99556fb4bfc3d72cd14f8a76432df3935ab77ec54f830517b3cb210f7fd334f50ccb772134a"; el.id = "ttzz"; var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(el, s); })(window) </script></div> </div> </footer> </body> <script src="https://www.ucloud.cn/yun/static/theme/ukd/js/common.js"></script> <<script type="text/javascript"> $(".site-seo-depict *,.site-content-answer-body *,.site-body-depict *").css("max-width","100%"); </script> </html>