写在前面
近几年单页应用(SPA)越来越来火,也成为了主流,其中Vue,React,Angular就是典型的代表。将导航的实现放在了客户端去处理,极大提升了用户体验,给人一直类似原生应用的效果。下面我们就简单了解下。
web应用中的导航
传统导航
在传统Web应用中,导航是一个页面为单位进行。在地址栏输入路径,页面请求会发往服务器,服务器响应并返回一个完整的HTML页面。浏览器收到HTML页面进行渲染。要显示新的内容,浏览器往往要执行一次完整的刷新动作。
单页应用导航
在单页应用中通常就一个index.html文件。在用户切换导航的时候视图无缝呈现。更像是原生应用。一旦页面加载完成,后续的操作都不需要刷新页面。
在单页应用中,路由承载了管理应用的程序状态,业务以及数据的状态。和服务器的往返交互已经不是必须。路由通过监测路由位置的变化,就会从路由的配置项来匹配新的URL需要显示的部分,然后将其渲染。所有的路由都在浏览器端完成。
路由的工作机制
在单页应用中,客户端的路由都有以下一些特性:
- 通过路由定义的路径来匹配URL模式
- 当匹配成功是允许应用程序执行代码
- 当路由触发时允许执行需要显示的视图
- 允许通过路由路径传入参数
- 允许用户使用浏览器的导航方法进行单页应用进行导航
下面介绍下两种路由的导航方式:
片段标识符
路由可以利用 location
对象以编程方式访问当前路由,并且可以通过 window
的 onhashchange
对路由的变化进行监听。
假设需要跳转到关于的页面,可以利用如下标签:
<a href="#/about">关于</a>
对应的地址栏的URL会变成,http://localhost:8080/#/about
。
在路由变化时,可以在监听到从从更新局部页面的渲染。
html5 history
在 history
模式中有 pushState
和 replaceState
两个方法:
pushState()
: 添加历史条目replaceState()
: 替换已有历史条目
通过两个方法可以修改浏览器历史记录栈:
history.pushState({}, null, 'about')
之后的路由会变化为:http://localhost:8080/about
然后在 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
中,可以查看到当前路由信息,包括 path
, name
等配置。
同时,还提供了两个全局组件 router-view
和 router-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()
注册插件,另外,注册 RouterView
、 RouterLink
两个全局组件。并将其暴露出去:
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
里面做的事情也比较简单:
- 保存当前的路由模式,存储路由信息
- 将路由配置表转化为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
中还有很多复杂的用法。大家可以参考官方文档...
参考文章:
- 《SPA设计与架构》中单页面导航部分
- MDN-history相关
- MDN-HashChangeEvent相关
- vue-router
写在最后
能看到这儿的都是人才,感谢!!!
欢迎大家批评指正!!!另外,能否点个赞,评个论!!!
最后附上github连接点击查看, 顺便求个Star!!!
生活不易,大家加油!!!