開發 React Native APP —— 從改造官方Demo開始(一)

本文使用的 Demo 完整代碼在這 react_native_complete_demo

最開始接到公司通知要開發 React Native APP 的時候,很興奮,因爲之前的技術棧主要是 Vue 和 Angular,對於 React 只是寫過幾個 Demo,一直想在實際項目中使用但沒有機會。不過公司給的開發時間很短,從設計需求到第一版送審只給了一個月時間。鑑於之前使用 Vue 的經驗(即便不是很熟的情況下也可以把官網 Demo 擼下來改改就能上線,功能及性能可以後續迭代優化)以及業務 API 90%以上都已和後臺同學聯調 OK,當時想一個月綽綽有餘。

雖說最後 APP 上線了(iOS安卓),但開發過程中踩了很多坑。

首先,難以找到一個開箱即用的 React Native APP Demo。目前存在的 Demo 要麼過於簡單,比如 React Native 官網提供的 Demo AwesomeProject ,這個 Demo 只提供了最簡功能,對於路由(導航組件)、狀態管理等並沒有涉及。雖然 React Native 教程中對於複雜應用應如何選擇組件及第三方庫都有提及,但並沒有給出完整示例。而另一方面,又有很多 React Native APP 雖已開源,但都是用於特定場合的完整 APP,有些 APP 的目錄結構本身就不友好,並且也沒有完整的說明文檔。

其次,React 本身的學習曲線就相對陡峭,尤其涉及狀態管理部分,很難找到可以直接 copy-paste 的代碼,除此之外原生 App 本身還有很多區別於 web 的需求。

鑑於以上原因,所以決定寫篇文章詳細介紹開發 React Native APP 的過程。

內容較多,分兩部分介紹,這部分主要內容爲:

  • react navigation 作爲路由(導航)組件的初步使用
  • 自定義組件
  • 通過 fetch API 發送網絡請求
  • 集成 redux,並實現 redux 狀態的持久化存儲

一 準備工作

1.1 開發工具

如果要開發 iOS 應用併發布到 APP Store 必須使用 Xcode 並有 Apple 開發者賬號。如果是開發安卓應用,有電腦就好,最好有梯子。

1.2 代碼檢查及自動修正

開始改造代碼之前,推薦先安裝 eslint 和 prettier 作爲代碼檢查和自動格式化工具,這樣可以確保自己寫的代碼始終如一且避免低級錯誤,我使用 vscode 作爲編輯器,之前寫過一篇文章VSCode 配置 react 開發環境,如果你也使用 vscode 可參考下。

最終的目的就是保存操作後代碼按照 eslint 的配置,自動格式化代碼。

二 官方 Demo 下載及介紹

官方 demo 雖然不完整,但卻是一個很好的開始。介紹完官方 Demo(包括環境配置),後文會一步步介紹如何從這個不完整的官方 Demo 改造成可用於生產的 APP。

2.1 環境配置

下載官方 Demo:AwesomeProject,然後運行。

所需的環境配置官方文檔講的很清楚,這裏不在贅述。需要指出的是 React Native 對於運行 Demo 提供了兩種方法:一種是在 Expo 客戶端中運行,另一種是編譯成原生代碼(安卓編譯成 Java,iOS 編譯成 objective-C)後在模擬器或者在真機上運行。推薦直接使用第二種,如果想發佈 APP 這也是繞不過去的。

如果之前沒有開發過原生 APP,還需要熟悉下原生 APP 的開發工具:安卓使用 Android Studio,iOS 使用 Xcode。它們如何配合 React Native 使用在 官方文檔有說明,這部分沒有太多坑,遇到問題自行谷歌一般都有解決方案。

需要說明的是 Android Studio 很多依賴更新需要訪問谷歌服務,所以請自備梯子。

step_by_step/other/react/react_native/ 中詳細記錄了我在初次使用 React Native 過程中遇到的問題及解決方案,因爲以記錄爲目的,所以稍顯囉嗦,有興趣可以看下。

2.2 官方 Demo 目錄介紹

良好的目錄結構有助於今後的開發及維護,本文後半部分每添加新功能,除了代碼部分,如果目錄結構有變,還會着重指出。首先,讓我們看下官方 Demo 的目錄結構:

上面的目錄結構說明如下,重要的有:

  • android/ android 原生代碼(使用 android studio 要打開這個目錄,如果直接打開父目錄報錯)
  • ios/ ios 原生代碼(使用 xcode 打開這個目錄,如果直接打開父目錄報錯)
  • index.js 打包 app 時進入 react native(js 部分) 的入口文件(0.49 以後安卓、ios 共用一個入口文件)
  • App.js 可以理解爲 react native(js 部分) 代碼部分的入口文件,比如整個項目的路由在這裏導入

上面是最重要的四個目錄/文件,其他說明如下:

  • _test_/ 測試用(暫未使用)
  • app.json 項目說明,主要給原生 app 打包用,包括項目名稱和手機桌面展示名稱 React Native : 0.41 app.json
  • package.json 項目依賴包配置文件
  • node_modules 依賴包安裝目錄
  • yarn.lock yarn 包管理文件
  • 其他配置文件暫時無需改動,在此不做說明

3 配置路由

這裏使用 react navigation 管理路由,大而全的介紹或者原理說明不是這部分的重點,這裏主要講怎麼用。

react navigation 常用 API 有三個:

  • StackNavigator:頁面間跳轉(每次跳轉後都會將前一個頁面推入返回棧,需要返回上個頁面特別好用)
  • TabNavigator:頂部或底部 tab 跳轉,一般在底部使用
  • DrawerNavigator:側滑導航

最爲常用的是前兩個,接下來也只介紹前兩個的使用。

3.1 StackNavigator 實現頁面間跳轉

首先我們要調整下目錄結構,調整後的結構如下:

  • src/ 放置所有原始的 react native 代碼
  • config/ 配置文件,比如路由配置
  • route.js 路由配置文件
  • screens/ 所有頁面文件
  • ScreenHome/ 這個目錄是放具體頁面文件的,爲了進一步進行代碼分離,裏面又分爲三個文件:index.js 中包含邏輯部分,style.js 中包含樣式部分;view.js 中包含視圖或者說頁面元素部分。其他頁面文案結構與此相同。

注意頁面文件的命名方式:大駝峯命名法,react native 推薦組件命名用大駝峯命名法,每個頁面相當於一個組件。

簡單介紹了 react navigation 下面進行具體改造:

1)首先配置路由:路由文件 route.js 此時內容如下,這也是 StackNavigator 最簡單的使用方式:

/**
 * route.js
 */

// 引入依賴
import React from "react";
import { StackNavigator } from "react-navigation";

// 引入頁面組件
import ScreenHome from "../screens/ScreenHome";
import ScreenSome1 from "../screens/ScreenSome1";

// 配置路由
const AppNavigator = StackNavigator({
  ScreenHome: {
    screen: ScreenHome
  },
  ScreenSome1: {
    screen: ScreenSome1
  }
});

export default () => <AppNavigator />;

2)更新 App.js,對接路由文件:

/**
 * App.js
 */

export default class RootApp extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    // 渲染頁面
    return <Route />;
  }
}

3)具體頁面設置,以 ScreenHome 爲例

index.js 中自定義當前頁面路由邏輯和樣式,比如 title 及其樣式、在導航欄自定義按鈕等,到目前爲止,我們只需要簡單設置 title 就好:

/**
 * ScreenHome/index.js
 */

export default class ScreenHome extends Component {
  // 自定義當前頁面路由配置,後面介紹的TabNavigator也使用這個對象中的屬性
  static navigationOptions = {
    // 設置 title
    title: "首頁"
  };

  constructor(props) {
    super(props);
    this.navigation = props.navigation;
  }

  render() {
    return view(this);
  }
}

view.js 中在具體元素上定義具體跳轉頁面

/**
 * ScreenHome/index.js
 */

// 引入依賴略

export default self => (
  <View>
    <Text style={{ fontSize: 36 }}>home</Text>
    <Button
      title="goSomePage1"
      // 路由跳轉
      onPress={() => self.navigation.navigate("ScreenSome1")}
    />
  </View>
);

經過上述配置,效果如下:

3.2 TabNavigator 實現頁面底部 tab 切換

首先在 screens 目錄下新建 ScreenBottomTab 頁面,用於配置 TabNavigator。每個 tab 對應一個頁面,按需新建頁面,並且新建的頁面需要在 route.js 中進行配置,更新後的目錄結構如下:

  • ScreenBottomTab 配置底部 tab 導航
  • ScreenTab1/2/3 新建頁面,配合底部 tab 導航

1)沒有 tab 圖標的最簡配置

此時只需要配置 ScreenBottomTab 裏面的 index.js 文件就好,如下:

/**
 * ScreenBottomTab/index.js
 */

const ScreenTab = TabNavigator(
  // 配置 tab 路由
  {
    ScreenHome: {
      screen: ScreenHome
    },
    ScreenTab1: {
      screen: ScreenTab1
    },
    ScreenTab2: {
      screen: ScreenTab2
    },
    ScreenTab3: {
      screen: ScreenTab3
    }
  },
  // 其他配置選項
  {
    tabBarPosition: "bottom"
  }
);

export default ScreenTab;

頁面文件現在無需配置,需要注意的是 tab 下面的文字默認和在 StackNavigator 中定義的頭部導航 title 相同。

2)自定義 tab 圖標

tab 圖標除了自定義外,還需要根據是否選中顯示不同顏色,這可以通過配置 TabNavigatortabBarIcon 實現,修改的具體文件是 tab 對應頁面的 index.js 文件。

/**
 * ScreenHome/index.js
 */

static navigationOptions = {
  title: '首頁',
  tabBarIcon: ({ focused }) => {
    // 根據是否選中,顯示不同圖片
    const icon = focused
      ? require('../../assets/images/tab_home_active.png')
      : require('../../assets/images/tab_home.png');
    return <Image source={icon} style={{ height: 22, width: 22 }} />;
  },
};

最終的效果如下:

3.3 單個頁面實現 modal 模式的切換

對於 ios 常見的需求是:登錄頁面是由下往上進入,而其他頁面是由左至右默認進入,react navigation 只提供了全局配置頁面的方式,並沒提供單個頁面的交互方式,但這個功能還是可以實現的,這個在第二部分開發 React Native APP —— 從改造官方 Demo 開始(二)中介紹。

四 自定義組件

react native 已經封裝了很多常用組件,但有時我們仍然需要在次基礎上進行封裝,比如某些組件需要大量複用而原生組件樣式或者交互邏輯不符合需求。

這裏只介紹目錄結構的調整,具體代碼可參考 Github 上項目代碼,因爲自定義組件的需求千差萬別,具體編寫過程也有很多教程,這裏不再具體介紹,只添加了自定義 Toast 組件。目錄結構調整如下:

  • components/ 自定義組件都放這裏
  • XgToast.js 自定義組件具體代碼

文件 config/pxToDp.js 用於尺寸自適應,在 XgToast.js 中有使用,開發 React Native APP —— 從改造官方 Demo 開始(二)中詳細介紹。

五 網絡請求

react native 使用上有個最大的好處是可以不用考慮新語法兼容性的問題,既然如此,自然使用設計更加優良的 API,在網絡請求方面,本項目使用fetch API

添加網絡請求後目錄結構調整如下:

  • xgHttp.js 配置 fetch api
  • xgRequest.js api 請求列表

5.1 配置 fetch api

xgHttp.js全部代碼如下,裏面有簡單註釋,這裏不再詳解,fetch api 的使用可以參考 fetch API 簡介

/**
 * xgHttp.js
 */

// 請求服務器host
const host = "http://api.juheapi.com";

export default async function(
  method,
  url,
  { bodyParams = {}, urlParams = {} }
) {
  const headers = new Headers();
  headers.append("Content-Type", "application/json");

  // 將url參數寫入URL
  let urlParStr = "";
  const urlParArr = Object.keys(urlParams);
  if (urlParArr.length) {
    Object.keys(urlParams).forEach(element => {
      urlParStr += `${element}=${urlParams[element]}&`;
    });
    urlParStr = `?${urlParStr}`.slice(0, -1);
  }

  const res = await fetch(
    new Request(`${host}${url}${urlParStr}`, {
      method,
      headers,
      // 如果是 get 或者 head 方法,不添加請求頭部
      body: method === ("GET" || "HEAD") ? null : JSON.stringify(bodyParams)
    })
  );

  if (res.status < 200 || res.status > 299) {
    console.log(`出錯啦:${res.status}`);
  } else {
    return res.json();
  }
}

上面的配置還不完善,比如,生產環境中很多接口都有驗證功能,一般是 token + 用戶 id,上面的配置並沒有這個功能。但現在實現這個功能還會涉及到在哪存放 token,一展開又有很多內容,缺少驗證功能暫時並不影響 APP 的完整度,所以這個坑後續填。

5.2 請求 api 編寫及使用

  • api 列表文件

具體 api 請求代碼我放在了 xgRequest.js 文件中,以 get 請求爲例,xgRequest.js 代碼如下:

/**
 * xgRequest.js
 */

import XgHttp from "./xgHttp";

export default {
  todayOnHistory: urlPar => XgHttp("GET", "/japi/toh", { urlParams: urlPar })
};

其中 "/japi/toh" 爲接口地址,這裏我使用了聚合數據歷史上的今天 API。

再調用聚合數據歷史上的今天 API 的時候使用了我自己的 APPKEY,每天免費調用 100 次,超出後回報錯request exceeds the limit!,如果你想進行更多的測試,註冊後替換成自己的 APPKEY 就可以。

  • 使用

首先,調用接口,獲取數據。

接口調用是在頁面文件的 index.js 中進行的,以 ScreenTab1/index.js 爲例:

/**
 * ScreenTab1/index.js
 */

const urlPar = {
  // 大佬們,這個是我申請的聚合數據應用的key,每天只有100免費請求次數
  key: '7606e878163d494b376802115f30dd4e',
  v: '1.0',
  month: Number(this.state.inputMonthText),
  day: Number(this.state.inputDayText),
};

// 拿到返回數據後就可以進一步操作了
const todayOnHistoryInfo = await XgRequest.todayOnHistory(urlPar);

然後,展示數據。

拿到數據以後就可以在做進一步操作了,一般就是在頁面中展示了。react 是數據驅動的框架,對於動態變化的展示數據一般是放在 react native 的 state 對象中,state 一經改變,便會觸發 render() 函數重新渲染 DOM 中變化了的那部分。

首先是在 index.js 中把需要動態展示的數據先寫入 state

/**
 * ScreenTab1/index.js
 */

// 將需要動態更新的數據放入 state
this.state = {
  todayOnHistoryInfo: {}
};

然後在 view.js 中讀取 state 中的數據:

/**
 * ScreenTab1/view.js
 */

{
  /* 查詢 */
}
<Button title="查詢" onPress={() => self.getTodayOnHistoryInfo()} />;

{
  /* 展示查詢數據 */
}
<Text>
  發生了啥事:{self.state.todayOnHistoryInfo.result
    ? self.state.todayOnHistoryInfo.result[0].des
    : "暫無數據"}
</Text>;

上述 view.js 中的代碼主要做兩件事:發送調用指令,展示返回數據。

最終的效果圖如下:

六 集成 redux

在 App 中有一些全局狀態是所有頁面共享的,比如登錄狀態,或者賬戶餘額(購買商品後所有展示餘額的頁面都要跟着更新)。在本項目中,使用 Redux 進行狀態管理。

引入 redux 後後目錄結構調整如下:

  • redux 存放 redux 相關配置文件
  • actions.js redux action
  • reducers.js redux reducer
  • store.js redux store

如果對 redux 毫無概念,可以看下這篇文章 Redux 入門教程

Redux 實際上是非常難用的,,,如果之前使用過 vuex,在使用 Redux 的過程中,會發現需要自己配置的東西太多(這裏沒有好壞之分,不想引戰,自己的使用感受而已),爲了簡化 Redux 的操作, Redux 作者開發了 react-redux,雖然使用的便捷性上還沒法和 vuex 比,但總算是比直接使用 Redux 好用很多。

在集成 Redux 進行狀態管理之前我們先思考一個問題:集成過程中難點在哪?

因爲在一個 App 中 Redux 只有一個 Store,這個 Store 應該爲所有(頁面)組件共享,所以,集成的難點就是如何使所有(頁面)組件可以訪問到這個唯一的 store,並且可以觸發 action。爲此,redux-react 引入了 connect 函數和 Provide 組件,他們必須配合使用才能實現 redux 的集成。

通過這 connectProvide 實現 store 在組件間共享的思想是:

  1. Redux store 可以(注意是“可以”,並不是“一定”,需要配置,見第 2 條)對 connect 方法可見,所以在組件中可以通過調用 connect 方法實現對 store 數據的訪問;
  2. 實現 Redux store 對 connect 的可見的前提條件是,需要保證這個組件爲 Provide 組件的子組件,這樣通過將 store 作爲 Provide 組件的 props,就可以層層往下傳遞給所有子組件;
  3. 但子組件必須通過 connect 方法實現對 store 的訪問,而無法直接訪問。

6.1 引入依賴

首先是安裝依賴 redux,react-redux:

yarn add redux react-redux

6.2 配置 redux

這裏指的是配置 actions, reducersstore

據說應用大了,最好將 redux 分拆,但現在項目還小,暫時沒有做拆分。

  • 配置 actions
/**
 * actions.js
 */

export function setUserInfo(userInfo) {
  return {
    // action 類型
    type: "SET_USER_INFO",

    // userinfo 是傳進來的參數
    userInfo
  };
}
export function clearReduxStore() {
  return {
    type: "CLEAR_REDUX_STORE"
  };
}
  • 配置 reducers
/**
 * reducers.js
 */

import { initialState } from "./store";

function reducer(state = initialState, action) {
  switch (action.type) {
    case "SET_USER_INFO":
      // 合併 userInfo 對象
      action.userInfo = Object.assign({}, state.userInfo, action.userInfo);

      // 更新狀態
      return Object.assign({}, state, { userInfo: action.userInfo });
    case "CLEAR_REDUX_STORE":
      // 清空 store 中的 userInfo 信息
      return { userInfo: {} };
    default:
      return state;
  }
}

export default reducer;

注意 SET_USER_INFO 這條路徑下的代碼,使用了 Object.assign()。這是因爲 reducer 函數每次都會返回全新的 state 對象,這意味着如果 state 對象含有多個屬性而在 reducer 函數返回時沒有合併之前的 state,可能會導致 state 對象屬性丟失

這是一個很常見的錯誤,因爲通常我們在觸發 actions 時只需要傳入更改的那部分 state 屬性,而不是將整個 state 再傳一遍。

redux 經典計數器教程在觸發 state 變化時通常這樣寫 return { defaultNum: state.defaultNum - 1 };,因爲計數器例子中只有一個屬性,即 defaultNum,所以合併之前的 state 就沒有意義了,但生產環境中的應用 state 對象中往往不止一個屬性,此時上述的寫法就會出錯。

  • 配置 store
/**
 * store.js
 */

import { createStore } from "redux";
import reducers from "./reducers";

// 定義初始值
const initialState = {
  userInfo: {
    name: "小光",
    gender: "男"
  }
};

const store = createStore(reducers, initialState);

export default store;

6.3 組件中使用

配置完 redux,接下來就是使用了。

  • 配置 index.js

在配置 index.js 中 主要是配置 Provide 作爲根組件,並傳入 store 作爲其屬性,爲接下來組件使用 redux 創造條件。

/**
 * index.js
 */

import React from "react";
import { AppRegistry } from "react-native";
import { Provider } from "react-redux";
import App from "./App";
import store from "./src/redux/store";

const ReduxApp = () => (
  // 配置 Provider 爲根組件,同時傳入 store 作爲其屬性
  <Provider store={store}>
    <App />
  </Provider>
);

AppRegistry.registerComponent("AwesomeProject", () => ReduxApp);
  • 配置組件

這裏以 ScreenTab2 爲例:

首先,在 index.js 中關聯 redux

/**
 * ScreenTab2/index.js
 */
// redux 依賴
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import * as actionCreators from "../../redux/actions";

changeReduxStore(userInfo) {
  // 設置 redux store,相當於 dispatch,這裏觸發 actions 中的 'SET_USER_INFO'
  this.props.setUserInfo(userInfo);
}

// 將 store 中的狀態映射(map)到當前組件的 props 中,這樣才能在該組建中訪問 redux state
function mapStateToProps(state) {
  return { userInfo: state.userInfo };
}

// 將 actions 中定義的方法映射到當前組件的 props 中,這樣才能在該組建中觸發 action
function mapDispatchToProps(dispatch) {
  return bindActionCreators(actionCreators, dispatch);
}

// 將 store 和 當前組件連接(connect)起來
export default connect(mapStateToProps, mapDispatchToProps)(ScreenTab2);

然後,就是在 view 中控制具體改變的數據

/**
 * ScreenTab2/view.js
 */

<Button title="改變名字" onPress={() => self.changeReduxStore({ name: 'vince' })} />
<Button title="改變性別" onPress={() => self.changeReduxStore({ gender: '女' })} />
<Button title="還原" onPress={() => self.changeReduxStore({ name: '小光', gender: '男' })} />

最終效果圖如下:

6.4 持久化存儲

手機 App 一般都有這樣的需求:除非用戶主動退出,不然即便 App 進程被殺死,App 重新打開後登錄信息依舊會保存

在本項目中,爲了便於各組件共享登錄狀態,我把登錄狀態寫在了 redux store 中,但原生 redux 有個特性:頁面刷新後 redux store 會回恢復初始狀態。爲了達到上述需求,就需要考慮 redux store 持久化存儲方案。本項目中使用了 redux-persist,下面介紹如何配置:

  • 引入依賴
yarn add redux-persist
  • 修改 redux 配置

1)修改 store.js

除了引入 redux-persist 外,這裏使用了 react native 提供的 AsyncStorage 作爲持久化存儲的容器。另外,初始化 state 移到了 reducers.js 中。

/**
 * store.js
 * 更改爲持久化存儲
 */

import { createStore } from "redux";

// 引入 AsyncStorage 作爲存儲容器
import { AsyncStorage } from "react-native";

// 引入 redux-persist
import { persistStore, persistCombineReducers } from "redux-persist";

import reducers from "./reducers";

// 持久化存儲配置
const config = {
  key: "root",
  storage: AsyncStorage
};

const persistReducers = persistCombineReducers(config, {
  reducers
});

const configureStore = () => {
  const store = createStore(persistReducers);
  const persistor = persistStore(store);

  return { persistor, store };
};

export default configureStore;

2)修改 reducers.js

只是將初始化 state 移入。至於爲什麼要將初始化 statestore.js 移入 reducers.js 實在是無奈之舉:不然在 store.js 中創建 store 報錯,後續再填坑,暫時先放在 reducers.js 中。

/**
 * reducers.js
 * 更改爲持久化存儲
 */

// 初始化 state 放在這裏
const initialState = {
  userInfo: {
    name: "小光",
    gender: "男"
  }
};

function reducers(state = initialState, action) {
  // ... 代碼未修改
}

export default reducers;
  • 修改使用 redux 的文件

1)修改根目錄下的 index.js

/**
 * index.js
 * 更改爲持久化存儲
 */
import { PersistGate } from "redux-persist/es/integration/react";
import configureStore from "./src/redux/store";

const { persistor, store } = configureStore();

const ReduxApp = () => (
  // 配置 Provider 爲根組件,同時傳入 store 作爲其屬性
  <Provider store={store}>
    {/* redux 持久化存儲 */}
    <PersistGate persistor={persistor}>
      <App />
    </PersistGate>
  </Provider>
);

2)因爲修改爲持久化存儲的過程過程中把初始化的 state 存在了 reducers.js 中,所以在頁面組件映射 state 到當前頁面時需要還需要修改對應屬性的引入地址,依然以 ScreenTab2 爲例:

/**
 * ScreenTab2/index.js
 * 更改爲持久化存儲
 */

// 修改前
function mapStateToProps(state) {
  // 引用 state.userInfo
  return { userInfo: state.userInfo };
}

// 修改後
function mapStateToProps(state) {
  // 引用 state.reducers.userInfo
  return { userInfo: state.reducers.userInfo };
}

經過上述修改,便可以實現 redux 的持久化存儲:初始化姓名是 小光,更改爲 vince 後重新加載頁面,姓名還是 vince(而非初始狀態 小光)。效果圖如下:

七 小結

經過這部分介紹,App 框架基本構建完成,在第二部分主要討論 UI/交互、App 發佈前的準備工作及如何發佈,具體內容包括:

  • 在使用 react navigation 的前提下,iOS 實現單個頁面從下往上(modal)的進入動畫
  • 尺寸自適應
  • 設置啓動頁,更換桌面圖標、app 展示名稱、appID
  • 打包發佈

參考資料

fetch API 簡介
Redux 入門教程
對 React-redux 中 connect 方法的理解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章