單頁面路由即在前端單頁面實現的一種路由,由於React,Vue等框架的火熱,我們可以很容易構建一個使用者體驗良好的單頁面應用,但是如果我們要在瀏覽器改變路由的時候,在不請求伺服器的情況下渲染不同的內容,就要類似於後端的路由系統,在前端也實現一套完整的路由系統
下面讓我們來實現一個簡單的路由系統。該路由系統將基於React進行書寫。在寫之前,我們先仔細想下,我們應該從哪方面入手。這是最終實現的效果simple-react-router-demo
功能思考
不論是前端還是後端路由,我們都可以通過一種路由匹配加匹配後回撥的方式來實現。如果沒有理解也沒有關係,後面會理解。
我們用一個物件Router
來表示我們的Router
物件。先來想下我們要做哪些工作
- 配置路由模式
history
和hash
- 新增和刪除路由
- 監聽路由變化並呼叫對應路由回撥
- 暴露路由跳轉函式
實現路由核心部分
首先我們來實現我們的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'
和history
api存在的時候我們設定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實現上來,根據我們一開始列的功能,我們還要實現路由改變監聽事件,由於我們有兩種路由模式history
和hash
,因此要進行判斷。
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下就更好了♪(・ω・)ノ,可能有部分理解錯誤或書寫錯誤的地方,歡迎指正。