Skip to main content

NextJS FAQ

caution
  • react router 会导致页面整个刷新 #49479
  • NextJS Server
    • 尽量避免太重的 Server
      • 导致前端开发慢
      • 依赖可能出现各种问题
    • Server 环境复杂 - middleware、server action、edge
      • 使用可能有各种注意事项
      • Server 会初始化多次
    • Server 会导致环境依赖
      • 复用度降低
    • 如果不需要 服务端渲染/SSR/SEO 尽量避免过多 Server 代码

环境变量

  • dotenv 会自动 reload
  • .env 加载顺序
    • .env.$(NODE_ENV).local
      • development production test
      • 不该加到 git 参考
    • .env.local
      • 不会打包到 standalone
      • test 不会加载
    • .env.$(NODE_ENV)
    • .env
      • 会被打包到 standalone
// 测试加载
import { loadEnvConfig } from '@next/env';
export default async () => {
const projectDir = process.cwd();
loadEnvConfig(projectDir);
};
envdemoproduction
BASE_PATH/auth
NEXT_PUBLIC_URLhttp://127.0.0.1:$PORThttps://wener.me
NEXT_PUBLIC_BASE_PATH$BASE_PATH
NEXT_PUBLIC_BASE_URL$NEXT_PUBLIC_URL$BASE_PATH
NEXTAUTH_URL
PORT3000
NEXTAUTH_URLhttp://127.0.0.1:$PORT/auth/api/authhttps://wener.me/auth/api/auth
NEXTAUTH_URL_INTERNALhttp://127.0.0.1:$PORT/auth/api/auth
NEXTAUTH_SECRET生产环境必须
NEXT_PUBLIC_VERCEL_URL
PORT=3000
BASE_PATH=/auth
NEXTAUTH_URL=http://127.0.0.1:$PORT/auth/api/auth
NEXTAUTH_URL_INTERNAL=http://127.0.0.1:$PORT/auth/api/auth
NEXTAUTH_SECRET=

避免 Build 预渲染

Build 预渲染 需要构建环境有完备的 ENV,比如数据库连接、接口请求都要能正常返回。

next build --experimental-build-mode compile
# next experimental-compile

Render Context

  • 静态渲染 - 预渲染阶段
    • 根据情况有时候可能需要,有时候可能不希望需要
    • 预渲染后的内容是纯静态的
    • 调用 cookies, headers, fetch 会 opt 为 服务端渲染
    • 纯静态内容相当于直接生成了文件
  • 服务端渲染
    • 能 async
    • 能很容易获取到数据
    • 能获取到的上下文信息较少
    • 不能包含任何 state、Context 的代码
    • 只能使用部分 React 功能
  • 客户端渲染
    • 能使用所有 React 功能
    • 根据构建的站点情况,可能需要对数据获取做特殊处理
    • 例如:使用 action 来避免请求到真实的服务器,避免暴露真实服务器信息

  • 通常不要求 静态渲染/预渲染,除非是非常大流量的站点,不期望任何额外的请求到服务端后端。
  • 因此大多数时候需要区分 服务端渲染/SSR 还是 客户端渲染/CSR
  • 营销、推广、官网 站点 尽可能的让代码能 SSR
  • 让代码能 SSR 的一些方式
    • 尽量使用 CSS 做一些效果
      • 例如 tab 切换、按钮效果
    • 使用统一的一个 CSR 组件维护全局状态
      • 例如 弹窗、按钮 action 处理
    • 尽量隔离 CSR 组件
      • 可能一个 CSR 组件只做一个事情
      • 例如 ShowContactButton 只是在 Button 的基础上增加了 onClick 操作

socket hang up - 30s timeout

module.exports = {
httpAgentOptions: {
keepAlive: false,
},
experimental: {
proxyTimeout: 30_000, // 默认 30s
},
};

pathname vs asPath

pathpathnameasPath
///
/user/123/user/[id]/user/123
/user/123#top/user/[id]/user/123#top
  • asPath 真实路径
  • pathname NextJS 的文件路径

server vs serverless

  • server
    • 打包为整个应用
    • 支持自定义 server
    • 支持长链接 - websocket
    • 建议使用
  • serverless
    • 不会构建一体化应用
    • 页面独立 - 服务与页面不耦合
    • 页面与服务分离后更容易部署
      • 但依然是需要 next 来运行 - #4496
    • vercel 默认模式
  • 参考

next.config.ts

next.config.js

require('ts-node').register(require('./tsconfig.json'));

module.exports = require('./next.config.ts');

next.config.js 类型提示

/** @type {import('next').NextConfig} */
module.exports = {};

Prop className did not match

nextjs 12 后需要配置 compiler

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
compiler: {
// 添加
styledComponents: true,
},
};
module.exports = nextConfig;

使用单一的 API 来处理所有接口

yarn add polka cors body-parser
import polka from 'polka';
import cors from 'cors';
import { json, text, urlencoded } from 'body-parser';

let _router;

export function getRouter() {
return _router || (_router = routes(setup(polka())));
}

function setup(route) {
// treat path params as query - same as how next api handle this
route.use((req, res, next) => {
if (req.params) {
const q = req.query;
for (const [k, v] of Object.entries(req.params)) {
if (!q[k]) {
q[k] = v;
}
}
}
return next();
});
return route;
}

export function routes(r: any) {
const route = r as Router<NextApiRequest, NextApiResponse>;
// handle error
route.use(async (req, res, next) => {
try {
return await next();
} catch (e) {
const detail = normalizeError(e);
res.status(detail.status).json(detail);
console.error(`ERROR Handle ${req.url}`, e);
}
});
route.use(json());
route.use(urlencoded({ extended: true }));
route.use(text());

const corsOrigin = cors({ origin: true });

// cors
route.get('/api/cors', corsOrigin as any, (req, res) => res.json({}));
// path params
route.get('/api/user/:id', corsOrigin as any, (req, res) => res.json({ id: req.query.id }));

return route;
}

export function handleRequest(req, res) {
getRouter().handler(req, res);
}

export default handleRequest;
export const config = {
api: {
bodyParser: false,
},
};

Critical dependency: the request of a dependency is an expression

构建为 serverless 时可能出现

禁用 minification

export default {
productionBrowserSourceMaps: true,
webpack(config) {
config.optimization.minimize = false;
config.optimization.minimizer = [];
return config;
},
};

css 顺序不一致

将 app 导入的 css 抽取放到一个 css 文件进行导入

@import '~normalize.css/normalize.css';
@import '~@blueprintjs/icons/lib/css/blueprint-icons.css';
@import '~@blueprintjs/core/lib/css/blueprint.css';
@import '~@blueprintjs/select/lib/css/blueprint-select.css';
@import '~@blueprintjs/datetime/lib/css/blueprint-datetime.css';
@import '~@blueprintjs/popover2/lib/css/blueprint-popover2.css';
@import '~@blueprintjs/table/lib/css/table.css';
@import '~tailwindcss/tailwind.css';
@import '~nprogress/nprogress.css';

确保 tailwind 覆盖 blueprintjs 样式

  • 开启 webpack5 该方式无效
  • #16630

访问 public 里的 index.html

module.export = {
async rewrites() {
return {
fallback: [
{
source: '/:path*',
destination: '/:path*/index.html',
},
],
};
},
};

dynamic rewrites

/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
// 会在构建时静态生成,不支持动态
const { SERVER_URL } = process.env;
let redir = [
{
source: '/api/:path*',
destination: `${SERVER_URL}/api/:path*`,
},
];
return redir;
},
};

Lockfile was successfully patched, please run "npm install" to ensure @next/swc dependencies are downloaded

standalone

  • 非常适用于 docker 环境
  • 只需要 .next/standalone 不需要 node_modules
  • 使用 @vercel/nft 分析 import
module.exports = {
output: 'standalone',
experimental: {
// monorepo 需要调整 root
outputFileTracingRoot: path.join(__dirname, '../../'),
},
};
FROM node:16-alpine
WORKDIR /app

ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# monorepo need prefix path
COPY next.config.js ./apps/web/
COPY public ./apps/web/public
COPY package.json ./apps/web/package.json

COPY --chown=nextjs:nodejs .next/standalone ./
COPY --chown=nextjs:nodejs .next/static ./apps/web/.next/static

USER nextjs

EXPOSE 3000
ENV PORT 3000

# monorepo
CMD ["node", "apps/web/server.js"]

dynamic preload

  • dyanmic 不会预加载 - prefetch
  • 使用 @loadable/component

safari tel

<meta name="format-detection" content="telephone=no" />

检测在浏览器

// Webpack 定义 - 会根据依赖来移除代码
// https://github.com/zeit/next.js/pull/5159
process.browser;
// 通用逻辑
typeof window === 'undefined';

如何在 getInitialProps 引入服务端模块

  • 如果直接 require 是会导致被打包到客户端代码的
const faker = eval("require('faker')");

这个方式是最简单的,其他方式参考 SSR and Server Only Modules

Invalid left-hand side in assignment "MyModule*" = namespaces;

分析服务端代码和 SSR 代码

安装依赖

npm install --save-dev webpack-bundle-analyzer @zeit/next-bundle-analyzer

next.config.js

const withBundleAnalyzer = require('@zeit/next-bundle-analyzer');

const nextConfig = {
analyzeServer: ['server', 'both'].includes(process.env.BUNDLE_ANALYZE),
analyzeBrowser: ['browser', 'both'].includes(process.env.BUNDLE_ANALYZE),
bundleAnalyzerConfig: {
server: {
analyzerMode: 'static',
reportFilename: '../bundles/server.html',
},
browser: {
analyzerMode: 'static',
reportFilename: '../bundles/client.html',
},
},
webpack(config) {
return config;
},
};

module.exports = withBundleAnalyzer(nextConfig);

添加脚本

添加到 package.json

"analyze": "BUNDLE_ANALYZE=both next build",
"analyze:server": "BUNDLE_ANALYZE=server next build",
"analyze:browser": "BUNDLE_ANALYZE=browser next build"

执行分析

npm run analyze

You're using a Node.js module (buffer) which is not supported in the Edge Runtime

Expected server HTML to contain a matching div

function App({ Component, pageProps }: AppProps) {
return (
<div suppressHydrationWarning> // <- ADD THIS
{typeof window === 'undefined' ? null : <Component {...pageProps} />}
</div>
);
}

SPA rewrites

next.config.js
module.exports = {
async rewrites() {
return [
{
source: '/:any*',
destination: '/',
},
];
},
};

Date cannot be serialized as JSON

superjson

Module build failed: UnhandledSchemeError: Reading from "node:assert" is not handled by plugins (Unhandled scheme).

next.config.js
const webpack = require('webpack');

module.exports = {
webpack: (config, options) => {
config.plugins.push(
new webpack.NormalModuleReplacementPlugin(/^node:/, (resource) => {
resource.request = resource.request.replace(/^node:/, '');
}),
);
return config;
},
};

Can't resolve 'assert'

npm add assert
  • node:assert -> assert

Creating an optimized production build

DYNAMIC_SERVER_USAGE

dynamic ssr:false 时出现

Warning: lazy: Expected the result of a dynamic import() call.

NextJS ssr 不支持 React.lazy

Cannot read properties of null (reading 'useRef')

库 import 必须要有 .js 后缀

NODE_OPTIONS=--experimental-specifier-resolution=node

process.env.NEXT_PHASE

export const PHASE_EXPORT = 'phase-export'
export const PHASE_PRODUCTION_BUILD = 'phase-production-build'
export const PHASE_PRODUCTION_SERVER = 'phase-production-server'
export const PHASE_DEVELOPMENT_SERVER = 'phase-development-server'
export const PHASE_TEST = 'phase-test'
export const PHASE_INFO = 'phase-info'

Failed to find Server Action. This request might be from an older or newer deployment.

await isn't allowed in non-async function

  • NextJS 14, Server Action
await __webpack_async_dependencies__
const config = {
experimental: {
// https://github.com/vercel/next.js/discussions/57535
esmExternals: false,
},
};