
在移动端项目里接入 UniWebView 做内嵌网页是 Unity 开发中非常常见但又特别容易踩坑的需求。尤其是当你需要把网页“嵌”进 UGUI 的某个面板里而不是简简单单弹出一个全屏页面时尺寸计算、层级遮挡、触摸事件冲突这些问题就会一股脑儿冒出来。这篇博文就围绕 UniWebView 在 UGUI 下的实战展开重点讲清楚动态调整网页尺寸的核心逻辑以及 Unity 与网页之间交互优化的几个关键细节。无论你是刚开始接触 UniWebView还是已经被它的层级和尺寸问题折磨过几轮这篇文章都值得你花几分钟看完。1. 为什么要在 UGUI 里嵌入 UniWebView需求场景与方案取舍1.1 移动端 App 内嵌网页的典型业务场景先聊聊什么情况下你会用到 UniWebView。最典型的是 H5 活动页、用户协议和隐私政策、第三方支付页面、或者 App 内的广告落地页。这些页面往往需要频繁更新如果全部用 Unity 原生 UI 重写更新一次的流程能让你怀疑人生。所以很多团队选择直接嵌入一个网页把更新频率高的内容交给 Web 端去维护。另一个场景是游戏内嵌社区或客服系统。比如玩家在游戏里点击“论坛”按钮弹出一个半屏的网页面板页面里面可以看帖子、发评论。这种场景下网页只是游戏 UI 的一部分它在屏幕上占据一块区域下面还要露出游戏界面。这种需求就要求网页尺寸和位置可以被灵活控制不能简单粗暴地全屏展示。还有一种比较特殊的场景是“网页与游戏画面混合渲染”。比如你要做一个互动玩法页面里展示游戏数据的可视化图表点击图表的某个节点后Unity 这边要响应并播放对应的 3D 动画。这种情况下Unity 和网页之间的通信链路就变得至关重要了。1.2 直接全屏 WebView 的痛点与“动态尺寸”的真实需求很多初学者一开始的做法是初始化 UniWebViewLoad 一个 URL然后直接 Show() 全屏显示。这当然简单但也带来几个问题。第一全屏 WebView 会完全遮住游戏画面用户体验非常割裂。想象一下玩家正在游戏里突然整个界面变成网页连个过渡都没有这体验说实话挺糟的。第二全屏 WebView 和游戏 UI 的交互完全隔离玩家想返回时只能靠网页里的返回按钮或者你额外做一个返回按钮叠在 WebView 上面。第三也是最重要的很多业务场景根本不需要全屏一个 800x600 的窗口面板就够了。这就引出了“动态调整内嵌网页尺寸”的真实需求。所谓动态指的是网页的 RectTransform 尺寸可以根据 UI 布局变化而实时调整。比如面板从屏幕底部滑出时网页高度要跟着动画逐渐展开再比如横竖屏切换时网页区域要自适应旋转。这里的关键在于UniWebView 本身是一个原生层 View它不会自动跟随 UGUI 的 RectTransform 变化所以你需要手动把 UI 的尺寸同步到 UniWebView 上。1.3 技术选型为什么不直接用 RawImage 渲染流程而选 UniWebView在 Unity 里还有一种“伪内嵌网页”的方案用 UnityWebRequest 去请求网页内容然后用 RawImage 显示截图。这个方案在技术上可行但实际用起来问题不少。HTML 页面里的 JS、CSS 动画、视频播放都没法完整支持交互也只能通过点击坐标映射来实现维护成本极高。相比之下UniWebView 直接调用 iOS 的 WKWebView 和 Android 的 WebView本质上是原生浏览器的内核。它的渲染不在 Unity 的渲染管线里而是作为原生 View 叠加在 Unity 视图之上。因此它支持完整的 HTML5、CSS3、JavaScript 能力交互也跟原生浏览器完全一致。代价就是你得处理好它和 UGUI 之间的层级与坐标关系这正是本文的核心内容。理解 UniWebView 是“原生 View 叠加层”这一点非常重要。它不在 Canvas 里也不是一个 UI 控件。你设置它的 Frame 时用的是屏幕坐标系而不是 UGUI 的本地坐标系。很多问题追根溯源都是这个本质没搞清楚。2. UniWebView 在 UGUI 中定位的原理从屏幕坐标到 RectTransform 的换算2.1 UniWebView 的本质原生 View 叠加而非 UGUI 组件先说个基础但重要的概念。UniWebView 不是 UGUI 组件它没有 RectTransform也不能作为 Canvas 的子物体来布局。它更像是一个“悬浮在 Unity 窗口之上的原生窗口”。你在 iOS 上看到的是 WKWebView 的实例在 Android 上看到的是 WebView 的实例。这个原生 View 的位置和大小由 Frame 参数决定Frame 的原点在屏幕左上角x 向右y 向下。这意味着如果你想让它“看起来像嵌在某个 UGUI 面板里”你需要做的是拿到目标 UGUI 面板在屏幕上的实际像素区域。把这块区域转换成 UniWebView 的 Frame也就是左上角坐标加宽高。在合适的时机调用 SetFrame 让原生 View 移动和缩放。这里有个天然的优势因为 UniWebView 是叠加层它的渲染层级天然高于所有 UGUI 元素。也就是说它不需要排序也能显示在所有 UI 之上。如果要做“网页下面露出游戏画面”的效果反而是很自然的。2.2 把 WebView“贴”到目标 RectTransform 上的计算方法假设你有一个名为 WebPanel 的 UGUI 面板它是一个带 Image 组件的空物体。想让 UniWebView 正好覆盖住这个面板核心代码可以这样写public RectTransform webPanelRect; public void AlignWebViewToRect(RectTransform rectTransform, UniWebView webView) { // 1. 获取 RectTransform 在世界空间的四个角 Vector3[] corners new Vector3[4]; rectTransform.GetWorldCorners(corners); // 2. 将世界坐标转成屏幕坐标 Vector2 screenPosLB RectTransformUtility.WorldToScreenPoint(Camera.main, corners[0]); Vector2 screenPosRT RectTransformUtility.WorldToScreenPoint(Camera.main, corners[2]); // 3. UniWebView 的 Frame 原点在左上角y 轴向下 float x screenPosLB.x; float y Screen.height - screenPosRT.y; float width screenPosRT.x - screenPosLB.x; float height screenPosRT.y - screenPosLB.y; // 4. 将像素单位转换为 Point如果需要适配屏幕缩放 Rect frame new Rect(x / UnityWebViewUtility.PixelsPerPoint(), y / UnityWebViewUtility.PixelsPerPoint(), width / UnityWebViewUtility.PixelsPerPoint(), height / UnityWebViewUtility.PixelsPerPoint()); webView.SetFrame(frame); }这里有一个特别需要注意的点如果你的 Canvas 渲染模式是 Screen Space - Camera 或者 Screen Space - OverlayWorldToScreenPoint 的用法会略有差异。Overlay 模式下不需要传 Camera传 null 即可Camera 模式下必须传对应的渲染相机。如果你用 ScreenSpaceCamera 但这里传了 null坐标会完全错乱而且这种错乱很难一眼看出来表现就是网页位置和 UI 位置差了一截。另一个关键点是UnityWebViewUtility.PixelsPerPoint()。在 iOS 上Retina 屏幕的屏幕像素密度是 2x 或 3x如果你直接把像素值传给 UniWebView网页会显示得非常小。UniWebView 内部使用 Point 作为单位所以你要把像素除以屏幕密度。Android 上虽然大部分情况下 1 像素等于 1 dp但也要注意部分设备的 density 值不是 1。如果不做这个换算你就会发现网页尺寸在真机上始终不对但在编辑器里看着还挺正常。2.3 关于 Canvas 渲染模式、缩放因子和 SafeArea 的注意事项使用 CanvasScaler 的时候UI 坐标和屏幕像素之间有一个缩放关系。比如你的 CanvasScaler 设置了 “Scale With Screen Size”参考分辨率是 1080x1920但实际屏幕是 2340x1080那 UI 的坐标和屏幕像素就不是一一对应的。在实际项目中我建议你直接以目标 RectTransform 的GetWorldCorners来算屏幕坐标而不是从 RectTransform.rect 去推。因为前者直接考虑到了 Canvas 的缩放和相机渲染算出来的位置是准确的。从 rect 和 anchoredPosition 推的话你要手动考虑 pivot、父节点缩放、Canvas 缩放中间任何一步出错都会导致偏移。还有 SafeArea 的问题。如果你的项目适配了 iPhone 的刘海屏而你的 UI 面板放在了 SafeArea 内用 GetWorldCorners 自动会算到 SafeArea 区域的实际屏幕位置一般不会出问题。真正容易出问题的是横竖屏切换的时候。设备方向一变SafeArea 会变如果你的 UGUI 布局跟着旋转了但 UniWebView 的 Frame 没有重新计算网页就会跑偏。解决办法是在OnRectTransformDimensionsChange或者监听屏幕方向变化事件里重新调用一次 Align 方法。建议在 Update 里做一个脏标记检测只要目标 RectTransform 的尺寸或位置发生变化就重新同步一次 Frame。不要每帧都同步那样性能损耗太大也不要只在初始化时同步一次那样旋转和动态布局的时候会跟不上。3. 核心实操动态调整内嵌网页尺寸的完整实现3.1 基础初始化与加载流程先把最基础的流程跑通。你要做一个 MonoBehaviour 脚本来管理 UniWebView 的生命周期包括创建、加载、显示、销毁。using UnityEngine; using UniWebView; public class InAppWebPageController : MonoBehaviour { [SerializeField] private RectTransform targetRect; [SerializeField] private string url https://example.com; private UniWebView webView; public void InitAndLoad() { // 如果已经在显示先销毁旧实例 if (webView ! null) { webView.Stop(); Destroy(webView.gameObject); } // 创建 UniWebView 实例 GameObject webGo new GameObject(UniWebView); webView webGo.AddComponentUniWebView(); webView.Frame GetScreenFrameFromRect(targetRect); // 监听加载完成事件 webView.OnPageFinished (view, statusCode, curUrl) { Debug.Log($页面加载完成: {curUrl}, 状态码: {statusCode}); webView.Show(); }; webView.OnPageErrorReceived (view, errorCode, errorMessage) { Debug.LogError($页面加载失败: {errorMessage} (错误码: {errorCode})); }; // 加载网页 webView.Load(url); } }这里有个容易忽略的点webView.Show()和webView.Load()的调用顺序。WebView 在加载过程中其实就可以调用 Show()但如果你希望用户看不到白屏过程比较稳妥的做法是先 Load等 OnPageFinished 再 Show()。但也有缺点就是网络慢的时候用户会等很久没反馈。更好的做法是先在 UI 上显示一个 Loading 转圈然后早期 Show()让用户看到网页在加载。3.2 动态调整尺寸的核心方法含动画过渡动态调整尺寸的核心就是调用SetFrame方法。UniWebView 提供两种设置方式一种是直接给绝对坐标的SetFrame另一种是相对父视图的SetRect。在 UGUI 的场景里我们主要用SetFrame。直接调用 SetFrame 是瞬间生效的但实际业务里经常需要平滑过渡。比如按钮展开一个网页面板网页要从 0 高度慢慢长高或者从底部滑入。这时候你就需要对 Frame 做插值。using DG.Tweening; public void ResizeWebViewAnimated(Rect newFrame, float duration) { Rect startFrame webView.Frame; // 这里用一个虚拟值做插值 DOTween.To(() 0f, t { Rect currentFrame new Rect(); currentFrame.x Mathf.Lerp(startFrame.x, newFrame.x, t); currentFrame.y Mathf.Lerp(startFrame.y, newFrame.y, t); currentFrame.width Mathf.Lerp(startFrame.width, newFrame.width, t); currentFrame.height Mathf.Lerp(startFrame.height, newFrame.height, t); webView.SetFrame(currentFrame); }, 1f, duration).SetEase(Ease.OutCubic); }注意这里的插值是以起始 Frame 和结束 Frame 为基准的。如果用 DOTween 直接对 Rect 插值中间值可能会因为 x、y、width、height 的插值速度不一致而出现“网页拉扯”的视觉问题。所以我把整个动画统一到同一个进度 t 上保证四个参数同步变化。在实际项目中动画的表现往往不是简单的矩形缩放。常见的效果是“抽屉滑出”比如网页面板从屏幕底部滑出来。这种效果其实就是起始 Frame 的 y 在屏幕底部以下或者高度极小终止 Frame 的 y 值等于 UI 面板的底部位置。这里要给一个提示SetFrame 有启用/禁用动画的内置参数。在 UniWebView 新版本中你还可以用SetFrame(frame, animated: true, duration: 0.35f)来触发内置的动画过渡。如果你的需求只是简单的展开收起用内置动画完全够了但如果你要 Ease 效果和节奏控制那就用 DOTween 自己控制更灵活。3.3 指定位置打开与关闭的完整封装实际项目中你不会只在一个地方用网页所以最好做一个通用管理器。这个管理器负责创建 WebView、记录当前显示的面板、提供统一的打开和关闭接口。public class WebViewPanelManager : MonoBehaviour { public static WebViewPanelManager Instance { get; private set; } [SerializeField] private RectTransform webPanelRect; private UniWebView webView; private string currentUrl; private void Awake() { Instance this; } public void Open(string url, RectTransform anchorRect null) { currentUrl url; RectTransform target anchorRect ! null ? anchorRect : webPanelRect; if (webView null) { GameObject webGo new GameObject(UniWebViewManager); webView webGo.AddComponentUniWebView(); } webView.Frame GetScreenFrameFromRect(target); webView.Load(url); webView.Show(); } public void Close() { if (webView ! null) { webView.Hide(); // 注意不要在这里 Destroy频繁销毁重建会造成性能损耗 } } public void SyncFrame(RectTransform targetRect) { if (webView ! null) { webView.SetFrame(GetScreenFrameFromRect(targetRect)); } } }这里有个经验之谈不要每次打开网页都销毁重建 UniWebView 实例。UniWebView 的创建和初始化开销不小尤其是在 Android 上每创建一个新的 WebView 就相当于重新起一个渲染进程。你应该复用一个实例需要打开新页面时调用 Load需要隐藏时调用 Hide 而不是直接销毁。需要销毁实例的时机是什么一般是整个页面流程彻底结束比如玩家退出了这个功能模块。这时候你可以调用webView.Stop()和Destroy(webView.gameObject)来释放原生资源。关闭动画和关闭逻辑也要注意顺序。如果你先调用 Hide()WebView 会立刻消失关闭动画就没有意义了。正确的方式是先播放 UI 面板的关闭动画同时在动画结束时或过程中调用 Hide()或者调整 Frame 让网页视觉上跟着面板一起退场。4. 交互优化Unity 与网页之间的双向通信4.1 网页点击事件与 UGUI 点击事件的冲突处理网页嵌入 UGUI 之后第一个要面对的问题就是点击穿透。你要理解一件事UniWebView 作为原生层它的触摸响应优先级天然高于 Unity 的 UGUI。只要你的手指点在 WebView 的 Frame 范围内事件就会被原生 WebView 消费掉UGUI 的按钮是收不到这个点击的。这就产生了一个经典问题如果 WebView 覆盖的面板右上角有个“关闭按钮”但这个按钮在 UGUI 层而 WebView 又恰好覆盖了整块区域点击关闭按钮就完全没反应。解决思路有两个。第一个思路是给关闭按钮留出“空隙”。我们做一个小的头部栏关闭按钮放在这个头部栏里而 WebView 的 Frame 从头部栏下方开始不覆盖按钮区域。这样按钮位于 WebView Frame 范围之外UGUI 自然能收到点击。实现也很简单就是把 targetRect 的 Top 部分裁掉一段高度再算 Frame。第二个思路是用 EventSystem 做点击判断。如果你必须让 WebView 覆盖按钮区域那么可以这样在按钮的点击脚本里先判断当前手指位置是否在 WebView 的 Frame 内。如果在就忽略这次点击如果不在就正常响应。public bool IsPointerOverWebView(Vector2 screenPos) { if (webView null) return false; Rect frame webView.Frame; // UniWebView.Frame 单位是 Point要将屏幕坐标转换为 Point Vector2 pointPos new Vector2( screenPos.x / UnityWebViewUtility.PixelsPerPoint(), (Screen.height - screenPos.y) / UnityWebViewUtility.PixelsPerPoint() ); return frame.Contains(pointPos); }不过要注意这个方案有一个隐患按钮收到了点击但被忽略玩家会困惑。所以从产品角度讲更推荐让按钮区域和 WebView 区域分开互不重叠。这也是 UGUI 内嵌网页的最佳实践留出操作安全区。4.2 从 Unity 调用网页方法EvaluateJavaScript 实战Unity 和网页交互的第一种方式是 Unity 主动调用网页中的 JavaScript 方法。UniWebView 提供了EvaluateJavaScript接口。场景举例游戏里有一个抽卡活动页玩家在网页里点“抽卡”网页把请求发给服务器服务器返回结果后Unity 这边需要更新顶部钻石数量显示。这时 Unity 可以调用网页暴露出来的刷新方法。public void RefreshWebPageData(string playerName, int diamondCount) { if (webView null) return; string js $window.updatePlayerData window.updatePlayerData({playerName}, {diamondCount});; webView.EvaluateJavaScript(js, (result) { Debug.Log($JS 执行结果: {result}); }); }这里的核心是window.updatePlayerData这个函数必须在网页里提前定义好。它属于网页和 Unity 之间约定的“接口”。在实际项目中这个接口协议最好写进需求文档里否则前端工程师和 Unity 工程师各写各的很容易对不上。还有一个细节EvaluateJavaScript 的结果是异步返回的。如果你需要在 Unity 里拿到返回值会有一定的延迟。它不像普通函数调用那样同步返回所以不要试图在调用完 EvaluateJavaScript 的下一行就去读取结果。如果有强时序要求用异步流程管理。4.3 从网页调用 Unity 方法AddJavaScript 与 MessageReceived网页反向调用 Unity是内嵌网页最有价值的能力。比如网页上的按钮点击后要通知 Unity 打开某个道具弹窗网页里的事件要触发 Unity 端的数据上报网页上的表单提交后要通知 Unity 关闭当前面板。UniWebView 有两种方式实现网页调 Unity。第一种是 AddJavaScript 方式。它会把一段 JavaScript 代码注入到网页中网页可以直接调用这个注入的函数。private void SetupJavascriptBridge() { webView.AddJavaScript(unityBridge, (payload) { // payload 是网页传过来的字符串参数 Debug.Log($收到网页消息: {payload}); HandleWebMessage(payload); }); }在网页端前端工程师调用方式是这样的// 网页里的按钮点击事件 document.getElementById(openGiftBtn).addEventListener(click, function() { unityBridge(open_gift_panel); });第二种是 MessageReceived 事件方式。这种方式的原理是网页里的 JS 代码通过跳转自定义 URL Scheme 来触发本地事件。UniWebView 会捕获到这种 scheme 跳转然后回调给 Unity。webView.OnMessageReceived (view, message) { Debug.Log($收到 scheme 消息: {message.Url}); // message.Url 形如 unitybridge://open_gift_panel string scheme unitybridge://; if (message.Url.StartsWith(scheme)) { string action message.Url.Substring(scheme.Length); HandleWebAction(action); } };网页端配合的 JS 代码是window.location.href unitybridge://open_gift_panel;两种方式怎么选两种方式都可以但我个人更推荐AddJavaScript因为它的参数传递更结构化你可以在函数名之后直接传 JSON 字符串Unity 解析起来更方便。而 scheme 方式容易因为 URL 编码问题导致特殊字符被转义传中文或复杂 JSON 时麻烦一些。注意如果项目要发布到微信小游戏或者 WebGL 环境UniWebView 是不支持的。微信小游戏的浏览器环境限制很多而且没有直接的 WebView 概念。WebGL 上你可以用 iframe 方案或者 UnityWebRequest 方案但功能和体验都不同。如果你的目标平台包含 WebGL建议从项目立项前就评估清楚。5. 常见问题与排查技巧实录5.1 白屏、黑屏、加载半天不出网页这是 UniWebView 新手最常遇到的问题。排查思路分几步走第一步确认 URL 本身能不能访问。不要一上来就在 Unity 里调试先把 URL 复制到手机浏览器里试试。很多白屏是因为 URL 本身是内网地址或者有 HTTPS 证书问题WebView 加载被拦截。第二步检查是否在加载函数里正确监听了事件。如果网页加载失败但没有注册 OnPageErrorReceived你是看不到任何错误提示的表现就是白屏。我强烈建议在初始化 UniWebView 的时候就把 OnPageErrorReceived、OnPageFinished、OnPageStarted 全部注册好并打日志。第三步检查 Android 的网络权限。如果你的 App 需要访问外部网络别忘了在 AndroidManifest.xml 里加uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE /Unity 打包 Android 时默认是带 INTERNET 权限的但如果你用了某些插件改了主工程配置可能会导致权限丢失。这一步排查成本很低但真的很常见。还有一个比较隐蔽的问题如果你的网页是 HTTP 地址Android 9.0 和 iOS 都默认禁止 HTTP 明文请求。需要做相应配置比如在 AndroidManifest 里设置 usesCleartextTraffic或者在 iOS 的 Info.plist 里配置 ATS 例外。如果你的页面是公司内网系统大概率是 HTTP这个坑几乎必踩。5.2 WebView 尺寸和位置不对pivot、缩放因子、多分辨率适配网页尺寸不对通常不是 UniWebView 的问题而是你的坐标换算逻辑有瑕疵。最常见的错误有两个。第一个是目标 RectTransform 的 anchor 和 pivot 设置太复杂。如果目标面板的 pivot 不是 (0.5, 0.5)而是 (0, 1) 或者其他值你在做本地坐标到世界坐标转换时很容易绕晕。虽然用 GetWorldCorners 可以规避 pivot 导致的坐标偏移但如果你在同步算法里手动计算了 pivot 的偏移量就很容易出错。我的建议是把网页目标面板的 pivot 统一为 (0.5, 0.5)能省掉很多麻烦。第二个是没考虑屏幕 DPI。iOS 和 Android 的真机屏幕密度差异很大。在编辑器里看起来正常的 Frame放到真机上可能偏小或者偏大。解决办法就是我前面提到的用PixelsPerPoint做一次单位转换。多说一句关于多分辨率适配。如果你的游戏需要支持 iPad 和手机屏幕宽高比差别很大。UI 布局通常会在不同设备上自动拉伸但也意味着同一个网页面板在不同设备上的尺寸是不一样的。这时候千万不要缓存 Frame 值每次显示网页时都要重新从 RectTransform 计算一次。最好在 OnRectTransformDimensionsChange 里加一个重新同步的逻辑否则你会看到网页在 iPad 上位置偏移。5.3 内存、性能与生命周期该释放就释放UniWebView 的本质是原生 WebView它的内存开销比较大。一个复杂的 H5 页面在 iOS 上可能占用 100-200MB 内存在 Android 上可能更高。如果项目频繁打开关闭网页内存问题会非常明显。第一个建议是关闭 WebView 之后调用webView.CleanCache()清理缓存。第二个建议是合理地管理生命周期。如果网页不再需要显示Hide()可以隐藏但原生渲染进程还挂着。如果确定一段时间内不会再打开应该Stop()和Destroy()。还有一个很实用的小技巧监听 App 的退后台事件。如果 App 退到后台建议主动调用webView.Stop()或者至少暂停网页里的视频播放。iOS 上如果 App 退后台时 WebView 还在播放视频回来之后可能出现音频和画面不同步。最后再提一下渲染性能。虽然 UniWebView 是原生层它的渲染不走 GPU 的 Unity 管线但它毕竟是全屏或半屏叠加的 View。如果网页里有大量硬件加速的 CSS 动画或者 3D 效果在低端机上会明显影响游戏本身的帧率。这种时候建议网页端工程师降低动画复杂度或者 Unity 这边在显示网页时暂时降低游戏渲染分辨率。6. 最后的经验补充在实际项目中摸爬滚打之后有几个实践中的体会想分享给读者。首先是 Debug 思路。UniWebView 出问题时千万不要只盯着 Unity 的 Console。在 Android 上你可以通过 adb logcat 查看 WebView 的日志在 iOS 上可以用 Safari 的开发者工具直接远程调试真机上的 WKWebView。这样能看到网页端的 JS 报错往往比 Unity 端的信息更有用。其次是关于 UI 结构设计。我后期项目里都会在 UGUI 层专门搭一层“网页占位容器”这个容器最大作用是视觉占位。当 WebView 还没加载出来的时候这个容器可以显示一个默认的占位背景图避免 WebView 出现后界面“突兀地长出来”。等 OnPageFinished 回调后再隐藏占位图视觉上过渡非常自然。最后是关于后续扩展的方向。如果你已经掌握了基础的尺寸同步下一步可以研究键盘弹出适配。当 WebView 里的输入框聚焦时iOS 和 Android 的软键盘会弹出键盘会遮住一部分网页。你需要监听键盘事件动态调整 WebView 的 Frame 高度。这也是一个非常常见的需求而且处理起来比想象中要复杂不少。另外如果你需要在同一个页面里加载多个 WebView 实例还要考虑它们之间的遮挡关系。这些都是可以继续深入的方向但基础的 Frame 换算和交互通信思路掌握熟练了这些后续问题绝大部分都能迎刃而解。