1. 前言
大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以加我微信 ruochuan12
想學源碼,極力推薦之前我寫的《學習源碼整體架構系列》jQuery
、underscore
、lodash
、vuex
、sentry
、axios
、redux
、koa
、vue-devtools
、vuex4
、koa-compose
、vue-next-release
、vue-this
、create-vue
等10余篇源碼文章。
最近組織了源碼共讀活動
在 vuejs組織[1] 下,找到了尤雨溪幾年前寫的“玩具 vite”vue-dev-server[2],發現100來行代碼,很值得學習。于是有了這篇文章。
閱讀本文,你將學到:
1.?學會?vite?簡單原理
2.?學會使用?VSCode?調試源碼
3.?學會如何編譯?Vue?單文件組件
4.?學會如何使用?recast?生成?ast?轉換文件
5.?如何加載包文件
6.?等等
2. vue-dev-server 它的原理是什么
vue-dev-server#how-it-works[3]README
文檔上有四句英文介紹。
發現谷歌翻譯[4]的還比較準確,我就原封不動的搬運過來。
瀏覽器請求導入作為原生 ES 模塊導入 - 沒有捆綁。
服務器攔截對 *.vue 文件的請求,即時編譯它們,然后將它們作為 JavaScript 發回。
對于提供在瀏覽器中工作的 ES 模塊構建的庫,只需直接從 CDN 導入它們。
導入到 .js 文件中的 npm 包(僅包名稱)會即時重寫以指向本地安裝的文件。 目前,僅支持 vue 作為特例。 其他包可能需要進行轉換才能作為本地瀏覽器目標 ES 模塊公開。
也可以看看vitejs 文檔[5],了解下原理,文檔中圖畫得非常好。
看完本文后,我相信你會有一個比較深刻的理解。
3. 準備工作
3.1 克隆項目
本文倉庫 vue-dev-server-analysis,求個star^_^[6]
#?推薦克隆我的倉庫
git?clone?https://github.com/lxchuan12/vue-dev-server-analysis.git
cd?vue-dev-server-analysis/vue-dev-server
#?npm?i?-g?yarn
#?安裝依賴
yarn#?或者克隆官方倉庫
git?clone?https://github.com/vuejs/vue-dev-server.git
cd?vue-dev-server
#?npm?i?-g?yarn
#?安裝依賴
yarn
一般來說,我們看源碼先從package.json
文件開始:
//?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",//?指定可執行的命令"bin":?{"vue-dev-server":?"./bin/vue-dev-server.js"},"scripts":?{//?先跳轉到?test?文件夾,再用?Node?執行?vue-dev-server?文件"test":?"cd?test?&&?node?../bin/vue-dev-server.js"}
}
根據 scripts
test
命令。我們來看 test
文件夾。
3.2 test 文件夾
vue-dev-server/test
文件夾下有三個文件,代碼不長。
index.html
main.js
text.vue
如圖下圖所示。

接著我們找到 vue-dev-server/bin/vue-dev-server.js
文件,代碼也不長。
3.3 vue-dev-server.js
//?vue-dev-server/bin/vue-dev-server.js
#!/usr/bin/env?nodeconst?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')
})
原來就是express
啟動了端口3000
的服務。重點在 vueMiddleware
中間件。接著我們來調試這個中間件。
鑒于估計很多小伙伴沒有用過VSCode
調試,這里詳細敘述下如何調試源碼。學會調試源碼后,源碼并沒有想象中的那么難。
3.4 用 VSCode 調試項目
vue-dev-server/bin/vue-dev-server.js
文件中這行 app.use(vueMiddleware())
打上斷點。
找到 vue-dev-server/package.json
的 scripts
,把鼠標移動到 test
命令上,會出現運行腳本
和調試腳本
命令。如下圖所示,選擇調試腳本。


點擊進入函數(F11)
按鈕可以進入 vueMiddleware
函數。如果發現斷點走到不是本項目的文件中,不想看,看不懂的情況,可以退出或者重新來過。可以用瀏覽器無痕(隱私)模式(快捷鍵Ctrl + Shift + N
,防止插件干擾)打開 http://localhost:3000
,可以繼續調試 vueMiddleware
函數返回的函數。
如果你的
VSCode
不是中文(不習慣英文),可以安裝簡體中文插件[7]。
如果VSCode
沒有這個調試功能。建議更新到最新版的VSCode
(目前最新版本v1.61.2
)。
接著我們來跟著調試學習 vueMiddleware
源碼。可以先看主線,在你覺得重要的地方繼續斷點調試。
4. vueMiddleware 源碼
4.1 有無 vueMiddleware 中間件對比
不在調試情況狀態下,我們可以在 vue-dev-server/bin/vue-dev-server.js
文件中注釋 app.use(vueMiddleware())
,執行 npm run test
打開 http://localhost:3000
。

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

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

4.3 對 .vue 結尾的文件進行處理
if?(req.path.endsWith('.vue'))?{const?key?=?parseUrl(req).pathnamelet?out?=?await?tryCache(key)if?(!out)?{//?Bundle?Single-File?Componentconst?result?=?await?bundleSFC(req)out?=?resultcacheData(key,?out,?result.updateTime)}send(res,?out.code,?'application/javascript')
}
4.3.1 bundleSFC 編譯單文件組件
這個函數,根據 @vue/component-compiler[8] 轉換單文件組件,最終返回瀏覽器能夠識別的文件。
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?}
}
接著我們來看 readSource
函數實現。
4.3.2 readSource 讀取文件資源
這個函數主要作用:根據請求獲取文件資源。返回文件路徑 filepath
、資源 source
、和更新時間 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
接著我們來看對 .js 文件的處理
4.4 對 .js 結尾的文件進行處理
if?(req.path.endsWith('.js'))?{const?key?=?parseUrl(req).pathnamelet?out?=?await?tryCache(key)if?(!out)?{//?transform?import?statements//?轉換?import?語句?//?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')
}
針對 vue-dev-server/test/main.js
轉換
import?Vue?from?'vue'
import?App?from?'./test.vue'new?Vue({render:?h?=>?h(App)
}).$mount('#app')//?公眾號:若川視野
//?加微信?ruochuan12
//?參加源碼共讀,一起學習源碼
import?Vue?from?"/__modules/vue"
import?App?from?'./test.vue'new?Vue({render:?h?=>?h(App)
}).$mount('#app')//?公眾號:若川視野
//?加微信?ruochuan12
//?參加源碼共讀,一起學習源碼
4.4.1 transformModuleImports 轉換 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.valueif?(!/^\.\/?/.test(source)?&&?isPkg(source))?{path.node.source?=?recast.types.builders.literal(`/__modules/${source}`)}this.traverse(path)}})return?recast.print(ast).code
}exports.transformModuleImports?=?transformModuleImports
也就是針對 npm
包轉換。 這里就是 "/__modules/vue"
import?Vue?from?'vue'?=>?import?Vue?from?"/__modules/vue"
4.5 對 /__modules/ 開頭的文件進行處理
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).pathnameconst?pkg?=?req.path.replace(/^\/__modules\//,?'')let?out?=?await?tryCache(key,?false)?//?Do?not?outdate?modulesif?(!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/distconst?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
至此,我們就基本分析完畢了主文件和一些引入的文件。對主流程有個了解。
5. 總結
最后我們來看上文中有無 vueMiddleware 中間件的兩張圖總結一下:

啟用中間件后,如下圖。

瀏覽器支持原生 type=module
模塊請求加載。vue-dev-server
對其攔截處理,返回瀏覽器支持內容,因為無需打包構建,所以速度很快。
<script?type="module">import?'./main.js'
</script>
5.1 import Vue from 'vue' 轉換
//?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 語句 import Vue from 'vue' 通過 recast[11] 生成 ast 轉換成 import Vue from "/__modules/vue"
而最終返回給瀏覽器的是 vue-dev-server/node_modules/vue/dist/vue.esm.browser.js
5.2 import App from './test.vue' 轉換
main.js
中的引入 .vue
的文件,import App from './test.vue'
則用 @vue/component-compiler[12] 轉換成瀏覽器支持的文件。
5.3 后續還能做什么?
鑒于文章篇幅有限,緩存 tryCache
部分目前沒有分析。簡單說就是使用了 node-lru-cache[13]最近最少使用
來做緩存的(這個算法常考)。后續應該會分析這個倉庫的源碼,歡迎持續關注我@若川。
非常建議讀者朋友按照文中方法使用VSCode
調試 vue-dev-server
源碼。源碼中還有很多細節文中由于篇幅有限,未全面展開講述。
值得一提的是這個倉庫的 `master` 分支[14],是尤雨溪兩年前寫的,相對本文會比較復雜,有余力的讀者可以學習。
也可以直接去看 `vite`[15] 源碼。
看完本文,也許你就能發現其實前端能做的事情越來越多,不由感慨:前端水深不可測,唯有持續學習。
最后歡迎加我微信 ruochuan12源碼共讀 活動,大家一起學習源碼,共同進步。
參考資料
[1]
vuejs組織: https://github.com/vuejs
[2]vue-dev-server: https://github.com/vuejs/vue-dev-server
[3]更多鏈接可以點擊閱讀原文查看
最近組建了一個江西人的前端交流群,如果你是江西人可以加我微信?ruochuan12?私信 江西?拉你進群。
推薦閱讀
1個月,200+人,一起讀了4周源碼
我歷時3年才寫了10余篇源碼文章,但收獲了100w+閱讀
老姚淺談:怎么學JavaScript?
我在阿里招前端,該怎么幫你(可進面試群)
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。歡迎分享、收藏、點贊、在看我的公眾號文章~