BT

您是否属于早期采用者或者创新人士?InfoQ正在努力为您设计更多新功能。了解更多

豆瓣音乐人app的PhoneGap实践

| 作者 苏丹 关注 0 他的粉丝 发布于 2013年11月1日. 估计阅读时间: 9 分钟 | ArchSummit社交架构图谱:Facebook、Snapchat、Tumblr等背后的核心技术

豆瓣音乐人app在2011年开发时,便采用了基于原生与webapp混合架构的PhoneGap框架,直到今天。这也是目前豆瓣唯一一款使用PhoneGap的app。最近我们刚发布了音乐人app的ios新版,仍然保持这一架构,在原生方面的功能上做了一些增强。PhoneGap为音乐人app的顺利发布带来很大帮助,当然同时也造成了一些局限。

当时如何做出使用PhoneGap的决定

我们之所以在技术选型时作出这一选择,主要有以下几个方面的原因:

  1. 开发效率的考虑

    尽管豆瓣音乐人的用户对app有很强的需求,但音乐人app的定位和发展方向,当时处于不断探索和快速迭代的阶段,这样的情况意味着,音乐产品线希望使用尽可能简单的方式、占用较少的人力资源进行开发,尽快在多个平台发布,并且对于迭代的需求能够快速响应。在各种因素的权衡中,优先考虑满足上述需求。

    对于原生app好还是webapp好这个问题,似乎一直有很大争议,实际上我不认为这是一个纯粹的技术问题。webapp在开发效率上的优势,原生app在性能和开发自由度上的优势,都是不言自明的,一个app是否采用混合架构,在我看来,最重要的因素还是产品定位和发展策略,如果希望尽快发布、跨平台、能快速响应可以预见的迭代,那么混合架构就很值得考虑。如果有足够的开发人员覆盖各平台、产品设计成熟度高、产品周期上可接受相对较长的开发时间,那么原生显然是更好的选择。

  2. 跨平台的考虑

    豆瓣音乐人会是一个以展示内容和收听流媒体音乐为主的app,那些只有原生代码才可以实现的功能,我们需要得比较少。这意味着,如果我们采用混合架构,需要实现的原生特性与需要解决的跨平台问题会较少,混合架构的优势会被放大。

    即使考虑了第一个因素后认为值得使用混合架构,如果app本身的特性不适合webapp的方式,那也会显得没有这个必要。Webapp之所以开发效率高,一方面在于html+css+js能做的事情,比用原生代码做同样的事情要简单得多,另一方面在于方便跨平台。如果app里面要实现的功能,很多都没法用html做,必须用原生代码,那这两方面的优势都消失殆尽。

    实际上,就在我们第一个版本发布前不久,设计方面进行了一次review,然后对app的整体外观风格和某些功能与交互做了大幅修改。但其实只用了几天,设计的修改就被完全实现了,这样的速度对web前端开发来说当然不是什么难事,但对原生app来说却是难以想象的。

App架构与开发工作流

PhoneGap只是个原生外壳,app的内核是一个完整的webapp,需要调用的原生功能将以原生插件的形式实现,以暴露js接口的方式调用。在webapp框架的选择上,我调研了当时的一些专用于webapp的js框架,几乎都不大成熟,没什么合适的,当然现在的情况已经大不一样了。由于音乐人app的规模不算大,而且在移动设备的webview中性能非常重要,我决定把一些小工具组合成一套微型框架来使用,尽可能优化执行效率。框架大致由以下小零件组成:

  • jQuery;
  • iScroll4(模拟app风格的滚动);
  • js模板机制;
  • url分发与访问历史管理;
  • 页面关系与页面切换机制;
  • 基于Jsonp的带用户认证的api接口封装。

这样就简洁地实现了最小化的js框架。之所以使用jsonp的方式通信,是因为我非常希望能使用chrome进行调试,这样开发时就很方便,只需要双击本地的html文件,chrome就会成为一个完美的移动设备模拟器,我可以使用自己喜欢的任意前端开发调试工作流,这比任何移动设备模拟器都要方便得多。

有了框架,接下来只需要一个页面一个页面实现就好了,我把webapp部分作为git submodule,ios和android的仓库都包含它,打包时使用各自的编译发布流程即可。

PhoneGap开发中遇到的问题

大致说一下遇到的印象深刻的问题。其实PhoneGap现在的版本已经有很大改进,而且主流手机的性能已经比以前好太多,现在新开发PhoneGap的话,应该会轻松很多。

css3性能问题

我们开始的设计中,有一些半透明和投影等效果,但我发现用css3实现后,会导致性能不好,这跟原生开发时可能遇到的半透明性能问题是一样的。Webkit并不如我们想象的那样有保障。后来,我们为此修改了设计,尽可能使用不透明的元素,去掉投影等效果,并减少dom复杂度,性能得到了明显提升。

像素密度问题

对于不同像素密度的屏幕,需要准备不同的图片,然后在css里面使用媒体选择器,根据 –webkit-device-pixel-ratio,分别插入密度为0.75(老android手机),1(非retina iPhone),1.5(一些android手机),2(retina)的不同css,以使用不同的背景图片。只需要修改一个css文件,MakeFile脚本会自动生成其他的几个css文件。当然,我还需要保证这四套图片是存在并且正确命名的。

Mp3播放问题

iOS的webview支持mp3播放是没有问题的,但当时会有一个限制,就是如果用户没有主动操作,webview就不能自动开始播放,为它做一个workaround也就能够解决了。比较麻烦的是android的某些较老的版本,虽然支持audio标签,但是不支持mp3格式的音频,事实上它不支持任何格式的音频。所以对于这种情况,只能使用PhoneGap自带的音频播放功能。对用户而言,效果是一样的,但这增加了webapp的依赖,使webapp部分变得复杂了。

不同系统的行为差异

虽然大体上来说,iOS和android使用的webview,其行为都是差不多的,而且由于都是webkit,样式会非常接近,几乎是自动完美跨平台。但是实际开发中发现,还是会有一些区别,例如:

  • app ready时触发的事件不一样,当然这和PhoneGap封装有关;
  • android有时候会有软键盘问题,有个插件可以解决;
  • 处理打开外部url时行为不一致;
  • 支持的动画方式有区别,对不同平台需要尽可能使用高效、硬加速的动画方式。
  • 需要为android的几个实体按键写专门的处理函数。

仍然需要编写原生代码

有的需求在webapp内无法做到,这是经常遇到的事。例如:

  • 推送消息;
  • 状态栏提醒;
  • 打开内置浏览器访问一些url;
  • 绑定社交平台账号;
  • 缓存下载的图片和音乐等文件。

很多都可以找到插件来解决,现在市面上的插件比当时开发音乐人app最初版本时,已经丰富得多了,但仍然可能无法满足需求,这时就需要自己写插件来完成,每出现一个这样的需求,就意味着要为每个系统做一次原生解决方案。我们正准备把音乐人app中使用的“推荐到社交平台”的插件进行开源,提供给其他有类似需求的开发者使用。

可以移植到更多平台,但也需要一些工作

我简单试过WebOS,样式上会非常一致,但存在一些其他问题,为稳定起见,我们没有发布WebOS的打包。另外,移动版IE10已经是对标准非常友好的浏览器,但样式上跟webkit仍然有差异,不能把webkit的webapp直接拿来就用,需要做适配。感觉上又回到了桌面web开发的世界。

有趣的是,使用MacGap把app核心部分打包成Mac的桌面应用,倒是完全无痛,几乎直接就可以用。

总结

使用感觉上来说, iOS上的效果要略好于android上的效果,几乎跟原生界面没太大区别。虽然跟原生app相比,渲染速度等细节上仍然略微吃亏,但总的看来是完全可以接受的水平。

总结一下的话,我们对基于PhoneGap得到的成果是满意的,开发性价比很高。用户对音乐人app的评价也比较好。将来在产品稳定后,我们是否会使用纯原生app替代PhoneGap,现在还不知道,这将取决于产品未来的决策。希望得到什么,也就同时决定了会放弃什么,一切都是权衡的结果,框架没有银弹。

关于作者

苏丹,北京师范大学数学系毕业,现任豆瓣音乐Techleader。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

我们也在用PhoneGap by Gu Star

不知道作者能不能看到,请问在Android(某些版本,比如小米系列)下链接点击出现橙色边框这个问题怎么解决,查了很多E文,说是通过CSS,但很难控制!下了你们的APP,也存在这个问题。

Re: 我们也在用PhoneGap by 唐 雷

我们也存在这个问题,也是查了很多资料,无法解决

Re: 我们也在用PhoneGap by YU JIANRONG

我觉得这个应该从设计上避免。不应该存在链接。用其他元素结合window.open应该可以和链接差不多?

Re: 我们也在用PhoneGap by su dan

我是作者,这个问题我们之前没有专门去解决。回头我也想办法搞一下,能用css+js解决是最好,实在不行也可以用YU JIANRONG所说的办法,用其他元素替代链接,但这样在语义上会有所影响。

是什么插件 by Zheng Marion

“android有时候会有软键盘问题,有个插件可以解决”,请明示~

豆瓣音乐人.app iOS版存在的问题 by h fa

1.phonegap优化
可以把不用的plugin 裁掉,比如Capture.bundle的大小为2.3M,应该裁掉。
2.js混淆
发布产品之前,应该把js混淆了,否则你们的源码被hacker都看光了。
如果你们要开源,就把代码发github呗。
3.iOS问题的解决方案
iOS的webview自动播放mp3:webview的mediaPlaybackRequiresUserAction设为NO
缓存下载的图片和音乐等文件: 可以通过NSURLProtocol实现。


豆瓣音乐人.app $ tree
├── Capture.bundle
│   ├── controls_bg.png
│   ├── controls_bg@2x.png
│   ├── controls_bg@2x~ipad.png
│   ├── controls_bg~ipad.png
│   ├── microphone-568h@2x~iphone.png
│   ├── microphone.png
│   ├── microphone@2x.png
│   ├── microphone@2x~ipad.png
│   ├── microphone~ipad.png
│   ├── record_button.png
│   ├── record_button@2x.png
│   ├── record_button@2x~ipad.png
│   ├── record_button~ipad.png
│   ├── recording_bg.png
│   ├── recording_bg@2x.png
│   ├── recording_bg@2x~ipad.png
│   ├── recording_bg~ipad.png
│   ├── stop_button.png
│   ├── stop_button@2x.png
│   ├── stop_button@2x~ipad.png
│   └── stop_button~ipad.png
├── Default-568h@2x~iphone.png
├── Default-Landscape@2x~ipad.png
├── Default-Landscape~ipad.png
├── Default-Portrait@2x~ipad.png
├── Default-Portrait~ipad.png
├── Default@2x~iphone.png
├── Default~iphone.png
├── Info.plist
├── MainViewController.nib
├── ResourceRules.plist
├── SC_Info
│   ├── ?\206?\223??\237??\220人.sinf
│   └── ?\206?\223??\237??\220人.supp
├── _CodeSignature
│   └── CodeResources
├── config.xml
├── en.lproj
│   ├── InfoPlist.strings
│   └── Localizable.strings
├── iVersion.bundle
│   ├── da.lproj
│   │   └── Localizable.strings
│   ├── de.lproj
│   │   └── Localizable.strings
│   ├── en.lproj
│   │   └── Localizable.strings
│   ├── es.lproj
│   │   └── Localizable.strings
│   ├── fr.lproj
│   │   └── Localizable.strings
│   ├── it.lproj
│   │   └── Localizable.strings
│   ├── ja.lproj
│   │   └── Localizable.strings
│   ├── pt-PT.lproj
│   │   └── Localizable.strings
│   ├── pt.lproj
│   │   └── Localizable.strings
│   ├── ru.lproj
│   │   └── Localizable.strings
│   └── zh-Hans.lproj
│   └── Localizable.strings
├── ic_share_copy.png
├── ic_share_copy@2x.png
├── ic_share_douban.png
├── ic_share_douban@2x.png
├── ic_share_email.png
├── ic_share_email@2x.png
├── ic_share_facebook.png
├── ic_share_facebook@2x.png
├── ic_share_info.png
├── ic_share_info@2x.png
├── ic_share_message.png
├── ic_share_message@2x.png
├── ic_share_qqwb.png
├── ic_share_qqwb@2x.png
├── ic_share_renren.png
├── ic_share_renren@2x.png
├── ic_share_sinawb.png
├── ic_share_sinawb@2x.png
├── ic_share_twitter.png
├── ic_share_twitter@2x.png
├── ic_share_weixin.png
├── ic_share_weixin@2x.png
├── icon-72.png
├── icon-72@2x.png
├── icon.png
├── icon@2x.png
├── navbar_cancel.png
├── navbar_cancel@2x.png
├── navbar_confirm.png
├── navbar_confirm@2x.png
├── versions.plist
├── www
│   ├── Makefile
│   ├── README.md
│   ├── childbrowser
│   │   ├── icon_arrow_left.png
│   │   ├── icon_arrow_right.png
│   │   └── icon_close.png
│   ├── image
│   │   └── ios
│   │   ├── 240
│   │   │   ├── back.png
│   │   │   ├── back_hover.png
│   │   │   ├── cancel.png
│   │   │   ├── cancel_hover.png
│   │   │   ├── current.png
│   │   │   ├── forward.png
│   │   │   ├── icon.psd
│   │   │   ├── input.png
│   │   │   ├── like.png
│   │   │   ├── like_current.png
│   │   │   ├── like_hover.png
│   │   │   ├── like_s.png
│   │   │   ├── liked_s.png
│   │   │   ├── link.png
│   │   │   ├── logo_text.png
│   │   │   ├── play.png
│   │   │   ├── play_hover.png
│   │   │   ├── player.png
│   │   │   ├── player_current.png
│   │   │   ├── player_next.png
│   │   │   ├── player_next_hover.png
│   │   │   ├── player_pause.png
│   │   │   ├── player_pause_hover.png
│   │   │   ├── player_play.png
│   │   │   ├── player_play_hover.png
│   │   │   ├── playing.png
│   │   │   ├── playing_current.png
│   │   │   ├── playing_hover.png
│   │   │   ├── search.png
│   │   │   ├── search_current.png
│   │   │   ├── search_hover.png
│   │   │   ├── search_s.png
│   │   │   ├── selected.png
│   │   │   ├── setting_arrow.png
│   │   │   ├── share.png
│   │   │   ├── submit.png
│   │   │   ├── toplist.png
│   │   │   ├── toplist_current.png
│   │   │   ├── toplist_hover.png
│   │   │   └── unselected.png
│   │   ├── 320
│   │   │   ├── back.png
│   │   │   ├── back_hover.png
│   │   │   ├── cancel.png
│   │   │   ├── cancel_hover.png
│   │   │   ├── current.png
│   │   │   ├── forward.png
│   │   │   ├── icon.psd
│   │   │   ├── input.png
│   │   │   ├── like.png
│   │   │   ├── like_current.png
│   │   │   ├── like_hover.png
│   │   │   ├── like_s.png
│   │   │   ├── liked_s.png
│   │   │   ├── link.png
│   │   │   ├── logo_text.png
│   │   │   ├── play.png
│   │   │   ├── play_hover.png
│   │   │   ├── player.png
│   │   │   ├── player_current.png
│   │   │   ├── player_next.png
│   │   │   ├── player_next_hover.png
│   │   │   ├── player_pause.png
│   │   │   ├── player_pause_hover.png
│   │   │   ├── player_play.png
│   │   │   ├── player_play_hover.png
│   │   │   ├── playing.png
│   │   │   ├── playing_current.png
│   │   │   ├── playing_hover.png
│   │   │   ├── search.png
│   │   │   ├── search_current.png
│   │   │   ├── search_hover.png
│   │   │   ├── search_s.png
│   │   │   ├── selected.png
│   │   │   ├── setting_arrow.png
│   │   │   ├── share.png
│   │   │   ├── submit.png
│   │   │   ├── submit_active.png
│   │   │   ├── toplist.png
│   │   │   ├── toplist_current.png
│   │   │   ├── toplist_hover.png
│   │   │   └── unselected.png
│   │   ├── 480
│   │   │   ├── back.png
│   │   │   ├── back_hover.png
│   │   │   ├── cancel.png
│   │   │   ├── cancel_hover.png
│   │   │   ├── current.png
│   │   │   ├── forward.png
│   │   │   ├── icon.psd
│   │   │   ├── input.png
│   │   │   ├── like.png
│   │   │   ├── like_current.png
│   │   │   ├── like_hover.png
│   │   │   ├── like_s.png
│   │   │   ├── liked_s.png
│   │   │   ├── link.png
│   │   │   ├── logo_text.png
│   │   │   ├── play.png
│   │   │   ├── play_hover.png
│   │   │   ├── player.png
│   │   │   ├── player_current.png
│   │   │   ├── player_next.png
│   │   │   ├── player_next_hover.png
│   │   │   ├── player_pause.png
│   │   │   ├── player_pause_hover.png
│   │   │   ├── player_play.png
│   │   │   ├── player_play_hover.png
│   │   │   ├── playing.png
│   │   │   ├── playing_current.png
│   │   │   ├── playing_hover.png
│   │   │   ├── search.png
│   │   │   ├── search_current.png
│   │   │   ├── search_hover.png
│   │   │   ├── search_s.png
│   │   │   ├── selected.png
│   │   │   ├── setting_arrow.png
│   │   │   ├── share.png
│   │   │   ├── submit.png
│   │   │   ├── toplist.png
│   │   │   ├── toplist_current.png
│   │   │   ├── toplist_hover.png
│   │   │   └── unselected.png
│   │   └── 640
│   │   ├── app_icons.jpg
│   │   ├── appicon.png
│   │   ├── back.png
│   │   ├── back_hover.png
│   │   ├── cancel.png
│   │   ├── cancel_hover.png
│   │   ├── current.png
│   │   ├── forward.png
│   │   ├── icon.psd
│   │   ├── input.png
│   │   ├── like.png
│   │   ├── like_current.png
│   │   ├── like_hover.png
│   │   ├── like_s.png
│   │   ├── liked_s.png
│   │   ├── link.png
│   │   ├── logo_text.png
│   │   ├── play.png
│   │   ├── play_hover.png
│   │   ├── player.png
│   │   ├── player_current.png
│   │   ├── player_next.png
│   │   ├── player_next_hover.png
│   │   ├── player_pause.png
│   │   ├── player_pause_hover.png
│   │   ├── player_play.png
│   │   ├── player_play_hover.png
│   │   ├── playing.png
│   │   ├── playing_current.png
│   │   ├── playing_hover.png
│   │   ├── search.png
│   │   ├── search_current.png
│   │   ├── search_hover.png
│   │   ├── search_s.png
│   │   ├── selected.png
│   │   ├── setting_arrow.png
│   │   ├── share.png
│   │   ├── submit.png
│   │   ├── submit_active.png
│   │   ├── toplist.png
│   │   ├── toplist1.png
│   │   ├── toplist_current.png
│   │   ├── toplist_hover.png
│   │   └── unselected.png
│   ├── index.html
│   ├── lib
│   │   ├── android
│   │   │   └── phonegap-1.1.0.js
│   │   └── ios
│   │   └── cordova-2.7.0.js
│   └── static
│   ├── Feedback.js
│   ├── PushNotification.js
│   ├── SocialSharing.js
│   ├── android.css
│   ├── childbrowser.js
│   ├── handlers.js
│   ├── icon240.css
│   ├── icon480.css
│   ├── iphone.css
│   ├── iscroll.js
│   ├── jquery-1.6.1.min.js
│   ├── jquery-2.0.1.min.js
│   ├── jquery.ba-hashchange.js
│   ├── loading.gif
│   ├── make.py
│   ├── phonegap.js
│   ├── player_phonegap.js
│   ├── retina.css
│   ├── softkeyboard.js
│   ├── statusbarnotification.js
│   ├── t.mp3
│   └── utils.js
├── zh.lproj
│   ├── InfoPlist.strings
│   └── Localizable.strings
├── zh_CN.lproj
│   └── InfoPlist.strings

Re: 我们也在用PhoneGap by Gu Star

android有时候会有软键盘问题,有个插件可以解决;————请问用的什么插件。

Re: 我们也在用PhoneGap by su dan

Re: 是什么插件 by su dan

Re: 豆瓣音乐人.app iOS版存在的问题 by su dan

谢谢你的建议。我们已经开源了iOS分享库:github.com/douban/DOUSNSSharing

Re: 是什么插件 by Gu Star

IOS 键盘用的原生的吗?

Re: 是什么插件 by su dan

对,ios没有针对键盘做任何事情。

联系方式 by Gu Star

你好,想问一下您的email是多少,可以进一步交流一下。目前我们的android版已经上线,马上要上iOS的,但听说AppStore对PhoneGAP开发的应用审核要求比较严格,有需要要注意的吗?

请教一个问题: ios上播放MP3时UI freeze by wang davi

您好,我在用phonegap本身的media api做一个MP3播放的 ios app, 当播放非本地的MP3比如abc.com/123.mp3 时发现 UI 会卡住几秒, 想请教一下你们是怎么解决这个问题的 多谢

Re: 请教一个问题: ios上播放MP3时UI freeze by h fa

那是因为播放网络音频文件时,先同步下载文件。建议直接使用 html5 的audio标签

请教个问题 by 夏 世超

我在使用同样的方式开发一个ios的应用,phonegap打包安装后,点击input输入框,弹出的软键盘上显示“done”,而不是“完成”。但是如果直接通过浏览器访问,显示的就是“完成”。请问这个问题如何解决。

谢谢

请教个问题 by 夏 世超

我在使用同样的方式开发一个ios的应用,phonegap打包安装后,点击input输入框,弹出的软键盘上显示“done”,而不是“完成”。但是如果直接通过浏览器访问,显示的就是“完成”。请问这个问题如何解决。

谢谢

Re: 我们也在用PhoneGap by Wang Joe

想请问下,用html5的audio标签来播放, 怎样做可以让APP最小化到后台, 音频仍然继续播放。

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

18 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT