<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>

          前綴樹(shù)在前端路由系統(tǒng)中的應(yīng)用

          共 11721字,需瀏覽 24分鐘

           ·

          2022-11-15 01:17

          術(shù)  堅(jiān)  


          背景

          本人自己曾經(jīng)造輪子搞過(guò)一個(gè) Node.js 端的應(yīng)用層 Web 框架,里面涉及到一個(gè)路由系統(tǒng)的實(shí)現(xiàn),當(dāng)時(shí)是通過(guò)一個(gè)叫前綴樹(shù)的數(shù)據(jù)結(jié)構(gòu)來(lái)實(shí)現(xiàn)便捷的路由查找與匹配機(jī)制,這里跟大家分享一下。

          前綴樹(shù)介紹

          前綴樹(shù),即字典樹(shù),又稱(chēng) Trie 樹(shù)。這種數(shù)據(jù)結(jié)構(gòu)通常用來(lái)儲(chǔ)存字符串,并且是以路徑字符節(jié)點(diǎn)的形式來(lái)儲(chǔ)存。擁有公共前綴的字符串,會(huì)共享同樣的父節(jié)點(diǎn)路徑。前綴樹(shù)是通過(guò)利用字符串的公共前綴來(lái)降低查詢時(shí)間的開(kāi)銷(xiāo)以達(dá)到提高效率的目的。

          前綴樹(shù)的 3 個(gè)基本性質(zhì):

          • 根節(jié)點(diǎn)不包含字符,除根節(jié)點(diǎn)外每一個(gè)節(jié)點(diǎn)都只包含一個(gè)字符。
          • 從根節(jié)點(diǎn)到某一節(jié)點(diǎn),路徑上經(jīng)過(guò)的字符連接起來(lái),為該節(jié)點(diǎn)對(duì)應(yīng)的字符串。
          • 每個(gè)節(jié)點(diǎn)的所有子節(jié)點(diǎn)包含的字符都不相同。

          路由匹配的場(chǎng)景

          這個(gè)問(wèn)題是在做 Web 框架的路由時(shí)碰見(jiàn)的,所以這里的請(qǐng)求路徑的形式可以當(dāng)做就是一個(gè)個(gè) URL 刨去協(xié)議名、域名、端口號(hào)(有的話)之后的部分,一般是通過(guò)斜杠 ("/") 來(lái)鏈接一個(gè)個(gè)元素,形如:

          • /api/user/nameList
          • /api/user/addressList

          而且在大部分時(shí)候,路由的路徑是根據(jù)模塊的層級(jí)來(lái)劃分功能,以達(dá)到顧名思義的目的,因此路由的路徑其實(shí)是會(huì)有很多公共的前綴。比如,上述例子中同屬于一個(gè)二級(jí)模塊 User 的接口:

          /api/user/list/api/user/create ,它們便有共同的前綴 /api/user ,聯(lián)系到上述前綴樹(shù)的性質(zhì),我們便可以通過(guò)前綴樹(shù)來(lái)儲(chǔ)存和搜索這些路由信息。

          應(yīng)用到路由匹配中

          根據(jù)上面的一個(gè)結(jié)論,可以得到一個(gè)基本思路:

          把路由路徑當(dāng)做是用斜杠連接起來(lái)的 Component 的組合,因此前綴樹(shù)當(dāng)中的節(jié)點(diǎn),儲(chǔ)存的就不再是單個(gè)字符,而是一個(gè)個(gè) Component,但這不會(huì)影響我們?nèi)ナ褂眠@種數(shù)據(jù)結(jié)構(gòu)來(lái)進(jìn)行搜索。

          按照上面的描述,將這兩串路徑用 / 分割,形成一組 Component,同時(shí)它們擁有兩層的公共路徑,那么將會(huì)形成這樣的樹(shù)結(jié)構(gòu):(葉子節(jié)點(diǎn)儲(chǔ)存的是對(duì)應(yīng)的 handler)

          將路由聲明添加到 Trie 樹(shù)中

          下面是將路由聲明的 Component 添加到 Trie 樹(shù)中的代碼:

          router.get( '/api/user/nameList', xxx);

          addToTree(urlPattern: string, handler: any) {
              let p = this.root;
              // Padding an element to the rear of the array to make the leaf node.
              const urlPatternComponents = [...urlPattern.split('/').filter(Boolean), LEAF_SIGN];

              urlPatternComponents.forEach(component => {
                const { quickMap } = p;

                // If quickMap has this component, it means the route has the same namespace
                // with existed route, so get to the next level directly. If the node is a leaf
                // node, just return cause it means redundant route is adding to the tree, we dont need it.
                if (p.quickMap.has(component as string)) {
                  const node = p.quickMap.get(component as string)!;
                  if (isLeafNode(node)) {
                    return;
                  }
                  p = node;
                  return;
                }

                if (component === LEAF_SIGN) {
                  const newNode = new RouterTreeLeafNode(handler);
                  quickMap.set(LEAF_SIGN, newNode);
                  return;
                }

                const newNode = new NTreeNode(component as string);
                p.quickMap.set(component as string, newNode);
                // When the expression like ':id' shows in the route, it should
                // treat it as a parameter node.One tree node can only have one parameter node.
                if ((component as string).indexOf(':') > -1) {
                  p.paramNode = newNode;
                }
                p = newNode;
              });
            }

          router.get(' /api/hi/:name'

          這里用一個(gè) quickMap 來(lái)儲(chǔ)存子節(jié)點(diǎn),keyComponentvalue 為節(jié)點(diǎn),用于在匹配過(guò)程中快速查找到與 Component 值相匹配的節(jié)點(diǎn)。注意在 urlComponents 數(shù)組末尾填充了一個(gè)叫 LEAF_SIGNSymbol,看上面的樹(shù)結(jié)構(gòu)圖就知道,實(shí)際路由聲明的 Component 遍歷完之后,葉子節(jié)點(diǎn)的值儲(chǔ)存的是最后一個(gè) Component,因此我們需要給它添加一個(gè)子節(jié)點(diǎn),用來(lái)儲(chǔ)存實(shí)際匹配的結(jié)果,也就是路由的 HandlerparamNode 放到動(dòng)態(tài)路由匹配一節(jié)再解析,這里可以先不管。

          靜態(tài)路由匹配

          在匹配時(shí),也實(shí)際的請(qǐng)求路徑同樣按照上面的分割方式切分成一組 Component,從 Trie 的根節(jié)點(diǎn)開(kāi)始,它的子節(jié)點(diǎn)必定只有一個(gè),將指針指向它的唯一子節(jié)點(diǎn),并將遍歷 Component 的指針往后挪,根據(jù)遍歷到的新的 Component 去匹配下一層的子節(jié)點(diǎn)。直到 Component 被遍歷完,若最后可以找到相匹配的子節(jié)點(diǎn),則該節(jié)點(diǎn)為葉子節(jié)點(diǎn),將其值取出作為結(jié)果返回。若未能匹配,在靜態(tài)路由匹配的情況中就是 Route not found 的情況了,但是實(shí)際場(chǎng)景肯定沒(méi)有這么簡(jiǎn)單粗暴,這里先留個(gè)坑,后面會(huì)講到。

          代碼如下:

          getHandlerFromTree(url: string): any{
              const [urlWithParams, _] = url.split('?');
              const urlComponents = urlWithParams.split('/').filter(Boolean);
              let p = this.root;
              let i = 0;
              let res;
              let path = '';
              while (p) {
                const component = urlComponents[i ++];

                // If the quickMap has the component, return it if it's also a leaf node.
                // Or just move to the next level and store the path.
                if (p.quickMap.has(component)) {
                  const node = p.quickMap.get(component)!;
                  if (isLeafNode(node)) {
                    res = node.value;
                    break;
                  }
                  path += '
          /' + node.value;
                  p = node;
                  continue;
                }

                const leafNode = p.quickMap.get(LEAF_SIGN);

                if (leafNode == null) {
                  // If quickMap has other node, it means static route cannot be matched.
                  if (p.quickMap.size > 0) {
                    const err = { message: '
          Route not defined', statusCode: 404, statusMessage: 'Not found' };
                    throw err;
                  }
                  
                  // Else it means no handler was defined.
                  const err = { message: '
          Handler not defined', statusCode: 500, statusMessage: 'Not found' };
                  throw err;
                }

                res = leafNode.value;
                break;
              }

              return {
                handler: res,
                path
              };
            }

          動(dòng)態(tài)路由匹配

          我們?cè)谑褂?Express 或是 Koa.js 時(shí),會(huì)用到形如 /api/user/:id 這樣的動(dòng)態(tài)路由聲明,這是一個(gè)實(shí)用的功能,來(lái)看下如何在基于 Trie 樹(shù)的路由系統(tǒng)中實(shí)現(xiàn)動(dòng)態(tài)路由。

          還是剛才的兩條路由聲明,現(xiàn)在我們加上一條新的:

          /api/user/nameList

          /api/user/addressList

          /api/user/:id

          得到這樣的結(jié)構(gòu):

          對(duì)這樣的動(dòng)態(tài)參數(shù)路由聲明,將其作為一個(gè)特殊節(jié)點(diǎn),用一個(gè)單獨(dú)的指針 paramNode 保存,我們限制一種路由聲明只能有一個(gè)動(dòng)態(tài)參數(shù),就是不能既有 /api/user/:id 又有 /api/user/:name,這樣的話在實(shí)際匹配時(shí)無(wú)法得知,路徑中對(duì)應(yīng)位置的 Component 代表的是什么含義。

          接上面的坑,實(shí)際匹配時(shí),當(dāng)碰見(jiàn)無(wú)法匹配的 Component 時(shí),那么代表這個(gè) Component 是一個(gè)動(dòng)態(tài)參數(shù)的實(shí)際值,所以無(wú)法跟任何靜態(tài)聲明匹配,這時(shí)就直接去找該節(jié)點(diǎn)的 paramNode 指針指向的節(jié)點(diǎn),也就是說(shuō)當(dāng)碰到這種情況時(shí),我們直接把它歸類(lèi)為動(dòng)態(tài)參數(shù)匹配的場(chǎng)景。

          看一下加入動(dòng)態(tài)參數(shù)匹配的代碼,省略共同部分:

          getHandlerFromTree(url: string): any {
              // ...
              if (p.quickMap.has(component)) {
                  const node = p.quickMap.get(component)!;
                  if (isLeafNode(node)) {
                    res = node.value;
                    break;
                  }
                  path += '/' + node.value;
                  p = node;
                  continue;
              }
              if (component) {
                  path += '/' + p.paramNode.value;
                  p = p.paramNode;
                  continue;
              }

              const leafNode = p.quickMap.get(LEAF_SIGN);
              // ...
          }

          那么這時(shí)又有一個(gè)坑,如果 paramNode 不存在,該怎么辦?下面來(lái)說(shuō)下一種場(chǎng)景:正則表達(dá)式匹配。

          正則表達(dá)式匹配

          router.get( /hi/all/ , xxx)

          因?yàn)檎齽t表達(dá)式是可以直接進(jìn)行字符串匹配的,所以這種路由聲明將會(huì)脫離 Trie 樹(shù)的數(shù)據(jù)結(jié)構(gòu)特點(diǎn)而存在,我們選擇將這種節(jié)點(diǎn)全部?jī)?chǔ)存在根結(jié)點(diǎn)下方,避免不必要的查找。上面說(shuō)到,如果一個(gè)節(jié)點(diǎn)的 paramNode 指針不存在,那么我們只好做最后一種選擇,將整個(gè)路徑進(jìn)行正則表達(dá)式匹配,如果仍然無(wú)法匹配,就只好拋出路由未找到的異常,由依賴(lài)路由的代碼去處理這個(gè)異常。

          getHandlerFromTree(url: string) {
              // ...
              if (component) {
                  // If no parameter node found, try regular expression matching.
                  if (!p. paramNode ) {
           const { handler, matched } = this . getHandlerFromRegExpNode (url);
          res = handler
          path = matched;
           break ;
          }
                  path += '/' + p.paramNode.value;
                  p = p.paramNode;
                  continue;
              }

              const leafNode = p.quickMap.get(LEAF_SIGN);
              // ...
          }

          添加正則表達(dá)式節(jié)點(diǎn)到樹(shù)中:

          addRegExpToTree(urlPattern: RegExp, handler: Function) {
              const root = this.root;

              root.children.push(new RouterRegExpLeafNode(urlPattern, handler));
          }

          得到的結(jié)構(gòu)是這樣的:

          這樣就實(shí)現(xiàn)了一個(gè)支持靜態(tài)、動(dòng)態(tài)、正則表達(dá)式三種匹配方式的路由機(jī)制。

          簡(jiǎn)單分析

          使用 Trie 樹(shù)來(lái)做路由匹配就是比較折中的方案,通常來(lái)說(shuō)路由聲明都會(huì)按照模塊來(lái)做分類(lèi),在同一個(gè)一級(jí)模塊下面的多個(gè)二級(jí)模塊路由,然后每個(gè)二級(jí)模塊下面會(huì)有多個(gè)三級(jí)模塊路由,就會(huì)產(chǎn)生公共前綴,就給了 Trie 樹(shù)節(jié)省空間的機(jī)會(huì),并且重復(fù)率越高節(jié)省的空間越多(聽(tīng)起來(lái)怎么像 gzip 壓縮),Trie 樹(shù)的最壞查找效率取決于所儲(chǔ)存的序列的最長(zhǎng)長(zhǎng)度,也就是樹(shù)的最大深度,是一個(gè)線性級(jí)別的時(shí)間復(fù)雜度。這里會(huì)有另一個(gè)問(wèn)題,如果所有路由聲明都是分散的,沒(méi)有公共前綴,假設(shè)有 m 條長(zhǎng)度為 n 的記錄,在彼此沒(méi)有公共前綴時(shí),Trie 樹(shù)的空間復(fù)雜度會(huì)達(dá)到 O(mn),故在使用時(shí)應(yīng)盡量收斂路由聲明的 namespace 數(shù)量。

          總結(jié)

          一開(kāi)始內(nèi)心有個(gè)問(wèn)題,為什么不直接用哈希表儲(chǔ)存靜態(tài)路由,然后動(dòng)態(tài)路由就使用遍歷查找的方式?后面對(duì)這個(gè)問(wèn)題有了一些自己的理解:

          使用哈希表儲(chǔ)存靜態(tài)路由,查找速度是常數(shù)級(jí)別的,非常快,但是需要線性空間來(lái)儲(chǔ)存,而且每一條靜態(tài)路由都需要完整儲(chǔ)存,那么就浪費(fèi)了公共前綴這個(gè)特性。其次,對(duì)動(dòng)態(tài)路由使用遍歷匹配的方式太過(guò)暴力,動(dòng)態(tài)路由沒(méi)有辦法去很好地做緩存,而路由匹配是個(gè)高頻的動(dòng)作,這種方式的性能開(kāi)銷(xiāo)相對(duì)來(lái)說(shuō)比較大,也是不夠合適的。

          代碼倉(cāng)庫(kù)

          https://github.com/divasatanica/auf/blob/main/packages/core/src/router/prefix-tree/index.ts

          有興趣的小伙伴可以去瞅瞅。

          ?? 謝謝支持

          以上便是本次分享的全部?jī)?nèi)容,希望對(duì)你有所幫助^_^

          喜歡的話別忘了 分享、點(diǎn)贊、收藏 三連哦~。

          歡迎關(guān)注公眾號(hào) 趣談前端 收貨大廠一手好文章~


          從零搭建全棧可視化大屏制作平臺(tái)V6.Dooring

          從零設(shè)計(jì)可視化大屏搭建引擎

          Dooring可視化搭建平臺(tái)數(shù)據(jù)源設(shè)計(jì)剖析

          可視化搭建的一些思考和實(shí)踐

          基于Koa + React + TS從零開(kāi)發(fā)全棧文檔編輯器(進(jìn)階實(shí)戰(zhàn)




          點(diǎn)個(gè)在看你最好看


          瀏覽 78
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  天天爱天天干天天色 | 国产乱码操逼片 | 日韩va在线观看 日韩成人免费大片 | 亚洲第一福利在线久 | 学生妹做爱在线播放 |