本文概述
几年前, 我的一个同事告诉我有关React Native的信息。我非常怀疑。我认为这只是另一个跨平台的框架, 在现实生活中永远无法使用-我几乎不知道自己是多么的错误。
多年过去了, React Native技能变得非常需求。既然有一段时间我学到了一些新知识, 所以我想为什么不试一试呢?今天, 我是React Native的拥护者。
缺点:
- 你不能再使用Android Studio
- 并非所有应用程序或功能都可以使用React Native
- React Native是一个新颖的框架, 更新可能会对你当前的代码库产生负面影响
- JavaScript不是严格类型的语言
- React Native需要运行JavaScript引擎, 这可能使其性能降低
优点:
- 简单易学
- Android和iOS应用程序之间的共享代码库, 仅需细微调整即可匹配平台体验
- 实时和热加载, 意味着不再需要无限的构建时间
- 两个平台的本机组件
- 不断完善
- 积极发展的社区
- 大量的图书馆
- 博览会消除了拥有Mac进行iOS开发的需要
- 减少人力资源—尽管你可能仍需要进行一些Android / iOS本机开发, 但这种情况很少见。
我可以继续下去, 但让我们在这里停止, 继续本博客文章的主题。在这篇文章中, 我将创建四个基于React Native的Android应用程序:
- 基本的计数器, 带有用于递增和递减计数器的按钮
- 搜寻r / pics subreddit的应用程式
- 通用登录页面
- 浏览r / pics subreddit的应用程序
HERE
如上所述, 我们无法将Android Studio用于React Native开发。我们需要替代品。 React Native可以使用任何可用的现代文本编辑器(Atom, VS Code, Sublime Text, Brackets等)进行开发, 但是由于我们具有Android Studio体验, 因此我最喜欢的是由同一家公司构建的WebStorm。尽管WebStorm是付费应用程序(每年129 $), 但是你可以安装它的Early Access版本。 WebStorm的EAP构建是免费的并且相当稳定。如果你喜欢完全免费的编辑器, 请使用VS Code。微软甚至为此开发了惊人的React Native插件, 并且效果很好。
创建一个新项目
先决条件:你的计算机上安装了Android SDK, Node和React Native。
有两种创建新的React Native项目的方法。
- 常规方式。使用WebStorm GUI或使用终端命令:react-native init AwesomesrcminiProject
- 简便的”创建React Native应用程序”。 create-react-native-app AwesomesrcminiProject
如果你使用create-react-native-app, 则将使用expo自举创建的项目。我不会详细介绍, 但是基本上, 这意味着你无需安装Xcode即可在iOS上运行该应用。通过expo.io的功能和其他一些功能, 使客户端始终保持最新状态也更加容易。但是你不能添加本机代码。因此, 如果你要开发特定功能, 则可能需要从expo中退出应用程序, 而要使用常规的React Native项目。
我将使用第一种方法。
让我们运行项目。首先, 打开仿真器或连接设备。如果使用WebStorm GUI创建了项目, 则只需选择一个配置即可。在WebStorm的右上角, 单击”运行”按钮左侧的下拉菜单, 选择” Android”, 然后单击”运行或调试”。如果使用Terminal创建了项目, 则可以添加新的React Native配置或使用Terminal运行它:
cd AwesomesrcminiProject
react-native run-android
如果一切顺利, 将会看到以下屏幕:
结构和基本设置
项目内值得注意的项目是:
- android-预先配置有React Native支持的Android Studio项目。
- ios-Xcode项目已预先配置有React Native支持。
- node_modules-包含React Native框架和其他Javascript库的文件夹。
- index.js-我们的应用程序的入口点。
- App.js-已加载初始组件。
让我们在项目根目录内创建一个文件夹” src”, 然后将App.js移到该文件夹中。你必须更新index.js导入以匹配新的App.js位置。
import App from './src/App';
删除App.js中的所有内容并粘贴以下代码:
import React from 'react';
import {Text} from 'react-native';
export default class App extends React.Component {
render() {
return (
<Text>Hello srcmini</Text>
);
}
}
我们粘贴的代码非常简单。我们创建了一个类App(React.Component的子类), 该类重写render()方法并返回Text组件。 React.Component是使用JSX构建UI的基类。 export default修饰符使该类公开。
现在, 我们准备开始设计布局。
使用Flexbox进行布局
Flexbox与LinearLayout相似, 但是Flexbox远远超出了LinearLayout的功能。
此JSX片段:
<View style={{
flex: 1, flexDirection: 'row'
}}>
<View style={{
width: 100, height: 100, backgroundColor: '#9575CD'
}}/>
<View style={{
width: 100, height: 100, backgroundColor: '#7E57C2'
}}/>
<View style={{
width: 100, height: 100, backgroundColor: '#673AB7'
}}/>
</View>
渲染此布局:
而此XML:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#9575CD" />
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#7E57C2" />
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#673AB7" />
</LinearLayout>
呈现此:
JSX代码看起来很熟悉, 是吗?让我们使用在JSX和Android XML中看起来相似的布局为布局创建一个”字典”(或备忘单)。
请注意, 功能不一定相等。我正在尝试帮助React Native新手掌握React Native中布局系统的思想。请参阅官方指南以获取详细信息。
考虑以下JSX属性:
flex: 1
等效于:
android:layout_width="match_parent"
android:layout_height="match_parent"
此JSX片段:
<View style={{flex: 1, flexDirection: 'row'}}>
<View style={{
width: 100, height: 100, backgroundColor: '#9575CD'}}/>
<View style={{
width: 100, height: 100, backgroundColor: '#7E57C2'}}/>
<View style={{
width: 100, height: 100, backgroundColor: '#673AB7'}}/>
</View>
而这个XML:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#9575CD" />
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#7E57C2" />
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#673AB7" />
</LinearLayout>
两者都生成此输出:
同样, 此JSX:
<View style={{flex: 1, flexDirection: 'column'}}>
<View style={{
width: 100, height: 100, backgroundColor: '#9575CD'}}/>
<View style={{
width: 100, height: 100, backgroundColor: '#7E57C2'}}/>
<View style={{
width: 100, height: 100, backgroundColor: '#673AB7'}}/>
</View>
而这个XML:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#9575CD" />
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#7E57C2" />
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#673AB7" />
</LinearLayout>
产生这个:
为了在容器内获得正确的位置, 我们通常会结合使用flexDirection, alignItems和justifyContent属性。
此JSX:
<View style={{flex: 1, flexDirection: 'column', alignItems: 'center'}}>
<View style={{
width: 100, height: 100, backgroundColor: '#9575CD'}}/>
<View style={{
width: 100, height: 100, backgroundColor: '#7E57C2'}}/>
<View style={{
width: 100, height: 100, backgroundColor: '#673AB7'}}/>
</View>
而这个XML:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#9575CD" />
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#7E57C2" />
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#673AB7" />
</LinearLayout>
将产生以下布局:
此JSX:
<View style={{flex: 1, flexDirection: 'column', justifyContent: 'center'}}>
<View style={{
width: 100, height: 100, backgroundColor: '#9575CD'}}/>
<View style={{
width: 100, height: 100, backgroundColor: '#7E57C2'}}/>
<View style={{
width: 100, height: 100, backgroundColor: '#673AB7'}}/>
</View>
而这个XML
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical">
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#9575CD" />
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#7E57C2" />
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#673AB7" />
</LinearLayout>
将产生以下布局:
此JSX:
<View style={{flex: 1, flexDirection: 'row', justifyContent: 'center'}}>
<View style={{
width: 100, height: 100, backgroundColor: '#9575CD'}}/>
<View style={{
width: 100, height: 100, backgroundColor: '#7E57C2'}}/>
<View style={{
width: 100, height: 100, backgroundColor: '#673AB7'}}/>
</View>
而这个XML:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="horizontal">
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#9575CD" />
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#7E57C2" />
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#673AB7" />
</LinearLayout>
将产生以下布局:
此JSX:
<View style={{flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center'}}>
<View style={{
width: 100, height: 100, backgroundColor: '#9575CD'}}/>
<View style={{
width: 100, height: 100, backgroundColor: '#7E57C2'}}/>
<View style={{
width: 100, height: 100, backgroundColor: '#673AB7'}}/>
</View>
和这个XML:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#9575CD" />
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#7E57C2" />
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#673AB7" />
</LinearLayout>
将产生以下布局:
经验教训:如果我们有flexDirection:row’, 则alignItems在Y轴上起作用, justifyContent在X轴上起作用。一切都针对flexDirection:”列”进行了镜像-justifyContent影响Y轴, alignItems影响Y轴。
justifyContent:”灵活启动” | 重力=”开始|左” |
alignItems:”弹性启动” | 重力=”开始|左” |
justifyContent:” flex-end” | 重力=”结束|右” |
alignItems:’flex-end’ | 重力=”结束|右” |
自己尝试。将justifyContent值设置为”周围”, “之间”和”均匀”。
国家管理
为了更新应用程序状态, 你将使用React的状态变量。每当状态更新时, 都会调用render()。
将以下代码复制到你的应用中:
import React from 'react';
import {Button, Text, View} from 'react-native';
export default class App extends React.Component {
/*
Initialize state object
with variable 'number'
set to 0 and variable name
with value of empty string
*/
state = {number: 0};
render() {
return (
<View style={{
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', flex: 1, padding: 20
}}>
<Button title='Decrement'
color='#e57373'
onPress={() => this.decrement()}/>
<Text>
{/*
Text will be automatically
updated whenever state.number
has changed value
*/}
Value = {this.state.number}
</Text>
<Button title='Increment'
color='#64B5F6'
{/*
Set listener for click
*/}
onPress={() => this.increment()}/>
</View>
);
}
//Declaration of decrement function
decrement() {
//To update the state we need invoke this.setState
//with new value for variable 'number'
this.setState({number: this.state.number - 1});
}
increment() {
this.setState({number: this.state.number + 1});
}
}
如果你点击DECREMENT和INCREMENT按钮, 则会看到该文本会自动为你更新。无需显式使用textView.setText(” Value” +数字)。
状态功能之所以派上用场, 有多种原因:
- 轻松获取值-你始终知道在何处以及如何获取特定变量的值。
- 数据未绑定到特定的小部件。
- 具有多个依赖于公共值更改的小部件。
为/ r / pics创建搜索应用
现在, 我们已经掌握了基本知识, 让我们创建一些更复杂的东西:/ r / pics的搜索应用程序。 Reddit提供了一个简单明了的JSON API端点, 因此我们无需再进行其他工作即可获得身份验证以使其正常工作。
React Native提供了一个内置的Fetch API。由于我们大多数人可能都习惯于翻新及其易用性, 因此我们将使用axios。你可以通过终端命令安装axios
使用纱线(我的首选方法):
纱线添加轴
或使用npm:
npm安装axios
进口:
import React from 'react';
import {
TextInput, View, Text, Image, ActivityIndicator, Platform, StyleSheet
} from 'react-native';
import axios from 'axios';
TextInput = EditText, ActivityIndicator = ProgressBar
Platform - Platform detecting module
StyleSheet - Module for creating stylesheets and moving them away from JSX
创建类:
export default class App extends React.Component {
}
初始化状态。我们需要:
- loading-用于显示进度条。
- 错误-显示在发出REST API请求时是否产生一些错误。
- imgUrl-预览搜索到的图像。
- 文字-搜索查询。
state = {text: '', loading: false, error: null, imgUrl: null};
添加JSX代码。我们具有TextInput和Image组件的垂直布局。
render() {
return (
//Predefined style. See below
<View style={styles.containerStyle}>
{/*
returnKeyType ~ imeOptions
onSubmitEditing ~ et.OnEditorActionListener
*/}
<TextInput
style={styles.textInputStyle}
placeholder="Enter text to search an image"
returnKeyType='search'
autoFocus={true}
onChangeText={(text) => this.setState({text})}
onSubmitEditing={() => this.searchPicture()}/>
{/*
Render error Image component
if this.state.imgUrl is
not equal to null
*/}
{
this.state.imgUrl &&
<Image
source={{uri: this.state.imgUrl}}
style={{flex: 1}}/>
}
</View>
);
}
新的东西:
onChangeText={(text) => this.setState({text})}
onSubmitEditing={() => this.searchPicture()}
{
this.state.imgUrl &&
<Image
source={{uri: this.state.imgUrl}}
style={{flex: 1}}/>
}
第一种方法与使用TextWatcher组件的EditText做类似的工作。老实说, React Native更好。
触发searchPicture()后, 在键盘上按下返回键(et.OnEditorActionListener)时, 将调用第二种方法。
当imgUrl不为null或未定义时, 将渲染Image, 因为” &&”运算符不会检查第二个参数(如果第一个参数已为false)。
你可能想知道为什么this.state.imgUrl为假。好吧, 当在JavaScript中使用逻辑运算符时, 除了‘’(一个空字符串), 0, false, null或undefined以外的其他都是真。无需进行特定检查。
searchPicture() {
//Default state
this.setState({loading: true, error: null, imgUrl: null});
axios.get('https://www.reddit.com/r/pics/search.json', {
params: { //the get param map
restrict_sr: 'on', //search only /r/pics
limit: 1, //limit to one search item
sort: 'new', //sort by creation date
q: this.state.text //our search query
}
}).then(response => { //promise is resolved and 'then' block is triggered
//set state with new values
this.setState({
imgUrl: response.data.data.children[0]
.data.preview.images[0].source.url, error: null, loading: false
})
}).catch(error => {//Some error occurred
//set error
this.setState({error: error.message, loading: false, imgUrl: null})
})
}
开始了。该应用程序现在应该可以正常工作了。输入搜索字符串, 然后按回车键。
由于我们的应用程序也已经准备好呈现ActivityIndicator和错误, 因此我们需要在Image组件之后添加一些代码:
{
//Separate method
this.renderProgress()
}
{/*
Render error Text component
if this.state.error is
not equal to null
*/}
{
this.state.error &&
<Text style={{margin: 16, color: 'red'}}>
{this.state.error}
</Text>
}
你也可以将渲染组件移到主要render()方法之外:
renderProgress() {
//If this.state.loading is true
//return View containing a progressbar
//View takes style array
if (this.state.loading === true) {
return (
<View style={
[styles.containerStyle, {justifyContent: 'center'}]}>
<ActivityIndicator color='#e57373'/>
</View>
);
}
}
剩下的就是样式。将它们放在App类之外。
const styles = StyleSheet.create({
containerStyle: {
flexDirection: 'column', flex: 1, //Since React Native is cross platform
//let's handle both platforms.
//Add top margin to fix status bar overlap
marginTop: Platform.OS === 'ios' ? 20 : 0, }, textInputStyle: {
marginLeft: 16, marginRight: 16, height: Platform.OS === 'ios' ? 30 : undefined
}
});
现在, 我们可以添加一些其他调整, 例如在启动应用程序时自动打开软键盘。
请注意, 有一种更简单的方法可以使TextInput自动聚焦(autoFocus = {true}道具), 但是就本示例而言, 我们将不再使用它。
用prop添加对TextInput的引用:
ref = {ref => this.searchInput = ref}
并重写componentDidMount()生命周期方法, 如下所示:
componentDidMount(){
this.searchInput.focus();
}
重新加载应用程序, 键盘会自动为我们打开。
组件生命周期方法
我们已经创建了一个组件, 但是让我们来回顾一下组件的整个生命周期。
这是React的生命周期流程:
- constructor()-构造函数总是在应用程序启动时被调用
- 静态_getDerivedStateFromProps_(属性, 状态)-在渲染之前和更新之后调用。返回用于更新状态的对象。返回null不更新任何内容。
- render()-每个React Component类都需要渲染。用于渲染视图。
- componentDidMount()-在呈现组件并将其安装到视图树后调用。
- shouldComponentUpdate(nextProps, nextState)-状态或道具更改后调用。每次状态更新后, 返回值默认为true。如果返回true, 则调用render()。
- getSnapshotBeforeUpdate(prevProps, prevState)-在提交呈现输出之前调用。
- componentDidUpdate(prevProps, prevState, 快照)-呈现新更新后调用。在第一个render()之后不会调用它。
- componentWillUnmount()-在组件卸载和销毁之前调用。
可重复使用的组件
在项目上工作时, 我们经常需要创建可重用的组件。有两种创建组件的方法:
- 创建一个扩展React.Component的类。如果需要生命周期方法, 则应使用此方法。
- 通过编写返回View的函数以简化语法。
由于我们已经创建了Component类, 因此可以为此实例创建一个函数。
假设我们需要一个<CardView>的类似物。在./src目录下创建一个”公用”文件夹。
创建CardView.js。
import React from "react";
import {View} from "react-native";
export default CardView = (props) => {
return (
//Style will be merged from default containerStyle
//and props.style. props.style attributes will override
//values if parameters are same.
<View style={{...styles.containerStyle, ...props.style}}>
{/*
props.children contain subviews
add this line if the component is container
*/}
{props.children}
</View>
);
};
const styles = {
containerStyle: {
borderRadius: 4, margin: 5, padding: 5, elevation: 5, shadowColor: 'black', shadowRadius: 5, shadowOpacity: 0.5, shadowOffset: {width: 0, height: 3}, backgroundColor: 'white'
}
};
使用我们新的CardView布局的LoginForm:
import React from "react";
import {TextInput, Platform, Button, StyleSheet} from "react-native";
import CardView from "../common/components/CardView";
export default class LoginForm extends React._Component _{
render() {
return (
//Override default style
<CardView style={{
borderRadius: 4, backgroundColor: '#fff'
}}>
<TextInput
placeholder="Email"
style={styles.textInputStyle}/>
<TextInput
placeholder="Password"
style={styles.textInputStyle}
secureTextEntry={true}/>
<Button color="#841584"
title="Login"
onPress={() => console.log("onLoginPress")}
buttonStyle={styles.buttonStyle}/>
</CardView>
);
}
}
const styles = StyleSheet.create({
buttonStyle: {
elevation: 5, height: 40
}, textInputStyle: {
padding: 10, //Additional params to make
//iOS inputs prettier
...Platform.select({
ios: {
borderRadius: 2, marginTop: 5, backgroundColor: '#eeeeee'
}
})
}
});
将LoginForm类导入App类中, 并用View包装
<View style={{flex: 1, justifyContent: 'center'}}>
<LoginForm/>
</View>
如果你调整样式中的参数, 则可以获得更好的东西。
导航
导航到不同的场景是大多数应用程序中必不可少的部分。我们将创建一个Reddit / r / pics浏览器应用。
在React Native中创建导航非常容易。
先决条件
- 使用yarn或npm安装反应导航
- 用yarn或npm安装axios
首先创建两个不同的组件。
注意:你应该已经熟悉下面的大多数代码。我将全班粘贴。
PictureList.js:
import React from 'react';
import {
ActivityIndicator, FlatList, Image, Text, TouchableHighlight, View
} from "react-native";
import axios from "axios";
import CardView from "../common/CardView";
export default class PictureList extends React.Component {
state = {loading: true, error: null, posts: null};
componentDidMount() {
axios.get('https://www.reddit.com/r/pics.json')
.then(response => {
this.setState({
posts: response.data.data.children, loading: false
})
}).catch(error => {
this.setState({
error: error.message, loading: false
})
})
}
render() {
return (
<View style={{flex: 1, justifyContent: 'center'}}>
// FlatList ~ ListView
// data - DataSource for the List
// renderItem - function returns View item
// keyExtractor - Unique id for items
{this.state.posts &&
<FlatList data={this.state.posts}
renderItem={this.renderItem.bind(this)}
keyExtractor={(item) => (item.data.id + '')}/>}
{this.state.loading &&
<ActivityIndicator size="large" color="#f4511e"/>}
</View>
);
}
navigateToPicture(title, url) {
this.props.navigation.navigate('PicturePreview', {
'title': title, 'url': url
})
}
renderItem(item) {
//Destructuring values from item
//Read more 'ES6 destructuring'
const {data} = item.item;
const {title} = data;
const {url} = data.preview.images[0].source;
return (
//Clickable view
<TouchableHighlight onPress={() =>
this.navigateToPicture(title, url)}>
{/Reusing our CardView/}
<CardView>
<Image style={{height: 150}}
source={{uri: url}}/>
<Text style={{padding: 5}}>{title}</Text>
</CardView>
</TouchableHighlight>
)
}
}
PicturePreview.js:
import React from 'react';
import {Image} from "react-native";
export default class PicturePreview extends React.Component {
//Destructure navigation
//Set title to header
static _navigationOptions = ({navigation}) => ({
title: navigation.state.params.title
});
render() {
const {url} = this.props.navigation.state.params;
return (<Image style={{flex: 1}} source={{uri: url}}/>)
}
}
navigationOptions将由React-Navigation自动调用。
现在转到App.js
注意:React-Navigation中有许多导航类型。今天, 我们将专注于StackNavigation。请参考官方网站了解详细信息。
import React from 'react';
import {createStackNavigator} from "react-navigation";
import PictureList from "./components/PictureList";
import PicturePreview from "./components/PicturePreview";
export default class App extends React.Component {
render() {
return (
<Router/>
);
}
}
//Customize the header_
const NavigationOptions = {
headerTintColor: '#fff', headerStyle: {
backgroundColor: '#f4511e', }
};
//Create the router.
const Router = createStackNavigator({
//Name the screen
'PictureList': {
//Link the Component
screen: PictureList, //Additional navigation options
navigationOptions: {
title: '/r/pics Browser', ...NavigationOptions
}
}, 'PicturePreview': {
screen: PicturePreview, navigationOptions: NavigationOptions
}
}, {
//Root
initialRouterName: 'PictureList'
}
);
如你所见, 我们需要做的就是创建一个导航路由器, 并使应用程序呈现它。如果一切顺利, 我们将提供功能强大的Reddit / r / pics浏览器应用程序。
Android:
iOS:
React Native很棒!
自从开始编程以来, 我就拥有纯粹的移动开发经验。但是现在我可以使用React编写几乎所有内容:移动, 桌面和Web。
如果你决定开始使用React Native开发下一个令人惊叹的应用程序, 你会发现它有它的怪癖和一些错误, 但是React Native非常实用, 适合大多数项目。
相关:构建QR扫描仪:React Native Camera教程