[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f6p52neWi7R0tbG3yElOuD5bOEcJlHjgC0RKz_JGcm18":3,"donations-sidebar":10,"posts-{\"page\":1,\"pageSize\":10,\"categoryId\":\"73a5f62c-3c47-45b9-9ae2-f29953ae8dc0\"}":11,"sidebar-data":95},{"id":4,"name":5,"slug":6,"description":5,"sortOrder":7,"createdAt":8,"updatedAt":9},"73a5f62c-3c47-45b9-9ae2-f29953ae8dc0","Node","node",11,"2022-07-16T13:15:39.000Z","2023-02-08T02:49:14.000Z",[],{"list":12,"total":92,"page":93,"pageSize":94},[13,31,48,63,76],{"id":14,"title":15,"slug":16,"content":17,"excerpt":18,"coverImage":19,"status":20,"isPinned":21,"pinnedAt":22,"viewCount":23,"publishedAt":24,"createdAt":24,"updatedAt":25,"category":26,"author":27,"tags":30},"10ddeb0d-9d91-450d-a20b-10fc0fff35f5","uv 使用教程","uv-使用教程","# uv 使用教程\n\n`uv` 是一个由 Rust 编写的极速 Python 包和项目管理工具，由 Ruff 的创建者 Astral 团队开发。它旨在用单一工具替代 `pip`、`pip-tools`、`pipx`、`poetry`、`pyenv`、`twine` 和 `virtualenv` 等工具。\n\n## 🌟 核心优势\n\n- **极速体验**：比普通的 `pip` 快 10-100 倍。\n- **多合一工具**：提供全面的项目管理、通用 lockfile 支持。\n- **节省空间**：支持全局缓存，实现依赖去重。\n- **兼容性强**：包含与 `pip` 兼容的接口，零成本迁移即可获得性能提升。\n- **跨平台**：完美支持 macOS、Linux 和 Windows。\n\n---\n\n## 🚀 1. 安装与更新\n\n`uv` 提供了独立安装脚本，无需预装 Rust 或 Python。\n\n**macOS 和 Linux:**\n```bash\ncurl -LsSf https:\u002F\u002Fastral.sh\u002Fuv\u002Finstall.sh | sh\n```\n\n**Windows:**\n```powershell\npowershell -ExecutionPolicy ByPass -c \\\"irm https:\u002F\u002Fastral.sh\u002Fuv\u002Finstall.ps1 | iex\\\"\n```\n\n**使用 pip 或 pipx 安装:**\n```bash\npip install uv\n# 或者\npipx install uv\n```\n\n**自我更新:**\n```bash\nuv self update\n```\n\n---\n\n## 📦 2. 项目管理 (Projects)\n\n`uv` 可以像 `poetry` 或 `rye` 一样管理项目依赖和虚拟环境。\n\n**初始化新项目:**\n```bash\nuv init example\ncd example\n```\n\n**添加依赖包:**\n```bash\n# 这会自动创建虚拟环境（如果不存在），并更新 pyproject.toml 和 uv.lock\nuv add ruff\n```\n\n**运行代码\u002F检查:**\n```bash\nuv run ruff check\n```\n\n**锁定与同步依赖:**\n```bash\nuv lock  # 解析并生成\u002F更新 uv.lock\nuv sync  # 同步当前环境与 lockfile 保持一致\n```\n\n---\n\n## 📜 3. 脚本执行 (Scripts)\n\n`uv` 支持管理单文件脚本的依赖和环境（支持内联依赖元数据）。\n\n**创建脚本并添加依赖:**\n```bash\necho 'import requests; print(requests.get(\\\"https:\u002F\u002Fastral.sh\\\"))' > example.py\n\n# 将依赖写入脚本的内联元数据中\nuv add --script example.py requests\n```\n\n**运行脚本:**\n```bash\n# uv 会在隔离的临时虚拟环境中自动安装依赖并运行\nuv run example.py\n```\n\n---\n\n## 🛠️ 4. 工具管理 (Tools)\n\n类似于 `pipx`，`uv` 可以执行和安装由 Python 包提供的命令行工具。\n\n**在临时环境中运行工具 (使用 `uvx` 或 `uv tool run`):**\n```bash\nuvx pycowsay 'hello world!'\n```\n\n**全局安装工具:**\n```bash\nuv tool install ruff\nruff --version\n```\n\n---\n\n## 🐍 5. Python 版本管理\n\n`uv` 可以自动下载、安装并切换 Python 版本。\n\n**安装多个 Python 版本:**\n```bash\nuv python install 3.12 3.13 3.14\n```\n\n**使用特定版本创建虚拟环境:**\n```bash\nuv venv --python 3.12.0\n```\n\n**在当前目录固定 Python 版本:**\n```bash\nuv python pin 3.11\n# 这会生成一个 .python-version 文件\n```\n\n**使用特定版本直接运行:**\n```bash\nuv run --python pypy@3.8 -- python --version\n```\n\n---\n\n## 🔄 6. pip 兼容接口\n\n如果你暂时不想改变现有的工作流，可以直接使用 `uv pip` 接口，它不仅完全兼容 `pip` 和 `pip-tools`，还能带来百倍的性能提升。\n\n**编译 requirements (替代 pip-compile):**\n```bash\nuv pip compile requirements.in --universal --output-file requirements.txt\n```\n\n**创建虚拟环境 (替代 virtualenv):**\n```bash\nuv venv\nsource .venv\u002Fbin\u002Factivate  # macOS\u002FLinux\n# .venv\\Scripts\\activate   # Windows\n```\n\n**安装依赖 (替代 pip install):**\n```bash\nuv pip install -r requirements.txt\nuv pip install flask\n```",null,"https:\u002F\u002Fcdn.xiaolong0418.com\u002Fapp%2Fbase%2FScreenShot_2026-04-02_181145_384_4aa08558cacf4ac18586a87a92f37d80.png","PUBLISHED",true,"2026-05-25T02:21:44.000Z",348,"2026-04-02T10:12:25.000Z","2026-06-26T08:40:26.915Z",{"id":4,"name":5,"slug":6},{"id":28,"name":29,"avatar":18},"f9d0f2de-c700-4f90-b535-afd3dbe78128","Admin",[],{"id":32,"title":33,"slug":34,"content":35,"excerpt":18,"coverImage":36,"status":20,"isPinned":37,"pinnedAt":18,"viewCount":38,"publishedAt":39,"createdAt":39,"updatedAt":40,"category":41,"author":42,"tags":43},"84e57f46-a67a-4a44-ba12-bc889f751ed9","MySQL 9 快速入门教程：从零开始掌握核心操作","mysql-9-快速入门教程-从零开始掌握核心操作","MySQL 9作为最新的版本，引入了一些新特性，不过对于初学者来说，核心的SQL操作和数据库设计思想依然是一脉相承的。这篇教程旨在帮你快速建立对MySQL 9的整体认识，并掌握最基本的操作。\n\n---\n\n# MySQL 9 快速入门教程：从零开始掌握核心操作\n\nMySQL 是目前世界上最受欢迎的开源关系型数据库之一。因其体积小、速度快、总体拥有成本低，被大量中小型企业和互联网公司采用 。2024年，MySQL 9 正式推出，带来了性能优化和细微的功能增强 。本教程将基于最新的 MySQL 9 版本，带你从零开始，完成安装、连接、建库、建表以及基本的增删改查操作。\n\n## 1. MySQL 9 的安装与连接\n\n在开始学习 SQL 语句之前，我们需要先在电脑上安装 MySQL 并成功连接上它。\n\n### 1.1 快速安装指南\n\n你可以根据你的操作系统选择不同的安装方式：\n\n- **Windows \u002F macOS \u002F Linux**：最推荐的方式是前往 MySQL 官方网站下载针对你操作系统的**安装包（MSI \u002F DMG \u002F RPM）**。对于初学者，使用图形化安装向导可以省去很多配置的麻烦 。\n- **通过 Docker 安装（推荐尝鲜）**：如果你想保持电脑环境的整洁，或者想快速体验 MySQL 9 而避免复杂的安装步骤，Docker 是最佳选择。\n    1. 拉取 MySQL 9 镜像（以 9.4 为例）：\n        ```bash\n        docker pull mysql:9.4.0\n        ```\n    2. 运行容器并设置 root 用户的密码：\n        ```bash\n        docker run -d --name mysql9-demo -p 3306:3306 -e MYSQL_ROOT_PASSWORD=你的密码 mysql:9.4.0\n        ```\n        这样，一个 MySQL 9 服务就在后台启动，并映射在本机的 3306 端口了 。\n\n### 1.2 登录 MySQL\n\n安装完成后，我们需要使用 MySQL 自带的命令行客户端连接服务器。\n\n打开终端（或 Windows 的命令提示符），输入以下命令：\n\n```bash\nmysql -u root -p\n```\n\n按回车后，系统会提示你输入密码。输入安装时设置的 root 密码，即可看到欢迎界面，进入 `mysql>` 命令行模式 。\n\n## 2. 数据库和数据表的基本操作\n\n进入 MySQL 客户端后，我们就可以开始操作数据库了。如果把数据库比作一个仓库，那么数据表就是仓库里一个个货架。\n\n### 2.1 数据库操作\n\n- **查看现有数据库**：\n  ```sql\n  SHOW DATABASES;\n  ```\n  初始化后，通常会看到 `information_schema`、`mysql`、`performance_schema` 和 `sys` 这几个系统库 。\n\n- **创建新数据库**：\n  我们来创建一个名为 `shop` （商城）的数据库。\n  ```sql\n  CREATE DATABASE shop;\n  ```\n\n- **选择使用数据库**：\n  接下来的所有操作都会在 `shop` 数据库中进行。\n  ```sql\n  USE shop;\n  ```\n\n### 2.2 数据类型简介\n\n在创建表之前，需要了解几个最常用的数据类型，这决定了“货架”上可以放什么类型的“商品” ：\n- **数值类型**：`INT` 整数，`DECIMAL(10,2)` 小数（如价格）。\n- **字符串类型**：`VARCHAR(255)` 可变长度字符串（如用户名、地址），`TEXT` 长文本（如商品详情）。\n- **日期类型**：`DATE` （日期），`DATETIME` （日期和时间，如订单创建时间）。\n\n### 2.3 创建数据表\n\n我们创建一个简单的 `users` （用户表）和 `orders` （订单表）来演示。\n\n- **创建用户表 （users）**：\n  ```sql\n  CREATE TABLE users (\n      id INT AUTO_INCREMENT PRIMARY KEY, -- 用户ID，自动增长，并设为主键\n      username VARCHAR(50) NOT NULL UNIQUE, -- 用户名，非空，且唯一\n      email VARCHAR(100) NOT NULL, -- 邮箱，非空\n      created_at DATETIME DEFAULT CURRENT_TIMESTAMP -- 创建时间，默认为当前时间\n  );\n  ```\n  `AUTO_INCREMENT` 可以让 `id` 字段在插入新记录时自动加1，`PRIMARY KEY` 则保证了每条记录的唯一性 。\n\n- **查看表结构**：\n  ```sql\n  DESCRIBE users;\n  ```\n  或者\n  ```sql\n  SHOW CREATE TABLE users \\G\n  ```\n  前者显示简要结构，后者显示建表的详细 SQL 语句 。\n\n## 3. 数据的增删改查（CRUD）\n\n有了表和字段，接下来就是最核心的数据操作：**增（Create）、删（Delete）、改（Update）、查（Retrieve）**。\n\n### 3.1 插入数据 （INSERT）\n\n向 `users` 表中添加几条用户记录。\n\n```sql\n-- 插入一条数据，id 会自动生成，created_at 会自动填充当前时间\nINSERT INTO users (username, email) VALUES ('张三', 'zhangsan@email.com');\n\n-- 插入多条数据\nINSERT INTO users (username, email) VALUES\n    ('李四', 'lisi@email.com'),\n    ('王五', 'wangwu@email.com');\n```\n\n### 3.2 查询数据 （SELECT）\n\n查询是数据库中使用最频繁的操作。\n\n- **查询所有数据**：\n  ```sql\n  SELECT * FROM users;\n  ```\n  `*` 代表所有字段，但在实际生产环境中，建议只列出需要查询的字段名。\n\n- **查询特定字段并添加条件**：\n  查询用户名为“张三”的邮箱和注册时间。\n  ```sql\n  SELECT email, created_at FROM users WHERE username='张三';\n  ```\n  `WHERE` 关键字用于过滤数据，类似编程中的 `if` 条件 。\n\n### 3.3 更新数据 （UPDATE）\n\n如果用户信息发生变化，需要修改数据。\n\n- **修改数据**：\n  把用户 “李四” 的邮箱更新为新地址。\n  ```sql\n  UPDATE users SET email = 'lisi_new@email.com' WHERE username = '李四';\n  ```\n  **⚠️ 警告**：`UPDATE` 语句中一定要加 `WHERE` 条件，否则会**修改表中所有行的数据**！\n\n### 3.4 删除数据 （DELETE）\n\n删除不再需要的用户记录。\n\n- **删除数据**：\n  删除用户名为 “王五” 的用户。\n  ```sql\n  DELETE FROM users WHERE username = '王五';\n  ```\n  **⚠️ 警告**：同样，`DELETE` 语句也必须加 `WHERE` 条件，否则会清空整个表！\n\n## 4. 高级查询初步\n\n掌握基础增删改查后，我们来了解一下稍微复杂一点的查询，为后续深入学习做准备。\n\n### 4.1 条件与排序\n\n- **查询并排序**：\n  查询所有用户，并按照注册时间倒序排列（最新的在前）。\n  ```sql\n  SELECT * FROM users ORDER BY created_at DESC;\n  ```\n  `DESC` 表示降序，`ASC` 表示升序（默认）。\n\n- **限制结果数量**：\n  只查询前2条数据。\n  ```sql\n  SELECT * FROM users LIMIT 2;\n  ```\n\n### 4.2 简单的表连接 （JOIN）\n\n假设我们创建了订单表 `orders`，里面包含 `user_id` 字段指向 `users` 表的 `id`。如果我们想查看每个订单对应的用户名，就需要将两张表连接起来查询。\n\n这是一个**内连接（INNER JOIN）**的例子：\n```sql\nSELECT orders.id AS order_id, users.username, orders.amount\nFROM orders\nINNER JOIN users ON orders.user_id = users.id;\n```\n这个查询会返回订单表中的订单ID、金额，以及通过 `user_id` 匹配到的用户表中的用户名 。\n\n### 4.3 使用聚合函数\n\n如果想统计用户总数或订单总金额，就需要使用聚合函数。\n\n```sql\n-- 统计用户总数\nSELECT COUNT(*) FROM users;\n\n-- 计算所有订单的总金额\nSELECT SUM(amount) FROM orders;\n```\n\n## 5. 总结与下一步\n\n至此，你已经完成了 MySQL 9 的快速入门。回顾一下，我们学习了：\n1.  如何安装和连接 MySQL 9。\n2.  数据库和数据表的新建与查看。\n3.  最核心的增删改查（CRUD）操作。\n4.  初步了解了排序、连接和聚合等进阶查询技巧。\n\n这些基础知识是你操作 MySQL 的基石。在后续的学习中，你可以继续探索 ：\n- **索引的设计和使用**：用于提升海量数据下的查询速度。\n- **视图与存储过程**：封装复杂的 SQL 逻辑。\n- **事务控制**：保证数据的一致性和完整性（如银行转账）。\n- **权限与安全管理**：控制不同用户的访问权限。\n\n希望这篇教程能帮你快速开启 MySQL 9 的学习之旅！","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fapp%2Fbase%2F1_5f66853e84fb465e82b1f8e7fc8be5bd.png",false,385,"2026-03-06T11:10:56.000Z","2026-06-26T08:40:26.391Z",{"id":4,"name":5,"slug":6},{"id":28,"name":29,"avatar":18},[44],{"id":45,"name":46,"slug":47},"c95bbe84-bdd0-410a-86a9-e87958c55f4f","Redis","redis",{"id":49,"title":50,"slug":51,"content":52,"excerpt":50,"coverImage":53,"status":20,"isPinned":37,"pinnedAt":18,"viewCount":54,"publishedAt":55,"createdAt":55,"updatedAt":56,"category":57,"author":58,"tags":59},"e5d658f5-ffa1-481e-913d-c8eb165fced5","快速删除node_modules","快速删除node-modules","### 用户痛点\n> 删除node_modules太慢，或者各种权限不足等问题\n\n### 使用步骤\n```shell\nnpm install rimraf -g \n```\n\n>输入绝对路径\u002F相对路径都可以\n```shell\nrimraf .\u002Fnode_modules\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Ff0bd7e70c911c09ac95cf426ccecb118.png",659,"2022-10-10T06:23:55.000Z","2026-05-24T20:16:42.000Z",{"id":4,"name":5,"slug":6},{"id":28,"name":29,"avatar":18},[60],{"id":61,"name":62,"slug":62},"0f1cc678-40e4-44b1-b2cf-a6fd8a1c867a","npm",{"id":64,"title":65,"slug":66,"content":67,"excerpt":65,"coverImage":68,"status":20,"isPinned":37,"pinnedAt":18,"viewCount":69,"publishedAt":70,"createdAt":70,"updatedAt":71,"category":72,"author":73,"tags":74},"036fe83d-52ef-40ac-84a1-4230d728fc70","Redis入门到精通","redis入门到精通","### 客户端下载\n> 官方地址\n```http\nhttps:\u002F\u002Fredis.com\u002Fredis-enterprise\u002Fredis-insight\u002F#insight-form\n```\n![](https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F5dbfe03ca97edb6640477b90b0ae4bd3.png)\n\n![](https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F6b06aeaca34c66fc50910cf7433fc390.png)\n![](https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F23b7f7d578c0199a495086fec830b342.png)\n\n\n### redis封装\n```shell\npnpm add -S ioredis\n```\n\n```js\nimport { Logger } from '@nestjs\u002Fcommon';\nimport Redis from 'ioredis';\n\nconst logger = new Logger('auth.service');\nconst redisIndex = []; \u002F\u002F 用于记录 redis 实例索引\nconst redisList = []; \u002F\u002F 用于存储 redis 实例\nconst redisOption = {\n\thost: 'IP地址',\n\tport: 6379,\u002F\u002F端口号\n\tpassword: '密码',\n};\nexport class RedisInstance {\n\tstatic async initRedis(method: string, db = 0) {\n\t\tconst isExist = redisIndex.some((x) => x === db);\n\t\tif (!isExist) {\n\t\t\tLogger.debug(`[Redis ${db}]来自 ${method} 方法调用 `);\n\t\t\tredisList[db] = new Redis({ ...redisOption, db });\n\t\t\tredisIndex.push(db);\n\t\t} else {\n\t\t\tLogger.debug(`[Redis ${db}]来自 ${method} 方法调用`);\n\t\t}\n\t\treturn redisList[db];\n\t}\n\n\tstatic async setRedis(method: string, db = 0, key: string, val: any, timeout = 60 * 60) {\n\t\tif (typeof val == 'object') {\n\t\t\tval = JSON.stringify(val);\n\t\t}\n\t\tconst redis = await RedisInstance.initRedis(method, db);\n\t\tredis.set(`${key}`, val);\n\t\tredis.expire(`${key}`, timeout);\n\t}\n\tstatic async getRedis(method: string, db = 0, key: string) {\n\t\treturn new Promise(async (resolve, reject) => {\n\t\t\tconst redis = await RedisInstance.initRedis(method, db);\n\t\t\tredis.get(`${key}`, (err, val) => {\n\t\t\t\tif (err) {\n\t\t\t\t\treject(err);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tresolve(val);\n\t\t\t});\n\t\t});\n\t}\n}\n\n```\n### Redis使用\n```js\nimport { Injectable } from '@nestjs\u002Fcommon';\nimport { InjectRepository } from '@nestjs\u002Ftypeorm';\nimport { Repository } from 'typeorm';\nimport { Navigation } from '.\u002Fentities\u002Fnavigation.entity';\nimport { CreateNavigationDto } from '.\u002Fdto\u002Fcreate-navigation.dto';\nimport { UpdateNavigationDto } from '.\u002Fdto\u002Fupdate-navigation.dto';\nimport { RedisInstance } from 'src\u002Futils\u002Fredis';\n\n@Injectable()\nexport class NavigationService {\n\tconstructor(\n\t\t@InjectRepository(Navigation)\n\t\tprivate readonly NavigationModel: Repository\u003CNavigation>,\n\t) {}\n\tasync create(params: CreateNavigationDto) {\n\t\treturn await this.NavigationModel.save(params);\n\t}\n\n\tasync update(params) {\n\t\t\u002F\u002F判断是不是数组\n\t\tif (!Array.isArray(params)) {\n\t\t\tparams = [params];\n\t\t}\n\n\t\tparams.forEach(async (item) => {\n\t\t\treturn await this.NavigationModel.createQueryBuilder()\n\t\t\t\t.update(Navigation)\n\t\t\t\t.set(item)\n\t\t\t\t.where('id = :id', { id: item.id })\n\t\t\t\t.execute();\n\t\t});\n\n\t\tthis.findAll('', true);\n\t}\n\tz;\n\n\tasync findAll(params, isUpdateCache = false) {\n\t\tconst { page = 1, pageSize = 1000, status, typeId } = params;\n\t\tconst where: any = {};\n\t\tstatus && (where.status = status);\n\t\ttypeId && (where.typeId = typeId);\n\t\tconst rowsKey = 'navigation_rows';\n\t\tconst countKey = 'navigation_count';\n\n\t\t\u002F\u002F存储导航列表到redis\n\t\tif (isUpdateCache) {\n\t\t\tconst rows = await this.NavigationModel.find({\n\t\t\t\torder: { orderId: 'ASC' },\n\t\t\t\twhere,\n\t\t\t\tskip: (page - 1) * pageSize,\n\t\t\t\ttake: pageSize,\n\t\t\t\tcache: true,\n\t\t\t});\n\t\t\tconst count = await this.NavigationModel.count({ where });\n\n\t\t\tawait RedisInstance.setRedis('navigation', 0, rowsKey, rows);\n\t\t\tawait RedisInstance.setRedis('navigation', 0, countKey, count);\n\t\t} else {\n\t\t\t\u002F\u002F查询全部数据,走缓存\n\t\t\tif (page === 1 && pageSize === 1000 && JSON.stringify(where) === '{}') {\n\t\t\t\tconst rowsRedis = (await RedisInstance.getRedis('navigation', 0, rowsKey)) as string;\n\t\t\t\tconst countRedis = (await RedisInstance.getRedis('navigation', 0, countKey)) as string;\n\n\t\t\t\t\u002F\u002F如果两个都为空就重新查询一次\n\t\t\t\tif (!rowsRedis && !countRedis) {\n\t\t\t\t\tconst rows = await this.NavigationModel.find({\n\t\t\t\t\t\torder: { orderId: 'ASC' },\n\t\t\t\t\t\twhere,\n\t\t\t\t\t\tskip: (page - 1) * pageSize,\n\t\t\t\t\t\ttake: pageSize,\n\t\t\t\t\t\tcache: true,\n\t\t\t\t\t});\n\t\t\t\t\tconst count = await this.NavigationModel.count({ where });\n\t\t\t\t\tawait RedisInstance.setRedis('navigation', 0, rowsKey, rows);\n\t\t\t\t\tawait RedisInstance.setRedis('navigation', 0, countKey, count);\n\t\t\t\t\treturn { rows, count };\n\t\t\t\t}\n\n\t\t\t\treturn { rows: JSON.parse(rowsRedis), count: countRedis };\n\t\t\t} else {\n\t\t\t\t\u002F\u002F不走缓存\n\t\t\t\tconst rows = await this.NavigationModel.find({\n\t\t\t\t\torder: { orderId: 'ASC' },\n\t\t\t\t\twhere,\n\t\t\t\t\tskip: (page - 1) * pageSize,\n\t\t\t\t\ttake: pageSize,\n\t\t\t\t\tcache: true,\n\t\t\t\t});\n\t\t\t\tconst count = await this.NavigationModel.count({ where });\n\t\t\t\treturn { rows, count };\n\t\t\t}\n\t\t}\n\t}\n\n\tasync remove(ids) {\n\t\tconst arr = ids?.split(',');\n\t\tconst res = await this.NavigationModel.createQueryBuilder()\n\t\t\t.delete()\n\t\t\t.from(Navigation)\n\t\t\t.whereInIds(arr.map((item) => Number(item)))\n\t\t\t.execute();\n\n\t\tthis.findAll('', true);\n\n\t\treturn res;\n\t}\n}\n\n```\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F9be55908232673906c382cfe08cf56dc.png",597,"2022-10-05T05:03:06.000Z","2026-05-23T10:49:18.000Z",{"id":4,"name":5,"slug":6},{"id":28,"name":29,"avatar":18},[75],{"id":45,"name":46,"slug":47},{"id":77,"title":78,"slug":79,"content":80,"excerpt":78,"coverImage":81,"status":20,"isPinned":37,"pinnedAt":18,"viewCount":82,"publishedAt":83,"createdAt":83,"updatedAt":84,"category":85,"author":86,"tags":87},"b75faef2-8942-4e84-95ec-d89f1809e6cc","Nest入门到精通","nest入门到精通","## 起步\n```js\nnpm i -g @nestjs\u002Fcli\n```\n## 创建项目\n```js\nnest new project-name\n```\n\n## 创建 Module\n> modules文件下的user\n\n```language\nnest g module user modules\n```\n\n\n## 创建 Controller\n\n```language\nnest g controller user modules\n```\n\n## 创建 Provider\n```language\nnest g service user modules\n```\n\n## 直接生成一个CRUD模块的navigation，选择PEST API\n```shell\n nest g resource navigation  modules\n```\n\n\n\n## 配置swagger\n> 新建文件src\u002Fswagger\u002Findex.ts\n```language\nimport { DocumentBuilder, SwaggerModule } from '@nestjs\u002Fswagger';\n\nconst swaggerOptions = new DocumentBuilder()\n\t.setTitle('小白龙博客接口文档')\n\t.setDescription('小白龙博客文档')\n\t.setVersion('1.0.0')\n\t.addBearerAuth()\n\t.build();\n\nexport function createSwagger(app) {\n\tconst document = SwaggerModule.createDocument(app, swaggerOptions);\n\tSwaggerModule.setup('swagger-doc', app, document);\n}\n\n```\n\n## 配置功能模块标签与接口名字\n```language\nimport { LinksSetDto } from '.\u002Fdto\u002Flinks.set.dto';\nimport { ApplyLinksService } from '.\u002Fapply-links.service';\nimport { ApiTags, ApiOperation } from '@nestjs\u002Fswagger';\nimport { Controller, Get, Post, Query, Body } from '@nestjs\u002Fcommon';\n\n@ApiTags('申请友链')\n@Controller('apply-links')\nexport class ApplyLinksController {\n\tconstructor(private readonly applyLinksService: ApplyLinksService) {}\n\n\t@Get('\u002Fset')\n\t@ApiOperation({ summary: '创建申请友链' })\n\tset(@Query() params: LinksSetDto) {\n\t\treturn this.applyLinksService.set(params);\n\t}\n\n\t@Get('\u002Fquery')\n\t@ApiOperation({ summary: '获取申请友链列表' })\n\tquery(@Query() params) {\n\t\treturn this.applyLinksService.query(params);\n\t}\n\n\t@Post('\u002Fdel')\n\t@ApiOperation({ summary: '删除{id}申请友链' })\n\tdel(@Body() params) {\n\t\treturn this.applyLinksService.del(params);\n\t}\n}\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F1657957992402.png",719,"2022-07-16T08:26:12.000Z","2026-05-25T02:21:46.000Z",{"id":4,"name":5,"slug":6},{"id":28,"name":29,"avatar":18},[88],{"id":89,"name":90,"slug":91},"ab034d3a-6e5b-4db5-a2dc-faf4ccbb63f5","Nest","nest",5,1,10,{"categories":96,"tags":176,"postCount":352,"tagCount":353,"hotPosts":354},[97,104,110,117,123,130,136,140,147,151,155,160,161,166,170],{"id":98,"name":99,"slug":100,"description":101,"sortOrder":102,"createdAt":103,"updatedAt":103,"postCount":93},"e8d0bd45-d10c-46d3-8afb-0c072df7f8a7","技术","tech","技术文章",0,"2026-06-27T04:18:37.371Z",{"id":105,"name":106,"slug":107,"description":106,"sortOrder":93,"createdAt":108,"updatedAt":9,"postCount":109},"15ac46ad-edf7-4435-9a64-ff78117d58c5","Vue3 生态","vue3-生态","2022-05-21T08:05:39.000Z",6,{"id":111,"name":112,"slug":113,"description":112,"sortOrder":114,"createdAt":115,"updatedAt":9,"postCount":116},"11d4d397-685c-4180-a7b3-9b0e3a1e411e","Css","css",2,"2022-05-23T07:19:37.000Z",9,{"id":118,"name":119,"slug":119,"description":119,"sortOrder":120,"createdAt":121,"updatedAt":9,"postCount":122},"d10456a5-e649-4741-a38f-f07f266ce5f2","开发环境",3,"2022-05-24T01:52:41.000Z",13,{"id":124,"name":125,"slug":126,"description":125,"sortOrder":127,"createdAt":128,"updatedAt":9,"postCount":129},"5ed5cc62-43ea-49a2-b0b2-38bc7aae52a0","Vue3","vue3",4,"2022-05-24T01:55:05.000Z",8,{"id":131,"name":132,"slug":133,"description":132,"sortOrder":92,"createdAt":134,"updatedAt":9,"postCount":135},"da130ba9-d4f4-49f3-aa0f-149078097ef0","JavaScript","javascript","2022-05-24T02:22:57.000Z",18,{"id":137,"name":138,"slug":138,"description":138,"sortOrder":109,"createdAt":139,"updatedAt":9,"postCount":93},"d8cbe380-54b3-4a61-a12d-5438c2918574","限时优惠","2022-05-25T07:18:03.000Z",{"id":141,"name":142,"slug":143,"description":142,"sortOrder":144,"createdAt":145,"updatedAt":9,"postCount":146},"e0f3b8d8-cfe7-41fb-802b-a79699d95968","JavaScript插件","javascript插件",7,"2022-06-01T14:08:31.000Z",16,{"id":148,"name":149,"slug":149,"description":149,"sortOrder":129,"createdAt":150,"updatedAt":9,"postCount":109},"4ea3d8af-9cc3-49bb-a9cd-34dbcdc3bd85","构建工具","2022-06-02T07:28:13.000Z",{"id":152,"name":153,"slug":153,"description":153,"sortOrder":116,"createdAt":154,"updatedAt":9,"postCount":129},"9ed9827c-9cbb-42da-80e4-d04c7fdba886","开发工具","2022-06-21T03:35:05.000Z",{"id":156,"name":157,"slug":158,"description":157,"sortOrder":94,"createdAt":159,"updatedAt":9,"postCount":92},"6b9179c3-17b2-43ff-a431-a03d6eb32d89","Vue2 生态","vue2-生态","2022-07-16T13:14:29.000Z",{"id":4,"name":5,"slug":6,"description":5,"sortOrder":7,"createdAt":8,"updatedAt":9,"postCount":92},{"id":162,"name":163,"slug":163,"description":163,"sortOrder":164,"createdAt":165,"updatedAt":9,"postCount":114},"2b696c16-48ef-403b-a88b-6e57cfc79596","开发问题",12,"2022-07-16T14:06:54.000Z",{"id":167,"name":168,"slug":168,"description":168,"sortOrder":122,"createdAt":169,"updatedAt":9,"postCount":93},"c0f0561e-a47a-4ecd-8caa-cc1df2315d57","算法","2022-07-16T14:22:34.000Z",{"id":171,"name":172,"slug":173,"description":172,"sortOrder":174,"createdAt":175,"updatedAt":9,"postCount":120},"a629c1f7-29f1-439e-be3c-29670b17ba20","Vue2","vue2",15,"2022-07-16T14:41:51.000Z",[177,183,188,191,196,201,205,209,214,219,221,226,227,232,236,240,245,247,252,257,261,265,269,271,275,280,285,288,291,294,297,300,304,307,309,311,314,318,321,325,329,332,335,337,340,342,346,349],{"id":178,"name":179,"slug":180,"createdAt":181,"updatedAt":182},"076bd8b9-293e-45cb-9dc3-e162007ca474","Axios","axios","2022-06-05T07:41:56.000Z","2025-12-30T07:26:21.000Z",{"id":184,"name":185,"slug":186,"createdAt":187,"updatedAt":9},"2aa7f6d0-1fac-4ed1-b9bb-f3afc813f42c","Axure","axure","2022-06-21T03:35:15.000Z",{"id":189,"name":112,"slug":113,"createdAt":190,"updatedAt":9},"b084ddd8-09be-4e57-98f0-cf4e376aecd7","2022-05-21T09:59:55.000Z",{"id":192,"name":193,"slug":194,"createdAt":195,"updatedAt":9},"78a62bff-ff77-4878-8c25-3e6aae18c668","Docker","docker","2022-07-16T14:34:37.000Z",{"id":197,"name":198,"slug":199,"createdAt":200,"updatedAt":9},"2de16806-ef3f-4e54-a259-d1e1e182468c","Git","git","2022-07-16T14:25:15.000Z",{"id":202,"name":203,"slug":204,"createdAt":190,"updatedAt":9},"994cc226-578b-4a72-a57e-a47a63d2793e","JavaScript生态","javascript生态",{"id":206,"name":207,"slug":208,"createdAt":190,"updatedAt":9},"5086e93c-23b9-43d3-9643-cc87f0e9ee94","JenKins","jenkins",{"id":210,"name":211,"slug":212,"createdAt":213,"updatedAt":9},"b73007a8-bb5c-42a8-9fd9-163033a5b45d","Linux","linux","2022-07-16T14:40:17.000Z",{"id":215,"name":216,"slug":217,"createdAt":218,"updatedAt":9},"0b658b92-dd6b-4db3-a398-9f6d69950a02","Markdown","markdown","2022-07-16T14:39:25.000Z",{"id":89,"name":90,"slug":91,"createdAt":220,"updatedAt":9},"2022-07-16T13:15:49.000Z",{"id":222,"name":223,"slug":224,"createdAt":225,"updatedAt":9},"52c41978-da06-4962-9636-45bbaeedda80","Nginx","nginx","2022-05-21T09:59:56.000Z",{"id":61,"name":62,"slug":62,"createdAt":225,"updatedAt":9},{"id":228,"name":229,"slug":230,"createdAt":231,"updatedAt":9},"a4370d78-70e1-4073-a8f6-3dc5d81fd8fd","Nuxt","nuxt","2022-06-01T13:07:07.000Z",{"id":233,"name":234,"slug":235,"createdAt":190,"updatedAt":9},"d232e01f-048e-4151-8a0a-fff9561f946f","Pinia","pinia",{"id":237,"name":238,"slug":239,"createdAt":225,"updatedAt":9},"14e9ab02-b0bb-4c85-8604-fe6f1f0f33cd","Pnpm","pnpm",{"id":241,"name":242,"slug":243,"createdAt":244,"updatedAt":244},"399d1d38-cc0d-43ce-8baf-c769447a2ebd","React生态","react生态","2023-02-21T02:03:09.000Z",{"id":45,"name":46,"slug":47,"createdAt":246,"updatedAt":9},"2022-10-05T05:14:14.000Z",{"id":248,"name":249,"slug":250,"createdAt":251,"updatedAt":9},"6d05f9df-e116-450f-af57-85ed710c4870","Swiper","swiper","2022-06-01T14:08:46.000Z",{"id":253,"name":254,"slug":255,"createdAt":256,"updatedAt":9},"66f3aeb0-84ef-45f6-a43a-944eefc9895a","Vite","vite","2022-06-02T07:28:24.000Z",{"id":258,"name":259,"slug":260,"createdAt":190,"updatedAt":9},"bf5b94d3-090b-4098-a03c-4bc69781fb2d","Vue","vue",{"id":262,"name":263,"slug":264,"createdAt":225,"updatedAt":9},"2f7fb1be-b9c5-4606-b54f-e9f66f2653b2","Vue-Router","vue-router",{"id":266,"name":267,"slug":268,"createdAt":190,"updatedAt":9},"2fef3b91-1c1c-4ae8-b2c1-0e04b4f9b3a2","Vue2生态","vue2生态",{"id":270,"name":125,"slug":126,"createdAt":190,"updatedAt":9},"62b94c93-724f-488d-8fc5-0449971d9204",{"id":272,"name":273,"slug":274,"createdAt":190,"updatedAt":9},"20bff9cd-7848-4c16-8775-42cf12b44b30","Vue3生态","vue3生态",{"id":276,"name":277,"slug":278,"createdAt":279,"updatedAt":9},"c807b2c6-cb12-4409-a1f1-6bea9f330a6b","Vuex","vuex","2022-07-16T13:14:59.000Z",{"id":281,"name":282,"slug":283,"createdAt":284,"updatedAt":9},"5782dff5-2ea2-4427-9696-d4363a7fd5bc","Webpack","webpack","2022-07-16T14:33:41.000Z",{"id":286,"name":287,"slug":287,"createdAt":190,"updatedAt":9},"d0aa41f4-68f8-48d4-a4ed-3a503ea90451","下载",{"id":289,"name":290,"slug":290,"createdAt":190,"updatedAt":9},"a046060c-39ef-474a-8c85-2546aca0e2e5","代码片段",{"id":292,"name":293,"slug":293,"createdAt":190,"updatedAt":9},"fee73435-b2be-4b55-85b1-d133ea96aea4","伪元素",{"id":295,"name":296,"slug":296,"createdAt":190,"updatedAt":9},"436bd369-8c57-4869-8827-e88e50e5e0ab","伪类",{"id":298,"name":299,"slug":299,"createdAt":190,"updatedAt":9},"4c6be544-8a00-4445-92a3-e3dcbaf6142e","动画",{"id":301,"name":302,"slug":302,"createdAt":303,"updatedAt":9},"9321a12e-ea72-49a9-a32d-5566149f812f","图片压缩","2022-08-02T00:37:47.000Z",{"id":305,"name":306,"slug":306,"createdAt":225,"updatedAt":9},"512b16fb-576a-4397-a7c5-dd20e6a8f9ca","布局",{"id":308,"name":153,"slug":153,"createdAt":190,"updatedAt":9},"f32faa96-f2ec-45c6-9a17-2c76062edcb0",{"id":310,"name":119,"slug":119,"createdAt":190,"updatedAt":9},"3c46ed3f-6d6b-4f91-bcb3-af5112860bf5",{"id":312,"name":313,"slug":313,"createdAt":190,"updatedAt":9},"dbfc086a-73a6-4560-814d-593acb61cf98","性能优化",{"id":315,"name":316,"slug":316,"createdAt":317,"updatedAt":9},"1831cd06-0d6b-48f7-94fa-324782fe23cb","拖拽","2022-07-28T12:39:13.000Z",{"id":319,"name":320,"slug":320,"createdAt":225,"updatedAt":9},"9a74300d-06f7-46d0-80d9-8fe67ec0539b","数组",{"id":322,"name":323,"slug":323,"createdAt":324,"updatedAt":9},"19ac8998-7e0a-459b-9702-bb1adca70e8c","文本复制","2022-07-17T01:54:45.000Z",{"id":326,"name":327,"slug":327,"createdAt":328,"updatedAt":9},"5ff33473-71a4-4e02-8c82-f9ea369a768f","时间","2022-07-17T01:51:12.000Z",{"id":330,"name":331,"slug":331,"createdAt":190,"updatedAt":9},"aa47ca4d-d3f6-4cac-b495-2c67c9592c36","最新优惠",{"id":333,"name":334,"slug":334,"createdAt":225,"updatedAt":9},"f6766d54-54fc-405e-932d-b7d550559125","服务器",{"id":336,"name":149,"slug":149,"createdAt":190,"updatedAt":9},"d856559a-03ff-40b4-980d-3f272b998c3c",{"id":338,"name":339,"slug":339,"createdAt":190,"updatedAt":9},"692d5d68-b188-4e5c-aca8-65d0229399a1","渐变",{"id":341,"name":168,"slug":168,"createdAt":190,"updatedAt":9},"38e1fd6b-d7c6-4d62-bf70-7bacc175bea9",{"id":343,"name":344,"slug":344,"createdAt":345,"updatedAt":9},"be7b10bc-49eb-4a03-bea7-ceb915d500fe","规范","2022-07-16T14:41:06.000Z",{"id":347,"name":348,"slug":348,"createdAt":225,"updatedAt":9},"b42e2916-ad62-4b8a-a863-cd8c19a829de","面试",{"id":350,"name":351,"slug":351,"createdAt":225,"updatedAt":9},"7069add9-b636-44f1-9cd4-ea3a6d2b85d3","面试题",104,48,[355,367,379,392,405],{"id":356,"title":357,"slug":357,"content":358,"excerpt":357,"coverImage":359,"status":20,"isPinned":37,"pinnedAt":18,"viewCount":360,"publishedAt":361,"createdAt":361,"updatedAt":362,"category":363,"author":364,"tags":365},"a1bd1f49-6f6d-4fea-9789-b5636e19a6b3","uni-app瀑布流","## 实现思路\n> 获取父组件的列表数组，watch监听数组长度变化，截取后面新的数据，创建两个左右数组，比较左右dom的长度，哪个短，就push一条数据进去，源数组删除一条数据。利用img的load（加载成功）和error方法（加载失败），触发数组的push，实现瀑布流\n\n## 代码实现\n\n```language\n\u003Ctemplate>\n    \u003Cview class=\\\"waterfall\\\">\n        \u003Cview class=\\\"waterfall_left\\\">\n            \u003Cview class=\\\"waterfall_list\\\" v-for=\\\"(item, index) in leftList\\\" :key=\\\"index\\\">\n                \u003CSearch\n                    :name=\\\"item.name\\\"\n                    :image=\\\"item.src\\\"\n                    :label=\\\"item.label\\\"\n                    :item=\\\"item\\\"\n                    @considerPush=\\\"considerPush\\\"\n                >\n                \u003C\u002FSearch>\n            \u003C\u002Fview>\n        \u003C\u002Fview>\n        \u003Cview class=\\\"waterfall_right\\\">\n            \u003Cview class=\\\"waterfall_list\\\" v-for=\\\"(item, index) in rightList\\\" :key=\\\"index\\\">\n                \u003CSearch\n                    :name=\\\"item.name\\\"\n                    :image=\\\"item.src\\\"\n                    :label=\\\"item.label\\\"\n                    :item=\\\"item\\\"\n                    @considerPush=\\\"considerPush\\\"\n                >\n                \u003C\u002FSearch>\n            \u003C\u002Fview>\n        \u003C\u002Fview>\n    \u003C\u002Fview>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nimport Search from '@\u002Fcomponents\u002Fsearch\u002Fsearch.vue';\nexport default {\n    components: { Search },\n    props: {\n        list: {\n            type: Array,\n            default: () => []\n        }\n    },\n    data() {\n        return {\n            \u002F\u002F 左侧列表\n            leftList: [],\n            \u002F\u002F 右侧列表\n            rightList: [],\n            \u002F\u002F 组件数据备份\n            newList: [],\n            \u002F\u002F默认请求数,主要为了正常排序\n            interceptNumber: 10\n        };\n    },\n    created() {\n        this.touchOff(); \u002F\u002F 触发排列\n    },\n    mounted() {},\n    watch: {\n        list(newValue, oldValue) {\n            this.interceptNumber = newValue.length - oldValue.length;\n            this.touchOff();\n        }\n    },\n    computed: {},\n    methods: {\n        \u002F\u002F 触发重新排列\n        touchOff() {\n            this.newList = [...this.list.slice(-this.interceptNumber)];\n            if (this.newList.length !== 0) {\n                this.leftList.push(this.newList.shift()); \u002F\u002F触发排列\n            }\n        },\n        \u002F\u002F 计算排列\n        considerPush() {\n            this.$nextTick(() => {\n                if (this.newList.length == 0) return; \u002F\u002F没有数据了\n                let leftH = 0;\n                let rightH = 0; \u002F\u002F左右高度\n                let query = uni.createSelectorQuery().in(this);\n                query.selectAll('.waterfall_left').boundingClientRect();\n                query.selectAll('.waterfall_right').boundingClientRect();\n                query.exec(res => {\n                    leftH = res[0].length != 0 ? res[0][0].height : 0; \u002F\u002F防止查询不到做个处理\n                    rightH = res[1].length != 0 ? res[1][0].height : 0;\n                    if (leftH == rightH || leftH \u003C rightH) {\n                        \u002F\u002F 相等 || 左边小\n                        this.leftList.push(this.newList.shift());\n                    } else {\n                        \u002F\u002F 右边小\n                        this.rightList.push(this.newList.shift());\n                    }\n\n                    \u002F\u002F console.log('左右高度：', leftH, rightH, leftH == rightH || leftH \u003C rightH);\n                });\n            });\n        }\n    }\n};\n\u003C\u002Fscript>\n\n\u003Cstyle scoped lang=\\\"scss\\\">\n.waterfall {\n    display: flex;\n    align-items: flex-start;\n    justify-content: flex-start;\n    .waterfall_left {\n        flex: 1;\n    }\n\n    .waterfall_right {\n        flex: 1;\n    }\n}\n\u003C\u002Fstyle>\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F7d4fa8d1d2775177b882a4656e3a5ed5.png",1817,"2022-11-22T07:54:41.000Z","2026-06-26T10:25:05.932Z",{"id":131,"name":132,"slug":133},{"id":28,"name":29,"avatar":18},[366],{"id":202,"name":203,"slug":204},{"id":368,"title":369,"slug":369,"content":370,"excerpt":369,"coverImage":371,"status":20,"isPinned":37,"pinnedAt":18,"viewCount":372,"publishedAt":373,"createdAt":373,"updatedAt":374,"category":375,"author":376,"tags":377},"b2c46bf6-d971-4cce-b21b-052dbea8e8a2","v-html使用img点击实现放大效果","## 代码实现\n```js\n\u002F**\n * JS获取html代码中所有的图片地址\n * @param htmlstr\n * @returns arr 数组\n *\u002F\n\nexport function getimgsrc(htmlstr) {\n    let reg = \u002F\u003Cimg.+?src=('|\\\")?([^'\\\"]+)('|\\\")?(?:\\s+|>)\u002Fg;\n    let arr = [];\n    let tem = 0;\n    \u002F\u002Feslint-disable-next-line\n    while ((tem = reg.exec(htmlstr))) {\n        arr.push(tem[2]); \u002F\u002F eslint-disable-line\n    }\n\n    return arr;\n}\n\n```\n\n\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\\\"image-expansion\\\" :class=\\\"classArr\\\">\n        \u003Cdiv @click.stop=\\\"hanldeImage($event)\\\" v-html=\\\"formatHtmlData\\\">\u003C\u002Fdiv>\n\n        \u003Cel-image-viewer\n            v-if=\\\"imgPreviewUrl\\\"\n            :initial-index=\\\"subscript\\\"\n            :src=\\\"imgPreviewUrl\\\"\n            :on-close=\\\"closeViewer\\\"\n            :url-list=\\\"imgList\\\"\n        >\u003C\u002Fel-image-viewer>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nimport { getimgsrc } from '..\u002F..\u002Futils\u002Fgetimgsrc';\nimport ElImageViewer from 'element-ui\u002Fpackages\u002Fimage\u002Fsrc\u002Fimage-viewer';\nexport default {\n    components: {\n        ElImageViewer\n    },\n    props: {\n        htmlData: {\n            type: String,\n            default: () => {\n                return '';\n            }\n        },\n        classArr: {\n            type: Array,\n            default: () => {\n                return ['min'];\n            }\n        },\n\n        isArticle: {\n            type: Boolean,\n            default: () => {\n                return false;\n            }\n        }\n    },\n    data() {\n        return {\n            imgList: [],\n            formatHtmlData: '',\n            imgPreviewUrl: '',\n            subscript: 0\n        };\n    },\n    computed: {},\n\n    watch: {\n        \u002F\u002F监听数据，防止数据不更新\n        htmlData: {\n            handler(newName, oldName) {\n                \u002F\u002F判断是否为文章\n                if (this.isArticle) {\n                    newName ? (this.formatHtmlData = newName.replace(\u002F(\u003C([^>]+)>)\u002Fgi, '').replace(\u002F[\\\r\\\n]\u002Fg, '')) : '';\n                } else {\n                    \u002F\u002F剔除strong和p标签\n                    newName ? (this.formatHtmlData = newName.replace(\u002F(\u003C\\\u002F?strong.*?>)|(\u003C\\\u002F?p.*?>)\u002Fg, '')) : '';\n\n                    \u002F\u002F获取html全部图片，push成图片数组\n                    this.imgList = Object.values(getimgsrc(this.formatHtmlData));\n                    \u002F\u002F获取图片下标\n                    let subscript = this.imgList.indexOf(this.imgPreviewUrl);\n                    this.subscript = subscript > -1 ? subscript : 0;\n                }\n            },\n\n            immediate: true\n        }\n    },\n\n    mounted() {},\n\n    methods: {\n        \u002F\u002F监听点击事件\n        hanldeImage(event) {\n            if (event.target.nodeName === 'IMG' || event.target.nodeName === 'img') {\n                \u002F\u002F获取点击的图片url,decodeURIComponent转码一下，防禁url转码\n                this.imgPreviewUrl = decodeURIComponent(event.target.currentSrc);\n\n                \u002F\u002F获取图片下标\n                let subscript = this.imgList.indexOf(this.imgPreviewUrl);\n                this.subscript = subscript > -1 ? subscript : 0;\n\n                \u002F\u002F禁止遮罩层后面的内容滚动\n                document.documentElement.style.overflowY = 'hidden';\n            } else {\n                this.$emit('goDetail');\n            }\n        },\n\n        \u002F\u002F关闭弹框\n        closeViewer() {\n            this.imgPreviewUrl = '';\n            \u002F\u002F恢复遮罩层后面的内容滚动\n            document.documentElement.style.overflowY = 'auto';\n        }\n    }\n};\n\u003C\u002Fscript>\n\n\u003Cstyle lang=\\\"scss\\\" scoped>\n.image-expansion {\n}\n\n.min {\n    \u002Fdeep\u002F img {\n        cursor: pointer;\n        height: 28px;\n        padding: 0 10px 3px;\n    }\n}\n\n.max {\n    \u002Fdeep\u002F img {\n        cursor: pointer;\n    }\n}\n\n.class1 {\n    \u002Fdeep\u002F div {\n        font-size: 15px;\n        font-family: Microsoft YaHei;\n        font-weight: 400;\n        color: #888888;\n        line-height: 30px;\n    }\n}\n\u003C\u002Fstyle>\n\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Fc77c3fb113d1ab2f67e7afba1ca33b95.png",1571,"2023-01-10T07:22:29.000Z","2026-06-26T10:25:07.650Z",{"id":156,"name":157,"slug":158},{"id":28,"name":29,"avatar":18},[378],{"id":301,"name":302,"slug":302},{"id":380,"title":381,"slug":382,"content":383,"excerpt":381,"coverImage":384,"status":20,"isPinned":37,"pinnedAt":18,"viewCount":385,"publishedAt":386,"createdAt":386,"updatedAt":387,"category":388,"author":389,"tags":390},"9f33b49b-0785-4404-9a4e-d85937e7fc92","在 vue3 中优雅的使用 jsx\u002Ftsx","在-vue3-中优雅的使用-jsx-tsx","## 安装插件（@vitejs\u002Fplugin-vue-jsx）\n```shell\npnpm add @vitejs\u002Fplugin-vue-jsx -D\n```\n\n## 配置vite.config.ts\n```ts\nimport vueJsx from \\\"@vitejs\u002Fplugin-vue-jsx\\\";\n\nexport default defineConfig({\n  plugins: [\n    vueJsx(),\n  ]\n})\n```\n\n## 插值\n```language\n\u002F\u002F vue3模板语法\n\u003Cspan>{{ a + b }}\u003C\u002Fspan>\n\n\u002F\u002F jsx\u002Ftsx\n\u003Cspan>{ a + b }\u003C\u002Fspan>\n```\n\n\n## class与style 绑定\n\n```ts\n\u002F\u002F 模板字符串\n\u003Cdiv className={`header ${ isBg ? 'headerBg' : '' }`}>header\u003C\u002Fdiv>\n\u002F\u002F数组\n\u003Cdiv class={ [ 'header', isBg && 'headerBg' ] } >header\u003C\u002Fdiv>\n```\n\n```ts\nconst color = 'red'\nconst element = \u003Csapn style={{ color, fontSize: '16px' }}>style\u003C\u002Fsapn>\n```\n\n## 条件渲染\n\n```ts\n   setup() {\n       const isShow = false\n       const element = () => {\n           if (isShow) {\n               return \u003Cspan>我是if\u003C\u002Fspan>\n           } else {\n               return \u003Cspan>我是else\u003C\u002Fspan>\n           }\n       }\n       return () => (\n           \u003Cdiv>\n               \u003Cspan v-show={isShow}>我是v-show\u003C\u002Fspan>\n               {\n                   element()\n               }\n               {\n                   isShow ? \u003Cp>我是三目1\u003C\u002Fp> : \u003Cp>我是三目2\u003C\u002Fp>\n               }\n           \u003Cdiv>\n       )\n   }\n```\n\n## 列表渲染\n```language\nsetup() {\n   const listData = [\n       {name: 'Tom', age: 18},\n       {name: 'Jim', age: 20},\n       {name: 'Lucy', age: 16}\n   ]\n   return () => (\n       \u003Cdiv>\n           \u003Cdiv class={'box'}>\n               \u003Cspan>姓名\u003C\u002Fspan>\n               \u003Cspan>年龄\u003C\u002Fspan>\n           \u003C\u002Fdiv>\n           {\n               prop.listData.map(item => {\n                   return \u003Cdiv class={'box'}>\n                       \u003Cspan>{item.name}\u003C\u002Fspan>\n                       \u003Cspan>{item.age}\u003C\u002Fspan>\n                   \u003C\u002Fdiv>\n               })\n           }\n       \u003C\u002Fdiv>\n   )\n}\n\n```\n\n## 事件处理\n\n```language\nsetup() {\n    const clickBox = val => {\n        console.log(val)\n    }\n    return () => (\n        \u003Cdiv class={'box1'} onClick={() => clickBox('box1')}>\n            \u003Cspan>我是box1\u003C\u002Fspan>\n            \u003Cdiv class={'box2'} onClick={() => clickBox('box2')}>\n                \u003Cspan>我是box2\u003C\u002Fspan>\n                \u003Cdiv class={'box3'} onClick={withModifiers(() => clickBox('box3'), ['stop'])}>我是box3\u003C\u002Fdiv>\n            \u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n    )\n}\n```\n\n## v-model\n\n```ts\n\u002F\u002F 正常写法\n\u003Cinput v-model=\\\"value\\\" \u002F> \u002F\u002F vue\n\u003Cinput v-model={value} \u002F> \u002F\u002F jsx\n\n\u002F\u002F 指定绑定值写法\n\u003Cinput v-model:modelValue=\\\"value\\\" \u002F> \u002F\u002F vue\n\u003Cinput v-model={[value,'modelValue']} \u002F> \u002F\u002F jsx\n\n\u002F\u002F 修饰符写法\n\u003Cinput v-model:modelValue.trim=\\\"value\\\" \u002F> \u002F\u002F vue\n\u003Cinput v-model={[value,'modelValue',['trim']]} \u002F> \u002F\u002F jsx\n```\n\n## slot插槽\n\n### 定义插槽\n```ts\nimport { renderSlot } from \\\"vue\\\"\nexport default defineComponent({\n    \u002F\u002F 从ctx中解构出来 slots\n    setup(props, { slots }) {\n        return () => (\n            \u003Cdiv>\n                { renderSlot(slots, 'default') }\n                { slots.title?.() }\n            \u003C\u002Fdiv>\n        )\n    }\n})\n\n```\n\n### 使用插槽\n\n```ts\nimport Vslot from '.\u002FslotTem'\nexport default defineComponent({\n    setup() {\n        return () => (\n            \u003Cdiv class={'box'}>\n                \u003CVslot v-slots={{\n                    title: () => {\n                        return \u003Cp>我是title插槽\u003C\u002Fp>\n                    },\n                    default: () => {\n                        return \u003Cp>我是default插槽\u003C\u002Fp>\n                    }\n                }} \u002F>\n            \u003C\u002Fdiv>\n        )\n    }\n})\n\n```\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F3e4cb0d3df611ee3f57b8ed503e1015e.png",1391,"2023-04-19T09:23:27.000Z","2026-06-26T09:54:47.369Z",{"id":124,"name":125,"slug":126},{"id":28,"name":29,"avatar":18},[391],{"id":270,"name":125,"slug":126},{"id":393,"title":394,"slug":395,"content":396,"excerpt":394,"coverImage":397,"status":20,"isPinned":37,"pinnedAt":18,"viewCount":398,"publishedAt":399,"createdAt":399,"updatedAt":400,"category":401,"author":402,"tags":403},"1dcdd755-0e46-45a3-8998-9a213fd3fcd5","Vue3 导入导出Excel","vue3-导入导出excel","## 安装\n```shell\npnpm add -S XLSX\n```\n\n\n## 方法封装\n```js\nimport * as XLSX from 'xlsx';\n\n\u002F\u002F参数说明\n\u002F\u002Fconfiguration: {\n\u002F\u002F  data: [], \u002F\u002F 导出的数据\n\u002F\u002F  head: {}, \u002F\u002F 导出的数据对应的表头\n\u002F\u002F  name: '', \u002F\u002F 导出的文件名\n\u002F\u002F  label: '', \u002F\u002F 导出的表单名\n\u002F\u002F  widthArr: [], \u002F\u002F 导出的表单列宽\n\u002F\u002F}\n\n\u002F\u002F 导出excel\nexport const ExportXlsx = (configuration) => {\n  const { data, head, name, label, widthArr } = configuration;\n\n  const list = data.map((item) => {\n    const obj = {};\n    for (const k in item) {\n      if (head[k]) {\n        obj[head[k]] = item[k];\n      }\n    }\n    return obj;\n  });\n\n  \u002F\u002F 创建工作表\n  const xLSXData = XLSX.utils.json_to_sheet(list);\n  \u002F\u002F 创建工作簿\n  const wb = XLSX.utils.book_new();\n  \u002F\u002F 将工作表放入工作簿中\n  XLSX.utils.book_append_sheet(wb, xLSXData, label);\n  xLSXData['!cols'] = [];\n  \u002F\u002F 设置列宽\n  widthArr.forEach((item) => {\n    xLSXData['!cols'].push({ wpx: item });\n  });\n\n  \u002F\u002F 生成文件并下载\n  XLSX.writeFile(wb, `${name}.xlsx`);\n};\n\n\u002F\u002F 导入excel\nexport const ImportXlsx = (e) => {\n  const file = e.target.files[0];\n  const reader = new FileReader();\n  reader.readAsArrayBuffer(file);\n  reader.onload = (e) => {\n    const data = e.target.result;\n    const workbook = XLSX.read(data, { type: 'binary', cellDates: true });\n    const wsname = workbook.SheetNames[0];\n    const outdata = XLSX.utils.sheet_to_json(workbook.Sheets[wsname]);\n    console.log(outdata);\n  };\n};\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F80231a326d3846b895a7d46c99e738ce.png",1334,"2023-03-14T06:18:59.000Z","2026-06-26T09:54:47.798Z",{"id":131,"name":132,"slug":133},{"id":28,"name":29,"avatar":18},[404],{"id":202,"name":203,"slug":204},{"id":406,"title":407,"slug":408,"content":409,"excerpt":407,"coverImage":410,"status":20,"isPinned":37,"pinnedAt":18,"viewCount":411,"publishedAt":412,"createdAt":412,"updatedAt":413,"category":414,"author":418,"tags":419},"80920598-a452-4357-bf53-842b200560e8","React18入门到精通教程","react18入门到精通教程","\n## JSX实现列表渲染\n> 使用`map()`方法遍历数组，必须添加`key`属性提高性能\n```jsx\nconst songs = [\n  { id: 1, name: \\\"helo1\\\" },\n  { id: 2, name: \\\"helo2\\\" },\n  { id: 3, name: \\\"helo3\\\" },\n];\n\nfunction App() {\n  return (\n    \u003Cdiv>\n      \u003Cul>\n        {songs.map((song) => (\n          \u002F\u002F 关键：添加唯一key标识符（避免使用索引）\n          \u003Cli key={song.id}>  \n            {song.id}-{song.name}\n          \u003C\u002Fli>\n        ))}\n      \u003C\u002Ful>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n**最佳实践：**\n1. 使用`\u003Cul>`包裹列表项\n2. `key`应使用稳定唯一标识（如ID），避免数组索引\n3. 空列表处理：`{songs.length > 0 && ...}` 或 `{songs.map(...) || \u003CEmptyView\u002F>}`\n\n## JSX实现条件渲染\n### 简单逻辑\n```jsx\n\u002F\u002F 三元表达式\n{isLoggedIn ? \u003CDashboard \u002F> : \u003CLoginForm \u002F>}\n\n\u002F\u002F 逻辑短路\n{hasNotification && \u003CNotificationBadge count={5} \u002F>}\n\n\u002F\u002F 空值处理\n{userProfile?.avatar || \u003CDefaultAvatar \u002F>}\n```\n\n### 复杂逻辑\n```jsx\nconst renderContent = (type) => {\n  switch(type) {\n    case 'success': \n      return \u003CSuccessAlert \u002F>;\n    case 'error':\n      return \u003CErrorAlert \u002F>;\n    default:\n      return \u003CInfoAlert \u002F>;\n  }\n}\n\nfunction App() {\n  return (\n    \u003Cdiv className=\\\"container\\\">\n      {renderContent(status)}\n      \n      {\u002F* 另一种模式：立即执行函数 *\u002F}\n      {(() => {\n        if (isLoading) return \u003CSpinner \u002F>;\n        if (isEmpty) return \u003CEmptyState \u002F>;\n        return \u003CDataTable \u002F>;\n      })()}\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n## JSX样式处理\n### 行内样式\n```jsx\n\u002F\u002F 直接对象\n\u003Cdiv style={{ \n  color: 'white', \n  backgroundColor: 'teal',\n  padding: '1rem'\n}}>\n\n\u002F\u002F 样式对象复用\nconst alertStyle = {\n  padding: '15px',\n  borderRadius: '4px',\n  margin: '10px 0'\n};\n\nfunction Alert({ type }) {\n  return (\n    \u003Cdiv style={{\n      ...alertStyle,  \u002F\u002F 扩展运算符合并样式\n      background: type === 'error' ? '#f8d7da' : '#d4edda'\n    }}>\n      {message}\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n### 类名控制（推荐）\n```css\n\u002F* styles.module.css *\u002F\n.card {\n  border: 1px solid #ddd;\n  border-radius: 8px;\n  padding: 20px;\n  box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\n\n.highlight {\n  background-color: #ffffe0;\n}\n```\n\n```jsx\nimport styles from '.\u002Fstyles.module.css';\n\nfunction ProductCard({ featured }) {\n  return (\n    \u003Cdiv className={`${styles.card} ${featured ? styles.highlight : ''}`}>\n      {\u002F* 内容 *\u002F}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## React 18新特性\n### 并发模式（Concurrent Mode）\n```jsx\nimport { startTransition } from 'react';\n\n\u002F\u002F 非紧急状态更新\nfunction handleSearch(query) {\n  startTransition(() => {\n    setSearchQuery(query); \u002F\u002F 可中断的渲染\n  });\n}\n```\n\n### 自动批处理（Automatic Batching）\n```jsx\n\u002F\u002F React 17及之前：两次渲染\n\u002F\u002F React 18：自动批处理，一次渲染\nfunction handleClick() {\n  setCount(c => c + 1);\n  setFlag(f => !f);\n}\n```\n\n## Redux状态管理（现代写法）\n### 安装依赖\n```bash\nnpm install @reduxjs\u002Ftoolkit react-redux\n```\n\n### 创建Store\n```js\n\u002F\u002F store.js\nimport { configureStore, createSlice } from '@reduxjs\u002Ftoolkit';\n\nconst counterSlice = createSlice({\n  name: 'counter',\n  initialState: { value: 0 },\n  reducers: {\n    increment: state => { state.value += 1 },\n    decrement: state => { state.value -= 1 },\n    incrementByAmount: (state, action) => {\n      state.value += action.payload\n    }\n  }\n});\n\n\u002F\u002F 异步操作示例\nexport const fetchUserData = () => async (dispatch) => {\n  const response = await fetch('\u002Fapi\u002Fuser');\n  dispatch(setUser(await response.json()));\n};\n\nexport const store = configureStore({\n  reducer: {\n    counter: counterSlice.reducer,\n    \u002F\u002F 其他reducer...\n  }\n});\n\nexport const { increment, decrement } = counterSlice.actions;\n```\n\n### 组件集成\n```jsx\n\u002F\u002F index.js\nimport { Provider } from 'react-redux';\nimport { store } from '.\u002Fstore';\n\nroot.render(\n  \u003CProvider store={store}>\n    \u003CApp \u002F>\n  \u003C\u002FProvider>\n);\n\n\u002F\u002F Counter.js\nimport { useSelector, useDispatch } from 'react-redux';\nimport { increment, decrement } from '.\u002Fstore';\n\nfunction Counter() {\n  const count = useSelector(state => state.counter.value);\n  const dispatch = useDispatch();\n\n  return (\n    \u003Cdiv>\n      \u003Cbutton onClick={() => dispatch(decrement())}>-\u003C\u002Fbutton>\n      \u003Cspan>{count}\u003C\u002Fspan>\n      \u003Cbutton onClick={() => dispatch(increment())}>+\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## 最佳实践总结\n1. **组件设计**：遵循单一职责原则，拆分智能组件（容器组件）和展示组件\n2. **状态管理**：\n   - 局部状态用`useState`\u002F`useReducer`\n   - 全局共享状态用Redux\n   - 避免过度使用状态提升\n3. **性能优化**：\n   - 使用`React.memo`记忆组件\n   - 使用`useCallback`\u002F`useMemo`避免不必要的重渲染\n   - 虚拟化长列表（react-window）\n4. **Hooks规范**：\n   - 避免在循环\u002F条件中使用Hook\n   - 自定义Hook以`use`前缀命名\n5. **TypeScript集成**：\n   ```tsx\n   interface UserCardProps {\n     name: string;\n     age: number;\n     onSelect: (id: string) => void;\n   }\n   \n   const UserCard: React.FC\u003CUserCardProps> = ({ name, age }) => (\n     \u003Cdiv>{name} ({age})\u003C\u002Fdiv>\n   )\n   ```\n\n下面为您完善教程，增加React路由和生命周期相关内容：\n\n## React路由管理（React Router v6）\n\n### 安装与基础配置\n```bash\nnpm install react-router-dom@6\n```\n\n### 路由基础结构\n```jsx\n\u002F\u002F index.js\nimport { BrowserRouter } from 'react-router-dom';\n\nconst root = ReactDOM.createRoot(document.getElementById('root'));\nroot.render(\n  \u003CBrowserRouter>\n    \u003CApp \u002F>\n  \u003C\u002FBrowserRouter>\n);\n\n\u002F\u002F App.js\nimport { Routes, Route, Link } from 'react-router-dom';\n\nfunction App() {\n  return (\n    \u003Cdiv>\n      \u003Cnav>\n        \u003CLink to=\\\"\u002F\\\">首页\u003C\u002FLink>\n        \u003CLink to=\\\"\u002Fabout\\\">关于\u003C\u002FLink>\n        \u003CLink to=\\\"\u002Fusers\\\">用户列表\u003C\u002FLink>\n      \u003C\u002Fnav>\n      \n      \u003CRoutes>\n        \u003CRoute path=\\\"\u002F\\\" element={\u003CHome \u002F>} \u002F>\n        \u003CRoute path=\\\"\u002Fabout\\\" element={\u003CAbout \u002F>} \u002F>\n        \u003CRoute path=\\\"\u002Fusers\\\" element={\u003CUserList \u002F>} \u002F>\n        \u003CRoute path=\\\"\u002Fusers\u002F:id\\\" element={\u003CUserDetail \u002F>} \u002F>\n        \u003CRoute path=\\\"*\\\" element={\u003CNotFound \u002F>} \u002F>\n      \u003C\u002FRoutes>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 嵌套路由\n```jsx\n\u002F\u002F Dashboard.js\nimport { Outlet } from 'react-router-dom';\n\nfunction Dashboard() {\n  return (\n    \u003Cdiv>\n      \u003Ch2>仪表盘\u003C\u002Fh2>\n      \u003COutlet \u002F> {\u002F* 子路由渲染位置 *\u002F}\n    \u003C\u002Fdiv>\n  );\n}\n\n\u002F\u002F 路由配置\n\u003CRoutes>\n  \u003CRoute path=\\\"\u002Fdashboard\\\" element={\u003CDashboard \u002F>}>\n    \u003CRoute index element={\u003CDashboardHome \u002F>} \u002F>\n    \u003CRoute path=\\\"settings\\\" element={\u003CDashboardSettings \u002F>} \u002F>\n    \u003CRoute path=\\\"analytics\\\" element={\u003CDashboardAnalytics \u002F>} \u002F>\n  \u003C\u002FRoute>\n\u003C\u002FRoutes>\n```\n\n### 编程式导航\n```jsx\nimport { useNavigate, useParams, useLocation } from 'react-router-dom';\n\nfunction UserCard({ user }) {\n  const navigate = useNavigate();\n  \n  return (\n    \u003Cdiv onClick={() => navigate(`\u002Fusers\u002F${user.id}`)}>\n      {user.name}\n    \u003C\u002Fdiv>\n  );\n}\n\nfunction UserDetail() {\n  const { id } = useParams(); \u002F\u002F 获取URL参数\n  const location = useLocation(); \u002F\u002F 获取位置对象\n  \n  return (\n    \u003Cdiv>\n      \u003Ch2>用户ID: {id}\u003C\u002Fh2>\n      \u003Cp>当前路径: {location.pathname}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 路由守卫（认证保护）\n```jsx\nimport { Navigate } from 'react-router-dom';\n\nfunction ProtectedRoute({ children }) {\n  const { isAuthenticated } = useAuth();\n  \n  if (!isAuthenticated) {\n    return \u003CNavigate to=\\\"\u002Flogin\\\" replace \u002F>;\n  }\n  \n  return children;\n}\n\n\u002F\u002F 使用\n\u003CRoute \n  path=\\\"\u002Fdashboard\\\" \n  element={\n    \u003CProtectedRoute>\n      \u003CDashboard \u002F>\n    \u003C\u002FProtectedRoute>\n  } \n\u002F>\n```\n\n## React生命周期\n\n### 类组件生命周期方法\n\n```jsx\nclass LifecycleDemo extends React.Component {\n  \u002F\u002F 1. 初始化阶段\n  constructor(props) {\n    super(props);\n    this.state = { count: 0 };\n    console.log('Constructor');\n  }\n\n  \u002F\u002F 2. 挂载阶段\n  componentDidMount() {\n    console.log('Component did mount');\n    \u002F\u002F 适合进行API调用、事件订阅\n    this.timer = setInterval(() => {\n      this.setState(prev => ({ count: prev.count + 1 }));\n    }, 1000);\n  }\n\n  \u002F\u002F 3. 更新阶段\n  shouldComponentUpdate(nextProps, nextState) {\n    console.log('Should component update?');\n    return nextState.count !== this.state.count;\n  }\n\n  componentDidUpdate(prevProps, prevState) {\n    console.log('Component did update');\n  }\n\n  \u002F\u002F 4. 卸载阶段\n  componentWillUnmount() {\n    console.log('Component will unmount');\n    \u002F\u002F 清理操作\n    clearInterval(this.timer);\n  }\n\n  render() {\n    console.log('Render');\n    return \u003Cdiv>Count: {this.state.count}\u003C\u002Fdiv>;\n  }\n}\n```\n\n### 函数组件生命周期（Hooks实现）\n\n```jsx\nimport { useState, useEffect } from 'react';\n\nfunction FunctionLifecycle() {\n  const [count, setCount] = useState(0);\n  const [data, setData] = useState(null);\n\n  \u002F\u002F 相当于componentDidMount + componentDidUpdate\n  useEffect(() => {\n    console.log('每次渲染后执行');\n  });\n\n  \u002F\u002F 相当于componentDidMount\n  useEffect(() => {\n    console.log('组件挂载后执行');\n    \n    \u002F\u002F 数据获取\n    fetch('\u002Fapi\u002Fdata')\n      .then(res => res.json())\n      .then(setData);\n    \n    \u002F\u002F 相当于componentWillUnmount\n    return () => {\n      console.log('组件卸载前执行');\n    };\n  }, []); \u002F\u002F 空依赖数组\n\n  \u002F\u002F 依赖变化时执行\n  useEffect(() => {\n    console.log('count变化时执行:', count);\n    \n    document.title = `Count: ${count}`;\n    \n    return () => {\n      console.log('清理count效果');\n    };\n  }, [count]); \u002F\u002F count依赖\n\n  return (\n    \u003Cdiv>\n      \u003Cp>Count: {count}\u003C\u002Fp>\n      \u003Cbutton onClick={() => setCount(c => c + 1)}>增加\u003C\u002Fbutton>\n      {data && \u003Cpre>{JSON.stringify(data, null, 2)}\u003C\u002Fpre>}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 生命周期阶段对比\n\n| 阶段 | 类组件方法 | 函数组件Hook |\n|------|------------|--------------|\n| **挂载** | constructor | useState初始化 |\n|       | render      | 函数体执行    |\n|       | componentDidMount | useEffect(() => {}, []) |\n| **更新** | shouldComponentUpdate | React.memo, useMemo |\n|       | render      | 函数体执行    |\n|       | componentDidUpdate | useEffect(() => {}) |\n| **卸载** | componentWillUnmount | useEffect返回函数 |\n| **错误处理** | componentDidCatch | 暂无直接等效，需错误边界组件 |\n\n### 现代React开发建议\n\n1. **优先使用函数组件+Hooks**：\n   - 90%的场景可替代类组件\n   - 更简洁的代码结构\n   - 更好的逻辑复用\n\n2. **关键生命周期替代**：\n   - `componentDidMount` → `useEffect(() => {}, [])`\n   - `componentDidUpdate` → `useEffect(() => {})` 或带依赖的 `useEffect`\n   - `componentWillUnmount` → `useEffect(() => { return cleanup }, [])`\n   - `shouldComponentUpdate` → `React.memo` 或 `useMemo`\n\n3. **数据获取最佳实践**：\n```jsx\nuseEffect(() => {\n  let isMounted = true;\n  \n  const fetchData = async () => {\n    try {\n      const result = await api.getData();\n      if (isMounted) setData(result);\n    } catch (error) {\n      if (isMounted) setError(error);\n    }\n  };\n  \n  fetchData();\n  \n  return () => {\n    isMounted = false; \u002F\u002F 避免组件卸载后设置状态\n  };\n}, []);\n```\n\n## 路由与生命周期整合示例\n\n```jsx\nfunction UserProfile() {\n  const { id } = useParams();\n  const [user, setUser] = useState(null);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    let isActive = true;\n    \n    const fetchUser = async () => {\n      try {\n        setLoading(true);\n        const data = await fetchUserById(id);\n        if (isActive) {\n          setUser(data);\n          setLoading(false);\n        }\n      } catch (error) {\n        if (isActive) {\n          setError(error.message);\n          setLoading(false);\n        }\n      }\n    };\n    \n    fetchUser();\n    \n    return () => {\n      isActive = false; \u002F\u002F 清理效果\n    };\n  }, [id]); \u002F\u002F id变化时重新获取\n\n  if (loading) return \u003CSpinner \u002F>;\n  \n  return (\n    \u003Cdiv>\n      \u003Ch2>{user.name}\u003C\u002Fh2>\n      \u003Cp>Email: {user.email}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## 完整项目结构建议\n\n```\nsrc\u002F\n├── components\u002F      # 通用UI组件\n├── pages\u002F           # 页面组件\n├── layouts\u002F         # 布局组件\n├── hooks\u002F           # 自定义Hooks\n├── store\u002F           # Redux状态\n│   ├── slices\u002F\n│   └── store.js\n├── services\u002F        # API服务\n├── routers\u002F         # 路由配置\n├── utils\u002F           # 工具函数\n├── assets\u002F          # 静态资源\n└── App.js           # 主应用组件\n```\n\n这些新增内容涵盖了React路由的现代用法（v6版本）以及React生命周期的详细解释，包括类组件和函数组件的实现方式对比。同时还提供了路由与生命周期整合的实际示例，帮助开发者理解如何在真实项目中应用这些概念。","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F46e205aa1bd33d1bb7201019fc2fdf43.png",1147,"2023-02-21T02:01:52.000Z","2026-06-26T10:25:15.418Z",{"id":415,"name":416,"slug":417},"f1701085-b8c1-413a-8750-58e7a0a33832","React","react",{"id":28,"name":29,"avatar":18},[420],{"id":241,"name":242,"slug":243}]