可复用性&组合
混入
基础
混入用于分发Vue组件中是可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
1 | //定义一个混入对象 |
选项合并
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行合并。如数据对象在内部会进行递归合并,并在发生冲突时会以组件数据为优先:
1 | var myMixin={ |
同名钩子函数将合并为一个数组,因此都将被调用。混入对象的钩子将在组件自身钩子之前被调用。
1 | var myMixin={ |
值为对象的选项如methods、components和directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对:
1 | var mixin={ |
注意:Vue.extend()也使用同样的策略进行合并
全局混入
混入也可以进行全局注册。一旦使用全局混入,它将影响每一个之后创建的Vue实例。恰当使用时,可以用来为自定义选项注入处理逻辑。
1 | //为自定义的选项myOption注入一个处理器 |
自定义选项合并策略
自定义选项合并将使用默认策略,即简单地覆盖已有值。若想让自定义选项以自定义逻辑合并,可以向Vue.config.optionMergeStrategies添加一个函数:
1 | Vue.config.optionMergeStrategies.myOption=function(toVal,fromVal){ |
对于多数值为对象的选项,可以使用与methods相同的合并策略:
1 | var strategies=Vue.config.optionMergeStrategies |
过滤器
Vue.js允许自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和v-bind表达式。过滤器应该被添加在JavaScript表达式的尾部,由“管道符号”表示:
1 | <!-- 在双花括号中 --> |
可以在一个组件的选项中定义本地的过滤器:
1 | filters:{ |
也可以在创建Vue实例之前全局定义过滤器:
1 | Vue.filters('capitalize',function(value){ |
当全局过滤器和局部过滤器重名时,会采用局部过滤器。
过滤器函数总接收表达式的值作为第一个参数。如上述代码中,capitalize过滤器函数将会收到message的值作为第一个参数。
过滤器可以串联:
1 | {{message}} | filterA | filterB}} |
过滤器是JavaScript函数,因此可以接收参数:
1 | {{message}} | filterA('arg1',arg2)}} |
从零开始简单的路由
若只需要非常简单的路由而不想引入一个功能完整的路由库,可以想这样动态渲染一个页面级的组件,结合HTML5 History API,便可以搭建一个客户端路由:
1 | const NotFound={template:'<p>Page not found</p>'} |
vue-router路由基础
对于大多数单页面应用,推荐使用官方支持的vue-router库。
下载安装
使用npm下载vue-router库:
1 | npm install vue-router@4 |
rounter-link
在vue-router里,不是使用常规的<a>标签,而是使用一个自定义组件rounter-link来创建链接。这样Vue
Router可以在不重新加载页面的情况下更改URL,处理URL的生成及编码。
rounter-view
rounter-view将显示与URL对应的组件,可以将其放在任何地方。
使用Vue Router创建单页应用例子:
1 |
|
通过调用app.use(router),可以在任意组件中以this.$router的形式访问它,且能以this.$route的形式访问当前路由:
1 | //Home.vue文件中 |
要在setup函数中访问路由,则可以调用useRouter或useRoute函数。
动态路由匹配
带参数的动态路由匹配
很多时候需要将给定匹配模式的路由映射到同一个组件中。例如有一个User组件,它应该对所有用户进行渲染,但用户ID不同。在Vue
Router中,可以在路径中使用一个动态字段来实现,该字段称之为路径参数:
1 | const User={ |
这样不同用户的URL都会映射到同一个路由上。
路径参数用冒号:表示。当一个路由被匹配时,它的params的值将在每个组件中以this.$route.params的形式暴露出来。因此可以通过更新User的模板来呈现当前的用户ID:
1 | const User={ |
可以在同一个路由中设置有多个路径参数,它们会映射到$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 | const User={ |
或使用beforeRouteUpdate导航守卫,也可以取消导航:
1 | const User={ |
捕获所有路由或404 Not found路由
常规参数只匹配url片段之间的字符,用/分隔。若想匹配任意路径,可使用自定义的路径参数正则表达式,在路径参数后面的括号中加入正则表达式:
1 | const routes=[ |
高级匹配模式
Vue Router使用自己的路径匹配语法,它支持许多高级匹配模式,如可选的参数,零或多个/一个或多个,甚至是自定义的正则匹配规则。
路由的匹配语法
在参数中自定义正则
当定义像:userId这样的参数时,在内部使用以下正则([^/]+)(至少有一个字符不是斜杠/)来从URL中提取参数。这个方法很好用,除非是需要根据参数的内容来区分两个路由,此时最简单的方法是在路径中添加一个静态部分来区分它们:
1 | const routes=[ |
但在一些情况下,并不想添加静态的/o、/p部分。由于orderId总是一个数字,而productName可以是任何东西,因此可以在括号中为参数指定一个自定义的正则:
1 | const routes=[ |
可重复的参数
若需要匹配具有多个部分的路由,如/first/second/third,则应该使用*(0个或多个)和+(1个或多个)将参数标记为可重复:
1 | const routes=[ |
这将是提供一个参数数组而不是一个字符串,并且在使用命名路由时也需要传递一个数组:
1 | //给定{path:'/:chapters*',name:'chapters'}, |
这些也可以通过右括号后添加它们与自定义正则结合使用:
1 | const routes=[ |
Sensitive与strict路由配置
默认情况下,所有路由是不区分大小写的,且能匹配带有或不带有尾部斜线的路由。这种行为可以通过sensitive和strict选项来修改,它们既可以应用在整个全局路由上,又可以应用在当前路由上:
1 | const router=createRouter({ |
可选参数
也可以通过使用?修饰符(0个或1个)将一个参数标记为可选:
1 | const routers=[ |
命名路由
除了path外,还可以为任何路由提供name。命名路由有以下优点:
- 没有硬编码的URL
params的自动编码/解码- 防止在URL中出现打字错误
- 绕过路径排序(如显示1个)
1 | const route=[ |
链接一个命名路由,可以向router-link组件的to属性传递一个对象:
1 | <router-link :to="{name:'user',params:{username:'lisi'}}">User</router-link> |
嵌套路由
通过Vue Router可以使用嵌套路由配置来对于应用程序的多层嵌套组件结构:
1 | <body> |
注意:以/开头的嵌套路径将被视为根路径,这便允许利用组件嵌套而不必使用嵌套URL。
上述代码中children的配置只是另一个路由数组。因此可以根据需要,不断地嵌套视图。
由于没有匹配到嵌套路由,当访问/user/eduardo时,在User的router-view里什么都不会呈现。若想在那里渲染一些东西,可以提供一个空的嵌套路径:
1 | const routes=[ |
嵌套命名路由
在处理命名路由时,通常会给子路由命名如下:
1 | const routes=[ |
这将确保导航到/user/:id时始终显示嵌套路由。
若希望导航到命名路由而不导航到嵌套路由,还可以命名父路由,但要注意重新加载页面将始终显示嵌套的子路由,这是以为它被指向路径/users/:id的导航,而不是命名路由:
1 | const routes=[ |
命名视图
命名视图可以同时展示多个视图而不是嵌套展示。一个界面中可以拥有多个单独命名的视图,而不是只有一个单独的出口。若router-view没有设置名字,则默认为default。
1 | <router-view class="view left-sidebar" name="LeftSidevar"></router-view> |
一个视图使用一个组件渲染。因此在同一个路由下,多个视图就需要多个组件:
1 | const router=createRouter({ |
嵌套命名视图
当要实现切换路由的同时,其页面下的视图也要从一个UserEmailsSubscriptions切换成两个UserProfile和UserProfilePreview,便应使用命名视图来创建嵌套视图的布局:
1 | /settings/emails /settings/profile |
Nav是一个常规组件UserSettings是一个视图组件UserEmailsSubscriptions、UserProfile、UserProfilePreview是嵌套的视图组件
UserSetting组件的<template>部分大致如下:
1 | <div> |
通过这个路由配置来实现上述布局:
1 | const router=createRouter({ |
编程式导航
使用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 | // 字符串路径 |
当指定params时,可提供string或number参数(或对于可重复从参数可以提供一个数组)。任何其他类型(如undefined、false等)都将被自动字符串化。对于可选参数,可以提供一个空字符串("")来跳过它。
由于属性to与router.push接收的对象种类相同,一次你两者的规则相同。
router.push和所有其他导航方法都会返回一个Promise,等到导航完成后才知道是成功还是失败。
使用router.replace替换当前位置
router.replace与router.push不同的是,router.replace在导航时不会向history添加新记录,它直接取代了当前的条目。
| 声明式 | 编程式 |
|---|---|
<router-link :to="..." replace> |
router.replace(...) |
可以直接使用router.replace,也可以在传递给router.push的routeLocation中增加一个属性replace:true。
1 | router.replace({path:'/home'}) |
使用router.go横跨历史
router.go采用一个整数作为参数,表示在历史堆栈中前进或后退多少步:
1 | // 向前移动1条记录,与router.forward()作用相同 |
重定向和别名
通过redirect属性实现重定向
通过routes实现重定向:
1 | // 将/home重定向到/ |
在写redirect时,可以省略component配置。由于组件从来没有被直接渲染过,因此没有组件要渲染,嵌套路由除外。若一个路由有children和redirect属性,那么它也应该有component属性。
导航守卫没有应用在跳转路由上,仅应用在其目标上。在上述代码中,在/home路由中添加beforeEnter守卫也不会有任何效果。
相对重定向
即重定向到相对位置:
1 | const routes=[ |
使用alias设置别名
将/别名为/home,便意味着当用户访问/home时,URL仍然是/home,但会被匹配为用户正在访问/:
1 | const routes=[{path:'/',component:Homepage,alias:'/home'}] |
通过别名可以自由地将UI结构映射到一个任意的URL,而不受配置的嵌套结构的限制。
别名以/开头,以使嵌套路径中的路径成为绝对路径,也可以用一个数组来提供多个别名:
1 | const routes=[ |
若路由有参数,则要确保在任何绝对别名中包含它们:
1 | const routes=[ |
关于SEO的注意事项:使用别名是,一定要定义规范链接
路由组件传参
将props传递给路由组件
在组件中使用$route会与路由紧密耦合,由于它只能用于特定的URL,这将限制了组件的灵活性。通过配置props来解除这种行为:
1 | const User={ |
这允许在任何地方使用该组件,使得该组件更容易重用和测试。
布尔模式
当props设置为true时,route.params将被设置为组件的props。
命名视图
对于有命名视图的路由,则必须为每个命名视图定义props配置:
1 | const routes=[ |
对象模式
当props是一个对象时,它将原样设置为组件props。当组件props是静态时很有用:
1 | const routes=[ |
函数模式
创建一个返回props的函数,可以将参数转换为其他类型,将静态值与基于路由的值相结合等:
1 | const routes=[ |
URL/search?q=vue将传递{query:'vue'}作为props传给SearchUser组件。
尽可能保持props函数为无状态的,因此它只会在路由发生变化时起作用。若需要状态来定义props,建议使用包装组件。
不同的历史记录模式
创建路由实例时,允许在不同的历史模式中选中history配置。
Hash模式
hash模式是用createWebHashHistory()创建的:
1 | import{createRouter,createWebHashHistory} from 'vue-router' |
它在内部传递的实际URL之前使用了一个哈希字符(#)。由于这部分URL从未被发送到服务器中,因此不需要再服务器上进行任何特殊处理。不过它在SEO中确实有不会的影响。若担心这个问题,可以使用HTML5模式。
HTML5模式
用createWebHistory()创建HTML5模式,推荐使用这个模式:
1 | import{createRouter,createWebHistory} from 'vue-router' |
当应用是单页的客户端应用时,若没有适当的服务器配置,用户在浏览器中直接访问URL会得到一个404错误。
要想解决这个问题,便是要在服务器上添加一个简单的回退路由。URL不匹配任何静态资源,则应提供与应用程序中index.html相同的页面。
服务器配置实例
假设正在从根目录提供服务。若要部署到子目录中,则应使用Vue
CLI的publicPath配置和相关路由的base属性。除此之外还需要调整服务端,使其使用子目录而不是根目录。
如在原生Node.js中,应调整为如下:
1 | const http=require('http') |
配置完成后,所有未找到的路径都会显示index.html文件,因此应该在Vue应用程序中实现一个万能路由来显示404页面:
1 | const router=createRouter({ |
若使用的是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 | var vm=new Vue({ |
对于已经创建的实例,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 | // 代替Object.assign(this.someObject,{a:1,b:2}) |
对于数组
Vue不能检测以下数组的变动:
- 利用索引直接设置一个数组项,如
vm.items[indexOfItem]=newValue - 修改数组长度,如
vm.items.length=newLength
1 | var vm=new Vue({ |
声明响应式property
由于Vue不允许动态添加根级响应式property,因此必须要在初始化实例之前声明所有根级响应式property,包括空值:
1 | var vm=new Vue({ |
若未在data选项中声明message,Vue将警告渲染函数正在视图访问不存在的property。
异步更新队列
Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。若同一个watcher被多次触发,其也只会被推入到队列中一次。
Vue在缓冲时去除重复数据避免了不必要的计算和DOM操作。然后在下一个事件循环的“tick”中,Vue刷新队列并执行实际(已去重后的)工作。Vue在内部对异步队列尝试使用原生的Promise.then、MutationObserver和setImmediate。若执行环境不支持,则会采用setTimeout(fn,0)代替。
为了在数据变化之后等待Vue完成更新DOM,可以在数据变化之后立即使用Vue.nextTick(callback),这样回调函数将在DOM更新完成后被调用:
1 | <body> |
在组件内使用vm.$nextTick()实例特别方便,因此它不需要全局Vue,且回调函数中的this将自动绑定在当前的Vue实例上:
1 | <body> |
因为$nextTick()返回一个Promise对象,因此可以使用新的ES2017
async/await 语法来完成相同的事情:
1 | methods:{ |
vue-router路由进阶
导航守卫
vue-router提供的导航守卫主要通过跳转或取消的方式来守卫导航。
全局前置守卫
可以使用router.beforeEach注册一个全局前置守卫:
1 | const router=createRouter({ |
当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫resolve完之前,一直处于等待中。