<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在 vuejs組織 下,找到了尤雨溪幾年前寫的“玩具 vite” vue-dev-server,發現100來行程式碼,很值得學習。於是有了這篇文章。
閱讀本文,你將學到:
1. 學會 vite 簡單原理
2. 學會使用 VSCode 偵錯原始碼
3. 學會如何編譯 Vue 單檔案元件
4. 學會如何使用 recast 生成 ast 轉換檔案
5. 如何載入包檔案
vue-dev-server#how-it-works README
檔案上有四句英文介紹。
發現谷歌翻譯的還比較準確,我就原封不動的搬運過來。
也可以看看vitejs 檔案,瞭解下原理,檔案中圖畫得非常好。
看完本文後,我相信你會有一個比較深刻的理解。
# 推薦克隆我的倉庫 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
資料夾。
vue-dev-server/test
資料夾下有三個檔案,程式碼不長。
如圖下圖所示。
接著我們找到 vue-dev-server/bin/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
偵錯,這裡詳細敘述下如何偵錯原始碼。學會偵錯原始碼後,原始碼並沒有想象中的那麼難。
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
不是中文(不習慣英文),可以安裝簡體中文外掛。
如果 VSCode
沒有這個偵錯功能。建議更新到最新版的 VSCode
(目前最新版本 v1.61.2
)。
接著我們來跟著偵錯學習 vueMiddleware
原始碼。可以先看主線,在你覺得重要的地方繼續斷點偵錯。
不在偵錯情況狀態下,我們可以在 vue-dev-server/bin/vue-dev-server.js
檔案中註釋 app.use(vueMiddleware())
,執行 npm run test
開啟 http://localhost:3000
。
再啟用中介軟體後,如下圖。
看圖我們大概知道了有哪些區別。
我們可以找到vue-dev-server/middleware.js
,檢視這個中介軟體函數的概覽。
// 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
最終返回一個函數。這個函數裡主要做了四件事:
.vue
結尾的檔案進行處理.js
結尾的檔案進行處理/__modules/
開頭的檔案進行處理next
方法,把控制權交給下一個中介軟體接著我們來看下具體是怎麼處理的。
我們也可以斷點這些重要的地方來檢視實現。比如:
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') }
這個函數,根據 @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
函數實現。
這個函數主要作用:根據請求獲取檔案資源。返回檔案路徑 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 檔案的處理
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')
也就是針對 npm
包轉換。 這裡就是 "/__modules/vue"
import Vue from 'vue' => import Vue from "/__modules/vue"
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') }
目前只支援 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
至此,我們就基本分析完畢了主檔案和一些引入的檔案。對主流程有個瞭解。
最後我們來看上文中有無 vueMiddleware 中介軟體的兩張圖總結一下:
啟用中介軟體後,如下圖。
瀏覽器支援原生 type=module
模組請求載入。vue-dev-server
對其攔截處理,返回瀏覽器支援內容,因為無需打包構建,所以速度很快。
<script type="module"> import './main.js' </script>
// 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
main.js
中的引入 .vue
的檔案,import App from './test.vue'
則用 @vue/component-compiler 轉換成瀏覽器支援的檔案。
鑑於文章篇幅有限,快取 tryCache
部分目前沒有分析。簡單說就是使用了 node-lru-cache 最近最少使用 來做快取的(這個演演算法常考)。後續應該會分析這個倉庫的原始碼,歡迎持續關注我@若川。
非常建議讀者朋友按照文中方法使用VSCode
偵錯 vue-dev-server
原始碼。原始碼中還有很多細節文中由於篇幅有限,未全面展開講述。
值得一提的是這個倉庫的 master 分支,是尤雨溪兩年前寫的,相對本文會比較複雜,有餘力的讀者可以學習。
也可以直接去看 vite 原始碼。
看完本文,也許你就能發現其實前端能做的事情越來越多,不由感慨:前端水深不可測,唯有持續學習,更多關於vue dev server理解vite原理的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45