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 VITE_APP_TITLE: string;
// More environment variables...
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
.env # Loaded in all cases.
.env.local # Loaded in all cases, ignored by git.
.env.[mode] # Only loaded in specified mode.
.env.[mode].local # Only loaded in specified mode, ignored by git.
Vue CLI
SCSS Configuration
Every element and every style for this scoped styled component
will have a data-v-2929
on them at runtime.
If import a Sass file into component that has actual styles in it,
Vue (via webpack) will pull in those styles and
"namespace" them with that dynamic data-
attribute.
The result is that is include Bulma
in your many times
with a bunch of data-v
weirdness in front of it.
/* bulma-custom.scss */
@import url('./variables.scss');
/* UTILITIES */
@import url('bulma/sass/utilities/animations.sass');
@import url('bulma/sass/utilities/controls.sass');
@import url('bulma/sass/utilities/mixins.sass');
/* etc... */
/* site.scss */
@import url('https://use.fontawesome.com/releases/v5.6.3/css/all.css');
@import url('./bulma-custom.scss');
html,
body {
height: 100%;
background-color: #f9fafc;
}
/* etc... */
// main.js
import Vue from 'vue';
import App from './App.vue';
import router from './router';
// import styles
import '@/styles/site.scss';
// webpack.config.js
module.exports = {
css: {
loaderOptions: {
sass: {
data: `@import "@/styles/variables.scss";`,
},
},
},
};
Vue Best Practice
When it comes to Vue 3, Evan You recommended:
- Use SFC +
<script setup>
+ Composition API (drop Options API). - Use VSCode + Volar.
- Not strictly required for TS, but if applicable, use Vite for build tooling.
Original intention for supporting both APIs: existing Options-API-based codebases can benefit from Composition API-based libraries, It's not for new codebases to mix Composition API and Options API.
Intentionally mixing Composition API and Options API should be avoided except in existing Options API codebases, to either replace mixins or leverage a Composition API-based library.