Vue Basic Notes
Directives
Control Flow Directives
<template>
<p v-if="isShow">Show</p>
<p v-if="isEnabled">Enabled</p>
<p v-else>Disabled</p>
<p v-if="inventory > 10">In Stock</p>
<p v-else-if="inventory <= 10 && inventory > 0">Almost Sold Out</p>
<p v-else>Out of Stock</p>
<ul>
<!-- list -->
<li v-for="item in items" :key="item.id">{{ item.message }}</li>
<li v-for="(item, index) in items">{{ item.message }} {{ index }}</li>
<!-- destructed list -->
<li v-for="{ message } in items">{{ message }}</li>
<li v-for="({ message }, index) in items">{{ message }} {{ index }}</li>
<!-- nested list -->
<li v-for="item in items">
<span v-for="childItem in item.children">
{{ item.message }} {{ childItem }}
</span>
</li>
<!-- iterator list -->
<li v-for="item of items">{{ item.message }}</li>
<!-- object list -->
<li v-for="(value, key, index) in myObject">
{{ index }}. {{ key }}: {{ value }}
</li>
<!-- range list -->
<li v-for="n in 10">{{ n }}</li>
</ul>
</template>
Prefer v-show
if you need to toggle something very often (display: none
),
and prefer v-if
if the condition is unlikely to change at runtime (lifecycle called).
- 不要把
v-if
和v-for
同时用在同一个元素上, 会带来性能方面的浪费, 且有语法歧义: Vue 2.x 中v-for
优先级高于v-if
, Vue 3.x 中v-if
优先级高于v-for
. - 外层嵌套 template (页面渲染不生成 DOM 节点),
在这一层进行
v-if
判断, 然后在内部进行v-for
循环.
// Vue 2.x: compiler/codegen/index.js
export function genElement(el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre;
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state);
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state);
} else if (el.for && !el.forProcessed) {
return genFor(el, state);
} else if (el.if && !el.ifProcessed) {
return genIf(el, state);
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0';
} else if (el.tag === 'slot') {
return genSlot(el, state);
} else {
// component or element
}
}
<template v-if="isShow"><p v-for="item in items"></p></template>
Attributes Binding Directives
<template>
<a :href="url">Dynamic Link</a>
<img :src="link" :alt="description" />
<button :disabled="item.length === 0">Save Item</button>
</template>
Class and Style Binding Directives
- Static class.
- Array binding.
- Object binding.
<script setup lang="ts">
const isActive = ref(true);
const hasError = ref(false);
const activeClass = ref('active');
const errorClass = ref('text-danger');
const classObject = reactive({
active: true,
'text-danger': false,
});
const classObject = computed(() => ({
active: isActive.value && !error.value,
'text-danger': hashError.value,
}));
const activeColor = ref('red');
const fontSize = ref(30);
const styleObject = reactive({
color: 'red',
fontSize: '13px',
});
</script>
<template>
<div class="static"></div>
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<div :class="[isActive ? activeClass : '', errorClass]"></div>
<div :class="[{ active: isActive }, errorClass]"></div>
<div :class="classObject"></div>
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<div :style="[baseStyles, overridingStyles]"></div>
<div :style="styleObject"></div>
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
</template>
Event Handlers Directives
Event Handlers and Modifiers
<div id="handler">
<button @click="warn('Warn message.', $event)">Submit</button>
<button @click="one($event), two($event)">Submit</button>
<!-- 阻止单击事件继续传播 -->
<a @click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a @click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form @submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理, 然后才交由内部元素进行处理 -->
<div @click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div @click.self="doThat">...</div>
<!-- 点击事件将只会触发一次 -->
<a @click.once="doThis"></a>
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成 -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div @scroll.passive="onScroll">...</div>
<input @keyup.enter="submit" />
<input @keyup.tab="submit" />
<input @keyup.delete="submit" />
<input @keyup.esc="submit" />
<input @keyup.space="submit" />
<input @keyup.up="submit" />
<input @keyup.down="submit" />
<input @keyup.left="submit" />
<input @keyup.right="submit" />
<input @keyup.page-down="onPageDown" />
<input @keyup.ctrl.enter="clear" />
<input @keyup.alt.space="clear" />
<input @keyup.shift.up="clear" />
<input @keyup.meta.right="clear" />
<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>
<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>
<button @click.left="onClick">Left click</button>
<button @click.right="onClick">Right click</button>
<button @click.middle="onClick">Middle click</button>
</div>
Vue.createApp({
methods: {
warn(message, event) {
if (event) event.preventDefault();
alert(message);
},
one(event) {
if (event) event.preventDefault();
console.log('one');
},
two(event) {
if (event) event.preventDefault();
console.log('two');
},
},
}).mount('#inline-handler');
Custom Events
Form events:
app.component('CustomForm', {
emits: {
// 没有验证
click: null,
// 验证 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true;
} else {
console.warn('Invalid submit event payload!');
return false;
}
},
},
methods: {
customEvent() {
this.$emit('custom-event');
},
submitForm(email, password) {
this.$emit('submit', { email, password });
},
},
});
<custom-form
@click="handleClick"
@submit="handleSubmit"
@custom-event="handleEvent"
></custom-form>
Drag and Drop events:
<!-- Drag.vue -->
<template>
<div
draggable="true"
@dragenter.prevent
@dragover.prevent
@dragstart.self="onDrag"
>
<slot />
</div>
</template>
<script>
export default {
props: {
transferData: {
type: Object,
required: true,
},
},
methods: {
onDrag(e) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.dropEffect = 'move';
e.dataTransfer.setData('payload', JSON.stringify(this.transferData));
},
},
};
</script>
<!-- Drop.vue -->
<template>
<div @dragenter.prevent @dragover.prevent @drop.stop="onDrop">
<slot />
</div>
</template>
<script>
export default {
methods: {
onDrop(e) {
const transferData = JSON.parse(e.dataTransfer.getData('payload'));
this.$emit('drop', transferData);
},
},
};
</script>
Model Directives
本质为语法糖 (v-model = v-bind + v-on
):
<input v-model="searchText" />
<input :value="searchText" @input="searchText = $event.target.value" />
checkbox
/radio
:checked
property and@change
event.- Multiple
checkbox
: value array. select
:value
property and@change
event.text
/textarea
:value
property and@input
event.- Child component:
- Default:
value
property and@input
event. - Use
options.model
on Child component to change defaultv-bind
andv-on
.
- Default:
<input v-model="message" placeholder="edit me" />
<textarea v-model="message" placeholder="add multiple lines"></textarea>
<input type="radio" id="one" value="One" v-model="picked" />
<input type="radio" id="two" value="Two" v-model="picked" />
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
<input type="checkbox" id="john" value="John" v-model="checkedNames" />
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<select v-model="selected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<!-- Debounce -->
<input v-model.lazy="msg" />
<!-- 自动将用户的输入值转为数值类型 -->
<input v-model.number="age" type="number" />
<!-- 自动过滤用户输入的首尾空白字符 -->
<input v-model.trim="msg" /
Component v-model
directive:
<CustomInput v-model="searchText" />
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue']);
defineEmits(['update:modelValue']);
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<!-- CustomInput.vue -->
<script setup>
import { computed } from 'vue';
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
const value = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
},
});
</script>
<template>
<input v-model="value" />
</template>
Custom component v-model
name:
<!-- MyComponent.vue -->
<script setup>
defineProps(['title']);
defineEmits(['update:title']);
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
Custom component v-model
modifier:
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) },
});
const emit = defineEmits(['update:modelValue']);
function emitValue(e) {
let value = e.target.value;
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1);
}
emit('update:modelValue', value);
}
</script>
<template>
<input type="text" :value="modelValue" @input="emitValue" />
</template>
Custom Directives
Custom build-in directives:
<script setup lang="ts">
import { ref, vModelText } from 'vue';
vModelText.updated = (el, { value, modifiers: { capitalize } }) => {
if (capitalize && Object.hasOwn(value, 0)) {
el.value = value[0].toUpperCase() + value.slice(1);
}
};
const value = ref('');
</script>
<template>
<input v-model.capitalize="value" type="text" />
</template>
Custom new directives:
const vDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {},
};
Components
Computed Properties
<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';
},
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];
},
},
},
}).mount('#computed-basics');
Slots
- Web Slot
name
attribute.fallback
content.- 插槽基本目的为自定义组件渲染细节: e.g 高级列表组件.
- Normal Slots: 在父组件编译和渲染阶段生成 Slots VNodes, 数据作用域为父组件实例 (使用插槽的组件), 即父组件同时提供 View 与 Data.
- Scoped Slots:
在父组件编译和渲染阶段为
vnode.data
添加scopedSlots
对象, 在子组件编译和渲染阶段生成 Slots VNodes, 数据作用域为子组件实例 (定义插槽的组件), 即父组件提供 View, 子组件提供 Data.
Fallback Slots
<!-- SubmitButton -->
<button type="submit">
<slot>Submit</slot>
</button>
<SubmitButton></SubmitButton>
render to
<button type="submit">Submit</button>
<SubmitButton>Save</SubmitButton>
render to
<button type="submit">Save</button>
Named Slots
#
:v-slot
directive shorthand.
<!-- Layout -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<Layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</Layout>
Named slot directive shorthand:
<Layout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</Layout>
Scoped Slots
Pass data from child to parent
(like Render Props
in React):
app.component('TodoList', {
data() {
return {
items: ['Feed a cat', 'Buy milk'],
};
},
template: `
<ul>
<li v-for="( item, index ) in items">
<slot :item="item"></slot>
</li>
</ul>
`,
});
<TodoList>
<!-- `default` can be other named slots -->
<template v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</template>
</TodoList>
Slot props shorthand
(default
can be other named slots):
<TodoList v-slot="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</TodoList>
<TodoList v-slot="{ item }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</TodoList>
<TodoList v-slot="{ item: todo }">
<i class="fas fa-check"></i>
<span class="green">{{ todo }}</span>
</TodoList>
<TodoList v-slot="{ item = 'Placeholder' }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</TodoList>
<TodoList #default="{ item }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</TodoList>
Provide and Inject
Root provide
context value:
import { provide, ref } from 'vue';
const count = ref(0);
provide('key', count);
Child inject
context value:
import { inject } from 'vue';
const message = inject('message', defaultValue);
Composition API
Can't access to this
inside of setup
,
we cannot directly access this.$emit
or this.$route
anymore.
Setup Method
- Executes before
Components
,Props
,Data
,Methods
,Computed
properties,Lifecycle
methods. - Can't access
this
. - Can access
props
andcontext
:props
.context.attrs
.context.slots
.context.emit
.context.expose
.context.parent
.context.root
.
import { ref, toRefs } from 'vue';
// eslint-disable-next-line import/no-anonymous-default-export
export default {
setup(props, { attrs, slots, emit, expose }) {
const { title } = toRefs(props);
const count = ref(0);
const increment = () => ++count.value;
console.log(title.value);
return { title, increment };
},
};
Composition LifeCycle Hooks
onBeforeMount
.onMounted
.onBeforeUpdate
.onUpdated
.onBeforeUnmount
.onUnmounted
.onErrorCaptured
.onRenderTracked
.onRenderTriggered
.onActivated
.onDeactivated
.
beforeCreate
-> setup
-> created
:
No need for onBeforeCreate
and onCreated
hooks,
just put code in setup
methods.
Reactivity
Reactive Value
import { reactive, toRefs } from 'vue';
const book = reactive({
author: 'Vue Team',
year: '2020',
title: 'Vue 3 Guide',
description: 'You are reading this book right now ;)',
price: 'free',
});
const { author, title } = toRefs(book);
title.value = 'Vue 3 Detailed Guide';
console.log(book.title); // 'Vue 3 Detailed Guide'
Ref Value
ref
API:
import type { Ref } from 'vue';
import { isRef, reactive, ref, toRef, unref } from 'vue';
const count = ref(10);
const state = reactive({
foo: 1,
bar: 2,
});
const fooRef = toRef(state, 'foo');
console.log(isRef(count));
console.log(unref(count) === 10);
fooRef.value++;
console.log(state.foo === 2);
state.foo++;
console.log(fooRef.value === 3);
toRef
/toRefs
:
function toRef(reactive, key) {
const wrapper = {
get value() {
return reactive[key];
},
set value(val) {
reactive[key] = val;
},
};
Object.defineProperty(wrapper, '__v_isRef', {
value: true,
});
return wrapper;
}
function toRefs(reactive) {
const refs = {};
for (const key in reactive) {
refs[key] = toRef(reactive, key);
}
return refs;
}
proxyRefs
(auto unref):
function proxyRefs(target) {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
return result.__v_isRef ? result.value : result;
},
set(target, key, value, receiver) {
const result = target[key];
if (result.__v_isRef) {
result.value = value;
return true;
}
return Reflect.set(target, key, value, receiver);
},
});
}
当一个 ref
被嵌套在一个响应式对象中作为属性被访问或更改时,
会自动解包 (无需使用 .value
):
const count = ref(0);
const state = reactive({
count,
});
console.log(state.count); // 0
state.count = 1;
console.log(count.value); // 1
当 ref
作为响应式数组或 Map
原生集合类型的元素被访问时,
不会进行解包 (需要使用 .value
):
const books = reactive([ref('Vue 3 Guide')]);
console.log(books[0].value);
const map = reactive(new Map([['count', ref(0)]]));
console.log(map.get('count').value);
Computed Value
计算属性的计算函数应只做计算而没有任何其他的副作用, 不要在计算函数中做异步请求或者更改 DOM:
const count = ref(1);
const plusOne = computed(() => count.value + 1);
console.log(plusOne.value); // 2
plusOne.value++; // error
特殊情况下可通过 getter 和 setter 创建可写计算属性:
const count = ref(1);
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1;
},
});
plusOne.value = 1; // 可写计算属性
console.log(count.value); // 0
Watch Value
Watch single value:
// watching a getter
const state = reactive({ count: 0 });
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
);
// directly watching a ref
const count = ref(0);
watch(count, (count, prevCount) => {
/* ... */
});
Watch multiple value:
const firstName = ref('');
const lastName = ref('');
watch([firstName, lastName], (newValues, prevValues) => {
console.log(newValues, prevValues);
});
firstName.value = 'John'; // logs: ["John", ""] ["", ""]
lastName.value = 'Smith'; // logs: ["John", "Smith"] ["John", ""]
Watch reactive value:
const numbers = reactive([1, 2, 3, 4]);
watch(
() => [...numbers],
(numbers, prevNumbers) => {
console.log(numbers, prevNumbers);
}
);
numbers.push(5); // logs: [1,2,3,4,5] [1,2,3,4]
Watch deep object:
const state = reactive({
id: 1,
attributes: {
name: '',
},
});
watch(
() => state,
(state, prevState) => {
console.log('not deep', state.attributes.name, prevState.attributes.name);
}
);
watch(
() => state,
(state, prevState) => {
console.log('deep', state.attributes.name, prevState.attributes.name);
},
{ deep: true }
);
// 直接给 `watch()` 传入一个响应式对象, 会隐式地创建一个深层侦听器
watch(state, (state, prevState) => {
console.log('deep', state.attributes.name, prevState.attributes.name);
});
state.attributes.name = 'Alex'; // Logs: "deep" "Alex" "Alex"
Watch effect:
watchEffect()
会立即执行一遍回调函数,
Vue 会自动追踪副作用的依赖关系,
自动分析出响应源 (自动追踪所有能访问到的响应式属性):
const url = ref('https://...');
const data = ref(null);
watchEffect(async () => {
const response = await fetch(url.value);
data.value = await response.json();
});
Watch post effect:
默认情况下,
用户创建的 watcher 会在 Vue 组件更新前被调用,
DOM 仍为旧状态,
通过 flush: post
显示访问 Vue 更新后的 DOM:
watch(source, callback, {
flush: 'post',
});
watchEffect(callback, {
flush: 'post',
});
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
});
Composable Functions
组合式函数始终返回一个包含多个 ref 的普通的非响应式对象, 该对象在组件中被解构为 ref 之后仍可以保持响应性:
import { isRef, ref, unref, watchEffect } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
function doFetch() {
// 在请求之前重设状态...
data.value = null;
error.value = null;
// unref() 解包可能为 ref 的值
fetch(unref(url))
.then(res => res.json())
.then(json => (data.value = json))
.catch(err => (error.value = err));
}
if (isRef(url)) {
// 若输入的 URL 是一个 ref
// 那么启动一个响应式的请求
watchEffect(doFetch);
} else {
// 否则只请求一次
// 避免监听器的额外开销
doFetch();
}
return { data, error };
}
export function App() {
const { data, error } = useFetch('https://...');
}
import { produce } from 'immer';
import { shallowRef } from 'vue';
export function useImmer(baseState) {
const state = shallowRef(baseState);
const update = updater => {
state.value = produce(state.value, updater);
};
return [state, update];
}
Animation and Transition
v-enter-from
.v-enter-to
: CSS defaults.v-enter-active
.v-leave-from
: CSS defaults.v-leave-to
.v-leave-active
.name
: transition name (different fromv
).mode
:out-in
: rectify router components transition.in-out
.
.v-enter-from {
opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease-out;
}
}
.v-leave-to {
opacity: 0;
}
Fade Transition
<template>
<div>
<h1>This is the modal page</h1>
<button @click="toggleModal">Open</button>
<transition name="fade" mode="out-in">
<div v-if="isOpen" class="modal">
<p><button @click="toggleModal">Close</button></p>
</div>
</transition>
</div>
</template>
<style>
.fade-enter-from {
opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease-out;
}
}
.fade-leave-to {
opacity: 0;
}
</style>
<style>
@media only screen and (prefers-reduced-motion: no-preference) {
.enter,
.leave {
transition: opacity 0.5s ease-out;
}
}
.before-enter,
.leave {
opacity: 0;
}
.enter,
.before-leave {
opacity: 1;
}
</style>
<script>
function enter(el, done) {
el.classList.add('before-enter');
setTimeout(() => {
el.classList.remove('before-enter');
el.classList.add('enter');
}, 20);
setTimeout(() => {
el.classList.remove('enter');
done();
}, 500);
}
function leave(el, done) {
el.classList.add('before-leave');
setTimeout(() => {
el.classList.remove('before-leave');
el.classList.add('leave');
}, 0);
setTimeout(() => {
el.classList.remove('leave');
done();
}, 500);
}
</script>
Slide Transition
.slide-fade-enter-from {
opacity: 0;
transform: translateX(10px);
}
@media (prefers-reduced-motion: no-preference) {
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: all 0.2s ease;
}
}
.slide-fade-leave-to {
opacity: 0;
transform: translateX(-10px);
}
Transition Group
<template>
<div>
<input type="text" v-model="newContact" placeholder="Name" />
<button @click="addContact">Add Contact</button>
<button @click="sortContacts">Sort</button>
<transition-group name="slide-up" tag="ul" appear>
<li v-for="contact in contacts" :key="contact">{{ contact }}</li>
</transition-group>
</div>
</template>
.slide-up-enter-from {
opacity: 0;
transform: translateY(10px);
}
@media (prefers-reduced-motion: no-preference) {
.slide-up-enter-active {
transition: all 0.2s ease;
}
.slide-up-move {
transition: transform 0.8s ease-in;
}
}
Transition Hooks
:css="false"
tells Vue don't handle transition classes,
we're relying on JavaScript hooks instead.
When it comes to JavaScript animation library, transition JavaScript hooks helps a lot.
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
:css="false"
>
<div>Modal</div>
</transition>
<transition-group
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
:css="false"
>
>
<div class="card" v-for="card in cards" :key="card.id">
<p>{{ card.title }}</p>
</div>
</transition-group>
// eslint-disable-next-line import/no-anonymous-default-export
export default {
methods: {
beforeEnter(el) {},
enter(el, done) {
done();
},
afterEnter(el) {},
enterCancelled(el) {},
beforeLeave(el) {},
leave(el, done) {
done();
},
afterLeave(el) {},
leaveCancelled(el) {},
},
};
<template>
<transition appear @before-enter="beforeEnter" @enter="enter" :css="false">
<div class="card"></div>
</transition>
</template>
<script>
import gsap from 'gsap';
export default {
methods: {
beforeEnter(el) {
el.style.opacity = 0;
el.style.transform = 'scale(0, 0)';
},
enter(el, done) {
gsap.to(el, {
duration: 1,
opacity: 1,
scale: 1,
ease: 'bounce.inOut',
onComplete: done,
});
},
},
};
</script>
Transition Internals
Transition Component
const Transition = {
name: 'Transition',
setup(props, { slots }) {
return () => {
const innerVNode = slots.default();
innerVNode.transition = {
beforeEnter(el) {
el.classList.add('enter-from');
el.classList.add('enter-active');
},
enter(el) {
nextFrame(() => {
el.classList.remove('enter-from');
el.classList.add('enter-to');
el.addEventListener('transitionend', () => {
el.classList.remove('enter-to');
el.classList.remove('enter-active');
});
});
},
leave(el, performRemove) {
el.classList.add('leave-from');
el.classList.add('leave-active');
nextFrame(() => {
el.classList.remove('leave-from');
el.classList.add('leave-to');
el.addEventListener('transitionend', () => {
el.classList.remove('leave-to');
el.classList.remove('leave-active');
performRemove();
});
});
},
};
return innerVNode;
};
},
};
Transition Module
platforms/web/runtime/modules/transition.js
:
- 自动嗅探目标元素是否应用了 CSS 过渡或动画, 在恰当的时机添加/删除 CSS 类名.
- 过渡组件提供 JavaScript 钩子函数接口, 钩子函数将在恰当的时机被调用.
- 核心逻辑位于
enter()
与leave()
函数.
// eslint-disable-next-line import/no-anonymous-default-export
export default {
create: _enter,
activate: _enter,
remove(vnode: VNode, rm: Function) {
if (vnode.data.show !== true) {
leave(vnode, rm);
} else {
rm();
}
},
};
function _enter(_: any, vnode: VNodeWithData) {
if (vnode.data.show !== true) {
enter(vnode);
}
}
Transition Group Component
const TransitionGroup = defineComponent({
props: extend(
{
tag: String,
moveClass: String,
},
transitionProps
),
beforeMount() {
const update = this._update;
this._update = (vnode, hydrating) => {
// force removing pass
this.__patch__(
this._vnode,
this.kept,
false, // hydrating
true // removeOnly (!important, avoids unnecessary moves)
);
this._vnode = this.kept;
update.call(this, vnode, hydrating);
};
},
updated() {
const children: Array<VNode> = this.prevChildren;
const moveClass: string = this.moveClass || `${this.name || 'v'}-move'`;
if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
return;
}
// we divide the work into three loops to avoid mixing DOM reads and writes
// in each iteration - which helps prevent layout thrashing.
children.forEach(callPendingCbs);
children.forEach(recordPosition);
children.forEach(applyTranslation);
// force reflow to put everything in position
// assign to this to avoid being removed in tree-shaking
// $flow-disable-line
this._reflow = document.body.offsetHeight;
children.forEach((c: VNode) => {
if (c.data.moved) {
const el: any = c.elm;
const s: any = el.style;
addTransitionClass(el, moveClass);
s.transform = s.WebkitTransform = s.transitionDuration = '';
el.addEventListener(
transitionEndEvent,
(el._moveCb = function cb(e) {
if (e?.propertyName.endsWith('transform')) {
el.removeEventListener(transitionEndEvent, cb);
el._moveCb = null;
removeTransitionClass(el, moveClass);
}
})
);
}
});
},
methods: {
hasMove(el: any, moveClass: string): boolean {
if (!hasTransition) {
return false;
}
if (this._hasMove) {
return this._hasMove;
}
// Detect whether an element with the move class applied has
// CSS transitions. Since the element may be inside an entering
// transition at this very moment, we make a clone of it and remove
// all other transition classes applied to ensure only the move class
// is applied.
const clone: HTMLElement = el.cloneNode();
if (el._transitionClasses) {
el._transitionClasses.forEach((cls: string) => {
removeClass(clone, cls);
});
}
addClass(clone, moveClass);
clone.style.display = 'none';
this.$el.appendChild(clone);
const info: Object = getTransitionInfo(clone);
this.$el.removeChild(clone);
return (this._hasMove = info.hasTransform);
},
},
render(h: Function) {
const tag: string = this.tag || this.$vnode.data.tag || 'span';
const map: Object = Object.create(null);
const prevChildren: Array<VNode> = (this.prevChildren = this.children);
const rawChildren: Array<VNode> = this.$slots.default || [];
const children: Array<VNode> = (this.children = []);
const transitionData: Object = extractTransitionData(this);
for (let i = 0; i < rawChildren.length; i++) {
const c: VNode = rawChildren[i];
if (c.tag && c.key != null && String(c.key).indexOf('__vList') !== 0) {
children.push(c);
map[c.key] = c;
(c.data || (c.data = {})).transition = transitionData;
}
}
if (prevChildren) {
const kept: Array<VNode> = [];
const removed: Array<VNode> = [];
for (let i = 0; i < prevChildren.length; i++) {
const c: VNode = prevChildren[i];
c.data.transition = transitionData;
c.data.pos = c.elm.getBoundingClientRect();
if (map[c.key]) {
kept.push(c);
} else {
removed.push(c);
}
}
this.kept = h(tag, null, kept);
this.removed = removed;
}
return h(tag, null, children);
},
});
Children
从旧位置按照的缓动时间过渡偏移到当前目标位置,
实现 Move
的过渡动画:
function callPendingCbs(c: VNode) {
if (c.elm._moveCb) {
c.elm._moveCb();
}
if (c.elm._enterCb) {
c.elm._enterCb();
}
}
function recordPosition(c: VNode) {
c.data.newPos = c.elm.getBoundingClientRect();
}
function applyTranslation(c: VNode) {
const oldPos = c.data.pos;
const newPos = c.data.newPos;
const dx = oldPos.left - newPos.left;
const dy = oldPos.top - newPos.top;
if (dx || dy) {
c.data.moved = true;
const s = c.elm.style;
s.transform = s.WebkitTransform = `translate(${dx}px,${dy}px)`;
s.transitionDuration = '0s';
}
}
Modern Vue
Async Component
// 1. Basic async component:
Vue.component('AsyncExample', function (resolve, reject) {
// 这个特殊的 require 语法告诉 webpack
// 自动将编译后的代码分割成不同的块,
// 这些块将通过 Ajax 请求自动下载.
require(['./my-async-component'], resolve);
});
// 2. Promise async component:
Vue.component(
'AsyncWebpackExample',
// 该 `import` 函数返回一个 `Promise` 对象.
() => import('./my-async-component')
);
// 3. Advanced async component:
const AsyncComp = () => ({
// 需要加载的组件, 应当是一个 Promise.
component: import('./MyComp.vue'),
// 加载中应当渲染的组件.
loading: LoadingComp,
// 出错时渲染的组件.
error: ErrorComp,
// 渲染加载中组件前的等待时间, 默认: 200ms.
delay: 200,
// 最长等待时间, 超出此时间则渲染错误组件, 默认: Infinity.
timeout: 3000,
});
Vue.component('AsyncExample', AsyncComp);
Resolve Async Component
core/vdom/helpers/resolve-async-component.js
:
- 3 种异步组件的实现方式.
- 高级异步组件实现了 loading/resolve/reject/timeout 4 种状态.
- 异步组件实现的本质是 2 次渲染:
- 第一次渲染生成一个注释节点/
<LoadingComponent>
. - 当异步获取组件成功后, 通过
forceRender
强制重新渲染.
- 第一次渲染生成一个注释节点/
import {
hasSymbol,
isDef,
isObject,
isPromise,
isTrue,
isUndef,
once,
remove,
} from 'core/util/index';
import { createEmptyVNode } from 'core/vdom/vnode';
import { currentRenderingInstance } from 'core/instance/render';
export function resolveAsyncComponent(
factory: Function,
baseCtor: Class<Component>
): Class<Component> | void {
// 3.
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp;
}
if (isDef(factory.resolved)) {
return factory.resolved;
}
const owner = currentRenderingInstance;
if (owner && isDef(factory.owners) && !factory.owners.includes(owner)) {
// already pending
factory.owners.push(owner);
}
// 3.
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp;
}
if (owner && !isDef(factory.owners)) {
const owners = (factory.owners = [owner]);
let sync = true;
let timerLoading = null;
let timerTimeout = null;
owner.$on('hook:destroyed', () => remove(owners, owner));
const forceRender = (renderCompleted: boolean) => {
for (let i = 0, l = owners.length; i < l; i++) {
owners[i].$forceUpdate();
}
if (renderCompleted) {
owners.length = 0;
if (timerLoading !== null) {
clearTimeout(timerLoading);
timerLoading = null;
}
if (timerTimeout !== null) {
clearTimeout(timerTimeout);
timerTimeout = null;
}
}
};
const resolve = once((res: Object | Class<Component>) => {
// cache resolved
factory.resolved = ensureCtor(res, baseCtor);
// invoke callbacks only if this is not a synchronous resolve
// (async resolves are shimmed as synchronous during SSR)
if (!sync) {
forceRender(true);
} else {
owners.length = 0;
}
});
const reject = once(reason => {
if (isDef(factory.errorComp)) {
factory.error = true;
forceRender(true);
}
});
const res = factory(resolve, reject);
if (isObject(res)) {
if (isPromise(res)) {
// 2. () => Promise.
if (isUndef(factory.resolved)) {
res.then(resolve, reject);
}
} else if (isPromise(res.component)) {
// 3.
res.component.then(resolve, reject);
if (isDef(res.error)) {
factory.errorComp = ensureCtor(res.error, baseCtor);
}
if (isDef(res.loading)) {
factory.loadingComp = ensureCtor(res.loading, baseCtor);
if (res.delay === 0) {
factory.loading = true;
} else {
timerLoading = setTimeout(() => {
timerLoading = null;
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true;
forceRender(false);
}
}, res.delay || 200);
}
}
if (isDef(res.timeout)) {
timerTimeout = setTimeout(() => {
timerTimeout = null;
if (isUndef(factory.resolved)) {
reject(null);
}
}, res.timeout);
}
}
}
sync = false;
// return in case resolved synchronously
return factory.loading ? factory.loadingComp : factory.resolved;
}
}
function ensureCtor(comp: any, base) {
if (comp.__esModule || (hasSymbol && comp[Symbol.toStringTag] === 'Module')) {
comp = comp.default;
}
return isObject(comp) ? base.extend(comp) : comp;
}
function createAsyncPlaceholder(
factory: Function,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag: ?string
): VNode {
const node = createEmptyVNode();
node.asyncFactory = factory;
node.asyncMeta = { data, context, children, tag };
return node;
}
Define Async Component
defineAsyncComponent
higher order component:
function defineAsyncComponent({
loader,
timeout,
loadingComponent,
errorComponent,
}) {
let InnerComponent = null;
return {
name: 'AsyncComponentWrapper',
setup() {
const loaded = ref(false);
const error = shallowRef(null);
let timer = null;
loader()
.then(Component => {
InnerComponent = Component;
loaded.value = true;
})
.catch(err => (error.value = err));
if (timeout) {
timer = setTimeout(() => {
const err = new Error(
`Async component timed out after ${timeout}ms.`
);
error.value = err;
}, timeout);
}
onUnmounted(() => clearTimeout(timer));
const placeholder = { type: Text, children: '' };
// Return render function.
return () => {
if (loaded.value) {
return { type: InnerComponent };
} else if (error.value && errorComponent) {
return { type: errorComponent };
} else {
return loadingComponent ? { type: loadingComponent } : placeholder;
}
};
},
};
}
Suspense
<!-- Events.vue -->
<script setup lang="ts">
import { getEvents } from '@/services';
import type { Event } from '@/services';
const events: Event[] = await getEvents();
</script>
<template>
<suspense>
<template #default>
<Events />
</template>
<template #fallback>
<div>Loading events list ...</div>
</template>
</suspense>
</template>
Keep Alive
include
:string | RegExp | Array<string>
, 匹配的组件会被缓存.exclude
:string | RegExp | Array<string>
, 匹配的组件不会被缓存.max
: 缓存大小.- 组件一旦被
<keep-alive>
缓存, 再次渲染的时候不会执行created
/mounted
等钩子函数 (core/vdom/create-component.js
). 但会执行activated
/deactivated
钩子函数 (core/vdom/create-component.js
/core/instance/lifecycle.js
).
// core/components/keep-alive.js
const KeepAlive = defineComponent({
name: 'KeepAlive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number],
},
created() {
this.cache = Object.create(null);
this.keys = [];
},
mounted() {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name));
});
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name));
});
},
unmounted() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys);
}
},
render() {
// eslint-disable-next-line vue/require-slots-as-functions
const slot = this.$slots.default;
const vnode: VNode = getFirstComponentChild(slot);
const componentOptions: ?VNodeComponentOptions =
vnode && vnode.componentOptions;
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions);
const { include, exclude } = this;
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode;
}
const { cache, keys } = this;
const key: ?string =
vnode.key == null
? // same constructor may get registered as different local components
// so cid alone is not enough (#3269)
componentOptions.Ctor.cid +
(componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key;
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
remove(keys, key);
keys.push(key);
} else {
cache[key] = vnode;
keys.push(key);
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
vnode.data.keepAlive = true;
}
return vnode || (slot && slot[0]);
},
});
const KeepAlive = {
__isKeepAlive: true,
props: {
include: RegExp,
exclude: RegExp,
},
setup(props, { slots }) {
const instance = currentInstance;
const { move, createElement } = instance.keepAliveCtx;
const storageContainer = createElement('div');
instance._deActivate = vnode => {
move(vnode, storageContainer);
};
instance._activate = (vnode, container, anchor) => {
move(vnode, container, anchor);
};
return () => {
const rawVNode = slots.default();
if (typeof rawVNode.type !== 'object') {
return rawVNode;
}
const name = rawVNode.type.name;
if (
name &&
((props.include && !props.include.test(name)) ||
(props.exclude && props.exclude.test(name)))
) {
return rawVNode;
}
const cachedVNode = cache.get(rawVNode.type);
if (cachedVNode) {
rawVNode.component = cachedVNode.component;
rawVNode.keptAlive = true;
} else {
cache.set(rawVNode.type, rawVNode);
}
rawVNode.shouldKeepAlive = true;
rawVNode.keepAliveInstance = instance;
return rawVNode;
};
},
};
Teleport
const Teleport = {
__isTeleport: true,
process(n1, n2, container, anchor, internals) {
const { patch, patchChildren, move } = internals;
if (!n1) {
// 挂载
const target =
typeof n2.props.to === 'string'
? document.querySelector(n2.props.to)
: n2.props.to;
n2.children.forEach(c => patch(null, c, target, anchor));
} else {
// 更新
patchChildren(n1, n2, container);
if (n2.props.to !== n1.props.to) {
const newTarget =
typeof n2.props.to === 'string'
? document.querySelector(n2.props.to)
: n2.props.to;
n2.children.forEach(c => move(c, newTarget));
}
}
},
};
Client Only
const ClientOnly = defineComponent({
setup(_, { slots }) {
const show = ref(false);
// `onMounted()` hooks 只在客户端执行,
// 服务端渲染时不执行.
onMounted(() => {
show.value = true;
});
return () => (show.value && slots.default ? slots.default() : null);
},
});
Vue Router
Basic Routes
import type { RouteRecordRaw } from 'vue-router';
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/about',
name: 'About',
component: () =>
import(/* webpackChunkName: "about" */ '../views/About.vue'),
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
createApp(App).use(store).use(router).mount('#app');
Dynamic Routes
Two methods to access route params
in components:
- Composition route API:
const { params } = useRoute()
. - Passing route props to component:
const props = defineProps<{ id: string }>()
:props
better testing friendly.props
better TypeScript types inference.
<template>
<router-link
class="event-link"
:to="{ name: 'EventDetails', params: { id: event.id } }"
>
<div class="event-card">
<span>@{{ event.time }} on {{ event.date }}</span>
<h4>{{ event.title }}</h4>
</div>
</router-link>
</template>
Can't access to this
inside of setup
,
we cannot directly access this.$router
or this.$route
anymore.
Routes Composition API
import type { RouteRecordRaw } from 'vue-router';
import { createRouter, createWebHistory } from 'vue-router';
import EventDetails from '../views/EventDetails.vue';
const routes: Array<RouteRecordRaw> = [
{
path: '/event/:id',
name: 'EventDetails',
component: EventDetails,
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { getEvent } from '@/services';
import type { Event } from '@/services';
const { params } = useRoute();
const event: Event = await getEvent(Number.parseInt(params.id));
</script>
Passing Routes Props
import type { RouteRecordRaw } from 'vue-router';
import { createRouter, createWebHistory } from 'vue-router';
import EventDetails from '../views/EventDetails.vue';
const routes: Array<RouteRecordRaw> = [
{
path: '/event/:id',
name: 'EventDetails',
component: EventDetails,
props: true /* Passing route props to component */,
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;
<script setup lang="ts">
import { getEvent } from '@/services';
import type { Event } from '@/services';
const props = defineProps<{ id: string }>();
const event: Event = await getEvent(Number.parseInt(props.id));
</script>
Named Routes
const routes = [
{
path: '/user/:username',
name: 'User',
component: User,
},
];
<router-link :to="{ name: 'User', params: { username: 'sabertaz' }}">
User
</router-link>
router.push({ name: 'User', params: { username: 'sabertaz' } });
Nested Routes
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'board',
component: Board,
children: [
{
path: 'task/:id',
name: 'task',
component: Task,
props: true,
},
],
},
];
<!-- App.vue -->
<!-- Root router view -->
<template><router-view /></template>
<!-- Board.vue -->
<!-- Nested router view -->
<template>
<div>Board View</div>
<router-view />
</template>
<!-- Task.vue -->
<script setup lang="ts">
defineProps<{ id: string }>();
</script>
<template>
<div>Task View {{ id }}</div>
</template>
Programmatic Routes Navigation
import { useRouter } from 'vue-router';
function App() {
const router = useRouter();
}
Navigate to Different Location
const username = 'eduardo';
// we can manually build the url but we will have to handle the encoding ourselves
router.push(`/user/${username}`); // -> /user/eduardo
// same as
router.push({ path: `/user/${username}` }); // -> /user/eduardo
// if possible use `name` and `params` to benefit from automatic URL encoding
router.push({ name: 'user', params: { username } }); // -> /user/eduardo
// `params` cannot be used alongside `path`
router.push({ path: '/user', params: { username } }); // -> /user
// literal string path
router.push('/users/eduardo');
// object with path
router.push({ path: '/users/eduardo' });
// named route with params to let the router build the url
router.push({ name: 'user', params: { username: 'eduardo' } });
// with query, resulting in /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } });
// with hash, resulting in /about#team
router.push({ path: '/about', hash: '#team' });
Replace Current Location
// replace current location
router.push({ path: '/home', replace: true });
// equivalent to
router.replace({ path: '/home' });
Traverse Routes History
// go forward by one record, the same as router.forward()
router.go(1);
// go back by one record, the same as router.back()
router.go(-1);
// go forward by 3 records
router.go(3);
// fails silently if there aren't that many records
router.go(-100);
router.go(100);
Navigation Guard Routes
Guard Routes Configuration
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false;
},
},
];
beforeEnter
guards only trigger when entering the route,
don't trigger when the params, query or hash change.
Going from /users/2
to /users/3
or going from /users/2#info
to /users/2#projects
don't trigger beforeEnter
guards.
Global Navigation Guards
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' });
else next();
});
router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission();
} catch (error) {
if (error instanceof NotAllowedError) {
// Handle the error and then cancel the navigation.
return false;
} else {
// Unexpected error: cancel the navigation and pass error to global handler.
throw error;
}
}
}
});
router.afterEach((to, from, failure) => {
if (!failure) sendToAnalytics(to.fullPath);
});
Full Navigation Resolution Flow
- Navigation triggered.
- Call
beforeRouteLeave
guards in deactivated components. - Call global
beforeEach
guards. - Call
beforeRouteUpdate
guards in reused components. - Call
beforeEnter
in route configs. - Resolve async route components.
- Call
beforeRouteEnter
in activated components. - Call global
beforeResolve
guards. - Navigation is confirmed.
- Call global
afterEach
hooks. - DOM updates triggered.
- Call
next
callbacks inbeforeRouteEnter
guards with instantiated instances.
Vuex
Vuex Types
- Vuex types guide.
// store.ts
import type { InjectionKey } from 'vue';
import type { Store } from 'vuex';
import { createStore, useStore } from 'vuex';
// define your typings for the store state
interface State {
count: number;
}
// define injection key
const key: InjectionKey<Store<State>> = Symbol('key');
const store = createStore<State>({
state: {
count: 0,
},
});
const useAppStore = () => useStore<State>(key);
export { key, useAppStore };
export type { State };
export default store;
// main.ts
import { createApp } from 'vue';
import store, { key } from './store';
const app = createApp({});
// pass the injection key
app.use(store, key);
app.mount('#app');
// in a vue component
import { useAppStore } from './store';
// eslint-disable-next-line import/no-anonymous-default-export
export default {
setup() {
// eslint-disable-next-line react-hooks/rules-of-hooks
const store = useAppStore();
const count = store.state.count; // typed as number
},
};
Vite
Basic Configuration
import path from 'node:path';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
base: '/awesome-web/vue-trello/',
plugins: [vue()],
resolve: {
alias: {
src: path.resolve(__dirname, './src'),
},
},
});
Environment Variables and Modes
import.meta.env.MODE
: {string} running mode.import.meta.env.BASE_URL
: {string} vitebase
url.import.meta.env.PROD
: {boolean} whether in production.import.meta.env.DEV
: {boolean} whether in development.
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
interface ImportMetaEnv extends Readonly<Record<string, string>> {
readonly