前端中的 hash 和 history 路由

前端路由有有 hash 路由和 history 路由兩種路由方式费彼,他們的原理是什么,又怎樣實(shí)現(xiàn)一個(gè)簡(jiǎn)單的路由監(jiān)聽呢喘鸟?

我們?cè)谑褂?Vue 或者 React 等前端渲染時(shí),通常會(huì)有 hash 路由和 history 路由兩種路由方式盈罐。

1.hash 路由:監(jiān)聽 url 中 hash 的變化,然后渲染不同的內(nèi)容闪唆,這種路由不向服務(wù)器發(fā)送請(qǐng)求盅粪,不需要服務(wù)端的支持;

2.history 路由:監(jiān)聽 url 中的路徑變化悄蕾,需要客戶端和服務(wù)端共同的支持票顾;

我們一步步實(shí)現(xiàn)這兩種路由,來深入理解下底層的實(shí)現(xiàn)原理帆调。我們主要實(shí)現(xiàn)以下幾個(gè)簡(jiǎn)單的功能:

1.監(jiān)聽路由的變化奠骄,當(dāng)路由發(fā)生變化時(shí),可以作出動(dòng)作番刊;

2.可以前進(jìn)或者后退含鳞;

3.可以配置路由;

1. hash 路由

當(dāng)頁面中的hash發(fā)生變化時(shí)芹务,會(huì)觸發(fā)hashchange事件蝉绷,因此我們可以監(jiān)聽這個(gè)事件,來判斷路由是否發(fā)生了變化枣抱。

window.addEventListener(
    'hashchange',
    function (event) {
        const oldURL = event.oldURL; // 上一個(gè)URL
        const newURL = event.newURL; // 當(dāng)前的URL
        console.log(newURL, oldURL);
    },
    false
);

1.1 實(shí)現(xiàn)的過程

對(duì) oldURL 和 newURL 進(jìn)行拆分后熔吗,就能獲取到更詳細(xì)的 hash 值。我們這里從創(chuàng)建一個(gè) HashRouter 的 class 開始一步步寫起:

class HashRouter {
    currentUrl = ''; // 當(dāng)前的URL
    handlers = {};

    getHashPath(url) {
        const index = url.indexOf('#');
        if (index >= 0) {
            return url.slice(index + 1);
        }
        return '/';
    }
}

事件hashchange只會(huì)在hash 發(fā)生變化時(shí)才能觸發(fā)沃但,而第一次進(jìn)入到頁面時(shí)并不會(huì)觸發(fā)這個(gè)事件磁滚,因此我們還需要監(jiān)聽load事件。這里要注意的是宵晚,兩個(gè)事件的event 是不一樣的:hashchange事件中的 event對(duì)象有 oldURLnewURL兩個(gè)屬性垂攘,但 load 事件中的 event 沒有這兩個(gè)屬性,不過我們可以通過 location.hash 來獲取到當(dāng)前的 hash 路由:

class HashRouter {
    currentUrl = ''; // 當(dāng)前的URL
    handlers = {};

    constructor() {
        this.refresh = this.refresh.bind(this);
        window.addEventListener('load', this.refresh, false);
        window.addEventListener('hashchange', this.refresh, false);
    }

    getHashPath(url) {
        const index = url.indexOf('#');
        if (index >= 0) {
            return url.slice(index + 1);
        }
        return '/';
    }

    refresh(event) {
        let curURL = '',
            oldURL = null;
        if (event.newURL) {
            oldURL = this.getHashPath(event.oldURL || '');
            curURL = this.getHashPath(event.newURL || '');
        } else {
            curURL = this.getHashPath(window.location.hash);
        }
        this.currentUrl = curURL;
    }
}

到這里已經(jīng)可以實(shí)現(xiàn)獲取當(dāng)前的 hash 路由淤刃,但路由發(fā)生變化時(shí)晒他,我們的頁面應(yīng)該進(jìn)行切換,因此我們需要監(jiān)聽這個(gè)變化:

class HashRouter {
    currentUrl = ''; // 當(dāng)前的URL
    handlers = {};

    // 暫時(shí)省略上面的代碼

    refresh(event) {
        // 當(dāng)hash路由發(fā)生變化時(shí)逸贾,則觸發(fā)change事件
        this.emit('change', curURL, oldURL);
    }

    on(evName, listener) {
        this.handlers[evName] = listener;
    }
    emit(evName, ...args) {
        const handler = this.handlers[evName];
        if (handler) {
            handler(...args);
        }
    }
}
const router = new HashRouter();
rouer.on('change', (curUrl, lastUrl) => {
    console.log('當(dāng)前的hash:', curUrl);
    console.log('上一個(gè)hash:', lastUrl);
});

1.2 調(diào)用的方式

到這里陨仅,我們把基本的功能已經(jīng)完成了。來配合一個(gè)例子就更形象了:

// 先定義幾個(gè)路由
const routes = [
    {
        path: '/',
        name: 'home',
        component: <Home />,
    },
    {
        path: '/about',
        name: 'about',
        component: <About />,
    },
    {
        path: '*',
        name: '404',
        component: <NotFound404 />,
    },
];
const router = new HashRouter();
// 監(jiān)聽change事件
router.on('change', (currentUrl, lastUrl) => {
    let route = null;
    // 匹配路由
    for (let i = 0, len = routes.length; i < len; i++) {
        const item = routes[i];
        if (currentUrl === item.path) {
            route = item;
            break;
        }
    }
    // 若沒有匹配到铝侵,則使用最后一個(gè)路由
    if (!route) {
        route = routes[routes.length - 1];
    }
    // 渲染當(dāng)前的組件
    ReactDOM.render(route.component, document.getElementById('app'));
});

hash 路由的樣例

html代碼:

<div class="container">
    <nav>
        <p><a href="#/">home</a></p>
        <p><a href="#/article">article</a></p>
        <p><a href="#/archive#a=1">archive#a=1</a></p>
        <p><a href="#/about">about</a></p>
    </nav>
    <div id="app"></div>
</div>

js代碼:

const { useState, useEffect, useMemo } = React;

const Home = () => {
    const [count, setCount] = useState(0);
    const [nowtime, setNowtime] = useState(0);
    const sum = useMemo(() => ((1 + count) * count) / 2 + " ,random: " + Math.random(), [count]);

    return (
        <div>
            <h1>Home Page</h1>
            <p> count: {count}</p>
            <p> sum: {sum}</p>
            <p> nowtime: {nowtime}</p>
            <button onClick={() => setCount(count + 1)}> add 1 </button>
            <button onClick={() => setNowtime(Date.now())}> set now time </button>
        </div>
    );
};
const About = () => {
    return (
        <div>
            <h1>About Page</h1>
        </div>
    );
};
const NotFound404 = () => {
    return (
        <div>
            <h1>404 Page</h1>
        </div>
    );
};
// ReactDOM.render(<Home />, document.getElementById("app"));
const routes = [
    {
        path: "/",
        name: "home",
        component: <Home />,
    },
    {
        path: "/about",
        name: "about",
        component: <About />,
    },
    {
        path: "*",
        name: "404",
        component: <NotFound404 />,
    },
];
const router = new HashRouter();
router.on("change", (currentUrl, lastUrl) => {
    console.log(currentUrl, lastUrl);
    let route = null;
    for (let i = 0, len = routes.length; i < len; i++) {
        const item = routes[i];
        if (currentUrl === item.path) {
            route = item;
            break;
        }
    }
    if (!route) {
        route = routes[routes.length - 1];
    }
    ReactDOM.render(route.component, document.getElementById("app"));
});

2. history 路由

history 路由中灼伤,我們一定會(huì)使用window.history中的方法,常見的操作有:
1.back():后退到上一個(gè)路由咪鲜;

2.forward():前進(jìn)到下一個(gè)路由狐赡,如果有的話;

3.go(number):進(jìn)入到任意一個(gè)路由疟丙,正數(shù)為前進(jìn)颖侄,負(fù)數(shù)為后退鸟雏;

4.pushState(obj, title, url):前進(jìn)到指定的 URL,不刷新頁面览祖;

5.replaceState(obj, title, url):用 url 替換當(dāng)前的路由孝鹊,不刷新頁面;
調(diào)用這幾種方式時(shí)展蒂,都會(huì)只是修改了當(dāng)前頁面的 URL又活,頁面的內(nèi)容沒有任何的變化。但前 3 個(gè)方法只是路由歷史記錄的前進(jìn)或者后退玄货,無法跳轉(zhuǎn)到指定的 URL皇钞;而pushStatereplaceState可以跳轉(zhuǎn)到指定的 URL悼泌。如果有面試官問起這個(gè)問題“如何僅修改頁面的 URL松捉,而不發(fā)送請(qǐng)求”,那么答案就是這 5 種方法馆里。

如果服務(wù)端沒有新更新的 url 時(shí)隘世,一刷新瀏覽器就會(huì)報(bào)錯(cuò),因?yàn)樗⑿聻g覽器后鸠踪,是真實(shí)地向服務(wù)器發(fā)送了一個(gè) http 的網(wǎng)頁請(qǐng)求丙者。因此若要使用 history 路由,需要服務(wù)端的支持营密。

2.1 應(yīng)用的場(chǎng)景

pushStatereplaceState 兩個(gè)方法跟 location.hreflocation.replace 兩個(gè)方法有什么區(qū)別呢械媒?應(yīng)用的場(chǎng)景有哪些呢?

1.location.hreflocation.replace 切換時(shí)要向服務(wù)器發(fā)送請(qǐng)求评汰,而 pushStatereplace 僅修改 url纷捞,除非主動(dòng)發(fā)起請(qǐng)求;

2.僅切換 url 而不發(fā)送請(qǐng)求的特性被去,可以在前端渲染中使用主儡,例如首頁是服務(wù)端渲染,二級(jí)頁面采用前端渲染惨缆;

3.可以添加路由切換的動(dòng)畫糜值;

4.在瀏覽器中使用類似抖音的這種場(chǎng)景時(shí),用戶滑動(dòng)切換視頻時(shí)坯墨,可以靜默修改對(duì)應(yīng)的 URL寂汇,當(dāng)用戶刷新頁面時(shí),還能停留在當(dāng)前視頻捣染。

2.2 無法監(jiān)聽路由的變化

當(dāng)我們用 history 的路由時(shí)骄瓣,必然要能監(jiān)聽到路由的變化才行。全局有個(gè)popstate事件液斜,別看這個(gè)事件名稱中有個(gè) state 關(guān)鍵詞累贤,但pushStatereplaceState被調(diào)用時(shí)叠穆,是不會(huì)觸發(fā)觸發(fā) popstate 事件的,只有上面列舉的前 3 個(gè)方法會(huì)觸發(fā)臼膏∨鸨唬可以點(diǎn)擊【popState 不會(huì)觸發(fā) popstate 事件】查看。
針對(duì)這種情況渗磅,我們可以使用window.dispatchEvent添加事件:

const listener = function (type) {
    var orig = history[type];
    return function () {
        var rv = orig.apply(this, arguments);
        var e = new Event(type);
        e.arguments = arguments;
        window.dispatchEvent(e);
        return rv;
    };
};
window.history.pushState = listener('pushState');
window.history.replaceState = listener('replaceState');

然后就可以添加對(duì)這兩個(gè)方法的監(jiān)聽了:

window.addEventListener('pushState', this.refresh, false);
window.addEventListener('replaceState', this.refresh, false);

2.3 完整的代碼

class HistoryRouter {
    currentUrl = '';
    handlers = {};

    constructor() {
        this.refresh = this.refresh.bind(this);
        this.addStateListener();
        window.addEventListener('load', this.refresh, false);
        window.addEventListener('popstate', this.refresh, false);
        window.addEventListener('pushState', this.refresh, false);
        window.addEventListener('replaceState', this.refresh, false);
    }
    addStateListener() {
        const listener = function (type) {
            var orig = history[type];
            return function () {
                var rv = orig.apply(this, arguments);
                var e = new Event(type);
                e.arguments = arguments;
                window.dispatchEvent(e);
                return rv;
            };
        };
        window.history.pushState = listener('pushState');
        window.history.replaceState = listener('replaceState');
    }
    refresh(event) {
        this.currentUrl = location.pathname;
        this.emit('change', location.pathname);
        document.querySelector('#app span').innerHTML = location.pathname;
    }
    on(evName, listener) {
        this.handlers[evName] = listener;
    }
    emit(evName, ...args) {
        const handler = this.handlers[evName];
        if (handler) {
            handler(...args);
        }
    }
}
const router = new HistoryRouter();
router.on('change', function (curUrl) {
    console.log(curUrl);
});
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末嚷硫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子始鱼,更是在濱河造成了極大的恐慌仔掸,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,548評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件医清,死亡現(xiàn)場(chǎng)離奇詭異起暮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)会烙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門负懦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人柏腻,你說我怎么就攤上這事纸厉。” “怎么了五嫂?”我有些...
    開封第一講書人閱讀 167,990評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵颗品,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我沃缘,道長(zhǎng)躯枢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,618評(píng)論 1 296
  • 正文 為了忘掉前任孩灯,我火速辦了婚禮闺金,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘峰档。我一直安慰自己败匹,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評(píng)論 6 397
  • 文/花漫 我一把揭開白布讥巡。 她就那樣靜靜地躺著掀亩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪欢顷。 梳的紋絲不亂的頭發(fā)上槽棍,一...
    開封第一講書人閱讀 52,246評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼炼七。 笑死缆巧,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的豌拙。 我是一名探鬼主播陕悬,決...
    沈念sama閱讀 40,819評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼按傅!你這毒婦竟也來了捉超?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤唯绍,失蹤者是張志新(化名)和其女友劉穎拼岳,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體况芒,經(jīng)...
    沈念sama閱讀 46,268評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惜纸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了牛柒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片堪簿。...
    茶點(diǎn)故事閱讀 40,488評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖皮壁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情哪审,我是刑警寧澤蛾魄,帶...
    沈念sama閱讀 36,181評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站湿滓,受9級(jí)特大地震影響滴须,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜叽奥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評(píng)論 3 333
  • 文/蒙蒙 一扔水、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧朝氓,春花似錦魔市、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至枫夺,卻和暖如春将宪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評(píng)論 1 272
  • 我被黑心中介騙來泰國打工较坛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留印蔗,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,897評(píng)論 3 376
  • 正文 我出身青樓丑勤,卻偏偏與公主長(zhǎng)得像喻鳄,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子确封,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評(píng)論 2 359