创建基于 Vue3 且将 request.js(请求封装)与页面 .vue 解耦的项目,核心思路是把请求逻辑抽离为独立模块,页面仅调用封装好的接口方法,而非直接操作 axios 或请求配置。以下是完整的实现步骤:
一、基础项目搭建
先通过 vite 快速创建 Vue3 项目:
bash
# 初始化项目
npm create vite@latest my-vue3-project -- --template vue
cd my-vue3-project
npm install
# 安装核心依赖(axios必装)
npm install axios # 网络请求库
npm install pinia # 可选,状态管理(非必须,但解耦更彻底)
npm install js-cookie # 可选,处理token(如登录态)
找一个空目录,输入cmd:

用 Vite 创建 Vue3 项目的初始化流程,当前提示是需要先安装create-vite@8.2.0这个工具包,输入y

这是 Vite 创建项目时的实验性工具选择提示,意思是 "是否使用rolldown-vite(实验性版本)?"。
rolldown-vite是 Vite 的实验性打包工具(替代默认的 Rollup),目前还处于测试阶段,稳定性不如正式版;
这里默认选了No(不使用),这是更稳妥的选择(适合大多数项目)。
直接按回车键确认选择No即可,后续会进入正常的 Vue 项目创建流程(比如选择 Vue 版本、是否用 TypeScript 等)。

这个提示是问你:"是否现在用 npm 安装依赖并启动项目?"
选Yes:系统会自动执行npm install安装项目依赖,然后启动开发服务器(可以直接打开项目页面);
选No:只创建项目文件,不自动安装依赖和启动,后续需要手动执行npm install和npm run dev。
如果是刚创建项目,直接按回车选Yes更方便,能快速进入开发环境。

这个报错是 端口 5173 被系统拒绝访问(权限不足),导致 Vite 开发服务器启动失败,具体原因是:
Error: listen EACCES: permission denied :::5173:5173 是 Vite 默认的开发端口,但当前系统用户没有使用这个端口的权限(常见于 Windows 系统的权限限制,或端口被其他程序占用)。

方法 :换一个端口启动项目
先进入项目目录:
bash
cd my-vue3-project
用指定端口启动(比如换 8080 端口):
bash
npm run dev -- --port 8080


问题原因:
- 默认端口差异 :Vite(Vue 3 官方构建工具)默认使用端口 5173,而不是 Vue CLI(Vue 2 时代)的默认端口
8080; - IPv6 权限问题 :错误信息 Error: listen EACCES: permission denied ::1:5173 表明在IPv6 地址 ::1 (即 localhost 的 IPv6 版本)上使用端口 5173 时遇到权限问题。
解决方法:
修改 vite.config.js 文件,添加服务器配置:
bash
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path' // 需导入path模块
export default defineConfig({
plugins: [vue()],
resolve: {
// 配置@别名(确保映射到src目录)
alias: {
'@': path.resolve(__dirname, 'src')
}
},
server: {
port: 8080 // 可选:固定端口,避免自动切换
}
})
二、目录结构设计(解耦请求层)
推荐的目录结构(按 "请求封装→接口管理→页面调用" 分层):
src/
├── api/ # 请求层(与页面/路由完全隔离)
│ ├── request.js # axios封装(拦截器、基础配置)
│ ├── modules/ # 业务接口模块(用户、商品)
│ │ ├── user.js
│ │ └── goods.js
│ └── index.js # 接口统一导出
├── router/ # 路由层(管理页面跳转,不碰请求逻辑)
│ └── index.js # 路由配置(映射路径→页面组件)
├── views/ # 页面层(仅调用api+配合路由,不写请求/路由配置)
│ ├── Login.vue # 登录页面
│ ├── GoodsList.vue # 商品列表页面
│ ├── GoodsDetail.vue # 商品详情页面
│ └── Home.vue # 首页
├── App.vue # 根组件(放路由出口)
└── main.js # 入口文件(挂载路由+全局配置)

三、路由依赖
- 步骤 1:安装路由依赖
首先安装 Vue Router(Vue3 配套的路由库):
bash
npm install vue-router@4
- 步骤 2:配置路由(router/index.js)
路由层只负责 "路径→页面" 的映射,不涉及任何请求逻辑,保证与请求层解耦:
bash
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// 替换原有静态导入 → 改为懒加载(按需导入)
// 首页/登录页可保留静态导入(首屏核心页面),非首屏页面懒加载
import Login from '@/views/Login.vue'
import Home from '@/views/Home.vue'
// 商品相关页面改为懒加载(访问时才加载)
const GoodsList = () => import('@/views/GoodsList.vue')
const GoodsDetail = () => import('@/views/GoodsDetail.vue')
// 路由规则:路径对应页面(component值不变,因为变量名还是GoodsList/GoodsDetail)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/goods/list',
name: 'GoodsList',
component: GoodsList // 依然用变量名,无需改
},
{
path: '/goods/detail/:id',
name: 'GoodsDetail',
component: GoodsDetail // 依然用变量名,无需改
}
]
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// 路由守卫保持不变
router.beforeEach((to, from, next) => {
const isLogin = localStorage.getItem('token')
if (to.path !== '/login' && !isLogin) {
next('/login')
} else {
next()
}
})
export default router
- 步骤 3:挂载路由到项目(main.js)
bash
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router' // 导入路由
const app = createApp(App)
app.use(router) // 挂载路由
app.mount('#app')
- 步骤 4:根组件添加路由出口(App.vue)
bash
<template>
<!-- 路由出口:所有页面都会渲染到这里 -->
<router-view></router-view>
</template>
<script setup>
// 根组件仅放路由出口,无任何业务逻辑
</script>
四、封装 request.js(基础请求配置)
在 src/api/request.js 中封装 axios,包含请求拦截、响应拦截、错误处理、基础配置,与页面完全解耦:
bash
// src/api/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router' // 仅在拦截器中跳转路由,不影响核心请求逻辑
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
// 请求拦截器:添加token(与路由无关)
service.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器:统一错误处理(仅在401时跳登录页,路由仅作为跳转工具)
service.interceptors.response.use(
(res) => res.data,
(error) => {
if (error.response?.status === 401) {
ElMessage.error('登录失效,请重新登录')
localStorage.removeItem('token')
router.push('/login') // 仅跳转,不写其他路由逻辑
}
ElMessage.error(error.message || '请求失败')
return Promise.reject(error)
}
)
export default service
解析 import axios from 'axios' 这行代码,需要导入 axios 库,Vite 安装 axios:
bash
# 安装axios(版本兼容Vue3/Vite)
npm install axios --save
request.js 还导入了 element-plus(ElMessage),如果没装这个库,后续也会报同样的导入错误,建议一起安装:
bash
# 安装element-plus(Vue3的UI库,提供ElMessage提示)
npm install element-plus --save
同时,需要在 main.js 中全局注册 element-plus(否则 ElMessage 用不了):
bash
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
// 新增:导入element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' // 导入样式
const app = createApp(App)
app.use(router)
app.use(ElementPlus) // 注册element-plus
app.mount('#app')

五、按业务模块拆分接口(解耦请求逻辑与页面)
在 src/api/modules/ 下按业务拆分接口,每个模块仅调用 request.js 的实例,不直接暴露给页面:
示例 1:用户模块接口(src/api/modules/user.js)
bash
// src/api/modules/user.js
import request from '../request'
// 登录接口
export const login = (params) => {
return request({
url: '/user/login', // 接口路径(拼接baseURL)
method: 'post',
data: params // 登录参数(账号、密码)
})
}
// 获取用户信息
export const getUserInfo = () => {
return request({
url: '/user/info',
method: 'get'
})
}
示例 2:商品模块接口(src/api/modules/goods.js)
bash
// src/api/modules/goods.js
import request from '../request'
// 商品列表接口(仅请求逻辑,不碰路由/页面)
export const getGoodsList = (params) => {
return request({
url: '/goods/list',
method: 'get',
params
})
}
// 商品详情接口
export const getGoodsDetail = (id) => {
return request({
url: `/goods/detail/${id}`,
method: 'get'
})
}
六、统一导出所有接口(页面仅需导入这个文件)
在 src/api/index.js 中汇总所有业务模块的接口,页面只需导入此文件,无需关心具体请求路径 / 方法:
bash
// src/api/index.js
// 导入所有业务模块接口
import * as userApi from './modules/user'
import * as goodsApi from './modules/goods'
// 统一导出(页面使用时:import { userApi, goodsApi } from '@/api')
export {
userApi,
goodsApi
}
七、页面.vue 中调用接口(完全解耦,仅调用方法)
页面组件中不写任何 axios 配置,仅导入 api/index.js 中的方法,实现请求逻辑与页面解耦:
示例 1:登录页面(Login.vue)→ 调用接口 + 路由跳转
bash
<template>
<div class="login">
<input v-model="form.username" placeholder="账号" />
<input v-model="form.password" type="password" placeholder="密码" />
<button @click="handleLogin">登录</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router' // 导入路由钩子
import { userApi } from '@/api' // 导入请求接口(不碰request.js)
const router = useRouter()
const form = ref({ username: '', password: '' })
// 登录逻辑:仅调用接口+路由跳转,无请求配置
const handleLogin = async () => {
try {
const res = await userApi.login(form.value)
localStorage.setItem('token', res.token)
router.push('/') // 登录成功跳首页(仅跳转,不配置路由规则)
} catch (err) {
console.error('登录失败:', err)
}
}
</script>
示例 2:商品列表页面(GoodsList.vue)→ 调用接口 + 跳转详情页
bash
<template>
<div class="goods-list">
<div v-for="item in goodsList" :key="item.id" @click="goDetail(item.id)">
{{ item.name }}
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { goodsApi } from '@/api' // 仅导入封装好的接口
const router = useRouter()
const goodsList = ref([])
// 加载商品列表:仅调用接口,无请求逻辑
onMounted(async () => {
const res = await goodsApi.getGoodsList({ page: 1, size: 10 })
goodsList.value = res.list
})
// 跳商品详情页:仅传参跳转,不修改路由配置
const goDetail = (id) => {
router.push(`/goods/detail/${id}`)
// 或用命名路由(更规范):
// router.push({ name: 'GoodsDetail', params: { id } })
}
</script>
示例 3:商品详情页(GoodsDetail.vue)→ 接收参数 + 调用接口
bash
<template>
<div class="goods-detail">
<h1>{{ goodsInfo.name }}</h1>
<p>{{ goodsInfo.desc }}</p>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router' // 接收路由参数
import { goodsApi } from '@/api'
const route = useRoute()
const goodsInfo = ref({})
// 加载详情:获取路由参数+调用接口
onMounted(async () => {
const goodsId = route.params.id // 接收路由传的商品ID
const res = await goodsApi.getGoodsDetail(goodsId)
goodsInfo.value = res
})
</script>
示例 4:首页(Home.vue)→ 简写
bash
<!-- 最简单的合法 Home.vue -->
<template>
<div>首页内容</div>
</template>
<!-- 或者只加script(无模板) -->
<script setup>
// 首页逻辑
console.log('首页加载')
</script>

八、环境变量配置(解耦不同环境的接口地址)
在项目根目录创建 .env.development(开发环境)和 .env.production(生产环境),统一管理接口基地址,避免硬编码:
.env.development
bash
# 开发环境接口地址
VITE_API_BASE_URL = 'http://localhost:8080/api'
.env.production
bash
# 生产环境接口地址
VITE_API_BASE_URL = 'https://prod-api.example.com/api'
九、核心解耦优势
请求逻辑集中管理:修改接口路径、添加拦截器、调整请求头,只需改 request.js 或 api/modules,无需修改所有页面;
页面专注业务逻辑:页面仅关心 "调用哪个接口、处理返回数据",不关心请求底层实现;
便于维护和复用:多个页面可复用同一接口方法,避免重复写请求代码;
环境隔离:通过环境变量切换接口地址,开发 / 生产环境无需改代码。
十、进阶优化(可选)
添加请求取消机制:防止页面切换后重复请求(如列表页快速切换页码);
接口缓存:对高频不变的接口(如商品分类)添加缓存,减少请求;
TypeScript 支持:给接口返回值定义类型,页面调用时获得类型提示;
全局加载状态:通过 Pinia 管理请求加载状态,统一控制页面 loading。
通过以上方式,request.js 与 .vue 页面完全解耦,符合 "单一职责" 原则,后期维护和扩展会非常方便。