Redux for react native 指南

giants發表於2018-09-27

前言

如果要看理論的童鞋點選這裡 redux中文文件 或者 redux官方文件 ,本文不會太刻意去介紹大篇幅的理論,本文不做框架之間的對比,只給想學redux的童鞋提供實質的、高效的、易理解的學習參考資源,分享自己在學習過程中的得到。文章更新完後會比較長,請耐心閱讀理解,仔細品味。不熟悉redux也沒關係,可以跟著文章思路,將三個demo敲完,相信你一定獲益匪淺。(文後有彩蛋 )。

已更新內容

  • redux 基本使用 (附demo)
  • redux Middleware使用(附demo)
  • redux 整合 navigation (附demo)

redux 基本使用
模仿官方 Todos demo 的react native版。

redux Middleware使用
模仿官方 async demo 的react native版。

整合react-native-navigation後ios演示圖
整合react-native-navigation後android演示圖

整合react-native-navigation後把前兩個domo綜合。

待更新內容

  • 待續......

為什麼我要寫這個demo

有的童鞋可能會有疑問

問:官方不是Todosdemo嗎?為什麼還要寫這個demo?

答:官方的demo都是react的,而並非react native的。我也找過很多關於介紹redux的文章,但我發現找到的資料要麼太基礎、要麼介紹不全面、提供的demo下載無法使用等等各種問題,迫使我有了自己動手造輪的衝動,而且這個demo並非只是介紹關於redux的基礎的東西,而是通過三套demo實踐連貫的圖文的方式,讓讀者更好的理解,後面還會陸續更新在使用redux過程中的得到,希望大家鼓勵支援。

demo採用的程式碼規範

通常一個大專案到後期是需要很多開發者參與的,如果每個開發者都使用自己的一套程式碼規範做事情,這樣帶來的後果就是:後期的程式碼管理工作帶來非常大的麻煩,浪費更多的時間去重構,而且也會讓新人看程式碼時理解花更多的時間,還容易把別人帶溝裡去,所以一個大型專案最初構建架構的時候就必須要遵守一些規範。

那麼我們怎麼能敲出清爽而又優雅的程式碼呢?又如何檢查我們程式碼質量合格呢? 我在這裡極力推薦遵守airbnb/javascript的規範和使用eslint來檢查自己程式碼的程式碼質量(是否遵守了規範),因為它們已經得到了很多公司和開發者的認可。(這裡過多的介紹airbnb eslint,本文只提供思路,想了解更多自行搜尋)

在沒有使用程式碼規範前我們可能用各自的風格寫了很多年的程式碼了,突然要適應這套規範可能非常不適應,沒關係,多敲多練習,時間長了就習慣了,誰還沒有一個過程,過程是痛苦的,但痛苦過後會給你帶來質的昇華,自己慢慢領悟體會吧。好的事物東西是會被世界所接受,差的事物最終是要被替代的,所以做為一個合格的程式設計師(特別是前端程式設計師)要擁抱變化,因為它會使你變得更加的優秀,得到大眾的認可,除非你不願意讓自己變得更優秀。

redux能幫我們做什麼

兩張圖示意:

未使用redux的大型react native專案
使用redux後的大型react native專案

redux特性

  • 單一資料來源: 整個應用的 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中。

  • State 是隻讀的:唯一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通物件。

  • 使用純函式來執行修改:為了描述 action 如何改變 state tree ,你需要編寫 reducers

  • 預見性:所有的使用者的行為都是你提前定義好的。

  • 統一管理state:所有的狀態都在一個store中分配管理。

哪些開發者和專案適合用redux

這裡只針對react native開發而言:

  • 初級:剛接觸react native我非常不建議去使用,因為你還不知道怎麼用它,建議先達到中級。

  • 中級:使用react native做出一個以上已經上架的不復雜的應用 redux,也可以不使用,因為使用它並不能讓你在前期快速的迭代開發,在這樣的專案下使用redux就好比大炮打蚊子,副作用很大。但是可以先了解起來,並發現它的優點。這類相對簡單的應用:當使用者觸發一個動作(程式需要setState({xxx:xxx}))的時候應用程式狀態流程是這樣的:

    簡單的狀態流程

  • 高階:使用react native做出一個以上已經上架的複雜的應用(涉及到即時通訊、介面佈局比較複雜,元件巢狀太多層次等),而這類複雜應用:當使用者觸發一個動作(程式需要setState({xxx:xxx}))的時候應用程式狀態流程是這樣的:

    複雜的狀態流程

這種狀態帶來的後果,兩方面分析:

  • 效能:祖父子元件之間多餘的狀態傳遞,導致寶貴的記憶體資源浪費,同時介面渲染的速度也會變慢,自然使用者體驗就變差了。
  • 狀態管理:當程式不斷的迭代,介面佈局越來越複雜,必然就會產生許多的state狀態,那你是如何有效的管理這些狀態?是什麼原因導致UI多次渲染?是哪一步操作導致的UI元件的變化?在沒有使用redux前你可能已經發現可以使用生命週期函式中的shouldComponentUpdate來減少子元件中沒必要的渲染,但終究解決不了狀態管理複雜的難題。 當你使用redux後,複雜的應用程式狀態流程是這樣的:
    使用redux後
    看完上面圖文後,是否很直觀的理解了怎樣的專案才適合用redux呢,這要感謝@justjavac文章提供的動圖支援。

redux for react native 工作邏輯圖

感謝@黑森林工作室作者提供的清晰的邏輯圖

清晰邏輯圖

redux工程結構分析

我對官方的demo小部分位置做了些改造具體看程式碼分析:

image.png

分工明細

  • js/actions 此資料夾下放內容做的事情是:定義使用者行為。
  • js/reducers 此資料夾下放內容做的事情是:響應使用者行為,返回改變後的狀態,併傳送到 store
  • js/components 此資料夾下放內容做的事情是:自定義的元件。
  • js/containers 此資料夾下放內容做的事情是:把components資料夾中涉及到狀態變化的元件進行第二次封裝。
  • App.js 入口檔案(store在這裡),為什麼我要把store定義在這裡? 因為它是唯一的,而且必須使用react-redux提供的Provider元件包裹入口的其他元件才能使redux中的store生效。
  • global.js 存放全域性定義的變數、常量、方法等。

需要注意的事

  • 一個工程中 reduxstore 是唯一的,不能在多個 store
  • 保持 reducer 純淨非常重要。永遠不要在 reducer 裡做這些操作:
  • 修改傳入引數;
  • 執行有副作用的操作,如 API 請求和路由跳轉;
  • 呼叫非純函式,如 Date.now()Math.random();
  • 使用物件展開運算子...代替Object.assign()才是最好的解決方案。
  • 元件名首字母要大寫,也就是說componentscontainers資料夾下的檔案首字母都要大寫。
  • 應該儘量減少傳遞到action 中的資料(能傳單個資料就不傳物件,能傳物件就不傳陣列)
//good
function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}
複製程式碼
//best
function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return { ...state, visibilityFilter: action.filter }
    default:
      return state
  }
}
複製程式碼

程式碼詳解

js/actions/types.js

//新增列表資料
export const ADD_TODO = 'ADD_TODO';
//篩選
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
//文字新增/取消中劃線
export const TOGGLE_TODO = 'TOGGLE_TODO';
複製程式碼

釋:

action定義

為什麼我要把使用者的action(行為)定義單獨抽出來寫一個type.js

  • 方便狀態管理。
  • 複用性。

js/actions/index.js

import  {
    ADD_TODO,
    SET_VISIBILITY_FILTER,
    TOGGLE_TODO,
} from './types'
let nextTodoId = 0;

export const addTodo = text => ({
    type: ADD_TODO,
    id: nextTodoId++,
    text
});

export const setVisibilityFilter = (filter) => ({
    type: SET_VISIBILITY_FILTER,
    filter
});

export const toggleTodo = id => ({
    type: TOGGLE_TODO,
    id
});
複製程式碼

釋:

Action 建立函式

Action 建立函式 就是生成 action 的方法。“action” 和 “action 建立函式” 這兩個概念很容易混在一起,使用時最好注意區分。

Redux 中的 action 建立函式只是簡單的返回一個 action:


js/reducers/todos.js

import  {
    ADD_TODO,
    TOGGLE_TODO,
} from '../actions/types'

const todos = (state = [], action) => {
    let {id, text, type} = action;
    switch (type) {
        case ADD_TODO:
            return [
                ...state,
                {
                    id: id,
                    text: text,
                    completed: false
                }
            ];
        case TOGGLE_TODO:
            return state.map(todo => (todo.id === id) ? {...todo, completed: !todo.completed} : todo);
        default:
            return state;
    }
};

export  default  todos;
複製程式碼

js/reducers/visibilityFilter.js

import { SET_VISIBILITY_FILTER } from '../actions/types'
import { visibilityFilters } from '../global'

const { SHOW_ALL } = visibilityFilters;
const visibilityFilter = (state = SHOW_ALL, action) => {
    let {type, filter} = action;
    switch (type){
        case SET_VISIBILITY_FILTER:
            return filter;
        default:
            return state
    }
};

export default  visibilityFilter;
複製程式碼

釋:

reducer 就是一個純函式,接收舊的 stateaction,返回新的 state(上面兩個檔案可以看著兩個reducer)。

注意:

  • Redux 首次執行時,stateundefined,此時需要設定返回應用的初始 state
  • 每個 reducer 只負責管理全域性 state 中它負責的一部分。每個 reducerstate 引數都不同,分別對應它管理的那部分 state 資料。

js/reducers/index.js

import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'

export default combineReducers({
    todos,
    visibilityFilter
})
複製程式碼

釋:

combineReducers()所做的只是生成一個函式,這個函式來呼叫你的一系列 reducer,每個 reducer 根據它們的 key 來篩選出 state 中的一部分資料並處理,然後這個生成的函式再將所有 reducer 的結果合併成一個大的物件。

表面上看上去combineReducers()的作用就是把多個reducer合成一個的reducer


js/components/Todo.js

import React, { Component } from 'react'
import {
    Text,
    TouchableOpacity
} from 'react-native'
import PropTypes from 'prop-types'

export default class Todo extends Component {
    static propTypes = {
        onClick: PropTypes.func.isRequired,
        completed: PropTypes.bool.isRequired,
        text: PropTypes.string.isRequired
    };

    render(){
        let { onClick, completed, text } = this.props;
        return (
            <TouchableOpacity
                style={{
                    flexDirection: 'row',
                    flex: 1,
                    height: 50,
                    alignItems: 'center',
                    justifyContent: 'center',
                    backgroundColor: '#cccccc',
                    marginTop: 10
                }}
                onPress={onClick}>
                <Text style={{ textDecorationLine: completed ? 'line-through' : 'none'}}>{text}</Text>
            </TouchableOpacity>
        );
    }
}
複製程式碼

js/components/TodoList.js

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import {
    FlatList
} from 'react-native'
import Todo from './Todo'

export default class TodoList extends Component {
    static propTypes = {
        todos: PropTypes.arrayOf(
            PropTypes.shape({
                id: PropTypes.number.isRequired,
                completed: PropTypes.bool.isRequired,
                text: PropTypes.string.isRequired
            }).isRequired
        ).isRequired,
        toggleTodo: PropTypes.func.isRequired
    };

    _renderItem = (data) => {
       let dataItem = data.item;
       let { id } = dataItem;
       let { toggleTodo } = this.props;
        return (
            <Todo
                {...dataItem}
                onClick={() => toggleTodo(id)}
            />
        )
    };

    render() {
        let { todos } = this.props;
        return (
            <FlatList
                data={todos}
                keyExtractor={(item)=>item.id.toString()}
                renderItem={this._renderItem}
            />
        )
    }
}
複製程式碼

js/components/Link.js.js

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import {
    TouchableOpacity,
    Text
} from 'react-native'

export default class Link extends Component {
    static propTypes = {
        active: PropTypes.bool.isRequired,
        filter: PropTypes.string.isRequired,
        onClick: PropTypes.func.isRequired
    };

    render() {
        let { active,  filter, onClick } = this.props;
        return (
           <TouchableOpacity
               style={{
                   marginLeft: 4,
                   height: 40,
                   flex:1,
                   borderWidth: 1,
                   borderColor: '#ccc',
                   alignItems: 'center',
                   justifyContent:'center'
               }}
               onPress={onClick}
           >
               <Text style={{fontSize: 10, color: active ? 'black' : '#cccccc'}}>{filter}</Text>
           </TouchableOpacity>
        );
    }
}
複製程式碼

js/components/Filters.js

import React, { Component } from 'react'
import {
    View,
} from 'react-native'
import FilterLink from '../containers/FilterLink'
import { visibilityFilters } from '../global'

const { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE  } = visibilityFilters;

export default  class Filters extends Component {

    render(){
        return(
            <View style={{ flexDirection: 'row', marginTop: 20}}>
                <FilterLink filter={SHOW_ALL} />
                <FilterLink filter={SHOW_COMPLETED} />
                <FilterLink filter={SHOW_ACTIVE} />
            </View>
        )
    }
}
複製程式碼

image.png
釋:

以上四個檔案是自定義的四個UI展示元件,這些元件只定義外觀並不關心資料來源和如何改變。傳入什麼就渲染什麼。如果你把程式碼從 Redux 遷移到別的架構,這些元件可以不做任何改動直接使用。它們並不依賴於 Redux


js/containers/AddTodo.js

import React, { Component } from 'react'
import {
    View,
    TextInput,
    Button,
} from 'react-native'
import { connect } from 'react-redux'
import { addTodo } from '../actions'

class AddTodo extends Component {
    constructor(props){
        super(props);
        this.inputValue = '';
    }

    render(){
        let { dispatch } = this.props;
        return (
            <View style={{flexDirection: 'row'}}>
                <TextInput
                    style={{flex:1, borderWidth: 1, borderColor: '#cccccc', textAlign: 'center'}}
                    onChangeText={text => this.inputValue = text}
                />
                <Button title="Add Todo" onPress={() => dispatch(addTodo(this.inputValue))}/>
            </View>
        )
    }
}

export default connect()(AddTodo)
複製程式碼

js/containers/FilterLink.js

import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'

const mapStateToProps = (state, ownProps) => ({
    active: ownProps.filter === state.visibilityFilter,
    filterText: ownProps.filter
});

const mapDispatchToProps = (dispatch, ownProps) => ({
    onClick: () => dispatch(setVisibilityFilter(ownProps.filter))
});

export default connect(
    mapStateToProps,
    mapDispatchToProps,
)(Link)
複製程式碼

js/containers/VisibleTodoList.js

import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
import { visibilityFilters } from '../global'

const { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } = visibilityFilters;

const getVisibleTodos = (todos, filter) => {
    switch (filter) {
        case SHOW_COMPLETED:
            return todos.filter(t => t.completed);
        case SHOW_ACTIVE:
            return todos.filter(t => !t.completed);
        case SHOW_ALL:
            return todos;
        default:
            throw new Error('Unknown filter: ' + filter)
    }
};

const mapStateToProps = state => ({
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
});

const mapDispatchToProps = dispatch => ({
    toggleTodo: id => dispatch(toggleTodo(id))
});

export default connect(
    mapStateToProps,
    mapDispatchToProps,
)(TodoList)
複製程式碼

釋:

以上三個是容器元件,作用是把展示元件連線到 Redux。 總之:只要記住一句話就可以了:UI展示元件負責 UI 的呈現,容器元件負責管理資料和邏輯。 有時很難分清到底該使用容器元件還是展示元件。如這個小的元件:

  • AddTodo.js 含有“Add”按鈕 和 輸入框

技術上講可以把它分成兩個元件,但一開始就這麼做有點早。在一些非常小的元件裡混用容器和展示是可以的。當業務變複雜後,如何拆分就很明顯了。所以現在就使用混合型的吧。

上面出現了使用react-reduxconnect()方法來把展示元件和容器元件關聯在一起,這個方法做了效能優化來避免很多不必要的重複渲染。(這樣你就不必為了效能而手動實現 React 效能優化建議 中的 shouldComponentUpdate 方法。)

使用 connect() 前,需要先定義 mapStateToProps 這個函式來指定如何把當前 Redux store state 對映到展示元件的 props 中。例如,VisibleTodoList 需要計算傳到 TodoList 中的 todos,所以定義了根據 state.visibilityFilter 來過濾 state.todos 的方法,並在 mapStateToProps 中使用。

除了讀取 state,容器元件還能分發 action。類似的方式,可以定義 mapDispatchToProps() 方法接收 dispatch() 方法並返回期望注入到展示元件的 props 中的回撥方法。例如,我們希望 VisibleTodoList 向 TodoList 元件中注入一個叫 onTodoClick 的 props ,還希望 onTodoClick能分發 TOGGLE_TODO 這個 action。 最後,使用 connect() 建立 VisibleTodoList,並傳入這兩個函式。


js/components/Group.js

import React, { Component } from 'react'
import {
    View
} from 'react-native'
import AddTodo from '../containers/AddTodo'
import Filters from '../components/Filters'
import VisibleTodoList from '../containers/VisibleTodoList'

export default class Group extends Component {
    render() {
        return (
            <View style={{paddingHorizontal: 20, paddingVertical: 44}}>
                <AddTodo/>
                <Filters/>
                <VisibleTodoList/>
            </View>
        );
    }
}
複製程式碼

釋:

Group.js 是把所有的關聯後的元件串起來,形成一個完整的介面。


App.js

import React, { Component } from 'react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import Group from './js/components/Group'
import rootReducer from './js/reducers'

export default class App extends Component {
    render() {
        const store = createStore(rootReducer);
        return (
            <Provider store={store}>
                <Group />
            </Provider>
        );
    }
}
複製程式碼

釋:

入口檔案傳入 Store

  • 建立store傳入reducers
  • 使用Provider元件包裹 Group元件, store作為屬性傳入Provider

進行到這一步,程式碼分析完畢。本次寫作到此結束。我相信大家如果仔細看完的話,多多少少會有些收穫吧,如果demo看不太懂,那就跟著程式碼分析的思路多敲幾遍程式碼,也就理解了,有空我會繼續更新未完成的內容。

〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️

⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️華麗的分割線⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️

〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️

離上次更新已經有好幾天了,今天抽空更新的內容是Middleware(中介軟體)。

Middleware(中介軟體)的作用

Middleware是在ActionsDispatcher之間嵌入的為了解決某些問題、提高我們開發效率而存在的工具。 下面介紹三種常用的中介軟體:

  • redux-thunk 中介軟體:專案中的非同步操作需要用到(例如:請求伺服器資料、本地儲存等)。
  • redux-actions 中介軟體:幫助處理和建立操作actions(本文不做介紹,後續專案複雜後可以使用它來建立)。
  • redux-logger 中介軟體:用來列印 action 日誌。 開啟react native遠端除錯模式,操作demo就能在控制檯看到列印的狀態前後變化。
    狀態日誌

加入中介軟體後的示意圖如下:

加入中介軟體後的示意圖

核心程式碼詳解

本次demo程式碼講解為了減少文章篇幅,只會講解涉及到Middleware的部分,也就是說 demo中在reducerscomponentscontainers檔案加下新增的檔案不會做過多的解釋,如果不理解,可以返回去把第一次更新的內容再解析一遍。

actions/types.js新增如下程式碼

//請求帖子列表
export const REQUEST_POSTS = 'REQUEST_POSTS';
//帖子返回資料
export const RECEIVE_POSTS = 'RECEIVE_POSTS';
//切換資料來源
export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT';
//使快取過期失效
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT';
複製程式碼

actions/index.js新增如下程式碼

export const selectSubreddit = subreddit => ({
    type: SELECT_SUBREDDIT,
    subreddit
});

export const invalidateSubreddit = subreddit => ({
    type: INVALIDATE_SUBREDDIT,
    subreddit
});

export const requestPosts = subreddit => ({
    type: REQUEST_POSTS,
    subreddit
});

export const receivePosts = (subreddit, json) => ({
    type: RECEIVE_POSTS,
    subreddit,
    posts: json.data,
    receivedAt: Date.now()
});

const fetchPosts = subreddit => dispatch => {
    // API 發起請求
    dispatch(requestPosts(subreddit));
    return fetch(`http://localhost:8081/data/${subreddit}.json`)
        .then(response => response.json())
        .then(json => {
            setTimeout(()=>{
                //使用 API 請求結果來更新應用的 state
                dispatch(receivePosts(subreddit, json))
            },2000);

        })
};

const shouldFetchPosts = (state, subreddit) => {
    const posts = state.postsBySubreddit[subreddit];
    if (!posts) {
        return true
    }
    if (posts.isFetching) {
        return false
    }
    return posts.didInvalidate
};

export const fetchPostsIfNeeded = subreddit => (dispatch, getState) => {
    if (shouldFetchPosts(getState(), subreddit)) {
        return dispatch(fetchPosts(subreddit))
    }
};
複製程式碼

以上主要需要注意的是

  • fetchPosts返回了一個函式,而普通的 Action 建立函式 預設返回一個物件。
  • 返回的函式的引數是dispatchgetState這兩個 Redux 方法,普通的 Action 建立函式 的引數是 Action 的內容。
  • 在返回的函式之中,先發出一個 Action: dispatch(requestPosts(subreddit)),表示操作開始。
  • 非同步操作結束之後,再發出一個 Action: receivePosts(subreddit, json),表示操作結束。

demo中資料來源解釋:

本來打算用官方的 reddit demo API,最終發現官方給出的demo請求資料會報錯,所以使用了本地的json資料,延遲兩秒模擬網路API載入資料的過程。

官方提供的redditAPI無法使用


App.js

import React, { Component } from 'react'
import { applyMiddleware, createStore } from 'redux'
import { createLogger } from 'redux-logger'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
import LoadPosts from './js/containers/LoadPosts'
import rootReducer from './js/reducers'

export default class App extends Component {
    render() {
        const logger = createLogger();
        const store = createStore(
            rootReducer,
            applyMiddleware(thunk, logger)
        );

        return (
            <Provider store={store}>
                <LoadPosts/>
            </Provider>
        );
    }
}
複製程式碼

相比前一個demoApp.js,在createStore的時候引數有變化,多了一個applyMiddleware(thunk, logger)中介軟體的引數。 理解了第一次更新內容的童鞋不難看出,Action 是由store.dispatch方法傳送的。而store.dispatch方法正常情況下,引數只能是物件,不能是函式。 為了解決這個問題,就要使用到中介軟體redux-thunk改造store.dispatch,使store.dispatch可以接受函式作為引數。

注意

有的中介軟體有次序要求,使用前要查一下文件。比如,logger就一定要放在最後,否則輸出結果會不正確。


到此本次寫作到此結束。

〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️

⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️華麗的分割線⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️

〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️

接著更新關於整合navigation的整合,如果使用過比較老版本的react native都知道在react-navigation沒有興起之前,大多數開發者都使用的官方提供的 Navigator,直到 react native v0.44.3 釋出時宣佈已經遺棄Navigator

image.png
因為我之前專案中只用過 Navigator或者 react-navigation,所以我並不知道市場上還有多少類似的導航解決方案,這次通過專案空檔期,又深入瞭解了一下,目前市場上比較流行的三款導航器:

這是官方推薦的,在 github 上已有 1.35W+ 的 ?,由React Native社群維護,目前,它是最受歡迎的React Native導航庫。它完全用JavaScript編寫,而不是使用本機API,它重新建立了一些子集。這個選擇允許使用者定製導航體驗的任何部分,而無需學習iOSAndroid導航邏輯。因為React Navigation的大部分邏輯都是在JavaScript中而不是在本機中執行,所以任何阻止JavaScript執行緒的情況都會造成卡頓顯現。另外說明一下 react navigation的v1版本跟v2版本差別挺大的,如果想了解的童鞋可以看我前面寫的這篇文章 [react native 強大的navigation V2.0+ ](www.jianshu.com/p/05fd0e9bc…)。

目前官方文件中已經明確提出:

Warning: in the next major version of React Navigation, to be released in Fall 2018, we will no longer provide any information about how to integrate with Redux and it may cease to work. Issues related to Redux that are posted on the React Navigation issue tracker will be immediately closed. Redux integration may continue to work but it will not be tested against or considered when making any design decisions for the library.

Some folks like to have their navigation state stored in the same place as the rest of their application state. Think twice before you consider doing this, there is an incredibly good chance that you do not need to do this!. Storing your React Navigation state in your own Redux store is likely to give you a very difficult time if you don't know what you're doing.

If your only reason for doing this is that you want to be able to perform navigation actions from outside of your components (eg: from a Redux middleware), you can learn more about this in navigating without the navigation prop.

翻譯:

警告: 在下一個大版本的 React Navigation 中, 將在2018年秋季釋出, 我們將不再提供有關如何整合 Redux 的任何資訊, 並且它可能會停止使用。 釋出在 React Navigation issue tracker 中有關 Redux 的 issue,也將立即關閉。 Redux 整合可能會繼續工作,但不會在為 library 作出任何設計決策時進行測試或考慮。

有些人喜歡將他們的 navigation state 儲存在與其他的應用程式的 state 相同的位置。 在你考慮這樣做之前請三思, 但是有一個非常好的機會, 你可以不需要這樣做!。 如果你不知道自己要做什麼,將 React Navigation state 儲存在你自己的 Redux store 中可能會會很困難。

如果你這樣做的唯一原因是希望能夠從元件外部執行導航操作 (例如: 從 Redux 中介軟體), 你可以瞭解更多關於 不使用 navigation prop 進行導航 的資訊。

翻譯成通俗易懂的話就是:React Navigation在下個版本中將不會再特意考慮去相容 Redux,用是可以用,但是出了問題需要自行解決。

哎,不理解官方為什麼要這麼做,可能是減少維護成本吧,但是這樣做無疑是一個不明智但選擇,也說不定會有驚喜,暫時期待一下吧。如果專案中整合了 redux 我個人不太推薦使用React Navigation

它是基於 React Navigation,但提供了與其互動的不同API。在 github 上已有 7600+ 的 ?,它允許您在一箇中心位置定義場景轉換,而無需傳遞導航器物件,並且可以在程式碼中的任何位置輕鬆訪問。

最新的beta版本 - 4,除了其他更改之外,還介紹了抽屜支援和Mob-X驅動的導航狀態機,它將導航邏輯與表示層分開。

另一個流行的導航庫是由 Wix 開源團隊開發的React Native Navigation,在 github 上已經接近9000+ 的 ?,它的最大優勢是跨平臺介面覆蓋的100%本機平臺導航,具有開箱即用的Redux支援。

您需要為iOSAndroid單獨配置此軟體包,其中包括連結iOS庫,更新iOS標頭搜尋路徑,在Android MainActivity中擴充套件SplashActivity而不是ReactActivity以及文件中詳細描述的其他幾個步驟。完成後,您只需要註冊所有應用程式的螢幕並啟動應用程式。

目前官方文件中也提出:

Note: example redux is deprecated. Since we did not have enough time and resources to maintain both example projects, we decided to stop maintaining the redux example. This does not mean redux can't be used with react-native-navigation (In fact, we use redux in the Wix app). For a working example project which uses redux with RNN you can refer to JuneDomingo/movieapp.

翻譯:

注意:不推薦使用示例redux。由於我們沒有足夠的時間和資源來維護這兩個示例專案,因此我們決定停止維護redux示例。這並不意味著redux不能與react-native-navigation一起使用(事實上,我們在Wix應用程式中使用redux)。對於使用帶RNN的redux的工作示例專案,您可以參考JuneDomingo / movieapp。

綜上所訴:就個人而言,從react navigationreact-native-navigation 官方對 Redux的態度完全是不一樣的,至少Wix內部在使用Redux。 如果專案中需要使用Redux,我的第一選擇會是React Native Navigation,因為它是純原生體驗,而且對Redux支援很好 。如果在不使用Redux 的專案中,可以嘗試前兩種導航,這兩種導航體驗也不錯的,非常接近原生體驗了。

結構分析

本文導航選擇使用 react-native-navigation,關於react-native-navigation的整合和API使用請參考官方文件,如果想了解在 React Navigation 中使用 redux 點這裡 或者 這裡,以下是這次更新改變和新增的檔案程式碼 index.js

//discard (廢棄)
import { AppRegistry } from 'react-native';
AppRegistry.registerComponent('ReduxForReactNativeDemo', () => App);
複製程式碼
//new
import App from './App';
new App();
複製程式碼

App.js

import React, { Component } from 'react'
import { applyMiddleware, createStore } from 'redux'
import { createLogger } from 'redux-logger'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
import rootReducer from './js/reducers'
import { Navigation } from 'react-native-navigation'
import { registerScreens } from './js/components/screens'

const logger = createLogger();
const store = createStore(
    rootReducer,
    applyMiddleware(thunk, logger)
);

registerScreens(store, Provider);

export default class App extends Component {

    constructor(props){
       super(props);
       this._startApp();
    }

    _startApp = () => {
        Navigation.startTabBasedApp({
            tabs: [
                {
                    label: 'Home',
                    screen: 'ReduxForReactNativeDemo.HomeScreen',
                    icon: require('./res/img/ic_home.png'),
                    // selectedIcon: require('./img/checkmark.png'),
                    title: 'Home',
                    overrideBackPress: false,
                    navigatorStyle: {}
                },
                {
                    label: 'Posts',
                    screen: 'ReduxForReactNativeDemo.PostsScreen',
                    icon: require('./res/img/ic_news.png'),
                    // selectedIcon: require('./img/checkmark.png'),
                    title: 'Posts',
                    navigatorStyle: {}

                }
            ]
        });
    }

}
複製程式碼

比起上個版本的demo,整個App.js檔案程式碼基本都改了


其他改變

components目錄下新增screens目錄,該資料夾下放一個一個的介面檔案,每個介面裡面又由多個元件組成。

  • Group.js改名為HomeScreen.js
  • 新增PostsDetail.jsPostsScreen.jsindex.jsindex.js檔案作用是註冊所有介面檔案。
  • Posts.js新增item點選事件,點選後進入列表詳細介面。
  • LoadPosts.js 68 行新增 {...this.props},為了在 Posts.js裡面可以通過 this.props獲取到navigator
  • 根目錄下新增res資原始檔夾。

總結:

本次結構分析就到這裡了,說下三個demo版本連貫做下來的感受吧。講真這次對我本人來說學到很多東西,實踐過程中也遇到各種問題,查閱海量資源,有很多疑問,最終一一攻破,答案慢慢浮出水面。看過很多demo千奇百怪的寫法都有,很少見到標準的專案工程結構,大多都是為了實現效果為目的,而不能在實際專案中去使用這種專案結構,我文章開始階段我就介紹我了為什麼要花這些時間和精力來寫這篇技術文章。我會把這種工程結構運用到以後整合了redux 的專案中。找到一份好的學習資料真的很不容易,如果你也覺得不錯的話,不妨把 ❤️ 點亮,讓更多人發現它。


彩蛋

附上 demo ,歡迎 ❤️⭐️? 指出錯誤或者釋出自己的見解探討,共勉。?

注意

直接 clone 下來執行的話,預設看到是最後一次(v3)更新的內容 demo, 執行git tag可以看到的demo有三個 tag,如果切換到前兩次更新的 demo內容:根目錄下執行:

切換到v0.1

git checkout -b dev v0.1

切換到v0.2

git checkout -b dev v0.2

相關文章