普通视图

发现新文章,点击刷新页面。
今天 — 2025年8月18日掘金 前端

2025.8.18实验室【代码跑酷指南】Jupyter Notebook程序员的魔法本:cpolar内网穿透实验室第622个成功挑战

软件名称:Jupyter Notebook 操作系统支持: 全平台通吃!Windows、Linux、macOS都能玩,甚至还能在树莓派上开个“迷你数据科学工作站”。 软件介绍 Jupyter是“交互式

nginx 如何配置防止慢速攻击 🤔🤔🤔

作者 Moment
2025年8月18日 09:01

最近在出一个前端的体系课程,里面的内容非常详细,如果你感兴趣,可以加我 v 进行联系 yunmz777:

image.png

浪费你几秒钟时间,内容正式开始

慢速攻击是一类用很少带宽就能长期占用服务器连接/资源的攻击方式。攻击者通过非常慢地发送请求头或请求体,或极慢地读取服务器响应,让每个连接都“挂着不结束”,从而耗尽 Web 服务器(或上游应用、数据库、代理)的并发与缓冲资源。

典型类型主要有以下几个方面:

  • Slowloris(慢请求头):客户端以极低速率分片发送 HTTP 头部,始终不把头部发完,服务器就一直等待。

  • Slow POST / RUDY(慢请求体):先宣称要上传较大的 Content-Length,然后以极慢速率发送请求体,服务器为其保留缓冲与上游连接。

  • Slow Read(慢读取响应):客户端窗口/读取速率极低,迫使服务器缓冲并保持连接很久(尤其响应较大时)。

  • HTTP/2 变种:滥用单连接多流(streams)和窗口控制:开很多流但每流都很慢,放大资源占用。

20250818085811

传统 DDoS 依靠高带宽与高包速直接压垮网络/设备,而慢速攻击用极低带宽长期占用连接,更隐蔽,常被误认为是网络状况差的正常用户。

现场通常会看到活跃连接(尤其 reading)持续攀升,但总体带宽并不高。

error_log 中频繁出现 client timed outclient sent invalid header while reading client request headers 等信息。

上游服务看似空闲却体验发卡,Nginx 的 429/502/504 增多,访问日志还能发现同一 IP 维持大量长期未完成的请求或异常长的响应时间。

  • 429 表示“请求过多被限流”,通常稍后或按 Retry-After 重试即可。

  • 502 表示“网关收到上游无效响应或连不上上游”,多见于上游挂掉、拒连或协议不匹配。

  • 504 表示“等待上游超时”,通常是上游处理太慢一直没回。

如何防护

核心目标就是尽快关闭拖延发送请求头/请求体或极慢读取响应的连接,限制单 IP 的并发与速率,避免慢连接占满 workerworker_connections 与上游资源。

  • 收紧超时:client_header_timeoutclient_body_timeoutsend_timeoutkeepalive_timeout

  • 超时立刻复位:reset_timedout_connection on; 减少 TIME_WAIT/ 资源滞留。

  • 限并发/限速:limit_connlimit_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"; }
    }
}

除此之外,还有一些数值上的建议:

  1. 高频 API 可将 rate 调小、burst 适度放大;页面类流量可相反。

  2. 对上传较多的业务,将 client_body_timeoutproxy_request_buffering on; 组合尤为关键。

  3. 如果公网复杂、遭遇中等强度慢攻:client_header_timeout 2-3sclient_body_timeout 5-8ssend_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,避免误伤。

20250818085955

总结

慢速攻击是用极低带宽长期占用服务器连接/缓冲的攻击:攻击者故意慢发请求头/请求体或慢读响应,让连接一直不结束,耗尽并发与内存。

常见形态有 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;同时对敏感路径更严、对可信来源适度放宽或白名单以减少误杀。

栗子前端技术周刊第 94 期 - React Native 0.81、jQuery 4.0.0 RC1、Bun v1.2.20...

2025年8月18日 08:50

🌰栗子前端技术周刊第 94 期 (2025.08.11 - 2025.08.17):浏览前端一周最新消息,学习国内外优秀文章视频,让我们保持对前端的好奇心。

📰 技术资讯

  1. React Native 0.81:React Native 0.81 版本新增了对 Android 16 的支持,提升了 iOS 构建速度,并进行了一系列稳定性改进。

  2. jQuery 4.0.0 RC1:jQuery 4.0.0 首个候选版本已发布,团队认为它已接近完成,希望开发者进行广泛测试,若未发现需修复的问题,将直接推出正式版本 jQuery 4.0.0。新版本主要变化与亮点包括:不再支持 IE 11 以下版本、删减遗留代码与弃用 API、引入精简版本等等。

  3. Bun v1.2.20:Bun v1.2.20 修复了 141 个问题,并带来了显著的性能提升,包括降低空闲状态下的 CPU 使用率,以及将 AbortSignal.timeout 的速度提升 40 倍。

📒 技术文章

  1. What we learned from creating PostCSS:那些从创建 PostCSS 中所学到的经验 - 12 年前,作者创建了 PostCSS —— 一款 CSS 自动化工具,其月下载量达 4 亿次,被谷歌、维基百科、Tailwind 以及 38% 的开发者所使用。在本文中,作者将分享在维护这个热门开源项目的漫长历程中所学到的经验。

  2. How to Use innerHTML, innerText, and textContent Correctly in JavaScript:如何在 JavaScript 中正确使用 innerHTMLinnerTexttextContent - 本文将解释 JavaScript 中三个 DOM 属性的区别:innerHTML 返回包含标签的完整 HTML 内容,innerText 仅返回受 CSS 规则影响的可见样式文本,而 textContent 则返回所有文本内容,无论其在 CSS 中是否可见。这三个属性在 DOM 操作中适用于不同的使用场景。

  3. 前端必学-完美组件封装原则:此文总结了作者多年组件封装经验,以及拜读 antdelement-plusvantfusion 等多个知名组件库所提炼的完美组件封装的经验;是一个开发者在封装项目组件,公共组件等场景时非常有必要遵循的一些原则。

🔧 开发工具

  1. ReactJS Cheatsheet:一份简洁且对开发者友好的 ReactJS 速查表,汇总了核心概念、必备 Hooks、路由、性能优化技巧以及 React 18 的新特性。
image-20250816142106838
  1. vue-scan:让你的组件在每次更新时都闪现红色边框,帮助你排查性能问题。
image-20250817090008862
  1. react-json-view:react-json-view(简称 rjv)是一个用于展示和编辑 JavaScript 数组及 JSON 对象的 React 组件。
image-20250817090054910

🚀🚀🚀 以上资讯文章选自常见周刊,如 JavaScript Weekly 等,周刊内容也会不断优化改进,希望你们能够喜欢。

💖 欢迎关注微信公众号:栗子前端

React状态更新踩坑记:我是这样优雅修改参数的

2025年8月18日 08:47

大家好,我是小杨,一名有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,使用setStateuseState的更新函数
  • 对象/数组更新时,创建新引用
  • 复杂状态逻辑用useReducer
  • 连续更新用函数式setState
  • useEffect依赖要写全

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

Next.js 教程系列(二十七)React Server Components (RSC) 与未来趋势

作者 鲫小鱼
2025年8月18日 08:45

前言

大家好,我是鲫小鱼。是一名不写前端代码的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!

第二十七章: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[完整交互页面]

渲染流程:

  1. 服务端渲染:Server Components 在服务端执行,生成 RSC Payload。
  2. 流式传输:RSC Payload 通过流式传输发送到客户端。
  3. 客户端水合:Client Components 在客户端水合,处理交互。
  4. 混合渲染: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、闭包、递归、尾调用、柯里化,一次性吃透

作者 excel
2025年8月18日 08:20

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. 参数个数/类型分派
  2. 可选对象参数(推荐,可扩展性强)
  3. 重载表(按规则挑处理器)
// 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 变量名提升为 undefinedlet/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/reducecompose/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) argumentscallee

  • arguments:类数组、动态实参集合;不建议新代码依赖。
  • arguments.callee严格模式禁用,不推荐使用(可用命名函数表达式替代)。
function show() {
  console.log(arguments.length); // 实参个数
}

(2) this

绑定规则优先级

  1. new 绑定(构造调用)
  2. 显式绑定:call/apply/bind
  3. 隐式绑定:作为对象方法调用 obj.fn()
  4. 默认绑定:非严格 this===window;严格模式 undefined
  5. 箭头函数:词法 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)

常见来源

  1. 全局变量/意外挂到全局
  2. 未清理的定时器/订阅/事件监听
  3. 闭包长期持有大对象/DOM 引用
  4. 脱离文档的 DOM 被引用(detached DOM)
  5. 无界缓存(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 Moduleexport / 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 校验)。
  • 性能:频繁创建闭包/绑定函数有成本,复用或上移到外层作用域。
昨天 — 2025年8月17日掘金 前端

vite和webpack打包结构控制

作者 gnip
2025年8月17日 22:45

概述

在工程化项目中,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 + 限制

总结

上面列举的部分配置,更多的详细配置,可以查阅官网解析。

promise & async await总结

作者 ZXT
2025年8月17日 21:48

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的处理会安排一个额外的微任务。

前端必会:如何创建一个可随时取消的定时器

作者 烛阴
2025年8月17日 20:58

一、原生的取消方式

JavaScript 原生就提供了取消定时器的方法。setTimeoutsetInterval 在调用时都会返回一个数字类型的 ID,我们可以将这个 ID 传递给 clearTimeoutclearInterval 来取消它。

// 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开发干货

Swift 应用在安卓系统上会怎么样?

作者 JarvanMo
2025年8月17日 20:34

欢迎关注我的公众号:OpenFlutter,感谢。

当我们想到 Swift 时,通常会想到 iPhone、iPad、Mac。基本上,任何带有闪亮 Apple 标志的产品。

但安卓?

那感觉有点……不对劲。

然而,一些有趣的事情正在发生:Swift——这门由苹果创建的语言——正在安卓上悄然使用。这不仅仅是实验,也不仅仅是业余项目。它通过 Swift.org 支持的一项正式工作,拥有适当的工具、文档和日益增长的社区支持。

那么,当你在安卓上运行 Swift 时,究竟会发生什么呢?

让我们清晰、实际地,并基于截至 2025 年正在进行的实际工作来分解一下。


Swift 在安卓上的核心要点

  • Swift 可以在安卓上使用,但不是用于用户界面 (UI)。
  • 它主要用于跨平台共享业务逻辑
  • 截至 2025 年 6 月,有一个官方的 Swift Android 工作组
  • 它使用 JNI将 Swift 代码桥接到安卓应用中。
  • 你需要使用 CMake + Gradle 设置自定义工具链

这不是要用 Swift 完全重写安卓开发。但它确实是一种在 iOS 和安卓之间共享逻辑的合法方式,而且无需放弃你熟悉的语言。

背景:Swift Android 工作组 (2025)

2025 年 6 月,Swift.org 团队宣布成立一个新的正式工作组:

🔗 安卓工作组

他们的使命是?

“为了指导和支持 Swift 不断壮大的安卓用户社区,确保提供高质量、可持续的工具,以及出色的开发者体验。”

这不是一次实验。它是 Swift 旨在扩展到苹果平台之外的更广泛目标的一部分——就像他们的 WebAssembly 和 C++ 互操作工作组一样。

该工作组的重点是:

  • 维护安卓工具链
  • 改进 Swift 的核心库(例如 Foundation)以支持安卓
  • 支持与安卓的 Gradle、NDK 和 JNI 集成
  • 让编写共享 Swift 库并在安卓应用中使用变得更容易

那么……它到底是如何工作的呢?

假设你有一些 Swift 逻辑——比如你应用的认证层:

public class AuthManager {
    public static func hash(password: String) -> String {
        return password.reversed() + "123" // 虚拟逻辑
    }
}

以下是你如何在安卓上使用它:

1. 将 Swift 编译成安卓 .so

你需要使用 swift-android-toolchain 将 Swift 代码编译成一个共享的本地库(例如 libauth.so)。

为此,你需要准备:

  • Android NDK
  • Swift 工具链(目标是 aarch64-linux-android
  • CMake/Gradle 构建脚本

2. 使用 JNI 将 Swift 桥接到 Kotlin

在 Kotlin 中:

external fun hash(password: String): String
init {
    System.loadLibrary("auth")
}

现在你的 Kotlin 应用可以调用你的 Swift 逻辑了——就像调用原生的 C 代码一样。

虽然这有点粗糙,但确实可行。

你能做什么,不能做什么

你可以:

  • 用 Swift 编写业务逻辑
  • 构建静态或动态 Swift 库
  • 使用 Foundation、Dispatch 和其他核心库(存在一些限制)
  • 通过 JNI 从 Kotlin 调用 Swift

你不能:

  • 用 Swift 编写 Android UI(不支持 SwiftUI 或 UIKit)
  • 使用依赖于 Apple 专属框架(例如 AVKit)的 Swift 包
  • 期望在 Android Studio 或 Xcode 中获得完整的 Swift 工具链支持

工具、文档和示例

以下是关键资源:

  • 🔧 swift-android-toolchain: 由 Swift Android 工作组维护的官方工具链
  • 📄 Android Workgroup Charter: 官方使命和职责章程
  • 📦 Readdle Swift Android Sample: 一个使用 CMake 和 Swift 共享代码的实际工作示例
  • 💬 Swift 论坛 — Android 讨论区: 活跃的贡献者社区和最新动态

架构是怎样的?

以下是其工作原理的简化图:

[iOS]
Swift UI + Swift 逻辑
         ↑
     共享 Swift 代码
         ↓
[Android]
Kotlin UI + JNI 桥接 → Swift .so 库

你用 Swift 编写一次代码(例如,模型、逻辑),然后将其桥接到两个平台,使用各自平台原生的 UI。

注意事项与陷阱

这不是即插即用的。你会遇到:

  • 繁琐的 JNI 桥接
  • 陡峭的工具链设置(除非你习惯了原生开发)
  • 针对 Swift 崩溃的 Android 调试能力有限
  • Android 目标尚不支持 Swift 包管理器 (SPM)

最大的问题是什么?你需要手动编写 CMake + Gradle 脚本。目前还没有图形用户界面工具。

跨平台 Swift UI 会出现吗?

目前还不会。

目前,没有迹象表明 SwiftUI 会被移植到安卓。Swift 安卓工作组只专注于非 UI 集成——即逻辑、库和系统支持。

但基础正在奠定。

如果社区的势头持续增长,未来可能有人会在原生安卓视图之上构建一个 类似 SwiftUI 的抽象层。就像 Flutter 一样,但使用 Swift。

现在预测还为时过早,但并非不可能。

总结:为什么要费这个劲?

你可能会问——如果 Kotlin 在安卓上是官方且原生的,为什么还要用 Swift?

这是个合理的问题。

而这是一个合理的答案:

如果你的团队主要使用 Swift 并且以 iOS 开发为主,那么在安卓上使用 Swift 能让你复用自身优势,避免逻辑重复,并加快开发速度

这并非适用于所有团队。但对合适的团队来说,它能实现真正的跨平台开发,同时又不牺牲原生性能或 Swift 的易用性。

Vue3之计算属性

作者 littleding
2025年8月17日 18:33

Vue3的计算属性为了解决依赖响应式状态的复杂逻辑

基本用法

<script setup> import { reactive, computed } from 'vue' 

const author = reactive({ 
    name: 'John Doe', 
    books: [ 
        'Vue 2 - Advanced Guide', 
        'Vue 3 - Basic Guide', 
        'Vue 4 - The Mystery' 
    ] 
}) 

// 一个计算属性 ref 
const publishedBooksMessage = computed(() => {
    return author.books.length > 0 ? 'Yes' : 'No' 
}) 
    
</script> 

<template> 
    <p>Has published books:</p> <span>{{ publishedBooksMessage }}</span>     
</template>

这里定义了一个计算属性 publishedBooksMessagecomputed() 方法期望接收一个 getter 函数,返回值为一个计算属性 ref。和其他一般的 ref 类似,你可以通过 publishedBooksMessage.value 访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value

Vue 的计算属性会自动追踪响应式依赖。它会检测到 publishedBooksMessage 依赖于 author.books,所以当 author.books 改变时,任何依赖于 publishedBooksMessage 的绑定都会同时更新。

不同写法

要绑定一个动态类

写法一

const isActive = ref(true) 
const error = ref(null) 
const classObject = computed(() => ({ 
    active: isActive.value && !error.value, 
    'text-danger': error.value && error.value.type === 'fatal' 
}))

<div :class="classObject"></div>

写法二

const isActive = ref(true) 
const error = ref(null) 
const classObject = computed(() => { 
    return {
        active: isActive.value && !error.value, 
        'text-danger': error.value && error.value.type === 'fatal' 
    }
})

<div :class="classObject"></div>

两种计算属性写法

写法一 用了对象字面量的简写形式(省略了return和大括号),直接返回一个对象。这是 ES6 的箭头函数语法:当函数体只有一句返回语句且为对象时,可以省略return和外层大括号,用小括号包裹对象。

写法二 用了完整的函数体语法,显式写出return语句。这种写法更适合函数体可能扩展的场景(比如未来需要添加更多逻辑,如条件判断、变量计算等)

当我把前端条件加载做到极致

作者 Linsk
2025年8月17日 18:28

各位网友晚上好,相信大家都对使用type="module"和nomodule这种技巧有所耳闻。今天我将为大家分享我把这种技巧应用到极致的经验:4 条件加载方案。

什么是分条件加载

在前端构建中,我们通常需要进行 Babel 语法降级和 polyfill 插入等操作以兼容低版本浏览器。但这会导致一个问题:在现代浏览器中也会加载大量兼容性代码,无法享受浏览器原生支持的新特性带来的性能优势。

分条件加载的核心思想是:为不同能力的浏览器提供差异化的构建产物。最基础的实现就是利用 HTML5 标准中的type="module"和nomodule属性进行区分:

<!-- 仅供参考 -->
<script type="module" src="modern-bundle.js"></script>
<script nomodule src="legacy-bundle.js"></script>

这样在支持 ES Module 的浏览器中会加载type="module"的脚本,而不支持的浏览器会忽略这个标签并加载带有nomodule属性的脚本。通过这种方式,现代浏览器就能直接运行原生 class、ES Module、解构赋值等新特性,无需加载冗余的兼容性代码。

3 个条件的分条件加载

由于IE8与IE9之间有巨大的差异,因此在上述2个版本的构建产物之上,使用条件注释在分出第3个版本专用于IE8及以下。

<!-- 仅供参考 -->
<!--[if gte IE 9]><!-->
<script type="module" src="modern-bundle.js"></script>
<script nomodule src="legacy-bundle.js"></script>
<!--><![endif]-->
<!--[if lte IE 8]>
  <script src="ie8-bundle.js"></script>
<![endif]-->

这样IE8及以下加载特制兼容包可以更加精简,其他版本也能避开恶心的兼容代码。

为什么需要分4条件加载

分3个版本似乎己经够用了,但是随着top-level-await的出现,变得不够用了。使用type="module"和nomodule分版本的一个目的就是能使用原生esmodule。而top-level-top出现后,无法用旧有的esmodule实现top-level-await。因此只能使用传统方式,比如构建成AMD格式,用AMD加载器加载。因此我们可以进一步细分出一个版本。 现在我们有4个版本了

  • IE8及以下
  • 不支持原生esm的版本(chrome4~chrome60)
  • 支持原生esm但不支持top-level-await(chrome61~chrome89)
  • 原生支持top-level-await的浏览器(chrome90+)

注意哦,上面最新的版本是chrome90,现在都chrome130+了,随着浏览器的更新,我希望用新浏览器的用户能够直接运行新特性,因此我需要用一个较新的特性做区分,于是我选择了Promise.try。现在的4个版本改变为

  • IE8及以下
  • 不支持原生esm的版本(chrome4~chrome60)
  • 支持原生esm但不支持Promise.try(chrome61~chrome127)
  • 原生支持Promise.try的浏览器(chrome128+)

这里我选择了Promise.try是因为我还没有在公司推开es2025的使用,业务人员是不允许使用es2025的特性,代码中也必然不存在es2025的代码。等我在公司构建工具链中做完了es2025的降级处理,就会把用Promise.try判断,改成用更新的特性来判断。

<!-- 仅供参考 -->
<!--[if gte IE 9]><!-->
<script type="module">
if(Promise.try){__import__("xxxx")}else{__import__("yyyy")}
</script>
<script nomodule src="legacy-bundle.js"></script>
<!--><![endif]-->
<!--[if lte IE 8]>
  <script src="ie8-bundle.js"></script>
<![endif]-->

最后我们看看打包后的运行效果。我们发现在最新浏览器中没有polyfill引用,也可以原生运行top-level-await。

总结

通过多条件加载,可以在全浏览器兼容的前提下,减少新式浏览器的加载内容大小。分4版本条件加载,是做到极致的水平。

❌
❌