1. 河豚號(hào) > 生活百科 >

webpack熱更新原理(熱更新和冷更新區(qū)別)

我們大致了解了webpack HMR 原理??梢钥闯鲆韵聨c(diǎn)核心思想:

1、監(jiān)聽(tīng)文件變化

2、服務(wù)器與客戶端通信

3、替換流程

4、降級(jí)操作

當(dāng)然,由于 webpack 本身有個(gè)很成熟的模塊思想和生態(tài),因此整個(gè)架構(gòu)設(shè)計(jì)會(huì)比我們實(shí)現(xiàn)的 HMR 復(fù)雜很多。在模塊熱替換中,是由 webpack 的全部流程出力來(lái)完成這一操作的,而并沒(méi)有局限于 webpack-dev-server 和 webpack 以及業(yè)務(wù)代碼本身,實(shí)際上,起到更重要作用的是各類 loader,它們需要使用 HMR API 來(lái)實(shí)現(xiàn) Hot Reload 的邏輯,決定什么時(shí)候注冊(cè)模塊、什么時(shí)候卸載模塊;如何注冊(cè)和卸載模塊。而 webpack 本身更像是一個(gè)調(diào)用方的角色,不需要考慮具體的注冊(cè)和反注冊(cè)邏輯。

HMR 的核心組織

經(jīng)過(guò)了上面的分析,我們基本上確認(rèn)了一個(gè)思路,也就是分析 webpack HMR 得出的結(jié)論。但是由于我們只有 runtime,所以實(shí)現(xiàn) Hot Reload 變成了一個(gè)下圖的簡(jiǎn)單流程:

1、Server 啟動(dòng)一個(gè) HTTP 服務(wù)器,并且注冊(cè)和啟動(dòng) WebSocket 服務(wù),用于屆時(shí)與客戶端通信

2、在啟動(dòng) Static 服務(wù)器后返回頁(yè)面前注入 HMR 的客戶端代碼,業(yè)務(wù)方無(wú)需關(guān)心 HMR 的具體實(shí)現(xiàn)和添加對(duì)應(yīng)的支持代碼服務(wù)端監(jiān)聽(tīng)磁盤文件的變更,將文件變更通過(guò) WebSocket 發(fā)送給客戶端

3、客戶端收到文件變更消息后進(jìn)行對(duì)應(yīng)的模塊處理

4、(模塊處理失敗,降級(jí)為 Live Reload)

live reload?

在實(shí)現(xiàn) HMR 之前,我們可以先實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 Live Reload 來(lái)保證我們 1-3 步的實(shí)現(xiàn)沒(méi)有異常。

const Koa = require('koa')

const WebSocket = require('ws')

const chokidar = require('chokidar')

const app = new Koa()

const fs = require('fs').promises

const wss = new WebSocket.Server({ port: 8000 })

const dir = './static'

const watcher = chokidar.watch('./static', {

ignored: /node_modules|.git|[/\]./

})

wss.on('connection', (ws) => {

watcher

.on('add', path => console.log(`File ${path} added`))

.on('change', path => console.log(`File ${path} has been changed`))

.on('unlink', path => console.log(`File ${path} has been moved`))

.on('all', async (event, path) => {

// Simple Live Reload

ws.send('reload')

})

ws.on('message', (message) => {

console.log('received: %s', message)

})

ws.send('HMR Client is Ready')

})

const injectedData = ` `

app.use(async (ctx, next) => {

let file = ctx.path

if (ctx.path.endsWith('/')) {

file = ctx.path + 'index.html'

}

let body

try {

body = await fs.readFile(dir + file, {

encoding: 'utf-8'

})

} catch(e) {

ctx.status = 404

return next()

}

if (file.endsWith('.html')) body = body.replace('', `${injectedData}`)

if (file.endsWith('.css')) ctx.type = 'text/css'

ctx.body = body

next()

})

app.listen(3001)

console.log('listen on port 3001')

手機(jī)看代碼不方便,我把代碼截圖貼這里了

 

WebSocket實(shí)現(xiàn)簡(jiǎn)單的webpack HMR(熱更新)效果

 

上述代碼中,簡(jiǎn)單的使用了 chokidar 這個(gè)文件監(jiān)聽(tīng)?zhēng)欤鼧O大的減輕了我們的工作量;而 WebSocket 和服務(wù)器的實(shí)現(xiàn)上暫不贅述,之所以不直接使用 koa-static 的原因是因?yàn)槲覀冃枰獙?duì)于 HTML 文件進(jìn)行一些注入操作,以上 Live Reload 的實(shí)現(xiàn)非常簡(jiǎn)單,基本可以總結(jié)為一句話:得知文件變化后向客戶端發(fā)送 reload 消息,客戶端收到消息執(zhí)行頁(yè)面刷新操作。

實(shí)現(xiàn)了一個(gè) Live Reload 之后,接下來(lái)我們只需要變更注入的代碼和發(fā)送到客戶端的消息兩個(gè)部分即可,其實(shí) Hot Reload 和 Live Reload 最大的區(qū)別也就是「最小模塊替換」與「刷新頁(yè)面」的區(qū)別,因此其他部分都是不用變動(dòng)的。

替換 HTML 和 CSS 則是其中最簡(jiǎn)單的兩項(xiàng)任務(wù)。

HTML

通常來(lái)說(shuō),我們要覆蓋 HTML 中的內(nèi)容,除了刷新這一操作外,還有一個(gè)就是 document.write(),實(shí)際上我們也是通過(guò)這個(gè)函數(shù)來(lái)實(shí)現(xiàn) HTML 的 Hot Reload 的:

// 監(jiān)聽(tīng)

.on('all', async (event, path) => {

if (path.endsWith('.html')) {

body = await fs.readFile(path, {

encoding: 'utf-8'

})

const message = JSON.stringify({ type: 'html', content: body })

ws.send(message)

}

})

// 注入

let data = {}

try {

data = JSON.parse(event.data)

} catch (e) {

// return

}

console.log(data)

if (data.type === 'html') {

document.write(data.content);

document.close();

console.log('[HMR] updated HTML');

}

 

WebSocket實(shí)現(xiàn)簡(jiǎn)單的webpack HMR(熱更新)效果

 

那么讀者最大的困惑可能變成了:精度怎么粗糙的熱更新,好像跟直接刷頁(yè)面并沒(méi)有什么區(qū)別?

如果我們要進(jìn)行精度更高的熱更新,那么帶來(lái)的性能差異其實(shí)是巨大的,我們來(lái)考慮一下如果我們希望盡可能細(xì)粒度的熱更新操作,接下來(lái)需要哪些操作:

讀取文件

構(gòu)造語(yǔ)法樹(shù)

對(duì)比和之前的語(yǔ)法樹(shù)的差異

通信將差異傳給客戶端

將差異轉(zhuǎn)換為對(duì)應(yīng)的 DOM 操作

那樣不可避免的,我們就要在內(nèi)存中緩存每個(gè)頁(yè)面最初的語(yǔ)法樹(shù),對(duì)于模塊化的組件來(lái)說(shuō),HTML 本身的變更其實(shí)是并不太多的,沒(méi)有必要進(jìn)行這么復(fù)雜的操作

CSS

CSS 也比較簡(jiǎn)單,只要移除舊的 CSS 文件重新引入就能更新 CSS 了,這次,我們的代碼將會(huì)更加精簡(jiǎn)。

// 監(jiān)聽(tīng)

if (path.endsWith('.css')) {

const message = JSON.stringify({ type: 'css', content: path.split('static/')[1] })

ws.send(message)

}

// 注入

if (data.type === 'css') {

const host = location.host

document.querySelectorAll('link[rel="stylesheet"]').forEach(el => {

const resource = el.href.split(host + '/')[1]

console.log(resource)

if (resource === data.content) el.remove()

})

document.head.insertAdjacentHTML('beforeend', '

')

console.log('[HMR] updated CSS');

}

 

WebSocket實(shí)現(xiàn)簡(jiǎn)單的webpack HMR(熱更新)效果

 

相比 HTML 來(lái)說(shuō),CSS 顯得更加「無(wú)公害」——即使是整個(gè)文件替換更新,也不會(huì)帶來(lái)什么壞處,甚至你都不需要對(duì)文件內(nèi)容進(jìn)行讀取,只需要重新加載文件內(nèi)容。

JavaScript

最大的難點(diǎn)在于 JavaScript 熱更新的實(shí)現(xiàn),如果我們參考 HTML 和 CSS 的實(shí)現(xiàn),簡(jiǎn)單的進(jìn)行二次寫入,很快的就會(huì)遇到各種各樣的問(wèn)題。在這里,我們通過(guò) eval 的方式進(jìn)行再寫入。

假設(shè)我們對(duì)按鈕綁定了一個(gè)點(diǎn)擊事件,console.log(123),然后變成 console.log(1),使用原本的方法寫入之后,就會(huì)響應(yīng)兩次事件,分別輸出 「123」和「1」。(這里就不貼代碼了,感興趣的同學(xué)可以自己做這個(gè)實(shí)驗(yàn))

但是如同 HTML 的實(shí)現(xiàn)部分一樣,我們并不像進(jìn)行復(fù)雜的語(yǔ)法樹(shù)構(gòu)建來(lái)感知操作的是哪一個(gè) DOM,那么這個(gè)需求就變的很難處理。

得益于組件化,我們現(xiàn)在并不用太過(guò)關(guān)心這個(gè)問(wèn)題,當(dāng)我更新了一個(gè)文件的時(shí)候,我必然是更新了一個(gè)組件,只需要把這個(gè)組件的實(shí)例化移除并且重新載入即可,那樣與之綁定的相關(guān)事件也會(huì)被刪除。

整理一下思路,要執(zhí)行 JS 的熱更新,我們大概會(huì)有以下幾個(gè)步驟:

感知每一個(gè)熱更新的組件:建立一個(gè) k-v 結(jié)構(gòu),確保存入每個(gè)組件的實(shí)例,便于之后更新時(shí)刪除 DOM 并且更新

執(zhí)行 eval 寫入代碼

遍歷 k-v 結(jié)構(gòu),刪除原先創(chuàng)建的 DOM,而實(shí)例渲染到 DOM 中的步驟是由框架本身處理的,我們甚至可以不用做任何操作

這里我們以我最近在使用的那個(gè)無(wú)需構(gòu)建即可運(yùn)行的前端框架為例,從上述步驟中,我們可以知道,最重要的就是要劫持構(gòu)造函數(shù),在轉(zhuǎn)換為 DOM 時(shí)存入我們的 k-v 結(jié)構(gòu),方便以后使用。

// 劫持構(gòu)造函數(shù)

const JKL = window.Jinkela

const storage = {}

let latest = true

window.Jinkela = class jkl extends JKL {

constructor(...args) {

super(...args)

const values = storage[this.constructor.name]

if (!latest) {

storage[this.constructor.name].forEach(el => el.remove())

storage[this.constructor.name] = []

latest = true

}

storage[this.constructor.name] = values ? [...values, this.element] : [ this.element ]

}

}

// 注入

if (data.type === 'js') {

latest = false

eval(data.content)

console.log('[HMR] updated JS');

}

 

WebSocket實(shí)現(xiàn)簡(jiǎn)單的webpack HMR(熱更新)效果

 

這樣在執(zhí)行 eval 的過(guò)程中就會(huì)先記性一遍 DOM 的整理,執(zhí)行完畢后新的組件就被渲染上去了。

當(dāng)然,讀者可以發(fā)現(xiàn)這里有一個(gè)前提條件,那就是沒(méi)有一個(gè)內(nèi)容處于全局作用域,否則就會(huì)遇到重復(fù)聲明的 error 導(dǎo)致熱更新失敗。

基本上來(lái)說(shuō)是一個(gè)非常簡(jiǎn)單的 Hot Reload,可以完善的地方還是相當(dāng)多的:

沒(méi)有維持連接的心跳包

頻繁對(duì)磁盤文件讀

降級(jí) Live Reload 的操作

目前這種 Hot Reload 只支持單文件組件

不支持繼承

那么,到底能不能有一個(gè)通用的支持任意 JS 的 hot reload 呢?目前為止感覺(jué)還不能解決重復(fù)聲明的問(wèn)題,實(shí)際上,webpack 的由 loader 實(shí)現(xiàn)大致也是因?yàn)楦鱾€(gè)模塊會(huì)有其自己的風(fēng)格,需要單獨(dú)去處理。

本文由網(wǎng)上采集發(fā)布,不代表我們立場(chǎng),轉(zhuǎn)載聯(lián)系作者并注明出處:http://m.zltfw.cn/shbk/37598.html

聯(lián)系我們

在線咨詢:點(diǎn)擊這里給我發(fā)消息

微信號(hào):15705946153

工作日:9:30-18:30,節(jié)假日休息