首页
壁纸
统计
友链
Search
1
Vue2详细笔记
37 阅读
2
Nestjs概述-中文
19 阅读
3
ExpressAPI
17 阅读
4
答辩
14 阅读
5
JavaScript企业数据处理实用指南
13 阅读
Nodejs
Vue
Java
Msql
登录
Search
Wasnl
累计撰写
36
篇文章
累计收到
1
条评论
首页
栏目
Nodejs
Vue
Java
Msql
页面
壁纸
统计
友链
搜索到
36
篇与
的结果
2026-03-27
答辩
校园二手交易系统答辩问题及回答一、系统架构与技术选型1. 请介绍一下系统的整体架构设计回答:本系统采用B/S三层架构模式构建,包括前端展示层、后端服务层和数据持久化层。前端展示层:基于Vue 3框架构建,使用Vant移动端组件库开发响应式界面。采用单页面应用(SPA)模式,通过Vue Router管理路由导航,使用Pinia进行全局状态管理,采用组件化开发模式提升代码复用性。后端服务层:基于NestJS框架开发,采用模块化方式组织代码结构。核心业务模块包括认证授权模块、用户信息模块、商品资源模块、交易订单模块、信用档案模块、后台管理模块等。每个模块内部遵循控制器-服务-数据访问对象的分层设计。数据持久化层:采用MySQL 8.0作为关系型数据库,引入Redis作为缓存层存储会话Token和热点数据,提升系统响应效率。代码位置:后端入口:back-end/src/main.ts模块配置:back-end/src/app.module.ts前端路由:frontEnd/src/router/index.ts2. 为什么选择NestJS作为后端框架?与Express有什么区别?回答:选择NestJS主要基于以下考虑:架构规范性:NestJS提供了完整的架构支撑体系,包括依赖注入、模块化组织、切面编程等特性,强制开发者遵循最佳实践,代码结构更清晰。TypeScript原生支持:NestJS完全采用TypeScript编写,与前端Vue 3的TypeScript开发形成统一的技术栈,类型定义一致,降低沟通成本。内置功能丰富:Guards组件实现权限访问控制Interceptors组件统一处理日志输出与响应格式标准化Pipes组件完成请求参数校验Filters组件统一异常处理与Express的关系:NestJS底层使用Express作为HTTP服务器,但在此基础上提供了更高层次的抽象和结构化支持。代码示例:// back-end/src/app.module.ts - 模块化组织 @Module({ imports: [ AuthModule, UsersModule, GoodsModule, OrdersModule, // ... ], providers: [ { provide: APP_FILTER, useClass: ApiExceptionFilter }, { provide: APP_INTERCEPTOR, useClass: ApiResponseInterceptor }, ], })3. 系统是如何实现前后端分离的?API是如何设计的?回答:系统采用RESTful API设计风格实现前后端分离:API设计规范:统一前缀:/api/v1资源命名:使用复数名词(如/goods、/orders)HTTP方法语义:GET查询、POST创建、PATCH更新、DELETE删除统一响应格式:{ code, message, data }接口文档:集成Swagger自动生成API文档,访问/api/docs查看认证机制:采用JWT双令牌认证,Access Token用于接口访问,Refresh Token用于令牌刷新代码位置:API文档配置:back-end/src/main.ts统一响应拦截器:back-end/src/common/interceptors/api-response.interceptor.ts统一异常过滤器:back-end/src/common/filters/api-exception.filter.ts4. 为什么选择Vue 3而不是Vue 2?组合式API有什么优势?回答:选择Vue 3的原因:性能提升:Vue 3重写了虚拟DOM算法,编译优化更智能,运行时性能更好组合式API优势:更好的逻辑复用:通过自定义Hook提取可复用逻辑更好的TypeScript支持:类型推断更准确更灵活的代码组织:相关逻辑可以放在一起,不再受选项式API的分散限制Tree-shaking支持:按需引入API,打包体积更小代码示例:// frontEnd/src/stores/auth.ts - 组合式API风格 export const useAuthStore = defineStore('auth', () => { const user = ref<User | null>(null) const token = ref<string | null>(localStorage.getItem('token')) const isAuthenticated = computed(() => !!token.value) const login = async (params: LoginParams) => { const response = await authApi.login(params) setToken(response.accessToken) // ... } return { user, token, isAuthenticated, login } })二、认证授权模块5. 请详细说明JWT双令牌认证机制的实现回答:系统采用Access Token + Refresh Token双令牌机制:Access Token:有效期:2小时载荷:用户ID、角色、唯一标识符(jti)用途:接口访问鉴权Refresh Token:有效期:7天载荷:用户ID、唯一标识符存储:Redis中存储,key为refresh:token:{userId}用途:刷新会话令牌旋转策略:每次刷新生成新的Refresh Token,旧Token立即失效,防止令牌被窃取后长期有效黑名单机制:用户登出时将Access Token的jti加入Redis黑名单,防止已登出的Token继续使用代码位置:back-end/src/modules/auth/auth.service.ts核心代码:// 签发Access Token private async signAccessToken(userId: number, role: number) { const jti = randomUUID(); const expiresInSeconds = 60 * 60 * 2; // 2小时 const accessToken = await this.jwt.signAsync( { sub: userId, role, jti }, { expiresIn: expiresInSeconds }, ); return { accessToken, expiresIn: expiresInSeconds, jti }; } // 用户登出 - 加入黑名单 async logout(userId: number, accessJti: string, refreshToken: string) { await this.redis.set(`jwt:blacklist:${accessJti}`, '1', 'EX', 60 * 60 * 24); // 删除Refresh Token await this.redis.del(`refresh:token:${userId}`); }6. 密码是如何加密存储的?为什么选择bcrypt?回答:系统使用bcrypt算法对密码进行加密存储:bcrypt特点:自带盐值:每次加密自动生成随机盐,防止彩虹表攻击计算成本可调:通过cost factor控制计算复杂度,抵御暴力破解单向加密:无法反向解密,只能通过比较验证实现方式:// 注册时加密 const passwordHash = await bcrypt.hash(dto.password, 10); // cost factor = 10 // 登录时验证 const ok = await bcrypt.compare(dto.password, user.password);安全性:即使数据库泄露,攻击者也无法获取原始密码代码位置:back-end/src/modules/auth/auth.service.ts 第62行、第132行7. 如何实现用户权限控制?管理员和普通用户如何区分?回答:系统通过角色字段实现权限控制:角色定义:role=1:普通用户role=2:管理员JWT载荷携带角色信息:{ sub: userId, role, jti }Guard守卫实现:JwtAuthGuard:验证JWT有效性AdminGuard:验证是否为管理员角色路由级别控制:// back-end/src/modules/admin/admin.guard.ts canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); const user = request.user; if (user.role !== 2) { throw new ForbiddenException('需要管理员权限'); } return true; }前端路由守卫:// frontEnd/src/router/index.ts if (roles && roles.includes('admin') && userRole !== 2) { next('/home') return }三、商品资源模块8. 商品状态流转是如何设计的?有哪些状态?回答:商品状态采用状态机模式设计,共7种状态:状态值状态名称说明0PENDING_AUDIT待审核1ONLINE在售2SOLD已售空3OFFLINE下架4REJECTED审核拒绝5VIOLATION违规商品6DELETED软删除状态流转规则:发布 → 待审核(0)审核通过 → 在售(1)审核拒绝 → 审核拒绝(4)库存为0 → 已售空(2)卖家主动下架 → 下架(3)管理员标记违规 → 违规商品(5)代码位置:back-end/src/modules/goods/goods.service.ts 第51-79行9. 商品浏览量是如何实现的?为什么采用异步累加?回答:商品浏览量采用异步累加机制:实现方式:// 异步累加浏览量(不阻塞响应) this.goodsRepo.increment({ id: goodsId }, 'viewCount', 1).catch(() => {});为什么异步:浏览量是非关键业务数据,不需要同步等待异步处理可以提升接口响应速度即使累加失败也不影响用户查看商品详情优化考虑:高并发场景可改为Redis计数器,定时批量同步到数据库可添加防刷机制(同一用户短时间内重复访问只计一次)代码位置:back-end/src/modules/goods/goods.service.ts 第181行10. 商品搜索是如何实现的?支持哪些筛选条件?回答:商品搜索通过TypeORM的QueryBuilder实现:支持的筛选条件:分类筛选:categoryId关键词搜索:标题或描述模糊匹配价格区间:minPrice、maxPrice成色等级:condition状态筛选:status实现代码:const qb = this.goodsRepo .createQueryBuilder('goods') .where('goods.status NOT IN (6, 7)'); // 排除软删除 if (query.keyword) { qb.andWhere('(goods.title LIKE :keyword OR goods.description LIKE :keyword)', { keyword: `%${query.keyword}%` }); } if (query.minPrice != null) { qb.andWhere('goods.price >= :minPrice', { minPrice: query.minPrice }); } // ...性能优化:分页查询,默认每页20条按创建时间倒序排列关键字段建立索引代码位置:back-end/src/modules/goods/goods.service.ts 第89-147行四、交易订单模块11. 订单状态机是如何设计的?状态如何流转?回答:订单状态采用状态机模式,共7种状态:状态值状态名称说明1CREATED待付款2PAID待发货3SHIPPED待收货4COMPLETED已完成5REFUNDING退款中6REFUNDED已退款7CANCELLED已取消状态流转规则:创建订单 → 待付款(1)支付成功 → 待发货(2)卖家发货 → 待收货(3)买家确认收货 → 已完成(4)申请退款 → 退款中(5)退款成功 → 已退款(6)取消订单 → 已取消(7)代码位置:back-end/src/modules/orders/orders.service.ts 第28-43行12. 订单创建时如何保证数据一致性?回答:订单创建使用数据库事务保证数据一致性:事务处理流程:await this.ordersRepo.manager.transaction(async (em) => { // 1. 扣减库存 goods.stock -= quantity; if (goods.stock <= 0) goods.status = 2; // 已售空 await em.save(goods); // 2. 创建订单 await em.save(this.ordersRepo.create({ orderNo, buyerId, sellerId: goods.sellerId, goodsId: goods.id, price: price.toFixed(2), // ... })); });一致性保证:库存扣减和订单创建在同一事务中任一步骤失败,整个事务回滚避免超卖问题代码位置:back-end/src/modules/orders/orders.service.ts 第183-227行13. 订单超时自动取消是如何实现的?回答:系统设计了订单超时机制:超时规则:待付款订单:30分钟超时自动取消待发货订单:72小时超时提醒待收货订单:7天自动确认收货前端倒计时:if (order.status === OrderStatus.CREATED) { const expireAt = new Date(order.createTime.getTime() + 30 * 60 * 1000); const seconds = Math.max(0, Math.floor((expireAt.getTime() - now.getTime()) / 1000)); countDown = { type: 'payment', expireAt, seconds }; }后端处理:可通过定时任务扫描超时订单或使用消息队列延迟队列实现代码位置:back-end/src/modules/orders/orders.service.ts 第388-400行14. 退款流程是如何设计的?回答:退款流程设计如下:退款申请条件:已付款、已发货、已完成(7天内)的订单可申请退款已完成订单超过7天无法申请退款处理流程:买家申请退款 → 订单状态变为退款中(5)卖家同意 → 金额退回买家钱包,恢复商品库存卖家拒绝 → 填写拒绝原因,订单恢复原状态退款代码实现:async handleRefund(userId: number, orderNo: string, dto: RefundHandleDto) { if (dto.action === RefundAction.AGREE) { await this.dataSource.transaction(async (em) => { // 1. 更新订单状态 order.status = OrderStatus.REFUNDED; await em.save(order); // 2. 退款到买家钱包 buyerWallet.balance = (Number(balanceBefore) + Number(order.price)).toFixed(2); await em.save(buyerWallet); // 3. 恢复商品库存 goods.stock += order.quantity; await em.save(goods); }); } }代码位置:back-end/src/modules/orders/orders.service.ts 第708-801行五、信用评价模块15. 信用评级体系是如何设计的?有什么作用?回答:信用评级体系是本系统的创新点:信用分规则:初始值:600分分值范围:0-1000分六个等级:极好(≥900)、优秀(≥800)、良好(≥700)、一般(≥600)、较差(≥500)、极差(<500)加分规则:实名认证:+50分订单完成:+5分收到好评:+3分扣分规则:取消订单:-10分超时违约:-20分违规处理:-50分周期限制:每条规则设定每日、每周、每月的积分变化上限,防止恶意刷分作用:为买家提供卖家信用参考约束用户行为,降低交易风险信用过低可限制部分功能代码位置:back-end/src/modules/credit/credit.service.ts16. 信用分的周期限制是如何实现的?回答:周期限制通过信用历史表查询实现:private async checkPeriodLimit( userId: number, ruleCode: string, dailyLimit: number, weeklyLimit: number, monthlyLimit: number, ): Promise<boolean> { const now = new Date(); const todayStart = new Date(now); todayStart.setHours(0, 0, 0, 0); // 检查每日上限 if (dailyLimit > 0) { const dailyCount = await this.creditHistoryRepo .createQueryBuilder('ch') .where('ch.userId = :userId', { userId }) .andWhere('ch.reason LIKE :reason', { reason: `%[${ruleCode}]%` }) .andWhere('ch.changeScore > 0') .andWhere('ch.createTime >= :todayStart', { todayStart }) .getCount(); if (dailyCount >= dailyLimit) return false; } // 类似检查每周、每月上限... }代码位置:back-end/src/modules/credit/credit.service.ts 第152-207行六、钱包支付模块17. 钱包充值流程是如何设计的?回答:钱包充值采用两阶段设计:阶段1:创建充值订单校验充值渠道(支付宝/微信)校验充值限额(单次≤10000元,单日≤50000元)生成支付单号,创建payment_record记录返回支付参数给前端阶段2:支付成功回调更新payment_record状态为PAID写入wallet_transaction流水更新wallet_account余额发送充值成功通知代码位置:back-end/src/modules/wallet/wallet.service.ts 第223-315行18. 提现功能有哪些限制?如何保证资金安全?回答:提现功能设计了多重限制:金额限制:单次提现上限:5000元单日提现上限:20000元最低提现金额:10元次数限制:每日最多提现3次余额校验:可用余额必须≥提现金额账户状态:钱包账户必须为启用状态资金安全:使用数据库事务悲观锁锁定账户先冻结余额,审核通过后扣减代码位置:back-end/src/modules/wallet/wallet.service.ts 第436-561行19. 如何防止钱包并发操作导致的数据不一致?回答:系统采用悲观锁机制防止并发问题:await this.dataSource.transaction(async (em) => { // 悲观锁锁定钱包账户 const account = await em.findOne(WalletAccountEntity, { where: { userId }, lock: { mode: 'pessimistic_write' }, }); // 执行余额操作 account.balance = newBalance.toFixed(2); await em.save(account); });原理:悲观锁在读取数据时加锁,其他事务必须等待结合数据库事务,保证操作的原子性避免超扣、重复扣款等问题代码位置:back-end/src/modules/wallet/wallet.service.ts 第504-515行七、数据库设计20. 数据库表是如何设计的?核心表有哪些?回答:系统核心包含7张数据表:表名说明核心字段user用户表id, student_id, username, password, credit_score, rolegoods商品表id, seller_id, title, price, stock, status, view_countorders订单表id, order_no, buyer_id, seller_id, goods_id, status, pricewallet_account钱包账户表id, user_id, balance, frozen_balancecategory商品分类表id, name, icon, sort_ordercredit_history信用历史表id, user_id, change_score, reasoncredit_rule信用规则表id, rule_code, rule_name, score_change表关系设计:用户-商品:1:N(一个用户可发布多个商品)用户-订单:1:N(用户可作为买家或卖家参与订单)用户-钱包:1:1(每个用户拥有唯一钱包账户)商品-分类:N:1(多个商品归属于同一分类)SQL文件位置:整体架构文档/campus_secondhand.sql21. 如何优化数据库查询性能?回答:系统从多个方面优化查询性能:索引设计:主键索引:所有表的id字段唯一索引:学号、手机号、订单号外键索引:seller_id、buyer_id、goods_id查询索引:status、create_time查询优化:使用QueryBuilder构建高效查询分页查询,避免全表扫描只查询需要的字段缓存策略:Redis缓存热点数据(如分类列表)JWT Token存储在Redis异步处理:浏览量累加异步执行通知发送异步处理八、系统安全性22. 系统有哪些安全防护措施?回答:系统实现了多层次的安全防护:认证安全:JWT双令牌机制密码bcrypt加密存储Token黑名单机制授权安全:基于角色的访问控制(RBAC)接口级别权限校验(Guard)输入安全:请求参数校验(ValidationPipe)XSS过滤SQL注入防护(TypeORM参数化查询)业务安全:信用评级约束用户行为交易金额限制举报机制代码位置:参数校验:back-end/src/main.ts 第14-20行XSS过滤:back-end/src/common/utils/xss.util.ts23. 如何防止XSS攻击?回答:系统通过以下方式防止XSS攻击:输入过滤:// xss.util.ts import validator from 'validator'; export function sanitizeXss(input: string): string { return validator.escape(input); }参数校验:// main.ts app.useGlobalPipes( new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true, }), );前端转义:Vue默认对插值表达式进行HTML转义九、前端实现24. 前端状态管理是如何实现的?回答:前端使用Pinia进行状态管理:Store设计:auth.ts:用户认证状态(token、用户信息、登录状态)app.ts:应用全局状态组合式API风格:export const useAuthStore = defineStore('auth', () => { const user = ref<User | null>(null) const token = ref<string | null>(null) const isAuthenticated = computed(() => !!token.value) const login = async (params) => { /* ... */ } const logout = async () => { /* ... */ } return { user, token, isAuthenticated, login, logout } })持久化:Token和用户信息存储在localStorage代码位置:frontEnd/src/stores/auth.ts25. 前端路由守卫是如何实现的?回答:前端通过Vue Router的导航守卫实现权限控制:router.beforeEach(async (to, from, next) => { const authStore = useAuthStore() const { requiresAuth, requiresGuest, roles } = to.meta // 需要登录但未登录 if (requiresAuth && !authStore.isAuthenticated) { next('/login') return } // 登录页但已登录 if (requiresGuest && authStore.isAuthenticated) { next('/home') return } // 角色权限校验 if (roles && roles.includes('admin') && authStore.user?.role !== 2) { next('/home') return } next() })代码位置:frontEnd/src/router/index.ts 第261-285行十、项目难点与解决方案26. 项目开发过程中遇到了哪些难点?如何解决的?回答:难点1:订单状态流转的复杂性问题:订单涉及多种状态和状态转换,容易出现非法状态转换解决:采用状态机模式,定义明确的状态枚举和转换规则,每次状态变更前校验当前状态是否允许转换难点2:并发场景下的数据一致性问题:钱包余额操作、库存扣减等场景存在并发风险解决:使用数据库事务+悲观锁机制,保证操作的原子性难点3:信用评级体系的规则配置问题:信用规则需要灵活配置,同时要防止恶意刷分解决:设计信用规则配置表,支持动态配置;实现周期限制检查机制难点4:前后端Token同步问题:Token过期后需要无感刷新解决:实现Token刷新机制,前端拦截401错误自动刷新Token27. 系统还有哪些可以改进的地方?回答:实时通讯:新增买卖双方实时在线沟通功能,降低沟通成本智能推荐:基于用户浏览历史、校园身份标签进行商品推荐搜索优化:引入Elasticsearch实现全文搜索,提升搜索体验消息推送:集成WebSocket实现实时消息推送安全升级:敏感数据加密存储,引入风控系统性能优化:引入消息队列处理异步任务,提升系统吞吐量十一、代码定位速查28. 如果老师问某个功能的代码在哪里,如何快速定位?回答:请参考本文档附件《后端代码架构快速定位指南》,按照以下方式快速定位:按模块定位:根据功能所属模块,在back-end/src/modules/下找到对应目录按层次定位:控制器(Controller):处理HTTP请求服务(Service):处理业务逻辑实体(Entity):数据库表映射按功能定位:登录注册 → modules/auth/商品管理 → modules/goods/订单交易 → modules/orders/钱包支付 → modules/wallet/信用评价 → modules/credit/后台管理 → modules/admin/附件:后端代码架构快速定位指南一、项目目录结构总览back-end/ ├── src/ │ ├── common/ # 公共模块 │ │ ├── constants/ # 常量定义(错误码) │ │ ├── decorators/ # 自定义装饰器 │ │ ├── exceptions/ # 自定义异常 │ │ ├── filters/ # 异常过滤器 │ │ ├── interceptors/ # 拦截器 │ │ ├── types/ # 类型定义 │ │ └── utils/ # 工具函数 │ │ │ ├── infrastructure/ # 基础设施 │ │ ├── database/ # 数据库模块 │ │ ├── logger/ # 日志配置 │ │ └── redis/ # Redis模块 │ │ │ ├── modules/ # 业务模块(核心) │ │ ├── admin/ # 后台管理模块 │ │ ├── auth/ # 认证授权模块 │ │ ├── categories/ # 商品分类模块 │ │ ├── comments/ # 评论模块 │ │ ├── credit/ # 信用评价模块 │ │ ├── goods/ # 商品资源模块 │ │ ├── notifications/ # 消息通知模块 │ │ ├── orders/ # 交易订单模块 │ │ ├── payments/ # 支付模块 │ │ ├── users/ # 用户信息模块 │ │ └── wallet/ # 电子钱包模块 │ │ │ ├── app.module.ts # 应用主模块 │ ├── app.controller.ts # 应用控制器 │ ├── app.service.ts # 应用服务 │ └── main.ts # 应用入口 │ ├── dist/ # 编译输出目录 ├── coverage/ # 测试覆盖率报告 ├── .env # 环境变量配置 └── package.json # 项目依赖配置二、核心模块详细定位1. 认证授权模块 (auth)目录: src/modules/auth/文件功能关键代码位置auth.controller.ts认证接口控制器登录、注册、刷新Token、登出接口auth.service.ts认证业务逻辑密码加密、Token签发、黑名单机制jwt.strategy.tsJWT策略Token验证策略jwt-auth.guard.tsJWT守卫接口权限拦截dto/auth.dto.ts数据传输对象登录、注册参数定义关键功能代码定位:用户注册:auth.service.ts → register() 方法(第40-96行)用户登录:auth.service.ts → login() 方法(第109-164行)Token刷新:auth.service.ts → refresh() 方法(第176-224行)用户登出:auth.service.ts → logout() 方法(第234-244行)2. 商品资源模块 (goods)目录: src/modules/goods/文件功能关键代码位置goods.controller.ts商品接口控制器商品CRUD接口goods.service.ts商品业务逻辑发布、查询、上下架、举报goods.entity.ts商品实体数据库表映射report.entity.ts举报实体举报记录表映射dto/goods.dto.ts数据传输对象商品参数定义关键功能代码定位:商品列表:goods.service.ts → list() 方法(第89-147行)商品详情:goods.service.ts → detail() 方法(第156-227行)发布商品:goods.service.ts → create() 方法(第236-270行)更新商品:goods.service.ts → update() 方法(第280-319行)删除商品:goods.service.ts → delete() 方法(第329-367行)下架商品:goods.service.ts → offline() 方法(第379-418行)举报商品:goods.service.ts → report() 方法(第520-560行)商品状态说明:// 状态枚举(goods.service.ts 第24-32行) // 0: 待审核 (PENDING_AUDIT) // 1: 在售 (ONLINE) // 2: 已售空 (SOLD) // 3: 下架 (OFFLINE) // 4: 审核拒绝 (REJECTED) // 5: 违规商品 (VIOLATION) // 6: 软删除 (DELETED)3. 交易订单模块 (orders)目录: src/modules/orders/文件功能关键代码位置orders.controller.ts订单接口控制器订单CRUD接口orders.service.ts订单业务逻辑创建、支付、发货、确认、退款order.entity.ts订单实体数据库表映射dto/orders.dto.ts数据传输对象订单参数定义关键功能代码定位:创建订单:orders.service.ts → createOrder() 方法(第148-241行)订单列表:orders.service.ts → listOrders() 方法(第250-324行)订单详情:orders.service.ts → detail() 方法(第369-443行)取消订单:orders.service.ts → cancel() 方法(第454-491行)订单发货:orders.service.ts → ship() 方法(第502-533行)确认收货:orders.service.ts → confirm() 方法(第544-642行)申请退款:orders.service.ts → refund() 方法(第653-695行)处理退款:orders.service.ts → handleRefund() 方法(第708-801行)订单状态说明:// 状态枚举(orders.service.ts 第28-43行) export enum OrderStatus { CREATED = 1, // 待付款 PAID = 2, // 待发货 SHIPPED = 3, // 待收货 COMPLETED = 4, // 已完成 REFUNDING = 5, // 退款中 REFUNDED = 6, // 已退款 CANCELLED = 7, // 已取消 }4. 电子钱包模块 (wallet)目录: src/modules/wallet/文件功能关键代码位置wallet.controller.ts钱包接口控制器余额查询、充值、提现接口wallet.service.ts钱包业务逻辑充值、提现、交易流水wallet-account.entity.ts钱包账户实体账户表映射wallet-transaction.entity.ts交易流水实体流水表映射dto/wallet.dto.ts数据传输对象钱包参数定义关键功能代码定位:钱包信息:wallet.service.ts → getMe() 方法(第114-128行)交易流水:wallet.service.ts → getTransactions() 方法(第140-181行)充值功能:wallet.service.ts → topup() 方法(第223-315行)提现功能:wallet.service.ts → withdraw() 方法(第436-561行)交易类型说明:// 交易类型(wallet.service.ts 第16-21行) // TOPUP: 充值 // ORDER_PAY: 订单支付 // ORDER_REFUND: 订单退款 // WITHDRAW: 提现 // ADMIN_ADJUST: 管理员调账5. 信用评价模块 (credit)目录: src/modules/credit/文件功能关键代码位置credit.service.ts信用业务逻辑加分、扣分、周期限制检查credit.module.ts信用模块定义模块配置关键功能代码定位:增加信用分:credit.service.ts → addCredit() 方法(第39-94行)扣减信用分:credit.service.ts → deductCredit() 方法(第104-140行)周期限制检查:credit.service.ts → checkPeriodLimit() 方法(第152-207行)按规则加分:credit.service.ts → addCreditByRule() 方法(第216-235行)按规则扣分:credit.service.ts → deductCreditByRule() 方法(第244-262行)信用等级:credit.service.ts → getCreditLevel() 方法(第269-283行)信用等级划分:// 信用等级(credit.service.ts 第269-283行) // ≥900: 极好 (excellent) // ≥800: 优秀 (great) // ≥700: 良好 (good) // ≥600: 一般 (normal) // ≥500: 较差 (poor) // <500: 极差 (bad)6. 用户信息模块 (users)目录: src/modules/users/文件功能关键代码位置users.controller.ts用户接口控制器用户信息接口users.service.ts用户业务逻辑用户信息管理、实名认证user.entity.ts用户实体用户表映射credit-history.entity.ts信用历史实体信用历史表映射receiver.entity.ts收货地址实体地址表映射dto/users.dto.ts数据传输对象用户参数定义7. 后台管理模块 (admin)目录: src/modules/admin/子目录/文件功能admin.controller.ts后台管理控制器admin.service.ts后台管理服务admin.guard.ts管理员权限守卫users/用户管理goods/商品审核管理orders/订单管理reports/举报管理credit/信用规则管理categories/分类管理stats/数据统计audit-log.entity.ts审计日志实体三、公共模块定位1. 错误码定义文件: src/common/constants/error-codes.ts// 错误码分段 // 1000-1999: 认证与授权 // 2000-2999: 用户域 // 3000-3999: 商品域 // 4000-4999: 订单域 // 5000-5999: 支付域 // 8000-8999: 钱包域 // 9000-9999: 系统与基础设施2. 异常过滤器文件: src/common/filters/api-exception.filter.ts功能:统一捕获和处理异常,返回标准格式的错误响应3. 响应拦截器文件: src/common/interceptors/api-response.interceptor.ts功能:统一包装响应数据,返回标准格式{ code, message, data }四、快速定位技巧1. 按功能关键词搜索在IDE中使用全局搜索功能,搜索以下关键词:接口路径:如/auth/login、/goods、/orders方法名:如createOrder、handleRefund、addCredit状态值:如OrderStatus.CREATED、GoodsStatus.ONLINE2. 按API路径定位在控制器文件中搜索路由装饰器找到对应的处理方法跟踪到服务层查看业务逻辑3. 按数据库表定位在实体文件中查看表结构定义在服务文件中查看对该表的操作五、前端代码定位1. 目录结构frontEnd/src/ ├── api/ # API接口定义 ├── components/ # 公共组件 ├── hooks/ # 自定义Hook ├── router/ # 路由配置 ├── stores/ # 状态管理 ├── styles/ # 样式文件 ├── types/ # 类型定义 ├── utils/ # 工具函数 └── views/ # 页面组件 ├── admin/ # 后台管理页面 ├── auth/ # 登录注册页面 ├── goods/ # 商品详情页面 ├── order/ # 订单页面 ├── publish/ # 发布页面 └── user/ # 用户中心页面2. 关键文件定位功能文件位置路由配置router/index.ts认证状态stores/auth.tsAPI请求封装utils/request.ts登录页面views/auth/Login.vue商品详情views/goods/GoodsDetail.vue订单列表views/order/OrderList.vue后台管理views/admin/文档版本: v1.0 更新日期: 2026年3月 适用项目: 基于Node+Vue融合信用评级的校园二手交易系统
2026年03月27日
14 阅读
0 评论
0 点赞
2026-03-26
JavaScript 核心知识笔记
# JavaScript 核心知识笔记一、异步编程核心1. Promise 基础Promise 是 JavaScript 异步编程的基础,代表一个异步操作的最终完成(或失败)及其结果值。new Promise():创建一个异步任务。执行器函数接收 resolve 和 reject 两个参数。then() / catch():用于处理Promise成功或失败的结果。then 接收成功回调,catch 接收失败回调。Promise.resolve():将一个值包装成一个已成功的Promise对象。Promise.reject():将一个值包装成一个已失败的Promise对象。Promise.all():接收一个Promise数组,等待所有Promise都成功,返回一个包含所有结果的数组。只要有一个失败,整体就失败。Promise.race():接收一个Promise数组,等待第一个完成的Promise(无论成功或失败),返回其结果。2. async/await 语法糖async/await 是基于Promise的语法糖,让异步代码写起来像同步代码一样清晰。async函数:声明一个异步函数,它会隐式地返回一个Promise对象。await关键字:在 async 函数内部使用,用于等待一个Promise完成。await 会暂停函数的执行,直到Promise状态变为 fulfilled(成功)或 rejected(失败)。成功时直接返回结果值,失败时需要用 try...catch 捕获。// 示例:使用 async/await 替代 then/catch async function fetchUserData() { try { const user = await fetch('/api/user').then(res => res.json()); const posts = await fetch(`/api/posts/${user.id}`).then(res => res.json()); console.log('用户和帖子数据:', { user, posts }); } catch (error) { console.error('请求失败:', error); } }二、数组核心处理函数数组方法是JavaScript中最常用的工具集,掌握它们是高效处理数据的关键。2.1 every() - 全真才为真作用:测试一个数组内的所有元素是否都能通过指定函数的测试。它返回一个布尔值。场景:表单校验:检查所有输入项是否都通过了校验。权限校验:判断用户是否拥有全部所需的权限。批量数据的合规性检查:如所有商品数量是否都大于0。// 表单校验示例 const formFields = [ { name: 'username', valid: true, value: '张三' }, { name: 'email', valid: true, value: 'zhangsan@example.com' }, { name: 'phone', valid: false, value: '123' } ]; const isFormValid = formFields.every(field => field.valid); // false2.2 some() - 一真即为真作用:测试数组中是否至少有一个元素通过了指定函数的测试。它返回一个布尔值。场景:判断列表中是否存在未完成/异常的状态项。快速判断搜索是否命中结果。权限判断:只要用户拥有一个相关权限就能访问。// 搜索命中示例 const users = [{ name: '张三' }, { name: '李四' }, { name: '王五' }]; const keywords = '张三'; const hasMatch = users.some(user => user.name.includes(keywords)); // true2.3 filter() - 筛选过滤作用:创建一个新数组,其包含通过所提供函数实现的测试的所有元素。不会改变原数组。场景:数据筛选:根据条件过滤列表数据(如筛选出价格大于100的商品)。清理无效数据:过滤掉数组中的 null、undefined、空字符串等。数据分类:根据条件将原数组分成不同的子集。// 过滤空值示例 const strings = ["苹果", "", "香蕉", " ", "橙子", null]; const validStrings = strings.filter(s => s && s.trim()); // ["苹果", "香蕉", "橙子"]2.4 map() - 一对一映射作用:创建一个新数组,其结果是该数组中的每个元素调用一次提供的函数后的返回值。常用于数据转换。场景:接口数据适配:把后端返回的字段名(如 user_name)转成前端需要的(如 name)。渲染列表:把数据转成UI组件所需的格式。批量修改数组中的每一项。// 接口数据适配示例 const apiData = [{ user_name: '张三', user_age: 25 }]; const frontData = apiData.map(item => ({ name: item.user_name, age: item.user_age })); // 结果: [{ name: '张三', age: 25 }]2.5 reduce() - 万能汇总工具作用:对数组中的每个元素执行一个reducer函数(升序执行),将其结果汇总为单个返回值。reduce 是函数式编程中最强大的工具之一。参数:回调函数:(accumulator, currentValue) => newAccumulator初始值:initialValue,为累加器提供初始值。关键点:必须 return 新的累加器,否则下次迭代时累加器为 undefined。acc 可以是数字、对象、数组等任何类型。场景:聚合统计:求和、求平均值、统计出现次数。数据分组:把列表按某个字段分成不同的组。复杂的数据变换:数组转对象、多层数据处理。// 按类别分组并统计总价示例 const orderItems = [ { category: '食品', price: 20 }, { category: '服装', price: 100 }, { category: '食品', price: 30 } ]; const totalByCategory = orderItems.reduce((acc, item) => { acc[item.category] = (acc[item.category] || 0) + item.price; return acc; // 必须 return acc }, {}); // 初始值是一个空对象 {} // 结果: { 食品: 50, 服装: 100 }2.6 find() & findIndex() - 精准查找find():返回数组中第一个满足提供的测试函数的元素。找不到返回 undefined。findIndex():返回数组中第一个满足提供的测试函数的元素的索引。找不到返回 -1。与 indexOf 的区别:indexOf 只能用于简单值(如数字、字符串)的严格相等比较,而 find / findIndex 支持复杂的自定义判断条件。场景:根据ID查找特定的用户/数据项,或找到元素位置以进行后续的删除/更新操作。// 根据 ID 查找用户 const users = [{ id: 1, name: '张三' }, { id: 2, name: '李四' }]; const user = users.find(user => user.id === 2); // { id: 2, name: '李四' } const index = users.findIndex(user => user.id === 2); // 12.7 flat() & flatMap() - 数组扁平化flat(depth):按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组。depth 默认是1,可以传 Infinity 展开所有层级。flatMap():先执行 map,再对结果执行 flat(1),一步到位。场景:处理多层嵌套的数据结构。在 map 操作后直接扁平化,如将句子数组拆分成单词数组。// flatMap 示例:将句子拆分成单词 const lines = ["Hello world", "JavaScript is good"]; const words = lines.flatMap(line => line.split(' ')); // 结果: ["Hello", "world", "JavaScript", "is", "good"]2.8 includes() - 判断是否包含作用:判断一个数组是否包含一个指定的值,根据情况返回 true 或 false。用于简单值的判断。场景:检查用户角色、状态值等是否在允许的列表中。const roles = ['admin', 'editor']; const isAdmin = roles.includes('admin'); // true2.9 sort() - 排序(会修改原数组!)作用:对数组的元素进行排序,并返回原数组。注意:sort 方法会改变原数组。默认排序顺序是将元素转换为字符串,然后比较它们的UTF-16代码单元值序列。安全用法:使用 [...arr].sort() 先拷贝,再排序,避免修改原数组。数字排序:必须传入比较函数 (a, b) => a - b 来升序排序,(a, b) => b - a 来降序排序。const nums = [3, 1, 2]; const sortedNums = [...nums].sort((a, b) => a - b); // 新数组 [1, 2, 3] console.log(nums); // 原数组 [3, 1, 2],未改变2.10 reverse() - 反转数组(会修改原数组!)作用:反转数组中的元素,并返回原数组。同样会改变原数组。如需不可变操作,先拷贝。const arr = [1, 2, 3]; const reversedArr = [...arr].reverse(); // 新数组 [3, 2, 1]2.11 slice() - 截取数组作用:返回一个新的数组对象,这一对象是一个由 start 和 end 决定的原数组的浅拷贝(包括 start,不包括 end)。不会修改原数组。场景:提取数组的子集,或浅拷贝整个数组 slice(0) 或 slice()。const arr = [1, 2, 3, 4]; const newArr = arr.slice(1, 3); // [2, 3]2.12 splice() - 删除/插入/替换(会修改原数组!)作用:通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。会修改原数组。应尽量避免在复杂逻辑中直接使用,或用不可变方式替代。参数:(start, deleteCount, item1, item2, ...)const arr = [1, 2, 3]; const removed = arr.splice(1, 1); // 从索引1开始删1个,removed = [2], arr = [1, 3] arr.splice(1, 0, 99); // 在索引1处插入99,arr = [1, 99, 3]三、对象核心处理函数3.1 Object.keys() / values() / entries() - 对象遍历三剑客Object.keys(obj):返回一个由对象的键名组成的数组。Object.values(obj):返回一个由对象的值组成的数组。Object.entries(obj):返回一个由对象的键值对组成的二维数组,每个内部项是 [key, value]。场景:遍历对象的属性,替代过去的 for...in。把对象转成数组,方便使用 map、filter 等数组方法处理。渲染键值对形式的详情表格。// 对象过滤示例:只保留指定属性 const obj = { name: '张三', age: 25, gender: '男' }; const picked = Object.fromEntries( Object.entries(obj).filter(([key]) => ['name', 'age'].includes(key)) ); // 结果: { name: '张三', age: 25 }3.2 Object.fromEntries() - 键值对转回对象作用:把键值对列表(如 Map 或 entries 返回的二维数组)转换成一个对象。是 Object.entries() 的逆操作。场景:解析URL查询参数。过滤/转换对象的属性,先转成 entries 处理,再转回来。把 Map 转成普通对象。3.3 Object.assign() - 对象合并/浅拷贝作用:将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。场景:对象合并。在现代JavaScript中,更推荐使用展开运算符 ... 来实现,因为它更简洁、更直观。const obj1 = { a: 1 }; const obj2 = { b: 2 }; const newObj = Object.assign({}, obj1, obj2); // 等价于 const newObj = { ...obj1, ...obj2 }; // newObj: { a: 1, b: 2 }四、实用操作符与技巧4.1 可选链 ?. 与空值合并 ??可选链 ?.:当访问嵌套对象的属性时,如果中间某个属性不存在(是 null 或 undefined),不会报错,直接返回 undefined。语法:obj?.prop、obj?.[expr]、func?.()。空值合并 ??:只有当左边的值是 null 或 undefined 时,才返回右边的默认值。与 || 的区别在于,?? 不会把 0、''、false 这些假值当成无效值。场景:安全地访问深层嵌套属性,给表单字段设置默认值。const user = { profile: { name: '张三' } }; console.log(user?.profile?.age); // undefined,不会报错 console.log(user?.address?.city); // undefined const value = 0; const defaultValue = value || 'default'; // 'default',因为 0 是假值 const safeDefault = value ?? 'default'; // 0,因为 0 不是 null/undefined4.2 剩余运算符与展开运算符 (...)剩余运算符 (Rest):用在函数参数或解构赋值中,将剩余的参数或属性收集到一个数组或对象中。展开运算符 (Spread):用在数组或对象字面量中,将一个可迭代对象(如数组)或对象“展开”到新的数组或对象中。// 解构:收集剩余属性 const currentNode = { children: [], id: 1, name: 'root', type: 'folder' }; const { children, ...editMenuData } = currentNode; // editMenuData 为 { id: 1, name: 'root', type: 'folder' } // 对象合并 const obj1 = { a: 1 }; const obj2 = { b: 2, ...obj1 }; // { b: 2, a: 1 } // 数组去重 const arr = [1, 2, 2, 3]; const uniqueArr = [...new Set(arr)]; // [1, 2, 3]4.3 深拷贝与浅拷贝浅拷贝:只拷贝对象的第一层属性,如果属性值是引用类型,则拷贝的是其引用。方法:展开运算符 ...、Object.assign()。深拷贝:递归地拷贝对象的所有层级,生成一个完全独立的副本。structuredClone():现代浏览器和Node.js推荐的原生API,支持大多数类型(如 Date、Map、Set)。JSON.parse(JSON.stringify(obj)):简单粗暴,但只适用于纯数据对象(不含函数、undefined、循环引用等)。Lodash _.cloneDeep:工业级最稳方案。五、函数式编程高级实战5.1 函数组合 pipe / compose - 数据流水线函数组合是将多个简单函数组合成一个复杂函数,数据从左到右(pipe)或从右到左(compose)依次流过每个函数。这是函数式编程的核心思想,能极大提升代码的可读性和可维护性。// 从左到右执行(pipe,更符合人类阅读习惯,企业更常用) const pipe = (...fns) => (initValue) => fns.reduce((acc, fn) => fn(acc), initValue); // 从右到左执行(compose,经典函数式写法) const compose = (...fns) => (initValue) => fns.reduceRight((acc, fn) => fn(acc), initValue); // 实战场景:后端接口数据全链路处理 const apiData = [ { goods_id: 1, goods_name: '手机', price: 5000, status: 1 }, { goods_id: 2, goods_name: '充电器', price: 50, status: 1 }, { goods_id: 3, goods_name: '耳机', price: 0, status: 0 } // 无效数据 ]; // 步骤1:过滤有效数据(价格>0 且 状态=1) const filterValid = (list) => list.filter(item => item.price > 0 && item.status === 1); // 步骤2:字段映射(后端字段转前端字段) const mapField = (list) => list.map(({ goods_id: id, goods_name: name, price }) => ({ id, name, price })); // 步骤3:按价格排序 const sortByPrice = (list) => [...list].sort((a, b) => a.price - b.price); // 步骤4:分组统计 const groupByPriceRange = (list) => list.reduce((acc, item) => { const range = item.price < 100 ? '低价' : item.price < 500 ? '中价' : '高价'; acc[range] = [...(acc[range] || []), item]; return acc; }, {}); // 把所有步骤串成流水线,代码线性、可读性拉满 const processGoodsData = pipe(filterValid, mapField, sortByPrice, groupByPriceRange); // 直接使用 const result = processGoodsData(apiData); console.log(result); // 输出: { 高价: [ { id: 1, name: '手机', price: 5000 } ], 低价: [ { id: 2, name: '充电器', price: 50 } ] }5.2 柯里化 (Currying)柯里化是将一个接受多个参数的函数转换为一系列接受单个参数的函数的技术。它允许你部分应用函数,预置一些参数。const curry = (fn) => (...args) => { if (args.length >= fn.length) { return fn(...args); } else { return curry(fn.bind(null, ...args)); } }; // 示例:一个计算价格的函数 const calcPrice = (price, tax, discount) => price * (1 + tax) * discount; const curriedCalc = curry(calcPrice); const priceWithTax = curriedCalc(100); // 预置价格100,返回一个新函数 const finalPrice = priceWithTax(0.1)(0.9); // 再传入税率0.1和折扣0.9,结果:99 console.log(finalPrice);六、异步编程高级实战6.1 带限流的异步并发控制 (Async Pool)解决批量请求并发过高导致服务端压力大或浏览器限制的问题。核心思想是始终维持最多 limit 个异步任务同时执行。/** * 异步任务并发控制 * @param {Array<Function>} tasks 返回 Promise 的任务函数数组 * @param {number} limit 最大并发数 * @returns {Promise<Array>} 所有任务的结果数组(按原顺序) */ const asyncPool = async (tasks, limit = 3) => { const results = []; const running = []; // 存放正在执行的 Promise for (const task of tasks) { // 包装任务,确保它是一个 Promise const promise = Promise.resolve().then(() => task()); results.push(promise); // 任务执行完后,从 running 数组中移除它 const finishPromise = promise.then(() => { const index = running.indexOf(finishPromise); if (index > -1) running.splice(index, 1); }); running.push(finishPromise); // 当并发数达到限制时,等待任意一个任务完成 if (running.length >= limit) { await Promise.race(running); } } // 等待所有任务完成 return Promise.all(results); }; // 使用示例 const createTask = (id, delay) => () => new Promise(resolve => { console.log(`任务 ${id} 开始`); setTimeout(() => { console.log(`任务 ${id} 结束`); resolve(id); }, delay); }); const tasks = [ createTask(1, 1000), createTask(2, 500), createTask(3, 800), createTask(4, 300), createTask(5, 200) ]; asyncPool(tasks, 2).then(results => console.log('所有任务结果:', results));七、数据处理与性能优化7.1 不可变数据更新 (Immutable Update)在React/Vue等框架开发中,直接修改状态可能导致视图不更新或难以追踪Bug。必须遵循“不修改原数据”的原则。改深层对象:用 ... 逐层展开复制。改数组元素:用 map,在需要修改的元素上返回新对象。删数组元素:用 filter,保留不需要删除的元素。加数组元素:用 [...arr, newItem]。// 原始状态 const state = { user: { name: '张三', profile: { age: 25, city: '北京' } }, posts: [{ id: 1, title: 'Post 1' }, { id: 2, title: 'Post 2' }] }; // 1. 修改深层属性 (user.profile.city 改为 '上海') const newState1 = { ...state, user: { ...state.user, profile: { ...state.user.profile, city: '上海' } } }; // 2. 修改数组元素 (将 id 为 2 的帖子标题改为 'New Title') const newState2 = { ...state, posts: state.posts.map(post => post.id === 2 ? { ...post, title: 'New Title' } : post ) }; // 3. 删除数组元素 (删除 id 为 1 的帖子) const newState3 = { ...state, posts: state.posts.filter(post => post.id !== 1) }; // 4. 添加数组元素 (在末尾添加新帖子) const newPost = { id: 3, title: 'Post 3' }; const newState4 = { ...state, posts: [...state.posts, newPost] };7.2 函数记忆化 (Memoization)缓存函数执行结果,对于相同参数,直接返回缓存值,避免重复计算。const memoize = (fn) => { const cache = new Map(); return (...args) => { const key = JSON.stringify(args); if (cache.has(key)) { return cache.get(key); } const result = fn(...args); cache.set(key, result); return result; }; }; // 一个昂贵的计算函数 const expensiveFibonacci = (n) => { if (n <= 1) return n; return expensiveFibonacci(n - 1) + expensiveFibonacci(n - 2); }; const memoizedFibonacci = memoize(expensiveFibonacci); console.time('第一次'); memoizedFibonacci(40); // 耗时较长 console.timeEnd('第一次'); console.time('第二次'); memoizedFibonacci(40); // 瞬间返回,从缓存读取 console.timeEnd('第二次');7.3 全局错误捕获在生产环境中,需要捕获未处理的Promise异常和同步错误,防止应用崩溃,并向用户提供友好的提示或记录错误日志。// 捕获未处理的 Promise rejection window.addEventListener('unhandledrejection', (event) => { console.error('未捕获的 Promise 错误:', event.reason); // 在这里可以调用后端接口上报错误,或向用户展示错误提示 alert('系统出错了,请刷新页面重试。'); }); // 捕获未处理的同步错误 window.addEventListener('error', (event) => { console.error('未捕获的同步错误:', event.error); // 上报错误 }); // React 中的错误边界 (Error Boundary) 概念类似7.4 防抖 (Debounce) 与节流 (Throttle)防抖 (Debounce):在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。适用于输入框搜索。节流 (Throttle):规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。适用于滚动事件、resize事件。// 防抖函数 function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // 节流函数 function throttle(func, limit) { let inThrottle; return function executedFunction(...args) { if (!inThrottle) { func(...args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; }
2026年03月26日
12 阅读
0 评论
0 点赞
2026-03-24
JavaScript企业数据处理实用指南
在日常的企业级前端开发中,我们绝大多数时间都在和数据打交道:处理接口返回的列表、转换表单数据、优化高频事件、构建树形菜单... 熟练掌握这些数据处理技巧,能让你的代码更简洁、高效、易维护。本文将系统梳理企业开发中最常用的内置函数与实用算法,针对每一个知识点,都会从帮助理解、实战例子、如何记忆、什么时候用四个维度帮你彻底掌握。一、数组核心处理函数数组是前端数据处理最常用的数据结构,ES5 + 提供的一系列函数式方法,彻底改变了我们过去写for循环的模式。1.1 every:全真才为真帮助理解检查数组中所有元素是否都满足指定条件。只要发现有一个元素不满足,就会立即停止遍历,直接返回false;只有全部满足,才返回true。实战例子// 表单校验:检查所有输入项是否都通过了校验const formFields = [ { name: 'username', valid: true, value: '张三' }, { name: 'email', valid: true, value: 'zhangsan@example.com' }, { name: 'phone', valid: false, value: '123' }];// 只有所有字段都valid为true,表单才可以提交const isFormValid = formFields.every(field => field.valid);console.log(isFormValid); // false,因为phone字段校验失败// 权限判断:用户是否拥有访问某个功能的所有权限const requiredPerms = ['user:read', 'user:edit'];const userPerms = ['user:read', 'user:edit', 'admin'];const hasAccess = requiredPerms.every(perm => userPerms.includes(perm));console.log(hasAccess); // true如何记忆every的英文意思就是 “每一个”,你可以这么记:每一个都要满足条件,才算通过。就像期末考试,必须所有科目都及格,才算全部通过。什么时候用表单整体校验,判断所有字段是否合法权限校验,判断用户是否拥有全部所需权限批量数据的合规性检查1.2 some:一真即为真帮助理解检查数组中是否存在至少一个元素满足指定条件。只要找到第一个满足条件的元素,就会立即停止遍历,直接返回true;如果全部都不满足,才返回false。实战例子// 任务列表:判断是否有未完成的任务const tasks = [ { id: 1, name: '写文档', done: true }, { id: 2, name: '改Bug', done: false }, { id: 3, name: '开例会', done: true }];const hasPendingTask = tasks.some(task => !task.done);console.log(hasPendingTask); // true,说明还有未完成的任务// 搜索功能:判断列表中是否存在包含关键词的条目const keywords = '张三';const hasMatch = users.some(user => user.name.includes(keywords));如何记忆some的英文意思是 “一些、若干”,你可以这么记:只要有一些(哪怕一个)满足条件,就算通过。就像期末考试,只要有一科及格,就不算全挂科。什么时候用判断列表中是否存在未完成 / 异常的状态项快速判断搜索是否命中结果权限判断:只要有一个相关权限就能访问1.3 filter:筛选过滤,生成新数组帮助理解遍历数组,把所有满足条件的元素筛选出来,返回一个全新的数组。原数组不会被修改。实战例子// 过滤掉无效的空字符串const strings = ["苹果", "", "香蕉", " ", "橙子", null];const validStrings = strings.filter(s => s && s.trim());// 结果:["苹果", "香蕉", "橙子"]// React中瀑布流布局:筛选出偶数索引的图片放到左列const leftColumnImages = images .filter((_, index) => index % 2 === 0) .map(img => );如何记忆filter的意思就是 “过滤”,顾名思义,就是把符合条件的留下来,不符合的过滤掉。什么时候用数据筛选,比如根据条件过滤列表数据清理无效数据,过滤掉空值、无效项数据分类,把数组分成不同的子集1.4 map:一对一映射,生成新数组帮助理解对数组的每一个元素执行同一个转换函数,返回一个长度和原数组完全一致的全新数组。原数组不会被修改。实战例子// 把字符串数组转成数字数组const strNums = ['1', '2', '3', '4'];const numbers = strNums.map(Number);// 结果:[1, 2, 3, 4]// React中渲染列表:把用户数据转成JSX {users.map(user => ( {user.name} - {user.age} ))} // 接口数据适配:把后端返回的字段名转成前端需要的const apiData = [{ user_name: '张三', user_age: 25 }];const frontData = apiData.map(item => ({ name: item.user_name, age: item.user_age}));如何记忆map的意思是 “映射”,就是一一对应,原数组有多少个,新数组就有多少个,每个元素都做一次转换。什么时候用数据格式转换,比如字段名映射、类型转换渲染列表,把数据转成 UI 组件批量修改数组中的每一项数据1.5 reduce:累计器,万能的汇总工具帮助理解这是数组方法中最强大的一个,它可以把数组中的所有元素,通过一个累加函数,最终汇总成一个结果。这个结果可以是数字、对象、数组,任何类型都可以。它的两个核心参数:回调函数:(累加器acc, 当前元素curr) => 新的累加器累加器的初始值实战例子// 1. 基础求和const numbers = [1, 2, 3, 4];const sum = numbers.reduce((acc, curr) => acc + curr, 0);console.log(sum); // 10// 2. 按分类分组统计const orderItems = [ { category: '食品', price: 20 }, { category: '服装', price: 100 }, { category: '食品', price: 30 }];// 按分类统计总销售额const totalByCategory = orderItems.reduce((acc, item) => { acc[item.category] = (acc[item.category] || 0) + item.price; return acc;}, {});// 结果:{ 食品: 50, 服装: 100 }// 3. 数组扁平化const nestedArr = [[1,2], [3,4], [5,6]];const flatArr = nestedArr.reduce((acc, curr) => acc.concat(curr), []);// 结果:[1,2,3,4,5,6]如何记忆reduce的意思是 “归约、缩减”,就是把一个数组,缩减成一个最终的结果。你可以这么记:只要你需要把多个元素汇总成一个结果,就用 reduce。什么时候用聚合统计:求和、求平均值、统计次数数据分组:把列表按某个字段分成不同的组复杂的数据变换:比如数组转对象、多层数据处理函数式管道:把多个处理步骤串起来1.6 find & findIndex:精准查找帮助理解find:找到数组中第一个满足条件的元素,找不到返回undefinedfindIndex:找到数组中第一个满足条件的元素的索引,找不到返回-1和indexOf的区别是,它们支持自定义复杂的判断条件,而不是只能判断严格相等。实战例子const users = [{ id: 1, name: '张三' },{ id: 2, name: '李四' },{ id: 3, name: '王五' }];// 查找id为2的用户const targetUser = users.find(user => user.id === 2);console.log(targetUser); // { id: 2, name: '李四' }// 查找名字是王五的用户的索引const targetIndex = users.findIndex(user => user.name === '王五');console.log(targetIndex); // 2如何记忆顾名思义,find就是找元素,findIndex就是找元素的索引,找到第一个就停。什么时候用根据 ID 查找特定的用户 / 数据项查找符合特定条件的第一个元素找到元素的位置,用于后续的删除 / 更新操作1.7 flat & flatMap:数组扁平化帮助理解flat:把嵌套的数组展开,参数是展开的深度,传Infinity可以展开所有层级flatMap:相当于先执行map,再对结果执行flat(1),一步到位实战例子// 展开多层嵌套数组const nestedArr = [1, [2, [3, [4]]], 5];const flatArr = nestedArr.flat(Infinity);console.log(flatArr); // [1, 2, 3, 4, 5]// flatMap:处理每行的单词const lines = ["Hello world", "JavaScript is good"];const words = lines.flatMap(line => line.split(' '));// 结果:["Hello", "world", "JavaScript", "is", "good"]如何记忆flat就是 “拍平”,把嵌套的数组拍平成一维的。flatMap就是 map + 拍平,一步搞定。什么时候用处理后端返回的嵌套数组结构处理文本拆分,比如把多行文本拆成单词列表处理树形结构的扁平化数组方法核心对比表方法返回值是否短路典型用途forEachundefined❌执行副作用(打印、修改外部变量)everyboolean✅检查所有元素是否都满足条件someboolean✅检查是否存在至少一个满足条件的元素filter新数组❌筛选出符合条件的元素map新数组❌一对一转换每个元素reduce累积值❌汇总、分组、聚合统计find元素 /undefined✅查找第一个符合条件的元素findIndex索引 /-1✅查找第一个符合条件的元素的位置flat新数组❌展开嵌套数组二、对象核心处理函数除了数组,对象也是我们最常用的数据结构,ES6 + 提供了一系列非常实用的对象处理方法。2.1 Object.keys/values/entries:对象遍历三剑客帮助理解这三个方法把对象转换成数组,方便我们用上面的数组方法来处理:Object.keys(obj):返回对象的所有键名组成的数组Object.values(obj):返回对象的所有值组成的数组Object.entries(obj):返回对象的所有键值对组成的二维数组,每个项是[key, value]实战例子const user = {name: '张三',age: 25,role: 'admin'};// 遍历键名console.log(Object.keys(user)); // ["name", "age", "role"]// 遍历值console.log(Object.values(user)); // ["张三", 25, "admin"]// 遍历键值对,用来渲染表格const tableRows = Object.entries(user).map(([key, value]) => ( <td>{key}</td> <td>{value}</td>));如何记忆顾名思义,keys 拿键,values 拿值,entries 拿键值对。什么时候用遍历对象的属性,替代过去的for...in把对象转成数组,方便用数组的 filter、map 等方法处理渲染键值对形式的详情表格2.2 Object.fromEntries:键值对转回对象帮助理解它是Object.entries的逆操作,把一个键值对的二维数组,转回一个对象。实战例子// 1. 把URL参数转成对象const search = '?name=张三&age=25';const params = new URLSearchParams(search);const query = Object.fromEntries(params);console.log(query); // { name: '张三', age: '25' }// 2. 过滤对象的属性const obj = { a: 1, b: 2, c: 3, d: 4 };// 过滤掉值小于3的属性const filteredObj = Object.fromEntries( Object.entries(obj).filter(([key, value]) => value >= 3));console.log(filteredObj); // { c: 3, d: 4 }如何记忆fromEntries就是 “从 entries 来”,把 entries 的结果转回去。什么时候用解析 URL 查询参数过滤 / 转换对象的属性,先转成 entries 处理,再转回来把 Map 转成普通对象2.3 可选链?.与空值合并??帮助理解可选链?.:访问嵌套对象的属性时,如果中间某个属性不存在,不会报错,直接返回undefined空值合并??:只有当左边的值是null或undefined时,才返回右边的默认值,不会把0、''、false这些假值当成无效值实战例子// 可选链:避免层层判断const user = { profile: { address: { city: '北京' } } };// 传统写法const street = user.address && user.address.street;// 可选链写法const street2 = user.profile?.address?.street; // undefined,不会报错// 空值合并:精准的默认值const age = 0;// 错误写法:||会把0当成假值const wrongAge = age || 18; // 18,不对,0是合法的年龄// 正确写法:??只处理null/undefinedconst rightAge = age ?? 18; // 0,正确如何记忆?.就是 “如果前面的不存在,就别往下找了,直接返回 undefined”??就是 “只有当左边是空的(null/undefined),才用右边的默认值”什么时候用访问嵌套对象的深层属性,防止报错给表单字段设置默认值,避免覆盖合法的假值(0、false)安全的调用可能不存在的函数:api?.getData?.()三、企业级常用数据处理算法除了内置函数,企业开发中还有一些高频使用的自定义算法,用来解决特定的业务问题。3.1 防抖(Debounce):只认最后一次触发帮助理解防抖的核心逻辑是:触发事件后,等待 n 秒,如果 n 秒内没有再次触发,才执行函数;如果触发了,就重新计时。就像电梯:电梯门要关了,这时候有人进来,电梯就会重新倒计时关门,直到没人进来了,才真正关门。实战例子// 防抖函数实现function debounce(fn, delay = 300) { let timer = null; return function(...args) {// 每次触发,清除之前的定时器 clearTimeout(timer); // 重新设置定时器 timer = setTimeout(() => { fn.apply(this, args); }, delay);};}// 实战:搜索框输入联想function search(keyword) { // 调用接口搜索 console.log(搜索:${keyword});}// 包装成防抖函数,用户停止输入500ms后才执行搜索const debouncedSearch = debounce(search, 500);// 绑定输入事件input.addEventListener('input', (e) => { debouncedSearch(e.target.value);});如何记忆防抖,就是 “防止抖动”,用户连续的操作就像抖动,我们只认最后一次稳定的操作。什么时候用搜索框输入联想,避免每次输入都发请求按钮防重复点击,防止用户连续点击提交多次窗口resize事件,避免窗口大小改变时频繁触发重排滚动结束后触发的操作3.2 节流(Throttle):固定频率执行帮助理解节流的核心逻辑是:规定时间内,无论触发多少次,函数只执行一次。就像水龙头:不管你开多大,水都只能每隔固定时间滴一滴,不会因为你开的大就滴的更快。实战例子// 节流函数实现function throttle(fn, limit = 300) { let inThrottle = false; return function(...args) {// 如果不在冷却期,才执行 if (!inThrottle) { fn.apply(this, args); inThrottle = true; // 冷却期结束后重置 setTimeout(() => { inThrottle = false; }, limit); }};}// 实战:页面滚动懒加载function handleScroll() { // 检查滚动位置,加载新的内容 console.log('处理滚动...');}// 包装成节流函数,每200ms最多执行一次const throttledScroll = throttle(handleScroll, 200);window.addEventListener('scroll', throttledScroll);如何记忆节流,就是 “节制流量”,把高频的触发,稀释成固定频率的执行,保证不会太频繁。什么时候用页面滚动事件,比如懒加载、滚动位置监听鼠标移动事件游戏中的射击冷却高频的 DOM 事件处理3.3 扁平数组转树形结构帮助理解后端经常会返回一个扁平的列表,每个元素有id和parentId,前端需要把它转换成带children的树形结构,用来渲染菜单、部门树、分类树等组件。最优的实现是用 Map 做映射,时间复杂度 O (n),比递归的 O (n²) 高效很多。实战例子/**扁平数组转树形结构@param {Array} items 扁平数组@param {Object} config 配置,兼容不同的字段名 */function arrayToTree(items, config = {}) { const { id = 'id', pid = 'parentId', children = 'children' } = config; // 用Map存所有节点,方便快速查找 const nodeMap = new Map(); const result = [];// 先给每个节点加上children属性,并存到Map里 for (const item of items) {nodeMap.set(item[id], { ...item, [children]: [] });}// 遍历每个节点,找到它的父节点,把自己塞到父节点的children里 for (const item of items) {const node = nodeMap.get(item[id]); if (item[pid] === null || item[pid] === 0) { // 根节点,直接放到结果里 result.push(node); } else { // 找到父节点 const parent = nodeMap.get(item[pid]); if (parent) { parent[children].push(node); } }}return result;}// 实战:后端返回的部门列表const flatDepartments = [ { id: 1, name: '总公司', parentId: null }, { id: 2, name: '研发部', parentId: 1 }, { id: 3, name: '产品部', parentId: 1 }, { id: 4, name: '前端组', parentId: 2 }, { id: 5, name: '后端组', parentId: 2 },];const tree = arrayToTree(flatDepartments);console.log(tree);/* 结果:[ {id: 1, name: '总公司', parentId: null, children: [ { id: 2, name: '研发部', parentId: 1, children: [...] }, { id: 3, name: '产品部', parentId: 1, children: [] } ]}]*/如何记忆两步走:先把所有节点存到 Map 里,建立 ID 到节点的映射每个节点找自己的爸爸,把自己塞到爸爸的孩子里最后把没有爸爸的根节点拿出来什么时候用渲染 ElementUI/AntD 的树形组件构建系统菜单、权限菜单处理商品分类、部门组织架构3.4 深拷贝:完全复制对象帮助理解引用类型的赋值只是复制了引用,修改新对象会影响原对象。深拷贝会创建一个完全独立的新对象,无论嵌套多深,两个对象都完全隔离。我们需要处理特殊类型(Date、RegExp)和循环引用,避免 JSON 方法的缺陷。实战例子/**完整的深拷贝实现,支持循环引用、Date、RegExp等 */function deepClone(obj, map = new WeakMap()) { // 基本类型,直接返回 if (obj === null || typeof obj !== 'object') {return obj;}// 处理循环引用:如果已经拷贝过这个对象,直接返回缓存的副本 if (map.has(obj)) {return map.get(obj);}// 处理特殊对象类型 if (obj instanceof Date) {return new Date(obj.getTime());} if (obj instanceof RegExp) {return new RegExp(obj.source, obj.flags);}// 创建新的对象/数组 const cloneObj = Array.isArray(obj) ? [] : {}; // 缓存已经拷贝的对象 map.set(obj, cloneObj);// 递归拷贝每个属性 for (const key in obj) {if (obj.hasOwnProperty(key)) { cloneObj[key] = deepClone(obj[key], map); }}return cloneObj;}// 实战:编辑表单时,不修改原数据const originalUser = await api.getUserInfo();// 深拷贝一份,用来修改表单,不会影响原数据const formData = deepClone(originalUser);// 修改表单数据formData.name = '新名字';// originalUser不会被修改如何记忆递归遍历每个属性:基本类型直接返回特殊类型(Date、正则)单独处理用 WeakMap 存已经拷贝过的对象,防止循环引用对象 / 数组就递归继续挖什么时候用表单编辑,需要修改数据但不想影响原接口数据Redux/Vuex 中的不可变状态更新需要完全隔离两个对象的场景3.5 表格数据处理流水线企业后台系统中最常见的场景:对表格数据做过滤→排序→分页的流水线处理,用函数式的链式调用,一行搞定。实战例子function processTableData(data, options) { const { filterKey, sortKey, sortAsc, page, pageSize } = options; return data// 1. 过滤:模糊搜索名字 .filter(item => item.name.includes(filterKey)) // 2. 排序:根据指定字段排序 .sort((a, b) => { const diff = a[sortKey] - b[sortKey]; return sortAsc ? diff : -diff; }) // 3. 分页:截取当前页的数据 .slice((page - 1) * pageSize, page * pageSize);}// 使用const tableData = processTableData(rawData, { filterKey: '张', sortKey: 'age', sortAsc: false, page: 1, pageSize: 10});什么时候用后台管理系统的表格数据处理前端本地的列表筛选排序分页总结企业开发中的数据处理,核心就是用好原生的函数式方法,配合常用的通用算法,避免写大量的循环和判断。记住一个简单的选择口诀:想判断?every(全对)/some(有对的)想筛选?filter想转换?map想汇总?reduce想查找?find/findIndex高频事件?防抖(最后一次)/ 节流(固定频率)树形数据?扁平转树改对象怕影响原数据?深拷贝掌握这些,你就能应对 90% 以上的企业级数据处理场景了。
2026年03月24日
13 阅读
0 评论
0 点赞
2026-03-21
Java 8 实战开发与 Spring Boot 学习笔记
目录第一部分:Java 8 核心特性1. Lambda 表达式2. 函数式接口3. Stream API4. Optional 类5. 日期时间 API6. 接口默认方法与静态方法第二部分:Spring Boot 实战开发1. Spring Boot 快速入门2. 自动配置原理3. Web 开发4. 数据访问5. 安全认证6. 日志与监控7. 测试8. 部署与运维第一部分:Java 8 核心特性1. Lambda 表达式1.1 什么是 Lambda 表达式Lambda 表达式是 Java 8 引入的一种简洁的函数式编程语法,允许将函数作为方法参数传递。语法格式:(parameters) -> expression 或 (parameters) -> { statements; }1.2 Lambda 表达式示例// 传统方式 Runnable r1 = new Runnable() { @Override public void run() { System.out.println("Hello World!"); } }; // Lambda 方式 Runnable r2 = () -> System.out.println("Hello World!"); // 带参数的 Lambda Comparator<Integer> cmp1 = (a, b) -> a.compareTo(b); // 代码块形式 Comparator<Integer> cmp2 = (a, b) -> { System.out.println("Comparing: " + a + " and " + b); return a.compareTo(b); };1.3 变量作用域Lambda 表达式可以访问外部变量,但外部变量必须是 final 或 effectively final 的:String prefix = "Hello, "; List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); // 正确:prefix 是 effectively final names.forEach(name -> System.out.println(prefix + name)); // 错误:不能修改外部变量 // prefix = "Hi, "; // 编译错误2. 函数式接口2.1 函数式接口定义函数式接口是只包含一个抽象方法的接口,使用 @FunctionalInterface 注解标记。@FunctionalInterface interface MyFunction { void apply(String s); }2.2 Java 8 内置函数式接口接口抽象方法描述Predicate<T>boolean test(T t)断言型接口Function<T, R>R apply(T t)函数型接口Consumer<T>void accept(T t)消费型接口Supplier<T>T get()供给型接口2.3 内置函数式接口示例// Predicate:判断 Predicate<String> isLongerThan5 = s -> s.length() > 5; System.out.println(isLongerThan5.test("Hello")); // false // Function:转换 Function<String, Integer> stringToLength = s -> s.length(); System.out.println(stringToLength.apply("Hello")); // 5 // Consumer:消费 Consumer<String> printer = s -> System.out.println(s); printer.accept("Hello World"); // Supplier:供给 Supplier<Double> randomSupplier = () -> Math.random(); System.out.println(randomSupplier.get());2.4 方法引用方法引用是 Lambda 表达式的简写形式:// 静态方法引用 Function<String, Integer> parseInt = Integer::parseInt; // 实例方法引用 List<String> list = Arrays.asList("a", "b", "c"); list.forEach(System.out::println); // 对象方法引用 String str = "Hello"; Predicate<String> startsWith = str::startsWith; // 构造方法引用 Supplier<List<String>> listSupplier = ArrayList::new;3. Stream API3.1 Stream 概述Stream 是 Java 8 中处理集合的关键抽象概念,用于对集合进行函数式操作。Stream 操作流程:创建 Stream中间操作(返回 Stream)终端操作(产生结果)3.2 创建 Stream// 从集合创建 List<String> list = Arrays.asList("a", "b", "c"); Stream<String> stream1 = list.stream(); // 从数组创建 String[] array = {"a", "b", "c"}; Stream<String> stream2 = Arrays.stream(array); // 使用 Stream.of Stream<String> stream3 = Stream.of("a", "b", "c"); // 无限流 Stream<Integer> stream4 = Stream.iterate(0, n -> n + 2); // 偶数 Stream<Double> stream5 = Stream.generate(Math::random);3.3 中间操作List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // filter:过滤 numbers.stream() .filter(n -> n % 2 == 0) .forEach(System.out::println); // map:转换 numbers.stream() .map(n -> n * n) .forEach(System.out::println); // sorted:排序 numbers.stream() .sorted((a, b) -> b - a) .forEach(System.out::println); // distinct:去重 List<Integer> duplicates = Arrays.asList(1, 2, 2, 3, 3, 3); duplicates.stream() .distinct() .forEach(System.out::println); // limit:限制数量 numbers.stream() .limit(5) .forEach(System.out::println);3.4 终端操作// collect:收集 List<String> words = Arrays.asList("apple", "banana", "cherry"); List<String> result = words.stream() .filter(w -> w.length() > 5) .collect(Collectors.toList()); // count:计数 long count = words.stream().count(); // reduce:归约 Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b); // forEach:遍历 words.stream().forEach(System.out::println); // anyMatch / allMatch / noneMatch boolean hasLongWord = words.stream().anyMatch(w -> w.length() > 10); boolean allShort = words.stream().allMatch(w -> w.length() < 10); // findFirst / findAny Optional<String> first = words.stream().findFirst(); Optional<String> any = words.stream().findAny();3.5 Collectors 工具类// 转 List List<String> list = words.stream().collect(Collectors.toList()); // 转 Set Set<String> set = words.stream().collect(Collectors.toSet()); // 转 Map Map<Integer, String> map = words.stream() .collect(Collectors.toMap(String::length, w -> w)); // 分组 Map<Integer, List<String>> groupByLength = words.stream() .collect(Collectors.groupingBy(String::length)); // 分区 Map<Boolean, List<String>> partition = words.stream() .collect(Collectors.partitioningBy(w -> w.length() > 5)); // 统计 IntSummaryStatistics stats = numbers.stream() .collect(Collectors.summarizingInt(Integer::intValue)); System.out.println("Count: " + stats.getCount()); System.out.println("Sum: " + stats.getSum()); System.out.println("Avg: " + stats.getAverage()); System.out.println("Max: " + stats.getMax()); System.out.println("Min: " + stats.getMin()); // 连接字符串 String joined = words.stream().collect(Collectors.joining(", "));3.6 并行流// 顺序流 long start = System.currentTimeMillis(); numbers.stream().forEach(n -> { try { Thread.sleep(10); } catch (InterruptedException e) {} }); System.out.println("Sequential: " + (System.currentTimeMillis() - start) + "ms"); // 并行流 start = System.currentTimeMillis(); numbers.parallelStream().forEach(n -> { try { Thread.sleep(10); } catch (InterruptedException e) {} }); System.out.println("Parallel: " + (System.currentTimeMillis() - start) + "ms");4. Optional 类4.1 Optional 概述Optional 是一个容器对象,用于优雅地处理空值(NullPointerException)。4.2 创建 Optional// 创建非空 Optional Optional<String> opt1 = Optional.of("Hello"); // 创建允许空的 Optional Optional<String> opt2 = Optional.ofNullable(null); // 创建空 Optional Optional<String> opt3 = Optional.empty();4.3 Optional 常用方法Optional<String> optional = Optional.of("Hello"); // isPresent:判断是否有值 if (optional.isPresent()) { System.out.println(optional.get()); } // ifPresent:如果有值则执行 optional.ifPresent(s -> System.out.println(s)); // orElse:如果为空则返回默认值 String result1 = optional.orElse("Default"); // orElseGet:如果为空则调用供给函数 String result2 = optional.orElseGet(() -> "Default"); // orElseThrow:如果为空则抛出异常 String result3 = optional.orElseThrow(IllegalArgumentException::new); // map:转换 Optional<Integer> length = optional.map(String::length); // flatMap:扁平转换 Optional<String> upper = optional.flatMap(s -> Optional.of(s.toUpperCase())); // filter:过滤 Optional<String> filtered = optional.filter(s -> s.length() > 5);4.4 Optional 最佳实践// 传统方式 String name = user != null ? user.getName() : "Unknown"; // Optional 方式 String name = Optional.ofNullable(user) .map(User::getName) .orElse("Unknown"); // 多层嵌套 String city = Optional.ofNullable(user) .map(User::getAddress) .map(Address::getCity) .orElse("Unknown");5. 日期时间 API5.1 新日期时间 API 概述Java 8 引入了全新的日期时间 API,位于 java.time 包下。类描述LocalDate日期(年-月-日)LocalTime时间(时:分:秒)LocalDateTime日期时间ZonedDateTime带时区的日期时间Instant时间戳Duration时间间隔(秒/纳秒)Period日期间隔(年/月/日)DateTimeFormatter日期格式化5.2 LocalDate / LocalTime / LocalDateTime// LocalDate LocalDate today = LocalDate.now(); LocalDate date = LocalDate.of(2024, 1, 1); LocalDate parsedDate = LocalDate.parse("2024-01-01"); int year = date.getYear(); Month month = date.getMonth(); int dayOfMonth = date.getDayOfMonth(); DayOfWeek dayOfWeek = date.getDayOfWeek(); // 日期运算 LocalDate tomorrow = today.plusDays(1); LocalDate lastWeek = today.minusWeeks(1); // LocalTime LocalTime now = LocalTime.now(); LocalTime time = LocalTime.of(14, 30, 0); LocalTime parsedTime = LocalTime.parse("14:30:00"); // LocalDateTime LocalDateTime nowDateTime = LocalDateTime.now(); LocalDateTime dateTime = LocalDateTime.of(2024, 1, 1, 14, 30);5.3 Instant 与 Duration / Period// Instant:时间戳 Instant now = Instant.now(); Instant timestamp = Instant.ofEpochMilli(System.currentTimeMillis()); // Duration:时间间隔 Instant start = Instant.now(); Thread.sleep(1000); Instant end = Instant.now(); Duration duration = Duration.between(start, end); System.out.println("Seconds: " + duration.getSeconds()); System.out.println("Millis: " + duration.toMillis()); // Period:日期间隔 LocalDate date1 = LocalDate.of(2024, 1, 1); LocalDate date2 = LocalDate.of(2024, 12, 31); Period period = Period.between(date1, date2); System.out.println("Years: " + period.getYears()); System.out.println("Months: " + period.getMonths()); System.out.println("Days: " + period.getDays());5.4 日期格式化与解析// 预定义格式 DateTimeFormatter isoDate = DateTimeFormatter.ISO_LOCAL_DATE; String dateStr = LocalDate.now().format(isoDate); LocalDate date = LocalDate.parse(dateStr, isoDate); // 自定义格式 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String formatted = LocalDateTime.now().format(formatter); LocalDateTime parsed = LocalDateTime.parse("2024-01-01 14:30:00", formatter);5.5 时区处理// ZonedDateTime ZonedDateTime nowBeijing = ZonedDateTime.now(ZoneId.of("Asia/Shanghai")); ZonedDateTime nowNewYork = ZonedDateTime.now(ZoneId.of("America/New_York")); // 时区转换 ZonedDateTime beijingTime = ZonedDateTime.of(2024, 1, 1, 14, 30, 0, 0, ZoneId.of("Asia/Shanghai")); ZonedDateTime newYorkTime = beijingTime.withZoneSameInstant(ZoneId.of("America/New_York"));6. 接口默认方法与静态方法6.1 默认方法interface MyInterface { void abstractMethod(); default void defaultMethod() { System.out.println("Default method"); } } class MyClass implements MyInterface { @Override public void abstractMethod() { System.out.println("Abstract method implementation"); } } public class Main { public static void main(String[] args) { MyClass obj = new MyClass(); obj.abstractMethod(); obj.defaultMethod(); } }6.2 静态方法interface MathUtil { static int add(int a, int b) { return a + b; } static int multiply(int a, int b) { return a * b; } } public class Main { public static void main(String[] args) { System.out.println(MathUtil.add(1, 2)); System.out.println(MathUtil.multiply(3, 4)); } }第二部分:Spring Boot 实战开发1. Spring Boot 快速入门1.1 什么是 Spring BootSpring Boot 是一个快速开发框架,基于 Spring 框架,简化了 Spring 应用的初始搭建和开发过程。核心特性:自动配置独立运行(内嵌 Tomcat)起步依赖(Starters)生产级特性(监控、健康检查等)1.2 创建第一个 Spring Boot 项目使用 Spring Initializr 创建:访问 https://start.spring.io/,选择:Project: Maven 或 GradleLanguage: JavaSpring Boot: 最新稳定版本Dependencies: Spring Webpom.xml 示例:<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.0</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>demo</artifactId> <version>1.0.0</version> <name>demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>1.3 主类与启动package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }1.4 第一个 REST 控制器package com.example.demo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api") public class HelloController { @GetMapping("/hello") public String hello() { return "Hello, Spring Boot!"; } }1.5 配置文件 application.ymlserver: port: 8080 servlet: context-path: /demo spring: application: name: demo-service2. 自动配置原理2.1 @SpringBootApplication 注解@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { }包含三个核心注解:@SpringBootConfiguration:标记为配置类@EnableAutoConfiguration:启用自动配置@ComponentScan:组件扫描2.2 自动配置流程Spring Boot 启动时加载 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports根据条件注解(@ConditionalOnClass、@ConditionalOnMissingBean 等)决定是否加载配置自动配置类创建所需的 Bean2.3 自定义自动配置创建自定义自动配置类:@Configuration @ConditionalOnClass(MyService.class) @EnableConfigurationProperties(MyProperties.class) public class MyAutoConfiguration { @Bean @ConditionalOnMissingBean public MyService myService(MyProperties properties) { return new MyService(properties.getMessage()); } }配置属性类:@ConfigurationProperties(prefix = "my.service") public class MyProperties { private String message = "default message"; public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }3. Web 开发3.1 RESTful API 开发@RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; @Autowired public UserController(UserService userService) { this.userService = userService; } @GetMapping public ResponseEntity<List<User>> getAllUsers() { List<User> users = userService.findAll(); return ResponseEntity.ok(users); } @GetMapping("/{id}") public ResponseEntity<User> getUserById(@PathVariable Long id) { return userService.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @PostMapping public ResponseEntity<User> createUser(@Valid @RequestBody User user) { User createdUser = userService.save(user); URI location = URI.create("/api/users/" + createdUser.getId()); return ResponseEntity.created(location).body(createdUser); } @PutMapping("/{id}") public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User user) { return userService.findById(id) .map(existingUser -> { user.setId(id); User updatedUser = userService.save(user); return ResponseEntity.ok(updatedUser); }) .orElse(ResponseEntity.notFound().build()); } @DeleteMapping("/{id}") public ResponseEntity<Void> deleteUser(@PathVariable Long id) { if (userService.existsById(id)) { userService.deleteById(id); return ResponseEntity.noContent().build(); } return ResponseEntity.notFound().build(); } }3.2 参数验证public class User { @NotBlank(message = "Name is required") @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters") private String name; @Email(message = "Invalid email format") @NotBlank(message = "Email is required") private String email; @Min(value = 18, message = "Age must be at least 18") private Integer age; // getters and setters }3.3 全局异常处理@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) { ErrorResponse error = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage()); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex) { List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.toList()); ErrorResponse error = new ErrorResponse(HttpStatus.BAD_REQUEST.value(), "Validation failed", errors); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error); } @ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) { ErrorResponse error = new ErrorResponse( HttpStatus.INTERNAL_SERVER_ERROR.value(), "An unexpected error occurred" ); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); } }3.4 文件上传@RestController @RequestMapping("/api/files") public class FileController { @PostMapping("/upload") public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) { try { String fileName = file.getOriginalFilename(); Path path = Paths.get("uploads/" + fileName); Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING); return ResponseEntity.ok("File uploaded successfully: " + fileName); } catch (IOException e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("Failed to upload file: " + e.getMessage()); } } @GetMapping("/download/{filename}") public ResponseEntity<Resource> downloadFile(@PathVariable String filename) { Path path = Paths.get("uploads/" + filename); try { Resource resource = new UrlResource(path.toUri()); if (resource.exists() && resource.isReadable()) { return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"") .body(resource); } else { return ResponseEntity.notFound().build(); } } catch (MalformedURLException e) { return ResponseEntity.badRequest().build(); } } }4. 数据访问4.1 Spring Data JPA实体类:@Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String email; @Column(nullable = false) private String name; @Column(nullable = false) private String password; @Column(name = "created_at") private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); updatedAt = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } // getters and setters }Repository 接口:public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByEmail(String email); List<User> findByNameContainingIgnoreCase(String name); boolean existsByEmail(String email); @Query("SELECT u FROM User u WHERE u.createdAt > :since") List<User> findUsersCreatedAfter(@Param("since") LocalDateTime since); @Modifying @Query("UPDATE User u SET u.password = :password WHERE u.id = :id") int updatePassword(@Param("id") Long id, @Param("password") String password); }Service 层:@Service @Transactional public class UserService { private final UserRepository userRepository; @Autowired public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public List<User> findAll() { return userRepository.findAll(); } public Optional<User> findById(Long id) { return userRepository.findById(id); } public User save(User user) { if (userRepository.existsByEmail(user.getEmail())) { throw new RuntimeException("Email already exists"); } return userRepository.save(user); } public void deleteById(Long id) { userRepository.deleteById(id); } public boolean existsById(Long id) { return userRepository.existsById(id); } }4.2 分页与排序@GetMapping("/page") public ResponseEntity<Page<User>> getUsersPage( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "id") String sortBy, @RequestParam(defaultValue = "ASC") Sort.Direction direction) { Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortBy)); Page<User> usersPage = userRepository.findAll(pageable); return ResponseEntity.ok(usersPage); }4.3 配置数据源(application.yml)spring: datasource: url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC username: root password: password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect5. 安全认证5.1 Spring Security 基础依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>安全配置:@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**", "/api/auth/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }5.2 JWT 认证JWT 工具类:@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private long expiration; public String generateToken(String username, List<String> roles) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + expiration); return Jwts.builder() .setSubject(username) .claim("roles", roles) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public String getUsernameFromToken(String token) { Claims claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); return claims.getSubject(); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(secret).parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; } } }JWT 过滤器:@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsService userDetailsService; @Autowired public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) { this.jwtUtil = jwtUtil; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); if (jwtUtil.validateToken(token)) { String username = jwtUtil.getUsernameFromToken(token); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); } } filterChain.doFilter(request, response); } }6. 日志与监控6.1 日志配置application.yml:logging: level: root: INFO com.example.demo: DEBUG org.springframework.web: INFO org.hibernate.SQL: DEBUG pattern: console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" file: name: logs/application.log max-size: 10MB max-history: 30代码中使用日志:@Service public class UserService { private static final Logger logger = LoggerFactory.getLogger(UserService.class); public User save(User user) { logger.info("Saving user: {}", user.getEmail()); try { User savedUser = userRepository.save(user); logger.debug("User saved successfully with id: {}", savedUser.getId()); return savedUser; } catch (Exception e) { logger.error("Error saving user", e); throw e; } } }6.2 Spring Boot Actuator依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>配置:management: endpoints: web: exposure: include: health,info,metrics,loggers endpoint: health: show-details: always访问端点:/actuator/health - 健康检查/actuator/info - 应用信息/actuator/metrics - 指标/actuator/loggers - 日志配置7. 测试7.1 单元测试@SpringBootTest class UserServiceTest { @MockBean private UserRepository userRepository; @Autowired private UserService userService; @Test void testFindAll() { User user1 = new User(); user1.setId(1L); user1.setName("Alice"); User user2 = new User(); user2.setId(2L); user2.setName("Bob"); when(userRepository.findAll()).thenReturn(Arrays.asList(user1, user2)); List<User> users = userService.findAll(); assertEquals(2, users.size()); verify(userRepository, times(1)).findAll(); } }7.2 Web 层测试@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void testGetAllUsers() throws Exception { User user = new User(); user.setId(1L); user.setName("Alice"); when(userService.findAll()).thenReturn(Arrays.asList(user)); mockMvc.perform(get("/api/users")) .andExpect(status().isOk()) .andExpect(jsonPath("$", hasSize(1))) .andExpect(jsonPath("$[0].name").value("Alice")); } }8. 部署与运维8.1 打包成 JARMaven 命令:mvn clean package运行 JAR:java -jar target/demo-1.0.0.jar指定配置文件运行:java -jar target/demo-1.0.0.jar --spring.profiles.active=prod8.2 Docker 部署Dockerfile:FROM eclipse-temurin:17-jdk-alpine VOLUME /tmp COPY target/*.jar app.jar ENTRYPOINT ["java","-jar","/app.jar"]docker-compose.yml:version: '3.8' services: app: build: . ports: - "8080:8080" depends_on: - db environment: SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/mydb SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: password db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: mydb ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql volumes: mysql-data:附录A. 常用注解速查表注解描述@SpringBootApplication主应用类注解@RestControllerREST 控制器@RequestMapping请求映射@GetMappingGET 请求映射@PostMappingPOST 请求映射@PutMappingPUT 请求映射@DeleteMappingDELETE 请求映射@PathVariable路径变量@RequestParam请求参数@RequestBody请求体@Autowired自动装配@Service服务层@Repository数据访问层@EntityJPA 实体@Transactional事务@Configuration配置类@BeanBean 定义B. 参考资源Spring Boot 官方文档Java 8 官方文档Spring Security 官方文档Spring Data JPA 官方文档
2026年03月21日
12 阅读
0 评论
0 点赞
2026-03-17
Vue2详细笔记
Vue2 企业级开发实战笔记技术栈限定:Vue 2.x + Vue Router 3.x + Vuex 3.x模块一:核心基础(必掌握)1.1 Vue2 响应式原理(简化版)核心作用理解 Vue2 如何实现数据响应式,从底层认知「数据变更 → 视图更新」的机制,是高效开发与排查响应式失效问题的基础。实现方式Vue2 使用 Object.defineProperty 实现数据劫持,结合 发布-订阅模式:递归遍历 data 对象的所有属性通过 getter 收集依赖通过 setter 触发更新代码示例// 简化版响应式实现 function defineReactive(obj, key, val) { // 递归处理嵌套对象 observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { console.log('收集依赖:', key); return val; }, set(newVal) { if (newVal === val) return; console.log('触发更新:', key); val = newVal; observe(newVal); } }); } function observe(obj) { if (typeof obj !== 'object' || obj === null) return; Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]); }); } // 使用示例 const data = { name: '张三', info: { age: 25 } }; observe(data); data.name; // 输出:收集依赖: name data.name = '李四'; // 输出:触发更新: name实战注意事项⚠️ 易踩坑点:直接添加新属性或删除属性无法触发响应式更新(需使用 $set / $delete)直接通过数组下标修改元素无法触发响应式(需使用 Vue.set 或数组变异方法)1.2 选项式 API 核心用法1.2.1 data核心作用:存储组件的响应式数据export default { data() { return { message: 'Hello Vue', userInfo: { name: '张三', age: 25 }, list: [1, 2, 3] }; } };✅ 最佳实践:data 必须返回一个函数(避免组件实例间共享数据)1.2.2 methods核心作用:定义组件的方法,处理用户交互、业务逻辑export default { data() { return { count: 0 }; }, methods: { increment() { this.count++; }, handleClick(event) { console.log('点击事件:', event); }, async fetchData() { const res = await api.getUser(); this.userInfo = res.data; } } };⚠️ 易踩坑点:methods 中不要使用箭头函数(会丢失 this 指向)1.2.3 computed核心作用:基于响应式数据计算衍生值,有缓存export default { data() { return { firstName: '张', lastName: '三', price: 100, count: 2 }; }, computed: { fullName() { return this.firstName + this.lastName; }, totalPrice: { get() { return this.price * this.count; }, set(val) { this.price = val / this.count; } } } };✅ 最佳实践:计算属性应纯函数,无副作用,仅用于计算1.2.4 watch核心作用:监听数据变化,执行异步或开销较大的操作export default { data() { return { searchText: '', userInfo: { name: '张三' } }; }, watch: { searchText(newVal, oldVal) { if (newVal) { this.debounceSearch(newVal); } }, userInfo: { handler(newVal) { console.log('用户信息变更:', newVal); }, deep: true, // 深度监听 immediate: true // 初始化时立即执行 } }, methods: { debounceSearch: _.debounce(function(keyword) { // 搜索逻辑 }, 300) } };1.3 模板语法核心指令系统指令作用示例v-bind动态绑定属性v-bind:src="imgUrl" 或 :src="imgUrl"v-on绑定事件v-on:click="handleClick" 或 @click="handleClick"v-model双向数据绑定<input v-model="inputVal">v-if / v-else条件渲染(真实 DOM 切换)<div v-if="isShow">内容</div>v-show条件渲染(CSS display 切换)<div v-show="isShow">内容</div>v-for列表渲染<li v-for="(item, index) in list" :key="item.id">{{ item.name }}</li>过滤器(Vue2 特有)// 定义过滤器 export default { filters: { capitalize(value) { if (!value) return ''; return value.charAt(0).toUpperCase() + value.slice(1); }, formatDate(value, fmt = 'YYYY-MM-DD') { return moment(value).format(fmt); } } };<!-- 使用过滤器 --> <p>{{ message | capitalize }}</p> <p>{{ date | formatDate('YYYY-MM-DD HH:mm') }}</p>1.4 生命周期详解八大常用钩子执行时机与应用场景生命周期钩子执行时机典型应用场景beforeCreate实例初始化后,数据观测和事件配置之前很少使用,可用于加载非响应式数据created实例创建完成,数据已观测,但未挂载 DOM✅ 发起异步请求、初始化数据beforeMount挂载开始前,模板编译完成,但未替换到页面很少使用mounted实例挂载完成,DOM 已渲染✅ 操作 DOM、初始化第三方库、获取元素尺寸beforeUpdate数据更新前,DOM 更新前可获取更新前的 DOM 状态updated数据更新后,DOM 已更新操作更新后的 DOMbeforeDestroy实例销毁前✅ 清除定时器、取消事件监听、销毁第三方实例destroyed实例销毁后很少使用export default { data() { return { timer: null }; }, created() { this.fetchData(); }, mounted() { this.initChart(); this.timer = setInterval(() => { console.log('定时器执行'); }, 1000); }, beforeDestroy() { clearInterval(this.timer); }, methods: { fetchData() { /* 异步请求 */ }, initChart() { /* 初始化图表 */ } } };模块二:开发高频技巧(重点详解)2.1 组件通信方案2.1.1 Props / $emit 传值(父子组件)核心作用:实现父子组件间的数据传递与事件通信Props 传值规范// 子组件:UserCard.vue export default { name: 'UserCard', props: { userId: { type: Number, required: true }, userInfo: { type: Object, default: () => ({}) }, editable: { type: Boolean, default: false }, tags: { type: Array, default: () => [] } }, methods: { handleEdit() { this.$emit('edit', this.userId); }, handleDelete() { this.$emit('delete', { id: this.userId, confirm: true }); } } };<!-- 父组件 --> <template> <UserCard :user-id="123" :user-info="user" :editable="true" @edit="handleUserEdit" @delete="handleUserDelete" /> </template>✅ 最佳实践:Props 命名使用 camelCase,在模板中使用 kebab-case始终定义 Props 的类型校验对象/数组类型的默认值必须返回函数替代方案对比方案优点缺点适用场景Props / $emit单向数据流清晰,易于追踪跨多层级需层层传递父子组件直接通信事件总线跨任意组件通信事件管理混乱,难维护小型应用跨组件通信Vuex集中式状态管理,可追踪增加代码复杂度大型应用全局状态2.1.2 事件总线(EventBus)核心作用:实现任意组件间的通信,无需考虑层级关系// 1. 创建事件总线:event-bus.js import Vue from 'vue'; export const EventBus = new Vue();// 2. 发送事件的组件 import { EventBus } from '@/utils/event-bus'; export default { methods: { sendMessage() { EventBus.$emit('message', { text: 'Hello', from: 'ComponentA' }); } } };// 3. 接收事件的组件 import { EventBus } from '@/utils/event-bus'; export default { created() { EventBus.$on('message', this.handleMessage); }, beforeDestroy() { // ⚠️ 关键:组件销毁前必须取消监听,防止内存泄漏 EventBus.$off('message', this.handleMessage); }, methods: { handleMessage(data) { console.log('收到消息:', data); } } };⚠️ 易踩坑点:忘记在 beforeDestroy 中取消事件监听,会导致内存泄漏和重复触发2.1.3 Vuex 状态管理最佳实践核心作用:集中管理应用全局状态,适合大型应用// store/index.js import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export default new Vuex.Store({ state: { userInfo: null, token: localStorage.getItem('token') || '', loading: false }, getters: { isLoggedIn: state => !!state.token, userName: state => state.userInfo?.name || '游客' }, mutations: { SET_TOKEN(state, token) { state.token = token; localStorage.setItem('token', token); }, SET_USER_INFO(state, info) { state.userInfo = info; }, SET_LOADING(state, status) { state.loading = status; }, LOGOUT(state) { state.token = ''; state.userInfo = null; localStorage.removeItem('token'); } }, actions: { async login({ commit }, credentials) { commit('SET_LOADING', true); try { const res = await api.login(credentials); commit('SET_TOKEN', res.data.token); commit('SET_USER_INFO', res.data.user); return res; } finally { commit('SET_LOADING', false); } }, async fetchUserInfo({ commit, state }) { if (!state.token) return; const res = await api.getUserInfo(); commit('SET_USER_INFO', res.data); }, logout({ commit }) { commit('LOGOUT'); } }, modules: { // 按模块划分大型状态 cart: { namespaced: true, state: { items: [] }, mutations: { ADD_ITEM(state, item) { state.items.push(item); } }, actions: { addToCart({ commit }, item) { commit('ADD_ITEM', item); } } } } });// 组件中使用 import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'; export default { computed: { ...mapState(['userInfo', 'token']), ...mapGetters(['isLoggedIn', 'userName']), ...mapState('cart', ['items']) }, methods: { ...mapMutations(['SET_LOADING']), ...mapActions(['login', 'logout']), ...mapActions('cart', ['addToCart']), async handleLogin() { await this.login({ username: 'admin', password: '123456' }); } } };✅ Vuex 最佳实践:使用 namespaced: true 隔离模块Mutations 只做同步状态更新,命名全大写Actions 处理异步逻辑Getters 用于计算派生状态使用辅助函数简化代码2.2 复杂表单校验核心作用处理企业级复杂表单,包含必填校验、格式校验、自定义规则校验等方案一:自定义表单校验<template> <div class="form-container"> <form @submit.prevent="handleSubmit"> <div class="form-item"> <label>用户名 *</label> <input v-model="form.username" @blur="validateField('username')" /> <span v-if="errors.username" class="error">{{ errors.username }}</span> </div> <div class="form-item"> <label>邮箱 *</label> <input v-model="form.email" @blur="validateField('email')" /> <span v-if="errors.email" class="error">{{ errors.email }}</span> </div> <div class="form-item"> <label>密码 *</label> <input type="password" v-model="form.password" @blur="validateField('password')" /> <span v-if="errors.password" class="error">{{ errors.password }}</span> </div> <div class="form-item"> <label>确认密码 *</label> <input type="password" v-model="form.confirmPassword" @blur="validateField('confirmPassword')" /> <span v-if="errors.confirmPassword" class="error">{{ errors.confirmPassword }}</span> </div> <button type="submit" :disabled="isSubmitting">提交</button> </form> </div> </template> <script> export default { data() { return { form: { username: '', email: '', password: '', confirmPassword: '' }, errors: {}, isSubmitting: false, rules: { username: [ { required: true, message: '请输入用户名' }, { min: 3, max: 20, message: '用户名长度3-20个字符' } ], email: [ { required: true, message: '请输入邮箱' }, { pattern: /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/, message: '邮箱格式不正确' } ], password: [ { required: true, message: '请输入密码' }, { min: 6, message: '密码至少6个字符' } ], confirmPassword: [ { required: true, message: '请确认密码' }, { validator: (val) => val === this.form.password, message: '两次密码不一致' } ] } }; }, methods: { validateField(field) { const rules = this.rules[field]; const value = this.form[field]; for (const rule of rules) { if (rule.required && !value) { this.$set(this.errors, field, rule.message); return false; } if (rule.min && value.length < rule.min) { this.$set(this.errors, field, rule.message); return false; } if (rule.max && value.length > rule.max) { this.$set(this.errors, field, rule.message); return false; } if (rule.pattern && !rule.pattern.test(value)) { this.$set(this.errors, field, rule.message); return false; } if (rule.validator && !rule.validator(value)) { this.$set(this.errors, field, rule.message); return false; } } this.$delete(this.errors, field); return true; }, validateAll() { let isValid = true; Object.keys(this.rules).forEach(field => { if (!this.validateField(field)) { isValid = false; } }); return isValid; }, async handleSubmit() { if (!this.validateAll()) { this.$message.error('请检查表单填写'); return; } this.isSubmitting = true; try { await api.register(this.form); this.$message.success('注册成功'); } catch (err) { this.$message.error(err.message); } finally { this.isSubmitting = false; } } } }; </script>方案二:使用 VeeValidate(企业级推荐)// 安装:npm install vee-validate@2 import Vue from 'vue'; import VeeValidate, { Validator } from 'vee-validate'; import zh_CN from 'vee-validate/dist/locale/zh_CN'; Validator.localize('zh_CN', zh_CN); Vue.use(VeeValidate, { inject: false }); // 自定义验证规则 Validator.extend('phone', { getMessage: field => '请输入正确的手机号', validate: value => /^1[3-9]\d{9}$/.test(value) });<template> <form @submit.prevent="handleSubmit"> <div class="form-item"> <label>手机号</label> <input v-model="form.phone" name="phone" v-validate="'required|phone'" /> <span v-if="errors.has('phone')" class="error">{{ errors.first('phone') }}</span> </div> <button type="submit">提交</button> </form> </template> <script> import { mapActions } from 'vuex'; export default { data() { return { form: { phone: '' } }; }, methods: { async handleSubmit() { const valid = await this.$validator.validateAll(); if (!valid) return; await this.submitForm(this.form); } } }; </script>替代方案对比方案优点缺点适用场景自定义校验灵活可控,无额外依赖代码量大,维护成本高简单表单,特殊定制需求VeeValidate规则丰富,国际化支持好学习成本,包体积较大中大型项目复杂表单ElementUI Form组件库集成,开箱即用依赖 UI 组件库使用 ElementUI 的项目2.3 路由守卫(Vue Router 3)核心作用拦截路由跳转,进行权限验证、登录校验、数据预加载等// router/index.js import Vue from 'vue'; import VueRouter from 'vue-router'; import store from '@/store'; Vue.use(VueRouter); const routes = [ { path: '/login', name: 'Login', component: () => import('@/views/Login.vue'), meta: { requiresAuth: false } }, { path: '/', component: () => import('@/layout/MainLayout.vue'), children: [ { path: 'dashboard', name: 'Dashboard', component: () => import('@/views/Dashboard.vue'), meta: { requiresAuth: true, title: '仪表盘' } }, { path: 'users', name: 'Users', component: () => import('@/views/Users.vue'), meta: { requiresAuth: true, title: '用户管理', roles: ['admin', 'superadmin'] }, beforeEnter: (to, from, next) => { // 路由独享守卫 const hasRole = to.meta.roles.includes(store.state.userInfo?.role); hasRole ? next() : next('/403'); } } ] } ]; const router = new VueRouter({ mode: 'history', routes }); // 全局前置守卫 router.beforeEach(async (to, from, next) => { document.title = to.meta.title || '管理系统'; const token = store.state.token; const requiresAuth = to.meta.requiresAuth !== false; if (requiresAuth && !token) { // 需要登录但未登录,跳转登录页 next({ path: '/login', query: { redirect: to.fullPath } }); return; } if (to.path === '/login' && token) { // 已登录用户访问登录页,跳转首页 next('/dashboard'); return; } // 获取用户信息(如果已登录但没有用户信息) if (token && !store.state.userInfo) { try { await store.dispatch('fetchUserInfo'); } catch (err) { store.dispatch('logout'); next('/login'); return; } } next(); }); // 全局后置钩子 router.afterEach((to, from) => { // 埋点上报、页面访问统计等 console.log('页面跳转:', from.path, '->', to.path); }); export default router;组件内守卫<script> export default { beforeRouteEnter(to, from, next) { // 进入路由前,组件实例还没创建,无法访问 this next(vm => { // 可以通过 vm 访问组件实例 vm.fetchData(); }); }, beforeRouteUpdate(to, from, next) { // 路由更新但组件复用时调用(如 /user/1 -> /user/2) this.userId = to.params.id; this.fetchData(); next(); }, beforeRouteLeave(to, from, next) { // 离开路由前调用 if (this.hasUnsavedChanges) { if (confirm('有未保存的内容,确定离开吗?')) { next(); } else { next(false); } } else { next(); } }, methods: { fetchData() { /* ... */ } } }; </script>2.4 Axios 封装核心作用统一处理请求/响应拦截、错误处理、请求取消、Token 管理等// utils/request.js import axios from 'axios'; import store from '@/store'; import router from '@/router'; import { Message } from 'element-ui'; // 创建 axios 实例 const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 15000 }); // 请求取消机制 const pendingMap = new Map(); const getPendingKey = (config) => { return [config.method, config.url, JSON.stringify(config.data || config.params)].join('&'); }; const addPending = (config) => { const key = getPendingKey(config); config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => { if (!pendingMap.has(key)) { pendingMap.set(key, cancel); } }); }; const removePending = (config) => { const key = getPendingKey(config); if (pendingMap.has(key)) { const cancel = pendingMap.get(key); cancel('请求已取消'); pendingMap.delete(key); } }; // 请求拦截器 service.interceptors.request.use( config => { removePending(config); addPending(config); const token = store.state.token; if (token) { config.headers['Authorization'] = `Bearer ${token}`; } if (config.method === 'post' && config.data) { config.headers['Content-Type'] = 'application/json'; } return config; }, error => { return Promise.reject(error); } ); // 响应拦截器 service.interceptors.response.use( response => { removePending(response.config); const res = response.data; if (res.code !== 200) { Message.error(res.message || '请求失败'); if (res.code === 401) { store.dispatch('logout'); router.push('/login'); } return Promise.reject(new Error(res.message)); } return res; }, error => { if (axios.isCancel(error)) { console.log('请求取消:', error.message); return Promise.reject(error); } removePending(error.config || {}); let message = '网络错误'; if (error.response) { const { status, data } = error.response; switch (status) { case 400: message = data?.message || '请求参数错误'; break; case 401: message = '登录已过期,请重新登录'; store.dispatch('logout'); router.push('/login'); break; case 403: message = '没有权限访问'; break; case 404: message = '请求资源不存在'; break; case 500: message = '服务器错误'; break; default: message = data?.message || `请求失败 (${status})`; } } else if (error.code === 'ECONNABORTED') { message = '请求超时,请检查网络'; } Message.error(message); return Promise.reject(error); } ); export default service;// api/user.js import request from '@/utils/request'; export function login(data) { return request({ url: '/auth/login', method: 'post', data }); } export function getUserInfo() { return request({ url: '/user/info', method: 'get' }); } export function updateUser(id, data) { return request({ url: `/user/${id}`, method: 'put', data }); } export function deleteUser(id) { return request({ url: `/user/${id}`, method: 'delete' }); }2.5 Mixin 的使用与潜在问题核心作用复用组件逻辑,但 Vue3 已废弃(推荐使用 Composition API)// mixins/table.js export default { data() { return { tableData: [], loading: false, pagination: { current: 1, pageSize: 10, total: 0 } }; }, methods: { async fetchData() { this.loading = true; try { const res = await this.getListApi({ page: this.pagination.current, pageSize: this.pagination.pageSize }); this.tableData = res.data.list; this.pagination.total = res.data.total; } finally { this.loading = false; } }, handlePageChange(page) { this.pagination.current = page; this.fetchData(); }, handleSizeChange(size) { this.pagination.pageSize = size; this.pagination.current = 1; this.fetchData(); } }, created() { if (this.getListApi) { this.fetchData(); } } };<!-- 使用 mixin --> <script> import tableMixin from '@/mixins/table'; import { getUserList } from '@/api/user'; export default { mixins: [tableMixin], data() { return { getListApi: getUserList }; } }; </script>⚠️ Mixin 潜在问题:命名冲突:多个 mixin 或组件有同名属性/方法,后者覆盖前者来源不透明:难以追踪数据/方法来自哪个 mixin隐式依赖:mixin 之间可能相互依赖,难以维护✅ 替代方案:Vue3 Composition API、Utils 函数抽取、Renderless 组件2.6 自定义指令开发核心作用封装 DOM 操作逻辑,复用低层级操作// directives/index.js import Vue from 'vue'; // 权限指令:v-permission="'admin'" Vue.directive('permission', { inserted(el, binding) { const { value } = binding; const permissions = store.state.userInfo?.permissions || []; if (value && !permissions.includes(value)) { el.parentNode && el.parentNode.removeChild(el); } } }); // 防抖指令:v-debounce="handleClick" Vue.directive('debounce', { bind(el, binding) { let timer = null; const delay = binding.arg || 300; const handler = binding.value; el.addEventListener('click', (e) => { if (timer) clearTimeout(timer); timer = setTimeout(() => { handler(e); }, delay); }); } }); // 节流指令:v-throttle="handleScroll" Vue.directive('throttle', { bind(el, binding) { let lastTime = 0; const delay = binding.arg || 300; const handler = binding.value; el.addEventListener('scroll', (e) => { const now = Date.now(); if (now - lastTime >= delay) { handler(e); lastTime = now; } }); } }); // 复制指令:v-copy="text" Vue.directive('copy', { bind(el, binding) { el.addEventListener('click', () => { const textarea = document.createElement('textarea'); textarea.value = binding.value; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); Vue.prototype.$message.success('复制成功'); }); } }); // 加载状态指令:v-loading="loading" Vue.directive('loading', { bind(el, binding) { const loading = document.createElement('div'); loading.className = 'v-loading'; loading.innerHTML = '<div class="spinner"></div>'; loading.style.display = 'none'; loading.style.position = 'absolute'; loading.style.top = 0; loading.style.left = 0; loading.style.right = 0; loading.style.bottom = 0; loading.style.background = 'rgba(255,255,255,0.8)'; loading.style.display = 'flex'; loading.style.alignItems = 'center'; loading.style.justifyContent = 'center'; el.appendChild(loading); el._loading = loading; el.style.position = 'relative'; }, update(el, binding) { el._loading.style.display = binding.value ? 'flex' : 'none'; } });<template> <div> <button v-permission="'user:delete'" @click="handleDelete">删除</button> <button v-debounce:500="handleSubmit">提交</button> <button v-copy="copyText">复制</button> <div v-loading="loading" style="height: 200px;">内容区域</div> </div> </template>模块三:进阶优化(按需掌握)3.1 v-for 渲染优化核心作用优化长列表渲染性能,避免页面卡顿3.1.1 Key 值规范<!-- ❌ 错误:使用 index 作为 key --> <li v-for="(item, index) in list" :key="index">{{ item.name }}</li> <!-- ✅ 正确:使用唯一 ID 作为 key --> <li v-for="item in list" :key="item.id">{{ item.name }}</li>3.1.2 虚拟列表实现<template> <div class="virtual-list" ref="container" @scroll="handleScroll" > <div class="list-phantom" :style="{ height: totalHeight + 'px' }" ></div> <div class="list-content" :style="{ transform: `translateY(${offset}px)` }" > <div v-for="item in visibleData" :key="item.id" class="list-item" :style="{ height: itemHeight + 'px' }" > {{ item.content }} </div> </div> </div> </template> <script> export default { props: { listData: { type: Array, default: () => [] }, itemHeight: { type: Number, default: 50 } }, data() { return { startIndex: 0, endIndex: 0, containerHeight: 0 }; }, computed: { totalHeight() { return this.listData.length * this.itemHeight; }, visibleCount() { return Math.ceil(this.containerHeight / this.itemHeight) + 2; }, visibleData() { return this.listData.slice(this.startIndex, this.endIndex); }, offset() { return this.startIndex * this.itemHeight; } }, mounted() { this.containerHeight = this.$refs.container.clientHeight; this.endIndex = this.startIndex + this.visibleCount; }, methods: { handleScroll() { const scrollTop = this.$refs.container.scrollTop; this.startIndex = Math.floor(scrollTop / this.itemHeight); this.endIndex = this.startIndex + this.visibleCount; } } }; </script> <style scoped> .virtual-list { height: 500px; overflow-y: auto; position: relative; } .list-phantom { position: absolute; left: 0; top: 0; right: 0; } .list-content { position: absolute; left: 0; right: 0; top: 0; } .list-item { padding: 0 10px; border-bottom: 1px solid #eee; display: flex; align-items: center; } </style>3.2 计算属性 vs 方法 vs 侦听器特性computedmethodswatch缓存✅ 有缓存,依赖不变不重新计算❌ 无缓存,每次调用都执行-异步❌ 不支持✅ 支持✅ 支持返回值✅ 必须有返回值可选可选适用场景计算衍生值,有缓存需求事件处理、业务逻辑数据变化执行异步操作export default { data() { return { firstName: '张', lastName: '三', searchText: '' }; }, computed: { fullName() { return this.firstName + this.lastName; } }, watch: { searchText(newVal) { this.debounceSearch(newVal); } }, methods: { getFullName() { return this.firstName + this.lastName; }, debounceSearch(keyword) { // 异步搜索 } } };3.3 Keep-alive 缓存组件核心作用缓存组件实例,避免重复渲染,提升用户体验<template> <div id="app"> <keep-alive :include="cachedViews" :exclude="['Detail']" :max="10"> <router-view /> </keep-alive> </div> </template> <script> export default { computed: { cachedViews() { return this.$store.state.tagsView.cachedViews; } } }; </script><!-- 被缓存的组件会有 activated/deactivated 钩子 --> <script> export default { activated() { // 组件激活时调用 console.log('组件被激活'); this.refreshData(); }, deactivated() { // 组件停用时调用 console.log('组件被缓存'); } }; </script>3.4 打包体积优化核心作用减少 bundle 体积,提升加载速度3.4.1 路由懒加载// ❌ 静态导入(打包到主 bundle) import Home from '@/views/Home.vue'; // ✅ 路由懒加载(代码分割) const Home = () => import('@/views/Home.vue'); const routes = [ { path: '/home', component: Home }, { path: '/about', component: () => import('@/views/About.vue') } ];3.4.2 第三方库 CDN 引入// vue.config.js module.exports = { configureWebpack: { externals: { vue: 'Vue', 'vue-router': 'VueRouter', vuex: 'Vuex', axios: 'axios', 'element-ui': 'ELEMENT' } } };<!-- public/index.html --> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/vue-router@3.5.3/dist/vue-router.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios@0.24.0/dist/axios.min.js"></script>3.4.3 Tree-shaking 配置// 引入方式很重要! // ❌ 全量引入 import _ from 'lodash'; // ✅ 按需引入(需配置 babel-plugin-component) import { debounce, throttle } from 'lodash-es';3.5 首屏加载优化优化方案实现方式效果路由懒加载() => import()减少首屏 bundle 大小图片懒加载v-lazy 或 IntersectionObserver减少初始请求数预加载<link rel="preload">提前加载关键资源骨架屏首屏显示占位提升感知体验Gzip 压缩Nginx 配置减少传输体积缓存策略Service Worker / HTTP 缓存减少重复加载模块四:冷门知识点(仅了解)4.1 过滤器(Vue3 已移除)核心概念Vue2 提供的一种文本格式化方式,Vue3 已移除适用场景简单的文本格式化(日期、数字、字符串处理)使用限制只能在模板插值和 v-bind 中使用无法访问组件实例的 this(全局过滤器除外)Vue3 已移除,迁移到计算属性或方法替代方案// Vue2 过滤器 filters: { formatDate(date) { return moment(date).format('YYYY-MM-DD'); } } // Vue3 替代:计算属性 computed: { formattedDate() { return moment(this.date).format('YYYY-MM-DD'); } }4.2 $set 底层原理(仅了解)核心概念解决 Vue2 响应式系统的两个限制:不能直接添加新的响应式属性到已创建的实例不能通过数组下标直接修改元素适用场景动态添加响应式属性通过下标修改数组元素使用限制不能给 Vue 实例或根数据对象添加根级响应式属性4.3 内置过渡动画核心概念Vue 提供的 transition 和 transition-group 组件,用于元素插入、更新、移除时的动画效果少用场景频繁触发的动画(影响性能)复杂动画(推荐使用 CSS 动画库如 animate.css)性能影响频繁的 DOM 操作可能导致布局抖动建议使用 CSS transforms 和 opacity(GPU 加速)总结学习路径建议第一阶段:掌握核心基础模块(响应式原理、选项式 API、生命周期)第二阶段:重点学习高频技巧(组件通信、表单校验、路由、Axios)第三阶段:按需学习进阶优化(性能优化、打包优化)第四阶段:了解冷门知识点,为 Vue3 迁移做准备企业级项目架构建议使用 Vuex 管理全局状态统一的 Axios 封装路由守卫做权限控制Mixin 谨慎使用,考虑 Utils 函数自定义指令封装常用 DOM 操作模块五:企业级实战场景(生产环境)5.1 企业级项目目录结构核心作用建立规范的项目结构,便于团队协作和代码维护src/ ├── api/ # API 接口层 │ ├── modules/ # 按业务模块划分 │ │ ├── user.js │ │ ├── order.js │ │ └── goods.js │ └── index.js # API 统一导出 ├── assets/ # 静态资源 │ ├── images/ │ ├── styles/ │ │ ├── index.scss # 全局样式入口 │ │ ├── variables.scss # 样式变量 │ │ └── mixins.scss # 样式混入 │ └── fonts/ ├── components/ # 公共组件 │ ├── business/ # 业务组件 │ │ ├── UserCard.vue │ │ └── OrderList.vue │ └── common/ # 通用组件 │ ├── Pagination.vue │ ├── SearchForm.vue │ └── TableToolbar.vue ├── directives/ # 自定义指令 │ ├── permission.js │ ├── debounce.js │ └── index.js ├── filters/ # 过滤器 │ ├── date.js │ ├── number.js │ └── index.js ├── layout/ # 布局组件 │ ├── MainLayout.vue │ ├── Sidebar.vue │ ├── Header.vue │ └── TagsView.vue ├── mixins/ # 混入 │ ├── table.js │ ├── form.js │ └── dialog.js ├── router/ # 路由 │ ├── modules/ # 路由模块 │ │ ├── user.js │ │ └── order.js │ ├── index.js │ └── permission.js # 路由权限控制 ├── store/ # Vuex 状态管理 │ ├── modules/ # 业务模块 │ │ ├── user.js │ │ ├── app.js │ │ ├── tagsView.js │ │ └── permission.js │ ├── getters.js │ └── index.js ├── utils/ # 工具函数 │ ├── request.js # axios 封装 │ ├── auth.js # 认证相关 │ ├── validate.js # 校验工具 │ ├── storage.js # 本地存储 │ ├── date.js # 日期处理 │ └── index.js ├── views/ # 页面视图 │ ├── login/ │ │ └── Login.vue │ ├── dashboard/ │ │ └── Dashboard.vue │ ├── user/ │ │ ├── UserList.vue │ │ └── UserDetail.vue │ └── error/ │ ├── 404.vue │ └── 403.vue ├── App.vue └── main.js✅ 最佳实践:按业务模块划分目录,而非按类型公共组件与业务组件分离API 层统一管理接口路由按模块懒加载5.2 标签页导航(TagsView)完整实现核心作用企业级后台系统必备功能,支持多标签页切换、关闭、刷新等// store/modules/tagsView.js const state = { visitedViews: [], cachedViews: [] }; const mutations = { ADD_VISITED_VIEW(state, view) { if (state.visitedViews.some(v => v.path === view.path)) return; state.visitedViews.push({ name: view.name, path: view.path, title: view.meta?.title || 'no-name', query: view.query, params: view.params, affix: view.meta?.affix || false }); }, ADD_CACHED_VIEW(state, view) { if (state.cachedViews.includes(view.name)) return; if (view.meta?.keepAlive !== false) { state.cachedViews.push(view.name); } }, DEL_VISITED_VIEW(state, view) { const index = state.visitedViews.findIndex(v => v.path === view.path); if (index > -1) { state.visitedViews.splice(index, 1); } }, DEL_CACHED_VIEW(state, view) { const index = state.cachedViews.indexOf(view.name); if (index > -1) { state.cachedViews.splice(index, 1); } }, DEL_OTHERS_VISITED_VIEWS(state, view) { state.visitedViews = state.visitedViews.filter( v => v.affix || v.path === view.path ); }, DEL_OTHERS_CACHED_VIEWS(state, view) { state.cachedViews = state.cachedViews.filter(name => name === view.name); }, DEL_ALL_VISITED_VIEWS(state) { state.visitedViews = state.visitedViews.filter(v => v.affix); }, DEL_ALL_CACHED_VIEWS(state) { state.cachedViews = []; }, UPDATE_VISITED_VIEW(state, view) { for (let v of state.visitedViews) { if (v.path === view.path) { Object.assign(v, view); break; } } } }; const actions = { addView({ dispatch }, view) { dispatch('addVisitedView', view); dispatch('addCachedView', view); }, addVisitedView({ commit }, view) { commit('ADD_VISITED_VIEW', view); }, addCachedView({ commit }, view) { commit('ADD_CACHED_VIEW', view); }, delView({ dispatch, state }, view) { return new Promise(resolve => { dispatch('delVisitedView', view); dispatch('delCachedView', view); resolve({ visitedViews: [...state.visitedViews], cachedViews: [...state.cachedViews] }); }); }, delVisitedView({ commit, state }, view) { return new Promise(resolve => { commit('DEL_VISITED_VIEW', view); resolve([...state.visitedViews]); }); }, delCachedView({ commit, state }, view) { return new Promise(resolve => { commit('DEL_CACHED_VIEW', view); resolve([...state.cachedViews]); }); }, delOthersViews({ dispatch, state }, view) { return new Promise(resolve => { dispatch('delOthersVisitedViews', view); dispatch('delOthersCachedViews', view); resolve({ visitedViews: [...state.visitedViews], cachedViews: [...state.cachedViews] }); }); }, delOthersVisitedViews({ commit, state }, view) { return new Promise(resolve => { commit('DEL_OTHERS_VISITED_VIEWS', view); resolve([...state.visitedViews]); }); }, delOthersCachedViews({ commit, state }, view) { return new Promise(resolve => { commit('DEL_OTHERS_CACHED_VIEWS', view); resolve([...state.cachedViews]); }); }, delAllViews({ dispatch, state }, view) { return new Promise(resolve => { dispatch('delAllVisitedViews', view); dispatch('delAllCachedViews', view); resolve({ visitedViews: [...state.visitedViews], cachedViews: [...state.cachedViews] }); }); }, delAllVisitedViews({ commit, state }) { return new Promise(resolve => { commit('DEL_ALL_VISITED_VIEWS'); resolve([...state.visitedViews]); }); }, delAllCachedViews({ commit, state }) { return new Promise(resolve => { commit('DEL_ALL_CACHED_VIEWS'); resolve([...state.cachedViews]); }); }, updateVisitedView({ commit }, view) { commit('UPDATE_VISITED_VIEW', view); } }; export default { namespaced: true, state, mutations, actions };<!-- layout/TagsView.vue --> <template> <div class="tags-view-container"> <scroll-pane ref="scrollPane" class="tags-view-wrapper"> <router-link v-for="tag in visitedViews" ref="tag" :key="tag.path" :class="isActive(tag) ? 'active' : ''" :to="{ path: tag.path, query: tag.query, params: tag.params }" class="tags-view-item" @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''" @contextmenu.prevent.native="openMenu(tag, $event)" > {{ tag.title }} <span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" ></span> </router-link> </scroll-pane> <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu"> <li @click="refreshSelectedTag(selectedTag)">刷新</li> <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li> <li @click="closeOthersTags">关闭其他</li> <li @click="closeAllTags(selectedTag)">关闭所有</li> </ul> </div> </template> <script> import { mapState, mapActions } from 'vuex'; import ScrollPane from './ScrollPane.vue'; export default { name: 'TagsView', components: { ScrollPane }, data() { return { visible: false, top: 0, left: 0, selectedTag: {}, affixTags: [] }; }, computed: { ...mapState({ visitedViews: state => state.tagsView.visitedViews, cachedViews: state => state.tagsView.cachedViews }) }, watch: { $route() { this.addTags(); this.moveToCurrentTag(); }, visible(value) { if (value) { document.body.addEventListener('click', this.closeMenu); } else { document.body.removeEventListener('click', this.closeMenu); } } }, mounted() { this.initTags(); this.addTags(); }, methods: { ...mapActions('tagsView', [ 'addView', 'delView', 'delOthersViews', 'delAllViews', 'updateVisitedView' ]), isActive(route) { return route.path === this.$route.path; }, isAffix(tag) { return tag.meta && tag.meta.affix; }, initTags() { const routes = this.$router.options.routes; this.affixTags = this.filterAffixTags(routes); for (const tag of this.affixTags) { if (tag.path) { this.addView(tag); } } }, filterAffixTags(routes, basePath = '/') { let tags = []; routes.forEach(route => { if (route.meta && route.meta.affix) { const tagPath = path.resolve(basePath, route.path); tags.push({ fullPath: tagPath, path: tagPath, name: route.name, meta: { ...route.meta } }); } if (route.children) { const tempTags = this.filterAffixTags(route.children, route.path); if (tempTags.length >= 1) { tags = [...tags, ...tempTags]; } } }); return tags; }, addTags() { const { name } = this.$route; if (name) { this.addView(this.$route); } return false; }, moveToCurrentTag() { const tags = this.$refs.tag; this.$nextTick(() => { for (const tag of tags) { if (tag.to === this.$route.path) { this.$refs.scrollPane.moveToTarget(tag); break; } } }); }, closeSelectedTag(view) { this.delView(view).then(({ visitedViews }) => { if (this.isActive(view)) { this.toLastView(visitedViews, view); } }); }, closeOthersTags() { this.$router.push(this.selectedTag); this.delOthersViews(this.selectedTag); }, closeAllTags(view) { this.delAllViews(view).then(({ visitedViews }) => { if (visitedViews.length) { this.toLastView(visitedViews); } else { this.$router.push('/'); } }); }, toLastView(visitedViews, view) { const latestView = visitedViews.slice(-1)[0]; if (latestView) { this.$router.push(latestView.fullPath); } else { if (view.name === 'Dashboard') { this.refreshSelectedTag(view); } else { this.$router.push('/'); } } }, openMenu(tag, e) { const menuMinWidth = 105; const offsetLeft = this.$el.getBoundingClientRect().left; const offsetWidth = this.$el.offsetWidth; const maxLeft = offsetWidth - menuMinWidth; const left = e.clientX - offsetLeft + 15; if (left > maxLeft) { this.left = maxLeft; } else { this.left = left; } this.top = e.clientY; this.visible = true; this.selectedTag = tag; }, closeMenu() { this.visible = false; }, refreshSelectedTag(view) { this.$nextTick(() => { const { fullPath } = view; this.$router.replace({ path: '/redirect' + fullPath }); }); } } }; </script> <style lang="scss" scoped> .tags-view-container { height: 34px; width: 100%; background: #fff; border-bottom: 1px solid #d8dce5; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04); .tags-view-wrapper { .tags-view-item { display: inline-block; position: relative; cursor: pointer; height: 26px; line-height: 26px; border: 1px solid #d8dce5; color: #495060; background: #fff; padding: 0 8px; font-size: 12px; margin-left: 5px; margin-top: 4px; &:first-of-type { margin-left: 15px; } &:hover { color: #409eff; } &.active { background-color: #409eff; color: #fff; border-color: #409eff; &::before { content: ''; background: #fff; display: inline-block; width: 8px; height: 8px; border-radius: 50%; position: relative; margin-right: 2px; } } } } .contextmenu { margin: 0; background: #fff; z-index: 100; position: absolute; list-style-type: none; padding: 5px 0; border-radius: 4px; font-size: 12px; font-weight: 400; color: #333; box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3); li { margin: 0; padding: 7px 16px; cursor: pointer; &:hover { background: #eee; } } } } </style>5.3 动态菜单与路由权限管理核心作用根据用户权限动态生成菜单和路由,实现细粒度权限控制// store/modules/permission.js import { asyncRoutes, constantRoutes } from '@/router'; import { getRoutes } from '@/api/user'; function hasPermission(roles, route) { if (route.meta && route.meta.roles) { return roles.some(role => route.meta.roles.includes(role)); } else { return true; } } export function filterAsyncRoutes(routes, roles) { const res = []; routes.forEach(route => { const tmp = { ...route }; if (hasPermission(roles, tmp)) { if (tmp.children) { tmp.children = filterAsyncRoutes(tmp.children, roles); } res.push(tmp); } }); return res; } const state = { routes: [], addRoutes: [] }; const mutations = { SET_ROUTES(state, routes) { state.addRoutes = routes; state.routes = constantRoutes.concat(routes); } }; const actions = { generateRoutes({ commit }, roles) { return new Promise(resolve => { let accessedRoutes; if (roles.includes('admin')) { accessedRoutes = asyncRoutes || []; } else { accessedRoutes = filterAsyncRoutes(asyncRoutes, roles); } commit('SET_ROUTES', accessedRoutes); resolve(accessedRoutes); }); }, generateRoutesByBackend({ commit }) { return new Promise((resolve, reject) => { getRoutes().then(response => { const accessedRoutes = filterAsyncRoutes(response.data, []); commit('SET_ROUTES', accessedRoutes); resolve(accessedRoutes); }).catch(error => { reject(error); }); }); } }; export default { namespaced: true, state, mutations, actions };// router/permission.js import router from './index'; import store from '@/store'; import { getToken } from '@/utils/auth'; const whiteList = ['/login', '/auth-redirect']; router.beforeEach(async (to, from, next) => { const hasToken = getToken(); if (hasToken) { if (to.path === '/login') { next({ path: '/' }); } else { const hasGetUserInfo = store.getters.name; if (hasGetUserInfo) { next(); } else { try { await store.dispatch('user/getInfo'); const accessRoutes = await store.dispatch('permission/generateRoutes', store.getters.roles); router.addRoutes(accessRoutes); next({ ...to, replace: true }); } catch (error) { await store.dispatch('user/resetToken'); next(`/login?redirect=${to.path}`); } } } } else { if (whiteList.indexOf(to.path) !== -1) { next(); } else { next(`/login?redirect=${to.path}`); } } });<!-- layout/Sidebar.vue --> <template> <div class="sidebar-container"> <el-scrollbar wrap-class="scrollbar-wrapper"> <el-menu :default-active="activeMenu" :collapse="isCollapse" :background-color="variables.menuBg" :text-color="variables.menuText" :unique-opened="false" :active-text-color="variables.menuActiveText" :collapse-transition="false" mode="vertical" > <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" /> </el-menu> </el-scrollbar> </div> </template> <script> import { mapGetters } from 'vuex'; import SidebarItem from './SidebarItem'; import variables from '@/assets/styles/variables.scss'; export default { name: 'Sidebar', components: { SidebarItem }, computed: { ...mapGetters([ 'permission_routes', 'sidebar' ]), activeMenu() { const route = this.$route; const { meta, path } = route; if (meta.activeMenu) { return meta.activeMenu; } return path; }, isCollapse() { return !this.sidebar.opened; }, variables() { return variables; } } }; </script>5.4 Excel 导入导出实战核心作用企业级系统必备功能,支持批量数据导入和导出# 安装依赖 npm install xlsx file-saver -S npm install script-loader -D// utils/export2Excel.js import * as XLSX from 'xlsx'; import { saveAs } from 'file-saver'; export function export_json_to_excel({ multiHeader = [], header, data, filename, merges = [], autoWidth = true, bookType = 'xlsx' } = {}) { filename = filename || 'excel-list'; data = [...data]; data.unshift(header); for (let i = multiHeader.length - 1; i > -1; i--) { data.unshift(multiHeader[i]); } const ws_name = 'SheetJS'; const wb = XLSX.utils.book_new(); const ws = XLSX.utils.aoa_to_sheet(data); if (merges.length > 0) { if (!ws['!merges']) ws['!merges'] = []; merges.forEach(item => { ws['!merges'].push(XLSX.utils.decode_range(item)); }); } if (autoWidth) { const colWidth = data.map(row => row.map(val => { if (val == null) { return { wch: 10 }; } else if (val.toString().charCodeAt(0) > 255) { return { wch: val.toString().length * 2 + 2 }; } else { return { wch: val.toString().length + 2 }; } })); const result = colWidth[0]; for (let i = 1; i < colWidth.length; i++) { for (let j = 0; j < colWidth[i].length; j++) { if (result[j]['wch'] < colWidth[i][j]['wch']) { result[j]['wch'] = colWidth[i][j]['wch']; } } } ws['!cols'] = result; } XLSX.utils.book_append_sheet(wb, ws, ws_name); const wbout = XLSX.write(wb, { bookType: bookType, bookSST: false, type: 'binary' }); saveAs(new Blob([s2ab(wbout)], { type: 'application/octet-stream' }), `${filename}.${bookType}`); } function s2ab(s) { if (typeof ArrayBuffer !== 'undefined') { const buf = new ArrayBuffer(s.length); const view = new Uint8Array(buf); for (let i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF; return buf; } else { const buf = new Array(s.length); for (let i = 0; i != s.length; ++i) buf[i] = s.charCodeAt(i) & 0xFF; return buf; } }// utils/importExcel.js import * as XLSX from 'xlsx'; export function readExcel(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { const data = e.target.result; const workbook = XLSX.read(data, { type: 'binary' }); const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; const jsonData = XLSX.utils.sheet_to_json(worksheet); resolve(jsonData); }; reader.onerror = (error) => reject(error); reader.readAsBinaryString(file.raw); }); } export function generateTemplate({ header = [], filename = 'template' }) { const data = [header]; const ws_name = 'Sheet1'; const wb = XLSX.utils.book_new(); const ws = XLSX.utils.aoa_to_sheet(data); XLSX.utils.book_append_sheet(wb, ws, ws_name); XLSX.writeFile(wb, `${filename}.xlsx`); }<!-- 组件中使用 --> <template> <div> <el-button @click="handleDownloadTemplate">下载模板</el-button> <el-upload class="upload-demo" action="" :auto-upload="false" :on-change="handleImport" :show-file-list="false" accept=".xlsx,.xls" > <el-button type="primary">导入Excel</el-button> </el-upload> <el-button type="success" @click="handleExport">导出Excel</el-button> </div> </template> <script> import { export_json_to_excel } from '@/utils/export2Excel'; import { readExcel, generateTemplate } from '@/utils/importExcel'; import { getUserList } from '@/api/user'; export default { data() { return { list: [] }; }, methods: { handleDownloadTemplate() { generateTemplate({ header: ['姓名', '手机号', '邮箱', '部门'], filename: '用户导入模板' }); }, async handleImport(file) { try { const data = await readExcel(file)); const importData = []; file.forEach(item => { importData.push({ name: item['姓名'], phone: item['手机号'], email: item['邮箱'], department: item['部门'] }); }); console.log('导入数据:', importData); this.$message.success('导入成功'); } catch (error) { this.$message.error('导入失败'); } }, async handleExport() { const tHeader = ['ID', '姓名', '手机号', '邮箱', '创建时间']; const filterVal = ['id', 'name', 'phone', 'email', 'createTime']; const list = await getUserList(); const data = list.map(v => filterVal.map(j => { if (j === 'createTime') { return this.parseTime(v[j]); } return v[j]; })); export_json_to_excel({ header: tHeader, data, filename: '用户列表', autoWidth: true }); }, parseTime(time) { const date = new Date(time); return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } } }; </script>5.5 图片上传与裁剪核心作用支持图片上传、预览、裁剪功能,适用于头像、商品图片等场景# 安装依赖 npm install vue-cropper -S<!-- components/ImageUpload.vue --> <template> <div class="image-upload"> <div class="image-upload__preview" v-if="imageUrl"> <img :src="imageUrl" /> <div class="image-upload__actions"> <i class="el-icon-plus" @click="dialogVisible = true"></i> <i class="el-icon-delete" @click="handleDelete"></i> </div> </div> <el-upload v-else class="image-uploader" :show-file-list="false" :before-upload="beforeUpload" :http-request="handleUpload" accept="image/*" > <i class="el-icon-plus"></i> </el-upload> <el-dialog title="图片裁剪" :visible.sync="dialogVisible" width="600px" append-to-body > <div class="cropper-container"> <vue-cropper ref="cropper" :img="option.img" :outputSize="option.size" :outputType="option.outputType" :info="option.info" :full="option.full" :canMove="option.canMove" :canMoveBox="option.canMoveBox" :original="option.original" :autoCrop="option.autoCrop" :autoCropWidth="option.autoCropWidth" :autoCropHeight="option.autoCropHeight" :fixedBox="option.fixedBox" :fixed="option.fixed" :fixedNumber="option.fixedNumber" ></vue-cropper> </div> <div slot="footer" class="dialog-footer"> <el-button @click="dialogVisible = false">取 消</el-button> <el-button type="primary" @click="finishCrop">确 定</el-button> </div> </el-dialog> </div> </template> <script> import { VueCropper } from 'vue-cropper'; import { uploadImage } from '@/api/common'; export default { name: 'ImageUpload', components: { VueCropper }, props: { value: { type: String, default: '' }, width: { type: Number, default: 200 }, height: { type: Number, default: 200 }, aspectRatio: { type: Array, default: () => [1, 1] } }, data() { return { imageUrl: this.value, dialogVisible: false, option: { img: '', size: 1, outputType: 'jpeg', info: true, full: false, canMove: true, canMoveBox: true, original: false, autoCrop: true, autoCropWidth: this.width, autoCropHeight: this.height, fixedBox: false, fixed: true, fixedNumber: this.aspectRatio }, uploadFile: null }; }, watch: { value(val) { this.imageUrl = val; } }, methods: { beforeUpload(file) { const isJPG = file.type === 'image/jpeg' || file.type === 'image/jpg' || file.type === 'image/png'; const isLt2M = file.size / 1024 / 1024 < 2; if (!isJPG) { this.$message.error('上传图片只能是 JPG/PNG 格式!'); return false; } if (!isLt2M) { this.$message.error('上传图片大小不能超过 2MB!'); return false; } this.uploadFile = file; const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = (e) => { this.option.img = e.target.result; this.dialogVisible = true; }; return false; }, handleUpload(file) { }, finishCrop() { this.$refs.cropper.getCropBlob((data) => { const formData = new FormData(); formData.append('file', data, 'avatar.jpg'); uploadImage(formData).then(res => { this.imageUrl = res.data.url; this.$emit('input', this.imageUrl); this.$emit('change', this.imageUrl); this.dialogVisible = false; this.$message.success('上传成功'); }); }); }, handleDelete() { this.imageUrl = ''; this.$emit('input', ''); this.$emit('change', ''); } } }; </script> <style lang="scss" scoped> .image-upload { .image-upload__preview { position: relative; width: 148px; height: 148px; border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; overflow: hidden; img { width: 100%; height: 100%; object-fit: cover; } &:hover .image-upload__actions { display: flex; } .image-upload__actions { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: none; align-items: center; justify-content: center; i { font-size: 24px; color: #fff; margin: 0 8px; cursor: pointer; &:hover { color: #409eff; } } } } .image-uploader { :deep(.el-upload) { border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; transition: border-color 0.3s; &:hover { border-color: #409eff; } .el-icon-plus { font-size: 28px; color: #8c939d; width: 178px; height: 178px; line-height: 178px; text-align: center; } } } .cropper-container { height: 400px; } } </style>5.6 全局错误处理与日志上报核心作用统一捕获应用错误,上报日志平台,便于问题排查// utils/error-log.js import Vue from 'vue'; import store from '@/store'; import { isString, isArray } from '@/utils/validate'; import settings from '@/settings'; const errorLog = store => { if (settings.errorLog === 'production') { Vue.config.errorHandler = function(err, vm, info) { Vue.nextTick(() => { store.dispatch('errorLog/addErrorLog', { err, vm, info, url: window.location.href }); console.error(err, info); }); }; } else if (settings.errorLog) { Vue.config.errorHandler = function(err, vm, info) { Vue.nextTick(() => { store.dispatch('errorLog/addErrorLog', { err, vm, info, url: window.location.href }); console.error(err, info); }); }; window.addEventListener('unhandledrejection', event => { store.dispatch('errorLog/addErrorLog', { err: new Error(event.reason), info: 'Promise', url: window.location.href }); }); window.onerror = function(message, source, lineno, colno, error) { store.dispatch('errorLog/addErrorLog', { err: error, info: `${message} at ${source}:${lineno}:${colno}`, url: window.location.href }); return false; }; } }; export default errorLog;// store/modules/errorLog.js import { reportErrorLog } from '@/api/monitor'; const state = { logs: [] }; const mutations = { ADD_ERROR_LOG(state, log) { state.logs.push(log); }, CLEAR_ERROR_LOG(state) { state.logs = []; } }; const actions = { addErrorLog({ commit }, log) { commit('ADD_ERROR_LOG', log); reportErrorLog({ message: log.err.message, stack: log.err.stack, info: log.info, url: log.url, userAgent: navigator.userAgent, timestamp: Date.now() }).catch(() => {}); }, clearErrorLog({ commit }) { commit('CLEAR_ERROR_LOG'); } }; export default { namespaced: true, state, mutations, actions };5.7 多环境配置核心作用支持开发、测试、生产等多环境配置切换// .env.development NODE_ENV=development VUE_APP_BASE_API=http://dev-api.example.com VUE_APP_TITLE=开发环境 // .env.test NODE_ENV=production VUE_APP_BASE_API=http://test-api.example.com VUE_APP_TITLE=测试环境 // .env.production NODE_ENV=production VUE_APP_BASE_API=https://api.example.com VUE_APP_TITLE=生产环境// package.json { "scripts": { "dev": "vue-cli-service serve", "build:test": "vue-cli-service build --mode test", "build:prod": "vue-cli-service build --mode production" } }// utils/request.js const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 15000 });5.8 大数据量表格处理方案核心作用处理万级以上数据的表格渲染,避免页面卡顿方案一:虚拟滚动(推荐)<!-- 使用 el-table-v2 或自定义虚拟列表 --> <template> <div class="virtual-table-container"> <el-table :data="visibleData" style="width: 100%" height="500" :row-key="row => row.id" > <el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="name" label="姓名" width="120" /> <el-table-column prop="email" label="邮箱" width="200" /> <el-table-column prop="phone" label="手机号" width="130" /> <el-table-column prop="createTime" label="创建时间" width="180" /> </el-table> <div ref="scrollBar" class="scroll-bar" @scroll="handleScroll" > <div class="scroll-content" :style="{ height: totalHeight + 'px' }" ></div> </div> </div> </template> <script> export default { name: 'VirtualTable', props: { data: { type: Array, default: () => [] }, rowHeight: { type: Number, default: 48 } }, data() { return { startIndex: 0, endIndex: 0, containerHeight: 500 }; }, computed: { totalHeight() { return this.data.length * this.rowHeight; }, visibleCount() { return Math.ceil(this.containerHeight / this.rowHeight) + 2; }, visibleData() { return this.data.slice(this.startIndex, this.endIndex); }, offset() { return this.startIndex * this.rowHeight; } }, mounted() { this.endIndex = this.startIndex + this.visibleCount; }, methods: { handleScroll(e) { const scrollTop = e.target.scrollTop; this.startIndex = Math.floor(scrollTop / this.rowHeight); this.endIndex = this.startIndex + this.visibleCount; } } }; </script> <style scoped> .virtual-table-container { position: relative; } .scroll-bar { position: absolute; right: 0; top: 0; width: 100%; height: 500px; overflow-y: auto; } .scroll-content { width: 1px; } </style>方案二:分页加载(常用)export default { data() { return { list: [], loading: false, pagination: { current: 1, pageSize: 20, total: 0 } }; }, methods: { async fetchData() { this.loading = true; try { const res = await getUserList({ page: this.pagination.current, pageSize: this.pagination.pageSize }); this.list = res.data.list; this.pagination.total = res.data.total; } finally { this.loading = false; } }, handleCurrentChange(page) { this.pagination.current = page; this.fetchData(); }, handleSizeChange(size) { this.pagination.pageSize = size; this.pagination.current = 1; this.fetchData(); } } };总结(补充)完整学习路径第一阶段:掌握核心基础模块(响应式原理、选项式 API、生命周期)第二阶段:重点学习高频技巧(组件通信、表单校验、路由、Axios)第三阶段:按需学习进阶优化(性能优化、打包优化)第四阶段:实战企业级场景(TagsView、动态菜单、Excel、图片上传)第五阶段:了解冷门知识点,为 Vue3 迁移做准备企业级项目架构建议(完整版)✅ 使用 Vuex 管理全局状态(user、app、tagsView、permission、errorLog)✅ 统一的 Axios 封装(请求/响应拦截、错误处理、请求取消)✅ 路由守卫做权限控制(全局守卫、路由独享守卫)✅ Mixin 谨慎使用,考虑 Utils 函数✅ 自定义指令封装常用 DOM 操作✅ TagsView 多标签页导航✅ 动态菜单与路由权限管理✅ Excel 导入导出功能✅ 图片上传与裁剪✅ 全局错误处理与日志上报✅ 多环境配置✅ 大数据量表格处理方案
2026年03月17日
37 阅读
0 评论
0 点赞
1
...
5
6
7
8