前言:简介与安装
Vue.js是什么?
Vue是一套用于构建用户界面的渐进式框架,并被设计为自底向上逐层应用。Vue的核心库只关心视图层。
安装
- CDN包导入:制作原型或学习时,可以这样使用最新版本:
<script src="https://unpkg.com/vue@next"></script> - 下载JavaScript文件并自行托管:下载相关文件并自行托管到在你的服务器上,然后通过
<script>标签引入。 - npm:在用. Vue 构建大型应用时推荐使用 npm 安装
# 最新稳定版
$ npm install vue@next
如果想使用单文件组件,还需要安装@vue/compiler-sfc:
$ npm install -D @vue/compiler-sfc
- Vue CLI:为单页面快速搭建繁杂的脚手架,为现代前端工作流提供了功能完备的构建设置,并带有热重载、保存时lint校验。
yarn global add @vue/cli
# 或
npm install -g @vue/cli
然后:
vue upgrade --next
- Vite(推荐):Vite也是一个web开发构建工具,可以实现闪电般的冷服务器启动。使用npm时:
# npm 6.x
$ npm init vite@latest <project-name> --template vue
# npm 7+,需要加上额外的双短横线
$ npm init vite@latest <project-name> -- --template vue
$ cd <project-name>
$ npm install
$ npm run dev
一、语法
1.模板语法
Vue.js使用了基于HTML的模板语法,允许开发者声明式地将DOM绑定至底层组件实例的数据。在底层的实现上,Vue将模板编译成虚拟DOM渲染函数,结合响应式系统,Vue可以智能计算出最少需要重新渲染多少组件,把DOM操作数减到最少。所有的Vue模板都是合法的HTML。
-
文本插值:最常见的形式是“Mustache”双大括号语法的文本插值:
<span>Message: {{ msg }}</span>该Mustache标签将被替换为对应的组件实例中
msgproperty的值,且响应式更新。(使用v-once指令则执行一次性插值,但可能会影响该节点上的其他数据绑定。) -
原始HTML字符串插值:由于双大括号会渲染为普通文本。为了输出真正的HTML,需要使用
v-html指令:<div id="example1" class="demo"> <p>Using v-html directive: <span v-html="rawHtml"></span></p> </div>const RenderHtmlApp = { data() { return { rawHtml: '<span style="color: red">This should be red.</span>' } } } Vue.createApp(RenderHtmlApp).mount('#example1')其中
rawHtmlproperty的字符串将被渲染为html代码到span中。 -
HTML特性插值:双大括号语法不能在HTML特性中使用,替代办法是使用
v-bind指令与组件实例的property进行绑定。<div v-bind:id="dynamicId"></div>如果绑定的值是
null或undefined,则该特性不会包含在渲染的元素上。 -
使用JavaScript表达式:在模板中,既可以只绑定简单的 property 键值,也可以支持完全的JavaScript表达式的支持。例如:
{{ number + 1 }} {{ ok ? 'YES' : 'NO' }} {{ message.split('').reverse().join('') }} <div v-bind:id="'list-' + id"></div>
2.Data属性、计算属性、侦听器
(1)Data属性与方法
- Data 属性
组件的
data选项是一个函数,返回一个对象,Vue会通过响应式系统将其包裹起来,并以$data的形式存储在组件实例中。同时,该对象的任何顶级property也会直接通过组件实例暴露出来。Vue 将使用$前缀通过组件实例暴露自己的内置 API。
const app = Vue.createApp({
data() {
return { count: 4 }
}
})
const vm = app.mount('#app')
console.log(vm.$data.count) // => 4
console.log(vm.count) // => 4
// 修改 vm.count 的值也会更新 $data.count
vm.count = 5
console.log(vm.$data.count) // => 5
// 反之亦然
vm.$data.count = 6
console.log(vm.count) // => 6
- 方法:
我们使用
methods选项向组件实例添加方法,它也是一个包含所需方法的对象:
const app = Vue.createApp({
data() {
return { count: 4 }
},
methods: {
increment() {
// `this` 指向该组件实例
this.count++
}
}
})
const vm = app.mount('#app')
console.log(vm.count) // => 4
vm.increment()
console.log(vm.count) // => 5
Vue会为methods自动绑定this,并始终指向组件实例。所以在定义方法时应避免使用箭头函数。
这些methods和组件实例的其他所有property一样均可在组件的模板中访问。在模板中,它们通常被当作事件监听使用:
<button @click="increment">Up vote</button>
当然也可以直接从模板(双大括号)中直接调用方法,但是换做计算属性会更好:
<span :title="toTitleDate(date)">
{{ formatDate(date) }}
</span>
注意,从模板中调用方法时不应该存在任何副作用(更改数据或者触发异步进程)。
(2)计算属性、侦听器
- 计算属性:如果模板中放入太多的逻辑会使模板过重且难以维护,对于任何包含响应式数据的复杂逻辑,应该使用计算属性。例如:
<div id="computed-basics">
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</div>
Vue.createApp({
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
}
}
},
computed: {
// 计算属性的 getter
publishedBooksMessage() {
// `this` 指向 vm 实例
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
}).mount('#computed-basics')
上面的例子声明了一个计算属性publishedBooksMessage,且Vue知道 vm.publishedBookMessage 依赖于 vm.author.books,因此当 vm.author.books 发生改变时,所有依赖 vm.publishedBookMessage 的绑定也会更新。
计算属性默认只有getter,当然在需要时也可以手动提供一个setter。
3.渲染
(1)条件渲染
- v-if:用于条件性渲染,只有在指令表达式返回truthy的时候被渲染。如果要应用于多个元素上,可以在包裹元素上面使用v-if,最终的渲染结果将不包含该包裹元素。
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
同时也可以使用v-else指令来表示v-if的else块,使用v-else-if指令来表示v-if的else-if块。
- v-show:用于条件性展示,只是简单地切换元素的
displayCSS属性。 注意不推荐同时使用 v-if 和 v-show。
(2)列表渲染
- v-for:用于将一个数组映射为一组元素。v-for 指令需要使用
item in items形式的特殊与发展,其中的 items 是源数据数组,而 item 则是被迭代的数组元素的别名。
<ul id="array-rendering">
<li v-for="item in items">
{{ item.message }}
</li>
</ul>
Vue.createApp({
data() {
return {
items: [{ message: 'Foo'}, { message: 'Bar' }]
}
}
}).mount('#array-rendering')
使用 v-for 时有三个参数,分别是value、key(可选)、index(可选)。
当 v-for 渲染的元素列表更新时,默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue不会移动DOM元素来匹配新顺序,而是就地更新每个元素,非常高效,但是只适用于不依赖子组件状态或临时DOM状态(例如表单输入值)的列表渲染输出。
为了能够跟踪每个节点的身份,从而重用和重排序现有的元素,你需要为每项提供一个唯一的keyattribute。
<div v-for="item in items" :key="item.id">
<!-- 内容 -->
</div>
即 尽可能在使用 v-for 时提供keyattribute。
类似于 v-if ,也可以利用带有 v-for 的包裹元素来循环渲染一段包含多个元素的内容:
<ul>
<template v-for="item in items" :key="item.msg">
<li>{{ item.msg }}</li>
<li class="divider" role="presentation"></li>
</ul>
当 v-for 与 v-if 一同使用时,v-if 的优先级更高,即v-if 没有权限访问 v-for 中的变量。
4.表单绑定(双向绑定应用)
(1)基本用法:
- 文本
<div id="v-model-basic" class="demo">
<input v-model="message" placeholder="edit me" />
<p>Message is: {{ message }}</p>
</div>
Vue.createApp({
data() {
return {
message: ''
}
}
}).mount('#v-model-basic')
- 多行文本
- 复选框:单个复选框,绑定到布尔值;多个复选框,绑定到同一数组。
- 单选框
- 选择框
(2)值绑定
v-model 绑定的值通常是静态字符串,但有时我们想将值绑定到当前活动实例的一个动态 property 上,这时可以使用 v-bind 。
- 复选框
<input type="checkbox" true-value="yes" false-value="no" />
// 当选中时:
vm.toggle === 'yes'
// 当未选中时:
vm.toggle === 'no'
- 单选框
<input type="radio" v-model="pick" v-bind:value="a" />
//当被选中时
vm.pick === vm.a
- 选择框选项
<select v-model="selected">
<!-- 内联对象字面量 -->
<option :value="{ number: 123 }">123</option>
</select>
//当选中时
typeof vm.selected // => 'object'
vm.selected.numver // => 123
二、组件基础
1.组件注册
注意区分应用实例和组件实例
每个 Vue 应用都是通过createApp函数创建一个新的应用实例开始的,而应用实例的作用是在该应用中注册子组件。
const app = Vue.createApp({
/* options */
})
传递给createApp的 options 用于配置根组件,挂载应用到某个DOM元素时,这个根组件将被用作渲染的起点。
const RootComponent ={
/* options */
}
const app = Vue.createApp(RootComponent)
const vm = app.mount('#app')
用于挂在应用实例到DOM元素的方法mount并不返回应用本身,而是返回的根组件实例。
大多数的真实应用都是被组织成一个嵌套的、可重用的组件树。例如:
Root Component
└─ TodoList
├─ TodoItem
│ ├─ DeleteTodoButton
│ └─ EditTodoButton
└─ TodoListFooter
├─ ClearTodosButton
└─ TodoListStatistics
每个组件都有自己的组件实例。某些组件在任何时候都可能有多个组件实例渲染,则这个应用中的所有组件实例共享同一个应用实例。
组件名
在注册组件时需要给它一个名字。在全局注册时,该组件名就是app.component的第一个参数。
const app = Vue.createApp({...})
app.component('my-component-name', {
/* ... */
})
- 组件的组织: 一个应用通常以一个组件树的形式组织,为了能在模板中使用组件,这些组件必须先注册。
- 全局注册:通过
app.component来创建组件。这些组件在注册之后可以用于任何新创建的组件实例的模板中。例如:
const app = Vue.createApp({})
app.component('component-a', {
/* ... */
})
app.component('component-b', {
/* ... */
})
app.component('component-c', {
/* ... */
})
app.mount('#app')
<div id="app">
<component-a></component-a>
<component-b></component-b>
<component-c></component-c>
</div>
上述代码中,三个组件在注册之后可以用于任何新创建的组件实例的模板中(指<div>代码块)
- 局部注册:一般情况下全局注册是不够理想的,全局注册所有的组件意味着即便不再使用其中的一个组件了,它仍然会被包含在最终的构建结果中。 这种情况下,可以通过一个普通的JavaScript对象来定义组件:
const ComponentA = {
/* ... */
}
const ComponentB = {
/* ... */
}
const ComponentC = {
/* ... */
}
然后在components选项中定义你想要使用的组件:
const app = Vue.createApp({
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
注意:局部注册的组件在其子组件中不可用,例如如果希望ComponentA 在 ComponentB 中可用,则需要这样写:
const ComponentA = {
/* ... */
}
const ComponentB = {
components: {
'component-a': ComponentA
}
/* ... */
}
2.Props
Prop是可以在组件上注册的一些自定义的 attribute,我们将 通过Prop来向子组件传递数据。 例如:
const app = Vue.createApp({})
app.component('blog-post', {
props: ['title'],
template: `<h4>{{ title }}</h4>` // 模板
})
app.mount('#blog-post-demo')
<div id="blog-post-demo" class="demo">
<blog-post title="My journey with Vue"></blog-post>
<blog-post title="Blogging with Vue"></blog-post>
<blog-post title="Why Vue is so fun"></blog-post>
</div>
- 限定Prop类型 例如:
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // 或任何其他构造函数
}
- 动态传值:通过 v-bind 动态赋值,例如:
<!-- 动态赋予一个变量的值 -->
<blog-post :title="post.title"></blog-post>
<!-- 动态赋予一个复杂表达式的值 -->
<blog-post :title="post.title + ' by ' + post.author.name"></blog-post>
- 单向数据流:所有的 prop 都使得其父子 prop 之间形成了一个单项下行绑定,即父级 prop 的更新会向下流动到子组件中,但是反过来则不行,这样设计的好处是防止从子组件意外变更父组件的状态。下面是两个常见的试图改变一个 prop 的情形:
/*
1. 这个prop用于传输一个初始值,子组件希望将其作为一个本地的prop数据来使用。
最好使用定义一个 data property 并将这个 prop 作为其初始值。
*/
props: ['initialCounter'],
data() {
return {
counter: this.initialCounter
}
}
/*
2. 这个prop以一种原始的值传入且需要进行转换。
最好使用这个prop的值来定义一个计算属性。
*/
props: ['size'],
computed: {
normalizedSize() {
return this.size.trim().toLowerCase()
}
}
3.非 Prop 的 Attribute
非 Prop 的 Attribute 指的是传向一个组件,但是该组件并没有相应的 props 或 emits 定义的attribute。比如 class, style, id attribute。我们可以通过 $attrs property 来访问这些 attribute。
特性:如果组件返回单个根节点,则 非 prop 的 attribute 将自动添加到根节点的 attribute 中。例如:
app.component('date-picker', {
template: `
<div class="date-picker">
<input type="datetime-local" />
</div>
`
})
<!-- 具有非 prop 的 attribute 的 date-picker 组件-->
<date-picker data-status="activated"></date-picker>
<!-- 渲染后的 date-picker 组件 -->
<div class="date-picker" data-status="activated">
<input type="datetime-local" />
</div>
在事件监听器上也同样适用:
<date-picker @change="submitChange"></date-picker>
app.component('date-picker', {
created() {
console.log(this.$attrs) // { onChange: () => {} }
}
})
如果不希望组件的根元素继承attribute,可以在组件的选项中设置 inheritAttrs: false。一般这么做的原因是要将 attribute 应用到除了根节点以外的元素中,这时可以通过组件的 $attrs property 将这个attribute 应用到其他元素上。例如:
app.component('date-picker', {
inheritAttrs: false,
template: `
<div class="date-picker">
<input type="datetime-local" v-bind="$attrs" />
</div>
`
})
<!-- date-picker 组件使用非 prop 的 attribute -->
<date-picker data-status="activated"></date-picker>
<!-- 渲染后的 date-picker 组件 -->
<div class="date-picker">
<input type="datetime-local" data-status="activated" />
</div>
如果存在多个根节点,则该组件不具有自动的 attribute 隐式贯穿行为。这就需要手动显式地绑定 $attrs 了,否则运行时将发出警告。例如:
<custom-layout id="custom-layout" @click="changeValue"></custom-layout>
// 这将发出警告
app.component('custom-layout', {
template: `
<header>...</header>
<main>...</main>
<footer>...</footer>
`
})
// 下面这样写将不会发出警告,$attrs 被显式地传递到了 <main> 元素
app.component('custom-layout', {
template: `
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
`
})
4.自定义事件
(1)事件名与自定义事件
和组件和 prop 一样,事件名提供了自动的大小写转换,且如果在子组件中触发了一个 驼峰式命名 的事件,则可以在父组件中添加一个 短横线分隔命名 的监听器。
定义自定义事件使用 emits 选项:
app.component('custom-form', {
emits: ['inFocus', 'submit']
})
这样就定义了新的事件侦听器
(2)对抛出的事件的验证
与 prop 的类型检验类似,如果使用对象语法而不是数组语法定义发出的事件,则可以对其进行验证,即为事件分配一个函数,且该函数接收传递给 $emit 调用的参数,并返回一个布尔值以指示事件是否有效:
app.component('custom-form', {
emits: {
//没有验证
click: null,
//验证 submit 事件
submit :({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload')
return false
}
}
},
methods: {
submitForm(email, password) {
this.$emit('submit', { email, password })
}
}
})
(3)监听子组件事件
当我们在开发与一个组件时,它的一些功可能需要与父组件进行沟通,即 通过父组件来监听子组件上触发的事件:
const App = {
data() {
return {
posts: [
/* ... */
],
postFontSize: 1
}
}
}
const app = Vue.createApp(App)
app.component('blog-post', {
props: ['title'],
template: `
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlargeText')">
Enlarge text
</button>
</div>
`
})
当点击按钮时,组件实例提供一个自定义事件系统,父组件就可以像处理原生DOM事件一样通过v-on或@来监听子组件实例的任意事件
<blog-post ... @enlarge-text="postFontSize += 0.1"></blog-post>
如果我们要检查组件所抛出的所有事件以及对它们进行验证,可以在组件的emits选项中列出已抛出的事件:
app.component('blog-post', {
props: ['title'],
emits: ['enlargeText']
})
如果要让子组件触发事件时同时抛出一个值,可以使用 $emit 的第二个参数提供该值:
<button @click="$emit('enlargeText', 0.1)">
Enlarge text
</button>
然后父组件监听这个事件的时候,可以通过 $event 访问到这个被抛出的值:
<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>
5.v-model、在组件中使用v-model
(1)v-model 表单绑定
v-model 用于表单的双向绑定,自定义事件也可以用于创建支持v-model的自定义输入组件。
先看<input>的一个例子,请记住下面的例子:
<input v-model="searchText" />
//等价于下面的形式
<input
v-bind:value="searchText"
@input="searchText = $event.target.value" />
(2)组件中使用 v-model
但是如果在组件上使用v-model时,则会变成下面这样:
<custom-input v-model="searchText"></custom-input>
// 等价于下面的形式
<custom-input
v-bind:model-value="searchText"
@update:model-value="searchText = $event"
></custom-input>
对应的组件实现如下:
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input
v-bind:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
`
})
如何理解上面的例子呢?从流程的最开始,父组件接收一个与model-value attribute 绑定的searchText的值,并通过 prop 传递到子组件,使子组件也可用该值,通过v-bind将该值绑定到子组件的value的attribute上,并当 [ 输入时 ] 在子组件上抛出事件名update:modelValue和相应的表单输入值$event.target.value,父组件监听这一update:modelValue自定义事件,事件触发后执行searchText = $event的操作。即 子组件需要一个 prop 并发出一个 自定义事件 来进行同步。
(3)组件中多个 v-model 的绑定
如上面的组件单个 v-model 的绑定,每个 v-model 将同步到对应的 prop ,且不需要在组件中添加额外的选项了:
<user-name
v-model:first-name="firstName"
v-model:last-name="lastName"
></user-name>
app.component('user-name', {
props: {
firstName: String,
lastName: String
},
emits: ['update:firstName', 'update:lastName'],
template: `
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)">
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)">
`
}
6.父子组件间的简单通信
父组件与子组件的概念容易混淆,简单来说,我们将某段代码封装成一个组件(一个.vue拓展名的文件),而这个组件又能被另一个组件(另一个.vue拓展名的文件)引入(通过import形式),那么引入该封装的组件的文件就叫父组件,被引入的文件称为子组件。下面看一个来自这里的例子:
test.vue,作为子组件:
<template>
<div>
{{ message }}
</div>
<p>{{ hello }}</p>
<p>{{ hellotests }}</p>
</template>
<script>
export default{
// props 用于接收从父组件传过来的数据
props:['msgTest', 'helloTest', 'CCCount']
data() {
return{
// 子组件中声明的变量
msg: '',
hello: '',
hellotest: ''
}
},
created() {
// ...
},
watch: {
// 监听从父组件传来的数据是否发生了变化,然后赋值给变量
CCCount() {
this.msg = this.msgTest;
this.hello = this.helloTest
}
},
methods: {
// 点击按钮,子组件操作父组件中的函数
btns() {
this.$emit('childsClick', '子组件操作父组件中的函数')
},
// 被父组件调用的子组件函数
msgs1() {
this.hellotests = '哈哈,我是父组件调用子组件的函数'
}
}
}
</script>
作为父组件
<template>
<div>test</div>
<p>hello world</p>
<span>{{ btnVal }}</span>
<msgChild
ref="msgRef"
@childsClick="btnClick"
:msgTest="msgTest2"
:helloTest="helloTest2"
:CCCount="CCCount"
>
<msgChild>
</template>
<script>
// 引入子组件
import msgChild form './test.vue'
export default{
data() {
return{
// 父组件中声明的变量
msgTest2: '',
helloTest2: '',
CCCount: 0,
btnVal: ''
}
},
component: {
// 声明子组件的名字,即局部注册
msgChild
},
created() {
this.msg()
},
methods: {
msg() {
// 组件变量赋值以传递给子组件
this.msgTest2 = 'test01';
this.helloTest = 'test02';
},
btnClick(val) {
this.CCCount++;
thos.btnVal = val;
// 触发子组件中的函数
this.$refs.msgRef.msgs1()
}
}
}
</script>