晴明的博客园 GitHub      CodePen      CodeWars     

[js] redux (3)

redux

action

我们应该尽量减少在 action 中传递的数据?

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

Redux 中只需把 action 创建函数的结果传给 dispatch() 方法即可发起一次 dispatch 过程。

dispatch(addTodo(text))
dispatch(completeTodo(index))

或者创建一个 被绑定的 action 创建函数 来自动 dispatch:

const boundAddTodo = (text) => dispatch(addTodo(text))
const boundCompleteTodo = (index) => dispatch(completeTodo(index))

然后直接调用它们:

boundAddTodo(text);
boundCompleteTodo(index);

store 里能直接通过 store.dispatch() 调用 dispatch() 方法,但是多数情况下你会使用 react-redux 提供的 connect() 帮助器来调用。bindActionCreators() 可以自动把多个 action 创建函数 绑定到 dispatch() 方法上。

Reducer

Action 只是描述了有事情发生了这一事实,并没有指明应用如何更新 state。而这正是 reducer 要做的事情。

[{id:1,name:'a'},{id:2,name:'b'}]转为[1,2] {1:{name:'a'},2:{name:'b'}}

永远不要在 reducer 里做这些操作:

  1. 修改传入参数;
  2. 执行有副作用的操作,如 API 请求和路由跳转;
  3. 调用非纯函数,如 Date.now() 或 Math.random()。

不要修改 state。 使用 Object.assign() 新建了一个副本。不能这样使用 Object.assign(state, { visibilityFilter: action.filter }),因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。你也可以开启对ES7提案对象展开运算符的支持, 从而使用 { ...state, ...newState } 达到相同的目的。

combineReducers 接收一个对象,可以把所有顶级的 reducer 放到一个独立的文件中,通过 export 暴露出每个 reducer 函数,然后使用 import * as reducers 得到一个以它们名字作为 key 的 object:

import { combineReducers } from 'redux'
import * as reducers from './reducers'

const todoApp = combineReducers(reducers)

Store

Store 有以下职责:

维持应用的 state;
提供 getState() 方法获取 state;
提供 dispatch(action) 方法更新 state;
通过 subscribe(listener) 注册监听器;
通过 subscribe(listener) 返回的函数注销监听器。

createStore() 的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。

let store = createStore(todoApp, window.STATE_FROM_SERVER)

可以在任何地方调用 store.dispatch(action),包括组件中、XHR 回调中、甚至定时器中。

定义 mapStateToProps 这个函数来指定如何把当前 Redux store state 映射到展示组件的 props 中

改变 store 内 state 的惟一途径是对它 dispatch 一个 action。

applyMiddleware() 加入中间件

thunk 的一个优点是它的结果可以再次被 dispatch

默认情况下,createStore() 所创建的 Redux store 没有使用 middleware,所以只支持 同步数据流。
可以使用 applyMiddleware() 来增强 createStore()。

尝试实现redux-logger

1.手动记录

let action = addTodo('Use Redux')

console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())

2.封装 Dispatch
用它替换 store.dispatch(),但每次都要导入一个外部方法总归还是不太方便。

function dispatchAndLog(store, action) {
  console.log('dispatching', action)
  store.dispatch(action)
  console.log('next state', store.getState())
}

dispatchAndLog(store, addTodo('Use Redux'))

3.Monkeypatching Dispatch
Redux store 只是一个包含一些方法的普通对象,因此可以这样实现 dispatch 的 monkeypatch:

let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

使用这种写法,当dispatch 上需要挂载其他方法时就不太方便了

function patchStoreToAddLogging(store) {
  let next = store.dispatch
  store.dispatch = function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

function patchStoreToAddCrashReporting(store) {
  let next = store.dispatch
  store.dispatch = function dispatchAndReportErrors(action) {
    try {
      return next(action)
    } catch (err) {
      console.error('捕获一个异常!', err)
      Raven.captureException(err, {
        extra: {
          action,
          state: store.getState()
        }
      })
      throw err
    }
  }
}

# 如果这些功能以不同的模块发布,需要在 store 中像这样使用它们:

patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)

4.隐藏 Monkeypatching
Monkeypatching 本质上是一种 hack。在之前用自己的函数替换掉了 store.dispatch。如果不这样做,而是在函数中返回新的 dispatch:

function logger(store) {
  let next = store.dispatch

  // 我们之前的做法:
  // store.dispatch = function dispatchAndLog(action) {

  return function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

然后可以在 Redux 内部提供一个可以将实际的 monkeypatching 应用到 store.dispatch 中的辅助方法:

function applyMiddlewareByMonkeypatching(store, middlewares) {
  middlewares = middlewares.slice();
  middlewares.reverse();

  // 在每一个 middleware 中变换 dispatch 方法。
  middlewares.forEach(middleware =>
    store.dispatch = middleware(store)
  )
}

# 然后像这样应用多个 middleware:

applyMiddlewareByMonkeypatching(store, [ logger, crashReporter ])

尽管做了很多,实现方式依旧是 monkeypatching。
因为仅仅是将它隐藏在框架的内部,并没有改变这个事实。

5.移除 Monkeypatching
为什么要替换原来的 dispatch 呢?
这样可以在后面直接调用它,
但是还有另一个原因:就是每一个 middleware 都可以操作(或者直接调用)前一个 middleware 包装过的 store.dispatch:

function logger(store) {
  // 这里的 next 必须指向前一个 middleware 返回的函数:
  let next = store.dispatch;

  return function dispatchAndLog(action) {
    console.log('dispatching', action);
    let result = next(action);
    console.log('next state', store.getState());
    return result;
  }
}

将 middleware 串连起来的必要性是显而易见的。

如果 applyMiddlewareByMonkeypatching 方法中没有在第一个 middleware 执行时立即替换掉 store.dispatch,那么 store.dispatch 将会一直指向原始的 dispatch 方法。也就是说,第二个 middleware 依旧会作用在原始的 dispatch 方法。

但是,还有另一种方式来实现这种链式调用的效果。可以让 middleware 以方法参数的形式接收一个 next() 方法,而不是通过 store 的实例去获取。

function logger(store) {
  return function wrapDispatchToAddLogging(next) {
    return function dispatchAndLog(action) {
      console.log('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      return result;
    }
  }
}

ES6 的箭头函数可以使其 柯里化 ,简化上面的代码:

const logger = store => next => action => {
  console.log('dispatching', action);
  let result = next(action);
  console.log('next state', store.getState());
  return result;
}

const crashReporter = store => next => action => {
  try {
    return next(action);
  } catch (err) {
    console.error('Caught an exception!', err);
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err;
  }
}

这正是 Redux middleware 的样子。

Middleware 接收了一个 next() 的 dispatch 函数,并返回一个 dispatch 函数,返回的函数会被作为下一个 middleware 的 next(),以此类推。由于 store 中类似 getState() 的方法依旧非常有用,将 store 作为顶层的参数,使得它可以在所有 middleware 中被使用。

6.“单纯”地使用 Middleware
可以写一个 applyMiddleware() 方法替换掉原来的 applyMiddlewareByMonkeypatching()。在新的 applyMiddleware() 中,取得最终完整的被包装过的 dispatch() 函数,并返回一个 store 的副本:

// 这只是一种“单纯”的实现方式
// 这并不是Redux 的 API.

function applyMiddleware(store, middlewares) {
  middlewares = middlewares.slice();
  middlewares.reverse();

  let dispatch = store.dispatch;
  middlewares.forEach(middleware =>
    dispatch = middleware(store)(dispatch)
  )

  return Object.assign({}, store, { dispatch });
}

这与 Redux 中 applyMiddleware() 的实现已经很接近了,但是有三个重要的不同之处:

  • 它只暴露一个 store API 的子集给 middleware:dispatch(action) 和 getState()。
  • 它用了一个非常巧妙的方式,以确保如果在 middleware 中调用的是 store.dispatch(action) 而不是 next(action),那么这个操作会再次遍历包含当前 middleware 在内的整个 middleware 链。这对异步的 middleware 非常有用。
  • 为了保证只能应用 middleware 一次,它作用在 createStore() 上而不是 store 本身。因此它的签名不是 (store, middlewares) => store, 而是 (...middlewares) => (createStore) => createStore。

由于在使用之前需要先应用方法到 createStore() 之上有些麻烦,createStore() 也接受将希望被应用的函数作为最后一个可选参数传入。

7.最终的方法
这是刚刚所写的 middleware:

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err
  }
}

然后将它们引用到 Redux store 中:

import { createStore, combineReducers, applyMiddleware } from 'redux'

let todoApp = combineReducers(reducers)
let store = createStore(
  todoApp,
  // applyMiddleware() 告诉 createStore() 如何处理中间件
  applyMiddleware(logger, crashReporter)
)

现在任何被发送到 store 的 action 都会经过 logger 和 crashReporter:

// 将经过 logger 和 crashReporter 两个 middleware
store.dispatch(addTodo('Use Redux'))

写一个用于生成 action creator 的函数

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

const ADD_TODO = 'ADD_TODO'
const EDIT_TODO = 'EDIT_TODO'
const REMOVE_TODO = 'REMOVE_TODO'

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

一些工具库也可以帮助生成 action creator ,例如 redux-act 和 redux-actions 。

异步 Action Creators

export function loadPosts(userId) {
  // 用 thunk 中间件解释:
  return function (dispatch, getState) {
    let { posts } = getState();
    if (posts[userId]) {
      // 这里是数据缓存!啥也不做。
      return;
    }

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

    // 异步分发原味 action
    fetch(`http://myapi.com/users/${userId}/posts`).then(
      response => dispatch({
        type: 'LOAD_POSTS_SUCCESS',
        userId,
        response
      }),
      error => dispatch({
        type: 'LOAD_POSTS_FAILURE',
        userId,
        error
      })
    );
  }
}

改写

export function loadPosts(userId) {
  return {
    // 要在之前和之后发送的 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 的开始和结束注入的参数
    payload: { userId }
  };
}

解释这个 actions 的中间件可以像这样:

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

    if (!types) {
      // Normal action: pass it on
      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 callAPI 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,
        type: successType
      })),
      error => dispatch(Object.assign({}, payload, {
        error,
        type: failureType
      }))
    )
  }
}

Reducers 生成器

写一个函数将 reducers 表达为 action types 到 handlers 的映射对象。

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

现在可以这样写

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

其他

支持类的静态属性

class MyClass {
  static myStaticProp = 42;

  constructor() {
    console.log(MyClass.myStaticProp); // 42
  }
}

需要安装
babel-plugin-transform-class-properties

支持对象里扩展运算符的使用

let n = { x, y, ...z };
console.log(n); // { x: 1, y: 2, a: 3, b: 4 }

需要安装
babel-plugin-transform-object-rest-spread

posted @ 2017-09-14 21:05  晴明桑  阅读(157)  评论(0编辑  收藏  举报