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能正确解析资源的相对路径引用
使用注意事项
- 将Ionic编译后的所有文件(包括子目录)放入Unity的
StreamingAssets文件夹,确保目录结构完整 - 在Inspector中设置
Url为index.html(如果index.html在子目录,比如webapp/index.html,则设置为对应路径) - 构建移动端包时,确保StreamingAssets目录被正确打包(Unity默认会打包该目录)
内容的提问来源于stack exchange,提问作者Gabriele
相关产品推荐
相关产品推荐

