Skip to content

Latest commit

 

History

History
579 lines (479 loc) · 19.4 KB

React-Router-V4-Migration-Guide.md

File metadata and controls

579 lines (479 loc) · 19.4 KB

React Router v4 非官方迁移指南

这是一篇译文,BY 张振衣(dancerphil@github),原文,2017-10-09

除非另行注明,页面上所有内容采用MIT协议共享

React-router v4 提供了更简单快速的的路由,但是你是不是有能力升级呢?

React-Router v4 在 v3 版本的基础上引入了一系列激进的改动:现在,<Route> components 是 real React components。只是一个巨大的成果,让 React 应用的路由变得十分简单。

不幸的是,react-router v4 改变了创建和嵌套路由的方式,现有的 react-router v3 应用在 v4 的框架下是 不能工作 的。你当然可以升级现有的 app,但是并不能做到 straightforward。我发现. I discovered this hard truth by upgrading admin-on-rest, the React admin framework for REST services, to react-router v4. 由于没有官方的迁移指南(更新: 有一个不完整的指南,但它被隐藏了),我决定分享我个人的经验,我的迁移路径和一些实际建议。

Note: 对于一个中等规模的 react 应用程序,你需要至少一天的时间来进行迁移。

下面的列表列出了你所需要完成的工作:

As a bonus:

react-router 包现在改名为 react-router-dom

主模块已经改名了,所以你需要卸载以前的包,并安装新包:

npm uninstall react-router --save
npm install react-router-dom --save

使用 git grep react-router 找到所有引用了旧包的代码,然后按照下面的格式替换 import 语句(或者require()):

// from
import { Route, Redirect } from 'react-router';
// to
import { Route, Redirect } from 'react-router-dom';

使用特殊化的 Router 组件

<Router> 过去需要一个 history prop。而在 v4,你可以使用特殊化的 router 组件来代替,采取以下的步骤:

// v3
import { Router, hashHistory } from 'react-router';
const MyApp = () => (
    <Router history={hashHistory}>
        ...
    </Router>
);
// v4
import { HashRouter } from 'react-router-dom';
const MyApp = () => (
    <HashRouter>
        ...
    </HashRouter>
);

然而,如果你需要将你的 history 和一个状态管理库进行同步,比如 Redux(过后会有更多的讨论),那么你就必须要继续使用 <Router> 组件,并且传递一个 history 对象,但这一次,是从 history 库得到这个对象:

// v4
import { Router } from 'react-router-dom';
import createHashHistory from 'history/createHashHistory';
const history = createHashHistory();
const MyApp = () => (
    <Router history={history}>
        ...
    </Router>
);

为 Routes 列表添加 <Switch>

<Route> 组件不再具有排他性。这意味着即使有一个 route 符合当前的 url,也不会阻止它和其他的 route 组件进行匹配。

我们假设你定义了以下路由,v3 风格:

import { Router, Route } from 'react-router-dom';
const MyApp = () => (
    <Router history={history}>
        <Route path="/posts" component={PostList} />
        <Route path="/posts/:id" component={PostEdit} />
        <Route path="/posts/:id/show" component={PostShow} />
        <Route path="/posts/:id/delete" component={PostDelete} />
    </Router>
)

在 v4,类似 /posts/12/show 的路径会同时触发前三个 route !你需要用一个 <Switch> 组件包裹住 <Routes> 列表,来防止一个集合中的多个路由匹配:

import { Router, Route, Switch } from 'react-router-dom';
const MyApp = () => (
    <Router history={history}>
        <Switch>
            <Route path="/posts" component={PostList} />
            <Route path="/posts/:id" component={PostEdit} />
            <Route path="/posts/:id/show" component={PostShow} />
            <Route path="/posts/:id/delete" component={PostDelete} />
        </Switch>
    </Router>
)

在 Routes 上添加 exact 属性

但这还不够:按照这个顺序,对于路径 /posts/12/show 来说,被渲染的 route 会是第一个(因为 url 匹配了 /posts 的形式)。为了获得与 v3 相类似的行为,你需要在你的 route 上添加 exact 属性:

import { Router, Route, Switch } from 'react-router-dom';
const MyApp = () => (
    <Router history={history}>
        <Switch>
            <Route exact path="/posts" component={PostList} />
            <Route exact path="/posts/:id" component={PostEdit} />
            <Route exact path="/posts/:id/show" component={PostShow} />
            <Route exact path="/posts/:id/delete" component={PostDelete} />
        </Switch>
    </Router>
)

不再使用嵌套路由

React-router v3 曾经支持嵌套路由:

// in src/MyApp.js
const MyApp = () => (
    <Router history={history}>
        <Route path="/main" component={Layout}>
            <Route path="/foo" component={Foo} />
            <Route path="/bar" component={Bar} />
        </Route>
    </Router>
)
// in src/Layout.js
const Layout = ({ children }) => (
    <div className="body">
        <h1 className="title">MyApp</h1>
        <div className="content">
            {children}
        </div>
    </div>
)

Layout 组件中,{children} 将被子路由组件(FooBar)所取代。

React-router v4 不再支持嵌套路由。你现在必须将嵌套路由放在子组件中:

// in src/MyApp.js
const MyApp = () => (
    <Router history={history}>
        <Route path="/main" component={Layout} />
    </Router>
)
// in src/Layout.js
const Layout = () => (
    <div className="body">
        <h1 className="title">MyApp</h1>
        <div className="content">
            <Switch>
                <Route path="/main/foo" component={Foo} />
                <Route path="/main/bar" component={Bar} />
            </Switch>
        </div>
    </div>
);

这是 react-router v4 最大的新特性:你可以把 <Route> 放在任何地方!

请注意,即使在其他的路由的<Route> 后代组件中, path 也必须是一个绝对路径。如果你不想重复的书写 top-level 路径,你可以使用 match 属性(由 react-router 向组件注入)来组合一个路径,如下所示:

// in src/Layout.js
const Layout = ({ match }) => (
    <div className="body">
        <h1 className="title">MyApp</h1>
        <div className="content">
            <Switch>
                <Route path={`${match.url}/foo`} component={Foo} />
                <Route path={`${match.url}/foo`} component={Bar} />
            </Switch>
        </div>
    </div>
);

对相对路由的支持正在进行中.

不再有 <IndexRoute>

在 v3 中,<IndexRoute> 组件可以允许路由在 top-level 路径时,路由至某个确定的组件上:

// in src/MyApp.js
const MyApp = () => (
    <Router history={history}>
        <Route path="/" component={Layout}>
            <IndexRoute component={Dashboard} />
            <Route path="/foo" component={Foo} />
            <Route path="/bar" component={Bar} />
        </Route>
    </Router>
)

这个组件在 v4 中不再存在。 你可以使用 <Switch>exact 和路由排序(把 index route 放在最后)的组合来替换它:

// in src/MyApp.js
const MyApp = () => {
    <Router history={history}>
        <Route path="/" component={Layout} />
    </Router>
}
// in src/Layout.js
const Layout = () => (
    <div className="body">
        <h1 className="title">MyApp</h1>
        <div className="content">
            <Switch>
                <Route exact path="/foo" component={Foo} />
                <Route exact path="/bar" component={Bar} />
                <Route exact path="/" component={Dashboard} />
            </Switch>
        </div>
    </div>
);

使用 match.params 来代替 params

在 v3 中,你可能会使用 params 属性来获取路径参数:

// in src/MyApp.js
const MyApp = () => (
    <Router history={history}>
        <Route path="/posts/:id" component={PostEdit} />
    </Router>
)
// in src/PostEdit.js
const PostEdit = ({ params }) => (
    <div>
        <h1>Post #{params.id}</h1>
        ...
    </div>
)

这个属性在 v4 中不再注入了,你可以在 match.params 找到相同的数据。

// v4
const PostEdit = ({ match }) => (
    <div>
        <h1>Post #{match.params.id}</h1>
        ...
    </div>
)

对了,params 在 v4 不再是默认解码的。你总是可以使用 decodeURIComponent 来正确地处理特殊字符。

Parse The Query String Yourself

React-router v3 used to parse the query string by default, storing the query parameters as an object accessible in location.query. For instance, you could grab the sort query parameter in the /posts?sort=foo path as follows:

// in src/PostList.js
const PostList = ({ location }) => (
    <div>
        <h1>List sorted by {location.query.sort}</h1>
    </div>
);

Well, location.query doesn’t exist anymore in v4, and the query string isn’t parsed at all. You’ll have to parse location.search by hand, using a third-party library (like query-string).

import { parse } from 'query-string';
const PostList = ({ location }) => {
    const query = parse(location.search);
    return (
        <div>
            <h1>List sorted by {query.sort}</h1>
        </div>
    );
};

Tip: If you use Redux, and if you parse the query string in mapStateToProps(), it’s a good idea to use a library like reselect to memoize the parsed query string, and avoid reparsing it on every state change.

Replace onEnter with componentWillMount

The onEnter prop was commonly used to verify user credentials before every route.

// v3
// in src/MyApp.js
import checkCredentials from '../checkCredentials';
function redirectToLoginIfNotAuthenticated = (nextState, replace, callback) =>
    checkCredentials()
        .then(callback)
        .catch(e => replace({ pathname: '/login' })
<Route onEnter={redirectToLoginIfNotAuthenticated} component={PostList} />

onEnter doesn’t exist anymore in v4. To do a check before rendering a route, place the code in componentWillMount and componentWillReceiveProps:

// v4
// in src/PostList.js
import { withRouter } from 'react-router-dom';
import checkCredentials from '../checkCredentials';
class PostList extends Component {
    componentWillMount() {
        this.checkAuthentication(this.props);
    }
    componentWillReceiveProps(nextProps) {
        if (nextProps.location !== this.props.location) {
            this.checkAuthentication(nextProps);
        }
    }
    checkAuthentication(params) {
        const { history } = params;
        checkCredentials()
            .catch(e => history.replace({ pathname: '/login' }));
    }
    render() {
        // ...
    }
}
export withRouter(PostList);
// in src/MyApp.js
<Route component={PostList} />

But this forces you to use class components. Besides, repeating the componentWillMount logic in all the components you want to place behind a login is cumbersome.

A good way to refactor the authentication logic is to move it to another component, or to a higher-order component. The following HOC, called restricted, makes the checkAuthentication check on mount:

// in src/restricted.js
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import checkCredentials from '../checkCredentials';
/**
 * Higher-order component (HOC) to wrap restricted pages
 */
export function BaseComponent => {
    class Restricted extends Component {
        componentWillMount() {
            this.checkAuthentication(this.props);
        }
        componentWillReceiveProps(nextProps) {
            if (nextProps.location !== this.props.location) {
                this.checkAuthentication(nextProps);
            }
        }
        checkAuthentication(params) {
            const { history } = params;
            checkCredentials()
                .catch(e => history.replace({ pathname: '/login' }));
        }
        render() {
            return <BaseComponent {...this.props} />;
        }
    }
    return withRouter(Restricted);
}

Use this HOC as follows:

const FooPage = () => { ... };
<Route path="/foo" component={restricted(FooPage)} />

Redux Integration

If your app uses Redux, you probably used reactjs/react-router-redux. This library is no longer maintained, and the react-training organization has taken over the maintenance of the Redux binding for react-router v4.

So升级 your package.json to use react-router-redux~5.0.0 (currently in alpha, but don’t worry), and update your code from:

import { combineReducers, createStore, compose, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { Router, Route, hashHistory } from 'react-router';
import { syncHistoryWithStore, routerMiddleware, routerReducer } from 'react-router-redux';
const history = syncHistoryWithStore(hashHistory, store)
const MyApp = () => {
    const reducer = combineReducers({
        ... // your reducers here
        routing: routerReducer,
    });
    const store = createStore(reducer, undefined, compose(
        applyMiddleware(sagaMiddleware, routerMiddleware(hashHistory)),
        window.devToolsExtension ? window.devToolsExtension() : f => f,
    ));
    return (
        <Provider store={store}>
            <Router history={history}>
                <Route ... />
            </Router>
        </Provider>
    );
}

To:

import { combineReducers, createStore, compose, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { Route, Switch } from 'react-router-dom';
import createHistory from 'history/createHashHistory';
import { ConnectedRouter, routerReducer, routerMiddleware } from 'react-router-redux';
const MyApp = () => {
    const reducer = combineReducers({
        ... // your reducers here
        routing: routerReducer,
    });
    const history = createHistory();
    const store = createStore(reducer, undefined, compose(
        applyMiddleware(routerMiddleware(history)),
        window.devToolsExtension ? window.devToolsExtension() : f => f,
    ));
    return (
        <Provider store={store}>
            <ConnectedRouter history={history}>
                <Switch>
                    <Route ... />
                </Switch>
            </ConnectedRouter>
        </Provider>
    );
};

The main differences are:

  • Use <ConnectedRouter> instead of <Router>

  • Pass the same history object to both the Redux middleware and the React Router component (no more syncHistoryWithStore)

Just as with the previous version, the Redux binding will give you Route actions that you can listen to in your actions and sagas.

Overall, the Redux integration is simplified in v4, and you don’t have to wonder which binding library to use — it’s now part of react-router.

You Can Pass Props To Route Components

In v3, in order to pass dynamic props to a route component, you had to use the withProp higher order component:

import withProps from 'recompose/withProps';
import Dashboard from './Dashboard';
const MyApp = ({ title }) => {
    const DashboardWithTitle = withProps(Dashboard, { title });
    return (
        <Router history={history}>
            <Route path="/" component={DashboardWithTitle} />
        </Router>
    );
}

Now you can use the render props to pass the props you want:

import Dashboard from './Dashboard';
const MyApp = ({ title }) => {
    return (
        <Router history={history}>
            <Route path="/" render={props =>
                <Dashboard title={title} {...props} />
            } />
        </Router>
    );
}

Custom Route Components Are Now A Piece Of Cake

Creating a custom Route component (e.g. a component that would embed several routes to handle Creation, Retrieval, Update, and Deletion of a resource) was almost impossible with react-router v3. I managed to find a workaround, but the trick felt a bit hacky. This was because the v3 router implementation was a bit hacky, too.

In v4, <Route> components are simple React components. So creating custom route components requires no special trick. Here is how the <CrudRoute> looks like in admin-on-rest:

import React, { createElement } from 'react';
import { Route, Switch } from 'react-router-dom';
const CrudRoute = ({ resource, list, create, edit, show, remove }) => {
    // inject the resource prop
    const ResourcePage = component => routeProps => createElement(component, {
        resource, ...routeProps })}
    return (
        <Switch>
            <Route
                exact
                path={`/${resource}`}
                render={ResourcePage(list)}
            />
            <Route
                exact
                path={`/${resource}/create`}
                render={ResourcePage(create)}
            />
            <Route
                exact
                path={`/${resource}/:id`}
                render={ResourcePage(edit)}
            />
            <Route
                exact
                path={`/${resource}/:id/show`}
                render={ResourcePage(show)}
            />
            <Route
                exact
                path={`/${resource}/:id/delete`}
                render={ResourcePage(remove)}
            />
        </Switch>
    );
};
export default CrudRoute;

Conclusion

React-router v4 dramatically reduced the size of the router API, and that’s a great idea. By doing less things, the library does it better, and is easier to understand.

But that’s not a small change. Migrating an existing app to v4 will take at least a couple hours, up to a couple days. For instance, the admin-on-rest 迁移 took about 2 days (not a small diff for a library of about 8,000 loc). But once it’s done, the routing logic is much simpler, and the library allows for future improvements that weren’t possible with v2/v3.

I definitely recommend migrating to react-router v4. Kudos to the react-training team for this great piece of software!