ReactNative仿某租車軟件

關於React-Native

循例都要介紹下React-Native,下面簡稱RN。 RN是讓你使用Javascript編寫的原生移動應用。它在設計原理上和React一致,經過聲明式的組件機制來搭建豐富多彩的用戶界面。javascript

中文文檔css

本文分爲如下幾點

  • 搭建RN環境
  • 封裝一些公共方法,請求,本地存儲
  • 使用typescript
  • 使用redux狀態管理工具
  • 使用iconfont
  • BackHandler
  • 頁面效果
  • 安卓打包APK

1. 搭建RN環境

  • 安裝

其實文檔上面寫得很清楚,很友好的分了開發平臺跟目標平臺,基本上按着上面作就能夠。我用的是本身的小米Note3真機開發的。按着官網的實例一步步作。安裝很簡單html

  • 運行

react-native run-androidjava

  • 遇到的問題
  1. 找不到ANDROID_HOME環境變量

image

在當前終端下執行一次 source ~/.bash_profile,問題解決。node

  • 成功運行時

image

  • 調試
  1. react-native log-android

在終端會輸出你console.log 出來的數據,不會影響程序的運行速度。react

  1. Debug JS Remotely

搖晃手機彈出開發者選項菜單,選擇Debug JS Remotely,瀏覽器會自動打開調試頁面 http://localhost:8081/debugger-ui,Chrome 中並不能直接看到 App 的用戶界面結構,而只能提供 console 的輸出。android

  1. React Developer Tools

這個插件能夠看到頁界面的插件佈局,以及props等屬性,可是這個貌似不能看console.log的內容。ios

  1. React Native Debugger 文檔

因爲我是在真機上面調試,因此須要配置setupDevtools.jsgit

reactDevTools.connectToDevTools({
      isAppActive,
      host:'你電腦的ip地址',
      // Read the optional global variable for backward compatibility.
      // It was added in https://github.com/facebook/react-native/commit/bf2b435322e89d0aeee8792b1c6e04656c2719a0.
      port: window.__REACT_DEVTOOLS_PORT__,
      resolveRNStyle: require('flattenStyle'),
    });
複製代碼

接着,執行上面第二種的操做,打開Debug JS Remotely。這個調試比較爽,既有能console.log的也有UI佈局的,可是個人會影響程序運行,會有卡頓狀況。es6

image

  • 拔掉數據線開發

若是你不想一直插着數據線,能夠經過網絡對你的程序進行調試。第一次運行react-native run-android時須要連着數據線,以後能夠進入Dev Settings -> Debud server host & port for device填上[你開發電腦的ip]:8081,在下一次從新運行的時候能夠用react-native start運行。

image

  • Hot Reload

局部刷新

  • Live Reload

整個應用從新運行一次


2. 目錄結構

image

  • api: 相關的功能模塊接口放在一個文件下面,例如訂車相關的功能就放在aboutBookCar.js裏面。
  • assets: 放一些圖片或者字體等靜態資源。
  • common: 放公共的方法。
  • components: 放置通用組件,這裏分功能組件跟UI組件。
  • redux: redux相關。
  • styles: 公共樣式。
  • types: ts聲明文件。
  • views: 放置各個主頁面。

3. 各個主要模塊

  • http模塊
// http.js 處理請求,儲存token

import { AsyncStorage } from 'react-native'
import { login } from '../api/login'
import axios from 'axios'

async function checkStatus(response) {
  // loading
  // 若是http狀態碼正常,則直接返回數據
  if (response) {
    if (response.data.status === 1) {
      // 成功
      return response.data
    } else if (response.data.status === 2) {
      await setToken()
      return {
        status: 0,
        msg: '從新登陸'
      }
      // 從新登陸
    } else if (response.data.status === 3) {
      // 數據格式解析異常
    } else {
      // 異常狀態下,把錯誤信息返回去
      return {
        status: -404,
        msg: '網絡異常'
      }
    }
  }
}

// 存放token到storage
async function setToken() {
  const res = await login()
  AsyncStorage.setItem('token', res.token)
  return res.token
}

export const Post = async (url, params = {}) => {
  params = {
    ...params,
    lang: 'cn'
  }

  // 當不是登陸接口時,從緩存中獲取token,若不存在就調用setToken方法
  if (url !== '登陸接口') {
    const storageData = await AsyncStorage.getItem('token')
    params['token'] = storageData ? storageData : null
    if (!params.token) {
      params['token'] = await setToken()
    }
  }
  return axios
    .post(url, params)
    .then(async response => {
      const res = await checkStatus(response)
      return res.data
    })
    .catch(function(error) {
      console.log(error)
    })
}

複製代碼
  • storage模塊
// storage.js ,使用RN自帶的AsyncStorage模塊,用來儲存token

import {AsyncStorage} from 'react-native'

// 保存數據
export const storeData = async (key, param) => {
  try {
    await AsyncStorage.setItem(key, JSON.stringify(param))
  } catch (error) {
    // Error saving data
  }
}

// 讀取數據
export const retrieveData = async key => {
  try {
    const value = await AsyncStorage.getItem(key)
    if (value !== null) {
      return value
    }
  } catch (error) {
    // Error retrieving data
    return null
  }
}

複製代碼
  • 導航

導航使用 React Navigation,因爲須要用到抽屜導航跟普通的路由跳轉,這裏須要用到stack navigatorDrawer navigation結合。

  1. 建立導航
import React, { Component } from 'react'
import { createStackNavigator, createDrawerNavigator } from 'react-navigation'

import BookCar from 'views/BookCar'
import UserInfo from 'views/UserInfo'
import SelectPosition from 'views/SelectPosition'
import ReturnPosition from 'views/ReturnPosition'
import PositionDetail from 'views/PositionDetail'
import BookingCarPage from 'views/BookingCarPage'
// 側面欄
import Journey from 'views/Journey/index'
import ChargingRule from 'views/ChargingRule/index'
import Recharge from 'views/Recharge/index'
import Wallet from 'views/Wallet/index'
// 抽屜內容的組件
import DrawerScreen from 'components/Ui/CustomDrawer/index'

// 全部頁面
const AllPage = createStackNavigator(
  //設置導航要展現的頁面
  {
    BookCar: { screen: BookCar },
    UserInfo: { screen: UserInfo },
    SelectPosition: { screen: SelectPosition },
    ReturnPosition: { screen: ReturnPosition },
    PositionDetail: { screen: PositionDetail },
    BookingCarPage: { screen: BookingCarPage },
    Journey: { screen: Journey },
    ChargingRule: { screen: ChargingRule },
    Recharge: { screen: Recharge },
    Wallet: { screen: Wallet }
  },
  //設置navigationOptions屬性對象
  {
    mode: 'card', //設置mode屬性,
    headerMode: 'none' // 去掉頭部
  }
)
// 結合抽屜跟全部頁面
const DrawerNavigator = createDrawerNavigator(
  {
    Home: {
      screen: AllPage // 全部頁面
    }
  },
  {
    contentComponent: DrawerScreen, // 用來呈現抽屜內容的組件
    drawerWidth: 250
  }
)

export default class Navigator extends Component {
  constructor(props) {
    super(props)
  }
  render() {
    return <DrawerNavigator /> } } 複製代碼
  1. 應用導航
// App.tsx

import * as React from 'react'

import Navigator from './src/components/Function/Navigator'

export default class App extends React.Component {
  render() {
    return (
        <Navigator /> ) } } 複製代碼

4. typescript

以前作的項目用到typescript,感受很不錯,因此接入typescript。

  1. install npm install react-native-typescript-transformer typescript -D

  2. 配置tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
    "module": "esnext",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "rootDirs": ["./src"],
    "baseUrl": "./src",
    "jsx": "preserve",
    "alwaysStrict": true,
    "noUnusedLocals": true,
    "importHelpers": true,
    "experimentalDecorators": true,
    "lib": ["es7", "dom"],
    "skipLibCheck": true,
    "typeRoots": ["node", "node_modules/@types"],
    "outDir": "./lib"
  },
  "exclude": ["node_modules"],
  "include": ["src/**/*"]
}

複製代碼
  1. 配置 the react native packager 在項目根目錄建立文件rn-cli.config.js
module.exports = {
  getTransformModulePath() {
    return require.resolve('react-native-typescript-transformer');
  },
  getSourceExts() {
    return ['ts', 'tsx'];
  }
}
複製代碼
  1. 加上react react-native react-navigation 的聲明文件
npm install @types/react @types/react-native @types/react-navigation -D
複製代碼
  1. 爲了能夠使用絕對路徑,能夠在須要引用的目錄下建立package.json文件

例如:在api目錄下建立package.json文件

{
  "name": "api"
}
複製代碼

就能夠使用api/xxx做爲路徑

  1. 運行驗證
tsc
複製代碼

5. redux

  1. install
npm install react-redux redux redux-actions redux-thunk -S
複製代碼
  1. 修改App.tsx文件
import * as React from 'react'
import { createStore, applyMiddleware, combineReducers } from 'redux'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'

import Navigator from './src/components/Function/Navigator'
import * as reducers from './src/redux/reducers'

const createStoreWithMiddleware = applyMiddleware(thunk)(createStore)
const reducer = combineReducers(reducers)
const store = createStoreWithMiddleware(reducer)

export default class App extends React.Component {
  render() {
    return (
      <Provider store={store}> <Navigator /> </Provider>
    )
  }
}
複製代碼
  1. actions

下面將車輛列表儲存在store爲例, 在redux目錄下建立actions文件夾,actions下包含actionTypes.tsCarAction.ts

actionTypes.ts

// 聲明一下action類型
export const SET_CARLIST = 'SET_CARLIST'
複製代碼

CarAction.ts

import * as types from './actionTypes' // action類型

// action方法
export function setCarList(carList) {
  return {
    type: types.SET_CARLIST,
    carList
  }
}
複製代碼
  1. reducers

redux目錄下建立reducers文件夾,reducers下包含index.tsCarReducer.ts

index.ts

import CarReducer from './CarReducer'

export { CarReducer }
複製代碼

CarReducer.ts

import * as types from '../actions/actionTypes'

const initialState = {
  carList: [],
}

export default function counter(state = initialState, action) {
  switch (action.type) {
    case types.SET_CARLIST:
      return {
        ...state,
        carList: action.carList
      }
    default:
      return state
  }
}
複製代碼
  1. 使用,如今已經建立好基本操做,下面就是獲取store跟操做action
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as CarAction from '../../redux/actions/CarAction'
import * as UserInfo from '../../redux/actions/UserInfo'
...

// 因爲使用的ts,咱們能夠先定義好interface
interface IStoreProps {
  // 方法
  actions?: {
    setCarList: (v: CarStore.ICarItem[]) => void
  }
  // 數據
  state?: {
    carList: CarStore.ICarItem[]
  }
}

class BookCar extends React.Component<IStoreProps> {
    ...
    
    // 能夠經過 this.props.actions.setCarList()來執行action方法
    // 一樣,能夠經過 this.props.state.carList來獲取數據
}

const StateToPoprs = state => ({
  state: { ...state.CarReducer }
})

const dispatchToProps = dispatch => ({
  actions: bindActionCreators({ ...CarAction }, dispatch)
})

export default connect(
  StateToPoprs,
  dispatchToProps
)(BookCar)
複製代碼

6. 使用iconfont

  1. install
npm install react-native-vector-icons --save
複製代碼
  1. 配置android/app/build.gradle
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
project.ext.vectoricons = [
   iconFontNames: [ 'iconfont.ttf'] // Name of the font files you want to copy
]
複製代碼
  1. 獲取.ttf文件

能夠從iconfont上面獲取,能夠在上面新建一個項目,而後將須要的圖標放到項目裏面,點擊下載至本地。

image

image

  1. 文件處理

下載完畢後,將iconfont.ttficonfont.css文件放在src/assets/fonts/目錄下,同時將iconfont.ttf文件放在android/app/src/main/assets/fonts/目錄下

// iconfont.css
...
.icon-jifei:before { content: "\e602"; }

.icon-quan:before { content: "\e603"; }
...
複製代碼

咱們須要獲得一個json文件,內容以下

{
  "icon-jifei": 58882,
  "icon-quan": 58883,
  ...
}
複製代碼

其實就是將iconfont.css裏面的樣式名跟conten的值的十進制提取出來,手動轉換比較麻煩,咱們增長一個自動轉換的腳本。 在工程根目錄下新建tools/getIconfontJson/getIconfontJson.js

const path = require('path')

const oldPath = path.join('./src/assets/fonts/iconfont.css')
const newPath = path.join('./src/assets/fonts/iconfont.json')

var gen = (module.exports = function() {
  const readline = require('readline')
  const fs = require('fs')

  const fRead = fs.createReadStream(oldPath)
  const fWrite = fs.createWriteStream(newPath, {
    flags: 'w+',
    defaultEncoding: 'utf8'
  })

  const objReadLine = readline.createInterface({
    input: fRead
  })

  var ret = {}

  objReadLine.on('line', line => {
    line = line && line.trim()
    if (!line.includes(':before') || !line.includes('content')) return
    var keyMatch = line.match(/\.(.*?):/)
    var valueMatch = line.match(/content:.*?\\(.*?);/)
    var key = keyMatch && keyMatch[1]
    var value = valueMatch && valueMatch[1]
    value = parseInt(value, 16)
    key && value && (ret[key] = value)
  })

  objReadLine.on('close', () => {
    console.log('readline close')
    fWrite.write(JSON.stringify(ret), 'utf8')
  })
})

gen()
複製代碼

運行腳本以後會在assets/fonts/目錄下生成iconfont.json文件。

最後,咱們在components目錄下建立一個公共組件IconFont

// IconFont/index.tsx
import { createIconSet } from 'react-native-vector-icons'
const glyphMap = require('assets/fonts/iconfont.json')
const IconFont = createIconSet(glyphMap, 'iconfont', 'iconfont.ttf')

export default IconFont
複製代碼
  1. usage
import Icon from 'components/Ui/IconFont/index'
... 

export default class CarList extends React.Component {
    ...
    render() {
        <Icon
            name="icon-jifei"
            size={50}
            color="red"
        />
    }
}
複製代碼

7. BackHandler

  1. 監聽設備上後退事件,當我選擇了車輛點的時候彈出底部框,此時,點擊後退按鈕的時候應該是低部框消失。
  2. 當切換到別的頁面的時候,應該銷燬以前的監聽事件。

image

...

  handleBackPress = () => {}

  // 監聽事件
  backHandlerListen = () => {
    BackHandler.addEventListener('hardwareBackPress', this.handleBackPress)
  }
  // 銷燬監聽
  destroyBackHandlerListen = () => {
    BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress)
  }

  // 從新進入這個頁面觸發
  didFocus = () => {
    this.props.navigation.addListener('didFocus', payload => {
      this.backHandlerListen()
    })
  }

 // 這個頁面失去焦點觸發 
  didBlur = () => {
    this.props.navigation.addListener('didBlur', payload => {
      this.destroyBackHandlerListen()
    })
  }

  componentDidMount() {
    this.backHandlerListen()
    this.didFocus()
    this.didBlur()
  }
...
複製代碼

8. 頁面效果

頁面暫時比較粗糙,demo性質的效果,實現主要租車功能。

image

9. 安卓打包APK 文檔

  1. 使用keytool生成祕鑰
keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000
複製代碼
  1. 在工程目錄下 android/gradle.properties加上如下配置
// *****是剛纔生成祕鑰時候填寫的密碼
MYAPP_RELEASE_STORE_FILE=my-release-key.keystore
MYAPP_RELEASE_KEY_ALIAS=my-key-alias
MYAPP_RELEASE_STORE_PASSWORD=*****
MYAPP_RELEASE_KEY_PASSWORD=*****
複製代碼
  1. 配置bulid.gradle
...
android {
    ...
    defaultConfig { ... }
    signingConfigs {
        release {
            if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {
                storeFile file(MYAPP_RELEASE_STORE_FILE)
                storePassword MYAPP_RELEASE_STORE_PASSWORD
                keyAlias MYAPP_RELEASE_KEY_ALIAS
                keyPassword MYAPP_RELEASE_KEY_PASSWORD
            }
        }
    }
    buildTypes {
        release {
            ...
            signingConfig signingConfigs.release
        }
    }
}
...
複製代碼
  1. 生成APK包
$ cd android
$ ./gradlew assembleRelease
複製代碼

生成的apk文件位於生成的 APK 文件位於android/app/build/outputs/apk/app-release.apk , 能夠直接將apk放到手機安裝 5. 遇到報錯

// error
Couldn't follow symbolic link' when testing release build for Android

// fix
rm -rf node_modules && npm install
複製代碼

謝謝觀賞

image
相關文章
相關標籤/搜索