创建基于 Vue3 且将 request.js(请求封装)与页面 .vue 解耦的项目

创建基于 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 页面完全解耦,符合 "单一职责" 原则,后期维护和扩展会非常方便。

相关推荐
LYFlied21 小时前
Vue3虚拟DOM更新机制源码深度解析
前端·算法·面试·vue·源码解读
@AfeiyuO1 天前
Vue3 玫瑰图
vue·echarts
running up3 天前
Pinia 完整使用指南
vue
安_3 天前
<style scoped>跟<style>有什么区别
前端·vue
辛-夷3 天前
TS封装axios
前端·vue.js·typescript·vue·axios
@AfeiyuO3 天前
Vue3 矩形树图
vue·echarts
weixin_422555424 天前
ezuikit-js官网使用示例
前端·javascript·vue·ezuikit-js
zhz52144 天前
代码之恋(第十五篇:分布式心跳与网络延迟)
网络·分布式·ai·重构·vue·结对编程
我看刑4 天前
【已解决】el-table 前端分页多选、跨页全选等
前端·vue·element