極簡上手指南—手寫mini-vite開發服務器—學習vite源碼

<a name="Zktlm"></a>

# 極簡上手指南—手寫mini-vite開發服務器—學習vite源碼

參考爪哇教育-波比老師課程;

<a name="ZJhCE"></a>

## 項目中會使用到的插件:

<a name="FNtbm"></a>

### esno:

可以運行es6的代碼,nodejs的node命令只能運行cmj的代碼,遇到export、import這些會報錯。

<a name="TNYZr"></a>

### chokidar:

參考:[https://www.npmjs.com/package/chokidar](https://www.npmjs.com/package/chokidar)<br />作用,監聽文件變化。做文件熱更新需要。

<a name="v5QnR"></a>

### esbuild:

參考:[https://esbuild.github.io/getting-started/#install-esbuild](https://esbuild.github.io/getting-started/#install-esbuild)<br />作用:快速打包文件。go語言編寫,多線程。

<a name="V00Ru"></a>

## 擼一個mini-vite可以學到:

1、vite開發服務器原理<br />2、esbuild相關知識<br />3、websocket相關知識<br />4、熱更新,chokidar 包監聽文件變化

<a name="hRqyF"></a>

## 整體思路:

先去官網看看vite這東西的介紹,去窺探一下它的實現原理:

<a name="lSaEJ"></a>

### vite是什么?

[官網](https://vitejs.cn/guide/)解釋:

> Vite(法語意為 "快速的",發音 /vit/,發音同 "veet")是一種新型前端構建工具,能夠顯著提升前端開發體驗。它主要由兩部分組成:

> - 一個開發服務器,它基于 [原生 ES 模塊](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) 提供了 [豐富的內建功能](https://vitejs.cn/guide/features.html),如速度快到驚人的 [模塊熱更新(HMR)](https://vitejs.cn/guide/features.html#hot-module-replacement)。

> - 一套構建指令,它使用 [Rollup](https://rollupjs.org/) 打包你的代碼,并且它是預配置的,可輸出用于生產環境的高度優化過的靜態資源。

我們今天寫的是vite的開發服務器模塊,有個重點 “基于原生ES模塊”,需要瀏覽器是能 [在 script 標簽上支持原生 ESM](https://caniuse.com/es6-module) 和 [原生 ESM 動態導入](https://caniuse.com/es6-module-dynamic-import)。

然后去看官網的[為什么選vite](https://vitejs.cn/guide/why.html)里提到

> 1、Vite 以 [原生 ESM](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) 方式提供源碼。這實際上是讓瀏覽器接管了打包程序的部分工作:Vite 只需要在瀏覽器請求源碼時進行轉換并按需提供源碼。根據情景動態導入代碼,即只在當前屏幕上實際使用時才會被處理。

> 2、在 Vite 中,HMR 是在原生 ESM 上執行的。當編輯一個文件時,Vite 只需要精確地使已編輯的模塊與其最近的 HMR 邊界之間的鏈失活[[1]](https://vitejs.cn/guide/why.html#footnote-1)(大多數時候只是模塊本身),使得無論應用大小如何,HMR 始終能保持快速更新。

<a name="POVRg"></a>

### 大致總結一下:

vite作為一個前端開發工具,<br />和webpack一樣它構建了一個本地服務器,當你訪問時,返回相應代碼。<br />和webpack不同的是:<br />webpack會把你寫的所有文件打包成一個個的js、css、html、靜態文件,然后你才能訪問;

但vite利用瀏覽器的esm功能動態返回相應文件,啟動的時候根本不需要去打包你寫的代碼文件,你訪問的時候只返回一個入口文件index.html,然后這個文件里有個script標簽,script標簽設置type為module,這樣瀏覽器會再發一個請求去拿script標簽src對應的文件。然后vite服務器根據這個請求返回相應文件,因為都是靜態文件,沒有什么邏輯交互,所以速度非常快。

那么還有個問題,我寫的是jsx文件,瀏覽器不認識啊!沒事vite服務器會在請求jsx文件的時候,通過esbuild去把jsx轉換成js文件,返回給瀏覽器。esbuild用go語言寫的,速度快到驚人。

<a name="X3mom"></a>

### react工程為例,寫一個mini-vite服務:

我們以react為例,最終需要在客戶端拿到html文件里,包含react里的相關文件。所以我們要先了解如何創建react-app,參考:[http://www.lxweimin.com/p/68e849768d8e](http://www.lxweimin.com/p/68e849768d8e)<br />簡單的來說要想瀏覽器運行你寫的react代碼,需要三個包:

- react包——負責react的核心邏輯

- react-dom包——負責瀏覽器的dom操作

- babel包——轉義jsx語法。(在script中加 type="text/babel",就可以在script中寫jsx)

如我們直接在html文件中引入這三個包后,就可以快樂的使用react了。

```jsx

<!DOCTYPE html>

<html lang="en">

? <head>

? ? <meta charset="UTF-8" />

? ? <title>React</title>

? ? <!-- 引入react -->

? ? <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>

? ? <!-- 引入react-dom -->

? ? <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>

? ? <!-- 引入Babel,使瀏覽器可以識別JSX語法,如果不使用JSX語法,可以不引入 -->

? ? <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

? </head>


? <body>

? ? <div id="app"></div>

? ? <script type="text/babel">

? ? ? // 必須添加type="text/babel",否則不識別JSX語法

? ? ? class App extends React.Component {

? ? ? ? render() {

? ? ? ? ? return (

? ? ? ? ? ? <div>

? ? ? ? ? ? ? <h1>Hello World</h1>

? ? ? ? ? ? </div>

? ? ? ? ? );

? ? ? ? }

? ? ? }

? ? ? ReactDOM.render(<App />, document.getElementById("app"));

? ? </script>

? </body>

</html>

```

而我們建立的項目中一般會在index.html中引入main.js文件,main.js中引入react的兩個包,并替換html中的root節點,這里我們需要清楚怎么main.js中的import react 是引入哪個文件,然后才好打包發給瀏覽器(這個后面寫的時候再詳細說)。<br />教程中我們使用koa建立本地服務器攔截文件中的import請求,返回給他經過esbuild轉換的文件<br />注意可以把esbuild轉換的文件分為兩種:

- 一種是基本不會變的文件,如react、babel等第三方包

- 一種是經常會變的,如我們自己寫的代碼文件

所以,對于不會變的文件我們應該實現編譯后,并設置緩存,而對于自己寫的代碼文件就每次都實時編譯,因為esbuild的速度非常快,且每個包都是單獨的模塊引入,所以會比webpack快很多。

<a name="V7vWL"></a>

## 實踐:

<a name="D2umZ"></a>

### 先建立一個基礎文件目錄如下:


<br />文件說明:<br />dev.command.js 和prett.command.js這兩個文件是為了啟動它對應的文件的。<br />index.html 是我們要返回給瀏覽器的文件,內容如下:

```html

<!DOCTYPE html>

<html lang="en">

? ? <head>

? ? ? ? <meta charset="UTF-8" />

? ? ? ? <meta http-equiv="X-UA-Compatible" content="IE=edge" />

? ? ? ? <meta name="viewport" content="width=device-width, initial-scale=1.0" />

? ? ? ? <title>react</title>

? ? </head>

? ? <body>

? ? ? ? <div id="root"></div>

? ? ? ? <script type="module" src="/target/main.jsx"></script>

? ? </body>

</html>

```

<a name="CO4i6"></a>

### 建立koa服務器(也可以用express):

```javascript

import Koa from "koa";

import koaRouter from "koa-router";

import fs from "fs";

import path from "path";

export async function dev() {

? ? console.log("div....");

? ? const app = new Koa();

? ? const router = new koaRouter();

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

? ? ? ? console.log("有請求:", ctx.request.url);

? ? ? ? next();

? ? ? ? console.log("請求over:", ctx.body);

? ? });

? ? // 根目錄請求返回html

? ? router.get("/", (ctx) => {

? ? ? ? let htmlPath = path.join(__dirname, "../target/index.html");

? ? ? ? let html = fs.readFileSync(htmlPath);

? ? ? ? ctx.set("Content-Type", "text/html");

? ? ? ? ctx.body = html;

? ? });

? ? app.use(router.routes());

? ? app.listen(3030, () => {

? ? ? ? console.log("app is listen 3030");

? ? });

}

```

這時候我們可以 使用 `esno src/dev.command.js`啟動一下服務器(因為我們語法中使用了ems模塊,所以不能用node命令去啟動,node只能啟動cmj模塊的文件。并且命令行不支持esno,所以把這句指令寫到package.json文件的script標簽,用npm run 的方式啟動)

<a name="wgtYc"></a>

### esbuild轉換jsx文件:

這時候瀏覽器中訪問發現可以得到對應的html文件,但是html文件中引入了 ` <script type="module" src="/target/main.jsx"></script>`這個main.jsx是我們編寫的文件,內容如下:

```jsx

import React from 'react'

import ReactDOM from 'react-dom'

import './index.css'

import App from './App.jsx'

ReactDOM.render(

? <React.StrictMode>

? ? <App />

? </React.StrictMode>,

? document.getElementById('root')

)

```

但是瀏覽器不能直接使用jsx文件,所以我們把jsx文件用esbuild轉換后再給瀏覽器。<br />使用esbuild中的 transformSync函數,實例代碼如下:

```javascript

require('esbuild').transformSync('<>x</>', {

? jsxFragment: 'Fragment', // 返回空節點

? loader: 'jsx',

})

{

? code: '/* @__PURE__ */ React.createElement(Fragment, null, "x");\n',

? map: '',

? warnings: []

}

```

轉換函數:

```javascript

import esbuild from "esbuild";

function transformCode(tranObj) {

? ? return esbuild.transformSync(tranObj.code, {

? ? ? ? loader: tranObj.loader || "js",

? ? ? ? format: "esm",

? ? ? ? sourcemap: true,

? ? });

}

export function transformJSX(opts) {

? ? let tranObj = { code: opts.code };

? ? tranObj.loader = "jsx";

? ? let res = transformCode(tranObj);

? ? return res;

}

```

把轉換后的文件返給瀏覽器:

```javascript

// html文件中有 /target/main.jsx ,返回我們處理過的main.jsx文件

? ? router.get("/target/main.jsx", (ctx) => {

? ? ? ? let filePath = path.join(__dirname, "../target/main.jsx");

? ? ? ? let fileText = fs.readFileSync(filePath, "utf-8");

? ? ? ? ctx.set("Content-Type", "application/javascript");

? ? ? ? ctx.body = transformJSX({

? ? ? ? ? ? code: fileText,

? ? ? ? }).code;

? ? });

```

<a name="JH0Wu"></a>

### 緩存第三方包:

此時會發現瀏覽器報錯:<br />Uncaught TypeError: Failed to resolve module specifier "react". Relative references must start with either "/", "./", or "../".<br />這個是因為我們返回的js文件中第一行 import 了一個 react但是路徑不對,應該以'/'開頭。所以下一步我們要處理導入的包。所以我們可以簡單的看import里 如果 不是以'/' 或 './'開頭的,那么把它當作第三方包,把第三方包做成緩存。<br />并且jsx等文件中引入第三方包的import 路徑,我們要轉換成我們設置的緩存路徑后再返給瀏覽器。<br />轉換import 路徑:

```javascript

import esbuild from "esbuild";

import path, { dirname } from "path";

import fs from "fs";

function transformCode(tranObj) {

? ? return esbuild.transformSync(tranObj.code, {

? ? ? ? loader: tranObj.loader || "js",

? ? ? ? format: "esm",

? ? ? ? sourcemap: true,

? ? });

}

export function transformJSX(opts) {

? ? let tranObj = { code: opts.code };

? ? tranObj.loader = "jsx";

? ? let res = transformCode(tranObj);

? ? let { code } = res;

? ? // 分析代碼字符串的 import

? ? // 為啥要分析 import 呢?

? ? // import type { XXXX } from 'xxx.ts';

? ? // import React from 'react';

? ? // 下面的正則取出 from 后面的 "react", 然后通過有沒有 "." 判斷是引用的本地文件還是三方庫

? ? // 本地文件就拼路徑

? ? // 三方庫就從我們預先編譯的緩存里面取

? ? code = code.replace(/\bimport(?!\s+type)(?:[\w*{}\n\r\t, ]+from\s*)?\s*("([^"]+)"|'([^']+)')/gm, (a, b, c) => {

? ? ? ? console.log("正則匹配:", a, "-------", b, "-------", c);

? ? ? ? let fromPath = "";

? ? ? ? // 以'.'開頭當作本地文件

? ? ? ? if (c.charAt(0) === ".") {

? ? ? ? ? ? let filePath = path.join(opts.rootPath, c);

? ? ? ? ? ? console.log("filePath", filePath, path.dirname(opts.path), fromPath);

? ? ? ? ? ? if (fs.existsSync(filePath)) {

? ? ? ? ? ? ? ? fromPath = path.join(path.dirname(opts.path), c);

? ? ? ? ? ? ? ? fromPath = fromPath.replace(/\\/g, "/");

? ? ? ? ? ? ? ? return a.replace(b, `"${fromPath}"`);

? ? ? ? ? ? }

? ? ? ? } else {

? ? ? ? ? // todo 對第三方庫的文件從緩存里拿

? ? ? ? }

? ? ? ? return a;

? ? });

? ? return { ...res, code };

}

```

注意:這個正則表達式 `/\bimport(?!\s+type)(?:[\w*{}\n\r\t, ]+from\s*)?\s*("([^"]+)"|'([^']+)')/gm`的分成三個組a,b,c;a是import那一整行代碼,b是帶引號的包名稱,c是不帶引號的包名稱。

上一步我們改寫了文件的import,讓我們服務器可以根據請求路徑返回相應文件。接下來在pretreatment.js文件中對第三方包進行處理,首先我們需要在項目啟動的時候把第三方包緩存起來。<br />緩存第三方包:

```javascript

import { build } from "esbuild";

import { join } from "path";

const appRoot = join(__dirname, "..");

const cache = join(appRoot, "target", ".cache");

export async function pretreatment(pkgs = ["react", "react-dom"]) {

? ? console.log("pretreatment");

? ? let entrys = pkgs.map((item) => {

? ? ? ? return join(appRoot, "node_modules", item, "cjs", `${item}.development.js`);

? ? });

? ? build({

? ? ? ? entryPoints: entrys,

? ? ? ? bundle: true,

? ? ? ? sourcemap: true,

? ? ? ? treeShaking: true,

? ? ? ? outdir: cache,

? ? ? ? splitting: true,

? ? ? ? logLevel: "error",

? ? ? ? metafile: true,

? ? ? ? format: "esm",

? ? });

}

```

注意:entry的路徑就是你本地安裝包的路徑;<br />運行pretreatment函數就可以得到緩存包了。<br />我們可以在package.json文件的script標簽中 寫成:`"dev": "esno src/prett.command.js && esno src/dev.command.js"`<br />.cache文件夾下出現兩個包:<br />

<br />接下來返回到transformJSX函數里處理第三方包;

```javascript

? ? ? ? // 以'.'開頭當作本地文件

? ? ? ? if (c.charAt(0) === ".") {

? ? ? ? ? ? let filePath = path.join(opts.rootPath, c);

? ? ? ? ? ? console.log("filePath", filePath, path.dirname(opts.path), fromPath, path.dirname("/aa/bb/cc/dd.js"));

? ? ? ? ? ? if (fs.existsSync(filePath)) {

? ? ? ? ? ? ? ? fromPath = path.join(path.dirname(opts.path), c);

? ? ? ? ? ? ? ? fromPath = fromPath.replace(/\\/g, "/");

? ? ? ? ? ? ? ? return a.replace(b, `"${fromPath}"`);

? ? ? ? ? ? }

? ? ? ? } else { // ============== 新增 從緩存里拿文件,避免再次打包

? ? ? ? ? ? let filePath = path.join(opts.rootPath, `.cache/${c}/cjs/${c}.development.js`);

? ? ? ? ? ? if (fs.existsSync(filePath)) {

? ? ? ? ? ? ? ? fromPath = path.join(dirname(opts.path), `.cache/${c}/cjs/${c}.development.js`);

? ? ? ? ? ? ? ? fromPath = fromPath.replace(/\\/g, "/");

? ? ? ? ? ? ? ? return a.replace(b, `"${fromPath}"`);

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? return a;

```

接下來再處理一下css和svg文件就可以了。<br />這時候又發現一個重大的問題:<br />我們的請求都成功了,如下圖:<br />

<br />但是界面卻沒有任何顯示,看下頁面結構,id 為 root 的div元素沒有變化,說明react腳本沒起作用,沒能替換我們的root元素。問題出在哪里呢?我們去看看有沒有報錯,發現:<br />

<br />發現這個獲取logo.svg的請求報了這個錯。意思我想要js的文件你卻給我一個svg的文件。等等,也就是說js腳本里的import在瀏覽器中都是只能引入其他js腳本的。所以我們這里不需要把靜態文件直接導過來,而是轉換成一個路徑,那個在用到的地方自然會被導入。如導入時返回這個:`export default "/target/logo.svg"` 。<br />logo.svg 使用的地方是 ` <imgsrc={logo}className="App-logo"alt="logo"/>`那么在使用的地方會請求這個路徑拿到真正的文件。

所以我們需要把 所有導入靜態文件的地方都加上一個標識符,然后import發請求請求這個文件時,我們返回的Content-Type寫 appliction/javascript ,然后文件里就一句話 export default "文件路徑" ;<br />然后文件被使用時瀏覽器就會再發一個請求,去請求這個文件,再請求里判斷一下靜態文件走靜態文件的處理。

代碼里做兩處更改:<br />transform函數:

```javascript

if (fs.existsSync(filePath)) {

? ? fromPath = path.join(path.dirname(opts.path), c);

? ? fromPath = fromPath.replace(/\\/g, "/");

? ? // svg等靜態文件加個標志 后面拼url參數===新增

? ? if (["svg"].includes(path.extname(fromPath).slice(1))) {

? ? ? ? fromPath += "?import";

? ? }

? ? return a.replace(b, `"${fromPath}"`);

}

```

dev的target請求里:

```javascript

router.get("/target/(.*)", (ctx) => {

? ? ? ? let filePath = path.join(__dirname, "..", ctx.path.slice(1));

? ? ? ? let rootPath = path.join(__dirname, "../target");

? ? ? ? console.log("target 請求 file path:", filePath);

? ? ? ? // query 中有import標識的就是靜態資源====新增

? ? ? ? if ("import" in ctx.query) {

? ? ? ? ? ? ctx.set("Content-Type", "application/javascript");

? ? ? ? ? ? ctx.body = `export default "${ctx.path}"`;

? ? ? ? ? ? return;

? ? ? ? }

```

這時候我們終于可以看到頁面了。

<a name="jkqNZ"></a>

### 建立websocket鏈接:

這時候發現頁面樣式css文件是沒起作用的,我們先放一放,先建一個websocket鏈接方便后續處理css文件和熱更新

建立websocket分兩步一個是客戶端,一個是服務器。<br />客戶端使用原生WebScoket類,服務器使用ws庫創建WebSocket服務。<br />客戶端的webScoket文件我們命名為client.js,在index.html中加入 script標簽,src為"@/vite/client",去請求 拿到。

<a name="k9AfN"></a>

#### 請求@/vite/client:

```javascript

? ? // 把客戶端代碼塞給瀏覽器,給 html

? ? router.get("/@vite/client", (ctx) => {

? ? ? ? console.log("get vite client");

? ? ? ? ctx.set("Content-Type", "application/javascript");

? ? ? ? ctx.body = transformCode({

? ? ? ? ? ? code: fs.readFileSync(path.join(__dirname, "client.js"), "utf-8"),

? ? ? ? }).code;

? ? ? ? // 這里返回的才是真正的內置的客戶端代碼

? ? });

```

<a name="s0mSQ"></a>

#### client.js函數:

```javascript

let host = location.host;

console.log("vite client:", host);

// Create WebSocket connection.

const socket = new WebSocket(`ws://${host}`, "vite-hmr");

// Connection opened

socket.addEventListener("open", function (event) {

? ? socket.send("Hello Server!");

});

// Listen for messages

socket.addEventListener("message", function (event) {

? ? handleServerMessage(event.data);

});

function handleServerMessage(payLoad) {

? ? let msg = JSON.parse(payLoad);

? ? console.log("Message from server ", payLoad, "====", msg);

? ? switch (msg.type) {

? ? ? ? case "connected": {

? ? ? ? ? ? console.log("vite websocket connected");

? ? ? ? ? ? setInterval(() => {

? ? ? ? ? ? ? ? socket.send("ping");

? ? ? ? ? ? }, 20000);

? ? ? ? ? ? break;

? ? ? ? }

? ? ? ? case "update": {

? ? ? ? ? ? console.log("Message update ", msg, msg.updates);

? ? ? ? ? ? msg.updates.forEach(async (update) => {

? ? ? ? ? ? ? ? if (update.type === "js-update") {

? ? ? ? ? ? ? ? ? ? console.log("[vite] js update....");

? ? ? ? ? ? ? ? ? ? await import(`/target/${update.path}?t=`);

? ? ? ? ? ? ? ? ? ? // 在這里應該是要只更新變成模塊的,不應該全部重新加載。

? ? ? ? ? ? ? ? ? ? // vite源碼這里是調用了一個queueUpdate函數

? ? ? ? ? ? ? ? ? ? location.reload();

? ? ? ? ? ? ? ? }

? ? ? ? ? ? });

? ? ? ? ? ? break;

? ? ? ? }

? ? }

? ? if (msg.type == "update") {

? ? }

}

// 封裝一些操作 css 的工具方法,因為 client 是放 html 里的,可以導出來給其它模塊使用

const sheetsMap = new Map();

// id 是css文件的絕對路徑, content是css文件的內容

export function updateStyle(id, content) {

? ? let style = sheetsMap.get(id);

? ? if (!style) {

? ? ? ? style = document.createElement("style");

? ? ? ? style.setAttribute("type", "text/css");

? ? ? ? style.innerHTML = content;

? ? ? ? document.head.appendChild(style);

? ? } else {

? ? ? ? style.innerHTML = content;

? ? }

? ? sheetsMap.set(id, style);

}

```

<a name="hfONj"></a>

#### dev.js中加入websocket服務:

```javascript

function createWebSocketServer(httpServer) {

? ? console.log("create web server:");

? ? const wss = new WebSocketServer({ noServer: true });

? ? wss.on("connection", (socket) => {

? ? ? ? console.log("connected ===");

? ? ? ? socket.send(JSON.stringify({ type: "connected" }));

? ? ? ? socket.on("message", handleSocketMsg);

? ? });

? ? wss.on("error", (socket) => {

? ? ? ? console.error("ws connect error", socket);

? ? });

? ? httpServer.on("upgrade", function upgrade(req, socket, head) {

? ? ? ? if (req.headers["sec-websocket-protocol"] == "vite-hmr") {

? ? ? ? ? ? console.log("upgrade", Object.keys(req.headers));

? ? ? ? ? ? wss.handleUpgrade(req, socket, head, (ws) => {

? ? ? ? ? ? ? ? wss.emit("connection", ws, req);

? ? ? ? ? ? });

? ? ? ? }

? ? });

? ? return {

? ? ? ? send(payLoad) {

? ? ? ? ? ? let sendMsg = JSON.stringify(payLoad);

? ? ? ? ? ? wss.clients.forEach((client) => {

? ? ? ? ? ? ? ? client.send(sendMsg);

? ? ? ? ? ? });

? ? ? ? },

? ? ? ? close() {

? ? ? ? ? ? console.log("close websocket");

? ? ? ? ? ? wss.close();

? ? ? ? },

? ? };

}

function handleSocketMsg(data) {

? ? console.log("received: %s", data);

}

```

運行createWebSocketServer函數可以得到 一個 websocket的服務實例。<br />我們在dev.js中調用:

```javascript

? ? // 使用原生的http.createServer 獲取http.Server實例

? ? // 因為ws庫,是基于這個實例去升級http協議成wesocket服務的

? ? // 因為我們使用的是koa,所以用app.callback函數獲取一個使用于httpServer 處理請求的函數

? ? let httpServer = http.createServer(app.callback());

? ? // eslint-disable-next-line no-unused-vars

? ? const ws = createWebSocketServer(httpServer);

? ? httpServer.listen(3030, () => {

? ? ? ? console.log("app is listen 3030");

? ? });

```

這樣我們就可以獲得一個websocket的鏈接,我們可以在client里寫一些公共函數用來進行一些 前端的操作。比如收到請求后 更新某個css文件。

<a name="xnKrW"></a>

### 處理css文件:

處理css文件,想想看html文件中是怎么加載css的,有兩種方法:<br />1、是link標簽指定href為文件地址,ref為stylesheet;<br />2、使用style標簽,里面直接寫css文件內容。<br />兩種方法我們都需要去創建一個標簽。那么問題來了,我們怎么在發送過去的html里面創建標簽呢? <br />因為html文件是第一個發送給瀏覽器的,后續的文件都是html標簽里的遞歸請求過去的。

所以為了解決這個問題,我們可以使用websocket,發給瀏覽器的html文件里加個script標簽,在里面起一個websocket的客戶端,然后這里面可以通過document來新建style標簽,加入css樣式。<br />websocket客戶端,上一步講過了,這一步將怎么更新css;

transform中添加:

```javascript

export function transformCss(opts) {

? ? // let filePath = path.join(opts.rootPath, "..", opts.path);

? ? // console.log("css path:", path.join(opts.rootPath, "..", opts.path));

? ? // css文件使用 在 client.js 中的updateStyle函數來創建style標簽 加入css的內容

? ? return `

? ? ? ? import { updateStyle } from '/@vite/client'

? ? ? ? const id = "${opts.path}";

? ? ? ? const css = "${opts.code.replace(/"/g, "'").replace(/\n/g, "")}";

? ? ? ? updateStyle(id, css);

? ? ? ? export default css;

? ? `.trim();

}

```

client.js文件中添加

```javascript

// 封裝一些操作 css 的工具方法,因為 client 是放 html 里的,可以導出來給其它模塊使用

const sheetsMap = new Map();

// id 是css文件的絕對路徑, content是css文件的內容

export function updateStyle(id, content) {

? ? let style = sheetsMap.get(id);

? ? if (!style) {

? ? ? ? style = document.createElement("style");

? ? ? ? style.setAttribute("type", "text/css");

? ? ? ? style.innerHTML = content;

? ? ? ? document.head.appendChild(style);

? ? } else {

? ? ? ? style.innerHTML = content;

? ? }

? ? sheetsMap.set(id, style);

}

```

dev中添加:

```javascript

switch (path.extname(ctx.url)) {

case ".svg":

? ? ctx.set("Content-Type", "image/svg+xml");

? ? ctx.body = fs.readFileSync(filePath, "utf-8");

? ? break;

case ".css": //====== 新增

? ? ctx.set("Content-Type", "application/javascript");

? ? ctx.body = transformCss({

? ? ? ? code: fs.readFileSync(filePath, "utf-8"),

? ? ? ? path: ctx.path,

? ? ? ? rootPath,

? ? });

? ? break;

```

簡單總結一下,這一步我們的操作使得有css文件引入的地方,就會發請求給服務器,服務器返回一個js文件,這個js文件里引入了client.js中的updateStyle函數,同時把css文件的內容和文件位置傳給這個函數。然后updateStyle函數里,根據這個位置判斷有沒有對應style標簽,有則更新,無則生成。

<a name="tIRLl"></a>

### 熱更新:

熱更新需要監聽文件變化,這個我們使用 chokidar<br />npm:[https://www.npmjs.com/package/chokidar](https://www.npmjs.com/package/chokidar)<br />思路就是監聽文件變化后,把變化的文件名做處理(就是把文件的絕對地址轉換成瀏覽器里請求的相對地址),通過websocket告訴前端,那個模塊變更了,讓他重新導入。

<a name="W5Ovh"></a>

#### 監聽文件變更:

dev.js中添加

```javascript

// 監聽文件變更

function watch() {

? ? return chokidar.watch(targetRootPath, {

? ? ? ? ignored: ["**/node_modules/**", "**/.cache/**"],

? ? ? ? ignoreInitial: true,

? ? ? ? ignorePermissionErrors: true,

? ? ? ? disableGlobbing: true,

? ? });

}

```

dev函數里添加watch:

```javascript

? ? let httpServer = http.createServer(app.callback());

? ? // eslint-disable-next-line no-unused-vars

? ? const ws = createWebSocketServer(httpServer);

? ? // 監聽文件變更 ======== 新增

? ? watch().on("change", (filePath) => {

? ? ? ? console.log("file is change", filePath, targetRootPath);

? ? ? ? handleHMRUpdate(ws, filePath);

? ? });

? ? httpServer.listen(3030, () => {

? ? ? ? console.log("app is listen 3030");

? ? });

```

處理熱更新的函數:

```javascript

function getShortName(filePath, root) {

? ? return `${filePath.replace(root, "").replace(/\\/g, "/")}`;

? ? // return path.extname(filePath);

}

// 處理文件更新

function handleHMRUpdate(ws, filePath) {

? ? // let file = fs.readFileSync(filePath);

? ? const shortFile = getShortName(filePath, targetRootPath);

? ? console.log("short file:", shortFile);

? ? let updates = [

? ? ? ? {

? ? ? ? ? ? type: "js-update",

? ? ? ? ? ? path: `/${shortFile}`,

? ? ? ? },

? ? ];

? ? let sendMsg = {

? ? ? ? type: "update",

? ? ? ? updates,

? ? };

? ? ws.send(sendMsg);

}

```

<a name="VxFKl"></a>

#### client.js中處理文件變更:

```javascript

function handleServerMessage(payLoad) {

? ? let msg = JSON.parse(payLoad);

? ? console.log("Message from server ", payLoad, "====", msg);

? ? switch (msg.type) {

? ? ? ? case "connected": {

? ? ? ? ? ? console.log("vite websocket connected");

? ? ? ? ? ? setInterval(() => {

? ? ? ? ? ? ? ? socket.send("ping");

? ? ? ? ? ? }, 20000);

? ? ? ? ? ? break;

? ? ? ? }

? ? ? ? case "update": { // ============= 新增,消息類型是要更新文件

? ? ? ? ? ? console.log("Message update ", msg, msg.updates);

? ? ? ? ? ? msg.updates.forEach(async (update) => {

? ? ? ? ? ? ? ? if (update.type === "js-update") {

? ? ? ? ? ? ? ? ? ? console.log("[vite] js update....");

? ? ? ? ? ? ? ? ? ? await import(`/target/${update.path}?t=`);

? ? ? ? ? ? ? ? ? ? // 在這里應該是要只更新變成模塊的,不應該全部重新加載。

? ? ? ? ? ? ? ? ? ? // vite源碼這里是調用了一個queueUpdate函數

? ? ? ? ? ? ? ? ? ? location.reload();

? ? ? ? ? ? ? ? }

? ? ? ? ? ? });

? ? ? ? ? ? break;

? ? ? ? }

? ? }

}

```

<a name="T3MBO"></a>

## 總結:

至此,我們就實現了mini-vite,大致思路和源碼是差不多的,但是一些細節處理的地方還沒有做,比如每個模塊的單獨渲染、簡單的用是不是"."開頭來判斷是不是第三方包,對是否靜態文件判斷不夠全面。<br />項目整體上理解一下vite設計的思路,還是可以的,還能學一下esbuild和webpack。

完整項目地址:[https://gitee.com/zyl-ll/vite_source_code.git](https://gitee.com/zyl-ll/vite_source_code.git)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容