本文主要介绍了开发一个Redux应用所需要的包。原文的地址在此:The Redux ecosystem,By Denis Raslov

现在正在发生什么

Redux & React是现在前端开发的主流技术。每个前端开发人员都应该了解这个事实,而且花费适当的时间去了解和应用它们。因为就现在的状况来看,R&R(表示Redux & React)正代表着未来web应用的开发方向。

然而想要改变开发者的思维来适应R&R不是那么顺畅的。开发者经常使用的框架(例如Backbone、Angular、jQuery等)是以另外一种完全不一样的方式工作。所以开发者需要花费一定的时间来转变思维。

显然,一开始听到Redux和React的时候,开发者都会一头雾水。就以下面这句话为他们的关系开头: React是作为View层的,而Redux是作为Data层的。

对的,就是你想的这样

正如你想的,React使用组件创建了界面,而Redux使用单例的data store、actions以及reducer函数处理数据。

但是,想要创建一个真正的产品级别的应用是不够的。所以下面就聊聊其他需要注意的要点。主要就看看那些R&R不能够帮助解决的问题。真正的恶魔通常都藏在细节中。

可重用组件

首先,当你创建大应用的时候,你需要重用组件。当前没有办法做到给组件层级中所有的组件都自动传递Redux Dispatcher,让它们具备执行action的能力。你只需要一小部分知道应用在做什么的组件,例如需要调用什么Action来改变状态,以及真正站展现的state是什么。让大部分的组件不需要关心这些,而仅仅展示数据以及处理父节点传来的callback就可以了,保持它们simple and stupid。只有这样,你才能让他们可以重用。

可以把上面说到的两种组件分为smart和dumb或者container和presentational。更多信息请参考这里

很幸运的是,已经有现成的工具可以帮助我们来应用这个思想,react-redux。使用该包,我们可以使用具备Dispatcher、Store访问能力的container来包装一个组件,并保证组件的状态会随着store的变化而变化。也就是说,他能够让一个dumb的组件变smart。

其实现的是高阶组件模式——该方法推荐你使用特殊的函数来封装已经存在的组件来创建新的组件。而封装层可以添加初始组件没有的功能,从而让你受益。

技术上来说,Mixins也提供了类似的功能,但mixins存在其他的问题,详情可以看这里

下面举个例子

import React from 'react'
import { connect } from 'react-redux'

// Create the component that you want to make smart.
class MyContainerComponent extends React.Component {
  //...
  
  // Here you have access to the Dispatcher through the props.
  doAction() {
      this.props.dispatch({ 
          type: 'SOME_ACTION',
          data: {}
      });
  }
  
  render() {
      // And you have access to the selected fields of the State too!
      return <div>{this.props.data}</div>;
}

// Select the props which you want to inject in the component, 
// given the global state.
function select(state) {
    return {
        data: state.data
    };
}

// Wrap the component to inject the Dispatcher and the State into it.
export default connect(select)(MyContainerComponent);

你可以将Provider组件放在组件的根节点。这样被封装的组件就能通过connect方式够访问到Store。就像下面这样。

import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import 'MyContainerComponent' from 'components/MyContainerComponent.jsx'
import store from 'store'

// Render the Provider component with the the Store 
// in the props at the root of the hierarhy.
ReactDOM.render(
  <Provider store={store}>
    <MyContainerComponent />
  </Provider>,
  document.getElementById('content')
)

路由

如果你的应用好几个页面,那么你需要定义URL来路由组件。每个前端框架都有router。

React也不例外。然而不像其他的框架,react框架本身是没有router的。相反你需要使用react-router包来提供这个功能。而且他和普通的router的用法还不太一样。

react-router允许你使用声明式的方式描述路由,每个路由仅仅是另外一个React组件。看下代码。

import ReactDOM from 'react-dom'
import { Router, Route, IndexRoute, browserHistory } from 'react-router'

import { Provider } from 'react-redux'
import store from 'store'

import LoginPage from 'components/pages/Login.jsx'
import SignupPage from 'components/pages/Signup.jsx'
import DashboardPage from 'components/pages/Dashboard.jsx'

// Add in the root of your components hierarhy the Router tag, 
// connect it to the browser history. Define the App component as 
// the parent component for pages inside of the Router. Define 
// the set of Routes as the pair of an URL path and a page component.

// And yes, here the Router isn't in the root exactly, because 
// I combined it to the react-redux staff to show much real example.

ReactDOM.render(
  <Provider store={store}>
    <Router history={browserHistory}>
      <Route path="/" component={App}>
        <Route path="login" component={LoginPage} />
        <Route path="signup" component={SignupPage} />
        <Route path="dashboard" component={DashboardPage)} />
        <Route path="*" component={DashboardPage} />
        <IndexRoute component={DashboardPage} />
      </Route>
    </Router>
  </Provider>,
  document.getElementById('content')
)
import React from 'react';

export default class App extends React.Component {
    //...

    // Now you have the "chidren" prop in the props of the App component.
    // That children is the one of the pages components you defined in routes and 
    // you can put in where you want to.
    render() {
        return (
            <div id='page' className='page'>
                {this.props.children}
            </div>
        );
    }
}

根据state数据路由

帅气,现在app可以路由了。但是如果你知道redux架构的最棒的优势的话,你就会问为什么不能够像别的数据一样实现页面切换的**time travelling呢?为什么不能让React Router在我重放action的时候实现页面切换呢?

这是因为Redux和react-router是两个不同的库,而且他们之间也不会自己协调工作。下面我们就想办法让redux和react-router互联。

react-router-redux就是干这个的。当你使用Redux DevTools重放应用state的时候,state的改变会传递到React Router,这样React Router就可以改变组件树了。

想要了解具体的工作原理,就需要知道Redux支持一个牛逼的特性,叫middleware。其在执行action和调用reducers之间提供了三方扩展点。这样你只需要将react-redux-router作为middleware插入到Store中就可以实现我们上面说的这些特性了。code time.

import { combineReducers, applyMiddleware, createStore, compose } from 'redux'
import { routerReducer, routerMiddleware } from 'react-router-redux'
import appReducer from 'appReducer';

// Combine your base app reducer with the router reducer, 
// now the application data will be in the "app" prop 
// and the routing data will be in the "routing" prop of the State.
export function getStore(history) {
    const reducer = combineReducers({
        app: appReducer,
        routing: routerReducer
    })
    
    // Create the routing middleware applying it to history passed through arguments.
    const routingMiddleware = routerMiddleware(history)

    // Apply this middleware to the Store.
    return applyMiddleware(routingMiddleware)(createStore)(reducer);
}
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { Router } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
import { getStore } from 'store'

// Get the store with integrated routing middleware.
const store = getStore(browserHistory);

// Sync browser history with the store.
const history = syncHistoryWithStore(browserHistory, store)

// And use the prepared history in your Router.
ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      //...
    </Router>
  </Provider>,
  document.getElementById('content')
)

认证

没有app是不需要认证的。当你的app中有了路由机制后,认证会决定如何判断一个用户是否登录,并决定其能看到哪个页面。

使用Redux,你的认证数据一定是放在Store中的。其可以是一个名叫‘user’的用户对象。

你在代码中添加了这个之后,,就需要工具来帮助你标记路由是否可达。redux-auth-wrapper就是干这个的。其也是基于高阶组件模式实现的。在react-redux connect方法中我们已经看见过这个例子了。

import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { Router, Route, IndexRoute, browserHistory } from 'react-router'
import { UserAuthWrapper } from 'redux-auth-wrapper'
import { getStore } from 'store'

import App from 'components/App.jsx'
import PageSignup from 'components/pages/signup.jsx'
import PageLogin from 'components/pages/login.jsx'
import PageDashboard from 'components/pages/dashboard.jsx'

// Do the Store staff (see above about it)
const store = getStore(browserHistory);
const history = syncHistoryWithStore(browserHistory, store)

// Create the wrapper that checks if user is authenticated.
const UserIsAuthenticated = UserAuthWrapper({
  // Select the field of the state with auth data
  authSelector: state => state.app.user,
  redirectAction: routerActions.replace,
  // Choose the url to redirect not authenticated users.
  failureRedirectPath: '/login',
  wrapperDisplayName: 'UserIsAuthenticated'
})

// Do the same to create the wrapper that checks if user is NOT authenticated.
const UserIsNotAuthenticated = UserAuthWrapper({
  authSelector: state => state.app.user,
  redirectAction: routerActions.replace,
  failureRedirectPath: '/dashboard',
  // Choose what exactly you need to check in the selected field of the state 
  // (in the previous wrapper it checks by default).
  predicate: user => !user,
  wrapperDisplayName: 'UserIsNotAuthenticated'
})

// Wrap the components in the routes depends on what users have the access to them.
ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      <Route path="/" component={App}>
        <Route path="login" component={UserIsNotAuthenticated(PageLogin)}/>
        <Route path="signup" component={UserIsNotAuthenticated(PageSignup)}/>
        <Route path="dashboard" component={UserIsAuthenticated(PageDashboard)}/>
        <Route path="*" component={UserIsAuthenticated(PageDashboard)}/>
        <IndexRoute component={UserIsAuthenticated(PageDashboard)}/>
      </Route>
    </Router>
  </Provider>,
  document.getElementById('content')
)

异步流

真正的app是通过异步请求数据的API来获得数据的。这意味着你需要只要Redux如何支持异步actions。而这个非常简单就能实现,同样是基于我们已经知道的Redux middleware。 a 这个middleware的名字是redux-thunk。该middleware允许你向Dispatcher传递action函数,而不仅仅是action对象。如果你需要的仅仅是同步的action的话,那么你应该让action函数返回一个action对象。但是如果你需要处理异步的东西的话,你可以写好处理函数,然后使用dispatcher派发另外的结果action。

例如,就拿API来说,一般请求会有成功和失败两种情况,可以根据请求成功还是失败,返回不同的action对象。例子如下:

import request from 'axios';

// Let's assume we need to get the user data through API.
// We create the action function that takes the user id 
// and make the async request.
export function getUser(id) {
    // We have access to the Dispather to dispath 
    // actions inside of the action function.
    return (dispatch) => {
        request
            .get(API_URL + '/users/' + id)
            .then(function(req) {
                // We dispatch the success action 
                // function when the request completes.
                dispatch(setUser(req.data));
            })
            .catch(function(req) {
                // We dispatch the error action function 
                // when the request ends with an error.
                dispatch(setUserError(id));
            });
    };
}

//This is two usual sync action functions.
export function setUser(data) {
    return {type: 'SET_USER', data};
}

export function setUserError(id) {
    return {type: 'SET_USER_ERROR', id};
}
import React from 'react'
import { getUser } from 'actions'

class MyComponent extends React.Component {
    //...
    
    // Dispatch the imported action function with the Dispatcher
    // that you have because of react-redux.
    getUser(id) {
        this.props.dispatch(getUser(id));
    }
}

结论

以上就是我总结的一些在使用R&R写真正的app的时候比较有用的包。如果你对R&R框架不是太熟悉,但已经了解了哪些框架的核心原则,本文推荐你最佳实践如下:

  • 连接Redux和React,使组件能更多的被复用
  • 组织路由
  • 在Redux DevTools中路由数据
  • 组织认证
  • 组织远程API调用的异步数据流

当然,不是所有这些都是你需要的,而且外面的世界也是很精彩的,还有其他各种各样的工具可以使用。尽情玩耍把。