brickspert/blog

react-router v4 使用 history 控制路由跳转

brickspert opened this issue · 93 comments

react-router v4 使用 history 控制路由跳转

问题

当我们使用react-router v3的时候,我们想跳转路由,我们一般这样处理

  1. 我们从react-router导出browserHistory
  2. 我们使用browserHistory.push()等等方法操作路由跳转。

类似下面这样

import browserHistory from 'react-router';

export function addProduct(props) {
  return dispatch =>
    axios.post(`xxx`, props, config)
      .then(response => {
        browserHistory.push('/cart'); //这里
      });
}

but!! 问题来了,在react-router v4中,不提供browserHistory等的导出~~

那怎么办?我如何控制路由跳转呢???

解决方法

1. 使用 withRouter

withRouter高阶组件,提供了history让你使用~

import React from "react";
import {withRouter} from "react-router-dom";

class MyComponent extends React.Component {
  ...
  myFunction() {
    this.props.history.push("/some/Path");
  }
  ...
}
export default withRouter(MyComponent);

这是官方推荐做法哦。但是这种方法用起来有点难受,比如我们想在redux里面使用路由的时候,我们只能在组件把history传递过去。。

就像问题章节的代码那种场景使用,我们就必须从组件中传一个history参数过去。。。

2. 使用 Context

react-router v4Router 组件中通过Contex暴露了一个router对象~

在子组件中使用Context,我们可以获得router对象,如下面例子~

import React from "react";
import PropTypes from "prop-types";

class MyComponent extends React.Component {
  static contextTypes = {
    router: PropTypes.object
  }
  constructor(props, context) {
     super(props, context);
  }
  ...
  myFunction() {
    this.context.router.history.push("/some/Path");
  }
  ...
}

当然,这种方法慎用~尽量不用。因为react不推荐使用contex哦。在未来版本中有可能被抛弃哦。

3. hack

其实分析问题所在,就是v3中把我们传递给Router组件的history又暴露出来,让我们调用了~~

react-router v4 的组件BrowserRouter自己创建了history
并且不暴露出来,不让我们引用了。尴尬~

我们可以不使用推荐的BrowserRouter,依旧使用Router组件。我们自己创建history,其他地方调用自己创建的history。看代码~

  1. 我们自己创建一个history
// src/history.js

import createHistory from 'history/createBrowserHistory';

export default createHistory();
  1. 我们使用Router组件
// src/index.js

import { Router, Link, Route } from 'react-router-dom';
import history from './history';

ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      ...
    </Router>
  </Provider>,
  document.getElementById('root'),
);
  1. 其他地方我们就可以这样用了
import history from './history';

export function addProduct(props) {
  return dispatch =>
    axios.post(`xxx`, props, config)
      .then(response => {
        history.push('/cart'); //这里
      });
}

4. 我非要用BrowserRouter

确实,react-router v4推荐使用BrowserRouter组件,而在第三个解决方案中,我们抛弃了这个组件,又回退使用了Router组件。

怎么办。 你去看看BrowserRouter源码,我觉得你就豁然开朗了。

源码非常简单,没什么东西。我们完全自己写一个BrowserRouter组件,然后替换第三种解决方法中的Router组件。嘿嘿。

讲到这里也结束了,我自己目前在使用第三种方法,虽然官方推荐第一种,我觉得用着比较麻烦唉。~

❤️感谢大家

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

貌似在组件中 可以直接使用 类似
let { history } = this.props; history.push('/cart')

@wagouwan. 不可以的奥。除非你自己传了history过来。

@wsgouwan 你在<Route>组件的下一级使用是没问题的,但是你在孙子组件,或者redux里面是没法使用的哦。

使用react-router-redux也可以的吧?不过这东西,支持react router4的还在开发中....

store.dispatch(push('/foo'))

@weishijun14 可以的,不过一般开发项目中其实用不到react-router-redux的,除非有强烈的管理路由的需求~哈哈。

感觉这样子做的话,react展示组件逻辑不纯了,有没有更好的根据store去跳转的方案呢?

@regiondavid 不是很理解你的意思~~

好棒!!!!!!!!!!!!!!

没有用mobx的嘛?

@sysoft 没用过mobx,不过history的使用方式也是一样的。
第三种方法,用在任何地方都可以的哩。

我用第一种了

第一种方法里的"/some/Path"这个路由是在哪里配置的啊, 我也是使用的这种方法,但是 层级比较深,就不能跳转路由,地址栏改变,路由不跳转

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import RouterComponent from './router/router';
import {BrowserRouter} from 'react-router-dom';
import {Provider} from 'react-redux';
import stores from './store/store';
const storeA = stores();

ReactDOM.render(







, document.getElementById('root'));
registerServiceWorker();

我直接app组件里面,用this.props.history.push()还是能跳转,为什么啊,没有用withRouter

@dongkeying /some/path就是普通的路由呀。。 你看看有没有报错信息。贴出来看看

页面没有报错,而且地址栏已经跳转到"/class/cart"的路由上了,但是页面没有跳转,重点就是我再刷新一下,就显示内容了... 然而直接跳转就不能,必须再刷新一下才显示
路由配置信息:
<Provider store={store}> <Router> <Switch> <Route exact path="/" component={Main} /> <Route path="/class/cart" component={Cart}></Route> <Route path="/search" component={Search} /> <Route component={NoMatch}></Route> </Switch> </Router> </Provider>
组件点击的时候:
gotoDetail(item){ // 传参方式是这样吗 ? this.props.history.push({ pathname: '/cart',state:{item}}); this.props.history.push("/class/cart"); } export default withRouter(Tab);
这个跟组件的嵌套深度有关系吗? 可能是我的路由嵌套太深了,是吗

@dongkeying 和嵌套深度没关系。写法也没啥问题呀。。再然后为什么不跳转,这个不太清楚。没报错信息有点奇怪哦。

@shengjudao
这样的,路由组件
<Route exact path="/" component={Home}/>
在Home组件里面可以访问到history的。
但是子组件就访问不到了哦。除非你手动一级一级传下去。

可是使用了withRouter 不就可以在层级较深的子组件访问到 history 了吗, 是不是就不需要一级一级传下去了

@dongkeying 是的。你的问题很可能代码写错了。既然路由变了,页面没变,肯定路由配置那边有问题。

你好,我想问一下,你在第三种方式hack的时候,import createHistory from 'history/createBrowserHistory'; 不需要npm install --save history 吗?可以直接用?

@yangxiufang1994 不用的,可以直接用。因为react-router v4 帮你安装了history啦。

@brickspert 我只npm install react-router-dom ,我如果不安装,他就会提示我抱错,你也是安装的 react-router-dom 吗?

@yangxiufang1994 是只安装react-router-dom就可以的呀。

有错误可以贴出来。

@brickspert import createHistory from 'history/createHashHistory'
const history = createHistory()
webpack3打包的时候就报Cannot find module 'history/createHashHistory', 我主要是想再ts 里跳一下路由
image

@yangxiufang1994 理论上装了react-router-dom就可以直接用了。 你看看你node_modules里面有木有history

@brickspert 嗯嗯,我在重安装一下,试试看,谢谢你。

我是采用您第三种方式,但是history.push之后页面路径会变化,但没有跳转。所以加了forceRefresh:true的初始化参数,强制每次路径变化刷新页面。这样页面就会有刷新,请问 可以有页面不刷新的路由跳转方式吗?

@Lmagic16 你好。我自己开发中也是使用第三种方法的,并没有使用forceRefresh: true呀。
是不是你代码哪里写的问题了。

@brickspert 谢谢你,现在可以了,主要我之前采用的import { BrowserRouter as Router } 加 的方式,现在采用您这种方式,就可以默认跳转了。

@brickspert 谢谢你,现在可以了,主要我之前采用的import { BrowserRouter as Router } 加 <Router></Router> 的方式,现在采用您<Router history={history}></Router>这种方式,就可以默认跳转了。

@Lmagic16 嗯嗯,不用谢。 全局只能有一个history实例。

import { BrowserRouter as Router }这个他会自己给你创建一个history实例的,相当于你有俩实例了。就不好使了。

开始试了下官方推荐的方法,发现如果用上redux, 需要 withRouter(connect(...)(MyComponent)) 这样包裹着容器组件才能得到 history

UserAdd.js:70 Uncaught TypeError: Cannot read property 'push' of undefined
一直这个错误,会是什么原因。 "react-router": "^4.2.0"

@Sir0xb 难道不是react-router-dom吗?

解决了,代码中确实用的"react-router-dom": "^4.2.2"。做了个provider包装了类,在最后导出的时候用了withRouter,所以实体里一直没有history属性。包装之前调用withRouter就好了。

Uncaught Error:
You should not use or withRouter() outside a

请问这个可能是什么原因呢

@cookcocck

  1. 你错误信息没写全啊。。outside a .... 啥?
  2. 我猜下,withRouter要在Router组件下面的。你全局有没有用Router呢?是不是路由那边写错了。

@brickspert
不好意思,一个很低级的错误~
非常感谢大佬的博客和耐心回复!!!!!!!!!!

点赞,用了你全家桶教程搭建的框架,十分受用。

@cookcocck 我也遇到这个问题,请问是错在哪里了,能说一下吗?

@brickspert 我使用了你的第三种方法,但是路由跳转的时候,url变了,可是UI确没有变,请问是什么原因呢?一下是我的路由配置,我用了react-loadable实现按需加载。
image
image

@SunnyXiao 你的问题解决了么,我没有使用按需加载,但出现了和你一模一样的问题。

@BenlovedLamphere 解决了,直接把路由配置放到app.jsx里面就好

2018-03-26 14 49 27

哪位大神能帮我看看用的方法三,结合《从零开始……》里的计数器例子做的JS跳转,究竟哪里出了问题,满足条件后,URL会成功改变,但UI并未重新加载,还是原来页面的UI。

@BenlovedLamphere 看看你路由咋写的

withRouter
const path = {
pathname: '/detail',
state: item,
}
this.props.history.push(path);
传递过去的参数 如果页面刷新了怎么破?

@jayguojianhai 页面刷新state还在的吧?如果不在了,可用query

@brickspert
2018-03-28 19 33 08
路由这么写的。

另已按要求将代码包上传至您指定邮箱,

十分感谢您的指导。

向你致敬。

大神我用了第三种方法,在Router中使用Link的时候报错this.context.history.createHref is not a function,You should not use outside a ,这个时候怎么让Link能访问自己定义的history

@yushiwho 你是用的第二种方法还是第三种?

@brickspert 第三种

@yushiwho 第三种怎么会有 this.context??

@brickspert 找到原因了,我的history用的是4.3.0,更新到最新的4.7.2就行了,4.3.0这个版本的history没有属性createHref,4.4.1加回去了,e20cebd

你好!我的项目可以正常跑起来,但是控制台一直提示:
warning.js:33 Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.
请问这是由于我的按需加载组件所引起的吗?还是别的原因?
谢谢!

@peterpanBest
把package-lock.json文件删掉,把node_modules删掉,
重新 npm install一下

你好,我有个问题
我在项目里用了BrowserRouter,/:id这种类型的路径可以直接访问到this.props.history,但是/:id/:date这样的没有props,加withroute也没用
咋办呢

没事啦解决了谢谢

本人也是用了方法三,也出现了url改变了,UI没更新,请问怎么解决!

@allenwei123 你node_modules删了,重新npm install下试试。前面碰到的都是一些很低级的错误~你再仔细核对下代码

你好 请问用createHistory怎么添加路由跳转提示,官方文档看完没用明白

你好。我使用的是第三种方法。但是必须添加forceRefresh: true才能成功,否则跳转失败。失败时状况如下:第一级路由通过Redirect跳转至第二级路由,二级路由回跳至一级路由失败;但是在二级路由所在页面刷新一次,则能跳转回一级路由;二级路由中操作即为 this.props.history.push('/') 。不知道为什么??

@wangchuan113057 我觉得是你姿势不对。第三种方法一共三步。你再核对下。尤其是第二步的:

<Router history={history}>  //这里的history对吗

按照你的步伐走的.... 就是这个写法。 在二级路由也是可以成功获取到history的,但是我发现从一级路由进入二级路由,在二级路由操作 this.props.history.push('/'),history 的action属性变成了replace,而不是push....我进行了监听发现了action 由push / 转变成了 replace /home

@brickspert 路由中有#的话 每次都会重新请求后台,怎么才能禁用# ?

@wangchuan113057 大兄弟,我第三种方法不是写的。文件里面跳转路由,这样用吗?

import history from './history';

history.push('xx');

你试试?

或者说,是不是路由里面配置拦截了。碰到/,自动定位到/home????

@PhotonAlpha 不是很懂你的意思呀~ 是说hashHistory吗?

按理单页面应用,不管有没有#,都不会向后台去请求呀。姿势不对?

@brickspert 非常感谢大佬能抽空回复我。
我重新描述一下,我使用的是 3. hack的配置,我在页面中打算使用锚点功能, <a href="#API" >Title</a> ,点击之后 页面URL变成http://localhost:4200/reveal#API,
这个时候,这个组件会重新加载

constructor(props) {
//这里会被调用
        super(props);
    }

这个情况是正常的吗?


大神我找到原因了:
我是参考你的 全家桶 文章的基础上使用了history,这个问题我刚才在全家桶文章的教程中验证过了。
bundle改造 src/router/Bundle.js
在 src/router/router.js 文件中

import UserInfo from 'bundle-loader?lazy&name=userInfo!pages/UserInfo/UserInfo';

const createComponent = (component) => (props) => (
    <Bundle load={component}>
        {
            (Component) => Component ? <Component {...props} /> : <Loading/>
        }
    </Bundle>
);

const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/userinfo">UserInfo</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={createComponent(Home)}/>
                <Route path="/userinfo" component={createComponent(UserInfo)}/>
            </Switch>
        </div>
    </Router>
);

改造了component变成按需加载,这个时候 如果我URL中加入了变成了http://localhost:4200/reveal#API 之后, 组建会重新load(
constructor(props) { super(props); } componentDidMount() {} render() {}
) 都会被调用
当我把代码换回原始版本

import UserInfo from 'pages/UserInfo/UserInfo';
<Route path="/counter" component={UserInfo}/>

URL里面出现 #hash的时候 只会render了。

大神可以帮我把Bundle 组件里面bug修复一下吗?

谢谢了..经过你的提醒我发现是路由拦截的问题....

对于方法三,如果用的是HashRouter,那么在history.js中应引入
import createHistory from 'history/createHashHistory';
同时,HashRouter标签中不再声明history属性
在需要使用的地方,直接引入history.js即可 import history from './history';
这样便不会出现url变化,但路由不跳转的情况,也就不需要forceRefresh: true

按照您的 第三种方法成功了,但是有个疑问
现在在路由组件的儿子组件上:用 props依旧能看到 路由对象:history、location、match,不是说 我们用createBrowserHistoryreact-router-domBrowserRouter 代替了吗? 为什么还能看到这三个对象
image

@hugeorange 这个props里面的history对象,就是我们history.js里面定义的那个hisory对象。
并没有说替换路由,是自定义路由。
第二步,我们不是把自己定义的history传进去了么。

connected-react-router

请问有没有小demo可以给参考一下,因为对redux还不是很了解

使用第三种方法,完美解决

@a12366456 将全局的方法绑定到react的配置文件中,使用脚手架创建的项目可以在你的index.js里面的顶部进行绑定。

// 在你使用React之前

...
import { createBrowserHistory /*createHashHistory */ } from 'history'
...

...
Object.assign(React.Component.prototype, {
  ...
  $router: createBrowserHistory()
})
...

// 使用React.Component

...
componentWillMount () { 
  console.log(this.$router)
}
...

如果使用的hash,这种该怎么解决呢?

已经解决了,把import createHistory from 'history/createBrowserHistory'换为import createHistory from 'history/createHashHistory'就可以

开始试了下官方推荐的方法,发现如果用上redux, 需要 withRouter(connect(...)(MyComponent)) 这样包裹着容器组件才能得到 history

这里才是重点啊,因为有时候需要在组件外的地方使用history,withRouter只能对组件使用,所以第三种方式更灵活

我用第三种方法实现了 但是 history.push("/login") 只改变了url 并没有刷新

我用第三种方法实现了 但是 history.push("/login") 只改变了url 并没有刷新

你确定你做了这一步<Router history={history}>

我用第三种方法实现了 但是 history.push("/login") 只改变了url 并没有刷新

你确定你做了这一步<Router history={history}>

多谢,之前的是用browserRouter包裹的,我换成了Router就可以了

我用第三种方法实现了 但是 history.push("/login") 只改变了url 并没有刷新

#22 这是什么

#22 这是什么

redux吧

mark

完完全全按照第三步来的,跳转后还是不加载页面,是不是5.0版本的history又有改动啊。

我用第三种方法实现了 但是 history.push("/login") 只改变了url 并没有刷新

老哥最后解决了么?

我用第三种方法实现了 但是 history.push("/login") 只改变了url 并没有刷新

你确定你做了这一步<Router history={history}>

多谢,之前的是用browserRouter包裹的,我换成了Router就可以了

Nice!

我用第三种方法实现了 但是 history.push("/login") 只改变了url 并没有刷新

老哥最后解决了么?

我把hisotry5降级为4后解决了