├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── __tests__ └── wx.js ├── dist └── xbosstrack.min.js ├── package.json ├── rollup.config.js ├── sonar-project.properties └── src ├── helper.js ├── index.js ├── report.js └── wrapper.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": ["latest"] 5 | }, 6 | "build": { 7 | "presets": [ 8 | [ 9 | "latest", 10 | { 11 | "es2015": { 12 | "modules": false 13 | } 14 | } 15 | ] 16 | ], 17 | "plugins": ["external-helpers"] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["airbnb-base"], 3 | globals: { 4 | Page:true, 5 | App:true, 6 | wx: true, 7 | getApp: true, 8 | getCurrentPages: true, 9 | Component: true, 10 | describe: true, 11 | it: true, 12 | expect: true, 13 | jest: true, 14 | beforeEach: true, 15 | beforeAll: true, 16 | afterEach: true, 17 | afterAll: true 18 | }, 19 | rules: { 20 | "indent": [0, 2], // 2个空格作为代码缩进 21 | "no-underscore-dangle": 0, // 允许使用下划线定义变量或方法,例如 _name 22 | "class-methods-use-this": 0, // 允许如果方法内没有用到this,不需强制定义成静态方法。 23 | "no-unused-expressions": [0, { "allowShortCircuit": true }], // 允许使用表达式 24 | "no-new": 0, // 这个规则旨在通过禁止使用new不将结果对象分配给变量的关键字的构造函数调用来维护一致性和约定。 25 | "object-property-newline": 0, // 允许对象属性不换行 26 | "object-curly-newline": 0, 27 | "no-plusplus": [0, { "allowForLoopAfterthoughts": true }], 28 | "no-param-reassign": [0, { "props": true }], 29 | "no-restricted-syntax": 0, 30 | "no-prototype-builtins":0, 31 | "func-names":0, 32 | "prefer-rest-params":0, 33 | "arrow-parens": 0, 34 | "comma-dangle":0 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | package-lock.json 4 | yarn.lock 5 | coverage -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | addons: 2 | sonarcloud: 3 | organization: "zhengguorong-github" # the key of the org you chose at step #3 4 | token: 5 | secure: 9785888673923c2fa351fb7e92da6d406ed2c962 # encrypted value of your token 6 | language: node_js 7 | node_js: 8 | - '8' 9 | install: 10 | - npm i npminstall && npminstall 11 | script: 12 | - npm run lint 13 | - npm run test 14 | - npm run dev 15 | - npm run build 16 | - npm run codecov 17 | - sonar-scanner 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2 | 3 | 996 License Version 1.0 (Draft) 4 | 5 | Permission is hereby granted to any individual or legal entity 6 | obtaining a copy of this licensed work (including the source code, 7 | documentation and/or related items, hereinafter collectively referred 8 | to as the "licensed work"), free of charge, to deal with the licensed 9 | work for any purpose, including without limitation, the rights to use, 10 | reproduce, prepare derivative works of, distribute and sublicense the 11 | licensed work, subject to the following conditions: 12 | 13 | 1. The individual or the legal entity must conspicuously display, 14 | without modification, this License on each redistributed or derivative 15 | copy of the Licensed Work. 16 | 17 | 2. The individual or the legal entity must strictly comply with all 18 | applicable laws, regulations, rules and standards of the jurisdiction 19 | relating to labor and employment where the individual is physically 20 | located or where the individual was born or naturalized; or where the 21 | legal entity is registered or is operating (whichever is stricter). In 22 | case that the jurisdiction has no such laws, regulations, rules and 23 | standards or its laws, regulations, rules and standards are 24 | unenforceable, the individual or the legal entity are required to 25 | comply with Core International Labor Standards. 26 | 27 | 3. The individual or the legal entity shall not induce or force its 28 | employee(s), whether full-time or part-time, or its independent 29 | contractor(s), in any methods, to agree in oral or written form, to 30 | directly or indirectly restrict, weaken or relinquish his or her 31 | rights or remedies under such laws, regulations, rules and standards 32 | relating to labor and employment as mentioned above, no matter whether 33 | such written or oral agreement are enforceable under the laws of the 34 | said jurisdiction, nor shall such individual or the legal entity 35 | limit, in any methods, the rights of its employee(s) or independent 36 | contractor(s) from reporting or complaining to the copyright holder or 37 | relevant authorities monitoring the compliance of the license about 38 | its violation(s) of the said license. 39 | 40 | THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 41 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 42 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 43 | IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, 44 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 45 | OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION WITH THE 46 | LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK. 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xbosstrack 小程序自动埋点 2 | ### 使用方法 3 | 4 | 1、App.js文件引入资源 5 | 6 | ``` 7 | // 引入埋点SDK 8 | import Tracker from './xbosstrack.min.js'; 9 | // 引入埋点配置信息,请自行参考tracks目录下埋点配置修改 10 | import trackConfig from './tracks/index'; 11 | ``` 12 | 13 | 2、初始化 14 | 15 | ``` 16 | new Tracker({ tracks: trackConfig }); 17 | ``` 18 | 19 | 3、加入你的埋点信息 20 | 21 | ``` 22 | /** 23 | * path 页面路径 24 | * elementTracks 页面元素埋点 25 | * methodTracks 执行函数埋点 26 | * comMethodTracks: 执行组件内函数埋点 27 | */ 28 | const tracks = { 29 | path: 'pages/film/index', 30 | elementTracks: [ 31 | { 32 | element: '.playing-item', 33 | dataKeys: ['imgUrls', 'playingFilms[$INDEX].filmId', 'playingFilms[0]'], 34 | }, 35 | { 36 | element: '.more', 37 | dataKeys: ['imgUrls', 'playingFilms', '$DATASET.test'], 38 | } 39 | ], 40 | methodTracks: [ 41 | { 42 | method: 'getBanner', 43 | dataKeys: ['imgUrls'], 44 | }, 45 | { 46 | method: 'toBannerDetail', 47 | dataKeys: ['imgUrls'], 48 | }, 49 | ], 50 | comMethodTracks: [ 51 | { 52 | method: '_test1', 53 | dataKeys: ['name', '$DATASET.test'], 54 | }, 55 | ], 56 | }; 57 | ``` 58 | 59 | 4、在wxml最外层插入监听方法 60 | 61 | ``` 62 | 63 | 64 | 65 | ``` 66 | 67 | 打开控制台,查看是否成功收集 68 | 69 | ![image](https://user-images.githubusercontent.com/2757932/51715472-d518a200-2073-11e9-874f-9cd1894a779c.png) 70 | 71 | element: 触发埋点元素class 72 | 73 | method:触发埋点函数 74 | 75 | name:收集数据的key值 76 | 77 | data:数据对应值 78 | 79 | 80 | 81 | 5、如果你要监听组件内元素 82 | 83 | 在elementTracks里加入 84 | 85 | ```javascript 86 | { 87 | element: '.page >>> .sub-component', 88 | dataKeys: ['name', '$DATASET.test'] 89 | } 90 | ``` 91 | 92 | .page表示包裹组件的元素class,或者你可以使用id或者任意选择器 93 | 94 | .sub-component 表示监听组件内元素class名 95 | 96 | 核心还是利用了微信提供的选择器,可以[参考文档](https://developers.weixin.qq.com/miniprogram/dev/api/wxml/SelectorQuery.selectAll.html) 97 | 98 | 99 | 100 | ### 特殊前缀 101 | 102 | $APP 表示读取App下定义的数据 103 | 104 | $DATASET.xxx 表示获取点击元素,定义data-xxx 中的 xxx值 105 | 106 | $INDEX 表示获取列表,当前点击元素的索引 107 | 108 | **需要获取$INDEX时,需要在wxml中加入data-index={{index}}标记** 109 | 110 | ``` 111 | 112 | ``` 113 | 114 | 115 | 116 | ### 兼容插件模式 117 | 118 | 由于SDK会改写Page对象,如果使用了插件,微信会禁止改写,可以通过以下方式改造。 119 | 120 | ``` 121 | // 初始化插件模式 122 | const tracker = new Tracker({ tracks: trackConfig, isUsingPlugin: true }); 123 | 124 | // 将原来的App包装 125 | tracker.createApp({ 126 | 127 | }) 128 | 129 | // 将原Page包装 130 | tracker.createPage({ 131 | 132 | }) 133 | 134 | // 将原Component包装 135 | tracker.createComponent({ 136 | 137 | }) 138 | ``` 139 | 140 | 141 | 142 | 143 | 144 | ### 方案实现说明 145 | 146 | [小程序从手动埋点到自动埋点](https://github.com/zhengguorong/articles/issues/34) 147 | 148 | [DEMO](https://github.com/zhengguorong/maizuo_wechat) 149 | 150 | ## License 151 | 152 | [996 License](https://github.com/zhengguorong/xbosstrack-wechat/blob/master/LICENSE) 153 | 154 | -------------------------------------------------------------------------------- /__tests__/wx.js: -------------------------------------------------------------------------------- 1 | global.wx = { 2 | request: jest.fn(), 3 | }; 4 | -------------------------------------------------------------------------------- /dist/xbosstrack.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.xbosstrack=t()}(this,function(){var e=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},t=function(){function e(e,t){for(var n=0;n3&&void 0!==arguments[3]?arguments[3]:[],o=e[n];e[n]=function(){for(var a=this,i=arguments.length,c=Array(i),u=0;u1&&void 0!==arguments[1]?arguments[1]:{},n=arguments[2],r=t.index,o=e.split("."),a=n;return o.length>-1?o.forEach(function(e){var t=function(e,t){var n=e.indexOf("["),r=e.indexOf("]"),o={};if(n>-1){var a=e.substring(n+1,r),i=e.substring(0,n);"$INDEX"===a&&(a=t),o.key=i,o.index=parseInt(a,10)}return o}(e,r);a=t.key?a[t.key][t.index]:a[e]}):a=n[e],a},h=function(e,t,n){try{return 0===e.indexOf("$")?function(e,t){var n="";e.indexOf("$APP.")>-1?n=getApp()[e.split("$APP.")[1]]:e.indexOf("$DATASET.")>-1?n=t[e.split("$DATASET.")[1]]:e.indexOf("$INDEX")>-1&&(n=t.index);return n}(e,t):u(e,t,n)}catch(e){return console.log(e),""}},s=function(e,t){var n=e.element,r=e.method,o=[];e.dataKeys.forEach(function(a){var i=h(a,e.dataset,t);o.push({element:n,method:r,name:a,data:i})}),console.table(o)};return function(r){function o(t){var r=t.tracks,a=t.isUsingPlugin;e(this,o);var i=n(this,(o.__proto__||Object.getPrototypeOf(o)).call(this,a));return i.tracks=r,i.addPageMethodExtra(i.elementTracker()),i.addPageMethodWrapper(i.methodTracker()),i.addComponentMethodWrapper(i.comMethodTracker()),i}return function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}(o,i),t(o,[{key:"elementTracker",value:function(){var e=this,t=function(t){var n=e.findActivePageTracks("element"),r=c().data;n.forEach(function(e){var n;(n=e.element,new Promise(function(e){var t=wx.createSelectorQuery();t.selectAll(n).boundingClientRect(),t.selectViewport().scrollOffset(),t.exec(function(t){return e({boundingClientRect:t[0],scrollOffset:t[1]})})})).then(function(n){n.boundingClientRect.forEach(function(o){var a=function(e,t,n){if(!t)return!1;var r=e.detail,o=r.x,a=r.y,i=t.left,c=t.right,u=t.top,h=t.height,s=n.scrollTop;return i3&&void 0!==arguments[3]?arguments[3]:{},a=e.findActivePageTracks("method"),i=c().data,u=(o.currentTarget||{}).dataset;a.forEach(function(e){e.method===r&&(e.dataset=u,s(e,i))})}}},{key:"comMethodTracker",value:function(){var e=this;return function(t,n,r){var o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},a=e.findActivePageTracks("comMethod"),i=n.data,c=(o.currentTarget||{}).dataset;a.forEach(function(e){e.method===r&&(e.dataset=c,s(e,i))})}}},{key:"findActivePageTracks",value:function(e){try{var t=c().route,n=this.tracks.find(function(e){return e.path===t})||{},r={};return"method"===e?r=n.methodTracks||[]:"element"===e?r=n.elementTracks||[]:"comMethod"===e&&(r=n.comMethodTracks||[]),r}catch(e){return{}}}}]),o}()}); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xbosstrack-wechat", 3 | "version": "1.0.1", 4 | "description": "### 声明", 5 | "main": "dist/xbosstrack.min.js", 6 | "scripts": { 7 | "lint": "./node_modules/.bin/eslint src", 8 | "test": "BABEL_ENV=test jest", 9 | "codecov": "node_modules/codecov/bin/codecov", 10 | "dev": "BABEL_ENV=build node_modules/rollup/bin/rollup --config rollup.config.js --environment entry:src/index.js,dest:dist/xbosstrack.js", 11 | "build": " BABEL_ENV=build node_modules/rollup/bin/rollup --config rollup.config.js --environment entry:src/index.js,dest:dist/xbosstrack.min.js,uglify", 12 | "watch": "BABEL_ENV=build node_modules/rollup/bin/rollup --config rollup.config.js --environment entry:src/index.js,dest:dist/xbosstrack.js -w" 13 | }, 14 | "jest": { 15 | "verbose": true, 16 | "testEnvironment": "node", 17 | "automock": false, 18 | "collectCoverage": true, 19 | "coverageDirectory": "./coverage/", 20 | "testPathIgnorePatterns": [ 21 | "./__tests__/wx.js" 22 | ], 23 | "setupFiles": ["./__tests__/wx.js"] 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/zhengguorong/xbosstrack.git" 28 | }, 29 | "author": "", 30 | "license": "ISC", 31 | "bugs": { 32 | "url": "https://github.com/zhengguorong/xbosstrack/issues" 33 | }, 34 | "homepage": "https://github.com/zhengguorong/xbosstrack#readme", 35 | "devDependencies": { 36 | "babel-core": "^6.26.3", 37 | "babel-jest": "^23.4.2", 38 | "babel-plugin-external-helpers": "^6.22.0", 39 | "babel-plugin-transform-class-properties": "^6.24.1", 40 | "babel-preset-env": "^1.7.0", 41 | "babel-preset-es2015-rollup": "^3.0.0", 42 | "babel-preset-latest": "^6.24.1", 43 | "codecov": "^3.1.0", 44 | "eslint": "^5.4.0", 45 | "eslint-config-airbnb-base": "^13.1.0", 46 | "eslint-plugin-import": "^2.14.0", 47 | "jest": "^23.5.0", 48 | "rollup": "^0.65.2", 49 | "rollup-plugin-babel": "^3.0.4", 50 | "rollup-plugin-uglify": "^3.0.0" 51 | }, 52 | "dependencies": {} 53 | } 54 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import uglify from 'rollup-plugin-uglify'; 3 | 4 | const config = { 5 | input: process.env.entry, 6 | output: { 7 | file: process.env.dest, 8 | format: 'umd', 9 | name: 'xbosstrack', 10 | strict: false, 11 | }, 12 | plugins: [ 13 | babel({ 14 | exclude: 'node_modules/**', 15 | plugins: ['transform-class-properties'], 16 | }), 17 | ], 18 | }; 19 | 20 | if (process.env.uglify) { 21 | config.plugins.push(uglify()); 22 | } 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | 2 | sonar.projectKey=zhengguorong_xbosstrack-wechat 3 | sonar.projectName=xbosstrack-wechat 4 | sonar.projectVersion=1.0 5 | 6 | # SQ standard properties 7 | sonar.sources=src/lib 8 | sonar.tests=__tests__ 9 | 10 | # Properties specific to language plugins: 11 | # - For JavaScript 12 | sonar.javascript.lcov.reportPaths=coverage/lcov.info -------------------------------------------------------------------------------- /src/helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取页面元素信息 3 | * @param {String} element 元素class或者id 4 | * @returns {Promise} 5 | */ 6 | export const getBoundingClientRect = function (element) { 7 | return new Promise((reslove) => { 8 | const query = wx.createSelectorQuery(); 9 | query.selectAll(element).boundingClientRect(); 10 | query.selectViewport().scrollOffset(); 11 | query.exec(res => reslove({ boundingClientRect: res[0], scrollOffset: res[1] })); 12 | }); 13 | }; 14 | /** 15 | * 判断点击是否落在目标元素 16 | * @param {Object} clickInfo 用户点击坐标 17 | * @param {Object} boundingClientRect 目标元素信息 18 | * @param {Object} scrollOffset 页面位置信息 19 | * @returns {Boolean} 20 | */ 21 | export const isClickTrackArea = function (clickInfo, boundingClientRect, scrollOffset) { 22 | if (!boundingClientRect) return false; 23 | const { x, y } = clickInfo.detail; // 点击的x y坐标 24 | const { left, right, top, height } = boundingClientRect; 25 | const { scrollTop } = scrollOffset; 26 | if (left < x && x < right && scrollTop + top < y && y < scrollTop + top + height) { 27 | return true; 28 | } 29 | return false; 30 | }; 31 | 32 | /** 33 | * 获取当前页面 34 | * @returns {Object} 当前页面Page对象 35 | */ 36 | export const getActivePage = function () { 37 | const curPages = getCurrentPages(); 38 | if (curPages.length) { 39 | return curPages[curPages.length - 1]; 40 | } 41 | return {}; 42 | }; 43 | 44 | /** 45 | * 获取前一页面 46 | * @returns {Object} 当前页面Page对象 47 | */ 48 | export const getPrevPage = function () { 49 | const curPages = getCurrentPages(); 50 | if (curPages.length > 1) { 51 | return curPages[curPages.length - 2]; 52 | } 53 | return {}; 54 | }; 55 | 56 | export const _isPromise = function (value) { 57 | return value && Object.prototype.toString.call(value) === '[object Promise]'; 58 | }; 59 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Wrapper from './wrapper'; 2 | import { getBoundingClientRect, isClickTrackArea, getActivePage } from './helper'; 3 | import report from './report'; 4 | 5 | class Tracker extends Wrapper { 6 | constructor({ tracks, isUsingPlugin }) { 7 | super(isUsingPlugin); 8 | // 埋点配置信息 9 | this.tracks = tracks; 10 | // 自动给每个page增加elementTracker方法,用作元素埋点 11 | this.addPageMethodExtra(this.elementTracker()); 12 | // 自动给page下预先定义的方法进行监听,用作方法执行埋点 13 | this.addPageMethodWrapper(this.methodTracker()); 14 | // 自动给page component下预先定义的方法进行监听,用作方法执行埋点 15 | this.addComponentMethodWrapper(this.comMethodTracker()); 16 | } 17 | 18 | elementTracker() { 19 | // elementTracker变量名尽量不要修改,因为他和wxml下的名字是相对应的 20 | const elementTracker = (e) => { 21 | const tracks = this.findActivePageTracks('element'); 22 | const { data } = getActivePage(); 23 | tracks.forEach((track) => { 24 | getBoundingClientRect(track.element).then((res) => { 25 | res.boundingClientRect.forEach((item) => { 26 | const isHit = isClickTrackArea(e, item, res.scrollOffset); 27 | track.dataset = item.dataset; 28 | isHit && report(track, data); 29 | }); 30 | }); 31 | }); 32 | }; 33 | return elementTracker; 34 | } 35 | 36 | methodTracker() { 37 | return (page, component, methodName, args = {}) => { 38 | const tracks = this.findActivePageTracks('method'); 39 | const { data } = getActivePage(); 40 | const { dataset } = args.currentTarget || {}; 41 | tracks.forEach((track) => { 42 | if (track.method === methodName) { 43 | track.dataset = dataset; 44 | report(track, data); 45 | } 46 | }); 47 | }; 48 | } 49 | 50 | /** 51 | * function函数改变上下文this指针,指向组件 52 | */ 53 | 54 | comMethodTracker() { 55 | var self = this 56 | return function(page, component, methodName, args = {}) { 57 | const tracks = self.findActivePageTracks('comMethod'); 58 | const data = this.data; 59 | const { dataset } = args.currentTarget || {}; 60 | tracks.forEach((track) => { 61 | if (track.method === methodName) { 62 | track.dataset = dataset; 63 | report(track, data); 64 | } 65 | }); 66 | }; 67 | } 68 | 69 | /** 70 | * 获取当前页面的埋点配置 71 | * @param {String} type 返回的埋点配置,options: method/element/comMethod 72 | * @returns {Object} 73 | */ 74 | findActivePageTracks(type) { 75 | try { 76 | const { route } = getActivePage(); 77 | const pageTrackConfig = this.tracks.find(item => item.path === route) || {}; 78 | let tracks = {}; 79 | if (type === 'method') { 80 | tracks = pageTrackConfig.methodTracks || []; 81 | } else if (type === 'element') { 82 | tracks = pageTrackConfig.elementTracks || []; 83 | }else if (type === 'comMethod') { 84 | tracks = pageTrackConfig.comMethodTracks || []; 85 | } 86 | return tracks; 87 | } catch (e) { 88 | return {}; 89 | } 90 | } 91 | } 92 | 93 | export default Tracker; 94 | -------------------------------------------------------------------------------- /src/report.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 解析数组类型dataKey 3 | * 例如list[$INDEX],返回{key:list, index: $INDEX} 4 | * 例如list[4],返回{key:list, index: 4} 5 | * @param {*} key 6 | * @param {*} index 7 | */ 8 | const resloveArrayDataKey = (key, index) => { 9 | const leftBracketIndex = key.indexOf('['); 10 | const rightBracketIndex = key.indexOf(']'); 11 | const result = {}; 12 | if (leftBracketIndex > -1) { 13 | let arrIndex = key.substring(leftBracketIndex + 1, rightBracketIndex); 14 | const arrKey = key.substring(0, leftBracketIndex); 15 | if (arrIndex === '$INDEX') { 16 | arrIndex = index; 17 | } 18 | result.key = arrKey; 19 | result.index = parseInt(arrIndex, 10); 20 | } 21 | return result; 22 | }; 23 | 24 | /** 25 | * 获取全局数据 26 | * @param {*} key 目前支持$APP.* $DATASET.* $INDEX 27 | * @param {*} dataset 点击元素dataset 28 | * @param {*} index 点击元素索引 29 | */ 30 | const getGloabData = (key, dataset) => { 31 | let result = ''; 32 | if (key.indexOf('$APP.') > -1) { 33 | const App = getApp(); 34 | const appKey = key.split('$APP.')[1]; 35 | result = App[appKey]; 36 | } else if (key.indexOf('$DATASET.') > -1) { 37 | const setKey = key.split('$DATASET.')[1]; 38 | result = dataset[setKey]; 39 | } else if (key.indexOf('$INDEX') > -1) { 40 | result = dataset.index; 41 | } 42 | return result; 43 | }; 44 | 45 | const getPageData = (key, dataset = {}, paegData) => { 46 | const { index } = dataset; 47 | const keys = key.split('.'); 48 | let result = paegData; 49 | if (keys.length > -1) { 50 | keys.forEach((name) => { 51 | const res = resloveArrayDataKey(name, index); 52 | if (res.key) { 53 | result = result[res.key][res.index]; 54 | } else { 55 | result = result[name]; 56 | } 57 | }); 58 | } else { 59 | result = paegData[key]; 60 | } 61 | return result; 62 | }; 63 | 64 | const dataReader = (key, dataset, pageData) => { 65 | try { 66 | let result = ''; 67 | if (key.indexOf('$') === 0) { 68 | result = getGloabData(key, dataset); 69 | } else { 70 | result = getPageData(key, dataset, pageData); 71 | } 72 | return result; 73 | } catch (e) { 74 | console.log(e); 75 | return ''; 76 | } 77 | }; 78 | 79 | 80 | const report = (track, pageData) => { 81 | const { element, method } = track; 82 | const logger = []; 83 | track.dataKeys.forEach(name => { 84 | const data = dataReader(name, track.dataset, pageData); 85 | logger.push({ element, method, name, data }); 86 | }); 87 | console.table(logger); 88 | }; 89 | 90 | export default report; 91 | -------------------------------------------------------------------------------- /src/wrapper.js: -------------------------------------------------------------------------------- 1 | const globalVarApp = App; // 小程序原App对象 2 | const globalVarPage = Page; // 小程序原Page对象 3 | const globalVarComponent = Component; // 小程序原Component对象 4 | 5 | class Wrapper { 6 | constructor(isUsingPlugin) { 7 | this.injectPageMethods = []; 8 | this.injectAppMethods = []; 9 | this.extraPageMethods = []; 10 | this.extraAppMethods = []; 11 | this.injectComponentMethods = []; 12 | this.extraComponentMethods = []; 13 | if (!isUsingPlugin) { 14 | App = (app) => globalVarApp(this._create(app, this.injectAppMethods, this.extraAppMethods)); 15 | Page = (page) => globalVarPage(this._create(page, this.injectPageMethods, this.extraPageMethods)); 16 | Component = (component) => globalVarComponent(this._createComponent(component, this.injectComponentMethods, this.extraComponentMethods)); 17 | } 18 | } 19 | 20 | /** 21 | * 对用户定义函数进行包装. 22 | * @param {Object} target page对象或者app对象 23 | * @param {String} methodName 需要包装的函数名 24 | * @param {Array} methods 函数执行前执行任务 25 | */ 26 | _wrapTargetMethod(target, component, methodName, methods = []) { 27 | const methodFunction = target[methodName]; 28 | target[methodName] = function _aa(...args) { 29 | const result = methodFunction && methodFunction.apply(this, args); 30 | const methodExcuter = () => { 31 | methods.forEach((fn) => { 32 | fn.apply(this, [target, component, methodName, ...args]); 33 | }); 34 | }; 35 | try { 36 | if (Object.prototype.toString.call(result) === '[object Promise]') { 37 | result.then(() => { 38 | methodExcuter(); 39 | }).catch(() => { 40 | methodExcuter(); 41 | }); 42 | } else { 43 | methodExcuter(); 44 | } 45 | } catch (e) { 46 | console.error(methodName, '钩子函数执行出现错误', e); 47 | } 48 | return result; 49 | }; 50 | } 51 | 52 | /** 53 | * 追加函数到Page/App对象 54 | * @param {Object} target page对象或者app对象 55 | * @param {Array} methods 需要追加的函数数组 56 | */ 57 | _addExtraMethod(target, methods) { 58 | methods 59 | .forEach(fn => { 60 | const methodName = fn.name; 61 | target[methodName] = fn; 62 | }); 63 | } 64 | 65 | /** 66 | * @param {*} target page对象或者app对象 67 | * @param {*} methods 需要插入执行的函数 68 | */ 69 | _create(target, injectMethods, extraMethods) { 70 | Object.keys(target) 71 | .filter((prop) => typeof target[prop] === 'function') 72 | .forEach((methodName) => { 73 | this._wrapTargetMethod(target, null, methodName, injectMethods); 74 | }); 75 | this._addExtraMethod(target, extraMethods); 76 | return target; 77 | } 78 | 79 | _createComponent(component, injectMethods, extraMethods) { 80 | const target = component.methods; 81 | Object.keys(target) 82 | .filter((prop) => typeof target[prop] === 'function') 83 | .forEach((methodName) => { 84 | this._wrapTargetMethod(target, component, methodName, injectMethods); 85 | }); 86 | this._addExtraMethod(target, extraMethods); 87 | return component; 88 | } 89 | 90 | addPageMethodWrapper(fn) { 91 | this.injectPageMethods.push(fn); 92 | } 93 | 94 | addComponentMethodWrapper(fn) { 95 | this.injectComponentMethods.push(fn); 96 | } 97 | 98 | addAppMethodWrapper(fn) { 99 | this.injectAppMethods.push(fn); 100 | } 101 | 102 | addPageMethodExtra(fn) { 103 | this.extraPageMethods.push(fn); 104 | } 105 | 106 | addAppMethodExtra(fn) { 107 | this.extraAppMethods.push(fn); 108 | } 109 | 110 | createApp(app) { 111 | globalVarApp(this._create(app, this.injectAppMethods, this.extraAppMethods)); 112 | } 113 | 114 | createPage(page) { 115 | globalVarPage(this._create(page, this.injectPageMethods, this.extraPageMethods)); 116 | } 117 | 118 | createComponent(component) { 119 | globalVarPage(this._createComponent(component, this.injectPageMethods, this.extraPageMethods)); 120 | } 121 | } 122 | 123 | export default Wrapper; 124 | --------------------------------------------------------------------------------