普通视图

发现新文章,点击刷新页面。
昨天以前首页

Tauri(十八)——如何开发 Tauri 插件

2025年3月31日 22:02

前言

Tauri 项目也做了一段时间了,也有用别人开发的插件。自己也想知道如何开发 Tauri 插件。

image.png

Tauri 插件介绍

插件可以挂载到 Tauri 的生命周期中暴露需求 webview API 的 Rust 代码,使用 Rust、Kotlin 或 Swift 代码处理命令并能处理更多需求。

Tauri 提供了一个基于 webview 功能的视窗系统,一个在 Rust 进程和 webview 之间发送信息的方式,一个事件系统,以及一些增强开发体验的工具。 设计上 Tauri Core 只包含所有人都需要的功能。相对地,它提供了一种将外部功能添加到 Tauri 应用程序中的机制,被称为插件。

一个 Tauri 插件由一个 Cargo 包和一个可选的 NPM 包(用于提供命令和事件 API 绑定)构成

何时需要开发 Tauri 插件?

  1. 需要访问系统原生 API(文件、硬件、网络底层)
  2. 性能关键型任务(大数据处理、实时计算)
  3. 代码复用与团队协作(跨项目共享功能模块)
  4. 安全敏感场景(加密、认证、许可证管理)

通过 Tauri 插件,开发者可以 兼顾 Web 的开发效率与 Native 的性能能力,构建真正高性能、跨平台的桌面应用。

Tauri 插件核心概念

插件作用:封装可复用的跨平台功能模块(如硬件访问、加密算法),通过 Rust 后端 + 前端 API 暴露能力。

技术栈

  • Rust:核心逻辑实现
  • TypeScript/JavaScript:前端 API 接口
  • WASM(可选):浏览器端直接调用

创建基础插件模板

1. 命名规范

Tauri 插件具有一个前缀(Rust 包使用 tauri-plugin- 前缀,NPM 包使用 @tauri-apps/plugin- 前缀),随后是插件名称。 插件名称由插件配置中的 tauri.conf.json > plugin 和许可列表中的配置确定。

默认情况下,Tauri 会在你的插件 crate 前面加上 tauri-plugin-。这有助于你的插件被 Tauri 社区发现,但不是必须的。初始化新插件项目时,必须提供其名称。生成包名称将为 tauri-plugin-{plugin-name},JavaScript NPM 包名称为 tauri-plugin-{plugin-name}-api(尽管我们建议使用 NPM 范围如果可能的话)。NPM 包的 Tauri 命名约定是 @scope-name/plugin-{plugin-name}

2. 初始化 Rust 插件库

使用引导创建一个新的插件项目,请运行 plugin new。如果不需要相应的 NPM 程序包,请使用 --no-api 命令行标志。

pnpm tauri plugin new [name]

cargo new tauri-plugin-example --lib
cd tauri-plugin-example

这会在 tauri-plugin-[name] 目录下初始化插件,取决于初始化插件时所选择的命令行标志,项目将具有以下结构:

. tauri-plugin-[name]/
├── src/                - Rust 代码
│ ├── commands.rs       - 定义 webview 可用的命令
| ├── desktop.rs        - 桌面实现
| ├── error.rs          - 用于返回 results 的默认的错误类型
│ ├── lib.rs            - 重新导出适当的实现、设置状态……
│ ├── mobile.rs         - 移动端实现
│ └── models.rs         - 公共的结构体
├── permissions/        - 这将托管(生成的)命令的权限文件
├── android             - 安卓库
├── ios                 - Swift 包
├── guest-js            - JavaScript API 绑定的源代码
├── dist-js             - 从 guest-js 转译的资源
├── Cargo.toml          - Cargo 包元数据
└── package.json        - NPM 包元数据

3. 编辑 Cargo.toml

[package]
name = "tauri-plugin-example"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]  # 编译为动态库

[dependencies]
tauri = { version = "2.0", features = ["plugin"] }
serde = { version = "1.0", features = ["derive"] }  # 序列化支持

4. 实现插件结构

// src/lib.rs
use tauri::{
  plugin::{Builder, TauriPlugin},
  Runtime, AppHandle,
};

// 定义插件配置(可选)
#[derive(serde::Deserialize)]
pub struct Config {
  api_key: String,
}

// 核心插件实现
pub fn init<R: Runtime>() -> TauriPlugin<R, Config> {
  Builder::new("example")
    .setup(|app, config| {
      println!("插件初始化,配置密钥: {}", config.api_key);
      Ok(())
    })
    .invoke_handler(|app, invoke| {
      // 处理前端调用
    })
    .build()
}

5. 生命周期事件

插件可以挂载到如下生命周期事件中:

实现前后端通信

1. 定义 Rust 命令

// 添加命令处理
#[tauri::command]
fn greet(name: &str) -> String {
  format!("Hello, {}!", name)
}

// 更新插件初始化
pub fn init<R: Runtime>() -> TauriPlugin<R, Config> {
  Builder::new("example")
    .setup(|app, config| { /* ... */ })
    .invoke_handler(tauri::generate_handler![greet])  // 注册命令
    .build()
}

2. 前端调用插件

// 前端集成 (JavaScript/TypeScript)
import { invoke } from "@tauri-apps/api";

async function callPlugin() {
  const response = await invoke("plugin:example|greet", { name: "World" });
  console.log(response); // 输出 "Hello, World!"
}

高级功能扩展

1. 事件系统

// Rust 端触发事件
app.emit_all("plugin-event", "数据载荷").unwrap();

// 前端监听
import { listen } from "@tauri-apps/api/event";
listen("plugin-event", (event) => {
  console.log("收到事件:", event.payload);
});

2. 状态管理

// 定义共享状态
struct AppState {
  counter: Mutex<i32>,
}

// 注册状态
app.manage(AppState { counter: Mutex::new(0) });

// 在命令中使用状态
#[tauri::command]
fn increment(state: State<AppState>) -> i32 {
  let mut counter = state.counter.lock().unwrap();
  *counter += 1;
  *counter
}

插件集成到 Tauri 应用

1. 本地插件引用

# 主应用 Cargo.toml
[dependencies]
tauri-plugin-example = { path = "../tauri-plugin-example" }

2. 初始化插件

// main.rs
fn main() {
  tauri::Builder::default()
    .plugin(tauri_plugin_example::init())
    .run(tauri::generate_context!())
    .expect("运行失败");
}

发布插件

1. 打包发布到 crates.io

cargo publish --allow-dirty

2. 前端用户安装

npm install tauri-plugin-example

调试技巧

  1. Rust 日志输出

    log::info!("调试信息: {:?}", data);
    

    启动应用时加环境变量:

    RUST_LOG=info cargo tauri dev
    
  2. 前端 DevTools

    .setup(|app| {
      #[cfg(debug_assertions)]
      app.get_window("main").unwrap().open_devtools();
      Ok(())
    })
    

相关插件

  1. github.com/tauri-apps/…
  2. v2.tauri.app/zh-cn/plugi…
  3. v2.tauri.app/zh-cn/devel…

Tauri(十七)—— 安装包和应用包的区别

2025年3月31日 19:40

Tauri 的 安装包应用包 在开发流程和最终产物中扮演不同角色,以下是两者的核心区别及技术细节:

定义与功能

  1. 安装包 (Installation Package)

    • 作用:用户直接运行的安装程序文件(如 .exe.dmg.deb),负责将应用程序部署到目标系统。
    • 特点
      • 包含应用程序二进制文件、依赖资源和安装脚本。
      • 可能集成 WebView 运行时(如 Windows 的 WebView2),具体取决于 tauri.conf.json 中的 webviewInstallMode 配置。
      • 支持多种安装模式:在线下载依赖(downloadBootstrapper)、离线嵌入依赖(offlineInstaller)等。
  2. 应用包 (Application Bundle)

    • 作用:安装完成后生成的应用程序本体,包含运行所需的所有文件。
    • 特点
      • 由前端资源(HTML/CSS/JS)、Rust 二进制文件、系统原生库(如 WebView)组成。
      • 平台差异显著:Windows 生成 .exe,macOS 生成 .app,Linux 生成 .debAppImage
      • 体积极小(通常仅 2-5 MB),因依赖系统 WebView 而非内置浏览器引擎。

技术实现差异

维度 安装包 应用包
核心内容 安装程序 + 可选依赖 可执行文件 + 前端资源 + 原生库
体积 受配置影响(如是否嵌入 WebView) 极小(依赖系统组件)
生成方式 通过 tauri build 命令打包 安装包解压或直接编译生成
用户交互 需要用户执行安装流程 直接运行无需安装(如绿色版 AppImage)
跨平台兼容 需为不同平台生成独立安装包 单平台专用(如 macOS 的 .app

配置与优化

  1. 安装包优化

    • WebView 集成策略
      • embedBootstrapper:增加约 1.8MB,兼容 Windows 7。
      • offlineInstaller:增加约 127MB,支持完全离线安装。
    • 签名与安全:支持代码签名防止篡改,需在 tauri.conf.json 中配置证书。
  2. 应用包优化

    • 资源压缩:通过 Tree Shaking 移除未使用的前端代码。
    • 原生功能调用:通过 Rust 接口访问系统 API(如文件读写、硬件传感器)。

典型场景对比

  • 场景 1:小型工具开发

    • 安装包:选择 downloadBootstrapper 减少体积,依赖用户联网安装 WebView。
    • 应用包:依赖系统 WebView,运行时内存占用低至 80MB(Electron 通常 120MB+)。
  • 场景 2:企业级分发

    • 安装包:使用 offlineInstaller 嵌入所有依赖,适合内网环境。
    • 应用包:通过签名确保来源可信,支持自动更新功能。

开发者注意事项

  1. 跨平台适配

    • Linux 需预装 libwebkit2gtk 等依赖,否则安装包可能失败。
    • Windows 7 需谨慎选择 WebView 安装模式(推荐 embedBootstrapper)。
  2. 调试与测试

    • 开发阶段使用 tauri dev 直接运行应用包,跳过安装流程。
    • 生产环境需测试不同安装模式下的用户兼容性。

小结

Tauri 通过分离 安装包(分发载体)和 应用包(运行实体),实现了轻量化与高性能的平衡。开发者需根据目标平台和用户环境,灵活选择安装包配置策略(如 WebView 嵌入方式),同时利用 Rust 的编译优化提升应用包效率。

Mac 命令行及 Linux 使用指南与示例

2025年3月30日 21:30

通用基础命令(Mac 和 Linux 均适用)

1. 文件与目录操作

命令 功能说明 示例
ls 列出目录内容 ls -al(显示所有文件,包括隐藏文件)
cd 切换目录 cd ~/Documents(进入用户文档目录)
pwd 显示当前目录路径 pwd
cp 复制文件或目录 cp file.txt backup/
mv 移动或重命名文件 mv old.txt new.txt
rm 删除文件或目录 rm -rf dir/(强制递归删除目录)
mkdir 创建目录 mkdir project
touch 创建空文件 touch newfile.txt

2. 文本操作与处理

命令 功能说明 示例
cat 查看文件内容 cat log.txt
grep 文本搜索 grep "error" /var/log/syslog
echo 输出内容或写入文件 echo "Hello" > hello.txt
nano / vim 文本编辑器 vim notes.md
head / tail 查看文件头/尾部内容 tail -f log.txt(实时追踪日志)

3. 系统信息与进程管理

命令 功能说明 示例
ps 查看进程信息 ps aux | grep chrome
top / htop 实时监控系统资源 htop(需安装)
kill 终止进程 kill -9 1234(强制终止 PID 1234 的进程)
df / du 查看磁盘使用情况 df -h(以易读格式显示磁盘空间)
free 查看内存使用(Linux) free -m(显示内存以 MB 为单位)

Mac 特有命令与工具

1. 系统信息与管理

命令/工具 功能说明 示例
sw_vers 查看 macOS 版本 sw_vers -productVersion
system_profiler 查看硬件和系统信息 system_profiler SPHardwareDataType
open 用默认程序打开文件或目录 open .(在 Finder 中打开当前目录)
pbcopy / pbpaste 剪贴板操作 cat file.txt | pbcopy(复制文件内容到剪贴板)

2. 包管理工具:Homebrew

# 安装 Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# 常用操作
brew install wget     # 安装软件
brew update           # 更新包列表
brew upgrade          # 升级所有已安装包
brew remove node      # 卸载软件

Linux 特有命令与工具

1. 包管理(Debian/Ubuntu)

# 更新与安装
sudo apt update        # 更新软件源
sudo apt install nginx # 安装软件
sudo apt remove nginx  # 卸载软件
sudo apt autoremove    # 清理无用依赖

# 查看已安装软件
apt list --installed

2. 系统服务管理(Systemd)

sudo systemctl start nginx    # 启动服务
sudo systemctl stop nginx     # 停止服务
sudo systemctl restart nginx  # 重启服务
sudo systemctl status nginx   # 查看服务状态

3. 网络工具

# 查看 IP 地址(Linux)
ip addr show

# 测试网络连通性
ping google.com

# 查看开放端口
netstat -tuln

Mac 与 Linux 的差异点

1. 命令参数差异

功能 Mac(BSD 风格) Linux(GNU 风格)
查看文件修改时间 ls -lT ls -l --time-style=full-iso
文本替换(sed) sed -i '' 's/old/new/g' file sed -i 's/old/new/g' file
计算 MD5 校验和 md5 file.txt md5sum file.txt

2. 文件系统路径差异

类型 Mac Linux
用户主目录 /Users/username /home/username
临时目录 /private/tmp /tmp
系统日志 /var/log/system.log /var/log/syslog

实用场景示例

1. 批量重命名文件

# Mac(需安装 rename)
brew install rename
rename 's/old/new/' *.txt

# Linux(使用 rename 或 mmv)
sudo apt install rename
rename 'old' 'new' *.txt

2. 查找文件

# 按名称查找
find ~/ -name "*.log"

# 按内容查找
grep -rn "error" /var/log/

3. 压缩与解压

# 压缩目录为 tar.gz
tar -czvf archive.tar.gz /path/to/dir

# 解压 tar.gz
tar -xzvf archive.tar.gz

# 压缩为 zip(Mac/Linux 通用)
zip -r archive.zip /path/to/dir

安全与权限管理

1. 修改文件权限

chmod 755 script.sh     # 设置权限为 rwxr-xr-x
chown user:group file   # 修改文件所有者和组

2. SSH 密钥管理

# 生成密钥对
ssh-keygen -t ed25519

# 将公钥复制到服务器
ssh-copy-id user@remote-server

开发环境配置

1. Python 虚拟环境

# 创建虚拟环境
python -m venv myenv

# 激活环境(Mac/Linux)
source myenv/bin/activate

2. Node.js 版本管理(nvm)

# 安装 nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash

# 安装 Node.js
nvm install 18
nvm use 18

注意事项

  1. 权限警告

    • 慎用 sudorm -rf,避免误删系统文件。
    • Mac 启用 SIP(系统完整性保护),部分系统目录不可修改。
  2. 脚本兼容性

    • 在 Mac 上使用 GNU 工具(如 gsedgrep)可提高与 Linux 的兼容性:
      brew install coreutils findutils gnu-sed
      
  3. 日志与调试

    • 使用 journalctl(Linux)或 log show(Mac)查看系统日志。

通过掌握这些命令和示例,您可以高效操作 Mac 和 Linux 系统!遇到问题时,记得善用 man <命令> 查看手册(如 man ls)。

useEffect 的核心使用技巧与避坑指南

2025年3月27日 00:18

背景

在 React 中,useStateuseEffect 是两个最关键的 Hooks。

据不完全统计,约 90% 的组件使用 useState,而 70% 的组件会用到 useEffect,这充分说明了它们的重要性。

useEffect 是处理副作用(数据请求、DOM 操作、订阅等),是替代生命周期方法(componentDidMountcomponentDidUpdatecomponentWillUnmount),是连接 React 与外部系统的桥梁。

最近我在使用 Trae 进行代码检查时,发现它总是提醒我注意 useEffect 的依赖项问题。因此,如何正确使用 useEffect 以确保依赖项设置正确,成为了我必须重点关注的问题。

避免无限循环

依赖项与状态更新

在 useEffect 中直接修改依赖项的状态(如 setState),导致依赖项变化 → 重新执行副作用 → 再次修改状态,形成循环。

// ❌ 错误示例:每次更新 count 都会触发 effect
useEffect(() => {
  setCount(count + 1);
}, [count]);

// ✅ 正确:函数式更新 + 空依赖
useEffect(() => {
  setCount(prev => prev + 1);
}, []);

// ✅ 正确:条件阻断循环
useEffect(() => {
  if (count < 10) setCount(count + 1);
}, [count]);

// ✅ 正确:根据 count 变化执行副作用,但不会修改 count
useEffect(() => {
  document.title = `Count: ${count}`;
}, [count]); // 依赖 count,但副作用不修改 count

依赖项为引用类型

对象或数组依赖项仅进行浅比较,内容变化但引用未变时,effect 不会触发

const [user, setUser] = useState({ id: 1 });
// ❌ 错误:直接修改对象属性
user.name = 'Alice';
setUser(user); // 引用未变,effect 不执行

// ✅ 创建新引用
setUser({ ...user, name: 'Alice' }); 

正确处理异步操作

避免直接使用 async 函数

useEffect 的回调函数不能是 async,因其返回 Promise 而非清理函数

// ❌ 错误:直接使用 async 函数
useEffect(async () => {
  const data = await fetchData();
}, []);

// ✅ 正确
useEffect(() => {
  const loadData = async () => {
    const data = await fetchData();
    setData(data);
  };
  loadData();
}, []); 

处理加载与错误状态

结合 useState 管理加载状态与错误信息,提升用户体验

// 定义状态变量,用于跟踪加载状态
const [loading, setLoading] = useState(true);
// 定义状态变量,用于存储错误信息
const [error, setError] = useState('');
// 定义状态变量,用于存储获取到的数据
const [data, setData] = useState(null);

useEffect(() => {
  // 创建 AbortController 实例,用于在组件卸载时取消请求
  const abortController = new AbortController();

  const fetchData = async () => {
    try {
      // 开始加载数据,设置加载状态为 true
      setLoading(true);
      // 重置错误状态,确保每次新请求开始时清除之前的错误
      setError(''); 
      // 发送 GET 请求获取数据,并传入 signal 用于可能的请求取消
      const result = await axios.get('/api/data', {
        signal: abortController.signal
      });
      // 请求成功,更新数据状态
      setData(result.data);
    } catch (err) {
      // 检查错误是否由于请求被取消导致,如果不是则设置错误状态
      if (!abortController.signal.aborted) {
        setError(err.message || '请求失败,请重试');
      }
    } finally {
      // 如果请求没有被取消,则更新加载状态为 false
      if (!abortController.signal.aborted) {
        setLoading(false);
      }
    }
  };

  // 调用数据获取函数
  fetchData();

  // 清理函数:组件卸载时取消正在进行的请求
  return () => abortController.abort();
}, []); // 依赖数组为空,表示仅在组件挂载时执行一次

这里顺带提一下另外一篇文章:# async/await 必须使用 try/catch 吗?

副作用清理与性能优化

清理资源

定时器、事件监听等需在组件卸载或依赖项变化时清理,防止内存泄漏或重复注册事件

useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(timer); // ✅ 清理定时器
}, []);

减少不必要执行

空依赖数组:当 useEffect 的依赖项数组为空([])时,它会在组件挂载后的首次渲染时执行一次,模拟类组件中 componentDidMount 的生命周期行为。

useEffect(() => {
  console.log('组件挂载');
}, []); // ✅ 空数组

依赖项精确控制:仅当特定变量变化时触发 effect

useEffect(() => {
  fetchUserData(userId);
}, [userId]); // ✅ 仅 userId 变化时执行

使用 useReducer 解耦复杂逻辑

当状态更新依赖前值或涉及多步骤时,useReducer 比 useState 更合适

const [state, dispatch] = useReducer(reducer, initialState);

useEffect(() => {
  const timer = setInterval(() => {
    dispatch({ type: 'increment' }); // ✅ 稳定的 dispatch
  }, 1000);
  return () => clearInterval(timer);
}, []);

依赖项传递函数

使用 useCallbackuseMemo 缓存函数和数据,减少不必要的更新.

组件内定义的函数每次渲染引用不同,需用 useCallback 缓存

const fetchData = useCallback(async () => {
  const res = await axios.get(`/api/data?id=${id}`);
  setData(res.data);
}, [id]); // ✅ 依赖 id

useEffect(() => {
  fetchData();
}, [fetchData]);

缓存计算结果,仅在依赖项变化时重新计算,需用 useMemo 缓存。

// 使用 useMemo 优化:仅在 list 变化时重新计算过滤结果
const filteredList = useMemo(() => {
    return list.filter(item => item.price > 100);
}, [list]); // 当 list 发生变化时,filteredList 才会重新计算

// 使用 useEffect,当 filteredList 更新时执行副作用操作(例如记录日志或更新状态)
useEffect(() => {
    console.log('Filtered list updated:', filteredList);
// 此处可添加其他需要在 filteredList 更新时执行的逻辑
}, [filteredList]); // 依赖 filteredList

依赖项过多

有时可能会遇到依赖项列表过长的情况,这时需要仔细思考哪些变量真正需要作为依赖。对那些频繁变动但实际上不影响 effect 内逻辑的值,可以考虑通过 useRef 存储。

const [count, setCount] = useState(0);
// 使用 useRef 存储最新的 count 值
const countRef = useRef(count);

// 每次 count 变化时更新 ref 的值
useEffect(() => {
    countRef.current = count;
}, [count]);

// 设置一个 effect,用于定时输出最新的 count 值
// 注意:这里我们不直接依赖 count,而是通过 countRef.current 获取最新值,
// 这样依赖项列表就不会因为 count 频繁变化而过长或导致 effect 重新执行
useEffect(() => {
    const timer = setInterval(() => {
      console.log('当前 count 值:', countRef.current);
    }, 1000);
    return () => clearInterval(timer);
}, []); // 依赖项为空,effect 只在组件挂载时执行一次

分离副作用

如果一个 effect 内部处理的逻辑过于复杂,可以考虑将逻辑拆分成多个 effect,每个 effect 只关注一个责任,便于维护和调试。

const [userData, setUserData] = useState(null);
const [windowWidth, setWindowWidth] = useState(window.innerWidth);

// Effect 1:当 userId 变化时,从 API 获取用户数据
useEffect(() => {
    async function fetchUserData() {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        const data = await response.json();
        setUserData(data);
      } catch (error) {
        console.error('获取用户数据失败:', error);
      }
    }

    if (userId) {
      fetchUserData();
    }
}, [userId]);

// Effect 2:监听窗口尺寸变化,更新窗口宽度
useEffect(() => {
    function handleResize() {
      setWindowWidth(window.innerWidth);
    }

    window.addEventListener('resize', handleResize);
    // 清理函数:组件卸载时移除事件监听
    return () => {
      window.removeEventListener('resize', handleResize);
    };
}, []); // 空依赖数组确保只在挂载和卸载时执行

使用自定义 Hook

将复杂的 effect 逻辑封装成自定义 Hook,可以使组件代码更加清晰,同时也方便复用和测试。

这部分就不提供示例代码了哈。主要思路和目的:

  • 封装复杂逻辑:将部分集中功能封装在自定义 Hook 中,使得主组件更加专注于 UI 展示。
  • 复用性:同样的 Hook 可在其他需要这部分功能逻辑的组件中复用,避免代码重复。
  • 测试方便:自定义 Hook 独立封装后,可以单独编写测试用例,对其异步逻辑和状态管理进行验证。

执行顺序

父子组件 effect 顺序

子组件的 useEffect 先于父组件执行。

// 父组件
useEffect(() => console.log('父 effect'));
// 子组件
useEffect(() => console.log('子 effect'));
// 输出顺序:子 effect → 父 effect

调试

react-devtools

react.dev/learn/react…

npm install -g react-devtools

react-devtools

然后通过将以下 <script> 标签添加到您网站 <head> 的开头来连接您的网站:

<html>  
    <head>  
        <script src="http://localhost:8097"></script>

image.png

这会启动一个独立窗口,你可以在浏览器中打开你的 React 应用,然后在 DevTools 中查看组件树、Props、State 以及 Hooks 的状态。

  • 在 React DevTools 中选中某个组件。
  • 查看组件的 Hooks 部分,你可以直观地看到每个 Hook 的当前值。
  • 当组件重新渲染且依赖项发生变化时,你可以观察到对应 Hook 的状态更新情况,从而帮助调试依赖项是否正确设置。

自定义 Hook 打印依赖项变化

import React, { useState, useEffect, useRef } from 'react';

/**
 * useTraceUpdate 用于打印 props 的变化情况,帮助调试依赖项
 * @param {object} props - 组件的 props 对象
 */
function useTraceUpdate(props) {
  const prevProps = useRef(props);

  useEffect(() => {
    // 使用 Object.entries 遍历所有属性
    const changedProps = Object.entries(props).reduce((acc, [key, value]) => {
      if (prevProps.current[key] !== value) {
        acc[key] = { from: prevProps.current[key], to: value };
      }
      return acc;
    }, {});

    if (Object.keys(changedProps).length > 0) {
      console.log('依赖项变化:', changedProps);
    }

    // 更新 prevProps 为最新的 props
    prevProps.current = props;
  });
}

// 示例组件:使用自定义 Hook 跟踪 props 变化
function ExampleComponent(props) {
  // 打印 props 的变化情况
  useTraceUpdate(props);

  return (
    <div>
      <h2>示例组件</h2>
      <p>当前 prop 值:{JSON.stringify(props)}</p>
    </div>
  );
}

export default ExampleComponent;

eslint-plugin-react-hooks

www.npmjs.com/package/esl…

建议安装 eslint-plugin-react-hooks 来帮助检查依赖是否正确,会给出提示。

小结

正确使用 useEffect 的关键在于深入理解其执行机制、精心配置依赖数组,以及合理管理清理函数。结合工具和最佳实践,规避常见陷阱,能够显著提升代码的健壮性和可维护性。

希望这份指南能为你高效、正确地使用 useEffect 提供有力支持!

❌
❌