react-router原理

本文深入探讨了前端路由的实现原理,包括hash路由和history路由两种方法。通过代码示例详细解释了如何使用hashchange事件和history API进行路由切换,以及React-Router库如何封装这些原生API以提供更高级的路由功能。

在一个web应用中,路由系统是不可或缺的一部分。许多前端框架都会有着不同的配套的路由系统,最近也开始接触react,了解了一点react-router的实现原理。

hash路由

早期的前端路由是通过hash来实现的。改变url的hash值并不会刷新页面,因此就可以通过hash来实现前端路由。在当前的页面中可以通过window.location.hash=‘test’,来改变当前页面的hash值,执行后页面的url会发生改变,但页面并不会刷新。
赋值前:http://localhost:8080 赋值后:http://localhost:8080/#test。
然后我们可以通过一个名为hashchange的事件来监听页面hash的变化。

window.addEventListener('hashchange',function(event){
   console.log(event);
})

有了这个监听事件我们就可以在监听事件的回调函数中渲染新的UI,这样就实现了一个简单的路由功能。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <a href="#/a">pageALink</a>
    <a href="#/b">pageBLink</a>
    <span id='body'></span>
    <script>
        window.addEventListener('hashchange',(e)=>{
            document.getElementById('body').innerHTML = window.location
        },false)
    </script>
</body>
</html>

history路由

history是H5的接口,允许我们操作浏览器会话历史记录。history提供了一些属性和方法,主要包括:
属性:

  • history.length:返回值为会话历史中有多少条记录,包含当前的会话页面。
  • history.state:保存了触发popState事件的方法所传递的属性对象。

方法:

  • History.back(): 返回浏览器会话历史中的上一页,跟浏览器的回退按钮功能相同
  • History.forward():指向浏览器会话历史中的下一页,跟浏览器的前进按钮相同。
  • History.go(): 可以跳转到浏览器会话历史中的指定的某一个记录页。
  • History.pushState():pushState可以将给定的数据压入到浏览器会话历史栈中,该方法接收3个参数,对象,title和一串url。pushState后会改变当前页面url,但是不会伴随着刷新。
  • History.replaceState():replaceState将当前的会话页面的url替换成指定的数据,replaceState后也会改变当前页面的url,但是也不会刷新页面。

虽然pushState和replaceState都会改变当前页面的url,并且不会刷新页面,但pushState是push一条新的记录到浏览器的会话历史栈中,会使history.length+1,而replaceState是替换当前这条会话历史记录,history.lenth并不会增加。
每次但我们触发history.back(), History.forward()或点击浏览器的前进后退按钮时,都会触发popstate事件。
所以如果使用history做为路由基础,就需要使用history.pushState和history.replaceState来改变url的值而不刷新页面,然后通过popstate事件执行页面的前进与后退

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <a onClick="go('/a')">pageALink</a>
    <a onClick="go('/b')">pageBLink</a>
    <span id='body'></span>
    <script>
        function go (pathname){
            window.history.pushState({},null,pathname);
            document.getElementById('body').innerHTML = window.location;
        }
        
        //这里主要处理浏览器前进后退功能,不加下面的代码就无法实现前进后退功能
        window.addEventListener('popstate',(e)=>{
            let pathname = window.location;
            document.getElementById('body').innerHTML = window.location;
        })
    </script>
</body>
</html>

react-router的基础——history

History

history是一个第三方js库,借鉴HTML5 history对象的理念,在其基础上又扩展了一些功能,用来管理历史记录,可以兼容不同的浏览器和不同的环境,根据不同的环境提供了三种不同的API。

  • HashHistory:针对老版本的浏览器,主要通过Hash实现。
  • BrowserHistory:针对较高版本的浏览器,主要通过H5的History实现。
  • MemoryHistory:主要通过内存中的历史记录实现。

History支持发布/订阅功能,当history发生改变的时候,可以自动触发订阅的函数,再对比一些两者api的异同。以下是history库的:

const history = {
    length,        // 属性,history中记录的state的数量	
    action,        // 属性,当前导航的action类型
    location,      // 属性,location对象,封装了pathname、search和hash等属性
    push,          // 方法,导航到新的路由,并记录在history中
    replace,       // 方法,替换掉当前记录在history中的路由信息
    go,            // 方法,前进或后退n个记录
    goBack,        // 方法,后退
    goForward,     // 方法,前进
    canGo,         // 方法,是否能前进或后退n个记录
    block,         // 方法,跳转前让用户确定是否要跳转
    listen         // 方法,订阅history变更事件
  };

以下是HTML5 history对象的:

const history = {
    length,         // 属性,history中记录的state的数量
    state,          // 属性,pushState和replaceState时传入的对象
    back,           // 方法,后退
    forward,        // 方法,前进
    go,             // 方法,前进或后退n个记录
    pushState,      // 方法,导航到新的路由,并记录在history中
    replaceState    // 方法,替换掉当前记录在history中的路由信息
}

// 订阅history变更事件
window.onpopstate = function (event) {
    ...
}

从对比中可以看出,两者的关系是非常密切的,history库可以说是history对象的超集,是功能更强大的history对象。

简单前端路由的实现

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>router</title>
</head>
<body>
    <ul> 
        <li><a href="#/">turn white</a></li> 
        <li><a href="#/blue">turn blue</a></li> 
        <li><a href="#/green">turn green</a></li> 
    </ul> 
<script>
    function Router() {
        this.routes = {};
        this.currentUrl = '';
    }
    <!--
    //针对不同的地址进行回调的匹配
    //1:用户在调用Router.route('address',function),在this.routes对象中进行记录或者说address与function的匹配
    -->
    Router.prototype.route = function(path, callback) {
        this.routes[path] = callback || function(){};
    };
    <!--
    //处理hash的变化,针对不同的值,进行页面的处理
    //1:在init中注册过事件,在页面load的时候,进行页面的处理
    //2:在hashchange变化时,进行页面的处理
    -->
    Router.prototype.refresh = function() {
        this.currentUrl = location.hash.slice(1) || '/';
        this.routes[this.currentUrl]();
    };
    <!--
    //1:在Router的prototype中定义init
    //2:在页面load/hashchange事件触发时,进行回调处理
    -->
    Router.prototype.init = function() {
        window.addEventListener('load', this.refresh.bind(this));
        window.addEventListener('hashchange', this.refresh.bind(this));
    }
    window.Router = new Router();//在window对象中构建一个Router对象
    window.Router.init();//页面初始化处理
    var content = document.querySelector('body');
    // change Page anything
    function changeBgColor(color) {
        content.style.backgroundColor = color;
    }
    Router.route('/', function() {
        changeBgColor('white');
    });
    Router.route('/blue', function() {
        changeBgColor('blue');
    });
    Router.route('/green', function() {
        changeBgColor('green');
    });
</script>
</body>
</html>

React-Router简单实现

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>包装方式</title>
</head>
<body>
<script>
	var body = document.querySelector('body'),
		newNode = null,
        append = function(str){
    		newNode = document.createElement("p");
    		newNode.innerHTML = str;
    		body.appendChild(newNode);
    	};
	
    // history库
    var historyModule = {
        listener: [],
        listen: function (listener) {
            this.listener.push(listener);
            append('historyModule listen.')
        },
        updateLocation: function(){
            append('historyModule updateLocation tirgger.');
            this.listener.forEach(function(listener){
                listener('new localtion');
            })
        }
    }
    // Router 将使用 historyModule 对象,并对其包装
    var Router = {
        source: {},
        //复制historyModule到Router中
        init: function(source){
            this.source = source;
        },
        //处理监听事件,在Router对页面进行处理时,利用historyModule中处理页面
        listen: function(listener) {
            append('Router listen.');
            // 对 historyModule的listen进行了一层包装
            return this.source.listen(function(location){
                append('Router listen tirgger.');
                listener(location);
            })
        }
    }
    // 将 historyModule 注入进 Router 中
    Router.init(historyModule);
    // Router 注册监听
    Router.listen(function(location){
        append(location + '-> Router setState.');
    })
    // historyModule 触发监听回调(对页面进行渲染等处理)
    historyModule.updateLocation();
</script>
</body>
</html>

其实上诉的操作就是只是针对前端简单路由+historyModule的升级处理。

react-router分析

BrowserRouter

用BrowserRouter 组件包裹整个App系统后,就是通过html5的history来实现无刷新条件下的前端路由。
与BrowserRouter对应的是HashRouter,HashRouter使用url中的hash属性来保证不重新刷新的情况下同时渲染页面。

import warning from "warning";
import React from "react";
import PropTypes from "prop-types";
import { createBrowserHistory as createHistory } from "history";//这里的history就是上面第二个例子中的historyModule
import Router from "./Router"; //对应第二个例子中的Router对象

/**
 * The public API for a <Router> that uses HTML5 history. //这里是重点
 */
class BrowserRouter extends React.Component {
  history = createHistory(this.props);
  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

export default BrowserRouter;
listen函数的注册

React-Router/Router.js

/**
 * The public API for putting history on context. //这里的道理类似于例子二中第二步
 */
class Router extends React.Component {
  
  static childContextTypes = {
    router: PropTypes.object.isRequired
  };

  getChildContext() {
    return {
      router: {
        ...this.context.router,
        history: this.props.history,
        route: {
          location: this.props.history.location,
          match: this.state.match
        }
      }
    };
  }

  state = {
    match: this.computeMatch(this.props.history.location.pathname)
  };

  computeMatch(pathname) {
    return {
      path: "/",
      url: "/",
      params: {},
      isExact: pathname === "/"
    };
  }

  componentWillMount() {
    const { children, history } = this.props;
    // Do this here so we can setState when a <Redirect> changes the
    // location in componentWillMount. This happens e.g. when doing
    // server rendering using a <StaticRouter>.
    this.unlisten = history.listen(() => {
      this.setState({
        match: this.computeMatch(history.location.pathname)
      });
    });
  }

  componentWillReceiveProps(nextProps) {
    warning(
      this.props.history === nextProps.history,
      "You cannot change <Router history>"
    );
  }

  componentWillUnmount() {
    this.unlisten();
  }

  render() {
    const { children } = this.props;
    return children ? React.Children.only(children) : null;
  }
}

export default Router;
Redirect.js

react-router/Redirect.js

//这里省去其他库的引用
import generatePath from "./generatePath";
/**
 * The public API for updating the location programmatically
 * with a component.
 */
class Redirect extends React.Component {
//这里是从Context中拿到history等数据
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        push: PropTypes.func.isRequired,
        replace: PropTypes.func.isRequired
      }).isRequired,
      staticContext: PropTypes.object
    }).isRequired
  };

  isStatic() {
    return this.context.router && this.context.router.staticContext;
  }

  componentWillMount() {
    invariant(
      this.context.router,
      "You should not use <Redirect> outside a <Router>"
    );

    if (this.isStatic()) this.perform();
  }

  componentDidMount() {
    if (!this.isStatic()) this.perform();
  }

  componentDidUpdate(prevProps) {
    const prevTo = createLocation(prevProps.to);
    const nextTo = createLocation(this.props.to);

    if (locationsAreEqual(prevTo, nextTo)) {
      warning(
        false,
        `You tried to redirect to the same route you're currently on: ` +
          `"${nextTo.pathname}${nextTo.search}"`
      );
      return;
    }

    this.perform();
  }

  computeTo({ computedMatch, to }) {
    if (computedMatch) {
      if (typeof to === "string") {
        return generatePath(to, computedMatch.params);
      } else {
        return {
          ...to,
          pathname: generatePath(to.pathname, computedMatch.params)
        };
      }
    }

    return to;
  }
 //进行路由的匹配操作
  perform() {
    const { history } = this.context.router;
    const { push } = this.props;
    //Router中拿到需要跳转的路径,然后传递给history
    const to = this.computeTo(this.props);

    if (push) {
      history.push(to);
    } else {
      history.replace(to);
    }
  }

  render() {
    return null;
  }
}

export default Redirect;
generatePath
//该方法只是对路径进行处理
/**
 * Public API for generating a URL pathname from a pattern and parameters.
 */
const generatePath = (pattern = "/", params = {}) => {
  if (pattern === "/") {
    return pattern;
  }
  const generator = compileGenerator(pattern);
  return generator(params);
};
针对路径进行页面渲染处理
 <Router>
      <Route exact path="/" component={Home}/>
      <Route path="/about" component={About}/>
      <Route path="/topics" component={Topics}/>
  </Router>

看一下Route对组件的处理

/**
 * The public API for matching a single path and rendering.
 */
class Route extends React.Component {
    //从Router中获取信息
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.object.isRequired,
      route: PropTypes.object.isRequired,
      staticContext: PropTypes.object
    })
  };
//自己定义了一套Contex用于子组件的使用
  static childContextTypes = {
    router: PropTypes.object.isRequired
  };
//自己定义了一套Contex用于子组件的使用
  getChildContext() {
    return {
      router: {
        ...this.context.router,
        route: {
          location: this.props.location || this.context.router.route.location,
          match: this.state.match
        }
      }
    };
  }

  state = {
    match: this.computeMatch(this.props, this.context.router)// matching a URL pathname to a path pattern.如果不匹配,返回null,也就是找不到页面信息
  };
  render() {
    const { match } = this.state;
    const { children, component, render } = this.props;//从Router结构中获取对应的处理方法
    const { history, route, staticContext } = this.context.router;//从Context中获取数据
    const location = this.props.location || route.location;
    const props = { match, location, history, staticContext };
    //如果页面匹配成功,进行createElement的渲染。在这里就会调用component的render===>页面刷新 这是处理第一次页面渲染
    if (component) return match ? React.createElement(component, props) : null;
    //这里针对首页已经被渲染,在进行路由处理的时候,根据props中的信息,进行页面的跳转或者刷新
    if (render) return match ? render(props) : null;

    return null;
  }
}

export default Route;

总结

针对React-Router来讲,其实就是对History进行了一次封装,使能够识别将url的变化与componet渲染进行匹配。

  1. 根据BrowserRouter等不同的API针对H5的history的重构
  2. 结构的构建,同时对history属性进行注册。
  3. 在Router的componentWillMount中注册history的事件回调。
  4. 在Redirect中进行路径的计算,调用history.push/history.replace等更新history信息。
  5. Route中根据计算的匹配结果,进行页面首次渲染/页面更新渲染处理。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值