├── lib └── alog.js ├── .bowerrc ├── .jshintrc ├── .travis.yml ├── test ├── setup.js ├── test.js └── test.html ├── .npmignore ├── tools ├── build.sh ├── build.bat ├── localhost.js ├── delblock.js └── build.js ├── bower.json ├── .gitignore ├── .editorconfig ├── .jsbeautifyrc ├── examples ├── pv │ └── index.html ├── tongji │ ├── index.html │ └── tongji.js └── speed │ ├── speed.js │ └── index.html ├── package.json ├── README.md ├── alog.min.js ├── doc ├── API.md └── alog代码浅析.md ├── alog.js └── src └── alog.js /lib/alog.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "lib" 3 | } -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "undef": false, 3 | "evil": true 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.11 4 | - 0.10 5 | script: 6 | - npm run lint 7 | - npm run test 8 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | assert = (typeof chai !== "undefined" && chai !== null ? chai.assert : void 0) || require('chai').assert; 3 | }()); 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | src/ 3 | tests/ 4 | tools/ 5 | node_modules/ 6 | CNAME 7 | Makefile 8 | *.ico 9 | *.html 10 | *.txt 11 | *.xml 12 | *.yml 13 | *.iml 14 | .* 15 | *~ 16 | -------------------------------------------------------------------------------- /tools/build.sh: -------------------------------------------------------------------------------- 1 | node delblock ../src/alog.js alog.lovely.js debug 2 | node build alog.lovely.js alog.lovely.js 3 | uglifyjs alog.lovely.js -o ../dist/alog.min.js --unsafe --lift-vars -c -nm 4 | rm alog.lovely.js 5 | -------------------------------------------------------------------------------- /tools/build.bat: -------------------------------------------------------------------------------- 1 | node delblock ..\src\alog.js alog.lovely.js debug 2 | node build alog.lovely.js alog.lovely.js 3 | call uglifyjs alog.lovely.js -o ..\dist\alog.min.js --unsafe --lift-vars -c -nm 4 | del alog.lovely.js 5 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | describe('Tests alog pv', function() { 4 | it('typeof alog', function(done) { 5 | assert.equal(typeof alog, 'function'); 6 | done(); 7 | }); 8 | it('alog set / get', function(done) { 9 | alog('set', 'name', 'zswang'); 10 | alog('get', 'name', function(value) { 11 | assert.equal(value, 'zswang'); 12 | done(); 13 | }); 14 | }); 15 | }); 16 | 17 | }).call(this); 18 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alogs", 3 | "version": "0.1.4", 4 | "homepage": "https://github.com/fex-team/alogs", 5 | "description": "The front-end log collection module management", 6 | "authors": [ 7 | "Zswang " 8 | ], 9 | "main": "alog.js", 10 | "moduleType": [ 11 | "globals" 12 | ], 13 | "keywords": [ 14 | "log", 15 | "analytic", 16 | "tracking" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "app/components", 24 | "test", 25 | "tests" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # vim 6 | *.sw[a-z] 7 | vim/.netrwhist 8 | 9 | # temp 10 | .DS_Store 11 | *.db 12 | *.bak 13 | *.tmp 14 | *.cmd 15 | ~* 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # Deployed apps should consider commenting this line out: 28 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 29 | node_modules 30 | lib 31 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [**.js] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [**.css] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [**.less] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [**.styl] 22 | indent_style = space 23 | indent_size = 4 24 | 25 | [**.html] 26 | indent_style = space 27 | indent_size = 4 28 | 29 | [**.tpl] 30 | indent_style = space 31 | indent_size = 4 32 | 33 | [**.json] 34 | indent_style = space 35 | indent_size = 4 36 | 37 | [*.md] 38 | trim_trailing_whitespace = false 39 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent_size": 4, 3 | "indent_char": " ", 4 | "indent_level": 0, 5 | "indent_with_tabs": false, 6 | "preserve_newlines": true, 7 | "max_preserve_newlines": 10, 8 | "jslint_happy": false, 9 | "space_in_paren": false, 10 | 11 | "brace_style": "end-expand", 12 | 13 | "jslint_happy": true, 14 | "space_after_anon_function": true, 15 | 16 | "keep_array_indentation": false, 17 | "keep_function_indentation": false, 18 | "space_before_conditional": false, 19 | "break_chained_methods": false, 20 | "eval_code": false, 21 | "unescape_strings": false, 22 | "wrap_line_length": 0 23 | } 24 | -------------------------------------------------------------------------------- /tools/localhost.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author 王集鹄(wangjihu,http://weibo.com/zswang) 3 | */ 4 | var http = require('http'); 5 | var url = require('url'); 6 | var path = require('path'); 7 | var fs = require('fs'); 8 | var util = require('util'); 9 | 10 | http.createServer(function(request, response){ 11 | var urlInfo = url.parse(request.url, true); 12 | response.writeHead(200, { 13 | 'Content-Type': 'text/javascript' 14 | }); 15 | var filename = path.join('.', urlInfo.pathname), text = ''; 16 | if (/\.js$/i.test(filename) && fs.existsSync(filename)){ 17 | text = fs.readFileSync(filename); 18 | } 19 | response.end(text); 20 | }).listen(process.argv[2] || "80"); 21 | -------------------------------------------------------------------------------- /examples/pv/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 12 | 13 | 14 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tools/delblock.js: -------------------------------------------------------------------------------- 1 | void function(){ 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var util = require('util'); 6 | var inputFile = process.argv[2]; 7 | var outputFile = process.argv[3]; 8 | var names = process.argv[4]; 9 | if (!inputFile || !outputFile) return; 10 | var content = String(fs.readFileSync(inputFile)).replace(/\r\n/ig, "\n"); 11 | names = names.split(/[,|\s]+/); 12 | for (var i = names.length - 1; i >= 0; i--){ 13 | var regex = eval("(" + 14 | String(/[ \f\t\v]*\/\*\s*#name#\s+start\s*\*\/[\s\S]*?\/\*\s*#name#\s+end\s*\*\/[ \f\t\v]*/ig).replace(/#name#/g, names[i]) 15 | + ")"); 16 | content = content.replace(regex, ''); 17 | } 18 | fs.writeFileSync(outputFile, content); 19 | }(); -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tests Passing 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 16 | 17 | 18 | 19 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alogs", 3 | "version": "0.1.5", 4 | "description": "The front-end log collection module management", 5 | "author": "zswang", 6 | "main": "index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/fex-team/alogs.git" 10 | }, 11 | "main": "alog.js", 12 | "keywords": [ 13 | "analytic", 14 | "tracking" 15 | ], 16 | "license": "MIT", 17 | "devDependencies": { 18 | "mocha": "~1.20.1", 19 | "jdists": "0.3.10", 20 | "fecs": "0.1.1", 21 | "mocha-phantomjs": "3.5.2", 22 | "chai": "1.8.x" 23 | }, 24 | "scripts": { 25 | "test": "mocha-phantomjs test/test.html -s webSecurityEnabled=false", 26 | "dist": "jdists src/alog.js -o alog.js && uglifyjs alog.js -o alog.min.js -p 5 -c -m", 27 | "lint": "fecs src/*.js --reporter=baidu" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/tongji/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 12 | 13 | 14 | 18 | 19 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | alogs 2 | 3 | ======= 4 | 5 | [![Build Status](https://img.shields.io/travis/fex-team/alogs/master.svg)](https://travis-ci.org/fex-team/alogs) 6 | [![NPM version](https://img.shields.io/npm/v/alogs.svg)](http://badge.fury.io/js/alogs) 7 | 8 | ## 概述 9 | 10 | alogs 是一个可以并行多个统计模块的框架 11 | 12 | ## 使用 13 | 14 | ### 安装 15 | 16 | ``` 17 | $npm install alogs 18 | ``` 19 | 20 | 或者 21 | 22 | ``` 23 | $bower install alogs 24 | ``` 25 | 26 | ### 引用 27 | 28 | ```html 29 | 34 | ``` 35 | 36 | ### 背景 37 | 38 | 我们会使用或开发各种不同的统计模块对产品的使用情况进行收集,以便衡量产品的健康状况和对产品发展方向进行决策 39 | 40 | + 随着前端交互越来越丰富,统计要分析的维度也越来越多样 41 | + 经常一个页面中就会并行着多个统计模块:有性能相关的、有点击相关的、有业务相关的 42 | + alogs 使用一些少量简单的 API 将这些复杂的统计模块统一组织起来 43 | 44 | ### 解决什么问题? 45 | 46 | + 减少统计模块加载对产品的影响 47 | 48 | > alogs 使用异步方式加载统计模块,不堵塞页面正常资源加载; 49 | > 另外值得一提的是,alogs 的模块文件不依赖加载顺序、兼容同步和异步加载。 50 | 51 | + 并行多个统计模块 52 | 53 | ### alogs 适合什么应用场景? 54 | 55 | + 简单统计 56 | 57 | > 直接这页面中调用 58 | 59 | + 复杂统计 60 | 61 | > 定义和业务紧密相关的复杂模块 62 | 63 | ### 代理统计 64 | 65 | > 接入第三方统计模块 66 | -------------------------------------------------------------------------------- /examples/speed/speed.js: -------------------------------------------------------------------------------- 1 | (function(winElement, docElement) { 2 | // 压缩代码相关 3 | /* compressor */ 4 | 5 | var objectName = winElement.alogObjectName || 'alog'; 6 | 7 | var alog = winElement[objectName] = winElement[objectName] || function() { 8 | winElement[objectName].l = winElement[objectName].l || +new Date; 9 | (winElement[objectName].q = winElement[objectName].q || []).push(arguments); 10 | }; 11 | var trackerName = 'speed'; 12 | alog('define', trackerName, function() { 13 | var tracker = alog.tracker(trackerName); 14 | var timestamp = alog.timestamp; // 获取时间戳的函数,相对于alog被声明的时间 15 | tracker.on('record', function(url, time) { 16 | var data = {}; 17 | data[url] = timestamp(time); 18 | tracker.send('timing', data); 19 | }); 20 | tracker.set('protocolParameter', { 21 | // 配置字段,不需要上报 22 | headend: null, 23 | bodyend: null, 24 | domready: null 25 | }); 26 | tracker.create({ 27 | postUrl: 'http://localhost:8080/t.gif' 28 | }); 29 | tracker.send('pageview', { 30 | ht: timestamp(tracker.get('headend')), 31 | lt: timestamp(tracker.get('bodyend')), 32 | drt: timestamp(tracker.get('domready')) 33 | }); 34 | return tracker; 35 | }); 36 | 37 | })(window, document); 38 | -------------------------------------------------------------------------------- /examples/speed/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 39 | 40 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /examples/tongji/tongji.js: -------------------------------------------------------------------------------- 1 | void 2 | function(winElement, docElement) { 3 | // 压缩代码相关 4 | /* compressor */ 5 | 6 | var objectName = winElement.alogObjectName || 'alog'; 7 | 8 | var alog = winElement[objectName] = winElement[objectName] || function() { 9 | winElement[objectName].l = winElement[objectName].l || +new Date; 10 | (winElement[objectName].q = winElement[objectName].q || []).push(arguments); 11 | }; 12 | 13 | winElement._hmt = winElement._hmt || []; 14 | 15 | function addScript(url, callback) { 16 | var script = docElement.createElement("script"), 17 | scriptLoaded = 0; 18 | 19 | // IE和opera支持onreadystatechange 20 | // safari、chrome、opera支持onload 21 | script.onload = script.onreadystatechange = function() { 22 | // 避免opera下的多次调用 23 | if (scriptLoaded) { 24 | return; 25 | }; 26 | 27 | var readyState = script.readyState; 28 | if ('undefined' == typeof readyState || readyState == "loaded" || readyState == "complete") { 29 | scriptLoaded = 1; 30 | try { 31 | callback(); 32 | } finally { 33 | script.onload = script.onreadystatechange = null; 34 | script.parentNode.removeChild(script); 35 | } 36 | } 37 | }; 38 | 39 | script.asyn = 1; 40 | script.src = url; 41 | var lastScript = docElement.getElementsByTagName("script")[0]; 42 | lastScript.parentNode.insertBefore(script, lastScript); 43 | } 44 | var trackerName = 'tongji'; 45 | alog('define', trackerName, function() { 46 | var tracker = alog.tracker(trackerName); 47 | addScript("http://hm.baidu.com/hm.js?" + tracker.get('id'), function() { 48 | tracker.create({ 49 | postUrl: null 50 | }); 51 | tracker.on('send', function(data) { 52 | if (window._hmt && (data.t == 'event' || data.hitType == 'event')) { 53 | //http://tongji.baidu.com/open/api/more?p=guide_trackEvent 54 | //category:要监控的目标的类型名称,通常是同一组目标的名字,比如"视频"、"音乐"、"软件"、"游戏"等等。该项必选。 55 | //action:用户跟目标交互的行为,如"播放"、"暂停"、"下载"等等。该项必选。 56 | //opt_label:事件的一些额外信息,通常可以是歌曲的名称、软件的名称、链接的名称等等。该项可选。 57 | //opt_value:事件的一些数值信息,比如权重、时长、价格等等,在报表中可以看到其平均值等数据。该项可选。 58 | window._hmt.push(['_trackEvent', data['eventCategory'], data['eventAction'] || '', data['eventLabel'] || 0]); 59 | } 60 | }); 61 | }); 62 | return tracker; 63 | }); 64 | 65 | }(window, document); 66 | -------------------------------------------------------------------------------- /tools/build.js: -------------------------------------------------------------------------------- 1 | void function(){ 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var util = require('util'); 6 | var inputFile = process.argv[2]; 7 | var outputFile = process.argv[3]; 8 | var wrap = process.argv[4]; 9 | if (!inputFile || !outputFile) return; 10 | var body = String(fs.readFileSync(inputFile)) 11 | .replace(/\r\n/g, '\n') 12 | .replace(/[ \f\t\v]*\/\*\s*debug\s+start\s*\*\/[\s\S]*?\/\*\s*debug\s+end\s*\*\/[ \f\t\v]*/ig, "") 13 | .replace(/[ \f\t\v]*\/\*\s*background\s+start\s*\*\/[\s\S]*?\/\*\s*background\s+end\s*\*\/[ \f\t\v]*/ig, "") 14 | .replace(/\/\*(?!\s*compressor)[\s\S]*?\*\//ig, "\n") // 注释 15 | if (wrap == 'wrap'){ 16 | body = body.replace(/\/\*[\s\S]*?\*\//ig, "\n"); 17 | body = 'void function(){\n\ 18 | /* compressor */\n\ 19 | ' + body + '\n\ 20 | }()'; 21 | } 22 | var dict = {}; 23 | 24 | body 25 | .replace(/([+\s()\[\]]+)(Math|parseInt|encodeURIComponent)\b/g, function(all, space, word){ 26 | if (dict[word]){ 27 | dict[word]++; 28 | } else { 29 | dict[word] = 1; 30 | } 31 | }); 32 | 33 | var var_list = [], var_dict = {}; 34 | for (var key in dict){ 35 | // 原始长度 key.length * dict[key] 36 | // 替换后长度 2 * (dict[key] - 1) 37 | if (key.length * dict[key] > 2 * dict[key] + key.length){ 38 | var alias = util.format('var_alias_%s', key); 39 | var_list.push(util.format('%s=%s', alias, key)); 40 | var_dict[key] = alias; 41 | } 42 | 43 | } 44 | 45 | body.replace(/\.([a-z]\w*)(?!\w*['"])/ig, function(key){ 46 | if (dict[key]){ 47 | dict[key]++; 48 | } else { 49 | dict[key] = 1; 50 | } 51 | }); 52 | //* 53 | var prop_dict = {}; 54 | var str_dict = {}; 55 | for (var key in dict){ 56 | if (var_dict[key]) continue; 57 | // 原始长度 key.length * dict[key] 58 | // 替换后长度 3 * dict[key] + key.length 59 | if (key.length * dict[key] > 3 * dict[key] + key.length){ 60 | var alias = util.format('prop_alias_%s', key.substr(1)); 61 | var_list.push(util.format('%s="%s"', alias, key.substr(1))); 62 | if (str_dict[key]){ 63 | str_dict[key] ++; 64 | } else { 65 | str_dict[key] = 1; 66 | } 67 | prop_dict[key] = '[' + alias + ']'; 68 | } 69 | } 70 | body.replace(/("|')(\w+)(\1)/g, function(all, $1, key, $2){ 71 | key = '.' + key; 72 | if (str_dict[key]){ 73 | str_dict[key]++; 74 | } else { 75 | str_dict[key] = 1; 76 | } 77 | }); 78 | 79 | for (var key in str_dict){ 80 | if (var_dict[key]) continue; 81 | if (str_dict[key] < 2) continue; 82 | var alias = util.format('prop_alias_%s', key.substr(1)); 83 | if (!prop_dict[key]){ 84 | var_list.push(util.format('%s="%s"', alias, key.substr(1))); 85 | prop_dict[key] = '[' + alias + ']'; 86 | } 87 | } 88 | 89 | body = body 90 | .replace(/([+\s()\[\]]+)(window|document|Math|parseInt|encodeURIComponent)\b/g, function(all, space, word){ 91 | return var_dict[word] ? space + var_dict[word] : all; 92 | }) 93 | .replace(/("|')(\w+)(\1)/g, function(all, $1, key, $2){ 94 | if (prop_dict['.' + key]){ 95 | return prop_dict['.' + key].replace(/^\[|\]$/g, ''); 96 | } else { 97 | return all; 98 | } 99 | }) 100 | .replace(/\.([a-z]\w*)(?!\w*['"])/ig, function(key){ 101 | return prop_dict[key] || key; 102 | }) 103 | .replace(/\/\*\s*compressor\s*\*\//i, 104 | 'var\n ' + var_list.join(',\n ') + ';' 105 | ); 106 | 107 | fs.writeFileSync(outputFile, body) 108 | }(); -------------------------------------------------------------------------------- /alog.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){function n(e){var t,r,a,o=arguments;if("define"===e||"require"===e){for(var s=1;si;i++)n.push(r[i]);for(var o=0,s=t.length;s--;)t[s].apply(this,n)&&o++;return o}}function u(e,r){if(e&&r){var i=t.createElement("img"),a=[];for(var o in r)r[o]&&a.push(o+"="+encodeURIComponent(r[o]));var s="img_"+ +new Date;n[s]=i,i.onload=i.onerror=function(){n[s]=i=i.onload=i.onerror=null,delete n[s]},i.src=e+(e.indexOf("?")<0?"?":"&")+a.join("&")}}function p(e,t){if(!e)return t;var n={};for(var r in t)null!==e[r]&&(n[e[r]||r]=t[r]);return n}function l(){var e=arguments,t=e[0];if(this.created||/^(on|un|set|get|create)$/.test(t)){for(var n=h.prototype[t],r=[],i=1,a=e.length;a>i;i++)r.push(e[i]);"function"==typeof n&&n.apply(this,r)}else this.argsList.push(e)}function v(e,t){var n={};for(var r in e)e.hasOwnProperty(r)&&(n[r]=e[r]);for(var i in t)t.hasOwnProperty(i)&&(n[i]=t[i]);return n}function h(e){this.name=e,this.fields={protocolParameter:{postUrl:null,protocolParameter:null}},this.argsList=[],this.alog=n}function d(e){if(e=e||"default","*"===e){var t=[];for(var n in x)x.hasOwnProperty(n)&&t.push(x[n]);return t}return x[e]=x[e]||new h(e)}function g(){if(!(P&&new Date-w<50||b)){b=!0;var e=0;for(var t in x)if(x.hasOwnProperty(t)){var n=x[t];n.created&&(e+=n.fire("unload"))}if(e)for(var r=new Date;new Date-r<100;);}}var m=e.alogObjectName||"alog",y=e[m];if(!y||!y.defined){var w,b,E,P=e.attachEvent&&!window.opera,D=y&&y.l||+new Date,L=e.logId||(+new Date).toString(36)+Math.random().toString(36).substr(2,3),O=0,j={},k={alog:{name:"alog",defined:!0,instance:n}},q={},x={};if(h.prototype.create=function(e){if(!this.created){"object"==typeof e&&this.set(e),this.created=new Date,this.fire("create",this);for(var t;t=this.argsList.shift();)l.apply(this,t)}},h.prototype.send=function(e,t){var n=v({ts:o().toString(36),t:e,sid:L},this.fields);if("object"==typeof t)n=v(n,t);else{var r=arguments;switch(e){case"pageview":r[1]&&(n.page=r[1]),r[2]&&(n.title=r[2]);break;case"event":r[1]&&(n.eventCategory=r[1]),r[2]&&(n.eventAction=r[2]),r[3]&&(n.eventLabel=r[3]),r[4]&&(n.eventValue=r[4]);break;case"timing":r[1]&&(n.timingCategory=r[1]),r[2]&&(n.timingVar=r[2]),r[3]&&(n.timingValue=r[3]),r[4]&&(n.timingLabel=r[4]);break;case"exception":r[1]&&(n.exDescription=r[1]),r[2]&&(n.exFatal=r[2]);break;default:return}}this.fire("send",n),u(this.fields.postUrl,p(this.fields.protocolParameter,n))},h.prototype.set=function(e,t){if("string"==typeof e)"protocolParameter"===e&&(t=v({postUrl:null,protocolParameter:null},t)),this.fields[e]=t;else if("object"==typeof e)for(var n in e)e.hasOwnProperty(n)&&this.set(n,e[n])},h.prototype.get=function(e,t){var n=this.fields[e];return"function"==typeof t&&t(n),n},h.prototype.fire=function(e){for(var t=[this.name+"."+e],n=arguments,r=1,i=n.length;i>r;r++)t.push(n[r]);return c.apply(this,t)},h.prototype.on=function(e,t){n.on(this.name+"."+e,t)},h.prototype.un=function(e,t){n.un(this.name+"."+e,t)},n.name="alog",n.sid=L,n.defined=!0,n.timestamp=o,n.un=f,n.on=s,n.fire=c,n.tracker=d,n("init"),E=d(),E.set("protocolParameter",{modules:null}),y){var N=[].concat(y.p||[],y.q||[]);y.p=y.q=null;for(var S in n)n.hasOwnProperty(S)&&(y[S]=n[S]);n.p=n.q={push:function(e){n.apply(n,e)}};for(var U=0;U." 11 | * @param {Object} params 方法 12 | */ 13 | alog('pv.send', 'pageview'); 14 | ``` 15 | 16 | #### 5-1-2. ALog 模块定义 `define:alog('define', moduleName, requires, dealFunc)` 17 | 18 | ```js 19 | /** 20 | * 定义模块 21 | * @param {string} 'define' 22 | * @param {string} moduleName 模块名 23 | * @param {Array=} requires 依赖模块名 24 | * @param {Function} dealFunc 处理函数 25 | */ 26 | alog('define', 'pv', function(){ 27 | var pvTracker = alog.tracker('pv'); 28 | pvTracker.set('ver', 1); 29 | pvTracker.set('px', window.screen.width + 'x' + window.screen.height); 30 | 31 | //由于模块一般用于统计,而每个统计模块都需要一个 tracker 实例,所以一般返回一个 tracker 32 | return pvTracker; 33 | /** 34 | * 也可以返回一个module 35 | * return { 36 | * 'on': function(){}, 37 | * 'un': function(){} 38 | * } 39 | */ 40 | }); 41 | ``` 42 | 43 | #### 5-1-3. ALog 模块引用 `require:alog('require', [moduleName], callback)` 44 | 45 | ```js 46 | /** 47 | * 引用模块 48 | * @param {string} 'require' 49 | * @param {Array} 引用的模块 50 | * @param {Function} callback 回调函数 51 | */ 52 | alog('require', ['pv'], function(pvTracker){ 53 | pvTracker.create({ 54 | postUrl: 'http://localhost/u.gif' 55 | }); 56 | }); 57 | ``` 58 | 59 | #### 5-1-4. ALog 获取/创建追踪器: `alog.tracker(trackerName)` 60 | 61 | ```js 62 | /** 63 | * 获取/创建追踪器,如果没有名为trackerName的tracker实例,将自动创建一个名为trackerName的tracker实例 64 | * @param {string}[optional] trackerName 追踪器名 65 | */ 66 | //获取alog的tracker 67 | alog.tracker(); 68 | alog.tracker('default'); 69 | //获取pv模块的tracker 70 | alog.tracker('pv'); 71 | ``` 72 | 73 | ### 5-2. ATracker 模块 74 | 75 | Tracker 模块的方法调用有两种方法: 76 | 77 | 1. 同步方法: `moduleTracker.method(params)` 78 | 79 | ```js 80 | alog('require', ['module'], function(moduleTracker){ 81 | var btn = document.getElementById('btn'); 82 | btn.onclick = function(){ 83 | moduleTracker.send('event', { 84 | type: 'click', 85 | target: btn, 86 | val: btn.value 87 | }); 88 | } 89 | }); 90 | ``` 91 | 92 | 2. 异步方法: `alog('moduleTracker.method', params)` 93 | 94 | ```js 95 | var btn = document.getElementById('btn'); 96 | btn.onclick = function(){ 97 | alog('module.send', 'event', { 98 | type: 'click', 99 | target: btn, 100 | val: btn.value 101 | }); 102 | }; 103 | ``` 104 | 105 | 上述两种方法都可以做到统计 `ID` 为 `btn` 的元素点击,并上报记录,但区别是: 106 | 107 | * 同步方法会在模块加载完,才开始绑定点击事件,所以可能会丢失加载前的点击。同步方法适合用户模块定义内部。 108 | * 异步方法在执行代码后生效,如果使用的模块还没加载完,会先放到事件队列里,等模块加载完成并执行start方法后,会自动执行事件队列里的时间 109 | 110 | #### 5-2-1. Tracker 开始上报 create 111 | 112 | 两种方法 113 | 114 | * `moduleTracker.create(fields)` 同步方法(Sync) 115 | * `alog('module.create', fields)` 异步方法(Async) 116 | 117 | ```js 118 | /** 119 | * 创建追踪器实例 120 | * @param {string} 'module.create' 模块创建追踪器实例 121 | * @param {Object} fields 字段对象 122 | */ 123 | alog('pv.create', { 124 | postUrl: 'http://localhost/u.gif' 125 | }); 126 | ``` 127 | 128 | #### 5-2-1. Tracker 设置上报字段 set 129 | 130 | 两种方法 131 | 132 | * moduelTracker.set([name, ]value) 同步方法(Sync) 133 | * alog('module.set', [name, ]value) 异步方法(Async) 134 | 135 | ```js 136 | /** 137 | * 设置上报字段值 138 | * @param {string} 'module.set' 139 | * @param {string}[optional] name 字段名 140 | * @param {string|Object} value 字段值 141 | */ 142 | //name, value 143 | alog('pv.set', 'page', 'hunter-index'); 144 | //value object 145 | alog('pv.set', {'page', 'hunter-index'}); 146 | ``` 147 | 148 | #### 5-2-2. Tracker 获取上报字段值 get 149 | 150 | 两种方法 151 | * moduleTracker.get(name, callback) 同步方法(Sync) 152 | * alog('module.get', name, callback) 异步方法(Async) 153 | 154 | ```js 155 | /** 156 | * 获取上报字段值 157 | * @param {string} 'module.get' 158 | * @param {string} name 字段名 159 | * @param {Function} callback 回调函数 160 | */ 161 | alog('pv.get', 'page', function(page){ 162 | alert(page); 163 | }); 164 | ``` 165 | 166 | #### 5-2-3. Tracker 上报数据 send 167 | 168 | 两种方法 169 | 170 | 171 | * moduleTracker.send(dataType, fields) 同步方法(Sync) 172 | * alog('module.send', dataType, fields) 异步方法(Async) 173 | 174 | ```js 175 | /** 176 | * 上报数据 177 | * @param {string} 'module.send' 178 | * @param {string} dataType 数据类型 179 | * @param {Object} fields 上报数据 180 | */ 181 | alog('pv.send', 'pageview', { 182 | 'page': 'hunter-index', 183 | 'title': 'Hunter首页' 184 | }); 185 | ``` 186 | 187 | #### 5-2-4. Tracker 注册事件 on 188 | 189 | 两种方法 190 | 191 | * moduleTracker.on(eventName, dealFunc) 同步方法(Sync) 192 | * alog('module.on', eventName, dealFunc) 异步方法(Async) 193 | 194 | ```js 195 | /** 196 | * 注册事件 197 | * @param {string} 'module.on' 198 | * @param {string} eventName 事件名称 199 | * @param {Function} dealFunc 处理函数 200 | */ 201 | function dealFunc(data){ 202 | if(data.type == "pageview"){ 203 | data.ref = document.referrer; 204 | } 205 | } 206 | alog('pv.on', 'report', dealFunc); 207 | ```` 208 | 209 | #### 5-2-5. Tacker 注销事件 un 210 | 211 | 两种方法 212 | 213 | * moduleTracker.un(eventname, dealFunc) 同步方法(Sync) 214 | * alog('module.un', eventName, dealFunc) 异步方法(Async) 215 | 216 | ```js 217 | /** 218 | * 注销事件 219 | * @param {string} 'module.on' 220 | * @param {string} eventName 事件名称 221 | * @param {Function} dealFunc 处理函数 222 | */ 223 | alog('pv.un', 'report', dealFunc); 224 | ``` 225 | 226 | #### 5-2-6. Tracker 派发事件 fire 227 | 228 | 两种方法 229 | 230 | * moduleTracker.fire(eventname) 同步方法(Sync) 231 | * alog('module.fire', eventName) 异步方法(Async) 232 | 233 | ```js 234 | /** 235 | * 派发事件 236 | * @param {string} 'module.fire' 237 | * @param {string} eventName 事件名称 238 | */ 239 | alog('pv.fire', 'report', {type: "pageview", title: "Hunter 首页"}); 240 | ``` 241 | 242 | ### 5-3. 保留字段 243 | 244 | * 上报地址:postUrl 245 | * 引用模块配置: alias 246 | * 协议字段,用于上报字段简写: protocolParameter 247 | 248 | ### 5-4. Tracker标准事件(自定义方法) 249 | 250 | Tracker标准事件是指可通过 `alog('module.on', '标准事件名', dealFunc)` 的方法自定义处理函数, 251 | 当模块的tracker执行标准事件时,会自动调用自定义方法。 `module.fire('标准事件名', data)` 252 | 253 | * 追踪器创建时触发: start 254 | 255 | ```js 256 | /** 257 | * 可用来设置一些页面统一变量 258 | */ 259 | alog('module.on', 'start', function(){ 260 | moduleTracker.set({ 261 | pageId: 'hunter-index', 262 | title: 'Hunter 首页' 263 | }); 264 | }); 265 | ``` 266 | 267 | * 数据上报时派发: send 268 | 269 | ```js 270 | /** 271 | * 可用来修改ALog默认上报字段 272 | */ 273 | alog('module.on', 'send', function(data){ 274 | data.t = data.title; 275 | delete data.title; 276 | data.r = data.refer; 277 | delete data.refer; 278 | }); 279 | ``` 280 | -------------------------------------------------------------------------------- /doc/alog代码浅析.md: -------------------------------------------------------------------------------- 1 | alog详细解析 2 | ============== 3 | 4 | ## 前言 5 | 6 | 本文主要目的是降低阅读 alog 代码的难度,节省大家更多时间,因为看完这个文档的话对理解 alog 的实现还是有一定帮助的。当然,也欢迎各位大神从零探索,获取其中的乐趣 7 | 8 | ### 3个极为重要的 object: 9 | 10 | 之所以在开头点出,因为只有带着这 3 个 object,才能看懂 alog 代码中的大部分函数 11 | 12 | ```javascript 13 | alog_listeners = { 14 | ... 15 | } 16 | // alog_listeners 一个样例 17 | alog_listeners['eventName'] = [ 18 | callback1, 19 | callback2, 20 | //other callbacks 21 | ] 22 | 23 | 24 | trackers = { 25 | ... 26 | } 27 | // trackers 一个样例 28 | trackers['trackerName'] = { 29 | name: 'trackerName', 30 | fields: { 31 | protocolParameter: { 32 | postUrl: null, 33 | protocolParameter: null 34 | }, 35 | //other properties 36 | }, 37 | argsList: [...], 38 | alog: $, 39 | //other properties 40 | } 41 | 42 | modules = { 43 | ... 44 | } 45 | // modules 一个样例 46 | modules['moduleName'] = { 47 | name: 'moduleName', 48 | requires: [...], 49 | creator: function(){...}, 50 | defining: ture, 51 | defined: ture, 52 | instance: ..., 53 | waiting: {...} 54 | } 55 | ``` 56 | 57 | ### alog 的同步方法与异步方法的区别: 58 | 59 | * 同步方法会在模块加载完,才开始绑定点击事件,所以可能会丢失加载前的点击。同步方法适合用户模块定义内部。 60 | * 异步方法在执行代码后生效,如果使用的模块还没加载完,会先放到事件队列里,等模块加载完成并执行 create 方法后,会自动执行事件队列里的事件。 61 | 62 | ### alog 中 tracker 与 module 的区别与联系 63 | 64 | 虽然 tracker 和 module 看起来很相似,而且在定义 module 和创建 tracker 的时候,使用的都是同样的名称,但是它们还是不同的概念: 65 | 66 | ##### 1. module 不是必需的 67 | 68 | 当在执行的统计非常简单的时候,例如 PV 的统计,则可以完全不需要使用 module 而直接完成对应功能: 69 | 70 | ```javascript 71 | alog('pv.create', { 72 | postUrl: 'http://localhost/v.gif' 73 | }); 74 | alog('pv.send', 'pageview'); 75 | ``` 76 | 77 | 但是当需要进行一些复杂的统计时,例如 speed 统计、 exception 统计,则使用 module 会带来很大的方便。 78 | 79 | 而 tracker 是必需的, alog 通过 tracker 来进行 set 、 get 、 on 、 fire 、 send 等方法。 80 | 81 | ##### 2. 在 module 中调用 tracker 82 | 83 | ```javascript 84 | alog('define', moduleName, function(){...}); 85 | ``` 86 | 87 | function return 的可以是一个 tracker 实例, 而这个实例就会作为 module 的 instance 属性保存下来。而且在 module 中,实例 tracker 执行了各种需要的操作: 88 | 89 | ```javascript 90 | alog('define', trackerName, function(){ 91 | var tracker = alog.tracker(trackerName); 92 | var timestamp = alog.timestamp; // 获取时间戳的函数,相对于alog被声明的时间 93 | tracker.on('record', function(url, time){ 94 | var data = {}; 95 | data[url] = timestamp(time); 96 | tracker.send('timing', data); 97 | }); 98 | tracker.set('protocolParameter', { 99 | // 配置字段,不需要上报 100 | headend: null, 101 | bodyend: null, 102 | domready: null 103 | }); 104 | tracker.create({ 105 | postUrl: 'http://localhost:8080/t.gif' 106 | }); 107 | tracker.send('pageview', { 108 | ht: timestamp(tracker.get('headend')), 109 | lt: timestamp(tracker.get('bodyend')), 110 | drt: timestamp(tracker.get('domready')) 111 | }); 112 | return tracker; 113 | }); 114 | ``` 115 | 116 | ##### 3. 在 tracker 中加载 module 117 | 118 | 在函数 loadModules 中,通过 moduleName 获取对应的文件,这个操作则是保存在一个名为 'default' 的 tracker 的 fields 属性中,即会在 loadModules 操作之前执行对应的 set 操作: 119 | 120 | ```javascript 121 | 122 | var modulesConfig = defaultTracker.get('alias') || {}; 123 | var scriptUrl = modulesConfig[moduleName] || (moduleName + '.js'); 124 | 125 | 126 | alog('set', 'alias', { 127 | speed: 'speed.js' 128 | }); 129 | ``` 130 | 131 | 132 | --- 133 | 134 | ## 1. alog入口调用解析 135 | 136 | 在alog.js代码中,通过 137 | 138 | ```javascript 139 | var objectName = winElement.alogObjectName || 'alog'; 140 | winElement[objectName] = $; 141 | ``` 142 | 143 | 将外部的 alog 调用重定义到 $ 中,也就是实际上调用与 $ 相关的一些方法 144 | 145 | 接下来看看 alog 的一些常见的使用方式: 146 | 147 | ```javascript 148 | alog.method(data); 149 | alog('[trackerName.]method', data); 150 | tracker.method(data); 151 | ``` 152 | 153 | ### 第一种: 154 | 155 | 它会直接调用 alog.js 里对应的 `$.method(data);` 156 | 157 | ### 第二种: 158 | 159 | 在入口函数中 160 | 161 | ```javascript 162 | $ = function(params) { 163 | ... 164 | } 165 | ``` 166 | 167 | 会判断传入的第一个参数 paramas 的内容,并调用对应的方法: 168 | 169 | 根据参数是 'define'、'require'、或者其它参数,来进行不同的处理: 170 | 171 | ```javascript 172 | if (params == 'define' || params == 'require') { 173 | ... 174 | } 175 | 176 | if (typeof params == 'function') { 177 | params($); 178 | return; 179 | } 180 | 181 | String(params).replace(/^(?:([\w$_]+)\.)?(\w+)$/, function(all, trackerName, method) { 182 | args[0] = method; 183 | command.apply($.tracker(trackerName), args); 184 | }); 185 | ``` 186 | 187 | ### 第三种: 188 | 189 | 已经获得了一个 tracker 对象,例如: 190 | 191 | ```javascript 192 | var tracker = alog.tracker(trackerName); 193 | ``` 194 | 195 | 从而调用 alog.js 中 Tracker 的对应方法 196 | 197 | ##2. alog API方法 198 | 199 | ### 2-1 alog.method(data) 200 | 201 | 首先声明这种方式的调用不太多,大多都是调用 alog.tracker(trackerName) 来获得一个 tracker 对象 202 | 203 | 因为在 tracker.method(data) 这种方式的调用中包装了 alog.method(data) 的一些方法 204 | 205 | #### 2-1-1 alog.tracker(trackerName) 206 | 207 | 该方法调用 alog.js 中的 getTracker 方法: 208 | 209 | ```javascript 210 | $.tracker = getTracker; 211 | ``` 212 | 213 | 然后根 据trackerName 的不同做出不同的处理: 214 | 215 | ##### 若trackerName为空 216 | 217 | 从当前 trackers 对象中,返回 trackers['default'] 218 | 若 trackers['default'] 也为空,则新建一个 name='default' 的 tracker 对象并返回 219 | 220 | ##### 若trackerName = '*' 221 | 222 | 将 trackers 对象中的所有 tracker 对象 push 到一个数组中并返回 223 | 224 | #####其它情况: 225 | 226 | 从当前 trackers 对象中,返回 trackers[trackerName] 227 | 若 trackers[trackerName] 也为空,则新建一个 name=trackerName 的 tracker 对象并返回 228 | 229 | #### 2-1-2 alog.timestamp() 230 | 231 | 获取当前时间戳 232 | 233 | #### 2-1-3 alog.on(element, eventName, callback) 234 | 235 | ##### 若element是字符串类型: 236 | 237 | 获取alog_listeners对象中key为element的数组(若未定义则初始化为[]) 238 | 239 | 然后向alog_listeners[eventName]这个数组的最前面插入eventName 240 | 241 | ##### 其它: 242 | 243 | 对element这个元素监听事件eventName,回调函数为callback: 244 | 245 | ```javascript 246 | if (element.addEventListener) { 247 | element.addEventListener(eventName, callback, false); 248 | } else if (element.attachEvent) { 249 | element.attachEvent('on' + eventName, callback); 250 | } 251 | ``` 252 | 253 | #### 2-1-4 alog.un(element, eventName, callback) 254 | 255 | 与on相反 256 | 257 | #### 2-1-5 alog.fire(eventName) 258 | 259 | 首先尝试获取alog_listeners[eventName]这个数组 260 | 然后获取其它参数: 261 | 262 | ```javascript 263 | var args = arguments; 264 | for(var i = 1; i < args.length; i++){ 265 | items.push(args[i]); 266 | } 267 | ``` 268 | 269 | 对于alog_listeners[eventName]这个数组的每个元素执行apply方法: 270 | 271 | ```javascript 272 | listener = alog_listeners[eventName] 273 | var i = listener.length; 274 | while (i--){ 275 | if (listener[i].apply(this, items)){ 276 | result++; 277 | } 278 | } 279 | ``` 280 | 281 | ### 2-2 tracker.method(data) 282 | 283 | 这种则是在代码中用的比较普遍的一种调用方式 284 | 285 | #### 2-2-1 tracker.set(name, value) 286 | 287 | 若name的类型是string,则将tracker对象的fields属性的name属性,赋值为value: 288 | 289 | ```javascript 290 | this.fields[name] = value; 291 | ``` 292 | 293 | 若name=='protocolParameter',则value还要增添一些默认属性 294 | 295 | 若name的类型为object,则对name里的每一对key-value: 296 | 调用Tracker.prototype.set(key, value) 297 | 298 | #### 2-2-2 tracker.get(name, callback) 299 | 300 | 获取该tracker的fields属性的name属性的值 301 | 302 | 若存在callback是函数类型的话,将上面的值传给这个callback函数并执行 303 | 304 | #### 2-2-3 tracker.fire(eventName) 305 | 306 | 将该 tracker 的 name 值与 eventName 连接: 307 | 308 | ```javascript 309 | var items = [this.name + '.' + eventName]; 310 | ``` 311 | 312 | 再获取其它参数,并调用alog.fire方法: 313 | 314 | ```javascript 315 | fire.apply(this, items) 316 | ``` 317 | 318 | #### 2-2-4 tracker.on(eventName, callback) 319 | 320 | ```javascript 321 | $.on(this.name + '.' + eventName, callback); 322 | ``` 323 | 324 | 可参考 alog.on 325 | 326 | #### 2-2-5 tracker.un(eventName, callback) 327 | 328 | ```javascript 329 | $.un(this.name + '.' + eventName, callback); 330 | ``` 331 | 332 | 可参考alog.un 333 | 334 | #### 2-2-6 tracker.create(fields) 335 | 336 | 首先如果 fields 是一个 object 类型的话: 337 | 338 | ```javascript 339 | this.set(fields); 340 | ``` 341 | 342 | 可参考 tracker.set(name, value) 343 | 344 | 设置 created 标记: 345 | 346 | ```javascript 347 | this.created = new Date; 348 | ``` 349 | 350 | 调用 fire 方法 351 | 352 | 即获取 alog_listeners[this.name + '.create'] 中的每一个方法 353 | 354 | 把 tracker 自身作为参数传给这些方法,并运行该方法: 355 | 356 | ```javascript 357 | this.fire('create', this); 358 | ``` 359 | 360 | 最后依次用 shift 方法弹出 tracker 的 argsList 数组的每一个元素,执行如下方法: 361 | 362 | ```javascript 363 | while(args = this.argsList.shift()) { 364 | command.apply(this, args); 365 | } 366 | ``` 367 | 368 | tracker 的 argsList 数组中保存的是在 tracker 实例创建之前,此 tracker 就已经开始调用了 send 或者 fire 方法,此时就把该方法所需的全部参数当成一个数组,保存进 argsList 数组中。直到 create 方法调用之后,tracker 实例被创建,才依次将这些参数传给对应的方法并执行。 369 | 370 | 这一块的代码建议配合实例和对应的数组来看,否则屡清楚比较难... 371 | 372 | #### 2-2-7 tracker.send(hitType, fieldObject) 373 | 374 | 将 tracker 的 fields 属性和一些数据合,以及 fieldObject 合并为 data 375 | 调用 `this.fire('send', data);` 376 | 并将数据的字段名简写之后,以创建一个1像素的图像的方式,将 tracker.fields.postUrl 保存在该图片的 src,以此完成数据的发送 377 | 378 | ### 2-3 alog('[trackerName.]method', data) 379 | 380 | 这种情况,与 `tracker.method(data)` 类似,只不过 tracker 的方式是同步执行的,而这种方式是异步执行,关于这两种方式的区别,在 API 文档中有详细说明: 381 | 382 | 在此只讲2个特殊又重要的方法:define 和 require 也建议大家好好分析这两个方法 383 | 384 | #### 2-3-1 alog('define', trackerName, function(){...}) 385 | 386 | 这个方法从$入口函数开始获取传入参数的 trackerName 和 function: 387 | 388 | ```javascript 389 | moduleName = args[i]; 390 | creator = args[i]; 391 | ``` 392 | 393 | 如果以 trackerName 为名的 module 不存在, 394 | 则以 trackerName 在 modules 数组中创建一个 module,赋予初始值,并执行 clearDepend(module) 这个方法。 395 | 396 | clearDepend 方法的作用主要是获取自身 module 所依赖的各 个module2,即 require 属性 397 | 398 | 如果这个 require 的 module2 存在,则将所依赖的 module2 的实例对象即 instance,作为参数,传到 module 自身的的 creator 方法中并运行,creator 即在 define 这个 module 的时候对应的那个 function。 399 | 400 | 然后执行 clearWaiting(module),因为在这个 module 被 define 之前,可能已经存在某个 module2 来 require 过这个 module,所以现在就相当于通知那个 module2,本 module 已经定义,可以重新执行 module2 的 clearDepend 方法 401 | 402 | 如果 require 的 module2 不存在,则加载这个 module2: 403 | 404 | ```javascript 405 | if (!depend.defining) { 406 | loadModules(moduleName); 407 | } 408 | ``` 409 | 410 | 并在 module2 的等待数组 waiting 中加入 module,表示 module 等待着 module2 的 define 方法的执行 411 | 412 | ```javascript 413 | depend.waiting = depend.waiting || {}; 414 | depend.waiting[module.name] = module; 415 | ``` 416 | 417 | #### 2-3-2 alog('require', moduleName) 418 | 419 | 首先依旧是先获取参数 moduleName: 420 | 421 | ```javascript 422 | case 'string': 423 | moduleName = args[i]; 424 | break; 425 | ``` 426 | 427 | 但是这里会新建一个临时的模块 newModule 名为 '#guid',guid 为一个自增的数字变量 428 | 429 | newModule 的 require 属性对应的则是 module 430 | 431 | ```javascript 432 | if (params == 'require') { 433 | if (moduleName && !requires) requires = [moduleName]; 434 | moduleName = null; 435 | } 436 | 437 | newModule.requires = requires; 438 | ``` 439 | 440 | 然后执行对 newModule 的 clearDepend() 方法,处理其依赖关系 441 | 442 | ## 3. ALog应用示例 443 | 444 | * 简单统计: pv统计,参看[pv统计](https://github.com/uxrp/alog/tree/master/examples/pv) 445 | * 复杂统计: 自定义模块统计,参看[speed统计](https://github.com/uxrp/alog/tree/master/examples/speed) 446 | * 代理统计: 接入第三方统计,参看[百度统计](https://github.com/uxrp/alog/tree/master/examples/tongji) 447 | 448 | -------------------------------------------------------------------------------- /alog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * alog 3 | * @version 1.0 4 | * @copyright www.baidu.com 5 | * 6 | * @file 前端统计框架,支持并行多个统计模块 7 | * @author 王集鹄(WangJihu,http://weibo.com/zswang) 8 | * 张军(ZhangJun08,http://weibo.com/zhangjunah) 9 | * 梁东杰(LiangDongjie,http://weibo.com/nedj) 10 | */ 11 | (function (win, doc) { 12 | // 压缩代码相关 13 | /* compressor */ 14 | var objectName = win.alogObjectName || 'alog'; 15 | var oldObject = win[objectName]; 16 | if (oldObject && oldObject.defined) { // 避免重复加载 17 | return; 18 | } 19 | /**/ 20 | var ie = win.attachEvent && !window.opera; 21 | /** 22 | * 是否IE 23 | */ 24 | /** 25 | * 点击javascript链接的时间 26 | */ 27 | var clickJsLinkTime; 28 | /**/ 29 | /** 30 | * 起始时间 31 | */ 32 | var startTime = (oldObject && oldObject.l) || (+new Date()); 33 | /** 34 | * session id 优先从服务端获取 35 | */ 36 | var sid = win.logId || 37 | ((+new Date()).toString(36) + Math.random().toString(36).substr(2, 3)); 38 | /** 39 | * id编码 40 | */ 41 | var guid = 0; 42 | /** 43 | * 正在加载的脚本 44 | */ 45 | var loadScripts = {}; 46 | /** 47 | * 模块列表 48 | */ 49 | var modules = { 50 | alog: { 51 | /** 52 | * 模块名 53 | */ 54 | name: 'alog', 55 | /** 56 | * 是否声明 57 | */ 58 | defined: true, 59 | /** 60 | * 模块实例 61 | */ 62 | instance: entry 63 | } 64 | }; 65 | /** 66 | * 处理入口 67 | * 68 | * @param {Object} params 配置项 69 | */ 70 | function entry(params) { 71 | var args = arguments; 72 | var moduleName; 73 | var requires; 74 | var creator; 75 | if (params === 'define' || params === 'require') { 76 | // 校正参数调用 77 | for (var i = 1; i < args.length; i++) { 78 | switch (typeof args[i]) { 79 | case 'string': 80 | moduleName = args[i]; 81 | break; 82 | case 'object': 83 | requires = args[i]; 84 | break; 85 | case 'function': 86 | creator = args[i]; 87 | break; 88 | } 89 | } 90 | if (params === 'require') { 91 | if (moduleName && !requires) { 92 | requires = [moduleName]; 93 | } 94 | moduleName = null; 95 | } 96 | // 如果是引用,这产生临时模块名 97 | moduleName = !moduleName ? '#' + (guid++) : moduleName; 98 | var module; 99 | if (modules[moduleName]) { 100 | module = modules[moduleName]; 101 | } 102 | else { 103 | module = {}; 104 | modules[moduleName] = module; 105 | } 106 | // 避免模块重复定义 107 | if (!module.defined) { 108 | module.name = moduleName; 109 | module.requires = requires; 110 | module.creator = creator; 111 | if (params === 'define') { 112 | module.defining = true; 113 | } 114 | clearDeps(module); 115 | } 116 | return; 117 | } 118 | if (typeof params === 'function') { 119 | params(entry); 120 | return; 121 | } 122 | /** 123 | * @example 124 | ```js 125 | alog('hunter.send', 'pageview'); 126 | alog('monkey.send', 'pageview'); 127 | alog('send', 'pageview'); // alog('default.send', 'pageview'); 128 | ``` 129 | */ 130 | // 'hunter.send' -> [1]=>'hunter', [2]=>'send' 131 | String(params).replace(/^(?:([\w$_]+)\.)?(\w+)$/, 132 | function (all, trackerName, method) { 133 | args[0] = method; // 'hunter.send' -> 'send' 134 | command.apply(entry.tracker(trackerName), args); 135 | } 136 | ); 137 | } 138 | /** 139 | * 监听列表 140 | */ 141 | var alogListeners = {}; 142 | /** 143 | * 追踪器字典 144 | */ 145 | var trackers = {}; 146 | /** 147 | * 页面关闭中 148 | */ 149 | var closing; 150 | /** 151 | * 默认追踪器 152 | */ 153 | var defaultTracker; 154 | /** 155 | * 加载模块 156 | * 157 | * @param {string} moduleName 模块名 158 | */ 159 | function loadModules(moduleName) { 160 | var modulesConfig = defaultTracker.get('alias') || {}; 161 | var scriptUrl = modulesConfig[moduleName] || (moduleName + '.js'); 162 | if (loadScripts[scriptUrl]) { 163 | return; 164 | } 165 | loadScripts[scriptUrl] = true; 166 | var scriptTag = 'script'; 167 | var scriptElement = doc.createElement(scriptTag); 168 | var lastElement = doc.getElementsByTagName(scriptTag)[0]; 169 | scriptElement.async = !0; 170 | scriptElement.src = scriptUrl; 171 | lastElement.parentNode.insertBefore(scriptElement, lastElement); 172 | } 173 | /** 174 | * 处理依赖关系 175 | * 176 | * @param {module} module 模块 177 | */ 178 | function clearDeps(module) { 179 | if (module.defined) { 180 | return; 181 | } 182 | var defined = true; 183 | var params = []; 184 | var requires = module.requires; 185 | if (requires) { 186 | for (var i = 0; i < requires.length; i++) { 187 | var moduleName = requires[i]; 188 | var deps = modules[moduleName] = (modules[moduleName] || {}); 189 | if (deps.defined || deps === module) { 190 | params.push(deps.instance); 191 | } 192 | else { 193 | defined = false; 194 | if (!deps.defining) { // 已经存在定义 195 | loadModules(moduleName); 196 | } 197 | deps.waiting = deps.waiting || {}; 198 | deps.waiting[module.name] = module; 199 | } 200 | } 201 | } 202 | if (defined) { 203 | module.defined = true; 204 | if (module.creator) { 205 | module.instance = module.creator.apply(module, params); 206 | } 207 | clearWaiting(module); 208 | } 209 | } 210 | /** 211 | * 清理等待依赖项加载的模块 212 | * 213 | * @param {Module} module 模块对象 214 | */ 215 | function clearWaiting(module) { 216 | for (var moduleName in module.waiting) { 217 | if (module.waiting.hasOwnProperty(moduleName)) { 218 | clearDeps(module.waiting[moduleName]); 219 | } 220 | } 221 | } 222 | /** 223 | * 获取时间戳 224 | * 225 | * @param {Date} now 当前时间 226 | * @return {number} 返回时间戳 227 | */ 228 | function timestamp(now) { 229 | return (now || new Date()) - startTime; 230 | } 231 | /** 232 | * 绑定事件 233 | * 234 | * @param {HTMLElement=} element 页面元素,没有指定则为 alog 对象 235 | * @param {string} eventName 事件名 236 | * @param {Function} callback 回调函数 237 | * @example 238 | ```js 239 | alog.on('report', function (data) { data.tt = +new Date; }); 240 | ``` 241 | */ 242 | function on(element, eventName, callback) { 243 | if (!element) { 244 | return; 245 | } 246 | if (typeof element === 'string') { 247 | callback = eventName; 248 | eventName = element; 249 | element = entry; 250 | } 251 | try { 252 | if (element === entry) { 253 | alogListeners[eventName] = alogListeners[eventName] || []; 254 | alogListeners[eventName].unshift(callback); 255 | return; 256 | } 257 | if (element.addEventListener) { 258 | element.addEventListener(eventName, callback, false); 259 | } 260 | else if (element.attachEvent) { 261 | element.attachEvent('on' + eventName, callback); 262 | } 263 | } 264 | catch (ex) {} 265 | } 266 | /** 267 | * 注销事件绑定 268 | * 269 | * @param {HTMLElement} element 页面元素 270 | * @param {string} eventName 事件名 271 | * @param {Function} callback 回调函数 272 | */ 273 | function un(element, eventName, callback) { 274 | if (!element) { 275 | return; 276 | } 277 | if (typeof element === 'string') { 278 | callback = eventName; 279 | eventName = element; 280 | element = entry; 281 | } 282 | try { 283 | if (element === entry) { 284 | var listener = alogListeners[eventName]; 285 | if (!listener) { 286 | return; 287 | } 288 | var i = listener.length; 289 | while (i--) { 290 | if (listener[i] === callback) { 291 | listener.splice(i, 1); 292 | } 293 | } 294 | return; 295 | } 296 | if (element.removeEventListener) { 297 | element.removeEventListener(eventName, callback, false); 298 | } 299 | else { 300 | element.detachEvent && element.detachEvent('on' + eventName, callback); 301 | } 302 | } 303 | catch (ex) {} 304 | } 305 | /** 306 | * 触发事件 307 | * 308 | * @param {string} eventName 事件名 "error"、"close" 309 | * @return {Object} 返回当前实例 310 | * @example 311 | ```js 312 | alog.fire('click', { element: document.getElementById('save') }); 313 | ``` 314 | */ 315 | function fire(eventName) { 316 | var listener = alogListeners[eventName]; 317 | if (!listener) { 318 | return; 319 | } 320 | var items = []; 321 | var args = arguments; 322 | for (var i = 1, len = args.length; i < len; i++) { 323 | items.push(args[i]); 324 | } 325 | var result = 0; 326 | var j = listener.length; 327 | while (j--) { 328 | if (listener[j].apply(this, items)) { 329 | result++; 330 | } 331 | } 332 | return result; 333 | } 334 | /** 335 | * 上报数据 336 | * 337 | * @param {string} url 目标链接 338 | * @param {Object} data 上报数据 339 | */ 340 | function report(url, data) { 341 | if (!url || !data) { 342 | return; 343 | } 344 | // @see http://jsperf.com/new-image-vs-createelement-img 345 | var image = doc.createElement('img'); 346 | var items = []; 347 | for (var key in data) { 348 | if (data[key]) { 349 | items.push(key + '=' + encodeURIComponent(data[key])); 350 | } 351 | } 352 | var name = 'img_' + (+new Date()); 353 | entry[name] = image; 354 | image.onload = image.onerror = function () { 355 | entry[name] = 356 | image = 357 | image.onload = 358 | image.onerror = null; 359 | delete entry[name]; 360 | }; 361 | image.src = url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&'); 362 | } 363 | /** 364 | * 字段名使用简写 365 | * 366 | * @param {Object} protocolParameter 字段名对照表,如果为null表示不上报 367 | * @param {Object} data 待处理的数据 368 | * @return {Object} 返回处理后的数据 369 | */ 370 | function runProtocolParameter(protocolParameter, data) { 371 | if (!protocolParameter) { 372 | return data; 373 | } 374 | var result = {}; 375 | for (var p in data) { 376 | if (protocolParameter[p] !== null) { 377 | result[protocolParameter[p] || p] = data[p]; 378 | } 379 | } 380 | return result; 381 | } 382 | /** 383 | * 执行命令 384 | */ 385 | function command() { 386 | var args = arguments; 387 | var method = args[0]; 388 | if (this.created || /^(on|un|set|get|create)$/.test(method)) { 389 | var methodFunc = Tracker.prototype[method]; 390 | var params = []; 391 | for (var i = 1, len = args.length; i < len; i++) { 392 | params.push(args[i]); 393 | } 394 | if (typeof methodFunc === 'function') { 395 | methodFunc.apply(this, params); 396 | } 397 | } 398 | else { // send|fire // 实例创建以后才能调用的方法 399 | this.argsList.push(args); 400 | } 401 | } 402 | /** 403 | * 合并两个对象 404 | * 405 | * @param {Object} a 对象1 406 | * @param {Object} b 对象2 407 | * @return {Object} 返回合并后的对象 408 | */ 409 | function merge(a, b) { 410 | var result = {}; 411 | for (var p in a) { 412 | if (a.hasOwnProperty(p)) { 413 | result[p] = a[p]; 414 | } 415 | } 416 | for (var q in b) { 417 | if (b.hasOwnProperty(q)) { 418 | result[q] = b[q]; 419 | } 420 | } 421 | return result; 422 | } 423 | /** 424 | * 追踪器构造器 425 | * 426 | * @param {string} name 追踪器名称 427 | */ 428 | function Tracker(name) { 429 | this.name = name; 430 | this.fields = { 431 | protocolParameter: { 432 | postUrl: null, 433 | protocolParameter: null 434 | } 435 | }; 436 | this.argsList = []; 437 | this.alog = entry; 438 | } 439 | /** 440 | * 获取追踪器 441 | * 442 | * @param {string} trackerName 追踪器名称,如果为 '*' 则获取全部追踪器 443 | * @return {Object|Array} 返回追踪器对象 444 | */ 445 | function getTracker(trackerName) { 446 | trackerName = trackerName || 'default'; 447 | if (trackerName === '*') { 448 | var result = []; 449 | for (var p in trackers) { 450 | if (trackers.hasOwnProperty(p)) { 451 | result.push(trackers[p]); 452 | } 453 | } 454 | return result; 455 | } 456 | return (trackers[trackerName] = 457 | trackers[trackerName] || new Tracker(trackerName)); 458 | } 459 | /** 460 | * 创建追踪器 461 | * 462 | * @param {Object} fields 字段列表 463 | */ 464 | Tracker.prototype.create = function (fields) { 465 | if (this.created) { 466 | return; 467 | } 468 | if (typeof fields === 'object') { 469 | this.set(fields); 470 | } 471 | this.created = new Date(); 472 | this.fire('create', this); 473 | var args; 474 | while (args = this.argsList.shift()) { 475 | command.apply(this, args); 476 | } 477 | }; 478 | /** 479 | * 发送日志数据 480 | * 481 | * @param {string} hitType 数据类型 482 | * @param {Object} fieldObject 发送数据 483 | */ 484 | Tracker.prototype.send = function (hitType, fieldObject) { 485 | var data = merge({ 486 | ts: timestamp().toString(36), 487 | t: hitType, 488 | sid: sid 489 | }, this.fields); 490 | if (typeof fieldObject === 'object') { 491 | data = merge(data, fieldObject); 492 | } 493 | else { 494 | var args = arguments; 495 | switch (hitType) { 496 | case 'pageview': 497 | // [page[, title]] 498 | if (args[1]) { 499 | data.page = args[1]; 500 | } 501 | if (args[2]) { 502 | data.title = args[2]; 503 | } 504 | break; 505 | case 'event': 506 | // eventCategory, eventAction[, eventLabel[, eventValue]] 507 | if (args[1]) { 508 | data.eventCategory = args[1]; 509 | } 510 | if (args[2]) { 511 | data.eventAction = args[2]; 512 | } 513 | if (args[3]) { 514 | data.eventLabel = args[3]; 515 | } 516 | if (args[4]) { 517 | data.eventValue = args[4]; 518 | } 519 | break; 520 | case 'timing': 521 | // timingCategory, timingVar, timingValue[, timingLabel] 522 | if (args[1]) { 523 | data.timingCategory = args[1]; 524 | } 525 | if (args[2]) { 526 | data.timingVar = args[2]; 527 | } 528 | if (args[3]) { 529 | data.timingValue = args[3]; 530 | } 531 | if (args[4]) { 532 | data.timingLabel = args[4]; 533 | } 534 | break; 535 | case 'exception': 536 | // exDescription[, exFatal] 537 | if (args[1]) { 538 | data.exDescription = args[1]; 539 | } 540 | if (args[2]) { 541 | data.exFatal = args[2]; 542 | } 543 | break; 544 | default: 545 | return; 546 | } 547 | } 548 | this.fire('send', data); 549 | report(this.fields.postUrl, runProtocolParameter(this.fields.protocolParameter, data)); 550 | }; 551 | /** 552 | * 设置字段值 553 | * 554 | * @param {string} name 字段名 555 | * @param {Any} value 字段值 556 | */ 557 | Tracker.prototype.set = function (name, value) { 558 | if (typeof name === 'string') { 559 | if (name === 'protocolParameter') { 560 | value = merge({ 561 | postUrl: null, 562 | protocolParameter: null 563 | }, value); 564 | } 565 | this.fields[name] = value; 566 | } 567 | else if (typeof name === 'object') { 568 | for (var p in name) { 569 | if (name.hasOwnProperty(p)) { 570 | this.set(p, name[p]); 571 | } 572 | } 573 | } 574 | }; 575 | /** 576 | * 获取字段值 577 | * 578 | * @param {string} name 字段名 579 | * @param {Function} callback 回调函数 580 | * @return {Object} 返回当前实例 581 | */ 582 | Tracker.prototype.get = function (name, callback) { 583 | var result = this.fields[name]; 584 | if (typeof callback === 'function') { 585 | callback(result); 586 | } 587 | return result; 588 | }; 589 | /** 590 | * 触发事件 591 | * 592 | * @param {string} eventName 事件名 593 | * @return {Object} 返回当前实例 594 | */ 595 | Tracker.prototype.fire = function (eventName) { 596 | var items = [this.name + '.' + eventName]; 597 | var args = arguments; 598 | for (var i = 1, len = args.length; i < len; i++) { 599 | items.push(args[i]); 600 | } 601 | return fire.apply(this, items); 602 | }; 603 | /** 604 | * 绑定事件 605 | * 606 | * @param {string} eventName 事件名 607 | * @param {Function} callback 回调函数 608 | */ 609 | Tracker.prototype.on = function (eventName, callback) { 610 | entry.on(this.name + '.' + eventName, callback); 611 | }; 612 | /** 613 | * 注销事件 614 | * 615 | * @param {string} eventName 事件名 616 | * @param {Function} callback 回调函数 617 | */ 618 | Tracker.prototype.un = function (eventName, callback) { 619 | entry.un(this.name + '.' + eventName, callback); 620 | }; 621 | entry.name = 'alog'; 622 | entry.sid = sid; 623 | entry.defined = true; 624 | entry.timestamp = timestamp; 625 | entry.un = un; 626 | entry.on = on; 627 | entry.fire = fire; 628 | entry.tracker = getTracker; 629 | entry('init'); 630 | defaultTracker = getTracker(); 631 | defaultTracker.set('protocolParameter', { 632 | modules: null 633 | }); 634 | if (oldObject) { 635 | // 处理临时alog对象 636 | var items = [].concat(oldObject.p || [], oldObject.q || []); 637 | oldObject.p = oldObject.q = null; // 清理内存 638 | for (var p in entry) { 639 | if (entry.hasOwnProperty(p)) { 640 | oldObject[p] = entry[p]; 641 | } 642 | } 643 | entry.p = entry.q = { // 接管之前的定义 644 | push: function (args) { 645 | entry.apply(entry, args); 646 | } 647 | }; 648 | // 开始处理缓存命令 649 | for (var i = 0; i < items.length; i++) { 650 | entry.apply(entry, items[i]); 651 | } 652 | } 653 | win[objectName] = entry; 654 | /**/ 655 | if (ie) { 656 | on(doc, 'mouseup', function (e) { 657 | var target = e.target || e.srcElement; 658 | if (target.nodeType === 1 && /^ajavascript:/i.test(target.tagName + target.href)) { 659 | clickJsLinkTime = new Date(); 660 | } 661 | }); 662 | } 663 | /**/ 664 | /** 665 | * 页面关闭时处理方法 666 | */ 667 | function unloadHandler() { 668 | /**/ 669 | // @see http://msdn.microsoft.com/en-us/library/ms536907(VS.85).aspx 670 | // Click an anchor that refers to another document. 671 | // 修复 IE 中点击 `...` 也会触发 beforeunload 事件的问题 672 | if (ie && (new Date() - clickJsLinkTime < 50)) { 673 | return; 674 | } 675 | /**/ 676 | if (closing) { 677 | return; 678 | } 679 | closing = true; 680 | var sleepCount = 0; 681 | for (var p in trackers) { 682 | if (trackers.hasOwnProperty(p)) { 683 | var tracker = trackers[p]; 684 | if (tracker.created) { 685 | sleepCount += tracker.fire('unload'); 686 | } 687 | } 688 | } 689 | if (sleepCount) { // isSleep 690 | var isSleep = new Date(); 691 | while ((new Date() - isSleep) < 100) { 692 | /* isSleep */ 693 | } 694 | } 695 | } 696 | on(win, 'beforeunload', unloadHandler); 697 | on(win, 'unload', unloadHandler); 698 | })(window, document); 699 | -------------------------------------------------------------------------------- /src/alog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * alog 3 | * @version 1.0 4 | * @copyright www.baidu.com 5 | * 6 | * @file 前端统计框架,支持并行多个统计模块 7 | * @author 王集鹄(WangJihu,http://weibo.com/zswang) 8 | * 张军(ZhangJun08,http://weibo.com/zhangjunah) 9 | * 梁东杰(LiangDongjie,http://weibo.com/nedj) 10 | */ 11 | 12 | (function (win, doc) { 13 | 14 | // 压缩代码相关 15 | /* compressor */ 16 | 17 | var objectName = win.alogObjectName || 'alog'; 18 | var oldObject = win[objectName]; 19 | if (oldObject && oldObject.defined) { // 避免重复加载 20 | return; 21 | } 22 | 23 | /**/ 24 | var ie = win.attachEvent && !window.opera; 25 | /** 26 | * 是否IE 27 | */ 28 | 29 | /** 30 | * 点击javascript链接的时间 31 | */ 32 | var clickJsLinkTime; 33 | /**/ 34 | 35 | /** 36 | * 起始时间 37 | */ 38 | var startTime = (oldObject && oldObject.l) || (+new Date()); 39 | 40 | /** 41 | * session id 优先从服务端获取 42 | */ 43 | var sid = win.logId || 44 | ((+new Date()).toString(36) + Math.random().toString(36).substr(2, 3)); 45 | 46 | /** 47 | * id编码 48 | */ 49 | var guid = 0; 50 | 51 | /** 52 | * 正在加载的脚本 53 | */ 54 | var loadScripts = {}; 55 | 56 | /** 57 | * 模块列表 58 | */ 59 | var modules = { 60 | alog: { 61 | /** 62 | * 模块名 63 | */ 64 | name: 'alog', 65 | /** 66 | * 是否声明 67 | */ 68 | defined: true, 69 | /** 70 | * 模块实例 71 | */ 72 | instance: entry 73 | } 74 | }; 75 | 76 | /** 77 | * 处理入口 78 | * 79 | * @param {Object} params 配置项 80 | */ 81 | function entry(params) { 82 | var args = arguments; 83 | var moduleName; 84 | var requires; 85 | var creator; 86 | 87 | if (params === 'define' || params === 'require') { 88 | // 校正参数调用 89 | for (var i = 1; i < args.length; i++) { 90 | switch (typeof args[i]) { 91 | case 'string': 92 | moduleName = args[i]; 93 | break; 94 | case 'object': 95 | requires = args[i]; 96 | break; 97 | case 'function': 98 | creator = args[i]; 99 | break; 100 | } 101 | } 102 | 103 | if (params === 'require') { 104 | if (moduleName && !requires) { 105 | requires = [moduleName]; 106 | } 107 | moduleName = null; 108 | } 109 | 110 | // 如果是引用,这产生临时模块名 111 | moduleName = !moduleName ? '#' + (guid++) : moduleName; 112 | var module; 113 | if (modules[moduleName]) { 114 | module = modules[moduleName]; 115 | } 116 | else { 117 | module = {}; 118 | modules[moduleName] = module; 119 | } 120 | 121 | // 避免模块重复定义 122 | if (!module.defined) { 123 | module.name = moduleName; 124 | module.requires = requires; 125 | module.creator = creator; 126 | if (params === 'define') { 127 | module.defining = true; 128 | } 129 | clearDeps(module); 130 | } 131 | return; 132 | } 133 | 134 | if (typeof params === 'function') { 135 | params(entry); 136 | return; 137 | } 138 | 139 | /** 140 | * @example 141 | ```js 142 | alog('hunter.send', 'pageview'); 143 | alog('monkey.send', 'pageview'); 144 | alog('send', 'pageview'); // alog('default.send', 'pageview'); 145 | ``` 146 | */ 147 | 148 | // 'hunter.send' -> [1]=>'hunter', [2]=>'send' 149 | String(params).replace(/^(?:([\w$_]+)\.)?(\w+)$/, 150 | function (all, trackerName, method) { 151 | args[0] = method; // 'hunter.send' -> 'send' 152 | command.apply(entry.tracker(trackerName), args); 153 | } 154 | ); 155 | } 156 | 157 | /** 158 | * 监听列表 159 | */ 160 | var alogListeners = {}; 161 | /** 162 | * 追踪器字典 163 | */ 164 | var trackers = {}; 165 | 166 | /** 167 | * 页面关闭中 168 | */ 169 | var closing; 170 | 171 | /** 172 | * 默认追踪器 173 | */ 174 | var defaultTracker; 175 | 176 | /** 177 | * 加载模块 178 | * 179 | * @param {string} moduleName 模块名 180 | */ 181 | function loadModules(moduleName) { 182 | var modulesConfig = defaultTracker.get('alias') || {}; 183 | var scriptUrl = modulesConfig[moduleName] || (moduleName + '.js'); 184 | if (loadScripts[scriptUrl]) { 185 | return; 186 | } 187 | loadScripts[scriptUrl] = true; 188 | var scriptTag = 'script'; 189 | var scriptElement = doc.createElement(scriptTag); 190 | var lastElement = doc.getElementsByTagName(scriptTag)[0]; 191 | scriptElement.async = !0; 192 | scriptElement.src = scriptUrl; 193 | lastElement.parentNode.insertBefore(scriptElement, lastElement); 194 | } 195 | 196 | /** 197 | * 处理依赖关系 198 | * 199 | * @param {module} module 模块 200 | */ 201 | function clearDeps(module) { 202 | if (module.defined) { 203 | return; 204 | } 205 | 206 | var defined = true; 207 | var params = []; 208 | var requires = module.requires; 209 | if (requires) { 210 | for (var i = 0; i < requires.length; i++) { 211 | var moduleName = requires[i]; 212 | var deps = modules[moduleName] = (modules[moduleName] || {}); 213 | if (deps.defined || deps === module) { 214 | params.push(deps.instance); 215 | } 216 | else { 217 | defined = false; 218 | if (!deps.defining) { // 已经存在定义 219 | loadModules(moduleName); 220 | } 221 | deps.waiting = deps.waiting || {}; 222 | deps.waiting[module.name] = module; 223 | } 224 | } 225 | } 226 | if (defined) { 227 | module.defined = true; 228 | if (module.creator) { 229 | module.instance = module.creator.apply(module, params); 230 | } 231 | clearWaiting(module); 232 | } 233 | } 234 | 235 | /** 236 | * 清理等待依赖项加载的模块 237 | * 238 | * @param {Module} module 模块对象 239 | */ 240 | function clearWaiting(module) { 241 | for (var moduleName in module.waiting) { 242 | if (module.waiting.hasOwnProperty(moduleName)) { 243 | clearDeps(module.waiting[moduleName]); 244 | } 245 | } 246 | } 247 | 248 | /** 249 | * 获取时间戳 250 | * 251 | * @param {Date} now 当前时间 252 | * @return {number} 返回时间戳 253 | */ 254 | function timestamp(now) { 255 | return (now || new Date()) - startTime; 256 | } 257 | 258 | /** 259 | * 绑定事件 260 | * 261 | * @param {HTMLElement=} element 页面元素,没有指定则为 alog 对象 262 | * @param {string} eventName 事件名 263 | * @param {Function} callback 回调函数 264 | * @example 265 | ```js 266 | alog.on('report', function (data) { data.tt = +new Date; }); 267 | ``` 268 | */ 269 | function on(element, eventName, callback) { 270 | if (!element) { 271 | return; 272 | } 273 | 274 | if (typeof element === 'string') { 275 | callback = eventName; 276 | eventName = element; 277 | element = entry; 278 | } 279 | 280 | try { 281 | if (element === entry) { 282 | alogListeners[eventName] = alogListeners[eventName] || []; 283 | alogListeners[eventName].unshift(callback); 284 | return; 285 | } 286 | if (element.addEventListener) { 287 | element.addEventListener(eventName, callback, false); 288 | } 289 | else if (element.attachEvent) { 290 | element.attachEvent('on' + eventName, callback); 291 | } 292 | } 293 | catch (ex) {} 294 | } 295 | 296 | /** 297 | * 注销事件绑定 298 | * 299 | * @param {HTMLElement} element 页面元素 300 | * @param {string} eventName 事件名 301 | * @param {Function} callback 回调函数 302 | */ 303 | function un(element, eventName, callback) { 304 | if (!element) { 305 | return; 306 | } 307 | 308 | if (typeof element === 'string') { 309 | callback = eventName; 310 | eventName = element; 311 | element = entry; 312 | } 313 | 314 | try { 315 | if (element === entry) { 316 | var listener = alogListeners[eventName]; 317 | if (!listener) { 318 | return; 319 | } 320 | var i = listener.length; 321 | while (i--) { 322 | if (listener[i] === callback) { 323 | listener.splice(i, 1); 324 | } 325 | } 326 | return; 327 | } 328 | if (element.removeEventListener) { 329 | element.removeEventListener(eventName, callback, false); 330 | } 331 | else { 332 | element.detachEvent && element.detachEvent('on' + eventName, callback); 333 | } 334 | } 335 | catch (ex) {} 336 | } 337 | 338 | /** 339 | * 触发事件 340 | * 341 | * @param {string} eventName 事件名 "error"、"close" 342 | * @return {Object} 返回当前实例 343 | * @example 344 | ```js 345 | alog.fire('click', { element: document.getElementById('save') }); 346 | ``` 347 | */ 348 | function fire(eventName) { 349 | var listener = alogListeners[eventName]; 350 | if (!listener) { 351 | return; 352 | } 353 | var items = []; 354 | var args = arguments; 355 | for (var i = 1, len = args.length; i < len; i++) { 356 | items.push(args[i]); 357 | } 358 | 359 | var result = 0; 360 | var j = listener.length; 361 | while (j--) { 362 | if (listener[j].apply(this, items)) { 363 | result++; 364 | } 365 | } 366 | return result; 367 | } 368 | 369 | /** 370 | * 上报数据 371 | * 372 | * @param {string} url 目标链接 373 | * @param {Object} data 上报数据 374 | */ 375 | function report(url, data) { 376 | if (!url || !data) { 377 | return; 378 | } 379 | 380 | // @see http://jsperf.com/new-image-vs-createelement-img 381 | var image = doc.createElement('img'); 382 | 383 | var items = []; 384 | for (var key in data) { 385 | if (data[key]) { 386 | items.push(key + '=' + encodeURIComponent(data[key])); 387 | } 388 | } 389 | 390 | var name = 'img_' + (+new Date()); 391 | entry[name] = image; 392 | image.onload = image.onerror = function () { 393 | entry[name] = 394 | image = 395 | image.onload = 396 | image.onerror = null; 397 | delete entry[name]; 398 | }; 399 | 400 | image.src = url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&'); 401 | } 402 | 403 | /** 404 | * 字段名使用简写 405 | * 406 | * @param {Object} protocolParameter 字段名对照表,如果为null表示不上报 407 | * @param {Object} data 待处理的数据 408 | * @return {Object} 返回处理后的数据 409 | */ 410 | function runProtocolParameter(protocolParameter, data) { 411 | if (!protocolParameter) { 412 | return data; 413 | } 414 | var result = {}; 415 | for (var p in data) { 416 | if (protocolParameter[p] !== null) { 417 | result[protocolParameter[p] || p] = data[p]; 418 | } 419 | } 420 | return result; 421 | } 422 | 423 | /** 424 | * 执行命令 425 | */ 426 | function command() { 427 | var args = arguments; 428 | var method = args[0]; 429 | 430 | if (this.created || /^(on|un|set|get|create)$/.test(method)) { 431 | var methodFunc = Tracker.prototype[method]; 432 | var params = []; 433 | for (var i = 1, len = args.length; i < len; i++) { 434 | params.push(args[i]); 435 | } 436 | if (typeof methodFunc === 'function') { 437 | methodFunc.apply(this, params); 438 | } 439 | } 440 | else { // send|fire // 实例创建以后才能调用的方法 441 | this.argsList.push(args); 442 | } 443 | } 444 | 445 | /** 446 | * 合并两个对象 447 | * 448 | * @param {Object} a 对象1 449 | * @param {Object} b 对象2 450 | * @return {Object} 返回合并后的对象 451 | */ 452 | function merge(a, b) { 453 | var result = {}; 454 | for (var p in a) { 455 | if (a.hasOwnProperty(p)) { 456 | result[p] = a[p]; 457 | } 458 | } 459 | for (var q in b) { 460 | if (b.hasOwnProperty(q)) { 461 | result[q] = b[q]; 462 | } 463 | } 464 | return result; 465 | } 466 | 467 | /** 468 | * 追踪器构造器 469 | * 470 | * @param {string} name 追踪器名称 471 | */ 472 | function Tracker(name) { 473 | this.name = name; 474 | this.fields = { 475 | protocolParameter: { 476 | postUrl: null, 477 | protocolParameter: null 478 | } 479 | }; 480 | this.argsList = []; 481 | this.alog = entry; 482 | } 483 | 484 | /** 485 | * 获取追踪器 486 | * 487 | * @param {string} trackerName 追踪器名称,如果为 '*' 则获取全部追踪器 488 | * @return {Object|Array} 返回追踪器对象 489 | */ 490 | function getTracker(trackerName) { 491 | trackerName = trackerName || 'default'; 492 | if (trackerName === '*') { 493 | var result = []; 494 | for (var p in trackers) { 495 | if (trackers.hasOwnProperty(p)) { 496 | result.push(trackers[p]); 497 | } 498 | } 499 | return result; 500 | } 501 | return (trackers[trackerName] = 502 | trackers[trackerName] || new Tracker(trackerName)); 503 | } 504 | /** 505 | * 创建追踪器 506 | * 507 | * @param {Object} fields 字段列表 508 | */ 509 | Tracker.prototype.create = function (fields) { 510 | if (this.created) { 511 | return; 512 | } 513 | 514 | if (typeof fields === 'object') { 515 | this.set(fields); 516 | } 517 | this.created = new Date(); 518 | this.fire('create', this); 519 | var args; 520 | while (args = this.argsList.shift()) { 521 | command.apply(this, args); 522 | } 523 | }; 524 | 525 | /** 526 | * 发送日志数据 527 | * 528 | * @param {string} hitType 数据类型 529 | * @param {Object} fieldObject 发送数据 530 | */ 531 | Tracker.prototype.send = function (hitType, fieldObject) { 532 | var data = merge({ 533 | ts: timestamp().toString(36), 534 | t: hitType, 535 | sid: sid 536 | }, this.fields); 537 | 538 | if (typeof fieldObject === 'object') { 539 | data = merge(data, fieldObject); 540 | } 541 | else { 542 | var args = arguments; 543 | switch (hitType) { 544 | case 'pageview': 545 | // [page[, title]] 546 | if (args[1]) { 547 | data.page = args[1]; 548 | } 549 | if (args[2]) { 550 | data.title = args[2]; 551 | } 552 | break; 553 | case 'event': 554 | // eventCategory, eventAction[, eventLabel[, eventValue]] 555 | if (args[1]) { 556 | data.eventCategory = args[1]; 557 | } 558 | if (args[2]) { 559 | data.eventAction = args[2]; 560 | } 561 | if (args[3]) { 562 | data.eventLabel = args[3]; 563 | } 564 | if (args[4]) { 565 | data.eventValue = args[4]; 566 | } 567 | break; 568 | case 'timing': 569 | // timingCategory, timingVar, timingValue[, timingLabel] 570 | if (args[1]) { 571 | data.timingCategory = args[1]; 572 | } 573 | if (args[2]) { 574 | data.timingVar = args[2]; 575 | } 576 | if (args[3]) { 577 | data.timingValue = args[3]; 578 | } 579 | if (args[4]) { 580 | data.timingLabel = args[4]; 581 | } 582 | break; 583 | case 'exception': 584 | // exDescription[, exFatal] 585 | if (args[1]) { 586 | data.exDescription = args[1]; 587 | } 588 | if (args[2]) { 589 | data.exFatal = args[2]; 590 | } 591 | break; 592 | default: 593 | return; 594 | } 595 | } 596 | this.fire('send', data); 597 | report(this.fields.postUrl, runProtocolParameter(this.fields.protocolParameter, data)); 598 | }; 599 | 600 | /** 601 | * 设置字段值 602 | * 603 | * @param {string} name 字段名 604 | * @param {Any} value 字段值 605 | */ 606 | Tracker.prototype.set = function (name, value) { 607 | if (typeof name === 'string') { 608 | if (name === 'protocolParameter') { 609 | value = merge({ 610 | postUrl: null, 611 | protocolParameter: null 612 | }, value); 613 | } 614 | this.fields[name] = value; 615 | } 616 | else if (typeof name === 'object') { 617 | for (var p in name) { 618 | if (name.hasOwnProperty(p)) { 619 | this.set(p, name[p]); 620 | } 621 | } 622 | } 623 | }; 624 | 625 | /** 626 | * 获取字段值 627 | * 628 | * @param {string} name 字段名 629 | * @param {Function} callback 回调函数 630 | * @return {Object} 返回当前实例 631 | */ 632 | Tracker.prototype.get = function (name, callback) { 633 | var result = this.fields[name]; 634 | if (typeof callback === 'function') { 635 | callback(result); 636 | } 637 | return result; 638 | }; 639 | 640 | /** 641 | * 触发事件 642 | * 643 | * @param {string} eventName 事件名 644 | * @return {Object} 返回当前实例 645 | */ 646 | Tracker.prototype.fire = function (eventName) { 647 | var items = [this.name + '.' + eventName]; 648 | 649 | var args = arguments; 650 | for (var i = 1, len = args.length; i < len; i++) { 651 | items.push(args[i]); 652 | } 653 | return fire.apply(this, items); 654 | }; 655 | 656 | /** 657 | * 绑定事件 658 | * 659 | * @param {string} eventName 事件名 660 | * @param {Function} callback 回调函数 661 | */ 662 | Tracker.prototype.on = function (eventName, callback) { 663 | entry.on(this.name + '.' + eventName, callback); 664 | }; 665 | 666 | /** 667 | * 注销事件 668 | * 669 | * @param {string} eventName 事件名 670 | * @param {Function} callback 回调函数 671 | */ 672 | Tracker.prototype.un = function (eventName, callback) { 673 | entry.un(this.name + '.' + eventName, callback); 674 | }; 675 | 676 | entry.name = 'alog'; 677 | entry.sid = sid; 678 | entry.defined = true; 679 | entry.timestamp = timestamp; 680 | entry.un = un; 681 | entry.on = on; 682 | entry.fire = fire; 683 | entry.tracker = getTracker; 684 | 685 | entry('init'); 686 | 687 | defaultTracker = getTracker(); 688 | defaultTracker.set('protocolParameter', { 689 | modules: null 690 | }); 691 | 692 | if (oldObject) { 693 | // 处理临时alog对象 694 | var items = [].concat(oldObject.p || [], oldObject.q || []); 695 | oldObject.p = oldObject.q = null; // 清理内存 696 | for (var p in entry) { 697 | if (entry.hasOwnProperty(p)) { 698 | oldObject[p] = entry[p]; 699 | } 700 | } 701 | entry.p = entry.q = { // 接管之前的定义 702 | push: function (args) { 703 | entry.apply(entry, args); 704 | } 705 | }; 706 | 707 | // 开始处理缓存命令 708 | for (var i = 0; i < items.length; i++) { 709 | entry.apply(entry, items[i]); 710 | } 711 | } 712 | win[objectName] = entry; 713 | 714 | /**/ 715 | if (ie) { 716 | on(doc, 'mouseup', function (e) { 717 | var target = e.target || e.srcElement; 718 | if (target.nodeType === 1 && /^ajavascript:/i.test(target.tagName + target.href)) { 719 | clickJsLinkTime = new Date(); 720 | } 721 | }); 722 | } 723 | /**/ 724 | 725 | /** 726 | * 页面关闭时处理方法 727 | */ 728 | function unloadHandler() { 729 | /**/ 730 | // @see http://msdn.microsoft.com/en-us/library/ms536907(VS.85).aspx 731 | // Click an anchor that refers to another document. 732 | // 修复 IE 中点击 `...` 也会触发 beforeunload 事件的问题 733 | if (ie && (new Date() - clickJsLinkTime < 50)) { 734 | return; 735 | } 736 | /**/ 737 | 738 | if (closing) { 739 | return; 740 | } 741 | closing = true; 742 | 743 | var sleepCount = 0; 744 | for (var p in trackers) { 745 | if (trackers.hasOwnProperty(p)) { 746 | var tracker = trackers[p]; 747 | if (tracker.created) { 748 | sleepCount += tracker.fire('unload'); 749 | } 750 | } 751 | } 752 | 753 | if (sleepCount) { // isSleep 754 | var isSleep = new Date(); 755 | while ((new Date() - isSleep) < 100) { 756 | /* isSleep */ 757 | } 758 | } 759 | } 760 | on(win, 'beforeunload', unloadHandler); 761 | on(win, 'unload', unloadHandler); 762 | 763 | })(window, document); 764 | --------------------------------------------------------------------------------