本文将介绍如何利用 jQuery 实现一个简单的前端路由。
一、前端路由与单页面富应用
具体请看:
SPA-阶段:网站应用化 - 前端架构 渲染模式的演进
二、实现原理
1. URL 的 hash
具体请看:
Vue Router - hash
并且,
- 可以通过
location.hash
获取和修改 hash 值
- 可以通过
hashchange
事件监听 hash 的改变
可以通过 hash 来传递路径。
2. jQuery 的 load()
jQuery 提供了 load()
方法,可以用于将内容加载至元素之中。
可以通过 load()
来进行动态的 dom 更新。
三、实现思路
通过 URL 的 hash 传递路径,通过监听 hashChange
来获取路径的变化,当路径变化时,执行 load()
方法加载对应的 dom。
四、实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
function onHashChange() { let hash = location.hash.slice(location.hash.indexOf("#") + 1) if (hash.charAt(0) === "/") { hash = hash.slice(1) } if (hash.charAt(hash.length - 1) === "/") { hash = hash.slice(0, hash.length - 1) }
pathParams = hash.split("/")
let matchRes = { son: routerMap }
matchAndLoad(0, matchRes) }
function matchAndLoad(i, matchRes) { if (i < pathParams.length) { if (matchRes.son.hasOwnProperty(pathParams[i])) { if (matchRes.son[pathParams[i]].redirect !== undefined) { matchRes = matchRes.son[matchRes.son[pathParams[i]].redirect] } else { matchRes = matchRes.son[pathParams[i]] } console.log(pathParams[i] + "匹配成功,其路径是:" + matchRes.path)
$('#router-view-' + (i + 1)).load(matchRes.path, () => { matchAndLoad(i + 1, matchRes) }) } else { $('#router-view-1').load('./pages/404/index.html') } } }
|
注意点:
五、路由工具封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
| (function () {
class Router {
constructor() { this.routerMap = {} this.pathParams = [] }
initial(routerMap) { this.routerMap = routerMap if (typeof jQuery == 'undefined') { window.alert('请引入 jQuery-Router 所依赖的 jQuery') return } jQuery(() => onHashChange()) window.addEventListener('hashchange', hashchangeEvent => { jQuery(() => onHashChange(hashchangeEvent)) })
const onHashChange = (hashchangeEvent) => { let newPathList = [] let oldPathList = [] let pathInfoList = [] if (hashchangeEvent) { newPathList = getPathList(hashchangeEvent.newURL) oldPathList = getPathList(hashchangeEvent.oldURL) } else { newPathList = getPathList(location.hash) } for (let i = 0, startChange = false; i < newPathList.length; i++) { let isChange = !(startChange === false && i < oldPathList.length && oldPathList[i] === newPathList[i]) if (isChange) { startChange = true } let pathInfo = { path: newPathList[i], isChange: isChange } pathInfoList.push(pathInfo) }
let matchRes = { son: this.routerMap }
matchAndLoad(1, matchRes, pathInfoList) }
const getPathList = (url) => { let hash = url.slice(url.indexOf('#') + 1) if (hash.indexOf('?') !== -1) { hash = hash.slice(0, hash.indexOf('?')) } if (hash.charAt(0) === '/') { hash = hash.slice(1) } if (hash.charAt(hash.length - 1) === '/') { hash = hash.slice(0, hash.length - 1) }
let pathList = hash.split('/')
return pathList }
const matchAndLoad = (i, matchRes, pathInfoList) => { if (i > pathInfoList.length) { return } let tempMatchRes = matchRes.son[pathInfoList[i - 1].path] if (tempMatchRes) { if (tempMatchRes.redirect) { matchRes = matchRes.son[tempMatchRes.redirect] } else { matchRes = tempMatchRes }
if (pathInfoList[i - 1].isChange) { $('#router-view-' + i).load(matchRes.path, () => { matchAndLoad(i + 1, matchRes, pathInfoList) }) } else { matchAndLoad(i + 1, matchRes, pathInfoList) } } else { $('#router-view-1').load(routerMap['404'].path) } } }
append(str) { location.hash = location.hash + str }
to(str) { location.hash = str } }
window.$router = new Router() })()
|
注意点:
- 采取自调用函数的方式执行代码,保证变量作用域局限在路由之内,避免被外部 js 污染
- 由于路由依赖 jQuery,因此需要检查 jQuery 是否被正确导入
- 将路由挂载在 window 上,以便在其它地方调用路由
- 提供
initial()
方法,使用者应该自行生成路由表,通过 window.$router.initial(路由表)
将路由表传入并初始化路由
- 提供
append()
方法和 to()
方法,以便使用者进行路由跳转
- 支持传递请求参数
- 避免了为改变组件的重复
load()
六、代码优化
- 优化代码
- 优化判断逻辑,避免修改参数后组件不更新的错误
- 调整方法:、
append()
方法用于追加路径,仅允许传入路径
to()
方法用于跳转路径,仅允许传入路径
setParam()
方法用于设置参数,该参数将会持久携带直至 hash 被改变
changeHash()
用于直接改变 hash
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
| (function () {
class Router {
constructor() { this.routerMap = {} this.path = '' this.oldPath = '' this.param = '' this.oldParam = '' }
initial(routerMap) { this.routerMap = routerMap if (typeof jQuery == 'undefined') { window.alert('请引入 jQuery-Router 所依赖的 jQuery') return } jQuery(() => onHashChange()) window.addEventListener('hashchange', hashchangeEvent => { jQuery(() => onHashChange()) })
const onHashChange = () => { this.oldPath = this.path this.path = getPath(location.hash) let pathList = this.path.split('/') let oldPathList = this.oldPath.split('/')
this.oldParam = this.param this.param = getParam(location.hash)
let pathInfoList = [] for (let i = 0, startChange = this.oldParam !== this.param; i < pathList.length; i++) { let isChange = true if (startChange === false && i < oldPathList.length && oldPathList[i] === pathList[i]) { isChange = false } if (i === pathList.length - 1 && pathList.length < oldPathList.length) { isChange = true } if (isChange) { startChange = true }
let pathInfo = { path: pathList[i], isChange: isChange } pathInfoList.push(pathInfo) }
let matchRes = { son: this.routerMap }
matchAndLoad(1, matchRes, pathInfoList) }
const getPath = (hash) => { let path = hash.slice(hash.indexOf('#') + 1) if (path.indexOf('?') !== -1) { path = path.slice(0, path.indexOf('?')) } if (path.charAt(0) === '/') { path = path.slice(1) } if (path.charAt(path.length - 1) === '/') { path = path.slice(0, path.length - 1) } return path }
const getParam = (hash) => { if (hash.indexOf('?') === -1) { return '' } return hash.slice(hash.indexOf('?') + 1) }
const matchAndLoad = (i, matchRes, pathInfoList) => { if (i > pathInfoList.length) { return } let tempMatchRes = matchRes.son[pathInfoList[i - 1].path] if (tempMatchRes) { if (tempMatchRes.redirect) { matchRes = matchRes.son[tempMatchRes.redirect] } else { matchRes = tempMatchRes }
if (pathInfoList[i - 1].isChange) { $('#router-view-' + i).load(matchRes.path, () => { matchAndLoad(i + 1, matchRes, pathInfoList) }) } else { matchAndLoad(i + 1, matchRes, pathInfoList) } } else { $('#router-view-1').load(routerMap['404'].path) } } }
append(subPath) { setHash(this.path + subPath, this.param) }
to(path) { setHash(path, this.param) }
setParam(param) { setHash(this.path, param) }
changeHash(hash) { location.hash = hash } }
const setHash = (path, param) => { if (path.indexOf('?') !== -1) { location.hash = '#404' console.log('路径不准携带参数') return } if (path.charAt(0) === '/') { path = path.slice(1) } if (path.charAt(path.length - 1) === '/') { path = path.slice(0, path.length - 1) } if (param != '') { location.hash = '/' + path + '?' + param } else { location.hash = '/' + path } }
window.$router = new Router() })()
|
七、使用方法
将路由 js 文件引入
配置路由表,配置方式请参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| { "一级路由名": { "redirect": "重定向路由名" }, "一级路由名": { "path": "路由路径,以最外层index.html为基准", "son": { "二级路由名": { ···二级路由参数··· } } } }
|
初始化路由
1 2
| let 路由表 = ···获取路由表··· window.$router.initial(路由表)
|
在需要通过路由匹配组件的位置使用 id 为 router-view-层级
的 div 占位
通过 http://IP:端口/index.html#一级路由名/二级路由名/···
访问对应路由下的页面
通过 window.$router.append()
方法和 window.$router.to()
方法调用路由跳转路径
八、开源地址
codewld/jQuery-Router: 基于jQuery的简单路由,适用于单页面富应用开发