🎸 一篇文章搞懂前后端路由及前端路由的实现

一、WEB 开发中的路由(以下简称路由)是指什么

路由简单来说根据网页 url 的改变来转发给对应控制器处理的应用程序,控制器就是我们俗称的Controller,一般是一个函数,所以路由也可以看作是url与函数的映射

二、开发中常见的「route」和 「router」有什么语义上的区别

1、 route就是我们常说的映射 url 到函数的路由,比如我们要定义一个用户详情页面的路由:

/user/:username => getUserInfo(username){...}

2、 router则为路由管理器,可以理解为管理 route 的容器。简单来说,route只是进行了 url 与函数的映射,而当接收到 url 的改变时,去对应路由映射表查找对应函数的过程则是由router来完成的。

3、 关于两者的关系,我觉得Stack Overflow上有个回答挺好的:

The router routes you to a route.

三、服务端路由和前端路由的区别

1、 服务端路由

对于服务器来说,当接收到客户端发来的 HTTP 请求,会根据请求的 URL,来找到相应的映射函数,然后执行该函数,并将函数的返回值发送给客户端。对于最简单的静态资源服务器,可以认为,所有 URL 的映射函数就是一个文件读取操作。对于动态资源,映射函数可能是一个数据库读取操作,也可能是进行一些数据的处理,等等。

比如我们使用Express定义一个服务端路由

// 读取文件操作
app.get('/', (req, res) => {
  res.sendFile('index')
})

// 数据库读取操作
app.get("/users", (req, res)=>{
  db.queryAllUsers()
    .then(data => res.send(data))
})

注意,router 在动态匹配 route 的过程中不仅会判断url是否匹配,还会判断请求的方式是否正确,比如上面两个例子,如果我们使用post方法请求上面的 url 地址,就会找不到正确的路由。

2、 客户端路由

客户端路由就是我们常说的前端路由,也叫单页应用路由客户端路由的映射函数通常是进行一些DOM的显示和隐藏操作。这样,当访问不同的路径的时候,会显示不同的页面组件

从用户角度来看的话,前端路由的作用是实现 url 与页面状态的的同步,即在不同的 url 下展示不同的内容、渲染不同的数据

因此,作为开发者,我们开发的路由器(即 router) 应该能够做到:

  1. 改变 url 且不向服务器发送请求;
  2. 检测 url 的变化并渲染对应状态;
  3. 检测 url 地址,解析匹配对应 route。

四、服务器路由和客户端路由的优缺点

优点 缺点
服务端路由 1. 安全性好;
2. SEO 好;
1. 代码耦合度差;
2. 服务器压力大。
3. 每次切换路由伴随请求,用户体验差

客户端路由 1. 用户体验好;
2. 代码耦合度低
1.SEO 差;
2. 页面切换会重新发送请求;

五、前端路由两种实现方案

  1. hash 路由

    1.1、 hash 是什么

    答:hash 指的是 url 的#及#后面的部分, 例如 https://www.baidu.com#123这个 url 里面#123就是 hash

    1.2、 为什么要使用 hash 路由?

    答:这是由于 hash 的特性决定的,hash 有以下特性:

    • hash 只是客户端的一种状态,向服务器发送请求时,hash 部分不会被发送
    • hash 值的改变会在浏览器中增加一个历史记录,因此我们能够根据浏览器的回退、前进按钮控制 hash 值的切换
    • 我们可以使用hashchange事件来监听 hash 值的变化

    1.3、 如何使用 hash 路由?

    答:hash 路由有两种使用方法:

    • 通过a标签,并设置href属性,这样我们点击这个链接时,url 会改变并触发hashchange事件,此时页面如果有 idhash值的元素时,点击链接也会跳转到对应的元素上方。
    <a href="#test" title="测试">测试</a>
    
    • 另一种方式就是使用javascript直接对location.hash赋值
    window.location.hash = "test"
    

    1.4、 hash 路由的实现

    因为 hash 路由的实现比较简单,就直接放出代码:

    class HashRouter {
      constructor(list) {
        this.list = list
        this.init()
      }
    
      init() {
        this.routerHandler()
        window.addEventListener('hashchange', () => {
          this.routerHandler()
        })
      }
    
      renderView(path) {
        const view =
          this.list.find(element => element.path === path)?.component ||
          this.list.find(element => element.path === '*')?.component
          document.querySelector('#app').innerHTML = view || '404'
      }
    
      routerHandler() {
        const path = this.getRouterPath()
        path === '/' && this.replace('/')
        this.renderView(path)
      }
    
      getRouterPath() {
        const hash = window.location.hash
        return hash ? hash.substring(1) : '/'
      }
    
      pushRoute(path) {
        window.location.hash = path
      }
    
      go(delta) {
        window.history.go(delta)
      }
    
      push() {
        window.history.go(1)
      }
    
      back() {
        window.history.back()
      }
    
      replace(path) {
        const currentUrl = window.location.href
        const replaceUrl = currentUrl.replace(/\/#(\/\S*)/, () => `/#${path}`)
        window.location.replace(replaceUrl)
      }
    }
    
    hash路由完整测试代码
    // index.html
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
      </head>
      <body>
        <button id="index">首页</button>
        <button id="users">用户列表</button>
        <button id="go">前进</button>
        <button id="back">后退</button>
        <button id="replaceIndex">替换为首页</button>
        <button id="replaceUsers">替换为用户页</button>
        <div id="app"></div>
        <script src="./index.js"></script>
      </body>
    </html>
    
    // index.js
    class HashRouter {
      constructor(list) {
        this.list = list
        this.init()
      }
    
      init() {
        this.routerHandler()
        window.addEventListener('hashchange', () => {
          this.routerHandler()
          console.log(window.history)
        })
      }
    
      renderView(path) {
        const view =
          this.list.find(element => element.path === path)?.component ||
          this.list.find(element => element.path === '*')?.component
        document.querySelector('#app').innerHTML = view || '404'
      }
    
      routerHandler() {
        const path = this.getRouterPath()
        path === '/' && this.replace('/')
        this.renderView(path)
      }
    
      getRouterPath() {
        const hash = window.location.hash
        return hash ? hash.substring(1) : '/'
      }
    
      pushRoute(path) {
        window.location.hash = path
      }
    
      go(delta) {
        window.history.go(delta)
      }
    
      push() {
        window.history.go(1)
      }
    
      back() {
        window.history.back()
      }
    
      replace(path) {
        const currentUrl = window.location.href
        const replaceUrl = currentUrl.replace(/\/#(\/\S*)/, () => `/#${path}`)
    
        window.location.replace(replaceUrl)
      }
    }
    
    const routes = [
      { path: '/', component: '主页' },
      { path: '/users', component: '用户列表' }
    ]
    
    const router = new HashRouter(routes)
    
    document.querySelector('#go').onclick = () => {
      router.push()
    }
    
    document.querySelector('#back').onclick = () => {
      router.back()
    }
    
    document.querySelector('#index').onclick = () => {
      router.pushRoute('/')
    }
    
    document.querySelector('#users').onclick = () => {
      router.pushRoute('/users')
    }
    
    document.querySelector('#replaceIndex').onclick = () => {
      router.replace('/')
    }
    
    document.querySelector('#replaceUsers').onclick = () => {
      router.replace('/users')
    }
    
  2. history 路由

    2.1、什么是 history 路由? history 路由是使用html5window.history 对象提供的对浏览器会话历史访问的功能实现的路由。

    2.2、为什么要使用 history 路由?

    • url 样式更美观
    • history 对象提供了 pushStatereplaceState 这个两个 API,可以在不进行刷新的情况下,操作浏览器的历史纪录
    • 我们可以使用 popstate 事件来监听 url 的变化,但是要注意,使用pushStatereplaceState 这个两个 API 改变路由不会触发popstate事件,能够触发popstate事件的只有gobackforward这三个方法
    • 要注意的是,使用 history 路由的时候如果刷新页面,浏览器会向服务器发送请求,如果后端没有做对应的处理页面会出现 404 错误,所以使用 history 路由的时候需要后端配合

    2.3、history 路由的实现

    class HistoryRouter {
      constructor(list) {
        this.list = list
        this.init()
      }
    
      init() {
        this.routerHandler()
        window.addEventListener('popstate', () => {
          this.routerHandler()
        })
      }
    
      renderView(path) {
        const view =
          this.list.find(element => element.path === path)?.component ||
          this.list.find(element => element.path === '*')?.component
    
        // 将视图挂载到页面上
        document.querySelector('#app').innerHTML = view || '404'
      }
    
      routerHandler() {
        const path = this.getRouterPath()
        this.renderView(path)
      }
    
      getRouterPath() {
        return window.location.pathname || '/'
      }
    
      pushRoute(path) {
        window.history.pushState(null, null, path)
        this.routerHandler()
      }
    
      go(delta) {
        window.history.go(delta)
      }
    
      push() {
        window.history.go(1)
      }
    
      back() {
        window.history.back()
      }
    
      replace(path) {
        window.history.replaceState(null, null, path)
        this.routerHandler()
      }
    }
    
    hash路由完整测试代码
    // index.html
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
      </head>
      <body>
        <button id="index">首页</button>
        <button id="users">用户列表</button>
        <button id="go">前进</button>
        <button id="back">后退</button>
        <button id="replaceIndex">替换为首页</button>
        <button id="replaceUsers">替换为用户页</button>
        <div id="app"></div>
        <script src="./index.js"></script>
      </body>
    </html>
    
    // index.js
    class HistoryRouter {
      constructor(list) {
        this.list = list
        this.init()
      }
    
      init() {
        this.routerHandler()
        window.addEventListener('popstate', () => {
          this.routerHandler()
        })
      }
    
      renderView(path) {
        const view =
          this.list.find(element => element.path === path)?.component ||
          this.list.find(element => element.path === '*')?.component
    
        // 将视图挂载到页面上
        document.querySelector('#app').innerHTML = view || '404'
      }
    
      routerHandler() {
        const path = this.getRouterPath()
        this.renderView(path)
      }
    
      getRouterPath() {
        return window.location.pathname || '/'
      }
    
      pushRoute(path) {
        window.history.pushState(null, null, path)
        this.routerHandler()
      }
    
      go(delta) {
        window.history.go(delta)
      }
    
      push() {
        window.history.go(1)
      }
    
      back() {
        window.history.back()
      }
    
      replace(path) {
        window.history.replaceState(null, null, path)
        this.routerHandler()
      }
    }
    
    const routes = [
      { path: '/', component: '主页' },
      { path: '/users', component: '用户列表' }
    ]
    
    const router = new HistoryRouter(routes)
    
    document.querySelector('#go').onclick = () => {
      router.push()
    }
    
    document.querySelector('#back').onclick = () => {
      router.back()
    }
    
    document.querySelector('#index').onclick = () => {
      router.pushRoute('/')
    }
    
    document.querySelector('#users').onclick = () => {
      router.pushRoute('/users')
    }
    
    document.querySelector('#replaceIndex').onclick = () => {
      router.replace('/')
    }
    
    document.querySelector('#replaceUsers').onclick = () => {
      router.replace('/users')
    }
    

五、两种方案的差别

这里掘友@我是你的超级英雄博客里面的表格已经描述的很详细了,我们可以借鉴过来给大家参考一下:

对比点 Hash 路由 History 路由
美观性 带着 # 字符,较丑 简洁美观
兼容性 >= ie 8,其它主流浏览器 >= ie 10,其它主流浏览器
实用性 不需要对服务端做改动 需要服务端对路由进行相应配合设置

六、最后

萌新发博,如有不对,留言讨论,谢谢~~

参考文章

juejin.im/post/5d469f… my.oschina.net/u/4357035/b…

本文使用 mdnice 排版

原文链接:juejin.im

上一篇:React Hooks 使用总结
下一篇:你的浏览器,独一无二--解读浏览器指纹

相关推荐

  • 🚩四年前端带你理解路由懒加载的原理

    前言 说起路由懒加载,大家很快就知道怎么实现它,但是问到路由懒加载的原理,怕有一部分小伙伴是一头雾水了吧。下面带大家一起去理解路由懒加载的原理。 路由懒加载也可以叫做路由组件懒加载,最常用的是通过im...

    5 个月前
  • 黄金搭档 -- JS 装饰器(Decorator)与Node.js路由

    很多面对象语言中都有装饰器(Decorator)函数的概念,Javascript语言的ES7标准中也提及了Decorator,个人认为装饰器是和async/await一样让人兴奋的的变化。

    1 年前
  • 首选客户端路由解决方案?[关闭]

    nbrolorefnon提出了一个问题:Preferred client side routing solution? [closed],或许与您遇到的问题类似。 回答者lorefnon给出了该问题的...

    3 年前
  • 项目整理11.28——路由

    1、路由基础 (1)添加路由链接 (2)添加路由填充位(占位符)~ (3)创建路由实例、~配置路由规则 2、路由重定向 3、路由组件传递参数 4、命名路由 ...

    1 年前
  • 面试官我想做个Reacter(React路由)

    路由的基础用法回顾,源码study,文章首发于docs,求个star 依赖 路由依赖于 react-router ,但是一般 pc 路由使用 react-router-dom ,它依赖于 react-...

    8 个月前
  • 链接路由器中的传递道具

    Geoffrey Halevishal atmakuri提出了一个问题:Pass props in Link react-router,或许与您遇到的问题类似。 回答者jmunsch给出了该问题的处理...

    3 年前
  • 针对还没搞懂javascript中this关键字的同学

    本篇文章主要针对搞不清this指向的的同学们!不定期更新文章都是我学习过程中积累下的经验,还请大家多多关注我的文章以帮助更多的同学,不对的地方还望留言支持改进! 首先,必须搞清楚在JS里面,函数的几种...

    2 年前
  • 针对Vue相同路由不同参数的刷新问题

    在使用vue和vue-router开发spa应用时,我们会遇到这样一种问题。 当页面跳转时,组件本身并没有发生改变: // 路由映射关系'/form/:type' // 当前页面路由/form/sho...

    2 年前
  • 配置式支持权限管理的 React 路由组件

    Router with Access Control for React Applications Installation yarn add react-acl-router react rea...

    2 年前
  • 逻辑强化系列(一):彻底搞懂自定义组件使用 v-model

    前言 阅读本文前,希望你已经彻底理解了语法糖 v-model 以及父子组件之间的通讯方法 v-model 在组件上使用 v-model 之前首先要知道,v-model 的用处以及实际操作流程,以方便理...

    5 个月前

官方社区

扫码加入 JavaScript 社区