上一篇Vite入門從手寫一個乞丐版的Vite開始(上)我們已經成功的將頁面渲染出來了,這一篇我們來簡單的實現一下熱更新的功能。
所謂熱更新就是修改了檔案,不用重新整理頁面,頁面的某個部分就自動更新了,聽著似乎挺簡單的,但是要實現一個很完善的熱更新還是很複雜的,要考慮的情況很多,所以本文只會實現一個最基礎的熱更新效果。
建立WebSocket連線
瀏覽器顯然是不知道檔案有沒有修改的,所以需要後端進行推送,我們先來建立一個WebSocket連線。
// app。jsconst server = http。createServer(app);const WebSocket = require(“ws”);// 建立WebSocket服務const createWebSocket = () => { // 建立一個服務例項 const wss = new WebSocket。Server({ noServer: true });// 不用額外建立http服務,直接使用我們自己建立的http服務 // 接收到http的協議升級請求 server。on(“upgrade”, (req, socket, head) => { // 當子協議為vite-hmr時就處理http的升級請求 if (req。headers[“sec-websocket-protocol”] === “vite-hmr”) { wss。handleUpgrade(req, socket, head, (ws) => { wss。emit(“connection”, ws, req); }); } }); // 連線成功 wss。on(“connection”, (socket) => { socket。send(JSON。stringify({ type: “connected” })); }); // 傳送訊息方法 const sendMsg = (payload) => { const stringified = JSON。stringify(payload, null, 2); wss。clients。forEach((client) => { if (client。readyState === WebSocket。OPEN) { client。send(stringified); } }); }; return { wss, sendMsg, };};const { wss, sendMsg } = createWebSocket();server。listen(3000);
WebSocket和我們的服務共用一個http請求,當接收到http協議的升級請求後,判斷子協議是否是vite-hmr,是的話我們就把建立的WebSocket例項連線上去,這個子協議是自己定義的,透過設定子協議,單個伺服器可以實現多個WebSocket 連線,就可以根據不同的協議處理不同型別的事情,服務端的WebSocket建立完成以後,客戶端也需要建立,但是客戶端是不會有這些程式碼的,所以需要我們手動注入,建立一個檔案client。js:
// client。js// vite-hmr代表自定義的協議字串const socket = new WebSocket(“ws://localhost:3000/”, “vite-hmr”);socket。addEventListener(“message”, async ({ data }) => { const payload = JSON。parse(data);});
接下來我們把這個client。js注入到html檔案,修改之前html檔案攔截的邏輯:
// app。jsconst clientPublicPath = “/client。js”;app。use(async function (req, res, next) { // 提供html頁面 if (req。url === “/index。html”) { let html = readFile(“index。html”); const devInjectionCode = `\n\n`; html = html。replace(/
/, `$&${devInjectionCode}`); send(res, html, “html”); }})透過import的方式引入,所以我們需要攔截一下這個請求:
// app。jsapp。use(async function (req, res, next) { if (req。url === clientPublicPath) { // 提供client。js let js = fs。readFileSync(path。join(__dirname, “。/client。js”), “utf-8”); send(res, js, “js”); }})
可以看到已經連線成功。
監聽檔案改變
接下來我們要初始化一下對檔案修改的監聽,監聽檔案的改變使用chokidar[1]:
// app。jsconst chokidar = require(chokidar);// 建立檔案監聽服務const createFileWatcher = () => { const watcher = chokidar。watch(basePath, { ignored: [/node_modules/, /\。git/], awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 10, }, }); return watcher;};const watcher = createFileWatcher();watcher。on(“change”, (file) => { // file檔案修改了})
構建匯入依賴圖
為什麼要構建依賴圖呢,很簡單,比如一個模組改變了,僅僅更新它自己肯定還不夠,依賴它的模組都需要修改才對,要做到這一點自然要能知道哪些模組依賴它才行。
// app。jsconst importerMap = new Map();const importeeMap = new Map();// map : key -> set// map : 模組 -> 依賴該模組的模組集合const ensureMapEntry = (map, key) => { let entry = map。get(key); if (!entry) { entry = new Set(); map。set(key, entry); } return entry;};
需要用到的變數和函式就是上面幾個,importerMap用來存放模組到依賴它的模組之間的對映;importeeMap用來存放模組到該模組所依賴的模組的對映,主要作用是用來刪除不再依賴的模組,比如a一開始依賴b和c,此時importerMap裡面存在b -> a和c -> a的對映關係,然後我修改了一下a,刪除了對c的依賴,那麼就需要從importerMap裡面也同時刪除c -> a的對映關係,這時就可以透過importeeMap來獲取到之前的a -> [b, c]的依賴關係,跟此次的依賴關係a -> [b]進行比對,就可以找出不再依賴的c模組,然後在importerMap裡刪除c -> a的依賴關係。
接下來我們從index。html頁面開始構建依賴圖,index。html內容如下:
可以看到它依賴了main。js,修改攔截html的方法:
// app。jsapp。use(async function (req, res, next) { // 提供html頁面 if (req。url === “/index。html”) { let html = readFile(“index。html”); // 查詢模組依賴圖 const scriptRE = /( {{ text }}
test。js又引入了test2。js:
// test。jsimport test2 from ”。/test2。js“;export default function () { let a = test2(); let b = ”我是測試1“; return a + ” ——- “ + b;}// test2。jsexport default function () { return ‘我是測試2’}
接下來修改test2。js測試效果:
可以看到重新發送了請求,但是頁面並沒有更新,這是為什麼呢,其實還是快取問題:
App。vue匯入的兩個檔案之前已經請求過了,所以瀏覽器會直接使用之前請求的結果,並不會重新發送請求,這要怎麼解決呢,很簡單,可以看到請求的App。vue的url是帶了時間戳的,所以我們可以檢查請求模組的url是否存在時間戳,存在則把它依賴的所有模組路徑也都帶上時間戳,這樣就會觸發重新請求了,修改一下模組路徑轉換方法parseBareImport:
// app。js// 處理裸匯入const parseBareImport = async (js, importer) => { // 。。。 // 檢查模組url是否存在時間戳 let hast = checkQueryExist(importer, ”t“);// ++ // 。。。 parseResult[0]。forEach((item) => { let url = ”“; if (item。n[0] !== ”。“ && item。n[0] !== ”/“) { url = `/@module/${item。n}?import${hast ? ”&t=“ + Date。now() : ”“}`;// ++ } else { url = `${item。n}?import${hast ? ”&t=“ + Date。now() : ”“}`;// ++ } // 。。。 }) // 。。。}
再來測試一下:
可以看到成功更新了。最後我們再來測試執行重新整理整個頁面的情況,修改一下main。js檔案即可:
總結
本文參考Vite-1。0。0-rc。5版本寫了一個非常簡單的Vite,簡化了非常多的細節,旨在對Vite及熱更新有一個基礎的認識,其中肯定有不合理或錯誤之處,歡迎指出~
示例程式碼在:https://github。com/wanglin2/vite-demo[3]。
[1]
chokidar:
https://github。com/paulmillr/chokidar
[2]
lru-cache:
https://github。com/isaacs/node-lru-cache
[3]
https://github。com/wanglin2/vite-demo:
https://github。com/wanglin2/vite-demo