尤雨溪寫(xiě)的100多行的“玩具 vite”,十分有助于理解 vite 原理
1. 前言
大家好,我是若川。最近組織了源碼共讀活動(dòng),感興趣的可以加我微信 ruochuan12 參與,已進(jìn)行兩個(gè)多月,大家一起交流學(xué)習(xí),共同進(jìn)步。
想學(xué)源碼,極力推薦之前我寫(xiě)的《學(xué)習(xí)源碼整體架構(gòu)系列》 包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue-next-release、vue-this、create-vue等10余篇源碼文章。
最近組織了源碼共讀活動(dòng),大家一起學(xué)習(xí)源碼。于是各種搜尋值得我們學(xué)習(xí),且代碼行數(shù)不多的源碼。
在 vuejs組織[1] 下,找到了尤雨溪幾年前寫(xiě)的“玩具 vite”vue-dev-server[2],發(fā)現(xiàn)100來(lái)行代碼,很值得學(xué)習(xí)。于是有了這篇文章。
閱讀本文,你將學(xué)到:
1.?學(xué)會(huì)?vite?簡(jiǎn)單原理
2.?學(xué)會(huì)使用?VSCode?調(diào)試源碼
3.?學(xué)會(huì)如何編譯?Vue?單文件組件
4.?學(xué)會(huì)如何使用?recast?生成?ast?轉(zhuǎn)換文件
5.?如何加載包文件
6.?等等
2. vue-dev-server 它的原理是什么
vue-dev-server#how-it-works[3]README 文檔上有四句英文介紹。
發(fā)現(xiàn)谷歌翻譯[4]的還比較準(zhǔn)確,我就原封不動(dòng)的搬運(yùn)過(guò)來(lái)。
瀏覽器請(qǐng)求導(dǎo)入作為原生 ES 模塊導(dǎo)入 - 沒(méi)有捆綁。 服務(wù)器攔截對(duì) *.vue 文件的請(qǐng)求,即時(shí)編譯它們,然后將它們作為 JavaScript 發(fā)回。 對(duì)于提供在瀏覽器中工作的 ES 模塊構(gòu)建的庫(kù),只需直接從 CDN 導(dǎo)入它們。 導(dǎo)入到 .js 文件中的 npm 包(僅包名稱)會(huì)即時(shí)重寫(xiě)以指向本地安裝的文件。目前,僅支持 vue 作為特例。其他包可能需要進(jìn)行轉(zhuǎn)換才能作為本地瀏覽器目標(biāo) ES 模塊公開(kāi)。
也可以看看vitejs 文檔[5],了解下原理,文檔中圖畫(huà)得非常好。

看完本文后,我相信你會(huì)有一個(gè)比較深刻的理解。
3. 準(zhǔn)備工作
3.1 克隆項(xiàng)目
本文倉(cāng)庫(kù) vue-dev-server-analysis,求個(gè)star^_^[6]
#?推薦克隆我的倉(cāng)庫(kù)
git?clone?https://github.com/lxchuan12/vue-dev-server-analysis.git
cd?vue-dev-server-analysis/vue-dev-server
#?npm?i?-g?yarn
#?安裝依賴
yarn
#?或者克隆官方倉(cāng)庫(kù)
git?clone?https://github.com/vuejs/vue-dev-server.git
cd?vue-dev-server
#?npm?i?-g?yarn
#?安裝依賴
yarn
一般來(lái)說(shuō),我們看源碼先從package.json文件開(kāi)始:
//?vue-dev-server/package.json
{
??"name":?"@vue/dev-server",
??"version":?"0.1.1",
??"description":?"Instant?dev?server?for?Vue?single?file?components",
??"main":?"middleware.js",
??//?指定可執(zhí)行的命令
??"bin":?{
????"vue-dev-server":?"./bin/vue-dev-server.js"
??},
??"scripts":?{
????//?先跳轉(zhuǎn)到?test?文件夾,再用?Node?執(zhí)行?vue-dev-server?文件
????"test":?"cd?test?&&?node?../bin/vue-dev-server.js"
??}
}
根據(jù) scripts test 命令。我們來(lái)看 test 文件夾。
3.2 test 文件夾
vue-dev-server/test 文件夾下有三個(gè)文件,代碼不長(zhǎng)。
index.html main.js text.vue
如圖下圖所示。

接著我們找到 vue-dev-server/bin/vue-dev-server.js 文件,代碼也不長(zhǎng)。
3.3 vue-dev-server.js
//?vue-dev-server/bin/vue-dev-server.js
#!/usr/bin/env?node
const?express?=?require('express')
const?{?vueMiddleware?}?=?require('../middleware')
const?app?=?express()
const?root?=?process.cwd();
app.use(vueMiddleware())
app.use(express.static(root))
app.listen(3000,?()?=>?{
??console.log('server?running?at?http://localhost:3000')
})
原來(lái)就是express啟動(dòng)了端口3000的服務(wù)。重點(diǎn)在 vueMiddleware 中間件。接著我們來(lái)調(diào)試這個(gè)中間件。
鑒于估計(jì)很多小伙伴沒(méi)有用過(guò)VSCode調(diào)試,這里詳細(xì)敘述下如何調(diào)試源碼。學(xué)會(huì)調(diào)試源碼后,源碼并沒(méi)有想象中的那么難。
3.4 用 VSCode 調(diào)試項(xiàng)目
vue-dev-server/bin/vue-dev-server.js 文件中這行 app.use(vueMiddleware()) 打上斷點(diǎn)。
找到 vue-dev-server/package.json 的 scripts,把鼠標(biāo)移動(dòng)到 test 命令上,會(huì)出現(xiàn)運(yùn)行腳本和調(diào)試腳本命令。如下圖所示,選擇調(diào)試腳本。


點(diǎn)擊進(jìn)入函數(shù)(F11)按鈕可以進(jìn)入 vueMiddleware 函數(shù)。如果發(fā)現(xiàn)斷點(diǎn)走到不是本項(xiàng)目的文件中,不想看,看不懂的情況,可以退出或者重新來(lái)過(guò)。可以用瀏覽器無(wú)痕(隱私)模式(快捷鍵Ctrl + Shift + N,防止插件干擾)打開(kāi) http://localhost:3000,可以繼續(xù)調(diào)試 vueMiddleware 函數(shù)返回的函數(shù)。
如果你的
VSCode不是中文(不習(xí)慣英文),可以安裝簡(jiǎn)體中文插件[7]。
如果VSCode沒(méi)有這個(gè)調(diào)試功能。建議更新到最新版的VSCode(目前最新版本v1.61.2)。
接著我們來(lái)跟著調(diào)試學(xué)習(xí) vueMiddleware 源碼??梢韵瓤粗骶€,在你覺(jué)得重要的地方繼續(xù)斷點(diǎn)調(diào)試。
4. vueMiddleware 源碼
4.1 有無(wú) vueMiddleware 中間件對(duì)比
不在調(diào)試情況狀態(tài)下,我們可以在 vue-dev-server/bin/vue-dev-server.js 文件中注釋 app.use(vueMiddleware()),執(zhí)行 npm run test 打開(kāi) http://localhost:3000。

再啟用中間件后,如下圖。

看圖我們大概知道了有哪些區(qū)別。
4.2 vueMiddleware 中間件概覽
我們可以找到vue-dev-server/middleware.js,查看這個(gè)中間件函數(shù)的概覽。
//?vue-dev-server/middleware.js
const?vueMiddleware?=?(options?=?defaultOptions)?=>?{
??//?省略
??return?async?(req,?res,?next)?=>?{
????//?省略
????//?對(duì)?.vue?結(jié)尾的文件進(jìn)行處理
????if?(req.path.endsWith('.vue'))?{
????//?對(duì)?.js?結(jié)尾的文件進(jìn)行處理
????}?else?if?(req.path.endsWith('.js'))?{
????//?對(duì)?/__modules/?開(kāi)頭的文件進(jìn)行處理
????}?else?if?(req.path.startsWith('/__modules/'))?{
????}?else?{
??????next()
????}
??}
}
exports.vueMiddleware?=?vueMiddleware
vueMiddleware 最終返回一個(gè)函數(shù)。這個(gè)函數(shù)里主要做了四件事:
對(duì) .vue結(jié)尾的文件進(jìn)行處理對(duì) .js結(jié)尾的文件進(jìn)行處理對(duì) /__modules/開(kāi)頭的文件進(jìn)行處理如果不是以上三種情況,執(zhí)行 next方法,把控制權(quán)交給下一個(gè)中間件
接著我們來(lái)看下具體是怎么處理的。
我們也可以斷點(diǎn)這些重要的地方來(lái)查看實(shí)現(xiàn)。比如:

4.3 對(duì) .vue 結(jié)尾的文件進(jìn)行處理
if?(req.path.endsWith('.vue'))?{
??const?key?=?parseUrl(req).pathname
??let?out?=?await?tryCache(key)
??if?(!out)?{
????//?Bundle?Single-File?Component
????const?result?=?await?bundleSFC(req)
????out?=?result
????cacheData(key,?out,?result.updateTime)
??}
??send(res,?out.code,?'application/javascript')
}
4.3.1 bundleSFC 編譯單文件組件
這個(gè)函數(shù),根據(jù) @vue/component-compiler[8] 轉(zhuǎn)換單文件組件,最終返回瀏覽器能夠識(shí)別的文件。
const?vueCompiler?=?require('@vue/component-compiler')
async?function?bundleSFC?(req)?{
??const?{?filepath,?source,?updateTime?}?=?await?readSource(req)
??const?descriptorResult?=?compiler.compileToDescriptor(filepath,?source)
??const?assembledResult?=?vueCompiler.assemble(compiler,?filepath,?{
????...descriptorResult,
????script:?injectSourceMapToScript(descriptorResult.script),
????styles:?injectSourceMapsToStyles(descriptorResult.styles)
??})
??return?{?...assembledResult,?updateTime?}
}
接著我們來(lái)看 readSource 函數(shù)實(shí)現(xiàn)。
4.3.2 readSource 讀取文件資源
這個(gè)函數(shù)主要作用:根據(jù)請(qǐng)求獲取文件資源。返回文件路徑 filepath、資源 source、和更新時(shí)間 updateTime。
const?path?=?require('path')
const?fs?=?require('fs')
const?readFile?=?require('util').promisify(fs.readFile)
const?stat?=?require('util').promisify(fs.stat)
const?parseUrl?=?require('parseurl')
const?root?=?process.cwd()
async?function?readSource(req)?{
??const?{?pathname?}?=?parseUrl(req)
??const?filepath?=?path.resolve(root,?pathname.replace(/^\//,?''))
??return?{
????filepath,
????source:?await?readFile(filepath,?'utf-8'),
????updateTime:?(await?stat(filepath)).mtime.getTime()
??}
}
exports.readSource?=?readSource
接著我們來(lái)看對(duì) .js 文件的處理
4.4 對(duì) .js 結(jié)尾的文件進(jìn)行處理
if?(req.path.endsWith('.js'))?{
??const?key?=?parseUrl(req).pathname
??let?out?=?await?tryCache(key)
??if?(!out)?{
????//?transform?import?statements
????//?轉(zhuǎn)換?import?語(yǔ)句?
????//?import?Vue?from?'vue'
????//?=>?import?Vue?from?"/__modules/vue"
????const?result?=?await?readSource(req)
????out?=?transformModuleImports(result.source)
????cacheData(key,?out,?result.updateTime)
??}
??send(res,?out,?'application/javascript')
}
針對(duì) vue-dev-server/test/main.js 轉(zhuǎn)換
import?Vue?from?'vue'
import?App?from?'./test.vue'
new?Vue({
??render:?h?=>?h(App)
}).$mount('#app')
//?公眾號(hào):若川視野
//?加微信?ruochuan12
//?參加源碼共讀,一起學(xué)習(xí)源碼
import?Vue?from?"/__modules/vue"
import?App?from?'./test.vue'
new?Vue({
??render:?h?=>?h(App)
}).$mount('#app')
//?公眾號(hào):若川視野
//?加微信?ruochuan12
//?參加源碼共讀,一起學(xué)習(xí)源碼
4.4.1 transformModuleImports 轉(zhuǎn)換 import 引入
recast[9]
validate-npm-package-name[10]
const?recast?=?require('recast')
const?isPkg?=?require('validate-npm-package-name')
function?transformModuleImports(code)?{
??const?ast?=?recast.parse(code)
??recast.types.visit(ast,?{
????visitImportDeclaration(path)?{
??????const?source?=?path.node.source.value
??????if?(!/^\.\/?/.test(source)?&&?isPkg(source))?{
????????path.node.source?=?recast.types.builders.literal(`/__modules/${source}`)
??????}
??????this.traverse(path)
????}
??})
??return?recast.print(ast).code
}
exports.transformModuleImports?=?transformModuleImports
也就是針對(duì) npm 包轉(zhuǎn)換。這里就是 "/__modules/vue"
import?Vue?from?'vue'?=>?import?Vue?from?"/__modules/vue"
4.5 對(duì) /__modules/ 開(kāi)頭的文件進(jìn)行處理
import?Vue?from?"/__modules/vue"
這段代碼最終返回的是讀取路徑 vue-dev-server/node_modules/vue/dist/vue.esm.browser.js 下的文件。
if?(req.path.startsWith('/__modules/'))?{
??//?
??const?key?=?parseUrl(req).pathname
??const?pkg?=?req.path.replace(/^\/__modules\//,?'')
??let?out?=?await?tryCache(key,?false)?//?Do?not?outdate?modules
??if?(!out)?{
????out?=?(await?loadPkg(pkg)).toString()
????cacheData(key,?out,?false)?//?Do?not?outdate?modules
??}
??send(res,?out,?'application/javascript')
}
4.5.1 loadPkg 加載包(這里只支持Vue文件)
目前只支持 Vue 文件,也就是讀取路徑 vue-dev-server/node_modules/vue/dist/vue.esm.browser.js 下的文件返回。
//?vue-dev-server/loadPkg.js
const?fs?=?require('fs')
const?path?=?require('path')
const?readFile?=?require('util').promisify(fs.readFile)
async?function?loadPkg(pkg)?{
??if?(pkg?===?'vue')?{
????//?路徑
????//?vue-dev-server/node_modules/vue/dist
????const?dir?=?path.dirname(require.resolve('vue'))
????const?filepath?=?path.join(dir,?'vue.esm.browser.js')
????return?readFile(filepath)
??}
??else?{
????//?TODO
????//?check?if?the?package?has?a?browser?es?module?that?can?be?used
????//?otherwise?bundle?it?with?rollup?on?the?fly?
????throw?new?Error('npm?imports?support?are?not?ready?yet.')
??}
}
exports.loadPkg?=?loadPkg
至此,我們就基本分析完畢了主文件和一些引入的文件。對(duì)主流程有個(gè)了解。
5. 總結(jié)
最后我們來(lái)看上文中有無(wú) vueMiddleware 中間件的兩張圖總結(jié)一下:

啟用中間件后,如下圖。

瀏覽器支持原生 type=module 模塊請(qǐng)求加載。vue-dev-server 對(duì)其攔截處理,返回瀏覽器支持內(nèi)容,因?yàn)闊o(wú)需打包構(gòu)建,所以速度很快。
<script?type="module">
????import?'./main.js'
script>
5.1 import Vue from 'vue' 轉(zhuǎn)換
//?vue-dev-server/test/main.js
import?Vue?from?'vue'
import?App?from?'./test.vue'
new?Vue({
??render:?h?=>?h(App)
}).$mount('#app')
main.js 中的 import 語(yǔ)句
import Vue from 'vue'
通過(guò) recast[11] 生成 ast 轉(zhuǎn)換成 import Vue from "/__modules/vue"而最終返回給瀏覽器的是 vue-dev-server/node_modules/vue/dist/vue.esm.browser.js
5.2 import App from './test.vue' 轉(zhuǎn)換
main.js 中的引入 .vue 的文件,import App from './test.vue'則用 @vue/component-compiler[12] 轉(zhuǎn)換成瀏覽器支持的文件。
5.3 后續(xù)還能做什么?
鑒于文章篇幅有限,緩存 tryCache 部分目前沒(méi)有分析。簡(jiǎn)單說(shuō)就是使用了 node-lru-cache[13] 最近最少使用 來(lái)做緩存的(這個(gè)算法??迹?。后續(xù)應(yīng)該會(huì)分析這個(gè)倉(cāng)庫(kù)的源碼,歡迎持續(xù)關(guān)注我@若川。
非常建議讀者朋友按照文中方法使用VSCode調(diào)試 vue-dev-server 源碼。源碼中還有很多細(xì)節(jié)文中由于篇幅有限,未全面展開(kāi)講述。
值得一提的是這個(gè)倉(cāng)庫(kù)的 `master` 分支[14],是尤雨溪兩年前寫(xiě)的,相對(duì)本文會(huì)比較復(fù)雜,有余力的讀者可以學(xué)習(xí)。
也可以直接去看 `vite`[15] 源碼。
看完本文,也許你就能發(fā)現(xiàn)其實(shí)前端能做的事情越來(lái)越多,不由感慨:前端水深不可測(cè),唯有持續(xù)學(xué)習(xí)。
參考資料
vuejs組織: https://github.com/vuejs
[2]vue-dev-server: https://github.com/vuejs/vue-dev-server
[3]更多鏈接可以點(diǎn)擊閱讀原文查看
