# 【手写代码】React-Router

手写一个 Router组件,支持 push 跳转,replace 跳转。

# 一、先来一个简单的 Router 类

一个简单的路由大概就是这样

  1. 注册路由对应的渲染组件(或渲染函数)
  2. 监听 hashchange 检测到路由发生变化,重新渲染
  3. 封装 push 页面跳转方法

同理的,browserHistory 则是使用 pushState, replaceState 来进行页面跳转,用 popState 来监听跳转。

pushState 是 在 HTML5 新增在 history 上的 api,因此会存在一些兼容性问题,不够利用 react 的 状态更新机制,不用 popState 也可以实现 Router 功能,只是跳转必须用 Router 封装好的函数进行跳转即可。

class Router {
  // 渲染的 dom 节点
  constructor(renderDom){
    this.routers = new Map()
    this.dom = renderDom || document.body
    window.addEventListener('hashchange',function(){
       this._render()
    })
  }
  
  // 初始化路由
  init(){
    this._render()
  }
  // 渲染内容
  _render(){
    let path = window.location.hash
    path = path.replace('#','')
    // 找到路由对应的渲染函数,在执行渲染
    if(this.routers.get(path)){
      this.dom.innerHTML = this.routers.get(path)()
    }
  }
  
  // 添加路由对应的渲染内容
  add(path,render){
    this.routers.set(path,render)
  }
  // 移出路由
  remove(path){
    this.routers.remove(path)
  }
  // 路由跳转
  push(path){
    window.location.href = `#${path}`
  }
}
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

# 二、React-Router

原理还是和上面一样,利用 popstate 监听路由跳转。

  1. BroswerRouter 维护着一个全局的地址状态(url),当地址发生变化,子组件都重新渲染
  2. 利用 createContext 让所有子组件共享
  3. Route 组件,判断渲染的组件内容,以及路由匹配判断
// BroswerRouter.js
import React, { useState, useEffect, createContext } from "react";
export const RouterContext = createContext({});

export default ({ children }) => {
  const [url, setUrl] = useState(window.location.pathname);

  useEffect(() => {
    // 路由变化,就改变状态
    window.addEventListener("popstate", () => {
      setUrl(window.location.pathname);
    });
  }, []);

  const router = {
    history: {
      push: function(url, state, title) {
        window.history.pushState(state, title, url);
        setUrl(url);
      },
      replace: function(url, state, title) {
        window.history.replaceState(state, title, url);
        setUrl(url);
      },
      go: window.history.go,
      back: window.history.back,
      goForward: window.history.forward,
      length: window.history.length
    },
    url: url
  };

  return (
    <RouterContext.Provider value={router}>{children}</RouterContext.Provider>
  );
};
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
// Route.js
import React, { useContext } from "react";
import { RouterContext } from "./BrowserRouter";

export default function Route({ component, path }) {
  const { history, url } = useContext(RouterContext);
  const match = {
    path,
    url
  };
  // react 要求组件名称必须是大写
  const Component = component;
  // 匹配上路由,就渲染,否则不渲染
  return url === path && <Component history={history} match={match} />;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

看效果,Online DEMO

看源码, 点击查看

# 三、如何实现路由参数 /post/:id

如何实现类似这种参与的形式呢?

实现步骤:

  1. context 增加 match 属性,把从地址上匹配出的参数传到子组件上
  2. route.js 判断路径匹配的规则要变下,不能用url ===path 这种简单的方式

如何从地址上匹配出参数的字段名和值呢?下面提供两种方法。

手写代码就选第二种,顺便还能展示下正则的掌握情况。

# 1、利用第三方库来实现,动态路由

path-to-regexp 这个库支持

const { pathToRegexp } = require("path-to-regexp");

const keys = [];
const regexp = pathToRegexp("/foo/:bar", keys);
// regexp = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{ name: 'bar', prefix: '/', suffix: '', pattern: '[^\\/#\\?]+?', modifier: '' }]
1
2
3
4
5
6

# 2、使用正则匹配

使用一个万能正则公式去匹配出想要的值。

(.*?) 没有换行的内容匹配,把需要取出来的值,用这个替换

//eg: `this is a {age}` 匹配出 age 字段名

'this is a {age}'.match(/this is a {(.*?)}$/)
// ["this is a {age}", "age", index: 0, input: "this is a {age}", groups: undefined] 
1
2
3
4
let route = '/post/:id'
let path = '/post/1'

// 如何匹配得出 {id:1}
let key = route.match(/\post\/:(.*?)$/)  // ["post/:id", "id", index: 1, input: "/post/:id", groups: undefined]
let value = path.match(/\/post\/(.*?)$/) // ["/post/1", "1", index: 0, input: "/post/1", groups: undefined]

// key[1] , value[1] 就是想要的值了

// 这里就可以拿到对应的 key 和 value 的值了。

// 多个参数的话
'/post/:id/:name'.match(/\/post\/:(.*?)\/:(.*?)$/)   // ["/post/:id/:name", "id", "name", index: 0, input: "/post/:id/:name", groups: undefined]
1
2
3
4
5
6
7
8
9
10
11
12
13
Last Updated: 12/30/2019, 10:29:07 AM