如何實現一個簡易版vite

相信點進來看文章的都對vite有一定的了解,下面我們一起來實現一個簡易版vite,來加深我們對vite的理解!(PS:建議 clone 項目下來對著看)

完整代碼

開始之前我們先來 預覽效果,啟動了vite服務,用官方vite的react示例代碼作為演示,修改 jsxcss 都進行了熱更新。

0. 先搭建基礎服務


首先,需要有一個服務讓我們去訪問項目,新建一個 index.js 文件,用 koa 創建一個http服務。

// index.js
import Koa from 'koa';

const app = new Koa();

// 運行
(function createServer() {
  app.listen(12345, () => {
    console.log(`listening:12345 port!!!`);
  });
})();

ok, 服務建好。

下面我們來創建 ws 服務。因為我們需要hmr, 當你的項目文件發生改變后,通過 ws 去通知客戶端:“喂,你的文件發生改變了,該去更新了!”

// index.js
import { WebSocketServer } from 'ws';

function createWs() {
  const Websocket = new WebSocketServer({ port: 5000 });
  Websocket.on('connection', function connection(ws) {
    console.log('ws is connected!!');
    ws.on('message', function incoming(message) {
      console.log('received: %s', message);
    });
  });
}

ok,很簡單是是吧。

1. hmr


開發怎么少得了 hmr 呢?
這里的 hmr 主要做一個通知工作,通過 chokidar 去監聽文件的變化,然后通過 ws 通知客戶端去進行更新操作,所以更新操作是在客戶端進行,下面會講到。

// 發起熱更新
const launchHmr = (ws, config) => {
  // 監聽到文件變化,處理文件
  chokidar.watch(resolveOnRoot('src')).on('change', (changePath) => {
    changePath = changePath.replace(__dirname, '');
    console.log(`${changePath} has changed!!`);
    const cssObj = {};
    if (changePath.endsWith('.css')) {
      cssObj.css = fs.readFileSync(resolveOnRoot(changePath)).toString();
    }
    // 將更新類型、文件路徑、樣式內容傳給客戶端
    ws.send(
      JSON.stringify({
        type: 'change',
        changePath,
        ...cssObj,
      }),
    );
  });
};

2. 中間件


vite源碼里面有14個中間件,用來處理請求,轉換資源,處理配置、日志等。我這個簡易版不會包含這么多,我主要寫了3個,分別是 baseMiddlewareindexHtmlMiddlewaretransformMiddleware,下面進行逐個講解。

  • baseMiddleware 處理請求資源的地址,用來去掉請求時帶上的參數,掛載在ctx對象上面
// 處理地址
export const baseMiddleware = (ctx, next) => {
  const requestUrl = ctx.request.url.split('?')[0];
  ctx.requestUrl = requestUrl;
  next();
};
  • indexHtmlMiddleware 處理入口文件
    因為我們手寫 vite 運行起來訪問的是 localhost: port, 所以這里默認為請求 / 為 html入口資源。(PS:這里沒有其他摻雜路由,這個簡單的列子單純讓你加深對 vite 的理解)
    說一說 下面的代碼,當用戶請求入口資源時候,服務端會找到入口文件,讀取文件內容,在文件中插入處理樣式(client/style.js)、處理websocket (client/ws.js)和react熱更新(react-refresh)這幾個資源,再返回處理后的 html 資源,資源具體干了什么下面會說,莫急。
export const indexHtmlMiddleware = (ctx, next) => {
  if (ctx.requestUrl === '/') {
    // 根路徑返回模版 HTML 文件
    const html = fs.readFileSync(`${__dirname}/index.html`, 'utf-8');
    const importHandleStyle =
      '<script type="module" src="/@vite/client/style.js"></script>';
    const importHandleWs =
      '<script type="module" src="/@vite/client/ws.js"></script>';
    const preambleCode = `
    <script type="module">
      import RefreshRuntime from "/@modules/react-refresh"
      RefreshRuntime.injectIntoGlobalHook(window)
      window.$RefreshReg$ = () => {}
      window.$RefreshSig$ = () => (type) => type
      window.__vite_plugin_react_preamble_installed__ = true
    </script>`;
    ctx.type = 'text/html';
    ctx.body = `${preambleCode}${importHandleStyle}${importHandleWs}${html}`;
  }
  next();
};
  • transformMiddleware 處理非html資源,既然上面處理了入口html資源,當然也要處理別的資源啦。
    可以看到下面代碼是通過判斷資源請求的后綴或前綴,對各種資源進行處理。分別包含 jsxnode_modules資源csssvgvite客戶端 資源。為方便大家看, 我這里已經將方法單獨抽了出來,大家可以看看 utils.js 文件。????傳送門??

簡單說說每個處理資源的方法
handleJsx:esbuild 對jsx進行轉換,之后再通過封裝 react-refresh插件對jsx文件插入熱更新內容(參考 @vite/plugin-react-refresh)
handleNodeModules: 處理第三方資源,通過 enbuild 對第三方資源進行打包,打包后放到緩存目錄 node_modules/.cvite,下次訪問就直接取 node_modules/.cvite中的資源。
handleCss:讀取樣式文件內容,通過標簽方式插入樣式,插入樣式時候記錄。
handleSvg:轉base64。
⑤ 最后一個通過判斷/@vite/client,找到客戶端資源的路徑,返回對應的請求資源。

export const transformMiddleware = (ctx, next) => {
  const { requestUrl } = ctx;
  if (requestUrl === '/') {
  } else if (requestUrl.endsWith('.jsx')) {
    handleJsx(ctx, requestUrl);
  } else if (requestUrl.startsWith('/@modules/')) {
    handleNodeModules(ctx, requestUrl);
  } else if (requestUrl.endsWith('.css')) {
    handleCss(ctx, requestUrl);
  } else if (requestUrl.endsWith('.svg')) {
    handleSvg(ctx, requestUrl);
  } else if (requestUrl.startsWith('/@vite/client')) {
    ctx.type = 'application/javascript';
    const filePath = requestUrl.replace('/@vite/client', 'vite/client');
    ctx.body = fs.readFileSync(resolveOnRoot(filePath), 'utf8');
  } else {
  }
  next();
};
好了,說完中間件了,最終我們啟動方法為
// 運行
(function createServer() {
  createWs();
  app.listen(12345, () => {
    console.log(`listening:${12345} port!!!`);
  });
  app.use(baseMiddleware);
  app.use(indexHtmlMiddleware);
  app.use(transformMiddleware);
})();

3. 再說hmr


接著,我們再補充剛才沒有細講的熱更新。

剛才說了,服務端通過 ws 去通知客戶端進行熱更新,客戶端收到指令后會執行 handleFile 方法。

export function handleFile(path, css) {
  const { JSX, CSS, SVG, NODE_MODULE } = FILE_TYPE;
  if (checkFileType(path) === CSS) {
    // 處理樣式 
    globalUpdateStyle(path, css);
  } else if (checkFileType(path) === JSX) {
    // 處理jsx
    import(`${path}?${timeParam()}`);
  }
  • css熱更新:上面說了,樣式是通過 style標簽 插入的,那我們需要維護一個 Map window.sheetsMap = new Map();, 在插入樣式同時,我們去設置 Map window.sheetsMap.set(id, style); 文件路徑作為 id,css內容作為 style。
    這樣,當客戶端收到更新 css 的指令后,就會調用 globalUpdateStyle(path, css); 去更新樣式資源,實現樣式熱更新。而 globalUpdateStyle方法在 /@vite/client/style.js 文件里面,這個我們在執行 indexHtmlMiddleware 中間件已經加載了該資源的。
export const globalUpdateStyle = (id, content) => {
  let style = window.sheetsMap.get(id);
  if (style) {
    style.innerHTML = content;
  } else {
    style = document.createElement('style');
    style.setAttribute('type', 'text/css');
    style.innerHTML = content;
    document.head.appendChild(style);
  }
  window.sheetsMap.set(id, style);
};
  • react熱更新:react熱更新用了react-fresh,在看 vite 源碼時候,發現 vite.config.js 文件里面使用了 @vitejs/plugin-react-refresh,而 vite 在處理 react jsx 時候會調用 transform 方法處理文件。
    下面簡單說下,transform 做了些什么。
    ① 用 babelreact-refresh/babel 插件對代碼進行轉換
// 轉換前
function Text1() {
  return <div>Test Component1</div>;
}
export default Text1;
// 轉換后
function Text1() {
  return /* @__PURE__ */React.createElement("div", null, "Test Component1");
}
_c = Text1;
export default Text1;
var _c;
// 幫我們注冊需要熱更新的組件
$RefreshReg$(_c, "Text1");

② 在文件頭部插入提供注冊熱更新的方法。

  const header = `
    ...
    window.$RefreshReg$ = (type, id) => {
      RefreshRuntime.register(type, ${JSON.stringify(id)} + " " + id)
    };
    ...
    `;

③ 在文件尾部插入刷新組件的方法。

const footer = `
  ...
  window.__vite_plugin_react_timeout = setTimeout(() => {
    window.__vite_plugin_react_timeout = 0;
    RefreshRuntime.performReactRefresh();
  }, 30);
  ...
 `;

ok,處理完畢,當客戶端收到更新 jsx 的指令后,會重新 import 對應的 jsx 文件,加上時間戳參數,確保是新的,然后文件執行RefreshRuntime.performReactRefresh(); 進行刷新。

4.錦上添花


主線流程就說完了,你可以做些錦上添花的內容,例如實現讀取vite.config.json文件的配置,嘗試ssr,資源緩存等等讓他看起來更高級。

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

推薦閱讀更多精彩內容