前言
一天傍晚,吃完晚饭后我在实验室像往常一样在B站宅舞区学习,正在兴头上;啪的一声,很快啊,实验室的门开了,小组前后端的学弟朝我逼近,我赶紧切到vscode,进入作战状态"有什么需要帮助的吗😊”,身为bug路由器的我能为同学们解决问题必然是荣(guan)幸(wo)至(pi)极(shi)。 (想要看实现的同学直接跳到最后)
听完他们的描述我笑了, 前端同学:我这个功能会生成一条链接,用户点进去会巴拉巴拉...,但是我在这个网站中点击就可以跳转到那个页面,把链接复制出来再访问结果就访问到后端的接口上去了
后端同学:我没写这个接口啊....
这时候我大概已经猜到问题出在哪了🙄,但还是装模做样的打开控制台分析,你看哈这个链接放到地址栏上发送请求后返回的是404,只是404页面没有处理过让你看起来像是在访问接口,还有你知道为什么会返回404吗,因为spa应用路由都是在前端生成的,你向服务器请求这个url,服务器这个路径上找不到资源所以返回了404哦~解决的话,把路由换成hash吧,学弟们一脸懵....
“算了,算了,我回头写一下再给你们看好吧”(能不能别打扰我看美女,哦不,学习)
于是乎,我连夜写了这篇前端路由,正好该我做技术分享了
history or hash ?
前端路由分为两种模式基于html5的history模式和古老的hash模式,我们从问题入手。看看当路由使用hash模式是如何解决404的。
事实上使用hash模式能解决上面的问题只是因为我们在浏览器输入url携带#xxxx时,#xxx会被截断,导致发送的请求是#之前url,这个url在服务器是存在相应资源的,所以就正常返回了。
好奇的学弟看到这一定会说这#号太丑了,你赶紧给我想个不带#号的解决方案😤
好吧,好吧 我们来看history如何解决
history模式如何解决
解决思路:发送到服务器端的url是实打实的路径,传统的spa应用就一个html文件,路由通过加载html中的script资源并运行才得到的,我们这里可以看一下spa把js禁掉之后会怎样
可以看到spa获取到的html本身只有一个div和一堆script,当script加载后运行才有了路由,我们直接去向服务器请求,服务器上哪给你找去,难不成在服务端运行一遍script?
这里比较通用的解决办法为通过修改nginx配置返回首页的资源
location / {
root /xxxxxxx;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
添加try_files这一行就可以了,这里表示按顺序请求资源,如果找不到,重定向到index.html
好的,这时我们请求到了index.html,前面我们说过了请求到的html文件只是一个包含script的空html ,那么问题来了,我们请求了index.html屏幕会显示哪个页面?
url的path指向的具体页面 还是index.html首页
大家可以去试一下,会显示url的指向的具体页面,大家不妨想一下,index.html把脚本资源都请求下来之后会直接运行,这里运行自然会运行路由对应的代码,那路由检测到地址栏上的url正是路由的一部分,于是开始了对应页面的渲染
到这里问题总算告一段落,,,下面献上一段我自己实现的乞丐版路由
实现
主要涉及到的api有hashchange,pushState,popstate
,对api不熟悉的同学建议先去mdn上查一下,简单的说就是浏览器维护了一个历史栈,栈中每一个元素对应了一个历史状态,pushState向栈中增加一个状态,replaceState替换一个状态,值得注意的是他俩都不会让浏览器去请求资源,只会改变地址栏的地址。
思路
前端路由的本质实际上就是根据不同的路径去渲染不同的组件
初始化
history模式和hash模式都有一个变量存储路由信息和同样的render函数,于是我们把他抽象出来放在父类上,对继承不是很熟悉的同学可以看我另一篇文章。
//index.js
function Route(config) {
this.routesMap = new Map
config && this.init(config)
}
Route.prototype.init = function (config) {
for (let i = 0; i < config.length; i++) {
this.routesMap.set(config[i].path, config[i].element)
}
}
//不涉及路由嵌套 只在root节点下更新
Route.prototype.render = function (element) {
let root = document.getElementById("root")
root.innerHTML = ""
if (element) {
root.appendChild(element)
} else {
root.innerHTML = '<h1>404</h1>'
}
}
historyRoute实现
//toy router 不对数据做异常处理
//history
function HistoryRoute(config) {
//init
// this.routesMap = new Map
Route.call(this, config)
// this.init(config)
let that = this
window.onpopstate = function (e) {
let path = e.state && e.state.path
let element = that.routesMap.get(path)
that.render(element)
}
}
//约定path 以 / 开头
HistoryRoute.prototype.navigate = function (path) {
//如果想对参数作处理 截断path ?后面的内容放进state就行了
let element = this.routesMap.get(path)
history.pushState({ path }, "", path)
this.render(element)
}
hashRoute实现
//hash
function HashRoute(config) {
Route.call(this,config)
let that = this
window.onhashchange = function () {
//去掉#号
let path = location.hash.slice(1)
let element = that.routesMap.get(path)
that.render(element)
}
}
HashRoute.prototype.navigate = function (path) {
location.hash = path
}
组合式继承连接父子构造函数
HistoryRoute.prototype = new Route
HistoryRoute.prototype.constructor = HistoryRoute
HashRoute.prototype = new Route
HashRoute.prototype.constructor = HashRoute
最后把html贴出来
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<button onclick="handleTouser()" id="back">to User</button>
<button onclick="handleTohome()" id="pushState">to home</button>
<button onclick="handleToarticle()">to article</button>
</body>
<script src="./index.js"></script>
<script>
let Home = document.createElement("h1")
let User = document.createElement("h1")
let Article = document.createElement("h1")
Home.innerText = "I am home!"
User.innerText = "I am user!"
Article.innerText = "I am article!"
let routeConfig = [
{
path: "/home",
element: Home
},
{
path: '/user',
element: User,
}, {
path: '/article',
element: Article
},
]
// let routeInstance = new HistoryRoute(routeConfig)
let routeInstance = new HashRoute(routeConfig)
function handleToarticle(){
routeInstance.navigate("/article")
}
function handleTohome() {
routeInstance.navigate("/home")
}
function handleTouser() {
routeInstance.navigate("/user")
}
</script>
</html>
最后
路由的实现比较简陋,不足之处还请在评论区指出