Skip to main content

内部分享:开发利器 React Query

· 9 min read

背景

@tanstack/react-query (下称 RQ)在 GitHub 上已经累积了 4w+ 的 ⭐,称得上 React 技术栈中必不可少的工具。

它并不像 Zustand 这些通用的状态管理库,而是针对请求这一场景,做了相关的深度优化和功能定制

RQ 在当前项目已经引入将近两个月,但是使用情况并不多,障碍可能来自于 RQ 的学习曲线和心智认知,所以开展本次 RQ 分享,让团队各位更多地了解 RQ 的能力,进而利用好 RQ,提高开发效率。

RQ 和 Axios 的关系

目前我们的项目中使用 Axios 作为请求器,而 RQ 并不是用来替代 Axios,而是用来增强 Axios 的。

看一下例子:

// 👉 这是用 Axios 去实现的获取站内信未读总数的接口方法
export function getUnreadNotificationCount(): Promise<number> {
return clientApi.get(URL.GET_UNREAD_NOTIFICATION_COUNT, {
fetchOptions: {
experimental_throw_all_errors: true,
experimental_no_toast_when_error: true,
},
});
}

// 👉 使用 RQ 通过 hook 封装的方式,去增强未读总信接口
import { useQuery } from '@tanstack/react-query';
export const useUnreadNotificationCount = () => {
return useQuery({
queryKey: ['notification', 'unread-count'],
// 👉 RQ 最终调用我们提供给接口方法
queryFn: getUnreadNotificationCount,
});
};

所以 RQ 其实是请求框架无关的工具,它可以和 Axios,GraphQL,或者原生 fetch() 方法等结合使用。

功能一:减少样板代码

不使用 RQ

假设我们不使用 RQ,那么正常调用一个接口就是:

import { useState, useEffect } from 'react';

function NotificationBadge() {
// 👉 定义一堆状态
const [count, setCount] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
// 👉 手动控制各个状态
const fetchCount = async () => {
try {
setIsLoading(true);
const count = await getUnreadNotificationCount();
setCount(count);
setError(null);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
};

fetchCount();
}, []);

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading notifications</div>;

return <div>Unread: {count}</div>;
}

如果这个接口在多个地方使用,我们需要封装为一个自定义 hook:

// 👉 创建自定义 hook
import { useState, useEffect } from 'react';

export function useUnreadNotificationCount() {
const [count, setCount] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const fetchCount = async () => {
try {
setIsLoading(true);
const count = await getUnreadNotificationCount();
setCount(count);
setError(null);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
};

fetchCount();
}, []);

return [count, isLoading, error];
}

function NotificationBadge() {
const { count, isLoading, error } = useUnreadNotificationCount();

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading notifications</div>;

return <div>Unread: {count}</div>;
}

使用 RQ

使用 RQ 之后:

// 1. 用 RQ 封装自定义 hook
import { useQuery } from '@tanstack/react-query';
export const useUnreadNotificationCount = () => {
return useQuery({
queryKey: ['notification', 'unread-count'],
queryFn: getUnreadNotificationCount,
});
};

// 2. 引用该 hook
function NotificationBadge() {
const { data: count, isLoading, error } = useUnreadNotificationCount();

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading notifications</div>;

return <div>Unread: {count}</div>;
}

RQ 相当于语法糖,简化了自定义 hook 的封装,可以大大减少我们写样板代码。

功能二:减少重复请求

在我们的通知页中,中间菜单和右侧内容都有标题【点赞】以及未读数【3】,这里的未读数都是通过接口获取:

不使用 RQ

那么两个地方引用了相同的数据,没有 RQ 的情况下,处理方式有:

状态提升。把请求接口提升到他们的共同的父组件中。

  • 优点: 简单直接,单一数据源。
  • 缺点: 父组件需要知道子组件的数据需求
function NotificationPage() {
const [unreadCount, setUnreadCount] = useState(0);
const [isLoading, setIsLoading] = useState(true);

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

const fetchUnreadCount = async () => {
try {
setIsLoading(true);
const count = await getUnreadNotificationCount();
setUnreadCount(count);
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
};

return (
<div>
<MiddleMenu unreadCount={unreadCount} />
<RightContent unreadCount={unreadCount} onRefresh={fetchUnreadCount} />
</div>
);
}

Context API:使用 <Context> 组件,跨层级共享数据。

  • 优点: 避免 Props 穿透(props drilling),组件解耦。
  • 缺点: 任何 context 值变化会导致所有消费者重新渲染,需要额外的 Provider 包裹
const NotificationContext = createContext();

function NotificationProvider({ children }) {
const [unreadCount, setUnreadCount] = useState(0);
const [isLoading, setIsLoading] = useState(true);

const fetchUnreadCount = async () => {
try {
setIsLoading(true);
const count = await getUnreadNotificationCount();
setUnreadCount(count);
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
};

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

return (
<NotificationContext.Provider value={{ unreadCount, isLoading, refetch: fetchUnreadCount }}>
{children}
</NotificationContext.Provider>
);
}

// 使用
function MiddleMenu() {
const { unreadCount, isLoading } = useContext(NotificationContext);
return <div>未读: {unreadCount}</div>;
}

function RightContent() {
const { unreadCount, refetch } = useContext(NotificationContext);
return <div>未读: {unreadCount}</div>;
}

封装 Hook:把请求封装到 hook 中

  • 优点:共享请求状态
  • 缺点:每个组件都会重复请求

使用 RQ

使用 RQ 之后:

function useUnreadNotificationCount() {
return useQuery({
queryKey: ['notificationCount'],
queryFn: getUnreadNotificationCount,
});
}

function MiddleMenu() {
const { data: unreadCount, refresh } = useUnreadNotificationCount();
return <div>未读: {unreadCount}</div>;
}

function RightContent() {
const { data: unreadCount } = useUnreadNotificationCount();
return <div>未读: {unreadCount}</div>;
}

优点:自动去重: 两个组件同时挂载只发 1 次请求(queryKey 相同)
无需 Provider: 不需要包裹父组件
组件独立: 每个组件独立使用,不依赖父组件传递

这里提到"两个组件同时挂载只会发起 1 次请求",其原理就是 RQ 会缓存请求的数据。RQ 的很多功能都是基于缓存来操作。

功能三:重刷新

还是通知的例子,当用户打开"点赞"页面,在这里查看消息的时候,【未读数】需要动态更新:

不使用 RQ

没有 RQ 的情况下,我们需要对这么多个组件做数据联动,方案还是状态提升、Context,当然,还可以通过事件总线(EventEmitter)的方式:

// 定义一个事件总线通用方法:eventBus.js
class EventBus {
constructor() {
this.events = {};
}

on(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
}

emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data));
}
}

off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
}
}

export const eventBus = new EventBus();


// 对于左侧菜单
function LeftMenu() {
const [totalUnreadCount, setTotalUnreadCount] = useState(0);

useEffect(() => {
const fetchCount = async () => {
const count = await getUnreadTotalNotificationCount();
setTotalUnreadCount(count);
};

fetchCount();

// ⚠️ 监听刷新事件
const handleRefresh = () => fetchCount();
eventBus.on('notification:read', handleRefresh);

return () => {
eventBus.off('notification:read', handleRefresh);
};
}, []);

return <div>通知 ({totalUnreadCount})</div>;
}

// 对于中间的菜单
function MiddleMenu() {
const [unreadCount, setUnreadCount] = useState(0);

useEffect(() => {
const fetchCount = async () => {
const count = await getUnreadNotificationCount();
setUnreadCount(count);
};

fetchCount();

// ⚠️ 监听刷新事件
const handleRefresh = () => fetchCount();
eventBus.on('notification:read', handleRefresh);

return () => {
eventBus.off('notification:read', handleRefresh);
};
}, []);

return <div>点赞 ({unreadCount})</div>;
}

// 对于右侧内容
function RightContent() {
const handleMarkAsRead = async (id) => {
await markNotificationAsRead(id);

// ⚠️ 触发刷新事件
eventBus.emit('notification:read');
};

return (
<button onClick={() => handleMarkAsRead(123)}>
标记已读
</button>
);
}

使用 RQ

使用 RQ,利用它的 invalidateQueries() 方法可以实现同样的效果:

// 对于左侧菜单
function LeftMenu() {
const { data: unreadTotalCount, refresh } = useQuery({
queryKey: ['totalNotificationCount'],
queryFn: getTotalUnreadNotificationCount,
staleTime: Infinite,
});

return <div>点赞 ({unreadCount})</div>;
}


// 对于中间的菜单
function MiddleMenu() {
const { data: unreadCount } = useQuery({
queryKey: ['notificationCount'],
queryFn: getUnreadNotificationCount,
});

return <div>点赞 ({unreadCount})</div>;
}

// 对于右侧内容
function RightContent() {
const queryClient = useQueryClient();
const { data: unreadCount } = useQuery({
queryKey: ['notificationCount'],
queryFn: getUnreadNotificationCount,
});

const { data: notifications } = useQuery({
queryKey: ['notifications', 'print-and-collect'],
queryFn: () => getNotifications('print-and-collect'),
});

const markAsReadMutation = useMutation({
mutationFn: markNotificationAsRead,
onSuccess: () => {
// ✅ 自动让所有相关查询失效并重新获取
queryClient.invalidateQueries({ queryKey: ['totalNotificationCount'] });
queryClient.invalidateQueries({ queryKey: ['notificationCount'] });
queryClient.invalidateQueries({ queryKey: ['notifications'] });
},
});

return (
<div>
<h2>点赞 (未读: {unreadCount})</h2>
{notifications?.map(n => (
<div key={n.id}>
{n.content}
{!n.isRead && (
<button onClick={() => markAsReadMutation.mutate(n.id)}>
标记已读
</button>
)}
</div>
))}
</div>
);
}

优势: ✅ 零配置自动同步: invalidateQueries 一个方法搞定
组件完全解耦: <RightMenu><MiddleMenu><RightContent> 互不依赖
数据一致性: 始终从服务器获取最新数据
无需手动管理: 不需要回调、Context、事件总线
乐观更新: 可以先更新 UI,再同步服务器

RQ 底层也是使用发布/订阅(pub/sub)设计模式。

功能四:乐观更新

什么是乐观更新?

乐观更新对应的是悲观更新,悲观更新也就是比较传统的做法——先请求 API,再更新 UI。
乐观更新是先更新 UI,再请求 API

✅ 适合场景❌ 不适合场景
点赞/收藏(高概率成功)支付、转账等关键操作
标记已读/未读复杂的业务逻辑(服务器可能拒绝)
切换开关状态数据结构复杂,难以预测结果
简单的增删改操作需要服务器计算的数据(如库存扣减)
用户期望即时反馈的操作

了解了乐观更新,我们来引用它去优化上一节提到的内容,先更新未读数的 UI,再请求 API:

const markAsReadMutation = useMutation({
mutationFn: markNotificationAsRead,

// ✅ 乐观更新:立即更新 UI,不等待服务器响应
onMutate: async (notificationId) => {
// 取消正在进行的查询
await queryClient.cancelQueries({ queryKey: ['notificationCount'] });

// 保存之前的值(用于回滚)
const previousCount = queryClient.getQueryData(['notificationCount']);

// 立即更新未读数
queryClient.setQueryData(['notificationCount'], (old) => old - 1);

return { previousCount };
},

// ❌ 如果失败,回滚
onError: (err, variables, context) => {
queryClient.setQueryData(['notificationCount'], context.previousCount);
},

// ✅ 无论成功失败,最终都重新获取确保数据一致
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['notificationCount'] });
},
});

DevTools

RQ 提供了一个 Devtools,可以作为调试使用,可以提升我们的开发效率。

可以在项目本地开发环境下集成 Devtools,在右下角这个浮动按钮:

TODO

对于 Devtools 来说,比较好用的主要有 Actions 和 Data Explorer,可以手动查看相关数据,也可以触发 RQ 的相关方法:

TODO

深入学习

  1. 中文文档
    目前官方文档只有英文,目前有挺多的相关中文文档镜像,比如:
    https://tanstack.com.cn/query/latest/docs/framework/react/overview
    也可以自行 google "tanstack 中文文档"

  2. 沉浸式翻译
    一款 web 插件,一键把英文翻译成中文,原汁原味,适合 RQ 原文档学习。
    https://chromewebstore.google.com/detail/bpoadfkcbjbfhfodiogcnhhhpibjhbnh?utm_source=item-share-cb

  3. Zread
    如果想进一步了解 RQ 的源码或者功能,可以使用 Zread,它已经索引了 RQ 的 GitHub 仓库,并且提供 AI 问答,目前免费: https://zread.ai/TanStack/query

扩展:服务端组件中如何使用?

留一个问题

TypeScript 为什么要增加一个 satisfies?

· 4 min read

最近,在很多依赖库的类型定义文件中,经常能看到了一个陌生的朋友:satisfies

相信很多人都和我一样,看完 TypeScript 的相关文档,对这个关键字还是一头浆糊。

satisfies 关键字是 TypeScript 4.9 版本引入的,用于类型断言

先看一下连接数据库的例子:

type Connection = {}

declare function createConnection(
host: string,
port: string,
reconnect: boolean,
poolSize: number,
): Connection;

这里,我们声明了一个函数 createConnection,它接收四个参数,返回一个 Connection 类型。

接着:

type Config = {
host: string;
port: string | number;
tryReconnect: boolean | (() => boolean);
poolSize?: number;
}

我们又声明了一个 Config 类型,它包含了四个属性:hostporttryReconnectpoolSize

接下来:

const config: Config = {
host: "localhost",
port: 3000,
tryReconnect: () => true,
}

我们声明了一个 config 变量,它包含这三个属性的值:hostporttryReconnect

OK,现在我们来调用 createConnection 函数,并传入 config 参数:

function main() {
const { host, port, tryReconnect, poolSize } = config;
const connection = createConnection(host, port, tryReconnect, poolSize);
}

问题出现了:

port 类型错误

这里 port 的类型是 string | number,而 createConnection 函数的参数类型是 string,所以会报错。

为了解决类型定义问题,我们需要加上类型断言的逻辑代码:

function main() {
let { host, port, tryReconnect, poolSize } = config;

if (typeof port === "number") {
port = port.toString();
}

const connection = createConnection(host, port, tryReconnect, poolSize);
}

port 类型正确了,但 tryReconnect 类型错误了:

tryReconnect 类型错误

我们一次性将这些类型修复:

function main() {
let { host, port, tryReconnect, poolSize } = config;

if (typeof port === "number") {
port = port.toString();
}
if (typeof tryReconnect === "function") {
tryReconnect = tryReconnect();
}
if (typeof poolSize === "undefined") {
poolSize = 10;
}

const connection = createConnection(host, port, tryReconnect, poolSize);
}

porttryReconnectpoolSize 都进行了类型断言,问题解决了。

但是,这样写起来很麻烦,有没有更简单的方法呢?

一种方式是,去掉 config 的类型定义,放飞自我,让它自动被推断:

const config = {
host: "localhost",
port: 3000,
tryReconnect: () => true,
}

这样,我们可以一步到位:

function main() {
let { host, port, tryReconnect } = config;

const connection = createConnection(host, port.toString(), tryReconnect(), 10);
}

但这样放飞类型,会引起另外的错误,比如 config 随便添加一个属性:

const config = {
host: "localhost",
port: 3000,
tryReconnect: () => true,
pool: 10, // 新增了一个属性
}

这样 TypeScript 是一点都不会报错,但却会埋下隐藏炸弹,在代码上线的时候,可能会抓马,为什么 poorSize 不生效?

层层排查,最后才发现原来 poolSize 写错成了 pool

这个时候,satisfies,千呼万唤始出来:

const config = {
host: "localhost",
port: 3000,
tryReconnect: () => true,
pool: 10,
} satisfies Config;

pool 类型错误

不负众望,TypeScript 终于报错,告诉我们 pool 属性不存在。

satisfies 关键字为我们提供了一种两全其美的解决方案:

  1. 保证类型安全:它会检查我们的对象结构是否满足(satisfies)指定的类型(如 Config)。如果你写了多余的属性(如 pool),或者属性类型不匹配,TypeScript 会立刻报错。这避免了“放飞自我”带来的隐患。
  2. 保留原始类型:与使用类型注解 (: Config) 不同,satisfies 不会改变变量被推断出的原始具体类型。变量 configport 属性类型仍然是 numbertryReconnect 属性类型仍然是 () => boolean

总结来说,satisfies 的核心优势在于:在不丢失(泛化)原始推断类型的前提下,对该值进行类型检查。

这使得我们既能获得编辑器对于具体类型的智能提示和类型推断的好处,又能确保这个值的结构符合我们预先定义好的更宽泛的类型约束,从而写出更安全、更灵活的代码。

REFERENCES

如何同时打开多个 Chrome 呢?

· 4 min read

哈喽,我是楷鹏。

今天想要分享 Chrome 的一个小技巧,可以一次性打开多个干净独立的 Chrome,让你的开发更丝滑。

开头做个小调查,你平时开发的时候,会使用哪些浏览器呢?

  • Chrome
  • Firefox
  • Safari
  • 其他

我平时开发的时候,主力就是使用 Chrome。

Chrome 的 DevTools 功能非常强大,满足前端开发调试的绝大数需求。

但是长期来有一个困扰我的问题,就是我的日常使用和开发是耦合在一起的。

比如,我的 Chrome 会装载很多的插件:

Chrome Extensions

这些插件会影响我的开发,因为他们可能在页面中会插入 HTML 或者 CSS 代码,以及会产生很多额外的请求,干扰我的正常开发调试。

比如下面侧边栏的插件 HTML:

Chrome Layer Tab

此时的选择,要么是开启无痕窗口,要么是换另外一个浏览器。

这两种方式都不错,但无痕窗口还是使用同一个 Chrome 实例,并且重新打开无痕窗口,所有的状态都会被清空。

另外一种方式是换另外一个浏览器,我曾经尝试过,但是后来又放弃了,换一个浏览器就相当于换一种全新的开发环境,需要重新适应界面、操作习惯等等,真的很别扭。

最近学到了另一种新方式,就是可以通过使用不同的用户数据目录,来创建不同的 Chrome 实例。

运行命令:

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir="/tmp/chrome_user_dir_1"

你就可以创建一个全新的 Chrome 实例,并且这个实例的配置、插件、历史记录等都是独立的。

Create Chrome Instance

甚至在 Dock 栏,你还可以看到两个 Chrome 图标:

Chrome Instances in Dock

这个新创建的 Chrome 实例,完全可以看作是一个全新的 Chrome 浏览器。

你可以修改主题,来和其他 Chrome 实例区分开来:

Modify Theme

或者登录不同的账号等等操作,这是完全属于你的第二 Chrome。

通过运行这条命令,理论上你可以创建无限个 Chrome 实例,只需要修改 --user-data-dir 参数即可,比如:

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir="/tmp/chrome_user_dir_2"
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir="/tmp/chrome_user_dir_3"
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir="/tmp/chrome_user_dir_4"
......

不过平时实际使用的时候,我一般使用两个 Chrome 实例,来回切换,一个用于网站浏览,一个用于开发调试。

在开发调试的时候,每次打开项目再打开新的 Chrome 会有一点点烦躁,所以你可以考虑将这条命令写入到你的前端项目 package.json 的脚本中:

  "scripts": {
"dev": "next dev --turbopack",
"open-chrome": "/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --args --user-data-dir=/tmp/ChromeNewProfile http://localhost:3000",
"dev:chrome": "npm run open-chrome && npm run dev"
},

这样你就可以通过 npm run dev:chrome 来打开 Chrome 实例,并且自动运行 next dev 命令。

Windows PowerShell 用户可以使用:

 "scripts": {
"dev": "next dev --turbopack",
"open-chrome": "powershell -Command \"Start-Process 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' -ArgumentList '--user-data-dir=D:\\temp\\ChromeNewProfile', 'http://localhost:3000'\"",
"dev:chrome": "npm run open-chrome && npm run dev"
},

如果你希望打开 Chrome 实例的时候,同时打开 localhost:3000 页面来看到页面效果,可以在命令后面直接添加 http://localhost:3000

{
"scripts": {
"dev": "next dev",
"dev:chrome": "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir=\"/tmp/chrome_user_dir_1\" http://localhost:3000 && npm run dev"
}
}

好了,这就是本期的全部内容,如果对你有帮助,欢迎点赞、收藏、转发。

我是楷鹏,我们下期再见。

Next.js 路由跳转显示进度条:使用 BProgress 提升用户体验

· 5 min read
吴楷鹏
前端开发工程师

本期已录制 B 站视频 👉 【Next.js】路由跳转显示进度条

哈喽,我是楷鹏。

先来看一个反面教材。

在 Dify.ai 中,当点击跳转页面之后,会有一段需要等待的时间,然后才会跳转页面。

Dify.ai 页面跳转时缺少进度反馈

然而,中间这段时间我并不知道是否跳转成功了,所以我会多点了几下,直到跳转。

这种体验很不好 👎

解决方案很简单,我们来看一下 GitHub 的跳转交互。

GitHub 页面跳转时的进度条效果

可以看到,GitHub 在跳转期间,会显示一个进度条,清晰地告诉用户——"我正在跳转,请稍等"。

那么在 Next.js 中,如何实现这个效果呢?

我们可以借助 BProgress 这个库来实现。

BProgress 官网首页展示

BProgress 是一个轻量级的进度条组件库,支持 Next.js 15+,同时也支持 Remix、Vue 等其他框架。

对于 BProgress 的使用,我做了一个 demo 项目 nextjs-progress-bar-demo,我们可以把这个项目先 clone 下来:

git clone git@github.com:wukaipeng-dev/nextjs-progress-bar-demo.git

然后进入项目目录:

cd nextjs-progress-bar-demo

先安装依赖:

npm install @bprogress/next

启动项目:

npm run dev

Next.js 进度条演示项目界面

可以看到,这是一个简单的 Next.js 项目,包含三个页面:首页、登录页、注册页。

main 分支已经配置好了进度条,我们切换到分支 without-progress-bar-demo

git checkout without-progress-bar-demo

当前分支下,我们没有配置进度条,所以跳转页面时,不会显示进度条。

接下来我们在根布局 app/layout.tsx 中引入 ProgressProvider

'use client';

import "./globals.css";
import { ProgressProvider } from '@bprogress/next/app';

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<ProgressProvider
height="4px"
color="#4c3aed"
options={{ showSpinner: false }}
shallowRouting
>
{children}
</ProgressProvider>
</body>
</html>
);
}

接下来,我们可以看一下,在首页和登录页、登录页和注册页之间跳转,都会显示一个进度条。

集成 BProgress 后的页面跳转效果

ProgressProvider 的参数如下:

  • height:进度条的高度
  • color:进度条的颜色
  • options:进度条的配置,这里 showSpinner 设置为 false,表示不显示一个动画的加载图标。
  • shallowRouting:是否启用浅层路由,如果开启的话,只改变路由的 query 参数,比如 ?page=1 变成 ?page=2,那么进度条不会重新加载。

但是,当我们登录成功之后,再点击跳转,却不会显示进度条。

使用 router.push 跳转时进度条未显示

这是因为,首页和登录页、登录页和注册页之间,是使用 <Link> 组件进行跳转的。

<Link> 组件实际会渲染成 <a>,BProgress 通过给所有 <a> 组件添加点击事件,来显示进度条。

我们可以看下在 DevTools → Elements → <a> → Event Listeners 中,是否添加了点击事件:

DevTools 中查看 Link 组件的点击事件监听器

但是,当我们登录成功之后,则是使用 router.push 进行跳转的。

BProgress 不会给 router.push 添加点击事件,自然也不会显示进度条。

不用慌,BProgress 为我们提供了 useRouter 方法。

将 Next.js 的 useRouter 替换为 BProgress 提供的 useRouter

// import { useRouter } from 'next/navigation';
import { useRouter } from '@bprogress/next/app';

然后,正常使用即可:

const router = useRouter();

router.push('/');

这时,你可以看到,在登录成功之后,自动跳转首页时,进度条就能正常显示了。

使用 BProgress 的 useRouter 后进度条正常显示

但如果你的项目已经封装过了自己的 useRouter,那么你可以将封装过的 useRouter 作为参数 customRouter 传入,进行二次封装:

import { useRouter } from '@bprogress/next/app';
import { useRouter as useNextIntlRouter } from '@/i18n/navigation';

export default function Home() {
const router = useRouter({
customRouter: useNextIntlRouter,
});

return (
<button
onClick={() =>
router.push('/about', {
startPosition: 0.3,
locale: 'en',
})
}
>
Go to about page
</button>
);
}

最后,让我们回到 app/layout.tsx,这里我们引入了 ProgressProvider,但却把 app/layout 变成了一个客户端组件,我们来把 ProgressProvider 抽离到其他地方,仍然保持 app/layout 是一个服务端组件。

// app/components/ProgressWrapper.tsx
'use client';

import { ProgressProvider } from '@bprogress/next/app';

interface ProgressWrapperProps {
children: React.ReactNode;
}

export function ProgressWrapper({ children }: ProgressWrapperProps) {
return (
<ProgressProvider
height="4px"
color="#0000ff"
options={{ showSpinner: false }}
shallowRouting
>
{children}
</ProgressProvider>
);
}

app/layout.tsx 中,我们引入 ProgressWrapper

import { ProgressWrapper } from './components/ProgressWrapper';

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<ProgressWrapper>
{children}
</ProgressWrapper>
</body>
</html>
);
}

好的,不愧是你,完成了一个 Next.js 集成路由跳转显式进度条的封装。

以上就是本期的全部内容,希望对你有所帮助。

感谢观看!👏

Who Can Tell the Difference Between Next.js, Nest.js, and Nuxt.js?

· 2 min read

As a frontend developer, at least once in your career or studies, you'll encounter a situation where you need to distinguish between Next.js, Nest.js, and Nuxt.js.

I just had one of these moments recently.

A new colleague joined our company, and during lunch chat, I heard him mention he had worked on Next.js projects before.

Since our company's new project is based on Next.js, I thought, "Great! Our new project is saved!"

But later in the group chat, he clarified that he had actually worked on Nest.js projects.

That really threw me off.

As an ordinary developer speaking ordinary Mandarin, it's completely normal not to be able to distinguish between the pronunciations of Next /nekst/ and Nest /nest/.

The authors who named these frameworks were really "clever."

These names are like wholesale products - all following the pattern N__t.js. The last time I was this confused was with the jewelry brands "Chow Tai Fook, Chow Luk Fook, Chow Sang Sang, Luk Fook Jewellery..."

This brand piggybacking actually peaked around 2016 when frontend frameworks were exploding.

That was a time when the frontend industry was blooming with various frameworks emerging constantly.

On October 25, 2016, Next.js 1.0 was released, making its debut as an open-source project.

Next.js is based on the React framework, providing Server-Side Rendering (SSR) and Static Site Generation (SSG) capabilities, along with features like automatic code splitting and routing systems.

Just one day later, on October 26, Nuxt.js was released.

I have to say, Nuxt.js copied really fast - it's based on Vue.js and created another version of Next.js.

Nest.js came later, releasing on February 26, 2017. It's quite different from Next.js and Nuxt.js.

It's a pure Node.js backend framework - essentially the JavaScript world's Spring Boot.

All three frameworks are doing well now. Besides their own technical merits, could their success be partly due to their "trend-riding" names?

Maybe next time when developing a new framework, we should just call it Not.js.

最强 Node.js 版本管理器,比 NVM 还好用

· 3 min read

Volta 介绍

最开始在安装 Node.js 的时候,我们只能通过官网下载安装包,安装指定的一个版本,比如 18.17.1。

但对于不同的项目,我们可能需要使用不同的 Node.js 版本,比如 18.17.1 和 20.17.1。

如果要去切换版本,那就需要卸载旧版本,安装新版本,再切换项目,非常麻烦(痛苦面具)。

于是出现了 Node.js 版本管理器,比如 NVMVolta 等。

它支持安装指定版本的 Node.js,并且可以自由切换版本。

但是,NVM 存在一些问题,for example,无法根据项目自动切换版本,不支持 Windows 平台(当然,有一个非官方支持的野鸡 nvm-windows 可以使用) 等等。

新一代的 Node.js 版本管理器 Volta 解决了这些问题。

它可以根据项目自动切换 Node.js 版本,并且还支持 Mac、Windows、Linux 三大平台。

Volta 基于 Rust 开发,速度更快,活更好。

安装 Volta

根据安装指南,我们在终端中输入以下命令来安装 Volta:

curl -fsSL https://get.volta.sh | bash

安装完成后,打开另一个新终端,输入以下命令来查看当前的 Volta 版本:

volta -v
2.0.2

恭喜你,Volta 安装成功。

接下来,我们就可以使用 Volta 来管理 Node.js 版本了。

在终端中输入以下命令来安装 Node.js:

volta install node

这条命令会安装最新 LTS 版本的 Node.js。

LTS:Long Term Support,长期支持版本。

当然,也可以用艾特符号 @ 安装特定版本的 Node.js,比如:

volta install node@20.17.1

项目级 Node.js 版本管理

打开一个你正在维护的 Node.js 项目,比如“shit-mountain”,找到 package.json 文件,添加以下内容:

{
//...
"volta": {
"node": "20.17.1"
}
}

当你执行 npm i 时,Volta 会寻找 20.17.1 版本的 Node.js。

如果找不到,Volta 会自动安装 20.17.1 版本的 Node.js,然后再执行 npm i

这样就确保了项目中使用的 Node.js 版本为 20.17.1。

Volta 还有其他一些特性,比如 Volta 的各种命令,listuninstall 等等,又比如 Hooks,可以指定下载源,这里就不再展开。

前往 Volta 官网查看更多信息 👉 https://volta.sh

开源项目如何提高吸引力?试试添加 Vercel 一键部署

· 3 min read

在一些成名的 GitHub 开源项目中,会支持 Vercel 一键部署,比如前两年爆火,如今坐拥 78.7k star 的 NextChat

那么 Vercel 是什么呢? 它是一个专为前端开发者设计的现代化部署平台,特别适合用于静态网站和前端应用的构建、预览和发布。

So,如果你的开源项目属于静态网站或者前端应用一类的,可以考虑 README.md 上添加 Vercel 一键部署,为你的开源项目增加吸引力

添加一键部署的方式也很简单,Vercel 提供了一个按钮生成工具deploy-button

按钮生成器会生成 Markdown、HTML 和 URL 三种方式,可以按需取用

提醒一下,这里的交互会有点奇怪,页面下方是表单输入,比如填写 Git 仓库地址之后,上方 Markdown 链接会自动改变,并且没有成功提醒,这里需要适应一下

这里必填的,只有你的 Git 仓库地址

其他的还有像是环境变量、默认项目名称、重定向、Demo、集成等,按需填写,最后将生成好的 Markdown 贴到你的开源项目 README.md 上:

整个流程就完成了,非常简单

对于用户侧来说,当他点击部署按钮之后,就会跳转到 Vercel 网站:

这里需要登录 Vercel,同时 Vercel 会要求授予 Git 仓库读写权限,因为 Vercel 会执行对目标仓库的克隆,再以克隆后的仓库为准进行部署:

填写项目名称,点击创建:

接下来就是等待大功告成:

Congratulations!

这里已经能看到网站运行成功之后的预览截图了,也可以点击「Continue to Dashboard」去到控制台,点击 domain 网址,同样能看到网站已经成功部署:

整体回顾来看,Vercel 的部署服务非常丝滑,我甚至都不需要提供框架信息、运行命令等等

So,觉得 Vercel 一键部署的方式不错,那么考虑为你的项目增加一下吧!

如有需要,可查看本文示例项目参考:https://github.com/wukaipeng-dev/ken

重度使用一年,我是如何把滴答清单用到极致?

· 7 min read

Raycast 的 2024 年度统计出来了,这份统计包含了我这一年来用 Raycast 去打开其他应用的情况,看到滴答清单被打开上千次,排列榜首,还是有点小意外

滴答清单英文名:TickTick

今年是尝试并逐渐重度使用滴答清单的一年,一开始,我只想要找一个任务管理的软件,对比过其他很多 todo 软件,滴答清单的免费版本是我个人认为最良心的,大部分功能都涵盖到了

虽然免费很诱人,但还不足以打动我,真正打动的点还是解决了我长久以来的痛点。

在做任务的时候,我用的是番茄工作法,这种方法指的是在 25 分钟(1 个番茄钟)内专注,然后 5 分钟短休息,累计 4 个番茄钟后进行长休息这样的一个周期性节奏去专注

之前我买了一个桌面计时钟,考研党朋友应该很熟悉,类似于下面这种:

这种计时钟既可以正计时,也可以倒计时,也就是做一个 25 分钟的番茄钟倒计时,就这我用了 2 年多,在家里买一个,在公司也买了一个。

它很方便,物理按键,一按就可以开启番茄钟,但它的优点也正是它的缺点。

物理的限制,我不可能随时随地带着一个计时钟,更麻烦的是,一天结束,我不知道每天花了多少番茄钟,不知道什么时候开启番茄钟,不知道番茄钟期间都做了什么任务。

滴答清单在添加任务之后,可以针对这个任务直接开启番茄钟,也可以在番茄钟上去关联对应的任务,同时能做一些额外的笔记,打开统计页面,能清晰地看到每个番茄钟的开始结束

这基本上解决了我的大痛点,另外一个小痒点就是它的通知功能,日常总会忘记开启番茄钟,滴答清单在电脑的菜单栏,可以显式地看到专注的进度:

如果开启了跨设备同步,那么手机和电脑都会实时同步当前的专注进度:

让我小小惊喜的是,移动端开启专注模式后,会自动进入沉浸模式,其中除了翻页时钟模式,还有下面这款像素模式,UI 做得特别好看:

滴答清单在解决我的 Big Trouble 之后,我开始慢慢上手其他功能。

对于核心功能——任务管理,第一个提的点是对任务的分类,我相信很多人的分类都比较杂乱,今天加一个「读书」的类目,明天加一个「项目管理」的类目,而且类目一旦多了起来就难以管理了

这里我推荐一个非常实用的分类方式,那就是按照「角色」划分

在公司里,角色就是「员工」,在学校里,角色就是「学生」,在跑道上,角色就是「运动员」,在任意的社会关系中,每个人都是不同的角色

在我个人的滴答任务分类中,会有管理员(Admin)、员工(Worker)、开发者(Dev)、Friend(朋友)、读书人(Reader)等等角色

这种划分方法非常稳定,我的任务分类一旦划分后,几乎没有大的改动,它还足够灵活,比如喜当爹了,那就加一个「父亲」角色,买奶粉、换尿布的任务都怼在这个角色上

按照角色划分,几乎囊括了所有的事情,但对于某些角色来说,它需要横向扩展,比如作为员工,需要做项目 a、项目 b 、项目 c 等等,那么可以使用标签,为任务打上对应标签,也就能够把角色下不同的任务类型区分开来:

通过角色划分 + 标签系统,基本可以建立一个有序稳定的分类体系了

第二点是任务处理方面,滴答清单藏了很多小心思,比如可以设置预估任务使用的番茄钟数量:

还比如可以设置任务进度百分比:

按住 Shift 或者 Command/Control 键选中多个任务之后,能够进行批量处理:

移动端长按应用图标添加任务,任务框右下角有个语音转文字功能,可以加速添加任务时间

另外是日程表功能,之前没有相应的使用习惯,最近发现了两点,让我开始觉得日程表非常香。

第一点发现是,可以运用筛选面板去查看目标任务,之前没有使用筛选,看着日历上所有任务都堆在一起,一个头两个大:

现在使用筛选功能,按照清单、标签等筛选,可以轻松地查看日程对应的任务:

第二点发现是原来日程之间是有通用协议的——CalDAV,它是一种日历数据共享和同步的协议,适用于安卓、iPhone、Windowns、macOS 等一切设备,只需要日历源即可在需要的日历地方导入即可同步到日程。

我在滴答清单上通过导入飞书的 CalCAV 配置,即能实现对飞书所有会议、日程的订阅:

还有一个习惯功能,我挖掘出了三种使用方法:

第一种就是最常用的正习惯:

第二种是坏习惯:

这种和正习惯相反,只有在出现这些坏习惯的时候才会做记录,坏习惯记录的场景是有:

  1. 记录一些低频、偶发的坏习惯
  2. 当培养成功出为每日习惯后,不需要再频繁记录,只需要记录某天未做的异常

第三是数据记录

习惯是有自带日历,可以当做一个数据记录,比如减肥的过程,可以记录每天的体重情况:

以上就是个人对滴答清单这一年来的使用,这确实是一款优秀的软件,但不可否认,它也存在一些局限性,比如艾森豪威尔矩阵,仅仅是对任务进行重要性、紧急性做一个简单的二维划分:

但这世界上不可能有完美的软件,有问题就解决问题,2024 年陆陆续续给滴答清单提的 bug 加上 feature request,有将近 30 个:

所以,我这算是编外的测试人员 + 产品经理吗 😆

It's Almost 2025, I Won't Let You Not Know This AI Reading Tool

· 3 min read

Recently, a friend asked me: what software can translate PDF documents into Chinese?

That stumped me for a moment. I usually only use simple Q&A AIs like ChatGPT, Kimi, or Doubao. For actual PDF document translation scenarios, I hadn't encountered that.

In the end, my friend had to pay for it on Xiaohongshu, spending 50 yuan 💰

Because I didn't know how to use AI, I actually missed a money-making opportunity!

Fortunately, I recently discovered an intelligent AI reading assistant called "Baoyue AI".

Testing it with a PDF document, the translation results were quite impressive:

Not only that, clicking to generate a mind map, a clearly structured mind map appears beautifully:

Moreover, Baoyue AI also automatically generated a summary:

Besides PDF documents, it also supports word, ppt, epub, mobi, txt, markdown, jpg/png, webp, wps, caj, and even video and audio files.

To help me read documents easily, Baoyue AI really went the extra mile.

Baoyue has a deep understanding of current AI large language model capabilities.

Current large models are actually not good at generating information - asking them to write articles just results in repetitive content going in circles.

But when it comes to deconstructing and understanding information, they excel.

Baoyue leverages AI's strengths, maximizing the advantages in deconstructing and understanding information.

In terms of specific product implementation, Baoyue does several things:

First, through easy-to-understand "reading scenarios" categorization, letting users quickly match their needs.

Current scenarios include academic papers, contracts, office work, self-media, teaching assistant, books, official documents, technical R&D, etc., meeting most requirements.

For example, recently while learning Coco's "CSS Technology Decryption and Practical Mastery", I used "Technical R&D > Line-by-line Code Explanation".

Line-by-line explanation of all CSS code properties, learning efficiency Max 💪

Second is rich information processing methods - it's not just a chatbot.

For example, using the "Self-media > Tech News Deep Reading" scenario, I had Baoyue analyze this article "Can Zed Beat VS Code?"

Besides the mind map mentioned earlier, there's also an introduction to help users quickly understand the article's core points, as well as rewriting features for secondary creation and more.

Third is adding "AI Search" to let users connect to more related information - external enhancement.

Fourth is having "note-taking capabilities", allowing users to record their divergent thoughts - internal enhancement.

Through this four-in-one approach, closing the entire loop from need identification, information processing, information expansion, to information digestion, perfectly solving reading scenarios.

I sincerely hope that domestic companies developing AI applications can be like Baoyue AI, truly focusing on and perfecting a vertical scenario, improving efficiency and saving time for users, creating real value.

👉 Baoyue AI official website: baoyueai.com

Amazing Uses of Mac Notes App

· 3 min read

Previously when using Windows, the most painful thing was not having an app that could satisfy my need for quick note-taking.

Most apps required opening the note software first, then creating a new note, and finally typing - too many steps and too cumbersome.

After switching to macOS, what surprised me was the built-in Notes app. Just simply move your mouse to the bottom right corner of the screen to create a Quick Note.

Amazing!

This method is called Hot Corners, which can be set in "System Settings » Desktop & Dock » Hot Corners":

The four hot corners can be freely configured:

Besides hot corners, the keyboard shortcut fn(🌐) + Q can also create a Quick Note.

There's another issue - hot corners or shortcuts will open the last edited note by default. If you want to create a new Quick Note every time, you can change the setting here:

Uncheck "Always resume last Quick Note".

Notes supports most frequently used text styles. After selecting text, you can modify styles from the Aa button in the top navigation bar:

It also supports checklists:

The table function is quite basic - just a simple table without advanced features like merging or freezing.

There are also images and links, which I won't elaborate on here.

Notes supports folder categorization by default, and also supports tag categorization. Just use the hashtag (#) followed by text in your note, and Mac will generate the corresponding tag list:

Previously when browsing web pages, I really wanted to highlight certain content and make some extended notes. I installed the Weava Highlighter plugin, but it wasn't user-friendly - Weava would pop up whenever I selected text, which was very annoying.

Surprisingly, Mac Notes natively supports this feature!

In Safari, you can select content you want to save, right-click "Add to Quick Note":

After creating the Quick Note, the selected text will be highlighted in Safari:

In the latest macOS 15 update, Notes now supports audio recording:

It also supports real-time speech-to-text, but currently it only supports English (again).

Cook's native language is English, mine is speechless 😅

Additionally, there are new highlight colors - purple, pink, orange, mint, and blue. Have to say, these colors are quite nice looking.

The most useful feature has to be the math function.

Just type formulas like (27/3)^2= or 47*96=, and Notes will automatically calculate the result:

It also supports custom variables:

Overall, Mac Notes is a decent note-taking app. While it lacks advanced features like Notion's document structure and block editing, it has native support and can meet the needs for quick note-taking and basic editing.