首发于公众号 前端混合开发,欢迎关注。
在这个教程中,我们将指导你如何在React Native中设计组件。通过构建一个示例电子商务移动应用,我们将展示React Native中的各种设计技巧,让你能够设计出有效的跨平台应用程序。
React Native 简介
React Native是一个开源的移动应用框架,由Facebook在2015年推出。全球的移动开发者使用React Native来为Android和iOS,以及网页和macOS/Windows格式构建应用程序。
React Native的主要功能包括:
- 采用与 React 相同的设计,使您能够使用熟悉的React范式和组件
- 可以使用JavaScript编程
- React Native通过在大多数情况下只编写一次代码,实现了iOS和Android 的代码重用
- 它使用原生组件而非WebViews来渲染用户界面,使你的应用程序真正成为原生应用
- React Native应用将其JavaScript代码编译成优化的原生应用二进制文件,提供极快的性能
React Native的卖点在于它使开发者能够结合使用React的框架和原生平台的功能。
设置环境
我们将使用Expo快速启动一个React Native项目。为了设置Expo项目的开发环境,你首先需要安装 Node.js 和 npm。这是运行 create-expo-app
命令和安装依赖项所必需的。
然后,使用 create-expo-app
命令创建一个名为 my-ecommerce-app
的新应用:
npx create-expo-app my-ecommerce-app
接下来,导航至项目目录:
cd my-ecommerce-app
使用以下命令启动开发服务器:
npx expo start
这个命令将启动一个Metro Bundler服务器和一个Expo Dev客户端,你可以用它在物理设备或网页浏览器上运行应用程序。
理解React Native样式
在我们深入研究如何为我们的应用程序进行样式设计之前,让我们先更好地理解 React Native 的样式。
一个应用的外观和感觉直接关联到用户体验。使用针对iOS和Android的平台特定风格指南,遵循一致的排版,甚至处理多种设备尺寸的布局,对于创建一个吸引人的、一致的、直观的应用非常重要。这些应用有更大的机会让用户回归,这是商业的一个重要方面。
React Native遵循CSS-in-JS的方式来设计组件,而不是普通的CSS。语法与我们已经熟悉的CSS非常相似,但让我们看看一些关键的区别:
- React Native 使用弹性盒子布局API,而不是CSS盒子模型进行布局。弹性盒子API更适合不同大小和方向的移动屏幕。
- 在 React Native 中,样式属性是以驼峰式的 JavaScript 属性形式编写的,而不是 CSS 语法。例如,写作
backgroundColor
而不是background-color
。 - 而不是类,样式被定义为JavaScript对象,并通过组件的
style
属性来应用 - 动画是通过使用 JavaScript 动画API而不是CSS过渡和动画来定义的。我们也可以使用React Native Reanimated 包来编写高性能的动画。
- 供应商前缀不需要,因为React Native在后台处理平台特定的样式。但是我们可以使用平台API来处理不同平台上的不同样式。
- 媒体查询不被React Native支持。通过定义不同的样式表并有条件地应用它们来实现响应性。
- 没有
:hover
或:active
伪类。我们可以使用React Native的Pressable
组件来处理悬停和活动状态的样式:
<Pressable
style={({ pressed }) => [
{
backgroundColor: pressed ? "rgb(210, 230, 255)" : "white",
},
]}
>
{({ pressed }) => (
<Text style={styles.text}>{pressed ? "Pressed!" : "Press Me"}</Text>
)}
</Pressable>;
React Native有一些特定的附加样式属性,如 borderRadius
用于圆角, flex
用于弹性盒布局
内联样式 vs. 样式表
你可以通过内联样式或 StyleSheet
来为 React Native 组件应用样式。
内联样式直接用作组件的 style
属性中的JavaScript对象:
<Text style={{fontSize: 20, color: 'blue'}}>Hello LogRocket!</Text>
StyleSheet
用于创建与 React Native 组件分离的样式。这种方法使得在多个地方维护和重用样式变得更加容易。
// define styles
const styles = StyleSheet.create({
text: {
fontSize: 20,
color: 'blue'
}
});
// use them in component
<Text style={styles.text}>Hello LogRocket!</Text>
// can also be reused
<Text style={styles.text}>This is a React Native tutorial</Text>
React Native 样式属性和单位
在 React Native 的样式中,单位与CSS有些不同。React Native 对于像 borderRadius
, padding
, fontSize
等属性使用无单位的数字。例如,我们不会说 10px
,而是直接写 10
,如 { fontSize: 10 }
所示。
像dp
,px
等单位会根据平台自动添加。这种无单位数字的使用确保了在不同分辨率的设备上进行统一的缩放。
此外,React Native支持基于百分比的单位,但它们必须以字符串的形式提供,如 { width: '100%' }
。在定义颜色和对齐方式时,你可以像平常一样使用字符串值来应用它们。
带代码演练的样式示例
对于这个React Native样式演示,我们将构建一个电子商务移动应用程序,展示各种产品供用户根据他们的偏好进行订购。特别是,我们将通过构建一个登陆页面和购物车功能来突出一些关键的React Native样式概念。
完成的应用程序将如下所示:
这款电商移动应用主要由两个部分构建,包含两大功能: Home
组件和 CartModal
组件。要跟随操作,请在此处克隆初始仓库:
git clone https://github.com/iamshadmirza/logrocket-online-store.git
导航至新创建的 log-rocket-online-store-starter
目录,并在你的终端/命令提示符中运行 npm install
。一旦安装完成,运行下面的命令来启动 Metro 打包器,这是一个由Facebook构建的JavaScript打包器:
cd logrocket-online-store && npx expo start
如果一切设置正确,你应该会看到一个新的终端窗口打开,如下图所示:
现在我们已经设置好了项目并运行了Metro打包器,让我们为我们的初始项目运行一个构建。
文件夹结构
我们的React Native样式示例的文件夹结构如下:
assets
包含了所有应用程序的资产,如图片、图标等。constants
包含images.ts
,theme.ts
和index.ts
。所有的资产都存储在JavaScript常量中。screens
包含Home.tsx
和Home.finished.tsx
文件。Home.finished.tsx
是供参考的——它包含最终的代码。src
文件夹包含了以上所有内容
在这个教程中,我们将专注于应用程序的样式设计,并理解何时使用主题和何时使用内联样式。欢迎自由探索代码库,以理解幕后的所有工作原理。
为React Native组件进行样式设计
Home
组件有两个部分:特色产品和最近搜索。
import React from "react";
import {
StyleSheet,
View,
Text,
FlatList,
TouchableOpacity,
Image,
Modal,
} from "react-native";
import { BlurView } from 'expo-blur';
import { images, COLORS, SIZES, FONTS } from "../constants";
const Home = () => {
const [selectedItem, setSelectedItem] = React.useState(null);
const [selectedSize, setSelectedSize] = React.useState("");
const [showAddToCartModal, setShowAddToCartModal] = React.useState(false);
const [featured, setFeatured] = React.useState([
{
id: 0,
name: "Jacket 4",
img: images.jacket4,
bgColor: "#D09040",
price: "$250",
type: "Featured",
sizes: [6, 7, 8, 9, 10, 16],
},
{
id: 1,
name: "Jacket 1",
img: images.jacket1,
bgColor: "#D3D1C8",
type: "Featured",
price: "$150",
sizes: [6, 7, 8, 9, 10, 12],
},
{
id: 2,
name: "Jacket 2",
img: images.jacket2,
type: "Featured",
bgColor: "#303946",
price: "$160",
sizes: [6, 7, 8, 9, 10],
},
]);
const [recentSearches, setRecentSearch] = React.useState([
{
id: 0,
name: "Jacket 4",
img: images.jacket4,
bgColor: "#D09040",
price: "$250",
type: "Featured",
sizes: [6, 7, 8, 9, 10, 16],
},
{
id: 1,
name: "Sweater 3",
img: images.sweater3,
type: "Featured",
bgColor: "#0F5144",
price: "$100",
sizes: [6, 7, 8, 9, 10, 16, 18],
},
{
id: 2,
name: "Sweater 5",
img: images.sweater5,
type: "Featured",
bgColor: "#888983",
price: "$100",
sizes: [6, 7, 8, 9, 10, 18],
},
{
id: 7,
name: "Jacket 1",
img: images.jacket1,
bgColor: "#D3D1C8",
type: "Featured",
price: "$150",
sizes: [6, 7, 8, 9, 10, 12],
},
{
id: 8,
name: "Jacket 2",
img: images.jacket2,
type: "Featured",
bgColor: "#303946",
price: "$160",
sizes: [6, 7, 8, 9, 10],
},
{
id: 3,
name: "Hat 1",
img: images.hat1,
type: "Featured",
bgColor: "#26232A",
price: "$100",
sizes: [6, 7, 8, 9, 10, 16],
},
{
id: 4,
name: "Shirt 1",
img: images.shirt1,
type: "Featured",
bgColor: "#575569",
price: "$100",
sizes: [6, 7, 8, 9, 10, 16],
},
{
id: 5,
name: "Shirt 2",
img: images.shirt2,
type: "Featured",
bgColor: "#2B3A6B",
price: "$100",
sizes: [6, 7, 8, 9, 10, 16],
},
{
id: 6,
name: "Shoe 1",
img: images.shoe1,
type: "Featured",
bgColor: "#9E7348",
price: "$100",
sizes: [6, 7, 8, 9, 10, 12],
},
]);
function renderFeaturedItems(item, index) {
return (
<TouchableOpacity
style={{
height: 300,
width: 200,
justifyContent: "center",
marginHorizontal: SIZES.base,
}}
onPress={() => {
setSelectedItem(item);
setShowAddToCartModal(true);
}}
>
<Text style={{ color: COLORS.lightGray, ...FONTS.h5 }}>
{item.type}
</Text>
<View
style={[
{
flex: 1,
justifyContent: "flex-end",
marginTop: SIZES.base,
borderRadius: 10,
marginRight: SIZES.padding,
backgroundColor: item.bgColor,
paddingRight: SIZES.padding,
paddingBottom: SIZES.radius,
},
style.featuredShadow,
]}
>
<View style={style.featuredDetails}>
<Text
style={{ color: COLORS.white, ...FONTS.body4, marginTop: 15 }}
>
{item.name}
</Text>
<Text style={{ color: COLORS.white, ...FONTS.h2 }}>
{item.price}
</Text>
</View>
</View>
<Image
source={item.img}
resizeMode="cover"
style={{
position: "absolute",
top: 25,
right: 20,
width: "90%",
height: 200,
}}
/>
</TouchableOpacity>
);
}
// paste recent searches code
// paste renderSizes
return (
<View style={style.container}>
<Text
style={{
marginTop: SIZES.radius,
marginHorizontal: SIZES.padding,
...FONTS.h2,
}}
>
FEATURED
</Text>
{/* Featured */}
<View style={{ height: 260, marginTop: SIZES.radius }}>
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={featured}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item, index }) => renderFeaturedItems(item, index)}
/>
</View>
{/* Recent Searches */}
{/* Modal */}
</View>
);
};
const style = StyleSheet.create({
container: {
flex: 1,
backgroundColor: COLORS.white,
},
featuredShadow: {
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 5,
},
shadowOpacity: 0.29,
shadowRadius: 4.65,
elevation: 7,
},
featuredDetails: {
position: "absolute",
top: 160,
left: 30,
flexDirection: "column",
marginLeft: 25,
marginBottom: 8,
},
});
export default Home;
如果你运行 npm run ios
,你的应用现在应该看起来像下面的图片:
上述大多数字体大小、内边距和外边距都在位于 constant
文件夹中的 theme.ts
文件中声明。在 renderFeaturedItems
函数内,注意我是如何结合使用内联样式和属性方法来设计特色项目的。react-native
的 style
属性可以接受一个数组,因此你可以传递一个第二参数来进行样式设置。请参见下面的示例:
style = {
[{
flex: 1,
justifyContent: "flex-end",
marginTop: SIZES.base,
borderRadius: 10,
marginRight: SIZES.padding,
backgroundColor: item.bgColor,
paddingRight: SIZES.padding,
paddingBottom: SIZES.radius,
},
style.featuredShadow,
]
}
在上面的代码块中, style prop
的值被包裹在一个括号 []
中,意味着prop
将接受一个对象数组作为值。 style prop
的第二个参数是使用 Stylesheet.create()
创建的。如前所述,这种方法在复杂的React Native应用中很受欢迎,是支持多种样式的绝佳方式。
既然我们已经构建了 Home 组件的第一部分,那么让我们来构建最后一部分: recent searches
部分。
在我们开始之前,让我们考虑一下最近搜索的结构应该是什么样的。我们首先需要创建一个弹性容器,其弹性方向设置为 row
,以及两列将作为容器的直接子元素。第一列包含一个图像,另一列是一个弹性容器,其中包含产品图像、产品名称和价格。
就像在CSS中,当在React Native中进行样式设计时,你应该在开始设计应用程序之前,总是将你的UI组件映射到CSS属性。这将为你提供一个更广泛的概述,说明如何从头开始设计你的整个应用程序。
要构建 Home 组件的第二部分,请找到 // paste recent searches code
注释,并将代码复制粘贴到注释下方:
function renderRecentSearches(item, index) {
return (
<TouchableOpacity
style={{ flex: 1, flexDirection: "row" }}
onPress={() => {
setSelectedItem(item);
setShowAddToCartModal(true);
}}
>
<View
style={{ flex: 1, alignItems: "center", justifyContent: "center" }}
>
<Image
source={item.img}
resizeMode="contain"
style={{
width: 130,
height: 100,
}}
/>
</View>
<View
style={{
flex: 1.5,
marginLeft: SIZES.radius,
justifyContent: "center",
}}
>
<Text>{item.name}</Text>
<Text style={{ ...FONTS.h3 }}>{item.price}</Text>
</View>
</TouchableOpacity>
);
}
接下来,在 Home.ts
中找到 {/* Recent Searches */}
注释,并将下面的代码粘贴在其下方:
<View
style={[
{
flex: 1,
flexDirection: "row",
marginTop: SIZES.padding,
borderTopLeftRadius: 30,
borderTopRightRadius: 30,
backgroundColor: COLORS.white,
},
style.recentSearchShadow,
]}
>
<View style={{ width: 70, height: "100%", marginLeft: SIZES.base }}>
<Image
source={images.searches}
style={{ width: "100%", height: "100%", resizeMode: "contain" }}
/>
</View>
<View style={{ flex: 1, paddingBottom: SIZES.padding }}>
<FlatList
showsVerticalScrollIndicator={false}
data={recentSearches}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item, index }) => renderRecentSearches(item, index)}
/>
</View>
</View>
添加阴影
在React Native 中为组件添加阴影的方式与在CSS中的做法相当不同。你需要指定 shadowColor
、 ShadowOffset
、 ShadowOpacity
、 ShadowRadius
和 elevation
。
将下面的代码添加到 StyleSheet.create
以完成 Home
组件中 recentSearches
部分的样式设计:
recentSearchShadow:{
shadowColor: "#000",
shadowOffset:{
width: 0,
height: 5,
},
shadowOpacity: 0.29,
shadowRadius: 4.65,
elevation: 7
},
recentSearches: {
width: "100%",
transform: [{ rotateY: "180deg" }]
},
这只适用于在iOS中创建阴影。要在Android中为组件添加阴影,请参阅此处的官方指南。
设置模态样式
最后,让我们创建一个模态框来显示每个选中的产品。在 Home.tsx
中找到 // paste renderSizes
注释,并在其下方复制粘贴以下代码:
function renderSizes() {
return selectedItem.sizes.map((item, index) => {
return (
<TouchableOpacity
key={index}
style={{
width: 35,
height: 25,
alignItems: "center",
justifyContent: "center",
marginHorizontal: 5,
marginBottom: 10,
backgroundColor:
selectedItem.sizes[index] == selectedSize ? COLORS.white : null,
borderWidth: 1,
borderColor: COLORS.white,
borderRadius: 5,
}}
onPress={() => {
setSelectedSize(item);
}}
>
<Text
style={{
color:
selectedItem.sizes[index] == selectedSize
? COLORS.black
: COLORS.white,
...FONTS.body4,
}}
>
{item}
</Text>
</TouchableOpacity>
);
});
}
我们需要在模态背景中使用模糊效果。安装所需的包以使用 BlurView
:
npx expo install expo-blur
然后你可以像这样导入它:
import { BlurView } from 'expo-blur';
当 selectedItem
存在时,要添加模态框,请在 Home 组件中的注释 {/* Modal */}
下方粘贴以下代码:
{selectedItem && (
<Modal
animationType="slide"
transparent={true}
visible={showAddToCartModal}
>
<BlurView
style={style.blur}
tint="light"
intencity={20}
>
<TouchableOpacity
style={style.absolute}
onPress={() => {
setSelectedItem(null);
setSelectedSize("");
setShowAddToCartModal(false);
}}
></TouchableOpacity>
{/* Modal content */}
<View
style={{
justifyContent: "center",
width: "85%",
backgroundColor: selectedItem.bgColor,
}}
>
<View>
<Image
source={selectedItem.img}
resizeMode="contain"
style={{
width: "100%",
height: 170,
}}
/>
</View>
<Text
style={{
marginTop: SIZES.padding,
marginHorizontal: SIZES.padding,
color: COLORS.white,
...FONTS.h2,
}}
>
{selectedItem.name}
</Text>
<Text
style={{
marginTop: SIZES.base / 2,
marginHorizontal: SIZES.padding,
color: COLORS.white,
...FONTS.body3,
}}
>
{selectedItem.type}
</Text>
<Text
style={{
marginTop: SIZES.radius,
marginHorizontal: SIZES.padding,
color: COLORS.white,
...FONTS.h1,
}}
>
{selectedItem.price}
</Text>
<View
style={{
flexDirection: "row",
marginTop: SIZES.radius,
marginHorizontal: SIZES.padding,
}}
>
<View>
<Text style={{ color: COLORS.white, ...FONTS.body3 }}>
Select Size
</Text>
</View>
<View
style={{
flex: 1,
flexWrap: "wrap",
flexDirection: "row",
marginLeft: SIZES.radius,
}}
>
{renderSizes()}
</View>
</View>
<TouchableOpacity
style={{
width: "100%",
height: 70,
justifyContent: "center",
alignItems: "center",
marginTop: SIZES.base,
backgroundColor: "rgba(0,0,0,0.5)",
}}
onPress={() => {
setSelectedItem(null);
setSelectedSize("");
setShowAddToCartModal(false);
}}
>
<Text style={{ color: COLORS.white, ...FONTS.largeTitleBold }}>
Add To Cart
</Text>
</TouchableOpacity>
</View>
</BlurView>
</Modal>
)}
你必须使用绝对定位来将模态框定位在应用程序的中心。复制并粘贴下面的代码到 Stylesheet.create()
对象中,以将模态框定位在中心:
blur: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
absolute: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
},
一旦你对代码感到满意,你可以将其与GitHub仓库中的完成代码进行比较。
高级造型技巧
现在我们已经在React Native应用程序中建立了基本样式的坚实基础,是时候探索一些可以增强应用程序外观和感觉的高级技术了。这些技术可能不会直接得到 React Native 的支持,但它们对于创建可扩展和高效的样式解冓方案至关重要。
响应式设计
- 使用弹性盒子:React Native有一个类似于CSS的弹性盒子实现。为了创建一个响应式布局,你可以使用弹性属性,如
flexDirection
,flexWrap
,alignItems
,justifyContent
等。 - 使用 Dimensions API:React Native有一个 Dimensions API,它可以提供设备屏幕的宽度和高度。你可以根据屏幕大小的条件来渲染组件。
- 使用百分比宽度:不要使用固定的像素宽度,而应为组件使用百分比宽度。这将使它们根据屏幕宽度进行响应式调整。
媒体查询
媒体查询默认不受支持,但我们可以使用 react-native-media-queries
包来获得类似的功能:
import StyleSheet from 'react-native-media-query';
import { View, TextInput } from 'react-native'
const {ids, styles} = StyleSheet.create({
example: {
width: 100,
height: 100,
backgroundColor: 'green',
'@media (max-width: 1600px) and (min-width: 800px)': {
backgroundColor: 'red',
},
'@media (max-width: 800px)': {
backgroundColor: 'blue',
},
},
exampleTextInput: {
paddingVertical: 27,
backgroundColor: 'pink',
'@media (max-width: 768px)': {
paddingVertical: 13,
},
// example CSS selectors. these only work on web based platforms
'::placeholder': {
color: 'green',
},
':hover': {
backgroundColor: 'red',
},
}
})
动态样式
因为React Native使用对象进行样式设计,我们可以用任何变量代替属性的样式,或者我们可以使用一个基于特定参数返回一些样式的函数:
const getBackgroundColor = (colorScheme) => {
return colorScheme === 'dark' ? '#000' : '#fff'
}
<View style={getBackgroundColor("dark")} />
主题化
所有现代应用程序现在都支持亮色和暗色主题以增强可访问性。我们可以检测设备的默认主题,或者允许用户设置他们的偏好。一旦我们知道他们的偏好,我们可以相应地调整应用程序的其余部分。
我们将使用 Appearance API 来更改我们应用的主题:
import { useColorScheme } from 'react-native';
const colorScheme = useColorScheme();
const styles = {
backgroundColor: colorScheme === 'dark' ? '#000' : '#fff'
};
接下来,将主题存储在状态中:
const [theme, setTheme] = useState('light');
const styles = {
backgroundColor: theme === 'dark' ? '#000' : '#fff'
};
// Toggle theme
toggleTheme = () => {
theme === 'light' ? setTheme('dark') : setTheme('light');
}
然后,使用一个 ThemeProvider:
const ThemeContext = createContext('light');
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Use context
const { theme } = useContext(ThemeContext);
const styles = {
backgroundColor: theme === 'dark' ? '#000' : '#fff'
};
为每个主题定义单独的样式表,使用以下代码:
const lightTheme = {...};
const darkTheme = {...};
const currentTheme = theme === 'dark' ? darkTheme : lightTheme;
<View style={currentTheme.container} />
React Native样式的最佳实践
让我们考虑一些在设计React Native应用程序时的最佳实践。
组织风格
我们使用了一个主题对象作为单一真理来源,这是推荐的方法。它允许你保存所有常用的属性,如字体大小、排版、空间、组件大小、颜色等。
通过使用React Context,可以在整个应用程序中访问此主题,从而无需在每次使用时更新主题文件。
将你的应用程序组件化
将用户界面分解为更小的组件可以帮助你使它们可重用且更易于管理。始终将屏幕分解为更小的组件,并尽可能多地重用。
这种模块化的方法节省了宝贵的开发时间,并显著提高了整个代码库的可维护性。通过将你的应用程序组织成不同的组件,每当设计规格或功能需求发生变化时,你都可以快速定位并更新相关代码。
避免使用像素值
移动应用程序被设计为适应不同屏幕尺寸和分辨率的设备。在定义布局和样式时,避免使用硬编码的像素值是至关重要的,以确保你的应用在所有设备上都能提供无缝的用户体验。相反,建议采用更灵活的方法,通过使用弹性盒和百分比单位来进行样式设计。
请注意,这只适用于组件布局,而不适用于字体大小。字体大小应使用无单位的值,并使用平台API相应地调整字体。
选择样式表而非内联样式
内联样式不仅使组件更难阅读,而且还妨碍了样式的可维护性和可重用性。建议使用 StyleSheet
对象来单独定义样式,与功能组件分开。
虽然内联样式可以用于添加条件样式,但StyleSheet应该是主要的方法。
总结
如果你理解了前面讨论的关键差异,那么在React Native中进行样式设计可以非常简单。我们探讨了使用flexbox和基于百分比的样式来构建复杂的布局。此外,我们还研究了如何通过一个公共主题对象来重用样式常量。你可以在React Native的文档中找到更多关于样式属性的信息。
交流
首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答
本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。