是什么尤大選擇放棄Webpack个曙?——vite 原理解析

來(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.htmlComp.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.htmlSFC等文件內(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、scriptstyle標(biāo)簽煎源;在vite中色迂,這些依賴都會(huì)被當(dāng)做cssjs`文件請(qǐng)求的方式進(jìn)行加載。

單頁(yè)面組件主要包含template手销、scriptstyle標(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"

styletemplate標(biāo)簽會(huì)被重寫(xiě)成/Comp.vue?type=xxx的形式诈悍,重新發(fā)送http請(qǐng)求祸轮,這個(gè)通過(guò)query參數(shù)的形式區(qū)分并加載SFC文件各個(gè)模塊內(nèi)容的方式,與vue-loader中通過(guò)webpackresourceQuery配置進(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)求

image

VuePlugin

前面提到單頁(yè)面組件的templatestyle會(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矿微、scriptstyles等屬性下面是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ù)descriptorctx.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為styles時(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)求,包括pathquery拂共,其中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-getkoa-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祥诽、compileSFCMaincompileSFCTemplate瓮恭、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ì)添加打包等功能吧~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市狞甚,隨后出現(xiàn)的幾起案子锁摔,更是在濱河造成了極大的恐慌,老刑警劉巖哼审,帶你破解...
    沈念sama閱讀 211,348評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谐腰,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡涩盾,警方通過(guò)查閱死者的電腦和手機(jī)十气,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)春霍,“玉大人砸西,你說(shuō)我怎么就攤上這事≈啡澹” “怎么了芹枷?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,936評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)莲趣。 經(jīng)常有香客問(wèn)我鸳慈,道長(zhǎng),這世上最難降的妖魔是什么喧伞? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,427評(píng)論 1 283
  • 正文 為了忘掉前任走芋,我火速辦了婚禮绩郎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘翁逞。我一直安慰自己肋杖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布挖函。 她就那樣靜靜地躺著状植,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挪圾。 梳的紋絲不亂的頭發(fā)上浅萧,一...
    開(kāi)封第一講書(shū)人閱讀 49,785評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音哲思,去河邊找鬼。 笑死吩案,一個(gè)胖子當(dāng)著我的面吹牛棚赔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播徘郭,決...
    沈念sama閱讀 38,931評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼靠益,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了残揉?” 一聲冷哼從身側(cè)響起胧后,我...
    開(kāi)封第一講書(shū)人閱讀 37,696評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎抱环,沒(méi)想到半個(gè)月后壳快,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,141評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡镇草,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評(píng)論 2 327
  • 正文 我和宋清朗相戀三年眶痰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梯啤。...
    茶點(diǎn)故事閱讀 38,625評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡竖伯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出因宇,到底是詐尸還是另有隱情七婴,我是刑警寧澤,帶...
    沈念sama閱讀 34,291評(píng)論 4 329
  • 正文 年R本政府宣布察滑,位于F島的核電站打厘,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏杭棵。R本人自食惡果不足惜婚惫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評(píng)論 3 312
  • 文/蒙蒙 一氛赐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧先舷,春花似錦艰管、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至捺球,卻和暖如春缸浦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背氮兵。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工裂逐, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人泣栈。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓卜高,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親南片。 傳聞我的和親對(duì)象是個(gè)殘疾皇子掺涛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評(píng)論 2 348