1

不使用vue-router的情况

代码
官方给出下面的例子在不使用vue-router的情况下来实现一个路由。
该示例结合了H5历史管理API、单文件组件、JS模块相关内容来实现路由

clipboard.png

后面说的页面只是一条浏览器历史记录,关于历史记录管理见历史记录管理(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.vueAbout.vue404.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.vueProps数据下发下来的(<v-link href="/about">),最后别忘记把这个新状态作为URL推入历史记录管理的状态栈中。

总结下就是 通过simple_router.js定义如何加载页面(运行时动态加载),通过VLink.vue触发这个加载,通过布局文件将这两部分结合在一起。其中夹杂了历史记录的管理。

使用vue-router的情况

该插件整体结构比较简单,而细节非常繁琐(用来解决各种各样的缺陷和bug)。

命名路由子路由构成整体结构,我们用它构建如下页面。

clipboard.png

目录结构如下

clipboard.png

代码在这vue-router v0.1

//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>

第二层路由(子路由)及其对应组件,这里

以下两张图说明路由和子路由是如何工作的。

clipboard.png

clipboard.png

第一张图说明当我们点击链接,经过路由就可以把对应的组件,放到页面指定的<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
        }
    }
]
//...

clipboard.png

具体代码 添加命名路由

为激活的链接添加样式

被激活的链接,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>

被解析成

clipboard.png

tag的写法

<router-link tag="li" to="/settings/userinfo"><a>基本信息</a></router-link>

被解析成

clipboard.png

router-link-active被添加在tag上。

细心的你可以发现不管哪个链接被激活,用户首页上始终存在着router-link-active,这是因为它的路由是/,所以在其他路由被解析时,/也会被匹配。我们可以用exact解决这个问题,它使路径字符串要完全匹配。

clipboard.png

<router-link to="/" exact>用户首页</router-link>

clipboard.png

动态路由模糊匹配

代码 v0.3 动态路由匹配与Props属性

有时候一堆同级路由它们所对应的组件基本相同,我们就可以使用动态路由,匹配到同一个组件。
我们在我的好友中有一个好友列表。当点击某好友会显示他的信息。

下面只使用了最简单的匹配模式,关于更复杂的匹配,vue-router使用path-to-regexp动态匹配请求路径,具体查看PocketLibs(2)—— 请求相关 path-to-regexp

clipboard.png

//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(),调用它时,才可以继续其他操作(此时系统处于等待),我们在获取到响应时再调用它,因此在获取响应后才会看渲染效果。

clipboard.png

仔细观察可以发现,多次查看好友信息,信息不会改变,查看控制台发现beforeRouteEnter只调用了一次,只发送了一次信息。

clipboard.png

首次查看信息时,解析路由并使组件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()不接收回调。

clipboard.png

我们看下youtube

clipboard.png

每次加载页面会有个进度条

clipboard.png

我们现在就使用全局钩子函数和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 }

clipboard.png

给路由视图添加动画

代码 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">

clipboard.png

重定向

代码 vue-router 0.6 重定向和别名
仿造我的好友,构造关注的人

clipboard.png

当点击单数用户时,显示用户信息,点击双数用户时,显示用户被销毁。
用户信息为一个组件,销毁为另一个组件。
路由配置如下:

{ 
    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/:idredirect还可以是个表示路由路径的字符串或命名路由对象({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>

clipboard.png

之后我想把这个页面改成我的消息页面

clipboard.png

为此我们修改路由路径/usermessage,将原来的名字配置为别名alias:'/notification',并保持routerLink的路径不变。

这样做一是其他网站引用该页面不会产生404,二是路由内部配置引用该路由也不会找不到。

{
    path: '/usermessage', component: SystemNotification, alias:'/notification'
},

滚动行为

代码 vue-router 0.7 滚动行为
我的消息中配置路由及其视图,如下图:

clipboard.png

配置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,如

clipboard.png

效果如下

clipboard.png

使用历史记录回退时,保持原纪录位置,是正常的。但点击链接,跳到新页面,也是原来位置,这不是我们预期的行为,我们使用路由属性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个参数tofromsavedPosition。以上函数中savedPosition当且仅当 popstate 导航 (通过浏览器的 前进/后退 按钮触发) 时可用,这里与默认的效果没区别。我们获取to路由的元数据meta.scrollToTop(保存自定义的数据),当为true时我们切换到to对应的页面时,我们定位到{x:0,y:0},否则保持默认行为。如果to存在hash,设置{selector:to.hash}定位到锚点,可以具体定位锚点定位的偏移量{selector:to.hash,offset:{y:100}}

clipboard.png

History模式

code 0.8

在以上所有的请求路径都带#,这不是我们所期望的,但是可用于所有的浏览器,这种模式为默认的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页面找不到,就像下面这样。

clipboard.png

这里使用webpack-dev-server,设置webpack的devServer.historyApiFallback为true,使其支持History API。如

devServer: {
    historyApiFallback:true,
    noInfo: true
},

其他服务器的配置见官方文档后端配置例子

然后又发现一个bug,像下面这样,显示http://localhost:8080/settings/app.js找不到

clipboard.png

其实这是个webpack的问题,插入js资源时,像下面这样

clipboard.png

它是相对于当前请求的路径,为了解决这个问题,我们要在webpack中设置output中的publicPath属性为/

output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '/',
    filename: '[name].js',
},

它在所有资源前加上虚拟路径/app.js就变为绝对路径localhost:8080/app.js,这下就没什么问题了。


yanniecheung
70 声望6 粉丝