普通视图

发现新文章,点击刷新页面。
今天 — 2026年5月4日首页

新加坡航空明年起将在部分飞机上部署星链Wi-Fi

2026年5月4日 10:05
新加坡航空公司表示,该公司将从2027年第一季度开始逐步推出星链的低地球轨道卫星宽带服务,预计将于2029年底完成部署。新加坡航空所有空客A350-900远程客机、A350-900超远程客机和A380客机的乘客均可享受星链高速网络连接服务。(新浪财经)

今天全国铁路预计发送旅客2030万人次

2026年5月4日 09:48
铁路方面,记者从国铁集团了解到,今天(5月4日),全国铁路预计发送旅客2030万人次,计划加开旅客列车1641列。昨天(5月3日),全国铁路发送旅客1859.8万人次,运输安全平稳有序。(央视新闻)

三星电子任命新的视觉显示业务负责人

2026年5月4日 09:37
韩国三星电子周一宣布了一项领导层调整,任命新的视觉显示业务负责人以应对当前挑战。此次调整后,原全球营销办公室负责人Lee Won-jin,将正式出任该业务部门负责人。三星在一份声明中表示:“凭借其过往的商业成就和市场洞察力,Lee Won-jin,有望带领团队扭转业务颓势,发掘新的增长点,从而进一步提升视觉显示业务的竞争力。(新浪财经)

恒指开盘涨1.4%,恒生科技指数涨1.58%

2026年5月4日 09:25
36氪获悉,恒指开盘涨1.4%,恒生科技指数涨1.58%;半导体、电气设备板块领涨,天数智芯涨超12%,宁德时代涨超3%;保健品、智能物流板块跌幅居前,顺丰同城跌超2%,华润医药跌超1%。

东阳光药甘精胰岛素获FDA批准

2026年5月4日 09:16
36氪获悉,东阳光药公告,其自主研发的甘精胰岛素注射液(商品名:Langlara)正式获美国FDA批准上市,成为第4款在美上市的甘精胰岛素产品,也是首个登陆美国市场的中国胰岛素。该产品同时获得“可互换”标签,可在药房直接替代原研药Lantus®。公司已与美国Lannett公司达成独家合作,并获得首批至少1800万支、供应期18个月的订单。此外,门冬胰岛素预计2028年在美获批,德谷胰岛素在美国的开发也在积极推进中。

游戏驿站提出近560亿美元收购eBay

2026年5月4日 09:00
据报道,游戏驿站CEO瑞安·科恩表示,他已主动提出以约560亿美元收购eBay。科恩称,游戏驿站已持有eBay约5%股份,提出以每股125美元现金加股票收购,较eBay上周五收盘价溢价约20%。(界面)

传小米新 SU7 锁单突破 7 万;微信输入法测「隔空发图」功能;豆包二代 AI 手机上半年发布

2026年5月4日 08:10

即将超越英伟达!谷歌母公司 Alphabet 市值已达 4.6 万亿美元

5 月 3 日消息,据媒体报道,受超预期财报提振,谷歌母公司 Alphabet 股价周四(4 月 30 日)大涨 10%,年内累计涨幅达到 140%,市值突破 4.6 万亿美元。

该公司周三公布的营收超出分析师预期,其中谷歌云业务收入更是突破 200 亿美元大关。

目前,全球市值第一的公司仍是人工智能芯片霸主英伟达,市值接近 4.9 万亿美元。不过,由于商业伙伴 OpenAI 被曝未能达成内部收入与增长预期,英伟达股价在两天内累计下跌超过 6%。

若英伟达在 5 月 20 日发布的财报中未能实现反弹,期權市场预测,Alphabet 最早可能在 5 月 15 日登顶全球市值第一。要实现这一目标,Alphabet 的市值需要追上英伟达当前水平,股价需再上涨约 4%,达到约 401 美元。

期权交易员认为,从现在到 5 月 15 日之间,Alphabet 股价触及 401 美元的概率约为 53%。

另据 ThinkOrSwim 数据显示,Alphabet 股价在 5 月 22 日(即英伟达财报发布后的星期五)收于 400 美元以上的概率约为 30%。

值得一提的是,Alphabet 上一次成为全球市值最高的公司是在 2016 年,当时它曾短暂超越苹果,登上榜首。(来源: 快科技)

黄仁勋称英伟达中国市场份额已降为零,美国出口管制效果适得其反

5 月 3 日消息,英伟达(Nvidia)CEO 黄仁勋 4 月 30 日在接受特别竞争研究项目(SCSP)采访时表示,该公司在中国 AI 加速器市场的份额已降至 0%(注:他这里仅谈及英伟达直接面向中国客户的销售)。

黄仁勋直言:「放弃像中国这样规模的一整个完整市场,在战略上恐怕并不合理,所以我认为这在很大程度上已经产生了反效果。我认为政策确实需要动态调整,需要保持与时俱进。我可以说,让美国芯片公司和其他美国企业留在中国市场,是非常有意义的。」

今年早些时候,伯恩斯坦(Bernstein)曾预测英伟达在中国 AI GPU 市场的份额可能从 2024 年的 66% 下降到未来几年的 8% 左右。不过根据黄仁勋的说法,这一下降趋势比预期更为剧烈。

与此同时,黄仁勋指出,即便没有美国开发的先进 AI GPU 和软件技术栈,中国在前沿 AI 模型领域仍是一个不容忽视的竞争对手。

实际上,中国开发者正越来越多地使用本土硬件,但在软件领域,尤其是所谓的「CUDA 护城河」,目前仍是美国 AI 技术的主要阵地,中国本土公司尚未完全攻克。

黄仁勋最后还警告称,威胁叙事和出口管制可能会在更宏观的层面上拖慢 AI 部署进程,而中国等其他地区正更积极地将 AI 作为经济工具加以接纳。他认为,长期的领导地位不应依赖于限制全球竞争对手,而应取决于确保美国 AI 生态系统在全球范围内占据主导地位。(来源:IT 之家)

谷歌将为 Gemini 投放广告,目前处于准备阶段

据科技媒体 Android Central 报道,谷歌母公司 Alphabet 本周举行财报电话会议,首席商务官 Philipp Schindler 在会议中表示,Gemini 未来可能会出现广告。

这名首席商务官在会议中透露:「我们需要明确,广告一直是将产品规模化、覆盖数十亿用户的重要手段。如果执行得当,广告可以非常有价值,也能提供真正有用的商业信息」。

援引 Android Central,谷歌高管说「可能出现广告」代表公司决心已定。并且业内已经有 OpenAI 为 ChatGPT 投放广告。

他也提到,目前谷歌仍在进行准备工作:「我们会在合适的时机公布计划,但不会仓促行事」。如果测试顺利,Gemini 移动端可能会出现广告。

事实上,早在去年 12 月就有传闻称,谷歌正在与广告提供商进行电话会议,预计 2026 年为 Gemini 引入广告。(来源: IT 之家)

美光 CEO 称 AI 仍处于「早期阶段」,DRAM 内存和 NAND 闪存供应持续吃紧

5 月 3 日消息,存储巨头美光科技(Micron)第二财季创下了营收、毛利率、每股收益和自由现金流的多项纪录。

美光 CEO 桑杰・梅赫罗特拉(Sanjay Mehrotra)在接受 CNBC 采访时指出,当前的 AI 浪潮仅处于「早期阶段」,随着 AI 智能体的崛起,更高速、更大容量的存储已成为支撑 AI 发挥全部能力的战略资产。

他表示,随着推理端迎来拐点,Token 生成需求的扩大对内存速度和容量提出了极高要求。然而目前存储行业正面临供应极其紧张的局面,且产能提升并非易事。

他还指出,问题不在于需求或定价,而在于供应商根本无法解决的产能问题,且展望未来,情况也不会有所好转。「目前内存供应非常紧张,而且供应无法轻易跟上,这些都能在我们的业绩中看到。」

美光预测,AI 对 DRAM 和 NAND 的需求预计将在今年超过行业总市场规模(TAM)的 50%。(来源: IT 之家)

马斯克 xAI 坐拥 55 万张英伟达 GPU 但算力利用率仅 11%,Meta 和谷歌可达 43~46%

5 月 3 日消息,据《The Information》报道,马斯克旗下人工智能公司 xAI——也就是 Grok 大模型的幕后团队,目前手头上约有 55 万块英伟达 GPU(包括 H100 与 H200),但实际利用率仅有 11%。

据介绍,这些硬件目前主要部署在孟菲斯的 Colossus 超算集群中,采用液冷配置。尽管与 Blackwell 最新一代产品相比稍显老旧,但这样的体量在全球范围内依然位居前列。

然而,如此海量的硬件并未转化为有效的计算产出。该集群的实际利用率仅有 11%。当然,这并非意味着其余 89% 的 GPU 处于完全闲置状态,而是指模型的实际浮点运算利用率远远低于理论峰值。

业内人士解释称,衡量 AI 算力效率的关键指标叫做 MFU(Model FLOPs Utilization),即模型浮点运算利用率。11% 的 MFU 意味着,理论上能产生 100 份训练吞吐量的硬件,实际只产出了 11 份,大量的电力和硬件时间都消耗在了数据等待、通信开销和重新计算等环节,而没有转化为有效的训练吞吐。

面对这一数字,xAI 总裁 Michael Nicolls 在一份内部备忘录中承认其「低得尴尬」,并为团队设定了在未来几个月内将利用率拉升至 50% 的目标。

xAI 并非个例,算力利用率偏低是整个 AI 基础设施领域的行业性难题。报道指出,在超大规模集群下,软件优化跟不上硬件部署速度是普遍现象。作为对比,Meta 和谷歌在软件堆栈上投入了大量精力,因此其 GPU 利用率相对较高,但也只有约 43% 和约 46%。(来源:IT 之家)

继「液态玻璃」之后:苹果 iOS 27 将重心转向 AI,Siri 迎来独立 App 并将深度整合到相机应用中

5 月 3 日消息,彭博社透露,继去年引入「液态玻璃」界面后,iOS 27 将以渐进式更新为主,重心聚焦于性能提升与 AI 两大领域,降低非核心功能优先级,打造更稳定、且针对 AI 深度优化的系统版本。

iOS 27 中 Siri 将迎来自诞生以来最重大的形态转变,被重塑为带有独立 App 的 AI 聊天机器人,采用极简设计,支持持续对话、历史记录查看、多任务指令处理、跨 App 联动及文件分析等功能,其底层架构基于谷歌 Gemini 技术重塑。

此外,iOS 27 将升级照片 AI 编辑工具,同时将「视觉智能」功能作为全新 Siri 模式整合到相机 App 中,新增通过相机识别食品营养信息、自动提取联系人信息等实用功能。(来源:IT 之家)

红果短剧回应「VIP 付费」:并非新增功能,仅适用于极少量版权方要求的内容

5 月 3 日消息,近期有网友反馈称,在红果短剧 App 中搜索电影《少年往事》,该影片封面左上角出现「VIP」标记,点击后仅能试看 6 分钟,随后页面提示需开通会员才能观看完整版。这一变化引发了部分用户讨论。

针对这一传闻,红果短剧相关负责人 5 月 3 日回应红星资本局称,为增加内容丰富性,满足不同用户的需求,应版权方要求,App 中确有极少量内容仅限开通 VIP 后观看,且该设置自平台上线之初即已存在,并非近期新增的功能。

公开资料显示,红果短剧是抖音集团于 2023 年 8 月正式推出的免费看剧应用,核心运营模式为「免费观看 + 广告分账」,用户通过观看广告可获得「金币」并兑换现金,平台则借助广告流量实现商业化。

依靠这一免费模式,上线不到两年的红果用户规模扩张极为迅猛,根据 QuestMobile 数据,2025 年 9 月其月活跃用户已达约 2.36 亿,超过了哔哩哔哩和优酷视频。

目前红果短剧设定的 VIP 价格体系为:7 天会员 8 元,1 个月会员 30 元,12 个月会员 260 元,暂无其他优惠折扣。(来源:IT 之家)

新一代小米 SU7 锁单突破七万,雷军否认纯靠营销,现阶段重心转向保交付

小米新一代 SU7 交出了一份答卷。根据官方最新披露的数据,新一代 SU7 的锁单量已成功突破 70000 台大关。

小米创始人雷军在此前的直播中,对友商的评价进行了正面反击。针对外界给他贴上的「营销大师」标签,雷军指出这其实是一个精心包装的话术陷阱。他表示,这种表面上的夸奖,实则是为了引导公众产生「小米只靠营销而无硬核技术与质量」的误解,本质上是想通过捧杀来消解小米汽车真正的产品竞争价值。

在直播中,雷军还分享了近期的内心挣扎。他透露,去年由于持续遭受海量负面舆情的裹挟,自己一度产生严重的抵触心理,甚至不想再面对任何直播或公开活动。但考虑到这些恶意揣测正在误导潜在消费者对小米汽车的真实认知,他最终选择强迫自己重回聚光灯下,期望用最直观的沟通,向外界传递小米在制造工艺和品质把控上的死磕精神。(来源: TechWeb)

运营三十年,老牌问答搜索引擎 Ask.com 停止运营

5 月 4 日消息,曾用名爱问吉夫斯(Ask Jeeves)的搜索引擎与问答服务网站 Ask.com 现已正式关停。

爱问吉夫斯于 1996 年首次上线,主打以自然语言解答日常口语化提问,堪称如今人工智能聊天机器人的前身雏形。然而在其近 30 年的发展历程中,始终被其他搜索引擎产品、尤其是谷歌的光芒所掩盖。

控股公司 IAC 于 2005 年收购了爱问吉夫斯,随后很快去掉了名称中的「吉夫斯(Jeeves)」字样;到 2010 年,该平台缩减搜索引擎业务规模,重新聚焦问答服务。同年,IAC 集团董事长巴里・迪勒在 TechCrunch Disrupt 上表示,Ask.com 已无法与谷歌抗衡,且在 IAC 的股价估值中也不再具备价值。

目前 Ask.com 官网发布公告称:「随着 IAC 持续精简业务、聚焦核心发展,我们决定终止旗下包括 Ask.com 在内的搜索业务。历经 25 年为全球用户答疑解惑,Ask.com 已于 2026 年 5 月 1 日正式关停。」

尽管网站已经停运,但其官网仍强调:吉夫斯的精神永不落幕。(来源:IT 之家)

豆包二代 AI 手机上半年发布:搭载第五代骁龙 8 至尊版

5 月 3 日消息,据博主「智慧芯片案内人」透露,第二代豆包 AI 手机有望在 2026 年上半年发布,核心升级为第五代骁龙 8 至尊版。

结合此前消息,第二代豆包 AI 手机依然由字节跳动与中兴通讯联合研发。

硬件端由中兴努比亚负责整机设计、制造与供应链,字节跳动主导「豆包手机助手 2.0」开发,深度集成大模型能力至操作系统底层。

豆包 AI 手机的目标是实现「AI 代为操作手机」的交互范式,而非传统 App 插件式 AI 功能。

据悉,首代豆包手机(努比亚 M153)于 2025 年 12 月以工程样机形式限量发售 3 万台,定价 3499 元,迅速售罄。

尽管因 AI 权限过高遭部分 App 厂商抵制(如微信、美团等限制其调用),但其「一句话自动比价下单」「跨应用任务执行」等能力引发行业震动。

二代机型将基于用户反馈与生态谈判成果,大幅提升产品完成度与兼容性。

消息称,新机可能与阿里系等部分主流应用厂商达成协议,在打车、外卖、订票等高频场景开放必要权限。

需要注意的是,上一代豆包手机就只是工程机,目前不确定这次的二代产品是否会开放给消费者随意购买。(来源: 快科技)

微信输入法内测隔空传送功能,支持跨设备收发照片、视频和文件

5 月 3 日消息,近日,微信输入法开始测试全新「隔空传送」功能,进一步强化跨设备文件传输能力。

想要使用该功能,双端设备均需要升级到最新内测版本(Android / iOS 3.3.0、Windows 2.0.0、MacOS 2.1.0),若你的设备暂未收到更新通知,可以在各端微信输入法帮助与反馈中发送「隔空传送」获取下载链接。

据介绍,该功能支持跨设备发送图片、视频和文件,除了可以给自己的关联设备「隔空传送」,还可以通过扫码建立连接,与其他人进行传送,无需流量。

目前该功能还在测试阶段,只有部分用户可以体验,预计不久后将推出正式版本。(来源:IT 之家)

国内首部院线 AI 原生动画电影将至,《三星堆:未来往事》获颁「龙标」

5 月 3 日消息,据北京国际电影节分享,《三星堆:未来往事》已正式获得国家电影局颁发的「龙标」,标志着国内首部将三星堆文化与 AI 原生技术深度结合的科幻院线电影即将登陆全国大银幕。

据介绍,电影《三星堆:未来往事》以三星堆文化为核心、以 AI 技术为手段,将古蜀文明与科幻叙事相结合,用 AI 技术呈现三星堆文物,构建一个连接过去与未来的科幻世界。

电影《三星堆:未来往事》概念预告片已在第 30 届香港国际影视展上正式亮相。另外,本片的创作脉络可追溯至 2024 年 7 月上线的 AI 科幻短剧集《三星堆 · 未来启示录》第一季,该短剧全网已斩获 1.6 亿播放量。

作为参考,《三星堆:未来启示录》的故事设定在科技飞速发展的近未来。地球古文明遗迹的异变引起全球古文明研究组织的高度关注,泛大西洋人工智能组织 ACE 推测三星堆文物中蕴藏着解决文明危机的关键信息,中国古文明研究组织「西安路 34 号」派出科学家吴星言监督 ACE 组织在中国的行动。四川广汉的江家三代都是三星堆考古工作者,江城联合吴星言展开了一场跨越时空的冒险。(来源:IT 之家)

 

cd Cheatsheet

Basic Syntax

Core command forms for changing directories.

Command Description
cd [DIRECTORY] Change to a directory
cd Change to your home directory
cd -- DIRECTORY Change to a directory whose name may start with -
pwd Print the current working directory

Everyday Navigation

Common ways to move around the filesystem.

Command Description
cd /etc Change to an absolute path
cd Downloads Change to a relative path
cd .. Move up one directory
cd ../.. Move up two directories
cd ./scripts Change to a directory under the current directory

Home Directories

Use shell shortcuts for your home directory and other users’ homes.

Command Description
cd ~ Change to your home directory
cd ~/Downloads Change to Downloads inside your home directory
cd ~username Change to another user’s home directory
cd "$HOME" Change to the directory stored in $HOME

Relative Paths

Build paths from your current directory.

Command Description
cd . Stay in the current directory
cd .. Move to the parent directory
cd ../src Move up one level, then into src
cd ../../var Move up two levels, then into var
cd project/docs Move through nested directories

Previous Directory

Switch between recently used directories.

Command Description
cd - Change to the previous working directory
echo "$OLDPWD" Show the previous working directory
cd "$OLDPWD" Change to the previous directory without using cd -
pushd /path Change directory and save the old one on the stack
popd Return to a directory from the stack

Paths with Spaces

Quote or escape paths that contain spaces or shell metacharacters.

Command Description
cd "Project Files" Quote a directory name with spaces
cd 'Project Files' Use single quotes for a literal path
cd Project\ Files Escape the space with a backslash
cd -- "-reports" Enter a directory whose name starts with -

Symlinks and Physical Paths

Control whether cd follows logical or physical paths.

Command Description
cd -L linkdir Follow symbolic links (default in Bash)
cd -P linkdir Resolve to the physical directory path
pwd Show the shell’s logical current directory
pwd -P Show the physical current directory
cd -P .. Move using the physical directory structure

CDPATH

Search extra base directories when changing by name.

Command Description
export CDPATH=.:~/projects:/opt Search current directory, ~/projects, and /opt
cd myapp Try matching myapp in each CDPATH entry
unset CDPATH Disable CDPATH for the current shell
CDPATH= cd myapp Run one cd command without CDPATH

Troubleshooting

Quick checks for common directory-change errors.

Issue Check
No such file or directory Verify the path with ls -ld path
Permission denied Check execute permission on the directory
Path with spaces fails Quote the path or escape spaces
cd - fails $OLDPWD is not set yet
Unexpected target with CDPATH Run unset CDPATH or use an absolute path
Symlink path looks different Compare pwd and pwd -P

Related Guides

Use these guides for detailed directory navigation workflows.

Guide Description
cd Command in Linux: Change Directories Full cd guide with examples
How to Get the Current Working Directory in Linux Use pwd and understand the current directory
pushd and popd Commands in Linux Work with the directory stack
Linux Commands Cheatsheet General Linux command quick reference

Initial Server Setup on Ubuntu 26.04

A fresh Ubuntu 26.04 server ships with root SSH access, no regular user, and no firewall rules. That works for the first login, but it is not a safe state to leave running on a public VPS.

This guide walks through the first tasks to perform on a new Ubuntu 26.04 server: creating a sudo user, enabling SSH key authentication, locking down SSH, configuring UFW, setting the hostname and timezone, and applying package updates.

Quick Reference

Task Command or file
Log in as root ssh root@server_ip_address
Create a user adduser username
Grant sudo access usermod -aG sudo username
Copy root SSH keys rsync --archive --chown=username:username /root/.ssh /home/username
Add a local key ssh-copy-id username@server_ip_address
SSH hardening file /etc/ssh/sshd_config.d/99-hardening.conf
Test SSH config sudo sshd -t
Allow SSH in UFW sudo ufw allow OpenSSH
Set hostname sudo hostnamectl set-hostname server-name
Set timezone sudo timedatectl set-timezone Europe/Berlin

Prerequisites

Before starting, make sure you have:

  • A new Ubuntu 26.04 server with a public IP address.
  • Root access over SSH, either with a password or a provider-supplied key.
  • A local SSH key pair on your workstation. If you do not have one yet, see how to generate SSH keys on Linux .
  • Access to the provider web console as a backup path in case SSH access stops working.

Keep your original root SSH session open until you have tested the new user login and the hardened SSH configuration.

Log In as Root

Open a terminal on your local machine and connect to the server using the public IP address from your hosting provider:

Terminal
ssh root@server_ip_address

Accept the host key when prompted and enter the root password if password authentication is still enabled. If your provider created the server with an SSH key, the connection should use that key automatically.

Create a New Sudo User

Working as root for daily administration is risky because every command runs with full privileges. Create a regular user account and give it administrative access through the sudo group.

Replace username with the account name you want to use:

Terminal
adduser username

The command prompts for a password and optional user details. Enter a strong password, then press Enter to skip any fields you do not need.

Add the new user to the sudo group:

Terminal
usermod -aG sudo username

The account can now run administrative commands with sudo.

Set Up SSH Key Authentication

SSH keys are safer than password logins and are easier to use once configured. The exact command depends on where your public key is currently stored.

If your public key is already present under the root account, copy the root SSH directory to the new user:

Terminal
rsync --archive --chown=username:username /root/.ssh /home/username

If you need to copy a key from your local workstation, run this command from the local machine:

Terminal
ssh-copy-id username@server_ip_address

Open a new terminal window and test the login before changing the SSH server configuration:

Terminal
ssh username@server_ip_address

The connection should succeed as the new user. Keep both the root session and the new user session open while you continue.

Disable Root Login and Password Authentication

After key-based login works, configure OpenSSH to reject direct root logins and password authentication. Ubuntu includes files from /etc/ssh/sshd_config.d/, which keeps local changes separate from the main SSH configuration file.

Create a hardening snippet:

Terminal
sudo nano /etc/ssh/sshd_config.d/99-hardening.conf

Add the following lines:

/etc/ssh/sshd_config.d/99-hardening.conftxt
PermitRootLogin no
PasswordAuthentication no

Save the file and test the SSH configuration syntax:

Terminal
sudo sshd -t

If the command prints no output, the configuration is valid. Reload SSH to apply the change:

Terminal
sudo systemctl reload ssh

Open another terminal and confirm that you can still log in as the regular user:

Terminal
ssh username@server_ip_address

Do not close your existing sessions until this test succeeds.

Set Up the Firewall with UFW

Ubuntu uses UFW (Uncomplicated Firewall) as a simple front-end for managing host firewall rules. Start by allowing SSH so the firewall does not block your current access:

Terminal
sudo ufw allow OpenSSH

Enable the firewall:

Terminal
sudo ufw enable

Confirm the prompt with y, then check the active rules:

Terminal
sudo ufw status

The output should show that OpenSSH is allowed:

output
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)

When you install services such as Nginx or Apache, open their profiles before expecting traffic to reach them. For example, an Nginx server that should accept HTTP and HTTPS traffic needs:

Terminal
sudo ufw allow 'Nginx Full'

For more examples, see how to set up a firewall with UFW .

Set the Hostname

A descriptive hostname makes logs, shell prompts, monitoring alerts, and dashboards easier to read. Set the hostname with hostnamectl:

Terminal
sudo hostnamectl set-hostname server-name

Replace server-name with a short name that matches the server role, such as web-01 or db-01.

Check the result:

Terminal
hostnamectl

You can update DNS records or your local SSH config separately if you want to connect by name instead of IP address.

Set the Timezone

Set the server timezone so logs, cron jobs, and timestamps match the region you use for operations:

Terminal
sudo timedatectl set-timezone Europe/Berlin

List available zones if you are unsure of the exact name:

Terminal
timedatectl list-timezones

See how to set or change the timezone on Ubuntu for a deeper explanation.

Update the System

Refresh the package index and install pending updates:

Terminal
sudo apt update
sudo apt upgrade

If the upgrade installed a new kernel or core system libraries, reboot the server:

Terminal
sudo reboot

After the reboot, reconnect as the regular sudo user:

Terminal
ssh username@server_ip_address

Troubleshooting

Locked out after disabling password authentication
Use your provider web console or recovery mode to log in. Edit /etc/ssh/sshd_config.d/99-hardening.conf, temporarily set PasswordAuthentication yes, run sudo sshd -t, reload SSH, and test key login again before disabling passwords.

usermod: group 'sudo' does not exist
Some minimal images may not include the sudo package. Install it with apt install sudo, then rerun usermod -aG sudo username.

sshd -t reports an error
Read the line number in the error message, fix the snippet in /etc/ssh/sshd_config.d/99-hardening.conf, and run sudo sshd -t again. Do not reload SSH until the syntax test passes.

UFW blocks an expected service
Check the active rules with sudo ufw status. Allow the needed service profile or port, such as sudo ufw allow 'Nginx Full' for Nginx web traffic, then test the connection again.

Conclusion

You now have an Ubuntu 26.04 server with a sudo user, key-based SSH access, direct root logins disabled, a basic firewall, and current packages. A good next step is to enable automatic security updates before installing the rest of your stack.

每日一题-旋转图像🟡

2026年5月4日 00:00

给定一个 × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

 

示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]

示例 2:

输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]

 

提示:

  • n == matrix.length == matrix[i].length
  • 1 <= n <= 20
  • -1000 <= matrix[i][j] <= 1000

 

Java和JavaScript的关系真是雷峰和雷峰塔的关系吗?

作者 Linsk
2026年5月3日 18:47

前端圈一直流传着一个经典段子:Java和JavaScript是什么关系?就是雷峰和雷峰塔的关系。听过后令人会心一笑。但静下来想想🤔,真是这样吗?

什么是雷峰和雷峰塔的关系

雷峰(人)和雷峰塔(建筑)的关系非常明确:除了名字读音相似之外,两者在血缘、历史、物理构成等任何维度上,都百分之百毫无关联。

那么,Java和Javascript是否只是名字有点相似,实则毫无关系呢?

Java和Javascript的关系

如果抛开段子,翻开真实的计算机史,你会发现Java和JavaScript不仅不是“毫无关系”,反而有着千丝万缕的渊源。

Javascript是Sun和Netscape联合发布的,Sun(现在是Oracle)是Javascript的商标持有者。

时间回到1995年,网景公司(Netscape)为了在浏览器里加入交互能力,搞出了一门脚本语言(最初叫Mocha,后改LiveScript)。当时Sun公司推出的Java语言正如日中天,被媒体炒作战无不胜的“神器”。网景为了蹭上这波热度,与Sun公司达成了战略合作,将这门语言正式更名为JavaScript
更硬核的事实是:直到今天,JavaScript的商标权依然掌握在Sun的继承者甲骨文(Oracle)手里。如果是毫无关系的两者,怎么可能共用一个具有法律效力的名字?

JavaScript就是按像Java设计的

网景公司在给语言改名的同时,也给开发者(Brendan Eich)提出了一个明确的需求:“让它的语法看起来像Java”。因此,JavaScript在诞生之初,大量借鉴了Java的基础语法结构。它的 if/else 分支、for 循环结构、try/catch 异常处理机制,甚至是 new 关键字的使用,看起来和Java几乎如出一辙。因此,JavaScript 不是巧合像,是故意设计成像 Java

Java中内置了JavaScript运行时

从JDK 6引入Rhino引擎,到JDK 8内置Nashorn引擎(后在JDK 15中移除),再到如今通过GraalVM JS等现代方案实现深度互操作,Java官方生态长期保持着对JavaScript运行时的支持。这意味着,你完全可以在Java程序里直接调用JavaScript代码,把它们当作业务中的“动态脚本层”。这绝不是两座毫无交集的孤岛,两者在运行层面长期深度集成。

Java脚本化后就是JavaScript的样子

如果说设计一门Java的脚本语言,要类似Java的语法,但是要脚本语言的特性,要能解释执行、方便灵活、宽松,还要高扩展性。那么设计出来就是JavaScript这个样子。

这是一个非常有趣的逻辑推导。假设1995年你需要为Java生态设计一门“附属脚本语言”,你的需求清单是这样的:

  • 融入Java生态:语法必须像Java;
  • 运行机制:不需要编译,直接解释执行,轻量级;
  • 类型系统:不能像Java那么严苛,要宽松、动态,写起来方便;
  • 高扩展性:面对复杂多变的Web环境,必须允许开发者随时往内置类中添加方法;

当你按照这份需求文档写出一门语言时,恭喜你,你重新发明了JavaScript。它从一出生,就是带着“ Java的轻量化脚本兄弟 ”这个定位来的。

JavaScript和Java是两门不同的语言,但是不代表毫无关系。

有些人觉得JavaScript还是不够 “像” Java,比如JavaScript的类的实现是基于原型链的,和Java类有本质不同。对此我想说,脚本语言和编译型的语言本来就是为不同场景设计的。JavaScript和Java的差异确实足够大,大到应当作为两门不同语言分别学习,但是不能否认它们的历史渊源。JavaScript和Java的差异更多的可以用不同场景设计来解释。比如原型链问题,在Java中,你的API更新了,你只要升级JDK;而浏览器环境上应当让内置类有较高的扩展性,原型链无疑是最优解,让你重新设计一遍Javascript你也会设计成这样。

总结

Java 和 JavaScript 并非毫无关系,把它俩比作雷锋和雷峰塔,实在是冤枉了这两门语言。它俩的关系更接近于 VB 和 VBScript 的关系:VB 是微软推出的完整版编译型语言,适合开发大型桌面应用,VBScript 则是基于 VB 语法设计的轻量级脚本语言,灵活简洁,用于自动化、网页脚本,二者语法同源、定位互补,是同体系下不同分工的语言。

微软推出JScript

故事的走向在浏览器大战时期变得更加复杂。微软为了让自家的Internet Explorer浏览器兼容已有网站,迅速搞出了一个 JScript。JScript虽然名字刻意避开了“Java”字眼,但就是为了兼容JavaScript而生的,可以看作微软的JavaScript。但由于JavaScript并非开放标准,微软是照着Netscape的JavaScript行为猜着做的,只能仿个大概,对边界场景可能存在不一致。

EcmaScript出现

为了推动Web标准化,1996年,网景将JavaScript提交给了欧洲计算机制造商协会(ECMA)进行标准化。第二年,ECMA出台了ECMA-262标准,这便是大名鼎鼎的 ECMAScript(简称ES)。

从此,技术界有了一个清晰的共识:JScript和JavaScript,本质上都只是ECMAScript标准的不同实现。 这个标准的诞生,把JavaScript从网景和微软的商业战中抽离出来,成为了一门真正开放的语言。

近年来ES朝着越来越不像Java的方向发展

随着Web标准化,ECMAScript已经不是Sun或Oracle可以控制的。如今ECMAScript的更新由TC39委员会主导,采用五阶段提案流程。如今ECMAScript的发展已经偏离的像Java的目的,比如Map/Set的API刻意避开了Java的命名规范。ES刻意都划清了界限。这导致JavaScript作为ES最核心的实现,如今越来越不像Java。

因此我更愿意把现在的js叫es,而不是否定Javascript和Java的关系

我知道有些人极度反感Java,甚至否定Javascript和Java的关系。对于这种掩耳盗铃的行为,我倒有个建议:既然这么嫌弃,不如彻底抛弃“JavaScript”这个带Java基因的名字,以后只准叫它ECMAScript。叫它ES,确实是对它如今独立设计哲学的最好宣告,证明它不再是任何人的附庸。也顺理成章地把“JavaScript”这个名字,留给那些真正需要“Java脚本化”的人。

浏览器文本复制到剪贴板:企业级最佳实践

2026年5月3日 17:27

1. 背景与需求分析

在 Web 开发中,复制文本到剪贴板是一个常见需求,比如:

  • 复制分享链接、邀请码
  • 复制代码片段
  • 一键复制表单内容

现代浏览器提供了 navigator.clipboard API,但存在兼容性和安全上下文的限制;传统的 document.execCommand('copy') 虽然兼容性更好,但使用方式较为繁琐。本质上,我们需要一个统一的工具函数来屏蔽这些差异。

2. API 介绍与演进

2.1 传统方案:document.execCommand

const textarea = document.createElement('textarea')
textarea.value = content
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)

优点:兼容性好,支持所有主流浏览器 缺点:需要创建临时 DOM 元素,代码冗长

2.2 现代方案:navigator.clipboard

await navigator.clipboard.writeText(content)

优点:简洁直观,直接操作剪贴板 缺点:需要安全上下文(HTTPS),部分浏览器支持受限

3. 核心实现解析

export interface CopyTextOptions {
  /** 是否允许复制空白内容(空字符串或纯空格),默认 false */
  allowWhitespace?: boolean
  /** 是否使用旧版复制方法(不支持空白内容复制),默认 false */
  legacy?: boolean
}

export interface CopyTextReturn {
  success: boolean
  message: string
}

export async function copyText(content: string, options: CopyTextOptions = {}): Promise<CopyTextReturn> {
  try {
    const { allowWhitespace = false, legacy = false } = options
    if (!allowWhitespace && (!content || content.trim() === '')) {
      return { success: false, message: '复制内容不能为空' }
    } else if (navigator.clipboard && window.isSecureContext && !legacy) {
      await navigator.clipboard.writeText(content)
    } else {
      const textarea = document.createElement('textarea')
      textarea.style.cssText = 'position:fixed; opacity:0; z-index:-9999; left:-9999px; top:-9999px;'
      textarea.value = content
      document.body.appendChild(textarea)
      textarea.select()
      textarea.setSelectionRange?.(0, content.length)
      const copied = document.execCommand('copy')
      document.body.removeChild(textarea)
      if (!copied) throw new Error('浏览器限制或无法复制')
    }
    return { success: true, message: '复制成功' }
  } catch (error: unknown) {
    const errMsg = error instanceof Error ? error.message : '未知错误'
    return { success: false, message: `${errMsg}` }
  }
}

关键逻辑说明

参数一:allowWhitespace

控制是否允许复制空白内容。默认 false 会过滤空字符串和纯空格内容,避免用户误操作。

参数二:legacy

强制使用传统 execCommand 方案。某些场景下(如在 iframe 内)可能需要降级处理。

优先级判断

navigator.clipboard 可用?
  └─ 是 → 判断 isSecureContext(安全上下文)
           └─ 是 → 使用现代 API
           └─ 否 → 降级到 execCommand
  └─ 否 → 降级到 execCommand

4. 兼容性处理策略

方案 兼容性 安全要求 代码复杂度
navigator.clipboard 现代浏览器 必须 HTTPS 简洁
execCommand 所有浏览器 较繁琐
// 降级逻辑核心代码
const textarea = document.createElement('textarea')
textarea.style.cssText = 'position:fixed; opacity:0; z-index:-9999; left:-9999px; top:-9999px;'
textarea.value = content
document.body.appendChild(textarea)
textarea.select()
textarea.setSelectionRange?.(0, content.length) // 兼容 iOS Safari
const copied = document.execCommand('copy')
document.body.removeChild(textarea)

iOS Safari 兼容要点setSelectionRange 在 iOS 设备上需要显式调用才能正确选中文本。

5. 安全上下文要求

navigator.clipboard 要求页面必须处于安全上下文:

  • HTTPS 协议
  • localhost 开发环境
  • Chrome Extension 内部页面

开发环境下通常没问题,但部署到生产环境务必确保使用 HTTPS,否则会自动降级到传统方案。

6. 使用场景与示例

6.1 基础用法

const result = await copyText('hello world')
if (result.success) {
  console.log('复制成功')
} else {
  console.error(result.message)
}

6.2 允许空白内容

// 复制可能为空的文本时
const result = await copyText(userInput, { allowWhitespace: true })

6.3 强制使用传统方案

// 在特殊场景下强制降级
const result = await copyText(content, { legacy: true })

6.4 集成提示组件

注释掉的 TipModal 部分可根据项目实际使用的 UI 库进行适配:

// Element Plus 示例
import { ElMessage } from 'element-plus'

if (!allowWhitespace && (!content || content.trim() === '')) {
  ElMessage.error('复制内容不能为空')
  return { success: false, message: '复制内容不能为空' }
}

// 复制成功后
ElMessage.success('复制成功')

7. 核心总结

copyText 函数的核心设计要点:

  • 自动降级:优先使用 navigator.clipboard,不支持时自动降级到 execCommand
  • 安全优先:判断 isSecureContext 确保在安全环境下使用现代 API
  • 灵活配置:通过 allowWhitespacelegacy 参数适配不同业务场景
  • 统一返回:返回 { success, message } 结构化结果,便于调用方处理

这个不到 50 行的工具函数覆盖了浏览器复制场景的绝大多数需求,可直接集成到项目中。

10_从 React Hooks 本质看 useState

2026年5月3日 17:22

一、Hooks 的本质

Hooks 是挂在 Fiber 上的一条“有序链表”,通过“调用顺序”来定位状态

每个函数组件对应一个 Fiber:

type Fiber = {
  memoizedState: Hook | null; // Hook 链表头
}

对于一个 Hook,有三种类型的 dispatcher(可以认为是操作策略):

/* 函数组件初始化用的 hooks */
// 初始化信息挂载到 fiber 上
const HooksDispatcherOnMount: Dispatcher = {
  ...
  useCallback: mountCallback,
  useEffect: mountEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  ...
};

/* 函数组件更新用的 hooks */
// 组件更新执行对应的方法,更新 fiber 信息
const HooksDispatcherOnUpdate: Dispatcher = {
  ...
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  ...
};

/* 当 hooks 不是函数组件内部调用或者嵌套 hooks 等“非正确使用”情况,调用这些报错相关的 dispatcher */
const ContextOnlyDispatcher: Dispatcher = {
  ...
  useCallback: throwInvalidHookError,
  useContext: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  useMemo: throwInvalidHookError,
  useReducer: throwInvalidHookError,
  useRef: throwInvalidHookError,
  useState: throwInvalidHookError,
  ...
};

二、Hook 的数据结构

type Hook = {
  memoizedState: any; // 当前值
  baseState: any;
  queue: UpdateQueue | null;
  next: Hook | null;
}

注意:这里要和 FiberNode 的 memoizedState 区分开:

  • FiberNode.memoizedState:保存的是 Hook 链表里面的第一个链表
  • hook.memoizedState:某个 Hook 自身的数据
Fiber.memoizedState
   ↓
[useState] → [useEffect] → [useMemo] → null

完全依赖调用顺序!

不同的 hook,memoizedState 所存储的内容不同:

  • useState:对于 const [state, updateState] = useState(initialState),memoizedState 保存的是 state 的值
  • useReducer:对于 const [state, dispatch] = useReducer(reducer, { } ),memoizedState 保存的是 state 的值
  • useEffect:对于 useEffect( callback, [...deps] ),memoizedState 保存的是 callback、[...deps] 等数据
  • useRef:对于 useRef(initialValue),memoizedState 保存的是 { current: initialValue}
  • useMemo:对于 useMemo( callback, [...deps] ),memoizedState 保存的是 [callback( )、[...deps]] 数据
  • useCallback:对于 u seCallback( callback, [...deps] ),memoizedState 保存的是 [callback、[...deps]] 数据
  • useContext:不需要 memoizedState 保存自身数据

三、执行流程(mount 阶段)

1️⃣ render 开始

function Component() {
  const [count, setCount] = useState(0);
}
// render 开始先执行
export function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
  renderLanes = nextRenderLanes;
  currentlyRenderingFiber = workInProgress;

  // 每一次执行函数组件之前,先清空 FiberNode 状态 (用于存放 hooks 列表)
  workInProgress.memoizedState = null;
  // 清空更新队列(用于存放 effect 列表)
  workInProgress.updateQueue = null;
  // ...
  // 根据不同的组件状态初始化不同的 dispatcher 对象和上下文
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;

  // 执行函数组件,所有的 hooks 将依次执行
  let children = Component(props, secondArg);

  // ...
  
  // 兜底
  finishRenderingHooks(current, workInProgress);
  return children;
}

function finishRenderingHooks(current, workInProgress) {
    // 防止 hooks 在不合规的情况下调用,如果调用直接报错
    ReactCurrentDispatcher.current = ContextOnlyDispatcher;
    // ...
}

2️⃣ mountState 做了什么

如果组件是挂载阶段:

function mountWorkInProgressHook() {
  const hook = {
    memoizedState: null,  // Hook 自身的状态
    baseState: null,
    baseQueue: null,
    queue: null, // hook 自身队列
    next: null, // next 指向下一个 hook
  };

  // 判断当前的 hook 是否是链表的第一个
  if (workInProgressHook === null) {
    // 如果当前组件的 Hook 链表为空,那么就将刚刚新建的 Hook 作为 Hook 链表的第一个节点(头结点) 
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // 果当前组件的 Hook 链表不为空,那么就将刚刚新建的 Hook 添加到 Hook 链表的末尾(作为尾结点)
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

function mountStateImpl(initialState) {
  // 获取 hook 对象
  const hook = mountWorkInProgressHook();
  
  //...
  
  // 初始化 memoizedState 
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer, // useState 内置的 reducer
    lastRenderedState: (initialState: any),
  };
  // 初始化 queue
  hook.queue = queue;
  return hook;
}

function mountState(initialState) {
  // 获取 hook 对象
  const hook = mountStateImpl(initialState);
  const queue = hook.queue;
  const dispatch = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ));
  // 初始化 dispatch (dispatch 就是用来修改状态的方法)
  queue.dispatch = dispatch;
  // 返回 [当前状态, dispatch函数]
  return [hook.memoizedState, dispatch];
}

其实 useReducer 和 useState 非常像,在源码层面:

  1. mount 阶段:mountState 和 mountReducer 的大体流程是一样的。但是有一个区别,mountState 的 queue 里面的 lastRenderedReducer 对应的是 basicStateReducer,而 mountReducer 的 queue 里面的 lastRenderedReducer 对应的是开发者自己传入的 reducer,这里说明了一个问题,useState 的本质就是 useReducer 的一个简化版,只不过在 useState 内部,会有一个内置的 reducer
  2. update 阶段:在 update 阶段,updateState 内部直接调用的就是 updateReducer,传入的 reducer 仍然是 basicStateReducer。
function mountReducer(reducer, initialArg, init) {
  // 创建 hook 对象
  const hook = mountWorkInProgressHook();
  let initialState;
  // 如果有 init 初始化函数,就执行该函数,并将执行的结果赋值给 initialState
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = initialArg;
  }
  // 赋值给 hook 对象的 memoizedState
  hook.memoizedState = hook.baseState = initialState;

  const queue = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: reducer, // 手动传入的 reducer
    lastRenderedState: initialState,
  };
  hook.queue = queue;
  const dispatch = (queue.dispatch = dispatchReducerAction.bind(
    null,
    currentlyRenderingFiber,
    queue
  ));
  return [hook.memoizedState, dispatch];
}

3️⃣ 构建 Hook 链表

第一次 render:

Fiber.memoizedState → Hook1 → Hook2 → Hook3

举个例子~

该示例来源:渡一教育。

function App() {
  const [number, setNumber] = React.useState(0); // 第一个hook
  const [num, setNum] = React.useState(1); // 第二个hook
  const dom = React.useRef(null); // 第三个hook
  React.useEffect(() => {
    // 第四个
    hookconsole.log(dom.current);
  }, []);
  return (
    <div ref={dom}>
    <div onClick={() => setNumber(number + 1)}> {number} </div>
    <div onClick={() => setNum(num + 1)}> {num}</div></div>
  );
}

四、更新阶段(update)

不再创建 Hook,而是“复用”

function updateWorkInProgressHook(){
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    // 从 alternate 上获取到 fiber 对象
    const current = currentlyRenderingFiber.alternate;
    
    // 获取第一个 hook
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    // 获取下一次 hook
    nextCurrentHook = currentHook.next;
  }

  // workInProgressHook 会指向下一个要工作的 hook
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // 已经存在,直接复用
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    // Clone from the current hook.
    // 如果 nextWorkInProgressHook 不为 null,那么就会复用之前的 hook
    // 划重点!!!
    // 更新的过程中,如果通过条件语句增加或者删除了 hook,复用的时候就会产生当前 hook 的顺序和之前 hook 的顺序不一致的问题
    if (nextCurrentHook === null) {
      const currentFiber = currentlyRenderingFiber.alternate;
      if (currentFiber === null) {
        // This is the initial render. This branch is reached when the component
        // suspends, resumes, then renders an additional hook.
        // Should never be reached because we should switch to the mount dispatcher first.
        throw new Error(
          'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
        );
      } else {
        // This is an update. We should always have a current hook.
        throw new Error('Rendered more hooks than during the previous render.');
      }
    }

    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null,
    };

    if (workInProgressHook === null) {
      // This is the first hook in the list.
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      // Append to the end of the list.
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

function updateReducer() {
  const hook = updateWorkInProgressHook();
  return updateReducerImpl(hook, ((currentHook)), reducer);
}

function updateState<S>(initialState) {
  return updateReducer(basicStateReducer, initialState);
}

接着上面的示例~

示例来源:渡一教育。

function App({ showNumber }) {
  let number, setNumber
  showNumber && ([ number,setNumber ] = React.useState(0)) // 第一个hooks
  const [num, setNum] = React.useState(1); // 第二个hook
  const dom = React.useRef(null); // 第三个hook
  React.useEffect(() => {
    // 第四个hook
    console.log(dom.current);
  }, []);
  return (
    <div ref={dom}>
    <div onClick={() => setNumber(number + 1)}> {number} </div>
    <div onClick={() => setNum(num + 1)}> {num}</div></div>
  );
}

假设第一次父组件传递过来的 showNumber 为 true,此时就会渲染第一个 hook;第二次渲染的时候,假设父组件传递过来的是 false,那么第一个 hook 就不会执行,那么逻辑就会变得:

第一次:useState -> useState

第二次:useState -> useRef

体现在我们开发者眼中就是报错。

五、setState 到底做了什么?

dispatch 流程

function dispatchSetState(action) {
  const update = {
    action,
    next: null
  };

  enqueueUpdate(queue, update);

  scheduleUpdateOnFiber(fiber);
}

UpdateQueue 结构

hook.queue
   ↓
update1 → update2 → update3(环形链表)

执行更新

function processUpdateQueue(queue) {
  let state = baseState;

  queue.forEach(update => {
    state = reducer(state, update.action);
  });

  return state;
}

六、调度机制(Hooks 如何触发更新)

scheduleUpdateOnFiber(fiber)
setState
   ↓
scheduleUpdate
   ↓
标记 lane(优先级)
   ↓
render(可中断)
   ↓
commit(不可中断)

Hooks 如何保证并发下的 hooks 行为正确?

关键:

  • 每次 render 都重新走一遍 Hook 链
  • 不依赖“执行次数”,只依赖“顺序”

Prisma 实战指南:像搭积木一样设计古诗词数据库

作者 Lee川
2026年5月3日 15:40

Prisma 实战指南:像搭积木一样设计古诗词数据库

在传统后端开发中,与数据库打交道往往意味着要编写大量晦涩的 SQL 语句。而 Prisma 就像一位精通多国语言的“翻译官”,它通过 ORM(对象关系映射)技术,将数据库的表映射为代码中的类,将行映射为实例。你不再需要手写 INSERTSELECT,只需像操作普通对象一样 createfindMany,Prisma 就会在幕后为你翻译成精准的 SQL。

接下来,我们就结合一个“古诗词社区”的实际项目,从零开始体验 Prisma 的魅力。

一、环境搭建与初始化

首先,我们需要为项目安装 Prisma 的核心依赖。建议锁定版本以避免兼容性问题:
pnpm i prisma@6.19.2
pnpm i @prisma/client@6.19.2

依赖安装完毕后,执行 npx prisma init。这条命令会为你生成两个关键文件:.env(存放环境变量)和 prisma/schema.prisma(数据库设计蓝图)。

打开 .env,填入你的 PostgreSQL 连接字符串,例如:
DATABASE_URL="postgresql://postgres:369369@localhost:5432/xue?schema=public"

二、Schema 设计:绘制数据库蓝图

schema.prisma 是整个 ORM 的灵魂。在这个文件中,我们通过 model 来定义数据表。让我们结合古诗词项目的实际设计,看看几个核心模型是如何构建的:

1. 基础配置与用户模型
文件头部定义了生成器和数据源,告诉 Prisma 我们要生成 JS 客户端并连接 PostgreSQL。

generator client {
  provider = "prisma-client-js"
}
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
model User {
  id        Int      @id @default(autoincrement())
  name      String   @unique @db.VarChar(255)
  password  String   @db.VarChar(255)
  // 使用 @map 将驼峰字段映射为数据库的下划线命名
  createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
  updatedAt DateTime? @default(now()) @map("updated_at") @db.Timestamptz(6)
  // 一对多关系:一个用户可以有多篇文章、评论、点赞等
  posts     Post[]
  comments  Comment[]
  likes     UserLikePost[] 
  files     File[]
  avatars   Avatar[]
  @@map("user") // 将表名映射为单数 user
}

2. 核心业务与级联策略
Post(诗词文章)模型中,我们看到了外键关联与删除策略的精妙配合:

model Post {
  id       Int     @id @default(autoincrement())
  title    String  @db.VarChar(255)
  content  String? @db.Text
  userId   Int?             
  // 关联 User,并设置 onDelete: SetNull
  // 意为:如果作者被删除,文章保留但作者ID置空
  user     User?   @relation(fields: [userId], references: [id], onDelete: SetNull) 
  comments Comment[]
  tags     PostTag[]
  @@index([userId]) // 为外键添加索引,提升查询效率
  @@map("posts")
}

3. 复杂关联:自关联与复合主键
古诗词社区少不了评论互动与标签分类,这里用到了两个高级技巧:

  • 自关联(评论回复) :在 Comment 模型中,通过 parentId@relation("CommmentToComment") 实现了评论的层级回复(父评论与子评论)。
  • 复合主键(多对多中间表)PostTag(文章标签)和 UserLikePost(用户点赞)作为中间表,使用 @@id([postId, tagId]) 定义了复合主键。这确保了“一篇文章不能被重复打同一个标签”以及“一个用户不能重复点赞同一篇文章”的业务逻辑。

三、迁移与可视化:让设计落地

设计好 Schema 后,我们需要将其同步到真实的数据库中。

  1. 数据迁移:执行 npx prisma migrate dev --name init_user。Prisma 会自动对比当前数据库结构,生成 SQL 迁移文件并执行,同时在数据库中记录版本日志。这不仅方便团队协作,也方便后续的版本回滚。
  2. 可视化操作:执行 npx prisma studio。这会打开一个精美的图形化界面,你可以在浏览器中直观地查看 UserPost 等表的数据,甚至手动添加测试数据(Seeds),完全告别黑乎乎的命令行。

四、代码操作:告别 SQL

当一切准备就绪,你就可以在代码中通过 Prisma Client 优雅地操作数据了。例如,查询李白发布的所有诗词:

const libaiPosts = await prisma.post.findMany({
  where: { user: { name: 'libai' } },
  include: { tags: true } // 顺带查出文章标签
});

从安装配置到模型设计,再到最终的代码调用,Prisma 用类型安全和高度抽象的 API,将开发者从繁琐的 SQL 中彻底解放了出来。

你的网页慢,用户不说直接走——前端性能监控教你“读心术”

作者 kyriewen
2026年5月3日 12:09

你上线了一个页面,自认为飞快。但用户那边转圈转了三秒,走了。你浑然不知。今天我们来装一套“网页心电图仪”——前端性能监控。它能告诉你:用户打开你的网站,到底有多卡?哪里卡?卡的人多不多?不用等用户骂你,你就知道该优化哪了。

前言

性能优化不是“我觉得快”,而是“数据证明快”。没有监控的优化,就像闭着眼睛射箭——中了是运气,脱靶是常态。

Google 定义了三个核心指标(Core Web Vitals):LCP(加载速度)、FID(交互响应)、CLS(视觉稳定)。加上我们自己业务关心的指标(比如首屏时间、API耗时),组合起来就是你的“网页健康报告”。

今天我们就来搭一套轻量级前端性能监控,从采集到上报,再到报警,一条龙。

一、三大核心指标:你的网页“体检三项”

LCP(最大内容绘制):加载速度的“裁判”

LCP 测量页面主要内容(比如大图、标题、视频)加载完成的时间。理想值:2.5秒以内

什么算“主要内容”?就是用户第一眼看到的那个最大的元素。可能是背景图,可能是大标题,也可能是视频封面。

FID(首次输入延迟):交互响应的“秒表”

用户第一次点击、触摸或按键,到浏览器真正开始处理的时间。理想值:100毫秒以内

如果你的JS主线程被长任务阻塞,用户点了按钮没反应,FID就会高。用户会觉得“这网站卡死了”。

CLS(累计布局偏移):视觉稳定的“防抖测试”

页面加载过程中,元素突然位移(比如图片加载出来把按钮挤下去了)。理想值:0.1以内

CLS 高,用户容易点错按钮,比如本来要点“购买”,结果图片加载完,按钮被挤开,点到了“不感兴趣”。

二、怎么采集这些指标?用 web-vitals

Google 官方提供了 web-vitals 库,几行代码就能拿到 LCP、FID、CLS。

npm install web-vitals
import { getLCP, getFID, getCLS } from 'web-vitals';

function sendToAnalytics({ name, value, id }) {
  // 上报到你的后端或第三方服务
  navigator.sendBeacon('/api/perf', JSON.stringify({ name, value, id }));
}

getLCP(sendToAnalytics);
getFID(sendToAnalytics);
getCLS(sendToAnalytics);

注意:这些指标需要在页面加载完成后才能拿到,而且可能会多次更新(比如CLS会在整个页面生命周期中变化)。你可以选择只上报最终值,或者每次变化都上报(去重)。

三、其他重要指标:你自己更关心什么?

  • TTFB(首字节时间):从请求到服务器返回第一个字节。影响LCP,但更偏后端。
  • DOM Ready / Load 时间:传统指标,用于对比。
  • 首屏时间(自定义):比如你的页面有一个“主要内容区”,可以通过 MutationObserver 监听该区域出现的时间。
// 手动打点
const start = performance.now();
// 某个关键组件渲染完成后
const end = performance.now();
report('custom:firstContent', end - start);
  • API 响应时间:在 axios 拦截器里记录每个接口耗时。

四、上报策略:别把服务器打满

性能指标上报不能像打点日志那么频繁。策略:

  • 只上报一部分用户(采样),比如 10%。用随机数或用用户ID哈希。
  • 批量上报:收集多个指标,页面关闭前一次性发走(用 sendBeacon)。
  • 避免阻塞:用 requestIdleCallbacksetTimeout 低优先级上报。
if (Math.random() > 0.9) { // 10%采样
  navigator.sendBeacon('/api/perf', JSON.stringify(data));
}

五、报警与可视化:指标变差,立刻知道

光采集不上报等于没采。你需要一个后端接收数据,然后做可视化+报警。

  • 自建:用 Node + ClickHouse(或 InfluxDB)存储,Grafana 展示,设置阈值报警(比如 LCP P95 > 3s 发钉钉)。
  • 第三方Sentry(也能做性能监控)、DataDogGoogle Analytics(有 Core Web Vitals 报告)、阿里云ARMS

最简单的起步:把数据发到 Google Analytics(GA4),它有现成的 Web Vitals 报告。

import { getCLS, getFID, getLCP } from 'web-vitals';
import ga4 from 'react-ga4';

function sendToGA({ name, value, id }) {
  ga4.event('web_vitals', {
    event_category: 'Web Vitals',
    event_label: id,
    value: Math.round(name === 'CLS' ? value * 1000 : value),
    non_interaction: true,
  });
}

六、实战:完整的前端性能监控 SDK(简化版)

class PerfMonitor {
  constructor(options) {
    this.endpoint = options.endpoint;
    this.sampleRate = options.sampleRate || 0.1;
    this.init();
  }
  shouldReport() {
    return Math.random() < this.sampleRate;
  }
  send(data) {
    if (!this.shouldReport()) return;
    navigator.sendBeacon(this.endpoint, JSON.stringify(data));
  }
  init() {
    // Web Vitals
    import('web-vitals').then(({ getLCP, getFID, getCLS }) => {
      getLCS(metric => this.send(metric));
      getFID(metric => this.send(metric));
      getCLS(metric => this.send(metric));
    });
    // 自定义首屏时间
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', () => {
        this.send({ type: 'domReady', value: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart });
      });
    }
    // 页面卸载时发送未发送的数据(可用Beacon队列)
  }
}
new PerfMonitor({ endpoint: '/api/perf', sampleRate: 0.1 });

七、常见坑点

  • CLS 在后台标签页不准确:用户切换标签页时,CLS 可能会误报。只在页面可见时收集。
  • 移动端 vs PC:指标分开统计,因为网络和设备差异大。
  • 缓存影响:已缓存的页面 LCP 会很快,应该区分首次访问和二次访问。

八、总结:让数据驱动你的优化

  • 监控 LCP、FID、CLS,用 web-vitals
  • 加上业务自定义指标(首屏时间、API 耗时)。
  • 采样上报,避免压力过大。
  • 用 GA4 或自建系统可视化+报警。
  • 定期查看指标趋势,倒退时立刻优化。

有了性能监控,你不再是“我觉得快”,而是“数据证明快”。老板问要不要优化,你甩出图表:“LCP 最近一周从2.1秒涨到3.5秒,用户流失率上升5%,建议立即优化图片。” 这才叫专业。

❌
❌