利用C#开发移动跨平台Hybrid App(一):从Native端聊Hybrid
0x00 前言
前一段时间,我阅读了叶小钗兄的《浅谈Hybrid技术的设计与实现》以及徐磊哥的《从技术经理的角度算一算,如何可以多快好省的做个app》,从中获得了很多启发,并形成了自己的一些见解。
目前,iOS、Android和Windows Phone这三大主流移动平台(尽管Windows Phone的市场份额相较于iOS和Android而言非常小)分别采用Objective - C(或Swift)、Java和C#作为开发语言。仅仅开发语言的差异,就给开发者带来了不小的挑战。当一个App需要同时在多个平台上线时,如何快速开发跨平台App并尽可能实现代码复用,成为开发者必须面对的问题。
正如叶小钗兄在博文中提到的:“使用iOS和Android原生开发一个App成本较高,而H5具有低成本、高效率、跨平台等特性,于是基于Web与Native结合的Hybrid APP开发模式应运而生。”在正式介绍使用C#开发跨平台App之前,我们有必要先从iOS(使用Objective - C)和Android(使用Java)这两个平台的角度来探讨Hybrid的实现。
本文中部分Web端代码来自叶小钗兄分享的“简单Hybrid框的实现”代码,链接为:https://github.com/yexiaochai/Hybrid 。 iOS端Hybrid的代码可在以下链接查看:https://github.com/chenjd/iOS - Hybrid - Sample
0x01 你好,WebView
Hybrid开发的Web端本质上是网络页面,在App中使用相关功能时,实际上是通过原生平台访问网页,从而变相实现了跨平台。对于iOS和Android平台而言,需要在Native端提供一个用于访问页面的容器,即WebView。
iOS的UIWebView
在iOS平台,我们使用UIKit.framework中的UIWebView来访问页面。借助UIWebView控件,iOS既可以获取网络资源,也能加载本地的HTML代码。主要使用的方法如下:
// UIWebView控件中加载资源的方法
- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)encodingName baseURL:(NSURL *)baseURL;
- (void)loadHTMLString:(NSString*)string baseURL:(NSURL*)baseURL;
- (void)loadRequest:(NSURLRequest*)request;
下面我们利用这些方法,在iOS平台使用UIWebView控件访问叶小钗兄提供的Web页面:
// ViewController.h
@interface ViewController : UIViewController
{
UIWebView *webView;
}
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
webView = [[UIWebView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// load yexiaochai's html
NSBundle *thisBundle = [NSBundle mainBundle];
NSString *path = [thisBundle pathForResource:@"webapp/hotel/index" ofType:@"html"];
NSURL *baseURL = [NSURL fileURLWithPath:path];
NSURLRequest *urlReq = [NSURLRequest requestWithURL:baseURL];
[self.view addSubview: webView];
[webView loadRequest:urlReq];
}
在iOS模拟器中运行上述代码,我们可以看到Native端通过UIWebView成功加载出Web端的内容。
不过,此时我们仅仅完成了在Native端渲染Web端内容,尚未涉及Native和Web之间的交互。在实现交互方面,UIWebView有两个重要方法:
// UIWebView控件中涉及Native和Web交互的方法
- (NSString*)stringByEvaluatingJavaScriptFromString:(NSString *)script;
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType;
此外,UIWebView还有一些用于控制页面前进后退的属性和方法:
// UIWebView控件中关于页面前进后退的属性和方法
@property (nonatomic, readonly) BOOL canGoBack;
@property (nonatomic, readonly) BOOL canGoForward;
- (void)goBack;
- (void)goForward;
Android的WebView
在Android平台,我们使用android.webkit包下的WebView来访问页面。和iOS类似,Android通过WebView获取网络资源或加载本地HTML代码。需要注意的是,为了让Activity能够正常连接网络,在使用WebView加载远程资源时,必须在AndroidManifest文件中开启INTERNET权限:
<uses-permission android:name="android.permission.INTERNET" />
WebView加载资源的方法主要有:
// WebView加载资源的方法
public void loadUrl(String url);
public void loadUrl(String url, Map<String, String> additionalHttpHeaders);
public void loadData(String data, String mimeType, String encoding);
public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl);
下面我们利用这些方法,在Android平台使用WebView控件访问叶小钗兄提供的Web页面。首先,在Native端添加WebView控件,可通过在layout文件中添加<WebView>元素实现:
<?xml version="1.0" encoding="utf-8"?>
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/webview"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
当然,我们也可以直接通过代码在Activity中设置WebView:
WebView webview = new WebView(this);
setContentView(webview);
为了便于说明,后续假设我们在layout中添加了<WebView>元素。接下来加载HTML资源:
WebView myWebView = (WebView)findViewById(R.id.webview);
myWebView.loadUrl("file:///android_asset/webapp/hotel/index.html");
此时,我们在Native端成功渲染出叶小钗兄的Web页面。同样,目前仅完成了页面渲染,尚未实现Native和Web之间的交互。与iOS不同的是,Android的WebView默认不支持JavaScript。若要使用JavaScript实现Native和Web的交互,需要获取WebView的WebSettings对象,并开启JavaScript支持:
WebView myWebView = (WebView)findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
开启JavaScript支持后,就可以实现Native和Web的交互。Android平台主要通过以下方法实现Native调用Web以及Web调用Native:
public void evaluateJavaScript(String script, ValueCallback<String> resultCallback);:在Native端调用JS代码,但存在Android版本兼容性问题,后续会详细说明。- 使用
JavascriptInterface.java接口完成Web调用Native的功能。
此外,Android的WebView也有控制页面前进后退的方法:
public boolean canGoBack();
public void goBack();
public boolean canGoForward();
public void goForward();
0x02 Native端调用JS
上一小节介绍了iOS和Android在Native端访问本地资源和远程资源的方法。在App开发中,将部分需要频繁更新的模块以Web形式创建,既能避免用户整包更新App,又能快速实现多平台的更新迭代。而这一切都依赖于Native和Web之间的交互,主要包括Native调用Web方法和Web调用Native方法。下面分别介绍iOS和Android调用Web方法的实现。
iOS Native调用JS
// ViewController.h
@interface ViewController : UIViewController
{
UIWebView *webView;
NSMutableString *msg;
UITextField *msgText;
UIButton *nativeCallJsBtn;
}
// ViewController.m
- (void)createNativeCallJsSample {
[webView loadHTMLString:@"<html><head><script language = 'JavaScript'>function msg(text){alert(text);}</script></head><body style=\"background-color: #0ff000; color: #FFFFFF; font-family: Helvetica; font-size: 10pt; width: 300px; word-wrap: break-word;\"><button type='button' onclick=\"msg('Js调用')\" style=\"margin:30 auto;width:100;height:25;\">web button</button></body></html>" baseURL:nil];
// 创建一个UITextField,用来在native调用js时向js的函数传送参数
[self createNativeTextField];
// 创建一个按钮,用来演示Native调用js
[self createNativeCallJsButton];
}
// native 调用 js
- (void)btnClickMsg {
[msg setString:@"msg('"];
[msg appendString:@"native调用js:"];
[msg appendString:[msgText text]];
[msg appendString:@"')"];
[webView stringByEvaluatingJavaScriptFromString:msg];
}
上述代码中,我们使用UIWebView的loadHTMLString方法加载一段包含JavaScript函数msg的HTML代码。在Native端,通过stringByEvaluatingJavaScriptFromString方法调用该JavaScript函数。
Android Native调用JS
Android平台调用JS的方式与iOS类似,但需要注意两个问题:一是Android的WebView默认不支持JavaScript,需要手动开启;二是存在Android版本兼容性问题。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
// 开启js支持
webSettings.setJavaScriptEnabled(true);
myWebView.loadData("<html><head><script language = 'JavaScript'>function msg(text){alert(text);}</script></head><body><button type='button' onclick= 'msg()'>web button</button></body></html>", "text/html", "utf-8");
evaluateJavascript(myWebView);
}
// Native调用JS
public static void evaluateJavascript(WebView mWebview) {
// 判断版本调用不同的方法
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mWebview.evaluateJavaScript("msg('native call js')", null);
} else {
mWebview.loadUrl("javascript:msg('native call js')");
}
}
在Android 4.4(KitKat)及以上版本,使用evaluateJavaScript方法调用JS代码;在4.4以下版本,使用loadUrl方法调用。
0x03 Web调用Native
Hybrid交互设计
前面已经介绍了iOS和Android的Native端调用JS代码的方法,本小节将探讨Web调用Native的实现。Web和Native交互的第一步是约定交互格式。本文采用叶小钗兄设计的Web请求Native的模型:
requestHybrid({
// 创建一个新的webview对话框窗口
tagname: 'hybridapi',
// 请求参数,会被Native使用
param: {},
// Native处理成功后回调前端的方法
callback: function (data) {
}
});
// index.html中的实际例子
requestHybrid({
tagname: 'forward',
param: {
topage: 'webapp/flight/index',
type: 'webview'
}
});
Native端会收到类似如下格式的URL:
hybrid://forward?t=1447949881120¶m=%7B%22topage%22%3A%22webapp%2Fflight%2Findex%22%2C%22type%22%3A%22webview%22%7D
这里使用的schema是hybrid://,Native端会监控WebView发出的所有schema://请求,并将其分发到“控制器”hybridapi处理程序,处理时会使用param提供的参数。下面分别介绍iOS和Android实现Web调用Native的方法。
iOS Web调用Native
在iOS平台,使用shouldStartLoadWithRequest方法捕获Web发出的请求:
// 按照和web端规定的格式,获取数据
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSURL * url = [request URL];
if ([[url scheme] isEqualToString:@"hybrid"]) {
NSString *actionType = request.URL.host;
// 从url中获取web传来的参数
NSDictionary *actionDict = [self getDicFromUrl : url];
// 根据web的指示,native端相应的做出回应
[self doActionType:actionType : actionDict];
return NO;
}
return YES;
}
// 从url中获取web传来的参数
- (NSDictionary *) getDicFromUrl : (NSURL *)url {
NSArray* paramArray = [[url query] componentsSeparatedByString:@"param="];
NSString* paramStr = paramArray[1];
NSString *jsonDictString = [paramStr stringByRemovingPercentEncoding];
NSData *jsonData = [jsonDictString dataUsingEncoding:NSUTF8StringEncoding];
NSError *e;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:jsonData options:nil error:&e];
return dict;
}
// 根据web的指示,native端相应的做出回应
-(void) doActionType : (NSString*) type : (NSDictionary*) dict {
if ([type isEqualToString:@"forward"]) {
[webView goForward];
}
// 打开一个新的Web
if([dict[@"type"] isEqualToString: @"webview"]) {
[self web2Web: dict[@"topage"]];
}
// 打开一个Native页面(我简化为了控件)
else if ([dict[@"type"] isEqualToString: @"native"]) {
[self web2Native];
}
}
上述代码实现了根据Web请求做出相应的Native端响应。点击Web页面中的按钮,可触发打开新的Web页面或创建Native UI控件的操作。
Android Web调用Native
Android平台实现Web调用Native相对复杂,需要借助JavaScriptInterface。以下是一个简单示例,通过Web调用Native的Dialog显示提示信息:
import android.webkit.JavascriptInterface;
import android.widget.Toast;
public class WebAppInterface {
Context mContext;
WebAppInterface(Context c) {
mContext = c;
}
@JavascriptInterface
public void showToast(String toast) {
Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
}
}
WebView webView = (WebView) findViewById(R.id.webview);
webView.addJavascriptInterface(new WebAppInterface(this), "Android");
<input type="button" value="Say hello" onClick="showAndroidToast('Hello Android!')" />
<script type="text/javascript">
function showAndroidToast(toast) {
Android.showToast(toast);
}
</script>
上述代码中,首先定义了一个WebAppInterface类,通过@JavascriptInterface注解将其方法暴露给JavaScript。然后使用addJavascriptInterface方法将该类与WebView绑定,最后在Web端调用该方法。
0x04 让Web的归Web,Native的归Native
至此,我们介绍了在iOS和Android平台使用Hybrid方式实现Web和Native交互的内容。但可以发现,Web和Native之间的界限仍然比较明显。
实际上,Hybrid的最大价值并非在于跨平台,而是在处理一些经常变动的模块的热更新问题上表现出色。它并没有从根本上解决跨平台问题,而是采用了一种较为巧妙的方式绕过平台差异。这也导致了Hybrid存在一些缺点,例如与Native体验相比仍有差距。因此,Hybrid是一种优秀的热更新技术,但并非完美的跨平台解决方案。
要解决跨平台问题,或许可以让Native端采用同一种开发语言甚至同一个IDE进行开发,再结合Hybrid方式实现热更新,这样可能会得到更好的效果。
未完待续……