路由分享记录

195 阅读6分钟

一 背景

单页应用

单页应用(英语:single-page application,缩写SPA)是一种网络应用程序网站的模型,它通过动态重写当前页面来与用户交互,而非传统的从服务器重新加载整个新页面。这种方法避免了页面之间切换打断用户体验,使应用程序更像一个桌面应用程序。在单页应用中,所有必要的代码(HTMLJavaScriptCSS)都通过单个页面的加载而检索[1],或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面。尽管可以用位置散列HTML5历史API来提供应用程序中单独逻辑页面的感知和导航能力,但页面在过程中的任何时间点都不会重新加载,也不会将控制转移到其他页面。[2]与单页应用的交互通常涉及到与网页服务器后端的动态通信。

—— 维基百科

浏览器历史记录[ 编辑 ]

根据单页应用(SPA)模型的定义,它只有“单个页面”,因此这打破了浏览器为页面历史记录导航所设计的“前进/后退”功能。当用户按下后退按钮时,可能会遇到可用性障碍,页面可能返回真正的上一个页面,而非用户所期望的上一个页面。

传统的解决方案是不断更改浏览器网址(URL)的散列片段标识符以保持与当前的屏幕状态一致。这种方案可以通过JavaScript实现,并在浏览器中创建起网址历史事件。只要单页应用能根据网址散列所包含的信息重新生成相同的屏幕状态,就能实现预期的后退按钮行为。

而为进一步解决此问题,HTML5规范引入了pushState (页面存档备份,存于互联网档案馆)和replaceState (页面存档备份,存于互联网档案馆)来提供代码对实际网址和浏览器历史的访问。

前端路由要解决的问题

  1. 保存屏幕状态

  2. 前进/后退功能

  3. 不会重新加载

二 方案

Hash 模式是使用 URL 的 Hash 来模拟一个完整的 URL,因此当 URL 改变的时候页面并不会重载。History 模式则会直接改变 URL,所以在路由跳转的时候会丢失一些地址信息,在刷新或直接访问路由地址的时候会匹配不到静态资源。因此需要在服务器上配置一些信息,让服务器增加一个覆盖所有情况的候选资源,比如跳转 index.html 什么的

示例

// hash路由示例
var hashPath = 'www.qunar.com/#/user/user…';

// history示例
var historyPath = 'www.qunar.com/user/userli…';

hash路由 优缺点

  • 优点

    • 实现简单,兼容性好(兼容到ie8
  • 绝大多数前端框架均提供了给予hash的路由实现
  • 不需要服务器端进行任何设置和开发
  • 除了资源加载和ajax请求以外,不会发起其他请求
  • 缺点

    • 对于部分需要重定向的操作,后端无法获取hash部分内容,导致后台无法取得url中的数据,典型的例子就是微信公众号的oauth验证
  • 服务器端无法准确跟踪前端路由信息
  • 对于需要锚点功能的需求会与目前路由机制冲突

History(browser)路由 优缺点

  • 优点

    • 对于重定向过程中不会丢失url中的参数。后端可以拿到这部分数据
  • 绝大多数前段框架均提供了browser的路由实现
  • 后端可以准确跟踪路由信息
  • 可以使用history.state来获取当前url对应的状态信息
  • 缺点

    • 兼容性不如hash路由(只兼容到IE10)
  • 需要后端支持,每次返回html文档

使用History路由实现一个简易的路由系统

$(function() {
    var app = $('#app');
    // 监听点击事件,修改页面的url
    $('button').on('click', function() {
        var path = $(this).data('path');
        var text = $(this).text();
        history.pushState(history.state, text, path);
        // pushState和replaceState无法触发popState事件,通过自定义事件,来广播页面路由修改
        var historyChange = new CustomEvent('historyChange', {
            detail: {
                path
            }
        });
        window.dispatchEvent(historyChange);
    });


    window.addEventListener('historyChange', function(e) {
        // 监听路由修改,渲染不同内容
        app.html('现在的路由地址是' + e.detail.path + '<br/> 你可以渲染一些别的组件了');
    });

    // window.addEventListener('popstate', function(e)  {
    //     console.log(1);
    // })
    // 处理初始化的路由和视图对应关系
    let pathname = window.location.pathname;
    if (pathname === '/router1' || pathname === '/router2') {
        publishHistoryChange();
    }
    // 处理浏览器前进后退, history.back, history.go, history.forward
    window.addEventListener('popstate', publishHistoryChange);

    // pulish事件
    function publishHistoryChange() {
        var historyChange = new CustomEvent('historyChange', {
            detail: {
                path: window.location.pathname
            }
        });
        window.dispatchEvent(historyChange);
    }
});

代码示例

三、项目中使用React-router

CELL/router-demo

四、react-router源码

准备工作

  1. 了解钩子函数的使用包括 React.useRef, React.useLayoutEffect

  2. 了解React中的Context的使用,React.createContext

源码

react-router-dom包中的BrowserRouter实现

export function BrowserRouter({
basename,
children,
window,
}: BrowserRouterProps) {
let historyRef = React.useRef<BrowserHistory>();
if (historyRef.current == null) {
historyRef.current = createBrowserHistory({ window });
}

let history = historyRef.current;
let [state, setState] = React.useState({
action: history.action,
location: history.location,
});

// 注册history监听,当history变化时,调用setState,触发更新
React.useLayoutEffect(() => history.listen(setState), [history]);

return (
<Router
basename={basename}
children={children}
location={state.location}
navigationType={state.action}
navigator={history}
/>
);
}

history包中的createBrowserHistory实现

export function createBrowserHistory(
options: BrowserHistoryOptions = {}
): BrowserHistory {
let { window = document.defaultView! } = options;
let globalHistory = window.history;

function getIndexAndLocation(): [number, Location] {

}

let blockedPopTx: Transition | null = null;
function handlePop() {
applyTx(nextAction);
}

// 通过监听popState事件,处理浏览器前进/后退的渲染处理
window.addEventListener(PopStateEventType, handlePop);

let action = Action.Pop;
let [index, location] = getIndexAndLocation();
let listeners = createEvents<Listener>();
let blockers = createEvents<Blocker>();

if (index == null) {
index = 0;
globalHistory.replaceState({ ...globalHistory.state, idx: index }, "");
}

function createHref(to: To) {
return typeof to === "string" ? to : createPath(to);
}

// state defaults to `null` because `window.history.state` does
function getNextLocation(to: To, state: any = null): Location {

}

function getHistoryStateAndUrl(
nextLocation: Location,
index: number
): [HistoryState, string] {

}

function allowTx(action: Action, location: Location, retry: () => void) {

}

function applyTx(nextAction: Action) {
action = nextAction;
[index, location] = getIndexAndLocation();
// 触发listeners, 触发组件渲染
listeners.call({ action, location });
}

function push(to: To, state?: any) {
let nextAction = Action.Push;
let nextLocation = getNextLocation(to, state);
function retry() {
push(to, state);
}

if (allowTx(nextAction, nextLocation, retry)) {
let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);

// TODO: Support forced reloading
// try...catch because iOS limits us to 100 pushState calls :/
try {
// 触发url修改
globalHistory.pushState(historyState, "", url);
} catch (error) {
// They are going to lose state here, but there is no real
// way to warn them about it since the page will refresh...
window.location.assign(url);
}
// 更新组件
applyTx(nextAction);
}
}

function replace(to: To, state?: any) {
let nextAction = Action.Replace;
let nextLocation = getNextLocation(to, state);
function retry() {
replace(to, state);
}

if (allowTx(nextAction, nextLocation, retry)) {
let [historyState, url] = getHistoryStateAndUrl(nextLocation, index);

// TODO: Support forced reloading
globalHistory.replaceState(historyState, "", url);

applyTx(nextAction);
}
}

function go(delta: number) {
globalHistory.go(delta);
}

let history: BrowserHistory = {
get action() {
return action;
},
get location() {
return location;
},
createHref,
push,
replace,
go,
back() {
go(-1);
},
forward() {
go(1);
},
listen(listener) {
return listeners.push(listener);
},
block(blocker) {
let unblock = blockers.push(blocker);

if (blockers.length === 1) {
window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
}

return function () {
unblock();

// Remove the beforeunload listener so the document may
// still be salvageable in the pagehide event.
// See https://html.spec.whatwg.org/#unloading-documents
if (!blockers.length) {
window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload);
}
};
},
};

return history;
}

react-router中的Router实现

react-router中的Routes和Route实现

这里借用了别人的一张更新图,新版本的更新流程是通过applyTx直接调用外部传入的state的hooksuseState来实现组件更新

image.png

引用

文档地址: React Router | API Reference

掘金文章: juejin.cn/post/688629…