Browse Source

Add XMLHttpRequest and related polyfills #44

Ben Hsieh 8 years ago
parent
commit
0eaf00da14

+ 23
- 1
src/ios/RNFetchBlobNetwork.m View File

@@ -137,15 +137,36 @@ NSOperationQueue *taskQueue;
137 137
     expectedBytes = [response expectedContentLength];
138 138
  
139 139
     NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
140
+    NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
140 141
     if ([response respondsToSelector:@selector(allHeaderFields)])
141 142
     {
142 143
         NSDictionary *headers = [httpResponse allHeaderFields];
144
+        NSString * respType = [[headers valueForKey:@"Content-Type"] lowercaseString];
145
+        if([headers valueForKey:@"Content-Type"] != nil)
146
+        {
147
+            if([respType containsString:@"text/plain"])
148
+            {
149
+                respType = @"text";
150
+            }
151
+            else if([respType containsString:@"application/json"])
152
+            {
153
+                respType = @"json";
154
+            }
155
+            else
156
+            {
157
+                respType = @"blob";
158
+            }
159
+        }
160
+        else
161
+            respType = @"";
143 162
         [self.bridge.eventDispatcher
144 163
          sendDeviceEventWithName: EVENT_STATE_CHANGE
145 164
          body:@{
146 165
                 @"taskId": taskId,
147 166
                 @"state": @"2",
148
-                @"headers": headers
167
+                @"headers": headers,
168
+                @"respType" : respType,
169
+                @"status": [NSString stringWithFormat:@"%d", statusCode ]
149 170
             }
150 171
          ];
151 172
     }
@@ -200,6 +221,7 @@ NSOperationQueue *taskQueue;
200 221
     }
201 222
     // base64 response
202 223
     else {
224
+        NSString * res = [[NSString alloc] initWithData:respData encoding:NSUTF8StringEncoding];
203 225
         callback(@[error == nil ? [NSNull null] : [error localizedDescription], [respData base64EncodedStringWithOptions:0]]);
204 226
     }
205 227
 }

+ 16
- 7
src/ios/RNFetchBlobReqBuilder.m View File

@@ -110,13 +110,22 @@
110 110
                 }
111 111
                 // otherwise convert it as BASE64 data string
112 112
                 else {
113
-                    blobData = [[NSData alloc] initWithBase64EncodedString:body options:0];
114
-                    [request setHTTPBody:blobData];
115
-                }
116
-                
117
-                if([mheaders valueForKey:@"content-type"] == nil)
118
-                {
119
-                    [mheaders setValue:@"application/octet-stream" forKey:@"content-type"];
113
+                    // the body is BASE64 encoded string
114
+                    if(([mheaders valueForKey:@"content-type"] == nil && [mheaders valueForKey:@"Content-Type"] == nil) ||
115
+                       ([[[mheaders valueForKey:@"content-type"] lowercaseString] isEqualToString:@"application/octet-stream"] ||
116
+                        [[[mheaders valueForKey:@"Content-Type"] lowercaseString] isEqualToString:@"application/octet-stream"]))
117
+                    {
118
+                        blobData = [[NSData alloc] initWithBase64EncodedString:body options:0];
119
+                        [mheaders setValue:@"application/octet-stream" forKey:@"Content-Type"];
120
+                        [request setHTTPBody:blobData];
121
+                        size = [blobData length];
122
+                    }
123
+                    // use the body string as is
124
+                    else
125
+                    {
126
+                        size = [body length];
127
+                        [request setHTTPBody: [body dataUsingEncoding:NSUTF8StringEncoding]];
128
+                    }
120 129
                 }
121 130
                 
122 131
             }

+ 7
- 0
src/polyfill/EventTarget.js View File

@@ -1,16 +1,21 @@
1 1
 // Copyright 2016 wkh237@github. All rights reserved.
2 2
 // Use of this source code is governed by a MIT-style license that can be
3 3
 // found in the LICENSE file.
4
+import Log from '../utils/log.js'
5
+
6
+const log = new Log('EventTarget')
4 7
 
5 8
 export default class EventTarget {
6 9
 
7 10
   listeners : any;
8 11
 
9 12
   constructor() {
13
+    log.info('constructor called')
10 14
     this.listeners = {}
11 15
   }
12 16
 
13 17
   addEventListener(type:string, cb : () => void) {
18
+    log.info('add event listener', type, cb)
14 19
     if(!(type in this.listeners)) {
15 20
       this.listeners[type] = []
16 21
     }
@@ -18,6 +23,7 @@ export default class EventTarget {
18 23
   }
19 24
 
20 25
   removeEventListener(type:string, cb:() => any) {
26
+    log.info('remove event listener', type, cb)
21 27
     if(!(type in this.listeners))
22 28
       return
23 29
     let handlers = this.listeners[type]
@@ -30,6 +36,7 @@ export default class EventTarget {
30 36
   }
31 37
 
32 38
   dispatchEvent(event:Event) {
39
+    log.info('dispatch event', event)
33 40
     if(!(event.type in this.listeners))
34 41
       return
35 42
     let handlers = this.listeners[event.type]

+ 182
- 74
src/polyfill/XMLHttpRequest.js View File

@@ -3,30 +3,30 @@
3 3
 // found in the LICENSE file.
4 4
 
5 5
 import RNFetchBlob from '../index.js'
6
-import EventTarget from './EventTarget.js'
7
-
8
-export default class XMLHttpRequest extends EventTarget{
9
-
10
-  onreadystatechange : () => void;
11
-  onabort : () => void;
12
-  onerror : () => void;
13
-  onload : () => void;
14
-  onloadstart : () => void;
15
-  onprogress : () => void;
16
-  ontimeout : () => void;
17
-  onloadend : () => void;
18
-
19
-  readyState : number;
20
-  response : any;
21
-  responseText : any;
22
-  responseHeaders : any;
23
-  responseType : '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text';
6
+import XMLHttpRequestEventTarget from './XMLHttpRequestEventTarget.js'
7
+
8
+const UNSENT = 0
9
+const OPENED = 1
10
+const HEADERS_RECEIVED = 2
11
+const LOADING = 3
12
+const DONE = 4
13
+
14
+export default class XMLHttpRequest extends XMLHttpRequestEventTarget{
15
+
16
+  _onreadystatechange : () => void;
17
+
18
+  _readyState : number = UNSENT;
19
+  _response : any = '';
20
+  _responseText : any = '';
21
+  _responseHeaders : any = '';
22
+  _responseType : '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' = '';
24 23
   // TODO : not suppoted for now
25
-  responseURL : null;
26
-  responseXML : null;
27
-  status : number;
28
-  statusText : string;
29
-  timeout : number;
24
+  _responseURL : null = '';
25
+  _responseXML : null = '';
26
+  _status : number;
27
+  _statusText : string;
28
+  _timeout : number = 0;
29
+  _upload : XMLHttpRequestEventTarget;
30 30
 
31 31
   // RNFetchBlob compatible data structure
32 32
   _config : RNFetchBlobConfig;
@@ -39,34 +39,40 @@ export default class XMLHttpRequest extends EventTarget{
39 39
   // `cancel` methods.
40 40
   _task: any;
41 41
 
42
+  static get UNSENT() {
43
+    return UNSENT
44
+  }
45
+
46
+  static get OPENED() {
47
+    return OPENED
48
+  }
49
+
50
+  static get HEADERS_RECEIVED() {
51
+    return HEADERS_RECEIVED
52
+  }
53
+
54
+  static get LOADING() {
55
+    return LOADING
56
+  }
57
+
58
+  static get DONE() {
59
+    return DONE
60
+  }
61
+
42 62
   constructor(...args) {
43 63
     super()
44 64
     console.log('XMLHttpRequest constructor called', args)
45 65
     this._config = {}
46 66
     this._args = {}
47 67
     this._headers = {}
48
-    this.readyState = 0
49
-    this.response = null
50
-    this.responseText = null
51
-    this.responseHeaders = null
52 68
   }
53 69
 
54 70
   // XMLHttpRequest.open, always async, user and password not supported.
55 71
   open(method:string, url:string, async:true, user:any, password:any) {
56
-    console.log('XMLHttpRequest open called', method, url, async, user, password)
72
+    console.log('XMLHttpRequest open ', method, url, async, user, password)
57 73
     this._method = method
58 74
     this._url = url
59
-    this.readyState = 1
60
-    if(this.onload)
61
-      this.onload()
62
-    if(this.onloadstart)
63
-      this.onloadstart()
64
-    this._invokeOnStateChange()
65
-  }
66
-
67
-  addEventListener(event, listener) {
68
-    console.log('XMLHttpRequest add listener', event, listener.toString())
69
-    this.addEventListener(event, listener)
75
+    this.readyState = XMLHttpRequest.OPENED
70 76
   }
71 77
 
72 78
   /**
@@ -74,28 +80,26 @@ export default class XMLHttpRequest extends EventTarget{
74 80
    * @param  {any} body Body in RNfetchblob flavor
75 81
    */
76 82
   send(body) {
77
-    console.log('XMLHttpRequest send called', body)
83
+    console.log('XMLHttpRequest send ', body)
78 84
     let {_method, _url, _headers } = this
79 85
     console.log('sending request with args', _method, _url, _headers, body)
86
+    this._upload = new XMLHttpRequestEventTarget()
87
+    this.dispatchEvent('loadstart')
88
+    if(this.onloadstart)
89
+      this.onloadstart()
80 90
 
81 91
     this._task = RNFetchBlob.fetch(_method, _url, _headers, body)
82 92
     this._task
83
-        .stateChange((e) => {
84
-          console.log('state change')
85
-          if(e.state === "2") {
86
-            this.readyState = 2
87
-            this.responseHeaders = e.headers
88
-          }
89
-          this._invokeOnStateChange()
90
-        })
91
-        .uploadProgress(this._progressEvent)
92
-        .onProgress(this._progressEvent)
93
-        .then(this._onDone)
94
-        .catch(this._onError)
93
+        .stateChange(this._headerReceived.bind(this))
94
+        .uploadProgress(this._progressEvent.bind(this))
95
+        .progress(this._progressEvent.bind(this))
96
+        .catch(this._onError.bind(this))
97
+        .then(this._onDone.bind(this))
95 98
   }
96 99
 
97 100
   overrideMimeType(mime:string) {
98
-    this.headers['content-type'] = mime
101
+    console.log('XMLHttpRequest overrideMimeType', mime)
102
+    this._headers['content-type'] = mime
99 103
   }
100 104
 
101 105
   setRequestHeader(name, value) {
@@ -104,7 +108,7 @@ export default class XMLHttpRequest extends EventTarget{
104 108
   }
105 109
 
106 110
   abort() {
107
-    console.log('XMLHttpRequest abort called', this._task)
111
+    console.log('XMLHttpRequest abort ')
108 112
     if(!this._task)
109 113
       return
110 114
     this._task.cancel((err) => {
@@ -113,7 +117,7 @@ export default class XMLHttpRequest extends EventTarget{
113 117
       }
114 118
       if(this.onabort)
115 119
         this.onabort()
116
-      if(!err) {
120
+      if(err) {
117 121
         e.detail = err
118 122
         e.type = 'error'
119 123
         this.dispatchEvent('error', e)
@@ -126,7 +130,7 @@ export default class XMLHttpRequest extends EventTarget{
126 130
   }
127 131
 
128 132
   getResponseHeader(field:string):string | null {
129
-
133
+    console.log('XMLHttpRequest get header', field)
130 134
     if(!this.responseHeaders)
131 135
       return null
132 136
     return this.responseHeaders[field] || null
@@ -134,7 +138,7 @@ export default class XMLHttpRequest extends EventTarget{
134 138
   }
135 139
 
136 140
   getAllResponseHeaders():string | null {
137
-
141
+    console.log('XMLHttpRequest get all headers')
138 142
     if(!this.responseHeaders)
139 143
       return null
140 144
     let result = ''
@@ -145,17 +149,33 @@ export default class XMLHttpRequest extends EventTarget{
145 149
     return result
146 150
   }
147 151
 
152
+  _headerReceived(e) {
153
+    console.log('header received ', e)
154
+    this.responseURL = this._url
155
+    if(e.state === "2") {
156
+      this.readyState = XMLHttpRequest.HEADERS_RECEIVED
157
+      this.responseHeaders = e.headers
158
+      this._responseText = e.status
159
+      this._responseType = e.respType || ''
160
+      this.status = Math.floor(e.status)
161
+    }
162
+  }
163
+
148 164
   _progressEvent(send:number, total:number) {
165
+    console.log(this.readyState)
166
+    if(this.readyState === XMLHttpRequest.HEADERS_RECEIVED)
167
+      this.readyState = XMLHttpRequest.LOADING
149 168
     let lengthComputable = false
169
+    let e = { lengthComputable }
150 170
     if(total && total >= 0)
151
-        lengthComputable = true
171
+        e.lengthComputable = true
152 172
     else {
153
-      this.dispatchEvent('progress',
154
-        { loaded : send, total, lengthComputable })
173
+      Object.assign(e, { loaded : send, total })
155 174
     }
156 175
 
157 176
     if(this.onprogress)
158
-      this.onprogress({loaded : send, total, lengthComputable})
177
+      this.onprogress(e)
178
+    this.dispatchEvent('progress', e)
159 179
   }
160 180
 
161 181
   _onError(err) {
@@ -163,42 +183,130 @@ export default class XMLHttpRequest extends EventTarget{
163 183
     this.statusText = err
164 184
     this.status = String(err).match(/\d+/)
165 185
     this.status = this.status ? Math.floor(this.status) : 404
166
-    this.readyState = 4
186
+    this.readyState = XMLHttpRequest.DONE
167 187
     if(String(err).match('timeout') !== null) {
188
+      this.dispatchEvent('timeout')
168 189
       if(this.ontimeout)
169 190
         this.ontimeout()
170 191
     }
171 192
     else if(this.onerror) {
193
+      this.dispatchEvent('error')
172 194
       this.onerror({
173 195
         type : 'error',
174 196
         detail : err
175 197
       })
176 198
     }
177
-    this._invokeOnStateChange()
178 199
   }
179 200
 
180 201
   _onDone(resp) {
181 202
     console.log('XMLHttpRequest done', resp.text())
182 203
     this.statusText = '200 OK'
183
-    this.status = 200
204
+    this._status = 200
184 205
     switch(resp.type) {
185 206
       case 'base64' :
186
-      this.responseType = 'text'
187
-      this.responseText = resp.text()
188
-      this.response = this.responseText
207
+        if(this.responseType === 'json') {
208
+            this._responseText = resp.text()
209
+            this._response = resp.json()
210
+        }
211
+        else {
212
+          this._responseType = resp.text()
213
+          this._response = this.responseText
214
+        }
189 215
       break;
190 216
       case 'path' :
191
-      this.responseType = 'blob'
192
-      this.response = resp.blob()
217
+        this.responseType = 'blob'
218
+        this.response = resp.blob()
193 219
       break;
194 220
     }
195
-    this.readyState = 4
196
-    this._invokeOnStateChange()
221
+    this.dispatchEvent('loadend')
222
+    if(this.onloadend)
223
+      this.onloadend()
224
+    this.dispatchEvent('load')
225
+    if(this._onload)
226
+      this._onload()
227
+    this.readyState = XMLHttpRequest.DONE
228
+  }
229
+
230
+  set onreadystatechange(fn:() => void) {
231
+    console.log('XMLHttpRequest set onreadystatechange', fn.toString())
232
+    this._onreadystatechange = fn
233
+  }
234
+
235
+  set readyState(val:number) {
236
+
237
+    console.log('XMLHttpRequest ready state changed to ', val)
238
+    this._readyState = val
239
+    if(this._onreadystatechange) {
240
+      console.log('trigger onreadystatechange event', this._readyState)
241
+      console.log(this._onreadystatechange)
242
+      this.dispatchEvent('readystatechange', )
243
+      if(this._onreadystatechange)
244
+        this._onreadystatechange()
245
+    }
246
+  }
247
+
248
+  get readyState() {
249
+    console.log('get readyState', this._readyState)
250
+    return this._readyState
251
+  }
252
+
253
+  get status() {
254
+    console.log('get status', this._status)
255
+    return this._status
256
+  }
257
+
258
+  set statusText(val) {
259
+    this._statusText = val
260
+  }
261
+
262
+  get statusText() {
263
+    console.log('get statusText', this._statusText)
264
+    return this._statusText
265
+  }
266
+
267
+  set response(val) {
268
+    console.log('set response', val)
269
+    this._response = val
270
+  }
271
+
272
+  get response() {
273
+    console.log('get response', this._response)
274
+    return this._response
275
+  }
276
+
277
+  get responseText() {
278
+    console.log('get responseText', this._responseText)
279
+    return this._responseText
280
+  }
281
+
282
+  get responseURL() {
283
+    console.log('get responseURL', this._responseURL)
284
+    return this._responseURL
285
+  }
286
+
287
+  get responseHeaders() {
288
+    console.log('get responseHeaders', this._responseHeaders)
289
+    return this._responseHeaders
290
+  }
291
+
292
+  set timeout(val) {
293
+    console.log('set timeout', this._timeout)
294
+    this._timeout = val
295
+  }
296
+
297
+  get timeout() {
298
+    console.log('get timeout', this._timeout)
299
+    return this._timeout
300
+  }
301
+
302
+  get upload() {
303
+    console.log('get upload', this._upload)
304
+    return this._upload
197 305
   }
198 306
 
199
-  _invokeOnStateChange() {
200
-    if(this.onreadystatechange)
201
-      this.onreadystatechange()
307
+  get responseType() {
308
+    console.log('get response type', this._responseType)
309
+    return this._responseType
202 310
   }
203 311
 
204 312
 }

+ 86
- 0
src/polyfill/XMLHttpRequestEventTarget.js View File

@@ -0,0 +1,86 @@
1
+// Copyright 2016 wkh237@github. All rights reserved.
2
+// Use of this source code is governed by a MIT-style license that can be
3
+// found in the LICENSE file.
4
+import EventTarget from './EventTarget.js'
5
+import Log from '../utils/log.js'
6
+
7
+const log = new Log('XMLHttpRequestEventTarget')
8
+
9
+export default class XMLHttpRequestEventTarget extends EventTarget {
10
+
11
+  _onabort : (e:Event) => void;
12
+  _onerror : (e:Event) => void;
13
+  _onload : (e:Event) => void;
14
+  _onloadstart : (e:Event) => void;
15
+  _onprogress : (e:Event) => void;
16
+  _ontimeout : (e:Event) => void;
17
+  _onloadend : (e:Event) => void;
18
+
19
+  constructor() {
20
+    super()
21
+    log.info('constructor called')
22
+  }
23
+
24
+  set onabort(fn:(e:Event) => void) {
25
+    log.info('set onabort')
26
+    this._onabort = fn
27
+  }
28
+
29
+  get onabort() {
30
+    return this._onabort
31
+  }
32
+  set onerror(fn:(e:Event) => void) {
33
+    log.info('set onerror')
34
+    this._onerror = fn
35
+  }
36
+
37
+  get onerror() {
38
+    return this._onerror
39
+  }
40
+
41
+  set onload(fn:(e:Event) => void) {
42
+    log.info('set onload')
43
+    this._onload = fn
44
+  }
45
+
46
+  get onload() {
47
+    return this._onload
48
+  }
49
+
50
+  set onloadstart(fn:(e:Event) => void) {
51
+    log.info('set onloadstart')
52
+    this._onloadstart = fn
53
+  }
54
+
55
+  get onloadstart() {
56
+    return this._onloadstart
57
+  }
58
+
59
+  set onprogress(fn:(e:Event) => void) {
60
+    log.info('set onprogress')
61
+    this._onprogress = fn
62
+  }
63
+
64
+  get onprogress() {
65
+    return this._onprogress
66
+  }
67
+
68
+  set ontimeout(fn:(e:Event) => void) {
69
+    log.info('set ontimeout')
70
+    this._ontimeout = fn
71
+  }
72
+
73
+  get ontimeout() {
74
+    return this._ontimeout
75
+  }
76
+
77
+  set onloadend(fn:(e:Event) => void) {
78
+    log.info('set onloadend')
79
+    this._onloadend = fn
80
+  }
81
+
82
+  get onloadend() {
83
+    return this._onloadend
84
+  }
85
+
86
+}

+ 21
- 0
src/utils/log.js View File

@@ -0,0 +1,21 @@
1
+export default class Log {
2
+
3
+  _name:string;
4
+
5
+  constructor(name:string) {
6
+    this._name = name
7
+  }
8
+
9
+  info(...args) {
10
+    console.log(this._name, '-info:', ...args)
11
+  }
12
+
13
+  debug(...args) {
14
+    console.log(this._name, '-debug:', ...args)
15
+  }
16
+
17
+  error(...args) {
18
+    console.log(this._name, '-error:', ...args)
19
+  }
20
+
21
+}