现在,基本上,每个开发人员都知道React,一个来自Facebook的开源JavaScript库。
组件是 React 的主要组成部分。它们与浏览器 DOM 元素非常相似,但使用 JavaScript 而不是 HTML 创建。根据Facebook的说法,组件的使用允许开发人员只构建一次界面,然后在所有设备和平台上显示它。很显然,它是如何在浏览器中完成的——组件被转换成DOM元素——但是移动应用呢?答案很简单:反应组件转换为本机组件。
在下面的文章中,我想分享我的经验,如何开发一个简单的步骤计数器应用程序。我将展示代码及其主要功能。该项目可在比特桶上使用。
所以,让我们挖吧!
要求
我们需要 OS X 和 Xcode 进行 iOS 开发。使用 Android 时,有多种选项可用,例如 Linux、OS X、Windows。还需要 Android SDK。为了测试应用程序,我们将使用iPhone和Android智能手机与棒棒糖。
项目结构
首先,让我们构建我们的项目结构。要操作应用程序的数据,我们将使用 Flux 和 Redux。我们还需要一个路由器。我决定使用反应原生-路由器-flux-flux,因为它支持Redux下架。
关于Redux,我们需要了解什么?它是存储应用程序状态的简单库。可以通过添加事件处理程序(包括显示呈现)来修改状态。这些是几个基本事实,但您始终可以在 Web 上找到有关它的详细信息。
让我们通过安装具有 npm 的响应原生来开始构建步骤计数器应用程序。它将帮助我们操纵项目。
npm install -g react-native-cli
然后,创建项目:
react-native init AwesomeProject
并设置依赖项:
npm install
到目前为止,我们已经在项目的根文件中创建了两个文件夹 – iOS 和 Android。在那里,您将分别找到每个平台的本机文件。索引.ios.js和android.js文件是应用程序的入口点。
让我们安装库:
npm install —save react-native-router-flux redux redux-thunk react-redux lodash
并创建目录的结构:
app/
actions/
components/
containers/
constants/
reducers/
services/
函数将存储在操作文件夹中。他们将描述存储中的数据。
Components
将包括单独的接口元素的组件。
Containers
将包含应用每个页面的根组件。
Constants
是不言自明的。
Reducers
将包含根据传入数据修改应用状态的特定函数js 在应用/容器文件夹中。Redux 将充当应用程序的根元素。所有路由器都设置为普通组件。初始通知路由器,在初始化应用时应激活哪个路由。然后,将当前激活的组件呈现给路径的组件属性。
app/containers/app.js
<Provider store={store}>
<Router hideNavBar={true}>
<Route
name="launch"
component={Launch}
initial={true}
wrapRouter={true}
title="Launch"/>
<Route
name="counter"
component={CounterApp}
title="Counter App"/>
</Router>
</Provider>
在应用/容器目录中创建 launch.js。Launch.js是一个普通的组件,包含一个用于访问计数器页面的简单按钮。
app/containers/launch.js
import { Actions } from ‘react-native-router-flux';
…
<TouchableOpacity
onPress={Actions.counter}>
<Text>Counter</Text>
</TouchableOpacity>
Actions
是每个路由对应于给定方法的对象。从路由的名称中收集方法名称。
让我们描述应用/常量/actionType.js文件中可能的步骤计数器操作:
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
在应用/操作文件夹中创建计数器操作.js:
app/actions/counterActions.js
import * as types from '../constants/actionTypes';
export function increment() {
return {
type: types.INCREMENT
};
}
export function decrement() {
return {
type: types.DECREMENT
};
}
increment
函数, decrement
并将当前操作描述给减速器。根据接收的操作,缩减器将更改应用的状态。
initialState
描述存储的初始状态。在应用的初始化过程中,计数器将设置为 0。
app/reducers/counter.js
import * as types from '../constants/actionTypes';
const initialState = {
count: 0
};
export default function counter(state = initialState, action = {}) {
switch (action.type) {
case types.INCREMENT:
return {
...state,
count: state.count + 1
};
case types.DECREMENT:
return {
...state,
count: state.count - 1
};
default:
return state;
}
}
在计数器.js 文件中,您可以找到两个按钮 – 递增和递减计数器的值。此外,它显示计数器的当前值。
app/components/counter.js
const { counter, increment, decrement } = this.props;
…
<Text>{counter}</Text>
<TouchableOpacity onPress={increment} style={styles.button}>
<Text>up</Text>
</TouchableOpacity>
<TouchableOpacity onPress={decrement} style={styles.button}>
<Text>down</Text>
</TouchableOpacity>
事件处理程序和计数器的值从容器的组件呈现。让我们仔细看看下面。
app/containers/counterApp.js
import React, { Component } from 'react-native';
import {bindActionCreators} from 'redux';
import Counter from '
/组件/计数器’;
导入 = 作为来自 ../行动/反行动’;
从”反应-重做”导入 [ 连接];
类计数器应用程序扩展组件 |
构造函数(道具)|
超级(道具);
}
渲染() |
const = 状态,操作 = 此.
返回 (
<计数器
计数器{状态.count}
{…行动* />
);
}
}
/* 使组件使用操作修改存储的状态。props.state 显示计数器的当前状态 [/
导出默认连接(状态 > (*
状态:状态计数器
}),
/= 将操作添加到组件。访问操作以操作计数器道具.行动.递增() 和道具.actions.dere() [/ ]
(派单) > (*
操作:绑定操作创建者(反操作、调度)
})
(计数器应用程序);
因此,我们构建了一个使用主要组件的简单应用程序。它可以用作 ReactNative 上任何其他应用的基础应用。
图表
要显示步长计数器结果,创建一个简单的条形图是有意义的:Y = 显示步骤数;X = 显示时间。
现成的 ReactNative 不支持画布。要利用画布,我们还必须依靠 Webview。因此,我们只有两个选项可用:1) 为每个平台编写本机组件;1) 为每个平台编写本机组件。2) 使用一组标准组件。第一个选项非常耗时,但结果在可扩展性和灵活性方面通常更好。不过,我们将选择第二个选项。
为了显示数据,我们将将它们作为对象数组呈现给组件:
[
{
label, // data displayed on X
value, // value
color // bar color
}
]
创建三个文件:
app/components/chart.js
app/components/chart-item.js
app/components/chart-label.js
下面,您将找到我为创建关系图而编写的代码的较大部分:
app/components/chart.js
import ChartItem from './chart-item';
import ChartLabel from './chart-label';
class Chart extends Component {
constructor(props) {
super(props);
let data = props.data || [];
this.state = {
data: data,
maxValue: this.countMaxValue(data)
}
}
/* function to calculate the max value of the transmitted data .*/
countMaxValue(data) {
return data.reduce((prev, curn) => (curn.value >= prev) ? curn.value : prev, 0);
}
componentWillReceiveProps(newProps) {
let data = newProps.data || [];
this.setState({
data: data,
maxValue: this.countMaxValue(data)
});
}
/* function to render the array of bar components */
renderBars() {
return this.state.data.map((value, index) => (
<ChartItem
value={value.value}
color={value.color}
key={index}
barInterval={this.props.barInterval}
maxValue={this.state.maxValue}/>
));
}
/* function to render the array of components for bar labels */
renderLabels() {
return this.state.data.map((value, index) => (
<ChartLabel
label={value.label}
barInterval={this.props.barInterval}
key={index}
labelFontSize={this.props.labelFontSize}
labelColor={this.props.labelFontColor}/>
));
}
render() {
let labelStyles = {
fontSize: this.props.labelFontSize,
color: this.props.labelFontColor
};
return(
<View style={[styles.container, {backgroundColor: this.props.backgroundColor}]}>
<View style={styles.labelContainer}>
<Text style={labelStyles}>
{this.state.maxValue}
</Text>
</View>
<View style={styles
多边形容器,[边框颜色:此.props.borderColor]\gt;
[此.renderBars()]
</视图>
<视图样式_样式.项目标签容器\gt;
[此.渲染标签()]
</视图>
</视图>
</视图>
);
}
}
/* 验证传输的数据 */
图表.propType |
数据:proptype.arrayOf(反应.Proptype.shape(*
值:PropType.number,
标签:PropType.string,
颜色:PropType.string
[)),// 显示数据的数组
条形间隔:propType.number,// 条形之间的间隔
标签FontSize:PropType.number,/标签的字体大小
标签FontColor:PropTypes.string,// 标签的字体颜色
边框颜色:PropType.string,// 轴颜色
背景颜色:PropType.string // 图表的背景颜色
}
导出默认图表;
现在,让我们深入探讨条形图的组件:
app/components/chart-item.js
export default class ChartItem extends Component {
constructor(props) {
super(props);
this.state = {
/* use animation for bars, set initial values */
animatedTop: new Animated.Value(1000),
/* current to max value ratio */
value: props.value / props.maxValue
}
}
componentWillReceiveProps(nextProps) {
this.setState({
value: nextProps.value / nextProps.maxValue,
animatedTop: new Animated.Value(1000)
});
}
render() {
const { color, barInterval } = this.props;
/* animation is fired up when rendering */
Animated.timing(this.state.animatedTop, {toValue: 0, timing: 2000}).start();
return(
<View style={[styles.item, {marginHorizontal: barInterval}]}>
<Animated.View style={[styles.animatedElement, {top: this.state.animatedTop}]}>
<View style={{flex: 1 - this.state.value}} />
<View style={{flex: this.state.value, backgroundColor: color}}/>
</Animated.View>
</View>
);
}
}
const styles = StyleSheet.create({
item: {
flex: 1,
overflow: 'hidden',
width: 1,
alignItems: 'center'
},
animatedElement: {
flex: 1,
left: 0,
width: 50
}
});
标签的代码如下:
app/components/chart-label.js
export default ChartLabel = (props) => {
const { label, barInterval, labelFontSize, labelColor } = props;
return(
<View style={[{marginHorizontal: barInterval}, styles.label]}>
<View style={styles.labelWrapper}>
<Text style={[styles.labelText, {fontSize: labelFontSize, color: labelColor}]}>
{label}
</Text>
</View>
</View>
);
}
因此,我们使用一组标准组件创建了一个简单的直方图。
步进计数器
反应原生是一个相当新的项目。它允许我们使用一组标准工具来创建从服务器收集和显示特定数据的简单应用程序。然而,当涉及到在设备上生成数据时,我们必须在”本机”语言上构建独特的模块。
我们的目标是创建一个简单的步骤计数器应用程序。如果您不知道给定设备的目标 c、Java 和 API,这将是一项极其复杂的任务。这是可行的,虽然如果你真的致力于任务,并有足够的时间发展。
幸运的是,像阿帕奇科尔多瓦和AdobePhoneGap这样的项目很容易访问。市场并不新鲜,他们的社区已经开发了一系列不同的模块逻辑部分不变。你所需要的只是重写一座桥。
当涉及到 iOS 时,您可以依靠 API HealthKit 收集有关应用内操作的数据。苹果提供了详细的指南,所以使用它不会是个问题。例如,这些准则包括有关如何解决简单问题等的说明。这是完全另一个故事与Android。我们拥有一套传感器。Android 指南很清楚 API 19 支持步骤计数器功能。Android 是一个广泛的操作系统,安装在全球数百万台设备上,但品牌往往只安装在传感器上的标准设置,如计步器、步进计数器、光传感器、接近传感器等。因此,我们必须为 Android 4.4 设备、板上带有步进计数器的设备以及较旧的设备编写代码。它将使我们能够有效地收集和分析数据。
让实现开始。
注意:下面的代码很难是完美的,因为我没有太多的时间。此外,我以前从未使用这些编程语言。
Ios
创建两个数据文件:
ios/BHealthKit.h
#ifndef BHealthKit_h
#define BHealthKit_h
#import <Foundation/Foundation.h>
#import "RCTBridgeModule.h"
@import HealthKit;
@interface BHealthKit : NSObject <RCTBridgeModule>
@property (nonatomic) HKHealthStore* healthKitStore;
@end
#endif /* BHealthKit_h */
ios/BHealthKit.m
#import "BHealthKit.h"
#import "RCTConvert.h"
@implementation BHealthKit
RCT_EXPORT_MODULE();
- (NSDictionary *)constantsToExport
{
NSMutableDictionary *hkConstants = [NSMutableDictionary new];
NSMutableDictionary *hkQuantityTypes = [NSMutableDictionary new];
[hkQuantityTypes setValue:HKQuantityTypeIdentifierStepCount forKey:@"StepCount"];
[hkConstants setObject:hkQuantityTypes forKey:@"Type"];
return hkConstants;
}
/* method to ask for permission to get access to data from HealthKit */
RCT_EXPORT_METHOD(askForPermissionToReadTypes:(NSArray *)types callback:(RCTResponseSenderBlock)callback){
if(!self.healthKitStore){
self.healthKitStore = [[HKHealthStore alloc] init];
}
NSMutableSet* typesToRequest = [NSMutableSet new];
for (NSString* type in types) {
[typesToRequest addObject:[HKQuantityType quantityTypeForIdentifier:type]];
}
[self.healthKitStore requestAuthorizationToShareTypes:nil readTypes:typesToRequest completion:^(BOOL success, NSError *error) {
/* if everything is fine, we call up a callback with argument null that triggers the error */
if(success){
callback(@[[NSNull null]]);
return;
}
/* otherwise, send the error message to callback */
callback(@[[error localizedDescription]]);
}];
}
/* method to receive the step count for a given time period. We send the initial time as the first argument, final time as the second one and callback as the third.
*/
RCT_EXPORT_METHOD(getStepsData:(NSDate *)startDate endDate:(NSDate *)endDate cb:(RCTResponseSenderBlock)callback){
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
NSLocale *enUSPOSIXLocale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionStrictStartDate];
[dateFormatter setLocale:enUSPOSIXLocale];
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZZ"];
HKSampleQuery *stepsQuery = [[HKSampleQuery alloc]
initWithSampleType:[HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount]
predicate:predicate
limit:2000 sortDescriptors:nil resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
if(error){
/* if there is an error, send its description to callback */
callback(@[[error localizedDescription]]);
return;
}
NSMutableArray *data = [NSMutableArray new];
for (HKQuantitySample* sample in results) {
double count = [sample
示例类型.说明 Key:[“数据_类型”*;
[s setValue:[日期Formatter字符串从日期:sample.startDate] forKey:[“开始[日期]”;
[s setValue:[日期Formatter字符串从日期:sample.endDate] forKey:[结束]日期”;
[数据添加对象:s];
}
/* 如果一切正常,请调用回调;null 将是第一个参数,因为存在 ni 错误,数据数组将位于它之后。*/
回调([NSNull null],数据*);
}];
[自我.健康工具包存储执行查询:步骤查询];
};
@end
然后,您需要将这些文件添加到项目中。启动 Xcode,单击目录上的鼠标右键 -> 将文件添加到”项目名称”。在”功能”选项卡中启动运行状况工具包。然后,转到常规 > 链接框架和库,按”+”并添加 HealthKit.framework。
就是这样。本机部分已准备就绪。现在,我们需要从项目的 JS 部分导入数据。为此,让我们创建应用/服务/health.ios.js:
app/services/health.ios.js
/* Add and start up our module. BHealthKit contains two methods that we created in BHealthKit.m
*/
const {
BHealthKit
} = React.NativeModules;
let auth;
// function to request authorization rights
function requestAuth() {
return new Promise((resolve, reject) => {
BHealthKit.askForPermissionToReadTypes([BHealthKit.Type.StepCount], (err) => {
if (err) {
reject(err);
} else {
resolve(true);
}
});
});
}
// function to request data
function requestData() {
let date = new Date().getTime();
let before = new Date();
before.setDate(before.getDate() - 5);
/* as native module requests are rendered asynchronously, add and return a promise */
return new Promise((resolve, reject) => {
BHealthKit.getStepsData(before.getTime(), date, (err, data) => {
if (err) {
reject(err);
} else {
let result = {};
/* Rended the data to display it as we need */
for (let val in data) {
const date = new Date(data[val].start_date);
const day = date.getDate();
if (!result[day]) {
result[day] = {};
}
result[day]['steps'] = (result[day] && result[day]['steps'] > 0) ?
result[day]['steps'] + data[val].value :
data[val].value;
result[day]['date'] = date;
}
resolve(Object.values(result));
}
});
});
}
export default () => {
if (auth) {
return requestData();
} else {
return requestAuth().then(() => {
auth = true;
return requestData();
});
}
}
Android
Android代码非常笨重,所以我将只描述我做了什么。
Android SDK 不提供在给定时间段内接收数据的存储。它只允许我们实时接收数据。因此,我们必须依靠始终处于打开的服务和收集必要的数据。一方面,这种方法非常灵活。但是,例如,如果您安装了 20 个步进计数器,并且每个计数器将收集与其他计数器相同的数据,则没有任何意义。
让我们创建两个服务:对于板上带有步进计数器的设备,无需使用步进计数器。以下是以下文件:
- 安卓/应用程序/src/main/java/com/真棒项目/计表/步进服务
Java
描述当给定设备在android/app/src/main/java/com/真棒项目/计分机/StepCounterBootReceiver.java文件中激活时,将启动哪个服务。
绑定应用程序和反应在android/应用程序/src/主/java/com/真棒项目/RNPedometerModule.java和RNPedometer包.java文件。
让我们通过在android/app/src/main/AndroidManifest.xml中添加以下代码来访问使用传感器
<uses-feature
android:name="android.hardware.sensor.stepcounter"
android:required="true"/>
<uses-feature
android:name="android.hardware.sensor.stepdetector"
android:required=“true"/>
<uses-feature
android:name="android.hardware.sensor.accelerometer"
android:required="true" />
通知应用程序有关我们的服务,并设置接收器,这将激活服务时,智能手机打开。
<application>
…
<service android:name=".pedometer.StepCounterService"/>
<service android:name=".pedometer.StepCounterOldService" />
<receiver android:name=".pedometer.StepCounterBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
将模块添加到我们的应用程序中。应用打开后,服务将自动激活。
android/app/src/main/java/com/awesomeproject/MainActivity.java
…
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new RNPedometerPackage(this)
);
}
…
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
Boolean can = StepCounterOldService.deviceHasStepCounter(this.getPackageManager());
/* if the device has a step counter sensor on board, activate a service that uses it */
if (!can) {
startService(new Intent(this, StepCounterService.class));
} else {
/* otherwise, start up a service that uses the step counter*/
startService(new Intent(this, StepCounterOldService.class));
}
}
从 JS 部件接收数据。创建应用/服务/运行状况.android.js
const Pedometer = React.NativeModules.PedometerAndroid; file
export default () => {
/* create promise for request because the data is rendered asynchronously. */
return new Promise((resolve, reject) => {
Pedometer.getHistory((result) => {
try {
result = JSON.parse(result);
// Render the data to get necessary view
result = Object.keys(result).map((key) => {
let date = new Date(key);
date.setHours(0);
return {
steps: result[key].steps,
date: date
}
});
resolve(result);
} catch(err) {
reject(err);
};
}, (err) => {
reject(err);
});
});
}
因此,我们创建了两个文件,health.ios.js和health.android.js,从本机模块收集有关用户活动的数据。我们可以在应用的任何部分使用以下代码字符串:
import Health from ‘<path>health’;
ReactNative 将根据其前缀自动激活必要的文件。我们可以在 iOS 和 Android 平台上使用此功能。
要求求求:
我们开发了一个简单的步进计数器应用程序,并研究了其开发的一些关键
优势
- 如果您擅长 JavaScript,您可以轻松地开发一个全新的应用程序;
- 您可以在 iOS 和 Android 上使用相同的应用,但创建代码一次;
- 您可以利用 React 的多个即用型组件的强大功能,以满足您的大多数需求;
- 您可以成为一个活跃的社区的成员,开发大量的模块以惊人的速度。
缺点
- 有时,在不同的平台上可以以不同的方式呈现代码。它会导致错误和性能问题;
- 如果您有一个特定的目标,则仅使用标准模块就很难达到目标。最有可能的是,你需要自己构建它们;
- 操作速度可能更高。反应是真正令人印象深刻的,如果与PhoneGap和Cordova相比,但本地应用程序将更快都相同。
何时使用反应原生
如果需要开发一个基本应用,从服务器收集数据进行渲染,则决策非常明显。但是,如果您要创建一个设计出色的应用程序、令人印象深刻的可扩展性和性能,ReactNative 几乎不是您的选择。如果您知道无法使用标准模块解决问题,则情况也是如此。如果这是真的,您必须自己编写代码的大部分,因此将不同的模块和代码段安装在彼此上没有任何意义。
谢谢你的时间!