最近一直在学
react-native
,看了几天,就想去找个app
练练手,然后我想自己不是经常刷掘金吗?索性就拿它当目标了,开整。
这里强烈建议,配置开发环境的时候,一定要有个稳定的科学上网的工具,不然你会卡死在这里的
项目目标(android)
- 应用适配
- 自适应主题(深色|浅色)
- 开屏封面
- 页面鉴权
- 部分页面开发 (首页、沸点、发现、课程、我的、登录、搜索、文章详情、沸点详情、webview内嵌、标签管理、阅读记录、内容数据、福利兑换)
- 公共组件开发(顶部导航栏、页面鉴权、搜索框、markdown、评论卡片、文章卡片、沸点卡片、分割线)
应用适配
我一开始也是没有方向,不知道怎么做,还是用web
开发去做类比。后面参考了很多文章,原理也是使用一个基准尺寸开发(比如:iphone6(375*667)
),然后不同的手机尺寸在这个基础上进行等比计算,公式如下
ts
const deviceWidth = Dimensions.get('window').width
const uiWidth = 375
const dp2px = (uiElementPx: number) => (uiElementPx * deviceWidth) / uiWidth
比如我的页面上的布局盒子是60
,我使用的基准尺寸是iphone6(375*667)
,如果去适配其他尺寸,我们这里用iphone14(430*932)
的尺寸来说明:通过这个公式可以得出60*430/375
得到68.8
,他在ihone14
上尺寸变化60--->68.8
。
具体到我们的页面怎么开发呢? 我是这么做的(也是参考的别人的方案)
- 封装一个对象
styleSheet
,然后提供create
方法。 create
内部就调用dp2px
方法做单位换算。
这样就和官方提供的开发方式保持一致
下面是具体代码示例
dp2px工具类
dp2px.ts
import { Dimensions } from 'react-native'
const deviceWidth = Dimensions.get('window').width
const uiWidth = 375
const dp2px = (uiElementPx: number) => (uiElementPx * deviceWidth) / uiWidth
export default dp2px
styleSheet工具类
styleSheet.ts
import { StyleSheet, ImageStyle, TextStyle, ViewStyle } from 'react-native'
type NamedStyles<T> = { [P in keyof T]: ViewStyle | TextStyle | ImageStyle }
import dp2px from './dp2px'
const styleSheet = {
create<T extends NamedStyles<T> | NamedStyles<any>>(style: T) {
let s = { ...style }
let list = [
'width',
'height',
'marginTop',
'marginBottom',
'marginLeft',
'marginRight',
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
'top',
'right',
'bottom',
'left',
'fontSize',
'lineHeight',
]
for (let outKey in s) {
for (let innerKey in s[outKey]) {
if (
list.includes(innerKey) &&
typeof s[outKey][innerKey] === 'number'
) {
s[outKey][innerKey] = dp2px(s[outKey][innerKey]) as T[Extract<
keyof T,
string
>][Extract<keyof T[Extract<keyof T, string>], string>] &
number
}
}
}
return StyleSheet.create(s)
},
}
export default styleSheet
页面调用
Login/Index.tsx
import styleSheet from '../../utils/styleSheet'
const styles = styleSheet.create({
close: {
width: 25,
height: 25,
marginTop: 10,
marginLeft: 10,
}
})
这样尺寸单位就完成适配了。
自适应主题
一般手机都是支持两种模式的(light
|dark
),然后我们的页面也要支持(因为晚上看白屏确实不太舒服)根据不同的模式,来切换页面配色 先说思路:
我的理解是,我们页面构成大致可以分为三层
- 页面容器
- 布局容器
- 文字容器
如下所示:
页面布局伪代码
<页面容器>
<布局容器>
<布局容器>
<文字容器>
</文字容器>
</布局容器>
<文字容器>
</文字容器>
</布局容器>
</页面容器>
那我只要对这三种容器进行封装(内部做主题切换),然后页面布局都使用这三个组件,那不就行了吗?
useTheme.ts(获取当前主题的hook)
useTheme.ts
import { useMemo } from 'react'
import { Appearance } from 'react-native'
const useTheme = () => {
const colorScheme = Appearance.getColorScheme()
const isDark = useMemo(() => colorScheme === 'dark', [colorScheme])
const backgroundColor = useMemo(() => (isDark ? '#222222' : '#fff'), [isDark])
const color = useMemo(() => (isDark ? '#fff' : '#000'), [isDark])
return { color, isDark, backgroundColor }
}
export default useTheme
页面容器
PageView.tsx
import React from 'react'
import { View, ViewProps, StatusBar } from 'react-native'
import useTheme from '../hooks/useTheme'
import { ViewStyle } from 'react-native'
interface Props extends ViewProps {
style?: ViewStyle | Array<ViewStyle>
children?: React.ReactNode
}
const PageView = (props: Props) => {
const { backgroundColor, isDark } = useTheme()
const { children, style = {} } = props
return (
<View {...props} style={[{ backgroundColor, flex: 1 }, style]}>
<StatusBar
backgroundColor={backgroundColor}
barStyle={isDark ? 'light-content' : 'dark-content'}
/>
{children}
</View>
)
}
export default PageView
布局容器
ContainerView.tsx
import React, { memo } from 'react'
import { View, ViewProps } from 'react-native'
import useTheme from '../hooks/useTheme'
import { ViewStyle } from 'react-native'
interface Props extends ViewProps {
style?: ViewStyle | Array<ViewStyle>
itemKey?: any
children?: React.ReactNode
}
const ContainerView = (props: Props) => {
const { children, style = {}, itemKey } = props
const { backgroundColor } = useTheme()
return (
<View {...props} key={itemKey} style={[{ backgroundColor }, style]}>
{children}
</View>
)
}
export default memo(ContainerView)
文字容器
ContainerText.tsx
import React from 'react'
import { Text, TextProps } from 'react-native'
import useTheme from '../hooks/useTheme'
import { TextStyle } from 'react-native'
interface Props extends TextProps {
style?: TextStyle | Array<TextStyle>
key?: any
children?: React.ReactNode
}
const ContainerText = (props: Props): React.JSX.Element => {
const { color } = useTheme()
const { children, style = {}, key } = props
return (
<Text ellipsizeMode="tail" {...props} key={key} style={[{ color }, style]}>
{children}
</Text>
)
}
export default ContainerText
页面使用
Page.tsx
import React from 'react'
import { Image, Pressable } from 'react-native'
import PageView from '../../components/PageView'
import SearchView from '../../components/SearchView'
import ContainerView from '../../components/ContainerView'
import ListView from './components/ListView'
import styleSheet from '../../utils/styleSheet'
import { useNavigation } from '@react-navigation/native'
const Home = () => {
return (
<PageView style={styles.container}>
<ContainerView style={styles.item}>
<Pressable style={styles.searchView}>
<SearchView />
</Pressable>
<Image
style={styles.icon}
source={require('../../assets/publicImg/sign.png')}
/>
</ContainerView>
<ListView />
</PageView>
)
}
const styles = styleSheet.create({
container: {
paddingTop: 10,
},
item: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: 'row',
},
searchView: {
flex: 1,
paddingLeft: 20,
paddingRight: 20,
},
icon: {
width: 20,
height: 20,
marginLeft: 10,
marginRight: 10,
},
})
export default Home
组件使用
Component.tsx
import React, { useState, memo } from 'react'
import { View, Image, Text } from 'react-native'
import ContainerView from '../../../components/ContainerView'
import ContainerText from '../../../components/ContainerText'
import styleSheet from '../../../utils/styleSheet'
import useTheme from '../../../hooks/useTheme'
const ArticleCard = (props: any): React.JSX.Element => {
props
const [row, setRow] = useState(3)
const { isDark } = useTheme()
return (
<ContainerView style={styles.container}>
{/* 1 */}
<View style={styles.userInfo}>
<Image
src="https://p3-passport.byteacctimg.com/img/mosaic-legacy/3796/2975850990~200x200.awebp"
style={styles.headPortrait}
/>
<View style={styles.info}>
<ContainerText style={styles.name} numberOfLines={1}>
kf_007
</ContainerText>
<ContainerText numberOfLines={1}>前端在线炒粉 三星期前</ContainerText>
</View>
<ContainerText>...</ContainerText>
</View>
{/* 2 */}
<View style={styles.content}>
<ContainerText numberOfLines={row}>
根据上一条沸点中兄弟们的建议,挑了两天的主机终于挑好了,结果刚送到家才拆开看一眼就被老婆给劝退了[绝望的凝视]
说买个这么大的机箱,把家里当网吧了...
说给我换个mac的全家桶,我说那玩意打游戏麻烦,光显示器就1w5了,这好钢不能用在刀把上。
所以问问兄弟们有没有其他方案推荐的,想搞个4070系的显卡的,准备打打黑神话,目前看了雷神的那个mini主机还有拯救者Y9000的今年新款...
</ContainerText>
<View>
{row === 3 ? (
<Text style={styles.open} onPress={() => setRow(0)}>
展开
</Text>
) : (
<Text style={styles.close} onPress={() => setRow(3)}>
收起
</Text>
)}
</View>
</View>
{/* 3 */}
{/* <View style={styles.coverContent}>
<Image
style={styles.cover}
src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/593c1df83f48400ba5723e2a355aaf25~tplv-k3u1fbpfcp-jj-mark:253:253:253:253:q75.avis"
/>
</View> */}
{/* 4 */}
<View style={styles.bottomContent}>
<View style={styles.view}>
<Image
style={styles.icon}
source={
isDark
? require('../../../assets/publicImg/share-dark.png')
: require('../../../assets/publicImg/share-light.png')
}
/>
<ContainerText>分享</ContainerText>
</View>
<View style={styles.view}>
<Image
style={styles.icon}
source={
isDark
? require('../../../assets/publicImg/commet-dark.png')
: require('../../../assets/publicImg/commet-light.png')
}
/>
<ContainerText numberOfLines={1}>2015</ContainerText>
</View>
<View style={styles.view}>
<Image
style={styles.icon}
source={
isDark
? require('../../../assets/publicImg/like-dark.png')
: require('../../../assets/publicImg/like-light.png')
}
/>
<ContainerText numberOfLines={1}>300</ContainerText>
</View>
</View>
</ContainerView>
)
}
const styles = styleSheet.create({
container: {},
userInfo: {
display: 'flex',
flexDirection: 'row',
marginTop: 10,
marginBottom: 10,
},
headPortrait: {
width: 48,
height: 48,
borderRadius: 24,
marginRight: 10,
},
info: {
flex: 1,
display: 'flex',
paddingRight: 80,
},
name: {
fontWeight: 600,
fontSize: 16,
height: 28,
verticalAlign: 'middle',
},
label: {
height: 20,
verticalAlign: 'middle',
},
content: {
marginTop: 8,
paddingLeft: 25,
paddingRight: 25,
},
contentText: {
fontSize: 14,
},
open: {
marginTop: 5,
color: '#1e80ff',
},
close: {
marginTop: 5,
color: '#1e80ff',
},
coverContent: {
display: 'flex',
flexDirection: 'row',
backgroundColor: 'blue',
marginTop: 5,
marginBottom: 5,
},
cover: {
width: 100,
height: 100,
borderRadius: 3,
},
bottomContent: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
height: 36,
marginTop: 10,
},
view: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 20,
paddingRight: 20,
maxWidth: 100,
},
icon: {
marginRight: 5,
width: 20,
height: 20,
},
})
export default memo(ArticleCard)
这样大部分的页面适配工作就完成了,基本不用怎么加代码,至于特殊的配色,可以自行调用useTheme
,再调整。
开屏封面
为啥需要开屏封面,是因为在应用初始化的时候,react要去加载资源,这中间页面会呈现一个空白的状态,这样就有点不太合适了,最好遮挡一下,所以开屏封面它来了。
我使用的react-native-splash-screen
插件 这个插件因为太久远了,代码是用java
写的,但是最新的react-native
的android
都是使用Kotlin
来开发的,所以示例代码要改一下,主要就是下面文件(不要重写oncreate
方法,真没用,这个解决方案我是在issues
里面找到的)
android\app\src\main\java\com\juejin\MainActivity.kt
MainActivity.kt
package com.juejin
import com.facebook.react.ReactActivity
import org.devio.rn.splashscreen.SplashScreen // 这行
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
class MainActivity : ReactActivity() {
init{
SplashScreen.show(this)
} // 这三行
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "juejin"
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
}
MainApplication.kt
这个文件,不要去添加SplashScreenReactPackage
包,你手动引入,他会引入两次的,编译的时候,代码就报错了,至于其他的,都按照引导代码
来就行了。 最后在android\app\src\main\res\drawable
添加一个launch_screen.png
就行了
App.tsx
这样使用
App.tsx
import React, { useEffect } from 'react'
import SplashScreen from 'react-native-splash-screen'
function App(){
useEffect(() => {
SplashScreen.hide()
}, [])
}
页面鉴权
一个应用某些页面是一定需要用户登录之后,才能访问的,比如(我的阅读记录,标签管理等等),那我们在react
中怎么做呢? 我想的封装一个鉴权组件(这个组件接收一个参数--->需要被鉴权组件) 组件内部做什么?
- 获取用户信息
- 根据用户信息是否存在,来进行不同的逻辑渲染,存在则渲染组件,不存在,则渲染登录组件
鉴权组件
Authentication.tsx
import React from 'react'
import PageView from './PageView'
import Login from '../views/Login/Index'
import userInfoManger from '../utils/userInfo'
interface Props {
children: React.ReactNode
}
const Authentication = (props: Props) => {
const userInfo = userInfoManger.userInfo
const { children } = props
return userInfo ? <PageView>{children}</PageView> : <Login />
}
export default Authentication
具体使用
<Authentication><pageComponent></pageComponent></Authentication>
App.tsx
import { NavigationContainer } from '@react-navigation/native'
import Authentication from './src/components/Authentication'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import Weal from './src/views/Weal/Index'
const Stack = createNativeStackNavigator()
function App(){
return (
<NavigationContainer>
<Stack.Navigator
screenOptions={{
headerShown: false,
}}>
<Stack.Screen name="Weal">
{() => {
return (
<Authentication>
<Weal />
</Authentication>
)
}}
</Stack.Screen>
</Stack.Navigator>
</NavigationContainer>
)
页面和组件开发
上述页面和组件都已经开发完了(纯静态,没敢调用它的接口,怕ip
给我封了),开发了这几天,发现react
和vue
还是有很大的不同,react
有种万般皆函数
的感觉,就比如页面鉴权那里,我去找了@react-navigation/native
的文档,看有没有全局拦截(类似于vue-router
的beforeEach
),结果它给出了这种示例代码:
auth-flow.tsx
isSignedIn ? (
<>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
<Stack.Screen name="Settings" component={SettingsScreen} />
</>
) : (
<>
<Stack.Screen name="SignIn" component={SignInScreen} />
<Stack.Screen name="SignUp" component={SignUpScreen} />
</>
);
后面参考这个写出了鉴权组件 ,还有一点就是循环渲染的组件 ,比如:一个页面有5
个tab
,然后每次切换到其他的tab
,我另外已经被加载出来的组件都会重新渲染,后来看了文档,说是父组件修改,子组件也会被重新渲染,如果确认props
没有变化,可以使用memo
来一层记忆包装,第一次开发,问题是真多,不过react
给我的感觉是真的灵活,之前在用vue
开发用的各种指令(v-if
v-for
等等),这边都是纯粹js
来实现,甚至使用if
来实现鉴权组件,后面继续在学习,代码有时间我传到github
上面去,大家可以看看。