Blog | Phodal - A Growth Engineerhttp://www.phodal.com/blog/2019-07-12T15:34:15.188709+00:00Blog【架构拾集】混合框架 CLI 设计(草稿)2019-07-12T15:33:23+00:002019-07-12T15:34:15.188709+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/design-a-hybird-cli-based-on-cordova-cli/常见的通过 Node.js 来构建 Hybird 应用的命令有:
- build
- run
## 过程
1. 获取 Target 设备。通过执行 ``adb devices``,来返回设备列表。
2. 执行构建。
1. 进行构建前的依赖项检查。如 JDK、Android SDK、目标设备、Gradle
2. 清除旧的构建。执行 ``gradlew clean``,并通过 shell.js 删除构建目录。
3. 执行 ``gradlew build`` 进行构建。
3. 安装应用
1. 找到构建目录中的 adb 包。
2. 执行 ``adb install`` 来安装对应的包。
4. 启动应用的 Activity
1. 通过 ``elementtree`` 解析 AndroidManifest 来获取包名。
2. 执行 ``adb`` 命令来启动应用的 Activity。
## 代码分析
详见 Cordova CLI 源码WebView <-> React Native <-> Native 相互调用2017-08-16T14:56:00+00:002017-08-16T15:15:39.062742+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/cordova-webview-react-native-native-code-call-chain/在《[我们是如何将 Cordova 应用嵌入到 React Native 中](https://www.phodal.com/blog/cordova-webview-embeded-react-native-case-study/)》 一文中,我们简单地介绍了『React Native 重写 Cordova 插件:复杂插件的调用』步骤:
1. WebView 调用 RN 方法,并监听 React Native 返回的相应事件
2. React Native 接收到 WebView 的调用,调用原生代码,并监听原生代码返回的相应事件
3. 原生代码执行 React Native 调用的方法,并响应事件给 React Native
4. React Native 接收到原生代码的值,执行 injectJavaScript 注入代码到 WebView 里并执行
5. 注入的 JavaScript 执行代码,并发出相应的广播
6. WebView 调用的地方,接收到广播,执行相应的方法
上面的 4 和 5 可以是:
4.React Native 接收到原生代码的值,并返回给原生代码
5.接收到相应的值,并发出相应的广播
本文则详细讨论一下这个过程。
步骤1:WebView 调用 RN 方法,并监听 React Native 返回的相应事件
---
这里,我们和《[React Native + Cordova WebView 演进:Plugin 篇](https://www.phodal.com/blog/react-native-inside-cordova-webview-with-plugin/)》中一样,仍然以 DatePicker 为例。
首先,我们需要一个广播:当 React Native 返回值时,我们就发出一个广播,这样可以解耦合代码。下面的代码则监听相应的广播:
```
$rootScope.$on('Bridge.datePicker', function(event, data) {
// 更新时间
});
```
然后便是相应的 datePicker 的调用:
```
function datePicker(options) {
function handler(event) {
event.target.removeEventListener('message', handler);
var data = JSON.parse(event.data);
$rootScope.$broadcast('Bridge.datePicker', data);
}
window.document.addEventListener('message', handler);
window.postMessage(JSON.stringify({
action: 'DATE_PICKER',
payload: payload
}));
}
```
先监听从 React Native 发过来的内容,当接收到内容将数据以广播的形式发出。然后,再通过 PostMessage 告诉 React Naitve,我们需要在调用哪个 action,并传递相应的参数。
步骤2:React Native 接收到 WebView 的调用,调用原生代码,并监听原生代码返回的相应事件
---
在 WebView 的 onMessage 方法里,我们需要处理不同的 action:
```
onMessage = (evt, webView) => {
const event = JSON.parse(evt.nativeEvent.data);
const action = event.action;
const payload = event.payload
...
switch (action) {
case 'DATE_PICKER': {
return DatePickerHandler.showDatePicker(payload, webView);
}
}
...
}
```
然后根据传过来的 action 类型,调用相应的方法,如这里是 DatePickerHandler.showDatePicker,其 Android 部分代码如下所示:
```
const { action, year, month, day } = await DatePickerAndroid.open(options);
if (action !== DatePickerAndroid.dismissedAction) {
webView.postMessage(JSON.stringify({
type: 'DATE_PICKER',
success: true,
date,
}));
}
```
iOS 则有一些不同,iOS 没有非标签的组件,需要自己写。而且,由于 iOS 的 DatePicker 是异步的,因此我们需要通过事件的方式进行。如下是写完插件后的调用示例:
```
const RNNoTagDatepicker = NativeModules.RNNoTagDatepicker;
const DatePickerEvent = new NativeEventEmitter(NativeModules.RNNoTagDatepicker);
...
const showPicker = async (options) => {
RNNoTagDatepicker.show(options);
};
```
步骤3:原生代码执行 React Native 调用的方法,并响应事件给 React Native
---
如上,由于 iOS 的日期插件是异步的,并且它只能通过方法,而非组件的方式来唤醒 UI,故而需要 sendEventWithName 来返回值
```
RCT_EXPORT_METHOD(show:(NSDictionary *) options) {
dispatch_async(dispatch_get_main_queue(), ^{
}
}
#pragma mark - Actions
- (IBAction)doneAction:(id)sender {
dispatch_async(dispatch_get_main_queue(), ^{
NSTimeInterval seconds = [self.datePicker.date timeIntervalSince1970];
[self sendEventWithName:@"DATEPICKER_NATIVE_INVOKE" body: @{@"status": @"success", @"value": [NSString stringWithFormat:@"%f", seconds]}];
[self hide];
});
}
```
步骤4:React Native 接收到原生代码的值,并返回给原生代码
---
在这个例子里,由于在 WebView 以广播的方式解绑,因此可以直接返回值:
```
DatePickerEvent.addListener('DATEPICKER_NATIVE_INVOKE', (evt: Event) => {
...
webView.postMessage(JSON.stringify({
type: 'DATE_PICKER',
success: true,
date
}));
...
});
```
如果是要不断地发送数据,则需要在 RN 代码里执行:
```
let js = 'var event = new CustomEvent("' + action + '", {detail: ' + JSON.stringify(detail) + '});';
js += 'window.document.dispatchEvent(event);';
webView.injectJavaScript(js);
```
步骤5:接收到相应的值,并发出相应的广播
---
紧接着,就回到步骤一中的 handler:
```
function handler(event) {
event.target.removeEventListener('message', handler);
var data = JSON.parse(event.data);
$rootScope.$broadcast('Bridge.datePicker', data);
}
```
步骤6:6. WebView 调用的地方,接收到广播,执行相应的方法
---
最后,我们终于到了:
```
$rootScope.$on('Bridge.datePicker', function(event, data) {
// 更新时间
});
```
如此复杂的过程,也是。。。
我们是如何将 Cordova 应用嵌入到 React Native 中2017-08-16T14:33:11+00:002017-08-16T15:16:30.635545+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/cordova-webview-embeded-react-native-case-study/> 重写一个应用是一件简单的事,可是演进一个应用则是一件复杂的工作。
过去的一年多里,我在工作上的主要职责是:手机 APP 开发。日常主要是编写基于 Ionic 和 Angular 的混合应用,并想方设法地帮助客户将之与 React Native 相结合。在完成了嵌入 WebView 后,重写插件等一系列工作后,便想记录一下这个过程中遇到的坑。
平滑地演进
---
如我在开头所说,在有足够人力和物力的情况下,最好的方式就是在重写应用**。
一来,应用在其生命周期里,经过了不同的开发人员、不同的业务变更,必然有大量的遗留代码。尽管,我已经尽量去保证 90% 左右的单元测试覆盖率,但是仍然没有 100% 的把握(甚至 90% 都没有),来保证了解每一行代码。
二来,演进过程中,必然会遇到很多技术上的挑战,有相当多的部分是别人没有遇到过的。在这期间里,我遇到了一系列的技术问题,找到一些行业内有经验的开发者,却也发现都没有遇到相似的案件。多数的问题,诸如 iOS 上的知识,只能了解一下大概,细节下来都得自己去解决。
再让我们回到 Cordova 嵌入 React Native 应用的这个话题里。在这个项目的一半时间里,业务功能都是由我一个人编写的。再加上剩下的一半时间,有两个人同时在编写应用。那么总的项目所需要的人年就是 1.5,即一个人写 1.5 年才能写完应用。而在采用 React Native 的时候,离上线就有几个月,没有三四个人,是不可能完成重写的。因此,在方案上只有结合原有 Cordova 的 WebView 方式。
而结合的方式则有两种:
- React Native 与 Cordova 是两个不同的视图,使用时从 Cordova 跳转 React Native,再由 React Native 转回 Cordova。
- React Native 嵌入原有的 Cordova 的 WebView
简单的介绍一下这两种方案。
React Native 与 Cordova 结合的两种方案
---
### React Native 结合 Cordova
这种方案的主要优点在于:集成很方便,只需要集成两个 Activity 就好了,就几天的工作量。而其缺点主要有两部分:界面跳转的时候,会存在一定的等待时间,加载 React Native 导致的。从技术上来说,这个可以在后期解决,算不上是一个问题。还有一个缺点是,入口代码无法使用新的技术编写。假设下图是一个 Tabbar 的截图,它是用 WebView 编写的:
![Tarbar 示例](/static/media/uploads/tabbar-example.jpg)
这个时候,假设我们要去掉『探索』Tab 的内容,而改成一个新的页面。那么,我们仍然只能在旧的 WebView 上编写,或者跳转到相应的 React Native 页面上。前者导致了不好的开发体验,后者则会导致不好的用户体验。
除了此,还可以做的一件事,嵌入 Cordova 的 WebView。
### React Native 嵌入 Cordova WebView
在 React Native 中嵌入 Cordova WebView 并不是一件容易的事,对于我们而言,工作量大概是一两个月。因此,其显著的缺点是:**开发周期长**,插件带来的风险不可控。其优点是,我们的演进变得很轻松,我们可以获得一个类似于『微信小程序』的框架。
因为 WebView 是运行在 React Native 框架之下,我们可以随意地在页面上嵌入 Native 的元素。这一点与 Cordova WebView 和 React Native 之间相互跳转,有着明显的差异。如:
- 想添加新的 Tab,只需要自己做一个 Tabbar,然后便能做一个新的 Native 页面。
- 原先我们用 Cordova 调用摄像头时,界面超难定制,而使用 React Native 则便得很轻松
- 当我们在 WebView 里,可以轻松地调用任何原生组件,在体验上也不比原生应用差
因此,主要工作就变成了:**重写 Cordova 的插件**。实际上,大部分的 Cordova 插件重写起来,都相当的简单——因为都有相应的 React Native 插件,只需要做一些相应的数据传递即可。
接着,让我们来看看这个过程中,我们遇到的一些坑。
React Native 处理 WebView
---
在我使用 RN 开发 [Growth 3.0](https://github.com/phodal/growth) 的时候,就发现 React Native 的 WebView 是有一些明显的坑的。即在开发环境和生产环境,我们需要处理好 WebView 的路径问题。生产环境时,Android 需要将路径放到 ``file:///android_asset/`` 目录下:
```
let source;
if (__DEV__) {
source = require(`./www/index.html`);
} else {
source = Platform.OS === 'ios' ? require(`./www/index.html`) : { uri: `file:///android_asset/www/index.html` };
}
```
实际上,那一点也适用于 iOS,在 iOS 打包的时候,我们也需要将 WebView 的代码放置到相应的 ``assets`` 目录下。因此,便需要编写打包脚本:
```
rm -rf ios/assets/src/components/ui/www
mkdir -p ios/assets/src/components/ui/www
cp -a src/components/ui/www ios/assets/src/components/ui/
```
而在那之前,还有 WebView 的跨域问题。**在 Android 版里的 WebView 可以支持 allowUniversalAccessFromFileURLs**。而 iOS 则不行,要支持的方式便是通过原生代码去获取,但是这样一来调用链太长。
除此,还需要了解的是 WebView 的各种生命周期。在不同的过程中,赋予不同的业务逻辑:
```
onNavigationStateChange={this.onNavigationStateChange}
onMessage={this.onMessage}
onLoad={this.onLoad}
onLoadStart={this.onLoadStart}
```
因此,就整体上来说,在这一部分只剩下一部分小问题了。
React Native 重写 Cordova 插件:常规插件调用
---
开始之前,让我们再说说一下调用链的问题。过去我们在 Cordova 是调用原生代码,便是 WebView <-> Cordova 原生插件(PS:感兴趣读者可以阅读:[Cordova插件 / 混合应用插件开发: hello,world解析](https://www.phodal.com/blog/create-cordova-plugin-demo-hello-world/)。而现在则变成了 WebView <-> React Native <-> 原生插件,整一个链条里直接多了一个节点。在那一篇《[React Native + Cordova WebView 演进:Plugin 篇](https://www.phodal.com/blog/react-native-inside-cordova-webview-with-plugin/)》里,我们介绍了这个过程:
由 WebView 执行 postMessage,并监听相应的事件:
```
window.postMessage(JSON.stringify({
command: 'DATE_PICKER',
payload: options
}));
window.document.addEventListener('message', function (e) {
var data = JSON.parse(e.data);
if(data.command && data.command === 'DATE_PICKER' && data.success) {
$rootScope.$broadcast('Bridge.datePicker', data)
}
});
```
再由 React Native 去调用原生组件,并返回相应的值:
```
const { command, year, month, day } = await DatePickerAndroid.open(options);
const date = new Date(year, month, day);
webView.postMessage(JSON.stringify({
command: 'DATE_PICKER',
success: true,
date,
}));
```
而在复杂的系统里,则需要一些更复杂的手段。
React Native 重写 Cordova 插件:复杂插件调用
---
在那篇《[Ionic 与 Cordova 插件编写:基于事件与广播的机制](https://www.phodal.com/blog/use-cordova-ionic-build-chat-app-sendjavascript-to-ui-thread/)》中,我介绍了一下项目里,所需要的一个由 Native 发出事件的例子。这时,需要在原生代码里,发出相应的事件:
```
cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
String js = "var event = new CustomEvent('someAction'});window.document.dispatchEvent(event);";
webView.loadUrl("javascript:" + js);
}
});
```
在 Cordova里,只是 WebView 监听原生代码发出的事件。而在结合 React Native 的情况下,过程则变成这样的:
1. WebView 调用方法,并监听 React Native 返回的相应事件
2. React Native 接收到 WebView 的调用,调用原生代码,并监听原生代码返回的相应事件
3. 原生代码执行 React Native 调用的方法,并响应事件给 React Native
4. React Native 接收到原生代码的值,执行 injectJavaScript 注入代码到 WebView 里并执行
5. 注入的 JavaScript 执行代码,并发出相应的广播
6. WebView 调用的地方,接收到广播,执行相应的方法
(PS:详细的代码说明见:[React Native 重新封装 Cordova 插件笔记:插件编写与第三方 SDK 编译](https://www.phodal.com/blog/react-native-repackage-cordova-plugin-notes/) 》及《[WebView <-> React Native <-> Native 相互调用](https://www.phodal.com/blog/cordova-webview-react-native-native-code-call-chain/)》)
上面的代码变成了 React Native 里的:
```
let js = 'var event = new CustomEvent("' + action + '", {detail: ' + JSON.stringify(detail) + '});';
js += 'window.document.dispatchEvent(event);';
webView.injectJavaScript(js);
```
这真是一个相当复杂的过程,特别是我们的调试的时候,需要:
- 使用 XCode/Android Studio 打断点,查看相应的日志
- 使用 React Native Remote Debug 打下相应的日志
- 使用 Safari/Chrome 查看 WebView 的日志
- 使用 Charles 抓包,查看调用情况
React Native 跳转 WebView
---
由于框架设计的原因,从 WebView 里跳转到 React Native,已经不是什么问题。
```
window.postMessage(JSON.stringify({
action: 'BLABLA'
}));
```
而从 React Native 返回到 WebView 也不算是什么问题。只需要按下返回的时候,发出相应的事件:
```
window.postMessage(JSON.stringify({
action: 'GO_BACK'
}));
```
然后在 React Native 里调用相应的代码即可:
```
BackHandler.handleRNBack = () => {
Actions.pop();
};
```
处理 Tabbar
---
在上节里,我们提到了 Tabbar 的问题,而由于第三方封装的 TabBar 都会绑定 View,所以只能自己去实现。以下是一个简单的 Tabbar 示例:
```
<view 0="" 1,="" 49}}="" :="" ?="" isloadingvisible="" paddingbottom:="" style="{{flex:">
<webview ...=""></webview>
</view>
<view>
<tabbar>
<tabbar.item ...="" =="" onpress="{()"> {
// 页面跳转
}}
title='首页'>
</tabbar.item>
</tabbar>
</view>
```
只需要在相应的 onPress 方法里,绑定对应的 WebView 的路由页面处理即可。
React Native 重新封装 Cordova 插件笔记:插件编写与第三方 SDK 编译2017-08-16T11:22:05+00:002017-08-16T11:22:01.873253+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/react-native-repackage-cordova-plugin-notes/
最近刚完成项目上的几个 React Native 插件的编写,便想记录这其中遇到的坑——主要是封装第三方 SDK 的问题。记录的过程中,顺便也就写一下相关的插件编写了。因此,本文分成三部分:
- React Native 插件封装
- Native 主动向 React Native 发送数据
- React Native 编译问题
React Native 插件封装
---
先让我们从 React Native 端的 JS 封装代码看起。假设我们的 SDK 名字是 SDK,那么对应在 React Native 上的 sdk.js 代码如下所示:
```
import { NativeModules } from 'react-native';
module.exports = NativeModules.SDK;
```
实际上,在这里,我们应该列出所有的方法。但是,由于时间限制原因,就没有在这一一列出了。
### React Native 插件 Android 示例
对应的,我们在 Android 端,只需要在方法开发之前添加 ``@ReactMethod`` 注解,再调用 promise 方法即可:
```
@ReactMethod
public void isLogined(Promise promise) {
try {
promise.resolve(SDK.app().is_logined());
} catch (Exception e) {
promise.reject(e);
}
}
```
**React Native Android 插件带参数示例**
```
@ReactMethod
public void login(int userid, String signature, Promise promise) {
}
```
而在 iOS 上则稍微麻烦一些。
### React Native 插件 iOS 示例
RN 对于 iOS 和 Android 的封装方式,到底还是因为两个系统的不同,而有所差异。如下是头文件 ``sdk.h`` 的代码:
```
#import "RCTBridgeModule.h"
@interface SDK : NSObject <rctbridgemodule>
@end
```
相应的代码文件 ``sdk.m`` 则如下所示:
```
@implementation SDK
RCT_EXPORT_MODULE();
```
```objc
RCT_EXPORT_METHOD(isLogined:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
BOOL isLogin = [[SDKHelper sharedHelper] isLogin];
resolve(@{@"isLogined": @(isLogin)});
}
```
从代码上来看,iOS 的 promise 使用方式与 Android 还是有些差异的。
**React Native iOS 插件带参数示例**
```
RCT_EXPORT_METHOD(login:(int64_t)userID
password:(NSString *) password
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
[[SDKHelper sharedHelper] login:userID password:password];
resolve(@"true");
}
```
Native 主动向 React Native 发送数据
---
在我们的需求里,还会遇到向 React Native 发送数据的功能。这个时候需要用到 EventEmitter,用于监听来自原生代码的示例。如下是两个不同的平台监听的代码:
```
webviewOnLoad = () => {
if(Platform.OS === 'android') {
DeviceEventEmitter.addListener('SDK_EVENT', this.handleNativeInvoke);
} else {
var RCTSDK = new NativeEventEmitter(NativeModules.SDK);
RCTSDK.addListener('SDK_EVENT', this.handleNativeInvoke)
}
}
```
这个时候就需要继承 RCTEventEmitter,其头文件如下所示:
```
#import <react rctbridgemodule.h="">
#import <react rcteventemitter.h="">
@interface SDK : RCTEventEmitter <rctbridgemodule>
@end
```
代码文件则如下:
```
#import <react rctbridge.h="">
@implementation SDK
@synthesize bridge = _bridge;
- (NSArray<nsstring *=""> *) supportedEvents
{
return @[@"SDK_EVENT"];
}
RCT_EXPORT_MODULE();
- (void)pushEvent:(NSString *)formatString eventName:(NSString *)eventName JSONObject:(id)JSONObject {
NSData* JSONObjectData = [NSJSONSerialization dataWithJSONObject:[JSONObject copy]
options:NSJSONWritingPrettyPrinted
error:nil];
NSString* JSONObjectString = [[NSString alloc] initWithData:JSONObjectData
encoding:NSUTF8StringEncoding];
NSString* javascript = [NSString stringWithFormat:formatString, eventName, JSONObjectString];
dispatch_async(dispatch_get_main_queue(), ^{
[self sendEventWithName:@"SDK_EVENT" body: @{@"injectedJS": javascript}];
});
}
@end
```
我们需要使用如下的代码,以此来声明事件的类型:
```
- (NSArray<nsstring *=""> *) supportedEvents
{
return @[@"SDK_EVENT"];
}
```
随后使用下面的代码,来向 React Native 发送事件:
```
[self sendEventWithName:@"SDK_EVENT" body: @{@"injectedJS": javascript}];
```
React Native 编译问题
---
在这个过程中,还遇到编译的问题。
以下是编加编译的示例:
```
//:configuration = Debug
HEADER_SEARCH_PATHS = $(SRCROOT)/** $(SRCROOT)/node_modules/react-native/React $(SRCROOT)/../../../node_modules/react-native/React/** $(SRCROOT)/../react-native/React/**
//:configuration = Release
HEADER_SEARCH_PATHS = $(SRCROOT)/** $(SRCROOT)/node_modules/react-native/React $(SRCROOT)/../../../node_modules/react-native/React/** $(SRCROOT)/../react-native/React/**
//:completeSettings = some
HEADER_SEARCH_PATHS
```
还有:
```
//:configuration = Debug
FRAMEWORK_SEARCH_PATHS = $(SRCROOT)/../node_modules/react-native-xxx/ios/RNxxx/**
//:configuration = Release
FRAMEWORK_SEARCH_PATHS = $(SRCROOT)/../node_modules/react-native-xxx/ios/RNZxxx/**
//:completeSettings = some
FRAMEWORK_SEARCH_PATHS
```
除此,还需要在**项目**里手动添加库:
Link Binary with Libraries
添加: ``CrashReporter.framework, CoreTelephony.framework``
以及相应的编译选项:
```
//:configuration = Debug
OTHER_LDFLAGS = -ObjC -lsqlite3 -lz -liconv -lstdc++ -lc++ -lstdc++.6
//:configuration = Release
OTHER_LDFLAGS = -ObjC -lsqlite3 -lz -liconv -lstdc++ -lc++ -lstdc++.6
//:completeSettings = some
OTHER_LDFLAGS
```
</nsstring></nsstring></react></rctbridgemodule></react></react></rctbridgemodule>React Native + Cordova WebView 演进:Plugin 篇2017-05-24T04:30:42+00:002017-05-24T04:58:55.645916+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/react-native-inside-cordova-webview-with-plugin/最近,项目上正在打算使用 React Native 来重写/重构/演讲原来的应用。由于早先使用 Cordova + Ionic 的时候,项目的业务代码很长一段时间里,主要是由我一个编写的。与此同时,也不会分配充足的人力,用于重写现有的业务逻辑。
因此,作为一个咨询师,我提供了几个不同的重构方案,并建议客户使用 React Native + WebView 的形式来进行演进。即在 React Native 里使用 WebView 来嵌入原有的业务逻辑,新的业务逻辑则采用 React Native 进行。考虑到未来的一段时间内, 业务代码将会继续采用 WebView 编写,而技术代码则用 React Native 编写,这是比较理想的方案。
而作为演进的其中一个难点是:重写原有的 Cordova 插件。我们使用到了数量众多的 Cordova 插件,如 Toast、日期控件等等。这个时候,就需要借助于 React Native WebView 的通信来做这件事。
React Native WebView 通信
---
早期的 React Native WebView 并不能直接与 WebView 通信,而自 React Native 0.37 版本后,则提供了:
- onMessage
- postMessage
两个方法来与 WebView 进行交互,如下是一个简单的 DEMO:
```
...
class xxWebView extends React.Component {
webview: WebView
handleMessage = (evt: any) => {
// doSomething()
}
render() {
return (
<webview =="" ref="{webview"> this.webview = webview}
onMessage={this.handleMessage}
/>
)
}
}
```
在我们的 WebView 里,只需要执行下:
```
window.postMessage({ plugin: 'TOAST' })
```
就可以向插件发送信息。因此,对于完成我们的插件来说,只需要做到下面的步骤:
- 当需要调用原生插件的时候,在 WebView 里调用 window.postMessage 来传递,相应的**插件名 + 插件的参数**
- React Native 通过 onMessage 来处理对应的类型,并调用对应的插件
- 当需要返回结果时,通过 webview.postMessage 来传递参数,并带上相应的**插件名 + 返回结果**
- 在 WebView 端 ,如果想获取返回的结果,则需要 window.document.addEventListener 来监听 message 事件
- 最后,再根据返回的值来做相应的处理。
接着,让我们来看一个简单的日期控件的 DEMO。
Cordova WebView 调用 React Native 日期控件
---
### WebView
重写这段逻辑前,先让我们来看看原有的逻辑代码:
```
function onSuccess(date) {
// 更新时间
}
datePicker.show(options, onSuccess, null);
```
我们通过 options 来传递参数,而 onSuccess 则是成功的回调。不过,由于已经没有 Cordova 的机制,这里的 success 和 error 的回调就没有啥用了。
因此,在 WebView 上这段逻辑就变成了:
```
$rootScope.$on('Bridge.datePicker', function(event, data) {
// 更新时间
});
BridgeHelper.datePicker(options);
//BridgeHelper.js 中的相关代码
window.postMessage(JSON.stringify({
command: 'DATE_PICKER',
payload: options
}));
```
同时,我们有一个全局的监听函数,在这里面判断是否有对应的 command 类型。如果是我们需要的 DATE_PICKER,并且是成功地修改值,便会发出这样的一个广播,上面的代码就可以成功地更新时间。
```
window.document.addEventListener('message', function (e) {
var data = JSON.parse(e.data);
if(data.command && data.command === 'DATE_PICKER' && data.success) {
$rootScope.$broadcast('Bridge.datePicker', data)
}
});
```
这个原理与之前提到的 [Ionic 与 Cordova 插件编写:基于事件与广播的机制](https://www.phodal.com/blog/use-cordova-ionic-build-chat-app-sendjavascript-to-ui-thread/) 是相似的,通过全局事件来控制逻辑。
### React Native
在 React Native 端,则也是对相应的 handleMessage 进行处理,然后调用相应的组件来处理,如下是调用系统的控件:
```
DatePickerHandler.showDatePicker = (payload, webView) => {
const showPicker = async (options, webView) => {
try {
const { command, year, month, day } = await DatePickerAndroid.open(options);
if (command === DatePickerAndroid.dismissedcommand) {
//
} else {
const date = new Date(year, month, day);
webView.postMessage(JSON.stringify({
command: 'DATE_PICKER',
success: true,
date,
}));
}
} catch ({ code, message }) {
console.warn('Cannot open date picker', message);
}
};
showPicker(options, webView);
};
```
通过这样复杂的工作,我们就可以完成大部分的工作。</webview>Ionic 与 Cordova 插件编写:基于事件与广播的机制2017-03-19T07:57:19+00:002017-03-19T07:58:05.841304+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/use-cordova-ionic-build-chat-app-sendjavascript-to-ui-thread/最近的一个多月里,都在忙于为项目上的移动应用,添加一个聊天的功能。不是从头开始写聊天,而是基于第三方的 SDK。这个 SDK 里提供了基本的聊天接口,因此我们所要做的就是编写一个 Cordova 插件来实现聊天。
由于第三方 SDK 已经提供了大部分的功能,我们只需要封装好相应的 Cordova 插件,再调用相应的接口即可。
开始介绍之前,我们要先了解一下 Cordova 插件通讯。
Cordova 插件通讯
---
我假设读者已经有一点相关的 Cordova 插件的使用经验:
- 了解怎样添加 Cordova 插件
- 了解 Cordova 插件中的 plugin.xml 与 www 目录下相应的平台的关系
- 了解 www 目录的 js 与对应的原生平台代码的关系
如果你不知道上述的内容,可以参考这篇文章《[Cordova插件 / 混合应用插件开发: hello,world解析](https://www.phodal.com/blog/create-cordova-plugin-demo-hello-world/)》 进行详细的了解。
对于一个 Cordova 插件来说,返回数据可以有两种形式(sendJavascript已经不推荐使用):
- 使用 callbackContext 回调返回结果
- 在 UI 线程上创建事件来返回结果
### 使用 callbackContext 回调返回结果
大部分情况下,我们都使用回调来返回结果:
```java
callbackContext.sendPluginResult(new PluginResult(Status.OK, result));
```
如果需要一直返回值的话,我们还需要保持这个回调的状态:
```
pluginResult.setKeepCallback(true);
```
如,当我们想监听系统的电池状态时,我们可以注册一个 receiver:
```
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
this.receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateBatteryInfo(intent);
}
};
webView.getContext().registerReceiver(this.receiver, intentFilter);
```
一旦状态发生变化(Intent.ACTION_BATTERY_CHANGED)的时候,我们就触发这个事件:``updateBatteryInfo(intent);``。
随后,不断地向 WebView 发送对应的状态变化即可。在相应的映射 javascript 代码里,触发这个事件:``cordova.fireWindowEvent("batterystatus", info);``。
而后,在我们的前端,我们只需要监听相应的事件即可:
```
window.addEventListener("batterystatus", onBatteryStatus, false);
function onBatteryStatus(status) {
console.log("Level: " + status.level + " isPlugged: " + status.isPlugged);
}
```
除了这个方法,还可以采用的作法是:在前端直接执行 JavaScript 代码。
### 在 UI 线程上创建事件来返回结果
当我们只有一个地方调用方法来返回值的时候,保持长回调是一件很简单的事。而当我们有 2~3 处的地方需要同时监听这个状态改变的时候,它就不能适用于这种场景。
这个时候,我们就可以直接通过 UiThread 来执行 JavaScript 代码。而在这个代码里,我们实际上是触发一个 JavaScript 事件:
```
var event = document.createEvent('Events');
event.initEvent('messages.arrived', true, true);
event.messages={};
document.dispatchEvent(event);
```
对应的,我们只需要在 Java 代码里将 JavaScript 代码交给 WebView 去 load 就可以了:
```
cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
String js = "";
webView.loadUrl("javascript:" + js);
}
});
```
而后,我们就可以直接在监听事件,并执行相应的业务功能了。
Ionic 广播
---
为了在 Angular 里监听相应的事件,我做了一些麻烦的事——先由 Ionic 去监听事件,再通过 Angular 的广播来发送事件。这样做可以帮助我建立一层隔离。
在初始化代码里,可以使用 $window 来监听事件:
```
$window.addEventListener('LaunchUrl', function(event) {
})
```
也可以使用 Ionic 来监听事件:
```
$ionicPlatform.on('LaunchUrl', function(event) {
});
```
然后再通过 Angular 的广播来通知所有的组件:
```
$rootScope.$broadcast("LaunchUrl", LaunchUrl);
```
Cordova iOS 10 问题列表2016-11-15T03:49:14+00:002016-11-15T04:16:50.765456+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/ios-10-cordova-issues-list/1.APP 无法运行,停留在 SplashScreen
添加``gap://ready``到``CSP``中:
```html
<meta content="default-src * gap://ready file:; style-src 'self' 'unsafe-inline' *; script-src 'self' 'unsafe-inline' 'unsafe-eval' *" http-equiv="Content-Security-Policy"/>
```
2.自动构建需要添加``developmentTeam``,如下:
```javascript
{
"ios": {
"debug": {
"developmentTeam": "YOURTEAMID"
},
"release": {
"developmentTeam": "YOURTEAMID"
}
}
}
```
获取 Team ID的地址:[https://developer.apple.com/account/#/membership](https://developer.apple.com/account/#/membership)
3. 记得升级 Cordova iOS 的版本
4. 还有``deploy``相关的:
```
npm install -g ios-deploy ios-sim
```
Cordova插件 / 混合应用插件开发: hello,world解析2016-05-07T15:16:45+00:002016-05-07T15:17:26.201250+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/create-cordova-plugin-demo-hello-world/周末学了一下怎么样开始Cordova的插件,也顺便分享一下这一天的经验。
Cordova插件创建
---
在解释之前,我们先来创建一个插件。这里我们用用到一个名为``plugman``的工具,然后执行如下的命令:
```bash
plugman create --name cordovaCoap --plugin_id coap-cordova-plugin --plugin_version 0.0.1
```
然后就会生成如下的文件内容,即下面的两个文件:
```
.
├── plugin.xml
├── src
└── www
└── cordovaCoap.js
```
**plugin.xml**
先让我们来看看``plugin.xml``文件里有什么:
```xml
<?xml version='1.0' encoding='utf-8'?>
<plugin id="coap-cordova-plugin" version="0.0.1" xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
<name>cordovaCoap</name>
<js-module name="cordovaCoap" src="www/cordovaCoap.js">
<clobbers target="cordovaCoap"></clobbers>
</js-module>
</plugin>
```
它定义了几个内容:
- plugin -- 命名空间、ID、版本
- name -- 名称
- js-module -- js文件地址,会被默认加载到首页面(index.html)。clobbers元素定义的内容将会被插入到window对象中,即变成window.cordovaCoap。
使用创建的命令少了一些内容,我们在hello,world中详细展开。
**src**目录中定义了不同平台的代码,我们就会在稍后的内容中看到。
让我们来看看``www``目录下的``cordovaCoap.js``吧:
```javascript
var exec = require('cordova/exec');
exports.coolMethod = function(arg0, success, error) {
exec(success, error, "cordovaCoap", "coolMethod", [arg0]);
};
```
我们给上面的cordovaCoap创建了一个名为coolMethod的方法,然后cordova将调用对应平台的Native方法。
插件创建的内容,还是比较简单,让我们来看看一个完整的hello,world示例代码吧。
Cordova插件hello,world
---
在网上寻找到一个已经创建好的Hello,world包含了iOS、Android和Windows Phone下的代码,地址:[https://github.com/don/cordova-plugin-hello](https://github.com/don/cordova-plugin-hello)
目录如下所示:
```
├── README.md
├── plugin.xml
├── src
│ ├── android
│ │ └── Hello.java
│ ├── ios
│ │ ├── HWPHello.h
│ │ └── HWPHello.m
│ └── wp7
│ └── Hello.cs
└── www
└── hello.js
```
让我们再从``plugin.xml文件看起:
```xml
<?xml version="1.0" encoding="utf-8"?>
<plugin id="com.example.hello" version="0.7.0" xmlns="http://www.phonegap.com/ns/plugins/1.0">
<name>Hello</name>
<engines>
<engine name="cordova" version=">=3.4.0"></engine>
</engines>
<asset src="www/hello.js" target="js/hello.js"></asset>
<js-module name="hello" src="www/hello.js">
<clobbers target="hello"></clobbers>
</js-module>
<platform name="android">
<config-file parent="/*" target="res/xml/config.xml">
<feature name="Hello">
<param name="android-package" value="com.example.plugin.Hello"/>
</feature>
</config-file>
<source-file src="src/android/Hello.java" target-dir="src/com/example/plugin/"></source-file>
</platform>
<platform name="ios">
<config-file parent="/widget" target="config.xml">
<feature name="Hello">
<param name="ios-package" value="HWPHello"/>
</feature>
</config-file>
<header-file src="src/ios/HWPHello.h" target-dir="HelloPlugin"></header-file>
<source-file src="src/ios/HWPHello.m" target-dir="HelloPlugin"></source-file>
</platform>
<platform name="wp7">
<source-file src="src/wp7/Hello.cs"></source-file>
</platform>
</plugin>
```
在这个``xml``里多定义了下面的一些内容:
- engines定义了基于Cordova插件的Codova版本,也可以定义不同平台的版本,如`` <engine name="cordova-android" version=">=1.8.0"></engine>
``
- platform定义了iOS、android、wp7三个不同平台的代码
- 在不同的平台里,又有不同的定义。如Android定义了包名和路径、iOS定义了头文件和源文件也有包名。
在我们的``www``目录的``hello.js``的内容也是一样的:
```javascript
/*global cordova, module*/
module.exports = {
greet: function (name, successCallback, errorCallback) {
cordova.exec(successCallback, errorCallback, "Hello", "greet", [name]);
}
};
```
区别比较大的就是三个不同平台的Native代码。
Cordova iOS、Android、WP原生代码
---
在开始看代码之前,我们先看看应该怎么去调用这些代码:
```javascript
var success = function(message) {
alert(message);
}
var failure = function() {
alert("Error calling Hello Plugin");
}
hello.greet("World", success, failure);
```
我们创建了一个成功和失败的回调,并且直接调用了hello对象的greet方法,将"World"这个值传了进去。并且,这个hello是绑定在window对象中。
现在我们来看看Android平台下的Java代码吧:
```java
package com.example.plugin;
import org.apache.cordova.*;
import org.json.JSONArray;
import org.json.JSONException;
public class Hello extends CordovaPlugin {
@Override
public boolean execute(String action, JSONArray data, CallbackContext callbackContext) throws JSONException {
if (action.equals("greet")) {
String name = data.getString(0);
String message = "Hello, " + name;
callbackContext.success(message);
return true;
} else {
return false;
}
}
}
```
我们创建了一个Hello类,它继承了CordovaPlugin,然后Override了execute方法。接着从data中取出了第一个值,即"World",并且callbackContext.success返回了处理完后的结果——Hello,World。
同理,对于Windows Phone平台来说也是如此:
```java
using WP7CordovaClassLib.Cordova;
using WP7CordovaClassLib.Cordova.Commands;
using WP7CordovaClassLib.Cordova.JSON;
namespace Cordova.Extension.Commands
{
public class Hello : BaseCommand
{
public void greet(string args)
{
string name = JsonHelper.Deserialize<string>(args);
string message = "Hello, " + name;
PluginResult result = new PluginResult(PluginResult.Status.OK, message);
DispatchCommandResult(result);
}
}
}
```
对于iOS来说就稍微麻烦一些了,我们需要源文件和头文件,如下是头文件:
```objc
#import <cordova cdv.h="">
@interface HWPHello : CDVPlugin
- (void) greet:(CDVInvokedUrlCommand*)command;
@end
```
下面是源文件:
```objc
#import "HWPHello.h"
@implementation HWPHello
- (void)greet:(CDVInvokedUrlCommand*)command
{
NSString* callbackId = [command callbackId];
NSString* name = [[command arguments] objectAtIndex:0];
NSString* msg = [NSString stringWithFormat: @"Hello, %@", name];
CDVPluginResult* result = [CDVPluginResult
resultWithStatus:CDVCommandStatus_OK
messageAsString:msg];
[self success:result callbackId:callbackId];
}
@end
```
方法总的来说都是一样的。
Cordova插件要素
---
从上面的代码中我们可以发现,对于一个插件来说,我们需要下面的一些内容:
- plugin.xml 清单文件,定义了插件的结构以及相关的设置。
- JavaScript接口,用于插件与混合应用的接口。
- 原生代码,提供原生功能和接口。
相关参考资料:
- [Cordova Hello World Plugin](https://github.com/don/cordova-plugin-hello)
- [Plugin.xml Spec](https://cordova.apache.org/docs/en/latest/plugin_ref/spec.html)</cordova></string>Cordova / Ionic Android应用 Debug 2016-05-07T14:47:00+00:002016-05-07T15:49:15.254107+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/cordova-ionic-debug-android-application/
Cordova / Ionic Debug: JS
---
Ionic可以在设备上运行的时候热加载:
```
ionic run android -lc
```
Cordova / Ionic Debug: WebView
---
还有一种有效的对Android设备的WebView
```
adb logcat chromium:D SystemWebViewClient:D *:S
```
Cordova / Ionic Debug: 应用Debug
---
直接查看APP
```
adb logcat | grep phodal
```
Cordova插件 / 混合应用插件开发: 解决duplicate issue2016-05-07T14:36:43+00:002016-05-07T15:45:10.447525+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/cordova-plugin-remove-duplicate-license-issue/今天在开发插件的时候,引用了两个Eclipse的库:
```xml
<source-file src="src/android/lib/element-connector-1.0.4.jar" target-dir="libs"></source-file>
<source-file src="src/android/lib/californium-core-1.0.4.jar" target-dir="libs"></source-file>
```
而这两个库里他们使用了一些相同的文件,接着在编译时就报错了:
```
Duplicate files copied in APK META-INF/LICENSE
```
解决方法就是创建一个``build-extras.gradle``文件,从中删除重复的文件:
```groovy
android {
packagingOptions {
exclude 'about.html'
exclude 'edl-v10.html'
exclude 'epl-v10.html'
exclude 'notice.html'
exclude 'META-INF/ECLIPSE_.RSA'
exclude 'META-INF/ECLIPSE_.SF'
}
}
```
并将这个文件添加到plugin.xml中:
```xml
<framework custom="true" src="src/android/build-extras.gradle" type="gradleReference"></framework>
```
Cordova插件 / 混合应用插件开发: 使用Jar包2016-05-07T14:33:26+00:002016-05-07T15:35:52.471904+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/cordova-plugin-import-jar-lib/这是一篇笔记哈,记录怎么在Cordova上使用Java语言的Jar包。
首先,在Plugin中引入jar包,并且定义好``target-dir``
```
<source-file src="src/android/lib/element-connector-1.0.4.jar" target-dir="libs"></source-file>
<source-file src="src/android/lib/californium-core-1.0.4.jar" target-dir="libs"></source-file>
```
接着就可以在代码中使用了,如下是Cordova与CoAP协议的一个简单接口
```java
package com.phodal.plugin.coap;
import org.apache.cordova.*;
import org.json.JSONArray;
import org.json.JSONException;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse;
import java.net.URI;
import java.net.URISyntaxException;
public class Coap extends CordovaPlugin {
@Override
public boolean execute(String action, JSONArray data, CallbackContext callbackContext) throws JSONException, URISyntaxException {
if (action.equals("get")) {
try {
URI uri = new URI("coap://iot.eclipse.org:5683/");
CoapClient mCoapClient = new CoapClient(uri);
CoapResponse response = mCoapClient.get();
callbackContext.success(response);
return true;
} catch (Exception e) {
}
} else {
return false;
}
}
}
```
Ionic Auth与Django JWT API2015-09-05T13:00:47+00:002015-09-05T13:01:15.889143+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/ionic-cordova-django-jwt-example/Ionic Auth与Django JWT API
---
如上一章中说到的那样在给博客添加App Indexing的时候,顺手做了登陆的功能。
主要用到的便是:
1. Django REST Framework
2. Django REST Framework JWT
前端是Django的REST框架,后者则是用于JWT验证的。
##Django JWT服务端
1.安装djangorestframework-jwt
$ pip install djangorestframework-jwt
2.添加相应的配置到``settings.py``
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
}
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=14)
}
3.创建相应的API
@api_view(['POST', 'OPTIONS'])
@authentication_classes([JSONWebTokenAuthentication])
@permission_classes((IsAuthenticated,))
def create_blog(request):
serializer = BlogpostSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
主要是用于POST,出现OPTIONS的主要原因是用于网页端测试,主要不同便是``JSONWebTokenAuthentication``这个``authentication_classes``。
接着我们就可以测试这个API了。
###测试
1.首先我们要发送用户名和密码获取token
curl -X POST -H "Content-Type: application/json" -d '{"username":"phodal","password":"phodal"}' http://localhost:8000/api-token-auth/
如token可能是
eyJhbGciOiJIUzI3NiIsInR5cCI3IkpXVCJ9.eyJ1c2VybmFtZSI6InJvb3QiLCJ1c2VyX2lkIjoxLCJlbWFpbCI6ImhAcGhvZGFsLmNvbSIsImV4cCI6MTQ0MjQ1MzEyNH0.viYOMaSSt2mG7k9sxhiF4C3VrODz0m-fvglbayb7I7s
2.创建blog
curl -H "Accept: application/json" -H "Content-type: application/json" -H "Authorization: JWT eyJhbGciOiJIUzI3NiIsInR5cCI3IkpXVCJ9.eyJ1c2VybmFtZSI6InJvb3QiLCJ1c2VyX2lkIjoxLCJlbWFpbCI6ImhAcGhvZGFsLmNvbSIsImV4cCI6MTQ0MjQ1MzEyNH0.viYOMaSSt2mG7k9sxhiF4C3VrODz0m-fvglbayb7I7s" -X POST --data '{"content":"在设计","title":"物联网架构设计","user":1}' http://localhost:8000/api/app/blog/
如果上面的步骤正常的话,那说明是可以工作的,我们就可以继续进行下一步了。
##Ionic/Cordova 客户端
借助于Ionic默认Demo的AppCtrl,我们在我们的doLogin就可以有下面的代码:
var username = $scope.loginData.username;
var password = $scope.loginData.password;
$http({
method: 'POST',
url: 'https://www.phodal.com/api-token-auth/',
data: {
username: username,
password: password
},
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'User-Agent': 'phodal/2.0 (iOS 8.1, Android 4.4) secret=75456d89c8a654'
}
}).success(function (response) {
$scope.noLogin = false;
$localstorage.set('token', response.token);
$scope.closeLogin();
}).error(function (data, status) {
console.log('data, status', data, status)
})
将用户名和密码取出,用POST到服务端,再将token存到LocalStorage中。接着在我们的Blog创建方法里就会有:
$scope.create = function () {
var token = $localstorage.get('token');
var data = serialData($scope.posts);
$http({
method: 'POST',
url: 'https://www.phodal.com/api/app/blog/',
data: data,
headers: {
'Authorization': 'JWT ' + token,
'User-Agent': 'phodal/2.0 (iOS 8.1, Android 4.4) secret=75456d89c8a654'
}
}).success(function (response) {
if ($localstorage.get('draft')) {
$localstorage.remove('draft');
}
$scope.posts = {};
$state.go('app.blog-detail', {slug: response.slug});
}).error(function (rep, status) {
if (status === 401) {
alert(rep.detail);
}
alert(JSON.stringify(rep));
$localstorage.set('draft', JSON.stringify(data));
});
}
如果成功,且存在draft就会删除draft。如果失败,则会保存为draft。
##其他
博客代码: [PhodalDev](https://github.com/phodal/phodaldev)
博客App代码:[Phodal App](https://github.com/phodal/app)
Ionic/Cordova Google App Indexing2015-09-05T12:22:02+00:002017-10-28T03:25:26.127283+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/ionic-cordova-google-app-indexing-support/由于某个需求需要添加Google App Indexing,便给博客写了个App来尝试这个功能。功能大概就是: 在手机上的Google的搜索结果页直接可以打开App的相应页面。
![Ionic Google App Indexing Example](/static/media/uploads/6.pic.jpg)
步骤大概可以分成两部分,一是在网页端添加相应的内容,一个则是在客户端App添加相应的配置和步骤。
##Google App Indexing网页端
如官网([在网站上添加应用深层链接](https://developers.google.com/app-indexing/webmasters/server))所示,一共提供了三种方法:
1. 在该网页的 < head > 部分使用 < link > 元素。
2. 在用于指定该网页的站点地图 < url > 元素中使用 < xhtml:link> 元素。
3. 针对 ViewAction 这一可能会发生的操作使用 Schema.org 标记。
由于方法3比较麻烦,方法2对Sitemap造成伤害,只有方法一是比较好的。即在每一个页面添加相应对的alternate,如我的首页会变成
<link href="android-app://com.phodal.blog/https/www.phodal.com/" rel="alternate"/>
这个href由四部分组成
1. Android Scheme: 即android-app
2. App包名: 在这里即com.phodal.blog
3. 协议: 在这里是HTTPS
4. URL: 即www.phodal.com
对应于其他页面可能就是
<link href="android-app://com.phodal.blog/https/www.phodal.com/blog/nginx-194-with-http2-install/" rel="alternate"/>
因为用的是Django,所以比较简单的在base.html里添加了
<link href="android-app://com.phodal.blog/https/www.phodal.com{{ request.get_full_path }}" rel="alternate"/>
这样我们就完成了网页部分的内容
##Google App Indexing手机端
由于我们这里用的是Ionic + Cordova,所以这里用到了Cordova的一个插件,叫[Custom URL scheme](https://github.com/EddyVerbruggen/Custom-URL-scheme)
1.安装插件
$ cordova plugin add cordova-plugin-customurlscheme --variable URL_SCHEME=android-app
2.添加配置到``AndroidManifest.xml``
```
<intent-filter>
<data android:scheme="android-app"></data>
<action android:name="android.intent.action.VIEW"></action>
<category android:name="android.intent.category.DEFAULT"></category>
<category android:name="android.intent.category.BROWSABLE"></category>
</intent-filter>
```
3.添加相应的处理函数,即handleOpenURL,在我的情况下是:
function handleOpenURL(url) {
var event = new CustomEvent('AppIndexing', {detail: {'url': url}});
setTimeout(function () {
window.dispatchEvent(event);
}, 0);
}
在app.js里还会有一个run
.run(['$state', '$window',
function ($state, $window) {
$window.addEventListener('AppIndexing', function (e) {
var urlSlug = e.detail.url.split("/");
if (urlSlug[3] \&\& urlSlug[4] &\\& urlSlug[3] === 'blog') {
$state.go('app.blog-detail', {slug: urlSlug[4]});
}
});
}
])
即当访问的URL是blog的时候,且存在博客的URL时才会到相应的页面。
##然后
1. 发布我们的应用,
2. 在Developer Console中点击服务和API,可以关联网站
##测试
除了用Google,还可以直接用ADB测试是否工作
adb shell am start -a android.intent.action.VIEW -d "https://www.phodal.com/blog/internet-of-things-architecture-iot" com.phodal.blog
如果没有意外的话,应该会打开相应的页面。
##其他
博客代码: [PhodalDev](https://github.com/phodal/phodaldev)
博客App代码:[Phodal App](https://github.com/phodal/app)Ionic ngMessage 表单验证2015-09-05T12:05:50+00:002015-09-05T13:10:36.295828+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/ionic-ngmessages-form-validate/出于不想写复杂的表单验证,找到了ngMessages,简单、粗暴、有效——只需要修改HTML模板。
##Ionic ngMessages
1.安装
bower install angular-messages --save
2.添加到``Index.html``
<script src="lib/angular-messages/angular-messages.min.js"></script>
3.模板中操作
如Input
<label class="item item-input">
<span class="input-label">Title</span>
<input id="slug" name="title" ng-maxlength="70" ng-minlength="2" ng-model="posts.title" placeholder="Write here" required="" type="text"/>
</label>
限定了长度和必选,其实还应该有ng-messages,如:
<div ng-messages="myForm.myField.$error" role="alert">
<div ng-message="required">You did not enter a field</div>
<div ng-message="minlength, maxlength">
Your email must be between 5 and 100 characters long
</div>
</div>
但是,我觉得个人用的博客没必要这样搞,于是直接往下了
<button class="button button-block button-positive" ng-click="create(postsForm)" ng-disabled="postsForm.$invalid" type="submit">发送
</button>
如果表单无效,则发送按钮是不可点的。
说说Ionic 框架2015-07-18T15:06:25+00:002015-08-04T07:37:11.974485+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/something-about-ionic/已经用Ionic框架开发了第五个应用,虽然都是简单地Demo,但是稍微做个小的总结吧。
再让我们看一下简单
> Ionic是一个新的、可以使用HTML5构建混合移动应用的用户界面框架,它自称为是“本地与HTML5的结合”。该框架提供了很多基本的移动用户界面范例,例如像列表(lists)、标签页栏(tab bars)和触发开关(toggle switches)这样的简单条目。它还提供了更加复杂的可视化布局示例,例如在下面显示内容的滑出式菜单。
简单地来说,他基于AngularJS上封装了一层,这就意味着我们需要使用AngluarJS来开发。让我们来简单地过一下优缺点
##Ionic
优点:
- 提供基本的UI模式。这就意味着我们在开发的时候可以关焦点集中于Layout,而不是样式的设计。
- Angular.js。使用Angular.js在某种程度上更加便捷,动态绑定的特性非常实用。
- ngCordova。Ionic的开发团队实现上也开发了ngCordova,这个库封装了很多Phonegap/Cordova的插件,使代码看上去更Angular。
- Crosswalk。Crosswalk已经支持Ionic,这就意味着可以提升webview的性能,然而也带来体积大的问题。
缺点:
- Hybird应用自有的性能缺陷。
- 局限于Angluar语法。
- Cordova插件。当没有相应的Cordova插件,我们就需要自己编写。
- Windows Phone支持比较弱。
Ionic ngCordova 添加APP评价2015-04-04T08:02:36+00:002015-08-16T09:25:37.826586+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/ionic-framework-add-apprate-for-application/ngCordova有一个插件叫`` AppRate``,可以用于APP评分,先看我们让让最后的效果图。
![Ionic App Rate](/static/media/uploads/apprate.jpg)
##Ionic AppRate
1.安装Cordova插件
cordova plugin add https://github.com/pushandplay/cordova-plugin-apprate.git
2.配置
正常的配置如官方所示:
module.config(function ($cordovaAppRateProvider) {
document.addEventListener("deviceready", function () {
var prefs = {
language: 'en',
appName: 'MY APP',
iosURL: '<my_app_id>',
androidURL: 'market://details?id=<package_name>',
windowsURL: 'ms-windows-store:Review?name=<...>'
};
$cordovaAppRateProvider.setPreferences(prefs)
}, false);
});
而,我还想修改上面的文字,谁让我不喜欢官方的中文翻译呢?这时我们需要用到
$cordovaAppRateProvider.setPreferences(prefs);
于是,最后我们的配置如下所示
module.config(function ($cordovaAppRateProvider) {
document.addEventListener("deviceready", function () {
var prefs = {
language: 'zh-Hans',
appName: '教你设计物联网',
androidURL: 'market://details?id=com.phodal.designiot'
};
var strings = {
title: '动动手指,为我们打分',
message: '无论是来自亲的赞美诗,还是让亲唾沫横飞的槽点,我们只愿——让评价来得更猛烈些吧!',
cancelButtonLabel: '残忍地拒绝',
laterButtonLabel: '容我考虑考虑',
rateButtonLabel: '马上就去'
};
$cordovaAppRateProvider.setCustomLocale(strings);
$cordovaAppRateProvider.setPreferences(prefs);
}, false);
})
3.调用
一个简单的调用可以是
module.controller('MyCtrl', function($scope, $cordovaAppRate) {
document.addEventListener("deviceready", function () {
$cordovaAppRate.navigateToAppStore().then(function (result) {
// success
});
}, false);
});
也可以是和我一样的:
.controller('AccountCtrl', function ($scope, $cordovaAppRate) {
$scope.promptRating = function () {
$cordovaAppRate.promptForRating(true).then(function (result) {
console.log("result: " + result);
$scope.words = result;
}, function (error) {
$scope.words = result;
})
};
}
接着在页面上添加一个ng-click
<a class="button button-full" href="http://www.phodal.com/blog/tag/cordova/feeds/atom/" ng-click="promptRating()" ng-controller="AccountCtrl">
<i class="ion-ios-star-outline"></i>
评价
</a>
##其他
代码: [https://github.com/phodal/designiot-app](https://github.com/phodal/designiot-app)
</package_name></my_app_id>Ionic Google Admob集成2015-04-04T07:51:24+00:002015-04-04T08:03:03.625545+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/ionic-framework-build-hello-world-applilcation-add-google-admob/记录一下之前往《程序语言答人》中添加Google Admob的过程。
![Application](/static/media/uploads/icon168x168.png)
<a href="https://play.google.com/store/apps/details?id=com.ionicframework.learningionic860614">
<img alt="Get it on Google Play" src="https://developer.android.com/images/brand/zh-cn_generic_rgb_wo_60.png"/>
</a>
##Ionic Google Admob
1.添加Cordova插件
cordova plugin add com.rjfun.cordova.plugin.admob
2.配置
同样如: [Ionic Google Anlytics集成](http://www.phodal.com/blog/ionic-framework-build-hello-world-applilcation-add-google-analytics/)一样,我们需要将其添加到初始化代码中。
$ionicPlatform.ready(function() {
if (typeof analytics !== 'undefined'){
analytics.startTrackerWithId('UA-60917659-1');
analytics.trackView('Screen Title')
}
if(window.plugins && window.plugins.AdMob) {
var admob_key = device.platform == "Android" ? "ANDROID_PUBLISHER_KEY" : "IOS_PUBLISHER_KEY";
var admob = window.plugins.AdMob;
admob.createBannerView(
{
'publisherId': 'ca-app-pub-3662578183051823/5276849408',
'adSize': admob.AD_SIZE.BANNER,
'bannerAtTop': false
},
function() {
admob.requestAd(
{ 'isTesting': false },
function() {
admob.showAd(true);
},
function() { console.log('failed to request ad'); }
);
},
function() { console.log('failed to create banner view'); }
);
}
});
3.Run
几天后我们就可以看到效果了~~
![Google Admob][1]
##其他
代码: [https://github.com/phodal/learning-ionic](https://github.com/phodal/learning-ionic)
[1]: /static/media/uploads/google_admob.jpgIonic Google Analytics集成2015-04-04T07:39:48+00:002015-04-06T10:18:41.228593+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/ionic-framework-build-hello-world-applilcation-add-google-analytics/记录一下之前往《程序语言答人》中添加Google Anlytics的过程。
![Application](/static/media/uploads/icon168x168.png)
<a href="https://play.google.com/store/apps/details?id=com.ionicframework.learningionic860614">
<img alt="Get it on Google Play" src="https://developer.android.com/images/brand/zh-cn_generic_rgb_wo_60.png"/>
</a>
##Ionic Google Analytics
1.添加插件
cordova plugin add https://github.com/danwilson/google-analytics-plugin.git
2.添加初始代码
$ionicPlatform.ready(function() {
if (typeof analytics !== 'undefined'){
analytics.startTrackerWithId('UA-xxxxxx-x');
analytics.trackView('Screen Title')
}
});
``注意``: **typeof analytics !== 'undefined'**是很有必要的,对于国内这种大部分的人都用不了Google的App来说。
3.对应Controller添加trackView
如在``levelSelectCtrl``中添加
if (typeof analytics !== 'undefined') {
analytics.trackView("LevelSelectCtrl");
}
4.run
接着我们就可以在Google Analytics看到结果了~~
![ScreenShot][1]
##Ionic Google Analytics
详情可见: [http://ngcordova.com/docs/plugins/googleAnalytics/](http://ngcordova.com/docs/plugins/googleAnalytics/)
module.controller('MyCtrl', function($scope, $cordovaGoogleAnalytics) {
$cordovaGoogleAnalytics.debugMode();
$cordovaGoogleAnalytics.startTrackerWithId('UA-000000-01');
$cordovaGoogleAnalytics.setUserId('USER_ID');
$cordovaGoogleAnalytics.trackView('Home Screen');
$cordovaGoogleAnalytics.addCustomDimension('dimension1', 'Level 1');
$cordovaGoogleAnalytics.trackEvent('Videos', 'Video Load Time', 'Gone With the Wind', 100);
$cordovaGoogleAnalytics.addTransaction('1234', 'Acme Clothing', '11.99', '5', '1.29', 'EUR');
$cordovaGoogleAnalytics.addTransactionItem(
'1234', 'Fluffy Pink Bunnies', 'DD23444', 'Party Toys', '11.99', '1', 'GBP'
);
});
##其他
代码: [https://github.com/phodal/learning-ionic](https://github.com/phodal/learning-ionic)
[1]: /static/media/uploads/screen_shot_2015-04-04_at_15.48.55.jpgIonic Android应用开发《程序语言答人》——Router2015-04-04T07:25:57+00:002015-05-26T09:22:00.949576+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/ionic-framework-build-hello-world-applilcation-router/在上一篇[Ionic Android应用开发《程序语言答人》](http://www.phodal.com/blog/ionic-framework-build-hello-world-applilcation/)中简单地介绍了一下开始的流程,接着我们来说说Router。
##UI Router
> AngularUI Router is a routing framework for AngularJS, which allows you to organize the parts of your interface into a state machine. Unlike the $route service in the Angular ngRoute module, which is organized around URL routes, UI-Router is organized around states, which may optionally have routes, as well as other behavior, attached.
有这样的一个中文介绍
> AngularUI Router是AngularJS的路由框架,和默认的$route不同,它将所有路由包装成可划分层级的状态机状态,路由路径在ui-router中不是必须的。由于ui-router的路由状态机是分层级的,所以使用ui-router可以非常方便地创建包含多个嵌入的子模板。
一个简单的路由如下所示:
.state('app', {
url: "/app",
abstract: true,
templateUrl: "templates/menu.html"
})
即有URL、模板,而对于一个复杂的Ionic的Router来说是这样子的
.state('app.wiki', {
url: "/wiki",
views: {
'menuContent': {
templateUrl: "templates/wiki.html",
controller: 'WikiCtrl'
}
}
})
即带有Controller,一个完整的Router还会有一个默认的路由
$urlRouterProvider.otherwise('/app/level');
##Router路由
**/app/level**对应的路由是:
.state('app.levelSelect', {
url: "/level",
views: {
'menuContent': {
templateUrl: "templates/level.html",
controller: 'LevelSelectCtrl'
}
}
})
``level.html``的内容如下所示:
<ion-view view-title="选择级别">
<ion-content>
<ion-list>
<ion-item href="#/app/level/{{level.id}}" ng-repeat="level in levels">
<div ng-if="level.id == 1 ">
<i class="icon ion-ios-unlocked"></i> {{level.title}}
</div>
<div ng-if="level.id != 1 ">
<i class="icon ion-ios-locked"></i> {{level.title}}
</div>
</ion-item>
</ion-list>
</ion-content>
</ion-view>
而``LevelSelectCtrl``如下所示:
.controller('LevelSelectCtrl', function ($scope) {
if (typeof analytics !== 'undefined') {
analytics.trackView("LevelSelectCtrl");
}
$scope.levels = [
{title: 'Level 1', id: 1},
{title: 'Level 2', id: 2},
{title: 'Level 3', id: 3},
{title: 'Level 4', id: 4}
];
})
Controller做的事,似乎就是将数据放到模板,接着渲染~~,就会有如下的结果。
![ScreentShot][1]
当我们选择level1时,逻辑就来到了
.state('app.single', {
url: "/level/:level",
views: {
'menuContent': {
templateUrl: "templates/level_quiz.html",
controller: 'QuizCtrl'
}
}
})
这就是一个简单的页面间的跳转与Router的关系。
##其他
代码: [https://github.com/phodal/learning-ionic](https://github.com/phodal/learning-ionic)
[1]: /static/media/uploads/screen_shot_2015-04-04_at_15.35.48.pngIonic Android应用开发《程序语言答人》——Hello, Sidemenu2015-04-04T03:10:11+00:002015-04-04T07:38:48.816277+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/ionic-framework-build-hello-world-applilcation/用Ionic可以轻轻松松地在一周的业余时间内打造三个应用——至少我就是这么干的,不过也是为了学习下Cordova + AngularJS。来张应用的Logo,以及Google Play的下载地址,接着让我们开始吧。
![Application](/static/media/uploads/icon168x168.png)
<a href="https://play.google.com/store/apps/details?id=com.ionicframework.learningionic860614">
<img alt="Get it on Google Play" src="https://developer.android.com/images/brand/zh-cn_generic_rgb_wo_60.png"/>
</a>
##Hello,World
在之前的《[Ionic框架简介与Ionic Hello World](http://www.phodal.com/blog/ionic-development-android-ios-windows-phone-application/)》中我们介绍了如何用Ionic创建一个App。
接着:
1.创建一个side menu
$ ionic start myApp sidemenu
返回
Creating Ionic app in folder /Users/fdhuang/learing/myApp based on sidemenu project
Downloading: https://github.com/driftyco/ionic-app-base/archive/master.zip
[=============================] 100% 0.0s
Downloading: https://github.com/driftyco/ionic-starter-sidemenu/archive/master.zip
[=============================] 100% 0.0s
Update config.xml
Initializing cordova project
Fetching plugin "org.apache.cordova.device" via plugin registry
npm http GET http://registry.cordova.io/org.apache.cordova.device
npm http 304 http://registry.cordova.io/org.apache.cordova.device
Fetching plugin "org.apache.cordova.console" via plugin registry
npm http GET http://registry.cordova.io/org.apache.cordova.console
npm http 304 http://registry.cordova.io/org.apache.cordova.console
Fetching plugin "com.ionic.keyboard" via plugin registry
npm http GET http://registry.cordova.io/com.ionic.keyboard
npm http 304 http://registry.cordova.io/com.ionic.keyboard
Your Ionic project is ready to go! Some quick tips:
* cd into your project: $ cd myApp
* Setup this project to use Sass: ionic setup sass
* Develop in the browser with live reload: ionic serve
* Add a platform (ios or Android): ionic platform add ios [android]
Note: iOS development requires OS X currently
See the Android Platform Guide for full Android installation instructions:
https://cordova.apache.org/docs/en/edge/guide_platforms_android_index.md.html
* Build your app: ionic build <platform>
* Simulate your app: ionic emulate <platform>
* Run your app on a device: ionic run <platform>
* Package an app using Ionic package service: ionic package <mode> <platform>
For more help use ionic --help or visit the Ionic docs: http://ionicframework.com/docs
+---------------------------------------------------------+
+ New Ionic Updates for April 2015
+
+ The View App just landed. Preview your apps on any device
+ http://view.ionic.io
+
+ Invite anyone to preview and test your app
+ ionic share EMAIL
+
+ Generate splash screens and icons with ionic resource
+ http://ionicframework.com/blog/automating-icons-and-splash-screens/
+
2.编译sass
$ ionic setup sass
3.运行
$ionic serve
##基础漫游
在index.html中有一个
```html
<body ng-app="starter">
<ion-nav-view></ion-nav-view>
</body>
```
这个告诉了angular我们的app叫``starter``,接着我们来看看app.js,首先是一些插件的使用
.run(function($ionicPlatform) {
$ionicPlatform.ready(function() {
// Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
// for form inputs)
if (window.cordova && window.cordova.plugins.Keyboard) {
cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
}
if (window.StatusBar) {
// org.apache.cordova.statusbar required
StatusBar.styleDefault();
}
});
})
在后面,我们会添加一些插件,当页面载入时,就会执行这些函数,如`` window.cordova.plugins.Keyboard``,则会在有Keyboard插件的时候,执行
cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
接着在app.js里有
.config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('app', {
url: "/app",
abstract: true,
templateUrl: "templates/menu.html",
controller: 'AppCtrl'
})
.state('app.search', {
url: "/search",
views: {
'menuContent': {
templateUrl: "templates/search.html"
}
}
})
.state('app.browse', {
url: "/browse",
views: {
'menuContent': {
templateUrl: "templates/browse.html"
}
}
})
.state('app.playlists', {
url: "/playlists",
views: {
'menuContent': {
templateUrl: "templates/playlists.html",
controller: 'PlaylistsCtrl'
}
}
})
.state('app.single', {
url: "/playlists/:playlistId",
views: {
'menuContent': {
templateUrl: "templates/playlist.html",
controller: 'PlaylistCtrl'
}
}
});
// if none of the above states are matched, use this as the fallback
$urlRouterProvider.otherwise('/app/playlists');
即,对应的路由、Controller、html模板。Controller对应在controller.js中,一个简化完的Controller是这样的:
angular.module('starter.controllers', [])
.controller('AppCtrl', function($scope, $ionicModal, $timeout) {
})
##运行
接着我们就可以在手机或者电脑上demo了:
1.添加平台
ionic platform add android
2.运行
ionic run android
从现在起就可以自由自在的hack Ionic.
##其他
代码: [https://github.com/phodal/learning-ionic](https://github.com/phodal/learning-ionic)
</platform></mode></platform></platform></platform>