來(lái)自:掘金锈嫩,作者:橙紅年代
鏈接:https://juejin.im/post/5ea2361de51d454714428b44
前些天尤大在Vue 3.0 beta
直播中提到了一個(gè)vite
的工具,其描述是:針對(duì)Vue單頁(yè)面組件的無(wú)打包開(kāi)發(fā)服務(wù)器垦搬,可以直接在瀏覽器運(yùn)行請(qǐng)求的vue文件呼寸,對(duì)其原理比較感興趣,因此體驗(yàn)并寫(xiě)下了本文猴贰,主要包括vite實(shí)現(xiàn)原理分析和一些思考对雪。
預(yù)備知識(shí)
vite
重度依賴module sciprt
的特性,因此需要提前做下功課米绕,參考:JavaScript modules 模塊 - MDN瑟捣。
module sciprt
允許在瀏覽器中直接運(yùn)行原生支持模塊
<script type="module">
// index.js可以通過(guò)export導(dǎo)出模塊,也可以在其中繼續(xù)使用import加載其他依賴
import App from './index.js'
</script>
當(dāng)遇見(jiàn)import依賴時(shí)义郑,會(huì)直接發(fā)起http請(qǐng)求對(duì)應(yīng)的模塊文件蝶柿。
開(kāi)發(fā)環(huán)境
本文使用的版本為vite@0.3.2
,附github項(xiàng)目地址~目前這個(gè)項(xiàng)目貌似每天都在更新
首先克隆倉(cāng)庫(kù)
git clone https://github.com/vuejs/vite
cd vite && yarn
環(huán)境安裝完畢后在項(xiàng)目下創(chuàng)建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目錄下運(yùn)行
../bin/vite.js
即可在瀏覽器http://localhost:3000
打開(kāi)預(yù)覽雏赦,同時(shí)支持文件熱更新哦~
如果需要調(diào)試源碼劫笙,啟動(dòng)npm run dev
即可芙扎,會(huì)開(kāi)啟tsc -w --p
監(jiān)聽(tīng)src目錄的改動(dòng)并實(shí)時(shí)輸出到dist目錄下,接下來(lái)就可以開(kāi)啟歡樂(lè)的源碼時(shí)間~
入口文件
目前這個(gè)項(xiàng)目迭代非常頻繁(昨天還有historyFallbackMiddleware
這個(gè)中間件呢今天貌似就沒(méi)了)填大,但是大概的實(shí)現(xiàn)思路應(yīng)該是基本確定了戒洼,因此先確定本次源碼閱讀目標(biāo):了解如何在不使用webpack等打包工具的前提下直接運(yùn)行vue文件≡驶基于這個(gè)目的圈浇,主要是了解實(shí)現(xiàn)思路,理清整體結(jié)構(gòu)靴寂,不用拘泥于具體細(xì)節(jié)磷蜀。
從入口bin/vite.js
開(kāi)始
const server = require('../dist/server').createServer(argv)
可以看見(jiàn)createServer
方法,直接定位到src/server/client.tx
百炬。vite使用的是Koa
構(gòu)建服務(wù)端褐隆,在createServer
中主要通過(guò)中間件注冊(cè)相關(guān)功能
// src/index.ts
// 提前預(yù)告這四個(gè)插件的作用
const internalPlugins: Plugin[] = [
modulesPlugin, // 處理入口html文件script標(biāo)簽和每個(gè)vue文件的模塊依賴
vuePlugin, // vue單頁(yè)面組件解析,將template剖踊、script庶弃、style解析成不同的響應(yīng)內(nèi)容,可以理解為簡(jiǎn)易版的vue-loader
hmrPlugin, // 使用websocket實(shí)現(xiàn)文件熱更新
servePlugin // koa配置插件德澈,目前看來(lái)主要是配置協(xié)商緩存相關(guān)
]
export function createServer({
root = process.cwd(),
middlewares: userMiddlewares = []
}: ServerConfig = {}): Server {
const app = new Koa()
const server = http.createServer(app.callback())
// 預(yù)留了userMiddlewares方便提供后續(xù)API
;[...userMiddlewares, ...middlewares].forEach((m) =>
m({
root,
app,
server
})
)
return server
}
vite
是通過(guò)下面這種middleware
的形式注冊(cè)koa中間件歇攻,
export const modulesPlugin: Plugin = ({ root, app }) => {
// 每個(gè)插件實(shí)際上是注冊(cè)koa中間件
app.use(async (ctx, next) => {})
}
看起來(lái)跟Vue2的源碼結(jié)構(gòu)比較類似,通過(guò)裝飾器逐步添加功能~目前只需要理清這四個(gè)插件的作用就可以了梆造。
// vue2源碼結(jié)構(gòu)
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
moduleResolverMiddleware
這個(gè)中間件的作用編譯index.html
和SFC
等文件內(nèi)容缴守,處理相關(guān)的依賴。
比如上面的html文件script
標(biāo)簽內(nèi)容澳窑,通過(guò)rewriteImports
等方法的處理會(huì)被編譯成
import { createApp } from '/__modules/vue'// 之前是import { createApp } from 'vue'
import Comp from './Comp.vue'
createApp(Comp).mount('#app'
這樣當(dāng)瀏覽器解析并運(yùn)行這個(gè)module類型的script標(biāo)簽時(shí)斧散,就會(huì)請(qǐng)求對(duì)應(yīng)的模塊文件,其中
-
/__modules/vue
是koa服務(wù)器的靜態(tài)資源目錄文件摊聋, -
./Comp.vue
是我們編寫(xiě)的單頁(yè)面組件文件 - 此外貌似還會(huì)提供
sourcemap
等功能
對(duì)于入口文件而言鸡捐,需要script標(biāo)簽下相關(guān)依賴。對(duì)于單頁(yè)面組件而言麻裁,在vue-loader
中箍镜,也需要處理tmplate、
script和
style標(biāo)簽煎源;在vite中色迂,這些依賴都會(huì)被當(dāng)做
css和
js`文件請(qǐng)求的方式進(jìn)行加載。
單頁(yè)面組件主要包含template
手销、script
和style
標(biāo)簽歇僧,其中script
標(biāo)簽內(nèi)代碼的導(dǎo)出會(huì)被編譯成
// 加載熱更新模塊客戶端,后面會(huì)提到
import "/__hmrClient"
let __script; export default (__script = {
data: () => ({ count: 0 })
})
// 根據(jù)type進(jìn)行區(qū)分,樣式文件type=style
import "/Comp.vue?type=style&index=0"
// 保留css scopeID
__script.__scopeId = "data-v-92a6df80"
// render函數(shù)文件type=template
import { render as __render } from "/Comp.vue?type=template"
__script.render = __render
__script.__hmrId = "/Comp.vue"
而style
及template
標(biāo)簽會(huì)被重寫(xiě)成/Comp.vue?type=xxx
的形式诈悍,重新發(fā)送http請(qǐng)求祸轮,這個(gè)通過(guò)query參數(shù)的形式區(qū)分并加載SFC文件各個(gè)模塊內(nèi)容的方式,與vue-loader
中通過(guò)webpack
的resourceQuery
配置進(jìn)行處理如出一轍侥钳,如果了解vue-loader
運(yùn)行原理的同學(xué)看到這里估計(jì)就已經(jīng)恍然大悟了适袜,之前寫(xiě)過(guò)一篇從vue-loader源碼分析CSS-Scoped的實(shí)現(xiàn),里面也介紹了vue-loader的大致原理舷夺。
回到vite苦酱,現(xiàn)在我們清楚了moduleResolverMiddleware
的作用,主要就是重寫(xiě)模塊路徑给猾,將SFC文件的依賴通過(guò)query參數(shù)進(jìn)行區(qū)分疫萤,方便瀏覽器通過(guò)url加載實(shí)際模塊。打開(kāi)瀏覽器控制臺(tái)耙册,可以查看具體的文件請(qǐng)求
VuePlugin
前面提到單頁(yè)面組件的template
和style
會(huì)被處理成單獨(dú)的的import路徑给僵,通過(guò)query.type區(qū)分,那么當(dāng)服務(wù)器接收到對(duì)應(yīng)的url請(qǐng)求時(shí)详拙,如何返回正確的資源內(nèi)容呢帝际?答案就在第二個(gè)插件VuePlugin
中。
單頁(yè)面文件的請(qǐng)求有個(gè)特點(diǎn)饶辙,都是以*.vue
作為請(qǐng)求路徑結(jié)尾蹲诀,當(dāng)服務(wù)器接收到這種特點(diǎn)的http請(qǐng)求,主要處理
- 根據(jù)
ctx.path
確定請(qǐng)求具體的vue文件 - 使用
parseSFC
解析該文件弃揽,獲得descriptor
脯爪,一個(gè)descriptor
包含了這個(gè)組件的基本信息,包括template
矿微、script
和styles
等屬性下面是Comp.vue
文件經(jīng)過(guò)處理后獲得的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: []
}
然后根據(jù)
descriptor
和ctx.query.type
選擇對(duì)應(yīng)類型的方法痕慢,處理后返回ctx.body
type為空時(shí)表示處理
script
標(biāo)簽,使用compileSFCMain
方法返回js
內(nèi)容type為
template
時(shí)表示處理template
標(biāo)簽涌矢,使用compileSFCTemplate
方法返回render
方法type為
style
s時(shí)表示處理style
標(biāo)簽掖举,使用compileSFCStyle
方法返回css
文件內(nèi)容
回頭整理一下流程
- 入口文件依賴
Comp.vue
的script代碼 -
Com.vue
依賴tempplate
編譯的render方法,依賴style
標(biāo)簽編譯的css代碼娜庇,這兩個(gè)文件放在script
的編譯代碼中進(jìn)行依賴聲明
// Comp.vue返回的文件內(nèi)容塔次,可以看見(jiàn)跟入口文件的script標(biāo)簽內(nèi)容比較相似
import { updateStyle } from "/__hmrClient"
const __script = {
data: () => ({ count: 0 })
}
// style標(biāo)簽內(nèi)容解析后的css代碼
updateStyle("92a6df80-0", "/Comp.vue?type=style&index=0")
__script.__scopeId = "data-v-92a6df80"
// temlpate標(biāo)簽內(nèi)容解析后的render
import { render as __render } from "/Comp.vue?type=template"
__script.render = __render
__script.__hmrId = "/Comp.vue"
export default __script
每個(gè)標(biāo)簽內(nèi)容解析完成之后,會(huì)通過(guò)LRUCache
緩存起來(lái)名秀,方便下次重復(fù)使用
export const vueCache = new LRUCache<string, CacheEntry>({
max: 65535
})
至此励负,我們就大致了解了vite
是如何通過(guò)koa直接運(yùn)行vue文件的,其思路跟vue-loader
比較類似匕得,借助module script
處理文件依賴继榆,然后通過(guò)拼接不同的query.type
處理單頁(yè)面文件解析后的各個(gè)資源文件,最后響應(yīng)給瀏覽器進(jìn)行渲染。
hmrPlugin
前面提到vite
也是支持文件熱更新的裕照,既然沒(méi)有使用webpack
攒发,那該是如何做到的呢调塌?答案就是自己實(shí)現(xiàn)一個(gè)哈哈哈~
熱更新主要通過(guò)webSocket
實(shí)現(xiàn)晋南,包括ws服務(wù)端和ws客戶端兩個(gè)部分,hmrPlugin
主要負(fù)責(zé)ws服務(wù)端的部分羔砾,ws客戶端在src/client.ts
中實(shí)現(xiàn)负间,并通過(guò)在第一步處理模塊依賴時(shí)import "/__hmrClient"
將服務(wù)端和客戶端關(guān)聯(lián)起來(lái)。
目前主要定義了下面幾種消息類型
reload
rerender
style-update
style-remove
full-reload
當(dāng)文件發(fā)生變化時(shí)姜凄,服務(wù)端在handleVueSFCReload
方法中會(huì)根據(jù)變化的類型推送不同的消息政溃,當(dāng)客戶端接收到對(duì)應(yīng)消息時(shí),會(huì)結(jié)合vue.HMRRuntime
進(jìn)行處理或者重新加載新的資源态秧。
熱更新這里目前還有不少TODO
董虱,感覺(jué)是一個(gè)學(xué)習(xí)熱更新原理的不錯(cuò)案例,先碼一下后面回頭重新細(xì)讀申鱼。
關(guān)于熱更新的原理愤诱,社區(qū)有不少原理分析了,不妨移步閱讀
- Webpack 熱更新
- 輕松理解webpack熱更新原理
servePlugin
這個(gè)插件主要用于實(shí)現(xiàn)一些koa請(qǐng)求和響應(yīng)的配置捐友。
經(jīng)過(guò)上面的分析淫半,每次請(qǐng)求時(shí),都會(huì)從入口文件開(kāi)始匣砖,依次分析每個(gè)依賴
- 對(duì)于普通文件科吭,直接查找服務(wù)器靜態(tài)資源,通過(guò)
servePlugin
中配置koa-static
實(shí)現(xiàn) - 對(duì)于vue文件猴鲫,會(huì)重新拼接http請(qǐng)求对人,對(duì)于每個(gè)請(qǐng)求,包括
path
和query
拂共,其中path用于確定組件文件牺弄,query.type
用于確定具體使用啥方法來(lái)返回響應(yīng)內(nèi)容
在上面這一步,很明顯對(duì)于每個(gè)vue文件而言匣缘,都會(huì)發(fā)送多個(gè)http請(qǐng)求猖闪,然后執(zhí)行查找和解析的操作是很頻繁的,如果不配置緩存肌厨,服務(wù)器的性能負(fù)擔(dān)比較大培慌,koa-conditional-get
和koa-etag
應(yīng)該就是為了解決這個(gè)問(wèn)題,不過(guò)目前看起來(lái)還沒(méi)有實(shí)現(xiàn)柑爸。
小結(jié)
至此吵护,就完成了vite
源碼的基礎(chǔ)閱讀,由于本地閱讀源碼的主要目的是了解整個(gè)工具的實(shí)現(xiàn)原理和大致功能,因此并沒(méi)有深入了解每個(gè)函數(shù)的實(shí)現(xiàn)細(xì)節(jié)馅而,幾個(gè)比較重要的方法包括rewriteImports
祥诽、compileSFCMain
、compileSFCTemplate
瓮恭、compileSFCStyle
雄坪、updateStyle
等均沒(méi)有展示具體代碼實(shí)現(xiàn),主要的收獲是了解了
- 結(jié)合module script和query.type實(shí)現(xiàn)一套類似于vue-loader的機(jī)制屯蹦,直接在服務(wù)端運(yùn)行vue文件
- 使用websocket手動(dòng)實(shí)現(xiàn)熱更新维哈,由于時(shí)間關(guān)系這里并沒(méi)有細(xì)讀~
剛看見(jiàn)vite介紹時(shí)就覺(jué)得這會(huì)是一個(gè)非常有趣的工具,雖然還沒(méi)有正式發(fā)布登澜,耐不住去看了一下阔挠。感覺(jué)主要的作用有
- 使用vite快速開(kāi)發(fā)demo,而不必安裝一大堆依賴
- 類似于
jsfiddle
等在線預(yù)覽vue文件脑蠕,方便開(kāi)發(fā)购撼、測(cè)試和分發(fā)單文件組件
目前看來(lái)vite還缺少打包等重要特性,應(yīng)該是沒(méi)法替代webpack等工具的谴仙。不過(guò)感覺(jué)vite應(yīng)該也不是用來(lái)替換現(xiàn)有開(kāi)發(fā)工具的迂求,所以后面大概也不會(huì)添加打包等功能吧~