├── .bowerrc ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── dist ├── bj-report-tryjs.js ├── bj-report-tryjs.min.js ├── bj-report.js └── bj-report.min.js ├── example ├── aa.js ├── bb.js ├── index-for-jquery.html ├── index-for-seajs.html ├── index.html ├── report.js ├── sea-debug.js └── zepto.js ├── package.json ├── src ├── bj-report.js └── bj-wrap.js └── test ├── index.html ├── index.js ├── setup.js └── testForLocation.html /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "proxy" : "http://web-proxyhk.oa.com:8080", 3 | "https-proxy" : "http://web-proxyhk.oa.com:8080" 4 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = crlf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # Tabs in JS unless otherwise specified 13 | [**.js] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [**.html] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [**.css] 22 | indent_style = space 23 | indent_size = 4 24 | 25 | [**.sass] 26 | indent_style = space 27 | indent_size = 4 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components* 2 | node_modules* 3 | npm-debug.log 4 | components* 5 | /temp/ 6 | .idea/ 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // 参考 http://jslinterrors.com/ 3 | // ==============预定义变量============ 4 | "globals":{ 5 | "$": true, 6 | "jQuery": true, 7 | "exports": true, 8 | "module": true, 9 | "define": true, 10 | "require": true, 11 | "it": true, 12 | "before": true, 13 | "describe": true, 14 | "beforeEach": true, 15 | "afterEach": true 16 | }, 17 | 18 | // ==============Legacy Options============ 19 | // 注: 这些选项继承自jslint 20 | 21 | // 当变量名以"_"开头或结尾时,是否告警 22 | "nomen": false, 23 | // 是否只允许出现一个var在函数中 24 | "onevar": false, 25 | // 是否遇到第一个错误的时候就终止 26 | "passfail": false, 27 | // 让JSHint检查你的代码是否违反道格拉斯的JS编码风格(严格的空白规范??). 28 | "white": false, 29 | // 最大错误数目,超过则停止分析(默认:50) 30 | "maxerr": 50, 31 | 32 | // ==============Environments Options============ 33 | 34 | // 是否预定义现代浏览器暴露出来的全局变量(不包括alert和console) 35 | "browser": true, 36 | 37 | // 是否预定义console,alert这种开发阶段的调试代码 38 | "devel": true, 39 | 40 | // 该选项定义了一些非标准但广泛使用的全局变量,像"escape"和"unescape". 41 | "nonstandard": true, 42 | 43 | // ==============Enforcing Options============ 44 | 45 | // 禁止使用位运算符 46 | "bitwise": true, 47 | // 只允许使用camelCase or UPPER_CASE 48 | "camelcase": false, 49 | // 必须给block加括号 50 | "curly": false, 51 | // 不安全的== (会建议换成===或者!==) 52 | "eqeqeq": false, 53 | // 不安全的for in 54 | "forin": false, 55 | // 立即函数调用必须用(function(){}())而不是(function(){})() 56 | "immed": true, 57 | // 必须先定义再使用 58 | "latedef": false, 59 | // 构造函数名的首字母必须大写 60 | "newcap": true, 61 | // 禁止arguments.caller和arguments.callee的使用(ES5的严格模式中被禁用了,并且不是所有js实现中都有) 62 | "noarg": true, 63 | // 禁止空的代码块或未赋值的构造器 64 | "noempty": false, 65 | // 不允许不做赋值的构造函数 66 | "nonew": true, 67 | // 不允许使用++或者--的操作符 68 | "plusplus": false, 69 | // 正则中不允许使用.或者[^…] 70 | "regexp": false, 71 | // 未定义的XXX 72 | "undef": true, 73 | // 未使用的XXX 74 | "unused": false, 75 | // 需要使用strict mode,详见http:// ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/ 76 | "strict": false, 77 | // 行末有多余的空格 78 | "trailing": false, 79 | 80 | // 配置单双引号规则: "false"或者不设置 不做限制 "single" 只许单引号 "double" 只许双引号 "true" 两者选其一,但不能同时出现 81 | "quotmark": "false", 82 | 83 | // 配置缩进规则: 设置缩进对应的空格数,如不设置,则不对缩进做要求 84 | // 注: 这里配置了indent,则即使上面的white设置了false,也还是会导致 swindent 报warning(https:// github.com/jshint/jshint/issues/935) 85 | // TODO:确认jshint解决了 swindent 的支持后可重新配置此项 86 | // "indent": 4, 87 | 88 | // 配置单行代码的最大长度 89 | // "maxlen": 120, 90 | 91 | // 配置函数的最多参数个数: 参数过多时,可读性下降,可考虑用options对象字段来减少参数个数 92 | // "maxparams": 5, 93 | 94 | // 配置函数内代码块嵌套最多层数 95 | // "maxdepth": 5, 96 | 97 | // 配置函数内最多有多少条语句(一个函数声明会被当作一条语句) 98 | // "maxstatements": 50, 99 | 100 | // 配置最大圈复杂度(http://en.wikipedia.org/wiki/Cyclomatic_complexity) 101 | // "maxcomplexity": 10, // TODO:打开来优化代码复杂度 102 | 103 | 104 | // ==============Relaxing Options============ 105 | 106 | // 代码末尾缺失分号,是否放过 107 | "asi": false, 108 | // for等预期是==的地方用了=,是否放过 109 | "boss": false, 110 | // 代码中有debugger语句,是否放过 111 | "debug": false, 112 | // 使用"=="来比较变量与null,是否放过 113 | "eqnull": false, 114 | // ES5语法校验报警,是否放过 115 | "es5": false, 116 | // ECMAScript 6语法校验报警,是否放过 117 | "esnext": true, 118 | // 代码中使用了eval,是否放过 119 | "evil": true, 120 | // 只允许在函数调用或赋值时使用表达式(原因???) 2 > 1 ? (alert(1)) : (alert(2)); 121 | "expr": true, 122 | // 在控制语句中定义了变量,却在控制语句之外使用变量 function(){ if(1) {var x = 0;} x = x + 1;} 123 | "funcscope": false, 124 | // 使用了全局级别的严格模式,是否放过 125 | "globalstrict": false, 126 | // 使用了__iterator__这个属性(并非所有浏览器支持), 是否放过 (原因???) 127 | "iterator": false, 128 | 129 | 130 | // 是否允许单行语句块中最后一条语句不写分号 131 | "lastsemic": false, 132 | // 是否允许出现不安全的换行(示例???) 133 | "laxbreak": true, 134 | // 是否允许逗号出现在行首的换行方式 135 | "laxcomma": false, 136 | // 是否允许在循环里定义函数 137 | "loopfunc": true, 138 | // 是否允许多行字符串(原因???) 139 | "multistr": true, 140 | // 是否允许在代码中使用__proto__属性(并非所有浏览器支持) 141 | "proto": false, 142 | // 是否允许在switch语句中只出现一个case 143 | "onecase": true, 144 | // 是否允许unescaped - in the end of regular expressions. 145 | "regexdash": false, 146 | // 是否允许在代码中使用"javascript:..."这样的url 147 | "scripturl": true, 148 | // 是否使用smarttab风格混用tab和space(http:// www.emacswiki.org/emacs/SmartTabs) 149 | "smarttabs": false, 150 | // 是否允许使用shadow变量(shadow变量:当一个变量所在的作用域之外还有一个同名的变量,称为shadow变量) 151 | // http:// stackoverflow.com/questions/5373278/variable-shadowing-in-javascript 152 | "shadow": false, 153 | // 是否允许用obj['name']和obj.name两种方式访问对象的属性 154 | "sub": true, 155 | // 是否允许使用像"new function() {...}"这样怪异的构造器 156 | "supernew": true, 157 | // 是否允许在严格模式下的非构造函数中使用this(该选项只能用于函数作用域中) 158 | "validthis": false, 159 | 160 | // Missing radix parameter to parseInt (defaults to 10) 161 | "-W065": false, 162 | // Literal accessor is better written in dot notation 163 | "-W069": false 164 | } 165 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '5.4.1' 4 | before_script: 5 | - npm install -g grunt-cli 6 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | module.exports = function(grunt) { 3 | 4 | grunt.initConfig({ 5 | clean: { 6 | dist: ['dist'], 7 | cleanOther: ['dist/bj-wrap.js'] 8 | }, 9 | uglify: { 10 | build: { 11 | files: [{ 12 | 'dist/bj-report.min.js': ["dist/bj-report.js"] 13 | }, { 14 | 'dist/bj-report-tryjs.min.js': ["dist/bj-report-tryjs.js"] 15 | },] 16 | } 17 | }, 18 | copy: { 19 | dist: { 20 | files: [{ 21 | expand: true, 22 | cwd: 'src/', 23 | src: ['**/*.js'], 24 | dest: 'dist/' 25 | }] 26 | } 27 | }, 28 | cssmin: { 29 | dist: { 30 | files: [{ 31 | expand: true, 32 | cwd: 'src/', 33 | src: ['**/*.css'], 34 | dest: 'dist/' 35 | }] 36 | } 37 | }, 38 | concat: { 39 | options: { 40 | separator: ';' 41 | }, 42 | dist: { 43 | src: ['src/bj-report.js', 'src/bj-wrap.js'], 44 | dest: 'dist/bj-report-tryjs.js' 45 | 46 | } 47 | }, 48 | mocha: { 49 | all: { 50 | src: 'test/index.html', 51 | run: false 52 | } 53 | }, 54 | jshint: { 55 | options: { 56 | jshintrc: '.jshintrc' 57 | }, 58 | all: ['Gruntfile.js', 'src/**/*.js'] 59 | } 60 | }); 61 | 62 | grunt.loadNpmTasks('grunt-mocha'); 63 | grunt.loadNpmTasks('grunt-contrib-copy'); 64 | grunt.loadNpmTasks('grunt-contrib-clean'); 65 | grunt.loadNpmTasks('grunt-contrib-jshint'); 66 | grunt.loadNpmTasks('grunt-contrib-uglify'); 67 | grunt.loadNpmTasks('grunt-contrib-concat'); 68 | 69 | // alias task 70 | grunt.registerTask('default', ['dev']); 71 | grunt.registerTask('dist', ['build']); 72 | grunt.registerTask('release', ['build']); 73 | 74 | // task 75 | grunt.registerTask('clear', ['clean']); 76 | grunt.registerTask('test', ['jshint', 'mocha']); 77 | grunt.registerTask('dev', ['clean:dev', 'jshint', 'uglify']); 78 | grunt.registerTask('build', ['clean:dist', 'jshint', 'copy:dist', 'concat:dist', 'clean:cleanOther', 'uglify']); 79 | 80 | }; 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | badjs-report -- 前端日志上报与JS异常监控 2 | --- 3 | 4 | [![Build Status](https://travis-ci.org/BetterJS/badjs-report.svg?branch=master)](https://travis-ci.org/BetterJS/badjs-report) 5 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/BetterJS?utm_source=share-link&utm_medium=link&utm_campaign=share-link) 6 | ## Author 7 | [caihuiji](https://github.com/caihuiji) [yorts52](https://github.com/yorts52) 8 | 9 | ## Install 10 | 11 | ```shell 12 | $ npm install badjs-report 13 | ``` 14 | ```shell 15 | $ bower install https://github.com/BetterJS/badjs-report.git 16 | ``` 17 | ```shell 18 | $ lego install badjs-report --save 19 | ``` 20 | 21 | ## Getting Started 22 | > badjs-report 必须在所有类库之前加载并初始化。 23 | > 但是要在 jquery、seajs、requrejs等类库后调用spyAll()。 24 | 25 | ##### 初始化 26 | ```javascript 27 | BJ_REPORT.init({ 28 | id: 1 // 不指定 id 将不上报 29 | }); 30 | ``` 31 | ##### 配置说明 32 | ```javascript 33 | BJ_REPORT.init({ 34 | id: 1, // 上报 id, 不指定 id 将不上报 35 | uin: 123, // 指定用户 id, (默认已经读取 qq uin) 36 | delay: 1000, // 延迟多少毫秒,合并缓冲区中的上报(默认) 37 | url: "//badjs2.qq.com/badjs", // 指定上报地址 38 | ignore: [/Script error/i], // 忽略某个错误 39 | random: 1, // 抽样上报,1~0 之间数值,1为100%上报(默认 1) 40 | repeat: 5, // 重复上报次数(对于同一个错误超过多少次不上报) 41 | // 避免出现单个用户同一错误上报过多的情况 42 | onReport: function(id, errObj){}, // 当上报的时候回调。 id: 上报的 id, errObj: 错误的对象 43 |  submit: null,                         // 覆盖原来的上报方式,可以自行修改为 post 上报等 44 |  ext: {},                              // 扩展属性,后端做扩展处理属性。例如:存在 msid 就会分发到 monitor, 45 |  offlineLog : false,                   // 是否启离线日志 [默认 false] 46 |  offlineLogExp : 5,                   // 离线有效时间,默认最近5天 47 | }); 48 | ``` 49 | BJ_Report 是重写了 window.onerror 进行上报的,无需编写任何捕获错误的代码 50 | 51 | ##### 手动上报 52 | ```javascript 53 | BJ_REPORT.report("error msg"); 54 | 55 | BJ_REPORT.report({ 56 | msg: "xx load error", // 错误信息 57 | target: "xxx.js", // 错误的来源js 58 | rowNum: 100, // 错误的行数 59 | colNum: 100, // 错误的列数 60 | }); 61 | 62 | try{ 63 | // something throw error ... 64 | }catch(error){ 65 | BJ_REPORT.report(e); 66 | } 67 | ``` 68 | 69 | ##### 延迟上报 70 | 71 | ```javascript 72 | BJ_REPORT.push("error msg"); 73 | 74 | BJ_REPORT.push({ 75 | msg: "xx load error", // 错误信息 76 | target: "xxx.js", // 错误的来源js 77 | rowNum: 100, // 错误的行数 78 | colNum: 100, // 错误的列数 79 | }); 80 | 81 | BJ_REPORT.report(); 82 | 83 | ``` 84 | 85 | #####  上报离线日志   86 | 87 | ```javascript 88 | BJ_REPORT.reportOfflineLog(); 89 | ``` 90 | 91 | > 什么是离线日志? [#25](https://github.com/BetterJS/badjs-report/issues/25) 92 | 93 | ##### 用法 94 | ```javascript 95 | //初始化 96 | BJ_REPORT.init({id: 1}) 97 | 98 | //主动上报错误日志 99 | BJ_REPORT.report("error msg 2"); 100 | 101 | //info上报,用于记录操作日志 102 | BJ_REPORT.info("info"); 103 | 104 | //可以结合实时上报,跟踪问题; 不存入存储 105 | BJ_REPORT.debug("debug"); 106 | 107 | //记录离线日志   108 | BJ_REPORT.offlineLog("offlineLog"); 109 | ``` 110 |
111 | 112 | ### 高级用法 113 | >script error 的错误,怎么解决? [#3](https://github.com/BetterJS/badjs-report/issues/3) 114 | 115 | 由于 BJ_Report 只是重写了onerror 方法而已,而且浏览器的跨域问题不能获得外链 javascript 的错误,所以使用tryJs 进行包裹。 116 | #### 包裹jquery 117 | ```javascript 118 | BJ_REPORT.tryJs().spyJquery(); 119 | ``` 120 | 包裹 jquery 的 event.add , event.remove , event.ajax 这几个异步方法。 121 |
122 |
123 | #### 包裹 define , require 124 | ```javascript 125 | BJ_REPORT.tryJs().spyModules(); 126 | ``` 127 | 包裹 模块化框架 的 define , require 方法 128 |
129 |
130 | #### 包裹 js 默认的方法 131 | ```javascript 132 | BJ_REPORT.tryJs().spySystem(); 133 | ``` 134 | 包裹 js 的 setTimeout , setInterval 方法 135 |
136 |
137 | #### 包裹 自定义的方法 138 | ```javascript 139 | var customFunction = function (){}; 140 | customFunction = BJ_REPORT.tryJs().spyCustom(customFunction ); 141 | 142 | // 只会包裹 customOne , customTwo 143 | var customObject = { customOne : function (){} , customTwo : function (){} , customVar : 1} 144 | BJ_REPORT.tryJs().spyCustom(customObject ); 145 | ``` 146 | 包裹 自定义的方法或则对象 147 |
148 |
149 | #### 运行所有默认的包裹 150 | ```javascript 151 | //自动运行 SpyJquery , SpyModule , SpySystem 152 | BJ_REPORT.tryJs().spyAll(); 153 | ``` 154 | 155 | ## update log 156 | ##### v1.3.3 157 | 1. BUGFIX 158 | 159 | ##### v1.3.1 160 | 1. 支持离线日志 161 | 2. 支持自动上报离线日志 162 | 163 | ##### v1.2.3 164 | 1. BUGFIX 165 | 166 | ##### v1.2.1 167 | 1. 增加去除重复参数 168 | 2. 修复了 webpack 引入问题 169 | 3. BUGFIX 170 | 171 | ##### v1.1.8 172 | 1. 项目重命名后更新项目路径(注: 之前名字为`report`) 173 | 174 | ##### v1.1.7 175 | 1. 合并上报的问题 176 | 2. 增加sea.use try-catch 处理 177 | 178 | ##### v1.1.6 179 | 1. add BJ_ERROR hash 180 | 181 | ##### v1.1.5 182 | 1. bugfix 183 | 184 | ##### v1.1.4 185 | 1. 增加info 和 debug 接口 186 | 2. report 增加对 error 对象处理 187 | 3. 处理 [Object event] 问题 188 | 189 | ##### v1.1.3 190 | 1. bugfix 191 | 192 | ##### v1.1.2 193 | 1. 增加抽样参数 random 194 | 195 | ##### v1.1.1 196 | 1. seajs 兼容的BUG修复 197 | 2. 增加 ext 属性,用户可以自己定义里面的值上报 198 | 199 | ##### v1.1.0 200 | 1. 增加对seajs 模块化的包裹 201 | 2. 增加对IE下面的错误的上报 202 | 203 | ##### v1.0.5 204 | 1. 修复异步环境下抛给浏览器的BUG也会上报, 205 | 2. 修复ignore 数组判断的迭代的问题 206 | 207 | ##### v1.0.4 208 | 1. 修复 spy 插件增加在 异步环境中,抛出异常捕获后,再抛给浏览器 209 | 2. 修复 增加在异步环境中,抛出异常,捕获后,将错误信息输出 210 | 3. 增加onReport 回调 211 | 212 | ##### v1.0.3 213 | 1. 修复说明文档 214 | 215 | ##### v1.0.2 216 | 1. 修复 uin 的正则 217 | 218 | ##### v1.0.1 219 | 1. 增加 spy 插件 220 | 221 | ##### v1.0.0 222 | 1. 功能上线 223 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "badjs-report", 3 | "version": "1.3.3", 4 | "homepage": "https://github.com/BetterJS/report", 5 | "author": "chriscai,kael", 6 | "description": "client report", 7 | "main": "dist/bj-report-tryjs.js", 8 | "license": "MIT", 9 | "keywords": ["report"], 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /dist/bj-report-tryjs.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @module report 3 | * @author kael, chriscai 4 | * @date @DATE 5 | * Copyright (c) 2014 kael, chriscai 6 | * Licensed under the MIT license. 7 | */ 8 | var BJ_REPORT = (function(global) { 9 | if (global.BJ_REPORT) return global.BJ_REPORT; 10 | 11 | var _log_list = []; 12 | var _log_map = {}; 13 | var _config = { 14 | id: 0, // 上报 id 15 | uin: 0, // user id 16 | url: "", // 上报 接口 17 | offline_url: "", // 离线日志上报 接口 18 | offline_auto_url: "", // 检测是否自动上报 19 | ext: null, // 扩展参数 用于自定义上报 20 | level: 4, // 错误级别 1-debug 2-info 4-error 21 | ignore: [], // 忽略某个错误, 支持 Regexp 和 Function 22 | random: 1, // 抽样 (0-1] 1-全量 23 | delay: 1000, // 延迟上报 combo 为 true 时有效 24 | submit: null, // 自定义上报方式 25 | repeat: 5, // 重复上报次数(对于同一个错误超过多少次不上报), 26 | offlineLog: false, 27 | offlineLogExp: 5, // 离线日志过期时间 , 默认5天 28 | offlineLogAuto: false, //是否自动询问服务器需要自动上报 29 | }; 30 | 31 | var Offline_DB = { 32 | db: null, 33 | ready: function(callback) { 34 | var self = this; 35 | if (!window.indexedDB || !_config.offlineLog) { 36 | _config.offlineLog = false; 37 | return callback(); 38 | } 39 | 40 | if (this.db) { 41 | setTimeout(function() { 42 | callback(null, self); 43 | }, 0); 44 | 45 | return; 46 | } 47 | var version = 1; 48 | var request = window.indexedDB.open("badjs", version); 49 | 50 | if (!request) { 51 | _config.offlineLog = false; 52 | return callback(); 53 | } 54 | 55 | request.onerror = function(e) { 56 | callback(e); 57 | _config.offlineLog = false; 58 | console.log("indexdb request error"); 59 | return true; 60 | }; 61 | request.onsuccess = function(e) { 62 | self.db = e.target.result; 63 | 64 | setTimeout(function() { 65 | callback(null, self); 66 | }, 500); 67 | 68 | 69 | }; 70 | request.onupgradeneeded = function(e) { 71 | var db = e.target.result; 72 | if (!db.objectStoreNames.contains('logs')) { 73 | db.createObjectStore('logs', { autoIncrement: true }); 74 | } 75 | }; 76 | }, 77 | insertToDB: function(log) { 78 | var store = this.getStore(); 79 | store.add(log); 80 | }, 81 | addLog: function(log) { 82 | if (!this.db) { 83 | return; 84 | } 85 | this.insertToDB(log); 86 | }, 87 | addLogs: function(logs) { 88 | if (!this.db) { 89 | return; 90 | } 91 | 92 | for (var i = 0; i < logs.length; i++) { 93 | this.addLog(logs[i]); 94 | } 95 | 96 | }, 97 | getLogs: function(opt, callback) { 98 | if (!this.db) { 99 | return; 100 | } 101 | var store = this.getStore(); 102 | var request = store.openCursor(); 103 | var result = []; 104 | request.onsuccess = function(event) { 105 | var cursor = event.target.result; 106 | if (cursor) { 107 | if (cursor.value.time >= opt.start && cursor.value.time <= opt.end && cursor.value.id == opt.id && cursor.value.uin == opt.uin) { 108 | result.push(cursor.value); 109 | } 110 | //# cursor.continue 111 | cursor["continue"](); 112 | } else { 113 | callback(null, result); 114 | } 115 | }; 116 | 117 | request.onerror = function(e) { 118 | callback(e); 119 | return true; 120 | }; 121 | }, 122 | clearDB: function(daysToMaintain) { 123 | if (!this.db) { 124 | return; 125 | } 126 | 127 | var store = this.getStore(); 128 | if (!daysToMaintain) { 129 | store.clear(); 130 | return; 131 | } 132 | var range = (Date.now() - (daysToMaintain || 2) * 24 * 3600 * 1000); 133 | var request = store.openCursor(); 134 | request.onsuccess = function(event) { 135 | var cursor = event.target.result; 136 | if (cursor && (cursor.value.time < range || !cursor.value.time)) { 137 | store["delete"](cursor.primaryKey); 138 | cursor["continue"](); 139 | } 140 | }; 141 | }, 142 | 143 | getStore: function() { 144 | var transaction = this.db.transaction("logs", 'readwrite'); 145 | return transaction.objectStore("logs"); 146 | }, 147 | 148 | }; 149 | 150 | var T = { 151 | isOBJByType: function(o, type) { 152 | return Object.prototype.toString.call(o) === "[object " + (type || "Object") + "]"; 153 | }, 154 | 155 | isOBJ: function(obj) { 156 | var type = typeof obj; 157 | return type === "object" && !!obj; 158 | }, 159 | isEmpty: function(obj) { 160 | if (obj === null) return true; 161 | if (T.isOBJByType(obj, "Number")) { 162 | return false; 163 | } 164 | return !obj; 165 | }, 166 | extend: function(src, source) { 167 | for (var key in source) { 168 | src[key] = source[key]; 169 | } 170 | return src; 171 | }, 172 | processError: function(errObj) { 173 | try { 174 | if (errObj.stack) { 175 | var url = errObj.stack.match("https?://[^\n]+"); 176 | url = url ? url[0] : ""; 177 | var rowCols = url.match(":(\\d+):(\\d+)"); 178 | if (!rowCols) { 179 | rowCols = [0, 0, 0]; 180 | } 181 | 182 | var stack = T.processStackMsg(errObj); 183 | return { 184 | msg: stack, 185 | rowNum: rowCols[1], 186 | colNum: rowCols[2], 187 | target: url.replace(rowCols[0], ""), 188 | _orgMsg: errObj.toString() 189 | }; 190 | } else { 191 | //ie 独有 error 对象信息,try-catch 捕获到错误信息传过来,造成没有msg 192 | if (errObj.name && errObj.message && errObj.description) { 193 | return { 194 | msg: JSON.stringify(errObj) 195 | }; 196 | } 197 | return errObj; 198 | } 199 | } catch (err) { 200 | return errObj; 201 | } 202 | }, 203 | 204 | processStackMsg: function(error) { 205 | var stack = error.stack 206 | .replace(/\n/gi, "") 207 | .split(/\bat\b/) 208 | .slice(0, 9) 209 | .join("@") 210 | .replace(/\?[^:]+/gi, ""); 211 | var msg = error.toString(); 212 | if (stack.indexOf(msg) < 0) { 213 | stack = msg + "@" + stack; 214 | } 215 | return stack; 216 | }, 217 | 218 | isRepeat: function(error) { 219 | if (!T.isOBJ(error)) return true; 220 | var msg = error.msg; 221 | var times = _log_map[msg] = (parseInt(_log_map[msg], 10) || 0) + 1; 222 | return times > _config.repeat; 223 | } 224 | }; 225 | 226 | var orgError = global.onerror; 227 | // rewrite window.oerror 228 | global.onerror = function(msg, url, line, col, error) { 229 | var newMsg = msg; 230 | 231 | if (error && error.stack) { 232 | newMsg = T.processStackMsg(error); 233 | } 234 | 235 | if (T.isOBJByType(newMsg, "Event")) { 236 | newMsg += newMsg.type ? 237 | ("--" + newMsg.type + "--" + (newMsg.target ? 238 | (newMsg.target.tagName + "::" + newMsg.target.src) : "")) : ""; 239 | } 240 | 241 | report.push({ 242 | msg: newMsg, 243 | target: url, 244 | rowNum: line, 245 | colNum: col, 246 | _orgMsg: msg 247 | }); 248 | 249 | _process_log(); 250 | orgError && orgError.apply(global, arguments); 251 | }; 252 | 253 | 254 | 255 | var _report_log_tostring = function(error, index) { 256 | var param = []; 257 | var params = []; 258 | var stringify = []; 259 | if (T.isOBJ(error)) { 260 | error.level = error.level || _config.level; 261 | for (var key in error) { 262 | var value = error[key]; 263 | if (!T.isEmpty(value)) { 264 | if (T.isOBJ(value)) { 265 | try { 266 | value = JSON.stringify(value); 267 | } catch (err) { 268 | value = "[BJ_REPORT detect value stringify error] " + err.toString(); 269 | } 270 | } 271 | stringify.push(key + ":" + value); 272 | param.push(key + "=" + encodeURIComponent(value)); 273 | params.push(key + "[" + index + "]=" + encodeURIComponent(value)); 274 | } 275 | } 276 | } 277 | 278 | // msg[0]=msg&target[0]=target -- combo report 279 | // msg:msg,target:target -- ignore 280 | // msg=msg&target=target -- report with out combo 281 | return [params.join("&"), stringify.join(","), param.join("&")]; 282 | }; 283 | 284 | 285 | 286 | var _offline_buffer = []; 287 | var _save2Offline = function(key, msgObj) { 288 | msgObj = T.extend({ id: _config.id, uin: _config.uin, time: new Date - 0 }, msgObj); 289 | 290 | if (Offline_DB.db) { 291 | Offline_DB.addLog(msgObj); 292 | return; 293 | } 294 | 295 | 296 | if (!Offline_DB.db && !_offline_buffer.length) { 297 | Offline_DB.ready(function(err, DB) { 298 | if (DB) { 299 | if (_offline_buffer.length) { 300 | DB.addLogs(_offline_buffer); 301 | _offline_buffer = []; 302 | } 303 | 304 | } 305 | }); 306 | } 307 | _offline_buffer.push(msgObj); 308 | }; 309 | 310 | var _autoReportOffline = function() { 311 | var script = document.createElement("script"); 312 | script.src = _config.offline_auto_url || _config.url.replace(/badjs$/, "offlineAuto") + "?id=" + _config.id + "&uin=" + _config.uin; 313 | window._badjsOfflineAuto = function(isReport) { 314 | if (isReport) { 315 | BJ_REPORT.reportOfflineLog(); 316 | } 317 | }; 318 | document.head.appendChild(script); 319 | }; 320 | 321 | 322 | 323 | var submit_log_list = []; 324 | var comboTimeout = 0; 325 | var _submit_log = function() { 326 | clearTimeout(comboTimeout); 327 | // https://github.com/BetterJS/badjs-report/issues/34 328 | comboTimeout = 0; 329 | 330 | if (!submit_log_list.length) { 331 | return; 332 | } 333 | 334 | var url = _config._reportUrl + submit_log_list.join("&") + "&count=" + submit_log_list.length + "&_t=" + (+new Date); 335 | 336 | if (_config.submit) { 337 | _config.submit(url, submit_log_list); 338 | } else { 339 | var _img = new Image(); 340 | _img.src = url; 341 | } 342 | 343 | submit_log_list = []; 344 | }; 345 | 346 | var _process_log = function(isReportNow) { 347 | if (!_config._reportUrl) return; 348 | 349 | var randomIgnore = Math.random() >= _config.random; 350 | 351 | 352 | while (_log_list.length) { 353 | var isIgnore = false; 354 | var report_log = _log_list.shift(); 355 | //有效保证字符不要过长 356 | report_log.msg = (report_log.msg + "" || "").substr(0, 500); 357 | // 重复上报 358 | if (T.isRepeat(report_log)) continue; 359 | var log_str = _report_log_tostring(report_log, submit_log_list.length); 360 | if (T.isOBJByType(_config.ignore, "Array")) { 361 | for (var i = 0, l = _config.ignore.length; i < l; i++) { 362 | var rule = _config.ignore[i]; 363 | if ((T.isOBJByType(rule, "RegExp") && rule.test(log_str[1])) || 364 | (T.isOBJByType(rule, "Function") && rule(report_log, log_str[1]))) { 365 | isIgnore = true; 366 | break; 367 | } 368 | } 369 | } 370 | if (!isIgnore) { 371 | _config.offlineLog && _save2Offline("badjs_" + _config.id + _config.uin, report_log); 372 | if (!randomIgnore && report_log.level != 20) { 373 | submit_log_list.push(log_str[0]); 374 | _config.onReport && (_config.onReport(_config.id, report_log)); 375 | } 376 | 377 | } 378 | } 379 | 380 | 381 | if (isReportNow) { 382 | _submit_log(); // 立即上报 383 | } else if (!comboTimeout) { 384 | comboTimeout = setTimeout(_submit_log, _config.delay); // 延迟上报 385 | } 386 | }; 387 | 388 | 389 | 390 | var report = global.BJ_REPORT = { 391 | push: function(msg) { // 将错误推到缓存池 392 | 393 | var data = T.isOBJ(msg) ? T.processError(msg) : { 394 | msg: msg 395 | }; 396 | 397 | // ext 有默认值, 且上报不包含 ext, 使用默认 ext 398 | if (_config.ext && !data.ext) { 399 | data.ext = _config.ext; 400 | } 401 | // 在错误发生时获取页面链接 402 | // https://github.com/BetterJS/badjs-report/issues/19 403 | if (!data.from) { 404 | data.from = location.href; 405 | } 406 | 407 | if (data._orgMsg) { 408 | var _orgMsg = data._orgMsg; 409 | delete data._orgMsg; 410 | data.level = 2; 411 | var newData = T.extend({}, data); 412 | newData.level = 4; 413 | newData.msg = _orgMsg; 414 | _log_list.push(data); 415 | _log_list.push(newData); 416 | } else { 417 | _log_list.push(data); 418 | } 419 | 420 | _process_log(); 421 | return report; 422 | }, 423 | report: function(msg, isReportNow) { // error report 424 | msg && report.push(msg); 425 | 426 | isReportNow && _process_log(true); 427 | return report; 428 | }, 429 | info: function(msg) { // info report 430 | if (!msg) { 431 | return report; 432 | } 433 | if (T.isOBJ(msg)) { 434 | msg.level = 2; 435 | } else { 436 | msg = { 437 | msg: msg, 438 | level: 2 439 | }; 440 | } 441 | report.push(msg); 442 | return report; 443 | }, 444 | debug: function(msg) { // debug report 445 | if (!msg) { 446 | return report; 447 | } 448 | if (T.isOBJ(msg)) { 449 | msg.level = 1; 450 | } else { 451 | msg = { 452 | msg: msg, 453 | level: 1 454 | }; 455 | } 456 | report.push(msg); 457 | return report; 458 | }, 459 | 460 | reportOfflineLog: function() { 461 | if (!window.indexedDB) { 462 | BJ_REPORT.info("unsupport offlineLog"); 463 | return; 464 | } 465 | Offline_DB.ready(function(err, DB) { 466 | if (!DB) { 467 | return; 468 | } 469 | var startDate = new Date - 0 - _config.offlineLogExp * 24 * 3600 * 1000; 470 | var endDate = new Date - 0; 471 | DB.getLogs({ 472 | start: startDate, 473 | end: endDate, 474 | id: _config.id, 475 | uin: _config.uin 476 | }, function(err, result) { 477 | var iframe = document.createElement("iframe"); 478 | iframe.name = "badjs_offline_" + (new Date - 0); 479 | iframe.frameborder = 0; 480 | iframe.height = 0; 481 | iframe.width = 0; 482 | iframe.src = "javascript:false;"; 483 | 484 | iframe.onload = function() { 485 | var form = document.createElement("form"); 486 | form.style.display = "none"; 487 | form.target = iframe.name; 488 | form.method = "POST"; 489 | form.action = _config.offline_url || _config.url.replace(/badjs$/, "offlineLog"); 490 | form.enctype.method = 'multipart/form-data'; 491 | 492 | var input = document.createElement("input"); 493 | input.style.display = "none"; 494 | input.type = "hidden"; 495 | input.name = "offline_log"; 496 | input.value = JSON.stringify({ logs: result, userAgent: navigator.userAgent, startDate: startDate, endDate: endDate, id: _config.id, uin: _config.uin }); 497 | 498 | iframe.contentDocument.body.appendChild(form); 499 | form.appendChild(input); 500 | form.submit(); 501 | 502 | setTimeout(function() { 503 | document.body.removeChild(iframe); 504 | }, 10000); 505 | 506 | iframe.onload = null; 507 | }; 508 | document.body.appendChild(iframe); 509 | }); 510 | }); 511 | }, 512 | offlineLog: function(msg) { 513 | if (!msg) { 514 | return report; 515 | } 516 | if (T.isOBJ(msg)) { 517 | msg.level = 20; 518 | } else { 519 | msg = { 520 | msg: msg, 521 | level: 20 522 | }; 523 | } 524 | report.push(msg); 525 | return report; 526 | }, 527 | init: function(config) { // 初始化 528 | if (T.isOBJ(config)) { 529 | for (var key in config) { 530 | _config[key] = config[key]; 531 | } 532 | } 533 | // 没有设置id将不上报 534 | var id = parseInt(_config.id, 10); 535 | if (id) { 536 | // set default report url and uin 537 | if (/qq\.com$/gi.test(location.hostname)) { 538 | if (!_config.url) { 539 | _config.url = "//badjs2.qq.com/badjs"; 540 | } 541 | 542 | if (!_config.uin) { 543 | _config.uin = parseInt((document.cookie.match(/\buin=\D+(\d+)/) || [])[1], 10); 544 | } 545 | } 546 | 547 | _config._reportUrl = (_config.url || "/badjs") + 548 | "?id=" + id + 549 | "&uin=" + _config.uin + 550 | // "&from=" + encodeURIComponent(location.href) + 551 | "&"; 552 | } 553 | 554 | // if had error in cache , report now 555 | if (_log_list.length) { 556 | _process_log(); 557 | } 558 | 559 | // init offline 560 | if (!Offline_DB._initing) { 561 | Offline_DB._initing = true; 562 | Offline_DB.ready(function(err, DB) { 563 | if (DB) { 564 | setTimeout(function() { 565 | DB.clearDB(_config.offlineLogExp); 566 | setTimeout(function() { 567 | _config.offlineLogAuto && _autoReportOffline(); 568 | }, 5000); 569 | }, 1000); 570 | } 571 | 572 | }); 573 | } 574 | 575 | 576 | 577 | return report; 578 | }, 579 | 580 | __onerror__: global.onerror 581 | }; 582 | 583 | typeof console !== "undefined" && console.error && setTimeout(function() { 584 | var err = ((location.hash || "").match(/([#&])BJ_ERROR=([^&$]+)/) || [])[2]; 585 | err && console.error("BJ_ERROR", decodeURIComponent(err).replace(/(:\d+:\d+)\s*/g, "$1\n")); 586 | }, 0); 587 | 588 | return report; 589 | 590 | }(window)); 591 | 592 | if (typeof module !== "undefined") { 593 | module.exports = BJ_REPORT; 594 | } 595 | ;(function(global) { 596 | 597 | if (!global.BJ_REPORT) { 598 | console.error("please load bg-report first"); 599 | return; 600 | } 601 | 602 | var _onthrow = function(errObj) { 603 | global.BJ_REPORT.push(errObj); 604 | }; 605 | 606 | var tryJs = {}; 607 | global.BJ_REPORT.tryJs = function(throwCb) { 608 | throwCb && (_onthrow = throwCb); 609 | return tryJs; 610 | }; 611 | 612 | // merge 613 | var _merge = function(org, obj) { 614 | for (var key in obj) { 615 | org[key] = obj[key]; 616 | } 617 | }; 618 | 619 | // function or not 620 | var _isFunction = function(foo) { 621 | return typeof foo === "function"; 622 | }; 623 | 624 | var timeoutkey; 625 | 626 | var cat = function(foo, args) { 627 | return function() { 628 | try { 629 | return foo.apply(this, args || arguments); 630 | } catch (error) { 631 | 632 | _onthrow(error); 633 | 634 | //some browser throw error (chrome) , can not find error where it throw, so print it on console; 635 | if (error.stack && console && console.error) { 636 | console.error("[BJ-REPORT]", error.stack); 637 | } 638 | 639 | // hang up browser and throw , but it should trigger onerror , so rewrite onerror then recover it 640 | if (!timeoutkey) { 641 | var orgOnerror = global.onerror; 642 | global.onerror = function() { }; 643 | timeoutkey = setTimeout(function() { 644 | global.onerror = orgOnerror; 645 | timeoutkey = null; 646 | }, 50); 647 | } 648 | throw error; 649 | } 650 | }; 651 | }; 652 | 653 | var catArgs = function(foo) { 654 | return function() { 655 | var arg, args = []; 656 | for (var i = 0, l = arguments.length; i < l; i++) { 657 | arg = arguments[i]; 658 | _isFunction(arg) && (arg = cat(arg)); 659 | args.push(arg); 660 | } 661 | return foo.apply(this, args); 662 | }; 663 | }; 664 | 665 | var catTimeout = function(foo) { 666 | return function(cb, timeout) { 667 | // for setTimeout(string, delay) 668 | if (typeof cb === "string") { 669 | try { 670 | cb = new Function(cb); 671 | } catch (err) { 672 | throw err; 673 | } 674 | } 675 | var args = [].slice.call(arguments, 2); 676 | // for setTimeout(function, delay, param1, ...) 677 | cb = cat(cb, args.length && args); 678 | return foo(cb, timeout); 679 | }; 680 | }; 681 | 682 | /** 683 | * makeArgsTry 684 | * wrap a function's arguments with try & catch 685 | * @param {Function} foo 686 | * @param {Object} self 687 | * @returns {Function} 688 | */ 689 | var makeArgsTry = function(foo, self) { 690 | return function() { 691 | var arg, tmp, args = []; 692 | for (var i = 0, l = arguments.length; i < l; i++) { 693 | arg = arguments[i]; 694 | if (_isFunction(arg)) { 695 | if (arg.tryWrap) { 696 | arg = arg.tryWrap; 697 | } else { 698 | tmp = cat(arg); 699 | arg.tryWrap = tmp; 700 | arg = tmp; 701 | } 702 | } 703 | args.push(arg); 704 | } 705 | return foo.apply(self || this, args); 706 | }; 707 | }; 708 | 709 | /** 710 | * makeObjTry 711 | * wrap a object's all value with try & catch 712 | * @param {Function} foo 713 | * @param {Object} self 714 | * @returns {Function} 715 | */ 716 | var makeObjTry = function(obj) { 717 | var key, value; 718 | for (key in obj) { 719 | value = obj[key]; 720 | if (_isFunction(value)) obj[key] = cat(value); 721 | } 722 | return obj; 723 | }; 724 | 725 | /** 726 | * wrap jquery async function ,exp : event.add , event.remove , ajax 727 | * @returns {Function} 728 | */ 729 | tryJs.spyJquery = function() { 730 | var _$ = global.$; 731 | 732 | if (!_$ || !_$.event) { 733 | return tryJs; 734 | } 735 | 736 | var _add, _remove; 737 | if (_$.zepto) { 738 | _add = _$.fn.on, _remove = _$.fn.off; 739 | 740 | _$.fn.on = makeArgsTry(_add); 741 | _$.fn.off = function() { 742 | var arg, args = []; 743 | for (var i = 0, l = arguments.length; i < l; i++) { 744 | arg = arguments[i]; 745 | _isFunction(arg) && arg.tryWrap && (arg = arg.tryWrap); 746 | args.push(arg); 747 | } 748 | return _remove.apply(this, args); 749 | }; 750 | 751 | } else if (window.jQuery) { 752 | _add = _$.event.add, _remove = _$.event.remove; 753 | 754 | _$.event.add = makeArgsTry(_add); 755 | _$.event.remove = function() { 756 | var arg, args = []; 757 | for (var i = 0, l = arguments.length; i < l; i++) { 758 | arg = arguments[i]; 759 | _isFunction(arg) && arg.tryWrap && (arg = arg.tryWrap); 760 | args.push(arg); 761 | } 762 | return _remove.apply(this, args); 763 | }; 764 | } 765 | 766 | var _ajax = _$.ajax; 767 | 768 | if (_ajax) { 769 | _$.ajax = function(url, setting) { 770 | if (!setting) { 771 | setting = url; 772 | url = undefined; 773 | } 774 | makeObjTry(setting); 775 | if (url) return _ajax.call(_$, url, setting); 776 | return _ajax.call(_$, setting); 777 | }; 778 | } 779 | 780 | return tryJs; 781 | }; 782 | 783 | /** 784 | * wrap amd or commonjs of function ,exp : define , require , 785 | * @returns {Function} 786 | */ 787 | tryJs.spyModules = function() { 788 | var _require = global.require, 789 | _define = global.define; 790 | if (_define && _define.amd && _require) { 791 | global.require = catArgs(_require); 792 | _merge(global.require, _require); 793 | global.define = catArgs(_define); 794 | _merge(global.define, _define); 795 | } 796 | 797 | if (global.seajs && _define) { 798 | global.define = function() { 799 | var arg, args = []; 800 | for (var i = 0, l = arguments.length; i < l; i++) { 801 | arg = arguments[i]; 802 | if (_isFunction(arg)) { 803 | arg = cat(arg); 804 | //seajs should use toString parse dependencies , so rewrite it 805 | arg.toString = (function(orgArg) { 806 | return function() { 807 | return orgArg.toString(); 808 | }; 809 | }(arguments[i])); 810 | } 811 | args.push(arg); 812 | } 813 | return _define.apply(this, args); 814 | }; 815 | 816 | global.seajs.use = catArgs(global.seajs.use); 817 | 818 | _merge(global.define, _define); 819 | } 820 | 821 | return tryJs; 822 | }; 823 | 824 | /** 825 | * wrap async of function in window , exp : setTimeout , setInterval 826 | * @returns {Function} 827 | */ 828 | tryJs.spySystem = function() { 829 | global.setTimeout = catTimeout(global.setTimeout); 830 | global.setInterval = catTimeout(global.setInterval); 831 | return tryJs; 832 | }; 833 | 834 | /** 835 | * wrap custom of function , 836 | * @param obj - obj or function 837 | * @returns {Function} 838 | */ 839 | tryJs.spyCustom = function(obj) { 840 | if (_isFunction(obj)) { 841 | return cat(obj); 842 | } else { 843 | return makeObjTry(obj); 844 | } 845 | }; 846 | 847 | /** 848 | * run spyJquery() and spyModules() and spySystem() 849 | * @returns {Function} 850 | */ 851 | tryJs.spyAll = function() { 852 | tryJs 853 | .spyJquery() 854 | .spyModules() 855 | .spySystem(); 856 | return tryJs; 857 | }; 858 | 859 | }(window)); 860 | -------------------------------------------------------------------------------- /dist/bj-report-tryjs.min.js: -------------------------------------------------------------------------------- 1 | var BJ_REPORT=function(u){if(u.BJ_REPORT)return u.BJ_REPORT;var f=[],t={},l={id:0,uin:0,url:"",offline_url:"",offline_auto_url:"",ext:null,level:4,ignore:[],random:1,delay:1e3,submit:null,repeat:5,offlineLog:!1,offlineLogExp:5,offlineLogAuto:!1},c={db:null,ready:function(n){var t=this;if(!window.indexedDB||!l.offlineLog)return l.offlineLog=!1,n();if(this.db)setTimeout(function(){n(null,t)},0);else{var e=window.indexedDB.open("badjs",1);if(!e)return l.offlineLog=!1,n();e.onerror=function(e){return n(e),l.offlineLog=!1,console.log("indexdb request error"),!0},e.onsuccess=function(e){t.db=e.target.result,setTimeout(function(){n(null,t)},500)},e.onupgradeneeded=function(e){var n=e.target.result;n.objectStoreNames.contains("logs")||n.createObjectStore("logs",{autoIncrement:!0})}}},insertToDB:function(e){this.getStore().add(e)},addLog:function(e){this.db&&this.insertToDB(e)},addLogs:function(e){if(this.db)for(var n=0;n=t.start&&n.value.time<=t.end&&n.value.id==t.id&&n.value.uin==t.uin&&o.push(n.value),n.continue()):r(null,o)},e.onerror=function(e){return r(e),!0}}},clearDB:function(e){if(this.db){var t=this.getStore();if(e){var r=Date.now()-24*(e||2)*3600*1e3;t.openCursor().onsuccess=function(e){var n=e.target.result;n&&(n.value.timel.repeat}},s=u.onerror;u.onerror=function(e,n,t,r,o){var i=e;o&&o.stack&&(i=d.processStackMsg(o)),d.isOBJByType(i,"Event")&&(i+=i.type?"--"+i.type+"--"+(i.target?i.target.tagName+"::"+i.target.src:""):""),y.push({msg:i,target:n,rowNum:t,colNum:r,_orgMsg:e}),a(),s&&s.apply(u,arguments)};var p=function(e,n){var t=[],r=[],o=[];if(d.isOBJ(e))for(var i in e.level=e.level||l.level,e){var u=e[i];if(!d.isEmpty(u)){if(d.isOBJ(u))try{u=JSON.stringify(u)}catch(e){u="[BJ_REPORT detect value stringify error] "+e.toString()}o.push(i+":"+u),t.push(i+"="+encodeURIComponent(u)),r.push(i+"["+n+"]="+encodeURIComponent(u))}}return[r.join("&"),o.join(","),t.join("&")]},g=[],m=[],v=0,h=function(){if(clearTimeout(v),v=0,m.length){var e=l._reportUrl+m.join("&")+"&count="+m.length+"&_t="+ +new Date;if(l.submit)l.submit(e,m);else(new Image).src=e;m=[]}},a=function(e){if(l._reportUrl){for(var n,t=Math.random()>=l.random;f.length;){var r=!1,o=f.shift();if(o.msg=(o.msg+""||"").substr(0,500),!d.isRepeat(o)){var i=p(o,m.length);if(d.isOBJByType(l.ignore,"Array"))for(var u=0,s=l.ignore.length;u= opt.start && cursor.value.time <= opt.end && cursor.value.id == opt.id && cursor.value.uin == opt.uin) { 108 | result.push(cursor.value); 109 | } 110 | //# cursor.continue 111 | cursor["continue"](); 112 | } else { 113 | callback(null, result); 114 | } 115 | }; 116 | 117 | request.onerror = function(e) { 118 | callback(e); 119 | return true; 120 | }; 121 | }, 122 | clearDB: function(daysToMaintain) { 123 | if (!this.db) { 124 | return; 125 | } 126 | 127 | var store = this.getStore(); 128 | if (!daysToMaintain) { 129 | store.clear(); 130 | return; 131 | } 132 | var range = (Date.now() - (daysToMaintain || 2) * 24 * 3600 * 1000); 133 | var request = store.openCursor(); 134 | request.onsuccess = function(event) { 135 | var cursor = event.target.result; 136 | if (cursor && (cursor.value.time < range || !cursor.value.time)) { 137 | store["delete"](cursor.primaryKey); 138 | cursor["continue"](); 139 | } 140 | }; 141 | }, 142 | 143 | getStore: function() { 144 | var transaction = this.db.transaction("logs", 'readwrite'); 145 | return transaction.objectStore("logs"); 146 | }, 147 | 148 | }; 149 | 150 | var T = { 151 | isOBJByType: function(o, type) { 152 | return Object.prototype.toString.call(o) === "[object " + (type || "Object") + "]"; 153 | }, 154 | 155 | isOBJ: function(obj) { 156 | var type = typeof obj; 157 | return type === "object" && !!obj; 158 | }, 159 | isEmpty: function(obj) { 160 | if (obj === null) return true; 161 | if (T.isOBJByType(obj, "Number")) { 162 | return false; 163 | } 164 | return !obj; 165 | }, 166 | extend: function(src, source) { 167 | for (var key in source) { 168 | src[key] = source[key]; 169 | } 170 | return src; 171 | }, 172 | processError: function(errObj) { 173 | try { 174 | if (errObj.stack) { 175 | var url = errObj.stack.match("https?://[^\n]+"); 176 | url = url ? url[0] : ""; 177 | var rowCols = url.match(":(\\d+):(\\d+)"); 178 | if (!rowCols) { 179 | rowCols = [0, 0, 0]; 180 | } 181 | 182 | var stack = T.processStackMsg(errObj); 183 | return { 184 | msg: stack, 185 | rowNum: rowCols[1], 186 | colNum: rowCols[2], 187 | target: url.replace(rowCols[0], ""), 188 | _orgMsg: errObj.toString() 189 | }; 190 | } else { 191 | //ie 独有 error 对象信息,try-catch 捕获到错误信息传过来,造成没有msg 192 | if (errObj.name && errObj.message && errObj.description) { 193 | return { 194 | msg: JSON.stringify(errObj) 195 | }; 196 | } 197 | return errObj; 198 | } 199 | } catch (err) { 200 | return errObj; 201 | } 202 | }, 203 | 204 | processStackMsg: function(error) { 205 | var stack = error.stack 206 | .replace(/\n/gi, "") 207 | .split(/\bat\b/) 208 | .slice(0, 9) 209 | .join("@") 210 | .replace(/\?[^:]+/gi, ""); 211 | var msg = error.toString(); 212 | if (stack.indexOf(msg) < 0) { 213 | stack = msg + "@" + stack; 214 | } 215 | return stack; 216 | }, 217 | 218 | isRepeat: function(error) { 219 | if (!T.isOBJ(error)) return true; 220 | var msg = error.msg; 221 | var times = _log_map[msg] = (parseInt(_log_map[msg], 10) || 0) + 1; 222 | return times > _config.repeat; 223 | } 224 | }; 225 | 226 | var orgError = global.onerror; 227 | // rewrite window.oerror 228 | global.onerror = function(msg, url, line, col, error) { 229 | var newMsg = msg; 230 | 231 | if (error && error.stack) { 232 | newMsg = T.processStackMsg(error); 233 | } 234 | 235 | if (T.isOBJByType(newMsg, "Event")) { 236 | newMsg += newMsg.type ? 237 | ("--" + newMsg.type + "--" + (newMsg.target ? 238 | (newMsg.target.tagName + "::" + newMsg.target.src) : "")) : ""; 239 | } 240 | 241 | report.push({ 242 | msg: newMsg, 243 | target: url, 244 | rowNum: line, 245 | colNum: col, 246 | _orgMsg: msg 247 | }); 248 | 249 | _process_log(); 250 | orgError && orgError.apply(global, arguments); 251 | }; 252 | 253 | 254 | 255 | var _report_log_tostring = function(error, index) { 256 | var param = []; 257 | var params = []; 258 | var stringify = []; 259 | if (T.isOBJ(error)) { 260 | error.level = error.level || _config.level; 261 | for (var key in error) { 262 | var value = error[key]; 263 | if (!T.isEmpty(value)) { 264 | if (T.isOBJ(value)) { 265 | try { 266 | value = JSON.stringify(value); 267 | } catch (err) { 268 | value = "[BJ_REPORT detect value stringify error] " + err.toString(); 269 | } 270 | } 271 | stringify.push(key + ":" + value); 272 | param.push(key + "=" + encodeURIComponent(value)); 273 | params.push(key + "[" + index + "]=" + encodeURIComponent(value)); 274 | } 275 | } 276 | } 277 | 278 | // msg[0]=msg&target[0]=target -- combo report 279 | // msg:msg,target:target -- ignore 280 | // msg=msg&target=target -- report with out combo 281 | return [params.join("&"), stringify.join(","), param.join("&")]; 282 | }; 283 | 284 | 285 | 286 | var _offline_buffer = []; 287 | var _save2Offline = function(key, msgObj) { 288 | msgObj = T.extend({ id: _config.id, uin: _config.uin, time: new Date - 0 }, msgObj); 289 | 290 | if (Offline_DB.db) { 291 | Offline_DB.addLog(msgObj); 292 | return; 293 | } 294 | 295 | 296 | if (!Offline_DB.db && !_offline_buffer.length) { 297 | Offline_DB.ready(function(err, DB) { 298 | if (DB) { 299 | if (_offline_buffer.length) { 300 | DB.addLogs(_offline_buffer); 301 | _offline_buffer = []; 302 | } 303 | 304 | } 305 | }); 306 | } 307 | _offline_buffer.push(msgObj); 308 | }; 309 | 310 | var _autoReportOffline = function() { 311 | var script = document.createElement("script"); 312 | script.src = _config.offline_auto_url || _config.url.replace(/badjs$/, "offlineAuto") + "?id=" + _config.id + "&uin=" + _config.uin; 313 | window._badjsOfflineAuto = function(isReport) { 314 | if (isReport) { 315 | BJ_REPORT.reportOfflineLog(); 316 | } 317 | }; 318 | document.head.appendChild(script); 319 | }; 320 | 321 | 322 | 323 | var submit_log_list = []; 324 | var comboTimeout = 0; 325 | var _submit_log = function() { 326 | clearTimeout(comboTimeout); 327 | // https://github.com/BetterJS/badjs-report/issues/34 328 | comboTimeout = 0; 329 | 330 | if (!submit_log_list.length) { 331 | return; 332 | } 333 | 334 | var url = _config._reportUrl + submit_log_list.join("&") + "&count=" + submit_log_list.length + "&_t=" + (+new Date); 335 | 336 | if (_config.submit) { 337 | _config.submit(url, submit_log_list); 338 | } else { 339 | var _img = new Image(); 340 | _img.src = url; 341 | } 342 | 343 | submit_log_list = []; 344 | }; 345 | 346 | var _process_log = function(isReportNow) { 347 | if (!_config._reportUrl) return; 348 | 349 | var randomIgnore = Math.random() >= _config.random; 350 | 351 | 352 | while (_log_list.length) { 353 | var isIgnore = false; 354 | var report_log = _log_list.shift(); 355 | //有效保证字符不要过长 356 | report_log.msg = (report_log.msg + "" || "").substr(0, 500); 357 | // 重复上报 358 | if (T.isRepeat(report_log)) continue; 359 | var log_str = _report_log_tostring(report_log, submit_log_list.length); 360 | if (T.isOBJByType(_config.ignore, "Array")) { 361 | for (var i = 0, l = _config.ignore.length; i < l; i++) { 362 | var rule = _config.ignore[i]; 363 | if ((T.isOBJByType(rule, "RegExp") && rule.test(log_str[1])) || 364 | (T.isOBJByType(rule, "Function") && rule(report_log, log_str[1]))) { 365 | isIgnore = true; 366 | break; 367 | } 368 | } 369 | } 370 | if (!isIgnore) { 371 | _config.offlineLog && _save2Offline("badjs_" + _config.id + _config.uin, report_log); 372 | if (!randomIgnore && report_log.level != 20) { 373 | submit_log_list.push(log_str[0]); 374 | _config.onReport && (_config.onReport(_config.id, report_log)); 375 | } 376 | 377 | } 378 | } 379 | 380 | 381 | if (isReportNow) { 382 | _submit_log(); // 立即上报 383 | } else if (!comboTimeout) { 384 | comboTimeout = setTimeout(_submit_log, _config.delay); // 延迟上报 385 | } 386 | }; 387 | 388 | 389 | 390 | var report = global.BJ_REPORT = { 391 | push: function(msg) { // 将错误推到缓存池 392 | 393 | var data = T.isOBJ(msg) ? T.processError(msg) : { 394 | msg: msg 395 | }; 396 | 397 | // ext 有默认值, 且上报不包含 ext, 使用默认 ext 398 | if (_config.ext && !data.ext) { 399 | data.ext = _config.ext; 400 | } 401 | // 在错误发生时获取页面链接 402 | // https://github.com/BetterJS/badjs-report/issues/19 403 | if (!data.from) { 404 | data.from = location.href; 405 | } 406 | 407 | if (data._orgMsg) { 408 | var _orgMsg = data._orgMsg; 409 | delete data._orgMsg; 410 | data.level = 2; 411 | var newData = T.extend({}, data); 412 | newData.level = 4; 413 | newData.msg = _orgMsg; 414 | _log_list.push(data); 415 | _log_list.push(newData); 416 | } else { 417 | _log_list.push(data); 418 | } 419 | 420 | _process_log(); 421 | return report; 422 | }, 423 | report: function(msg, isReportNow) { // error report 424 | msg && report.push(msg); 425 | 426 | isReportNow && _process_log(true); 427 | return report; 428 | }, 429 | info: function(msg) { // info report 430 | if (!msg) { 431 | return report; 432 | } 433 | if (T.isOBJ(msg)) { 434 | msg.level = 2; 435 | } else { 436 | msg = { 437 | msg: msg, 438 | level: 2 439 | }; 440 | } 441 | report.push(msg); 442 | return report; 443 | }, 444 | debug: function(msg) { // debug report 445 | if (!msg) { 446 | return report; 447 | } 448 | if (T.isOBJ(msg)) { 449 | msg.level = 1; 450 | } else { 451 | msg = { 452 | msg: msg, 453 | level: 1 454 | }; 455 | } 456 | report.push(msg); 457 | return report; 458 | }, 459 | 460 | reportOfflineLog: function() { 461 | if (!window.indexedDB) { 462 | BJ_REPORT.info("unsupport offlineLog"); 463 | return; 464 | } 465 | Offline_DB.ready(function(err, DB) { 466 | if (!DB) { 467 | return; 468 | } 469 | var startDate = new Date - 0 - _config.offlineLogExp * 24 * 3600 * 1000; 470 | var endDate = new Date - 0; 471 | DB.getLogs({ 472 | start: startDate, 473 | end: endDate, 474 | id: _config.id, 475 | uin: _config.uin 476 | }, function(err, result) { 477 | var iframe = document.createElement("iframe"); 478 | iframe.name = "badjs_offline_" + (new Date - 0); 479 | iframe.frameborder = 0; 480 | iframe.height = 0; 481 | iframe.width = 0; 482 | iframe.src = "javascript:false;"; 483 | 484 | iframe.onload = function() { 485 | var form = document.createElement("form"); 486 | form.style.display = "none"; 487 | form.target = iframe.name; 488 | form.method = "POST"; 489 | form.action = _config.offline_url || _config.url.replace(/badjs$/, "offlineLog"); 490 | form.enctype.method = 'multipart/form-data'; 491 | 492 | var input = document.createElement("input"); 493 | input.style.display = "none"; 494 | input.type = "hidden"; 495 | input.name = "offline_log"; 496 | input.value = JSON.stringify({ logs: result, userAgent: navigator.userAgent, startDate: startDate, endDate: endDate, id: _config.id, uin: _config.uin }); 497 | 498 | iframe.contentDocument.body.appendChild(form); 499 | form.appendChild(input); 500 | form.submit(); 501 | 502 | setTimeout(function() { 503 | document.body.removeChild(iframe); 504 | }, 10000); 505 | 506 | iframe.onload = null; 507 | }; 508 | document.body.appendChild(iframe); 509 | }); 510 | }); 511 | }, 512 | offlineLog: function(msg) { 513 | if (!msg) { 514 | return report; 515 | } 516 | if (T.isOBJ(msg)) { 517 | msg.level = 20; 518 | } else { 519 | msg = { 520 | msg: msg, 521 | level: 20 522 | }; 523 | } 524 | report.push(msg); 525 | return report; 526 | }, 527 | init: function(config) { // 初始化 528 | if (T.isOBJ(config)) { 529 | for (var key in config) { 530 | _config[key] = config[key]; 531 | } 532 | } 533 | // 没有设置id将不上报 534 | var id = parseInt(_config.id, 10); 535 | if (id) { 536 | // set default report url and uin 537 | if (/qq\.com$/gi.test(location.hostname)) { 538 | if (!_config.url) { 539 | _config.url = "//badjs2.qq.com/badjs"; 540 | } 541 | 542 | if (!_config.uin) { 543 | _config.uin = parseInt((document.cookie.match(/\buin=\D+(\d+)/) || [])[1], 10); 544 | } 545 | } 546 | 547 | _config._reportUrl = (_config.url || "/badjs") + 548 | "?id=" + id + 549 | "&uin=" + _config.uin + 550 | // "&from=" + encodeURIComponent(location.href) + 551 | "&"; 552 | } 553 | 554 | // if had error in cache , report now 555 | if (_log_list.length) { 556 | _process_log(); 557 | } 558 | 559 | // init offline 560 | if (!Offline_DB._initing) { 561 | Offline_DB._initing = true; 562 | Offline_DB.ready(function(err, DB) { 563 | if (DB) { 564 | setTimeout(function() { 565 | DB.clearDB(_config.offlineLogExp); 566 | setTimeout(function() { 567 | _config.offlineLogAuto && _autoReportOffline(); 568 | }, 5000); 569 | }, 1000); 570 | } 571 | 572 | }); 573 | } 574 | 575 | 576 | 577 | return report; 578 | }, 579 | 580 | __onerror__: global.onerror 581 | }; 582 | 583 | typeof console !== "undefined" && console.error && setTimeout(function() { 584 | var err = ((location.hash || "").match(/([#&])BJ_ERROR=([^&$]+)/) || [])[2]; 585 | err && console.error("BJ_ERROR", decodeURIComponent(err).replace(/(:\d+:\d+)\s*/g, "$1\n")); 586 | }, 0); 587 | 588 | return report; 589 | 590 | }(window)); 591 | 592 | if (typeof module !== "undefined") { 593 | module.exports = BJ_REPORT; 594 | } 595 | -------------------------------------------------------------------------------- /dist/bj-report.min.js: -------------------------------------------------------------------------------- 1 | var BJ_REPORT=function(u){if(u.BJ_REPORT)return u.BJ_REPORT;var l=[],t={},f={id:0,uin:0,url:"",offline_url:"",offline_auto_url:"",ext:null,level:4,ignore:[],random:1,delay:1e3,submit:null,repeat:5,offlineLog:!1,offlineLogExp:5,offlineLogAuto:!1},c={db:null,ready:function(n){var t=this;if(!window.indexedDB||!f.offlineLog)return f.offlineLog=!1,n();if(this.db)setTimeout(function(){n(null,t)},0);else{var e=window.indexedDB.open("badjs",1);if(!e)return f.offlineLog=!1,n();e.onerror=function(e){return n(e),f.offlineLog=!1,console.log("indexdb request error"),!0},e.onsuccess=function(e){t.db=e.target.result,setTimeout(function(){n(null,t)},500)},e.onupgradeneeded=function(e){var n=e.target.result;n.objectStoreNames.contains("logs")||n.createObjectStore("logs",{autoIncrement:!0})}}},insertToDB:function(e){this.getStore().add(e)},addLog:function(e){this.db&&this.insertToDB(e)},addLogs:function(e){if(this.db)for(var n=0;n=t.start&&n.value.time<=t.end&&n.value.id==t.id&&n.value.uin==t.uin&&r.push(n.value),n.continue()):o(null,r)},e.onerror=function(e){return o(e),!0}}},clearDB:function(e){if(this.db){var t=this.getStore();if(e){var o=Date.now()-24*(e||2)*3600*1e3;t.openCursor().onsuccess=function(e){var n=e.target.result;n&&(n.value.timef.repeat}},s=u.onerror;u.onerror=function(e,n,t,o,r){var i=e;r&&r.stack&&(i=d.processStackMsg(r)),d.isOBJByType(i,"Event")&&(i+=i.type?"--"+i.type+"--"+(i.target?i.target.tagName+"::"+i.target.src:""):""),y.push({msg:i,target:n,rowNum:t,colNum:o,_orgMsg:e}),a(),s&&s.apply(u,arguments)};var g=function(e,n){var t=[],o=[],r=[];if(d.isOBJ(e))for(var i in e.level=e.level||f.level,e){var u=e[i];if(!d.isEmpty(u)){if(d.isOBJ(u))try{u=JSON.stringify(u)}catch(e){u="[BJ_REPORT detect value stringify error] "+e.toString()}r.push(i+":"+u),t.push(i+"="+encodeURIComponent(u)),o.push(i+"["+n+"]="+encodeURIComponent(u))}}return[o.join("&"),r.join(","),t.join("&")]},p=[],m=[],v=0,h=function(){if(clearTimeout(v),v=0,m.length){var e=f._reportUrl+m.join("&")+"&count="+m.length+"&_t="+ +new Date;if(f.submit)f.submit(e,m);else(new Image).src=e;m=[]}},a=function(e){if(f._reportUrl){for(var n,t=Math.random()>=f.random;l.length;){var o=!1,r=l.shift();if(r.msg=(r.msg+""||"").substr(0,500),!d.isRepeat(r)){var i=g(r,m.length);if(d.isOBJByType(f.ignore,"Array"))for(var u=0,s=f.ignore.length;u 2 | 3 | 4 | 5 | example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /example/index-for-seajs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | example 6 | 7 | 8 | 9 | 10 | 11 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /example/report.js: -------------------------------------------------------------------------------- 1 | 2 | define(function (require, exports, module){ 3 | 4 | module.exports ={ 5 | init : function (){ 6 | bbbb+++1 7 | } 8 | } 9 | }); 10 | 11 | s13cc(); 12 | -------------------------------------------------------------------------------- /example/sea-debug.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sea.js 3.0.0 | seajs.org/LICENSE.md 3 | */ 4 | (function(global, undefined) { 5 | 6 | // Avoid conflicting when `sea.js` is loaded multiple times 7 | if (global.seajs) { 8 | return 9 | } 10 | 11 | var seajs = global.seajs = { 12 | // The current version of Sea.js being used 13 | version: "3.0.0" 14 | } 15 | 16 | var data = seajs.data = {} 17 | 18 | 19 | /** 20 | * util-lang.js - The minimal language enhancement 21 | */ 22 | 23 | function isType(type) { 24 | return function(obj) { 25 | return {}.toString.call(obj) == "[object " + type + "]" 26 | } 27 | } 28 | 29 | var isObject = isType("Object") 30 | var isString = isType("String") 31 | var isArray = Array.isArray || isType("Array") 32 | var isFunction = isType("Function") 33 | 34 | var _cid = 0 35 | function cid() { 36 | return _cid++ 37 | } 38 | 39 | 40 | /** 41 | * util-events.js - The minimal events support 42 | */ 43 | 44 | var events = data.events = {} 45 | 46 | // Bind event 47 | seajs.on = function(name, callback) { 48 | var list = events[name] || (events[name] = []) 49 | list.push(callback) 50 | return seajs 51 | } 52 | 53 | // Remove event. If `callback` is undefined, remove all callbacks for the 54 | // event. If `event` and `callback` are both undefined, remove all callbacks 55 | // for all events 56 | seajs.off = function(name, callback) { 57 | // Remove *all* events 58 | if (!(name || callback)) { 59 | events = data.events = {} 60 | return seajs 61 | } 62 | 63 | var list = events[name] 64 | if (list) { 65 | if (callback) { 66 | for (var i = list.length - 1; i >= 0; i--) { 67 | if (list[i] === callback) { 68 | list.splice(i, 1) 69 | } 70 | } 71 | } 72 | else { 73 | delete events[name] 74 | } 75 | } 76 | 77 | return seajs 78 | } 79 | 80 | // Emit event, firing all bound callbacks. Callbacks receive the same 81 | // arguments as `emit` does, apart from the event name 82 | var emit = seajs.emit = function(name, data) { 83 | var list = events[name] 84 | 85 | if (list) { 86 | // Copy callback lists to prevent modification 87 | list = list.slice() 88 | 89 | // Execute event callbacks, use index because it's the faster. 90 | for(var i = 0, len = list.length; i < len; i++) { 91 | list[i](data) 92 | } 93 | } 94 | 95 | return seajs 96 | } 97 | 98 | /** 99 | * util-path.js - The utilities for operating path such as id, uri 100 | */ 101 | 102 | var DIRNAME_RE = /[^?#]*\// 103 | 104 | var DOT_RE = /\/\.\//g 105 | var DOUBLE_DOT_RE = /\/[^/]+\/\.\.\// 106 | var MULTI_SLASH_RE = /([^:/])\/+\//g 107 | 108 | // Extract the directory portion of a path 109 | // dirname("a/b/c.js?t=123#xx/zz") ==> "a/b/" 110 | // ref: http://jsperf.com/regex-vs-split/2 111 | function dirname(path) { 112 | return path.match(DIRNAME_RE)[0] 113 | } 114 | 115 | // Canonicalize a path 116 | // realpath("http://test.com/a//./b/../c") ==> "http://test.com/a/c" 117 | function realpath(path) { 118 | // /a/b/./c/./d ==> /a/b/c/d 119 | path = path.replace(DOT_RE, "/") 120 | 121 | /* 122 | @author wh1100717 123 | a//b/c ==> a/b/c 124 | a///b/////c ==> a/b/c 125 | DOUBLE_DOT_RE matches a/b/c//../d path correctly only if replace // with / first 126 | */ 127 | path = path.replace(MULTI_SLASH_RE, "$1/") 128 | 129 | // a/b/c/../../d ==> a/b/../d ==> a/d 130 | while (path.match(DOUBLE_DOT_RE)) { 131 | path = path.replace(DOUBLE_DOT_RE, "/") 132 | } 133 | 134 | return path 135 | } 136 | 137 | // Normalize an id 138 | // normalize("path/to/a") ==> "path/to/a.js" 139 | // NOTICE: substring is faster than negative slice and RegExp 140 | function normalize(path) { 141 | var last = path.length - 1 142 | var lastC = path.charCodeAt(last) 143 | 144 | // If the uri ends with `#`, just return it without '#' 145 | if (lastC === 35 /* "#" */) { 146 | return path.substring(0, last) 147 | } 148 | 149 | return (path.substring(last - 2) === ".js" || 150 | path.indexOf("?") > 0 || 151 | lastC === 47 /* "/" */) ? path : path + ".js" 152 | } 153 | 154 | 155 | var PATHS_RE = /^([^/:]+)(\/.+)$/ 156 | var VARS_RE = /{([^{]+)}/g 157 | 158 | function parseAlias(id) { 159 | var alias = data.alias 160 | return alias && isString(alias[id]) ? alias[id] : id 161 | } 162 | 163 | function parsePaths(id) { 164 | var paths = data.paths 165 | var m 166 | 167 | if (paths && (m = id.match(PATHS_RE)) && isString(paths[m[1]])) { 168 | id = paths[m[1]] + m[2] 169 | } 170 | 171 | return id 172 | } 173 | 174 | function parseVars(id) { 175 | var vars = data.vars 176 | 177 | if (vars && id.indexOf("{") > -1) { 178 | id = id.replace(VARS_RE, function(m, key) { 179 | return isString(vars[key]) ? vars[key] : m 180 | }) 181 | } 182 | 183 | return id 184 | } 185 | 186 | function parseMap(uri) { 187 | var map = data.map 188 | var ret = uri 189 | 190 | if (map) { 191 | for (var i = 0, len = map.length; i < len; i++) { 192 | var rule = map[i] 193 | 194 | ret = isFunction(rule) ? 195 | (rule(uri) || uri) : 196 | uri.replace(rule[0], rule[1]) 197 | 198 | // Only apply the first matched rule 199 | if (ret !== uri) break 200 | } 201 | } 202 | 203 | return ret 204 | } 205 | 206 | 207 | var ABSOLUTE_RE = /^\/\/.|:\// 208 | var ROOT_DIR_RE = /^.*?\/\/.*?\// 209 | 210 | function addBase(id, refUri) { 211 | var ret 212 | var first = id.charCodeAt(0) 213 | 214 | // Absolute 215 | if (ABSOLUTE_RE.test(id)) { 216 | ret = id 217 | } 218 | // Relative 219 | else if (first === 46 /* "." */) { 220 | ret = (refUri ? dirname(refUri) : data.cwd) + id 221 | } 222 | // Root 223 | else if (first === 47 /* "/" */) { 224 | var m = data.cwd.match(ROOT_DIR_RE) 225 | ret = m ? m[0] + id.substring(1) : id 226 | } 227 | // Top-level 228 | else { 229 | ret = data.base + id 230 | } 231 | 232 | // Add default protocol when uri begins with "//" 233 | if (ret.indexOf("//") === 0) { 234 | ret = location.protocol + ret 235 | } 236 | 237 | return realpath(ret) 238 | } 239 | 240 | function id2Uri(id, refUri) { 241 | if (!id) return "" 242 | 243 | id = parseAlias(id) 244 | id = parsePaths(id) 245 | id = parseAlias(id) 246 | id = parseVars(id) 247 | id = parseAlias(id) 248 | id = normalize(id) 249 | id = parseAlias(id) 250 | 251 | var uri = addBase(id, refUri) 252 | uri = parseAlias(uri) 253 | uri = parseMap(uri) 254 | 255 | return uri 256 | } 257 | 258 | // For Developers 259 | seajs.resolve = id2Uri; 260 | 261 | // Check environment 262 | var isWebWorker = typeof window === 'undefined' && typeof importScripts !== 'undefined' && isFunction(importScripts); 263 | 264 | // Ignore about:xxx and blob:xxx 265 | var IGNORE_LOCATION_RE = /^(about|blob):/; 266 | var loaderDir; 267 | // Sea.js's full path 268 | var loaderPath; 269 | // Location is read-only from web worker, should be ok though 270 | var cwd = (!location.href || IGNORE_LOCATION_RE.test(location.href)) ? '' : dirname(location.href); 271 | 272 | if (isWebWorker) { 273 | // Web worker doesn't create DOM object when loading scripts 274 | // Get sea.js's path by stack trace. 275 | var stack; 276 | try { 277 | var up = new Error(); 278 | throw up; 279 | } catch (e) { 280 | // IE won't set Error.stack until thrown 281 | stack = e.stack.split('\n'); 282 | } 283 | // First line is 'Error' 284 | stack.shift(); 285 | 286 | var m; 287 | // Try match `url:row:col` from stack trace line. Known formats: 288 | // Chrome: ' at http://localhost:8000/script/sea-worker-debug.js:294:25' 289 | // FireFox: '@http://localhost:8000/script/sea-worker-debug.js:1082:1' 290 | // IE11: ' at Anonymous function (http://localhost:8000/script/sea-worker-debug.js:295:5)' 291 | // Don't care about older browsers since web worker is an HTML5 feature 292 | var TRACE_RE = /.*?((?:http|https|file)(?::\/{2}[\w]+)(?:[\/|\.]?)(?:[^\s"]*)).*?/i 293 | // Try match `url` (Note: in IE there will be a tailing ')') 294 | var URL_RE = /(.*?):\d+:\d+\)?$/; 295 | // Find url of from stack trace. 296 | // Cannot simply read the first one because sometimes we will get: 297 | // Error 298 | // at Error (native) <- Here's your problem 299 | // at http://localhost:8000/_site/dist/sea.js:2:4334 <- What we want 300 | // at http://localhost:8000/_site/dist/sea.js:2:8386 301 | // at http://localhost:8000/_site/tests/specs/web-worker/worker.js:3:1 302 | while (stack.length > 0) { 303 | var top = stack.shift(); 304 | m = TRACE_RE.exec(top); 305 | if (m != null) { 306 | break; 307 | } 308 | } 309 | var url; 310 | if (m != null) { 311 | // Remove line number and column number 312 | // No need to check, can't be wrong at this point 313 | var url = URL_RE.exec(m[1])[1]; 314 | } 315 | // Set 316 | loaderPath = url 317 | // Set loaderDir 318 | loaderDir = dirname(url || cwd); 319 | // This happens with inline worker. 320 | // When entrance script's location.href is a blob url, 321 | // cwd will not be available. 322 | // Fall back to loaderDir. 323 | if (cwd === '') { 324 | cwd = loaderDir; 325 | } 326 | } 327 | else { 328 | var doc = document 329 | var scripts = doc.scripts 330 | 331 | // Recommend to add `seajsnode` id for the `sea.js` script element 332 | var loaderScript = doc.getElementById("seajsnode") || 333 | scripts[scripts.length - 1] 334 | 335 | function getScriptAbsoluteSrc(node) { 336 | return node.hasAttribute ? // non-IE6/7 337 | node.src : 338 | // see http://msdn.microsoft.com/en-us/library/ms536429(VS.85).aspx 339 | node.getAttribute("src", 4) 340 | } 341 | loaderPath = getScriptAbsoluteSrc(loaderScript) 342 | // When `sea.js` is inline, set loaderDir to current working directory 343 | loaderDir = dirname(loaderPath || cwd) 344 | } 345 | 346 | /** 347 | * util-request.js - The utilities for requesting script and style files 348 | * ref: tests/research/load-js-css/test.html 349 | */ 350 | if (isWebWorker) { 351 | function requestFromWebWorker(url, callback, charset) { 352 | // Load with importScripts 353 | var error; 354 | try { 355 | importScripts(url); 356 | } catch (e) { 357 | error = e; 358 | } 359 | callback(error); 360 | } 361 | // For Developers 362 | seajs.request = requestFromWebWorker; 363 | } 364 | else { 365 | var doc = document 366 | var head = doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement 367 | var baseElement = head.getElementsByTagName("base")[0] 368 | 369 | var currentlyAddingScript 370 | 371 | function request(url, callback, charset) { 372 | var node = doc.createElement("script") 373 | 374 | if (charset) { 375 | var cs = isFunction(charset) ? charset(url) : charset 376 | if (cs) { 377 | node.charset = cs 378 | } 379 | } 380 | 381 | addOnload(node, callback, url) 382 | 383 | node.async = true 384 | node.src = url 385 | 386 | // For some cache cases in IE 6-8, the script executes IMMEDIATELY after 387 | // the end of the insert execution, so use `currentlyAddingScript` to 388 | // hold current node, for deriving url in `define` call 389 | currentlyAddingScript = node 390 | 391 | // ref: #185 & http://dev.jquery.com/ticket/2709 392 | baseElement ? 393 | head.insertBefore(node, baseElement) : 394 | head.appendChild(node) 395 | 396 | currentlyAddingScript = null 397 | } 398 | 399 | function addOnload(node, callback, url) { 400 | var supportOnload = "onload" in node 401 | 402 | if (supportOnload) { 403 | node.onload = onload 404 | node.onerror = function() { 405 | emit("error", { uri: url, node: node }) 406 | onload(true) 407 | } 408 | } 409 | else { 410 | node.onreadystatechange = function() { 411 | if (/loaded|complete/.test(node.readyState)) { 412 | onload() 413 | } 414 | } 415 | } 416 | 417 | function onload(error) { 418 | // Ensure only run once and handle memory leak in IE 419 | node.onload = node.onerror = node.onreadystatechange = null 420 | 421 | // Remove the script to reduce memory leak 422 | if (!data.debug) { 423 | head.removeChild(node) 424 | } 425 | 426 | // Dereference the node 427 | node = null 428 | 429 | callback(error) 430 | } 431 | } 432 | 433 | // For Developers 434 | seajs.request = request 435 | 436 | } 437 | var interactiveScript 438 | 439 | function getCurrentScript() { 440 | if (currentlyAddingScript) { 441 | return currentlyAddingScript 442 | } 443 | 444 | // For IE6-9 browsers, the script onload event may not fire right 445 | // after the script is evaluated. Kris Zyp found that it 446 | // could query the script nodes and the one that is in "interactive" 447 | // mode indicates the current script 448 | // ref: http://goo.gl/JHfFW 449 | if (interactiveScript && interactiveScript.readyState === "interactive") { 450 | return interactiveScript 451 | } 452 | 453 | var scripts = head.getElementsByTagName("script") 454 | 455 | for (var i = scripts.length - 1; i >= 0; i--) { 456 | var script = scripts[i] 457 | if (script.readyState === "interactive") { 458 | interactiveScript = script 459 | return interactiveScript 460 | } 461 | } 462 | } 463 | 464 | /** 465 | * util-deps.js - The parser for dependencies 466 | * ref: tests/research/parse-dependencies/test.html 467 | * ref: https://github.com/seajs/searequire 468 | */ 469 | 470 | function parseDependencies(s) { 471 | if(s.indexOf('require') == -1) { 472 | return [] 473 | } 474 | var index = 0, peek, length = s.length, isReg = 1, modName = 0, parentheseState = 0, parentheseStack = [], res = [] 475 | while(index < length) { 476 | readch() 477 | if(isBlank()) { 478 | } 479 | else if(isQuote()) { 480 | dealQuote() 481 | isReg = 1 482 | } 483 | else if(peek == '/') { 484 | readch() 485 | if(peek == '/') { 486 | index = s.indexOf('\n', index) 487 | if(index == -1) { 488 | index = s.length 489 | } 490 | } 491 | else if(peek == '*') { 492 | index = s.indexOf('*/', index) 493 | if(index == -1) { 494 | index = length 495 | } 496 | else { 497 | index += 2 498 | } 499 | } 500 | else if(isReg) { 501 | dealReg() 502 | isReg = 0 503 | } 504 | else { 505 | index-- 506 | isReg = 1 507 | } 508 | } 509 | else if(isWord()) { 510 | dealWord() 511 | } 512 | else if(isNumber()) { 513 | dealNumber() 514 | } 515 | else if(peek == '(') { 516 | parentheseStack.push(parentheseState) 517 | isReg = 1 518 | } 519 | else if(peek == ')') { 520 | isReg = parentheseStack.pop() 521 | } 522 | else { 523 | isReg = peek != ']' 524 | modName = 0 525 | } 526 | } 527 | return res 528 | function readch() { 529 | peek = s.charAt(index++) 530 | } 531 | function isBlank() { 532 | return /\s/.test(peek) 533 | } 534 | function isQuote() { 535 | return peek == '"' || peek == "'" 536 | } 537 | function dealQuote() { 538 | var start = index 539 | var c = peek 540 | var end = s.indexOf(c, start) 541 | if(end == -1) { 542 | index = length 543 | } 544 | else if(s.charAt(end - 1) != '\\') { 545 | index = end + 1 546 | } 547 | else { 548 | while(index < length) { 549 | readch() 550 | if(peek == '\\') { 551 | index++ 552 | } 553 | else if(peek == c) { 554 | break 555 | } 556 | } 557 | } 558 | if(modName) { 559 | res.push(s.slice(start, index - 1)) 560 | modName = 0 561 | } 562 | } 563 | function dealReg() { 564 | index-- 565 | while(index < length) { 566 | readch() 567 | if(peek == '\\') { 568 | index++ 569 | } 570 | else if(peek == '/') { 571 | break 572 | } 573 | else if(peek == '[') { 574 | while(index < length) { 575 | readch() 576 | if(peek == '\\') { 577 | index++ 578 | } 579 | else if(peek == ']') { 580 | break 581 | } 582 | } 583 | } 584 | } 585 | } 586 | function isWord() { 587 | return /[a-z_$]/i.test(peek) 588 | } 589 | function dealWord() { 590 | var s2 = s.slice(index - 1) 591 | var r = /^[\w$]+/.exec(s2)[0] 592 | parentheseState = { 593 | 'if': 1, 594 | 'for': 1, 595 | 'while': 1, 596 | 'with': 1 597 | }[r] 598 | isReg = { 599 | 'break': 1, 600 | 'case': 1, 601 | 'continue': 1, 602 | 'debugger': 1, 603 | 'delete': 1, 604 | 'do': 1, 605 | 'else': 1, 606 | 'false': 1, 607 | 'if': 1, 608 | 'in': 1, 609 | 'instanceof': 1, 610 | 'return': 1, 611 | 'typeof': 1, 612 | 'void': 1 613 | }[r] 614 | modName = /^require\s*\(\s*(['"]).+?\1\s*\)/.test(s2) 615 | if(modName) { 616 | r = /^require\s*\(\s*['"]/.exec(s2)[0] 617 | index += r.length - 2 618 | } 619 | else { 620 | index += /^[\w$]+(?:\s*\.\s*[\w$]+)*/.exec(s2)[0].length - 1 621 | } 622 | } 623 | function isNumber() { 624 | return /\d/.test(peek) 625 | || peek == '.' && /\d/.test(s.charAt(index)) 626 | } 627 | function dealNumber() { 628 | var s2 = s.slice(index - 1) 629 | var r 630 | if(peek == '.') { 631 | r = /^\.\d+(?:E[+-]?\d*)?\s*/i.exec(s2)[0] 632 | } 633 | else if(/^0x[\da-f]*/i.test(s2)) { 634 | r = /^0x[\da-f]*\s*/i.exec(s2)[0] 635 | } 636 | else { 637 | r = /^\d+\.?\d*(?:E[+-]?\d*)?\s*/i.exec(s2)[0] 638 | } 639 | index += r.length - 1 640 | isReg = 0 641 | } 642 | } 643 | /** 644 | * module.js - The core of module loader 645 | */ 646 | 647 | var cachedMods = seajs.cache = {} 648 | var anonymousMeta 649 | 650 | var fetchingList = {} 651 | var fetchedList = {} 652 | var callbackList = {} 653 | 654 | var STATUS = Module.STATUS = { 655 | // 1 - The `module.uri` is being fetched 656 | FETCHING: 1, 657 | // 2 - The meta data has been saved to cachedMods 658 | SAVED: 2, 659 | // 3 - The `module.dependencies` are being loaded 660 | LOADING: 3, 661 | // 4 - The module are ready to execute 662 | LOADED: 4, 663 | // 5 - The module is being executed 664 | EXECUTING: 5, 665 | // 6 - The `module.exports` is available 666 | EXECUTED: 6, 667 | // 7 - 404 668 | ERROR: 7 669 | } 670 | 671 | 672 | function Module(uri, deps) { 673 | this.uri = uri 674 | this.dependencies = deps || [] 675 | this.deps = {} // Ref the dependence modules 676 | this.status = 0 677 | 678 | this._entry = [] 679 | } 680 | 681 | // Resolve module.dependencies 682 | Module.prototype.resolve = function() { 683 | var mod = this 684 | var ids = mod.dependencies 685 | var uris = [] 686 | 687 | for (var i = 0, len = ids.length; i < len; i++) { 688 | uris[i] = Module.resolve(ids[i], mod.uri) 689 | } 690 | return uris 691 | } 692 | 693 | Module.prototype.pass = function() { 694 | var mod = this 695 | 696 | var len = mod.dependencies.length 697 | 698 | for (var i = 0; i < mod._entry.length; i++) { 699 | var entry = mod._entry[i] 700 | var count = 0 701 | for (var j = 0; j < len; j++) { 702 | var m = mod.deps[mod.dependencies[j]] 703 | // If the module is unload and unused in the entry, pass entry to it 704 | if (m.status < STATUS.LOADED && !entry.history.hasOwnProperty(m.uri)) { 705 | entry.history[m.uri] = true 706 | count++ 707 | m._entry.push(entry) 708 | if(m.status === STATUS.LOADING) { 709 | m.pass() 710 | } 711 | } 712 | } 713 | // If has passed the entry to it's dependencies, modify the entry's count and del it in the module 714 | if (count > 0) { 715 | entry.remain += count - 1 716 | mod._entry.shift() 717 | i-- 718 | } 719 | } 720 | } 721 | 722 | // Load module.dependencies and fire onload when all done 723 | Module.prototype.load = function() { 724 | var mod = this 725 | 726 | // If the module is being loaded, just wait it onload call 727 | if (mod.status >= STATUS.LOADING) { 728 | return 729 | } 730 | 731 | mod.status = STATUS.LOADING 732 | 733 | // Emit `load` event for plugins such as combo plugin 734 | var uris = mod.resolve() 735 | emit("load", uris) 736 | 737 | for (var i = 0, len = uris.length; i < len; i++) { 738 | mod.deps[mod.dependencies[i]] = Module.get(uris[i]) 739 | } 740 | 741 | // Pass entry to it's dependencies 742 | mod.pass() 743 | 744 | // If module has entries not be passed, call onload 745 | if (mod._entry.length) { 746 | mod.onload() 747 | return 748 | } 749 | 750 | // Begin parallel loading 751 | var requestCache = {} 752 | var m 753 | 754 | for (i = 0; i < len; i++) { 755 | m = cachedMods[uris[i]] 756 | 757 | if (m.status < STATUS.FETCHING) { 758 | m.fetch(requestCache) 759 | } 760 | else if (m.status === STATUS.SAVED) { 761 | m.load() 762 | } 763 | } 764 | 765 | // Send all requests at last to avoid cache bug in IE6-9. Issues#808 766 | for (var requestUri in requestCache) { 767 | if (requestCache.hasOwnProperty(requestUri)) { 768 | requestCache[requestUri]() 769 | } 770 | } 771 | } 772 | 773 | // Call this method when module is loaded 774 | Module.prototype.onload = function() { 775 | var mod = this 776 | mod.status = STATUS.LOADED 777 | 778 | // When sometimes cached in IE, exec will occur before onload, make sure len is an number 779 | for (var i = 0, len = (mod._entry || []).length; i < len; i++) { 780 | var entry = mod._entry[i] 781 | if (--entry.remain === 0) { 782 | entry.callback() 783 | } 784 | } 785 | 786 | delete mod._entry 787 | } 788 | 789 | // Call this method when module is 404 790 | Module.prototype.error = function() { 791 | var mod = this 792 | mod.onload() 793 | mod.status = STATUS.ERROR 794 | } 795 | 796 | // Execute a module 797 | Module.prototype.exec = function () { 798 | var mod = this 799 | 800 | // When module is executed, DO NOT execute it again. When module 801 | // is being executed, just return `module.exports` too, for avoiding 802 | // circularly calling 803 | if (mod.status >= STATUS.EXECUTING) { 804 | return mod.exports 805 | } 806 | 807 | mod.status = STATUS.EXECUTING 808 | 809 | if (mod._entry && !mod._entry.length) { 810 | delete mod._entry 811 | } 812 | 813 | //non-cmd module has no property factory and exports 814 | if (!mod.hasOwnProperty('factory')) { 815 | mod.non = true 816 | return 817 | } 818 | 819 | // Create require 820 | var uri = mod.uri 821 | 822 | function require(id) { 823 | var m = mod.deps[id] || Module.get(require.resolve(id)) 824 | if (m.status == STATUS.ERROR) { 825 | throw new Error('module was broken: ' + m.uri); 826 | } 827 | return m.exec() 828 | } 829 | 830 | require.resolve = function(id) { 831 | return Module.resolve(id, uri) 832 | } 833 | 834 | require.async = function(ids, callback) { 835 | Module.use(ids, callback, uri + "_async_" + cid()) 836 | return require 837 | } 838 | 839 | // Exec factory 840 | var factory = mod.factory 841 | 842 | var exports = isFunction(factory) ? 843 | factory(require, mod.exports = {}, mod) : 844 | factory 845 | 846 | if (exports === undefined) { 847 | exports = mod.exports 848 | } 849 | 850 | // Reduce memory leak 851 | delete mod.factory 852 | 853 | mod.exports = exports 854 | mod.status = STATUS.EXECUTED 855 | 856 | // Emit `exec` event 857 | emit("exec", mod) 858 | 859 | return mod.exports 860 | } 861 | 862 | // Fetch a module 863 | Module.prototype.fetch = function(requestCache) { 864 | var mod = this 865 | var uri = mod.uri 866 | 867 | mod.status = STATUS.FETCHING 868 | 869 | // Emit `fetch` event for plugins such as combo plugin 870 | var emitData = { uri: uri } 871 | emit("fetch", emitData) 872 | var requestUri = emitData.requestUri || uri 873 | 874 | // Empty uri or a non-CMD module 875 | if (!requestUri || fetchedList.hasOwnProperty(requestUri)) { 876 | mod.load() 877 | return 878 | } 879 | 880 | if (fetchingList.hasOwnProperty(requestUri)) { 881 | callbackList[requestUri].push(mod) 882 | return 883 | } 884 | 885 | fetchingList[requestUri] = true 886 | callbackList[requestUri] = [mod] 887 | 888 | // Emit `request` event for plugins such as text plugin 889 | emit("request", emitData = { 890 | uri: uri, 891 | requestUri: requestUri, 892 | onRequest: onRequest, 893 | charset: isFunction(data.charset) ? data.charset(requestUri) || 'utf-8' : data.charset 894 | }) 895 | 896 | if (!emitData.requested) { 897 | requestCache ? 898 | requestCache[emitData.requestUri] = sendRequest : 899 | sendRequest() 900 | } 901 | 902 | function sendRequest() { 903 | seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset) 904 | } 905 | 906 | function onRequest(error) { 907 | delete fetchingList[requestUri] 908 | fetchedList[requestUri] = true 909 | 910 | // Save meta data of anonymous module 911 | if (anonymousMeta) { 912 | Module.save(uri, anonymousMeta) 913 | anonymousMeta = null 914 | } 915 | 916 | // Call callbacks 917 | var m, mods = callbackList[requestUri] 918 | delete callbackList[requestUri] 919 | while ((m = mods.shift())) { 920 | // When 404 occurs, the params error will be true 921 | if(error === true) { 922 | m.error() 923 | } 924 | else { 925 | m.load() 926 | } 927 | } 928 | } 929 | } 930 | 931 | // Resolve id to uri 932 | Module.resolve = function(id, refUri) { 933 | // Emit `resolve` event for plugins such as text plugin 934 | var emitData = { id: id, refUri: refUri } 935 | emit("resolve", emitData) 936 | 937 | return emitData.uri || seajs.resolve(emitData.id, refUri) 938 | } 939 | 940 | // Define a module 941 | Module.define = function (id, deps, factory) { 942 | var argsLen = arguments.length 943 | 944 | // define(factory) 945 | if (argsLen === 1) { 946 | factory = id 947 | id = undefined 948 | } 949 | else if (argsLen === 2) { 950 | factory = deps 951 | 952 | // define(deps, factory) 953 | if (isArray(id)) { 954 | deps = id 955 | id = undefined 956 | } 957 | // define(id, factory) 958 | else { 959 | deps = undefined 960 | } 961 | } 962 | 963 | // Parse dependencies according to the module factory code 964 | if (!isArray(deps) && isFunction(factory)) { 965 | deps = typeof parseDependencies === "undefined" ? [] : parseDependencies(factory.toString()) 966 | } 967 | 968 | var meta = { 969 | id: id, 970 | uri: Module.resolve(id), 971 | deps: deps, 972 | factory: factory 973 | } 974 | 975 | // Try to derive uri in IE6-9 for anonymous modules 976 | if (!isWebWorker && !meta.uri && doc.attachEvent && typeof getCurrentScript !== "undefined") { 977 | var script = getCurrentScript() 978 | 979 | if (script) { 980 | meta.uri = script.src 981 | } 982 | 983 | // NOTE: If the id-deriving methods above is failed, then falls back 984 | // to use onload event to get the uri 985 | } 986 | 987 | // Emit `define` event, used in nocache plugin, seajs node version etc 988 | emit("define", meta) 989 | 990 | meta.uri ? Module.save(meta.uri, meta) : 991 | // Save information for "saving" work in the script onload event 992 | anonymousMeta = meta 993 | } 994 | 995 | // Save meta data to cachedMods 996 | Module.save = function(uri, meta) { 997 | var mod = Module.get(uri) 998 | 999 | // Do NOT override already saved modules 1000 | if (mod.status < STATUS.SAVED) { 1001 | mod.id = meta.id || uri 1002 | mod.dependencies = meta.deps || [] 1003 | mod.factory = meta.factory 1004 | mod.status = STATUS.SAVED 1005 | 1006 | emit("save", mod) 1007 | } 1008 | } 1009 | 1010 | // Get an existed module or create a new one 1011 | Module.get = function(uri, deps) { 1012 | return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps)) 1013 | } 1014 | 1015 | // Use function is equal to load a anonymous module 1016 | Module.use = function (ids, callback, uri) { 1017 | var mod = Module.get(uri, isArray(ids) ? ids : [ids]) 1018 | 1019 | mod._entry.push(mod) 1020 | mod.history = {} 1021 | mod.remain = 1 1022 | 1023 | mod.callback = function() { 1024 | var exports = [] 1025 | var uris = mod.resolve() 1026 | 1027 | for (var i = 0, len = uris.length; i < len; i++) { 1028 | exports[i] = cachedMods[uris[i]].exec() 1029 | } 1030 | 1031 | if (callback) { 1032 | callback.apply(global, exports) 1033 | } 1034 | 1035 | delete mod.callback 1036 | delete mod.history 1037 | delete mod.remain 1038 | delete mod._entry 1039 | } 1040 | 1041 | mod.load() 1042 | } 1043 | 1044 | 1045 | // Public API 1046 | 1047 | seajs.use = function(ids, callback) { 1048 | Module.use(ids, callback, data.cwd + "_use_" + cid()) 1049 | return seajs 1050 | } 1051 | 1052 | Module.define.cmd = {} 1053 | global.define = Module.define 1054 | 1055 | 1056 | // For Developers 1057 | 1058 | seajs.Module = Module 1059 | data.fetchedList = fetchedList 1060 | data.cid = cid 1061 | 1062 | seajs.require = function(id) { 1063 | var mod = Module.get(Module.resolve(id)) 1064 | if (mod.status < STATUS.EXECUTING) { 1065 | mod.onload() 1066 | mod.exec() 1067 | } 1068 | return mod.exports 1069 | } 1070 | 1071 | /** 1072 | * config.js - The configuration for the loader 1073 | */ 1074 | 1075 | // The root path to use for id2uri parsing 1076 | data.base = loaderDir 1077 | 1078 | // The loader directory 1079 | data.dir = loaderDir 1080 | 1081 | // The loader's full path 1082 | data.loader = loaderPath 1083 | 1084 | // The current working directory 1085 | data.cwd = cwd 1086 | 1087 | // The charset for requesting files 1088 | data.charset = "utf-8" 1089 | 1090 | // data.alias - An object containing shorthands of module id 1091 | // data.paths - An object containing path shorthands in module id 1092 | // data.vars - The {xxx} variables in module id 1093 | // data.map - An array containing rules to map module uri 1094 | // data.debug - Debug mode. The default value is false 1095 | 1096 | seajs.config = function(configData) { 1097 | 1098 | for (var key in configData) { 1099 | var curr = configData[key] 1100 | var prev = data[key] 1101 | 1102 | // Merge object config such as alias, vars 1103 | if (prev && isObject(prev)) { 1104 | for (var k in curr) { 1105 | prev[k] = curr[k] 1106 | } 1107 | } 1108 | else { 1109 | // Concat array config such as map 1110 | if (isArray(prev)) { 1111 | curr = prev.concat(curr) 1112 | } 1113 | // Make sure that `data.base` is an absolute path 1114 | else if (key === "base") { 1115 | // Make sure end with "/" 1116 | if (curr.slice(-1) !== "/") { 1117 | curr += "/" 1118 | } 1119 | curr = addBase(curr) 1120 | } 1121 | 1122 | // Set config 1123 | data[key] = curr 1124 | } 1125 | } 1126 | 1127 | emit("config", configData) 1128 | return seajs 1129 | } 1130 | 1131 | })(this); 1132 | -------------------------------------------------------------------------------- /example/zepto.js: -------------------------------------------------------------------------------- 1 | // Zepto.js 2 | // (c) 2010-2014 Thomas Fuchs 3 | // Zepto.js may be freely distributed under the MIT license. 4 | 5 | var Zepto = (function() { 6 | var undefined, key, $, classList, emptyArray = [], slice = emptyArray.slice, filter = emptyArray.filter, 7 | document = window.document, 8 | elementDisplay = {}, classCache = {}, 9 | cssNumber = { 'column-count': 1, 'columns': 1, 'font-weight': 1, 'line-height': 1,'opacity': 1, 'z-index': 1, 'zoom': 1 }, 10 | fragmentRE = /^\s*<(\w+|!)[^>]*>/, 11 | singleTagRE = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, 12 | tagExpanderRE = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, 13 | rootNodeRE = /^(?:body|html)$/i, 14 | capitalRE = /([A-Z])/g, 15 | 16 | // special attributes that should be get/set via method calls 17 | methodAttributes = ['val', 'css', 'html', 'text', 'data', 'width', 'height', 'offset'], 18 | 19 | adjacencyOperators = [ 'after', 'prepend', 'before', 'append' ], 20 | table = document.createElement('table'), 21 | tableRow = document.createElement('tr'), 22 | containers = { 23 | 'tr': document.createElement('tbody'), 24 | 'tbody': table, 'thead': table, 'tfoot': table, 25 | 'td': tableRow, 'th': tableRow, 26 | '*': document.createElement('div') 27 | }, 28 | readyRE = /complete|loaded|interactive/, 29 | simpleSelectorRE = /^[\w-]*$/, 30 | class2type = {}, 31 | toString = class2type.toString, 32 | zepto = {}, 33 | camelize, uniq, 34 | tempParent = document.createElement('div'), 35 | propMap = { 36 | 'tabindex': 'tabIndex', 37 | 'readonly': 'readOnly', 38 | 'for': 'htmlFor', 39 | 'class': 'className', 40 | 'maxlength': 'maxLength', 41 | 'cellspacing': 'cellSpacing', 42 | 'cellpadding': 'cellPadding', 43 | 'rowspan': 'rowSpan', 44 | 'colspan': 'colSpan', 45 | 'usemap': 'useMap', 46 | 'frameborder': 'frameBorder', 47 | 'contenteditable': 'contentEditable' 48 | }, 49 | isArray = Array.isArray || 50 | function(object){ return object instanceof Array } 51 | 52 | zepto.matches = function(element, selector) { 53 | if (!selector || !element || element.nodeType !== 1) return false 54 | var matchesSelector = element.webkitMatchesSelector || element.mozMatchesSelector || 55 | element.oMatchesSelector || element.matchesSelector 56 | if (matchesSelector) return matchesSelector.call(element, selector) 57 | // fall back to performing a selector: 58 | var match, parent = element.parentNode, temp = !parent 59 | if (temp) (parent = tempParent).appendChild(element) 60 | match = ~zepto.qsa(parent, selector).indexOf(element) 61 | temp && tempParent.removeChild(element) 62 | return match 63 | } 64 | 65 | function type(obj) { 66 | return obj == null ? String(obj) : 67 | class2type[toString.call(obj)] || "object" 68 | } 69 | 70 | function isFunction(value) { return type(value) == "function" } 71 | function isWindow(obj) { return obj != null && obj == obj.window } 72 | function isDocument(obj) { return obj != null && obj.nodeType == obj.DOCUMENT_NODE } 73 | function isObject(obj) { return type(obj) == "object" } 74 | function isPlainObject(obj) { 75 | return isObject(obj) && !isWindow(obj) && Object.getPrototypeOf(obj) == Object.prototype 76 | } 77 | function likeArray(obj) { return typeof obj.length == 'number' } 78 | 79 | function compact(array) { return filter.call(array, function(item){ return item != null }) } 80 | function flatten(array) { return array.length > 0 ? $.fn.concat.apply([], array) : array } 81 | camelize = function(str){ return str.replace(/-+(.)?/g, function(match, chr){ return chr ? chr.toUpperCase() : '' }) } 82 | function dasherize(str) { 83 | return str.replace(/::/g, '/') 84 | .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') 85 | .replace(/([a-z\d])([A-Z])/g, '$1_$2') 86 | .replace(/_/g, '-') 87 | .toLowerCase() 88 | } 89 | uniq = function(array){ return filter.call(array, function(item, idx){ return array.indexOf(item) == idx }) } 90 | 91 | function classRE(name) { 92 | return name in classCache ? 93 | classCache[name] : (classCache[name] = new RegExp('(^|\\s)' + name + '(\\s|$)')) 94 | } 95 | 96 | function maybeAddPx(name, value) { 97 | return (typeof value == "number" && !cssNumber[dasherize(name)]) ? value + "px" : value 98 | } 99 | 100 | function defaultDisplay(nodeName) { 101 | var element, display 102 | if (!elementDisplay[nodeName]) { 103 | element = document.createElement(nodeName) 104 | document.body.appendChild(element) 105 | display = getComputedStyle(element, '').getPropertyValue("display") 106 | element.parentNode.removeChild(element) 107 | display == "none" && (display = "block") 108 | elementDisplay[nodeName] = display 109 | } 110 | return elementDisplay[nodeName] 111 | } 112 | 113 | function children(element) { 114 | return 'children' in element ? 115 | slice.call(element.children) : 116 | $.map(element.childNodes, function(node){ if (node.nodeType == 1) return node }) 117 | } 118 | 119 | // `$.zepto.fragment` takes a html string and an optional tag name 120 | // to generate DOM nodes nodes from the given html string. 121 | // The generated DOM nodes are returned as an array. 122 | // This function can be overriden in plugins for example to make 123 | // it compatible with browsers that don't support the DOM fully. 124 | zepto.fragment = function(html, name, properties) { 125 | var dom, nodes, container 126 | 127 | // A special case optimization for a single tag 128 | if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1)) 129 | 130 | if (!dom) { 131 | if (html.replace) html = html.replace(tagExpanderRE, "<$1>") 132 | if (name === undefined) name = fragmentRE.test(html) && RegExp.$1 133 | if (!(name in containers)) name = '*' 134 | 135 | container = containers[name] 136 | container.innerHTML = '' + html 137 | dom = $.each(slice.call(container.childNodes), function(){ 138 | container.removeChild(this) 139 | }) 140 | } 141 | 142 | if (isPlainObject(properties)) { 143 | nodes = $(dom) 144 | $.each(properties, function(key, value) { 145 | if (methodAttributes.indexOf(key) > -1) nodes[key](value) 146 | else nodes.attr(key, value) 147 | }) 148 | } 149 | 150 | return dom 151 | } 152 | 153 | // `$.zepto.Z` swaps out the prototype of the given `dom` array 154 | // of nodes with `$.fn` and thus supplying all the Zepto functions 155 | // to the array. Note that `__proto__` is not supported on Internet 156 | // Explorer. This method can be overriden in plugins. 157 | zepto.Z = function(dom, selector) { 158 | dom = dom || [] 159 | dom.__proto__ = $.fn 160 | dom.selector = selector || '' 161 | return dom 162 | } 163 | 164 | // `$.zepto.isZ` should return `true` if the given object is a Zepto 165 | // collection. This method can be overriden in plugins. 166 | zepto.isZ = function(object) { 167 | return object instanceof zepto.Z 168 | } 169 | 170 | // `$.zepto.init` is Zepto's counterpart to jQuery's `$.fn.init` and 171 | // takes a CSS selector and an optional context (and handles various 172 | // special cases). 173 | // This method can be overriden in plugins. 174 | zepto.init = function(selector, context) { 175 | var dom 176 | // If nothing given, return an empty Zepto collection 177 | if (!selector) return zepto.Z() 178 | // Optimize for string selectors 179 | else if (typeof selector == 'string') { 180 | selector = selector.trim() 181 | // If it's a html fragment, create nodes from it 182 | // Note: In both Chrome 21 and Firefox 15, DOM error 12 183 | // is thrown if the fragment doesn't begin with < 184 | if (selector[0] == '<' && fragmentRE.test(selector)) 185 | dom = zepto.fragment(selector, RegExp.$1, context), selector = null 186 | // If there's a context, create a collection on that context first, and select 187 | // nodes from there 188 | else if (context !== undefined) return $(context).find(selector) 189 | // If it's a CSS selector, use it to select nodes. 190 | else dom = zepto.qsa(document, selector) 191 | } 192 | // If a function is given, call it when the DOM is ready 193 | else if (isFunction(selector)) return $(document).ready(selector) 194 | // If a Zepto collection is given, just return it 195 | else if (zepto.isZ(selector)) return selector 196 | else { 197 | // normalize array if an array of nodes is given 198 | if (isArray(selector)) dom = compact(selector) 199 | // Wrap DOM nodes. 200 | else if (isObject(selector)) 201 | dom = [selector], selector = null 202 | // If it's a html fragment, create nodes from it 203 | else if (fragmentRE.test(selector)) 204 | dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null 205 | // If there's a context, create a collection on that context first, and select 206 | // nodes from there 207 | else if (context !== undefined) return $(context).find(selector) 208 | // And last but no least, if it's a CSS selector, use it to select nodes. 209 | else dom = zepto.qsa(document, selector) 210 | } 211 | // create a new Zepto collection from the nodes found 212 | return zepto.Z(dom, selector) 213 | } 214 | 215 | // `$` will be the base `Zepto` object. When calling this 216 | // function just call `$.zepto.init, which makes the implementation 217 | // details of selecting nodes and creating Zepto collections 218 | // patchable in plugins. 219 | $ = function(selector, context){ 220 | return zepto.init(selector, context) 221 | } 222 | 223 | function extend(target, source, deep) { 224 | for (key in source) 225 | if (deep && (isPlainObject(source[key]) || isArray(source[key]))) { 226 | if (isPlainObject(source[key]) && !isPlainObject(target[key])) 227 | target[key] = {} 228 | if (isArray(source[key]) && !isArray(target[key])) 229 | target[key] = [] 230 | extend(target[key], source[key], deep) 231 | } 232 | else if (source[key] !== undefined) target[key] = source[key] 233 | } 234 | 235 | // Copy all but undefined properties from one or more 236 | // objects to the `target` object. 237 | $.extend = function(target){ 238 | var deep, args = slice.call(arguments, 1) 239 | if (typeof target == 'boolean') { 240 | deep = target 241 | target = args.shift() 242 | } 243 | args.forEach(function(arg){ extend(target, arg, deep) }) 244 | return target 245 | } 246 | 247 | // `$.zepto.qsa` is Zepto's CSS selector implementation which 248 | // uses `document.querySelectorAll` and optimizes for some special cases, like `#id`. 249 | // This method can be overriden in plugins. 250 | zepto.qsa = function(element, selector){ 251 | var found, 252 | maybeID = selector[0] == '#', 253 | maybeClass = !maybeID && selector[0] == '.', 254 | nameOnly = maybeID || maybeClass ? selector.slice(1) : selector, // Ensure that a 1 char tag name still gets checked 255 | isSimple = simpleSelectorRE.test(nameOnly) 256 | return (isDocument(element) && isSimple && maybeID) ? 257 | ( (found = element.getElementById(nameOnly)) ? [found] : [] ) : 258 | (element.nodeType !== 1 && element.nodeType !== 9) ? [] : 259 | slice.call( 260 | isSimple && !maybeID ? 261 | maybeClass ? element.getElementsByClassName(nameOnly) : // If it's simple, it could be a class 262 | element.getElementsByTagName(selector) : // Or a tag 263 | element.querySelectorAll(selector) // Or it's not simple, and we need to query all 264 | ) 265 | } 266 | 267 | function filtered(nodes, selector) { 268 | return selector == null ? $(nodes) : $(nodes).filter(selector) 269 | } 270 | 271 | $.contains = document.documentElement.contains ? 272 | function(parent, node) { 273 | return parent !== node && parent.contains(node) 274 | } : 275 | function(parent, node) { 276 | while (node && (node = node.parentNode)) 277 | if (node === parent) return true 278 | return false 279 | } 280 | 281 | function funcArg(context, arg, idx, payload) { 282 | return isFunction(arg) ? arg.call(context, idx, payload) : arg 283 | } 284 | 285 | function setAttribute(node, name, value) { 286 | value == null ? node.removeAttribute(name) : node.setAttribute(name, value) 287 | } 288 | 289 | // access className property while respecting SVGAnimatedString 290 | function className(node, value){ 291 | var klass = node.className || '', 292 | svg = klass && klass.baseVal !== undefined 293 | 294 | if (value === undefined) return svg ? klass.baseVal : klass 295 | svg ? (klass.baseVal = value) : (node.className = value) 296 | } 297 | 298 | // "true" => true 299 | // "false" => false 300 | // "null" => null 301 | // "42" => 42 302 | // "42.5" => 42.5 303 | // "08" => "08" 304 | // JSON => parse if valid 305 | // String => self 306 | function deserializeValue(value) { 307 | try { 308 | return value ? 309 | value == "true" || 310 | ( value == "false" ? false : 311 | value == "null" ? null : 312 | +value + "" == value ? +value : 313 | /^[\[\{]/.test(value) ? $.parseJSON(value) : 314 | value ) 315 | : value 316 | } catch(e) { 317 | return value 318 | } 319 | } 320 | 321 | $.type = type 322 | $.isFunction = isFunction 323 | $.isWindow = isWindow 324 | $.isArray = isArray 325 | $.isPlainObject = isPlainObject 326 | 327 | $.isEmptyObject = function(obj) { 328 | var name 329 | for (name in obj) return false 330 | return true 331 | } 332 | 333 | $.inArray = function(elem, array, i){ 334 | return emptyArray.indexOf.call(array, elem, i) 335 | } 336 | 337 | $.camelCase = camelize 338 | $.trim = function(str) { 339 | return str == null ? "" : String.prototype.trim.call(str) 340 | } 341 | 342 | // plugin compatibility 343 | $.uuid = 0 344 | $.support = { } 345 | $.expr = { } 346 | 347 | $.map = function(elements, callback){ 348 | var value, values = [], i, key 349 | if (likeArray(elements)) 350 | for (i = 0; i < elements.length; i++) { 351 | value = callback(elements[i], i) 352 | if (value != null) values.push(value) 353 | } 354 | else 355 | for (key in elements) { 356 | value = callback(elements[key], key) 357 | if (value != null) values.push(value) 358 | } 359 | return flatten(values) 360 | } 361 | 362 | $.each = function(elements, callback){ 363 | var i, key 364 | if (likeArray(elements)) { 365 | for (i = 0; i < elements.length; i++) 366 | if (callback.call(elements[i], i, elements[i]) === false) return elements 367 | } else { 368 | for (key in elements) 369 | if (callback.call(elements[key], key, elements[key]) === false) return elements 370 | } 371 | 372 | return elements 373 | } 374 | 375 | $.grep = function(elements, callback){ 376 | return filter.call(elements, callback) 377 | } 378 | 379 | if (window.JSON) $.parseJSON = JSON.parse 380 | 381 | // Populate the class2type map 382 | $.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { 383 | class2type[ "[object " + name + "]" ] = name.toLowerCase() 384 | }) 385 | 386 | // Define methods that will be available on all 387 | // Zepto collections 388 | $.fn = { 389 | // Because a collection acts like an array 390 | // copy over these useful array functions. 391 | forEach: emptyArray.forEach, 392 | reduce: emptyArray.reduce, 393 | push: emptyArray.push, 394 | sort: emptyArray.sort, 395 | indexOf: emptyArray.indexOf, 396 | concat: emptyArray.concat, 397 | 398 | // `map` and `slice` in the jQuery API work differently 399 | // from their array counterparts 400 | map: function(fn){ 401 | return $($.map(this, function(el, i){ return fn.call(el, i, el) })) 402 | }, 403 | slice: function(){ 404 | return $(slice.apply(this, arguments)) 405 | }, 406 | 407 | ready: function(callback){ 408 | // need to check if document.body exists for IE as that browser reports 409 | // document ready when it hasn't yet created the body element 410 | if (readyRE.test(document.readyState) && document.body) callback($) 411 | else document.addEventListener('DOMContentLoaded', function(){ callback($) }, false) 412 | return this 413 | }, 414 | get: function(idx){ 415 | return idx === undefined ? slice.call(this) : this[idx >= 0 ? idx : idx + this.length] 416 | }, 417 | toArray: function(){ return this.get() }, 418 | size: function(){ 419 | return this.length 420 | }, 421 | remove: function(){ 422 | return this.each(function(){ 423 | if (this.parentNode != null) 424 | this.parentNode.removeChild(this) 425 | }) 426 | }, 427 | each: function(callback){ 428 | emptyArray.every.call(this, function(el, idx){ 429 | return callback.call(el, idx, el) !== false 430 | }) 431 | return this 432 | }, 433 | filter: function(selector){ 434 | if (isFunction(selector)) return this.not(this.not(selector)) 435 | return $(filter.call(this, function(element){ 436 | return zepto.matches(element, selector) 437 | })) 438 | }, 439 | add: function(selector,context){ 440 | return $(uniq(this.concat($(selector,context)))) 441 | }, 442 | is: function(selector){ 443 | return this.length > 0 && zepto.matches(this[0], selector) 444 | }, 445 | not: function(selector){ 446 | var nodes=[] 447 | if (isFunction(selector) && selector.call !== undefined) 448 | this.each(function(idx){ 449 | if (!selector.call(this,idx)) nodes.push(this) 450 | }) 451 | else { 452 | var excludes = typeof selector == 'string' ? this.filter(selector) : 453 | (likeArray(selector) && isFunction(selector.item)) ? slice.call(selector) : $(selector) 454 | this.forEach(function(el){ 455 | if (excludes.indexOf(el) < 0) nodes.push(el) 456 | }) 457 | } 458 | return $(nodes) 459 | }, 460 | has: function(selector){ 461 | return this.filter(function(){ 462 | return isObject(selector) ? 463 | $.contains(this, selector) : 464 | $(this).find(selector).size() 465 | }) 466 | }, 467 | eq: function(idx){ 468 | return idx === -1 ? this.slice(idx) : this.slice(idx, + idx + 1) 469 | }, 470 | first: function(){ 471 | var el = this[0] 472 | return el && !isObject(el) ? el : $(el) 473 | }, 474 | last: function(){ 475 | var el = this[this.length - 1] 476 | return el && !isObject(el) ? el : $(el) 477 | }, 478 | find: function(selector){ 479 | var result, $this = this 480 | if (!selector) result = $() 481 | else if (typeof selector == 'object') 482 | result = $(selector).filter(function(){ 483 | var node = this 484 | return emptyArray.some.call($this, function(parent){ 485 | return $.contains(parent, node) 486 | }) 487 | }) 488 | else if (this.length == 1) result = $(zepto.qsa(this[0], selector)) 489 | else result = this.map(function(){ return zepto.qsa(this, selector) }) 490 | return result 491 | }, 492 | closest: function(selector, context){ 493 | var node = this[0], collection = false 494 | if (typeof selector == 'object') collection = $(selector) 495 | while (node && !(collection ? collection.indexOf(node) >= 0 : zepto.matches(node, selector))) 496 | node = node !== context && !isDocument(node) && node.parentNode 497 | return $(node) 498 | }, 499 | parents: function(selector){ 500 | var ancestors = [], nodes = this 501 | while (nodes.length > 0) 502 | nodes = $.map(nodes, function(node){ 503 | if ((node = node.parentNode) && !isDocument(node) && ancestors.indexOf(node) < 0) { 504 | ancestors.push(node) 505 | return node 506 | } 507 | }) 508 | return filtered(ancestors, selector) 509 | }, 510 | parent: function(selector){ 511 | return filtered(uniq(this.pluck('parentNode')), selector) 512 | }, 513 | children: function(selector){ 514 | return filtered(this.map(function(){ return children(this) }), selector) 515 | }, 516 | contents: function() { 517 | return this.map(function() { return slice.call(this.childNodes) }) 518 | }, 519 | siblings: function(selector){ 520 | return filtered(this.map(function(i, el){ 521 | return filter.call(children(el.parentNode), function(child){ return child!==el }) 522 | }), selector) 523 | }, 524 | empty: function(){ 525 | return this.each(function(){ this.innerHTML = '' }) 526 | }, 527 | // `pluck` is borrowed from Prototype.js 528 | pluck: function(property){ 529 | return $.map(this, function(el){ return el[property] }) 530 | }, 531 | show: function(){ 532 | return this.each(function(){ 533 | this.style.display == "none" && (this.style.display = '') 534 | if (getComputedStyle(this, '').getPropertyValue("display") == "none") 535 | this.style.display = defaultDisplay(this.nodeName) 536 | }) 537 | }, 538 | replaceWith: function(newContent){ 539 | return this.before(newContent).remove() 540 | }, 541 | wrap: function(structure){ 542 | var func = isFunction(structure) 543 | if (this[0] && !func) 544 | var dom = $(structure).get(0), 545 | clone = dom.parentNode || this.length > 1 546 | 547 | return this.each(function(index){ 548 | $(this).wrapAll( 549 | func ? structure.call(this, index) : 550 | clone ? dom.cloneNode(true) : dom 551 | ) 552 | }) 553 | }, 554 | wrapAll: function(structure){ 555 | if (this[0]) { 556 | $(this[0]).before(structure = $(structure)) 557 | var children 558 | // drill down to the inmost element 559 | while ((children = structure.children()).length) structure = children.first() 560 | $(structure).append(this) 561 | } 562 | return this 563 | }, 564 | wrapInner: function(structure){ 565 | var func = isFunction(structure) 566 | return this.each(function(index){ 567 | var self = $(this), contents = self.contents(), 568 | dom = func ? structure.call(this, index) : structure 569 | contents.length ? contents.wrapAll(dom) : self.append(dom) 570 | }) 571 | }, 572 | unwrap: function(){ 573 | this.parent().each(function(){ 574 | $(this).replaceWith($(this).children()) 575 | }) 576 | return this 577 | }, 578 | clone: function(){ 579 | return this.map(function(){ return this.cloneNode(true) }) 580 | }, 581 | hide: function(){ 582 | return this.css("display", "none") 583 | }, 584 | toggle: function(setting){ 585 | return this.each(function(){ 586 | var el = $(this) 587 | ;(setting === undefined ? el.css("display") == "none" : setting) ? el.show() : el.hide() 588 | }) 589 | }, 590 | prev: function(selector){ return $(this.pluck('previousElementSibling')).filter(selector || '*') }, 591 | next: function(selector){ return $(this.pluck('nextElementSibling')).filter(selector || '*') }, 592 | html: function(html){ 593 | return 0 in arguments ? 594 | this.each(function(idx){ 595 | var originHtml = this.innerHTML 596 | $(this).empty().append( funcArg(this, html, idx, originHtml) ) 597 | }) : 598 | (0 in this ? this[0].innerHTML : null) 599 | }, 600 | text: function(text){ 601 | return 0 in arguments ? 602 | this.each(function(idx){ 603 | var newText = funcArg(this, text, idx, this.textContent) 604 | this.textContent = newText == null ? '' : ''+newText 605 | }) : 606 | (0 in this ? this[0].textContent : null) 607 | }, 608 | attr: function(name, value){ 609 | var result 610 | return (typeof name == 'string' && !(1 in arguments)) ? 611 | (!this.length || this[0].nodeType !== 1 ? undefined : 612 | (!(result = this[0].getAttribute(name)) && name in this[0]) ? this[0][name] : result 613 | ) : 614 | this.each(function(idx){ 615 | if (this.nodeType !== 1) return 616 | if (isObject(name)) for (key in name) setAttribute(this, key, name[key]) 617 | else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name))) 618 | }) 619 | }, 620 | removeAttr: function(name){ 621 | return this.each(function(){ this.nodeType === 1 && name.split(' ').forEach(function(attribute){ 622 | setAttribute(this, attribute) 623 | }, this)}) 624 | }, 625 | prop: function(name, value){ 626 | name = propMap[name] || name 627 | return (1 in arguments) ? 628 | this.each(function(idx){ 629 | this[name] = funcArg(this, value, idx, this[name]) 630 | }) : 631 | (this[0] && this[0][name]) 632 | }, 633 | data: function(name, value){ 634 | var attrName = 'data-' + name.replace(capitalRE, '-$1').toLowerCase() 635 | 636 | var data = (1 in arguments) ? 637 | this.attr(attrName, value) : 638 | this.attr(attrName) 639 | 640 | return data !== null ? deserializeValue(data) : undefined 641 | }, 642 | val: function(value){ 643 | return 0 in arguments ? 644 | this.each(function(idx){ 645 | this.value = funcArg(this, value, idx, this.value) 646 | }) : 647 | (this[0] && (this[0].multiple ? 648 | $(this[0]).find('option').filter(function(){ return this.selected }).pluck('value') : 649 | this[0].value) 650 | ) 651 | }, 652 | offset: function(coordinates){ 653 | if (coordinates) return this.each(function(index){ 654 | var $this = $(this), 655 | coords = funcArg(this, coordinates, index, $this.offset()), 656 | parentOffset = $this.offsetParent().offset(), 657 | props = { 658 | top: coords.top - parentOffset.top, 659 | left: coords.left - parentOffset.left 660 | } 661 | 662 | if ($this.css('position') == 'static') props['position'] = 'relative' 663 | $this.css(props) 664 | }) 665 | if (!this.length) return null 666 | var obj = this[0].getBoundingClientRect() 667 | return { 668 | left: obj.left + window.pageXOffset, 669 | top: obj.top + window.pageYOffset, 670 | width: Math.round(obj.width), 671 | height: Math.round(obj.height) 672 | } 673 | }, 674 | css: function(property, value){ 675 | if (arguments.length < 2) { 676 | var computedStyle, element = this[0] 677 | if(!element) return 678 | computedStyle = getComputedStyle(element, '') 679 | if (typeof property == 'string') 680 | return element.style[camelize(property)] || computedStyle.getPropertyValue(property) 681 | else if (isArray(property)) { 682 | var props = {} 683 | $.each(property, function(_, prop){ 684 | props[prop] = (element.style[camelize(prop)] || computedStyle.getPropertyValue(prop)) 685 | }) 686 | return props 687 | } 688 | } 689 | 690 | var css = '' 691 | if (type(property) == 'string') { 692 | if (!value && value !== 0) 693 | this.each(function(){ this.style.removeProperty(dasherize(property)) }) 694 | else 695 | css = dasherize(property) + ":" + maybeAddPx(property, value) 696 | } else { 697 | for (key in property) 698 | if (!property[key] && property[key] !== 0) 699 | this.each(function(){ this.style.removeProperty(dasherize(key)) }) 700 | else 701 | css += dasherize(key) + ':' + maybeAddPx(key, property[key]) + ';' 702 | } 703 | 704 | return this.each(function(){ this.style.cssText += ';' + css }) 705 | }, 706 | index: function(element){ 707 | return element ? this.indexOf($(element)[0]) : this.parent().children().indexOf(this[0]) 708 | }, 709 | hasClass: function(name){ 710 | if (!name) return false 711 | return emptyArray.some.call(this, function(el){ 712 | return this.test(className(el)) 713 | }, classRE(name)) 714 | }, 715 | addClass: function(name){ 716 | if (!name) return this 717 | return this.each(function(idx){ 718 | if (!('className' in this)) return 719 | classList = [] 720 | var cls = className(this), newName = funcArg(this, name, idx, cls) 721 | newName.split(/\s+/g).forEach(function(klass){ 722 | if (!$(this).hasClass(klass)) classList.push(klass) 723 | }, this) 724 | classList.length && className(this, cls + (cls ? " " : "") + classList.join(" ")) 725 | }) 726 | }, 727 | removeClass: function(name){ 728 | return this.each(function(idx){ 729 | if (!('className' in this)) return 730 | if (name === undefined) return className(this, '') 731 | classList = className(this) 732 | funcArg(this, name, idx, classList).split(/\s+/g).forEach(function(klass){ 733 | classList = classList.replace(classRE(klass), " ") 734 | }) 735 | className(this, classList.trim()) 736 | }) 737 | }, 738 | toggleClass: function(name, when){ 739 | if (!name) return this 740 | return this.each(function(idx){ 741 | var $this = $(this), names = funcArg(this, name, idx, className(this)) 742 | names.split(/\s+/g).forEach(function(klass){ 743 | (when === undefined ? !$this.hasClass(klass) : when) ? 744 | $this.addClass(klass) : $this.removeClass(klass) 745 | }) 746 | }) 747 | }, 748 | scrollTop: function(value){ 749 | if (!this.length) return 750 | var hasScrollTop = 'scrollTop' in this[0] 751 | if (value === undefined) return hasScrollTop ? this[0].scrollTop : this[0].pageYOffset 752 | return this.each(hasScrollTop ? 753 | function(){ this.scrollTop = value } : 754 | function(){ this.scrollTo(this.scrollX, value) }) 755 | }, 756 | scrollLeft: function(value){ 757 | if (!this.length) return 758 | var hasScrollLeft = 'scrollLeft' in this[0] 759 | if (value === undefined) return hasScrollLeft ? this[0].scrollLeft : this[0].pageXOffset 760 | return this.each(hasScrollLeft ? 761 | function(){ this.scrollLeft = value } : 762 | function(){ this.scrollTo(value, this.scrollY) }) 763 | }, 764 | position: function() { 765 | if (!this.length) return 766 | 767 | var elem = this[0], 768 | // Get *real* offsetParent 769 | offsetParent = this.offsetParent(), 770 | // Get correct offsets 771 | offset = this.offset(), 772 | parentOffset = rootNodeRE.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset() 773 | 774 | // Subtract element margins 775 | // note: when an element has margin: auto the offsetLeft and marginLeft 776 | // are the same in Safari causing offset.left to incorrectly be 0 777 | offset.top -= parseFloat( $(elem).css('margin-top') ) || 0 778 | offset.left -= parseFloat( $(elem).css('margin-left') ) || 0 779 | 780 | // Add offsetParent borders 781 | parentOffset.top += parseFloat( $(offsetParent[0]).css('border-top-width') ) || 0 782 | parentOffset.left += parseFloat( $(offsetParent[0]).css('border-left-width') ) || 0 783 | 784 | // Subtract the two offsets 785 | return { 786 | top: offset.top - parentOffset.top, 787 | left: offset.left - parentOffset.left 788 | } 789 | }, 790 | offsetParent: function() { 791 | return this.map(function(){ 792 | var parent = this.offsetParent || document.body 793 | while (parent && !rootNodeRE.test(parent.nodeName) && $(parent).css("position") == "static") 794 | parent = parent.offsetParent 795 | return parent 796 | }) 797 | } 798 | } 799 | 800 | // for now 801 | $.fn.detach = $.fn.remove 802 | 803 | // Generate the `width` and `height` functions 804 | ;['width', 'height'].forEach(function(dimension){ 805 | var dimensionProperty = 806 | dimension.replace(/./, function(m){ return m[0].toUpperCase() }) 807 | 808 | $.fn[dimension] = function(value){ 809 | var offset, el = this[0] 810 | if (value === undefined) return isWindow(el) ? el['inner' + dimensionProperty] : 811 | isDocument(el) ? el.documentElement['scroll' + dimensionProperty] : 812 | (offset = this.offset()) && offset[dimension] 813 | else return this.each(function(idx){ 814 | el = $(this) 815 | el.css(dimension, funcArg(this, value, idx, el[dimension]())) 816 | }) 817 | } 818 | }) 819 | 820 | function traverseNode(node, fun) { 821 | fun(node) 822 | for (var i = 0, len = node.childNodes.length; i < len; i++) 823 | traverseNode(node.childNodes[i], fun) 824 | } 825 | 826 | // Generate the `after`, `prepend`, `before`, `append`, 827 | // `insertAfter`, `insertBefore`, `appendTo`, and `prependTo` methods. 828 | adjacencyOperators.forEach(function(operator, operatorIndex) { 829 | var inside = operatorIndex % 2 //=> prepend, append 830 | 831 | $.fn[operator] = function(){ 832 | // arguments can be nodes, arrays of nodes, Zepto objects and HTML strings 833 | var argType, nodes = $.map(arguments, function(arg) { 834 | argType = type(arg) 835 | return argType == "object" || argType == "array" || arg == null ? 836 | arg : zepto.fragment(arg) 837 | }), 838 | parent, copyByClone = this.length > 1 839 | if (nodes.length < 1) return this 840 | 841 | return this.each(function(_, target){ 842 | parent = inside ? target : target.parentNode 843 | 844 | // convert all methods to a "before" operation 845 | target = operatorIndex == 0 ? target.nextSibling : 846 | operatorIndex == 1 ? target.firstChild : 847 | operatorIndex == 2 ? target : 848 | null 849 | 850 | var parentInDocument = $.contains(document.documentElement, parent) 851 | 852 | nodes.forEach(function(node){ 853 | if (copyByClone) node = node.cloneNode(true) 854 | else if (!parent) return $(node).remove() 855 | 856 | parent.insertBefore(node, target) 857 | if (parentInDocument) traverseNode(node, function(el){ 858 | if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' && 859 | (!el.type || el.type === 'text/javascript') && !el.src) 860 | window['eval'].call(window, el.innerHTML) 861 | }) 862 | }) 863 | }) 864 | } 865 | 866 | // after => insertAfter 867 | // prepend => prependTo 868 | // before => insertBefore 869 | // append => appendTo 870 | $.fn[inside ? operator+'To' : 'insert'+(operatorIndex ? 'Before' : 'After')] = function(html){ 871 | $(html)[operator](this) 872 | return this 873 | } 874 | }) 875 | 876 | zepto.Z.prototype = $.fn 877 | 878 | // Export internal API functions in the `$.zepto` namespace 879 | zepto.uniq = uniq 880 | zepto.deserializeValue = deserializeValue 881 | $.zepto = zepto 882 | 883 | return $ 884 | })() 885 | 886 | // If `$` is not yet defined, point it to `Zepto` 887 | window.Zepto = Zepto 888 | window.$ === undefined && (window.$ = Zepto) 889 | 890 | 891 | // Zepto.js 892 | // (c) 2010-2016 Thomas Fuchs 893 | // Zepto.js may be freely distributed under the MIT license. 894 | 895 | ;(function($){ 896 | var _zid = 1, undefined, 897 | slice = Array.prototype.slice, 898 | isFunction = $.isFunction, 899 | isString = function(obj){ return typeof obj == 'string' }, 900 | handlers = {}, 901 | specialEvents={}, 902 | focusinSupported = 'onfocusin' in window, 903 | focus = { focus: 'focusin', blur: 'focusout' }, 904 | hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' } 905 | 906 | specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents' 907 | 908 | function zid(element) { 909 | return element._zid || (element._zid = _zid++) 910 | } 911 | function findHandlers(element, event, fn, selector) { 912 | event = parse(event) 913 | if (event.ns) var matcher = matcherFor(event.ns) 914 | return (handlers[zid(element)] || []).filter(function(handler) { 915 | return handler 916 | && (!event.e || handler.e == event.e) 917 | && (!event.ns || matcher.test(handler.ns)) 918 | && (!fn || zid(handler.fn) === zid(fn)) 919 | && (!selector || handler.sel == selector) 920 | }) 921 | } 922 | function parse(event) { 923 | var parts = ('' + event).split('.') 924 | return {e: parts[0], ns: parts.slice(1).sort().join(' ')} 925 | } 926 | function matcherFor(ns) { 927 | return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)') 928 | } 929 | 930 | function eventCapture(handler, captureSetting) { 931 | return handler.del && 932 | (!focusinSupported && (handler.e in focus)) || 933 | !!captureSetting 934 | } 935 | 936 | function realEvent(type) { 937 | return hover[type] || (focusinSupported && focus[type]) || type 938 | } 939 | 940 | function add(element, events, fn, data, selector, delegator, capture){ 941 | var id = zid(element), set = (handlers[id] || (handlers[id] = [])) 942 | events.split(/\s/).forEach(function(event){ 943 | if (event == 'ready') return $(document).ready(fn) 944 | var handler = parse(event) 945 | handler.fn = fn 946 | handler.sel = selector 947 | // emulate mouseenter, mouseleave 948 | if (handler.e in hover) fn = function(e){ 949 | var related = e.relatedTarget 950 | if (!related || (related !== this && !$.contains(this, related))) 951 | return handler.fn.apply(this, arguments) 952 | } 953 | handler.del = delegator 954 | var callback = delegator || fn 955 | handler.proxy = function(e){ 956 | e = compatible(e) 957 | if (e.isImmediatePropagationStopped()) return 958 | e.data = data 959 | var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args)) 960 | if (result === false) e.preventDefault(), e.stopPropagation() 961 | return result 962 | } 963 | handler.i = set.length 964 | set.push(handler) 965 | if ('addEventListener' in element) 966 | element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture)) 967 | }) 968 | } 969 | function remove(element, events, fn, selector, capture){ 970 | var id = zid(element) 971 | ;(events || '').split(/\s/).forEach(function(event){ 972 | findHandlers(element, event, fn, selector).forEach(function(handler){ 973 | delete handlers[id][handler.i] 974 | if ('removeEventListener' in element) 975 | element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture)) 976 | }) 977 | }) 978 | } 979 | 980 | $.event = { add: add, remove: remove } 981 | 982 | $.proxy = function(fn, context) { 983 | var args = (2 in arguments) && slice.call(arguments, 2) 984 | if (isFunction(fn)) { 985 | var proxyFn = function(){ return fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments) } 986 | proxyFn._zid = zid(fn) 987 | return proxyFn 988 | } else if (isString(context)) { 989 | if (args) { 990 | args.unshift(fn[context], fn) 991 | return $.proxy.apply(null, args) 992 | } else { 993 | return $.proxy(fn[context], fn) 994 | } 995 | } else { 996 | throw new TypeError("expected function") 997 | } 998 | } 999 | 1000 | $.fn.bind = function(event, data, callback){ 1001 | return this.on(event, data, callback) 1002 | } 1003 | $.fn.unbind = function(event, callback){ 1004 | return this.off(event, callback) 1005 | } 1006 | $.fn.one = function(event, selector, data, callback){ 1007 | return this.on(event, selector, data, callback, 1) 1008 | } 1009 | 1010 | var returnTrue = function(){return true}, 1011 | returnFalse = function(){return false}, 1012 | ignoreProperties = /^([A-Z]|returnValue$|layer[XY]$)/, 1013 | eventMethods = { 1014 | preventDefault: 'isDefaultPrevented', 1015 | stopImmediatePropagation: 'isImmediatePropagationStopped', 1016 | stopPropagation: 'isPropagationStopped' 1017 | } 1018 | 1019 | function compatible(event, source) { 1020 | if (source || !event.isDefaultPrevented) { 1021 | source || (source = event) 1022 | 1023 | $.each(eventMethods, function(name, predicate) { 1024 | var sourceMethod = source[name] 1025 | event[name] = function(){ 1026 | this[predicate] = returnTrue 1027 | return sourceMethod && sourceMethod.apply(source, arguments) 1028 | } 1029 | event[predicate] = returnFalse 1030 | }) 1031 | 1032 | if (source.defaultPrevented !== undefined ? source.defaultPrevented : 1033 | 'returnValue' in source ? source.returnValue === false : 1034 | source.getPreventDefault && source.getPreventDefault()) 1035 | event.isDefaultPrevented = returnTrue 1036 | } 1037 | return event 1038 | } 1039 | 1040 | function createProxy(event) { 1041 | var key, proxy = { originalEvent: event } 1042 | for (key in event) 1043 | if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key] 1044 | 1045 | return compatible(proxy, event) 1046 | } 1047 | 1048 | $.fn.delegate = function(selector, event, callback){ 1049 | return this.on(event, selector, callback) 1050 | } 1051 | $.fn.undelegate = function(selector, event, callback){ 1052 | return this.off(event, selector, callback) 1053 | } 1054 | 1055 | $.fn.live = function(event, callback){ 1056 | $(document.body).delegate(this.selector, event, callback) 1057 | return this 1058 | } 1059 | $.fn.die = function(event, callback){ 1060 | $(document.body).undelegate(this.selector, event, callback) 1061 | return this 1062 | } 1063 | 1064 | $.fn.on = function(event, selector, data, callback, one){ 1065 | var autoRemove, delegator, $this = this 1066 | if (event && !isString(event)) { 1067 | $.each(event, function(type, fn){ 1068 | $this.on(type, selector, data, fn, one) 1069 | }) 1070 | return $this 1071 | } 1072 | 1073 | if (!isString(selector) && !isFunction(callback) && callback !== false) 1074 | callback = data, data = selector, selector = undefined 1075 | if (callback === undefined || data === false) 1076 | callback = data, data = undefined 1077 | 1078 | if (callback === false) callback = returnFalse 1079 | 1080 | return $this.each(function(_, element){ 1081 | if (one) autoRemove = function(e){ 1082 | remove(element, e.type, callback) 1083 | return callback.apply(this, arguments) 1084 | } 1085 | 1086 | if (selector) delegator = function(e){ 1087 | var evt, match = $(e.target).closest(selector, element).get(0) 1088 | if (match && match !== element) { 1089 | evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element}) 1090 | return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1))) 1091 | } 1092 | } 1093 | 1094 | add(element, event, callback, data, selector, delegator || autoRemove) 1095 | }) 1096 | } 1097 | $.fn.off = function(event, selector, callback){ 1098 | var $this = this 1099 | if (event && !isString(event)) { 1100 | $.each(event, function(type, fn){ 1101 | $this.off(type, selector, fn) 1102 | }) 1103 | return $this 1104 | } 1105 | 1106 | if (!isString(selector) && !isFunction(callback) && callback !== false) 1107 | callback = selector, selector = undefined 1108 | 1109 | if (callback === false) callback = returnFalse 1110 | 1111 | return $this.each(function(){ 1112 | remove(this, event, callback, selector) 1113 | }) 1114 | } 1115 | 1116 | $.fn.trigger = function(event, args){ 1117 | event = (isString(event) || $.isPlainObject(event)) ? $.Event(event) : compatible(event) 1118 | event._args = args 1119 | return this.each(function(){ 1120 | // handle focus(), blur() by calling them directly 1121 | if (event.type in focus && typeof this[event.type] == "function") this[event.type]() 1122 | // items in the collection might not be DOM elements 1123 | else if ('dispatchEvent' in this) this.dispatchEvent(event) 1124 | else $(this).triggerHandler(event, args) 1125 | }) 1126 | } 1127 | 1128 | // triggers event handlers on current element just as if an event occurred, 1129 | // doesn't trigger an actual event, doesn't bubble 1130 | $.fn.triggerHandler = function(event, args){ 1131 | var e, result 1132 | this.each(function(i, element){ 1133 | e = createProxy(isString(event) ? $.Event(event) : event) 1134 | e._args = args 1135 | e.target = element 1136 | $.each(findHandlers(element, event.type || event), function(i, handler){ 1137 | result = handler.proxy(e) 1138 | if (e.isImmediatePropagationStopped()) return false 1139 | }) 1140 | }) 1141 | return result 1142 | } 1143 | 1144 | // shortcut methods for `.bind(event, fn)` for each event type 1145 | ;('focusin focusout focus blur load resize scroll unload click dblclick '+ 1146 | 'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave '+ 1147 | 'change select keydown keypress keyup error').split(' ').forEach(function(event) { 1148 | $.fn[event] = function(callback) { 1149 | return (0 in arguments) ? 1150 | this.bind(event, callback) : 1151 | this.trigger(event) 1152 | } 1153 | }) 1154 | 1155 | $.Event = function(type, props) { 1156 | if (!isString(type)) props = type, type = props.type 1157 | var event = document.createEvent(specialEvents[type] || 'Events'), bubbles = true 1158 | if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name]) 1159 | event.initEvent(type, bubbles, true) 1160 | return compatible(event) 1161 | } 1162 | 1163 | })(Zepto) 1164 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "badjs-report", 3 | "version": "1.3.4", 4 | "main": "dist/bj-report-tryjs.js", 5 | "description": "client report", 6 | "homepage": "https://github.com/BetterJS/badjs-report", 7 | "scripts": { 8 | "test": "grunt dist", 9 | "GFW": "npm install --phantomjs_cdnurl=https://npm.taobao.org/mirrors/phantomjs" 10 | }, 11 | "author": [ 12 | { 13 | "name": "yorts52", 14 | "email": "yorts52@qq.com" 15 | }, 16 | { 17 | "name": "chriscai", 18 | "email": "caihuijigood@gmail.com" 19 | } 20 | ], 21 | "repository": { 22 | "url": "https://github.com/BetterJS/badjs-report.git" 23 | }, 24 | "bugs": "https://github.com/BetterJS/badjs-report.git/issues", 25 | "license": "MIT", 26 | "keywords": [ 27 | "BetterJS", 28 | "badjs", 29 | "report" 30 | ], 31 | "devDependencies": { 32 | "grunt": "1.0.2", 33 | "mocha": "~5.0", 34 | "chai": "~4.1.2", 35 | "grunt-mocha": "1.0", 36 | "requirejs": "^2.3.5", 37 | "jquery": "*", 38 | "grunt-contrib-copy": "~1.0.0", 39 | "grunt-contrib-clean": "^1.1.0", 40 | "grunt-contrib-jshint": "~1.1.0", 41 | "grunt-contrib-uglify": "^3.3.0", 42 | "grunt-contrib-concat": "~1.0.1" 43 | }, 44 | "lego": { 45 | "category": "35", 46 | "categoryName": "基础框架", 47 | "main": "./dist/bj-report-tryjs.js", 48 | "dependencies": {}, 49 | "devDependencies": {} 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/bj-report.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @module report 3 | * @author kael, chriscai 4 | * @date @DATE 5 | * Copyright (c) 2014 kael, chriscai 6 | * Licensed under the MIT license. 7 | */ 8 | var BJ_REPORT = (function(global) { 9 | if (global.BJ_REPORT) return global.BJ_REPORT; 10 | 11 | var _log_list = []; 12 | var _log_map = {}; 13 | var _config = { 14 | id: 0, // 上报 id 15 | uin: 0, // user id 16 | url: "", // 上报 接口 17 | offline_url: "", // 离线日志上报 接口 18 | offline_auto_url: "", // 检测是否自动上报 19 | ext: null, // 扩展参数 用于自定义上报 20 | level: 4, // 错误级别 1-debug 2-info 4-error 21 | ignore: [], // 忽略某个错误, 支持 Regexp 和 Function 22 | random: 1, // 抽样 (0-1] 1-全量 23 | delay: 1000, // 延迟上报 combo 为 true 时有效 24 | submit: null, // 自定义上报方式 25 | repeat: 5, // 重复上报次数(对于同一个错误超过多少次不上报), 26 | offlineLog: false, 27 | offlineLogExp: 5, // 离线日志过期时间 , 默认5天 28 | offlineLogAuto: false, //是否自动询问服务器需要自动上报 29 | }; 30 | 31 | var Offline_DB = { 32 | db: null, 33 | ready: function(callback) { 34 | var self = this; 35 | if (!window.indexedDB || !_config.offlineLog) { 36 | _config.offlineLog = false; 37 | return callback(); 38 | } 39 | 40 | if (this.db) { 41 | setTimeout(function() { 42 | callback(null, self); 43 | }, 0); 44 | 45 | return; 46 | } 47 | var version = 1; 48 | var request = window.indexedDB.open("badjs", version); 49 | 50 | if (!request) { 51 | _config.offlineLog = false; 52 | return callback(); 53 | } 54 | 55 | request.onerror = function(e) { 56 | callback(e); 57 | _config.offlineLog = false; 58 | console.log("indexdb request error"); 59 | return true; 60 | }; 61 | request.onsuccess = function(e) { 62 | self.db = e.target.result; 63 | 64 | setTimeout(function() { 65 | callback(null, self); 66 | }, 500); 67 | 68 | 69 | }; 70 | request.onupgradeneeded = function(e) { 71 | var db = e.target.result; 72 | if (!db.objectStoreNames.contains('logs')) { 73 | db.createObjectStore('logs', { autoIncrement: true }); 74 | } 75 | }; 76 | }, 77 | insertToDB: function(log) { 78 | var store = this.getStore(); 79 | store.add(log); 80 | }, 81 | addLog: function(log) { 82 | if (!this.db) { 83 | return; 84 | } 85 | this.insertToDB(log); 86 | }, 87 | addLogs: function(logs) { 88 | if (!this.db) { 89 | return; 90 | } 91 | 92 | for (var i = 0; i < logs.length; i++) { 93 | this.addLog(logs[i]); 94 | } 95 | 96 | }, 97 | getLogs: function(opt, callback) { 98 | if (!this.db) { 99 | return; 100 | } 101 | var store = this.getStore(); 102 | var request = store.openCursor(); 103 | var result = []; 104 | request.onsuccess = function(event) { 105 | var cursor = event.target.result; 106 | if (cursor) { 107 | if (cursor.value.time >= opt.start && cursor.value.time <= opt.end && cursor.value.id == opt.id && cursor.value.uin == opt.uin) { 108 | result.push(cursor.value); 109 | } 110 | //# cursor.continue 111 | cursor["continue"](); 112 | } else { 113 | callback(null, result); 114 | } 115 | }; 116 | 117 | request.onerror = function(e) { 118 | callback(e); 119 | return true; 120 | }; 121 | }, 122 | clearDB: function(daysToMaintain) { 123 | if (!this.db) { 124 | return; 125 | } 126 | 127 | var store = this.getStore(); 128 | if (!daysToMaintain) { 129 | store.clear(); 130 | return; 131 | } 132 | var range = (Date.now() - (daysToMaintain || 2) * 24 * 3600 * 1000); 133 | var request = store.openCursor(); 134 | request.onsuccess = function(event) { 135 | var cursor = event.target.result; 136 | if (cursor && (cursor.value.time < range || !cursor.value.time)) { 137 | store["delete"](cursor.primaryKey); 138 | cursor["continue"](); 139 | } 140 | }; 141 | }, 142 | 143 | getStore: function() { 144 | var transaction = this.db.transaction("logs", 'readwrite'); 145 | return transaction.objectStore("logs"); 146 | }, 147 | 148 | }; 149 | 150 | var T = { 151 | isOBJByType: function(o, type) { 152 | return Object.prototype.toString.call(o) === "[object " + (type || "Object") + "]"; 153 | }, 154 | 155 | isOBJ: function(obj) { 156 | var type = typeof obj; 157 | return type === "object" && !!obj; 158 | }, 159 | isEmpty: function(obj) { 160 | if (obj === null) return true; 161 | if (T.isOBJByType(obj, "Number")) { 162 | return false; 163 | } 164 | return !obj; 165 | }, 166 | extend: function(src, source) { 167 | for (var key in source) { 168 | src[key] = source[key]; 169 | } 170 | return src; 171 | }, 172 | processError: function(errObj) { 173 | try { 174 | if (errObj.stack) { 175 | var url = errObj.stack.match("https?://[^\n]+"); 176 | url = url ? url[0] : ""; 177 | var rowCols = url.match(":(\\d+):(\\d+)"); 178 | if (!rowCols) { 179 | rowCols = [0, 0, 0]; 180 | } 181 | 182 | var stack = T.processStackMsg(errObj); 183 | return { 184 | msg: stack, 185 | rowNum: rowCols[1], 186 | colNum: rowCols[2], 187 | target: url.replace(rowCols[0], ""), 188 | _orgMsg: errObj.toString() 189 | }; 190 | } else { 191 | //ie 独有 error 对象信息,try-catch 捕获到错误信息传过来,造成没有msg 192 | if (errObj.name && errObj.message && errObj.description) { 193 | return { 194 | msg: JSON.stringify(errObj) 195 | }; 196 | } 197 | return errObj; 198 | } 199 | } catch (err) { 200 | return errObj; 201 | } 202 | }, 203 | 204 | processStackMsg: function(error) { 205 | var stack = error.stack 206 | .replace(/\n/gi, "") 207 | .split(/\bat\b/) 208 | .slice(0, 9) 209 | .join("@") 210 | .replace(/\?[^:]+/gi, ""); 211 | var msg = error.toString(); 212 | if (stack.indexOf(msg) < 0) { 213 | stack = msg + "@" + stack; 214 | } 215 | return stack; 216 | }, 217 | 218 | isRepeat: function(error) { 219 | if (!T.isOBJ(error)) return true; 220 | var msg = error.msg; 221 | var times = _log_map[msg] = (parseInt(_log_map[msg], 10) || 0) + 1; 222 | return times > _config.repeat; 223 | } 224 | }; 225 | 226 | var orgError = global.onerror; 227 | // rewrite window.oerror 228 | global.onerror = function(msg, url, line, col, error) { 229 | var newMsg = msg; 230 | 231 | if (error && error.stack) { 232 | newMsg = T.processStackMsg(error); 233 | } 234 | 235 | if (T.isOBJByType(newMsg, "Event")) { 236 | newMsg += newMsg.type ? 237 | ("--" + newMsg.type + "--" + (newMsg.target ? 238 | (newMsg.target.tagName + "::" + newMsg.target.src) : "")) : ""; 239 | } 240 | 241 | report.push({ 242 | msg: newMsg, 243 | target: url, 244 | rowNum: line, 245 | colNum: col, 246 | _orgMsg: msg 247 | }); 248 | 249 | _process_log(); 250 | orgError && orgError.apply(global, arguments); 251 | }; 252 | 253 | 254 | 255 | var _report_log_tostring = function(error, index) { 256 | var param = []; 257 | var params = []; 258 | var stringify = []; 259 | if (T.isOBJ(error)) { 260 | error.level = error.level || _config.level; 261 | for (var key in error) { 262 | var value = error[key]; 263 | if (!T.isEmpty(value)) { 264 | if (T.isOBJ(value)) { 265 | try { 266 | value = JSON.stringify(value); 267 | } catch (err) { 268 | value = "[BJ_REPORT detect value stringify error] " + err.toString(); 269 | } 270 | } 271 | stringify.push(key + ":" + value); 272 | param.push(key + "=" + encodeURIComponent(value)); 273 | params.push(key + "[" + index + "]=" + encodeURIComponent(value)); 274 | } 275 | } 276 | } 277 | 278 | // msg[0]=msg&target[0]=target -- combo report 279 | // msg:msg,target:target -- ignore 280 | // msg=msg&target=target -- report with out combo 281 | return [params.join("&"), stringify.join(","), param.join("&")]; 282 | }; 283 | 284 | 285 | 286 | var _offline_buffer = []; 287 | var _save2Offline = function(key, msgObj) { 288 | msgObj = T.extend({ id: _config.id, uin: _config.uin, time: new Date - 0 }, msgObj); 289 | 290 | if (Offline_DB.db) { 291 | Offline_DB.addLog(msgObj); 292 | return; 293 | } 294 | 295 | 296 | if (!Offline_DB.db && !_offline_buffer.length) { 297 | Offline_DB.ready(function(err, DB) { 298 | if (DB) { 299 | if (_offline_buffer.length) { 300 | DB.addLogs(_offline_buffer); 301 | _offline_buffer = []; 302 | } 303 | 304 | } 305 | }); 306 | } 307 | _offline_buffer.push(msgObj); 308 | }; 309 | 310 | var _autoReportOffline = function() { 311 | var script = document.createElement("script"); 312 | script.src = _config.offline_auto_url || _config.url.replace(/badjs$/, "offlineAuto") + "?id=" + _config.id + "&uin=" + _config.uin; 313 | window._badjsOfflineAuto = function(isReport) { 314 | if (isReport) { 315 | BJ_REPORT.reportOfflineLog(); 316 | } 317 | }; 318 | document.head.appendChild(script); 319 | }; 320 | 321 | 322 | 323 | var submit_log_list = []; 324 | var comboTimeout = 0; 325 | var _submit_log = function() { 326 | clearTimeout(comboTimeout); 327 | // https://github.com/BetterJS/badjs-report/issues/34 328 | comboTimeout = 0; 329 | 330 | if (!submit_log_list.length) { 331 | return; 332 | } 333 | 334 | var url = _config._reportUrl + submit_log_list.join("&") + "&count=" + submit_log_list.length + "&_t=" + (+new Date); 335 | 336 | if (_config.submit) { 337 | _config.submit(url, submit_log_list); 338 | } else { 339 | var _img = new Image(); 340 | _img.src = url; 341 | } 342 | 343 | submit_log_list = []; 344 | }; 345 | 346 | var _process_log = function(isReportNow) { 347 | if (!_config._reportUrl) return; 348 | 349 | var randomIgnore = Math.random() >= _config.random; 350 | 351 | 352 | while (_log_list.length) { 353 | var isIgnore = false; 354 | var report_log = _log_list.shift(); 355 | //有效保证字符不要过长 356 | report_log.msg = (report_log.msg + "" || "").substr(0, 500); 357 | // 重复上报 358 | if (T.isRepeat(report_log)) continue; 359 | var log_str = _report_log_tostring(report_log, submit_log_list.length); 360 | if (T.isOBJByType(_config.ignore, "Array")) { 361 | for (var i = 0, l = _config.ignore.length; i < l; i++) { 362 | var rule = _config.ignore[i]; 363 | if ((T.isOBJByType(rule, "RegExp") && rule.test(log_str[1])) || 364 | (T.isOBJByType(rule, "Function") && rule(report_log, log_str[1]))) { 365 | isIgnore = true; 366 | break; 367 | } 368 | } 369 | } 370 | if (!isIgnore) { 371 | _config.offlineLog && _save2Offline("badjs_" + _config.id + _config.uin, report_log); 372 | if (!randomIgnore && report_log.level != 20) { 373 | submit_log_list.push(log_str[0]); 374 | _config.onReport && (_config.onReport(_config.id, report_log)); 375 | } 376 | 377 | } 378 | } 379 | 380 | 381 | if (isReportNow) { 382 | _submit_log(); // 立即上报 383 | } else if (!comboTimeout) { 384 | comboTimeout = setTimeout(_submit_log, _config.delay); // 延迟上报 385 | } 386 | }; 387 | 388 | 389 | 390 | var report = global.BJ_REPORT = { 391 | push: function(msg) { // 将错误推到缓存池 392 | 393 | var data = T.isOBJ(msg) ? T.processError(msg) : { 394 | msg: msg 395 | }; 396 | 397 | // ext 有默认值, 且上报不包含 ext, 使用默认 ext 398 | if (_config.ext && !data.ext) { 399 | data.ext = _config.ext; 400 | } 401 | // 在错误发生时获取页面链接 402 | // https://github.com/BetterJS/badjs-report/issues/19 403 | if (!data.from) { 404 | data.from = location.href; 405 | } 406 | 407 | if (data._orgMsg) { 408 | var _orgMsg = data._orgMsg; 409 | delete data._orgMsg; 410 | data.level = 2; 411 | var newData = T.extend({}, data); 412 | newData.level = 4; 413 | newData.msg = _orgMsg; 414 | _log_list.push(data); 415 | _log_list.push(newData); 416 | } else { 417 | _log_list.push(data); 418 | } 419 | 420 | _process_log(); 421 | return report; 422 | }, 423 | report: function(msg, isReportNow) { // error report 424 | msg && report.push(msg); 425 | 426 | isReportNow && _process_log(true); 427 | return report; 428 | }, 429 | info: function(msg) { // info report 430 | if (!msg) { 431 | return report; 432 | } 433 | if (T.isOBJ(msg)) { 434 | msg.level = 2; 435 | } else { 436 | msg = { 437 | msg: msg, 438 | level: 2 439 | }; 440 | } 441 | report.push(msg); 442 | return report; 443 | }, 444 | debug: function(msg) { // debug report 445 | if (!msg) { 446 | return report; 447 | } 448 | if (T.isOBJ(msg)) { 449 | msg.level = 1; 450 | } else { 451 | msg = { 452 | msg: msg, 453 | level: 1 454 | }; 455 | } 456 | report.push(msg); 457 | return report; 458 | }, 459 | 460 | reportOfflineLog: function() { 461 | if (!window.indexedDB) { 462 | BJ_REPORT.info("unsupport offlineLog"); 463 | return; 464 | } 465 | Offline_DB.ready(function(err, DB) { 466 | if (!DB) { 467 | return; 468 | } 469 | var startDate = new Date - 0 - _config.offlineLogExp * 24 * 3600 * 1000; 470 | var endDate = new Date - 0; 471 | DB.getLogs({ 472 | start: startDate, 473 | end: endDate, 474 | id: _config.id, 475 | uin: _config.uin 476 | }, function(err, result) { 477 | var iframe = document.createElement("iframe"); 478 | iframe.name = "badjs_offline_" + (new Date - 0); 479 | iframe.frameborder = 0; 480 | iframe.height = 0; 481 | iframe.width = 0; 482 | iframe.src = "javascript:false;"; 483 | 484 | iframe.onload = function() { 485 | var form = document.createElement("form"); 486 | form.style.display = "none"; 487 | form.target = iframe.name; 488 | form.method = "POST"; 489 | form.action = _config.offline_url || _config.url.replace(/badjs$/, "offlineLog"); 490 | form.enctype.method = 'multipart/form-data'; 491 | 492 | var input = document.createElement("input"); 493 | input.style.display = "none"; 494 | input.type = "hidden"; 495 | input.name = "offline_log"; 496 | input.value = JSON.stringify({ logs: result, userAgent: navigator.userAgent, startDate: startDate, endDate: endDate, id: _config.id, uin: _config.uin }); 497 | 498 | iframe.contentDocument.body.appendChild(form); 499 | form.appendChild(input); 500 | form.submit(); 501 | 502 | setTimeout(function() { 503 | document.body.removeChild(iframe); 504 | }, 10000); 505 | 506 | iframe.onload = null; 507 | }; 508 | document.body.appendChild(iframe); 509 | }); 510 | }); 511 | }, 512 | offlineLog: function(msg) { 513 | if (!msg) { 514 | return report; 515 | } 516 | if (T.isOBJ(msg)) { 517 | msg.level = 20; 518 | } else { 519 | msg = { 520 | msg: msg, 521 | level: 20 522 | }; 523 | } 524 | report.push(msg); 525 | return report; 526 | }, 527 | init: function(config) { // 初始化 528 | if (T.isOBJ(config)) { 529 | T.extend(_config, config); 530 | } 531 | // 没有设置id将不上报 532 | var id = parseInt(_config.id, 10); 533 | if (id) { 534 | // set default report url and uin 535 | if (/qq\.com$/gi.test(location.hostname)) { 536 | if (!_config.url) { 537 | _config.url = "//badjs2.qq.com/badjs"; 538 | } 539 | 540 | if (!_config.uin) { 541 | _config.uin = parseInt((document.cookie.match(/\buin=\D+(\d+)/) || [])[1], 10); 542 | } 543 | } 544 | 545 | _config._reportUrl = (_config.url || "/badjs") + 546 | "?id=" + id + 547 | "&uin=" + _config.uin + 548 | // "&from=" + encodeURIComponent(location.href) + 549 | "&"; 550 | } 551 | 552 | // if had error in cache , report now 553 | if (_log_list.length) { 554 | _process_log(); 555 | } 556 | 557 | // init offline 558 | if (!Offline_DB._initing) { 559 | Offline_DB._initing = true; 560 | Offline_DB.ready(function(err, DB) { 561 | if (DB) { 562 | setTimeout(function() { 563 | DB.clearDB(_config.offlineLogExp); 564 | setTimeout(function() { 565 | _config.offlineLogAuto && _autoReportOffline(); 566 | }, 5000); 567 | }, 1000); 568 | } 569 | 570 | }); 571 | } 572 | 573 | 574 | 575 | return report; 576 | }, 577 | 578 | __onerror__: global.onerror 579 | }; 580 | 581 | typeof console !== "undefined" && console.error && setTimeout(function() { 582 | var err = ((location.hash || "").match(/([#&])BJ_ERROR=([^&$]+)/) || [])[2]; 583 | err && console.error("BJ_ERROR", decodeURIComponent(err).replace(/(:\d+:\d+)\s*/g, "$1\n")); 584 | }, 0); 585 | 586 | return report; 587 | 588 | }(window)); 589 | 590 | if (typeof module !== "undefined") { 591 | module.exports = BJ_REPORT; 592 | } 593 | -------------------------------------------------------------------------------- /src/bj-wrap.js: -------------------------------------------------------------------------------- 1 | (function(global) { 2 | 3 | if (!global.BJ_REPORT) { 4 | console.error("please load bg-report first"); 5 | return; 6 | } 7 | 8 | var _onthrow = function(errObj) { 9 | global.BJ_REPORT.push(errObj); 10 | }; 11 | 12 | var tryJs = {}; 13 | global.BJ_REPORT.tryJs = function(throwCb) { 14 | throwCb && (_onthrow = throwCb); 15 | return tryJs; 16 | }; 17 | 18 | // merge 19 | var _merge = function(org, obj) { 20 | for (var key in obj) { 21 | org[key] = obj[key]; 22 | } 23 | }; 24 | 25 | // function or not 26 | var _isFunction = function(foo) { 27 | return typeof foo === "function"; 28 | }; 29 | 30 | var timeoutkey; 31 | 32 | var cat = function(foo, args) { 33 | return function() { 34 | try { 35 | return foo.apply(this, args || arguments); 36 | } catch (error) { 37 | 38 | _onthrow(error); 39 | 40 | //some browser throw error (chrome) , can not find error where it throw, so print it on console; 41 | if (error.stack && console && console.error) { 42 | console.error("[BJ-REPORT]", error.stack); 43 | } 44 | 45 | // hang up browser and throw , but it should trigger onerror , so rewrite onerror then recover it 46 | if (!timeoutkey) { 47 | var orgOnerror = global.onerror; 48 | global.onerror = function() { }; 49 | timeoutkey = setTimeout(function() { 50 | global.onerror = orgOnerror; 51 | timeoutkey = null; 52 | }, 50); 53 | } 54 | throw error; 55 | } 56 | }; 57 | }; 58 | 59 | var catArgs = function(foo) { 60 | return function() { 61 | var arg, args = []; 62 | for (var i = 0, l = arguments.length; i < l; i++) { 63 | arg = arguments[i]; 64 | _isFunction(arg) && (arg = cat(arg)); 65 | args.push(arg); 66 | } 67 | return foo.apply(this, args); 68 | }; 69 | }; 70 | 71 | var catTimeout = function(foo) { 72 | return function(cb, timeout) { 73 | // for setTimeout(string, delay) 74 | if (typeof cb === "string") { 75 | try { 76 | cb = new Function(cb); 77 | } catch (err) { 78 | throw err; 79 | } 80 | } 81 | var args = [].slice.call(arguments, 2); 82 | // for setTimeout(function, delay, param1, ...) 83 | cb = cat(cb, args.length && args); 84 | return foo(cb, timeout); 85 | }; 86 | }; 87 | 88 | /** 89 | * makeArgsTry 90 | * wrap a function's arguments with try & catch 91 | * @param {Function} foo 92 | * @param {Object} self 93 | * @returns {Function} 94 | */ 95 | var makeArgsTry = function(foo, self) { 96 | return function() { 97 | var arg, tmp, args = []; 98 | for (var i = 0, l = arguments.length; i < l; i++) { 99 | arg = arguments[i]; 100 | if (_isFunction(arg)) { 101 | if (arg.tryWrap) { 102 | arg = arg.tryWrap; 103 | } else { 104 | tmp = cat(arg); 105 | arg.tryWrap = tmp; 106 | arg = tmp; 107 | } 108 | } 109 | args.push(arg); 110 | } 111 | return foo.apply(self || this, args); 112 | }; 113 | }; 114 | 115 | /** 116 | * makeObjTry 117 | * wrap a object's all value with try & catch 118 | * @param {Function} foo 119 | * @param {Object} self 120 | * @returns {Function} 121 | */ 122 | var makeObjTry = function(obj) { 123 | var key, value; 124 | for (key in obj) { 125 | value = obj[key]; 126 | if (_isFunction(value)) obj[key] = cat(value); 127 | } 128 | return obj; 129 | }; 130 | 131 | /** 132 | * wrap jquery async function ,exp : event.add , event.remove , ajax 133 | * @returns {Function} 134 | */ 135 | tryJs.spyJquery = function() { 136 | var _$ = global.$; 137 | 138 | if (!_$ || !_$.event) { 139 | return tryJs; 140 | } 141 | 142 | var _add, _remove; 143 | if (_$.zepto) { 144 | _add = _$.fn.on, _remove = _$.fn.off; 145 | 146 | _$.fn.on = makeArgsTry(_add); 147 | _$.fn.off = function() { 148 | var arg, args = []; 149 | for (var i = 0, l = arguments.length; i < l; i++) { 150 | arg = arguments[i]; 151 | _isFunction(arg) && arg.tryWrap && (arg = arg.tryWrap); 152 | args.push(arg); 153 | } 154 | return _remove.apply(this, args); 155 | }; 156 | 157 | } else if (window.jQuery) { 158 | _add = _$.event.add, _remove = _$.event.remove; 159 | 160 | _$.event.add = makeArgsTry(_add); 161 | _$.event.remove = function() { 162 | var arg, args = []; 163 | for (var i = 0, l = arguments.length; i < l; i++) { 164 | arg = arguments[i]; 165 | _isFunction(arg) && arg.tryWrap && (arg = arg.tryWrap); 166 | args.push(arg); 167 | } 168 | return _remove.apply(this, args); 169 | }; 170 | } 171 | 172 | var _ajax = _$.ajax; 173 | 174 | if (_ajax) { 175 | _$.ajax = function(url, setting) { 176 | if (!setting) { 177 | setting = url; 178 | url = undefined; 179 | } 180 | makeObjTry(setting); 181 | if (url) return _ajax.call(_$, url, setting); 182 | return _ajax.call(_$, setting); 183 | }; 184 | } 185 | 186 | return tryJs; 187 | }; 188 | 189 | /** 190 | * wrap amd or commonjs of function ,exp : define , require , 191 | * @returns {Function} 192 | */ 193 | tryJs.spyModules = function() { 194 | var _require = global.require, 195 | _define = global.define; 196 | if (_define && _define.amd && _require) { 197 | global.require = catArgs(_require); 198 | _merge(global.require, _require); 199 | global.define = catArgs(_define); 200 | _merge(global.define, _define); 201 | } 202 | 203 | if (global.seajs && _define) { 204 | global.define = function() { 205 | var arg, args = []; 206 | for (var i = 0, l = arguments.length; i < l; i++) { 207 | arg = arguments[i]; 208 | if (_isFunction(arg)) { 209 | arg = cat(arg); 210 | //seajs should use toString parse dependencies , so rewrite it 211 | arg.toString = (function(orgArg) { 212 | return function() { 213 | return orgArg.toString(); 214 | }; 215 | }(arguments[i])); 216 | } 217 | args.push(arg); 218 | } 219 | return _define.apply(this, args); 220 | }; 221 | 222 | global.seajs.use = catArgs(global.seajs.use); 223 | 224 | _merge(global.define, _define); 225 | } 226 | 227 | return tryJs; 228 | }; 229 | 230 | /** 231 | * wrap async of function in window , exp : setTimeout , setInterval 232 | * @returns {Function} 233 | */ 234 | tryJs.spySystem = function() { 235 | global.setTimeout = catTimeout(global.setTimeout); 236 | global.setInterval = catTimeout(global.setInterval); 237 | return tryJs; 238 | }; 239 | 240 | /** 241 | * wrap custom of function , 242 | * @param obj - obj or function 243 | * @returns {Function} 244 | */ 245 | tryJs.spyCustom = function(obj) { 246 | if (_isFunction(obj)) { 247 | return cat(obj); 248 | } else { 249 | return makeObjTry(obj); 250 | } 251 | }; 252 | 253 | /** 254 | * run spyJquery() and spyModules() and spySystem() 255 | * @returns {Function} 256 | */ 257 | tryJs.spyAll = function() { 258 | tryJs 259 | .spyJquery() 260 | .spyModules() 261 | .spySystem(); 262 | return tryJs; 263 | }; 264 | 265 | }(window)); 266 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global chai, mocha, BJ_REPORT */ 2 | var assert = chai.assert; 3 | var should = chai.should(); 4 | var expect = chai.expect; 5 | 6 | define(['../src/bj-report.js', '../src/bj-wrap.js'], function(report) { 7 | describe('init', function() { 8 | 9 | // close repeat 10 | BJ_REPORT.init({ 11 | repeat: 10000 12 | }); 13 | 14 | 15 | it(' use push and report ', function(done) { 16 | 17 | BJ_REPORT.init({ 18 | id: 1, 19 | url: "http://test.qq.com/report", 20 | combo: 1, 21 | delay: 200, 22 | submit: function(url) { 23 | var match1 = url.indexOf("errorTest1"); 24 | var match2 = url.indexOf("errorTest2"); 25 | var match3 = url.indexOf("errorTest3"); 26 | var match4 = url.indexOf("errorTest4"); 27 | var match5 = url.indexOf("count"); 28 | should.not.equal(match1, -1); 29 | should.not.equal(match2, -1); 30 | should.not.equal(match3, -1); 31 | should.not.equal(match4, -1); 32 | should.not.equal(match5, -1); 33 | done(); 34 | } 35 | }); 36 | BJ_REPORT.push("errorTest1"); 37 | BJ_REPORT.push("errorTest2"); 38 | BJ_REPORT.push("errorTest3"); 39 | BJ_REPORT.report("errorTest4", true); 40 | }); 41 | 42 | 43 | it(' onerror and report ', function(done) { 44 | 45 | var count = 0; 46 | BJ_REPORT.init({ 47 | id: 1, 48 | url: "http://test.qq.com/report", 49 | delay: 200, 50 | submit: function(url) { 51 | count++; 52 | 53 | } 54 | }); 55 | BJ_REPORT.__onerror__("msg", 0, 0, null); 56 | BJ_REPORT.report(null, true); 57 | 58 | setTimeout(function() { 59 | should.equal(count, 1); 60 | done(); 61 | }, 500); 62 | }); 63 | 64 | 65 | it('onReport callback ', function(done) { 66 | 67 | var count = 0; 68 | BJ_REPORT.init({ 69 | id: 1, 70 | url: "http://test.qq.com/report", 71 | delay: 200, 72 | submit: function(url) { 73 | count++; 74 | should.equal(count, 2); 75 | done(); 76 | }, 77 | onReport: function() { 78 | count++; 79 | } 80 | }); 81 | BJ_REPORT.report("errorTest4", true); 82 | }); 83 | 84 | 85 | it('ignore report1 ', function(done) { 86 | 87 | var count = 0; 88 | BJ_REPORT.init({ 89 | id: 1, 90 | url: "http://test.qq.com/report", 91 | delay: 200, 92 | ignore: [ 93 | /ignore/gi, 94 | function(error, str) { 95 | 96 | if (str.indexOf("ignore2") >= 0) { 97 | return true; 98 | } else { 99 | false; 100 | } 101 | 102 | } 103 | ], 104 | submit: function(url) { 105 | should.equal(count, 1); 106 | done(); 107 | BJ_REPORT.init({ 108 | ignore: [] 109 | }); 110 | }, 111 | onReport: function() { 112 | count++; 113 | } 114 | }); 115 | BJ_REPORT.push("ignore"); 116 | BJ_REPORT.push("pass"); 117 | BJ_REPORT.report("ignore2", true); 118 | }); 119 | 120 | 121 | it('ignore report2 ', function(done) { 122 | var count = 0; 123 | BJ_REPORT.init({ 124 | id: 1, 125 | url: "http://test.qq.com/report", 126 | delay: 200, 127 | ignore: [ 128 | /ignore/gi, 129 | function(error, str) { 130 | return str.indexOf("ignore2") >= 0; 131 | } 132 | ], 133 | submit: function(url) { 134 | should.equal(count, 3); 135 | done(); 136 | BJ_REPORT.init({ 137 | ignore: [] 138 | }); 139 | }, 140 | onReport: function() { 141 | count++; 142 | } 143 | }); 144 | BJ_REPORT.push("pass"); 145 | BJ_REPORT.push("pass"); 146 | BJ_REPORT.report("pass", true); 147 | }); 148 | 149 | 150 | it('report Error Event', function(done, err) { 151 | 152 | BJ_REPORT.init({ 153 | id: 1, 154 | url: "http://test.qq.com/report", 155 | delay: 200, 156 | submit: function(url) { 157 | var match1 = url.indexOf("ReferenceError"); 158 | should.not.equal(match1, -1); 159 | done(); 160 | } 161 | }).tryJs().spyAll(); 162 | 163 | try { 164 | error111; // jshint ignore:line 165 | } catch (e) { 166 | BJ_REPORT.report(e); 167 | } 168 | 169 | }); 170 | 171 | it('repeat check', function(done, err) { 172 | 173 | BJ_REPORT.init({ 174 | id: 1, 175 | url: "http://test.qq.com/report", 176 | delay: 1000, 177 | repeat: 1, 178 | submit: function(url) { 179 | var match_count = (url.match(/repeatTest/g) || []).length; 180 | should.equal(match_count, 1); 181 | done(); 182 | // close repeat 183 | BJ_REPORT.init({ 184 | repeat: 1e10 185 | }); 186 | } 187 | }).tryJs().spyAll(); 188 | 189 | BJ_REPORT.push('repeatTest'); 190 | BJ_REPORT.push('repeatTest'); 191 | BJ_REPORT.push('repeatTest'); 192 | BJ_REPORT.report('repeatTest', true); 193 | }); 194 | }); 195 | 196 | 197 | describe('spy', function() { 198 | 199 | it('spyCustom', function(done, err) { 200 | 201 | var spyCustomFun = function() { 202 | throw "errorTest1"; 203 | }; 204 | 205 | spyCustomFun = BJ_REPORT.init({ 206 | id: 1, 207 | url: "http://test.qq.com/report", 208 | delay: 200, 209 | submit: function(url) { 210 | var match1 = url.indexOf("errorTest1"); 211 | should.not.equal(match1, -1); 212 | done(); 213 | } 214 | }).tryJs().spyCustom(spyCustomFun); 215 | 216 | (function() { 217 | spyCustomFun(); 218 | }).should.throw(); 219 | }); 220 | 221 | 222 | it('spyAll', function(done, err) { 223 | var _cb; 224 | 225 | window.require = function(requires, cb) { 226 | 227 | }; 228 | window.define = function(name, cb) { 229 | if (_cb) { 230 | _cb(); 231 | } else { 232 | _cb = cb; 233 | } 234 | }; 235 | window.define.amd = true; 236 | 237 | BJ_REPORT.init({ 238 | id: 1, 239 | url: "http://test.qq.com/report", 240 | delay: 200, 241 | submit: function(url) { 242 | var match1 = url.indexOf("testDefine"); 243 | should.not.equal(match1, -1); 244 | done(); 245 | } 246 | }).tryJs().spyAll(); 247 | 248 | define("testDefine", function() { 249 | throw "testDefine"; 250 | }); 251 | 252 | (function() { 253 | console.log(window.define); 254 | define(); 255 | }).should.throw(); 256 | }); 257 | 258 | 259 | 260 | }); 261 | }); 262 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | /* global chai, mocha */ 2 | var assert = chai.assert; 3 | var expect = chai.expect; 4 | 5 | mocha.setup({ 6 | ui: 'bdd' 7 | }); 8 | 9 | require.config({ 10 | baseUrl: '../src/', 11 | 12 | paths: { 13 | 'jquery': '../node_modules/jquery/dist/jquery.min' 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /test/testForLocation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 7 | 8 | 9 | 22 | 23 | 24 | --------------------------------------------------------------------------------