首頁/ 汽車/ 正文

Vite入門從手寫一個乞丐版的Vite開始(下)

上一篇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:

Vite入門從手寫一個乞丐版的Vite開始(下)

// 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”); }})

Vite入門從手寫一個乞丐版的Vite開始(下)

可以看到已經連線成功。

監聽檔案改變

接下來我們要初始化一下對檔案修改的監聽,監聽檔案的改變使用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內容如下:

Vite入門從手寫一個乞丐版的Vite開始(下)

可以看到它依賴了main。js,修改攔截html的方法:

// app。jsapp。use(async function (req, res, next) { // 提供html頁面 if (req。url === “/index。html”) { let html = readFile(“index。html”); // 查詢模組依賴圖 const scriptRE = /(]*>)([\s\S]*?)<\/script>/gm; const srcRE = /\balt="Vite入門從手寫一個乞丐版的Vite開始(下)" data-isLoading="0" src="/static/img/blank.gif" data-src=(?:“([^”]+)“|‘([^’]+)‘|([^’”\s]+)\b)/; // 找出script標籤 html = html。replace(scriptRE, (matched, openTag) => { const srcAttr = openTag。match(srcRE); if (srcAttr) { // 建立script到html的依賴關係 const importee = removeQuery(srcAttr[1] || srcAttr[2]); ensureMapEntry(importerMap, importee)。add(removeQuery(req。url)); } return matched; }); // 注入client。js // 。。。 }})

Vite入門從手寫一個乞丐版的Vite開始(下)

接下來我們需要分別修改js的攔截方法,註冊依賴關係;修改Vue單檔案的攔截方法,註冊js部分的依賴關係,因為上一篇文章裡我們已經把轉換裸匯入的邏輯都提取成一個公共函式parseBareImport了,所以我們只要修改這個函式就可以了:

// 處理裸匯入// 增加了importer入參,req。urlconst parseBareImport = async (js, importer) => { await init; let parseResult = parseEsModule(js); let s = new MagicString(js); importer = removeQuery(importer);// ++ parseResult[0]。forEach((item) => { let url = “”; if (item。n[0] !== “。” && item。n[0] !== “/”) { url = `/@module/${item。n}?import`; } else { url = `${item。n}?import`; } s。overwrite(item。s, item。e, url); // 註冊importer模組所以依賴的模組到它的對映關係 ensureMapEntry(importerMap, removeQuery(url))。add(importer);// ++ }); return s。toString();};

Vite入門從手寫一個乞丐版的Vite開始(下)

再來增加一下前面提到的去除不再依賴的關係的邏輯:

// 處理裸匯入const parseBareImport = async (js, importer) => { // 。。。 importer = removeQuery(importer); // 上一次的依賴集合 const prevImportees = importeeMap。get(importer);// ++ // 這一次的依賴集合 const currentImportees = new Set();// ++ importeeMap。set(importer, currentImportees);// ++ parseResult[0]。forEach((item) => { // 。。。 let importee = removeQuery(url);// ++ // url -> 依賴 currentImportees。add(importee);// ++ // 依賴 -> url ensureMapEntry(importerMap, importee)。add(importer); }); // 刪除不再依賴的關係++ if (prevImportees) { prevImportees。forEach((importee) => { if (!currentImportees。has(importee)) { // importer不再依賴importee,所以要從importee的依賴集合中刪除importer const importers = importerMap。get(importee); if (importers) { importers。delete(importer); } } }); } return s。toString();};

Vue單檔案的熱更新

先來實現一下Vue單檔案的熱更新,先監聽一下Vue單檔案的改變事件:

// app。js// 監聽檔案改變watcher。on(“change”, (file) => { if (file。endsWith(“。vue”)) { handleVueReload(file); }});

如果修改的檔案是以。vue結尾,那麼就進行處理,怎麼處理呢,Vue單檔案會解析成js、template、style三部分,我們把解析資料快取起來,當檔案修改了以後會再次進行解析,然後分別和上一次的解析結果進行比較,判斷單檔案的哪部分發生變化了,最後給瀏覽器傳送不同的事件,由前端頁面來進行不同的處理,快取我們使用lru-cache[2]:

// app。jsconst LRUCache = require(“lru-cache”);// 快取Vue單檔案的解析結果const vueCache = new LRUCache({ max: 65535,});

然後修改一下Vue單檔案的攔截方法,增加快取:

// app。jsapp。use(async function (req, res, next) { if (/\。vue\??[^。]*$/。test(req。url)) { // 。。。 // vue單檔案 let descriptor = null; // 如果存在快取則直接使用快取 let cached = vueCache。get(removeQuery(req。url)); if (cached) { descriptor = cached; } else { // 否則進行解析,並且將解析結果進行快取 descriptor = parseVue(vue)。descriptor; vueCache。set(removeQuery(req。url), descriptor); } // 。。。 }})

然後就來到handleVueReload方法了:

// 處理Vue單檔案的熱更新const handleVueReload = (file) => { file = filePathToUrl(file);};// 處理檔案路徑到urlconst filePathToUrl = (file) => { return file。replace(/\\/g, “/”)。replace(/^\。\。\/test/g, “”);};

我們先轉換了一下檔案路徑,因為監聽到的是本地路徑,和請求的url是不一樣的:

Vite入門從手寫一個乞丐版的Vite開始(下)

const handleVueReload = (file) => { file = filePathToUrl(file); // 獲取上一次的解析結果 const prevDescriptor = vueCache。get(file); // 從快取中刪除上一次的解析結果 vueCache。del(file); if (!prevDescriptor) { return; } // 解析 let vue = readFile(file); descriptor = parseVue(vue)。descriptor; vueCache。set(file, descriptor);};

接著獲取了一下快取資料,然後進行了這一次的解析,並更新快取,接下來就要判斷哪一部分發生了改變。

熱更新template

我們先來看一下比較簡單的模板熱更新:

const handleVueReload = (file) => { // 。。。 // 檢查哪部分發生了改變 const sendRerender = () => { sendMsg({ type: “vue-rerender”, path: file, }); }; // template改變了傳送rerender事件 if (!isEqualBlock(descriptor。template, prevDescriptor。template)) { return sendRerender(); }}// 判斷Vue單檔案解析後的兩個部分是否相同function isEqualBlock(a, b) { if (!a && !b) return true; if (!a || !b) return false; if (a。src && b。src && a。src === b。src) return true; if (a。content !== b。content) return false; const keysA = Object。keys(a。attrs); const keysB = Object。keys(b。attrs); if (keysA。length !== keysB。length) { return false; } return keysA。every((key) => a。attrs[key] === b。attrs[key]);}

邏輯很簡單,當template部分發生改變後向瀏覽器傳送一個rerender事件,帶上修改模組的url。

現在我們來修改一下HelloWorld。vue的template看看:

Vite入門從手寫一個乞丐版的Vite開始(下)

可以看到已經成功收到了訊息。

接下來需要修改一下client。js檔案,增加收到vue-rerender訊息後的處理邏輯。

檔案更新了,瀏覽器肯定需要請求一下更新的檔案,Vite使用的是import()方法,但是這個方法js本身是沒有的,另外筆者沒有找到是哪裡注入的,所以載入模組的邏輯只能自己來簡單實現一下:

// client。js// 回撥idlet callbackId = 0;// 記錄回撥const callbackMap = new Map();// 模組匯入後呼叫的全域性方法window。onModuleCallback = (id, module) => { document。body。removeChild(document。getElementById(“moduleLoad”)); // 執行回撥 let callback = callbackMap。get(id); if (callback) { callback(module); }};// 載入模組const loadModule = ({ url, callback }) => { // 儲存回撥 let id = callbackId++; callbackMap。set(id, callback); // 建立一個模組型別的script let script = document。createElement(“script”); script。type = “module”; script。id = “moduleLoad”; script。innerHTML = ` import * as module from ‘${url}’ window。onModuleCallback(${id}, module) `; document。body。appendChild(script);};

因為要載入的都是ES模組,直接請求是不行的,所以建立一個type為module的script標籤,來讓瀏覽器載入,這樣請求都不用自己發,只要把想辦法獲取到模組的匯出就行了,這個也很簡單,建立一個全域性函式即可,這個很像jsonp的原理。

接下來就可以處理vue-rerender訊息了:

// app。jssocket。addEventListener(“message”, async ({ data }) => { const payload = JSON。parse(data); handleMessage(payload);});const handleMessage = (payload) => { switch (payload。type) { case “vue-rerender”: loadModule({ url: payload。path + “?type=template&t=” + Date。now(), callback: (module) => { window。__VUE_HMR_RUNTIME__。rerender(payload。path, module。render); }, }); break; }};

就這麼簡單,我們來修改一下HelloWorld。vue檔案的模板來看看:

Vite入門從手寫一個乞丐版的Vite開始(下)

可以看到沒有重新整理頁面,但是更新了,接下來詳細解釋一下原理。

因為我們修改的是模板部分,所以請求的url為payload。path + “?type=template,這個源於上一篇文章裡我們請求Vue單檔案的模板部分是這麼設計的,為什麼要加個時間戳呢,因為不加的話瀏覽器認為這個模組已經載入過了,是不會重新請求的。

模板部分的請求結果如下:

Vite入門從手寫一個乞丐版的Vite開始(下)

匯出了一個render函式,這個其實就是HelloWorld。vue元件的渲染函式,所以我們透過module。render來獲取這個函式。

__VUE_HMR_RUNTIME__。rerender這個函式是哪裡來的呢,其實來自於Vue,Vue非生產環境的原始碼會提供一個__VUE_HMR_RUNTIME__物件,顧名思義就是用於熱更新的,有三個方法:

Vite入門從手寫一個乞丐版的Vite開始(下)

rerender就是其中一個:

function rerender(id, newRender) { const record = map。get(id); if (!record) return; Array。from(record)。forEach(instance => { if (newRender) { instance。render = newRender;// 1 } instance。renderCache = []; isHmrUpdating = true; instance。update();// 2 isHmrUpdating = false; });}

核心程式碼就是上面的1、2兩行,直接用新的渲染函式覆蓋元件舊的渲染函式,然後觸發元件更新就達到了熱更新的效果。

另外要解釋一下其中涉及到的id,需要熱更新的元件會被新增到map裡,那怎麼判斷一個元件是不是需要熱更新呢,也很簡單,給它新增一個屬性即可:

Vite入門從手寫一個乞丐版的Vite開始(下)

在mountComponent方法裡會判斷元件是否存在__hmrId屬性,存在則認為是需要進行熱更新的,那麼就新增到map裡,註冊方法如下:

Vite入門從手寫一個乞丐版的Vite開始(下)

這個__hmrId屬性需要我們手動新增,所以需要修改一下之前攔截Vue單檔案的方法:

// app。jsapp。use(async function (req, res, next) { if (/\。vue\??[^。]*$/。test(req。url)) { // vue單檔案 // 。。。 // 新增熱更新標誌 code += `\n__script。__hmrId = ${JSON。stringify(removeQuery(req。url))}`;// ++ // 匯出 code += `\nexport default __script`; // 。。。 }})

熱更新js

趁熱打鐵,接下來看一下Vue單檔案中的js部分發生了修改怎麼進行熱更新。

基本套路是一樣的,檢查兩次的js部分是否發生了修改了,修改了則向瀏覽器傳送熱更新訊息:

// app。jsconst handleVueReload = (file) => { const sendReload = () => { sendMsg({ type: ”vue-reload“, path: file, }); }; // js部分發生了改變傳送reload事件 if (!isEqualBlock(descriptor。script, prevDescriptor。script)) { return sendReload(); }}

js部分發生改變了就傳送一個vue-reload訊息,接下來修改client。js增加對這個訊息的處理邏輯:

// client。jsconst handleMessage = (payload) => { switch (payload。type) { case ”vue-reload“: loadModule({ url: payload。path + ”?t=“ + Date。now(), callback: (module) => { window。__VUE_HMR_RUNTIME__。reload(payload。path, module。default); }, }); break; }}

和模板熱更新很類似,只不過是呼叫reload方法,這個方法會稍微複雜一點:

function reload(id, newComp) { const record = map。get(id); if (!record) return; Array。from(record)。forEach(instance => { const comp = instance。type; if (!hmrDirtyComponents。has(comp)) { // 更新原元件 extend(comp, newComp); for (const key in comp) { if (!(key in newComp)) { delete comp[key]; } } // 標記為髒元件,在虛擬DOM樹patch的時候會直接替換 hmrDirtyComponents。add(comp); // 重新載入後取消標記元件 queuePostFlushCb(() => { hmrDirtyComponents。delete(comp); }); } if (instance。parent) { // 強制父例項重新渲染 queueJob(instance。parent。update); } else if (instance。appContext。reload) { // 透過createApp()裝載的根例項具有reload方法 instance。appContext。reload(); } else if (typeof window !== ‘undefined’) { window。location。reload(); } });}

透過註釋應該能大概看出來它的原理,透過強制父例項重新渲染、呼叫根例項的reload方法、透過標記為髒元件等等方式來重新渲染元件達到更新的效果。

Vite入門從手寫一個乞丐版的Vite開始(下)

style熱更新

樣式更新的情況比較多,除了修改樣式本身,還有作用域修改了、使用到了CSS變數等情況,簡單起見,我們只考慮修改了樣式本身。

根據上一篇的介紹,Vue單檔案中的樣式也是透過js型別傳送到瀏覽器,然後動態建立style標籤插入到頁面,所以我們需要能刪除之前新增的標籤,這就需要給新增的style標籤增加一個id了,修改一下上一篇文章裡我們編寫的insertStyle方法:

// app。js// css to jsconst cssToJs = (css, id) => { return ` const insertStyle = (css) => { // 刪除之前的標籤++ if (‘${id}’) { let oldEl = document。getElementById(‘${id}’) if (oldEl) document。head。removeChild(oldEl) } let el = document。createElement(‘style’) el。setAttribute(‘type’, ‘text/css’) el。id = ‘${id}’ // ++ el。innerHTML = css document。head。appendChild(el) } insertStyle(\`${css}\`) export default insertStyle `;};

給style標籤增加一個id,然後新增之前先刪除之前的標籤,接下來需要分別修改一下css的攔截邏輯增加removeQuery(req。url)作為id;以及Vue單檔案的style部分的攔截請求,增加removeQuery(req。url) + ‘-’ + index作為id,要加上index是因為一個Vue單檔案裡可能有多個style標籤。

Vite入門從手寫一個乞丐版的Vite開始(下)

接下來繼續修改handleVueReload方法:

// app。jsconst handleVueReload = (file) => { // 。。。 // style部分發生了改變 const prevStyles = prevDescriptor。styles || [] const nextStyles = descriptor。styles || [] nextStyles。forEach((_, i) => { if (!prevStyles[i] || !isEqualBlock(prevStyles[i], nextStyles[i])) { sendMsg({ type: ‘style-update’, path: `${file}?import&type=style&index=${i}`, }) } })}

遍歷新的樣式資料,根據之前的進行對比,如果某個樣式塊之前沒有或者不一樣那就傳送style-update事件,注意url需要帶上import及type=style引數,這是上一篇裡我們規定的。

client。js也要配套修改一下:

// client。jsconst handleMessage = (payload) => { switch (payload。type) { case ”style-update“: loadModule({ url: payload。path + ”&t=“ + Date。now(), }); break; }}

很簡單,加上時間戳重新載入一下樣式檔案即可。

Vite入門從手寫一個乞丐版的Vite開始(下)

不過還有個小問題,比如原來有兩個style塊,我們刪掉了一個,目前頁面上還是存在的,比如一開始存在兩個style塊:

Vite入門從手寫一個乞丐版的Vite開始(下)

刪掉第二個style塊,也就是設定背景顏色的那個:

Vite入門從手寫一個乞丐版的Vite開始(下)

可以看到還是存在,我們是透過索引來新增的,所以更新後有多少個樣式塊,就會從頭覆蓋之前已經存在的多少個樣式塊,最後多出來的是不會被刪除的,所以需要手動刪除不再需要的標籤:

// app。jsconst handleVueReload = (file) => { // 。。。 // 刪除已經被刪掉的樣式塊 prevStyles。slice(nextStyles。length)。forEach((_, i) => { sendMsg({ type: ‘style-remove’, path: file, id: `${file}-${i + nextStyles。length}` }) })}

傳送一個style-remove事件,通知頁面刪除不再需要的標籤:

// client。jsconst handleMessage = (payload) => { switch (payload。type) { case ”style-remove“: document。head。removeChild(document。getElementById(payload。id)); break; }}

Vite入門從手寫一個乞丐版的Vite開始(下)

可以看到被成功刪掉了。

普通js檔案的熱更新

最後我們來看一下非Vue單檔案,普通js檔案更新後要怎麼處理。

增加一個處理js熱更新的函式:

// app。js// 監聽檔案改變watcher。on(”change“, (file) => { if (file。endsWith(”。vue“)) { handleVueReload(file); } else if (file。endsWith(”。js“)) {// ++ handleJsReload(file);// ++ }});

普通js熱更新就需要用到前面的依賴圖資料了,如果監聽到某個js檔案修改了,先判斷它是否在依賴圖中,不是的話就不用管,是的話就遞迴獲取所有依賴它的模組,因為所有模組的最上層依賴肯定是index。html,如果只是簡單的獲取所有依賴模組再更新,那麼每次都相當於要重新整理整個頁面了,所以我們規定如果檢查到某個依賴是Vue單檔案,那麼就代表支援熱更新,否則就相當於走到死衚衕,需要重新整理整個頁面。

// 處理js檔案的熱更新const handleJsReload = (file) => { file = filePathToUrl(file); // 因為構建依賴圖的時候有些是以相對路徑引用的,而監聽獲取到的都是絕對路徑,所以稍微相容一下 let importers = getImporters(file); // 遍歷直接依賴 if (importers && importers。size > 0) { // 需要進行熱更新的模組 const hmrBoundaries = new Set(); // 遞迴依賴圖獲取要更新的模組 const hasDeadEnd = walkImportChain(importers, hmrBoundaries); const boundaries = [。。。hmrBoundaries]; // 無法熱更新,重新整理整個頁面 if (hasDeadEnd) { sendMsg({ type: ”full-reload“, }); } else { // 可以熱更新 sendMsg({ type: ”multi“,// 可能有多個模組,所以傳送一個multi型別的訊息 updates: boundaries。map((boundary) => { return { type: ”vue-reload“, path: boundary, }; }), }); } }};// 獲取模組的直接依賴模組const getImporters = (file) => { let importers = importerMap。get(file); if (!importers || importers。size <= 0) { importers = importerMap。get(”。“ + file); } return importers;};

遞迴獲取修改的js檔案的依賴模組,判斷是否支援熱更新,支援則傳送熱更新事件,否則傳送重新整理整個頁面事件,因為可能同時要更新多個模組,所以透過type=multi來標識。

看一下遞迴的方法walkImportChain:

// 遞迴遍歷依賴圖const walkImportChain = (importers, hmrBoundaries, currentChain = []) => { for (const importer of importers) { if (importer。endsWith(”。vue“)) { // 依賴是Vue單檔案那麼支援熱更新,新增到熱更新模組集合裡 hmrBoundaries。add(importer); } else { // 獲取依賴模組的再上層用來模組 let parentImpoters = getImporters(importer); if (!parentImpoters || parentImpoters。size <= 0) { // 如果沒有上層依賴了,那麼代表走到死衚衕了 return true; } else if (!currentChain。includes(importer)) { // 透過currentChain來儲存已經遍歷過的模組 // 遞迴再上層的依賴 if ( walkImportChain( parentImpoters, hmrBoundaries, currentChain。concat(importer) ) ) { return true; } } } } return false;};

邏輯很簡單,就是遞迴遇到Vue單檔案就停止,否則繼續遍歷,直到頂端,代表走到死衚衕。

最後再來修改一下client。js:

// client。jssocket。addEventListener(”message“, async ({ data }) => { const payload = JSON。parse(data); // 同時需要更新多個模組 if (payload。type === ”multi“) {// ++ payload。updates。forEach(handleMessage);// ++ } else { handleMessage(payload); }});

如果訊息型別是multi,那麼就遍歷updates列表依次呼叫處理方法:

// client。jsconst handleMessage = (payload) => { switch (payload。type) { case ”full-reload“: location。reload(); break; }}

vue-rerender事件之前已經有了,所以只需要增加一個重新整理整個頁面的方法即可。

測試一下,App。vue裡面引入一個test。js檔案:

// App。vue

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測試效果:

Vite入門從手寫一個乞丐版的Vite開始(下)

可以看到重新發送了請求,但是頁面並沒有更新,這是為什麼呢,其實還是快取問題:

Vite入門從手寫一個乞丐版的Vite開始(下)

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() : ”“}`;// ++ } // 。。。 }) // 。。。}

再來測試一下:

Vite入門從手寫一個乞丐版的Vite開始(下)

可以看到成功更新了。最後我們再來測試執行重新整理整個頁面的情況,修改一下main。js檔案即可:

Vite入門從手寫一個乞丐版的Vite開始(下)

總結

本文參考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

相關文章

頂部