使用webpack有一段時間了,對其中的熱更新的大概理解是:對某個模塊作了修改,頁面只作局部更新而不須要刷新整個頁面來進行更新。這樣就能節省由於整個頁面刷新所產生開銷的時間,模塊熱加載加快了開發的速度。javascript
熱加載的基礎是模塊熱替換(HMR,Hot Module Replacement)。css
具體的是:webpack能夠監控文件的改動,在模塊文件代碼發生改動時,併發送 HMR 更新消息(HMR update)給HMR 運行時(HMR runtime)環境,它決定模塊的替換,具體能夠參考下圖:html
HMR實現的具體效果能夠先看下下圖的效果:java
但是最近,親自搭建一個webpack應用項目時,在實現開發環境的模塊熱更新時,遇到這樣那樣的問題。因爲以前都是使用第三方插件來實現應用的熱更新,它們都封裝了實現熱更新的一些細節,致使在不用第三方插件實現模塊熱更新時出現問題,其實仍是理解的不夠深刻。因而在搞明白以後寫下此文與你們分享。node
webpack的自帶的HMR插件HotModuleReplacementPlugin
是使用webpack熱更新功能的基礎。其餘的第三方插件如webpack-hot-middleware
、react-hot-loader
、babel-plugin-dva-hmr
等等都是要配合webpack自帶的HotModuleReplacementPlugin插件提供的api來實現代碼的熱更新。例以下面在某個模塊中使用HMR代碼一個例子:react
if (module.hot) { module.hot.accept('./containers/rootContainer.js', () => { const NextRootContainer = require('./containers/rootContainer.js').default; render(<NextRootContainer />, document.getElementById('react-root')); } }
固然HotModuleReplacementPlugin
爲可使用HMR的模塊提供了module.hot
,它爲一個對象,其含有不少api,具體能夠參考這裏。這樣利用插件提供的這些api能夠爲模塊實現自定義的熱更新邏輯。webpack
可是,在開發過程當中,大家可能也發現了,咱們並無爲項目中的每一個模塊提供這種多餘的HMR代碼,儘管全部代碼都有可能變化。那麼當這些代碼沒有HMR代碼的模塊發生變化時,他是如何實現熱更新的呢?這就要說到webpack HMR更新的冒泡(bubble)機制。具體能夠看下圖所展現的冒泡機制:git
從圖中能夠看出:github
模塊C發生了變化,可是模塊C沒有用HMR代碼捕獲變化,則模塊C的變化消息將冒泡到依賴C模塊的其餘模塊A和B中。web
模塊B因爲使用了HMR代碼進行捕獲變化,那麼應用的變化就按照代碼進行了更新。而且不會再冒泡了。
模塊A因爲一樣沒有HMR代碼捕獲變化,一樣將變動消息冒泡到依賴A模塊的模塊entry中。
入口entry模塊沒有HMR代碼捕獲變化的話:
一、 若項目使用webpack-dev-server的webpack/hot/dev-server
,則頁面會刷新整個頁面來加載變化;若使用webapck/hot/only-dev-server
的話,不會刷新頁面,會在控制檯展現一些有用的信息供開發者參考。具體能夠參考這裏。
二、若爲webpack-hot-middleware
配置了reload:true
,那麼頁面就會整個刷新來加載加載變化,這就變成liveroad模式;不然webpack就不知道如何加載變化模塊,控制檯也會有對應的提示。
例如,在本人的實例中,修改了searchForm.jsx模塊,能夠在控制檯清晰的看到,它一直冒泡到入口模塊index.js。以下圖:
在用webpack構建的項目中,在開發階段咱們爲了實現開發過程代碼的熱更新,若是對使用HMR不熟悉,可能會遇到這樣或者那樣的問題。下面就在本人開發過程當中遇到過:
--hot
選項的webpack-dev-server
命令時,不要在webpack的配置文件在配置HMR插件。不然會報下面的錯誤,具體可參考這裏。
注意:
webpack-dev-server的node api模式下配置
hot: true
仍然須要在webpack配置文件中配置該插件
重要更新:
\(\color{#FF0000}{該規則已不是問題,目前的webpack4已作了處理,即若webpack的配置項配置過HMR插件就不作處理,沒有配置則會主動幫咱們添加。}\)
其中源碼以下:
[].concat(config).forEach((config) => { config.entry = prependEntry(config.entry || './src'); if (options.hot || options.hotOnly) { config.plugins = config.plugins || []; if ( !config.plugins.find( (plugin) => plugin.constructor === webpack.HotModuleReplacementPlugin ) ) { config.plugins.push(new webpack.HotModuleReplacementPlugin()); } } });
module.hot.accept
代碼來接受更新消息以實現熱更新。在本人另外一個項目中,使用dora
插件系列的dora-plugin-webpack-hmr
插件來實現熱更新,因爲沒有在入口模塊添加HMR代碼來接受變動,致使模塊一產生變化就刷新整個頁面。
具體是由於:dora-plugin-webpack-hmr
使用webpack-hot-middleware
時,默認配置了其reload:true
(參考這裏),因此每次修改都會刷新整個頁面。
前面說到,要想實現webpack的HMR功能,須要兩點:webpack配置HMR和入口文件添加HMR代碼。兩者缺一不可,不然模塊熱更新就會失敗。
可是,在開發過程當中,咱們可能根本沒有配置過上面所說的兩點;這主要是由於咱們在項目中使用第三方HMR插件或者庫,它們自動替咱們完成這些;要麼是兩者都會給配置掉,要麼就配置其中之一。 比方在本人項目中使用過的dora-plugin-webpack-hmr
和babel-plugin-dva-hmr
,以及Gaearon大神的react-hot-loader
;下面就來講說他們的他們爲咱們作了什麼隱蔽的事。
該插件是爲dora
系列的插件,主要用在基於dora的項目中。該插件是基於webpack-hot-middleware
庫來實現熱加載的,它主要爲咱們作了兩件事:
代碼更新沒有捕獲時會刷新整個頁面來加載更新。 也就是爲webpack-hot-middleware
的reload屬性默認配置true,可看源碼1
自動爲webpack配置項添加HMR插件配置。具體看源碼2這樣,咱們使用該插件就不須要在webpack中配置HMR,不然會遇到常見問題1中的狀況。
因此:
使用dora-plugin-webpack-hmr插件仍是須要在入口模塊添加
module.hot.accept
來接受更新,不然達不到熱更新效果。
該插件是與dva
配套的,用在使用dva
框架下的代碼熱更新插件。該插件自動替咱們在入口模塊添加HMR代碼,具體可看源碼3,開發環境下入口模塊添加的代碼以下圖:
由此該插件只幫咱們在入口模塊添加HMR代碼接受變動,可是它沒有幫咱們在webpack中配置HMR,這樣HMR的api是不能用的。因此:
使用babel-plugin-dva-hmr插件還須要在webpack配置項中配置HMR。
該loader的目的是:保持組件狀態的熱更新。即不只達到模塊的熱更新,還要保持各個模塊的狀態不會丟失,具體可參考Gaearon大神的Hot Reloading in React。它如何保持狀態不在本文範圍,可自行查詢。
在該loader的3.0.0
版本前,與babel-plugin-dva-hmr插件相似,它也是自動爲咱們在模塊中注入接受更新的HMR代碼而沒有在webpack配置項自動添加HMR配置,具體可參考源碼4。可是它與前者不一樣是:它爲每一個啓用該loader的js文件都注入接受更新的HMR代碼。
例如,在webpack.config.js中爲js文件配置該插件:
//這樣src目錄下的全部.js文件都將被自動添加HMR熱更新代碼 loaders: [{ test: /\.js$/, loaders: ['react-hot', 'babel'], include: path.join(__dirname, 'src') }]
自動添加的有關HMR代碼以下,只截取部分代碼:
可是一樣的,
咱們須要在webpack配置項中添加HMR插件配置。
注意:
react-hot-loader在3.0.0版本以後就廢棄掉該方式,不會自動添加HMR熱更新代碼,須要開發者在項目入口模塊手動添加HMR代碼,參考這裏
以前,與webpack配合的webpack-dev-server
服務,經過配置就能夠實現代碼熱更新,可是隱藏了實現細節。下面咱們手動搭建一個自帶HMR功能的本地開發node sever。
webpack-dev-middleware
搭建本地服務webpack-dev-server就是基於webpack-dev-middleware來搭建內部node server。咱們搭建本身的開發環境就用它來直接搭建。
webpack-hot-middleware
來實現客戶端與服務端的通訊以接受更新該模塊只是負責客戶端與服務器通訊及接受變化,可是如何實現根據熱加載來完成應用的無縫變化銜接就超出了該模塊的範圍,正如其官網所描述:
This module is only concerned with the mechanisms to connect a browser client to a webpack server & receive updates. It will subscribe to changes from the server and execute those changes using webpack's HMR API. Actually making your application capable of using hot reloading to make seamless changes is out of scope, and usually handled by another library.
這句話的意思是:
What this means in practice, is you either need to add some code which calls module.hot.accept(), or use a plugin which can automatically add this code to your modules - otherwise webpack doesn't know how to apply the hot update.
也就是, 要麼你在模塊中增長調用module.hot.accept()
的代碼,要麼使用第三方插件自動的爲你模塊添加這些代碼;不然webpack不知道怎麼更新這些模塊。具體能夠參考這裏。
另外,要使用HMR功能,須要在webpack的配置項的每一個入口項數組中添加webpack-hot-middleware/client
,即:
entry: { index: ['./src/index','webpack-hot-middleware/client'] }
正如上文所描述的,它分爲兩步:
plugins: [ new webpack.HotModuleReplacementPlugin()]
if(module.hot){ module.hot.accpet() //接受模塊更新的事件,同時阻止這個事件繼續冒泡 }
若爲每一個模塊添加HMR代碼來熱更新對應的模塊機制是不可取的,這會產生大量冗餘代碼,極不推薦這種作法,除非像第三方插件那樣自動幫咱們完成。
通常在入口模塊添加module.hot的相關api來更新具體變化,入口模塊沒有添加的話就不會達到熱更新的效果,瀏覽器控制檯也會出現以下警告(前提是webpack-hot-middleware的reload配置爲false):
在瀏覽器控制檯中出現這樣一句提示:
This is usually because the modules which have changed (and their parents) do not know how to hot reload themselves.
正如提示所說的,修改某個子模塊時,若不在模塊自己或者頂級的入口模塊添加熱更新接受機制,那麼產生變化的模塊及其父模塊不知道怎麼加載他們。
最終,用戶自定義的開發環境node server具體的核心開發代碼以下:
//dev-server.js 文件 var webpackDevMiddleware = require('webpack-dev-middleware'); var webpackHotMiddleware = require('webpack-hot-middleware'); Object.keys(webpackConfig.entry).forEach(function(name){ webpackConfig.entry[name] = ['webpack-hot-middleware/client'].concat(webpackConfig.entry[name]); }) var compiler = webpack(webpackConfig); var devMiddleware = webpackDevMiddleware(compiler, { publicPath: webpackConfig.output.publicPath, hot: true, noInfo: true, stats: { colors: true } }); var hotMiddleware = webpackHotMiddleware(compiler); app.use(devMiddleware); app.use(hotMiddleware); app.listen(port, function(err){ if(err){ console.log(err); }else { var url = 'http://localhost:' + port; console.log("listening on port %s", port); } })
另外,咱們可能會想到,在使用redux的react項目中,這種熱更新會致使應用的state丟失,爲了防止state隨熱更新而丟失,通常須要在針對reducer
的修改來實現進行state的保存,最經常使用的作法是在store模塊下添加以下reducer熱更新代碼:
if(module.hot){ module.hot.accept('../reducers/index.js', ()=>{ const nextReducer = require('../reducers/index.js'); store.replaceReducer(nextReducer || nextReducer.default); }) }
至此,一個帶HMR代碼熱更新功能的本地開發node server就搭建成功了。
上面的帶HMR熱更新功能的node server雖已搭建,可是就能知足咱們的開發需求了麼?我想答案是否認的。上面的熱更新實際上是針對js文件的熱更新,也就是說對js文件的變動作熱更新。在實際項目中,咱們修改的可不只僅是js文件,還有css文件、html文件等等,這些都須要考慮熱更新。
在項目中,咱們使用html-webpack-plugin
來生成webpack spa頁面。因爲該插件不支持HMR,爲了支持html的HMR,咱們須要利用webpack-hot-middleware
提供對外接口來實現。具體須要三步:
html-webpack-plugin-after-emit
增長回調,釋放一個信號表示html頁面已經構建完成。// dev-server.js compiler.plugin('compilation', function (compilation) {//webpack編譯完成 //在這個插件合成出頁面以後,添加一個回調,調用中間件emit一個action爲reload的事件,對應另外一邊client訂閱的事件,實現瀏覽器的刷新 compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { hotMiddleware.publish({action: 'reload'}) cb() }) });
// 新建一個build/dev-client.js文件 var hotClient = require('webpack-hot-middleware/client'); // 添加一個訂閱事件,當監聽到 event.action === 'reload' 時執行頁面刷新 hotClient.subscribe(function (event) { if (event.action === 'reload') { window.location.reload() } })
build/dev-client
// 在webpack配置中設置 Object.keys(config.entry).forEach(function (name, i) { config.entry[name] = ['./build/dev-client'].concat(config.entry[name]) })
至此,html文件的熱更新就完成了,不過這裏不是真正意義上的熱更新,而是刷新整個頁面。
通常狀況下,webpack項目中的css處理都是經過 extract-text-webpack-plugin
插件把css抽離到單獨css文件中,但使人遺憾的是該插件是不支持熱加載的,具體能夠參考issue。
可是,可喜的是webpack的style-loader
是支持css熱加載的。 該插件經過js建立一個 style 標籤,而後注入內聯的css。
因此,按照上面描述,要想實現css的熱加載,只須要: 開發環境不要用extract-text-webpack-plugin
插件,而是用style-loader
代替。 可是,這種作法被開發者狠狠的吐槽了,而且還列出的緣由:
用隔離的css文件能更好的調試
開發和生產環境的儘量的一致,能夠保證儘量少的bug
吐槽歸吐槽,官方仍是沒有提供熱加載支持,可是社區出現了extract-text-webpack-plugin支持熱加載的各類實現方式,雖然有些是hack,可是能工做的很好啊,例以下面的列舉的實現:
相似於html文件熱更新,採用事件通知機制來實現,能夠參加這裏
將引入js模塊中的css模塊文件,如require('<path to css file>')
這行代碼抽取成一個單獨的js文件,並在該js文件實現模塊更新接收,能夠參考這裏。
用一個babel插件css-hot-loader來實現。
該插件的實現原理:
每次熱加載都是一個 js 文件的修改,每一個 css 文件在 webpack 中也是一個 js 模塊,那麼只須要在這個 css 文件對應的模塊裏面加一段代碼就能夠實現 css 文件的更新了(具體的是更新外鏈link的地址url,爲其添加時間戳),它會自動在每一個css文件中添加以下代碼:
if(module.hot) { // ${Date.now()} const cssReload = require(${loaderUtils.stringifyRequest(this, require.resolve('./hotModuleReplacement'))})(${JSON.stringify(options)}); module.hot.dispose(cssReload); module.hot.accept(undefined, cssReload); }
最終對應的CSS文件編譯生成的代碼多是這樣子:
// removed by extract-text-webpack-plugin if(module.hot) { // 1498744720173 const cssReload = require("../../../node_modules/css-hot-loader/hotModuleReplacement.js")({"fileMap":"{fileName}"}); module.hot.dispose(cssReload); module.hot.accept(undefined, cssReload); } /***************** ** WEBPACK FOOTER ** ./src/routes/main.less ** module id = 636 ** module chunks = 1 **/
這裏不說代碼熱更新,而是提供一種代碼變更更新機制。
在項目中,咱們能夠很容易實現js、css和html文件的熱更新;可是,咱們有沒有想到過,在咱們項目中其餘文件變動時也要加載變化後的文件,例如項目中package.json
或者webpack.config.js
配置文件發生了變化,咱們也想瀏覽器有所反應而不是無動於衷,那麼咱們能夠監控這些文件的變化來實現。具體:
dev-server.js
文件中用chokidar
添加對指定文件的監控,好比webpack.config.js//dev-server.js var chokidar = require('chokidar'); chokidar.watch(path.resolve(process.cwd(), 'webpack.dev.conf.js')).on('change', function(){ process.send('restart'); //向父進程傳遞消息信號 })
dev-server.js
對應的子進程。//dev-server-main.js var cp = require('child_process'); function start(){ const p = cp.fork(__dirname + '/dev-server.js'); p.on('message', function(data){ if(data === 'restart'){ p.kill('SIGINT'); start(); } }) } if(!process.send){ start(); }
node dev-server-main.js
開啓服務這樣,就能夠實現修改webpack.config.js達到從新加載配置的目的。不過它的作法是webpack從新對項目編譯。