<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Cocos游戲開發(fā)|使用zip壓縮減少web請(qǐng)求,加速資源加載

          共 27967字,需瀏覽 56分鐘

           ·

          2024-04-11 06:48

          點(diǎn)擊上方碼不了一點(diǎn)+關(guān)注和★ 星標(biāo)


          1 引言

          Cocos Creator 3.8 有提供 zip 格式的 bundle,但不支持 web 平臺(tái)。今天就給大家分享一下如何使用 Zip 加速 Cocos Creator 在 Web 平臺(tái)的資源加載。

          前段時(shí)間使用 Cocos Creator 3.8 做一個(gè)云展廳項(xiàng)目,要求在 Web 平臺(tái)上線(微信 H5&瀏覽器)。

          這個(gè)云展廳項(xiàng)目使用 gltf 模型,gltf 模型中有拆分很多 Mesh 和材質(zhì)。

          而在 Cocos 中 gltf 會(huì)被拆分解析為 Cocos 資產(chǎn),發(fā)布 Web 后加載一個(gè)這種大 gltf 就可能有幾百個(gè) request,明明網(wǎng)速飛起,但是加載還是很慢,因?yàn)榇藭r(shí)項(xiàng)目中加載速度的瓶頸已經(jīng)不是網(wǎng)速,而是 request 的數(shù)量太多了。

          2 產(chǎn)生的原因&如何解決

          為什么有這么多 request

          • 新建一個(gè)項(xiàng)目,將下圖這個(gè)gltf(編輯注:想要這個(gè)資源的朋友請(qǐng)查看 碼不了一點(diǎn) 公眾號(hào))直接放在resources文件夾下,方便在demo進(jìn)行預(yù)加載,這個(gè)gltf中共28材質(zhì),32個(gè)Mesh和一些骨骼&貼圖

          8e58f41523cf192f0495a1ac11e4d795.webp

          • 創(chuàng)建Start場(chǎng)景用于預(yù)加載資源,和Game場(chǎng)景用于展示模型

          b791c9629f7c0876827a87e2c6c9422f.webp

          • 創(chuàng)建Start腳本,對(duì)資源做一個(gè)簡(jiǎn)單的預(yù)加載,加載完成后進(jìn)入Game場(chǎng)景
                
                import {_decorator, Component, director, Label, settings, ProgressBar, resources, assetManager, Settings} from 'cc';

          const {ccclass, property} = _decorator;

          @ccclass('Start')
          export class Start extends Component {

              @property(ProgressBar)
              progressBar: ProgressBar;

              @property(Label)
              barLab: Label = null;

              async start() {
              
                  // 直接加載resources根目錄
                  await this.preload([
                      {
                          path: "/",
                          type"dir",
                      },
                  ]);

                  director.loadScene("Game");

              }

              /**
               * 預(yù)加載資源
               */

              preload = (pkg) => {
                  return new Promise<void>((resolve, reject) => {
                      const pathArr = [];

                      pkg.forEach((asset) => {
                          if (typeof asset == "string") {
                              return pathArr.push(asset);
                          }
                          switch (asset.type) {
                              case "dir":
                                  resources.getDirWithPath(asset.path).forEach((v) => pathArr.push(v.path));
                                  break;
                                  
                              default:
                                  pathArr.push(asset.path);
                          }
                      }
          );

                      resources.load(
                          pathArr,
                          (finish: number, total: number, _) => {
                              const pro = finish / total;
                              if (pro < this.progressBar.progress) {
                                  return;
                              }
                              this.progressBar.progress = pro;
                              this.barLab.string = `正在加載中   ${(pro * 100).toFixed(0)}%`;
                          },
                          async (error) => {
                              if (errorconsole.error(error);
                              resolve();
                          }
                      
          );
                  }
          );
              }
          }
          • 運(yùn)行游戲,在本地web環(huán)境查看Network,request數(shù)量為379,和這個(gè)gltf相關(guān)的就有216個(gè),打包發(fā)布至Web環(huán)境,選擇合并所有Json,加載該gltf總共用了35次request。
          0948242f272b9920c65754de230979a0.webp本地 Web 預(yù)覽 b39fd78cc27a2e246a8244f113f5a29f.webp打包合并后
          • 明明只有一個(gè)gltf卻用了35次request來加載。
          • 原因在于Cocos將gltf資源轉(zhuǎn)換成了Cocos資產(chǎn),將Mesh,材質(zhì)等拆解了出來,每個(gè)資源除了資源本身外還會(huì)有一個(gè)記錄屬性依賴的Json文件

          0c4f2ba83dfe7477b962e416e0a33a82.webp

          如何解決

          • 將整個(gè)bundle打包,比如打包成zip文件,進(jìn)入游戲先加載需要的bundle的zip文件一次下載并且解壓,之后需要資源直接從解壓完的文件里取。

          3 Zip 和 JsZip 的使用

          Zip

          不必多言,想必大家都知道

          JsZip 的使用

          不必重復(fù)了,直接上npm平臺(tái)參考文檔吧

          jszip[1]https://www.npmjs.com/package/jszip

          文檔看起來肯定很抽象,不如直接跟著下面的步驟實(shí)操。

          4 探索 Cocos 加載資源的奧秘

          • 查看Network,可以發(fā)現(xiàn)Cocos下載資源都會(huì)通過一個(gè)download-file.ts文件,移動(dòng)鼠標(biāo)到download-file.ts上就可以看到他的調(diào)用棧,其中主要是download-file.tsdownloader.ts也就是資源下載管線的一部分。那么我們直接打開源碼進(jìn)入到這里

          3b20e9c18c501788837ff994a7b0ebbb.webp


          9f5d7d5acd9e9c38ddefd87c4fc2cd16.webp

          • 在代碼中我們可以看到,大部分文件的下載都是通過downloadFile方法進(jìn)行下載的,這個(gè)方法就是剛才的download-file.ts中的方法,該方法使用XMLHttpRequest下載文件

          86674007d87964aea255e295f4440a10.webp


          a9936a3e81628a447bcf0f6a6e3c6d1a.webp


          • 既然我們已經(jīng)知道在Cocos中,大部分資源的下載都依賴于XMLHttpRequest,那么我們可以想辦法攔截它,重定向到我們解包的zip包就可以避免發(fā)起它真實(shí)的網(wǎng)絡(luò)請(qǐng)求從而消耗大量時(shí)間了。

          5 如何加載自己的 Zip 包

          裝載自己的 zip

          • 淺寫一個(gè) ZipLoader 并作為單例使用
          • 偷個(gè)懶,這里直接使用 Cocos 內(nèi)置 API 加載遠(yuǎn)程文件吧,注意這個(gè) API 已經(jīng)棄用,未來可能刪除
          • 我們直接不管容錯(cuò),把 demo 跑通再說
          • 并使用 Promise 配合外部 async/await 來簡(jiǎn)化控制流。
                
                import {assetManager} from "cc";
          import JSZIP from "jszip";

          export default class ZipLoader {

              static _ins: ZipLoader;
              static get ins() {
                  if (!this._ins) {
                      this._ins = new ZipLoader();
                  }
                  return this._ins;
              }

              /**
               * 下載單個(gè)zip文件為buffer
               * 為什么這里帶上后綴名后面會(huì)講到,是為了方面自動(dòng)化
               * @param path 文件路徑
               * @returns zip的buffer
               */

              downloadZip(path: string) {
                  return new Promise((resolve) => {
                      assetManager.downloader.downloadFile(
                          path + '.zip',
                          {xhrResponseType: "arraybuffer"},
                          null,
                          (err, data) => {
                              resolve(data);
                          }
                      );
                  });
              }

              /**
               * 解析加載Zip文件
               * @param path 文件路徑
               */

              async loadZip(path: string) {
                  // 這里沒用npm包的形式而是采用umd形式的js包
                  const jsZip = window["JSZip"]();

                  // 下載
                  const zipBuffer = await this.downloadZip(path);

                  // 解壓
                  const zipFile = await jsZip.loadAsync(zipBuffer);
              }
          }
          • 在之前的Start.ts中添加代碼
          • 注意以下幾點(diǎn)
          • 作者這里有自動(dòng)化壓縮上傳插件,會(huì)自動(dòng)修改server字段,server就是項(xiàng)目發(fā)布的根目錄帶協(xié)議和域名,例如https://xxx.com/cc_project/version/
          • 作者這里會(huì)將需要zip加載的包注入到window上
          • 注入的js類似window["zipBundle"] = ["internal", "main", "resources"];
          • 作者這里所有bundle全都在遠(yuǎn)程所以只加載remote中的文件就行了且zip文件和bundle文件夾在同一目錄下
                
                /* ... */

          @ccclass('Start')
          export class Start extends Component {

              /* ... */
              
              async start() {

                  // 作者這里有自動(dòng)化壓縮上傳插件,會(huì)自動(dòng)修改server字段
                  // 并且會(huì)將需要zip加載的包注入到window上
                  // 注入的js類似與下面這行
                  // window["zipBundle"] = ["internal", "main", "resources"];
                  const remoteUrl = settings.querySettings(Settings.Category.ASSETS, "server");
                  const zipBundle = window["zipBundle"] || [];

                  // 作者這里所有bundle全都是遠(yuǎn)程bundle所以只加載remote中的文件就行了
                  // 且zip文件和bundle文件夾在同一目錄下
                  const loadZipPs = zipBundle.map((name: string) => {
                      return ZipLoader.ins.loadZip(`${remoteUrl}remote/${name}`);
                  });
                  
                  // 先等zip加載完
                  await Promise.all(loadZipPs);

                  // 直接加載resources根目錄
                  await this.preload([
                      {
                          path: "/",
                          type"dir",
                      },
                  ]);

                  director.loadScene("Game");

              }
              
              /* ... */
              
          }

          不自定義引擎,攔截 Cocos 加載

          查閱了 Cocos 的文檔沒有很好的批量實(shí)現(xiàn)這個(gè)需求的方式,又因?yàn)?Cocos 引擎更新比較頻繁,我個(gè)人又喜歡多用新引擎新功能,所以我選擇不自定義引擎,直接采用攔截 Cocos 加載的方法實(shí)現(xiàn)將加載資源替換到自己的zip包。

          通過閱讀源碼我們已經(jīng)知道除了圖片資源,其他資源都是通過 XMLHttpRequest 來加載的,那么很簡(jiǎn)單,我們直接攔截 XMLHttpRequest 就行了。

          那么你問我怎么才能攔截一個(gè)瀏覽器 Native 對(duì)象,這可是 Js,Js 無所不能!

          攔截open和send

          • 不必多說,按下面這種方法就可攔截一個(gè)XMLHttpRequest來做一些操作
                
                
          // 攔截open
          const oldOpen = XMLHttpRequest.prototype.open;
          // @ts-ignore
          XMLHttpRequest.prototype.open = function (method, url, async, user, password{
            return oldOpen.apply(thisarguments);
          }

          // 攔截send
          const oldSend = XMLHttpRequest.prototype.send;
          XMLHttpRequest.prototype.send = async function (data{
            return oldSend.apply(thisarguments);
          }
          • 添加解析zip的代碼,將zip中的代碼解析到完整的路徑上
                
                /* ... */;

          const ZipCache = new Map<stringany>();

          export default class ZipLoader {

              /* ... */

              constructor() {
                  this.init();
              }

              /* ... */

              /**
               * 解析加載Zip文件
               * @param path 文件路徑
               */

              async loadZip(path: string) {

                  const jsZip = JSZIP();

                  const zipBuffer = await this.downloadZip(path);

                  const zipFile = await jsZip.loadAsync(zipBuffer);
                  // 解析zip文件,將路徑,bundle名,文件名拼起來,直接存在一個(gè)map里吧
                  zipFile.forEach((v, t) => {
                      if (t.dir) return;
                      ZipCache.set(path + "/" + v, t);
                  });
              }
              
              init() {
                  // 攔截open
                  const oldOpen = XMLHttpRequest.prototype.open;
                  // @ts-ignore
                  XMLHttpRequest.prototype.open = function (method, url, async, user, password{
                      return oldOpen.apply(thisarguments);
                  }

                  // 攔截send
                  const oldSend = XMLHttpRequest.prototype.send;
                  XMLHttpRequest.prototype.send = async function (data{
                      return oldSend.apply(thisarguments);
                  }
              }
          • 在攔截的open和send中取消網(wǎng)絡(luò)請(qǐng)求,直接定向到我們緩存在zip資源,由于我們不能直接修改xhr的response,因?yàn)樗侵蛔x屬性,所以我們要借助Object.getOwnPropertyDescriptorObject.defineProperty,話不多說,直接看代碼把
          • 在測(cè)試過程中發(fā)現(xiàn)Cocos可能會(huì)請(qǐng)求多次同一個(gè)json,且可能修改解析后的對(duì)象,所以我暫時(shí)給json類型的資源加了一個(gè)id,可以讓他每次都重新獲取zip中的內(nèi)容并解析
                
                /* ... */

          const ZipCache = new Map<stringany>();
          const ResCache = new Map<stringany>();
          let jsonId = 0;  // 兼容json

          export default class ZipLoader {

              /* ... */

              constructor() {
                  this.init();
              }

              /* ... */

              init() {

                  const accessor = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'response');
                  Object.defineProperty(XMLHttpRequest.prototype, 'response', {
                      getfunction () {
                          if (this.zipCacheUrl) {
                              return ResCache.get(this.zipCacheUrl);
                          }
                          return accessor.get.call(this);
                      },
                      setfunction (str{
                          // console.log('set responseText: %s', str);
                          // return accessor.set.call(this, str);
                      },
                      configurable: true
                  });

                  // 攔截open
                  const oldOpen = XMLHttpRequest.prototype.open;
                  // @ts-ignore
                  XMLHttpRequest.prototype.open = function (method, url, async, user, password{
                      // 有這個(gè)資源就記錄下來
                      if (ZipCache.has(url as string)) {
                          this.zipCacheUrl = url;
                      }
                      return oldOpen.apply(thisarguments);
                  }

                  // 攔截send
                  const oldSend = XMLHttpRequest.prototype.send;
                  XMLHttpRequest.prototype.send = async function (data{
                      if (this.zipCacheUrl) {
                          // 有緩存就不解析了
                          if (!ResCache.has(this.zipCacheUrl)) {

                              const cache = ZipCache.get(this.zipCacheUrl);

                              if (this.responseType === "json") {
                                  // 兼容json
                                  this.zipCacheUrl += jsonId++;
                                  const text = await cache.async("text");
                                  ResCache.set(this.zipCacheUrl, JSON.parse(text));
                              } else {
                                  // 直接拿cocos設(shè)置的responseType給zip解析
                                  const res = await cache.async(this.responseType);
                                  ResCache.set(this.zipCacheUrl, res);
                              }
                          }
                          
                          // 解析完了直接調(diào)用onload,并且不再發(fā)起真實(shí)的網(wǎng)絡(luò)請(qǐng)求
                          this.onload();
                          return;
                      }

                      return oldSend.apply(thisarguments);
                  }
              }
          }
          • 打包項(xiàng)目手動(dòng)壓縮bundle文件夾上傳進(jìn)行測(cè)試,可以看到我們下載了三個(gè)zip資源,大量的json和bin文件夾都沒有下載,和我們測(cè)試gltf相關(guān)的文件,僅有兩張貼圖而已,而且能正常進(jìn)入Game場(chǎng)景,說明我們剛才寫的代碼是有效的,加載該gltf文件的request次數(shù)從35次降到了3次

          c16a7e48fcb2842f8aef6fa493e72b1c.webp


          0097b5f0731dafb9a3911386aac31176.webp

          6 發(fā)布自動(dòng)化

          • 編寫 Cocos 插件打包自動(dòng)壓縮 bundle 為 zip
          • 這個(gè)就比較簡(jiǎn)單了,新建一個(gè)構(gòu)建插件
          • 編寫一個(gè)zip.ts,文件內(nèi)容如下
                
                import * as fs from "fs";
          import JSZIP from "jszip";

          //讀取目錄及文件
          function readDir(zip, nowPath{
              const files = fs.readdirSync(nowPath);
              files.forEach(function (fileName, index{//遍歷檢測(cè)目錄中的文件
                  console.log(fileName, index);//打印當(dāng)前讀取的文件名
                  const fillPath = nowPath + "/" + fileName;
                  const file = fs.statSync(fillPath);//獲取一個(gè)文件的屬性
                  if (file.isDirectory()) {//如果是目錄的話,繼續(xù)查詢
                      const dirlist = zip.folder(fileName);//壓縮對(duì)象中生成該目錄
                      readDir(dirlist, fillPath);//重新檢索目錄文件
                  } else {
                      // 排除圖片文件,下面會(huì)講到
                      if (fileName.endsWith(".png") || fileName.endsWith(".jpg")) {
                          return;
                      }
                      zip.file(fileName, fs.readFileSync(fillPath));//壓縮目錄添加文件
                  }
              });
          }

          //開始?jí)嚎s文件
          export function zipDir(name, dir, dist{
              return new Promise<void>((resolve, reject) => {
                  const zip = new JSZIP();
                  readDir(zip, dir);
                  zip.generateAsync({//設(shè)置壓縮格式,開始打包
                      type: "nodebuffer",//nodejs用
                      compression: "DEFLATE",//壓縮算法
                      compressionOptions: {//壓縮級(jí)別
                          level: 9
                      }
                  }
          ).then(function (content) {
                      fs.writeFileSync(`${dist}/${name}.zip`, content, "utf-8");
                      resolve();
                  }
          );
              }
          );
          }
          • 在hooks中onAfterBuild中編寫壓縮腳本的內(nèi)容,壓縮腳本的內(nèi)容在其他操作(如壓縮圖片,混淆代碼,修改js等)都做完之后,且在上傳資源前,且只針對(duì)web模板大致內(nèi)容如下
                
                export const onAfterBuild: BuildHook.onAfterBuild = async function (options: ITaskOptions, result: IBuildResult{
              
              // 非需要的模板不進(jìn)行這個(gè)操作
              if (options.platform !== "web-mobile"return;

              // 修改腳本,混淆代碼,壓縮資源等
              / ... /
              if (fs.existsSync(result.dest + "/remote")) {
                  await Promise.all(
                      fs.readdirSync(result.dest + "/remote")
                          .map((dirName) => {
                              return zipDir(dirName, result.dest + "/remote/" + dirName, result.dest + "/remote");
                          })
                  )
              }
              / ... /
              
              // 上傳

          };

          7 做一個(gè)簡(jiǎn)單的優(yōu)化

          通過前面閱讀源碼和 Network 中看到,Cocos 加載圖片的方式不是通過 XMLHttpRequest,而是通過創(chuàng)建 Image 對(duì)象的方式。

          此片文章的內(nèi)容暫時(shí)不研究如何將加載圖片也替換到使用自己的 Zip,因?yàn)槲易约阂策€沒做。

          所以我選擇直接在打包 zip 的時(shí)候過濾 png/jpg 文件來降低 zip 包的大小,僅在 zip 中打包需要的文件即可。

          8 關(guān)注我

          歡迎大家關(guān)注我的公眾號(hào),只搞實(shí)用的。

          參考資料 [1]

          jszip: https://www.npmjs.com/package/jszip


          瀏覽 33
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本爱爱视频 | 九九九在线视 | 青青草视频网站在线免费观看 | 99热2| 五月婷婷激情网 |