夜听城嚣 夜听城嚣
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏

dwfrost

前端界的小学生
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏
  • 读书笔记

  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
    • 前端路由原理及实践
      • 性能优化之虚拟列表
      • mysql教程
      • python项目实践
      • 图片懒加载原理
      • WallectConnect开发心得
      • 企微机器人
    • 项目实践

    • 框架应用

    • 前端一览
    • 专题分享
    frost
    2022-01-04

    前端路由原理及实践

    在单页应用出现之前,页面的跳转会导致整个页面刷新,体验很不友好。ajax的出现可以让页面进行局部刷新,而单页的前端路由可以做到“页面跳转”时同样是页面级别的局部刷新。

    前端路由的基本原理是:匹配不同的 url 路径,进行解析,然后动态的渲染出区域 html 内容。

    实现方式有2种:hash实现和history实现。

    # hash模式

    我们知道,从a.html通过location.href的方式跳转到b.html,页面会重新向服务器发出页面请求,导致重新加载页面。但是,如果只是hash值变化,浏览器不发出请求,页面也就不会刷新,这个通常是a标签进行锚点跳转的时候的行为。利用这一特性,可以应用于前端路由,当hash发生变化时,可以被hashchange事件或popstate事件(需要html5 API支持)监听到,之后就可以根据配置的路由页面进行页面的局部更新。

    # hash原理

    vue-router源码 (opens new window)

    提取hash相关代码,如下

      setupListeners () {
        if (this.listeners.length > 0) {
          return
        }
        const handleRoutingEvent = () => {
          this.transitionTo(getHash(), route => {
            if (!supportsPushState) {
              replaceHash(route.fullPath)
            }
          })
        }
        const eventType = supportsPushState ? 'popstate' : 'hashchange'
        window.addEventListener(
          eventType,
          handleRoutingEvent
        )
        this.listeners.push(() => {
          window.removeEventListener(eventType, handleRoutingEvent)
        })
      }
    
      push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
        const { current: fromRoute } = this
        this.transitionTo(
          location,
          route => {
            pushHash(route.fullPath)
            onComplete && onComplete(route)
          },
          onAbort
        )
      }
        function pushHash (path) {
          if (supportsPushState) {
            pushState(getUrl(path))
          } else {
            window.location.hash = path
          }
        }
        function pushState (url?: string, replace?: boolean) {
          // try...catch the pushState call to get around Safari
          // DOM Exception 18 where it limits to 100 pushState calls
          const history = window.history
          try {
            if (replace) {
              // preserve existing history state as it could be overriden by the user
              const stateCopy = extend({}, history.state)
              stateCopy.key = getStateKey()
              history.replaceState(stateCopy, '', url)
            } else {
              history.pushState({ key: setStateKey(genStateKey()) }, '', url)
            }
          } catch (e) {
            window.location[replace ? 'replace' : 'assign'](url)
          }
        }
    
    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

    可以看到,vue-router会优先选择popstate监听,如果浏览器不支持,则使用hashchange兜底。

    # hash实现

    由于只探讨路由原理,此处不包含模板编译、路由守卫、路由记录、滚动记录等功能。

            <style>
                #app {
                    text-align: center;
                }
    
                #nav {
                    padding: 20px;
                }
            </style>
     		<div id="app">
                <div id="nav">
                    <a href="#/">Home</a>
                    |
                    <a href="#/about">About</a>
                </div>
                <div id="nav">
                    <button class="hash-push-home">hash push Home</button>
                    <button class="hash-push-about">hash push About</button>
                </div>
                <div id="nav">
                    <button class="history-push-home">history push Home</button>
                    <button class="history-push-about">history push About</button>
                </div>
                <div class="router"></div>
            </div>
    
    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
    const homeView = `
    <div class="home">
    <h1>This is home page</h1>
    </div>
    `
    const aboutView = `
    <div class="about">
        <h1>This is about page</h1>
    </div>
    `
    
    const routerView = document.querySelector('.router')
    
    // 监听hashchange事件
    // window.addEventListener('hashchange', e => {
    window.addEventListener('popstate', e => {
        console.log('popstate/hashchange')
        const hash = window.location.hash
    
        // 根据hash变化,加载对应模块
        if (hash === '#/') {
            routerView.innerHTML = homeView
        } else if (hash === '#/about') {
            routerView.innerHTML = aboutView
        }
    })
    
    // 模拟编程导航API,如push,使用hash相关api
    const hashPushHome = document.querySelector('.hash-push-home')
    const hashPushAbout = document.querySelector('.hash-push-about')
    
    hashPushHome.addEventListener('click', () => {
        location.hash = '/'
    })
    hashPushAbout.addEventListener('click', () => {
        location.hash = '/about'
    })
    
    // 模拟编程导航API,如push,使用pushstate相关api
    const historyPushHome = document.querySelector('.history-push-home')
    const historyPushAbout = document.querySelector('.history-push-about')
    
    historyPushHome.addEventListener('click', () => {
         // 让地址栏变化
        history.pushState({ key: Date.now() }, '', '#/')
    
         // 因为pushState不会再次触发popstate/hashchange
        //  所以手动替换视图
        routerView.innerHTML = homeView
    })
    historyPushAbout.addEventListener('click', () => {
        history.pushState({ key: Date.now() }, '', '#/about')
        routerView.innerHTML = aboutView
    })
    
    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

    # history模式

    # history原理

    使用了html5的api,即history.pushState,需要注意的是,使用该方式后地址栏会变化,但不会再触发popstate,所以需要手动替换视图。

      setupListeners () {
        if (this.listeners.length > 0) {
          return
        }
    
        const handleRoutingEvent = () => {
          const current = this.current
    
          // Avoiding first `popstate` event dispatched in some browsers but first
          // history route not updated since async guard at the same time.
          const location = getLocation(this.base)
          if (this.current === START && location === this._startLocation) {
            return
          }
    
          this.transitionTo(location)
        }
        window.addEventListener('popstate', handleRoutingEvent)
        this.listeners.push(() => {
          window.removeEventListener('popstate', handleRoutingEvent)
        })
      }
    
      push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
        const { current: fromRoute } = this
        this.transitionTo(location, route => {
          pushState(cleanPath(this.base + route.fullPath))
          handleScroll(this.router, route, fromRoute, false)
          onComplete && onComplete(route)
        }, onAbort)
      }
    
    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

    该方式和上述hash模式的pushState没有太大区别,唯一区别在于路由对“#”的处理。

    history模式没有了"#",但直接访问非首页页面时会出现404,需要服务端进行配置。见官方文档 (opens new window)。

    # history实现

    		<div id="app">
                <div id="nav">
                    <button class="history-push-home">history push Home</button>
                    <button class="history-push-about">history push About</button>
                </div>
                <div class="router"></div>
            </div>
    
    1
    2
    3
    4
    5
    6
    7
    
    const routerView = document.querySelector('.router')
    
    // 模拟编程导航API,如push,使用history相关api
    const historyPushHome = document.querySelector('.history-push-home')
    const historyPushAbout = document.querySelector('.history-push-about')
    
    init()
    
    historyPushHome.addEventListener('click', () => {
        // 让地址栏变化
        history.pushState({ key: Date.now() }, '', '/')
    
        // 因为pushState不会再次触发popstate/hashchange
        //  所以手动替换视图
        routerView.innerHTML = homeView
    })
    historyPushAbout.addEventListener('click', () => {
        history.pushState({ key: Date.now() }, '', '/about')
        routerView.innerHTML = aboutView
    })
    
    // 初始化页面
    // 注意刷新页面后为404的场景
    // 需要服务器配置页面重定向
    function init() {
        const { pathname } = location
        switch (pathname) {
            case '/about':
                routerView.innerHTML = aboutView
                break
            default:
                routerView.innerHTML = homeView
                break
        }
    }
    
    
    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

    如果使用http-server开启本地服务,可以直接访问http://127.0.0.1:8080,如下

    案例是在windows上测试的,于是下载windows版本的nginx进行调试。

    下载:下载nginx (opens new window)

    安装:将zip文件解压到目标目录。

    切换到安装目录下,运行nginx命令。

    nginx命令集合

    # 启动
    start nginx
    # 重启
    nginx -s reload
    # 停止
    nginx -s stop
    
    1
    2
    3
    4
    5
    6

    启动后,可以配置nginx.config文件。如下:

        server {
            listen       9090;
            location / {
                root   xxx\vue-router-study/src/history;
                try_files $uri $uri/ /index.html;
            }
        }
    
    1
    2
    3
    4
    5
    6
    7

    然后重启nginx,访问页面http://127.0.0.1:9090,搞定。

    # 路由基础

    安装和使用参考vue-router (opens new window)。

    # 简单示例

    以下是一个基本示例。

    1. 路由配置
    // router.js
    import Vue from 'vue'
    import Router from 'vue-router'
    Vue.use(Router)
    
    export default new Router({
        mode: 'history',
        base: process.env.BASE_URL, // 对应到vue.config.js中的publicPath
        routes: [
            {
                // 重定向
                path: '/',
                redirect: '/home'
            },
            {
                path: '/home',
                name: 'home',
                meta: {
                    title: '首页'
                },
                // 路由懒加载,减小chunk
                component: () => import(/* webpackChunkName: "home" */ '@/views/home/index.vue')
            },
            {
                path: '/about',
                name: 'about',
                meta: {
                    title: '关于'
                },
                component: () => import(/* webpackChunkName: "about" */ '@/views/about/index.vue')
            },
            {
                path: '/404',
                name: 'page404',
                meta: {
                    title: '404'
                },
                component: () => import(/* webpackChunkName: "page404" */ '@/views/page404/index.vue')
            },
            {
                // 放在最后,用于404页面显示
                path: '*',
                redirect: '/404'
            }
        ]
    })
    
    
    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
    1. Vue使用路由
    // main.js
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    
    new Vue({
        render: h => h(App),
        router
    }).$mount('#app')
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    1. 使用<router-view>来渲染匹配到的组件。

      <!-- App.vue -->
      <template>
        <router-view></router-view>
      </template>
      
      1
      2
      3
      4
    2. 2种方式导航

      <router-link :to="{ name: 'about'}">goto about</router-link>
      
      1
      this.$router.push({ path: `/about?num=${this.num}` })
      
      1

    # 导航方式

    Vue Router提供2种导航方式:

    • <router-link>声明式导航。

      平时用这种方式较少,详细使用见官方文档。

    • router编程式导航。

      router实例上的方法,如下

      router.push

      // 字符串
      router.push('home')
      
      // 对象
      router.push({ path: 'home' })
      
      // 命名的路由
      router.push({ name: 'user', params: { userId: '123' }})
      
      // 带查询参数,变成 /register?plan=private
      router.push({ path: 'register', query: { plan: 'private' }})
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11

      router.replace

      用法同router.push,区别在于它不会向history添加新记录。

      router.go

      用于前进和后退,类似于window.history.go(n)。

    # history模式

    当配置mode: 'history',时,就摆脱了颜值不高的#,但会带来一个问题:直接访问非首页的页面时,服务器返回404,因为url匹配不到任何资源。

    解决方式官网 (opens new window)已给出,只需要在匹配不到资源时返回index.html即可。如下:

    location / {
      try_files $uri $uri/ /index.html;
    }
    
    1
    2
    3

    # 路由进阶

    # 导航守卫

    守卫即拦截,当从一个页面进入到下一个页面时,将会触发路由导航钩子,实现我们的需求,比如权限和条件控制。包括3类守卫:

    • 全局守卫:前置守卫,解析守卫,后置钩子
    • 路由(配置)守卫
    • 组件内守卫

    回调结构基本相似,比如

    router.beforeEach((to, from, next) => {
      // ...
    })
    
    1
    2
    3
    • to: Route: 即将要进入的目标
    • from: Route: 当前导航正要离开的路由
    • next: Function: 一定要调用该方法来 resolve 这个钩子。

    # 登录鉴权

    我们可以配置meta字段,配合全局守卫来进行登录鉴权。

    router.beforeEach((to, from, next) => {
      if (to.matched.some(record => record.meta.requiresAuth)) {
        // this route requires auth, check if logged in
        // if not, redirect to login page.
        if (!store.state.isLogin) {
          next({
            path: '/login',
            query: { redirect: to.fullPath }
          })
        } else {
          next()
        }
      } else {
        next() // 确保一定要调用 next()
      }
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    这里之所以使用to.matched.some方法判断,是因为二级路由会全部匹配到,只要一级路由设置了需要登录态即可。

    # 进入页面前加参数

    这个常见于分享链接需求中,要求每次链接进行跳转时带上比如渠道id等信息,实现方式如下:

    router.beforeEach((to, from, next) => {
    
        const { channel: fromChannel } = from.query
        const { channel: toChannel } = to.query
        if (fromChannel && !toChannel) { // 只有当前页面有channel,下一页面无channel时,才透传
          const { query, path } = to
          query.channel = fromChannel
          next({
            path,
            query
          })
        } else {
          next()
        }
      })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    # 过渡动画

    实际是<transition>组件的效果之一,路由在切换时会切换类名,此时可以加上转场动画。

    // App.vue
    <template>
        <div class="App-wrap">
            <transition :name="transitionName" mode="out-in">
                <router-view />
            </transition>
        </div>
    </template>
    <script>
    export default {
        name: 'App',
        data() {
            return {
                transitionName: 'slide-right'
            }
        },
        watch: {
            $route(to, from) {
                const toDepth = to.path.split('/').length
                const fromDepth = from.path.split('/').length
                this.transitionName = toDepth < fromDepth ? 'slide-right' : toDepth > fromDepth ? 'slide-left' : ''
            }
        },
    }
    </script>
    <style lang="scss" scoped>
    .App-wrap {
        .slide-left-enter {
            transform: translate3d(100%, 0, 0);
        }
        .slide-left-enter-active,
        .slide-left-leave-active {
            transition: transform 0.4s ease;
        }
    
        .slide-right-leave-to {
            transform: translate3d(100%, 0, 0);
        }
        .slide-right-enter-active,
        .slide-right-leave-active {
            transition: transform 0.4s ease;
        }
    }
    </style>
    
    
    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
    #前端#源码#实践
    上次更新: 2022/01/13, 16:59:45
    HTML和CSS知识点
    性能优化之虚拟列表

    ← HTML和CSS知识点 性能优化之虚拟列表→

    最近更新
    01
    提交代码时修改commit消息
    04-09
    02
    如何快速定位bug
    02-20
    03
    云端web项目开发踩坑
    08-25
    更多文章>
    Theme by Vdoing | Copyright © 2021-2025 dwfrost | 粤ICP备2021118995号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式
    ×