首页
直播
壁纸
友链
搜索
1
微信小程序支付全链路实战:JSAPI 下单、调起支付、回调验签与退款
266 阅读
2
微信小程序云开发实战:云函数、云数据库与云存储的正确使用姿势
256 阅读
3
微信小程序自定义 tabBar 实战:custom-tab-bar 从适配到深色模式
255 阅读
4
微信小程序 Skyline 渲染引擎实战:worklet 动画从原理到落地
253 阅读
5
微信小程序分包进阶:独立分包、预下载与分包异步化实战
246 阅读
服务器运维
后端技术
前端技术
梯子
数据库
小程序
登录
搜索
标签搜索
fastadmin
Redis
微信小程序
前端开发
RabbitMQ
Go
服务器
codex
buildadmin
小程序
mysql
Nginx
Docker
Vue3
Node.js
MySQL优化
Linux
TypeScript
JWT
PHP
沿途的风景
累计撰写
74
篇文章
累计收到
0
条评论
首页
栏目
服务器运维
后端技术
前端技术
梯子
数据库
小程序
页面
直播
壁纸
友链
搜索到
11
篇与
» Vue3
的结果
2026-07-05
Vue 3 + Vite 工程化实战:从零搭建企业级项目
Vite 已成为 Vue 项目的标配构建工具。本文从零搭建一个包含目录规范、请求封装、权限路由、Mock 数据、环境配置和 CI/CD 的企业级 Vue 3 项目。一、项目初始化创建项目# 创建 Vue 3 项目 npm create vue@latest my-app # 交互选择: # ✓ TypeScript # ✓ JSX Support # ✓ Router # ✓ Pinia # ✓ Vitest # ✓ ESLint # ✓ Prettier cd my-app npm install安装核心依赖# UI 组件库(按需选择) npm install element-plus npm install -D unplugin-vue-components unplugin-auto-import # HTTP 请求 npm install axios # 实用工具 npm install dayjs lodash-es @vueuse/core # 图表(按需) npm install echarts二、目录结构规范my-app/ ├── public/ # 静态资源(不参与构建) ├── src/ │ ├── api/ # API 接口定义 │ │ ├── modules/ │ │ │ ├── user.js │ │ │ ├── order.js │ │ │ └── product.js │ │ └── index.js # 统一导出 │ ├── assets/ # 静态资源(参与构建) │ │ ├── images/ │ │ └── styles/ │ ├── components/ # 全局公共组件 │ │ ├── AppTable/ │ │ │ └── index.vue │ │ └── AppDialog/ │ ├── composables/ # 组合式函数 │ │ ├── useAuth.js │ │ ├── usePagination.js │ │ └── useRequest.js │ ├── config/ # 项目配置 │ │ └── index.js │ ├── constants/ # 常量定义 │ │ └── index.js │ ├── directives/ # 自定义指令 │ │ ├── permission.js │ │ └── lazy.js │ ├── layouts/ # 布局组件 │ │ ├── DefaultLayout.vue │ │ └── BlankLayout.vue │ ├── plugins/ # 插件配置 │ │ └── elementPlus.js │ ├── router/ # 路由 │ │ ├── index.js │ │ ├── guards.js │ │ └── routes/ │ │ ├── modules/ # 路由模块 │ │ └── index.js │ ├── stores/ # Pinia Store │ │ ├── user.js │ │ ├── permission.js │ │ └── app.js │ ├── styles/ # 全局样式 │ │ ├── variables.scss │ │ ├── mixins.scss │ │ └── index.scss │ ├── utils/ # 工具函数 │ │ ├── request.js # axios 封装 │ │ ├── storage.js │ │ └── format.js │ ├── views/ # 页面组件 │ │ ├── login/ │ │ ├── dashboard/ │ │ └── system/ │ ├── App.vue │ └── main.js ├── .env # 所有环境通用 ├── .env.development # 开发环境 ├── .env.production # 生产环境 ├── .env.staging # 预发布环境 ├── vite.config.js └── package.json三、Vite 配置详解// vite.config.js import { fileURLToPath, URL } from 'node:url' import { defineConfig, loadEnv } from 'vite' import vue from '@vitejs/plugin-vue' import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd()) return { plugins: [ vue(), // 自动导入 Vue API AutoImport({ imports: ['vue', 'vue-router', 'pinia'], resolvers: [ElementPlusResolver()], dts: 'src/auto-imports.d.js' }), // 组件自动注册 Components({ resolvers: [ElementPlusResolver()], dts: 'src/components.d.js' }) ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, css: { preprocessorOptions: { scss: { additionalData: `@use "@/styles/variables.scss" as *;` } } }, server: { port: 3000, open: true, proxy: { '/api': { target: env.VITE_API_BASE_URL, changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } }, build: { // 输出目录 outDir: 'dist', // 静态资源分包 rollupOptions: { output: { manualChunks: { 'vendor-vue': ['vue', 'vue-router', 'pinia'], 'vendor-ui': ['element-plus'], 'vendor-utils': ['lodash-es', 'dayjs'] }, // 分包体积警告阈值 chunkFileNames: 'assets/js/[name]-[hash].js', entryFileNames: 'assets/js/[name]-[hash].js', assetFileNames: 'assets/[ext]/[name]-[hash].[ext]' } }, // chunk 大小警告 chunkSizeWarningLimit: 1000, // 生产环境移除 console minify: 'terser', terserOptions: { compress: { drop_console: env.VITE_DROP_CONSOLE === 'true', drop_debugger: true } } } } })四、环境变量配置# .env — 通用配置 VITE_APP_TITLE=企业管理系统 # .env.development VITE_API_BASE_URL=http://localhost:8080 VITE_DROP_CONSOLE=false # .env.production VITE_API_BASE_URL=https://api.example.com VITE_DROP_CONSOLE=true// src/config/index.js export const config = { title: import.meta.env.VITE_APP_TITLE, apiBaseUrl: import.meta.env.VITE_API_BASE_URL, tokenKey: 'access_token', tokenExpireKey: 'token_expire' }五、Axios 封装// src/utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' import { useUserStore } from '@/stores/user' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 15000 }) // 请求拦截器 service.interceptors.request.use( (config) => { const userStore = useUserStore() // 携带 token if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } // GET 请求防缓存 if (config.method === 'get') { config.params = { ...config.params, _t: Date.now() } } return config }, (error) => Promise.reject(error) ) // 响应拦截器 let isRefreshing = false let pendingRequests = [] service.interceptors.response.use( (response) => { const { code, data, message } = response.data // 业务状态码判断 switch (code) { case 200: return data case 401: handleUnauthorized() return Promise.reject(new Error(message)) default: ElMessage.error(message || '请求失败') return Promise.reject(new Error(message)) } }, async (error) => { const { response } = error if (response) { switch (response.status) { case 401: handleUnauthorized() 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) } ) // Token 过期处理 async function handleUnauthorized() { const userStore = useUserStore() if (!isRefreshing) { isRefreshing = true try { await userStore.refreshToken() // 重放失败请求 pendingRequests.forEach(cb => cb()) pendingRequests = [] } catch { userStore.logout() router.push('/login') } finally { isRefreshing = false } } return new Promise((resolve) => { pendingRequests.push(() => resolve(service(error.config))) }) } export default service// src/api/modules/user.js import request from '@/utils/request' export const userApi = { login: (data) => request.post('/auth/login', data), logout: () => request.post('/auth/logout'), getProfile: () => request.get('/user/profile'), updateProfile: (data) => request.put('/user/profile', data), changePassword: (data) => request.put('/user/password', data), // 分页列表 getUsers: (params) => request.get('/users', { params }), createUser: (data) => request.post('/users', data), updateUser: (id, data) => request.put(`/users/${id}`, data), deleteUser: (id) => request.delete(`/users/${id}`) }六、路由与权限// src/router/index.js import { createRouter, createWebHistory } from 'vue-router' import { setupGuards } from './guards' // 基础路由 — 无需权限 export const constantRoutes = [ { path: '/login', name: 'Login', component: () => import('@/views/login/index.vue'), meta: { title: '登录' } }, { path: '/', component: () => import('@/layouts/DefaultLayout.vue'), redirect: '/dashboard', children: [ { path: 'dashboard', name: 'Dashboard', component: () => import('@/views/dashboard/index.vue'), meta: { title: '仪表盘', icon: 'Odometer' } } ] }, { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/404.vue') } ] const router = createRouter({ history: createWebHistory(), routes: constantRoutes }) setupGuards(router) export default router// src/router/guards.js import { useUserStore } from '@/stores/user' import { usePermissionStore } from '@/stores/permission' const whiteList = ['/login'] export function setupGuards(router) { router.beforeEach(async (to, from, next) => { document.title = `${to.meta.title || ''} - 系统名称` const userStore = useUserStore() if (userStore.token) { if (to.path === '/login') { next({ path: '/' }) } else { // 未加载用户信息和动态路由 if (!userStore.user) { try { // 获取用户信息 await userStore.getProfile() // 生成动态路由 const permissionStore = usePermissionStore() const accessRoutes = await permissionStore.generateRoutes() // 动态添加路由 accessRoutes.forEach(route => router.addRoute(route)) // 重新导航,确保路由已注册 next({ ...to, replace: true }) } catch (error) { userStore.resetToken() next(`/login?redirect=${to.path}`) } } else { next() } } } else { // 白名单直接放行 if (whiteList.includes(to.path)) { next() } else { next(`/login?redirect=${to.path}`) } } }) }// src/stores/permission.js import { defineStore } from 'pinia' import api from '@/api' // 视图模块映射 const viewModules = import.meta.glob('@/views/**/*.vue') export const usePermissionStore = defineStore('permission', () => { const routes = ref([]) async function generateRoutes() { // 从后端获取菜单权限 const menus = await api.menu.getUserMenus() // 转换后端菜单为路由 const asyncRoutes = transformRoutes(menus) routes.value = asyncRoutes return asyncRoutes } function transformRoutes(menus, parentPath = '') { return menus.map(menu => { const route = { path: menu.path.startsWith('/') ? menu.path : `${parentPath}/${menu.path}`, name: menu.name, meta: { title: menu.title, icon: menu.icon }, children: [] } // 组件解析 const componentPath = menu.component || 'layouts/DefaultLayout' route.component = viewModules[`/src/views/${componentPath}.vue`] if (menu.children?.length) { route.children = transformRoutes(menu.children, route.path) } return route }) } return { routes, generateRoutes } })七、Mock 数据// vite.config.js 增加 mock 插件 import { viteMockServe } from 'vite-plugin-mock' plugins: [ vue(), viteMockServe({ mockPath: 'mock', localEnabled: true }) ]// mock/user.js export default [ { url: '/api/user/profile', method: 'get', response: () => { return { code: 200, data: { id: 1, name: '张三', roles: ['admin'], permissions: ['system:user:list', 'system:user:create'] } } } }, { url: '/api/users', method: 'get', response: ({ query }) => { const { page = 1, pageSize = 10 } = query const list = Array.from({ length: 50 }, (_, i) => ({ id: i + 1, name: `用户${i + 1}`, email: `user${i + 1}@example.com`, status: i % 2 })) return { code: 200, data: { list: list.slice((page - 1) * pageSize, page * pageSize), total: list.length } } } } ]八、代码规范// eslint.config.js (Flat Config) import js from '@eslint/js' import vue from 'eslint-plugin-vue' import prettier from 'eslint-config-prettier' export default [ js.configs.recommended, ...vue.configs['flat/recommended'], prettier, { rules: { 'vue/multi-word-component-names': 'off', 'no-console': 'warn', 'no-debugger': 'warn' } } ]// .prettierrc { "semi": false, "singleQuote": true, "printWidth": 100, "trailingComma": "none", "arrowParens": "always" }九、Git 提交规范与 CI// commitlint.config.js export default { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [2, 'always', [ 'feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore' ]] } }# .github/workflows/ci.yml name: CI on: push: branches: [main, develop] pull_request: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install run: npm ci - name: Lint run: npm run lint - name: Test run: npm run test:unit - name: Build run: npm run build - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: dist path: dist十、小结企业级 Vue 项目的工程化核心:规范的目录结构让团队协作有章可循,统一的请求封装处理认证与错误,动态路由实现权限控制,Mock 让前后端并行开发,ESLint + CI 保证代码质量。这套脚手架可以直接作为新项目的起点。本文由 inspirecl.asia 原创,转载请注明出处。
2026年07月05日
4 阅读
0 评论
0 点赞
2026-07-04
Vue 3 组件进阶:性能优化与最佳实践
从"能跑"到"跑得快"是前端工程的分水岭。本文从渲染机制出发,系统讲解 Vue 3 的性能优化手段:虚拟列表、组件缓存、懒加载、防抖节流,以及性能分析工具的完整使用。一、性能优化总览性能优化层次 │ ├── 网络层 — 资源体积、加载策略 │ ├── 路由懒加载 │ ├── 组件异步加载 │ └── 图片/资源优化 │ ├── 渲染层 — 减少不必要的渲染 │ ├── v-show vs v-if │ ├── v-memo 缓存 │ ├── KeepAlive 组件缓存 │ └── 虚拟列表 │ ├── 计算层 — 减少计算量 │ ├── computed 缓存 │ ├── 防抖节流 │ └── Web Worker │ └── 监控层 — 持续度量 ├── Performance 面板 ├── Vue DevTools └── 自定义性能指标二、渲染优化v-if vs v-show<template> <!-- v-if:真正移除/创建 DOM,切换开销大 --> <HeavyComponent v-if="show" /> <!-- v-show:只切换 display,初始开销大 --> <HeavyComponent v-show="show" /> <!-- 选择原则: 频繁切换 → v-show 条件很少变化 → v-if --> </template>v-memo 缓存子树<script setup> import { ref } from 'vue' const items = ref([...10000 条数据]) const selectedId = ref(null) </script> <template> <!-- v-memo 依赖数组不变时跳过整个列表的 diff --> <div v-for="item in items" :key="item.id" v-memo="[item.id === selectedId]"> <p>{{ item.name }}</p> <p>{{ item.description }}</p> <!-- ... 复杂内容 ... --> </div> </template>KeepAlive 组件缓存<script setup> import { ref } from 'vue' import TabA from './TabA.vue' import TabB from './TabB.vue' import TabC from './TabC.vue' const currentTab = ref('A') const tabs = { A: TabA, B: TabB, C: TabC } </script> <template> <!-- 缓存组件实例,切换时保留状态,避免重新创建 --> <KeepAlive include="TabA,TabB" :max="10"> <component :is="tabs[currentTab]" /> </KeepAlive> </template><!-- TabA.vue --> <script setup> import { onActivated, onDeactivated } from 'vue' onActivated(() => { // 组件从缓存激活时(如刷新数据) refreshData() }) onDeactivated(() => { // 组件被缓存时(如清除定时器) clearInterval(timer) }) </script>异步组件与 Suspense<script setup> import { defineAsyncComponent } from 'vue' // 基本异步组件 const HeavyChart = defineAsyncComponent(() => import('./components/HeavyChart.vue') ) // 带加载和错误状态 const AdminPanel = defineAsyncComponent({ loader: () => import('./components/AdminPanel.vue'), loadingComponent: LoadingSpinner, errorComponent: ErrorDisplay, delay: 200, // 200ms 后显示 loading timeout: 10000 // 10s 超时显示错误 }) </script> <template> <Suspense> <template #default> <AsyncDashboard /> </template> <template #fallback> <LoadingSpinner /> </template> </Suspense> </template>虚拟列表(大数据量必备)npm install @tanstack/vue-virtual<script setup> import { ref } from 'vue' import { useVirtualizer } from '@tanstack/vue-virtual' const parentRef = ref(null) const rows = ref(Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` }))) const virtualizer = useVirtualizer({ count: rows.value.length, getScrollElement: () => parentRef.value, estimateSize: () => 48, // 每行预估高度 overscan: 5, // 预渲染缓冲行数 }) </script> <template> <div ref="parentRef" style="height: 600px; overflow: auto;"> <div :style="{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }"> <div v-for="row in virtualizer.getVirtualItems()" :key="rows[row.index].id" :style="{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${row.size}px`, transform: `translateY(${row.start}px)` }"> {{ rows[row.index].name }} </div> </div> </div> </template>三、计算与交互优化防抖与节流// composables/useDebounce.js import { customRef } from 'vue' // 防抖 ref export function useDebouncedRef(value, delay = 300) { let timeout return customRef((track, trigger) => ({ get() { track() return value }, set(newValue) { clearTimeout(timeout) timeout = setTimeout(() => { value = newValue trigger() }, delay) } })) }<script setup> import { useDebouncedRef } from '@/composables/useDebounce' // 搜索输入防抖 — 300ms 内的连续输入只触发一次请求 const keyword = useDebouncedRef('', 300) // watch keyword 触发搜索 watchEffect(async () => { if (keyword.value) { const results = await search(keyword.value) searchResults.value = results } }) </script> <template> <input v-model="keyword" placeholder="搜索..." /> </template>深度 watch 的替代方案// ❌ 深度监听大对象 — 开销大 watch(bigObject, handler, { deep: true }) // ✅ 精确监听目标属性 watch(() => bigObject.targetProp, handler) // ✅ 用 shallowRef + 整体替换 const list = shallowRef([]) list.value = [...list.value, newItem] // 触发更新 // 修改内部属性不触发(性能需要时) // ✅ 数组方法触发 list.value.push(item) // 会触发(Vue 3 拦截数组方法)Web Worker 处理重计算// workers/dataProcessor.js self.onmessage = (e) => { const { data, type } = e.data let result switch (type) { case 'SORT': result = heavySort(data) break case 'STATS': result = calculateStats(data) break } self.postMessage({ result }) } function heavySort(arr) { return [...arr].sort((a, b) => a.value - b.value) }// composables/useWorker.js export function useDataProcessor() { const worker = new Worker( new URL('../workers/dataProcessor.js', import.meta.url), { type: 'module' } ) const result = ref(null) const processing = ref(false) worker.onmessage = (e) => { result.value = e.data.result processing.value = false } function process(type, data) { processing.value = true worker.postMessage({ type, data }) } onUnmounted(() => worker.terminate()) return { result, processing, process } }四、加载优化路由懒加载// router/index.js import { createRouter } from 'vue-router' const routes = [ { path: '/', component: () => import('../views/Home.vue') // 懒加载 }, { path: '/admin', component: () => import(/* webpackChunkName: "admin" */ '../views/Admin.vue'), // Vite: 单独分包 // component: () => import('../views/Admin.vue'), meta: { requiresAuth: true } } ] // 按组分包(Vite) // vite.config.js export default { build: { rollupOptions: { output: { manualChunks: { 'vendor-vue': ['vue', 'vue-router', 'pinia'], 'vendor-ui': ['element-plus'], 'vendor-chart': ['echarts'] } } } } }图片优化<template> <!-- 懒加载图片 --> <img v-lazy="item.image" :alt="item.title" /> <!-- 响应式图片 --> <picture> <source media="(max-width: 768px)" :srcset="item.imageMobile"> <source media="(max-width: 1200px)" :srcset="item.imageTablet"> <img :src="item.imageDesktop" :alt="item.title" loading="lazy"> </picture> </template> <script setup> // 自定义图片懒加载指令 const vLazy = { mounted(el, binding) { const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { el.src = binding.value observer.unobserve(el) } }) observer.observe(el) } } </script>骨架屏<template> <div v-if="loading" class="skeleton-card"> <div class="skeleton-avatar"></div> <div class="skeleton-lines"> <div class="skeleton-line w-80"></div> <div class="skeleton-line w-60"></div> <div class="skeleton-line w-40"></div> </div> </div> <div v-else class="content"> <!-- 真实内容 --> </div> </template> <style> .skeleton-card { display: flex; gap: 16px; padding: 16px; } .skeleton-avatar { width: 64px; height: 64px; border-radius: 50%; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; } .skeleton-line { height: 16px; border-radius: 4px; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; margin-bottom: 8px; } .w-80 { width: 80%; } .w-60 { width: 60%; } .w-40 { width: 40%; } @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } </style>五、性能分析工具Vue DevTools 性能面板Vue DevTools → Timeline / Performance - 组件渲染耗时 - 事件处理耗时 - 响应式依赖追踪 Component Inspector: - 查看组件的 props、state、computed - 高亮重新渲染的组件Performance API 自定义测量// main.js — Vue 性能追踪 const app = createApp(App) // 开启性能追踪(开发模式) app.config.performance = true // 自定义性能标记 performance.mark('app-init-start') app.mount('#app') performance.mark('app-init-end') performance.measure('应用初始化', 'app-init-start', 'app-init-end') // 组件级测量 // MyComponent.vue onMounted(() => { performance.mark(`${instance.uid}-mounted`) }) onBeforeMount(() => { performance.mark(`${instance.uid}-before-mount`) })Web Vitals 监控// 监控核心 Web 指标 import { onLCP, onFID, onCLS, onINP, onTTFB } from 'web-vitals' function sendToAnalytics(metric) { const body = JSON.stringify({ name: metric.name, value: metric.value, rating: metric.rating, id: metric.id }) navigator.sendBeacon('/api/vitals', body) } onLCP(sendToAnalytics) // 最大内容绘制 onFID(sendToAnalytics) // 首次输入延迟 onCLS(sendToAnalytics) // 累积布局偏移 onINP(sendToAnalytics) // 交互到下一次绘制 onTTFB(sendToAnalytics) // 首字节时间渲染性能检查清单// 开发环境全局组件渲染追踪 // main.js if (import.meta.env.DEV) { app.config.warnHandler = (msg, instance, trace) => { console.warn(`[Vue warn]: ${msg}${trace}`) } } // 检测不必要的重新渲染 import { watch } from 'vue' // 在 DevTools 中使用 "Highlight updates" 功能六、优化检查清单类别手段收益首屏路由懒加载 + 组件分包减少首包体积 50%+首屏骨架屏感知性能提升列表虚拟列表万级数据流畅渲染列表v-memo跳过不必要的 diff组件KeepAlive切换零重建成本组件异步组件按需加载重组件交互防抖节流减少触发频率计算computed 缓存避免重复计算计算Web Worker主线程零阻塞图片懒加载 + 响应式节省带宽七、小结性能优化的核心是"度量驱动":先用 DevTools 和 Web Vitals 找到瓶颈,再对症下药。渲染层关注"减少不必要的更新"(v-memo、KeepAlive、虚拟列表),计算层关注"不阻塞主线程"(防抖、Worker),加载层关注"更快呈现"(懒加载、分包、骨架屏)。本文由 inspirecl.asia 原创,转载请注明出处。
2026年07月04日
5 阅读
0 评论
0 点赞
2026-06-28
Pinia 状态管理实战:从 Vuex 迁移到 Pinia
Pinia 是 Vue 官方推荐的状态管理库,以其简洁的 API 和完整的 TypeScript 支持取代了 Vuex。本文系统讲解 Pinia 的核心概念、模块化设计、持久化方案以及从 Vuex 迁移的完整指南。一、为什么选择 PiniaPinia vs Vuex特性Vuex 4PiniaAPI 风格Mutation + Action只有 Action(同步异步统一)TypeScript支持较弱完整类型推导模块化嵌套模块 + namespace扁平 Store,天然模块化代码量较多样板代码极简组合式 API适配一般原生设计DevTools支持支持(时间旅行更佳)SSR支持支持热更新部分完整支持Pinia 移除了什么Mutations:直接修改 state 或在 action 中修改嵌套模块:每个 Store 独立扁平命名空间:Store 天然隔离辅助函数:mapState/mapGetters 不再必要(组合式写法)二、快速上手安装与初始化// 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// stores/counter.js import { defineStore } from 'pinia' // 选项式写法(类似组件的 data/computed/methods) export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, name: '计数器' }), getters: { double: (state) => state.count * 2, doublePlusOne() { // 通过 this 访问其他 getter return this.double + 1 } }, actions: { increment() { this.count++ }, async incrementAsync() { // 直接写异步逻辑,无需 dispatch await new Promise(r => setTimeout(r, 1000)) this.count++ } } })组合式写法(推荐)// stores/counter.js — Setup Store 写法 import { ref, computed } from 'vue' import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', () => { // state const count = ref(0) const name = ref('计数器') // getters const double = computed(() => count.value * 2) const doublePlusOne = computed(() => double.value + 1) // actions function increment() { count.value++ } async function incrementAsync() { await new Promise(r => setTimeout(r, 1000)) count.value++ } // 必须返回所有使用的属性 return { count, name, double, doublePlusOne, increment, incrementAsync } })在组件中使用<script setup> import { useCounterStore } from '@/stores/counter' import { storeToRefs } from 'pinia' const counter = useCounterStore() // ⚠️ 解构会丢失响应式,必须用 storeToRefs const { count, double } = storeToRefs(counter) // action 可以直接解构(它是函数) const { increment } = counter // 也可以不解构直接用 console.log(counter.count) counter.increment() </script> <template> <div> <p>{{ counter.name }}: {{ count }}</p> <p>双倍: {{ double }}</p> <button @click="increment">+1</button> </div> </template>三、核心概念详解State// stores/user.js export const useUserStore = defineStore('user', { state: () => ({ user: null, token: '', permissions: [], settings: { theme: 'light', language: 'zh-CN' } }), actions: { // 重置整个 state 到初始值 resetState() { this.$reset() }, // 批量修改(替代 Vuex 的 mutation) setUser(user) { this.user = user this.permissions = user.permissions }, // $patch 批量更新,性能更好 updateSettings(partial) { this.$patch({ settings: { ...this.settings, ...partial } }) // 函数式写法 this.$patch((state) => { state.settings.theme = 'dark' }) } } })Gettersexport const useCartStore = defineStore('cart', { state: () => ({ items: [] }), getters: { // 基础 getter itemCount: (state) => state.items.length, // 带参数的 getter(返回函数,无缓存优势) itemById: (state) => { return (id) => state.items.find(item => item.id === id) }, // 访问其他 getter subtotal() { return this.items.reduce((sum, item) => sum + item.price * item.qty, 0) }, // 访问其他 Store total() { const userStore = useUserStore() const discount = userStore.user?.discount ?? 1 return this.subtotal * discount } } })Actions// stores/products.js import { defineStore } from 'pinia' import api from '@/api' export const useProductStore = defineStore('products', () => { const products = ref([]) const loading = ref(false) const error = ref(null) // 获取产品列表 async function fetchProducts(params) { loading.value = true error.value = null try { const { data } = await api.get('/products', { params }) products.value = data } catch (e) { error.value = e.message } finally { loading.value = false } } // 跨 Store 调用 async function purchase(productId) { const cartStore = useCartStore() const product = products.value.find(p => p.id === productId) await api.post(`/cart/items`, { productId, qty: 1 }) cartStore.addItem(product) } return { products, loading, error, fetchProducts, purchase } })四、插件系统持久化插件// plugins/persist.js import { watch } from 'vue' export function createPersistPlugin() { return ({ store }) => { // 从 localStorage 恢复 const saved = localStorage.getItem(`pinia-${store.$id}`) if (saved) { store.$patch(JSON.parse(saved)) } // 订阅变化并保存 store.$subscribe((mutation, state) => { localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state)) }) } }// main.js import { createPinia } from 'pinia' import { createPersistPlugin } from './plugins/persist' const pinia = createPinia() pinia.use(createPersistPlugin())使用官方持久化插件npm install pinia-plugin-persistedstate// stores/user.js export const useUserStore = defineStore('user', { state: () => ({ user: null, token: '' }), // 持久化配置 persist: { key: 'my-app-user', storage: localStorage, // 或 sessionStorage // 只持久化部分字段 pick: ['token', 'user.id', 'user.name'] }, actions: { login(credentials) { /* ... */ }, logout() { this.user = null this.token = '' } } })自定义日志插件// plugins/logger.js export function createLoggerPlugin() { return ({ store }) => { store.$subscribe((mutation, state) => { console.log( `[Pinia] ${mutation.storeId} ${mutation.type}:`, mutation.type === 'patch' ? mutation.events : `${mutation.payload}` ) }) store.$onAction(({ name, args, after, onError }) => { const startTime = Date.now() console.log(`[Action] ${store.$id}/${name} 开始`, args) after((result) => { console.log(`[Action] ${store.$id}/${name} 完成 (${Date.now() - startTime}ms)`) }) onError((error) => { console.error(`[Action] ${store.$id}/${name} 失败:`, error) }) }) } }五、Store 组合与模块化Store 之间相互调用// stores/cart.js import { defineStore } from 'pinia' import { useUserStore } from './user' import { useProductStore } from './products' export const useCartStore = defineStore('cart', () => { const items = ref([]) const userStore = useUserStore() // 在 setup 内调用 const total = computed(() => { const discount = userStore.user?.discount ?? 1 return items.value.reduce( (sum, item) => sum + item.price * item.qty, 0 ) * discount }) return { items, total } })组合式 Store 设计模式// stores/composables/useEntityCRUD.js // 通用的 CRUD Store 工厂 export function createCrudStore(entityName, apiEndpoint) { return defineStore(entityName, () => { const items = ref([]) const current = ref(null) const loading = ref(false) async function fetchAll(params = {}) { loading.value = true try { const { data } = await api.get(apiEndpoint, { params }) items.value = data } finally { loading.value = false } } async function fetchOne(id) { const { data } = await api.get(`${apiEndpoint}/${id}`) current.value = data } async function create(payload) { const { data } = await api.post(apiEndpoint, payload) items.value.unshift(data) return data } async function update(id, payload) { const { data } = await api.put(`${apiEndpoint}/${id}`, payload) const index = items.value.findIndex(i => i.id === id) if (index > -1) items.value[index] = data return data } async function remove(id) { await api.delete(`${apiEndpoint}/${id}`) items.value = items.value.filter(i => i.id !== id) } return { items, current, loading, fetchAll, fetchOne, create, update, remove } }) } // 使用工厂创建具体的 Store export const usePostStore = createCrudStore('posts', '/api/posts') export const useCategoryStore = createCrudStore('categories', '/api/categories')六、从 Vuex 迁移对照转换// === Vuex 4 === // store/index.js export default createStore({ state: () => ({ count: 0 }), mutations: { increment(state) { state.count++ } }, actions: { incrementAsync({ commit }) { setTimeout(() => commit('increment'), 1000) } }, getters: { double: (state) => state.count * 2 } }) // === Pinia 等价写法 === export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), // mutation 直接变成 action actions: { increment() { this.count++ }, incrementAsync() { setTimeout(() => this.increment(), 1000) } }, getters: { double: (state) => state.count * 2 } })// === 组件中使用对比 === // Vuex // this.$store.state.count // this.$store.commit('increment') // this.$store.dispatch('incrementAsync') // mapState('counter', ['count']) // mapGetters('counter', ['double']) // Pinia const counter = useCounterStore() counter.count counter.increment() // 同步异步统一调用 counter.incrementAsync() const { count } = storeToRefs(counter)渐进式迁移策略同时安装两个库,逐模块迁移从叶子模块开始(无其他模块依赖的)用组合函数桥接:// 桥接层:让旧 Vuex 代码访问新 Pinia // stores/bridge.js export function useCounterCompat() { const counter = useCounterStore() return { get count() { return counter.count }, increment: counter.increment } }迁移完成移除 Vuex七、SSR 中的注意事项// Nuxt 3 中使用 // stores/counter.js export const useCounterStore = defineStore('counter', () => { const count = ref(0) return { count } }) // 组件中(Nuxt 自动导入 Pinia) const counter = useCounterStore() // SSR 中跨请求状态污染防范 // ✅ 在 setup 内调用 useStore(),而不是模块顶层 // ❌ const store = useCounterStore() // 模块顶层会共享状态八、小结Pinia 以极简的 API 提供了完整的状态管理能力:去掉 mutation 减少样板代码,扁平 Store 天然模块化,完整的 TypeScript 和 DevTools 支持。从 Vuex 迁移成本低,新项目直接上 Pinia 是唯一正确选择。本文由 inspirecl.asia 原创,转载请注明出处。
2026年06月28日
4 阅读
0 评论
0 点赞
2026-06-26
Vue 3 响应式原理剖析:从 Proxy 到副作用调度
Vue 3 用 Proxy 重写了响应式系统,性能与能力都大幅提升。本文从源码设计角度剖析依赖收集、触发更新、副作用调度和计算属性的实现原理,并手写一个迷你响应式系统。一、Vue 2 vs Vue 3 响应式对比特性Vue 2 (defineProperty)Vue 3 (Proxy)属性新增/删除需要 Vue.set/Vue.delete直接支持数组索引赋值不支持支持Map/Set不支持支持初始化递归遍历所有属性惰性代理,访问时才转换性能大对象初始化慢按需代理,更快// Vue 2 的局限 this.obj.newProp = 'value' // ❌ 不触发更新 this.arr[0] = newItem // ❌ 不触发更新 this.arr.length = 0 // ❌ 不触发更新 // Vue 3 全部支持 state.newProp = 'value' // ✅ state.arr[0] = newItem // ✅ state.arr.length = 0 // ✅ state.map.set('key', val) // ✅二、Proxy 基础const raw = { price: 100, quantity: 2 } const proxy = new Proxy(raw, { get(target, key, receiver) { console.log(`读取 ${key}`) track(target, key) // 依赖收集 return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log(`设置 ${key} = ${value}`) const result = Reflect.set(target, key, value, receiver) trigger(target, key) // 触发更新 return result }, deleteProperty(target, key) { const result = Reflect.deleteProperty(target, key) trigger(target, key) return result }, has(target, key) { track(target, key) return Reflect.has(target, key) } }) proxy.price // 读取 price proxy.quantity = 3 // 设置 quantity = 3为什么用 Reflect? 保证 this 指向代理对象,让内嵌对象也能被正确追踪:const obj = reactive({ nested: { count: 0 } // nested 会被惰性转换为响应式 }) // obj.nested 访问时返回嵌套对象的代理 // 这是 Vue 3 实现深层响应式的关键三、手写迷你响应式系统1. 副作用函数与依赖收集let activeEffect = null const targetMap = new WeakMap() // 追踪依赖 function track(target, key) { if (!activeEffect) return // targetMap: target → key → Set<effect> let depsMap = targetMap.get(target) if (!depsMap) { depsMap = new Map() targetMap.set(target, depsMap) } let deps = depsMap.get(key) if (!deps) { deps = new Set() depsMap.set(key, deps) } deps.add(activeEffect) } // 触发更新 function trigger(target, key) { const depsMap = targetMap.get(target) if (!depsMap) return const deps = depsMap.get(key) if (!deps) return // 复制一份,避免无限循环 const effectsToRun = new Set(deps) effectsToRun.forEach(effect => { // 避免递归调用自身 if (effect !== activeEffect) { effect() } }) }2. effect 函数function effect(fn) { const effectFn = () => { // 清除旧依赖(解决分支切换问题) cleanup(effectFn) activeEffect = effectFn fn() activeEffect = null } effectFn.deps = [] effectFn() return effectFn } function cleanup(effectFn) { effectFn.deps.forEach(depSet => { depSet.delete(effectFn) }) effectFn.deps.length = 0 }3. reactive 实现const reactiveMap = new Map() function reactive(target) { // 同一对象只代理一次 if (reactiveMap.has(target)) { return reactiveMap.get(target) } const proxy = new Proxy(target, { get(target, key, receiver) { if (key === '__isReactive') return true track(target, key) const result = Reflect.get(target, key, receiver) // 惰性深层代理:嵌套对象访问时才转换 if (typeof result === 'object' && result !== null) { return reactive(result) } return result }, set(target, key, value, receiver) { const oldValue = target[key] const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD' const result = Reflect.set(target, key, value, receiver) // 值变化才触发 if (oldValue !== value || (typeof oldValue === 'NaN' && typeof value === 'NaN')) { trigger(target, key, type) } return result }, deleteProperty(target, key) { const hadKey = Object.prototype.hasOwnProperty.call(target, key) const result = Reflect.deleteProperty(target, key) if (hadKey && result) { trigger(target, key, 'DELETE') } return result } }) reactiveMap.set(target, proxy) return proxy }4. ref 实现function ref(value) { return { get value() { track(this, 'value') return value }, set value(newValue) { if (newValue !== value) { value = newValue trigger(this, 'value') } } } }5. computed 实现function computed(getter) { let value let dirty = true // 脏标记:是否需要重新计算 // 作为副作用运行,依赖变化时标记为脏 const effectFn = effect(() => { // 每次依赖变化重新执行,但不会立即触发渲染 if (dirty) { value = getter() } }) // 依赖变化时置脏 // (简化实现,真实实现用 scheduler) return { get value() { if (dirty) { value = getter() dirty = false } return value } } }更接近真实的 computed:function computed(getter) { let value let dirty = true const runner = effect(getter, { lazy: true, // scheduler:依赖变化时不立即执行,只标记脏 scheduler() { if (!dirty) { dirty = true // 通知依赖该 computed 的渲染副作用更新 triggerRefValue(this) } } }) return { get value() { if (dirty) { value = runner() dirty = false } // 让当前 activeEffect 追踪此 computed trackRefValue(this) return value } } }四、副作用调度与批更新Vue 3 的组件更新是异步批处理的:const queue = new Set() let isFlushing = false function queueJob(job) { queue.add(job) // Set 自动去重,同一组件多次修改只更新一次 if (!isFlushing) { isFlushing = true Promise.resolve().then(flushJobs) } } function flushJobs() { // 按组件 id 排序,父组件先更新 const jobs = [...queue].sort((a, b) => a.id - b.id) queue.clear() isFlushing = false jobs.forEach(job => job()) }这就是为什么修改多次数据只触发一次渲染:import { ref, nextTick } from 'vue' const count = ref(0) // 触发 3 次修改,但只渲染 1 次 count.value++ count.value++ count.value++ // DOM 更新是异步的 console.log(document.querySelector('#count').textContent) // 还是旧值 nextTick(() => { console.log(document.querySelector('#count').textContent) // "3" })五、Vue 3 编译优化静态提升(hoistStatic)// 模板中的静态节点会被提升到 render 函数外部 // 只创建一次,重复渲染时直接复用 // 编译前 // <div> // <span class="static">静态内容</span> // <span>{{ dynamic }}</span> // </div> // 编译后 const _hoisted_1 = createVNode("span", { class: "static" }, "静态内容") function render() { return createVNode("div", null, [ _hoisted_1, // 直接复用 createVNode("span", null, toDisplayString(dynamic)) ]) }Patch Flags(补丁标记)// 动态节点带上标记,diff 时只对比标记的部分 createVNode("span", null, text, 1 /* TEXT */) createVNode("span", { class: dynamicClass }, null, 2 /* CLASS */) createVNode("span", { id: dynamicId }, null, 4 /* PROPS */, ["id"]) // PatchFlag 枚举: // TEXT = 1 只有文本变化 // CLASS = 2 只有 class 变化 // STYLE = 4 只有 style 变化 // PROPS = 8 只有 props 变化 // FULL_PROPS = 16 props 有动态 key // NEED_HYDRATION = 32 // STABLE_FRAGMENT = 64 // KEYED_FRAGMENT = 128 带 key 的 fragment // ...Block Tree(块树)// 动态节点被收集到 block 中,diff 时跳过静态内容 function render() { return (openBlock(), createElementBlock("div", null, [ // 大量静态节点... createVNode("span", null, dynamic, 1 /* TEXT */) // 这个动态节点会被"block"追踪 // 更新时直接定位,不需要遍历整棵树 ])) }六、调试响应式onTrack 与 onTrigger// 仅开发模式可用 const state = reactive({ count: 0 }, { onTrack(event) { // 依赖被追踪时 console.log('track:', event.key, event.target) }, onTrigger(event) { // 依赖变化触发时 console.log('trigger:', event.key, event.type, event.newValue) } }) watch(source, callback, { onTrack(e) { debugger }, onTrigger(e) { debugger } })常见响应式陷阱// 1. 解构 reactive 丢失响应式 const state = reactive({ count: 0 }) let { count } = state // ❌ count 是普通值 const { count } = toRefs(state) // ✅ 保持响应式 // 2. ref 在模板中自动解包,在 JS 中需要 .value const count = ref(0) console.log(count) // Ref 对象 console.log(count.value) // 0 // 3. reactive 被整体替换 let state = reactive({ list: [] }) state = reactive({ list: [1] }) // ❌ 旧引用的依赖全部失效 // ✅ 用 ref 包裹,或只修改属性 // 4. markRaw 跳过转换 const state = reactive({ chart: markRaw(new ECharts()) // ✅ 第三方实例不代理 })七、小结Vue 3 响应式系统的核心是 Proxy 拦截 + WeakMap 存储依赖 + 副作用调度。理解 track/trigger 机制、惰性深层代理和异步批更新,不仅能帮你避开响应式陷阱,也是进阶 Vue 源码的第一步。手写迷你版响应式系统是最好的学习方式。本文由 inspirecl.asia 原创,转载请注明出处。
2026年06月26日
5 阅读
0 评论
0 点赞
2026-06-25
Vue 3 组合式 API 深度解析:Composition API 实战
组合式 API(Composition API)是 Vue 3 最重要的特性,解决了 Options API 在复杂组件中逻辑分散的问题。本文通过实战案例深入讲解 setup、响应式 API、生命周期钩子和自定义组合函数。一、为什么需要组合式 APIOptions API 的痛点// Options API:同一功能的代码被拆散到不同选项中 export default { data() { return { // 搜索功能的数据 searchQuery: '', searchResults: [], // 用户功能的数据 user: null, userPermissions: [], } }, computed: { // 搜索功能的计算属性 filteredResults() { /* ... */ }, // 用户功能的计算属性 isAdmin() { /* ... */ }, }, methods: { // 搜索功能的方法 handleSearch() { /* ... */ }, // 用户功能的方法 loadUser() { /* ... */ }, }, mounted() { // 搜索和用户的初始化逻辑混在一起 this.handleSearch() this.loadUser() } }组合式 API 的优势<script setup> // 搜索功能的所有代码聚合在一起 const { searchQuery, searchResults, handleSearch } = useSearch() // 用户功能的所有代码聚合在一起 const { user, isAdmin, loadUser } = useUser() </script>优势说明逻辑复用自定义组合函数替代 mixin,无命名冲突代码组织相关逻辑聚合,而非按选项类型分散类型推导更好的 TypeScript 支持代码压缩setup 语法糖减少样板代码生产性能编译优化(静态提升、patch flag)二、setup 与 script setup基本用法<script setup> import { ref, computed, onMounted } from 'vue' // 顶层变量自动暴露给模板 const count = ref(0) const message = 'Hello' const double = computed(() => count.value * 2) function increment() { count.value++ } onMounted(() => { console.log('组件已挂载') }) </script> <template> <button @click="increment">{{ count }} × 2 = {{ double }}</button> </template>defineProps 与 defineEmits<script setup> // 编译宏,无需导入 const props = defineProps({ title: String, items: { type: Array, default: () => [] }, disabled: Boolean }) // TypeScript 方式 // const props = defineProps<{ // title: string // items: Item[] // disabled?: boolean // }>() const emit = defineEmits(['update', 'delete']) // 带验证的 emit // const emit = defineEmits<{ // (e: 'update', value: number): void // (e: 'delete', id: string): void // }>() function handleUpdate() { emit('update', props.title) } </script>defineExpose 与 defineModel<script setup> import { ref } from 'vue' const count = ref(0) // 暴露给父组件的方法 defineExpose({ reset: () => { count.value = 0 }, getValue: () => count.value }) // Vue 3.4+ defineModel 双向绑定 const modelValue = defineModel({ default: '' }) const visible = defineModel('visible', { type: Boolean, default: false }) </script>三、响应式 APIref 与 reactiveimport { ref, reactive, shallowRef, shallowReactive } from 'vue' // ref — 适合基本类型和需要整体替换的对象 const count = ref(0) const user = ref({ name: '张三', age: 25 }) console.log(count.value) // 访问需 .value user.value.name = '李四' // 修改属性 user.value = { name: '王五' } // 整体替换 // reactive — 适合不替换整体的复合对象 const state = reactive({ items: [], loading: false, filters: { category: null, price: null } }) state.loading = true // 直接访问,无需 .value state.items.push({ id: 1 }) // ⚠️ reactive 的陷阱 let { items } = state // ❌ 解构丢失响应式 items = toRef(state, 'items') // ✅ 用 toRef 保持响应式ref vs reactive 选择原则场景推荐基本类型ref需要整体替换ref表单对象reactive组合函数返回值ref(保持统一)大型只读配置shallowRef / markRawreadonly 与 shallowRefimport { readonly, shallowRef, triggerRef, markRaw } from 'vue' // readonly — 防止修改(如传递给子组件的 props) const original = reactive({ count: 0 }) const copy = readonly(original) // copy.count++ // ⚠️ 警告:无法修改 // shallowRef — 只追踪 .value 本身,适合大列表 const list = shallowRef([]) // 替换整个数组触发更新 list.value = [...list.value, newItem] // 修改内部属性后手动触发 list.value.push(anotherItem) triggerRef(list) // markRaw — 排除响应式转换(如第三方类实例) const chart = markRaw(new ECharts())computed 与 watchimport { ref, computed, watch, watchEffect } from 'vue' const firstName = ref('三') const lastName = ref('张') // computed — 有缓存 const fullName = computed(() => `${lastName.value}${firstName.value}`) // 可写 computed const fullNameWritable = computed({ get: () => `${lastName.value}${firstName.value}`, set: (val) => { lastName.value = val[0] firstName.value = val.slice(1) } }) // watch — 明确监听源,可获取旧值 watch(fullName, (newVal, oldVal) => { console.log(`${oldVal} → ${newVal}`) }) // watch 对象属性 watch( () => state.filters, (newFilters) => { fetchList(newFilters) }, { deep: true, immediate: true } ) // watchEffect — 立即执行,自动追踪依赖 watchEffect(() => { console.log(`${firstName.value} ${lastName.value}`) }) // watchEffect 清理副作用 watchEffect((onCleanup) => { const timer = setTimeout(() => { console.log(firstName.value) }, 500) onCleanup(() => clearTimeout(timer)) // 依赖变化时清理 })toRefs 与 toValueimport { toRefs, toRef, toValue, reactive } from 'vue' const state = reactive({ x: 1, y: 2 }) // toRefs — 用于解构 reactive 对象时保持响应式 const { x, y } = toRefs(state) // toRef — 单个属性 const yRef = toRef(state, 'y') // toValue — 统一处理 ref / getter / 普通值(Vue 3.3+) function resolveValue(maybeRefOrGetter) { return toValue(maybeRefOrGetter) } resolveValue(refValue) // ref → .value resolveValue(() => 123) // getter → 调用结果 resolveValue(123) // 普通值 → 原样返回四、生命周期钩子<script setup> import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onErrorCaptured, onActivated, onDeactivated } from 'vue' onBeforeMount(() => console.log('挂载前')) onMounted(() => console.log('DOM 已就绪')) onBeforeUpdate(() => console.log('更新前')) onUpdated(() => console.log('更新后')) onBeforeUnmount(() => console.log('卸载前')) onUnmounted(() => console.log('已卸载')) // 捕获子组件错误 onErrorCaptured((err, instance, info) => { console.error('子组件错误:', err) return false // 阻止继续传播 }) // KeepAlive 相关 onActivated(() => console.log('组件激活')) onDeactivated(() => console.log('组件失活')) </script>五、自定义组合函数useFetch — 数据请求// composables/useFetch.js import { ref, shallowRef, triggerRef, toValue, watchEffect } from 'vue' export function useFetch(url) { const data = shallowRef(null) const error = ref(null) const loading = ref(false) async function doFetch() { data.value = null error.value = null loading.value = true try { const res = await fetch(toValue(url)) if (!res.ok) throw new Error(res.statusText) data.value = await res.json() } catch (e) { error.value = e } finally { loading.value = false } } // url 是响应式时自动重新请求 watchEffect(doFetch) return { data, error, loading, retry: doFetch } }<script setup> import { ref } from 'vue' import { useFetch } from '@/composables/useFetch' const userId = ref(1) const { data: user, error, loading } = useFetch( () => `/api/users/${userId.value}` // getter 形式 ) </script> <template> <div v-if="loading">加载中...</div> <div v-else-if="error">错误: {{ error.message }}</div> <div v-else-if="user">{{ user.name }}</div> </template>useLocalStorage — 本地存储// composables/useLocalStorage.js import { ref, watch } from 'vue' export function useLocalStorage(key, defaultValue) { const value = ref( JSON.parse(localStorage.getItem(key)) ?? defaultValue ) watch(value, (val) => { localStorage.setItem(key, JSON.stringify(val)) }, { deep: true }) function remove() { localStorage.removeItem(key) value.value = defaultValue } return { value, remove } }useMouse — 鼠标追踪// composables/useMouse.js import { ref, onMounted, onUnmounted } from 'vue' export function useMouse() { const x = ref(0) const y = ref(0) function update(e) { x.value = e.pageX y.value = e.pageY } onMounted(() => window.addEventListener('mousemove', update)) onUnmounted(() => window.removeEventListener('mousemove', update)) return { x, y } }useDebounceFn — 防抖// composables/useDebounceFn.js import { customRef } from 'vue' export function debouncedRef(value, delay = 200) { let timeout return customRef((track, trigger) => { return { get() { track() return value }, set(newValue) { clearTimeout(timeout) timeout = setTimeout(() => { value = newValue trigger() }, delay) } } }) }六、provide 与 inject<!-- 祖先组件 --> <script setup> import { provide, ref, readonly } from 'vue' const theme = ref('dark') const user = ref({ name: '张三' }) // 提供只读数据 provide('theme', readonly(theme)) provide('user', readonly(user)) // 提供修改方法 provide('updateTheme', (val) => { theme.value = val }) </script><!-- 后代组件(任意深度) --> <script setup> import { inject } from 'vue' const theme = inject('theme', 'light') // 带默认值 const user = inject('user') const updateTheme = inject('updateTheme') updateTheme('light') </script>七、小结组合式 API 是 Vue 3 的核心范式:ref/reactive 管理状态,computed/watch 处理派生与副作用,自定义组合函数实现逻辑复用。掌握这些 API 后,你会发现复杂组件的开发变得清晰而高效。本文由 inspirecl.asia 原创,转载请注明出处。
2026年06月25日
5 阅读
0 评论
0 点赞
1
2
3
0:00