點選頁面元素,這個Vite外掛幫我開啟了Vue元件 ?

null仔發表於2022-03-21

前言

大家好,我是webfansplz.這兩天肝了個Vite外掛,本文主要跟大家分享一下它的功能和實現思路.如果你覺得它對你有幫助,請給一個star支援作者 ?.

介紹

vite-plugin-vue-inspector的功能是點選頁面元素,自動開啟本地IDE並跳轉到對應的Vue元件.類似於Vue DevToolsOpen component in editor功能.

vite-plugin-vue-inspector.gif

用法

vite-plugin-vue-inspector支援Vue2 & Vue3,並且只需要進行簡單的配置就可以使用.

Vue2

// vite.config.ts

import { 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.ts

import { 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-plugin-step.drawio (2).png

實現細節

接下來,我們來看具體的實現細節.在這之前,我們先簡單看下我們需要用到的幾個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.ts

function 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.ts

import 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 => AST
  const 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.ts

function 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.query
    if (!file) {
      res.statusCode = 500
      res.end("launch-editor-middleware: required query param \"file\" is missing.")
    }
    const lineNumber = +line || 1
    const 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.ts

function 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等原始碼排查解決.這裡給想看原始碼的童鞋一個建議,從實踐和帶著問題的角度出發,也許會有更好的效果和更深刻的印象 (教訓) :)

===,先別跑,點個star再走,感謝老鐵. ?

相關文章