基於react的hash路由簡易實現

拉風小牡丹發表於2019-04-09

基於react的hash路由簡易實現

背景介紹

在SPA專案中,如果展示頁面繁多,根據前端工程化理論中元件化思想,我們通常會將不同的頁面,按照功能,職責等進行劃分。 這個時候,每個頁面,就需要一個相應的路由去對應。 現在react社群中已經有react-router這個成熟的輪子,我們可以直接引入並使用。 但具體hash路由是如何實現的,我們現在就來探討一下...

開始

首先我們要了解一下,監聽路由改變,其實是監聽2個事件並操作回撥,進行渲染:

  1. 頁面載入事件(window.onload)
  2. 頁面url中hash部分改變事件(window.onhashchange)

原生html程式碼編寫

<!doctype html>
<html>
	<head>
		<title>hash-router</title>
	</head>
	<body>
		<ul>
			<li><a href="#/">home</a></li>
			<li><a href="#/login">
					login
					<ul>
						<li><a href="#/login/login1">login1</a></li>
						<li><a href="#/login/login2">login2</a></li>
						<li><a href="#/login/login3">login3</a></li>
					</ul>
				</a></li>
			<li><a href="#/abort">abort</a></li>
		</ul>
		<div id = "content"></div>
	</body>
</html>
複製程式碼

在這裡 我們指定了一系列路由,包括"#/","#/login"等。然後,我們就要針對頁面onload事件和點選每個a標籤之後hash改變事件來進行處理了。

原生js程式碼編寫

建立一個HashRouter類

<script type = 'text/javascript'>
	"use strict"
	class HashRouter{
		constructor(){
			this.routers = {},
			this.init();
		}
	}
</script>
複製程式碼
  1. 在這段程式碼中 我們建立了一個HashRouter類,並且指定了一個routers空物件來儲存路由資訊。
  2. 將來在這個routers物件中,我們儲存路由的名稱及回撥,如routers = { "#/login" : [ callback1, callback2 ] }。鍵名為hash路由名稱,鍵值為陣列,陣列內為跳轉到該路由所要執行的回撥方法。
  3. 現在,我們來執行init方法,當HashRouter類被建立的時候,我們需要執行的一些初始化方法

當HashRouter類被建立時,執行的初始化方法

<script type = 'text/javascript'>
	"use strict"
	class HashRouter{
		constructor(){
			this.routers = {},
			this.init();
		}
		trigger(){
			//取出當前url中的hash部分,並過濾掉引數
			let hash = window.location.hash && window.location.hash.split('?')[0];
			//在routers中,找到相應的hash,並執行已儲存在其中的回撥方法
			if(this.routers[hash] && this.routers[hash].length > 0){
				for(let i = 0 ; i < this.routers[hash].length ; i++){
					this.routers[hash][i]();
				}	
			}
		}
		init(){
			window.addEventListener('load', () => this.trigger(), false);
			window.addEventListener('hashchange', () => this.trigger(), false);
		}
	}
</script>
複製程式碼

兼聽了頁面載入時和hash改變時事件,並針對改變事件做回撥處理,即trigger方法

需要丟擲api,讓開發者新增監聽事件回撥

在上一步我們進行了初始化,監聽了頁面hash改變的事件並做相應的處理。但是,我們需要執行的回撥方法,需要開發人員手動新增才行。

<script type = 'text/javascript'>
	"use strict"
	class HashRouter{
		constructor(){
			this.routers = {},
			this.init();
		}
		listen(path, callback){
			//如果routers中已經存在該hash,則為它pushcallback方法,否則新建一個相應陣列,並push回撥方法
			if(!this.routers[path]){
				this.routers[path] = [];	
			}
			this.routers[path].push(callback);
		}
		trigger(){
			//取出當前url中的hash部分,並過濾掉引數
			let hash = window.location.hash && window.location.hash.split('?')[0];
			//在routers中,找到相應的hash,並執行已儲存在其中的回撥方法
			if(this.routers[hash] && this.routers[hash].length > 0){
				for(let i = 0 ; i < this.routers[hash].length ; i++){
					this.routers[hash][i]();
				}	
			}
		}
		init(){
			window.addEventListener('load', () => this.trigger(), false);
			window.addEventListener('hashchange', () => this.trigger(), false);
		}
	}
</script>
複製程式碼

原生程式碼實操

<!doctype html>
<html>
	<head>
		<title>hash-router</title>
	</head>
	<body>
		<ul>
			<li><a href="#/">home</a></li>
			<li><a href="#/login">
					login
					<ul>
						<li><a href="#/login/login1">login1</a></li>
						<li><a href="#/login/login2">login2</a></li>
						<li><a href="#/login/login3">login3</a></li>
					</ul>
				</a></li>
			<li><a href="#/abort">abort</a></li>
		</ul>
		<div id = "content"></div>
	</body>
	<script type = 'text/javascript'>
		"use strict"
		let getById = (id) => document.getElementById(id);
	</script>
	<script type = 'text/javascript'>
		"use strict"
		class HashRouter{
			constructor(){
				this.routers = {},
				this.init();
			}
			listen(path, callback){
				//如果routers中已經存在該hash,則為它push回撥方法,否則新建一個相應陣列,並push回撥方法
				if(!this.routers[path]){
					this.routers[path] = [];	
				}
				this.routers[path].push(callback);
			}
			trigger(){
				//取出當前url中的hash部分,並過濾掉引數
				let hash = window.location.hash && window.location.hash.split('?')[0];
				//在routers中,找到相應的hash,並執行已儲存在其中的回撥方法
				if(this.routers[hash] && this.routers[hash].length > 0){
					for(let i = 0 ; i < this.routers[hash].length ; i++){
						this.routers[hash][i]();
					}	
				}
			}
			init(){
				window.addEventListener('load', () => this.trigger(), false);
				window.addEventListener('hashchange', () => this.trigger(), false);
			}
		}
	</script>
	<script type = 'text/javascript'>
		"use strict"
		let router = new HashRouter();
		router.listen('#/',() => { getById('content').innerHTML = 'home' });
		router.listen('#/login',() => { console.info('login-') });
		router.listen('#/login',() => { console.info('login+') });
		router.listen('#/login',() => { getById('content').innerHTML = 'login' });
		router.listen('#/login/login1',() => { getById('content').innerHTML = 'login1' });
		router.listen('#/login/login2',() => { getById('content').innerHTML = 'login2' });
		router.listen('#/login/login3',() => { getById('content').innerHTML = 'login3' });
		router.listen('#/abort',() => { getById('content').innerHTML = 'abort' });
	</script>
</html>
複製程式碼

可以看到,在頁面點選時,url改變,並執行了我們根據執行路徑所註冊的回撥方法。此時,一個簡易原生的hash-router我們便實現了

對原生程式碼進行react封裝

由於es6模組化橫空出世,我們可以封裝儲存變數,丟擲方法,操作內部變數,不會變數汙染。 如果放在以前,我們需要通過閉包來實現類似功能

實際開發人員需要編寫的業務程式碼部分(檔案引用路徑根據個人專案不同可以修改)

import React from 'react';
import ReactDOM from 'react-dom';
import { Router , Route , dispatchRouter , listenAll , listenPath , listenAll } from './EasyRouter';
import MainLayout from './main-layout/MainLayout';
import Menu from './page/menu/Menu';
import Login from './page/login/Login';
import Abort from './page/abort/Abort';

const Routers = [
	{ path : '#/login' , menu : 'login' , component : () => Login({ bread : '#/abort' }) },
	{ path : '#/abort' , menu : 'abort' , component : <Abort bread = { '#/login' }/> },
]

export default function RouterPage(){
	return(
		<Router>
			<Menu routers = { Routers }/>
			<MainLayout>
				{ Routers && Routers.map((item, index) => (<Route path = { item.path } key = { index } component = { item.component }/>)) }
			</MainLayout>
		</Router>
	)
}

ReactDOM.render(<EasyRouter/>, document.getElementById('easy-router'));
複製程式碼

下面我們來逐個分析{ Router , Route , dispatchRouter , listenAll , listenPath }中的每一個作用

Router

其實是個很簡單的方法,相當於包裝了一層div(未做容錯處理,意思一下)

export function Router(props){
	let { className , style , children } = props;
	return(
		<div className = { className } style = { style }>
			{ children }
		</div>
	)
}
Router.defaultProps = {
	className : '',
	style : {},
	children : []
}
複製程式碼

Route

這個算是路由元件中的核心元件了,我們需要在這個方法中對監聽進行封裝

//路由元件定義
const Routers = [
	{ path : '#/login' , menu : 'login' , component : () => Login({ bread : '#/abort' }) },
	{ path : '#/abort' , menu : 'abort' , component : <Abort bread = { '#/login' }/> },
]

//return方法中Route的應用
<MainLayout>
	{ Routers && Routers.map((item, index) => (<Route path = { item.path } component = { item.component }/>)) }
</MainLayout>
複製程式碼

注意,在我們寫Route標籤並傳參的同時,其實path和component已經註冊到變數中儲存下來了,當路徑條件成立時,就會渲染已經相應存在的component(這裡的component可以是ReactDOM或者function,具體參見Routers陣列中定義)。話不多說,show me code:

export class Route extends React.Component{
	constructor(props){
		super(props);
		this.state = {
			renderItem : [],            //需要渲染的內容
		}		
	}
	componentDidMount(){
		this.initRouter();
		window.addEventListener('load', () => this.changeReturn());
		window.addEventListener('hashchange', () => this.changeReturn());	
	}
	
	initRouter(){
		let { path , component } = this.props;
		//保證相同的路由只有一個元件 不能重複
		if(routers[path]){
			throw new Error(`router error:${path} has existed`);
		}else{
			routers[path] = component;	
		}
	}
	
	changeReturn(){
		//防止url中有引數干擾監聽
		let hash = window.location.hash.split('?')[0];
		let { path } = this.props;
		//當前路由是選中路由時載入當前元件
		if(hash === path && routers[hash]){
			let renderItem;
			//如果元件引數的方法 則執行並push
			//如果元件引數是ReactDOM 則直接渲染
			if(typeof routers[hash] === 'function'){
				renderItem = (routers[hash]())	
			}else{
				renderItem = (routers[hash])		
			}
			//當前路由是選中路由 渲染元件並執行回撥
			this.setState({ renderItem }, () => callListen(hash));
		}else{
			//當前路由非選中路由 清空當前元件
			this.setState({ renderItem : [] });
		}		
	}
	
	render(){
		let { renderItem } = this.state;
		return(
		 	<React.Fragment>
				{ renderItem }
			</React.Fragment>
		)
	}
}
複製程式碼

listenPath和listenAll

這兩個方法就是開發者需要新增的監聽方法。我們先來介紹如何使用,通過使用方法,再進行實現。

listenPath('#/login', () => {
	console.info('listenPath login1')
})

listenAll((pathname) => {
	if(pathname === '#/login'){
		console.info('listenAll login')
	}
})
複製程式碼
  1. listenPath方法,有兩個引數,第一個是指定路由,第二個是該路由執行回撥
  2. listenAll方法,只有一個方法引數,但是這個方法會將當前hash路徑pathname返回,我們可以根據pathname來進行處理
  3. 這兩種方法只是在表現上做了區別,本質還是一樣的
  4. 注意this.setState({ renderItem }, () => callListen(hash)),頁面路由為該元件指定path的時候,需要執行回撥

具體實現:

/**
 * 路由監聽事件物件,分為2部分
 * all array 存listenAll監聽方法中註冊的陣列
 * path array 存listenPath監聽方法中註冊的hash路徑名稱和相應的回撥方法
 * { all : [callback1, callback2] , path : [{ path: path1, callback : callback1 }, { path : path2, callback : callback2 }] }
 */
let listenEvents = {};	

/**
 * 執行回撥
 * 將listenEvents中的all陣列中的方法全部執行
 * 遍歷listenEvents中的path,找出與當前hash對應的path並執行callback(可能存在多個path相同的情況,因為開發人員可以多次註冊)
 */
function callListen(path){
	if(listenEvents && listenEvents.all && listenEvents.all.length > 0){
		let listenArr = listenEvents.all;
		for(let i = 0 ; i < listenArr.length ; i++){
			listenArr[i](path);
		}	
	}
	if(listenEvents && listenEvents.path && listenEvents.path.length > 0){
		let listenArr = listenEvents.path;
		for(let i = 0 ; i < listenArr.length ; i++){
			if(path === listenArr[i].path){
				listenArr[i].callback();	
			}
		}
	}
}

/**
 * 監聽路由並觸發回撥事件
 * @params
 * path string 需要監聽的路由
 * callback function 需要執行的回撥
 */
export function listenPath(path, callback){
	if(!listenEvents.path){
		listenEvents.path = [];		
	}		
	listenEvents.path.push({ path, callback });
}

/**
 * 監聽路由改變並觸發所有回撥事件(會將當前路由傳出)
 * @params
 * callback function 需要執行的回撥
 */
export function listenAll(callback){
	if(!listenEvents.all){
		listenEvents.all = [];	
	}
	listenEvents.all.push(callback);
}
複製程式碼

dispatchRouter

簡單的一個路由跳轉方法,沒怎麼深思,往上應該有更大佬的方法,這裡就是意思一下

//路由跳轉
export function dispatchRouter({ path = '' , query = {} }){
	let queryStr = [];
	for(let i in query){
		queryStr.push(`${i}=${query[i]}`);
	}
	window.location.href = `${path}?${queryStr.join('&')}`;
}
複製程式碼

成品展示

Router元件

import React from 'react';

//當前路由元件儲存物件{ path : [ component1, component2 ] }
let routers = {};

/**
 * 路由監聽事件物件,分為2部分
 * all array 存listenAll監聽方法中註冊的陣列
 * path array 存listenPath監聽方法中註冊的hash路徑名稱和相應的回撥方法
 * { all : [callback1, callback2] , path : [{ path: path1, callback : callback1 }, { path : path2, callback : callback2 }] }
 */
let listenEvents = {};	

export function Router(props){
	let { className , style , children } = props;
	return(
		<div className = { className } style = { style }>
			{ children }
		</div>
	)
}

Router.defaultProps = {
	className : '',
	style : {},
	children : []
}

/*
 * 執行所有的路由事件
 * @parmas
 * path string 當前的hash路徑
 */
function callListen(path){
	if(listenEvents && listenEvents.all && listenEvents.all.length > 0){
		let listenArr = listenEvents.all;
		for(let i = 0 ; i < listenArr.length ; i++){
			listenArr[i](path);
		}	
	}
	if(listenEvents && listenEvents.path && listenEvents.path.length > 0){
		let listenArr = listenEvents.path;
		for(let i = 0 ; i < listenArr.length ; i++){
			if(path === listenArr[i].path){
				listenArr[i].callback();	
			}
		}
	}
}

//路由監聽路由並載入相應元件
export class Route extends React.Component{
	constructor(props){
		super(props);
		this.state = {
			renderItem : [],				//需要渲染的內容
		}		
	}
	componentDidMount(){
		this.initRouter();
		window.addEventListener('load', () => this.changeReturn());
		window.addEventListener('hashchange', () => this.changeReturn());	
	}
	
	initRouter(){
		let { path , component } = this.props;
		//保證相同的路由只有一個元件 不能重複
		if(routers[path]){
			throw new Error(`router error:${path} has existed`);
		}else{
			routers[path] = component;	
		}
	}
	
	changeReturn(){
		//防止url中有引數干擾監聽
		let hash = window.location.hash.split('?')[0];
		let { path } = this.props;
		//當前路由是選中路由時載入當前元件
		if(hash === path && routers[hash]){
			let renderItem;
			//如果元件引數的方法 則執行並push
			//如果元件引數是ReactDOM 則直接渲染
			if(typeof routers[hash] === 'function'){
				renderItem = (routers[hash]())	
			}else{
				renderItem = (routers[hash])		
			}
			//當前路由是選中路由 渲染元件並執行回撥
			this.setState({ renderItem }, () => callListen(hash));
		}else{
			//當前路由非選中路由 清空當前元件
			this.setState({ renderItem : [] });
		}		
	}
	
	render(){
		let { renderItem } = this.state;
		return(
		 	<React.Fragment>
				{ renderItem }
			</React.Fragment>
		)
	}
}

//路由跳轉
export function dispatchRouter({ path = '' , query = {} }){
	let queryStr = [];
	for(let i in query){
		queryStr.push(`${i}=${query[i]}`);
	}
	window.location.href = `${path}?${queryStr.join('&')}`;
}

/**
 * 監聽路由並觸發回撥事件
 * @params
 * path string 需要監聽的路由
 * callback function 需要執行的回撥
 */
export function listenPath(path, callback){
	if(!listenEvents.path){
		listenEvents.path = [];		
	}		
	listenEvents.path.push({ path, callback });
}

/**
 * 監聽路由改變並觸發所有回撥事件(會將當前路由傳出)
 * @params
 * callback function 需要執行的回撥
 */
export function listenAll(callback){
	if(!listenEvents.all){
		listenEvents.all = [];	
	}
	listenEvents.all.push(callback);
}
複製程式碼

業務程式碼

程式碼結構

這裡簡單寫了個結構

基於react的hash路由簡易實現

index.html

<!doctype html>
<html>
	<head>
		<title>簡易路由</title>	
	</head>
	<body>
		<div id = 'easy-router'></div>
	</body>
</html>
複製程式碼

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Router , Route } from './component/router/router';
import MainLayout from './page/main-layout/MainLayout';
import Menu from './page/menu/Menu';
import Login from './page/login/Login';
import Abort from './page/abort/Abort';

const Routers = [
	{ path : '#/login' , menu : 'login' , component : () => Login({ bread : '#/abort' }) },
	{ path : '#/abort' , menu : 'abort' , component : <Abort bread = { '#/login' }/> },
]

class EasyRouter extends React.Component{
	render() {
        return(
			<Router>
				<Menu routers = { Routers }/>
				<MainLayout>
					{ Routers && Routers.map((item, index) => (<Route path = { item.path } component = { item.component }/>)) }
				</MainLayout>
			</Router>
		)
    }
}

ReactDOM.render(<EasyRouter/>, document.getElementById('easy-router'));
複製程式碼

MainLayout.js

import React from 'react';

export default function MainLayout({ children }){
	return(
		<div>
			{ children }
		</div>
	)
}
複製程式碼

Menu.js

import React from 'react';

export default function Menu({ routers }){
	return(
		<div>
			{ routers && routers.map((item, index) => {
				let { path , menu , component } = item;
				return(
					<div key = { path } onClick = {() => { window.location.href = path }}><a>{ menu }</a></div>
				)
			}) }
		</div>
	)
}
複製程式碼

Login.js

import React from 'react';
import { listenAll , listenPath } from '../../component/router/router';

listenPath('#/login', () => {
	console.info('listenPath login1')
})

listenAll((pathname) => {
	if(pathname === '#/login'){
		console.info('listenAll login')
	}
})

export default function Login(props = {}){
	let { bread } = props;
	return (
		<div style = {{ width : '100%' , height : '100%' , border : '1px solid #5d9cec' }}>
			<div>login</div>
			{ bread && <a href = { bread }>bread{ bread }</a> }
		</div>
	)
}
複製程式碼

Abort.js

import React from 'react';
import { listenAll , listenPath } from '../../component/router/router';

listenPath('#/abort', () => {
	console.info('listenPath abort')
})

listenAll((pathname) => {
	if(pathname === '#/abort'){
		console.info('listenAll abort')
	}
})

export default function Abort(props = {}){
	let { bread } = props;
	return (
		<div style = {{ width : '100%' , height : '100%' , border : '1px solid #5d9cec' }}>
			<div>abort</div>
			{ bread && <a href = { bread }>bread{ bread }</a> }
		</div>
	)
}
複製程式碼

大功告成!

相關文章