
前端(vue)入門到精通課程,老師在線輔導(dǎo):聯(lián)系老師
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API調(diào)試工具:點擊使用
埋點一直是 H5 項目中的重要一環(huán),埋點數(shù)據(jù)更是后期改善業(yè)務(wù)和技術(shù)優(yōu)化的重要基礎(chǔ)。【推薦學(xué)習(xí):web前端、編程教學(xué)】
在日常的工作中,經(jīng)常會有產(chǎn)品或者業(yè)務(wù)的同學(xué)來問,“這個項目現(xiàn)在有哪些埋點?”,“這個埋點用在哪些地方?”像這樣的問題基本上都是問一次查一次代碼,效率很低。

這也許跟埋點本身的性質(zhì)有關(guān)系。埋點屬于相對獨立的功能,隨著迭代的進行,開發(fā)者很難記住埋點的用途。開發(fā)者出于自測驗證的需要,也得對項目中的埋點數(shù)據(jù)加以整理。因此結(jié)合當(dāng)前的場景,可以實現(xiàn)一個工具:通過對代碼進行掃描,分析埋點相關(guān)的代碼,并對之加以處理,轉(zhuǎn)化成特定的數(shù)據(jù),供后續(xù)在其他的管理平臺中使用。
實現(xiàn)思路
這個工具大致可以分成三個部分,JSDoc 提取埋點、路由依賴分析和 ESLint 插件。
- JSDoc 是根據(jù) JavaScript 中的注釋信息,生成 API 文檔的一個工具。結(jié)合 JSDoc 的這一個特性,這個埋點工具把 JSDoc 作為核心部分,用于輸出代碼中的埋點數(shù)據(jù)。
- Webpack 插件作為輔助,為 JSDoc 提供路由信息。
- ESLint 插件則作為最后的檢驗,確保文件中的埋點代碼都有對應(yīng)的 JSDoc 注釋。

自定義 JSDoc 標(biāo)記埋點
我們知道,JSDoc 可以根據(jù)代碼中的注釋輸出一份文檔。首先我們自定義一個 JSDoc 的 tag 來標(biāo)注這是一個埋點的注釋,這樣后續(xù)處理時可以過濾掉其他注釋的干擾。結(jié)合具體項目中使用的代碼可以畫出這樣一個流程圖:

下面是具體的代碼實現(xiàn)的過程。
編寫 JSDoc 插件,自定義一個 tag:
// jsdoc.plugin.js // 自定義一個 @log,含有 @log 才是埋點的注釋 exports.defineTags = function (dictionary) { dictionary.defineTag('log', { canHaveName: true, onTagged: function (doclet, tag) { doclet.meta.log = tag.text; }, }); };
解析 .ts 和 .vue 文件。
// jsdoc.plugin.js exports.handlers = { beforeParse: function (e) { // 對文件預(yù)處理 if (/.vue/.test(e.filename)) { // 解析 vue 文件 const component = compiler.parseComponent(e.source); // 獲取 vue 文件的 script 代碼 const ast = parse.parse(component.script.content, { // ... }); } if (/.ts/.test(e.filename)) { // ts 轉(zhuǎn) js } }, };
自定義 JSDoc 模版。
// publish.js exports.publish = function (taffyData, opts, tutorials) { // ... data().each(function (doclet) { // 有 log 這個 tag 的才是埋點注釋 if (doclet.meta && doclet.meta.log) { doclet.tags?.forEach((item) => { // 獲取對應(yīng)的路由地址 }); // 拿到埋點數(shù)據(jù) logData.push({}); } }); // 輸出 md 文檔 fs.writeFileSync(outpath, mdContent, 'utf8'); };
到這里,已經(jīng)可以完整地輸出代碼中的所有埋點了。此時再來看下目前這個工具的能力:
- 自動提取埋點信息,生成埋點文檔:✅
- 自動給埋點注釋添加自定義 tag(@log):❌
- 自動給埋點注釋添加上報的埋點信息:❌
- 自動給埋點注釋添加路由信息:❌
- 自動給埋點注釋添加埋點描述信息:❌
- 自動提示沒有注釋的埋點代碼:❌
通過上面的梳理我們可以看出:
- 需要手動給每個埋點加上注釋
- 需要手動去查每個埋點所對應(yīng)的路由
- 如果忘了給埋點加注釋怎么辦?
做這個工具的初衷,就是為省去一些重復(fù)繁瑣的工作,如果為了能自動從代碼中輸入一份文檔而增加了其他一些工作量,這未免有點得不償失。通過對這些問題的分析,可以得出以下的解決方案:
- 需要手動給每個埋點加上注釋 -> 自動填充代碼 -> ESLint fix 功能 / VSCode 插件
- 需要手動去查每個埋點所對應(yīng)的路由 -> 自動找到組件所對應(yīng)的路由 -> Webpack 依賴分析
- 如果忘了給埋點加注釋怎么辦?-> 忘寫注釋有提示 -> ESLint 插件
到這一步解決問題的方法就已經(jīng)變得明朗了。接下來讓看一下 webpack 插件與 ESLint 插件的實現(xiàn)過程。
路由依賴分析
webpack 本身自帶依賴分析,輕松就能拿到組件間的父子關(guān)系。
compiler.hooks.normalModuleFactory.tap('routeAnalysePlugin', (nmf) => { nmf.hooks.afterResolve.tapAsync('routeAnalysePlugin', (result, callback) => { const { resourceResolveData } = result; // 子組件 const path = resourceResolveData.path; // 父組件 const fatherPath = resourceResolveData.context.issuer; // 只獲取 vue 文件的依賴關(guān)系 if (/.vue/.test(path) && /.vue/.test(fatherPath)) { // 將組件間的父子關(guān)系存到變量中 } }); });
把組件之間的依賴關(guān)系拼成我們想要的數(shù)據(jù)格式
[ { "path": "src/views/register-v2/index.vue", "deps": [ { "path": "src/components/landing-banner/index.vue", "deps": [] } ] } // ... ]
組件之間的依賴關(guān)系有了,接下來就是找到組件和路由的對應(yīng)關(guān)系,這里我們用 AST 來解析路由文件,獲取路由和組件的對應(yīng)關(guān)系。
// 遍歷路由文件 for (let i = 0; i < this.routePaths.length; i++) { // ... traverse(ast, { enter(path) { // 找出組件和路由的對應(yīng)關(guān)系 path.node.properties.forEach((item) => { // 組件 if (item.key.name === 'component') { } // 路由地址 if (item.key.name === 'path') { } }); }, }); }
同樣地,把組件與路由的映射關(guān)系拼成合適的數(shù)據(jù)格式。
{ "src/views/register-v3/index.vue": "/register" // ... }
再將路由的映射關(guān)系和組件間的依賴關(guān)系整合到一起,得出每個組件與路由的對應(yīng)關(guān)系。
{ "src/components/landing-banner/index.vue": [ "/register_v2", "/register" //... ] // ... }
因為使用 AST 遍歷的方式來解析路由文件,目前支持的解析的路由文件寫法有以下四種,基本上滿足了當(dāng)前的場景:
const page1 = (resolve) => { require.ensure( [], () => { resolve(require('page1.vue')); }, 'page1', ); }; const page2 = () => import( /* webpackChunkName: "page2" */ 'page2.vue' ); export default [ { path: '/page1', component: page1 }, { path: '/page2', component: page2 }, { path: '/page3', component: (resolve) => { require.ensure( [], () => { resolve(require('page3.vue')); }, 'page3', ); }, }, { path: '/page4', component: () => import( /* webpackChunkName: "page4" */ 'page4.vue' ), }, ];
再得到了上面的對應(yīng)關(guān)系之后,可以把埋點數(shù)據(jù)放到傳到埋點管理平臺上,從而實現(xiàn)一鍵查詢:

編寫 ESLint 插件
先來看看代碼中埋點上報的三種方式:
// 神策 sdk sensors.track('xxx', {}); // 掛載到 Vue 實例中 this.$sa.track('xxx', {}); // 裝飾器 @SensorTrack('xxx', {})
觀察上面三種方式,可以知道埋點上報是通過 track 函數(shù)和 SensorTrack 函數(shù),所以我們的 ESLint 插件對這兩個函數(shù)進行校驗。
function create(context) { // 調(diào)用 track 函數(shù)的對象 const checkList = ['sensor', 'sensors', '$sa', 'sa']; return { Literal: function (node) { // ... // 調(diào)用埋點函數(shù)而缺少注釋時 if ( isNoComment && ((isTrack && isSensor) || (is$Track && isThisExpression)) ) { context.report({ node, messageId: 'missingComment', fix: function (fixer) { // 自動修復(fù) }, }); } // 使用修飾器但沒有注釋時 if ( callee.name === 'SensorTrack' && sourceCode.getCommentsBefore(node).length === 0 ) { context.report({ node, messageId: 'missingComment', fix: function (fixer) { // 自動修復(fù) }, }); } }, }; }
看下完成后的效果:

效果對比
我們再來對比下優(yōu)化前后的區(qū)別:
| 優(yōu)化前 | 優(yōu)化后 | |
|---|---|---|
| 自動提取埋點信息,生成埋點文檔 | ✅ | ✅ |
| 自動給埋點注釋添加自定義 tag(@log) | ❌ | ✅ |
| 自動給埋點注釋添加上報的埋點信息 | ❌ | ✅ |
| 自動給埋點注釋添加路由信息 | ❌ | ✅ |
| 自動給埋點注釋添加埋點描述信息 | ❌ | ❌ |
| 自動提示沒有注釋的埋點代碼 | ❌ | ✅ |
優(yōu)化之后除了整個流程基本都由工具自動完成,剩下一個埋點描述信息。因為埋點的描述信息只是為了讓我們更好地理解這個埋點,本身并不在上報的代碼中,所以工具沒有辦法自動生成,但是我們可以直接在產(chǎn)品提供的埋點文檔中拷貝過來完成這一步。
總結(jié)
在項目中接入這個工具之后,可以快速地知道項目的埋點有哪些以及各個埋點所在的頁面,也方便我們對埋點的梳理,同時利用導(dǎo)出的埋點數(shù)據(jù)開發(fā)后臺應(yīng)用,有效地提升了開發(fā)者效率。
這個工具的實現(xiàn)是在 JSDoc、webpack 和 ESLint 插件的加持下水到渠成的,說是水到渠成是因為一開始的想法只是做到第一步,先有個一鍵查詢功能和能夠輸出一份文檔用著先。但是第一版出來后發(fā)現(xiàn)要手動去處理這些埋點注釋還是比較繁瑣,恰巧平常開發(fā)中常見的 webpack 插件和 ESLint 插件可以很好地解決這些問題,于是便有路由依賴分析和 ESLint 插件。像是《牧羊少年奇幻之旅》中所說的,“如果你下定決心要做一件事情,整個宇宙都會合力幫助你。”
【推薦學(xué)習(xí):web前端開發(fā)、編程基礎(chǔ)視頻教程】
站長資訊網(wǎng)