Vue3文档
demo地址
https://gitee.com/galen.zhang/vue3-demo/tree/master/vue3-base-api
Vue3常用插件
Volar、Vue VSCode Snippets、Auto Close Tag、Vue Peek、Vite
测试模板文件 template.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue模板</title>
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
</head>
<body>
<div id="app">
<h1>Vue实例</h1>
<hr />
</div>
<script>
const app = Vue.createApp({
data() {
return {
message: "Hello Vue!",
};
},
});
app.mount("#app");
</script>
</body>
</html>
指令
v-model
实现数据和表单元素绑定
v-model
的修饰符:tirm
、number
<p>前面{{msg}}后面</p>
<input type="text" placeholder="请输入内容" v-model.trim="msg" />
<br />
<input type="text" v-model.number="a" />+<input type="text" v-model.number="b" />={{a+b}}
<!-- .enter是键盘修饰符,相当于按下回车键 -->
<input type="text" @keyup.enter="keyUpHandle" />
v-on
绑定事件,可以简写为 @
修饰符 https://cn.vuejs.org/guide/essentials/event-handling.html#key-modifiers
v-bind
绑定属性,可以简写成:
<p :class="classP3"></p>
#script
// 对象表示样式的时候,属性名表示样式名,属性值为bool值(true表示生效,false表示无效)
classP3: {
a: true,
b: false,
c: true,
},
watch
监听一个数据的改变,数据改变之后执行一个操作
computed
计算属性,当依赖的数据改变之后会重新计算一个结果
计算属性具有缓存功能
计算属性和方法调用的区别:
-
计算属性具有缓存功能,依赖的数据不改变不会重新计算
-
计算属性可以直接调用,不需要加括号
-
方法调用的时候需要加括号
-
方法在每一次组件更新的时候都会重新执行
// 模拟购物中全选、取消选择,计算总价
<label>全选</label>
<label >{{item.name}},¥{{item.price}},数量:<button
@click="item.amount>1?item.amount--:null"
>
-</button >{{item.amount}}<button @click="item.amount++">+</button></label >
总价:{{sumPrice}}
// script
data() {
return {
carts: [
{
id: 1,
name: "外套",
chk: false,
amount: 2,
price: 100,
},
{
id: 2,
name: "裙子",
chk: true,
amount: 1,
price: 50,
},
{
id: 3,
name: "鞋子",
chk: false,
amount: 3,
price: 200,
},
],
};
},
computed: {
// 直接返回一个结果,相当于只设置了get
double() {
console.log("计算属性执行了");
return this.num * 2;
},
// 另一种写法
// set表示主动设置值
// get表示被动取值
chkAll: {
set(v) {
// console.log(v);
this.carts.forEach((item) => (item.chk = v));
},
get() {
return this.carts.every((item) => item.chk);
},
},
sumPrice() {
return this.carts
.filter((item) => item.chk) // 获取选中的
.reduce((pre, cur) => pre + cur.price * cur.amount, 0); // 求和
},
},
refs
获取dom元素
nextTick
Vue中dom更新是异步的,数据改变后无法直接获取dom中的最新值。使用这个api可以获取到
<h1 ref="txt">当前的num为:{{num}}</h1>
<button @click="clickHandle">点一下</button>
// script
data() {
return {
num: 1
};
},
methods: {
clickHandle() {
this.num++;
// console.log(this.$refs.txt.innerText);
// $nextTick 是一个内置的全局api,可以在其回调函数中获取dom的最新值
this.$nextTick(() => {
console.log(this.$refs.txt.innerText);
});
},
}
组件
自定义标签
- 局部组件,定义好之后需要先注册再使用
- 全局组件,定义好之后可以直接使用
组件传参
-
父传子,使用props属性
-
子传父,使用事件派发
-
非相关组件,使用provide/inject依赖注入,或者使用vuex各pinia全局状态管理插件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Vue 组件</title> <script src="https://cdn.jsdelivr.net/npm/vue@3"></script> </head> <body>Vue实例
<hello-world title="这是一个很厉害的组件" desc="简单的描述"></hello-world>
</body> </html><script> const Shower = { template: `<span>这是一个组件</span> <demo></demo> <button @click="clickHandle">点一下</button> `, methods: { clickHandle() { // // console.log('按钮被点了'); // 通过emit派发事件 // 参数一 表示事件的名字 // 参数二 表示传递的数据 this.$emit("dianle", "111"); }, }, }; const HelloWorld = { // 表示组件的模版.在Vue3中不再限制组件的最外层只能有一个根节点 // 但是还是建议大家为组件只设置一个根节点 template: `<div class="hw"> <h2>{{title}}</h2> <p>{{txt}}</p> <button @click="count++">{{count}}</button> <Shower @dianle="dlHandle"></Shower> </div>`, // props 表示从外部传递到组件中的参数 props: ["title", "desc"], data() { return { txt: "哈哈", count: 1, }; }, methods: { dlHandle(txt) { console.log(txt); }, }, components: { Shower, }, }; const app = Vue.createApp({ data() { return { message: "Hello Vue!", }; }, components: { HelloWorld, }, }); // 全局组件 // 参数一 表示组建的名字 // 参数二 表示组件的配置信息 app.component("demo", { template: '<a href="#">这是一个链接</a>', }); app.mount("#app"); </script>
组件生命周期函数
8个常见的生命周期钩子函数
- beforeCreate
- created:调用接口获取数据
- beforeMount
- mounted:获取dom元素
- beforeUpdate,重复执行
- updated,重复执行,在5和6两个钩子函数中不能修改数据,改了数据之后会引起死循环
- beforeUnmount
- unmounted
下面2个需要配合keep-alive使用。keep-alive可以对组件做缓存
- activated,激活
- deactivated,取消激活
嵌套组件的生命周期钩子函数
- 先执行到父组件的beforeMount之后,开始解析dom
- 当遇到子组件时,会执行所有子组件的创建到挂载完成
- 当所有的子组件都挂载完成之后,执行父组件的挂载完成
component
动态组件,通过is属性控制当前显示哪一个组件
<button @click="current='Home'">Home</button>
<button @click="current='List'">List</button>
<button @click="current='About'">About</button>
<button @click="current='User'">User</button>
<hr />
<keep-alive>
<component :is="current" />
</keep-alive>
创建单页面应用
2种创建项目的方式
-
使用Vite创建项目
npm init vite@latest
-
使用
npm create vue@latest
npm init vite@latest
vue-demo#文档
https://cn.vuejs.org/guide/quick-start.html#creating-a-vue-applicationcd vue-demo
npm install
npm run dev
script setup
语法方式
<script setup>
import { ref } from 'vue'
const count = ref(0)
const txt = ref('')
const clickHandle = () => {
console.log(111);
}
</script>
<template>
<h1>Vue3</h1>
<button @click="count++">{{ count }}</button>
<p>{{ txt }}</p>
<input type="text" placeholder="请输入内容" v-model="txt" />
<button @click="clickHandle">点一下</button>
</template>
<style scoped></style>
安装路由vue-router
npm i vue-router
定义路由文件 router/index.js
import { createRouter, createWebHashHistory } from "vue-router";
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: "/",
name: "Home",
component: () => import("../views/Home.vue"),
},
{
path: "/user",
name: "User",
component: () => import("../views/User.vue"),
},
],
});
//路由前置守卫
router.beforeEach((to, from, next) => {
console.log("beforeEach");
next();
});
export default router;
在 main.js
中引入路由
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
测试一下,在 App.vue
中加入路由链接
<script setup>
</script>
<template>
<router-link :to="{ name: 'Home' }">【首页】</router-link>
<router-link :to="{ name: 'List' }">【列表】</router-link>
<router-link :to="{ name: 'User' }">【我的】</router-link>
<hr />
<router-view class="main"></router-view>
</template>
<style scoped></style>
demo地址
https://gitee.com/galen.zhang/vue3-demo/tree/master/vue-demo
处理组件复用不会再次初始化数据
http://localhost:5173/#/list // 查询所有数据
http://localhost:5173/#/list?tid=1
url路由改变了,但组件复用不会重新初始化
<script setup>
import { onBeforeRouteUpdate } from 'vue-router'
import router from '../router';
const categories = [
{
id: 1,
name: '手机'
},
{
id: 2,
name: '相机'
}, {
id: 3,
name: '美妆'
}, {
id: 4,
name: '服装'
}
]
// onBeforeRouteUpdate 路由改变,但是组件复用的时候执行
onBeforeRouteUpdate((to, from) => {
console.log(to.query)
queryData(to.query.tid);
})
</script>
<template>
<div class="list">
<h1>列表</h1>
<router-link v-for="item in categories" :key="item.id"
:to="{ name: 'List', query: { tid: item.id } }">【{{ item.name }}】</router-link>
</div>
</template>
<style scoped></style>
computed
计算属性
如果只需要计算出一个值,参数为一个函数,返回计算的结果
如果需要主动设置值+被动改变,传入一个对象,里面包含set/get函数
<script setup>
import { ref, computed } from 'vue';
const carts = ref([
{
id: 1,
amount: 2,
chk: false,
product: {
id: 111,
name: 'AirPods Pro',
price: 1299,
},
},
{
id: 2,
amount: 3,
chk: true,
product: {
id: 112,
name: 'AirPods Pro 2',
price: 1899,
},
},
{
id: 3,
amount: 1,
chk: true,
product: {
id: 113,
name: 'IPhone 14 pro',
price: 7899,
},
},
]);
const chkAll = computed({
// 主动设置值
set(v) {
carts.value.forEach((item) => (item.chk = v));
},
// 被动改变
get() {
return carts.value.every((item) => item.chk);
},
});
const sumPrice = computed(
() =>
carts.value
.filter((item) => item.chk) // 获取选中的
.reduce((pre, cur) => pre + cur.amount * cur.product.price, 0) // 求和
);
</script>
<template>
<div class="cart">
<label><input type="checkbox" v-model="chkAll" />全选</label>
<hr />
<ul>
<li v-for="item in carts">
<label><input type="checkbox" v-model="item.chk" />{{
item.product.name
}}</label>
<p>价格:{{ item.product.price }}</p>
<p>
数量:<button @click="item.amount > 1 ? item.amount-- : null">-</button>{{ item.amount }}<button
@click="item.amount++">+</button>
</p>
</li>
</ul>
<h5>总价:{{ sumPrice }}</h5>
</div>
</template>
<style scoped>
li {
list-style: none;
border-bottom: 1px solid saddlebrown;
padding: 8px;
}
</style>
安装Vant
#https://vant-contrib.gitee.io/vant/#/zh-CN
npm i vant
在 main.js
中引入样式
import { createApp } from 'vue'
import 'vant/lib/index.css'; // 引入vant的样式库
import './style.css'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
在页面中引入需要使用的vant组件
<script setup>
import { Button, Card } from 'vant'
</script>
<template>
<div class="home">
<Button>测试</Button>
<card num="2" price="2.00" desc="描述信息" title="商品标题"
thumb="https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg" />
</div>
</template>
<style scoped></style>
底部Tabbar组件
在 App.vue
中增加Tabbar组件
<script setup>
import { Tabbar, TabbarItem } from 'vant';
</script>
<template>
<router-view class="main"></router-view>
<tabbar route :fixed="false" active-color="deeppink">
<tabbar-item icon="home-o" :to="{name: 'Home'}">首页</tabbar-item>
<tabbar-item icon="search" :to="{name: 'List'}">热卖</tabbar-item>
<tabbar-item icon="shopping-cart-o" :to="{name: 'Cart'}">购物车</tabbar-item>
<tabbar-item icon="friends-o" :to="{name: 'User'}">我的</tabbar-item>
</tabbar>
</template>
<style scoped>
.main {
flex: 1;
overflow: auto;
}
</style>
调整整个页面布局 style.css
html,body,#app {
height: 100%;
}
#app {
display: flex;
flex-direction: column;
}
轮播图Swipe + 宫格Grid
<script setup>
import { Swipe, SwipeItem, Grid, GridItem } from 'vant'
</script>
<template>
<div class="home">
<swipe class="my-swipe" :autoplay="3000" indicator-color="white">
<swipe-item>1</swipe-item>
<swipe-item>2</swipe-item>
<swipe-item>3</swipe-item>
<swipe-item>4</swipe-item>
</swipe>
<grid :column-num="3">
<grid-item v-for="value in 6" :key="value" icon="photo-o" text="文字" />
</grid>
</div>
</template>
<style scoped>
.my-swipe .van-swipe-item {
color: #fff;
font-size: 20px;
line-height: 150px;
text-align: center;
background-color: #39a9ed;
}
</style>
侧边导航Sidebar
<script setup>
import { ref, computed } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import { Sidebar, SidebarItem } from 'vant'
import router from '../router';
import { useCategories } from '../hooks/use-categories'
const { categories } = useCategories()
const route = useRoute()
const currentCategoryId = ref(route.query.tid)
const active = computed({
set(v) {
return v
},
get() {
return categories.value.findIndex(item => item.id == currentCategoryId.value)
}
})
// onBeforeRouteUpdate 路由改变,但是组件复用的时候执行
onBeforeRouteUpdate((to, from) => {
console.log(to.query)
currentCategoryId.value = to.query.tid
})
</script>
<template>
<div class="list">
<sidebar v-model="active">
<sidebar-item v-for="item in categories" :key="item.id" :title="item.name"
:to="{ name: 'List', query: { tid: item.id } }" />
</sidebar>
</div>
</template>
<style scoped></style>
列表List
<script setup>
import { ref, computed } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import { Sidebar, SidebarItem, Card, List } from 'vant'
import { useCategories } from '../hooks/use-categories'
import { loadProductsAPI } from '../api/products'
import { dalImg } from '../utils/tools'
const page = ref(1); // 当前页码
const loading = ref(false); // 是否加载中
const finished = ref(false); // 是否加载完成
const { categories } = useCategories()
const route = useRoute()
const currentCategoryId = ref(route.query.tid)
const active = computed({
set(v) {
return v
},
get() {
return categories.value.findIndex(item => item.id == currentCategoryId.value)
}
})
const products = ref([])
const loadDataFromServer = () => {
loading.value = true
loadProductsAPI(page.value, currentCategoryId.value).then((res) => {
// 当页数大于总页数时,加载完成
finished.value = res.pages < page.value
loading.value = false
products.value.push(...res.data)
page.value++; // 加载数据成功之后也码+1
})
}
// onBeforeRouteUpdate 路由改变,但是组件复用的时候执行(切换左侧的产品分类)
onBeforeRouteUpdate((to, from) => {
// console.log(to.query)
currentCategoryId.value = to.query.tid
// 需要重置数据
finished.value = false
page.value = 1
products.value = []
loadDataFromServer()
})
</script>
<template>
<div class="list">
<sidebar v-model="active">
<sidebar-item v-for="item in categories" :key="item.id" :title="item.name"
:to="{ name: 'List', query: { tid: item.id } }" />
</sidebar>
<list class="product" :loading="loading" :finished="finished" finished-text="没有更多了"
@load="loadDataFromServer">
<card v-for="item in products" :num="item.amount" :price="item.price.toFixed(2)" :title="item.name"
:thumb="dalImg(item.coverImage)" />
</list>
</div>
</template>
<style scoped>
.list {
display: flex;
}
.list .van-sidebar {
width: 98px;
}
.product {
overflow: auto;
flex: 1;
}
</style>
动作栏ActionBar
<script setup>
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ActionBar, ActionBarIcon, ActionBarButton, NavBar } from 'vant'
import { loadProductByIdAPI } from '../api/products'
const route = useRoute()
const router = useRouter()
const proudct = ref({})
loadProductByIdAPI(route.query.id).then(res => proudct.value = res.data)
const onClickIcon = () => { }
const onClickButton = () => { }
const goHOme = () => {
router.push({ name: 'Home' })
}
const goBack = () => {
router.go(-1)
}
</script>
<template>
<div class="detail">
<nav-bar :title="proudct.name" left-text="返回" left-arrow @click-left="goBack" />
<h1>{{ proudct.name }}</h1>
<div class="content" v-html="proudct.content"></div>
<action-bar>
<action-bar-icon icon="chat-o" text="客服" @click="onClickIcon" />
<action-bar-icon icon="cart-o" text="购物车" @click="onClickIcon" />
<action-bar-icon icon="shop-o" text="店铺" @click="goHOme" />
<action-bar-button type="danger" text="立即购买" @click="onClickButton" />
</action-bar>
</div>
</template>
<style scoped></style>
是否需要隐藏底部Tabber
-
在路由中添加
meta
{ path: "/detail", name: "Detail", component: () => import("../views/Detail.vue"), meta: { needHideTabber: true, // 需要隐藏底部Tabber }, },
-
在
<script setup> import { ref, watch } from 'vue' import { useRoute } from 'vue-router'; import { Tabbar, TabbarItem } from 'vant';App.vue
中监听路由的变化const route = useRoute()
const isShowTabber = ref(true)// 监听路由的变化
</script> <template> <router-view class="main"></router-view> <tabbar route :fixed="false" active-color="deeppink" v-show="isShowTabber"> <tabbar-item icon="home-o" :to="{ name: 'Home' }">首页</tabbar-item> <tabbar-item icon="search" :to="{ name: 'List' }">热卖</tabbar-item> <tabbar-item icon="shopping-cart-o" :to="{ name: 'Cart' }">购物车</tabbar-item> <tabbar-item icon="friends-o" :to="{ name: 'User' }">我的</tabbar-item> </tabbar> </template> <style scoped> .main { flex: 1; overflow: auto; } </style>
watch(route, (v) => {
if (v.meta.needHideTabber) {
isShowTabber.value = false
} else {
isShowTabber.value = true
}
})
网络请求
npm i axios
// 进度条
npm i nprogress
#测试后台接口数据
#https://gitee.com/galen.zhang/vue3-demo/tree/master/honey-home-main/honey-home-server
cd honey-home-server
npm i # 安装依赖项
node app.js # 启动项目
http://localhost:1337
http://localhost:1337/api/products
http://localhost:1337/manager
#管理页面
封装axios使用 utils/request.js
import axios from "axios";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
export const serverUrl = "http://localhost:1337";
const service = axios.create({
baseURL: serverUrl,
timeout: 5000,
});
// Add a request interceptor 全局请求拦截
service.interceptors.request.use(
function (config) {
// Do something before request is sent
NProgress.start(); // 启动进度条
// 此处还可以设置token
return config;
},
function (error) {
// Do something with request error
return Promise.reject(error);
}
);
// Add a response interceptor 全局相应拦截
service.interceptors.response.use(
function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
NProgress.done();
// 如果是固定的数据返回模式,此处可以做继续完整的封装
return response.data;
},
function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
NProgress.done();
// 此处需要对返回的状态码或者异常信息作统一处理
return Promise.reject(error);
}
);
export const get = (url, params) => {
return service.get(url, {
params,
});
};
export const post = (url, data) => service.post(url, data);
export const put = (url, data) => service.put(url, data);
export const del = (url, data) => service.delete(url);
使用样例 api/categories.js
import { get } from '../utils/request';
/**
* 获取商品分类
* @returns
*/
export const loadCategoriesAPI = () => get('/api/v1/product_categories');
封装重复代码到hooks
创建文件 hooks/use-categories.js
import { ref } from "vue";
import { loadCategoriesAPI } from "../api/categories";
export const useCategories = () => {
const categories = ref([]);
loadCategoriesAPI().then((res) => (categories.value = res.data));
return {
categories,
};
};
使用方式
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { Card, Swipe, SwipeItem, Grid, GridItem } from 'vant'
import { loadBannersAPI } from '../api/banners'
import { useCategories } from '../hooks/use-categories'
import { useProducts } from '../hooks/use-products';
import { dalImg } from '../utils/tools'
const router = useRouter()
const { products, loadDataFromServer } = useProducts()
loadDataFromServer()
// 轮播图
const banners = ref([]);
loadBannersAPI().then((res) => (banners.value = res.data));
// 商品分类
const { categories } = useCategories()
const toDetail = (id) => {
router.push({ name: 'Detail', query: { id } })
}
</script>
<template>
<div class="home">
<swipe class="my-swipe" :autoplay="3000" indicator-color="white">
<swipe-item v-for="item in banners" :key="item.id">
<img :src="dalImg(item.coverImage)" :alt="item.name" />
</swipe-item>
</swipe>
<grid :column-num="4">
<grid-item v-for="item in categories" :key="item.id" :icon="dalImg(item.coverImage)" :text="item.name"
:to="{ name: 'List', query: { tid: item.id } }" />
</grid>
<card v-for="item in products" :num="item.amount" :price="item.price.toFixed(2)" :title="item.name"
:thumb="dalImg(item.coverImage)" @click-thumb="toDetail(item.id)" />
</div>
</template>
<style scoped>
</style>