如何在 Vue 項(xiàng)目中,通過(guò)點(diǎn)擊 DOM 自動(dòng)定位VSCode中的代碼行?
作者 | vivo 互聯(lián)網(wǎng)大前端團(tuán)隊(duì)- Youchen
一、背景
現(xiàn)在大型的 Vue項(xiàng)目基本上都是多人協(xié)作開(kāi)發(fā),并且隨著版本的迭代,Vue 項(xiàng)目中的組件數(shù)也會(huì)越來(lái)越多,如果此時(shí)讓你負(fù)責(zé)不熟悉的頁(yè)面功能開(kāi)發(fā),甚至你才剛剛加入這個(gè)項(xiàng)目,那么怎么樣才能快速找到相關(guān)組件在整個(gè)項(xiàng)目代碼中的文件位置呢?想必大家都有采取過(guò)以下這幾種方法:
- 【搜類(lèi)名】,在工程文件里搜索頁(yè)面 DOM元素中的樣式類(lèi)名
- 【找路由】,根據(jù)頁(yè)面鏈接找到Vue路由匹配的頁(yè)面組件
- 【找人】,找到當(dāng)初負(fù)責(zé)開(kāi)發(fā)該頁(yè)面的人詢(xún)問(wèn)對(duì)應(yīng)的代碼路徑
以上幾種方法確實(shí)能夠幫助我們找到具體的代碼文件路徑,但都需要人工去搜索,并不是很高效,那有沒(méi)有其它更高效的方式呢?
答案是有的。Vue官方就提供了一款 vue-devtools 插件,使用該插件就能自動(dòng)在 VSCode 中打開(kāi)對(duì)應(yīng)頁(yè)面組件的源代碼文件,操作路徑如下:
使用vue-devtools插件可以很好地提高我們查找對(duì)應(yīng)頁(yè)面組件代碼的效率,但只能定位到對(duì)應(yīng)的組件代碼,如果我們想要直接找到頁(yè)面上某個(gè)元素相關(guān)的具體代碼位置,還需要在當(dāng)前組件源代碼中進(jìn)行二次查找,并且每次都要先選擇組件,再點(diǎn)擊打開(kāi)按鈕才能打開(kāi)代碼文件,不是特別快捷。
針對(duì)這個(gè)問(wèn)題,我們開(kāi)發(fā)了輕量級(jí)的頁(yè)面元素代碼映射插件,使用該插件可以通過(guò)點(diǎn)擊頁(yè)面元素的方式,一鍵打開(kāi)對(duì)應(yīng)代碼源文件,并且精準(zhǔn)定位對(duì)應(yīng)代碼行,無(wú)需手動(dòng)查找,能夠極大地提高開(kāi)發(fā)效率和體驗(yàn),實(shí)際的使用效果如下:
二、實(shí)現(xiàn)原理
整個(gè)插件主要分為3個(gè)功能模塊:client、server、add-code-location,client端發(fā)送特定請(qǐng)求給server端,server端接收到該請(qǐng)求后執(zhí)行定位代碼行命令,而add-code-location模塊用于源碼的轉(zhuǎn)換。
2.1 client
client端這里其實(shí)就是指瀏覽器,我們?cè)邳c(diǎn)擊頁(yè)面元素時(shí),瀏覽器就會(huì)發(fā)送一個(gè)特定請(qǐng)求給server端,該請(qǐng)求信息包含了具體的代碼文件路徑和對(duì)應(yīng)代碼行號(hào)信息。
function openEditor(filePath) {
axios
.get(`${protocol}//${host}:${port}/code`, {
params: {
filePath: `${filePath}`
}
})
.catch(error => {
console.log(error)
})
}
而監(jiān)聽(tīng)頁(yè)面元素的點(diǎn)擊事件則通過(guò)事件代理的方式全局監(jiān)聽(tīng),給document綁定了點(diǎn)擊事件,監(jiān)聽(tīng)鍵盤(pán)和鼠標(biāo)點(diǎn)擊組合事件來(lái)發(fā)起定位代碼行請(qǐng)求,避免和頁(yè)面原生的click事件發(fā)生沖突。
function openCode(e) {
if (isShiftKey || isMetaKey || e.metaKey || e.shiftKey) {
e.preventDefault()
const filePath = getFilePath(e.target)
openEditor(filePath)
}
...
}
2.2 server
server端是指本地起的一個(gè)服務(wù)器,可以監(jiān)聽(tīng)client端發(fā)送的特定請(qǐng)求,當(dāng)接收到執(zhí)行定位命令的請(qǐng)求時(shí),執(zhí)行VSCode打開(kāi)代碼文件命令,并定位到對(duì)應(yīng)的代碼行。
2.2.1 webpack devServer
如果是采用webpack構(gòu)建的項(xiàng)目,webpack的devServer開(kāi)發(fā)服務(wù)器已經(jīng)提供了一個(gè)before屬性,可以通過(guò)它來(lái)監(jiān)聽(tīng)發(fā)送給開(kāi)發(fā)服務(wù)器的請(qǐng)求。
before: function (app) {
app.get('/code', function (req, res) {
if (req.query.filePath) {
// 執(zhí)行vscode定位代碼行命令
openCodeFile(req.query.filePath)
...
}
...
})
}
2.2.2 vite configureServer
如果是采用Vite構(gòu)建的項(xiàng)目,可以使用Vite插件來(lái)實(shí)現(xiàn)server端監(jiān)聽(tīng)特定請(qǐng)求,Vite插件擴(kuò)展于rollup插件接口,并且在原有的基礎(chǔ)上增加了一些特有的鉤子函數(shù),例如configureServer鉤子,通過(guò)該鉤子函數(shù)可以用于配置開(kāi)發(fā)服務(wù)器來(lái)監(jiān)聽(tīng)特定的請(qǐng)求。
const codeServer = () => ({
name: 'open-code-vite-server',
configureServer(server) {
server.middlewares.use((req, res, next) => {
...
if (pathname == '/code') {
...
if (filePath) {
openCodeFile(filePath) // 執(zhí)行vscode定位代碼行命令
...
}
res.end()
}
...
})
}
})
2.2.3 執(zhí)行 VSCode 定位命令
當(dāng)server端監(jiān)聽(tīng)到client端發(fā)送的特定請(qǐng)求后,接下來(lái)就是執(zhí)行VSCode定位代碼行命令。實(shí)際上,VSCode編輯器是可以通過(guò)code命令來(lái)啟動(dòng),并且可以相應(yīng)使用一些命令行參數(shù),例如:
"code --reuse-window"或"code -r"命令可以打開(kāi)最后活動(dòng)窗口的文件或文件夾;"code --goto"或"code -g"命令后面可以拼接具體文件路徑和行列號(hào),當(dāng)使用"code -g file:line:column"命令時(shí)可以打開(kāi)某個(gè)文件并定位到具體的行列位置。
利用 VSCode 編輯器的這個(gè)特性,我們就能實(shí)現(xiàn)自動(dòng)定位代碼行功能,對(duì)應(yīng)的代碼路徑信息可以從client端發(fā)送的請(qǐng)求信息當(dāng)中獲得,再借助node的child_process.exec方法來(lái)執(zhí)行VSCode定位代碼行命令。
const child_process = require('child_process')
function openCodeFile(path) {
let pathBefore = __dirname.substring(0, __dirname.search('node_modules'))
let filePath = pathBefore + path
child_process.exec(`code -r -g ${filePath}`)
}
另外,為了正常使用 VSCode 的 Code命令,我們需要確保添加VSCode Code命令到環(huán)境變量當(dāng)中。Mac系統(tǒng)用戶(hù)可以在VSCode界面使用command+shift+p快捷鍵,然后搜索Code 并選擇install 'code' command in path;Windows用戶(hù)可以找到VSCode安裝位置的bin文件夾目錄,并將該目錄添加到系統(tǒng)環(huán)境變量當(dāng)中。
2.3 add-code-location
通過(guò)前面的介紹,大家應(yīng)該了解了client端和server端的執(zhí)行機(jī)制,并且在執(zhí)行定位命令時(shí)需要獲取到頁(yè)面元素的代碼路徑,而具體的代碼路徑是以屬性的方式綁定到了DOM元素上,這時(shí)候就需要用到add-code-location模塊在編譯時(shí)轉(zhuǎn)換我們的源碼,并給 DOM元素添加對(duì)應(yīng)的代碼路徑屬性。
整個(gè)源碼轉(zhuǎn)換處理流程如下:
2.3.1 獲取文件路徑
源碼轉(zhuǎn)換過(guò)程的第一步是獲取代碼文件的具體路徑,對(duì)于webpack打包的項(xiàng)目來(lái)說(shuō),webpack loader用來(lái)處理源碼字符串再合適不過(guò),loader的上下文this對(duì)象包含一個(gè)resourcePath資源文件的路徑屬性,利用這個(gè)屬性我們很容易就能獲得每個(gè)代碼文件的具體路徑。
module.exports = function (source) {
const { resourcePath } = this
return sourceCodeChange(source, resourcePath)
}
對(duì)于Vite構(gòu)建的項(xiàng)目來(lái)說(shuō),源碼的轉(zhuǎn)化操作也是通過(guò)插件來(lái)完成,Vite插件有通用的鉤子transform,可用于轉(zhuǎn)換已加載的模塊內(nèi)容,它接收兩個(gè)參數(shù),code參數(shù)代表著源碼字符串,id參數(shù)是文件的全路徑。
module.exports = function() {
return {
name: 'add-code-location',
transform(code, id) {
...
return sourceCodeChange(code, id)
}
}
}
2.3.2 計(jì)算代碼行號(hào)
接著在遍歷源碼文件的過(guò)程中,需要處理對(duì)應(yīng)Vue文件template模板中的代碼,以“\n”分割template模板部分字符串為數(shù)組,通過(guò)數(shù)組的索引即可精準(zhǔn)得到每一行html標(biāo)簽的代碼行號(hào)。
function codeLineTrack(str, resourcePath) {
let lineList = str.split('\n')
let newList = []
lineList.forEach((item, index) => {
newList.push(addLineAttr(item, index + 1, resourcePath)) // 添加位置屬性,index+1為具體的代碼行號(hào)
})
return newList.join('\n')
}
2.3.3 添加位置屬性
在獲取到代碼文件路徑和代碼行號(hào)以后,接下來(lái)就是對(duì)Vue template模板中分割的每一行標(biāo)簽元素添加最終的位置屬性。這里采用的是正則替換的方式來(lái)添加位置屬性,分別對(duì)每一行標(biāo)簽元素先正則匹配出所有元素的開(kāi)始標(biāo)簽部分,例如<div、<span、<img等,然后將其正則替換成帶有code-location屬性的開(kāi)始標(biāo)簽,對(duì)應(yīng)的屬性值就是前面獲取的代碼路徑和對(duì)應(yīng)標(biāo)簽的行號(hào)。
function addLineAttr(lineStr, line, resourcePath) {
let reg = /<[\w-]+/g
let leftTagList = lineStr.match(reg)
if (leftTagList) {
leftTagList = Array.from(new Set(leftTagList))
leftTagList.forEach(item => {
if (item && item.indexOf('template') == -1) {
let regx = new RegExp(`${item}`, 'g')
let location = `${item} code-location="${resourcePath}:${line}"`
lineStr = lineStr.replace(regx, location)
}
})
}
return lineStr
}
2.4 其他處理
2.4.1 源碼相對(duì)路徑
在給DOM元素添加對(duì)應(yīng)的源碼位置屬性時(shí),實(shí)際上采用的是相對(duì)路徑,這樣可以使得DOM元素上的屬性值更加簡(jiǎn)潔明了。node_modules文件夾通常是在項(xiàng)目的根目錄下,而插件是以npm包的形式安裝在node_modules路徑下,利用node的__dirname變量可以獲得當(dāng)前模塊的絕對(duì)路徑,因此在源碼轉(zhuǎn)換過(guò)程中就可以獲取到項(xiàng)目的根路徑,從而就能獲得Vue代碼文件的相對(duì)路徑。
let pathBefore = __dirname.substring(0, __dirname.search('node_modules'))
let filePath = filePath.substring(pathBefore.length) // vue代碼相對(duì)路徑
在server端執(zhí)行代碼定位命令時(shí),再將對(duì)應(yīng)的代碼相對(duì)路徑拼接成完整的絕對(duì)路徑。
2.4.2 外部引入組件
add-code-location雖然可以對(duì)本地的Vue文件進(jìn)行代碼路徑信息的添加,但是對(duì)于外部引入或解析加載的組件目前是沒(méi)有辦法進(jìn)行轉(zhuǎn)換的,例如element ui組件,實(shí)際上的代碼行信息只會(huì)添加在element ui組件的最外層。這時(shí)候client端在獲取點(diǎn)擊元素的代碼路徑時(shí)會(huì)做一個(gè)向上查找的處理,獲取其父節(jié)點(diǎn)的代碼路徑,如果還是沒(méi)有,會(huì)繼續(xù)查找父節(jié)點(diǎn)的父節(jié)點(diǎn),直到成功獲取代碼路徑。
function getFilePath(element) {
if (!element || !element.getAttribute) return null
if (element.getAttribute('code-location')) {
return element.getAttribute('code-location')
}
return getFilePath(element.parentNode)
}
這樣就可以在點(diǎn)擊后臺(tái)element ui搭建的頁(yè)面元素時(shí),也能成功定位打開(kāi)對(duì)應(yīng)代碼文件。
三、接入方案
通過(guò)前面的介紹,想必大家對(duì)頁(yè)面元素代碼映射插件原理有了清晰的了解,接下來(lái)就介紹一下在項(xiàng)目中的接入方式。接入方式其實(shí)很簡(jiǎn)單,并且可以選擇只在本地開(kāi)發(fā)環(huán)境接入,不用擔(dān)心對(duì)我們的生產(chǎn)環(huán)境造成影響,放心使用。
3.1 webpcak構(gòu)建項(xiàng)目
對(duì)于webpack構(gòu)建的項(xiàng)目來(lái)說(shuō),首先在構(gòu)建配置項(xiàng)vue.config.js文件中配置一下devServer和webpack loader,接著在main.js入口文件中初始化插件。
// vue.config.js
const openCodeServe = require('@vivo/vue-dev-code-link/server')
devServer: {
...
before: openCodeServe.before
},
if (!isProd) { // 本地開(kāi)發(fā)環(huán)境
config.module
.rule('vue')
.test(/\.vue/)
.use('@vivo/vue-dev-code-link/add-location-loader')
.loader('@vivo/vue-dev-code-link/add-location-loader')
.end()
}
// main.js
import openCodeClient from '@vivo/vue-dev-code-link/client'
if (process.env.NODE_ENV == 'development') {
openCodeClient.init()
}
3.2 Vite構(gòu)建項(xiàng)目
Vite構(gòu)建項(xiàng)目接入該插件的方案和webpack構(gòu)建項(xiàng)目基本上一致,唯一不一樣的地方在于打包配置文件里引入的是兩個(gè)Vite插件。
// vite.config.js
import openCodeServer from '@vivo/vue-dev-code-link/vite/server'
import addCodeLocation from '@vivo/vue-dev-code-link/vite/add-location'
export default defineConfig({
plugins: [
openCodeServer(),
addCodeLocation()
]
}
四、總結(jié)
以上就是對(duì)頁(yè)面元素代碼映射插件核心原理和接入方案的介紹,實(shí)現(xiàn)的方式充分利用了項(xiàng)目代碼打包構(gòu)建的流程,實(shí)際上無(wú)論是哪個(gè)打包工具,本質(zhì)上都是對(duì)源碼文件的轉(zhuǎn)換處理,當(dāng)我們理解了打包工具的運(yùn)行機(jī)制后,就可以做一些自己認(rèn)為有意義的事。就拿頁(yè)面元素代碼映射插件來(lái)說(shuō),使用它可以極大提升開(kāi)發(fā)效率,不再需要花費(fèi)時(shí)間在尋找代碼文件上,特別是頁(yè)面數(shù)和組件數(shù)比較多的項(xiàng)目,只需點(diǎn)擊頁(yè)面元素,即可一鍵打開(kāi)對(duì)應(yīng)代碼文件,精準(zhǔn)定位具體代碼行,無(wú)需查找,哪里不會(huì)點(diǎn)哪里,so easy!