活动公告

系统通知
05-18 21:22
系统通知
通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,资源失效请在帖子内回复要求补档,会尽快处理!
10-23 09:31

Vue3结合Vant UI构建高效移动端应用 从环境搭建到组件使用再到项目优化与部署的完整实战指南助您成为移动开发领域的专家

SunJu_FaceMall

3万

主题

2860

科技点

3万

积分

白金月票

碾压王

积分
32872

塔罗立华奏

<font color=白金月票" /> 发表于 2025-9-4 01:50:07 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
引言

在当今移动互联网时代,高效、美观、易用的移动应用成为企业的核心竞争力。Vue3作为目前最流行的前端框架之一,凭借其响应式系统、组合式API和优化的性能,为开发者提供了更强大的开发能力。而Vant UI则是一款专为移动端设计的轻量、可靠的UI组件库,它提供了丰富的组件和良好的用户体验。将Vue3与Vant UI结合使用,可以大大提高移动应用的开发效率和质量。本文将详细介绍如何从零开始,使用Vue3和Vant UI构建高效的移动端应用,涵盖环境搭建、组件使用、项目优化与部署的全过程,助您成为移动开发领域的专家。

环境搭建

Node.js安装

首先,我们需要安装Node.js,它是Vue项目运行的基础环境。Vue3要求Node.js版本在12.0.0或以上。

1. 访问Node.js官网下载最新的LTS版本。
2. 根据操作系统选择对应的安装包进行安装。
3. 安装完成后,在终端或命令提示符中运行以下命令验证安装:
  1. node -v
  2. npm -v
复制代码

如果显示版本号,则表示安装成功。

安装Vue CLI或Vite

Vue CLI是Vue的官方脚手架工具,而Vite是下一代前端构建工具,两者都可以用来创建Vue3项目。Vite具有更快的冷启动和热更新速度,推荐使用。
  1. npm create vite@latest my-vue-app -- --template vue
复制代码

首先安装Vue CLI:
  1. npm install -g @vue/cli
复制代码

然后创建项目:
  1. vue create my-vue-app
复制代码

在创建过程中,选择Vue3预设。

项目初始化

创建Vue3项目

我们以Vite为例创建Vue3项目:
  1. npm create vite@latest my-vue-app -- --template vue
  2. cd my-vue-app
  3. npm install
复制代码

安装和配置Vant UI

Vant UI可以通过npm或yarn安装:
  1. npm install vant
复制代码

引入Vant UI

Vant UI支持两种引入方式:完整引入和按需引入。

在main.js中引入:
  1. import { createApp } from 'vue'
  2. import App from './App.vue'
  3. import Vant from 'vant'
  4. import 'vant/lib/index.css'
  5. const app = createApp(App)
  6. app.use(Vant)
  7. app.mount('#app')
复制代码

按需引入可以减少打包体积,提高应用加载速度。

首先安装插件:
  1. npm install unplugin-vue-components unplugin-auto-import -D
复制代码

然后在vite.config.js中配置:
  1. import { defineConfig } from 'vite'
  2. import vue from '@vitejs/plugin-vue'
  3. import Components from 'unplugin-vue-components/vite'
  4. import { VantResolver } from 'unplugin-vue-components/resolvers'
  5. export default defineConfig({
  6.   plugins: [
  7.     vue(),
  8.     Components({
  9.       resolvers: [VantResolver()],
  10.     }),
  11.   ],
  12. })
复制代码

这样配置后,可以直接在模板中使用Vant组件,无需手动引入。

项目结构说明

一个典型的Vue3+Vant项目结构如下:
  1. my-vue-app/
  2. ├── public/             # 静态资源
  3. ├── src/                # 源代码
  4. │   ├── assets/         # 项目资源
  5. │   ├── components/     # 公共组件
  6. │   ├── views/          # 页面组件
  7. │   ├── router/         # 路由配置
  8. │   ├── store/          # 状态管理
  9. │   ├── utils/          # 工具函数
  10. │   ├── App.vue         # 根组件
  11. │   └── main.js         # 入口文件
  12. ├── .gitignore          # Git忽略文件
  13. ├── index.html          # HTML模板
  14. ├── package.json        # 项目配置
  15. ├── vite.config.js      # Vite配置
  16. └── README.md           # 项目说明
复制代码

基础配置

在vite.config.js中,我们可以进行一些基础配置:
  1. import { defineConfig } from 'vite'
  2. import vue from '@vitejs/plugin-vue'
  3. import { resolve } from 'path'
  4. import Components from 'unplugin-vue-components/vite'
  5. import { VantResolver } from 'unplugin-vue-components/resolvers'
  6. export default defineConfig({
  7.   plugins: [
  8.     vue(),
  9.     Components({
  10.       resolvers: [VantResolver()],
  11.     }),
  12.   ],
  13.   resolve: {
  14.     alias: {
  15.       '@': resolve(__dirname, 'src'),
  16.     },
  17.   },
  18.   server: {
  19.     port: 3000,
  20.     open: true,
  21.     proxy: {
  22.       '/api': {
  23.         target: 'http://localhost:8080',
  24.         changeOrigin: true,
  25.         rewrite: (path) => path.replace(/^\/api/, ''),
  26.       },
  27.     },
  28.   },
  29. })
复制代码

基础组件使用

Vant UI提供了丰富的组件,下面我们介绍一些常用组件的使用方法。

布局组件

Vant提供了Row和Col组件来实现栅格布局:
  1. <template>
  2.   <van-row>
  3.     <van-col span="8">span: 8</van-col>
  4.     <van-col span="8">span: 8</van-col>
  5.     <van-col span="8">span: 8</van-col>
  6.   </van-row>
  7.   
  8.   <van-row>
  9.     <van-col span="12">span: 12</van-col>
  10.     <van-col span="12">span: 12</van-col>
  11.   </van-row>
  12.   
  13.   <van-row gutter="20">
  14.     <van-col span="8">span: 8</van-col>
  15.     <van-col span="8">span: 8</van-col>
  16.     <van-col span="8">span: 8</van-col>
  17.   </van-row>
  18. </template>
复制代码

Vant提供了Flex组件来实现弹性布局:
  1. <template>
  2.   <van-flex>
  3.     <div class="flex-item">1</div>
  4.     <div class="flex-item">2</div>
  5.     <div class="flex-item">3</div>
  6.   </van-flex>
  7.   
  8.   <van-flex justify="space-between">
  9.     <div class="flex-item">1</div>
  10.     <div class="flex-item">2</div>
  11.     <div class="flex-item">3</div>
  12.   </van-flex>
  13. </template>
  14. <style>
  15. .flex-item {
  16.   width: 50px;
  17.   height: 50px;
  18.   background-color: #f2f6fc;
  19.   text-align: center;
  20.   line-height: 50px;
  21. }
  22. </style>
复制代码

表单组件
  1. <template>
  2.   <van-button type="primary">主要按钮</van-button>
  3.   <van-button type="success">成功按钮</van-button>
  4.   <van-button type="default">默认按钮</van-button>
  5.   <van-button type="warning">警告按钮</van-button>
  6.   <van-button type="danger">危险按钮</van-button>
  7.   
  8.   <van-button plain type="primary">朴素按钮</van-button>
  9.   <van-button disabled type="primary">禁用按钮</van-button>
  10.   <van-button loading type="primary">加载中</van-button>
  11.   
  12.   <van-button square type="primary">方形按钮</van-button>
  13.   <van-button round type="primary">圆形按钮</van-button>
  14.   
  15.   <van-block>
  16.     <van-button type="primary" block>块级按钮</van-button>
  17.   </van-block>
  18.   
  19.   <van-button type="primary" size="large">大号按钮</van-button>
  20.   <van-button type="primary" size="normal">普通按钮</van-button>
  21.   <van-button type="primary" size="small">小型按钮</van-button>
  22.   <van-button type="primary" size="mini">迷你按钮</van-button>
  23. </template>
复制代码
  1. <template>
  2.   <van-field
  3.     v-model="value"
  4.     label="文本"
  5.     placeholder="请输入文本"
  6.   />
  7.   
  8.   <van-field
  9.     v-model="value2"
  10.     label="密码"
  11.     type="password"
  12.     placeholder="请输入密码"
  13.   />
  14.   
  15.   <van-field
  16.     v-model="value3"
  17.     label="手机号"
  18.     type="tel"
  19.     placeholder="请输入手机号"
  20.   />
  21.   
  22.   <van-field
  23.     v-model="value4"
  24.     label="数字"
  25.     type="number"
  26.     placeholder="请输入数字"
  27.   />
  28.   
  29.   <van-field
  30.     v-model="value5"
  31.     label="短信验证码"
  32.     center
  33.     placeholder="请输入短信验证码"
  34.   >
  35.     <template #button>
  36.       <van-button size="small" type="primary">发送验证码</van-button>
  37.     </template>
  38.   </van-field>
  39.   
  40.   <van-field
  41.     v-model="value6"
  42.     label="留言"
  43.     type="textarea"
  44.     placeholder="请输入留言"
  45.     rows="2"
  46.     autosize
  47.   />
  48. </template>
  49. <script>
  50. import { ref } from 'vue';
  51. export default {
  52.   setup() {
  53.     const value = ref('');
  54.     const value2 = ref('');
  55.     const value3 = ref('');
  56.     const value4 = ref('');
  57.     const value5 = ref('');
  58.     const value6 = ref('');
  59.    
  60.     return {
  61.       value,
  62.       value2,
  63.       value3,
  64.       value4,
  65.       value5,
  66.       value6
  67.     };
  68.   }
  69. }
  70. </script>
复制代码
  1. <template>
  2.   <van-radio-group v-model="radio" direction="horizontal">
  3.     <van-radio name="1">单选框 1</van-radio>
  4.     <van-radio name="2">单选框 2</van-radio>
  5.   </van-radio-group>
  6.   
  7.   <van-radio-group v-model="radio2">
  8.     <van-cell-group inset>
  9.       <van-cell title="单选框 1" clickable @click="radio2 = '1'">
  10.         <template #right-icon>
  11.           <van-radio name="1" />
  12.         </template>
  13.       </van-cell>
  14.       <van-cell title="单选框 2" clickable @click="radio2 = '2'">
  15.         <template #right-icon>
  16.           <van-radio name="2" />
  17.         </template>
  18.       </van-cell>
  19.     </van-cell-group>
  20.   </van-radio-group>
  21. </template>
  22. <script>
  23. import { ref } from 'vue';
  24. export default {
  25.   setup() {
  26.     const radio = ref('1');
  27.     const radio2 = ref('1');
  28.    
  29.     return {
  30.       radio,
  31.       radio2
  32.     };
  33.   }
  34. }
  35. </script>
复制代码
  1. <template>
  2.   <van-checkbox-group v-model="checked" direction="horizontal">
  3.     <van-checkbox name="a">复选框 a</van-checkbox>
  4.     <van-checkbox name="b">复选框 b</van-checkbox>
  5.     <van-checkbox name="c">复选框 c</van-checkbox>
  6.   </van-checkbox-group>
  7.   
  8.   <van-checkbox-group v-model="checked2">
  9.     <van-cell-group inset>
  10.       <van-cell
  11.         v-for="(item, index) in list"
  12.         clickable
  13.         :key="item"
  14.         :title="`复选框 ${item}`"
  15.         @click="toggle(index)"
  16.       >
  17.         <template #right-icon>
  18.           <van-checkbox :name="item" ref="checkboxes" />
  19.         </template>
  20.       </van-cell>
  21.     </van-cell-group>
  22.   </van-checkbox-group>
  23. </template>
  24. <script>
  25. import { ref } from 'vue';
  26. export default {
  27.   setup() {
  28.     const checked = ref(['a', 'b']);
  29.     const checked2 = ref([]);
  30.     const list = ['a', 'b', 'c'];
  31.     const checkboxes = ref([]);
  32.    
  33.     const toggle = (index) => {
  34.       checkboxes.value[index].toggle();
  35.     };
  36.    
  37.     return {
  38.       checked,
  39.       checked2,
  40.       list,
  41.       checkboxes,
  42.       toggle
  43.     };
  44.   }
  45. }
  46. </script>
复制代码
  1. <template>
  2.   <van-form @submit="onSubmit">
  3.     <van-cell-group inset>
  4.       <van-field
  5.         v-model="username"
  6.         name="username"
  7.         label="用户名"
  8.         placeholder="用户名"
  9.         :rules="[{ required: true, message: '请填写用户名' }]"
  10.       />
  11.       <van-field
  12.         v-model="password"
  13.         type="password"
  14.         name="password"
  15.         label="密码"
  16.         placeholder="密码"
  17.         :rules="[{ required: true, message: '请填写密码' }]"
  18.       />
  19.     </van-cell-group>
  20.     <div style="margin: 16px;">
  21.       <van-button round block type="primary" native-type="submit">
  22.         提交
  23.       </van-button>
  24.     </div>
  25.   </van-form>
  26. </template>
  27. <script>
  28. import { ref } from 'vue';
  29. import { showToast } from 'vant';
  30. export default {
  31.   setup() {
  32.     const username = ref('');
  33.     const password = ref('');
  34.    
  35.     const onSubmit = (values) => {
  36.       console.log('form submit', values);
  37.       showToast('提交成功');
  38.     };
  39.    
  40.     return {
  41.       username,
  42.       password,
  43.       onSubmit,
  44.     };
  45.   },
  46. };
  47. </script>
复制代码

反馈组件
  1. <template>
  2.   <van-button type="primary" @click="showToast">成功提示</van-button>
  3.   <van-button type="primary" @click="showLoadingToast">加载提示</van-button>
  4.   <van-button type="primary" @click="showFailToast">失败提示</van-button>
  5. </template>
  6. <script>
  7. import { showToast, showLoadingToast, showFailToast } from 'vant';
  8. export default {
  9.   setup() {
  10.     const showToast = () => {
  11.       showToast('成功提示');
  12.     };
  13.    
  14.     const showLoadingToast = () => {
  15.       showLoadingToast({
  16.         message: '加载中...',
  17.         forbidClick: true,
  18.       });
  19.     };
  20.    
  21.     const showFailToast = () => {
  22.       showFailToast('失败提示');
  23.     };
  24.    
  25.     return {
  26.       showToast,
  27.       showLoadingToast,
  28.       showFailToast
  29.     };
  30.   }
  31. }
  32. </script>
复制代码
  1. <template>
  2.   <van-button type="primary" @click="showDialog">提示弹窗</van-button>
  3.   <van-button type="primary" @click="showConfirmDialog">确认弹窗</van-button>
  4.   <van-button type="primary" @click="showAsyncCloseDialog">异步关闭</van-button>
  5. </template>
  6. <script>
  7. import { showDialog, showConfirmDialog } from 'vant';
  8. export default {
  9.   setup() {
  10.     const showDialog = () => {
  11.       showDialog({
  12.         title: '标题',
  13.         message: '这是一个提示弹窗',
  14.       }).then(() => {
  15.         // on close
  16.       });
  17.     };
  18.    
  19.     const showConfirmDialog = () => {
  20.       showConfirmDialog({
  21.         title: '标题',
  22.         message: '这是一个确认弹窗',
  23.       })
  24.         .then(() => {
  25.           // on confirm
  26.           console.log('确认');
  27.         })
  28.         .catch(() => {
  29.           // on cancel
  30.           console.log('取消');
  31.         });
  32.     };
  33.    
  34.     const showAsyncCloseDialog = () => {
  35.       showDialog({
  36.         title: '标题',
  37.         message: '弹窗内容',
  38.         beforeClose: (action, done) => {
  39.           if (action === 'confirm') {
  40.             setTimeout(done, 1000);
  41.           } else {
  42.             done();
  43.           }
  44.         },
  45.       });
  46.     };
  47.    
  48.     return {
  49.       showDialog,
  50.       showConfirmDialog,
  51.       showAsyncCloseDialog
  52.     };
  53.   }
  54. }
  55. </script>
复制代码

展示组件
  1. <template>
  2.   <van-cell-group>
  3.     <van-cell title="单元格" value="内容" />
  4.     <van-cell title="单元格" value="内容" label="描述信息" />
  5.   </van-cell-group>
  6.   
  7.   <van-cell-group inset>
  8.     <van-cell title="单元格" value="内容" />
  9.     <van-cell title="单元格" value="内容" label="描述信息" />
  10.   </van-cell-group>
  11.   
  12.   <van-cell-group>
  13.     <van-cell title="单元格" is-link />
  14.     <van-cell title="单元格" is-link value="内容" />
  15.     <van-cell title="单元格" is-link value="内容" label="描述信息" />
  16.   </van-cell-group>
  17.   
  18.   <van-cell-group>
  19.     <van-cell title="标题" icon="location-o" />
  20.     <van-cell title="标题" icon="location-o" value="内容" />
  21.     <van-cell title="标题" icon="location-o" value="内容" label="描述信息" is-link />
  22.   </van-cell-group>
  23. </template>
复制代码
  1. <template>
  2.   <van-image
  3.     width="100"
  4.     height="100"
  5.     src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
  6.   />
  7.   
  8.   <van-image
  9.     round
  10.     width="100"
  11.     height="100"
  12.     src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
  13.   />
  14.   
  15.   <van-image
  16.     width="100"
  17.     height="100"
  18.     fit="contain"
  19.     src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
  20.   />
  21.   
  22.   <van-image
  23.     width="100"
  24.     height="100"
  25.     src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
  26.     @click="previewImage"
  27.   />
  28.   
  29.   <van-image
  30.     width="100"
  31.     height="100"
  32.     src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
  33.     lazy-load
  34.   />
  35. </template>
  36. <script>
  37. import { showImagePreview } from 'vant';
  38. export default {
  39.   setup() {
  40.     const previewImage = () => {
  41.       showImagePreview([
  42.         'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg',
  43.       ]);
  44.     };
  45.    
  46.     return {
  47.       previewImage
  48.     };
  49.   }
  50. }
  51. </script>
复制代码
  1. <template>
  2.   <van-list
  3.     v-model:loading="loading"
  4.     :finished="finished"
  5.     finished-text="没有更多了"
  6.     @load="onLoad"
  7.   >
  8.     <van-cell v-for="item in list" :key="item" :title="item" />
  9.   </van-list>
  10. </template>
  11. <script>
  12. import { ref } from 'vue';
  13. export default {
  14.   setup() {
  15.     const list = ref([]);
  16.     const loading = ref(false);
  17.     const finished = ref(false);
  18.    
  19.     const onLoad = () => {
  20.       // 异步更新数据
  21.       // setTimeout 仅做示例,真实场景中一般为 ajax 请求
  22.       setTimeout(() => {
  23.         for (let i = 0; i < 10; i++) {
  24.           list.value.push(list.value.length + 1);
  25.         }
  26.         // 加载状态结束
  27.         loading.value = false;
  28.         
  29.         // 数据全部加载完成
  30.         if (list.value.length >= 40) {
  31.           finished.value = true;
  32.         }
  33.       }, 1000);
  34.     };
  35.    
  36.     return {
  37.       list,
  38.       loading,
  39.       finished,
  40.       onLoad,
  41.     };
  42.   },
  43. };
  44. </script>
复制代码

导航组件
  1. <template>
  2.   <van-tabs v-model:active="active">
  3.     <van-tab title="标签 1">内容 1</van-tab>
  4.     <van-tab title="标签 2">内容 2</van-tab>
  5.     <van-tab title="标签 3">内容 3</van-tab>
  6.     <van-tab title="标签 4">内容 4</van-tab>
  7.   </van-tabs>
  8.   
  9.   <van-tabs v-model:active="active2" type="card">
  10.     <van-tab title="标签 1">内容 1</van-tab>
  11.     <van-tab title="标签 2">内容 2</van-tab>
  12.     <van-tab title="标签 3">内容 3</van-tab>
  13.   </van-tabs>
  14.   
  15.   <van-tabs v-model:active="active3" swipeable>
  16.     <van-tab v-for="index in 4" :title="'标签 ' + index" :key="index">
  17.       内容 {{ index }}
  18.     </van-tab>
  19.   </van-tabs>
  20. </template>
  21. <script>
  22. import { ref } from 'vue';
  23. export default {
  24.   setup() {
  25.     const active = ref(0);
  26.     const active2 = ref(0);
  27.     const active3 = ref(0);
  28.    
  29.     return {
  30.       active,
  31.       active2,
  32.       active3
  33.     };
  34.   }
  35. }
  36. </script>
复制代码
  1. <template>
  2.   <van-nav-bar
  3.     title="标题"
  4.     left-text="返回"
  5.     right-text="按钮"
  6.     left-arrow
  7.     @click-left="onClickLeft"
  8.     @click-right="onClickRight"
  9.   />
  10.   
  11.   <van-nav-bar
  12.     title="标题"
  13.     left-text="返回"
  14.     left-arrow
  15.   >
  16.     <template #right>
  17.       <van-icon name="search" size="18" />
  18.     </template>
  19.   </van-nav-bar>
  20. </template>
  21. <script>
  22. import { showToast } from 'vant';
  23. export default {
  24.   setup() {
  25.     const onClickLeft = () => showToast('返回');
  26.     const onClickRight = () => showToast('按钮');
  27.    
  28.     return {
  29.       onClickLeft,
  30.       onClickRight
  31.     };
  32.   }
  33. }
  34. </script>
复制代码
  1. <template>
  2.   <div class="content">
  3.     <router-view />
  4.   </div>
  5.   
  6.   <van-tabbar v-model="active">
  7.     <van-tabbar-item icon="home-o">标签</van-tabbar-item>
  8.     <van-tabbar-item icon="search">标签</van-tabbar-item>
  9.     <van-tabbar-item icon="friends-o">标签</van-tabbar-item>
  10.     <van-tabbar-item icon="setting-o">标签</van-tabbar-item>
  11.   </van-tabbar>
  12. </template>
  13. <script>
  14. import { ref } from 'vue';
  15. export default {
  16.   setup() {
  17.     const active = ref(0);
  18.     return { active };
  19.   },
  20. };
  21. </script>
  22. <style>
  23. .content {
  24.   padding-bottom: 50px;
  25. }
  26. </style>
复制代码

高级组件与自定义

复杂组件的使用
  1. <template>
  2.   <van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
  3.     <van-swipe-item>1</van-swipe-item>
  4.     <van-swipe-item>2</van-swipe-item>
  5.     <van-swipe-item>3</van-swipe-item>
  6.     <van-swipe-item>4</van-swipe-item>
  7.   </van-swipe>
  8.   
  9.   <van-swipe :loop="false" class="my-swipe-2">
  10.     <van-swipe-item>1</van-swipe-item>
  11.     <van-swipe-item>2</van-swipe-item>
  12.     <van-swipe-item>3</van-swipe-item>
  13.     <van-swipe-item>4</van-swipe-item>
  14.   </van-swipe>
  15.   
  16.   <van-swipe class="my-swipe-3" :autoplay="3000">
  17.     <van-swipe-item v-for="(image, index) in images" :key="index">
  18.       <img :src="image" />
  19.     </van-swipe-item>
  20.   </van-swipe>
  21. </template>
  22. <script>
  23. import { ref } from 'vue';
  24. export default {
  25.   setup() {
  26.     const images = [
  27.       'https://fastly.jsdelivr.net/npm/@vant/assets/apple-1.jpeg',
  28.       'https://fastly.jsdelivr.net/npm/@vant/assets/apple-2.jpeg',
  29.     ];
  30.    
  31.     return {
  32.       images
  33.     };
  34.   }
  35. }
  36. </script>
  37. <style>
  38. .my-swipe .van-swipe-item {
  39.   color: #fff;
  40.   font-size: 20px;
  41.   line-height: 150px;
  42.   text-align: center;
  43.   background-color: #39a9ed;
  44. }
  45. .my-swipe-2 .van-swipe-item {
  46.   color: #fff;
  47.   font-size: 20px;
  48.   line-height: 150px;
  49.   text-align: center;
  50.   background-color: #39a9ed;
  51. }
  52. .my-swipe-3 .van-swipe-item {
  53.   display: flex;
  54.   justify-content: center;
  55.   align-items: center;
  56.   height: 200px;
  57. }
  58. .my-swipe-3 img {
  59.   width: 100%;
  60.   height: 100%;
  61.   object-fit: cover;
  62. }
  63. </style>
复制代码
  1. <template>
  2.   <van-grid :column-num="3">
  3.     <van-grid-item icon="photo-o" text="文字" />
  4.     <van-grid-item icon="photo-o" text="文字" />
  5.     <van-grid-item icon="photo-o" text="文字" />
  6.     <van-grid-item icon="photo-o" text="文字" />
  7.     <van-grid-item icon="photo-o" text="文字" />
  8.     <van-grid-item icon="photo-o" text="文字" />
  9.   </van-grid>
  10.   
  11.   <van-grid :border="false" :column-num="4">
  12.     <van-grid-item>
  13.       <van-image src="https://fastly.jsdelivr.net/npm/@vant/assets/apple-1.jpeg" />
  14.       <span style="margin-top: 8px">自定义内容</span>
  15.     </van-grid-item>
  16.     <van-grid-item>
  17.       <van-image src="https://fastly.jsdelivr.net/npm/@vant/assets/apple-2.jpeg" />
  18.       <span style="margin-top: 8px">自定义内容</span>
  19.     </van-grid-item>
  20.     <van-grid-item>
  21.       <van-image src="https://fastly.jsdelivr.net/npm/@vant/assets/apple-3.jpeg" />
  22.       <span style="margin-top: 8px">自定义内容</span>
  23.     </van-grid-item>
  24.     <van-grid-item>
  25.       <van-image src="https://fastly.jsdelivr.net/npm/@vant/assets/apple-4.jpeg" />
  26.       <span style="margin-top: 8px">自定义内容</span>
  27.     </van-grid-item>
  28.   </van-grid>
  29.   
  30.   <van-grid :gutter="10" :column-num="3">
  31.     <van-grid-item v-for="value in 6" :key="value" icon="photo-o" text="文字" />
  32.   </van-grid>
  33.   
  34.   <van-grid direction="horizontal" :column-num="3">
  35.     <van-grid-item icon="photo-o" text="文字" />
  36.     <van-grid-item icon="photo-o" text="文字" />
  37.     <van-grid-item icon="photo-o" text="文字" />
  38.   </van-grid>
  39. </template>
复制代码
  1. <template>
  2.   <van-collapse v-model="activeNames">
  3.     <van-collapse-item title="标题1" name="1">
  4.       内容1
  5.     </van-collapse-item>
  6.     <van-collapse-item title="标题2" name="2">
  7.       内容2
  8.     </van-collapse-item>
  9.     <van-collapse-item title="标题3" name="3" disabled>
  10.       内容3(禁用)
  11.     </van-collapse-item>
  12.   </van-collapse>
  13.   
  14.   <van-collapse v-model="activeNames2" accordion>
  15.     <van-collapse-item title="标题1" name="1">
  16.       内容1
  17.     </van-collapse-item>
  18.     <van-collapse-item title="标题2" name="2">
  19.       内容2
  20.     </van-collapse-item>
  21.     <van-collapse-item title="标题3" name="3">
  22.       内容3
  23.     </van-collapse-item>
  24.   </van-collapse>
  25. </template>
  26. <script>
  27. import { ref } from 'vue';
  28. export default {
  29.   setup() {
  30.     const activeNames = ref(['1']);
  31.     const activeNames2 = ref('1');
  32.    
  33.     return {
  34.       activeNames,
  35.       activeNames2
  36.     };
  37.   }
  38. }
  39. </script>
复制代码

自定义主题

Vant UI支持通过CSS变量覆盖主题样式。在App.vue或全局CSS文件中定义:
  1. :root {
  2.   --van-primary-color: #07c160;
  3.   --van-success-color: #07c160;
  4.   --van-danger-color: #ee0a24;
  5.   --van-warning-color: #ff976a;
  6.   --van-text-color: #323233;
  7.   --van-text-color-2: #666;
  8.   --van-text-color-3: #969799;
  9.   --van-active-color: #f2f3f5;
  10.   --van-background-color: #f7f8fa;
  11.   --van-background-color-light: #fafafa;
  12.   --van-white: #fff;
  13. }
复制代码

如果需要更深入的自定义,可以通过修改源码或使用less/sass变量来实现。

自定义组件开发

下面是一个自定义组件的示例,创建一个ProductCard组件:
  1. <!-- src/components/ProductCard.vue -->
  2. <template>
  3.   <div class="product-card" @click="onClick">
  4.     <div class="product-card__img">
  5.       <van-image :src="product.image" fit="cover" />
  6.     </div>
  7.     <div class="product-card__content">
  8.       <div class="product-card__title">{{ product.title }}</div>
  9.       <div class="product-card__desc">{{ product.desc }}</div>
  10.       <div class="product-card__price">
  11.         <span class="product-card__price--current">¥{{ product.price }}</span>
  12.         <span class="product-card__price--origin" v-if="product.originPrice">¥{{ product.originPrice }}</span>
  13.       </div>
  14.     </div>
  15.   </div>
  16. </template>
  17. <script>
  18. import { defineProps, defineEmits } from 'vue';
  19. export default {
  20.   name: 'ProductCard',
  21.   props: {
  22.     product: {
  23.       type: Object,
  24.       required: true,
  25.       default: () => ({
  26.         id: '',
  27.         image: '',
  28.         title: '',
  29.         desc: '',
  30.         price: 0,
  31.         originPrice: 0
  32.       })
  33.     }
  34.   },
  35.   emits: ['click'],
  36.   setup(props, { emit }) {
  37.     const onClick = () => {
  38.       emit('click', props.product);
  39.     };
  40.    
  41.     return {
  42.       onClick
  43.     };
  44.   }
  45. }
  46. </script>
  47. <style scoped>
  48. .product-card {
  49.   background: #fff;
  50.   border-radius: 8px;
  51.   overflow: hidden;
  52.   margin-bottom: 12px;
  53.   box-shadow: 0 2px 12px rgba(100, 101, 102, 0.08);
  54. }
  55. .product-card__img {
  56.   height: 180px;
  57. }
  58. .product-card__content {
  59.   padding: 12px;
  60. }
  61. .product-card__title {
  62.   font-size: 14px;
  63.   font-weight: 500;
  64.   color: #323233;
  65.   margin-bottom: 4px;
  66.   overflow: hidden;
  67.   text-overflow: ellipsis;
  68.   display: -webkit-box;
  69.   -webkit-line-clamp: 2;
  70.   -webkit-box-orient: vertical;
  71. }
  72. .product-card__desc {
  73.   font-size: 12px;
  74.   color: #969799;
  75.   margin-bottom: 8px;
  76.   overflow: hidden;
  77.   text-overflow: ellipsis;
  78.   display: -webkit-box;
  79.   -webkit-line-clamp: 1;
  80.   -webkit-box-orient: vertical;
  81. }
  82. .product-card__price {
  83.   display: flex;
  84.   align-items: baseline;
  85. }
  86. .product-card__price--current {
  87.   font-size: 16px;
  88.   font-weight: 500;
  89.   color: #ee0a24;
  90. }
  91. .product-card__price--origin {
  92.   font-size: 12px;
  93.   color: #969799;
  94.   text-decoration: line-through;
  95.   margin-left: 4px;
  96. }
  97. </style>
复制代码

使用自定义组件:
  1. <template>
  2.   <div class="product-list">
  3.     <product-card
  4.       v-for="product in products"
  5.       :key="product.id"
  6.       :product="product"
  7.       @click="onProductClick"
  8.     />
  9.   </div>
  10. </template>
  11. <script>
  12. import { ref } from 'vue';
  13. import ProductCard from '@/components/ProductCard.vue';
  14. import { showToast } from 'vant';
  15. export default {
  16.   components: {
  17.     ProductCard
  18.   },
  19.   setup() {
  20.     const products = ref([
  21.       {
  22.         id: '1',
  23.         image: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-1.jpeg',
  24.         title: '高品质苹果',
  25.         desc: '新鲜采摘,口感脆甜',
  26.         price: 6.8,
  27.         originPrice: 8.9
  28.       },
  29.       {
  30.         id: '2',
  31.         image: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-2.jpeg',
  32.         title: '进口红富士',
  33.         desc: '产地直供,品质保证',
  34.         price: 12.8,
  35.         originPrice: 15.9
  36.       },
  37.       {
  38.         id: '3',
  39.         image: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-3.jpeg',
  40.         title: '有机青苹果',
  41.         desc: '无农药,健康食用',
  42.         price: 9.9,
  43.         originPrice: 12.9
  44.       }
  45.     ]);
  46.    
  47.     const onProductClick = (product) => {
  48.       showToast(`点击了商品: ${product.title}`);
  49.     };
  50.    
  51.     return {
  52.       products,
  53.       onProductClick
  54.     };
  55.   }
  56. }
  57. </script>
  58. <style>
  59. .product-list {
  60.   padding: 12px;
  61. }
  62. </style>
复制代码

组件封装技巧

在实际开发中,我们经常需要对一些常用功能进行封装,下面是一个封装表单组件的示例:
  1. <!-- src/components/BaseForm.vue -->
  2. <template>
  3.   <van-form @submit="onSubmit">
  4.     <van-cell-group inset>
  5.       <slot></slot>
  6.     </van-cell-group>
  7.     <div style="margin: 16px;">
  8.       <van-button round block type="primary" native-type="submit">
  9.         {{ submitText }}
  10.       </van-button>
  11.     </div>
  12.   </van-form>
  13. </template>
  14. <script>
  15. import { defineProps, defineEmits } from 'vue';
  16. export default {
  17.   name: 'BaseForm',
  18.   props: {
  19.     submitText: {
  20.       type: String,
  21.       default: '提交'
  22.     }
  23.   },
  24.   emits: ['submit'],
  25.   setup(props, { emit }) {
  26.     const onSubmit = (values) => {
  27.       emit('submit', values);
  28.     };
  29.    
  30.     return {
  31.       onSubmit
  32.     };
  33.   }
  34. }
  35. </script>
复制代码

使用封装的表单组件:
  1. <template>
  2.   <base-form submit-text="登录" @submit="onLogin">
  3.     <van-field
  4.       v-model="username"
  5.       name="username"
  6.       label="用户名"
  7.       placeholder="请输入用户名"
  8.       :rules="[{ required: true, message: '请填写用户名' }]"
  9.     />
  10.     <van-field
  11.       v-model="password"
  12.       type="password"
  13.       name="password"
  14.       label="密码"
  15.       placeholder="请输入密码"
  16.       :rules="[{ required: true, message: '请填写密码' }]"
  17.     />
  18.   </base-form>
  19. </template>
  20. <script>
  21. import { ref } from 'vue';
  22. import BaseForm from '@/components/BaseForm.vue';
  23. import { showToast } from 'vant';
  24. export default {
  25.   components: {
  26.     BaseForm
  27.   },
  28.   setup() {
  29.     const username = ref('');
  30.     const password = ref('');
  31.    
  32.     const onLogin = (values) => {
  33.       console.log('login form:', values);
  34.       showToast('登录成功');
  35.     };
  36.    
  37.     return {
  38.       username,
  39.       password,
  40.       onLogin
  41.     };
  42.   }
  43. }
  44. </script>
复制代码

状态管理

Pinia的安装与配置

Pinia是Vue3官方推荐的状态管理库,它比Vuex更轻量、更直观。

安装Pinia:
  1. npm install pinia
复制代码

在main.js中引入:
  1. import { createApp } from 'vue'
  2. import { createPinia } from 'pinia'
  3. import App from './App.vue'
  4. const app = createApp(App)
  5. app.use(createPinia())
  6. app.mount('#app')
复制代码

状态定义与使用

创建一个store:
  1. // src/store/counter.js
  2. import { defineStore } from 'pinia'
  3. export const useCounterStore = defineStore('counter', {
  4.   state: () => ({
  5.     count: 0,
  6.   }),
  7.   getters: {
  8.     doubleCount: (state) => state.count * 2,
  9.   },
  10.   actions: {
  11.     increment() {
  12.       this.count++
  13.     },
  14.     decrement() {
  15.       this.count--
  16.     },
  17.     reset() {
  18.       this.count = 0
  19.     },
  20.   },
  21. })
复制代码

在组件中使用store:
  1. <template>
  2.   <div>
  3.     <h2>计数器: {{ count }}</h2>
  4.     <h2>双倍计数: {{ doubleCount }}</h2>
  5.     <button @click="increment">增加</button>
  6.     <button @click="decrement">减少</button>
  7.     <button @click="reset">重置</button>
  8.   </div>
  9. </template>
  10. <script>
  11. import { storeToRefs } from 'pinia';
  12. import { useCounterStore } from '@/store/counter';
  13. export default {
  14.   setup() {
  15.     const counterStore = useCounterStore();
  16.     // 使用storeToRefs解构state和getters,保持响应性
  17.     const { count, doubleCount } = storeToRefs(counterStore);
  18.     // 直接解构actions
  19.     const { increment, decrement, reset } = counterStore;
  20.    
  21.     return {
  22.       count,
  23.       doubleCount,
  24.       increment,
  25.       decrement,
  26.       reset
  27.     };
  28.   }
  29. }
  30. </script>
复制代码

异步操作

在Pinia中处理异步操作:
  1. // src/store/user.js
  2. import { defineStore } from 'pinia'
  3. import { login as userLogin, getUserInfo } from '@/api/user'
  4. export const useUserStore = defineStore('user', {
  5.   state: () => ({
  6.     token: '',
  7.     userInfo: null,
  8.   }),
  9.   getters: {
  10.     isLogin: (state) => !!state.token,
  11.     userName: (state) => state.userInfo?.name || '',
  12.   },
  13.   actions: {
  14.     // 登录
  15.     async login(loginForm) {
  16.       try {
  17.         const { token } = await userLogin(loginForm)
  18.         this.token = token
  19.         return Promise.resolve()
  20.       } catch (error) {
  21.         return Promise.reject(error)
  22.       }
  23.     },
  24.    
  25.     // 获取用户信息
  26.     async fetchUserInfo() {
  27.       try {
  28.         const userInfo = await getUserInfo()
  29.         this.userInfo = userInfo
  30.         return Promise.resolve(userInfo)
  31.       } catch (error) {
  32.         return Promise.reject(error)
  33.       }
  34.     },
  35.    
  36.     // 登出
  37.     logout() {
  38.       this.token = ''
  39.       this.userInfo = null
  40.     },
  41.   },
  42. })
复制代码

在组件中使用:
  1. <template>
  2.   <div v-if="isLogin">
  3.     <h2>欢迎, {{ userName }}</h2>
  4.     <button @click="handleLogout">登出</button>
  5.   </div>
  6.   <div v-else>
  7.     <van-form @submit="handleLogin">
  8.       <van-field
  9.         v-model="loginForm.username"
  10.         name="username"
  11.         label="用户名"
  12.         placeholder="请输入用户名"
  13.         :rules="[{ required: true, message: '请填写用户名' }]"
  14.       />
  15.       <van-field
  16.         v-model="loginForm.password"
  17.         type="password"
  18.         name="password"
  19.         label="密码"
  20.         placeholder="请输入密码"
  21.         :rules="[{ required: true, message: '请填写密码' }]"
  22.       />
  23.       <div style="margin: 16px;">
  24.         <van-button round block type="primary" native-type="submit">
  25.           登录
  26.         </van-button>
  27.       </div>
  28.     </van-form>
  29.   </div>
  30. </template>
  31. <script>
  32. import { ref } from 'vue';
  33. import { storeToRefs } from 'pinia';
  34. import { useUserStore } from '@/store/user';
  35. import { showToast } from 'vant';
  36. export default {
  37.   setup() {
  38.     const userStore = useUserStore();
  39.     const { isLogin, userName } = storeToRefs(userStore);
  40.     const { login, logout } = userStore;
  41.    
  42.     const loginForm = ref({
  43.       username: '',
  44.       password: ''
  45.     });
  46.    
  47.     const handleLogin = async () => {
  48.       try {
  49.         await login(loginForm.value);
  50.         await userStore.fetchUserInfo();
  51.         showToast('登录成功');
  52.       } catch (error) {
  53.         showToast('登录失败');
  54.         console.error(error);
  55.       }
  56.     };
  57.    
  58.     const handleLogout = () => {
  59.       logout();
  60.       showToast('已登出');
  61.     };
  62.    
  63.     return {
  64.       isLogin,
  65.       userName,
  66.       loginForm,
  67.       handleLogin,
  68.       handleLogout
  69.     };
  70.   }
  71. }
  72. </script>
复制代码

持久化存储

使用插件实现Pinia状态持久化:
  1. npm install pinia-plugin-persistedstate
复制代码

在main.js中配置:
  1. import { createApp } from 'vue'
  2. import { createPinia } from 'pinia'
  3. import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
  4. import App from './App.vue'
  5. const pinia = createPinia()
  6. pinia.use(piniaPluginPersistedstate)
  7. const app = createApp(App)
  8. app.use(pinia)
  9. app.mount('#app')
复制代码

在store中启用持久化:
  1. // src/store/user.js
  2. import { defineStore } from 'pinia'
  3. import { login as userLogin, getUserInfo } from '@/api/user'
  4. export const useUserStore = defineStore('user', {
  5.   state: () => ({
  6.     token: '',
  7.     userInfo: null,
  8.   }),
  9.   getters: {
  10.     isLogin: (state) => !!state.token,
  11.     userName: (state) => state.userInfo?.name || '',
  12.   },
  13.   actions: {
  14.     async login(loginForm) {
  15.       try {
  16.         const { token } = await userLogin(loginForm)
  17.         this.token = token
  18.         return Promise.resolve()
  19.       } catch (error) {
  20.         return Promise.reject(error)
  21.       }
  22.     },
  23.    
  24.     async fetchUserInfo() {
  25.       try {
  26.         const userInfo = await getUserInfo()
  27.         this.userInfo = userInfo
  28.         return Promise.resolve(userInfo)
  29.       } catch (error) {
  30.         return Promise.reject(error)
  31.       }
  32.     },
  33.    
  34.     logout() {
  35.       this.token = ''
  36.       this.userInfo = null
  37.     },
  38.   },
  39.   // 启用持久化
  40.   persist: {
  41.     enabled: true,
  42.     strategies: [
  43.       {
  44.         key: 'user',
  45.         storage: localStorage,
  46.       },
  47.     ],
  48.   },
  49. })
复制代码

路由管理

Vue Router的安装与配置

安装Vue Router:
  1. npm install vue-router@4
复制代码

创建路由配置文件:
  1. // src/router/index.js
  2. import { createRouter, createWebHistory } from 'vue-router'
  3. const routes = [
  4.   {
  5.     path: '/',
  6.     name: 'Home',
  7.     component: () => import('@/views/Home.vue'),
  8.     meta: {
  9.       title: '首页',
  10.       keepAlive: true
  11.     }
  12.   },
  13.   {
  14.     path: '/category',
  15.     name: 'Category',
  16.     component: () => import('@/views/Category.vue'),
  17.     meta: {
  18.       title: '分类',
  19.       keepAlive: true
  20.     }
  21.   },
  22.   {
  23.     path: '/cart',
  24.     name: 'Cart',
  25.     component: () => import('@/views/Cart.vue'),
  26.     meta: {
  27.       title: '购物车',
  28.       keepAlive: false
  29.     }
  30.   },
  31.   {
  32.     path: '/user',
  33.     name: 'User',
  34.     component: () => import('@/views/User.vue'),
  35.     meta: {
  36.       title: '我的',
  37.       keepAlive: true
  38.     }
  39.   },
  40.   {
  41.     path: '/product/:id',
  42.     name: 'Product',
  43.     component: () => import('@/views/Product.vue'),
  44.     meta: {
  45.       title: '商品详情',
  46.       keepAlive: false
  47.     }
  48.   },
  49.   {
  50.     path: '/login',
  51.     name: 'Login',
  52.     component: () => import('@/views/Login.vue'),
  53.     meta: {
  54.       title: '登录',
  55.       keepAlive: false
  56.     }
  57.   },
  58.   {
  59.     path: '/:pathMatch(.*)*',
  60.     name: 'NotFound',
  61.     component: () => import('@/views/NotFound.vue'),
  62.     meta: {
  63.       title: '页面不存在',
  64.       keepAlive: false
  65.     }
  66.   }
  67. ]
  68. const router = createRouter({
  69.   history: createWebHistory(),
  70.   routes
  71. })
  72. // 全局前置守卫
  73. router.beforeEach((to, from, next) => {
  74.   // 设置页面标题
  75.   document.title = to.meta.title || 'Vue3+Vant App'
  76.   
  77.   // 登录权限控制
  78.   const userStore = useUserStore()
  79.   if (to.meta.requiresAuth && !userStore.isLogin) {
  80.     next({ name: 'Login', query: { redirect: to.fullPath } })
  81.   } else {
  82.     next()
  83.   }
  84. })
  85. export default router
复制代码

在main.js中引入路由:
  1. import { createApp } from 'vue'
  2. import { createPinia } from 'pinia'
  3. import App from './App.vue'
  4. import router from './router'
  5. const app = createApp(App)
  6. app.use(createPinia())
  7. app.use(router)
  8. app.mount('#app')
复制代码

路由定义与导航

在App.vue中使用路由:
  1. <template>
  2.   <div class="app">
  3.     <!-- 路由视图 -->
  4.     <router-view v-slot="{ Component }">
  5.       <keep-alive :include="keepAliveViews">
  6.         <component :is="Component" />
  7.       </keep-alive>
  8.     </router-view>
  9.    
  10.     <!-- 底部导航 -->
  11.     <van-tabbar v-model="active" route>
  12.       <van-tabbar-item replace to="/" icon="home-o">首页</van-tabbar-item>
  13.       <van-tabbar-item replace to="/category" icon="apps-o">分类</van-tabbar-item>
  14.       <van-tabbar-item replace to="/cart" icon="cart-o">购物车</van-tabbar-item>
  15.       <van-tabbar-item replace to="/user" icon="user-o">我的</van-tabbar-item>
  16.     </van-tabbar>
  17.   </div>
  18. </template>
  19. <script>
  20. import { ref, computed } from 'vue';
  21. import { useRoute } from 'vue-router';
  22. export default {
  23.   setup() {
  24.     const route = useRoute();
  25.     const active = ref(0);
  26.    
  27.     // 需要缓存的页面
  28.     const keepAliveViews = computed(() => {
  29.       return route.matched
  30.         .filter(item => item.meta.keepAlive)
  31.         .map(item => item.name);
  32.     });
  33.    
  34.     return {
  35.       active,
  36.       keepAliveViews
  37.     };
  38.   }
  39. }
  40. </script>
  41. <style>
  42. .app {
  43.   padding-bottom: 50px;
  44. }
  45. </style>
复制代码

在组件中进行导航:
  1. <template>
  2.   <div>
  3.     <h2>首页</h2>
  4.    
  5.     <!-- 声明式导航 -->
  6.     <van-button type="primary" to="/category">跳转到分类页</van-button>
  7.    
  8.     <!-- 编程式导航 -->
  9.     <van-button type="primary" @click="goToCategory">跳转到分类页</van-button>
  10.    
  11.     <!-- 动态路由导航 -->
  12.     <van-button type="primary" @click="goToProduct">跳转到商品详情</van-button>
  13.    
  14.     <!-- 带查询参数的导航 -->
  15.     <van-button type="primary" @click="goToCategoryWithQuery">跳转到分类页(带参数)</van-button>
  16.   </div>
  17. </template>
  18. <script>
  19. import { useRouter } from 'vue-router';
  20. export default {
  21.   setup() {
  22.     const router = useRouter();
  23.    
  24.     const goToCategory = () => {
  25.       router.push('/category');
  26.     };
  27.    
  28.     const goToProduct = () => {
  29.       router.push({
  30.         name: 'Product',
  31.         params: {
  32.           id: '123'
  33.         }
  34.       });
  35.     };
  36.    
  37.     const goToCategoryWithQuery = () => {
  38.       router.push({
  39.         path: '/category',
  40.         query: {
  41.           id: '1',
  42.           name: '电子产品'
  43.         }
  44.       });
  45.     };
  46.    
  47.     return {
  48.       goToCategory,
  49.       goToProduct,
  50.       goToCategoryWithQuery
  51.     };
  52.   }
  53. }
  54. </script>
复制代码

路由守卫

路由守卫用于控制路由的访问权限,Vue Router提供了全局守卫、路由独享守卫和组件内守卫。
  1. // src/router/index.js
  2. // 全局前置守卫
  3. router.beforeEach((to, from, next) => {
  4.   // 设置页面标题
  5.   document.title = to.meta.title || 'Vue3+Vant App'
  6.   
  7.   // 登录权限控制
  8.   const userStore = useUserStore()
  9.   if (to.meta.requiresAuth && !userStore.isLogin) {
  10.     next({ name: 'Login', query: { redirect: to.fullPath } })
  11.   } else {
  12.     next()
  13.   }
  14. })
  15. // 全局后置钩子
  16. router.afterEach((to, from) => {
  17.   // 可以在这里进行页面滚动等操作
  18.   window.scrollTo(0, 0)
  19. })
复制代码
  1. // src/router/index.js
  2. const routes = [
  3.   {
  4.     path: '/admin',
  5.     name: 'Admin',
  6.     component: () => import('@/views/Admin.vue'),
  7.     meta: {
  8.       title: '管理后台',
  9.       requiresAuth: true,
  10.       requiresAdmin: true
  11.     },
  12.     beforeEnter: (to, from, next) => {
  13.       const userStore = useUserStore()
  14.       if (userStore.userInfo?.role !== 'admin') {
  15.         next({ name: 'Home' })
  16.       } else {
  17.         next()
  18.       }
  19.     }
  20.   }
  21. ]
复制代码
  1. <template>
  2.   <div>
  3.     <h2>商品详情</h2>
  4.     <p>商品ID: {{ productId }}</p>
  5.   </div>
  6. </template>
  7. <script>
  8. import { ref, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue';
  9. import { useRoute, useRouter } from 'vue-router';
  10. export default {
  11.   setup() {
  12.     const route = useRoute();
  13.     const router = useRouter();
  14.     const productId = ref(route.params.id);
  15.    
  16.     // 组件内守卫:离开当前路由时
  17.     onBeforeRouteLeave((to, from, next) => {
  18.       // 如果表单有未保存的更改,可以提示用户
  19.       if (hasUnsavedChanges()) {
  20.         const answer = window.confirm(
  21.           '您有未保存的更改,确定要离开吗?'
  22.         )
  23.         if (answer) {
  24.           next()
  25.         } else {
  26.           next(false)
  27.         }
  28.       } else {
  29.         next()
  30.       }
  31.     });
  32.    
  33.     // 组件内守卫:在当前路由改变,但是该组件被复用时调用
  34.     onBeforeRouteUpdate((to, from, next) => {
  35.       // 例如,从 /product/1 导航到 /product/2
  36.       productId.value = to.params.id;
  37.       fetchProductData(to.params.id);
  38.       next();
  39.     });
  40.    
  41.     const hasUnsavedChanges = () => {
  42.       // 检查是否有未保存的更改
  43.       return false;
  44.     };
  45.    
  46.     const fetchProductData = (id) => {
  47.       // 根据ID获取商品数据
  48.       console.log('Fetching product data for ID:', id);
  49.     };
  50.    
  51.     return {
  52.       productId
  53.     };
  54.   }
  55. }
  56. </script>
复制代码

懒加载

懒加载可以按需加载页面组件,减少初始加载时间。在路由配置中,我们可以使用动态导入实现懒加载:
  1. // src/router/index.js
  2. const routes = [
  3.   {
  4.     path: '/',
  5.     name: 'Home',
  6.     component: () => import('@/views/Home.vue'),
  7.     meta: {
  8.       title: '首页',
  9.       keepAlive: true
  10.     }
  11.   },
  12.   {
  13.     path: '/category',
  14.     name: 'Category',
  15.     component: () => import('@/views/Category.vue'),
  16.     meta: {
  17.       title: '分类',
  18.       keepAlive: true
  19.     }
  20.   },
  21.   {
  22.     path: '/cart',
  23.     name: 'Cart',
  24.     component: () => import('@/views/Cart.vue'),
  25.     meta: {
  26.       title: '购物车',
  27.       keepAlive: false
  28.     }
  29.   },
  30.   {
  31.     path: '/user',
  32.     name: 'User',
  33.     component: () => import('@/views/User.vue'),
  34.     meta: {
  35.       title: '我的',
  36.       keepAlive: true
  37.     }
  38.   }
  39. ]
复制代码

我们还可以使用魔法注释为懒加载的组件命名,方便调试:
  1. const routes = [
  2.   {
  3.     path: '/',
  4.     name: 'Home',
  5.     component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue'),
  6.     meta: {
  7.       title: '首页',
  8.       keepAlive: true
  9.     }
  10.   },
  11.   {
  12.     path: '/category',
  13.     name: 'Category',
  14.     component: () => import(/* webpackChunkName: "category" */ '@/views/Category.vue'),
  15.     meta: {
  16.       title: '分类',
  17.       keepAlive: true
  18.     }
  19.   }
  20. ]
复制代码

项目优化

代码分割

代码分割是优化应用加载性能的重要手段。除了路由懒加载,我们还可以对第三方库和大型组件进行代码分割。

在vite.config.js中配置:
  1. import { defineConfig } from 'vite'
  2. import vue from '@vitejs/plugin-vue'
  3. import Components from 'unplugin-vue-components/vite'
  4. import { VantResolver } from 'unplugin-vue-components/resolvers'
  5. export default defineConfig({
  6.   plugins: [
  7.     vue(),
  8.     Components({
  9.       resolvers: [VantResolver()],
  10.     }),
  11.   ],
  12.   build: {
  13.     rollupOptions: {
  14.       output: {
  15.         manualChunks: {
  16.           'vendor': ['vue', 'vue-router', 'pinia'],
  17.           'vant': ['vant']
  18.         }
  19.       }
  20.     }
  21.   }
  22. })
复制代码

对于大型组件,我们可以使用动态导入实现按需加载:
  1. <template>
  2.   <div>
  3.     <h2>首页</h2>
  4.     <button @click="showLargeComponent">加载大型组件</button>
  5.    
  6.     <div v-if="showComponent">
  7.       <component :is="LargeComponent" />
  8.     </div>
  9.   </div>
  10. </template>
  11. <script>
  12. import { ref, defineAsyncComponent } from 'vue';
  13. export default {
  14.   setup() {
  15.     const showComponent = ref(false);
  16.    
  17.     // 异步加载大型组件
  18.     const LargeComponent = defineAsyncComponent(() =>
  19.       import('@/components/LargeComponent.vue')
  20.     );
  21.    
  22.     const showLargeComponent = () => {
  23.       showComponent.value = true;
  24.     };
  25.    
  26.     return {
  27.       showComponent,
  28.       LargeComponent,
  29.       showLargeComponent
  30.     };
  31.   }
  32. }
  33. </script>
复制代码

懒加载

Vant UI的Image组件已经内置了懒加载功能:
  1. <template>
  2.   <van-image
  3.     width="100"
  4.     height="100"
  5.     src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
  6.     lazy-load
  7.   />
  8. </template>
复制代码

使用Vant的List组件实现列表的懒加载:
  1. <template>
  2.   <van-list
  3.     v-model:loading="loading"
  4.     :finished="finished"
  5.     finished-text="没有更多了"
  6.     @load="onLoad"
  7.   >
  8.     <van-cell v-for="item in list" :key="item" :title="item" />
  9.   </van-list>
  10. </template>
  11. <script>
  12. import { ref } from 'vue';
  13. export default {
  14.   setup() {
  15.     const list = ref([]);
  16.     const loading = ref(false);
  17.     const finished = ref(false);
  18.    
  19.     const onLoad = () => {
  20.       // 异步更新数据
  21.       setTimeout(() => {
  22.         for (let i = 0; i < 10; i++) {
  23.           list.value.push(list.value.length + 1);
  24.         }
  25.         // 加载状态结束
  26.         loading.value = false;
  27.         
  28.         // 数据全部加载完成
  29.         if (list.value.length >= 40) {
  30.           finished.value = true;
  31.         }
  32.       }, 1000);
  33.     };
  34.    
  35.     return {
  36.       list,
  37.       loading,
  38.       finished,
  39.       onLoad,
  40.     };
  41.   },
  42. };
  43. </script>
复制代码

缓存策略

在vite.config.js中配置HTTP缓存:
  1. import { defineConfig } from 'vite'
  2. import vue from '@vitejs/plugin-vue'
  3. export default defineConfig({
  4.   plugins: [vue()],
  5.   build: {
  6.     rollupOptions: {
  7.       output: {
  8.         chunkFileNames: 'static/js/[name]-[hash].js',
  9.         entryFileNames: 'static/js/[name]-[hash].js',
  10.         assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
  11.       }
  12.     }
  13.   }
  14. })
复制代码

使用Workbox实现Service Worker缓存:
  1. npm install workbox-webpack-plugin -D
复制代码

在vite.config.js中配置:
  1. import { defineConfig } from 'vite'
  2. import vue from '@vitejs/plugin-vue'
  3. import { GenerateSW } from 'workbox-webpack-plugin'
  4. export default defineConfig({
  5.   plugins: [
  6.     vue(),
  7.     {
  8.       ...GenerateSW({
  9.         clientsClaim: true,
  10.         skipWaiting: true,
  11.         runtimeCaching: [
  12.           {
  13.             urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
  14.             handler: 'CacheFirst',
  15.             options: {
  16.               cacheName: 'image-cache',
  17.               expiration: {
  18.                 maxEntries: 60,
  19.                 maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
  20.               },
  21.             },
  22.           },
  23.           {
  24.             urlPattern: /\.(?:js|css)$/,
  25.             handler: 'StaleWhileRevalidate',
  26.             options: {
  27.               cacheName: 'static-resources',
  28.               expiration: {
  29.                 maxEntries: 60,
  30.                 maxAgeSeconds: 24 * 60 * 60, // 24 hours
  31.               },
  32.             },
  33.           },
  34.         ],
  35.       }),
  36.       apply: 'build'
  37.     }
  38.   ]
  39. })
复制代码

性能优化技巧

对于长列表,可以使用虚拟滚动来提高性能:
  1. <template>
  2.   <van-list
  3.     v-model:loading="loading"
  4.     :finished="finished"
  5.     finished-text="没有更多了"
  6.     @load="onLoad"
  7.   >
  8.     <van-cell v-for="item in list" :key="item" :title="item" />
  9.   </van-list>
  10. </template>
  11. <script>
  12. import { ref } from 'vue';
  13. export default {
  14.   setup() {
  15.     const list = ref([]);
  16.     const loading = ref(false);
  17.     const finished = ref(false);
  18.    
  19.     const onLoad = () => {
  20.       // 异步更新数据
  21.       setTimeout(() => {
  22.         for (let i = 0; i < 10; i++) {
  23.           list.value.push(list.value.length + 1);
  24.         }
  25.         // 加载状态结束
  26.         loading.value = false;
  27.         
  28.         // 数据全部加载完成
  29.         if (list.value.length >= 40) {
  30.           finished.value = true;
  31.         }
  32.       }, 1000);
  33.     };
  34.    
  35.     return {
  36.       list,
  37.       loading,
  38.       finished,
  39.       onLoad,
  40.     };
  41.   },
  42. };
  43. </script>
复制代码

对于频繁触发的事件,如滚动、输入等,可以使用防抖和节流来优化性能:
  1. // src/utils/debounce.js
  2. export function debounce(fn, delay) {
  3.   let timer = null;
  4.   return function (...args) {
  5.     if (timer) clearTimeout(timer);
  6.     timer = setTimeout(() => {
  7.       fn.apply(this, args);
  8.     }, delay);
  9.   };
  10. }
  11. // src/utils/throttle.js
  12. export function throttle(fn, delay) {
  13.   let last = 0;
  14.   return function (...args) {
  15.     const now = Date.now();
  16.     if (now - last > delay) {
  17.       fn.apply(this, args);
  18.       last = now;
  19.     }
  20.   };
  21. }
复制代码

在组件中使用:
  1. <template>
  2.   <div>
  3.     <van-search
  4.       v-model="searchValue"
  5.       placeholder="请输入搜索关键词"
  6.       @input="onSearch"
  7.     />
  8.    
  9.     <div class="scroll-container" @scroll="onScroll">
  10.       <!-- 滚动内容 -->
  11.     </div>
  12.   </div>
  13. </template>
  14. <script>
  15. import { ref } from 'vue';
  16. import { debounce, throttle } from '@/utils';
  17. export default {
  18.   setup() {
  19.     const searchValue = ref('');
  20.    
  21.     // 使用防抖处理搜索输入
  22.     const onSearch = debounce((value) => {
  23.       console.log('搜索:', value);
  24.       // 执行搜索逻辑
  25.     }, 300);
  26.    
  27.     // 使用节流处理滚动事件
  28.     const onScroll = throttle((event) => {
  29.       console.log('滚动位置:', event.target.scrollTop);
  30.       // 执行滚动逻辑
  31.     }, 200);
  32.    
  33.     return {
  34.       searchValue,
  35.       onSearch,
  36.       onScroll
  37.     };
  38.   }
  39. }
  40. </script>
复制代码

对于非首屏的组件,可以使用Intersection Observer API实现懒加载:
  1. <template>
  2.   <div>
  3.     <h2>首页</h2>
  4.    
  5.     <div ref="lazyComponentContainer" class="lazy-container">
  6.       <component :is="LazyComponent" v-if="showLazyComponent" />
  7.     </div>
  8.   </div>
  9. </template>
  10. <script>
  11. import { ref, onMounted, defineAsyncComponent } from 'vue';
  12. export default {
  13.   setup() {
  14.     const lazyComponentContainer = ref(null);
  15.     const showLazyComponent = ref(false);
  16.    
  17.     // 异步加载懒加载组件
  18.     const LazyComponent = defineAsyncComponent(() =>
  19.       import('@/components/LazyComponent.vue')
  20.     );
  21.    
  22.     onMounted(() => {
  23.       const observer = new IntersectionObserver((entries) => {
  24.         entries.forEach(entry => {
  25.           if (entry.isIntersecting) {
  26.             showLazyComponent.value = true;
  27.             observer.unobserve(entry.target);
  28.           }
  29.         });
  30.       }, {
  31.         threshold: 0.1
  32.       });
  33.       
  34.       if (lazyComponentContainer.value) {
  35.         observer.observe(lazyComponentContainer.value);
  36.       }
  37.     });
  38.    
  39.     return {
  40.       lazyComponentContainer,
  41.       showLazyComponent,
  42.       LazyComponent
  43.     };
  44.   }
  45. }
  46. </script>
  47. <style>
  48. .lazy-container {
  49.   min-height: 200px;
  50.   margin-top: 500px;
  51. }
  52. </style>
复制代码

SEO优化

对于SPA应用,SEO是一个挑战。我们可以使用以下方法来优化SEO:

使用prerender-spa-plugin进行预渲染:
  1. npm install prerender-spa-plugin -D
复制代码

在vite.config.js中配置:
  1. import { defineConfig } from 'vite'
  2. import vue from '@vitejs/plugin-vue'
  3. import path from 'path'
  4. export default defineConfig({
  5.   plugins: [
  6.     vue(),
  7.     {
  8.       name: 'prerender',
  9.       closeBundle() {
  10.         const PrerenderSPAPlugin = require('prerender-spa-plugin');
  11.         const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
  12.         
  13.         new PrerenderSPAPlugin({
  14.           staticDir: path.join(__dirname, 'dist'),
  15.           routes: ['/', '/about', '/contact'],
  16.           renderer: new Renderer({
  17.             headless: true,
  18.             renderAfterDocumentEvent: 'render-event'
  19.           })
  20.         }).apply();
  21.       }
  22.     }
  23.   ]
  24. })
复制代码

在main.js中添加:
  1. import { createApp } from 'vue'
  2. import App from './App.vue'
  3. const app = createApp(App)
  4. app.mount('#app')
  5. // 添加渲染事件
  6. document.dispatchEvent(new Event('render-event'))
复制代码

使用vue-meta管理页面的meta标签:
  1. npm install vue-meta -S
复制代码

在main.js中配置:
  1. import { createApp } from 'vue'
  2. import { createMetaManager } from 'vue-meta'
  3. import App from './App.vue'
  4. const app = createApp(App)
  5. app.use(createMetaManager())
  6. app.mount('#app')
复制代码

在组件中使用:
  1. <template>
  2.   <div>
  3.     <h2>关于我们</h2>
  4.     <p>这是关于我们的页面</p>
  5.   </div>
  6. </template>
  7. <script>
  8. import { useMeta } from 'vue-meta';
  9. export default {
  10.   setup() {
  11.     useMeta({
  12.       title: '关于我们 - Vue3+Vant App',
  13.       meta: [
  14.         { name: 'description', content: '这是关于我们的页面描述' },
  15.         { name: 'keywords', content: '关于我们,公司介绍' },
  16.         { property: 'og:title', content: '关于我们 - Vue3+Vant App' }
  17.       ]
  18.     });
  19.    
  20.     return {};
  21.   }
  22. }
  23. </script>
复制代码

项目部署

构建生产版本

使用Vite构建生产版本:
  1. npm run build
复制代码

构建完成后,会在项目根目录下生成一个dist文件夹,包含了所有生产环境所需的文件。

部署到服务器

将dist文件夹中的内容上传到服务器的Web目录,如Nginx的html目录。

配置Nginx:
  1. server {
  2.     listen 80;
  3.     server_name yourdomain.com;
  4.     root /usr/share/nginx/html;
  5.     index index.html;
  6.    
  7.     location / {
  8.         try_files $uri $uri/ /index.html;
  9.     }
  10.    
  11.     # 静态资源缓存
  12.     location ~* \.(?:css|js)$ {
  13.         expires 1y;
  14.         add_header Cache-Control "public, immutable";
  15.     }
  16.    
  17.     # 图片资源缓存
  18.     location ~* \.(?:jpg|jpeg|gif|png|webp|svg|ico)$ {
  19.         expires 1y;
  20.         add_header Cache-Control "public";
  21.     }
  22. }
复制代码

使用Express作为静态文件服务器:
  1. // server.js
  2. const express = require('express');
  3. const path = require('path');
  4. const app = express();
  5. // 静态文件服务
  6. app.use(express.static(path.join(__dirname, 'dist')));
  7. // 所有路由都返回index.html
  8. app.get('*', (req, res) => {
  9.   res.sendFile(path.join(__dirname, 'dist', 'index.html'));
  10. });
  11. const port = process.env.PORT || 3000;
  12. app.listen(port, () => {
  13.   console.log(`Server is running on port ${port}`);
  14. });
复制代码

安装依赖并启动:
  1. npm install express
  2. node server.js
复制代码

CDN配置

将静态资源上传到CDN,可以加快访问速度。在vite.config.js中配置:
  1. import { defineConfig } from 'vite'
  2. import vue from '@vitejs/plugin-vue'
  3. export default defineConfig({
  4.   plugins: [vue()],
  5.   base: 'https://cdn.yourdomain.com/your-project/',
  6.   build: {
  7.     rollupOptions: {
  8.       output: {
  9.         chunkFileNames: 'static/js/[name]-[hash].js',
  10.         entryFileNames: 'static/js/[name]-[hash].js',
  11.         assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
  12.       }
  13.     }
  14.   }
  15. })
复制代码

域名配置

配置HTTPS:
  1. server {
  2.     listen 443 ssl http2;
  3.     server_name yourdomain.com;
  4.    
  5.     ssl_certificate /path/to/your/certificate.crt;
  6.     ssl_certificate_key /path/to/your/private.key;
  7.    
  8.     root /usr/share/nginx/html;
  9.     index index.html;
  10.    
  11.     location / {
  12.         try_files $uri $uri/ /index.html;
  13.     }
  14. }
  15. # HTTP重定向到HTTPS
  16. server {
  17.     listen 80;
  18.     server_name yourdomain.com;
  19.     return 301 https://$server_name$request_uri;
  20. }
复制代码

总结与展望

本文详细介绍了如何使用Vue3和Vant UI构建高效的移动端应用,从环境搭建、项目初始化、组件使用、状态管理、路由管理到项目优化与部署的全过程。通过本文的学习,您应该已经掌握了Vue3和Vant UI的核心使用方法,并能够独立开发高质量的移动端应用。

Vue3的组合式API、响应式系统和优化的性能为开发者提供了更强大的开发能力,而Vant UI丰富的组件和良好的用户体验则大大提高了移动应用的开发效率。两者结合使用,可以快速构建出美观、高效、易用的移动应用。

未来,随着Vue3和Vant UI的不断发展,我们可以期待更多的新特性和优化。同时,随着5G、AI等技术的发展,移动应用将会有更多的创新和突破。作为开发者,我们需要不断学习和探索,紧跟技术发展的步伐,才能在移动开发领域保持竞争力。

学习资源推荐

1. Vue3官方文档
2. Vant UI官方文档
3. Pinia官方文档
4. Vue Router官方文档
5. Vite官方文档

希望本文能够帮助您成为移动开发领域的专家,祝您在移动应用开发的道路上取得更大的成功!
「七転び八起き(ななころびやおき)」
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则