一、熱更新原理
服務端:- 1.啟動
webpack-dev-server
伺服器 - 2.建立
webpack
例項 - 3.建立
Server
伺服器 - 4.新增
webpack
的done
事件回撥 編譯完成向客戶端傳送訊息(hash和描述檔案oldhash.js和oldhash.json)
- 5.建立
express
應用app
- 6.設定檔案系統為記憶體檔案系統
- 7.新增
webpack-dev-middleware
中介軟體 負責返回生成的檔案 - 8.建立
http
服務 啟動 - 9.使用
socket
實現瀏覽器和伺服器的通訊(這裡先傳送一次hash
,將socket
存入到第四步,初次編譯完第四步中的socket
是空,不會觸發hash
下發)
客戶端:
- 1
.webpack-dev-server/client-src
下檔案監聽hash
,儲存此hash
值 - 2.客戶端收到
ok
訊息執行reload
更新 - 3.在
reload
中進行判斷,如果支援熱更新執行webpackHotUpdate
,不支援的話直接重新整理頁面 - 4.在
webpack/hot/dev-server.js
監聽webpackHotUpdate
然後執行check()
方法進行檢測 - 5.在
check
方法裡面呼叫module.hot.check
- 6.通過呼叫
JsonpMainTemplate.runtime
的hotDownloadmainfest
方法,向server
端傳送ajax
請求,服務端返回一個Mainfest
檔案,該檔案包含所有要更新模組的hash
值和chunk
名 - 7.呼叫
JsonpMainTemplate.runtime
的hotDownloadUpdateChunk
方法通過jsonp
請求獲取到最新的模組程式碼 - 8.補丁js取回後呼叫
JsonpMainTemplate.runtime
的webpackHotUpdate
方法,裡面會呼叫hotAddUpdateChunk
方法,用心的模組替換掉舊的模組 - 9.呼叫
HotMoudleReplacement.runtime.js
的hotAddUpdateChunk
方法動態更新模組程式碼 - 10.呼叫
hotApply
方法熱更新
客戶端程式碼輔助流程理解:
- 客戶端這裡初次載入 先走
socket.on("hash")和socket.on("ok")
拿到服務端首次生成的hash
值 - 然後執行
reloadApp
這個函式 這裡派發hotEmitter.emit('webpackHotUpdate')
事件 - 然後執行
hotEmitter.on('webpackHotUpdate')
這個函式, - 因為是初次編譯 所以
hotCurrentHash 為 undefined
然後將首次拿到的currentHash
賦值給hotCurrentHash
- 到這裡 初次載入的邏輯執行完畢
- ------------------next--------------------
- 假如使用者修改了某個模組的程式碼,將會再次執行
socket.on("hash")和socket.on("ok")
拿到最新的程式碼編譯後的 hash - 如上述步驟進入 hotEmitter.on('webpackHotUpdate') 中的事件判斷, if(!hotCurrentHash || hotCurrentHash == currentHash) hotCurrentHash為上次的hash值 currentHash為最新收到的 並且判斷兩次是否一致,一致則不需要更新,不一致就執行熱更新邏輯
hotCheck
會通過ajax請求服務端拉取最新的hot-update.json
描述檔案 說明哪些模組哪些chunk
(大集合)發生了更新改變- 然後根據描述檔案
hotDownloadUpdateChunk
去建立jsonp
拉取到最新的更新後的程式碼,返回形式為:webpackHotUpdate(id, {...})
- 為了拉取到的程式碼直接執行,客戶端需要定義一個
window.webpackHotUpdate
函式來處理 - 這裡面將快取的舊程式碼更新為最新的程式碼,接著將父模組中的
render
函式執行一下 - 最後將
hotCurrentHash = currentHash
置舊hash
方便下次比較
二、根據流程實現程式碼:
客戶端:
//釋出訂閱
class Emitter{
constructor(){
this.listeners = {}
}
on(type, listener){
this.listeners[type] = listener
}
emit(){
this.listeners[type] && this.listeners[type]()
}
}
let socket = io('/');
let hotEmitter = new Emitter();
const onConnected = () => {
console.log('客戶端連線成功')
}
//存放服務端傳給的hash 本次的hash 和 上一次的hash
let currentHash, hotCurrentHash;
socket.on("hash",(hash)=>{
currentHash = hash
});
//收到ok事件之後
socket.on('ok',()=>{
//true代表熱更新
reloadApp(true);
})
hotEmitter.on('webpackHotUpdate',()=>{
if(!hotCurrentHash || hotCurrentHash == currentHash){
return hotCurrentHash = currentHash
}
hotCheck()
})
function hotCheck(){
hotDownloadMainfest().then((update)=>{
let chunkIds = Object.keys(update.c)
chunkIds.forEach(chunkId=>{
hotDownloadUpdateChunk(chunkId);
})
})
}
function hotDownloadUpdateChunk(chunkId){
let script = document.createElement('script');
script.charset = 'utd-8'
script.src = '/'+chunkId+'.'+hotCurrentHash+'.hot-update.js'
document.head.appendChild(script);
}
//此方法用來詢問伺服器到底這一次編譯相對於上一次編譯改變了哪些chunk、哪些模組
function hotDownloadMainfest(){
return new Promise(function(resolve){
let request = new XMLHttpRequest()
let requestPath = '/'+hotCurrentHash+".hot-update.json"
request.open('GET', requestPath, true)
request.onreadystatechange = function(){
if(request.readyState === 4){
let update = JSON.parse(request.responseText)
resolve(update)
}
}
request.send()
})
}
function reloadApp(hot){
if(hot){
//釋出
hotEmitter.emit('webpackHotUpdate')
}else{
//不支援熱更新直接重新整理
window.location.reload()
}
}
window.hotCreateModule = function(){
let hot = {
_acceptedDependencies:{},
accept: function(deps, callback){
//callback 對應render回撥
for(let i = 0; i < deps.length; i++){
hot._acceptedDependencies[deps[i]] = callback
}
}
}
return hot
}
//通過jsonp獲取的最新程式碼 jsonp中有webpackHotUpdate這個函式
window.webpackHotUpdate = function(chunkId, moreModules){
for(let moduleId in moreModules){
//從模組快取中取到老的模組定義
let oldModule - __webpack_requrie__.c[moduleId]
let {parents, children} = oldModule
//parents哪些模組引用和這個模組 children這個模組用了哪些模組
//更新快取為最新程式碼
let module = __webpack_requrie__.c[moduleId] = {
i: moduleId,
l: false,
exports: {},
parents,
children,
hot: window.hotCreateModule(moduleId)
}
moreModules[moduleId].call(module.exports, module, module.exports, __webpack_requrie__)
module.l = true
//index.js ---import a.js import b.js a.js和b.js的父模組(index.js)
parents.forEach(par=>{
//父中的老模組的物件
let parModule = __webpack_requrie__.c[par]
parModule && parModule.hot && parModule.hot._acceptedDependencies[moduleId] && parModule.hot._acceptedDependencies[moduleId]()
})
//熱更新之後 本次的hash變為上一次的hash 置舊操作
hotCurrentHash = currentHash
}
}
socket.on("connect", onConnected);
複製程式碼
服務端實現:
const path = require('path');
const express = require('express');
const mime = require('mime');
const webpack = require('webpack');
const MemoryFileSystem = require('memory-fs');
const config = require('./webpack.config');
//compiler代表整個webpack編譯任務,全域性只有一個
const compiler = webpack(config);
class Server{
constructor(compiler){
this.compiler = compiler;
let sockets = [];
let lasthash;//每次編譯完成後都會產生一個stats物件,其中有一個hash值代表這一次編譯結果hash就是一個32的字串
compiler.hooks.done.tap('webpack-dev-server',(stats)=>{
lasthash = stats.hash;
//每當新一個編譯完成後都會向客戶端傳送訊息
sockets.forEach(socket=>{
//先向客戶端傳送最新的hash值
//每次編譯都會產生一個hash值,另外如果是熱更新的話,還會產出二個補丁檔案。
//裡面描述了從上一次結果到這一次結果都有哪些chunk和模組發生了變化
socket.emit('hash',stats.hash);
//再向客戶端傳送一個ok
socket.emit('ok');
});
});
let app = new express();
//以監控的模組啟動一次webpack編譯,當編譯成功之後執行回撥
compiler.watch({},err=>{
console.log('又一次編譯任務成功完成了')
});
let fs = new MemoryFileSystem();
//如果你把compiler的輸出檔案系統改成了 MemoryFileSystem的話,則以後再產出檔案都打包記憶體裡去了
compiler.outputFileSystem = fs;
function middleware(req, res, next) {
// /index.html dist/index.html
let filename = path.join(config.output.path,req.url.slice(1));
let stat = fs.statSync(filename);
if(stat.isFile()){//判斷是否存在這個檔案,如果在的話直接把這個讀出來發給瀏覽器
let content = fs.readFileSync(filename);
let contentType = mime.getType(filename);
res.setHeader('Content-Type',contentType);
res.statusCode = res.statusCode || 200;
res.send(content);
}else{
// next();
return res.senStatus(404);
}
}
//express app 其實是一個請求監聽函式
app.use(middleware);
this.server = require('http').createServer(app);
let io = require('socket.io')(this.server);
//啟動一個 websocket伺服器,然後等待連線來到,連線到來之後socket
io.on('connection',(socket)=>{
sockets.push(socket);
socket.emit('hash',lasthash);
//再向客戶端傳送一個ok
socket.emit('ok');
});
}
listen(port){
this.server.listen(port,()=>{
console.log(`伺服器已經在${port}埠上啟動了`)
});
}
}
let server = new Server(compiler);
server.listen(8000);複製程式碼