模型资源往往比较大,数据内容变动不大,可重复利用率不高。频繁请求比较浪费网络资源,加载到本地可以既可以节约请求,充分利用资源,并且可以节省用户下载时间,提高用户体验。那么怎么做才能实现我们的需求呢?
(1)存
考虑用indexDB将大量数据储存在客户端,这样可以减少从服务器获取数据,直接从本地获取数据。IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。完全可以满足我们的需求。
(2)取
取的关键在于如何从本地数据库中加载资源而不是直接加载网络资源。这两者有何不同呢?我们注意到,以GLTFLoader为例:
let manager = new THREE.LoadingManager();
let gltfloader = new THREE.GLTFLoader(manager);
gltfloader.load(url, obj => {
}, progress => {
}, err => {
})
这里的url一般是一个网络地址。如果我们将数据存储在本地,不会有这样有一个URL。那么该怎么加载存储在本地的资源呢?这就是本文主要探索的目的。
处理的关键还在于LoadingManager那里,three.js官方文档上有这样一个示例:
// 将文件拖入网页时创建的Blob或File对象。
var blobs = {'fish.gltf': blob1, 'diffuse.png': blob2, 'normal.png': blob3};
var manager = new THREE.LoadingManager();
// 使用URL回调初始化加载管理器。
var objectURLs = [];
manager.setURLModifier( ( url ) => {
url = URL.createObjectURL( blobs[ url ] );
objectURLs.push( url );
return url;
} );
// 像通常一样加载,然后撤消blob URL
var loader = new THREE.GLTFLoader( manager );
loader.load( 'fish.gltf', (gltf) => {
scene.add( gltf.scene );
objectURLs.forEach( ( url ) => URL.revokeObjectURL( url ) );
});
搞懂了这个例子,我们就知道怎么加载本地资源了。
首先,blobs对象里存模型名称类型和该模型的二进制数据。
我们把前面的资源名称类型姑且认为就是本地资源地址,我们的目的就是将它转为loader能识别的url。
objectURLs存转换后的url。
// 这是干嘛的?
manager.setURLModifier(() => {})
// LoadingManager.js:
// 查看LoadingManager的源码可以看到:
this.setURLModifier = function ( transform ) {
urlModifier = transform;
return this;
};
// setURLModifier的回调,就是自定义修改URL的函数。但是manager拿着我们定义url的函数把它赋值给urlModifier有什么用呢?
继续回到那个例子,接下来执行了loader.load方法
loader.load( 'fish.gltf', (gltf) => {
});
也就是说,经过上一步,自定义设置url函数后,该loader已经可以直接读取名称类型就可以成功加载本地资源了,那么肯定就是在loader.load方法里做了文章,查看源码GLTFLoader的load方法(别被那么多吓到,我们只看我们用到的):
// GLTFLoader.js
load: function ( url, onLoad, onProgress, onError ) {
...
// 其实GLTFLoader是继承FileLoader这个类的,我们需要继续到这个类中找答案
var loader = new THREE.FileLoader( scope.manager );
...
}
继续查看FileLoader的load方法的源码:
// FileLoader.js
load: function ( url, onLoad, onProgress, onError ) {
if ( url === undefined ) url = '';
if ( this.path !== undefined ) url = this.path + url;
// 关键就在这里了,绕来绕去,又回到manager里了,原来url在manager的resolveURL方法被处理了。
url = this.manager.resolveURL( url );
......
}
回到LoadingManager.js:
// LoadingManager.js:
this.resolveURL = function ( url ) {
if ( urlModifier ) {
return urlModifier( url );
}
return url;
};
也就是说我们自定义url函数返回的url必须是loader能读取的url。除了常规的网络url还可以是blob url等。
看看上面例子是怎么为本地资源自定义url的。
参考1
参考2
Blob URL / Object URL是一种伪协议,允许Blob和File对象用作图像,下载二进制数据链接等的URL源。
Blob URL或Object-URL与Blob或File对象一起使用。
例如,不能处理Image对象的原始字节数据,因为它不知道如何处理它。
它需要例如图像(二进制数据)通过URL加载。这适用于任何需要URL作为源的东西。
不用上传二进制数据,而是通过URL提供回来,最好使用额外的本地步骤来直接访问数据而无需通过服务器。
对于编码为Base-64的字符串的Data-URI也是更好的选择。Data-URI的问题是每个char在JavaScript中占用两个字节。
最重要的是,由于Base-64编码增加了33%。
Blob是纯粹的二进制字节数组,它不像Data-URI那样具有任何重要的开销,这使得它们处理速度越来越快。
objectURL = URL.createObjectURL(object);
URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。
这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示指定的 File 对象或 Blob 对象。
window.URL.revokeObjectURL(objectURL);
URL.revokeObjectURL() 静态方法用来释放一个之前已经存在的、通过调用 URL.createObjectURL() 创建的 URL 对象。
当你结束使用某个 URL 对象之后,应该通过调用这个方法来让浏览器知道不用在内存中继续保留对这个文件的引用了。
你可以在 sourceopen 被处理之后的任何时候调用 revokeObjectURL()。
这是因为 createObjectURL() 仅仅意味着将一个媒体元素的 src 属性关联到一个 MediaSource 对象上去。
调用revokeObjectURL() 使这个潜在的对象回到原来的地方,允许平台在合适的时机进行垃圾收集。
其实在GLTFLoader.js也定义了resolveURL方法并且在GLTFParser.prototype.loadBuffer和GLTFParser.prototype.loadTexture都有调用
// 看来loader加载某些特殊类型资源,除了支持常规的网络url,还要支持base64地址和blob url。
function resolveURL( url, path ) {
// Invalid URL
if ( typeof url !== 'string' || url === '' ) return '';
// Absolute URL http://,https://,//
if ( /^(https?:)?\/\//i.test( url ) ) return url;
// Data URI
if ( /^data:.*,.*$/i.test( url ) ) return url;
// Blob URL
if ( /^blob:.*$/i.test( url ) ) return url;
// Relative URL
return path + url;
}
......
GLTFParser.prototype.loadBuffer = function ( bufferIndex ) {
......
return new Promise( function ( resolve, reject ) {
// 这里
loader.load( resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () {
reject( new Error( 'THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ));
});
});
}
GLTFParser.prototype.loadTexture = function ( textureIndex ) {
......
return Promise.resolve( sourceURI ).then( function ( sourceURI ) {
var loader = THREE.Loader.Handlers.get( sourceURI );
if (!loader ) {
loader = textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ]
? parser.extensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].ddsLoader
: textureLoader;
}
return new Promise( function ( resolve, reject ) {
// 这里
loader.load( resolveURL( sourceURI, options.path ), resolve, undefined, reject );
});
}).then( function ( texture ) {
if ( isObjectURL === true ) {
URL.revokeObjectURL( sourceURI );
}
texture.flipY = false;
if ( textureDef.name !== undefined ) texture.name = textureDef.name;
if ( source.mimeType in MIME_TYPE_FORMATS ) {
texture.format = MIME_TYPE_FORMATS[ source.mimeType ];
}
var samplers = json.samplers || {};
var sampler = samplers[ textureDef.sampler ] || {};
texture.magFilter = WEBGL_FILTERS[ sampler.magFilter ] || THREE.LinearFilter;
texture.minFilter = WEBGL_FILTERS[ sampler.minFilter ] || THREE.LinearMipMapLinearFilter;
texture.wrapS = WEBGL_WRAPPINGS[ sampler.wrapS ] || THREE.RepeatWrapping;
texture.wrapT = WEBGL_WRAPPINGS[ sampler.wrapT ] || THREE.RepeatWrapping;
return texture;
});
}