文档章节

前端路由实现与 react-router 源码分析

ayoway
 ayoway
发布于 2016/05/19 17:00
字数 2033
阅读 9
收藏 1

一个极简前端路由实现

说一下前端路由实现的简要原理,以 hash 形式(也可以使用 History API 来处理)为例,当 url 的 hash 发生变化时,触发 hashchange 注册的回调,回调中去进行不同的操作,进行不同的内容的展示。直接看代码或许更直观。

function Router() {
    this.routes = {};
    this.currentUrl = '';
}
Router.prototype.route = function(path, callback) {
    this.routes[path] = callback || function(){};
};
Router.prototype.refresh = function() {
    this.currentUrl = location.hash.slice(1) || '/';
    this.routes[this.currentUrl]();
};
Router.prototype.init = function() {
    window.addEventListener('load', this.refresh.bind(this), false);
    window.addEventListener('hashchange', this.refresh.bind(this), false);
}
window.Router = new Router();
window.Router.init();

上面路由系统 Router 对象实现,主要提供三个方法

  • init 监听浏览器 url hash 更新事件
  • route 存储路由更新时的回调到回调数组routes中,回调函数将负责对页面的更新
  • refresh 执行当前url对应的回调函数,更新页面

Router 调用方式以及呈现效果如下:点击触发 url 的 hash 改变,并对应地更新内容(这里为 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> 
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');
});

以上为一个前端路由的简单实现,

<!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 = '';
    }
    Router.prototype.route = function(path, callback) {
        this.routes[path] = callback || function(){};
    };
    Router.prototype.refresh = function() {
        this.currentUrl = location.hash.slice(1) || '/';
        this.routes[this.currentUrl]();
    };
    Router.prototype.init = function() {
        window.addEventListener('load', this.refresh.bind(this), false);
        window.addEventListener('hashchange', this.refresh.bind(this), false);
    }
    window.Router = new 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 配套的 react-router。

react-router 分析

react-router 与 history 结合形式

react-router 是基于 history 模块提供的 api 进行开发的,结合的形式本文记为 包装方式。所以在开始对其分析之前,先举一个简单的例子来说明如何进行对象的包装。

// 原对象
var historyModule = {
    listener: [],
    listen: function (listener) {
        this.listener.push(listener);
        console.log('historyModule listen..')
    },
    updateLocation: function(){
        this.listener.forEach(function(listener){
            listener('new localtion');
        })
    }
}
// Router 将使用 historyModule 对象,并对其包装
var Router = {
    source: {},
    init: function(source){
        this.source = source;
    },
    // 对 historyModule的listen进行了一层包装
    listen: function(listener) {
        return this.source.listen(function(location){
            console.log('Router listen tirgger.');
            listener(location);
        })
    }
}
// 将 historyModule 注入进 Router 中
Router.init(historyModule);
// Router 注册监听
Router.listen(function(location){
    console.log(location + '-> Router setState.');
})
// historyModule 触发回调
historyModule.updateLocation();

返回:
22

可看到 historyModule 中含有机制:historyModule.updateLocation() -> listener( ),Router 通过对其进行包装开发,针对 historyModule 的机制对 Router 也起到了作用,即historyModule.updateLocation() 将触发 Router.listen 中的回调函数 。点击查看完整代码
这种包装形式能够充分利用原对象(historyModule )的内部机制,减少开发成本,也更好的分离包装函数(Router)的逻辑,减少对原对象的影响。

react-router 使用方式

react-router 以 react component 的组件方式提供 API, 包含 Router,Route,Redirect,Link 等等,这样能够充分利用 react component 提供的生命周期特性,同时也让定义路由跟写 react component 达到统一,如下

render((
  <Router history={browserHistory}>
    <Route path="/" component={App}>
      <Route path="about" component={About}/>
      <Route path="users" component={Users}>
        <Route path="/user/:userId" component={User}/>
      </Route>
      <Route path="*" component={NoMatch}/>
    </Route>
  </Router>
), document.body)

就这样,声明了一份含有 path to component 的各个映射的路由表。
react-router 还提供的 Link 组件(如下),作为提供更新 url 的途径,触发 Link 后最终将通过如上面定义的路由表进行匹配,并拿到对应的 component 及 state 进行 render 渲染页面。

<Link to={`/user/89757`}>'joey'</Link>

这里不细讲 react-router 的使用,详情可见:https://github.com/reactjs/react-router

从点击 Link 到 render 对应 component ,路由中发生了什么

为何能够触发 render component ?

主要是因为触发了 react setState 的方法从而能够触发 render component。
从顶层组件 Router 出发(下面代码从 react-router/Router 中摘取),可看到 Router 在 react component 生命周期之组件被挂载前 componentWillMount 中使用 this.history.listen 去注册了 url 更新的回调函数。回调函数将在 url 更新时触发,回调中的 setState 起到 render 了新的 component 的作用。

Router.prototype.componentWillMount = function componentWillMount() {
    // .. 省略其他
    var createHistory = this.props.history;

    this.history = _useRoutes2['default'](createHistory)({
      routes: _RouteUtils.createRoutes(routes || children),
      parseQueryString: parseQueryString,
      stringifyQuery: stringifyQuery
    });

    this._unlisten = this.history.listen(function (error, state) {
        _this.setState(state, _this.props.onUpdate);
    });
  };

上面的 _useRoutes2 对 history 操作便是对其做一层包装,所以调用的 this.history 实际为包装以后的对象,该对象含有 _useRoutes2 中的 listen 方法,如下

function listen(listener) {
      return history.listen(function (location) {
          // .. 省略其他
          match(location, function (error, redirectLocation, nextState) {
            listener(null, nextState);
          });
      });
}

可看到,上面代码中,主要分为两部分
1. 使用了 history 模块的 listen 注册了一个含有 setState 的回调函数(这样就能使用 history 模块中的机制)
2. 回调中的 match 方法为 react-router 所特有,match 函数根据当前 location 以及前面写的 Route 路由表匹配出对应的路由子集得到新的路由状态值 state,具体实现可见 react-router/matchRoutes ,再根据 state 得到对应的 component ,最终执行了 match 中的回调 listener(null, nextState) ,即执行了 Router 中的监听回调(setState),从而更新了展示。

以上,为起始注册的监听,及回调的作用。

如何触发监听的回调函数的执行?

这里还得从如何更新 url 说起。一般来说,url 更新主要有两种方式:简单的 hash 更新或使用 history api 进行地址更新。在 react-router 中,其提供了 Link 组件,该组件能在 render 中使用,最终会表现为 a 标签,并将 Link 中的各个参数组合放它的 href 属性中。可以从 react-router/ Link 中看到,对该组件的点击事件进行了阻止了浏览器的默认跳转行为,而改用 history 模块的 pushState 方法去触发 url 更新。

Link.prototype.render = function render() {
    // .. 省略其他
    props.onClick = function (e) {
      return _this.handleClick(e);
    };
    if (history) {
     // .. 省略其他
      props.href = history.createHref(to, query);
    }
    return _react2['default'].createElement('a', props);
};

Link.prototype.handleClick = function handleClick(event) {
    // .. 省略其他
    event.preventDefault();
    this.context.history.pushState(this.props.state, this.props.to, this.props.query);
};

对 history 模块的 pushState 方法对 url 的更新形式,同样分为两种,分别在 history/createBrowserHistory 及 history/createHashHistory 各自的 finishTransition 中,如 history/createBrowserHistory 中使用的是 window.history.replaceState(historyState, null, path); 而 history/createHashHistory 则使用 window.location.hash = url,调用哪个是根据我们一开始创建 history 的方式。

更新 url 的显示是一部分,另一部分是根据 url 去更新展示,也就是触发前面的监听。这是在前面 finishTransition 更新 url 之后实现的,调用的是 history/createHistory 中的 updateLocation 方法,changeListeners 中为 history/createHistory 中的 listen 中所添加的,如下

function updateLocation(newLocation) {
   // 示意代码
    location = newLocation;
    changeListeners.forEach(function (listener) {
      listener(location);
    });
}
function listen(listener) {
     // 示意代码
    changeListeners.push(listener);
}

总结

可以将以上 react-router 的整个包装闭环总结为

  1. 回调函数:含有能够更新 react UI 的 react setState 方法。
  2. 注册回调:在 Router componentWillMount 中使用 history.listen 注册的回调函数,最终放在 history 模块的 回调函数数组 changeListeners 中。
  3. 触发回调:Link 点击触发 history 中回调函数数组 changeListeners 的执行,从而触发原来 listen 中的 setState 方法,更新了页面

至于前进与后退的实现,是通过监听 popstate 以及 hashchange 的事件,当前进或后退 url 更新时,触发这两个事件的回调函数,回调的执行方式 Link 大致相同,最终同样更新了 UI ,这里就不再说明。

react-router 主要是利用底层 history 模块的机制,通过结合 react 的架构机制做一层包装,实际自身的内容并不多,但其包装的思想笔者认为很值得学习,有兴趣的建议阅读下源码,相信会有其他收获。

 

本文转载自:https://github.com/joeyguo/blog/issues/2?f=tt&hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=to...

上一篇: Js 笔记
下一篇: RESTful API规范
ayoway
粉丝 3
博文 44
码字总数 13558
作品 0
深圳
高级程序员
私信 提问
React-Router底层原理分析与实现

用技术改变生活,用生活完善技术。来自微播易一枚向全栈方向努力奔跑的前端工程师。 微信同步:wDxKn89 React-Router基本了解 对于React-Router是针对React定义的路由库,用于将URL和compone...

北宸南蓁
2018/08/20
0
0
react-router源码解析

前言 上篇文章介绍了前端路由的两种实现原理,今天我想从源码分析下他们是如何管理前端路由的。因为之前一直都是使用V4的版本,所以接下来分析的也是基于react-router 的(以下简称 V4),欢...

liweiRose
08/10
0
0
前端路由实现及 react-router v4 源码分析

原文发布于我的 GitHub blog,有所有文章的归档,欢迎 star 前言 react-router 目前作为 react 最流行的路由管理库,已经成为了某种意义上的官方路由库(不过下一代的路由库 reach-router 已...

fi3ework
2018/07/12
0
0
react-router 源码浅析

用 react-router 也用了比较久了,对他的内部工作方式却只是了解皮毛,而且大部分还是通过别人的博客。最近两周打算自己探究一下他的实现。 注意!因为我只使用过 v3 版本的 react-router,因...

刘zx
2018/06/09
0
0
从路由原理出发,深入阅读理解react-router 4.0的源码

  react-router等前端路由的原理大致相同,可以实现无刷新的条件下切换显示不同的页面。路由的本质就是页面的URL发生改变时,页面的显示结果可以根据URL的变化而变化,但是页面不会刷新。通...

yuxiaoliang
2018/09/18
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Git ssh配置

生成密钥对 ssh-keygen -t rsa -C "email@email.com"邮箱替换自己邮箱在地址C:\Users\账户\.ssh下,id_rsa、id_rsa.pub两个文件复制文件id_rsa.pub内容到github\gitlab的Settings-> SSH ......

JUKE
27分钟前
5
0
014、使用docker-compose安装软件

创建docker-compose基础目录 mkdir -p /usr/local/docker 1、安装mysql 在/usr/local/docker/目录下创建mysql目录 mkdir -p /usr/local/docker/mysql 在/usr/local/docker/mysql目录编写doc......

北岩
27分钟前
5
0
【并发那些事 】创建线程的三种方式

创建线程可以说是并发知识中最基础的操作了,JDK 提供的创建线程的方式,如果不包括通过线程池的话,目前有三种形式,它们分别是通过继承 Thread 类,通过实现 Runable 接口,通过 FutureTa...

K1W1
33分钟前
6
0
判断链表是否有环

如果列表中不存在环,最终快指针将会最先到达尾部,此时我们可以返回 false。 如果存在环则会相遇。返回true。 Java代码实现: public boolean hasCycle(ListNode head) { if (head == ...

无名氏的程序员
35分钟前
5
0
uni-app 项目记录

await 等候,等待;期待 什么是async、await await 用于等待异步完成 通常async、await都是跟随Promise一起使用的 async返回的都是一个Promise对象同时async适用于任何类型的函数上。这样awa...

达达前端小酒馆
36分钟前
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部