在过去的 6 个月里,我一直在开发 Rizer。这是个手机应用,它让用户给各种分类(目前有动物、小孩、食物、搞笑、男人、自然和女人)的照片 PK 赛评分,用户也可以提交自己的照片到这些分类里,让别人评分。照片采用 ELO 算法来排名(就是 电影《社交网络》里介绍FaceMash网站的场景中Eduardo Saverin 在窗户上画的那个公式),评分最高的将出现在 Rizer 的排行榜上。
Rizer 是一个混合应用,采用 Apache Cordova开发, 通过它上面大量的 native 插件来获取用户设备上的重要的原生功能,例如推送通知、用相机拍照以及进行应用内购买。
目前我霸占了男人排行榜😎
一开始,一般用户并不知道 Rizer 是混合应用,这对我来说至关重要。这意味着它必须设计良好,性能出色 (嘿,performant不是个英语单词),不缺少用户会对此类应用期望的任何原生功能,并且当设备失去网络连接时不至于无法使用。幸运的是,根据我的40名亲友做beta测试的反馈,我实现了这个目标(我甚至可能都同意他们的看法了)。
如果你不相信我,或者你已经玩过一阵子混合应用,那么我推荐你去下载 Rizer,给照片比赛评分,看看排行榜上的一些分类,并且如果你有勇气的话,上传一些自己的照片。体验完后你很可能会被混合应用的能力所折服,如果没有,那至少也欣赏了一些可爱的动物和小宝贝。
毫无疑问,转向混合应用是绝大多数应用的正确选择
理解以下三个关键点非常重要:
一个应用的混合版本一定不会跟原生版本一样快,但这无关紧要,只要混合版本足够快。 简单来说,如果你的用户从来不抱怨应用的性能,那么你的应用就是足够快的。作为一个性能狂,知道自己的应用运行得尽可能地快,我理解这种吸引力,但是我们必须小心避免过早发生和过度优化。知道自己的原生应用在页面上渲染 100 张照片比混合应用版本快 8 毫秒,内心可能自我感觉良好,但用户能感觉得到或者在乎吗?不会。
非常耗资源的 3D 游戏或类似的东西,不在我定义的 “应用”范围内。 这种情况下原生应用当然是你的最好选择,因为混合应用想要变快会非常吃力。Canvas 的性能在最近几年已经改善了很多,但是在旧设备上运行耗资源的应用还是会很慢。
设计良好的混合应用,在现代 JavaScript UI 框架上运行,绝对够快。性能差不再是放弃混合方式的借口了。如果你最近尝试做一个混合应用,发现性能不够满意,那你应该认真重新评估一下你选择的设计模式,或者是用了大量像 jQuery 这样不需要的库。学习下现代的 DOM API,如果还没学的话。我们的应用的所有的目标设备都支持它。
混合方式有一些主要的优势:
- 你可以利用现有的 JS,HTML和CSS技术开发一套代码,可同时运行在 iOS 和 Android 上。这是混合应用最明显的优势,但也不能过分强调。在我的项目中,需要根据运行的平台来写分支代码的次数不超过十次(这只是为了解决一个 iOS 上的 bug,后面再详细说)。代码里的分支看起来像这样:
if (device.platform === "iOS") {
doRidiculousWorkaround();
} else {
doThingThatWorksAsExpected();
}
开发阶段在设备上查看变动几乎是瞬时的。整个开发过程中,我在自己的 Galaxy S7 Edge 上测试了所有代码改动。这个手机连上我的电脑,启用 USB 调试模式,运行 Rizer 的 debug 版本。在 Chrome 地址栏输入
chrome://inspect/#devices
,你会看到连接到你的电脑的设备上运行的所有 Chrome 标签页。点击列表里的 Rizer 页面,将会打开一个开发工具窗口,我就可以完整地分析 App 了,就像调试一个普通网站一样。这个功能加上构建系统里的 live reload 工具,我就可以在改动代码后几秒内在设备屏幕上看到更新后的结果了。棒呆。你可以把更新后的 JS,CSS,HTML,字体文件和图片直接推送给用户,无需重新编译 App 的二进制代码,也无需 App store 的审核。Rizer 使用了 微软的 CodePush 服务(目前免费)以及它的Cordova 插件,每次启动时用来检查更新,说实话没有它我都不知道该怎么做。或者, Ionic 的 Deploy 也可以用同样的方式使用,但是因为它不免费并且我没有用 Ionic,没理由去用它。
你可以选择自己最喜欢的 UI 框架和构建工具。 Rizer 运行在我自己的前端框架 Samson.js上(别去用这个,它没有文档,并且为了满足我个人的需求经常变化),但我本来也可以用 React,Vue 或者 Ionic/Angular 来开发。
“哇,Sam,那些优势听起来好赞!那有什么缺点吗?”
混合应用方式有一个主要的缺点:
- 作为开发者,我们仍然需要操心 iOS,也就是说我们不得不忍受苹果的那些狗屎。嘿,还押韵呢。
你可能会困惑。即使选择原生方式,我们仍然要操心 iOS,因此也要忍受苹果的那些狗屎。对吧?
还真是。
无论采用哪种方式,我们都无法避免苹果的这些狗屎:
Xcode
配置文件
需要一台 Mac 来编译 App
需要使用 TestFlight 构建 beta 版本
App 审核流程 不可否认,app 审核时长已经缩短到平均 2 天,但是当你的 app 像 Rizer 一样被拒 7 次(因为一些愚蠢的理由),你的耐心真的会磨没了。
除了以上几点,混合应用还要处理这坨狗屎:
- iOS WebView
等等,什么?iOS WebView 真的有那么差劲吗?没错。
两种 WebView 的故事
混合应用里引入的 JavaScript 代码运行在 WebView 中,本质上它就是 app 里附带的原生浏览器。在 Android 上,我们的代码运行在基于 Chromium 的 WebView 里,大多数情况下会完全按照预期运行。在 iOS 上,我们可以选择让代码运行在两种 WebView 中: UIWebView 或 WKWebView.
如何选择这两种 WebViews 很伤脑筋.
UIWebView 在 iOS 2 中引入,当时是唯一的选择,直到 iOS 8 中加入了 WKWebView。UIWebView 与现有的大部分 Cordova 插件兼容性很好,但总体上太过臃肿、缓慢。两者之间 对 HTML 5 的支持情况差异很小, 但由于 WKWebView 使用了 Nitro JavaScript 引擎,性能差距非常大。
据 Nitishkumar Singh 所说, WKWebView 提供了:
极大减少内存占用,启动更快,JavaScript 即时编译,独立进程渲染,占用更少的存储空间,稳定性更好,更安全,采用最新 web 标准等等。
在 2014 年 iOS 8 发行之前,很多鼓吹 WKWebView 对混合应用的重要性的文章开始冒出来。能够利用 WKWebView 提供的以上优化特性,让混合应用的开发者们兴奋不已。
不幸的是,当 iOS 8 最终发版以后,WKWebView 完全不能兼容现有的混合应用,因为它缺少一些 必备的功能。整整 3 年之后,情况只是稍微改观了一点。
WKWebView 的现状
感谢 Ionic 团队的出色工作,一些自讨苦吃的开发者(包括我自己)开始在他们的混合应用里使用 WKWebView。我会第一个站出来说,当 WKWebView 不出问题的时候,的确非常好用。在我老婆的 iPhone 7 上,Rizer 冷启动并安装 CodePush 更新的速度比在我的 Galaxy S7 Edge 上快多了。但如果它出问题时会怎样?让我们继续探究。
那些需要奇技淫巧的变通方案或者只能干脆放弃功能的 WKWebView 问题:
不能访问应用根部
www
目录以外的文件各种跨域问题,包括不能从 canvas 里提取图片数据,而这个 canvas 是用本地图片画上去的。 这差点搅黄了 Rizer,因为我需要允许用户裁剪照片和加滤镜效果,两者都需要在 canvas 里操作图片,然后从中提取像素数据。我之所以说 差点搅黄,是因为实际上我在 iOS 上通过代码分支曲线救国,做了下面这种扯淡的事:
把从设备上获取的照片 fileURL 传给 FileReader API 的 "readAsArrayBuffer" 函数。
-
用下面的函数将 "readAsArrayBuffer" 函数返回的 arrayBuffer 转成 base64 字符串:
function arrayBufferToBase64(arrayBuffer) { var binary = ''; var bytes = new Uint8Array(arrayBuffer); var len = bytes.byteLength; for (var i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return window.btoa(binary); }
在返回的 base64 字符串前面加上 "data:image/jpeg;base64,",以创建 DOM 可读的 "imageDataURL"
-
创建一个 Image 对象,加载我们的 imageDataURL 作为它的 "src",当 image 加载完成时,创建一个新的 canvas 元素,把图片画上去。就像这样:
var image = new Image(); image.onload = function() { var canvas = document.createElement("canvas"); canvas.width = image.width; canvas.height = image.height; canvas.getContext("2d").drawImage(image, 0, 0); callback(canvas); // No CORS issues }; image.src = imageDataURL;
把你的电脑扔出窗外 :)
捉摸不定的垂直滚动行为,以及从我的经历来看完全不可用的水平滚动。有好几次,我老婆打开 Rizer 的排行榜页面后无法滚动照片,必须回到上一个页面并重新打开排行榜页面。这个问题如此严重,我到头来需要去实现 iScroll。 唉。
完全不支持 getUserMedia。这对 Rizer 来说不是问题,但是它阻止了我开始另一个需要这个特性的项目。都 2017 年了,这完全不可接受。也进一步证明了 Safari 成了新的 IE。
无法通过程序控制显示和隐藏键盘
跟稳定版的 Cordova SplashScreen 插件有导致 App 崩溃的冲突
跟稳定版的 Cordova In-App Browser 插件有导致 App 崩溃的冲突
你要知道,运行在 Android 上的混合应用不存在以上任何一个问题。
说真的,我对 Google 提供的应用开发部署流程没有任何不满。Google Play 开发者控制台非常不可思议,有健壮的发布管理,A/B 测试,应用内产品创建,订单管理,应用安装和使用统计,用户反馈收集,并且也没有荒唐的审核流程。锦上添花的是,我们知道混合应用会按照预期来渲染和运行,这要感谢 Android 基于 Chromium 的 WebView。 苹果在这点上落后太多了,以至于我一说起谷歌的产品就像个狂热粉丝。
毫无疑问,Rizer 的开发过程比我想象的更费时,也更令人沮丧……拜苹果所赐。
当我的家人问及 Rizer 的开发进度时,他们可以证明我对苹果和 iOS 有多深的怨念。我一直被这样一个事实打败: 世界上最有价值的公司提供了如此糟糕的开发体验。
我试图找出苹果为什么没有动力去改善混合应用开发的流程并让 Safari 像其他现代浏览器一样功能相当(它不像是没有资源去做这个),或者像其他平台(包括 Windows)一样直接允许使用第三方浏览器。难道他们是想让开发者在 Android 应用之前开发出 iOS 应用,然后 iPhone 或 iPad 用户就没那么想转向 Android 设备,因为他们每天使用的 App 在 Google Play 上没有? 我真心希望不是这样.
好吧,那……选择混合还是原生?
从我的经验来看,选择混合方式引起的所有潜在的卡壳问题,都有变通方案。发现和实现这些方案很麻烦,但的确能解决问题。因为这个原因,我很乐意说混合方式是你下一个 App 的正确选择。开发一个混合应用肯定会让你花费更长时间,但是依然比开发两个独立的原生应用要省时间,并且如果设计合理,对你的用户来说也会足够快。
嘿,或许将来某一天苹果迷途知返,会为我们提供我们应得的开发体验。在那天到来之前,我还是希望这个“混合与原生”之争继续火下去。
原文:If It Weren’t For Apple, Hybrid App Development Would Be The Clear Winner Over Native