react-router 4 升级攻略

react-router 从 2 / 3 升级到 4.x.x

1. 说明

react-router 版本更新到 4.x.x 已经有一段时间了,但是由于 API 改动较大,为了项目的稳定,一直没有更新,最近有了空闲时间,整体对 react 的项目进行了一次升级,这里记录一下 如何从 react-router 2.x.x/3.x.x 迁移到 react-router 4.x.x

本次使用 react-router 版本为 4.1.0

源码地址

React-router-v4 - Webpack 实现按需加载(code-splitting)

导航

  1. 说明
  2. 重要文件版本
  3. 新的API说明
  4. 从 2 / 3 迁移到 4.x.x

2. 重要文件版本

    {
        "postcss-loader": "^1.3.3",
        "react-hot-loader": "^3.0.0-beta.7",
        "webpack": "^2.6.1",
        "webpack-dev-server": "^2.5.0",
        "webpack-merge": "^4.1.0",

        "es6-promise": "^4.1.0",
        "history": "^4.6.3",
        "isomorphic-fetch": "^2.2.1",
        "pure-render-decorator": "^1.2.1",
        "react": "^15.6.1",
        "react-addons-css-transition-group": "^15.6.0",
        "react-dom": "^15.6.1",
        "react-redux": "^5.0.5",
        "react-router-dom": "^4.1.1",
        "react-router-redux": "^5.0.0-alpha.6",
        "react-tap-event-plugin": "^2.0.1",
        "redux": "^3.7.0",
        "redux-thunk": "^2.2.0"
    }

注意:有很多 webpack 配置需要的插件没有列出,因为改动不是很大,对项目运行没有很大的影响,具体的 package.json 可以看源码,这个是旧版的模板,新版本会在近期提交(新版源码

  1. postcss-loader 插件 最新版本已经到了 2.x.x ,但是最新版与 1.x.x配置使用上有出入,如果使用新版本遇到报错,可以查看 官方文档 或者 这篇博客, 大概意思就是不能像之前一样使用 webpack.LoaderOptionsPlugin 配置 这个插件 例如 配置 autoprefixer ,可以添加一个 postcss.config.js 配置文件,或者 在 写成下边这样,我这里还是使用的 1.x.x 版本
    {
        loader:"postcss-loader",
        options: {    
            sourceMap: true,       
            plugins: (loader) => [
                require('autoprefixer')(), 
            ]
        }
    }
  1. webpack 使用的是 2.x.x 的版本,因为测试的时候出了点 BUG ,所以没有升级到最新的 3.x.x

  2. react 相关组件都升级到最新版本,其中 为配合 react-router 4 的使用 react-router-redux 使用的是 5.0.0-alpha.6, 而且增加了 history 组件

  3. react-router-dom 代码是基于 react-router 的 而且做了升级,只用下载 react-router-dom 就可以了

3. 新的 API 说明

这里查看官方文档

1. <Router>

Router 是所有路由组件共用的底层接口,用来与Redux状态管理库的history 保持同步,但是在 4.x.x版本中一般不使用这个组件,而是使用 <BrowserRouter> <HashRouter>

    <Router history={history}>
      <App/>
    </Router>
2. <BrowserRouter>

使用 HTML5 提供的 history API 来保持 UI 和 URL 的同步,一下是他的属性

  1. basename: 设置基准的 URL,使用场景:应用部署在 服务器的二级目录,将其设置为目录名称,不能以 /结尾,设置之后跳转的页面都会加上 basename的前缀

  2. forceRefresh: 是否强制刷新页面,用于适配不支持 h5 history 的浏览器

  3. getConfirmation: 弹出浏览器的确认框,进行确认

3. <Route>

与 2 / 3 版本的 Route 作用一致,都是在 location 匹配 path 的路径的时候,渲染指定组件,但是写法上有变化,而且增加了一些设定

  1. 注意:4.x.x 版本的 <Route>不在有 onEnter onLeave 这样的路由钩子函数,如果需要这个功能,要在 Route 对应组件中 写在 componentWillMount 或者 componentWillUnmount

  2. 渲染内容方法-1:component 类似 2 / 3 中的 component 属性,值为一个 react 组件,只有地址匹配的时候才会渲染组件

    // 定义组件
    class App extends React.Component {
        constructor(props) {
            super(props);
        }
        render() {
            return <span>App</span>;
        }
    }

    // Route
    <Route component={App}/>
  1. 渲染内容方法-2:render 值可以选择传一个在地址匹配时被调用的函数,而不是创建一个组件,但是需要一个返回值,返回一个组件或者null
    <Route render={(props) => (
        <App>
            <SomeCom>
        </App>
    )/>
  1. 渲染内容方法-3:childrenrender 一样,但是不会匹配地址,路径不匹配时 URL的match 值为 null,可以用来根据路由是否匹配动态调整UI

  2. 绑定在 Route 上的组件或者函数都会获得以下的属性 component绑定的组件可以通过 this.props.[history, location, match] 获取;render, children 下的函数;会得到 param = {match, location, history} 的参数

    // 伪代码
    history: {
        goBack(), // 浏览器回退
        goForward(), // 前进
        push(), // 新增跳转
        replace(), // 覆盖跳转
    }
    location: {
        hash, // hash (#123)
        pathname, // 路径 (/home)
        search, // (?id=123)
        state, // ({name: 'name', id: 'id'})
        // query, // 4.x.x 没有 query 了,要从 search 中解析
    }
    match: {
        isExact, // 是否整个URL都需要匹配
        params, // 路径参数,通过解析URL中动态的部分获得的键值对
        path, // 路径格式
        url, // URL匹配的部分
        params, // 
    }
  1. path 属性, 用于匹配 location 路径 如果没有 path 属性则总是匹配

  2. exact 属性 是否需要完全匹配 ,不会判断末尾的/

    /*
        以下两个就是不完全匹配,只有前半部分 一致 /one 
        如果没有设置 exact=true,则可以进入到 path 对应的组件,
        如果设置 exact 则 不能渲染组件
    */ 
    path: /one
    location.pathname : /one/two
  1. strict 属性 用来强制判断路径结尾是否含有/, 只有path 和 loation.pathname 的结尾都含有或者都不含有/ 才会匹配

  2. 关于路由嵌套,4.x.x 不能像以前的版本那样嵌套了,需要新的方式,会在下边的 从 2 / 3 迁移到 4.x.x 中有演示

4. <Redirect>

与 2 / 3 版本一样都是用来重定向到新的地址,默认会覆盖访问记录中的原地址,但是多了一些属性

  1. to={string|object} 重定向的目标,可以是字符串,也可以是一个地址的对象

  2. from={string} 匹配需要重定向的地址

  3. push bool 表示是否需要不替换地址,值为true的时候,不会把访问记录中的地址覆盖

    <Redirect push to={{pathname: '/login', search: '?id=123'}}>
5. <Switch/>

用来渲染匹配地址的第一个 或者 , 可以用来配置过度动画,更多介绍看这里

与 2 / 3 相同,增加了属性 replace 表示是否替换掉原地址

7. withRouter

很重要的一个功能,将普通组件用 withRouter 包裹后,组件就会获得 location history match 三个属性,可以用来直接为子组件提供 历史相关的功能


export default withRouter(App);

// App.js
const {history, location, match} = this.props;

4. 从 2 / 3 迁移到 4.x.x

由于API 改动比较大,使用方式也有变化,因此

1. 构建工具

webpack 的配置文件方面没有什么需要改动的,如果你的插件postcss-loader 更新了,则需要修改一下配置部分

    // webpack.config.js
    rules: [
        {
            test: /\.(scss|sass|css)$/,
            use: [
                'style-loader',
                'css-loader',
                {
                    loader: 'postcss-loader',
                    options: {
                        sourceMap: false,
                        plugins: (loader) => [
                            autoprefixer(config.autoConfig) // autoConfig 为 autoprefixer 的配置
                        ]
                    }
                },
                'sass-loader' + config.sassLoaderSuffix // sass的配置
            ]
        }
    ]
    ...
    // 取消掉之前的 postcss 插件配置
    plugins: [
        // new webpack.LoaderOptionsPlugin({
         //    options: {
         //       context: '/',
         //       minimize: true,
         //       postcss: [autoprefixer(config.autoConfig)]
         //    }
         // }),
    ]
2. app 入口文件 app/js/index.js

改动不大

    /*
       app/js/index.js
       入口文件, 配置 webpack 热加载模块
    */
    import '../scss/index.scss'; // 引入样式

    import React from 'react';
    import ReactDOM from 'react-dom';
    import {AppContainer} from 'react-hot-loader';
    import injectTapEventPlugin from 'react-tap-event-plugin';

    // 引入路由配置模块
    import Root from './routes.js';

    const mountNode = document.getElementById('app'); // 设置要挂在的点

    // react 的插件,提供onTouchTap() // 更新后没有测试是否好使
    injectTapEventPlugin();

    // 封装 render
    const render = (Component) => {
        ReactDOM.render((
            <AppContainer>
                <Component/>
            </AppContainer>
        ), mountNode);
    };

    render(Root); // 初始化
    console.log(process.env.NODE_ENV);

    if (module.hot && process.env.NODE_ENV !== 'production') {
        module.hot.accept('./routes.js', (err) => {
            if (err) {
                console.log(err);
            }
            /*
                卸载 react 模块后 重装
            */ 
            ReactDOM.unmountComponentAtNode(mountNode);
            render(Root);
        });
    }
3. Root 基础的路由配置文件 app/js/routes.js

这部分变动很大,上一个版本还有一个叫Root.js 的文件 用来协助配置 路由,这个版本合并在一起

    /*
       Root, Router 配置
    */
    import React from 'react';
    import {Provider} from 'react-redux';
    import {
        /*
            注意:这里也可以使用 BrowserRouter ,但是为了配合 redux 使用,引入了 react-router-redux,提供了一个用来关联的 ConnectedRouter
        */ 
        // BrowserRouter as Router, 
        Route, 
        Switch, 
        Redirect
    } from 'react-router-dom';
    import {ConnectedRouter} from 'react-router-redux'; // 版本需要 5.0.0-alpha.6
    import createHistory from 'history/createBrowserHistory';

    import store from './store/index';
    import {App, Home, Test} from './containers/index';

    const history = createHistory();
    // Router 下边只能有一个节点
    const Root = () => (
       <Provider store={store}>
          <ConnectedRouter history={history} >
             <div>
                <Switch>
                   <Route path="/" render={(props) => (
                      <App>
                         <Switch>
                            <Route path="/" exact component={Home}/>
                            <Route path="/home" component={Home}/>
                            <Route path="/test" component={Test}/>
                            <Redirect from="/undefined" to={{pathname: '/', search: '?mold=redirect'}}/>
                         </Switch>
                      </App>
                   )}/>
                   <Route render={() => (<Redirect to="/"/>)}/>
                </Switch>
             </div>
          </ConnectedRouter>
       </Provider>
    );
    export default Root;
4. 路由嵌套的写法

4.x.x 定义每一个 Route 都是普通的 react 组件,所有使用的时候也要像普通的组件一样,嵌套的时候,可以按照下边的方式

    // 定义外层路由
    const Root = () => {
        <Provider store={store}>
          <ConnectedRouter history={history} >
             <Route path="/" exact component={App}/>
          </ConnectedRouter>
       </Provider>
    }

    // App.js 组件中定义在App下的路由
    import {Route, Link} from 'react-router-dom';

    class App extends React.Component {
        constructor(props) {
            super(props);
        }
        render() {
            return (
                <div id="app-container">
                    <header className="app-header">成员列表</header>
                    <div className="app-body">
                        <Link to="/test">点击进入 Test 页面</Link>
                        <Route path="/test" component={Test}/>
                    </div>
                 </div>
            );

        }
    }

上边这样的写法,与没有升级前的写法变化很大,但是更加灵活了,不过如果用来版本迁移,工作量会很大,下边是一个用于兼容上个版本代码的写法,可以把所有路由集中在一个文件中管理

    // 方法就是在需要有子级路由的地方使用 render 方法,方法中使用父级路由对应的组件将其他子级路由包裹住
    const Root = () => (
        <Provider store={store}>
            <ConnectedRouter history={history}>
                <div>
                    <Switch>
                        <Route path="/" render={(props) => (
                            <App>
                                <Switch>
                                    <Route path="/" exact component={Login}/>
                                    <Route path="/login" component={Login}/>
                                    <Route path="/unable" component={Unable}/>
                                    <Route path="/show" render={(props) => (
                                        <Show>
                                            <Switch>
                                                <Route path="/show" exact component={Home}/>
                                                <Route path="/show/test" component={Test}/>
                                            </Switch>
                                        </Show>
                                    )}/>
                                    <Route path="/404" component={NotFoundPage}/>
                                </Switch>
                            </App>
                        )}/>
                    </Switch>
                </div>
            </ConnectedRouter>
        </Provider>
    );
5. store 的配置 app/js/store

增加了 router 的中间件

    // store/configureStore.js

    import { compose, createStore, applyMiddleware } from 'redux';
    import { routerMiddleware } from 'react-router-redux'; // 新增 route 中间件
    // 引入thunk 中间件,处理异步操作
    import thunk from 'redux-thunk';
    import createHistory from 'history/createBrowserHistory'; // 引入 history

    export const history = createHistory();
    const routeMiddleware = routerMiddleware(history);

    const middleware = [routeMiddleware, thunk];
    /*
        辅助使用chrome浏览器进行redux调试
    */
    const composeEnhancers =
      process.env.NODE_ENV !== 'production' &&
      typeof window === 'object' &&
      window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
      ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
          // Specify here name, actionsBlacklist, actionsCreators and other options
        }) : compose;

    /*
       调用 applyMiddleware ,使用 middleware 来增强 createStore
    */
    const configureStore = composeEnhancers(
       applyMiddleware(...middleware)
    )(createStore);

    export default configureStore; 
    // store/index.js
    import configureStore from './configureStore';
    import reducer from '../reducers';

    // 给增强后的store传入reducer
    const store = configureStore(reducer);

    export default store;
6. reducer 配置 app/js/reducers/index.js

与升级前没有区别

    // 引入reducer
    import {combineReducers} from 'redux';
    import {routerReducer} from 'react-router-redux';
    import rootReducer from './rootReduer';

    // 合并到主reducer
    const reducer = combineReducers({
        rootReducer,
        routing: routerReducer
    });

    export default reducer;
7. 组件中使用

写在 <Router component={Component}> 中的组件自动获得,location,history,match 三个属性

普通组件使用 需要通过父组件传递或者通过withRouter


    import React, { Component } from 'react';
    import {withRouter} from 'react-router-dom';

    import '../../../scss/app.scss';

    class AppCom extends Component {
       constructor(props) {
          super(props);
       }
       render() {
        // 获得的 location 属性
          const path = this.props.location.pathname;

          return (
             <div id="app-container">
                <header className="app-header">地址:{path}</header>
                <div className="app-body">
                   {this.props.children}
                </div>
             </div>
          );
       }
    }

    export default withRouter(AppCom)
8. 手动跳转页面与路由钩子函数onEnter onLeave

上一个版本中大量使用了 browserHistory 进行页面跳转,这个版本中需要使用 传入的 history 来进行跳转

新版本中路由的onEnter onLeave 方法取消,可以通过组件的 componentWillMount 和 componentWillUnmount 实现

    import React, { Component } from 'react';
    import {withRouter} from 'react-router-dom';

    import confPut from './utils/confPut';

    import '../../../scss/home.scss';

    class TestCom extends Component {
       constructor(props) {
          super(props);
       }
       componentWillMount() {
            // 代替 原 Route 组件的 onEnter()
       }
       componentWillUnmount() {
            // 代替 原 Route 组件的 onLeave()
       }
       handleClick = () => {
          confPut('add', 'will');
          this.props.history.push({
             pathname: '/home',
             search: '?name=testname'
          });
       }
       render() {
          console.log(this.props);

          return (
             <div className="test-container">
                this is Test Page
                <button onClick={this.handleClick}>点击回到</button>
             </div>
          );
       }
    }

    export default withRouter(TestCom);

5. 总结

版本升级工作还在进行中,应该还会有其他的问题出现,这里只是一个阶段的记录,还有一些像 code splitting 还没有尝试,留着下一阶段试验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值