阅读视图
nginx 如何配置防止慢速攻击 🤔🤔🤔
最近在出一个前端的体系课程,里面的内容非常详细,如果你感兴趣,可以加我 v 进行联系 yunmz777
:
浪费你几秒钟时间,内容正式开始
慢速攻击是一类用很少带宽就能长期占用服务器连接/资源的攻击方式。攻击者通过非常慢地发送请求头或请求体,或极慢地读取服务器响应,让每个连接都“挂着不结束”,从而耗尽 Web 服务器(或上游应用、数据库、代理)的并发与缓冲资源。
典型类型主要有以下几个方面:
-
Slowloris(慢请求头):客户端以极低速率分片发送 HTTP 头部,始终不把头部发完,服务器就一直等待。
-
Slow POST / RUDY(慢请求体):先宣称要上传较大的
Content-Length
,然后以极慢速率发送请求体,服务器为其保留缓冲与上游连接。 -
Slow Read(慢读取响应):客户端窗口/读取速率极低,迫使服务器缓冲并保持连接很久(尤其响应较大时)。
-
HTTP/2 变种:滥用单连接多流(streams)和窗口控制:开很多流但每流都很慢,放大资源占用。
传统 DDoS 依靠高带宽与高包速直接压垮网络/设备,而慢速攻击用极低带宽长期占用连接,更隐蔽,常被误认为是网络状况差的正常用户。
现场通常会看到活跃连接(尤其 reading
)持续攀升,但总体带宽并不高。
error_log
中频繁出现 client timed out
、client sent invalid header while reading client request headers
等信息。
上游服务看似空闲却体验发卡,Nginx 的 429/502/504
增多,访问日志还能发现同一 IP 维持大量长期未完成的请求或异常长的响应时间。
-
429 表示“请求过多被限流”,通常稍后或按
Retry-After
重试即可。 -
502 表示“网关收到上游无效响应或连不上上游”,多见于上游挂掉、拒连或协议不匹配。
-
504 表示“等待上游超时”,通常是上游处理太慢一直没回。
如何防护
核心目标就是尽快关闭拖延发送请求头/请求体或极慢读取响应的连接,限制单 IP 的并发与速率,避免慢连接占满 worker
的 worker_connections
与上游资源。
-
收紧超时:
client_header_timeout
、client_body_timeout
、send_timeout
、keepalive_timeout
。 -
超时立刻复位:
reset_timedout_connection on;
减少TIME_WAIT/
资源滞留。 -
限并发/限速:
limit_conn
、limit_req
(必要时返回 429 并带Retry-After
)。 -
HTTP/2 参数:降低
http2_max_concurrent_streams
,设置http2_recv_timeout/http2_idle_timeout
。 -
反向代理场景:
proxy_request_buffering on;
先把请求缓冲到 Nginx,避免慢上传占住上游。 -
分路径/分人群:对登录、搜索等接口更严;对可信源/健康检查放宽或白名单。
-
边缘清洗:结合 CDN/WAF 的连接层/应用层限速更稳。
一些相关的配置可以参考下面的 Nginx 配置:
worker_processes auto;
events {
worker_connections 4096;
multi_accept on;
}
http {
# 1) 关键超时(防慢头/慢体/慢读)
client_header_timeout 5s; # 等头部时间
client_body_timeout 10s; # 等请求体每个读周期的时间
send_timeout 10s; # 发送响应给客户端每个写周期的时间
keepalive_timeout 10s; # keep-alive 连接空闲时间
keepalive_requests 100; # 单连接最大请求数,防长时间占用
# 2) 连接超时直接复位(释放资源更快)
reset_timedout_connection on;
# 3) 并发限制(每 IP)
# 10m 可容纳 ~160k 键(基于 $binary_remote_addr)
limit_conn_zone $binary_remote_addr zone=perip:10m;
# 4) 速率限制(每 IP),按需调大/调小 rate
limit_req_zone $binary_remote_addr zone=req_perip:10m rate=10r/s;
# 5) HTTP/2 专项(若开启了 http2)
http2_max_concurrent_streams 64; # 降并发流数
http2_recv_timeout 5s; # 接收客户端帧超时
http2_idle_timeout 10s; # HTTP/2 空闲超时
# 6) 合理的头部缓冲(避免过大内存占用;默认已够用,按需微调)
large_client_header_buffers 4 8k;
server {
listen 443 ssl http2;
server_name example.com;
# 并发/速率在 server 层生效
limit_conn perip 20; # 每 IP 并发连接上限
limit_conn_status 429;
limit_req zone=req_perip burst=20 nodelay; # 短突发
limit_req_status 429;
# 限制请求体大小(配合 body_timeout 可更快淘汰异常大/慢上传)
client_max_body_size 10m;
# 【反向代理站点强烈推荐】先把完整请求缓冲到 Nginx
# 避免上游被慢上传拖住连接
location / {
proxy_pass http://app_backend;
proxy_request_buffering on;
proxy_connect_timeout 3s;
proxy_send_timeout 10s; # 向上游发送(写)超时
proxy_read_timeout 30s; # 自上游读取(读)超时
}
# 对静态资源可放宽速率限制以提升体验(示例)
location ~* \.(?:css|js|png|jpg|jpeg|gif|webp|ico|svg)$ {
root /var/www/html;
access_log off;
expires 30d;
}
# 自定义 429 页面(可选)
error_page 429 /429.html;
location = /429.html { internal; return 429 "Too Many Requests\n"; }
}
}
除此之外,还有一些数值上的建议:
-
高频
API
可将rate
调小、burst
适度放大;页面类流量可相反。 -
对上传较多的业务,将
client_body_timeout
与proxy_request_buffering on
; 组合尤为关键。 -
如果公网复杂、遭遇中等强度慢攻:
client_header_timeout 2-3s
、client_body_timeout 5-8s
、send_timeout 8-10s
往往更稳。
考虑到移动网络或跨境访问确实可能很慢,限流需要在防护与容错间取平衡。可以适度调大 burst
,并返回合理的 Retry-After
,让偶发拥塞得以通过。把严格策略仅应用在登录、搜索等敏感接口,对静态资源和页面流量适当放宽。对可信来源(如办公网、监控、合作方)设置白名单或更高配额,尽量减少误杀。
之于上面的理解,我们可以针对不同慢速攻击的做不同的优化了:
-
Slowloris(慢头部):用
client_header_timeout
严控请求头收齐时间,配合较短的keepalive_timeout
降低长连驻留,并用limit_conn
限制每 IP 并发;一旦超时,借助reset_timedout_connection on;
立即复位断开。 -
RUDY / Slow POST(慢体):设置较短的
client_body_timeout
,并开启proxy_request_buffering on;
先在 Nginx 缓冲请求体,慢上传直接在边缘被淘汰且不上游;必要时配合client_max_body_size
约束体积。 -
Slow Read(客户端读超慢):通过
send_timeout
限制客户端读取过慢的连接,触发即复位释放缓冲;若是 SSE/长轮询等合法长连,为对应路径单独放宽send_timeout
,避免误伤。
总结
慢速攻击是用极低带宽长期占用服务器连接/缓冲的攻击:攻击者故意慢发请求头/请求体或慢读响应,让连接一直不结束,耗尽并发与内存。
常见形态有 Slowloris(慢头部)、RUDY/Slow POST(慢请求体)与 Slow Read(慢读响应),在 HTTP/2 下还能通过多流+窗口控制放大影响。
典型症状是活跃连接(尤其 reading
)持续升高但总体带宽不高,日志频繁出现超时/异常头部,且 429/502/504
增多、同一 IP 大量长时间未完成请求。
防护要点是收紧超时(client_header/body/send/keepalive
)、开启 reset_timedout_connection
、用 limit_conn/limit_req
控制每 IP 并发与速率,反向代理时启用 proxy_request_buffering on;
并调优 HTTP/2;同时对敏感路径更严、对可信来源适度放宽或白名单以减少误杀。
React Native 实战心得
栗子前端技术周刊第 94 期 - React Native 0.81、jQuery 4.0.0 RC1、Bun v1.2.20...
🌰栗子前端技术周刊第 94 期 (2025.08.11 - 2025.08.17):浏览前端一周最新消息,学习国内外优秀文章视频,让我们保持对前端的好奇心。
📰 技术资讯
-
React Native 0.81:React Native 0.81 版本新增了对 Android 16 的支持,提升了 iOS 构建速度,并进行了一系列稳定性改进。
-
jQuery 4.0.0 RC1:jQuery 4.0.0 首个候选版本已发布,团队认为它已接近完成,希望开发者进行广泛测试,若未发现需修复的问题,将直接推出正式版本 jQuery 4.0.0。新版本主要变化与亮点包括:不再支持 IE 11 以下版本、删减遗留代码与弃用 API、引入精简版本等等。
-
Bun v1.2.20:Bun v1.2.20 修复了 141 个问题,并带来了显著的性能提升,包括降低空闲状态下的 CPU 使用率,以及将
AbortSignal.timeout
的速度提升 40 倍。
📒 技术文章
-
What we learned from creating PostCSS:那些从创建 PostCSS 中所学到的经验 - 12 年前,作者创建了 PostCSS —— 一款 CSS 自动化工具,其月下载量达 4 亿次,被谷歌、维基百科、Tailwind 以及 38% 的开发者所使用。在本文中,作者将分享在维护这个热门开源项目的漫长历程中所学到的经验。
-
How to Use innerHTML, innerText, and textContent Correctly in JavaScript:如何在 JavaScript 中正确使用
innerHTML
、innerText
和textContent
- 本文将解释 JavaScript 中三个 DOM 属性的区别:innerHTML
返回包含标签的完整 HTML 内容,innerText
仅返回受 CSS 规则影响的可见样式文本,而textContent
则返回所有文本内容,无论其在 CSS 中是否可见。这三个属性在 DOM 操作中适用于不同的使用场景。 -
前端必学-完美组件封装原则:此文总结了作者多年组件封装经验,以及拜读
antd
、element-plus
、vant
、fusion
等多个知名组件库所提炼的完美组件封装的经验;是一个开发者在封装项目组件,公共组件等场景时非常有必要遵循的一些原则。
🔧 开发工具
- ReactJS Cheatsheet:一份简洁且对开发者友好的 ReactJS 速查表,汇总了核心概念、必备 Hooks、路由、性能优化技巧以及 React 18 的新特性。
- vue-scan:让你的组件在每次更新时都闪现红色边框,帮助你排查性能问题。
- react-json-view:react-json-view(简称 rjv)是一个用于展示和编辑 JavaScript 数组及 JSON 对象的 React 组件。
🚀🚀🚀 以上资讯文章选自常见周刊,如 JavaScript Weekly 等,周刊内容也会不断优化改进,希望你们能够喜欢。
💖 欢迎关注微信公众号:栗子前端
React状态更新踩坑记:我是这样优雅修改参数的
大家好,我是小杨,一名有6年经验的前端开发工程师。在React开发中,状态(State)和参数(Props)的修改是最基础但也最容易踩坑的部分。今天我就来分享几种常见的React参数修改方法,以及我在项目中总结的最佳实践,避免大家走弯路。
1. 直接修改State?大忌!
新手常犯的一个错误是直接修改state
,比如:
// ❌ 错误示范:直接修改state
this.state.count = 10;
React的state
是不可变(Immutable) 的,直接修改不会触发重新渲染。正确的做法是使用setState
(类组件)或useState
的更新函数(函数组件)。
2. 类组件:setState的正确姿势
在类组件里,修改状态必须用setState
:
class Counter extends React.Component {
state = { count: 0 };
increment = () => {
// ✅ 正确方式:使用setState
this.setState({ count: this.state.count + 1 });
};
render() {
return <button onClick={this.increment}>Count: {this.state.count}</button>;
}
}
注意:setState
是异步的,如果依赖前一个状态,应该用函数式更新:
this.setState((prevState) => ({ count: prevState.count + 1 }));
3. 函数组件:useState + 不可变更新
在函数组件里,我们使用useState
,同样要遵循不可变原则:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
// ✅ 正确方式:使用useState的更新函数
setCount(count + 1);
};
return <button onClick={increment}>Count: {count}</button>;
}
如果新状态依赖旧状态,推荐使用函数式更新:
setCount((prevCount) => prevCount + 1);
4. 修改对象或数组:避免引用突变
React要求状态更新必须是不可变的,所以直接修改对象或数组的属性是不行的:
const [user, setUser] = useState({ name: 'Alice', age: 25 });
// ❌ 错误:直接修改对象
user.age = 26;
setUser(user); // 不会触发更新!
// ✅ 正确:创建新对象
setUser({ ...user, age: 26 });
数组的更新也要遵循不可变原则:
const [todos, setTodos] = useState(['Learn React', 'Write Blog']);
// ✅ 正确:使用展开运算符或map/filter
setTodos([...todos, 'New Task']); // 添加
setTodos(todos.filter((todo) => todo !== 'Learn React')); // 删除
5. 性能优化:useState vs useReducer
如果状态逻辑较复杂,useState
可能会变得臃肿,这时可以用useReducer
:
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error('Unknown action');
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</div>
);
}
useReducer
适合管理复杂状态逻辑,比如表单、全局状态等。
6. 常见坑点 & 解决方案
① 连续setState不会立即更新
// ❌ 连续调用setState,count只会+1
setCount(count + 1);
setCount(count + 1);
// ✅ 使用函数式更新
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 现在会+2
② useEffect依赖问题
如果useEffect
依赖state
,但忘记加进依赖数组,可能导致闭包问题:
useEffect(() => {
console.log(count); // 可能拿到旧值
}, []); // ❌ 缺少依赖
useEffect(() => {
console.log(count); // ✅ 正确
}, [count]); // 依赖正确
总结
- 不要直接修改
state
,使用setState
或useState
的更新函数 - 对象/数组更新时,创建新引用
- 复杂状态逻辑用
useReducer
- 连续更新用函数式
setState
useEffect
依赖要写全
⭐ 写在最后
请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.
✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式
✅ 认为我部分代码过于老旧,可以提供新的API或最新语法
✅ 对于文章中部分内容不理解
✅ 解答我文章中一些疑问
✅ 认为某些交互,功能需要优化,发现BUG
✅ 想要添加新功能,对于整体的设计,外观有更好的建议
✅ 一起探讨技术加qq交流群:906392632
最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!
Next.js 教程系列(二十七)React Server Components (RSC) 与未来趋势
前言
大家好,我是鲫小鱼。是一名不写前端代码
的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》
。欢迎点赞、收藏、关注,一键三连!!
第二十七章:React Server Components (RSC) 与未来趋势
理论讲解
1. 什么是 React Server Components (RSC)?
React Server Components (RSC) 是 React 18 引入的革命性特性,允许组件在服务端渲染,而无需发送到客户端。这是 React 生态系统的重大演进,标志着从传统的客户端渲染向服务端优先架构的转变。
RSC 的核心思想:
- 服务端优先:组件默认在服务端渲染,减少客户端 JavaScript 包大小。
- 渐进式增强:可以混合使用 Server Components 和 Client Components。
- 数据获取优化:服务端组件可以直接访问数据库、文件系统等资源。
- 安全性提升:敏感逻辑和 API 密钥保留在服务端,不暴露给客户端。
RSC 的优势:
- 性能提升:减少客户端 JavaScript 包大小,提升首屏加载速度。
- SEO 友好:服务端渲染内容对搜索引擎更友好。
- 安全性:敏感逻辑和 API 密钥保留在服务端。
- 开发体验:更简单的数据获取,无需 useEffect、useState 等 hooks。
- 缓存优化:服务端组件可以更好地利用缓存策略。
RSC 的挑战:
- 学习曲线:需要理解服务端和客户端组件的边界。
- 调试复杂:服务端组件调试相对困难。
- 状态管理:服务端组件无法使用 useState、useEffect 等客户端 hooks。
- 交互限制:服务端组件无法直接处理用户交互。
2. RSC 渲染模型与架构
RSC 采用了一种全新的渲染模型,称为"流式服务端渲染":
graph TD;
A[客户端请求] --> B[服务端渲染 Server Components]
B --> C[生成 RSC Payload]
C --> D[流式传输到客户端]
D --> E[客户端水合 Client Components]
E --> F[完整交互页面]
渲染流程:
- 服务端渲染:Server Components 在服务端执行,生成 RSC Payload。
- 流式传输:RSC Payload 通过流式传输发送到客户端。
- 客户端水合:Client Components 在客户端水合,处理交互。
- 混合渲染:Server Components 和 Client Components 协同工作。
3. Next.js App Router 中的 RSC 实践
Next.js 13+ 的 App Router 天然支持 RSC,所有组件默认都是 Server Components:
- 默认 Server Components:App Router 中的组件默认在服务端渲染。
-
'use client' 指令:使用
'use client'
指令将组件标记为 Client Component。 - 数据获取:Server Components 可以直接使用 async/await 进行数据获取。
-
缓存策略:支持多种缓存策略,如
cache()
、revalidate
等。
4. 企业级 RSC 应用场景
- 内容管理系统:文章内容、产品信息等静态内容使用 Server Components。
- 电商平台:产品列表、详情页等使用 Server Components,购物车、用户交互使用 Client Components。
- 企业门户:公司介绍、新闻资讯等使用 Server Components。
- 管理后台:数据展示使用 Server Components,表单交互使用 Client Components。
详细代码示例
1. 基础 Server Component
// app/page.tsx (默认 Server Component)
import { getArticles } from '@/lib/articles';
export default async function HomePage() {
const articles = await getArticles();
return (
<div>
<h1>博客首页</h1>
<ArticleList articles={articles} />
</div>
);
}
2. 数据获取 Server Component
// app/articles/[id]/page.tsx
import { getArticle, getComments } from '@/lib/articles';
import { CommentSection } from '@/components/CommentSection';
export default async function ArticlePage({ params }: { params: { id: string } }) {
const [article, comments] = await Promise.all([
getArticle(params.id),
getComments(params.id)
]);
return (
<article>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.content }} />
<CommentSection articleId={params.id} initialComments={comments} />
</article>
);
}
3. Client Component 交互
// components/CommentSection.tsx
'use client';
import { useState } from 'react';
interface Comment {
id: string;
content: string;
author: string;
}
export function CommentSection({
articleId,
initialComments
}: {
articleId: string;
initialComments: Comment[];
}) {
const [comments, setComments] = useState(initialComments);
const [newComment, setNewComment] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const response = await fetch('/api/comments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ articleId, content: newComment })
});
if (response.ok) {
const comment = await response.json();
setComments([...comments, comment]);
setNewComment('');
}
};
return (
<div>
<h3>评论 ({comments.length})</h3>
{comments.map(comment => (
<div key={comment.id}>
<strong>{comment.author}</strong>: {comment.content}
</div>
))}
<form onSubmit={handleSubmit}>
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="写下你的评论..."
/>
<button type="submit">提交评论</button>
</form>
</div>
);
}
4. 混合使用 Server 和 Client Components
// app/layout.tsx
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { ThemeProvider } from '@/components/ThemeProvider';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh">
<body>
<ThemeProvider>
<Header />
<main>{children}</main>
<Footer />
</ThemeProvider>
</body>
</html>
);
}
// components/Header.tsx
'use client';
import { useState } from 'react';
import { UserMenu } from './UserMenu';
export function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
return (
<header>
<nav>
<div className="logo">我的博客</div>
<UserMenu isOpen={isMenuOpen} onToggle={() => setIsMenuOpen(!isMenuOpen)} />
</nav>
</header>
);
}
5. 缓存与性能优化
// lib/articles.ts
import { cache } from 'react';
export const getArticles = cache(async () => {
const response = await fetch('https://api.example.com/articles', {
next: { revalidate: 3600 } // 缓存1小时
});
return response.json();
});
export const getArticle = cache(async (id: string) => {
const response = await fetch(`https://api.example.com/articles/${id}`, {
next: { revalidate: 1800 } // 缓存30分钟
});
return response.json();
});
6. 错误处理与加载状态
// app/articles/[id]/page.tsx
import { Suspense } from 'react';
import { getArticle } from '@/lib/articles';
import { ArticleContent } from '@/components/ArticleContent';
import { LoadingSpinner } from '@/components/LoadingSpinner';
export default async function ArticlePage({ params }: { params: { id: string } }) {
return (
<Suspense fallback={<LoadingSpinner />}>
<ArticleContent articleId={params.id} />
</Suspense>
);
}
// components/ArticleContent.tsx
import { getArticle } from '@/lib/articles';
export async function ArticleContent({ articleId }: { articleId: string }) {
const article = await getArticle(articleId);
if (!article) {
throw new Error('文章不存在');
}
return (
<article>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.content }} />
</article>
);
}
实战项目:企业级博客 RSC 实践
1. 项目背景
我们有一个企业级博客系统,需要实现:
- 文章内容使用 Server Components 渲染,提升 SEO 和性能。
- 评论区使用 Client Components 处理用户交互。
- 用户认证和权限控制。
- 移动端适配和性能优化。
2. 项目结构
app/
layout.tsx
page.tsx
articles/
[id]/
page.tsx
api/
comments/
route.ts
components/
ArticleContent.tsx
CommentSection.tsx
Header.tsx
Footer.tsx
ThemeProvider.tsx
lib/
articles.ts
auth.ts
3. 核心实现
文章页面(Server Component)
// app/articles/[id]/page.tsx
import { getArticle, getComments } from '@/lib/articles';
import { ArticleContent } from '@/components/ArticleContent';
import { CommentSection } from '@/components/CommentSection';
import { notFound } from 'next/navigation';
export default async function ArticlePage({ params }: { params: { id: string } }) {
try {
const [article, comments] = await Promise.all([
getArticle(params.id),
getComments(params.id)
]);
if (!article) {
notFound();
}
return (
<div className="max-w-4xl mx-auto px-4">
<ArticleContent article={article} />
<CommentSection articleId={params.id} initialComments={comments} />
</div>
);
} catch (error) {
console.error('加载文章失败:', error);
throw new Error('文章加载失败');
}
}
文章内容组件(Server Component)
// components/ArticleContent.tsx
import { formatDate } from '@/lib/utils';
interface Article {
id: string;
title: string;
content: string;
author: string;
publishedAt: string;
}
export function ArticleContent({ article }: { article: Article }) {
return (
<article className="prose lg:prose-xl">
<header className="mb-8">
<h1 className="text-3xl font-bold mb-4">{article.title}</h1>
<div className="text-gray-600">
<span>作者: {article.author}</span>
<span className="mx-2">•</span>
<span>发布时间: {formatDate(article.publishedAt)}</span>
</div>
</header>
<div
className="article-content"
dangerouslySetInnerHTML={{ __html: article.content }}
/>
</article>
);
}
评论区组件(Client Component)
// components/CommentSection.tsx
'use client';
import { useState } from 'react';
import { useSession } from 'next-auth/react';
interface Comment {
id: string;
content: string;
author: string;
createdAt: string;
}
export function CommentSection({
articleId,
initialComments
}: {
articleId: string;
initialComments: Comment[];
}) {
const { data: session } = useSession();
const [comments, setComments] = useState(initialComments);
const [newComment, setNewComment] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!session) {
alert('请先登录');
return;
}
setIsSubmitting(true);
try {
const response = await fetch('/api/comments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ articleId, content: newComment })
});
if (response.ok) {
const comment = await response.json();
setComments([comment, ...comments]);
setNewComment('');
} else {
throw new Error('提交失败');
}
} catch (error) {
console.error('提交评论失败:', error);
alert('提交失败,请重试');
} finally {
setIsSubmitting(false);
}
};
return (
<section className="mt-12">
<h3 className="text-xl font-semibold mb-6">
评论 ({comments.length})
</h3>
{/* 评论列表 */}
<div className="space-y-4 mb-8">
{comments.map(comment => (
<div key={comment.id} className="border-b pb-4">
<div className="flex items-center mb-2">
<span className="font-medium">{comment.author}</span>
<span className="text-gray-500 text-sm ml-2">
{new Date(comment.createdAt).toLocaleDateString()}
</span>
</div>
<p className="text-gray-700">{comment.content}</p>
</div>
))}
</div>
{/* 评论表单 */}
{session ? (
<form onSubmit={handleSubmit} className="space-y-4">
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="写下你的评论..."
className="w-full p-3 border rounded-lg resize-none"
rows={4}
required
/>
<button
type="submit"
disabled={isSubmitting || !newComment.trim()}
className="px-6 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50"
>
{isSubmitting ? '提交中...' : '提交评论'}
</button>
</form>
) : (
<p className="text-gray-600">请登录后发表评论</p>
)}
</section>
);
}
4. API 路由实现
// app/api/comments/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const { articleId, content } = await request.json();
if (!content?.trim()) {
return NextResponse.json({ error: '评论内容不能为空' }, { status: 400 });
}
// 这里应该保存到数据库
const comment = {
id: Date.now().toString(),
content: content.trim(),
author: session.user?.name || '匿名用户',
createdAt: new Date().toISOString(),
articleId
};
return NextResponse.json(comment);
} catch (error) {
console.error('保存评论失败:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
5. 性能优化与移动端适配
// app/layout.tsx
import { Inter } from 'next/font/google';
import { ThemeProvider } from '@/components/ThemeProvider';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh">
<body className={inter.className}>
<ThemeProvider>
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1">{children}</main>
<Footer />
</div>
</ThemeProvider>
</body>
</html>
);
}
最佳实践与常见问题
最佳实践
- 合理划分边界:静态内容使用 Server Components,交互功能使用 Client Components。
- 数据获取优化:利用 Server Components 直接获取数据,减少客户端请求。
- 缓存策略:合理使用 Next.js 的缓存机制。
- 错误处理:为 Server Components 添加适当的错误边界。
- 性能监控:监控 RSC 的性能表现。
常见问题与解决方案
Q1: 什么时候使用 Server Components? A: 静态内容、SEO 重要内容、数据获取、不需要交互的组件。
Q2: 什么时候使用 Client Components? A: 需要用户交互、使用浏览器 API、需要状态管理、使用事件监听器。
Q3: 如何处理 Server Components 中的错误? A: 使用 try-catch、error.tsx 文件、Suspense 边界。
Q4: 如何优化 RSC 性能? A: 合理使用缓存、避免不必要的重新渲染、优化数据获取。
Q5: 如何调试 Server Components? A: 使用 console.log、Next.js 开发工具、服务端日志。
Mermaid 配图说明
graph TD;
A[客户端请求] --> B[Next.js App Router]
B --> C[Server Components 渲染]
C --> D[数据获取]
D --> E[生成 HTML + RSC Payload]
E --> F[流式传输到客户端]
F --> G[Client Components 水合]
G --> H[完整交互页面]
RSC 渲染流程示意图
flowchart LR
subgraph "Server Components"
A1[文章内容]
A2[产品信息]
A3[导航菜单]
end
subgraph "Client Components"
B1[评论区]
B2[购物车]
B3[用户菜单]
end
A1 --> B1
A2 --> B2
A3 --> B3
Server Components 与 Client Components 混合使用
资源链接与完整源码
最后感谢阅读!欢迎关注我,微信公众号:
《鲫小鱼不正经》
。欢迎点赞、收藏、关注,一键三连!!
JS 函数终极指南:this、闭包、递归、尾调用、柯里化,一次性吃透
1. 箭头函数 vs 普通函数
核心差异
- 没有自己的
this / arguments / super / new.target
(都词法绑定,取自外层作用域)。 -
不能做构造函数:
new (()=>{})
会抛错。 -
prototype
属性不存在。 - 返回值:单表达式可省略
{}
和return
;要返回对象字面量需用括号包裹。
坑点
- 对象方法不要随手写成箭头函数,否则
this
指向外层而非对象本身。 - 需要
arguments
时,用 rest 参数(...args)
代替。
示例
const counter = {
n: 0,
// ❌ 用箭头写方法会拿不到对象的 this
incBad: () => ++this.n, // this 来自外层,非 counter
// ✅ 正确:普通函数做方法
inc() { return ++this.n; },
};
const add = (a, b) => a + b;
const makeUser = (name) => ({ name }); // 返回对象需用括号包住
建议
- 事件/类方法/需要
this
的场景用普通函数;回调与组合函数用箭头函数。
2. 函数名(Function.prototype.name
)
用途
- 调试堆栈 & 日志。
- 自递归(命名函数表达式)。
细节
- 赋给变量的匿名函数会推断名字:
const f = function(){}; f.name === "f"
。 - 绑定函数:
bound
前缀——f.bind(obj).name === "bound f"
(实现相关)。
function foo() {}
const bar = function baz() {};
console.log(foo.name); // "foo"
console.log(bar.name); // "baz"(仅在函数体内能引用 baz 递归)
3. 理解参数(JS 没有“真正重载”)
现状
- JS 不按签名区分重载;后定义的同名函数覆盖前者。
常见“重载”策略
- 参数个数/类型分派
- 可选对象参数(推荐,可扩展性强)
- 重载表(按规则挑处理器)
// 1) 个数/类型分派
function area(shape, a, b) {
if (shape === 'circle') return Math.PI * a * a;
if (shape === 'rect') return a * b;
throw new TypeError('unknown shape');
}
// 2) options 对象
function fetchUser({ id, withPosts = false } = {}) { /* ... */ }
// 3) 重载表
const overloads = {
string: (x) => x.toUpperCase(),
number: (x) => x * x,
};
function process(x) {
const f = overloads[typeof x];
if (!f) throw new TypeError('unsupported');
return f(x);
}
4. 箭头函数中的参数
写法速览
const id = x => x; // 单参省略括号
const add = (a, b) => a + b; // 多参
const sum = (...nums) => nums.reduce((p, c) => p + c, 0); // rest
const greet = ({name} = {}) => `Hi, ${name||'Guest'}`; // 解构+默认
5. “没有重载”意味着什么
- 同名函数只保留最后一次定义。
- API 设计宜避免同名多义,使用不同名字或 options。
function f(a) {}
function f(a, b) {} // 覆盖前面的 f(a)
6. 默认参数值
规则
- 只有当实参为
undefined
才会触发默认值(null
不会)。 - 默认值在调用时求值,可引用前面参数。
- 与解构组合很强大。
示例
function hello(name = 'Guest') { return `Hi, ${name}`; }
hello(); // "Hi, Guest"
hello(null); // "Hi, null"(不会触发默认)
function f(a, b = a * 2) { return b; }
f(3); // 6
function draw({x = 0, y = 0} = {}) { /* ... */ } // 支持缺参与缺字段
必传参数技巧
const required = (n) => { throw new Error(`${n} is required`); };
function connect(url = required('url')) { /*...*/ }
7. 默认参数与“暂时性死区”(TDZ)
- 默认参数形成独立作用域;在其作用域内存在 TDZ。
- 默认值表达式可用之前的参数,不可用之后的。
let x = 1;
function f(a = x, x = 2) { // 形参 x 遮蔽外层 x
console.log(a, x); // a=1, x=2
}
function g(a = b, b = 2) {} // ❌ ReferenceError,b 尚未初始化
8. 参数扩展与收集(spread / rest)
(1) 扩展参数(spread)
- 用在调用/字面量中“展开”可迭代对象;浅拷贝。
const arr = [1, 2, 3];
Math.max(...arr); // 3
const a = [1, 2]; const b = [3, 4];
const ab = [...a, ...b]; // [1,2,3,4]
const o1 = {a:1}, o2 = {b:2, a:9};
const o = { ...o1, ...o2 }; // {a:9, b:2}(后者覆盖前者)
(2) 收集参数(rest)
- 用在形参最后位置,收集剩余实参为真数组。
function sum(first, ...rest) {
return rest.reduce((p, c) => p + c, first);
}
sum(10, 1, 2, 3); // 16
对比 arguments
-
arguments
类数组,strict 下与形参不再联动;rest 是真数组、更推荐。
9. 函数声明 vs 函数表达式
区别
- 声明(Function Declaration)整体提升:可在声明前调用。
-
表达式(Function Expression)赋值给变量;
var
变量名提升为undefined
,let/const
有 TDZ。 - 命名函数表达式仅在函数体内可见其名(便于自递归)。
示例
foo(); // ✅
function foo() {}
bar(); // ❌ ReferenceError(若使用 let/const)
const bar = function() {};
const baz = function qux(n){ return n ? qux(n-1) : 0; };
10. 函数作为值 & 柯里化(Currying)
概念
- 柯里化:把
f(a,b,c)
变为f(a)(b)(c)
(或更灵活地累积参数)。 - 用途:参数复用、延迟求值、函数组合、偏应用、构建 DSL。
通用柯里化实现
const curry = (fn, arity = fn.length) =>
function curried(...args) {
return args.length >= arity
? fn.apply(this, args)
: (...rest) => curried.apply(this, args.concat(rest));
};
const sum3 = (a,b,c)=>a+b+c;
const csum3 = curry(sum3);
csum3(1)(2)(3); // 6
csum3(1,2)(3); // 6
实战
- 事件处理器“预置参数”:
onClick={handle(type)}
- 日志封装:
const logNs = ns => (...a)=>console.log(ns, ...a)
- React/FP:与
map/filter/reduce
、compose/pipe
搭配。
偏应用(partial)对比
const partial = (fn, ...bound) => (...rest) => fn(...bound, ...rest);
const add = (a,b,c)=>a+b+c;
partial(add, 1, 2)(3); // 6
11. 函数内容
(1) arguments
与 callee
-
arguments
:类数组、动态实参集合;不建议新代码依赖。 -
arguments.callee
:严格模式禁用,不推荐使用(可用命名函数表达式替代)。
function show() {
console.log(arguments.length); // 实参个数
}
(2) this
绑定规则优先级
-
new
绑定(构造调用) - 显式绑定:
call/apply/bind
- 隐式绑定:作为对象方法调用
obj.fn()
- 默认绑定:非严格
this===window
;严格模式undefined
- 箭头函数:词法 this(创建时确定,无法改)
function who() { console.log(this.tag); }
const o = { tag:'O', who };
who(); // undefined / window
o.who(); // 'O'
who.call({tag:'X'}); // 'X'
const arrow = () => console.log(this.tag);
arrow.call({tag:'Y'}); // 仍来自外层 this
(3) caller
-
function.caller
:非标准、严格模式下受限,不可依赖。
(4) new.target
- 判断是否通过
new
调用;在 class 中可做“抽象类”限制。
function Person() {
if (!new.target) throw new Error('use new');
}
class Shape {
constructor() {
if (new.target === Shape) throw new Error('abstract');
}
}
12. 函数属性与方法:call / apply / bind
共性:改变 this
。
-
call(thisArg, ...args)
:立即调用,参数散列。 -
apply(thisArg, argsArray)
:立即调用,参数数组(适合已有数组)。 -
bind(thisArg, ...args)
:返回新函数,可部分应用 & 固定this
。
示例
function greet(g, p) { console.log(`${g}, ${this.name}${p}`); }
const u = { name:'Ada' };
greet.call(u, 'Hi', '!'); // Hi, Ada!
greet.apply(u, ['Hello', '!!!']); // Hello, Ada!!!
const hiAda = greet.bind(u, 'Hi'); // 预置 g 与 this
hiAda('?'); // Hi, Ada?
实战场景
- 借用数组方法:
[].slice.call(arguments)
(老法,现在用Array.from
) - DOM 事件里预绑定处理器
this
/参数 - 函数组合与偏应用
小心
-
bind
返回的函数不可再被call/apply
更改this
。 - 频繁
bind
会增开销,优先在构造时一次性绑定或用箭头函数持有外层this
。
13. 函数表达式与“声明提升”
- 函数声明整体提升。
-
函数表达式不会提升实现体;若用
let/const
,在声明前访问触发 TDZ。
foo(); // ok
function foo(){}
bar(); // ReferenceError
const bar = function(){};
14. 递归(以阶乘为例)
function factorial(n) {
if (n < 0) throw new RangeError('n>=0');
if (n <= 1) return 1;
return n * factorial(n - 1);
}
注意
- 大
n
可能栈溢出;可改成循环或“尾递归+蹦床(trampoline)”。
蹦床示意:
const trampoline = (f) => (...args) => {
let res = f(...args);
while (typeof res === 'function') res = res();
return res;
};
const factT = trampoline(function step(n, acc=1){
if (n<=1) return acc;
return () => step(n-1, n*acc);
});
factT(10000); // 不会爆栈
15. 尾调用优化(TCO)
概念
- “尾调用”是函数返回位置直接返回另一个调用的结果。
- “尾递归”是递归调用位于尾位置。
- 理论上可节省栈帧。
现实
- 主流引擎目前并未实现规范化 TCO(不要依赖它避免爆栈)。生产中请用循环或蹦床。
尾递归写法(语义正确,但不指望优化)
'use strict';
function factorialTR(n, acc = 1) {
if (n <= 1) return acc;
return factorialTR(n - 1, n * acc); // 尾位置
}
16. 尾调用的判定条件(理论)
- 调用必须在返回语句的尾位置(
return f(...)
)。 - 不能在
try/finally
等会产生额外工作的位置。 - 不能对返回值再做运算(如
1 + f()
不是尾调用)。 - 严格模式(规范讨论语境)。
实战:当成编码风格理解,不依赖优化。
17. 闭包(Closure)
定义
- 函数“记住”其创建时的词法作用域,即使在外层函数已执行完后仍可访问到。
常见用途
- 私有状态、函数工厂、缓存/记忆化、模块封装。
示例:计数器
function makeCounter() {
let n = 0;
return () => ++n;
}
const c = makeCounter();
c(); c(); // 1, 2
坑点
- 循环里
var
会共享一个变量;用let
或 IIFE 捕获值。
for (var i=0;i<3;i++){
setTimeout(()=>console.log(i),0); // 3,3,3
}
for (let j=0;j<3;j++){
setTimeout(()=>console.log(j),0); // 0,1,2
}
18. this
对象(再补充要点)
-
优先级:
new
> 显式 > 隐式 > 默认;箭头函数跳出规则,直接词法绑定。 -
类字段里的箭头常用于把回调里的
this
固定为实例:
class View {
constructor() { this.count = 0; }
onClick = () => { this.count++; } // 在实例上创建,this 永远是实例
}
19. 内存泄漏(Memory Leak)
常见来源
- 全局变量/意外挂到全局
- 未清理的定时器/订阅/事件监听
- 闭包长期持有大对象/DOM 引用
- 脱离文档的 DOM 被引用(detached DOM)
- 无界缓存(Map/Object 不清理)
示例与修复
// 1) 全局
function bad() { leaky = new Array(1e6).fill('*'); } // ❌ 隐式全局(漏写 var/let/const)
function good() { const safe = new Array(1e6).fill('*'); }
// 2) 定时器/监听
const id = setInterval(()=>{/*...*/}, 1000);
// 当组件卸载或不需要时
clearInterval(id);
const onScroll = () => {/*...*/};
window.addEventListener('scroll', onScroll);
// ...
window.removeEventListener('scroll', onScroll);
// 3) 缓存
const cache = new Map();
function get(key, create) {
if (!cache.has(key)) cache.set(key, create());
return cache.get(key);
}
// 若 key 可能只被临时对象引用,考虑 WeakMap 以便 GC:
const wcache = new WeakMap();
建议
- 打开浏览器内存快照找“保留对象”。
- 组件/模块成对清理资源(定时器、监听、订阅)。
- 能用
WeakMap/WeakRef
的场景尽量用。
20. IIFE(立即调用函数表达式)
作用
- 立即执行并创建私有作用域;历史上用于“模块化”(在 ES 模块前)。
示例
(function(){
const secret = 42;
console.log('IIFE run');
})();
// 异步 IIFE(常见于顶层 await 替代)
(async () => {
const data = await Promise.resolve(123);
console.log(data);
})();
21. 私有变量(基于闭包)
function Counter() {
let n = 0; // 私有
return {
inc: () => ++n,
get: () => n
};
}
const c = Counter();
c.inc(); c.get(); // 1
特点:无法从外部直接访问/修改 n
,天然私有。
22. 静态私有变量
方式 A:类的私有静态字段(#)
class IdGen {
static #next = 1; // 静态私有
static alloc() { return this.#next++; }
}
IdGen.alloc(); // 1
方式 B:模块作用域变量(文件级私有)
let next = 1; // 仅模块内可见
export function alloc() { return next++; }
23. 模块模式(Module Pattern / Revealing Module Pattern)
思想
- 用 IIFE 封装私有变量与函数,只暴露公共 API。
const Store = (function(){
const data = new Map(); // 私有
function set(k, v){ data.set(k, v); }
function get(k){ return data.get(k); }
function has(k){ return data.has(k); }
return { set, get, has }; // Revealing:名称一致
})();
Store.set('a', 1);
提示
- 现代项目优先使用 ES Module:
export
/import
。 - 但在无打包器或老环境,模块模式仍很实用。
24. 增强模块模式(Augmented / Hybrid Module)
用途
- 在原模块基础上扩展或注入依赖,支持可测试性/可插拔性。
// 基础模块
const Base = (function(){
const list = [];
return {
add(x){ list.push(x); },
all(){ return list.slice(); }
};
})();
// 增强:添加 reset、filter 功能
const Enhanced = (function(mod){
mod.reset = function(){ mod._reset?.() ?? (function(){
// 通过内部 API 或暴露的方式清空(演示用)
while (mod.all().length) mod.all().pop(); // 这里只为演示,实际应在 Base 提供 clear
})(); };
mod.filter = function(pred){ return mod.all().filter(pred); };
return mod;
})(Base);
// 依赖注入式增强(Hybrid)
const WithLogger = (function(mod, logger){
const oldAdd = mod.add;
mod.add = (x)=>{ logger('add', x); oldAdd(x); };
return mod;
})(Base, console.log);
建议
- 设计时预留最小必要的扩展点(如
clear
/onChange
)。 - 现代 ESM 下可通过组合导出或插件机制实现增强。
额外实用补充
-
函数属性:
length
(形参个数,不含 rest/有默认值之后的参数),name
。 -
Function
构造器:new Function(code)
动态创建函数(绕过作用域封闭,慎用)。 -
参数校验:运行时校验(
typeof
/Array.isArray
/小型 schema 校验)。 - 性能:频繁创建闭包/绑定函数有成本,复用或上移到外层作用域。
每日一题-24 点游戏🔴
给定一个长度为4的整数数组 cards
。你有 4
张卡片,每张卡片上都包含一个范围在 [1,9]
的数字。您应该使用运算符 ['+', '-', '*', '/']
和括号 '('
和 ')'
将这些卡片上的数字排列成数学表达式,以获得值24。
你须遵守以下规则:
- 除法运算符
'/'
表示实数除法,而不是整数除法。- 例如,
4 /(1 - 2 / 3)= 4 /(1 / 3)= 12
。
- 例如,
- 每个运算都在两个数字之间。特别是,不能使用
“-”
作为一元运算符。- 例如,如果
cards =[1,1,1,1]
,则表达式“-1 -1 -1 -1”
是 不允许 的。
- 例如,如果
- 你不能把数字串在一起
- 例如,如果
cards =[1,2,1,2]
,则表达式“12 + 12”
无效。
- 例如,如果
如果可以得到这样的表达式,其计算结果为 24
,则返回 true
,否则返回 false
。
示例 1:
输入: cards = [4, 1, 8, 7] 输出: true 解释: (8-4) * (7-1) = 24
示例 2:
输入: cards = [1, 2, 1, 2] 输出: false
提示:
cards.length == 4
1 <= cards[i] <= 9
Java 回溯, 经典面试题
微软一面平行面出了这个题, 比这个还麻烦, 给的输入是 n 个数字
用的 Java 回溯解决的, 第一轮已经通过
提一下需要注意的细节
- 不要使用魔法数字 24, 1e-6 等, 需要使用有意义的变量代替
- double 类型不能使用 "==", 需要用做差和一个较小的值比较判断
- 将函数拆分成几个小的函数分别求解, 可以先提出思路和写一个空函数
- 从 2 个数字开始逐步扩展
- 注意不能产生除 0 错误
- 一旦回溯有一条路能产生 true 需要立即返回
###Java
class Solution {
private static final double TARGET = 24;
private static final double EPISLON = 1e-6;
public boolean judgePoint24(int[] cards) {
return helper(new double[]{ cards[0], cards[1], cards[2], cards[3] });
}
private boolean helper(double[] nums) {
if (nums.length == 1) return Math.abs(nums[0] - TARGET) < EPISLON;
// 每次选择两个不同的数进行回溯
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
// 将选择出来的两个数的计算结果和原数组剩下的数加入 next 数组
double[] next = new double[nums.length - 1];
for (int k = 0, pos = 0; k < nums.length; k++) if (k != i && k != j) next[pos++] = nums[k];
for (double num : calculate(nums[i], nums[j])) {
next[next.length - 1] = num;
if (helper(next)) return true;
}
}
}
return false;
}
private List<Double> calculate(double a, double b) {
List<Double> list = new ArrayList<>();
list.add(a + b);
list.add(a - b);
list.add(b - a);
list.add(a * b);
if (!(Math.abs(b) < EPISLON)) list.add(a / b);
if (!(Math.abs(a) < EPISLON)) list.add(b / a);
return list;
}
}
Python一行大法好!
###Python
class Solution:
def judgePoint24(self, nums: List[int]) -> bool:
return sorted(nums) in [[1, 1, 1, 8], [1, 1, 2, 6], [1, 1, 2, 7], [1, 1, 2, 8], [1, 1, 2, 9], [1, 1, 3, 4], [1, 1, 3, 5], [1, 1, 3, 6], [1, 1, 3, 7], [1, 1, 3, 8], [1, 1, 3, 9], [1, 1, 4, 4], [1, 1, 4, 5], [1, 1, 4, 6], [1, 1, 4, 7], [1, 1, 4, 8], [1, 1, 4, 9], [1, 1, 5, 5], [1, 1, 5, 6], [1, 1, 5, 7], [1, 1, 5, 8], [1, 1, 6, 6], [1, 1, 6, 8], [1, 1, 6, 9], [1, 1, 8, 8], [1, 2, 2, 4], [1, 2, 2, 5], [1, 2, 2, 6], [1, 2, 2, 7], [1, 2, 2, 8], [1, 2, 2, 9], [1, 2, 3, 3], [1, 2, 3, 4], [1, 2, 3, 5], [1, 2, 3, 6], [1, 2, 3, 7], [1, 2, 3, 8], [1, 2, 3, 9], [1, 2, 4, 4], [1, 2, 4, 5], [1, 2, 4, 6], [1, 2, 4, 7], [1, 2, 4, 8], [1, 2, 4, 9], [1, 2, 5, 5], [1, 2, 5, 6], [1, 2, 5, 7], [1, 2, 5, 8], [1, 2, 5, 9], [1, 2, 6, 6], [1, 2, 6, 7], [1, 2, 6, 8], [1, 2, 6, 9], [1, 2, 7, 7], [1, 2, 7, 8], [1, 2, 7, 9], [1, 2, 8, 8], [1, 2, 8, 9], [1, 3, 3, 3], [1, 3, 3, 4], [1, 3, 3, 5], [1, 3, 3, 6], [1, 3, 3, 7], [1, 3, 3, 8], [1, 3, 3, 9], [1, 3, 4, 4], [1, 3, 4, 5], [1, 3, 4, 6], [1, 3, 4, 7], [1, 3, 4, 8], [1, 3, 4, 9], [1, 3, 5, 6], [1, 3, 5, 7], [1, 3, 5, 8], [1, 3, 5, 9], [1, 3, 6, 6], [1, 3, 6, 7], [1, 3, 6, 8], [1, 3, 6, 9], [1, 3, 7, 7], [1, 3, 7, 8], [1, 3, 7, 9], [1, 3, 8, 8], [1, 3, 8, 9], [1, 3, 9, 9], [1, 4, 4, 4], [1, 4, 4, 5], [1, 4, 4, 6], [1, 4, 4, 7], [1, 4, 4, 8], [1, 4, 4, 9], [1, 4, 5, 5], [1, 4, 5, 6], [1, 4, 5, 7], [1, 4, 5, 8], [1, 4, 5, 9], [1, 4, 6, 6], [1, 4, 6, 7], [1, 4, 6, 8], [1, 4, 6, 9], [1, 4, 7, 7], [1, 4, 7, 8], [1, 4, 7, 9], [1, 4, 8, 8], [1, 4, 8, 9], [1, 5, 5, 5], [1, 5, 5, 6], [1, 5, 5, 9], [1, 5, 6, 6], [1, 5, 6, 7], [1, 5, 6, 8], [1, 5, 6, 9], [1, 5, 7, 8], [1, 5, 7, 9], [1, 5, 8, 8], [1, 5, 8, 9], [1, 5, 9, 9], [1, 6, 6, 6], [1, 6, 6, 8], [1, 6, 6, 9], [1, 6, 7, 9], [1, 6, 8, 8], [1, 6, 8, 9], [1, 6, 9, 9], [1, 7, 7, 9], [1, 7, 8, 8], [1, 7, 8, 9], [1, 7, 9, 9], [1, 8, 8, 8], [1, 8, 8, 9], [2, 2, 2, 3], [2, 2, 2, 4], [2, 2, 2, 5], [2, 2, 2, 7], [2, 2, 2, 8], [2, 2, 2, 9], [2, 2, 3, 3], [2, 2, 3, 4], [2, 2, 3, 5], [2, 2, 3, 6], [2, 2, 3, 7], [2, 2, 3, 8], [2, 2, 3, 9], [2, 2, 4, 4], [2, 2, 4, 5], [2, 2, 4, 6], [2, 2, 4, 7], [2, 2, 4, 8], [2, 2, 4, 9], [2, 2, 5, 5], [2, 2, 5, 6], [2, 2, 5, 7], [2, 2, 5, 8], [2, 2, 5, 9], [2, 2, 6, 6], [2, 2, 6, 7], [2, 2, 6, 8], [2, 2, 6, 9], [2, 2, 7, 7], [2, 2, 7, 8], [2, 2, 8, 8], [2, 2, 8, 9], [2, 3, 3, 3], [2, 3, 3, 5], [2, 3, 3, 6], [2, 3, 3, 7], [2, 3, 3, 8], [2, 3, 3, 9], [2, 3, 4, 4], [2, 3, 4, 5], [2, 3, 4, 6], [2, 3, 4, 7], [2, 3, 4, 8], [2, 3, 4, 9], [2, 3, 5, 5], [2, 3, 5, 6], [2, 3, 5, 7], [2, 3, 5, 8], [2, 3, 5, 9], [2, 3, 6, 6], [2, 3, 6, 7], [2, 3, 6, 8], [2, 3, 6, 9], [2, 3, 7, 7], [2, 3, 7, 8], [2, 3, 7, 9], [2, 3, 8, 8], [2, 3, 8, 9], [2, 3, 9, 9], [2, 4, 4, 4], [2, 4, 4, 5], [2, 4, 4, 6], [2, 4, 4, 7], [2, 4, 4, 8], [2, 4, 4, 9], [2, 4, 5, 5], [2, 4, 5, 6], [2, 4, 5, 7], [2, 4, 5, 8], [2, 4, 5, 9], [2, 4, 6, 6], [2, 4, 6, 7], [2, 4, 6, 8], [2, 4, 6, 9], [2, 4, 7, 7], [2, 4, 7, 8], [2, 4, 7, 9], [2, 4, 8, 8], [2, 4, 8, 9], [2, 4, 9, 9], [2, 5, 5, 7], [2, 5, 5, 8], [2, 5, 5, 9], [2, 5, 6, 6], [2, 5, 6, 7], [2, 5, 6, 8], [2, 5, 6, 9], [2, 5, 7, 7], [2, 5, 7, 8], [2, 5, 7, 9], [2, 5, 8, 8], [2, 5, 8, 9], [2, 6, 6, 6], [2, 6, 6, 7], [2, 6, 6, 8], [2, 6, 6, 9], [2, 6, 7, 8], [2, 6, 7, 9], [2, 6, 8, 8], [2, 6, 8, 9], [2, 6, 9, 9], [2, 7, 7, 8], [2, 7, 8, 8], [2, 7, 8, 9], [2, 8, 8, 8], [2, 8, 8, 9], [2, 8, 9, 9], [3, 3, 3, 3], [3, 3, 3, 4], [3, 3, 3, 5], [3, 3, 3, 6], [3, 3, 3, 7], [3, 3, 3, 8], [3, 3, 3, 9], [3, 3, 4, 4], [3, 3, 4, 5], [3, 3, 4, 6], [3, 3, 4, 7], [3, 3, 4, 8], [3, 3, 4, 9], [3, 3, 5, 5], [3, 3, 5, 6], [3, 3, 5, 7], [3, 3, 5, 9], [3, 3, 6, 6], [3, 3, 6, 7], [3, 3, 6, 8], [3, 3, 6, 9], [3, 3, 7, 7], [3, 3, 7, 8], [3, 3, 7, 9], [3, 3, 8, 8], [3, 3, 8, 9], [3, 3, 9, 9], [3, 4, 4, 4], [3, 4, 4, 5], [3, 4, 4, 6], [3, 4, 4, 7], [3, 4, 4, 8], [3, 4, 4, 9], [3, 4, 5, 5], [3, 4, 5, 6], [3, 4, 5, 7], [3, 4, 5, 8], [3, 4, 5, 9], [3, 4, 6, 6], [3, 4, 6, 8], [3, 4, 6, 9], [3, 4, 7, 7], [3, 4, 7, 8], [3, 4, 7, 9], [3, 4, 8, 9], [3, 4, 9, 9], [3, 5, 5, 6], [3, 5, 5, 7], [3, 5, 5, 8], [3, 5, 5, 9], [3, 5, 6, 6], [3, 5, 6, 7], [3, 5, 6, 8], [3, 5, 6, 9], [3, 5, 7, 8], [3, 5, 7, 9], [3, 5, 8, 8], [3, 5, 8, 9], [3, 5, 9, 9], [3, 6, 6, 6], [3, 6, 6, 7], [3, 6, 6, 8], [3, 6, 6, 9], [3, 6, 7, 7], [3, 6, 7, 8], [3, 6, 7, 9], [3, 6, 8, 8], [3, 6, 8, 9], [3, 6, 9, 9], [3, 7, 7, 7], [3, 7, 7, 8], [3, 7, 7, 9], [3, 7, 8, 8], [3, 7, 8, 9], [3, 7, 9, 9], [3, 8, 8, 8], [3, 8, 8, 9], [3, 8, 9, 9], [3, 9, 9, 9], [4, 4, 4, 4], [4, 4, 4, 5], [4, 4, 4, 6], [4, 4, 4, 7], [4, 4, 4, 8], [4, 4, 4, 9], [4, 4, 5, 5], [4, 4, 5, 6], [4, 4, 5, 7], [4, 4, 5, 8], [4, 4, 6, 8], [4, 4, 6, 9], [4, 4, 7, 7], [4, 4, 7, 8], [4, 4, 7, 9], [4, 4, 8, 8], [4, 4, 8, 9], [4, 5, 5, 5], [4, 5, 5, 6], [4, 5, 5, 7], [4, 5, 5, 8], [4, 5, 5, 9], [4, 5, 6, 6], [4, 5, 6, 7], [4, 5, 6, 8], [4, 5, 6, 9], [4, 5, 7, 7], [4, 5, 7, 8], [4, 5, 7, 9], [4, 5, 8, 8], [4, 5, 8, 9], [4, 5, 9, 9], [4, 6, 6, 6], [4, 6, 6, 7], [4, 6, 6, 8], [4, 6, 6, 9], [4, 6, 7, 7], [4, 6, 7, 8], [4, 6, 7, 9], [4, 6, 8, 8], [4, 6, 8, 9], [4, 6, 9, 9], [4, 7, 7, 7], [4, 7, 7, 8], [4, 7, 8, 8], [4, 7, 8, 9], [4, 7, 9, 9], [4, 8, 8, 8], [4, 8, 8, 9], [4, 8, 9, 9], [5, 5, 5, 5], [5, 5, 5, 6], [5, 5, 5, 9], [5, 5, 6, 6], [5, 5, 6, 7], [5, 5, 6, 8], [5, 5, 7, 7], [5, 5, 7, 8], [5, 5, 8, 8], [5, 5, 8, 9], [5, 5, 9, 9], [5, 6, 6, 6], [5, 6, 6, 7], [5, 6, 6, 8], [5, 6, 6, 9], [5, 6, 7, 7], [5, 6, 7, 8], [5, 6, 7, 9], [5, 6, 8, 8], [5, 6, 8, 9], [5, 6, 9, 9], [5, 7, 7, 9], [5, 7, 8, 8], [5, 7, 8, 9], [5, 8, 8, 8], [5, 8, 8, 9], [6, 6, 6, 6], [6, 6, 6, 8], [6, 6, 6, 9], [6, 6, 7, 9], [6, 6, 8, 8], [6, 6, 8, 9], [6, 7, 8, 9], [6, 7, 9, 9], [6, 8, 8, 8], [6, 8, 8, 9], [6, 8, 9, 9], [7, 8, 8, 9]]
【详解】递归回溯,考察基本功 | 679. 24点游戏
思路
-
游戏的第一步是挑出两个数,算出一个新数替代这两个数。
-
然后,在三个数中玩 24 点,再挑出两个数,算出一个数替代它们。
-
然后,在两个数中玩 24 点……
很明显的递归思路。每次递归都会挑出两个数,我们尝试挑出不同的两数组合:
- 挑 1、2,基于它,继续递归。
- 挑 1、3,基于它,继续递归。
- 挑 ……
即通过两层 for 循环,枚举所有的两数组合,展开出不同选择所对应的递归分支。
挑出的每一对数,我们…
- 枚举出所有可能的运算操作:加减乘除…——(对应不同的递归调用)
- 逐个尝试每一种运算——(选择进入一个递归分支)
- 传入长度变小的新数组继续递归——(递归计算子问题)
- 当递归到只剩一个数——(到达了递归树的底部),看看是不是 24 。
- 是就返回 true——结束当前递归,并且控制它不进入别的递归分支,整个结束掉。
- 否则返回 false,离开错误的分支,进入别的递归分支,尝试别的运算。
剪枝小技巧
当递归返回 true,代表游戏成功,不用继续探索了,剩下的搜索分支全部砍掉。怎么做到?
- 代码如下。标识变量
isValid
初始为 false,默认会执行||
后面的递归。 - 一旦某个递归返回真,
isValid
就变为真,由于||
的短路特性,后面的递归不会执行。 - 所有递归子调用都这么写,
isValid
就像一个开关,避免写很多判断语句。
###js
isValid = isValid || judgePoint24([...newNums, n1 + n2]);
isValid = isValid || judgePoint24([...newNums, n1 - n2]);
isValid = isValid || judgePoint24([...newNums, n2 - n1]);
isValid = isValid || judgePoint24([...newNums, n1 * n2]);
// ...
代码
const judgePoint24 = (nums) => {
const len = nums.length;
if (len == 1) { // 递归的出口
return Math.abs(nums[0] - 24) < 1e-9;
}
let isValid = false; // 用于控制是否进入递归
for (let i = 0; i < len; i++) { // 两层循环,枚举出所有的两数组合
for (let j = i + 1; j < len; j++) {
const n1 = nums[i];
const n2 = nums[j]; // 选出的两个数 n1 n2
const newNums = []; // 存放剩下的两个数
for (let k = 0; k < len; k++) {
if (k != i && k != j) { // 剔除掉选出的两个数
newNums.push(nums[k]);
}
}
// 加
isValid = isValid || judgePoint24([...newNums, n1 + n2]);
// 减与被减
isValid = isValid || judgePoint24([...newNums, n1 - n2]);
isValid = isValid || judgePoint24([...newNums, n2 - n1]);
// 乘
isValid = isValid || judgePoint24([...newNums, n1 * n2]);
if (n2 !== 0) { // 除
isValid = isValid || judgePoint24([...newNums, n1 / n2]);
}
if (n1 !== 0) { // 被除
isValid = isValid || judgePoint24([...newNums, n2 / n1]);
}
if (isValid) {
return true;
}
}
}
return false; // 遍历结束,始终没有返回真,则返回false
};
func judgePoint24(nums []int) bool {
floatNums := make([]float64, len(nums))
for i := range floatNums {
floatNums[i] = float64(nums[i])
}
return dfs(floatNums)
}
func dfs(nums []float64) bool {
if len(nums) == 1 {
return math.Abs(nums[0]-24) < 1e-9
}
flag := false
for i := 0; i < len(nums); i++ {
for j := i + 1; j < len(nums); j++ {
n1, n2 := nums[i], nums[j]
newNums := make([]float64, 0, len(nums))
for k := 0; k < len(nums); k++ {
if k != i && k != j {
newNums = append(newNums, nums[k])
}
}
flag = flag || dfs(append(newNums, n1+n2))
flag = flag || dfs(append(newNums, n1-n2))
flag = flag || dfs(append(newNums, n2-n1))
flag = flag || dfs(append(newNums, n1*n2))
if n1 != 0 {
flag = flag || dfs(append(newNums, n2/n1))
}
if n2 != 0 {
flag = flag || dfs(append(newNums, n1/n2))
}
if flag {
return true
}
}
}
return false
}
执行情况
Runtime: 68 ms, faster than 100.00% of JavaScript online submissions for 24 Game.
Runtime: 0 ms, faster than 100.00% of Go online submissions for 24 Game.
感谢阅读,文字经过反复修改打磨,希望你能感受到这份真诚。欢迎提出建议。
最后修改于:2022-01-10
24 点游戏
方法一:回溯
一共有 $4$ 个数和 $3$ 个运算操作,因此可能性非常有限。一共有多少种可能性呢?
首先从 $4$ 个数字中有序地选出 $2$ 个数字,共有 $4 \times 3=12$ 种选法,并选择加、减、乘、除 $4$ 种运算操作之一,用得到的结果取代选出的 $2$ 个数字,剩下 $3$ 个数字。
然后在剩下的 $3$ 个数字中有序地选出 $2$ 个数字,共有 $3 \times 2=6$ 种选法,并选择 $4$ 种运算操作之一,用得到的结果取代选出的 $2$ 个数字,剩下 $2$ 个数字。
最后剩下 $2$ 个数字,有 $2$ 种不同的顺序,并选择 $4$ 种运算操作之一。
因此,一共有 $12 \times 4 \times 6 \times 4 \times 2 \times 4=9216$ 种不同的可能性。
可以通过回溯的方法遍历所有不同的可能性。具体做法是,使用一个列表存储目前的全部数字,每次从列表中选出 $2$ 个数字,再选择一种运算操作,用计算得到的结果取代选出的 $2$ 个数字,这样列表中的数字就减少了 $1$ 个。重复上述步骤,直到列表中只剩下 $1$ 个数字,这个数字就是一种可能性的结果,如果结果等于 $24$,则说明可以通过运算得到 $24$。如果所有的可能性的结果都不等于 $24$,则说明无法通过运算得到 $24$。
实现时,有一些细节需要注意。
-
除法运算为实数除法,因此结果为浮点数,列表中存储的数字也都是浮点数。在判断结果是否等于 $24$ 时应考虑精度误差,这道题中,误差小于 $10^{-6}$ 可以认为是相等。
-
进行除法运算时,除数不能为 $0$,如果遇到除数为 $0$ 的情况,则这种可能性可以直接排除。由于列表中存储的数字是浮点数,因此判断除数是否为 $0$ 时应考虑精度误差,这道题中,当一个数字的绝对值小于 $10^{-6}$ 时,可以认为该数字等于 $0$。
还有一个可以优化的点。
- 加法和乘法都满足交换律,因此如果选择的运算操作是加法或乘法,则对于选出的 $2$ 个数字不需要考虑不同的顺序,在遇到第二种顺序时可以不进行运算,直接跳过。
###Java
class Solution {
static final int TARGET = 24;
static final double EPSILON = 1e-6;
static final int ADD = 0, MULTIPLY = 1, SUBTRACT = 2, DIVIDE = 3;
public boolean judgePoint24(int[] nums) {
List<Double> list = new ArrayList<Double>();
for (int num : nums) {
list.add((double) num);
}
return solve(list);
}
public boolean solve(List<Double> list) {
if (list.size() == 0) {
return false;
}
if (list.size() == 1) {
return Math.abs(list.get(0) - TARGET) < EPSILON;
}
int size = list.size();
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
if (i != j) {
List<Double> list2 = new ArrayList<Double>();
for (int k = 0; k < size; k++) {
if (k != i && k != j) {
list2.add(list.get(k));
}
}
for (int k = 0; k < 4; k++) {
if (k < 2 && i > j) {
continue;
}
if (k == ADD) {
list2.add(list.get(i) + list.get(j));
} else if (k == MULTIPLY) {
list2.add(list.get(i) * list.get(j));
} else if (k == SUBTRACT) {
list2.add(list.get(i) - list.get(j));
} else if (k == DIVIDE) {
if (Math.abs(list.get(j)) < EPSILON) {
continue;
} else {
list2.add(list.get(i) / list.get(j));
}
}
if (solve(list2)) {
return true;
}
list2.remove(list2.size() - 1);
}
}
}
}
return false;
}
}
###C++
class Solution {
public:
static constexpr int TARGET = 24;
static constexpr double EPSILON = 1e-6;
static constexpr int ADD = 0, MULTIPLY = 1, SUBTRACT = 2, DIVIDE = 3;
bool judgePoint24(vector<int> &nums) {
vector<double> l;
for (const int &num : nums) {
l.emplace_back(static_cast<double>(num));
}
return solve(l);
}
bool solve(vector<double> &l) {
if (l.size() == 0) {
return false;
}
if (l.size() == 1) {
return fabs(l[0] - TARGET) < EPSILON;
}
int size = l.size();
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
if (i != j) {
vector<double> list2 = vector<double>();
for (int k = 0; k < size; k++) {
if (k != i && k != j) {
list2.emplace_back(l[k]);
}
}
for (int k = 0; k < 4; k++) {
if (k < 2 && i > j) {
continue;
}
if (k == ADD) {
list2.emplace_back(l[i] + l[j]);
} else if (k == MULTIPLY) {
list2.emplace_back(l[i] * l[j]);
} else if (k == SUBTRACT) {
list2.emplace_back(l[i] - l[j]);
} else if (k == DIVIDE) {
if (fabs(l[j]) < EPSILON) {
continue;
}
list2.emplace_back(l[i] / l[j]);
}
if (solve(list2)) {
return true;
}
list2.pop_back();
}
}
}
}
return false;
}
};
###C
const int TARGET = 24;
const double EPSILON = 1e-6;
const int ADD = 0, MULTIPLY = 1, SUBTRACT = 2, DIVIDE = 3;
bool solve(double *l, int l_len) {
if (l_len == 0) {
return false;
}
if (l_len == 1) {
return fabs(l[0] - TARGET) < EPSILON;
}
int size = l_len;
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
if (i != j) {
double list2[20];
int l2_len = 0;
for (int k = 0; k < size; k++) {
if (k != i && k != j) {
list2[l2_len++] = l[k];
}
}
for (int k = 0; k < 4; k++) {
if (k < 2 && i > j) {
continue;
}
if (k == ADD) {
list2[l2_len++] = l[i] + l[j];
} else if (k == MULTIPLY) {
list2[l2_len++] = l[i] * l[j];
} else if (k == SUBTRACT) {
list2[l2_len++] = l[i] - l[j];
} else if (k == DIVIDE) {
if (fabs(l[j]) < EPSILON) {
continue;
}
list2[l2_len++] = l[i] / l[j];
}
if (solve(list2, l2_len)) {
return true;
}
l2_len--;
}
}
}
}
return false;
}
bool judgePoint24(int *nums, int numsSize) {
double l[20];
int l_len = 0;
for (int i = 0; i < numsSize; i++) {
l[l_len++] = nums[i];
}
return solve(l, l_len);
}
###Python
class Solution:
def judgePoint24(self, nums: List[int]) -> bool:
TARGET = 24
EPSILON = 1e-6
ADD, MULTIPLY, SUBTRACT, DIVIDE = 0, 1, 2, 3
def solve(nums: List[float]) -> bool:
if not nums:
return False
if len(nums) == 1:
return abs(nums[0] - TARGET) < EPSILON
for i, x in enumerate(nums):
for j, y in enumerate(nums):
if i != j:
newNums = list()
for k, z in enumerate(nums):
if k != i and k != j:
newNums.append(z)
for k in range(4):
if k < 2 and i > j:
continue
if k == ADD:
newNums.append(x + y)
elif k == MULTIPLY:
newNums.append(x * y)
elif k == SUBTRACT:
newNums.append(x - y)
elif k == DIVIDE:
if abs(y) < EPSILON:
continue
newNums.append(x / y)
if solve(newNums):
return True
newNums.pop()
return False
return solve(nums)
###golang
const (
TARGET = 24
EPSILON = 1e-6
ADD, MULTIPLY, SUBTRACT, DIVIDE = 0, 1, 2, 3
)
func judgePoint24(nums []int) bool {
list := []float64{}
for _, num := range nums {
list = append(list, float64(num))
}
return solve(list)
}
func solve(list []float64) bool {
if len(list) == 0 {
return false
}
if len(list) == 1 {
return abs(list[0] - TARGET) < EPSILON
}
size := len(list)
for i := 0; i < size; i++ {
for j := 0; j < size; j++ {
if i != j {
list2 := []float64{}
for k := 0; k < size; k++ {
if k != i && k != j {
list2 = append(list2, list[k])
}
}
for k := 0; k < 4; k++ {
if k < 2 && i < j {
continue
}
switch k {
case ADD:
list2 = append(list2, list[i] + list[j])
case MULTIPLY:
list2 = append(list2, list[i] * list[j])
case SUBTRACT:
list2 = append(list2, list[i] - list[j])
case DIVIDE:
if abs(list[j]) < EPSILON {
continue
} else {
list2 = append(list2, list[i] / list[j])
}
}
if solve(list2) {
return true
}
list2 = list2[:len(list2) - 1]
}
}
}
}
return false
}
func abs(x float64) float64 {
if x < 0 {
return -x
}
return x
}
复杂度分析
-
时间复杂度:$O(1)$。一共有 $9216$ 种可能性,对于每种可能性,各项操作的时间复杂度都是 $O(1)$,因此总时间复杂度是 $O(1)$。
-
空间复杂度:$O(1)$。空间复杂度取决于递归调用层数与存储中间状态的列表,因为一共有 $4$ 个数,所以递归调用的层数最多为 $4$,存储中间状态的列表最多包含 $4$ 个元素,因此空间复杂度为常数。
vite和webpack打包结构控制
概述
在工程化项目中,Vite 和 Webpack 作为当前最流行的两大构建工具,它们在打包输出目录结构的配置上各有特点,webpack和vite默认打包构建的输出目录结构可能不满足我们的需求,因此需要根据实际情况进行控制。
默认输出结构对比
webpack
默认情况下,基本上都不会根据情况进行分块,所有资源都是默认被打包到了一个文件中。
dist/
├── main.js
├── index.html
|—— .....
vite
dist/
├── assets/
│ ├── index.[hash].js
│ ├── vendor.[hash].js
│ └── style.[hash].css
└── index.html
Vite目录结构精细控制
文件指纹策略
Webpack 提供了多种 hash 类型:
-
[hash]
: 项目级hash -
[chunkhash]
: chunk级hash -
[contenthash]
: 内容级hash
基础配置方案
// vite.config.js
export default {
build: {
outDir: 'dist',
assetsDir: 'static',
emptyOutDir: true
}
}
Rollup 输出配置
由于vite内部打包使用rollup,因此打包输出相关配置需参考rollup的配置
export default {
build: {
rollupOptions: {
output: {
//资源块输出目录配置
chunkFileNames: 'static/js/[name]-[hash].js',
//入口文件输出目录配置
entryFileNames: 'static/js/[name]-[hash].js',
//静态输出目录配置(图片、音频、字体)
assetFileNames: ({ name }) => {
const ext = name.split('.').pop()
//函数形式动态返回文件输出名及其位置
return `static/${ext}/[name]-[hash].[ext]`
}
}
}
}
}
webpack 目录结构精细控制
基础输出配置
// webpack.config.js
module.exports = {
output: {
path: path.resolve(__dirname, 'build'), // 修改输出目录
filename: 'js/[name].[contenthash:8].js', // JS文件输出路径
chunkFilename: 'js/[name].[contenthash:8].chunk.js', // 异步chunk
assetModuleFilename: 'media/[name].[hash:8][ext]', // 静态资源
clean: true // 构建前清空目录
}
}
高级资源管理
使用 mini-css-extract-plugin
控制 CSS 输出:
module: {
rules: [
{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
],
},
Webpack 5 引入了资源模块类型,取代了传统的 file-loader/url-loader,用来处理之前繁琐的配置
module: {
rules: [
{
test: /.(png|jpe?g|gif|svg)$/i,
type: 'asset/resource' // 替换 file-loader
},
{
test: /.(mp4|webm|ogg)$/i,
type: 'asset/resource'
}
],
},
四种资源模块类型
类型 | 作用 | 等价 loader |
---|---|---|
asset/resource |
导出单独文件并生成 URL | file-loader |
asset/inline |
导出资源的 Data URI | url-loader |
asset/source |
导出资源的源代码 | raw-loader |
asset |
自动选择 resource 或 inline | url-loader + 限制 |
总结
上面列举的部分配置,更多的详细配置,可以查阅官网解析。
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
AI 代理是什么,其有助于我们实现更智能编程
Trae助力,可视化面板登录页面接口联调与提测一气呵成
在鸿蒙里优雅地处理网络错误:从 Demo 到实战案例
promise & async await总结
promise
- promise 有三种状态 等待(pedding),成功(resolve) 拒绝(reject)
- promise.then 会等待promise内部resolve,并且promise的executor执行完,接着将.then里面的东西放入微任务队列。new promise.resolve().then(callback) callback也会放入微任务队列
- promise.then()的返回值始终是一个新的promise对象。promise的状态是.then回调的返回值。
- promise.then中的回调会等待前一个promise的resolve,前一个.then函数没返回值会等到执行完成之后自动resolve
- Promise.then会立即返回一个状态为pending的新Promise对象,这正是它能够支持链式调用的关键。
- new 的promise不会自动resolve,但是.then里面会自动resolve。
碰到.then 跳过 找同步代码执行完成 回来
catch跟then一样,都会将任务放入微任务队列中。
async await
- await 会等待右侧的promise完成之后将剩余代码放入微任务队列。
- async对显式返回创建的Promise的处理会安排一个额外的微任务。
前端必会:如何创建一个可随时取消的定时器
一、原生的取消方式
JavaScript 原生就提供了取消定时器的方法。setTimeout
和 setInterval
在调用时都会返回一个数字类型的 ID,我们可以将这个 ID 传递给 clearTimeout
或 clearInterval
来取消它。
// 1. 设置一个定时器
const timerId: number = setTimeout(() => {
console.log("这个消息可能永远不会被打印");
}, 2000);
// 2. 在它触发前取消它
clearTimeout(timerId);
常见痛点:
-
timerId
变量需要被保留在组件或模块的作用域中,状态分散。 - 启动、暂停、取消的逻辑是割裂的,代码可读性和可维护性差。
二、封装一个可取消的定时器类
我们可以简单的封装一个 CancellableTimer
类,将定时器的状态和行为内聚在一起。后续可以扩展,把项目中的所有定时器进行统一管理。
// 定义定时器ID类型
type TimeoutId = ReturnType<typeof setTimeout>;
class CancellableTimer {
private timerId: TimeoutId | null = null;
constructor(private callback: () => void, private delay: number) {}
public start(): void {
// 防止重复启动
if (this.timerId !== null) {
this.cancel();
}
this.timerId = setTimeout(() => {
this.callback();
// 执行完毕后重置 timerId
this.timerId = null;
}, this.delay);
}
public cancel(): void {
if (this.timerId !== null) {
clearTimeout(this.timerId);
this.timerId = null;
}
}
}
// 使用示例
console.log('定时器将在3秒后触发...');
const myTimer = new CancellableTimer(() => {
console.log('定时器任务执行!');
}, 3000);
myTimer.start();
// 模拟在1秒后取消
setTimeout(() => {
console.log('用户取消了定时器。');
myTimer.cancel();
}, 1000);
三、实现可暂停和恢复的定时器
在很多场景下,我们需要的不仅仅是取消,还有暂停和恢复。
要实现这个功能,我们需要在暂停时记录剩余时间。
type TimeoutId = ReturnType<typeof setTimeout>;
class AdvancedTimer {
private timerId: TimeoutId | null = null;
private startTime: number = 0;
private remainingTime: number;
private callback: () => void;
private delay: number;
constructor(callback: () => void, delay: number) {
this.remainingTime = delay;
this.callback = callback;
this.delay = delay;
}
public resume(): void {
if (this.timerId) {
return; // 已经在运行
}
this.startTime = Date.now();
this.timerId = setTimeout(() => {
this.callback();
// 任务完成,重置
this.remainingTime = this.delay;
this.timerId = null;
}, this.remainingTime);
}
public pause(): void {
if (!this.timerId) {
return;
}
clearTimeout(this.timerId);
this.timerId = null;
// 计算并更新剩余时间
const timePassed = Date.now() - this.startTime;
this.remainingTime -= timePassed;
}
public cancel(): void {
if (this.timerId) {
clearTimeout(this.timerId);
}
this.timerId = null;
this.remainingTime = this.delay; // 重置
}
}
// 使用示例
console.log('定时器启动,5秒后执行...');
const advancedTimer = new AdvancedTimer(() => console.log('Done!'), 5000);
advancedTimer.resume();
setTimeout(() => {
console.log('2秒后暂停定时器');
advancedTimer.pause();
}, 2000);
setTimeout(() => {
console.log('4秒后恢复定时器 , 应该还剩3秒');
advancedTimer.resume();
}, 4000);
总结
如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货