RN 從上手到“放棄”


RN 從上手到“放棄”

前言: react-native,相對於最近🔥的飛起的flutter,不算是一個新技術,2015年Facebook 開源,到現在已經4 5 個年頭,一直在維護當中,但是至今未發布 v1 版本,目前已經更新到0.59。 該技術目標: 跨平台實現原生應用。 GitHub start 數目: 77602(2019-5-29)。
文章內,圖片很多,占據了一定的篇幅。班門弄斧之作,若有大神見到,敬請指教,有不對不合理之處,敬請指出!我是邇伶貳!

git倉庫地址: https://github.com/adouwt/react-native-wx

正文

1、項目預覽

現在已完成的功能展示:

入手demo項目,本打算模仿微信的功能做一遍。現在已經完成微信的一級界面。截圖如下:
首頁:
通信錄:

發現:

我:

朋友圈(上拉加載和下拉刷新):
(未完成,就是調用了接口)

聊天界面:

攝像頭拍照(安卓虛擬機):


github地址: react-native-wx

nodejs后台:nodejsApi

項目主要使用插件(庫):

  1. react-native-camera (調用攝像頭)
  2. react-native-vector-icons (圖標庫)
  3. react-navigation (路由導航)

參考資料:

2、項目運行

~前提: 環境搭建及相關軟件、安卓或者ios 的模擬器安裝, 參考官網即可,https://reactnative.cn/docs/getting-started.html

git clone https://github.com/adouwt/react-native-wx.git

cd react-native-wx

npm i

npm run and (安卓)

npm run ios (蘋果)

(上面的運行命令,我在package.json 做了封裝,一些處理編譯錯誤的命令,我也已經封裝進去)

執行命令后,會自動彈出nodejs 執行終端界面,這個是程序運行的一個監控

模擬器顯示:

3、分步實現

3.1 初始化並運行項目

```
react-native init AwesomeProject
react-native run-ios
```

3.2 項目結構說明

3.3 新建文件夾 app,接下來所有的源碼文件代碼將在這里

目前新建 component組件、page頁面、及utils 工具三個,后面會根據需要建新的文件夾。

四個一級界面+ 聊天和朋友圈的界面

3.4 安裝插件做頁面導航跳轉

3.4.1 npm install react-navigation -S

3.4.2 修改項目文件下的App.js

這是根文件,我們的頁面導航寫進這個組件,我項目中已經完成代碼片段,這里直接使用,代碼如下:

import React from 'react';
import HomeScreen  from './app/page/Home'
import DiscoverScreen from './app/page/Discover'
import UserListScreen from './app/page/UserList'
import MyScreen from './app/page/My'
import CameraComponent from './app/component/camera'
import ChatScreen from './app/page/Chat'
import FriendCircle from './app/page/friendCircle'
import Icon from "react-native-vector-icons/Ionicons";
import { View, Text } from 'react-native';
import { createAppContainer, createBottomTabNavigator, createStackNavigator, createDrawerNavigator } from 'react-navigation'; // Version can be specified in package.json


const HomeNav = createStackNavigator({
  Home: {
    screen: HomeScreen,
    navigationOptions:{
      headerTitle:'微信',
      headerBackTitle:null,
    }
  },
})
const UserListNav = createStackNavigator({
  UserList: {
    screen: UserListScreen,
  },
})

// 二級頁面寫進一級頁面中
const DiscoverNav = createStackNavigator(
  {
    Discover: {
      screen: DiscoverScreen,
    },
  }
)

const MyNav = createStackNavigator(
  {
    My: MyScreen,
  }
);

let BottomNav = createBottomTabNavigator(
  // createBottomTabNavigator 兩個參數,一個頁面路由,一個是路由配置
  {
    微信: HomeNav,
    通訊錄: UserListNav,
    發現: DiscoverNav,
    我: MyNav,
  },
  {
    defaultNavigationOptions: ({ navigation }) => ({
      tabBarIcon: ({ focused, horizontal, tintColor }) => {
        const { routeName } = navigation.state;
        let iconName;
        let badgeCount = 3
        switch(routeName) {
          case '微信':
            iconName = 'ios-text';
            break;
          case '通訊錄':
            iconName = 'md-person-add';
            break;
          case '發現':
            iconName = 'md-compass';
            break;
          case '我':
            iconName = 'ios-person';
            break;
        }
        iconColor = `${focused ? '#1AAD19' : '#4D4D4D'}`;

        return (
          <View>
            <Icon name={iconName} size={18} color={iconColor}></Icon>
            { routeName === '發現' && badgeCount > 0 && (
              <View style={{
                // If you're using react-native < 0.57 overflow outside of the parent
                // will not work on Android, see https://git.io/fhLJ8
                position: 'absolute',
                right: -6,
                top: -3,
                backgroundColor: 'red',
                borderRadius: 6,
                width: 12,
                height: 12,
                justifyContent: 'center',
                alignItems: 'center',
                color: '#fff'
              }}>
                <Text style={{ color: '#fff', fontSize: 10, fontWeight: 'bold' }}>{badgeCount}</Text>
              </View>
            )}
          </View>
        )
      },
    }),
    tabBarOptions: {
      activeTintColor: '#1AAD19',
      inactiveTintColor: '#4D4D4D',
    },
  }
);

let RootNav = createStackNavigator({
  BottomNav: {
    screen: BottomNav,
    navigationOptions: ({ navigation, screenProps }) => {
      return {
        header: null,
      };
    }
  },
  Camera: {
    screen: CameraComponent
  },
  Chat: {
    screen: ChatScreen
  },
  FriendCircle: {
    screen: FriendCircle
  }
})

export default createAppContainer(RootNav);

這里需要參考導航資料:navigation

文檔講的很明白,看看示例就知道怎么用了,我下面講兩個注意內容,這也是在這幾天的學習中遇到的troubles.

a、創建底部導航:

createBottomTabNavigator 方法,接受兩個參數,一個頁面路由,一個是路由配置,
直接看這個方法名字,就知道這個是創建底部導航的方法。
--第一個參數,頁面路由,這里你寫多少tab, 底部就會呈現幾個tab 均勻分布,(不要有杠精來襲,“要是有100個tab,怎么顯示?”,哪有這樣的設計,你要是有100個tab,你試試這樣排版?)

參數的key,就是底部顯示的名稱,value 就是這個頁面 screen。頁面screen可以單獨定義引入,如下:

可以像第一個DiscoverNav,以screen定義的方式引入,也可以簡略使用,如下面的MyNav

-- 第二個參數,路由配置,在這里配置,底部導航的樣式、圖標、foucs 狀態及badge等
tabBarIcon 顧名思義,配置他的圖標,我這里根據navigation.state 里的routeName 來區分頁面路由,從而為他們配置不同的 icon

b、二級頁面注入Stack Navigator

我們寫的頁面要注入我們的導航,這樣才能訪問到,我們這里采用的是react-navigation的 createStackNavigaor 的createStackNavigator方法,如圖:

3.4.3 具體頁面邏輯

這里講兩個頁面,一個是靜態頁面,一個是調用接口的長列表的界面。

靜態頁面 discoverScreen

布局方式: flex, 屬性和web 書寫不一致,語法參考這個不完全手冊: https://shenbao.github.io/ishehui/html/RN 基礎/React Native 樣式表指南.html

點擊按鈕封裝: RN 里面的點擊方法只能綁定在它的button 組件上,提供的其他組件我們么辦法直接綁定事件,它提供了一個封裝子組件可以綁定事件的自定義按鈕-Touchable 系列 (TouchableOpacity ,TouchableNativeFeedback)如下書寫可以點擊的item:

注意: 上面划線的位置,這個樣式(flex: 1, flexDirection: 'row',)要寫上。有一定的兼容問題,如果沒有這個樣式,在安卓上無法點擊,ios上沒有影響。說明在實際開發中,我們還要處理一定的平台差異問題,真正實現無差異的跨平台還是有些困難。

頁面header:

在static 里面沒辦法直接調用組件的方法,需要借助 navigation 來做一下中轉,調用setParams將方法放進navigation里面,這樣在static里面就可以使用navigation.getParams 獲取這個方法了,如下:

過渡動畫

這個方法實現的是一個 動畫,我們在 寫web 的時候,會用 transform transaction 這樣的動畫屬性,RN里面也支持這樣的動畫,具體語法有所差異。這里我們用一個絕對定位里面的 right值 做過渡效果。
開始定義:

在點擊時候,修改這個 this.state.animateRightValue 的值,實現動畫效果,

Animated有幾個動畫(),這里采用了timing,他接受兩個參數,一個是監聽的動畫值,另一個是這個值的配置,配置動畫方式,動畫時間等。

這個頁面也沒有復雜的頁面邏輯,基本一看就知道怎么回事,一些語法 api 不會的話,可以上官網lou 一眼:

調接口的頁面 friendCircle

這個頁面調用了一個分頁接口,上拉加載更多,長列表的組件用的是RN 原生的 FlatList 組件,這個具體使用可以參考api 文檔看看,

但是就個人使用之后的感覺而言,這個真正要用到生產,還得要稍微改造一下,比如loading菊花圖片要改一改。
在生命周期函數componentDidMount 里面,調用我們的接口。說道這里,我們引出了接口封裝問題,用的是自帶的fetch,這個fetch 底層具體我們就不考慮怎么實現的,現在我們需要對fetch 封裝一下,方便后面在多處使用,fetch 封裝如下:

let base_url = 'https://api.scampus.cn';  //服務器基本地址
// let base_url = 'http://18.10.1.115:4000';  //服務器基本地址
let token = '';   
/**
 * @param {string} url 接口地址
 * @param {string} method 請求方法:GET、POST,只能大寫
 * @param {JSON} [params=''] body的請求參數,默認為空
 * @return 返回Promise
 */
const  fetchRequest = (url, method, params = '') => {
    let header = {
        "Content-Type": "application/json;charset=UTF-8",
        "accesstoken":token  //用戶登陸后返回的token,某些涉及用戶數據的接口需要在header中加上token
    };
    if(params == ''){   //如果網絡請求中帶有參數
        return new Promise(function (resolve, reject) {
            fetch(base_url + url, {
                method: method,
                headers: header
            }).then((response) => response.json())
                .then((responseData) => {
                    resolve(responseData);
                })
                .catch( (err) => {
                    reject(err);
                });
        });
    } else{   //如果網絡請求中沒有參數
        return new Promise(function (resolve, reject) {
            fetch(base_url + url, {
                method: method,
                headers: header,
                body:JSON.stringify(params)   //body參數,通常需要轉換成字符串后服務器才能解析
            }).then((response) => response.json())
                .then((responseData) => {
                    resolve(responseData);
                })
                .catch( (err) => {
                    reject(err);
                });
        });
    }
}

export default fetchRequest

使用Promise 處理異步問題,將我們最后的需要的數據統統resolve 出去。封裝中規中距,基本是按照文檔說明 fetch 的用法,稍加修改

4、使用第三方的圖標

npm install -S react-native-vector-icons
圖標地址: https://oblador.github.io/react-native-vector-icons/ 注意這站點不是圖標全部可用,滾動條快速找到中間位置,就能看到我們需要的圖標。
使用: name 值可以在上面的地址中尋找,哪個合適就用哪個,
就個人看來,這個圖標庫基本夠開發使用,如果不夠可以繼續引用字體圖標庫。

5、調用手機硬件設備-攝像頭

具體演示實例,拍照功能,用的第三方庫,react-native-camera
安裝: npm install -S react-native-camera
使用:import { RNCamera } from 'react-native-camera';

<View style={styles.container}>
    <RNCamera
        ref={ref => {
            this.camera = ref;
        }}
        style={styles.preview}
        type={ this.state.cameraType}
        flashMode={RNCamera.Constants.FlashMode.on}
        autoFocus={RNCamera.Constants.AutoFocus.on}
        androidCameraPermissionOptions={{
            title: 'Permission to use camera',
            message: 'We need your permission to use your camera',
            buttonPositive: 'Ok',
            buttonNegative: 'Cancel',
        }}
        androidRecordAudioPermissionOptions={{
            title: 'Permission to use audio recording',
            message: 'We need your permission to use your audio',
            buttonPositive: 'Ok',
            buttonNegative: 'Cancel',
        }}
        onGoogleVisionBarcodesDetected={({ barcodes }) => {
            console.log(barcodes);
        }}
    >
        {({ camera, status, recordAudioPermissionStatus }) => {
            if (status !== 'READY') return <PendingView />;
            return (
            <View style={{ flex: 1, flexDirection: 'row', justifyContent: 'space-around',marginBottom: 20 }}>
                <TouchableOpacity onPress={() => this.takePicture(camera)} style={styles.capture}>
                    <Text style={{ fontSize: 14 }}> 拍照 </Text>
                </TouchableOpacity>
                <TouchableOpacity onPress={this.swtichCamera} style={styles.capture}>
                    <Icon name="ios-reverse-camera" size={18} color="#333"></Icon>
                </TouchableOpacity>

                <TouchableOpacity onPress={this.lookAlbum} style={styles.imgPreview}>
                        <Image
                            style={styles.imgPreview}
                            source={{uri: this.state.currentUri || 'https://yyb.gtimg.com/aiplat/page/product/visionimgidy/img/demo6-16a47e5d31.jpg?', isStatic: true}}
                        />
                    </TouchableOpacity>
                    
                </View>
                );
            }}
        </RNCamera>
    </View>

在組件里面可以定義照相機界面的ui,可以自定義拍照按鈕,切換攝像頭的按鈕,拍照圖片預覽等,調用api 不難,問題難點在配置調用的文件,你得有權限調用原生的設備。
1、修改android/gradle/wrapper/gradle-wrapper.properties

    distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip  

2、修改android/app/build.gradle

missingDimensionStrategy 'react-native-camera', 'general'

在實際安裝使用的時候會有相關的提示報錯,按照報錯信息去尋找解決辦法,這里還是得推薦github,上面有很多類似的問題,可以耐心找找。

6、結言

從一開始了解RN 到最后上手demo,到現在陸續修改項目,差不多十天時間,本人的技術棧是vue,react 並沒有生產項目,看看文檔,基本可以上手。總結而言,使用一些基本的功能,並不難,文檔很全,使用的群體很大,所以遇到的問題也可以在相關社區找到合適的解決方法或者替換方案。還沒有具體開發生產項目,但是我覺得我將要面臨的問題,應該在體驗優化上,比如過渡動畫,上拉下拉刷新加載,切換視圖;集成第三方庫,調用硬件設備;性能優化問題等。

7、TODO

后面有時間,繼續把這個項目做下去,

  • 登錄注冊
  • 聊天,后面集成聊天機器人
  • 通訊錄的人員分組,現在因為是后台接口還沒有完成,只是本地造了一個數據
  • 掃碼功能
  • 發動態
  • 集成地圖
  • 拍照后,圖像識別

如果有興趣的同學歡迎加入一起完成。

github地址: react-native-wx

nodejs后台:nodejsApi

班門弄斧之作,若有RN 老手見到,請勿見笑,有不對不合理之處,敬請指教!我是邇伶貳!

-1、相關錯誤處理

  • react-native-camera 插件的使用問題:

解決: [解決](https://github.com/react-native-community/react-native-camera/issues/2150)
  • 編譯問題

    解決: cd android   &&  ./gradlew clean
  • Unable to resolve module 'scheduler/tracing' in ReactNative

    解決: yarn add @babel/runtime@7.0.0 再重新跑 react-native run-android


開發中還遇到了其他問題,但是忘了做記錄 ~~ RN 暫時放一段落,接下來要使用 flutter,打算兩周后 出一個flutter版本。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM