面 App 内嵌 H5 相关的岗位,hybrid 几乎是必问。问法也很固定,先问「hybrid 是什么」,再问「为什么不直接用 H5」,然后一路追到「静态资源怎么更新」「前端怎么调用客户端的相机」。我见过不少同学能背出「混合开发」四个字,但一到追问静态包怎么下发、schema 协议长什么样就卡住了。
这篇按面试官的追问顺序把这条线走一遍。下面这张图是整篇的骨架,先扫一眼有个印象:

这张图值得先看的地方是它的分叉:左边是「怎么加载」,右边是「怎么通信」。hybrid 的所有问题基本都能归到这两类里,记住这个分法,面试时不至于把答案讲散。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- hybrid 到底混合了什么,它解决的是哪个具体痛点
- webview 是什么,它和普通浏览器的区别在哪
- file 协议和 http(s) 协议在 App 里各自的角色
- 什么功能该用 NA,什么该用 hybrid,什么随便丢个 H5 就行
- 静态资源 zip 包的版本更新上线全流程
- schema 协议的设计、调用和封装
- 2018 年的这套方案,放到今天还剩多少能用
一、hybrid 是什么,为什么会有它
1.1 先把「混合」两个字拆开
hybrid 即「混合」,指的是前端和客户端的混合开发。它需要前端开发人员和客户端开发人员配合完成,某些环节也可能涉及到 server 端。
这里提前说一句:不要以为自己是前端就可以不理会客户端的知识。hybrid 出问题的时候,一半的锅在两端的交界处,你不懂客户端那侧发生了什么,就只能干等着别人排查。
下面这张图画的是 hybrid 在整个技术栈里的位置:

看这张图要抓的重点是它夹在中间的位置。hybrid 不是一门新技术,它是一种妥协的产物,往左靠一点体验好、更新慢,往右靠一点更新快、体验糙。后面所有的取舍都是围着这条轴线转的。
1.2 存在价值,核心只有一条
hybrid 的价值通常被列成三条:
- 可以快速迭代更新【关键】(无需 app 审核,思考为何?)
- 体验流畅(和 NA 的体验基本类似)
- 减少开发和沟通成本,双端公用一套代码
三条里真正的核心是第一条。
App 发版要走应用商店审核,iOS 那边尤其慢,一个文案错别字都得等一轮。而 hybrid 页面的 HTML、JS、CSS 是可以被替换的,替换完客户端不用重新安装,用户下次打开就是新的。这就是那个「思考为何」的答案:审核管的是二进制包,不管你包里的静态文件被换成了什么。
后面两条是顺带的好处,不是它诞生的原因。
1.3 webview,一个装在 App 里的浏览器内核
webview 是 app 中的一个组件,app 可以有 webview,也可以没有。它用于加载 h5 页面,是一个小型的浏览器内核。

这张图要记的是 webview 和 App 的从属关系:webview 是 App 里的一块区域,不是反过来。所以 webview 能干什么、有什么限制,最终由客户端那侧的配置说了算。

这张接着说 webview 的能力边界。容易记混的地方是把 webview 当成 Chrome,它确实是浏览器内核,但它没有地址栏、没有前进后退按钮、Cookie 策略和调试方式也和浏览器不一样。你在 Chrome 里跑得好好的页面,进了 webview 出问题,八成是这些差异导致的。
1.4 file 协议,其实你早就用过
其实在一开始接触 html 开发,就已经使用了 file 协议,只不过你当时没有「协议」「标准」这些概念。双击一个本地 html 文件,浏览器地址栏出现的 file:// 就是它。

这张图的重点是 file 协议读的是磁盘上的文件,没有网络请求这一步。所以它快,快得不是一星半点。代价是它没有域的概念,跨域策略、Cookie、Ajax 这些在 file 协议下的行为都和 http 不一样,这是最容易踩的坑。
1.5 http(s) 协议

这张是对照组。http(s) 走网络,能力完整,但要经历 DNS、TCP、TLS、下载这一整套流程。弱网环境下白屏时间会非常难看,缓存这块可以配合 HTTP 缓存策略优化,具体规则我在 浏览器缓存机制详解 里单独展开过。
两者的区别一句话就能说完:
- file 协议:本地文件,快
- http(s) 协议:网络加载,慢
1.6 什么时候用 hybrid,什么时候不用
不是所有场景都适合使用 hybrid。判断标准是两个维度的组合,体验要求和变更频率:
- 使用 NA:体验要求极致,变化不频繁(如头条的首页)
- 使用 hybrid:体验要求高,变化频繁(如头条的新闻详情页)
- 使用 h5:体验无要求,不常用(如举报、反馈等页面)
你想想看,首页是用户每天都看的门面,卡一下就掉口碑,但它半年也改不了几次结构,那就值得用原生把体验做到顶。反过来,举报页面一年打开不了几次,为它单独走一遍发版流程不值当。
具体实现的流程是这样的:
- 前端做好静态页面(html js css),将文件交给客户端
- 客户端拿到前端静态页面,以文件形式存储在 app 中
- 客户端在一个 webview 中
- 使用 file 协议加载静态页面

这张图把上面四步画成了链路,重点在最后一环:加载用的是 file 协议,不是 http。这就是 hybrid 体验能接近原生的直接原因,页面根本没有网络加载这一步,打开就是本地文件。
二、更新上线流程,hybrid 最难的那部分
2.1 先想清楚要解决什么
静态文件内置在 App 里,带来的新问题是:改一行代码,用户手机里那份文件怎么变?

这张流程图要看的是箭头方向,从我们的构建产物一路流到用户设备。中间每多一个环节,就多一个可能失败的点,后面讲的版本号比对、下载、解压、覆盖,每一步都能出事。
顺着这个思路往下推:

这张图讲的是推导过程,比结论本身更值得记。逻辑链是这样的:
- 要替换每个客户端的静态文件
- 只能客户端来做(客户端是我们开发的)
- 客户端去 server 下载最新的静态文件
- 我们维护 server 的静态文件
面试时能把这个推导讲出来,比直接背「用 zip 包更新」高一个档次,因为它说明你知道为什么必须是客户端来做这件事,浏览器侧根本没有写本地文件的权限。
2.2 完整流程

这张是整篇最值得截下来的图。它对应的五个步骤是:
- 分版本,有版本号,如
201803211015 - 将静态文件压缩成 zip 包,上传到服务端
- 客户端每次启动,都去服务端检查版本号
- 如果服务端版本号大于客户端版本号,就去下载最新的 zip 包
- 下载完之后解压包,然后将现有文件覆盖
容易记混的是检查时机。它是在客户端启动时检查,不是每次打开页面都检查。所以用户这次冷启动拿到的还是旧版本,下次启动才生效,这个延迟是这套方案的固有特性,产品同学经常搞不清楚为什么改了内容不是马上就见效。
要点归纳成三句话:
- 要点1:服务端的版本和 zip 包维护
- 要点2:更新 zip 包之前,先对比版本号
- 要点3:zip 下载解压和覆盖
这里有个坑要注意,覆盖这一步不是原子操作。如果下载到一半断网,或者解压过程中 App 被杀掉,就可能出现新旧文件混在一起的状态,页面直接白屏。稳妥的做法是先解压到临时目录,校验完整性之后再整体切换目录指针,而不是边解压边覆盖原目录。这块我只在做过的一个内嵌活动项目里验证过,不同 App 的实现差异挺大。
2.3 hybrid 和 h5 到底差在哪
优点:
- 体验更好,跟 NA 体验基本一致
- 可快速迭代,无需 app 审核【关键】
缺点:
- 开发成本高。联调、测试、查 bug 都比较麻烦
- 运维成本高。参考此前讲过的更新上线的流程
适用场景:
- hybrid:产品的稳定功能,体验要求高,迭代频繁
- h5:单次的运营活动(如 xx 红包)或不常用功能
那两条缺点不是随口说的。联调麻烦是因为你改一行代码要重新打包、重新内置、重新装 App 才能看到效果;查 bug 麻烦是因为 webview 里没有 DevTools,得靠远程调试连上去,安卓和 iOS 的连法还不一样。真正做过一轮就知道,这两条成本比想象中高得多。
三、前端和客户端怎么通信
3.1 基本形式
到这里进入 hybrid 的第二条主线。页面跑在 webview 里,但相机、定位、通讯录这些能力在客户端那边,前端要用就得喊一声。
基本形式是:
- JS 访问客户端能力,传递参数和回调函数
- 客户端通过回调函数返回内容

这张图的重点是回调函数这个设计。因为客户端拿数据是异步的,比如拍照要等用户按快门,所以不可能用同步返回值。这套「传参数 + 传回调」的形式,和你写 Node 早期 API 的感觉是一样的。
3.2 schema 协议
之前介绍了 http(s) 和 file 协议,schema 协议则是前端和客户端通讯的约定。它不是什么标准协议,就是双方商量出来的一套 URL 格式。

这张图拆的是 schema 的 URL 结构,看的时候盯住协议名那一段。它不是 http,而是 App 自己注册的一个私有协议名,客户端拦截到这个协议名开头的请求就知道「这是自己人在喊我」。

这张是具体调用的样子。容易记混的是参数怎么传,参数是拼在 URL 的 query 里的,所以有长度上限,也需要做编码。传一张 base64 图片过去这条路基本走不通,这个限制在设计接口时就要考虑进去。

这张讲的是客户端侧怎么接住。核心机制是拦截,客户端监听 webview 发起的跳转,发现协议名对得上就不真的去加载,转而执行对应的原生逻辑。
3.3 为什么必须封装
先说结论,裸着用 schema 是不能上生产的。

这张图讲封装的第一层:统一入口。所有调用都从一个函数走,参数拼接、编码、协议名这些细节收在里面,业务代码只管传一个对象。

这张是封装里最关键的一块,回调的注册和命名。回调函数没法直接塞进 URL 里,所以做法是把它挂到全局对象上生成一个唯一的函数名,把这个名字作为参数传给客户端,客户端执行完再按名字回调回来。
这里的坑是回调名冲突和内存泄漏。名字要带随机数或者自增序号,用完之后记得从全局对象上删掉,否则页面开久了全局对象会越挂越多。

这张是封装完之后业务代码长什么样,对比前面裸调用的写法能看出差别有多大。重点是业务层完全不知道 schema 的存在,将来协议改了也只需要动封装层,这就是封装真正的价值。
3.4 内置上线
封装的代码最终会被打包成一个文件,叫做 invoke.js,内置到客户端:
- 将以上封装的代码打包,叫做
invoke.js,内置到客户端 - 客户端每次启动 webview,都默认执行
invoke.js - 本地加载,免去网络加载的时间,更快
- 本地加载,没有网络请求,黑客看不到 schema 协议,更安全
安全那条要客观看待。把 invoke.js 内置进去确实提高了抓包的门槛,但它挡不住反编译,也挡不住有人用调试工具直接在 webview 里执行 JS。真正的安全线还是要在客户端侧做,比如校验调用来源的域名白名单,只把敏感能力开放给自家域名的页面。
四、2018 年的这套方案,现在还剩什么
原文写在 2018 年,这套流程在当时的国内 App 里是主流。这几年变化不小,但变的多是实现细节,思路的骨架其实没动。
webview 这一层变化最大。安卓侧的系统 WebView 早就从内核不一、版本碎片的状态,变成可以随 Google Play 服务独立更新的组件,很多大厂 App 还会内置自己的浏览器内核以求两端一致;iOS 侧则从 UIWebView 全面迁移到了 WKWebView。带来的实际影响是,那些为了绕开老内核 bug 写的 hack 代码,今天大部分可以删掉了。
通信这一层,schema 拦截的写法仍然能用,但主流做法换了。现在更常见的是通过客户端注入的 JS 接口对象直接调用,安卓有对应的注入机制,iOS 侧 WKWebView 提供了消息处理器的方式。相比拼 URL,这条路没有长度限制,也不需要处理编码,传结构化数据方便得多。老写法作为兜底还留着,新代码一般不再首选它。
静态资源更新这块基本没变,仍然是版本号比对加增量包下发,只是很多团队把「整包覆盖」换成了差分更新,只下发变化的文件,包体小很多。热更新在 iOS 侧有合规红线,改的必须是 Web 资源而不是可执行逻辑,这条线不能踩。
还有一条更大的变化是,纯 hybrid 的地盘被小程序容器和 React Native、Flutter 这类方案分走了不少。但小程序容器的底子其实还是这一套:本地包 + 版本更新 + JS 与原生的桥接通信。所以这篇讲的东西不是过时的知识,它是理解那些新方案的地基。
同系列的另外两篇也建议一起看,前端面试之组件化 讲组件划分和通信,前端面试之MVVM浅析 讲响应式和模板编译,三篇凑齐正好是这套面试题的完整地图。
总结
回过头看,hybrid 这套东西回答面试题时可以收成两条线。
加载这条线:webview 是 App 里的浏览器内核,静态资源内置到本地用 file 协议加载所以快,要更新就靠版本号比对加 zip 包下发,检查时机在客户端启动时,覆盖过程不是原子的要小心。
通信这条线:JS 传参数和回调给客户端,走 schema 协议的 URL 拦截,回调靠挂全局函数加唯一命名实现,封装成 invoke.js 内置到客户端,业务层不接触协议细节。
如果只能记一句,那就记 hybrid 存在的唯一硬理由是绕开应用商店审核。其它所有设计,本地包也好、schema 也好,都是为了在「像原生一样快」和「像网页一样能改」这两个目标之间找平衡点。今天的方案换了实现,这个平衡点本身没变。