今天分享一篇關于 vite 的文章。
文章推薦詞:三元同學最近也對 Vue3 有所關注,不過我更加感興趣的是尤大新設計出來的一個小工具——vite。大家都知道,webpack 打包的時候會有兩個階段: 編譯和打包,但打包之後會有一個問題,就是随着子產品的增多,會造成打出的 bundle 體積過大,進而會造成熱更新速度明顯拖慢。vite 的誕生就是為了解決這樣的問題,當子產品越來越多時,熱更新速度并不會變慢。 當然,有一說一,這僅僅隻是針對 Vue 項目開發階段的工具,其他的場景還是需要依賴強大的 Webpack 的。vite 也并不是萬能的。
另外值得一提的是,vite 也即将用于部落格搭建系統 vuepress,來解決它熱更新慢的問題。這是尤大的原話:
是不是迫不及待想了解這個工具的原理了呢?我最近看到了一篇比較 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請求對應的子產品檔案。
開發環境
本文使用的版本為
,附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标簽時,就會請求對應的子產品檔案,其中
-
是koa伺服器的靜态資源目錄檔案,/__modules/vue
-
是我們編寫的單頁面元件檔案./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加載實際子產品。打開浏覽器控制台,可以檢視具體的檔案請求
VuePlugin
前面提到單頁面元件的
template
和
style
會被處理成單獨的的import路徑,通過query.type區分,那麼當伺服器接收到對應的url請求時,如何傳回正确的資源内容呢?答案就在第二個插件
VuePlugin
中。
單頁面檔案的請求有個特點,都是以
*.vue
作為請求路徑結尾,當伺服器接收到這種特點的http請求,主要處理
- 根據
确定請求具體的vue檔案ctx.path
- 使用
解析該檔案,獲得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為
s時表示處理style
标簽,使用style
方法傳回compileSFCStyle
檔案内容css
- type為空時表示處理
回頭整理一下流程
- 入口檔案依賴
的script代碼Comp.vue
-
依賴Com.vue
編譯的render方法,依賴tempplate
标簽編譯的css代碼,這兩個檔案放在style
的編譯代碼中進行依賴聲明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
,其中path用于确定元件檔案,query
用于确定具體使用啥方法來傳回響應内容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,而不必安裝一大堆依賴
- 類似于
等線上預覽vue檔案,友善開發、測試和分發單檔案元件jsfiddle
目前看來vite還缺少打包等重要特性,應該是沒法替代webpack等工具的。不過感覺vite應該也不是用來替換現有開發工具的,是以後面大概也不會添加打包等功能吧~