SPA中的路由导航

2,416 阅读4分钟

写在前面

近几年单页应用(SPA)越来越来火,也成为了主流,其中Vue,React,Angular就是典型的代表。将导航的实现放在了客户端去处理,极大提升了用户体验,给人一直类似原生应用的效果。下面我们就简单了解下。

web应用中的导航

传统导航

在传统Web应用中,导航是一个页面为单位进行。在地址栏输入路径,页面请求会发往服务器,服务器响应并返回一个完整的HTML页面。浏览器收到HTML页面进行渲染。要显示新的内容,浏览器往往要执行一次完整的刷新动作。

单页应用导航

在单页应用中通常就一个index.html文件。在用户切换导航的时候视图无缝呈现。更像是原生应用。一旦页面加载完成,后续的操作都不需要刷新页面。

在单页应用中,路由承载了管理应用的程序状态,业务以及数据的状态。和服务器的往返交互已经不是必须。路由通过监测路由位置的变化,就会从路由的配置项来匹配新的URL需要显示的部分,然后将其渲染。所有的路由都在浏览器端完成。

路由的工作机制

在单页应用中,客户端的路由都有以下一些特性:

  • 通过路由定义的路径来匹配URL模式
  • 当匹配成功是允许应用程序执行代码
  • 当路由触发时允许执行需要显示的视图
  • 允许通过路由路径传入参数
  • 允许用户使用浏览器的导航方法进行单页应用进行导航

下面介绍下两种路由的导航方式:

片段标识符

路由可以利用 location 对象以编程方式访问当前路由,并且可以通过 windowonhashchange 对路由的变化进行监听。

假设需要跳转到关于的页面,可以利用如下标签:

<a href="#/about">关于</a>

对应的地址栏的URL会变成,http://localhost:8080/#/about

在路由变化时,可以在监听到从从更新局部页面的渲染。

hashchange事件查看

html5 history

history 模式中有 pushStatereplaceState 两个方法:

  • pushState(): 添加历史条目
  • replaceState(): 替换已有历史条目

通过两个方法可以修改浏览器历史记录栈:

history.pushState({}, null, 'about')

之后的路由会变化为:http://localhost:8080/about

然后在 popstate 事件中对路由变化进行监听,然后更新页面视图。

popstate事件查看

相比hash模式,更加美观,也是目前看到比较多的路由类型。但同时需要服务器配置,在使用标签或页面刷新时,服务器需要配置重定向到相同的URL。该方式不兼容一些老式的浏览器。

实现一个简版的vue-router

先看看怎么使用 vue-router

main.js 中,我们一般可以这样写:

// 路由队形的组件
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 路由表
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]
// 创建 router 实例,然后传入路由表
const router = new VueRouter({
  mode: 'hash', // 使用hash模式
  routes
})

// 创建和挂载根实例
const app = new Vue({
  router
}).$mount('#app')

然后,在每个组件中,可以通过 $router 获取到 router 实例, 然后他可以调用 push , go 等方法。
$route 中,可以查看到当前路由信息,包括 pathname 等配置。

同时,还提供了两个全局组件 router-viewrouter-link:

  • router-view 渲染路由匹配到的组件
  • router-link 实现导航

主要思路

实现的大致思路如下:

具体目录如下:

│-- History.js    // 存放路由信息
│-- index.js      // 导出并添加install方法
│-- MyRouter.js   // Router实例
└─components   
  │-- RouterLink.js   // router-link 组件
  └─  RouterView.js   // router-view 组件

首先在 index.js 中, 这里我们给 Router 添加 install 方法,这样就可以使用 Vue.use() 注册插件,另外,注册 RouterViewRouterLink 两个全局组件。并将其暴露出去:

import RouterView from './components/RouterView'
import RouterLink from './components/RouterLink'
import Router from './MyRouter'

Router.install = function (Vue) {
  // todo: 为组件添加 $router 和 $route
  // 全局注册组件
  Vue.component('RouterView', RouterView)
  Vue.component('RouterLink', RouterLink)
}

export default Router

Router 里面做的事情也比较简单:

  1. 保存当前的路由模式,存储路由信息
  2. 将路由配置表转化为map
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]

转化为:

this.routesMap = {
  '/foo': Foo,
  '/bar': Bar,
}

这样可以利用当前路径获取到要显示的组件
3. 存储当前路由
4. 绑定路由监听事件
5. 实现一些 push, go等之类的方法,用于组件控制路由

具体的话,可以参考下面的代码:

import History from './History'

export default class Router {
  constructor (options) {
    // hash模式或history模式
    this.mode = options.mode || 'hash'
    // 接收路由表
    this.routes = options.routes || []
    // 将路由表转化为map
    this.routesMap = this.createMap(this.routes)
    // 保存当前路由
    this.history = new History()
    this.init()
  }
  // 将路由表转化为map
  createMap (routes) {
    return routes.reduce((memo, current) => {
      memo[current.path] = current.component
      return memo
    }, {})
  }
  // 判断是不是hash路由
  isHashMode () {
    return this.mode === 'hash'
  }
  // 初始化方法,绑定监听事件
  init () {
    if (this.isHashMode()) {
      // hash模式
      this.initHash()
    } else {
      // history模式
      this.initHistory()
    }
  }
  // hash模式绑定监听事件
  initHash () {
    !location.hash && (location.hash = '/')
    window.addEventListener('load', () => {
      this.history.current = location.hash.slice(1)
    })
    window.addEventListener('hashchange', () => {
      this.history.current = location.hash.slice(1)
    })
  }
  // history模式绑定监听事件
  initHistory () {
    !location.pathname && (location.pathname = '/')
    window.addEventListener('load', () => {
      this.history.current = location.pathname
    })
    window.addEventListener('popstate', () => {
      this.history.current = location.pathname
    })
  }
  // 前进后退方法
  go (n) {
    window.history.go(n)
  }
  // 路由跳转
  push (path) {
    if (!this.isHashMode()) {
      // history模式
      history.pushState({ }, '', path)
      this.history.current = path
    } else {
      // hash模式
      window.location.hash = path
    }
  }
}

下面给组件添加 $router$route:

  • $router: 其实就是在根实例中 router, 也就是上面文件中的 Router
  • $route: 存储当前路由的一些信息, path , name

install 中,我们可以获取到 Vue 实例;

在所有组件中,只有根组件有 router 属性, 这样就可以拿到 Router 实例,里面的 history 就存放当前路由信息。
在vue 组件的渲染顺序, 根组件->父组件->子组件->孙子组件-> ...
同时,可以通过 $parent 获取到父组件。在每个组件中 _root 都拿取父组件的 _root。这样就可以在每个组件中拿取到 Router 实例。
这里使用了 vue 中的 Vue.util.defineReactive 会数据进行了响应式绑定。这样在后面数据更新时,页面就可以变化。
具体可以查看相关文档defineReactive方法

Router.install = function (Vue) {
  Vue.mixin({
    beforeCreate () {
      if (this.$options && this.$options.router) { // 根实例
        this._root = this
        this._router = this.$options.router
        Vue.util.defineReactive(this, '$history', this._router.history)
      } else {
        // 拿取父组件的_root
        this._root = this.$parent._root
      }

      Object.defineProperty(this, '$router', { // router 的实例
        get () {
          return this._root._router
        }
      })

      Object.defineProperty(this, '$route', { // current 为当前路由信息
        get () {
          return {
            current: this._root.$history.current
          }
        }
      })
    }
  })
  // 全局注册组件
  Vue.component('RouterView', RouterView)
  Vue.component('RouterLink', RouterLink)
}

下面就完成两个全局组件:
这里需要用到 渲染函数

RouterView: 作用就是根据当前path渲染对应的组件。
对应代码如下:

export default {
  name: 'RouterView',
  render (h) {
    // 当前路由
    let current = this._root._router.history.current
    // 路由map对象
    let routesMap = this._root._router.routesMap
    return (
      h(routesMap[current])
    )
  }
}

RouterLink: 作用就是对路由进行导航,类似于a标签。
一般会用到两个参数:

  • to:必填 需要跳转的path
  • tag: 非必填 指定要渲染的标签类型 默认为 a
    对应代码如下:
export default {
  name: 'RouterLink',
  props: {
    tag: {
      type: String,
      default: 'a'
    },
    to: {
      type: String,
      required: true
    }
  },
  methods: {
    handleClick () {
      this.$router.push(this.to)
    }
  },
  render: function (h) {
    let obj = {}
    if (this.tag === 'a' && this.$router.mode === 'hash') {
      obj.attrs = {
        href: '#' + this.to
      }
    } else {
      obj.on = {
        click: this.handleClick
      }
    }
    return h(
      this.tag,
      obj,
      this.$slots.default
    )
  }
}

最后成果

最后预览下效果:

上面只是简单的实现了一下,在 vue-router 中还有很多复杂的用法。大家可以参考官方文档...

参考文章:

  1. 《SPA设计与架构》中单页面导航部分
  2. MDN-history相关
  3. MDN-HashChangeEvent相关
  4. vue-router

写在最后

能看到这儿的都是人才,感谢!!!
欢迎大家批评指正!!!另外,能否点个赞,评个论!!!
最后附上github连接点击查看, 顺便求个Star!!!

生活不易,大家加油!!!