跳到主要内容

内部分享:开发利器 React Query

· 阅读需 9 分钟

背景

@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 分钟

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

哈喽,我是楷鹏。

今天想要分享 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 分钟
吴楷鹏
前端开发工程师

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

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

感谢观看!👏

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

· 阅读需 2 分钟

作为一个前端 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 还好用

· 阅读需 3 分钟

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 分钟

在一些成名的 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 分钟

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 个:

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

Mac 备忘录妙用

· 阅读需 3 分钟

之前使用 Windows 的过程中,最痛苦的事是没有一款可以满足我快速进行记录的应用

基本都得先打开该笔记软件,然后创建新笔记,最后才能输入,这么多步骤太麻烦了

在切换到 MacOS 之后,让我惊喜的就是自带的备忘录,只需要简单地把鼠标移动到屏幕右下角,就可以创建一篇快速备忘录

Amazing!

这种方式叫做触发角,触发角可以在「系统设置 » 桌面与程序坞 » 触发角」设置: 四个触发角分别可以自由设置:

除了触发角,快捷键【 fn(🌐) + Q】同样能创建一篇快速备忘录

还有一个问题是,触发角 or 快捷键默认会打开上一次编辑的备忘录,如果想要每次都创建一篇新的快速备忘录的话,可以在设置这里:

把「始终回到上个快速备忘录」取消勾选

备忘录支持大部分高频的文本样式,选取文本后,在头部导航栏 Aa 这里做修改样式:

也能支持 check 清单:

表格功能比较弱鸡,就一个简单的表格,什么合并、冻结等高级功能都没有

另外还有图片、链接,这里就不再赘述。

备忘录默认支持文件夹分类,另外还支持标签分类,只需要在备忘录中使用井号(#)加上对应文字,Mac 即会生成对应的标签清单:

之前在浏览网页的时候,特别想高亮某些内容,同时做一些拓展记录,安装过插件 Weava Highlighter,但是不好用,每次只要选中文字就 Weava 就会弹出,特别烦人。

没想到 Mac 备忘录居然原生支持这个功能

在 Safari 中,可以选择想要收藏的内容,右键「添加到快速备忘录」

创建快速备忘录之后,选中的这句话在 Safari 中会被高亮:

在最新的 MacOS 15 中更新中,备忘录新支持了录音功能

并且还支持实时的语言转文本,但目前又又又又仅支持英语

库克的母语是英语,我的母语是无语 😅

另外,还新增了高亮颜色,分别有紫色、粉色、橙色、薄荷色和蓝色,不得不说,这几种颜色确实还挺好看的

最有用的功能当属于这个数学功能

直接输入像是 (27/3)^2= 或者 47*96= 算式,备忘录会自动计算结果:

还支持自定义变量:

总体来说,Mac 的备忘录还算是一个不错的笔记软件,虽然缺乏像 Notion 的文档目录结构和块编辑的一些先进笔记能力,但它有着原生的支持,能够满足快速记录和基础编辑的需求

前端开发实用的面试备考分享

· 阅读需 11 分钟

最近一句话很火「好的工作就像 HIV,只通过母婴,血液和性传播

排除 HIV 患者,对于普通家庭的程序员朋友来说,面试是必经之路

一般来说,面试会有这么四个困难:

  1. 技术太菜:基础不牢固?算法不熟练?
  2. 项目不足:没有亮眼的项目?都是失败项目?
  3. 话少嘴笨:不会表达?说话没有逻辑?
  4. 心态问题:简历石投大海着急?一面挂了自闭?裸辞焦虑?

这些其实都是表症,真正的病根是:

对面试没有系统性的认知和准备

接下来我将从几方面给大家分享一下个人的面试备考思路

一、对自己要有清晰的认知

很多人心比天高,但往往命比纸薄

对自己没有清晰的认知和规划,进入就业市场一开始野心勃勃,但很快就被现实来几个大逼兜

面试一开始,要厘清自己的现有资源,比如对于我:

  • 学历方面,双非本科
  • 技术方面,基础一般,算法不会
  • 工作方面,没有入职过大厂
  • 项目方面,没有中大型的项目经验

这些都是我在面试中的劣势,那我的优势是什么:

  • 年轻白菜价,三年工作经验,有一定的项目经验,对企业来说性价比高
  • 有些许写作成就,同时维护一个个人的技术博客网站
  • 心态 open,愿意学习,愿意尝试

综合分析自己的现有资源,我得出的结论:

  • 有一定市场竞争力,但还达不到大厂的门槛,可以冲一冲中厂
  • 次一点的结果是小厂或者创业公司
  • 最差的就是大厂的子公司、外包公司或者华为 OD

二、做好准备,打一个持久战

面试就是打仗,而且是一个持久战

首先就是「钱」这方面,尤其裸辞,是否有足够的储蓄支撑自己的生活

不要想着一两个月就能找到满意的工作,这是不现实的

尤其是现有大环境的情况下,至少要规划半年以上的时间的储蓄

该消费降级的还是要降级,该省的还是要省

心态

其次是「心态」,大部分人刚开始找工作自信满满

但随着时间推移,投简历没有回复,面试没有通过,家人朋友的关心和询问也会让你感到压力

这时候很容易产生焦虑,甚至自闭,我也有过这样的经历

我的做法是「记录」下来,接纳自己的情绪,给自己积极的心理暗示

罗翔老师曾经说过:对于不可控的事情,我们要保持乐观;对于可控的事情,我们要保持谨慎

体力

最后是「体力」,既然找工作是一个持久战,那就需要有足够的体力支撑

一直待在家里刷着求职网站,海投无果、不断被拒,在这种精神和体力的双重内耗之下,人很容易崩溃

不如出去走走,呼吸新鲜空气,做一些运动

保持一个健康的身心,反而能保持自己的自信心和提升求职效率

三、工具提效

这一次备考,主要使用了这么一些工具

Flomo

Flomo 是一个「简单、高效」的碎片笔记工具,可以帮助你记录、整理、回顾自己的备考过程

我在 Flomo 中主要记录了自己的面试经历、面试题目、面试感受、面试反思等等

飞书

飞书支持飞书个人版,这次备考主要是使用了「飞书文档」 和「飞书妙记」

首先是飞书文档,真的很香,文档类型丰富,有像 Notion 一样的块文档,还有思维导图、Base 二维表格、画板等等

我用飞书文档做一个 ✨ 前端求职大攻略 ,下一节会提到

飞书妙记的话,可以上传语音 MP3,然后转成文字,这个功能主要用于面试后的自我复盘

你可以点击点击文字,直接跳转到对应的音频位置,非常方便,还有倍速播放、跳过空白等功能

如果你还没用过飞书 👉 邀请使用飞书

滴答清单

滴答清单是一个「任务管理工具」,主要用来做备考计划

另外滴答清单有一个不错的番茄钟专注功能,用来跟进每天的任务开销和状态

如果你还没用过滴答清单 👉 邀请使用滴答清单

AI 类工具

不得不承认,现在 AI 工具的效果真的不错

我本来想要付费找一些前辈做一些备考指导,但是使用 AI 工具也能达到不错的效果,而且免费

  • ChatGPT、Claude:回答质量不错,麻烦的是需要梯子 🪜

  • 豆包:字节跳动家的 AI 问答产品,对于非技术类的问题,回答质量赶得上 ChatGPT,并且支持 PC 客户端,我非常喜欢的一个功能是像 Spotlight 一样,快捷键唤起就可以向直接 AI 提问

  • 腾讯元宝:回答效果一般,但胜在可以搜索相关微信公众号文章再做回答,毕竟现在中文高质量回答都被圈在微信封闭的平台上

四、做一个个人的求职攻略

我认为,每个求职的程序员都应该做一个个人的求职攻略

为什么做一个求职攻略

其实原因很简单,就是把备考的过程系统化,将备考的攻略、求职记录、面试题目、面试过程等都汇总在一个知识库

抛开这一次面试,你的求职攻略可以用到下一次面试,现在的付出也是在增援未来的自己

个人选用飞书文档也是刚好它比较强大,思维导图也能免费使用

当然,用自己熟练的笔记应用也可以,比如语雀、Notion、Obsidian,甚至 Word、Excel 也可以

软件工具只是手段,重要的是你的思考和记录

求职攻略参考内容

目前个人的飞书文档 ✨ 前端求职大攻略 已经公开,欢迎大家参考

首先是《大攻略》部分,按照求职时间线排列,涵盖求职的各种事项

如果觉得烦,可以只看《Lite 极简说明书》

接下来就是《作业 1:PDCJ》这次面试对于阶段性复盘我使用了 PDCJ ,它是一种思维模型:

  • P 是 Plan 计划
  • D 是 Do 做事
  • C 是 Check 检查
  • J 是 Just 调整。

通过阶段性地计划、施行,再不断地检查、调整,确保自己的面试正确又高效地前行,不会出轨。

《作业 2:面试题》这个比较重要,除了技术八股文之外,还要深入挖掘自己的项目

不仅仅是成功的项目,失败的项目也要挖掘,让面试官看到到你对项目的深层次、多角度的思考

然后就是《作业3:个性化自我介绍》,这个就是 show 自己

在面试中,自我介绍是最重要的环节之一,它可以让面试官对你有一个初步的印象

不过也看面试官,有的面试官可能不太喜欢开头的「自我介绍」,他们更喜欢直接问问题,比如我遇到上来就直接问“介绍一下你做过的最有成就感的项目”

对于《作业 4:公司跟进表》,维护自己所有的投递状态,包括公司名称、岗位、投递时间、面试状态等等

目前程序员求职渠道还是挺多的,用这个表可以维护自己的求职状态,能够一个统筹和及时跟进

当这个数据量持续增长之后,你也可以分析出自己的投递效率,比如你会发现内推的简历通过率会比 Boss 直聘投递高很多,这样你就可以调整自己的求职策略

五、最后一些小建议

1. 不要否定自己

考试会考砸,谈恋爱会分手,面试会挂,这些都是正常的

人生失败才是常态,成功是少数

不要因为面试失败就着急否定自己

如果失败了,就去复盘,去反思,去总结,再去尝试(PDCJ)

分享一句很喜欢的话:

2. 谦虚

叔本华说过,每个人都把自己视野的极限,当做世界的极限

井底之蛙的对世界极限认知是一口井

我之前就犯过这样的错误,以为自己很厉害,但是越往深处学,越发现自己的渺小和无知

现在面试的时候,就保持谦逊的态度

面试遇到自己的强势领域,娓娓道来,展示你的能力和思考

遇到自己的弱势领域,坦诚承认,表示愿意学习和提升

3. 关于外包公司

外包公司的话,能不去就不去

待遇这方面,工资、年终奖等先被外包公司扒一层皮

工作内容方面,往往是一些维护、二次开发、测试等杂活

技术方面,外包公司的技术氛围往往不好,身边外包同事可能都比较躺平

自尊心方面,外包的同学往往是低人一等,公司内网权限、工牌、餐卡等都在提示你和正式员工的差距

所以,除非是过渡阶段(1-2 年),外包公司不建议去

另外,不建议用外包岗位来作为面试练手

外包岗位的面试题目、难度、面试官心态等和正常岗位有区别,参考意义不大

并且还要投入相当多的时间和精力,比如和 HR 对接、约笔试、面试、通勤去现场面试等

没必要做这种无用功,还是保存体力,专注正常岗位的面试

对于 Boss 直聘,可以在设置 > 隐私保护 > 屏蔽公司 中,把外包公司都屏蔽掉,避免干扰

4. 利用你能利用的一切

之前听过,创业的状态是「小姐心态,寡妇待遇,妇联追求

求职的过程,这句话也适用

你的追求和目的是寻求一份满意的工作,那就放下身段,动用你能利用的一切资源

把你的朋友圈、同学圈、前同事挨个联系一遍,问问有没有内推的机会

发邮箱找大佬,看看有没有合适的工作岗位 or 请教面试的相关事宜

加技术群,加 QQ 群,加微信群,多和大佬交流,多和同行交流

把自己的求职面扩大,不要局限在 Boss 直聘、拉勾、猎聘等求职网站