MoreRSS

site iconWuKaiPeng | 吴楷鹏修改

关注程序员生活、健康和成长;热衷探索各种工具,提高效能;兴趣是英语、篮球和读书。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

WuKaiPeng | 吴楷鹏的 RSS 预览

内部分享:开发利器 React Query

2025-11-05 08:00:00

背景

@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?

2025-09-09 08:00:00

最近,在很多依赖库的类型定义文件中,经常能看到了一个陌生的朋友: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 呢?

2025-07-03 08:00:00

哈喽,我是楷鹏。

今天想要分享 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 提升用户体验

2025-06-09 08:00:00

本期已录制 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 [email protected]: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 集成路由跳转显式进度条的封装。

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

感谢观看!👏

谁分得清 Next.js、Nest.js、Nuxt.js 啊

2025-05-31 08:00:00

作为一个前端 er,工作或者学习中,至少会遇到这么一次,需要区分 Next.jsNest.jsNuxt.js 的场景。

我最近就遇到这么一次。

公司有一位新入职的同事,吃饭聊天的时候,听他说之前做过 Next.js 的项目。

由于公司最近的新项目基于 Next.js,我就在想,“太好了,我们的新项目有救了”。

结果,在群聊的时候,他澄清了下,做的是 Nest.js 的项目。

一下子给我立不住了。

作为一个说着普通话的普通程序员,听不清 Next /nekst/ 和 Nest /nest/ 这两个发音,实在是太正常了。

这些框架的起名作者真是聪明。

命名跟批发一样,都是 N__t.js,上一次让我这么犯难的,还是黄金届的“周大福、周六福、周生生、六福珠宝……”

而这场品牌碰瓷,其实主要集中在前端框架爆发的 2016 年左右。

那个时候,前端行业百花齐放,各种框架层出不穷。

在 2016 年 10 月 25 号,Next.js 1.0 发布,首次作为开源项目亮相。

Next.js 基于 React 框架,提供服务端渲染(SSR)和静态站点生成(SSG)功能,以及自动代码拆分、路由系统等特性。

随后的一天,也就是 10 月 26 号,Nuxt.js 发布。

不得不说,Nuxt.js 抄得真快,它基于 Vue.js,整出了另一个 Next.js 翻版。

而 Nest.js 则是在下一年 2017 年的 2 月 26 号发布,它跟 Next.js 和 Nuxt.js 关系就比较远了。

它是纯 Node.js 后端框架,属于是 JavaScript 届的 Spring Boot。

现在这三个框架都发展得很好,除了打铁自身硬之外,是不是更多地得益于“蹭热度”的命名呢?

或许下一次,开发新框架的时候,就叫做 Not.js 吧。

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

2025-05-24 08:00:00

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 [email protected]

项目级 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