Vue tutorial
参考:教程 | Vue.js (vuejs.org)
该教程需要前置知识:HTML, CSS, JavaScript
学习前置知识,你可以去 MDN
Vue framework 是一个 JavaScript framework,以下简称 Vue,下面是它的特点
- 声明式渲染(Declarative Rendering):即声明 JavaScript 对象,改变对象状态来更改 HTML,这个过程由 Vue 完成
- 响应式(Reactivity):JavaScript 的对象状态改变会马上反映到 DOM(不知道 DOM 的去查 MDN 文档)
Declarative Rendering and Reactivity
Vue 实现了:
JavaScript Obejct <-> Vue <-> DOM
但是 Vue 显然是利用 JavaScript 机制,那就是 Proxy,Proxy 可以实现
JavaScript Object <-> Proxy <-> DOM
所以 Vue 把 Proxy 改造后封装成了 reactive()
,调用这个 API 会返回一个特殊的对象,称之为响应式对象(reactive object)。
reactive()
import { reactive } from 'vue'
const counter = reactive({
count: 0
})
console.log(counter.count) // 0
counter.count++
但是 reactive()
参数只能是对象(还有数组和内置类型),Vue 又把 reactive()
改造封装成了 ref()
,它也会返回一个响应式对象,并且带一个 .value
property,只不过它的参数可以填写值。
ref()
import { ref } from 'vue'
const message = ref('Hello World!')
console.log(message.value) // "Hello World!"
message.value = 'Changed'
以上给 ref()
或 reactive()
填写参数得到响应式对象的过程,就被成为数据绑定(data binding)。
Template syntax
在这里复习一下 HTML element 和 attribute 概念
Anatomy of an HTML element
这是 HTML element
这是 HTML attribute
在此之中,
Class
为 attribute name,editor-note
为 attribute value
Vue 自己创造了一套 template language。最基本的数据绑定是文本插值(Text Interpolation),它可以改变 element 的 content,像这样
<span>{{ message }}</span>
这种语法被成为 “Mustache”语法 (即双大括号)。再结合上节讲到的 reactive object,我们可以这样写
import { ref } from 'vue'
const message = ref('Hello World!')
效果是这样的
可能你会有些疑问,为什么不是写 {{message.value}}
,因为它是 top-level property,会自动解包(unwrapping)
const object = { id: ref(1) }
比如这个里面,id
就不是 top-level property,如果你这么写
{{ object.id + 1 }}
渲染的结果将是 [object Object]1
你需要手动解包,才会渲染出 2
{{ object.id.value + 1 }}
Directive
v-xxx
就是一种 attribute,在它的 template language 中被称为 directive
v-bind
Attribute bind
在 Vue 中,Mustache 语法只能用于文本插值来改变 element content,没法儿操作 element attribute。而且 element attribute 是静态的,为了给 element attribute 绑定一个动态值,需要使用 Vue 的 v-bind
directive
<div v-bind:id="dynamicId"></div>
冒号后面的 id
被称为 directive 的参数(argument),dynamicId
则为参数值,它会和响应式对象的 property 同步。它可以简写为
<div :id="dynamicId"></div>
例子:
<script setup>
import { ref } from 'vue'
const titleClass = ref('title')
</script>
<template>
<h1 :class="titleClass">Make me red</h1>
</template>
<style>
.title {
color: red;
}
</style>
v-on
Event Listen
可以通过 v-on
来监听 DOM event
<button v-on:click="increment">{{ count }}</button>
简写
<button @click="increment">{{ count }}</button>
点击 button 会触发 increment()
这个函数。
例子:
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">count is: {{ count }}</button>
</template>
v-model
Form bind
同时使用 v-bind
和 v-on
对表单(form)进行绑定和监听
<script setup>
import { ref } from 'vue'
const text = ref('')
function onInput(e) {
text.value = e.target.value
}
</script>
<template>
<input :value="text" @input="onInput" placeholder="Type here">
<p>{{ text }}</p>
</template>
啊,这么写实在太麻烦,所以 vue 提供了 v-model
。当然,最好看一下它支持哪些 element。
<script setup>
import { ref } from 'vue'
const text = ref('')
</script>
<template>
<input v-model="text" placeholder="Type here">
<p>{{ text }}</p>
</template>
v-if and v-else
Conditional Rendering
v-if
和 v-else
可以根据条件来决定 element 是否在 DOM 中。
例子:
<script setup>
import { ref } from 'vue'
const awesome = ref(true)
function toggle() {
awesome.value = !awesome.value
}
</script>
<template>
<button @click="toggle">toggle</button>
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
</template>
改变 awesome 的值来显示 “Vue is awesome!” 和 “Oh no 😢”。
v-for
当你想写一个列表时,一个个写列表的 element 实在太累了,如果有 1000 个那不就完蛋了,所以 v-for 可以通过循环,直接渲染出列表(当然,你得给相应的数据)
<script setup>
import { ref } from 'vue'
// 给每个 todo 对象一个唯一的 id
let id = 0
const newTodo = ref('')
const todos = ref([
{ id: id++, text: 'Learn HTML' },
{ id: id++, text: 'Learn JavaScript' },
{ id: id++, text: 'Learn Vue' }
])
function addTodo() {
todos.value.push({ id: id++, text: newTodo.value })
newTodo.value = ''
}
function removeTodo(todo) {
todos.value = todos.value.filter((t) => t !== todo)
}
</script>
<template>
<form @submit.prevent="addTodo">
<input v-model="newTodo" required placeholder="new todo">
<button>Add Todo</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
<button @click="removeTodo(todo)">X</button>
</li>
</ul>
</template>
computed()
template 里面可以写这种计算表达式
<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
但是这种计算写在 template 里是真的不好理解,个人感觉在结构上 View 里不能有逻辑,所以尽量不要这么写。
所以我们可以用 computed()
,这样
<script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
它和 method()
最大区别在于,它是只有在响应式对象更新时才会重新被调用和计算,否则就会直接返回缓存值,即 books 不变,重渲染(比如页面刷新,更新)时 computed()
不会被调用,而 method()
则会。插嘴一句 method()
,重渲染时总会被调用。
Lifecycle and Template Refs
手动用 JavaScript 操作 DOM 是一件苦差事,所以我们用 vue 来帮忙,但是有时我们不得不操作 DOM,这个时候我们就得使用模板引用(template ref)
这个时候就要使用 ref
attribute
<p ref="pElementRef">hello</p>
如果要访问这个 ref,我们需要声明(declare)ref 并初始化
const pElementRef = ref(null)
注意我们使用给 ref 的 argument 为 null,这是因为<script setup>
执行时,DOM 还没有初始化,template ref 只能在挂在(mount)后访问,所以我们可以使用生命周期钩子(lifecycle hook)比如 onMounted()
,关于 lifecycle 请看生命周期图示
例子:
<script setup>
import { ref, onMounted } from 'vue'
const pElementRef = ref(null)
onMounted(() => {
pElementRef.value.textContent = 'mounted!'
})
</script>
<template>
<p ref="pElementRef">hello</p>
</template>
watch()
watch()
可以监察一个 ref,并触发一个 callback function,比如下面的例子就是监察 todoId
,触发 fetchData
<script setup>
import { ref, watch } from 'vue'
const todoId = ref(1)
const todoData = ref(null)
async function fetchData() {
todoData.value = null
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
todoData.value = await res.json()
}
fetchData()
watch(todoId, fetchData)
</script>
<template>
<p>Todo id: {{ todoId }}</p>
<button @click="todoId++" :disabled="!todoData">Fetch next todo</button>
<p v-if="!todoData">Loading...</p>
<pre v-else>{{ todoData }}</pre>
</template>
Component
Vue application 常常是多个 component 嵌套创建,所以就有 parent component 包含 child component
如果要使用 child component,就需要导入它
import ChildComp from './ChildComp.vue'
使用 child component
<ChildComp />
例子:
<!--App.vue-->
<script setup>
import ChildComp from './ChildComp.vue'
</script>
<template>
<ChildComp />
</template>
Props
child component 可以通过 props 从 parent component 获取动态数据
<!--ChildComp.vue-->
<script setup>
const props = defineProps({
msg: String
})
</script>
注意,
defineProps()
是一个 runtime marcro,不需要导入。
这样,msg 就可以在 child component 的 <template>
中使用
<template>
<h2>{{ msg || 'No props passed yet' }}</h2>
</template>
而 parent component 则可以用 v-bind
传递数据
<!--App.vue-->
<ChildComp :msg="greeting" />
例子:
<!--App.vue-->
<script setup>
import { ref } from 'vue'
import ChildComp from './ChildComp.vue'
const greeting = ref('Hello from parent')
</script>
<template>
<ChildComp :msg="greeting" />
</template>
<!--ChildComp.vue-->
<script setup>
const props = defineProps({
msg: String
})
</script>
<template>
<h2>{{ msg || 'No props passed yet' }}</h2>
</template>
child component 中采用的 “runtime declaration”,还有一种是如果你用 typescript,需要采用 “type-based declaration”,具体看官方文档。
Emits
child component 可以向 parent component 传 event,emit()
中,第一个 argument 是 event name,其他的会传给 event listener。
parent component 可以通过 v-on
监听 child-emitted event,并且可以将额外的 argument 赋值给 local state
<!--App.vue-->
<script setup>
import { ref } from 'vue'
import ChildComp from './ChildComp.vue'
const childMsg = ref('No child msg yet')
</script>
<template>
<ChildComp @response="(msg) => childMsg = msg" />
<p>{{ childMsg }}</p>
</template>
<!--ChildComp.vue-->
<script setup>
const emit = defineEmits(['response'])
emit('response', 'hello from child')
</script>
<template>
<h2>Child component</h2>
</template>
Solots
除了 props,parent component 可以将 template 片段传给 child component,而在 child component,则可以使用 <slot>
来显示片段的内容。
<!--App.vue-->
<script setup>
import { ref } from 'vue'
import ChildComp from './ChildComp.vue'
const msg = ref('from parent')
</script>
<template>
<ChildComp>Message: {{ msg }}</ChildComp>
</template>
<template>
<slot>Fallback content</slot>
</template>
以上 parent component 中,<ChildComp>
的内容会传给 child component 中的 <slot>
,最后渲染在 parent component 上。
Essential
Create application
application instance and root component
Vue application 通常是通过 createApp
来创建一个 application instance
而 createApp
的参数则被称为 root component,而且其他 component 则作为 root 的 child component,所以 vue application 是由 root component 和 child component 组成的。
一般这种创建代码都在 <project-name>/src/main.js
<!--main.js-->
import { createApp } from 'vue'
// 导入一个单组件
import App from './App.vue'
// 将这个单组件作为根组件
const app = createApp(App)
一个 Todo application 的例子
App (root component)
├─ TodoList
│ └─ TodoItem
│ ├─ TodoDeleteButton
│ └─ TodoEditButton
└─ TodoFooter
├─ TodoClearButton
└─ TodoStatistics
mount application
application instance 必须调用 mount()
才能渲染,而必须的 argument 则为 DOM 的 element 或者 CSS selector
在 vue project 中一般在 <project-name>/index.html
<div id="app"></div>
注意
mount()
应该始终在应用配置完成后调用,简单点儿说就是最后调用。
Applicaiton configuration
application instance 会提供一个 config
object,这样就可以配置 vue app,比如 app-level option,capture error
app.config.errorHandler = (err) => {
/* 处理错误 */
}
或者 component registration
比如 global registration
app.component('TodoDeleteButton', TodoDeleteButton)
其他细节清参考 vue 文档。
Component registration
如果要使用 component,则必须是 registered
Global registration
使用 .component()
method:
import { createApp } from 'vue'
const app = createApp({})
app.component(
// the registered name
'MyComponent',
// the implementation
{
/* ... */
}
)
如果使用 SFC,则
import MyComponent from './App.vue'
app.component('MyComponent', MyComponent)
Local registration
使用 component
option
import ComponentA from './ComponentA.js'
export default {
components: {
ComponentA
},
setup() {
// ...
}
}
使用 SFC
<script setup>
import ComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
Toolchain
当你创建一个 vue project 时,手动新建目录和文件是很麻烦的事情,所有我们有项目脚手架(Project Scaffolding)来自动创建 project 基本的目录和文件。
Vite
尤雨溪开发的 build tool,支持 SFC,通过 Vite 创建项目:
npm create vue@latest
Vue CLI
基于 webpack 的 build tool,但现在是维护状态,建议使用 Vite。