react-router路由的模式选择

用过react-router的会比较熟悉react路由模式,一般有两种,分别是hashHistory和history, 使用hashHistory模式,url后面会带有#号不太美观,而使用history模式,就是正常的url,但是如果匹配不到这个路由就会出现404请求。这种情况需要在服务器配置,如果URL匹配不到任何静态资源,就跳转到默认的index.html

两种方式实现原理

1.hashHistory路由

hash值变化不会导致浏览器向服务器发出请求,而且 hash 改变会触发 hashchange 事件,浏览器的进后退也能对其进行控制

http://localhost:3000/detail#/home,这段url的#号后面的就为hash值
window.location.hash 取到的就是#home

  1. 1. //监听hash变化
  2. 2. window.addEventListener ('hashchange', (e)=> {
  3. 3. this.setState({
  4. 4. ...this.state,
  5. 5. location:{
  6. 6. ...location,
  7. 7. hash:window.location.hash
  8. 8. pathname:window.location.hash
  9. 9. },
  10. 10. })
  11. 11. });
  12. 12. 复制代码

2.history路由

window.history 对象表示窗口的浏览历史,它只有back()、forward() 和 go() 方法可以让用户调用, 而h5规范中又新增了几个关于操作history记录的APi,分别是replaceState,pushState,popstate
在点击浏览器前进和后退的时候,都会触发popstate事件,而采用pushState和replaceState不会触发此事件,

  1. 1. 代码示例
  2. 2. /*
  3. 3. state 要跳转到的URL对应的状态信息,可以存一些需要想保存的值,也可以直接传{}
  4. 4. title 该条记录的title,现在大多数浏览器不支持或者忽略这个参数
  5. 5. url 这个参数提供了新历史纪录的地址,可以是相对路径,不可跨域
  6. 6. */
  7. 7. window.history.pushState(state, title, url)
  8. 8. //replaceState和pushState的不同之处在与,replace是替换栈顶上的那个元素,不会影响栈的长度
  9. 9. window.history.replaceState(state, title, url)
  10. 10.
  11. 11. //例子
  12. 12. window.addEventListener('popstate',(e)=>{
  13. 13. this.setState({
  14. 14. ...this.state,
  15. 15. location:{
  16. 16. ...location,
  17. 17. pathname:e.state.path,
  18. 18. },
  19. 19. })
  20. 20. })
  21. 21. 复制代码

实现路由

有了以上的知识点,就可以动手写组件了,在动手写组件之前,先来看看官方路由的具体用法,才能知道如何去设计这些组件

模块导入和导出
  1. 1. import { HashRouter as Router, Route,Link, Redirect,Switch,} from 'react-router-dom';
  2. 2. 复制代码

react-router-dom中引出了很多的组件,模块中向外部导出接口,常见的做法是文件夹中有一个index.js向外暴露出这个模块的所有接口,所以可以设计为react-router-dom文件夹会下有一堆组件,通过一个index.js,使用export defalut向外部导出接口对接

路由中的组件使用示例
  1. 1. //router.js 配置路由
  2. 2. export default const BasicRouter = () => {
  3. 3. return (
  4. 4. <div>
  5. 5. <Router>
  6. 6. <div>
  7. 7. <div>
  8. 8. <Link to="/home">首页</Link>
  9. 9. <Link to="/detail">详情</Link>
  10. 10. </div>
  11. 11. <Switch>
  12. 12. <Route path="/home" component={Home} />
  13. 13. <Route path="/detail" component={Detail} />
  14. 14. <Redirect to="/home" />
  15. 15. </Switch>
  16. 16. </div>
  17. 17. </Router>
  18. 18. </div>
  19. 19. )
  20. 20. }
  21. 21. 复制代码

可以看到Router是最外层的父组件,它里面的每个子组件都可以从props中拿到Router组件中的state,router看作是父组件,而里面的route、Switch组件等,一般做法是采用porps向下级传递的方法,但如父子组件中间跨了多个子组件,采用props传值就很麻烦,这里采用组件的context来传递共享数据

  1. 1. //使用路由后,在所有子组件中打印this.props,会发现有这一陀东西,这里只是router组件中的部分state状态
  2. 2. {
  3. 3. history:{
  4. 4. replace:e=>{},
  5. 5. push:e=>{},
  6. 6. },
  7. 7. match:{
  8. 8. params:'',
  9. 9. isExact:false
  10. 10. },
  11. 11. location:{
  12. 12. pathname:'',
  13. 13. hash:'',
  14. 14. }
  15. 15. }
  16. 16. 复制代码

熟悉redux的人应该都知道,store中的共享状态需要通过一个顶层组件作为父组件,一般将顶级组件叫做Provider组件,由它内部创建context来作为数据的提供者
例如redux中的connect方法,它就是一个高阶组件,connext方法的参数在函数中通过解构拿到store中的数据,再通过props的方式给到connext传入的组件中,而在react 16.3版本中新增createContext方法,它返回了Provider, Consumer组件等,

context实现
  1. 1. //context.js
  2. 2.
  3. 3. import React from 'react';
  4. 4. let { Provider,Consumer } = React.createContext()
  5. 5. export { Provider, Consumer}
  6. 6.
  7. 7. //顶级组件
  8. 8.
  9. 9. import { Provider } from './context'
  10. 10. <Provider value={this.state}>
  11. 11. {this.props.children}
  12. 12. </Provider>
  13. 13.
  14. 14. //所有的子级组件 Consumer里面的childer是一个函数,由函数来返回渲染的块,state就是provider传入的value
  15. 15.
  16. 16. import { Consumer} from './context'
  17. 17. render(){
  18. 18. <Consumer>
  19. 19. {state => {
  20. 20. //这里的state就是provider传入的value
  21. 21. if(state.pathname===path){
  22. 22. return this.props.component
  23. 23. }
  24. 24. return null
  25. 25. }}
  26. 26. </Consumer>
  27. 27. }
  28. 28. 复制代码

Provider组件实现了,其他的就比较好办了,在hashRouter顶级组件中使用Provider组件,里面每个子组件中外层采用Consumer包裹,这样每个组件都能拿到provider的数据

hashRouter.js实现

hashRouter用于提供hisotry的数据以及方法给到子组件,如push,go等方法

  1. 1. //react-router-dom文件夹下hashRouter.js
  2. 2.
  3. 3. import React, {Component} from 'react';
  4. 4. import {Provider} from './context';
  5. 5.
  6. 6. export default class HashRouter extends Component {
  7. 7. constructor () {
  8. 8. super (...arguments);
  9. 9. this.state = {
  10. 10. location: {
  11. 11. pathname: window.location.hash.slice(1), //去除#号
  12. 12. hash: window.location.hash,
  13. 13. },
  14. 14. history:{
  15. 15. push(to){
  16. 16. window.location.hash = to
  17. 17. }
  18. 18. }
  19. 19. };
  20. 20. }
  21. 21.
  22. 22. componentDidMount () {
  23. 23. let location = this.state
  24. 24. window.addEventListener ('hashchange', (e)=> {
  25. 25. this.setState ({
  26. 26. location: {
  27. 27. ...location,
  28. 28. hash:window.location.hash,
  29. 29. pathname: window.location.hash.slice (1) || '', //去除#号
  30. 30. },
  31. 31. });
  32. 32. });
  33. 33. }
  34. 34. render () {
  35. 35. return (
  36. 36. <Provider value={this.state}>
  37. 37. {this.props.children}
  38. 38. </Provider>
  39. 39. );
  40. 40. }
  41. 41. }
  42. 42. 复制代码

hashRouter组件state中的的push方法,直接将 window.location.hash值改变,会触发haschange时间,而在componentDidMount钩子函数中,监听hashchange事件中,在变化后将hash值存入state中
在componentWillUnmount记得要把绑定的事件解绑,remove事件需要将函数抽出来作为一个变量引用才能清除掉

Route.js实现

该组件用来传入component和path

  1. 1. import React, {Component} from 'react';
  2. 2. import { Consumer} from './context'
  3. 3. const pathToRegexp = require('path-to-regexp');
  4. 4.
  5. 5. export default class Route extends Component {
  6. 6. constructor () {
  7. 7. super (...arguments)
  8. 8. }
  9. 9. render () {
  10. 10. let { path, component: Component, exact=false } = this.props;
  11. 11. return (
  12. 12. <Consumer>
  13. 13. {state => {
  14. 14. //pathToRegexp 方法,第一个参数,
  15. 15. let reg= pathToRegexp(path,[],{end:exact })
  16. 16. let pathname = state.location.pathname
  17. 17. if (reg.test(pathname)) {
  18. 18. return <Component {...state} />;
  19. 19. }
  20. 20. return null;
  21. 21. }}
  22. 22. </Consumer>
  23. 23. );
  24. 24. }
  25. 25. }
  26. 26. 复制代码

正常情况下,url可能会有这几种情况,如/foo/bar, 或者/foo:123,这种url如果不处理,默认是匹配不到的,而exact参数就是控制是否精确匹配,这里引入了 pathToRegexp库来生成正则表达式,来处理 url 中地址查询参数

  1. 1. //示例代码
  2. 2.
  3. 3.
  4. 4. //如果需要精确匹配,将pathToRegexp的第三个参数end传为true,pathToRegexp第二个参数是匹配到的值
  5. 5. let ret = []
  6. 6. var re = pathToRegexp('/detail',ret,{
  7. 7. end:true
  8. 8. })
  9. 9. re.test('/foo/1') // true
  10. 10.
  11. 11. //生成的正则
  12. 12. /^\/detail(?:\/)?$/i
  13. 13. /^\/detail(?:\/(?=$))?(?=\/|$)/i
  14. 14.
  15. 15. 复制代码

Switch.js实现

用于匹配只渲染一个route组件

  1. 1. import React, {Component} from 'react';
  2. 2. import { Consumer} from './context'
  3. 3. const pathToRegexp = require('path-to-regexp');
  4. 4.
  5. 5. export default class Switch extends Component {
  6. 6. constructor () {
  7. 7. super (...arguments);
  8. 8. }
  9. 9.
  10. 10. render () {
  11. 11. return (
  12. 12. <Consumer>
  13. 13. {state => {
  14. 14. let pathname =state.location.pathname;
  15. 15. let children = this.props.children
  16. 16. for(let i=0;i<children.length;i++){
  17. 17. let child = children[i]
  18. 18. let path = child.props.path || ''
  19. 19. let reg = pathToRegexp(path,[],{end:false})
  20. 20. if(reg.test(pathname)){
  21. 21. return child
  22. 22. }
  23. 23. }
  24. 24. return null
  25. 25. }}
  26. 26. </Consumer>
  27. 27. );
  28. 28. }
  29. 29. }
  30. 30.
  31. 31.
  32. 32. //使用Switchs
  33. 33.
  34. 34. <Switch>
  35. 35. <Route path="/home" component={Home} />
  36. 36. <Route path="/detail" component={Detail} />
  37. 37. <Redirect to="/home"/>
  38. 38. </Switch>
  39. 39.
  40. 40. 复制代码

Switch组件将传入的children,遍历拿到每一个组件传入的path,并生成正则,如果正则能够匹配的上,则直接渲染child,否则return null,确保switch中包裹的子组件,只能渲染其中一个,switch组件是用于配合redirect组件来使用的

redirect.js实现

用于重定向

  1. 1. import React, {Component} from 'react';
  2. 2. import { Consumer} from './context'
  3. 3.
  4. 4. export default class Redirect extends Component {
  5. 5. constructor () {
  6. 6. super (...arguments);
  7. 7. }
  8. 8.
  9. 9. render () {
  10. 10. return (
  11. 11. <Consumer>
  12. 12. {state => {
  13. 13. let { history }= state;
  14. 14. history.push(this.props.to)
  15. 15. return null
  16. 16. }}
  17. 17. </Consumer>
  18. 18. );
  19. 19. }
  20. 20. }
  21. 21. 复制代码

redirect组件实现非常简单,如果该组件渲染,直接将window.location.hash = to

browserRouter.js的实现

browserRouter与hashRouter的实现不同点是,在state的push方法中调用window.history.pushState,压入后,浏览器的url会直接变化页面不会刷新,另外popstate监听事件,也需要同步一次state里面的pathname

  1. 1. import React, {Component} from 'react';
  2. 2. import {Provider} from './context';
  3. 3.
  4. 4. class browserRouter extends Component {
  5. 5. constructor () {
  6. 6. super (...arguments);
  7. 7. this.state = {
  8. 8. location: {
  9. 9. pathname: window.location.pathname ,
  10. 10. hash: window.location.hash,
  11. 11. },
  12. 12. history:{
  13. 13. push :(to)=>{
  14. 14. this.pushState(null,null,to)
  15. 15. }
  16. 16. },
  17. 17. queue:[]
  18. 18. };
  19. 19. this.pushState = this.pushState.bind(this)
  20. 20. }
  21. 21.
  22. 22.
  23. 23. pushState = (state="",title="",path="")=>{
  24. 24. let queue = this.state.queue
  25. 25. let {location} = this.state
  26. 26. let historyInfo ={state,title,path}
  27. 27. queue.push(historyInfo)
  28. 28. this.setState({
  29. 29. ...this.state,
  30. 30. location:{
  31. 31. ...location,
  32. 32. pathname:path,
  33. 33. },
  34. 34. queue,
  35. 35. })
  36. 36. window.history.pushState(historyInfo,title,path)
  37. 37. }
  38. 38.
  39. 39. componentDidMount () {
  40. 40. let {location} = this.state
  41. 41. window.addEventListener('popstate',(e)=>{
  42. 42. this.setState({
  43. 43. ...this.state,
  44. 44. location:{
  45. 45. ...location,
  46. 46. pathname:e.state.path,
  47. 47. },
  48. 48. queue:this.state.queue,
  49. 49. })
  50. 50. })
  51. 51. }
  52. 52.
  53. 53. render () {
  54. 54. return (
  55. 55. <Provider value={this.state}>
  56. 56. {this.props.children}
  57. 57. </Provider>
  58. 58. );
  59. 59. }
  60. 60. }
  61. 61.
  62. 62. export default browserRouter;
  63. 63. 复制代码

如何使用?

1.新建一个router.js,用于管理route组件
2.在index.js中导入使用

  1. 1.
  2. 2. import React from 'react';
  3. 3. import {
  4. 4. HashRouter as Router,
  5. 5. // BrowserRouter as Router,
  6. 6. Route,
  7. 7. Link,
  8. 8. Redirect,
  9. 9. Switch,
  10. 10. } from './react-router-dom';
  11. 11.
  12. 12. import Home from './pages/home';
  13. 13. import Detail from './pages/detail';
  14. 14.
  15. 15. const BasicRoute = () => {
  16. 16. return (
  17. 17. <div>
  18. 18. <Router>
  19. 19. <div>
  20. 20. <div>
  21. 21. <Link to="/home">首页</Link>
  22. 22. <Link to="/detail">详情</Link>
  23. 23. </div>
  24. 24. <Switch>
  25. 25. <Route path="/home" component={Home} />
  26. 26. <Route path="/detail" component={Detail} />
  27. 27. <Redirect to="/home" />
  28. 28. </Switch>
  29. 29. </div>
  30. 30.
  31. 31. </Router>
  32. 32. </div>
  33. 33. );
  34. 34. };
  35. 35. export default BasicRoute;
  36. 36.
  37. 37.
  38. 38. // index.js中 使用
  39. 39.
  40. 40. import Router from './router'
  41. 41. ReactDOM.render(<Router/>, document.getElementById('root'));
  42. 42.
  43. 43. 复制代码