面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 06月2日 23:11

React Server Components 是什么?和 Client Components 怎么配合?

React Server Components(RSC)是只在服务端渲染的组件——它们的代码不会发送到浏览器。这是 React 架构的根本变化:组件不再默认跑在客户端,而是默认跑在服务端。RSC 解决什么问题传统 React 应用把所有组件代码打包成 JS 发给浏览器。一个列表页可能有 200KB 的 JS,但大部分是数据获取和渲染逻辑,用户交互只占一小部分。用户要等 JS 下载、解析、执行完才能看到页面。RSC 的解决方案:数据获取和渲染在服务端完成,只把 HTML 和少量交互代码发给浏览器。结果:更快的首屏、更小的 JS 包、更简单的数据获取。Server Component vs Client Component// Server Component(默认)— 不发 JS 给浏览器async function ArticleList() { const articles = await db.article.findMany(); // 直接查数据库 return ( <ul> {articles.map(a => <li key={a.id}>{a.title}</li>)} </ul> );}// Client Component — JS 会发给浏览器'use client'import { useState } from 'react';function SearchBox() { const [query, setQuery] = useState(''); // 需要状态 return <input value={query} onChange={e => setQuery(e.target.value)} />;}判断标准很简单:需要 useState、useEffect、onClick 等 React hooks/事件的就是 Client Component,否则就是 Server Component。数据获取方式的变化Pages Router 时代,客户端获取数据:// Pages Router — 客户端获取useEffect(() => { fetch('/api/articles').then(r => r.json()).then(setArticles);}, []);App Router + RSC,服务端直接获取:// App Router — 服务端获取async function Page() { const articles = await db.article.findMany(); return <ArticleList articles={articles} />;}不需要 API 路由,不需要 loading 状态管理,不需要客户端缓存。服务端拿到数据直接渲染成 HTML。组合模式Server Component 可以渲染 Client Component,但反过来不行:// Server Componentimport SearchBox from './SearchBox'; // Client Componentasync function Page() { const data = await fetchData(); return ( <div> <SearchBox /> {/* Client Component:交互 */} <ArticleList data={data} /> {/* Server Component:展示 */} </div> );}关键规则:Server Component 可以 import 和渲染 Client ComponentClient Component 不能 import Server ComponentServer Component 可以通过 props 把数据传给 Client Component(必须是可序列化的数据)什么时候用 Client Component只有这四种情况需要 'use client':需要交互(onClick、onChange)需要状态(useState、useReducer)需要生命周期(useEffect)需要浏览器 API(window、localStorage)其他都用 Server Component。一个常见错误:因为不熟悉 RSC 而给所有组件加 'use client'——这样 App Router 就退化成了 Pages Router,失去了 RSC 的性能优势。RSC 的局限Server Component 不能用 hooks(useState、useEffect)Server Component 不能用浏览器 API(window、document)Server Component 传给 Client Component 的 props 必须可序列化(不能传函数、类实例)调试更难——错误堆栈跨服务端和客户端RSC 仍然在快速演进,API 可能在未来版本变化。但方向是明确的:服务端渲染更多,客户端 JS 更少。
服务端阅读 06月2日 23:10

Next.js Server Actions 怎么用?表单提交、状态管理和安全验证

Server Actions 是 Next.js 的服务端函数——在服务端执行,客户端直接调用,不需要手写 API 路由。底层是 POST 请求 + 加密签名,比传统 fetch + API Route 更简洁。基本用法// app/actions.ts'use server'export async function createPost(formData: FormData) { const title = formData.get('title') as string; const content = formData.get('content') as string; await db.post.create({ data: { title, content } }); revalidatePath('/posts'); // 刷新缓存}// app/posts/page.tsximport { createPost } from './actions';export default function NewPost() { return ( <form action={createPost}> <input name="title" /> <textarea name="content" /> <button type="submit">发布</button> </form> );}没有 API 路由,没有 fetch,没有 JSON 序列化。表单提交直接触发服务端函数。Server Action 的本质Server Action 编译后变成一个 POST 请求:POST /_next/data/... HTTP/1.1Content-Type: text/x-componentNext-Action: hashed-action-idFormData bodyNext.js 自动给 action 生成加密 ID,客户端调用时带这个 ID。请求到达服务端后,Next.js 根据 ID 找到对应函数执行。所以即使有人猜到 URL,没有正确的 action ID 也无法调用。useActionState 管理状态'use client'import { useActionState } from 'react';import { createPost } from './actions';export default function NewPost() { const [state, formAction, isPending] = useActionState( async (prevState, formData) => { const result = await createPost(formData); return result; }, null ); return ( <form action={formAction}> <input name="title" /> <button type="submit" disabled={isPending}> {isPending ? '提交中...' : '发布'} </button> {state?.error && <p>{state.error}</p>} </form> );}isPending 在请求期间为 true,可以显示加载状态。state 保存上一次 action 的返回值。输入验证永远不要信任客户端数据。Server Action 里必须验证:'use server'import { z } from 'zod';const schema = z.object({ title: z.string().min(1).max(200), content: z.string().min(10),});export async function createPost(formData: FormData) { const parsed = schema.safeParse({ title: formData.get('title'), content: formData.get('content'), }); if (!parsed.success) { return { error: parsed.error.flatten().fieldErrors }; } await db.post.create({ data: parsed.data }); revalidatePath('/posts');}Zod 验证比手写 if-else 更可靠,错误信息也结构化。revalidatePath 和 revalidateTagServer Action 修改数据后,需要告诉 Next.js 刷新缓存:revalidatePath('/posts'):刷新指定路径的缓存revalidateTag('posts'):刷新所有带 fetch(..., { next: { tags: ['posts'] } }) 标记的请求缓存revalidateTag 更灵活——一个 tag 可以对应多个页面,改一次全部刷新。安全注意事项Server Action 虽然有加密 ID 保护,但仍然是公开的 HTTP 端点:必须在 action 内做认证检查(getServerSession())必须验证输入(Zod)不要在 action 里返回敏感信息(返回值会发给客户端)'use server'export async function deleteAccount(formData: FormData) { const session = await getServerSession(); if (!session) throw new Error('Unauthorized'); // ...}和 API Route 的区别Server Action:适合表单提交、数据变更,自动处理 loading/error 状态API Route:适合第三方回调(Webhook)、文件上传、需要自定义响应头简单场景用 Server Action 更简洁。需要精细控制 HTTP 响应时用 API Route。
服务端阅读 06月2日 23:10

Next.js 中间件怎么用?认证重定向、A/B 测试和 Edge Runtime 限制

Next.js 中间件在请求到达页面之前执行,适合做认证检查、路由重写、A/B 测试等。它跑在 Edge Runtime 上,冷启动快但功能有限——不能用 Node.js API。基本用法在项目根目录创建 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(); // 放行,继续处理请求}// 限制中间件只对匹配的路径生效export const config = { matcher: ['/dashboard/:path*', '/api/:path*'],};matcher 很重要——不设的话每个请求(包括静态资源)都经过中间件,拖慢性能。认证重定向最常见的用例:未登录用户跳转到登录页:export function middleware(request: NextRequest) { const token = request.cookies.get('session-token'); if (!token) { const loginUrl = new URL('/login', request.url); loginUrl.searchParams.set('callbackUrl', request.nextUrl.pathname); return NextResponse.redirect(loginUrl); } return NextResponse.next();}export const config = { matcher: ['/dashboard/:path*'],};修改请求头中间件可以给请求加 header,页面里用 headers() 读取:export function middleware(request: NextRequest) { const requestHeaders = new Headers(request.headers); requestHeaders.set('x-pathname', request.nextUrl.pathname); return NextResponse.next({ request: { headers: requestHeaders }, });}页面里:import { headers } from 'next/headers';export default function Page() { const pathname = headers().get('x-pathname'); // ...}A/B 测试根据 Cookie 或随机分配给用户不同版本:export function middleware(request: NextRequest) { let variant = request.cookies.get('ab-variant')?.value; if (!variant) { variant = Math.random() > 0.5 ? 'A' : 'B'; } const response = NextResponse.next(); response.cookies.set('ab-variant', variant); // 重写到不同页面 if (variant === 'B' && request.nextUrl.pathname === '/pricing') { return NextResponse.rewrite(new URL('/pricing-b', request.url)); } return response;}NextResponse.rewrite 在服务端切换到另一个页面,用户看到的 URL 不变。限制:Edge Runtime中间件跑在 Edge Runtime,不是 Node.js。这意味着:不能用 fs、path、crypto(Node.js 内置模块)不能用 prisma、mongoose(数据库客户端)不能用 jsonwebtoken(需要 crypto)执行时间限制 30 秒(Vercel 上是 25 秒)简单的 token 验证、Cookie 操作没问题。复杂的认证逻辑(查数据库验证 token)不应该放中间件——放在页面组件或 API 路由里。常见问题中间件死循环:中间件重定向到 /login,但 /login 也匹配了 matcher。解决:matcher 排除 /login,或在中间件里判断路径。cookies 在 Server Component 里读不到:中间件 response.cookies.set() 设置的 Cookie 在同一请求的 Server Component 里读不到——因为中间件的 response 还没返回给浏览器。需要通过 request headers 传递。
服务端阅读 06月2日 23:08

Next.js 性能怎么优化?Server Components、图片和缓存策略实战

Next.js 性能优化从三个方向入手:减少客户端 JavaScript 体积、加快页面加载速度、优化数据获取策略。App Router 的 Server Components 天然比 Pages Router 快——大部分代码不发给浏览器。1. Server Components 优先App Router 默认所有组件都是 Server Component。只在需要交互(useState、useEffect、onClick)时才加 'use client':// Server Component(默认)— 不发 JS 给浏览器export default function Page() { return ( <div> <h1>标题</h1> {/* 静态内容 → Server Component */} <SearchBox /> {/* 需要交互 → Client Component */} <ArticleList articles={articles} /> {/* 纯展示 → Server Component */} </div> );}常见错误:整个页面都标 'use client'。正确做法是把交互部分抽成小的 Client Component,外层保持 Server Component。2. 图片优化next/image 自动做三件事:懒加载、按设备尺寸返回合适分辨率、WebP 格式转换。import Image from 'next/image';<Image src="/hero.jpg" width={1200} height={600} alt="描述" priority // 首屏图片加这个,跳过懒加载 placeholder="blur" // 模糊占位,加载时不会闪白/>必须填 width 和 height——防止布局偏移(CLS)。priority 只给首屏可见图片用,多了反而拖慢 LCP。外部图片需要配置域名白名单:// next.config.jsimages: { remotePatterns: [ { protocol: 'https', hostname: 'cdn.example.com' }, ],},3. 字体优化next/font 自动内联字体文件,消除 FOUT(字体闪烁):import { Inter } from 'next/font/google';const inter = Inter({ subsets: ['latin'] });export default function Layout({ children }) { return <body className={inter.className}>{children}</body>;}不要用 CSS @import 加载 Google Fonts——它会阻塞渲染。next/font 在构建时下载字体文件,零网络请求。4. 动态导入减少首屏 JS非首屏需要的组件用 dynamic 懒加载:import dynamic from 'next/dynamic';const HeavyChart = dynamic(() => import('./HeavyChart'), { loading: () => <div>加载中...</div>, ssr: false, // 纯客户端组件不需要 SSR});ssr: false 跳过服务端渲染——适合图表、编辑器这类重交互、不需要 SEO 的组件。5. 数据获取策略App Router 的 fetch 默认有缓存:// 缓存(默认)— 适合不常变的数据const data = await fetch('https://api.example.com/data');// 不缓存 — 每次请求都重新获取const data = await fetch('https://api.example.com/data', { cache: 'no-store'});// 定时重新验证 — 适合有一定延迟容忍的数据const data = await fetch('https://api.example.com/data', { next: { revalidate: 3600 } // 1 小时后重新验证});ISR(Incremental Static Regeneration)是 Next.js 的杀手锏:静态页面生成后定时更新,不需要每次请求都渲染。revalidate 的时间根据数据变化频率设置——新闻列表 60 秒,配置数据 3600 秒。6. Layout 防止重复渲染App Router 的 layout.tsx 在导航时不会重新渲染。把不会变的 UI(导航栏、页脚)放在 layout 里:// app/layout.tsx — 只渲染一次export default function RootLayout({ children }) { return ( <html> <body> <nav>导航栏</nav> {children} {/* 只有这部分会随路由变化 */} <footer>页脚</footer> </body> </html> );}7. 分析打包体积npx @next/bundle-analyzer生成可视化报告,找出最大的包。常见问题:整个 lodash 被 import(改用 lodash-es 的按需导入)、moment.js 太大(改用 dayjs)、客户端不必要的包。性能优化检查清单Server Component 优先,'use client' 只在需要交互时用图片用 next/image + width/height + priority(首屏)字体用 next/font,不用 CSS @import非首屏组件 dynamic 导入fetch 设置合理的 cache/revalidate 策略静态 UI 放 layout,不放 pagebundle-analyzer 检查大包
服务端阅读 06月2日 23:07

Next.js 应用有哪些安全风险?环境变量泄露、XSS 和 CSRF 防护实战

Next.js 应用的安全风险主要来自三方面:服务端渲染(SSR)泄露敏感数据、API 路由缺乏认证、客户端代码暴露过多信息。逐个堵住就行。1. 环境变量:服务端 vs 客户端Next.js 的环境变量默认只在服务端可用。以 NEXT_PUBLIC_ 开头的才会暴露给浏览器。最常见的错误:把数据库密码、API Key 加了 NEXT_PUBLIC_ 前缀。# .env.local — 只在服务端可用DATABASE_URL=postgresql://...STRIPE_SECRET_KEY=sk_live_...# .env.local — 暴露给浏览器(谨慎使用)NEXT_PUBLIC_API_URL=https://api.example.comNEXT_PUBLIC_GA_ID=G-XXXXXXX检查方法:浏览器 F12 > Sources > 搜索你的密钥。如果搜到了,说明 NEXT_PUBLIC_ 用错了。2. API 路由必须加认证App Router 的 Route Handlers 默认没有任何认证:// app/api/users/route.ts — 裸奔的 API,任何人都能访问export async function GET() { const users = await db.user.findMany(); return Response.json(users);}加上认证中间件:import { getServerSession } from 'next-auth';export async function GET(request: Request) { const session = await getServerSession(); if (!session) { return new Response('Unauthorized', { status: 401 }); } const users = await db.user.findMany(); return Response.json(users);}更高效的方式:用 Next.js 中间件统一拦截,不用每个路由单独写:// middleware.tsimport { NextResponse } from 'next/server';import type { NextRequest } from 'next/server';export function middleware(request: NextRequest) { const token = request.cookies.get('session-token'); if (!token && request.nextUrl.pathname.startsWith('/api/')) { return new Response('Unauthorized', { status: 401 }); } return NextResponse.next();}export const config = { matcher: '/api/:path*',};3. 防止 XSSReact 默认转义 HTML,XSS 风险不大。但 dangerouslySetInnerHTML 是例外:// 危险:用户输入的 HTML 直接渲染<div dangerouslySetInnerHTML={{ __html: userContent }} />// 安全:先用 DOMPurify 清洗import DOMPurify from 'isomorphic-dompurify';<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userContent) }} />另一个 XSS 来源:URL 参数直接插入页面。Next.js 的 useSearchParams() 读取的值如果不转义就渲染,可能被注入。4. CSRF 防护SameSite Cookie 是最简单的 CSRF 防护:// 设置 Cookie 时加 SameSitecookies().set('session-token', token, { httpOnly: true, secure: true, sameSite: 'lax', // 阻止跨站请求携带 Cookie path: '/', maxAge: 60 * 60 * 24 * 7,});sameSite: 'lax' 允许顶层导航携带 Cookie(用户点链接跳转正常),但阻止跨站 POST 请求携带。对大部分应用够用。5. Content Security PolicyCSP 限制页面能加载哪些外部资源,防止恶意脚本注入:// middleware.tsexport function middleware(request: NextRequest) { const response = NextResponse.next(); response.headers.set('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-eval' https://cdn.example.com; style-src 'self' 'unsafe-inline';" ); return response;}CSP 配置比较繁琐,建议用 next-safe 库简化。先从 default-src 'self' 开始,逐步放宽需要的域名。6. 依赖安全审计npm audit # 检查已知漏洞npm audit fix # 自动修复npx better-npm-audit audit # 更详细的审计定期跑 npm audit,高危漏洞必须修。Next.js 自身也有安全更新——保持版本最新。安全检查清单环境变量没有敏感信息泄露到客户端API 路由都有认证Cookie 设置了 httpOnly + secure + sameSitedangerouslySetInnerHTML 用了 DOMPurify有 CSP 头npm audit 无高危漏洞
服务端阅读 06月2日 23:06

TypeORM 实体关系怎么定义?一对一、一对多、多对多装饰器详解

TypeORM 用装饰器定义实体间的关系:一对一、一对多、多对多。关键是搞清谁是"拥有方"(存外键的一方),谁是"反方"(只声明关系不存外键)。一对一(OneToOne)一个人只有一个档案,一个档案只属于一个人:@Entity()export class User { @PrimaryGeneratedColumn() id: number; @OneToOne(() => Profile, profile => profile.user) @JoinColumn() // 外键加在这一侧(拥有方) profile: Profile;}@Entity()export class Profile { @PrimaryGeneratedColumn() id: number; @OneToOne(() => User, user => user.profile) user: User; // 反方,不加 @JoinColumn}@JoinColumn() 标记拥有方——数据库会在 User 表加一个 profileId 外键列。一对一关系只有一方能加 @JoinColumn,另一方是反向引用。一对多 / 多对一(OneToMany / ManyToOne)一个用户有多篇文章,一篇文章属于一个用户:@Entity()export class User { @PrimaryGeneratedColumn() id: number; @OneToMany(() => Post, post => post.author) posts: Post[];}@Entity()export class Post { @PrimaryGeneratedColumn() id: number; @ManyToOne(() => User, user => user.posts) @JoinColumn({ name: 'authorId' }) // 外键在 Post 表 author: User;}ManyToOne 总是拥有方——外键存在"多"的一侧的表里。OneToMany 只是反向引用,不能单独存在,必须配对 ManyToOne。数据库里 Post 表有一个 authorId 列指向 User.id。多对多(ManyToMany)一个文章有多个标签,一个标签属于多篇文章:@Entity()export class Post { @PrimaryGeneratedColumn() id: number; @ManyToMany(() => Tag, tag => tag.posts) @JoinTable() // 拥有方加 @JoinTable,自动创建中间表 tags: Tag[];}@Entity()export class Tag { @PrimaryGeneratedColumn() id: number; @ManyToMany(() => Post, post => post.tags) posts: Post[]; // 反方,不加 @JoinTable}@JoinTable() 让 TypeORM 自动创建中间表 post_tags_tags(postid + tagid)。只有一方加 @JoinTable,另一方是反向引用。加载关联数据定义了关系不代表查询时自动加载。必须显式指定:// 方式一:find 时指定 relationsconst user = await userRepo.findOne({ where: { id: 1 }, relations: ['posts', 'posts.tags'] // 支持嵌套});// 方式二:QueryBuilder joinconst user = await userRepo .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .leftJoinAndSelect('post.tags', 'tag') .where('user.id = :id', { id: 1 }) .getOne();不加 relations 或 leftJoinAndSelect,返回的 user.posts 就是 undefined。这是 TypeORM 的设计选择——避免无意义的 JOIN 查询。常见坑循环引用:User 引用 Post,Post 引用 User。TypeScript 用 () => Post 延迟求值避免循环依赖。不要直接写 Post,必须用箭头函数。级联操作:cascade: true 让保存 User 时自动保存关联的 Post。小心使用——可能在你不知情时写入大量数据。建议只在明确的父子关系上使用。删除行为:onDelete: 'CASCADE' 在数据库层面级联删除。删除 User 时自动删除其所有 Post。比应用层级联更可靠。
服务端阅读 06月2日 23:04

TypeORM Active Record 和 Data Mapper 有什么区别?怎么选?

TypeORM 支持两种数据访问模式:Active Record(实体自带 CRUD 方法)和 Data Mapper(通过 Repository 操作实体)。选哪种影响代码组织方式,但不影响功能。Active Record 模式实体继承 BaseEntity,直接调用静态方法操作数据:@Entity()export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ unique: true }) email: string;}// 使用:实体自带 find/save/removeconst users = await User.find();const user = await User.findOne({ where: { email: 'test@test.com' } });user.name = 'Updated';await user.save();await user.remove();优点:代码简洁,一个对象既承载数据又负责持久化。适合简单 CRUD 和小项目。缺点:实体和数据库操作耦合——同一个类既定义数据结构又包含查询逻辑,业务复杂后类会膨胀。Data Mapper 模式实体是纯数据对象,通过 Repository 操作:@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ unique: true }) email: string;}// 使用:通过 Repository 操作const userRepo = dataSource.getRepository(User);const users = await userRepo.find();const user = await userRepo.findOne({ where: { email: 'test@test.com' } });user.name = 'Updated';await userRepo.save(user);await userRepo.remove(user);优点:关注点分离——实体只管数据结构,Repository 负责持久化逻辑。业务代码不依赖数据库 API,方便单元测试(mock Repository)。缺点:多一层间接,简单操作代码量稍多。怎么选Active Record 适合:小项目,实体少,查询逻辑简单快速原型,不想写太多样板代码Rails/Django 背景,习惯模型自带操作Data Mapper 适合:中大型项目,业务逻辑复杂需要单元测试,要 mock 数据层领域驱动设计(DDD)风格,实体只表达业务概念NestJS 项目推荐 Data Mapper——NestJS 的 @InjectRepository() 天然就是 Data Mapper 风格,依赖注入也让测试更方便。两种模式可以混用TypeORM 不强制二选一。同一个实体可以同时用两种方式:@Entity()export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number; // Active Record 方式 static findActive() { return this.find({ where: { active: true } }); }}// Data Mapper 方式也可以const repo = dataSource.getRepository(User);const users = await repo.find({ where: { active: true } });但不建议混用——团队应该统一风格,避免同一项目里两种模式交叉出现。自定义查询放哪里Active Record 模式的常见问题:复杂查询方法堆积在实体类上。// 不推荐:实体类越来越胖export class User extends BaseEntity { static findWithStats() { ... } static findActiveUsers() { ... } static findByDepartment() { ... } static searchByName() { ... }}// 推荐:抽取到单独的 Service 或自定义 Repositoryexport class UserService { constructor(@InjectRepository(User) private repo: Repository<User>) {} async findWithStats() { return this.repo.createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .loadRelationCountAndMap('user.postCount', 'user.posts') .getMany(); }}Data Mapper 模式自然避免这个问题——查询逻辑在 Service 里,实体保持干净。
服务端阅读 06月2日 01:50

TypeORM 查询缓存怎么配?Redis 缓存和缓存一致性实战

数据库查询是后端应用最常见的性能瓶颈。缓存是第一道防线——能从缓存拿的数据就不查数据库。TypeORM 支持查询级缓存和 Redis 缓存,配置简单但有几个细节要注意。TypeORM 查询缓存开启缓存后,相同的 SQL 查询结果会被缓存,指定时间内不重复查数据库:TypeOrmModule.forRoot({ type: 'postgres', cache: { type: 'redis', options: { host: 'localhost', port: 6379, }, duration: 30000, // 缓存 30 秒 },}),在查询时指定缓存:const users = await userRepo.find({ cache: true, // 使用全局 duration(30 秒)});// 或自定义缓存时间const users = await userRepo.find({ cache: 60000, // 缓存 60 秒});// QueryBuilder 也可以const users = await createQueryBuilder('user') .cache(true) .getMany();缓存 key 基于 SQL 语句自动生成——相同 SQL 共享缓存,不同 SQL 各自缓存。什么时候用缓存适合缓存的查询:配置数据(很少变)用户信息(变更频率低)热门文章列表(可以接受短暂不一致)不适合缓存的查询:实时数据(库存、余额)分页偏移大的查询(缓存命中率低)写多读少的数据(缓存频繁失效)缓存一致性缓存的经典问题:数据库更新了但缓存还是旧数据。TypeORM 不会自动在数据变更时清除缓存——需要手动处理。方案一:写入后清除缓存async function updateUser(id: number, data: Partial<User>) { await userRepo.update(id, data); // 清除与 User 相关的所有缓存 await getConnection().queryResultCache.clear();}queryResultCache.clear() 清除所有缓存,比较粗暴但简单。精细清除需要知道具体的缓存 key,TypeORM 没有直接提供按表清除的 API。方案二:短过期时间 + 接受短暂不一致const users = await userRepo.find({ cache: 5000, // 5 秒过期,最多延迟 5 秒});简单有效,大部分场景够用。5 秒的不一致对用户体验几乎没有影响。方案三:手动管理缓存(Redis 直接操作)绕过 TypeORM 的缓存机制,用 ioredis 自己管理:import Redis from 'ioredis';const redis = new Redis();async function getUser(id: string) { const cached = await redis.get(`user:${id}`); if (cached) return JSON.parse(cached); const user = await userRepo.findOne(id); await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 60); return user;}async function updateUser(id: string, data: Partial<User>) { await userRepo.update(id, data); await redis.del(`user:${id}`); // 精确清除}自己管理更灵活——可以按 key 精确清除、设置不同过期时间、做缓存预热。但需要更多代码。缓存命中率监控Redis 缓存命中率反映了缓存是否有效:redis-cli info stats | grep keyspace_hitsredis-cli info stats | grep keyspace_misses命中率低于 50% 说明缓存策略有问题——要么过期时间太短,要么查询太分散没有复用。TypeORM 缓存的局限缓存粒度是 SQL 级别,不是实体级别。同一个 User 的不同查询(列表查询 vs 详情查询)有各自独立的缓存没有 Cache Aside 模式的内置支持(先查缓存再查数据库)分布式环境下需要用 Redis 共享缓存,内存缓存(默认)会导致各实例缓存不一致如果缓存需求复杂,建议绕过 TypeORM 缓存,直接用 Redis + 自定义缓存层。
服务端阅读 06月2日 01:49

TypeORM 软删除怎么用?@DeleteDateColumn 配置和唯一约束冲突解决

软删除不是真的删除数据,而是标记 deletedAt 字段,查询时自动过滤已删除的记录。TypeORM 内置支持软删除,一行配置开启。开启软删除在实体上加 @DeleteDateColumn():@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @DeleteDateColumn() deletedAt: Date; // null = 未删除,有值 = 已删除}delete 操作变成 UPDATE:await userRepo.softDelete(1);// SQL: UPDATE user SET "deletedAt" = NOW() WHERE id = 1await userRepo.remove(user);// 同样效果,设置 deletedAt 而不是 DELETEfind 自动排除已删除记录:const users = await userRepo.find();// SQL: SELECT * FROM user WHERE "deletedAt" IS NULL查询包含已删除的记录// 包含已删除的记录const allUsers = await userRepo.find({ withDeleted: true});// 只查已删除的记录const deletedUsers = await userRepo.find({ where: { deletedAt: Not(IsNull()) }, withDeleted: true});withDeleted: true 告诉 TypeORM 不要加 WHERE "deletedAt" IS NULL 条件。恢复已删除的记录await userRepo.restore(1);// SQL: UPDATE user SET "deletedAt" = NULL WHERE id = 1restore 把 deletedAt 设回 NULL,记录恢复正常。软删除的坑1. 唯一约束冲突软删除后,唯一字段(如 email)仍然占用唯一约束。删除 user@email.com 后,新建同名用户会报唯一冲突。解决方案:唯一约束包含 deletedAt。@Entity()@Unique(['email', 'deletedAt'])export class User { @Column() email: string; @DeleteDateColumn() deletedAt: Date;}但这样未删除记录的 deletedAt 是 NULL,多条 NULL 在 PostgreSQL 的唯一约束里不冲突(符合 SQL 标准),MySQL 则需要用条件索引或去掉唯一约束。2. 关联数据不会级联软删除@OneToMany(() => Post, post => post.author)posts: Post[];软删除 User 时,Post 不会被自动软删除。需要手动处理:async function softDeleteUser(id: number) { await postRepo.softDelete({ authorId: id }); await userRepo.softDelete(id);}3. 物理删除仍然是需要的软删除的数据积累会膨胀表。定期清理:// 物理删除 30 天前软删除的记录await userRepo .createQueryBuilder() .delete() .where("deletedAt < :date", { date: new Date(Date.now() - 30 * 86400000) }) .execute();不用软删除的替代方案事件溯源:不删除数据,而是追加"已删除"事件。查询时通过事件回放构建当前状态归档表:DELETE 前把数据 INSERT 到归档表,然后物理删除。查询只看主表,归档表做审计软删除适合"需要恢复、需要审计追踪"的场景。如果只是为了安全,备份 + 物理删除更干净。
服务端阅读 06月2日 01:49

Node.js ORM 怎么选?TypeORM、Prisma、Sequelize、MikroORM 对比

Node.js 的 ORM 主要有四个:TypeORM、Prisma、Sequelize、MikroORM。2025 年的格局:Prisma 增长最快,TypeORM 用量最大但有退潮趋势,Sequelize 已经老化,MikroORM 是小众但口碑好。四个 ORM 对比| 维度 | TypeORM | Prisma | Sequelize | MikroORM ||------|---------|--------|-----------|----------|| 类型安全 | 弱(装饰器 + any) | 强(自动生成类型) | 弱 | 强 || 查询方式 | 装饰器 + QueryBuilder | 链式 API | 链式 API | 链式 API + QB || 迁移工具 | 内置,体验一般 | 最好 | 内置 | 内置 || 关联查询 | 容易 N+1 | include 清晰 | include | 自动 JOIN || 社区 | 最大 | 增长最快 | 缩小中 | 小但活跃 || 学习曲线 | 中 | 低 | 高 | 中 || NestJS 集成 | 官方推荐 | 社区包 | 社区包 | 社区包 |Prisma:类型安全的赢家Prisma 的核心优势是类型推导——schema 定义一次,查询自动补全、返回类型自动推导,几乎不需要手写类型。model User { id String @id @default(uuid()) email String @unique posts Post[]}const user = await prisma.user.findUnique({ where: { email: 'test@test.com' }, include: { posts: true }});// user 的类型自动包含 posts,不需要手动标注迁移体验也是最好的:npx prisma migrate dev --name init 自动生成迁移 SQL,prisma migrate deploy 在生产环境执行。Prisma 的缺点:schema 用自己的 DSL(不是 TypeScript),复杂查询要写 raw SQL,N+1 问题用 include 解决但不如 DataLoader 灵活。TypeORM:装饰器风格的老牌TypeORM 用装饰器定义实体,和 NestJS 风格统一:@Entity()export class User { @PrimaryGeneratedColumn('uuid') id: string; @Column({ unique: true }) email: string; @OneToMany(() => Post, post => post.author) posts: Post[];}优势:和 NestJS 深度集成,装饰器风格统一。QueryBuilder 灵活,复杂查询不依赖 raw SQL。劣势:类型安全弱——findOne 返回的是 User | undefined,关联字段需要手动标注类型。N+1 问题严重,relations 的加载策略容易踩坑。迁移工具粗糙,自动生成的迁移经常需要手动修改。Sequelize:该退休了Sequelize 是最老的 Node.js ORM,v5 及以前用 callback 风格,v6 改成了 Promise 但 API 设计仍然笨重。类型安全最差(TypeScript 支持是后加的)。不建议新项目使用。MikroORM:小众精品MikroORM 的设计理念更接近 Doctrine(PHP),强调 Unit of Work 模式——所有修改先在内存中记录,flush() 时一次性写入数据库。好处是自动处理关联关系,不需要手动 save 每个实体。const user = em.create(User, { email: 'test@test.com' });await em.flush(); // 一次性写入类型安全比 TypeORM 好很多,自动推导关联类型。但社区小,遇到问题不好查。适合对 ORM 设计有洁癖的开发者。怎么选新项目:Prisma(类型安全 + 迁移体验),或 MikroORM(如果你喜欢 Unit of Work)NestJS 项目:TypeORM 仍是官方默认,但 Prisma 在 NestJS 社区的采用率快速上升已有 TypeORM 项目:不必迁移。TypeORM 能正常工作,迁移成本 > 收益不要选 Sequelize:除非你在维护老项目
服务端阅读 06月2日 01:46

TypeORM 查询太慢怎么优化?N+1、索引和批量插入实战

TypeORM 性能问题主要来自三个地方:N+1 查询、缺少索引、大量数据操作用了逐条插入。逐一解决后,大部分应用的数据库性能就够了。1. 解决 N+1 查询最常见也最致命的性能问题。查询用户列表后逐个查用户的文章:// N+1:1 次查用户 + N 次查文章const users = await userRepo.find();for (const user of users) { user.posts = await postRepo.find({ where: { authorId: user.id } });}100 个用户 = 101 条 SQL。解决方案:用 relations 或 join 一次性查出。// 方案一:relations(发 2 条 SQL,但比 N+1 好很多)const users = await userRepo.find({ relations: ['posts'] });// 方案二:QueryBuilder join(1 条 SQL)const users = await userRepo .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .getMany();leftJoinAndSelect 同时查出关联数据。leftJoin 只 join 不 select——用于过滤条件但不返回关联数据。2. 只查需要的字段find() 默认 SELECT 所有列。大表里有 TEXT/BLOB 列时,查全列浪费网络带宽和内存:// 查全列const users = await userRepo.find();// 只查需要的列const users = await userRepo.find({ select: ['id', 'name', 'email']});// QueryBuilder 方式const users = await userRepo .createQueryBuilder('user') .select(['user.id', 'user.name', 'user.email']) .getMany();列表页只需要 id 和 name,详情页才需要所有字段。按场景选择查询范围。3. 分页不要用 skip/take 处理大偏移// 大偏移时很慢:MySQL 要扫描前 10000 行然后丢掉const posts = await postRepo.find({ skip: 10000, take: 20});改用游标分页(cursor-based pagination):// 基于 ID 的游标分页const posts = await postRepo.find({ where: { id: LessThan(lastId) }, order: { id: 'DESC' }, take: 20});游标分页不依赖偏移量,性能与页码无关。缺点是不能跳转到指定页码。4. 批量插入逐条插入 1000 条数据 = 1000 条 INSERT 语句,极慢:// 慢:逐条插入for (const item of data) { await repo.save(item);}// 快:批量插入await repo.save(data); // TypeORM 自动合并成批量 INSERTsave 传入数组时,TypeORM 会合并成一条 INSERT 语句(部分数据库支持)。更大数据量用 createQueryBuilder 的 INSERT:await createQueryBuilder() .insert() .into(User) .values(data) .execute();极大数据量(10 万+)分批插入,每批 1000 条,避免单条 SQL 过长。5. 索引优化TypeORM 的 @Index 装饰器声明索引:@Entity()@Index(['email']) // 单列索引@Index(['firstName', 'lastName']) // 复合索引export class User { @PrimaryGeneratedColumn() id: number; @Column() email: string; @Column() firstName: string; @Column() lastName: string;}索引设计原则:WHERE 条件里的列加索引复合索引把区分度高的列放前面(@Index(['status', 'createdAt']) 如果 status 只有 3 个值就不如 @Index(['createdAt', 'status']))不要过度索引——每个索引增加写入开销查看慢查询:EXPLAIN ANALYZE + SQL 日志。TypeORM 开启 SQL 日志:TypeOrmModule.forRoot({ logging: true, // 打印所有 SQL})6. 连接池配置默认连接池大小可能不够用:TypeOrmModule.forRoot({ type: 'postgres', extra: { max: 20, // 最大连接数 idleTimeoutMillis: 30000, },})max 设为 CPU 核心数 * 2 + 磁盘数是常见公式。太多连接反而增加数据库锁争用。
服务端阅读 06月2日 01:46

npm 是什么?包管理器核心概念和 package.json 详解

npm(Node Package Manager)是 Node.js 的默认包管理器,负责安装、管理和发布 JavaScript 包。npm 由三部分组成:命令行工具、在线仓库(registry)和网站(npmjs.com)。npm 做什么安装别人写的包:npm install react 从 registry 下载 react 及其依赖到 node_modules 目录。一个项目通常有几十到上百个依赖,手动下载不现实。管理项目依赖:package.json 记录项目需要哪些包和版本范围,package-lock.json 锁定精确版本。任何人 clone 项目后 npm install 就能还原相同的依赖。运行脚本:package.json 的 scripts 字段定义常用命令:{ "scripts": { "dev": "vite", "build": "vite build", "test": "jest" }}npm run dev 执行 vite 命令,比手动敲完整命令方便。npm 自动把 node_modules/.bin 加到 PATH,不用全局安装工具。发布自己的包:npm publish 把代码上传到 registry,其他人就能 npm install 使用。package.json 核心字段{ "name": "my-app", "version": "1.0.0", "dependencies": { "react": "^18.2.0" }, "devDependencies": { "jest": "^29.0.0" }, "scripts": { "start": "node index.js" }}dependencies:生产环境需要的包(React、Express)devDependencies:开发环境需要的包(测试工具、构建工具)安装时 npm install -D 写入 devDependencies,npm install 写入 dependenciesnode_modules 的结构npm install 把所有包下载到 nodemodules 目录。包之间可以互相依赖,形成依赖树。npm 3+ 用扁平结构——尽量把依赖提升到顶层 nodemodules,减少嵌套。node_modules 通常很大(几百 MB),不要提交到 Git。.gitignore 里加上 node_modules/。npm vs yarn vs pnpmnpm:Node.js 自带,零配置,生态兼容性最好Yarn:Facebook 出品,早期比 npm 快,现在差距不大。Yarn 1 已停止维护pnpm:用硬链接共享包文件,磁盘占用少 3-5 倍,安装速度最快新项目推荐 pnpm 或 npm。已有项目没必要迁移。初始化项目mkdir my-project && cd my-projectnpm init -y # 生成默认 package.jsonnpm install express # 安装第一个依赖npm init -y 跳过交互式提问,直接用默认值生成 package.json。
服务端阅读 06月2日 01:45

npm 版本号 ^ 和 ~ 有什么区别?SemVer 和 package-lock 详解

npm 用语义化版本(SemVer)管理包版本,package.json 声明版本范围,package-lock.json 锁定精确版本。理解版本范围符号能避免"昨天还好好的今天就挂了"的问题。语义化版本号版本号格式:主版本.次版本.补丁版本(Major.Minor.Patch)Patch(1.0.x):Bug 修复,不改变 APIMinor(1.x.0):新增功能,向后兼容Major(x.0.0):破坏性变更,不向后兼容npm install 默认安装最新版本,但 package.json 里记录的是版本范围,不是精确版本。版本范围符号精确版本"react": "18.2.0" // 只安装 18.2.0,不安装任何其他版本插入号 ^(Caret)"react": "^18.2.0" // >=18.2.0 <19.0.0"lodash": "^4.17.0" // >=4.17.0 <5.0.0允许 Minor 和 Patch 更新,锁定主版本。最常用的范围符号——npm install react 默认就用 ^。规则:左边第一个非零数字锁定。^1.2.3 → >=1.2.3 <2.0.0^0.2.3 → >=0.2.3 <0.3.0(0.x 视为开发阶段,次版本也可能破坏兼容)^0.0.3 → >=0.0.3 <0.0.4(0.0.x 视为实验阶段,几乎锁定)波浪号 ~(Tilde)"react": "~18.2.0" // >=18.2.0 <18.3.0只允许 Patch 更新,锁定主版本和次版本。比 ^ 更保守,适合需要稳定但偶尔接受 bug 修复的场景。其他范围"react": ">=18.0.0" // 大于等于 18"react": "18.0.0 - 18.2.0" // 闭区间"react": "*" // 任意版本(危险,不要用)"react": "latest" // 最新版本(同上,危险)"react": "file:../local-pkg" // 本地路径package-lock.json 的作用package.json 声明范围,package-lock.json 锁定精确版本。// package.json"react": "^18.2.0" // 范围:18.2.0 到 18.x.x// package-lock.json"react": "18.2.0" // 实际安装的精确版本没有 lock 文件时,npm install 每次可能安装不同版本(18.2.0 → 18.3.1),导致团队成员或 CI 环境的依赖不一致。lock 文件保证所有人安装完全相同的版本。必须把 package-lock.json 提交到 Git。不要把它加入 .gitignore。版本冲突排查npm ls react # 查看项目中安装的 react 版本npm outdated # 列出所有过时的包npm view react versions # 查看包的所有已发布版本重复依赖问题(同一个包安装了多个版本):npm dedupe 尝试去重。save-exact:精确安装不想用 ^ 范围?配置 npm 默认用精确版本:npm config set save-exact true# 或在 .npmrc 里加save-exact=true之后 npm install react 会写 "react": "18.2.0" 而不是 "react": "^18.2.0"。适合对版本控制严格的团队。
服务端阅读 06月2日 01:43

npm、Yarn 和 pnpm 怎么选?2025 年包管理器对比

npm 和 Yarn 都是 JavaScript 包管理器,做的事情一样(安装依赖、管理版本、运行脚本),区别在于速度、稳定性和锁文件机制。2025 年的实际情况:npm 够用,pnpm 值得切换,Yarn 1 已过时。核心差异| 维度 | npm | Yarn 1 (Classic) | Yarn 2+ (Berry) | pnpm ||------|-----|-------------------|-----------------|------|| 锁文件 | package-lock.json | yarn.lock | yarn.lock | pnpm-lock.yaml || 安装速度 | 中 | 快 | 快 | 最快 || 离线安装 | 缓存但需网络 | 支持 | 支持 | 支持 || Monorepo | workspaces (npm 7+) | workspaces | workspaces | workspaces || 磁盘占用 | 高 | 高 | 高 | 低(硬链接) || Plug'n'Play | 不支持 | 不支持 | 支持 | 不支持 |Yarn 曾经的优势,npm 已经追平Yarn 1 在 2016 年发布时碾压 npm:确定性安装(yarn.lock)、并行下载、离线缓存。但 npm 5+ 引入了 package-lock.json,npm 7+ 加了 workspaces,核心差距已经很小。2025 年不建议新项目用 Yarn 1。它已经停止维护(最后版本 1.22.x),安全漏洞不会修复。Yarn 2+ (Berry):激进但有代价Yarn Berry 引入了 Plug'n'Play(PnP)——不生成 nodemodules,用 .pnp.cjs 文件映射包路径。好处是安装快、磁盘占用小。代价是很多依赖 nodemodules 的工具不兼容,需要额外配置。零安装(Zero-Install)是 Berry 的另一个特性——把 .yarn/cache 提交到 Git,clone 后不用 npm install 直接开发。对小团队方便,但 cache 目录会让 Git 仓库膨胀。实际采用率不高——PnP 的兼容性问题导致迁移成本大,很多团队试了又切回 npm。pnpm:当前最值得切换的方案pnpm 用硬链接替代复制——所有项目共享同一份包文件,磁盘占用只有 npm 的 1/3 到 1/5。安装速度也最快(比 npm 快 2-3 倍)。npm install -g pnpmpnpm install # 替代 npm installpnpm add react # 替代 npm install reactpnpm run dev # 替代 npm run devpnpm 的严格模式(非扁平的 node_modules)避免了幽灵依赖——你只能 import 声明过的包,不会意外引用到间接依赖。这对项目长期维护是好事,但迁移老项目时可能暴露隐藏的依赖问题。怎么选新项目:pnpm(磁盘省、速度快、严格依赖)或 npm(零配置、生态兼容性最好)已有项目用 npm:没必要迁移。npm 够用,迁移收益不大已有项目用 Yarn 1:建议迁移到 pnpm 或 npm。Yarn 1 不再维护已有项目用 Yarn Berry:如果 PnP 工作正常就继续用,没有问题就不要换一句话:没有强需求就不要换包管理器。迁移成本 > 收益的情况很常见。
服务端阅读 06月2日 01:42

.npmrc 怎么配?registry 镜像、私有包和常用配置项详解

.npmrc 是 npm 的配置文件,控制 registry 源、代理、认证信息等。分三层:全局、项目、用户级,优先级从高到低。三层 .npmrc| 文件位置 | 作用范围 | 优先级 ||----------|----------|--------|| 项目根目录/.npmrc | 只对当前项目生效 | 最高 || ~/.npmrc | 对当前用户所有项目生效 | 中 || $PREFIX/etc/npmrc | 全局,对所有用户生效 | 最低 |项目级配置覆盖用户级,用户级覆盖全局。大多数配置写在项目级或用户级就够了。最常用的配置1. 切换 registry(国内开发者必备)registry=https://registry.npmmirror.com默认的 npmjs.org 在国内经常超时。淘宝镜像 npmmirror.com 速度快且稳定。项目级 .npmrc 加这一行,团队成员都能用。2. 私有包的 scoped registry@mycompany:registry=https://npm.mycompany.com@mycompany scope 的包从公司私有 registry 拉取,其他包走公共 registry。不需要配置 VPN 或全局代理。3. 认证 token//npm.mycompany.com/:_authToken=${NPM_TOKEN}用环境变量避免把 token 写死在文件里。CI/CD 里设 NPM_TOKEN 环境变量即可。4. 代理配置proxy=http://127.0.0.1:7890https-proxy=http://127.0.0.1:7890公司内网需要通过代理访问外网时设置。其他实用配置save-exact=true # npm install 默认用精确版本号而非 ^ 前缀package-lock=false # 不生成 package-lock.json(不推荐)audit=false # 关闭 npm audit 检查fund=false # 关闭 npm fund 提示legacy-peer-deps=true # 忽略 peerDependencies 冲突(npm 7+ 经常需要)legacy-peer-deps=true 是 npm 7+ 升级后最常见的配置——npm 7 默认严格检查 peerDeps,很多老包会报冲突。加上这行回到 npm 6 的宽松模式。查看当前生效的配置npm config list # 显示所有配置(包括来源)npm config get registry # 查看某个配置项的值npm config edit # 直接编辑用户级 .npmrc.npmrc 要提交到 Git 吗?项目级 .npmrc:应该提交,确保团队成员用相同的 registry 和配置。但不要包含 auth token——用环境变量代替。用户级 .npmrc:不提交,是个人偏好(代理、token 等)。# .gitignore.npmrc如果项目需要共享 registry 配置,把不含敏感信息的部分提交,token 用 .npmrc + .gitignore 或环境变量处理。
服务端阅读 06月2日 01:41

npm link 怎么用?本地包开发链接和常见坑

npm link 让你在本地开发时把一个包"链接"到另一个项目,改了代码立即生效,不用反复 npm publish + npm install。工作原理npm link 分两步:第一步:在要开发的包目录里执行 npm link,把当前包注册到全局 node_modules。cd ~/projects/my-ui-libnpm link# 把 my-ui-lib 链接到 /usr/local/lib/node_modules/my-ui-lib第二步:在使用这个包的项目里执行 npm link my-ui-lib,创建一个符号链接。cd ~/projects/my-appnpm link my-ui-lib# node_modules/my-ui-lib -> /usr/local/lib/node_modules/my-ui-lib -> ~/projects/my-ui-lib本质就是创建符号链接(symlink)。修改 my-ui-lib 的代码,my-app 里立即生效,不用重新安装。实际使用场景1. 开发组件库你在开发一个 UI 组件库,同时在业务项目里使用它。用 npm link 把组件库链接到业务项目,改组件代码后业务项目自动更新。2. 开发 CLI 工具CLI 工具通常全局安装测试。npm link 在全局注册你的 CLI,修改代码后直接运行最新版本。3. 修复第三方包的 bugfork 一个包,本地修改后 link 到项目里验证修复。确认无误后再提 PR。常见问题1. 多个包互相依赖monorepo 里 A 依赖 B,B 也依赖 A?用 npm link 双向链接:先在 B 目录 npm link,再去 A 目录 npm link B;然后在 A 目录 npm link,再去 B 目录 npm link A。更好的方案:用 workspace(npm 7+ 的 workspaces)替代 npm link,自动处理内部依赖。2. React 双实例问题组件库和业务项目各有一份 React 实例,导致 Hooks 报错 "Invalid hook call"。解决:在业务项目里 link React 到组件库。cd ~/projects/my-app/node_modules/reactnpm linkcd ~/projects/my-ui-libnpm link react这样两个项目共用同一份 React。3. link 后 publish 会把符号链接打包npm link 创建的是符号链接,npm publish 时可能把链接路径打包进去。发布前务必 npm unlink 确保包内容正确。4. npm unlink 清除链接cd ~/projects/my-appnpm unlink my-ui-lib # 在项目里取消链接npm install my-ui-lib # 重新安装正式版本cd ~/projects/my-ui-libnpm unlink # 取消全局注册替代方案npm workspaces:monorepo 场景首选,不需要 link,npm 自动处理内部包的符号链接yalc:把包发布到本地仓库(不是全局 node_modules),比 link 更稳定,不受全局污染影响pnpm link:pnpm 的 link 命令,行为类似但更严格
服务端阅读 06月2日 01:40

npm 常用命令速查:安装、版本管理、脚本和高效技巧

日常开发用到的 npm 命令其实不多,核心就 10 个左右。记住这些够用 90% 的场景,剩下的需要时再查。安装和卸载npm install # 根据 package.json 安装所有依赖npm install react # 安装到 dependenciesnpm install -D jest # 安装到 devDependenciesnpm install -g typescript # 全局安装npm uninstall react # 从 dependencies 移除npm i 是 install 的缩写,npm un 是 uninstall 的缩写。版本管理npm outdated # 查看过时的包npm update # 更新到 semver 允许的最新版本npm install react@18.2.0 # 安装特定版本npm install react@^18 # 18.x.x 最新版本号语义:^18.2.0 允许 18.x.x(主版本不变),~18.2.0 允许 18.2.x(次版本不变)。运行脚本npm run dev # 运行 scripts.devnpm run build # 运行 scripts.buildnpm start # 等价于 npm run startnpm test # 等价于 npm run teststart 和 test 可以省略 run,其他脚本必须加。查看所有可用脚本:npm run。发布npm login # 登录npm publish # 发布当前包npm version patch # +0.0.1 并自动 git commit + tagnpm version minor # +0.1.0npm version major # +1.0.0查看包信息npm view react version # 最新版本号npm view react versions # 所有已发布版本npm ls # 当前项目依赖树npm ls react # 某个包的安装版本高效技巧npx 执行一次性命令:不用全局安装,npx create-react-app my-app 用完即弃。npx 先找项目本地,找不到再下载临时执行。npm ci 代替 npm install:CI/CD 环境必须用 npm ci。根据 package-lock.json 精确安装,比 install 快且版本完全一致。清理缓存:安装报错"Unexpected end of JSON input"时,npm cache clean --force 清理重试。
服务端阅读 06月2日 01:38

Python 装饰器是怎么工作的?@ 语法糖、执行时机和 wraps 详解

装饰器的本质是一个接收函数作为参数并返回新函数的高阶函数。@decorator 语法糖等价于 func = decorator(func)。理解装饰器的关键:它只是函数替换,在定义时执行,不是调用时。装饰器做了什么def log(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper@logdef hello(name): print(f"Hello, {name}")# 等价于hello = log(hello)@log 发生在函数定义时,不是调用时。Python 解释器看到 @log 后,把 hello 传给 log(),返回的 wrapper 替换掉原来的 hello。之后所有对 hello() 的调用实际上调的是 wrapper()。执行时机def log(func): print("装饰器执行了") # 模块加载时就执行 def wrapper(*args, **kwargs): print("wrapper 执行了") # 每次调用函数时执行 return func(*args, **kwargs) return wrapper@logdef hello(): print("hello")# 输出: 装饰器执行了(定义时立即执行)hello()# 输出: wrapper 执行了 / hello装饰器外层的代码在模块加载时执行(一次),wrapper 内的代码在每次函数调用时执行。多个装饰器的叠加@a@bdef f(): pass# 等价于f = a(b(f))从下到上装饰。调用 f() 时,执行顺序是 a -> b -> f -> b 的后处理 -> a 的后处理。带参数的装饰器需要三层嵌套——最外层接收装饰器参数,中间层接收被装饰函数,最内层是 wrapper:def retry(max_attempts=3): def decorator(func): def wrapper(*args, **kwargs): for i in range(max_attempts): try: return func(*args, **kwargs) except Exception: if i == max_attempts - 1: raise return wrapper return decorator@retry(max_attempts=5)def call_api(): ...# 等价于call_api = retry(max_attempts=5)(call_api)@retry(5) 先调用 retry(5) 返回 decorator,再 decorator(call_api) 返回 wrapper。三层嵌套是固定模式。functools.wraps装饰器替换了原函数,导致 __name__、__doc__ 等元信息丢失。@wraps(func) 复制原函数的元信息到 wrapper:from functools import wrapsdef log(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper@logdef hello(): passprint(hello.__name__) # hello(没有 @wraps 会是 wrapper)装饰器 vs 闭包装饰器是基于闭包的。wrapper 闭包了 func 变量——每次调用 wrapper 时都能访问到被装饰的原始函数。装饰器就是"创建闭包的工厂函数"。追问装饰器能装饰类吗?能。装饰器接收的参数不一定是函数,也可以是类:def add_repr(cls): def __repr__(self): return f"{cls.__name__}({self.__dict__})" cls.__repr__ = __repr__ return cls@add_reprclass User: def __init__(self, name): self.name = namedataclass 的 @dataclass 就是类装饰器。装饰器有性能开销吗?有一层函数调用的开销,通常可以忽略。但在极高性能场景(百万次/秒调用),装饰器的额外调用栈可能成为瓶颈。可以用 functools.lru_cache 缓存结果避免重复执行。
服务端阅读 06月2日 01:37

Python GIL 是什么?为什么多线程不能利用多核?怎么绕过?

GIL(Global Interpreter Lock)是 CPython 的一把全局互斥锁,同一时刻只允许一个线程执行 Python 字节码。这意味着 Python 多线程无法利用多核 CPU 做计算密集型任务。但 IO 密集型任务不受影响——线程等待 IO 时会释放 GIL。GIL 为什么存在CPython 的内存管理(引用计数)不是线程安全的。如果多个线程同时修改 ob_refcnt,可能导致内存泄漏或提前释放。GIL 是最简单的解决方案——一个线程执行 Python 代码时,其他线程不能运行。为什么不去掉?Python 核心开发者试过多次,去掉 GIL 会导致单线程性能下降 10-30%(因为要加细粒度锁替代全局锁),C 扩展也需要大量改写。社区不愿意接受这个代价。Python 3.13 引入了实验性的 free-threaded 模式(PEP 703),允许禁用 GIL,但目前还是实验阶段,性能和兼容性都不成熟。GIL 的影响范围| 任务类型 | 多线程表现 | 原因 ||----------|-----------|------|| CPU 密集 | 比单线程还慢 | 线程争抢 GIL,切换开销大 || IO 密集 | 有效加速 | 等 IO 时释放 GIL,其他线程可运行 || 混合型 | 部分有效 | 计算部分受 GIL 限制 |CPU 密集比单线程还慢是因为 GIL 切换本身有开销,多线程反而增加了竞争。绕过 GIL 的三种方式1. 多进程(最常用)from multiprocessing import Pooldef heavy_compute(n): return sum(i * i for i in range(n))with Pool(4) as p: results = p.map(heavy_compute, [10**7] * 4) # 4 个进程并行每个进程有独立的 GIL,真正并行。缺点:进程间通信成本高(数据要序列化),启动慢。2. C 扩展释放 GIL在 C 扩展中做计算密集型操作时,可以手动释放 GIL:Py_BEGIN_ALLOW_THREADS// 这里做纯 C 计算,不操作 Python 对象result = heavy_computation(data);Py_END_ALLOW_THREADSNumPy、Pandas、hashlib 等库在底层 C 代码中释放了 GIL,所以它们在多线程中可以并行运行。3. 用 asyncio 替代多线程IO 密集型任务不需要多线程,用协程就够了:import asyncioimport aiohttpasync def fetch(url): async with aiohttp.ClientSession() as session: async with session.get(url) as resp: return await resp.text()async def main(): tasks = [fetch(url) for url in urls] results = await asyncio.gather(*tasks)单线程 + 事件循环,没有 GIL 问题,也没有线程切换开销。GIL 的释放时机CPython 在两种情况下释放 GIL:IO 操作:网络请求、文件读写、time.sleep() 等C 扩展主动释放:NumPy 运算、hashlib 哈希等纯 Python 代码(循环、计算、字符串操作)永远不会释放 GIL。追问Python 3.13 的 no-GIL 模式能用吗?实验阶段,不推荐生产使用。性能比有 GIL 模式慢约 10-15%,很多 C 扩展还不兼容。等 Python 3.14/3.15 稳定后再考虑。多线程在 Python 里完全没用吗?不是。IO 密集型场景(网络爬虫、API 调用、数据库查询)多线程比单线程快得多。只是 CPU 密集型场景多线程没有加速效果。
服务端阅读 06月2日 01:36

Python 迭代器和生成器有什么区别?yield 和迭代器协议详解

迭代器是实现了 __iter__ 和 __next__ 方法的对象,生成器是用 yield 关键字自动创建的迭代器。生成器是迭代器的子集——所有生成器都是迭代器,但迭代器不一定是生成器。核心区别:生成器更简洁,且天然支持惰性求值。迭代器协议迭代器必须实现两个方法:__iter__:返回 self(让迭代器本身也可迭代)__next__:返回下一个值,没有值时抛出 StopIterationclass Countdown: def __init__(self, start): self.current = start def __iter__(self): return self def __next__(self): if self.current <= 0: raise StopIteration self.current -= 1 return self.current + 1for n in Countdown(3): print(n) # 3, 2, 1手动实现迭代器要写 __iter__、__next__、维护状态、处理 StopIteration。代码量多,容易写错。生成器:迭代器的语法糖生成器用 yield 关键字,Python 自动实现迭代器协议:def countdown(start): current = start while current > 0: yield current current -= 1for n in countdown(3): print(n) # 3, 2, 1调用 countdown(3) 不会执行函数体,而是返回一个生成器对象。每次 next() 执行到 yield 暂停并返回值,下次 next() 从暂停处继续。3 行代码 vs 10 行代码,效果完全一样。这就是为什么 Python 社区几乎总是用生成器而不是手写迭代器。生成器表达式类似列表推导式,但用圆括号,惰性求值:# 列表推导式 — 一次性生成所有数据squares = [x**2 for x in range(1000000)] # 占用大量内存# 生成器表达式 — 按需生成squares = (x**2 for x in range(1000000)) # 几乎不占内存# 在 sum/max/min 等函数里直接用total = sum(x**2 for x in range(1000000))处理大数据时,生成器表达式是列表推导式的直接替代。yield from:委托给子生成器yield from 把迭代委托给另一个生成器,避免手写 for 循环:def flatten(nested): for item in nested: if isinstance(item, list): yield from flatten(item) # 递归展开 else: yield itemlist(flatten([1, [2, [3, 4]], 5])) # [1, 2, 3, 4, 5]yield from 不只是语法糖——它还正确处理了 send()、throw()、close() 等生成器方法的传递。生成器做协程yield 不只能返回值,还能接收值(通过 send()),这让生成器可以用来实现协程:def accumulator(): total = 0 while True: value = yield total if value is None: break total += valuegen = accumulator()next(gen) # 启动生成器,返回 0gen.send(10) # 返回 10gen.send(20) # 返回 30Python 3.5+ 推荐用 async/await 替代这种用法,但理解 yield 的双向通信有助于理解协程原理。追问迭代器只能遍历一次吗?是的。迭代器是有状态的,遍历完就空了。要重新遍历,需要创建新的迭代器。可迭代对象(如列表)每次调用 iter() 都返回新的迭代器。生成器的内存优势有多大?读 10GB 日志文件,for line in open("log.txt") 只占几 KB 内存(每次读一行)。readlines() 会把整个文件加载到内存。差距在数据量大时非常显著。