在之前的浅谈 Flux 架构及 Redux 实践一文中我们初步的谈及了 Redux 的数据流思想,并做了一个简单的加减器。但是还没有接触到 Redux 更多常用的场景,异步操作、API 调用,如何连接到 UI 层等,Redux 可以与很多框架搭配包括 Vue、React 甚至是纯 JavaScript。后面我们会用一个实例–通过 github API 获取个人信息,来将 Redux middleware、async action、连接到 React 贯穿其中。先看看我们最后写的 demo 的样子。

/images/redux-demo.png

Middleware 与异步 Action

依然先看看 Redux 作者 Dam 的描述:

It provides a third-party extension point between dispatching an
action, and the moment it reaches the reducer.

我的理解是,middleware 提供了一个你可以修改 action 的机制,这和 Express/Koa 的中间件有些类似,只不过这里的中间件主要是操作 action。中间件对异步的 action 实现非常重要,因为在之前的文章中我们谈到,action 是一个行为抽象,只是一个对象,reducer 是一个纯函数,不应该有 API 调用和副作用的操作。那么怎么解决异步的问题?我们肯定不能在 reducer 中写,那么就考虑到了 action -> reducer 这个过程,这就是 redux middleware:

action -> middleware modify action -> reducer

它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。 你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。

在上一篇文章中我们使用的同步 action,action creator 返回的是一个对象,但是异步 action 可以是一个函数,虽然函数也是对象,这里我们只是为了区分两种不同的情况。通过使用指定的 middleware,action creator 可以返回函数。这时,这个 action creator 就成为了 thunk。当 action creator 返回函数时,这个函数会被 Redux Thunk middleware 执行。这个函数并不需要保持纯净,它还可以带有副作用,包括执行异步 API 请求。这个函数还可以 dispatch action,就像 dispatch 前面定义的同步 action 一样。那么如何在 action 中进行网络请求?标准的做法是使用 Redux Thunk middleware。要引入 redux-thunk 这个专门的库才能使用。

搭建工作流

我们将采用 ES6 语法,webpack 进行打包,webpack-dev-server 启一个本地服务器,然后用 HMR 技术进行 React 热加载,看看 webpack 配置信息:

var webpack = require("webpack");
var OpenBrowserPlugin = require("open-browser-webpack-plugin");

module.exports = {
  entry: {
    index: [
      "webpack/hot/dev-server",
      "webpack-dev-server/client?http://localhost:8080",
      "./src/index.js",
    ],
  },
  output: {
    path: "./build",
    filename: "[name].js",
  },
  devtool: "source-map",
  module: {
    loaders: [
      {
        test: /\.js$/,
        loader: "babel",
        query: {
          presets: ["es2015", "stage-0", "react"],
        },
      },
      {
        test: /\.less$/,
        loader: "style!css!less",
      },
    ],
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin(),
    new OpenBrowserPlugin({ url: "http://localhost:8080" }),
  ],
};

其中open-browser-webpack-plugin插件将会帮助我们自动打开浏览器,用babel进行 es 编译,less来维护我们的 css 样式,以及使用 dev-tool 来生成 source map,HotModuleReplacementPlugin来进行热更新。

再看看我们最后的目录结构:

├── build
│   ├── index.html
│   └── index.js
├── node_modules
├── package.json
├── src
│   ├── actions
│   │   └── actions.js
│   ├── components
│   │   ├── index.js
│   │   ├── Profile
│   │   │   ├── Profile.js
│   │   │   └── Profile.less
│   │   └── Search
│   │       ├── Search.js
│   │       └── Search.less
│   ├── containers
│   │   ├── App.js
│   │   ├── App.less
│   │   └── test.less
│   ├── index.html
│   ├── index.js
│   └── reducers
│       └── reducers.js
└── webpack.config.js

其中containers放置我们的容器组件,components放置展示性组件,打包入口是index.js

Demo

Redux

state

使用 Redux 非常重要的一点就是设计好顶层的 state,在 demo 中我们需要的 state 大概长这个样子:

{
  isFetchingData, // boolean
  username, // string
  profile, // object
}

其中isFetchingData是网络请求的状态,正在拉取数据为 true,username是我们要获取用户信息的名字,profile是我们拉取用户的详细信息,这个将会是一个 Ajax 请求,最后由 github API 提供。

actions

同步 action 我们不再讲述,上一篇文章已经说得比较清楚,这里我们重点说异步 action,app 的所有 action 如下:

export const GET_INFO = "GET_INFO"; // 获取用户信息
export const FETCHING_DATA = "FETCHING_DATA"; // 拉取状态
export const RECEIVE_USER_DATA = "RECEIVE_USER_DATA"; //接收到拉取的状态

// async action creator
export function fetchUserInfo(username) {
  return function (dispatch) {
    dispatch(fetchingData(true));
    return fetch(`https://api.github.com/users/${username}`)
      .then((response) => {
        console.log(response);
        return response.json();
      })
      .then((json) => {
        console.log(json);
        return json;
      })
      .then((json) => {
        dispatch(receiveUserData(json));
      })
      .then(() => dispatch(fetchingData(false)));
  };
}

上面网络请求用到了fetch这个 API,它会返回一个 Promise,还比较新可以使用社区提供的 polyfill 或者使用纯粹的 XHR 都行,这都不是重点。我们看看这个 action 生成函数返回了一个函数,并且在这个函数中还有dispatch操作,我们通过中间件传入的 dispatch 可以用来 dispatch actions。在上面的 promise 链式中首先我们打印了 github API 返回 Response object,然后输出了 json 格式的数据,然后 dispatch 了RECEIVE_USER_DATA这个 action 表示接收到了网络请求,并需要修改 state(注:这里我们没有考虑网络请求失败的情况),最后我们 dispatch 了FETCHING_DATA并告诉对应 reducer 下一个 state 的 isFetchingData 为 false,表示数据拉取完毕。

reducer

这里看看最核心的 reducer,操作 profile 这一块的:

function profile(state = {}, action) {
  switch (action.type) {
    case GET_INFO:
      return Object.assign({}, state, {
        username: action.username,
      });
    case RECEIVE_USER_DATA:
      return Object.assign({}, state, action.profile);
    default: return state;
  }
}
function isFetchingData() {...}
function username() {...}
const rootReducer = combineReducers({
  isFetchingData,
  username,
  profile,
});
export default rootReducer;

将拉取到的 profile 对象 assign 到之前的 state,最后通过combineReducers函数合并为一个 reducer。

连接到 React

我们通过react-redux提供的connect方法与Provider来连接到 React,Provider主要的作用是包装我们的容器组件,connect用于将 redux 与 react 进行连接,connect() 允许你从 Redux store 中指定准确的 state 到你想要获取的组件中。这让你能获取到任何级别颗粒度的数据,了解更多可以参考它的API,这里我们不再敖述。它的形式可以是这样:

function mapStateToProps(state) {
  return {
    profile: state.profile,
    isFetchingData: state.isFetchingData,
  };
}

function mapDispatchToProps(dispatch) {
  return {
    fetchUserInfo: (username) => dispatch(fetchUserInfo(username)),
  };
}

class App extends Component {
  render() {
    const { fetchUserInfo, profile, isFetchingData } = this.props;
    return (
      <div className="container">
        <Search fetchUserInfo={fetchUserInfo} isFetchingData={isFetchingData} />
        {"name" in profile ? (
          <Profile profile={profile} isFetchingData={isFetchingData} />
        ) : (
          ""
        )}
      </div>
    );
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App);

connect是个可以执行两次的柯里化函数,第一次传入的参数相当于一系列的定制化东西,第二次传入的是你要连接的 React 组件,然后返回一个新的 React 组件。第一次执行时传入的参数是 mapStateToProps, mapDispatchToProps, mergeProps, options。也就是说这里相当于帮组容器选择它在整个 Store 中所需的 state 与 dispatch 回调,这些将会被 connect 以 Props 的形式绑定到 App 容器,我们可以通过 React 开发者工具看到这一点:

第一次执行,选择需要的 state,第二次传入 App 容器组件然后返回新的组件。然后创建整个应用的 store:

const loggerMiddleware = createLogger();
const store = createStore(
  rootReducer,
  compose(
    applyMiddleware(thunkMiddleware, loggerMiddleware),
    window.devToolsExtension ? window.devToolsExtension() : (f) => f
  )
);

这里我们用到了两个中间件,loggerMiddleware用于输出我们每一次的 action,可以明确的看到每次 action 后 state 的前后状态,thunkMiddleware用于网络请求处理,最后window.devToolsExtension ? window.devToolsExtension() : f => f是为了连接我们的redux-dev-tool,可以明确的看到我们 dispatch 的 action,还能达到时间旅行的效果。最后通过Provider输入我们的 store,整个应用就跑起来啦!

let mountRoot = document.getElementById("app");
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  mountRoot
);

Run

命令行输入npm run dev,整个应用就跑起来了,在输入框输入 Jiavan,我们来看看 action 与数据流:

在 console 面板,logger 中间件为我们打印除了每一次 dispatch action 以及前后的 state 值,是不是非常直观,然而厉害的还在后面。redux-dev-tool 可以直接查看我们 state tree 以及对 action 做 undo 操作达到时间旅行的效果!

完整的 demo 在文章最后将会贴出,现在总结下:首先我们规划了整个应用的 state,然后进行数据流层的代码开发,同步异步 action 的编写以及 reducer 的开发,再通过选择我们容器组件所需的 state 与 dispatch 回调通过 connect 方法绑定后输出新的组件,通过创建 store 与 Provider 将其与 React 连接,这样整个应用的任督二脉就被打通了。最后极力推荐 Redux 的官方文档。

完整 demo -> https://github.com/Jiavan/react-async-redux

运行

1. npm install
2. webpack
3. npm run dev

参考文章:

转载请注明出处 http://jiavan.com/react-async-redux/