You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity集成Ionic+React Web应用:修复WebView本地资源加载问题

解决Unity WebView加载StreamingAssets中Ionic/Capacitor本地资源问题

原脚本仅处理了jpg、js、html三种文件,且只复制单个文件,无法适配Ionic项目的多资源类型(CSS、字体、图片等)和目录结构,导致WebView加载本地index.html后,相关资源无法正确加载。以下是修改后的完整脚本:

using System.Collections;
using UnityEngine;
#if UNITY_2018_4_OR_NEWER
using UnityEngine.Networking;
#endif
using UnityEngine.UI;
using System.IO;

public class SampleWebView : MonoBehaviour
{
    public string Url = "index.html"; // 指向StreamingAssets中的Web应用入口文件
    public Text status;
    WebViewObject webViewObject;

    IEnumerator Start()
    {
        webViewObject = (new GameObject("WebViewObject")).AddComponent<WebViewObject>();
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
        webViewObject.canvas = GameObject.Find("Canvas");
#endif
        webViewObject.Init(
            cb: (msg) =>
            {
                Debug.Log(string.Format("CallFromJS[{0}]", msg));
                status.text = msg;
                status.GetComponent<Animation>()?.Play();
            },
            err: (msg) =>
            {
                Debug.Log(string.Format("CallOnError[{0}]", msg));
                status.text = msg;
                status.GetComponent<Animation>()?.Play();
            },
            httpErr: (msg) =>
            {
                Debug.Log(string.Format("CallOnHttpError[{0}]", msg));
                status.text = msg;
                status.GetComponent<Animation>()?.Play();
            },
            started: (msg) =>
            {
                Debug.Log(string.Format("CallOnStarted[{0}]", msg));
            },
            hooked: (msg) =>
            {
                Debug.Log(string.Format("CallOnHooked[{0}]", msg));
            },
            cookies: (msg) =>
            {
                Debug.Log(string.Format("CallOnCookies[{0}]", msg));
            },
            ld: (msg) =>
            {
                Debug.Log(string.Format("CallOnLoaded[{0}]", msg));
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS
                var js = @"
                    if (!(window.webkit && window.webkit.messageHandlers)) {
                        window.Unity = {
                            call: function(msg) {
                                window.location = 'unity:' + msg;
                            }
                        };
                    }
                ";
#elif UNITY_WEBPLAYER || UNITY_WEBGL
                var js = @"
                    window.Unity = {
                        call:function(msg) {
                            parent.unityWebView.sendMessage('WebViewObject', msg);
                        }
                    };
                ";
#else
                var js = "";
#endif
                webViewObject.EvaluateJS(js + @"Unity.call('ua=' + navigator.userAgent)");
            }
        );

        while (!webViewObject.IsInitialized())
        {
            yield return null;
        }

#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
        webViewObject.bitmapRefreshCycle = 1;
        webViewObject.devicePixelRatio = 1;
#endif

        webViewObject.SetMargins(5, 100, 5, Screen.height / 4);
        webViewObject.SetTextZoom(100);
        webViewObject.SetVisibility(true);

#if !UNITY_WEBPLAYER && !UNITY_WEBGL
        if (Url.StartsWith("http"))
        {
            webViewObject.LoadURL(Url.Replace(" ", "%20"));
        }
        else
        {
            // 获取Web应用的根目录(假设Url是index.html,根目录为其所在文件夹)
            string webRootDir = Path.GetDirectoryName(Url);
            if (string.IsNullOrEmpty(webRootDir))
            {
                webRootDir = "";
            }

            string srcRoot = Path.Combine(Application.streamingAssetsPath, webRootDir);
            string dstRoot = Path.Combine(Application.temporaryCachePath, webRootDir);

            // 递归复制整个Web应用目录到临时缓存
            yield return CopyDirectoryRecursively(srcRoot, dstRoot);

            // 加载临时目录中的index.html
            string dstIndex = Path.Combine(dstRoot, Path.GetFileName(Url));
            webViewObject.LoadURL("file://" + dstIndex.Replace(" ", "%20"));
        }
#else
        if (Url.StartsWith("http")) {
            webViewObject.LoadURL(Url.Replace(" ", "%20"));
        } else {
            webViewObject.LoadURL("StreamingAssets/" + Url.Replace(" ", "%20"));
        }
#endif
        yield break;
    }

    /// <summary>
    /// 递归复制目录(适配Android的jar路径和iOS的本地路径)
    /// </summary>
    IEnumerator CopyDirectoryRecursively(string srcDir, string dstDir)
    {
        if (!Directory.Exists(dstDir))
        {
            Directory.CreateDirectory(dstDir);
        }

        // 处理Android的jar:file://路径
        bool isAndroidJar = srcDir.Contains("://");

        if (isAndroidJar)
        {
            // 获取目录下的所有文件和子目录(Android需要用UnityWebRequest列出目录)
            string dirUrl = srcDir.EndsWith("/") ? srcDir : srcDir + "/";
            #if UNITY_2018_4_OR_NEWER
            UnityWebRequest req = UnityWebRequest.Get(dirUrl);
            yield return req.SendWebRequest();

            if (req.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError("Failed to list directory: " + req.error);
                yield break;
            }

            // 解析Android StreamingAssets目录的文件列表(简单解析,假设返回的是文件名列表)
            string[] files = req.downloadHandler.text.Split('\n');
            foreach (string file in files)
            {
                string trimmedFile = file.Trim();
                if (string.IsNullOrEmpty(trimmedFile)) continue;

                string srcPath = dirUrl + trimmedFile;
                string dstPath = Path.Combine(dstDir, trimmedFile);

                if (trimmedFile.EndsWith("/"))
                {
                    // 子目录,递归复制
                    yield return CopyDirectoryRecursively(srcPath, dstPath.TrimEnd('/'));
                }
                else
                {
                    // 文件,下载并写入
                    UnityWebRequest fileReq = UnityWebRequest.Get(srcPath);
                    yield return fileReq.SendWebRequest();

                    if (fileReq.result == UnityWebRequest.Result.Success)
                    {
                        File.WriteAllBytes(dstPath, fileReq.downloadHandler.data);
                    }
                    else
                    {
                        Debug.LogError("Failed to copy file: " + srcPath + ", Error: " + fileReq.error);
                    }
                }
            }
            #endif
        }
        else
        {
            // iOS和桌面平台,直接访问文件系统
            foreach (string file in Directory.GetFiles(srcDir))
            {
                string fileName = Path.GetFileName(file);
                string dstPath = Path.Combine(dstDir, fileName);
                File.Copy(file, dstPath, true);
            }

            foreach (string subDir in Directory.GetDirectories(srcDir))
            {
                string subDirName = Path.GetFileName(subDir);
                string dstSubDir = Path.Combine(dstDir, subDirName);
                yield return CopyDirectoryRecursively(subDir, dstSubDir);
            }
        }
    }

    void OnGUI()
    {
        var x = 10;

        GUI.enabled = (webViewObject != null) && webViewObject.CanGoBack();
        if (GUI.Button(new Rect(x, 10, 80, 80), "<"))
        {
            webViewObject?.GoBack();
        }
        GUI.enabled = true;
        x += 90;

        GUI.enabled = (webViewObject != null) && webViewObject.CanGoForward();
        if (GUI.Button(new Rect(x, 10, 80, 80), ">"))
        {
            webViewObject?.GoForward();
        }
        GUI.enabled = true;
        x += 90;

        if (GUI.Button(new Rect(x, 10, 80, 80), "r"))
        {
            webViewObject?.Reload();
        }
        x += 90;

        GUI.TextField(new Rect(x, 10, 180, 80), "" + ((webViewObject == null) ? 0 : webViewObject.Progress()));
        x += 190;

        if (GUI.Button(new Rect(x, 10, 80, 80), "*"))
        {
            var g = GameObject.Find("WebViewObject");
            if (g != null)
            {
                Destroy(g);
            }
            else
            {
                StartCoroutine(Start());
            }
        }
        x += 90;

        if (GUI.Button(new Rect(x, 10, 80, 80), "c"))
        {
            webViewObject?.GetCookies(Url);
        }
        x += 90;

        if (GUI.Button(new Rect(x, 10, 80, 80), "x"))
        {
            webViewObject?.ClearCookies();
        }
        x += 90;

        if (GUI.Button(new Rect(x, 10, 80, 80), "D"))
        {
            webViewObject?.SetInteractionEnabled(false);
        }
        x += 90;

        if (GUI.Button(new Rect(x, 10, 80, 80), "E"))
        {
            webViewObject?.SetInteractionEnabled(true);
        }
        x += 90;
    }
}

关键修改说明

  • 递归复制整个目录:替换原有的单个文件复制逻辑,完整复制StreamingAssets中的Web应用所有文件和子目录,确保CSS、字体、图片等资源都被同步到临时缓存
  • Android路径适配:处理Android的jar:file://特殊路径,用UnityWebRequest读取目录和文件,避免直接文件系统访问失败
  • iOS路径兼容:保留iOS平台的直接文件系统复制,保证效率
  • 相对路径支持:将整个Web应用目录复制到临时路径后,加载其中的index.html,WebView能正确解析资源的相对路径引用

使用注意事项

  1. 将Ionic编译后的所有文件(包括子目录)放入Unity的StreamingAssets文件夹,确保目录结构完整
  2. 在Inspector中设置Url为index.html(如果index.html在子目录,比如webapp/index.html,则设置为对应路径)
  3. 构建移动端包时,确保StreamingAssets目录被正确打包(Unity默认会打包该目录)

内容的提问来源于stack exchange,提问作者Gabriele

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 23:34:51