本文档详细介绍了如何在Unity应用中通过WebView加载React应用,并解决在Android平台上常见的路径和网络问题。
该项目采用以下架构:
Unity应用
├── WebView组件
├── 本地HTTP服务器
└── StreamingAssets
└── web/
├── index.html (React应用入口)
├── assets/ (资源文件)
└── image/ (图片文件)
确保React项目的package.json
中包含正确的构建脚本:
"scripts": {
"build": "react-scripts build"
}
npm run build
这将生成一个build
文件夹,包含所有静态资源。
将React构建输出复制到Unity项目的StreamingAssets目录:
复制 build/* 到 Assets/StreamingAssets/web/
ServerManager.cs
负责启动和管理本地HTTP服务器:
using UnityEngine;
using System.Threading.Tasks;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.Networking;
public class ServerManager : MonoBehaviour
{
private UnityStaticFileServer _server;
private List<string> _scannedFiles = new List<string>();
private async void Start()
{
string rootPath = Application.streamingAssetsPath + "/web";
if (Application.platform == RuntimePlatform.Android)
{
Debug.Log($"Android platform detected. Using StreamingAssets path: {rootPath}");
StartCoroutine(ScanAndroidStreamingAssets());
}
else
{
// 在其他平台上列出目录内容
if (Directory.Exists(rootPath))
{
Debug.Log($"Web directory exists at: {rootPath}");
try
{
string[] files = Directory.GetFiles(rootPath, "*", SearchOption.AllDirectories);
foreach (string file in files)
{
string relativePath = file.Replace(rootPath, "").TrimStart('/', '\\');
_scannedFiles.Add(relativePath);
}
}
catch (System.Exception ex)
{
Debug.LogError($"Error listing directory contents: {ex.Message}");
}
}
}
_server = new UnityStaticFileServer(rootPath);
_server.SetScannedFiles(_scannedFiles);
await _server.StartAsync();
}
// 扫描Android上的StreamingAssets/web文件夹
private IEnumerator ScanAndroidStreamingAssets()
{
string webPath = Application.streamingAssetsPath + "/web";
// 获取index.html并解析其中的资源
string indexPath = webPath + "/index.html";
UnityWebRequest indexRequest = UnityWebRequest.Get(indexPath);
yield return indexRequest.SendWebRequest();
if (indexRequest.result == UnityWebRequest.Result.Success)
{
_scannedFiles.Add("index.html");
string html = indexRequest.downloadHandler.text;
ParseHtmlForResources(html, webPath);
}
// 尝试常见的文件路径
string[] commonPaths = new string[]
{
"/css/style.css",
"/js/main.js",
"/assets/gameConfig.json",
"/image/title_memory_card_text_tu.png",
// 添加其他常见文件
};
foreach (string path in commonPaths)
{
string fullPath = webPath + path;
UnityWebRequest request = UnityWebRequest.Get(fullPath);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
_scannedFiles.Add(path.TrimStart('/'));
}
}
_server.SetScannedFiles(_scannedFiles);
}
// 解析HTML中的资源引用
private void ParseHtmlForResources(string html, string basePath)
{
// 解析CSS、JS、图片等资源
ParseResourceLinks(html, "href=\"", "\"", ".css", basePath);
ParseResourceLinks(html, "src=\"", "\"", ".js", basePath);
ParseResourceLinks(html, "src=\"", "\"", ".png", basePath);
// 添加其他资源类型
}
private void OnDestroy()
{
_server?.Stop();
_server?.Dispose();
}
}
UnityStaticFileServer.cs
实现了HTTP服务器功能:
using System;
using System.IO;
using System.Net;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
public class UnityStaticFileServer : IDisposable
{
private readonly HttpListener _listener;
private readonly string _rootPath;
private bool _isRunning;
private readonly Dictionary<string, string> _mimeTypes;
private CancellationTokenSource _cancellationTokenSource;
private int _port;
private Dictionary<string, byte[]> _androidCache = new Dictionary<string, byte[]>();
private List<string> _scannedFiles = new List<string>();
public UnityStaticFileServer(string rootPath, int port = 8080)
{
_rootPath = rootPath;
_port = port;
_listener = new HttpListener();
// 绑定到所有网络接口
_listener.Prefixes.Add($"http://*:{port}/");
_listener.Prefixes.Add($"http://+:{port}/");
// 初始化MIME类型
_mimeTypes = new Dictionary<string, string>
{
{ ".html", "text/html" },
{ ".css", "text/css" },
{ ".js", "application/javascript" },
// 添加其他MIME类型
};
}
// 设置扫描到的文件列表
public void SetScannedFiles(List<string> scannedFiles)
{
if (scannedFiles != null)
{
_scannedFiles = new List<string>(scannedFiles);
// 预加载Android文件
if (Application.platform == RuntimePlatform.Android)
{
foreach (string file in _scannedFiles)
{
string fullPath = Path.Combine(_rootPath, file).Replace("\\", "/");
Debug.Log($"Will preload file: {fullPath}");
}
}
}
}
// 在Android平台上从StreamingAssets读取文件
private async Task<byte[]> ReadAndroidStreamingAssetAsync(string filePath)
{
string fullPath = filePath;
if (!fullPath.StartsWith("jar:"))
{
if (filePath.StartsWith(Application.streamingAssetsPath))
{
fullPath = filePath;
}
else
{
fullPath = Path.Combine(Application.streamingAssetsPath, filePath.TrimStart('/'));
}
}
// 检查缓存
if (_androidCache.TryGetValue(fullPath, out byte[] cachedData))
{
return cachedData;
}
// 使用UnityWebRequest读取文件
using (UnityWebRequest webRequest = UnityWebRequest.Get(fullPath))
{
var asyncOperation = webRequest.SendWebRequest();
while (!asyncOperation.isDone)
{
await Task.Delay(10);
}
if (webRequest.result == UnityWebRequest.Result.Success)
{
byte[] data = webRequest.downloadHandler.data;
_androidCache[fullPath] = data;
return data;
}
else
{
string relativePath = filePath.Replace(_rootPath, "").TrimStart('/');
if (_scannedFiles.Contains(relativePath))
{
Debug.LogError($"File {relativePath} is in scanned list but cannot be read!");
}
return null;
}
}
}
// 处理HTTP请求
private async Task HandleRequestAsync(HttpListenerContext context)
{
try
{
var response = context.Response;
var request = context.Request;
// 获取本地路径
var localPath = request.Url?.LocalPath.TrimStart('/') ?? "";
// 路径修正 - 处理web/game/路径的映射
if (localPath.StartsWith("game/") || localPath.Contains("/game/"))
{
string originalPath = localPath;
// 处理game/assets/到assets/的映射
if (localPath.Contains("game/assets/"))
{
localPath = localPath.Replace("game/assets/", "assets/");
}
// 处理game/image/到image/的映射
else if (localPath.Contains("game/image/"))
{
localPath = localPath.Replace("game/image/", "image/");
}
// 处理其他可能的game/目录映射
else
{
localPath = localPath.Replace("game/", "");
}
Debug.Log($"Path corrected: {originalPath} -> {localPath}");
}
// 如果路径为空,提供index.html
if (string.IsNullOrEmpty(localPath))
localPath = "index.html";
// 构建完整路径
string fullPath = Path.Combine(_rootPath, localPath).Replace("\\", "/");
// 处理文件请求
byte[] fileData = null;
if (Application.platform == RuntimePlatform.Android)
{
// 在Android上使用特殊方法读取文件
fileData = await ReadAndroidStreamingAssetAsync(fullPath);
// 如果找不到请求的文件,尝试提供index.html(用于SPA路由)
if (fileData == null && !localPath.Equals("index.html", StringComparison.OrdinalIgnoreCase))
{
string indexPath = Path.Combine(_rootPath, "index.html").Replace("\\", "/");
fileData = await ReadAndroidStreamingAssetAsync(indexPath);
}
}
else
{
// 在其他平台上使用标准文件IO
if (File.Exists(fullPath))
{
fileData = File.ReadAllBytes(fullPath);
}
else if (!localPath.Equals("index.html", StringComparison.OrdinalIgnoreCase))
{
// 尝试提供index.html(用于SPA路由)
string indexPath = Path.Combine(_rootPath, "index.html");
if (File.Exists(indexPath))
{
fileData = File.ReadAllBytes(indexPath);
}
}
}
// 如果找不到文件,返回404
if (fileData == null)
{
response.StatusCode = (int)HttpStatusCode.NotFound;
return;
}
// 设置MIME类型
var extension = Path.GetExtension(fullPath).ToLowerInvariant();
response.ContentType = _mimeTypes.TryGetValue(extension, out var mimeType)
? mimeType
: "application/octet-stream";
// 添加CORS头
response.Headers.Add("Access-Control-Allow-Origin", "*");
// 发送文件数据
response.ContentLength64 = fileData.Length;
response.OutputStream.Write(fileData, 0, fileData.Length);
}
finally
{
context.Response.Close();
}
}
}
WebViewLoader.cs
负责初始化WebView并加载URL:
using UnityEngine;
using Vuplex.WebView;
using System.Net.NetworkInformation;
using System.Net;
using System.Net.Sockets;
public class WebViewLoader : MonoBehaviour
{
[SerializeField] private CanvasWebViewPrefab canvasWebView;
[SerializeField] private bool useLocalServer = true;
private void Start()
{
// 初始化WebView
canvasWebView.InitialUrl = "about:blank";
canvasWebView.NativeOnScreenKeyboardEnabled = true;
// 设置事件处理
canvasWebView.WebView.SetDefaultBackgroundEnabled(false);
// 加载网页
if (useLocalServer)
{
// 在Android上使用本地IP地址而不是localhost
string url = Application.platform == RuntimePlatform.Android
? $"http://{GetLocalIPAddress()}:8080/"
: "http://localhost:8080/";
Debug.Log($"Loading local URL: {url}");
canvasWebView.WebView.LoadUrl(url);
}
else
{
// 使用远程URL
canvasWebView.WebView.LoadUrl("https://3zentree.cn/testGlove/");
}
}
// 获取设备的本地IP地址
private string GetLocalIPAddress()
{
string localIP = "127.0.0.1"; // 默认值
try
{
// 获取所有网络接口
NetworkInterface[] networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
foreach (NetworkInterface networkInterface in networkInterfaces)
{
// 只考虑活跃的接口
if (networkInterface.OperationalStatus != OperationalStatus.Up)
continue;
// 跳过虚拟接口和环回接口
if (networkInterface.NetworkInterfaceType == NetworkInterfaceType.Loopback ||
networkInterface.Description.Contains("Virtual") ||
networkInterface.Name.Contains("vEthernet"))
continue;
// 获取IP属性
IPInterfaceProperties ipProperties = networkInterface.GetIPProperties();
// 查找IPv4地址
foreach (UnicastIPAddressInformation ipInfo in ipProperties.UnicastAddresses)
{
if (ipInfo.Address.AddressFamily == AddressFamily.InterNetwork)
{
localIP = ipInfo.Address.ToString();
return localIP;
}
}
}
}
catch (System.Exception ex)
{
Debug.LogError($"Error getting local IP address: {ex.Message}");
}
return localIP;
}
}
在Android平台上,StreamingAssets文件夹被打包到APK中,无法通过标准的File.Exists
和File.ReadAllBytes
访问。解决方案:
UnityWebRequest
读取文件在Android上,服务器需要绑定到设备的实际IP地址,而不是localhost:
// 在Android上使用本地IP地址而不是localhost
string url = Application.platform == RuntimePlatform.Android
? $"http://{GetLocalIPAddress()}:8080/"
: "http://localhost:8080/";
React应用可能使用不同的路径结构,导致在Unity WebView中加载时出现404错误。我们实现了路径映射解决方案:
// 路径修正 - 处理web/game/路径的映射
if (localPath.StartsWith("game/") || localPath.Contains("/game/"))
{
string originalPath = localPath;
// 处理game/assets/到assets/的映射
if (localPath.Contains("game/assets/"))
{
localPath = localPath.Replace("game/assets/", "assets/");
}
// 处理game/image/到image/的映射
else if (localPath.Contains("game/image/"))
{
localPath = localPath.Replace("game/image/", "image/");
}
// 处理其他可能的game/目录映射
else
{
localPath = localPath.Replace("game/", "");
}
Debug.Log($"Path corrected: {originalPath} -> {localPath}");
}
这解决了以下错误:
Failed to read file jar:file:///.../base.apk!/assets/web/game/assets/gameConfig.json
Failed to read file jar:file:///.../base.apk!/assets/web/game/image/title_memory_card_text_tu.png
在AndroidManifest.xml
中添加必要的网络权限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:icon="@drawable/app_icon"
android:label="@string/app_name"
android:usesCleartextTraffic="true">
application>
准备React项目
npm run build
Assets/StreamingAssets/web/
配置Unity项目
ServerManager.cs
、UnityStaticFileServer.cs
和WebViewLoader.cs
AndroidManifest.xml
包含必要的网络权限设置场景
ServerManager
组件到场景中的GameObjectWebViewLoader
组件到包含WebView的GameObjectCanvasWebViewPrefab
引用构建和部署
如果遇到404错误,检查:
如果遇到网络错误,检查:
如果WebView显示空白,检查:
如果遇到文件读取错误,检查:
通过以上步骤和解决方案,您应该能够成功将React应用打包到Android,并通过Unity中的本地服务器和WebView加载它。如果仍然遇到问题,请查看Unity控制台日志以获取更详细的错误信息。