(五)React-router

参考资料:

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

基础概念:简单来说路由就是用来跟后端服务器进行交互的一种方式,通过不同的路径,来请求不同的资源,请求不同的页面是路由的其中一种功能。

访问过程为:

    1. 浏览器发出请求
    1. 服务器监听到80端口(或443)有请求过来,并解析url路径
    1. 根据服务器路由配置,返回相应信息(可以是html信息,也可以是json数据,图片等)
    1. 浏览器根据数据包的Content-Type来决定如何解析数据

一、router 原理

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

1、hash路由

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

未命名绘图.png

2、history路由

  • 特点
    • url无#,美观,服务器可接收到路径和参数变化,需服务器适配
    • 基于浏览器的history对象实现,主要为history.pushStatehistory.replaceState来进行路由控制。通过这两个方法,可以实现改变 url 且不向服务器发送请求
  • 修改
    • 点击后退/前进触发 popstate事件,监听进行页面更新
    • 调用history.pushStatehistory.replaceState触发相应的函数后,在后面手动添加回调更新页面
  • 监测:无监测事件

未命名绘图(1).png

3、非浏览器模式

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

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

二、vue-router

可参考 vue-router

1、实现原理

1、hash模式
/**
 * 添加 url hash 变化的监听器
 */
setupListeners () {
    const router = this.router
    /**
     * 每当 hash 变化时就解析路径
     * 匹配路由
     */
    window.addEventListener('hashchange', () => {
        const current = this.current
        /**
         * transitionTo:
         * 匹配路由
         * 并通过路由配置,把新的⻚⾯ render 到 ui-view 的节点
         */
        this.transitionTo(getHash(), route => {
            replaceHash(route.fullPath)
        })
    })
}
复制代码
2、history模式
export class HTML5History extends History {
    constructor (router, base) {
        super(router, base)
        /**
         * 原理还是跟 hash 实现⼀样
         * 通过监听 popstate 事件
         * 匹配路由,然后更新⻚⾯ DOM
         */
        window.addEventListener('popstate', e => {
            const current = this.current
            // Avoiding first `popstate` event dispatched in some browsers but first
            // history route not updated since async guard at the same time.

            const location = getLocation(this.base)
            if (this.current === START && location === initLocation) {
                return
            }

            this.transitionTo(location, route => {
                if (supportsScroll) {
                    handleScroll(router, route, current, true)
                }
            })
        })

    }

    go (n) {
        window.history.go(n)
    }

    push (location, onComplete, onAbort) {
        const { current: fromRoute } = this
        this.transitionTo(location, route => {
            // 使⽤ pushState 更新 url,不会导致浏览器发送请求,从⽽不会刷新⻚⾯
            pushState(cleanPath(this.base + route.fullPath))
            onComplete && onComplete(route)

        }, onAbort)
     }

     replace (location, onComplete, onAbort) {
        const { current: fromRoute } = this
        this.transitionTo(location, route => {
            // replaceState 跟 pushState 的区别在于,不会记录到历史栈
            replaceState(cleanPath(this.base + route.fullPath))
            onComplete && onComplete(route)
        }, onAbort)
    }
}
复制代码

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
}
复制代码
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
}
复制代码
  • 构造函数
//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}`)
        }
    }
  }
}
复制代码
//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)
  }
}
复制代码

3、使用

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");
复制代码
//正则表达式匹配参数
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'}
复制代码

三、react-router

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

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

react-router-dom和react-router和history库三者什么关系?

  • history 可以理解为react-router的核心,也是整个路由原理的核心,里面集成了popState,history.pushState等底层路由实现的原理方法,接下来我们会一一解释。
  • react-router可以理解为是react-router-dom的核心,里面封装了Router,Route,Switch等核心组件,实现了从路由的改变到组件的更新的核心功能,在我们的项目中只要一次性引入react-router-dom就可以了。
  • react-router-dom,在react-router的核心基础上,添加了用于跳转的Link组件,和histoy模式下的BrowserRouter和hash模式下的HashRouter组件等。所谓BrowserRouter和HashRouter,也只不过用了history库中createBrowserHistory和createHashHistory方法

1、使用和原理

不像 Vue 那样,将 router 的一些实例方法挂载到全局,组件可以通过 this.routerthis.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')
)
复制代码
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、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;
复制代码
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>
  )
}
复制代码

原理解析:通过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;
复制代码
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);
复制代码

2、路由守卫

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')
)
复制代码

3、手动实现

1、BrowserRouter
// BrowserRouter.js
import React, { Component } from "react";
import Context from './context';

export default class BrowserRouter extends Component {
    constructor(props) {
        super(props);
        this.state = {
            location: {
                pathname: window.location.pathname || "/",
                search: undefined,
            },
            match: {
            }
        }
    }

    componentWillMount() {
        window.addEventListener("popstate", () => {
            this.setState({
                location: {
                    pathname: window.location.pathname
                }
            })
        })

    }

    render() {
        const currentRoute = {
            location: this.state.location,
            match: this.state.match,
            history: {
                push: (to) => {
                    // 根据当前to 去匹配不同的路由 实现路由切换
                    if (typeof to === 'object') {
                        let { pathname, query } = to;
                        // 只是改变当前state的数据, 不触发reRender
                        this.setState({
                            location: {
                                query: to.query,
                                pathname: to.pathname
                            }
                        });
                        window.history.pushState({}, {}, pathname)
                    } else {
                        // 如果是字符串
                        this.setState({
                            location: {
                                pathname: to
                            }
                        })
                        window.history.pushState({}, {}, to)
                    }
                 }
            }
        }
        return (
            <Context.Provider value={currentRoute}>{this.props.children}</Context.Provider>
        )
    }
}



// context.js
import React from 'react';
export default React.createContext();
复制代码
2、Route
import React, { Component } from "react";
import { pathToRegexp, match } from "path-to-regexp";
import context from "./context";

export default class Route extends Component {
    static contextType = context;
    render() {
        const currenRoutePath = this.context.location.pathname; // 从上下文context中获取到当前路由
        const { path, component: Component, exact = false } = this.props; // 获取Route组件props的路由
        const paramsRegexp = match(path, { end: exact }); // 生成获取params的表达式
        const matchResult = paramsRegexp(currenRoutePath);
        console.log("路由匹配结果", matchResult);

        this.context.match.params = matchResult.params;
        const props = {
            ...this.context
        }

        const pathRegexp = pathToRegexp(path, [], { end: exact }); // 生成路径匹配表达式
        if (pathRegexp.test(currenRoutePath)) {
            return (<Component {...props}></Component>) // 将蛋清概念上下文路由信息当作props传递给组件
        }
        return null;
    }
}
复制代码
// 调用
import Page1 from './pages/page1'
import Page2 from './pages/page2'
import Page3 from './pages/page3'
import React, { Component } from 'react'
import { Route, BrowserRouter } from './react-router-dom';

console.log(Route, BrowserRouter)

class Router extends Component {
    constructor(props) {
        super(props)
        this.state = {}
    }

    render() {
        return (
            <BrowserRouter>
                <Route exact path="/" component={(Page1)} />
                <Route exact path="/page2" component={(Page2)} />
                <Route exact path="/page3" component={(Page3)} />
            </BrowserRouter>
        )
    }
}
export default Router
复制代码

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 组件移除原有 component 及 render 属性,统一通过 element 属性传递:<Route element={<Home />}>
  • Route 组件支持嵌套写法(v3 版本用法回归)
  • Route 组件的 path 规则变更,不再支持类正则写法
  • 消除了 v5 版本中带后斜杠的路径时,Link 组件的跳转模糊的问题
  • Link 组件支持自动携带当前父路由,以及相对路径写法../home
  • 新增 useRoutes 方法,代替之前的react-router-config写法,同样支持嵌套
  • 其他一些 API 名称变更
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享