不使用vue-router的情况
代码
官方给出下面的例子在不使用vue-router的情况下来实现一个路由。
该示例结合了H5历史管理API、单文件组件、JS模块相关内容来实现路由
后面说的页面只是一条浏览器历史记录,关于历史记录管理见历史记录管理(window.history)
simple_router.js为入口,进行组件的加载,和其他一些功能初始化工作。
//simple_router.js
data: {
currentRoute: window.location.pathname
},
computed: {
ViewComponent() {
const matchingView = routes[this.currentRoute]
return matchingView
? require('./pages/' + matchingView + '.vue').default
: require('./pages/404.vue').default
}
},
render(h) {
return h(this.ViewComponent)
}
属性currentRoute
是一个状态转换器(在VLink.vue
中改变状态),在下层组件中通过改变它来随时切换页面。它在转换页面前,会去路由表中查找对应的页面路径。
routes.js为路由表
//routes.js
export default {
'/':'Home',
'/about':'About'
}
每个路由的页面都是一个组件,切换页面就是在这些组件中切换。要来回切换这些组件需要先加载它们,我们在ViewComponent
计算属性中完成这个工作,通过currentRoute
定位好对应的组件,在通过require(组件路径).default
在运行时动态的加载它,并在渲染函数中使用它。
特别注意的是,如果你使用vue-loader@1.4+
版本,这个require后面要加上default,否则报个加载模板失败的错误,因为在这个版本及更高版本上,vue
文件导出的全是esModule,而require加载的是commonjs格式的模块,在这里具体就是它返回的是一个包含default
的对象的对象。
具体查看vue-loader API esModule选项、ES6入门模块部分、记升级vue-loader版本时遇到的一个坑
这里入口中最重要的是别忘了,使用window.onpopstate管理历史记录,激活后退按钮的功能
window.addEventListener('popstate', () => {
app.currentRoute = window.location.pathname
})
之后就是定义需要切换的几个组件,Home.vue
、About.vue
和404.vue
,它们大同小异
<!--Home.vue-->
<template>
<main-layout>
<p>Welcome home</p>
</main-layout>
</template>
<script>
import MainLayout from '../layouts/Main.vue'
export default {
components: {
MainLayout
}
}
</script>
这些组件通过Main.vue
完成具体的布局
<!--Main.vue-->
<template>
<div class="container">
<ul>
<li>
<v-link href="/">Home</v-link>
<v-link href="/about">About</v-link>
</li>
</ul>
<slot></slot>
</div>
</template>
<script>
import VLink from '../components/VLink.vue'
export default {
components: {
VLink
}
}
</script>
接着就是在Main.vue
中使用子组件VLink.vue
<!--VLink.vue-->
<template>
<a
v-bind:href="href"
v-bind:class="{ active: isActive }"
v-on:click="go"
>
<slot></slot>
</a>
</template>
<script>
import routes from '../routes'
export default {
props: {
href: {
type:String,
required: true
}
},
computed: {
isActive () {
return this.href === this.$root.currentRoute
}
},
methods: {
go (event) {
event.preventDefault()
this.$root.currentRoute = this.href
window.history.pushState(
null,
routes[this.href],
this.href
)
}
}
}
</script>
VLink.vue
很也是很关键,定义了一个链接,在开始入口文件中的currentRoute
这个状态转换器,就是通过点击链接改变状态的,而这个改变值this.href
是通过布局组件Main.vue
Props数据下发下来的(<v-link href="/about">
),最后别忘记把这个新状态作为URL推入历史记录管理的状态栈中。
总结下就是 通过simple_router.js
定义如何加载页面(运行时动态加载),通过VLink.vue
触发这个加载,通过布局文件将这两部分结合在一起。其中夹杂了历史记录的管理。
使用vue-router的情况
该插件整体结构比较简单,而细节非常繁琐(用来解决各种各样的缺陷和bug)。
由命名路由与子路由构成整体结构,我们用它构建如下页面。
目录结构如下
//router.js
export default new VueRouter({
routes: [
{ path: '/', component: Home },
{
path: '/settings', component: UserSettings,
children: [
{
path: 'userinfo',
component:UserInfo
},
{
path: 'useremail',
component:UserEmail
}
]
},
{
path: '/interaction', component: UserInteraction,
children: [
{ path: 'userfriend', component: UserFriend },
{ path: 'userFollow', component: UserFollow}
]
}
]
})
顶层路由及其对应的组件
<!-- index.html -->
<div id="app">
<h1>vue-router 测试页</h1>
<div class="nav-header">
<router-link to="/">用户首页</router-link>
<router-link to="/settings">用户设置</router-link>
<router-link to="/interaction">社交管理</router-link>
</div>
<router-view></router-view>
</div>
<!-- UserSettings.vue -->
<template>
<div class="container">
<ul class="nav-left">
<router-link tag="li" to="/settings/userinfo"><a>基本信息</a></router-link>
<router-link tag="li" to="/settings/useremail"><a>更换邮箱</a></router-link>
</ul>
<div class="main-right">
<router-view></router-view>
</div>
</div>
</template>
<!-- UserInteraction.vue -->
<template>
<div class="container">
<ul class="nav-left">
<router-link tag="li" to="/interaction/userfriend"><a>我的好友</a></router-link>
<router-link tag="li" to="/interaction/userfollow"><a>关注的人</a></router-link>
</ul>
<div class="main-right">
<router-view></router-view>
</div>
</div>
</template>
第二层路由(子路由)及其对应组件,这里。
以下两张图说明路由和子路由是如何工作的。
第一张图说明当我们点击链接,经过路由就可以把对应的组件,放到页面指定的<router-view>
中。而同样经过子路由就可以把对应的组件,放到顶层组件中指定的<router-view>
中。
而第二张,就是对路由过程的补充,通过路由或子路由去寻找对应组件,找到的组件再反过来放入视图中。
命名路由
有时候我们需要在一个组件中使用多块<router-view>
,以便增加组件的重用,就可以使用命名路由。
比如我们需要在用户设置中的基本信息
和更换邮箱
中都加一段广告。
增加一个广告组件Advertisement.vue
<!-- pages/common/Advertisement.vue -->
<template>
<div class="adver">
<h1>某广告</h1>
</div>
</template>
在UserSettings.vue
中添加一段命名的<router-view>
<!-- UserSettings.vue -->
<div class="main-right">
<router-view></router-view>
<!-- 添加一段视图 -->
<router-view name="adver"></router-view>
</div>
在路由文件中为命名的<router-view>
添加命名路由。default
路由对应那些未命名的<router-view>
//router.js
//...
children: [
{
path: 'userinfo',
components: {
default:UserInfo,
//添加命名路由
adver:Advertisement
}
},
{
path: 'useremail',
components: {
default:UserEmail,
//添加命名路由
adver:Advertisement
}
}
]
//...
具体代码 添加命名路由
为激活的链接添加样式
被激活的链接,vue-router会为其添加样式类router-link-active
,我们在这个class中可以为其添加具体样式,上面代码中已经被添加了样式。
a.router-link-active,
li.router-link-active>a {
background-color: gainsboro;
color: #37C6C0;
}
这里被分成两种情况
当这种不带tag
的写法
<router-link to="/">用户首页</router-link>
被解析成
带tag
的写法
<router-link tag="li" to="/settings/userinfo"><a>基本信息</a></router-link>
被解析成
类router-link-active
被添加在tag
上。
细心的你可以发现不管哪个链接被激活,用户首页上始终存在着router-link-active
,这是因为它的路由是/
,所以在其他路由被解析时,/
也会被匹配。我们可以用exact
解决这个问题,它使路径字符串要完全匹配。
<router-link to="/" exact>用户首页</router-link>
动态路由模糊匹配
有时候一堆同级路由它们所对应的组件基本相同,我们就可以使用动态路由,匹配到同一个组件。
我们在我的好友
中有一个好友列表。当点击某好友会显示他的信息。
下面只使用了最简单的匹配模式,关于更复杂的匹配,vue-router使用path-to-regexp
动态匹配请求路径,具体查看PocketLibs(2)—— 请求相关 path-to-regexp
//router.js
//......
{
path: '/interaction', component: UserInteraction,
children: [
{
path: 'userfriend', component: UserFriend,
//添加好友信息 为组件FriendInfo添加动态路由
children: [
{ path:'fd/:id',component:FriendInfo}
]
},
{ path: 'userFollow', component: UserFollow}
]
}
//......
修改组件UserFriend.vue
,添加好友列表与好友信息视图
<!-- UserFriend.vue -->
<template>
<div class="friend-list">
<h3>好友列表</h3>
<ul>
<li v-for="item in 10" :key="item">
<router-link :to="'/interaction/userfriend/fd/' + item ">好友{{item}}</router-link>
</li>
</ul>
<div>
<!--好友信息视图-->
<router-view></router-view>
</div>
</div>
</template>
添加好友信息组件FriendInfo.vue
<!-- FriendInfo.vue -->
<template>
<div>好友{{$route.params.id}}信息</div>
</template>
使用命名组件和Props改进我的好友模块
在刚才的代码中,UserFriend.vue
中有以下代码
<li v-for="item in 10" :key="item">
<router-link :to="'/interaction/userfriend/fd/' + item ">好友{{item}}</router-link>
</li>
:to="'/interaction/userfriend/fd/' + item "
有点违和。我们可以用命名路由改造一下。
先在路由文件中为用户信息对应的路由添加成name
属性,切换成命名路由。
//router.js
{
path: 'userfriend', component: UserFriend,
//添加好友信息 为组件FriendInfo添加动态路由
children: [
//为动态路由添加name
{ path:'fd/:id',name:'fd',component:FriendInfo}
]
}
然后修改UserFriend.vue
中的<router-link>
为
<!-- UserFriend.vue -->
<router-link :to="{name:'fd',params:{id:item}}">好友{{item}}</router-link>
效果与上面的一样。
我们再仔细看看组件FriendInfo.vue
<!--FriendInfo.vue-->
<template>
<div>好友{{$route.params.id}}信息</div>
</template>
在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。我们可以为路径参数添加Props数据下发。
继续修改好友信息的路由部分
//router.js
{
path: 'userfriend', component: UserFriend,
//添加好友信息 为组件FriendInfo添加动态路由
children: [
//为动态路由添加name
//为路径参数添加Props数据下发
{ path:'fd/:id',name:'fd',component:FriendInfo,props:true}
]
}
为组件FriendInfo
添加Props,并使用它。
<template>
<!-- <div>好友{{$route.params.id}}信息</div> -->
<div>好友{{id}}信息</div>
</template>
<script>
export default {
props: ['id']
}
</script>
在路由中使用Props的其他模式
如果在命名路由中使用Props数据下发,要为每一个对应组件,都设置Props。
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
Props还可以是一个对象
{ path:'fd/:id',name:'fd',component:FriendInfo,props:{id:1}}
还可以是一个函数,函数接收一个$route
对象
function dynamicPropsFn (route) {
const now = new Date()
return {
name: (now.getFullYear() + parseInt(route.params.years)) + '!'
}
}
//...
{ path: '/dynamic/:years', component: Hello, props: dynamicPropsFn }
钩子函数、请求数据和进度条
代码 vue-router 0.4 钩子、请求数据与全局进度条
我们使用axios发送请求,http://schematic-ipsum.heroku... 可以作为响应模拟一些基本的数据。
改造好友信息组件FriendInfo.vue
,我们在组件级钩子beforeRouteUpdate
中发送请求。
<template>
<!-- <div>好友{{$route.params.id}}信息</div> -->
<div class="friend-info">
好友{{id}}信息
<ul>
<li v-if="name">姓名:{{name}}</li>
<li v-if="phone">电话:{{phone}}</li>
<li v-if="email">邮件:{{email}}</li>
</ul>
</div>
</template>
<script>
import axios from 'axios'
const reqObject = {
"type": "object",
"properties": {
"name": {
"type": "string",
"ipsum": "name"
},
"phone": {
"type": "string",
"format": "phone"
},
"email": {
"type": "string",
"format": "email"
}
}
}
export default {
props: ["id"],
data(){
return {
name:null,
phone:null,
email:null,
}
},
beforeRouteEnter(to, from, next) {
console.log('beforeRouteEnter running..........')
axios.post("http://schematic-ipsum.herokuapp.com/", reqObject)
.then(response => {
next(vm => {
vm.name = response.data.name;
vm.phone = response.data.phone;
vm.email = response.data.email;
});
});
}
};
</script>
beforeRouteEnter
钩子中第三个参数next()
,调用它时,才可以继续其他操作(此时系统处于等待),我们在获取到响应时再调用它,因此在获取响应后才会看渲染效果。
仔细观察可以发现,多次查看好友信息,信息不会改变,查看控制台发现beforeRouteEnter
只调用了一次,只发送了一次信息。
首次查看信息时,解析路由并使组件FriendInfo
激活,我们调用beforeRouteEnter
,之后每次查看,组件不会重新被渲染,只会被重复利用,因此不会再调用beforeRouteEnter
。注意该钩子中不能使用this
,因为在执行它时组件还没有被实例,但是next()
的回调执行时,组件已被实例,它接收组件实例作为参数(示例中的vm)
解决这个问题就靠beforeRouteUpdate
,该钩子在当前路由改变,组件被复用时调用。比如从/interaction/userfriend/fd/1
到/interaction/userfriend/fd/2
时它就会调用。下面我们就在FriendInfo
中添加这个钩子。
beforeRouteUpdate(to, from, next) {
axios.post("http://schematic-ipsum.herokuapp.com/", reqObject)
.then(response => {
this.name = response.data.name;
this.phone = response.data.phone;
this.email = response.data.email;
next();
});
},
注意它是可以使用this
的,它的next()
不接收回调。
我们看下youtube
每次加载页面会有个进度条
我们现在就使用全局钩子函数和NProgress实现这个功能,在入口app.js文件中添加全局路由钩子。
import Vue from 'Vue'
import nprogress from 'nprogress'
import 'nprogress/nprogress.css'
import router from './router'
const app = function () {
//在任何导航被触发前执行
router.beforeEach((to, from, next) => {
console.log('beforeEach')
nprogress.start()
next()
})
//导航中的最后一个钩子
router.afterEach((to, from) => {
console.log('afterEach')
nprogress.done()
})
new Vue({
router,
el: '#app'
})
}
export { app }
给路由视图添加动画
代码 vue-router 0.5 为<router-view>添加动画
为显示好友信息添加动画
修改组件UserFriend
<!-- UserFriend.vue -->
<div>
<transition name="slide-left" mode="out-in">
<router-view class="child-view"></router-view>
</transition>
</div>
添加过渡类
.child-view {
position: absolute;
transition: all 1s cubic-bezier(.55,0,.1,1);
}
.slide-left-enter {
opacity: 0;
-webkit-transform: translate(60px, 0);
transform: translate(60px, 0);
}
.slide-left-leave-active {
opacity: 0;
-webkit-transform: translate(-60px, 0);
transform: translate(-60px, 0);
}
由于在切换查看信息时,组件FriendInfo
不会重新渲染,即除第一次外不会有动画,因此需要设置key
,这里有个不可预期的错误,即在<router-view>
上设置key。
<!-- UserFriend.vue -->
<router-view class="child-view" :key="$route.params.id"></router-view>
千万不能这么做,key
无法传递给FriendInfo
的根节点。
我们需要在组件FriendInfo
上设置key
//FriendInfo.vue
<div class="friend-info" :key="id">
重定向
代码 vue-router 0.6 重定向和别名
仿造我的好友
,构造关注的人
当点击单数用户时,显示用户信息,点击双数用户时,显示用户被销毁。
用户信息为一个组件,销毁为另一个组件。
路由配置如下:
{
path: 'userFollow', component: UserFollow,
children: [
{
path:'fw/:id', name:'fw',
redirect: to => {
if(to.params.id%2===0)
return 'fd/:id'
else
return 'fi/:id'
},
beforeEnter:(to, from, next) => {
console.log(from)
console.log(to)
next()
}
},
{
path:'fd/:id', component:UserDestroy, props:true
},
{
path:'fi/:id', component:FollowInfo, props:true
}
]
}
每个routerLink
的链接路径为fw/:id
,在redirect
中配置重定向函数,路径参数id
为单数重定向到路由fi/:id
,双数时,重定向到fd/:id
。redirect
还可以是个表示路由路径的字符串或命名路由对象({name:'foo',params:{bar:'baz'}})。
另外在redirect
属性所在的路由中,导航守卫不会执行,如上面的beforeEnter
不会执行(全局的守卫也不会执行)。
别名
我们增加一个顶级导航系统公告
<div class="nav-header">
<router-link to="/" exact>用户首页</router-link>
<router-link to="/settings">用户设置</router-link>
<router-link to="/interaction">社交管理</router-link>
<router-link to="/notification">系统公告</router-link>
</div>
为它添加路由
{
path: '/notification', component: SystemNotification
}
SystemNotification
组件只是一个简单的标题
<template>
<div>
<H3>系统公告</H3>
</div>
</template>
之后我想把这个页面改成我的消息页面
为此我们修改路由路径/usermessage
,将原来的名字配置为别名alias:'/notification'
,并保持routerLink
的路径不变。
这样做一是其他网站引用该页面不会产生404
,二是路由内部配置引用该路由也不会找不到。
{
path: '/usermessage', component: SystemNotification, alias:'/notification'
},
滚动行为
代码 vue-router 0.7 滚动行为
在我的消息
中配置路由及其视图,如下图:
配置4个路由及其对应组件
<H3>我的消息</H3>
<router-link to="/notification/system-msg">系统消息</router-link>
<router-link to="/notification/friend-msg">好友消息</router-link>
<router-link to="/notification/group-msg">用户组消息</router-link>
<router-link to="/notification/other-msg">其他消息</router-link>
<router-view></router-view>
path: '/usermessage', component: SystemNotification, alias: '/notification',
children: [
{ path: 'system-msg', component: SystemMessage, meta: { scrollToTop: false } },
{ path: 'friend-msg', component: FriendMessage, meta: { scrollToTop: true } },
{ path: 'group-msg', component: GroupMessage, meta: { scrollToTop: true } },
{ path: 'other-msg', component: OtherMessage, meta: { scrollToTop: true } }
]
四个组件中定义一系列字符串列表。在其中插入下一个消息列表的routerLink
,如
效果如下
使用历史记录回退时,保持原纪录位置,是正常的。但点击链接,跳到新页面,也是原来位置,这不是我们预期的行为,我们使用路由属性scrollBehavior
解决这个问题。它与属性routes
是同一级别的属性。
//router.js
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
const position = {}
if(to.hash) {
position.selector = to.hash
if(to.hash === "#anchor") {
position.offset = { y: 100 }
}
}
if (to.matched.some(m => m.meta.scrollToTop)) {
position.x = 0
position.y = 0
}
return position;
}
}
为了后面定位到锚点,我们改造FriendMessage
,为链接到下一个消息列表的连接添加hash#anchor
<li><router-link to="/notification/group-msg#anchor">用户组消息</router-link></li>
修改GroupMessage
<ul>
<li v-for="item in 29" :key="item">用户组消息{{item}}</li>
<li id="anchor" style="border:1px solid red;" key="30">用户组消息30</li>
<li v-for="item in 20" :key="item+30">用户组消息{{item+30}}</li>
<li><router-link to="/notification/system-msg">其他消息</router-link></li>
<li v-for="item in 30" :key="item+50">用户组消息{{item+50}}</li>
</ul>
scrollBehavior
接收3个参数to
、from
和savedPosition
。以上函数中savedPosition
当且仅当 popstate 导航 (通过浏览器的 前进/后退 按钮触发) 时可用,这里与默认的效果没区别。我们获取to
路由的元数据meta.scrollToTop
(保存自定义的数据),当为true时我们切换到to
对应的页面时,我们定位到{x:0,y:0}
,否则保持默认行为。如果to
存在hash,设置{selector:to.hash}
定位到锚点,可以具体定位锚点定位的偏移量{selector:to.hash,offset:{y:100}}
。
History模式
在以上所有的请求路径都带#
,这不是我们所期望的,但是可用于所有的浏览器,这种模式为默认的hash
模式,如:
http://localhost:8080/#/settings/useremail
现在使用history
模式,设置属性mode
mode:"history",
routes: [/*...*/],
scrollBehavior(to, from, savedPosition) {/*...*/}
现在路径就正常了,但它只能用于支持H5 History API的浏览器。
http://localhost:8080/settings/useremail
History API需要服务器的支持,否则当重载页面时,会发生404页面找不到,就像下面这样。
这里使用webpack-dev-server,设置webpack的devServer.historyApiFallback
为true,使其支持History API。如
devServer: {
historyApiFallback:true,
noInfo: true
},
其他服务器的配置见官方文档后端配置例子
然后又发现一个bug,像下面这样,显示http://localhost:8080/settings/app.js
找不到
其实这是个webpack的问题,插入js资源时,像下面这样
它是相对于当前请求的路径,为了解决这个问题,我们要在webpack中设置output
中的publicPath
属性为/
output: {
path: path.resolve(__dirname, './dist'),
publicPath: '/',
filename: '[name].js',
},
它在所有资源前加上虚拟路径/
,app.js
就变为绝对路径localhost:8080/app.js
,这下就没什么问题了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。