Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的过渡效果,Vue 会给我们一些钩子或者一些关键的样式来帮助我们实现,包括以下工具:
组件在进行显示的时候,transition 组件会为嵌套元素自动添加一些跟动画相关的类名称,这个类名称取决于你在 transition 组件上起的 name,所以我们使用这些类名称来做 css 过度动画就可以了。
<style>
/* 定义过度动画 */
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
.fade-enter-to, .fade-leave {
opacity: 1;
}
</style>
<script>
Vue.component(‘message‘, {
// 使用transition组件应用过度动画
template: `
<transition name="fade">
<div> /* 这个div元素上面将来就会动态的添加一些跟动画相关的类 */
...
</div>
</transition>
`,
})
</script>
我们可以使用一些现成的像 animate.css 这样的动画库来制作更精美的动画效果。
animate.css 里面是有自己的动画类名称的,它跟 Vue 动态添加的类名称是不一样的,那我们怎么把它们结合起来呢,就是通过自定义 Vue 过度状态的类名的方式,这样我们设置的自定义的类名将来就会在相对应的 Vue 的六个过渡状态的时机代替原有的 Vue 的过度类名出现在需要做动画的元素上,就达到了使用第三方定义好的动画类名的效果。
结合 CSS 动画库,动画设计的过程就不需要操心了,只需要在合适的时间点把类名给它加上去或者移除就可以了。
引入animate.css
<link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel="stylesheet" type="text/css">
transition 组件设置
<transition enter-active-class="animated bounceIn" leave-active-class="animated bounceOut">
<div> /* 将来这个元素上的类名会是这样 class="animated bounceIn v-enter-to" */
...
</div>
</transition>
他们的优先级高于普通的类名,这对于 Vue 的过渡系统和其他第三方 CSS 动画库,如 Animate.css 结合使用十分有用。
可以在 transition 属性中声明 JavaScript 钩子,使用JS实现动画。
<transition
v-on:before-enter="beforeEnter" // 动画开始前,设置初始状态
v-on:enter="enter" // 执行动画
v-on:after-enter="afterEnter" // 动画结束,清理工作
v-on:enter-cancelled="enterCancelled" // 取消动画
v-on:before-leave="beforeLeave"
v-on:leave="leave"
v-on:after-leave="afterLeave"
v-on:leave-cancelled="leaveCancelled"
></transition>
<style>
/* 定义过度动画 */
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
/* opacity修改不用css做 */
/*
.fade-enter, .fade-leave-to {
opacity: 0;
}
.fade-enter-to, .fade-leave {
opacity: 1;
}
*/
</style>
<script>
Vue.component(‘message‘, {
template: `
<transition
@before-enter="beforeEnter"
@enter="enter"
@before-leave="beforeLeave"
@leave="leave"
>
...
</transition>
`,
methods: {
beforeEnter(el) {
el.style.opacity = 0 // 设置初始状态
},
enter(el, done) {
document.body.offsetHeight; // 触发回流激活动画
el.style.opacity = 1 // 设置结束状态
el.addEventListener(‘transitionend‘, done) // 监听动画结束事件,并执行done函数
},
beforeLeave(el) {
el.style.opacity = 1 // 设置初始状态
},
leave(el, done) {
document.body.offsetHeight; // 触发回流激活动画
el.style.opacity = 0 // 设置结束状态
el.addEventListener(‘transitionend‘, done) // 监听动画结束事件,并执行done函数
}
},
})
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
<script>
Vue.component(‘message‘, {
template: `
<transition name="fade"
:css="false" // 禁用css
@before-enter="beforeEnter"
@enter="enter"
@before-leave="beforeLeave"
@leave="leave">
</transition>
`,
methods: {
beforeEnter(el) {
el.style.opacity = 0
},
enter(el, done) {
Velocity(el, { opacity: 1 }, { duration: 500, complete: done })
},
beforeLeave(el) {
el.style.opacity = 1
},
leave(el, done) {
Velocity(el, { opacity: 0 }, { duration: 500, complete: done })
}
},
})
</script>
有时候列表中,条目的新增或删除也需要加入一些动画,这时候可以考虑列表过度的方式。
利用 transition-group 可以对 v-for 渲染的每个元素应用过度。
用 transition-group 包裹 v-for 的元素,最终 transition-group 会展开成 n 个 transition,每一个 transition 包裹着一个单独的 v-for 元素。
<transition-group name="fade">
<div v-for="c in courses" :key="c.name">
{{ c.name }} - ¥{{c.price}}
<button @click="addToCart(c)">加购</button>
</div>
</transition-group>
Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:
{{ c.price | currency(‘RMB‘) }}
// 局部方式
filters: {
currency(value, symbol = ‘¥‘) {
return symbol + value;
}
}
// 全局方式
Vue.filter(‘currency‘, function(value, symbol = ‘¥‘) {
// 工厂函数,接收一个值返回一个值
return symbol + value;
})
除了核心功能默认内置的指令 ( v-model 和 v-show ),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作的复用功能的时候,就会用到自定义指令。因为 Vue 的设计理念是数据驱动,一般情况下尽量不要直接接触底层 DOM 操作,如果要做那最好用自定义指令的方式去做这件事情。
范例:输入框获取焦点
Vue.directive(‘focus‘, {
inserted(el) {
el.focus()
}
})
<input v-focus>
自定义指令钩子函数:
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。update
:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下):指令所在组件的 VNode **及其子 VNode** 全部更新后调用。
unbind
:只调用一次,指令与元素解绑时调用。钩子函数参数:
el
:指令所绑定的元素,可以用来直接操作 DOM。name
:指令名,不包括 v-
前缀。value
:指令的绑定值,例如:v-my-directive="1 + 1"
中,绑定值为 2
。oldValue
:指令绑定的前一个值,仅在 update
和 componentUpdated
钩子中可用。无论值是否改变都可用。expression
:字符串形式的指令表达式。例如 v-my-directive="1 + 1"
中,表达式为 "1 + 1"
。arg
:传给指令的参数,可选。例如 v-my-directive:foo
中,参数为 "foo"
。modifiers
:一个包含修饰符的对象。例如:v-my-directive.foo.bar
中,修饰符对象为 { foo: true, bar: true }
。vnode
:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。oldVnode
:上一个虚拟节点,仅在 update
和 componentUpdated
钩子中可用。渲染函数的主要作用是在将来程序进行更新的时候,它再次执行,从而得到最新的虚拟 DOM,
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
// render函数接收一个参数createElement叫创建元素,createElement函数会返回VNode称为虚拟DOM,这个元素就是一 个原生的JS对象,可以描述我们的DOM结构,将来在后续的虚拟DOM的比对中来产生它的作用。
render: function (createElement) {
return createElement(
tag, // 标签名称,组件名字,组件配置对象,组件的构造函数
data, // 传递属性
children // 子节点数组
)
}
使用 render 方法实现 heading 组件:
Vue.component(‘heading‘, {
props: [‘level‘, ‘title‘],
render(h) { // Vue虚拟DOM在底层使用的算法叫snabdom,这个算法里生成虚拟DOM的方法就叫h
// 返回createElement返回的VNode
return h(
‘h‘ + level, // 参数1:tagname
// 参数2:Vue有一个默认行为,如果属性你没处理,会把所有组件上传递的属性动态的移到 组件内部的根结点上。
this.$slots.default // 参数3:子节点VNode数组
)
}
})
<heading :level="2" :title="title">{{title}}</heading>
虚拟 DOM 到底是什么,它是怎样的一个结构。
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。虚拟 DOM 就是真实dom的一个映射。它来描述真实 DOM 的方式,它更轻量,更快速。
它的结构是通过一些属性来描述它将来到底是一个什么样的节点,VNode 本身是一棵树,和真实的 DOM 树是对应的。将来通过一些算法把这个 VNode 转换成真实的 DOM 结构。这就是虚拟 DOM 的核心思想了。我们比较熟悉的 diff 算法将来也是主要在这上面进行的。
范例:输出虚拟 DOM 观察其结构:
const vnode = h(
‘h‘ + level,
{ attrs: { title: this.title } }, // 之前省略了title的处理
this.$slots.default
)
console.log(vnode);
接下来你需要熟悉的是如何在 createElement 函数中使用模板中的那些功能。这里是 createElement 接受的参数:
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
‘div‘,
// {Object}
// 一个与模板中属性对应的数据对象。可选。
{
// (详情见下一节)
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
‘先写一些文字‘,
createElement(‘h1‘, ‘一则头条‘),
createElement(MyComponent, {
props: {
someProp: ‘foobar‘
}
})
]
)
深入数据对象
有一点要注意:正如 v-bind:class
和 v-bind:style
在模板语法中会被特别对待一样,它们在 VNode 数据对象中也有对应的顶层字段。该对象也允许你绑定普通的 HTML attribute,也允许绑定如 innerHTML
这样的 DOM property (这会覆盖 v-html
指令)。
{
// 与 `v-bind:class` 的 API 相同,
// 接受一个字符串、对象或字符串和对象组成的数组
‘class‘: {
foo: true,
bar: false
},
// 与 `v-bind:style` 的 API 相同,
// 接受一个字符串、对象,或对象组成的数组
style: {
color: ‘red‘,
fontSize: ‘14px‘
},
// 普通的 HTML attribute
attrs: {
id: ‘foo‘
},
// 组件 prop
props: {
myProp: ‘bar‘
},
// DOM property
domProps: {
innerHTML: ‘baz‘
},
// 事件监听器在 `on` 内,
// 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
// 需要在处理函数中手动检查 keyCode。
on: {
click: this.clickHandler
},
// 仅用于组件,用于监听原生事件,而不是组件内部使用
// `vm.$emit` 触发的事件。
nativeOn: {
click: this.nativeClickHandler
},
// 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
// 赋值,因为 Vue 已经自动为你进行了同步。
directives: [
{
name: ‘my-custom-directive‘,
value: ‘2‘,
expression: ‘1 + 1‘,
arg: ‘foo‘,
modifiers: {
bar: true
}
}
],
// 作用域插槽的格式为
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement(‘span‘, props.text)
},
// 如果组件是其它组件的子组件,需为插槽指定名称
slot: ‘name-of-slot‘,
// 其它特殊顶层 property
key: ‘myKey‘,
ref: ‘myRef‘,
// 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
// 那么 `$refs.myRef` 会变成一个数组。
refInFor: true
}
范例:处理 title、添加 icon:
Vue.component(‘heading‘, {
props: [‘level‘, ‘title‘, ‘icon‘],
render(h) {
// 子节点数组
let children = [];
// 添加图标功能
// <svg><use xlink:use="#icon-xxx"></use></svg>
if (this.icon) {
children.push(h(
‘svg‘,
{ class: ‘icon‘ },
[ h(‘use‘, { attrs: { ‘xlink:href‘: ‘#icon-‘ + this.icon } }) ]
))
}
children = children.concat(this.$slots.default)
vnode = h(
‘h‘ + level,
{ attrs: { this.title } }, // 之前省略了title的处理
children
)
console.log(vnode);
return vnode
}
})
组件没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法时,可以将组件标记为 functional ,这意味它无状态(没有响应式数据),也没有实例(没有 this 上下文),这样它就更加轻量了,消耗资源会更少,这是一种优化手段。
Vue.component(‘heading‘, {
functional: true, // 标记函数式组件
props: [‘level‘, ‘title‘, ‘icon‘],
render(h, context) { // 上下文传参
let children = [];
const {icon, title, level} = context.props // 属性获取
if (icon) {
children.push(h(
‘svg‘,
{ class: ‘icon‘ },
[h(‘use‘, { attrs: { ‘xlink:href‘: ‘#icon-‘ + icon } })]
))
}
children = children.concat(context.children)
vnode = h(
‘h‘ + level,
{ attrs: { title } },
children
)
console.log(vnode);
return vnode
}
})
关于可复用性的一个重要特性叫混入,它是一种设计模式。
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。比如在一个组件中有一个方法,这个方法它很常用,除了当前组件,另外的几个组件也会用到,这种情况最典型的方式是把这个方法单独的提取出来放到一个公用的地方,将来大家想用的时候把它注入进来,然后可以直接用。
混入是一种非常重要的组件扩展和逻辑复用的一种方式。
一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
mixin 在以后插件的开发和源码的学习中会多次的见到。
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log(‘hello from mixin!‘)
}
}
}
// 定义一个使用混入对象的组件
Vue.component(‘comp‘, {
mixins: [myMixin]
})
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
详情见文档。
插件是 Vue 扩展的终极方案,我们前面说的像自定义指令、自定义组件、自定义过滤器、自定义混入等等所有这些东西,其实不适合去分发,就是需要把这些东西放到 github、npm 上想要发给别人,让别人去用最佳的方式应该是用插件的形式来组织,因为插件是最安全、最好、最有效的方式。别人引入的时候如果重复引也可以有效的规避,而且在插件里也可以很好的组织这些上述说到的复用功能。
我们平常用到的 vue-router、vuex 都是典型的插件。
插件通常用来为 Vue 添加全局功能,插件的功能范围一般有下面几种:
Vue.js 的插件应该暴露一个 install 方法,这个 install 方法将来会被 Vue 的构造函数去调用,就可以有效的和 Vue 进行交互了,这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。
在 install 方法里面我们就可以做很多事情了。
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或属性
Vue.myGlobalMethod = function () {}
// 2. 添加全局资源
Vue.directive(‘my-directive‘, {})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {}
}
使用Vue.use即可引入插件
Vue.use(MyPlugin)
范例:修改heading组件为插件
const MyPlugin = {
install (Vue, options) {
Vue.component(‘heading‘, {...})
}
}
if (typeof window !== ‘undefined‘ && window.Vue) {
window.Vue.use(MyPlugin)
}
Vue Cli 是一个脚手架的工具。cli 的全称是 command line interface 命令行的接口,通过这个命令行的接口我们可以执行一系列的自动化的方式来创建、管理项目。是我们平常开发项目所必须的,我们需要它最重要的原因是我们自己写的项目太缺乏一些系统性的工程化管理了。
当你不需要创建一个大型项目,只是想很快的看到一个用 Vue 的方式去写的一个组件它最终生成的样子给你的领导或客户去看。用这种方式就最佳不过了。它是最快速的开发页面组件原型的方式。
安装 @vue/cli-service-global 扩展
npm install -g @vue/cli-service-global
然后你就可以使用 vue serve 和 vue build 命令对单个 *.vue 文件进行快速原型开发。vue serve 是运行效果,vue build 是打包。
准备一个内容原型 Hello.vue。
启动一个服务并运行原型。
vue serve Hello.vue
我们现在要真正的开发一个完整项目,我要创建一个新的基于 Vue 的项目。
使用 vue create 创建一个 Vue 项目:
这时候命令行的接口工具会提供一系列的问题让你去回答,主要是一些项目的选项。这些选项都选完后,就会经历项目的基本结构的创建和依赖的安装的过程,需要等上几分钟时间。
所有的配置文件会在 package.json 来组织。
vue create my-vue-test
图形化项目管理
有一个有用的功能就是可以输出 webpack 配置,因为 vue-cli3.0 开始我们已经完全看不到 webpack 配置了。
vue ui
Vue CLI 使用了一套基于插件的架构,插件可以修改 webpack 的内部配置,也可以向 vue-cliservice 注入命令,插件的架构可以很方便的扩展一些功能。在项目创建的过程中,绝大部分列出的特性都是通过插件来实现的。
在现有的项目中安装插件:
如果你想在一个已经被创建好的项目中安装一个插件,可以使用 vue add 命令。路由、状态管理、UI 库等都需要用这种插件的方式去安装。
vue add router
这种方式安装,插件本身可能对你的项目产生破坏性的结构上的变更,甚至是文件的修改,它可能破坏掉你文件中所有的代码结构,因为它这次的修改一定要满足它安装的插件能够顺利的跑起来,不会管你原来的代码的,你的代码如果跟新安装的插件有冲突,那它会把你的代码全部干掉。所以在做这个操作的时候,要保留你之前的代码版本。
当你在 JavaScript、CSS 或 *.vue 文件中使用相对路径 (必须以 . 开头) 引用一个静态资源时,该资源模块将被 webpack 处理,需要打包,给它起个合适的名字放到一个合适的地方去。
转换规则:
如果 URL 是一个绝对路径 (例如 /images/foo.png ),它将会被保留不变。
<img alt="Vue logo" src="/assets/logo.png">
<img alt="Vue logo" src="http://image.xx.com/logo.png">
如果 URL 以 . 开头会作为一个相对模块请求被解释并基于文件系统相对路径。
<img alt="Vue logo" src="./assets/logo.png">
如果 URL 以 ~ 开头会作为一个模块请求被解析。这意味着你甚至可以引用 Node 模块中的资源:
<img src="~some-npm-package/foo.png">
如果 URL 以 @ 开头会作为一个模块请求被解析。Vue CLI 默认会设置一个指向 src 的别名 @ 。
import Hello from ‘@/components/Hello.vue‘
关于 public 里面存放素材的选择。
将来静态的资源会放到 public 里头,因为 public 会作为开发服务器的静态路径。这里的资源 webpack 是不会处理的,原封不动,位置和名字都不会变。
通过 webpack 的处理(相对路径)并获得如下好处:
如下情况考虑使用 public 文件夹: