如何在Vue專案中,通過點選DOM自動定位VScode中的程式碼行?

2022-06-14 12:01:16

作者:vivo 網際網路大前端團隊- Youchen

一、背景

現在大型的 Vue專案基本上都是多人共同作業開發,並且隨著版本的迭代,Vue 專案中的元件數也會越來越多,如果此時讓你負責不熟悉的頁面功能開發,甚至你才剛剛加入這個專案,那麼怎麼樣才能快速找到相關元件在整個專案程式碼中的檔案位置呢?想必大家都有采取過以下這幾種方法:

  • 【搜類名】,在工程檔案裡搜尋頁面 DOM元素中的樣式類名
  • 【找路由】,根據頁面連結找到Vue路由匹配的頁面元件
  • 【找人】,找到當初負責開發該頁面的人詢問對應的程式碼路徑

以上幾種方法確實能夠幫助我們找到具體的程式碼檔案路徑,但都需要人工去搜尋,並不是很高效,那有沒有其它更高效的方式呢?

答案是有的。Vue官方就提供了一款 vue-devtools  外掛,使用該外掛就能自動在 VSCode 中開啟對應頁面元件的原始碼檔案,操作路徑如下:

使用vue-devtools外掛可以很好地提高我們查詢對應頁面元件程式碼的效率,但只能定位到對應的元件程式碼,如果我們想要直接找到頁面上某個元素相關的具體程式碼位置,還需要在當前元件原始碼中進行二次查詢,並且每次都要先選擇元件,再點選開啟按鈕才能開啟程式碼檔案,不是特別快捷。

針對這個問題,我們開發了輕量級的頁面元素程式碼對映外掛,使用該外掛可以通過點選頁面元素的方式,一鍵開啟對應程式碼原始檔,並且精準定位對應程式碼行,無需手動查詢,能夠極大地提高開發效率和體驗,實際的使用效果如下:

二、實現原理

整個外掛主要分為3個功能模組:client、server、add-code-location,client端傳送特定請求給server端,server端接收到該請求後執行定位程式碼行命令,而add-code-location模組用於原始碼的轉換。

2.1 client

client端這裡其實就是指瀏覽器,我們在點選頁面元素時,瀏覽器就會傳送一個特定請求給server端,該請求資訊包含了具體的程式碼檔案路徑和對應程式碼行號資訊。

function openEditor(filePath) {
  axios
    .get(`${protocol}//${host}:${port}/code`, {
      params: {
        filePath: `${filePath}`
      }
    })
    .catch(error => {
      console.log(error)
    })
}

而監聽頁面元素的點選事件則通過事件代理的方式全域性監聽,給document繫結了點選事件,監聽鍵盤和滑鼠點選組合事件來發起定位程式碼行請求,避免和頁面原生的click事件發生衝突。

function openCode(e) {
  if (isShiftKey || isMetaKey || e.metaKey || e.shiftKey) {
    e.preventDefault()
    const filePath = getFilePath(e.target)
    openEditor(filePath)
  }
  ...
}

2.2 server

server端是指本地起的一個伺服器,可以監聽client端傳送的特定請求,當接收到執行定位命令的請求時,執行VSCode開啟程式碼檔案命令,並定位到對應的程式碼行。

2.2.1 webpack devServer

如果是採用webpack構建的專案,webpack的devServer開發伺服器已經提供了一個before屬性,可以通過它來監聽傳送給開發伺服器的請求。

before: function (app) {
  app.get('/code', function (req, res) {
    if (req.query.filePath) {
      // 執行vscode定位程式碼行命令
      openCodeFile(req.query.filePath)
      ...
    }
    ...
  })
}

2.2.2 vite configureServer

如果是採用Vite構建的專案,可以使用Vite外掛來實現server端監聽特定請求,Vite外掛擴充套件於rollup外掛介面,並且在原有的基礎上增加了一些特有的勾點函數,例如configureServer勾點,通過該勾點函數可以用於設定開發伺服器來監聽特定的請求。

const codeServer = () => ({
  name: 'open-code-vite-server',
  configureServer(server) {
    server.middlewares.use((req, res, next) => {
      ...
      if (pathname == '/code') {
        ...
        if (filePath) {
          openCodeFile(filePath) // 執行vscode定位程式碼行命令
          ...
        }
        res.end()
      }
      ...
    })
  }
})

2.2.3 執行 VSCode 定位命令

當server端監聽到client端傳送的特定請求後,接下來就是執行VSCode定位程式碼行命令。實際上,VSCode編輯器是可以通過code命令來啟動,並且可以相應使用一些命令列引數,例如:

"code --reuse-window"或"code -r"命令可以開啟最後活動視窗的檔案或資料夾;"code --goto"或"code -g"命令後面可以拼接具體檔案路徑和行列號,當使用"code -g file:line:column"命令時可以開啟某個檔案並定位到具體的行列位置。

利用 VSCode 編輯器的這個特性,我們就能實現自動定位程式碼行功能,對應的程式碼路徑資訊可以從client端傳送的請求資訊當中獲得,再借助node的child_process.exec方法來執行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命令到環境變數當中。Mac系統使用者可以在VSCode介面使用command+shift+p快捷鍵,然後搜尋Code 並選擇install 'code' command in path;Windows使用者可以找到VSCode安裝位置的bin資料夾目錄,並將該目錄新增到系統環境變數當中。

2.3 add-code-location

通過前面的介紹,大家應該瞭解了client端和server端的執行機制,並且在執行定位命令時需要獲取到頁面元素的程式碼路徑,而具體的程式碼路徑是以屬性的方式繫結到了DOM元素上,這時候就需要用到add-code-location模組在編譯時轉換我們的原始碼,並給 DOM元素新增對應的程式碼路徑屬性。

整個原始碼轉換處理流程如下:

2.3.1 獲取檔案路徑

原始碼轉換過程的第一步是獲取程式碼檔案的具體路徑,對於webpack打包的專案來說,webpack loader用來處理原始碼字串再合適不過,loader的上下文this物件包含一個resourcePath資原始檔的路徑屬性,利用這個屬性我們很容易就能獲得每個程式碼檔案的具體路徑。

module.exports = function (source) {
  const { resourcePath } = this
  return sourceCodeChange(source, resourcePath)
}

對於Vite構建的專案來說,原始碼的轉化操作也是通過外掛來完成,Vite外掛有通用的勾點transform,可用於轉換已載入的模組內容,它接收兩個引數,code引數代表著原始碼字串,id引數是檔案的全路徑。

module.exports = function() {
  return {
    name: 'add-code-location',
    transform(code, id) {
      ...
      return sourceCodeChange(code, id)
    }
  }
}

2.3.2 計算程式碼行號

接著在遍歷原始碼檔案的過程中,需要處理對應Vue檔案template模板中的程式碼,以「\n」分割template模板部分字串為陣列,通過陣列的索引即可精準得到每一行html標籤的程式碼行號。

function codeLineTrack(str, resourcePath) {
  let lineList =  str.split('\n')
  let newList = []
  lineList.forEach((item, index) => {
    newList.push(addLineAttr(item, index + 1, resourcePath)) // 新增位置屬性,index+1為具體的程式碼行號
  })
  return newList.join('\n')
}

2.3.3 新增位置屬性

在獲取到程式碼檔案路徑和程式碼行號以後,接下來就是對Vue template模板中分割的每一行標籤元素新增最終的位置屬性。這裡採用的是正則替換的方式來新增位置屬性,分別對每一行標籤元素先正則匹配出所有元素的開始標籤部分,例如<div、<span、<img等,然後將其正則替換成帶有code-location屬性的開始標籤,對應的屬性值就是前面獲取的程式碼路徑和對應標籤的行號。

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 原始碼相對路徑

在給DOM元素新增對應的原始碼位置屬性時,實際上採用的是相對路徑,這樣可以使得DOM元素上的屬性值更加簡潔明瞭。node_modules資料夾通常是在專案的根目錄下,而外掛是以npm包的形式安裝在node_modules路徑下,利用node的__dirname變數可以獲得當前模組的絕對路徑,因此在原始碼轉換過程中就可以獲取到專案的根路徑,從而就能獲得Vue程式碼檔案的相對路徑。

let pathBefore = __dirname.substring(0, __dirname.search('node_modules'))
let filePath = filePath.substring(pathBefore.length) // vue程式碼相對路徑

在server端執行程式碼定位命令時,再將對應的程式碼相對路徑拼接成完整的絕對路徑。

2.4.2 外部引入元件

add-code-location雖然可以對原生的Vue檔案進行程式碼路徑資訊的新增,但是對於外部引入或解析載入的元件目前是沒有辦法進行轉換的,例如element ui元件,實際上的程式碼行資訊只會新增在element ui元件的最外層。這時候client端在獲取點選元素的程式碼路徑時會做一個向上查詢的處理,獲取其父節點的程式碼路徑,如果還是沒有,會繼續查詢父節點的父節點,直到成功獲取程式碼路徑。

function getFilePath(element) {
  if (!element || !element.getAttribute) return null
  if (element.getAttribute('code-location')) {
    return element.getAttribute('code-location')
  }
  return getFilePath(element.parentNode)
}

這樣就可以在點選後臺element ui搭建的頁面元素時,也能成功定位開啟對應程式碼檔案。

三、接入方案

通過前面的介紹,想必大家對頁面元素程式碼對映外掛原理有了清晰的瞭解,接下來就介紹一下在專案中的接入方式。接入方式其實很簡單,並且可以選擇只在本地開發環境接入,不用擔心對我們的生產環境造成影響,放心使用。

3.1 webpcak構建專案

對於webpack構建的專案來說,首先在構建設定項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) { // 本地開發環境
  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構建專案

Vite構建專案接入該外掛的方案和webpack構建專案基本上一致,唯一不一樣的地方在於打包組態檔裡引入的是兩個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()
  ]
}

四、總結

以上就是對頁面元素程式碼對映外掛核心原理和接入方案的介紹,實現的方式充分利用了專案程式碼打包構建的流程,實際上無論是哪個打包工具,本質上都是對原始碼檔案的轉換處理,當我們理解了打包工具的執行機制後,就可以做一些自己認為有意義的事。就拿頁面元素程式碼對映外掛來說,使用它可以極大提升開發效率,不再需要花費時間在尋找程式碼檔案上,特別是頁面數和元件數比較多的專案,只需點選頁面元素,即可一鍵開啟對應程式碼檔案,精準定位具體程式碼行,無需查詢,哪裡不會點哪裡,so easy!