單頁面路由原理及實現


單頁面路由原理及實現

單頁面路由即在前端單頁面實現的一種路由,由於React,Vue等框架的火熱,我們可以很容易構建一個用戶體驗良好的單頁面應用,但是如果我們要在瀏覽器改變路由的時候,在不請求服務器的情況下渲染不同的內容,就要類似於后端的路由系統,在前端也實現一套完整的路由系統

下面讓我們來實現一個簡單的路由系統。該路由系統將基於React進行書寫。在寫之前,我們先仔細想下,我們應該從哪方面入手。這是最終實現的效果simple-react-router-demo

功能思考

不論是前端還是后端路由,我們都可以通過一種路由匹配加匹配后回調的方式來實現。如果沒有理解也沒有關系,后面會理解。 我們用一個對象Router來表示我們的Router對象。先來想下我們要做哪些工作

  1. 配置路由模式historyhash
  2. 添加和刪除路由
  3. 監聽路由變化並調用對應路由回調
  4. 暴露路由跳轉函數

實現路由核心部分

首先我們來實現我們的router-core.js

const Router = { routes: [], // 用來存放注冊過的路由 mode: null, // 用來標識路由模式 } 復制代碼

我們用兩個屬性來存放我們需要存儲的路由和路由模式

下面在剛才的基礎上添加一個config函數,讓我們的路由能夠配置

const Router = { // ... routes mode config: (options) => { Router.mode = options && options.mode && options.mode === 'history' && !!history.pushState ? 'history' : 'hash'; return Router; } } 復制代碼

config函數中我們通過傳入的配置,來配置我們的路由模式,當且僅當options.mode === 'history'historyapi存在的時候我們設置Router模式為history。返回Router是為了實現鏈式調用,除了工具函數,后面其他函數也會保持這種寫法。

配置完Router模式后,我們要能夠添加和刪除路由

const Router = { // ... routes mode config add: (pathname, handler) => { Router.routes.push({ pathname: clearEndSlash(pathname), handler }); return Router; }, remove: (pathname) => { Router.routes.forEach((route, index) => { if (route.pathname === clearEndSlash(pathname)) { Router.routes.splice(index, 1); } }); return Router; } } 復制代碼

在添加和刪除路由函數中,我們傳入了名為pathname的變量,為了跟后面普通的path區分開。直白點來說,就是pathname對應/person/:id的寫法,path對應/person/2的寫法。

這里有個clearEndSlash函數,是為了防止有/home/的寫法。我們將尾部的/去掉。該函數實現如下

const clearEndSlash = (path = '') => path.toString().replace(/\/$/, '') || '/'; 復制代碼

為了方便比較,在完成添加和刪除后我們來實現一個match函數,來確定pathname是否和path相匹配。

const Router = { // ... routes mode config add remove match: (pathname, path) => { const reg = pathToRegexp(pathname); return reg.test(path); } } 復制代碼

這里使用pathToRegexp函數將pathname解析成正則表達式,然后用該正則表達式來判斷時候和path匹配。pathToRegexp的實現如下

const pathToRegexp = (pathname, keys = []) => { const regStr = '^' + pathname.replace(/\/:([^\/]+)/g, (_, name) => { keys.push({ name }); return '/([^/]+)'; }); return new RegExp(regStr); } 復制代碼

函數返回解析后的正則表達式,其中keys參數用來記錄參數name。舉個例子

// 調用pathToRegexp函數 const keys = []; const reg = pathToRegexp('/person/:id/:name', keys); console.log(reg, keys); // reg: /^\/person\/([^\/]+)\/([^\/]+)/ // keys: [ { name: 'id' }, { name: 'name' } ] 復制代碼

好像有點扯遠了,回到我們的Router實現上來,根據我們一開始列的功能,我們還要實現路由改變監聽事件,由於我們有兩種路由模式historyhash,因此要進行判斷。

const Router = { // ... routes mode config add remove match current: () => { if (Router.mode === 'history') { return location.pathname; } return location.hash; }, listen: () => { let currentPath = Router.current(); const fn = () => { const nextPath = Router.current(); if (nextPath !== currentPath) { currentPath = nextPath; const routes = Router.routes.filter(route => Router.match(route.pathname, currentPath)); routes.forEach(route => route.handler(currentPath)); } } clearInterval(Router.interval); Router.interval = setInterval(fn, 50); return Router; } } 復制代碼

路由改變事件監聽,在使用History API的時候可以使用popstate事件進行監聽,Hash改變可以使用hashchnage事件進行監聽,但是由於在不同瀏覽器上有些許不同和兼容性問題,為了方便我們使用setInterval來進行監聽,每隔50ms我們就來判斷一次。

最后我們需要實現一個跳轉函數。

const Router = { // ... routes mode config add remove match current listen navigate: path => { if (Router.mode === 'history') { history.pushState(null, null, path); } else { window.location.href = window.location.href.replace(/#(.*)$/, '') + path; } } return Router; } 復制代碼

但這里我們基本已經完成了我們路由的核心部分。

下一節,我們將在該核心部分的基礎上,實現幾個路由組件,以達到react-router的部分效果。

這是原文地址,該部分的完整代碼可以在我的Github的simple-react-router上看到,如果你喜歡,能順手star下就更好了♪(・ω・)ノ,可能有部分理解錯誤或書寫錯誤的地方,歡迎指正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM