手写history里的createBrowserHistory

531 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第22天,点击查看活动详情

前言

《说说react-router里的history》的文章中,我们讲到react-router里的核心history库,本篇章主要讲述该库的history模式实现方式。

地址栈创建方案

关于地址栈的创建,我们可以通过创建自己的地址栈并维护、保持与浏览器地址栈统一。

当history在内部维护地址栈时,同时又必须与浏览器内部的地址栈一致,不过问题在于这样的方案可能由于操作不一致而导致自己的地址栈和浏览器的地址栈维护统一的麻烦。

而此时我们可以想到另一种简便的方案,就是通过浏览器的地址栈模型,基于history API映射出自己的地址栈,所以这种方案不需要维护地址栈,维护地址栈是浏览器内部完成。

手写createLocation

在实现createBrowserHistory之前,我们需要先实现目标history的location对象。

关于location对象的相关属性,我们通过控制台打印了解一下:

import {createBrowserHistory} from "history"
const history = createBrowserHistory()
export default function App() {
    console.log(history)
    return <></>
}

控制台输出结果:

截屏2022-06-19 下午6.42.58.png

可以看出location的属性有hash、key、pathname、search、state等。 所以我们可以通过其属性的语义,基于window.location去实现我们的location属性,不过为了通用性,我们应该基于一个可配置的basename再去结合window.location去实现下列代码:

function createLocation(basename = "") {
    // 通过window.location读取地址栏的pathname
    const { hash, search, pathname } = window.location

    // 处理存在basename的情况
    const reg = new RegExp(`^${basename}`)
    pathname = pathname.replace(reg, "")

    const location = {
        hash,
        search,
        pathname
    }

    //处理state
    let state, historyState = window.history.state

    if (historyState === null) {
        state = undefined
    }
    else if (typeof historyState !== "object"){
        state = historyState
    }else {
        if ("key" in historyState) {
            location.key = historyState.key
            state = historyState.state
        } else {
            state = historyState
        }
    }
    location.state = state
    return location
}

pathname的解析需要基于一个可配置的basename去匹配。

state的处理基于两种情况:

  • 如果historyState没有值,则state为undefined。
  • 如果historyState有值
    • 如果值的类型不是对象,直接赋值给我们location的state
    • 如果值的类型是对象
      • 该对象中有key属性,将key属性作为location的key属性值,并且将historyState对象中的state属性作为state属性值
      • 如果没有key属性,则直接将historyState赋值给state

手写createBrowserHistory

创建基本的history对象

我们先看history对象里的属性和方法:

截屏2022-06-19 下午10.54.15.png

基于window.history,我们可以先实现简单的history对象:

function createBrowserHistory(options = {}) {
    // createBrowserHistory的配置项
    const {
        basename = "",
        forceRefresh = false,
        keyLength = 6,
        getUserConfirmation = (message, callback) => callback(window.confirm(message))
    } = options;
    
    function go(step) {
        window.history.go(step);
    }
    function goBack() {
        window.history.back();
    }
    function goForward() {
        window.history.forward();
    }

    const history = {
        action: "POP",
        length: window.history.length,
        go,
        goBack,
        goForward,
        location: createLocation(basename)
    };
    //返回history对象
    return history;
}

完善push和replace操作

上面的history缺少了地址栏操作方法,我们基于window.history封装我们的push、replace方法,由于push和replace有相同的操作,我们对其共同操作的代码进行提炼:

function push(path, state) {
    changePage(path, state, true);
}

function replace(path, state) {
    changePage(path, state, false);
}

function changePage(path, state, isPush) {
    let action = "PUSH";
    if (!isPush) {
        action = "REPLACE"
    }
    const pathInfo = handlePathAndState(path, state, basename);
    const location = createLoactionFromPath(pathInfo);
    
    // 后续需要封装的阻塞器
    blockManager.triggerBlock(location, action, () => {
        if (isPush) {
            window.history.pushState({
                key: createKey(keyLength),
                state: pathInfo.state
            }, null, pathInfo.path);
        }
        else {
            window.history.replaceState({
                key: createKey(keyLength),
                state: pathInfo.state
            }, null, pathInfo.path);
        }
        
        // 后续需要封装的监听器
        listenerManager.triggerListener(location, action);
        
        //改变action
        history.action = action;
        
        //改变location
        history.location = location;
        
        if (forceRefresh) {
            //强制刷新
            window.location.href = pathInfo.path;
        }
    })
}

// 根据path和state得到统一的格式
function handlePathAndState(path, state, basename) {
    if (typeof path === "string") {
        return {
            path,
            state
        }
    }
    else if (typeof path === "object") {
        let pathResult = basename + path.pathname;
        let { search = "", hash = "" } = path;
        if (search.charAt(0) !== "?") {
            search = "?" + search;
        }
        if (hash.charAt(0) !== "#") {
            hash = "#" + hash;
        }
        pathResult += search;
        pathResult += hash;
        return {
            path: pathResult,
            state: path.state
        }
    }
    else {
        throw new TypeError("path must be string or object");
    }
}

function createKey(keyLength) {
    return Math.random().toString(36).substr(2, keyLength);
}

实现createHref

function createHref(location) {
        return basename + location.pathname + location.search + location.hash;
    }

至此我们完成了基本的history模型:

const history = {
        action: "POP",
        length: window.history.length,
        createHref,
        go,
        goBack,
        goForward,
        location: createLocation(basename),
        push,
        replace
};

小结

history是react路由的核心,上面的history基于window.history和window.location实现了大部分的代码逻辑,而实际在源码里更多的是对于细节的处理。