關於React-Native
循例都要介紹下React-Native,下面簡稱RN。 RN是讓你使用Javascript編寫的原生移動應用。它在設計原理上和React一致,通過宣告式的元件機制來搭建豐富多彩的使用者介面。
本文分為以下幾點
- 搭建RN環境
- 封裝一些公共方法,請求,本地儲存
- 使用typescript
- 使用redux狀態管理工具
- 使用iconfont
- BackHandler
- 頁面效果
- 安卓打包APK
1. 搭建RN環境
- 安裝
其實文件上面寫得很清楚,很友好的分了開發平臺跟目標平臺,基本上按著上面做就可以。我用的是自己的小米Note3真機開發的。按著官網的例項一步步做。安裝很簡單
- 執行
react-native run-android
- 遇到的問題
- 找不到
ANDROID_HOME
環境變數
在當前終端下執行一次 source ~/.bash_profile
,問題解決。
- 成功執行時
- 除錯
- react-native log-android
在終端會輸出你console.log 出來的資料,不會影響程式的執行速度。
- Debug JS Remotely
搖晃手機彈出開發者選項選單,選擇Debug JS Remotely,瀏覽器會自動開啟除錯頁面 http://localhost:8081/debugger-ui,Chrome 中並不能直接看到 App 的使用者介面結構,而只能提供 console 的輸出。
- React Developer Tools
這個外掛可以看到頁介面的外掛佈局,以及props等屬性,但是這個貌似不能看console.log的內容。
- React Native Debugger 文件
由於我是在真機上面除錯,所以需要配置setupDevtools.js
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佈局的,但是我的會影響程式執行,會有卡頓情況。
- 拔掉資料線開發
如果你不想一直插著資料線,可以通過網路對你的程式進行除錯。第一次執行react-native run-android時需要連著資料線,之後可以進入Dev Settings
-> Debud server host & port for device
填上[你開發電腦的ip]:8081
,在下一次重新執行的時候可以用react-native start
執行。
- Hot Reload
區域性重新整理
- Live Reload
整個應用重新執行一次
2. 目錄結構
- 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 navigator
和Drawer navigation
結合。
- 建立導航
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 />
}
}
複製程式碼
- 應用導航
// 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。
-
install npm install react-native-typescript-transformer typescript -D
-
配置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/**/*"]
}
複製程式碼
- 配置 the react native packager
在專案根目錄建立檔案
rn-cli.config.js
module.exports = {
getTransformModulePath() {
return require.resolve('react-native-typescript-transformer');
},
getSourceExts() {
return ['ts', 'tsx'];
}
}
複製程式碼
- 加上react react-native react-navigation 的宣告檔案
npm install @types/react @types/react-native @types/react-navigation -D
複製程式碼
- 為了可以使用絕對路徑,可以在需要引用的目錄下建立package.json檔案
例如:在api目錄下建立package.json檔案
{
"name": "api"
}
複製程式碼
就可以使用api/xxx
作為路徑
- 執行驗證
tsc
複製程式碼
5. redux
- install
npm install react-redux redux redux-actions redux-thunk -S
複製程式碼
- 修改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>
)
}
}
複製程式碼
- actions
下面將車輛列表儲存在store為例,
在redux
目錄下建立actions
資料夾,actions
下包含actionTypes.ts
和CarAction.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
}
}
複製程式碼
- reducers
在redux
目錄下建立reducers
資料夾,reducers
下包含index.ts
和CarReducer.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
}
}
複製程式碼
- 使用,現在已經建立好基本操作,下面就是獲取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
- install
npm install react-native-vector-icons --save
複製程式碼
- 配置
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
]
複製程式碼
- 獲取.ttf檔案
可以從iconfont上面獲取,可以在上面新建一個專案,然後將需要的圖示放到專案裡面,點選下載至本地。
- 檔案處理
下載完畢後,將iconfont.ttf
和iconfont.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
複製程式碼
- 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
- 監聽裝置上後退事件,當我選擇了車輛點的時候彈出底部框,此時,點選後退按鈕的時候應該是低部框消失。
- 當切換到別的頁面的時候,應該銷燬之前的監聽事件。
...
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性質的效果,實現主要租車功能。
9. 安卓打包APK 文件
- 使用
keytool
生成祕鑰
keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000
複製程式碼
- 在工程目錄下
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=*****
複製程式碼
- 配置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
}
}
}
...
複製程式碼
- 生成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
複製程式碼
謝謝觀賞