脚手架构建项目
- 方式一
- 方式二
生命周期
写了多个生命周期时会顺序触发
获取dom
引入字体图标
index.html里引入
代码结构
- Script
普通的script块允许有多个,但setup的script块不允许有多个
下面则是OK的
- Template
单文件只允许出现一个Template块
Template里允许有多个div
- Style
style块允许出现多个
插件
模板语法
- 与表达式向结合
v-show和v-if区别
v-show操作的是css
v-if是操作元素节点的
点击事件
- 冒泡
解决
- 默认事件
点击后会刷新页面
解决
动态样式
- 案例1
- 案例二
- 案例三
- 案例四
- 案例五
如果是对象,需要加引号
- 案例六
$style也可以自定义名称
- 案例7
当y大于78时,添加show的class
ref
- 复杂类型
- 自定义ref
- customRef实现防抖
reactive
- 数组添加元素
to系列
- toRef对非响应式对象无效
点击无效
点击变化
应用场景:
例如一个方法需要传参,参数是一个对象里的某个属性,且需要这个参数是响应式的
- toRefs
- toRaw
computed
另一种写法
计算属性里不应该有副作用(如在计算里修改dom,请求数据等)
watch
- 基本用法
- 监听多个
或者
- 深度监听
-
精确的深度监听
-
第一次就触发监听
- 监听对象的单一属性
字符类型需使用回调函数
watchEffect
- 监听单个
- 监听多个
-
监听之前操作
-
停止监听
组件生命周期
A.vue
<template>
<div>{{ msg }}</div>
<button @click="update">更新组件</button>
</template>
<script lang="ts" setup>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
ref,
} from "vue";
const msg = ref("我是个组件");
const update = () => {
msg.value = "我被更新了";
};
// 挂载前
onBeforeMount(() => {
console.log("挂载前");
});
// 挂载完成
onMounted(() => {
console.log("挂载完成");
});
// 更新前
onBeforeUpdate(() => {
console.log("更新前");
});
// 更新完成
onUpdated(() => {
console.log("更新完成");
});
// 卸载前
onBeforeUnmount(() => {
console.log("卸载前");
});
// 卸载完成
onUnmounted(() => {
console.log("卸载完成");
});
</script>
<style lang="scss" scoped></style>
APP.vue
<template>
<A v-if="flag"></A>
<br />
<button @click="flag = !flag">组件卸载</button>
</template>
<script setup lang="ts">
import { ref } from "vue";
import A from "@/components/A.vue";
const flag = ref(true);
console.log("setup");
</script>
<style scoped></style>
父传子
- 子组件
<template>
<div>我是子组件:{{ name }}</div>
</template>
<script lang="ts" setup>
const props = defineProps({
name: {
type: String,
default: "xiaohong",
requier: false,
},
});
</script>
<style lang="scss" scoped></style>
- 父组件
<template>
<input type="text" v-model="myName" />
<A :name="myName"></A>
</template>
<script setup lang="ts">
import { ref } from "vue";
import A from "@/components/A.vue";
const myName = ref("");
console.log("setup");
</script>
<style scoped></style>
子传父
- 子组件
<template>
<button @click="send">子组件更新</button>
</template>
<script lang="ts" setup>
const emit = defineEmits(["update"]);
const send = () => {
emit("update", "我是来自子组件的数据");
};
</script>
<style lang="scss" scoped></style>
- 父组件
<template>
<input type="text" v-model="myName" />
<A @update="updateFromChild"></A>
</template>
<script setup lang="ts">
import { ref } from "vue";
import A from "@/components/A.vue";
const myName = ref("");
const updateFromChild = (newVal: string) => {
myName.value = newVal;
};
</script>
<style scoped></style>
父调子里的方法和参数
子组件
<template>
<div>我是子组件</div>
</template>
<script lang="ts" setup>
defineExpose({
name: "xiaohong",
open: () => {
console.log("我是子组件里的open方法");
},
});
</script>
<style lang="scss" scoped></style>
父组件
<template>
<A ref="myA"></A>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import A from "@/components/A.vue";
// 变量名字需要与ref的值一致
const myA = ref<InstanceType<typeof A>>();
onMounted(() => {
console.log(myA.value?.name);
myA.value?.open();
});
</script>
<style scoped></style>
跨层组件
局部组件
上面都是
全局组件
main.ts里注册
递归组件
可以自定义当前组件的名称,在script块内
需要注意的是递归组件会有点击冒泡
动态组件
<template>
<div style="display: flex">
<div
class="tabs"
v-for="(item, index) in data"
:key="index"
@click="comId = item.com"
>
<div>{{ item.name }}</div>
</div>
</div>
<component :is="comId"></component>
</template>
<script setup lang="ts">
import { reactive, markRaw, shallowRef } from "vue";
import A from "@/components/A.vue";
import B from "@/components/B.vue";
import C from "@/components/C.vue";
const comId = shallowRef(A);
const data = reactive([
{
name: "A组件",
com: markRaw(A),
},
{
name: "B组件",
com: markRaw(B),
},
{
name: "C组件",
com: markRaw(C),
},
]);
</script>
<style scoped>
.tabs {
border: 1px solid #ccc;
padding: 5px 10px;
margin: 5px;
cursor: pointer;
}
</style>
注意这里使用了markRaw和shallowRef
不然会报警告
插槽
- 匿名插槽
- 具名插槽
- 插槽传值
子组件
<template>
<div style="color: red">我是上面<slot name="up"></slot></div>
<div style="color: red" v-for="(item, index) in datas" :key="index">
<slot name="center" :item="item" :index="index"></slot>
</div>
<div style="color: red">我是下面<slot name="foot"></slot></div>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
type names = {
name: string;
age: number;
};
const datas = reactive<names[]>([
{
name: "name1",
age: 18,
},
{
name: "name2",
age: 19,
},
{
name: "name3",
age: 20,
},
]);
</script>
<style lang="scss" scoped></style>
父组件
<template>
<A>
<template v-slot:center="{ item, index }">
<div>{{ item }} ---- {{ index }}</div>
</template>
</A>
</template>
<script setup lang="ts">
import A from "@/components/A.vue";
</script>
<style scoped></style>
v-slot可以简写成#
- 动态插槽
直接访问json数据
将json数据放在public目录下
启动服务,直接用浏览器访问即可
使用ts封装axios
export const axios = {
get<T>(url: string): Promise<T> {
return new Promise<T>((resolve) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
xhr.onreadystatechange = () => {
// 0 -(未初始化)还没有调用send()方法
// 1 -(载入)已调用send()方法,正在发送请求
// 2-(载入完成) send()方法执行完成,已经接收到全部响应内容3 -(交互)正在解析响应内容
// 4 -(完成)响应内容解析完成,可以在客户端调用了
if (xhr.readyState == 4 && xhr.status == 200) {
// 模拟响应时间
setTimeout(() => {
resolve(JSON.parse(xhr.responseText));
}, 2000);
}
};
});
},
};
使用
<script lang="ts" setup>
import { axios } from "@/server/axios";
interface Data {
data: {
name: string;
age: string;
url: string;
desc: string;
};
}
const { data } = await axios.get<Data>("./data.json");
</script>
异步组件和suspense
<template>
<Suspense>
<template #default>
<!-- 数据加载完成时显示的内容 -->
<SyncVue></SyncVue>
</template>
<template #fallback>
<!-- 数据未加载完成时显示的内容 -->
<Skeleton></Skeleton>
</template>
</Suspense>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from "vue";
// 骨架屏
import Skeleton from "@/components/skeleton.vue";
// 异步引入组件
const SyncVue = defineAsyncComponent(() => import("@/components/sync.vue"));
</script>
<style scoped></style>
defineAsyncComponent的另一种写法
异步组件在打包的时候会被单独拆出来,在使用到的时候才会去加载,算一种调优手段
Teleport
加前
加后
keep-alive
未加
切换组件不缓存值
加上keep-live
切换也有值
如果只想缓存B组件
注意数组里面填的需要是组件的名称
没有的记得去组件里面加上
排除某个组件不缓存
这里同样也需要组件有名称
最大缓存组件数量
内部算法,将不常用的给忽略
加入keep-alive后的生命周期
transition
<template>
<button @click="flag = !flag">切换</button>
<Transition name="fade">
<div v-if="flag" class="box"></div>
</Transition>
</template>
<script setup lang="ts">
import { ref } from "vue";
const flag = ref(false);
</script>
<style scoped>
.box {
width: 200px;
height: 200px;
background-color: green;
}
/* 进入之前 */
.fade-enter-from {
width: 0px;
height: 0px;
}
/* 进入时 */
.fade-enter-active {
transition: all 1.5s ease;
}
/* 退出 */
.fade-enter-to {
width: 200px;
height: 200px;
}
</style>
自定义动画样式名
如结合第三方库animate.css使用
定义动画时间
transition生命周期
页面加载完成就触发动画
transition-group
- 渲染列表动画时使用
- 实现动态数字列表移动过渡
<template>
<div>
<button @click="random">random</button>
<TransitionGroup move-class="mct" class="wraps" tag="div">
<div v-for="item in list" :key="item.id" class="items">
{{ item.number }}
</div>
</TransitionGroup>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import _ from "lodash";
let list = ref(
Array.apply(null, { length: 81 } as number[]).map((_, index) => {
return {
id: index,
number: (index % 9) + 1,
};
})
);
const random = () => {
list.value = _.shuffle(list.value);
};
</script>
<style scoped lang="less">
.wraps {
display: flex;
flex-wrap: wrap;
width: calc(25px * 9 + 9px);
.items {
width: 25px;
height: 25px;
border: 1px solid #ccc;
display: flex;
justify-content: center;
align-items: center;
}
}
.mct {
transition: all 1.5s;
}
</style>
- 状态过渡
<template>
<div>
<input type="number" step="20" v-model="num.current" />
<div>
{{ num.tweenedNumber.toFixed(0) }}
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, watch } from "vue";
import gsap from "gsap";
const num = reactive({
current: 0,
tweenedNumber: 0,
});
watch(
() => num.current,
(newVal, oldVal) => {
gsap.to(num, {
duration: 1,
tweenedNumber: newVal,
});
}
);
</script>
<style scoped lang="less"></style>
数字变化动画
mitt
用来兄弟组件传参
加入
import { createApp } from "vue";
import App from "./App.vue";
import "./assets/main.css";
import A from "@/components/A.vue";
import mitt from "mitt";
const Mit = mitt();
declare module "vue" {
export interface ComponentCustomProperties {
$Bus: typeof Mit;
}
}
const app = createApp(App);
app.config.globalProperties.$Bus = Mit
app.component("MyA", A);
app.mount("#app");
使用
B组件
<template>
<div>我是B组件</div>
<input type="text" v-model="msg" />
<button @click="emit">发送</button>
</template>
<script lang="ts" setup>
import { ref, getCurrentInstance } from "vue";
const msg = ref("");
const instance = getCurrentInstance();
const emit = () => {
instance?.proxy?.$Bus.emit("on-send", msg.value);
};
</script>
<style lang="scss" scoped></style>
C组件
<template>
<div>我是C组件</div>
<input type="text" v-model="msg" />
</template>
<script lang="ts" setup>
import { ref, getCurrentInstance } from "vue";
const msg = ref("");
const instance = getCurrentInstance();
instance?.proxy?.$Bus.on("on-send", (str) => {
msg.value = "来自B===>" + JSON.stringify(str);
});
</script>
<style lang="scss" scoped></style>
父子组件v-model
- 单个v-model
父组件
<template>
<div>我是父组件</div>
<div>msg===>{{ msg }}</div>
<input type="text" v-model="msg" />
<A v-model="msg"></A>
</template>
<script setup lang="ts">
import { ref } from "vue";
import A from "@/components/A.vue";
const msg = ref<string>("");
</script>
<style scoped lang="less"></style>
子组件
<template>
<div>我是A组件</div>
<div>收到父组件值===>{{ modelValue }}</div>
<input type="text" v-model="msgA" /><button @click="update">send</button>
</template>
<script lang="ts" setup>
import { ref } from "vue";
// modelValue是固定名称
defineProps<{
modelValue: string;
}>();
const msgA = ref("");
// update:modelValue是固定名称
const emit = defineEmits(["update:modelValue"]);
const update = () => {
console.log("触发了");
emit("update:modelValue", msgA.value);
};
</script>
<style lang="scss" scoped></style>
- 多v-model
父组件
<template>
<div>我是父组件</div>
<div>msg===>{{ msg }}</div>
msg: <input type="text" v-model="msg" /> text:
<input type="text" v-model="text" />
<A v-model="msg" v-model:text="text"></A>
</template>
<script setup lang="ts">
import { ref } from "vue";
import A from "@/components/A.vue";
const msg = ref<string>("");
const text = ref<string>("");
</script>
<style scoped lang="less"></style>
子组件
<template>
<div>我是A组件</div>
<div>收到msg===>{{ modelValue }}</div>
<div>收到text===>{{ text }}</div>
<input type="text" v-model="msgA" @input="updateMsg" />
<input type="text" v-model="textA" @input="updateText" />
</template>
<script lang="ts" setup>
import { ref } from "vue";
// modelValue是固定名称
defineProps<{
modelValue: string;
text: string;
}>();
const msgA = ref("");
const textA = ref("");
// update:modelValue是固定名称
const emit = defineEmits(["update:modelValue", "update:text"]);
const updateMsg = () => {
emit("update:modelValue", msgA.value);
};
const updateText = () => {
emit("update:text", textA.value);
};
</script>
<style lang="scss" scoped></style>
- 自定义v-model属性
不加.isBt
加.isBt
directive自定义指令
- 在mounted打印出所有信息
<template>
<div>我是父组件</div>
<A v-move:aaa.xiaohong="{ background: 'red' }"></A>
</template>
<script setup lang="ts">
import type { Directive } from "vue";
import A from "@/components/A.vue";
// 自定义指令名字必须以v开头
const vMove: Directive = {
// 元素初始化的时候
created() {
console.log("===>created");
},
// 指令绑定到元素后调用,只调用一次
beforeMount() {
console.log("===>beforeMount");
},
// 元素插入父级dom调用
mounted(...args: Array<any>) {
console.log("===>mounted");
console.log(args);
},
// 元素被更新之前调用
beforeUpdate() {
console.log("===>beforeUpdate");
},
// 元素更新后调用
updated() {
console.log("===>updated");
},
// 在元素被移除前调用
beforeUnmount() {
console.log("===>beforeUnmount");
},
// 执行被移除后调用,只调用一次
unmounted() {
console.log("===>unmounted");
},
};
</script>
<style scoped lang="less"></style>
- 具体解析
<template>
<div>我是父组件</div>
<A v-move:aaa.xiaohong="{ background: 'red' }" class="box"></A>
</template>
<script setup lang="ts">
import type { Directive, DirectiveBinding } from "vue";
import A from "@/components/A.vue";
type Dir = {
background: string;
};
// 自定义指令名字必须以v开头
const vMove: Directive = {
// 元素初始化的时候
created() {
console.log("===>created");
},
// 指令绑定到元素后调用,只调用一次
beforeMount() {
console.log("===>beforeMount");
},
// 元素插入父级dom调用
mounted(el: HTMLElement, dir: DirectiveBinding<Dir>) {
console.log("===>mounted");
el.style.backgroundColor = dir.value.background;
},
// 元素被更新之前调用
beforeUpdate() {
console.log("===>beforeUpdate");
},
// 元素更新后调用
updated() {
console.log("===>updated");
},
// 在元素被移除前调用
beforeUnmount() {
console.log("===>beforeUnmount");
},
// 执行被移除后调用,只调用一次
unmounted() {
console.log("===>unmounted");
},
};
</script>
<style scoped lang="less">
.box {
height: 200px;
width: 200px;
}
</style>
- 简写
<template>
<div>我是父组件</div>
<input type="text" v-model="color" />
<A v-move="{ background: color }" class="box"></A>
</template>
<script setup lang="ts">
import type { Directive, DirectiveBinding } from "vue";
import { ref } from "vue";
import A from "@/components/A.vue";
const color = ref("");
type Dir = {
background: string;
};
const vMove: Directive = (el: HTMLElement, dir: DirectiveBinding<Dir>) => {
el.style.backgroundColor = dir.value.background;
};
</script>
<style scoped lang="less">
.box {
height: 200px;
width: 200px;
}
</style>
- 自定义拖拽指令
<template>
<div v-move>
<div class="box">我是父组件</div>
</div>
</template>
<script setup lang="ts">
import type { Directive, DirectiveBinding } from "vue";
import { ref } from "vue";
import A from "@/components/A.vue";
const vMove: Directive = (el: HTMLElement, bingding: DirectiveBinding) => {
console.log(el);
let moveElement: HTMLDivElement = el.firstElementChild as HTMLDivElement;
const mouseDown = () => {
const move = (e: MouseEvent) => {
// let X = e.clientX - el.offsetLeft;
// let Y = e.clientY - el.offsetTop;
el.style.left = e.clientX + "px";
el.style.top = e.clientY + "px";
};
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", () => {
document.removeEventListener("mousemove", move);
});
};
moveElement.addEventListener("mousedown", mouseDown);
};
</script>
<style scoped lang="less">
.box {
height: 200px;
width: 200px;
text-align: center;
background-color: #ccc;
}
</style>
hooks
- 自定义将图片转base64的hook
useBase64.ts
import { onMounted } from "vue";
type Options = {
el: string;
};
export default function (options: Options): Promise<{ baseUrl: string }> {
return new Promise((resolve) => {
onMounted(() => {
const img: HTMLImageElement = document.querySelector(
options.el
) as HTMLImageElement;
console.log(img, "=====>");
img.onload = () => {
resolve({
baseUrl: base64(img),
});
};
});
const base64 = (el: HTMLImageElement) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = el.width;
canvas.height = el.height;
ctx?.drawImage(el, 0, 0, canvas.width, canvas.height);
return canvas.toDataURL("image/png");
};
});
}
App.vue
<template>
<div v-move>
<img id="img" width="300" height="300" src="./assets/inther.png" alt="" />
</div>
</template>
<script setup lang="ts">
import useBase64 from "./hooks/useBase64";
useBase64({
el: "#img",
}).then((res) => {
console.log(res.baseUrl);
});
</script>
<style scoped lang="less"></style>
定义全局变量和函数
main.ts
import { createApp } from "vue";
import App from "./App.vue";
import "./assets/main.css";
import A from "@/components/A.vue";
import mitt from "mitt";
const Mit = mitt();
declare module "vue" {
export interface ComponentCustomProperties {
$Bus: typeof Mit;
}
}
const app = createApp(App);
app.config.globalProperties.$Bus = Mit;
// 定义全局变量
app.config.globalProperties.$env = "env";
// 定义全局方法
app.config.globalProperties.$filters = {
format<T>(str: T) {
return `小鸿-${str}`;
},
};
type Filter = {
format<T>(str: T): string;
};
// 对全局变量和方法进行声明,解决报错
declare module "vue" {
export interface ComponentCustomProperties {
$filters: Filter;
$env: string;
}
}
app.component("MyA", A);
app.mount("#app");
App.vue
<template>
<div>
{{ $env }}
</div>
<div>
{{ $filters.format("Hello World") }}
</div>
</template>
<script setup lang="ts">
// 在script里使用
import { getCurrentInstance } from "vue";
const instance = getCurrentInstance();
console.log(instance?.proxy?.$env);
</script>
<style scoped lang="less"></style>
自定义插件
- 目录结构
- index.ts
import type { App, VNode } from "vue";
import Loading from "./index.vue";
import { createVNode, render } from "vue";
export default {
install(app: App) {
// 将组件转成Vnode
const Vn: VNode = createVNode(Loading);
// 将组件挂载到body上
render(Vn, document.body);
// 全局挂载方法
app.config.globalProperties.$loading = {
show: Vn.component?.exposed?.show,
hide: Vn.component?.exposed?.hide,
};
// 调用全局方法
// app.config.globalProperties.$loading.show();
},
};
- index.vue
<template>
<div v-if="isShow" class="loading">Loading</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const isShow = ref(false);
const show = () => (isShow.value = true);
const hide = () => (isShow.value = false);
defineExpose({
isShow,
show,
hide,
});
</script>
<style scoped>
.loading {
background: black;
opacity: 0.2;
font-size: 30px;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
color: white;
}
</style>
- main.ts
import { createApp } from "vue";
import App from "./App.vue";
import "./assets/main.css";
import Loading from "./components/Loading";
const app = createApp(App);
type Lod = {
show: () => void;
hide: () => void;
};
// 编写ts 解决loading 声明文件放置错误和智能提示
declare module "vue" {
export interface ComponentCustomProperties {
$loading: Lod;
}
}
app.use(Loading);
app.mount("#app");
- App.vue
<template>
<div></div>
</template>
<script setup lang="ts">
import { getCurrentInstance } from "vue";
const instance = getCurrentInstance();
instance?.proxy?.$loading.show();
setTimeout(() => {
instance?.proxy?.$loading.hide();
}, 5000);
</script>
<style scoped lang="less"></style>
- 效果
5s后消失
deep
选择器
- 插槽选择器
父组件
子组件
效果
- 全局选择器
环境变量
默认是生产环境
切换环境
在ts里使用
vite.config.ts
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { loadEnv } from "vite";
// https://vitejs.dev/config/
export default ({ mode }: any) => {
// 获取当前环境
// development 生产环境
// production 线上环境
console.log(mode);
// 获取对应环境变量文件里的内容
console.log(loadEnv(mode, process.cwd()));
return defineConfig({
plugins: [vue()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
};
Pinia
重置
监听
Test里的内容每次改变都会触发
也可以加参数
detached: 组件销毁后是否继续监听
deep: 是否深度监听
flush: 监听的时机
调用action监听
加参数
组件销毁后是否继续监听
小案例
getters
action异步
响应式丢失
调试
router
-
router-link和a标签的区别
router-link不会刷新页面
a标签会刷新当前页面 -
利用name跳转
- 编程式跳转
- 不保留历史记录
- 编程前进后退
- 路由传参
-
query传参
-
params传参
只能用name跳转
传递的产生不会展示在url上,而是存在内存里
-
动态路由参数
- 路由重定向
-
别名
配置后访问/root,/root1,/root2都是同一页面 -
前置守卫
beforeEach可以有多个,next()表示进入到下一个beforeEach
- 后置守卫
这里用来做进度条
-
路由元信息
-
路由过渡动效
-
滚动行为
切换回来可以保持之前的页面位置
也可以加上延时 -
动态路由
后端返回内容
动态添加
json转ts插件
选中需要转换的json
ctrl+alt+shift+s