最近将公司项目的 react-router 从 v3 版本升到了 v4 版本,react-router v4 跟 v3 彻底不兼容,是一次完全的重写。这也给升级形成了极大的困难,与其说升级不如说是对 router 层重写。以前我也将项目的 react 从 v15 版本升级到了 v16 版本,相较而言升级 react-router 比升级 react 困难多了。升级过程当中踩了很多的坑,也有一些值得分享的点。写成一篇小文,供你们参考。react
react-router v4 跟 react 同样拆成了两部分,核心的 react-router 和依运行环境而定的 react-router-dom 或 react-router-native(跟 react-dom 和 react-native 同样)。本文要说的是浏览器环境,也就是 react-router + react-router-domwebpack
先安装依赖(推荐使用 yarn)git
yarn add react-router react-router-dom history
为何要安装 history 后面会解释。github
以前咱们项目中使用了 react-router-redux 你有不少理由使用它,但对于咱们来讲惟一的理由或者用处就是用于在页面组件以外导航,react-router-redux 让你能够在任何地方经过 dispatch 处理页面跳转,如:store.dispatch(push('/'))。由于这个咱们就必须使用 react-router-redux 吗?固然不须要,有更简单的办法实现这个需求。因此此次升级我移除了react-router-redux, 写做此文时支持 react-router v4 的 react-router-redux 还处于 v5.0.0-alpha.7 也是缘由之一。web
还记得以前安装的 history 吗?history 是 react-router 惟二的主要依赖之一,之因此要显式安装,是由于咱们要使用它来实现页面组件外导航。如下以 browser history 为例(hash history 和 memory history 都是同样的):redux
咱们不使用 react-router-dom 提供的 BrowserRouter 而是本身实现一个react-native
// history.js import createHistory from 'history/createBrowserHistory'; const history = createHistory(); export default history;
// index.js import React from 'react'; import ReactDOM from 'react-dom'; import { Router } from 'react-router'; import history from './history'; import App from './app'; ReactDOM.render( <Router history={history}> <App /> </Router>, document.getElementById('app') );
搞定!就这么简单,这样在任何地方只要引用 history 就可使用它进行导航操做,如 history.push('/'),更多使用方式请参考 history 文档。其实 react-router-dom 的 BrowserRouter 跟咱们作了一样的事,区别在于咱们这么作能把 history 暴露出来。这个 history 就是页面组件 props 里面的 history 天然也就能作一样的事情。浏览器
react-router v3 是面向配置的,组件写法只是一种语法糖。而 react-router v4 是彻底面向组件的,提供的 Route Switch 等都是真正的组件。这也就致使只能按组件的方式写路由,不能写配置。可是 v3 那样的配置确实有一些方便之处,如统一管理、使用方便等。缓存
多亏 JSX 灵活的语法,咱们依然有办法按配置的方式写 react-router v4 的路由。babel
// routes.js import Home from './home'; import About from './about'; import Help from './help'; export default [{ path: '/', exact: true, component: Home }, { path: '/about', component: About }, { path: '/help', component: Help }];
// app.js import React from 'react'; import { Switch, Route } from 'react-router'; import routes from './routes'; import NotFound from './not-found'; class App extends React.Component { render() { return ( <Switch> {routes.map((route, i) => <Route key={i} exact={!!route.exact} path={route.path} component={route.component} />)} <Route component={NotFound} /> </Switch> ); } } export default App;
这样咱们就用配置的方式写出了面向组件的路由,兼顾二者的优势。若是有嵌套路由需求,能够参考官方示例。官方也提供了一个 react-router-config, 不过我没有使用,一来以为不必,二来写做此文时它还处于 v1.0.0-beta.4 版本。
Web 应用最大的一个优点就是没必要下载整个应用,只用下载须要的部分就可使用。要达到这样的目标,就须要对代码进行分片,异步加载组件。惋惜 react-router v4 没有像 v3 同样提供加载异步组件的接口。这部分工做就须要咱们本身来处理。
咱们能够建立一个高阶组件 Bundle,专门用来加载异步组件。
// bundle.js import React from 'react'; class Bundle extends React.Component { constructor(props) { super(props); this.state = { Component: null }; props.load().then(Component => this.setState({ Component: Component.default })); } render() { const { load, ...props } = this.props; const Component = this.state.Component; return Component ? <Component {...props} /> : null; } } export default Bundle;
而后修改一下 routes.js
// routes.js import React from 'react'; import Bundle from './bundle'; export default [{ path: '/', exact: true, component(props) { // 这里的 component 函数也是一个高阶组件 return <Bundle {...props} load={() => import('./home')} />; } }, { path: '/about', component(props) { return <Bundle {...props} load={() => import('./about')} />; } }, { path: '/help', component(props) { return <Bundle {...props} load={() => import('./help')} />; } }];
这样每一个页面都会打包成单独的 JS,访问相应页面才会去异步加载对应的组件。这样也能够作精细化缓存控制。
须要注意的是 import() 语法在写做本文时还处于 Stage 2 的状态,需给 Babel 添加 syntax-dynamic-import 插件才能正常工做,另外需 webpack 2 及以上才支持。
由于各类缘由 react-router v4 再也不解析 ?key=value 这样的 URL 的查询参数,页面组件 props.location 中只有 search 字符串。这跟 v3 不兼容,并且很不方便。咱们有办法兼容一下吗?固然有,这时候以前写的 histroy.js 又有新的用处了。
// history.js import qs from 'qs'; import createHistory from 'history/createBrowserHistory'; function addQuery(history) { const location = history.location; history.location = { ...location, query: qs.parse(location.search, { ignoreQueryPrefix: true }) }; } const history = createHistory(); addQuery(history); export const unlisten = history.listen(() => { // 每次页面跳转都会执行 addQuery(history); }); export default history;
这样咱们就能在页面组件 props.location.query 拿到解析好的 URL 查询参数了,跟 v3 完美兼容。还有个额外的好处是在任何地方引用 history 均可以拿到解析好的 URL 查询参数。须要注意的是,在 history 的设计中,history 对象是 Mutable 的,因此咱们能够直接修改 history。可是 history.location 是 Immutable 的,因此咱们要确保每个 location 对象都是全新的。
react-router v4 跟 redux 搭配有一个大坑(mobx 应该也有一样的问题),详情请看这篇文章,这里就再也不赘述。简单来讲,若是一个组件用 redux 的 connect 包装过,又️不是 Route 的子组件,那么 history 的变动就不会触发这个组件的更新,它的子组件天然也不会更新。好比应用的根组件(上文的 App)。
解决方案也很简单,能够用 react-router v4 提供的 withRouter 再包装一遍:withRouter(connect(...)(App)),或者让 App 作为 Router 的子组件,原理都同样。我采用的后者。
// app.js import React from 'react'; import { connect } from 'react-redux'; import { Switch, Route } from 'react-router'; import routes from './routes'; import NotFound from './not-found'; class App extends React.Component { render() { return ( <Switch> {routes.map((route, i) => <Route key={i} exact={!!route.exact} path={route.path} component={route.component} />)} <Route component={NotFound} /> </Switch> ); } } function mapStateToProps(state) { return { someState: state.someState }; } export default connect(mapStateToProps)(App);
// index.js import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { Router, Route } from 'react-router'; import store from './store'; import history from './history'; import App from './app'; ReactDOM.render( <Provider store={store}> <Router history={history}> <Route component={App} /> {/* 没有 path 就会匹配全部路由 */} </Router> </Provider>, document.getElementById('app') );
不得不说升级 react-router 很困难,坑也不少。可是把坑一个个填完,最终完美升级也是一件颇有意思,颇有成就感的事。但愿这篇文章能对你有所帮助。
另外完整的 Demo 请戳个人 GitHub,喜欢的话点个 Star 吧 :P