跳至正文
Wener Notes

NextJS

约 8 分钟阅读
  • NextJS 的构建相当复杂 - lambda、server action、pages、app
    • dev 能运行,build 后可能又无法运行
  • 不要用于后端太重的场景
    • 构建会遇到各种问题
    • 前后端 Refresh 的频度不一样 - 后端 refresh 时慢
  • getInitialProps
    • 只能用于页面组件不能用于子组件
    • 在第一次渲染会执行,之后会在客户端做转换
    • 不要能服务端的模块,通过 API 实现调用
  • 内置了 node-fetch
  • 增量缓存的页面位于 .next/server/pages
    • 可以考虑映射到外部 - 需要拷贝初始数据
  • 无法按页面切分 CSS
    • 最终会生成单个 css 文件
    • 只有 _app 可以导入全局 css
  • 单一 HTML 入口 - 不像 vite 支持多入口
  • rewrites 会在 build 时生成 router-manifest, 因此 start 时配置的变量 无法 产生影响
  • ESM 使用还有问题
  • 路径不支持 UTF8

快速开始

bash
# 初始化项目
mkdir my-web && cd my-web
# 依赖
# npm install --save next react react-dom
# npm install --save-dev typescript @types/react @types/node
yarn add next@latest react@latest react-dom@latest
yarn add --dev typescript @types/react @types/node

# 首页
mkdir pages
cat << INDEX > pages/index.tsx
import React from 'react'
function Home({ pid }) {
  return <div>NextJS Running on {pid}!</div>
}
Home.getInitialProps = function () {
  return { pid: process.pid }
}
export default Home
INDEX
# 启动服务
./node_modules/.bin/next
# 访问 http://localhost:3000

# 其它常用目录
mkdir -p public libs hooks types components modules reducers hooks
依赖包

Tips

bash
# 基础依赖
yarn add next@latest react@latest react-dom@latest
yarn add --dev typescript @types/react @types/node

# Next 扩展增强插件
# 已内建
# yarn add @zeit/next-css @zeit/next-sass @next/mdx
yarn add next-transpile-modules @next/bundle-analyzer

# 已内建
# yarn add dotenv tsconfig-paths-webpack-plugin
yarn add --dev @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties
yarn add --dev babel-plugin-import

# 服务端开发
# isomorphic-unfetch isomorphic-ws

# 常用工具
yarn add moment lodash date-fns
yarn add --dev @types/lodash

# UI 框架
yarn add antd

# 测试
yarn add --dev ts-node jest ts-jest @types/jest
依赖包

提示

目录结构

  • next.config.js - 配置文件
  • next-env.d.ts - 针对 TS 的类型定义
  • pages - 页面 - 可直接访问
    • _document.js
    • _app.js
    • api - 接口 - 通过 /api/* 访问

自定义 _app.js

JavaScript
function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

// Only uncomment this method if you have blocking data requirements for
// every single page in your application. This disables the ability to
// perform automatic static optimization, causing every page in your app to
// be server-side rendered.
//
// MyApp.getInitialProps = async (appContext) => {
//   // calls page's `getInitialProps` and fills `appProps.pageProps`
//   const appProps = await App.getInitialProps(appContext);
//
//   return { ...appProps }
// }

export default MyApp;

自定义 _document.js

TypeScript
// _document is only rendered on the server side and not on the client side
// Event handlers like onClick can't be added to this file

// ./pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

自定义 _error.js

JavaScript

接口

修改 pages/api/test.ts 为以下内容

TypeScript
import { NextApiRequest, NextApiResponse } from 'next';
export default (req: NextApiRequest, res: NextApiResponse) => {
  res.status(200).json({ title: 'From server side' });
};

然后访问 localhost/api/test

接口配置

JavaScript
export const config = {
  api: {
    // 默认会解析 1mb 的 body - 可以关闭
    bodyParser: false,

    // 修改配置
    bodyParser: {
      sizeLimit: '1mb',
    },
  },
};

支持 CORS

JavaScript
import Cors from 'micro-cors';

const cors = Cors({
  allowedMethods: ['GET', 'HEAD'],
});

function Endpoint(req, res) {
  res.json({ message: 'Hello Everyone!' });
}

export default cors(Endpoint);

常用脚本

packages.json

JavaScript
{
  "scripts": {
    "test": "jest --passWithNoTests",
    "dev": "next",
    "build": "next build",
    "start": "next start"
  }
}

支持自定义端口

Heroku 部署需要这样

JavaScript
{
  "scripts": {
    "test": "jest --passWithNoTests",
    "dev": "next dev -p ${PORT:-3000}",
    "build": "next build",
    "start": "next start -p ${PORT:-3000}"
  }
}

workspace

JavaScript
{
  "scripts": {
    "test": "yarn --cwd packages/server test",
    "dev": "yarn --cwd packages/server dev",
    "build": "yarn --cwd packages/server build",
    "start": "yarn --cwd packages/server start"
  }
}

配置

JavaScript
module.exports = {
  // 自定义环境变量
  env: { customKey: 'my-value' },
  // 页面扩展名
  pageExtensions: ['mdx', 'jsx', 'js', 'ts', 'tsx'],
  // 资源路径替换
  assetPrefix: isProd ? 'https://cdn.mydomain.com' : '',
  // 支持 server 和 serverless
  target: 'serverless',

  // 自定义 webpack 配置
  webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
    // Note: we provide webpack above so you should not `require` it
    // Perform customizations to webpack config
    // Important: return the modified config
    config.plugins.push(new webpack.IgnorePlugin(/\/__tests__\//));
    return config;
  },
  webpackDevMiddleware: (config) => {
    // Perform customizations to webpack dev middleware config
    // Important: return the modified config
    return config;
  },

  // target 为 server 支持压缩
  // 如果前端后 web 服务可以不考虑开启
  compress: false,

  // 自动静态优化的提示 - 闪电图标
  devIndicators: {
    autoPrerender: false,
  },

  // 关闭 X-Powered-By 头
  poweredByHeader: false,
  // 生成 ETag - 默认开启
  generateEtags: false,

  // 构建目录
  distDir: 'build',

  // 生成构建 ID
  generateBuildId: async () => {
    // You can, for example, get the latest git commit hash here
    return 'my-build-id';
  },

  onDemandEntries: {
    // period (in ms) where the server will keep pages in the buffer
    maxInactiveAge: 25 * 1000,
    // number of pages that should be kept simultaneously without being disposed
    pagesBufferLength: 2,
  },

  typescript: {
    // 忽略开发时的 typescript 错误
    ignoreDevErrors: true,
  },

  // 静态导出的路径 - next export
  exportPathMap: async function (defaultPathMap, { dev, dir, outDir, distDir, buildId }) {
    return {
      '/': { page: '/' },
      '/about': { page: '/about' },
      '/p/hello-nextjs': { page: '/post', query: { title: 'hello-nextjs' } },
      '/p/learn-nextjs': { page: '/post', query: { title: 'learn-nextjs' } },
      '/p/deploy-nextjs': { page: '/post', query: { title: 'deploy-nextjs' } },
    };
  },

  // 会导出为 about/index.html 而不是 about.html
  exportTrailingSlash: true,
};
JavaScript
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');

module.exports = (phase, { defaultConfig }) => {
  if (phase === PHASE_DEVELOPMENT_SERVER) {
    return {
      /* development only config options here */
    };
  }

  return {
    /* config options for all phases except development here */
  };
};

针对服务端配置 fs

JavaScript
module.exports = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.node = {
        fs: 'empty',
      };
    }
    return config;
  },
};

支持 CSS

⚠️ 9.2 后内建支持 css

JavaScript
const withCSS = require('@zeit/next-css');
module.exports = withCSS({});

apollo

Create

bash
npx -y create-next-app@latest --ts
npx -y tailwindcss init -p
npm add -D tailwindcss postcss autoprefixer @tailwindcss/typography
npm add @headlessui/react zustand
# npm add react-query

npm add -D prettier
npm add -D @trivago/prettier-plugin-sort-imports

mkdir src/{components,hooks,contents,server,client}

# 按需
npm add classnames
npm add react-icons
npm add daisyui
# 数据
npm add axios react-query

# 后台
npm add react-table @tanstack/react-virtual
依赖包
package.json
{
  "prettier": {
    "bracketSameLine": false,
    "importOrder": ["^(?!@src/)", "^[^.]", "^[.][.]", "^[.][/]"],
    "importOrderSeparation": false,
    "trailingComma": "all",
    "printWidth": 120,
    "singleQuote": true,
    "overrides": [
      {
        "files": ["*.html", "*.css"],
        "options": {
          "singleQuote": false
        }
      }
    ]
  },
  "lint-staged": {
    "**/*": "prettier --write --ignore-unknown"
  }
}
postcss.config.js
module.exports = {
  plugins: {
    'tailwindcss/nesting': {},
    tailwindcss: {},
    autoprefixer: {},
  },
};
  • WebStorm
    • Languages & Frameworks -> Style Sheets -> Dialects -> PostCSS
    • Languages & Frameworks -> JavaScript -> Prettier -> On 'Reformat Code' action

middleware

pages/_middleware.ts
import type { NextFetchEvent, NextRequest } from 'next/server';

export function middleware(req: NextRequest, ev: NextFetchEvent) {
  return new Response('Hello, world!');
}

next/image

  • 缓存到 <distDir>/cache/images - .next/cache/images
  • 返回包含 x-nextjs-cache - MISS, STALE, HIT
  • 缓存时效 max(minimumCacheTTL,Cache-Control.s-maxage || Cache-Control.max-age)

| prop | default | | ----------- | --------- | --------------------- | | quality | 75 | | sizes | 100vw | | priority | false | | placeholder | empty | blur 提供 blurDataURL | | unoptimized | false | | layout | intrinsic | | loading | lazy |

  • width/height
    • 除了 fill 其他都需要 width 和 height
    • responsive, fill - 表示图片本来大小
    • intrinsic, fixed, raw - 表示渲染大小
  • sizes - md sizes
    • responsive 和 fill 默认使用 100vw
      • 100vw 优化空间小
      • 建议提供 sizes 属性
    • 如果实际没显示这么大,尽量设置一个合理的更小的值,提高压缩比 - 例如 50vw
    • intrinsic 和 fixed 不使用 sizes

| layout | srcSet | sizes | | ---------- | ------------------------ | ----- | ------------------------------- | | intrinsic | imageSizes | | 缩小适配容器 | | fixed | imageSizes | | 固定 width, height | | responsive | imageSizes + deviceSizes | 100vw | 适配容器 width | | fill | imageSizes + deviceSizes | 100vw | | raw | | | 有 sizes 同 sizes, 否则同 fixed |

  • intrinsic - 根据 viewport 缩放
  • fixed - 固定大小,不考虑 viewport - 接近 img
  • responsive - intrunsic+fixed - 要求 parent 是 display: block
  • fill
    • 需要 parent 是 position: relative
    • 通常配合 objectFit
  • 考虑 viewport -> 有 srcset

  • sizes - 选择 srcsec 的依据
    • <media-query> <size>, <size>
    • 最后一个不能有 media-query
    • (max-width: 600px) 200px, 50vw
    • tailwindcss container
      • (max-width: 1536px) 1536px, (max-width: 1280px) 1280px, (max-width: 1024px) 1024px, (max-width: 768px) 768px, (max-width: 640px) 640px, 100vw
      • (min-width: 720px) 35vw, 50vw
  • srcsec
    • <url> <descriptor>
      • descriptor - <width>w|<density>x
        • 默认 1x
      • 不应该同时包含 width 和 density
    • 200.png 200w, 400.png 400w
    • 1x.png 1x, 2x.png 2x
  • img.currentSrc - 判断当前使用的 src
  • 参考
JavaScript
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    minimumCacheTTL: 180,
    // avif 比 webp 慢 20%,小 20%
    // 默认只有 image/webp 建议添加 image/avif
    formats: ['image/avif', 'image/webp'],

    // 图片变种 deviceSizes*imageSizes
    // responsive 和 fill 时使用
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    // srcset - 在提供了 size 时使用
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],

    // svg
    dangerouslyAllowSVG: true,
    contentSecurityPolicy: `default-src 'self'; script-src 'none'; sandbox;`,
    // hosts
    domains: ['via.placeholder.com', 'lh3.googleusercontent.com'],

    // 支持的平台 imgix, cloudinary, akamai
    // 使用 custom 可自行提供实现 - ({ src, width, quality }) => string
    // 开发环境使用 squoosh - 慢但易于安装
    // 生产环境推荐 sharp
    // loader: 'default',
  },
};
module.exports = nextConfig;

动态路由

  • pages/post/[pid].js
  • pages/post/[pid]/[comment].js
  • pages/post/[...slug].js
  • pages/post/[[...slug]].js
    • 可选全部捕获
    • 会匹配 - /post

FAQ

sharp

bash
# 指定 platform 可从 dist 下载预编译
npm install --verbose --platform=linuxmusl sharp
依赖包

next.config.js

next

关联信息

反向链接、本文链接的其他页面和外部资料。

反向链接

  • Web Awesome
    笔记 · nextjs

    https://www.c82.net/iconography/ · https://www.photopea.com/ · ryohey/signal · MIT, TS · Online MIDI Editor: signal · http://oskarstalberg.com/Townscaper/

  • Web Dev FAQ
    笔记 · NextJS

    | abbr. | stand for | · | iife | Immediately Invoked Function Expression | · 在开发时 assert 错误 · invariant · 早期来自 React, flux

参考资料

GitHub27 条
其他外链10 条
最近更新commit 136452c编辑