發布WebGL的過程


  今天測試了一下發布 WebGL 的過程, 通過 Unity3D 創建, 相當麻煩, 它不僅對API有限制, 對測試Debug有限制, 也對服務器有要求, 並且現在的瀏覽器都很注重安全策略, 這些都增加了復雜度...

  流程大概如下:

  1. 做個簡單場景, 放到 BuildSettings 里面去

  2. 如果有代碼, 檢查是不是有不能使用的API或是引用不能用的命名空間, 比如 System.Threading 這些, 即使引用了打包也不報錯, 然后發布之后運行拋異常, 它就不動了.

  3. Build 出來的工程不能直接拖到瀏覽器運行, 360 / Firefox / Google Chrome 試過了都不讓運行, 安全策略的問題

  4. 打開IIS服務, 創建本地服務器, 把生成的WebGL的工程拖進去, 綁定端口

  5. 添加Web.config文件, 添加各種文件流支持, 要不然瀏覽器會報Unexpected Token錯誤

  6. 使用各種瀏覽器直接 localhost:端口 打開都沒有問題

 

  最簡單的工程坑還是挺多, 按順序看下來:

  2.1 WebGL多線程不能用, 所以Threading有關都不能用. 

  2.2 部署在服務器上, 所以文件讀寫都不能用, StreamingAssets的地址在本地變成了 [ http:/localhost:61281/StreamingAssets ] , 所以只能老實用WebRequest來進行下載了

  2.3 Resources文件夾下的東西還是能正常讀取, 它的資源應該是會在加載時就全部下載了, 所以很大的話基本沒有用戶體驗了, 不過小工程還是能用

  

  5.1 沒有Web.config的話似乎任何傳輸都不正確, 就是資源 跨域/傳輸 之類的問題了

 

  其實還有很多問題, 中文輸入法跟隨啊, Shader啊.......

 

  先從搭建IIS開始:

  必須用服務器, 先打開本地的IIS服務, win10比win7快了至少10倍:

  啟動完成后繼續打開管理工具, 可以設置IIS了:

  在設置中設置本地硬盤映射, 直接設置到WebGL的輸出目錄:

  添加一個綁定端口, 免得多個地址沖突:

  設置好了之后, 需要在根目錄添加 Web.config 文件支持資源類型文件傳輸:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <staticContent>
      <remove fileExtension=".mem" />
      <remove fileExtension=".data" />
      <remove fileExtension=".unity3d" />
      <remove fileExtension=".jsbr" />
      <remove fileExtension=".membr" />
      <remove fileExtension=".databr" />
      <remove fileExtension=".unity3dbr" />
      <remove fileExtension=".jsgz" />
      <remove fileExtension=".memgz" />
      <remove fileExtension=".datagz" />
      <remove fileExtension=".unity3dgz" />
      <remove fileExtension=".json" />
      <remove fileExtension=".unityweb" />
      <remove fileExtension=".obj" />
      <remove fileExtension=".mjs" />
      <remove fileExtension="." />
      <remove fileExtension=".assetbundle" />

      <mimeMap fileExtension=".mem" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".data" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".unity3d" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".jsbr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".membr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".databr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".unity3dbr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".jsgz" mimeType="application/x-javascript; charset=UTF-8" />
      <mimeMap fileExtension=".memgz" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".datagz" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".unity3dgz" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".json" mimeType="application/json; charset=UTF-8" />
      <mimeMap fileExtension=".unityweb" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".obj" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".mjs" mimeType="text/javascript; charset=UTF-8" />
      <mimeMap fileExtension="." mimeType="application/octet-stream" />
      <mimeMap fileExtension=".assetbundle" mimeType="application/octet-stream" />
    </staticContent>
  </system.webServer>
</configuration>

  我猜測在創建 Stream 的時候服務器會指定類型, 沒有指定的就不知道怎么傳了, 一般默認肯定是二進制傳吧, 怎么這么無聊...

  這里有個特殊的就是無后綴的文件, 直接 "." 代表即可. "application/octet-stream" 就是二進制了吧...

  

  這樣就能在本地瀏覽器打開了, 放在 Resources 中的 Txt 資源文件可以正常讀取:

  text2.text = Resources.Load<TextAsset>("Test").text;    // Test Resources

  放在 StreamingAssets 下的 Txt 文件不能通過IO讀取, 使用 UnityWebRequest 進行獲取, 讀取地址經過 System.Uri 加工:

    private void Start()
    {
        StartCoroutine(GetData(Application.streamingAssetsPath + "/Test.txt", (_handle) =>
        {
            text1.text = _handle.text;    // Test streamingAssetsPath
        }));
    }

    IEnumerator GetData(string loadPath, System.Action<DownloadHandler> succ)
    {
        var uri = new System.Uri(loadPath);
        UnityWebRequest www = UnityWebRequest.Get(uri.ToString());
        yield return www.SendWebRequest();

        if(www.isNetworkError || www.isHttpError)
        {
            Debug.Log(www.error);
        }
        else
        {
            Debug.Log(www.downloadHandler.text);
            succ.Invoke(www.downloadHandler);
        }
    }

  

  不過如果在Build的時候選擇 [ Build & Run ] 的話, 它會臨時起一個服務器來跑打包出來的工程, 不需要IIS也是可以的......

 

(2020.07.22)

  最近想要測試一下資源遠程加載的方案, 於是把加載邏輯添加了通過 UnityWebRequest 方式獲取 AssetBundle 的邏輯, 然而這又是一個坑, 在編輯器下, 可以從網站上獲取到 AssetBundle, 可是如果發布到服務器上, 並且資源在其它服務器, 就產生了一個跨域問題, 然后資源是無法傳輸的, 雖然理解這是 http 服務器設計的問題, 可是我網上查了半天也沒解決, 真是神奇了...

  首先把打出的包放到服務器上 : 

  然后使用服務器的路徑來下載包 : 

  看到 Sprite 的 AssetBundle 可以正確通過網址下載來, 並且正確加載出來了, 可是發布到服務器后 (資源服務器 localhost:12354, 運行服務器 localhost:44599), 因為跨域問題, 無法獲取了 : 

XMLHttpRequest cannot load http://localhost:12354/unityassets/AssetBundleManifest. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:44599' is therefore not allowed access.

  然后找跨域的相關描述, 是需要服務器添加一個 Access-Control-Allow-Origin 相關的返回? 

  在360瀏覽器里面可以查看到各個 http 請求的信息, F12 -> NetWork -> XHR -> xxxxx

  找到很多論壇都說添加一個 customheader 就行了, 下面這樣 (https://enable-cors.org/server_iis7.html) :

<?xml version="1.0" encoding="utf-8"?>
<configuration>
 <system.webServer>
   <httpProtocol>
     <customHeaders>
       <add name="Access-Control-Allow-Origin" value="*" />
     </customHeaders>
   </httpProtocol>
 </system.webServer>
</configuration>

  那么加到原有的 Web.config 文件里 : 

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
      </customHeaders>
    </httpProtocol>
    <staticContent>
      <remove fileExtension=".mem" />
      <remove fileExtension=".data" />
      <remove fileExtension=".unity3d" />
      <remove fileExtension=".jsbr" />
      <remove fileExtension=".membr" />
      <remove fileExtension=".databr" />
      <remove fileExtension=".unity3dbr" />
      <remove fileExtension=".jsgz" />
      <remove fileExtension=".memgz" />
      <remove fileExtension=".datagz" />
      <remove fileExtension=".unity3dgz" />
      <remove fileExtension=".json" />
      <remove fileExtension=".unityweb" />
      <remove fileExtension=".obj" />
      <remove fileExtension=".mjs" />
      <remove fileExtension="." />
      <remove fileExtension=".assetbundle" />

      <mimeMap fileExtension=".mem" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".data" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".unity3d" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".jsbr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".membr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".databr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".unity3dbr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".jsgz" mimeType="application/x-javascript; charset=UTF-8" />
      <mimeMap fileExtension=".memgz" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".datagz" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".unity3dgz" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".json" mimeType="application/json; charset=UTF-8" />
      <mimeMap fileExtension=".unityweb" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".obj" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".mjs" mimeType="text/javascript; charset=UTF-8" />
      <mimeMap fileExtension="." mimeType="application/octet-stream" />
      <mimeMap fileExtension=".assetbundle" mimeType="application/octet-stream" />
    </staticContent>    
  </system.webServer>
</configuration>

  還是報錯, 我就納悶了, 然后有些人在 Unity 請求代碼里面添加了一些頭, 我也添加之后測試仍然報錯, 無用 : 

    var unityWebRequest = UnityWebRequest.GetAssetBundle(url, 0);

    unityWebRequest.SetRequestHeader("Access-Control-Allow-Credentials", "true");
    unityWebRequest.SetRequestHeader("Access-Control-Allow-Headers", "Accept, X-Access-Token, X-Application-Name, X-Request-Sent-Time");
    unityWebRequest.SetRequestHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
    unityWebRequest.SetRequestHeader("Access-Control-Allow-Origin", "*");

    var request = unityWebRequest.SendWebRequest();
    // ...

  換了個錯誤再來一遍, 變成了認證錯誤之類的....

  繼續再找, 看到一個說 IIS 跨域還要安裝一個 CORS Module 的東西的 : 

I had a similar issue recently. Most tutorial/documentation only suggests adding custom headers in the configuration. But this does not tell IIS to handle the CORS Pre-flight request by itself.

To do so, you must install the CORS Module in IIS and add some configuration in the web.config file, as explained here: IIS CORS module Configuration Reference

  好吧, 進入微軟找 CORS Module (https://www.iis.net/downloads/microsoft/iis-cors-module), 下載安裝之后, 再打包一次, Unity 代碼也使用最簡單的看看 : 

            public void SendWebRequest()
            {
                if(request == null)
                {
                    var unityWebRequest = this.hash.HasValue ? UnityWebRequest.GetAssetBundle(url, hash.Value, 0) : UnityWebRequest.GetAssetBundle(url, 0);
                    request = unityWebRequest.SendWebRequest();
                    request.completed += OnLoaded;
                }
            }

  結果居然可以讀取了, 反正不知道是不是安裝了 CORS Module, 能用就行了 : 

  沒想到部署個 WebGL 測試也這么多幺蛾子, 這些服務器就不能給個省心的邏輯嗎, 要啥功能給個界面式的功能列表也好啊, 如果明天用阿帕奇服務器, 又是查資料查半天, 心累...

 

   然后看一下各個默認文件夾在運行時的位置 : 

    Debug.Log("Application.dataPath : " + Application.dataPath);    // http://localhost:44599
    Debug.Log("Application.persistentDataPath : " + Application.persistentDataPath);    // /idbfs/9ed0d20bc957a25b21e872bbf1c2f671
    Debug.Log("Application.streamingAssetsPath : " + Application.streamingAssetsPath);    // http://localhost:44599/StreamingAssets
    Debug.Log("Application.temporaryCachePath : " + Application.temporaryCachePath);    // /tmp
    Debug.Log("Caching.currentCacheForWriting.path : " + Caching.currentCacheForWriting.path);    // /idbfs/9ed0d20bc957a25b21e872bbf1c2f671/UnityCache/Shared
    Debug.Log("System.Environment.CurrentDirectory : " + System.Environment.CurrentDirectory);    // /

  Application.dataPath 和 Application.streamingAssetsPath 是服務器的相對路徑, 所以服務器資源可以正常讀取, 而其它的應該都是本地路徑, 至於這個路徑在哪, 可能是瀏覽器的緩存路徑, 我用360瀏覽器, 直接到下面去找看看 : 

  我看論壇他們是說用的 IndexedDB 存儲臨時文件的, 不知道用什么開來看...

  發現瀏覽器自帶了查看器的樣子, F12 -> Appliction ->  IndexedDB -> xxxx

   不知道這些是不是本地緩存, 我在初始界面就有一個加載代碼 : 

    void Start()
    {
        // 屏幕右上角 
        AssetBundleMaster.Core.ABM_ResourceLoader.Instance.LoadAsync<Sprite>("Sprites/Pic002PNG", (_pic) =>
        {
            image.sprite = _pic;
            Debug.Log("Loaded Sprite " + _pic + " : " + Time.frameCount);
        });
        // 屏幕左下角
        rawImage.texture = AssetBundleMaster.Core.ABM_ResourceLoader.Instance.Load<Texture2D>("Textures/Pic001");
        Debug.Log("Loaded Texture2D " + rawImage.texture + " : " + Time.frameCount);
    }

  看看刪除 IndexedDB 相關文件夾后第一次運行的情況, 因為屏幕左下角的讀取使用的是同步讀取 (UnityWebRequest 發送請求后馬上返回) : 

  這樣結果左下角是肯定沒有圖片的, 因為 UnityWebRequest 是遠程請求, 必定不能馬上得到結果...

  這時候發起的資源請求有3個 (使用Hash128作為參數的請求) : 

assetBundleCreateRequest = UnityWebRequest.GetAssetBundle(loadPath, assetBundleManifest.GetAssetBundleHash(this.assetName), 0).SendWebRequest();
    [RuntimeInitializeOnLoadMethod]
    private static void StartUpRun()
    {
        AssetBundleMaster.Core.ABM_AssetLoadManager.Instance.OnModuleLoaded(() =>
        {
            Debug.Log("OnModuleLoaded At : " + Time.frameCount);

            AssetBundleMaster.Core.ABM_SceneLoader.Instance.LoadScene("Scenes/S1");
        });

    }

  1. 場景 : s1.assetbundle

  2. 右上角圖片 : common.assetbundle

  3. 左下角圖片 : pic001.assetbundle -- 因為是異步請求, 同步回調沒有獲得圖片, 可是也進行了下載

  這時看到 IndexedDB 中顯示的也是這三個文件 : 

   然后我關閉瀏覽器, 從新再打開網址 : 

  再打開一次的話, 異步加載的左下角圖片, 居然在同步回調里面就能獲取圖片了, 這難道就是本地緩存的威力嗎? ( 2020.07.28 -- 經測試並不一定能在同步回調中返回, 應該跟IO速度有關, 並不是進行了同步加載 ). 我再點擊一下按鈕加載一張新的圖片覆蓋左下角, 看看 IndexedDB中是不是有了新的圖片了 : 

  左下角的圖片改變了, 看看本地緩存 : 

  資源變成4個了, 關閉瀏覽器從新加載網頁看看, 如果不是本地緩存的話, 不操作應該還是只會加載3個資源. 

  重新加載后還是4個資源, 說明 FILE_DATA 這個數據庫這就是本地緩存無疑了. 當我們的 AssetBundleManifest 里面獲取的 Hash128 跟本地不一樣的時候, 就會去下載最新包了. 當然如果是 WebGL 的話 AssetBundleManifest 直接放服務器的 StreamingAssets 文件夾下就行了, 而資源一般會放到CDN服務器上, 所以前面搞了半天跨域的問題. 當然遠程資源+緩存的模式, 也能作為PC, Android, IOS之類的平台加載邏輯也是可行的, 並且有 Cache.expirationDelay 這些自動刪除邏輯在, 用不到的緩存資源能自動刪除, 省了更新刪除邏輯了...

  測試一下看看, 給另一個按鈕添加清除緩存的功能 :

    clearBtn.onClick.AddListener(() =>
    {
        if(Caching.ClearCache())
        {
            Debug.Log("ClearCache Succ");
        }
        else
        {
            Debug.Log("ClearCache FAILED");
        }
    });

  ClearCache FAILED ......

  清除緩存失敗, 這又是什么神操作, 找了下資料, 說要 Unload 掉所有已經加載的 AssetBundle 之后才能行, 修改代碼來硬核一點的試試 : 

    void Start()
    {
        clearBtn.onClick.AddListener(() =>
        {
            StartCoroutine(ClearCache());
        });
    }
        
    IEnumerator ClearCache()
    {
        AssetBundle.UnloadAllAssetBundles(true);
        yield return new WaitForSeconds(1.0f);
        System.GC.Collect(0);
        yield return new WaitForSeconds(1.0f);
        yield return Resources.UnloadUnusedAssets();
        yield return new WaitForSeconds(1.0f);
        if(Caching.ClearCache())
        {
            Debug.Log("ClearCache Succ");
        }
        else
        {
            Debug.Log("ClearCache FAILED");
        }
    }

  居然還是不行!!! 震驚!! 刪除了 AssetBundle 之后, UI 都變黑了 : 

  這就無語了, 雖然不是很大問題......

(2020.11.19)

  補充一下在Windows下的緩存測試, 緩存路徑在 C:\Users\XXXX\AppData\LocalLow\Unity\DefaultCompany_工程名稱     文件夾下, 設定了 expirationDelay 之后, 需要程序下載對象, 然后超過過期時間程序還在運行, 它才能幫你刪除對應的緩存(在程序退出的時候), 也就是一個運行期間的功能, 不是 Cookie 那樣通過系統維護的東西. 也就是說運行時間需要超過設定時間, 才會刪除緩存, 非常搞笑......

 

 (2020.07.28)

  今天才發現 IndexedDB 其實就是鍵值對, 里面存儲的數據能夠跟 AssetBundle 對應得上 : 

  今天新打的包在瀏覽器的緩存中查看, 它的 __data 大小跟服務器上的原始文件 s1.assetbundle 大小是一樣的 : 

  這樣就知道它在瀏覽器中的緩存是以 [/idbfs/用戶ID/UnityCache/Shared/文件名/文件哈希值/__data] 的key來做存儲的, 還不知道它的用戶ID是怎樣來的, 不過默認路徑根目錄可以通過 Caching.currentCacheForWriting.path 來獲得, 得到的是 : 

/idbfs/bc9cc5aa7eef26ca01726eef3f44ae31/UnityCache/Shared

  這個路徑, 如果可以調用 IndexedDB 的API的話, 也是可以自己查詢的...

 

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM