├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── ajaxhook.png ├── build.js ├── change-list.md ├── dist ├── ajaxhook.core.js ├── ajaxhook.core.min.js ├── ajaxhook.core.min.js.map ├── ajaxhook.js ├── ajaxhook.min.js ├── ajaxhook.min.js.map ├── ajaxhook.umd.js ├── ajaxhook.umd.min.js └── ajaxhook.umd.min.js.map ├── examples ├── esm.html ├── hook.html ├── iframe │ ├── inner.html │ └── main.html ├── proxy-request.html ├── proxy.html └── xhr.html ├── index.d.ts ├── index.js ├── package.json ├── pay.png ├── src ├── cdn-core.js ├── cdn.js ├── xhr-hook.js └── xhr-proxy.js ├── test ├── es.js ├── hook.ts ├── proxy.ts └── test.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | /node_modules 4 | /test/hook.js 5 | /test/proxy.js 6 | /test/dist 7 | 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | node_modules/ 3 | build.js 4 | .idea/ 5 | ajaxhook.png 6 | pay.png 7 | dist.js 8 | .travis.yml 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ajax-hook 2 | 3 | [![npm version](https://img.shields.io/npm/v/ajax-hook.svg)](https://www.npmjs.org/package/ajax-hook) [![build status](https://travis-ci.org/wendux/Ajax-hook.svg?branch=master)](https://travis-ci.org/wendux/Ajax-hook) [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://opensource.org/licenses/mit-license.php) ![](https://img.shields.io/badge/TypeScript-support-orange.svg) [![](https://img.shields.io/github/size/wendux/Ajax-hook/dist/ajaxhook.core.min.js.svg)](https://unpkg.com/ajax-hook@2.0.0/dist/ajaxhook.core.min.js) 4 | 5 | 6 | ## 简介 7 | 8 | ajax-hook是用于拦截浏览器 XMLHttpRequest 对象的轻量库,它可以在 XMLHttpRequest 对象发起请求之前、收到响应内容之后以及发生错误时获得处理权,通过它你可以提前对请求、响应以及错误进行一些预处理。Ajax-hook具有很好的兼容性,可以在任何支持**ES5**的浏览器上运行,因为它并不依赖 ES6 特性。 9 | 10 | ## 使用 11 | 12 | ### 安装 13 | 14 | - CDN引入 15 | 16 | ```html 17 | 18 | ``` 19 | 20 | 引入后会有一个名为"ah"(ajax hook)的全局对象,通过它可以调用ajax-hook的API,如`ah.proxy(hooks)` 21 | 22 | - NPM引入 23 | 24 | ```shell 25 | npm install ajax-hook 26 | ``` 27 | 28 | 一个简单示例: 29 | 30 | ```javascript 31 | import { proxy } from "ajax-hook"; 32 | proxy({ 33 | //请求发起前进入 34 | onRequest: (config, handler) => { 35 | console.log(config.url) 36 | handler.next(config); 37 | }, 38 | //请求发生错误时进入,比如超时;注意,不包括http状态码错误,如404仍然会认为请求成功 39 | onError: (err, handler) => { 40 | console.log(err.type) 41 | handler.next(err) 42 | }, 43 | //请求成功后进入 44 | onResponse: (response, handler) => { 45 | console.log(response.response) 46 | handler.next(response) 47 | } 48 | }) 49 | ``` 50 | 51 | 现在,我们便拦截了浏览器中通过`XMLHttpRequest`发起的所有网络请求!在请求发起前,会先进入`onRequest`钩子,调用`handler.next(config)` 请求继续,如果请求成功,则会进入`onResponse`钩子,如果请求发生错误,则会进入`onError` 。我们可以更改回调钩子的第一个参数来修改修改数据。 52 | 53 | [点击查看更多项目示例](https://github.com/wendux/ajax-hook/tree/master/examples)。 54 | 55 | ### API介绍 56 | 57 | #### `proxy(proxyObject, [window])` 58 | 59 | 拦截全局`XMLHttpRequest` 60 | 61 | > 注意:proxy 是通过ES5的getter和setter特性实现的,并没有使用ES6 的Proxy对象,所以可以兼容ES5浏览器。 62 | 63 | 参数: 64 | 65 | - `proxyObject`是一个对象,包含三个可选的钩子`onRequest`、`onResponse`、`onError`,我们可以直接在这三个钩子中对请求进行预处理。 66 | - `window`:可选参数,默认情况会使用当前窗口的`window`对象,如果要拦截iframe中的请求,可以将`iframe.contentWindow` 传入,注意,只能拦截**同源**的iframe页面(不能跨域)。 67 | 68 | 返回值: 69 | `ProxyReturnObject` 70 | 71 | ProxyReturnObject 是一个对象,包含了 `unProxy` 和 `originXhr` 72 | - `unProxy([window])`:取消拦截;取消后 `XMLHttpRequest` 将不会再被代理,浏览器原生`XMLHttpRequest`会恢复到全局变量空间 73 | - `originXhr`: 浏览器原生的 `XMLHttpRequest` 74 | 75 | 76 | 77 | ### 钩子函数的签名 78 | 79 | #### Handler 80 | 81 | 在钩子函数中,我们可以通过其第二个参数`handler`对象提供的方法来决定请求的后续流程,`handler`对象它有个方法: 82 | 83 | 1. `next(arg)`:继续进入后续流程;如果不调用,则请求链便会暂停,这种机制可以支持在钩子中执行一些异步任务。该方法在`onResponse`钩子中等价于`resolve`,在`onError`钩子中等价于`reject` 84 | 2. `resolve(response)`:调用后,请求后续流程会被阻断,直接返回响应数据,上层`xhr.onreadystatechange`或`xhr.onload`会被调用。 85 | 3. `reject(err)`:调用后,请求后续流程会被阻断,直接返回错误,上层的`xhr.onerror`、`xhr.ontimeout`、`xhr.onabort`之一会被调用,具体调用哪个取决于`err.type`的值,比如我们设置`err.type`为"timeout",则`xhr.ontimeout`会被调用。 86 | 87 | > 关于`config`、`response`、`err`的结构定义请参考[类型定义文件](./index.d.ts)中的`XhrRequestConfig`、`XhrResponse`、`XhrError` 88 | 89 | #### 示例 90 | 91 | ```javascript 92 | const { unProxy, originXhr } = proxy({ 93 | onRequest: (config, handler) => { 94 | if (config.url === 'https://aa/') { 95 | handler.resolve({ 96 | config: config, 97 | status: 200, 98 | headers: {'content-type': 'text/text'}, 99 | response: 'hi world' 100 | }) 101 | } else { 102 | handler.next(config); 103 | } 104 | }, 105 | onError: (err, handler) => { 106 | if (err.config.url === 'https://bb/') { 107 | handler.resolve({ 108 | config: err.config, 109 | status: 200, 110 | headers: {'content-type': 'text/text'}, 111 | response: 'hi world' 112 | }) 113 | } else { 114 | handler.next(err) 115 | } 116 | }, 117 | onResponse: (response, handler) => { 118 | if (response.config.url === location.href) { 119 | handler.reject({ 120 | config: response.config, 121 | type: 'error' 122 | }) 123 | } else { 124 | handler.next(response) 125 | } 126 | } 127 | }) 128 | 129 | // 使用jQuery发起网络请求 130 | function testJquery(url) { 131 | $.get(url).done(function (d) { 132 | console.log(d) 133 | }).fail(function (e) { 134 | console.log('hi world') 135 | }) 136 | } 137 | 138 | //测试 139 | testJquery('https://aa/'); 140 | testJquery('https://bb/'); 141 | testJquery(location.href) 142 | 143 | // 取消拦截 144 | unProxy(); 145 | ``` 146 | 147 | 运行后,控制台输出3次 "hi world"。 148 | 149 | 150 | 151 | ## 核心API - `hook(hooks,[window])` 152 | 153 | Ajax-hook在1.x版本中只提供了一个核心拦截功能的库,在1.x中,我们通过`hookAjax()` 方法(2.x中改名为`hook()`)实现了对`XMLHttpRequest`对象**具体属性、方法、回调的细粒度拦截**。而2.x中的`proxy()`方法则是基于`hook`的一个封装。 154 | 155 | ### `hook(Hooks,[window])` 156 | 157 | 拦截全局`XMLHttpRequest`,此方法调用后,浏览器原生的`XMLHttpRequest`将会被代理,代理对象会覆盖浏览器原生`XMLHttpRequest`,直到调用`unHook(...)`后才会取消代理。 158 | 159 | 参数: 160 | 161 | - `hooks`:钩子对象,里面是XMLHttpRequest对象的回调、方法、属性的钩子函数,钩子函数会在执行`XMLHttpRequest`对象真正的回调、方法、属性访问器前执行。 162 | - `window`:可选参数,默认情况会使用当前窗口的`window`对象,如果要拦截iframe中的请求,可以将`iframe.contentWindow` 传入,注意,只能拦截**同源**的iframe页面(不能跨域)。 163 | 164 | 返回值: 165 | `HookReturnObject` 166 | 167 | HookReturnObject 是一个对象,包含了 `unHook` 和 `originXhr` 168 | - `unHook([window])`:取消拦截;取消后 `XMLHttpRequest` 将不会再被代理,浏览器原生`XMLHttpRequest` 会恢复到全局变量空间 169 | - `originXhr`: 浏览器原生的 `XMLHttpRequest` 170 | 171 | 172 | #### 示例 173 | 174 | 下面我们看一下如何使用`hook`方法来拦截`XMLHttpRequest`对象: 175 | 176 | ```javascript 177 | import { hook } from "ajax-hook" 178 | const { unHook, originXhr } = hook({ 179 | //拦截回调 180 | onreadystatechange:function(xhr,event){ 181 | console.log("onreadystatechange called: %O") 182 | //返回false表示不阻断,拦截函数执行完后会接着执行真正的xhr.onreadystatechange回调. 183 | //返回true则表示阻断,拦截函数执行完后将不会执行xhr.onreadystatechange. 184 | return false 185 | }, 186 | onload:function(xhr,event){ 187 | console.log("onload called") 188 | return false 189 | }, 190 | //拦截方法 191 | open:function(args,xhr){ 192 | console.log("open called: method:%s,url:%s,async:%s",args[0],args[1],args[2]) 193 | //拦截方法的返回值含义同拦截回调的返回值 194 | return false 195 | } 196 | }) 197 | 198 | // 取消拦截 199 | unHook(); 200 | ``` 201 | 202 | 这样拦截就生效了,拦截的全局的`XMLHttpRequest`,所以,无论你使用的是哪种JavaScript http请求库,它们只要最终是使用`XMLHttpRequest`发起的网络请求,那么拦截都会生效。下面我们用jQuery发起一个请求: 203 | 204 | ```javascript 205 | // 获取当前页面的源码(Chrome中测试) 206 | $.get().done(function(d){ 207 | console.log(d.substr(0,30)+"...") 208 | }) 209 | ``` 210 | 211 | 输出: 212 | 213 | ``` 214 | > open called: method:GET,url:http://localhost:63342/Ajax-hook/demo.html,async:true 215 | > onload called 216 | > 217 | 218 | 假设对于同一个请求,你需要在其`open`的拦截函数和`onload` 回调中共享一个变量,但由于拦截的是全局XMLHttpRequest对象,所有网络请求会无次序的走到拦截的方法中,这时你可以通过`xhr`来对应请求的上下文信息。在上述场景中,你可以在`open`拦截函数中给`xhr`设置一个属性,然后在`onload`回调中获取即可。 252 | 253 | - `XMLHttpRequest`的所有方法如`open`、`send等`,他们拦截函数的第一个参数是一个数组,数组的内容是其对应的原生方法的参数列表,第二个参数是本次请求对应的`XMLHttpRequest`对象(已代理)。返回值类型是一个布尔值,为true时会阻断对应的请求。 254 | 255 | - 对于属性拦截器,为了避免循环调用导致的栈溢出,不可以在其getter拦截器中再读取其同名属性或在其setter拦截器中在给其同名属性赋值。 256 | 257 | - 本库需要在支持ES5的浏览器环境中运行(不支持IE8),但本库并不依赖ES6新特性。 258 | 259 | ### 拦截`XMLHttpRequest`的属性 260 | 261 | 除了拦截`XMLHttpRequest`回调和方法外,也可以拦截属性的**读/写**操作。比如设置超时`timeout`,读取响应内容`responseText`等。下面我们通过两个示例说明。 262 | 263 | - 假设为了避免用户设置不合理的超时时间,如,小于30ms,那么这将导致超过30ms的网络请求都将触发超时,因此,我们在底层做一个判断,确保超时时间最小为1s: 264 | 265 | ```javascript 266 | hook( 267 | //需要拦截的属性名 268 | timeout: { 269 | //拦截写操作 270 | setter: function (v, xhr) { 271 | //超时最短为1s,返回值为最终值。 272 | return Math.max(v, 1000); 273 | } 274 | } 275 | ) 276 | ``` 277 | 278 | - 假设在请求成功后,但在返回给用户之前,如果发现响应内容是JSON文本,那么我们想自动将JSON文本转为对象,要实现这个功能,有两种方方法: 279 | 280 | - 拦截成功回调 281 | 282 | ```javascript 283 | 284 | function tryParseJson1(xhr){ 285 | var contentType=xhr.getResponseHeader("content-type")||""; 286 | if(contentType.toLocaleLowerCase().indexOf("json")!==-1){ 287 | xhr.responseText=JSON.parse(xhr.responseText); 288 | } 289 | } 290 | 291 | hookAjax({ 292 | //拦截回调 293 | onreadystatechange:tryParseJson1, 294 | onload:tryParseJson1 295 | }); 296 | ``` 297 | 298 | 299 | 300 | - 拦截`responseText` 和 `response`读操作 301 | 302 | ```javascript 303 | function tryParseJson2(v,xhr){ 304 | var contentType=xhr.getResponseHeader("content-type")||""; 305 | if(contentType.toLocaleLowerCase().indexOf("json")!==-1){ 306 | v=JSON.parse(v); 307 | //不能在属性的getter钩子中再读取该属性,这会导致循环调用 308 | //v=JSON.parse(xhr.responseText); 309 | } 310 | return v; 311 | } 312 | 313 | //因为无法确定上层使用的是responseText还是response属性,为了保险起见,两个属性都拦截一下 314 | hook( 315 | responseText: { 316 | getter: tryParseJson2 317 | }, 318 | response: { 319 | getter:tryParseJson2 320 | } 321 | ) 322 | ``` 323 | 324 | ### ajax-hook.core.js 325 | 326 | 如果你只想使用`hook(...)`方法(不需要使用`proxy()`),我们提供了只包含`hook()`方法的核心库,你可以在[dist目录](https://github.com/wendux/Ajax-hook/tree/master/dist)找到名为`ajax-hook.core.js`的文件,直接使用它即可。 327 | 328 | 329 | 330 | ## `proxy(...)` vs `hook(...)` 331 | 332 | `proxy()` 和`hook()`都可以用于拦截全局`XMLHttpRequest`。它们的区别是:`hook()`的拦截粒度细,可以具体到`XMLHttpRequest`对象的某一方法、属性、回调,但是使用起来比较麻烦,很多时候,不仅业务逻辑需要散落在各个回调当中,而且还容易出错。而`proxy()`抽象度高,并且构建了请求上下文(请求信息config在各个回调中都可以直接获取),使用起来更简单、高效。 333 | 334 | 大多数情况下,我们建议使用`proxy()` 方法,除非`proxy()` 方法不能满足你的需求。 335 | 336 | 337 | 338 | ## 拦截iframe 339 | 340 | 显示指定iframe 的 window 对象即可,比如: 341 | ```javascript 342 | var iframeWindow = ...; 343 | const { unProxy } = proxy({...},iframeWindow) 344 | unProxy(iframeWindow) 345 | //或 346 | const { unHook } = hook({...},iframeWindow) 347 | unHook(frameWindow) 348 | ``` 349 | 完整示例见:[拦截iframe中的请求](https://github.com/wendux/ajax-hook/tree/master/examples/iframe)。 350 | 351 | > 注意:只能拦截**同源**的iframe页面(不能跨域)。 352 | 353 | ## 原理解析 354 | 通过ES5 getter和setter特性实现对 XMLHttpRequest 对象的代理: 355 | ![image](https://github.com/wendux/Ajax-hook/raw/master/ajaxhook.png) 356 | 357 | 具体原理解析请查看:[ajax-hook 原理解析](https://juejin.cn/post/6844903470181384206)。 358 | 359 | ## 最后 360 | 361 | 本库在2016年首次开源,最初只是个人研究所用,源码50行左右,实现了一个Ajax拦截的核心,并非一个完整可商用的项目。自开源后,有好多人对这个黑科技比较感兴趣,于是我便写了篇介绍的博客,由于代码比较精炼,所以对于JavaScript不是很精通的同学可能看起来比较吃力,之后专门写了一篇原理解析的文章,现在已经有很多公司已经将 ajax-hook 用于线上项目中,直到我知道美团、滴滴也用到之后,笔者对此库进行了修改和扩展以增强其健壮性和实用性,现在已经达到商用的标准,本库也将进行技术支持。如果你喜欢,欢迎Star,如果有问题,欢迎提Issue, 如果你觉得对自己有用想请作者喝杯咖啡的话,请扫描下面二维码: 362 | 363 | 364 | 365 | -------------------------------------------------------------------------------- /ajaxhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wendux/ajax-hook/93d62e0eaf8c2592cbc81efb292ee0d931852118/ajaxhook.png -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by du on 16/9/24. 3 | */ 4 | let path = require('path'); 5 | let webpack = require('webpack'); 6 | let env = process.argv[2] || "dev" 7 | console.log(env) 8 | let entry = { 9 | "ajaxhook": "./index.js", 10 | } 11 | 12 | let plugins = []; 13 | 14 | var output={ 15 | path: path.resolve("./dist/") 16 | 17 | } 18 | 19 | if (env === "test") { 20 | entry = { 21 | "es": "./test/es.js", 22 | 'hook': './test/hook.js', 23 | 'proxy': './test/proxy.js' 24 | } 25 | output = { 26 | path: path.resolve("./test/dist"), 27 | filename: "[name].js" 28 | } 29 | } else if (env === "cdn") { 30 | entry = { 31 | "ajaxhook": "./src/cdn.js", 32 | "ajaxhook.core": "./src/cdn-core.js", 33 | } 34 | output.filename = "[name].js" 35 | output.libraryTarget = 'window' 36 | } else if (env === "cdn-min") { 37 | entry = { 38 | "ajaxhook": "./src/cdn.js", 39 | "ajaxhook.core": "./src/cdn-core.js", 40 | // "ajaxhook": "./src/xhr-proxy.js", 41 | // "ajaxhook.core": "./src/xhr-hook.js", 42 | } 43 | output.filename = "[name].min.js" 44 | output.libraryTarget = 'window' 45 | plugins.push(new webpack.optimize.UglifyJsPlugin({ 46 | // compress: { 47 | // warnings: true 48 | // }, 49 | sourceMap: true 50 | })) 51 | } 52 | else if (env === "umd") { 53 | output.libraryTarget = "umd" 54 | output.filename = "[name].umd.js" 55 | }else if (env === "umd-min") { 56 | output.libraryTarget = "umd" 57 | output.filename = "[name].umd.min.js" 58 | plugins.push(new webpack.optimize.UglifyJsPlugin({ 59 | // compress: { 60 | // warnings: true 61 | // }, 62 | sourceMap: true 63 | })) 64 | } 65 | 66 | 67 | let config = { 68 | devtool: env.endsWith('min')?'source-map':'none', 69 | entry: entry, 70 | output: output, 71 | module: { 72 | rules: [ 73 | { 74 | test: /\.js$/, 75 | include: [path.resolve('./src'), path.resolve('./test'), path.resolve('./index.js')], 76 | use: [ 77 | { 78 | loader: "babel-loader", 79 | options: { 80 | presets: ['es2015'] 81 | } 82 | }, 83 | ] 84 | } 85 | ] 86 | }, 87 | watch: true, 88 | watchOptions: { 89 | ignored: '**/node_modules', 90 | }, 91 | plugins: plugins 92 | } 93 | 94 | 95 | webpack(config, function (err, stats) { 96 | if (err) throw err; 97 | process.stdout.write(stats.toString({ 98 | colors: true, 99 | modules: false, 100 | children: false, 101 | chunks: false, 102 | chunkModules: false 103 | }) + '\n') 104 | }); 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /change-list.md: -------------------------------------------------------------------------------- 1 | # Change list 2 | 3 | ## 3.0.0 4 | API签名相对2.x发生了变化,故增加大版本号,更新列表: 5 | - 支持多次拦截请求,修改取消拦截的方式,调用 `proxy` 或 `unHook` 时返回 `unProxy` 或 `unHook` 方法 6 | - fix #103 7 | 8 | ## 2.1.2 9 | 10 | - 添加可选参数[window],可以指定iframe的window对象来拦截同源iframe页面内的ajax请求。 11 | 12 | 13 | ## 2.0.3 14 | 15 | - 兼容IE fix #49 #52 16 | 17 | ## 2.0.2 18 | 19 | - fix #47 20 | 21 | ## 2.0 22 | 23 | - 添加了两个新API: `proxy(...)` 、 `unProxy()` 24 | 25 | - 将 `hookAjax(...)` 重命名为 `hook(...)` , `unHookAjax(...)` 重命名为 `unHook(...)` 26 | 27 | - 通过`hook()`拦截时,2.0中,XHR事件回调钩子函数(以"on"开头的),如`onreadystatechange`、`onload`等,他们的拦截函数的第一个参数都为"**原生xhr对象**" ,而1.x中为**代理xhr对象**。 28 | 29 | - npm包支持es6 module;cdn方式引入后API会挂在名为“ah”的全局变量下; 30 | 31 | - 支持 TypeScript 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /dist/ajaxhook.core.js: -------------------------------------------------------------------------------- 1 | (function(e, a) { for(var i in a) e[i] = a[i]; }(window, /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // identity function for calling harmony imports with the correct context 37 | /******/ __webpack_require__.i = function(value) { return value; }; 38 | /******/ 39 | /******/ // define getter function for harmony exports 40 | /******/ __webpack_require__.d = function(exports, name, getter) { 41 | /******/ if(!__webpack_require__.o(exports, name)) { 42 | /******/ Object.defineProperty(exports, name, { 43 | /******/ configurable: false, 44 | /******/ enumerable: true, 45 | /******/ get: getter 46 | /******/ }); 47 | /******/ } 48 | /******/ }; 49 | /******/ 50 | /******/ // getDefaultExport function for compatibility with non-harmony modules 51 | /******/ __webpack_require__.n = function(module) { 52 | /******/ var getter = module && module.__esModule ? 53 | /******/ function getDefault() { return module['default']; } : 54 | /******/ function getModuleExports() { return module; }; 55 | /******/ __webpack_require__.d(getter, 'a', getter); 56 | /******/ return getter; 57 | /******/ }; 58 | /******/ 59 | /******/ // Object.prototype.hasOwnProperty.call 60 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 61 | /******/ 62 | /******/ // __webpack_public_path__ 63 | /******/ __webpack_require__.p = ""; 64 | /******/ 65 | /******/ // Load entry module and return exports 66 | /******/ return __webpack_require__(__webpack_require__.s = 2); 67 | /******/ }) 68 | /************************************************************************/ 69 | /******/ ([ 70 | /* 0 */ 71 | /***/ (function(module, exports, __webpack_require__) { 72 | 73 | "use strict"; 74 | 75 | 76 | Object.defineProperty(exports, "__esModule", { 77 | value: true 78 | }); 79 | 80 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 81 | 82 | exports.configEvent = configEvent; 83 | exports.hook = hook; 84 | /* 85 | * author: wendux 86 | * email: 824783146@qq.com 87 | * source code: https://github.com/wendux/Ajax-hook 88 | */ 89 | 90 | var events = exports.events = ['load', 'loadend', 'timeout', 'error', 'readystatechange', 'abort']; 91 | 92 | var OriginXhr = '__origin_xhr'; 93 | 94 | function configEvent(event, xhrProxy) { 95 | var e = {}; 96 | for (var attr in event) { 97 | e[attr] = event[attr]; 98 | } // xhrProxy instead 99 | e.target = e.currentTarget = xhrProxy; 100 | return e; 101 | } 102 | 103 | function hook(proxy, win) { 104 | win = win || window; 105 | var originXhr = win.XMLHttpRequest; 106 | 107 | var hooking = true; 108 | 109 | var HookXMLHttpRequest = function HookXMLHttpRequest() { 110 | // We shouldn't hookAjax XMLHttpRequest.prototype because we can't 111 | // guarantee that all attributes are on the prototype。 112 | // Instead, hooking XMLHttpRequest instance can avoid this problem. 113 | 114 | var xhr = new originXhr(); 115 | 116 | // Generate all callbacks(eg. onload) are enumerable (not undefined). 117 | for (var i = 0; i < events.length; ++i) { 118 | var key = 'on' + events[i]; 119 | if (xhr[key] === undefined) xhr[key] = null; 120 | } 121 | 122 | for (var attr in xhr) { 123 | var type = ""; 124 | try { 125 | type = _typeof(xhr[attr]); // May cause exception on some browser 126 | } catch (e) {} 127 | if (type === "function") { 128 | // hookAjax methods of xhr, such as `open`、`send` ... 129 | this[attr] = hookFunction(attr); 130 | } else if (attr !== OriginXhr) { 131 | Object.defineProperty(this, attr, { 132 | get: getterFactory(attr), 133 | set: setterFactory(attr), 134 | enumerable: true 135 | }); 136 | } 137 | } 138 | var that = this; 139 | xhr.getProxy = function () { 140 | return that; 141 | }; 142 | this[OriginXhr] = xhr; 143 | }; 144 | 145 | HookXMLHttpRequest.prototype = originXhr.prototype; 146 | HookXMLHttpRequest.prototype.constructor = HookXMLHttpRequest; 147 | 148 | win.XMLHttpRequest = HookXMLHttpRequest; 149 | 150 | Object.assign(win.XMLHttpRequest, { UNSENT: 0, OPENED: 1, HEADERS_RECEIVED: 2, LOADING: 3, DONE: 4 }); 151 | 152 | // Generate getter for attributes of xhr 153 | function getterFactory(attr) { 154 | return function () { 155 | var originValue = this[OriginXhr][attr]; 156 | if (hooking) { 157 | var v = this.hasOwnProperty(attr + "_") ? this[attr + "_"] : originValue; 158 | var attrGetterHook = (proxy[attr] || {})["getter"]; 159 | return attrGetterHook && attrGetterHook(v, this) || v; 160 | } else { 161 | return originValue; 162 | } 163 | }; 164 | } 165 | 166 | // Generate setter for attributes of xhr; by this we have an opportunity 167 | // to hookAjax event callbacks (eg: `onload`) of xhr; 168 | function setterFactory(attr) { 169 | return function (v) { 170 | var xhr = this[OriginXhr]; 171 | if (hooking) { 172 | var that = this; 173 | var hook = proxy[attr]; 174 | // hookAjax event callbacks such as `onload`、`onreadystatechange`... 175 | if (attr.substring(0, 2) === 'on') { 176 | that[attr + "_"] = v; 177 | xhr[attr] = function (e) { 178 | e = configEvent(e, that); 179 | var ret = proxy[attr] && proxy[attr].call(that, xhr, e); 180 | ret || v.call(that, e); 181 | }; 182 | } else { 183 | //If the attribute isn't writable, generate proxy attribute 184 | var attrSetterHook = (hook || {})["setter"]; 185 | v = attrSetterHook && attrSetterHook(v, that) || v; 186 | this[attr + "_"] = v; 187 | try { 188 | // Not all attributes of xhr are writable(setter may undefined). 189 | xhr[attr] = v; 190 | } catch (e) {} 191 | } 192 | } else { 193 | xhr[attr] = v; 194 | } 195 | }; 196 | } 197 | 198 | // Hook methods of xhr. 199 | function hookFunction(fun) { 200 | return function () { 201 | var args = [].slice.call(arguments); 202 | if (proxy[fun] && hooking) { 203 | var ret = proxy[fun].call(this, args, this[OriginXhr]); 204 | // If the proxy return value exists, return it directly, 205 | // otherwise call the function of xhr. 206 | if (ret) return ret; 207 | } 208 | return this[OriginXhr][fun].apply(this[OriginXhr], args); 209 | }; 210 | } 211 | 212 | function unHook() { 213 | hooking = false; 214 | if (win.XMLHttpRequest === HookXMLHttpRequest) { 215 | win.XMLHttpRequest = originXhr; 216 | HookXMLHttpRequest.prototype.constructor = originXhr; 217 | originXhr = undefined; 218 | } 219 | } 220 | 221 | // Return the real XMLHttpRequest and unHook func 222 | return { originXhr: originXhr, unHook: unHook }; 223 | } 224 | 225 | /***/ }), 226 | /* 1 */, 227 | /* 2 */ 228 | /***/ (function(module, exports, __webpack_require__) { 229 | 230 | "use strict"; 231 | 232 | 233 | Object.defineProperty(exports, "__esModule", { 234 | value: true 235 | }); 236 | exports.ah = undefined; 237 | 238 | var _xhrHook = __webpack_require__(0); 239 | 240 | var ah = exports.ah = { hook: _xhrHook.hook }; /* 241 | * author: wendux 242 | * email: 824783146@qq.com 243 | * source code: https://github.com/wendux/Ajax-hook 244 | */ 245 | 246 | /***/ }) 247 | /******/ ]))); -------------------------------------------------------------------------------- /dist/ajaxhook.core.min.js: -------------------------------------------------------------------------------- 1 | !function(t,r){for(var e in r)t[e]=r[e]}(window,function(t){function r(n){if(e[n])return e[n].exports;var o=e[n]={i:n,l:!1,exports:{}};return t[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}var e={};return r.m=t,r.c=e,r.i=function(t){return t},r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:n})},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,r){return Object.prototype.hasOwnProperty.call(t,r)},r.p="",r(r.s=2)}([function(t,r,e){"use strict";function n(t,r){var e={};for(var n in t)e[n]=t[n];return e.target=e.currentTarget=r,e}function o(t,r){function e(r){return function(){var e=this[c][r];if(l){var n=this.hasOwnProperty(r+"_")?this[r+"_"]:e,o=(t[r]||{}).getter;return o&&o(n,this)||n}return e}}function o(r){return function(e){var o=this[c];if(l){var i=this,u=t[r];if("on"===r.substring(0,2))i[r+"_"]=e,o[r]=function(u){u=n(u,i),t[r]&&t[r].call(i,o,u)||e.call(i,u)};else{var a=(u||{}).setter;e=a&&a(e,i)||e,this[r+"_"]=e;try{o[r]=e}catch(t){}}}else o[r]=e}}function a(r){return function(){var e=[].slice.call(arguments);if(t[r]&&l){var n=t[r].call(this,e,this[c]);if(n)return n}return this[c][r].apply(this[c],e)}}function s(){l=!1,r.XMLHttpRequest===p&&(r.XMLHttpRequest=f,p.prototype.constructor=f,f=void 0)}r=r||window;var f=r.XMLHttpRequest,l=!0,p=function(){for(var t=new f,r=0;r 2 | 3 | 4 | 5 | Ajax hook Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 23 |
24 |
Ajax Hook !
25 |
26 | open console panel to view log. 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/hook.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ajax hook Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 22 |
24 |
Ajax Hook !
25 |
26 | open console panel to view log. 27 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /examples/iframe/inner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | proxy request 6 | 7 | 8 | 9 | 10 |
连续点击按钮并查看日志
11 | 12 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/iframe/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hook iframe 6 | 7 | 8 | 9 | 10 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/proxy-request.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | proxy request 6 | 7 | 8 | 9 | 10 | 11 |
连续点击按钮并查看日志
12 | 13 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/proxy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ajax hook Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 |
22 |
Ajax Hook !
23 |
24 | open console panel to view log. 25 | 26 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /examples/xhr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ajax hook Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 |
22 |
Ajax Hook !
23 |
24 | open console panel to view log. 25 | 26 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | interface XMLHttpRequestProxy extends XMLHttpRequest { 2 | responseText: string, 3 | readyState: number; 4 | response: any; 5 | responseURL: string; 6 | responseXML: Document | null; 7 | status: number; 8 | statusText: string; 9 | xhr: OriginXMLHttpRequest 10 | } 11 | 12 | interface OriginXMLHttpRequest extends XMLHttpRequest { 13 | getProxy(): XMLHttpRequestProxy; 14 | } 15 | 16 | interface AttrGetterAndSetter { 17 | getter?: (value: T, xhr: OriginXMLHttpRequest) => T; 18 | setter?: (value: T, xhr: OriginXMLHttpRequest) => T; 19 | } 20 | 21 | interface XhrRequestConfig { 22 | method: string, 23 | url: string, 24 | headers: any, 25 | body: any, 26 | async: boolean, 27 | user: string, 28 | password: string, 29 | withCredentials: boolean 30 | xhr: OriginXMLHttpRequest, 31 | } 32 | 33 | interface XhrResponse { 34 | config: XhrRequestConfig, 35 | headers: any, 36 | response: any, 37 | status: number, 38 | statusText?: string, 39 | } 40 | 41 | type XhrErrorType = 'error' | 'timeout' | 'abort' 42 | 43 | 44 | interface XhrError { 45 | config: XhrRequestConfig, 46 | type: XhrErrorType 47 | } 48 | 49 | interface Hooks { 50 | onreadystatechange?: ((this: XMLHttpRequestProxy, xhr: OriginXMLHttpRequest, ev: ProgressEvent) => any) | null; 51 | onabort?: ((this: XMLHttpRequestProxy, xhr: OriginXMLHttpRequest, ev: ProgressEvent) => any) | null; 52 | onerror?: ((this: XMLHttpRequestProxy, xhr: OriginXMLHttpRequest, ev: ProgressEvent) => any) | null; 53 | onload?: ((this: XMLHttpRequestProxy, xhr: OriginXMLHttpRequest, ev: ProgressEvent) => any) | null; 54 | onloadend?: ((this: XMLHttpRequestProxy, xhr: OriginXMLHttpRequest, ev: ProgressEvent) => any) | null; 55 | onloadstart?: ((this: XMLHttpRequestProxy, xhr: OriginXMLHttpRequest, ev: ProgressEvent) => any) | null; 56 | onprogress?: ((this: XMLHttpRequestProxy, xhr: OriginXMLHttpRequest, ev: ProgressEvent) => any) | null; 57 | ontimeout?: ((this: XMLHttpRequestProxy, xhr: OriginXMLHttpRequest, ev: ProgressEvent) => any) | null; 58 | abort?: ((args: Array, xhr: OriginXMLHttpRequest) => any); 59 | getAllResponseHeaders?: (args: Array, xhr: OriginXMLHttpRequest) => any; 60 | getResponseHeader?: (args: Array, xhr: OriginXMLHttpRequest) => any; 61 | open?: (args: Array, xhr: OriginXMLHttpRequest) => any; 62 | overrideMimeType?: (args: Array, xhr: OriginXMLHttpRequest) => any; 63 | send?: (args: Array, xhr: OriginXMLHttpRequest) => any; 64 | setRequestHeader?: (args: Array, xhr: OriginXMLHttpRequest) => any; 65 | addEventListener?: (args: Array, xhr: OriginXMLHttpRequest) => any; 66 | removeEventListener?: (args: Array, xhr: OriginXMLHttpRequest) => any; 67 | 68 | response?: AttrGetterAndSetter, 69 | responseText?: AttrGetterAndSetter, 70 | readyState?: AttrGetterAndSetter, 71 | responseType?: AttrGetterAndSetter; 72 | responseURL?: AttrGetterAndSetter; 73 | responseXML?: AttrGetterAndSetter; 74 | status?: AttrGetterAndSetter; 75 | statusText?: AttrGetterAndSetter; 76 | timeout?: AttrGetterAndSetter; 77 | upload?: AttrGetterAndSetter; 78 | withCredentials?: AttrGetterAndSetter; 79 | } 80 | 81 | interface XhrHandler { 82 | resolve(response: XhrResponse): void 83 | 84 | reject(err: XhrError): void 85 | } 86 | 87 | interface XhrRequestHandler extends XhrHandler { 88 | next(config: XhrRequestConfig): void 89 | } 90 | 91 | interface XhrResponseHandler extends XhrHandler { 92 | next(response: XhrResponse): void 93 | } 94 | 95 | interface XhrErrorHandler extends XhrHandler { 96 | next(error: XhrError): void 97 | } 98 | 99 | interface Proxy { 100 | onRequest?: (config: XhrRequestConfig, handler: XhrRequestHandler) => void, 101 | onResponse?: (response: XhrResponse, handler: XhrResponseHandler) => void, 102 | onError?: (err: XhrError, handler: XhrErrorHandler) => void, 103 | } 104 | 105 | export function proxy(proxy: Proxy, win?: Window): { originXhr: XMLHttpRequest, unProxy: () => void }; 106 | 107 | export function unProxy(win?: Window); 108 | 109 | export function hook(hooks: Hooks,win?: Window): { originXhr: XMLHttpRequest, unHook: () => void }; 110 | 111 | export function unHook(win?: Window); 112 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * author: wendux 3 | * email: 824783146@qq.com 4 | * source code: https://github.com/wendux/Ajax-hook 5 | **/ 6 | export { hook }from "./src/xhr-hook" 7 | export { proxy } from "./src/xhr-proxy" 8 | 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ajax-hook", 3 | "version": "3.0.3", 4 | "main": "index.js", 5 | "typings": "./index.d.ts", 6 | "author": { 7 | "name": "wendux", 8 | "email": "824783146@qq.com", 9 | "url": "https://github.com/wendux" 10 | }, 11 | "description": "Hooking XMLHttpRequest object in browser", 12 | "keywords": [ 13 | "ajax hook", 14 | "hook ajax", 15 | "ajax-hook", 16 | "intercept ajax" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/wendux/Ajax-hook" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/wendux/Ajax-hook/issues" 24 | }, 25 | "scripts": { 26 | "dev": "tsc test/hook.ts & tsc test/proxy.ts & node build.js test", 27 | "build": "node build.js umd & node build.js umd-min & node build.js cdn & node build.js cdn-min" 28 | }, 29 | "devDependencies": { 30 | "babel-core": "^6.0.0", 31 | "babel-loader": "^6.0.0", 32 | "babel-plugin-transform-runtime": "^6.0.0", 33 | "babel-preset-es2015": "^6.0.0", 34 | "babel-preset-es2017": "^6.24.1", 35 | "webpack": "^2.6.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wendux/ajax-hook/93d62e0eaf8c2592cbc81efb292ee0d931852118/pay.png -------------------------------------------------------------------------------- /src/cdn-core.js: -------------------------------------------------------------------------------- 1 | /* 2 | * author: wendux 3 | * email: 824783146@qq.com 4 | * source code: https://github.com/wendux/Ajax-hook 5 | */ 6 | 7 | import { hook } from "./xhr-hook"; 8 | 9 | export var ah = { hook }; 10 | -------------------------------------------------------------------------------- /src/cdn.js: -------------------------------------------------------------------------------- 1 | /* 2 | * author: wendux 3 | * email: 824783146@qq.com 4 | * source code: https://github.com/wendux/Ajax-hook 5 | */ 6 | 7 | import { hook } from "./xhr-hook"; 8 | import { proxy } from "./xhr-proxy"; 9 | // ah(ajax hook) 10 | export var ah = { 11 | proxy, 12 | hook, 13 | }; 14 | -------------------------------------------------------------------------------- /src/xhr-hook.js: -------------------------------------------------------------------------------- 1 | /* 2 | * author: wendux 3 | * email: 824783146@qq.com 4 | * source code: https://github.com/wendux/Ajax-hook 5 | */ 6 | 7 | export var events = ['load', 'loadend', 'timeout', 'error', 'readystatechange', 'abort']; 8 | 9 | var OriginXhr = '__origin_xhr'; 10 | 11 | export function configEvent(event, xhrProxy) { 12 | var e = {}; 13 | for (var attr in event) e[attr] = event[attr]; 14 | // xhrProxy instead 15 | e.target = e.currentTarget = xhrProxy 16 | return e; 17 | } 18 | 19 | export function hook(proxy, win) { 20 | win = win || window; 21 | var originXhr = win.XMLHttpRequest; 22 | 23 | var hooking = true; 24 | 25 | var HookXMLHttpRequest = function () { 26 | // We shouldn't hookAjax XMLHttpRequest.prototype because we can't 27 | // guarantee that all attributes are on the prototype。 28 | // Instead, hooking XMLHttpRequest instance can avoid this problem. 29 | 30 | var xhr = new originXhr(); 31 | 32 | // Generate all callbacks(eg. onload) are enumerable (not undefined). 33 | for (var i = 0; i < events.length; ++i) { 34 | var key='on'+events[i]; 35 | if (xhr[key] === undefined) xhr[key] = null; 36 | } 37 | 38 | for (var attr in xhr) { 39 | var type = ""; 40 | try { 41 | type = typeof xhr[attr] // May cause exception on some browser 42 | } catch (e) { 43 | } 44 | if (type === "function") { 45 | // hookAjax methods of xhr, such as `open`、`send` ... 46 | this[attr] = hookFunction(attr); 47 | } else if (attr !== OriginXhr) { 48 | Object.defineProperty(this, attr, { 49 | get: getterFactory(attr), 50 | set: setterFactory(attr), 51 | enumerable: true 52 | }) 53 | } 54 | } 55 | var that = this; 56 | xhr.getProxy = function () { 57 | return that 58 | } 59 | this[OriginXhr] = xhr; 60 | } 61 | 62 | HookXMLHttpRequest.prototype = originXhr.prototype; 63 | HookXMLHttpRequest.prototype.constructor = HookXMLHttpRequest; 64 | 65 | win.XMLHttpRequest = HookXMLHttpRequest; 66 | 67 | Object.assign(win.XMLHttpRequest, {UNSENT: 0, OPENED: 1, HEADERS_RECEIVED: 2, LOADING: 3, DONE: 4}); 68 | 69 | // Generate getter for attributes of xhr 70 | function getterFactory(attr) { 71 | return function () { 72 | var originValue = this[OriginXhr][attr]; 73 | if (hooking) { 74 | var v = this.hasOwnProperty(attr + "_") ? this[attr + "_"] : originValue; 75 | var attrGetterHook = (proxy[attr] || {})["getter"]; 76 | return attrGetterHook && attrGetterHook(v, this) || v; 77 | } else { 78 | return originValue; 79 | } 80 | } 81 | } 82 | 83 | // Generate setter for attributes of xhr; by this we have an opportunity 84 | // to hookAjax event callbacks (eg: `onload`) of xhr; 85 | function setterFactory(attr) { 86 | return function (v) { 87 | var xhr = this[OriginXhr]; 88 | if (hooking) { 89 | var that = this; 90 | var hook = proxy[attr]; 91 | // hookAjax event callbacks such as `onload`、`onreadystatechange`... 92 | if (attr.substring(0, 2) === 'on') { 93 | that[attr + "_"] = v; 94 | xhr[attr] = function (e) { 95 | e = configEvent(e, that) 96 | var ret = proxy[attr] && proxy[attr].call(that, xhr, e) 97 | ret || v.call(that, e); 98 | } 99 | } else { 100 | //If the attribute isn't writable, generate proxy attribute 101 | var attrSetterHook = (hook || {})["setter"]; 102 | v = attrSetterHook && attrSetterHook(v, that) || v 103 | this[attr + "_"] = v; 104 | try { 105 | // Not all attributes of xhr are writable(setter may undefined). 106 | xhr[attr] = v; 107 | } catch (e) { 108 | } 109 | } 110 | } else { 111 | xhr[attr] = v; 112 | } 113 | } 114 | } 115 | 116 | // Hook methods of xhr. 117 | function hookFunction(fun) { 118 | return function () { 119 | var args = [].slice.call(arguments); 120 | if (proxy[fun] && hooking) { 121 | var ret = proxy[fun].call(this, args, this[OriginXhr]) 122 | // If the proxy return value exists, return it directly, 123 | // otherwise call the function of xhr. 124 | if (ret) return ret; 125 | } 126 | return this[OriginXhr][fun].apply(this[OriginXhr], args); 127 | } 128 | } 129 | 130 | function unHook() { 131 | hooking = false; 132 | if (win.XMLHttpRequest === HookXMLHttpRequest) { 133 | win.XMLHttpRequest = originXhr; 134 | HookXMLHttpRequest.prototype.constructor = originXhr; 135 | originXhr = undefined; 136 | } 137 | } 138 | 139 | // Return the real XMLHttpRequest and unHook func 140 | return { originXhr, unHook }; 141 | } 142 | 143 | 144 | -------------------------------------------------------------------------------- /src/xhr-proxy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * author: wendux 3 | * email: 824783146@qq.com 4 | * source code: https://github.com/wendux/Ajax-hook 5 | */ 6 | 7 | import {hook, configEvent, events} from "./xhr-hook"; 8 | 9 | var eventLoad = events[0], 10 | eventLoadEnd = events[1], 11 | eventTimeout = events[2], 12 | eventError = events[3], 13 | eventReadyStateChange = events[4], 14 | eventAbort = events[5]; 15 | 16 | 17 | var prototype = 'prototype'; 18 | 19 | 20 | export function proxy(proxy, win) { 21 | win = win || window; 22 | return proxyAjax(proxy, win); 23 | } 24 | 25 | function trim(str) { 26 | return str.replace(/^\s+|\s+$/g, ''); 27 | } 28 | 29 | function getEventTarget(xhr) { 30 | return xhr.watcher || (xhr.watcher = document.createElement('a')); 31 | } 32 | 33 | function triggerListener(xhr, name) { 34 | var xhrProxy = xhr.getProxy(); 35 | var callback = 'on' + name + '_'; 36 | var event = configEvent({type: name}, xhrProxy); 37 | xhrProxy[callback] && xhrProxy[callback](event); 38 | var evt; 39 | if (typeof(Event) === 'function') { 40 | evt = new Event(name, {bubbles: false}); 41 | } else { 42 | // https://stackoverflow.com/questions/27176983/dispatchevent-not-working-in-ie11 43 | evt = document.createEvent('Event'); 44 | evt.initEvent(name, false, true); 45 | } 46 | getEventTarget(xhr).dispatchEvent(evt); 47 | } 48 | 49 | 50 | function Handler(xhr) { 51 | this.xhr = xhr; 52 | this.xhrProxy = xhr.getProxy(); 53 | } 54 | 55 | Handler[prototype] = Object.create({ 56 | resolve: function resolve(response) { 57 | var xhrProxy = this.xhrProxy; 58 | var xhr = this.xhr; 59 | xhrProxy.readyState = 4; 60 | xhr.resHeader = response.headers; 61 | xhrProxy.response = xhrProxy.responseText = response.response; 62 | xhrProxy.statusText = response.statusText; 63 | xhrProxy.status = response.status; 64 | triggerListener(xhr, eventReadyStateChange); 65 | triggerListener(xhr, eventLoad); 66 | triggerListener(xhr, eventLoadEnd); 67 | }, 68 | reject: function reject(error) { 69 | this.xhrProxy.status = 0; 70 | triggerListener(this.xhr, error.type); 71 | triggerListener(this.xhr, eventLoadEnd); 72 | } 73 | }); 74 | 75 | function makeHandler(next) { 76 | function sub(xhr) { 77 | Handler.call(this, xhr); 78 | } 79 | 80 | sub[prototype] = Object.create(Handler[prototype]); 81 | sub[prototype].next = next 82 | return sub; 83 | } 84 | 85 | var RequestHandler = makeHandler(function (rq) { 86 | var xhr = this.xhr; 87 | rq = rq || xhr.config; 88 | xhr.withCredentials = rq.withCredentials; 89 | xhr.open(rq.method, rq.url, rq.async !== false, rq.user, rq.password); 90 | for (var key in rq.headers) { 91 | xhr.setRequestHeader(key, rq.headers[key]); 92 | } 93 | xhr.send(rq.body); 94 | }); 95 | 96 | var ResponseHandler = makeHandler(function (response) { 97 | this.resolve(response); 98 | }); 99 | 100 | var ErrorHandler = makeHandler(function (error) { 101 | this.reject(error); 102 | }); 103 | 104 | function proxyAjax(proxy, win) { 105 | var onRequest = proxy.onRequest, 106 | onResponse = proxy.onResponse, 107 | onError = proxy.onError; 108 | 109 | function getResponseData(xhrProxy) { 110 | var responseType = xhrProxy.responseType; 111 | if (!responseType || responseType === 'text') { 112 | return xhrProxy.responseText; 113 | } 114 | // reference: https://shanabrian.com/web/html-css-js-technics/js-ie10-ie11-xhr-json-string.php 115 | // reference: https://github.com/axios/axios/issues/2390 116 | // json - W3C standard - xhrProxy.response = JSON object; responseText is unobtainable 117 | // For details, see https://github.com/wendux/ajax-hook/issues/117 118 | // IE 9, 10 & 11 - only responseText 119 | var response = xhrProxy.response; 120 | if (responseType === 'json' && !response) { 121 | try { 122 | return JSON.parse(xhrProxy.responseText); 123 | } catch (e) { 124 | console.warn(e); 125 | } 126 | } 127 | return response; 128 | }; 129 | 130 | function handleResponse(xhr, xhrProxy) { 131 | var handler = new ResponseHandler(xhr); 132 | var ret = { 133 | response: getResponseData(xhrProxy), 134 | status: xhrProxy.status, 135 | statusText: xhrProxy.statusText, 136 | config: xhr.config, 137 | headers: xhr.resHeader || xhr.getAllResponseHeaders().split('\r\n').reduce(function (ob, str) { 138 | if (str === "") return ob; 139 | var m = str.split(":"); 140 | ob[m.shift()] = trim(m.join(':')); 141 | return ob; 142 | }, {}) 143 | }; 144 | if (!onResponse) return handler.resolve(ret); 145 | onResponse(ret, handler); 146 | } 147 | 148 | function onerror(xhr, xhrProxy, error, errorType) { 149 | var handler = new ErrorHandler(xhr); 150 | error = {config: xhr.config, error: error, type: errorType}; 151 | if (onError) { 152 | onError(error, handler); 153 | } else { 154 | handler.next(error); 155 | } 156 | } 157 | 158 | function preventXhrProxyCallback() { 159 | return true; 160 | } 161 | 162 | function errorCallback(errorType) { 163 | return function (xhr, e) { 164 | onerror(xhr, this, e, errorType); 165 | return true; 166 | } 167 | } 168 | 169 | function stateChangeCallback(xhr, xhrProxy) { 170 | if (xhr.readyState === 4 && xhr.status !== 0) { 171 | handleResponse(xhr, xhrProxy); 172 | } else if (xhr.readyState !== 4) { 173 | triggerListener(xhr, eventReadyStateChange); 174 | } 175 | return true; 176 | } 177 | 178 | 179 | var { originXhr, unHook } = hook({ 180 | onload: preventXhrProxyCallback, 181 | onloadend: preventXhrProxyCallback, 182 | onerror: errorCallback(eventError), 183 | ontimeout: errorCallback(eventTimeout), 184 | onabort: errorCallback(eventAbort), 185 | onreadystatechange: function (xhr) { 186 | return stateChangeCallback(xhr, this); 187 | }, 188 | open: function open(args, xhr) { 189 | var _this = this; 190 | var config = xhr.config = {headers: {}}; 191 | config.method = args[0]; 192 | config.url = args[1]; 193 | config.async = args[2]; 194 | config.user = args[3]; 195 | config.password = args[4]; 196 | config.xhr = xhr; 197 | var evName = 'on' + eventReadyStateChange; 198 | if (!xhr[evName]) { 199 | xhr[evName] = function () { 200 | return stateChangeCallback(xhr, _this); 201 | }; 202 | } 203 | 204 | // 如果有请求拦截器,则在调用onRequest后再打开链接。因为onRequest最佳调用时机是在send前, 205 | // 所以我们在send拦截函数中再手动调用open,因此返回true阻止xhr.open调用。 206 | // 207 | // 如果没有请求拦截器,则不用阻断xhr.open调用 208 | if (onRequest) return true; 209 | }, 210 | send: function (args, xhr) { 211 | var config = xhr.config 212 | config.withCredentials = xhr.withCredentials 213 | config.body = args[0]; 214 | if (onRequest) { 215 | // In 'onRequest', we may call XHR's event handler, such as `xhr.onload`. 216 | // However, XHR's event handler may not be set until xhr.send is called in 217 | // the user's code, so we use `setTimeout` to avoid this situation 218 | var req = function () { 219 | onRequest(config, new RequestHandler(xhr)); 220 | } 221 | config.async === false ? req() : setTimeout(req) 222 | return true; 223 | } 224 | }, 225 | setRequestHeader: function (args, xhr) { 226 | // Collect request headers 227 | xhr.config.headers[args[0].toLowerCase()] = args[1]; 228 | if (onRequest) return true; 229 | }, 230 | addEventListener: function (args, xhr) { 231 | var _this = this; 232 | if (events.indexOf(args[0]) !== -1) { 233 | var handler = args[1]; 234 | getEventTarget(xhr).addEventListener(args[0], function (e) { 235 | var event = configEvent(e, _this); 236 | event.type = args[0]; 237 | event.isTrusted = true; 238 | handler.call(_this, event); 239 | }); 240 | return true; 241 | } 242 | }, 243 | getAllResponseHeaders: function (_, xhr) { 244 | var headers = xhr.resHeader 245 | if (headers) { 246 | var header = ""; 247 | for (var key in headers) { 248 | header += key + ': ' + headers[key] + '\r\n'; 249 | } 250 | return header; 251 | } 252 | }, 253 | getResponseHeader: function (args, xhr) { 254 | var headers = xhr.resHeader 255 | if (headers) { 256 | return headers[(args[0] || '').toLowerCase()]; 257 | } 258 | } 259 | }, win); 260 | 261 | return { 262 | originXhr, 263 | unProxy: unHook 264 | } 265 | } 266 | 267 | 268 | 269 | 270 | -------------------------------------------------------------------------------- /test/es.js: -------------------------------------------------------------------------------- 1 | import {proxy} from "../index"; 2 | import {testProxy} from './test' 3 | 4 | proxy({ 5 | onRequest: (config, handler) => { 6 | if (config.url === 'https://aa/') { 7 | handler.resolve({ 8 | config: config, 9 | status: 200, 10 | headers: {'content-type': 'text/text'}, 11 | response: 'hi world' 12 | 13 | }) 14 | } else { 15 | handler.next(config); 16 | } 17 | }, 18 | onError: (err, handler) => { 19 | if (err.config.url === 'https://bb/') { 20 | handler.resolve({ 21 | config: err.config, 22 | status: 200, 23 | headers: {'content-type': 'text/text'}, 24 | response: 'hi world' 25 | }) 26 | } else { 27 | handler.next(err) 28 | } 29 | }, 30 | onResponse: (response, handler) => { 31 | if (response.config.url === location.href) { 32 | handler.reject({ 33 | config: response.config, 34 | type: 'error' 35 | }) 36 | } else { 37 | handler.next(response) 38 | } 39 | } 40 | }) 41 | 42 | testProxy() 43 | 44 | proxy({ 45 | onRequest: (config, handler) => { 46 | console.log(config.url) 47 | handler.next(config); 48 | 49 | }, 50 | onError: (err, handler) => { 51 | console.log(err.type) 52 | handler.next(err) 53 | }, 54 | onResponse: (response, handler) => { 55 | console.log(response.response) 56 | handler.next(response) 57 | } 58 | }) 59 | 60 | -------------------------------------------------------------------------------- /test/hook.ts: -------------------------------------------------------------------------------- 1 | import { hook } from "../index" 2 | import { testRequest } from './test' 3 | 4 | function testHook() { 5 | testRequest(location.href); 6 | // testRequest('https://aa'); 7 | } 8 | 9 | const { unHook: unHook1, originXhr: originXhr1 } = hook({ 10 | onreadystatechange: function (xhr) { 11 | console.log("1. onreadystatechange") 12 | }, 13 | onload: function (xhr) { 14 | console.log("1. onload") 15 | // xhr.getProxy().responseText='xhr.responseText' 16 | this.responseText = "hookAjax" + xhr.responseText; 17 | }, 18 | onerror: function (xhr) { 19 | console.log("1. onerror"); 20 | }, 21 | open: function (arg, xhr) { 22 | //add tag 23 | arg[1] += "&hook_tag=1"; 24 | console.log("1. open"); 25 | 26 | }, 27 | send: function (arg, xhr) { 28 | console.log("1. send"); 29 | xhr.setRequestHeader("_custom_header_1", "ajaxhook1"); 30 | }, 31 | setRequestHeader: function (args, xhr) { 32 | console.log("1. setRequestHeader"); 33 | }, 34 | response: { 35 | getter: () => { 36 | return { data: '1 res' } 37 | }, 38 | setter(value, target) { 39 | console.log('1. set response'); 40 | return '1. set response: set response value'; 41 | }, 42 | } 43 | }); 44 | 45 | const { unHook: unHook2, originXhr: originXhr2 } = hook({ 46 | onreadystatechange: function (xhr) { 47 | console.log("2. onreadystatechange") 48 | }, 49 | onload: function (xhr) { 50 | console.log("2. onload") 51 | // xhr.getProxy().responseText='xhr.responseText' 52 | this.responseText = "hookAjax" + xhr.responseText; 53 | }, 54 | onerror: function (xhr) { 55 | console.log("2. onerror"); 56 | }, 57 | open: function (arg, xhr) { 58 | //add tag 59 | arg[1] += "?hook_tag=2"; 60 | console.log("2. open"); 61 | }, 62 | send: function (arg, xhr) { 63 | console.log("2. send"); 64 | xhr.setRequestHeader("_custom_header_2", "ajaxhook2"); 65 | }, 66 | setRequestHeader: function (args, xhr) { 67 | console.log("2. setRequestHeader") 68 | }, 69 | response: { 70 | getter: () => { 71 | return { data: '2. res' } 72 | }, 73 | setter(value, target) { 74 | console.log('2. set response'); 75 | return '2. set response'; 76 | }, 77 | } 78 | }); 79 | 80 | unHook1(); 81 | 82 | // unHook2(); 83 | 84 | testHook() 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /test/proxy.ts: -------------------------------------------------------------------------------- 1 | import { proxy } from "../index" 2 | import { testRequest } from './test' 3 | 4 | 5 | export function testProxy() { 6 | testRequest('https://cc/'); 7 | } 8 | 9 | const { unProxy: unProxy1, originXhr: originXhr1 } = proxy({ 10 | onRequest: (config, handler) => { 11 | console.log(`1. onRequest: ${config.url}`); 12 | config.headers = { 'content-type': 'text/text', customHeader1: 'customHeader1', ...config.headers }; 13 | handler.next(config); 14 | 15 | }, 16 | onError: (err, handler) => { 17 | console.log(`1. onError: ${err.config.url}`); 18 | handler.next(err); 19 | }, 20 | onResponse: (response, handler) => { 21 | console.log(`1. onResponse: ${response.config.url}`); 22 | handler.next(response); 23 | } 24 | }, window); 25 | 26 | 27 | // const { unProxy: unProxy2, originXhr: originXhr2 } = proxy({ 28 | // onRequest: (config, handler) => { 29 | // console.log(`2. onRequest: ${config.url}`); 30 | // config.headers = { 'content-type': 'text/text', customHeader2: 'customHeader2', ...config.headers }; 31 | // handler.next(config); 32 | 33 | // }, 34 | // onError: (err, handler) => { 35 | // console.log(`2. onError: ${err.config.url}`); 36 | // handler.next(err); 37 | // }, 38 | // onResponse: (response, handler) => { 39 | // console.log(`2. onResponse: ${response.config.url}`); 40 | // handler.next(response) 41 | // } 42 | // }, window); 43 | 44 | 45 | // unProxy1(); 46 | // unProxy2(); 47 | 48 | testProxy() 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | export function testRequest(url) { 2 | var xhr = new XMLHttpRequest(); 3 | xhr.open('get', url); 4 | 5 | xhr.onload = () => { 6 | xhr.response = 'xhr response has been reset'; 7 | console.log('origin onload :', xhr.response); 8 | } 9 | 10 | xhr.onerror = () => { 11 | console.log(`${url}: xhr error`); 12 | } 13 | 14 | xhr.onreadystatechange = (...args) => {} 15 | xhr.setRequestHeader('header1', 'header1'); 16 | xhr.send(); 17 | } 18 | 19 | 20 | --------------------------------------------------------------------------------