唐抉的个人博客

前端框架之Vue.js(六)

字数统计: 8.4k阅读时长: 36 min
2022/11/11

可复用性&组合

混入

基础

混入用于分发Vue组件中是可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//定义一个混入对象
var myMixin={
created:function(){
this.hello()
},
methods:{
hello:function(){
console.log('hello from mixin!')
}
}
}
//定义一个使用混入对象的组件
var Component=Vue.extend({
mixins:[myMixin]
})
var component=new Component()
</script>

选项合并

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行合并。如数据对象在内部会进行递归合并,并在发生冲突时会以组件数据为优先:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var myMixin={
data:function(){
return{
message:'hello',
foo:'abc'
}
}
}
new Vue({
mixins:[myMixin],
data:function(){
return{
message:'goodbye',
bar:'def'
}
},
created:function(){
console.log(this.$data)
}
})
/*合并结果为:
{ message: "goodbye", foo: "abc", bar: "def" }*/

同名钩子函数将合并为一个数组,因此都将被调用。混入对象的钩子将在组件自身钩子之前被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var myMixin={
created:function(){
console.log('混入对象的钩子被调用')
}
}
new Vue({
mixins:[myMixin],
created:function(){
console.log('组件钩子被调用')
},
created:function(){
console.log(this.$data)
}
})
/*合并结果为:
混入对象的钩子被调用
组件钩子被调用*/

值为对象的选项如methodscomponentsdirectives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对:

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
var mixin={
methods:{
foo:function(){
console.log('foo')
},
conflicting:function(){
console.log('from mixin')
}
}
}
var vm=new Vue({
mixins:[mixin],
methods:{
bar:function(){
console.log('bar')
},
conflicting:function(){
console.log('from self')
}
}
})
vm.foo()
vm.bar()
vm.conflicting()
/*运行结果如下:
foo
bar
from self*/

注意:Vue.extend()也使用同样的策略进行合并

全局混入

混入也可以进行全局注册。一旦使用全局混入,它将影响每一个之后创建的Vue实例。恰当使用时,可以用来为自定义选项注入处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//为自定义的选项myOption注入一个处理器
Vue.mixin({
created:function(){
var myOption=this.$options.myOption
if(myOption){
console.log(myOption)
}
}
})
new Vue({
myOption:'hello!'
})
/*运行结果如下:
hello!*/

自定义选项合并策略

自定义选项合并将使用默认策略,即简单地覆盖已有值。若想让自定义选项以自定义逻辑合并,可以向Vue.config.optionMergeStrategies添加一个函数:

1
2
3
Vue.config.optionMergeStrategies.myOption=function(toVal,fromVal){
//返回合并后的值
}

对于多数值为对象的选项,可以使用与methods相同的合并策略:

1
2
var strategies=Vue.config.optionMergeStrategies
strategies.myOption=strategies.methods

过滤器

Vue.js允许自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和v-bind表达式。过滤器应该被添加在JavaScript表达式的尾部,由“管道符号”表示:

1
2
3
4
5
<!-- 在双花括号中 -->
{{message | capitalize}}

<!-- 在v-bind中 -->
<div v-bind:id="rawId | formatId"></div>

可以在一个组件的选项中定义本地的过滤器:

1
2
3
4
5
6
7
filters:{
capitalize:function(value){
if(!value) return ''
value=value.toString()
return value.charAt(0).toUpperCase()+value.slice(1)
}
}

也可以在创建Vue实例之前全局定义过滤器:

1
2
3
4
5
6
7
8
Vue.filters('capitalize',function(value){
if(!value) return ''
value=value.toString()
return value.charAt(0).toUpperCase()+value.slice(1)
})
new Vue({
//...
})

当全局过滤器和局部过滤器重名时,会采用局部过滤器。

过滤器函数总接收表达式的值作为第一个参数。如上述代码中,capitalize过滤器函数将会收到message的值作为第一个参数。

过滤器可以串联:

1
2
3
{{message}} | filterA | filterB}}
<!-- filterA为接收单个参数的过滤器参数,message值将作为参数传入到filterA中
然后调用fliterB,将filterA的结果传递到fliterB中 -->

过滤器是JavaScript函数,因此可以接收参数:

1
2
3
{{message}} | filterA('arg1',arg2)}}
<!-- filterA为接收三个参数的过滤器参数
message值将作为第一个参数,普通字符串arg1作为第二个参数,表达式arg2的值作为第三个参数 -->

从零开始简单的路由

若只需要非常简单的路由而不想引入一个功能完整的路由库,可以想这样动态渲染一个页面级的组件,结合HTML5 History API,便可以搭建一个客户端路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const NotFound={template:'<p>Page not found</p>'}
const Home={template:'<p>Home Page</p>'}
const About={template:'<p>About Page</p>'}

const routes={
'/':Home,
'/about':About
}
new Vue({
el:'#app',
data:{
currentRoute:window.location.pathname
},
computed:{
ViewComponent(){
return routes[this.crruentRoute]||NotFound
}
},
render(h){return h(this.ViewComponent)}
})

vue-router路由基础

对于大多数单页面应用,推荐使用官方支持的vue-router库。

下载安装

使用npm下载vue-router库:

1
npm install vue-router@4

在vue-router里,不是使用常规的<a>标签,而是使用一个自定义组件rounter-link来创建链接。这样Vue Router可以在不重新加载页面的情况下更改URL,处理URL的生成及编码。

rounter-view

rounter-view将显示与URL对应的组件,可以将其放在任何地方。

使用Vue Router创建单页应用例子:

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
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue@3"></script>
<script src="https://unpkg.com/vue-router@4"></script>
</head>

<body>
<div id="app">
<h1>Hello App!</h1>
<p>
<!-- 使用router-link组件进行导航 -->
<!-- 通过传递to来指定连接 -->
<!-- <rounter-link>将呈现以一个带有正确href属性的<a>标签 -->
<router-link to="/">Go to Home</router-link>
<router-link to="/about">Go to About</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
</body>

<script>
//定义路由组件,也可以从其他文件中导入
const Home={template:'<div>Home</div>'}
const About={template:'<div>About</div>'}

//定义一些路由.每个路由都需要映射到一个组件中
const routes=[
{path:'/',component:Home},
{path:'/about',component:About},
]

// 创建路由实例并传递routes配置
const router=VueRouter.createRouter({
// 内部提供了history模式的实现,为了简便,这里使用hash模式
history:VueRouter.createWebHashHistory(),
routes,//这句为routes:routes的缩写
})

// 创建并挂载根示例
const app=Vue.createApp({})

// 确保user路由实例使整个应用支持路由
// 在任意组件中能以this.$router的形式访问它,且能以this.$route的形式访问当前路由
app.use(router)

// 启动应用
app.mount('#app')

</script>
</html>

通过调用app.use(router),可以在任意组件中以this.$router的形式访问它,且能以this.$route的形式访问当前路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Home.vue文件中
export default{
computed:{
username(){
return this.$route.params.username
},
},
methods:{
goToDashboard(){
if(isAuthenticated){
this.$router.push('/dashboard')
}else{
this.$router.push('/login')
}
},
},
}

要在setup函数中访问路由,则可以调用useRouteruseRoute函数。

动态路由匹配

带参数的动态路由匹配

很多时候需要将给定匹配模式的路由映射到同一个组件中。例如有一个User组件,它应该对所有用户进行渲染,但用户ID不同。在Vue Router中,可以在路径中使用一个动态字段来实现,该字段称之为路径参数:

1
2
3
4
5
6
7
8
const User={
template:'<div>User</div>',
}
//这些都会传递给createRouter
const routes=[
//动态字段以冒号开始
{path:'/users/:id',component:User},
]

这样不同用户的URL都会映射到同一个路由上。

路径参数用冒号:表示。当一个路由被匹配时,它的params的值将在每个组件中以this.$route.params的形式暴露出来。因此可以通过更新User的模板来呈现当前的用户ID:

1
2
3
const User={
template:'<div>User{{$route.params.id}}</div>',
}

可以在同一个路由中设置有多个路径参数,它们会映射到$route.params上的相应字段上。

匹配模式 匹配路径 $route.params
/users/:username /users/eduardo { username: 'eduardo' }
/users/:username/posts/:postId /users/eduardo/posts/123 { username: 'eduardo', postId: '123' }

除了$route.params之外,route对象还公开了其他有用的信息,如route.query(若URL中存在参数)、$route.hash等。

相应路由参数的变化

使用带有参数的路由时需要注意的时,当用户从/users/johnny导航到/users/jolyne时,相同的组件实例将会被重复使用,这也意味着组件的生命周期钩子不会被调用。

要对同一个组件中参数的变化做出相应,可以用watch $route对象上的任意属性,如下列代码中是$route.params:

1
2
3
4
5
6
7
8
9
10
11
const User={
template:'...',
created(){
this.$watch(
()=>this.$route.params,
(toParams,previousParams)=>{
//对路由变化做出响应
}
)
},
}

或使用beforeRouteUpdate导航守卫,也可以取消导航:

1
2
3
4
5
6
7
const User={
template:'...',
async beforeRouteUpdate(to,from){
//对路由变化做出响应
this.userData=await fetchUser(to,params.id)
},
}

捕获所有路由或404 Not found路由

常规参数只匹配url片段之间的字符,用/分隔。若想匹配任意路径,可使用自定义的路径参数正则表达式,在路径参数后面的括号中加入正则表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const routes=[
//将匹配所有内容并将其放在$route.params.pathMatch下
//pathMatch标记为可选可重复,可以在需要时将path拆分成数组,直接导航到路由
{path:'/:pathMatch(.*)*',name:'NotFound',component:NotFound},
//将匹配以/user-开头的所有内容并将其放在$route.params.afterUser下
{path:'/user-:afterUser(.*)',component:UserGeneric},
]
this.$router.push({
name:'NotFound',
//保留当前路径并删除第一个字符,以避免目标URL以//开头
params:{pathMatch:this.$route.path.substring(1).split('/')},
//保留现有的查询和hash值
query:this.$route.query,
hash:this.$route.hash,
})

高级匹配模式

Vue Router使用自己的路径匹配语法,它支持许多高级匹配模式,如可选的参数,零或多个/一个或多个,甚至是自定义的正则匹配规则。

路由的匹配语法

在参数中自定义正则

当定义像:userId这样的参数时,在内部使用以下正则([^/]+)(至少有一个字符不是斜杠/)来从URL中提取参数。这个方法很好用,除非是需要根据参数的内容来区分两个路由,此时最简单的方法是在路径中添加一个静态部分来区分它们:

1
2
3
4
5
6
const routes=[
//匹配/0/3549
{path:'/o/:orderId'},
//匹配/p/books
{path:'/p/:productName'},
]

但在一些情况下,并不想添加静态的/o/p部分。由于orderId总是一个数字,而productName可以是任何东西,因此可以在括号中为参数指定一个自定义的正则:

1
2
3
4
5
6
7
const routes=[
// /:orderId只匹配数字
//'\\dd'是为了确保反斜杠能被转义出来
{path:'/:orderId(\\d+)'},
// /:productName匹配其他任何内容
{path:'/:productName'},
]

可重复的参数

若需要匹配具有多个部分的路由,如/first/second/third,则应该使用*(0个或多个)和+(1个或多个)将参数标记为可重复:

1
2
3
4
5
6
const routes=[
//匹配1个以上的参数,如/one,/one/two,/one/two/three等
{path:'/:chapters+'},
//匹配0个以上的参数,如/,/one,/one/two,/one/two/three等
{path:'/:chapters*'},
]

这将是提供一个参数数组而不是一个字符串,并且在使用命名路由时也需要传递一个数组:

1
2
3
4
5
//给定{path:'/:chapters*',name:'chapters'},
router.resolve({name:'chapters',params:{chapters:[]}}).href//产生路由/
router.resolve({name:'chapters',params:{chapters:['a','b']}}).href//产生路由/a/b
//给定{path:'/:chapters+',name:'chapters'},
router.resolve({name:'chapters',params:{chapters:[]}}).href//chapters为空,抛出错误

这些也可以通过右括号后添加它们与自定义正则结合使用:

1
2
3
4
5
6
const routes=[
//只匹配1个以上的数字,如/1,/1/2,/1/2/3等
{path:'/:chapters(\\d+)+'},
//匹配0个以上的数字,如/,/1,/1/2,/1/2/3等
{path:'/:chapters(\\d+)*'},
]

Sensitive与strict路由配置

默认情况下,所有路由是不区分大小写的,且能匹配带有或不带有尾部斜线的路由。这种行为可以通过sensitivestrict选项来修改,它们既可以应用在整个全局路由上,又可以应用在当前路由上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const router=createRouter({
history:createWebHistory(),
routes:[
//匹配/users/zhangsan
/*
当strict:true时,不匹配/users/zhangsan/
当sensitive:true时,不匹配/Users/zhangsan
*/
{path:'/users/:id',sensitive:true},
//匹配/users,/Users,以及/users/42,不匹配/users/或users/42/
{path:'/users/:id?'},
]
strict:true,//应用于所有路由
sensitive:true
})

可选参数

也可以通过使用?修饰符(0个或1个)将一个参数标记为可选:

1
2
3
4
5
6
const routers=[
//匹配/users和/users/zhangsan
{path:'/users/:userId?'},
//匹配/users和/users/42
{path:'/users/:userID(\\d)?'}
]

命名路由

除了path外,还可以为任何路由提供name。命名路由有以下优点:

  • 没有硬编码的URL
  • params的自动编码/解码
  • 防止在URL中出现打字错误
  • 绕过路径排序(如显示1个)
1
2
3
4
5
6
7
const route=[
{
path:'/user/:username',
name:'user',
component:User,
},
]

链接一个命名路由,可以向router-link组件的to属性传递一个对象:

1
2
3
4
5
6
<router-link :to="{name:'user',params:{username:'lisi'}}">User</router-link>
<!-- 等价于 -->
<script>
router.push({name:'user',params:{username:'lisi'}})
</script>
<!-- 两种方法路由都将导航到路径/user/lisi中 -->

嵌套路由

通过Vue Router可以使用嵌套路由配置来对于应用程序的多层嵌套组件结构:

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
<body>
<div id="app">
<!-- 这是一个顶层的router-view,其渲染顶层路由匹配的组件 -->
<router-view></router-view>
</div>
</body>

<script>
const User={
//被渲染的组件中包含自己嵌套的router-view
template:`
<div class="User">
<h2>User{{$route.params.id}}</h2>
<router-view></router-view>
</div>
`,
}
//这些传递给createRouter
const routes=[
{
//当/user/:id/profile匹配成功时,UserProfile将被渲染到User的router-view内部
path:'/user/:id',
component:User,
//将组件渲染到嵌套的router-view中
children:[
{
path:'profile',
component:UserProfile,
},
{
//当/user/:id/posts匹配成功时,UserPosts将被渲染到User的router-view内部
path:'posts',
component:UserPosts,
},
],
},
]
</script>
</html>

注意:以/开头的嵌套路径将被视为根路径,这便允许利用组件嵌套而不必使用嵌套URL。

上述代码中children的配置只是另一个路由数组。因此可以根据需要,不断地嵌套视图。

由于没有匹配到嵌套路由,当访问/user/eduardo时,在Userrouter-view里什么都不会呈现。若想在那里渲染一些东西,可以提供一个空的嵌套路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
const routes=[
{
path:'/user/:id',
component:User,
children:[
{ //当/user/:id匹配成功时,UserHome将被渲染到User的router-view内部
path:'',
component:UserHome
},
//其他子路由
],
},
]

嵌套命名路由

在处理命名路由时,通常会给子路由命名如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const routes=[
{
path:'/user/:id',
component:User,
children:[
{ //只有子路由具有名称
path:'',
name:'user',
component:UserHome
},
],
},
]

这将确保导航到/user/:id时始终显示嵌套路由。

若希望导航到命名路由而不导航到嵌套路由,还可以命名父路由,但要注意重新加载页面将始终显示嵌套的子路由,这是以为它被指向路径/users/:id的导航,而不是命名路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const routes=[
{
path:'/user/:id',
name:'user-parent',
component:User,
children:[
{
path:'',
name:'user',
component:UserHome
},
],
},
]

命名视图

命名视图可以同时展示多个视图而不是嵌套展示。一个界面中可以拥有多个单独命名的视图,而不是只有一个单独的出口。若router-view没有设置名字,则默认为default

1
2
3
<router-view class="view left-sidebar" name="LeftSidevar"></router-view>
<router-view class="view main-content"></router-view>
<router-view class="view right-sidebar" name="RightSidebar"></router-view>

一个视图使用一个组件渲染。因此在同一个路由下,多个视图就需要多个组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const router=createRouter({
history:createWebHashHistory(),
routes:[
{
path:'/',
components:{
default:Home,
//LeftSidebar:LeftSidebar的缩写
LeftSidebar,
//与<router-view>上的name属性相匹配
RightSidebar,
},
},
],
})

嵌套命名视图

当要实现切换路由的同时,其页面下的视图也要从一个UserEmailsSubscriptions切换成两个UserProfileUserProfilePreview,便应使用命名视图来创建嵌套视图的布局:

1
2
3
4
5
6
7
8
9
/settings/emails                                       /settings/profile
+-----------------------------------+ +------------------------------+
| UserSettings | | UserSettings |
| +-----+-------------------------+ | | +-----+--------------------+ |
| | Nav | UserEmailsSubscriptions | | +------------> | | Nav | UserProfile | |
| | +-------------------------+ | | | +--------------------+ |
| | | | | | | | UserProfilePreview | |
| +-----+-------------------------+ | | +-----+--------------------+ |
+-----------------------------------+ +------------------------------+
  • Nav 是一个常规组件
  • UserSettings 是一个视图组件
  • UserEmailsSubscriptionsUserProfileUserProfilePreview 是嵌套的视图组件

UserSetting组件的<template>部分大致如下:

1
2
3
4
5
6
<div>
<h1>User Settings</h1>
<NavVar />
<router-view />
<router-view name="helper"/>
</div>

通过这个路由配置来实现上述布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const router=createRouter({
routes:[
{
path:'/settings',
component:UserSettings,
children:[{
path:'emails',
component:UserEmailsSubscriptions
},
{
path:'profile',
components:{
default:UserProfile,
helper:UserProfilePreview
}
}]
},
],
})

编程式导航

使用router.push导航到不同的位置

若想要导航到不同的URL,可以使用router.push方法向history栈添加一个新的记录,当用户点击浏览器后退按钮时,会回到之前的URL。

当点击<router-link>时,内部会调用router.push这个方法。因此点击<router-link :to="...">便相当于调用router.push(...)

声明式 编程式
<router-link :to="..."> router.push(...)

注意:在Vue实例中,可以通过$router访问路由实例,因此也可以在实例中调用this.$router.push

router.push方法的参数可以时一个字符串路径,或者一个描述地址的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 字符串路径
router.push('/users/zhangsan')
// 带有路径的对象
router.push({path:'/users/zhangsan'})
// 命名的路由,并加上参数,让路由建立url
// 注意params不能与path一起使用,若提供了path,params会被忽略,而query不会
router.push({name:'user',params:{username:'zhangsan'}})
// 带查询参数,其结果是/register?plan=private
router.push({path:'/register',query:{plan:'private'}})
// 带hash,其结果是/about#team
router.push({path:'/about',hash:'#team'})

const username='zhangsan'
// 可以手动建立url,但必须自己处理编码
router.push(`/user/${username}`)//其结果为/user/zhangsan
router.push({path:`/user/${username}`})//其结果为/user/zhangsan
// 使用name和params从自动url编码中获益
router.push({name:'user',params:{username}})//其结果为/user/zhangsan
//params不能与path一起使用
router.push({path:'/user',params:{username}})//其结果为/user

当指定params时,可提供stringnumber参数(或对于可重复从参数可以提供一个数组)。任何其他类型(如undefinedfalse等)都将被自动字符串化。对于可选参数,可以提供一个空字符串("")来跳过它。

由于属性torouter.push接收的对象种类相同,一次你两者的规则相同。

router.push和所有其他导航方法都会返回一个Promise,等到导航完成后才知道是成功还是失败。

使用router.replace替换当前位置

router.replacerouter.push不同的是,router.replace在导航时不会向history添加新记录,它直接取代了当前的条目。

声明式 编程式
<router-link :to="..." replace> router.replace(...)

可以直接使用router.replace,也可以在传递给router.pushrouteLocation中增加一个属性replace:true

1
2
3
router.replace({path:'/home'})
//等价于
router.push({path:'/home',replace:true})

使用router.go横跨历史

router.go采用一个整数作为参数,表示在历史堆栈中前进或后退多少步:

1
2
3
4
5
6
7
8
9
10
// 向前移动1条记录,与router.forward()作用相同
router.go(1)
// 向后移动1条记录,与router.back()作用相同
router.go(-1)
// 前进3条记录
router.go(3)

// 若没有那么多记录,静默失败,即不执行也不报错
router.go(-100)
router.gi(100)

重定向和别名

通过redirect属性实现重定向

通过routes实现重定向:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 将/home重定向到/
const routes=[{path:'home',redirect:'/'}]
// 重定向的目标也可以是一个命名的路由
const routes=[{path:'/home',redirect:{name:'homepage'}}]

// 重定向的目标还可以是一个方法,动态返回重定向目标
const routes=[
{
// 将/search/screens重定向到/search?q=screens
path:'search/:searchText',
redirect: to=>{
//方法接收目标路由作为参数
//return 重定向的字符串路径/路径对象
return {path:'/search',query:{q:to.parpams.searchText}}
},
},
{
path:'/search',
},
]

在写redirect时,可以省略component配置。由于组件从来没有被直接渲染过,因此没有组件要渲染,嵌套路由除外。若一个路由有childrenredirect属性,那么它也应该有component属性。

导航守卫没有应用在跳转路由上,仅应用在其目标上。在上述代码中,在/home路由中添加beforeEnter守卫也不会有任何效果。

相对重定向

即重定向到相对位置:

1
2
3
4
5
6
7
8
9
10
11
12
const routes=[
{
// 将/users/123/posts重定向到/users/123/profile
path:'users/:id/posts',
redirect: to=>{
//方法接收目标路由作为参数
//相对位置不以/开头
return 'profile'
//或是return {path:'profile'}
},
},
]

使用alias设置别名

/别名为/home,便意味着当用户访问/home时,URL仍然是/home,但会被匹配为用户正在访问/

1
const routes=[{path:'/',component:Homepage,alias:'/home'}]

通过别名可以自由地将UI结构映射到一个任意的URL,而不受配置的嵌套结构的限制。

别名以/开头,以使嵌套路径中的路径成为绝对路径,也可以用一个数组来提供多个别名:

1
2
3
4
5
6
7
8
9
10
const routes=[
{
path:'/users',
component:UsersLayout,
children:[
//为/users、/users/list、/people这3个URL呈现UserList
{path:'',component:UserList,alias:['/people','list']},
],
},
]

若路由有参数,则要确保在任何绝对别名中包含它们:

1
2
3
4
5
6
7
8
9
10
const routes=[
{
path:'/users:id',
component:UsersByIdLayout,
children:[
//为/users/123、/users/123/profile、/123这3个URL呈现UserDetails
{path:'profile',component:UserDetails,alias:['/:id','']},
],
},
]

关于SEO的注意事项:使用别名是,一定要定义规范链接

路由组件传参

将props传递给路由组件

在组件中使用$route会与路由紧密耦合,由于它只能用于特定的URL,这将限制了组件的灵活性。通过配置props来解除这种行为:

1
2
3
4
5
6
7
8
9
10
11
12
const User={
template:'<div>User{{$route.params.id}}</div>'
}
const routes=[{path:'/user/:id',component:User}]

//可以通过配置props将代码替换成如下所示
const User={
//添加一个与路由参数完全相同的prop名
props:['id'],
template:'<div>User{{id}}</div>'
}
const routes=[{path:'/user/:id',component:User,props:true}]

这允许在任何地方使用该组件,使得该组件更容易重用和测试。

布尔模式

props设置为true时,route.params将被设置为组件的props。

命名视图

对于有命名视图的路由,则必须为每个命名视图定义props配置:

1
2
3
4
5
6
7
const routes=[
{
path:'/user/:id',
components:{default:User,sidebar:Sidebar},
props:{default:true,sidebar:false}
}
]

对象模式

props是一个对象时,它将原样设置为组件props。当组件props是静态时很有用:

1
2
3
4
5
6
7
const routes=[
{
path:'/promotion/from-newsletter',
components:Promotion,
props:{newsletterPopup:false}
}
]

函数模式

创建一个返回props的函数,可以将参数转换为其他类型,将静态值与基于路由的值相结合等:

1
2
3
4
5
6
7
const routes=[
{
path:'/search',
components:SearchUser,
props:route=>({query:route.query.q})
}
]

URL/search?q=vue将传递{query:'vue'}作为props传给SearchUser组件。

尽可能保持props函数为无状态的,因此它只会在路由发生变化时起作用。若需要状态来定义props,建议使用包装组件。

不同的历史记录模式

创建路由实例时,允许在不同的历史模式中选中history配置。

Hash模式

hash模式是用createWebHashHistory()创建的:

1
2
3
4
5
6
7
8
import{createRouter,createWebHashHistory} from 'vue-router'

const router=createRouter({
history:createWebHashHistory(),
routes:[
//...
],
})

它在内部传递的实际URL之前使用了一个哈希字符(#)。由于这部分URL从未被发送到服务器中,因此不需要再服务器上进行任何特殊处理。不过它在SEO中确实有不会的影响。若担心这个问题,可以使用HTML5模式。

HTML5模式

createWebHistory()创建HTML5模式,推荐使用这个模式:

1
2
3
4
5
6
7
8
import{createRouter,createWebHistory} from 'vue-router'

const router=createRouter({
history:createWebHistory(),
routes:[
//...
],
})

当应用是单页的客户端应用时,若没有适当的服务器配置,用户在浏览器中直接访问URL会得到一个404错误。

要想解决这个问题,便是要在服务器上添加一个简单的回退路由。URL不匹配任何静态资源,则应提供与应用程序中index.html相同的页面。

服务器配置实例

假设正在从根目录提供服务。若要部署到子目录中,则应使用Vue CLI的publicPath配置和相关路由的base属性。除此之外还需要调整服务端,使其使用子目录而不是根目录。

如在原生Node.js中,应调整为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const http=require('http')
const fs=require('fs')
const httpPort=80

http
.createServer((req,res)=>{
fs.readFile('index.html','utf-8',(err,content)=>{
if(err){
console.log('We cannot open "index.html" file.')
}
res.writeHead(200,{
'Content-Type':'text/html;charset=utf-8',
})
res.end(content)
})
})
.listen(httpPort,()=>{
console.log('Server listening on:http://localhost:%s',httpPort)
})

配置完成后,所有未找到的路径都会显示index.html文件,因此应该在Vue应用程序中实现一个万能路由来显示404页面:

1
2
3
4
const router=createRouter({
history:createWebHistory(),
routes:[{path:'/:pathMatch(.*)',component:NotFoundComponent}],
})

若使用的是Node.js服务器,则可以通过在服务器端使用路由来匹配URL,若没有匹配到路由,则用404来回应,从而实现回退。

内在

深入响应式原理

Vue最独特的特性之一是其非侵入性的响应式系统。数据模型仅仅是普通的JavaScript对象。当修改它们时,视图会进行更新。

如何追踪变化

当一个普通的JavaScript对象传入Vue实例作为data选项式,Vue将遍历此对象的所有property并使用Object.defineProperty把这些property全部转为getter/setter。这些getter/setter对用户来说是不可见的。但在内部它们能够让Vue追踪依赖,并在property被访问和修改是同时变更。

需要注意的是,不同浏览器在控制台打印数据对象时,对getter/setter的格式化也不同。

每个组件实例都对应一个watcher实例,它会在组件渲染时把“接触”过的数据property记录为依赖,随后依赖项的setter触发时会通知watcher,从而使它关联的组件重新渲染。

检测变化的注意事项

由于JavaScript的限制,Vue不能检测数组和对象的变化。但也还是有一些方法来回避这些限制并保证它们的响应性。

对于对象

Vue无法检测property的添加或移除。由于Vue会在初始化实例时对property执行getter/setter转化,因此property必须在data对象上存在,才能让Vue将他转换为响应式的。

1
2
3
4
5
6
7
8
9
var vm=new Vue({
data:{
a:1
}
})
//vm.a是响应式的

vm.b=2
//vm.b是非响应式的

对于已经创建的实例,Vue不允许动态添加根级别的响应式property,但可以使用Vue.set(Object,propertName,value)方法向嵌套对象添加响应式property,如:

1
Vue.set(vm.someObject,'b',2)

除此之外还可以使用vm.$set实例方法,也是全局Vue.set方法的别名:

1
this.$set(this.someObject,'b',2)

有时需要为已有对象赋值多个新property,若使用Object.assign()_.extend(),则它们添加到对象上的新property不会触发更新。此时应该用原对象与要混合进入对象的property一起创建一个新的对象:

1
2
// 代替Object.assign(this.someObject,{a:1,b:2})
this.someObject=Object.assign({},this.someObject,{a:1,b:2})

对于数组

Vue不能检测以下数组的变动:

  1. 利用索引直接设置一个数组项,如vm.items[indexOfItem]=newValue
  2. 修改数组长度,如vm.items.length=newLength
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var vm=new Vue({
data:{
items:['a','b','c']
}
})
// 非响应式的
vm.items[1]='x'
vm.items.length=2

// 响应式的
//Vue.set方法设置数组项
Vue.set(vm.items,indexOfItem,newValue)
//vm.$set实例方法设置数组项,是Vue.set方法的一个别名
vm.$set(vm.items,indexOfItem,newValue)
//Array.prototype.splice方法设置数组项
vm.items.splice(indexOfItem,1,newValue)

//splice方法设置数组长度
vm.items.splice(newLength)

声明响应式property

由于Vue不允许动态添加根级响应式property,因此必须要在初始化实例之前声明所有根级响应式property,包括空值

1
2
3
4
5
6
7
8
9
var vm=new Vue({
data:{
//声明message为一个空值字符串
message:''
},
template:'<div>{{message}}</div>'
})
// 给message赋值
vm.message='Hello!'

若未在data选项中声明message,Vue将警告渲染函数正在视图访问不存在的property。

异步更新队列

Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。若同一个watcher被多次触发,其也只会被推入到队列中一次。

Vue在缓冲时去除重复数据避免了不必要的计算和DOM操作。然后在下一个事件循环的“tick”中,Vue刷新队列并执行实际(已去重后的)工作。Vue在内部对异步队列尝试使用原生的Promise.thenMutationObserversetImmediate。若执行环境不支持,则会采用setTimeout(fn,0)代替。

为了在数据变化之后等待Vue完成更新DOM,可以在数据变化之后立即使用Vue.nextTick(callback),这样回调函数将在DOM更新完成后被调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<body>
<div id="example">
{{message}}
</div>
</body>

<script>
var vm=new Vue({
el:'#example',
data:{
message:'123'
}
})
vm.message='new message'//更改数据
console.log(vm.$el.textContent)//返回123
Vue.nextTick(function(){
console.log(vm.$el.textContent)//返回new message
})

在组件内使用vm.$nextTick()实例特别方便,因此它不需要全局Vue,且回调函数中的this将自动绑定在当前的Vue实例上:

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
<body>
<div id="example">
<example-oi></example-oi>
</div>
</body>

<script>
Vue.component('example-oi',{
data:function(){
return{
message:'未更新'
}
},
template:`
<div>
<button v-on:click="updateMessage">点击我更新文字</button>
<span>{{message}}</span>
</div>
`,
methods:{
updateMessage:function(){
this.message='已更新'
console.log(this.$el.textContent)
this.$nextTick(function(){
console.log(this.$el.textContent)
})
},
},
})
new Vue({
el:'#example'
})

</script>

因为$nextTick()返回一个Promise对象,因此可以使用新的ES2017 async/await 语法来完成相同的事情:

1
2
3
4
5
6
7
8
methods:{
updateMessage:async function(){
this.message='已更新'
console.log(this.$el.textContent)
await this.$nextTick()
console.log(this.$el.textContent)
},
}

vue-router路由进阶

导航守卫

vue-router提供的导航守卫主要通过跳转或取消的方式来守卫导航。

全局前置守卫

可以使用router.beforeEach注册一个全局前置守卫:

1
2
3
4
5
6
7
8
const router=createRouter({
//...
})
router.beforeEach((to,from)=>{
//...
//返回false以取消导航
return false
})

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫resolve完之前,一直处于等待中。

CATALOG
  1. 1. 可复用性&组合
    1. 1.1. 混入
      1. 1.1.1. 基础
      2. 1.1.2. 选项合并
      3. 1.1.3. 全局混入
      4. 1.1.4. 自定义选项合并策略
    2. 1.2. 过滤器
  2. 2. 从零开始简单的路由
  3. 3. vue-router路由基础
    1. 3.1. 下载安装
    2. 3.2. rounter-link
    3. 3.3. rounter-view
    4. 3.4. 动态路由匹配
      1. 3.4.1. 带参数的动态路由匹配
      2. 3.4.2. 相应路由参数的变化
      3. 3.4.3. 捕获所有路由或404 Not found路由
      4. 3.4.4. 高级匹配模式
    5. 3.5. 路由的匹配语法
      1. 3.5.1. 在参数中自定义正则
      2. 3.5.2. 可重复的参数
      3. 3.5.3. Sensitive与strict路由配置
      4. 3.5.4. 可选参数
    6. 3.6. 命名路由
      1. 3.6.1. 嵌套路由
      2. 3.6.2. 嵌套命名路由
    7. 3.7. 命名视图
      1. 3.7.1. 嵌套命名视图
    8. 3.8. 编程式导航
      1. 3.8.1. 使用router.push导航到不同的位置
      2. 3.8.2. 使用router.replace替换当前位置
      3. 3.8.3. 使用router.go横跨历史
    9. 3.9. 重定向和别名
      1. 3.9.1. 通过redirect属性实现重定向
      2. 3.9.2. 相对重定向
      3. 3.9.3. 使用alias设置别名
    10. 3.10. 路由组件传参
      1. 3.10.1. 将props传递给路由组件
      2. 3.10.2. 布尔模式
      3. 3.10.3. 命名视图
      4. 3.10.4. 对象模式
      5. 3.10.5. 函数模式
    11. 3.11. 不同的历史记录模式
      1. 3.11.1. Hash模式
      2. 3.11.2. HTML5模式
      3. 3.11.3. 服务器配置实例
  4. 4. 内在
    1. 4.1. 深入响应式原理
      1. 4.1.1. 如何追踪变化
      2. 4.1.2. 检测变化的注意事项
        1. 4.1.2.1. 对于对象
        2. 4.1.2.2. 对于数组
      3. 4.1.3. 声明响应式property
      4. 4.1.4. 异步更新队列
  5. 5. vue-router路由进阶
    1. 5.1. 导航守卫
      1. 5.1.1. 全局前置守卫