單頁面路由原理及實現

單頁面路由即在前端單頁面實現的一種路由,因爲React,Vue等框架的火熱,咱們能夠很容易構建一個用戶體驗良好的單頁面應用,可是若是咱們要在瀏覽器改變路由的時候,在不請求服務器的狀況下渲染不一樣的內容,就要相似於後端的路由系統,在前端也實現一套完整的路由系統前端

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

功能思考

不管是前端仍是後端路由,咱們均可以經過一種路由匹配加匹配後回調的方式來實現。若是沒有理解也沒有關係,後面會理解。 咱們用一個對象Router來表示咱們的Router對象。先來想下咱們要作哪些工做git

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

實現路由核心部分

首先咱們來實現咱們的router-core.jsgithub

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是爲了實現鏈式調用,除了工具函數,後面其餘函數也會保持這種寫法。api

配置完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/的寫法。咱們將尾部的/去掉。該函數實現以下react-router

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下就更好了♪(・ω・)ノ,可能有部分理解錯誤或書寫錯誤的地方,歡迎指正。

參考連接

相關文章
相關標籤/搜索