Vue3
官方文档是最好的教程:https://v3.cn.vuejs.org/guide/
认识
<div id="counter">
Counter: {{ counter }}
</div>
const Counter = {
data() {
return {
counter: 0
}
},
mounted() {
setInterval(() => {
this.counter++
}, 1000)
}
}
Vue.createApp(Counter).mount('#counter')
另一个例子
const app = Vue.createApp({
data() {
return { count: 4 }
},
methods: {
increment() {
// `this` 指向该组件实例
this.count++
}
}
})
const vm = app.mount('#app')
不成熟的认知习惯
- 在 OptionsAPI 中
- data 写成函数形式,里面 return 一个对象
- methods 写成对象形式
- computed 写成对象形式,里面的计算属性写成函数形式
- mounted 写成函数形式
新特性
- 树摇(Tree shacking)
- 组合式 API(Composition API)
安装
vue-cli
基于webpack
封装,生态非常强大,可配置性也非常高,几乎能够满足前端工程化的所有要求。缺点就是配置复杂,甚至有公司有专门的 webpack 工程师专门做配置,另外就是 webpack 由于开发环境需要打包编译,开发体验实际上不如vite
。vite
开发模式基于esbuild
,打包使用的是rollup
。急速的冷启动
和无缝的hmr
在开发模式下获得极大的体验提升。缺点就是该脚手架刚起步,生态上还不及webpack
Vite
vite 是伴随这 Vue3.0 诞生的单文件组件的非打包开发服务器,用来进行 3.0 的编译
安装 vite 命令
# yarn 安装
yarn global add create-vite-app
# npm 全局安装
npm i -g create-vite-app
创建项目
# 完整命令
create-vite-app <project-name>
# 缩写命令
cva <project-name>
# or 推荐
npm init vite@latest <project-name> -- --template vue
配置 vite.config.ts
// vite.config.ts
module.exports = {
port: 8077, // 服务端口
proxy: { // 代理
// string shorthand
"/foo": "http://localhost:4567/foo",
// with options
"/api": {
target: "http://jsonplaceholder.typicode.com",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
};
Vue CLI
安装
npm install -g @vue/cli
创建项目
vue create my-vue3-demo
# 通过可视化工具创建
vue ui
启动
npm run serve
vue.config.js
配置
// vue.config.js
module.exports = {
outputDir: 'dist', // 打包的目录
lintOnSave: true, // 在保存时校验格式
productionSourceMap: false, // 生产环境是否生成 SourceMap
devServer: {
open: true, // 启动服务后是否打开浏览器
overlay: { // 错误信息展示到页面
warnings: true,
errors: true
},
host: '0.0.0.0',
port: 8066, // 服务端口
https: false,
hotOnly: false,
// proxy: { // 设置代理
// '/api': {
// target: host,
// changeOrigin: true,
// pathRewrite: {
// '/api': '/',
// }
// },
// },
},
}
一些思想
- 对于任何包含响应式数据的复杂逻辑,都应该使用计算属性
- 我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的响应依赖关系缓存的。计算属性只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要
author.books
还没有发生改变,多次访问publishedBookMessage
计算属性会立即返回之前的计算结果,而不必再次执行函数 - 但是如果你不希望有缓存,请用
method
来替代 - 当需要在数据变化时执行异步或开销较大的操作时,使用侦听器更为合适。
- 我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的响应依赖关系缓存的。计算属性只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要
- 因为
v-if
是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可 以把一个<template>
元素当做不可见的包裹元素,并在上面使用v-if
。最终的渲染结果将不包含<template>
元素 - 一般来说,
v-if
有更高的切换开销,而v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用v-show
较好;如果在运行时条件很少改变,则使用v-if
较好。 - 建议尽可能在使用
v-for
时提供key
attribute,如v-for="item in items" :key="item.id"
- 有时,我们想要显示一个数组经过过滤或排序后的版本,而不实际变更或重置原始数据。在这种情况下,可以创建一个计算属性,来返回过滤或排序后的数组
- 类似于
v-if
,你也可以利用带有v-for
的<template>
来循环渲染一段包含多个元素的内容
TS 支持
配置文件
// tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
// 这样就可以对 `this` 上的数据属性进行更严格的推断
"strict": true,
"jsx": "preserve",
"moduleResolution": "node"
}
}
请注意,必须包含 strict: true
(或至少包含 noImplicitThis: true
,它是 strict
标志的一部分) 才能在组件方法中利用 this
的类 型检查,否则它总是被视为 any
类型。
如果你使用自定义 Webpack 配置,需要配置 ' ts-loader ' 来解析 vue 文件里的 <script lang="ts">
代码块:
// webpack.config.js
module.exports = {
...
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
},
exclude: /node_modules/,
},
{
test: /\.vue$/,
loader: 'vue-loader',
}
...
定义 Vue 组件
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
// 已启用类型推断
})
</script>
基本知识
组件化
组件系统是 Vue 的另一个重要概念,因为它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树:
在 Vue 中,组件本质上是一个具有预定义选项的实例。在 Vue 中注册组件很简单:如对 App
对象所做的那样创建一个组件对象,并将其定义在父级组件的 components
选项中:
// 创建 Vue 应用
const app = Vue.createApp(...)
// 定义名为 todo-item 的新组件
app.component('todo-item', {
template: `<li>This is a todo</li>`
})
// 挂载 Vue 应用
app.mount(...)
现在,你可以将其放到到另一个组件的模板中:
<ol>
<!-- 创建一个 todo-item 组件实例 -->
<todo-item></todo-item>
</ol>
但是这样会为每个待办项渲染同样的文本。我们应该能将数据从父组件传入子组件才对。让我们来修改一下组件的定义,使之能够接受一个 prop;然后使用 v-bind
指令将待办项传 到循环输出的每个组件中:
<div id="todo-list-app">
<ol>
<!--
现在我们为每个 todo-item 提供 todo 对象
todo 对象是变量,即其内容可以是动态的。
我们也需要为每个组件提供一个“key”,稍后再
作详细解释。
-->
<todo-item
v-for="item in groceryList"
v-bind:todo="item"
v-bind:key="item.id"
></todo-item>
</ol>
</div>
const TodoList = {
data() {
return {
groceryList: [
{ id: 0, text: 'Vegetables' },
{ id: 1, text: 'Cheese' },
{ id: 2, text: 'Whatever else humans are supposed to eat' }
]
}
}
}
const app = Vue.createApp(TodoList)
app.component('todo-item', {
props: ['todo'],
template: `<li>{{ todo.text }}</li>`
})
app.mount('#todo-list-app')
在一个大型应用中,有必要将整个应用程序划分为多个组件,以使开发更易管理。不过这里有一个 (假想的) 例子,以展示使用了组件的应用模板是什么样的:
<div id="app">
<app-nav></app-nav>
<app-view>
<app-sidebar></app-sidebar>
<app-content></app-content>
</app-view>
</div>
单个文件组件
在典型的 Vue 应用中,我们使用单个文件组件而不是字符串模板。
在模块系统中局部注册
如果你使用了诸如 Babel 和 webpack 的模块系统。在这些情况下,我们推荐创建一个 components
目录,并将每个组件放置在其各自的文件中。
然后你需要在局部注册之前导入每个你想使用的组件。例如,在一个假设的 ComponentB.js
或 ComponentB.vue
文件中:
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'
export default {
components: {
ComponentA,
ComponentC
}
// ...
}
现在 ComponentA
和 ComponentC
都可以在 ComponentB
的模板中使用了。
字母不分大小写
另外,HTML attribute 名不区分大小写,因此浏览器将所有大写字符解释为小写。这意味着当你在 DOM 模板中使用时,驼峰 prop 名称和 event 处理器参数需要使用它们的 kebab-cased (横线字符分隔) 等效值:
// 在JavaScript中的驼峰
app.component('blog-post', {
props: ['postTitle'],
template: `
<h3>{{ postTitle }}</h3>
`
})
<!-- 在HTML则是横线字符分割 -->
<blog-post post-title="hello!"></blog-post>
应用实例
每个 Vue 应用都是通过用 createApp
函数创建一个新的应用实例开始的:
const app = Vue.createApp({
/* 选项 */
})
该应用实例是用来在应用中注册“全局”组件的。简单的例子:
const app = Vue.createApp({})
app.component('SearchInput', SearchInputComponent)
app.directive('focus', FocusDirective)
app.use(LocalePlugin)
应用实例暴露的大多数方法都会返回该同一实例,允许链式:
Vue.createApp({})
.component('SearchInput', SearchInputComponent)
.directive('focus', FocusDirective)
.use(LocalePlugin)
根组件与挂载
传递给 createApp
的选项用于配置根组件。当我们挂载应用时,该组件被用作渲染的起点。
一个应用需要被挂载到一个 DOM 元素中。例如,如果你想把一个 Vue 应用挂载到 <div id="app"></div>
,应 该传入 #app
:
const RootComponent = {
/* 选项 */
}
const app = Vue.createApp(RootComponent)
const vm = app.mount('#app')
mount
不返回应用本身,它返回的是根组件实例
大多数的真实应用都是被组织成一个嵌套的、可重用的组件树。举个例子,一个 todo 应用组件树可能是这样的:
Root Component
└─ TodoList
├─ TodoItem
│ ├─ DeleteTodoButton
│ └─ EditTodoButton
└─ TodoListFooter
├─ ClearTodosButton
└─ TodoListStatistics
组件实例 property
在 data
中定义的 property 是通过组件实例暴露的:
const app = Vue.createApp({
data() {
return { count: 4 }
}
})
const vm = app.mount('#app')
console.log(vm.count) // => 4
有各种其他的组件选项,可以将用户定义的 property 添加到组件实例中,例如 methods
,props
,computed
,inject
和 setup
。组件实例的所有 property,无论如何定义,都可以在组件的模板中访问。
Vue 还通过组件实例暴露了一些内置 property,如 $attrs
和 $emit
。这些 property 都有一个 $
前缀,以避免与用户定义的 property 名冲突。
生命周期钩子
每个组件在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
生命周期钩子的
this
上下文指向调用它的当前活动实例。
不要在选项 property 或回调上使用箭头函数,因为他们没有
this
模板语法
部分内容详见 Vue2
动态参数
也可以在指令参数中使用 JavaScript 表达式,方法是用方括号括起来:
<!--
注意,参数表达式的写法存在一些约束,如之后的“对动态参数表达式的约束”章节所述。
-->
<a v-bind:[attributeName]="url"> ... </a>
这里的 attributeName
会被作为一个 JavaScript 表达式进行动态求值,求得的值将会作为最终的参数来使用。
例如,如果你的组件实例有一个 data property 为 attributeName
,其值为 "href"
,那么这个绑定将等价于 v-bind:href
。
同样地,你可以使用动态参数为一个动态的事件名绑定处理函数:
<a v-on:[eventName]="doSomething"> ... </a>
在这个示例中,当 eventName
的值为 "focus"
时,v-on:[eventName]
将等价于 v-on:focus
对动态参数表达式约定
动态参数表达式有一些语法约束,因为某些字符,如空格和引号,放在 HTML attribute 名里是无效的。例如:
<!-- 这会触发一个编译警告 -->
<a v-bind:['foo' + bar]="value"> ... </a>
变通的办法是使用没有空格或引号的表达式,或用计算属性替代这种复杂表达式。
在 DOM 中使用模板时 (直接在一个 HTML 文件里撰写模板),还需要避免使用大写字符来命名键名,因为浏览器会把 attribute 名全部强制转为小写:
<!--
在 DOM 中使用模板时这段代码会被转换为 `v-bind:[someattr]`。
除非在实例中有一个名为“someattr”的 property,否则代码不会工作。
-->
<a v-bind:[someAttr]="value"> ... </a>
修饰符
修饰符 (modifier) 是以半角句号 .
指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。例如,.prevent
修饰符告诉 v-on
指令对于触发的事件调用 event.preventDefault()
:
<form v-on:submit.prevent="onSubmit">...</form>
在接下来对 v-on
和 v-for
等功能的探索中,将会看到修饰符的其它例子
Data Property
组件的 data
选项是一个函数。Vue 会在创建新组件实例的过程中调用此函数。它应该返回一个对象,然后 Vue 会通过响应性系统将其包裹起来,并以 $data
的形式存储在组件实例中。为方便起见,该对象的任何顶级 property 也会直接通过组件实例暴露出来:
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
直接将不包含在 data
中的新 property 添加到组件实例是可行的。但由于该 property 不在背后的响应式 $data
对象内,所以 Vue 的响应性系统不会自动跟踪它。
Vue 使用 $
前缀通过组件实例暴露自己的内置 API。它还为内部 property 保留 _
前缀。你应该避免使用这两个字符开头的的顶级 data
property 名称
防抖和节流
Vue 没有内置支持防抖和节流,但可以使用 Lodash 等库来实现。
如果某个组件仅使用一次,可以在 methods
中直接应用防抖:
<script src="https://unpkg.com/lodash@4.17.20/lodash.min.js"></script>
<script>
Vue.createApp({
methods: {
// 用 Lodash 的防抖函数
click: _.debounce(function() {
// ... 响应点击 ...
}, 500)
}
}).mount('#app')
</script>
但是,这种方法对于可复用组件有潜在的问题,因为它们都共享相同的防抖函数。为了使组件实例彼此独立,可以在生命周期钩子的 created
里添加该防抖函数:
app.component('save-button', {
created() {
// 用 Lodash 的防抖函数
this.debouncedClick = _.debounce(this.click, 500)
},
unmounted() {
// 移除组件时,取消定时器
this.debouncedClick.cancel()
},
methods: {
click() {
// ... 响应点击 ...
}
},
template: `
<button @click="debouncedClick">
Save
</button>
`
})
计算属性的 Setter
计算属性默认只有 getter,不过在需要时你也可以提供一个 setter:
// ...
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
// ...
现在再运行 vm.fullName = 'John Doe'
时,setter 会被调用,vm.firstName
和 vm.lastName
也会相应地被更新。
侦听器
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch
选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
例如:
<div id="watch-example">
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</div>
<!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 -->
<!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 -->
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script>
const watchExampleVM = Vue.createApp({
data() {
return {
question: '',
answer: 'Questions usually contain a question mark. ;-)'
}
},
watch: {
// whenever question changes, this function will run
question(newQuestion, oldQuestion) {
if (newQuestion.indexOf('?') > -1) {
this.getAnswer()
}
}
},
methods: {
getAnswer() {
this.answer = 'Thinking...'
axios
.get('https://yesno.wtf/api')
.then(response => {
this.answer = response.data.answer
})
.catch(error => {
this.answer = 'Error! Could not reach the API. ' + error
})
}
}
}).mount('#watch-example')
</script>
在这个示例中,使用 watch
选项允许我们执行异步操作 (访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
计算属性 vs 侦听器
Vue 提供了一种更通用的方式来观察和响应当前活动的实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,watch
很容易被滥用——特别是如果你之前使用过 AngularJS。然而,通常更好的做法是使用计算属性而不是命令式的 watch
回调。细想一下这个例子:
<div id="demo">{{ fullName }}</div>
const vm = Vue.createApp({
data() {
return {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
}
},
watch: {
firstName(val) {
this.fullName = val + ' ' + this.lastName
},
lastName(val) {
this.fullName = this.firstName + ' ' + val
}
}
}).mount('#demo')
上面代码是命令式且重复的。将它与计算属性的版本进行比较:
const vm = Vue.createApp({
data() {
return {
firstName: 'Foo',
lastName: 'Bar'
}
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
}).mount('#demo')
好很多了,不是吗?
class 的对象语法
在模板里定义对象
我们可以传给 :class
(v-bind:class
的简写) 一个对象,以动态地切换 class:
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
></div>
data() {
return {
isActive: true,
hasError: false
}
}
渲染的结果为:
<div class="static active"></div>
在 data 中定义对象
绑定的数据对象不必内联定义在模板里
<div :class="classObject"></div>
data() {
return {
classObject: {
active: true,
'text-danger': false
}
}
}
在计算属性里定义对象
我们也可以在这里绑定一个返回对象的计算属性。这是一个常用且强大的模式:
<div :class="classObject"></div>
data() {
return {
isActive: true,
error: null
}
},
computed: {
classObject() {
return {
active: this.isActive && !this.error,
'text-danger': this.error && this.error.type === 'fatal'
}
}
}
style
也可以用:
<div :style="styleObject"></div>
data() {
return {
styleObject: {
color: 'red',
fontSize: '13px'
}
}
}
数组语法
我们也可以把一个数组传给 :class
,以应用一个 class 列表:
<div :class="[activeClass, errorClass]"></div>
data() {
return {
activeClass: 'active',
errorClass: 'text-danger'
}
}
渲染的结果为:
<div class="active text-danger"></div>
如果你想根据条件切换列表中的 class,可以使用三元表达式:
<div :class="[isActive ? activeClass : '', errorClass]"></div>
这样写将始终添加 errorClass
,但是只有在 isActive
为 truthy 时才添加 activeClass
不过,当有多个条件 class 时这样写有些繁琐。所以在数组语法中也可以使用对象语法:
<div :class="[{ active: isActive }, errorClass]"></div>