jQuery 简单路由的实现

本文将介绍如何利用 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
/**
* 获取url中的路径参数并进行匹配
*/
function onHashChange() {
// 获取url中的有效路径参数
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)
}

/**
* 匹配路由并加载组件
* 使用递归方式,保证元素加载完成后再继续下一步
* @param 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')
}
}
}

注意点:

  • 获取 location.hash 之后应该去除 hash 前的 # 以及前后的 /

    以便生成正确的路径参数数组

  • 由于存在嵌套路由关系,因此应该逐级匹配路由并加载 dom,这里的解决方案是:

    • router-view-层级 作为路由占位的标记,以便正确逐级加载 dom

    • 使用递归的方式逐级匹配路由

      需要等待 dom 加载完成后再进行下一级的匹配,因此需要通过 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
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 = []
}

/**
* 初始化方法
* @param routerMap json格式的参数表
*/
initial(routerMap) {
this.routerMap = routerMap
if (typeof jQuery == 'undefined') {
window.alert('请引入 jQuery-Router 所依赖的 jQuery')
return
}
jQuery(() => onHashChange())
window.addEventListener('hashchange', hashchangeEvent => {
jQuery(() => onHashChange(hashchangeEvent))
})

/**
* 获取url中的路径参数并进行匹配
*/
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)
}

/**
* 将URL转化为路径数组
*/
const getPathList = (url) => {
// 获取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
}

/**
* 匹配路由并加载组件
* 使用递归方式,保证元素加载
* @param matchRes 匹配结果
*/
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 = ''
}

/**
* 初始化方法
* @param routerMap json格式的参数表
*/
initial(routerMap) {
this.routerMap = routerMap
if (typeof jQuery == 'undefined') {
window.alert('请引入 jQuery-Router 所依赖的 jQuery')
return
}
jQuery(() => onHashChange())
window.addEventListener('hashchange', hashchangeEvent => {
jQuery(() => onHashChange())
})

/**
* 获取url中的路径参数并进行匹配
*/
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)
}

/**
* 获取hash中的路径信息
*/
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
}

/**
* 获取hash中的参数信息
*/
const getParam = (hash) => {
if (hash.indexOf('?') === -1) {
return ''
}
return hash.slice(hash.indexOf('?') + 1)
}

/**
* 匹配路由并加载组件
* 使用递归方式,保证元素加载
* @param matchRes 匹配结果
*/
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)
}

/**
* 直接改变hash
*/
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的简单路由,适用于单页面富应用开发