下一代前端開發利器——Vite(原理源碼解析)

本文作者是 360 奇舞團前端開發工程師

前言

Hi,大家好!

前段時間用Vue3搭建項目時看到同時推出的Vite,只當它是一個新打包工具或者vue-cli的升級版,仍然選擇了用Webpack構建項目。最近看了尤雨溪在 VueConf 上的演講視頻:《Vue3 生態進展和計劃》[1],感覺它確實解決了現階段前端工程化的一些痛點,也能體會到尤雨溪對Vite的重視和大力推廣的決心,再加上Vue本身的龐大用戶基數,Vite確實有可能成爲下一代前端構建工具的突破口。

本文將討論下Vite出現的背景,解決的痛點,核心功能的實現,存在的意義和預期的未來。Vite本身並不複雜。中文官方文檔非常清晰簡潔,建議大家使用前仔細讀下文檔。


大綱


背景

這裏的背景介紹會從與Vite緊密相關的兩個概念的發展史說起,一個是JavaScript的模塊化標準,另一個是前端構建工具。

共存的模塊化標準

爲什麼JavaScript會有多種共存的模塊化標準?因爲 js 在設計之初並沒有模塊化的概念,隨着前端業務複雜度不斷提高,模塊化越來越受到開發者的重視,社區開始湧現多種模塊化解決方案,它們相互借鑑,也爭議不斷,形成多個派系,從CommonJS開始,到ES6正式推出ES Modules規範結束,所有爭論,終成歷史,ES Modules也成爲前端重要的基礎設施。

對模塊化發展史感興趣的可以看下《前端模塊化開發那點歷史》@玉伯 [2],而Vite的核心正是依靠瀏覽器對 ES Module 規範的實現。

發展中的構建工具

近些年前端工程化發展迅速,各種構建工具層出不窮,目前Webpack仍然佔據統治地位,npm 每週下載量達到兩千多萬次。下面是我按 npm 發版時間線列出的開發者比較熟知的一些構建工具。

當前工程化痛點

現在常用的構建工具如Webpack,主要是通過抓取 - 編譯 - 構建整個應用的代碼(也就是常說的打包過程),生成一份編譯、優化後能良好兼容各個瀏覽器的的生產環境代碼。在開發環境流程也基本相同,需要先將整個應用構建打包後,再把打包後的代碼交給dev server(開發服務器)。

Webpack等構建工具的誕生給前端開發帶來了極大的便利,但隨着前端業務的複雜化,js 代碼量呈指數增長,打包構建時間越來越久,dev server(開發服務器)性能遇到瓶頸:

緩慢的開發環境,大大降低了開發者的幸福感,在以上背景下Vite應運而生。


什麼是 Vite?

基於 esbuild 與 Rollup,依靠瀏覽器自身 ESM 編譯功能, 實現極致開發體驗的新一代構建工具!

概念

先介紹以下文中會經常提到的一些基礎概念:

開發環境

生產環境

處理流程對比

Webpack通過先將整個應用打包,再將打包後代碼提供給dev server,開發者才能開始開發。

Vite直接將源碼交給瀏覽器,實現dev server秒開,瀏覽器顯示頁面需要相關模塊時,再向dev server發起請求,服務器簡單處理後,將該模塊返回給瀏覽器,實現真正意義的按需加載。


基本用法

創建 vite 項目

$ npm create vite@latest

選取模板

Vite 內置 6 種常用模板與對應的 TS 版本,可滿足前端大部分開發場景,可以點擊下列表格中模板直接在 StackBlitz[3] 中在線試用,還有其他更多的 社區維護模板 [4] 可以使用。

38EDnM

啓動

{
  "scripts": {
    "dev": "vite", // 啓動開發服務器,別名:`vite dev`,`vite serve`
    "build": "vite build", // 爲生產環境構建產物
    "preview": "vite preview" // 本地預覽生產構建產物
  }
}

實現原理

ESbuild 編譯

esbuild 使用 go 編寫,cpu 密集下更具性能優勢,編譯速度更快,以下摘自官網的構建速度對比:
瀏覽器:“開始了嗎?”
服務器:“已經結束了。”
開發者:“好快,好喜歡!!”

依賴預構建

按需加載

緩存

重寫模塊路徑

瀏覽器import只能引入相對 / 絕對路徑,而開發代碼經常使用npm包名直接引入node_module中的模塊,需要做路徑轉換後交給瀏覽器。

// 開發代碼
import { createApp } from 'vue'

// 轉換後
import { createApp } from '/node_modules/vue/dist/vue.js'

源碼分析

Webpack-dev-server類似Vite同樣使用WebSocket與客戶端建立連接,實現熱更新,源碼實現基本可分爲兩部分,源碼位置在:

client 代碼會在啓動服務時注入到客戶端,用於客戶端對於WebSocket消息的處理(如更新頁面某個模塊、刷新頁面);server 代碼是服務端邏輯,用於處理代碼的構建與頁面模塊的請求。

簡單看了下源碼(vite@2.7.2),核心功能主要是以下幾個方法(以下爲源碼截取,部分邏輯做了刪減):

  1. 命令行啓動服務npm run dev後,源碼執行cli.ts,調用createServer方法,創建 http 服務,監聽開發服務器端口。
// 源碼位置 vite/packages/vite/src/node/cli.ts
const { createServer } = await import('./server')
try {
    const server = await createServer({
        root,
        base: options.base,
        ...
    })
    if (!server.httpServer) {
        throw new Error('HTTP server not available')
    }
    await server.listen()
}
  1. createServer方法的執行做了很多工作,如整合配置項、創建 http 服務(早期通過 koa 創建)、創建WebSocket服務、創建源碼的文件監聽、插件執行、optimize 優化等。下面註釋中標出。
// 源碼位置 vite/packages/vite/src/node/server/index.ts
export async function createServer(
    inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
    // Vite 配置整合
    const config = await resolveConfig(inlineConfig, 'serve', 'development')
    const root = config.root
    const serverConfig = config.server

    // 創建http服務
    const httpServer = await resolveHttpServer(serverConfig, middlewares, httpsOptions)

    // 創建ws服務
    const ws = createWebSocketServer(httpServer, config, httpsOptions)

    // 創建watcher,設置代碼文件監聽
    const watcher = chokidar.watch(path.resolve(root), {
        ignored: [
            '**/node_modules/**',
            '**/.git/**',
            ...(Array.isArray(ignored) ? ignored : [ignored])
        ],
        ...watchOptions
    }) as FSWatcher

    // 創建server對象
    const server: ViteDevServer = {
        config,
        middlewares,
        httpServer,
        watcher,
        ws,
        moduleGraph,
        listen,
        ...
    }

    // 文件監聽變動,websocket向前端通信
    watcher.on('change', async (file) => {
        ...
        handleHMRUpdate()
    })

    // 非常多的 middleware
    middlewares.use(...)
    
    // optimize
    const runOptimize = async () => {...}

    return server
}
  1. 使用 chokidar[5] 監聽文件變化,綁定監聽事件。
// 源碼位置 vite/packages/vite/src/node/server/index.ts
  const watcher = chokidar.watch(path.resolve(root), {
    ignored: [
      '**/node_modules/**',
      '**/.git/**',
      ...(Array.isArray(ignored) ? ignored : [ignored])
    ],
    ignoreInitial: true,
    ignorePermissionErrors: true,
    disableGlobbing: true,
    ...watchOptions
  }) as FSWatcher
  1. 通過 ws[6] 來創建WebSocket服務,用於監聽到文件變化時觸發熱更新,向客戶端發送消息。
// 源碼位置 vite/packages/vite/src/node/server/ws.ts
export function createWebSocketServer(...){
    let wss: WebSocket
    const hmr = isObject(config.server.hmr) && config.server.hmr
    const wsServer = (hmr && hmr.server) || server

    if (wsServer) {
        wss = new WebSocket({ noServer: true })
        wsServer.on('upgrade', (req, socket, head) => {
            // 服務就緒
            if (req.headers['sec-websocket-protocol'] === HMR_HEADER) {
                wss.handleUpgrade(req, socket as Socket, head, (ws) => {
                    wss.emit('connection', ws, req)
                })
            }
        })
    } else {
        ...
    }
    // 服務準備就緒,就能在瀏覽器控制檯看到熟悉的打印 [vite] connected.
    wss.on('connection', (socket) => {
        socket.send(JSON.stringify({ type: 'connected' }))
        ...
    })
    // 失敗
    wss.on('error', (e: Error & { code: string }) => {
        ...
    })
    // 返回ws對象
    return {
        on: wss.on.bind(wss),
        off: wss.off.bind(wss),
        // 向客戶端發送信息
        // 多個客戶端同時觸發
        send(payload: HMRPayload) {
            const stringified = JSON.stringify(payload)
            wss.clients.forEach((client) => {
                // readyState 1 means the connection is open
                client.send(stringified)
            })
        }
    }
}
  1. 在服務啓動時會向瀏覽器注入代碼,用於處理客戶端接收到的WebSocket消息,如重新發起模塊請求、刷新頁面。
//源碼位置 vite/packages/vite/src/client/client.ts
async function handleMessage(payload: HMRPayload) {
  switch (payload.type) {
    case 'connected':
      console.log(`[vite] connected.`)
      break
    case 'update':
      notifyListeners('vite:beforeUpdate', payload)
      ...
      break
    case 'custom': {
      notifyListeners(payload.event as CustomEventName<any>, payload.data)
      ...
      break
    }
    case 'full-reload':
      notifyListeners('vite:beforeFullReload', payload)
      ...
      break
    case 'prune':
      notifyListeners('vite:beforePrune', payload)
      ...
      break
    case 'error': {
      notifyListeners('vite:error', payload)
      ...
      break
    }
    default: {
      const check: never = payload
      return check
    }
  }
}

優勢

不足


與 webpack 對比

由於Vite主打的是開發環境的極致體驗,生產環境集成Rollup,這裏的對比主要是Webpack-dev-serverVite-dev-server的對比:


兼容性


未來探索


相關資源

官方插件

除了支持現有的Rollup插件系統外,官方提供了四個最關鍵的插件

UI 組件庫

相關鏈接

參考資料

[1] 《Vue3 生態進展和計劃》: https://www.yuque.com/vueconf/mkwv0c/xqyxix

[2] 《前端模塊化開發那點歷史》: https://github.com/seajs/seajs/issues/588

[3] StackBlitz: https://vite.new/

[4] 社區維護模板: https://github.com/vitejs/awesome-vite#templates

[5] chokidar: https://www.npmjs.com/package/chokidar

[6] ws: https://www.npmjs.com/package/ws

[7] Element UI: https://element-plus.gitee.io/zh-CN/guide/quickstart.html#%E6%8C%89%E9%9C%80%E5%AF%BC%E5%85%A5

[8] Vite 官網: https://cn.vitejs.dev/

[9] Vue3 生態進展和計劃 - 尤雨溪: https://www.yuque.com/vueconf/mkwv0c/xqyxix

[10] Vite 源碼解析: http://vite.ssr-fc.com/

[11] Develop with Vite | Vite 快速入門 - Anthony Fu • Vue 北京聚會 Day 13: https://www.youtube.com/watch?v=xx8gEHet6n8

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/oroQSMSPxtSEfnjuxu2pew