使用 Zip 加速 CocosWeb 加载

還沒試過,這一兩天下班來搞一下

1赞

新增拦截loadBundle
这样可以动态加载zip

const loadBundle = assetManager.loadBundle.bind(assetManager);
assetManager.loadBundle = function (nameOrUrl: string, ...args) {
    const zipBundle = window["zipBundle"] || [];
    if (zipBundle.indexOf(nameOrUrl) > -1) {
        ZipLoader.ins.loadZip(`${window["__remoteUrl__"]}remote/${nameOrUrl}`)
            .then(() => {
                loadBundle(nameOrUrl, ...args);
            });
    } else {
        loadBundle(nameOrUrl,...args);
    }
};

我按你的改了以后,发现.js不能被加载,因为downscript不是直接走的http,或者你那里有什么好的方案,可以告知一下吗

这个模型加载真的是头疼的问题,感谢大佬分享。

1赞

大佬请教一下,我是用3.8.4打开demo工程的,但是assetsUrlRecordList.json文件里面一直没有内容的,也没有看到F12里面有什么截图里面的资源数组的log出现,这个是怎么回事啊?
而且还有个问题:debug.ts:79 [ZipLoader] The requested asset not in cache: /query-extname/b59e0761-ea04-4a78-b7c0-1672219c7f18
这个是其他插件的query-extname,也被tsc监控了很奇怪,我明明是在webzip文件夹里面运行的tsc啊

1赞

這個真的是比較複雜的一步:

  1. 在 Assets 中找到 web-zip-bundle 資料夾
  2. 在資料夾中找到 zip-loader-boot 場景
  3. 場景中會有 ZipLoader 組件,在 Load Next Scen 欄位填入你的項目原本啟動的場景名稱。
  4. 在 Build Setting 中設定 zip-loader-boot 為第一個載入場景 (Start Scene)。
  5. 建置並執行
  6. 瀏覽器執行中按下 F12 開啟 Develop Console
  7. 接著 ALT + W (Mac: option + W)
  8. Console 會跑出 assetsUrlRecordList.json 的內容
  9. 複製貼回 assetsUrlRecordList.json 檔案中
  10. 再重新建置一次專案即可
2赞

第7步没有这个热键功能的吧?你在代码里面做了按键监听了吗?
没有assetsUrlRecordList.json的内容是不是这个就没有优化效果了啊?我测试的两个方法不管怎么设置都是一样request数量(本地浏览器调试)。

1赞

需要勾选调试模式

1赞

感謝幫忙解答,我自己一時間都忘記這個設定要勾,ALT + W (Mac: option + W) 才會生效。
:pray:

是的,專案建置時會依照這個List的內容,將資源打包入zip。

然后 在 3.74 上面 image 这里要改成image 不然请求 都失败了 我不知道 是不是 我别的地方有改动

还是没看明白怎么用,连怎么验证到底有没有生效的方法都没懂啊。

import { assetManager, log} from “cc”;

import * as zip from “jszip”;

const JSZip = zip[“default”];

export class ZipManager {

private static _instance: ZipManager;

private _caches: Map<string, JSZip> = new Map();

private _fileCaches: Map<string, any> = new Map();

private _cachesKey = ""



public static get instance(): ZipManager {

    if (!this._instance) {

        this._instance = new ZipManager();

    }

    return this._instance;

}

private _webZipDownloader(url: string, options: any, onComplete: Function) {

    const xhr = new XMLHttpRequest();

    xhr.responseType = 'arraybuffer';

    xhr.open('GET', url, true);

   

    if (options.onProgress) {

        xhr.onprogress = (e) => {

            if (e.lengthComputable) {

                options.onProgress(e.loaded / e.total);

            }

        };

    }

   

    xhr.onload = () => {

        if (xhr.status === 200 || xhr.status === 0) {

            onComplete(null, xhr.response);

        } else {

            onComplete(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));

        }

    };

   

    xhr.onerror = () => {

        onComplete(new Error('Download failed'));

    };

   

    xhr.send();

}

private _webZipParser(zipData: ArrayBuffer, options: any, onComplete: Function) {

    JSZip.loadAsync(zipData)

        .then(zip => onComplete(null, zip))

        .catch(err => onComplete(err));

}

constructor(){

    assetManager.downloader.maxConcurrency = 6;

    assetManager.downloader.maxRequestsPerFrame = 3;

   

    // 2. 注册自定义扩展名

    assetManager.downloader.register('.zip', this._webZipDownloader);

    assetManager.parser.register('.zip', this._webZipParser);

}

initZip(){

    const originalDownload = assetManager.downloader.download;

    assetManager.downloader["oldDownload"] = originalDownload

    let _zip = this._caches.get(this._cachesKey)

    assetManager.downloader.download = (id: string, url: string, type: string, options: Record<string, any>, onComplete: ((err: Error | null, data?: any) => void)): void => {

        if( type == ".mp3"

            ||type == ".ccon" )

        {

            assetManager.downloader["oldDownload"](id,url,type, options, onComplete);

        }

        else

        {

            ZipManager.instance.getFileFromZip(_zip,url,type,(err,data)=>{

                if(err != null){

                    assetManager.downloader["oldDownload"](id,url,type, options, onComplete);

                }

                else{

                    if(type == ".cconb"){

                        let ccon = cc.internal.decodeCCONBinary(new Uint8Array(data))

                        onComplete(null,ccon);

                    }

                    else{

                        onComplete(err,data);

                    }

                }

            });

        }

       

    };

    const oldOpen = XMLHttpRequest.prototype.open

    XMLHttpRequest.prototype.open = function(method,url){

        if(ZipManager.instance.hasKey(url as string)){

            this['zipCacheUrl'] = url

        }

        return oldOpen.apply(this,arguments)

    }

    const oldSend = XMLHttpRequest.prototype.send

    XMLHttpRequest.prototype.send = function(data){

        let _url = this['zipCacheUrl']

        if(_url != null && _url.length > 0){

            if(ZipManager.instance._fileCaches.has(_url) == false){

                ZipManager.instance.getFileFromZip(_zip,_url,'.'+this.responseType,()=>{

                    this.onload()

                })

            }

            return

        }

        else{

            return oldSend.apply(this,arguments)

        }

    }

    const accessor = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype,'response');

    Object.defineProperty(XMLHttpRequest.prototype,'response',{

        set:function(str){

        },

        get:function(){

            if(this['zipCacheUrl']){

                let data = ZipManager.instance._fileCaches.get(this['zipCacheUrl'])

                return data

            }

            return accessor.get.call(this)

        },

        configurable:true

    })

}



/**

 * 加载ZIP文件

 * @param url ZIP文件URL

 * @param progressCallback 进度回调

 * @param options 选项

 */

public async loadBundleZip(

    url: string,

    progressCallback?: (progress: number) => void,

    options?: { cacheKey?: string },

    compileCallback?:()=>void)

{

    const cacheKey = options?.cacheKey || url;

    this._cachesKey = cacheKey

    log("cacheKey : ",cacheKey)

   

    // 检查缓存

    if (this._caches.has(cacheKey)) {

        return this._caches.get(cacheKey)!;

    }

   

    // 发起请求

    this._fetchWithProgress(

        url,

        progressCallback,

        async (arrayBuffer)=>{

            const zip = await JSZip.loadAsync(arrayBuffer);

   

            // 缓存结果

            this._caches.set(cacheKey, zip);

            compileCallback();

        }

    );

}

hasKey(url){

    let _zip = this._caches.get(this._cachesKey)

    if(_zip.file(url)){

        return true

    }

    return false

}

public async getFileFromZip(

    zip: JSZip,

    path: string,

    type: string,

    onComplete?,

    cache: boolean = true

){

    log("getFileFromZip type :",type)

    // 检查文件缓存

    const cacheKey = path;// `${zip.name || ''}_${path}`;

    if (cache && this._fileCaches.has(cacheKey)) {

        onComplete(null,this._fileCaches.get(cacheKey))

        return

    }

   

    const file = zip.file(path);

    if (!file) {

        onComplete("is not file : "+ path,null);

        return

    }

   

    type = type.toLowerCase()

    let result = null

    if(path.includes(".png") == true){

        result = await file.async('blob');

    }

    else if(path.includes(".jpg") == true){

        result = await file.async('blob');

    }

    else if(path.includes(".jpeg") == true){

        result = await file.async('blob');

    }

    else if(path.includes(".webp") == true){

        result = await file.async('blob');

    }

    else{

        switch (type) {

        case '.png':

        case '.jpg':

        case '.jpeg':

        case '.webp':

        case '.mp3':

            // 图片返回ArrayBuffer

            result = await file.async('blob');

            break;

       

        case '.binary':

        case '.bin':

        case '.dbbin':

        case '.skel':

        case '.cconb':

        case '.arraybuffer':

            // JSON文件返回文本

            result = await file.async('arraybuffer');

            break;

        case '.json':

        case '.ExportJson':

        case '.ccon':

            result = await file.async('text');

            result = JSON.parse(result)

            break;

        default:

            // 其他文件返回Uint8Array

            result = await file.async('text');

            break;

    }

    }

           

    // // 获取文件内容

    // let content = file.async(type);;

   

    // // 缓存文件内容

    if (cache) {

        this._fileCaches.set(cacheKey, result);

    }

   

    if(onComplete){

        onComplete(null,result)

    }

    //return result;

}



/**

 * 释放指定ZIP缓存

 * @param cacheKey 缓存键

 */

public releaseZip(cacheKey: string): void {

    if (this._caches.has(cacheKey)) {

        this._caches.delete(cacheKey);

       

        // 清理相关文件缓存

        Array.from(this._fileCaches.keys())

            .filter(key => key.startsWith(`${cacheKey}_`))

            .forEach(key => this._fileCaches.delete(key));

    }

}



/**

 * 释放所有缓存

 */

public clearAllCache(): void {

    this._caches.clear();

    this._fileCaches.clear();

}

   

// 私有方法 -------------------------------

private async _fetchWithProgress(

    url: string,

    progressCallback?: (progress: number) => void,

    finishCallback?:(data)=>void

){

    console.log("_fetchWithProgress 1 start ")

    fetch(url, { mode: 'cors' }) // 注意:通常不需要在同源请求中设置 mode: 'cors'

        .then(response => {

            console.log("_fetchWithProgress 2 start ")

            const contentLength = +response.headers.get('Content-Length'); // 获取内容长度

            let receivedLength = 0; // 接收到的长度

            const reader = response.body.getReader();

            const content = new Uint8Array(contentLength); // 根据内容长度创建缓冲区

            let position = 0; // 当前填充位置

       

            function read() {

                return reader.read().then(({done, value}) => {

                    if (done) {

                        console.log('读取完成');

                        finishCallback(content)

                        return;

                    }

                    content.set(value, position); // 将数据填充到缓冲区中

                    position += value.length; // 更新位置

                    receivedLength += value.length; // 更新接收到的长度

                    console.log(`已接收:${receivedLength} / ${contentLength}`); // 打印进度

                   

                    progressCallback(receivedLength/contentLength)

                    return read(); // 递归读取下一个数据块

                });

            }

       

            read(); // 开始读取

        })

        .catch(error => console.error('请求失败', error));

}

}

let _url = “assets/game.zip”
ZipManager.instance.loadBundleZip(_url,(pro)=>{
//下载进度展示
},{ cacheKey: name },()=>{
ZipManager.instance.initZip()
//资源加载
})

不展示zip加载进度,可用使用 cc 的方式下载zip
这里只用了单zip,需要多zip的可用做扩展

同样,现在web上遇到了http请求好几千,希望新用户进入减少http请求加速下载; 这是目前可行的最优方案了吧。。单单是压缩json我就觉得提升很大
web环境bundle打包,官方浅做一下就能实现啊。。

web上我亲测没问题,微信小游戏“Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, ‘response’)” response是空的,不知道怎么搞了。

微信小游戏就不用这个方案啦,这个针对web的,微信小游戏就用官方的分包和zip就好啦

嗯嗯,我已经用官网分包方案了

我发现把bundle稍微细分一下,然后打包选择"合并所有json",请求数就会变成一个bundle一个,进游戏前预加载bundle也能达到减少请求的目的。配合楼主这个方案,在web端把比较零散的资源合并起来,开启一个界面请求数可以达到个位数。合并后的json会有几M,CDN开gzip压缩会小很多,进游戏速度也能接受