[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-config":3,"footer-socials":22,"articles-init":23},{"site_title":4,"site_subtitle":5,"home_intro":6,"avatar":7,"seo_keywords":8,"seo_description":9,"site_subtitle_highlight":10,"home_description":11,"about_name":12,"about_intro":13,"code_comment_2":14,"code_log":15,"code_skills":16,"code_goal":17,"code_comment_1":18,"meteor_density":19,"meteor_max_count":19,"meteor_enabled":20,"meteor_speed":21},"ShineGoldYao","架构代码，","全栈开发者 \u002F 开源爱好者 \u002F 技术探索者","https:\u002F\u002Fforuda.gitee.com\u002Favatar\u002F1762402862010015318\u002F16382196_yaoxingjin_1762402861.png!avatar200","ShiGoldYao,技术博客,全栈开发","专注于前沿技术分享与开源项目展示的个人技术博客","书写未来。","大家好，我是 ShiGoldYao。一名全栈学习与技术爱好者。在这里，我分享关于现代 Web 开发、技术框架学习记录以及极客生活的深度思考。","ShiGoldYao","我是一名全栈技术学习者，对构建高性能、可扩展的现代 Web 应用充满热情。过去一年里，我从初识互联网前后端开发，到逐步沉淀技术体系，始终保持着对前端、后端与工程化的持续探索。\n\n我坚信 “代码如诗”。除了日常学习与项目实践，我也会花大量时间关注开源社区，尝试摸索 WebAssembly、Rust 等前沿技术在浏览器端的更多可能，不断挑战性能与体验的边界。\n\n生活里的我并不只有代码：闲暇时会打打永劫无间，享受博弈与操作的快感；也喜欢打乒乓球，在运动中放松自己；当然，最幸福的时光，还是和女朋友一起慢慢生活、认真恋爱。","\u002F\u002F 🚀 开启学习之旅","","Vue , TypeScript , Nest , Mysql","成为优秀的前端全栈工程师","\u002F\u002F 欢迎来到我的技术世界","3","true","5",[],{"cats":24,"articleRes":65},[25,33,40,47,53,57,61],{"id":26,"categoryName":27,"slug":28,"description":29,"sort":30,"isEnable":26,"createTime":31,"updateTime":32,"deleteTime":30},1,"前端开发","frontend-engineering","2222",0,"2026-04-03T02:36:11.945Z","2026-04-07T16:38:46.496Z",{"id":34,"categoryName":35,"slug":36,"description":37,"sort":30,"isEnable":26,"createTime":38,"updateTime":39,"deleteTime":30},2,"后端开发","backend-engineering","222","2026-04-03T02:36:33.016Z","2026-04-07T16:38:50.279Z",{"id":41,"categoryName":42,"slug":43,"description":44,"sort":30,"isEnable":26,"createTime":45,"updateTime":46,"deleteTime":30},3,"数据库开发","database-design","333","2026-04-03T02:37:05.639Z","2026-04-07T16:38:56.350Z",{"id":48,"categoryName":49,"slug":50,"description":44,"sort":30,"isEnable":26,"createTime":51,"updateTime":52,"deleteTime":30},4,"UI\u002FUX设计","ui-ux-design","2026-04-03T02:37:27.405Z","2026-04-03T02:39:33.804Z",{"id":54,"categoryName":55,"slug":55,"description":55,"sort":30,"isEnable":26,"createTime":56,"updateTime":56,"deleteTime":30},5,"跨端开发","2026-04-07T16:38:39.509Z",{"id":58,"categoryName":59,"slug":59,"description":59,"sort":30,"isEnable":26,"createTime":60,"updateTime":60,"deleteTime":30},6,"生活随笔","2026-04-07T16:43:11.028Z",{"id":62,"categoryName":63,"slug":63,"description":63,"sort":30,"isEnable":26,"createTime":64,"updateTime":64,"deleteTime":30},7,"工程化技术","2026-04-07T16:50:31.279Z",{"list":66,"total":191,"page":26,"pageSize":192},[67,87,99,111,129,140,151,169,180],{"id":68,"title":69,"slug":70,"coverUrl":15,"summary":15,"content":71,"htmlContent":72,"categoryId":73,"viewCount":74,"likeCount":26,"isTop":30,"isPublish":26,"seoKeywords":15,"seoDescription":15,"publishTime":75,"createTime":76,"updateTime":77,"deleteTime":78,"category":79,"tags":80},"18","NestJS 学习笔记","nestjs学习笔记","# NestJS 学习笔记\n\n## 目录\n\n1. [NestJS 简介](#1-nestjs-简介)\n2. [环境搭建](#2-环境搭建)\n3. [项目结构](#3-项目结构)\n4. [控制器 Controller](#4-控制器-controller)\n5. [服务 Service（Provider）](#5-服务-serviceprovider)\n6. [模块 Module](#6-模块-module)\n7. [中间件 Middleware](#7-中间件-middleware)\n8. [异常过滤器 Exception Filter](#8-异常过滤器-exception-filter)\n9. [管道 Pipe](#9-管道-pipe)\n10. [守卫 Guard](#10-守卫-guard)\n11. [拦截器 Interceptor](#11-拦截器-interceptor)\n12. [自定义装饰器](#12-自定义装饰器)\n13. [数据库集成（TypeORM）](#13-数据库集成typeorm)\n14. [数据验证与转换（DTO）](#14-数据验证与转换dto)\n15. [身份认证（JWT）](#15-身份认证jwt)\n16. [文件上传](#16-文件上传)\n17. [Swagger 接口文档](#17-swagger-接口文档)\n18. [配置管理](#18-配置管理)\n19. [部署与优化](#19-部署与优化)\n\n***\n\n## 1. NestJS 简介\n\n### 1.1 什么是 NestJS\n\n​\tNestJS 是一个用于构建高效、可扩展的 Node.js 服务端应用程序的框架。它使用 TypeScript 构建，底层默认使用 Express（也可以切换为 Fastify），借鉴了 Angular 的架构思想（模块化、依赖注入、装饰器）。\n\n**核心特性：**\n\n- 完全支持 TypeScript\n- 模块化架构（Module）\n- 依赖注入（DI \u002F IoC）\n- 装饰器驱动开发\n- 丰富的生态系统（微服务、WebSocket、GraphQL 等）\n- 内置对测试的支持\n\n### 1.2 与 Express 的对比\n\n| 特性       | Express          | NestJS                    |\n| ---------- | ---------------- | ------------------------- |\n| 语言       | JavaScript       | TypeScript（原生支持）    |\n| 架构       | 无固定架构       | 模块化 + 依赖注入         |\n| 学习曲线   | 低               | 中等                      |\n| 适用场景   | 小型\u002F中型项目    | 中大型企业级项目          |\n| 代码组织   | 自由组织         | 强约束，分层清晰          |\n| 底层框架   | 自身             | Express \u002F Fastify         |\n| 装饰器     | 不支持           | 核心特性                  |\n| 依赖注入   | 无               | 内置 IoC 容器             |\n\n### 1.3 核心概念一览\n\nNestJS 的请求生命周期（按顺序执行）：\n\n```\n客户端请求 → 中间件(Middleware) → 守卫(Guard) → 拦截器(Interceptor-前) \n→ 管道(Pipe) → 控制器(Controller) → 服务(Service) \n→ 拦截器(Interceptor-后) → 异常过滤器(Exception Filter) → 客户端响应\n```\n\n***\n\n## 2. 环境搭建\n\n### 2.1 系统要求\n\n- Node.js >= 16.0.0\n- 包管理器：npm \u002F yarn \u002F pnpm\n- TypeScript 基础知识\n\n### 2.2 安装 NestJS CLI\n\n```bash\n# 全局安装 NestJS 脚手架\nnpm i -g @nestjs\u002Fcli\n\n# 验证安装\nnest --version\n```\n\n### 2.3 创建项目\n\n```bash\n# 创建新项目\nnest new my-nest-app\n\n# 选择包管理器 npm\u002Fyarn\u002Fpnpm\n\n# 进入项目目录\ncd my-nest-app\n\n# 启动开发服务器（热重载）\nnpm run start:dev\n```\n\n​\t启动后访问 http:\u002F\u002Flocalhost:3000，看到 `Hello World!` 即成功。\n\n### 2.4 常用 CLI 命令\n\n```bash\n# 生成模块\nnest g module users\n\n# 生成控制器\nnest g controller users\n\n# 生成服务\nnest g service users\n\n# 一键生成完整 CRUD 资源（推荐）\nnest g resource users\n\n# 生成中间件\nnest g middleware logger\n\n# 生成守卫\nnest g guard auth\n\n# 生成拦截器\nnest g interceptor transform\n\n# 生成管道\nnest g pipe validation\n\n# 生成过滤器\nnest g filter http-exception\n```\n\n***\n\n## 3. 项目结构\n\n### 3.1 目录结构说明\n\n```\nmy-nest-app\u002F\n├── dist\u002F                    # 编译输出目录\n├── node_modules\u002F\n├── src\u002F\n│   ├── app.controller.ts    # 根控制器\n│   ├── app.controller.spec.ts # 控制器测试\n│   ├── app.module.ts        # 根模块\n│   ├── app.service.ts       # 根服务\n│   └── main.ts              # 应用入口文件\n├── test\u002F                    # 端到端测试\n│   ├── app.e2e-spec.ts\n│   └── jest-e2e.json\n├── nest-cli.json            # NestJS CLI 配置\n├── tsconfig.json            # TypeScript 配置\n├── tsconfig.build.json\n└── package.json\n```\n\n### 3.2 入口文件 main.ts\n\n```typescript\n\u002F\u002F src\u002Fmain.ts\nimport { NestFactory } from '@nestjs\u002Fcore';\nimport { AppModule } from '.\u002Fapp.module';\n\nasync function bootstrap() {\n  \u002F\u002F 创建 Nest 应用实例\n  const app = await NestFactory.create(AppModule);\n  \n  \u002F\u002F 设置全局路由前缀\n  app.setGlobalPrefix('api');\n  \n  \u002F\u002F 启用 CORS 跨域\n  app.enableCors();\n  \n  \u002F\u002F 监听端口\n  await app.listen(3000);\n  console.log('服务器运行在 http:\u002F\u002Flocalhost:3000');\n}\nbootstrap();\n```\n\n### 3.3 推荐的项目结构（模块化）\n\n```\nsrc\u002F\n├── main.ts\n├── app.module.ts\n├── common\u002F                  # 公共模块\n│   ├── decorators\u002F          # 自定义装饰器\n│   ├── filters\u002F             # 异常过滤器\n│   ├── guards\u002F              # 守卫\n│   ├── interceptors\u002F        # 拦截器\n│   ├── middleware\u002F           # 中间件\n│   └── pipes\u002F               # 管道\n├── config\u002F                  # 配置模块\n│   └── config.module.ts\n├── users\u002F                   # 用户模块\n│   ├── dto\u002F                 # 数据传输对象\n│   │   ├── create-user.dto.ts\n│   │   └── update-user.dto.ts\n│   ├── entities\u002F            # 数据库实体\n│   │   └── user.entity.ts\n│   ├── users.controller.ts\n│   ├── users.service.ts\n│   ├── users.module.ts\n│   └── users.controller.spec.ts\n└── auth\u002F                    # 认证模块\n    ├── auth.controller.ts\n    ├── auth.service.ts\n    ├── auth.module.ts\n    └── strategies\u002F\n        └── jwt.strategy.ts\n```\n\n***\n\n## 4. 控制器 Controller\n\n### 4.1 控制器的作用\n\n​\t控制器负责处理传入的 HTTP 请求，返回响应给客户端。使用 `@Controller()` 装饰器标记类。\n\n### 4.2 基本用法\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fusers.controller.ts\nimport {\n  Controller,\n  Get,\n  Post,\n  Put,\n  Delete,\n  Param,\n  Body,\n  Query,\n  HttpCode,\n  HttpStatus,\n} from '@nestjs\u002Fcommon';\n\n@Controller('users') \u002F\u002F 路由前缀 \u002Fusers\nexport class UsersController {\n\n  \u002F\u002F GET \u002Fusers\n  @Get()\n  findAll() {\n    return { code: 200, msg: '查询全部用户成功', data: [] };\n  }\n\n  \u002F\u002F GET \u002Fusers\u002F:id\n  @Get(':id')\n  findOne(@Param('id') id: string) {\n    return { code: 200, msg: '查询成功', data: { id } };\n  }\n\n  \u002F\u002F POST \u002Fusers\n  @Post()\n  @HttpCode(HttpStatus.CREATED) \u002F\u002F 设置状态码 201\n  create(@Body() createUserDto: any) {\n    return { code: 201, msg: '创建成功', data: createUserDto };\n  }\n\n  \u002F\u002F PUT \u002Fusers\u002F:id\n  @Put(':id')\n  update(@Param('id') id: string, @Body() updateUserDto: any) {\n    return { code: 200, msg: '修改成功', data: { id, ...updateUserDto } };\n  }\n\n  \u002F\u002F DELETE \u002Fusers\u002F:id\n  @Delete(':id')\n  remove(@Param('id') id: string) {\n    return { code: 200, msg: '删除成功', data: { id } };\n  }\n}\n```\n\n### 4.3 请求参数获取\n\n```typescript\n@Controller('users')\nexport class UsersController {\n\n  \u002F\u002F Query 参数: GET \u002Fusers?page=1&limit=10\n  @Get()\n  findAll(@Query('page') page: number, @Query('limit') limit: number) {\n    return { page, limit };\n  }\n\n  \u002F\u002F 动态路由参数: GET \u002Fusers\u002F123\n  @Get(':id')\n  findOne(@Param('id') id: string) {\n    return { id };\n  }\n\n  \u002F\u002F 请求体: POST \u002Fusers  body: { name: 'zhangsan', age: 20 }\n  @Post()\n  create(@Body() body: any) {\n    return body;\n  }\n\n  \u002F\u002F 获取请求头\n  @Get('info')\n  getInfo(@Headers('authorization') token: string) {\n    return { token };\n  }\n\n  \u002F\u002F 获取原始 Request 和 Response 对象\n  @Get('raw')\n  getRaw(@Req() req: Request, @Res() res: Response) {\n    res.status(200).json({ url: req.url });\n  }\n}\n```\n\n### 4.4 路由通配符与版本控制\n\n```typescript\n\u002F\u002F 通配符路由\n@Get('ab*cd')\nfindWildcard() {\n  return '匹配 abcd, ab_cd, abecd 等';\n}\n\n\u002F\u002F 路由版本控制（需在 main.ts 中启用）\n\u002F\u002F main.ts: app.enableVersioning({ type: VersioningType.URI });\n@Controller({ path: 'users', version: '1' })\nexport class UsersV1Controller {\n  @Get()\n  findAll() {\n    return 'v1 版本';\n  }\n}\n```\n\n***\n\n## 5. 服务 Service（Provider）\n\n### 5.1 服务的作用\n\n​\t服务负责处理业务逻辑，是 NestJS 中 **Provider** 的主要形式。通过 **依赖注入（DI）** 的方式注入到控制器中使用。\n\n### 5.2 创建服务\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fusers.service.ts\nimport { Injectable, NotFoundException } from '@nestjs\u002Fcommon';\n\n\u002F\u002F @Injectable() 标记这个类可以被 IoC 容器管理\n@Injectable()\nexport class UsersService {\n  \u002F\u002F 模拟数据库\n  private users = [\n    { id: 1, name: '张三', age: 20 },\n    { id: 2, name: '李四', age: 25 },\n  ];\n\n  \u002F\u002F 查询全部\n  findAll() {\n    return this.users;\n  }\n\n  \u002F\u002F 根据 ID 查询\n  findOne(id: number) {\n    const user = this.users.find((u) => u.id === id);\n    if (!user) {\n      throw new NotFoundException(`用户 ID ${id} 不存在`);\n    }\n    return user;\n  }\n\n  \u002F\u002F 新增\n  create(createUserDto: { name: string; age: number }) {\n    const newUser = {\n      id: this.users.length + 1,\n      ...createUserDto,\n    };\n    this.users.push(newUser);\n    return newUser;\n  }\n\n  \u002F\u002F 修改\n  update(id: number, updateUserDto: { name?: string; age?: number }) {\n    const user = this.findOne(id);\n    Object.assign(user, updateUserDto);\n    return user;\n  }\n\n  \u002F\u002F 删除\n  remove(id: number) {\n    const index = this.users.findIndex((u) => u.id === id);\n    if (index === -1) {\n      throw new NotFoundException(`用户 ID ${id} 不存在`);\n    }\n    return this.users.splice(index, 1)[0];\n  }\n}\n```\n\n### 5.3 在控制器中注入服务\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fusers.controller.ts\nimport { Controller, Get, Post, Put, Delete, Param, Body } from '@nestjs\u002Fcommon';\nimport { UsersService } from '.\u002Fusers.service';\n\n@Controller('users')\nexport class UsersController {\n  \u002F\u002F 构造函数注入（推荐方式）\n  constructor(private readonly usersService: UsersService) {}\n\n  @Get()\n  findAll() {\n    return {\n      code: 200,\n      msg: '查询成功',\n      data: this.usersService.findAll(),\n    };\n  }\n\n  @Get(':id')\n  findOne(@Param('id') id: string) {\n    return {\n      code: 200,\n      msg: '查询成功',\n      data: this.usersService.findOne(+id), \u002F\u002F +id 将字符串转为数字\n    };\n  }\n\n  @Post()\n  create(@Body() createUserDto: { name: string; age: number }) {\n    return {\n      code: 201,\n      msg: '创建成功',\n      data: this.usersService.create(createUserDto),\n    };\n  }\n\n  @Put(':id')\n  update(@Param('id') id: string, @Body() updateUserDto: any) {\n    return {\n      code: 200,\n      msg: '修改成功',\n      data: this.usersService.update(+id, updateUserDto),\n    };\n  }\n\n  @Delete(':id')\n  remove(@Param('id') id: string) {\n    return {\n      code: 200,\n      msg: '删除成功',\n      data: this.usersService.remove(+id),\n    };\n  }\n}\n```\n\n### 5.4 依赖注入原理\n\n```\n1. @Injectable() 标记 UsersService 为可注入的 Provider\n2. 在 UsersModule 的 providers 数组中注册 UsersService\n3. 控制器通过构造函数声明依赖\n4. NestJS 的 IoC 容器自动创建实例并注入\n\n好处：解耦、易测试、易替换\n```\n\n***\n\n## 6. 模块 Module\n\n### 6.1 模块的作用\n\n​\t模块是 NestJS 组织代码的基本单元。每个应用至少有一个根模块 `AppModule`，通过模块将相关的控制器和服务组织在一起。\n\n### 6.2 基本模块定义\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fusers.module.ts\nimport { Module } from '@nestjs\u002Fcommon';\nimport { UsersController } from '.\u002Fusers.controller';\nimport { UsersService } from '.\u002Fusers.service';\n\n@Module({\n  controllers: [UsersController],   \u002F\u002F 注册控制器\n  providers: [UsersService],        \u002F\u002F 注册服务（Provider）\n  exports: [UsersService],          \u002F\u002F 导出服务，供其他模块使用\n})\nexport class UsersModule {}\n```\n\n### 6.3 根模块注册\n\n```typescript\n\u002F\u002F src\u002Fapp.module.ts\nimport { Module } from '@nestjs\u002Fcommon';\nimport { UsersModule } from '.\u002Fusers\u002Fusers.module';\nimport { AuthModule } from '.\u002Fauth\u002Fauth.module';\n\n@Module({\n  imports: [UsersModule, AuthModule], \u002F\u002F 导入子模块\n  controllers: [],\n  providers: [],\n})\nexport class AppModule {}\n```\n\n### 6.4 模块分类\n\n```typescript\n\u002F\u002F 1. 功能模块 - 组织特定业务功能\n@Module({\n  controllers: [UsersController],\n  providers: [UsersService],\n})\nexport class UsersModule {}\n\n\u002F\u002F 2. 共享模块 - 被多个模块复用\n@Module({\n  providers: [HelperService],\n  exports: [HelperService], \u002F\u002F 必须导出才能被其他模块使用\n})\nexport class SharedModule {}\n\n\u002F\u002F 3. 全局模块 - 注册一次，全局可用\n@Global() \u002F\u002F 添加 @Global() 装饰器\n@Module({\n  providers: [ConfigService],\n  exports: [ConfigService],\n})\nexport class ConfigModule {}\n\n\u002F\u002F 4. 动态模块 - 可接受配置参数\n@Module({})\nexport class DatabaseModule {\n  static forRoot(options: DatabaseOptions): DynamicModule {\n    return {\n      module: DatabaseModule,\n      providers: [\n        {\n          provide: 'DATABASE_OPTIONS',\n          useValue: options,\n        },\n        DatabaseService,\n      ],\n      exports: [DatabaseService],\n    };\n  }\n}\n\u002F\u002F 使用: imports: [DatabaseModule.forRoot({ host: 'localhost', port: 3306 })]\n```\n\n***\n\n## 7. 中间件 Middleware\n\n### 7.1 中间件的作用\n\n​\t在路由处理程序之前执行的函数，可以访问请求和响应对象，常用于日志记录、身份验证前置处理等。\n\n### 7.2 创建中间件\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Fmiddleware\u002Flogger.middleware.ts\nimport { Injectable, NestMiddleware } from '@nestjs\u002Fcommon';\nimport { Request, Response, NextFunction } from 'express';\n\n@Injectable()\nexport class LoggerMiddleware implements NestMiddleware {\n  use(req: Request, res: Response, next: NextFunction) {\n    const { method, originalUrl } = req;\n    const start = Date.now();\n\n    \u002F\u002F 响应完成时记录日志\n    res.on('finish', () => {\n      const duration = Date.now() - start;\n      console.log(`[${method}] ${originalUrl} - ${res.statusCode} ${duration}ms`);\n    });\n\n    next(); \u002F\u002F 必须调用 next()，否则请求会挂起\n  }\n}\n```\n\n### 7.3 注册中间件\n\n```typescript\n\u002F\u002F src\u002Fapp.module.ts\nimport { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs\u002Fcommon';\nimport { LoggerMiddleware } from '.\u002Fcommon\u002Fmiddleware\u002Flogger.middleware';\nimport { UsersModule } from '.\u002Fusers\u002Fusers.module';\n\n@Module({\n  imports: [UsersModule],\n})\nexport class AppModule implements NestModule {\n  configure(consumer: MiddlewareConsumer) {\n    consumer\n      .apply(LoggerMiddleware)\n      \u002F\u002F 指定路由\n      .forRoutes('users');\n    \n    \u002F\u002F 也可以指定请求方法\n    \u002F\u002F .forRoutes({ path: 'users', method: RequestMethod.GET });\n    \n    \u002F\u002F 排除某些路由\n    \u002F\u002F .exclude({ path: 'users', method: RequestMethod.POST })\n    \u002F\u002F .forRoutes('users');\n  }\n}\n```\n\n### 7.4 函数式中间件（简单场景）\n\n```typescript\n\u002F\u002F 简单中间件可以用函数代替\nexport function logger(req: Request, res: Response, next: NextFunction) {\n  console.log(`[请求] ${req.method} ${req.url}`);\n  next();\n}\n\n\u002F\u002F 注册\nconsumer.apply(logger).forRoutes('*');\n```\n\n### 7.5 全局中间件\n\n```typescript\n\u002F\u002F src\u002Fmain.ts\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n  \n  \u002F\u002F 全局中间件\n  app.use(logger);\n  \n  await app.listen(3000);\n}\n```\n\n***\n\n## 8. 异常过滤器 Exception Filter\n\n### 8.1 内置异常类\n\nNestJS 提供了多种内置 HTTP 异常：\n\n```typescript\nimport {\n  BadRequestException,      \u002F\u002F 400\n  UnauthorizedException,     \u002F\u002F 401\n  ForbiddenException,        \u002F\u002F 403\n  NotFoundException,         \u002F\u002F 404\n  ConflictException,         \u002F\u002F 409\n  InternalServerErrorException, \u002F\u002F 500\n} from '@nestjs\u002Fcommon';\n\n\u002F\u002F 使用方式：直接在 Service 或 Controller 中抛出\nthrow new NotFoundException('用户不存在');\nthrow new BadRequestException('参数错误');\nthrow new UnauthorizedException('请先登录');\n```\n\n### 8.2 自定义异常过滤器\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Ffilters\u002Fhttp-exception.filter.ts\nimport {\n  ExceptionFilter,\n  Catch,\n  ArgumentsHost,\n  HttpException,\n  HttpStatus,\n} from '@nestjs\u002Fcommon';\nimport { Request, Response } from 'express';\n\n@Catch(HttpException)\nexport class HttpExceptionFilter implements ExceptionFilter {\n  catch(exception: HttpException, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const response = ctx.getResponse\u003CResponse>();\n    const request = ctx.getRequest\u003CRequest>();\n    const status = exception.getStatus();\n    const exceptionResponse = exception.getResponse();\n\n    \u002F\u002F 统一异常响应格式\n    response.status(status).json({\n      code: status,\n      msg: typeof exceptionResponse === 'string'\n        ? exceptionResponse\n        : (exceptionResponse as any).message,\n      data: null,\n      timestamp: new Date().toISOString(),\n      path: request.url,\n    });\n  }\n}\n```\n\n### 8.3 捕获所有异常\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Ffilters\u002Fall-exception.filter.ts\nimport {\n  ExceptionFilter,\n  Catch,\n  ArgumentsHost,\n  HttpException,\n  HttpStatus,\n} from '@nestjs\u002Fcommon';\n\n@Catch() \u002F\u002F 不传参数，捕获所有异常\nexport class AllExceptionFilter implements ExceptionFilter {\n  catch(exception: unknown, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const response = ctx.getResponse();\n    const request = ctx.getRequest();\n\n    const status =\n      exception instanceof HttpException\n        ? exception.getStatus()\n        : HttpStatus.INTERNAL_SERVER_ERROR;\n\n    const message =\n      exception instanceof HttpException\n        ? exception.message\n        : '服务器内部错误';\n\n    response.status(status).json({\n      code: status,\n      msg: message,\n      data: null,\n      timestamp: new Date().toISOString(),\n      path: request.url,\n    });\n  }\n}\n```\n\n### 8.4 注册过滤器\n\n```typescript\n\u002F\u002F 方式一：全局注册（推荐）在 main.ts 中\napp.useGlobalFilters(new AllExceptionFilter());\n\n\u002F\u002F 方式二：控制器级别\n@Controller('users')\n@UseFilters(new HttpExceptionFilter())\nexport class UsersController {}\n\n\u002F\u002F 方式三：方法级别\n@Get(':id')\n@UseFilters(new HttpExceptionFilter())\nfindOne(@Param('id') id: string) {}\n```\n\n***\n\n## 9. 管道 Pipe\n\n### 9.1 管道的作用\n\n​\t管道有两个典型用途：**数据转换**（将输入数据转为目标类型）和 **数据验证**（验证输入数据是否合法）。\n\n### 9.2 内置管道\n\n```typescript\nimport {\n  ParseIntPipe,       \u002F\u002F 转换为整数\n  ParseFloatPipe,     \u002F\u002F 转换为浮点数\n  ParseBoolPipe,      \u002F\u002F 转换为布尔值\n  ParseUUIDPipe,      \u002F\u002F 验证 UUID 格式\n  ParseArrayPipe,     \u002F\u002F 转换为数组\n  DefaultValuePipe,   \u002F\u002F 设置默认值\n  ValidationPipe,     \u002F\u002F 类验证管道\n} from '@nestjs\u002Fcommon';\n\n\u002F\u002F 使用示例\n@Get(':id')\nfindOne(@Param('id', ParseIntPipe) id: number) {\n  \u002F\u002F id 已经被自动转换为 number 类型\n  \u002F\u002F 如果传入 'abc' 会自动返回 400 错误\n  return this.usersService.findOne(id);\n}\n\n@Get()\nfindAll(\n  @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,\n  @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,\n) {\n  return { page, limit };\n}\n```\n\n### 9.3 全局验证管道（最常用）\n\n```bash\n# 安装验证相关依赖\nnpm i class-validator class-transformer\n```\n\n```typescript\n\u002F\u002F src\u002Fmain.ts\nimport { ValidationPipe } from '@nestjs\u002Fcommon';\n\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n  \n  \u002F\u002F 全局启用验证管道\n  app.useGlobalPipes(\n    new ValidationPipe({\n      whitelist: true,            \u002F\u002F 自动剥离 DTO 中未定义的属性\n      forbidNonWhitelisted: true, \u002F\u002F 传入未定义属性时直接报错\n      transform: true,            \u002F\u002F 自动类型转换\n    }),\n  );\n  \n  await app.listen(3000);\n}\n```\n\n### 9.4 自定义管道\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Fpipes\u002Fparse-int.pipe.ts\nimport {\n  PipeTransform,\n  Injectable,\n  ArgumentMetadata,\n  BadRequestException,\n} from '@nestjs\u002Fcommon';\n\n@Injectable()\nexport class MyParseIntPipe implements PipeTransform\u003Cstring, number> {\n  transform(value: string, metadata: ArgumentMetadata): number {\n    const val = parseInt(value, 10);\n    if (isNaN(val)) {\n      throw new BadRequestException(`\"${value}\" 不是一个有效的数字`);\n    }\n    return val;\n  }\n}\n```\n\n***\n\n## 10. 守卫 Guard\n\n### 10.1 守卫的作用\n\n​\t守卫决定请求是否应该被路由处理程序处理，通常用于**身份认证和权限控制**。返回 `true` 放行，返回 `false` 拒绝。\n\n### 10.2 创建守卫\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Fguards\u002Fauth.guard.ts\nimport {\n  CanActivate,\n  ExecutionContext,\n  Injectable,\n  UnauthorizedException,\n} from '@nestjs\u002Fcommon';\nimport { Request } from 'express';\n\n@Injectable()\nexport class AuthGuard implements CanActivate {\n  canActivate(context: ExecutionContext): boolean {\n    const request = context.switchToHttp().getRequest\u003CRequest>();\n    const token = request.headers['authorization'];\n\n    if (!token) {\n      throw new UnauthorizedException('缺少认证 Token');\n    }\n\n    \u002F\u002F 验证 token 的逻辑...\n    return true;\n  }\n}\n```\n\n### 10.3 角色守卫（基于权限）\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Fguards\u002Froles.guard.ts\nimport { Injectable, CanActivate, ExecutionContext } from '@nestjs\u002Fcommon';\nimport { Reflector } from '@nestjs\u002Fcore';\n\n@Injectable()\nexport class RolesGuard implements CanActivate {\n  constructor(private reflector: Reflector) {}\n\n  canActivate(context: ExecutionContext): boolean {\n    \u002F\u002F 获取路由上通过 @Roles() 装饰器设置的角色\n    const requiredRoles = this.reflector.get\u003Cstring[]>(\n      'roles',\n      context.getHandler(),\n    );\n    if (!requiredRoles) {\n      return true; \u002F\u002F 没设置角色要求，直接放行\n    }\n\n    const request = context.switchToHttp().getRequest();\n    const user = request.user; \u002F\u002F 通常由认证中间件设置\n    return requiredRoles.some((role) => user?.roles?.includes(role));\n  }\n}\n```\n\n### 10.4 注册守卫\n\n```typescript\n\u002F\u002F 方式一：全局注册\napp.useGlobalGuards(new AuthGuard());\n\n\u002F\u002F 方式二：控制器级别\n@Controller('users')\n@UseGuards(AuthGuard)\nexport class UsersController {}\n\n\u002F\u002F 方式三：方法级别\n@Get('profile')\n@UseGuards(AuthGuard, RolesGuard)\ngetProfile() {}\n```\n\n***\n\n## 11. 拦截器 Interceptor\n\n### 11.1 拦截器的作用\n\n​\t拦截器可以在方法执行之前\u002F之后绑定额外的逻辑，常用于：统一响应格式、日志记录、缓存、超时处理等。\n\n### 11.2 统一响应格式拦截器\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Finterceptors\u002Ftransform.interceptor.ts\nimport {\n  Injectable,\n  NestInterceptor,\n  ExecutionContext,\n  CallHandler,\n} from '@nestjs\u002Fcommon';\nimport { Observable } from 'rxjs';\nimport { map } from 'rxjs\u002Foperators';\n\n\u002F\u002F 统一响应结构\ninterface Response\u003CT> {\n  code: number;\n  msg: string;\n  data: T;\n}\n\n@Injectable()\nexport class TransformInterceptor\u003CT>\n  implements NestInterceptor\u003CT, Response\u003CT>>\n{\n  intercept(\n    context: ExecutionContext,\n    next: CallHandler,\n  ): Observable\u003CResponse\u003CT>> {\n    return next.handle().pipe(\n      map((data) => ({\n        code: 200,\n        msg: '请求成功',\n        data,\n      })),\n    );\n  }\n}\n```\n\n### 11.3 日志拦截器\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Finterceptors\u002Flogging.interceptor.ts\nimport {\n  Injectable,\n  NestInterceptor,\n  ExecutionContext,\n  CallHandler,\n} from '@nestjs\u002Fcommon';\nimport { Observable } from 'rxjs';\nimport { tap } from 'rxjs\u002Foperators';\n\n@Injectable()\nexport class LoggingInterceptor implements NestInterceptor {\n  intercept(context: ExecutionContext, next: CallHandler): Observable\u003Cany> {\n    const request = context.switchToHttp().getRequest();\n    const { method, url } = request;\n    const now = Date.now();\n\n    console.log(`[请求开始] ${method} ${url}`);\n\n    return next.handle().pipe(\n      tap(() => {\n        console.log(`[请求结束] ${method} ${url} - ${Date.now() - now}ms`);\n      }),\n    );\n  }\n}\n```\n\n### 11.4 超时拦截器\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Finterceptors\u002Ftimeout.interceptor.ts\nimport {\n  Injectable,\n  NestInterceptor,\n  ExecutionContext,\n  CallHandler,\n  RequestTimeoutException,\n} from '@nestjs\u002Fcommon';\nimport { Observable, throwError, TimeoutError } from 'rxjs';\nimport { catchError, timeout } from 'rxjs\u002Foperators';\n\n@Injectable()\nexport class TimeoutInterceptor implements NestInterceptor {\n  intercept(context: ExecutionContext, next: CallHandler): Observable\u003Cany> {\n    return next.handle().pipe(\n      timeout(5000), \u002F\u002F 5秒超时\n      catchError((err) => {\n        if (err instanceof TimeoutError) {\n          return throwError(() => new RequestTimeoutException('请求超时'));\n        }\n        return throwError(() => err);\n      }),\n    );\n  }\n}\n```\n\n### 11.5 注册拦截器\n\n```typescript\n\u002F\u002F 全局注册（推荐）\napp.useGlobalInterceptors(new TransformInterceptor());\n\n\u002F\u002F 控制器级别\n@Controller('users')\n@UseInterceptors(LoggingInterceptor)\nexport class UsersController {}\n```\n\n***\n\n## 12. 自定义装饰器\n\n### 12.1 参数装饰器\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Fdecorators\u002Fcurrent-user.decorator.ts\nimport { createParamDecorator, ExecutionContext } from '@nestjs\u002Fcommon';\n\n\u002F\u002F 获取当前登录用户信息\nexport const CurrentUser = createParamDecorator(\n  (data: string, ctx: ExecutionContext) => {\n    const request = ctx.switchToHttp().getRequest();\n    const user = request.user;\n    \u002F\u002F 如果传了属性名，返回指定属性；否则返回整个 user 对象\n    return data ? user?.[data] : user;\n  },\n);\n\n\u002F\u002F 使用\n@Get('profile')\ngetProfile(@CurrentUser() user: any) {\n  return user;\n}\n\n@Get('name')\ngetName(@CurrentUser('name') name: string) {\n  return { name };\n}\n```\n\n### 12.2 方法\u002F类装饰器\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Fdecorators\u002Froles.decorator.ts\nimport { SetMetadata } from '@nestjs\u002Fcommon';\n\n\u002F\u002F 角色装饰器\nexport const Roles = (...roles: string[]) => SetMetadata('roles', roles);\n\n\u002F\u002F 使用：配合 RolesGuard\n@Post()\n@Roles('admin')\ncreate(@Body() dto: CreateUserDto) {}\n```\n\n### 12.3 组合装饰器\n\n```typescript\n\u002F\u002F 将多个装饰器组合为一个\nimport { applyDecorators, UseGuards } from '@nestjs\u002Fcommon';\n\nexport function Auth(...roles: string[]) {\n  return applyDecorators(\n    Roles(...roles),\n    UseGuards(AuthGuard, RolesGuard),\n  );\n}\n\n\u002F\u002F 使用：一个装饰器搞定认证 + 权限\n@Post()\n@Auth('admin')\ncreate(@Body() dto: CreateUserDto) {}\n```\n\n***\n\n## 13. 数据库集成（TypeORM）\n\n### 13.1 安装依赖\n\n```bash\n# TypeORM + MySQL\nnpm i @nestjs\u002Ftypeorm typeorm mysql2\n\n# 如果使用 PostgreSQL\n# npm i @nestjs\u002Ftypeorm typeorm pg\n```\n\n### 13.2 配置数据库连接\n\n```typescript\n\u002F\u002F src\u002Fapp.module.ts\nimport { Module } from '@nestjs\u002Fcommon';\nimport { TypeOrmModule } from '@nestjs\u002Ftypeorm';\n\n@Module({\n  imports: [\n    TypeOrmModule.forRoot({\n      type: 'mysql',\n      host: 'localhost',\n      port: 3306,\n      username: 'root',\n      password: '123456',\n      database: 'nest_db',\n      entities: [__dirname + '\u002F**\u002F*.entity{.ts,.js}'], \u002F\u002F 自动加载实体\n      synchronize: true, \u002F\u002F 开发环境自动同步表结构（生产环境务必关闭！）\n    }),\n    UsersModule,\n  ],\n})\nexport class AppModule {}\n```\n\n### 13.3 定义实体\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fentities\u002Fuser.entity.ts\nimport {\n  Entity,\n  Column,\n  PrimaryGeneratedColumn,\n  CreateDateColumn,\n  UpdateDateColumn,\n} from 'typeorm';\n\n@Entity('users') \u002F\u002F 表名\nexport class User {\n  @PrimaryGeneratedColumn() \u002F\u002F 自增主键\n  id: number;\n\n  @Column({ length: 50, comment: '用户名' })\n  username: string;\n\n  @Column({ comment: '密码' })\n  password: string;\n\n  @Column({ nullable: true, comment: '邮箱' })\n  email: string;\n\n  @Column({ default: 1, comment: '状态 1正常 0禁用' })\n  status: number;\n\n  @Column({ type: 'enum', enum: ['admin', 'user'], default: 'user' })\n  role: string;\n\n  @CreateDateColumn({ comment: '创建时间' })\n  createTime: Date;\n\n  @UpdateDateColumn({ comment: '更新时间' })\n  updateTime: Date;\n}\n```\n\n### 13.4 在模块中注册实体\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fusers.module.ts\nimport { Module } from '@nestjs\u002Fcommon';\nimport { TypeOrmModule } from '@nestjs\u002Ftypeorm';\nimport { User } from '.\u002Fentities\u002Fuser.entity';\nimport { UsersController } from '.\u002Fusers.controller';\nimport { UsersService } from '.\u002Fusers.service';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([User])], \u002F\u002F 注册实体\n  controllers: [UsersController],\n  providers: [UsersService],\n  exports: [UsersService],\n})\nexport class UsersModule {}\n```\n\n### 13.5 Service 中使用 Repository\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fusers.service.ts\nimport { Injectable, NotFoundException } from '@nestjs\u002Fcommon';\nimport { InjectRepository } from '@nestjs\u002Ftypeorm';\nimport { Repository, Like } from 'typeorm';\nimport { User } from '.\u002Fentities\u002Fuser.entity';\n\n@Injectable()\nexport class UsersService {\n  constructor(\n    @InjectRepository(User)\n    private readonly userRepository: Repository\u003CUser>,\n  ) {}\n\n  \u002F\u002F 查询全部\n  async findAll(): Promise\u003CUser[]> {\n    return await this.userRepository.find();\n  }\n\n  \u002F\u002F 分页查询\n  async findPage(page: number, limit: number) {\n    const [data, total] = await this.userRepository.findAndCount({\n      skip: (page - 1) * limit,\n      take: limit,\n      order: { createTime: 'DESC' },\n    });\n    return { data, total, page, limit };\n  }\n\n  \u002F\u002F 条件查询\n  async findByUsername(username: string): Promise\u003CUser[]> {\n    return await this.userRepository.find({\n      where: { username: Like(`%${username}%`) },\n    });\n  }\n\n  \u002F\u002F 根据 ID 查询\n  async findOne(id: number): Promise\u003CUser> {\n    const user = await this.userRepository.findOneBy({ id });\n    if (!user) {\n      throw new NotFoundException(`用户 ID ${id} 不存在`);\n    }\n    return user;\n  }\n\n  \u002F\u002F 新增\n  async create(createUserDto: Partial\u003CUser>): Promise\u003CUser> {\n    const user = this.userRepository.create(createUserDto);\n    return await this.userRepository.save(user);\n  }\n\n  \u002F\u002F 修改\n  async update(id: number, updateUserDto: Partial\u003CUser>): Promise\u003CUser> {\n    await this.userRepository.update(id, updateUserDto);\n    return this.findOne(id);\n  }\n\n  \u002F\u002F 删除\n  async remove(id: number): Promise\u003Cvoid> {\n    const result = await this.userRepository.delete(id);\n    if (result.affected === 0) {\n      throw new NotFoundException(`用户 ID ${id} 不存在`);\n    }\n  }\n}\n```\n\n### 13.6 实体关系\n\n```typescript\n\u002F\u002F 一对多关系示例\n\u002F\u002F 一个用户有多篇文章\n\n\u002F\u002F user.entity.ts\n@Entity()\nexport class User {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  username: string;\n\n  @OneToMany(() => Article, (article) => article.author)\n  articles: Article[];\n}\n\n\u002F\u002F article.entity.ts\n@Entity()\nexport class Article {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  title: string;\n\n  @Column('text')\n  content: string;\n\n  @ManyToOne(() => User, (user) => user.articles)\n  @JoinColumn({ name: 'author_id' })\n  author: User;\n}\n\n\u002F\u002F 查询时关联加载\nconst users = await this.userRepository.find({\n  relations: ['articles'],\n});\n```\n\n***\n\n## 14. 数据验证与转换（DTO）\n\n### 14.1 什么是 DTO\n\n​\tDTO（Data Transfer Object）数据传输对象，用于定义接口接收数据的结构和验证规则。配合 `class-validator` 和 `class-transformer` 使用。\n\n### 14.2 安装依赖\n\n```bash\nnpm i class-validator class-transformer\n```\n\n### 14.3 定义 DTO\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fdto\u002Fcreate-user.dto.ts\nimport {\n  IsString,\n  IsNotEmpty,\n  IsEmail,\n  IsOptional,\n  MinLength,\n  MaxLength,\n  IsEnum,\n} from 'class-validator';\n\nexport class CreateUserDto {\n  @IsString({ message: '用户名必须是字符串' })\n  @IsNotEmpty({ message: '用户名不能为空' })\n  @MinLength(2, { message: '用户名最少2个字符' })\n  @MaxLength(20, { message: '用户名最多20个字符' })\n  username: string;\n\n  @IsString()\n  @IsNotEmpty({ message: '密码不能为空' })\n  @MinLength(6, { message: '密码最少6个字符' })\n  password: string;\n\n  @IsEmail({}, { message: '邮箱格式不正确' })\n  @IsOptional() \u002F\u002F 可选字段\n  email?: string;\n\n  @IsEnum(['admin', 'user'], { message: '角色只能是 admin 或 user' })\n  @IsOptional()\n  role?: string;\n}\n```\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fdto\u002Fupdate-user.dto.ts\nimport { PartialType } from '@nestjs\u002Fmapped-types';\nimport { CreateUserDto } from '.\u002Fcreate-user.dto';\n\n\u002F\u002F PartialType 将所有字段变为可选\nexport class UpdateUserDto extends PartialType(CreateUserDto) {}\n```\n\n### 14.4 在控制器中使用 DTO\n\n```typescript\n@Controller('users')\nexport class UsersController {\n  constructor(private readonly usersService: UsersService) {}\n\n  @Post()\n  create(@Body() createUserDto: CreateUserDto) {\n    \u002F\u002F 到达这里时，数据已经过验证\n    return this.usersService.create(createUserDto);\n  }\n\n  @Put(':id')\n  update(\n    @Param('id', ParseIntPipe) id: number,\n    @Body() updateUserDto: UpdateUserDto,\n  ) {\n    return this.usersService.update(id, updateUserDto);\n  }\n}\n```\n\n### 14.5 常用验证装饰器\n\n| 装饰器                | 说明           |\n| --------------------- | -------------- |\n| `@IsString()`         | 必须是字符串   |\n| `@IsNumber()`         | 必须是数字     |\n| `@IsInt()`            | 必须是整数     |\n| `@IsBoolean()`        | 必须是布尔值   |\n| `@IsEmail()`          | 必须是邮箱     |\n| `@IsNotEmpty()`       | 不能为空       |\n| `@IsOptional()`       | 可选字段       |\n| `@MinLength(n)`       | 最小长度       |\n| `@MaxLength(n)`       | 最大长度       |\n| `@Min(n)`             | 最小值         |\n| `@Max(n)`             | 最大值         |\n| `@IsEnum(entity)`     | 枚举值验证     |\n| `@IsArray()`          | 必须是数组     |\n| `@IsDate()`           | 必须是日期     |\n| `@Matches(regex)`     | 正则匹配       |\n| `@IsUrl()`            | 必须是URL      |\n| `@ValidateNested()`   | 嵌套对象验证   |\n\n***\n\n## 15. 身份认证（JWT）\n\n### 15.1 安装依赖\n\n```bash\nnpm i @nestjs\u002Fjwt @nestjs\u002Fpassport passport passport-jwt\nnpm i -D @types\u002Fpassport-jwt\n```\n\n### 15.2 Auth 模块\n\n```typescript\n\u002F\u002F src\u002Fauth\u002Fauth.module.ts\nimport { Module } from '@nestjs\u002Fcommon';\nimport { JwtModule } from '@nestjs\u002Fjwt';\nimport { PassportModule } from '@nestjs\u002Fpassport';\nimport { AuthController } from '.\u002Fauth.controller';\nimport { AuthService } from '.\u002Fauth.service';\nimport { JwtStrategy } from '.\u002Fstrategies\u002Fjwt.strategy';\nimport { UsersModule } from '..\u002Fusers\u002Fusers.module';\n\n@Module({\n  imports: [\n    UsersModule,\n    PassportModule,\n    JwtModule.register({\n      secret: 'your-secret-key', \u002F\u002F 实际项目中应放到环境变量\n      signOptions: { expiresIn: '7d' }, \u002F\u002F token 有效期 7 天\n    }),\n  ],\n  controllers: [AuthController],\n  providers: [AuthService, JwtStrategy],\n  exports: [AuthService],\n})\nexport class AuthModule {}\n```\n\n### 15.3 Auth Service\n\n```typescript\n\u002F\u002F src\u002Fauth\u002Fauth.service.ts\nimport { Injectable, UnauthorizedException } from '@nestjs\u002Fcommon';\nimport { JwtService } from '@nestjs\u002Fjwt';\nimport { UsersService } from '..\u002Fusers\u002Fusers.service';\nimport * as bcrypt from 'bcryptjs'; \u002F\u002F npm i bcryptjs @types\u002Fbcryptjs\n\n@Injectable()\nexport class AuthService {\n  constructor(\n    private readonly usersService: UsersService,\n    private readonly jwtService: JwtService,\n  ) {}\n\n  \u002F\u002F 登录\n  async login(username: string, password: string) {\n    const user = await this.usersService.findByUsername(username);\n    if (!user) {\n      throw new UnauthorizedException('用户名或密码错误');\n    }\n\n    const isPasswordValid = await bcrypt.compare(password, user.password);\n    if (!isPasswordValid) {\n      throw new UnauthorizedException('用户名或密码错误');\n    }\n\n    \u002F\u002F 生成 JWT Token\n    const payload = { sub: user.id, username: user.username, role: user.role };\n    return {\n      access_token: this.jwtService.sign(payload),\n      user: { id: user.id, username: user.username, role: user.role },\n    };\n  }\n\n  \u002F\u002F 注册\n  async register(username: string, password: string) {\n    \u002F\u002F 密码加密\n    const hashedPassword = await bcrypt.hash(password, 10);\n    return this.usersService.create({\n      username,\n      password: hashedPassword,\n    });\n  }\n}\n```\n\n### 15.4 JWT 策略\n\n```typescript\n\u002F\u002F src\u002Fauth\u002Fstrategies\u002Fjwt.strategy.ts\nimport { Injectable, UnauthorizedException } from '@nestjs\u002Fcommon';\nimport { PassportStrategy } from '@nestjs\u002Fpassport';\nimport { ExtractJwt, Strategy } from 'passport-jwt';\n\n@Injectable()\nexport class JwtStrategy extends PassportStrategy(Strategy) {\n  constructor() {\n    super({\n      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),\n      ignoreExpiration: false,\n      secretOrKey: 'your-secret-key',\n    });\n  }\n\n  \u002F\u002F 验证通过后，返回值会被挂载到 request.user\n  async validate(payload: any) {\n    return {\n      id: payload.sub,\n      username: payload.username,\n      role: payload.role,\n    };\n  }\n}\n```\n\n### 15.5 Auth Controller\n\n```typescript\n\u002F\u002F src\u002Fauth\u002Fauth.controller.ts\nimport { Controller, Post, Body, UseGuards, Get } from '@nestjs\u002Fcommon';\nimport { AuthGuard } from '@nestjs\u002Fpassport';\nimport { AuthService } from '.\u002Fauth.service';\nimport { CurrentUser } from '..\u002Fcommon\u002Fdecorators\u002Fcurrent-user.decorator';\n\n@Controller('auth')\nexport class AuthController {\n  constructor(private readonly authService: AuthService) {}\n\n  @Post('login')\n  login(@Body() body: { username: string; password: string }) {\n    return this.authService.login(body.username, body.password);\n  }\n\n  @Post('register')\n  register(@Body() body: { username: string; password: string }) {\n    return this.authService.register(body.username, body.password);\n  }\n\n  \u002F\u002F 需要登录才能访问\n  @Get('profile')\n  @UseGuards(AuthGuard('jwt'))\n  getProfile(@CurrentUser() user: any) {\n    return user;\n  }\n}\n```\n\n***\n\n## 16. 文件上传\n\n### 16.1 安装依赖\n\n```bash\nnpm i @nestjs\u002Fplatform-express multer\nnpm i -D @types\u002Fmulter\n```\n\n### 16.2 文件上传接口\n\n```typescript\n\u002F\u002F src\u002Fupload\u002Fupload.controller.ts\nimport {\n  Controller,\n  Post,\n  UseInterceptors,\n  UploadedFile,\n  UploadedFiles,\n  ParseFilePipe,\n  MaxFileSizeValidator,\n  FileTypeValidator,\n} from '@nestjs\u002Fcommon';\nimport { FileInterceptor, FilesInterceptor } from '@nestjs\u002Fplatform-express';\nimport { diskStorage } from 'multer';\nimport { extname } from 'path';\n\n@Controller('upload')\nexport class UploadController {\n\n  \u002F\u002F 单文件上传\n  @Post('single')\n  @UseInterceptors(\n    FileInterceptor('file', {\n      storage: diskStorage({\n        destination: '.\u002Fuploads',\n        filename: (req, file, callback) => {\n          \u002F\u002F 自定义文件名：时间戳 + 随机数 + 原始扩展名\n          const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;\n          callback(null, `${uniqueName}${extname(file.originalname)}`);\n        },\n      }),\n    }),\n  )\n  uploadSingle(\n    @UploadedFile(\n      new ParseFilePipe({\n        validators: [\n          new MaxFileSizeValidator({ maxSize: 5 * 1024 * 1024 }), \u002F\u002F 5MB\n          new FileTypeValidator({ fileType: \u002Fimage\\\u002F(png|jpeg|jpg|gif)\u002F }),\n        ],\n      }),\n    )\n    file: Express.Multer.File,\n  ) {\n    return {\n      originalname: file.originalname,\n      filename: file.filename,\n      path: file.path,\n      size: file.size,\n    };\n  }\n\n  \u002F\u002F 多文件上传\n  @Post('multiple')\n  @UseInterceptors(FilesInterceptor('files', 10)) \u002F\u002F 最多10个文件\n  uploadMultiple(@UploadedFiles() files: Express.Multer.File[]) {\n    return files.map((file) => ({\n      originalname: file.originalname,\n      filename: file.filename,\n      size: file.size,\n    }));\n  }\n}\n```\n\n### 16.3 静态文件托管\n\n```typescript\n\u002F\u002F src\u002Fmain.ts\nimport { NestFactory } from '@nestjs\u002Fcore';\nimport { NestExpressApplication } from '@nestjs\u002Fplatform-express';\nimport { join } from 'path';\n\nasync function bootstrap() {\n  const app = await NestFactory.create\u003CNestExpressApplication>(AppModule);\n  \n  \u002F\u002F 静态文件托管，访问 http:\u002F\u002Flocalhost:3000\u002Fuploads\u002Fxxx.jpg\n  app.useStaticAssets(join(__dirname, '..', 'uploads'), {\n    prefix: '\u002Fuploads',\n  });\n  \n  await app.listen(3000);\n}\n```\n\n***\n\n## 17. Swagger 接口文档\n\n### 17.1 安装依赖\n\n```bash\nnpm i @nestjs\u002Fswagger\n```\n\n### 17.2 配置 Swagger\n\n```typescript\n\u002F\u002F src\u002Fmain.ts\nimport { SwaggerModule, DocumentBuilder } from '@nestjs\u002Fswagger';\n\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n\n  \u002F\u002F Swagger 配置\n  const config = new DocumentBuilder()\n    .setTitle('我的API文档')\n    .setDescription('NestJS 项目接口文档')\n    .setVersion('1.0')\n    .addBearerAuth() \u002F\u002F 添加 JWT 认证\n    .build();\n\n  const document = SwaggerModule.createDocument(app, config);\n  SwaggerModule.setup('api-docs', app, document); \u002F\u002F 访问 \u002Fapi-docs\n\n  await app.listen(3000);\n  console.log('Swagger 文档: http:\u002F\u002Flocalhost:3000\u002Fapi-docs');\n}\n```\n\n### 17.3 使用装饰器标注接口\n\n```typescript\nimport { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiBody } from '@nestjs\u002Fswagger';\n\n@ApiTags('用户管理') \u002F\u002F 分组标签\n@Controller('users')\nexport class UsersController {\n\n  @Get()\n  @ApiOperation({ summary: '获取所有用户', description: '返回用户列表' })\n  @ApiResponse({ status: 200, description: '查询成功' })\n  findAll() {\n    return this.usersService.findAll();\n  }\n\n  @Post()\n  @ApiBearerAuth() \u002F\u002F 标记需要 Token\n  @ApiOperation({ summary: '创建用户' })\n  @ApiBody({ type: CreateUserDto })\n  @ApiResponse({ status: 201, description: '创建成功' })\n  @ApiResponse({ status: 400, description: '参数错误' })\n  create(@Body() dto: CreateUserDto) {\n    return this.usersService.create(dto);\n  }\n}\n```\n\n### 17.4 DTO 中添加 Swagger 描述\n\n```typescript\nimport { ApiProperty, ApiPropertyOptional } from '@nestjs\u002Fswagger';\n\nexport class CreateUserDto {\n  @ApiProperty({ description: '用户名', example: 'zhangsan', minLength: 2 })\n  @IsString()\n  @IsNotEmpty()\n  username: string;\n\n  @ApiProperty({ description: '密码', example: '123456', minLength: 6 })\n  @IsString()\n  @MinLength(6)\n  password: string;\n\n  @ApiPropertyOptional({ description: '邮箱', example: 'zhangsan@example.com' })\n  @IsEmail()\n  @IsOptional()\n  email?: string;\n}\n```\n\n​\t启动后访问 http:\u002F\u002Flocalhost:3000\u002Fapi-docs 即可看到自动生成的接口文档，支持在线测试。\n\n***\n\n## 18. 配置管理\n\n### 18.1 安装依赖\n\n```bash\nnpm i @nestjs\u002Fconfig\n```\n\n### 18.2 环境变量配置\n\n```env\n# .env\nDB_HOST=localhost\nDB_PORT=3306\nDB_USERNAME=root\nDB_PASSWORD=123456\nDB_DATABASE=nest_db\nJWT_SECRET=my-jwt-secret-key\nJWT_EXPIRES_IN=7d\nPORT=3000\n```\n\n### 18.3 配置模块注册\n\n```typescript\n\u002F\u002F src\u002Fapp.module.ts\nimport { Module } from '@nestjs\u002Fcommon';\nimport { ConfigModule, ConfigService } from '@nestjs\u002Fconfig';\nimport { TypeOrmModule } from '@nestjs\u002Ftypeorm';\n\n@Module({\n  imports: [\n    \u002F\u002F 全局配置模块\n    ConfigModule.forRoot({\n      isGlobal: true,      \u002F\u002F 全局可用，无需在每个模块中导入\n      envFilePath: '.env',  \u002F\u002F 指定 .env 文件路径\n    }),\n\n    \u002F\u002F 使用 ConfigService 动态配置数据库\n    TypeOrmModule.forRootAsync({\n      imports: [ConfigModule],\n      inject: [ConfigService],\n      useFactory: (configService: ConfigService) => ({\n        type: 'mysql',\n        host: configService.get('DB_HOST'),\n        port: configService.get\u003Cnumber>('DB_PORT'),\n        username: configService.get('DB_USERNAME'),\n        password: configService.get('DB_PASSWORD'),\n        database: configService.get('DB_DATABASE'),\n        autoLoadEntities: true,\n        synchronize: true,\n      }),\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\n### 18.4 在 Service 中使用配置\n\n```typescript\nimport { Injectable } from '@nestjs\u002Fcommon';\nimport { ConfigService } from '@nestjs\u002Fconfig';\n\n@Injectable()\nexport class AuthService {\n  constructor(private configService: ConfigService) {}\n\n  getJwtSecret(): string {\n    return this.configService.get\u003Cstring>('JWT_SECRET');\n  }\n}\n```\n\n***\n\n## 19. 部署与优化\n\n### 19.1 构建命令\n\n```bash\n# 开发模式（热重载）\nnpm run start:dev\n\n# 调试模式\nnpm run start:debug\n\n# 生产构建\nnpm run build\n\n# 运行生产版本\nnpm run start:prod\n```\n\n### 19.2 生产环境 main.ts 完整配置\n\n```typescript\n\u002F\u002F src\u002Fmain.ts\nimport { NestFactory } from '@nestjs\u002Fcore';\nimport { ValidationPipe } from '@nestjs\u002Fcommon';\nimport { SwaggerModule, DocumentBuilder } from '@nestjs\u002Fswagger';\nimport { AppModule } from '.\u002Fapp.module';\nimport { HttpExceptionFilter } from '.\u002Fcommon\u002Ffilters\u002Fhttp-exception.filter';\nimport { TransformInterceptor } from '.\u002Fcommon\u002Finterceptors\u002Ftransform.interceptor';\n\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n\n  \u002F\u002F 全局路由前缀\n  app.setGlobalPrefix('api');\n\n  \u002F\u002F 跨域\n  app.enableCors();\n\n  \u002F\u002F 全局验证管道\n  app.useGlobalPipes(\n    new ValidationPipe({\n      whitelist: true,\n      forbidNonWhitelisted: true,\n      transform: true,\n    }),\n  );\n\n  \u002F\u002F 全局异常过滤器\n  app.useGlobalFilters(new HttpExceptionFilter());\n\n  \u002F\u002F 全局拦截器（统一响应格式）\n  app.useGlobalInterceptors(new TransformInterceptor());\n\n  \u002F\u002F Swagger 文档（仅开发环境）\n  if (process.env.NODE_ENV !== 'production') {\n    const config = new DocumentBuilder()\n      .setTitle('API 文档')\n      .setVersion('1.0')\n      .addBearerAuth()\n      .build();\n    const document = SwaggerModule.createDocument(app, config);\n    SwaggerModule.setup('api-docs', app, document);\n  }\n\n  const port = process.env.PORT || 3000;\n  await app.listen(port);\n  console.log(`服务器运行在 http:\u002F\u002Flocalhost:${port}`);\n}\nbootstrap();\n```\n\n### 19.3 Docker 部署\n\n```dockerfile\n# Dockerfile\nFROM node:18-alpine\n\nWORKDIR \u002Fapp\n\nCOPY package*.json .\u002F\nRUN npm ci --only=production\n\nCOPY . .\nRUN npm run build\n\nEXPOSE 3000\n\nCMD [\"node\", \"dist\u002Fmain.js\"]\n```\n\n```yaml\n# docker-compose.yml\nversion: '3.8'\nservices:\n  app:\n    build: .\n    ports:\n      - '3000:3000'\n    env_file:\n      - .env\n    depends_on:\n      - mysql\n\n  mysql:\n    image: mysql:8.0\n    environment:\n      MYSQL_ROOT_PASSWORD: 123456\n      MYSQL_DATABASE: nest_db\n    ports:\n      - '3306:3306'\n    volumes:\n      - mysql_data:\u002Fvar\u002Flib\u002Fmysql\n\nvolumes:\n  mysql_data:\n```\n\n### 19.4 PM2 部署\n\n```javascript\n\u002F\u002F ecosystem.config.js\nmodule.exports = {\n  apps: [\n    {\n      name: 'nest-app',\n      script: 'dist\u002Fmain.js',\n      instances: 'max',          \u002F\u002F 多进程\n      exec_mode: 'cluster',      \u002F\u002F 集群模式\n      env_production: {\n        NODE_ENV: 'production',\n        PORT: 3000,\n      },\n    },\n  ],\n};\n```\n\n```bash\n# 启动\npm2 start ecosystem.config.js --env production\n\n# 查看日志\npm2 logs nest-app\n\n# 重启\npm2 restart nest-app\n```\n\n***\n\n## 最佳实践总结\n\n### 项目开发流程\n\n```\n1. nest g resource xxx      → 生成 CRUD 资源\n2. 定义 Entity 实体          → 数据库表结构\n3. 定义 DTO                  → 请求参数验证\n4. 编写 Service 业务逻辑     → 操作数据库\n5. 编写 Controller           → 处理请求路由\n6. 注册到 Module             → 模块化管理\n7. 添加 Swagger 注解         → 自动生成文档\n```\n\n### 常用装饰器速查表\n\n| 装饰器               | 作用                           |\n| -------------------- | ------------------------------ |\n| `@Controller()`      | 标记控制器类                   |\n| `@Injectable()`      | 标记可注入的 Provider          |\n| `@Module()`          | 标记模块                       |\n| `@Get() @Post()` 等  | HTTP 请求方法                  |\n| `@Param()`           | 获取路由参数                   |\n| `@Query()`           | 获取查询参数                   |\n| `@Body()`            | 获取请求体                     |\n| `@Headers()`         | 获取请求头                     |\n| `@UseGuards()`       | 使用守卫                       |\n| `@UseInterceptors()` | 使用拦截器                     |\n| `@UseFilters()`      | 使用异常过滤器                 |\n| `@UsePipes()`        | 使用管道                       |\n| `@SetMetadata()`     | 设置元数据                     |\n\n***\n\n## 学习资源\n\n- [NestJS 官方文档](https:\u002F\u002Fdocs.nestjs.com)\n- [NestJS 中文文档](https:\u002F\u002Fdocs.nestjs.cn)\n- [TypeORM 文档](https:\u002F\u002Ftypeorm.io)\n- [class-validator 文档](https:\u002F\u002Fgithub.com\u002Ftypestack\u002Fclass-validator)\n- [NestJS GitHub](https:\u002F\u002Fgithub.com\u002Fnestjs\u002Fnest)\n\n***\n\n## 学习路线建议\n\n1. **入门阶段**\n   - 掌握 TypeScript 基础和装饰器语法\n   - 理解 Controller \u002F Service \u002F Module 三件套\n   - 学会使用 CLI 快速生成代码\n   - 完成简单的 CRUD 接口\n\n2. **进阶阶段**\n   - 掌握 DTO 验证与管道\n   - 学会中间件、守卫、拦截器、异常过滤器\n   - 集成 TypeORM 数据库操作\n   - 实现 JWT 身份认证\n\n3. **高级阶段**\n   - Swagger 自动文档生成\n   - 配置管理与环境变量\n   - 文件上传与静态资源\n   - 自定义装饰器与高级 DI\n\n4. **实战阶段**\n   - 完整项目开发（用户系统 + 权限管理）\n   - Docker 部署与 PM2 运维\n   - 微服务架构\n   - WebSocket 实时通信\n",null,"2",131,"2026-04-07T16:59:45.709Z","2026-04-07T16:59:45.712Z","2026-05-25T22:05:43.476Z","0",{"id":73,"categoryName":35,"slug":36,"description":37,"sort":30,"isEnable":26,"createTime":38,"updateTime":39,"deleteTime":30},[81],{"articleId":68,"tagId":82,"createTime":76,"tag":83},"4",{"id":82,"tagName":84,"slug":84,"themeColor":85,"description":84,"createTime":86,"updateTime":86,"deleteTime":78},"Nest","#ff00c3","2026-04-07T16:39:43.674Z",{"id":88,"title":89,"slug":90,"coverUrl":15,"summary":15,"content":91,"htmlContent":72,"categoryId":92,"viewCount":93,"likeCount":26,"isTop":30,"isPublish":26,"seoKeywords":15,"seoDescription":15,"publishTime":94,"createTime":95,"updateTime":96,"deleteTime":78,"category":97,"tags":98},"17","Nuxt.js 学习笔记","nuxt学习笔记","# Nuxt.js 学习笔记\n\n## 目录\n\n1. [Nuxt.js 简介](#1-nuxtjs-简介)\n2. [环境搭建](#2-环境搭建)\n3. [项目结构](#3-项目结构)\n4. [路由系统](#4-路由系统)\n5. [视图与布局](#5-视图与布局)\n6. [数据获取](#6-数据获取)\n7. [状态管理](#7-状态管理)\n8. [中间件](#8-中间件)\n9. [插件系统](#9-插件系统)\n10. [模块系统](#10-模块系统)\n11. [部署与优化](#11-部署与优化)\n\n***\n\n## 1. Nuxt.js 简介\n\n### 1.1 什么是 Nuxt.js\n\nNuxt.js 是基于 Vue.js 的通用应用框架，主要用于构建服务端渲染（SSR）应用。\n\n**核心特性：**\n\n- 服务端渲染（SSR）\n- 静态站点生成（SSG）\n- 自动路由生成\n- 代码分割\n- 热模块替换\n- 强大的模块生态系统\n\n### 1.2 渲染模式对比\n\n| 模式     | 说明         | 适用场景      |\n| ------ | ---------- | --------- |\n| SSR    | 服务端渲染，首屏直出 | SEO要求高的应用 |\n| SSG    | 静态站点生成     | 博客、文档站    |\n| CSR    | 客户端渲染      | 后台管理系统    |\n| Hybrid | 混合渲染       | 复杂业务场景    |\n\n***\n\n## 2. 环境搭建\n\n### 2.1 系统要求\n\n- Node.js >= 18.0.0\n- 包管理器：npm \u002F yarn \u002F pnpm\n\n### 2.2 创建项目\n\n```bash\n# 使用 npx\nnpx nuxi@latest init my-nuxt-app\n\n# 或使用 pnpm\npnpm dlx nuxi@latest init my-nuxt-app\n\n# 进入项目目录\ncd my-nuxt-app\n\n# 安装依赖\nnpm install\n\n# 启动开发服务器\nnpm run dev\n```\n\n### 2.3 配置文件\n\n`nuxt.config.ts` 核心配置：\n\n```typescript\nexport default defineNuxtConfig({\n  devtools: { enabled: true },\n  \n  app: {\n    head: {\n      title: 'My Nuxt App',\n      meta: [\n        { name: 'description', content: 'My amazing Nuxt application' }\n      ]\n    }\n  },\n  \n  modules: [\n    '@nuxtjs\u002Ftailwindcss'\n  ],\n  \n  runtimeConfig: {\n    public: {\n      apiBase: process.env.NUXT_PUBLIC_API_BASE\n    }\n  }\n})\n```\n\n***\n\n## 3. 项目结构\n\n### 3.1 目录结构说明\n\n```\nmy-nuxt-app\u002F\n├── .nuxt\u002F              # 构建生成目录（自动生成）\n├── .output\u002F            # 构建输出目录\n├── app.vue             # 根组件\n├── nuxt.config.ts      # Nuxt 配置文件\n├── package.json\n├── pages\u002F              # 页面目录（自动路由）\n├── components\u002F         # 组件目录（自动导入）\n├── composables\u002F        # 组合式函数（自动导入）\n├── layouts\u002F            # 布局组件\n├── plugins\u002F            # 插件目录\n├── middleware\u002F         # 中间件目录\n├── public\u002F             # 静态资源\n├── assets\u002F             # 需要构建的资源\n├── server\u002F             # 服务端代码\n│   ├── api\u002F           # API 路由\n│   ├── routes\u002F        # 服务端路由\n│   └── middleware\u002F    # 服务端中间件\n└── utils\u002F              # 工具函数\n```\n\n### 3.2 自动导入机制\n\nNuxt 自动导入以下目录中的模块：\n\n- `components\u002F` - 组件\n- `composables\u002F` - 组合式函数\n- `utils\u002F` - 工具函数\n- Vue 3 API（ref, computed, watch 等）\n\n***\n\n## 4. 路由系统\n\n### 4.1 文件路由\n\nNuxt 基于文件系统自动生成路由：\n\n```\npages\u002F\n├── index.vue           → \u002F\n├── about.vue           → \u002Fabout\n├── users\u002F\n│   ├── index.vue       → \u002Fusers\n│   └── [id].vue        → \u002Fusers\u002F:id\n└── blog\u002F\n    └── [slug].vue      → \u002Fblog\u002F:slug\n```\n\n### 4.2 动态路由\n\n```vue\n\u003C!-- pages\u002Fusers\u002F[id].vue -->\n\u003Cscript setup lang=\"ts\">\nconst route = useRoute()\nconst userId = route.params.id\n\nconst { data: user } = await useFetch(`\u002Fapi\u002Fusers\u002F${userId}`)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>User: {{ user?.name }}\u003C\u002Fh1>\n    \u003Cp>ID: {{ userId }}\u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n### 4.3 路由导航\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst router = useRouter()\nconst route = useRoute()\n\nfunction navigateToAbout() {\n  router.push('\u002Fabout')\n}\n\nfunction goBack() {\n  router.back()\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cnav>\n    \u003CNuxtLink to=\"\u002F\">首页\u003C\u002FNuxtLink>\n    \u003CNuxtLink to=\"\u002Fabout\">关于\u003C\u002FNuxtLink>\n    \u003CNuxtLink :to=\"`\u002Fusers\u002F${userId}`\">用户详情\u003C\u002FNuxtLink>\n    \n    \u003Cbutton @click=\"navigateToAbout\">跳转关于页\u003C\u002Fbutton>\n    \u003Cbutton @click=\"goBack\">返回\u003C\u002Fbutton>\n  \u003C\u002Fnav>\n\u003C\u002Ftemplate>\n```\n\n### 4.4 嵌套路由\n\n```\npages\u002F\n├── parent.vue\n└── parent\u002F\n    ├── index.vue\n    └── child.vue\n```\n\n```vue\n\u003C!-- pages\u002Fparent.vue -->\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>Parent Page\u003C\u002Fh1>\n    \u003CNuxtPage \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n### 4.5 路由中间件\n\n```typescript\n\u002F\u002F middleware\u002Fauth.ts\nexport default defineNuxtRouteMiddleware((to, from) => {\n  const isAuthenticated = useState('auth')\n  \n  if (!isAuthenticated.value) {\n    return navigateTo('\u002Flogin')\n  }\n})\n```\n\n```vue\n\u003C!-- pages\u002Fdashboard.vue -->\n\u003Cscript setup lang=\"ts\">\ndefinePageMeta({\n  middleware: 'auth'\n})\n\u003C\u002Fscript>\n```\n\n***\n\n## 5. 视图与布局\n\n### 5.1 布局系统\n\n```vue\n\u003C!-- layouts\u002Fdefault.vue -->\n\u003Ctemplate>\n  \u003Cdiv class=\"layout\">\n    \u003Cheader>\n      \u003Cnav>\n        \u003CNuxtLink to=\"\u002F\">首页\u003C\u002FNuxtLink>\n        \u003CNuxtLink to=\"\u002Fabout\">关于\u003C\u002FNuxtLink>\n      \u003C\u002Fnav>\n    \u003C\u002Fheader>\n    \n    \u003Cmain>\n      \u003Cslot \u002F>\n    \u003C\u002Fmain>\n    \n    \u003Cfooter>\n      \u003Cp>&copy; 2024 My App\u003C\u002Fp>\n    \u003C\u002Ffooter>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n```vue\n\u003C!-- layouts\u002Fadmin.vue -->\n\u003Ctemplate>\n  \u003Cdiv class=\"admin-layout\">\n    \u003Caside>侧边栏\u003C\u002Faside>\n    \u003Cmain>\n      \u003Cslot \u002F>\n    \u003C\u002Fmain>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n### 5.2 页面使用布局\n\n```vue\n\u003C!-- pages\u002Fadmin\u002Fdashboard.vue -->\n\u003Cscript setup lang=\"ts\">\ndefinePageMeta({\n  layout: 'admin'\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>Admin Dashboard\u003C\u002Fh1>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n### 5.3 页面元信息\n\n```vue\n\u003Cscript setup lang=\"ts\">\nuseHead({\n  title: '页面标题',\n  meta: [\n    { name: 'description', content: '页面描述' },\n    { property: 'og:title', content: '分享标题' }\n  ]\n})\n\u003C\u002Fscript>\n```\n\n***\n\n## 6. 数据获取\n\n### 6.1 useFetch\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst { data, pending, error, refresh } = await useFetch('\u002Fapi\u002Fusers')\n\nif (error.value) {\n  console.error('获取数据失败:', error.value)\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"pending\">加载中...\u003C\u002Fdiv>\n  \u003Cdiv v-else-if=\"error\">加载失败\u003C\u002Fdiv>\n  \u003Cdiv v-else>\n    \u003Cul>\n      \u003Cli v-for=\"user in data\" :key=\"user.id\">\n        {{ user.name }}\n      \u003C\u002Fli>\n    \u003C\u002Ful>\n    \u003Cbutton @click=\"refresh\">刷新\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n### 6.2 useAsyncData\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst { data } = await useAsyncData('users', () => \n  $fetch('\u002Fapi\u002Fusers')\n)\n\u003C\u002Fscript>\n```\n\n### 6.3 useLazyFetch\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst { data, pending } = await useLazyFetch('\u002Fapi\u002Farticles')\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"pending\">\n    \u003CSkeletonLoader \u002F>\n  \u003C\u002Fdiv>\n  \u003CArticleList v-else :articles=\"data\" \u002F>\n\u003C\u002Ftemplate>\n```\n\n### 6.4 服务端 API\n\n```typescript\n\u002F\u002F server\u002Fapi\u002Fusers.ts\nexport default defineEventHandler(async (event) => {\n  const method = getMethod(event)\n  \n  if (method === 'GET') {\n    const users = await db.users.findMany()\n    return users\n  }\n  \n  if (method === 'POST') {\n    const body = await readBody(event)\n    const user = await db.users.create({\n      data: body\n    })\n    return user\n  }\n})\n```\n\n```typescript\n\u002F\u002F server\u002Fapi\u002Fusers\u002F[id].ts\nexport default defineEventHandler(async (event) => {\n  const id = getRouterParam(event, 'id')\n  const method = getMethod(event)\n  \n  if (method === 'GET') {\n    return await db.users.findUnique({ where: { id } })\n  }\n  \n  if (method === 'DELETE') {\n    return await db.users.delete({ where: { id } })\n  }\n})\n```\n\n***\n\n## 7. 状态管理\n\n### 7.1 useState\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst counter = useState('counter', () => 0)\n\nfunction increment() {\n  counter.value++\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cp>Count: {{ counter }}\u003C\u002Fp>\n    \u003Cbutton @click=\"increment\">+1\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n### 7.2 全局状态管理\n\n```typescript\n\u002F\u002F composables\u002FuseAuth.ts\nexport const useAuth = () => {\n  const user = useState\u003CUser | null>('user', () => null)\n  const isAuthenticated = computed(() => !!user.value)\n  \n  async function login(credentials: LoginCredentials) {\n    const response = await $fetch('\u002Fapi\u002Fauth\u002Flogin', {\n      method: 'POST',\n      body: credentials\n    })\n    user.value = response.user\n  }\n  \n  async function logout() {\n    await $fetch('\u002Fapi\u002Fauth\u002Flogout', { method: 'POST' })\n    user.value = null\n  }\n  \n  return {\n    user,\n    isAuthenticated,\n    login,\n    logout\n  }\n}\n```\n\n### 7.3 Pinia 集成\n\n```bash\nnpm install @pinia\u002Fnuxt pinia\n```\n\n```typescript\n\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: ['@pinia\u002Fnuxt']\n})\n```\n\n```typescript\n\u002F\u002F stores\u002Fuser.ts\nexport const useUserStore = defineStore('user', {\n  state: () => ({\n    user: null as User | null,\n    token: null as string | null\n  }),\n  \n  getters: {\n    isAuthenticated: (state) => !!state.token,\n    userName: (state) => state.user?.name ?? 'Guest'\n  },\n  \n  actions: {\n    async login(credentials: LoginCredentials) {\n      const response = await $fetch('\u002Fapi\u002Fauth\u002Flogin', {\n        method: 'POST',\n        body: credentials\n      })\n      this.user = response.user\n      this.token = response.token\n    },\n    \n    logout() {\n      this.user = null\n      this.token = null\n    }\n  }\n})\n```\n\n***\n\n## 8. 中间件\n\n### 8.1 路由中间件\n\n```typescript\n\u002F\u002F middleware\u002Fauth.ts\nexport default defineNuxtRouteMiddleware((to, from) => {\n  const { isAuthenticated } = useAuth()\n  \n  if (!isAuthenticated.value) {\n    return navigateTo('\u002Flogin')\n  }\n})\n```\n\n```typescript\n\u002F\u002F middleware\u002Fadmin.ts\nexport default defineNuxtRouteMiddleware((to, from) => {\n  const { user } = useAuth()\n  \n  if (user.value?.role !== 'admin') {\n    return navigateTo('\u002Funauthorized')\n  }\n})\n```\n\n### 8.2 全局中间件\n\n```typescript\n\u002F\u002F middleware\u002Fauth.global.ts\nexport default defineNuxtRouteMiddleware((to, from) => {\n  console.log(`导航: ${from.path} → ${to.path}`)\n})\n```\n\n### 8.3 服务端中间件\n\n```typescript\n\u002F\u002F server\u002Fmiddleware\u002Fauth.ts\nexport default defineEventHandler(async (event) => {\n  const url = getRequestURL(event)\n  \n  if (url.pathname.startsWith('\u002Fapi\u002Fadmin')) {\n    const token = getHeader(event, 'authorization')\n    \n    if (!token) {\n      throw createError({\n        statusCode: 401,\n        message: 'Unauthorized'\n      })\n    }\n    \n    event.context.user = await verifyToken(token)\n  }\n})\n```\n\n***\n\n## 9. 插件系统\n\n### 9.1 创建插件\n\n```typescript\n\u002F\u002F plugins\u002Fmy-plugin.ts\nexport default defineNuxtPlugin((nuxtApp) => {\n  return {\n    provide: {\n      myHelper: () => 'Hello from plugin'\n    }\n  }\n})\n```\n\n### 9.2 使用插件\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst { $myHelper } = useNuxtApp()\nconsole.log($myHelper())\n\u003C\u002Fscript>\n```\n\n### 9.3 第三方库插件\n\n```typescript\n\u002F\u002F plugins\u002Faxios.ts\nimport axios from 'axios'\n\nexport default defineNuxtPlugin((nuxtApp) => {\n  const instance = axios.create({\n    baseURL: 'https:\u002F\u002Fapi.example.com'\n  })\n  \n  instance.interceptors.request.use((config) => {\n    const token = useAuth().token.value\n    if (token) {\n      config.headers.Authorization = `Bearer ${token}`\n    }\n    return config\n  })\n  \n  return {\n    provide: {\n      axios: instance\n    }\n  }\n})\n```\n\n### 9.4 仅客户端\u002F服务端插件\n\n```typescript\n\u002F\u002F plugins\u002Fclient-only.client.ts\nexport default defineNuxtPlugin(() => {\n  console.log('仅在客户端执行')\n})\n```\n\n```typescript\n\u002F\u002F plugins\u002Fserver-only.server.ts\nexport default defineNuxtPlugin(() => {\n  console.log('仅在服务端执行')\n})\n```\n\n***\n\n## 10. 模块系统\n\n### 10.1 常用模块\n\n```typescript\n\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: [\n    '@nuxtjs\u002Ftailwindcss',    \u002F\u002F Tailwind CSS\n    '@nuxtjs\u002Fcolor-mode',      \u002F\u002F 暗色模式\n    '@nuxt\u002Fimage',             \u002F\u002F 图片优化\n    '@nuxtjs\u002Fgoogle-fonts',    \u002F\u002F Google Fonts\n    '@pinia\u002Fnuxt',             \u002F\u002F Pinia\n    '@nuxt\u002Feslint',            \u002F\u002F ESLint\n  ]\n})\n```\n\n### 10.2 自定义模块\n\n```typescript\n\u002F\u002F modules\u002Fmy-module.ts\nimport { defineNuxtModule, addComponent } from '@nuxt\u002Fkit'\n\nexport default defineNuxtModule({\n  meta: {\n    name: 'my-module',\n    configKey: 'myModule'\n  },\n  \n  defaults: {\n    enabled: true\n  },\n  \n  setup(options, nuxt) {\n    if (!options.enabled) return\n    \n    addComponent({\n      name: 'MyComponent',\n      filePath: resolve('.\u002Fruntime\u002Fcomponents\u002FMyComponent.vue')\n    })\n  }\n})\n```\n\n***\n\n## 11. 部署与优化\n\n### 11.1 构建命令\n\n```bash\n# 开发模式\nnpm run dev\n\n# 构建生产版本\nnpm run build\n\n# 预览生产版本\nnpm run preview\n\n# 生成静态站点\nnpm run generate\n```\n\n### 11.2 环境变量\n\n```env\n# .env\nNUXT_PUBLIC_API_BASE=https:\u002F\u002Fapi.example.com\nNUXT_SECRET_KEY=your-secret-key\n```\n\n```typescript\n\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  runtimeConfig: {\n    secretKey: process.env.NUXT_SECRET_KEY,\n    public: {\n      apiBase: process.env.NUXT_PUBLIC_API_BASE\n    }\n  }\n})\n```\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst config = useRuntimeConfig()\nconst apiBase = config.public.apiBase\n\u003C\u002Fscript>\n```\n\n### 11.3 性能优化\n\n```typescript\n\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  app: {\n    head: {\n      link: [\n        { rel: 'preconnect', href: 'https:\u002F\u002Ffonts.googleapis.com' }\n      ]\n    }\n  },\n  \n  nitro: {\n    compressPublicAssets: true\n  },\n  \n  vite: {\n    build: {\n      chunkSizeWarningLimit: 1000\n    }\n  }\n})\n```\n\n### 11.4 部署配置\n\n**Docker 部署：**\n\n```dockerfile\nFROM node:18-alpine\n\nWORKDIR \u002Fapp\n\nCOPY package*.json .\u002F\nRUN npm ci\n\nCOPY . .\nRUN npm run build\n\nEXPOSE 3000\n\nCMD [\"node\", \".output\u002Fserver\u002Findex.mjs\"]\n```\n\n**PM2 部署：**\n\n```javascript\n\u002F\u002F ecosystem.config.js\nmodule.exports = {\n  apps: [{\n    name: 'nuxt-app',\n    script: '.output\u002Fserver\u002Findex.mjs',\n    instances: 'max',\n    exec_mode: 'cluster'\n  }]\n}\n```\n\n***\n\n## 最佳实践\n\n### 代码组织\n\n```\ncomposables\u002F\n├── useAuth.ts\n├── useApi.ts\n└── useUtils.ts\n\nutils\u002F\n├── format.ts\n├── validate.ts\n└── constants.ts\n\ntypes\u002F\n├── user.ts\n├── api.ts\n└── index.ts\n```\n\n### TypeScript 支持\n\n```typescript\n\u002F\u002F types\u002Fuser.ts\nexport interface User {\n  id: string\n  name: string\n  email: string\n  role: 'admin' | 'user'\n}\n\nexport interface LoginCredentials {\n  email: string\n  password: string\n}\n```\n\n### 错误处理\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst { data, error } = await useFetch('\u002Fapi\u002Fusers')\n\nif (error.value) {\n  throw createError({\n    statusCode: 404,\n    message: '用户不存在'\n  })\n}\n\u003C\u002Fscript>\n```\n\n***\n\n## 学习资源\n\n- [Nuxt 官方文档](https:\u002F\u002Fnuxt.com)\n- [Vue 3 文档](https:\u002F\u002Fvuejs.org)\n- [Nuxt Modules](https:\u002F\u002Fnuxt.com\u002Fmodules)\n- [Nuxt GitHub](https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fnuxt)\n\n***\n\n## 学习路线建议\n\n1. **入门阶段**\n   - 掌握 Vue 3 基础\n   - 理解 Nuxt 项目结构\n   - 熟悉自动导入机制\n   - 掌握路由系统\n2. **进阶阶段**\n   - 数据获取与状态管理\n   - 中间件与插件开发\n   - 服务端 API 开发\n   - 布局系统\n3. **高级阶段**\n   - 模块开发\n   - 性能优化\n   - 部署与运维\n   - TypeScript 深度集成\n4. **实战阶段**\n   - 完整项目开发\n   - SEO 优化\n   - 安全加固\n   - CI\u002FCD 集成\n\n","1",111,"2026-04-07T16:59:02.887Z","2026-04-07T16:59:02.889Z","2026-05-25T22:05:43.795Z",{"id":92,"categoryName":27,"slug":28,"description":29,"sort":30,"isEnable":26,"createTime":31,"updateTime":32,"deleteTime":30},[],{"id":100,"title":101,"slug":102,"coverUrl":15,"summary":15,"content":103,"htmlContent":72,"categoryId":104,"viewCount":105,"likeCount":26,"isTop":30,"isPublish":26,"seoKeywords":15,"seoDescription":15,"publishTime":106,"createTime":107,"updateTime":108,"deleteTime":78,"category":109,"tags":110},"16","Git提交到Gitee学习笔记","git提交到gitee学习笔记","# Git提交到Gitee学习笔记\n\n## 一、准备工作\n\n### 1. 安装Git\n- 下载地址：https:\u002F\u002Fgit-scm.com\u002Fdownloads\n- 安装后验证：`git --version`\n\n### 2. 配置Git用户信息\n```bash\ngit config --global user.name \"你的用户名\"\ngit config --global user.email \"你的邮箱\"\n```\n\n### 3. 查看配置信息\n```bash\ngit config --global --list\n```\n\n## 二、Gitee仓库创建\n\n### 1. 登录Gitee\n- 访问：https:\u002F\u002Fgitee.com\n- 注册\u002F登录账号\n\n### 2. 创建新仓库\n- 点击右上角 \"+\" 按钮\n- 选择 \"新建仓库\"\n- 填写仓库名称（如：my-project）\n- 选择仓库类型（公开\u002F私有）\n- 初始化仓库（可选：添加README、.gitignore、开源许可证）\n- 点击 \"创建\"\n\n### 3. 获取仓库地址\n- 进入仓库页面\n- 点击 \"克隆\u002F下载\"\n- 复制HTTPS或SSH地址（推荐HTTPS）\n\n## 三、本地仓库操作\n\n### 1. 初始化本地仓库\n```bash\n# 进入项目目录\ncd your-project-folder\n\n# 初始化Git仓库\ngit init\n```\n\n### 2. 添加文件到暂存区\n```bash\n# 添加所有文件\ngit add .\n\n# 添加指定文件\ngit add filename.txt\n\n# 添加多个文件\ngit add file1.txt file2.txt\n```\n\n### 3. 提交到本地仓库\n```bash\n# 提交并添加说明\ngit commit -m \"第一次提交\"\n\n# 提交并添加详细说明\ngit commit -m \"feat: 添加新功能\"\n```\n\n## 四、连接Gitee远程仓库\n\n### 1. 添加远程仓库\n```bash\n# 添加远程仓库（origin是远程仓库的别名）\ngit remote add origin https:\u002F\u002Fgitee.com\u002F用户名\u002F仓库名.git\n\n# 例如：\ngit remote add origin https:\u002F\u002Fgitee.com\u002Fzhangsan\u002Fmy-project.git\n```\n\n### 2. 查看远程仓库\n```bash\n# 查看远程仓库列表\ngit remote -v\n```\n\n### 3. 修改远程仓库地址\n```bash\n# 修改远程仓库地址\ngit remote set-url origin https:\u002F\u002Fgitee.com\u002F用户名\u002F新仓库名.git\n```\n\n### 4. 删除远程仓库\n```bash\n# 删除远程仓库\ngit remote remove origin\n```\n\n## 五、推送到Gitee\n\n### 1. 首次推送（建立关联）\n```bash\n# 推送到main分支并建立关联\ngit push -u origin main\n\n# 如果是master分支\ngit push -u origin master\n```\n\n### 2. 后续推送\n```bash\n# 推送到当前分支\ngit push\n\n# 推送到指定分支\ngit push origin main\n\n# 强制推送（慎用）\ngit push -f origin main\n```\n\n## 六、从Gitee拉取更新\n\n### 1. 拉取最新代码\n```bash\n# 拉取并合并\ngit pull\n\n# 拉取指定分支\ngit pull origin main\n\n# 查看远程更新但不合并\ngit fetch\n```\n\n### 2. 克隆远程仓库\n```bash\n# 克隆整个仓库\ngit clone https:\u002F\u002Fgitee.com\u002F用户名\u002F仓库名.git\n\n# 克隆到指定目录\ngit clone https:\u002F\u002Fgitee.com\u002F用户名\u002F仓库名.git my-folder\n\n# 克隆指定分支\ngit clone -b 分支名 https:\u002F\u002Fgitee.com\u002F用户名\u002F仓库名.git\n```\n\n## 七、常用Git命令\n\n### 1. 查看状态\n```bash\n# 查看当前状态\ngit status\n\n# 查看提交历史\ngit log\n\n# 查看简洁的提交历史\ngit log --oneline\n\n# 查看分支\ngit branch\n\n# 查看所有分支（包括远程）\ngit branch -a\n```\n\n### 2. 分支操作\n```bash\n# 创建新分支\ngit branch feature-branch\n\n# 切换分支\ngit checkout feature-branch\n\n# 创建并切换分支\ngit checkout -b feature-branch\n\n# 合并分支\ngit merge feature-branch\n\n# 删除分支\ngit branch -d feature-branch\n\n# 删除远程分支\ngit push origin --delete feature-branch\n```\n\n### 3. 撤销操作\n```bash\n# 撤销工作区修改\ngit restore filename.txt\n\n# 撤销暂存区修改\ngit restore --staged filename.txt\n\n# 回退到上一个版本\ngit reset --hard HEAD~1\n\n# 回退到指定版本\ngit reset --hard commit-id\n```\n\n### 4. 查看差异\n```bash\n# 查看工作区与暂存区的差异\ngit diff\n\n# 查看暂存区与上一次提交的差异\ngit diff --cached\n\n# 查看两个版本的差异\ngit diff commit1 commit2\n```\n\n## 八、Gitee SSH配置（可选）\n\n### 1. 生成SSH密钥\n```bash\n# 生成SSH密钥\nssh-keygen -t rsa -C \"你的邮箱\"\n\n# 一路回车，使用默认设置\n```\n\n### 2. 查看公钥\n```bash\n# Windows\ncat ~\u002F.ssh\u002Fid_rsa.pub\n\n# 或直接打开文件\ntype %USERPROFILE%\\.ssh\\id_rsa.pub\n```\n\n### 3. 添加公钥到Gitee\n- 登录Gitee\n- 点击右上角头像 -> 设置\n- 选择 \"SSH公钥\"\n- 点击 \"添加公钥\"\n- 粘贴公钥内容\n- 点击 \"确定\"\n\n### 4. 测试SSH连接\n```bash\nssh -T git@gitee.com\n```\n\n### 5. 使用SSH地址\n```bash\n# 添加远程仓库（SSH方式）\ngit remote add origin git@gitee.com:用户名\u002F仓库名.git\n```\n\n## 九、常见问题解决\n\n### 1. 推送失败：远程仓库有更新\n```bash\n# 先拉取远程更新\ngit pull --rebase origin main\n\n# 再推送\ngit push origin main\n```\n\n### 2. 冲突解决\n```bash\n# 拉取时出现冲突\ngit pull origin main\n\n# 手动编辑冲突文件\n# 解决冲突后\n\ngit add .\ngit commit -m \"解决冲突\"\ngit push origin main\n```\n\n### 3. 忽略文件\n创建 `.gitignore` 文件：\n```\n# Node modules 依赖包\nnode_modules\u002F\n\n# 编译输出目录\ndist\u002F\nbuild\u002F\n\n# 本地环境配置文件\n.env\n.env.local\n.env.*.local\n\n# 日志文件\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# IDE 和编辑器相关文件\n.vscode\u002F\n.idea\u002F\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n\n# 操作系统文件\n.DS_Store\nThumbs.db\n\n# 测试覆盖率报告\ncoverage\u002F\n```\n\n### 4. 查看远程仓库信息\n```bash\n# 查看远程仓库详细信息\ngit remote show origin\n```\n\n## 十、常见使用场景详解\n\n### 场景一：将新文件用Git管理并提交到Gitee\n\n**适用情况**：你有一个本地项目或新文件，想要用Git管理并提交到Gitee远程仓库\n\n#### 步骤详解：\n\n```bash\n# 1. 进入你的项目目录\ncd your-project-folder\n\n# 2. 初始化Git仓库（如果还没有初始化）\ngit init\n\n# 3. 查看当前状态，确认哪些文件未被追踪\ngit status\n\n# 4. 添加文件到暂存区\n# 方式1：添加所有文件\ngit add .\n\n# 方式2：添加指定文件\ngit add filename.txt\n\n# 方式3：添加多个文件\ngit add file1.txt file2.txt file3.txt\n\n# 5. 再次查看状态，确认文件已添加到暂存区\ngit status\n\n# 6. 提交到本地仓库\ngit commit -m \"feat: 初始化项目，添加新文件\"\n\n# 7. 连接Gitee远程仓库（如果还没有连接）\ngit remote add origin https:\u002F\u002Fgitee.com\u002F用户名\u002F仓库名.git\n\n# 8. 查看远程仓库连接\ngit remote -v\n\n# 9. 首次推送到Gitee（-u参数建立本地分支与远程分支的关联）\ngit push -u origin main\n\n# 如果遇到错误，可能是远程仓库已有内容，可以尝试：\n# git pull --rebase origin main\n# git push -u origin main\n```\n\n#### 完整示例（从零开始）：\n\n```bash\n# 假设你有一个新项目 my-project\ncd C:\\Users\\YourName\\Desktop\\my-project\n\n# 初始化Git\ngit init\n\n# 创建一些文件（示例）\necho \"console.log('Hello World')\" > app.js\necho \"# My Project\" > README.md\n\n# 添加所有文件\ngit add .\n\n# 提交\ngit commit -m \"feat: 初始化项目\"\n\n# 连接Gitee远程仓库\ngit remote add origin https:\u002F\u002Fgitee.com\u002Fzhangsan\u002Fmy-project.git\n\n# 推送到Gitee\ngit push -u origin main\n```\n\n#### 注意事项：\n\n- **首次推送时**：如果Gitee仓库已经存在（比如创建了README），需要先 `git pull` 再 `git push`\n- **分支名称**：新版本Git默认使用 `main` 分支，旧版本可能使用 `master`，用 `git branch` 查看\n- **忽略文件**：如果有不需要提交的文件，先创建 `.gitignore` 文件\n\n---\n\n### 场景二：已提交到Gitee，后续更新文件并同步\n\n**适用情况**：项目已经用Git管理并提交到Gitee，现在修改了文件需要同步更新\n\n#### 步骤详解：\n\n```bash\n# 1. 查看当前状态，了解哪些文件被修改\ngit status\n\n# 2. 查看具体修改内容\ngit diff\n\n# 3. 添加修改的文件到暂存区\n# 方式1：添加所有修改的文件\ngit add .\n\n# 方式2：添加指定修改的文件\ngit add modified-file.js\n\n# 方式3：交互式添加（可以选择性地添加某些修改）\ngit add -i\n\n# 4. 查看状态，确认文件已添加到暂存区\ngit status\n\n# 5. 提交修改到本地仓库\ngit commit -m \"feat: 更新功能描述\"\n\n# 或者使用更详细的提交信息\ngit commit -m \"fix: 修复登录页面的bug\n- 修复了用户名验证逻辑\n- 优化了错误提示信息\"\n\n# 6. 推送到Gitee远程仓库\ngit push\n\n# 或者指定分支推送\ngit push origin main\n```\n\n#### 完整示例（日常更新流程）：\n\n```bash\n# 1. 进入项目目录\ncd C:\\Users\\YourName\\Desktop\\my-project\n\n# 2. 先拉取远程最新代码（多人协作时很重要）\ngit pull origin main\n\n# 3. 修改文件（使用编辑器修改代码）\n# ... 编辑文件 ...\n\n# 4. 查看修改内容\ngit status\ngit diff\n\n# 5. 添加修改的文件\ngit add .\n\n# 6. 提交修改\ngit commit -m \"feat: 添加用户注册功能\"\n\n# 7. 推送到Gitee\ngit push origin main\n```\n\n#### 常见更新场景：\n\n##### 场景A：修改单个文件\n```bash\n# 修改了 app.js 文件\ngit add app.js\ngit commit -m \"fix: 修复app.js中的bug\"\ngit push\n```\n\n##### 场景B：修改多个文件\n```bash\n# 修改了多个文件\ngit add .\ngit commit -m \"feat: 添加新功能模块\"\ngit push\n```\n\n##### 场景C：删除文件\n```bash\n# 删除了 old-file.js\ngit add old-file.js\ngit commit -m \"chore: 删除废弃文件\"\ngit push\n\n# 或者使用 git rm 命令\ngit rm old-file.js\ngit commit -m \"chore: 删除废弃文件\"\ngit push\n```\n\n##### 场景D：重命名或移动文件\n```bash\n# 重命名文件\ngit mv old-name.js new-name.js\ngit commit -m \"refactor: 重命名文件\"\ngit push\n```\n\n#### 注意事项：\n\n- **多人协作**：推送前先 `git pull` 拉取最新代码，避免冲突\n- **冲突处理**：如果 `git pull` 时出现冲突，手动解决冲突后再提交\n- **查看历史**：使用 `git log --oneline` 查看提交历史\n- **撤销修改**：如果修改错了，可以用 `git restore filename.txt` 撤销\n\n#### 快捷操作（一步完成添加和提交）：\n\n```bash\n# 添加所有修改并提交（不包括未追踪的新文件）\ngit commit -am \"feat: 快速提交修改\"\n\n# 注意：-a 参数只对已追踪的文件有效，新文件仍需先 git add\n```\n\n---\n\n## 十一、完整工作流程示例\n\n### 首次提交项目到Gitee\n```bash\n# 1. 初始化本地仓库\ngit init\n\n# 2. 添加所有文件\ngit add .\n\n# 3. 提交到本地仓库\ngit commit -m \"初始化项目\"\n\n# 4. 添加远程仓库\ngit remote add origin https:\u002F\u002Fgitee.com\u002F用户名\u002F仓库名.git\n\n# 5. 推送到Gitee\ngit push -u origin main\n```\n\n### 日常开发流程\n```bash\n# 1. 拉取最新代码\ngit pull origin main\n\n# 2. 创建新分支\ngit checkout -b feature\u002Fnew-feature\n\n# 3. 修改代码\n# ... 编辑文件 ...\n\n# 4. 查看状态\ngit status\n\n# 5. 添加修改的文件\ngit add .\n\n# 6. 提交修改\ngit commit -m \"feat: 添加新功能\"\n\n# 7. 切换回主分支\ngit checkout main\n\n# 8. 合并分支\ngit merge feature\u002Fnew-feature\n\n# 9. 推送到远程\ngit push origin main\n\n# 10. 删除本地分支\ngit branch -d feature\u002Fnew-feature\n```\n\n## 十二、提交信息规范\n\n### 提交信息格式\n```\n\u003Ctype>(\u003Cscope>): \u003Csubject>\n\n\u003Cbody>\n\n\u003Cfooter>\n```\n\n### 常用type类型\n- `feat`: 新功能\n- `fix`: 修复bug\n- `docs`: 文档更新\n- `style`: 代码格式调整\n- `refactor`: 重构代码\n- `test`: 测试相关\n- `chore`: 构建过程或辅助工具的变动\n\n### 示例\n```bash\ngit commit -m \"feat: 添加用户登录功能\"\ngit commit -m \"fix: 修复首页显示bug\"\ngit commit -m \"docs: 更新README文档\"\n```\n\n## 十三、学习资源\n\n- Gitee官方文档：https:\u002F\u002Fgitee.com\u002Fhelp\n- Git官方文档：https:\u002F\u002Fgit-scm.com\u002Fdoc\n- Git教程：https:\u002F\u002Fwww.liaoxuefeng.com\u002Fwiki\u002F896043488029600\n\n---\n\n**提示**：建议多练习这些命令，熟练掌握Git的基本操作对日常开发非常重要！\n","7",112,"2026-04-07T16:50:22.022Z","2026-04-07T16:50:22.025Z","2026-05-25T22:05:44.056Z",{"id":104,"categoryName":63,"slug":63,"description":63,"sort":30,"isEnable":26,"createTime":64,"updateTime":64,"deleteTime":30},[],{"id":112,"title":113,"slug":114,"coverUrl":15,"summary":15,"content":115,"htmlContent":72,"categoryId":21,"viewCount":116,"likeCount":26,"isTop":30,"isPublish":26,"seoKeywords":15,"seoDescription":15,"publishTime":117,"createTime":118,"updateTime":119,"deleteTime":78,"category":120,"tags":121},"15","HBuilderX、微信开发者工具和uniapp学习笔记","hbuilderx和uniapp学习笔记","# HBuilderX、微信开发者工具和uniapp学习笔记\n\n## 目录\n1. [开发环境搭建](#开发环境搭建)\n2. [HBuilderX详解](#hbuilderx详解)\n3. [微信开发者工具详解](#微信开发者工具详解)\n4. [uniapp基础](#uniapp基础)\n5. [uniapp项目开发](#uniapp项目开发)\n6. [uniapp与Vue差异](#uniapp与vue差异)\n7. [跨平台开发实战](#跨平台开发实战)\n8. [性能优化与调试](#性能优化与调试)\n\n---\n\n## 开发环境搭建\n\n### 1. HBuilderX安装\n\n#### 下载安装\n- 官网下载：https:\u002F\u002Fwww.dcloud.io\u002Fhbuilderx.html\n- 选择标准版或App开发版（推荐App开发版）\n- 支持Windows、macOS、Linux\n\n#### 常用插件安装\n1. **uni-app编译插件**（必装）\n   - 运行到浏览器\n   - 运行到小程序模拟器\n   - 运行到手机或模拟器\n\n2. **其他推荐插件**\n   - SCSS\u002FSASS编译\n   - less编译\n   - git插件\n   - 内置浏览器\n\n#### HBuilderX配置\n```json\n\u002F\u002F settings.json\n{\n  \"editor.fontSize\": 14,\n  \"editor.tabSize\": 2,\n  \"editor.wordWrap\": \"on\",\n  \"editor.formatOnSave\": true,\n  \"editor.minimap.enabled\": true\n}\n```\n\n### 2. 微信开发者工具安装\n\n#### 下载安装\n- 官网下载：https:\u002F\u002Fdevelopers.weixin.qq.com\u002Fminiprogram\u002Fdev\u002Fdevtools\u002Fdownload.html\n- 支持Windows、macOS\n\n#### 基本配置\n```json\n\u002F\u002F project.config.json\n{\n  \"miniprogramRoot\": \".\u002Fdist\u002Fdev\u002Fmp-weixin\u002F\",\n  \"cloudfunctionRoot\": \"\",\n  \"setting\": {\n    \"urlCheck\": false,\n    \"es6\": true,\n    \"postcss\": true,\n    \"minified\": true\n  },\n  \"appid\": \"your-appid\",\n  \"projectname\": \"uniapp-project\"\n}\n```\n\n#### 开启服务端口\n- 设置 -> 安全设置 -> 安全 -> 开启服务端口\n- 用于HBuilderX自动打开微信开发者工具\n\n### 3. 环境配置检查\n\n```bash\n# 检查Node.js版本（建议12+）\nnode -v\n\n# 检查npm版本\nnpm -v\n\n# 配置淘宝镜像（可选）\nnpm config set registry https:\u002F\u002Fregistry.npmmirror.com\n```\n\n---\n\n## HBuilderX详解\n\n### 1. 界面介绍\n\n#### 主要区域\n- **左侧项目管理器**：文件树结构\n- **中间编辑区**：代码编辑\n- **右侧工具栏**：运行、发行等\n- **底部控制台**：日志输出\n\n#### 常用快捷键\n```\nCtrl+N      新建文件\nCtrl+S      保存文件\nCtrl+D      选中单词\nCtrl+F      查找\nCtrl+H      替换\nCtrl+G      跳转行\nCtrl+\u002F      注释\nCtrl+Shift+R 格式化代码\nAlt+Shift+F 格式化选中代码\n```\n\n### 2. 项目创建\n\n#### 创建uni-app项目\n1. 文件 -> 新建 -> 项目\n2. 选择uni-app\n3. 选择模板（默认模板、Hello uni-app等）\n4. 填写项目名称和路径\n\n#### 项目结构\n```\nmy-uniapp\u002F\n├── pages\u002F              # 页面文件\n│   ├── index\u002F\n│   │   └── index.vue\n│   └── login\u002F\n│       └── login.vue\n├── static\u002F             # 静态资源\n│   ├── images\u002F\n│   └── fonts\u002F\n├── components\u002F         # 组件\n│   └── my-component\u002F\n│       └── my-component.vue\n├── uni_modules\u002F        # uni-app插件\n├── App.vue             # 应用配置\n├── main.js             # 入口文件\n├── manifest.json       # 应用配置\n├── pages.json          # 页面路由配置\n└── package.json        # 依赖配置\n```\n\n### 3. 运行和调试\n\n#### 运行到浏览器\n- 点击工具栏 -> 运行 -> 运行到浏览器 -> Chrome\n- 支持实时预览\n\n#### 运行到微信开发者工具\n- 点击工具栏 -> 运行 -> 运行到小程序模拟器 -> 微信开发者工具\n- 自动打开微信开发者工具\n\n#### 运行到手机\n- 连接手机，开启USB调试\n- 点击工具栏 -> 运行 -> 运行到手机或模拟器\n\n### 4. 代码提示和智能感知\n\n#### 内置代码块\n- 输入 `uview` 生成视图模板\n- 输入 `uinput` 生成输入框\n- 输入 `uimage` 生成图片\n- 输入 `uapi` 生成API调用模板\n\n#### 自定义代码块\n```json\n\u002F\u002F settings.json -> snippets\n{\n  \"Vue Template\": {\n    \"prefix\": \"vtemp\",\n    \"body\": [\n      \"\u003Ctemplate>\",\n      \"\\t\u003Cview class=\\\"container\\\">\",\n      \"\\t\\t\",\n      \"\\t\u003C\u002Fview>\",\n      \"\u003C\u002Ftemplate>\",\n      \"\",\n      \"\u003Cscript>\",\n      \"export default {\",\n      \"\\tdata() {\",\n      \"\\t\\treturn {\",\n      \"\\t\\t\\t\",\n      \"\\t\\t}\",\n      \"\\t},\",\n      \"\\tonLoad() {\",\n      \"\\t\\t\",\n      \"\\t}\",\n      \"}\",\n      \"\u003C\u002Fscript>\",\n      \"\",\n      \"\u003Cstyle>\",\n      \".container {\",\n      \"\\t\",\n      \"}\",\n      \"\u003C\u002Fstyle>\"\n    ]\n  }\n}\n```\n\n---\n\n## 微信开发者工具详解\n\n### 1. 界面介绍\n\n#### 主要区域\n- **模拟器**：小程序预览\n- **编辑器**：代码编辑（可选）\n- **调试器**：调试工具\n\n#### 调试器功能\n- **Console**：控制台输出\n- **Sources**：源代码调试\n- **Network**：网络请求\n- **Storage**：本地存储\n- **AppData**：页面数据\n- **Wxml**：元素结构\n- **Sensor**：传感器模拟\n\n### 2. 常用功能\n\n#### 真机调试\n- 点击工具栏 -> 真机调试\n- 扫码连接手机\n\n#### 性能监控\n- 工具栏 -> 性能监控\n- 查看FPS、内存使用等\n\n#### 代码上传\n- 工具栏 -> 上传\n- 填写版本号和备注\n\n#### 预览\n- 工具栏 -> 预览\n- 扫码在手机上预览\n\n### 3. 调试技巧\n\n#### Console调试\n```javascript\nconsole.log('普通日志')\nconsole.warn('警告信息')\nconsole.error('错误信息')\nconsole.table(data)  \u002F\u002F 表格形式输出\nconsole.time('timer')\nconsole.timeEnd('timer')\n```\n\n#### 断点调试\n- 在编辑器中点击行号设置断点\n- 使用debugger语句\n```javascript\ndebugger  \u002F\u002F 程序会在此处暂停\n```\n\n#### 网络请求调试\n- Network面板查看所有请求\n- 查看请求头、响应体、时间等\n\n### 4. 常见问题解决\n\n#### 端口占用\n```\n错误：服务端口未开启\n解决：设置 -> 安全设置 -> 开启服务端口\n```\n\n#### 域名白名单\n```\n错误：不在以下request合法域名列表中\n解决：开发阶段可关闭域名校验（详情 -> 本地设置 -> 不校验合法域名）\n```\n\n#### 缓存问题\n```\n解决：工具 -> 清缓存 -> 清除全部缓存\n```\n\n---\n\n## uniapp基础\n\n### 1. 什么是uni-app\n\nuni-app是一个使用Vue.js开发所有前端应用的框架，开发者编写一套代码，可发布到iOS、Android、Web（响应式）、以及各种小程序（微信\u002F支付宝\u002F百度\u002F头条\u002FQQ\u002F钉钉\u002F淘宝）、快应用等多个平台。\n\n### 2. 核心特性\n\n- **跨平台**：一套代码，多端运行\n- **Vue生态**：基于Vue.js，学习成本低\n- **原生体验**：调用原生能力，性能接近原生\n- **丰富组件**：内置大量组件和API\n- **插件市场**：丰富的插件生态\n\n### 3. 支持平台\n\n| 平台 | 支持情况 |\n|------|----------|\n| 微信小程序 | ✓ |\n| 支付宝小程序 | ✓ |\n| 百度小程序 | ✓ |\n| 字节跳动小程序 | ✓ |\n| QQ小程序 | ✓ |\n| H5 | ✓ |\n| Android App | ✓ |\n| iOS App | ✓ |\n\n### 4. 项目配置\n\n#### manifest.json\n```json\n{\n  \"name\": \"uni-app\",\n  \"appid\": \"__UNI__XXXXXX\",\n  \"versionName\": \"1.0.0\",\n  \"versionCode\": \"100\",\n  \"mp-weixin\": {\n    \"appid\": \"wx1234567890abcdef\",\n    \"setting\": {\n      \"urlCheck\": false\n    },\n    \"usingComponents\": true\n  },\n  \"h5\": {\n    \"title\": \"uni-app H5\",\n    \"domain\": \"\"\n  }\n}\n```\n\n#### pages.json\n```json\n{\n  \"pages\": [\n    {\n      \"path\": \"pages\u002Findex\u002Findex\",\n      \"style\": {\n        \"navigationBarTitleText\": \"首页\",\n        \"navigationBarBackgroundColor\": \"#007AFF\",\n        \"navigationBarTextStyle\": \"white\"\n      }\n    },\n    {\n      \"path\": \"pages\u002Flogin\u002Flogin\",\n      \"style\": {\n        \"navigationBarTitleText\": \"登录\"\n      }\n    }\n  ],\n  \"globalStyle\": {\n    \"navigationBarTextStyle\": \"black\",\n    \"navigationBarTitleText\": \"uni-app\",\n    \"navigationBarBackgroundColor\": \"#F8F8F8\",\n    \"backgroundColor\": \"#F8F8F8\"\n  },\n  \"tabBar\": {\n    \"color\": \"#7A7E83\",\n    \"selectedColor\": \"#007AFF\",\n    \"borderStyle\": \"black\",\n    \"backgroundColor\": \"#F8F8F8\",\n    \"list\": [\n      {\n        \"pagePath\": \"pages\u002Findex\u002Findex\",\n        \"iconPath\": \"static\u002Ftab-home.png\",\n        \"selectedIconPath\": \"static\u002Ftab-home-active.png\",\n        \"text\": \"首页\"\n      },\n      {\n        \"pagePath\": \"pages\u002Fuser\u002Fuser\",\n        \"iconPath\": \"static\u002Ftab-user.png\",\n        \"selectedIconPath\": \"static\u002Ftab-user-active.png\",\n        \"text\": \"我的\"\n      }\n    ]\n  }\n}\n```\n\n---\n\n## uniapp项目开发\n\n### 1. 页面开发\n\n#### 基本页面结构\n```vue\n\u003Ctemplate>\n  \u003Cview class=\"container\">\n    \u003Cview class=\"header\">{{ title }}\u003C\u002Fview>\n    \u003Cview class=\"content\">\n      \u003Ctext>{{ message }}\u003C\u002Ftext>\n    \u003C\u002Fview>\n  \u003C\u002Fview>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nexport default {\n  data() {\n    return {\n      title: 'uni-app',\n      message: 'Hello uni-app!'\n    }\n  },\n  onLoad(options) {\n    console.log('页面加载', options)\n  },\n  onShow() {\n    console.log('页面显示')\n  },\n  onReady() {\n    console.log('页面渲染完成')\n  },\n  onHide() {\n    console.log('页面隐藏')\n  },\n  onUnload() {\n    console.log('页面卸载')\n  }\n}\n\u003C\u002Fscript>\n\n\u003Cstyle>\n.container {\n  padding: 20rpx;\n}\n.header {\n  font-size: 36rpx;\n  font-weight: bold;\n}\n\u003C\u002Fstyle>\n```\n\n### 2. 常用组件\n\n#### 视图容器\n```vue\n\u003C!-- view -->\n\u003Cview class=\"box\">容器\u003C\u002Fview>\n\n\u003C!-- scroll-view -->\n\u003Cscroll-view scroll-y style=\"height: 300rpx;\">\n  \u003Cview v-for=\"item in 10\" :key=\"item\">内容 {{ item }}\u003C\u002Fview>\n\u003C\u002Fscroll-view>\n\n\u003C!-- swiper -->\n\u003Cswiper indicator-dots autoplay circular>\n  \u003Cswiper-item v-for=\"(item, index) in bannerList\" :key=\"index\">\n    \u003Cimage :src=\"item\" mode=\"aspectFill\">\u003C\u002Fimage>\n  \u003C\u002Fswiper-item>\n\u003C\u002Fswiper-view>\n\n\u003C!-- rich-text -->\n\u003Crich-text :nodes=\"htmlContent\">\u003C\u002Frich-text>\n```\n\n#### 基础内容\n```vue\n\u003C!-- text -->\n\u003Ctext selectable>可选中的文本\u003C\u002Ftext>\n\n\u003C!-- image -->\n\u003Cimage src=\"\u002Fstatic\u002Flogo.png\" mode=\"aspectFit\">\u003C\u002Fimage>\n\n\u003C!-- icon -->\n\u003Cicon type=\"success\" size=\"20\">\u003C\u002Ficon>\n\n\u003C!-- progress -->\n\u003Cprogress percent=\"20\" show-info stroke-width=\"10\">\u003C\u002Fprogress>\n```\n\n#### 表单组件\n```vue\n\u003C!-- button -->\n\u003Cbutton type=\"primary\" @click=\"handleClick\">主要按钮\u003C\u002Fbutton>\n\n\u003C!-- input -->\n\u003Cinput v-model=\"inputValue\" placeholder=\"请输入内容\" \u002F>\n\n\u003C!-- textarea -->\n\u003Ctextarea v-model=\"textValue\" placeholder=\"多行文本\" \u002F>\n\n\u003C!-- checkbox -->\n\u003Ccheckbox-group @change=\"checkboxChange\">\n  \u003Clabel v-for=\"item in items\" :key=\"item.value\">\n    \u003Ccheckbox :value=\"item.value\" :checked=\"item.checked\" \u002F>\n    {{ item.name }}\n  \u003C\u002Flabel>\n\u003C\u002Fcheckbox-group>\n\n\u003C!-- radio -->\n\u003Cradio-group @change=\"radioChange\">\n  \u003Clabel v-for=\"item in items\" :key=\"item.value\">\n    \u003Cradio :value=\"item.value\" :checked=\"item.checked\" \u002F>\n    {{ item.name }}\n  \u003C\u002Flabel>\n\u003C\u002Fradio-group>\n\n\u003C!-- switch -->\n\u003Cswitch :checked=\"switchValue\" @change=\"switchChange\" \u002F>\n\n\u003C!-- picker -->\n\u003Cpicker mode=\"selector\" :range=\"array\" @change=\"pickerChange\">\n  \u003Cview>当前选择：{{ array[pickerIndex] }}\u003C\u002Fview>\n\u003C\u002Fpicker>\n\n\u003C!-- slider -->\n\u003Cslider :value=\"sliderValue\" @change=\"sliderChange\" \u002F>\n```\n\n#### 导航组件\n```vue\n\u003C!-- navigator -->\n\u003Cnavigator url=\"\u002Fpages\u002Fdetail\u002Fdetail?id=123\" open-type=\"navigate\">\n  跳转到详情页\n\u003C\u002Fnavigator>\n\n\u003Cnavigator url=\"\u002Fpages\u002Fdetail\u002Fdetail\" open-type=\"switchTab\">\n  跳转到TabBar页面\n\u003C\u002Fnavigator>\n```\n\n### 3. 常用API\n\n#### 网络请求\n```javascript\nuni.request({\n  url: 'https:\u002F\u002Fapi.example.com\u002Fdata',\n  method: 'GET',\n  data: {\n    id: 1\n  },\n  header: {\n    'content-type': 'application\u002Fjson'\n  },\n  success: (res) => {\n    console.log(res.data)\n  },\n  fail: (err) => {\n    console.error(err)\n  }\n})\n\n\u002F\u002F Promise风格\nuni.request({\n  url: 'https:\u002F\u002Fapi.example.com\u002Fdata'\n}).then(res => {\n  console.log(res.data)\n}).catch(err => {\n  console.error(err)\n})\n\n\u002F\u002F async\u002Fawait\nasync function fetchData() {\n  try {\n    const res = await uni.request({\n      url: 'https:\u002F\u002Fapi.example.com\u002Fdata'\n    })\n    console.log(res.data)\n  } catch (err) {\n    console.error(err)\n  }\n}\n```\n\n#### 页面跳转\n```javascript\n\u002F\u002F 保留当前页面，跳转到应用内的某个页面\nuni.navigateTo({\n  url: '\u002Fpages\u002Fdetail\u002Fdetail?id=123'\n})\n\n\u002F\u002F 关闭当前页面，跳转到应用内的某个页面\nuni.redirectTo({\n  url: '\u002Fpages\u002Fdetail\u002Fdetail'\n})\n\n\u002F\u002F 跳转到tabBar页面，并关闭其他所有非tabBar页面\nuni.switchTab({\n  url: '\u002Fpages\u002Findex\u002Findex'\n})\n\n\u002F\u002F 关闭所有页面，打开到应用内的某个页面\nuni.reLaunch({\n  url: '\u002Fpages\u002Findex\u002Findex'\n})\n\n\u002F\u002F 返回上一页面\nuni.navigateBack({\n  delta: 1\n})\n```\n\n#### 数据存储\n```javascript\n\u002F\u002F 同步存储\nuni.setStorageSync('key', 'value')\nconst value = uni.getStorageSync('key')\nuni.removeStorageSync('key')\nuni.clearStorageSync()\n\n\u002F\u002F 异步存储\nuni.setStorage({\n  key: 'key',\n  data: 'value',\n  success: () => {\n    console.log('存储成功')\n  }\n})\n\nuni.getStorage({\n  key: 'key',\n  success: (res) => {\n    console.log(res.data)\n  }\n})\n```\n\n#### 提示框\n```javascript\n\u002F\u002F 消息提示\nuni.showToast({\n  title: '操作成功',\n  icon: 'success',\n  duration: 2000\n})\n\n\u002F\u002F 加载提示\nuni.showLoading({\n  title: '加载中...'\n})\n\n\u002F\u002F 隐藏加载\nuni.hideLoading()\n\n\u002F\u002F 模态框\nuni.showModal({\n  title: '提示',\n  content: '确定要删除吗？',\n  success: (res) => {\n    if (res.confirm) {\n      console.log('用户点击确定')\n    } else if (res.cancel) {\n      console.log('用户点击取消')\n    }\n  }\n})\n\n\u002F\u002F 操作菜单\nuni.showActionSheet({\n  itemList: ['选项1', '选项2', '选项3'],\n  success: (res) => {\n    console.log('选中了第' + (res.tapIndex + 1) + '个按钮')\n  }\n})\n```\n\n#### 系统信息\n```javascript\n\u002F\u002F 获取系统信息\nconst systemInfo = uni.getSystemInfoSync()\nconsole.log(systemInfo.platform)      \u002F\u002F 平台\nconsole.log(systemInfo.system)        \u002F\u002F 操作系统\nconsole.log(systemInfo.model)         \u002F\u002F 设备型号\nconsole.log(systemInfo.pixelRatio)    \u002F\u002F 设备像素比\nconsole.log(systemInfo.screenWidth)   \u002F\u002F 屏幕宽度\nconsole.log(systemInfo.screenHeight)  \u002F\u002F 屏幕高度\n\n\u002F\u002F 获取网络状态\nuni.getNetworkType({\n  success: (res) => {\n    console.log(res.networkType)  \u002F\u002F wifi、4g、none等\n  }\n})\n\n\u002F\u002F 监听网络状态变化\nuni.onNetworkStatusChange((res) => {\n  console.log(res.isConnected)\n  console.log(res.networkType)\n})\n```\n\n#### 媒体API\n```javascript\n\u002F\u002F 选择图片\nuni.chooseImage({\n  count: 9,\n  sizeType: ['original', 'compressed'],\n  sourceType: ['album', 'camera'],\n  success: (res) => {\n    console.log(res.tempFilePaths)\n  }\n})\n\n\u002F\u002F 预览图片\nuni.previewImage({\n  urls: ['https:\u002F\u002Fexample.com\u002Fimage1.jpg', 'https:\u002F\u002Fexample.com\u002Fimage2.jpg'],\n  current: 0\n})\n\n\u002F\u002F 上传文件\nuni.uploadFile({\n  url: 'https:\u002F\u002Fapi.example.com\u002Fupload',\n  filePath: tempFilePath,\n  name: 'file',\n  success: (res) => {\n    console.log(res.data)\n  }\n})\n\n\u002F\u002F 下载文件\nuni.downloadFile({\n  url: 'https:\u002F\u002Fexample.com\u002Ffile.pdf',\n  success: (res) => {\n    console.log(res.tempFilePath)\n  }\n})\n```\n\n### 4. 条件编译\n\n#### 平台判断\n```javascript\n\u002F\u002F #ifdef H5\nconsole.log('只在H5平台执行')\n\u002F\u002F #endif\n\n\u002F\u002F #ifndef H5\nconsole.log('不在H5平台执行')\n\u002F\u002F #endif\n\n\u002F\u002F #ifdef MP-WEIXIN\nconsole.log('只在微信小程序执行')\n\u002F\u002F #endif\n\n\u002F\u002F #ifdef APP-PLUS\nconsole.log('只在App执行')\n\u002F\u002F #endif\n```\n\n#### 模板条件编译\n```vue\n\u003Ctemplate>\n  \u003Cview>\n    \u003C!-- #ifdef H5 -->\n    \u003Cview>H5平台显示\u003C\u002Fview>\n    \u003C!-- #endif -->\n    \n    \u003C!-- #ifdef MP-WEIXIN -->\n    \u003Cview>微信小程序显示\u003C\u002Fview>\n    \u003C!-- #endif -->\n  \u003C\u002Fview>\n\u003C\u002Ftemplate>\n```\n\n#### 样式条件编译\n```css\n\u002F* #ifdef H5 *\u002F\n.container {\n  background-color: #f0f0f0;\n}\n\u002F* #endif *\u002F\n\n\u002F* #ifdef MP-WEIXIN *\u002F\n.container {\n  background-color: #ffffff;\n}\n\u002F* #endif *\u002F\n```\n\n---\n\n## uniapp与Vue差异\n\n### 1. 标签差异\n\n### Vue标签\n| Vue标签 | uni-app标签 | 说明 |\n|---------|-------------|------|\n| div | view | 容器 |\n| span | text | 文本 |\n| img | image | 图片 |\n| a | navigator | 导航 |\n| input | input | 输入框 |\n| textarea | textarea | 多行文本 |\n| button | button | 按钮 |\n\n### 不支持的Vue标签\n- header, footer, section, article, aside, nav\n- iframe, canvas, video, audio\n- select, option, optgroup\n\n### 2. 样式差异\n\n#### 单位\n```css\n\u002F* 推荐使用rpx单位 *\u002F\n.container {\n  width: 750rpx;  \u002F* 屏幕宽度 *\u002F\n  height: 100rpx;\n}\n\n\u002F* px也会被转换 *\u002F\n.box {\n  font-size: 16px;  \u002F* 在小程序中会转换为rpx *\u002F\n}\n```\n\n#### 选择器限制\n```css\n\u002F* 不支持通配符 * *\u002F\n\u002F* * { margin: 0; }  不支持 *\u002F\n\n\u002F* 不支持属性选择器 *\u002F\n\u002F* input[type=\"text\"] { }  不支持 *\u002F\n\n\u002F* 支持的选择器 *\u002F\n.class { }\n#id { }\nview { }\nview > text { }\nview + view { }\nview text { }\n```\n\n#### 样式隔离\n```vue\n\u003Cstyle scoped>\n\u002F* scoped样式只作用于当前组件 *\u002F\n.container {\n  padding: 20rpx;\n}\n\u003C\u002Fstyle>\n```\n\n### 3. 生命周期差异\n\n#### 应用生命周期\n```javascript\nexport default {\n  onLaunch(options) {\n    \u002F\u002F 应用启动\n  },\n  onShow(options) {\n    \u002F\u002F 应用显示\n  },\n  onHide() {\n    \u002F\u002F 应用隐藏\n  },\n  onError(msg) {\n    \u002F\u002F 应用错误\n  }\n}\n```\n\n#### 页面生命周期\n```javascript\nexport default {\n  onLoad(options) {\n    \u002F\u002F 页面加载\n  },\n  onShow() {\n    \u002F\u002F 页面显示\n  },\n  onReady() {\n    \u002F\u002F 页面渲染完成\n  },\n  onHide() {\n    \u002F\u002F 页面隐藏\n  },\n  onUnload() {\n    \u002F\u002F 页面卸载\n  },\n  onPullDownRefresh() {\n    \u002F\u002F 下拉刷新\n  },\n  onReachBottom() {\n    \u002F\u002F 触底加载更多\n  },\n  onShareAppMessage() {\n    \u002F\u002F 分享\n  },\n  onPageScroll(e) {\n    \u002F\u002F 页面滚动\n  },\n  onResize(size) {\n    \u002F\u002F 页面尺寸变化\n  }\n}\n```\n\n### 4. 指令差异\n\n#### 不支持的指令\n- v-show（小程序不支持）\n- v-html（使用rich-text组件代替）\n\n#### 自定义指令\n```javascript\n\u002F\u002F uni-app不支持自定义指令\n\u002F\u002F Vue自定义指令不适用\n```\n\n### 5. 事件差异\n\n#### 事件绑定\n```vue\n\u003C!-- 阻止冒泡 -->\n\u003Cview @click.stop=\"handleClick\">点击\u003C\u002Fview>\n\n\u003C!-- 阻止默认行为 -->\n\u003Cview @click.prevent=\"handleClick\">点击\u003C\u002Fview>\n\n\u003C!-- 事件捕获 -->\n\u003Cview @click.capture=\"handleClick\">点击\u003C\u002Fview>\n\n\u003C!-- 只触发一次 -->\n\u003Cview @click.once=\"handleClick\">点击\u003C\u002Fview>\n```\n\n#### 触摸事件\n```vue\n\u003Cview \n  @touchstart=\"touchStart\"\n  @touchmove=\"touchMove\"\n  @touchend=\"touchEnd\"\n  @touchcancel=\"touchCancel\"\n>\n  触摸区域\n\u003C\u002Fview>\n```\n\n---\n\n## 跨平台开发实战\n\n### 1. 项目初始化\n\n#### 创建项目\n1. HBuilderX -> 文件 -> 新建 -> 项目\n2. 选择uni-app\n3. 选择默认模板\n4. 填写项目名称：my-uniapp\n\n#### 安装依赖\n```bash\n# 如果使用npm管理依赖\nnpm install\n```\n\n### 2. 目录结构规划\n\n```\nmy-uniapp\u002F\n├── api\u002F                # API接口\n│   ├── user.js\n│   └── product.js\n├── common\u002F             # 公共资源\n│   ├── constants.js    # 常量\n│   ├── utils.js        # 工具函数\n│   └── mixins.js       # 混入\n├── components\u002F         # 组件\n│   ├── nav-bar\u002F        # 导航栏\n│   └── tab-bar\u002F        # 底部导航\n├── pages\u002F              # 页面\n│   ├── index\u002F\n│   ├── user\u002F\n│   └── product\u002F\n├── static\u002F             # 静态资源\n│   ├── images\u002F\n│   ├── icons\u002F\n│   └── fonts\u002F\n├── store\u002F              # 状态管理\n│   └── index.js\n├── styles\u002F             # 全局样式\n│   └── common.scss\n├── uni_modules\u002F        # uni-app插件\n├── App.vue\n├── main.js\n├── manifest.json\n├── pages.json\n└── package.json\n```\n\n### 3. 网络请求封装\n\n#### request.js\n```javascript\nconst BASE_URL = 'https:\u002F\u002Fapi.example.com'\n\nfunction request(options) {\n  return new Promise((resolve, reject) => {\n    uni.request({\n      url: BASE_URL + options.url,\n      method: options.method || 'GET',\n      data: options.data || {},\n      header: {\n        'content-type': 'application\u002Fjson',\n        'Authorization': uni.getStorageSync('token') || ''\n      },\n      success: (res) => {\n        if (res.statusCode === 200) {\n          resolve(res.data)\n        } else {\n          reject(res)\n        }\n      },\n      fail: (err) => {\n        reject(err)\n      }\n    })\n  })\n}\n\nexport default {\n  get(url, data) {\n    return request({ url, method: 'GET', data })\n  },\n  post(url, data) {\n    return request({ url, method: 'POST', data })\n  },\n  put(url, data) {\n    return request({ url, method: 'PUT', data })\n  },\n  delete(url, data) {\n    return request({ url, method: 'DELETE', data })\n  }\n}\n```\n\n#### 使用示例\n```javascript\nimport http from '@\u002Fcommon\u002Frequest.js'\n\nexport default {\n  data() {\n    return {\n      userList: []\n    }\n  },\n  onLoad() {\n    this.fetchUsers()\n  },\n  methods: {\n    async fetchUsers() {\n      try {\n        const res = await http.get('\u002Fusers')\n        this.userList = res.data\n      } catch (err) {\n        console.error(err)\n        uni.showToast({\n          title: '获取数据失败',\n          icon: 'none'\n        })\n      }\n    }\n  }\n}\n```\n\n### 4. 状态管理\n\n#### 简单状态管理\n```javascript\n\u002F\u002F store\u002Findex.js\nconst store = {\n  state: {\n    userInfo: null,\n    token: ''\n  },\n  mutations: {\n    setUserInfo(state, userInfo) {\n      state.userInfo = userInfo\n      uni.setStorageSync('userInfo', userInfo)\n    },\n    setToken(state, token) {\n      state.token = token\n      uni.setStorageSync('token', token)\n    }\n  },\n  actions: {\n    login({ commit }, credentials) {\n      return http.post('\u002Flogin', credentials).then(res => {\n        commit('setToken', res.token)\n        commit('setUserInfo', res.userInfo)\n      })\n    },\n    logout({ commit }) {\n      commit('setToken', '')\n      commit('setUserInfo', null)\n    }\n  }\n}\n\nexport default store\n```\n\n#### 使用Vuex（推荐）\n```javascript\n\u002F\u002F store\u002Findex.js\nimport Vue from 'vue'\nimport Vuex from 'vuex'\n\nVue.use(Vuex)\n\nexport default new Vuex.Store({\n  state: {\n    userInfo: null,\n    token: ''\n  },\n  mutations: {\n    SET_USER_INFO(state, userInfo) {\n      state.userInfo = userInfo\n    },\n    SET_TOKEN(state, token) {\n      state.token = token\n    }\n  },\n  actions: {\n    async login({ commit }, credentials) {\n      const res = await http.post('\u002Flogin', credentials)\n      commit('SET_TOKEN', res.token)\n      commit('SET_USER_INFO', res.userInfo)\n      return res\n    }\n  },\n  getters: {\n    isLoggedIn: state => !!state.token\n  }\n})\n```\n\n### 5. 组件开发\n\n#### 组件定义\n```vue\n\u003C!-- components\u002Fmy-card\u002Fmy-card.vue -->\n\u003Ctemplate>\n  \u003Cview class=\"my-card\" @click=\"handleClick\">\n    \u003Cimage class=\"card-image\" :src=\"image\" mode=\"aspectFill\">\u003C\u002Fimage>\n    \u003Cview class=\"card-content\">\n      \u003Ctext class=\"card-title\">{{ title }}\u003C\u002Ftext>\n      \u003Ctext class=\"card-desc\">{{ description }}\u003C\u002Ftext>\n    \u003C\u002Fview>\n  \u003C\u002Fview>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nexport default {\n  name: 'MyCard',\n  props: {\n    title: {\n      type: String,\n      default: ''\n    },\n    description: {\n      type: String,\n      default: ''\n    },\n    image: {\n      type: String,\n      default: ''\n    }\n  },\n  methods: {\n    handleClick() {\n      this.$emit('click', this.title)\n    }\n  }\n}\n\u003C\u002Fscript>\n\n\u003Cstyle scoped>\n.my-card {\n  background-color: #fff;\n  border-radius: 12rpx;\n  overflow: hidden;\n  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);\n}\n.card-image {\n  width: 100%;\n  height: 300rpx;\n}\n.card-content {\n  padding: 20rpx;\n}\n.card-title {\n  font-size: 32rpx;\n  font-weight: bold;\n  display:block;\n  margin-bottom: 10rpx;\n}\n.card-desc {\n  font-size: 28rpx;\n  color: #666;\n}\n\u003C\u002Fstyle>\n```\n\n#### 组件使用\n```vue\n\u003Ctemplate>\n  \u003Cview>\n    \u003Cmy-card \n      v-for=\"item in list\" \n      :key=\"item.id\"\n      :title=\"item.title\"\n      :description=\"item.description\"\n      :image=\"item.image\"\n      @click=\"handleCardClick\"\n    \u002F>\n  \u003C\u002Fview>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nimport MyCard from '@\u002Fcomponents\u002Fmy-card\u002Fmy-card.vue'\n\nexport default {\n  components: {\n    MyCard\n  },\n  data() {\n    return {\n      list: []\n    }\n  },\n  methods: {\n    handleCardClick(title) {\n      console.log('点击了卡片：', title)\n    }\n  }\n}\n\u003C\u002Fscript>\n```\n\n### 6. 混入使用\n\n#### 定义混入\n```javascript\n\u002F\u002F common\u002Fmixins.js\nexport const pageMixin = {\n  data() {\n    return {\n      loading: false,\n      page: 1,\n      pageSize: 10,\n      hasMore: true\n    }\n  },\n  methods: {\n    async loadData() {\n      if (this.loading || !this.hasMore) return\n      \n      this.loading = true\n      try {\n        const res = await this.fetchData()\n        this.handleData(res)\n      } catch (err) {\n        console.error(err)\n      } finally {\n        this.loading = false\n      }\n    },\n    onReachBottom() {\n      this.page++\n      this.loadData()\n    }\n  }\n}\n```\n\n#### 使用混入\n```javascript\nimport { pageMixin } from '@\u002Fcommon\u002Fmixins.js'\n\nexport default {\n  mixins: [pageMixin],\n  data() {\n    return {\n      list: []\n    }\n  },\n  methods: {\n    async fetchData() {\n      return http.get('\u002Flist', {\n        page: this.page,\n        pageSize: this.pageSize\n      })\n    },\n    handleData(res) {\n      if (res.data.length \u003C this.pageSize) {\n        this.hasMore = false\n      }\n      this.list = this.list.concat(res.data)\n    }\n  }\n}\n```\n\n---\n\n## 性能优化与调试\n\n### 1. 性能优化\n\n#### 图片优化\n```vue\n\u003C!-- 使用合适的mode -->\n\u003Cimage src=\"logo.png\" mode=\"aspectFit\">\u003C\u002Fimage>\n\n\u003C!-- 懒加载 -->\n\u003Cimage :src=\"item.image\" lazy-load>\u003C\u002Fimage>\n\n\u003C!-- 使用webp格式（支持的平台） -->\n\u003Cimage src=\"image.webp\">\u003C\u002Fimage>\n```\n\n#### 列表优化\n```vue\n\u003C!-- 使用key -->\n\u003Cview v-for=\"item in list\" :key=\"item.id\">\n  {{ item.name }}\n\u003C\u002Fview>\n\n\u003C!-- 长列表使用虚拟滚动 -->\n\u003Crecycle-list :list=\"longList\" :item-height=\"100\">\n  \u003Ctemplate v-slot=\"{ item }\">\n    \u003Cview>{{ item.name }}\u003C\u002Fview>\n  \u003C\u002Ftemplate>\n\u003C\u002Frecycle-list>\n```\n\n#### 数据优化\n```javascript\n\u002F\u002F 避免大对象响应式\n\u002F\u002F 使用Object.freeze冻结不需要响应式的数据\nconst staticData = Object.freeze(largeArray)\n\n\u002F\u002F 合理使用computed\ncomputed: {\n  filteredList() {\n    return this.list.filter(item => item.active)\n  }\n}\n```\n\n#### 代码分包\n```json\n\u002F\u002F pages.json\n{\n  \"pages\": [\n    {\n      \"path\": \"pages\u002Findex\u002Findex\"\n    }\n  ],\n  \"subPackages\": [\n    {\n      \"root\": \"pages\u002Fuser\",\n      \"pages\": [\n        {\n          \"path\": \"profile\u002Fprofile\"\n        },\n        {\n          \"path\": \"settings\u002Fsettings\"\n        }\n      ]\n    }\n  ]\n}\n```\n\n### 2. 调试技巧\n\n#### Console调试\n```javascript\nconsole.log('调试信息')\nconsole.table(data)  \u002F\u002F 表格输出\nconsole.time('timer')\nconsole.timeEnd('timer')\n```\n\n#### 微信开发者工具调试\n- 使用Sources面板打断点\n- 使用AppData查看页面数据\n- 使用Network查看网络请求\n- 使用Performance分析性能\n\n#### vconsole调试\n```javascript\n\u002F\u002F 安HBuilderX插件：vconsole\n\u002F\u002F 在小程序中自动显示调试面板\n```\n\n### 3. 常见问题解决\n\n#### 样式不生效\n```css\n\u002F* 检查选择器是否支持 *\u002F\n\u002F* 检查是否使用了不支持的样式属性 *\u002F\n\u002F* 检查scoped样式是否正确 *\u002F\n```\n\n#### 图片不显示\n```vue\n\u003C!-- 检查图片路径 -->\n\u003C!-- 本地图片放在static目录 -->\n\u003Cimage src=\"\u002Fstatic\u002Flogo.png\">\u003C\u002Fimage>\n\n\u003C!-- 网络图片需要配置域名白名单 -->\n```\n\n#### 跨域问题\n```javascript\n\u002F\u002F H5开发时配置代理\n\u002F\u002F manifest.json -> h5 -> devServer\n{\n  \"h5\": {\n    \"devServer\": {\n      \"proxy\": {\n        \"\u002Fapi\": {\n          \"target\": \"https:\u002F\u002Fapi.example.com\",\n          \"changeOrigin\": true,\n          \"pathRewrite\": {\n            \"^\u002Fapi\": \"\"\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n#### 小程序审核失败\n```\n常见原因：\n1. 域名未配置白名单\n2. 使用了未授权的API\n3. 内容违规\n4. 功能不完整\n\n解决方案：\n1. 登录微信公众平台配置服务器域名\n2. 检查API使用权限\n3. 审核内容合规性\n4. 完善功能流程\n```\n\n---\n\n## 学习资源\n\n### 官方文档\n- uni-app官方文档：https:\u002F\u002Funiapp.dcloud.net.cn\u002F\n- HBuilderX使用教程：https:\u002F\u002Fhx.dcloud.net.cn\u002F\n- 微信小程序文档：https:\u002F\u002Fdevelopers.weixin.qq.com\u002Fminiprogram\u002Fdev\u002Fframework\u002F\n\n### 插件市场\n- uni-app插件市场：https:\u002F\u002Fext.dcloud.net.cn\u002F\n- 常用插件：uView UI、uni-ui、colorUI等\n\n### 社区资源\n- DCloud社区：https:\u002F\u002Fask.dcloud.net.cn\u002F\n- GitHub：https:\u002F\u002Fgithub.com\u002Fdcloudio\u002Funi-app\n\n---\n\n## 最佳实践\n\n### 1. 开发规范\n- 统一使用rpx单位\n- 组件命名使用kebab-case\n- 页面文件放在pages目录\n- 静态资源放在static目录\n- API接口统一封装\n\n### 2. 代码规范\n- 使用ESLint进行代码检查\n- 统一代码风格\n- 添加必要的注释\n- 合理拆分组件\n\n### 3. 版本管理\n- 使用Git进行版本控制\n- 提交前进行代码review\n- 使用语义化版本号\n- 保留重要版本备份\n\n### 4. 测试策略\n- 多平台测试\n- 边界情况测试\n- 性能测试\n- 兼容性测试\n\n---\n\n## 总结\n\nuni-app是一个强大的跨平台开发框架，结合HBuilderX和微信开发者工具，可以高效地开发小程序、H5和App应用。掌握这些工具的使用方法和uni-app的核心概念，将大大提升开发效率。\n\n**学习路径建议：**\n1. 熟悉HBuilderX基本操作\n2. 掌握微信开发者工具调试\n3. 学习uni-app基础语法\n4. 理解uni-app与Vue的差异\n5. 实战项目开发\n6. 性能优化和问题解决\n",104,"2026-04-07T16:38:10.473Z","2026-04-07T16:38:10.477Z","2026-05-25T22:05:44.322Z",{"id":21,"categoryName":55,"slug":55,"description":55,"sort":30,"isEnable":26,"createTime":56,"updateTime":56,"deleteTime":30},[122],{"articleId":112,"tagId":123,"createTime":124,"tag":125},"6","2026-04-07T16:40:32.144Z",{"id":123,"tagName":126,"slug":126,"themeColor":127,"description":126,"createTime":128,"updateTime":128,"deleteTime":78},"Uniapp","#00ff33","2026-04-07T16:40:20.885Z",{"id":130,"title":131,"slug":132,"coverUrl":15,"summary":15,"content":133,"htmlContent":72,"categoryId":92,"viewCount":134,"likeCount":30,"isTop":30,"isPublish":26,"seoKeywords":15,"seoDescription":15,"publishTime":135,"createTime":136,"updateTime":137,"deleteTime":78,"category":138,"tags":139},"14","SASS\u002FSCSS 学习笔记","sass学习笔记","# SASS\u002FSCSS 学习笔记\n\n## 一、SASS 简介\n\n### 1.1 什么是 SASS\n- SASS (Syntactically Awesome Style Sheets) 是一种 CSS 预处理器\n- SCSS (Sassy CSS) 是 SASS 3 引入的新语法，完全兼容 CSS3\n- SASS 提供了变量、嵌套、混合、继承等功能，使 CSS 更易维护\n\n### 1.2 SASS vs SCSS\n```scss\n\u002F\u002F SCSS 语法（推荐，完全兼容 CSS）\n$color: red;\n.container {\n  color: $color;\n}\n\n\u002F\u002F SASS 语法（缩进式，不兼容 CSS）\n$color: red\n.container\n  color: $color\n```\n\n### 1.3 安装\n```bash\n# 使用 npm 安装\nnpm install -g sass\n\n# 使用 Node.js 编译\nsass input.scss output.css\n\n# 监听文件变化\nsass --watch input.scss output.css\n\n# 监听整个目录\nsass --watch scss:css\n```\n\n---\n\n## 二、变量\n\n### 2.1 定义变量\n```scss\n\u002F\u002F 使用 $ 符号定义变量\n$primary-color: #3498db;\n$font-size: 16px;\n$border-radius: 4px;\n```\n\n### 2.2 使用变量\n```scss\n.button {\n  background-color: $primary-color;\n  font-size: $font-size;\n  border-radius: $border-radius;\n}\n```\n\n### 2.3 变量作用域\n```scss\n$global-color: blue;\n\n.container {\n  $local-color: red;\n  color: $local-color;  \u002F\u002F 可以使用局部变量\n  color: $global-color; \u002F\u002F 可以使用全局变量\n}\n\n\u002F\u002F $local-color 在这里不可用\n```\n\n### 2.4 默认值\n```scss\n$color: red !default; \u002F\u002F 如果变量未定义，则使用默认值\n```\n\n---\n\n## 三、嵌套\n\n### 3.1 选择器嵌套\n```scss\n.container {\n  background: white;\n  \n  .header {\n    padding: 20px;\n  }\n  \n  .content {\n    margin: 10px;\n  }\n}\n\n\u002F\u002F 编译后：\n\u002F\u002F .container { background: white; }\n\u002F\u002F .container .header { padding: 20px; }\n\u002F\u002F .container .content { margin: 10px; }\n```\n\n### 3.2 属性嵌套\n```scss\n.button {\n  border: {\n    width: 1px;\n    style: solid;\n    color: #ccc;\n  }\n}\n\n\u002F\u002F 编译后：\n\u002F\u002F .button { border-width: 1px; border-style: solid; border-color: #ccc; }\n```\n\n### 3.3 父选择器引用 (&)\n```scss\n.button {\n  &:hover {\n    background: darken($color, 10%);\n  }\n  \n  &.active {\n    background: $primary-color;\n  }\n}\n\n\u002F\u002F 编译后：\n\u002F\u002F .button:hover { background: ...; }\n\u002F\u002F .button.active { background: ...; }\n```\n\n---\n\n## 四、混合\n\n### 4.1 基本混合\n```scss\n@mixin border-radius($radius) {\n  -webkit-border-radius: $radius;\n  -moz-border-radius: $radius;\n  border-radius: $radius;\n}\n\n.box {\n  @include border-radius(10px);\n}\n```\n\n### 4.2 默认参数\n```scss\n@mixin box-shadow($x: 0, $y: 0, $blur: 5px, $color: rgba(0,0,0,0.3)) {\n  box-shadow: $x $y $blur $color;\n}\n\n.card {\n  @include box-shadow();\n}\n\n.button {\n  @include box-shadow(2px, 2px, 10px, red);\n}\n```\n\n### 4.3 可变参数\n```scss\n@mixin box-shadow($shadows...) {\n  box-shadow: $shadows;\n}\n\n.element {\n  @include box-shadow(0 0 5px #000, 0 0 10px #333);\n}\n```\n\n### 4.4 内容块\n```scss\n@mixin mobile {\n  @media (max-width: 768px) {\n    @content;\n  }\n}\n\n.container {\n  width: 100%;\n  \n  @include mobile {\n    width: 50%;\n  }\n}\n```\n\n---\n\n## 五、继承\n\n### 5.1 基本继承\n```scss\n.message {\n  padding: 10px;\n  border: 1px solid #ccc;\n  border-radius: 4px;\n}\n\n.success {\n  @extend .message;\n  background-color: #dff0d8;\n  border-color: #d6e9c6;\n}\n\n.error {\n  @extend .message;\n  background-color: #f2dede;\n  border-color: #ebccd1;\n}\n```\n\n### 5.2 占位符选择器\n```scss\n\u002F\u002F 使用 % 定义占位符，不会生成独立的 CSS\n%message-base {\n  padding: 10px;\n  border: 1px solid #ccc;\n}\n\n.success {\n  @extend %message-base;\n  background: green;\n}\n\n.error {\n  @extend %message-base;\n  background: red;\n}\n```\n\n---\n\n## 六、运算\n\n### 6.1 数字运算\n```scss\n$width: 100px;\n$padding: 10px;\n\n.container {\n  width: $width + $padding * 2; \u002F\u002F 120px\n}\n```\n\n### 6.2 颜色运算\n```scss\n$color: #369;\n\n.button {\n  background: $color + #111; \u002F\u002F 变亮\n  border: $color - #222;    \u002F\u002F 变暗\n}\n```\n\n### 6.3 字符串运算\n```scss\n$name: \"icon\";\n\n.icon-#{$name} {\n  background: url(\"images\u002F#{$name}.png\");\n}\n```\n\n### 6.4 单位运算\n```scss\n$width: 100px;\n\n.container {\n  width: $width \u002F 2;   \u002F\u002F 50px\n  height: 100% \u002F 2;    \u002F\u002F 50%\n}\n```\n\n---\n\n## 七、函数\n\n### 7.1 内置颜色函数\n```scss\n$color: #3498db;\n\n.button {\n  background: lighten($color, 10%);  \u002F\u002F 变亮\n  border: darken($color, 10%);       \u002F\u002F 变暗\n  color: invert($color);             \u002F\u002F 反色\n  opacity: opacity($color);          \u002F\u002F 透明度\n}\n```\n\n### 7.2 其他内置函数\n```scss\n$number: 10px;\n$string: \"hello\";\n$list: 1, 2, 3, 4;\n\n.container {\n  width: abs(-10px);           \u002F\u002F 绝对值\n  height: round(10.5px);       \u002F\u002F 四舍五入\n  margin: ceil(10.2px);        \u002F\u002F 向上取整\n  padding: floor(10.8px);      \u002F\u002F 向下取整\n  content: length($list);      \u002F\u002F 列表长度\n  content: nth($list, 2);      \u002F\u002F 获取列表第2项\n}\n```\n\n### 7.3 自定义函数\n```scss\n@function double($n) {\n  @return $n * 2;\n}\n\n.container {\n  width: double(100px); \u002F\u002F 200px\n}\n```\n\n---\n\n## 八、条件语句\n\n### 8.1 if 语句\n```scss\n@mixin theme($theme) {\n  @if $theme == 'dark' {\n    background: #333;\n    color: #fff;\n  } @else if $theme == 'light' {\n    background: #fff;\n    color: #333;\n  } @else {\n    background: #ccc;\n    color: #000;\n  }\n}\n\n.container {\n  @include theme('dark');\n}\n```\n\n### 8.2 三元运算符\n```scss\n$width: 100px;\n$max-width: 200px;\n\n.container {\n  width: if($width > $max-width, $max-width, $width);\n}\n```\n\n---\n\n## 九、循环\n\n### 9.1 for 循环\n```scss\n@for $i from 1 through 5 {\n  .col-#{$i} {\n    width: 100% \u002F 5 * $i;\n  }\n}\n\n\u002F\u002F 编译后生成 .col-1, .col-2, .col-3, .col-4, .col-5\n```\n\n### 9.2 each 循环\n```scss\n$colors: (red: #f00, green: #0f0, blue: #00f);\n\n@each $name, $color in $colors {\n  .text-#{$name} {\n    color: $color;\n  }\n}\n\n\u002F\u002F 编译后生成 .text-red, .text-green, .text-blue\n```\n\n### 9.3 while 循环\n```scss\n$i: 6;\n\n@while $i > 0 {\n  .item-#{$i} {\n    width: $i * 10px;\n  }\n  $i: $i - 1;\n}\n```\n\n---\n\n## 十、列表和映射\n\n### 10.1 列表\n```scss\n$fonts: \"Helvetica\", \"Arial\", sans-serif;\n\nbody {\n  font-family: $fonts;\n}\n\n\u002F\u002F 列表函数\n$numbers: 1, 2, 3, 4, 5;\n$first: nth($numbers, 1);      \u002F\u002F 1\n$length: length($numbers);    \u002F\u002F 5\n$last: nth($numbers, -1);     \u002F\u002F 5\n$joined: join($numbers, 6);  \u002F\u002F 1, 2, 3, 4, 5, 6\n```\n\n### 10.2 映射\n```scss\n$colors: (\n  primary: #3498db,\n  secondary: #2ecc71,\n  danger: #e74c3c\n);\n\n.button {\n  background: map-get($colors, primary);\n}\n\n\u002F\u002F 映射函数\n$keys: map-keys($colors);       \u002F\u002F (primary, secondary, danger)\n$values: map-values($colors);   \u002F\u002F (#3498db, #2ecc71, #e74c3c)\n$has-key: map-has-key($colors, primary); \u002F\u002F true\n```\n\n---\n\n## 十一、模块化\n\n### 11.1 使用 @import\n```scss\n\u002F\u002F _variables.scss\n$primary-color: #3498db;\n$font-size: 16px;\n\n\u002F\u002F _mixins.scss\n@mixin button {\n  padding: 10px 20px;\n  border-radius: 4px;\n}\n\n\u002F\u002F main.scss\n@import 'variables';\n@import 'mixins';\n\n.button {\n  @include button;\n  background: $primary-color;\n}\n```\n\n### 11.2 使用 @use（推荐）\n```scss\n\u002F\u002F _variables.scss\n$primary-color: #3498db !default;\n@forward 'mixins';\n\n\u002F\u002F main.scss\n@use 'variables' as *;\n\n.button {\n  background: $primary-color;\n}\n```\n\n### 11.3 使用 @forward\n```scss\n\u002F\u002F _mixins.scss\n@mixin button { ... }\n\n\u002F\u002F _utilities.scss\n@forward 'mixins';\n@forward 'functions';\n\n\u002F\u002F main.scss\n@use 'utilities';\n\n.button {\n  @include utilities.button;\n}\n```\n\n---\n\n## 十二、注释\n\n### 12.1 单行注释\n```scss\n\u002F\u002F 这个注释不会出现在编译后的 CSS 中\n$color: red;\n```\n\n### 12.2 多行注释\n```scss\n\u002F* 这个注释会出现在编译后的 CSS 中 *\u002F\n$color: blue;\n```\n\n### 12.3 强制注释\n```scss\n\u002F*! 这个注释会出现在压缩后的 CSS 中 *\u002F\n$color: green;\n```\n\n---\n\n## 十三、调试\n\n### 13.1 @debug\n```scss\n$width: 100px;\n\n@debug \"Width is: #{$width}\";\n\u002F\u002F 输出：Width is: 100px\n```\n\n### 13.2 @warn\n```scss\n@warn \"This feature is deprecated\";\n\u002F\u002F 输出警告信息\n```\n\n### 13.3 @error\n```scss\n@if $width \u003C 0 {\n  @error \"WidthWidth cannot be negative\";\n}\n\u002F\u002F 抛出错误，停止编译\n```\n\n---\n\n## 十四、最佳实践\n\n### 14.1 文件组织\n```\nstyles\u002F\n├── main.scss          # 主文件\n├── abstracts\u002F         # 抽象层\n│   ├── _variables.scss\n│   ├── _functions.scss\n│   └── _mixins.scss\n├── base\u002F              # 基础样式\n│   ├── _reset.scss\n│   └── _typography.scss\n├── components\u002F        # 组件\n│   ├── _button.scss\n│   └── _card.scss\n├── layout\u002F            # 布局\n│   ├── _header.scss\n│   └── _footer.scss\n└── pages\u002F             # 页面\n    └── _home.scss\n```\n\n### 14.2 命名规范\n```scss\n\u002F\u002F 使用 BEM 命名规范\n.block {\n  &__element {\n    &--modifier {\n      \n    }\n  }\n}\n\n\u002F\u002F 变量使用有意义的名称\n$primary-color: #3498db;\n$spacing-unit: 8px;\n$border-radius: 4px;\n```\n\n### 14.3 性能优化\n```scss\n\u002F\u002F 避免过度嵌套（最多3层）\n\u002F\u002F 不好\n.container {\n  .header {\n    .title {\n      .text {\n        color: red;\n      }\n    }\n  }\n}\n\n\u002F\u002F 好\n.container {\n  .header {\n    .title-text {\n      color: red;\n    }\n  }\n}\n\n\u002F\u002F 使用占位符选择器而不是类继承\n%base-button {\n  padding: 10px;\n}\n\n.primary-button {\n  @extend %base-button;\n  background: blue;\n}\n```\n\n---\n\n## 十五、常用代码片段\n\n### 15.1 响应式断点\n```scss\n$breakpoints: (\n  mobile: 480px,\n  tablet: 768px,\n  desktop: 1024px\n);\n\n@mixin respond-to($breakpoint) {\n  @if map-has-key($breakpoints, $breakpoint) {\n    @media (min-width: map-get($breakpoints, $breakpoint)) {\n      @content;\n    }\n  }\n}\n\n.container {\n  width: 100%;\n  \n  @include respond-to(tablet) {\n    width: 50%;\n  }\n}\n```\n\n### 15.2 三角形\n```scss\n@mixin triangle($size, $color, $direction) {\n  width: 0;\n  height: 0;\n  border-style: solid;\n  \n  @if $direction == 'up' {\n    border-width: 0 $size \u002F 2 $size $size \u002F 2;\n    border-color: transparent transparent $color transparent;\n  } @else if $direction == 'down' {\n    border-width: $size $size \u002F 2 0 $size \u002F 2;\n    border-color: $color transparent transparent transparent;\n  }\n}\n\n.arrow-up {\n  @include triangle(10px, red, up);\n}\n```\n\n### 15.3 文本截断\n```scss\n@mixin text-truncate {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.truncate {\n  @include text-truncate;\n}\n```\n\n### 15.4 居中\n```scss\n@mixin center {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n@mixin absolute-center {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n}\n```\n\n---\n\n## 十六、SASS 与现代 CSS\n\n### 16.1 CSS 变量 vs SASS 变量\n```scss\n\u002F\u002F SASS 变量（编译时）\n$sass-color: red;\n\n\u002F\u002F CSS 变量（运行时）\n:root {\n  --css-color: red;\n}\n\n.element {\n  color: $sass-color;    \u002F\u002F 编译为 color: red;\n  color: var(--css-color); \u002F\u002F 保持为 var(--css-color);\n}\n```\n\n### 16.2 何时使用 SASS\n- 需要复杂的计算和逻辑\n- 需要模块化和代码复用\n- 需要兼容旧浏览器\n- 团队需要统一的样式系统\n\n### 16.3 何时使用原生 CSS\n- 简单项目\n- 不需要预处理器功能\n- 需要运行时动态修改变量\n- 追求更快的编译速度\n\n---\n\n## 总结\n\nSASS\u002FSCSS 提供了强大的功能来增强 CSS：\n- **变量**：便于维护和统一管理\n- **嵌套**：代码结构更清晰\n- **混合**：代码复用\n- **继承**：减少重复代码\n- **函数**：强大的计算能力\n- **模块化**：代码组织更合理\n\n合理使用这些功能，可以大大提高 CSS 的开发效率和可维护性。\n",100,"2026-04-07T16:23:17.106Z","2026-04-07T16:23:17.108Z","2026-05-25T22:05:44.026Z",{"id":92,"categoryName":27,"slug":28,"description":29,"sort":30,"isEnable":26,"createTime":31,"updateTime":32,"deleteTime":30},[],{"id":141,"title":142,"slug":143,"coverUrl":15,"summary":15,"content":144,"htmlContent":72,"categoryId":92,"viewCount":145,"likeCount":30,"isTop":30,"isPublish":26,"seoKeywords":15,"seoDescription":15,"publishTime":146,"createTime":147,"updateTime":148,"deleteTime":78,"category":149,"tags":150},"13","一，项目收获及技术","硅谷甄选项目笔记","\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n# 一，项目收获及技术\n\n## 1.1 收获\n\n* 企业级的编码规范\n\n  ![image-20260129230723775](images\u002Fimage-20260129230723775.png)\n\n* 从零开始，封装一个后台管理系统\n\n  ![image-20260129230803256](images\u002Fimage-20260129230803256.png)\n\n* 菜单权限与按钮权限\n\n  ![image-20260129230823689](images\u002Fimage-20260129230823689.png)\n\n* 数据可视化大屏\n\n  ![image-20260129230843202](images\u002Fimage-20260129230843202.png)\n\n* svg 矢量图在项目中的应用\n\n  ![image-20260129230907605](images\u002Fimage-20260129230907605.png)\n\n* 主题颜色切换与暗黑模式的切换\n\n  ![image-20260129230933375](images\u002Fimage-20260129230933375.png)\n\n\n\n## 1.2 技术选选型\n\nVue3 + 组合式API + Vite构建工具 + element-plus + Axios + Echarts + pinia + TypeScript + vue-router\n\n\n# 二，Vue3 组件通信的艺术：构建灵活可复用的UI\n\n在任何复杂的单页应用中，组件之的通信都是一个核心挑战。高效的组件通信能够显著提高代码的复用性，可维护性以及团队的协作效率\n\n## 2.1 props: 父子组件通信的基石\n\n`props（道具）`是Vue中最直接，最常用的父子组件通信方式。在Vue3中，通过 defineProps 宏，我们可以轻松地接受父组件传递的数据，并且无需显示引入，可以在\u003Cscript setup> 中使用，极大的简化了开发流程\n\n**父组件向子组件传递数据案例** ：\n\n子组件可以通过两种方式接受 `props`\n\n父组件传数据的代码\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"box\">\n  \u003Ch1>父组件\u003C\u002Fh1>\n  \u003CChild info=\"我爱祖国\" :money=\"money\">\u003C\u002FChild>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport {ref} from \"vue\"\nimport Child from \".\u002FChild.vue\";\n\nlet money = ref(1000)\n\u003C\u002Fscript>\n\n```\n\n**方式一：带类型和默认值的声明**\n\n这种方式提供了更强的类型检查和默认值设置，增加了代码的健壮性。\n\n```typescript\n\nlet props = defineProps({\n  info:{\n    type:String,  \u002F\u002F 接受的数据类型\n    defalut:'默认参数' \u002F\u002F 接受默认数据\n  },\n  money:{\n    type:Number,\n    default:0\n\n  }\n})\n\u002F\u002F 在模板中可以直接使用 props.info,props.money   省略直接 info,money 也可以\n```\n\n**方式二：数组形式的简洁声明**\n\n适用于简单的 `props` 声明，但缺乏类型检查\n\n```typescript\nlet props = defineProps(['info','money'])\n```\n\n**重要提示：** `props` 是单向数据流，子组件只能读取 `props` 数据，不能直接修改它。\n\n\n\n## 2.2 自定义事件：子组件向父组件传递数据\n\n自定义事件是实现子组件向父组件传递数据的关键机制。与原生 DOM 事件不同，自定义事件是 Vue 组件特有的通信方式。\n\n**父组件绑定自定义事件：**\n\n父组件在子组件标签上通过 `@` 符号绑定自定义事件。\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"father\">\n  \u003Ch1>父组件\u003C\u002Fh1>\n  \u003CEvent1 @xxx=\"handler2\">\u003C\u002FEvent1>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport Event1 from '.\u002FEvent1.vue'; \n\nconst handler2 = (param1:string,param2:string) =>{\n  console.log('从子组件接收到数据',param1,param2);\n  \n}\n\n\u003C\u002Fscript>\n\n```\n\n**子组件触发自定义事件：**\n\n在子组件内部，使用 `defineEmits` 宏声明需要触发的自定义事件，然后通过 `$emit` 方法触发事件并传递数据。\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"son\">\n  \u003Ch1>我是子组件1\u003C\u002Fh1>\n  \u003Cbutton @click=\"handler\">点我触发事件xxx自定义事件\u003C\u002Fbutton>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\nlet $emit = defineEmits(['xxx']) \u002F\u002F 声明自定义事件 'xxx',\nconst handler =() =>{\n  $emit('xxx','法拉利','茅台')  \u002F\u002F 第一个参数，自定义事件的名称，父组件通过这个来监听这个事件，剩下的参数是子组件传给父组件的数据\n}\n\u003C\u002Fscript>\n\n```\n\n**Vue3 中原生 DOM 事件的特殊性：**\n\n在 Vue3 中，像 `click`、`dbclick`、`change` 这类原生 DOM 事件，无论是在普通 HTML 标签上还是在自定义组件标签上，默认都视为原生 DOM 事件。这与 Vue2 中需要 `native` 修饰符才能将组件上的事件变为原生 DOM 事件有所不同。但如果子组件内部通过 `defineEmits` 定义了同名的事件，那么它将优先被视为自定义事件。\n\n## 2.3 全局事件总线 (mitt)：实现任意组件通信\n\n在 Vue2 中，我们常利用 `Vue.prototype.$bus` 实现全局事件总线。然而，Vue3 没有 Vue 构造函数，且组合式 API 中没有 `this` 上下文，因此传统的全局事件总线方式不再适用。在 Vue3 中，我们可以借助轻量级的第三方库 `mitt` 来实现全局事件总线功能，从而让任意组件之间进行通信。\n\n**mitt 官网：** https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fmitt\n\n使用：\n\n* 下载安装mitt `$ npm install --save mitt`\n* 在 src 中创建文件夹 bus，index.ts\n* 在 index.ts 中引入 mitt\n\n```vue\n\u002F\u002F引入mitt插件:mitt一个方法,方法执行会返回bus对象\nimport mitt from 'mitt';\nconst $bus = mitt();\nexport default $bus;\n\n```\n\n* 送东西的兄弟\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"child2\">\n     \u003Ch2>我是子组件2:曹丕\u003C\u002Fh2>\n     \u003Cbutton @click=\"handler\">点击我给兄弟送一台法拉利\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n    \n\u002F\u002F引入$bus对象\nimport $bus from '..\u002F..\u002Fbus';\n\n\u002F\u002F点击按钮回调\nconst handler = ()=>{\n  $bus.emit('car',{car:\"法拉利\"});  \u002F\u002F 第一个参数事件的名称（类型），告诉另一个兄弟是哪个事件，类似于一个表示这个事件的id\n}\n\u003C\u002Fscript>\n\n```\n\n* 收到东西的兄弟\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"child1\">\n    \u003Ch3>我是子组件1:曹植\u003C\u002Fh3>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n    \nimport $bus from \"..\u002F..\u002Fbus\";\n\u002F\u002F组合式API函数\nimport { onMounted } from \"vue\";\n\u002F\u002F组件挂载完毕的时候,当前组件绑定一个事件,接受将来兄弟组件传递的数据\n\nonMounted(() => {\n  \u002F\u002F第一个参数:即为事件类型  第二个参数:即为事件回调\n  $bus.on(\"car\", (car) => {\n    console.log(car);\n  });\n});\n\u003C\u002Fscript>\n```\n\n\n\n## 2.4 `v-model`：实现父子组件数据的双向绑定(同步数据)\n\n`v-model` 指令不仅用于收集表单数据实现双向绑定，它也是实现父子组件数据同步的强大工具。在底层，`v-model` 实际上是 `props` (`modelValue`) 和自定义事件 (`update:modelValue`) 的语法糖。\n\n`v-model` 实现表单数据收集，数据双向绑定：\n\n```vue\n\u003Ctemplate>\n\u003Cdiv>\n  \u003Ch1>v-model\u003C\u002Fh1>\n  \u003Cinput type=\"text\" v-model=\"info\">\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F v-model指令，收集表单数据，实现双向绑定\n\nimport {ref} from 'vue'\n\nlet info = ref('')\n\n\u003C\u002Fscript>\n\n```\n\n`v-model` 实现父子组件数据的同步：\n\n父组件：\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"fa\">\n  \u003Ch1>v-model 钱数：{{ money }}\u003C\u002Fh1>\n  \u003Cinput type=\"text\" v-model=\"info\">\n  \u003Chr>\n  \u003C!-- props：父亲给儿子数据 -->\n   \u003C!-- \u003CChild :modelValue=\"money\" @updata:model-value=\"handler\">\u003C\u002FChild> -->\n\n   \u003C!-- v-model组件身上的作用\n    第一：相当于给子组件传递props[modelValue] = 1000\n    第二：相当于给子组件绑定自定义事件 update:modelValue   事件名不能错\n    \n   -->\n   \u003CChild v-model=\"money\">\u003C\u002FChild>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F v-model指令，收集表单数据，实现双向绑定\nimport {ref} from 'vue'\n\nlet info = ref('')\n\n\u002F\u002F v-model 实现组件之间的通信，实现父子组件的数据同步\n\u002F\u002F 父亲给子组件数据 Props\n\u002F\u002F 子组件给父组件数据 自定义事件\n\n\u002F\u002F 引入子组件\nimport Child from '.\u002FChild.vue';\n\n\u002F\u002F 父组件的数据钱数\nlet money = ref(10000)\n\n\u002F\u002F 自定义事件的回调\nconst handler = (num) =>{\n  \u002F\u002F 将来接受子组件传递过来的数据\n  \u002F\u002F console.log(num)\n  money.value = num\n}\n\u003C\u002Fscript>\n```\n\n子组件：\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"child\">\n  \u003Ch3>钱数：{{ modelValue }}\u003C\u002Fh3>\n  \u003Cbutton @click=\"handler\">父子组件数据绑定\u003C\u002Fbutton>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 接受 props\nlet props = defineProps(['modelValue'])\nlet $emit = defineEmits(['update:modelValue'])\n\n\u002F\u002F 子组件内部按钮的点击回调\nconst handler = () =>{\n  \u002F\u002F 触发自定义事件\n  $emit('update:modelValue',props.modelValue+1000)\n}\n\u003C\u002Fscript>\n\n```\n\n**多 v-model 绑定：**\n\nVue3 允许一个组件使用多个 `v-model`，从而实现父子组件多个数据的同步。\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>分页示例\u003C\u002Fh1>\n    \u003C!-- 使用 v-model 绑定多个属性 -->\n    \u003CChild1 v-model:pageNo=\"pageNo\" v-model:pageSize=\"pageSize\" \u002F>\n    \u003Cp>当前页码：{{ pageNo }}\u003C\u002Fp>\n    \u003Cp>每页显示条数：{{ pageSize }}\u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { ref } from 'vue';\nimport Child1 from '.\u002FChild1.vue';  \u002F\u002F 引入子组件\n\n\u002F\u002F 定义响应式数据\nlet pageNo = ref(1);   \u002F\u002F 当前页码\nlet pageSize = ref(3); \u002F\u002F 每页显示条数\n\u003C\u002Fscript>\n\n```\n\n子组件\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"son2\">\n    \u003Ch1>同时绑定多个 v-model\u003C\u002Fh1>\n    \u003Cbutton @click=\"handler\">pageNo {{ pageNo }}\u003C\u002Fbutton>\n    \u003Cbutton @click=\"updatePageSize\">pageSize {{ pageSize }}\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nconst props = defineProps({\n  pageNo: Number,  \u002F\u002F 接收父组件传递的 pageNo\n  pageSize: Number \u002F\u002F 接收父组件传递的 pageSize\n});\n\nconst emit = defineEmits(['update:pageNo', 'update:pageSize']); \u002F\u002F 声明触发的事件\n\n\u002F\u002F 更新 pageNo\nconst handler = () => {\n  emit('update:pageNo', props.pageNo + 1);  \u002F\u002F 每次点击增加页码\n};\n\n\u002F\u002F 更新 pageSize\nconst updatePageSize = () => {\n  emit('update:pageSize', props.pageSize + 2);  \u002F\u002F 每次点击增加每页显示的条数\n};\n\u003C\u002Fscript>\n```\n\n\n\n## 2.5 `useAttrs`：获取组件的非 `props` 属性和事件\n\n`useAttrs` 是 Vue3 提供的一个 Composition API，用于获取组件实例上未被 `defineProps` 声明的属性和事件（包括原生 DOM 事件和自定义事件）。这类似于 Vue2 中的 `$attrs` 和 `$listeners`。\n\n父组件\n\n```vue\n\u003Ctemplate>\n\u003Cdiv>\n  \u003Ch1>useAttrs\u003C\u002Fh1>\n  \u003Cel-button type=\"primary\" size=\"small\" :icon=\"Edit\">\u003C\u002Fel-button>\n  \u003C!-- 自定义组件 加上文字显示 -->\n   \u003CHintButton type=\"primary\" size=\"small\" :icon=\"Edit\" title=\"编辑按钮\" @click=\"handler\">\u003C\u002FHintButton>\n   \n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { Edit } from '@element-plus\u002Ficons-vue';\nimport HintButton from '.\u002FHintButton.vue';\n\n\u002F\u002F 按钮点击的回调\nconst handler = () =>{\n  alert('12306')\n}\n\u003C\u002Fscript>\n\n```\n\n子组件\n\n```vue\n\n\u003Ctemplate>\n\u003Cdiv :title=\"$attrs.title\">\n  \u003Cel-button :=\"$attrs\">\u003C\u002Fel-button>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 引入useAttrs方法：获取组件标签上属性与事件\nimport {useAttrs} from \"vue\"\n\u002F\u002F 此方法会执行返回一个对象\nlet $attrs = useAttrs()\nconsole.log($attrs)\n\n\u003C\u002Fscript>\n```\n\n需要注意的是，如果 `defineProps` 已经接收了某个属性，那么 `useAttrs` 返回的对象中将不再包含该属性。\n\n\n\n## 2.6 `ref` 与 `$parent`：直接访问子\u002F父组件实例\n\n`ref` 允许我们在父组件中获取子组件的实例（VC），从而直接访问子组件的方法和响应式数据。而 `$parent` 则允许子组件获取其父组件的实例。\n\n**父组件通过 `ref` 访问子组件：**\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"fa\">\n\u003Ch1>我是父亲，我有{{ money }}钱\u003C\u002Fh1>\n\u003Cbutton @click=\"handler\">点我向儿子借10块钱\u003C\u002Fbutton>\n\u003Cbr>\n\u003CSon ref=\"son\">\u003C\u002FSon>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 引入子组件\nimport Son from '.\u002FSon.vue';\nimport {ref} from 'vue'\n\n\u002F\u002F 父组件钱数\nlet money = ref(1000)\n\u002F\u002F 获取子组件钱数\nlet son = ref()  \u002F\u002F 必须同名\n\u002F\u002F 父组件内部按钮点击回调\nconst handler = () =>{\n  money.value += 10\n  \u002F\u002F 让儿子的钱数-10\n  son.value.money -= 10\n  \u002F\u002F 调用到儿子的方法\n  son.value.fly()\n  \n  console.log(son.value)\n}\n\u003C\u002Fscript>\n```\n\n**子组件通过 `defineExpose` 暴露数据和方法：**\n\n在 Vue3 中，组件内部的数据和方法默认不对外暴露。如果希望父组件通过 `ref` 访问，子组件必须使用 `defineExpose` 明确地暴露它们。\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"son\">\n  \u003Ch3>我是儿子，我有{{ money }}钱\u003C\u002Fh3>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\nimport {ref} from 'vue'\n\n\u002F\u002F 儿子钱数\nlet money = ref(100)\n\nconst fly = () =>{\n  console.log('我可以飞');\n}\n\n\u002F\u002F 组件内部数据对外关闭，别人不能访问\n\u002F\u002F 访问需要通过defineExpose方法对外暴露\ndefineExpose({\n  money,\n  fly\n})\n\u003C\u002Fscript>\n```\n\n**子组件通过 `$parent` 访问父组件：同样父亲的数据也得对外暴露**\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"dau\">\n    \u003Ch3>我是闺女,我有{{ money }}钱\u003C\u002Fh3>\n    \u003Cbutton @click=\"handler($parent)\">点击父亲给我20块钱\u003C\u002Fbutton>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport {ref} from 'vue'\n\u002F\u002F 闺女的钱\nlet money = ref(5000)\n\n\u002F\u002F 闺女内部按钮触发的回调\nconst handler = (parent:any) =>{\n    money.value += 20\n    parent.money -= 20   \n}\n\u003C\u002Fscript>\n\n\u003Cstyle scoped>\n.dau{\n    width: 300px;\n    height: 300px;\n    background-color: purple;\n}\n\u003C\u002Fstyle>\n```\n\n\n\n## 2.7 Pinia：新一代集中式状态管理容器\n\nPinia 是 Vue3 推荐的集中式状态管理库，它类似于 Vuex，但更加轻量、直观，并且移除了 `mutations` 和 `modules` 等概念，使得状态管理更加扁平化和易于理解。\n\n使用：\n\n* 安装pinia `npm install pinia`\n\n* 操作新建文件夹 `src\u002Fmain.ts `     \n\n* ```typescript\n  \u002F\u002F 操作main.ts\n  \u002F\u002F创建大仓库\n  import { createPinia } from 'pinia';\n  \u002F\u002FcreatePinia方法可以用于创建大仓库\n  let store = createPinia();\n  \u002F\u002F对外暴露,安装仓库\n  export default store;\n  ```\n\n* ```typescript\n  \u002F\u002F 主文件 main.ts 引入仓库\n  \u002F\u002F引入仓库\n  import store from '.\u002Fstore'\n  app.use(store)\n  ```\n\n* 创建小仓库  `scr\u002Fmodules\u002Finfo.ts`      **(选择式api用法)**\n\n* ```typescript\n  \u002F\u002F定义info小仓库\n  import { defineStore } from \"pinia\";\n  \n  \u002F\u002F第一个仓库:小仓库名字  第二个参数:小仓库配置对象\n  \u002F\u002FdefineStore方法执行会返回一个函数,函数作用就是让组件可以获取到仓库数据\n  let useInfoStore = defineStore(\"info\", {\n      \u002F\u002F存储数据:state\n      state: () => {\n          return {\n              count: 99,\n              arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n          }\n      },\n      \n      \u002F\u002F 业务逻辑: actions\n      actions: {\n          \u002F\u002F注意:函数没有context上下文对象\n          \u002F\u002F没有commit、没有mutations去修改数据\n          updateNum(a: number, b: number) {\n              this.count += a;\n          }\n      },\n      \n      \u002F\u002F 计算属性:getters\n      getters: {\n          total() {\n              \u002F\u002F 计算和\n              let result:any = this.arr.reduce((prev: number, next: number) => {\n                  return prev + next;\n              }, 0);\n              return result;\n          }\n      }\n  });\n  \u002F\u002F对外暴露方法\n  export default useInfoStore;\n  ```\n\n* 使用仓库数据，方法\n\n* ```vue\n  \u003Ctemplate>\n    \u003Cdiv class=\"child\">\n      \u003Ch1>{{ infoStore.count }}---{{infoStore.total}}\u003C\u002Fh1>\n      \u003Cbutton @click=\"updateCount\">点击我修改仓库数据\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  \u003C\u002Ftemplate>\n  \n  \u003Cscript setup lang=\"ts\">\n      \n  import useInfoStore from \"..\u002F..\u002Fstore\u002Fmodules\u002Finfo\";\n  \u002F\u002F获取小仓库对象\n  let infoStore = useInfoStore();\n  console.log(infoStore);\n      \n  \u002F\u002F修改数据方法\n  const updateCount = () => {\n    \u002F\u002F仓库调用自身的方法去修改仓库的数据\n    infoStore.updateNum(66,77);\n  };\n  \u003C\u002Fscript>\n  \n  ```\n\n* 组合式api\n\n* ```typescript\n  \u002F\u002F定义组合式API仓库\n  import { defineStore } from \"pinia\";\n  import { ref, computed,watch} from 'vue';\n  \u002F\u002F创建小仓库\n  let useTodoStore = defineStore('todo', () => {\n      let todos = ref([{ id: 1, title: '吃饭' }, { id: 2, title: '睡觉' }, { id: 3, title: '打豆豆' }]);\n      let arr = ref([1,2,3,4,5]);\n  \n      const total = computed(() => {\n          return arr.value.reduce((prev, next) => {\n              return prev + next;\n          }, 0)\n      })\n      \u002F\u002F务必要返回一个对象:属性与方法可以提供给组件使用\n      return {\n          todos,\n          arr,\n          total,\n          updateTodo() {\n              todos.value.push({ id: 4, title: '组合式API方法' });\n          }\n      }\n  });\n  \n  export default useTodoStore;\n  ```\n\n\n\n## 2.8 插槽 (`slot`)：灵活的内容分发\n\n插槽是 Vue 组件提供内容分发能力的机制，它允许父组件向子组件的指定位置注入内容，从而实现更灵活的组件组合。插槽分为默认插槽、具名插槽和作用域插槽。\n\n**默认插槽：**\n\n子组件定义一个 `\u003Cslot>` 标签，父组件在使用子组件时，在双标签内部书写的内容将填充到这个插槽中。\n\n```vue\n子组件 Test.vue\n\u003Ctemplate>\n  \u003Cdiv class=\"box\">\n    \u003Ch1>我是子组件默认插槽\u003C\u002Fh1>\n    \u003C!-- 默认插槽 -->\n    \u003Cslot>\u003C\u002Fslot>\n    \u003Ch1>我是子组件默认插槽\u003C\u002Fh1>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n父组件\n    \u003CTest>\n      \u003Cdiv>\n        \u003Cpre>大江东去浪淘尽,千古分流人物\u003C\u002Fpre>\n      \u003C\u002Fdiv>\n    \u003C\u002FTest>\n\n\u003Cscript setup lang=\"ts\">\nimport Test from \".\u002FTest.vue\";\n\u003C\u002Fscript>\n```\n\n**具名插槽：**\n\n子组件定义多个带有 `name` 属性的插槽，父组件通过 `v-slot:` 或 `#` 指令指定内容填充到哪个具名插槽。\n\n```vue\n子组件 Test.vue\n\u003Ch1>具名插槽填充数据\u003C\u002Fh1>\n \u003Cslot name=\"a\">\u003C\u002Fslot>\n \u003Ch1>具名插槽填充数据\u003C\u002Fh1>\n\n \u003Ch1>具名插槽填充数据\u003C\u002Fh1>\n \u003Cslot name=\"b\">\u003C\u002Fslot>\n \u003Ch1>具名插槽填充数据\u003C\u002Fh1>\n\n父组件\n\u003C!-- 具名插槽填充a -->\n      \u003Ctemplate v-slot:a>\n        \u003Cdiv>我是填充具名插槽a位置结构\u003C\u002Fdiv>\n      \u003C\u002Ftemplate>\n      \u003C!-- 具名插槽填充b v-slot指令可以简化为# -->\n      \u003Ctemplate #b>\n        \u003Cdiv>我是填充具名插槽b位置结构\u003C\u002Fdiv>\n      \u003C\u002Ftemplate>\n```\n\n![image-20260129222630502](images\u002Fimage-20260129222630502.png)\n\n\n\n**作用域插槽：**\n\n作用域插槽允许子组件在渲染插槽内容时向父组件传递数据，父组件可以根据这些数据决定插槽内容的结构和样式。\n\n```vue\n父组件\n  \u003CTest1 :todos=\"todos\">    \u003C!-- 将数据传给子组件 -->\n      \u003Ctemplate v-slot=\"{ $row, $index }\">\n \t\t\u003C!-- 父组件决定子组件的结构与外观 -->\n        \u003Cp :style=\"{ color: $row.done ? 'green' : 'red' }\">\n          {{ $row.title }}--{{ $index }}\n        \u003C\u002Fp>\n      \u003C\u002Ftemplate>\n      \n     \u003C!-- 解构\n\t\t\u003Ctemplate v-slot=\"slotProps\">\n \t \t\u003Cp>\n    \t\t {{ slotProps.$row.title }}\n   \t\t\t {{ slotProps.$index }}\n  \t\t\u003C\u002Fp>\n\t\t\u003C\u002Ftemplate>\n\t\t\t\t-->\n    \u003C\u002FTest1>\n\n\u003Cscript setup lang=\"ts\">\n\nimport Test1 from \".\u002FTest1.vue\";\nimport { ref } from \"vue\";\n\u002F\u002F 父组件内部数据\nlet todos = ref([\n  { id: 1, title: \"吃饭\", done: true },\n  { id: 2, title: \"睡觉\", done: false },\n  { id: 3, title: \"打豆豆\", done: true },\n  { id: 4, title: \"打游戏\", done: false },\n]);\n\u003C\u002Fscript>\n\n\n子组件 Test1.vue\n\u003Ctemplate>\n  \u003Cdiv class=\"box\">\n    \u003Ch1>作用域插槽\u003C\u002Fh1>\n    \u003Cul>\n         \u003C!-- 组件内部遍历数组 -->\n      \u003Cli v-for=\"(item, index) in todos\" :key=\"item.id\">\n        \u003C!--作用域插槽:可以将数据回传给父组件-->\n        \u003Cslot :$row=\"item\" :$index=\"index\">\u003C\u002Fslot>\n      \u003C\u002Fli>\n    \u003C\u002Ful>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F通过props接受父组件传递数据\ndefineProps([\"todos\"]);\n\u003C\u002Fscript>\n\n```\n\n\n\n# 二、项目初始化与规范化：构建高质量代码的基石\n\n一个高质量的项目离不开严格的代码规范和统一的开发流程。“硅谷甄选运营平台”项目在初始化阶段就集成了多项工具，确保了代码的质量和团队协作的顺畅\n\n## 2.1 环境准备与项目初始化\n\n项目采用 **Vue3** 和 **Vite** 进行构建，并强制使用 **pnpm** 作为包管理器，以确保依赖安装的一致性和高性能。\n\n**环境要求：**\n\n- Node.js v16.14.2+\n\n- pnpm 8.0.0+\n\n- **pnpm 安装：**\n\n  ```css\n  npm i -g pnpm\n  ```\n\n  ![image-20260129233201061](images\u002Fimage-20260129233201061.png)\n\n  **项目初始化命令：**\n\n  ```lua\n  pnpm create vite\n  ```\n\n  * 项目名字\n  * 项目框架\n  * 语言选择\n\n  ![image-20260129233658337](images\u002Fimage-20260129233658337.png)\n\n  * 如果没有自动安装依赖\n\n    ​\t手动安装依赖\n\n    ​\t进入项目根目录后，运行 `pnpm install` 安装所有依赖，然后 `pnpm run dev` 即可启动项目。\n\n  * 项目目录结构\n\n    ![image-20260129234148057](images\u002Fimage-20260129234148057.png)\n\n  ## 注意：\n\n  ### 1️⃣ `node_modules`（**依赖仓库**）\n\n  ```\n  node_modules\u002F\n  ```\n\n  - 所有第三方包都在这\n  - 比如：vue、vite、typescript……\n  - **不要动、不要删、不要提交到 git**\n\n  ### 2️⃣ `src`（你 90% 的代码都在这）\n\n  ```\n  src\u002F\n  ```\n\n  这是**主战场**，之后你会在里面：\n\n  - 写页面\n  - 写组件\n  - 配路由\n  - 写状态管理（pinia）\n\n  👉 admin 项目，`src` 就是“总部”\n\n  ### 3️⃣ `public`（静态资源）\n\n  ```\n  public\u002F\n  ```\n\n  - logo\n  - favicon\n  - 不需要打包处理的资源\n\n  使用方式：\n\n  ```\n  \u003Cimg src=\"\u002Flogo.png\" \u002F>\n  ```\n\n  ### 4️⃣ `index.html`（入口 HTML，非常重要）\n\n  ```\n  index.html\n  ```\n\n  ⚠️ Vite 的一个**大特点**：\n\n  - 这是项目的 **真正入口**\n  - 不是 build 后生成的\n  - **开发阶段就直接用**\n\n  里面会看到：\n\n  ```\n  \u003Cdiv id=\"app\">\u003C\u002Fdiv>\n  \u003Cscript type=\"module\" src=\"\u002Fsrc\u002Fmain.ts\">\u003C\u002Fscript>\n  ```\n\n  ### 5️⃣ `package.json`（项目说明书）\n\n  ```\n  {\n    \"scripts\": {\n      \"dev\": \"vite\",\n      \"build\": \"vite build\"\n    }\n  }\n  ```\n\n  它决定了：\n\n  - 项目叫什么\n  - 用了哪些依赖\n  - `pnpm run dev` 是干嘛的\n\n  ### 6️⃣ `pnpm-lock.yaml`（依赖锁）\n\n  - 锁定依赖版本\n  - **不能随便删**\n  - 团队协作非常重要\n\n  ### 7️⃣ `vite.config.ts`（Vite 配置文件）\n\n  ```\n  export default defineConfig({\n    plugins: [vue()]\n  })\n  ```\n\n  后面你会在这配置：\n\n  - 路径别名 `@`\n  - 代理（解决跨域）\n  - 打包配置\n\n  ### 8️⃣ `tsconfig*.json`（TypeScript 配置）\n\n  ```\n  tsconfig.json\n  tsconfig.app.json\n  tsconfig.node.json\n  ```\n\n  - 给 TS 编译器用的\n  - 初期基本不用碰\n  - 后面你学 TS 深了才会改\n\n  ### 9️⃣ `.vscode`（编辑器配置）\n\n  - VS Code 专用\n  - 代码格式、插件配置\n  - 不影响项目运行\n\n- scr\u002Fstyle.css 删掉\n\n  ![image-20260129234946814](images\u002Fimage-20260129234946814.png)\n\n- APP.vue 代码删除\n\n  ![image-20260129235030953](images\u002Fimage-20260129235030953.png)\n\n- index.html 改标题\n\n  ![image-20260129235601820](images\u002Fimage-20260129235601820.png)\n\n\n\n## 2.2 项目配置：代码质量与开发效率的保障\n\n### 2.2.1 项目自动启动\n\n package.json 中 \"dev\"项目启动 加上 --open 当项目启动的时候自动打开浏览器运行项目\n\n![image-20260130000152141](images\u002Fimage-20260130000152141.png)\n\n### 2.2.2  ESLint 配置：JavaScript\u002FTypeScript 代码质量检测\n\nESLint 是一个可插拔的 JavaScript\u002FTypeScript 代码检测工具，用于发现和报告代码中的问题。\n\n**安装：**\n\n`pnpm create @eslint\u002Fconfig@latest`\n\n![image-20260130134425119](images\u002Fimage-20260130134425119.png)\n\n![image-20260130134442157](images\u002Fimage-20260130134442157.png)\n\n![image-20260130122120694](images\u002Fimage-20260130122120694.png)\n\n\n\n**vue3校验插件安装**\n\n```javascript\npnpm install -D eslint-plugin-import eslint-plugin-vue eslint-plugin-node eslint-plugin-prettier eslint-config-prettier eslint-plugin-node @babel\u002Feslint-parser\n```\n\n重新配置文件\n\n```javascript\nimport js from \"@eslint\u002Fjs\"\nimport globals from \"globals\"\nimport tseslint from \"typescript-eslint\"\nimport pluginVue from \"eslint-plugin-vue\"\nimport importPlugin from \"eslint-plugin-import\"\nimport nPlugin from \"eslint-plugin-n\"\nimport prettierPlugin from \"eslint-plugin-prettier\"\nimport prettierConfig from \"eslint-config-prettier\"\nimport { defineConfig } from \"eslint\u002Fconfig\"\n\nexport default defineConfig([\n   \u002F\u002F 配置需要忽略的文件或目录，例如 dist 和 node_modules\n    {\n    ignores: [\"node_modules\u002F**\", \"dist\u002F**\", \"build\u002F**\"],\n  },\n    \n  \u002F\u002F 基础 JavaScript 规则\n  {\n    files: [\"**\u002F*.{js,mjs,cjs,ts,mts,cts,vue}\"],\n    plugins: {\n      js,\n      import: importPlugin,\n      n: nPlugin,\n      prettier: prettierPlugin,\n    },\n    extends: [\n      \"js\u002Frecommended\",\n      prettierConfig, \u002F\u002F 关闭和 Prettier 冲突的规则\n    ],\n    languageOptions: {\n      globals: {\n        ...globals.browser,\n        ...globals.node,\n      },\n      parserOptions: {\n        ecmaVersion: \"latest\",\n        sourceType: \"module\",\n      },\n    },\n    rules: {\n      \"prettier\u002Fprettier\": \"warn\",\n      \"import\u002Forder\": [\n        \"warn\",\n        {\n          groups: [\"builtin\", \"external\", \"internal\", \"parent\", \"sibling\", \"index\"],\n          \"newlines-between\": \"always\",\n        },\n      ],\n      \"n\u002Fno-process-exit\": \"off\",\n    },\n  },\n\n  \u002F\u002F TypeScript 规则\n  tseslint.configs.recommended,\n\n  \u002F\u002F Vue 规则\n  pluginVue.configs[\"flat\u002Fessential\"],\n\n  \u002F\u002F Vue 文件中 TS 解析\n  {\n    files: [\"**\u002F*.vue\"],\n    languageOptions: {\n      parserOptions: {\n        parser: tseslint.parser,\n      },\n    },\n  },\n])\n\n```\n\n注意：\n\n`在cmd命令行中运行\npnpm remove eslint-plugin-node   移除旧版\npnpm add -D eslint-plugin-n      安装新版`\n\n\n\n在 `package.json` 中添加运行脚本，方便进行代码检查和修复：\n\n```javascript\n\"scripts\": {\n    \"lint\": \"eslint src\",\n    \"fix\": \"eslint src --fix\"\n}\n```\n\n配置其他相关\n\n```javascript\n rules: {\n      \u002F\u002F JavaScript 基础规则\n      'no-var': 'error', \u002F\u002F 禁止使用 var，强制使用 let\u002Fconst\n      'no-multiple-empty-lines': ['warn', { max: 1 }], \u002F\u002F 警告：最多允许一行空行\n      'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', \u002F\u002F 生产环境禁止使用 console\n      'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', \u002F\u002F 生产环境禁止使用 debugger\n      'no-unexpected-multiline': 'error', \u002F\u002F 防止由于自动分号插入引发的 bug\n      'no-useless-escape': 'off', \u002F\u002F 允许多余的转义字符（某些正则场景需要）\n\n      \u002F\u002F TypeScript 相关规则\n      '@typescript-eslint\u002Fno-unused-vars': 'error', \u002F\u002F 禁止未使用的变量\n      '@typescript-eslint\u002Fprefer-ts-expect-error': 'error', \u002F\u002F 更推荐使用 ts-expect-error 替代 ts-ignore\n      '@typescript-eslint\u002Fno-explicit-any': 'off', \u002F\u002F 允许使用 any 类型\n      '@typescript-eslint\u002Fno-non-null-assertion': 'off', \u002F\u002F 允许使用非空断言（!）\n      '@typescript-eslint\u002Fno-namespace': 'off', \u002F\u002F 允许使用命名空间（namespace）\n      '@typescript-eslint\u002Fsemi': 'off', \u002F\u002F 关闭对分号的强制检查\n\n      \u002F\u002F Vue 相关规则\n      'vue\u002Fmulti-word-component-names': 'off', \u002F\u002F 允许使用单词组件名（适用于页面组件等）\n      \u002F\u002F 'vue\u002Fscript-setup-uses-vars': 'error', \u002F\u002F 新版插件可能移除了此规则，先注释避免错误\n      'vue\u002Fno-mutating-props': 'off', \u002F\u002F 允许修改 props（某些业务场景可能需要）\n      'vue\u002Fattribute-hyphenation': 'off', \u002F\u002F 关闭模板中 attribute 必须使用连字符的限制\n    },\n  },\n)\n```\n\n安装加载ts格式的配置文件\n\n`pnpm add -D jiti`\n\n检查代码规范\n\n`pnpm run lint`\n\n修复代码规范\n\n`pnpm run fix`\n\n### 2.2.2 Prettier 配置：代码格式化工具\n\nPrettier 是一个强大的代码格式化工具，它专注于代码的美观和一致性，支持多种语言。ESLint 保证代码质量，而 Prettier 保证代码美观。两者结合，相得益彰。\n\n**安装 Prettier 相关依赖：**\n\n```mipsasm\npnpm install -D eslint-plugin-prettier prettier eslint-config-prettier\n```\n\n**新建.prettierrc 文件**\n\n![image-20260130152740128](images\u002Fimage-20260130152740128.png)\n\n\n\n\n\n``semi`: 代码末尾加分号\n\n`singleQuote`: 用单引号替代双引号\n\n`printWidth`: 最大行长度\n\n`tabWidth`: tab缩进宽度\n\n`trailingComma`: 多行时尾逗号风格\n\n`arrowParens`: 箭头函数参数是否加括号\n\n`endOfLine`: 换行符，防止跨平台差异`\n\n\n\n\n\n**新建.prettierignore 忽略文件**\n\n![image-20260130152833257](images\u002Fimage-20260130152833257.png)\n\n**vscode新建settings.json文件**\n\n保存文件时自动格式化代码\n\n统一使用 Prettier 插件格式化 `.vue`、`.ts`、`.js` 文件\n\n```javascript\n{\n  \"editor.formatOnSave\": true,\n  \"[vue]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[javascript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  }\n}\n\n```\n\n\n\n### 2.2.3 Stylelint 配置：CSS\u002FSCSS 代码规范\n\nStylelint 是 CSS 的 Lint 工具，用于格式化 CSS 代码、检查语法错误和不合理的写法，并指定 CSS 书写顺序等。项目中使用 SCSS 作为预处理器。\n\n**安装 Stylelint 及其相关依赖：**\n\n`pnpm add sass sass-loader stylelint postcss postcss-scss postcss-html stylelint-config-prettier stylelint-config-recess-order stylelint-config-recommended-scss stylelint-config-standard stylelint-config-standard-vue stylelint-scss stylelint-order stylelint-config-standard-scss -D`\n\n**创建.stylelintrc.cjs文件，配置**\n\n```javascript\nmodule.exports = {\n  ignoreFiles: ['**\u002F*.min.css', '**\u002Fnode_modules\u002F**', '**\u002Fdist\u002F**', '**\u002Funpackage\u002F**'],\n\n  extends: [\n    'stylelint-config-standard',\n    'stylelint-config-recess-order',\n    'stylelint-config-prettier',\n  ],\n\n  plugins: ['stylelint-scss'],\n\n  overrides: [\n    {\n      files: ['**\u002F*.vue', '**\u002F*.html'],\n      customSyntax: 'postcss-html',\n    },\n    {\n      files: ['**\u002F*.scss'],\n      customSyntax: 'postcss-scss',\n    },\n  ],\n\n  rules: {\n    \u002F* ===== 新手期先别炸 ===== *\u002F\n    'no-empty-source': null,\n    'block-no-empty': null,\n    'declaration-empty-line-before': null,\n\n    \u002F* ===== 基础风格 ===== *\u002F\n    indentation: 2,\n    'string-quotes': 'single',\n    'color-hex-case': 'lower',\n    'number-leading-zero': 'always',\n\n    \u002F* ===== 命名规则放行 ===== *\u002F\n    'selector-class-pattern': null,\n    'custom-property-pattern': null,\n    'keyframes-name-pattern': null,\n\n    \u002F* ===== uniapp \u002F vue 深度选择器 ===== *\u002F\n    'selector-pseudo-element-no-unknown': [\n      true,\n      {\n        ignorePseudoElements: ['v-deep', 'deep'],\n      },\n    ],\n\n    \u002F* ===== scss \u002F uniapp at-rule 全放行 ===== *\u002F\n    'at-rule-no-unknown': null,\n\n    \u002F* ===== :global ===== *\u002F\n    'selector-pseudo-class-no-unknown': [\n      true,\n      {\n        ignorePseudoClasses: ['global'],\n      },\n    ],\n  },\n};\n\n```\n\n**配置package.json文件**\n\n```json\n{\n  \"scripts\": {\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n   \t\"format\": \"prettier --write \\\".\u002F**\u002F*.{html,vue,ts,js,json,md}\\\"\", \u002F\u002F 添加这个\n  }\n}\n```\n\n**运行**\n\n`pnpm run format`\n\n\n\n### 2.2.4 Husky 配置：Git Hook 自动化\n\n为了强制开发人员遵循代码规范，我们引入了 Husky。Husky 可以在 Git 提交（commit）前触发 Git 钩子，自动执行代码格式化和检查。\n\n**安装 Husky：**\n\n```mipsasm\npnpm install -D husky\n```\n\n**先初始化git 仓库**\n\n`git init`\n\n**初始化 Husky：**\n\n```csharp\nnpx husky-init\n```\n\n这会在项目根目录下生成 `.husky` 目录，其中包含 `pre-commit` 文件。\n\n**修改 `.husky\u002Fpre-commit` 文件：**\n\n```perl\npnpm run format # 在提交前自动执行格式化\n```\n\n这样，每次执行 `git commit` 时，代码都会自动格式化。\n\n\n\n### 2.2.5 Commitlint 配置：统一 Git 提交信息规范\n\n为了保持 Git 提交信息的清晰和一致性，我们使用 Commitlint 来强制执行提交规范。\n\n**安装 Commitlint：**\n\n```sql\npnpm add @commitlint\u002Fconfig-conventional @commitlint\u002Fcli -D\n```\n\n**创建 `commitlint.config.cjs` 配置文件：**\n\n```java\n\u002F\u002F commitlint.config.cjs\nmodule.exports = {\n  \u002F\u002F 继承社区推荐的 'config-conventional' 规范\n  extends: ['@commitlint\u002Fconfig-conventional'],\n\n  \u002F\u002F 自定义规则 (0=禁用, 1=警告, 2=报错)\n  rules: {\n    \u002F\u002F type 类型必须是以下之一\n    'type-enum': [\n      2,\n      'always',\n      [\n        'feat',     \u002F\u002F 新功能\n        'fix',      \u002F\u002F 修复bug\n        'docs',     \u002F\u002F 文档\n        'style',    \u002F\u002F 代码格式\n        'refactor', \u002F\u002F 重构\n        'perf',     \u002F\u002F 性能优化\n        'test',     \u002F\u002F 测试\n        'chore',    \u002F\u002F 构建或辅助工具变动\n        'revert',   \u002F\u002F 回退\n        'build',    \u002F\u002F 打包\n      ],\n    ],\n\n    \u002F\u002F type 大小写: 不作限制\n    'type-case': [0],\n\n    \u002F\u002F type 是否为空: 不作限制\n    'type-empty': [0],\n\n    \u002F\u002F scope 是否为空: 不作限制\n    'scope-empty': [0],\n\n    \u002F\u002F scope 大小写: 不作限制\n    'scope-case': [0],\n\n    \u002F\u002F subject 结尾标点: 不作限制\n    'subject-full-stop': [0, 'never'],\n\n    \u002F\u002F subject 大小写: 不作限制\n    'subject-case': [0, 'never'],\n\n    \u002F\u002F header 最大长度: 不作限制\n    'header-max-length': [0, 'always', 72],\n  },\n};\n```\n\n在 `package.json` 中添加 Commitlint 脚本：\n\n```json\n\"scripts\": {\n    \"commitlint\": \"commitlint --config commitlint.config.cjs -e -V\"\n}\n```\n\n配置 Husky，在 `commit-msg` 钩子中执行 Commitlint 检查：(cmd命令行运行)\n\n```sql\nnpx husky add .husky\u002Fcommit-msg\n```\n\n在生成的 `.husky\u002Fcommit-msg` 文件中添加：(删除里面的undefined),添加下面这个\n\n```undefined\npnpm commitlint\n```\n\n**git提交规范**\n\n`git commit -m \"feat: 增加用户登录功能\"\ngit commit -m \"fix: 修复登录接口错误\"`\n\n\n\n# 三、项目集成：提升开发效率与用户体验\n\n在项目规范化之后，我们将核心的第三方库和工具集成到项目中，进一步提升开发效率和用户体验。\n\n## 3.1 Element Plus 集成：美观高效的 UI 组件库\n\n硅谷甄选运营平台采用 Element Plus 作为 UI 组件库，它提供了丰富的组件和友好的开发体验。\n\n**安装 Element Plus：**\n\n```bash\npnpm install element-plus @element-plus\u002Ficons-vue\n```\n\n```typescript\nimport { createApp } from 'vue';\nimport App from '.\u002FApp.vue';\n\n\u002F\u002F 引入element-plus插件与样式\nimport ElementPlus from 'element-plus';\nimport 'element-plus\u002Fdist\u002Findex.css';\n\u002F\u002F 配置element-plus国际化\nimport { zhCn } from 'element-plus\u002Fes\u002Flocales.mjs';\n\n\u002F\u002F 获取应用实例对象\nconst app = createApp(App);\n\u002F\u002F 安装element-plus插件\napp.use(ElementPlus,{\n    locale:zhCn \u002F\u002F element-plus国际化\n});\n\u002F\u002F 将应用挂在到挂载点上\napp.mount('#app');\n\n```\n\n## 3.2 `src` 别名配置：简化模块导入\n\n为了简化文件路径，提高代码可读性，我们为 `src` 目录配置了 `@` 别名。\n\n**`vite.config.ts` 配置：**\n\n```typescript\nimport { defineConfig } from 'vite';\nimport vue from '@vitejs\u002Fplugin-vue';\nimport path from 'path';\n\nexport default defineConfig({\n  plugins: [vue()],\n  resolve: {\n    alias: {\n      '@': path path.resolve(__dirname, '.\u002Fsrc'),\n    },\n  },\n});\n```\n\n**`tsconfig.json` TypeScript 编译配置：**\n\n```typescript\n{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@\u002F*\": [\"src\u002F*\"]\n    }\n  }\n}\n```\n\n`eslint.config.ts` **ESLint 导入解析器配置**：\n\n```typescript\nsettings: {\n  'import\u002Fresolver': {\n    typescript: {\n      alwaysTryTypes: true,\n      project: '.\u002Ftsconfig.app.json',\n    },\n  },\n}\n```\n\n**创建**`.eslint-import-resolver-typescript.json `**ESLint 导入解析器配置**\n\n```json\n{\n  \"alwaysTryTypes\": true,\n  \"project\": \".\u002Ftsconfig.app.json\"\n}\n```\n\n**安装依赖**\n\n```bash\npnpm add -D @types\u002Fnode\npnpm add -D eslint-import-resolver-typescript\n```\n\n\n\n## 3.3 环境变量配置：灵活适应多环境部署\n\n项目开发通常会经历开发、测试和生产等不同环境，每个环境的接口地址等配置可能不同。通过环境变量配置，我们可以轻松地在不同环境之间切换。\n\n**项目根目录创建环境变量文件：**\n\n- `.env.development` (开发环境)\n- `.env.production` (生产环境)\n- `.env.test` (测试环境)\n\n**`.env.development` 示例：**\n\n```ini\n✅ Node 环境变量\n❌ 前端代码里不能直接读（Vite 不会暴露它）\n✔ 常用于：\n后端 Node\n构建工具判断环境\nNODE_ENV = 'development' \n\n✅ 可以在前端读取\n用途：\n页面标题\n系统名称\nlogo 旁边文字\nVITE_APP_TITLE = '硅谷甄选运营平台'\n\n✅ 前端可读\n✅ 常配合 代理 使用\nVITE_APP_BASE_API = '\u002Fdev-api' # 变量必须以 VITE_ 为前缀才能暴露给外部读取\n```\n\n**`.env.test` 示例：**\n\n```ini\nNODE_ENV = 'test'\nVITE_APP_TITLE = '硅谷甄选运营平台'\nVITE_APP_BASE_API = '\u002Ftest-api' # 变量必须以 VITE_ 为前缀才能暴露给外部读取\n\n```\n\n**`.env.production` 示例：**\n\n```ini\nNODE_ENV = 'production'\nVITE_APP_TITLE = '硅谷甄选运营平台'\nVITE_APP_BASE_API = '\u002Fprod-api' # 变量必须以 VITE_ 为前缀才能暴露给外部读取\nVITE_SERVE=\"HTTP:\u002F\u002Fyyy.com\"\n```\n\n\n\n**`package.json` 配置运行命令：**\n\n```json\n\"scripts\": {\n    \"dev\": \"vite --open\",\n    \"build:test\": \"vue-tsc && vite build --mode test\",\n    \"build:pro\": \"vue-tsc && vite build --mode production\",\n    \"preview\": \"vite preview\"\n}\n```\n\n在代码中通过 `import.meta.env` 对象获取环境变量。\n\n![image-20260131131902927](images\u002Fimage-20260131131902927.png)\n\n## 3.4 SVG 图标配置与封装：轻量级矢量图方案\n\nSVG 矢量图相比传统图片资源具有体积小、不失真、性能优越等优点。项目中通过 `vite-plugin-svg-icons` 插件集成 SVG 图标。\n\n**安装插件：**\n\n```mipsasm\npnpm install vite-plugin-svg-icons -D\n```\n\n**`vite.config.ts` 配置插件：**\n\n```javascript\nimport { defineConfig } from 'vite';\nimport vue from '@vitejs\u002Fplugin-vue';\nimport { createSvgIconsPlugin } from 'vite-plugin-svg-icons';\nimport path from 'path';\n\nexport default defineConfig(({ command }) => {\n  return {\n    plugins: [\n      vue(),\n      createSvgIconsPlugin({\n        \u002F\u002F 指定需要缓存的图标文件夹\n        iconDirs: [path.resolve(process.cwd(), 'src\u002Fassets\u002Ficons')],\n        \u002F\u002F 指定 symbolId 的格式\n        symbolId: 'icon-[dir]-[name]',\n      }),\n    ],\n  };\n});\n```\n\n**入口文件 `main.ts` 导入：**\n\n```go\nimport 'virtual:svg-icons-register'; \u002F\u002F注册 SVG 图标 ，让 Vite 的 SVG 插件生效。\n```\n\n**注意：在src\u002Fassets下创建icons**\n\n**SVG图标使用**\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"\">\n    \u003Ch1>测试\u003C\u002Fh1>\n    \u003C!-- svg图标使用 -->\n    \u003Csvg style=\"width: 30px;height: 30px;\">\n      \u003Cuse xlink:href=\"#icon-suo\" fill=\"\">\u003C\u002Fuse>\n    \u003C\u002Fsvg>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n```\n\n**SVG 图标全局组件封装 (`src\u002Fcomponents\u002FSvgIcon\u002Findex.vue`)：**\n\n为了方便使用，我们将 SVG 图标封装成一个全局组件 `SvgIcon`。\n\n```vue\n\u003Ctemplate>\n    \u003Csvg  class=\"svg-icon\" :style=\"{ width: width, height: height }\">\n      \u003Cuse :href=\"prefix + name\" :fill=\"color\">\u003C\u002Fuse>\n    \u003C\u002Fsvg>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\ndefineProps({\n  \u002F\u002F xlink:href 属性值的前缀\n  prefix: {\n    type: String,\n    default: '#icon-'    \u002F\u002F default 是 Vue 官方提供的 props 选项，用来给“父组件没传的 prop”一个默认值。\n\t\t\t\n  },\n  \u002F\u002F svg 矢量图的名字\n  name: String,\n  \u002F\u002F svg 图标的颜色\n  color: {\n    type: String,\n    default: \"\"\n  },\n  \u002F\u002F svg 宽度\n  width: {\n    type: String,\n    default: '16px'\n  },\n  \u002F\u002F svg 高度\n  height: {\n    type: String,\n    default: '16px'\n  }\n});\n\u003C\u002Fscript>\n\u003Cstyle scoped>\n   .svg-icon {\n  fill: currentColor;        \u002F\u002F 让 SVG 图形的填充颜色跟随当前元素的字体颜色，保持一致。\n  vertical-align: middle;    \u002F\u002F 是让行内或行内块元素在行高里垂直居中，更美观对齐文本和其他元素。\n}\n\u003C\u002Fstyle>\n\n\n```\n\n**全局组件注册 (`src\u002Fcomponents\u002Findex.ts`)：**\n\n```typescript\n\u002F\u002F 引入项目中的全部组件\nimport SvgIcon from '.\u002FSvgIcon\u002Findex.vue';\nimport type { App, Component } from 'vue';\n\u002F\u002F 全局对象\nconst components: Record\u003Cstring, Component> = { SvgIcon };\n\u002F\u002F 对外暴露插件对象\nexport default {\n    install(app: App) {\n        \u002F\u002F 注册项目全部的全局组件\n        Object.keys(components).forEach((key: string) => {\n            \u002F\u002F 注册为全局组件\n            app.component(key, components[key]!); \u002F\u002F !告诉ts不可能为空\n        });\n    }\n};\n```\n\n注意：\n\n`类型注解：`Record\u003Cstring, Component>`\n\n这是TypeScript的**工具类型**（Utility Type），用于约束`components`对象的结构：\n\n- `Record\u003CK, V>`表示：**键的类型为`K`，值的类型为`V`的对象**。\n- 这里`K`是`string`（键必须是字符串，即组件的名称），`V`是`Component`（值必须是Vue组件，来自`vue`的类型定义）。\n\n**作用**：确保`components`对象的键只能是字符串（组件名），值只能是Vue组件，避免类型错误（比如不小心放入非组件的值）。`\n\n\n\n**在 `main.ts` 中安装全局组件：引入自定义插件**\n\n```javascript\n\u002F\u002F 引入自定义插件\nimport gloablComponent from '.\u002Fcomponents\u002Findex';\n\u002F\u002F 安装自定义插件\napp.use(gloablComponent);\n```\n\n**使用**\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"\">\n    \u003Ch1>测试\u003C\u002Fh1>\n    \u003C!-- svg图标使用 -->\n    \u003Csvg-icon name=\"suo\" color=\"red\" width=\"100px\" height=\"100px\">\u003C\u002Fsvg-icon>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport SvgIcon from '@\u002Fcomponents\u002FSvgIcon\u002Findex.vue'\n\u003C\u002Fscript>\n```\n\n`注意`：\n\n为什么使用组件是svg-icon呢，而不是SvgIcon\n\nVue官网推荐组件名（大驼峰）\n\nVue 会自动将 PascalCase （大驼峰）转换为 kebab-case （短横线）：\n\n自定义标签名：\n\n```typescript\nconst components = {\n  'my-custom-icon': SvgIcon,  \u002F\u002F ← 自定义标签名\n};\n```\n\n## 3.5 集成 Sass：增强样式开发能力\n\n项目已通过 Stylelint 配置安装了 `sass` 和 `sass-loader`，可以直接在 Vue 组件中使用 SCSS 语法，只需在 `\u003Cstyle>` 标签上添加 `lang=\"scss\"`。\n\n**引入全局样式和变量：**\n\n在 `src\u002Fstyles` 目录下创建 `index.scss` (用于引入全局重置样式) 和 `variable.scss` (用于定义全局 SCSS 变量)。\n\n**`src\u002Fstyles\u002Findex.scss`：清楚默认样式**\n\n`npm官网`\n\n```scss\nhtml, body, div, span, applet, object, iframe,\nh1, h2, h3, h4, h5, h6, p, blockquote, pre,\na, abbr, acronym, address, big, cite, code,\ndel, dfn, em, img, ins, kbd, q, s, samp,\nsmall, strike, strong, sub, sup, tt, var,\nb, u, i, center,\ndl, dt, dd, ol, ul, li,\nfieldset, form, label, legend,\ntable, caption, tbody, tfoot, thead, tr, th, td,\narticle, aside, canvas, details, embed, \nfigure, figcaption, footer, header, hgroup, \nmenu, nav, output, ruby, section, summary,\ntime, mark, audio, video {\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tfont-size: 100%;\n\tfont: inherit;\n\tvertical-align: baseline;\n}\n\u002F* HTML5 display-role reset for older browsers *\u002F\narticle, aside, details, figcaption, figure, \nfooter, header, hgroup, menu, nav, section {\n\tdisplay: block;\n}\nbody {\n\tline-height: 1;\n}\nol, ul {\n\tlist-style: none;\n}\nblockquote, q {\n\tquotes: none;\n}\nblockquote:before, blockquote:after,\nq:before, q:after {\n\tcontent: '';\n\tcontent: none;\n}\ntable {\n\tborder-collapse: collapse;\n\tborder-spacing: 0;\n}\n```\n\n**`main.ts` 引入全局样式：**\n\n```go\nimport '@\u002Fstyles\u002Findex.scss';\n```\n\n**`src\u002Fstyles\u002Findex.scss`：**\n\n```scss\n@use '.\u002Freset.scss'; \u002F\u002F 引入重置样式,npm官网复制\n\t\t\t\t\t\u002F\u002F 其他全局样式\n```\n\n**`vite.config.ts` 配置全局 SCSS 变量：**\n\n为了在所有组件中都能使用全局 SCSS 变量，需要在 Vite 配置中进行设置。\n\n```typescript\nimport { defineConfig } from 'vite';\nimport vue from '@vitejs\u002Fplugin-vue';\nimport path from 'path';\n\n\u002F\u002F 引入svg需要用到的插件\nimport { createSvgIconsPlugin } from 'vite-plugin-svg-icons';\n\nexport default defineConfig({\n  plugins: [\n    vue(),\n    createSvgIconsPlugin({\n      \u002F\u002F 指定需要缓存的图标文件夹\n      iconDirs: [path.resolve(process.cwd(), 'src\u002Fassets\u002Ficons')],\n      \u002F\u002F 指定 symbolId 的格式\n      symbolId: 'icon-[dir]-[name]',\n    }),\n  ],\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, '.\u002Fsrc'),\n    },\n  },\n  css: {\n    preprocessorOptions: {\n      scss: {\n        javascriptEnabled: true,\n        additionalData: `@use \"@\u002Fstyles\u002Fvariable.scss\" as *;`, \u002F\u002F 引入全局变量\n      },\n    },\n  },\n});\n\n```\n\n## 3.6 Mock 数据：前端独立开发利器\n\n在后端接口尚未完成时，Mock 数据能够让前端独立进行开发和测试，提高开发效率。项目使用了 `vite-plugin-mock` 和 `mockjs`。\n\n**安装依赖：**\n\n```mipsasm\npnpm install -D vite-plugin-mock mockjs\n```\n\n```typescript\nexport default defineConfig(({ mode }) => {\n  const isDev = mode === 'development';\n    \u002F\u002F mode 的值：\n  \t\u002F\u002F - 'development'  开发环境\n  \t\u002F\u002F - 'production' 生产环境\n    \u002F\u002F - 'test'       测试环境\n\n  return {\n    plugins: [\n      vue(),\n      createSvgIconsPlugin({\n        \u002F\u002F 指定需要缓存的图标文件夹\n        iconDirs: [path.resolve(process.cwd(), 'src\u002Fassets\u002Ficons')],\n        \u002F\u002F 指定 symbolId 的格式\n        symbolId: 'icon-[dir]-[name]',\n      }),\n        \u002F\u002F 条件加载插件\n      isDev &&\n        viteMockServe({\n          mockPath: 'mock',\n          enable: true,\n        }),\n    ],\n```\n\n在项目根目录下创建\n\n**`mock\u002Fuser.ts` 示例：**\n\n在 `mock` 文件夹下创建 `user.ts` 文件，定义模拟的用户登录和信息接口。\n\n```typescript\n\u002F\u002F mock\u002Fuser.ts\n\u002F\u002F 用户信息数据\n\u002F\u002F 该函数返回一个数组，包含用户的两个信息\nfunction createUserList() {\n    return [\n        {\n            userId: 1,\n            avatar:\n                'https:\u002F\u002Fwpimg.wallstcn.com\u002Ff778738c-e4f8-4870-b634-56703b4acafe.gif',\n            username: 'admin',\n            password: '111111',\n            desc: '平台管理员',\n            roles: ['平台管理员'],\n            buttons: ['cuser.detail'],\n            routes: ['home'],\n            token: 'Admin Token',\n        },\n        {\n            userId: 2,\n            avatar:\n                'https:\u002F\u002Fwpimg.wallstcn.com\u002Ff778738c-e4f8-4870-b634-56703b4acafe.gif',\n            username: 'system',\n            password: '111111',\n            desc: '系统管理员',\n            roles: ['系统管理员'],\n            buttons: ['cuser.detail', 'cuser.user'],\n            routes: ['home'],\n            token: 'System Token',\n        },\n    ];\n}\n\u002F\u002F 对外暴露一个数组：数组里面包含两个接口\n\u002F\u002F 登录假的接口，获取用户信息的假的接口\nexport default [\n    \u002F\u002F 用户登录接口\n    {\n        url: '\u002Fapi\u002Fuser\u002Flogin', \u002F\u002F 请求地址\n        method: 'post', \u002F\u002F 请求方式\n        response: ({ body }) => {\n            \u002F\u002F 获取请求体携带过来的用户名与密码\n            const { username, password } = body;\n            \u002F\u002F 调用获取用户信息函数,用于判断是否有此用户\n            const checkUser = createUserList().find(\n                (item) => item.username === username && item.password === password,\n            );\n            \u002F\u002F 没有用户返回失败信息\n            if (!checkUser) {\n                return { code: 201, data: { message: '账号或者密码不正确' } };\n            }\n            \u002F\u002F 如果有返回成功信息\n            const { token } = checkUser;\n            return { code: 200, data: { token } };\n        },\n    },\n    \u002F\u002F 获取用户信息\n    {\n        url: '\u002Fapi\u002Fuser\u002Finfo',\n        method: 'get',\n        response: (request) => {\n            \u002F\u002F 获取请求头携带token\n            const token = request.headers.token;\n            \u002F\u002F 查看用户信息是否包含有次token用户\n            const checkUser = createUserList().find((item) => item.token === token);\n            \u002F\u002F 没有返回失败的信息\n            if (!checkUser) {\n                return { code: 201, data: { message: '获取用户信息失败' } };\n            }\n            \u002F\u002F 如果有返回成功信息\n            return { code: 200, data: { checkUser } };\n        },\n    },\n];\n\n```\n\n## 3.7 Axios 二次封装：统一网络请求与错误处理\n\n在前端项目中，Axios 是常用的 HTTP 客户端。对其进行二次封装，可以实现请求拦截、响应拦截、统一错误处理等功能，极大地提升开发效率和代码健壮性。\n\n**安装 Axios：**\n\n```mipsasm\npnpm install axios\n```\n\n**`src\u002Futils\u002Frequest.ts` Axios 二次封装：**\n\n```typescript\n\u002F\u002F 进行axios二次封装:使用请求与相应拦截器\nimport axios from 'axios';\nimport { ElMessage } from 'element-plus';\n\n\u002F\u002F 第一步：利用axios对象的create方法，去创建axios实例（其他的配置：基础路径，超时的时间）\nconst request = axios.create({\n  baseURL: import.meta.env.VITE_APP_BASE_API, \u002F\u002F 基础路径会携带上\u002Fapi\n  timeout: 5000, \u002F\u002F超时的时间\n});\n\u002F\u002F 第二步：request实例添加请求与响应拦截器\nrequest.interceptors.request.use((config) => {\n  \u002F\u002F config配置对象，headers属性请求头，经常给服务器端携带公共参数\n  return config;\n});\n\n\u002F\u002F 第三步：响应拦截器\nrequest.interceptors.response.use(\n  (response) => {\n    \u002F\u002F 成功回调\n    \u002F\u002F 简化数据\n\n    return response.data;\n  },\n  (error) => {\n    \u002F\u002F 失败回调，处理http网络错误\n    \u002F\u002F 定义一个变量：存储网络错误信息\n    let message = '';\n    \u002F\u002F http状态码\n    const status = error.response.status;\n    switch (status) {\n      case 401:\n        message = 'token过期';\n        break;\n      case 403:\n        message = '无权访问';\n        break;\n      case 404:\n        message = '请求地址错误';\n        break;\n      case 500:\n        message = '服务器出现问题';\n        break;\n      default:\n        message = '网络出现问题';\n        break;\n    }\n    \u002F\u002F 使用 Element Plus 的 ElMessage 显示错误信息\n    ElMessage({\n      type: 'error',\n      message: message,\n    });\n    return Promise.reject(error); \u002F\u002F继续向下传递错误\n  }\n);\n\u002F\u002F 对外暴露\nexport default request;\n\n```\n\n## 3.8 API 接口统一管理：清晰的接口结构\n\n为了避免接口地址硬编码、提高代码可读性和维护性，项目采用了统一的 API 接口管理方式。\n\n**在 `src` 目录下创建 `api` 文件夹，并按模块（如 `user`、`product`、`acl`）进行分类管理。**\n\n**`src\u002Fapi\u002Fuser\u002Findex.ts` 示例：**\n\n```typescript\n\u002F\u002F 统一管理项目用户相关的接口\nimport request from '@\u002Futils\u002Frequest';\n\nimport type { loginForm, loginResponse, userResponseData } from '.\u002Ftype';\n\u002F\u002F 统一管理接口\nenum API {\n  LOGIN_URL = '\u002Fuser\u002Flogin',\n  USERINFO_URL = '\u002Fuser\u002Finfo',\n}\n\u002F\u002F 暴露请求函数\n\u002F\u002F 登录接口方法\nexport const reqLogin = (data: loginForm) => request.post\u003Cany, loginResponse>(API.LOGIN_URL, data);\n\u002F\u002F 获取用户信息接口方法\nexport const reqUserInfo = () => request.get\u003Cany, userResponseData>(API.USERINFO_URL);\n\n```\n\n**`src\u002Fapi\u002Fuser\u002Ftype.ts` 示例 (定义数据类型)：**\n\n```typescript\n\u002F\u002F 登录接口需要携带参数ts类型\nexport interface loginForm {\n  username: string;\n  password: string;\n}\n\ninterface dataType {\n  token: string;\n}\n\u002F\u002F 登录接口返回的数据类型\nexport interface loginResponse {\n  code: number;\n  data: dataType;\n}\n\ninterface userInfo {\n  userId: number;\n  avater: string;\n  username: string;\n  password: string;\n  desc: string;\n  roles: string[];\n  buttons: string[];\n  routes: string[];\n  token: string;\n}\n\u002F\u002F 定义服务器返回用户信息相关的数据类型\ninterface user {\n  checkUser: userInfo;\n}\nexport interface userResponseData {\n  code: number;\n  data: user;\n}\n\n```\n\n这种模块化的接口管理方式，使得接口定义清晰、易于查找和维护，并且结合 TypeScript 提供了强大的类型检查，进一步提升了代码质量。\n\n\n\n# 四，项目的实现\n\n## 4.1 路由配置\n\n用 vue-router@4 配置路由\n\n安装\n\n`pnpm install vue-router`\n\n`src\u002Fviews`新建三个页面组件文件\u002Flogin\u002Findex.vue，\u002Fhome\u002Findex.vue，\u002F404\u002Findex.vue\n\n`src\u002Frouter\u002Findex.ts`创建路由器(路由器配置)\n\n```typescript\n\u002F\u002F 通过vue-router插件实现模板路由配置\nimport { createRouter, createWebHashHistory } from 'vue-router';\n\nimport { constantRoute } from '.\u002Frouters';\n\u002F\u002F创建路由器\nconst router = createRouter({\n  \u002F\u002F 使用 hash 模式（URL 带 #）\n  history: createWebHashHistory(),\n  \u002F\u002F 路由规则数组  \n  routes: constantRoute,\n  \u002F\u002F 页面切换时的滚动行为\n  scrollBehavior() {\n    return {\n      left: 0,\n      top: 0,\n    };\n  },\n});\n\nexport default router;\n\n```\n\n`src\u002Frouter\u002Frouters.ts`配置路由（路由规则的定义）\n\n```typescript\n\u002F\u002F 对外暴漏配置路由\nexport const constantRoute = [\n  {\n    \u002F\u002F 登录\n     \u002F\u002FURL 路径\n    path: '\u002Flogin',\n      \u002F\u002F 对应的页面组件\n    component: () => import('@\u002Fviews\u002Flogin\u002Findex.vue'),\n      \u002F\u002F 路由名称（用于编程式导航）\n    name: 'login',\n  },\n  {\n    \u002F\u002F 登录成功以后展示数据的路由\n    path: '\u002F',\n    component: () => import('@\u002Fviews\u002Fhome\u002Findex.vue'),\n    name: 'layout',\n      children: [\n  {\n    path: 'home',  \u002F\u002F 子路由写相对路径\n    component: () => import('@\u002Fviews\u002Fhome\u002Findex.vue'),\n    meta: {\n      title: '首页',\n      hidden: false,\n    },\n  },\n  },\n  {\n    \u002F\u002F404\n    path: '\u002F404',\n    component: () => import('@\u002Fviews\u002F404\u002Findex.vue'),\n    name: '404',\n  },\n  \u002F\u002F 重定向到其他路由\n  {\n    path: '\u002F:pathMatch(.*)*',\n    redirect: '\u002F404',\n    name: 'Any',\n  },\n];\n\n```\n\n`注意：懒加载特性`\n\n语法 () => import('组件路径') \n\n作用 按需加载页面组件 \n\n优势 减少首次加载时间、提升用户体验 \n\n打包 每个页面单独打包 \n\n推荐 所有路由都使用懒加载\n\n**子路由路径**\n\n子路由一律写相对路径\n\n\n\n`Vue Router 的两种模式`\n\n**Hash 模式 （当前使用）**\n\n```typescript\nimport { createWebHashHistory } from 'vue-router';\n\nconst router = createRouter({\n  history: createWebHashHistory(),\n  routes: constantRoute,\n});\n```\n\n```text\nurl风格\t带 # 号\nhttp:\u002F\u002Flocalhost:5173\u002F#\u002Flogin\nhttp:\u002F\u002Flocalhost:5173\u002F#\u002Fhome\nhttp:\u002F\u002Flocalhost:5173\u002F#\u002Fabout\n```\n\n**History 模式**\n\n```typescript\nimport { createWebHistory } from 'vue-router';\n\nconst router = createRouter({\n  history: createWebHistory(),\n  routes: constantRoute,\n});\n```\n\n```reStructuredText\nurl风格\nhttp:\u002F\u002Flocalhost:5173\u002Flogin\nhttp:\u002F\u002Flocalhost:5173\u002Fhome\nhttp:\u002F\u002Flocalhost:5173\u002Fabout\n```\n\n## 4.2 登录静态页面\n\n静态页面搭建\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"login_container\">\n        \u003Cel-row>\n            \u003Cel-col :span=\"12\" :xs=\"0\">\u003C\u002Fel-col>\n            \u003Cel-col :span=\"12\" :xs=\"24\">\n                \u003Cel-form class=\"login_form\" :model=\"loginForm\">\n                    \u003Ch1>Hello\u003C\u002Fh1>\n                    \u003Ch2>欢迎来来到硅谷甄选\u003C\u002Fh2>\n                    \u003Cel-form-item>\n                        \u003Cel-input v-model=\"loginForm.username\" placeholder=\"请输入用户名\" :prefix-icon=\"User\">\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n                    \u003Cel-form-item>\n                        \u003Cel-input v-model=\"loginForm.password\" placeholder=\"请输入密码\" :prefix-icon=\"Lock\" type=\"password\"\n                            show-password>\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n                    \u003Cel-form-item>\n                        \u003Cel-button class=\"login_btn\" type=\"primary\" size=\"default\">登录\u003C\u002Fel-button>\n                    \u003C\u002Fel-form-item>\n                \u003C\u002Fel-form>\n            \u003C\u002Fel-col>\n        \u003C\u002Fel-row>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { Lock, User } from '@element-plus\u002Ficons-vue'\nimport { reactive } from 'vue';\n\n\u002F\u002F手机账号和密码的数据\nconst loginForm = reactive({\n    username: 'admin',\n    password: '111111'\n})\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n.login_container {\n    width: 100%;\n    height: 100vh;\n    \u002F\u002F background-color: skyblue;\n    background: url('@\u002Fassets\u002Fimages\u002Fbackground.jpg') no-repeat;\n    background-size: cover;\n}\n\n.login_form {\n    position: relative;\n    width: 70%;\n    top: 30vh;\n    background: url('@\u002Fassets\u002Fimages\u002Flogin_form.png');\n    padding: 40px;\n}\n\nh1 {\n    color: white;\n    font-size: 40px;\n}\n\nh2 {\n    font-size: 20px;\n    color: white;\n    margin: 20px 0px;\n}\n\n.login_btn {\n    width: 100%;\n}\n\u003C\u002Fstyle>\n\n```\n\n![image-20260203161820084](images\u002Fimage-20260203161820084.png)\n\n### 4.2.1 什么是栅格布局（先把“感觉”建立起来）\n\n 1️⃣栅格布局 = 把一行切成很多等宽的小格子\n\n想象一条横线：\n\n```\n|--------------------------------|\n```\n\nElement Plus 规定：\n\n> **一行 = 24 个小格子**\n\n```\n|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|\n```\n\n2️⃣ 页面里的任何一块内容\n\n本质上就是：\n\n> **“我占这一行的第几格到第几格”**\n\n### 4.2.2 el-row \u002F el-col 在干什么（核心）\n\nel-row 是什么？\n\n👉 **一条“横着的参考线”**\n\n```\n\u003Cel-row>\n  ...\n\u003C\u002Fel-row>\n```\n\n它只干一件事：\n\n> 告诉浏览器：\n>  **现在开始按 24 栅格来分配宽度**\n\nel-col 是什么？\n\n👉 **在这一行里占多少格**\n\n```\n\u003Cel-col :span=\"6\">\u003C\u002Fel-col>\n```\n\n意思是：\n\n```\n我占 6 \u002F 24 = 25% 宽度\n```\n\n### 4.2.3 span 是怎么工作的（一定要吃透）\n\n一行总宽 = 24\n\n| span | 占比  |\n| ---- | ----- |\n| 24   | 100%  |\n| 12   | 50%   |\n| 8    | 33%   |\n| 6    | 25%   |\n| 4    | 16.6% |\n\n例子 1：左右两栏\n\n```\n\u003Cel-row>\n  \u003Cel-col :span=\"12\">左\u003C\u002Fel-col>\n  \u003Cel-col :span=\"12\">右\u003C\u002Fel-col>\n\u003C\u002Fel-row>\n```\n\n视觉效果：\n\n```\n| 左 左 左 左 左 左 | 右 右 右 右 右 右 |\n```\n\n### 4.2.4 响应式是怎么回事（重点）\n\n屏幕不是只有一种大小\n\n- 手机 📱\n- 平板 💻\n- 电脑 🖥️\n\n👉 所以 **同一列，在不同屏幕占的宽度可以不一样**\n\n------\n\nElement Plus 的断点（你只要记 4 个）\n\n| 属性 | 含义     |\n| ---- | -------- |\n| xs   | 手机     |\n| sm   | 平板     |\n| md   | 普通电脑 |\n| lg   | 大屏     |\n| xl   | 超大屏   |\n\n### 4.2.5 例子：你现在的登录页（拆解）\n\n```\n\u003Cel-col :span=\"12\" :xs=\"0\">\u003C\u002Fel-col>\n\u003Cel-col :span=\"12\" :xs=\"24\">\u003C\u002Fel-col>\n```\n\n在电脑上：\n\n```\n| 左空白 12 | 登录表单 12 |\n```\n\n在手机上：\n\n```\n| 登录表单 24 |\n```\n\n左边那一列直接消失（`xs=0`）\n\n\n\n## 4.3 模板封装登录业务\n\n`安装pinia管理数据`\n\n`npm i pinia`\n\n**src下新建store文件**\n\n**创建大仓库store\u002Findex.ts**\n\n```typescript\n\u002F\u002F 大仓库\nimport { createPinia } from 'pinia';\n\u002F\u002F 创建大仓库\nconst pinia = createPinia();\n\u002F\u002F对外暴露，入口文件需要安装仓库\nexport default pinia;\n\n```\n\n**创建小仓库store\u002Fmodules\u002Fuser.ts**\n\n```typescript\n\u002F\u002F 选项式api\n\u002F\u002F 创建用户相关的小仓库\nimport { defineStore } from 'pinia';\n\u002F\u002F创建用户小仓库\nconst useUserStore = defineStore('User', {\n  \u002F\u002F 小仓库存储数据的地方\n  state: () => {\n    return {};\n  },\n  \u002F\u002F 异步|逻辑的地方\n  actions: {},\n  getters: {},\n});\n\n\u002F\u002F 对外暴露获取小仓库方法\nexport default useUserStore;\n\n\n\u002F\u002F 组合式api\nimport { defineStore } from 'pinia';\nimport { ref, computed } from 'vue';\n\nconst useUserStore = defineStore('User', () => {\n  \u002F\u002Fstate\n  const user = ref(null);\n\n  \u002F\u002Fgetters\n\n  \u002F\u002Factions\n\n\u002F\u002F 对外暴露state,getters,actions\n})\nexport default useUserStore;\n\n\n```\n\n**src\u002Fmodules\u002Fuser.ts选项式api写法**\n\n```typescript\n\u002F\u002F 创建用户相关的小仓库\nimport { el } from 'element-plus\u002Fes\u002Flocales.mjs';\nimport { defineStore } from 'pinia';\n\n\u002F\u002F 引入接口\nimport { reqLogin } from '@\u002Fapi\u002Fuser';\n\u002F\u002F 引入数据类型\nimport type { loginForm } from '@\u002Fapi\u002Fuser\u002Ftype';\n\u002F\u002F创建用户小仓库\nconst useUserStore = defineStore('User', {\n  \u002F\u002F 小仓库存储数据的地方\n  state: () => {\n    return {\n      token: localStorage.getItem('TOKEN'), \u002F\u002F用户唯一标识token\n    };\n  },\n  \u002F\u002F 异步|逻辑的地方\n  actions: {\n    \u002F\u002F用户登录的方法\n    async userLogin(data: loginForm) {\n      \u002F\u002F登录请求\n      const result = await reqLogin(data);\n      \u002F\u002F 登录请求:成功200->token\n      \u002F\u002F 登录请求:失败201->登录失败错误的信息\n      if (result.code === 200) {\n        \u002F\u002F pinia仓库存储一下token\n        this.token = result.data.token;\n        \u002F\u002F由于pinia\u002Fvuex存储数据其实利用js对象\n        \u002F\u002F本地存储持久化存储一份\n        localStorage.setItem('TOKEN', result.data.token);\n        \u002F\u002F 能保证当前async函数返回一个成功的promise\n        return 'ok';\n      } else {\n        return Promise.reject(new Error(result.data.message));\n      }\n    },\n  },\n  getters: {},\n});\n\n\u002F\u002F 对外暴露获取小仓库方法\nexport default useUserStore;\n\n```\n\n\n\n**views\u002Flogin\u002Findex.vue 登录页面代码**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"login_container\">\n        \u003Cel-row>\n            \u003Cel-col :span=\"12\" :xs=\"0\">\u003C\u002Fel-col>\n            \u003Cel-col :span=\"12\" :xs=\"24\">\n                \u003Cel-form class=\"login_form\" :model=\"loginForm\">\n                    \u003Ch1>Hello\u003C\u002Fh1>\n                    \u003Ch2>欢迎来来到硅谷甄选\u003C\u002Fh2>\n                    \u003Cel-form-item>\n                        \u003Cel-input v-model=\"loginForm.username\" placeholder=\"请输入用户名\" :prefix-icon=\"User\">\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n                    \u003Cel-form-item>\n                        \u003Cel-input v-model=\"loginForm.password\" placeholder=\"请输入密码\" :prefix-icon=\"Lock\" type=\"password\"\n                            show-password>\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n                    \u003Cel-form-item>\n                        \u003Cel-button class=\"login_btn\" type=\"primary\" size=\"default\" @click=\"login\"\n                            :loading=\"loading\">登录\u003C\u002Fel-button>\n                    \u003C\u002Fel-form-item>\n                \u003C\u002Fel-form>\n            \u003C\u002Fel-col>\n        \u003C\u002Fel-row>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { Lock, User } from '@element-plus\u002Ficons-vue';\nimport { ElNotification } from 'element-plus';\nimport { reactive, ref } from 'vue';\nimport { useRouter } from 'vue-router';\n\n\u002F\u002F 引入用户相关的小仓库\nimport useUserStore from '@\u002Fstore\u002Fmodules\u002Fuser';\nimport { fa } from 'element-plus\u002Fes\u002Flocales.mjs';\n\nlet useStore = useUserStore();\n\u002F\u002F 获取路由器\nlet $router = useRouter();\n\u002F\u002F 定义加载效果开始为false\nlet loading = ref(false)\n\u002F\u002F收集账号和密码的数据\nconst loginForm = reactive({\n    username: 'admin',\n    password: '111111',\n});\n\u002F\u002F 登录按钮的回调\nconst login = async () => {\n    \u002F\u002F 点击登录出现加载效果\n    loading.value = true\n    \n    \u002F\u002F 点击登录按你以后能干什么？\n    \u002F\u002F 通知仓库发登录请求\n    \u002F\u002F 请求成功->首页展示数据的地方\n    \u002F\u002F 请求失败->弹出登录失败信息\n    \n    try {\n        \u002F\u002F调用用户仓库的登录方法，发送登录请求，等待返回\n        await useStore.userLogin(loginForm);\n        \u002F\u002F登录成功提示信息\n        ElNotification({\n            type: 'success',\n            message: '登录成功',\n        });\n        \u002F\u002F编程式导航跳转到展示数据首页\n        $router.push('\u002F');\n        \u002F\u002F登录成功，加载按钮也消失\n        loading.value = false\n\n    } catch (error) {\n        \u002F\u002F 如果登录请求失败（如账号密码错误或网络错误），执行这里的代码\n        \u002F\u002F 关闭加载状态，隐藏“加载中”\n        loading.value = false\n        ElNotification({\n            type: 'error',\n            message: (error as Error).message, \u002F\u002F 告诉 TS error 是 Error 类型(断言)\n        });\n\n    }\n};\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n.login_container {\n    width: 100%;\n    height: 100vh;\n    \u002F\u002F background-color: skyblue;\n    background: url('@\u002Fassets\u002Fimages\u002Fbackground.jpg') no-repeat;\n    background-size: cover;\n}\n\n.login_form {\n    position: relative;\n    width: 70%;\n    top: 30vh;\n    background: url('@\u002Fassets\u002Fimages\u002Flogin_form.png');\n    padding: 40px;\n}\n\nh1 {\n    color: white;\n    font-size: 40px;\n}\n\nh2 {\n    font-size: 20px;\n    color: white;\n    margin: 20px 0px;\n}\n\n.login_btn {\n    width: 100%;\n}\n\u003C\u002Fstyle>\n\n```\n\n\n\n## 4.4 用户仓库数据ts类型定义与封装token\n\n**定义用户仓库state数据类型store\u002Fmodules\u002Ftypes\u002Ftype.ts**\n\n```typescript\nexport interface UserState {\n  token: string | null;\n}\n\n```\n\n**修改store\u002Fmodules\u002Fuser.ts中store数据类型**\n\n```typescript\n\u002F\u002F 创建用户相关的小仓库\nimport { defineStore } from 'pinia';\n\n\u002F\u002F 引入接口\n\nimport { reqLogin } from '@\u002Fapi\u002Fuser';\n\n\u002F\u002F 引入操作存储的工具方法\nimport { SET_TOKEN, GET_TOKEN } from '@\u002Futils\u002Ftoken';\n\n\u002F\u002F 引入数据类型\nimport type { loginForm, loginResponseData } from '@\u002Fapi\u002Fuser\u002Ftype';\n\nimport type { UserState } from '.\u002Ftypes\u002Ftype';\n\n\u002F\u002F创建用户小仓库\nconst useUserStore = defineStore('User', {\n    \n  \u002F\u002F 小仓库存储数据的地方 使用UserState规范state数据类型\n  state: (): UserState => {\n    return {\n      token: GET_TOKEN(), \u002F\u002F用户唯一标识token\n    };\n  }\n```\n\n**封装本地存储，存储数据与读取数据方法**\n\n用于用户登录后的身份验证\n\n优势：\n\n- 代码简洁\n- 统一管理\n- 易于维护\n- 类型安全\n\n**新建utils\u002Ftoken.ts**\n\n```typescript\n\u002F\u002F 封装本地存储存储数据与读取数据方法\n\n\u002F\u002F 存储数据\nexport const SET_TOKEN = (token: string) => {\n  localStorage.setItem('TOKEN', token);\n};\n\u002F\u002F 本地存储获取数据\nexport const GET_TOKEN = () =>{\n    return localStorage.getItem('TOKEN'),\n}\n```\n\n**修改store\u002Fmodules\u002Fuser.ts中存储和获取token方法**\n\n```typescript\n\u002F\u002F 创建用户相关的小仓库\nimport { defineStore } from 'pinia';\n\n\u002F\u002F 引入接口\n\nimport { reqLogin } from '@\u002Fapi\u002Fuser';\n\n\u002F\u002F 引入操作存储的工具方法\nimport { SET_TOKEN, GET_TOKEN } from '@\u002Futils\u002Ftoken';\n\n\u002F\u002F 引入数据类型\nimport type { loginForm, loginResponseData } from '@\u002Fapi\u002Fuser\u002Ftype';\n\nimport type { UserState } from '.\u002Ftypes\u002Ftype';\n\n\u002F\u002F创建用户小仓库\nconst useUserStore = defineStore('User', {\n  \u002F\u002F 小仓库存储数据的地方\n  state: (): UserState => {\n    return {\n      token: GET_TOKEN(), \u002F\u002F用户唯一标识token\n    };\n  },\n  \u002F\u002F 异步|逻辑的地方\n  actions: {\n    \u002F\u002F用户登录的方法\n    async userLogin(data: loginForm) {\n      \u002F\u002F登录请求\n      const result: loginResponseData = await reqLogin(data);\n      \u002F\u002F 登录请求:成功200->token\n      \u002F\u002F 登录请求:失败201->登录失败错误的信息\n      if (result.code === 200) {\n        \u002F\u002F pinia仓库存储一下token\n        this.token = result.data.token as string;\n        \u002F\u002F由于pinia\u002Fvuex存储数据其实利用js对象\n        \u002F\u002F本地存储持久化存储一份\n        SET_TOKEN(result.data.token as string);\n        \u002F\u002F 能保证当前async函数返回一个成功的promise\n        return 'ok';\n      } else {\n        return Promise.reject(new Error(result.data.message));\n      }\n    },\n  },\n  getters: {},\n});\n\n\u002F\u002F 对外暴露获取小仓库方法\nexport default useUserStore;\n\n```\n\n## 4.5 登录模板表单校验\n\n登录表单校验的作用，是在前端提前拦截不合法输入，提升用户体验、减少无效请求、让系统更安全、更专业。\n\n**表单绑定model**\n\n```vue\n\u003Cel-form class=\"login_form\" :model=\"loginForm\" :rules=\"rules\" ref=\"loginForms\">\u003C\u002Fel-form>\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F收集账号和密码的数据\nconst loginForm = reactive({\n    username: 'admin',\n    password: '111111',\n});\n\u003C\u002Fscript>\n```\n\n**定义校验规则（rules），并且绑定rules**\n\n```vue\n\u003Cel-form class=\"login_form\" :model=\"loginForm\" :rules=\"rules\" ref=\"loginForms\">\u003C\u002Fel-form>\n\n\u003Cscript setup lang=\"ts\">\n \t\u002F\u002F 定义表单校验需要配置对象\nconst rules = {\n    \u002F\u002F规则对象属性\n    \u002F\u002F required,代表这个字段务必要校验的\n    \u002F\u002F min:文本长度至少多少位\n    \u002F\u002F max:文本长度最多多少位\n    \u002F\u002F message:错误的提示信息\n    \u002F\u002F trigger:触发校验表单的时机，change->文本发生变化触发校验，blur->失去焦点校验\n    username: [\n        { required: true, min: 6, max: 10, message: '账号长度至少六位，最多十位', trigger: 'change' }\n    ],\n    password: [\n        { required: true, min: 6, message: '密码至少六位', trigger: 'change' }\n    ]\n}\n\u003C\u002Fscript>\n```\n\n**在 el-form-item 上写 `prop`**\n\n```vue\n\u003Cel-form-item prop=\"username\">\n                        \u003Cel-input v-model=\"loginForm.username\" placeholder=\"请输入用户名\" :prefix-icon=\"User\">\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n\u003Cel-form-item prop=\"password\">\n                        \u003Cel-input v-model=\"loginForm.password\" placeholder=\"请输入密码\" :prefix-icon=\"Lock\" type=\"password\"\n                            show-password>\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n```\n\n**给 el-form 设置 ref（为了手动触发校验，validate的使用）**\n\n```vue\n\u003Cel-form class=\"login_form\" :model=\"loginForm\" :rules=\"rules\" ref=\"loginForms\">\u003C\u002Fel-form>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F获取el-form组件\nlet loginForms = ref()\n\n\u002F\u002F 登录按钮的回调\nconst login = async () => {\n    \u002F\u002F保证全部表单相校验通过在发请求\n    await loginForms.value.validate()    \n    \u002F\u002F 点击登录出现加载效果\n    loading.value = true\n    \u002F\u002F 点击登录按你以后能干什么？\n    \u002F\u002F 通知仓库发登录请求\n    \u002F\u002F 请求成功->首页展示数据的地方\n    \u002F\u002F 请求失败->弹出登录失败信息\n    try {\n        \u002F\u002F 保证登录成功\n        useStore.userLogin(loginForm);\n        \u002F\u002F编程式导航跳转到展示数据首页\n        \u002F\u002F登录成功提示信息\n        ElNotification({\n            type: 'success',\n            message: '登录成功',\n        });\n        \u002F\u002F编程式导航跳转到展示数据首页\n        $router.push('\u002F');\n        \u002F\u002F登录成功，加载按钮也消失\n        loading.value = false\n\n    } catch (error) {\n        \u002F\u002F 登录失败 加载效果消失\n        loading.value = false\n        ElNotification({\n            type: 'error',\n            message: (error as Error).message,\n        });\n\n    }\n};\n\u003C\u002Fscript>\n\n\nvalidate() 是一个 Promise\n成功时（表单校验通过）：Promise 变成 resolved，await validate() 后代码继续执行\n失败时（有校验不通过）：Promise 变成 rejected，await validate() 抛出异常，可以用 catch 捕获\n\nVue 3 的 ref + template ref 机制\n你在模板里给某个组件或元素加上 ref=\"loginForms\"，Vue 会把对应的组件实例或 DOM 元素，自动赋值给你定义的 loginForms 变量。\nVue 编译模板时，发现 ref=\"loginForms\"\n运行时，会把 \u003Cel-form> 的实例赋给 loginForms.value\n```\n\n### 4.5.1 自定义校验规则使用（可写正则）\n\n**给 `\u003Cel-form-item>` 设置 `prop`**\n\n这个 `prop` 对应表单绑定数据的字段名（`loginForm.username`）\n\n**在rules对象中自定义校验规则**\n\n```typescript\nconst rules = {\n  username: [\n    { trigger: 'change', validator: validatorUserName }\n  ],\n  password: [\n    { trigger: 'change', validator: validatorPassword }\n  ]\n}\n\n```\n\n**写自定义校验函数**\n\n函数签名固定，接受三个参数\n\n```typescript\nconst validatorUserName = (rule, value, callback) => {\n  \u002F\u002F rule：校验规则对象\n  \u002F\u002F value：当前表单字段的值\n  \u002F\u002F callback：校验结果的回调\n};\n\u002F\u002F 自定义校验规则\nconst validatorUserName = (rule: any, value: any, callback: any) => {\n    \u002F\u002F rule:即为校验规则对象\n    \u002F\u002F value：即为表单元素文本内容\n    \u002F\u002F 函数：如果符合条件callback放行通过\n    \u002F\u002F 如果不符合条件callback方法，注入错误提示信息\n    \u002F\u002F 可以写正则\n    if (value.length >= 5) {\n        callback()\n    } else {\n        callback(new Error('账号长度至少五位'))\n    }\n}\n```\n\n**`把rules` 传给 `\u003Cel-form>` 组件**\n\n\n\n## 4.6 _layout组件的静态搭建\n\n**创建src\u002Flayout\u002Findex.vue**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"layout_container\">\n        \u003C!-- 左侧菜单 -->\n        \u003Cdiv class=\"layout_slider\">123\u003C\u002Fdiv>\n        \u003C!-- 顶部导航 -->\n        \u003Cdiv class=\"layout_tabbar\">456\u003C\u002Fdiv>\n        \u003C!-- 内容展示区域 -->\n        \u003Cdiv class=\"layout_main\">\n\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n.layout_container {\n    width: 100%;\n    height: 100vh;\n    background-color: skyblue;\n    \n.layout_slider {\n    width: $base-menu-width;\n    height: 100vh;\n    background-color: $base-menu-background;\n}\n\n.layout_tabbar {\n    position: fixed;\n    left: $base-menu-width ;\n    top: 0;\n    width: calc(100% - $base-menu-width);\n    height: $base-tabbar-height;\n    background-color: springgreen;\n}\n\n.layout_main {\n    position: absolute;\n    width: calc(100% - $base-menu-width);\n    height: calc(100vh - $base-tabbar-height );\n    background-color: red;\n    left: $base-menu-width ;\n    top: $base-tabbar-height;\n    padding: 20px;\n    overflow: auto;\n}\n    }\n    \n\u003C\u002Fstyle>\n```\n\n**配置全局SCSS样式 src\u002Fstyles\u002Fvariable.scss**\n\n```scss\n\u002F\u002F 项目提供scss全局变量\n\u002F\u002F 左侧的菜单的宽度\n$base-menu-width:260px;\n\u002F\u002F 左侧菜单的背景颜色\n$base-menu-background:#00152b;\n\u002F\u002F 顶部导航的高度\n$base-tabbar-height:50px;\n\n```\n\n**滚动条设置src\u002Fstyles\u002Findex.scss**\n\n```scss\n\u002F\u002F 滚动条外观设置\n::-webkit-scrollbar {\n    width: 10px;\n}\n\n::-webkit-scrollbar-track{\n    background-color: $base-menu-background;\n}\n\n::-webkit-scrollbar-thumb{\n    width: 10px;\n    background-color: yellow;\n    border-radius: 10px;\n}\n\n```\n\n**修改src\u002Frouter\u002Frouters.ts**\n\n将登录成功后看到的页面\n\n```typescript\n \u002F\u002F 登录成功以后展示数据的路由\n    path: '\u002F',\n    component: () => import('@\u002Flayout\u002Findex.vue'),\n    name: 'layout',\n  },\n```\n\n\n\n## 4.7 logo组件的封装\n\n封装 Logo 是为了用配置统一控制显示与样式，做到一处修改、全局生效，提升代码可维护性。\n\n**新建src\u002Fsetting.ts用来管理配置logo和标题**\n\n```typescript\n\u002F\u002F 用于项目logo,标题的配置\nexport default {\n  title: '硅谷甄选运营平台', \u002F\u002F 项目的标题\n  logo: 'public\u002Fimage.png', \u002F\u002F 项目的logo\n  logoHidden: true, \u002F\u002F logo组件是否隐藏设置\n};\n\n```\n\n**新建src\u002Flayout\u002Flogo\u002Findex.vue**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"logo\" v-if=\"setting.logoHidden\">\n        \u003Cimg :src='setting.logo' alt=\"\">\n        \u003Cp>{{ setting.title }}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 引入设置标题与logo这配置文件\nimport setting from '@\u002Fsetting';\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n    \n.logo {\n    width: 100%;\n    height: $base-menu-logo-height;\n    color: white;\n    display: flex;\n    align-items: center;\n    font-size: $base-logo-tittle-fontsize;\n    padding: 10px;\n\nimg {\n    width: 40px;\n    height: 40px;\n}\n\np {\n    margin-left: 10px;\n}\n    }\n\u003C\u002Fstyle>\n```\n\n注意：样式收敛到 logo 内，这样以后你在这个组件里再加别的 `img \u002F p` 也不会乱。\n\n**src\u002Flayout\u002Findex.vue引入logo组件**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"layout_container\">\n        \u003C!-- 左侧菜单 -->\n        \u003Cdiv class=\"layout_slider\">\n            \u003CLogo>\u003C\u002FLogo>\n        \u003C\u002Fdiv>\n        \u003C!-- 顶部导航 -->\n        \u003Cdiv class=\"layout_tabbar\">456\u003C\u002Fdiv>\n        \u003C!-- 内容展示区域 -->\n        \u003Cdiv class=\"layout_main\">\n\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\u003Cscript setup lang=\"ts\">\nimport Logo from '.\u002Flogo\u002Findex.vue'\n\u003C\u002Fscript>\n```\n\n**全局variable.scss样式设置**\n\n```scss\n\u002F\u002F左侧菜单logo高度设置\n$base-menu-logo-height:50px;\n\u002F\u002F左侧菜单logo右侧文字大小\n$base-logo-tittle-fontsize:20px;\n\n```\n\n\n\n## 4.8 左侧菜单静态搭建\n\n**src\u002Flayout\u002Findex.vue**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"layout_container\">\n        \u003C!-- 左侧菜单 -->\n        \u003Cdiv class=\"layout_slider\">\n            \u003CLogo>\u003C\u002FLogo>\n            \u003C!-- 展示菜单 -->\n            \u003C!-- 滚动菜单 -->\n            \u003Cel-scrollbar class=\"scrollbar\">\n                \u003C!-- 菜单组件 -->\n                \u003Cel-menu class=\"el_menu\">\n                    \u003Cel-menu-item index=\"1\">首页\u003C\u002Fel-menu-item>\n                    \u003Cel-menu-item index=\"2\">数据大屏\u003C\u002Fel-menu-item>\n                    \u003C!-- 折叠菜单 -->\n                    \u003Cel-sub-menu index=\"3\">\n                        \u003Ctemplate #title>\n                            \u003Cspan>权限管理\u003C\u002Fspan>\n                        \u003C\u002Ftemplate>\n                        \u003Cel-menu-item index=\"3-1\">用户管理\u003C\u002Fel-menu-item>\n                        \u003Cel-menu-item index=\"3-2\">角色管理\u003C\u002Fel-menu-item>\n                        \u003Cel-menu-item index=\"3-3\">菜单管理\u003C\u002Fel-menu-item>\n                    \u003C\u002Fel-sub-menu>\n                \u003C\u002Fel-menu>\n            \u003C\u002Fel-scrollbar>\n        \u003C\u002Fdiv>\n        \u003C!-- 顶部导航 -->\n        \u003Cdiv class=\"layout_tabbar\">456\u003C\u002Fdiv>\n        \u003C!-- 内容展示区域 -->\n        \u003Cdiv class=\"layout_main\">\n\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport Logo from '.\u002Flogo\u002Findex.vue'\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n.layout_container {\n    width: 100%;\n    height: 100vh;\n    color: white;\n\n    .layout_slider {\n        width: $base-menu-width;\n        height: 100vh;\n        background-color: $base-menu-background;\n\n        .scrollbar {\n            width: 100%;\n            height: calc(100vh - $base-menu-logo-height);\n\n            .el_menu {\n                --el-menu-bg-color: #00152b;\n                --el-menu-text-color: #fff;\n            }\n        }\n    }\n\n    .layout_tabbar {\n        position: fixed;\n        left: $base-menu-width ;\n        top: 0;\n        width: calc(100% - $base-menu-width);\n        height: $base-tabbar-height;\n        background-color: springgreen;\n    }\n\n    .layout_main {\n        position: absolute;\n        width: calc(100% - $base-menu-width);\n        height: calc(100vh - $base-tabbar-height );\n        background-color: red;\n        left: $base-menu-width ;\n        top: $base-tabbar-height;\n        padding: 20px;\n        overflow: auto;\n    }\n}\n\u003C\u002Fstyle>\n```\n\n### 4.8.1 滚动菜单的使用（`el-scrollbar`）\n\n当侧边菜单内容过多，超出固定高度时，使用 `el-scrollbar` 给菜单区域增加自定义滚动条，避免页面整体滚动。\n\n```vue\n典型用法结构\n\n\u003Cel-scrollbar class=\"scrollbar\">\n  \u003Cel-menu class=\"menu\">\n    \u003Cel-menu-item index=\"1\">首页\u003C\u002Fel-menu-item>\n    \u003Cel-sub-menu index=\"2\">\n      \u003Ctemplate #title>\n        \u003Cspan>权限管理\u003C\u002Fspan>\n      \u003C\u002Ftemplate>\n      \u003Cel-menu-item index=\"2-1\">用户管理\u003C\u002Fel-menu-item>\n    \u003C\u002Fel-sub-menu>\n  \u003C\u002Fel-menu>\n\u003C\u002Fel-scrollbar>\n\n样式配合（关键点）\nel-scrollbar 高度固定或计算，保证菜单超出时出现滚动条\n\u003Cstyle scoped lang=\"scss\">\n    .scrollbar {\n \t\t height: calc(100vh - $base-menu-logo-height);\n \t\t \u002F* 高度 = 全屏高度 - logo 高度 *\u002F\n}\n\u003C\u002Fstyle>\n```\n\n**注意：**\n\n`el-scrollbar` 包裹的内容区域高度要超过自身高度，滚动条才会出现\n\n`el-scrollbar` 只能有一个直接子元素（通常是菜单容器）\n\n保证菜单组件的高度不要超出 `el-scrollbar` 的高度\n\n### 4.8.2 el-menu组件的使用\n\n* 普通菜单（`el-menu-item`）\n\n  没有下级菜单的入口\n\n  ```vue\n  \u003Cel-menu>\n  \t\u003Cel-menu-item index=\"1\">首页\u003C\u002Fel-menu-item>\n  \u003C\u002Fel-menu>\n  ```\n\n* 展开菜单 \u002F 子菜单（`el-sub-menu`）\n\n  有下级菜单，需要展开\u002F收起\n\n  ```vue\n  \u003Cel-menu>\n  \t\u003Cel-sub-menu index=\"2\">\n      具名插槽写子菜单的标题\n    \t\t\u003Ctemplate #title>\n      \t\t\u003Cspan>权限管理\u003C\u002Fspan>\n    \t\t\u003C\u002Ftemplate>\n  \n    \t\t\u003Cel-menu-item index=\"2-1\">用户管理\u003C\u002Fel-menu-item>\n    \t\t\u003Cel-menu-item index=\"2-2\">角色管理\u003C\u002Fel-menu-item>\n  \t\u003C\u002Fel-sub-menu>\n  \u003C\u002Fel-menu>\n  ```\n\n* 样式设置\n\n  ```scss\n  .menu {\n    border-right: none;  \u002F\u002F 去掉右侧边框\n    --el-menu-bg-color: #00152b;  \u002F\u002F 背景颜色\n    --el-menu-text-color: #fff;\t\u002F\u002F 文本颜色\n    --el-menu-hover-bg-color: #409eff;  \u002F\u002F 鼠标悬停颜色\n    --el-menu-active-color: #409eff;    \u002F\u002F 鼠标点击颜色\n  }\n  ```\n\n**注意：**\n\n`el-menu`：菜单容器\n\n`el-menu-item`：普通菜单项\n\n`el-sub-menu`：子菜单\n\n`index`：**唯一标识（必须）**\n\n### 4.8.3 递归组件生成动态菜单\n\n通过递归组件实现菜单的无限层级和结构复用，简化代码维护，提高扩展性和可读性\n\n\n\n**src\u002Frouter\u002Frouters.ts**\n\n在路由中起每个菜单的名字，并配置展示逻辑，写入子路由\n\n```typescript\n\u002F\u002F 对外暴漏配置路由\nexport const constantRoute = [\n  {\n    \u002F\u002F 登录\n    path: '\u002Flogin',\n    component: () => import('@\u002Fviews\u002Flogin\u002Findex.vue'),\n    name: 'login',\n    meta: {\n      title: '登录',\n      hidden: true, \u002F\u002F 代表路由标题在菜单中是否隐藏，true隐藏。false不隐藏\n    },\n  },\n  {\n    \u002F\u002F 登录成功以后展示数据的路由\n    path: '\u002F',\n    component: () => import('@\u002Flayout\u002Findex.vue'),\n    name: 'layout',\n    meta: {\n      title: 'layout',\n      hidden: false,\n    },\n    children: [\n      {\n        path: 'home',\n        component: () => import('@\u002Fviews\u002Fhome\u002Findex.vue'),\n        meta: {\n          title: '首页',\n          hidden: false,\n        },\n      },\n      {\n        path: 'text',\n        component: () => import('@\u002Fviews\u002Fhome\u002Findex.vue'),\n        meta: {\n          title: '测试',\n          hidden: false,\n        },\n      },\n    ],\n  },\n  {\n    \u002F\u002F404\n    path: '\u002F404',\n    component: () => import('@\u002Fviews\u002F404\u002Findex.vue'),\n    name: '404',\n    meta: {\n      title: '404',\n      hidden: true,\n    },\n  },\n  \u002F\u002F 重定向\n  {\n    path: '\u002F:pathMatch(.*)*',\n    redirect: '\u002F404',\n    name: 'Any',\n    meta: {\n      title: 'Any',\n      hidden: true,\n    },\n  },\n];\n\n\u002F\u002F 子路由一律写相对路径\n\n```\n\n\n\n**创建src\u002Flayout\u002Fmenu\u002Findex.vue**\n\n```vue\n\u003Ctemplate>\n  \u003Ctemplate v-for=\"(item, index) in menuList\" :key=\"item.path\">\n    \u003C!-- 没有子路由 -->\n    \u003Ctemplate v-if=\"!item.children\">\n      \u003Cel-menu-item :index=\"item.path\" v-if=\"!item.meta.hidden\">\n        \u003Cspan>标\u003C\u002Fspan>\n        \u003Cspan>{{ item.meta.title }}\u003C\u002Fspan>\n      \u003C\u002Fel-menu-item>\n    \u003C\u002Ftemplate>\n    \u003C!-- 有子路由但是只有一个子路由 -->\n    \u003Ctemplate v-if=\"item.children && item.children.length == 1\">\n      \u003Cel-menu-item :index=\"item.children[0].path\" v-if=\"!item.children[0].meta.hidden\">\n        \u003Cspan>{{ item.children[0].meta.title }}\u003C\u002Fspan>\n      \u003C\u002Fel-menu-item>\n    \u003C\u002Ftemplate>\n    \u003C!-- 有子路由且个数大于1 -->\n    \u003Cel-sub-menu :index=\"item.path\" v-if=\"item.children && item.children.length > 1\">\n      \u003Ctemplate #title>\n        \u003Cspan>{{ item.meta.title }}\u003C\u002Fspan>\n      \u003C\u002Ftemplate>\n      \u003C!-- 使用递归 -->\n      \u003C!-- 组件调用自己  处理无限层级、代码复用、易于维护 -->\n      \u003CSideMenu :menuList=\"item.children\">\u003C\u002FSideMenu>\n    \u003C\u002Fel-sub-menu>\n  \u003C\u002Ftemplate>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 给组件起名字\n\u002F\u002F 告诉 Vue：\n\u002F\u002F “递归时，请用 组件名注册表 去找自己”    \ndefineOptions({\n  name: 'SideMenu'\n})\n\u002F\u002F 有一个名为 menuList 的 props，来自父组件传递的数据\ndefineProps(['menuList']);\n\n\n\u003C\u002Fscript>\n\n\u003Cstyle scoped>\u003C\u002Fstyle>\n\n```\n\n**注意：**\n\n在 Vue 3 + `\u003Cscript setup>` 里：\n `defineOptions({ name: 'Menu' })`\n 就是用来“给组件起名字”的\n\n它等价于你以前写的：\n\n```vue\nexport default {\n  name: 'Menu'\n}\n```\n\n只是 **`script setup` 专用写法**。\n\n```vue\nv-if=\"!item.meta.hidden\" 写入是否展示逻辑，有的一级路由不需要展示,login,404,Any\n```\n\n**v-if 是 Vue 的指令，用来根据表达式的真假决定是否渲染这个节点。**\n\n\n\n**写入动态生成路由组件src\u002Flayout\u002Findex.vue**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"layout_container\">\n        \u003C!-- 左侧菜单 -->\n        \u003Cdiv class=\"layout_slider\">\n            \u003CLogo>\u003C\u002FLogo>\n            \u003C!-- 展示菜单 -->\n            \u003C!-- 滚动菜单 -->\n            \u003Cel-scrollbar class=\"scrollbar\">\n                \u003C!-- 菜单组件 -->\n                \u003Cel-menu class=\"el_menu\">\n                    \u003C!-- 根据路由动态生成菜单 -->\n                    \u003C!-- 给子组件传路由数组 -->\n                    \u003CSideMenu :menuList=\"useStore.menuRouters\">\u003C\u002FSideMenu> \n                \u003C\u002Fel-menu>\n            \u003C\u002Fel-scrollbar>\n        \u003C\u002Fdiv>\n        \u003C!-- 顶部导航 -->\n        \u003Cdiv class=\"layout_tabbar\">456\u003C\u002Fdiv>\n        \u003C!-- 内容展示区域 -->\n        \u003Cdiv class=\"layout_main\">\n\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 引入左侧菜单logo子组件\nimport Logo from '.\u002Flogo\u002Findex.vue'\n\u002F\u002F 引入菜单组件\nimport SideMenu from '.\u002Fmenu\u002Findex.vue'\n\n\u002F\u002F 获取用户相关的小仓库\nimport useUserStore from '@\u002Fstore\u002Fmodules\u002Fuser';\nlet useStore = useUserStore()\n\n\n\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n.layout_container {\n    width: 100%;\n    height: 100vh;\n    color: white;\n\n    .layout_slider {\n        width: $base-menu-width;\n        height: 100vh;\n        background-color: $base-menu-background;\n\n        .scrollbar {\n            width: 100%;\n            height: calc(100vh - $base-menu-logo-height);\n\n            .el_menu {\n                --el-menu-bg-color: #00152b;\n                --el-menu-text-color: #fff;\n                --el-menu-hover-bg-color: #00152b;\n                border-right: none; \u002F\u002F 去掉右侧边框\n            }\n        }\n    }\n\n    .layout_tabbar {\n        position: fixed;\n        left: $base-menu-width ;\n        top: 0;\n        width: calc(100% - $base-menu-width);\n        height: $base-tabbar-height;\n        background-color: springgreen;\n    }\n\n    .layout_main {\n        position: absolute;\n        width: calc(100% - $base-menu-width);\n        height: calc(100vh - $base-tabbar-height );\n        background-color: red;\n        left: $base-menu-width ;\n        top: $base-tabbar-height;\n        padding: 20px;\n        overflow: auto;\n    }\n}\n\u003C\u002Fstyle>\n```\n\n",99,"2026-04-07T16:22:29.810Z","2026-04-07T16:22:29.815Z","2026-05-25T22:05:44.500Z",{"id":92,"categoryName":27,"slug":28,"description":29,"sort":30,"isEnable":26,"createTime":31,"updateTime":32,"deleteTime":30},[],{"id":152,"title":153,"slug":154,"coverUrl":15,"summary":15,"content":155,"htmlContent":72,"categoryId":92,"viewCount":156,"likeCount":30,"isTop":30,"isPublish":26,"seoKeywords":15,"seoDescription":15,"publishTime":157,"createTime":158,"updateTime":159,"deleteTime":78,"category":160,"tags":161},"12","1. Vue3简介","vue3快速上手","# 1. Vue3简介\n- 2020年9月18日，`Vue.js`发布版`3.0`版本，代号：`One Piece`（n\n- 经历了：[4800+次提交](https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fcommits\u002Fmain)、[40+个RFC](https:\u002F\u002Fgithub.com\u002Fvuejs\u002Frfcs\u002Ftree\u002Fmaster\u002Factive-rfcs)、[600+次PR](https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fvue-next\u002Fpulls?q=is%3Apr+is%3Amerged+-author%3Aapp%2Fdependabot-preview+)、[300+贡献者](https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fgraphs\u002Fcontributors)\n- 官方发版地址：[Release v3.0.0 One Piece · vuejs\u002Fcore](https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Freleases\u002Ftag\u002Fv3.0.0)\n- 截止2023年10月，最新的公开版本为：`3.3.4`\n\n  \u003Cimg src=\"images\u002F1695089947298-161c1b47-eb86-42fb-b1f8-d6a4fcab8ee2.png\" alt=\"image.png\" style=\"zoom:30%;\" \u002F> \n\n## 1.1. 【性能的提升】\n\n- 打包大小减少`41%`。\n\n- 初次渲染快`55%`, 更新渲染快`133%`。\n\n- 内存减少`54%`。\n\n  \n## 1.2.【 源码的升级】\n\n- 使用`Proxy`代替`defineProperty`实现响应式。\n\n- 重写虚拟`DOM`的实现和`Tree-Shaking`。\n\n  \n## 1.3. 【拥抱TypeScript】\n\n- `Vue3`可以更好的支持`TypeScript`。\n\n  \n## 1.4. 【新的特性】\n\n1. `Composition API`（组合`API`）：\n   - `setup`\n   - `ref`与`reactive`\n   - `computed`与`watch`\n   \n     ......\n   \n2. 新的内置组件：\n   - `Fragment`\n   - `Teleport`\n   - `Suspense`\n\n     ......\n\n3. 其他改变：\n   - 新的生命周期钩子\n   - `data` 选项应始终被声明为一个函数\n   - 移除`keyCode`支持作为` v-on` 的修饰符\n\n     ......\n\n\n\n# 2. 创建Vue3工程\n\n## 2.1. 【基于 vue-cli 创建】\n点击查看[官方文档](https:\u002F\u002Fcli.vuejs.org\u002Fzh\u002Fguide\u002Fcreating-a-project.html#vue-create)\n\n> 备注：目前`vue-cli`已处于维护模式，官方推荐基于 `Vite` 创建项目。\n\n```powershell\n## 查看@vue\u002Fcli版本，确保@vue\u002Fcli版本在4.5.0以上\nvue --version\n\n## 安装或者升级你的@vue\u002Fcli \nnpm install -g @vue\u002Fcli\n\n## 执行创建命令\nvue create vue_test\n\n##  随后选择3.x\n##  Choose a version of Vue.js that you want to start the project with (Use arrow keys)\n##  > 3.x\n##    2.x\n\n## 启动\ncd vue_test\nnpm run serve\n```\n\n---\n\n## 2.2. 【基于 vite 创建】(推荐)\n`vite` 是新一代前端构建工具，官网地址：[https:\u002F\u002Fvitejs.cn](https:\u002F\u002Fvitejs.cn\u002F)，`vite`的优势如下：\n\n- 轻量快速的热重载（`HMR`），能实现极速的服务启动。\n- 对 `TypeScript`、`JSX`、`CSS` 等支持开箱即用。\n- 真正的按需编译，不再等待整个应用编译完成。\n- `webpack`构建 与 `vite`构建对比图如下：\n\u003Cimg src=\"images\u002F1683167182037-71c78210-8217-4e7d-9a83-e463035efbbe.png\" alt=\"webpack构建\" title=\"webpack构建\" style=\"zoom:20%;box-shadow:0 0 10px black\" \u002F>\t\u003Cimg src=\"images\u002F1683167204081-582dc237-72bc-499e-9589-2cdfd452e62f.png\" alt=\"vite构建\" title=\"vite构建\" style=\"zoom: 20%;box-shadow:0 0 10px black\" \u002F>\n* 具体操作如下（点击查看[官方文档](https:\u002F\u002Fcn.vuejs.org\u002Fguide\u002Fquick-start.html#creating-a-vue-application)）\n\n```powershell\n## 1.创建命令\nnpm create vue@latest\n\n## 2.具体配置\n## 配置项目名称\n√ Project name: vue3_test\n## 是否添加TypeScript支持\n√ Add TypeScript?  Yes\n## 是否添加JSX支持\n√ Add JSX Support?  No\n## 是否添加路由环境\n√ Add Vue Router for Single Page Application development?  No\n## 是否添加pinia环境\n√ Add Pinia for state management?  No\n## 是否添加单元测试\n√ Add Vitest for Unit Testing?  No\n## 是否添加端到端测试方案\n√ Add an End-to-End Testing Solution? » No\n## 是否添加ESLint语法检查\n√ Add ESLint for code quality?  Yes\n## 是否添加Prettiert代码格式化\n√ Add Prettier for code formatting?  No\n```\n自己动手编写一个App组件\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"app\">\n    \u003Ch1>你好啊！\u003C\u002Fh1>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\"ts\">\n  export default {\n    name:'App' \u002F\u002F组件名\n  }\n\u003C\u002Fscript>\n\n\u003Cstyle>\n  .app {\n    background-color: #ddd;\n    box-shadow: 0 0 10px;\n    border-radius: 10px;\n    padding: 20px;\n  }\n\u003C\u002Fstyle>\n```\n\n安装官方推荐的`vscode`插件：\n\n\u003Cimg src=\"images\u002Fvolar.png\" alt=\"Snipaste_2023-10-08_20-46-34\" style=\"zoom:50%;\" \u002F> \n\n\u003Cimg src=\"images\u002Fimage-20231218085906380.png\" alt=\"image-20231218085906380\" style=\"zoom:42%;\" \u002F> \n\n总结：\n\n- `Vite` 项目中，`index.html` 是项目的入口文件，在项目最外层。\n- 加载`index.html`后，`Vite` 解析 `\u003Cscript type=\"module\" src=\"xxx\">` 指向的`JavaScript`。\n- `Vue3`**中是通过 **`createApp` 函数创建一个应用实例。\n## 2.3. 【一个简单的效果】\n\n`Vue3`向下兼容`Vue2`语法，且`Vue3`中的模板中可以没有根标签\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"person\">\n    \u003Ch2>姓名：{{name}}\u003C\u002Fh2>\n    \u003Ch2>年龄：{{age}}\u003C\u002Fh2>\n    \u003Cbutton @click=\"changeName\">修改名字\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeAge\">年龄+1\u003C\u002Fbutton>\n    \u003Cbutton @click=\"showTel\">点我查看联系方式\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\"ts\">\n  export default {\n    name:'App',\n    data() {\n      return {\n        name:'张三',\n        age:18,\n        tel:'13888888888'\n      }\n    },\n    methods:{\n      changeName(){\n        this.name = 'zhang-san'\n      },\n      changeAge(){\n        this.age += 1\n      },\n      showTel(){\n        alert(this.tel)\n      }\n    },\n  }\n\u003C\u002Fscript>\n```\n\n\n# 3. Vue3核心语法\n## 3.1.  【OptionsAPI 与 CompositionAPI】\n\n- `Vue2`的`API`设计是`Options`（配置）风格的。\n- `Vue3`的`API`设计是`Composition`（组合）风格的。\n###  Options API 的弊端\n\n`Options`类型的 `API`，数据、方法、计算属性等，是分散在：`data`、`methods`、`computed`中的，若想新增或者修改一个需求，就需要分别修改：`data`、`methods`、`computed`，不便于维护和复用。\n\n\u003Cimg src=\"images\u002F1696662197101-55d2b251-f6e5-47f4-b3f1-d8531bbf9279.gif\" alt=\"1.gif\" style=\"zoom:70%;border-radius:20px\" \u002F>\u003Cimg src=\"images\u002F1696662200734-1bad8249-d7a2-423e-a3c3-ab4c110628be.gif\" alt=\"2.gif\" style=\"zoom:70%;border-radius:20px\" \u002F>\n\n### Composition API 的优势\n\n可以用函数的方式，更加优雅的组织代码，让相关功能的代码更加有序的组织在一起。\n\n\u003Cimg src=\"images\u002F1696662249851-db6403a1-acb5-481a-88e0-e1e34d2ef53a.gif\" alt=\"3.gif\" style=\"height:300px;border-radius:10px\"  \u002F>\u003Cimg src=\"images\u002F1696662256560-7239b9f9-a770-43c1-9386-6cc12ef1e9c0.gif\" alt=\"4.gif\" style=\"height:300px;border-radius:10px\"  \u002F>\n\n> 说明：以上四张动图原创作者：大帅老猿\n\n## 3.2. 【拉开序幕的 setup】\n### setup 概述\n`setup`是`Vue3`中一个新的配置项，值是一个函数，它是 `Composition API` **“表演的舞台**_**”**_，组件中所用到的：数据、方法、计算属性、监视......等等，均配置在`setup`中。\n\n特点如下：\n\n- `setup`函数返回的对象中的内容，可直接在模板中使用。\n- `setup`中访问`this`是`undefined`。\n- `setup`函数会在`beforeCreate`之前调用，它是“领先”所有钩子执行的。\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"person\">\n    \u003Ch2>姓名：{{name}}\u003C\u002Fh2>\n    \u003Ch2>年龄：{{age}}\u003C\u002Fh2>\n    \u003Cbutton @click=\"changeName\">修改名字\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeAge\">年龄+1\u003C\u002Fbutton>\n    \u003Cbutton @click=\"showTel\">点我查看联系方式\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\"ts\">\n  export default {\n    name:'Person',\n    setup(){\n      \u002F\u002F 数据，原来写在data中（注意：此时的name、age、tel数据都不是响应式数据）\n      let name = '张三'\n      let age = 18\n      let tel = '13888888888'\n\n      \u002F\u002F 方法，原来写在methods中\n      function changeName(){\n        name = 'zhang-san' \u002F\u002F注意：此时这么修改name页面是不变化的\n        console.log(name)\n      }\n      function changeAge(){\n        age += 1 \u002F\u002F注意：此时这么修改age页面是不变化的\n        console.log(age)\n      }\n      function showTel(){\n        alert(tel)\n      }\n\n      \u002F\u002F 返回一个对象，对象中的内容，模板中可以直接使用\n      return {name,age,tel,changeName,changeAge,showTel}\n    }\n  }\n\u003C\u002Fscript>\n```\n### setup 的返回值\n\n- 若返回一个**对象**：则对象中的：属性、方法等，在模板中均可以直接使用**（重点关注）。**\n- 若返回一个**函数**：则可以自定义渲染内容，代码如下：\n```jsx\nsetup(){\n  return ()=> '你好啊！'\n}\n```\n### setup 与 Options API 的关系\n\n- `Vue2` 的配置（`data`、`methos`......）中**可以访问到** `setup`中的属性、方法。（Vue2和Vue3可以共存）\n- 但在`setup`中**不能访问到**`Vue2`的配置（`data`、`methos`......）。\n- 如果与`Vue2`冲突，则`setup`优先。\n### setup 语法糖\n`setup`函数有一个语法糖，这个语法糖，可以让我们把`setup`独立出去，代码如下：\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"person\">\n    \u003Ch2>姓名：{{name}}\u003C\u002Fh2>\n    \u003Ch2>年龄：{{age}}\u003C\u002Fh2>\n    \u003Cbutton @click=\"changName\">修改名字\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changAge\">年龄+1\u003C\u002Fbutton>\n    \u003Cbutton @click=\"showTel\">点我查看联系方式\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\"ts\">\n  export default {\n    name:'Person',\n  }\n\u003C\u002Fscript>\n\n\u003C!-- 下面的写法是setup语法糖 -->\n\u003Cscript setup lang=\"ts\">\n  console.log(this) \u002F\u002Fundefined\n  \n  \u002F\u002F 数据（注意：此时的name、age、tel都不是响应式数据）\n  let name = '张三'\n  let age = 18\n  let tel = '13888888888'\n\n  \u002F\u002F 方法\n  function changName(){\n    name = '李四'\u002F\u002F注意：此时这么修改name页面是不变化的\n  }\n  function changAge(){\n    console.log(age)\n    age += 1 \u002F\u002F注意：此时这么修改age页面是不变化的\n  }\n  function showTel(){\n    alert(tel)\n  }\n\u003C\u002Fscript>\n```\n扩展：上述代码，还需要编写一个不写`setup`的`script`标签，去指定组件名字，比较麻烦，我们可以借助`vite`中的插件简化\n\n1. 第一步：`npm i vite-plugin-vue-setup-extend -D`\n2. 第二步：`vite.config.ts`\n```jsx\nimport { defineConfig } from 'vite'\nimport VueSetupExtend from 'vite-plugin-vue-setup-extend'\n\nexport default defineConfig({\n  plugins: [ VueSetupExtend() ]\n})\n```\n\n3. 第三步：`\u003Cscript setup lang=\"ts\" name=\"Person\">`\n## 3.3. 【ref 创建：基本类型的响应式数据】\n\n- **作用：**定义响应式变量。\n- **语法：**`let xxx = ref(初始值)`。\n- **返回值：**一个`RefImpl`的实例对象，简称`ref对象`或`ref`，`ref`对象的`value`**属性是响应式的**。\n- **注意点：**\n   - `JS`中操作数据需要：`xxx.value`，但模板中不需要`.value`，直接使用即可。\n   - 对于`let name = ref('张三')`来说，`name`不是响应式的，`name.value`是响应式的。\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"person\">\n    \u003Ch2>姓名：{{name}}\u003C\u002Fh2>\n    \u003Ch2>年龄：{{age}}\u003C\u002Fh2>\n    \u003Cbutton @click=\"changeName\">修改名字\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeAge\">年龄+1\u003C\u002Fbutton>\n    \u003Cbutton @click=\"showTel\">点我查看联系方式\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\" name=\"Person\">\n  import {ref} from 'vue'\n  \u002F\u002F name和age是一个RefImpl的实例对象，简称ref对象，它们的value属性是响应式的。\n  let name = ref('张三')\n  let age = ref(18)\n  \u002F\u002F tel就是一个普通的字符串，不是响应式的\n  let tel = '13888888888'\n\n  function changeName(){\n    \u002F\u002F JS中操作ref对象时候需要.value\n    name.value = '李四'\n    console.log(name.value)\n\n    \u002F\u002F 注意：name不是响应式的，name.value是响应式的，所以如下代码并不会引起页面的更新。\n    \u002F\u002F name = ref('zhang-san')\n  }\n  function changeAge(){\n    \u002F\u002F JS中操作ref对象时候需要.value\n    age.value += 1 \n    console.log(age.value)\n  }\n  function showTel(){\n    alert(tel)\n  }\n\u003C\u002Fscript>\n```\n## 3.4. 【reactive 创建：对象类型的响应式数据】\n\n- **作用：**定义一个**响应式对象**（基本类型不要用它，要用`ref`，否则报错）\n- **语法：**`let 响应式对象= reactive(源对象)`。\n- **返回值：**一个`Proxy`的实例对象，简称：响应式对象。\n- **注意点：**`reactive`定义的响应式数据是“深层次”的。\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"person\">\n    \u003Ch2>汽车信息：一台{{ car.brand }}汽车，价值{{ car.price }}万\u003C\u002Fh2>\n    \u003Ch2>游戏列表：\u003C\u002Fh2>\n    \u003Cul>\n      \u003Cli v-for=\"g in games\" :key=\"g.id\">{{ g.name }}\u003C\u002Fli>\n    \u003C\u002Ful>\n    \u003Ch2>测试：{{obj.a.b.c.d}}\u003C\u002Fh2>\n    \u003Cbutton @click=\"changeCarPrice\">修改汽车价格\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeFirstGame\">修改第一游戏\u003C\u002Fbutton>\n    \u003Cbutton @click=\"test\">测试\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\"ts\" setup name=\"Person\">\nimport { reactive } from 'vue'\n\n\u002F\u002F 数据\nlet car = reactive({ brand: '奔驰', price: 100 })\nlet games = reactive([\n  { id: 'ahsgdyfa01', name: '英雄联盟' },\n  { id: 'ahsgdyfa02', name: '王者荣耀' },\n  { id: 'ahsgdyfa03', name: '原神' }\n])\nlet obj = reactive({\n  a:{\n    b:{\n      c:{\n        d:666\n      }\n    }\n  }\n})\n\nfunction changeCarPrice() {\n  car.price += 10\n}\nfunction changeFirstGame() {\n  games[0].name = '流星蝴蝶剑'\n}\nfunction test(){\n  obj.a.b.c.d = 999\n}\n\u003C\u002Fscript>\n```\n## 3.5. 【ref 创建：对象类型的响应式数据】\n\n- 其实`ref`接收的数据可以是：**基本类型**、**对象类型**。\n- 若`ref`接收的是对象类型，内部其实也是调用了`reactive`函数。\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"person\">\n    \u003Ch2>汽车信息：一台{{ car.brand }}汽车，价值{{ car.price }}万\u003C\u002Fh2>\n    \u003Ch2>游戏列表：\u003C\u002Fh2>\n    \u003Cul>\n      \u003Cli v-for=\"g in games\" :key=\"g.id\">{{ g.name }}\u003C\u002Fli>\n    \u003C\u002Ful>\n    \u003Ch2>测试：{{obj.a.b.c.d}}\u003C\u002Fh2>\n    \u003Cbutton @click=\"changeCarPrice\">修改汽车价格\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeFirstGame\">修改第一游戏\u003C\u002Fbutton>\n    \u003Cbutton @click=\"test\">测试\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\"ts\" setup name=\"Person\">\nimport { ref } from 'vue'\n\n\u002F\u002F 数据\nlet car = ref({ brand: '奔驰', price: 100 })\nlet games = ref([\n  { id: 'ahsgdyfa01', name: '英雄联盟' },\n  { id: 'ahsgdyfa02', name: '王者荣耀' },\n  { id: 'ahsgdyfa03', name: '原神' }\n])\nlet obj = ref({\n  a:{\n    b:{\n      c:{\n        d:666\n      }\n    }\n  }\n})\n\nconsole.log(car)\n\nfunction changeCarPrice() {\n  car.value.price += 10\n}\nfunction changeFirstGame() {\n  games.value[0].name = '流星蝴蝶剑'\n}\nfunction test(){\n  obj.value.a.b.c.d = 999\n}\n\u003C\u002Fscript>\n```\n## 3.6. 【ref 对比 reactive】\n宏观角度看：\n\n> 1. `ref`用来定义：**基本类型数据**、**对象类型数据**；\n>\n> 2. `reactive`用来定义：**对象类型数据**。\n\n- 区别：\n\n> 1. `ref`创建的变量必须使用`.value`（可以使用`Official`插件自动添加`.value`）。\n>\n>    ![image-20260110235114221](images\u002Fimage-20260110235114221.png)\n>\n>    ![image-20260110235122904](images\u002Fimage-20260110235122904.png)\n>\n> 2. `reactive`重新分配一个新对象，会**失去**响应式（可以使用`Object.assign`去整体替换）。\n\n- 使用原则：\n> 1. 若需要一个基本类型的响应式数据，必须使用`ref`。\n> 2. 若需要一个响应式对象，层级不深，`ref`、`reactive`都可以。\n> 3. 若需要一个响应式对象，且层级较深，推荐使用`reactive`。\n\n## 3.7. 【toRefs 与 toRef】\n\n- 作用：将一个响应式对象中的每一个属性，转换为`ref`对象。\n- 备注：`toRefs`与`toRef`功能一致，但`toRefs`可以批量转换。\n- 语法如下：\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"person\">\n    \u003Ch2>姓名：{{person.name}}\u003C\u002Fh2>\n    \u003Ch2>年龄：{{person.age}}\u003C\u002Fh2>\n    \u003Ch2>性别：{{person.gender}}\u003C\u002Fh2>\n    \u003Cbutton @click=\"changeName\">修改名字\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeAge\">修改年龄\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeGender\">修改性别\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\"ts\" setup name=\"Person\">\n  import {ref,reactive,toRefs,toRef} from 'vue'\n\n  \u002F\u002F 数据\n  let person = reactive({name:'张三', age:18, gender:'男'})\n\t\n  \u002F\u002F 通过toRefs将person对象中的n个属性批量取出，且依然保持响应式的能力\n  let {name,gender} =  toRefs(person)\n\t\n  \u002F\u002F 通过toRef将person对象中的gender属性取出，且依然保持响应式的能力\n  let age = toRef(person,'age')\n\n  \u002F\u002F 方法\n  function changeName(){\n    name.value += '~'\n  }\n  function changeAge(){\n    age.value += 1\n  }\n  function changeGender(){\n    gender.value = '女'\n  }\n\u003C\u002Fscript>\n```\n## 3.8. 【computed】\n\n作用：根据已有数据计算出新数据（和`Vue2`中的`computed`作用一致）。\n\n\u003Cimg src=\"images\u002Fcomputed.gif\" style=\"zoom:20%;\" \u002F>  \n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"person\">\n    姓：\u003Cinput type=\"text\" v-model=\"firstName\"> \u003Cbr>\n    名：\u003Cinput type=\"text\" v-model=\"lastName\"> \u003Cbr>\n    全名：\u003Cspan>{{fullName}}\u003C\u002Fspan> \u003Cbr>\n    \u003Cbutton @click=\"changeFullName\">全名改为：li-si\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\" name=\"App\">\n  import {ref,computed} from 'vue'\n\n  let firstName = ref('zhang')\n  let lastName = ref('san')\n\n  \u002F\u002F 计算属性——只读取，不修改\n  \u002F* let fullName = computed(()=>{\n    return firstName.value + '-' + lastName.value\n  }) *\u002F\n\n\n  \u002F\u002F 计算属性——既读取又修改\n  let fullName = computed({\n    \u002F\u002F 读取\n    get(){\n      return firstName.value + '-' + lastName.value\n    },\n    \u002F\u002F 修改\n    set(val){\n      console.log('有人修改了fullName',val)\n      firstName.value = val.split('-')[0]\n      lastName.value = val.split('-')[1]\n    }\n  })\n\n  function changeFullName(){\n    fullName.value = 'li-si'\n  } \n\u003C\u002Fscript>\n```\n## 3.9.【watch】\n\n- 作用：监视数据的变化（和`Vue2`中的`watch`作用一致）\n- 特点：`Vue3`中的`watch`只能监视以下**四种数据**：\n> 1. `ref`定义的数据。\n> 2. `reactive`定义的数据。\n> 3. 函数返回一个值（`getter`函数）。\n> 4. 一个包含上述内容的数组。\n\n我们在`Vue3`中使用`watch`的时候，通常会遇到以下几种情况：\n### * 情况一\n监视`ref`定义的【基本类型】数据：直接写数据名即可，监视的是其`value`值的改变。\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"person\">\n    \u003Ch1>情况一：监视【ref】定义的【基本类型】数据\u003C\u002Fh1>\n    \u003Ch2>当前求和为：{{sum}}\u003C\u002Fh2>\n    \u003Cbutton @click=\"changeSum\">点我sum+1\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\"ts\" setup name=\"Person\">\n  import {ref,watch} from 'vue'\n  \u002F\u002F 数据\n  let sum = ref(0)\n  \u002F\u002F 方法\n  function changeSum(){\n    sum.value += 1\n  }\n  \u002F\u002F 监视，情况一：监视【ref】定义的【基本类型】数据\n  const stopWatch = watch(sum,(newValue,oldValue)=>{\n    console.log('sum变化了',newValue,oldValue)\n    if(newValue >= 10){\n      stopWatch()\n    }\n  })\n\u003C\u002Fscript>\n```\n### * 情况二\n监视`ref`定义的【对象类型】数据：直接写数据名，监视的是对象的【地址值】，若想监视对象内部的数据，要手动开启深度监视。\n\n> 注意：\n>\n> * 若修改的是`ref`定义的对象中的属性，`newValue` 和 `oldValue` 都是新值，因为它们是同一个对象。\n>\n> * 若修改整个`ref`定义的对象，`newValue` 是新值， `oldValue` 是旧值，因为不是同一个对象了。\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"person\">\n    \u003Ch1>情况二：监视【ref】定义的【对象类型】数据\u003C\u002Fh1>\n    \u003Ch2>姓名：{{ person.name }}\u003C\u002Fh2>\n    \u003Ch2>年龄：{{ person.age }}\u003C\u002Fh2>\n    \u003Cbutton @click=\"changeName\">修改名字\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeAge\">修改年龄\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changePerson\">修改整个人\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\"ts\" setup name=\"Person\">\n  import {ref,watch} from 'vue'\n  \u002F\u002F 数据\n  let person = ref({\n    name:'张三',\n    age:18\n  })\n  \u002F\u002F 方法\n  function changeName(){\n    person.value.name += '~'\n  }\n  function changeAge(){\n    person.value.age += 1\n  }\n  function changePerson(){\n    person.value = {name:'李四',age:90}\n  }\n  \u002F* \n    监视，情况一：监视【ref】定义的【对象类型】数据，监视的是对象的地址值，若想监视对象内部属性的变化，需要手动开启深度监视\n    watch的第一个参数是：被监视的数据\n    watch的第二个参数是：监视的回调\n    watch的第三个参数是：配置对象（deep、immediate等等.....） \n  *\u002F\n  watch(person,(newValue,oldValue)=>{\n    console.log('person变化了',newValue,oldValue)\n  },{deep:true})\n  \n\u003C\u002Fscript>\n```\n### *  情况三\n监视`reactive`定义的【对象类型】数据，且默认开启了深度监视。\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"person\">\n    \u003Ch1>情况三：监视【reactive】定义的【对象类型】数据\u003C\u002Fh1>\n    \u003Ch2>姓名：{{ person.name }}\u003C\u002Fh2>\n    \u003Ch2>年龄：{{ person.age }}\u003C\u002Fh2>\n    \u003Cbutton @click=\"changeName\">修改名字\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeAge\">修改年龄\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changePerson\">修改整个人\u003C\u002Fbutton>\n    \u003Chr>\n    \u003Ch2>测试：{{obj.a.b.c}}\u003C\u002Fh2>\n    \u003Cbutton @click=\"test\">修改obj.a.b.c\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\"ts\" setup name=\"Person\">\n  import {reactive,watch} from 'vue'\n  \u002F\u002F 数据\n  let person = reactive({\n    name:'张三',\n    age:18\n  })\n  let obj = reactive({\n    a:{\n      b:{\n        c:666\n      }\n    }\n  })\n  \u002F\u002F 方法\n  function changeName(){\n    person.name += '~'\n  }\n  function changeAge(){\n    person.age += 1\n  }\n  function changePerson(){\n    Object.assign(person,{name:'李四',age:80})\n  }\n  function test(){\n    obj.a.b.c = 888\n  }\n\n  \u002F\u002F 监视，情况三：监视【reactive】定义的【对象类型】数据，且默认是开启深度监视的\n  watch(person,(newValue,oldValue)=>{\n    console.log('person变化了',newValue,oldValue)\n  })\n  watch(obj,(newValue,oldValue)=>{\n    console.log('Obj变化了',newValue,oldValue)\n  })\n\u003C\u002Fscript>\n```\n### * 情况四\n监视`ref`或`reactive`定义的【对象类型】数据中的**某个属性**，注意点如下：\n\n1. 若该属性值**不是**【对象类型】，需要写成函数形式。\n2. 若该属性值是**依然**是【对象类型】，可直接编，也可写成函数，建议写成函数。\n\n结论：监视的要是对象里的属性，那么最好写函数式，注意点：若是对象监视的是地址值，需要关注对象内部，需要手动开启深度监视。\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"person\">\n    \u003Ch1>情况四：监视【ref】或【reactive】定义的【对象类型】数据中的某个属性\u003C\u002Fh1>\n    \u003Ch2>姓名：{{ person.name }}\u003C\u002Fh2>\n    \u003Ch2>年龄：{{ person.age }}\u003C\u002Fh2>\n    \u003Ch2>汽车：{{ person.car.c1 }}、{{ person.car.c2 }}\u003C\u002Fh2>\n    \u003Cbutton @click=\"changeName\">修改名字\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeAge\">修改年龄\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeC1\">修改第一台车\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeC2\">修改第二台车\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeCar\">修改整个车\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\"ts\" setup name=\"Person\">\n  import {reactive,watch} from 'vue'\n\n  \u002F\u002F 数据\n  let person = reactive({\n    name:'张三',\n    age:18,\n    car:{\n      c1:'奔驰',\n      c2:'宝马'\n    }\n  })\n  \u002F\u002F 方法\n  function changeName(){\n    person.name += '~'\n  }\n  function changeAge(){\n    person.age += 1\n  }\n  function changeC1(){\n    person.car.c1 = '奥迪'\n  }\n  function changeC2(){\n    person.car.c2 = '大众'\n  }\n  function changeCar(){\n    person.car = {c1:'雅迪',c2:'爱玛'}\n  }\n\n  \u002F\u002F 监视，情况四：监视响应式对象中的某个属性，且该属性是基本类型的，要写成函数式\n  \u002F* watch(()=> person.name,(newValue,oldValue)=>{\n    console.log('person.name变化了',newValue,oldValue)\n  }) *\u002F\n\n  \u002F\u002F 监视，情况四：监视响应式对象中的某个属性，且该属性是对象类型的，可以直接写，也能写函数，更推荐写函数\n  watch(()=>person.car,(newValue,oldValue)=>{\n    console.log('person.car变化了',newValue,oldValue)\n  },{deep:true})\n\u003C\u002Fscript>\n```\n### * 情况五\n监视上述的多个数据\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"person\">\n    \u003Ch1>情况五：监视上述的多个数据\u003C\u002Fh1>\n    \u003Ch2>姓名：{{ person.name }}\u003C\u002Fh2>\n    \u003Ch2>年龄：{{ person.age }}\u003C\u002Fh2>\n    \u003Ch2>汽车：{{ person.car.c1 }}、{{ person.car.c2 }}\u003C\u002Fh2>\n    \u003Cbutton @click=\"changeName\">修改名字\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeAge\">修改年龄\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeC1\">修改第一台车\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeC2\">修改第二台车\u003C\u002Fbutton>\n    \u003Cbutton @click=\"changeCar\">修改整个车\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\"ts\" setup name=\"Person\">\n  import {reactive,watch} from 'vue'\n\n  \u002F\u002F 数据\n  let person = reactive({\n    name:'张三',\n    age:18,\n    car:{\n      c1:'奔驰',\n      c2:'宝马'\n    }\n  })\n  \u002F\u002F 方法\n  function changeName(){\n    person.name += '~'\n  }\n  function changeAge(){\n    person.age += 1\n  }\n  function changeC1(){\n    person.car.c1 = '奥迪'\n  }\n  function changeC2(){\n    person.car.c2 = '大众'\n  }\n  function changeCar(){\n    person.car = {c1:'雅迪',c2:'爱玛'}\n  }\n\n  \u002F\u002F 监视，情况五：监视上述的多个数据\n  watch([()=>person.name,person.car],(newValue,oldValue)=>{\n    console.log('person.car变化了',newValue,oldValue)\n  },{deep:true})\n\n\u003C\u002Fscript>\n```\n## 3.10. 【watchEffect】\n\n* 官网：立即运行一个函数，同时响应式地追踪其依赖，并在依赖更改时重新执行该函数。\n\n* `watch`对比`watchEffect`\n\n  > 1. 都能监听响应式数据的变化，不同的是监听数据变化的方式不同\n  >\n  > 2. `watch`：要明确指出监视的数据\n  >\n  > 3. `watchEffect`：不用明确指出监视的数据（函数中用到哪些属性，那就监视哪些属性）。\n\n* 示例代码：\n\n  ```vue\n  \u003Ctemplate>\n    \u003Cdiv class=\"person\">\n      \u003Ch1>需求：水温达到50℃，或水位达到20cm，则联系服务器\u003C\u002Fh1>\n      \u003Ch2 id=\"demo\">水温：{{temp}}\u003C\u002Fh2>\n      \u003Ch2>水位：{{height}}\u003C\u002Fh2>\n      \u003Cbutton @click=\"changePrice\">水温+1\u003C\u002Fbutton>\n      \u003Cbutton @click=\"changeSum\">水位+10\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  \u003C\u002Ftemplate>\n  \n  \u003Cscript lang=\"ts\" setup name=\"Person\">\n    import {ref,watch,watchEffect} from 'vue'\n    \u002F\u002F 数据\n    let temp = ref(0)\n    let height = ref(0)\n  \n    \u002F\u002F 方法\n    function changePrice(){\n      temp.value += 10\n    }\n    function changeSum(){\n      height.value += 1\n    }\n  \n    \u002F\u002F 用watch实现，需要明确的指出要监视：temp、height\n    watch([temp,height],(value)=>{\n      \u002F\u002F 从value中获取最新的temp值、height值\n      const [newTemp,newHeight] = value\n      \u002F\u002F 室温达到50℃，或水位达到20cm，立刻联系服务器\n      if(newTemp >= 50 || newHeight >= 20){\n        console.log('联系服务器')\n      }\n    })\n  \n    \u002F\u002F 用watchEffect实现，不用\n    const stopWtach = watchEffect(()=>{\n      \u002F\u002F 室温达到50℃，或水位达到20cm，立刻联系服务器\n      if(temp.value >= 50 || height.value >= 20){\n        console.log(document.getElementById('demo')?.innerText)\n        console.log('联系服务器')\n      }\n      \u002F\u002F 水温达到100，或水位达到50，取消监视\n      if(temp.value === 100 || height.value === 50){\n        console.log('清理了')\n        stopWtach()\n      }\n    })\n  \u003C\u002Fscript>\n  ```\n  \n  \n\n## 3.11. 【标签的 ref 属性】\n\n作用：用于注册模板引用。\n\n> * 用在普通`DOM`标签上，获取的是`DOM`节点。\n>\n> * 用在组件标签上，获取的是组件实例对象。\n\n用在普通`DOM`标签上：\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"person\">\n    \u003Ch1 ref=\"title1\">尚硅谷\u003C\u002Fh1>\n    \u003Ch2 ref=\"title2\">前端\u003C\u002Fh2>\n    \u003Ch3 ref=\"title3\">Vue\u003C\u002Fh3>\n    \u003Cinput type=\"text\" ref=\"inpt\"> \u003Cbr>\u003Cbr>\n    \u003Cbutton @click=\"showLog\">点我打印内容\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\"ts\" setup name=\"Person\">\n  import {ref} from 'vue'\n\t\n  let title1 = ref()\n  let title2 = ref()\n  let title3 = ref()\n\n  function showLog(){\n    \u002F\u002F 通过id获取元素\n    const t1 = document.getElementById('title1')\n    \u002F\u002F 打印内容\n    console.log((t1 as HTMLElement).innerText)\n    console.log((\u003CHTMLElement>t1).innerText)\n    console.log(t1?.innerText)\n    \n\t\t\u002F************************************\u002F\n\t\t\n    \u002F\u002F 通过ref获取元素\n    console.log(title1.value)\n    console.log(title2.value)\n    console.log(title3.value)\n  }\n\u003C\u002Fscript>\n```\n\n用在组件标签上：\n\n```vue\n\u003C!-- 父组件App.vue -->\n\u003Ctemplate>\n  \u003CPerson ref=\"ren\"\u002F>\n  \u003Cbutton @click=\"test\">测试\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\"ts\" setup name=\"App\">\n  import Person from '.\u002Fcomponents\u002FPerson.vue'\n  import {ref} from 'vue'\n\n  let ren = ref()\n\n  function test(){\n    console.log(ren.value.name)\n    console.log(ren.value.age)\n  }\n\u003C\u002Fscript>\n\n\n\u003C!-- 子组件Person.vue中要使用defineExpose暴露内容 -->\n\u003Cscript lang=\"ts\" setup name=\"Person\">\n  import {ref,defineExpose} from 'vue'\n\t\u002F\u002F 数据\n  let name = ref('张三')\n  let age = ref(18)\n  \u002F****************************\u002F\n  \u002F****************************\u002F\n  \u002F\u002F 使用defineExpose将组件中的数据交给外部\n  defineExpose({name,age})\n\u003C\u002Fscript>\n```\n\n\n\n## 3.12. 【props】\n\n> ```js\n>\u002F\u002F 定义一个接口，限制每个Person对象的格式\n> export interface PersonInter {\n>  id:string,\n>  name:string,\n>     age:number\n>    }\n>    \n> \u002F\u002F 定义一个自定义类型Persons\n> export type Persons = Array\u003CPersonInter>\n> ```\n> \n> `App.vue`中代码：\n>\n> ```vue\n>\u003Ctemplate>\n> \t\u003CPerson :list=\"persons\"\u002F>\n> \u003C\u002Ftemplate>\n>   \n> \u003Cscript lang=\"ts\" setup name=\"App\">\n>   import Person from '.\u002Fcomponents\u002FPerson.vue'\n>   import {reactive} from 'vue'\n>     import {type Persons} from '.\u002Ftypes'\n>   \n>     let persons = reactive\u003CPersons>([\n>      {id:'e98219e12',name:'张三',age:18},\n>       {id:'e98219e13',name:'李四',age:19},\n>        {id:'e98219e14',name:'王五',age:20}\n>      ])\n>    \u003C\u002Fscript>\n>   \n> ```\n> \n> `Person.vue`中代码：\n>\n> ```Vue\n>\u003Ctemplate>\n> \u003Cdiv class=\"person\">\n>  \u003Cul>\n>      \u003Cli v-for=\"item in list\" :key=\"item.id\">\n>         {{item.name}}--{{item.age}}\n>       \u003C\u002Fli>\n>     \u003C\u002Ful>\n>    \u003C\u002Fdiv>\n>    \u003C\u002Ftemplate>\n>   \n> \u003Cscript lang=\"ts\" setup name=\"Person\">\n> import {defineProps} from 'vue'\n> import {type PersonInter} from '@\u002Ftypes'\n>   \n>   \u002F\u002F 第一种写法：仅接收\n> \u002F\u002F const props = defineProps(['list'])\n>   \n>   \u002F\u002F 第二种写法：接收+限制类型\n> \u002F\u002F defineProps\u003C{list:Persons}>()\n>   \n>   \u002F\u002F 第三种写法：接收+限制类型+指定默认值+限制必要性\n> let props = withDefaults(defineProps\u003C{list?:Persons}>(),{\n>      list:()=>[{id:'asdasg01',name:'小猪佩奇',age:18}]\n>   })\n>    console.log(props)\n>   \u003C\u002Fscript>\n>   ```\n> \n\n## 3.13. 【生命周期】\n\n* 概念：`Vue`组件实例在创建时要经历一系列的初始化步骤，在此过程中`Vue`会在合适的时机，调用特定的函数，从而让开发者有机会在特定阶段运行自己的代码，这些特定的函数统称为：生命周期钩子\n\n* 规律：\n\n  > 生命周期整体分为四个阶段，分别是：**创建、挂载、更新、销毁**，每个阶段都有两个钩子，一前一后。\n\n* `Vue2`的生命周期\n\n  > 创建阶段：`beforeCreate`、`created`\n  >\n  > 挂载阶段：`beforeMount`、`mounted`\n  >\n  > 更新阶段：`beforeUpdate`、`updated`\n  >\n  > 销毁阶段：`beforeDestroy`、`destroyed`\n\n* `Vue3`的生命周期\n\n  > 创建阶段：`setup`\n  >\n  > 挂载阶段：`onBeforeMount`、`onMounted`\n  >\n  > 更新阶段：`onBeforeUpdate`、`onUpdated`\n  >\n  > 卸载阶段：`onBeforeUnmount`、`onUnmounted`\n\n* 常用的钩子：`onMounted`(挂载完毕)、`onUpdated`(更新完毕)、`onBeforeUnmount`(卸载之前)\n\n* 示例代码：\n\n  ```vue\n  \u003Ctemplate>\n    \u003Cdiv class=\"person\">\n      \u003Ch2>当前求和为：{{ sum }}\u003C\u002Fh2>\n      \u003Cbutton @click=\"changeSum\">点我sum+1\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  \u003C\u002Ftemplate>\n  \n  \u003C!-- vue3写法 -->\n  \u003Cscript lang=\"ts\" setup name=\"Person\">\n    import { \n      ref, \n      onBeforeMount, \n      onMounted, \n      onBeforeUpdate, \n      onUpdated, \n      onBeforeUnmount, \n      onUnmounted \n    } from 'vue'\n  \n    \u002F\u002F 数据\n    let sum = ref(0)\n    \u002F\u002F 方法\n    function changeSum() {\n      sum.value += 1\n    }\n    console.log('setup')\n    \u002F\u002F 生命周期钩子\n    onBeforeMount(()=>{\n      console.log('挂载之前')\n    })\n    onMounted(()=>{\n      console.log('挂载完毕')\n    })\n    onBeforeUpdate(()=>{\n      console.log('更新之前')\n    })\n    onUpdated(()=>{\n      console.log('更新完毕')\n    })\n    onBeforeUnmount(()=>{\n      console.log('卸载之前')\n    })\n    onUnmounted(()=>{\n      console.log('卸载完毕')\n    })\n  \u003C\u002Fscript>\n  ```\n\n## 3.14. 【自定义hook】\n\n- 什么是`hook`？—— 本质是一个函数，把`setup`函数中使用的`Composition API`进行了封装，类似于`vue2.x`中的`mixin`。\n\n- 自定义`hook`的优势：复用代码, 让`setup`中的逻辑更清楚易懂。\n\n示例代码：\n\n- `useSum.ts`中内容如下：\n\n  ```js\n  import {ref,onMounted} from 'vue'\n  \n  export default function(){\n    let sum = ref(0)\n  \n    const increment = ()=>{\n      sum.value += 1\n    }\n    const decrement = ()=>{\n      sum.value -= 1\n    }\n    onMounted(()=>{\n      increment()\n    })\n  \n    \u002F\u002F向外部暴露数据\n    return {sum,increment,decrement}\n  }\t\t\n  ```\n  \n- `useDog.ts`中内容如下：\n\n  ```js\n  import {reactive,onMounted} from 'vue'\n  import axios,{AxiosError} from 'axios'\n  \n  export default function(){\n    let dogList = reactive\u003Cstring[]>([])\n  \n    \u002F\u002F 方法\n    async function getDog(){\n      try {\n        \u002F\u002F 发请求\n        let {data} = await axios.get('https:\u002F\u002Fdog.ceo\u002Fapi\u002Fbreed\u002Fpembroke\u002Fimages\u002Frandom')\n        \u002F\u002F 维护数据\n        dogList.push(data.message)\n      } catch (error) {\n        \u002F\u002F 处理错误\n        const err = \u003CAxiosError>error\n        console.log(err.message)\n      }\n    }\n  \n    \u002F\u002F 挂载钩子\n    onMounted(()=>{\n      getDog()\n    })\n  \t\n    \u002F\u002F向外部暴露数据\n    return {dogList,getDog}\n  }\n  ```\n\n- 组件中具体使用：\n\n  ```vue\n  \u003Ctemplate>\n    \u003Ch2>当前求和为：{{sum}}\u003C\u002Fh2>\n    \u003Cbutton @click=\"increment\">点我+1\u003C\u002Fbutton>\n    \u003Cbutton @click=\"decrement\">点我-1\u003C\u002Fbutton>\n    \u003Chr>\n    \u003Cimg v-for=\"(u,index) in dogList.urlList\" :key=\"index\" :src=\"(u as string)\"> \n    \u003Cspan v-show=\"dogList.isLoading\">加载中......\u003C\u002Fspan>\u003Cbr>\n    \u003Cbutton @click=\"getDog\">再来一只狗\u003C\u002Fbutton>\n  \u003C\u002Ftemplate>\n  \n  \u003Cscript lang=\"ts\">\n    import {defineComponent} from 'vue'\n  \n    export default defineComponent({\n      name:'App',\n    })\n  \u003C\u002Fscript>\n  \n  \u003Cscript setup lang=\"ts\">\n    import useSum from '.\u002Fhooks\u002FuseSum'\n    import useDog from '.\u002Fhooks\u002FuseDog'\n  \t\n    let {sum,increment,decrement} = useSum()\n    let {dogList,getDog} = useDog()\n  \u003C\u002Fscript>\n  ```\n\n    \n\n---\n\n# 4. 路由\n\n## 4.1. 【对路由的理解】\n\nVue Router 是 Vue 官方的客户端路由解决方案。\n\n客户端路由的作用是在单页应用 (SPA) 中将浏览器的 URL 和用户看到的内容绑定起来。当用户在应用中浏览不同页面时，URL 会随之更新，但页面不需要从服务器重新加载。 \n\n## 4.2. 【基本切换效果】\n\n- `Vue3`中要使用`vue-router`的最新版本，目前是`4`版本。\n\n- 安装Vue Router.   npm i vue-router\n\n- 路由配置文件代码如下：\n\n- 创建新的文件夹router > index.ts\n\n  ```js\n  \u002F\u002F 创建一个路由器，并暴露出去\n  \n  \u002F\u002F 第一步：引入creatRouter\n  import {createRouter,createWebHistory} from 'vue-router'\n  \n  \u002F\u002F 引入一个一个可能要呈现的组件\n  import Home from '@\u002Fpages\u002FHome.vue'\n  import News from '@\u002Fpages\u002FNews.vue'\n  import About from '@\u002Fpages\u002FAbout.vue'\n  \n  \u002F\u002F 第二步：创建路由器\n  const router = createRouter({\n  \thistory:createWebHistory(),    \u002F\u002F 路由的工作模式\n  \troutes:[     \u002F\u002F 一个一个的路由规则\n  \t\t{\n  \t\t\tpath:'\u002Fhome',\n  \t\t\tcomponent:Home\n  \t\t},\n  \t\t{\n  \t\t\tpath:'\u002Fabout',\n  \t\t\tcomponent:About\n  \t\t}\n  \t]\n  })\n  \u002F\u002F 第三步：暴露出去router\n  export default router\n  ```\n* `main.ts`代码如下：\n\n  ```js\n  \u002F\u002F 引入 creatApp 用于创建应用\n  import { createApp } from \"vue\";\n  \n  \u002F\u002F 引入App根组件\n  import App from \".\u002FApp.vue\";\n  \u002F\u002F 引入路由器\n  import router from '.\u002Frouter\u002Findex'\n  \n  \u002F\u002F 创建一个应用\n   const app = createApp(App)\n  \u002F\u002F  使用路由器\n  app.use(router)\n  \u002F\u002F 挂载到整个应用到app容器中\n  app.mount('#app')\n  ```\n\n- `App.vue`代码如下\n\n  ```vue\n  \u003Ctemplate>\n    \u003Cdiv class=\"app\">\n      \u003Ch2 class=\"title\">Vue路由测试\u003C\u002Fh2>\n      \u003C!-- 导航区 -->\n      \u003Cdiv class=\"navigate\">\n        \u003CRouterLink to=\"\u002Fhome\" active-class=\"active\">首页\u003C\u002FRouterLink>\n        \u003CRouterLink to=\"\u002Fnews\" active-class=\"active\">新闻\u003C\u002FRouterLink>\n        \u003CRouterLink to=\"\u002Fabout\" active-class=\"active\">关于\u003C\u002FRouterLink>\n      \u003C\u002Fdiv>\n      \u003C!-- 展示区 -->\n      \u003Cdiv class=\"main-content\">\n        \u003CRouterView>\u003C\u002FRouterView>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  \u003C\u002Ftemplate>\n  \n  \u003Cscript lang=\"ts\" setup name=\"App\">\n    import {RouterLink,RouterView} from 'vue-router'  \n      \n      \u002F\u002F不同于常规的 \u003Ca> 标签，我们使用组件 RouterLink 来创建链接。这使得 Vue Router 能够在不重新加载页面的情况下改变 URL，处理 URL 的生成、编码和其他功能。我们将会在之后的部分深入了解 RouterLink 组件。\n      \n      \u002F\u002FRouterView 组件可以使 Vue Router 知道你想要在哪里渲染当前 URL 路径对应的路由组件。它不一定要在 App.vue 中，你可以把它放在任何地方，但它需要在某处被导入，否则 Vue Router 就不会渲染任何东西。\n  \u003C\u002Fscript>\n  \n  ```\n\n## 4.3. 【两个注意点】\n\n> 1. 路由组件通常存放在`pages` 或 `views`文件夹，一般组件通常存放在`components`文件夹。\n>\n> 2. 通过点击导航，视觉效果上“消失” 了的路由组件，默认是被**卸载**掉的，需要的时候再去**挂载**。\n\n## 4.4.【路由器工作模式】\n\n1. `history`模式\n\n   > 优点：`URL`更加美观，不带有`#`，更接近传统的网站`URL`。\n   >\n   > 缺点：后期项目上线，需要服务端配合处理路径问题，否则刷新会有`404`错误。\n   >\n   > ```js\n   > const router = createRouter({\n   >   \thistory:createWebHistory(), \u002F\u002Fhistory模式\n   >   \t\u002F******\u002F\n   > })\n   > ```\n\n2. `hash`模式\n\n   > 优点：兼容性更好，因为不需要服务器端处理路径。\n   >\n   > 缺点：`URL`带有`#`不太美观，且在`SEO`优化方面相对较差。\n   >\n   > ```js\n   > const router = createRouter({\n   >   \thistory:createWebHashHistory(), \u002F\u002Fhash模式\n   >   \t\u002F******\u002F\n   > })\n   > ```\n\n## 4.5. 【to的两种写法】\n\n```vue\n\u003C!-- 第一种：to的字符串写法 -->\n\u003Crouter-link active-class=\"active\" to=\"\u002Fhome\">主页\u003C\u002Frouter-link>\n\n\u003C!-- 第二种：to的对象写法 -->\n\u003Crouter-link active-class=\"active\" :to=\"{path:'\u002Fhome'}\">Home\u003C\u002Frouter-link>\n```\n\n## 4.6. 【命名路由】\n\n作用：可以简化路由跳转及传参（后面就讲）。\n\n给路由规则命名：\n\n```js\nroutes:[\n  {\n    name:'zhuye',\n    path:'\u002Fhome',\n    component:Home\n  },\n  {\n    name:'xinwen',\n    path:'\u002Fnews',\n    component:News,\n  },\n  {\n    name:'guanyu',\n    path:'\u002Fabout',\n    component:About\n  }\n]\n```\n\n跳转路由：\n\n```vue\n\u003C!--简化前：需要写完整的路径（to的字符串写法） -->\n\u003Crouter-link to=\"\u002Fnews\u002Fdetail\">跳转\u003C\u002Frouter-link>\n\n\u003C!--简化后：直接通过名字跳转（to的对象写法配合name属性） -->\n\u003Crouter-link :to=\"{name:'guanyu'}\">跳转\u003C\u002Frouter-link>\n```\n\n\n\n## 4.7. 【嵌套路由】\n\n1. 编写`News`的子路由：`Detail.vue`\n\n2. 配置路由规则，使用`children`配置项：\n\n   ```ts\n   const router = createRouter({\n     history:createWebHistory(),\n   \troutes:[\n   \t\t{\n   \t\t\tname:'zhuye',\n   \t\t\tpath:'\u002Fhome',\n   \t\t\tcomponent:Home\n   \t\t},\n   \t\t{\n   \t\t\tname:'xinwen',\n   \t\t\tpath:'\u002Fnews',\n   \t\t\tcomponent:News,\n   \t\t\tchildren:[\n   \t\t\t\t{\n   \t\t\t\t\tname:'xiang',\n   \t\t\t\t\tpath:'detail',\n   \t\t\t\t\tcomponent:Detail\n   \t\t\t\t}\n   \t\t\t]\n   \t\t},\n   \t\t{\n   \t\t\tname:'guanyu',\n   \t\t\tpath:'\u002Fabout',\n   \t\t\tcomponent:About\n   \t\t}\n   \t]\n   })\n   export default router\n   ```\n   \n3. 跳转路由（记得要加完整路径）：\n\n   ```vue\n   \u003Crouter-link to=\"\u002Fnews\u002Fdetail\">xxxx\u003C\u002Frouter-link>\n   \u003C!-- 或 -->\n   \u003Crouter-link :to=\"{path:'\u002Fnews\u002Fdetail'}\">xxxx\u003C\u002Frouter-link>\n   ```\n\n4. 记得去`Home`组件中预留一个`\u003Crouter-view>`\n\n   ```vue\n   \u003Ctemplate>\n     \u003Cdiv class=\"news\">\n       \u003Cnav class=\"news-list\">\n         \u003CRouterLink v-for=\"news in newsList\" :key=\"news.id\" :to=\"{path:'\u002Fnews\u002Fdetail'}\">\n           {{news.name}}\n         \u003C\u002FRouterLink>\n       \u003C\u002Fnav>\n       \u003Cdiv class=\"news-detail\">\n         \u003CRouterView\u002F>\n       \u003C\u002Fdiv>\n     \u003C\u002Fdiv>\n   \u003C\u002Ftemplate>\n   ```\n\n   \n\n## 4.8. 【路由传参】\n\n### query参数\n\n   1. 传递参数\n\n      ```vue\n      \u003C!-- 跳转并携带query参数（to的字符串写法） -->\n      \u003Crouter-link to=\"\u002Fnews\u002Fdetail?a=1&b=2&content=欢迎你\">\n      \t跳转\n      \u003C\u002Frouter-link>\n      \t\t\t\t\n      \u003C!-- 跳转并携带query参数（to的对象写法） -->\n      \u003CRouterLink \n        :to=\"{\n          \u002F\u002Fname:'xiang', \u002F\u002F用name也可以跳转\n          path:'\u002Fnews\u002Fdetail',\n          query:{\n            id:news.id,\n            title:news.title,\n            content:news.content\n          }\n        }\"\n      >\n        {{news.title}}\n      \u003C\u002FRouterLink>\n      ```\n\n   2. 接收参数：\n\n      ```js\n      import {useRoute} from 'vue-router'\n      const route = useRoute()\n      \u002F\u002F 打印query参数\n      console.log(route.query)\n      ```\n\n\n### params参数\n\n   1. 传递参数\n\n      ```vue\n      \u003C!-- 跳转并携带params参数（to的字符串写法） -->\n      \u003CRouterLink :to=\"`\u002Fnews\u002Fdetail\u002F001\u002F新闻001\u002F内容001`\">{{news.title}}\u003C\u002FRouterLink>\n      \t\t\t\t\n      \u003C!-- 跳转并携带params参数（to的对象写法） -->\n      \u003CRouterLink \n        :to=\"{\n          name:'xiang', \u002F\u002F用name跳转\n          params:{\n            id:news.id,\n            title:news.title,\n            content:news.title\n          }\n        }\"\n      >\n        {{news.title}}\n      \u003C\u002FRouterLink>\n      ```\n\n   2. 接收参数：\n\n      ```js\n      import {useRoute} from 'vue-router'\n      const route = useRoute()\n      \u002F\u002F 打印params参数\n      console.log(route.params)\n      ```\n\n> 备注1：传递`params`参数时，若使用`to`的对象写法，必须使用`name`配置项，不能用`path`。\n>\n> 备注2：传递`params`参数时，需要提前在规则中占位。\n\n## 4.9. 【路由的props配置】\n\n作用：让路由组件更方便的收到参数（可以将路由参数作为`props`传给组件）\n\n```js\n{\n\tname:'xiang',\n\tpath:'detail\u002F:id\u002F:title\u002F:content',\n\tcomponent:Detail,\n\n  \u002F\u002F props的对象写法，作用：把对象中的每一组key-value作为props传给Detail组件\n  \u002F\u002F props:{a:1,b:2,c:3}, \n\n  \u002F\u002F props的布尔值写法，作用：把收到了每一组params参数，作为props传给Detail组件\n  \u002F\u002F props:true\n  \n  \u002F\u002F props的函数写法，作用：把返回的对象中每一组key-value作为props传给Detail组件\n  props(route){\n    return route.query\n  }\n}\n```\n\n## 4.10. 【 replace属性】\n\n  1. 作用：控制路由跳转时操作浏览器历史记录的模式。\n\n  2. 浏览器的历史记录有两种写入方式：分别为```push```和```replace```：\n\n     - ```push```是追加历史记录（默认值）。\n     - `replace`是替换当前记录。\n\n  3. 开启`replace`模式：\n\n     ```vue\n     \u003CRouterLink replace .......>News\u003C\u002FRouterLink>\n     ```\n\n## 4.11. 【编程式导航】\n\n路由组件的两个重要的属性：`$route`和`$router`变成了两个`hooks`\n\n```js\nimport {useRoute,useRouter} from 'vue-router'\n\nconst route = useRoute()\nconst router = useRouter()\n\nconsole.log(route.query)\nconsole.log(route.parmas)\nconsole.log(router.push)\nconsole.log(router.replace)\n\n\n\u002F\u002F 倒计时跳转\nonMounted(() =>{\n        setTimeout(() =>{\n            router.push('\u002Fnews')\n            console.log('跳转成功');\n        },3000)\n    })\n\n\u002F\u002F 按钮点击跳转\nfunction showNews(news:NewInter){\n        \u002F\u002F 和to的用法相同，不过得传参数\n        router.push({\n            name:'xiangqing',\n                    query:{\n                        id:news.id,\n                        title:news.title,\n                        content:news.content\n                    }\n        })\n    }\n```\n\n## 4.12. 【重定向】\n\n1. 作用：将特定的路径，重新定向到已有路由。\n\n2. 具体编码：\n\n   ```js\n   {\n       path:'\u002F',\n       redirect:'\u002Fabout'\n   }\n   ```\n\n\n\n# 5. pinia \n\n[**Pinia是一个轻量级的状态管理库，专为Vue.js设计**。它允许在组件之间共享和管理状态，类似于Vuex，但提供了更简洁的API和更好的TypeScript集成。Pinia的优势在于其易用性和可扩展性，适合Vue 3的组合式API思维。](https:\u002F\u002Fblog.csdn.net\u002Ftyxjolin\u002Farticle\u002Fdetails\u002F130032199)\n\n## 5.1【准备一个效果】\n\n## ![image-20260115171150626](images\u002Fimage-20260115171150626.png)\n\n了解了一个id库，**npm i nanoid**\n\n```javascript\n\u002F\u002F 引入id字符串随机生成库\n    import { nanoid } from 'nanoid';\n\n\u002F\u002F nanoid是一个函数直接用\n\u002F\u002F 把请求回来的情话，包装成一个对象，插入到talkList中\n        let obj = {id:nanoid(),title:data}\n```\n\n\n\n## 5.2【搭建 pinia 环境】\n\n第一步：`npm install pinia`\n\n第二步：操作`src\u002Fmain.ts`\n\n```ts\nimport { createApp } from 'vue'\nimport App from '.\u002FApp.vue'\n\n\u002F* 引入createPinia，用于创建pinia *\u002F\nimport { createPinia } from 'pinia'\n\n\u002F* 创建pinia *\u002F\nconst pinia = createPinia()\nconst app = createApp(App)\n\n\u002F* 使用插件 *\u002F{}\napp.use(pinia)\napp.mount('#app')\n```\n\n此时开发者工具中已经有了`pinia`选项\n\n\u003Cimg src=\"https:\u002F\u002Fcdn.nlark.com\u002Fyuque\u002F0\u002F2023\u002Fpng\u002F35780599\u002F1684309952481-c67f67f9-d1a3-4d69-8bd6-2b381e003f31.png\" style=\"zoom:80%;border:1px solid black;border-radius:10px\" \u002F>\n\n## 5.3【存储+读取数据】\n\n1. `Store`是一个保存：**状态**、**业务逻辑** 的实体，每个组件都可以**读取**、**写入**它。\n\n2. 它有三个概念：`state`、`getter`、`action`，相当于组件中的： `data`、 `computed` 和 `methods`。\n\n3. 具体编码：`src\u002Fstore\u002Fcount.ts`\n\n   ```ts\n   \u002F\u002F 引入defineStore用于创建store\n   import {defineStore} from 'pinia'\n   \n   \u002F\u002F 定义并暴露一个store\n   export const useCountStore = defineStore('count',{\n     \u002F\u002F 动作\n     actions:{},\n     \u002F\u002F 状态\n     state(){\n       return {\n         sum:6\n       }\n     },\n     \u002F\u002F 计算\n     getters:{}\n   })\n   ```\n\n4. 具体编码：`src\u002Fstore\u002Ftalk.ts`\n\n   ```js\n   \u002F\u002F 引入defineStore用于创建store\n   import {defineStore} from 'pinia'\n   \n   \u002F\u002F 定义并暴露一个store\n   export const useTalkStore = defineStore('talk',{\n     \u002F\u002F 动作\n     actions:{},\n     \u002F\u002F 状态\n     state(){\n       return {\n         talkList:[\n           {id:'yuysada01',content:'你今天有点怪，哪里怪？怪好看的！'},\n        \t\t{id:'yuysada02',content:'草莓、蓝莓、蔓越莓，你想我了没？'},\n           {id:'yuysada03',content:'心里给你留了一块地，我的死心塌地'}\n         ]\n       }\n     },\n     \u002F\u002F 计算\n     getters:{}\n   })\n   ```\n   \n5. 组件中使用`state`中的数据\n\n   ```vue\n   \u003Ctemplate>\n     \u003Ch2>当前求和为：{{ sumStore.sum }}\u003C\u002Fh2>\n   \u003C\u002Ftemplate>\n   \n   \u003Cscript setup lang=\"ts\" name=\"Count\">\n     \u002F\u002F 引入对应的useXxxxxStore\t\n     import {useSumStore} from '@\u002Fstore\u002Fsum'\n     \n     \u002F\u002F 调用useXxxxxStore得到对应的store\n     const sumStore = useSumStore()\n   \u003C\u002Fscript>\n   ```\n\n   ```vue\n   \u003Ctemplate>\n   \t\u003Cul>\n       \u003Cli v-for=\"talk in talkStore.talkList\" :key=\"talk.id\">\n         {{ talk.content }}\n       \u003C\u002Fli>\n     \u003C\u002Ful>\n   \u003C\u002Ftemplate>\n   \n   \u003Cscript setup lang=\"ts\" name=\"Count\">\n     import axios from 'axios'\n     import {useTalkStore} from '@\u002Fstore\u002Ftalk'\n   \n     const talkStore = useTalkStore()\n   \u003C\u002Fscript>\n   ```\n\n   \n\n## 5.4.【修改数据】(三种方式)\n\n1. 第一种修改方式，直接修改\n\n   ```ts\n   countStore.sum = 666\n   ```\n\n2. 第二种修改方式：批量修改\n\n   ```ts\n   countStore.$patch({\n     sum:999,\n     school:'atguigu'\n   })\n   ```\n\n3. 第三种修改方式：借助`action`修改（`action`中可以编写一些业务逻辑）\n\n   ```js\n   import { defineStore } from 'pinia'\n   \n   export const useCountStore = defineStore('count', {\n     \u002F*************\u002F\n     actions: {\n       \u002F\u002F加\n       increment(value:number) {\n         if (this.sum \u003C 10) {\n           \u002F\u002F操作countStore中的sum\n           this.sum += value\n         }\n       },\n       \u002F\u002F减\n       decrement(value:number){\n         if(this.sum > 1){\n           this.sum -= value\n         }\n       }\n     },\n     \u002F*************\u002F\n   })\n   ```\n\n4. 组件中调用`action`即可\n\n   ```js\n   \u002F\u002F 使用countStore\n   const countStore = useCountStore()\n   \n   \u002F\u002F 调用对应action\n   countStore.incrementOdd(n.value)\n   ```\n\n\n## 5.5.【storeToRefs】\n\n- 借助`storeToRefs`将`store`中的数据转为`ref`对象，方便在模板中使用。\n  - 注意：`pinia`提供的`storeToRefs`只会将数据做转换，而`Vue`的`toRefs`会转换`store`中的方法，浪费内存。\n\n```vue\n\u003Ctemplate>\n\t\u003Cdiv class=\"count\">\n\t\t\u003Ch2>当前求和为：{{sum}}\u003C\u002Fh2>\n\t\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\" name=\"Count\">\n  import { useCountStore } from '@\u002Fstore\u002Fcount'\n  \u002F* 引入storeToRefs *\u002F\n  import { storeToRefs } from 'pinia'\n\n\t\u002F* 得到countStore *\u002F\n  const countStore = useCountStore()\n  \u002F* 使用storeToRefs转换countStore，随后解构 *\u002F\n  const {sum} = storeToRefs(countStore)\n\u003C\u002Fscript>\n\n```\n\n## 5.6.【getters】\n\n  1. 概念：当`state`中的数据，需要经过处理后再使用时，可以使用`getters`配置。\n\n  2. 追加```getters```配置。\n\n     ```js\n     \u002F\u002F 引入defineStore用于创建store\n     import {defineStore} from 'pinia'\n     \n     \u002F\u002F 定义并暴露一个store\n     export const useCountStore = defineStore('count',{\n       \u002F\u002F 动作\n       actions:{\n         \u002F************\u002F\n       },\n       \u002F\u002F 状态\n       state(){\n         return {\n           sum:1,\n           school:'atguigu'\n         }\n       },\n       \u002F\u002F 计算\n       getters:{\n         bigSum:(state):number => state.sum *10,\n         upperSchool():string{\n           return this. school.toUpperCase()\n         }\n       }\n     })\n     ```\n\n  3. 组件中读取数据：\n\n     ```js\n     const {increment,decrement} = countStore\n     let {sum,school,bigSum,upperSchool} = storeToRefs(countStore)\n     ```\n\n     \n\n## 5.7.【$subscribe】\n\n通过 store 的 `$subscribe()` 方法侦听 `state` 及其变化\n\n```ts\ntalkStore.$subscribe((mutate,state)=>{\n  console.log('LoveTalk',mutate,state)\n  localStorage.setItem('talk',JSON.stringify(talkList.value))\n})\n```\n\n\n\n## 5.8. 【store组合式写法】\n\n```ts\nimport {defineStore} from 'pinia'\nimport axios from 'axios'\nimport {nanoid} from 'nanoid'\nimport {reactive} from 'vue'\n\nexport const useTalkStore = defineStore('talk',()=>{\n  \u002F\u002F talkList就是state\n  const talkList = reactive(\n    JSON.parse(localStorage.getItem('talkList') as string) || []\n  )\n\n  \u002F\u002F getATalk函数相当于action\n  async function getATalk(){\n    \u002F\u002F 发请求，下面这行的写法是：连续解构赋值+重命名\n    let {data:{content:title}} = await axios.get('https:\u002F\u002Fapi.uomg.com\u002Fapi\u002Frand.qinghua?format=json')\n    \u002F\u002F 把请求回来的字符串，包装成一个对象\n    let obj = {id:nanoid(),title}\n    \u002F\u002F 放到数组中\n    talkList.unshift(obj)\n  }\n  return {talkList,getATalk}\n})\n```\n\n\n\n# 6. 组件通信\n\n**`Vue3`组件通信和`Vue2`的区别：**\n\n* 移出事件总线，使用`mitt`代替。\n\n- `vuex`换成了`pinia`。\n- 把`.sync`优化到了`v-model`里面了。\n- 把`$listeners`所有的东西，合并到`$attrs`中了。\n- `$children`被砍掉了。\n\n**常见搭配形式：**\n\n\u003Cimg src=\"images\u002Fimage-20231119185900990.png\" alt=\"image-20231119185900990\" style=\"zoom:60%;\" \u002F> \n\n## 6.1. 【props】\n\n概述：`props`是使用频率最高的一种通信方式，常用与 ：**父 ↔ 子**。\n\n- 若 **父传子**：属性值是**非函数**。\n- 若 **子传父**：属性值是**函数**。\n\n父组件：\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"father\">\n    \u003Ch3>父组件，\u003C\u002Fh3>\n\t\t\u003Ch4>我的车：{{ car }}\u003C\u002Fh4>\n\t\t\u003Ch4>儿子给的玩具：{{ toy }}\u003C\u002Fh4>\n\t\t\u003CChild :car=\"car\" :getToy=\"getToy\"\u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\" name=\"Father\">\n\timport Child from '.\u002FChild.vue'\n\timport { ref } from \"vue\";\n\t\u002F\u002F 数据\n\tconst car = ref('奔驰')\n\tconst toy = ref()\n\t\u002F\u002F 方法\n\tfunction getToy(value:string){\n\t\ttoy.value = value\n\t}\n\u003C\u002Fscript>\n```\n\n子组件\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"child\">\n    \u003Ch3>子组件\u003C\u002Fh3>\n\t\t\u003Ch4>我的玩具：{{ toy }}\u003C\u002Fh4>\n\t\t\u003Ch4>父给我的车：{{ car }}\u003C\u002Fh4>\n\t\t\u003Cbutton @click=\"getToy(toy)\">玩具给父亲\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\" name=\"Child\">\n\timport { ref } from \"vue\";\n\tconst toy = ref('奥特曼')\n\t\n\tdefineProps(['car','getToy'])\n\u003C\u002Fscript>\n```\n\n## 6.2. 【自定义事件】\n\n1. 概述：自定义事件常用于：**子 => 父。**\n2. 注意区分好：原生事件、自定义事件。\n\n- 原生事件：\n  - 事件名是特定的（`click`、`mosueenter`等等）\t\n  - 事件对象`$event`: 是包含事件相关信息的对象（`pageX`、`pageY`、`target`、`keyCode`）\n- 自定义事件：\n  - 事件名是任意名称\n  - \u003Cstrong style=\"color:red\">事件对象`$event`: 是调用`emit`时所提供的数据，可以是任意类型！！！\u003C\u002Fstrong >\n\n3. 示例：\n\n   ```html\n   \u003C!--在父组件中，给子组件绑定自定义事件：-->\n   \u003CChild @send-toy=\"toy = $event\"\u002F>\n   \n   \u003C!--注意区分原生事件与自定义事件中的$event-->\n   \u003Cbutton @click=\"toy = $event\">测试\u003C\u002Fbutton>\n   ```\n\n   ```js\n   \u002F\u002F子组件中，触发事件：\n   this.$emit('send-toy', 具体数据)\n   ```\n\n## 6.3. 【mitt】\n\n概述：与消息订阅与发布（`pubsub`）功能类似，可以实现任意组件间通信。\n\n安装`mitt`\n\n```shell\nnpm i mitt\n```\n\n新建文件：`src\\utils\\emitter.ts`\n\n```javascript\n\u002F\u002F 引入mitt \nimport mitt from \"mitt\";\n\n\u002F\u002F 创建emitter\nconst emitter = mitt()\n\n\u002F*\n  \u002F\u002F 绑定事件\n  emitter.on('abc',(value)=>{\n    console.log('abc事件被触发',value)\n  })\n  emitter.on('xyz',(value)=>{\n    console.log('xyz事件被触发',value)\n  })\n\n  setInterval(() => {\n    \u002F\u002F 触发事件\n    emitter.emit('abc',666)\n    emitter.emit('xyz',777)\n  }, 1000);\n\n  setTimeout(() => {\n    \u002F\u002F 清理事件\n    emitter.all.clear()\n  }, 3000); \n*\u002F\n\n\u002F\u002F 创建并暴露mitt\nexport default emitter\n```\n\n接收数据的组件中：绑定事件、同时在销毁前解绑事件：\n\n```typescript\nimport emitter from \"@\u002Futils\u002Femitter\";\nimport { onUnmounted } from \"vue\";\n\n\u002F\u002F 绑定事件\nemitter.on('send-toy',(value)=>{\n  console.log('send-toy事件被触发',value)\n})\n\nonUnmounted(()=>{\n  \u002F\u002F 解绑事件\n  emitter.off('send-toy')\n})\n```\n\n【第三步】：提供数据的组件，在合适的时候触发事件\n\n```javascript\nimport emitter from \"@\u002Futils\u002Femitter\";\n\nfunction sendToy(){\n  \u002F\u002F 触发事件\n  emitter.emit('send-toy',toy.value)\n}\n```\n\n**注意这个重要的内置关系，总线依赖着这个内置关系**\n\n## 6.4.【v-model】\n\n1. 概述：实现 **父↔子** 之间相互通信。\n\n2. 前序知识 —— `v-model`的本质\n\n   ```vue\n   \u003C!-- 使用v-model指令 -->\n   \u003Cinput type=\"text\" v-model=\"userName\">\n   \n   \u003C!-- v-model的本质是下面这行代码 -->\n   \u003Cinput \n     type=\"text\" \n     :value=\"userName\" \n     @input=\"userName =(\u003CHTMLInputElement>$event.target).value\"\n   >\n   ```\n\n3. 组件标签上的`v-model`的本质：`:moldeValue` ＋ `update:modelValue`事件。\n\n   ```vue\n   \u003C!-- 组件标签上使用v-model指令 -->\n   \u003CAtguiguInput v-model=\"userName\"\u002F>\n   \n   \u003C!-- 组件标签上v-model的本质 -->\n   \u003CAtguiguInput :modelValue=\"userName\" @update:model-value=\"userName = $event\"\u002F>\n   ```\n\n   `AtguiguInput`组件中：\n\n   ```vue\n   \u003Ctemplate>\n     \u003Cdiv class=\"box\">\n       \u003C!--将接收的value值赋给input元素的value属性，目的是：为了呈现数据 -->\n   \t\t\u003C!--给input元素绑定原生input事件，触发input事件时，进而触发update:model-value事件-->\n       \u003Cinput \n          type=\"text\" \n          :value=\"modelValue\" \n          @input=\"emit('update:model-value',$event.target.value)\"\n       >\n     \u003C\u002Fdiv>\n   \u003C\u002Ftemplate>\n   \n   \u003Cscript setup lang=\"ts\" name=\"AtguiguInput\">\n     \u002F\u002F 接收props\n     defineProps(['modelValue'])\n     \u002F\u002F 声明事件\n     const emit = defineEmits(['update:model-value'])\n   \u003C\u002Fscript>\n   ```\n\n4. 也可以更换`value`，例如改成`abc`\n\n   ```vue\n   \u003C!-- 也可以更换value，例如改成abc-->\n   \u003CAtguiguInput v-model:abc=\"userName\"\u002F>\n   \n   \u003C!-- 上面代码的本质如下 -->\n   \u003CAtguiguInput :abc=\"userName\" @update:abc=\"userName = $event\"\u002F>\n   ```\n\n   `AtguiguInput`组件中：\n\n   ```vue\n   \u003Ctemplate>\n     \u003Cdiv class=\"box\">\n       \u003Cinput \n          type=\"text\" \n          :value=\"abc\" \n          @input=\"emit('update:abc',$event.target.value)\"\n       >\n     \u003C\u002Fdiv>\n   \u003C\u002Ftemplate>\n   \n   \u003Cscript setup lang=\"ts\" name=\"AtguiguInput\">\n     \u002F\u002F 接收props\n     defineProps(['abc'])\n     \u002F\u002F 声明事件\n     const emit = defineEmits(['update:abc'])\n   \u003C\u002Fscript>\n   ```\n\n5. 如果`value`可以更换，那么就可以在组件标签上多次使用`v-model`\n\n   ```vue\n   \u003CAtguiguInput v-model:abc=\"userName\" v-model:xyz=\"password\"\u002F>\n   ```\n\n   \n\n\n## 6.5.【$attrs 】\n\n1. 概述：`$attrs`用于实现**当前组件的父组件**，向**当前组件的子组件**通信（**祖→孙**）。\n\n2. 具体说明：`$attrs`是一个对象，包含所有父组件传入的标签属性。\n\n   >  注意：`$attrs`会自动排除`props`中声明的属性(可以认为声明过的 `props` 被子组件自己“消费”了)\n\n父组件：\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"father\">\n    \u003Ch3>父组件\u003C\u002Fh3>\n\t\t\u003CChild :a=\"a\" :b=\"b\" :c=\"c\" :d=\"d\" v-bind=\"{x:100,y:200}\" :updateA=\"updateA\"\u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\" name=\"Father\">\n\timport Child from '.\u002FChild.vue'\n\timport { ref } from \"vue\";\n\tlet a = ref(1)\n\tlet b = ref(2)\n\tlet c = ref(3)\n\tlet d = ref(4)\n\n\tfunction updateA(value){\n\t\ta.value = value\n\t}\n\u003C\u002Fscript>\n```\n\n子组件：\n\n```vue\n\u003Ctemplate>\n\t\u003Cdiv class=\"child\">\n\t\t\u003Ch3>子组件\u003C\u002Fh3>\n\t\t\u003CGrandChild v-bind=\"$attrs\"\u002F>\n\t\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\" name=\"Child\">\n\timport GrandChild from '.\u002FGrandChild.vue'\n\u003C\u002Fscript>\n```\n\n孙组件：\n\n```vue\n\u003Ctemplate>\n\t\u003Cdiv class=\"grand-child\">\n\t\t\u003Ch3>孙组件\u003C\u002Fh3>\n\t\t\u003Ch4>a：{{ a }}\u003C\u002Fh4>\n\t\t\u003Ch4>b：{{ b }}\u003C\u002Fh4>\n\t\t\u003Ch4>c：{{ c }}\u003C\u002Fh4>\n\t\t\u003Ch4>d：{{ d }}\u003C\u002Fh4>\n\t\t\u003Ch4>x：{{ x }}\u003C\u002Fh4>\n\t\t\u003Ch4>y：{{ y }}\u003C\u002Fh4>\n\t\t\u003Cbutton @click=\"updateA(666)\">点我更新A\u003C\u002Fbutton>\n\t\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\" name=\"GrandChild\">\n\tdefineProps(['a','b','c','d','x','y','updateA'])\n\u003C\u002Fscript>\n```\n\n## 6.6. 【$refs、$parent】\n\n1. 概述：\n\n   * `$refs`用于 ：**父→子。**\n   * `$parent`用于：**子→父。**\n\n2. 原理如下：\n\n   | 属性      | 说明                                                     |\n   | --------- | -------------------------------------------------------- |\n   | `$refs`   | 值为对象，包含所有被`ref`属性标识的`DOM`元素或组件实例。 |\n   | `$parent` | 值为对象，当前组件的父组件实例对象。                     |\n\n## 6.7. 【provide、inject】\n\n1. 概述：实现**祖孙组件**直接通信\n\n2. 具体使用：\n\n   * 在祖先组件中通过`provide`配置向后代组件提供数据\n   * 在后代组件中通过`inject`配置来声明接收数据\n\n4. 具体编码：\n\n   【第一步】父组件中，使用`provide`提供数据\n\n   ```vue\n   \u003Ctemplate>\n     \u003Cdiv class=\"father\">\n       \u003Ch3>父组件\u003C\u002Fh3>\n       \u003Ch4>资产：{{ money }}\u003C\u002Fh4>\n       \u003Ch4>汽车：{{ car }}\u003C\u002Fh4>\n       \u003Cbutton @click=\"money += 1\">资产+1\u003C\u002Fbutton>\n       \u003Cbutton @click=\"car.price += 1\">汽车价格+1\u003C\u002Fbutton>\n       \u003CChild\u002F>\n     \u003C\u002Fdiv>\n   \u003C\u002Ftemplate>\n   \n   \u003Cscript setup lang=\"ts\" name=\"Father\">\n     import Child from '.\u002FChild.vue'\n     import { ref,reactive,provide } from \"vue\";\n     \u002F\u002F 数据\n     let money = ref(100)\n     let car = reactive({\n       brand:'奔驰',\n       price:100\n     })\n     \u002F\u002F 用于更新money的方法\n     function updateMoney(value:number){\n       money.value += value\n     }\n     \u002F\u002F 提供数据\n     provide('moneyContext',{money,updateMoney})\n     provide('car',car)\n   \u003C\u002Fscript>\n   ```\n   \n   > 注意：子组件中不用编写任何东西，是不受到任何打扰的\n   \n   【第二步】孙组件中使用`inject`配置项接受数据。\n   \n   ```vue\n   \u003Ctemplate>\n     \u003Cdiv class=\"grand-child\">\n       \u003Ch3>我是孙组件\u003C\u002Fh3>\n       \u003Ch4>资产：{{ money }}\u003C\u002Fh4>\n       \u003Ch4>汽车：{{ car }}\u003C\u002Fh4>\n       \u003Cbutton @click=\"updateMoney(6)\">点我\u003C\u002Fbutton>\n     \u003C\u002Fdiv>\n   \u003C\u002Ftemplate>\n   \n   \u003Cscript setup lang=\"ts\" name=\"GrandChild\">\n     import { inject } from 'vue';\n     \u002F\u002F 注入数据\n    let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(x:number)=>{}})\n     let car = inject('car')\n\u003C\u002Fscript>\n   ```\n\n\n## 6.8. 【pinia】\n\n参考之前`pinia`部分的讲解\n\n## 6.9. 【slot】\n\n### 1. 默认插槽\n\n![img](http:\u002F\u002F49.232.112.44\u002Fimages\u002Fdefault_slot.png)\n\n```vue\n父组件中：\n        \u003CCategory title=\"今日热门游戏\">\n          \u003Cul>\n            \u003Cli v-for=\"g in games\" :key=\"g.id\">{{ g.name }}\u003C\u002Fli>\n          \u003C\u002Ful>\n        \u003C\u002FCategory>\n子组件中：\n        \u003Ctemplate>\n          \u003Cdiv class=\"item\">\n            \u003Ch3>{{ title }}\u003C\u002Fh3>\n            \u003C!-- 默认插槽 -->\n            \u003Cslot>\u003C\u002Fslot>\n          \u003C\u002Fdiv>\n        \u003C\u002Ftemplate>\n```\n\n### 2. 具名插槽\n\n```vue\n父组件中：\n        \u003CCategory title=\"今日热门游戏\">\n          \u003Ctemplate v-slot:s1>\n            \u003Cul>\n              \u003Cli v-for=\"g in games\" :key=\"g.id\">{{ g.name }}\u003C\u002Fli>\n            \u003C\u002Ful>\n          \u003C\u002Ftemplate>\n          \u003Ctemplate #s2>\n            \u003Ca href=\"\">更多\u003C\u002Fa>\n          \u003C\u002Ftemplate>\n        \u003C\u002FCategory>\n子组件中：\n        \u003Ctemplate>\n          \u003Cdiv class=\"item\">\n            \u003Ch3>{{ title }}\u003C\u002Fh3>\n            \u003Cslot name=\"s1\">\u003C\u002Fslot>\n            \u003Cslot name=\"s2\">\u003C\u002Fslot>\n          \u003C\u002Fdiv>\n        \u003C\u002Ftemplate>\n```\n\n### 3. 作用域插槽 \n\n1. 理解：\u003Cspan style=\"color:red\">数据在组件的自身，但根据数据生成的结构需要组件的使用者来决定。\u003C\u002Fspan>（新闻数据在`News`组件中，但使用数据所遍历出来的结构由`App`组件决定）\n\n3. 具体编码：\n\n   ```vue\n   父组件中：\n         \u003CGame v-slot=\"params\">\n         \u003C!-- \u003CGame v-slot:default=\"params\"> -->\n         \u003C!-- \u003CGame #default=\"params\"> -->\n           \u003Cul>\n             \u003Cli v-for=\"g in params.games\" :key=\"g.id\">{{ g.name }}\u003C\u002Fli>\n           \u003C\u002Ful>\n         \u003C\u002FGame>\n   \n   子组件中：\n         \u003Ctemplate>\n           \u003Cdiv class=\"category\">\n             \u003Ch2>今日游戏榜单\u003C\u002Fh2>\n             \u003Cslot :games=\"games\" a=\"哈哈\">\u003C\u002Fslot>\n           \u003C\u002Fdiv>\n         \u003C\u002Ftemplate>\n   \n         \u003Cscript setup lang=\"ts\" name=\"Category\">\n           import {reactive} from 'vue'\n           let games = reactive([\n             {id:'asgdytsa01',name:'英雄联盟'},\n             {id:'asgdytsa02',name:'王者荣耀'},\n             {id:'asgdytsa03',name:'红色警戒'},\n             {id:'asgdytsa04',name:'斗罗大陆'}\n           ])\n         \u003C\u002Fscript>\n   ```\n\n\n\n# 7. 其它 API\n\n## 7.1.【shallowRef 与 shallowReactive 】\n\n### `shallowRef`\n\n1. 作用：创建一个响应式数据，但只对顶层属性进行响应式处理。\n\n2. 用法：\n\n   ```js\n   let myVar = shallowRef(initialValue);\n   ```\n\n3. 特点：只跟踪引用值的变化，不关心值内部的属性变化。\n\n### `shallowReactive`\n\n1. 作用：创建一个浅层响应式对象，只会使对象的最顶层属性变成响应式的，对象内部的嵌套属性则不会变成响应式的\n\n2. 用法：\n\n   ```js\n   const myObj = shallowReactive({ ... });\n   ```\n\n3. 特点：对象的顶层属性是响应式的，但嵌套对象的属性不是。\n\n### 总结\n\n> 通过使用 [`shallowRef()`](https:\u002F\u002Fcn.vuejs.org\u002Fapi\u002Freactivity-advanced.html#shallowref) 和 [`shallowReactive()`](https:\u002F\u002Fcn.vuejs.org\u002Fapi\u002Freactivity-advanced.html#shallowreactive) 来绕开深度响应。浅层式 `API` 创建的状态只在其顶层是响应式的，对所有深层的对象不会做任何处理，避免了对每一个内部属性做响应式所带来的性能成本，这使得属性的访问变得更快，可提升性能。\n\n\n\n## 7.2.【readonly 与 shallowReadonly】\n\n### **`readonly`**\n\n1. 作用：用于创建一个对象的深只读副本。\n\n2. 用法：\n\n   ```js\n   const original = reactive({ ... });\n   const readOnlyCopy = readonly(original);\n   ```\n\n3. 特点：\n\n   * 对象的所有嵌套属性都将变为只读。\n   * 任何尝试修改这个对象的操作都会被阻止（在开发模式下，还会在控制台中发出警告）。\n\n4. 应用场景：\n   * 创建不可变的状态快照。\n   * 保护全局状态或配置不被修改。\n\n### **`shallowReadonly`**\n\n1. 作用：与 `readonly` 类似，但只作用于对象的顶层属性。\n\n2. 用法：\n\n   ```js\n   const original = reactive({ ... });\n   const shallowReadOnlyCopy = shallowReadonly(original);\n   ```\n\n3. 特点：\n\n   * 只将对象的顶层属性设置为只读，对象内部的嵌套属性仍然是可变的。\n\n   * 适用于只需保护对象顶层属性的场景。\n\n     \n\n## 7.3.【toRaw 与 markRaw】\n\n### `toRaw`\n\n1. 作用：用于获取一个响应式对象的原始对象， `toRaw` 返回的对象不再是响应式的，不会触发视图更新。\n\n   > 官网描述：这是一个可以用于临时读取而不引起代理访问\u002F跟踪开销，或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用，请谨慎使用。\n\n   > 何时使用？ —— 在需要将响应式对象传递给非 `Vue` 的库或外部系统时，使用 `toRaw` 可以确保它们收到的是普通对象\n\n2. 具体编码：\n\n   ```js\n   import { reactive,toRaw,markRaw,isReactive } from \"vue\";\n   \n   \u002F* toRaw *\u002F\n   \u002F\u002F 响应式对象\n   let person = reactive({name:'tony',age:18})\n   \u002F\u002F 原始对象\n   let rawPerson = toRaw(person)\n   \n   \n   \u002F* markRaw *\u002F\n   let citysd = markRaw([\n     {id:'asdda01',name:'北京'},\n     {id:'asdda02',name:'上海'},\n     {id:'asdda03',name:'天津'},\n     {id:'asdda04',name:'重庆'}\n   ])\n   \u002F\u002F 根据原始对象citys去创建响应式对象citys2 —— 创建失败，因为citys被markRaw标记了\n   let citys2 = reactive(citys)\n   console.log(isReactive(person))\n   console.log(isReactive(rawPerson))\n   console.log(isReactive(citys))\n   console.log(isReactive(citys2))\n   ```\n\n### `markRaw`\n\n1. 作用：标记一个对象，使其**永远不会**变成响应式的。\n\n   > 例如使用`mockjs`时，为了防止误把`mockjs`变为响应式对象，可以使用 `markRaw` 去标记`mockjs`\n\n2. 编码：\n\n   ```js\n   \u002F* markRaw *\u002F\n   let citys = markRaw([\n     {id:'asdda01',name:'北京'},\n     {id:'asdda02',name:'上海'},\n     {id:'asdda03',name:'天津'},\n     {id:'asdda04',name:'重庆'}\n   ])\n   \u002F\u002F 根据原始对象citys去创建响应式对象citys2 —— 创建失败，因为citys被markRaw标记了\n   let citys2 = reactive(citys)\n   ```\n\n## 7.4.【customRef】\n\n作用：创建一个自定义的`ref`，并对其依赖项跟踪和更新触发进行逻辑控制。\n\n实现防抖效果（`useSumRef.ts`）：\n\n```typescript\nimport {customRef } from \"vue\";\n\nexport default function(initValue:string,delay:number){\n  let msg = customRef((track,trigger)=>{\n    let timer:number\n    return {\n      get(){\n        track() \u002F\u002F 告诉Vue数据msg很重要，要对msg持续关注，一旦变化就更新\n        return initValue\n      },\n      set(value){\n        clearTimeout(timer)\n        timer = setTimeout(() => {\n          initValue = value\n          trigger() \u002F\u002F通知Vue数据msg变化了\n        }, delay);\n      }\n    }\n  }) \n  return {msg}\n}\n```\n\n组件中使用：\n\n\n\n\n\n# 8. Vue3新组件\n\n## 8.1. 【Teleport】\n\n- 什么是Teleport？—— Teleport 是一种能够将我们的**组件html结构**移动到指定位置的技术。\n\n```html\n\u003Cteleport to='body' >\n    \u003Cdiv class=\"modal\" v-show=\"isShow\">\n      \u003Ch2>我是一个弹窗\u003C\u002Fh2>\n      \u003Cp>我是弹窗中的一些内容\u003C\u002Fp>\n      \u003Cbutton @click=\"isShow = false\">关闭弹窗\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n\u003C\u002Fteleport>\n```\n\n## 8.2. 【Suspense】\n\n-  等待异步组件时渲染一些额外内容，让应用有更好的用户体验 \n-  使用步骤： \n   -  异步引入组件\n   -  使用`Suspense`包裹组件，并配置好`default` 与 `fallback`\n\n```tsx\nimport { defineAsyncComponent,Suspense } from \"vue\";\nconst Child = defineAsyncComponent(()=>import('.\u002FChild.vue'))\n```\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"app\">\n        \u003Ch3>我是App组件\u003C\u002Fh3>\n        \u003CSuspense>\n          \u003Ctemplate v-slot:default>\n            \u003CChild\u002F>\n          \u003C\u002Ftemplate>\n          \u003Ctemplate v-slot:fallback>\n            \u003Ch3>加载中.......\u003C\u002Fh3>\n          \u003C\u002Ftemplate>\n        \u003C\u002FSuspense>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n\n\n## 8.3.【全局API转移到应用对象】\n\n- `app.component`\n- `app.config`\n- `app.directive`\n- `app.mount`\n- `app.unmount`\n- `app.use`\n\n## 8.4.【其他】\n\n- 过渡类名 `v-enter` 修改为 `v-enter-from`、过渡类名 `v-leave` 修改为 `v-leave-from`。\n\n\n- `keyCode` 作为 `v-on` 修饰符的支持。\n\n- `v-model` 指令在组件上的使用已经被重新设计，替换掉了 `v-bind.sync。`\n\n- `v-if` 和 `v-for` 在同一个元素身上使用时的优先级发生了变化。\n\n- 移除了`$on`、`$off` 和 `$once` 实例方法。\n\n- 移除了过滤器 `filter`。\n\n- 移除了`$children` 实例 `propert`。\n\n  ......\n",37,"2026-04-07T16:18:29.196Z","2026-04-07T16:18:29.200Z","2026-05-25T14:45:15.078Z",{"id":92,"categoryName":27,"slug":28,"description":29,"sort":30,"isEnable":26,"createTime":31,"updateTime":32,"deleteTime":30},[162],{"articleId":152,"tagId":73,"createTime":158,"tag":163},{"id":73,"tagName":164,"slug":164,"themeColor":165,"description":166,"createTime":167,"updateTime":168,"deleteTime":78},"Vue3","#00d9ff","记录学习Vue3","2026-04-03T02:42:17.413Z","2026-04-03T02:42:38.863Z",{"id":170,"title":171,"slug":172,"coverUrl":15,"summary":15,"content":173,"htmlContent":72,"categoryId":19,"viewCount":174,"likeCount":30,"isTop":30,"isPublish":26,"seoKeywords":15,"seoDescription":15,"publishTime":175,"createTime":176,"updateTime":177,"deleteTime":78,"category":178,"tags":179},"11","MySQL 全栈开发学习笔记","mysql全栈开发学习笔记","# MySQL 全栈开发学习笔记\n\n> 专为前端工程师转全栈设计的 MySQL 学习笔记\n\n---\n\n## 目录\n\n- [一、MySQL 基础概念](#一mysql-基础概念)\n- [二、SQL 语句基础](#二sql-语句基础)\n- [三、数据库和表的操作（DDL）](#三数据库和表的操作ddl)\n- [四、数据的增删改（DML）](#四数据的增删改dml)\n- [五、数据查询（DQL）](#五数据查询dql)\n- [六、约束与数据完整性](#六约束与数据完整性)\n- [七、多表操作](#七多表操作)\n- [八、视图](#八视图)\n- [九、前端开发实战示例](#九前端开发实战示例)\n\n---\n\n## 一、MySQL 基础概念\n\n### 1.1 为什么前端工程师需要学习 MySQL\n\n作为全栈开发者，你需要掌握数据库技能来：\n- 存储和管理用户数据\n- 实现后端 API 的数据持久化\n- 理解数据结构和关系设计\n- 优化查询性能\n\n### 1.2 核心概念\n\n**数据库（Database, DB）**\n- 存储数据的仓库，数据是有组织的进行存储\n- 类比：类似于前端的状态管理（如 Redux、Vuex），但数据持久化到磁盘\n\n**数据库管理系统（DBMS）**\n- 管理数据库的大型软件\n- MySQL 就是一种 DBMS\n\n**SQL（Structured Query Language）**\n- 结构化查询语言，操作关系型数据库的编程语言\n- 类比：类似于前端的 DOM 操作 API\n\n### 1.3 MySQL 数据模型\n\n关系型数据库由多张能互相连接的二维表组成：\n\n```\n数据库服务器\n    ├── 数据库1\n    │   ├── 表1（用户表）\n    │   │   ├── 列1（id）\n    │   │   ├── 列2（name）\n    │   │   └── 列3（email）\n    │   └── 表2（订单表）\n    └── 数据库2\n```\n\n**优点：**\n- 使用表结构，格式一致，易于维护\n- 使用通用的 SQL 语言操作\n- 数据存储在磁盘中，安全\n\n### 1.4 常见的关系型数据库\n\n| 数据库 | 特点 | 适用场景 |\n|--------|------|----------|\n| MySQL | 开源免费，中小型 | Web 应用、中小型项目 |\n| PostgreSQL | 开源免费，功能强大 | 复杂数据处理、地理信息 |\n| SQLite | 嵌入式微型数据库 | 移动应用、本地存储 |\n| Oracle | 收费大型数据库 | 企业级应用 |\n| SQL Server | Microsoft 收费中型 | .NET 生态 |\n\n---\n\n## 二、SQL 语句基础\n\n### 2.1 SQL 简介\n\nSQL（Structured Query Language）是操作关系型数据库的统一标准语言。\n\n**类比前端：**\n- SQL 就像是数据库的\"查询语言\"\n- 类似于前端的 CSS 选择器或 XPath\n\n### 2.2 SQL 通用语法\n\n```sql\n-- SQL 语句可以单行或多行书写，以分号结尾\nSELECT * FROM users;\n\n-- MySQL 不区分大小写，但关键字建议使用大写\nselect * from users;  -- 可以\nSELECT * FROM users;  -- 推荐\n\n-- 注释\n-- 单行注释（方式1）\n# 单行注释（方式2，MySQL 特有）\n\n\u002F* \n多行注释\n可以写多行\n*\u002F\n```\n\n### 2.3 SQL 分类\n\n| 分类 | 全称 | 作用 | 类比前端 |\n|------|------|------|----------|\n| DDL | Data Definition Language | 定义数据库对象：数据库、表、列等 | 创建\u002F删除 DOM 元 |\n | DML | Data Manipulation Language | 对表的数据进行增删改 | 修改 DOM 内容 |\n| DQL | Data Query Language | 查询表的记录 | 查询 DOM 元素 |\n| DCL | Data Control Language | 定义访问权限和安全级别 | 权限控制 |\n\n---\n\n## 三、数据库和表的操作（DDL）\n\n### 3.1 操作数据库\n\n```sql\n-- 查询所有数据库\nSHOW DATABASES;\n\n-- 创建数据库\nCREATE DATABASE myapp;\n\n-- 创建数据库（如果不存在则创建）\nCREATE DATABASE IF NOT EXISTS myapp;\n\n-- 删除数据库\nDROP DATABASE myapp;\n\n-- 删除数据库（如果存在则删除）\nDROP DATABASE IF EXISTS myapp;\n\n-- 使用数据库\nUSE myapp;\n\n-- 查看当前使用的数据库\nSELECT DATABASE();\n```\n\n### 3.2 数据类型\n\nMySQL 支持多种数据类型：\n\n**数值类型：**\n- `INT`：整数（类似 JavaScript 的 Number）\n- `DOUBLE(m,d)`：浮点数，m 总位数，d 小数位数\n- `DECIMAL(m,d)`：精确小数（金融计算推荐）\n\n**字符串类型：**\n- `VARCHAR(n)`：可变长度字符串，最大 n 个字符\n- `CHAR(n)`：固定长度字符串\n- `TEXT`：长文本\n\n**日期类型：**\n- `DATE`：日期（YYYY-MM-DD）\n- `DATETIME`：日期时间（YYYY-MM-DD HH:MM:SS）\n- `TIMESTAMP`：时间戳\n\n### 3.3 创建表\n\n```sql\n-- 基本语法\nCREATE TABLE 表名 (\n    字段名1 数据类型1,\n    字段名2 数据类型2,\n    ...\n    字段名n 数据类型n\n);\n\n-- 示例：创建用户表\nCREATE TABLE users (\n    id INT,\n    username VARCHAR(50),\n    email VARCHAR(100),\n    age INT,\n    create_time DATETIME\n);\n```\n\n**实战示例：设计一个电商用户表**\n\n```sql\nCREATE TABLE users (\n    id INT,                    -- 用户ID\n    username VARCHAR(20),      -- 用户名，最长20个字符\n    password VARCHAR(64),       -- 密码（加密后）\n    email VARCHAR(100),         -- 邮箱\n    phone VARCHAR(20),         -- 手机号\n    gender CHAR(1),            -- 性别：M\u002FF\n    birthday DATE,              -- 生日\n    Bbalance DECIMAL(10,2),    -- 余额，小数点后2位\n    status TINYINT,            -- 状态：0-禁用，1-正常\n    create_time DATETIME,      -- 创建时间\n    update_time DATETIME       -- 更新时间\n);\n```\n\n### 3.4 查询表\n\n```sql\n-- 查询当前数据库所有表\nSHOW TABLES;\n\n-- 查询表结构\nDESC users;\n\n-- 查看建表语句\nSHOW CREATE TABLE users;\n```\n\n### 3.5 修改表\n\n```sql\n-- 修改表名\nALTER TABLE users RENAME TO app_users;\n\n-- 添加列\nALTER TABLE users ADD COLUMN nickname VARCHAR(50);\n\n-- 修改数据类型\nALTER TABLE users MODIFY COLUMN username VARCHAR(30);\n\n-- 修改列名和数据类型\nALTER TABLE users CHANGE COLUMN nickname display_name VARCHAR(50);\n\n-- 删除列\nALTER TABLE users DROP COLUMN nickname;\n```\n\n### 3.6 删除表\n\n```sql\n-- 删除表\nDROP TABLE users;\n\n-- 删除表（如果存在）\nDROP TABLE IF EXISTS users;\n```\n\n---\n\n## 四、数据的增删改（DML）\n\n### 4.1 添加数据（INSERT）\n\n```sql\n-- 给指定列添加数据\nINSERT INTO users (id, username, email) VALUES (1, '张三', 'zhangsan@example.com');\n\n-- 给全部列添加数据（值的顺序要与表结构一致）\nINSERT INTO users VALUES (2, '李四', 'lisi@example.com', 25, NOW());\n\n-- 批量添加数据\nINSERT INTO users (id, username, email) VALUES\n    (3, '王五', 'wangwu@example.com'),\n    (4, '赵六', 'zhaoliu@example.com'),\n    (5, '孙七', 'sunqi@example.com');\n\n-- 批量添加数据（全部列）\nINSERT INTO users VALUES\n    (6, '周八', 'zhouba@example.com', 30, NOW()),\n    (7, '吴九', 'wujiu@example.com', 28, NOW());\n```\n\n### 4.2. 修改数据（UPDATE）\n\n```sql\n-- 修改数据（带条件）\nUPDATE users SET age = 26 WHERE id = 1;\n\n-- 修改多个字段\nUPDATE users \nSET age = 27, email = 'newemail@example.com' \nWHERE id = 2;\n\n-- 注意：如果不加 WHERE 条件，会修改所有数据！\nUPDATE users SET status = 0;  -- 危险！会把所有用户状态都改为0\n```\n\n### 4.3 删除数据（DELETE）\n\n```sql\n-- 删除数据（带条件）\nDELETE FROM users WHERE id = 1;\n\n-- 删除多条数据\nDELETE FROM users WHERE id IN (2, 3, 4);\n\n-- 注意：如果不加 WHERE 条件，会删除所有数据！\nDELETE FROM users;  -- 危险！会清空整个表\n```\n\n---\n\n## 五、数据查询（DQL）\n\n### 5.1 基础查询语法\n\n```sql\nSELECT \n    字段列表\nFROM \n    表名列表 \nWHERE \n    条件列表\nGROUP BY\n    分组字段\nHAVING\n    分组后条件\nORDER BY\n    排序字段\nLIMIT\n    分页限定\n```\n\n### 5.2 基础查询\n\n```sql\n-- 查询指定字段\nSELECT username, email FROM users;\n\n-- 查询所有字段\nSELECT * FROM users;\n\n-- 去除重复记录\nSELECT DISTINCT status FROM users;\n\n-- 起别名\nSELECT \n    username AS '用户名',\n    email AS '邮箱地址',\n    age AS '年龄'\nFROM users;\n\n-- AS 可以省略\nSELECT username '用户名', email '邮箱' FROM users;\n```\n\n### 5.3 条件查询（WHERE）\n\n**条件运算符：**\n- `>`、`\u003C`、`=`、`>=`、`\u003C=`、`!=`、`\u003C>`\n- `BETWEEN ... AND ...`：在某个范围内\n- `IN(...)`：在多个值集合中\n- `LIKE`：模糊查询\n- `IS NULL`、`IS NOT NULL`：判断空值\n- `AND`、`OR`：逻辑运算符\n\n```sql\n-- 查询年龄大于25的用户\nSELECT * FROM users WHERE age > 25;\n\n-- 查询年龄在20到30之间的用户\nSELECT * FROM users WHERE age >= 20 AND age \u003C= 30;\nSELECT * FROM users WHERE age BETWEEN 20 AND 30;\n\n-- 查询指定ID的用户\nSELECT * FROM users WHERE id IN (1, 3, 5);\n\n-- 模糊查询（% 表示任意多个字符，_ 表示一个字符）\nSELECT * FROM users WHERE username LIKE '张%';  -- 姓张的用户\nSELECT * FROM users WHERE email LIKE '%@gmail.com';  -- Gmail邮箱\n\n-- 查询邮箱为空的用户\nSELECT * FROM users WHERE email IS NULL;\n\n-- 组合条件\nSELECT * FROM users \nWHERE age > 25 AND status = 1;\n```\n\n### 5.4 排序查询（ORDER BY）\n\n```sql\n-- 升序排列（默认）\nSELECT * FROM users ORDER BY age;\n\n-- 降序排列\nSELECT * FROM users ORDER BY age DESC;\n\n-- 多字段排序\nSELECT * FROM users \nORDER BY age DESC, create_time ASC;\n\n-- 先按年龄降序，年龄相同则按创建时间升序\n```\n\n### 5.5 聚合函数\n\n| 函数 | 作用 |\n|------|------|\n| COUNT() | 统计数量 |\n| MAX() | 最大值 |\n| MIN() | 最小值 |\n| SUM() | 求和 |\n| AVG() | 平均值 |\n\n```sql\n-- 统计用户总数\nSELECT COUNT(*) FROM users;\n\n-- 统计年龄不为空的用户数\nSELECT COUNT(age) FROM users;\n\n-- 查询最大年龄\nSELECT MAX(age) FROM users;\n\n-- 查询最小年龄\nSELECT MIN(age) FROM users;\n\n-- 查询年龄总和\nSELECT SUM(age) FROM users;\n\n-- 查询平均年龄\nSELECT AVG(age) FROM users;\n\n-- 注意：NULL 值不参与聚合函数运算\n```\n\n### 5.6 分组查询（GROUP BY）\n\n```sql\n-- 按性别分组，统计查询每组人数\nSELECT gender, COUNT(*) \nFROM users \nGROUP BY gender;\n\n-- 按状态分组，统计每组平均年龄\nSELECT status, AVG(age) \nFROM users \nGROUP BY status;\n\n-- WHERE 和 HAVING 的区别\n-- WHERE：分组前过滤\n-- HAVING：分组后过滤\n\n-- 查询年龄大于20的用户，按性别分组，统计每组人数\nSELECT gender, COUNT(*) \nFROM users \nWHERE age > 20 \nGROUP BY gender;\n\n-- 按性别分组，统计每组人数，只显示人数大于2的组\nSELECT gender, COUNT(*) AS count \nFROM users \nGROUP BY gender \nHAVING count > 2;\n\n-- 执行顺序：WHERE > GROUP BY > 聚合函数 > HAVING\n```\n\n### 5.7 分页查询（LIMIT）\n\n```sql\n-- 语法：LIMIT 起始索引, 查询条目数\n-- 起始索引从0开始\n\n-- 查询前3条数据\nSELECT * FROM users LIMIT 0, 3;\n\n-- 查询第2页数据（每页3条）\n-- 起始索引 = (当前页码-1) * 每页显示条数\nSELECT * FROM users LIMIT 3, 3;\n\n-- 查询第3页数据（每页3条）\nSELECT * FROM users LIMIT 6, 3;\n\n-- 前端分页公式\n-- page: 当前页码\n-- pageSize: 每页条数\n-- LIMIT (page-1)*pageSize, pageSize\n```\n\n### 5.8 综合查询示例\n\n```sql\n-- 需求：查询状态为1的用户，按性别分组，统计每组人数和平均年龄，\n--      只显示人数大于2的组，按人数降序排列，取前2条\n\nSELECT \n    gender,\n    COUNT(*) AS user_count,\n    AVG(age) AS avg_age\nFROM users\nWHERE status = 1\nGROUP BY gender\nHAVING user_count > 2\nORDER BY user_count DESC\nLIMIT 0, 2;\n```\n\n---\n\n## 六、约束与数据完整性\n\n### 6.1 约束的概念\n\n约束是作用于表中列上的规则，用于限制加入表的数据，保证数据的正确性、有效性和完整性。\n\n**约束分类：**\n- 非空约束（NOT NULL）\n- 唯一约束（UNIQUE）\n- 主键约束（PRIMARY KEY）\n- 默认约束（DEFAULT）\n- 外键约束（FOREIGN KEY）\n\n### 6.2 非空约束（NOT NULL）\n\n保证列中所有数据不能有 NULL 值。\n\n```sql\n-- 创建表时添加\nCREATE TABLE users (\n    id INT,\n    username VARCHAR(50) NOT NULL,  -- 用户名不能为空\n    email VARCHAR(100)\n);\n\n-- 建表后添加\nALTER TABLE users MODIFY COLUMN username VARCHAR(50) NOT NULL;\n\n-- 删除非空约束\nALTER TABLE users MODIFY COLUMN username VARCHAR(50);\n```\n\n### 6.3 唯一约束（UNIQUE）\n\n保证列中所有数据各不相同。\n\n```sql\n-- 创建表时添加\nCREATE TABLE users (\n    id INT,\n    username VARCHAR(50) UNIQUE,  -- 用户名必须唯一\n    email VARCHAR(100)\n);\n\n);\n\n-- 建表后添加\nALTER TABLE users MODIFY COLUMN username VARCHAR(50) UNIQUE;\n\n-- 删除唯一约束\nALTER TABLE users DROP INDEX username;\n```\n\n### 6.4 主键约束（PRIMARY KEY）\n\n主键是一行数据的唯一标识，要求非空且唯一。一张表只能有一个主键。\n\n```sql\n-- 创建表时添加\nCREATE TABLE users (\n    id INT PRIMARY KEY,  -- id是主键\n    username VARCHAR(50)\n);\n\n-- 主键自增长\nCREATE TABLE users (\n    id INT PRIMARY KEY AUTO_INCREMENT,  -- 自动增长\n    username VARCHAR(50)\n);\n\n-- 建表后添加\nALTER TABLE users ADD PRIMARY KEY(id);\n\n-- 删除主键\nALTER TABLE users DROP PRIMARY KEY;\n```\n\n### 6.5 默认约束（DEFAULT）\n\n保存数据时，未指定值则采用默认值。\n\n```sql\n-- 创建表时添加\nCREATE TABLE users (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    username VARCHAR(50),\n    status TINYINT DEFAULT 1  -- 默认状态为1\n);\n\n-- 建表后添加\nALTER TABLE users ALTER COLUMN status SET DEFAULT 1;\n\n-- 删除默认约束\nALTER TABLE users ALTER COLUMN status DROP DEFAULT;\n```\n\n### 6.6 外键约束（FOREIGN KEY）\n\n让表与表之间产生关联关系，保证数据的一致性和完整性。\n\n```sql\n-- 创建部门表\nCREATE TABLE departments (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    name VARCHAR(50) NOT NULL\n);\n\n-- 创建员工表（添加外键）\nCREATE TABLE employees (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    name VARCHAR(50) NOT NULL,\n    dept_id INT,\n    FOREIGN KEY (dept_id) REFERENCES departments(id)\n);\n\n-- 建表后添加外键\nALTER TABLE employees \nADD CONSTRAINT fk_dept \nFOREIGN KEY (dept_id) REFERENCES departments(id);\n\n-- 删除外键\nALTER TABLE employees DROP FOREIGN KEY fk_dept;\n```\n\n### 6.7 级联操作\n\n```sql\n-- 级联删除和更新\nALTER TABLE employees \nADD CONSTRAINT fk_dept \nFOREIGN KEY (dept_id) REFERENCES departments(id)\nON DELETE CASCADE  -- 删除部门时，级联删除该部门的所有员工\nON UPDATE CASCADE;  -- 更新部门ID时，级联更新员工表中的部门ID\n```\n\n**级联操作类型：**\n- `CASCADE`：级联操作（慎用）\n- `SET NULL`：置空\n- `NO ACTION`：拒绝操作（默认）\n- `RESTRICT`：拒绝操作\n\n---\n\n## 七、多表操作\n\n### 7.1 表关系类型\n\n**一对一（1:1）**\n- 适用场景：用户和用户详情\n- 建表原则：在任意一个表建立外键\n\n**一对多（1:N）**\n- 适用场景：部门和员工、用户和订单\n- 建表原则：在多的一方建立外键\n\n**多对多（M:N）**\n- 适用场景：学生和课程、用户和角色\n- 建表原则：借助第三张中间表\n\n### 7.2 一对多关系示例\n\n```sql\n-- 部门表\nCREATE TABLE departments (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    name VARCHAR(50) NOT NULL,\n    location VARCHAR(100)\n);\n\n-- 员工表（多的一方，建立外键）\nCREATE TABLE employees (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    name VARCHAR(50) NOT NULL,\n    salary DECIMAL(10,2),\n    dept_id INT,\n    FOREIGN KEY (dept_id) REFERENCES departments(id)\n);\n\n-- 插入测试数据\nINSERT INTO departments (name, location) VALUES\n('技术部', '北京'),\n('市场部', '上海'),\n('财务部', '广州');\n\nINSERT INTO employees (name, salary, dept_id) VALUES\n('张三', 8000, 1),\n('李四', 9000, 1),\n('王五', 7000, 2),\n('赵六', 7500, 2),\n('孙七', 8500, 3);\n```\n\n### 7.3 多对多关系示例\n\n```sql\n-- 学生表\nCREATE TABLE students (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    name VARCHAR(50) NOT NULL\n);\n\n-- 课程表\nCREATE TABLE courses (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    name VARCHAR(50) NOT NULL\n);\n\n-- 中间表（学生选课表）\nCREATE TABLE student_courses (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    student_id INT,\n    course_id INT,\n    FOREIGN KEY (student_id) REFERENCES students(id),\n    FOREIGN KEY (course_id) REFERENCES courses(id)\n);\n\n-- 插入测试数据\nINSERT INTO students (name) VALUES ('张三'), ('李四'), ('王五');\nINSERT INTO courses (name) VALUES ('数学'), ('英语'), ('物理');\n\n-- 张三选了数学和英语\nINSERT INTO student_courses (student_id, course_id) VALUES (1, 1), (1, 2);\n-- 李四选了英语和物理\nINSERT INTO student_courses (student_id, course_id) VALUES (2, 2), (2, 3);\n```\n\n### 7.4 多表查询\n\n**笛卡尔积（不推荐）**\n```sql\n-- 笛卡尔积：A表每条数据与B表每条数据组合\nSELECT * FROM employees, departments;\n-- 结果：员工数 × 部门数\n```\n\n**内连接（INNER JOIN）**\n```sql\n-- 显式内连接\nSELECT e.name, e.salary, d.name AS dept_name\nFROM employees e\nINNER JOIN departments d ON e.dept_id = d.id;\n\n-- 隐式内连接\nSELECT e.name, e.salary, d.name AS dept_name\nFROM employees e, departments d\nWHERE e.dept_id = d.id;\n```\n\n**外连接（OUTER JOIN）**\n```sql\n-- 左外连接：查询左表所有数据，以及右表匹配的数据\nSELECT e.name, e.salary, d.name AS dept_name\nFROM employees e\nLEFT JOIN departments d ON e.dept_id = d.id;\n\n-- 右外连接：查询右表所有数据，以及左表匹配的数据\nSELECT e.name, e.salary, d.name AS dept_name\nFROM employees e\nRIGHT JOIN departments d ON e.dept_id = d.id;\n```\n\n### 7.5 子查询\n\n查询语句中嵌套查询语句。\n\n**单行单列（作为条件值）**\n```sql\n-- 查询工资高于张三的员工\nSELECT * FROM employees \nWHERE salary > (SELECT salary FROM employees WHERE name = '张三');\n```\n\n**多行单列（使用 IN）**\n```sql\n-- 查询技术部和市场部的所有员工\nSELECT * FROM employees\nWHERE dept_id IN (\n    SELECT id FROM departments \n    WHERE name IN ('技术部', '市场部')\n);\n```\n\n**多行多列（作为虚拟表）**\n```sql\n-- 查询工资大于8000的员工及其部门信息\nSELECT t1.name, t1.salary, t2.name AS dept_name\nFROM (SELECT * FROM employees WHERE salary > 8000) t1\nLEFT JOIN departments t2 ON t1.dept_id = t2.id;\n```\n\n### 7.6 自关联查询\n\n同一张表中的数据有关联性。\n\n```sql\n-- 员工表（包含领导ID）\nCREATE TABLE employees (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    name VARCHAR(50) NOT NULL,\n    manager_id INT,  -- 领导ID\n    FOREIGN KEY (manager_id) REFERENCES employees(id)\n);\n\n-- 插入测试数据\nINSERT INTO employees (id, name, manager_id) VALUES\n(1, 'CEO', NULL),\n(2, '技术总监', 1),\n(3, '市场总监', 1),\n(4, '前端主管', 2),\n(5, '后端主管', 2);\n\n-- 查询员工及其领导姓名\nSELECT \n    e1.name AS '员工',\n    e2.name AS '领导'\nFROM employees e1\nLEFT JOIN employees e2 ON e1.manager_id = e2.id;\n```\n\n---\n\n## 八、视图\n\n### 8.1 视图的概念\n\n视图是一种虚拟存在的数据表，不实际存储在数据库中。\n\n**作用：**\n- 将复杂查询语句的结果封装到虚拟表中\n- 简化后续查询\n- 提供数据安全性（限制访问权限）\n\n**类比前端：**\n- 类似于计算属性或 memoized 函数\n\n### 8.2 创建和查询视图\n\n```sql\n-- 创建视图（员工及其部门信息）\nCREATE VIEW employee_dept_view AS\nSELECT \n    e.id,\n    e.name AS employee_name,\n    e.salary,\n    d.name AS dept_name,\n    d.location\nFROM employees e\nLEFT JOIN departments d ON e.dept_id = d.id;\n\n-- 查询视图\nSELECT * FROM employee_dept_view;\n\n-- 带条件的查询\nSELECT * FROM employee_dept_view WHERE dept_name = '技术部';\n```\n\n### 8.3 修改和删除视图\n\n```sql\n-- 修改视图数据（会影响原表）\nUPDATE employee_dept_view SET salary = 10000 WHERE id = 1;\n\n-- 修改视图结构\nALTER VIEW employee_dept_view AS\nSELECT \n    e.id,\n    e.name AS employee_name,\n    e.salary,\n    d.name AS dept_name\nFROM employees e\nINNER JOIN departments d ON e.dept_id = d.id;\n\n-- 删除视图\nDROP VIEW IF EXISTS employee_dept_view;\n```\n\n---\n\n## 九、前端开发实战示例\n\n### 9.1 用户管理系统\n\n```sql\n-- 创建用户表\nCREATE TABLE users (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    username VARCHAR(50) NOT NULL UNIQUE,\n    password VARCHAR(255) NOT NULL,\n    email VARCHAR(100) NOT NULL UNIQUE,\n    phone VARCHAR(20),\n    avatar VARCHAR(255),\n    status TINYINT DEFAULT 1 COMMENT '0-禁用,1-正常',\n    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,\n    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n    INDEX idx_username (username),\n    INDEX idx_email (email)\n);\n\n-- 用户注册\nINSERT INTO users (username, password, email, phone) \nVALUES ('zhangsan', 'hashed_password', 'zhangsan@example.com', '13800138000');\n\n-- 用户登录验证\nSELECT id, username, email, avatar, status \nFROM users \nWHERE username = 'zhangsan' AND password = 'hashed_password';\n\n-- 更新用户信息\nUPDATE users \nSET email = 'newemail@example.com', phone = '13900139000' \nWHERE id = 1;\n\n-- 分页查询用户列表（每页10条）\nSELECT id, username, email, status, create_time \nFROM users \nWHERE status = 1\nORDER BY create_time DESC\nLIMIT 0, 10;\n```\n\n### 9.2 博客系统\n\n```sql\n-- 用户表\nCREATE TABLE users (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    username VARCHAR(50) NOT NULL UNIQUE,\n    password VARCHAR(255) NOT NULL,\n    avatar VARCHAR(255),\n    create_time DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\n-- 文章表\nCREATE TABLE articles (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    title VARCHAR(200) NOT NULL,\n    content TEXT NOT NULL,\n    author_id INT NOT NULL,\n    category_id INT,\n    views INT DEFAULT 0,\n    likes INT DEFAULT 0,\n    status TINYINT DEFAULT 1 COMMENT '0-草稿,1-已发布',\n    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,\n    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n    FOREIGN KEY (author_id) REFERENCES users(id),\n    INDEX idx_author (author_id),\n    INDEX idx_status (status)\n);\n\n-- 分类表\nCREATE TABLE categories (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    name VARCHAR(50) NOT NULL,\n    create_time DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\n-- 评论表\nCREATE TABLE comments (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    article_id INT NOT NULL,\n    user_id INT NOT NULL,\n    content TEXT NOT NULL,\n    parent_id INT COMMENT '父评论ID，用于回复',\n    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,\n    FOREIGN KEY (article_id) REFERENCES articles(id),\n    FOREIGN KEY (user_id) REFERENCES users(id),\n    FOREIGN KEY (parent_id) REFERENCES comments(id)\n);\n\n-- 发布文章\nINSERT INTO articles (title, content, author_id, category_id, status) \nVALUES ('MySQL学习笔记', '这是一篇关于MySQL的学习笔记...', 1, 1, 1);\n\n-- 查询文章列表（带作者和分类信息）\nSELECT \n    a.id,\n    a.title,\n    a.views,\n    a.likes,\n    a.create_time,\n    u.username AS author,\n    c.name AS category\nFROM articles a\nLEFT JOIN users u ON a.author_id = u.id\nLEFT JOIN categories c ON a.category_id = c.id\nWHERE a.status = 1\nORDER BY a.create_time DESC\nLIMIT 0, 10;\n\n-- 查询文章详情（带评论）\nSELECT \n    a.id,\n    a.title,\n    a.content,\n    a.views,\n    a.likes,\n    u.username AS author,\n    c.name AS category\nFROM articles a\nLEFT JOIN users u ON a.author_id = u.id\nLEFT JOIN categories c ON a.category_id = c.id\nWHERE a.id = 1;\n\n-- 查询文章的评论\nSELECT \n    c.id,\n    c.content,\n    c.create_time,\n    u.username,\n    u.avatar\nFROM comments c\nLEFT JOIN users u ON c.user_id = u.id\nWHERE c.article_id = 1 AND c.parent_id IS NULL\nORDER BY c.create_time DESC;\n\n-- 增加文章浏览量\nUPDATE articles SET views = views + 1 WHERE id = 1;\n\n-- 点赞文章\nUPDATE articles SET likes = likes + 1 WHERE id = 1;\n```\n\n### 9.3 电商订单系统\n\n```sql\n-- 用户表\nCREATE TABLE users (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    username VARCHAR(50) NOT NULL UNIQUE,\n    password VARCHAR(255) NOT NULL,\n    balance DECIMAL(10,2) DEFAULT 0,\n    create_time DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\n-- 商品表\nCREATE TABLE products (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    name VARCHAR(200) NOT NULL,\n    price DECIMAL(10,2) NOT NULL,\n    stock INT DEFAULT 0,\n    image VARCHAR(255),\n    status TINYINT DEFAULT 1 COMMENT '0-下架,1-上架',\n    create_time DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\n-- 订单表\nCREATE TABLE orders (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    order_no VARCHAR(50) NOT NULL UNIQUE,\n    user_id INT NOT NULL,\n    total_amount DECIMAL(10,2) NOT NULL,\n    status TINYINT DEFAULT 1 COMMENT '1-待付款,2-待发货,3-待收货,4-已完成',\n    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,\n    FOREIGN KEY (user_id) REFERENCES users(id),\n    INDEX idx_user (user_id),\n    INDEX idx_order_no (order_no)\n);\n\n-- 订单详情表\nCREATE TABLE order_items (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    order_id INT NOT NULL,\n    product_id INT NOT NULL,\n    quantity INT NOT NULL,\n    price DECIMAL(10,2) NOT NULL,\n    subtotal DECIMAL(10,2) NOT NULL,\n    FOREIGN KEY (order_id) REFERENCES orders(id),\n    FOREIGN KEY (product_id) REFERENCES products(id)\n);\n\n-- 创建订单（事务处理）\nSTART TRANSACTION;\n\n-- 1. 创建订单\nINSERT INTO orders (order_no, user_id, total_amount, status) \nVALUES ('ORD20240130001', 1, 299.00, 1);\n\n-- 2. 添加订单详情\nINSERT INTO order_items (order_id, product_id, quantity, price, subtotal) \nVALUES \n(LAST_INSERT_ID(), 1, 2, 99.00, 198.00),\n(LAST_INSERT_ID(), 2, 1, 101.00, 101.00);\n\n-- 3. 扣减库存\nUPDATE products SET stock = stock - 2 WHERE id = 1;\nUPDATE products SET stock = stock - 1 WHERE id = 2;\n\n-- 4. 扣减用户余额\nUPDATE users SET balance = balance - 299.00 WHERE id = 1;\n\nCOMMIT;\n\n-- 查询用户订单列表\nSELECT \n    o.id,\n    o.order_no,\n    o.total_amount,\n    o.status,\n    o.create_time\nFROM orders o\nWHERE o.user_id = 1\nORDER BY o.create_time DESC\nLIMIT 0, 10;\n\n-- 查询订单详情（包含商品信息）\nSELECT \n    oi.id,\n    p.name AS product_name,\n    p.image,\n    oi.quantity,\n    oi.price,\n    oi.subtotal\nFROM order_items oi\nLEFT JOIN products p ON oi.product_id = p.id\nWHERE oi.order_id = 1;\n```\n\n### 9.4 Node.js + MySQL 连接示例\n\n```javascript\n\u002F\u002F 安装 mysql2\n\u002F\u002F npm install mysql2\n\nconst mysql = require('mysql2');\n\n\u002F\u002F 创建连接池\nconst pool = mysql.createPool({\n    host: 'localhost',\n    user: 'root',\n    password: 'password',\n    database: 'myapp',\n    waitForConnections: true,\n    connectionLimit: 10,\n    queueLimit: 0\n});\n\n\u002F\u002F 查询用户\nasync function getUserById(id) {\n    const [rows] = await pool.promise().query(\n        'SELECT id, username, email, avatar FROM users WHERE id = ?',\n        [id]\n    );\n    return rows[0];\n}\n\n\u002F\u002F 用户注册\nasync function registerUser(username, password, email) {\n    const [result] = await pool.promise().query(\n        'INSERT INTO users (username, password, email) VALUES (?, ?, ?)',\n        [username, password, email]\n    );\n    return result.insertId;\n}\n\n\u002F\u002F 分页查询文章列表\nasync function getArticles(page = 1, pageSize = 10) {\n    const offset = (page - 1) * pageSize;\n    const [rows] = await pool.promise().query(`\n        SELECT \n            a.id,\n            a.title,\n            a.views,\n            a.likes,\n            a.create_time,\n            u.username AS author\n        FROM articles a\n        LEFT JOIN users u ON a.author_id = u.id\n        WHERE a.status = 1\n        ORDER BY a.create_time DESC\n        LIMIT ?, ?\n    `, [offset, pageSize]);\n    return rows;\n}\n\n\u002F\u002F 使用示例\n(async () => {\n    try {\n        const user = await getUserById(1);\n        console.log('用户信息:', user);\n\n        const articles = await getArticles(1, 10);\n        console.log('文章列表:', articles);\n    } catch (error) {\n        console.error('错误:', error);\n    }\n})();\n```\n\n---\n\n## 十、学习建议\n\n### 10.1 学习路径\n\n1. **基础概念**（1-2天）\n   - 理解数据库、表、字段的概念\n   - 掌握 MySQL 的安装和配置\n\n2. **SQL 基础**（3-5天）\n   - DDL：创建数据库和表\n   - DML：增删改数据\n   - DQL：基础查询\n\n3. **进阶查询**（3-5天）\n   - 条件查询、排序、分页\n   - 聚合函数、分组查询\n   - 多表查询、子查询\n\n4. **数据库设计**（3-5天）\n   - 约束的使用\n   - 表关系设计（一对一、一对多、多对多）\n   - 数据库设计范式\n\n5. **实战应用**（持续）\n   - 结合 Node.js\u002FExpress\u002FKoa\n   - 设计实际项目的数据库\n   - 优化查询性能\n\n### 10.2 常用工具\n\n- **Navicat**：图形化管理工具\n- **MySQL Workbench**：官方工具\n- **DBeaver**：开源免费\n- **命令行**：mysql 命令\n\n### 10.3 注意事项\n\n1. **SQL 注入防护**\n   - 使用参数化查询\n   - 避免字符串拼接 SQL\n\n2. **性能优化**\n   - 合理使用索引\n   - 避免 SELECT *\n   - 使用 LIMIT 限制结果集\n\n3. **事务处理**\n   - 重要操作使用事务\n   - 确保数据一致性\n\n4. **数据备份**\n   - 定期备份数据库\n   - 测试恢复流程\n\n---\n\n## 十一、快速参考\n\n### 11.1 常用 SQL 语句速查\n\n```sql\n-- 数据库操作\nSHOW DATABASES;\nCREATE DATABASE dbname;\nUSE dbname;\nDROP DATABASE dbname;\n\n-- 表操作\nSHOW TABLES;\nCREATE TABLE table_name (...);\nDESC table_name;\nDROP TABLE table_name;\n\n-- 数据操作\nINSERT INTO table_name (col1, col2) VALUES (val1, val2);\nUPDATE table_name SET col1 = val1 WHERE condition;\nDELETE FROM table_name WHERE condition;\n\n-- 查询操作\nSELECT * FROM table_name WHERE condition ORDER BY col LIMIT offset, count;\nSELECT COUNT(*) FROM table_name WHERE condition;\nSELECT col, COUNT(*) FROM table_name GROUP BY col HAVING count > 1;\n```\n\n### 11.2 数据类型速查\n\n| 类型 | 说明 | 示例 |\n|------|------|------|\n| INT | 整数 | age INT |\n| VARCHAR(n) | 可变字符串 | name VARCHAR(50) |\n| TEXT | 长文本 | content TEXT |\n| DECIMAL(m,d) | 精确小数 | price DECIMAL(10,2) |\n| DATE | 日期 | birthday DATE |\n| DATETIME | 日期时间 | create_time DATETIME |\n| TIMESTAMP | 时间戳 | update_time TIMESTAMP |\n\n---\n\n**祝学习愉快！成为优秀的全栈工程师！** 🚀\n",35,"2026-04-07T16:16:40.024Z","2026-04-07T16:16:40.027Z","2026-05-22T06:20:03.041Z",{"id":19,"categoryName":42,"slug":43,"description":44,"sort":30,"isEnable":26,"createTime":45,"updateTime":46,"deleteTime":30},[],{"id":181,"title":182,"slug":183,"coverUrl":15,"summary":15,"content":184,"htmlContent":72,"categoryId":92,"viewCount":185,"likeCount":26,"isTop":30,"isPublish":26,"seoKeywords":15,"seoDescription":15,"publishTime":186,"createTime":187,"updateTime":188,"deleteTime":78,"category":189,"tags":190},"10","Axios 学习笔记","axios学习笔记","# Axios 学习笔记\n\n> 专为前端工程师设计的 Axios HTTP 客户端学习笔记\n\n---\n\n## 目录\n\n- [一、Axios 简介](#一axios-简介)\n- [二、安装与配置](#二安装与配置)\n- [三、基础用法](#三基础用法)\n- [四、请求配置](#四请求配置)\n- [五、响应结构](#五响应结构)\n- [六、并发请求](#六并发请求)\n- [七、拦截器](#七拦截器)\n- [八、错误处理](#八错误处理)\n- [九、实例创建](#九实例创建)\n- [十、取消请求](#十取消请求)\n- [十一、实战示例](#十一实战示例)\n- [十二、最佳实践](#十二最佳实践)\n- [十三、常见问题](#十三常见问题)\n- [十四、快速参考](#十四快速参考)\n\n---\n\n## 一、Axios 简介\n\n### 1.1 什么是 Axios\n\nAxios 是一个基于 Promise 的 HTTP 客户端，用于浏览器和 Node.js 环境。\n\n**特点：**\n- 支持 Promise API\n- 支持请求和响应拦截器\n- 支持请求数据和响应数据转换\n- 支持取消请求\n- 自动转换 JSON 数据\n- 客户端支持防御 XSRF\n\n### 1.2 为什么选择 Axios\n\n| 特性 | Axios | Fetch API |\n|------|-------|-----------|\n| Promise 支持 | ✅ | ✅ |\n| 拦截器 | ✅ | ❌ |\n| 自动 JSON 转换 | ✅ | ❌ |\n| 请求取消 | ✅ | ✅ |\n| 超时控制 | ✅ | ❌ |\n| 浏览器兼容性 | ✅ | IE 不支持 |\n| Node.js 支持 | ✅ | ✅ |\n\n### 1.3 与 Fetch API 对比\n\n```javascript\n\u002F\u002F Fetch API\nfetch('\u002Fapi\u002Fusers')\n  .then(response => response.json())\n  .then(data => console.log(data))\n  .catch(error => console.error(error));\n\n\u002F\u002F Axios\naxios.get('\u002Fapi\u002Fusers')\n  .then(response => console.log(response.data))\n  .catch(error => console.error(error));\n```\n\n---\n\n## 二、安装与配置\n\n### 2.1 安装\n\n```bash\n# 使用 npm\nnpm install axios\n\n# 使用 yarn\nyarn add axios\n\n# 使用 pnpm\npnpm add axios\n\n# 使用 CDN\n\u003Cscript src=\"https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Faxios\u002Fdist\u002Faxios.min.js\">\u003C\u002Fscript>\n```\n\n### 2.2 基本引入\n\n```javascript\n\u002F\u002F ES6 模块\nimport axios from 'axios';\n\n\u002F\u002F CommonJS\nconst axios = require('axios');\n\n\u002F\u002F CDN 方式\n\u002F\u002F 直接使用全局变量 axios\n```\n\n### 2.3 全局默认配置\n\n```javascript\n\u002F\u002F 设置默认 baseURL\naxios.defaults.baseURL = 'https:\u002F\u002Fapi.example.com';\n\n\u002F\u002F 设置默认请求头\naxios.defaults.headers.common['Authorization'] = 'Bearer token';\naxios.defaults.headers.post['Content-Type'] = 'application\u002Fjson';\n\n\u002F\u002F 设置超时时间（毫秒）\naxios.defaults.timeout = 5000;\n\n\u002F\u002F 设置响应数据类型\naxios.defaults.responseType = 'json';\n```\n\n---\n\n## 三、基础用法\n\n### 3.1 GET 请求\n\n```javascript\n\u002F\u002F 基本 GET 请求\naxios.get('\u002Fapi\u002Fusers')\n  .then(response => {\n    console.log(response.data);\n  })\n  .catch(error => {\n    console.error(error);\n  });\n\n\u002F\u002F 带参数的 GET 请求\naxios.get('\u002Fapi\u002Fusers', {\n  params: {\n    page: 1,\n    pageSize: 10\n  }\n})\n  .then(response => {\n    console.log(response.data);\n  });\n\n\u002F\u002F 使用 async\u002Fawait\nasync function getUsers getUsers() {\n  try {\n    const response = await axios.get('\u002Fapi\u002Fusers');\n    console.log(response.data);\n  } catch (error) {\n    console.error(error);\n  }\n}\n```\n\n### 3.2 POST 请求\n\n```javascript\n\u002F\u002F 基本 POST 请求\naxios.post('\u002Fapi\u002Fusers', {\n  name: '张三',\n  email: 'zhangsan@example.com'\n})\n  .then(response => {\n    console.log(response.data);\n  });\n\n\u002F\u002F 带配置的 POST 请求\naxios.post('\u002Fapi\u002Fusers', {\n  name: '李四',\n  email: 'lisi@example.com'\n}, {\n  headers: {\n    'Content-Type': 'application\u002Fjson'\n  }\n})\n  .then(response => {\n    console.log(response.data);\n  });\n\n\u002F\u002F 使用 async\u002Fawait\nasync function createUser createUser() {\n  try {\n    const response = await axios.post('\u002Fapi\u002Fusers', {\n      name: '王五',\n      email: 'wangwu@example.com'\n    });\n    console.log(response.data);\n  } catch (error) {\n    console.error(error);\n  }\n}\n```\n\n### 3.3 PUT 请求\n\n```javascript\n\u002F\u002F 更新用户\naxios.put('\u002Fapi\u002Fusers\u002F1', {\n  name: '张三更新',\n  email: 'zhangsan_new@example.com'\n})\n  .then(response => {\n    console.log(response.data);\n  });\n```\n\n### 3.4 DELETE 请求\n\n```javascript\n\u002F\u002F 删除用户\naxios.delete('\u002Fapi\u002Fusers\u002F1')\n  .then(response => {\n    console.log(response.data);\n  });\n```\n\n### 3.5 PATCH 请求\n\n```javascript\n\u002F\u002F 部分更新用户\naxios.patch('\u002Fapi\u002Fusers\u002F1', {\n  name: '张三更新'\n})\n  .then(response => {\n    console.log(response.data);\n  });\n```\n\n---\n\n## 四、请求配置\n\n### 4.1 完整配置选项\n\n```javascript\naxios({\n  method: 'get', \u002F\u002F 请求方法：get, post, put, delete, patch\n  url: '\u002Fapi\u002Fusers', \u002F\u002F 请求地址\n  baseURL: 'https:\u002F\u002Fapi.example.com', \u002F\u002F 基础 URL\n  \n  \u002F\u002F 请求头\n  headers: {\n    'Authorization': 'Bearer token',\n    'Content-Type': 'application\u002Fjson'\n  },\n  \n  \u002F\u002F 请求参数\n  params: {\n    page: 1,\n    pageSize: 10\n  },\n  \n  \u002F\u002F 请求体数据\n  data: {\n    name: '张三',\n    email: 'zhangsan@example.com'\n  },\n  \n  \u002F\u002F 超时时间（毫秒）\n  timeout: 5000,\n  \n  \u002F\u002F 响应数据类型：arraybuffer, blob, document, json, text, stream\n  responseType: 'json',\n  \n  \u002F\u002F 响应编码\n  responseEncoding: 'utf8',\n  \n  \u002F\u002F 跨域请求时是否携带凭证\n  withCredentials: false,\n  \n  \u002F\u002F 上传进度回调\n  onUploadProgress: function(progressEvent) {\n    const percentCompleted = Math.round(\n      (progressEvent.loaded * 100) \u002F progressEvent.total\n    );\n    console.log('上传进度:', percentCompleted + '%');\n  },\n  \n  \u002F\u002F 下载进度回调\n  onDownloadProgress: function(progressEvent) {\n    const percentCompleted = Math.round(\n      (progressEvent.loaded * 100) \u002F progressEvent.total\n    );\n    console.log('下载进度:', percentCompleted + '%');\n  },\n  \n  \u002F\u002F 请求体最大长度\n  maxContentLength: 2000,\n  \n  \u002F\u002F HTTP 响应体最大长度\n  maxBodyLength: 2000,\n  \n  \u002F\u002F 是否验证 SSL 证书\n  validateStatus: function(status) {\n    return status >= 200 && status \u003C 300;\n  }\n});\n```\n\n### 4.2 常用配置示例\n\n```javascript\n\u002F\u002F 带认证的请求\naxios.get('\u002Fapi\u002Fusers', {\n  headers: {\n    'Authorization': 'Bearer ' + localStorage.getItem('token')\n  }\n});\n\n\u002F\u002F 带超时的请求\naxios.get('\u002Fapi\u002Fusers', {\n  timeout: 3000\n});\n\n\u002F\u002F 上传文件\nconst formData = new FormData();\nformData.append('file', fileInput.files[0]);\n\naxios.post('\u002Fapi\u002Fupload', formData, {\n  headers: {\n    'Content-Type': 'multipart\u002Fform-data'\n  },\n  onUploadProgress: (progressEvent) => {\n    const progress = Math.round(\n      (progressEvent.loaded * 100) \u002F progressEvent.total\n    );\n    console.log('上传进度:', progress + '%');\n  }\n});\n\n\u002F\u002F 下载文件\naxios.get('\u002Fapi\u002Fdownload\u002Ffile.pdf', {\n  responseType: 'blob',\n  onDownloadProgress: (progressEvent) => {\n    const progress = Math.round(\n      (progressEvent.loaded * 100) \u002F progressEvent.total\n    );\n    console.log('下载进度:', progress + '%');\n  }\n})\n  .then(response => {\n    const url = window.URL.createObjectURL(new Blob([response.data]));\n    const link = document.createElement('a');\n    link.href = url;\n    link.setAttribute('download', 'file.pdf');\n    document.body.appendChild(link);\n    link.click();\n  });\n```\n\n---\n\n## 五、响应结构\n\n### 5.1 响应对象结构\n\n```javascript\naxios.get('\u002Fapi\u002Fusers')\n  .then(response => {\n    \u002F\u002F 响应数据\n    console.log(response.data);\n    \n    \u002F\u002F HTTP 状态码\n    console.log(response.status);\n    \n    \u002F\u002F HTTP 状态消息\n    console.log(response.statusText);\n    \n    \u002F\u002F 响应头\n    console.log(response.headers);\n    \n    \u002F\u002F 请求配置\n    console.log(response.config);\n    \n    \u002F\u002F 请求对象\n    console.log(response.request);\n  });\n```\n\n### 5.2 响应示例\n\n```javascript\n\u002F\u002F 成功响应\n{\n  data: {\n    id: 1,\n    name: '张三',\n    email: 'zhangsan@example.com'\n  },\n  status: 200,\n  statusText: 'OK',\n  headers: {\n    'content-type': 'application\u002Fjson',\n    'content-length': '123'\n  },\n  config: {\n    url: '\u002Fapi\u002Fusers\u002F1',\n    method: 'get',\n    headers: { ... }\n  },\n  request: XMLHttpRequest\n}\n\n\u002F\u002F 错误响应\n{\n  message: 'Request failed with status code 404',\n  name: 'Error',\n  code: 'ERR_BAD_REQUEST',\n  config: { ... },\n  request: XMLHttpRequest,\n  response: {\n    data: {\n      error: 'User not found'\n    },\n    status: 404,\n    statusText: 'Not Found',\n    headers: { ... }\n  }\n}\n```\n\n---\n\n## 六、并发请求\n\n### 6.1 使用 Promise.all\n\n```javascript\n\u002F\u002F 并发发送多个请求\nfunction getUserAndOrders getUserAndOrders(userId) {\n  const getUser = axios.get(`\u002Fapi\u002Fusers\u002F${userId}`);\n  const getOrders = axios.get(`\u002Fapi\u002Fusers\u002F${userId}\u002Forders`);\n  \n  Promise.all([getUser, getOrders])\n    .then(([userResponse, ordersResponse]) => {\n      console.log('用户信息:', userResponse.data);\n      console.log('订单信息:', ordersResponse.data);\n    })\n    .catch(error => {\n      console.error('请求失败:', error);\n    });\n}\n\n\u002F\u002F 使用 async\u002Fawait\nasync function getUserAndOrdersAsync(userId) {\n  try {\n    const [userResponse, ordersResponse] = await Promise.all([\n      axios.get(`\u002Fapi\u002Fusers\u002F${userId}`),\n      axios.get(`\u002Fapi\u002Fusers\u002F${userId}\u002Forders`)\n    ]);\n    \n    console.log('用户信息:', userResponse.data);\n    console.log('订单信息:', ordersResponse.data);\n  } catch (error) {\n    console.error('请求失败:', error);\n  }\n}\n```\n\n### 6.2 使用 axios.all\n\n```javascript\n\u002F\u002F axios.all 是 Promise.all 的别名\naxios.all([\n  axios.get('\u002Fapi\u002Fusers'),\n  axios.get('\u002Fapi\u002Forders'),\n  axios.get('\u002Fapi\u002Fproducts')\n])\n  .then(axios.spread((usersResponse, ordersResponse, productsResponse) => {\n    console.log('用户:', usersResponse.data);\n    console.log('订单:', ordersResponse.data);\n    console.log('产品:', productsResponse.data);\n  }));\n```\n\n### 6.3 并发请求的错误处理\n\n```javascript\n\u002F\u002F 处理部分失败的情况\nasync function fetchMultipleData() {\n  const requests = [\n    axios.get('\u002Fapi\u002Fusers').catch(e => ({ error: e, type: 'users' })),\n    axios.get('\u002Fapi\u002Forders').catch(e => ({ error: e, type: 'orders' })),\n    axios.get('\u002Fapi\u002Fproducts').catch(e => ({ error: e, type: 'products' }))\n  ];\n  \n  const results = await Promise.all(requests);\n  \n  results.forEach(result => {\n    if (result.error) {\n      console.error(`${result.type} 请求失败:`, result.error);\n    } else {\n      console.log(`${result.type} 数据:`, result.data);\n    }\n  });\n}\n```\n\n---\n\n## 七、拦截器\n\n### 7.1 请求拦截器\n\n```javascript\n\u002F\u002F 添加请求拦截器\naxios.interceptors.request.use(\n  config => {\n    \u002F\u002F 在发送请求之前做些什么\n    \n    \u002F\u002F 添加 token\n    const token = localStorage.getItem('token');\n    if (token) {\n      config.headers.Authorization = `Bearer ${token}`;\n    }\n    \n    \u002F\u002F 添加时间戳\n    config.params = {\n      ...config.params,\n      _t: Date.now()\n    };\n    \n    \u002F\u002F 显示加载动画\n    showLoading();\n    \n    console.log('请求配置:', config);\n    return config;\n  },\n  error => {\n    \u002F\u002F 对请求错误做些什么\n    console.error('请求错误:', error);\n    return Promise.reject(error);\n  }\n);\n```\n\n### 7.2 响应拦截器\n\n```javascript\n\u002F\u002F 添加响应拦截器\naxios.interceptors.response.use(\n  response => {\n    \u002F\u002F 对响应数据做点什么\n    \n    \u002F\u002F 隐藏加载动画\n    hideLoading();\n    \n    \u002F\u002F 统一处理响应数据\n    const { data, status } = response;\n    \n    if (status === 200) {\n      return data;\n    }\n    \n    return response;\n  },\n  error => {\n    \u002F\u002F 对响应错误做点什么\n    \n    \u002F\u002F 隐藏加载动画\n    hideLoading();\n    \n    \u002F\u002F 统一错误处理\n    if (error.response) {\n      const { status, data } = error.response;\n      \n      switch (status) {\n        case 401:\n          console.error('未授权，请登录');\n          \u002F\u002F 跳转到登录页\n          window.location.href = '\u002Flogin';\n          break;\n        case 403:\n          console.error('没有权限');\n          break;\n        case 404:\n          console.error('请求的资源不存在');\n          break;\n        case 500:\n          console.error('服务器错误');\n          break;\n        default:\n          console.error('请求错误:', data.message || '未知错误');\n      }\n    } else if (error.request) {\n      console.error('网络错误，请检查网络连接');\n    } else {\n      console.error('请求配置错误:', error.message);\n    }\n    \n    return Promise.reject(error);\n  }\n);\n```\n\n### 7.3 移除拦截器\n\n```javascript\n\u002F\u002F 保存拦截器引用\nconst requestInterceptor = axios.interceptors.request.use(config => {\n  console.log('请求拦截器');\n  return config;\n});\n\nconst responseInterceptor = axios.interceptors.response.use(response => {\n  console.log('响应拦截器');\n  return response;\n});\n\n\u002F\u002F 移除拦截器\naxios.interceptors.request.eject(requestInterceptor);\naxios.interceptors.response.eject(responseInterceptor);\n```\n\n### 7.4 完整拦截器示例\n\n```javascript\n\u002F\u002F utils\u002Frequest.js\nimport axios from 'axios';\nimport { message } from 'antd';\n\nconst service = axios.create({\n  baseURL: process.env.VUE_APP_BASE_API,\n  timeout: 10000\n});\n\nlet loadingCount = 0;\n\nfunction showLoading() {\n  if (loadingCount === 0) {\n    message.loading('加载中...', 0);\n  }\n  loadingCount++;\n}\n\nfunction hideLoading() {\n  loadingCount--;\n  if (loadingCount === 0) {\n    message.destroy();\n  }\n}\n\nservice.interceptors.request.use(\n  config => {\n    const token = localStorage.getItem('token');\n    if (token) {\n      config.headers.Authorization = `Bearer ${token}`;\n    }\n    \n    showLoading();\n    return config;\n  },\n  error => {\n    hideLoading();\n    return Promise.reject(error);\n  }\n);\n\nservice.interceptors.response.use(\n  response => {\n    hideLoading();\n    \n    const { code, data, message: msg } = response.data;\n    \n    if (code === 200) {\n      return data;\n    } else {\n      message.error(msg || '请求失败');\n      return Promise.reject(new Error(msg || '请求失败'));\n    }\n  },\n  error => {\n    hideLoading();\n    \n    if (error.response) {\n      const { status, data } = error.response;\n      \n      switch (status) {\n        case 401:\n          message.error('登录已过期，请重新登录');\n          localStorage.removeItem('token');\n          window.location.href = '\u002Flogin';\n          break;\n        case 403:\n          message.error('没有权限访问');\n          break;\n        case 404:\n          message.error('请求的资源不存在');\n          break;\n        case 500:\n          message.error('服务器错误');\n          break;\n        default:\n          message.error(data.message || '请求失败');\n      }\n    } else {\n      message.error('网络错误，请检查网络连接');\n    }\n    \n    return Promise.reject(error);\n  }\n);\n\nexport default service;\n```\n\n---\n\n## 八、错误处理\n\n### 8.1 错误类型\n\n```javascript\naxios.get('\u002Fapi\u002Fusers')\n  .catch(error => {\n    if (error.response) {\n      \u002F\u002F 服务器响应了，但状态码不在 2xx 范围内\n      console.log('响应数据:', error.response.data);\n      console.log('状态码:', error.response.status);\n      console.log('响应头:', error.response.headers);\n    } else if (error.request) {\n      \u002F\u002F 请求已发出，但没有收到响应\n      console.log('请求对象:', error.request);\n    } else {\n      \u002F\u002F 设置请求时出错\n      console.log('错误消息:', error.message);\n    }\n    \n    console.log('错误配置:', error.config);\n  });\n```\n\n### 8.2 统一错误处理\n\n```javascript\n\u002F\u002F 封装错误处理函数\nfunction handleError(error) {\n  if (!error) {\n    return '未知错误';\n  }\n  \n  if (error.response) {\n    const { status, data } = error.response;\n    \n    const errorMessages = {\n      400: '请求参数错误',\n      401: '未授权，请登录',\n      403: '没有权限访问',\n      404: '请求的资源不存在',\n      405: '请求方法不允许',\n      408: '请求超时',\n      500: '服务器内部错误',\n      502: '网关错误',\n      503: '服务不可用',\n      504: '网关超时'\n    };\n    \n    return data.message || errorMessages[status] || '请求失败';\n  } else if (error.request) {\n    return '网络错误，请检查网络连接';\n  } else {\n    return error.message || '请求配置错误';\n  }\n}\n\naxios.get('\u002Fapi\u002Fusers')\n  .catch(error => {\n    const errorMessage = handleError(error);\n    console.error(errorMessage);\n    alert(errorMessage);\n  });\n```\n\n### 8.3 重试机制\n\n```javascript\n\u002F\u002F 带重试的请求\nasync function axiosWithRetry(config, maxRetries = 3, delay = 1000) {\n  let retries = 0;\n  \n  while (retries \u003C maxRetries) {\n    try {\n      const response = await axios(config);\n      return response;\n    } catch (error) {\n      retries++;\n      \n      if (retries >= maxRetries) {\n        throw error;\n      }\n      \n      console.log(`请求失败，第 ${retries} 次重试...`);\n      await new Promise(resolve => setTimeout(resolve, delay));\n    }\n  }\n}\n\naxiosWithRetry({\n  method: 'get',\n  url: '\u002Fapi\u002Fusers'\n}, 3, 1000)\n  .then(response => {\n    console.log('请求成功:', response.data);\n  })\n  .catch(error => {\n    console.error('重试失败:', error);\n  });\n```\n\n---\n\n## 九、实例创建\n\n### 9.1 创建实例\n\n```javascript\n\u002F\u002F 创建自定义实例\nconst instance = axios.create({\n  baseURL: 'https:\u002F\u002Fapi.example.com',\n  timeout: 5000,\n  headers: {\n    'Content-Type': 'application\u002Fjson',\n    'Authorization': 'Bearer token'\n  }\n});\n\n\u002F\u002F 使用实例发送请求\ninstance.get('\u002Fusers')\n  .then(response => {\n    console.log(response.data);\n  });\n\ninstance.post('\u002Fusers', {\n  name: '张三'\n});\n```\n\n### 9.2 多实例示例\n\n```javascript\n\u002F\u002F 用户服务实例\nconst userService = axios.create({\n  baseURL: 'https:\u002F\u002Fapi.example.com\u002Fusers',\n  timeout: 5000\n});\n\n\u002F\u002F 订单服务实例\nconst orderService = axios.create({\n  baseURL: 'https:\u002F\u002Fapi.example.com\u002Forders',\n  timeout: 10000\n});\n\n\u002F\u002F 使用不同实例\nuserService.get('\u002Fprofile');\norderService.get('\u002Flist');\n```\n\n### 9.3 完整实例封装\n\n```javascript\n\u002F\u002F api\u002Fuser.js\nimport axios from 'axios';\n\nconst userApi = axios.create({\n  baseURL: 'https:\u002F\u002Fapi.example.com\u002Fapi\u002Fusers',\n  timeout: 5000\n});\n\nuserApi.interceptors.request.use(config => {\n  const token = localStorage.getItem('token');\n  if (token) {\n    config.headers.Authorization = `Bearer ${token}`;\n  }\n  return config;\n});\n\nexport default {\n  getUser(id) {\n    return userApi.get(`\u002F${id}`);\n  },\n  \n  getUsers(params) {\n    return userApi.get('\u002F', { params });\n  },\n  \n  createUser(data) {\n    return userApi.post('\u002F', data);\n  },\n  \n  updateUser(id, data) {\n    return userApi.put(`\u002F${id}`, data);\n  },\n  \n  deleteUser(id) {\n    return userApi.delete(`\u002F${id}`);\n  }\n};\n\n\u002F\u002F 使用\nimport userApi from '@\u002Fapi\u002Fuser';\n\nuserApi.getUser(1)\n  .then(user => {\n    console.log(user);\n  });\n```\n\n---\n\n## 十、取消请求\n\n### 10.1 使用 CancelToken\n\n```javascript\nconst CancelToken = axios.CancelToken;\nlet cancel;\n\naxios.get('\u002Fapi\u002Fusers', {\n  cancelToken: new CancelToken(function executor(c) {\n    cancel = c;\n  })\n});\n\n\u002F\u002F 取消请求\ncancel('请求被用户取消');\n```\n\n### 10.2 使用 AbortController\n\n```javascript\nconst controller = new AbortController();\n\naxios.get('\u002Fapi\u002Fusers', {\n  signal: controller.signal\n});\n\n\u002F\u002F 取消请求\ncontroller.abort();\n```\n\n### 10.3 实际应用场景\n\n```javascript\n\u002F\u002F 搜索框防抖 + 取消请求\nlet searchController = null;\n\nasync function searchUsers(keyword) {\n  \u002F\u002F 取消上一次请求\n  if (searchController) {\n    searchController.abort();\n  }\n  \n  searchController = new AbortController();\n  \n  try {\n    const response = await axios.get('\u002Fapi\u002Fusers', {\n      params: { keyword },\n      signal: searchController.signal\n    });\n    \n    return response.data;\n  } catch (error) {\n    if (axios.isCancel(error)) {\n      console.log('请求被取消:', error.message);\n    } else {\n      console.error('搜索失败:', error);\n    }\n  }\n}\n\n\u002F\u002F 页面卸载时取消请求\nuseEffect(() => {\n  const controller = new AbortController();\n  \n  fetchData(controller.signal);\n  \n  return () => {\n    controller.abort();\n  };\n}, []);\n```\n\n---\n\n## 十一、实战示例\n\n### 11.1 用户管理系统\n\n```javascript\n\u002F\u002F api\u002Fuser.js\nimport request from '@\u002Futils\u002Frequest';\n\nexport default {\n  login(username, password) {\n    return request.post('\u002Fapi\u002Fauth\u002Flogin', {\n      username,\n      password\n    });\n  },\n  \n  register(data) {\n    return request.post('\u002Fapi\u002Fauth\u002Fregister', data);\n  },\n  \n  logout() {\n    return request.post('\u002Fapi\u002Fauth\u002Flogout');\n  },\n  \n  getUserInfo() {\n    return request.get('\u002Fapi\u002Fuser\u002Finfo');\n  },\n  \n  updateUserInfo(data) {\n    return request.put('\u002Fapi\u002Fuser\u002Finfo', data);\n  },\n  \n  changePassword(oldPassword, newPassword) {\n    return request.post('\u002Fapi\u002Fuser\u002Fchange-password', {\n      oldPassword,\n      newPassword\n    });\n  },\n  \n  uploadAvatar(file) {\n    const formData = new FormData();\n    formData.append('avatar', file);\n    \n    return request.post('\u002Fapi\u002Fuser\u002Favatar', formData, {\n      headers: {\n        'Content-Type': 'multipart\u002Fform-data'\n      }\n    });\n  }\n};\n\n\u002F\u002F React 组件中使用\nimport React, { useState } from 'react';\nimport userApi from '@\u002Fapi\u002Fuser';\n\nfunction LoginForm() {\n  const [username, setUsername] = useState('');\n  const [password, setPassword] = useState('');\n  const [loading, setLoading] = useState(false);\n  \n  const handleLogin = async (e) => {\n    e.preventDefault();\n    setLoading(true);\n    \n    try {\n      const { token, userInfo } = await userApi.login(username, password);\n      \n      localStorage.setItem('token', token);\n      localStorage.setItem('userInfo', JSON.stringify(userInfo));\n      \n      alert('登录成功');\n      window.location.href = '\u002Fdashboard';\n    } catch (error) {\n      console.error('登录失败:', error);\n    } finally {\n      setLoading(false);\n    }\n  };\n  \n  return (\n    \u003Cform onSubmit={handleLogin}>\n      \u003Cinput\n        type=\"text\"\n        value={username}\n        onChange={(e) => setUsername(e.target.value)}\n        placeholder=\"用户名\"\n      \u002F>\n      \u003Cinput\n        type=\"password\"\n        value={password}\n        onChange={(e) => setPassword(e.target.value)}\n        placeholder=\"密码\"\n      \u002F>\n      \u003Cbutton type=\"submit\" disabled={loading}>\n        {loading ? '登录中...' : '登录'}\n      \u003C\u002Fbutton>\n    \u003C\u002Fform>\n  );\n}\n```\n\n### 11.2 博客系统\n\n```javascript\n\u002F\u002F api\u002Farticle.js\nimport request from '@\u002Futils\u002Frequest';\n\nexport default {\n  getArticles(params) {\n    return request.get('\u002Fapi\u002Farticles', { params });\n  },\n  \n  getArticle(id) {\n    return request.get(`\u002Fapi\u002Farticles\u002F${id}`);\n  },\n  \n  createArticle(data) {\n    return request.post('\u002Fapi\u002Farticles', data);\n  },\n  \n  updateArticle(id, data) {\n    return request.put(`\u002Fapi\u002Farticles\u002F${id}`, data);\n  },\n  \n  deleteArticle(id) {\n    return request.delete(`\u002Fapi\u002Farticles\u002F${id}`);\n  },\n  \n  likeArticle(id) {\n    return request.post(`\u002Fapi\u002Farticles\u002F${id}\u002Flike`);\n  },\n  \n  getComments(articleId, params) {\n    return request.get(`\u002Fapi\u002Farticles\u002F${articleId}\u002Fcomments`, { params });\n  },\n  \n  createComment(articleId, content) {\n    return request.post(`\u002Fapi\u002Farticles\u002F${articleId}\u002Fcomments`, {\n      content\n    });\n  }\n};\n\n\u002F\u002F Vue 组件中使用\nimport { ref, onMounted } from 'vue';\nimport articleApi from '@\u002Fapi\u002Farticle';\n\nexport default {\n  setup() {\n    const articles = ref([]);\n    const loading = ref(false);\n    const pagination = ref({\n      page: 1,\n      pageSize: 10,\n      total: 0\n    });\n    \n    const fetchArticles = async () => {\n      loading.value = true;\n      \n      try {\n        const { list, total } = await articleApi.getArticles({\n          page: pagination.value.page,\n          pageSize: pagination.value.pageSize\n        });\n        \n        articles.value = list;\n        pagination.value.total = total;\n      } catch (error) {\n        console.error('获取文章列表失败:', error);\n      } finally {\n        loading.value = false;\n      }\n    };\n    \n    const handlePageChange = (page) => {\n      pagination.value.page = page;\n      fetchArticles();\n    };\n    \n    onMounted(() => {\n      fetchArticles();\n    });\n    \n    return {\n      articles,\n      loading,\n      pagination,\n      handlePageChange\n    };\n  }\n};\n```\n\n### 11.3 电商系统\n\n```javascript\n\u002F\u002F api\u002Fproduct.js\nimport request from '@\u002Futils\u002Frequest';\n\nexport default {\n  getProducts(params) {\n    return request.get('\u002Fapi\u002Fproducts', { params });\n  },\n  \n  getProduct(id) {\n    return request.get(`\u002Fapi\u002Fproducts\u002F${id}`);\n  },\n  \n  searchProducts(keyword, params) {\n    return request.get('\u002Fapi\u002Fproducts\u002Fsearch', {\n      params: { keyword, ...params }\n    });\n  }\n};\n\n\u002F\u002F api\u002Fcart.js\nexport default {\n  getCart() {\n    return request.get('\u002Fapi\u002Fcart');\n  },\n  \n  addToCart(productId, quantity) {\n    return request.post('\u002Fapi\u002Fcart\u002Fitems', {\n      productId,\n      quantity\n    });\n  },\n  \n  updateCartItem(itemId, quantity) {\n    return request.put(`\u002Fapi\u002Fcart\u002Fitems\u002F${itemId}`, {\n      quantity\n    });\n  },\n  \n  removeFromCart(itemId) {\n    return request.delete(`\u002Fapi\u002Fcart\u002Fitems\u002F${itemId}`);\n  },\n  \n  clearCart() {\n    return request.delete('\u002Fapi\u002Fcart');\n  }\n};\n\n\u002F\u002F api\u002Forder.js\nexport default {\n  createOrder(data) {\n    return request.post('\u002Fapi\u002Forders', data);\n  },\n  \n  getOrders(params) {\n    return request.get('\u002Fapi\u002Forders', { params });\n  },\n  \n  getOrder(id) {\n    return request.get(`\u002Fapi\u002Forders\u002F${id}`);\n  },\n  \n  cancelOrder(id) {\n    return request.post(`\u002Fapi\u002Forders\u002F${id}\u002Fcancel`);\n  },\n  \n  payOrder(id, paymentMethod) {\n    return request.post(`\u002Fapi\u002Forders\u002F${id}\u002Fpay`, {\n      paymentMethod\n    });\n  }\n};\n\n\u002F\u002F 购物车组件\nimport React, { useState, useEffect } from 'react';\nimport cartApi from '@\u002Fapi\u002Fcart';\n\nfunction ShoppingCart() {\n  const [cartItems, setCartItems] = useState([]);\n  const [loading, setLoading] = useState(false);\n  \n  const fetchCart = async () => {\n    setLoading(true);\n    \n    try {\n      const items = await cartApi.getCart();\n      setCartItems(items);\n    } catch (error) {\n      console.error('获取购物车失败:', error);\n    } finally {\n      setLoading(false);\n    }\n  };\n  \n  const addToCart = async (productId, quantity = 1) => {\n    try {\n      await cartApi.addToCart(productId, quantity);\n      await fetchCart();\n      alert('添加到购物车成功');\n    } catch (error) {\n      console.error('添加到购物车失败:', error);\n    }\n  };\n  \n  const updateQuantity = async (itemId, quantity) => {\n    try {\n      await cartApi.updateCartItem(itemId, quantity);\n      await fetchCart();\n    } catch (error) {\n      console.error('更新数量失败:', error);\n    }\n  };\n  \n  const removeFromCart = async (itemId) => {\n    try {\n      await cartApi.removeFromCart(itemId);\n      await fetchCart();\n    } catch (error) {\n      console.error('删除失败:', error);\n    }\n  };\n  \n  useEffect(() => {\n    fetchCart();\n  }, []);\n  \n  return (\n    \u003Cdiv>\n      \u003Ch2>购物车\u003C\u002Fh2>\n      {loading ? (\n        \u003Cp>加载中...\u003C\u002Fp>\n      ) : (\n        \u003Cul>\n          {cartItems.map(item => (\n            \u003Cli key={item.id}>\n              \u003Cspan>{item.productName}\u003C\u002Fspan>\n              \u003Cspan>¥{item.price}\u003C\u002Fspan>\n              \u003Cinput\n                type=\"number\"\n                value={item.quantity}\n                onChange={(e) => updateQuantity(item.id, parseInt(e.target.value))}\n              \u002F>\n              \u003Cbutton onClick={() => removeFromCart(item.id)}>删除\u003C\u002Fbutton>\n            \u003C\u002Fli>\n          ))}\n        \u003C\u002Ful>\n      )}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 11.4 文件上传\n\n```javascript\n\u002F\u002F api\u002Fupload.js\nimport request from '@\u002Futils\u002Frequest';\n\nexport default {\n  uploadFile(file, onProgress) {\n    const formData = new FormData();\n    formData.append('file', file);\n    \n    return request.post('\u002Fapi\u002Fupload', formData, {\n      headers: {\n        'Content-Type': 'multipart\u002Fform-data'\n      },\n      onUploadProgress: (progressEvent) => {\n        const progress = Math.round(\n          (progressEvent.loaded * 100) \u002F progressEvent.total\n        );\n        if (onProgress) {\n          onProgress(progress);\n        }\n      }\n    });\n  },\n  \n  uploadMultipleFiles(files, onProgress) {\n    const formData = new FormData();\n    files.forEach((file, index) => {\n      formData.append(`files[${index}]`, file);\n    });\n    \n    return request.post('\u002Fapi\u002Fupload\u002Fmultiple', formData, {\n      headers: {\n        'Content-Type': 'multipart\u002Fform-data'\n      },\n      onUploadProgress: (progressEvent) => {\n        const progress = Math.round(\n          (progressEvent.loaded * 100) \u002F progressEvent.total\n        );\n        if (onProgress) {\n          onProgress(progress);\n        }\n      }\n    });\n  }\n};\n\n\u002F\u002F 文件上传组件\nimport React, { useState } from 'react';\nimport uploadApi from '@\u002Fapi\u002Fupload';\n\nfunction FileUpload() {\n  const [file, setFile] = useState(null);\n  const [uploading, setUploading] = useState(false);\n  const [progress, setProgress] = useState(0);\n  \n  const handleFileChange = (e) => {\n    setFile(e.target.files[0]);\n  };\n  \n  const handleUpload = async () => {\n    if (!file) {\n      alert('请选择文件');\n      return;\n    }\n    \n    setUploading(true);\n    setProgress(0);\n    \n    try {\n      const result = await uploadApi.uploadFile(file, (progress) => {\n        setProgress(progress);\n      });\n      \n      alert('上传成功');\n      console.log('上传结果:', result);\n    } catch (error) {\n      console.error('上传失败:', error);\n      alert('上传失败');\n    } finally {\n      setUploading(false);\n    }\n  };\n  \n  return (\n    \u003Cdiv>\n      \u003Cinput type=\"file\" onChange={handleFileChange} \u002F>\n      \u003Cbutton onClick={handleUpload} disabled={uploading}>\n        {uploading ? `上传中 ${progress}%` : '上传'}\n      \u003C\u002Fbutton>\n      {uploading && (\n        \u003Cdiv>\n          \u003Cdiv>上传进度: {progress}%\u003C\u002Fdiv>\n          \u003Cprogress value={progress} max={100} \u002F>\n        \u003C\u002Fdiv>\n      )}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n---\n\n## 十二、最佳实践\n\n### 12.1 统一 API 封装\n\n```javascript\n\u002F\u002F utils\u002Frequest.js\nimport axios from 'axios';\nimport { message } from 'antd';\n\nconst service = axios.create({\n  baseURL: process.env.REACT_APP_BASE_API,\n  timeout: 10000\n});\n\nservice.interceptors.request.use(\n  config => {\n    const token = localStorage.getItem('token');\n    if (token) {\n      config.headers.Authorization = `Bearer ${token}`;\n    }\n    return config;\n  },\n  error => {\n    return Promise.reject(error);\n  }\n);\n\nservice.interceptors.response.use(\n  response => {\n    const { code, data, message: msg } = response.data;\n    \n    if (code === 200) {\n      return data;\n    } else {\n      message.error(msg || '请求失败');\n      return Promise.reject(new Error(msg || '请求失败'));\n    }\n  },\n  error => {\n    if (error.response?.status === 401) {\n      localStorage.removeItem('token');\n      window.location.href = '\u002Flogin';\n    }\n    message.error(error.message || '请求失败');\n    return Promise.reject(error);\n  }\n);\n\nexport default service;\n\n\u002F\u002F api\u002Findex.js\nimport request from '@\u002Futils\u002Frequest';\n\nexport default {\n  user: {\n    login(data) {\n      return request.post('\u002Fauth\u002Flogin', data);\n    },\n    getInfo() {\n      return request.get('\u002Fuser\u002Finfo');\n    }\n  },\n  article: {\n    getList(params) {\n      return request.get('\u002Farticles', { params });\n    },\n    getDetail(id) {\n      return request.get(`\u002Farticles\u002F${id}`);\n    }\n  }\n};\n```\n\n### 12.2 环境配置\n\n```javascript\n\u002F\u002F .env.development\nREACT_APP_BASE_API=http:\u002F\u002Flocalhost:3000\u002Fapi\n\n\u002F\u002F .env.production\nREACT_APP_BASE_API=https:\u002F\u002Fapi.example.com\n\n\u002F\u002F config.js\nconst config = {\n  development: {\n    baseURL: 'http:\u002F\u002Flocalhost:3000\u002Fapi',\n    timeout: 10000\n  },\n  production: {\n    baseURL: 'https:\u002F\u002Fapi.example.com',\n    timeout: 5000\n  }\n};\n\nexport default config[process.env.NODE_ENV];\n```\n\n### 12.3 TypeScript 支持\n\n```typescript\n\u002F\u002F types\u002Fapi.ts\nexport interface User {\n  id: number;\n  username: string;\n  email: string;\n  avatar?: string;\n}\n\nexport interface Article {\n  id: number;\n  title: string;\n  content: string;\n  author: User;\n  createdAt: string;\n}\n\nexport interface ApiResponse\u003CT> {\n  code: number;\n  data: T;\n  message: string;\n}\n\n\u002F\u002F api\u002Fuser.ts\nimport request from '@\u002Futils\u002Frequest';\nimport type { User, ApiResponse } from '@\u002Ftypes\u002Fapi';\n\nexport const getUserInfo = (): Promise\u003CApiResponse\u003CUser>> => {\n  return request.get('\u002Fuser\u002Finfo');\n};\n\nexport const updateUser = (\n  data: Partial\u003CUser>\n): Promise\u003CApiResponse\u003CUser>> => {\n  return request.put('\u002Fuser\u002Finfo', data);\n};\n```\n\n### 12.4 请求缓存\n\n```javascript\n\u002F\u002F utils\u002Fcache.js\nconst cache = new Map();\n\nexport function withCache(key, fn, ttl = 60000) {\n  const cached = cache.get(key);\n  \n  if (cached && Date.now() - cached.timestamp \u003C ttl) {\n    return Promise.resolve(cached.data);\n  }\n  \n  return fn().then(data => {\n    cache.set(key, {\n      data,\n      timestamp: Date.now()\n    });\n    return data;\n  });\n}\n\nexport function clearCache(key) {\n  if (key) {\n    cache.delete(key);\n  } else {\n    cache.clear();\n  }\n}\n\n\u002F\u002F 使用\nimport { withCache } from '@\u002Futils\u002Fcache';\n\nfunction getUserInfo() {\n  return withCache('userInfo', () => {\n    return axios.get('\u002Fapi\u002Fuser\u002Finfo');\n  });\n}\n```\n\n### 12.5 请求防抖\n\n```javascript\n\u002F\u002F utils\u002Fdebounce.js\nlet debounceTimer = null;\n\nexport function debounceRequest(fn, delay = 300) {\n  return new Promise((resolve, reject) => {\n    if (debounceTimer) {\n      clearTimeout(debounceTimer);\n    }\n    \n    debounceTimer = setTimeout(async () => {\n      try {\n        const result = await fn();\n        resolve(result);\n      } catch (error) {\n        reject(error);\n      }\n    }, delay);\n  });\n}\n\n\u002F\u002F 使用\nimport { debounceRequest } from '@\u002Futils\u002Fdebounce';\n\nfunction searchUsers(keyword) {\n  return debounceRequest(() => {\n    return axios.get('\u002Fapi\u002Fusers', { params: { keyword } });\n  });\n}\n```\n\n---\n\n## 十三、常见问题\n\n### 13.1 跨域问题\n\n**解决方案 1：配置代理（开发环境）**\n\n```javascript\n\u002F\u002F package.json\n{\n  \"proxy\": \"http:\u002F\u002Flocalhost:3000\"\n}\n\n\u002F\u002F 或使用 http-proxy-middleware\n\u002F\u002F src\u002FsetupProxy.js\nconst { createProxyMiddleware } = require('http-proxy-middleware');\n\nmodule.exports = function(app) {\n  app.use(\n    '\u002Fapi',\n    createProxyMiddleware({\n      target: 'http:\u002F\u002Flocalhost:3000',\n      changeOrigin: true\n    })\n  );\n};\n```\n\n**解决方案 2：CORS（生产环境）**\n\n```javascript\n\u002F\u002F 后端配置 CORS（Node.js + Express）\nconst express = require('express');\nconst cors = require('cors');\n\nconst app = express();\n\napp.use(cors({\n  origin: 'http:\u002F\u002Flocalhost:8080',\n  credentials: true\n}));\n```\n\n### 13.2 请求超时\n\n```javascript\n\u002F\u002F 设置超时时间\naxios.get('\u002Fapi\u002Fusers', {\n  timeout: 5000\n});\n\n\u002F\u002F 全局设置\naxios.defaults.timeout = 5000;\n\n\u002F\u002F 实例设置\nconst instance = axios.create({\n  timeout: 5000\n});\n```\n\n### 13.3 大文件上传\n\n```javascript\n\u002F\u002F 分片上传\nasync function uploadLargeFile(file, chunkSize = 1024 * 1024) {\n  const chunks = Math.ceil(file.size \u002F chunkSize);\n  const results = [];\n  \n  for (let i = 0; i \u003C chunks; i++) {\n    const start = i * chunkSize;\n    const end = Math.min(start + chunkSize, file.size);\n    const chunk = file.slice(start, end);\n    \n    const formData = new FormData();\n    formData.append('file', chunk);\n    formData.append('chunkIndex', i);\n    formData.append('totalChunks', chunks);\n    formData.append('fileName', file.name);\n    \n    const result = await axios.post('\u002Fapi\u002Fupload\u002Fchunk', formData);\n    results.push(result.data);\n  }\n  \n  return results;\n}\n```\n\n### 13.4 并发请求限制\n\n```javascript\n\u002F\u002F 限制并发请求数量\nclass RequestQueue {\n  constructor(maxConcurrent = 5) {\n    this.maxConcurrent = maxConcurrent;\n    this.queue = [];\n    this.running = 0;\n  }\n  \n  add(request) {\n    return new Promise((resolve, reject) => {\n      this.queue.push({\n        request,\n        resolve,\n        reject\n      });\n      this.run();\n    });\n  }\n  \n  async run() {\n    if (this.running >= this.maxConcurrent || this.queue.length === 0) {\n      return;\n    }\n    \n    this.running++;\n    const { request, resolve, reject } = this.queue.shift();\n    \n    try {\n      const result = await request();\n      resolve(result);\n    } catch (error) {\n      reject(error);\n    } finally {\n      this.running--;\n      this.run();\n    }\n  }\n}\n\nconst queue = new RequestQueue(3);\n\nqueue.add(() => axios.get('\u002Fapi\u002Fusers\u002F1'));\nqueue.add(() => axios.get('\u002Fapi\u002Fusers\u002F2'));\nqueue.add(() => axios.get('\u002Fapi\u002Fusers\u002F3'));\n```\n\n---\n\n## 十四、快速参考\n\n### 14.1 常用方法\n\n```javascript\n\u002F\u002F GET 请求\naxios.get(url[, config])\n\n\u002F\u002F POST 请求\naxios.post(url[, data[, config]])\n\n\u002F\u002F PUT 请求\naxios.put(url[, data[, config]])\n\n\u002F\u002F DELETE 请求\naxios.delete(url[, config])\n\n\u002F\u002F PATCH 请求\naxios.patch(url[, data[, config]])\n\n\u002F\u002F 通用请求\naxios(config)\n```\n\n### 14.2 常用配置\n\n```javascript\n{\n  baseURL: 'https:\u002F\u002Fapi.example.com',\n  timeout: 5000,\n  headers: {\n    'Authorization': 'Bearer token',\n    'Content-Type': 'application\u002Fjson'\n  },\n  params: {},\n  data: {},\n  responseType: 'json',\n  withCredentials: false\n}\n```\n\n### 14.3 响应结构\n\n```javascript\n{\n  data: {},\n  status: 200,\n  statusText: 'OK',\n  headers: {},\n  config: {},\n  request: {}\n}\n```\n\n### 14.4 错误处理\n\n```javascript\naxios.get('\u002Fapi\u002Fusers')\n  .catch(error => {\n    if (error.response) {\n      \u002F\u002F 服务器响应了，但状态码不在 2xx 范围内\n    } else if (error.request) {\n      \u002F\u002F 请求已发出，但没有收到响应\n    } else {\n      \u002F\u002F 设置请求时出错\n    }\n  });\n```\n\n---\n\n**祝学习愉快！掌握 Axios，轻松处理 HTTP 请求！** 🚀\n",31,"2026-04-07T16:15:56.021Z","2026-04-07T16:15:56.024Z","2026-05-21T10:28:52.910Z",{"id":92,"categoryName":27,"slug":28,"description":29,"sort":30,"isEnable":26,"createTime":31,"updateTime":32,"deleteTime":30},[],16,9]