FileLoader.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import { Cache } from './Cache.js';
  2. import { Loader } from './Loader.js';
  3. import { warn } from '../utils.js';
  4. const loading = {};
  5. class HttpError extends Error {
  6. constructor( message, response ) {
  7. super( message );
  8. this.response = response;
  9. }
  10. }
  11. /**
  12. * A low level class for loading resources with the Fetch API, used internally by
  13. * most loaders. It can also be used directly to load any file type that does
  14. * not have a loader.
  15. *
  16. * This loader supports caching. If you want to use it, add `THREE.Cache.enabled = true;`
  17. * once to your application.
  18. *
  19. * ```js
  20. * const loader = new THREE.FileLoader();
  21. * const data = await loader.loadAsync( 'example.txt' );
  22. * ```
  23. *
  24. * @augments Loader
  25. */
  26. class FileLoader extends Loader {
  27. /**
  28. * Constructs a new file loader.
  29. *
  30. * @param {LoadingManager} [manager] - The loading manager.
  31. */
  32. constructor( manager ) {
  33. super( manager );
  34. /**
  35. * The expected mime type. Valid values can be found
  36. * [here](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString#mimetype)
  37. *
  38. * @type {string}
  39. */
  40. this.mimeType = '';
  41. /**
  42. * The expected response type.
  43. *
  44. * @type {('arraybuffer'|'blob'|'document'|'json'|'')}
  45. * @default ''
  46. */
  47. this.responseType = '';
  48. /**
  49. * Used for aborting requests.
  50. *
  51. * @private
  52. * @type {AbortController}
  53. */
  54. this._abortController = new AbortController();
  55. }
  56. /**
  57. * Starts loading from the given URL and pass the loaded response to the `onLoad()` callback.
  58. *
  59. * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
  60. * @param {function(any)} onLoad - Executed when the loading process has been finished.
  61. * @param {onProgressCallback} [onProgress] - Executed while the loading is in progress.
  62. * @param {onErrorCallback} [onError] - Executed when errors occur.
  63. * @return {any|undefined} The cached resource if available.
  64. */
  65. load( url, onLoad, onProgress, onError ) {
  66. if ( url === undefined ) url = '';
  67. if ( this.path !== undefined ) url = this.path + url;
  68. url = this.manager.resolveURL( url );
  69. const cached = Cache.get( `file:${url}` );
  70. if ( cached !== undefined ) {
  71. this.manager.itemStart( url );
  72. setTimeout( () => {
  73. if ( onLoad ) onLoad( cached );
  74. this.manager.itemEnd( url );
  75. }, 0 );
  76. return cached;
  77. }
  78. // Check if request is duplicate
  79. if ( loading[ url ] !== undefined ) {
  80. loading[ url ].push( {
  81. onLoad: onLoad,
  82. onProgress: onProgress,
  83. onError: onError
  84. } );
  85. return;
  86. }
  87. // Initialise array for duplicate requests
  88. loading[ url ] = [];
  89. loading[ url ].push( {
  90. onLoad: onLoad,
  91. onProgress: onProgress,
  92. onError: onError,
  93. } );
  94. // create request
  95. const req = new Request( url, {
  96. headers: new Headers( this.requestHeader ),
  97. credentials: this.withCredentials ? 'include' : 'same-origin',
  98. signal: ( typeof AbortSignal.any === 'function' ) ? AbortSignal.any( [ this._abortController.signal, this.manager.abortController.signal ] ) : this._abortController.signal
  99. } );
  100. // record states ( avoid data race )
  101. const mimeType = this.mimeType;
  102. const responseType = this.responseType;
  103. // start the fetch
  104. fetch( req )
  105. .then( response => {
  106. if ( response.status === 200 || response.status === 0 ) {
  107. // Some browsers return HTTP Status 0 when using non-http protocol
  108. // e.g. 'file://' or 'data://'. Handle as success.
  109. if ( response.status === 0 ) {
  110. warn( 'FileLoader: HTTP Status 0 received.' );
  111. }
  112. // Workaround: Checking if response.body === undefined for Alipay browser #23548
  113. if ( typeof ReadableStream === 'undefined' || response.body === undefined || response.body.getReader === undefined ) {
  114. return response;
  115. }
  116. const callbacks = loading[ url ];
  117. const reader = response.body.getReader();
  118. // Nginx needs X-File-Size check
  119. // https://serverfault.com/questions/482875/why-does-nginx-remove-content-length-header-for-chunked-content
  120. const contentLength = response.headers.get( 'X-File-Size' ) || response.headers.get( 'Content-Length' );
  121. const total = contentLength ? parseInt( contentLength ) : 0;
  122. const lengthComputable = total !== 0;
  123. let loaded = 0;
  124. // periodically read data into the new stream tracking while download progress
  125. const stream = new ReadableStream( {
  126. start( controller ) {
  127. readData();
  128. function readData() {
  129. reader.read().then( ( { done, value } ) => {
  130. if ( done ) {
  131. controller.close();
  132. } else {
  133. loaded += value.byteLength;
  134. const event = new ProgressEvent( 'progress', { lengthComputable, loaded, total } );
  135. for ( let i = 0, il = callbacks.length; i < il; i ++ ) {
  136. const callback = callbacks[ i ];
  137. if ( callback.onProgress ) callback.onProgress( event );
  138. }
  139. controller.enqueue( value );
  140. readData();
  141. }
  142. }, ( e ) => {
  143. controller.error( e );
  144. } );
  145. }
  146. }
  147. } );
  148. return new Response( stream );
  149. } else {
  150. throw new HttpError( `fetch for "${response.url}" responded with ${response.status}: ${response.statusText}`, response );
  151. }
  152. } )
  153. .then( response => {
  154. switch ( responseType ) {
  155. case 'arraybuffer':
  156. return response.arrayBuffer();
  157. case 'blob':
  158. return response.blob();
  159. case 'document':
  160. return response.text()
  161. .then( text => {
  162. const parser = new DOMParser();
  163. return parser.parseFromString( text, mimeType );
  164. } );
  165. case 'json':
  166. return response.json();
  167. default:
  168. if ( mimeType === '' ) {
  169. return response.text();
  170. } else {
  171. // sniff encoding
  172. const re = /charset="?([^;"\s]*)"?/i;
  173. const exec = re.exec( mimeType );
  174. const label = exec && exec[ 1 ] ? exec[ 1 ].toLowerCase() : undefined;
  175. const decoder = new TextDecoder( label );
  176. return response.arrayBuffer().then( ab => decoder.decode( ab ) );
  177. }
  178. }
  179. } )
  180. .then( data => {
  181. // Add to cache only on HTTP success, so that we do not cache
  182. // error response bodies as proper responses to requests.
  183. Cache.add( `file:${url}`, data );
  184. const callbacks = loading[ url ];
  185. delete loading[ url ];
  186. for ( let i = 0, il = callbacks.length; i < il; i ++ ) {
  187. const callback = callbacks[ i ];
  188. if ( callback.onLoad ) callback.onLoad( data );
  189. }
  190. } )
  191. .catch( err => {
  192. // Abort errors and other errors are handled the same
  193. const callbacks = loading[ url ];
  194. if ( callbacks === undefined ) {
  195. // When onLoad was called and url was deleted in `loading`
  196. this.manager.itemError( url );
  197. throw err;
  198. }
  199. delete loading[ url ];
  200. for ( let i = 0, il = callbacks.length; i < il; i ++ ) {
  201. const callback = callbacks[ i ];
  202. if ( callback.onError ) callback.onError( err );
  203. }
  204. this.manager.itemError( url );
  205. } )
  206. .finally( () => {
  207. this.manager.itemEnd( url );
  208. } );
  209. this.manager.itemStart( url );
  210. }
  211. /**
  212. * Sets the expected response type.
  213. *
  214. * @param {('arraybuffer'|'blob'|'document'|'json'|'')} value - The response type.
  215. * @return {FileLoader} A reference to this file loader.
  216. */
  217. setResponseType( value ) {
  218. this.responseType = value;
  219. return this;
  220. }
  221. /**
  222. * Sets the expected mime type of the loaded file.
  223. *
  224. * @param {string} value - The mime type.
  225. * @return {FileLoader} A reference to this file loader.
  226. */
  227. setMimeType( value ) {
  228. this.mimeType = value;
  229. return this;
  230. }
  231. /**
  232. * Aborts ongoing fetch requests.
  233. *
  234. * @return {FileLoader} A reference to this instance.
  235. */
  236. abort() {
  237. this._abortController.abort();
  238. this._abortController = new AbortController();
  239. return this;
  240. }
  241. }
  242. export { FileLoader };
粤ICP备19079148号