阅读视图
韩国央行对货币供应和流动性统计数据进行全面修改
「美国贴吧」被 AI 水军攻陷:为了证明是人类,2400 万用户被迫像傻瓜一样说话

「这是一项革命性的工作」、「不是……而是……」、「首先……其次……」;在一篇文章里读到这些词,你是不是本能地开始觉得,有点不对劲了。
三年前,这些用法还都是正常的表达,它们出现在各种经典文学作品的段落里。但现在,不管怎么用它,只要有这些词存在,就有充分的迹象表明,你正在阅读的内容是由 AI 生成。
这很听起来很荒诞,但我们似乎都已经完全接受了这样的现实。甚至再过几年,可能所有的文字都可能是由 AI 生成,到时每个词都将被打上了「AI 味」的标签。

AI 文字的泛滥速度有多快,美国版贴吧 Reddit 前几天表示,正在遭受大规模的 AI 内容入侵,几大热门板块都被 AI Slop(AI 垃圾)淹没。
其中一个拥有 2400 万用户的超大社区 r/AmItheAsshole(我是不是个混蛋)里,管理员 Cassie 悲观地估计,高达 50% 的内容,都在某种程度上被 AI 污染过。

▲Reddit「我是混蛋吗」版块有超过 2400 万成员,里面分享的内容大多是个人故事
曾经我们以为这些「活人感」极强的内容分享,AI 还没办法替代。如今,AI 却在那里恣意扮演着,拥有真情实感的人类;屏幕前的我们为了自证清白,反而开始学习「AI 味」是什么,然后被迫像傻瓜一样说话。
AI 也学会了阴阳怪气
国内的社交媒体也不例外,刷小红书的时候,看到那种评论特别多、吵得相当厉害的帖子,点进去用户个人主页一看,一个昵称都是默认数字的用户。有网友在帖子下面评论,「大家别回复了,过几天他就要在小红书接广告,开始带货了。」
从开始用 AI 发表各种旅游攻略、工具分享、科技新闻,这些比较理性的内容,到现在 AI 进军家长里短、校园职场、情感成长等感性的内容。
经常是一些故意挑起对立的帖子,然后追根溯源,发现整件事情根本就不存在,都是 AI 生成的「本故事纯属虚构」,光是浪费了我们的心情。

▲ 大量能直接生成,爆款小红书笔记的 AI 工具
这种现象在「人在美国,刚下飞机」的知乎、还有本就是各种广告的微博,同样泛滥。我们曾经以为的活人味,真实的争吵、离谱的求助、和没有任何修饰的情绪,现在都开始被 AI 承包了。
连线前几天报道的 Reddit 这则类似消息,里面提到了多个有着千万成员的版块,充斥着大量的 AI 内容。而这些 AI 不再是那种只会发硬广的僵尸号,它们学会了生成各种挑起对立、增加评论热度(Rage-bait)的故事。
像是有人用 AI 编造了一个「新娘要求客人,穿特定丑陋颜色衣服」的婚礼闹剧,还有虚构大量「子女与父母爆发冲突」的共情话题。而这些 AI 故事唯一的目的,就是挑动真实用户的愤怒,骗取点击和互动,然后养号、卖账号。

▲ Reddit 的贡献者计划,发帖者通过获得点赞(karma)、评论等互动来赚钱
AI 现在可以模拟上亿种人生,想要什么故事他都有。另一个热门版块的版主在接受采访时说,AI 是 Reddit 生存的巨大威胁。因为 AI 也使用 Reddit 上的真人语料训练,而现在 AI 内容又出现在 Reddit 上。
AI 喂养 AI,就像蛇开始吞噬自己的尾巴,不采取行动,Reddit 终会自取灭亡。
Reddit 发言人说,2025 年上半年已经删除了超过 4000 万条垃圾和虚假内容。但现在的 AI,进化到学会了人类的口癖、情绪甚至偏见。当我们在评论区为一个离谱的故事义愤填膺时,屏幕对面让人血压升高的,可能只是一串代码。
我们越来越难分辨,这段写得太顺的话,到底是不是 AI 写的?也开始接受,无论是那些正式的、得体的表达,还是微博上某句没有逻辑和章法的吐槽,都正在被 AI 吸收。

▲ChatGPT 发布后的 18 个月内,统计了 36 个 YouTube 视频,发现 AI 味词语(Delve、Boast、Swift、Inquiry、Meticulous)的使用量激增;这些词不仅出现在正式、脚本化的视频或播客节目中,也渗透到了随机的对话中。例如在 ChatGPT 出现之前,一般不会说 delve(钻研),而是直接说 look into(调查研究)
就像前几年,社交媒体上热衷于用一些缩写,像是 YYDS、Y1S1、DDDD 等表达,现在也同样常见,但这些用法,现在的 AI 显然也能直接拿过来用。
这种感觉很像三体第一部里面,「物理学不存在了」,AI Slop 的成功,甚至不需要伪造一个新的世界,它只需要让我们现在的内容世界变得不可信,这种怀疑就足够我们折腾了。
哪怕帖子不是 AI 写的,但 AI 的存在就像房间里有个间谍,怀疑本身成了最大的敌人。
Reddit 上的管理员,也解释了他们如何检测 AI 生成的内容,但最后都统一到「凭感觉」这一方法。很明显,这种方法并不管用。他们说,要识别这些 AI 内容非常困难。真实用户发的帖子会被 AI 改写,但有些真实用户的写作方式也越来越像 AI,不需要 AI 辅助,有些人现在让他写东西,就是一个行走的 ChatGPT。
高级感,就是 AI 使用者嫌疑犯
AI 内容检测的困难,也发生在维基百科,它们的编辑发起了 AI 清理计划(Project AI Cleanup),并在上个月发布了一份 AI 写作的典型格式和风格列表,试图总结出一套能直接识别 AI 文风的特征。
科技媒体 TechCrunch 把这份指南,称作是目前最好的 AI 识别手册。但当你打开它,会感到一阵深深的无力感——因为它把许多标准的、甚至优美的书面语,直接打成了「AI 嫌疑犯」。
里面的内容非常详细,我们做了一些大致的整理,如果你用了下面这些词语,被认定为 AI 的可能性是相当大。

▲图片来源:https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing
使用各种高级和 AI 常用词汇: 比如「革命性的」、「效率革命」、「根深蒂固的」、「格局」、「至关重要的」、「不仅……而且」、「不是……而是」、「最重要的是记住」等将近 100 多个相关的词语和表达。

语法过于严谨: 像是大量地使用现在分词做从句,在语法专家眼里这是标准用法,但在维基编辑眼里,这就是 AI 的惯用语。放到中文语境下,就是使用大量的长难句,可以拆分成几句话来表述的内容,非要写成论文一样,增大阅读障碍。

使用营销感明显的辞藻: 所有的风景都是「壮丽的」,所有的观点都是「惊人的」,这些词一旦出现,就像电视广告一样虚假。维基百科解释说,AI 在描述某些特定内容时,很难保持中立的语气,它会不可避免地使用到这些营销味十足的夸张用法。
此外还有一些编辑风格上的迹象,同样能表明内容是可能由 AI 生成。例如大量的分点列表,无论是数字列表还是无序列表,把不适合分点说明的内容,统统用列表来解释。还有反复出现的文本加粗、使用过度的破折号、坚持认为文章就是要有小标题和章节、以及排比句式等。
还挺讽刺,无论是破折号还是分点,甚至是那些已经被列为 AI 常用的词语,在 AI 出现之前,它们都有其合适的位置。现在的 AI 因为在模仿我们的高级写作,而我们为了避嫌,不得不放弃使用这些词汇和风格。
如果我们想证明自己是人,最好的方法是犯点错。
AI 时代的八股文
犯错也不是那么容易,我们没有 AI 那么善变。一边是 AI 在 Reddit 等社区的泛滥和伪装,导致整个互联网环境的信任崩塌,而我们普通人不得不为 AI 的入侵买单。
另一边,词汇丰富、逻辑严密、语法规范这些原本是写作水平高的表现,但在维基百科的 AI 检测逻辑下,这些特征变成了嫌疑。我们「买单」的成本变得越来越高,放弃追求完美的表达,转而模仿一种更笨拙的风格。
例如故意把「的、得、地」,这种明显但又不会被太在意的字词误用。还有,尝试各种具有自己风格的表达,像是前段时间网友热议的「居然人」和「竟然人」,在写作中继续保持住自己「X然」身份。

▲腾讯官方的朱雀 AI 检测助手,https://matrix.tencent.com/ai-detect/
每一个用过 AI 写作的网友,大概都会有类似为了摆脱 AI 检测,把自己的文字改来改去,煎熬的感受。在 Reddit 上,也有用户分享了同样令人哭笑不得的真实案例。
一位丈夫帮妻子修改研究生论文,妻子的初稿写作风格偏向意识流,不太懂学术规范,AI 检测率为 0%。这位丈夫帮她大改了一遍,修正了逻辑,润色了表达。结果,这篇「更好」的文章,AI 检测率飙升到了 12%。
最后他们不得不眼睁睁,看着那些漂亮的句子被一个个删掉,换成更笨拙、更破碎的表达。她必须把文章改得「不像 AI」,哪怕这意味着文章质量的下降。

▲Reddit 帖子链接:https://www.reddit.com/r/artificial/comments/1pci14r/writing_detection_tools_preventing_people_from/
过去我们担心应试教育,让学生写出千篇一律的文章;现在,AI 检测器正在制造同样的问题。为了通过检测,学生们开始被迫学习一种奇怪、笨拙的写作方式。
这是一个完全错位的惩罚机制,我们正在惩罚那些写得好的人,奖励那些写得笨拙的人。人类正在刻意让自己看起来像没受过教育一样,只为了向算法证明:看,这真是我写的。
但也有网友说,只要你写的比 AI 好,那就没什么好担心的;把正常的文章改的四不像,那是因为你自己能力不够。
如果 AI 能写出比人类更好的东西,那么人类就需要适应,并创造出更好的内容。
如果 AI 只能产出垃圾,那么我不认为我们有什么好担心的,因为人们通常会选择更好的东西。
说实话,AI 学习进化的速度,远超过我们发现「更好的表达」;今天可能用了某个新颖的句子,明天就能被 AI 学走。

技术发展的初衷,本意是让 AI 帮我们处理繁琐的文字工作,我们能回归创造性的思考。
但现实却是,AI 似乎正在享受「创作」的乐趣,哪怕是生成垃圾;而人类却在耗费精力,去证明自己不是那该死的机器,无论是通过做得更好还是更差。
也许我们就是躲不开,吃了一百个鲁迅和李白的 AI,就是越来越会写,我们也会越来越依赖它。AI 写的邮件、AI 写的年度报告、社交软件上的 AI 广告帖、甚至妈妈发来的短信也是 AI 写的……即便那些从来不用 AI 的人,也被周围的人潜移默化,开始说出一些「AI 特征」明显的话。
「有没有用 AI」这个问题,甚至总有一天在评论区里面不会再出现。
而那些在 AI 帖子下面付出的情感、激烈的争论、还有专门学习的「像傻瓜一样说话」,都变成了对 AI 的臣服。
现在的互联网,用来验证是人类的,是识别几个数字,或者选出所有包含公共汽车的图片;再过几年,不知道还会有哪些考验,来让你相信我是一个人。

和三体一样,地球人在面对三体人到来时,说人类比蝗虫强大那么多倍,但是几百年了,蝗虫还是在漫天飞舞。今天的 AI,也开始比我们的写作更强大,但只要我们还在表达,人类的声音就永远不会被 AI 取代。
#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。
俄罗斯12月服务业PMI为52.3
*** 都不用tailwind!!!哎嘛 真香😘😘😘
🎯 前言
刚入职新公司,老板就给了我一个“惊喜”任务:
老板:“需要根据现有后台系统,做个新的后台系统,好看点、大气点、不要太模板化,对了用 React。”
我:“???没有 UI、没有产品。我尼玛*** ***”
💩 了解旧版系统
旧版系统是 Vue3 + ant-design-vue 做的,一个巨大的“屎山”直接丢了过来。几个比较严重的问题:
- 代码质量极差:完全没有组件化可言,所有页面都是一坨一坨的代码堆在一起
- UI 设计极差:应该也是没有设计师,完全靠前端随意发挥(主题配色红配绿、乱七八糟的字体大小、间距)
- 功能逻辑混乱:代码注释极少,完全看不懂业务逻辑
- 后端也是一坨屎:接口设计差,前后端配合差,很多东西都是前端做的兼容/硬编码等等
代码是老板不知道从哪里弄来的压缩包。如上所示,“屎山”把我压得喘不过气。我有时候真想找到之前写这个代码的人骂一顿(没有 Git 记录也不知道是谁)。这个代码存在的唯一价值就是主流程能跑,或许这就是小公司老板所关心的吧。
🧠 思考架构
没有 UI、产品,我真的是一头雾水,不知道该如何下手。只能先把旧版系统的功能理清楚,然后一步步实现。因为老板要求用 React,正好我之前用 React 自己搭建了一套后台管理模板(文章链接),它是基于 Ant Design 的一套系统,有兴趣的可以去看看。
🚀 开始实现
由于我之前搭建的 React 后台模板已经比较完善了,所以我直接拿来用。然后一步步把旧版系统的功能搬过来,并且升级了一些依赖版本等等。老板要求的一个重点是不要太“模板化”,我一直思考应该怎么样去处理和优化。
由于没有设计师,我只能凭借我的审美去调整一些 UI 细节,比如配色、字体、间距、图标等等。总之就是尽量让它看起来不那么像一个模板系统。初版如下图:
上图的 UI,我都有微调过包括字体、图标、间距等等,让它看起来不那么“模板化”。奈何有天老板从我旁边过,看了一眼说:“你是用的 Antd 吗?能不能搞得不那么模板化,看着还是太大众了。”当时我内心真的想骂人,辛辛苦苦做的工作就这样被一句话否定了,而且又没 UI、又没产品。
🔄 重新出发
没办法,谁让人家是老板,还是照做。我在网上有看到过 shadcn/ui 这个组件库的,它是基于 TailwindCSS,我本人是非常厌恶这个玩意的:
- 因为上一家公司有个同事在项目里使用TailwindCSS,满屏的 classname,看得我头皮发麻。想要改一个样式都不知道从哪下手
- shadcn/ui 和我认知里的 Antd Pro 是完全不同的设计体系。比如在 Antd Pro 里写一个表格,请求接口拿到数据喂给 Table 就行了。但是在 shadcn/ui 里你得自己写分页、排序、筛选这些功能。你得自己实现很多功能,会导致我的工作量大增。虽然现在都用 AI 编码了,但是我还是觉得很麻烦
但是由于我知道 TailwindCSS 的流行趋势,且确实 shadcn/ui 的可定制化更高一点,所以还是决定用 shadcn/ui 来重构。但是时间紧、任务重,我想从网上找找有没有现成的可以二开的 shadcn 后台模板。结果发现了这个项目:shadcn-admin。里面封装好了 Table、Layout 等功能且支持移动端。推荐给各位,太香啦!
🛠️ 技术栈
- React 19
- TypeScript
- TanStack 家族(@tanstack/react-query、@tanstack/react-router、@tanstack/react-table)
- shadcn/ui
- TailwindCSS
- Zustand
- Zod
- react-hook-form 等
🔧 集成适配
虽说是现成的模板,但是有一些功能没有、或者需要根据业务适配。比如国际化、Table 远程数据请求(模板是本地模拟数据)等。
🎨 最终形态
📌 总结
这一套新兴模板,上手还是有点难度的(比如 TanStack 家族、shadcn/ui)。TanStack 体系庞大,shadcn 定制化高(功能都要自己实现)。但是目前 AI 盛行,资料也是大把,相信对你来说也都是小菜一碟。
💭 最后
Tailwind 还是挺香的,我觉得它是一把双刃剑。我的前同事(Tailwind 写 class 十几、几十行)确实给维护的人增加心智负担,但是也可以通过一些技术手段改善和优化。这取决于用它的人是怎样的。
深圳市资本运营集团注册资本增至201.3亿,增幅约30%
前端 Token 无感刷新全解析:Vue3 与 React 实现方案
在前后端分离架构中,Token 是主流的身份认证方式。但 Token 存在有效期限制,若在用户操作过程中 Token 过期,会导致请求失败,影响用户体验。「无感刷新」技术应运而生——它能在 Token 过期前或过期瞬间,自动刷新 Token 并继续完成原请求,全程对用户透明。
本文将先梳理 Token 无感刷新的核心原理,再分别基于 Vue3(Composition API + Pinia)和 React(Hooks + Axios)给出完整实现方案,同时解析常见问题与优化思路,帮助开发者快速落地。
一、核心原理:为什么需要无感刷新?怎么实现?
1. 基础概念:Access Token 与 Refresh Token
无感刷新依赖「双 Token 机制」,后端需返回两种 Token:
- Access Token(访问 Token) :有效期短(如 2 小时),用于接口请求的身份认证,放在请求头(如 Authorization: Bearer {token});
- Refresh Token(刷新 Token) :有效期长(如 7 天),仅用于 Access Token 过期时请求新的 Access Token,安全性要求更高(建议存储在 HttpOnly Cookie 中,避免 XSS 攻击)。
2. 无感刷新核心流程
- 前端发起接口请求,携带 Access Token;
- 拦截响应:若返回 401 状态码(Access Token 过期),则触发刷新逻辑;
- 用 Refresh Token 调用后端「刷新 Token 接口」,获取新的 Access Token;
- 更新本地存储的 Access Token;
- 重新发起之前失败的请求(携带新 Token);
- 若 Refresh Token 也过期(刷新接口返回 401),则跳转至登录页,要求用户重新登录。
关键优化点:避免重复刷新——当多个请求同时因 Token 过期失败时,需保证只发起一次 Refresh Token 请求,其他请求排队等待新 Token 生成后再重试。
二、前置准备:Axios 拦截器封装(通用基础)
无论是 Vue 还是 React,都可基于 Axios 的「请求拦截器」和「响应拦截器」实现 Token 统一处理。先封装一个基础 Axios 实例:
// utils/request.js
import axios from 'axios';
// 创建 Axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量中的接口基础地址
timeout: 5000 // 请求超时时间
});
// 1. 请求拦截器:添加 Access Token
service.interceptors.request.use(
(config) => {
const accessToken = localStorage.getItem('accessToken'); // 简化存储,实际建议 Vue 用 Pinia/React 用状态管理
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 2. 响应拦截器:处理 Token 过期逻辑(核心,后续框架差异化实现)
// 此处先留空,后续在 Vue/React 中补充具体逻辑
service.interceptors.response.use(
(response) => response.data, // 直接返回响应体
(error) => handleResponseError(error, service) // 错误处理,传入 service 用于重试请求
);
export default service;
三、Vue3 实现方案(Composition API + Pinia)
Vue3 中推荐用 Pinia 管理全局状态(存储 Token),结合 Composition API 封装刷新逻辑,保证代码复用性。
1. 步骤 1:Pinia 状态管理(存储 Token)
创建 Pinia Store 管理 Access Token 和 Refresh Token,提供刷新 Token 的方法:
// stores/authStore.js
import { defineStore } from 'pinia';
import axios from 'axios';
export const useAuthStore = defineStore('auth', {
state: () => ({
accessToken: localStorage.getItem('accessToken') || '',
refreshToken: localStorage.getItem('refreshToken') || '' // 实际建议存 HttpOnly Cookie
}),
actions: {
// 更新 Token
updateTokens(newAccessToken, newRefreshToken) {
this.accessToken = newAccessToken;
this.refreshToken = newRefreshToken;
localStorage.setItem('accessToken', newAccessToken);
localStorage.setItem('refreshToken', newRefreshToken); // 仅演示,生产环境用 HttpOnly Cookie
},
// 刷新 Token 核心方法
async refreshAccessToken() {
try {
const res = await axios.post('/api/refresh-token', {
refreshToken: this.refreshToken
});
const { accessToken, refreshToken } = res.data;
this.updateTokens(accessToken, refreshToken);
return accessToken; // 返回新 Token,用于重试请求
} catch (error) {
// 刷新 Token 失败(如 Refresh Token 过期),清除状态并跳转登录
this.clearTokens();
window.location.href = '/login';
return Promise.reject(error);
}
},
// 清除 Token
clearTokens() {
this.accessToken = '';
this.refreshToken = '';
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
}
});
2. 步骤 2:实现响应拦截器的错误处理
完善之前的响应拦截器,添加 Token 过期处理逻辑,核心是「避免重复刷新」:
// utils/request.js(Vue3 版本补充)
import { useAuthStore } from '@/stores/authStore';
// 用于存储刷新 Token 的请求(避免重复刷新)
let refreshPromise = null;
// 响应错误处理函数
async function handleResponseError(error, service) {
const authStore = useAuthStore();
const originalRequest = error.config; // 原始请求配置
// 1. 不是 401 错误,直接 reject
if (error.response?.status !== 401) {
return Promise.reject(error);
}
// 2. 是 401 错误,但已经重试过一次,避免死循环
if (originalRequest._retry) {
return Promise.reject(error);
}
try {
// 3. 标记当前请求已重试,避免重复
originalRequest._retry = true;
// 4. 若没有正在进行的刷新请求,发起刷新;否则等待已有请求完成
if (!refreshPromise) {
refreshPromise = authStore.refreshAccessToken();
}
// 5. 等待刷新完成,获取新 Token
const newAccessToken = await refreshPromise;
// 6. 刷新完成后,重置 refreshPromise
refreshPromise = null;
// 7. 更新原始请求的 Authorization 头,重新发起请求
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return service(originalRequest);
} catch (refreshError) {
// 刷新失败,重置 refreshPromise
refreshPromise = null;
return Promise.reject(refreshError);
}
}
// 响应拦截器(补充完整)
service.interceptors.response.use(
(response) => response.data,
(error) => handleResponseError(error, service)
);
3. 步骤 3:组件中使用
封装好后,组件中直接使用 request 发起请求即可,无需关注 Token 刷新逻辑:
// components/Example.vue
<script setup>
import request from '@/utils/request';
import { ref, onMounted } from 'vue';
const data = ref(null);
onMounted(async () => {
try {
// 发起请求,Token 过期时会自动无感刷新
const res = await request.get('/api/user-info');
data.value = res.data;
} catch (error) {
console.error('请求失败:', error);
}
});
</script>
<template>
<div>{{ data ? data.name : '加载中...' }}</div>
</template>
四、React 实现方案(Hooks + Context)
React 中推荐用「Context + Hooks」管理全局 Token 状态,结合 Axios 拦截器实现无感刷新,逻辑与 Vue3 类似,但状态管理方式不同。
1. 步骤 1:创建 Auth Context(管理 Token 状态)
用 Context 提供 Token 相关的状态和方法,供全局组件使用:
// context/AuthContext.js
import { createContext, useContext, useState, useEffect } from 'react';
import axios from 'axios';
// 创建 Context
const AuthContext = createContext();
// Provider 组件:提供 Token 状态和方法
export function AuthProvider({ children }) {
const [accessToken, setAccessToken] = useState(localStorage.getItem('accessToken') || '');
const [refreshToken, setRefreshToken] = useState(localStorage.getItem('refreshToken') || '');
// 更新 Token
const updateTokens = (newAccessToken, newRefreshToken) => {
setAccessToken(newAccessToken);
setRefreshToken(newRefreshToken);
localStorage.setItem('accessToken', newAccessToken);
localStorage.setItem('refreshToken', newRefreshToken); // 演示用,生产环境用 HttpOnly Cookie
};
// 刷新 Token
const refreshAccessToken = async () => {
try {
const res = await axios.post('/api/refresh-token', { refreshToken });
const { accessToken: newAccessToken, refreshToken: newRefreshToken } = res.data;
updateTokens(newAccessToken, newRefreshToken);
return newAccessToken;
} catch (error) {
// 刷新失败,清除状态并跳转登录
clearTokens();
window.location.href = '/login';
return Promise.reject(error);
}
};
// 清除 Token
const clearTokens = () => {
setAccessToken('');
setRefreshToken('');
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
};
// 提供给子组件的内容
const value = {
accessToken,
refreshToken,
updateTokens,
refreshAccessToken,
clearTokens
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// 自定义 Hook:方便组件获取 Auth 状态
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
2. 步骤 2:在入口文件中包裹 AuthProvider
确保全局组件都能访问到 Auth Context:
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { AuthProvider } from './context/AuthContext';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<AuthProvider>
<App />
</AuthProvider>
);
3. 步骤 3:完善 Axios 响应拦截器
逻辑与 Vue3 一致,核心是避免重复刷新,通过 useAuth Hook 获取刷新 Token 方法:
// utils/request.js(React 版本补充)
import { useAuth } from '../context/AuthContext';
// 注意:React 中不能在 Axios 拦截器中直接使用 useAuth(Hook 只能在组件/自定义 Hook 中使用)
// 解决方案:用一个函数封装,在组件初始化时调用,注入 auth 实例
export function initRequestInterceptors() {
const { refreshAccessToken } = useAuth();
let refreshPromise = null;
// 响应错误处理函数
async function handleResponseError(error, service) {
const originalRequest = error.config;
// 1. 非 401 错误,直接 reject
if (error.response?.status !== 401) {
return Promise.reject(error);
}
// 2. 已重试过,避免死循环
if (originalRequest._retry) {
return Promise.reject(error);
}
try {
originalRequest._retry = true;
// 3. 避免重复刷新
if (!refreshPromise) {
refreshPromise = refreshAccessToken();
}
// 4. 等待新 Token
const newAccessToken = await refreshPromise;
refreshPromise = null;
// 5. 重试原始请求
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return service(originalRequest);
} catch (refreshError) {
refreshPromise = null;
return Promise.reject(refreshError);
}
}
// 重新设置响应拦截器(注入 auth 实例后)
service.interceptors.response.use(
(response) => response.data,
(error) => handleResponseError(error, service)
);
}
export default service;
4. 步骤 4:在组件中初始化拦截器并使用
在根组件(如 App.js)中初始化拦截器,确保 useAuth 能正常使用:
// App.js
import { useEffect } from 'react';
import { initRequestInterceptors } from './utils/request';
import request from './utils/request';
import { useState } from 'react';
function App() {
const [userInfo, setUserInfo] = useState(null);
// 初始化 Axios 拦截器(注入 Auth 上下文)
useEffect(() => {
initRequestInterceptors();
}, []);
// 发起请求(Token 过期自动刷新)
const fetchUserInfo = async () => {
try {
const res = await request.get('/api/user-info');
setUserInfo(res.data);
} catch (error) {
console.error('请求失败:', error);
}
};
useEffect(() => {
fetchUserInfo();
}, []);
return (
<div className="App">
{userInfo ? <h1>欢迎,{userInfo.name}</h1> : <p>加载中...</p>}
</div>
);
}
export default App;
五、关键优化与安全注意事项
1. 避免重复刷新的核心逻辑
用「refreshPromise」变量存储正在进行的刷新 Token 请求,当多个请求同时失败时,都等待同一个 refreshPromise 完成,避免发起多个刷新请求,这是无感刷新的核心优化点。
2. 安全优化:Refresh Token 的存储方式
-
不建议将 Refresh Token 存储在 localStorage/sessionStorage 中,容易遭受 XSS 攻击;
-
推荐存储在「HttpOnly Cookie」中,由浏览器自动携带,无法通过 JavaScript 访问,有效防御 XSS 攻击;
-
若后端支持,可给 Refresh Token 增加「设备绑定」「IP 限制」等额外安全措施。
3. 主动刷新:提前预防 Token 过期
被动刷新(等待 401 后再刷新)可能存在延迟,可增加「主动刷新」逻辑:
- 记录 Access Token 的生成时间和过期时间;
- 在请求拦截器中判断 Token 剩余有效期(如小于 5 分钟),主动发起刷新请求;
- 避免在用户无操作时刷新,可结合「用户活动监听」(如 click、keydown 事件)触发主动刷新。
4. 异常处理:刷新失败的兜底方案
当 Refresh Token 过期或无效时,必须跳转至登录页,并清除本地残留的 Token 状态,避免死循环请求。同时,可给用户提示「登录已过期,请重新登录」,提升体验。
六、Vue3 与 React 实现方案对比
| 对比维度 | Vue3 实现 | React 实现 |
|---|---|---|
| 状态管理 | Pinia(官方推荐,API 简洁,支持 TypeScript) | Context + Hooks(原生支持,无需额外依赖) |
| 拦截器初始化 | 可直接在 Pinia 中获取状态,无需额外注入 | 需在组件中初始化拦截器,注入 Auth Context |
| 核心逻辑 | 基于 Composition API,逻辑封装更灵活 | 基于自定义 Hooks,符合函数式编程思想 |
| 学习成本 | Pinia 学习成本低,适合 Vue 生态开发者 | Context + Hooks 需理解 React 状态传递机制 |
本质差异:状态管理方式不同,但无感刷新的核心逻辑(双 Token、拦截器、避免重复刷新)完全一致,开发者可根据自身技术栈选择对应方案。
七、总结
前端 Token 无感刷新的核心是「双 Token 机制 + Axios 拦截器」,关键在于解决「重复刷新」和「安全存储」问题。Vue3 和 React 的实现方案虽在状态管理上有差异,但核心逻辑相通:
- 用请求拦截器统一添加 Access Token;
- 用响应拦截器捕获 401 错误,触发刷新逻辑;
- 通过一个全局变量控制刷新请求的唯一性,避免重复请求;
- 刷新成功后重试原始请求,失败则跳转登录。
实际项目中,需结合后端接口设计(如刷新 Token 的接口地址、参数格式)和安全需求(如 Refresh Token 存储方式)调整实现细节。合理的无感刷新方案能大幅提升用户体验,避免因 Token 过期导致的操作中断。
多只货币ETF场内异动,基金公司火速发布溢价风险提示公告
品牌如何应对复杂的市场情绪、消退的流量红利?「鲸鸿动能」提出营销四步走
文|周鑫雨
编辑|苏建勋
在宏观环境趋于平缓、企业增长压力持续加大的背景下,品牌营销正在经历一轮明显的调整。流量红利减弱、获客成本走高,传统以投放效率为核心的数字营销模式,正在遭遇瓶颈。
12 月底,华为旗下全场景智慧营销平台“鲸鸿动能”,在上海举办了一场名为“心驰寰宇,智骋新程”的心智沟通会。
鲸鸿动能方面的判断是:单点投放和碎片化触达,已经难以支撑品牌长期增长,营销正在从“流量竞争”转向对用户真实生活场景的理解和渗透。
为了应对市场情绪愈发复杂、获客成本持续走高的市场环境,帮助品牌拿回市场主动权,鲸鸿动能提出了新的营销概念:数智驱动的“全场景营销”。
简单来讲,这一营销体系,包含四个层面:
1、数据科学驱动的“先知”洞察(营销决策上的主动性)
在传统数字营销体系中,品牌对用户的理解往往来自单一平台或设备的数据。这类数据虽然规模庞大,但更多反映的是局部行为,很难还原用户的完整决策过程。
鲸鸿动能试图解决的问题,正是这种“割裂感”。依托华为统一的账号体系和“1+8+N”硬件生态,鲸鸿动能将手机、PC、智慧屏、可穿戴设备等多终端连接在一起,在合规前提下,形成跨场景的用户行为理解。
鲸鸿动能相关负责人在现场提到,品牌看到的不再只是某一次点击或曝光,而是用户在不同时间、不同设备上的连续行为轨迹。这种跨终端、跨场景的数据整合,被用于构建更立体的用户画像,并服务于后续营销决策。
2、智能化的“先手”触达(执行层面的主动性)
在洞察之外,执行效率同样是当前营销的一大难题。预算分配、媒介组合、实时优化,仍高度依赖人工经验,响应速度有限。
鲸鸿动能在此次沟通会上重点介绍了其以 AI Agent 为核心的投放体系。与传统工具不同,AI Agent 被设计为参与决策过程,而不仅是执行指令。品牌可以通过自然语言描述营销目标,由系统自动完成受众理解、媒介选择和投放节奏规划。
从鲸鸿动能给出的案例来看,AI 更多承担的是“策略助理”的角色,缩短从目标设定到投放落地之间的链路,提升整体反应速度。这种方式,试图缓解营销执行对专业投放经验的高度依赖。
3、沉浸式场景化的”先机”抢占(场景触达上的主动性)
在注意力高度分散的环境中,鲸鸿动能并未强调在传统广告位中竞争,而是将重点放在“高专注场景”的挖掘上。
例如,针对高频商务出行人群,鲸鸿动能在航旅场景中推出“无网专区”,通过内容预加载的方式,在用户候机等高专注时段完成品牌触达。另一类重点场景,则来自家庭空间。
通过打通个人账号与家庭账号,鲸鸿动能推出跨屏投放方案,让品牌可以在客厅智慧屏进行展示,再在个人设备上完成互动和转化。其披露的合作数据显示,在部分品牌案例中,跨屏联动带来了明显的点击率提升。
4、以用户为中心的全旅程伴随 (营销关系上的主动性)
最后,鲸鸿动能将“全场景营销”的��标,归结为品牌与用户关系的重塑。鲸鸿动能方面认为,这种方式更接近长期关系的建立,而非单次转化。
借助鸿蒙生态内的服务和智能助手能力,品牌可以在不同设备和使用阶段持续出现,而非只在购买节点短暂露面。比如,用户在不同终端完成信息查询、提醒接收和交易闭环,品牌服务嵌入其中,而不是作为单独广告存在。
在流量红利逐渐消退的背景下,围绕场景、数据和 AI 重构营销逻辑,正在成为平台和品牌共同探索的方向。
这条路径能否在更大范围内跑通,仍有待市场检验。但可以确定的是,单纯依赖流量博弈的时代,正在接近尾声。
韩国反驳Coupang关于数据泄露有限的说法
zustand 入门
一、先看两段代码:Zustand vs 原生 setState
1. 原生 React 组件内 setState 实现 count
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
- 特点:
count是组件私有状态,只能在当前组件内修改 / 使用;如果其他组件需要用count,只能通过 props 透传。
2. Zustand 实现 count(官网简化版)
import { create } from "zustand";
// 1. 创建全局store(可在任意组件导入使用)
const useCountStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })), // 批量更新、依赖旧状态
}));
// 2. 组件A使用count
function CounterA() {
const count = useCountStore((state) => state.count);
const increment = useCountStore((state) => state.increment);
return (
<div>
<span>组件A:{count}</span>
<button onClick={increment}>+1</button>
</div>
);
}
// 3. 组件B(无关组件)也能使用同一个count
function CounterB() {
const count = useCountStore((state) => state.count); // 共享同一份状态
return <div>组件B:{count}</div>;
}
- 特点:
count是全局共享状态,CounterA、CounterB 甚至项目任意组件,都能直接读取 / 修改同一份 count,无需 props 透传。 - Zustand 的
store是一个全局单一数据源,组件 A 和组件 B 读取的count都指向这个容器里的同一个值,实时跟踪变化,实现跨组件状态同步。
3. 原生 React 组件内使用多个状态
jsx
import { useState } from "react";
// 子组件A:只显示name
function CounterA({ name }) {
console.log("🔴 原生CounterA重渲染了"); // 重渲染日志
return <div>CounterA:{name}</div>;
}
// 子组件B:只显示count
function CounterB({ count }) {
console.log("🔴 原生CounterB重渲染了"); // 重渲染日志
return <div>CounterB:{count}</div>;
}
// 父组件:管理name和count,渲染A、B
function Parent() {
const [name, setName] = useState("张三");
const [count, setCount] = useState(0);
return (
<div>
<CounterA name={name} />
<CounterB count={count} />
<button onClick={() => setName("李四")}>原生:改name</button>
</div>
);
}
export default Parent;
运行结果(点击「改 name」按钮后)
控制台会打印两行日志:
🔴 原生CounterA重渲染了
🔴 原生CounterB重渲染了
- 原因:原生 React 中,父组件的任意 state 变化,会导致整个父组件重新渲染,所有子组件也会跟着重新渲染—— 哪怕子组件 B 只接收 count,和 name 无关,也会被强制重渲染(这就是原生的冗余重渲染问题)。
4. zustand 组件内多个状态精准订阅
先明确页面结构(原生和 Zustand 完全一致)
页面外层是一个父组件,里面包含 3 个东西:
-
<CounterA />:只显示「name」(不关心 count) -
<CounterB />:只显示「count」(不关心 name) -
一个按钮:要么改 name,要么改 count
import { create } from "zustand";
// 1. 创建全局store(管理name和count,和父组件无关)
const useStore = create((set) => ({
name: "张三",
count: 0,
setName: (newName) => set({ name: newName }),
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// 子组件A:只读取store中的name(不关心count)
function CounterA() {
console.log("🟢 Zustand CounterA重渲染了"); // 重渲染日志
const name = useStore((state) => state.name); // 只订阅name
return <div>CounterA:{name}</div>;
}
// 子组件B:只读取store中的count(不关心name)
function CounterB() {
console.log("🟢 Zustand CounterB重渲染了"); // 重渲染日志
const count = useStore((state) => state.count); // 只订阅count
return <div>CounterB:{count}</div>;
}
// 父组件:只渲染A、B和按钮,不管理状态
function Parent() {
const increment = useStore((state) => state.increment); // 读取修改count的方法
return (
<div>
<CounterA /> {/* 不用传props,内部自己读store */}
<CounterB /> {/* 不用传props,内部自己读store */}
<button onClick={increment}>Zustand:count++</button>
</div>
);
}
export default Parent;
运行结果(点击「count++」按钮后)
控制台只会打印一行日志:
🟢 Zustand CounterB重渲染了
-
原因:Zustand 的核心是「组件独立订阅自己需要的状态」:
- CounterA 只订阅了 store 中的
name,没有订阅count—— 所以count变化时,CounterA 完全感知不到,不会重渲染; - CounterB 只订阅了 store 中的
count——count变化时,只有 CounterB 会重渲染; - 父组件没有管理任何 state,所以父组件也不会重渲染(自然不会强制子组件渲染)。
- CounterA 只订阅了 store 中的
二、zustand的核心优势
和原生的setState相比:
-
可维护性提升:全局状态管理,解决 “状态共享” 问题,避免多组件间通过透传等复杂方式传递共享的状态。所有修改状态的逻辑都集中在 store 里,组件只需要调用方法,无需关心内部实现。
-
重渲染性能提升:让组件只订阅自己需要的状态,只有订阅的状态变了,组件才重渲染。
-
天然支持异步状态管理:如果状态的更新依赖异步请求(比如从接口获取初始 count),原生 setState 需要在组件内写 useEffect,而 Zustand 可直接在 store 内封装异步函数。
和其他全局状态管理方案相比,官方提供了详细说明zustand.docs.pmnd.rs/getting-sta…
三、基础使用方法
定义 Store:包含状态和修改状态的方法
import { create } from 'zustand'
const useBear = create((set) => ({
// 状态
bears: 0,
// 方法
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
updateBears: (newBears) => set({ bears: newBears }),
}))
在组件中使用:订阅状态 + 调用方法
function BearCounter() {
const bears = useBear((state) => state.bears)
return <h1>{bears} bears around here...</h1>
}
function Controls() {
const increasePopulation = useBear((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
四、高级特性
(1)异步状态管理(官方原生支持,无需额外依赖)
const useUserStore = create((set) => ({
user: null,
isLoading: false,
// 异步方法:直接在 Store 内封装请求逻辑
fetchUser: async (userId) => {
set({ isLoading: true });
const res = await fetch(`/api/user/${userId}`);
const data = await res.json();
set({ user: data, isLoading: false }); // 请求完成更新状态
},
}));
// 组件中使用
function UserProfile({ userId }) {
const { user, isLoading, fetchUser } = useUserStore((state) => ({
user: state.user,
isLoading: state.isLoading,
fetchUser: state.fetchUser,
}));
useEffect(() => {
fetchUser(userId);
}, [userId, fetchUser]);
if (isLoading) return <div>Loading...</div>;
return <div>Name: {user?.name}</div>;
}
(2)状态持久化(官方推荐 persist 中间件)
import { create } from "zustand";
import { persist } from "zustand/middleware"; // 导入官方中间件
// 用 persist 包裹 Store,自动持久化到 localStorage(默认)
const useCartStore = create(
persist(
(set) => ({
cartItems: [],
addToCart: (item) => set((state) => ({ cartItems: [...state.cartItems, item] })),
}),
{
name: "cart-storage", // 本地存储的 key(默认是 "zustand")
// 可选:自定义存储方式(如 sessionStorage)
// storage: sessionStorage,
}
)
);
(3)精准订阅优化(官方性能核心)
- 订阅多个状态(用
shallow避免引用类型误判):
import { shallow } from "zustand/shallow";
// 订阅多个状态,只有状态值真的变化才重渲染(适合对象/数组)
const { bears, user } = useBearStore(
(state) => ({ bears: state.bears, user: state.user }),
shallow // 浅比较:对比对象/数组的引用,避免不必要重渲染
);
- 选择性订阅(组件只关心部分状态):
// 只订阅 bears 的偶数状态(官方示例,灵活筛选)
const evenBears = useBearStore((state) => state.bears % 2 === 0);
(4)拆分 Store(官方推荐:按业务模块拆分)
// store/userStore.js
export const useUserStore = create((set) => ({/* 用户相关状态 */}));
// store/cartStore.js
export const useCartStore = create((set) => ({/* 购物车相关状态 */}));
// 组件中按需导入
import { useUserStore } from "./store/userStore";
import { useCartStore } from "./store/cartStore";
3. 调试工具(官方支持 Redux DevTools)
import { create } from "zustand";
import { devtools } from "zustand/middleware"; // 导入调试中间件
// 用 devtools 包裹,支持 Redux DevTools 查看状态变更记录
const useBearStore = create(
devtools((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
}))
);
五、官方强调的注意事项
1. 状态订阅相关
-
🔴 避免订阅整个 Store(导致不必要重渲染):错误写法:
const store = useBearStore();(订阅所有状态,任意状态变化都触发重渲染)正确写法:const bears = useBearStore((state) => state.bears);(精准订阅单个状态) -
🟡 引用类型(对象 / 数组)的订阅:若订阅对象 / 数组,默认浅比较引用地址(如
{a:1}和{a:1}是不同引用,会触发重渲染),需手动用shallow或deepEqual优化:import { deepEqual } from "zustand/shallow"; const user = useBearStore((state) => state.user, deepEqual); // 深比较值
2. 状态更新相关
-
🔴 依赖旧状态必须用函数参数:错误写法:
set({ bears: bears + 1 })(可能获取到旧状态,因闭包问题)正确写法:set((state) => ({ bears: state.bears + 1 }))(官方推荐,确保拿到最新状态) -
🟡 批量更新:多次
set会自动批量合并(官方优化),无需手动处理:const updateUser = () => { set({ name: "李四" }); set({ age: 20 }); // 会和上一句合并为一次更新,避免多次重渲染 };
3. 组件卸载相关
-
🔴 异步请求未完成时组件卸载:若组件卸载后异步请求才返回,更新状态会导致警告,官方推荐用
abortController或状态判断:const fetchUser = async (userId) => { set({ isLoading: true }); const controller = new AbortController(); try { const res = await fetch(`/api/user/${userId}`, { signal: controller.signal }); const data = await res.json(); set((state) => ({ user: data, isLoading: false })); // 用函数参数确保状态最新 } catch (err) { if (err.name !== "AbortError") console.error(err); } return () => controller.abort(); // 组件卸载时取消请求 };
4. 性能优化相关
- 🟡 避免在订阅函数中创建新对象:错误写法:
useBearStore((state) => ({ bears: state.bears }))(每次返回新对象,触发重渲染)正确写法:useBearStore((state) => state.bears)或配合shallow使用 - 🟡 大型项目拆分细粒度 Store:官方不推荐创建 “单一大 Store”,而是按业务模块拆分(如用户、购物车、全局设置各自一个 Store),减少状态冗余和重渲染范围。
5. 其他官方提醒
- 无需 Provider 包裹:Zustand 不依赖 React Context,无需在根组件套 Provider(和 Redux/Pinia 不同);
- 支持非 React 环境:Store 可在非组件文件(如工具函数、API 请求文件)中直接使用
useBearStore.getState()读取状态、useBearStore.setState()修改状态; - 类型安全(TS 支持):官方原生支持 TypeScript,状态和方法自动推导类型,无需额外定义接口(推荐使用 TS 提升开发体验)。