乐闻世界logo
搜索文章和话题

面试题手册

如何通过 .npmrc 文件配置 npm?常用配置项有哪些?

npm 提供了强大的配置系统,允许开发者通过 .npmrc 文件和命令行选项自定义 npm 的行为。理解 npm 配置对于优化开发流程和解决常见问题非常重要。.npmrc 文件配置文件位置npm 配置文件可以存在于多个位置,按优先级从高到低:项目级:项目根目录/.npmrc用户级:~/.npmrc全局级:$PREFIX/etc/npmrc内置级:npm 内置默认配置项目级配置在项目根目录创建 .npmrc 文件:# 项目级 .npmrcregistry=https://registry.npmmirror.comsave-exact=trueengine-strict=true优势:项目特定的配置可以提交到版本控制团队成员共享配置用户级配置位于用户主目录的 .npmrc 文件:# 查看用户级配置文件位置npm config get userconfig常用配置:# 用户级 .npmrcregistry=https://registry.npmmirror.comprefix=/usr/localcache=/Users/username/.npm-cache全局级配置位于 npm 安装目录的配置文件:# 查看全局配置文件位置npm config get globalconfig常用配置选项Registry 配置# 设置 registryregistry=https://registry.npmjs.org# 使用淘宝镜像registry=https://registry.npmmirror.com# 使用私有 registryregistry=https://registry.yourcompany.com# 为特定 scope 设置 registry@yourcompany:registry=https://registry.yourcompany.com认证配置# 使用 token 认证//registry.npmjs.org/:_authToken=YOUR_TOKEN# 使用用户名密码//registry.yourcompany.com/:username=your-username//registry.yourcompany.com/:_password=YOUR_PASSWORD# 从环境变量读取//registry.npmjs.org/:_authToken=${NPM_TOKEN}缓存配置# 设置缓存目录cache=/path/to/cache# 设置缓存最大大小(字节)cache-max=10737418240# 设置缓存最小保留时间(秒)cache-min=3600安装配置# 精确安装版本save-exact=true# 保存前缀save-prefix=^# 保存开发依赖save-dev=false# 保存可选依赖save-optional=false# 保存同伴依赖save-peer=false# 使用严格模式engine-strict=true# 忽略脚本ignore-scripts=false网络配置# 设置代理https-proxy=http://proxy.example.com:8080http-proxy=http://proxy.example.com:8080# 设置超时时间(毫秒)fetch-timeout=60000# 设置重试次数fetch-retries=3# 设置重试最小超时(毫秒)fetch-retry-mintimeout=10000# 设置重试最大超时(毫秒)fetch-retry-maxtimeout=60000# 设置最大并发连接数maxsockets=50# 设置网络请求并发数network-concurrency=16安全配置# 启用严格 SSLstrict-ssl=true# 启用审计audit=true# 设置审计级别audit-level=moderate# 设置 CA 证书cafile=/path/to/ca.pem# 设置证书cert=/path/to/cert.pemkey=/path/to/key.pem工作区配置# 启用工作区workspaces=true# 设置工作区目录workspace=/path/to/workspace其他配置# 设置前缀prefix=/usr/local# 设置日志级别loglevel=info# 设置颜色输出color=true# 设置进度条progress=true# 设置 Unicode 字符unicode=true# 设置包装器shell=bash# 设置编辑器editor=vim# 设置浏览器browser=google-chrome命令行配置查看配置# 查看所有配置npm config list# 查看特定配置npm config get registry# 查看用户配置npm config list --user# 查看全局配置npm config list --global# 查看项目配置npm config list --project设置配置# 设置配置npm config set registry https://registry.npmmirror.com# 设置全局配置npm config set registry https://registry.npmmirror.com --global# 删除配置npm config delete registry# 编辑配置文件npm config edit环境变量npm 支持通过环境变量配置:# 设置 registryexport npm_config_registry=https://registry.npmmirror.com# 设置缓存目录export npm_config_cache=/path/to/cache# 设置代理export npm_config_https-proxy=http://proxy.example.com:8080# 设置 tokenexport npm_config__authToken=YOUR_TOKEN高级配置Scope 配置为不同的 scope 设置不同的配置:# 公开包使用官方 registryregistry=https://registry.npmjs.org# 私有包使用私有 registry@yourcompany:registry=https://registry.yourcompany.com# 为特定 scope 设置认证@yourcompany:registry=https://registry.yourcompany.com//registry.yourcompany.com/:_authToken=${YOURCOMPANY_TOKEN}条件配置根据条件选择不同的配置:# 开发环境$npm_config_env=developmentregistry=https://registry.npmmirror.com# 生产环境$npm_config_env=productionregistry=https://registry.npmjs.org脚本配置在 npm scripts 中使用配置:{ "scripts": { "install:dev": "npm install --registry=https://registry.npmmirror.com", "install:prod": "npm install --registry=https://registry.npmjs.org" }}配置优先级配置优先级从高到低:命令行选项环境变量项目级 .npmrc用户级 .npmrc全局级 npmrcnpm 内置默认配置示例:# 命令行选项优先级最高npm install --registry=https://custom-registry.com# 即使 .npmrc 中设置了其他 registry# 命令行选项会覆盖最佳实践1. 项目级配置# 项目 .npmrcregistry=https://registry.npmmirror.comsave-exact=trueengine-strict=trueaudit=trueaudit-level=moderate2. 用户级配置# 用户 .npmrcregistry=https://registry.npmmirror.comcache=~/.npm-cacheprefix=~/.npm-globalloglevel=warn3. CI/CD 配置# GitHub Actions- name: Configure npm run: | echo "registry=https://registry.npmjs.org" > .npmrc echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" >> .npmrc env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }}4. 安全配置# 安全配置strict-ssl=trueaudit=trueaudit-level=highignore-scripts=false5. 性能配置# 性能配置cache=~/.npm-cachecache-max=10737418240maxsockets=50network-concurrency=16fetch-retries=3常见问题1. 配置不生效# 检查配置优先级npm config list# 检查特定配置npm config get registry# 清除缓存npm cache clean --force2. 认证失败# 检查 tokennpm config get _authToken# 重新登录npm login# 检查 registrynpm config get registry3. 网络问题# 检查代理配置npm config get https-proxy# 检查超时设置npm config get fetch-timeout# 使用镜像npm config set registry https://registry.npmmirror.com4. 缓存问题# 清理缓存npm cache clean --force# 验证缓存npm cache verify# 检查缓存位置npm config get cache配置示例开发环境# 开发环境 .npmrcregistry=https://registry.npmmirror.comsave-exact=falsesave-prefix=^engine-strict=falseaudit=falseloglevel=info生产环境# 生产环境 .npmrcregistry=https://registry.npmjs.orgsave-exact=trueengine-strict=trueaudit=trueaudit-level=highstrict-ssl=trueloglevel=warn企业环境# 企业环境 .npmrcregistry=https://registry.yourcompany.com@yourcompany:registry=https://registry.yourcompany.com//registry.yourcompany.com/:_authToken=${NPM_TOKEN}strict-ssl=trueaudit=trueaudit-level=moderate理解 npm 配置系统可以帮助开发者优化开发流程、解决常见问题,并确保团队使用一致的配置。
阅读 0·2月17日 23:22

npm依赖类型有哪些?如何配置package.json?

npm 支持多种依赖类型,每种类型在 package.json 中有不同的作用和使用场景。依赖类型1. dependencies(生产依赖)应用运行时必需的依赖,部署到生产环境时需要安装。{ "dependencies": { "express": "^4.18.0", "lodash": "^4.17.21", "axios": "^1.0.0" }}安装命令:npm install <package>npm install <package> --save-prodnpm install <package> -P使用场景:Web 框架(Express、React、Vue)工具库(lodash、moment)API 客户端(axios、fetch)2. devDependencies(开发依赖)仅在开发时需要的依赖,生产环境不需要。{ "devDependencies": { "jest": "^29.0.0", "eslint": "^8.0.0", "webpack": "^5.0.0", "nodemon": "^3.0.0" }}安装命令:npm install <package> --save-devnpm install <package> -D使用场景:测试框架(Jest、Mocha)代码检查工具(ESLint、Prettier)构建工具(Webpack、Rollup)开发服务器(nodemon)3. peerDependencies(同伴依赖)期望宿主项目提供的依赖,不会自动安装。{ "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" }}使用场景:插件或库需要宿主提供核心库避免重复安装相同版本的包确保与宿主项目的兼容性示例:React 组件库{ "name": "my-react-components", "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" }}4. optionalDependencies(可选依赖)安装失败不会中断安装过程的依赖。{ "optionalDependencies": { "fsevents": "^2.3.0", "chokidar": "^3.5.0" }}安装命令:npm install <package> --save-optionalnpm install <package> -O使用场景:平台特定的包(如 macOS 的 fsevents)可选功能的增强包不影响核心功能的依赖5. bundledDependencies(打包依赖)打包时包含的依赖,发布时会一起打包。{ "bundledDependencies": [ "my-internal-lib" ]}注意:必须在 dependencies 或 devDependencies 中声明不能是可选依赖会增加包的大小package.json 核心字段基本信息{ "name": "my-project", "version": "1.0.0", "description": "A sample project", "keywords": ["npm", "javascript", "node"], "author": "Your Name <email@example.com>", "license": "MIT", "homepage": "https://github.com/user/repo#readme", "repository": { "type": "git", "url": "https://github.com/user/repo.git" }}入口和配置{ "main": "index.js", "module": "dist/index.esm.js", "types": "dist/index.d.ts", "bin": { "my-cli": "./bin/cli.js" }, "files": [ "dist", "README.md", "LICENSE" ], "directories": { "lib": "lib", "test": "test" }}配置字段{ "config": { "port": 3000, "env": "development" }, "engines": { "node": ">=14.0.0", "npm": ">=6.0.0" }, "os": ["!win32"], "cpu": ["x64", "arm64"]}私有和发布配置{ "private": true, "publishConfig": { "registry": "https://registry.npmjs.org", "access": "public" }}工作区(Workspaces)npm 7+ 支持工作区,用于管理 monorepo:{ "name": "my-monorepo", "version": "1.0.0", "workspaces": [ "packages/*" ], "scripts": { "install": "npm install -ws", "build": "npm run build -ws" }}目录结构:my-monorepo/├── package.json├── packages/│ ├── package-a/│ │ └── package.json│ └── package-b/│ └── package.json依赖解析策略npm 使用嵌套依赖结构(node_modules):node_modules/├── package-a/│ └── node_modules/│ └── package-c/└── package-b/ └── node_modules/ └── package-c/解析规则:首先查找当前包的 node_modules然后查找父包的 node_modules递归向上查找直到根目录常见问题1. 依赖冲突当两个包需要不同版本的同一依赖时:{ "dependencies": { "package-a": "^1.0.0", // 需要 lodash@^4.0.0 "package-b": "^2.0.0" // 需要 lodash@^3.0.0 }}npm 会安装两个版本的 lodash。2. 依赖提升npm 会尝试将依赖提升到更高级别以减少重复:node_modules/├── lodash/ // 提升到顶层├── package-a/└── package-b/3. 依赖锁定使用 package-lock.json 确保依赖一致性:{ "lockfileVersion": 2, "packages": { "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-..." } }}最佳实践明确区分依赖类型:正确使用 dependencies 和 devDependencies使用 peerDependencies:插件库应使用 peerDependencies最小化依赖:只安装必要的包定期更新:使用 npm outdated 和 npm audit锁定版本:提交 package-lock.json 到版本控制使用工作区:大型项目使用 workspaces 管理 monorepo文档化依赖:在 README 中说明主要依赖及其用途常用命令# 安装生产依赖npm install <package># 安装开发依赖npm install <package> -D# 安装所有依赖npm install# 只安装生产依赖npm install --production# 检查依赖更新npm outdated# 更新依赖npm update# 检查安全漏洞npm audit# 自动修复安全漏洞npm audit fix理解 npm 依赖类型和 package.json 配置对于管理项目依赖、优化包大小和确保项目稳定性至关重要。
阅读 0·2月17日 23:21

npm workspaces如何实现Monorepo管理?有哪些最佳实践?

npm 的 monorepo 支持通过工作区(workspaces)功能实现,允许在单个仓库中管理多个相关的包。这对于大型项目和团队协作非常有用。工作区(Workspaces)基础什么是工作区工作区允许你在单个 npm 项目中管理多个包,这些包可以相互依赖,并且可以共享依赖。基本配置在根目录的 package.json 中配置工作区:{ "name": "my-monorepo", "version": "1.0.0", "private": true, "workspaces": [ "packages/*" ], "scripts": { "install": "npm install -ws", "build": "npm run build -ws", "test": "npm test -ws", "clean": "npm run clean -ws" }}目录结构my-monorepo/├── package.json├── packages/│ ├── shared/│ │ ├── package.json│ │ └── index.js│ ├── app/│ │ ├── package.json│ │ └── index.js│ └── utils/│ ├── package.json│ └── index.js├── node_modules/│ ├── shared/ # 符号链接到 packages/shared│ ├── app/ # 符号链接到 packages/app│ └── utils/ # 符号链接到 packages/utils└── package-lock.json工作区配置选项1. 数组格式{ "workspaces": [ "packages/*", "apps/*" ]}2. 对象格式{ "workspaces": { "packages": [ "packages/*" ] }}3. 混合格式{ "workspaces": { "packages": [ "packages/*", "apps/*" ], "nohoist": [ "**/lodash" ] }}工作区命令安装依赖# 在所有工作区中安装依赖npm install -wsnpm install --workspaces# 在特定工作区中安装依赖npm install lodash --workspace=packages/appnpm install lodash -w packages/app# 安装工作区依赖npm install ../shared --workspace=packages/app运行脚本# 在所有工作区中运行脚本npm run build -wsnpm run build --workspaces# 在特定工作区中运行脚本npm run build --workspace=packages/appnpm run build -w packages/app# 在多个工作区中运行脚本npm run build -w packages/app -w packages/utils更新依赖# 更新所有工作区的依赖npm update -ws# 更新特定工作区的依赖npm update --workspace=packages/app删除依赖# 从所有工作区删除依赖npm uninstall lodash -ws# 从特定工作区删除依赖npm uninstall lodash --workspace=packages/app工作区间依赖添加工作区依赖# 在 packages/app 中添加对 packages/shared 的依赖cd packages/appnpm install ../shared# 或者使用工作区名称npm install shared --workspace=packages/apppackage.json 示例packages/app/package.json:{ "name": "app", "version": "1.0.0", "dependencies": { "shared": "*", "lodash": "^4.17.21" }}packages/shared/package.json:{ "name": "shared", "version": "1.0.0", "dependencies": { "lodash": "^4.17.21" }}高级配置1. nohoist 配置防止某些包被提升到根目录:{ "workspaces": { "packages": [ "packages/*" ], "nohoist": [ "**/lodash", "**/react", "**/webpack" ] }}2. 私有包确保工作区包不会被发布:{ "name": "my-monorepo", "private": true, "workspaces": [ "packages/*" ]}3. 共享依赖在根目录定义共享依赖:根 package.json:{ "name": "my-monorepo", "version": "1.0.0", "private": true, "workspaces": [ "packages/*" ], "devDependencies": { "jest": "^29.0.0", "eslint": "^8.0.0", "typescript": "^5.0.0" }}实际示例React + Express Monorepomy-monorepo/├── package.json├── packages/│ ├── web/│ │ ├── package.json│ │ └── src/│ ├── api/│ │ ├── package.json│ │ └── src/│ └── shared/│ ├── package.json│ └── src/根 package.json:{ "name": "my-monorepo", "version": "1.0.0", "private": true, "workspaces": [ "packages/*" ], "scripts": { "install": "npm install -ws", "build": "npm run build -ws", "dev": "npm run dev -ws", "test": "npm test -ws", "lint": "npm run lint -ws" }}packages/web/package.json:{ "name": "web", "version": "1.0.0", "dependencies": { "react": "^18.0.0", "shared": "*" }}packages/api/package.json:{ "name": "api", "version": "1.0.0", "dependencies": { "express": "^4.18.0", "shared": "*" }}packages/shared/package.json:{ "name": "shared", "version": "1.0.0", "dependencies": { "lodash": "^4.17.21" }}最佳实践1. 统一版本管理{ "scripts": { "version": "npm version --workspaces", "publish": "npm publish --workspaces" }}2. 共享配置{ "devDependencies": { "jest": "^29.0.0", "eslint": "^8.0.0", "prettier": "^3.0.0" }}3. 统一脚本{ "scripts": { "build": "npm run build -ws", "test": "npm test -ws", "lint": "npm run lint -ws", "clean": "npm run clean -ws" }}4. 使用 Lerna(可选)Lerna 是一个专门用于管理 JavaScript monorepo 的工具:# 安装 Lernanpm install -g lerna# 初始化 Lernalerna init# 运行命令lerna run buildlerna publish常见问题1. 依赖冲突# 检查依赖npm ls -ws# 使用 overrides 解决冲突npm config set overrides '{"package": "1.2.3"}'2. 符号链接问题# 检查符号链接ls -la node_modules/# 重新安装rm -rf node_modules package-lock.jsonnpm install -ws3. TypeScript 路径问题tsconfig.json:{ "compilerOptions": { "baseUrl": ".", "paths": { "shared": ["packages/shared/src"], "app": ["packages/app/src"] } }}4. 构建顺序问题# 按顺序构建npm run build --workspace=packages/sharednpm run build --workspace=packages/appnpm run build --workspace=packages/api与其他工具集成1. TypeScript{ "references": [ { "path": "./packages/shared" }, { "path": "./packages/app" } ]}2. Jest{ "projects": [ "<rootDir>/packages/*/jest.config.js" ]}3. ESLint{ "overrides": [ { "files": ["packages/*/src/**/*.ts"], "extends": ["@mycompany/eslint-config"] } ]}4. Webpackmodule.exports = { resolve: { alias: { shared: path.resolve(__dirname, 'packages/shared/src') } }};性能优化1. 并行构建# 并行运行脚本npm run build -ws --parallel2. 增量构建# 只构建更改的包npm run build -ws --if-present3. 缓存# 使用缓存npm install -ws --prefer-offline监控和调试1. 查看工作区信息# 列出所有工作区npm workspaces info# 查看特定工作区npm workspaces info --workspace=packages/app2. 调试依赖# 查看依赖树npm ls -ws# 查看特定包的依赖npm ls shared -ws3. 查看安装日志# 详细日志npm install -ws --verbose# 调试日志npm install -ws --loglevel=verbosenpm 工作区是管理 monorepo 的强大工具,可以显著提高大型项目的开发效率和可维护性。
阅读 0·2月17日 23:20

Next.js 性能优化的最佳实践是什么?

Next.js 的性能优化是构建高性能 Web 应用的关键。Next.js 提供了多种内置优化功能,同时也需要开发者掌握各种优化技巧。核心性能优化策略1. 图片优化// 使用 next/image 组件import Image from 'next/image';// 基础用法<Image src="/hero.jpg" alt="Hero image" width={1920} height={1080} priority // 首屏图片使用 priority/>// 响应式图片<Image src="/hero.jpg" alt="Hero image" fill sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" placeholder="blur" blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."/>// 远程图片配置// next.config.jsmodule.exports = { images: { remotePatterns: [ { protocol: 'https', hostname: 'example.com', port: '', pathname: '/images/**', }, ], deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], formats: ['image/webp', 'image/avif'], },};// 动态图片async function getImages() { const images = await fetch('https://api.example.com/images').then(r => r.json()); return images;}export default async function Gallery() { const images = await getImages(); return ( <div className="grid"> {images.map((image) => ( <Image key={image.id} src={image.url} alt={image.alt} width={image.width} height={image.height} loading="lazy" // 非首屏图片使用 lazy /> ))} </div> );}2. 字体优化// 使用 next/font 优化字体加载import { Inter, Roboto } from 'next/font/google';const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-inter', preload: true,});const roboto = Roboto({ weight: ['400', '500', '700'], subsets: ['latin'], display: 'swap', variable: '--font-roboto',});// 在 layout.js 中应用export default function RootLayout({ children }) { return ( <html lang="en" className={`${inter.variable} ${roboto.variable}`}> <body className={inter.className}>{children}</body> </html> );}// 使用本地字体import localFont from 'next/font/local';const myFont = localFont({ src: [ { path: './fonts/MyFont-Regular.woff2', weight: '400', style: 'normal', }, { path: './fonts/MyFont-Bold.woff2', weight: '700', style: 'normal', }, ], display: 'swap', variable: '--font-my-font',});3. 代码分割和懒加载// 动态导入组件import dynamic from 'next/dynamic';// 基础动态导入const DynamicComponent = dynamic(() => import('../components/HeavyComponent'));// 带加载状态的动态导入const DynamicComponentWithLoading = dynamic( () => import('../components/HeavyComponent'), { loading: () => <p>Loading...</p>, ssr: false, // 禁用服务端渲染 });// 条件加载const ConditionalComponent = dynamic( () => import('../components/ConditionalComponent'), { ssr: false });export default function Page() { const [showComponent, setShowComponent] = useState(false); return ( <div> <button onClick={() => setShowComponent(true)}> Load Component </button> {showComponent && <ConditionalComponent />} </div> );}// 路由级别的代码分割// Next.js 自动为每个路由创建独立的代码块// app/page.js -> page.js// app/about/page.js -> about/page.js4. 数据获取优化// 使用 fetch API 的缓存选项async function getData() { // 强制缓存(默认) const res1 = await fetch('https://api.example.com/data', { cache: 'force-cache', }); // 不缓存 const res2 = await fetch('https://api.example.com/data', { cache: 'no-store', }); // 重新验证缓存 const res3 = await fetch('https://api.example.com/data', { next: { revalidate: 3600 }, // 1小时后重新验证 }); // 按需重新验证 const res4 = await fetch('https://api.example.com/data', { next: { tags: ['posts'] }, }); return res.json();}// 使用 React Query 进行客户端数据缓存'use client';import { useQuery } from '@tanstack/react-query';function usePosts() { return useQuery({ queryKey: ['posts'], queryFn: () => fetch('/api/posts').then(r => r.json()), staleTime: 5 * 60 * 1000, // 5分钟内数据视为新鲜 cacheTime: 10 * 60 * 1000, // 10分钟后清除缓存 });}// 使用 SWR 进行数据获取'use client';import useSWR from 'swr';const fetcher = (url) => fetch(url).then((r) => r.json());function usePosts() { const { data, error, isLoading } = useSWR('/api/posts', fetcher, { revalidateOnFocus: false, revalidateOnReconnect: false, dedupingInterval: 60000, // 1分钟内去重请求 }); return { data, error, isLoading };}5. 缓存策略// 使用 Redis 进行缓存import { Redis } from '@upstash/redis';const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL, token: process.env.UPSTASH_REDIS_REST_TOKEN,});async function getCachedData(key, fetcher, ttl = 3600) { const cached = await redis.get(key); if (cached) { return JSON.parse(cached); } const data = await fetcher(); await redis.set(key, JSON.stringify(data), { ex: ttl }); return data;}// 使用示例async function getPosts() { return getCachedData( 'posts', () => fetch('https://api.example.com/posts').then(r => r.json()), 3600 );}// 使用 Vercel KV 缓存import { kv } from '@vercel/kv';async function getCachedPosts() { const cached = await kv.get('posts'); if (cached) { return cached; } const posts = await fetch('https://api.example.com/posts').then(r => r.json()); await kv.set('posts', posts, { ex: 3600 }); return posts;}6. 预加载和预取// 预加载页面import Link from 'next/link';export default function Navigation() { return ( <nav> <Link href="/about" prefetch={true}> About </Link> <Link href="/contact" prefetch={true}> Contact </Link> </nav> );}// 预加载资源export default function Page() { return ( <> <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossOrigin="" /> <link rel="preload" href="/hero.jpg" as="image" /> <link rel="preconnect" href="https://api.example.com" /> </> );}// 使用 prefetch 进行数据预取'use client';import { useEffect } from 'react';import { useRouter } from 'next/navigation';export default function ProductCard({ product }) { const router = useRouter(); const handleMouseEnter = () => { router.prefetch(`/products/${product.id}`); }; return ( <div onMouseEnter={handleMouseEnter}> <h3>{product.name}</h3> <Link href={`/products/${product.id}`}> View Details </Link> </div> );}7. 构建优化// next.config.jsmodule.exports = { // 压缩输出 compress: true, // 生产环境优化 productionBrowserSourceMaps: false, // SWC 压缩器(比 Terser 更快) swcMinify: true, // 实验性功能 experimental: { // 优化 CSS optimizeCss: true, // 优化包导入 optimizePackageImports: ['lucide-react', '@radix-ui/react-icons'], }, // Webpack 优化 webpack: (config, { isServer }) => { if (!isServer) { config.resolve.fallback = { ...config.resolve.fallback, fs: false, net: false, tls: false, }; } return config; }, // 模块联邦(微前端) webpack: (config) => { config.optimization = { ...config.optimization, splitChunks: { chunks: 'all', cacheGroups: { default: false, vendors: false, vendor: { name: 'vendor', chunks: 'all', test: /node_modules/, priority: 20, }, common: { name: 'common', minChunks: 2, chunks: 'all', priority: 10, reuseExistingChunk: true, enforce: true, }, }, }, }; return config; },};8. 性能监控// 使用 Web Vitals 监控// app/layout.js'use client';import { useReportWebVitals } from 'next/web-vitals';export function WebVitals() { useReportWebVitals((metric) => { // 发送到分析服务 fetch('/api/analytics', { method: 'POST', body: JSON.stringify(metric), }); }); return null;}// 自定义性能监控'use client';import { useEffect } from 'react';export function PerformanceMonitor() { useEffect(() => { if (typeof window !== 'undefined' && 'PerformanceObserver' in window) { // 监控 LCP const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log('LCP:', entry.startTime); } }); observer.observe({ entryTypes: ['largest-contentful-paint'] }); // 监控 FID const fidObserver = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log('FID:', entry.processingStart - entry.startTime); } }); fidObserver.observe({ entryTypes: ['first-input'] }); // 监控 CLS const clsObserver = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log('CLS:', entry.value); } }); clsObserver.observe({ entryTypes: ['layout-shift'] }); } }, []); return null;}9. 服务端优化// 使用 Edge Runtimeexport const runtime = 'edge';export default async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 60 }, }).then(r => r.json()); return <div>{data.title}</div>;}// 使用 ISR(增量静态再生)export const revalidate = 3600; // 1小时export default async function BlogPage() { const posts = await getPosts(); return ( <div> {posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> <p>{post.excerpt}</p> </article> ))} </div> );}// 使用 On-Demand Revalidationimport { revalidatePath } from 'next/cache';export async function POST(request) { const res = await request.json(); await updatePost(res.id, res.data); // 重新验证特定页面 revalidatePath(`/blog/${res.id}`); revalidatePath('/blog'); return Response.json({ success: true });}10. 客户端优化// 使用 React.memo 避免不必要的重新渲染'use client';import { memo } from 'react';const ExpensiveComponent = memo(({ data }) => { return <div>{/* 复杂渲染逻辑 */}</div>;});// 使用 useMemo 缓存计算结果'use client';import { useMemo } from 'react';function Component({ items }) { const sortedItems = useMemo(() => { return items.sort((a, b) => a.value - b.value); }, [items]); return <div>{/* 使用 sortedItems */}</div>;}// 使用 useCallback 缓存函数'use client';import { useCallback } from 'react';function ParentComponent() { const handleClick = useCallback(() => { console.log('Clicked'); }, []); return <ChildComponent onClick={handleClick} />;}// 虚拟化长列表'use client';import { useVirtualizer } from '@tanstack/react-virtual';function VirtualList({ items }) { const parentRef = useRef(); const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, }); return ( <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}> <div style={{ height: `${virtualizer.getTotalSize()}px` }}> {virtualizer.getVirtualItems().map((virtualItem) => ( <div key={virtualItem.key} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${virtualItem.size}px`, transform: `translateY(${virtualItem.start}px)`, }} > {items[virtualItem.index]} </div> ))} </div> </div> );}性能检查清单Core Web VitalsLCP (Largest Contentful Paint): < 2.5sFID (First Input Delay): < 100msCLS (Cumulative Layout Shift): < 0.1优化检查项✅ 使用 next/image 优化图片✅ 使用 next/font 优化字体加载✅ 实现代码分割和懒加载✅ 优化数据获取和缓存策略✅ 使用 ISR 和 On-Demand Revalidation✅ 实现预加载和预取✅ 优化构建配置✅ 监控 Web Vitals✅ 使用 Edge Runtime✅ 客户端性能优化(memo、useMemo、useCallback)通过综合运用这些优化策略,可以显著提升 Next.js 应用的性能表现。
阅读 0·2月17日 23:18

Next.js 应用的安全最佳实践是什么?

Next.js 应用的安全性是生产环境中不可忽视的重要方面。全面的安全防护措施可以保护应用免受各种网络攻击和数据泄露。核心安全概念1. 安全威胁类型XSS(跨站脚本攻击):恶意脚本注入CSRF(跨站请求伪造):伪造用户请求SQL 注入:恶意 SQL 代码执行SSRF(服务端请求伪造):伪造服务端请求点击劫持:UI 伪装攻击中间人攻击:通信拦截安全配置和防护1. 环境变量管理// .env.local(开发环境)DATABASE_URL=postgresql://localhost:5432/myappNEXTAUTH_SECRET=dev-secret-key-change-in-productionNEXTAUTH_URL=http://localhost:3000API_KEY=dev-api-key// .env.production(生产环境)DATABASE_URL=postgresql://prod-db-server:5432/myappNEXTAUTH_SECRET=${NEXTAUTH_SECRET} # 从部署平台获取NEXTAUTH_URL=https://myapp.comAPI_KEY=${API_KEY} # 从部署平台获取// .env.example(模板文件)DATABASE_URL=postgresql://localhost:5432/myappNEXTAUTH_SECRET=your-secret-key-hereNEXTAUTH_URL=http://localhost:3000API_KEY=your-api-key-here// .gitignore.env.local.env.production.env.development*.env// 环境变量验证// lib/env.jsimport { z } from 'zod';const envSchema = z.object({ DATABASE_URL: z.string().url(), NEXTAUTH_SECRET: z.string().min(32), NEXTAUTH_URL: z.string().url(), API_KEY: z.string().min(16), NODE_ENV: z.enum(['development', 'production', 'test']),});export const env = envSchema.parse(process.env);// 使用环境变量// lib/db.jsimport { env } from './env';export const db = createConnection(env.DATABASE_URL);2. CSP(内容安全策略)配置// next.config.jsconst ContentSecurityPolicy = require('./lib/csp');module.exports = { async headers() { return [ { source: '/(.*)', headers: [ { key: 'Content-Security-Policy', value: ContentSecurityPolicy.toString(), }, { key: 'X-Frame-Options', value: 'DENY', }, { key: 'X-Content-Type-Options', value: 'nosniff', }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin', }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()', }, ], }, ]; },};// lib/csp.jsconst ContentSecurityPolicy = ` default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https: blob:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; media-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests;`;export default ContentSecurityPolicy;// 动态 CSP 策略// lib/csp.jsimport { NextResponse } from 'next/server';export function getCSPHeaders() { const nonce = Buffer.from(crypto.randomUUID()).toString('base64'); return { 'Content-Security-Policy': ` default-src 'self'; script-src 'self' 'nonce-${nonce}' https://cdn.example.com; style-src 'self' 'nonce-${nonce}' https://fonts.googleapis.com; img-src 'self' data: https: blob:; `.replace(/\s{2,}/g, ' ').trim(), 'X-Nonce': nonce, };}// 在页面中使用 nonce// app/layout.jsexport default function RootLayout({ children }) { const nonce = headers().get('X-Nonce'); return ( <html lang="en"> <head> <script nonce={nonce} src="https://cdn.example.com/analytics.js" /> </head> <body>{children}</body> </html> );}3. XSS 防护// 使用 React 的自动转义// app/page.jsexport default function Page({ userContent }) { // React 自动转义,防止 XSS return <div>{userContent}</div>;}// 危险操作:使用 dangerouslySetInnerHTMLexport default function Page({ userContent }) { // 如果必须使用,需要先清理内容 const sanitizedContent = DOMPurify.sanitize(userContent); return ( <div dangerouslySetInnerHTML={{ __html: sanitizedContent }} /> );}// 使用 DOMPurify 清理 HTML// lib/sanitize.jsimport DOMPurify from 'isomorphic-dompurify';export function sanitizeHTML(html) { return DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'], ALLOWED_ATTR: [], });}export function sanitizeURL(url) { try { const parsed = new URL(url); if (!['http:', 'https:'].includes(parsed.protocol)) { return ''; } return parsed.href; } catch { return ''; }}// API 路由中的输入验证// app/api/comments/route.jsimport { z } from 'zod';import { sanitizeHTML } from '@/lib/sanitize';const commentSchema = z.object({ content: z.string().min(1).max(1000), userId: z.string().uuid(),});export async function POST(request) { const body = await request.json(); // 验证输入 const validatedData = commentSchema.parse(body); // 清理 HTML 内容 const sanitizedContent = sanitizeHTML(validatedData.content); // 保存到数据库 const comment = await saveComment({ ...validatedData, content: sanitizedContent, }); return Response.json(comment);}4. CSRF 防护// 使用 next-safe-action 防止 CSRF// app/actions.js'use server';import { action, makeSafeActionClient } from 'next-safe-action';import { z } from 'zod';const safeActionClient = makeSafeActionClient();export const createComment = action( z.object({ content: z.string().min(1).max(1000), postId: z.string().uuid(), }), async ({ content, postId }) => { // 自动验证 CSRF token const comment = await createCommentInDB({ content, postId }); return { success: true, comment }; });// 自定义 CSRF 防护// lib/csrf.jsimport { cookies } from 'next/headers';const CSRF_SECRET = process.env.CSRF_SECRET;export async function generateCSRFToken() { const token = crypto.randomUUID(); const cookieStore = await cookies(); cookieStore.set('csrf_token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 60 * 60, // 1 hour }); return token;}export async function validateCSRFToken(token) { const cookieStore = await cookies(); const storedToken = cookieStore.get('csrf_token'); if (!storedToken || storedToken.value !== token) { throw new Error('Invalid CSRF token'); } return true;}// API 路由中使用 CSRF 防护// app/api/comments/route.jsimport { validateCSRFToken } from '@/lib/csrf';export async function POST(request) { const body = await request.json(); const csrfToken = request.headers.get('X-CSRF-Token'); // 验证 CSRF token await validateCSRFToken(csrfToken); // 处理请求 const comment = await createComment(body); return Response.json(comment);}// 客户端发送请求时包含 CSRF token// components/CommentForm.js'use client';import { useState, useEffect } from 'react';export default function CommentForm({ postId }) { const [content, setContent] = useState(''); const [csrfToken, setCsrfToken] = useState(''); useEffect(() => { // 获取 CSRF token fetch('/api/csrf-token') .then(res => res.json()) .then(data => setCsrfToken(data.token)); }, []); const handleSubmit = async (e) => { e.preventDefault(); await fetch('/api/comments', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken, }, body: JSON.stringify({ content, postId }), }); }; return ( <form onSubmit={handleSubmit}> <textarea value={content} onChange={(e) => setContent(e.target.value)} /> <button type="submit">Submit</button> </form> );}5. 认证和授权// 使用 NextAuth.js 进行认证// app/api/auth/[...nextauth]/route.jsimport NextAuth from 'next-auth';import CredentialsProvider from 'next-auth/providers/credentials';import GoogleProvider from 'next-auth/providers/google';import { verifyPassword } from '@/lib/auth';export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), CredentialsProvider({ name: 'Credentials', credentials: { email: { label: 'Email', type: 'email' }, password: { label: 'Password', type: 'password' }, }, async authorize(credentials) { const user = await getUserByEmail(credentials.email); if (!user) { throw new Error('User not found'); } const isValidPassword = await verifyPassword( credentials.password, user.password ); if (!isValidPassword) { throw new Error('Invalid password'); } return { id: user.id, email: user.email, name: user.name, role: user.role, }; }, }), ], callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id; token.role = user.role; } return token; }, async session({ session, token }) { session.user.id = token.id; session.user.role = token.role; return session; }, }, pages: { signIn: '/login', error: '/auth/error', }, session: { strategy: 'jwt', maxAge: 30 * 24 * 60 * 60, // 30 days }, secret: process.env.NEXTAUTH_SECRET,});export { handlers as GET, handlers as POST };// 基于角色的访问控制(RBAC)// lib/auth.jsimport { auth } from '@/app/api/auth/[...nextauth]/config';export const ROLES = { ADMIN: 'admin', USER: 'user', GUEST: 'guest',};export async function requireAuth() { const session = await auth(); if (!session) { throw new Error('Unauthorized'); } return session;}export async function requireRole(role) { const session = await requireAuth(); if (session.user.role !== role) { throw new Error('Forbidden'); } return session;}export async function requireAnyRole(...roles) { const session = await requireAuth(); if (!roles.includes(session.user.role)) { throw new Error('Forbidden'); } return session;}// 在 API 路由中使用授权// app/api/admin/users/route.jsimport { requireRole, ROLES } from '@/lib/auth';export async function GET() { const session = await requireRole(ROLES.ADMIN); const users = await getAllUsers(); return Response.json(users);}// 在页面中使用授权// app/admin/page.jsimport { redirect } from 'next/navigation';import { requireRole, ROLES } from '@/lib/auth';export default async function AdminPage() { const session = await requireRole(ROLES.ADMIN); return ( <div> <h1>Admin Dashboard</h1> <p>Welcome, {session.user.name}</p> </div> );}6. 数据库安全// 使用参数化查询防止 SQL 注入// lib/db.jsimport { Pool } from 'pg';const pool = new Pool({ connectionString: process.env.DATABASE_URL,});export async function getUserById(id) { const result = await pool.query( 'SELECT * FROM users WHERE id = $1', [id] ); return result.rows[0];}export async function createUser(data) { const result = await pool.query( 'INSERT INTO users (email, password, name) VALUES ($1, $2, $3) RETURNING *', [data.email, data.password, data.name] ); return result.rows[0];}// 使用 ORM 防止 SQL 注入// lib/prisma.jsimport { PrismaClient } from '@prisma/client';const prisma = new PrismaClient();export async function getUserById(id) { return prisma.user.findUnique({ where: { id }, });}export async function createUser(data) { return prisma.user.create({ data: { email: data.email, password: data.password, name: data.name, }, });}// 数据库连接安全// lib/db.jsimport { Pool } from 'pg';const pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false, } : false, max: 20, // 最大连接数 idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000,});// 数据库迁移安全// prisma/migrate.tsimport { PrismaClient } from '@prisma/client';const prisma = new PrismaClient();async function main() { // 使用事务确保数据一致性 await prisma.$transaction(async (tx) => { await tx.user.create({ data: { email: 'admin@example.com', password: await hashPassword('admin123'), role: 'admin', }, }); });}main() .catch((e) => { console.error(e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });7. API 安全// 速率限制// lib/rate-limit.jsimport { Ratelimit } from '@upstash/ratelimit';import { Redis } from '@upstash/redis';const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(10, '10 s'), analytics: true,});export async function rateLimit(identifier) { const { success, limit, remaining, reset } = await ratelimit.limit(identifier); if (!success) { throw new Error('Rate limit exceeded'); } return { limit, remaining, reset };}// 在 API 路由中使用速率限制// app/api/comments/route.jsimport { rateLimit } from '@/lib/rate-limit';import { headers } from 'next/headers';export async function POST(request) { const ip = headers().get('x-forwarded-for') || 'unknown'; // 应用速率限制 await rateLimit(ip); const body = await request.json(); const comment = await createComment(body); return Response.json(comment);}// API 密钥验证// lib/api-key.jsexport async function validateApiKey(apiKey) { const validKeys = process.env.API_KEYS?.split(',') || []; if (!validKeys.includes(apiKey)) { throw new Error('Invalid API key'); } return true;}// 在 API 路由中验证 API 密钥// app/api/data/route.jsimport { validateApiKey } from '@/lib/api-key';import { headers } from 'next/headers';export async function GET(request) { const apiKey = headers().get('x-api-key'); // 验证 API 密钥 await validateApiKey(apiKey); const data = await fetchData(); return Response.json(data);}// 输入验证// lib/validation.jsimport { z } from 'zod';export const commentSchema = z.object({ content: z.string().min(1).max(1000), postId: z.string().uuid(), userId: z.string().uuid(),});export const userSchema = z.object({ email: z.string().email(), password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/), name: z.string().min(2).max(100),});// 在 API 路由中使用验证// app/api/users/route.jsimport { userSchema } from '@/lib/validation';export async function POST(request) { const body = await request.json(); // 验证输入 const validatedData = userSchema.parse(body); // 处理请求 const user = await createUser(validatedData); return Response.json(user);}8. 文件上传安全// 文件上传验证// lib/file-upload.jsimport { z } from 'zod';const ALLOWED_MIME_TYPES = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp',];const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MBexport const fileSchema = z.object({ name: z.string().max(255), type: z.enum(ALLOWED_MIME_TYPES), size: z.number().max(MAX_FILE_SIZE),});export async function validateFile(file) { // 验证文件类型 if (!ALLOWED_MIME_TYPES.includes(file.type)) { throw new Error('Invalid file type'); } // 验证文件大小 if (file.size > MAX_FILE_SIZE) { throw new Error('File too large'); } // 验证文件内容 const buffer = await file.arrayBuffer(); const uint8Array = new Uint8Array(buffer); // 检查文件头(magic numbers) if (file.type === 'image/jpeg') { if (uint8Array[0] !== 0xFF || uint8Array[1] !== 0xD8) { throw new Error('Invalid JPEG file'); } } return true;}// 安全的文件上传 API// app/api/upload/route.jsimport { validateFile } from '@/lib/file-upload';import { uploadToS3 } from '@/lib/s3';export async function POST(request) { const formData = await request.formData(); const file = formData.get('file'); if (!file) { return Response.json({ error: 'No file provided' }, { status: 400 }); } try { // 验证文件 await validateFile(file); // 生成安全的文件名 const ext = file.name.split('.').pop(); const safeName = `${crypto.randomUUID()}.${ext}`; // 上传到 S3 const url = await uploadToS3(file, safeName); return Response.json({ url }); } catch (error) { return Response.json({ error: error.message }, { status: 400 }); }}9. 日志和监控// 安全日志记录// lib/logger.jsimport pino from 'pino';const logger = pino({ level: process.env.LOG_LEVEL || 'info', transport: process.env.NODE_ENV === 'development' ? { target: 'pino-pretty', options: { colorize: true, }, } : undefined,});export function logSecurityEvent(event, data) { logger.warn({ type: 'SECURITY_EVENT', event, timestamp: new Date().toISOString(), ...data, });}export function logError(error, context = {}) { logger.error({ type: 'ERROR', error: error.message, stack: error.stack, timestamp: new Date().toISOString(), ...context, });}// 使用示例// app/api/auth/login/route.jsimport { logSecurityEvent } from '@/lib/logger';export async function POST(request) { const body = await request.json(); try { const user = await authenticateUser(body.email, body.password); return Response.json({ user }); } catch (error) { // 记录安全事件 logSecurityEvent('LOGIN_FAILED', { email: body.email, ip: request.headers.get('x-forwarded-for'), userAgent: request.headers.get('user-agent'), }); return Response.json({ error: 'Invalid credentials' }, { status: 401 }); }}安全最佳实践环境变量安全: 使用 .env.local,不要提交到版本控制CSP 配置: 严格的内容安全策略输入验证: 所有用户输入都必须验证输出编码: 防止 XSS 攻击CSRF 防护: 使用 token 验证认证授权: 实施适当的认证和授权机制速率限制: 防止暴力攻击HTTPS: 强制使用 HTTPS依赖更新: 定期更新依赖包安全审计: 定期进行安全审计和渗透测试通过实施这些安全措施,可以显著提高 Next.js 应用的安全性。
阅读 0·2月17日 23:18

Next.js 的 Server Actions 是如何工作的?

Server Actions 是 Next.js 13.4+ 引入的一个强大功能,它允许你在服务器组件中直接调用服务器端函数,简化了表单提交和数据变更操作。什么是 Server Actions?Server Actions 是一种在服务器上执行的异步函数,可以从客户端或服务器组件中调用。它们提供了一种简单的方式来处理表单提交、数据变更和其他服务器端操作。基本语法'use server';export async function createTodo(formData) { const title = formData.get('title'); const description = formData.get('description'); await db.todo.create({ data: { title, description } }); revalidatePath('/todos');}Server Actions 的使用方式1. 在表单中使用import { createTodo } from './actions';export default function TodoForm() { return ( <form action={createTodo}> <input name="title" placeholder="Title" required /> <textarea name="description" placeholder="Description" /> <button type="submit">Create Todo</button> </form> );}2. 在事件处理器中使用'use client';import { createTodo } from './actions';export default function CreateTodoButton() { const [pending, startTransition] = useTransition(); const handleClick = () => { const formData = new FormData(); formData.append('title', 'New Todo'); formData.append('description', 'Description'); startTransition(async () => { await createTodo(formData); }); }; return ( <button onClick={handleClick} disabled={pending}> {pending ? 'Creating...' : 'Create Todo'} </button> );}3. 在服务器组件中直接调用import { createTodo } from './actions';export default async function Page() { // 直接在服务器组件中调用 await createTodo(new FormData()); const todos = await db.todo.findMany(); return <TodoList todos={todos} />;}Server Actions 的特性1. 自动处理表单数据'use server';export async function updateUser(formData) { const name = formData.get('name'); const email = formData.get('email'); const avatar = formData.get('avatar'); // File 对象 // 处理文件上传 if (avatar instanceof File) { const url = await uploadFile(avatar); await db.user.update({ where: { id: userId }, data: { name, email, avatar: url } }); }}2. 返回数据'use server';export async function searchProducts(query) { const products = await db.product.findMany({ where: { name: { contains: query } } }); return { success: true, data: products, count: products.length };}// 使用'use client';import { searchProducts } from './actions';export default function SearchComponent() { const [results, setResults] = useState(null); const handleSearch = async (e) => { e.preventDefault(); const formData = new FormData(e.target); const query = formData.get('query'); const response = await searchProducts(query); setResults(response.data); }; return ( <form onSubmit={handleSearch}> <input name="query" /> <button type="submit">Search</button> {results && <ResultsList results={results} />} </form> );}3. 错误处理'use server';export async function createPost(formData) { try { const title = formData.get('title'); const content = formData.get('content'); if (!title || !content) { return { error: 'Title and content are required' }; } const post = await db.post.create({ data: { title, content } }); return { success: true, post }; } catch (error) { return { error: 'Failed to create post' }; }}// 使用错误状态'use client';import { useFormState } from 'react-dom';import { createPost } from './actions';export default function PostForm() { const [state, formAction] = useFormState(createPost, null); return ( <form action={formAction}> <input name="title" /> <textarea name="content" /> <button type="submit">Create Post</button> {state?.error && ( <div className="error">{state.error}</div> )} {state?.success && ( <div className="success">Post created!</div> )} </form> );}4. 重定向'use server';import { redirect } from 'next/navigation';export async function login(formData) { const email = formData.get('email'); const password = formData.get('password'); const user = await authenticate(email, password); if (user) { redirect('/dashboard'); } else { return { error: 'Invalid credentials' }; }}5. 缓存重新验证'use server';import { revalidatePath, revalidateTag } from 'next/cache';export async function updatePost(postId, formData) { const title = formData.get('title'); const content = formData.get('content'); await db.post.update({ where: { id: postId }, data: { title, content } }); // 重新验证特定路径 revalidatePath('/posts'); revalidatePath(`/posts/${postId}`); // 或使用标签重新验证 revalidateTag('posts');}高级用法1. 带参数的 Server Actions'use server';export async function deletePost(postId: string) { await db.post.delete({ where: { id: postId } }); revalidatePath('/posts');}// 使用'use client';import { deletePost } from './actions';export default function PostCard({ post }) { const handleDelete = async () => { if (confirm('Are you sure?')) { await deletePost(post.id); } }; return ( <div> <h2>{post.title}</h2> <button onClick={handleDelete}>Delete</button> </div> );}2. 使用 bind 绑定参数'use server';export async function updateTodo(todoId, formData) { const title = formData.get('title'); const completed = formData.get('completed') === 'true'; await db.todo.update({ where: { id: todoId }, data: { title, completed } }); revalidatePath('/todos');}// 使用 bind'use client';import { updateTodo } from './actions';export default function TodoItem({ todo }) { const updateTodoWithId = updateTodo.bind(null, todo.id); return ( <form action={updateTodoWithId}> <input name="title" defaultValue={todo.title} /> <input type="checkbox" name="completed" defaultChecked={todo.completed} value="true" /> <button type="submit">Update</button> </form> );}3. 乐观更新'use client';import { useOptimistic } from 'react';import { toggleTodo } from './actions';export default function TodoList({ todos }) { const [optimisticTodos, addOptimisticTodo] = useOptimistic( todos, (state, newTodo) => { return state.map(todo => todo.id === newTodo.id ? { ...todo, completed: newTodo.completed } : todo ); } ); return ( <ul> {optimisticTodos.map(todo => ( <li key={todo.id}> <input type="checkbox" checked={todo.completed} onChange={async () => { addOptimisticTodo({ id: todo.id, completed: !todo.completed }); await toggleTodo(todo.id); }} /> {todo.title} </li> ))} </ul> );}4. 文件上传'use server';export async function uploadAvatar(formData) { const file = formData.get('avatar') as File; if (!file) { return { error: 'No file uploaded' }; } const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); // 上传到云存储 const url = await uploadToCloudStorage(buffer, file.name); // 更新用户头像 await db.user.update({ where: { id: userId }, data: { avatar: url } }); revalidatePath('/profile'); return { success: true, url };}// 使用'use client';export default function AvatarUpload() { return ( <form action={uploadAvatar}> <input type="file" name="avatar" accept="image/*" /> <button type="submit">Upload</button> </form> );}实际应用场景1. 博客文章创建// app/actions/posts.ts'use server';import { revalidatePath } from 'next/navigation';import { auth } from '@/auth';export async function createPost(formData: FormData) { const session = await auth(); if (!session) { return { error: 'Unauthorized' }; } const title = formData.get('title') as string; const content = formData.get('content') as string; const tags = formData.get('tags') as string; const post = await db.post.create({ data: { title, content, authorId: session.user.id, tags: tags.split(',').map(tag => tag.trim()) } }); revalidatePath('/blog'); revalidatePath('/blog/new'); return { success: true, post };}// app/blog/new/page.tsximport { createPost } from '@/app/actions/posts';export default function NewPostPage() { return ( <div> <h1>Create New Post</h1> <form action={createPost}> <input name="title" placeholder="Title" required /> <textarea name="content" placeholder="Content" required /> <input name="tags" placeholder="Tags (comma separated)" /> <button type="submit">Publish</button> </form> </div> );}2. 电商购物车// app/actions/cart.ts'use server';import { revalidateTag } from 'next/cache';import { auth } from '@/auth';export async function addToCart(productId: string, quantity: number = 1) { const session = await auth(); if (!session) { return { error: 'Unauthorized' }; } await db.cartItem.upsert({ where: { userId_productId: { userId: session.user.id, productId } }, update: { quantity: { increment: quantity } }, create: { userId: session.user.id, productId, quantity } }); revalidateTag('cart'); return { success: true };}export async function removeFromCart(itemId: string) { const session = await auth(); if (!session) { return { error: 'Unauthorized' }; } await db.cartItem.delete({ where: { id: itemId } }); revalidateTag('cart'); return { success: true };}// app/components/AddToCartButton.tsx'use client';import { addToCart } from '@/app/actions/cart';import { useTransition } from 'react';export default function AddToCartButton({ productId }) { const [pending, startTransition] = useTransition(); const handleClick = () => { startTransition(async () => { await addToCart(productId); }); }; return ( <button onClick={handleClick} disabled={pending}> {pending ? 'Adding...' : 'Add to Cart'} </button> );}3. 评论系统// app/actions/comments.ts'use server';import { revalidatePath } from 'next/navigation';import { auth } from '@/auth';export async function addComment(postId: string, formData: FormData) { const session = await auth(); if (!session) { return { error: 'Unauthorized' }; } const content = formData.get('content') as string; const comment = await db.comment.create({ data: { content, postId, authorId: session.user.id } }); revalidatePath(`/posts/${postId}`); return { success: true, comment };}// app/posts/[id]/page.tsximport { addComment } from '@/app/actions/comments';export default function PostPage({ params }) { const post = await getPost(params.id); return ( <div> <PostContent post={post} /> <CommentList comments={post.comments} /> <form action={addComment.bind(null, post.id)}> <textarea name="content" placeholder="Write a comment..." required /> <button type="submit">Post Comment</button> </form> </div> );}最佳实践1. 验证输入'use server';import { z } from 'zod';const createPostSchema = z.object({ title: z.string().min(1).max(200), content: z.string().min(1), tags: z.array(z.string()).optional()});export async function createPost(formData: FormData) { const validatedFields = createPostSchema.safeParse({ title: formData.get('title'), content: formData.get('content'), tags: formData.get('tags')?.split(',') }); if (!validatedFields.success) { return { error: 'Invalid input', details: validatedFields.error }; } // 处理验证后的数据 const { title, content, tags } = validatedFields.data; await db.post.create({ data: { title, content, tags } }); revalidatePath('/posts'); return { success: true };}2. 权限检查'use server';import { auth } from '@/auth';export async function deleteUser(userId: string) { const session = await auth(); // 检查用户是否已登录 if (!session) { return { error: 'Unauthorized' }; } // 检查用户是否有权限 if (session.user.role !== 'admin' && session.user.id !== userId) { return { error: 'Forbidden' }; } await db.user.delete({ where: { id: userId } }); revalidatePath('/users'); return { success: true };}3. 错误处理和日志记录'use server';import { revalidatePath } from 'next/navigation';export async function sensitiveAction(formData: FormData) { try { // 执行操作 await performAction(formData); revalidatePath('/'); return { success: true }; } catch (error) { // 记录错误 console.error('Action failed:', error); // 返回用户友好的错误消息 return { error: 'Something went wrong. Please try again.' }; }}4. 使用 TypeScript'use server';import { z } from 'zod';// 定义输入类型const CreatePostInput = z.object({ title: z.string(), content: z.string()});// 定义返回类型type CreatePostResult = | { success: true; post: Post } | { success: false; error: string };export async function createPost( formData: FormData): Promise<CreatePostResult> { const input = CreatePostInput.parse({ title: formData.get('title'), content: formData.get('content') }); const post = await db.post.create({ data: input }); revalidatePath('/posts'); return { success: true, post };}Server Actions 简化了服务器端操作,使表单处理和数据变更变得更加直观和高效。通过合理使用 Server Actions,可以构建出更简洁、更易维护的 Next.js 应用。
阅读 0·2月17日 23:16

Tailwind CSS 是什么,它与传统 CSS 框架有什么区别?

Tailwind CSS 是一个实用优先的 CSS 框架,它提供了大量的预定义类名,让开发者能够快速构建用户界面。与传统 CSS 框架(如 Bootstrap)不同,Tailwind CSS 不提供预构建的组件,而是提供底层的工具类,让开发者可以自由组合这些类来创建独特的设计。Tailwind CSS 的核心优势:快速开发:无需编写自定义 CSS,直接使用预定义类高度可定制:通过配置文件完全自定义设计系统响应式设计:内置响应式前缀(sm、md、lg、xl、2xl)深色模式:内置深色模式支持小体积:通过 PurgeCSS 自动删除未使用的样式一致性:强制使用统一的设计令牌安装和配置:通过 npm 安装:npm install -D tailwindcss初始化配置:npx tailwindcss init配置文件:tailwind.config.js在 CSS 中引入:@tailwind base; @tailwind components; @tailwind utilities;常用类名示例:间距:p-4(padding)、m-2(margin)、mt-4(margin-top)颜色:bg-blue-500(背景色)、text-white(文字颜色)布局:flex、grid、block、inline-block尺寸:w-full(宽度 100%)、h-screen(高度 100vh)字体:text-xl、font-bold、leading-relaxed边框:border-2、rounded-lg、shadow-md响应式设计:断点:sm(640px)、md(768px)、lg(1024px)、xl(1280px)、2xl(1536px)示例:md:w-1/2(中等屏幕及以上宽度为 50%)配置文件示例:module.exports = { content: ["./src/**/*.{html,js}"], theme: { extend: { colors: { primary: '#3b82f6', }, }, }, plugins: [],}最佳实践:使用 @apply 提取重复的类合理使用配置文件自定义设计系统利用 JIT 模式提高性能使用插件扩展功能保持 HTML 结构清晰
阅读 0·2月17日 23:16

Tailwind CSS 的 Flexbox 布局类有哪些,如何创建常见的 Flex 布局?

Tailwind CSS 的 Flexbox 布局通过 flex 相关类实现,让开发者能够轻松创建灵活的布局。基础 Flexbox 类:flex:启用 Flexbox 布局inline-flex:启用内联 Flexbox 布局flex-row:主轴为水平方向(默认)flex-row-reverse:主轴为水平反向方向flex-col:主轴为垂直方向flex-col-reverse:主轴为垂直反向方向主轴对齐(justify):justify-start:主轴起始对齐justify-end:主轴结束对齐justify-center:主轴居中对齐justify-between:主轴两端对齐,项目之间间隔相等justify-around:主轴两端对齐,项目周围间隔相等justify-evenly:主轴均匀分布,项目之间间隔相等交叉轴对齐(items):items-start:交叉轴起始对齐items-end:交叉轴结束对齐items-center:交叉轴居中对齐items-baseline:基线对齐items-stretch:拉伸以填充容器(默认)多行对齐(content):content-start:多行起始对齐content-end:多行结束对齐content-center:多行居中对齐content-between:多行两端对齐content-around:多行周围间隔相等content-evenly:多行均匀分布Flex 项目属性:flex-1:flex: 1 1 0%flex-auto:flex: 1 1 autoflex-initial:flex: 0 1 autoflex-none:flex: noneflex-grow-0:flex-grow: 0flex-grow:flex-grow: 1flex-shrink-0:flex-shrink: 0flex-shrink:flex-shrink: 1flex-wrap:允许换行flex-nowrap:不允许换行(默认)flex-wrap-reverse:反向换行常用布局示例:水平居中: <div class="flex justify-center items-center h-screen"> 居中内容 </div>导航栏: <nav class="flex justify-between items-center p-4"> <div class="font-bold">Logo</div> <ul class="flex space-x-4"> <li>Home</li> <li>About</li> <li>Contact</li> </ul> </nav>卡片网格: <div class="flex flex-wrap gap-4"> <div class="flex-1">Card 1</div> <div class="flex-1">Card 2</div> <div class="flex-1">Card 3</div> </div>垂直布局: <div class="flex flex-col space-y-4"> <div>Item 1</div> <div>Item 2</div> <div>Item 3</div> </div>响应式布局: <div class="flex flex-col md:flex-row"> <div class="flex-1">Sidebar</div> <div class="flex-3">Content</div> </div>间距控制:space-x-*:水平间距 <div class="flex space-x-4"> <div>Item 1</div> <div>Item 2</div> <div>Item 3</div> </div>space-y-*:垂直间距 <div class="flex flex-col space-y-4"> <div>Item 1</div> <div>Item 2</div> <div>Item 3</div> </div>最佳实践:使用语义化的 HTML 结构合理使用 flex 属性控制布局结合响应式前缀创建自适应布局使用间距类控制项目间距测试不同屏幕尺寸的显示效果
阅读 0·2月17日 23:01

Tailwind CSS 的 Grid 布局类有哪些,如何创建常见的 Grid 布局?

Tailwind CSS 的 Grid 布局通过 grid 相关类实现,让开发者能够轻松创建二维网格布局。基础 Grid 类:grid:启用 Grid 布局inline-grid:启用内联 Grid 布局grid-cols-1:1 列网格grid-cols-2:2 列网格grid-cols-3:3 列网格grid-cols-4:4 列网格grid-cols-5:5 列网格grid-cols-6:6 列网格grid-cols-7:7 列网格grid-cols-8:8 列网格grid-cols-9:9 列网格grid-cols-10:10 列网格grid-cols-11:11 列网格grid-cols-12:12 列网格grid-cols-none:不指定列数行设置:grid-rows-1:1 行网格grid-rows-2:2 行网格grid-rows-3:3 行网格grid-rows-4:4 行网格grid-rows-5:5 行网格grid-rows-6:6 行网格grid-rows-none:不指定行数间距控制:gap-0:无间距gap-1:0.25rem(4px)gap-2:0.5rem(8px)gap-3:0.75rem(12px)gap-4:1rem(16px)gap-5:1.25rem(20px)gap-6:1.5rem(24px)gap-8:2rem(32px)gap-x-*:水平间距gap-y-*:垂直间距对齐方式:justify-items-start:水平起始对齐justify-items-end:水平结束对齐justify-items-center:水平居中对齐justify-items-stretch:水平拉伸(默认)items-start:垂直起始对齐items-end:垂直结束对齐items-center:垂直居中对齐items-stretch:垂直拉伸(默认)justify-start:网格容器水平起始对齐justify-end:网格容器水平结束对齐justify-center:网格容器水平居中对齐justify-between:网格容器水平两端对齐justify-around:网格容器水平周围间隔相等content-start:多行垂直起始对齐content-end:多行垂直结束对齐content-center:多行垂直居中对齐content-between:多行垂直两端对齐content-around:多行垂直周围间隔相等跨列跨行:col-span-1:跨越 1 列col-span-2:跨越 2 列col-span-3:跨越 3 列- col-span-full:跨越所有列col-start-1:从第 1 列开始col-end-2:到第 2 列结束row-span-1:跨越 1 行row-span-2:跨越 2 行row-span-full:跨越所有行row-start-1:从第 1 行开始row-end-2:到第 2 行结束常用布局示例:基本网格: <div class="grid grid-cols-3 gap-4"> <div>Item 1</div> <div>Item 2</div> <div>Item 3</div> <div>Item 4</div> <div>Item 5</div> <div>Item 6</div> </div>响应式网格: <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div>Item 1</div> <div>Item 2</div> <div>Item 3</div> </div>卡片布局: <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div class="card">Card 1</div> <div class="card">Card 2</div> <div class="card">Card 3</div> <div class="card">Card 4</div> </div>复杂布局: <div class="grid grid-cols-4 grid-rows-3 gap-4"> <div class="col-span-2 row-span-2">Header</div> <div>Sidebar</div> <div class="col-span-3 row-span-2">Content</div> <div>Footer</div> </div>自动填充: <div class="grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] gap-4"> <div>Item 1</div> <div>Item 2</div> <div>Item 3</div> </div>最佳实践:使用语义化的 HTML 结构结合响应式前缀创建自适应网格合理使用间距控制布局使用跨列跨行创建复杂布局测试不同屏幕尺寸的显示效果
阅读 0·2月17日 23:01

Tailwind CSS 的 JIT 模式是什么,如何使用任意值和优化性能?

Tailwind CSS 的 JIT(Just-In-Time)模式是 v3.0 引入的新特性,按需生成 CSS,显著提高性能。JIT 模式的优势:按需生成:只生成实际使用的类更小的文件体积:减少最终 CSS 大小更快的构建速度:只处理使用的类支持任意值:可以使用任意 CSS 值更好的开发体验:实时生成样式启用 JIT 模式:Tailwind CSS v3.0+ 默认启用 JIT 模式在 tailwind.config.js 中配置: module.exports = { mode: 'jit', // v3.0+ 默认启用 content: ["./src/**/*.{html,js}"], }使用任意值:方括号语法:使用任意 CSS 值示例: <div class="w-[123px]">自定义宽度</div> <div class="h-[50%]">自定义高度</div> <div class="bg-[#1da1f2]">自定义颜色</div> <div class="p-[10px]">自定义内边距</div> <div class="text-[20px]">自定义字体大小</div> <div class="grid-cols-[repeat(auto-fill,minmax(200px,1fr))]">自定义网格</div>JIT 模式配置:content 配置:指定要扫描的文件示例: module.exports = { content: [ "./src/**/*.{html,js,jsx,ts,tsx,vue}", "./public/**/*.html", ], }safelist 配置:防止某些类被清除示例: module.exports = { safelist: [ 'bg-red-500', { pattern: /bg-(red|green|blue)-(100|200|300)/, variants: ['hover', 'focus'], } ] }JIT 模式与 AOT 模式对比:JIT(Just-In-Time):按需生成优点:文件小、构建快、支持任意值缺点:需要配置 content 路径AOT(Ahead-Of-Time):预先生成所有类优点:无需配置、简单缺点:文件大、构建慢性能优化:合理配置 content 路径,避免扫描不必要的文件使用 safelist 保护动态生成的类利用任意值减少自定义 CSS定期清理未使用的样式开发体验:实时生成:修改 HTML 后立即生成样式热重载:支持开发服务器热重载错误提示:提供清晰的错误信息构建配置:Webpack: // webpack.config.js module.exports = { module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader', 'postcss-loader'], }, ], }, }Vite: // vite.config.js export default { css: { postcss: { plugins: [ require('tailwindcss'), require('autoprefixer'), ], }, }, }最佳实践:确保所有使用的类都在 content 路径中使用任意值减少自定义 CSS定期审查和优化配置测试构建输出的大小利用 JIT 模式的优势提高开发效率
阅读 0·2月17日 23:01