普通视图

发现新文章,点击刷新页面。
今天 — 2025年8月13日首页

Next.js 中间件:掌握请求拦截与处理的核心机制(六)

2025年8月12日 23:58

Next.js 中间件是什么?

Next.js 中间件是一个在 请求处理 管道中运行的函数,它能够在请求到达页面或 API 路由之前 拦截请求 并对其进行处理。这种机制允许开发者执行各种操作,如 身份验证、重定向、请求修改、日志记录 等,而无需在每个路由中重复编写相同的代码。

核心原理:

Next.js 中间件基于 Edge Runtime(边缘运行时)构建,这意味着它们可以在全球的 CDN 边缘节点上运行。这种设计带来了显著的性能优势:

  1. 低延迟: 中间件在离用户最近的边缘节点执行,减少了请求到达源服务器的往返时间,从而加快了响应速度。
  2. 高扩展性: 边缘运行时能够轻松处理大量并发请求,无需担心服务器过载。
  3. 安全性: 可以在请求到达你的应用核心逻辑之前,进行身份验证、授权和安全检查。

中间件的生命周期与执行流程

Next.js 中间件的生命周期与请求处理流程紧密相连,它在请求到达应用程序代码之前或之后执行。理解其生命周期有助于更好地设计和实现中间件逻辑。

生命周期阶段

  1. 请求进入 (Incoming Request)

    • 当客户端向 Next.js 应用发送请求时,中间件是第一个接收到请求的组件。
    • 此时,中间件可以访问原始的 NextRequest 对象,其中包含了请求的所有信息(URL、Headers、Cookies 等)。
  2. 中间件执行 (Middleware Execution)

    中间件函数开始执行。在这个阶段,可以根据业务逻辑对请求进行处理。你可以执行以下操作:

    • 读取请求信息:检查请求路径、查询参数、请求头、Cookie 等。
    • 修改请求:例如,添加或修改请求头,或者根据逻辑重写 URL。
    • 执行逻辑判断:进行身份验证、权限检查、A/B 测试分流等。
    • 生成响应:直接返回一个 NextResponse 对象,从而终止请求并发送响应给客户端(例如,重定向到登录页,或返回错误信息)。
  3. 响应生成与传递 (Response Generation and Passing)

    • 如果中间件没有直接返回响应(即调用了 NextResponse.next()NextResponse.rewrite()),请求会继续传递。
    • NextResponse.next():请求继续流向匹配的 Next.js 页面或 API 路由。这是最常见的操作,表示中间件完成了它的任务,允许请求继续正常处理。
    • NextResponse.rewrite(url):请求被内部重写到另一个 URL。浏览器地址栏不会改变,但服务器会处理重写后的路径。这常用于美化 URL、国际化路由或将旧路径映射到新路径。
    • NextResponse.redirect(url):向客户端发送一个重定向响应(HTTP 307 或 308)。浏览器会收到重定向指令并加载新的 URL。这常用于未授权访问、强制 HTTPS 或处理旧链接。
  4. 页面/API 路由处理 (Page/API Route Handling)

    • 如果请求通过中间件并被允许继续,它将到达 Next.js 应用程序中匹配的页面组件或 API 路由处理程序。
    • 这些组件会生成最终的 HTML 页面或 API 响应。
  5. 响应返回 (Response Return)

    • 最终的响应(无论是中间件直接生成的,还是页面/API 路由生成的)会返回给客户端。
    • 在返回之前,中间件有机会再次修改响应头(例如,添加安全策略头、设置新的 Cookie 等)。

执行流程图解

上述流程可以用以下图表概括:

A[客户端请求] --> B{中间件 (middleware.ts)};
B -- 读取/修改请求 --> C{执行业务逻辑}; 
C -- 返回 NextResponse.next() --> D[匹配的页面/API路由];
C -- 返回 NextResponse.rewrite(url) --> D;
C -- 返回 NextResponse.redirect(url) --> E[客户端重定向];
D -- 生成响应 --> F[响应返回给客户端];
E -- 新请求 --> A;
B -- 直接返回响应 --> F;

结合使用场景分析

  • 身份验证:在中间件中检查用户会话或认证令牌。如果用户未登录且尝试访问受保护路由,中间件可以直接 redirect 到登录页。
  • A/B 测试:根据用户 ID 或其他条件,在中间件中 rewrite 请求到不同的页面版本,实现无感知的 A/B 测试。
  • 国际化 (i18n):根据用户浏览器语言偏好或 Cookie,在中间件中 rewrite URL 以包含语言前缀,例如将 /about 重写为 /en/about/zh/about,而用户在浏览器中看到的 URL 不变。
  • 日志记录与监控:在中间件中记录所有传入请求的元数据(如 IP 地址、User-Agent、请求时间等),用于后续的分析和监控,然后调用 NextResponse.next() 让请求继续。
  • 安全头部注入:在中间件中获取 NextResponse.next() 返回的响应对象,然后向其添加或修改安全相关的 HTTP 头部(如 Content-Security-Policy, X-Frame-Options 等),增强应用安全性。

同时,也需要认识到中间件并非适用于所有场景。以下是一些不适合在中间件中执行的任务:

  • 复杂的数据获取和操作:中间件不适合直接进行复杂的数据获取或操作。这些任务应在路由处理程序(Route Handlers)或服务器端工具函数中完成。
  • 繁重的计算任务:中间件应保持轻量级并快速响应,否则可能导致页面加载延迟。繁重的计算任务或长时间运行的进程应在专门的路由处理程序中完成。
  • 广泛的会话管理:虽然中间件可以处理基本的会话任务,但更广泛的会话管理应由专门的身份验证服务或在路由处理程序内部进行。
  • 直接数据库操作:不建议在中间件中执行直接的数据库操作。数据库交互应在路由处理程序或服务器端工具函数中完成。

理解这些限制有助于你更有效地利用 Next.js 中间件,并避免潜在的性能瓶颈。

如何使用 Next.js 中间件?

在 Next.js 15 中,你只需要在项目的根目录下(与 apppages 目录同级)创建一个名为 middleware.ts (或 middleware.js) 的文件即可。这个文件需要导出一个默认函数,该函数接收一个 NextRequest 对象作为参数,并返回一个 NextResponse 对象。

基本结构

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // ... 中间件逻辑 ...
  return NextResponse.next();
}

export const config = {
  matcher: [
    /*
     * 匹配所有请求路径,除了以下划线开头的内部路径(如 _next/static)
     * 和文件扩展名(如 .ico, .png)
     */
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};

路径匹配 (Matching Paths)

Next.js 中间件默认会匹配项目中的所有路径。然而,在实际应用中,你通常需要让中间件只在特定的路径上运行。Next.js 提供了两种主要方式来定义中间件的运行路径:自定义 matcher 配置条件语句

matchermiddleware.ts 文件中 config 对象的一个属性,它允许你使用路径匹配模式来过滤请求。matcher 的值必须是常量,以便在构建时进行静态分析。它支持完整的正则表达式语法,因此非常灵活。

基本用法:

// middleware.ts
export const config = {
  matcher: '/about/:path*', // 匹配 /about 及其所有子路径,例如 /about/a, /about/a/b
};

匹配多个路径:

可以使用数组来匹配一个或多个路径。

// middleware.ts
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'], // 同时匹配 /about 和 /dashboard 及其子路径
};

反向匹配(排除特定路径):

matcher 支持正则表达式,可以用来匹配除特定路径外的所有路径。这对于排除静态文件、API 路由等非常有用。

// middleware.ts
export const config = {
  matcher: [
    /*
     * 匹配所有请求路径,除了以下划线开头的内部路径(如 /_next/static, /_next/image)
     * 和根目录下的 favicon.ico 文件。
     * `?!` 是一个负向先行断言,表示不匹配紧随其后的模式。
     */
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};

matcher 配置规则:

  • 必须以 / 开头。
  • 可包含命名参数:/about/:path 可以匹配 /about/a/about/b,但不包含 /about/a/c
  • 可对命名参数使用修饰符(以 : 开头):
    • *:表示零个或多个,例如 /about/:path* 可匹配 /about/a/b/c
    • ?:表示零个或一个,例如 /about/:path? 可匹配 /about/about/a
    • +:表示一个或多个。
  • 可以使用括号中的正则表达式:/about/(.*)/about/:path_ 作用相同。

NextResponse API

NextResponse 扩展了标准的 Web Response API,用于创建、修改和返回响应。它提供了 next()redirect()rewrite() 等便捷方法,以及用于操作请求和响应头、Cookie 的功能。

1. NextResponse.next()

NextResponse.next() 允许请求继续流向匹配的 Next.js 页面或 API 路由。这是最常见的操作,表示中间件完成了它的任务,允许请求继续正常处理。你也可以通过 NextResponse.next({ request: newRequest }) 来修改请求对象并传递给下一个处理程序。

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // 可以在这里读取请求信息,例如记录日志
  console.log('请求路径:', request.nextUrl.pathname);

  // 继续请求,不进行任何修改
  return NextResponse.next();
}

2. NextResponse.redirect(url, status?)

NextResponse.redirect() 用于向客户端发送一个重定向响应(HTTP 307 或 308)。浏览器会收到重定向指令并加载新的 URL。这常用于未授权访问、强制 HTTPS 或处理旧链接。

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const isAuthenticated = false; // 假设用户未认证
  if (!isAuthenticated && request.nextUrl.pathname !== '/login') {
    // 如果用户未认证且不在登录页,则重定向到登录页
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}

3. NextResponse.rewrite(url)

NextResponse.rewrite() 允许你将一个传入路径内部重写到另一个 URL,而不会改变浏览器地址栏中的 URL。这对于创建更友好的 URL、国际化路由或将旧路径映射到新路径非常有用。

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname === '/old-page') {
    // 将 /old-page 内部重写到 /new-page,用户浏览器地址栏不变
    return NextResponse.rewrite(new URL('/new-page', request.url));
  }
  return NextResponse.next();
}

4. 操作请求/响应头 (Headers)

NextResponse 提供了 headers 属性,允许你设置响应头。你也可以通过 request.headers 访问请求头。

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // 设置响应头
  response.headers.set('X-Custom-Header', 'Hello from Middleware');
  response.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');

  // 也可以读取请求头
  const userAgent = request.headers.get('user-agent');
  console.log('User-Agent:', userAgent);

  return response;
}

5. 操作 Cookie (Cookies)

NextRequestNextResponse 都提供了方便的 API 来获取和操作 Cookie。request.cookies 用于读取请求中的 Cookie,response.cookies 用于设置响应中的 Cookie。

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // 从请求中获取 Cookie
  const theme = request.cookies.get('theme');
  console.log('Current theme:', theme?.value);

  const response = NextResponse.next();

  // 在响应中设置 Cookie
  response.cookies.set('last_visit', new Date().toISOString(), { path: '/' });

  // 删除 Cookie
  // response.cookies.delete('some_old_cookie');

  return response;
}

NextResponse 总结:

NextResponse 是中间件中进行响应控制的核心。通过灵活运用 next()redirect()rewrite() 以及对 Headers 和 Cookies 的操作,你可以实现各种复杂的请求处理逻辑,从而增强 Next.js 应用的功能和用户体验。

NextRequestNextResponse

  • NextRequest 扩展了标准的 Web Request API,提供了更多 Next.js 特有的属性和方法,例如 nextUrl(包含解析后的 URL 信息)、cookies 等。
  • NextResponse 扩展了标准的 Web Response API,用于创建、修改和返回响应。它提供了 next()redirect()rewrite() 等便捷方法。

常见应用场景与实践

1. 身份验证与重定向

假设你有一个需要登录才能访问的 /dashboard 页面。你可以使用中间件来检查用户是否已认证,如果未认证则重定向到登录页。

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const isAuthenticated = request.cookies.has('auth_token'); // 假设通过 cookie 判断认证状态
  const loginUrl = new URL('/login', request.url);

  // 如果用户尝试访问 /dashboard 且未认证,则重定向到登录页
  if (request.nextUrl.pathname.startsWith('/dashboard') && !isAuthenticated) {
    return NextResponse.redirect(loginUrl);
  }

  // 如果用户已认证且尝试访问 /login,则重定向到 /dashboard
  if (request.nextUrl.pathname.startsWith('/login') && isAuthenticated) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/login'], // 匹配 /dashboard 及其所有子路径,以及 /login 路径
};

实践要点:

  • 使用 request.cookies 访问请求中的 Cookie。
  • NextResponse.redirect(url) 用于执行客户端重定向(HTTP 307 或 308)。
  • new URL('/login', request.url) 构造完整的 URL,确保在不同环境下都能正确重定向。

2. URL 重写 (Rewriting)

重写允许你将一个传入路径映射到另一个内部路径,而不会改变浏览器地址栏中的 URL。这对于创建更友好的 URL 或处理内部路由非常有用。

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // 将 /old-page 重写到 /new-page,用户浏览器地址栏仍显示 /old-page
  if (request.nextUrl.pathname === '/old-page') {
    return NextResponse.rewrite(new URL('/new-page', request.url));
  }

  // 示例:将 /blog/post-slug 重写到 /blog/[slug] 的实际页面
  // 假设你的博客文章页面是 /app/blog/[slug]/page.tsx
  if (request.nextUrl.pathname.startsWith('/blog/')) {
    const slug = request.nextUrl.pathname.split('/').pop();
    if (slug) {
      return NextResponse.rewrite(new URL(`/blog/${slug}`, request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/old-page', '/blog/:path*'],
};

实践要点:

  • NextResponse.rewrite(url) 用于执行内部重写,URL 不变。
  • 重写通常用于将外部友好的 URL 映射到内部组件结构。

3. 设置响应头 (Setting Headers)

你可以在中间件中修改响应头,例如添加安全策略头(CSP)、设置 Cookie 或修改缓存控制。

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // 添加一个自定义响应头
  response.headers.set('X-Custom-Header', 'Hello from Middleware');

  // 设置一个 Cookie
  response.cookies.set('my_cookie', 'some_value', { path: '/', maxAge: 3600 });

  // 移除一个 Cookie
  // response.cookies.delete('another_cookie');

  // 设置内容安全策略 (CSP) 头
  response.headers.set(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
  );

  return response;
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

实践要点:

  • 首先调用 NextResponse.next() 获取一个可修改的响应对象。
  • 通过 response.headers.set()response.cookies.set() 来操作响应头和 Cookie。

4. 国际化 (i18n) 路由

中间件是实现国际化路由的理想场所,你可以根据用户偏好或浏览器设置来重写 URL,以显示不同语言的内容。

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const PUBLIC_FILE = /\.(.*)$/;

const locales = ['en', 'zh', 'fr']; // 支持的语言
const defaultLocale = 'en';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 检查路径是否包含文件扩展名(如 .js, .css, .png 等),如果是则跳过中间件处理
  if (PUBLIC_FILE.test(pathname)) {
    return NextResponse.next();
  }

  // 检查路径是否已经包含语言前缀
  const pathnameHasLocale = locales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (pathnameHasLocale) {
    return NextResponse.next();
  }

  // 如果路径不包含语言前缀,则根据用户偏好或默认语言重写 URL
  const locale = request.cookies.get('NEXT_LOCALE')?.value || defaultLocale;

  request.nextUrl.pathname = `/${locale}${pathname}`;
  // 重写 URL,但浏览器地址栏不变
  return NextResponse.rewrite(request.nextUrl);
}

export const config = {
  matcher: [
    // 匹配所有路径,除了 API 路由、Next.js 内部文件和公共文件
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

实践要点:

  • 通过 request.nextUrl.pathname 获取当前请求路径。
  • 使用 request.cookies.get() 获取用户语言偏好。
  • NextResponse.rewrite() 用于在不改变浏览器 URL 的情况下,将请求重写到带有语言前缀的内部路径。

5. 错误处理与调试技巧

// middleware.ts
export function middleware(request: NextRequest) {
  try {
    // 业务逻辑
  } catch (error) {
    console.error('中间件错误:', error);
    return NextResponse.json(
      { error: '服务器内部错误' },
      { status: 500 }
    );
  }
}

调试工具

  1. 使用 console.log 查看边缘运行时日志
  2. 在响应头添加调试信息:
response.headers.set('X-Middleware-Debug', 'executed');
  1. 通过 ?__middlewareDebug=1 URL参数触发详细日志

Next.js 中间件的代码维护

如果项目比较简单,中间件的代码通常不会写很多,将所有代码写在一起倒也不是什么太大问题。可当项目复杂了,比如在中间件里又要鉴权、又要控制请求、又要国际化等等,各种逻辑写在一起,中间件很快就变得难以维护。如果我们要在中间件里实现多个需求,该怎么合理的拆分代码呢?

一种简单的方式是拆分为多个函数:

import { NextResponse } from 'next/server'

async function middleware1(request) {
  console.log(request.url)
  return NextResponse.next()
}

async function middleware2(request) {
  console.log(request.url)
  return NextResponse.next()
}

export async function middleware(request) {
  await middleware1(request)
  await middleware2(request)
}

export const config = {
  matcher: '/api/:path*',
}

一种更为优雅的方式是借助高阶函数:

import { NextResponse } from 'next/server'

function withMiddleware1(middleware) {
  return async (request) => {
    console.log('middleware1 ' + request.url)
    return middleware(request)
  }
}

function withMiddleware2(middleware) {
  return async (request) => {
    console.log('middleware2 ' + request.url)
    return middleware(request)
  }
}

async function middleware(request) {
  console.log('middleware ' + request.url)
  return NextResponse.next()
}

export default withMiddleware2(withMiddleware1(middleware))

export const config = {
  matcher: '/api/:path*',
}

请问此时的执行顺序是什么?试着打印一下吧。是不是感觉回到了学 redux 的时候?

但这样写起来还是有点麻烦,让我们写一个工具函数帮助我们:

import { NextResponse } from 'next/server'

function chain(functions, index = 0) {
  const current = functions[index];
  if (current) {
    const next = chain(functions, index + 1);
    return current(next);
  }
  return () => NextResponse.next();
}

function withMiddleware1(middleware) {
  return async (request) => {
    console.log('middleware1 ' + request.url)
    return middleware(request)
  }
}

function withMiddleware2(middleware) {
  return async (request) => {
    console.log('middleware2 ' + request.url)
    return middleware(request)
  }
}

export default chain([withMiddleware1, withMiddleware2])

export const config = {
  matcher: '/api/:path*',
}

请问此时的执行顺序是什么?答案是按数组的顺序,middleware1、middleware2。

如果使用这种方式,实际开发的时候,代码类似于:

import { chain } from "@/lib/utils";
import { withHeaders } from "@/middlewares/withHeaders";
import { withLogging } from "@/middlewares/withLogging";

export default chain([withLogging, withHeaders]);

export const config = {
  matcher: '/api/:path*',
}

具体写中间件时:

export const withHeaders = (next) => {
  return async (request) => {
    // ...
    return next(request);
  };
};

React生态蓝图梳理:前端、全栈与跨平台全景指南

作者 石小石Orz
2025年8月12日 23:37

Hi,我是石小石!


很久没和大家聊 React 了,今天心血来潮,想带大家一起梳理一下 2025 最新的 React 生态蓝图,帮你在做技术选型和架构设计时少走弯路。
发展到今天,React 早已不只是一个前端 UI 库,而是形成了一个覆盖 前端开发、全栈架构、跨平台应用、性能优化、AI 驱动开发 的完整生态圈。
接下来,我会从 核心语言 → 脚手架与框架 → 状态管理 → UI 组件 → 性能优化 → 移动端 → 测试 → AI 开发辅助 → 全栈与部署 这条完整链路,梳理 2025 年最新的 React 技术栈,并为每个环节提供 官方链接、技术特点、适用场景、优缺点分析,让你能一篇掌握全貌。

React 核心与基础演进

React 19(2024 年 12 月发布)带来了不少实用更新。新增了 Actions API,让异步状态更新更简单,同时支持阻塞和非阻塞渲染,提升了渲染控制能力。静态站点生成和 React Server Components 也更成熟了。

Hooks 方面,除了 useDeferredValueuseTransition 继续优化渲染优先级,新增的 useFormStatususeOptimisticuseActionState等 Hooks 让交互体验更顺畅。

渲染机制依然基于成熟的 Fiber 架构,支持可中断和增量更新,结合实验性的 Concurrent Mode,响应速度和用户体验有明显提升。React 的核心理念没变,声明式 UI、虚拟 DOM、组件化和单向数据流,依然是写出高质量代码的基础。

附官网文档:react.nodejs.cn/learn

脚手架、构建与运行时

  • Vite:启动快到飞起,热更新(HMR)响应迅速,非常适合轻量级 SPA 和快速迭代开发。
  • Bun:新晋的 JavaScript 运行时,集打包、SSR 和依赖管理于一身,能给 React 项目带来更快的构建和运行体验。
  • Next.js 15:完美支持 React 19,内置 React Server Components、Server Functions,还有基于 Rust 的 Turbopack,轻松搞定混合渲染(SSR、SSG、ISR)和 AI 优化代码,打造全栈和边缘部署应用效率满满。
  • Remix(现 React Router v7) :2024 年 11 月正式更名,结合前端路由和全栈特性,支持 SSR,是 Next.js 的有力竞争者。
  • Astro:主打静态优先,JS 体积极小,只在必须交互的组件上做 hydration,非常适合内容驱动或性能敏感的项目。

状态管理与数据获取

  • TanStack Query(React Query) :自动缓存数据、后台刷新,让服务端状态管理更智能,性能和体验双提升,数据请求处理更简单。
  • Zustand / Jotai / Recoil / MobX / XState:一系列轻量级状态管理方案,适合局部或全局状态,各有侧重,写法简洁直观,且易于扩展。
  • Redux Toolkit:Redux 的现代升级版,集成状态管理和数据获取,代码结构清晰,是复杂项目的稳定选择。
  • React Server Components / Functions:把数据处理和渲染逻辑放到服务器端,减轻客户端负担,性能和用户体验都能明显提升。

选取指南:

场景 推荐库
需要稳定且可预测的大型复杂应用 Redux + Redux Toolkit
快速开发,追求轻量和简洁 Zustand 发音: /ˈzuːstænd/
组件状态依赖复杂,异步多 Recoil / Jotai 发音: /ˈriːkɔɪl/ /ˈdʒoʊtaɪ/
需要响应式、自动追踪依赖 MobX 发音: /mɒb ɛks/
状态流程明确且复杂,流程化管理 XState
混合局部与全局状态管理 Zustand + Jotai 组合使用

路由与全栈结构

  • React Router v7:支持 SSR 和全栈开发,适合轻量级多页面应用,灵活度高。
  • Next.js 自带路由系统:基于文件系统的路由,简单直观,非常适合企业级项目和结构复杂的页面。
  • TanStack Router:新晋的 TypeScript 优选路由库,设计现代,未来计划与 React Server Components 深度集成,值得持续关注。

UI 组件与样式体系

  • Tailwind CSS + shadcn/ui:原子化 CSS 带来极高定制自由度,配合无样式且注重可访问性的组件库,开发效率和体验都非常棒。
  • Material UI / Chakra UI / DaisyUI / Ant Design:主流组件生态,提供丰富现成的样式和交互组件,适合快速迭代和企业级项目。
  • CSS Modules / Styled Components / Emotion:模块化样式方案中,CSS Modules 通过限定 CSS 作用域避免全局冲突,简单易用,适合渐进式迁移;Styled Components 支持在 JS 中编写动态样式,方便主题切换和复用,但有一定运行时开销;Emotion 则更轻量灵活,支持静态提取和 CSS-in-JS,兼顾性能和开发体验。

未完待续

写不动了,留个坑,明天继续整理

昨天 — 2025年8月12日首页

Dot

作者 烛阴
2025年8月12日 22:54

Task

Write a program that draws a triangle in the center of the screen. The triangle should have an apex at (0.5, 0.75) in normalized device coordinates, and an apex angle of 120 degrees. The height of the triangle should be 0.5 times the height of the screen.

编写一个程序,在屏幕正中央绘制一个三角形。该三角形的顶点位于归一化设备坐标(NDC)的 (0.5, 0.75),顶角为 120 度,且三角形的高度为屏幕高度的 0.5 倍。

Theory

函数介绍

dot 函数用于计算两个向量的点积(数量积),返回一个标量值。

主要用途

  1. 计算夹角余弦值
vec2 a = normalize(vector1);
vec2 b = normalize(vector2);
float angle = dot(a, b);  // cos(夹角)

2. 计算投影长度

//投影长度 = dot(a, b) / length(b)
vec2 direction = normalize(dir);
vec2 toPoint = point - origin;
float projectionLength = dot(direction, toPoint);

3. 判断向量方向关系

  • dot(A, B) = 1:方向完全相同。
  • dot(A, B) = 0:方向互相垂直。
  • dot(A, B) = -1:方向完全相反。

Answer

uniform vec2 iResolution;

#define PI 3.14

void main() {
  vec2 uv = gl_FragCoord.xy / iResolution.xy;

  vec2 ratio = vec2(iResolution.x / iResolution.y, 1.0);
  uv -= 0.5;
  uv *= ratio;
 
  // 顶上的点(0.5, 0.75)
  vec2 p1 = vec2(0, 0.25);
  // 底部点
  vec2 p2 = vec2(0, -0.25);
  // 已知线的方向
  vec2 p1Dir = normalize(p2 - p1);
  // 当前点到顶点的向量
  vec2 dis = uv - p1;
   // 当前点到顶点的方向
  vec2 p2Dir = normalize(dis);
  // 计算夹角
  float t = dot(p1Dir, p2Dir);
  // 剔除大于60° cos(60) == 0.5
  float red = step(0.5, t);
  // 获取当前点在已知线上的投影长度(已知线的长度为0.5)
  float d = dot(p1Dir, dis); 
  red *= (1.0 - step(0.5, d));

  gl_FragColor = vec4(red, 0.0, 0.0, 1.0);
}

效果

image.png

练习

Dot

最后

如果你觉得这篇文章有用,记得点赞、关注、收藏,学Shader更轻松!!

8.12实验室 指尖魔法变出艺术感 Excalidraw:cpolar内网穿透实验室第495个成功挑战

NO.495  Excalidraw-01.png

软件名称:Excalidraw

操作系统支持:Windows/macOS/Linux/手机浏览器(全平台无压力)

软件介绍:

Excalidraw 是一款“手残党救星”级的电子白板工具,主打极简操作和自然画风。无论是随手涂鸦、会议记录还是架构设计,都能像在真实纸张上画画一样流畅。支持实时协作、链接分享,甚至能导出成PNG/JPG/PDF等格式,适合学生、职场人、教育者和创意工作者。

NO.495  Excalidraw-02.png

Excalidraw × 手绘魔法:你的创意永不掉线!

  • “手残友好”模式:线条自动平滑但保留手写质感,连画直线都不用担心歪斜。
  • 超级协作:链接一甩,队友就能在线帮你补全脑暴图,比微信传照片快十倍。
  • 一键变身专业工具:支持插入文本框、数学公式甚至Markdown代码块,从涂鸦到PPT配图无缝衔接。

NO.495  Excalidraw-03.png

手绘魔法的三大应用场景

  1. 学生党课后救星
    • 爽点:用Excalidraw随手画出带表情包的思维导图,复习时笑到崩溃!
  2. 团队远程头脑风暴
    • 爽点:Excalidraw+共享链接=实时协作的“云端草稿本”,连老板都能用手机涂鸦批注。
  3. 设计师快速原型测试
    • 爽点:5分钟画出带箭头和备注的UI草稿,导出高清图直接发给开发。

NO.495  Excalidraw-04.png

cpolar × Excalidraw = 手绘自由不设限!

  • 突破局域网限制:如果把Excalidraw部署到本地服务器,用Cpolar一行命令就能生成外网访问链接。
  • 场景示例:公司内网禁止使用第三方协作工具?用Cpolar让远程同事也能实时帮你画“老板想要的那张图”!
  • 安全放心:Cpolar支持自定义域名和加密通道,手绘创意不被偷走。

cpolar将内网穿透简单到只要三步! 1.下载安装 → 2. 输入要穿透的端口号 → 3. 立刻获得专属访问链接。

NO.495  Excalidraw-05.png

组合优势

Excalidraw是当代人的“数字素描本”,用最自然的方式把脑中的创意变现实;而Cpolar则像给它装上翅膀,让协作突破物理限制。无论是学生、职场人还是设计师,这对CP都能让你的灵感从指尖飞向世界!

这么好的组合赶紧去安装吧,教程如下👇

本文主要介绍如何在Ubuntu系统使用Docker部署开源白板工具Excalidraw,并结合cpolar内网穿透工具实现公网远程访问绘制流程图。

image-20240206172031658

1. 安装Docker

本教程操作环境为Linux Ubuntu系统,在开始之前,我们需要先安装Docker。

在终端中执行下方命令:

添加Docker源

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

安装Dokcer包

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

通过运行映像来验证 Docker 引擎安装是否成功

sudo docker run hello-world

2. 使用Docker拉取Excalidraw镜像

sudo docker pull excalidraw/excalidraw

image-20240206172328932

然后执行查看镜像命令:

sudo docker images

image-20240206172422782

可以看到成功拉取了Excalidraw镜像。

3. 创建并启动Excalidraw容器

成功拉取Excalidraw镜像后,我们可以使用该镜像创建并运行一个Excalidraw容器。

在终端执行以下命令:

$ sudo docker run -d --name excalidraw -p 5000:80 excalidraw/excalidraw

参数说明:

  • –name excalidraw:本例容器名称为excalidraw,大家可以自己起名。
  • -p 5000:80: 端口进行映射,将本地 5000 端口映射到容器内部的 80 端口。
  • -d :设置容器在在后台一直运行。

image-20240206172707031

然后执行下方命令查看容器是否正在运行:

sudo docker ps

image-20240206172810409

可以看到刚才创建的Excalidraw容器正在运行中。

4. 本地连接测试

现在我们可以通过浏览器直接访问 localhost:5000 端口的 Excalidraw 服务:

image-20240206172909607

可以看到,本地连接 Excalidraw 服务测试成功。

5. 公网远程访问本地Excalidraw

不过我们目前只能在本地连接刚刚使用docker部署的Excalidraw服务,如果身在异地,想要远程访问在本地部署的Excalidraw容器,但又没有公网ip怎么办呢?

我们可以使用cpolar内网穿透工具来实现无公网ip环境下的远程访问需求。

5.1 内网穿透工具安装

下面是安装cpolar步骤:

cpolar官网地址: www.cpolar.com

  • 使用一键脚本安装命令
curl -L https://www.cpolar.com/static/downloads/install-release-cpolar.sh | sudo bash
  • 向系统添加服务
sudo systemctl enable cpolar
  • 启动cpolar服务
sudo systemctl start cpolar

cpolar安装成功后,在外部浏览器上访问Linux 的9200端口即:【http://服务器的局域网ip:9200】,使用cpolar账号登录,登录后即可看到cpolar web 配置界面,结下来在web 管理界面配置即可。

image-20230831171159175

5.2 创建远程连接公网地址

登录cpolar web UI管理界面后,点击左侧仪表盘的隧道管理——创建隧道:

  • 隧道名称:可自定义,注意不要与已有的隧道名称重复,本例使用了:exdraw
  • 协议:http
  • 本地地址:5000
  • 域名类型:随机域名
  • 地区:选择China Top

点击创建

image-20240207095612018

创建成功后,打开左侧在线隧道列表,可以看到刚刚通过创建隧道生成了两个公网地址,接下来就可以在其他电脑(异地)上,使用任意一个地址在浏览器中访问即可。

image-20240207095720991

如下图所示,成功实现在公网环境访问本地部署的Excalidraw服务!

image-20240207095940715

小结

为了方便演示,我们在上边的操作过程中使用了cpolar生成的HTTP公网地址隧道,其公网地址是随机生成的。

这种随机地址的优势在于建立速度快,可以立即使用。然而,它的缺点是网址是随机生成,这个地址在24小时内会发生随机变化,更适合于临时使用。

如果有长期远程访问本地Excalidraw服务的需求,但又不想每天重新配置公网地址,还想地址好看又好记,那我推荐大家选择使用固定的二级子域名方式来远程访问。

5.3 使用固定公网地址远程访问

登录cpolar官网,点击左侧的预留,选择保留二级子域名,地区选择China VIP,设置一个二级子域名名称,点击保留,保留成功后复制保留的二级子域名名称,这里我填写的是exdraw,大家也可以自定义喜欢的名称。

image-20240207100140130

保留成功后复制保留成功的二级子域名的名称:exdraw,返回登录Cpolar web UI管理界面,点击左侧仪表盘的隧道管理——隧道列表,找到所要配置的隧道exdraw,点击右侧的编辑:

image-20240207100319169

修改隧道信息,将保留成功的二级子域名配置到隧道中

  • 域名类型:选择二级子域名
  • Sub Domain:填写保留成功的二级子域名:exdraw
  • 地区:选择China VIP

点击更新(注意,点击一次更新即可,不需要重复提交)

image-20240207100431685

更新完成后,打开在线隧道列表,此时可以看到公网地址已经发生变化,地址名称也变成了固定的二级子域名名称的域名:

image-20240207100508737

最后,我们使用任意一个固定公网地址在浏览器访问,可以看到访问成功,这样一个固定且永久不变的公网地址就设置好了,随时随地都可以远程访问本地部署的Excalidraw服务了!

image-20240207100658129

以上就是如何在Ubuntu系统使用Docker部署Excalidraw容器,并结合cpolar内网穿透工具实现公网远程访问内网本地服务的全部流程,感谢您的观看。

当便捷部署遇上稳定穿透技术,Excalidraw正在重新定义专业绘图工具的使用边界。

本篇文章知识点来源cpolar官网

  1. cpolar博客:配置二级子域名: www.cpolar.com/blog/config…
  2. cpolar博客:配置自定义域名: www.cpolar.com/blog/config…
  3. cpolar博客:配置固定TCP端口地址: www.cpolar.com/blog/config…
  4. cpolar博客:配置固定FTP地址: www.cpolar.com/blog/config…

不是吧,还在手搓翻页时钟?快来看看Trae怎么完成的

2025年8月12日 22:39

前言

最近闲来无事,想看看Trae能不能帮我实现一下一款苹果风格的翻页时钟,用来展示时间,提示当前时间。

同时也可以展示倒计时,比如倒计时到某个时间点,比如倒计时到某个事件发生,比如倒计时到某个倒计时结束。最好是可以根据夜晚时间。

例如23:00-7:00,切换到深色主题,早上就自动展示浅色主题

看看最后的效果

深色主题 image.png 浅色主题

image.png

没有多说什么,trae就帮我完成了

image.png

首先是HTML结构,很简单,就是一个div,里面包含了时间和倒计时的元素,主要还是时分秒,不足10的在前面补0。

image.png

js代码是储存当前显示的时间,星期使用中文显示,更加通俗易懂

首先是初始化方法,初始化时间和日期显示,以及翻页卡片的元素。

 function initClock() {
        // 立即更新一次时钟
        updateClock();
        
        // 设置定时器,每秒更新一次
        setInterval(updateClock, 1000);
    }

然后是核心代码,就是更新时间的函数,每隔1秒调用一次,更新时间和倒计时,同时也更新日期显示,将翻页卡片进行翻转。

   // 更新时钟显示
    function updateClock() {
        const now = new Date();
        const hours = now.getHours();
        const minutes = now.getMinutes();
        const seconds = now.getSeconds();
        
        // 计算各个位的数字
        const newTime = {
            hoursTens: Math.floor(hours / 10),
            hoursOnes: hours % 10,
            minutesTens: Math.floor(minutes / 10),
            minutesOnes: minutes % 10,
            secondsTens: Math.floor(seconds / 10),
            secondsOnes: seconds % 10
        };
        
        // 更新翻页卡片
        updateFlipCard(hoursTens, currentTime.hoursTens, newTime.hoursTens);
        updateFlipCard(hoursOnes, currentTime.hoursOnes, newTime.hoursOnes);
        updateFlipCard(minutesTens, currentTime.minutesTens, newTime.minutesTens);
        updateFlipCard(minutesOnes, currentTime.minutesOnes, newTime.minutesOnes);
        updateFlipCard(secondsTens, currentTime.secondsTens, newTime.secondsTens);
        updateFlipCard(secondsOnes, currentTime.secondsOnes, newTime.secondsOnes);
        
        // 更新当前时间
        currentTime = newTime;
        
        // 更新日期显示
        updateDateDisplay(now);
    }

主题切换,根据时间切换主题,晚上切换到深色主题,早上切换到浅色主题,并且在切换主题时,添加过渡效果。

将其存储到本地缓存,只要你不去清理本地缓存,就会一直保持你切换的主题,下次刷新页面还是会在的。

// 主题切换
function switchTheme() {
    const now = new Date();
    const hours = now.getHours();
    if (hours >= 23 || hours < 7) {
        // 晚上切换到深色主题
        document.body.classList.add('dark-theme');
    } else {
        // 早上切换到浅色主题
        document.body.classList.remove('dark-theme');
    }
}

日期显示,根据当前时间,展示当前的日期,格式为:2023年10月10日 星期一

// 更新日期显示
function updateDateDisplay(now) {
    const dateDisplay = document.getElementById('date-display');
    const dateString = now.toLocaleDateString('zh-CN', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        weekday: 'long'
    });
    dateDisplay.textContent = dateString;
}

总结

1、通过Trae的代码,我们实现了一个苹果风格的翻页时钟,展示了当前时间和倒计时,并且根据时间切换了主题。

2、同时也实现了日期显示,根据当前时间,展示当前的日期,格式为:2023年10月10日 星期一

3、最后,我们也实现了主题切换,根据时间切换主题,晚上切换到深色主题,早上切换到浅色主题,并且在切换主题时,添加过渡效果,将其存储到本地缓存。

来试试Trae这一款企业级的登录界面吧

2025年8月12日 22:20

前言

之前我们团队在开发一个项目的时候,需要用到登录功能,当时我们的前端就简单的写了个登录页面,样式及其不优雅,但是功能是实现了,

当时我就想,前端的登录页面样式能不能做的更好看一些,所以我就想到了使用Trae来试试,看看能不能不费吹灰之力实现登录页面的样式,要求简约大气,上档次,还要兼容现在的登录、注册,以及微信、短信等方式登录

image.png 于是我把我的需求整理给Trae,看看能不能实现我的要求,然后再让老板过目,样式可以的话,再让Trae帮我把之前的功能对接好

先来看看最终的效果

image.png 登录成功的提示 image.png 是不是有点企业级的登陆页面那个味道了,我只是简单的使用了Trae,自己写了个登录页面,但是效果还是不错的,至少比我之前写的登录页面好看很多,看起来像企业级的登录页面

接下来看看Trae是如何实现的

image.png 首先看看Trae的登录页面DOM的代码,采用的是居中的左右布局,右边是登录表单,左边是登录背景

image.png 其他登录方式的实现也比较简单,就是在登录表单下面添加个其他登录方式的按钮,点击按钮就跳转到对应的登录页面,放在登录按钮下方。

也是很显眼的位置,让人一眼就可以看的到有其他登录方式,比如微信登录、短信登录等

trae的完成度还是很高的, 登录页面的样式都是Trae提供的,我只是简单的修改了下登录按钮的样式,其他的都没有修改

然后来看看里面的js代码是怎么实现

关键的密码隐藏与显示的实现,根据密码输入框的类型来切换,点击图标切换密码输入框的类型

切换图标也会切换,从显示密码切换为隐藏密码,从隐藏密码切换为显示密码

 // 密码显示/隐藏切换
    const togglePassword = document.querySelector('.toggle-password');
    const passwordInput = document.getElementById('password');
    
    if (togglePassword && passwordInput) {
        togglePassword.addEventListener('click', function() {
            // 切换密码输入框的类型
            const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
            passwordInput.setAttribute('type', type);
            
            // 切换图标
            this.classList.toggle('fa-eye');
            this.classList.toggle('fa-eye-slash');
        });
    }

获取验证码的倒计时,点击获取验证码按钮后,按钮会倒计时,此时按钮无法点击,倒计时结束后,按钮才会重新点击可用

image.png

    // 获取验证码倒计时
    const getCodeBtn = document.querySelector('.get-code-btn');
    const phoneInput = document.getElementById('phone');
    
    if (getCodeBtn && phoneInput) {
        getCodeBtn.addEventListener('click', function() {
            const phone = phoneInput.value.trim();
            
            // 简单的手机号验证
            if (!phone || !/^1\d{10}$/.test(phone)) {
                showMessage('请输入正确的手机号码', 'error');
                return;
            }
            
            // 模拟发送验证码
            showMessage('验证码已发送', 'success');
            
            // 倒计时
            let countdown = 60;
            getCodeBtn.disabled = true;
            getCodeBtn.textContent = `${countdown}秒后重新获取`;
            
            const timer = setInterval(() => {
                countdown--;
                getCodeBtn.textContent = `${countdown}秒后重新获取`;
                
                if (countdown <= 0) {
                    clearInterval(timer);
                    getCodeBtn.disabled = false;
                    getCodeBtn.textContent = '获取验证码';
                }
            }, 1000);
        });
    }

总结

如果你想要体验一下ai编程的魅力,可以试试Trae,国内用户低门槛上手,快点来试试把你的网站进行样式升级吧~

JavaScript 中的二进制数据:ArrayBuffer 与 SharedArrayBuffer 全面解析

作者 excel
2025年8月12日 22:16

1. ArrayBuffer

  • 是什么:一段固定长度、连续的二进制内存。

  • 不能直接读写,必须通过视图(TypedArrayDataView)访问。

  • 常用场景

    • 文件读写(图片、音视频)
    • 网络传输(WebSocket、Fetch)
    • WebGL / GPU 数据
    • 加密解密数据
  • 示例

    const buf = new ArrayBuffer(4);
    const u8 = new Uint8Array(buf);
    u8[0] = 255;
    console.log(u8[0]); // 255
    

2. SharedArrayBuffer

  • 是什么:可以被多个线程同时访问的二进制内存。

  • 不会复制,跨线程传递时是零拷贝。

  • 需要配合 Atomics 保证数据同步。

  • 浏览器限制:需跨源隔离(COOP/COEP)。

  • 常用场景

    • 多线程数据共享(主线程 + Worker)
    • 实时游戏(物理计算 + 渲染)
    • 音视频解码与播放共享缓冲
    • 科学计算、机器学习
  • 示例

    const sab = new SharedArrayBuffer(4);
    const u32 = new Uint32Array(sab);
    u32[0] = 123;
    // 传给 Worker 后,两个线程可同时访问并修改
    

3. TypedArray

  • 是什么:定型数组视图,用固定类型访问 ArrayBuffer / SharedArrayBuffer

  • 特点

    • 高性能批量数值运算
    • 必须按类型大小对齐(如 Int32 按 4 字节)
    • 按系统字节序读写
  • 常用类型Uint8Array, Int16Array, Float32Array

  • 示例

    const arr = new Uint8Array([1, 2, 3]); // 自动分配底层 ArrayBuffer
    

4. DataView

  • 是什么:灵活的二进制视图,可按任意偏移读写各种类型,并指定字节序。

  • 特点

    • 不要求按类型大小对齐
    • 可选大端/小端序
    • 适合自定义二进制格式解析
  • 示例

    const buf = new ArrayBuffer(4);
    const dv = new DataView(buf);
    dv.setUint16(0, 65535, true); // 小端序
    console.log(dv.getUint16(0, true)); // 65535
    

5. 对比表

特性 ArrayBuffer SharedArrayBuffer TypedArray DataView
是否共享内存 ❌ 否 ✅ 是 依赖底层缓冲区 依赖底层缓冲区
跨线程传递 转移(原失效) 保留引用 依赖底层缓冲区 依赖底层缓冲区
类型 固定类型 任意类型
字节序控制 按系统字节序 ✅ 可指定
是否对齐 N/A N/A 必须对齐 不需要
常见用途 文件、网络、GPU 数据 多线程并发 数值计算 文件解析

ChatGPT Image 2025年8月12日 22_09_28.png

  • ArrayBuffer(蓝色块)

    • 位于中间,是原始二进制内存块,不能直接读写。
    • 需要通过视图(上方两个白色块)来访问。
  • TypedArray(左上白色块)

    • 一类固定类型、定长的数组视图(如 Uint8Array, Float32Array)。
    • 按系统字节序访问,速度快,适合批量数值计算。
    • 依赖底层的 ArrayBuffer 存储数据。
  • DataView(右上白色块)

    • 更通用的视图,可以按任意偏移读取各种类型,并指定大小端字节序。
    • 灵活度高,适合解析自定义二进制格式。
    • 也依赖底层的 ArrayBuffer
  • SharedArrayBuffer(下方蓝色块)

    • ArrayBuffer 类似,但可以在多个线程之间共享同一块内存(零拷贝)。
    • 需要配合 Atomics 来保证多线程数据安全。
    • 也能被 TypedArrayDataView 作为底层缓冲区使用。

📌 解读

  • ArrayBuffer / SharedArrayBuffer内存本体,负责存放原始数据。
  • TypedArray / DataView访问工具,决定如何解释和读写这块内存。
  • SharedArrayBuffer 可以理解为“多线程版 ArrayBuffer”,功能更强,但需要同步控制。

SharedArrayBuffer 使用限制及注意事项

SharedArrayBuffer 是 JavaScript 中用于在多个线程(如 Web Worker)间共享内存的底层对象,允许高效的并发数据访问。但它的使用受到多方面严格限制,主要出于安全和兼容性考虑:

  1. 跨源隔离要求(COOP + COEP)
    为防止安全漏洞(如 Spectre 侧信道攻击),现代浏览器要求页面必须开启跨源隔离才能启用 SharedArrayBuffer。
    具体来说,服务器必须设置以下 HTTP 响应头:

    • Cross-Origin-Opener-Policy: same-origin
    • Cross-Origin-Embedder-Policy: require-corp
      只有满足这两个条件,浏览器才允许页面访问 SharedArrayBuffer,否则会被禁用。
  2. 浏览器兼容性
    并非所有浏览器或版本都支持 SharedArrayBuffer。尤其是旧版浏览器或某些隐私模式下,可能完全禁用该功能。开发时应先检测是否支持。

  3. 固定大小且不可变
    SharedArrayBuffer 一旦创建,内存大小就固定了,无法动态扩展,使用时需要提前规划好内存大小。

  4. 必须通过视图访问
    SharedArrayBuffer 本身是底层原始内存块,不能直接读写,必须通过 TypedArray(如 Uint8Array)或 DataView 来访问。

  5. 多线程同步与安全
    由于 SharedArrayBuffer 允许多个线程共享内存,必须使用 Atomics 对象提供的原子操作来同步访问,避免竞态条件和数据损坏。

  6. 安全风险与性能考虑
    该技术虽高效,但使用不当容易引入竞态错误和安全隐患,建议仅在确有多线程共享内存需求且理解同步机制时使用。

王田苗:具身智能产业“破局”,先找能算过来账的垂类环境 | 最前线

2025年8月12日 21:38

作者:富充

编辑:苏建勋

产业化落地,仍是当下具身智能领域最受关注的话题。

2025 年世界机器人大会期间,首程控股举办“2025机器人创投主题交流会”。智友·雅瑞科创平台发起人、北航机器人研究所名誉所长王田苗等学者及知名投资人,围绕具身智能的研究与商业前景分享了观点。

关于 2025 年具身智能的产业“破局”方向,王田苗指出,下一个三年中,行业内将主要存在两条技术路线。

王田苗提出的路径一,是具身智能通过模仿人类智能,构建能够深度泛化的通用世界模型。这种更适合平台型公司。

路径二,是以“智能定界”为路径,从垂类环境入手,这条路线将更可能先实现PMF(产品市场匹配度)。

所谓“智能定界”,是在明确任务、环境与载体(机型)约束的前提下,先设定精度、成功率、安全、成本等可接受范围(“容忍度”);并在此边界内通过裁剪算力与模型,满足细分场景的需求。

王田苗指出,就像陪伴、药房、咖啡店所使用的具身智能形态不可能统一、模型不可能统一,因此要在“能算过来账”的场景验证小型Scaling Law(开发数据量变引起质变的规模定律)。

据王田苗推测,商超、环卫、农副产品加工等领域的部分环境中,具身智能将率先应用。具身智能也将先运用于特定工业任务,但不会泛化到任何工业场景。

智友·雅瑞科创平台发起人、北航机器人研究所名誉所长王田苗,图片:论坛提供

“我们认为,当前制约机器人产业发展的关键不在供应链,而在成熟应用场景的缺乏。短期内会更多使用‘尚不聪明但稳定可用’的机器人产品。”首程控股董事会办公室总经理康雨在接受36氪采访时表示。这一观察与王田苗的判断相互印证。

在康雨看来,严肃工业的落地往往是“点状突破”,一旦通用性和方案能力成熟,便有望催生明星企业。产业趋势上,今年“小脑”(运动控制)更趋成熟,本体企业加快出货;而“大脑”(具身智能)仍在快速进化中。

首程控股近年持续投资具身智能企业。2024 年,公司参与设立并管理规模 100 亿元人民币的“北京机器人产业发展投资基金”,目前已布局宇树科技、银河通用、加速进化等近 20 家具身智能企业。

代码规范与提交

作者 ZXT
2025年8月12日 21:21

husky

husky 是一个用于简化Git钩子(hooks)的设置的工具,允许开发者轻松地在各种Git事件触发时运行脚本。例如,在提交之前(pre-commit)、推送之前(pre-push)、或者在提交信息被写入后(commit-msg)等。

husky的使用可以提高项目团队的工作效率,确保代码库中的代码符合特定的质量标准。它通常与lint-staged一起使用,以在提交前自动执行代码的静态检查。

使用husky包括以下简单步骤:

添加husky到项目依赖。 配置Git钩子,使用husky的配置。 当相应的Git事件被触发时,定义的脚本就会自动执行。

  • husky解决了git hook 的什么痛点? husky是如何将hooks跟package.json关联的?

git原生的钩子存储在项目的.git/hooks目录下,属于本地私有目录,不会被提交到git远程仓库。多人协作时候无法共享,可能出现配置不一样的情况。 安装husky后会修改git配置(git config),将git的原生钩子改成husky管理的脚本目录。开发者可以在packge.json和.husky进行配置

  • 在前端工作流中,husky 最常与哪些工具搭配使用?请至少列举两个,并说明 husky 在这个组合中扮演的角色。

eslint commitlint prettier。角色:通过钩子进行触发。

  • pre-commit 和 commit-msg 是两个最常用的 Git hook。请解释一下它们分别在什么时机触发,以及通常用它们来做什么?

Git 先触发 pre-commit 钩子(执行检查脚本) → 如果检查通过 → 直接使用 -m 后的内容作为提交信息,完成提交 → 如果检查失败 → 提交中断 检查代码语法和检查提交信息。

  • husky v4 与 v5+ 在安装、配置和工作原理上的主要区别

husky v4 的钩子规则(比如 “提交前要跑 ESLint”)虽然写在 package.json 里能被 Git 跟踪,但实际执行钩子的脚本文件藏在本地 .git/hooks(不被 Git 跟踪)。团队成员需要通过 npm install 手动触发脚本生成,本质是 “依赖安装步骤间接同步”,容易因为漏执行步骤导致钩子失效。

husky v5+  的钩子规则直接以  .husky 目录下的脚本文件 存在(比如 .husky/pre-commit),这些文件能被 Git 跟踪,所以 “规则可见”。团队成员拉取代码后,钩子脚本会直接同步到本地,再通过 prepare 脚本自动绑定 Git 配置,本质是 “依赖 Git 文件同步直接生效”,几乎不需要额外手动操作。 image.png

  • git commit 过程的变化:
    如果 pre-commit 钩子脚本以非零状态码退出(例如 ESLint 检查出错误,返回 1),Git 会立即中断整个提交流程,不会创建提交记录。终端会显示脚本的错误输出(如 ESLint 的报错信息),提示用户修复问题后重新提交。

  • husky 的实现方式:
    husky 本身并不改变 Git 钩子的原生机制 ——Git 规定:任何钩子脚本如果以非零状态码退出,后续提交流程都会终止。 ** husky 的作用是将用户定义的命令(如 eslint .)包装到 .husky/pre-commit 脚本中,当用户命令失败时,脚本会继承其非零退出码,从而触发 Git 的原生中断逻辑。简单说,husky 只是 “传递” 了命令的失败状态,借助 Git 本身的机制实现提交拦截。

  • 现在一些 CI/CD 工具(如 GitHub Actions, GitLab CI)也能在代码推送到远程仓库后执行检查。既然如此,为什么我们还需要在本地通过 husky 做提交前的检查?这两者是重复的吗?

反馈速度:本地检查比 CI 快 10 倍以上,如果没有本地检查,错误代码可能被提交到远程仓库,导致:CI 任务频繁失败,浪费团队共享的 CI 资源,CI/CD 的核心作用是确保合并到主分支的代码符合生产标准

  • 在一个大型项目中,pre-commit 钩子可能需要运行 lint、类型检查和单元测试,导致每次提交等待时间很长。你有什么策略或建议来优化这个体验?

配合lint-staged只对提交代码检查 ,ESLint:通过 --cache 选项启用缓存(eslint --cache),并行检查:run-p命令

lint-staged

lint-staged可以在git staged阶段的文件上执行代码检查(Linters),包括ESLint和Stylelint等。简单说就是,当开发者运行ESlint或Stylelint命令时,可以通过设置指定只检查通过git add添加到暂存区的文件,避免每次检查都把整个项目的代码都检查一遍,从而提高效率。

  •  lint-staged 如何获取 "staged files" 列表?调用了哪些 Git 命令?

lint-staged 本质是通过 Git 命令 查询暂存区状态来获取文件列表,核心命令是:

git diff --cached --name-only --diff-filter=ACMR
  • 配置中可以使用函数 例如对不同类型的文件进行不同的规则匹配,函数效果更好,能根据文件的动态信息生成个性化的命令。

  • lint-staged 如何处理大量暂存文件?如何避免命令行长度限制? 将文件列表拆分成多个 “批次”,每个批次的文件名拼接后不超过长度限制。对每个批次分别执行命令(如 prettier --write file1.js file2.js ...),直到所有文件处理完成。

commitlint

commitlint 是一个用于检查 Git 提交信息(commit message)是否符合规范的工具

  • 除了 commitlint,你还知道什么工具可以帮助开发者更方便地编写出符合规范的 Commit Message?

例如:cz-git 用 cz 生成信息 → commit-msg 钩子触发 commitlint 检查 → 检查通过才允许提交。

  • 请深入解释 commit-msg 这个 Git Hook 的工作机制。当 husky 调用 commitlint 时,commitlint 是如何获取到你正在编辑的 Commit Message 内容的?

通过将msg放入临时文件中,然后将文件路径交给钩子脚本去进行校验。校验失败返回非0,代码非0停止终端提交。

  • commitlint 内部是如何解析一个 Commit Message 字符串并进行规则校验的?你可以猜测一下它可能用到的技术或核心逻辑吗? 将字符串转换成结构化的数据,正则,配置,插件,错误处理。

eslint

ESLint 是一个针对 JavaScript(及衍生语言如 TypeScript、JSX)的静态代码分析工具。它通过预设或自定义的规则,在代码运行前检查潜在问题,比如语法错误、未使用的变量、不合理的逻辑结构,以及代码风格不一致(如缩进、引号类型)等,帮助开发者提升代码质量、减少 bug 并保持团队代码风格统一。

  • AST 的定义及 ESLint 如何利用 AST 检查代码

解析生成 AST:通过解析器将源代码转换为 AST,使代码的语法结构可被程序 “理解”。 遍历 AST 节点:ESLint 会深度遍历 AST 的每个节点,触发配置中规则对特定节点类型的监听。 规则校验节点:每个规则本质是一个函数,会针对特定节点类型(如 CallExpression)进行检查。例如,no-console 规则会监听 CallExpression 节点,判断其是否是 console.log 之类的调用,若是则标记为错误。

  • TypeScript 为何需要 @typescript-eslint/parser?与 Espree 的本质区别

ESLint 本身无法直接处理 TypeScript 代码,必须依赖 @typescript-eslint/parser,原因是:
TypeScript 包含大量 JavaScript 不具备的语法(如类型注解 : number、接口 interface、泛型 <T> 等),而 ESLint 默认的解析器 Espree 仅能解析标准 JavaScript 语法,无法识别 TS 特有语法,会导致解析失败。

prettier

Prettier 是一款专注于代码格式化的工具,核心功能是通过自动化方式,按照预设规则统一代码的格式风格(如缩进、引号、换行、空格等)。

  • Prettier 与 ESLint/TSLint 的核心区别

Prettier:只负责代码格式(如缩进、引号、换行),不关心代码质量(如未定义变量、死循环、不合理的逻辑)。 ESLint/TSLint:主要负责代码质量检查(如语法错误、变量未使用、不符合最佳实践的逻辑),同时也包含部分格式规则(如缩进、分号)。

  • Prettier 的工作原理及与正则替换的本质区别

Prettier 的流程如下:文件遍历与过滤代码解析(生成 AST)AST 遍历与代码生成:通过AST能够理解代码,而不是直接通过正则。

  • eslint和Prettier的冲突

eslint-plugin-prettier 会把 Prettier 的格式化逻辑转换成一条 ESLint 规则(可以理解为 “将 Prettier 的规则注册到 ESLint 中”),这样 ESLint 在检查时,会把不符合 Prettier 格式的代码标记为 “ESLint 错误”。 eslint-config-prettier 则负责 “清场”,禁用 ESLint 自带的、可能与 Prettier 冲突的格式规则(比如 ESLint 的 indent 规则和 Prettier 的缩进逻辑可能不一致),避免出现 “同一处格式被两个工具判为‘错误’” 的矛盾。

英格兰严重缺水已成“国家重大事件”

2025年8月12日 20:49
英国环境局8月12日发布新闻公报说,“国家干旱小组”已将英格兰当前的缺水情况定义为“国家重大事件”。公报说,英格兰5个地区正式陷入干旱,另有6个地区在经历了自1976年来最干旱的上半年后,持续处于干燥天气中。与6月相比,英格兰许多河流流量和水库水位在7月持续下降。上周,英格兰的水库水位下降了2%,目前平均蓄水量为67.7%,而往年8月第一周平均值为80.5%。(新华社)

美国7月份CPI同比上涨2.7%,外界普遍预计美联储9月或将大概率降息

2025年8月12日 20:37
美国劳工部12日公布的数据显示,今年7月美国消费者价格指数(CPI)同比上涨2.7%,剔除波动较大的食品和能源价格后,7月核心CPI同比上涨3.1%。华尔街投行高盛预计,到今年12月时,美国核心CPI以及核心个人消费支出(PCE)价格指数的同比涨幅都将升至3.3%的水平。目前,外界普遍预计美联储9月将大概率降息,主要原因是美国就业市场出现降温迹象。不过,美联储官员仍然强调将依据最新数据制定货币政策,谨慎对待降息。(央视财经)

永和智控:终止协议转让暨终止控制权变更

2025年8月12日 20:35
36氪获悉,永和智控公告,公司收到控股股东、实际控制人曹德莅的《告知函》,获悉其与杭州润锋智能装备有限责任公司签署的《股份转让协议》及相关协议终止。曹德莅拟转让给杭州润锋的35660326股股份(占公司股份总数的8.00%)的转让协议自始无效并自动解除。同时,杭州润锋与曹德莅、夏祖望签署的《表决权委托协议》也自动失效。此次终止不会对公司产生重大影响,曹德莅仍为公司控股股东、实际控制人。

react-scripts的webpack.config.js配置解析

作者 北海天空
2025年8月12日 20:31

gitHub链接:github.com/facebook/cr…

解析 以下是 Create React App Webpack 配置的核心模块解析(基于 v4.0.3 版本):

一、基础架构

  1. 环境判断
    通过 webpackEnv 参数区分开发/生产环境:

    javascript
    const isEnvDevelopment = webpackEnv === 'development';
    const isEnvProduction = webpackEnv === 'production';
    
  2. 入口配置
    动态注入热更新客户端和业务代码:

    javascript
    entry: [
      isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient'),
      paths.appIndexJs // 主入口 src/index.js
    ].filter(Boolean)
    

二、核心优化策略

  1. 输出策略

    javascript
    output: {
      filename: isEnvProduction 
        ? 'static/js/[name].[contenthash:8].js' // 生产环境哈希命名
        : 'static/js/bundle.js', // 开发环境固定命名
      chunkFilename: isEnvProduction
        ? 'static/js/[name].[contenthash:8].chunk.js' 
        : 'static/js/[name].chunk.js',
      publicPath: paths.publicUrlOrPath // 从 package.json 的 homepage 推导
    }
    
  2. 分包策略
    自动拆分公共模块和运行时文件:

    javascript
    optimization: {
      splitChunks: { chunks: 'all', name: false }, // 自动拆分 vendor
      runtimeChunk: { name: entrypoint => `runtime-${entrypoint.name}` } 
    }
    

三、关键模块处理规则

  1. JS/TS 处理

    • 使用 babel-loader 配合 babel-preset-react-app

    • 通过 include: paths.appSrc 限制编译范围

    • 开发环境启用缓存加速构建:

      javascript
      cacheDirectory: true,
      cacheCompression: false
      
  2. 样式处理
    开发/生产环境差异化配置:

    javascript
    getStyleLoaders = (cssOptions, preProcessor) => {
      const loaders = [
        isEnvDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
        { loader: 'css-loader', options: cssOptions },
        {
          loader: 'postcss-loader',
          options: { // 自动添加浏览器前缀
            postcssOptions: {
              plugins: [['postcss-preset-env', { flexbox: 'no-2009' }]]
            }
          }
        }
      ]
    }
    
  3. 静态资源处理

    javascript
    {
      test: [/.bmp$/, /.gif$/, /.jpe?g$/, /.png$/],
      type: 'asset',
      parser: { dataUrlCondition: { maxSize: 10*1024 } } // <10KB 内联
    }
    

四、核心插件机制18

  1. HTML 生成

    javascript
    new HtmlWebpackPlugin({
      template: paths.appHtml, // 使用 public/index.html 模板
      minify: isEnvProduction ? { // 生产环境压缩
        removeComments: true,
        collapseWhitespace: true,
        minifyJS: true
      } : undefined
    })
    
  2. 环境变量注入

    javascript
    new webpack.DefinePlugin(env.stringified) // 注入 process.env
    
  3. 开发工具链

    • CaseSensitivePathsPlugin:强制区分文件名大小写
    • WatchMissingNodeModulesPlugin:监控模块安装状态
    • ForkTsCheckerWebpackPlugin:独立线程进行 TS 类型检查

五、安全策略

  1. 模块作用域限制

    javascript
    new ModuleScopePlugin(paths.appSrc) // 禁止导入 src 外部的源码
    
  2. 源码保护
    通过 @remove-on-eject 注释保护关键配置:

    javascript
    // @remove-on-eject-begin
    cacheIdentifier: getCacheIdentifier(...) // eject 后移除
    // @remove-on-eject-end
    

实现了开箱即用的最佳实践:开发环境优化构建速度(热更新、缓存),生产环境优化输出质量(代码分割、哈希命名)。

源码:

// @remove-on-eject-begin
/* 版权声明 */
// @remove-on-eject-end
'use strict';

// 核心模块引入
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const resolve = require('resolve');
// 插件模块
const PnpWebpackPlugin = require('pnp-webpack-plugin');          // PnP模块解析
const HtmlWebpackPlugin = require('html-webpack-plugin');        // HTML生成
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); // 路径大小写敏感
const TerserPlugin = require('terser-webpack-plugin');           // JS压缩
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // CSS提取

// ------------------------ 基础配置 ------------------------
const paths = require('./paths');          // 路径配置文件
const modules = require('./modules');      // 模块配置
const getClientEnvironment = require('./env'); // 环境变量

// ======================= 主配置函数 =======================
module.exports = function(webpackEnv) {
  // --------------- 环境判断 ---------------
  const isEnvDevelopment = webpackEnv === 'development'; // 开发模式
  const isEnvProduction = webpackEnv === 'production';   // 生产模式

  // --------------- 样式加载器工厂函数 ---------------
  const getStyleLoaders = (cssOptions, preProcessor) => {
    const loaders = [
      // 开发环境用style-loader注入<style>标签
      isEnvDevelopment && 'style-loader',
      // 生产环境提取CSS为独立文件
      isEnvProduction && {
        loader: MiniCssExtractPlugin.loader,
        options: { publicPath: '../../' } // 路径调整
      },
      {
        loader: 'css-loader', // 解析CSS导入
        options: cssOptions
      },
      {
        loader: 'postcss-loader', // 自动添加浏览器前缀
        options: {
          postcssOptions: {
            plugins: [
              ['postcss-preset-env', { flexbox: 'no-2009' }] // 兼容旧版flexbox
            ]
          }
        }
      }
    ].filter(Boolean); // 过滤空值

    // 预处理器(如Sass)
    if (preProcessor) {
      loaders.push(
        'resolve-url-loader',  // 解析相对路径
        {
          loader: preProcessor,
          options: { sourceMap: true } // 启用sourcemap
        }
      );
    }
    return loaders;
  };

  // ================ 返回完整配置对象 ================
  return {
    // 模式配置
    mode: isEnvProduction ? 'production' : 'development',
    // 生产环境构建失败时终止
    bail: isEnvProduction,
    // 开发工具配置
    devtool: isEnvProduction
      ? 'source-map'          // 生产环境完整sourcemap
      : 'cheap-module-source-map', // 开发环境快速sourcemap

    // --------------- 入口配置 ---------------
    entry: [
      // 开发环境注入热更新客户端
      isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient'),
      paths.appIndexJs // 主入口文件
    ].filter(Boolean),

    // --------------- 输出配置 ---------------
    output: {
      path: isEnvProduction ? paths.appBuild : undefined, // 生产输出目录
      filename: isEnvProduction
        ? 'static/js/[name].[contenthash:8].js' // 生产带哈希文件名
        : 'static/js/bundle.js', // 开发固定文件名
      chunkFilename: isEnvProduction // 异步块文件名
        ? 'static/js/[name].[contenthash:8].chunk.js'
        : 'static/js/[name].chunk.js',
      publicPath: paths.publicUrlOrPath, // 公共资源路径
      globalObject: 'this' // 兼容Web Worker
    },

    // --------------- 优化配置 ---------------
    optimization: {
      minimize: isEnvProduction, // 生产环境启用压缩
      minimizer: [
        // JS压缩器(生产环境)
        new TerserPlugin({
          terserOptions: {
            parse: { ecma: 8 }, // 支持ES2017解析
            compress: { 
              ecma: 5,
              comparisons: false, // 禁用对比优化
              inline: 2           // 函数内联级别
            }
          }
        }),
        // CSS压缩(生产环境)
        new OptimizeCSSAssetsPlugin({
          cssProcessorOptions: {
            parser: safePostCssParser,
            map: shouldUseSourceMap // 是否生成sourcemap
          }
        })
      ],
      // 代码分割策略
      splitChunks: {
        chunks: 'all',    // 同步/异步代码均分割
        name: false       // 自动生成chunk名称
      },
      // 分离Webpack运行时文件
      runtimeChunk: {
        name: entrypoint => `runtime-${entrypoint.name}`
      }
    },

    // --------------- 模块解析配置 ---------------
    resolve: {
      modules: ['node_modules', paths.appNodeModules], // 模块搜索路径
      extensions: ['.js', '.jsx', '.ts', '.tsx'],      // 自动解析扩展名
      plugins: [
        PnpWebpackPlugin, // 支持Yarn PnP
        // 限制src目录外部的模块导入
        new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson])
      ]
    },

    // --------------- 模块处理规则 ---------------
    module: {
      rules: [
        // JS/TS规则
        {
          test: /\.(js|mjs|jsx|ts|tsx)$/,
          include: paths.appSrc,
          loader: 'babel-loader', // 使用Babel转译
          options: {
            presets: ['react-app'], // CRA预设
            cacheDirectory: true,   // 启用缓存
            cacheCompression: false // 禁用缓存压缩
          }
        },
        // CSS规则
        {
          test: cssRegex,
          use: getStyleLoaders({ importLoaders: 1 })
        },
        // 图片资源处理
        {
          test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
          type: 'asset', // Webpack5内置资源模块
          parser: {
            dataUrlCondition: {
              maxSize: 10 * 1024 // 10KB以下转base64
            }
          }
        }
      ]
    },

    // --------------- 插件配置 ---------------
    plugins: [
      // 生成HTML文件
      new HtmlWebpackPlugin({
        template: paths.appHtml,   // HTML模板
        minify: isEnvProduction && { // 生产环境压缩
          removeComments: true,
          collapseWhitespace: true,
          removeRedundantAttributes: true
        }
      }),
      // 注入环境变量
      new webpack.DefinePlugin(getClientEnvironment().stringified),
      // 开发环境插件
      isEnvDevelopment && new webpack.HotModuleReplacementPlugin(), // HMR
      isEnvDevelopment && new CaseSensitivePathsPlugin(), // 路径大小写敏感
      // 生产环境插件
      isEnvProduction &&
        new MiniCssExtractPlugin({ // 提取CSS
          filename: 'static/css/[name].[contenthash:8].css',
          chunkFilename: 'static/css/[name].[contenthash:8].chunk.css'
        })
    ].filter(Boolean)
  };
};

游说团体称,瑞士希望特朗普就黄金关税作出具约束力的承诺

2025年8月12日 20:30
瑞士贵金属协会ASFCMP负责人周二表示,美国总统唐纳德・特朗普关于不会对黄金征收关税的声明为贸易稳定释放了令人鼓舞的信号,但只有正式决定才能带来确定性。特朗普周一称,他不会对黄金征收关税。这一举措受到全球黄金市场的欢迎,也结束了连日来关于黄金可能卷入当前全球贸易争端的猜测。(新浪财经)
❌
❌