Vue3.2基础及Vant的使用

Vue3文档

官方文档 https://cn.vuejs.org/

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 的修饰符:tirmnumber

	  <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 计算属性,当依赖的数据改变之后会重新计算一个结果

计算属性具有缓存功能

计算属性和方法调用的区别:

  1. 计算属性具有缓存功能,依赖的数据不改变不会重新计算

  2. 计算属性可以直接调用,不需要加括号

  3. 方法调用的时候需要加括号

  4. 方法在每一次组件更新的时候都会重新执行

    // 模拟购物中全选、取消选择,计算总价
    <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);
            });
          },
        }  

组件

自定义标签

  1. 局部组件,定义好之后需要先注册再使用
  2. 全局组件,定义好之后可以直接使用

组件传参

  1. 父传子,使用props属性

  2. 子传父,使用事件派发

  3. 非相关组件,使用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>
     <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>
    
    </body> </html>

组件生命周期函数

8个常见的生命周期钩子函数

  1. beforeCreate
  2. created:调用接口获取数据
  3. beforeMount
  4. mounted:获取dom元素
  5. beforeUpdate,重复执行
  6. updated,重复执行,在5和6两个钩子函数中不能修改数据,改了数据之后会引起死循环
  7. beforeUnmount
  8. unmounted

下面2个需要配合keep-alive使用。keep-alive可以对组件做缓存

  1. activated,激活
  2. deactivated,取消激活

嵌套组件的生命周期钩子函数

  1. 先执行到父组件的beforeMount之后,开始解析dom
  2. 当遇到子组件时,会执行所有子组件的创建到挂载完成
  3. 当所有的子组件都挂载完成之后,执行父组件的挂载完成

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种创建项目的方式

  1. 使用Vite创建项目

    npm init vite@latest

  2. 使用 npm create vue@latest

    npm init vite@latest
    vue-demo

    #文档
    https://cn.vuejs.org/guide/quick-start.html#creating-a-vue-application

    cd 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

  1. 在路由中添加 meta

     {
       path: "/detail",
       name: "Detail",
       component: () => import("../views/Detail.vue"),
       meta: {
         needHideTabber: true, // 需要隐藏底部Tabber
       },
     },
    
  2. App.vue 中监听路由的变化

    <script setup> import { ref, watch } from 'vue' import { useRoute } from 'vue-router'; import { Tabbar, TabbarItem } from 'vant';

    const route = useRoute()
    const isShowTabber = ref(true)

    // 监听路由的变化
    watch(route, (v) => {
    if (v.meta.needHideTabber) {
    isShowTabber.value = false
    } else {
    isShowTabber.value = 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>

网络请求

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>
相关推荐
小白学习日记35 分钟前
【复习】HTML常用标签<table>
前端·html
john_hjy39 分钟前
11. 异步编程
运维·服务器·javascript
风清扬_jd1 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
丁总学Java1 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele1 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
It'sMyGo1 小时前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式
懒羊羊大王呀1 小时前
CSS——属性值计算
前端·css
xgq2 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
李是啥也不会2 小时前
数组的概念
javascript
用户3157476081352 小时前
前端之路-了解原型和原型链
前端