我用react-native写了个app

最近一直在学react-native,看了几天,就想去找个app练练手,然后我想自己不是经常刷掘金吗?索性就拿它当目标了,开整。

这里强烈建议,配置开发环境的时候,一定要有个稳定的科学上网的工具,不然你会卡死在这里的

项目目标(android)

  1. 应用适配
  2. 自适应主题(深色|浅色)
  3. 开屏封面
  4. 页面鉴权
  5. 部分页面开发 (首页、沸点、发现、课程、我的、登录、搜索、文章详情、沸点详情、webview内嵌、标签管理、阅读记录、内容数据、福利兑换)
  6. 公共组件开发(顶部导航栏、页面鉴权、搜索框、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

具体到我们的页面怎么开发呢? 我是这么做的(也是参考的别人的方案)

  1. 封装一个对象styleSheet,然后提供create方法。
  2. 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),然后我们的页面也要支持(因为晚上看白屏确实不太舒服)根据不同的模式,来切换页面配色 先说思路:

我的理解是,我们页面构成大致可以分为三层

  1. 页面容器
  2. 布局容器
  3. 文字容器

如下所示:

页面布局伪代码 复制代码
<页面容器>
  <布局容器>
    <布局容器>
      <文字容器>
      </文字容器>
     </布局容器>
     <文字容器>
    </文字容器>
  </布局容器>
</页面容器>

那我只要对这三种容器进行封装(内部做主题切换),然后页面布局都使用这三个组件,那不就行了吗?

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-nativeandroid都是使用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中怎么做呢? 我想的封装一个鉴权组件(这个组件接收一个参数--->需要被鉴权组件) 组件内部做什么?

  1. 获取用户信息
  2. 根据用户信息是否存在,来进行不同的逻辑渲染,存在则渲染组件,不存在,则渲染登录组件

鉴权组件

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给我封了),开发了这几天,发现reactvue还是有很大的不同,react有种万般皆函数的感觉,就比如页面鉴权那里,我去找了@react-navigation/native的文档,看有没有全局拦截(类似于vue-routerbeforeEach),结果它给出了这种示例代码:

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} />
  </>
);

后面参考这个写出了鉴权组件 ,还有一点就是循环渲染的组件 ,比如:一个页面有5tab,然后每次切换到其他的tab,我另外已经被加载出来的组件都会重新渲染,后来看了文档,说是父组件修改,子组件也会被重新渲染,如果确认props没有变化,可以使用memo来一层记忆包装,第一次开发,问题是真多,不过react给我的感觉是真的灵活,之前在用vue开发用的各种指令(v-if v-for等等),这边都是纯粹js来实现,甚至使用if来实现鉴权组件,后面继续在学习,代码有时间我传到github上面去,大家可以看看。

相关推荐
早點睡3902 分钟前
高级进阶 React Native 鸿蒙跨平台开发:react-native-device-info 设备信息获取
react native·react.js·harmonyos
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端