技术栈选用 Vue3 + Vite5 + Pinia + Vant4 + Sass
源码地址:
javascript
git clone https://gitee.com/gaiya001/h5-APP.git
1. 1.vite.config.js文件配置
**
javascript
import { defineConfig } from 'vite' // 导入 Vite 的配置函数
import vue from '@vitejs/plugin-vue' // 导入 Vue 插件
import path from 'path' // 导入 Node.js 的 path 模块
import Components from 'unplugin-vue-components/vite' // 导入 Vue 组件自动注册插件
import { VantResolver } from 'unplugin-vue-components/resolvers' // 导入 Vant 组件解析器
import { visualizer } from 'rollup-plugin-visualizer' // 导入 Rollup 打包分析插件
import { execSync } from 'child_process' // 导入 Node.js 的 child_process 模块
export default defineConfig({
base: '/', // 设置应用的基础路径
server: {
port: 3000, // 设置开发服务器端口
open: true, // 启动开发服务器时自动打开浏览器
proxy: {
'/api': {
target: 'https://baidu', // 设置代理的目标地址
changeOrigin: true, // 改变请求的源头
rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径
},
},
},
plugins: [
vue(), // 使用 Vue 插件
Components({
resolvers: [VantResolver()], // 使用 Vant 组件解析器自动注册 Vant 组件
}),
visualizer({ open: true }), // 使用打包分析工具并在打包后自动打开分析报告
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'), // 设置路径别名,@ 指向 src 目录
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'axios', 'vant'], // 手动分割 vendor 包,包含 vue, axios, vant
},
},
},
chunkSizeWarningLimit: 500, // 设置警告阈值,单位 KB
brotliSize: false, // 关闭 Brotli 压缩大小报告
},
optimizeDeps: {
include: ['vue', 'axios', 'vant'], // 预构建依赖,提高构建速度
},
// 添加打包完成后清除 log 文件的钩子
buildEnd: () => {
try {
execSync('rimraf logs'); // 使用 rimraf 清除 logs 目录
// console.log('日志文件已清除');
} catch (error) {
// console.error('清除日志文件失败:', error);
}
},
})
2.main.js文件配置详解
javascript
// 导入 Vue 的 createApp 函数,用于创建 Vue 应用实例
import { createApp } from 'vue';
// 导入根组件 App.vue
import App from './App.vue';
// 引入路由配置
import router from './router/index.js';
// 引入路由权限配置
import '@/router/permission.js';
// 引入 Vant 的样式文件
import 'vant/lib/index.css';
// 引入全局样式文件(使用 SCSS)
import './assets/index.sass';
// 引入 Pinia 实例
import pinia from './store/index.js';
// 创建 Vue 应用实例
const app = createApp(App);
// 使用路由插件
app.use(router);
// 使用 Pinia 插件
app.use(pinia);
// 将 Pinia 实例注册为全局属性(可选)
// app.config.globalProperties.$store = pinia;
// 挂载应用到 DOM 中的 #app 元素
app.mount('#app');
## ```3.axios-req.js文件详解
```javascript
// 导入 axios 库,用于发送 HTTP 请求
import axios from 'axios';
// 从 Vant 库中导入 Toast 和 Dialog 组件,用于显示提示信息和对话框
import { Toast, Dialog } from 'vant';
// 从 Pinia 存储中导入 basic store,用于管理全局状态
import { useBasicStore } from '@/store/basic';
// 创建 axios 请求实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_URL, // 设置基础 URL,从环境变量中获取
timeout: 12000 // 设置请求超时时间为 12000 毫秒
});
let loadingInstance = null; // 用于存储 loading 提示实例
let tempReqUrlSave = ''; // 临时保存请求 URL
let authorTipDoor = true; // 控制授权提示的开关
// 定义一个函数,用于处理未授权的情况
const noAuthDill = () => {
authorTipDoor = false; // 关闭授权提示开关
Dialog.confirm({
message: '请重新登录', // 提示信息
confirmButtonText: '重新登录', // 确认按钮文本
closeOnClickOverlay: false, // 点击遮罩层不关闭对话框
showCancelButton: false, // 不显示取消按钮
showClose: false, // 不显示关闭按钮
theme: 'round-button' // 对话框主题
}).then(() => {
useBasicStore().resetStateAndToLogin(); // 重置状态并跳转到登录页面
authorTipDoor = true; // 重新打开授权提示开关
});
};
// 请求前拦截器
service.interceptors.request.use(
(req) => {
const { token, axiosPromiseArr } = useBasicStore();
// 收集请求地址,用于取消请求
req.cancelToken = new axios.CancelToken((cancel) => {
tempReqUrlSave = req.url;
axiosPromiseArr.push({
url: req.url,
cancel
});
});
// 设置 token 到请求头
if (token) req.headers['X-Auth-Token'] = token;
// 如果请求方法是 GET 并且没有 params,使用 data 作为 params
if (req.method?.toLowerCase() === 'get' && !req.params) req.params = req.data;
// 请求加载提示
if (req.reqLoading ?? true) {
loadingInstance = Toast.loading({
forbidClick: true, // 禁止点击
duration: 0, // 持续时间,0 表示持续显示
message: '数据载入中...' // 提示信息
});
}
console.log('Request:', req); // 添加请求日志
return req;
},
(err) => {
console.error('Request Error:', err); // 添加请求错误日志
return Promise.reject(err);
}
);
// 请求后拦截器
service.interceptors.response.use(
(res) => {
// 取消请求
useBasicStore().remotePromiseArrByReqUrl(tempReqUrlSave);
if (loadingInstance) {
Toast.clear(); // 清除 loading 提示
}
// 处理文件下载
if (res.data?.type?.includes("sheet")) {
return res;
}
const { code, msg } = res.data;
const successCode = [0, 200, 20000]; // 成功状态码
const noAuthCode = [401, 403]; // 未授权状态码
if (successCode.includes(code)) {
console.log('Response:', res); // 添加响应日志
return res.data;
} else {
// authorTipDoor 防止多个请求多次提示
if (authorTipDoor) {
if (noAuthCode.includes(code)) {
noAuthDill(); // 处理未授权情况
} else {
if (!res.config?.isNotTipErrorMsg) {
Toast.fail({
message: msg || '请求失败', // 提示信息
duration: 2 * 1000 // 持续时间 2 秒
});
} else {
return res;
}
return Promise.reject(msg || '请求失败');
}
}
}
},
(err) => {
// 取消请求
useBasicStore().remotePromiseArrByReqUrl(tempReqUrlSave);
if (loadingInstance) {
Toast.clear(); // 清除 loading 提示
}
console.error('Response Error:', err); // 添加响应错误日志
Toast.fail({
message: err.message || '网络请求失败', // 提示信息
duration: 2 * 1000 // 持续时间 2 秒
});
return Promise.reject(err);
}
);
// 手动取消请求
export function cancelRequest(url) {
const { axiosPromiseArr } = useBasicStore();
const index = axiosPromiseArr.findIndex(item => item.url === url);
if (index !== -1) {
axiosPromiseArr[index].cancel(); // 取消请求
axiosPromiseArr.splice(index, 1); // 从数组中移除已取消的请求
}
}
// 导出 service 实例给页面调用,config -> 页面的配置
export default function axiosReq(config) {
return service(config);
}
4.路由permission.js文件配置
javascript
import router from './index'; // 引入主路由模块
import { useUserStore } from '@/store/modules/user'; // pinia持久化的信息
const whiteList = ['/login']; // 不需要鉴权的路由白名单
router.beforeEach(async (to, from, next) => {
console.log(to,"路由前置守望");
const userStore = useUserStore();
// 设置页面标题
document.title = to.meta?.title;
// 获取用户信息
const { token } = userStore.userInfo;
console.log(token,'token')
// 如果用户已经登录,则直接放行
if (token) {
next();
return;
}
// 如果用户未登录并且目标路由不在白名单中
if (!token && !whiteList.includes(to.path)) {
// 重定向到登录页,并携带当前路由路径作为查询参数
next(`/login?redirect=${to.path}`);
} else {
// 目标路由在白名单中,直接放行
next();
}
});
// 路由后置守卫
router.afterEach((to, from) => {
// console.log('路由后置守望', to, from);
});
export default router;