我用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上面去,大家可以看看。

相关推荐
IT ·南栀几秒前
Day18_1--Vue基础学习入门
前端·vue.js·学习
我叫白小猿11 分钟前
【日常记录-JS】HTML动态加载JS脚本
前端·javascript·html·验签·动态加载
Array[赵]21 分钟前
Vue 使用elementUI-plus el-calendar加 公历转农历 是否节假日 等
前端·javascript·vue.js
q5673152342 分钟前
如何在 Python 中测试文件修改
开发语言·前端·python·mysql·正则表达式
偷得浮生半日闲@1 小时前
Vue屏蔽打印信息
前端·javascript·vue.js
雷特IT1 小时前
前端HTML总结
前端·html
天涯学馆1 小时前
GraphQL Subscriptions与WebSocket
前端·javascript·websocket·前端框架·graphql
光影少年1 小时前
React 常用 Hooks 和使用的易错点
前端·react.js·前端框架
极客小张2 小时前
打造智能家居:用React、Node.js和WebSocket构建ESP32设备控制面板(代码说明)
前端·单片机·物联网·网络协议·react.js·node.js·智能家居
张天龙2 小时前
【SpringBoot】数据验证之分组校验
java·服务器·前端·spring boot