React Router v4 几乎误我一生

React Router v4 几乎误我一生

在写《深入浅出React和Redux》这本书的时候,在2017年春节前后,React Router的正式发布版还是v3,v4还只有beta版,从beta版看来,API有巨大变化,几乎是一个全新的库。我预计当这本书正式出版的时候,v4的正式版可能就已经发布,当然,以React Router一年多憋一个大版本的风格,也很有可能没有发布。

所以,当写这本书第11章《多页面应用》时,就面临一个纠结的选择:是以v4的API来介绍,还是以v3的API来介绍?

最后,我决定还是按照v3的API来介绍,因为毕竟v3版本已经很稳定了,就算v4发布了,并不表示v3是一个过时的产品,而且,我估计v4刚出来的时候估计bug会不少,不要做第一个小白鼠是我们的信条,所以,还是先不要去尝这个螃蟹了。

果然,《深入浅出React和Redux》正式出版前几周,React Router v4正式发布了。

同样,果然,React Router v4有问题。

我还没来得及做小白鼠,就在Facebook上看到台湾的朋友发出绝望的吼声。

我仔细一问这位朋友,他说,React Router v4不容易玩,玩不明白,倒不是有bug什么的,而是有比bug更可怕的事情:照着教材上的代码去写,运行时就出一些莫名其妙的红色错误,当然,大家都明白,肯定是哪里写错了或者写得不恰当,但是出错信息完全看不出来哪里写得有问题!

在这上面耗了一整天也没有进展。最后,他放弃了v4,默默地回去继续用v3了。

嗯,我说过对吧,不要做第一个小白鼠。

现在React Router v4最新版本已经到了v4.1.1,我觉得应该差不多了吧,于是着手把《深入浅出React和Redux》随书的代码升级针对React Routerv4的实现。

好了,现在回顾一下代码“升级”的过程,我的感受就是:唉,我想抓狂,我想骂人!我终于体会到台湾朋友的痛苦!~#&¥@(!``1=_!

还好我撑过来了,所以不算“误我一生”,只能算“几乎误我一生”。

v3到v4的改变,不只是简单的API改变,而是整个设计哲学的改变,这个哲学怎么样,我们在后面再说,我们先来看看如何把书中第11章的代码改为使用React Router v4。

如果对细节没有兴趣,可以直接玩下翻到哲学的部分。

迁移到React Router v4

用词要准确,这里我们用“迁移”,而不用“升级”,因为虽然版本数增加了,但真的谈不上是“升”。

对应的使用v3的代码在这里 mocheng/react-and-redux ,使用了传统的嵌套式路由(Nested Routes),使用了react-router-redux来把routing和Redux关联起来。

github上的Pull Request在这 migrate to react-router-v4 by mocheng · Pull Request #20 · mocheng/react-and-redux ,想看diff代码的直接看。

1. npm的改变

当然,React-Router不再是v3而是v4,还有,和以前只有一个react-router不同,多出来一个react-router-dom,这可能也是受React把dom相关逻辑分离出来一个package的影响。有些函数可以从react-router导入,也可以从react-router-dom导入,但是react-router-dom中有的react-router中不一定有,所以,简单起见,就都从react-router-dom导入吧。

如果要和Redux结合,也不能再用以前v4之前的react-router-redux了,原来的react-router-redux代码库 reactjs/react-router-redux 已经退休,react-router-redux这个招牌已经被react-router接管了,代码库都放在一起了 ReactTraining/react-router ,只是,目前还只发布了npm的alpha版,我们姑且相信这个alpha版也稳定吧。

同样的,以前react-router-redux里创建浏览器历史对象的方法也没有了,需要直接导入history这个npm。

最终package.json中的dependencies部分差不多这样。

  "dependencies": {
    "history": "^4.6.2",
    "react": "^15.4.1",
    "react-dom": "^15.4.1",
    "react-redux": "^5.0.1",
    "react-router": "^4.1.1",
    "react-router-dom": "^4.1.1",
    "react-router-redux": "^5.0.0-alpha.6",
    "redux": "^3.6.0"
  },

2. 不再有嵌套路由

v3中路由是嵌套的,Route里面可以有Route,如下面这样。

 <Router history={history} createElement={createElement}>
   <Route path="/" component={App}>
     <IndexRoute component={Home} />
     <Route path="home" component={Home} />
     <Route path="about" component={About} />
     <Route path="*" component={NotFound} />
   </Route>
 </Router>

最顶层的Route(看清楚了,不是Router,是Route)对应的是App这个React组件,这个组件做的事情就是渲染TopMenu,然后把子组件也渲染出来。

const App = ({children}) => {
  return (
    <div>
      <TopMenu />
      <div>{children}</div>
    </div>
  );
};

因为App对应的Route对应/,所以所有的路由都会先中App,然后App会渲染子组件,渲染子组件其实就是渲染下层的Route,于是,/home就会中Home对应的Route,最后/home就会既画出TopMenu,又画出Home。

v4中,Route中不能再包含Route了,代码要改成这样。

 <Router history={history}>
   <div>
     <TopMenu />
     <Switch>
       <Route exact path="/" component={Home} />
       <Route exact path="/about" component={About} />
       <Route path="*" component={NotFound} />
     </Switch>
   </div>
 </Router>

Route现在和TopMenu一样就是普通的React组件,你看在代码里摆在一个层级,所以,可以认为,Route是在渲染时动态决定路由对应什么组件的。

这里还要用上Switch,代表只渲染子组件Route中第一个匹配的Route,不用Switch的话,NotFound对应的Route也会每次都渲染,那可不是我们想要的。

2. 和Redux联合

和Redux联合的意义,在于让routing信息在Redux Store里也存一份,这样使用Redux Devtools是,可以方便地进行时间旅行(Time Travel),从一个状态跳到另一个状态。既然都已经咬着牙做迁移了,那没有理由不精益求精走完最后一步。

我们使用ConnectedRouter。

 import {ConnectedRouter} from 'react-router-redux';

 <ConnectedRouter history={history}>
   <div>
     <TopMenu />
     <Switch>
       <Route exact path="/" component={Home} />
       <Route exact path="/about" component={About} />
       <Route path="*" component={NotFound} />
     </Switch>
   </div>
 </ConnectedRouter>

差不就是这个迁移的过程。

React Router v4的哲学

平心而论,React Router v3的设计更容易前端工程师接受,因为前端工程师往往都接触过类似的路由配置设计,常见的框架,比如express、rails等,虽然细节各有不同,但是都是一个套路:将path映射为渲染模块,而且这种映射关系是静态的。

换句话说,只要程序一启动,映射关系就不能改变了。

React Router v4觉得这样不够牛逼,要玩就玩动态映射,在这里有他们的哲学

具体实现方式也很巧妙,既然React组件渲染是动态发生的,那么就让Route变成一个React组件,和其他组件一样被渲染,在运行时完全可以决定某个Route渲染还是不渲染,也可以决定渲染这个Route的参数props是怎样,如此一来,路由规则也就完全动态了。

不过,还是那句老话,工具只是我们的武器,根据实际需要来用,别枪没玩好,把自己的脚给崩了。

如果你的应用根本不需要动态路由,那么v3就足够使用,如果你也没有兴趣去探索v4复杂的API,那就继续使用v3吧,React Router v3和v4是两个完全独立的库,并不是说用v4就真的比v3更厉害。

多想一步

让我们多思考一点,按道理说,路由这个功能是程序和外界(浏览器URL)的交互,可以看做一种输入输出(I/O)操作,可是,从纯函数式编程的原则来说,程序中的纯函数是不应该参与I/O操作的,React一直高举函数式变成的大旗,怎么作为React组件的Route能够和IO操作扯上关系呢?

仔细一想,其实组件的渲染过程也是一种副作用(side effect),因为在浏览器中渲染DOM也是一种输出操作。

在Cycle.js中,对副作用的管理比React更加严格,把DOM操作当做side effect处理,独立在data flow之外。

React的组件的render函数虽然是一个纯函数,但是渲染的过程却是有副作用的,如果利用这一点,就可以用React组件的渲染过程来实现副作用,比如,mount某个组件,实现对一个API的调用,实现当前网页的跳转。

当然,这只是一种可能,知道这种可能就行,并不表示我们一定会使用这些技巧,就像并不一定要使用React Router的v4版一样。

编辑于 2017-06-17 23:35