Redux 減少樣板代碼

2021-09-16 10:07 更新

減少樣板代碼

Redux 很大部分 受到 Flux 的啟發(fā),并且最常見的關(guān)于 Flux 抱怨是它如何使得你寫了一大堆的模板。在這個技巧中,我們將考慮 Redux 如何使得我們選擇我們的代碼會變得怎樣繁復(fù),取決于個人樣式,團隊選項,長期可維護等等。

Actions

Actions 是描述了在 app 中所發(fā)生的,以單獨方式描述對象變異意圖的服務(wù)的一個普通對象。很重要的一點是 你必須分發(fā)的 action 對象并不是一個模板,而是 Redux 的一個基本設(shè)計選項.

有些框架生成自己和 Flux 很像,不過缺少了 action 對象的概念。為了變得可預(yù)測,這是一個從 Flux or Redux 的倒退。如果沒有可串行的普通對象 action,便無法記錄或重放用戶會話,或者無法實現(xiàn) 帶有時間旅行的熱重載。如果你更喜歡直接修改數(shù)據(jù),那么你并不需要 Redux 。

Action 一般長這樣:

{ type: 'ADD_TODO', text: 'Use Redux' }
{ type: 'REMOVE_TODO', id: 42 }
{ type: 'LOAD_ARTICLE', response: { ... } }

一個約定俗成的是 actions 擁有一個定值 type 幫助 reducer (或 Flux 中的 Stores ) 識別它們。我們建議的你使用 string 而不是 Symbols 作為 action type ,因為 string 是可串行的,而使用 Symbols 的話你會把記錄和重演變得比所需要的更難。

在 Flux 中,傳統(tǒng)上認為你將每個 action type 定義為string定值:

const ADD_TODO = 'ADD_TODO';
const REMOVE_TODO = 'REMOVE_TODO';
const LOAD_ARTICLE = 'LOAD_ARTICLE';

這么做的優(yōu)勢?人們通常聲稱定值不是必要的,對于小的項目可能是正確的。 對于大的項目,將action types定義為定值有如下好處:

  • 幫助維護命名一致性,因為所有的 action type 匯總在同一位置。
  • 有的時候,在開發(fā)一個新功能之前你想看到所有現(xiàn)存的 actions ??赡艿那闆r是你的團隊里已經(jīng)有人添加了你所需要的action,而你并不知道。
  • Action types 列表在Pull Request中能查到所有添加,刪除,修改的記錄。這能幫助團隊中的所有人及時追蹤新功能的范圍與實現(xiàn)。
  • 如果你在導(dǎo)入一個 Action 定值的時候拼寫錯誤,你會得到 undefined 。當你納悶 action 被分發(fā)出去而什么也沒發(fā)生的時候,一個拼寫錯誤更容易被發(fā)現(xiàn)。

你的項目的約定取決與你自己。你開始的時候可能用的是inline string,之后轉(zhuǎn)為定值,也許之后將他們歸為一個獨立文件。Redux 不會給予任何建議,選擇你自己最喜歡的。

Action Creators

另一個約定是,你創(chuàng)建生成 action 對象的函數(shù),而不是在你分發(fā)的時候內(nèi)聯(lián)生成它們。

例如,用文字對象取代調(diào)用 dispatch

// somewhere in an event handler
dispatch({
  type: 'ADD_TODO',
  text: 'Use Redux'
});

你可以在單獨的文件中寫一個 action creator ,然后從 component 里導(dǎo)入:

actionCreators.js

export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  };
}

AddTodo.js

import { addTodo } from './actionCreators';

// event handler 里的某處
dispatch(addTodo('Use Redux'))

Action creators 總被當作模板受到批評。好吧,其實你并不用把他們寫出來!如果你覺得更適合你的項目你可以選用對象文字 然而,你應(yīng)該知道寫 action creators 是存在某種優(yōu)勢的。

假設(shè)有個設(shè)計師看完我們的原型之后回來說,我們需要允許三個 todo 不能再多了。我們可以使用 redux-thunk 中間件添加一個提前退出,把我們的 action creator 重寫成回調(diào)形式:

function addTodoWithoutCheck(text) {
  return {
    type: 'ADD_TODO',
    text
  };
}

export function addTodo(text) {
  // Redux Thunk 中間件允許這種形式
  // 在下面的 “異步 Action Creators” 段落中有寫
  return function (dispatch, getState) {
    if (getState().todos.length === 3) {
      // 提前退出
      return;
    }

    dispatch(addTodoWithoutCheck(text));
  }
}

我們剛修改了 addTodo action creator 的行為,對調(diào)用它的代碼完全不可見。我們不用擔心去看每個添加 todo 的地方保證他們有了這個檢查 Action creator 讓你可以解耦額外的分發(fā) action 邏輯與實際的 components 發(fā)送這些 actions,而且當你在重開發(fā)經(jīng)常要改變需求的時候也會非常有用。

生成 Action Creators

某些框架如 Flummox 自動從 action creator 函數(shù)定義生成 action type 定值。這個想法是說你不需要 ADD_TODO 定值和 addTodo() action creator兩個都自己定義。這樣的方法在底層也生成 action type 定值,但他們是隱式生成的,也就是間接級。

我們不建議用這樣的方法。如果你寫像這樣簡單的 action creator 寫煩了:

export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  };
}

export function removeTodo(id) {
  return {
    type: 'REMOVE_TODO',
    id
  };
}

你可以寫一個生成 action creator 的函數(shù):

function makeActionCreator(type, ...argNames) {
  return function(...args) {
    let action = { type };
    argNames.forEach((arg, index) => {
      action[argNames[index]] = args[index];
    });
    return action;
  }
}

export const addTodo = makeActionCreator('ADD_TODO', 'todo');
export const removeTodo = makeActionCreator('REMOVE_TODO', 'id');

參見 redux-action-utilsredux-actions 獲得更多介紹這樣的常用工具。

注意這樣的工具給你的代碼添加了魔法。魔法和間接聲明真的值得多寫一兩行代碼么?

異步 Action Creators

中間件 讓你注入一個定制邏輯,可以在每個 action 對象分發(fā)出去之前解釋。異步 actions 是中間件的最常見用例。

沒有中間件的話,dispatch 只能接收一個普通對象。所以我們在 components 里面進行 AJAX 調(diào)用:

actionCreators.js

export function loadPostsSuccess(userId, response) {
  return {
    type: 'LOAD_POSTS_SUCCESS',
    userId,
    response
  };
}

export function loadPostsFailure(userId, error) {
  return {
    type: 'LOAD_POSTS_FAILURE',
    userId,
    error
  };
}

export function loadPostsRequest(userId) {
  return {
    type: 'LOAD_POSTS_REQUEST',
    userId
  };
}

UserInfo.js

import { Component } from 'react';
import { connect } from 'react-redux';
import { loadPostsRequest, loadPostsSuccess, loadPostsFailure } from './actionCreators';

class Posts extends Component {
  loadData(userId) {
    // 調(diào)用 React Redux `connect()` 注入 props :
    let { dispatch, posts } = this.props;

    if (posts[userId]) {
      // 這里是被緩存的數(shù)據(jù)!啥也不做。
      return;
    }

    // Reducer 可以通過設(shè)置 `isFetching` 反應(yīng)這個 action 
    // 因此讓我們顯示一個 Spinner 控件。
    dispatch(loadPostsRequest(userId));

    // Reducer 可以通過填寫 `users` 反應(yīng)這些 actions
    fetch(`http://myapi.com/users/${userId}/posts`).then(
      response => dispatch(loadPostsSuccess(userId, response)),
      error => dispatch(loadPostsFailure(userId, error))
    );
  }

  componentDidMount() {
    this.loadData(this.props.userId);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.userId !== this.props.userId) {
      this.loadData(nextProps.userId);
    }
  }

  render() {
    if (this.props.isLoading) {
      return <p>Loading...</p>;
    }

    let posts = this.props.posts.map(post =>
      <Post post={post} key={post.id} />
    );

    return <div>{posts}</div>;
  }
}

export default connect(state => ({
  posts: state.posts
}))(Posts);

然而,不久就需要再來一遍,因為不同的 components 從同樣的 API 端點請求數(shù)據(jù)。而且,我們想要在多個components 中重用一些邏輯(比如,當緩存數(shù)據(jù)有效的時候提前退出)。

中間件讓我們寫的更清楚M的潛在的異步 action creators. 它使得我們分發(fā)普通對象之外的東西,并且解釋它們的值。比如,中間件能 “捕捉” 到已經(jīng)分發(fā)的 Promises 并把他們變?yōu)橐粚φ埱蠛统晒?失敗 actions.

最簡單的中間件例子是 redux-thunk. “Thunk” 中間件讓你把 action creators 寫成 “thunks”,也就是返回函數(shù)的函數(shù)。 這使得控制被反轉(zhuǎn)了: 你會像一個參數(shù)一樣取得 dispatch ,所以你也能寫一個多次分發(fā)的 action creator 。

注意

Thunk 只是中間件的一個例子。中間件不是關(guān)于 “讓你分發(fā)函數(shù)” 的:它是關(guān)于讓你分發(fā)你用的特定中間件知道如何處理的任何東西的。Thunk 中間件添加了一個特定的行為用來分發(fā)函數(shù),但這實際上取決于你用的中間件。

考慮上面的代碼用 redux-thunk 重寫:

actionCreators.js

export function loadPosts(userId) {
  // 用 thunk 中間件解釋:
  return function (dispatch, getState) {
    let { posts } = getState();
    if (posts[userId]) {
      // 這里是數(shù)據(jù)緩存!啥也不做。
      return;
    }

    dispatch({
      type: 'LOAD_POSTS_REQUEST',
      userId
    });

    // 異步分發(fā)原味 actions 
    fetch(`http://myapi.com/users/${userId}/posts`).then(
      response => dispatch({
        type: 'LOAD_POSTS_SUCCESS',
        userId,
        respone
      }),
      error => dispatch({
        type: 'LOAD_POSTS_FAILURE',
        userId,
        error
      })
    );
  }
}

UserInfo.js

import { Component } from 'react';
import { connect } from 'react-redux';
import { loadPosts } from './actionCreators';

class Posts extends Component {
  componentDidMount() {
    this.props.dispatch(loadPosts(this.props.userId));
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.userId !== this.props.userId) {
      this.props.dispatch(loadPosts(nextProps.userId));
    }
  }

  render() {
    if (this.props.isLoading) {
      return <p>Loading...</p>;
    }

    let posts = this.props.posts.map(post =>
      <Post post={post} key={post.id} />
    );

    return <div>{posts}</div>;
  }
}

export default connect(state => ({
  posts: state.posts
}))(Posts);

這樣打得字少多了!如果你喜歡,你還是可以保留 “原味” action creators 比如從一個 “聰明的” loadPosts action creator 里用到的 loadPostsSuccess 。

最后,你可以重寫中間件 你可以把上面的模式泛化,然后代之以這樣的異步 action creators :

export function loadPosts(userId) {
  return {
    // 要在之前和之后發(fā)送的 action types 
    types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
    // 檢查緩存 (可選):
    shouldCallAPI: (state) => !state.users[userId],
    // 進行?。?    callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
    // 在 actions 的開始和結(jié)束注入的參數(shù)
    payload: { userId }
  };
}

解釋這個 actions 的中間件可以像這樣:

function callAPIMiddleware({ dispatch, getState }) {
  return function (next) {
    return function (action) {
      const {
        types,
        callAPI,
        shouldCallAPI = () => true,
        payload = {}
      } = action;

      if (!types) {
        // 普通 action:傳走
        return next(action);
      }

      if (
        !Array.isArray(types) ||
        types.length !== 3 ||
        !types.every(type => typeof type === 'string')
      ) {
        throw new Error('Expected an array of three string types.');
      }

      if (typeof callAPI !== 'function') {
        throw new Error('Expected fetch to be a function.');
      }

      if (!shouldCallAPI(getState())) {
        return;
      }

      const [requestType, successType, failureType] = types;

      dispatch(Object.assign({}, payload, {
        type: requestType
      }));

      return callAPI().then(
        response => dispatch(Object.assign({}, payload, {
          response: response,
          type: successType
        })),
        error => dispatch(Object.assign({}, payload, {
          error: error,
          type: failureType
        }))
      );
    };
  };
}

在傳給 applyMiddleware(...middlewares) 一次以后,你能用相同方式寫你的 API-調(diào)用 action creators :

export function loadPosts(userId) {
  return {
    types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
    shouldCallAPI: (state) => !state.users[userId],
    callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
    payload: { userId }
  };
}

export function loadComments(postId) {
  return {
    types: ['LOAD_COMMENTS_REQUEST', 'LOAD_COMMENTS_SUCCESS', 'LOAD_COMMENTS_FAILURE'],
    shouldCallAPI: (state) => !state.posts[postId],
    callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`),
    payload: { postId }
  };
}

export function addComment(postId, message) {
  return {
    types: ['ADD_COMMENT_REQUEST', 'ADD_COMMENT_SUCCESS', 'ADD_COMMENT_FAILURE'],
    callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`, {
      method: 'post',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ message })
    }),
    payload: { postId, message }
  };
}

Reducers

Redux 用函數(shù)描述邏輯更新減少了模版里大量的 Flux stores 。函數(shù)比對象簡單,比類更簡單得多。

考慮這個 Flux store:

let _todos = [];

export default const TodoStore = assign({}, EventEmitter.prototype, {
  getAll() {
    return _todos;
  }
});

AppDispatcher.register(function (action) {
  switch (action.type) {
  case ActionTypes.ADD_TODO:
    let text = action.text.trim();
    _todos.push(text);
    TodoStore.emitChange();
  }
});

用了 Redux 之后,同樣的邏輯更新可以被寫成 reducing function:

export function todos(state = [], action) {
  switch (action.type) {
  case ActionTypes.ADD_TODO:
    let text = action.text.trim();
    return [...state, text];
  default:
    return state;
  }
}

switch 語句 不是 真正的模版。真正的 Flux 模版是概念性的:發(fā)送更新的需求,用 Dispatcher 注冊 Store 的需求,Store 是對象的需求 (當你想要一個哪都能跑的 App 的時候復(fù)雜度會提升)。

不幸的是很多人仍然靠文檔里用沒用 switch 來選擇 Flux 框架。如果你不愛用 switch 你可以用一個單獨的函數(shù)來解決,下面會演示。

生成 Reducers

讓我們寫一個函數(shù)使得我們將 reducers 表達為 action types 到 handlers 的映射對象。例如,在我們的 todos reducer 里這樣定義:

export const todos = createReducer([], {
  [ActionTypes.ADD_TODO](state, action) {
    let text = action.text.trim();
    return [...state, text];
  }
}

我們可以寫下面的幫忙函數(shù)來完成:

function createReducer(initialState, handlers) {
  return function reducer(state = initialState, action) {
    if (handlers.hasOwnProperty(action.type)) {
      return handlers[action.type](state, action);
    } else {
      return state;
    }
  }
}

不難對吧?Redux 沒有默認提供這樣的幫忙函數(shù),因為有好多種寫的方法??赡苣阆胍詣影哑胀?JS 對象變成不可變對象通過濕化服務(wù)器狀態(tài)??赡苣阆牒喜⒎祷貭顟B(tài)和當前狀態(tài)。有很多方法 “獲取所有” handler。這些都取決于你為你的團隊在特定項目中選擇的約定。

Redux reducer 的 API 是 (state, action) => state,但是怎么創(chuàng)建這些 reducers 由你來定。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號