阅读 398

跨端应用--ReactNative开发

前言

    React Native使你只使用JavaScript也能编写原生移动应用AndroidiOS。 想必前端跨平台都听说过现在现在比较火热的FlutterReactNativeWeexFlutter它可能是使用了dart语言也是跨端,然后是一种新的语法可能对前端来说得重新学习一遍。而ReactNativeWeex它两本身就是通过前端ReactVue语法,只要会这两个这两个框架还是很好上手的。ReactNative 它在设计原理上和React一致,通过声明式的组件机制来搭建丰富多彩的用户界面。稍微区别的就是style样式的写法稍微有点区别感觉剩下的都是跟React一致。

环境搭建

    因为是跨端项目需要安卓环境以及iOS的开发环境,目前我自己就只是搭建了安卓的环境。

    必须安装的依赖有:Node、JDK Android Studio。 这里有关Node以及JDK和安卓的的环境搭建就不一一介绍了,网上都是有的,官网也是有的。

(这里注意的是node的版本以及jdk的版本还有就是安卓中SDK的版本)

  • node >=12
  • jdk 1.8 (必须)
  • Android SDK Platform 29

官网ReactNative安卓环境搭建(官方文档): reactnative.cn/docs/enviro…

初始化项目

通过node安装带的npx初始化项目

npx react-native init AwesomeProject
复制代码

这里的AwesomeProject就是项目的名称, 也可以通过react-native-cli初始化项目,但是这个必须是全局安装了react-native-cli,感觉是还是npx来的方便所以我也是选择这个。这个我们默认初始化react-native的最高版版本,我们也可以通过--version X.XX.X来指定项目react-native的版本。

还有就是我们可以通过--template来指定下载项目初始化的指定模板,比如官网有示列写的使用typescript的版本

npx react-native init AwesomeTSProject --template react-native-template-typescript
复制代码

因为到接触到react-native开发APP项目自己也是开发好几个,但是每次新建项目都是通过复制之前的项目的依赖来做项目的初始化感觉很麻烦所以自己也手动根据自己需要基础依赖来搭建了一个初始化的模板。

npm包地址:www.npmjs.com/package/rea…

github地址:github.com/jetBn/react…

该模板主要是配置了一些eslint和一些项目基本依赖配置信息,比如使用Antreact-native的UI框架以及网络请求,上传文件等等。 具体依赖以及文件目录信息如下:

主要入口文件

项目主要入口是index.js但是我们页面有很多页面,所以项目中使用React Navigation承接页面中tabbar以及子页面等等。

React Navigation(官方文档): reactnavigation.org/

创建TabBar主要是是用React Navigation@react-navigation/bottom-tabs这个包官方文档中都是有详细的创建说明,所以这里也不列举说明了,我相信这个文档还是难不倒我们的,虽然文档是英文的。

在此记录下自定义tabBar

import React, { useEffect, useState } from 'react'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' // 引入创建底部Tab主包

const Tab = createBottomTabNavigator() // 获取组件

const MyTabBar=({ state, descriptors, navigation }) => {
  const focusedOptions = descriptors[state.routes[state.index].key].options

  if (focusedOptions.tabBarVisible === false) {
    return null
  }
  
  return (
   // 主要tabbar版块布局
    <View
      style={{
        // overflow: 'hidden',
        height: calc(50, 'height'),
        flexDirection: 'row',
        justifyContent: 'space-between',
        alignItems: 'center',
        borderTopColor: '#F4F4F4',
        backgroundColor: '#FFFFFF',
        borderTopWidth: 1,
        paddingBottom: isIPhoneX() ? 16 : 0, // 判断是否为isIPhoneX
        position: 'relative'
      }}>
      
      // 这里我们获取我在Tab.Navigator中定义的Tab.Screen循环出来
      {state.routes.map((route, index) => {
      
        const { options } = descriptors[route.key]
        // console.log(options.tabBarIcon())
        
        //这里判断我们是否有些tabBarLabel没有写就是用title再没有就是用route的name
        const label =
          options.tabBarLabel !== undefined
            ? options.tabBarLabel
            : options.title !== undefined
            ? options.title
            : route.name

        const isFocused = state.index === index //判断是否是是选中的tab

        //点击事件
        const onPress = () => {
          const event = navigation.emit({
            type: 'tabPress',
            target: route.key
          })

          if (!isFocused && !event.defaultPrevented) {
            navigation.navigate(route.name)
          }
        }
        
        //长按事件
        const onLongPress = () => {
          navigation.emit({
            type: 'tabLongPress',
            target: route.key
          })
        }
    
       // 返回每一项通过TouchableOpacity
        return (
          <TouchableOpacity
            accessibilityRole="button"
            accessibilityStates={isFocused ? ['selected'] : []}
            accessibilityLabel={options.tabBarAccessibilityLabel}
            testID={options.tabBarTestID}
            onPress={onPress}
            onLongPress={onLongPress}
            key={index}
            style={{
              // flex: ,
              width: calc(150),
              justifyContent: 'center',
              alignItems: 'center'
            }}>
            {options.tabBarIcon({ focused: isFocused })}  
            <Text
              style={{
                color: isFocused ? '#222222' : '#777777',
                fontSize: sT(11),
                textAlign: 'center',
                fontWeight: '500',
                marginTop: calc(6)
              }}>
              {label}
            </Text>
          </TouchableOpacity>
        )
      })}
      
      // 由于上面是我们定义布局flex这里我定义我们自定义的图标定位在中间
      <View
        style={{
          width: calc(83),
          height: calc(83),
          position: 'absolute',
          left: '50%',
          marginLeft: -calc(41),
          bottom: calc(5)
        }}>
        <TouchableOpacity
          activeOpacity={1}
          onPress={() => {
            getToken().then((res) => {
              if (res === '') {
                navigation.navigate('Login')
              } else {
                showImgPikcer()
              }
            })
          }}>
          <Image
            style={{
              width: '100%',
              height: '100%'
            }}
            source={require('../../assets/images/home_tab_scanning.png')}
          />
        </TouchableOpacity>
      </View>
    </View>
  )
}


const  Tabs = () => {
 return (
    <Tab.Navigator
      tabBar={(props) => <MyTabBar {...props} />} //将tabBar中主要的pops传入我自定义的方法
      tabBarOptions={{
        labelStyle: { bottom: 5 },
        keyboardHidesTabBar: true,
        allowFontScaling: false
      }}>
      <Tab.Screen
        name="Home"
        component={Home}
        options={{
          tabBarLabel: '首页',
          title: '首页',
          tabBarVisible: isShowTabbar,
          tabBarIcon: ({ focused }) => {
            if (focused) {
              return (
                <Image
                  source={require('../../assets/images/home_tab_file_s.png')}
                />
              )
            } else {
              return (
                <Image
                  source={require('../../assets/images/home_tab_file_n.png')}
                />
              )
            }
          }
        }}
      />
      <Tab.Screen
        name="Mine"
        component={Mine}
        options={{
          tabBarLabel: '我的',
          tabBarIcon: ({ focused }) => {
            if (focused) {
              return (
                <Image
                  source={require('../../assets/images/home_tab_user_s.png')}
                />
              )
            } else {
              return (
                <Image
                  source={require('../../assets/images/home_tab_user_n.png')}
                />
              )
            }
          }
        }}
      />
    </Tab.Navigator>
  )

}
export default Tabs


复制代码

在此我们定义的tab.js文件只需在我的模板中router文件夹下的index.js中路由栈中引入使用就可以了,效果图如下图。

image.png

基本的页面创建方式是在index.js中引入@react-navigation/stack包这里我使用分出来文件的形式在main文件夹下写我们所有的页面文件配置。

image.png

image.png

image.png

这里配置了webview的页面在screenprops配置中我可以配置页面的信息比如标题栏颜色,标题栏左侧,右侧显示等等,也可以自定义标题栏。

网络请求封装

在项目中我主要使用axios作为网络请求的封装,具体主要实现网络请求以及响应的拦截器。主要是实现了网络请求中参数使用qs封装变成form表单请求数据,以及响应结果中全局判断是否正常返回数据。


const service = axios.create({
  baseURL: BASE_URL, // api 的 base_url
  timeout: 60000, // 请求超时时间
  header: {
    'Content-Type':'application/x-www-form-urlencoded',
  }
})

// request拦截器
service.interceptors.request.use(
  async config => {
    if(config.method === 'post') {
      config.data = Qs.stringify(config.data)
    }
    
    config.headers["token"] = await getToken()

 // 获取网络的状态
    NetInfo.fetch().then(state => {
      if(!state.isConnected) {
        Toast.show('网络连接失败', {
          duration: Toast.durations.LONG,
          position: 0,
          shadow: true,
          animation: true,
          hideOnPress: true,
          delay: 1000
        })
      }
    });
    console.log(config.headers)
    return config
  },
  error => {
    console.log('error', error)
    Promise.reject(error)
  }
)

// response 拦截器
service.interceptors.response.use(
  response => {
    console.log(response.data.status)
    if(typeof response.data.status !=='undefined' && response.data.status !== 200000500){
      return response.data.data
    }
    if(typeof response.data ==='object' && typeof response.data.status === 'undefined') {
      return response.data
    }
    // console.log(response.status) 
    Toast.show(response.data.message)
    return Promise.reject(response.data)

  },
  error => {
    if (error.response.status == 401) {
   
    }
    return Promise.reject(error)
  }
)

export default service

复制代码

文件上传封装

因为我这边网络请求用的是axios当时文件上传一下就懵了,不知道该怎么弄,毕竟手机上跟网页上还是有差别的。因为axios也是在网页上使用的。因为在网页上选择的文件都是使用blob上传的。而我在手机上选择的文件都是file开头的文件路径或者就直接返回文件路径。而且后端的api也是通过网页中上传文件的方式接收对应文件来进行上传。

当时就想这怎么转成跟前端网页的一样形式,最后还是也是找到了一个fetch封装的包rn-fetch-blob,它支持文件上传下载。

github地址:github.com/joltup/rn-f…

我自己通过Promise的方式封装了图片文件上传等方法

image.png

黑暗模式适配

由于使用React Navigation所以我也是直接使用官方提供的react-native-appearance包中提供的hook方法(useColorScheme),并且使用此包中提供AppearanceProvider包裹整体的路由组件。

  1. 定义一个主题的config配置信息在config文件下,然后对应darklight模式下颜色配置,然后我需要通过useColorScheme中获取的值直接对应这个Object的字段key就可以了,配置文件如下

页面中使用

image.png 2. 手动设置与自动设置配置,如果当我们设置了自动跟随系统还好,就直接通过这个hook我们就能获取对应的颜色信息了,但是如果用户设置了手动那我们又得重新判断。我的做法就是设置要给key存到缓存中,然后在index.js中也就是主的入口中通过hookuseEffect获取缓存中的key判断是否为自动还是手动,如果是手动就获取对应的value通过Appearance.set方法设置我们的主题颜色。

image.png

这里的Appearance是通过react-native-appearance导入的。

页面适配方法

由于是跨端的然后手机也是各式各样这个页面适配的js是必不可少的,不然在不同的手机上就会产生页面bug显示不一。主要是通过基准的设计稿作为固定参数然后再通过api获取设备的屏幕宽高进行相除获取对应值然后再乘设置的值。

image.png

还有就是横屏的适配,其实就是判断是否为横屏然后再通过将基准的宽度设置为屏幕的高度,这样横过来也就能就进行页面的适配了。

总结

react-native开发中主要的还是环境的搭建可能比较费时。在写法中跟react中基本都是一样的,除了样式的编写,native中主要用style的形式用Object的形式,写多了感觉跟写css就剥离了,之前写多了react-native的样式,然后回去写css就会总会写成native中一样写Object的形式。

还有就是现在的react-native使用第三的包都不用手动的link了,现在自动会关联进去,所以使用还是很方便的。虽然有时候使用的还是有各种bug所以尽量不要去使用很久以前的第三方包。😂😂

文章分类
前端