|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
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. 安装完成后,在终端或命令提示符中运行以下命令验证安装:
如果显示版本号,则表示安装成功。
安装Vue CLI或Vite
Vue CLI是Vue的官方脚手架工具,而Vite是下一代前端构建工具,两者都可以用来创建Vue3项目。Vite具有更快的冷启动和热更新速度,推荐使用。
- npm create vite@latest my-vue-app -- --template vue
复制代码
首先安装Vue CLI:
然后创建项目:
在创建过程中,选择Vue3预设。
项目初始化
创建Vue3项目
我们以Vite为例创建Vue3项目:
- npm create vite@latest my-vue-app -- --template vue
- cd my-vue-app
- npm install
复制代码
安装和配置Vant UI
Vant UI可以通过npm或yarn安装:
引入Vant UI
Vant UI支持两种引入方式:完整引入和按需引入。
在main.js中引入:
- import { createApp } from 'vue'
- import App from './App.vue'
- import Vant from 'vant'
- import 'vant/lib/index.css'
- const app = createApp(App)
- app.use(Vant)
- app.mount('#app')
复制代码
按需引入可以减少打包体积,提高应用加载速度。
首先安装插件:
- npm install unplugin-vue-components unplugin-auto-import -D
复制代码
然后在vite.config.js中配置:
- import { defineConfig } from 'vite'
- import vue from '@vitejs/plugin-vue'
- import Components from 'unplugin-vue-components/vite'
- import { VantResolver } from 'unplugin-vue-components/resolvers'
- export default defineConfig({
- plugins: [
- vue(),
- Components({
- resolvers: [VantResolver()],
- }),
- ],
- })
复制代码
这样配置后,可以直接在模板中使用Vant组件,无需手动引入。
项目结构说明
一个典型的Vue3+Vant项目结构如下:
- my-vue-app/
- ├── public/ # 静态资源
- ├── src/ # 源代码
- │ ├── assets/ # 项目资源
- │ ├── components/ # 公共组件
- │ ├── views/ # 页面组件
- │ ├── router/ # 路由配置
- │ ├── store/ # 状态管理
- │ ├── utils/ # 工具函数
- │ ├── App.vue # 根组件
- │ └── main.js # 入口文件
- ├── .gitignore # Git忽略文件
- ├── index.html # HTML模板
- ├── package.json # 项目配置
- ├── vite.config.js # Vite配置
- └── README.md # 项目说明
复制代码
基础配置
在vite.config.js中,我们可以进行一些基础配置:
- import { defineConfig } from 'vite'
- import vue from '@vitejs/plugin-vue'
- import { resolve } from 'path'
- import Components from 'unplugin-vue-components/vite'
- import { VantResolver } from 'unplugin-vue-components/resolvers'
- export default defineConfig({
- plugins: [
- vue(),
- Components({
- resolvers: [VantResolver()],
- }),
- ],
- resolve: {
- alias: {
- '@': resolve(__dirname, 'src'),
- },
- },
- server: {
- port: 3000,
- open: true,
- proxy: {
- '/api': {
- target: 'http://localhost:8080',
- changeOrigin: true,
- rewrite: (path) => path.replace(/^\/api/, ''),
- },
- },
- },
- })
复制代码
基础组件使用
Vant UI提供了丰富的组件,下面我们介绍一些常用组件的使用方法。
布局组件
Vant提供了Row和Col组件来实现栅格布局:
- <template>
- <van-row>
- <van-col span="8">span: 8</van-col>
- <van-col span="8">span: 8</van-col>
- <van-col span="8">span: 8</van-col>
- </van-row>
-
- <van-row>
- <van-col span="12">span: 12</van-col>
- <van-col span="12">span: 12</van-col>
- </van-row>
-
- <van-row gutter="20">
- <van-col span="8">span: 8</van-col>
- <van-col span="8">span: 8</van-col>
- <van-col span="8">span: 8</van-col>
- </van-row>
- </template>
复制代码
Vant提供了Flex组件来实现弹性布局:
- <template>
- <van-flex>
- <div class="flex-item">1</div>
- <div class="flex-item">2</div>
- <div class="flex-item">3</div>
- </van-flex>
-
- <van-flex justify="space-between">
- <div class="flex-item">1</div>
- <div class="flex-item">2</div>
- <div class="flex-item">3</div>
- </van-flex>
- </template>
- <style>
- .flex-item {
- width: 50px;
- height: 50px;
- background-color: #f2f6fc;
- text-align: center;
- line-height: 50px;
- }
- </style>
复制代码
表单组件
- <template>
- <van-button type="primary">主要按钮</van-button>
- <van-button type="success">成功按钮</van-button>
- <van-button type="default">默认按钮</van-button>
- <van-button type="warning">警告按钮</van-button>
- <van-button type="danger">危险按钮</van-button>
-
- <van-button plain type="primary">朴素按钮</van-button>
- <van-button disabled type="primary">禁用按钮</van-button>
- <van-button loading type="primary">加载中</van-button>
-
- <van-button square type="primary">方形按钮</van-button>
- <van-button round type="primary">圆形按钮</van-button>
-
- <van-block>
- <van-button type="primary" block>块级按钮</van-button>
- </van-block>
-
- <van-button type="primary" size="large">大号按钮</van-button>
- <van-button type="primary" size="normal">普通按钮</van-button>
- <van-button type="primary" size="small">小型按钮</van-button>
- <van-button type="primary" size="mini">迷你按钮</van-button>
- </template>
复制代码- <template>
- <van-field
- v-model="value"
- label="文本"
- placeholder="请输入文本"
- />
-
- <van-field
- v-model="value2"
- label="密码"
- type="password"
- placeholder="请输入密码"
- />
-
- <van-field
- v-model="value3"
- label="手机号"
- type="tel"
- placeholder="请输入手机号"
- />
-
- <van-field
- v-model="value4"
- label="数字"
- type="number"
- placeholder="请输入数字"
- />
-
- <van-field
- v-model="value5"
- label="短信验证码"
- center
- placeholder="请输入短信验证码"
- >
- <template #button>
- <van-button size="small" type="primary">发送验证码</van-button>
- </template>
- </van-field>
-
- <van-field
- v-model="value6"
- label="留言"
- type="textarea"
- placeholder="请输入留言"
- rows="2"
- autosize
- />
- </template>
- <script>
- import { ref } from 'vue';
- export default {
- setup() {
- const value = ref('');
- const value2 = ref('');
- const value3 = ref('');
- const value4 = ref('');
- const value5 = ref('');
- const value6 = ref('');
-
- return {
- value,
- value2,
- value3,
- value4,
- value5,
- value6
- };
- }
- }
- </script>
复制代码- <template>
- <van-radio-group v-model="radio" direction="horizontal">
- <van-radio name="1">单选框 1</van-radio>
- <van-radio name="2">单选框 2</van-radio>
- </van-radio-group>
-
- <van-radio-group v-model="radio2">
- <van-cell-group inset>
- <van-cell title="单选框 1" clickable @click="radio2 = '1'">
- <template #right-icon>
- <van-radio name="1" />
- </template>
- </van-cell>
- <van-cell title="单选框 2" clickable @click="radio2 = '2'">
- <template #right-icon>
- <van-radio name="2" />
- </template>
- </van-cell>
- </van-cell-group>
- </van-radio-group>
- </template>
- <script>
- import { ref } from 'vue';
- export default {
- setup() {
- const radio = ref('1');
- const radio2 = ref('1');
-
- return {
- radio,
- radio2
- };
- }
- }
- </script>
复制代码- <template>
- <van-checkbox-group v-model="checked" direction="horizontal">
- <van-checkbox name="a">复选框 a</van-checkbox>
- <van-checkbox name="b">复选框 b</van-checkbox>
- <van-checkbox name="c">复选框 c</van-checkbox>
- </van-checkbox-group>
-
- <van-checkbox-group v-model="checked2">
- <van-cell-group inset>
- <van-cell
- v-for="(item, index) in list"
- clickable
- :key="item"
- :title="`复选框 ${item}`"
- @click="toggle(index)"
- >
- <template #right-icon>
- <van-checkbox :name="item" ref="checkboxes" />
- </template>
- </van-cell>
- </van-cell-group>
- </van-checkbox-group>
- </template>
- <script>
- import { ref } from 'vue';
- export default {
- setup() {
- const checked = ref(['a', 'b']);
- const checked2 = ref([]);
- const list = ['a', 'b', 'c'];
- const checkboxes = ref([]);
-
- const toggle = (index) => {
- checkboxes.value[index].toggle();
- };
-
- return {
- checked,
- checked2,
- list,
- checkboxes,
- toggle
- };
- }
- }
- </script>
复制代码- <template>
- <van-form @submit="onSubmit">
- <van-cell-group inset>
- <van-field
- v-model="username"
- name="username"
- label="用户名"
- placeholder="用户名"
- :rules="[{ required: true, message: '请填写用户名' }]"
- />
- <van-field
- v-model="password"
- type="password"
- name="password"
- label="密码"
- placeholder="密码"
- :rules="[{ required: true, message: '请填写密码' }]"
- />
- </van-cell-group>
- <div style="margin: 16px;">
- <van-button round block type="primary" native-type="submit">
- 提交
- </van-button>
- </div>
- </van-form>
- </template>
- <script>
- import { ref } from 'vue';
- import { showToast } from 'vant';
- export default {
- setup() {
- const username = ref('');
- const password = ref('');
-
- const onSubmit = (values) => {
- console.log('form submit', values);
- showToast('提交成功');
- };
-
- return {
- username,
- password,
- onSubmit,
- };
- },
- };
- </script>
复制代码
反馈组件
- <template>
- <van-button type="primary" @click="showToast">成功提示</van-button>
- <van-button type="primary" @click="showLoadingToast">加载提示</van-button>
- <van-button type="primary" @click="showFailToast">失败提示</van-button>
- </template>
- <script>
- import { showToast, showLoadingToast, showFailToast } from 'vant';
- export default {
- setup() {
- const showToast = () => {
- showToast('成功提示');
- };
-
- const showLoadingToast = () => {
- showLoadingToast({
- message: '加载中...',
- forbidClick: true,
- });
- };
-
- const showFailToast = () => {
- showFailToast('失败提示');
- };
-
- return {
- showToast,
- showLoadingToast,
- showFailToast
- };
- }
- }
- </script>
复制代码- <template>
- <van-button type="primary" @click="showDialog">提示弹窗</van-button>
- <van-button type="primary" @click="showConfirmDialog">确认弹窗</van-button>
- <van-button type="primary" @click="showAsyncCloseDialog">异步关闭</van-button>
- </template>
- <script>
- import { showDialog, showConfirmDialog } from 'vant';
- export default {
- setup() {
- const showDialog = () => {
- showDialog({
- title: '标题',
- message: '这是一个提示弹窗',
- }).then(() => {
- // on close
- });
- };
-
- const showConfirmDialog = () => {
- showConfirmDialog({
- title: '标题',
- message: '这是一个确认弹窗',
- })
- .then(() => {
- // on confirm
- console.log('确认');
- })
- .catch(() => {
- // on cancel
- console.log('取消');
- });
- };
-
- const showAsyncCloseDialog = () => {
- showDialog({
- title: '标题',
- message: '弹窗内容',
- beforeClose: (action, done) => {
- if (action === 'confirm') {
- setTimeout(done, 1000);
- } else {
- done();
- }
- },
- });
- };
-
- return {
- showDialog,
- showConfirmDialog,
- showAsyncCloseDialog
- };
- }
- }
- </script>
复制代码
展示组件
- <template>
- <van-cell-group>
- <van-cell title="单元格" value="内容" />
- <van-cell title="单元格" value="内容" label="描述信息" />
- </van-cell-group>
-
- <van-cell-group inset>
- <van-cell title="单元格" value="内容" />
- <van-cell title="单元格" value="内容" label="描述信息" />
- </van-cell-group>
-
- <van-cell-group>
- <van-cell title="单元格" is-link />
- <van-cell title="单元格" is-link value="内容" />
- <van-cell title="单元格" is-link value="内容" label="描述信息" />
- </van-cell-group>
-
- <van-cell-group>
- <van-cell title="标题" icon="location-o" />
- <van-cell title="标题" icon="location-o" value="内容" />
- <van-cell title="标题" icon="location-o" value="内容" label="描述信息" is-link />
- </van-cell-group>
- </template>
复制代码- <template>
- <van-image
- width="100"
- height="100"
- src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
- />
-
- <van-image
- round
- width="100"
- height="100"
- src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
- />
-
- <van-image
- width="100"
- height="100"
- fit="contain"
- src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
- />
-
- <van-image
- width="100"
- height="100"
- src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
- @click="previewImage"
- />
-
- <van-image
- width="100"
- height="100"
- src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
- lazy-load
- />
- </template>
- <script>
- import { showImagePreview } from 'vant';
- export default {
- setup() {
- const previewImage = () => {
- showImagePreview([
- 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg',
- ]);
- };
-
- return {
- previewImage
- };
- }
- }
- </script>
复制代码- <template>
- <van-list
- v-model:loading="loading"
- :finished="finished"
- finished-text="没有更多了"
- @load="onLoad"
- >
- <van-cell v-for="item in list" :key="item" :title="item" />
- </van-list>
- </template>
- <script>
- import { ref } from 'vue';
- export default {
- setup() {
- const list = ref([]);
- const loading = ref(false);
- const finished = ref(false);
-
- const onLoad = () => {
- // 异步更新数据
- // setTimeout 仅做示例,真实场景中一般为 ajax 请求
- setTimeout(() => {
- for (let i = 0; i < 10; i++) {
- list.value.push(list.value.length + 1);
- }
- // 加载状态结束
- loading.value = false;
-
- // 数据全部加载完成
- if (list.value.length >= 40) {
- finished.value = true;
- }
- }, 1000);
- };
-
- return {
- list,
- loading,
- finished,
- onLoad,
- };
- },
- };
- </script>
复制代码
导航组件
- <template>
- <van-tabs v-model:active="active">
- <van-tab title="标签 1">内容 1</van-tab>
- <van-tab title="标签 2">内容 2</van-tab>
- <van-tab title="标签 3">内容 3</van-tab>
- <van-tab title="标签 4">内容 4</van-tab>
- </van-tabs>
-
- <van-tabs v-model:active="active2" type="card">
- <van-tab title="标签 1">内容 1</van-tab>
- <van-tab title="标签 2">内容 2</van-tab>
- <van-tab title="标签 3">内容 3</van-tab>
- </van-tabs>
-
- <van-tabs v-model:active="active3" swipeable>
- <van-tab v-for="index in 4" :title="'标签 ' + index" :key="index">
- 内容 {{ index }}
- </van-tab>
- </van-tabs>
- </template>
- <script>
- import { ref } from 'vue';
- export default {
- setup() {
- const active = ref(0);
- const active2 = ref(0);
- const active3 = ref(0);
-
- return {
- active,
- active2,
- active3
- };
- }
- }
- </script>
复制代码- <template>
- <van-nav-bar
- title="标题"
- left-text="返回"
- right-text="按钮"
- left-arrow
- @click-left="onClickLeft"
- @click-right="onClickRight"
- />
-
- <van-nav-bar
- title="标题"
- left-text="返回"
- left-arrow
- >
- <template #right>
- <van-icon name="search" size="18" />
- </template>
- </van-nav-bar>
- </template>
- <script>
- import { showToast } from 'vant';
- export default {
- setup() {
- const onClickLeft = () => showToast('返回');
- const onClickRight = () => showToast('按钮');
-
- return {
- onClickLeft,
- onClickRight
- };
- }
- }
- </script>
复制代码- <template>
- <div class="content">
- <router-view />
- </div>
-
- <van-tabbar v-model="active">
- <van-tabbar-item icon="home-o">标签</van-tabbar-item>
- <van-tabbar-item icon="search">标签</van-tabbar-item>
- <van-tabbar-item icon="friends-o">标签</van-tabbar-item>
- <van-tabbar-item icon="setting-o">标签</van-tabbar-item>
- </van-tabbar>
- </template>
- <script>
- import { ref } from 'vue';
- export default {
- setup() {
- const active = ref(0);
- return { active };
- },
- };
- </script>
- <style>
- .content {
- padding-bottom: 50px;
- }
- </style>
复制代码
高级组件与自定义
复杂组件的使用
- <template>
- <van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
- <van-swipe-item>1</van-swipe-item>
- <van-swipe-item>2</van-swipe-item>
- <van-swipe-item>3</van-swipe-item>
- <van-swipe-item>4</van-swipe-item>
- </van-swipe>
-
- <van-swipe :loop="false" class="my-swipe-2">
- <van-swipe-item>1</van-swipe-item>
- <van-swipe-item>2</van-swipe-item>
- <van-swipe-item>3</van-swipe-item>
- <van-swipe-item>4</van-swipe-item>
- </van-swipe>
-
- <van-swipe class="my-swipe-3" :autoplay="3000">
- <van-swipe-item v-for="(image, index) in images" :key="index">
- <img :src="image" />
- </van-swipe-item>
- </van-swipe>
- </template>
- <script>
- import { ref } from 'vue';
- export default {
- setup() {
- const images = [
- 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-1.jpeg',
- 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-2.jpeg',
- ];
-
- return {
- images
- };
- }
- }
- </script>
- <style>
- .my-swipe .van-swipe-item {
- color: #fff;
- font-size: 20px;
- line-height: 150px;
- text-align: center;
- background-color: #39a9ed;
- }
- .my-swipe-2 .van-swipe-item {
- color: #fff;
- font-size: 20px;
- line-height: 150px;
- text-align: center;
- background-color: #39a9ed;
- }
- .my-swipe-3 .van-swipe-item {
- display: flex;
- justify-content: center;
- align-items: center;
- height: 200px;
- }
- .my-swipe-3 img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- </style>
复制代码- <template>
- <van-grid :column-num="3">
- <van-grid-item icon="photo-o" text="文字" />
- <van-grid-item icon="photo-o" text="文字" />
- <van-grid-item icon="photo-o" text="文字" />
- <van-grid-item icon="photo-o" text="文字" />
- <van-grid-item icon="photo-o" text="文字" />
- <van-grid-item icon="photo-o" text="文字" />
- </van-grid>
-
- <van-grid :border="false" :column-num="4">
- <van-grid-item>
- <van-image src="https://fastly.jsdelivr.net/npm/@vant/assets/apple-1.jpeg" />
- <span style="margin-top: 8px">自定义内容</span>
- </van-grid-item>
- <van-grid-item>
- <van-image src="https://fastly.jsdelivr.net/npm/@vant/assets/apple-2.jpeg" />
- <span style="margin-top: 8px">自定义内容</span>
- </van-grid-item>
- <van-grid-item>
- <van-image src="https://fastly.jsdelivr.net/npm/@vant/assets/apple-3.jpeg" />
- <span style="margin-top: 8px">自定义内容</span>
- </van-grid-item>
- <van-grid-item>
- <van-image src="https://fastly.jsdelivr.net/npm/@vant/assets/apple-4.jpeg" />
- <span style="margin-top: 8px">自定义内容</span>
- </van-grid-item>
- </van-grid>
-
- <van-grid :gutter="10" :column-num="3">
- <van-grid-item v-for="value in 6" :key="value" icon="photo-o" text="文字" />
- </van-grid>
-
- <van-grid direction="horizontal" :column-num="3">
- <van-grid-item icon="photo-o" text="文字" />
- <van-grid-item icon="photo-o" text="文字" />
- <van-grid-item icon="photo-o" text="文字" />
- </van-grid>
- </template>
复制代码- <template>
- <van-collapse v-model="activeNames">
- <van-collapse-item title="标题1" name="1">
- 内容1
- </van-collapse-item>
- <van-collapse-item title="标题2" name="2">
- 内容2
- </van-collapse-item>
- <van-collapse-item title="标题3" name="3" disabled>
- 内容3(禁用)
- </van-collapse-item>
- </van-collapse>
-
- <van-collapse v-model="activeNames2" accordion>
- <van-collapse-item title="标题1" name="1">
- 内容1
- </van-collapse-item>
- <van-collapse-item title="标题2" name="2">
- 内容2
- </van-collapse-item>
- <van-collapse-item title="标题3" name="3">
- 内容3
- </van-collapse-item>
- </van-collapse>
- </template>
- <script>
- import { ref } from 'vue';
- export default {
- setup() {
- const activeNames = ref(['1']);
- const activeNames2 = ref('1');
-
- return {
- activeNames,
- activeNames2
- };
- }
- }
- </script>
复制代码
自定义主题
Vant UI支持通过CSS变量覆盖主题样式。在App.vue或全局CSS文件中定义:
- :root {
- --van-primary-color: #07c160;
- --van-success-color: #07c160;
- --van-danger-color: #ee0a24;
- --van-warning-color: #ff976a;
- --van-text-color: #323233;
- --van-text-color-2: #666;
- --van-text-color-3: #969799;
- --van-active-color: #f2f3f5;
- --van-background-color: #f7f8fa;
- --van-background-color-light: #fafafa;
- --van-white: #fff;
- }
复制代码
如果需要更深入的自定义,可以通过修改源码或使用less/sass变量来实现。
自定义组件开发
下面是一个自定义组件的示例,创建一个ProductCard组件:
- <!-- src/components/ProductCard.vue -->
- <template>
- <div class="product-card" @click="onClick">
- <div class="product-card__img">
- <van-image :src="product.image" fit="cover" />
- </div>
- <div class="product-card__content">
- <div class="product-card__title">{{ product.title }}</div>
- <div class="product-card__desc">{{ product.desc }}</div>
- <div class="product-card__price">
- <span class="product-card__price--current">¥{{ product.price }}</span>
- <span class="product-card__price--origin" v-if="product.originPrice">¥{{ product.originPrice }}</span>
- </div>
- </div>
- </div>
- </template>
- <script>
- import { defineProps, defineEmits } from 'vue';
- export default {
- name: 'ProductCard',
- props: {
- product: {
- type: Object,
- required: true,
- default: () => ({
- id: '',
- image: '',
- title: '',
- desc: '',
- price: 0,
- originPrice: 0
- })
- }
- },
- emits: ['click'],
- setup(props, { emit }) {
- const onClick = () => {
- emit('click', props.product);
- };
-
- return {
- onClick
- };
- }
- }
- </script>
- <style scoped>
- .product-card {
- background: #fff;
- border-radius: 8px;
- overflow: hidden;
- margin-bottom: 12px;
- box-shadow: 0 2px 12px rgba(100, 101, 102, 0.08);
- }
- .product-card__img {
- height: 180px;
- }
- .product-card__content {
- padding: 12px;
- }
- .product-card__title {
- font-size: 14px;
- font-weight: 500;
- color: #323233;
- margin-bottom: 4px;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- }
- .product-card__desc {
- font-size: 12px;
- color: #969799;
- margin-bottom: 8px;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 1;
- -webkit-box-orient: vertical;
- }
- .product-card__price {
- display: flex;
- align-items: baseline;
- }
- .product-card__price--current {
- font-size: 16px;
- font-weight: 500;
- color: #ee0a24;
- }
- .product-card__price--origin {
- font-size: 12px;
- color: #969799;
- text-decoration: line-through;
- margin-left: 4px;
- }
- </style>
复制代码
使用自定义组件:
- <template>
- <div class="product-list">
- <product-card
- v-for="product in products"
- :key="product.id"
- :product="product"
- @click="onProductClick"
- />
- </div>
- </template>
- <script>
- import { ref } from 'vue';
- import ProductCard from '@/components/ProductCard.vue';
- import { showToast } from 'vant';
- export default {
- components: {
- ProductCard
- },
- setup() {
- const products = ref([
- {
- id: '1',
- image: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-1.jpeg',
- title: '高品质苹果',
- desc: '新鲜采摘,口感脆甜',
- price: 6.8,
- originPrice: 8.9
- },
- {
- id: '2',
- image: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-2.jpeg',
- title: '进口红富士',
- desc: '产地直供,品质保证',
- price: 12.8,
- originPrice: 15.9
- },
- {
- id: '3',
- image: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-3.jpeg',
- title: '有机青苹果',
- desc: '无农药,健康食用',
- price: 9.9,
- originPrice: 12.9
- }
- ]);
-
- const onProductClick = (product) => {
- showToast(`点击了商品: ${product.title}`);
- };
-
- return {
- products,
- onProductClick
- };
- }
- }
- </script>
- <style>
- .product-list {
- padding: 12px;
- }
- </style>
复制代码
组件封装技巧
在实际开发中,我们经常需要对一些常用功能进行封装,下面是一个封装表单组件的示例:
- <!-- src/components/BaseForm.vue -->
- <template>
- <van-form @submit="onSubmit">
- <van-cell-group inset>
- <slot></slot>
- </van-cell-group>
- <div style="margin: 16px;">
- <van-button round block type="primary" native-type="submit">
- {{ submitText }}
- </van-button>
- </div>
- </van-form>
- </template>
- <script>
- import { defineProps, defineEmits } from 'vue';
- export default {
- name: 'BaseForm',
- props: {
- submitText: {
- type: String,
- default: '提交'
- }
- },
- emits: ['submit'],
- setup(props, { emit }) {
- const onSubmit = (values) => {
- emit('submit', values);
- };
-
- return {
- onSubmit
- };
- }
- }
- </script>
复制代码
使用封装的表单组件:
- <template>
- <base-form submit-text="登录" @submit="onLogin">
- <van-field
- v-model="username"
- name="username"
- label="用户名"
- placeholder="请输入用户名"
- :rules="[{ required: true, message: '请填写用户名' }]"
- />
- <van-field
- v-model="password"
- type="password"
- name="password"
- label="密码"
- placeholder="请输入密码"
- :rules="[{ required: true, message: '请填写密码' }]"
- />
- </base-form>
- </template>
- <script>
- import { ref } from 'vue';
- import BaseForm from '@/components/BaseForm.vue';
- import { showToast } from 'vant';
- export default {
- components: {
- BaseForm
- },
- setup() {
- const username = ref('');
- const password = ref('');
-
- const onLogin = (values) => {
- console.log('login form:', values);
- showToast('登录成功');
- };
-
- return {
- username,
- password,
- onLogin
- };
- }
- }
- </script>
复制代码
状态管理
Pinia的安装与配置
Pinia是Vue3官方推荐的状态管理库,它比Vuex更轻量、更直观。
安装Pinia:
在main.js中引入:
- import { createApp } from 'vue'
- import { createPinia } from 'pinia'
- import App from './App.vue'
- const app = createApp(App)
- app.use(createPinia())
- app.mount('#app')
复制代码
状态定义与使用
创建一个store:
- // src/store/counter.js
- import { defineStore } from 'pinia'
- export const useCounterStore = defineStore('counter', {
- state: () => ({
- count: 0,
- }),
- getters: {
- doubleCount: (state) => state.count * 2,
- },
- actions: {
- increment() {
- this.count++
- },
- decrement() {
- this.count--
- },
- reset() {
- this.count = 0
- },
- },
- })
复制代码
在组件中使用store:
- <template>
- <div>
- <h2>计数器: {{ count }}</h2>
- <h2>双倍计数: {{ doubleCount }}</h2>
- <button @click="increment">增加</button>
- <button @click="decrement">减少</button>
- <button @click="reset">重置</button>
- </div>
- </template>
- <script>
- import { storeToRefs } from 'pinia';
- import { useCounterStore } from '@/store/counter';
- export default {
- setup() {
- const counterStore = useCounterStore();
- // 使用storeToRefs解构state和getters,保持响应性
- const { count, doubleCount } = storeToRefs(counterStore);
- // 直接解构actions
- const { increment, decrement, reset } = counterStore;
-
- return {
- count,
- doubleCount,
- increment,
- decrement,
- reset
- };
- }
- }
- </script>
复制代码
异步操作
在Pinia中处理异步操作:
- // src/store/user.js
- import { defineStore } from 'pinia'
- import { login as userLogin, getUserInfo } from '@/api/user'
- export const useUserStore = defineStore('user', {
- state: () => ({
- token: '',
- userInfo: null,
- }),
- getters: {
- isLogin: (state) => !!state.token,
- userName: (state) => state.userInfo?.name || '',
- },
- actions: {
- // 登录
- async login(loginForm) {
- try {
- const { token } = await userLogin(loginForm)
- this.token = token
- return Promise.resolve()
- } catch (error) {
- return Promise.reject(error)
- }
- },
-
- // 获取用户信息
- async fetchUserInfo() {
- try {
- const userInfo = await getUserInfo()
- this.userInfo = userInfo
- return Promise.resolve(userInfo)
- } catch (error) {
- return Promise.reject(error)
- }
- },
-
- // 登出
- logout() {
- this.token = ''
- this.userInfo = null
- },
- },
- })
复制代码
在组件中使用:
- <template>
- <div v-if="isLogin">
- <h2>欢迎, {{ userName }}</h2>
- <button @click="handleLogout">登出</button>
- </div>
- <div v-else>
- <van-form @submit="handleLogin">
- <van-field
- v-model="loginForm.username"
- name="username"
- label="用户名"
- placeholder="请输入用户名"
- :rules="[{ required: true, message: '请填写用户名' }]"
- />
- <van-field
- v-model="loginForm.password"
- type="password"
- name="password"
- label="密码"
- placeholder="请输入密码"
- :rules="[{ required: true, message: '请填写密码' }]"
- />
- <div style="margin: 16px;">
- <van-button round block type="primary" native-type="submit">
- 登录
- </van-button>
- </div>
- </van-form>
- </div>
- </template>
- <script>
- import { ref } from 'vue';
- import { storeToRefs } from 'pinia';
- import { useUserStore } from '@/store/user';
- import { showToast } from 'vant';
- export default {
- setup() {
- const userStore = useUserStore();
- const { isLogin, userName } = storeToRefs(userStore);
- const { login, logout } = userStore;
-
- const loginForm = ref({
- username: '',
- password: ''
- });
-
- const handleLogin = async () => {
- try {
- await login(loginForm.value);
- await userStore.fetchUserInfo();
- showToast('登录成功');
- } catch (error) {
- showToast('登录失败');
- console.error(error);
- }
- };
-
- const handleLogout = () => {
- logout();
- showToast('已登出');
- };
-
- return {
- isLogin,
- userName,
- loginForm,
- handleLogin,
- handleLogout
- };
- }
- }
- </script>
复制代码
持久化存储
使用插件实现Pinia状态持久化:
- npm install pinia-plugin-persistedstate
复制代码
在main.js中配置:
- import { createApp } from 'vue'
- import { createPinia } from 'pinia'
- import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
- import App from './App.vue'
- const pinia = createPinia()
- pinia.use(piniaPluginPersistedstate)
- const app = createApp(App)
- app.use(pinia)
- app.mount('#app')
复制代码
在store中启用持久化:
- // src/store/user.js
- import { defineStore } from 'pinia'
- import { login as userLogin, getUserInfo } from '@/api/user'
- export const useUserStore = defineStore('user', {
- state: () => ({
- token: '',
- userInfo: null,
- }),
- getters: {
- isLogin: (state) => !!state.token,
- userName: (state) => state.userInfo?.name || '',
- },
- actions: {
- async login(loginForm) {
- try {
- const { token } = await userLogin(loginForm)
- this.token = token
- return Promise.resolve()
- } catch (error) {
- return Promise.reject(error)
- }
- },
-
- async fetchUserInfo() {
- try {
- const userInfo = await getUserInfo()
- this.userInfo = userInfo
- return Promise.resolve(userInfo)
- } catch (error) {
- return Promise.reject(error)
- }
- },
-
- logout() {
- this.token = ''
- this.userInfo = null
- },
- },
- // 启用持久化
- persist: {
- enabled: true,
- strategies: [
- {
- key: 'user',
- storage: localStorage,
- },
- ],
- },
- })
复制代码
路由管理
Vue Router的安装与配置
安装Vue Router:
创建路由配置文件:
- // src/router/index.js
- import { createRouter, createWebHistory } from 'vue-router'
- const routes = [
- {
- path: '/',
- name: 'Home',
- component: () => import('@/views/Home.vue'),
- meta: {
- title: '首页',
- keepAlive: true
- }
- },
- {
- path: '/category',
- name: 'Category',
- component: () => import('@/views/Category.vue'),
- meta: {
- title: '分类',
- keepAlive: true
- }
- },
- {
- path: '/cart',
- name: 'Cart',
- component: () => import('@/views/Cart.vue'),
- meta: {
- title: '购物车',
- keepAlive: false
- }
- },
- {
- path: '/user',
- name: 'User',
- component: () => import('@/views/User.vue'),
- meta: {
- title: '我的',
- keepAlive: true
- }
- },
- {
- path: '/product/:id',
- name: 'Product',
- component: () => import('@/views/Product.vue'),
- meta: {
- title: '商品详情',
- keepAlive: false
- }
- },
- {
- path: '/login',
- name: 'Login',
- component: () => import('@/views/Login.vue'),
- meta: {
- title: '登录',
- keepAlive: false
- }
- },
- {
- path: '/:pathMatch(.*)*',
- name: 'NotFound',
- component: () => import('@/views/NotFound.vue'),
- meta: {
- title: '页面不存在',
- keepAlive: false
- }
- }
- ]
- const router = createRouter({
- history: createWebHistory(),
- routes
- })
- // 全局前置守卫
- router.beforeEach((to, from, next) => {
- // 设置页面标题
- document.title = to.meta.title || 'Vue3+Vant App'
-
- // 登录权限控制
- const userStore = useUserStore()
- if (to.meta.requiresAuth && !userStore.isLogin) {
- next({ name: 'Login', query: { redirect: to.fullPath } })
- } else {
- next()
- }
- })
- export default router
复制代码
在main.js中引入路由:
- import { createApp } from 'vue'
- import { createPinia } from 'pinia'
- import App from './App.vue'
- import router from './router'
- const app = createApp(App)
- app.use(createPinia())
- app.use(router)
- app.mount('#app')
复制代码
路由定义与导航
在App.vue中使用路由:
- <template>
- <div class="app">
- <!-- 路由视图 -->
- <router-view v-slot="{ Component }">
- <keep-alive :include="keepAliveViews">
- <component :is="Component" />
- </keep-alive>
- </router-view>
-
- <!-- 底部导航 -->
- <van-tabbar v-model="active" route>
- <van-tabbar-item replace to="/" icon="home-o">首页</van-tabbar-item>
- <van-tabbar-item replace to="/category" icon="apps-o">分类</van-tabbar-item>
- <van-tabbar-item replace to="/cart" icon="cart-o">购物车</van-tabbar-item>
- <van-tabbar-item replace to="/user" icon="user-o">我的</van-tabbar-item>
- </van-tabbar>
- </div>
- </template>
- <script>
- import { ref, computed } from 'vue';
- import { useRoute } from 'vue-router';
- export default {
- setup() {
- const route = useRoute();
- const active = ref(0);
-
- // 需要缓存的页面
- const keepAliveViews = computed(() => {
- return route.matched
- .filter(item => item.meta.keepAlive)
- .map(item => item.name);
- });
-
- return {
- active,
- keepAliveViews
- };
- }
- }
- </script>
- <style>
- .app {
- padding-bottom: 50px;
- }
- </style>
复制代码
在组件中进行导航:
- <template>
- <div>
- <h2>首页</h2>
-
- <!-- 声明式导航 -->
- <van-button type="primary" to="/category">跳转到分类页</van-button>
-
- <!-- 编程式导航 -->
- <van-button type="primary" @click="goToCategory">跳转到分类页</van-button>
-
- <!-- 动态路由导航 -->
- <van-button type="primary" @click="goToProduct">跳转到商品详情</van-button>
-
- <!-- 带查询参数的导航 -->
- <van-button type="primary" @click="goToCategoryWithQuery">跳转到分类页(带参数)</van-button>
- </div>
- </template>
- <script>
- import { useRouter } from 'vue-router';
- export default {
- setup() {
- const router = useRouter();
-
- const goToCategory = () => {
- router.push('/category');
- };
-
- const goToProduct = () => {
- router.push({
- name: 'Product',
- params: {
- id: '123'
- }
- });
- };
-
- const goToCategoryWithQuery = () => {
- router.push({
- path: '/category',
- query: {
- id: '1',
- name: '电子产品'
- }
- });
- };
-
- return {
- goToCategory,
- goToProduct,
- goToCategoryWithQuery
- };
- }
- }
- </script>
复制代码
路由守卫
路由守卫用于控制路由的访问权限,Vue Router提供了全局守卫、路由独享守卫和组件内守卫。
- // src/router/index.js
- // 全局前置守卫
- router.beforeEach((to, from, next) => {
- // 设置页面标题
- document.title = to.meta.title || 'Vue3+Vant App'
-
- // 登录权限控制
- const userStore = useUserStore()
- if (to.meta.requiresAuth && !userStore.isLogin) {
- next({ name: 'Login', query: { redirect: to.fullPath } })
- } else {
- next()
- }
- })
- // 全局后置钩子
- router.afterEach((to, from) => {
- // 可以在这里进行页面滚动等操作
- window.scrollTo(0, 0)
- })
复制代码- // src/router/index.js
- const routes = [
- {
- path: '/admin',
- name: 'Admin',
- component: () => import('@/views/Admin.vue'),
- meta: {
- title: '管理后台',
- requiresAuth: true,
- requiresAdmin: true
- },
- beforeEnter: (to, from, next) => {
- const userStore = useUserStore()
- if (userStore.userInfo?.role !== 'admin') {
- next({ name: 'Home' })
- } else {
- next()
- }
- }
- }
- ]
复制代码- <template>
- <div>
- <h2>商品详情</h2>
- <p>商品ID: {{ productId }}</p>
- </div>
- </template>
- <script>
- import { ref, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue';
- import { useRoute, useRouter } from 'vue-router';
- export default {
- setup() {
- const route = useRoute();
- const router = useRouter();
- const productId = ref(route.params.id);
-
- // 组件内守卫:离开当前路由时
- onBeforeRouteLeave((to, from, next) => {
- // 如果表单有未保存的更改,可以提示用户
- if (hasUnsavedChanges()) {
- const answer = window.confirm(
- '您有未保存的更改,确定要离开吗?'
- )
- if (answer) {
- next()
- } else {
- next(false)
- }
- } else {
- next()
- }
- });
-
- // 组件内守卫:在当前路由改变,但是该组件被复用时调用
- onBeforeRouteUpdate((to, from, next) => {
- // 例如,从 /product/1 导航到 /product/2
- productId.value = to.params.id;
- fetchProductData(to.params.id);
- next();
- });
-
- const hasUnsavedChanges = () => {
- // 检查是否有未保存的更改
- return false;
- };
-
- const fetchProductData = (id) => {
- // 根据ID获取商品数据
- console.log('Fetching product data for ID:', id);
- };
-
- return {
- productId
- };
- }
- }
- </script>
复制代码
懒加载
懒加载可以按需加载页面组件,减少初始加载时间。在路由配置中,我们可以使用动态导入实现懒加载:
- // src/router/index.js
- const routes = [
- {
- path: '/',
- name: 'Home',
- component: () => import('@/views/Home.vue'),
- meta: {
- title: '首页',
- keepAlive: true
- }
- },
- {
- path: '/category',
- name: 'Category',
- component: () => import('@/views/Category.vue'),
- meta: {
- title: '分类',
- keepAlive: true
- }
- },
- {
- path: '/cart',
- name: 'Cart',
- component: () => import('@/views/Cart.vue'),
- meta: {
- title: '购物车',
- keepAlive: false
- }
- },
- {
- path: '/user',
- name: 'User',
- component: () => import('@/views/User.vue'),
- meta: {
- title: '我的',
- keepAlive: true
- }
- }
- ]
复制代码
我们还可以使用魔法注释为懒加载的组件命名,方便调试:
- const routes = [
- {
- path: '/',
- name: 'Home',
- component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue'),
- meta: {
- title: '首页',
- keepAlive: true
- }
- },
- {
- path: '/category',
- name: 'Category',
- component: () => import(/* webpackChunkName: "category" */ '@/views/Category.vue'),
- meta: {
- title: '分类',
- keepAlive: true
- }
- }
- ]
复制代码
项目优化
代码分割
代码分割是优化应用加载性能的重要手段。除了路由懒加载,我们还可以对第三方库和大型组件进行代码分割。
在vite.config.js中配置:
- import { defineConfig } from 'vite'
- import vue from '@vitejs/plugin-vue'
- import Components from 'unplugin-vue-components/vite'
- import { VantResolver } from 'unplugin-vue-components/resolvers'
- export default defineConfig({
- plugins: [
- vue(),
- Components({
- resolvers: [VantResolver()],
- }),
- ],
- build: {
- rollupOptions: {
- output: {
- manualChunks: {
- 'vendor': ['vue', 'vue-router', 'pinia'],
- 'vant': ['vant']
- }
- }
- }
- }
- })
复制代码
对于大型组件,我们可以使用动态导入实现按需加载:
- <template>
- <div>
- <h2>首页</h2>
- <button @click="showLargeComponent">加载大型组件</button>
-
- <div v-if="showComponent">
- <component :is="LargeComponent" />
- </div>
- </div>
- </template>
- <script>
- import { ref, defineAsyncComponent } from 'vue';
- export default {
- setup() {
- const showComponent = ref(false);
-
- // 异步加载大型组件
- const LargeComponent = defineAsyncComponent(() =>
- import('@/components/LargeComponent.vue')
- );
-
- const showLargeComponent = () => {
- showComponent.value = true;
- };
-
- return {
- showComponent,
- LargeComponent,
- showLargeComponent
- };
- }
- }
- </script>
复制代码
懒加载
Vant UI的Image组件已经内置了懒加载功能:
- <template>
- <van-image
- width="100"
- height="100"
- src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
- lazy-load
- />
- </template>
复制代码
使用Vant的List组件实现列表的懒加载:
- <template>
- <van-list
- v-model:loading="loading"
- :finished="finished"
- finished-text="没有更多了"
- @load="onLoad"
- >
- <van-cell v-for="item in list" :key="item" :title="item" />
- </van-list>
- </template>
- <script>
- import { ref } from 'vue';
- export default {
- setup() {
- const list = ref([]);
- const loading = ref(false);
- const finished = ref(false);
-
- const onLoad = () => {
- // 异步更新数据
- setTimeout(() => {
- for (let i = 0; i < 10; i++) {
- list.value.push(list.value.length + 1);
- }
- // 加载状态结束
- loading.value = false;
-
- // 数据全部加载完成
- if (list.value.length >= 40) {
- finished.value = true;
- }
- }, 1000);
- };
-
- return {
- list,
- loading,
- finished,
- onLoad,
- };
- },
- };
- </script>
复制代码
缓存策略
在vite.config.js中配置HTTP缓存:
- import { defineConfig } from 'vite'
- import vue from '@vitejs/plugin-vue'
- export default defineConfig({
- plugins: [vue()],
- build: {
- rollupOptions: {
- output: {
- chunkFileNames: 'static/js/[name]-[hash].js',
- entryFileNames: 'static/js/[name]-[hash].js',
- assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
- }
- }
- }
- })
复制代码
使用Workbox实现Service Worker缓存:
- npm install workbox-webpack-plugin -D
复制代码
在vite.config.js中配置:
- import { defineConfig } from 'vite'
- import vue from '@vitejs/plugin-vue'
- import { GenerateSW } from 'workbox-webpack-plugin'
- export default defineConfig({
- plugins: [
- vue(),
- {
- ...GenerateSW({
- clientsClaim: true,
- skipWaiting: true,
- runtimeCaching: [
- {
- urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
- handler: 'CacheFirst',
- options: {
- cacheName: 'image-cache',
- expiration: {
- maxEntries: 60,
- maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
- },
- },
- },
- {
- urlPattern: /\.(?:js|css)$/,
- handler: 'StaleWhileRevalidate',
- options: {
- cacheName: 'static-resources',
- expiration: {
- maxEntries: 60,
- maxAgeSeconds: 24 * 60 * 60, // 24 hours
- },
- },
- },
- ],
- }),
- apply: 'build'
- }
- ]
- })
复制代码
性能优化技巧
对于长列表,可以使用虚拟滚动来提高性能:
- <template>
- <van-list
- v-model:loading="loading"
- :finished="finished"
- finished-text="没有更多了"
- @load="onLoad"
- >
- <van-cell v-for="item in list" :key="item" :title="item" />
- </van-list>
- </template>
- <script>
- import { ref } from 'vue';
- export default {
- setup() {
- const list = ref([]);
- const loading = ref(false);
- const finished = ref(false);
-
- const onLoad = () => {
- // 异步更新数据
- setTimeout(() => {
- for (let i = 0; i < 10; i++) {
- list.value.push(list.value.length + 1);
- }
- // 加载状态结束
- loading.value = false;
-
- // 数据全部加载完成
- if (list.value.length >= 40) {
- finished.value = true;
- }
- }, 1000);
- };
-
- return {
- list,
- loading,
- finished,
- onLoad,
- };
- },
- };
- </script>
复制代码
对于频繁触发的事件,如滚动、输入等,可以使用防抖和节流来优化性能:
- // src/utils/debounce.js
- export function debounce(fn, delay) {
- let timer = null;
- return function (...args) {
- if (timer) clearTimeout(timer);
- timer = setTimeout(() => {
- fn.apply(this, args);
- }, delay);
- };
- }
- // src/utils/throttle.js
- export function throttle(fn, delay) {
- let last = 0;
- return function (...args) {
- const now = Date.now();
- if (now - last > delay) {
- fn.apply(this, args);
- last = now;
- }
- };
- }
复制代码
在组件中使用:
- <template>
- <div>
- <van-search
- v-model="searchValue"
- placeholder="请输入搜索关键词"
- @input="onSearch"
- />
-
- <div class="scroll-container" @scroll="onScroll">
- <!-- 滚动内容 -->
- </div>
- </div>
- </template>
- <script>
- import { ref } from 'vue';
- import { debounce, throttle } from '@/utils';
- export default {
- setup() {
- const searchValue = ref('');
-
- // 使用防抖处理搜索输入
- const onSearch = debounce((value) => {
- console.log('搜索:', value);
- // 执行搜索逻辑
- }, 300);
-
- // 使用节流处理滚动事件
- const onScroll = throttle((event) => {
- console.log('滚动位置:', event.target.scrollTop);
- // 执行滚动逻辑
- }, 200);
-
- return {
- searchValue,
- onSearch,
- onScroll
- };
- }
- }
- </script>
复制代码
对于非首屏的组件,可以使用Intersection Observer API实现懒加载:
- <template>
- <div>
- <h2>首页</h2>
-
- <div ref="lazyComponentContainer" class="lazy-container">
- <component :is="LazyComponent" v-if="showLazyComponent" />
- </div>
- </div>
- </template>
- <script>
- import { ref, onMounted, defineAsyncComponent } from 'vue';
- export default {
- setup() {
- const lazyComponentContainer = ref(null);
- const showLazyComponent = ref(false);
-
- // 异步加载懒加载组件
- const LazyComponent = defineAsyncComponent(() =>
- import('@/components/LazyComponent.vue')
- );
-
- onMounted(() => {
- const observer = new IntersectionObserver((entries) => {
- entries.forEach(entry => {
- if (entry.isIntersecting) {
- showLazyComponent.value = true;
- observer.unobserve(entry.target);
- }
- });
- }, {
- threshold: 0.1
- });
-
- if (lazyComponentContainer.value) {
- observer.observe(lazyComponentContainer.value);
- }
- });
-
- return {
- lazyComponentContainer,
- showLazyComponent,
- LazyComponent
- };
- }
- }
- </script>
- <style>
- .lazy-container {
- min-height: 200px;
- margin-top: 500px;
- }
- </style>
复制代码
SEO优化
对于SPA应用,SEO是一个挑战。我们可以使用以下方法来优化SEO:
使用prerender-spa-plugin进行预渲染:
- npm install prerender-spa-plugin -D
复制代码
在vite.config.js中配置:
- import { defineConfig } from 'vite'
- import vue from '@vitejs/plugin-vue'
- import path from 'path'
- export default defineConfig({
- plugins: [
- vue(),
- {
- name: 'prerender',
- closeBundle() {
- const PrerenderSPAPlugin = require('prerender-spa-plugin');
- const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
-
- new PrerenderSPAPlugin({
- staticDir: path.join(__dirname, 'dist'),
- routes: ['/', '/about', '/contact'],
- renderer: new Renderer({
- headless: true,
- renderAfterDocumentEvent: 'render-event'
- })
- }).apply();
- }
- }
- ]
- })
复制代码
在main.js中添加:
- import { createApp } from 'vue'
- import App from './App.vue'
- const app = createApp(App)
- app.mount('#app')
- // 添加渲染事件
- document.dispatchEvent(new Event('render-event'))
复制代码
使用vue-meta管理页面的meta标签:
在main.js中配置:
- import { createApp } from 'vue'
- import { createMetaManager } from 'vue-meta'
- import App from './App.vue'
- const app = createApp(App)
- app.use(createMetaManager())
- app.mount('#app')
复制代码
在组件中使用:
- <template>
- <div>
- <h2>关于我们</h2>
- <p>这是关于我们的页面</p>
- </div>
- </template>
- <script>
- import { useMeta } from 'vue-meta';
- export default {
- setup() {
- useMeta({
- title: '关于我们 - Vue3+Vant App',
- meta: [
- { name: 'description', content: '这是关于我们的页面描述' },
- { name: 'keywords', content: '关于我们,公司介绍' },
- { property: 'og:title', content: '关于我们 - Vue3+Vant App' }
- ]
- });
-
- return {};
- }
- }
- </script>
复制代码
项目部署
构建生产版本
使用Vite构建生产版本:
构建完成后,会在项目根目录下生成一个dist文件夹,包含了所有生产环境所需的文件。
部署到服务器
将dist文件夹中的内容上传到服务器的Web目录,如Nginx的html目录。
配置Nginx:
- server {
- listen 80;
- server_name yourdomain.com;
- root /usr/share/nginx/html;
- index index.html;
-
- location / {
- try_files $uri $uri/ /index.html;
- }
-
- # 静态资源缓存
- location ~* \.(?:css|js)$ {
- expires 1y;
- add_header Cache-Control "public, immutable";
- }
-
- # 图片资源缓存
- location ~* \.(?:jpg|jpeg|gif|png|webp|svg|ico)$ {
- expires 1y;
- add_header Cache-Control "public";
- }
- }
复制代码
使用Express作为静态文件服务器:
- // server.js
- const express = require('express');
- const path = require('path');
- const app = express();
- // 静态文件服务
- app.use(express.static(path.join(__dirname, 'dist')));
- // 所有路由都返回index.html
- app.get('*', (req, res) => {
- res.sendFile(path.join(__dirname, 'dist', 'index.html'));
- });
- const port = process.env.PORT || 3000;
- app.listen(port, () => {
- console.log(`Server is running on port ${port}`);
- });
复制代码
安装依赖并启动:
- npm install express
- node server.js
复制代码
CDN配置
将静态资源上传到CDN,可以加快访问速度。在vite.config.js中配置:
- import { defineConfig } from 'vite'
- import vue from '@vitejs/plugin-vue'
- export default defineConfig({
- plugins: [vue()],
- base: 'https://cdn.yourdomain.com/your-project/',
- build: {
- rollupOptions: {
- output: {
- chunkFileNames: 'static/js/[name]-[hash].js',
- entryFileNames: 'static/js/[name]-[hash].js',
- assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
- }
- }
- }
- })
复制代码
域名配置
配置HTTPS:
- server {
- listen 443 ssl http2;
- server_name yourdomain.com;
-
- ssl_certificate /path/to/your/certificate.crt;
- ssl_certificate_key /path/to/your/private.key;
-
- root /usr/share/nginx/html;
- index index.html;
-
- location / {
- try_files $uri $uri/ /index.html;
- }
- }
- # HTTP重定向到HTTPS
- server {
- listen 80;
- server_name yourdomain.com;
- return 301 https://$server_name$request_uri;
- }
复制代码
总结与展望
本文详细介绍了如何使用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官方文档
希望本文能够帮助您成为移动开发领域的专家,祝您在移动应用开发的道路上取得更大的成功! |
|