在SPA項目中,若是展現頁面繁多,根據前端工程化理論中組件化思想,咱們一般會將不一樣的頁面,按照功能,職責等進行劃分。 這個時候,每一個頁面,就須要一個相應的路由去對應。javascript
如今react社區中已經有react-router這個成熟的輪子,咱們能夠直接引入並使用。html
但具體hash路由是如何實現的,咱們如今就來探討一下...前端
在路由設計中,咱們用到了設計模式中的 單例模式 觀察者模式 中介者模式java
因此若是有該設計模式基礎或實踐的小夥伴,會更容易看懂一些...react
首先咱們要了解一下,監聽路由改變,實際上是監聽2個事件並操做回調,進行渲染:es6
<!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改變事件來進行處理了。設計模式
<script type = 'text/javascript'>
"use strict"
class HashRouter{
constructor(){
this.routers = {},
this.init();
}
}
</script>
複製代碼
<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方法前端工程化
在上一步咱們進行了初始化,監聽了頁面hash改變的事件並作相應的處理。可是,咱們須要執行的回調方法,須要開發人員手動添加才行。api
<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咱們便實現了數組
因爲es6模塊化橫空出世,咱們能夠封裝存儲變量,拋出方法,操做內部變量,不會變量污染。 若是放在之前,咱們須要經過閉包來實現相似功能
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' }/> },
]
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(<RouterPage/>, document.getElementById('easy-router'));
複製代碼
下面咱們來逐個分析{ Router , Route , dispatchRouter , listenAll , listenPath }中的每個做用
實際上是個很簡單的方法,至關於包裝了一層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('#/login', () => {
console.info('listenPath login1')
})
listenAll((pathname) => {
if(pathname === '#/login'){
console.info('listenAll login')
}
})
複製代碼
具體實現:
/**
* 路由監聽事件對象,分爲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);
}
複製代碼
簡單的一個路由跳轉方法,沒怎麼深思,網上應該有更大佬的方法,這裏就是意思一下
//路由跳轉
export function dispatchRouter({ path = '' , query = {} }){
let queryStr = [];
for(let i in query){
queryStr.push(`${i}=${query[i]}`);
}
window.location.href = `${path}?${queryStr.join('&')}`;
}
複製代碼
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);
}
複製代碼
這裏簡單寫了個結構
<!doctype html>
<html>
<head>
<title>簡易路由</title>
</head>
<body>
<div id = 'easy-router'></div>
</body>
</html>
複製代碼
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' }/> },
]
export default function RouterPage(){
return(
<Router>
<Menu routers = { Routers }/>
<MainLayout>
{ Routers && Routers.map((item, index) => (<Route path = { item.path } component = { item.component }/>)) }
</MainLayout>
</Router>
)
}
ReactDOM.render(<RouterPage/>, document.getElementById('easy-router'));
複製代碼
import React from 'react';
export default function MainLayout({ children }){
return(
<div>
{ children }
</div>
)
}
複製代碼
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>
)
}
複製代碼
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>
)
}
複製代碼
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>
)
}
複製代碼