实现前端路由

现代的前端框架react/vue/angular都有路由router的概念,通过手写实现可以帮助我们更好地了解它的工作原理。它们都推荐单页面应用 SPA 开发模式,在路由切换时替换 DOM Tree 中最小修改的部分 DOM,来减少原先因为多页应用的页面跳转带来的巨量性能损耗。它们都有自己的典型路由解决方案,@angular/router、react-router、vue-router等。

一般来说,这些路由插件总是提供两种不同方式的路由方式: Hash 和 History,有时也会提供非浏览器环境下的路由方式 Abstract(支持所有 JavaScript 运行 环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式)。

实现路由的2种方式

  1. hash模式 状态保存需要另行传递
  2. history模式 原生提供了自定义状态传递的能力

缺点

hash: 地址栏会多出一个#号,对url造成影响,在某些场景下如微信支付有坑。 history: 兼容性差,直接访问报404,需要服务器做处理。

基本原理

  • hash 主要原理是通过监听 # 后的 URL 路径标识符的更改而触发的浏览器 hashchange 事件(当 location.hash 发生改变时,将触发这个事件)

注意: Hash 方法是利用了相当于页面锚点的功能,所以与原来的通过锚点定位来进行页面滚动定位的方式冲突,导致定位到错误的路由路径,因此需要采用别的办法。

  • history 则基于 pushState 和 popState replaceState

Hash 模式是使用 URL 的 Hash 来模拟一个完整的 URL,因此当 URL 改变的时候页面并不会重载。History 模式则会直接改变 URL,所以在路由跳转的时候会丢失一些地址信息(只是动态的通过js操作window.history来改变浏览器地址栏里的路径,并没有发起http请求),在刷新或直接访问路由地址的时候会匹配不到静态资源。因此需要在服务器上配置一些信息,让服务器增加一个覆盖所有情况的候选资源。

history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 www.xxx.com/items/id。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。


  • 服务器配置
  1. Nginx方式 采用Nginx方案需要先将所有资源打包生成到对应的目录,比如dist,然后做如下配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    server {
    server_name react.thinktxt.com;
    listen 80;

    root /Users/txBoy/WEB-Project/React-Demo/dist;
    index index.html;
    location / {
    // $uri 是 nginx 的变量,就是当前这次请求的路径
    // try_files 会尝试在这个路径下寻找资源,如果找不到,会继续朝下一个寻找
    // $uri/ 的意思是在路径目录下寻找 index.html 或 index.htm
    // 最后都找不到的话,返回 index.html
    try_files $uri $uri/ /index.html;
    }
    }
  2. 通过修改webpack-dev-server运行方式 这个解决方法很简单,直接在运行时加入参数“–history-api-fallback”就可以了

  3. Node服务端配置

1
2
3
4
5
6
7
8
9
10
11
12
13
// express
//配置任何请求都转到index.html,而index.html会根据React-Router规则去匹配任何一个route
app.get('*', function (request, response){
response.sendFile(path.resolve(__dirname, 'dist', 'index.html'))
})

// koa
import xtpl from 'koa-xtpl';
app.use(xtpl({
root: path.resolve(__dirname, '../dist'),
extname: 'html',
commands: {}
}));

由于koa的这种方式端口与webpack-dev-server(8080)必须不同,所以还需要配合Nginx代理。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server {
server_name react.thinktxt.com;
listen 80;

location / {
proxy_pass http://localhost:8081;
}
}

server {
server_name static.react.thinktxt.com;
listen 80;

location / {
proxy_pass http://localhost:8080;
}
}

既然我们的Nginx代理用了真实域名,自然别忘了修改一下host,如下:

1
2
127.0.0.1 react.thinktxt.com
127.0.0.1 static.react.thinktxt.com

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
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
body {
margin: 0;
padding: 0;
}

#login, #index {
width: 100%;
height: 100%;
display: none;
}

#login {
background: #f5f5f5;
display: block;
}

#index {
background: #aaddff;
}
</style>
</head>
<body>

<!--登录页-->
<div id="login" class="component">
<input type="button" onclick="hashPush('index?id=1')" value="登陆">
</div>

<!--首页-->
<div id="index" class="component">
<input type="button" onclick="hashPush('login?key=2',{data:'params data'})" value="退出登陆">
</div>

</body>
<script>
// 全局变量
var params = {}

// 页面跳转
function hashPush(url, param) {
location.hash = "#" + url

// 解析url
let questionIndex = url.indexOf("?")
let path = url
if (questionIndex >= 0) {
path = url.substr(0, questionIndex)
}

if (param) {
// path路由作为key标识内容传参 存储路由
params[path] = param
}
}

// 监听hash的变动
window.addEventListener('hashchange', function (e) {
// let newURL = event.newURL; // hash 改变后的新 url
// let oldURL = event.oldURL; // hash 改变前的旧 url
let url = location.hash.slice(1) || "index"
// 解析url
let questionIndex = url.indexOf("?")
let path, query
if (questionIndex >= 0) {
// 获取当前变化的路由名
path = url.substr(0, questionIndex)
// 获取url上的传参
let queryString = url.substr(questionIndex + 1)
let queryArray = queryString.split("&")
let queryObject = {}
queryArray.map(str => {
let equalIndex = str.indexOf("=")
if (equalIndex > 0) {
let key = str.substr(0, equalIndex)
let value = str.substr(equalIndex + 1)
queryObject[key] = value
}
})
query = queryObject
} else {
path = url
query = {}
}

console.log('接收到url传递的参数', query)
console.log('直接用params传递的参数', params[path])
setVisible(path)
})

// 显示跟路由地址对应的内容,隐藏其他内容
function setVisible(url) {
let components = Array.from(document.body.querySelectorAll(".component"))
components.map(item => {
if (item.id === url) {
// console.log('显示',item.id)
item.style.display = 'block'
} else {
// console.log('隐藏',item.id)
item.style.display = 'none'
}
})
}
</script>
</html>

history模式(需要服务器环境)

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
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
body {
margin: 0;
padding: 0;
}

#login, #index {
width: 100%;
height: 100%;
display: none;
}

#login {
background: #f5f5f5;
display: block;
}

#index {
background: #aaddff;
}
</style>
</head>
<body>

<!--登录页-->
<div id="login" class="component">
<input type="button" onclick="historyPush('index')" value="登陆">
</div>

<!--首页-->
<div id="index" class="component">
<input type="button" onclick="historyPush('login',{data:'params data'})" value="退出登陆">
</div>

</body>
<script>
// 全局变量
var params = {}

// 页面跳转
function historyPush(url, param) {
history.pushState(param, '我是页面标题', url)
if (param) {
params[url] = param
}
}

// 监听前进后退
window.addEventListener('popstate', function (e) {
// e.state 就是pushState 的时候,传的第一个参数
let url = state.target.location.pathName
console.log('接收到传递的参数', e.state)
console.log('直接用params传递的参数', params[path])
setVisible(path)
})

// 显示跟路由地址对应的内容,隐藏其他内容
function setVisible(url) {
let components = Array.from(document.body.querySelectorAll(".component"))
components.map(item => {
if (item.id === url) {
// console.log('显示',item.id)
item.style.display = 'block'
} else {
// console.log('隐藏',item.id)
item.style.display = 'none'
}

})
}
</script>
</html>

参考资料

本文结束感谢您的阅读

本文标题:实现前端路由

文章作者:陈宇(cosyer)

发布时间:2018年11月24日 - 11:11

最后更新:2020年08月31日 - 00:08

原始链接:http://mydearest.cn/%E5%AE%9E%E7%8E%B0%E5%89%8D%E7%AB%AF%E8%B7%AF%E7%94%B1.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!