前端(vue)入門到精通課程,老師在線輔導:聯系老師
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API調試工具:點擊使用
埋點一直是 H5 項目中的重要一環,埋點數據更是后期改善業務和技術優化的重要基礎。【推薦學習:web前端、編程教學】
在日常的工作中,經常會有產品或者業務的同學來問,“這個項目現在有哪些埋點?”,“這個埋點用在哪些地方?”像這樣的問題基本上都是問一次查一次代碼,效率很低。
這也許跟埋點本身的性質有關系。埋點屬于相對獨立的功能,隨著迭代的進行,開發者很難記住埋點的用途。開發者出于自測驗證的需要,也得對項目中的埋點數據加以整理。因此結合當前的場景,可以實現一個工具:通過對代碼進行掃描,分析埋點相關的代碼,并對之加以處理,轉化成特定的數據,供后續在其他的管理平臺中使用。
實現思路
這個工具大致可以分成三個部分,JSDoc 提取埋點、路由依賴分析和 ESLint 插件。
- JSDoc 是根據 JavaScript 中的注釋信息,生成 API 文檔的一個工具。結合 JSDoc 的這一個特性,這個埋點工具把 JSDoc 作為核心部分,用于輸出代碼中的埋點數據。
- Webpack 插件作為輔助,為 JSDoc 提供路由信息。
- ESLint 插件則作為最后的檢驗,確保文件中的埋點代碼都有對應的 JSDoc 注釋。
自定義 JSDoc 標記埋點
我們知道,JSDoc 可以根據代碼中的注釋輸出一份文檔。首先我們自定義一個 JSDoc 的 tag 來標注這是一個埋點的注釋,這樣后續處理時可以過濾掉其他注釋的干擾。結合具體項目中使用的代碼可以畫出這樣一個流程圖:
下面是具體的代碼實現的過程。
編寫 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) { // 對文件預處理 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 轉 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) => { // 獲取對應的路由地址 }); // 拿到埋點數據 logData.push({}); } }); // 輸出 md 文檔 fs.writeFileSync(outpath, mdContent, 'utf8'); };
到這里,已經可以完整地輸出代碼中的所有埋點了。此時再來看下目前這個工具的能力:
- 自動提取埋點信息,生成埋點文檔:✅
- 自動給埋點注釋添加自定義 tag(@log):❌
- 自動給埋點注釋添加上報的埋點信息:❌
- 自動給埋點注釋添加路由信息:❌
- 自動給埋點注釋添加埋點描述信息:❌
- 自動提示沒有注釋的埋點代碼:❌
通過上面的梳理我們可以看出:
- 需要手動給每個埋點加上注釋
- 需要手動去查每個埋點所對應的路由
- 如果忘了給埋點加注釋怎么辦?
做這個工具的初衷,就是為省去一些重復繁瑣的工作,如果為了能自動從代碼中輸入一份文檔而增加了其他一些工作量,這未免有點得不償失。通過對這些問題的分析,可以得出以下的解決方案:
- 需要手動給每個埋點加上注釋 -> 自動填充代碼 -> ESLint fix 功能 / VSCode 插件
- 需要手動去查每個埋點所對應的路由 -> 自動找到組件所對應的路由 -> Webpack 依賴分析
- 如果忘了給埋點加注釋怎么辦?-> 忘寫注釋有提示 -> ESLint 插件
到這一步解決問題的方法就已經變得明朗了。接下來讓看一下 webpack 插件與 ESLint 插件的實現過程。
路由依賴分析
webpack 本身自帶依賴分析,輕松就能拿到組件間的父子關系。
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 文件的依賴關系 if (/.vue/.test(path) && /.vue/.test(fatherPath)) { // 將組件間的父子關系存到變量中 } }); });
把組件之間的依賴關系拼成我們想要的數據格式
[ { "path": "src/views/register-v2/index.vue", "deps": [ { "path": "src/components/landing-banner/index.vue", "deps": [] } ] } // ... ]
組件之間的依賴關系有了,接下來就是找到組件和路由的對應關系,這里我們用 AST 來解析路由文件,獲取路由和組件的對應關系。
// 遍歷路由文件 for (let i = 0; i < this.routePaths.length; i++) { // ... traverse(ast, { enter(path) { // 找出組件和路由的對應關系 path.node.properties.forEach((item) => { // 組件 if (item.key.name === 'component') { } // 路由地址 if (item.key.name === 'path') { } }); }, }); }
同樣地,把組件與路由的映射關系拼成合適的數據格式。
{ "src/views/register-v3/index.vue": "/register" // ... }
再將路由的映射關系和組件間的依賴關系整合到一起,得出每個組件與路由的對應關系。
{ "src/components/landing-banner/index.vue": [ "/register_v2", "/register" //... ] // ... }
因為使用 AST 遍歷的方式來解析路由文件,目前支持的解析的路由文件寫法有以下四種,基本上滿足了當前的場景:
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' ), }, ];
再得到了上面的對應關系之后,可以把埋點數據放到傳到埋點管理平臺上,從而實現一鍵查詢:
編寫 ESLint 插件
先來看看代碼中埋點上報的三種方式:
// 神策 sdk sensors.track('xxx', {}); // 掛載到 Vue 實例中 this.$sa.track('xxx', {}); // 裝飾器 @SensorTrack('xxx', {})
觀察上面三種方式,可以知道埋點上報是通過 track 函數和 SensorTrack 函數,所以我們的 ESLint 插件對這兩個函數進行校驗。
function create(context) { // 調用 track 函數的對象 const checkList = ['sensor', 'sensors', '$sa', 'sa']; return { Literal: function (node) { // ... // 調用埋點函數而缺少注釋時 if ( isNoComment && ((isTrack && isSensor) || (is$Track && isThisExpression)) ) { context.report({ node, messageId: 'missingComment', fix: function (fixer) { // 自動修復 }, }); } // 使用修飾器但沒有注釋時 if ( callee.name === 'SensorTrack' && sourceCode.getCommentsBefore(node).length === 0 ) { context.report({ node, messageId: 'missingComment', fix: function (fixer) { // 自動修復 }, }); } }, }; }
看下完成后的效果:
效果對比
我們再來對比下優化前后的區別:
優化前 | 優化后 | |
---|---|---|
自動提取埋點信息,生成埋點文檔 | ✅ | ✅ |
自動給埋點注釋添加自定義 tag(@log) | ❌ | ✅ |
自動給埋點注釋添加上報的埋點信息 | ❌ | ✅ |
自動給埋點注釋添加路由信息 | ❌ | ✅ |
自動給埋點注釋添加埋點描述信息 | ❌ | ❌ |
自動提示沒有注釋的埋點代碼 | ❌ | ✅ |
優化之后除了整個流程基本都由工具自動完成,剩下一個埋點描述信息。因為埋點的描述信息只是為了讓我們更好地理解這個埋點,本身并不在上報的代碼中,所以工具沒有辦法自動生成,但是我們可以直接在產品提供的埋點文檔中拷貝過來完成這一步。
總結
在項目中接入這個工具之后,可以快速地知道項目的埋點有哪些以及各個埋點所在的頁面,也方便我們對埋點的梳理,同時利用導出的埋點數據開發后臺應用,有效地提升了開發者效率。
這個工具的實現是在 JSDoc、webpack 和 ESLint 插件的加持下水到渠成的,說是水到渠成是因為一開始的想法只是做到第一步,先有個一鍵查詢功能和能夠輸出一份文檔用著先。但是第一版出來后發現要手動去處理這些埋點注釋還是比較繁瑣,恰巧平常開發中常見的 webpack 插件和 ESLint 插件可以很好地解決這些問題,于是便有路由依賴分析和 ESLint 插件。像是《牧羊少年奇幻之旅》中所說的,“如果你下定決心要做一件事情,整個宇宙都會合力幫助你。”
【推薦學習:web前端開發、編程基礎視頻教程】