Vue3
MVVM 架构
Section titled “MVVM 架构”MVVM, Model-View-ViewModel
- View 视图层, Vanilla DOM
- ViewModel 视图模型层: Vue
- Model 模型层: Vanilla JavaScript
使用 vscode 调试
Section titled “使用 vscode 调试”{ "version": "0.2.0", "configurations": [ { "type": "chrome", "request": "launch", "name": "Vue: chrome", "url": "http://localhost:5173", "webRoot": "${workspaceFolder}/src" } ]}Vue 新特性
Section titled “Vue 新特性”重写双向数据绑定
Section titled “重写双向数据绑定”- Vue2 的双向数据绑定基于
Object.defineProperty(); 创建一个 Vue 实例时, for…in 遍历 data 字段中的所有属性, 使用Object.defineProperty()将属性转换为 getter 和 setter - Vue3 的双向数据绑定基于 Proxy 代理对象
优点
- 不需要数据备份
- 可以监听数组的索引和 length 属性
- 可以监听新增属性, 删除属性操作
虚拟 DOM 性能优化
Section titled “虚拟 DOM 性能优化”- Vue2 中, 每次使用 diff 算法更新虚拟 DOM 时, 都是全量对比
- Vue3 中, 每次使用 diff 算法更新虚拟 DOM 时, 只对比有 patch 标记的节点
Vue Fragments
Section titled “Vue Fragments”Vue 允许组件有多个根节点, 支持 JSX, TSX
<template> <div>root1</div> <div>root2</div></template>render() { return ( <> <div>root1</div> <div>root2</div> </> )}创建 Vue 项目
Section titled “创建 Vue 项目”Vue 脚手架
Section titled “Vue 脚手架”pnpm create vue@latestpnpm dlx create-vue@latest
pnpm create vite@latestpnpm dlx create-vite@latestVue 项目结构
Section titled “Vue 项目结构”- public 公有目录会被直接
cp -r到 dist 目录下, 不会被 vite 打包 - src/assets 静态资源目录会被 vite 打包
- src/App.vue Vue 应用的根组件
- src/main.ts Vue 应用的入口 JS/TS 文件, 导入 ./App.vue 根组件并创建 App 对象, 并挂载到 index.html, 也可以导入全局样式, 全局 api
- index.html Vue 应用的入口 HTML 文件,
<div id="app"></div>是 App 对象的挂载点
SFC, Single File Component 单文件组件
对于 .vue 文件
- script 标签: setup 只能有一个, 非 setup 可以有多个
- template 标签: 只能有一个
- style 标签: 可以有多个
setup 函数
Section titled “setup 函数”<script lang="ts">import { ref } from "vue";
export default { setup() { const cnt = ref(1); const addCnt = () => { cnt.value++; }; // 一定要 return! return { cnt, addCnt, }; },};</script>- 单向绑定: 模型 (数据) 改变 —> 视图 (页面) 改变. 例:
{{ }}插值; v-bind 指令 - 双向绑定: 模型 (数据) 改变 <-> 视图 (页面) 改变. 例: v-model 指令, 常用于输入框
setup 语法糖
Section titled “setup 语法糖”<script lang="ts" setup>import { ref } from "vue";const cnt = ref(1);const addCnt = () => { cnt.value++;};</script>vue 指令
Section titled “vue 指令”- v-text 渲染文本字符串, 会忽略子节点
- v-html 渲染 HTML 字符串, 会忽略子节点, 不支持渲染 Vue 组件
- v-if, v-else-if, v-else 节点的条件渲染, 不渲染则将节点卸载, 表现为注释节点
<!-- v-if -->, 操作 DOM - v-show 节点的显示/隐藏: 改变内联 CSS 样式
display: none, 操作 CSS - v-on 为元素绑定事件
- v-bind 为元素绑定属性, 模型到视图的单向绑定; v-bind 也可以绑定 style
- v-model 模型, 视图的双向绑定, 本质是 v-bind 和 v-on 的语法糖
- v-for 遍历元素
- v-once 性能优化, 只渲染一次
- v-memo 性能优化, 缓存
<script lang="ts" setup>const eventName = "click";const handleClick = (ev) => console.log(ev);</script>
<template> <!-- 动态事件名 --> <button @[eventName]="handleClick">log</button></template>- v-on: 可以简写为 @
- v-bind: 可以简写为 :
- v-model 本质是 v-bind 和 v-on 的语法糖
<template> <input v-model="text" /> <!-- 等价于 --> <input v-bind:value="text" @input="text = $event.target.value" /> <!-- 等价于 --> <input :value="text" @input="(ev) => (text = ev.target.value)" /></template><script lang="ts" setup>const evType = ref("click");function clickHandler(ev: Event) { console.log("[Child] evType:", evType);}</script>
<template> <!-- 动态事件名 --> <!-- ev: PointerEvent --> <!-- evType: click --> <div @click=" (ev) => { console.log('[Parent] ev:', ev); } " > <button v-on:[evType]="clickHandler">点击</button> <button @[evType]="(ev: Event) => clickHandler(ev)">点击</button> <!-- 阻止事件冒泡 --> <button @[evType].stop="clickHandler">点击</button> </div></template>这里点击 button 子元素时, 事件会冒泡到 div 父元素, 触发 div 父元素的点击事件, 可以使用 .stop 修饰符阻止事件冒泡
事件传播分为 3 个阶段: 捕获阶段, 目标阶段和冒泡阶段
| v-on 指令的修饰符 | 原生 JS (Vanilla JS) |
|---|---|
v-on:[evType].stop | ev.stopPropagation(); .stop 指令: 阻止事件冒泡 |
v-on:[evType].prevent | ev.preventDefault(); .prevent 指令: 阻止事件的默认行为 |
v-on:[evType].capture | elem.addEventListener(evType, listener, true /* useCapture */).capture 指令: 事件在捕获阶段触发, 而不是在默认的冒泡阶段触发 |
v-on:[evType].self | .self 指令: 只触发本元素绑定的事件, 不触发从子元素冒泡的事件 |
v-on:[evType].once | elem.removeEventListener(*args) .once 指令: 事件只触发一次, 触发后移除监听器 |
@scroll.passive | .passive 指令: 对于滚动, 触摸事件, 不调用 ev.preventDefault(), 提高流畅度 |
@keydown.enter | 键修饰符 Key Modifiers: 按 enter 键 |
@click.ctrl | 系统修饰符 System Modifiers: 按 ctrl 键并点击 |
<script lang="ts" setup>const autofill = ref("");function handleEnter(ev: Event) { console.log("[handleEnter] ev: ", ev); console.log("[handleEnter] autofill:", autofill); autofill.value = "Autofill context";}</script>
<!-- v-model: 双向绑定 --><template> <input id="text" type="text" @keydown.enter="handleEnter" v-model="autofill" placeholder="按 enter 键自动填充" /></template>v-memo
Section titled “v-memo”- v-memo 接收一个依赖项数组
- 组件更新时, 如果 v-memo 标记的元素的依赖项都未改变, 则跳过该元素的更新
- 依赖项数组为空时,
v-memo="[]"等价于v-once, 该元素只会渲染一次
<script lang="ts" setup>const cnt = ref(1);const cnt2 = ref(1);const addCnt = () => { cnt.value++;};const addCnt2 = () => { cnt2.value++;};</script>
<template> <!-- addCnt2 时, 不会触发组件更新 --> <div v-memo="[cnt]">cnt: {{ cnt }}; cnt2: {{ cnt2 }}</div> <button @click="() => addCnt()">addCnt</button> <button @click="() => addCnt2()">addCnt2</button></template>虚拟 DOM 和 diff 算法
Section titled “虚拟 DOM 和 diff 算法”vnode: Virtual DOM Node
真实 DOM 的属性过多, 操作真实 DOM 浪费性能, 虚拟 DOM 是一个 JS 对象
diff 算法
Section titled “diff 算法”- 前序对比: 从头到尾对比 vnode 类型和 key, 相同则复用, 不同则转到 2
- 后序对比: 从尾到头对比 vnode 类型和 key, 相同则复用, 不同则转到 3
- 如果旧节点全部 patch, 有多余的新节点, 则新增 (挂载)
- 如果新节点全部 patch, 有多余的旧节点, 则删除 (卸载)
- 特殊情况: 乱序 (基于最长递增子序列 LIS)
- 例: 原序列 2,3,4,0,6,1 的最长递增子序列为 2,3,4
- 将原 vnode 序列的最长递增子序列作为参照序列, 复用, 新增或删除不在参照序列中的节点
- 错误实践: 使用索引 index (拼接其他值) 作为 key
- 正确实践: 使用唯一 id 作为 key
<script lang="ts" setup>const arr = ref<string[]>(["a", "b", "c", "d"]);</script>
<template> <!-- eslint-disable-next-line vue/require-v-for-key --> <span v-for="val of arr"> <!-- 没有 key --> {{ val }} </span> <br />
<span :key="idx" v-for="(val, idx) of arr"> <!-- 有 key --> {{ val }} </span> <br />
<button @click="(console.log($event), arr.splice(2, 0, 'e'))">splice</button></template>家族成员: ref, shallowRef, isRef, triggerRef, customRef
- ref 深层响应式, 底层会调用 triggerRef 强制收集依赖, 触发深层响应式
- shallowRef 浅层响应式, 只响应
.value的改变 - isRef 判断是否为使用 ref, shallowRef 创建的响应式对象
isRef(refObj) === true,isRef(shallowRefObj) === trueisRef(reactiveObj) === false,isRef(shallowReactiveObj) === false
- triggerRef 调用 triggerRef 强制收集依赖, 触发深层响应式,
shallowRef + triggerRef等价于ref
同时使用 ref 和 shallowRef 时, shallowRef 的浅层响应式会失效, 表现为深层响应式 (参考 setUser4)
<script lang="ts" setup>import { ref, shallowRef, triggerRef } from "vue";
const user = ref({ name: "Alice", age: 1 });const user2 = shallowRef({ name: "Bob", age: 2 });
// user.value.age++const setUser = () => user.value.age++;// 无改变const setUser2 = () => user2.value.age++;// user2.value.age++const setUser3 = () => (user2.value = { ...user2.value, age: user2.value.age + 1 });// user.value.age++; user2.value.age++const setUser4 = () => { user.value.age++; user2.value.age++;};// user2.value.age++const setUser5 = () => { user2.value.age++; triggerRef(user2); // 手动依赖收集};</script>
<template> <main> <div>user: {{ JSON.stringify(user) }}</div> <div>user2: {{ JSON.stringify(user2) }}</div>
<button @click="setUser">setUser</button> <button @click="setUser2">setUser2</button> <button @click="setUser3">setUser3</button> <button @click="setUser4">setUser4</button> <button @click="setUser5">setUser5</button> </main></template>customRef
Section titled “customRef”<script lang="ts" setup>import { customRef, type Ref } from "vue";
const primaryValue = "raw";
function debouncedRef<T>(value: T, timeout: number) { let timer: number | null = null; const ret: Ref<T> = customRef<T>( ( track: () => void /** 收集依赖 */, trigger: () => void /** 触发更新 */, ) => { return { get: () => { track(); // 收集依赖 return value; }, set: (newValue: T) => { if (timer) { clearTimeout(timer); } timer = setTimeout(() => { value = newValue; trigger(); // 触发更新 timer = null; }, timeout); }, }; }, ); return ret;}
const str = debouncedRef(primaryValue, 3000);const setPrimaryValue = () => { str.value = "brandNew";};</script>
<template> <main> <div>str: {{ str }}</div> <button @click="setPrimaryValue">setPrimaryValue</button> </main></template>ref 绑定 DOM 元素
Section titled “ref 绑定 DOM 元素”<script lang="ts" setup>import { ref, onMounted, useTemplateRef } from "vue";
const sameName = ref<HTMLDivElement>();// 也可以使用 useTemplateRefconst divElem = useTemplateRef("sameName");onMounted(() => { console.log(sameName.value?.innerText); console.log(divElem.value?.innerText);});</script>
<template> <div ref="sameName">Awesome Vue</div></template>reactive, readonly
Section titled “reactive, readonly”家族成员: reactive, shallowReactive
- reactive 深层响应式, 底层会调用 triggerRef 依赖收集, 触发深层响应式
- shallowReactive 浅层响应式, 只响应
.[keyName]的改变 - readonly 返回一个只读的响应式对象
相同的, 同时使用 reactive 和 shallowReactive 时, shallowReactive 的浅层响应式会失效, 表现为深层响应式
reactive 对比 React 的 useState
Section titled “reactive 对比 React 的 useState”- React 的 useState 可以接收任意的数据类型
- reactive 只能接收引用数据类型
- React 的 useState 返回一个普通对象 (状态) 和 set 函数, 调用 set 函数更新状态时, 必须修改该普通对象 (状态) 的引用的指向
- reactive 返回一个代理对象, 不能修改该代理对象的引用的指向, 否则会失去响应式!
ref 对比 reactive
Section titled “ref 对比 reactive”const refObj = ref(1);const refObj2 = ref({ name: "lark", age: 1 });const reactiveObj = reactive({ name: "lark", age: 1 });- ref 可以接收任意的数据类型; reactive 只能接收引用数据类型
- ref 存取时需要加
.value; reactive 不需要 - ref 更适合简单数据结构; reactive 更适合复杂数据结构
- reactiveObj 是一个 Proxy 对象
- ref 接收基本数据类型时, refObj.value 是一个基本数据类型的值
- ref 接收引用数据类型时, refObj2.value 是一个 Proxy 对象
- 可以直接对 refObj.value 赋值; 不能直接对 reactiveObj 赋值, 否则会失去响应式!
readonly
Section titled “readonly”import { reactive, readonly } from "vue";
const items = reactive<string[]>([]);const readonlyItems = readonly(items);readonlyItems.push("item");console.log(items, readonlyItems); // [] []
items.push("item");console.log(items, readonlyItems); // ["item"] ["item"]toRef, toRefs, toRaw
Section titled “toRef, toRefs, toRaw”- toRef: 将 ref/reactive 创建的响应式对象上的属性值, 转换为响应式对象 (值是绑定的)
- toRefs: 将 ref/reactive 创建的响应式对象上的属性值, 批量解构为响应式对象 (值是绑定的)
- toRaw: 将代理对象 refObj.value, reactiveObj 转换为普通对象
- toRef/toRefs 作用于普通对象时, 视图不会更新 (没有 track, trigger)
// 实现 toRefsconst myToRefs = (obj) => { const ret = {}; for (const k in obj) { ret[k] = toRef(obj, k); } return ret;};// 实现 toRawconst myToRaw = (obj) => obj["__v_raw"];computed 计算属性
Section titled “computed 计算属性”- 计算属性
computed({ getter, setter }) - 只读的计算属性
computed(getter) - 计算属性会缓存计算结果, 只有当依赖项改变时, 才会重新计算 (基于脏值检测)
const firstName = ref("Tiancheng");const lastName = ref("Hang");const fullName = computed<string>({ get() { return firstName.value + "-" + lastName.value; }, // getter set(newVal: string) { [firstName.value, lastName.value] = newVal.split("-"); }, // setter});
const readonlyFullName = computed<string>( () => firstName.value + "-" + lastName.value, // getter);watch 侦听器
Section titled “watch 侦听器”数据源
- 一个 ref/reactive 变量
- 一个 getter 函数
() => refObj.value[.prop]或() => reactiveObj.prop - 1,2 的数组
const refObj = ref( /* (.value) deep = 0 */ { // deep = 1 foo: { // deep = 2 bar: { // deep = 3 type: "ref", }, }, },);
// ref 创建的响应式对象, 默认浅层侦听 deep: false; deep: 0watch( refObj, (newVal, oldVal) => { console.log("[watch] newVal", newVal); console.log("[watch] oldVal", oldVal); }, { deep: 3 },);const reactiveObj = reactive( /* deep = 0 */ { // deep = 1 foo: { // deep = 2 bar: { // deep = 3 type: "reactive", }, }, },);
// reactive 创建的响应式对象, 默认深层侦听 deep: truewatch( reactiveObj, (newVal, oldVal) => { console.log("[watch2] newVal", newVal); console.log("[watch2] oldVal", oldVal); }, { deep: 3 },);const name = ref("lark");
// 返回停止侦听的函数// 调用 watchHandle() 或 watchHandle.stop() 停止侦听const watchHandle = watch( [refObj, reactiveObj, name], (newVal, oldVal, onCleanup) => { console.log("[watch3] newVal:", newVal); console.log("[watch3] oldVal:", oldVal); } /** watchCallback */, { // deep: true, // 默认 false, 深层侦听 immediate: false, // 是否立即执行 watchCallback // 默认 false, 即默认懒执行 watchCallback flush: "pre", // "pre" | "post" | "sync", 默认 pre // pre: 组件挂载, 更新前调用 watchCallback // post: 组件挂载, 更新后调用 watchCallback // sync: 同步调用 watchCallback once: false, // 一次性侦听, watchCallback 只调用一次 } /** options */,);
// 可以传递一个 getter, 侦听响应式对象中指定的属性watch( [() => refObj.value.foo.bar.type, () => reactiveObj.foo.bar.type], (newVal, oldVal) => { console.log("[watch4] newVal:", newVal); console.log("[watch4] oldVal:", oldVal); },);watchEffect
Section titled “watchEffect”不需要指定依赖项, 自动侦听 (自动收集 watchEffectCallback 中的响应式依赖), 默认立即执行 watchEffectCallback
// 返回停止侦听的函数// 调用 watchHandle() 或 watchHandle.stop() 停止侦听const watchHandle = watchEffect( // watchEffectCallback (onCleanup) => { console.log("[watchEffect]", msg.value, msg2.value); onCleanup(() => { console.log("[onCleanup]", msg.value, msg2.value); }); }, { flush: "post", // "pre" | "post" | "sync" // pre: 组件挂载, 更新前调用 watchCallback // post: 组件挂载, 更新后调用 watchCallback // sync: 同步调用 watchCallback onTrigger: (ev) => { console.log(ev); }, // 调试选项 onTrack: (ev) => { console.log(ev); }, // 调试选项 },);总结: 未指定 deep 时, 地址改变则可以侦听到, 地址未改变则侦听不到
组件的生命周期
Section titled “组件的生命周期”setup 语法糖中, 将 beforeCreate, created 合并为 setup
组件的生命周期: setup -> onBeforeMount -> onMounted -> onBeforeUpdate -> onUpdated -> onBeforeUnmount -> onUnmount
- setup 创建阶段
- onBeforeMount 挂载前, 获取不到 DOM
- onMounted 挂载后, 可以获取到 DOM
- onRenderTriggered 触发更新后, 回调函数接收一个事件对象, 可以同时获取到 newValue 和 oldValue, 调试用 hook, 不属于组件生命周期
- onBeforeUpdate 更新前, 获取的是 oldValue
- onRenderTracked 收集依赖后, 回调函数接收一个事件对象, 只能获取到 newValue, 调试用 hook, 不属于组件生命周期
- onUpdated 更新后, 获取的是 newValue
- onBeforeUnmount 卸载前, 可以获取到 DOM
- onUnmounted 卸载后, 获取不到 DOM
父子组件通信
Section titled “父子组件通信”子组件中使用宏函数 defineProps 定义自定义属性
父传子, defineProps 对比 useAttrs
Section titled “父传子, defineProps 对比 useAttrs”父组件
<script lang="ts" setup>import { ref, reactive } from "vue";import ChildDemo from "./ChildDemo.vue";
// 父子组件传参const str_ = "str_parent";const refStr_ = ref("refStr_parent");const reactiveArr_ = reactive([6, 6, 6]);</script>
<template> <div>ParentDemo: {{ str_ }} {{ refStr_ }} {{ reactiveArr_ }}</div> <ChildDemo :str="str_" :refStr="refStr_" :reactiveArr="reactiveArr_" extraAttr="1" extraAttr2="2" /> <!-- str_ 不是响应式的, refStr_, reactiveArr_ 是响应式的 --> <button @click="str_ += '!'">setStr</button> <button @click="refStr_ += '!'">setRefStr</button> <button @click="reactiveArr_.push(6)">setReactiveArr</button></template>子组件
<script lang="ts" setup>import { useAttrs } from "vue";
const props = defineProps(["str", "refStr", "reactiveArr"]);// {str: 'str_parent', refStr: 'refStr_parent', reactiveArr: Proxy(Array)}console.log("[Child] props:", props);
const attrs = useAttrs();// {extraAttr: '1', extraAttr2: '2'}console.log("[Child] attrs:", attrs);</script>
<template> <!-- template 中, 使用 props.propName 或直接使用 propName 都可以 --> <div>ChildDemo: {{ str }} {{ props.refStr }} {{ reactiveArr }}</div></template>import { toRefs, useAttrs } from "vue";
const props = defineProps({ str: { type: String, default: "str_default", }, refStr: { type: String, default: "refStr_default", }, reactiveArr: { type: Array<number>, // Array default: () => [5, 2, 8], // 引用类型必须转换为箭头函数 },});
const { str, refStr, reactiveArr } = toRefs(props);console.log("[ChildDemo] props:", str, refStr, reactiveArr);const props = defineProps<{ str?: string; refStr?: string; reactiveArr?: number[];}>();
console.log("[ChildDemo] props:", props.str, props.refStr, props.reactiveArr);const props = withDefaults( defineProps<{ str?: string; refStr?: string; reactiveArr?: number[]; }>(), { str: "str_default", refStr: "refStr_default", reactiveArr: () => [5, 2, 8], // 引用类型必须转换为箭头函数 },);
console.log("[ChildDemo] props:", props.str, props.refStr, props.reactiveArr);Grandparent 传 Child
Section titled “Grandparent 传 Child”<script lang="ts" setup>import { reactive, ref } from "vue";import ParentDemo from "./ParentDemo.vue";
const a = ref(1);const b = reactive({ v: 2 });const addA = (da: number) => (a.value += da);</script>
<template> <div> <!-- v-bind="{ p1: "v1", p2: "v2" }" 等价于 :p1="v1" :p2="v2" --> <ParentDemo :a="a" :b="b" :addA="addA" :="{ p1: 'v1', p2: 'v2' }" /> </div></template><script lang="ts" setup>import { useAttrs } from "vue";import ChildDemo from "./ChildDemo.vue";
const props = defineProps(["a", "b", "addA"]);// {a: 1, b: Proxy(Object), addA: ƒ}console.log("[ParentDemo] props:", props);
const attrs = useAttrs();// {p1: 'v1', p2: 'v2'}console.log("[ParentDemo] attrs:", attrs);</script>
<template> <div> <div>[ParentDemo] a={{ a }} b={{ b }} attrs={{ attrs }}</div> <ChildDemo :a="a" :b="b" :addA="addA" :="attrs" /> </div></template><script lang="ts" setup>import { useAttrs } from "vue";
const props = defineProps(["p1", "p2"]);// {p1: 'v1', p2: 'v2'}console.log("[ChildDemo] props:", props);
const attrs = useAttrs();// {a: 1, b: Proxy(Object)}console.log("[ChildDemo] attrs:", attrs);</script>
<template> <div> <p>[ChildDemo] p1={{ p1 }} p2={{ p2 }} attrs={{ attrs }}</p> <button @click="(attrs.addA as Function)(1)">Add grandparent's a</button> </div></template>- 子组件使用 defineEmits 定义自定义事件
- 子组件派发自定义事件, emit 发射参数给父组件
- 父组件为子组件的自定义事件绑定回调函数, 监听子组件派发的自定义事件; 自定义事件派发时, 父组件接收子组件发射的参数, 作为回调函数的参数
子组件
<script lang="ts" setup>// 子组件使用 defineEmits 定义自定义事件// 自定义事件名 evName, evName2const emit = defineEmits(["evName", "evName2"]);
const emitToParent = (ev: Event) => { // 子组件派发自定义事件, emit 发射参数给父组件 emit("evName", ev);};const emitToParent2 = () => { emit("evName2", "foo", "bar");};</script>
<template> <button @click="(ev) => emitToParent(ev)">子传父</button> <button @click="emitToParent2">子传父2</button></template>// 子组件使用 defineEmits 定义自定义事件// 自定义事件名 evName, evName2const emit = defineEmits<{ (e: "evName", arg: Event): void; (e: "evName2", arg: string, arg2: string): void;}>();
const emitToParent = (ev: Event) => { // 子组件派发自定义事件, emit 发射参数给父组件 emit("evName", ev);};const emitToParent2 = () => { emit("evName2", "foo", "bar");};// 子组件使用 defineEmits 定义自定义事件// 自定义事件名 evName, evName2const emit = defineEmits<{ evName: [arg: Event], // 具名元组 evName2: [arg: string, arg2: string] // 具名元组}>()
const emitToParent = (ev: Event) => {// 子组件派发自定义事件, emit 发射参数给父组件emit('evName', ev)}const emitToParent2 = () => {emit('evName2', 'foo', 'bar')}父组件
<script lang="ts" setup>import ChildDemo from "./ChildDemo.vue";// 子传父// 自定义事件派发时, 父组件接收子组件发射的数据, 作为回调函数的参数const receiveFromChild = (...args: unknown[]) => console.log(args);</script>
<template> <!-- 父组件为子组件的自定义事件绑定回调函数, 监听子组件派发的自定义事件 --> <ChildDemo @evName="(...args: unknown[]) => receiveFromChild(...args)" @evName2="receiveFromChild" /></template>子组件暴露接口
Section titled “子组件暴露接口”子组件使用 defineExpose 暴露接口, 包括属性和方法
<script lang="ts" setup>defineExpose({ name: "lark", getAge() { return 23; },});</script>
<template>ChildDemo</template><script lang="ts" setup>import { onMounted, ref } from "vue";import ChildDemo from "./ChildDemo.vue";
const sameName = ref<InstanceType<typeof ChildDemo>>();onMounted(() => { console.log( "[ParentDemo] Child expose:", sameName.value?.name, sameName.value?.getAge(), );});</script>
<template> <ChildDemo ref="sameName" /></template>兄弟组件通信
Section titled “兄弟组件通信”方式 1: 通过父组件转发 (forward)
Section titled “方式 1: 通过父组件转发 (forward)”BoyDemo.vue -> ParentDemo.vue (forward) -> GirlDemo.vue
<!-- Boy 组件使用 defineEmits 定义自定义事件 --><script lang="ts" setup>// const emit = defineEmits(['customEvent'])const emit = defineEmits<{ customEvent: [flag: boolean, timestamp: string]; // 具名元组}>();
let flag = false;const emitArgs = () => { flag = !flag; const timestamp = new Date().toLocaleTimeString(); emit("customEvent", flag, timestamp);};</script>
<template> <!-- 点击按钮以触发自定义事件, 向父组件发射参数 --> <button @click="emitArgs">emitArgs</button></template><!-- 父组件为子组件的自定义事件绑定回调函数自定义事件发生时, 父组件接收子组件发射的参数, 作为回调函数的参数 --><script lang="ts" setup>import { ref } from "vue";import BoyDemo from "./BoyDemo.vue";import GirlDemo from "./GirlDemo.vue";
const flag = ref<boolean>(false);const timestamp = ref<string>("");
const receiveArgs = (flag_: boolean, timestamp_: string) => { flag.value = flag_; timestamp.value = timestamp_;};</script>
<template> <div> <BoyDemo @custom-event=" (flag: boolean, timestamp: string) => receiveArgs(flag, timestamp) " /> <!-- 转发: 将父组件接收的 BoyDemo 子组件发射的参数, 传递给 GirlDemo 子组件 --> <GirlDemo :flag="flag" :timestamp="timestamp" /> </div></template><script lang="ts" setup>defineProps<{ flag: boolean; timestamp: string;}>();</script>
<template> <div>[GirlDemo] flag: {{ flag }}</div> <div>[GirlDemo] timestamp: {{ timestamp }}</div></template>方式 2: 事件总线 (发布/订阅)
Section titled “方式 2: 事件总线 (发布/订阅)”BoyDemo 发布, GirlDemo 订阅, 无需父组件参与
// type TCallback = (...args: any[]) => void | Promise<void>interface ICallback { (...args: any[]): void | Promise<void>;}
class Bus { evName2cbs: Map<string, Set<ICallback>> = new Map();
// 发布 pub(evName: string, ...args: unknown[]) { const cbs = this.evName2cbs.get(evName); if (!cbs) { return; } for (const cb of cbs) { cb.apply(this, args); } }
// 订阅 sub(evName: string, cb: ICallback) { const cbs = this.evName2cbs.get(evName); if (!cbs) { this.evName2cbs.set(evName, new Set([cb])); return; } cbs.add(cb); }
// 取消订阅 off(evName: string, cb: ICallback) { const cbs = this.evName2cbs.get(evName); if (!cbs) { return; } cbs.delete(cb); if (cbs.size === 0) { this.evName2cbs.delete(evName); } }
// 订阅一次 once(evName: string, cb: ICallback) { const onceCb = (...args: Parameters<typeof cb>) => { cb.apply(this, args); this.off(evName, onceCb); }; this.sub(evName, onceCb); }}
export default new Bus();<script lang="ts" setup>import bus from "./bus";
let flag = false;const emitArgs = () => { flag = !flag; // 发布 bus.pub("customEvent", flag, new Date().toLocaleTimeString()); // 发布};</script>
<template> <button @click="emitArgs">emitArgs</button></template><script lang="ts" setup>import { ref } from "vue";import bus from "./bus";
const flag = ref(false);const timestamp = ref("");
// 订阅bus.sub("customEvent", (flag_: boolean, timestamp_: string) => { flag.value = flag_; timestamp.value = timestamp_;});</script>
<template> <div>[GirlDemo] flag: {{ flag }}</div> <div>[GirlDemo] timestamp: {{ timestamp }}</div></template>mitt 发布/订阅库
Section titled “mitt 发布/订阅库”<script setup lang="ts">import mitt from "mitt";
const emitter = mitt();
const handlerA = (args: unknown) => console.log("[handlerA] args:", args);const handlerB = (args: unknown) => console.log("[handlerB] args:", args);emitter.on("eventA", handlerA);emitter.on("eventB", handlerB);emitter.on("*", (evName, args) => console.log("[*]:", evName, args));</script>
<template> <button @click="emitter.emit('eventA', { a: 1 })">emitA</button> <button @click="emitter.emit('eventB', { b: 2 })">emitB</button> <button @click="emitter.off('eventA', handlerA)">offA</button> <button @click="emitter.off('eventB', handlerB)">offB</button> <button @click="emitter.all.clear()">clear</button></template>依赖注入 provide/inject
Section titled “依赖注入 provide/inject”类似的技术: IoC/DI
控制反转 IoC, Inversion of Control, 不手动 new 对象, 或导入对象, 而是从容器 (Map) 中取对象依赖注入 DI, Dependency Injection: 不导出对象, 而是将对象放到容器 (Map) 中
provide/inject: 祖先 provide 提供, 并 inject 注入到后代, 实现祖孙通信
<script lang="ts" setup>import { provide, ref } from "vue";import ParentDemo from "./ParentDemo.vue";
const colorVal = ref("lightpink");// 祖先 provide 提供provide("colorKey" /** key */, colorVal /** value */);// 可以提供一个 readonly 的 colorVal, 防止后代组件修改// provide('colorKey', readonly(colorVal))</script>
<template> <div>[Grandparent] colorVal: {{ colorVal }}</div> <button @click="colorVal = 'lightpink'">lightpink</button> <ParentDemo /></template><script lang="ts" setup>import { inject, ref, type Ref } from "vue";import ChildDemo from "./ChildDemo.vue";// 并 inject 注入到后代const injectedColor = inject<Ref<string>>( "colorKey", ref("unknown-color") /** defaultVal */,);</script>
<template> <div>[Parent] injectedColor {{ injectedColor }}</div> <button @click="injectedColor = 'lightgreen'">lightgreen</button> <ChildDemo /></template><script lang="ts" setup>import { inject, type Ref, ref } from "vue";// 并 inject 注入到后代const injectedColor = inject<Ref<string>>( "colorKey", ref("unknown-color") /** defaultVal */,);</script>
<template> <div>[Child] injectedColor {{ injectedColor }}</div> <button @click="injectedColor = 'lightblue'">lightblue</button></template>局部组件, 全局组件, 递归组件
Section titled “局部组件, 全局组件, 递归组件”局部组件, 全局组件
Section titled “局部组件, 全局组件”- 默认是局部组件
- 可以在 main.ts 中注册全局组件
import CardComponent from "@/components/global/CardComponent.vue";const app = createApp(App);app.component("GlobalCard", CardComponent); // 注册 <GlobalCard /> 全局组件// .vue 文件可以直接使用 <GlobalCard /> 全局组件, 无需导入批量注册全局组件
import * as GlobalComponents from "./components/global";
const app = createApp(App);for (const [key, component] of Object.entries(GlobalComponents)) { app.component(key, component);}父组件 ParentDemo.vue
<script lang="ts" setup>import { reactive } from "vue";import RecursiveChild from "./RecursiveChild.vue";
export interface ITreeNode { name: string; checked: boolean; children?: ITreeNode[];}
const data = reactive<ITreeNode[]>([ { name: "1", checked: false }, { name: "2", checked: true, children: [{ name: "2-1", checked: false }] }, { name: "3", checked: true, children: [ { name: "3-1", checked: false, children: [{ name: "3-1-1", checked: true }], }, ], },]);</script>
<template> <RecursiveChild :data="data" /></template>递归子组件 RecursiveChild.vue
使用递归组件时, 需要阻止事件冒泡 (使用 .stop 修饰符)
<!-- <script lang="ts">// 可以自定义组件名// 不能同时使用 defineOptions 宏函数和 export default 默认导出export default { name: "RecursiveChild" };</script> -->
<script lang="ts" setup>import type { ITreeNode } from "./ParentDemo.vue";
defineProps<{ data?: ITreeNode[] }>();// 可以自定义组件名// 不能同时使用 defineOptions 宏函数和 export default 默认导出defineOptions({ name: "RecursiveChild" });
const check = (item: ITreeNode) => console.log(item);</script>
<template> <!-- .stop 修饰符: 阻止事件冒泡 --> <div @click.stop="check(item)" v-for="(item, idx) of data" :key="idx"> <div> <input type="checkbox" v-model="item.checked" /> <span>{{ item.name }}</span> </div> <!-- 递归组件, 默认组件名等于文件名 --> <RecursiveChild v-if="item.children" :data="item.children" /> </div></template>动态组件 <component />
Section titled “动态组件 <component />”多个组件使用同一个 <component /> 挂载点, 并可以动态切换
<component :is="componentShallowRef | componentName" />
不要创建组件的 ref 对象, 避免不必要的性能开销, 可以使用 shallowRef 代替 ref, 也可以使用 markRaw 跳过代理
<script lang="ts" setup>import { markRaw, reactive, shallowRef } from "vue";import DynamicA from "./DynamicA.vue";import DynamicB from "./DynamicB.vue";import DynamicC from "./DynamicC.vue";type DynamicComp = typeof DynamicA | typeof DynamicB | typeof DynamicC;
const activeComp = shallowRef<DynamicComp>(DynamicA);const setComp = (comp: DynamicComp) => (activeComp.value = comp);const options = reactive([ { name: "compA", handler: () => setComp(DynamicA) }, { name: "compB", handler: () => setComp(DynamicB) }, // 不要创建组件的 ref 对象, 避免不必要的性能开销 // 可以使用 shallowRef 代替 ref // 也可以使用 markRaw 跳过代理 { name: "compC ", handler: () => setComp(markRaw(DynamicC)) },]);</script>
<template> <div v-for="{ name, handler } of options" :key="name"> <div @click="handler">{{ name }}</div> </div> <!-- is 可以是组件的 shallowRef, 也可以是注册的组件名 componentName--> <component :is="activeComp" /></template><script lang="ts">import DynamicA from "./DynamicA.vue";import DynamicB from "./DynamicB.vue";import DynamicC from "./DynamicC.vue";
export default { // 注册子组件 components: { compA: DynamicA, // 注册的组件名 compA compB: DynamicB, // 注册的组件名 compB compC: DynamicC, // 注册的组件名 compC },};</script>
<script lang="ts" setup>import { reactive, ref } from "vue";
const activeComp = ref<string>("compA");const setComp = (comp: string) => (activeComp.value = comp);const options = reactive([ { name: "compA_", handler: () => setComp("compA") }, { name: "compB_", handler: () => setComp("compB") }, { name: "compC_", handler: () => setComp("compC") },]);</script>
<template> <div v-for="{ name, handler } of options" :key="name"> <div @click="handler">{{ name }}</div> </div> <!-- is 可以是组件的 shallowRef, 也可以是注册的组件名 componentName--> <component :is="activeComp" /></template><script lang="ts" setup>import { reactive, ref } from "vue";import DynamicA from "./DynamicA.vue";import DynamicB from "./DynamicB.vue";import DynamicC from "./DynamicC.vue";
defineOptions({ // 注册子组件 components: { compA: DynamicA, // 注册的组件名 compA compB: DynamicB, // 注册的组件名 compB compC: DynamicC, // 注册的组件名 compC },});
const activeComp = ref<string>("compA");const setComp = (comp: string) => (activeComp.value = comp);const options = reactive([ { name: "compA_", handler: () => setComp("compA") }, { name: "compB_", handler: () => setComp("compB") }, { name: "compC_", handler: () => setComp("compC") },]);</script>
<template> <div v-for="{ name, handler } of options" :key="name"> <div @click="handler">{{ name }}</div> </div> <!-- is 可以是组件的 shallowRef, 也可以是注册的组件名 componentName--> <component :is="activeComp" /></template>插槽 <slot />
Section titled “插槽 <slot />”插槽: 子组件提供给父组件的占位符, 可以插入父组件的 template
- 匿名插槽 name=“default”
- 具名插槽
- 作用域插槽
- 动态插槽
<script lang="ts" setup>import { reactive } from "vue";
const users = reactive([ { name: "foo", age: 1 }, { name: "bar", age: 2 }, { name: "baz", age: 3 },]);</script>
<template> <div> <header> <!-- 匿名插槽 name="default" --> <slot>placeholder: 匿名插槽</slot> </header>
<main> <div v-for="(item, idx) of users" :key="idx"> <!-- 作用域插槽 --> <slot name="scoped" :item="item" :idx="idx" >placeholder: 作用域插槽</slot > </div> </main>
<footer> <!-- 具名插槽 --> <slot name="named">placeholder: 具名插槽</slot> </footer> </div></template><script lang="ts" setup>import ChildDemo from "./ChildDemo.vue";</script>
<template> <div> <!-- 子组件 --> <ChildDemo> <!-- <div>默认插入到子组件的匿名插槽</div> --> <template v-slot:default> <div>插入到子组件的匿名插槽 default</div> </template>
<template v-slot:scoped="{ item, idx }"> <div>插入到子组件的作用域插槽 scoped</div> <div>{{ `idx: ${idx}, name: ${item.name}, age: ${item.age}` }}</div> </template>
<template #named> <div>插入到子组件的具名插槽 named, v-slot: 可以简写为 #</div> </template> </ChildDemo> </div></template><script lang="ts" setup>import { ref } from "vue";import ChildDemo from "./ChildDemo.vue";
const slotName = ref("default");
</script>
<template> <div> <ChildDemo> <!-- 动态插槽, 等价于 #[slotName] --> <template v-slot:[slotName]="{ item, idx }"> <div>动态插槽</div> <div v-if="item"> {{ `idx: ${idx}, name: ${item.name}, age: ${item.age}` }} </div> </template> </ChildDemo> <button @click="slotName = 'default'">default</button> <button @click="slotName = 'scoped'">scoped</button> <button @click="slotName = 'named'">named</button> </div></template>传送模板 <Teleport />
Section titled “传送模板 <Teleport />”<Teleport /> 将部分 template 传送到指定 DOM 节点上, 成为该 DOM 节点的直接子元素
<script lang="ts" setup>import { ref } from "vue";
const popupVisible = ref(false);</script>
<template> <button @click="popupVisible = true">显示弹窗</button>
<!-- .popup 是 #app 的直接子元素 --> <Teleport to="#app" :disabled="false"> <!-- disable 是否禁用 <Teleport /> --> <div class="h-20 w-20 bg-lime-200" v-show="popupVisible"> 我是 #app 的直接子元素 <button @click="popupVisible = false">隐藏弹窗</button> </div> <div>我也是 #app 的直接子元素</div> </Teleport></template>异步组件 <Suspense />
Section titled “异步组件 <Suspense />”- setup 语法糖中使用顶层 await, 会被编译为
async setup() - 父组件使用
defineAsyncComponent(() => import(...))导入异步组件 <Suspense />组件有两个插槽: default 和 fallback, 两个插槽都只允许一个直接子节点
xhr.readyState
- xhr.readyState === 0 [unsent] 未调用 open 方法
- xhr.readyState === 1 [opened] 已调用 open 方法, 未调用 send 方法
- xhr.readyState === 2 [headers_received] 已调用 send 方法, 已收到响应头
- xhr.readyState === 3 [loading] 正在接收响应体
- xhr.readyState === 4 [done] 请求结束, 数据传输成功或失败
{ "data": { "name": "lark", "age": 23, "url": "https://hangtiancheng.github.io/homepage/", "desc": "homepage" }}// 原生 AJAXexport const myAxios = { get<T>(url: string): Promise<T> { return new Promise((resolve) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.onreadystatechange = () => { // xhr.readyState === 4 [done] 请求结束, 数据传输成功或失败 if (xhr.readyState === 4 && xhr.status === 200) { setTimeout(() => { resolve(JSON.parse(xhr.responseText)); }, 3000); } }; xhr.send(null); }); },};<script lang="ts" setup>import { myAxios } from "@/utils/axios.ts";// setup 语法糖中使用顶层 await, 会被编译为 async setup()const { data } = await myAxios.get<{ data: unknown }>("/data.json");</script>
<template> <div>ChildAsync</div> <div>data: {{ JSON.stringify(data) }}</div></template><template> <div>ChildSkeleton</div> <div>请等待...</div></template><script lang="ts" setup>import { defineAsyncComponent } from "vue";import ChildSkeleton from "./ChildSkeleton.vue";// 父组件使用 defineAsyncComponent(() => import(...)) 导入异步组件const ChildAsync = defineAsyncComponent( () => import("@/components/ChildAsync.vue"),);</script>
<template> <Suspense> <template #default> <ChildAsync /> </template> <template v-slot:fallback> <ChildSkeleton /> </template> </Suspense></template>- setup 语法糖中使用顶层 await, 父组件使用
defineAsyncComponent(() => import(...))导入的异步组件, vite 会 chunk 分包, 懒加载 import(...)异步导入的路由组件, vite 会 chunk 分包, 懒加载
缓存组件 <KeepAlive />
Section titled “缓存组件 <KeepAlive />”- 默认缓存
<KeepAlive />内部的所有组件 - include 包含属性: 缓存指定 name 的组件, 支持
string(可以以逗号分隔),RegExp或(string | RegExp)[] - exclude 属性: 不缓存指定 name 的组件
- max 属性: 最大缓存组件数, 如果实际组件数 > max, 则使用 LRU 算法计算具体缓存哪些组件
<script lang="ts" setup>import { ref } from "vue";import BoyDemo from "./BoyDemo.vue";import GirlDemo from "./GirlDemo.vue";
const flag = ref<boolean>(true);</script>
<template> <KeepAlive> <BoyDemo v-if="flag" /> <GirlDemo v-else /> </KeepAlive> <button @click="flag = !flag">switch</button></template><script lang="ts" setup>import { ref } from "vue";
const name = ref("");const age = ref(0);</script>
<template> <div>Boy</div> <input v-model="name" type="text" /> <input v-model="age" type="number" /></template><script lang="ts" setup>import { ref } from "vue";
const name = ref("");const age = ref(0);</script>
<template> <div>Girl</div> <input v-model="name" type="text" /> <input v-model="age" type="number" /></template>缓存组件的生命周期
Section titled “缓存组件的生命周期”使用 <KeepAlive /> 缓存组件时, 会增加两个生命周期 onActivated 和 onDeactivated
// 这两个生命周期钩子不仅适用于 <KeepAlive /> 缓存的根组件, 也适用于缓存树的后代组件onActivated(() => { // 调用时机为组件挂载时, 和每次读缓存后插入到 DOM 中});
onDeactivated(() => { // 调用时机为组件卸载时, 和每次从 DOM 中移除后写缓存});<Transition /> 过渡/动画组件
Section titled “<Transition /> 过渡/动画组件”<Transition />只允许一个直接子元素; 同时,<Transition />包裹组件时, 组件必须有唯一的根元素, 否则无法应用过渡动画<Transition />会在一个元素或组件插入/移除 DOM (v-if 挂载/卸载), 显示/隐藏 (v-show) 时应用过渡动画<TransitionGroup />允许多个直接子元素, 会在一个 v-for 列表中的元素或组件插入, 删除, 移动时应用过渡动画
[enter | leave]-[from | active | to]
Section titled “[enter | leave]-[from | active | to]”对比 CSS 过渡 transition 和动画 animation
| 过渡 transition | 动画 animation | |
|---|---|---|
| 触发 | 需要事件触发, 例如 :hover | 可以自动触发, 例如页面加载后自动播放 |
| 状态 | 只有起始状态和结束状态 | 可以使用 @keyframes 定义多个关键帧 |
| 自动循环播放 | 不支持 | 支持 |
前置要求: 安装 tailwindcss 和 animate.css
<script lang="ts" setup>import { ref } from "vue";
const flag = ref<boolean>(true);</script>
<template> <button @click="flag = !flag">mount/unMount</button> <!-- 默认 name="v" --> <Transition name="my-prefix"> <div v-if="flag" className="w-50 h-50 bg-lime-200">TransitionDemo</div> </Transition></template>
<style lang="css" scoped>@reference "tailwindcss";
/** 默认 .v-enter-from, .v-leave-to */.my-prefix-enter-from,.my-prefix-leave-to { @apply h-0 w-0;}
/** 默认 v-enter-active, .v-leave-active */.my-prefix-enter-active,.my-prefix-leave-active { @apply transition-all duration-1500;}
/** 默认 v-enter-to, .v-leave-from */.my-prefix-enter-to,.my-prefix-leave-from { @apply h-50 w-50 rotate-360;}</style><script lang="ts" setup>import { ref } from "vue";
const flag = ref<boolean>(true);</script>
<template> <button @click="flag = !flag">mount/unMount</button> <!-- 除了 .[my-prefix]-[enter | leave]-[from | active | to] 约定的类名 --> <!-- 也可以自定义类名 [enter | leave]-[from | active | to]-class="your_custom_className" -->
<!-- :duration="1500" 表示持续时间 1500ms --> <!-- 或 :duration="{ enter: 1500, leave: 1500 }" --> <Transition :duration="{ enter: 1500, leave: 1500 }" leaveActiveClass="animate__animated animate__fadeOut" enterActiveClass="animate__animated animate__fadeIn" > <div v-if="flag" className="w-50 h-50 bg-lime-200">TransitionDemo</div> </Transition></template><Transition /> 的钩子函数
Section titled “<Transition /> 的钩子函数”| 事件名 | 对应的 CSS 类名 |
|---|---|
| beforeEnter | v-enter-from |
| enter | v-enter-active |
| afterEnter | v-enter-to |
| enterCancelled | |
| beforeLeave | v-leave-from |
| leave | v-leave-active |
| afterLeave | v-leave-to |
| leaveCancelled |
示例
<script lang="ts" setup>import { ref } from "vue";
const flag = ref(true);
const handleEnterActive = (el: Element, done: () => void) => { console.log("onEnterActive"); setTimeout(() => done() /** 过渡结束 */, 3000);};
const handleLeaveActive = (el: Element, done: () => void) => { console.log("onLeaveActive"); setTimeout(() => done() /** 过渡结束 */, 3000);};</script>
<template> <div> <button type="button" @click="flag = !flag">switch</button> <Transition class="animate__animated" enterActiveClass="animate__fadeIn" leaveActiveClass="animate__fadeOut" :duration="1000" @beforeEnter="(el: Element) => console.log('onBeforeEnter')" @enter="handleEnterActive" @afterEnter="(el: Element) => console.log('onAfterEnter')" @enterCancelled="(el: Element) => console.log('onEnterCancelled')" @beforeLeave="(el: Element) => console.log('onBeforeLeave')" @leave="handleLeaveActive" @afterLeave="(el: Element) => console.log('onAfterLeave')" @leaveCancelled="(el: Element) => console.log('onLeaveCancelled')" > <div class="box" v-if="flag">Transition by animate.css</div> </Transition>
<Transition name="my-prefix"> <!-- className prefix --> <div class="box" v-show="flag" style="background: lightpink"> Transition by custom CSS </div> </Transition> </div></template>
<style lang="scss" scoped>@mixin wh0 { width: 0; height: 0;}
@mixin wh50 { width: 200px; height: 200px;}
.box { @include wh50; background: skyblue;}
.my-prefix-enter-from { @include wh0; transform: rotate(360deg);}
.my-prefix-enter-active { transition: all 3s ease;}
// .my-prefix-enter-to {}// .my-prefix-leave-from {}
.my-prefix-leave-active { transition: all 3s ease;}
.my-prefix-leave-to { @include wh0; transform: rotate(360deg);}</style><Transition /> + GSAP
Section titled “<Transition /> + GSAP”<!-- pnpm add gsap --><script lang="ts" setup>import gsap from "gsap";import { ref } from "vue";
const isAlive = ref(true);const handleBeforeEnter = (el: Element) => gsap.set(el, { width: 0, height: 0 });
const handleEnter = (el: Element, done: () => void) => gsap.to(el, { width: 200, height: 200, onComplete: done });
const handleLeave = (el: Element, done: () => void) => gsap.to(el, { width: 0, height: 0, onComplete: done });</script>
<template> <button type="button" @click="isAlive = !isAlive">switch</button> <Transition @beforeEnter="handleBeforeEnter" @enter="handleEnter" @leave="handleLeave" > <div v-if="isAlive" class="h-50 w-50 bg-lime-200">Transition by GASP</div> </Transition></template>appear-[from | active | to]-class
Section titled “appear-[from | active | to]-class”appear-[from | active | to]-class 只在首次渲染时应用 1 次过渡动画
<script lang="ts" setup>import { ref } from "vue";
const flag = ref<boolean>(true);</script>
<template> <button @click="flag = !flag">mount/unMount</button> <Transition appear appearFromClass="my-appear-from" appearActiveClass="my-appear-active" appearToClass="my-appear-to" > <!-- 只在首次渲染时应用 1 次过渡动画 --> <div v-if="flag" className="w-50 h-50 bg-lime-200">TransitionDemo</div> </Transition></template>
<style lang="css" scoped>@reference "tailwindcss";
.my-appear-from { @apply h-0 w-0;}
.my-appear-active { @apply transition-all duration-1500;}
.my-appear-to { @apply h-50 w-50;}</style><TransitionGroup />
Section titled “<TransitionGroup />”<Transition />只允许一个直接子元素; 同时,<Transition />包裹组件时, 组件必须有唯一的根元素, 否则无法应用过渡动画<Transition />会在一个元素或组件插入/移除 DOM (v-if 挂载/卸载), 显示/隐藏 (v-show) 时应用过渡动画<TransitionGroup />允许多个直接子元素, 会在一个 v-for 列表中的元素或组件插入, 删除, 移动时应用过渡动画
<TransitionGroup /> 列表的插入, 删除过渡
Section titled “<TransitionGroup /> 列表的插入, 删除过渡”<script lang="ts" setup>import { reactive } from "vue";import "animate.css";
const list = reactive<number[]>([0, 1, 2]);</script>
<template> <button @click="list.push(list.length)">push</button> <button @click="list.pop()">pop</button> <!-- tag="htmlTagName" tag 属性为多个列表项包裹一层 htmlTagName 元素 --> <div class="wrapper"> <TransitionGroup tag="main" class="flex flex-wrap gap-1 border-1" enter-active-class="animate__animated animate__bounceIn" leave-active-class="animate__animated animate__bounceOut" > <div class="item" v-for="(item, idx) of list" :key="idx">{{ item }}</div> </TransitionGroup> </div></template><TransitionGroup /> 列表的移动过渡
Section titled “<TransitionGroup /> 列表的移动过渡”<!-- pnpm i lodash && pnpm i @types/lodash -D --><script lang="ts" setup>import { ref } from "vue";import { shuffle } from "lodash";
const arr = ref( Array.from({ length: 81 }, (_, idx) => ({ key: idx, val: (idx % 9) + 1 })),);
const shuffleList = () => (arr.value = shuffle(arr.value));</script>
<template> <div> <button @click="shuffleList">shuffleList</button> <!-- move-class: 平移的过渡动画 --> <TransitionGroup moveClass="mv" class="flex w-[378px] flex-wrap" tag="div"> <!-- v-for 绑定 key 时, 不能使用数组下标, 否则无法应用过渡动画 --> <div class="flex h-10 w-10 items-center justify-center border-1 border-slate-300" v-for="item of arr" :key="item.key" > {{ item.val }} </div> </TransitionGroup> </div></template>
<style lang="css" scoped>.mv { transition: all 1s;}</style>示例
<script setup lang="ts">import gsap from "gsap";
import { reactive, watch } from "vue";const num = reactive({ targetVal: 0, renderVal: 0,});
watch( () => num.targetVal, (newVal, oldVal) => { console.log(newVal, "<-", oldVal); gsap.to(num, { duration: 1, // 1s renderVal: newVal, }); },);</script>
<template> <input v-model="num.targetVal" :step="20" type="number" /> <div>{{ num.renderVal.toFixed(0) }}</div></template>集成 JSX
Section titled “集成 JSX”import { defineComponent, type Component, type RenderFunction, type VNode,} from "vue";
interface IProps { element: string | VNode;}
const MyComponent: Component<IProps> = defineComponent<IProps>( (props: IProps /** , ctx */) => { const { element } = props; const vNode: VNode = ( <div> {import.meta.env.DEV ? "My Component" : ""} {element} </div> ); const renderFunc: RenderFunction = () => vNode; return renderFunc; }, { props: ["element"], },);
export default MyComponent;编写 vite 插件解析 JSX
Section titled “编写 vite 插件解析 JSX”安装依赖
pnpm i @vue/babel-plugin-jsx -D && \pnpm i @babel/core -D && \pnpm i @babel/plugin-transform-typescript -D && \pnpm i @babel/plugin-syntax-import-meta -D && \pnpm i @types/babel__core -Dimport type { Plugin } from "vite";import babel from "@babel/core";import babelPluginJsx from "@vue/babel-plugin-jsx";
function vitePluginVueTsx(): Plugin { return { name: "vite-plugin-vue-tsx", config(/** config */) { return { esbuild: { include: /\.ts$/, }, }; }, async transform(code, id) { if (/.tsx$/.test(id)) { const ts = await import("@babel/plugin-transform-typescript").then( (res) => res.default, ); const res = await babel.transformAsync(code, { ast: true, // ast 抽象语法树 babelrc: false, // 没有 .babelrc 文件, 所以是 false configFile: false, // 没有 babel.config.json 文件, 所以是 false plugins: [ babelPluginJsx, [ts, { isTSX: true, allowExtensions: true }], ], }); return res?.code; } return code; }, };}v-model 双向绑定
Section titled “v-model 双向绑定”v-model 本质是语法糖
Section titled “v-model 本质是语法糖”- 父组件使用
v-bind传递 props 给子组件, 预定义的属性名modelValue - 子组件派发预定义事件, 父组件使用
v-on为预定义事件绑定回调函数, 监听子组件派发的预定义事件, 预定义事件名update:modelValue - 父组件修改值时, 父组件使用
v-bind传递新的modelValue值给子组件 - 子组件修改值时, 子组件派发
update:modelValue预定义事件, emit 发射新的modelValue值给父组件 - 支持多个 v-model: v-model 预定义的属性名是
modelValue, 事件名是update:modelValue, 支持自定义 v-model 的属性名, 事件名 - v-model 修饰符:
.trim,.number,.lazy, 支持自定义修饰符v-model.customModifier
<script setup lang="ts">import { ref } from "vue";import ChildDemo from "./ChildDemo.vue";
const text = ref<string>("Awesome Vue");</script>
<template> ParentDemo <div>text: {{ text }}</div> <ChildDemo v-model:textVal.myModifier="text" /> <ChildDemo :textVal="text" @update:textVal="(newVal) => (text = newVal)" /></template><script setup lang="ts">const props = defineProps<{ textVal: string; // 约定 xxxModifiers textValModifiers?: { myModifier: boolean; // 修饰符存在则为 true };}>();
const emit = defineEmits(["update:textVal"]);
const handleInput = (ev: Event) => { emit("update:textVal", (ev.target as HTMLInputElement).value);};</script>
<template> ChildDemo <div>Has myModifier: {{ props.textValModifiers?.myModifier ?? false }}</div> <div> textVal: <input type="text" :value="textVal" @input="handleInput" /> </div></template>自定义指令名: 以 v 开头, vDirectiveName
自定义指令的钩子函数
- created
- beforeMount/mounted
- beforeUpdate/updated
- beforeUnmount/unmounted
<script setup lang="ts">import { ref, type Directive, type DirectiveBinding } from "vue";import ChildDemo from "./ChildDemo.vue";
// 自定义指令名: 以 v 开头, vDirectiveNameconst vCustomDirective: Directive = { created(...args) { console.log("[vCustomDirective] created:", args); },
beforeMount(...args) { console.log("[vCustomDirective] beforeMount:", args); },
mounted( el: HTMLElement, binding: DirectiveBinding<{ background: string; textContent: string }>, ) { console.log("[vCustomDirective] mounted:", el, binding); el.style.background = binding.value.background; el.textContent = binding.value.textContent; },
beforeUpdate(...args) { console.log("[vCustomDirective] beforeUpdate:", args); },
updated(...args) { const el = args[0]; el.textContent = textContent.value; console.log("[vCustomDirective] updated:", args); },
beforeUnmount(...args) { console.log("[vCustomDirective] beforeUnmount", args); },
unmounted(...args) { console.log("[vCustomDirective] unmounted", args); },};
const isAlive = ref(true);const textContent = ref("Vue");const handleUpdate = () => { textContent.value += "!";};</script>
<template> <button @click="isAlive = !isAlive">挂载/卸载</button> <button @click="handleUpdate">更新</button> <ChildDemo v-if="isAlive" v-custom-directive:propName.myModifier="{ background: 'skyblue', textContent, }" /></template>自定义指令 v-auth 实现按钮鉴权
Section titled “自定义指令 v-auth 实现按钮鉴权”<script setup lang="ts">import type { Directive, DirectiveBinding } from "vue";
const userId = "lark";const authList = [ "lark:item:create", "lark:item:update" /** 'lark:item:delete' */,];
const vAuth: Directive<HTMLElement, string> = (el, binding) => { if (!authList.includes(userId + ":" + binding.value)) { el.style.display = "none"; // 如果没有权限, 则隐藏按钮 }};</script>
<template> <button v-auth="'item:create'">创建</button> <button v-auth="'item:update'">更新</button> <button v-auth="'item:delete'">删除</button></template>自定义指令 v-drag 实现可拖拽窗口
Section titled “自定义指令 v-drag 实现可拖拽窗口”<script lang="ts" setup>import type { Directive } from "vue";
const vDrag: Directive<HTMLElement> = (el) => { const draggableElem = el.firstElementChild as HTMLElement; const handleMouseDown = (downEv: MouseEvent) => { const dx = downEv.clientX - el.offsetLeft; const dy = downEv.clientY - el.offsetTop;
const handleMouseMove = (moveEv: MouseEvent) => { el.style.left = `${moveEv.clientX - dx}px`; el.style.top = `${moveEv.clientY - dy}px`; };
document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", () => document.removeEventListener("mousemove", handleMouseMove), ); };
draggableElem.addEventListener("mousedown", handleMouseDown);};</script>
<template> <!-- fixed 固定定位 --> <div v-drag class="fixed"> <div class="h-20 w-50 cursor-pointer bg-lime-100" /> <div class="h-50 w-50 bg-lime-200" /> </div></template>自定义指令 v-lazy 实现图片懒加载
Section titled “自定义指令 v-lazy 实现图片懒加载”<script lang="ts" setup>import { type Directive } from "vue";
// glob 默认懒加载const images = import.meta.glob(["@/assets/*.jpg", "@/assets/*.png"], { eager: true, // 指定立即加载});const arr = Object.values(images).map((item) => (item as any).default);// arr.length = 1const flattedArr = arr.flatMap((item) => new Array(10).fill(item));// flattedArr.length = 10const vLazy: Directive<HTMLImageElement, string> = async (el, binding) => { const placeholder = await import("@/assets/vue.svg"); el.src = placeholder.default;
// 监听目标元素与祖先元素或视口 viewport 的相交情况 // 监听目标元素和视口 viewport 的相交情况, 即监听一个元素是否可见 // entries[0].intersectionRatio 相交的比例, 一个元素可见的比例 const intersectionObserver = new IntersectionObserver((entries) => { const visibleRatio = entries[0].intersectionRatio; if (visibleRatio > 0) { setTimeout(() => (el.src = binding.value), 1500); intersectionObserver.unobserve(el); } }); intersectionObserver.observe(el);};</script>
<template> <div> <img v-lazy="item" width="1000" v-for="(item, idx) of flattedArr" :key="idx" /> </div></template>自定义 hook
Section titled “自定义 hook”<script lang="ts" setup>import { onMounted, ref, type Ref } from "vue";
const useBase64str = ( el: Ref<HTMLImageElement | null>,): Promise<{ base64str: string }> => { const toBase64str = (img: HTMLImageElement) => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); [canvas.width, canvas.height] = [img.width, img.height]; if (ctx) { ctx.drawImage(img, 0, 0, canvas.width, canvas.height); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { const avg = (data[i] + // r data[i + 1] + // g data[i + 2]) / // b 3; data[i] = data[i + 1] = data[i + 2] = avg; } ctx.putImageData(imageData, 0, 0); } const base64str = canvas.toDataURL(`image/${getExtName(img.src)}`); return base64str; };
const getExtName = (url: string) => { const urlObj = new URL(url); return urlObj.pathname.split(".").at(-1); };
return new Promise((resolve) => { onMounted(() => { el.value!.onload = () => { const base64str = toBase64str(el.value!); resolve({ base64str }); }; }); });};
const imgRef = ref<HTMLImageElement | null>(null);useBase64str(imgRef).then((res) => { imgRef.value!.src = res.base64str;});</script>
<template> <img src="@/assets/bg.jpg" id="bg" ref="imgRef" /></template>自定义指令 + 自定义 hook 综合示例
Section titled “自定义指令 + 自定义 hook 综合示例”- InterSectionObserver 监听目标元素与祖先元素或视口 viewport 的相交情况
- MutationObserver 监听整个 DOM 树的改变
- ResizeObserver 监听元素宽高的改变
import App from "./App.vue";import { createApp } from "vue";import type { App as VueApp } from "vue";
// Vue 插件可以是一个有 install 方法的对象// 也可以直接是一个安装函数// 也可以是一个有 install 属性的安装函数, install 属性值也是一个函数, 接收一个 App 实例// useResize 是一个自定义 hook, 也是一个 Vue 插件export const useResize = ( el: HTMLElement, cb: (contentRect: DOMRectReadOnly) => void,) => { const resizeObserver = new ResizeObserver((entries) => { cb(entries[0].contentRect); }); resizeObserver.observe(el);};
useResize.install = (app: VueApp) => { // 注册 v-resize 自定义指令 app.directive("resize", { mounted(el, binding) { console.log("[v-resize] mounted:", el, binding); // binding.value // (rect) => console.log("[v-resize] contentRect:", rect) useResize(el, binding.value /** cb */); }, });};
const app = createApp(App);app.use(useResize);app.mount("#app");<script lang="ts" setup>import { useResize } from '@/main'import { onMounted } from 'vue'
onMounted(() => {useResize(document.querySelector('#parent') as HTMLElement, (rect) =>console.log('[useResize] contentRect:', rect),)})
</script>
<template> <textarea id="parent" v-resize="(rect: DOMRectReadOnly) => console.log('[v-resize] contentRect:', rect)" /></template>全局变量 app.config.globalProperties
Section titled “全局变量 app.config.globalProperties”import { createApp } from "vue";import App from "./App.vue";import mitt from "mitt";
interface IEncoding { jsonMarshal<T extends object>(arg: T): string;}
const app = createApp(App);// 类型扩展declare module "vue" { export interface ComponentCustomProperties { $env: string; $encoding: IEncoding; $bus: ReturnType<typeof mitt>; }}
// 全局变量 $bus, $env, $encodingconst emitter = mitt();app.config.globalProperties.$bus = emitter;app.config.globalProperties.$env = "DEV";app.config.globalProperties.$encoding = { jsonMarshal<T extends object>(arg: T) { return JSON.stringify(arg); },};
app.mount("#app");<template> <div>$env: {{ $env }}</div> <div> $encoding.jsonMarshal: {{ $encoding.jsonMarshal({ name: "lark", age: 23 }) }} </div></template>
<script lang="ts" setup>import { getCurrentInstance } from "vue";
const app = getCurrentInstance();console.log(app?.proxy?.$env);console.log(app?.proxy?.$encoding.jsonMarshal({ name: "lark", age: 23 }));</script>全局变量 + Vue 插件综合示例
Section titled “全局变量 + Vue 插件综合示例”- Vue 插件可以是一个有 install 方法的对象
- 也可以直接是一个安装函数
<script setup lang="ts">import { ref } from "vue";const visible = ref<boolean>(true);
defineExpose({ visible, show: () => (visible.value = true), hide: () => (visible.value = false),});</script>
<template> <Transition enter-active-class="animate__animated animate__bounceIn" leave-active-class="animate__animated animate__bounceOut" > <div v-if="visible" class="h-50 w-50 bg-lime-100" /> </Transition></template>import "animate.css";
import { createApp, createVNode, render } from "vue";import App from "./App.vue";import type { Ref, VNode, App as VueApp } from "vue";import ToastDemo from "./components/ToastDemo.vue";
declare module "vue" { export interface ComponentCustomProperties { $toast: { show: () => void; hide: () => void; visible: Ref<boolean>; }; }}
// Vue 插件可以是一个有 install 方法的对象// 也可以直接是一个安装函数export const vuePluginToast = { install(app: VueApp) { const vnode: VNode = createVNode(ToastDemo); render(vnode, document.body); app.config.globalProperties.$toast = { show: vnode.component?.exposed?.show, hide: vnode.component?.exposed?.hide, visible: vnode.component?.exposed?.visible, }; },};
const app = createApp(App);app.use(vuePluginToast);
app.mount("#app");<template> <div class="flex flex-col gap-5"> <button @click="$toast.show">show</button> <button @click="$toast.hide">hide</button> <button @click="$toast.visible.value = true">show2</button> <button @click="$toast.visible.value = false">hide2</button> </div></template>app.use() 源码
Section titled “app.use() 源码”import { createApp } from "vue";import { createPinia } from "pinia";
import App from "./App.vue";import type { App as VueApp } from "vue";
interface Plugin { install: (app: VueApp, ...options: unknown[]) => unknown;}const installed = new Set();
function myUse<T extends Plugin>(plugin: T, ...options: Array<unknown>) { if (installed.has(plugin)) { return; } plugin.install(this as VueApp /** app */, ...options); installed.add(plugin); return;}
const app = createApp(App);
// app.use(createPinia())myUse.call(app, createPinia());
app.mount("#app");nextTick
Section titled “nextTick”Vue 同步更新数据, 异步更新 DOM
- Vue 将 DOM 更新加入任务队列, 等到下一个 tick (类似事件循环) 时, 才统一更新 DOM, 避免不必要的重复渲染, 提高性能
- nextTick 延迟执行 callback, 即等到下一个 tick, DOM 更新后, 再执行 callback
示例
<script setup lang="ts">import { reactive, ref, useTemplateRef, nextTick } from "vue";
const itemList = reactive([ { name: "item1", id: 1 }, { name: "item2", id: 2 },]);
const inputVal = ref("");const box = useTemplateRef<HTMLDivElement>("box");
// Vue 同步更新数据, 异步更新 DOMconst addItem = () => { itemList.push({ name: inputVal.value, id: itemList.length }); box.value!.scrollTop = 520_520_520; // 更新滚动位置 (此时 DOM 未更新)};
const addItem2 = () => { itemList.push({ name: inputVal.value, id: itemList.length }); // nextTick 延迟执行 callback, 即等到下一个 tick, DOM 更新后, 再执行 callback nextTick( () => (box.value!.scrollTop = 520_520_520), // callback (此时 DOM 已更新) );};
const addItem3 = async () => { itemList.push({ name: inputVal.value, id: itemList.length }); await nextTick(); // 等到下一个 tick, DOM 更新后 box.value!.scrollTop = 520_520_520; // 更新滚动位置 (此时 DOM 已更新)};</script>
<template> <div ref="box" class="h-30 w-50 overflow-auto border-1"> <div class="truncate border-b-1" v-for="item in itemList" :key="item.id"> {{ item }} </div> </div> <div> <textarea v-model="inputVal" type="text" class="my-3 border-1" /> <div class="flex gap-5"> <button @click="addItem">addItem</button> <button @click="addItem2">addItem2</button> <button @click="addItem3">addItem3</button> </div> </div></template>scoped 样式隔离, :deep() 样式穿透
Section titled “scoped 样式隔离, :deep() 样式穿透”scoped 样式隔离
Section titled “scoped 样式隔离”- 通过 PostCSS, 为 DOM 添加唯一的
data-v-[hash:base64:8]属性 - CSS 使用
.selector[data-v-[hash:base64:8]]属性选择器, 以实现样式隔离
:deep() 样式穿透
Section titled “:deep() 样式穿透”<script setup lang="ts">import ChildDemo from "./ChildDemo.vue";</script>
<template> <main class="wrap"> <ChildDemo class="child-bg" /> </main></template>
<style lang="css" scoped>.wrap { width: 10rem; height: 10rem; background: lightpink;}
/* .child-bg { // [!code --] */:deep(.child-bg) { width: 5rem; height: 5rem; background: lightblue;}</style><template> <div class="child-bg" /></template>
<style lang="css" scoped>.child-bg { width: 5rem; height: 5rem; background: lightgreen;}</style><main data-v-[parent-hash:base64:8] class="wrap"> <div data-v-[child-hash:base64:8] class="child-bg"></div></main><!-- ChildDemo --><style type="text/css"> .child-bg[data-v-<child-hash:base64:8>] { }</style>
<!-- ParentDemo --><style type="text/css"> .wrap[data-v-<parent-hash:base64:8>] { } .child-bg[data-v-<parent-hash:base64:8>] { }</style><!-- ChildDemo --><style type="text/css"> /* 类选择器, 交集, 属性选择器; 优先级 (0, 0, 2, 0) */ .child-bg[data-v-<child-hash:base64:8>] { background: lightpink; }</style>
<!-- ParentDemo --><style type="text/css"> .wrap[data-v-<parent-hash:base64:8>] { } /* 属性选择器, 子代, 类选择器; 优先级 (0, 0, 2, 0) */ [data-v-<parent-hash:base64:8>] .child-bg { background: lightblue; }</style>:slotted 插槽选择器, :global 全局选择器
Section titled “:slotted 插槽选择器, :global 全局选择器”:slotted() 插槽选择器
Section titled “:slotted() 插槽选择器”<script setup lang="ts">import ChildDemo from "./ChildDemo.vue";</script>
<template> <ChildDemo> <div class="parent-bg">插入到子组件的匿名插槽 default</div> </ChildDemo></template><template> <!-- 匿名插槽 name="default" --> <slot /></template>
<style lang="css" scoped>/* .parent-bg { // [!code --] */:slotted(.parent-bg) { background: lightpink;}</style>:global 全局选择器
Section titled “:global 全局选择器”- 全局选择器: 使用
:global的选择器, 不会被 vite 编译 <style lang="css">中的选择器, 是全局选择器<style lang="css" scoped>中, 并使用:global的选择器, 也是全局选择器
v-bind 动态 CSS
Section titled “v-bind 动态 CSS”<script setup lang="ts">import { ref } from "vue";
const bg = ref("#000");const text = ref({ color: "#fff" });
setInterval(() => { bg.value = bg.value === "#fff" ? "#000" : "#fff"; text.value.color = text.value.color === "#fff" ? "#000" : "#fff";}, 1000);</script>
<template> <div class="box h-20 w-20 border-1">v-bind: Dynamic CSS</div></template>
<style scoped lang="css">.box { background: v-bind(bg); color: v-bind("text.color");}</style>CSS 模块化
Section titled “CSS 模块化”<script setup lang="ts">import { useCssModule } from "vue";
const styles = useCssModule(); // 默认模块 $styleconst customStyles = useCssModule("customName"); // 自定义模块名 customNameconsole.log("styles:", styles);console.log("customStyles:", customStyles);</script>
<template> <main class="flex flex-col gap-5"> <!-- 默认模块 $style --> <div :class="$style.box">CSS Module</div> <div :class="styles.box">CSS Module</div> <!-- class 可以绑定数组 --> <div :class="[$style.box, styles.border]">CSS Module</div> <!-- 可以自定义模块名 --> <div :class="[$style.box, customName.bg]">CSS Module</div> <div :class="[styles.box, customStyles.bg]">CSS Module</div> </main></template>
<style module lang="css">.box { width: 5rem; height: 5rem; background: lightblue;}
.border { border: 1px solid #333;}</style>
<!-- 可以自定义模块名 --><style module="customName">.bg { background: lightpink;}</style><!-- h5 适配: 设置 meta 标签 --><meta name="viewport" content="width=device-width,initial-scale=1" />圣杯布局 + 全局字体大小
Section titled “圣杯布局 + 全局字体大小”圣杯布局: 两侧盒子宽度固定, 中间盒子宽度自适应的三栏布局
- rem: 相对
<html>根元素的字体大小 - vw/vh: 相对视口 viewport 的宽高, 1vw 是视口宽度的 1%, 1vh 是视口高度的 1%
- 百分比: 相对父元素的宽高
全局字体大小原理
- 定义 :root 伪类选择器的全局 CSS 变量, 所有页面都可以使用
- :root 伪类选择器和 html 元素选择器都选中
<html>根元素, 但是 :root 伪类选择器的优先级更高
<script setup lang="ts">import { useCssVar } from "@vueuse/core";
const setGlobalFontSize = (pxVal: number) => { const fontSize = useCssVar("--font-size"); fontSize.value = `${pxVal}px`; // 底层: document.documentElement.style.setProperty('--font-size', `${pxVal}px`)};</script>
<template> <header class="flex"> <div class="my-div w-[100px] bg-lime-200">left</div> <div class="my-div flex-1 bg-blue-300"> center <button class="mx-[10px]" @click="setGlobalFontSize(36)">大号字体</button> <button class="mx-[10px]" @click="setGlobalFontSize(24)">中号字体</button> <button class="mx-[10px]" @click="setGlobalFontSize(12)">小号字体</button> </div> <div class="my-div w-[100px] bg-lime-200">right</div> </header></template>
<style scoped lang="css">@reference "tailwindcss";
.my-div { @apply h-[100px] text-center leading-[100px] text-slate-500; font-size: var(--font-size);}</style>编写 postcss 插件
Section titled “编写 postcss 插件”import { defineConfig } from "vite";import vue from "@vitejs/plugin-vue";import { Plugin } from "postcss";
// pnpm i postcss -Dfunction postcssPluginPx2viewport(): Plugin { return { postcssPlugin: "postcss-plugin-px2viewport", Declaration(node) { if (node.value.includes("px")) { // console.log(node.prop, node.value); const val = Number.parseFloat(node.value); node.value = `${((val / 375) /** 设计稿宽度 375 */ * 100).toFixed(2)}vw`; } }, };}
// https://vite.dev/config/export default defineConfig({ plugins: [vue()], css: { postcss: { // 自定义 postcss 插件 plugins: [postcssPluginPx2viewport()], }, },});Vue 函数式编程
Section titled “Vue 函数式编程”<script setup lang="ts">import { h } from "vue";
interface IProps { type: "primary" | "danger";}
// Vue 函数式编程const Btn = (props: IProps, ctx: any /** { attrs, emit, slots } */) => { console.log("[btn] ctx", ctx); return h( "button", // type { style: { color: props.type === "primary" ? "lightblue" : "lightcoral" }, onClick: () => { console.log(ctx); }, }, // props ctx.slots.default(), // children );};</script>
<template> <Btn type="primary">primary</Btn> <Btn type="danger">danger</Btn></template>Vue 宏函数
Section titled “Vue 宏函数”- defineProps
- defineEmits
- defineOptions
- defineSlots
defineSlots
Section titled “defineSlots”<script setup lang="ts">import ChildDemo from "./ChildDemo.vue";const list = [ { name: "love", age: 1 }, { name: "you", age: 2 },];</script>
<template> <main> <ChildDemo :defaultList="list" :namedList="list"> <!-- item 先通过子组件的 props 父传子, 再通过子组件的 slot 子传父 --> <template #default="{ item }"> <div>defaultSlot {{ `name: ${item.name}, age: ${item.age}` }}</div> </template>
<template #named="{ item }"> <div>namedSlot {{ `name: ${item.name}, age: ${item.age}` }}</div> </template> </ChildDemo> </main></template><!-- 泛型支持 --><script generic="T extends object" setup lang="ts">import { toRefs, type RenderFunction } from "vue";
const props = defineProps<{ defaultList: T[]; namedList: T[] }>();const { defaultList, namedList } = toRefs(props);
defineSlots<{ default(props: { item: T }): unknown; named(props: { item: T }): unknown;}>();</script>
<template> <main> <ul> <li v-for="(item, idx) of defaultList" :key="idx"> <!-- 匿名的作用域插槽 --> <slot :item="item" /> </li> </ul>
<ul> <li v-for="(item, idx) of namedList" :key="idx"> <!-- 具名的作用域插槽 --> <slot :item="item" name="named" /> </li> </ul> </main></template>在项目根目录下创建环境变量文件 .env.development, .env.production, 修改 package.json
VITE_CUSTOM_ENV = '[VITE_CUSTOM_ENV] development'VITE_CUSTOM_ENV = '[VITE_CUSTOM_ENV] production'{ "scripts": { "dev": "vite --mode development", "build": "run-p type-check \"build-only {@}\" --", "preview": "vite preview" }}console.log("import.meta.env:", import.meta.env);// {// BASE_URL: '/',// DEV: true,// MODE: 'development',// PROD: false,// SSR: false// VITE_CUSTOM_ENV: '[VITE_CUSTOM_ENV] development'// }console.log("import.meta.env:", import.meta.env);// {// BASE_URL: '/',// DEV: false,// MODE: 'production',// PROD: true,// SSR: false// VITE_CUSTOM_ENV: '[VITE_CUSTOM_ENV] production'// }vite.config.ts 是 node 环境, 无法使用 import.meta.env 读取项目根目录下的环境变量文件
import { defineConfig, loadEnv } from "vite";import vue from "@vitejs/plugin-vue";
// https://vite.dev/config/export default ({ mode }: { mode: string }) => { // mode: development console.log("mode:", mode); // loadEnv: { VITE_CUSTOM_ENV: '[custom_env] development' } console.log("loadEnv:", loadEnv(mode, process.cwd())); return defineConfig({ plugins: [vue()] });};原生 Web Component 自定义元素
Section titled “原生 Web Component 自定义元素”优点: CSS, JS 隔离
class Btn extends HTMLElement { constructor() { super(); const shadowDOM = this.attachShadow({ mode: "open" }); this.div = this.h("div"); this.div.innerText = "d2vue-btn"; this.div.setAttribute( "style", `width: 100px; height: 30px; line-height: 30px; text-align: center; border: 1px solid #ccc; border-radius: 15px; cursor: pointer; `, ); shadowDOM.appendChild(this.div); }
h(el) { return document.createElement(el); }
connectedCallback() { console.log("[d2vue-btn] Connected"); } disconnectedCallback() { console.log("[d2vue-btn] Disconnect"); } adoptedCallback() { console.log("[d2vue-btn] Adopted"); } attributeChangedCallback() { console.log("[d2vue-btn] Attribute changed"); }}
window.customElements.define("d2vue-btn", Btn);class Btn2 extends HTMLElement { constructor() { super(); const shadowDOM = this.attachShadow({ mode: "open" }); this.template = this.h("template"); this.template.innerHTML = ` <style> .btn { width: 100px; height: 30px; line-height: 30px; text-align: center; border: 1px solid #ccc; border-radius: 15px; cursor: pointer; } </style> <div class="btn">d2vue-btn2</div>`; shadowDOM.appendChild(this.template.content.cloneNode(true)); }
h(el) { return document.createElement(el); } connectedCallback() { console.log("[d2vue-btn2] Connected"); } disconnectedCallback() { console.log("[d2vue-btn2] Disconnect"); } adoptedCallback() { console.log("[d2vue-btn2] Adopted"); } attributeChangedCallback() { console.log("[d2vue-btn2] Attribute changed"); }}
window.customElements.define("d2vue-btn2", Btn2);<!doctype html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <script src="./btn.js"></script> <script src="./btn2.js"></script> </head> <body> <d2vue-btn></d2vue-btn> <d2vue-btn2></d2vue-btn2> </body></html>Vue 中使用 Web Component 自定义元素
Section titled “Vue 中使用 Web Component 自定义元素”import { defineConfig } from "vite";import vue from "@vitejs/plugin-vue";
// https://vite.dev/config/export default defineConfig({ plugins: [ vue({ template: { compilerOptions: { // 文件名带 - , 文件拓展名 .ce.vue 的单文件组件, 视为 Web Component 自定义元素 isCustomElement: (tag) => tag.startsWith("d2vue-"), }, }, }), ],});<script setup lang="ts">defineProps<{ item: { name: string; age: number } }>();</script>
<template> <!-- 不能使用 tailwindcss --> <div class="btn">name: {{ item.name }}, age: {{ item.age }}</div></template>
<style lang="css" scoped>.btn { width: 250px; height: 50px; line-height: 50px; text-align: center; border: 1px solid #ccc; border-radius: 25px; cursor: pointer;}</style><script setup lang="ts">import { defineCustomElement } from "vue";import D2vueBtn from "@/components/d2vue-btn.ce.vue";
// Vue 中使用 Web Component 自定义元素const Btn = defineCustomElement(D2vueBtn);window.customElements.define("d2vue-btn", Btn);const item = { name: "lark", age: 23 };</script>
<template> <d2vue-btn :item="item"></d2vue-btn></template>Proxy 跨域
Section titled “Proxy 跨域”同源: 主机 (域名), 端口, 协议都相同
原理: HTML 文件的 <script> 标签没有跨域限制
<!doctype html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <script> function jsonp(req /* { url, callback } */) { const script = document.createElement("script"); const url = `${req.url}?callback=${req.callback.name}`; script.src = url; // 浏览器请求该 <script> 标签的 src // 响应: frontendFn({"data":"I love you"}) document.getElementsByTagName("head")[0].appendChild(script); }
function frontendFn(res) { alert(`res.data: ${res.data}`); }
// frontendFn.name: frontendFn console.log("frontendFn.name:", frontendFn.name); jsonp({ url: "http://localhost:8080", callback: frontendFn }); </script> </head> <body></body></html>import http from "node:http";import urllib from "node:url";
const port = 8080;const cbParams = { data: "I love you" };http .createServer((req, res) => { const params = urllib.parse(req.url, true); if (params.query.callback) { // callback: frontendFn console.log("callback:", params.query.callback); // JSONP, JSON with Padding const jsonWithPadding = `${params.query.callback}(${JSON.stringify(cbParams)})`; // jsonWithPadding: frontendFn({"data":"I love you"}) console.log("jsonWithPadding:", jsonWithPadding); res.end(jsonWithPadding); } else { res.end(); } }) .listen(port, () => { console.log(`http://localhost:${port}`); });Vite 代理
Section titled “Vite 代理”import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";import vue from "@vitejs/plugin-vue";
// https://vite.dev/config/export default defineConfig({ plugins: [vue()], resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)), }, }, server: { proxy: { "/api": { target: "http://localhost:8080", rewrite: (path) => path.replace(/^\/api/, ""), }, }, },});后端允许跨域
Section titled “后端允许跨域”function cors(req, res, next) { res.header("Access-Control-Allow-Origin", "*"); res.header( "Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization", ); // res.header("Access-Control-Allow-Credentials", true); res.header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS"); res.header("Content-type", "application/json;charset=utf-8"); // 预检 (pre-flight) 请求 if (req.method.toUpperCase() === "OPTIONS") { return res.sendStatus(204); } next();}