Vite插件開發紀實:vite-plugin-monitor(下)

前言

上一篇介紹了Vite啟動,HMR等時間的獲取。

但各階段詳細的耗時信息,只能通過debug的日誌獲取

本文就實現一下debug日誌的攔截

插件效果預覽

圖片

–debug做了什麼

項目啟動指令

vite --debug

在源碼中搜索 --debug,可以在vite/packages/vite/bin/vite.js文件中定位到目標代碼

const debugIndex = process.argv.findIndex((arg) => /^(?:-d|--debug)$/.test(arg))

if (debugIndex > 0) {
  let value = process.argv[debugIndex + 1]
  if (!value || value.startsWith('-')) {
    value = 'vite:*'
  } else {
    // support debugging multiple flags with comma-separated list
    value = value
      .split(',')
      .map((v) => `vite:${v}`)
      .join(',')
  }
  process.env.DEBUG = value
}

可以看到如果使用了--debug或者-d參數,process.env上掛載DEBUG變量標識開啟了Debug

定位打印日誌方法

debug下每條日誌都是以vite:label開頭,比如

vite:load 1ms   [fs] /src/router/routes/index.ts

全局搜一下vite:load就定位到了如下的代碼,可以看到createDebugger是返回了一個可以打印日誌的方法

import {
  createDebugger,
} from '../utils'
const debugLoad = createDebugger('vite:load')
const isDebug = !!process.env.DEBUG
// ..code
isDebug && debugLoad(`${timeFrom(loadStart)} [fs] ${prettyUrl}`)

createDebugger 的源碼如下,其返回一個自定函數,簡單捋一下就能看出,負責打印的方法是log(msg,...args)

import debug from 'debug'

export function createDebugger(
  namespace: ViteDebugScope,
  options: DebuggerOptions = {}
): debug.Debugger['log'] {
  const log = debug(namespace)
  const { onlyWhenFocused } = options
  const focus =
    typeof onlyWhenFocused === 'string' ? onlyWhenFocused : namespace
  return (msg: string, ...args: any[]) => {
    if (filter && !msg.includes(filter)) {
      return
    }
    if (onlyWhenFocused && !DEBUG?.includes(focus)) {
      return
    }
    log(msg, ...args)
  }
}

其中log實例通過debug方法創建,但這個debug方法是一個第三方的庫visionmedia/debug

圖片

這個方庫雖小,能在Vite中被用上想必也不簡單,在線查看源碼

debug方法源碼分析

入口文件比較簡單,這裡直接去看./node.js中的邏輯

if (typeof process === 'undefined' || process.type === 'renderer' || process.browser === true || process.__nwjs) {
	module.exports = require('./browser.js');
} else {
	module.exports = require('./node.js');
}

這部分代碼一共只有264行,關鍵代碼如下

exports.log = log;

function log(...args) {
	return process.stderr.write(util.format(...args) + '\n');
}

module.exports = require('./common')(exports);

./common.js中部分代碼

function setup(env) {
	createDebug.debug = createDebug;
	createDebug.default = createDebug;

	function createDebug(namespace) {
		function debug(...args) {
			const self = debug;
			const logFn = self.log || createDebug.log;
			logFn.apply(self, args);
		}
		return debug;
	}
	return createDebug;
}

module.exports = setup;

到此能夠確定日誌的打印都是通過process.stderr.write方法輸出的內容

這個方法的好處就是,輸出內容不會直接換行

那麼我們在插件中重新定義一下這個方法就能攔截到打印的內容

debug日誌攔截實現

定義插件入參

interface PluginOptions {
    /**
     * 是否在終端中輸出原來的日誌
     */
    log?: boolean
    /**
     * 默認回調
     */
    monitor?: MonitorCallback
    /**
     * debug回調
     */
    debug?: DebugCallback
}

直接在調用插件方法的時候進行write方法重寫,具體實現邏輯如下

  • 啟用了--debug,傳入了monitordebug方法才重新定義write方法
  • 將獲取到的日誌信息做簡單解析,通過monitor方法傳遞給外部
  • 原始參數傳遞給外部的debug方法

其中解析出的幾個參數幾個參數與原日誌內容對應關係如下

圖片

import type { Plugin } from 'vite';
import type { PluginOptions } from './types';

export default function Monitor(ops: PluginOptions = {}): Plugin {
  const { log, monitor, debug } = ops;
  // 如果debug方法且啟動時添加了--debug參數
  if ((typeof debug === 'function' || typeof monitor === 'function') && process.env.DEBUG) {
    const { write } = process.stderr;
    Object.defineProperty(process.stderr, 'write', {
      get() {
        return function _write(...argv) {

          // log為true才執行原來的打印邏輯
          if (log && typeof argv[0] === 'string') {
            process.stdout.write(argv[0]);
          }
          const originStr = argv[0];

          // 解析日誌的label與打印的時間信息
          const tag = (originStr.match(/vite:(.*?)\s/) || [])[1];
          const time1 = (originStr.replace(/\+\d+ms/, '').match(/(\d+)ms/) || [])[1];
          const time2 = (originStr.match(/\+(\d+)ms/) || [])[1];
          const time = +(time1 || 0) + +(time2 || 0);


          if (tag && monitor) {
            monitor(tag, time, {
              time1: +(time1 || 0),
              time2: +(time2 || 0),
              originValue: originStr,
            });
          }

          if (debug) {
            debug(...argv);
          }
        };
      },
    });
  }
  return {
    name: 'vite-plugin-monitor',
    apply: 'serve',
    },
  };
}

到此攔截日誌的feature就完成了,最初定下目標也已完成

體驗插件

插件源碼

安裝依賴

yarn add vite-plugin-monitor --dev

引入插件,修改vite.config.js文件

import { defineConfig } from 'vite'
import vitePluginMonitor from 'vite-plugin-monitor'

export default defineConfig({
  plugins: [
    vitePluginMonitor({
      // log: false,
      monitor(label, time, originData) {
        const { time1, time2, originValue } = originVal
        console.log(originValue)
        console.log(label, time1, time2, `${time}ms`)
      },
      debug(str) {
        // 打印完整日誌
        // process.stdout.write(str)
      },
    }),
  ],
})

啟動指令中添加--debug

vite --debug

通過monitordebug方法中就能拿到原始的日誌和簡單處理後的日誌,在此處加入自定義的埋點監控代碼即可

一點補充:logfalse的時,並且定義了monitordebug方法,那麼原來的日誌內容都將會被這兩個方法攔截

小結

目前已經能夠完全攔截到debug下的所有內容,但內容由於有彩色打印相關的字符,提取信息比較麻煩

下一步將對日誌的提取再做一些格式化,確保能夠解析出完整的日誌內容