ai软件开发全栈工程师
文章目录
- 一、今日学习内容概览
- 二、Pinia 状态管理:Vue 生态的官方推荐
- 三、Axios 请求封装:打造企业级 HTTP 客户端
- 四、Vue 3 进阶特性
- 五、前端工程化:ESLint + Prettier + 环境变量
- 六、今日踩坑记录
- 七、收获总结
- 八、明日学习计划
第三阶段的第二天,今天的目标是把前端开发的"基础设施"打牢。昨天学会了 Vue 3 的核心语法和组件化,但一个真实的企业级项目远不止这些——你需要管理全局状态(Pinia)、与后端 API 通信(Axios)、处理跨层级数据传递(Provide/Inject)、以及保证代码质量和可维护性(ESLint/Prettier)。
今天的学习让我深刻体会到:前端工程化的复杂度,丝毫不亚于后端。从状态管理到请求拦截,从代码规范到环境配置,每一个环节都直接影响着团队协作效率和项目的长期可维护性。
一、今日学习内容概览
- Pinia 状态管理:Store 定义与使用、State/Getters/Actions、模块化拆分、持久化存储
- Axios 请求封装:实例创建、请求/响应拦截器、统一错误处理、与后端 Spring Boot 联调
- Vue 3 进阶特性:Provide/Inject 跨层级通信、Teleport 传送门、异步组件与 Suspense
- 前端工程化:ESLint 代码规范、Prettier 格式化、环境变量配置(.env)、Vite 路径别名
阶段里程碑:
今天是第三阶段第 2 天 / 共 20 天。经过今天的学习,"智能写手"前端项目的基础设施已初步搭建完成:状态管理、API 通信、代码规范三件套全部就位。
二、Pinia 状态管理:Vue 生态的官方推荐
在 Vue 2 时代,Vuex 是状态管理的事实标准。但 Vuex 的 mutations 和 actions 分离设计、以及类型支持不佳的问题,一直让开发者诟病。Vue 官方在 Vue 3 时代推出了 Pinia——一个更轻量、更直观、TypeScript 友好的状态管理库,现在已经成为 Vue 生态的推荐方案。
2.1 安装与 Store 定义
Bash终端
# 安装 Pinia
npm install pinia
# 如果需持久化存储(页面刷新不丢失)
npm install pinia-plugin-persistedstate
JavaScriptmain.js — 注册 Pinia
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.mount('#app')
2.2 定义一个完整的 Store
Pinia 的 Store 定义非常直观——就是一个函数,返回 state、getters 和 actions。没有 mutations,直接通过 actions 修改 state,更符合直觉。
JavaScriptstores/article.js — 文章状态管理
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { fetchArticles, createArticle, deleteArticleById } from '@/api/article'
// 定义 Store:第一个参数是 ID,第二个是 setup 函数
export const useArticleStore = defineStore('article', () => {
// ========== State ==========
const articles = ref([])
const loading = ref(false)
const error = ref(null)
const filter = ref({ status: 'all', keyword: '' })
// ========== Getters ==========
const publishedArticles = computed(() => {
return articles.value.filter(a => a.status === 'PUBLISHED')
})
const filteredArticles = computed(() => {
let result = articles.value
if (filter.value.status !== 'all') {
result = result.filter(a => a.status === filter.value.status)
}
if (filter.value.keyword) {
result = result.filter(a =>
a.title.includes(filter.value.keyword)
)
}
return result
})
const totalWordCount = computed(() => {
return articles.value.reduce((sum, a) => sum + (a.wordCount || 0), 0)
})
// ========== Actions ==========
async function loadArticles() {
loading.value = true
error.value = null
try {
const data = await fetchArticles()
articles.value = data
} catch (err) {
error.value = err.message
console.error('加载文章失败:', err)
} finally {
loading.value = false
}
}
async function addArticle(articleData) {
const newArticle = await createArticle(articleData)
articles.value.unshift(newArticle)
return newArticle
}
function removeArticle(id) {
articles.value = articles.value.filter(a => a.id !== id)
return deleteArticleById(id)
}
function setFilter(newFilter) {
filter.value = { ...filter.value, ...newFilter }
}
function $reset() {
articles.value = []
loading.value = false
error.value = null
filter.value = { status: 'all', keyword: '' }
}
return {
articles, loading, error, filter,
publishedArticles, filteredArticles, totalWordCount,
loadArticles, addArticle, removeArticle, setFilter, $reset
}
}, {
// 持久化配置:刷新页面后状态不丢失
persist: {
key: 'article-store',
paths: ['filter'] // 只持久化 filter,articles 从接口重新加载
}
})
2.3 在组件中使用 Store
Vue SFCviews/ArticleListView.vue
<template>
<div class="article-list">
<div class="toolbar">
<input
v-model="articleStore.filter.keyword"
placeholder="搜索文章..."
@input="articleStore.setFilter({ keyword: $event.target.value })"
/>
<span class="stats">
共 {{ articleStore.filteredArticles.length }} 篇 / 总字数 {{ articleStore.totalWordCount }}
</span>
</div>
<div v-if="articleStore.loading" class="loading">加载中...</div>
<div v-else-if="articleStore.error" class="error">{{ articleStore.error }}</div>
<ArticleCard
v-for="article in articleStore.filteredArticles"
:key="article.id"
:article="article"
@delete="articleStore.removeArticle"
/>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
import { useArticleStore } from '@/stores/article'
import ArticleCard from '@/components/ArticleCard.vue'
const articleStore = useArticleStore()
onMounted(() => {
if (articleStore.articles.length === 0) {
articleStore.loadArticles()
}
})
</script>
Pinia vs Vuex 核心区别:
1. 没有 mutations:直接通过 actions 修改 state,更符合直觉。
2. TypeScript 友好:天生支持类型推断,无需额外配置。
3. 更轻量:核心代码仅约 1KB,API 设计更简洁。
4. 模块化更简单:每个 Store 独立定义,无需手动注册模块。
三、Axios 请求封装:打造企业级 HTTP 客户端
Axios 是前端最流行的 HTTP 客户端库。但在真实项目中,直接使用 axios.get() 是远远不够的——你需要统一处理请求头、错误提示、Loading 状态、Token 刷新、请求重试等。今天我把 Axios 封装成了一个可复用的模块。
3.1 基础封装:创建 Axios 实例
JavaScriptutils/request.js — Axios 实例与拦截器
import axios from 'axios'
import { useUserStore } from '@/stores/user'
import { ElMessage } from 'element-plus'
// 创建 axios 实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// ========== 请求拦截器 ==========
request.interceptors.request.use(
(config) => {
// 从 Pinia Store 获取 token(注意:要在请求时动态获取,不能缓存)
const userStore = useUserStore()
const token = userStore.token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 添加时间戳防止缓存
if (config.method === 'get') {
config.params = { ...config.params, _t: Date.now() }
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// ========== 响应拦截器 ==========
request.interceptors.response.use(
(response) => {
// 直接返回业务数据(脱壳)
const res = response.data
// 如果后端统一包装了响应格式 { code, data, message }
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败')
return Promise.reject(new Error(res.message))
}
return res.data
},
(error) => {
// 统一错误处理
const { response } = error
if (response) {
switch (response.status) {
case 401:
ElMessage.error('登录已过期,请重新登录')
// 清除 token 并跳转到登录页
const userStore = useUserStore()
userStore.logout()
window.location.href = '/login'
break
case 403:
ElMessage.error('没有权限执行此操作')
break
case 404:
ElMessage.error('请求的资源不存在')
break
case 500:
ElMessage.error('服务器内部错误')
break
default:
ElMessage.error(response.data?.message || '网络异常')
}
} else if (error.code === 'ECONNABORTED') {
ElMessage.error('请求超时,请检查网络')
} else {
ElMessage.error('网络连接失败')
}
return Promise.reject(error)
}
)
export default request
3.2 API 模块拆分
按照业务模块拆分 API 文件,每个模块一个文件,清晰可维护。
JavaScriptapi/article.js — 文章相关接口
import request from '@/utils/request'
export const fetchArticles = (params) => {
return request.get('/articles', { params })
}
export const fetchArticleById = (id) => {
return request.get(`/articles/${id}`)
}
export const createArticle = (data) => {
return request.post('/articles', data)
}
export const updateArticle = (id, data) => {
return request.put(`/articles/${id}`, data)
}
export const deleteArticleById = (id) => {
return request.delete(`/articles/${id}`)
}
// 文件上传(Content-Type 为 multipart/form-data)
export const uploadImage = (file) => {
const formData = new FormData()
formData.append('file', file)
return request.post('/upload/image', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
3.3 与 Spring Boot 后端联调
前后端联调时,跨域是最常见的问题。昨天在 Vite 中配置了代理,今天补充后端的 CORS 配置。
JavaSpring Boot 跨域配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
};
}
}
四、Vue 3 进阶特性
4.1 Provide / Inject:跨层级组件通信
当组件层级很深时,props 逐层传递(prop drilling)会变得非常繁琐。Provide/Inject 允许祖先组件向后代组件注入依赖,无论中间隔了多少层。
Vue SFCApp.vue — 提供全局主题配置
<script setup>
import { provide, readonly } from 'vue'
// 提供响应式数据(后代组件可 inject 使用)
const themeConfig = ref({
primaryColor: '#06d6a0',
darkMode: true,
fontSize: 14
})
// 用 readonly 包裹,防止后代直接修改(只能通过提供的方法修改)
provide('themeConfig', readonly(themeConfig))
// 提供修改方法
const updateTheme = (config) => {
themeConfig.value = { ...themeConfig.value, ...config }
}
provide('updateTheme', updateTheme)
</script>
Vue SFCcomponents/DeepChild.vue — 注入使用
<script setup>
import { inject } from 'vue'
// 注入祖先提供的数据
const themeConfig = inject('themeConfig')
const updateTheme = inject('updateTheme')
// 提供默认值(防止祖先未 provide 时报错)
const userInfo = inject('userInfo', { name: '访客', role: 'guest' })
</script>
<template>
<div :style="{ color: themeConfig.primaryColor }">
当前主题色: {{ themeConfig.primaryColor }}
</div>
<button @click="updateTheme({ primaryColor: '#f59e0b' })">
切换为琥珀色主题
</button>
</template>
Provide/Inject 使用建议:
适合全局配置、主题、用户认证信息等跨多层级共享的数据。但不要滥用——如果只是父子通信,还是用 props/emits 更清晰。Inject 的数据不具备响应式追踪的透明性,调试时可能增加心智负担。
4.2 Teleport:将组件渲染到 DOM 其他位置
Teleport 允许你把组件的 HTML 渲染到当前组件 DOM 树之外的位置。最常见的场景是 Modal 弹窗、Toast 通知——这些元素需要跳出父容器的 z-index 和 overflow 限制。
Vue SFCcomponents/Modal.vue — Teleport 示例
<template>
<!-- 将内容传送到 body 末尾,跳出父容器限制 -->
<Teleport to="body">
<Transition name="fade">
<div v-if="visible" class="modal-overlay" @click="closeOnOverlay && close()">
<div class="modal-content" @click.stop>
<header>
<h3>{{ title }}</h3>
<button class="close-btn" @click="close">×</button>
</header>
<div class="modal-body">
<slot></slot>
</div>
<footer v-if="$slots.footer">
<slot name="footer"></slot>
</footer>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
defineProps({
visible: { type: Boolean, required: true },
title: { type: String, default: '提示' },
closeOnOverlay: { type: Boolean, default: true }
})
const emit = defineEmits(['update:visible'])
const close = () => {
emit('update:visible', false)
}
</script>
4.3 Suspense:异步组件加载状态
Suspense 是 Vue 3 提供的内置组件,用于在异步依赖(如异步组件、async setup)解析时显示 fallback 内容。
Vue SFCviews/DashboardView.vue — Suspense 示例
<template>
<div class="dashboard">
<h2>数据面板</h2>
<!-- Suspense 包裹异步组件 -->
<Suspense>
<!-- 默认插槽:异步组件加载完成后显示 -->
<template #default>
<AsyncChart />
<AsyncStatsTable />
</template>
<!-- fallback 插槽:加载中显示 -->
<template #fallback>
<div class="skeleton-loading">
<div class="skeleton-chart"></div>
<div class="skeleton-table"></div>
</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
// 异步加载组件(代码分割,按需加载)
const AsyncChart = defineAsyncComponent(() =>
import('@/components/Chart.vue')
)
const AsyncStatsTable = defineAsyncComponent(() =>
import('@/components/StatsTable.vue')
)
</script>
五、前端工程化:ESLint + Prettier + 环境变量
5.1 ESLint 代码规范配置
ESLint 是 JavaScript/TypeScript 的静态代码分析工具,可以在编码阶段发现潜在错误和风格问题。
Bash终端
# 安装 ESLint 及 Vue 插件
npm install -D eslint eslint-plugin-vue @vue/eslint-config-prettier
# 初始化配置(生成 eslint.config.js)
npm init @eslint/config@latest
JavaScripteslint.config.js
import js from '@eslint/js'
import pluginVue from 'eslint-plugin-vue'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
export default [
{
name: 'app/files-to-lint',
files: ['**/*.{js,mjs,jsx,vue}'],
},
{
name: 'app/files-to-ignore',
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
},
js.configs.recommended,
...pluginVue.configs['flat/essential'],
{
rules: {
// 自定义规则
'vue/multi-word-component-names': 'off', // 允许单单词组件名
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
}
},
skipFormatting,
]
5.2 Prettier 代码格式化
JSON.prettierrc
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"arrowParens": "avoid",
"endOfLine": "lf",
"vueIndentScriptAndStyle": true
}
JSONpackage.json — 脚本配置
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .js,.vue --fix",
"format": "prettier --write \"src/**/*.{js,vue,css,html}\""
}
}
5.3 环境变量配置
Vite 使用 dotenv 加载环境变量,文件名格式为 .env.[mode]。
Text.env.development — 开发环境
# 必须以 VITE_ 前缀开头才能被客户端代码访问
VITE_API_BASE_URL=http://localhost:8080/api
VITE_APP_TITLE=AI 智能写手(开发版)
VITE_UPLOAD_MAX_SIZE=10485760
Text.env.production — 生产环境
VITE_API_BASE_URL=https://api.smartwriter.com/api
VITE_APP_TITLE=AI 智能写手
VITE_UPLOAD_MAX_SIZE=5242880
JavaScript代码中使用环境变量
// 访问环境变量
console.log(import.meta.env.VITE_API_BASE_URL)
console.log(import.meta.env.VITE_APP_TITLE)
// 判断当前模式
if (import.meta.env.DEV) {
console.log('当前是开发环境')
}
// 所有可用的环境变量
const envConfig = {
baseURL: import.meta.env.VITE_API_BASE_URL,
title: import.meta.env.VITE_APP_TITLE,
maxSize: parseInt(import.meta.env.VITE_UPLOAD_MAX_SIZE),
mode: import.meta.env.MODE, // "development" | "production"
dev: import.meta.env.DEV, // boolean
prod: import.meta.env.PROD // boolean
}
5.4 Vite 路径别名配置
JavaScriptvite.config.js — 路径别名
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@views': path.resolve(__dirname, './src/views'),
'@stores': path.resolve(__dirname, './src/stores'),
'@api': path.resolve(__dirname, './src/api'),
'@utils': path.resolve(__dirname, './src/utils'),
}
},
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})
路径别名 + VS Code 智能提示:
配置 jsconfig.json 或 tsconfig.json 中的 compilerOptions.paths,让 VS Code 能够识别别名并提供自动导入提示。
六、今日踩坑记录
坑 1:Pinia Store 在组件外调用时获取不到实例
现象:在 Axios 拦截器中调用 useUserStore() 报错 "getActivePinia was called with no active Pinia"。
原因:Pinia 需要在 Vue 应用实例化之后才能使用。如果在模块顶层直接调用 useUserStore(),此时 Pinia 还未挂载。
解决方案:在拦截器内部动态获取 Store,而不是在模块顶层缓存实例:
// ❌ 错误:模块顶层缓存
const userStore = useUserStore() // 报错!
// ✅ 正确:在函数内部动态获取
request.interceptors.request.use((config) => {
const userStore = useUserStore() // 此时 Pinia 已就绪
const token = userStore.token
...
})
坑 2:Axios 拦截器返回数据脱壳后,TypeScript 类型丢失
现象:在响应拦截器中直接返回 response.data.data,导致调用方无法获得正确的类型推断。
原因:拦截器修改了返回值的结构,但 Axios 的泛型类型定义没有同步更新。
解决方案:为 Axios 实例添加类型声明,或使用泛型封装请求方法:
// 封装带类型的请求方法
export const request = <T>(config) => {
return axiosInstance.request<T, T>(config) // 双重泛型:响应类型 + 返回类型
}
// 使用
interface Article { id: number; title: string }
const article = await request<Article>({ url: '/articles/1' })
// article 类型为 Article,而非 AxiosResponse<Article>
坑 3:Provide 提供的 ref 对象在 Inject 后失去响应式
现象:祖先组件 provide 了一个 ref,后代组件 inject 后在模板中使用,但数据更新时视图不刷新。
原因:如果 provide 的是 ref.value 而不是 ref 本身,后代拿到的是原始值,没有响应式能力。
解决方案:provide ref 本身(或 reactive 对象),后代通过 .value 访问:
// ❌ 错误:提供了原始值
provide('count', count.value)
// ✅ 正确:提供 ref 本身
provide('count', count)
// 后代组件
const count = inject('count')
console.log(count.value) // 保持响应式
坑 4:ESLint 和 Prettier 规则冲突导致格式化后报 lint 错误
现象:保存文件时 Prettier 自动格式化为单引号,但 ESLint 报错要求双引号;或者 Prettier 添加分号,ESLint 禁止分号。
原因:ESLint 和 Prettier 都有代码风格规则,两者规则不一致时就会冲突。
解决方案:安装 @vue/eslint-config-prettier 或 eslint-config-prettier,它会关闭 ESLint 中与 Prettier 冲突的规则,让 Prettier 全权负责代码格式,ESLint 专注于代码质量检查:
// eslint.config.js 中
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
export default [
// ...其他配置
skipFormatting, // 放在最后,覆盖冲突规则
]
坑 5:Vite 环境变量必须以 VITE_ 开头,否则获取不到
现象:在 .env 文件中定义了 API_KEY=xxx,但在代码中 import.meta.env.API_KEY 返回 undefined。
原因:Vite 为了安全考虑,默认只暴露以 VITE_ 开头的环境变量给客户端代码。
解决方案:环境变量名前加 VITE_ 前缀;或者在 vite.config.js 中配置 envPrefix 自定义前缀:
// vite.config.js
export default defineConfig({
envPrefix: 'APP_', // 现在 APP_ 开头的变量也会被暴露
})
// .env
APP_API_KEY=your-secret-key
七、收获总结
今日知识地图
今天的内容是前端工程化的"基础设施大建设"——从状态管理到网络请求,从代码规范到环境配置。如果说昨天学的是 Vue 3 的"语法",那今天学的就是"工程实践"。
1. Pinia 的简洁哲学。相比 Vuex 的 mutations/actions 分离,Pinia 直接通过 actions 修改 state,API 设计更符合直觉。配合 pinia-plugin-persistedstate 实现状态持久化,配合 setup 语法糖实现完整的类型推导——这套组合已经让我彻底告别 Vuex 了。
2. Axios 拦截器的威力。以前写前端项目时,每个请求都要手动加 token、手动处理错误提示。封装了拦截器之后,这些逻辑全部自动化了。特别是统一错误处理——401 跳转登录、403 提示权限不足、500 提示服务器错误——用户体验提升了一个档次。
3. Provide/Inject 的适用边界。这个特性让我想起了后端开发中的"上下文传递"。适合主题配置、全局用户状态等"穿透多层组件"的场景,但不适合普通父子通信——过度使用会让数据流变得难以追踪。
4. 工程化是团队协作的基石。ESLint + Prettier 的配置花了将近一个小时,但一旦配好,整个项目的代码风格就统一了。环境变量的规范使用,让开发/测试/生产三套配置清晰分离。这些"看不见的工作",恰恰是区分"玩具项目"和"企业级项目"的关键。
明天将进入第三阶段更硬核的内容——开始搭建"智能写手"前端项目的页面框架,包括登录页、文章列表页、创作页的 UI 实现,以及 Element Plus 组件库的使用。
八、明日学习计划
Day 10 · 2026-07-12 — Element Plus 组件库与页面开发实战
- Element Plus 组件库:安装与主题定制、常用组件(Table/Form/Dialog/Menu)、图标使用
- 登录页面开发:表单验证、JWT Token 存储、路由守卫集成
- 文章列表页:ElTable 分页与排序、搜索过滤、批量操作
- 创作页面:富文本编辑器集成、Markdown 预览、AI 生成调用
更多推荐

所有评论(0)