H5混合开发出现的背景
在移动端早期(2010年前后),App 市场被 iOS(苹果)和 Android(安卓)两分天下。
-
原生开发的困境: 如果一家公司想做一个 App,必须雇两拨人。一波人用 Swift/OC 写 iOS 版,另一波人用 Java 写 Android 版。
-
成本翻倍: 逻辑一模一样,但代码完全不通,开发成本、测试成本、维护成本全部翻倍。
-
时间成本高: 原生 App 更新一次,需要重新打包、上传应用商店、经历数天的审核(尤其是 App Store)。
-
无法即时触达: 如果 App 发现一个紧急 Bug,原生开发无法像网页一样“刷新即修复”,用户必须手动下载更新包。
-
痛点: 开发者在想,能不能写一套代码,在两个系统都能跑?
在这个背景之下,H5混合开发出现了,H5 混合开发(Hybrid App Development) ,本质上是:
- 用 Web 技术(HTML / CSS / JS)开发页面,在“原生 App 壳”下运行,并且可以调用原生能力的一种开发模式。
这样一套逻辑,多端使用,提高了开发效率,减少了开发/测试/运维的成本。
H5混合的核心是WebView
什么是WebView
WebView 本质上是:
- App 里内置的一个“浏览器内核组件”,用来加载和渲染网页(HTML / CSS / JS)
一句话理解: WebView = 没有地址栏的浏览器
这样使得前端开发的页面/逻辑,能在手机app页面中运行起来
在开发混合应用时,我们不一定要像浏览器那样去输入网址访问。我们可以直接把写好的 HTML、CSS、JS 文件打包进 App 的安装包里。当用户打开 App 时,WebView 会直接加载这些本地文件。
- 感觉上: 它就是一个本地页面,加载速度极快,甚至没网也能打开。
- 效果上: 用户根本察觉不到自己是在看网页
混合开发的底层逻辑
-
H5 (内容) :负责页面的样子和逻辑(像装修)。
-
WebView (容器) :负责把 H5 渲染出来(像房间)。
-
原生外壳 (载体) :负责提供 WebView 运行的环境(像地基)。
H5 混合开发如何解决原生痛点?
通过 WebView 这个桥梁,混合开发模式精准地击碎了原生开发的枷锁:
1. 一套代码,到处运行
- 原生做法: 雇两拨人,写两套代码(iOS + Android)。
- 混合做法: 核心业务逻辑都用 H5 编写。由于 WebView 在不同系统上的渲染效果基本一致,开发者只需写一套 H5 代码,就可以同时跑在苹果和安卓手机上。
- 结果: 研发人力成本直接降低了近 50% 。
2. 解决“审核慢”:跳过商店,实时生效(热更新)
- 原生做法: 改个错别字都要重新打包、提交审核、等待数天。
- 混合做法: H5 页面可以放在服务器上。当需要修改内容时,开发者只需在服务器更新文件,用户重新打开 App 时,WebView 就会加载最新的页面。
- 结果: 实现了**“Bug 秒修复”和“活动秒上线”**,完全不受应用商店审核周期的限制。
H5 和原生是怎么通信的?
H5 只是“住”在 WebView 里,它依然只是个普通的网页。要让它变成真正的 App,必须让它能和手机硬件(如相机、蓝牙)交流。
既然 H5 被包裹在原生系统的“外壳”里,它们之间就必须有一条沟通的管道。这个通信机制有一个专业术语叫:JSBridge(JS 桥)。
你可以把它想象成“房间里的对讲机”,让住在 WebView 里的 H5 能和外面的原生系统对话。
JSBridge 是一种 JS 实现的 Bridge,连接着桥两端的 Native 和 WebView。
协作链条
JS (发出指令) → Bridge (格式转换) → Native (执行动作) → OS (调用硬件)
- 这种结构的本质是:利用 Native 的权限,扩展 Web 的能力。
它在 APP 内方便地让 Native 调用 JS,JS 调用 Native ,是双向通信的通道。
JSBridge-JS调用原生能力
1.拦截URL scheme
原理一句话
JS 构造一个特殊协议的 URL, WebView 在加载 URL 时被原生拦截, 原生解析 URL → 执行对应 Native 方法。
它是如何工作的?(以调用相机为例)
- 定暗号: 原生开发和前端开发约定好一个特殊的“暗号”(协议),比如:
myapp://。 - 发指令: 网页(H5)里不需要真的跳转页面,它只需要执行一行代码:
window.location.href = "myapp://utils/openCamera?id=123";这看起来像是在跳网址,但其实是发出了一个“调用相机”的指令。 - 拦截并执行: WebView 像一个过滤器,它发现这个请求不是
http或https开头,而是约定的暗号myapp,于是立刻拦下这个请求,不让它真的跳转,而是解析出后面的指令(openCamera),然后直接在后台打开手机摄像头。
JS侧代码示例
/**
* 1. 定义发送指令的函数
* @param {String} action 要执行的动作,如 'openCamera'
* @param {Object} params 传递给原生的参数
*/
function callNative(action, params) {
// 构造约定的特殊协议头(暗号)
const scheme = "myapp://utils/";
// 将参数转为字符串(如:id=123)
const query = new URLSearchParams(params).toString();
// 拼接成完整的伪 URL:myapp://utils/openCamera?id=123
const url = `${scheme}${action}?${query}`;
// 发送暗号:通过创建一个不可见的 iframe 来触发请求(比直接改 location 更安全,不会导致页面刷新)
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
// 记得移除,保持页面整洁
setTimeout(() => {
document.body.removeChild(iframe);
}, 200);
}
// 2. 实际调用:点击按钮打开相机
document.getElementById('cameraBtn').onclick = function() {
console.log("准备调用原生相机...");
callNative('openCamera', { id: 123, quality: 80 });
};
/**
* 3. 结果回调:原生处理完后(拍完照),会主动调用这个 JS 函数把图片传回来
*/
window.onCameraSuccess = function(photoData) {
const img = document.getElementById('preview');
img.src = "data:image/jpeg;base64," + photoData;
alert("原生相机调用成功!照片已显示。");
};
-
构造 URL: 我们把想做的事情(
openCamera)和参数(id=123)像写网址一样拼起来。 -
触发拦截: 当这个
iframe尝试加载这个myapp://开头的地址时,WebView 内部的拦截器会瞬间抓取到这个请求。 -
识别指令: 原生代码会看到这个地址,它会想:“这不是普通的网页,这是我们约定的暗号!”于是它解析出
openCamera,立刻调起系统相机。 -
异步回调: 因为拍照需要时间,所以原生处理完后,会反向执行一段 JS 代码(比如
window.onCameraSuccess(...)),把结果“喂”回给网页。
2. 注入 API(Context Injection)
原理一句话
原生系统直接在 WebView 的 JavaScript 环境中注入一个全局对象,JS 像调用普通本地函数一样调用原生功能。
比喻理解
- URL Scheme(拦截): 网页在屋里大喊一声暗号,门外的原生系统听到了去办事(像对讲机)。
- API 注入: 原生系统直接送给网页一个“万能遥控器”(全局对象),网页想干嘛,直接按遥控器上的按钮就行了。
JS侧代码示例
这种方式下,JS 不需要构造复杂的 URL,代码非常自然
/**
* 现代主流方式:直接通过原生注入的对象调用
*/
document.getElementById('cameraBtn').onclick = function() {
// 这里的 'NativeBridge' 是原生端提前注入到 window 对象上的名
// 就像 window.alert 一样,不需要定义,直接使用
if (window.NativeBridge && window.NativeBridge.openCamera) {
// 直接调用,像写普通的 JS 函数一样
// 甚至可以像原生一样直接传复杂的 JSON 对象
window.NativeBridge.openCamera({
id: 123,
quality: 80,
callback: "onCameraSuccess" // 告诉原生:处理完后调用我的哪个函数
});
} else {
console.error("当前环境不支持原生调用");
}
};
/**
* 回调函数(保持不变)
*/
window.onCameraSuccess = function(photoData) {
document.getElementById('preview').src = "data:image/jpeg;base64," + photoData;
};
API 注入的优势
-
性能更高: 不需要创建
iframe或修改location,没有解析 URL 字符串的开销,通信几乎是瞬时的。 -
数据传输量大: URL Scheme 受限于 URL 的长度限制(通常 2KB 左右),而 API 注入可以传递非常大的 JSON 数据。
-
代码更优雅: 前端开发者不需要关心“暗号”怎么拼,只需要像调用普通的
Library一样开发即可。
API注入是现在目前主流的JSbridge通信方式