<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入揭秘前端路由本質(zhì),手寫 mini-router

          共 5412字,需瀏覽 11分鐘

           ·

          2020-09-22 17:20


          前言

          前端路由一直是一個很經(jīng)典的話題,不管是日常的使用還是面試中都會經(jīng)常遇到。本文通過實現(xiàn)一個簡單版的 react-router 來一起揭開路由的神秘面紗。

          通過本文,你可以學(xué)習(xí)到:

          • 前端路由本質(zhì)上是什么。
          • 前端路由里的一些坑和注意點。
          • hash 路由和 history 路由的區(qū)別。
          • Router 組件和 Route 組件分別是做什么的。
          b5ec4cc00a34be9596a1b51c2edda0ee.webp

          路由的本質(zhì)

          簡單來說,瀏覽器端路由其實并不是真實的網(wǎng)頁跳轉(zhuǎn)(和服務(wù)器沒有任何交互),而是純粹在瀏覽器端發(fā)生的一系列行為,本質(zhì)上來說前端路由就是:

          對 url 進行改變和監(jiān)聽,來讓某個 dom 節(jié)點顯示對應(yīng)的視圖。

          僅此而已。新手不要被路由這個概念給嚇到。

          路由的區(qū)別

          一般來說,瀏覽器端的路由分為兩種:

          1. hash 路由,特征是 url 后面會有 # 號,如 baidu.com/#foo/bar/baz。
          2. history 路由,url 和普通路徑?jīng)]有差異。如 baidu.com/foo/bar/baz。

          我們已經(jīng)講過了路由的本質(zhì),那么實際上只需要搞清楚兩種路由分別是如何 改變,并且組件是如何監(jiān)聽并完成視圖的展示,一切就真相大白了。

          不賣關(guān)子,先分別談?wù)剝煞N路由用什么樣的 api 實現(xiàn)前端路由:

          hash

          通過 location.hash = 'foo' 這樣的語法來改變,路徑就會由 baidu.com 變更為 baidu.com/#foo。

          通過 window.addEventListener('hashchange') 這個事件,就可以監(jiān)聽hash 值的變化。

          history

          其實是用了 history.pushState 這個 API 語法改變,它的語法乍一看比較怪異,先看下 mdn 文檔里對它的定義:

          history.pushState(state, title[, url])

          其中 state 代表狀態(tài)對象,這讓我們可以給每個路由記錄創(chuàng)建自己的狀態(tài),并且它還會序列化后保存在用戶的磁盤上,以便用戶重新啟動瀏覽器后可以將其還原。

          title 當(dāng)前沒啥用。

          url 在路由中最重要的 url 參數(shù)反而是個可選參數(shù),放在了最后一位。

          通過 history.pushState({}, '', foo),可以讓 baidu.com 變化為 baidu.com/foo。

          為什么路徑更新后,瀏覽器頁面不會重新加載?

          這里我們需要思考一個問題,平常通過 location.href = 'baidu.com/foo' 這種方式來跳轉(zhuǎn),是會讓瀏覽器重新加載頁面并且請求服務(wù)器的,但是 history.pushState 的神奇之處就在于它可以讓 url 改變,但是不重新加載頁面,完全由用戶決定如何處理這次 url 改變。

          因此,這種方式的前端路由必須在支持 histroy API 的瀏覽器上才可以使用。

          為什么刷新后會 404?

          本質(zhì)上是因為刷新以后是帶著 baidu.com/foo 這個頁面去請求服務(wù)端資源的,但是服務(wù)端并沒有對這個路徑進行任何的映射處理,當(dāng)然會返回 404,處理方式是讓服務(wù)端對于"不認識"的頁面,返回 index.html,這樣這個包含了前端路由相關(guān)js代碼的首頁,就會加載你的前端路由配置表,并且此時雖然服務(wù)端給你的文件是首頁文件,但是你的 url 上是 baidu.com/foo,前端路由就會加載 /foo 這個路徑相對應(yīng)的視圖,完美的解決了 404 問題。

          history 路由的監(jiān)聽也有點坑,瀏覽器提供了 window.addEventListener('popstate') 事件,但是它只能監(jiān)聽到瀏覽器回退和前進所產(chǎn)生的路由變化,對于主動的 pushState 卻監(jiān)聽不到。解決方案當(dāng)然有,下文實現(xiàn) react-router 的時候再細講~

          實現(xiàn) react-mini-router

          本文實現(xiàn)的 react-router 基于 history 版本,用最小化的代碼還原路由的主要功能,所以不會有正則匹配或者嵌套子路由等高階特性,回歸本心,從零到一實現(xiàn)最簡化的版本。

          實現(xiàn) history

          對于 history 難用的官方 API,我們專門抽出一個小文件對它進行一層封裝,對外提供:

          1. history.push。
          2. history.listen。

          這兩個 API,減輕用戶的心智負擔(dān)。

          我們利用觀察者模式封裝了一個簡單的 listen API,讓用戶可以監(jiān)聽到 history.push 所產(chǎn)生的路徑改變。

          //?存儲?history.listen?的回調(diào)函數(shù)
          let?listeners:?Listener[]?=?[];
          function?listen(fn:?Listener)?{
          ??listeners.push(fn);
          ??return?function()?{
          ????listeners?=?listeners.filter(listener?=>?listener?!==?fn);
          ??};
          }

          這樣外部就可以通過:

          history.listen(location?=>?{
          ??console.log('changed',?location);
          });

          這樣的方式感知到路由的變化了,并且在 location 中,我們還提供了 state、pathname、search 等關(guān)鍵的信息。

          實現(xiàn)改變路徑的核心方法 push 也很簡單:

          function?push(to:?string,?state?:?State)?{
          ??//?解析用戶傳入的?url
          ??//?分解成?pathname、search?等信息
          ??location?=?getNextLocation(to,?state);
          ??//?調(diào)用原生?history?的方法改變路由
          ??window.history.pushState(state,?'',?to);
          ??//?執(zhí)行用戶傳入的監(jiān)聽函數(shù)
          ??listeners.forEach(fn?=>?fn(location));
          }

          history.push('foo') 的時候,本質(zhì)上就是調(diào)用了 window.history.pushState 去改變路徑,并且通知 listen 所掛載的回調(diào)函數(shù)去執(zhí)行。

          當(dāng)然,別忘了用戶點擊瀏覽器后退前進按鈕的行為,也需要用 popstate 這個事件來監(jiān)聽,并且執(zhí)行同樣的處理:

          //?用于處理瀏覽器前進后退操作
          window.addEventListener('popstate',?()?=>?{
          ??location?=?getLocation();
          ??listeners.forEach(fn?=>?fn(location));
          });

          接下來我們需要實現(xiàn) RouterRoute 組件,你就會看到它們是如何和這個簡單的 history 庫結(jié)合使用了。

          實現(xiàn) Router

          Router 的核心原理就是通過 Providerlocationhistory 等路由關(guān)鍵信息傳遞給子組件,并且在路由發(fā)生變化的時候要讓子組件可以感知到:

          import?React,?{?useState,?useEffect,?ReactNode?}?from?'react';
          import?{?history,?Location?}?from?'./history';
          interface?RouterContextProps?{
          ??history:?typeof?history;
          ??location:?Location;
          }

          export?const?RouterContext?=?React.createContextnull>(
          ??null,
          );

          export?const?Router:?React.FC?=?({?children?})?=>?{
          ??const?[location,?setLocation]?=?useState(history.location);
          ??//?初始化的時候?訂閱?history?的變化
          ??//?一旦路由發(fā)生改變?就會通知使用了?useContext(RouterContext)?的子組件去重新渲染
          ??useEffect(()?=>?{
          ????const?unlisten?=?history.listen(location?=>?{
          ??????setLocation(location);
          ????});
          ????return?unlisten;
          ??},?[]);

          ??return?(
          ????<RouterContext.Provider?value={{?history,?location?}}>
          ??????{children}
          ????RouterContext.Provider>

          ??);
          };

          注意看注釋的部分,我們在組件初始化的時候利用 history.listen 監(jiān)聽了路由的變化,一旦路由發(fā)生改變,就會調(diào)用 setLocation 去更新 location 并且通過 Provider 傳遞給子組件。

          并且這一步也會觸發(fā) Providervalue 值的變化,通知所有用 useContext 訂閱了 historylocation 的子組件去重新 render。

          實現(xiàn) Route

          Route 組件接受 pathchildren 兩個 prop,本質(zhì)上就決定了在某個路徑下需要渲染什么組件,我們又可以通過 RouterProvider 傳遞下來的 location 信息拿到當(dāng)前路徑,所以這個組件需要做的就是判斷當(dāng)前的路徑是否匹配,渲染對應(yīng)組件。

          import?{?ReactNode?}?from?'react';
          import?{?useLocation?}?from?'./hooks';

          interface?RouteProps?{
          ??path:?string;
          ??children:?ReactNode;
          }

          export?const?Route?=?({?path,?children?}:?RouteProps)?=>?{
          ??const?{?pathname?}?=?useLocation();
          ??const?matched?=?path?===?pathname;

          ??if?(matched)?{
          ????return?children;
          ??}
          ??return?null;
          };

          這里的實現(xiàn)比較簡單,路徑直接用了全等,實際上真正的實現(xiàn)考慮的情況比較復(fù)雜,使用了 path-to-regexp 這個庫去處理動態(tài)路由等情況,但是核心原理其實就是這么簡單。

          實現(xiàn) useLocation、useHistory

          這里就很簡單了,利用 useContext 簡單封裝一層,拿到 Router 傳遞下來的 historylocation 即可。

          import?{?useContext?}?from?'react';
          import?{?RouterContext?}?from?'./Router';

          export?const?useHistory?=?()?=>?{
          ??return?useContext(RouterContext)!.history;
          };

          export?const?useLocation?=?()?=>?{
          ??return?useContext(RouterContext)!.location;
          };

          實現(xiàn)驗證 demo

          至此為止,以下的路由 demo 就可以跑通了:

          import?React,?{?useEffect?}?from?'react';
          import?{?Router,?Route,?useHistory?}?from?'react-mini-router';

          const?Foo?=?()?=>?'foo';
          const?Bar?=?()?=>?'bar';

          const?Links?=?()?=>?{
          ??const?history?=?useHistory();

          ??const?go?=?(path:?string)?=>?{
          ????const?state?=?{?name:?path?};
          ????history.push(path,?state);
          ??};

          ??return?(
          ????<div?className="demo">
          ??????<button?onClick={()?=>?go('foo')}>foobutton>

          ??????<button?onClick={()?=>?go('bar')}>barbutton>
          ????div>
          ??);
          };

          export?default?()?=>?{
          ??return?(
          ????<div>
          ??????<Router>
          ????????<Links?/>
          ????????<Route?path="foo">
          ??????????<Foo?/>
          ????????Route>

          ????????<Route?path="bar">
          ??????????<Bar?/>
          ????????Route>
          ??????Router>
          ????div>
          ??);
          };

          結(jié)語

          通過本文的學(xué)習(xí),相信小伙伴們已經(jīng)搞清楚了前端路由的原理,其實它只是對瀏覽器提供 API 的一個封裝,以及在框架層去聯(lián)動做對應(yīng)的渲染,換個框架 vue-router 也是類似的原理。

          本文源碼地址:https://github.com/sl1673495/react-mini-router


          ??看完三件事

          如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:

          1. 點贊,讓更多的人也能看到介紹內(nèi)容(收藏不點贊,都是耍流氓-_-)
          2. 關(guān)注公眾號“前端勸退師”,不定期分享原創(chuàng)知識。
          3. 也看看其他文章

          勸退師個人微信:huab119

          也可以來我的GitHub博客里拿所有文章的源文件:

          前端勸退指南:https://github.com/roger-hiro/BlogFN一起玩耍呀

          在看、點贊、轉(zhuǎn)發(fā)支持作者??



          瀏覽 51
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  下载日本一级片 | 污视频在线观看免费 | 青青草免费在线公开视频 | 黄色视频在线观看完整版 | 亚洲中字视频 |