3.4 App 与 HTML5 的交互
App与HTML5的交互,是一个可以大做文章的话题。有的团队直接使用PhoneGap来实现交互的功能,而我则认为PhoneGap太重了。我们完全可以把这些交互操作在底层封装好,然后给开发人员使用。
为了开发人员方便,我们要准备一台测试用的PC服务器,在上面搭建一个IIS,这样可以快速搭建自己的Demo,对于App开发人员而言,不需要等待HTML5团队就可以自行开发并测试了。他们只需知道一些基本的Html和JavaScript语法,而相应的培训非常简单。
3.4.1 App操作HTML5页面的方法
为了演示方便,我在assets中内置了一个HTML5页面。现实中,这个HTML5页面是放在远程服务器上的。
首先要定好通信协议,也就是App要调用的HTML5页面中JavaScript的方法名称。
例如,App要调用HTML5页面的changeColor(color)方法,改变HTML5页面的背景颜色。
1)HTML5
<script type="text/javascript"> function changeColor (color) { document.body.style.backgroundColor = color; } </script>
2)Android
wvAds.getSettings().setJavaScriptEnabled(true); wvAds.loadUrl("file:// /android_asset/104.html"); btnShowAlert.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String color = "#00ee00"; wvAds.loadUrl("javascript: changeColor ('" + color + "');"); } });
3.4.2 HTML5页面操作App页面的方法
仍然是先定义通信协议,这次定义的是JavaScript要调用的Android中方法名称。
例如,点击HTML5的文字,回调Java中的callAndroidMethod方法:
1)HTML5
<a onclick="baobao.callAndroidMethod(100,100,'ccc',true)"> CallAndroidMethod</a>
2)Android
新创建一个JSInterface1类,包括callAndroidMethod方法的实现:
class JSInteface1 { public void callAndroidMethod(int a, float b, String c, boolean d) { if (d) { String strMessage = "-" + (a + 1) + "-" + (b + 1) + "-" + c + "-" + d; new AlertDialog.Builder(MainActivity.this) .setTitle("title") .setMessage(strMessage).show(); } } }
同时,需要注册baobao和JSInterface1的对应关系:
wvAds.addJavascriptInterface(new JSInteface1(), "baobao");
调试期间我发现对于小米3系统,要在方法前增加@JavascriptInterface,否则,就不能触发JavaScript方法。
3.4.3 App和HTML5之间定义跳转协议
根据上面的例子,运营团队就找到了在App中搞活动的解决方案。不必等到App每次发新版才能看到新的活动页面,而是每次做一个HTML5的活动页面,然后通过MobileAPI把这个HTML5页面的地址告诉App,由App加载这个HTML5页面即可。
在这个HTML5页面中,我们可以定义各种JavaScript点击事件,从而跳转回App的任意Native页面。
为此,HTML5团队需要事先和App团队约定好一个格式,例如:
gotoPersonCenter gotoMovieDetail:movieId=100 gotoNewsList:cityId=1&cityName=北京 gotoUrl:http://www.sina.com
这个协议具体在HTML5页面中是这样的,以gotoNewsList为例:
<a onclick="baobao.gotoAnyWhere( 'gotoNewsList:cityId=(int)12&cityName=北京')"> gotoAnyWhere</a>
其中,有些协议是不需要参数的,比如说gotoPersonCenter,也就是个人中心;有些则需要跳转到具体的电影详情页,我们需要知道movieId;有时候1个参数不够用,我们需要更多的参数,才能准确获取到我们想要的数据,比如说gotoNewsList,我们想要跳转到2014年12月31号北京的所有新闻信息,就不得不需要cityId和createdTime两个参数,处理协议的代码如下所示:
public void gotoAnyWhere(String url) { if (url != null) { if (url.startsWith("gotoMovieDetail:")) { String strMovieId = url.substring(24); int movieId = Integer.valueOf(strMovieId); Intent intent = new Intent(MainActivity.this, MovieDetailActivity.class); intent.putExtra("movieId", movieId); startActivity(intent); } else if (url.startsWith("gotoNewsList:")) { // as above } else if (url.startsWith("gotoPersonCenter")) { Intent intent = new Intent(MainActivity.this, PersonCenterActivity.class); startActivity(intent); } else if (url.startsWith("gotoUrl:")) { String strUrl = url.substring(8); wvAds.loadUrl(strUrl); } } }
这里的if分支逻辑太多,我们要想办法将其进行抽象,参见后面3.4.6节介绍的页面分发器。
3.4.4 在App中内置HTML5页面
什么时候在App中内置HTML5页面?根据我的经验,当有些UI不太容易在App中使用原生语言实现时,比如画一个奇形怪状的表格,这是HTML5所擅长的领域,只要调整好屏幕适配,就可以很好地应用在App中。
下面详细介绍如何在页面中显示一个表格,表格里的数据都是动态填充的。
1)首先定义两个HTML5文件,放在assets目录下。
其中,102.html是静态页:
<html> <head> </head> <body> <table> <data1DefinedByBaobao> </table> </body> </html>
而data1_template.html是一个数据模板,它负责提供表格中一行的样式:
<tr> <td> <name> </td> <td> <price> </td> </tr>
像<name>、<price>和<data1DefinedByBaobao>都是占位符,我们接下来会使用真实的数据来替换这些占位符。
2)在MovieDetailActivity中,通过遍历movieList这个集合,我们把数据填充到sbContent中,最终,把拼接好的字符串替换<data1DefinedByBaobao>标签:
String template = getFromAssets("data1_template.html"); StringBuilder sbContent = new StringBuilder(); ArrayList<MovieInfo> movieList = organizeMovieList(); for (MovieInfo movie : movieList) { String rowData; rowData = template.replace("<name>", movie.getName()); rowData = rowData.replace("<price>", movie.getPrice()); sbContent.append(rowData); } String realData = getFromAssets("102.html"); realData = realData.replace("<data1DefinedByBaobao>", sbContent.toString()); wvAds.loadData(realData, "text/html", "utf-8");
3.4.5 灵活切换Native和HTML5页面的策略
对于经常需要改动的页面,我们会把它做成HTML5页面,在App中以WebView的形式加载。这样就避免了Native页面每次修改,都要等一次迭代上线后才能看到——周期太长了,这不是产品经理所希望的。
此外,HTML5的另一个好处是,开发周期短——相比App开发而言。
但是HTML5的缺点是慢。我们来看一下HTML5页面生成的步骤:
1)从服务器端动态获取数据并拼接成一个HTML。
2)返回给客户端WebView。
3)在WebView中解析并生成这个HTML。
相对于Native原生页面加载JSON这种短小精悍的数据并展现在客户端而言,HTML5肯定是慢了很多。鱼和熊掌不可兼得,于是我们只能在灵活性和性能上作出取舍。
但是我们可以换一个思路来解决这个问题。我同时做两套页面,Native一套,HTML5一套,然后在App中设置一个变量,来判断该页面将显示Native还是HTML5的。
这个变量可以从MobileAPI获取,这样的话,正常情况下,是Native页面,如果有类似双十一或双十二的促销活动,我们可以修改这个变量,让页面以HTML5的形式展现。这样,我们只要做个HTML5的页面发布到线上就行了。等活动结束后再撤回到Native页面。
以此类推,App中所有的页面,都可以做成上述这种形式,为此,我们需要改变之前做App的思路,比如:
1)需要做一个后台,根据版本进行配置每个页面是使用Native页面还是HTML5页面。
2)在App启动的时候,从MobileAPI获取到每个页面是Native还是HTML5。
3)在App的代码层面,页面之间要实现松耦合。为此,我们要设计一个导航器Navigator,由它来控制该跳转到Native页面还是HTML5页面。最大的挑战是页面间参数传递,字典是一种比较好的形式,消除了不同页面对参数类型的不同要求。
接下来,就是App运营人员和产品经理随心所欲的进行配置了。
在实际的操作中,一定要注意,HTML5页面只是权宜之计,可以快速上一个活动,比如类似于双十一的节假日,从而以迅雷不及掩耳之势打击竞争对手。随着HTML5和Native的不同步,当一个页面再从HTML5切换回Native时,我们会发现,它们的逻辑已经差了很多了,切回来就会有很多bug,而我们又只能是在App发布后才发现这样的问题。
唯一的解决方案是,把App和HTML5划归到一个团队,由产品经理整理二者的差异性,要做到二者尽量同步,一言以蔽之,App要时刻追赶HTML5的逻辑,追赶上了就切换回Native。
3.4.6 页面分发器
我们知道,跳转到一个Activity,需要传递一些参数。这些参数的类型简单如int和String,复杂的则是列表数据或者可序列化的自定义实体。
但是,如果从HTML5页面跳转到Native页面,是不大可能传递复杂类型的实体的,只能传递简单类型。所以,并不是每个Native页面都可以替换为HTML5。
接下来要讨论的是,对于那些来自HTML5页面、传递简单类型的页面跳转请求,我们将其抽象为一个分发器,放到BaseActivity中。
还记得我们在3.4.3节定义的协议吗,以gotoMovieDetail为例:
<a onclick="baobao.gotoAnyWhere( 'gotoMovieDetail:movieId=12')"> gotoAnyWhere</a>
我们将其改写为:
<a onclick="baobao.gotoAnyWhere( 'com.example.youngheart.MovieDetailActivity, iOS.MovieDetailViewController:movieId=(int)123')"> gotoAnyWhere</a>
我们看到,协议的内容分成3段,第一段是Android要跳转到的Activity的名称。第二段是iOS要跳转到的ViewController的名称,第三段是需要传递的参数,以key-value的形式进行组装。
我们接下来要做的就是从协议URL中取出第1段,将其反射为一个Activity对象,取出第3段,将其解析为key-value的形式,然后从当前页面跳转到目标页面并配以正确的参数。其中,写一个辅助函数getAndroidPageName,用来获取Activity名称:
public class BaseActivity extends Activity { private String getAndroidPageName(String key) { String pageName = null; int pos = key.indexOf(","); if (pos == -1) { pageName = key; } else { pageName = key.substring(0, pos); } return pageName; } public void gotoAnyWhere(String url) { if (url == null) return; String pageName = getAndroidPageName(url); if (pageName == null || pageName.trim() == "") return; Intent intent = new Intent(); int pos = url.indexOf(":"); if (pos > 0) { String strParams = url.substring(pos); String[] pairs = strParams.split("&"); for (String strKeyAndValue : pairs) { String[] arr = strKeyAndValue.split("="); String key = arr[0]; String value = arr[1]; if (value.startsWith("(int)")) { intent.putExtra(key, Integer.valueOf(value.substring(5))); } else if (value.startsWith("(Double)")) { intent.putExtra(key, Double.valueOf(value.substring(8))); } else { intent.putExtra(key, value); } } } try { intent.setClass(this, Class.forName(pageName)); } catch (ClassNotFoundException e) { e.printStackTrace(); } startActivity(intent); } }
注意,在协议中定义这些简单数据类型的时候,String是不需要指定类型的,这是使用最广泛的类型。对于int、Double等简单类型,我们要在值前面加上类似(int)这样的约定,这样才能在解析时不出问题。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论