大家好,我是若川。持續組織了8個月源碼共讀活動,感興趣的可以點此加我微信 ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列
前言
這兩天肝了個Vite插件,本文主要跟大家分享一下它的功能和實現思路.如果你覺得它對你有幫助,請給一個star支持作者 💗.
介紹
vite-plugin-vue-inspector的功能是點擊頁面元素,自動打開本地IDE并跳轉到對應的Vue組件.類似于Vue DevTools
的 Open component in editor
功能。(若川批注:關于原理可以看我寫過的文章:據說 99% 的人不知道 vue-devtools 還能直接打開對應組件文件?本文原理揭秘)

用法
vite-plugin-vue-inspector支持Vue2 & Vue3,并且只需要進行簡單的配置就可以使用.
Vue2
//?vite.config.tsimport?{?defineConfig?}?from?"vite"
import?{?createVuePlugin?}?from?"vite-plugin-vue2"
import?Inspector?from?"vite-plugin-vue-inspector"export?default?defineConfig({plugins:?[createVuePlugin(),Inspector({vue:?2,}),],
})
Vue3
//?vite.config.tsimport?{?defineConfig?}?from?"vite"
import?Vue?from?"@vitejs/plugin-vue"
import?Inspector?from?"vite-plugin-vue-inspector"export?default?defineConfig({plugins:?[Vue(),?Inspector()],
})
IDE也要進行配置,這里就不啰嗦了, 👉 傳送門.
實現思路
看到這里,如果你覺得這個插件索然無味的話先別跑,插件沒意思,看看怎么寫插件還是有點意思的嘛 ! 接下來跟大家介紹一下這個插件的實現思路.
我們先來分析一下實現這個功能我們需要有哪些元素 :
Open IDE
: 打開編輯器功能.Web
層: 提供該功能所需的頁面元素及交互功能.Server
層: 用戶交互時傳遞數據到Server
層,由Server
層調用Open IDE
功能.DOM
=>Vue SFC
映射關系: 告訴OPen IDE
打開哪個文件并定位到對應的行列.
明確我們需要什么元素,我們就可以進一步來梳理它的實現方式,直接曬圖:

實現細節
接下來,我們來看具體的實現細節.在這之前,我們先簡單看下我們需要用到的幾個Vite插件API:
function?VitePluginInspector():?Plugin?{return?{name:?"vite-plugin-vue-inspector",//?應用順序enforce:?"pre",//?應用模式?(只在開發模式應用)apply:?"serve",//?含義:?轉換鉤子,接收每個傳入請求模塊的內容和文件路徑//?應用:?在這個鉤子對SFC模版進行解析并注入自定義屬性transform(code,?id)?{},//?含義:?配置開發服務器鉤子,可以添加自定義中間件//?應用:?在這個鉤子實現Open?Editor調用服務configureServer(server)?{},//?含義:?轉換index.html的專用鉤子,接收當前HTML字符串和轉換上下文//?應用:?在這個鉤子注入交互功能transformIndexHtml(html)?{},}
}
解析SFC模版 & 注入自定義屬性
這部分的實現主要分為兩步:
SFC Template
=>AST
獲取元素所在組件的行和列的編號
獲取自定義屬性插入的位置
注入自定義屬性
file (SFC路徑,用于跳轉到指定文件)
line (元素所在行編號,用于跳轉到指定行)
column (元素所在列編號,用于跳轉到指定列)
title (SFC名稱,用于展示)
//?vite.config.tsfunction?VitePluginInspector():?Plugin?{return?{name:?"vite-plugin-vue-inspector",transform(code,?id)?{const?{?filename,?query?}?=?parseVueRequest(id)//?只處理SFC文件if?(filename.endsWith(".vue")?&&?query.type?!==?"style")?return?compileSFCTemplate(code,?filename)return?code},}
}
//?compiler.tsimport?path?from?"path"
import?MagicString?from?"magic-string"
import?{?parse,?transform?}?from?"@vue/compiler-dom"const?EXCLUDE_TAG?=?["template",?"script",?"style"]export?async?function?compileSFCTemplate(code:?string,id:?string,
)?{//?MagicString是一個非常好用的字符串操作庫,也如它的名字一樣,非常的神奇?!//?有了它,我們可以直接操作字符串,避免操作AST,換來更好的性能.?Vue3的實現也大量的用到了它.const?s?=?new?MagicString(code)//?SFC?=>?ASTconst?ast?=?parse(code,?{?comments:?true?})const?result?=?await?new?Promise((resolve)?=>?{transform(ast,?{//?ast?node節點訪問器nodeTransforms:?[(node)?=>?{if?(node.type?===?1)?{//?只解析html標簽?if?(node.tagType?===?0?&&?!EXCLUDE_TAG.includes(node.tag))?{const?{?base?}?=?path.parse(id)//?獲取到相關信息,并進行自定義屬性注入!node.loc.source.includes("data-v-inspecotr-file")&&?s.prependLeft(node.loc.start.offset?+?node.tag.length?+?1,`?data-v-inspecotr-file="${id}"?data-v-inspecotr-line=${node.loc.start.line}?data-v-inspecotr-column=${node.loc.start.column}?data-v-inspecotr-title="${base}"`,)}}},],})resolve(s.toString())})return?result
}
注入后的DOM元素
長這樣 :
<h3?data-v-inspector-file="/xxx/src/Hi.vue"???data-v-inspector-line="3"?data-v-inspector-column="5"?data-v-inspector-title="Hi.vue">
</h3>
Open Editor Server服務
前面我們提到了創建Server服務的思路是在vite的configureServer
的鉤子函數注入中間件:
//?vite.config.tsfunction?VitePluginInspector():?Plugin?{return?{name:?"vite-plugin-vue-inspector",configureServer(server)?{//?注冊中間件//?請求Query參數解析中間件?server.middlewares.use(queryParserMiddleware)//?Open?Edito服務中間件server.middlewares.use(launchEditorMiddleware)},}
}
//?middleware.ts//?請求Query參數解析中間件?
export?const?queryParserMiddleware:?Connect.NextHandleFunction?=?(req:?RequestMessage?&?{query?:?object},_,next,
)?=>?{if?(!req.query?&&?req.url?.startsWith(SERVER_URL))?{const?url?=?new?URL(req.url,?"http://domain.inspector")req.query?=?Object.fromEntries(url.searchParams.entries())}next()
}//?Open?Editor服務中間件
export?const?launchEditorMiddleware:?Connect.NextHandleFunction?=?(req:?RequestMessage?&?{query?:?{?line:?number;?column:?number;?file:?string?}},res,next,
)?=>?{//?只處理Open?Editor接口if?(req.url.startsWith(SERVER_URL))?{//?解析SFC路徑,行號,列號const?{?file,?line,?column?}?=?req.queryif?(!file)?{res.statusCode?=?500res.end("launch-editor-middleware:?required?query?param?\"file\"?is?missing.")}const?lineNumber?=?+line?||?1const?columnNumber?=?+column?||?1//?見下方鏈接launchEditor(file,?lineNumber,?columnNumber)res.end()}else?{next()}
}
關于launchEditor
的具體邏輯我直接fork了react-dev-utils的實現,它支持很多IDE (vscode
,atom
,webstorm
...),它的大致原理就是通過維護一些進程映射表和環境變量,然后通過調用Node.js
的子進程喚醒IDE:
child_process.spawn(editor,?args,?{?stdio:?'inherit'?});
交互功能注入
這個功能的實現原理其實就在transformIndexHtml
注入功能所需要的html,scripts,styles
.
//?vite.config.tsfunction?VitePluginInspector():?Plugin?{return?{transformIndexHtml(html)?{return?{html,tags:?[{tag:?"script",children:?...,injectTo:?"body",},?{tag:?"script",attrs:?{type:?"module",},children:?scripts,injectTo:?"body",},?{tag:?"style",children:?styles,injectTo:?"head",}],}}}
}
關于交互的頁面實現有很多種,最簡單的無非就是編寫原生js
,這樣我們無需任何編譯就可以直接注入到html
中,但是用原生js
來寫頁面真的是慢又不好維護,于是我選擇了Vue
進行開發,使用Vue
就意味著要進行編譯才能在瀏覽器中跑起來.為了這個所謂的研發體驗,又折騰了一波,大概過程就是通過compile-sfc
等包編譯出render函數,樣式代碼等
,為了兼容Vue2
,我又引入了祖傳的vue-template-compiler
...噼里啪啦噼里啪啦..感興趣的童鞋可以點傳送門詳看. (u1s1,還是有點意思的!!) 當然了,這部分的編譯都是在插件打包時完成的,用戶在使用插件的時候并不會有這部分的運行時開銷.
致謝
這個項目的靈感來自于react-dev-inspector,使用React
的童鞋可以看看.
結語
在做這個插件的時候也踩了一些坑,通過查看vue,vite
等源碼排查解決.這里給想看源碼的童鞋一個建議,從實踐和帶著問題的角度出發,也許會有更好的效果和更深刻的印象 (教訓) :)
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經堅持寫了8年,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助3000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
掃碼加我微信 ruochuan02、拉你進源碼共讀群
今日話題
略。分享、收藏、點贊、在看我的文章就是對我最大的支持~