你真的理解前端路由了吗

2,718 阅读5分钟

前言

一天傍晚,吃完晚饭后我在实验室像往常一样在B站宅舞区学习,正在兴头上;啪的一声,很快啊,实验室的门开了,小组前后端的学弟朝我逼近,我赶紧切到vscode,进入作战状态"有什么需要帮助的吗😊”,身为bug路由器的我能为同学们解决问题必然是荣(guan)幸(wo)至(pi)极(shi)。 (想要看实现的同学直接跳到最后)

d354cdb8-e843-4ae1-a7f6-56f879986b00..jpg

听完他们的描述我笑了, 前端同学:我这个功能会生成一条链接,用户点进去会巴拉巴拉...,但是我在这个网站中点击就可以跳转到那个页面,把链接复制出来再访问结果就访问到后端的接口上去了

后端同学:我没写这个接口啊....

5f01d2d6-5b5b-4d59-9b95-cbf385be90ae..jpg

这时候我大概已经猜到问题出在哪了🙄,但还是装模做样的打开控制台分析,你看哈这个链接放到地址栏上发送请求后返回的是404,只是404页面没有处理过让你看起来像是在访问接口,还有你知道为什么会返回404吗,因为spa应用路由都是在前端生成的,你向服务器请求这个url,服务器这个路径上找不到资源所以返回了404哦~解决的话,把路由换成hash吧,学弟们一脸懵....

“算了,算了,我回头写一下再给你们看好吧”(能不能别打扰我看美女,哦不,学习)

于是乎,我连夜写了这篇前端路由,正好该我做技术分享了

history or hash ?

前端路由分为两种模式基于html5的history模式和古老的hash模式,我们从问题入手。看看当路由使用hash模式是如何解决404的。

事实上使用hash模式能解决上面的问题只是因为我们在浏览器输入url携带#xxxx时,#xxx会被截断,导致发送的请求是#之前url,这个url在服务器是存在相应资源的,所以就正常返回了。

image.png

好奇的学弟看到这一定会说这#号太丑了,你赶紧给我想个不带#号的解决方案😤

好吧,好吧 我们来看history如何解决

history模式如何解决

解决思路:发送到服务器端的url是实打实的路径,传统的spa应用就一个html文件,路由通过加载html中的script资源并运行才得到的,我们这里可以看一下spa把js禁掉之后会怎样

image.png

可以看到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>

最后

路由的实现比较简陋,不足之处还请在评论区指出

源码:github.com/yang1666204…