黑科技::小程序自动埋点
答题思路:
- 通过标签 data-log-* 属性声明 埋点key&data
- 为标签添加 bind/catch/mut-bindtap="__logtap" 方法
- 重写 Page/Component 函数, 给所有 Page/Component 实例注入 __logtap 方法
- 处理原来的 tap 方法(如有), 这也是为什么不能在全局, 而是必须要在实例上注入方法的原因, 因为原始方法里面大概率会用到 this
- 写个 插件来处理 上述标签处理的转化
- 正则我不行, 借助 @vue/dom-compiler 将 wxml -> ast, 这样能够将标签的处理精确到 单独节点上
- 感谢vue
- 理论上讲, 将 logKey 转换成 class, 添加 wx.createIntersectionObserver 方法, 应该也能处理掉节点是否展示的埋点, 等弄完贴过来, 留个TODO 吧
- 理论上讲也适用于 vue
input -> output
input
<view
logtap
bindtap="原始的tap方法"
bind:tap="bind:tap也行"
catchtap="catchtap也行"
mut-bindtap="mut-bindtap也行"
data-log-key="logkey"
data-log-key="{{表达式也可以}}"
data-log-data="字符串, 或者"
data-log-data="{{ {表达式: '也可以' } }}"
...
>
你的内容
<view
logtap
data-log-key="一定要有 logtap 和 data-log-key 两个属性,不然会被忽略"
/>
支持嵌套
</view>
</view>
output
<view
data-tap-fn="原始的tap方法"
bindtap="__logtap"
data-log-key="logkey"
data-log-data="{{可以表达式取值}}"
>
你的内容
<view data-log-key="一定要有 logtap 和 data-log-key 两个属性,不然会被忽略" />
支持嵌套
</view>
其中, __logtap 方法, 通过 src/third-utils/adaptor 中注入, 详情看源码
const { logger } = require('./log/logger');
const autoAdaptorPage = () => {
const oPage = Page;
function XPage(config) {
function logTap(e) {
const { tapFn: tapFnName, logKey: logTapKey, logData } = e.currentTarget.dataset || {};
if (!logTapKey) return;
if (tapFnName && this[tapFnName]) {
this[tapFnName](e);
}
console.log('send tap', logTapKey, logData);
logger.send(logTapKey, logData, { event_type: 'click' });
}
config.__logtap = logTap;
return oPage(config);
}
Page = XPage;
};
const autoAdaptorComponent = () => {
const oComp = Component;
function XComp(config) {
function logTap(e) {
const { tapFn: tapFnName, logKey: logaTapKey, logData } = e.currentTarget.dataset || {};
if (tapFnName && this[tapFnName]) {
this[tapFnName](e);
}
console.log('send log', logTapKey, logData);
logger.send(logTapKey, logData, { event_type: 'click' });
}
config.methods = config.methods || {};
config.methods.__logtap = logTap;
return oComp(config);
}
Component = XComp;
};
export const autoAdaptor = at => {
console.log('auto adaptor...', at);
autoAdaptorPage();
autoAdaptorComponent();
};
autoAdaptor();
不要忘了在 app.js 中引入
webpack 配置
// webpack config loaders
...
{
test: /\.wxml$/,
use: [
fileLoader('[path][name].[ext]'),
path.resolve(__dirname, './wxml-autolog-loader.js'),
'mini-program-webpack-loader'
]
}
...
wxml-autolog-loader.js 源码
留神一下嵌套的处理
// wxml-autolog-laoder.js
const compiler = require('@vue/compiler-dom');
const visitor = (node, list = []) => {
const haslog =
node.props &&
node.props.find(x => x.name == 'logtap') &&
node.props.find(x => x.name === 'data-log-key');
if (haslog) {
const hastap = ['bind', 'bind:', 'catch', 'catch:', 'mut-bind', 'mut-bind:'].find(prefix =>
node.props.find(p => p.name === prefix + 'tap')
);
let replaceto = 'bindtap="__logtap"';
let oreplace = '';
if (hastap) {
const ofn = node.props.find(x => x.name == hastap + 'tap');
const fnName = ofn.value.content;
replaceto = `data-tap-fn="${fnName}" ${hastap}tap="__logtap"`;
oreplace = `${hastap}tap="${fnName}"`;
}
const source = node.loc.source;
let neo = source.replace('logtap="{{ true }}"', replaceto);
if (oreplace) {
neo = neo.replace(oreplace, '');
}
list.unshift({ neo, old: source });
}
if (node.children) {
node.children.forEach(item => {
visitor(item, list);
});
}
return list;
}; //loader函数
module.exports = function(content) {
const ast = compiler.parse(content);
// console.log('ast', ast);
const replaceList = visitor(ast);
let neo = content;
replaceList.forEach((item, idx) => {
neo = neo.replace(item.old, item.neo);
// 处理嵌套
replaceList.forEach((iitem, iidx) => {
if (iidx <= idx) return;
iitem.old.replace(item.old, item.neo);
iitem.neo.replace(item.old, item.neo);
});
});
if (replaceList.length > 0) {
// console.log('wxml log content', { content, neo });
console.log('ohhhh');
}
return neo;
};