返回介绍

3.4 App 与 HTML5 的交互

发布于 2024-08-17 23:46:12 字数 8234 浏览 0 评论 0 收藏 0

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文