# react-router

  1. 最新 React Router 全面整理 (opens new window)
  2. react-router官方文档 (opens new window)
  3. 从零开始,超简单react-router使用教程 (opens new window)
  4. 解读react-router从V5到V6 (opens new window)

# 1、router原理

web前端,路由位URL与UI的映射关系,改变URL不引起页面刷新。

# 1、hash路由

  • 特点

    • url 中带有一个#符号,但是#只是浏览器端/客户端的状态,不会传递给服务端
    • hash 值的更改,不会导致页面的刷新
    • hash 值的更改,会在浏览器的访问历史中添加一条记录。所以我们才可以通过浏览器的返回、前进按钮来控制 hash 的切换
  • 修改:location.hash='#aaa'

  • 监测:hashchange事件

# 2、history路由

  • 特点

    • url无#,美观,服务器可接收到路径和参数变化,需服务器适配
    • 基于浏览器的history对象实现,主要为history.pushState 和 history.replaceState来进行路由控制。通过这两个方法,可以实现改变 url 且不向服务器发送请求
  • 修改

    • 点击后退/前进触发 popstate事件,监听进行页面更新
    • 调用history.pushState或history.replaceState触发相应的函数后,在后面手动添加回调更新页面
  • 监测:无监测事件

# 3、非浏览器模式

在非浏览器环境,使用抽象路由实现导航的记录功能

  • react-router的 memoryHistory
  • vue-router 的 abstract 封装

# 2、vue-router

# 1、使用

import Vue from 'vue'
import VueRouter from 'vue-router'
import App from "./App.vue"
import Home from "./Home.vue"
//全局挂载router相关实例
Vue.use(VueRouter)

const router = new VueRouter({
  mode: 'history',
  routes: [
    {path: '/app', component: App},
    {
      path: '/home', 
      component: Home,
      children: [
        {
          path: 'home-sub',
          //异步加载,独立拆包
         	component: () => import(/* webpackChunkName: "home-sub" */ "@/HomeSub.vue"),
        }
      ]
    },
  ]
})

new Vue({
  router
}).$mount("#app");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//正则表达式匹配参数
const reg = /([^#&?]+)=([^#&?]+)/g;
const url = "wangjxk.top/#weriw?we=12&jjj=eee"
const obj = {}
url.replace(reg, (_, c1, c2) => {
  obj[c1] = c2;
})
console.log(obj); //{we: '12', jjj: 'eee'}
1
2
3
4
5
6
7
8

# 2、源码分析

# 1、use

  • 用法:Vue.use(plugin),参数{Object | Function} plugin
  • 作用:安装Vue.js插件,如果插件是一个对象,必须提供install方法。如果插件是一个函数,被作为install方法。调用install方法时,会将Vue作为参数传入。install方法被同一个插件多次调用时,插件也只会被安装一次。
  • 本质:调用插件install方法,将插件推入数组保存
Vue.use = function (plugin) {
  const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
  if(installedPlugins.indexOf(plugin) > -1){
    return this
  }
  const args = toArray(arguments, 1)
  args.unshift(this)
  if(typeof plugin.install === 'function'){
    plugin.install.apply(plugin, args);
  } else if (typeof plugin === 'function'){
    plugin.apply(null, args)
  }
  installedPlugins.push(plugin)
  return this
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2、router

  • install
//vue-router中install.js
//1、原型挂载$router、$route对象
//2、全局注册<router-view>、<router-link>组件
import View from './components/view'
import Link from './components/link'

export let _Vue

export function install (Vue) {
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined

  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })
	//挂载$router对象
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
  //挂载$route对象
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })
	//全局注册组件,<router-view>、<router-link>可全局使用
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
  • 构造函数
//index.js, vue-router构造器
export default class VueRouter {
  //...
  //路由守卫钩子相关定义
  beforeEach(){/* */}
  //路由方法定义
  push(){/* */}replace(){/* */},
  //构造函数
  constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    this.matcher = createMatcher(options.routes || [], this)

    let mode = options.mode || 'hash'
    this.fallback =
      mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    //非浏览器,默认abstract模式
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode
		//不同模式,封装了不同的对象
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//HTML5History对象
//html5history:封装了go、push等方法,transitionTo代码在History里
export class HTML5History extends History {
   go (n: number) {
    window.history.go(n)
  }

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      //内部调用history.pushState({ key: setStateKey(genStateKey()) }, '', url)
      pushState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      //内部调用history.replaceState(stateCopy, '', url)
      replaceState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 3、react-router

# 1、安装

React Router已被拆分成三个包:react-router,react-router-dom和react-router-native

  • react-router包提供核心的路由组件与函数,不建议直接安装使用
  • react-router-dom和react-router-native提供运行环境(浏览器与react-native)所需的特定组件,但是他们都暴露出react-router中暴露的对象与方法(底层安装了react-router库)

# 2、使用和原理

不像 Vue 那样,将 router 的一些实例方法挂载到全局,组件可以通过 this.$router,this.$route 访问到,react-router 为组件注入的方法,使用函数式编程。

# 1、react-router使用

import React from 'react';
import ReactDOM from 'react-dom';
import App from "./App";
import Home from "./views/Home";
import Home1 from "./views/Home1";
import Home2 from "./views/Home2";
import About from "./views/About";

//路由配置
import {Router, Route} from 'react-router';
import {createBrowserHistory} from 'history';
const histroy = createBrowserHistory();

//访问: http://localHost:3000/home,展示Home组件
ReactDOM.render(
  <Router history={history}>
  	<App>
      <Route path="/home" component={Home}/>
      <Route path="/about" component={props => {
          return (
            /* 子路由配置 */
            <App1>
              <Route path="/home1" component={Home1}/>
              <Route path="/home2" component={Home2}/>
            </App1>
          )
        }}/>
    </App>
  </Router>,
  document.getElementById('root')
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 2、react-router-dom使用

  • Link:封装的专属a标签,用于路由跳转,类似于router-link
    • to:字符串或者位置对象(包含路径名,search,hash和state的组合)
  • Switch:匹配多个路由选择其一组件,从前往后,匹配成功,将route克隆返回
  • Route:路由组件,类似于router-view
    • path:字符串,描述路由匹配的路径名类型
    • component:一个React组件。当带有component 参数的的路由匹配时,路由将返回一个新元素,其类型是一个React component (使用React.createElement创建)
    • render:一个返回React元素的函数,它将在path匹配时被调用。这与component类似,应用于内联渲染和更多参数传递
    • children:一个返回React元素的函数,无论路由的路径是否与当前位置相匹配,都将始终被渲染
    • exact属性:指定 path 是否需要绝对匹配才会跳转
  • BrowserRouter:histroy路由组件
  • HashRouter:hash路由组件
  • withRouter:高阶函数,使用后可使用注入的context对象上的属性
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import Home from './views/Home'
import About from './views/About'
const NoMatch = () => <>404</>

import {Route, BrowserRouter, Switch} from 'react-router-dom'

//switch和route组件可分散在任何组件中,下例子为根组件使用
const extraProps = { color: 'red' }
ReactDOM.render(
  <BrowserRouter>
    <App>
      <Switch>
        <Route path="/home" component={Home}/>
        <Route path="/about" component={About}/>
        <Route path='/page' render={(props) => (  
          <Page {...props} data={extraProps}/>
        )}/>
        {/* props.match指通过路径匹配获取到的参数信息,包含如下信息:
        1. params – (object)从对应于路径的动态段的URL解析的键/值对
				2. isExact – (boolean)true如果整个URL匹配(没有尾随字符)
				3. path – (string)用于匹配的路径模式,作用于构建嵌套的
				4. url – (string)URL的匹配部分,作用于构建嵌套的
        */}
        <Route path='/page' children={(props) => (
            props.match
            ? <Page {...props}/>
            : <EmptyPage {...props}/>
          )}/>
        <Route path="*" component={NoMatch}/>
      </Switch>
    </App>
  </BrowserRouter>,
  document.getElementById('root')
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 1、switch代码分析

代码在react-router/modules/Switch.js中

/**
 * The public API for rendering the first <Route> that matches.
 */
class Switch extends React.Component {
  render() {
    return (
      {/* 使用RouterContext.Consumer包裹,可使用注入的context */}
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Switch> outside a <Router>");
          const location = this.props.location || context.location;
          let element, match;
          // React.Children.forEach为react遍历this.props.children的方法
          // switch子元素的遍历,即Route组件遍历
          React.Children.forEach(this.props.children, child => {
            if (match == null && React.isValidElement(child)) {
              element = child;
              const path = child.props.path || child.props.from;
              match = path
                ? matchPath(location.pathname, { ...child.props, path })
                : context.match;
            }
          });
          return match  //匹配到则返回,保证唯一,克隆的新元素,注入了location等对象参数
            ? React.cloneElement(element, { location, computedMatch: match })
            : null;
        }}
      </RouterContext.Consumer>
    );
  }
}
//...
export default Switch;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 2、Route代码分析

组件通过Route组件包装后,添加了location、history等对象参数props

调试技巧:在chrome中,source栏目中使用ctrl+p查找文件Home.js,在函数中添加断点,可查看props添加如下对象:history、location、match、staticContext,可直接使用。

export default function Home({location, history}){
  return (
    <div onClick={() => history.push({
        pathname: '/player',
        state: {age: 18}
      })}>
      Home
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10

原理解析:通过Routers组件通过context注入使用对象,Route组件中通过props注入创建组件

//react-router/modules/RouterContext.js
//react-router/modules/createNameContext.js
import createContext from "mini-create-react-context";
const createNamedContext = name => {
  const context = createContext();
  context.displayName = name;
  return context;
};
export default createNamedContext;

//react-router/modules/Routers.js
import HistoryContext from "./HistoryContext.js";
import RouterContext from "./RouterContext.js";
class Router extends React.Component {
  //通过Provider注入history、location、match、staticContext对象
  render() {
    return (
      <RouterContext.Provider
        value={{
          history: this.props.history,
          location: this.state.location,
          match: Router.computeRootMatch(this.state.location.pathname),
          staticContext: this.props.staticContext
        }}
      >
        <HistoryContext.Provider
          children={this.props.children || null}
          value={this.props.history}
        />
      </RouterContext.Provider>
    );
  }
}

//react-router/modules/Route.js
//The public API for matching a single path and rendering.
class Route extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Route> outside a <Router>");
          const location = this.props.location || context.location;
          const match = this.props.computedMatch
            ? this.props.computedMatch // <Switch> already computed the match for us
            : this.props.path
            ? matchPath(location.pathname, this.props)
            : context.match;
          const props = { ...context, location, match };
          let { children, component, render } = this.props;
          // Preact uses an empty array as children by
          // default, so use null if that's the case.
          if (Array.isArray(children) && isEmptyChildren(children)) {
            children = null;
          }
          //解析顺序:children、component、render
          return (
            <RouterContext.Provider value={props}>
              {props.match
                ? children
                  ? typeof children === "function"
                    ? __DEV__
                      ? evalChildrenDev(children, props, this.props.path)
                      : children(props) //将props以入参形式注入
                    : children
                  : component
                  ? React.createElement(component, props)
                  : render
                  ? render(props)
                  : null
                : typeof children === "function"
                ? __DEV__
                  ? evalChildrenDev(children, props, this.props.path)
                  : children(props)
                : null}
            </RouterContext.Provider>
          );
        }}
      </RouterContext.Consumer>
    );
  }
}
export default Route;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# 3、withRouter原理及使用

想让业务组件即非Route组件包裹的组件使用context对象上的内容,可使用withRouter高阶函数包裹

//使用通用语法实现
import {__RouterContext} from 'react-router'
function App(){
  return (
    <__RouterContext.Consumer>
      {
        /* 此时props已包含context传递的属性 */
        props => {
          return <div className = "App">App</div>
        }
      }
    </__RouterContext.Consumer>
  )
}

//通常使用高阶组件实现
import {__RouterContext} from 'react-router'
function App({children}){
  return (
   <div className = "App">App</div>
  )
}
function Ctx(Component){
  return props => <__RouterContext.Consumer>
    {
      /* context为router注入的对象 */
      context => <Component {...context} {...props}/>
    }
  </__RouterContext.Consumer>
}
export default ctx(App);
  
//使用默认withRouter方法,使用原context方法
import {withRouter} from 'react-router'
function App(){
  return (
   <div className = "App">
      App
    </div>
  )
}
export default withRouter(App);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 3、路由守卫

react-router-dom使用Prompt和getUserConfirmation函数实现路由守卫

//Home组件
import React from 'react'
import {Prompt} from 'react-router'
export default function Home({location, history}){
  const [message, setMessage] = useState("")
  return (
    <>
    	<div onClick={()=> history.push({
        pathname: '/about',
        state: {state: 12}
      })}>
      	Home
      	{/* 确定继续跳转路由,取消不跳转 */}
      	<Prompt message={'是否跳转'} when={!!message}/>
    	</div>
    	<span onClick={()=> setMessage(Math.random())}>修改了{message}</span>
    </>
  )
}

//App.js组件
//使用子定义函数处理实现路由守卫,定义后,不会弹alert框
//getUserConfirmation函数使用在history.js中
const getUserConfirmation = (msg, callback) => {
  console.log(msg) 			//Home中Prompt的message值
  callback(true);  			//跳转路由
}
ReactDOM.render(
  <BrowserRouter getUserConfirmation={getUserConfirmation}>
    <App>
      <Switch>
        <Route path="/home" component={Home}/>
        <Route path="/about" component={About}/>
        <Route path="*" component={NoMatch}/>
      </Switch>
    </App>
  </BrowserRouter>,
  document.getElementById('root')
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 4、v6版本

v6相关组件和属性:BrowserRouter, HashRouter, Link, MemoryRouter, NavLink, Navigate, Outlet, Route, Router, Routes, UNSAFE_LocationContext, UNSAFE_NavigationContext, UNSAFE_RouteContext, createRoutesFromChildren, createSearchParams, generatePath, matchPath, matchRoutes, renderMatches, resolvePath, unstable_HistoryRouter, useHref, useInRouterContext, useLinkClickHandler, useLocation, useMatch, useNavigate, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRoutes, useSearchParams

  • 废弃 Switch 组件,由 Routes 代替(使用了智能匹配路径算法)
  • 废弃 Redirect 组件,由 Navigate 代替
  • 废弃 useHistory 方法,由 useNavigate 代替
  • Route 组件移除原有 componentrender 属性,统一通过 element 属性传递:<Route element={<Home />}>
  • Route 组件支持嵌套写法(v3 版本用法回归)
  • Route 组件的 path 规则变更,不再支持类正则写法
  • 消除了 v5 版本中带后斜杠的路径时,Link 组件的跳转模糊的问题
  • Link 组件支持自动携带当前父路由,以及相对路径写法../home
  • 新增 useRoutes 方法,代替之前的react-router-config写法,同样支持嵌套
  • 其他一些 API 名称变更
Last Updated: 12/21/2021, 8:06:06 PM