webpack Hmr 源碼實現

1、熱更新原理 

服務端:
  •  1.啓動webpack-dev-server服務器 
  • 2.建立webpack實例 
  • 3.建立Server服務器 
  • 4.添加webpackdone事件回調 編譯完成向客戶端發送消息(hash和描述文件oldhash.js和oldhash.json) 
  • 5.建立express應用app 
  • 6.設置文件系統爲內存文件系統 
  • 7.添加webpack-dev-middleware中間件 負責返回生成的文件 
  • 8.建立http服務 啓動 
  • 9.使用socket 實現瀏覽器和服務器的通訊(這裏先發送一次hash,將socket存入到第四步,初次編譯完第四步中的socket是空,不會觸發hash下發) 


客戶端: html

  • 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.runtimehotDownloadmainfest方法,向server端發送ajax請求,服務端返回一個Mainfest文件,該文件包含全部要更新模塊的hash值和chunk名 
  • 7.調用 JsonpMainTemplate.runtimehotDownloadUpdateChunk方法經過jsonp請求獲取到最新的模塊代碼 
  • 8.補丁js取回後調用 JsonpMainTemplate.runtimewebpackHotUpdate方法,裏面會調用hotAddUpdateChunk方法,用心的模塊替換掉舊的模塊 
  • 9.調用HotMoudleReplacement.runtime.jshotAddUpdateChunk方法動態更新模塊代碼 
  • 10.調用 hotApply 方法熱更新

客戶端代碼輔助流程理解
webpack

  • 客戶端這裏初次加載 先走 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方便下次比較

2、根據流程實現代碼:web

客戶端:ajax

//發佈訂閱
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);

複製代碼


服務端實現:express

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);複製代碼
相關文章
相關標籤/搜索