尤雨溪開發(fā)vue?dev?server理解vite原理
1.引言
在 vuejs組織 下,找到了尤雨溪幾年前寫的“玩具 vite” vue-dev-server,發(fā)現(xiàn)100來行代碼,很值得學習。于是有了這篇文章。
閱讀本文,你將學到:
1. 學會 vite 簡單原理
2. 學會使用 VSCode 調試源碼
3. 學會如何編譯 Vue 單文件組件
4. 學會如何使用 recast 生成 ast 轉換文件
5. 如何加載包文件
2. vue-dev-server 它的原理是什么
vue-dev-server#how-it-works README 文檔上有四句英文介紹。
發(fā)現(xiàn)谷歌翻譯的還比較準確,我就原封不動的搬運過來。
- 瀏覽器請求導入作為原生 ES 模塊導入 - 沒有捆綁。
- 服務器攔截對 *.vue 文件的請求,即時編譯它們,然后將它們作為 JavaScript 發(fā)回。
- 對于提供在瀏覽器中工作的 ES 模塊構建的庫,只需直接從 CDN 導入它們。
- 導入到 .js 文件中的 npm 包(僅包名稱)會即時重寫以指向本地安裝的文件。 目前,僅支持 vue 作為特例。 其他包可能需要進行轉換才能作為本地瀏覽器目標 ES 模塊公開。
也可以看看vitejs 文檔,了解下原理,文檔中圖畫得非常好。

看完本文后,我相信你會有一個比較深刻的理解。
3. 準備工作
3.1 克隆項目
# 推薦克隆我的倉庫 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",
// 指定可執(zhí)行的命令
"bin": {
"vue-dev-server": "./bin/vue-dev-server.js"
},
"scripts": {
// 先跳轉到 test 文件夾,再用 Node 執(zhí)行 vue-dev-server 文件
"test": "cd test && node ../bin/vue-dev-server.js"
}
}
根據(jù) 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 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')
})
原來就是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 命令上,會出現(xiàn)運行腳本和調試腳本命令。如下圖所示,選擇調試腳本。


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

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

看圖我們大概知道了有哪些區(qū)別。
4.2 vueMiddleware 中間件概覽
我們可以找到vue-dev-server/middleware.js,查看這個中間件函數(shù)的概覽。
// vue-dev-server/middleware.js
const 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 最終返回一個函數(shù)。這個函數(shù)里主要做了四件事:
- 對
.vue結尾的文件進行處理 - 對
.js結尾的文件進行處理 - 對
/__modules/開頭的文件進行處理 - 如果不是以上三種情況,執(zhí)行
next方法,把控制權交給下一個中間件
接著我們來看下具體是怎么處理的。
我們也可以斷點這些重要的地方來查看實現(xiàn)。比如:

4.3 對 .vue 結尾的文件進行處理
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 編譯單文件組件
這個函數(shù),根據(jù) @vue/component-compiler 轉換單文件組件,最終返回瀏覽器能夠識別的文件。
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 函數(shù)實現(xiàn)。
4.3.2 readSource 讀取文件資源
這個函數(shù)主要作用:根據(jù)請求獲取文件資源。返回文件路徑 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).pathname
let 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')
import Vue from "/__modules/vue"
import App from './test.vue'
new Vue({
render: h => h(App)
}).$mount('#app')
4.4.1 transformModuleImports 轉換 import 引入
也就是針對 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).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
至此,我們就基本分析完畢了主文件和一些引入的文件。對主流程有個了解。
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 生成 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 轉換成瀏覽器支持的文件。
5.3 后續(xù)還能做什么?
鑒于文章篇幅有限,緩存 tryCache 部分目前沒有分析。簡單說就是使用了 node-lru-cache 最近最少使用 來做緩存的(這個算法??迹?。后續(xù)應該會分析這個倉庫的源碼,歡迎持續(xù)關注我@若川。
非常建議讀者朋友按照文中方法使用VSCode調試 vue-dev-server 源碼。源碼中還有很多細節(jié)文中由于篇幅有限,未全面展開講述。
值得一提的是這個倉庫的 master 分支,是尤雨溪兩年前寫的,相對本文會比較復雜,有余力的讀者可以學習。
也可以直接去看 vite 源碼。
看完本文,也許你就能發(fā)現(xiàn)其實前端能做的事情越來越多,不由感慨:前端水深不可測,唯有持續(xù)學習,更多關于vue dev server理解vite原理的資料請關注腳本之家其它相關文章!
相關文章
Vue條件循環(huán)判斷+計算屬性+綁定樣式v-bind的實例
今天小編就為大家分享一篇Vue條件循環(huán)判斷+計算屬性+綁定樣式v-bind的實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-09-09
結合el-upload組件實現(xiàn)大文件分片上傳功能
Element UI的el-upload上傳組件相信各位小伙伴都已經(jīng)非常熟悉,最近接了一個新需求,要求在el-upload組件基礎上實現(xiàn)分片上傳功能,即小于等于5M文件正常上傳,大于5M文件切成5M每片上傳,那么這個功能怎么實現(xiàn)呢?一起看看吧2022-09-09
Vue報錯Component?name"Home"should?always?be?mult
這篇文章主要介紹了Vue報錯Component?name"Home"should?always?be?multi問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09
vue swipeCell滑動單元格(仿微信)的實現(xiàn)示例
這篇文章主要介紹了vue swipeCell滑動單元格(仿微信)的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-09-09
解決antd日期選擇組件,添加value就無法點擊下一年和下一月問題
這篇文章主要介紹了解決antd日期選擇組件,添加value就無法點擊下一年和下一月問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10
vue新玩法VueUse工具庫具體用法@vueuse/core詳解
這篇文章主要介紹了vue新玩法VueUse-工具庫@vueuse/core,VueUse不是Vue.use,它是一個基于?Composition?API?的實用函數(shù)集合,下面是具體的一些用法,需要的朋友可以參考下2022-08-08

