深入浅出react-router原理

366 阅读4分钟

引言

路由即根据不同的路径渲染不同的组件 实现方式有两种:

HashRouter: 利用hash实现路由的切换
BrowserRouter:利用h5 api 实现路由的切换

自己手写实现react-router,彻底掌握react-router原理,我将以这样的顺序进行。

  • 路由的两种实现方式
  • 实现react-router-dom
  • 实现Router和Route
  • 实现hashHistory
  • 实现browserHistory
  • 正则表达式的应用
  • 实现matchPath
  • 实现Switch
  • 实现Redirect、Link
  • 实现嵌套路由
  • 受保护路由
  • 实现NavLink
  • 实现WithRouter、Prompt
  • 实现react-router(路由)中的hooks

props路由参数: history:历史对象 location: 路径对象 match: 匹配参数

image.png

hashRouter的实现方式

function createHashHistory() {
    let action;
    const listeners = [];
    const historyStack = [];//历史栈
    let historyIndex = -1;
    let state;
    function listen(listener){
        listeners.push(listener);
        return () => {
            const index = listeners.indexOf(listener);
            listeners.splice(index, 1);
        }
    }
    
    const hashChange = () => {
        let pathname = window.location.hash.slice(1);
        Object.assign(history, { action, location: { pathanme, state }})
    }
    if(!action || action === 'PUSH') {
        historyStack[++historyIndex] = history.location;
    }else if(action === 'REPLACE'){
        historyStack[historyIndex] = history.location;
    }
    listeners.forEach(listener => listener(history.location));
}
window.addEventListener('hashChange', hashChange);

function push(pathname, nextState) {
/* history.push('user', {name: 'tom'}) ; history.push({ pathname: '/user', state: {name: "用户管理"} })*/
    action = 'PUSH';
    if(typeof pathname === 'object') {
        state = pathname.state;
        pathname = pathname.pathname;
    }else {
        state = nextState;
    }
    window.location.hash = pathname;
}

function replace(pathname, nextState) {
    if(typeof pathname === 'object') {
        pathname = pathname.pathname;
        state = pathname.state;
    }else {
        state = nextState;
    }
    window.location.hash = pathname;
}

/* go通过历史栈实现 */

function go(n) {
    action = 'POP';
    historyIndex += n;
    const nextLocation = historyStack[historyIndex];
    state = nextLocation.state;
    window.location.hash = nextLocation.pathname;
}

function goBack(-1) {
    go(-1);
}

function goForward() {
    go(1);
}
/* hashHistory中的history没有兼容性问题 */
const history = {
    action: 'POP',
    location: { pathname: "/", state: undefined },
    go,
    goBack,
    goForward,
    push,
    replace,
    listen
}
action = 'PUSH';
if(window.location.hash) {
    hashChange();
}else {
    window.location.hash = '/';
}
return history;

browserHistory的实现方式

利用H5 API实现路由的切换,HTML5规范给我们提供了一个history接口,HISTORY给我们提供了两个方法一个事件: history.pushState()和history.replaceState()、window.onpopstate。


history.pushState(stateObject, title, url),包括三个参数
第一个参数用于存储该url对应的状态对象,该对象可在onpopstate事件中获取,也可在history对象中获取
第二个参数是标题,目前浏览器并未实现
第三个参数则是设定的url
pushState函数向浏览器的历史堆栈压入一个url为设定值的记录,并改变历史堆栈的当前指针至栈顶

history.replaceState(stateObject, title, url):
该接口与pushState参数相同,含义也相同
唯一的区别在于replaceState是替换浏览器历史堆栈的当前历史记录为设定的url
需要注意的是replaceState不会改动浏览器历史堆栈的当前指针


history.onpopstate,该事件是window的属性
该事件会在调用浏览器的前进、后退以及执行history.forward、history.back、和history.go触发,因为这些操作有一个共性,即修改了历史堆栈的当前指针
在不改变document的前提下,一旦当前指针改变则会触发onpopstate事件
function createBrowserHistory() {
    const globalHistory = window.history;
    const listeners = [];
    let action, state, message;
    
    function go(n) {
        globalHistory.go(n);
    }
    
    function goForward() {
        go(1);
    }
    function goBack(n) {
        go(-1);
    }
    function listen(listener) {
        listeners.push(listener);
        return () => {
            let index = listeners.indexOf(listener);
            listeners.splice(index, 1);
        }
    }
    function setState(newState) {
        Object.assign(history, newState);
        listeners.forEach(listener => listener(history.location));
    }
    function push(pathname, nextState) {
        action = 'PUSH';
        if(typeof pathname === 'object') {
            state = pathname.state;
            pathname = pathname.pathname;
        }else { state = nextState }
        if(messgage) {
        let showMessage = message({ pathname });
        let allow = window.confirm(showMessage);
        if(!allow) return;
    }
    globalHistory.pushState(state, null, pathname);
    let location = { state, pathname };
    setState({ action, location });
    }
    /* 当回退或者前进的时候执行该函数,浏览器自带的,默认支持 */
    window.onpopstate = (event) => {
        setState({ action: 'POP', location: { pathname: window.location.pathname, state: globalHistory.state }})
    }
    function block(newMessage) {
        message = newMessage;
        return () => message = null;
    }
    const history = {
        action: 'POP';
        location: { pathname: window.location.pathname, state: globalHistory.state },
        go,
        goForward,
        goBack,
        push,
        listen,
        block
    }
    return history;
}

实现matchPath

import pathToRegexp from 'path-to-regexp';

const cache = {};

function compilePath(path, options = {}) {
    let cacheKey = path + JSON.stringify(options);
    if (cache[cacheKey]) return cache[cacheKey];

    const keys = [];//处理路径参数
    const regexp = pathToRegexp(path, keys, options);
    let result = { keys, regexp };
    cache[cacheKey] = result;
    return { keys, regexp };
}
/* 
pathname: 浏览器当前真实的路径名
options: Route组件的props  path Component exact
path Route的路径
exact 是否精确匹配
strict 是否严格匹配
sensitive 是否大小写敏感
*/
/* 根据路径匹配到match */
function matchPath(pathname, options = {}) {
    let { path = '/', exact = false, strict = false, sentive = false } = options;
    let { keys, regexp } = compilePath(path, { end: exact, strict, sentive });
    const match = regexp.exec(pathname);
    if (!match) return null;
    const [url, ...values] = match;
    const isExact = pathname === url;
    /* 要求精确但是并不精确 */
    if (exact && !isExact) return null;
    return {
        path,//来自Route里的path路径
        url,//来自浏览器地址中的url
        isExact,//是否精确匹配
        params: keys.reduce((memo, key, index) => {
            memo[key.name] = values[index];
            return memo;
        }, {})
    }
}

export default matchPath;
  • 自定义event事件:使用customEvent构造函数
window.addEventListener('eventName', (e) => {
    console.log('111', e);
})
let event = new CustomEvent('eventName', {
    detail: {
    message: 'Hello World'
    }
})

window.dispatchEvent(event);

image.png

正则表达式的使用

  • path-to-regext 把路径转成一个正则,跟真实的路径做匹配

正则表达式可视化工具: jex.im/regulex/#!f…

  • 嵌套路由不需要在源码中有额外的代码,Route已经有支持了
剩下的内容见: github.com/China-forre…