天天看點

是什麼尤大選擇放棄Webpack?——vite 原了解析

今天分享一篇關于 vite 的文章。

文章推薦詞:三元同學最近也對 Vue3 有所關注,不過我更加感興趣的是尤大新設計出來的一個小工具——vite。大家都知道,webpack 打包的時候會有兩個階段: 編譯和打包,但打包之後會有一個問題,就是随着子產品的增多,會造成打出的 bundle 體積過大,進而會造成熱更新速度明顯拖慢。vite 的誕生就是為了解決這樣的問題,當子產品越來越多時,熱更新速度并不會變慢。 當然,有一說一,這僅僅隻是針對 Vue 項目開發階段的工具,其他的場景還是需要依賴強大的 Webpack 的。vite 也并不是萬能的。

另外值得一提的是,vite 也即将用于部落格搭建系統 vuepress,來解決它熱更新慢的問題。這是尤大的原話:

是什麼尤大選擇放棄Webpack?——vite 原了解析

是不是迫不及待想了解這個工具的原理了呢?我最近看到了一篇比較 nice 的文章,對原理講的比較清楚了。不過需要聲明的是,現在 vite 這個項目更新的非常迅速,每天都有更新,是以,這篇文章涉及的代碼會和最新的代碼有些出入,不過原理和思路還是一樣的,關于更多細節上的變更,小夥伴們還是直接去看源碼吧。源碼位址: https://github.com/vuejs/vite。

以下是文章具體内容:

本文同步在掘金部落客:「橙紅年代」個人部落格shymean.com上,歡迎關注。掘金原文連結: https://juejin.im/post/5ea2361de51d454714428b44

前兩天尤大在

Vue 3.0 beta

直播中提到了一個

vite

的工具,其描述是:針對Vue單頁面元件的無打包開發伺服器,可以直接在浏覽器運作請求的vue檔案,對其原理比較感興趣,是以體驗并寫下了本文,主要包括vite實作原理分析和一些思考。

預備知識

vite

重度依賴

module sciprt

的特性,是以需要提前做下功課,參考:JavaScript modules 子產品 - MDN。

module sciprt

允許在浏覽器中直接運作原生支援子產品

<script type="module">
    // index.js可以通過export導出子產品,也可以在其中繼續使用import加載其他依賴 
    import App from './index.js'
</script>           

複制

當遇見import依賴時,會直接發起http請求對應的子產品檔案。

開發環境

本文使用的版本為

[email protected]

,附github項目位址~目前這個項目貌似每天都在更新

首先克隆倉庫

git clone https://github.com/vuejs/vite
cd vite && yarn           

複制

環境安裝完畢後在項目下建立

examples

目錄,新增

index.html

Comp.vue

檔案,這裡直接用README.md中的例子

首先是

inidex.html

<div id="app"></div>
<script type="module">
import { createApp } from 'vue'
import Comp from './Comp.vue'

createApp(Comp).mount('#app')
</script>           

複制

然後是`Comp.vue``

<template>
  <button @click="count++">{{ count }} times</button>
</template>

<script>
export default {
  data: () => ({ count: 0 })
}
</script>

<style scoped>
button { color: red }
</style>           

複制

然後在exmples目錄下運作

../bin/vite.js           

複制

即可在浏覽器

http://localhost:3000

打開預覽,同時支援檔案熱更新哦~

如果需要調試源碼,啟動

npm run dev

即可,會開啟

tsc -w --p

監聽src目錄的改動并實時輸出到dist目錄下,接下來就可以開啟歡樂的源碼時間~

入口檔案

目前這個項目疊代非常頻繁(昨天還有

historyFallbackMiddleware

這個中間件呢今天貌似就沒了),但是大概的實作思路應該是基本确定了,是以先确定本次源碼閱讀目标:了解如何在不使用webpack等打包工具的前提下直接運作vue檔案。基于這個目的,主要是了解實作思路,理清整體結構,不用拘泥于具體細節。

從入口

bin/vite.js

開始

const server = require('../dist/server').createServer(argv)           

複制

可以看見

createServer

方法,直接定位到

src/server/client.tx

。vite使用的是

Koa

建構服務端,在

createServer

中主要通過中間件注冊相關功能

// src/index.ts
// 提前預告這四個插件的作用
const internalPlugins: Plugin[] = [
  modulesPlugin, // 處理入口html檔案script标簽和每個vue檔案的子產品依賴
  vuePlugin, // vue單頁面元件解析,将template、script、style解析成不同的響應内容,可以了解為簡易版的vue-loader
  hmrPlugin, // 使用websocket實作檔案熱更新
  servePlugin // koa配置插件,目前看來主要是配置協商緩存相關
]

export function createServer({
  root = process.cwd(),
  middlewares: userMiddlewares = []
}: ServerConfig = {}): Server {
  const app = new Koa()
  const server = http.createServer(app.callback())
  // 預留了userMiddlewares友善提供後續API
  ;[...userMiddlewares, ...middlewares].forEach((m) =>
    m({
      root,
      app,
      server
    })
  )

  return server
}           

複制

vite

是通過下面這種

middleware

的形式注冊koa中間件,

export const modulesPlugin: Plugin = ({ root, app }) => {
  // 每個插件實際上是注冊koa中間件
  app.use(async (ctx, next) => {})
}           

複制

看起來跟Vue2的源碼結構比較類似,通過裝飾器逐漸添加功能~目前隻需要理清這四個插件的作用就可以了。

// vue2源碼結構
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)           

複制

moduleResolverMiddleware

這個中間件的作用編譯

index.html

SFC

等檔案内容,處理相關的依賴。

比如上面的html檔案

script

标簽内容,通過

rewriteImports

等方法的處理會被編譯成

import { createApp } from '/__modules/vue'// 之前是import { createApp } from 'vue'
import Comp from './Comp.vue'

createApp(Comp).mount('#app')           

複制

這樣當浏覽器解析并運作這個module類型的script标簽時,就會請求對應的子產品檔案,其中

  • /__modules/vue

    是koa伺服器的靜态資源目錄檔案,
  • ./Comp.vue

    是我們編寫的單頁面元件檔案
  • 此外貌似還會提供

    sourcemap

    等功能

對于入口檔案而言,需要script标簽下相關依賴。對于單頁面元件而言,在

vue-loader

中,也需要處理

tmplate、

script

style

标簽;在vite中,這些依賴都會被當做

css

js`檔案請求的方式進行加載。

單頁面元件主要包含

template

script

style

标簽,其中

script

标簽内代碼的導出會被編譯成

// 加載熱更新子產品用戶端,後面會提到
import "/__hmrClient"

let __script; export default (__script = {
  data: () => ({ count: 0 })
})
// 根據type進行區分,樣式檔案type=style
import "/Comp.vue?type=style&index=0"
// 保留css scopeID
__script.__scopeId = "data-v-92a6df80"
// render函數檔案type=template
import { render as __render } from "/Comp.vue?type=template"
__script.render = __render
__script.__hmrId = "/Comp.vue"           

複制

style

template

标簽會被重寫成

/Comp.vue?type=xxx

的形式,重新發送http請求,這個通過query參數的形式區分并加載SFC檔案各個子產品内容的方式,與

vue-loader

中通過

webpack

resourceQuery

配置進行處理如初一轍,如果了解

vue-loader

運作原理的同學看到這裡估計就已經恍然大悟了,之前寫過一篇從vue-loader源碼分析CSS-Scoped的實作,裡面也介紹了vue-loader的大緻原理。

回到vite,現在我們清楚了

moduleResolverMiddleware

的作用,主要就是重寫子產品路徑,将SFC檔案的依賴通過query參數進行區分,友善浏覽器通過url加載實際子產品。打開浏覽器控制台,可以檢視具體的檔案請求

是什麼尤大選擇放棄Webpack?——vite 原了解析

VuePlugin

前面提到單頁面元件的

template

style

會被處理成單獨的的import路徑,通過query.type區分,那麼當伺服器接收到對應的url請求時,如何傳回正确的資源内容呢?答案就在第二個插件

VuePlugin

中。

單頁面檔案的請求有個特點,都是以

*.vue

作為請求路徑結尾,當伺服器接收到這種特點的http請求,主要處理

  • 根據

    ctx.path

    确定請求具體的vue檔案
  • 使用

    parseSFC

    解析該檔案,獲得

    descriptor

    ,一個

    descriptor

    包含了這個元件的基本資訊,包括

    template

    script

    styles

    等屬性 下面是

    Comp.vue

    檔案經過處理後獲得的

    descriptor

{
 filename: '/Users/Txm/source_code/vite/examples/Comp.vue',
 template: {
   type: 'template',
   content: '\n  <button @click="count++">{{ count }} times1</button>\n',
   loc: {
     source: '\n  <button @click="count++">{{ count }} times1</button>\n',
     start: [Object],
     end: [Object]
   },
   attrs: {},
   map: {
     version: 3,
     sources: [Array],
     names: [],
     mappings: ';AACA',
     file: '/Users/Txm/source_code/vite/examples/Comp.vue',
     sourceRoot: '',
     sourcesContent: [Array]
   }
 },
 script: {
   type: 'script',
   content: '\nexport default {\n  data: () => ({ count: 0 })\n}\n',
   loc: {
     source: '\nexport default {\n  data: () => ({ count: 0 })\n}\n',
     start: [Object],
     end: [Object]
   },
   attrs: {},
   map: {
     version: 3,
     sources: [Array],
     names: [],
     mappings: ';AAKA;AACA;AACA',
     file: '/Users/Txm/source_code/vite/examples/Comp.vue',
     sourceRoot: '',
     sourcesContent: [Array]
   }
 },
 styles: [
   {
     type: 'style',
     content: '\nbutton { color: red }\n',
     loc: [Object],
     attrs: [Object],
     scoped: true,
     map: [Object]
   }
 ],
 customBlocks: []
}           

複制

  • 然後根據

    descriptor

    ctx.query.type

    選擇對應類型的方法,處理後傳回

    ctx.body

    • type為空時表示處理

      script

      标簽,使用

      compileSFCMain

      方法傳回

      js

      内容
    • type為

      template

      時表示處理

      template

      标簽,使用

      compileSFCTemplate

      方法傳回

      render

      方法
    • type為

      style

      s時表示處理

      style

      标簽,使用

      compileSFCStyle

      方法傳回

      css

      檔案内容

回頭整理一下流程

  • 入口檔案依賴

    Comp.vue

    的script代碼
  • Com.vue

    依賴

    tempplate

    編譯的render方法,依賴

    style

    标簽編譯的css代碼,這兩個檔案放在

    script

    的編譯代碼中進行依賴聲明
// Comp.vue傳回的檔案内容,可以看見跟入口檔案的script标簽内容比較相似
import { updateStyle } from "/__hmrClient"

const __script = {
  data: () => ({ count: 0 })
}
// style标簽内容解析後的css代碼
updateStyle("92a6df80-0", "/Comp.vue?type=style&index=0")
__script.__scopeId = "data-v-92a6df80"
// temlpate标簽内容解析後的render
import { render as __render } from "/Comp.vue?type=template"
__script.render = __render
__script.__hmrId = "/Comp.vue"
export default __script           

複制

每個标簽内容解析完成之後,會通過

LRUCache

緩存起來,友善下次重複使用

export const vueCache = new LRUCache<string, CacheEntry>({
  max: 65535
})           

複制

至此,我們就大緻了解了

vite

是如何通過koa直接運作vue檔案的,其思路跟

vue-loader

比較類似,借助

module script

處理檔案依賴,然後通過拼接不同的

query.type

處理單頁面檔案解析後的各個資源檔案,最後響應給浏覽器進行渲染。

hmrPlugin

前面提到

vite

也是支援檔案熱更新的,既然沒有使用

webpack

,那該是如何做到的呢?答案就是自己實作一個哈哈哈~

熱更新主要通過

webSocket

實作,包括ws服務端和ws用戶端兩個部分,

hmrPlugin

主要負責ws服務端的部分,ws用戶端在

src/client.ts

中實作,并通過在第一步處理子產品依賴時

import "/__hmrClient"

将服務端和用戶端關聯起來。

目前主要定義了下面幾種消息類型

  • reload

  • rerender

  • style-update

  • style-remove

  • full-reload

當檔案發生變化時,服務端在

handleVueSFCReload

方法中會根據變化的類型推送不同的消息,當用戶端接收到對應消息時,會結合

vue.HMRRuntime

進行處理或者重新加載新的資源。

熱更新這裡目前還有不少

TODO

,感覺是一個學習熱更新原理的不錯案例,先碼一下後面回頭重新細讀。

關于熱更新的原理,社群有不少原理分析了,不妨移步閱讀

  • Webpack 熱更新
  • 輕松了解webpack熱更新原理

servePlugin

這個插件主要用于實作一些koa請求和響應的配置。

經過上面的分析,每次請求時,都會從入口檔案開始,依次分析每個依賴

  • 對于普通檔案,直接查找伺服器靜态資源, 通過

    servePlugin

    中配置

    koa-static

    實作
  • 對于vue檔案,會重新拼接http請求,對于每個請求,包括

    path

    query

    ,其中path用于确定元件檔案,

    query.type

    用于确定具體使用啥方法來傳回響應内容

在上面這一步,很明顯對于每個vue檔案而言,都會發送多個http請求,然後執行查找和解析的操作是很頻繁的,如果不配置緩存,伺服器的性能負擔比較大,

koa-conditional-get

koa-etag

應該就是為了解決這個問題,不過目前看起來還沒有實作。

小結

至此,就完成了

vite

源碼的基礎閱讀,由于本地閱讀源碼的主要目的是了解整個工具的實作原理和大緻功能,是以并沒有深入了解每個函數的實作細節,幾個比較重要的方法包括

rewriteImports

compileSFCMain

compileSFCTemplate

compileSFCStyle

updateStyle

等均沒有展示具體代碼實作,主要的收獲是了解了

  • 結合module script和query.type實作一套類似于vue-loader的機制,直接在服務端運作vue檔案
  • 使用websocket手動實作熱更新,由于時間關系這裡并沒有細讀~

剛看見vite介紹時就覺得這會是一個非常有趣的工具,雖然還沒有正式釋出,耐不住去看了一下。感覺主要的作用有

  • 使用vite快速開發demo,而不必安裝一大堆依賴
  • 類似于

    jsfiddle

    等線上預覽vue檔案,友善開發、測試和分發單檔案元件

目前看來vite還缺少打包等重要特性,應該是沒法替代webpack等工具的。不過感覺vite應該也不是用來替換現有開發工具的,是以後面大概也不會添加打包等功能吧~