[回到首页](../) | [上一章：6.多模块应用](./6.多模块应用.md)

# 7.规模化

这一章，我们将介绍 pastate 如何构建大规模应用，以及如何在原生 redux 项目中使用等。

* [路由](#路由)
    * [无参数路由](#无参数路由)
        * [使用 Router 作为根容器](#使用-router-作为根容器)
        * [使用 Route 组件来定义路由](#使用-route-组件来定义路由)
    * [有参数路由](#有参数路由)
    * [在 actions 中使用路由](#在-actions-中使用路由)
* [嵌入 redux 应用](#嵌入-redux-应用)
* [开发调试工具](#开发调试工具)
* [中间件](#中间件)
    * [内置中间件](#内置中间件)
    * [自定义中间件](#自定义中间件)
* [编译与部署](#编译与部署)

## 路由
当我们的应用日益复杂时，需要用前端路由的模式来管理多个界面。

### 无参数路由
Pastate 应用可以与通用的 [react-router](https://reacttraining.com/react-router/) 路由框架配合使用。我们还是以 **班级信息管理系统** 为例来介绍如何在 pastate 中使用路由，我们接下来把学生板块和课程板块分别放在 /student 和 /class 路由目录下。

首先安装 react-router 的网页版 react-router-dom：   
```bash
$ npm install react-router-dom --save
或
$ yarn add react-router-dom
```

#### 使用 Router 作为根容器

接着我们用路由 Router 组件作为新的根容器来包装我们的原来的根容器 Navigator，代码如下：  
` src/index.js`  
``` javascript
...
import { BrowserRouter as Router } from 'react-router-dom';

ReactDOM.render(
    makeApp(
        <Router>
            <Navigator.view />
        </Router>,
        storeTree
    ),
    document.getElementById('root')
);
```

#### 使用 Route 组件来定义路由

接下来，我们来看看导航模块 Navigator 的视图如何修改：  
`Navigator.view.js`
```javascript
import { Route, Redirect, Switch, withRouter, NavLink } from 'react-router-dom'

class Navigator extends React.PureComponent{
    render(){
        /** @type {initState} */
        const state = this.props.state;

        return (
            <div>
                <div className="nav">
                    <div className="nav-title">班级信息管理系统</div>
                    <div className="nav-bar">
                        <NavLink 
                            className="nav-item"
                            activeClassName="nav-item-active"
                            to="/student"
                        >
                            学生({this.props.count.student})
                        </NavLink>
                        <NavLink 
                            className="nav-item"
                            activeClassName="nav-item-active"
                            to="/class"
                        >
                            课程({this.props.count.class})
                        </NavLink>
                    </div>
                </div>
                <div className="main-panel">
                    <Switch>
                        <Redirect exact from='/' to='/student'/>
                        <Route path="/student" component={StudentPanel.view}/>
                        <Route path="/class" component={ClassPanel.view}/>
                    </Switch>
                </div>
            </div>
        )
    }
}

export default withRouter(makeContainer(Navigator, state => ({...})))
```  
我们对该文件进行了 3 处修改
- 使用路由组件 `Switch` + `Route` 来代替之前手动判断渲染子模块的代码，让路由自动根据当前 url 选择对应的子组件来显示;
- 使用路由组件 `NavLink` 来代替之前的导航栏按钮，把每个 tab 绑定到一个特定的 url；
- 使用路由的 withRouter 函数包装我们原来生成的 container，这使得当路由变化时，container 会收到通知并重新渲染

完成！这时我们就可以看到，当我们切换导航标签时，url 和显示的子组件同时改变：

![路由生效](https://upload-images.jianshu.io/upload_images/1234637-85d7cb1f736cb7ac.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

当我们在 `/class` 路径下刷新或进入时，应用可以显示为课程板块，这是路由组件为我们提供的一个在原来的无路由模式中没有的功能。

我们这里使用的是 BrowserRouter 对应的 `browserHistory ` ，因此在 url 中是不用 HashRouter 的 `hashHistory ` 模式下的 `#` 分割符来分割前端和后端路由，所以在服务器端需要做一些[相关配置](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#serving-apps-with-client-side-routing)，把这些路径都路由到相同的一个HTML应用。如果后端难以配合修改，你可以使用 `HashRouter` 代替我们上面的 `BrowserRouter`:  

```javascript
// index.js
import {  HashRouter as Router } from 'react-router-dom';
```
这是就会自动启用 `#` （hash路由）来分割前端和后端路由：  

![启用 `#` 来分割前端和后端路由](https://upload-images.jianshu.io/upload_images/1234637-52d37845a7dac2a9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

### 有参数路由
我们在上面的路由不涉及参数，如果我们需要使用类似 `\student\1` 、`\student\2` 的方式来表示目前显示哪一个 index 或 id 的学生，我们可以在定义路由时使用参数：
 
```javascript
// Navigator.view.js
<Route path="/student/:id" component={StudentPanel.view}/>
```
并在被路由的组件的 view 中这样获取参数：

```javascript
// StudentPanel.view.js
let selected = this.props.match.params.id;
```

### 在 actions 中使用路由

如果你要在 actions 中简单地获取当前网址的路径信息，你可以直接使用 `window.location` 获取：

```javascript
// StudentPanel.model.js
const actions = {
    handleClick(){
        console.log(window.location)
        console.log(window.location.pathname) // 当使用 BrowserRouter 时
        console.log(window.location.hash) // 当使用 HashRouter 时
    }
}
```

如果你需要在 actions 获取和**更改**路由信息，如跳转页面等，你可以在 view 视图中把 this.props.history 传入 action, 并通过 [history](https://reacttraining.com/react-router/web/api/history) 的 API 获取并修改路由信息：

```javascript
// StudentPanel.model.js
const actions = {
    selectStudent(history, index){
        console.log(history)
        history.push(index+'') 
        // history.push('/student/' + index)
        // history.goBack() // or .go(-1)
    }
}
```

经过基础的测试，pastate 兼容 react-router 的路由参数、history 等功能，如果你发现问题，请提交 issue 告诉我们。

如果你对开发调试体验的要求非常高，要实现 “Keep your state in sync with your router”， 以支持用开发工具来实现高级调试，可以参考  [react-router-redux](https://github.com/reacttraining/react-router/tree/master/packages/react-router-redux) 把路由功能封装为一个只有 store 而没有 veiw 的 pastate **服务模块**,  并挂载到 storeTree。如果你做了，请提交 issue 告诉我们。

## 嵌入 redux 应用
Pastate 内部使用 redux 作为默认的多模块引擎，这意味着，你可以在原生的 redux 项目中使用 pastate 模块， 只需两步：
1. 使用 Pastate 模块的 store 中提供的 getReduxReducer 获取该模块的 redux reducer，并把它挂载到 redux 的 reducerTree 中; 
2. 把 redux 的 store 实例的 dispatch  注入 astate 模块的 store 的 dispatch 属性 。  

```javascript
import { createStore, combineReducers } from 'redux';
import { makeApp, combineStores } from 'pastate';
...
import * as StudentPanel from './StudentPanel';
const reducerTree = combineReducers({
    panel1: oldReducer1,
    panel2: oldReducer2,
    panel3: StudentPanel.store.getReduxReducer() // 1. 获取 Pastate 模块的 reducer 并挂载到你想要的位置
})

let reduxStore = createStore(reducerTree)
StudentPanel.store.dispatch = reduxStore.dispatch // 2. 把 redux 的 dispatch 注入 Pastate 模块中
```

完成！这时你就可以在 redux 应用中渐进式地使用 pastate 啦！
如果你在使用 [dva.js](https://github.com/dvajs/dva) 等基于 redux 开发的框架，同样可以用这种方式嵌入 pastate 模块。 

## 开发调试工具

由于 pastate 内部使用 redux 作为多模块引擎，所以你可以直接使用 [redux devtools](https://github.com/gaearon/redux-devtools) 作为 pastate 应用的调试工具。Pastate 对其做了友好支持，你无需任何配置，就可以直接打开 redux devtools 来调试你的应用：

![redux devtools](https://upload-images.jianshu.io/upload_images/1234637-975b104c81c24c53.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

使用 redux devtools  **不要求** 你把 pastate 嵌入到原生 redux 应用中，你不需要懂得什么是 redux，在纯 pastate 项目中就可以使用 redux devtools！

## 中间件
### 内置中间件
Pastate 目前实现了基于 actions 的中间件系统，可用于对 actions 的调用进行前置或后置处理。Pastate 内置了几个实用的中间件**生成器**：
- logActions(time: boolean = true, spend: boolean = true, args: boolean = true):  把 actions 的调用情况 log 到控制台
- syncActions(onlyMutations: boolean = false): 把每个 action 都转化为同步 action, 方便互相调用时的调试观察每个 action 对数据的改变情况
- dispalyActionNamesInReduxTool(onlyMutations: boolean = false): 把 actions 名称显示在 redux devtools 中，方便调试

### 自定义中间件
你也可以很轻松的定义中间件，pastate 中间件定义方式和 [koa](https://github.com/koajs/koa) 中间件类似，例如我们定义一个简单的 log 中间件：

```javascript
const myLogMiddleware = function (ctx, next) {
    let before = Date.now();
    next();
    console.log(ctx.name + ': '+ (Date.now() - before) + 'ms');
}
```
- `ctx` 参数是上下文（context）对象，包括如下属性：
```
type MiddlewareContext = {
    name: string, // action 的名称
    agrs?: IArguments, // action 的调用参数
    return: any, // action 的返回值
    store: XStore // action 绑定的 store
}
```
- `next` 参数是下一个中间件或已做参数绑定的 action 实体, 你可以实现自己的中间件逻辑，决定要不要调用或在什么时候调用 next。

## 编译与部署
Pastate 推荐使用 [create-react-app](https://github.com/facebook/create-react-app) 来作为应用的初始化和开发工具，create-react-app 不只是一个简单的 react 应用模板，它还为我们提供了非常完善的 react 开发支持，详见其文档。

在 pastate 应用中，你可以简单的使用默认的 build 指令来编译应用：
```bash
$ npm run build
或
$ yarn build
```
其他编译和部署方式请参考[这里](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#deployment)。

Pastate 在编译之后仅为 ~28kb, gzip 之后仅为 ~9kb，非常轻便。Pastate 包的整体结构如下（图中显示的是未 gzip 的大小）：

![ pastate 包结构](https://upload-images.jianshu.io/upload_images/1234637-5bd6ca443442ccc4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

[下一章](./8.原理与API文档.md)，我们来详细介绍 pastate 的原理。