项目初始化

  1. 创建项目
npx create-next-app@latest

初始选择以下配置

What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias (@/*)? No / Yes
  1. 创建.editorconfig

统一代码风格

# top-most EditorConfig file
root = true

# 针对所有文件
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
  1. 创建.vscode/settings.json

针对vscode配置,提升vscode开发效率

{
    "editor.tabSize": 2,
    "typescript.inlayHints.parameterNames.enabled": "all",
    "typescript.inlayHints.parameterTypes.enabled": true,
    "typescript.inlayHints.variableTypes.enabled": true,
    "typescript.inlayHints.propertyDeclarationTypes.enabled": true,
    "typescript.inlayHints.enumMemberValues.enabled": true,
    "typescript.inlayHints.functionLikeReturnTypes.enabled": true
  }

多端适配

多端适配采用的是tailwindcss方案

  1. 安装tailwindcss
npm i -D tailwindcss postcss autoprefixer
npx tailwindcss init

该操作会自动以下操作:

  • 更新package.json
  • 创建tailwind.config.js
  1. 更新tailwind.config.js配置
import type { Config } from 'tailwindcss';

const config: Config = {
  content: [ // 配置tailwindcss的作用范围:使用tailwindcss的地方
    './app/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}'
  ],
  theme: {
    extend: { }
  },
  plugins: []
};
export default config;
  1. 引入tailwindcss内置样式组件
/* app/global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
  1. 创建postcss.config.js文件
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}
  1. 测试可用性
// 更新app/page.tsx
export default function Home() {
  return (
    <main>
      <div className="text-3xl text-white bg-black">22222</div>
    </main>
  );
}
  1. 特定断点配置
// tailwind.config.ts
const config: Config = {
  content: [
    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}'
  ],
  screens: {
    xl: { min: '1281px' }, // pc
    lg: { max: '1280px' }, // pad
    md: { max: '760px' }, // 折叠屏
    sm: { max: '450px' } // 手机
  },
  plugins: []
};
export default config;

Caveat:

screens的配置是有优先级的,上述配置是大屏优先,后续的media样式会覆盖先序的media匹配:sm > md > lg > xl。

移动端适配

rem + tailwindcss

移动端适配采用flexiblejs的rem方案

  • tailwindcss单位为remhtml#fontsize以浏览器默认字号为基准(通用为16px)
  • 移动端rem基础由设计图&设备宽度确定,是动态的
  1. 定制化tailwindcss

根据设计稿规范定制化tailwindcss,单位为px,后续由插件自动转换。

import type { Config } from 'tailwindcss';

const config: Config = {
  content: [
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    fontSize: {
      xs: '12px',
      sm: '14px',
      base: '16px',
      lg: '18px',
      xl: '20px',
      '2xl': '24px',
      '3xl': '30px',
      '4xl': '36px',
      '5xl': '48px',
      '6xl': '60px',
      '7xl': '72px',
    },
    spacing: {
      px: '1px',
      0: '0',
      0.5: '2px',
      1: '4px',
      1.5: '6px',
      2: '8px',
      2.5: '10px',
      3: '12px',
      3.5: '14px',
      4: '16px',
      5: '20px',
      6: '24px',
      7: '28px',
      8: '32px',
      9: '36px',
      10: '40px',
      11: '44px',
      12: '48px',
      14: '56px',
      16: '64px',
      20: '80px',
      24: '96px',
      28: '112px',
      32: '128px',
      36: '144px',
      40: '160px',
      44: '176px',
      48: '192px',
      52: '208px',
      56: '224px',
      60: '240px',
      64: '256px',
      72: '288px',
      80: '320px',
      96: '384px',
    },
    extend: {
      colors: { // colors在className中使用,才会被打包;否则,自定义颜色不起作用;
        "whiteFix": "white"
      }
      lineHeight: {
        3: '12px',
        4: '16px',
        5: '20px',
        6: '24px',
        7: '28px',
        8: '32px',
        9: '36px',
        10: '40px',
      },
      backgroundImage: {
        'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
        'gradient-conic':
          'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
      },
    },
  },
  plugins: [],
};
export default config;
  • colors在className中使用,才会被打包;否则,自定义颜色不起作用;
  1. 安装postcss-pxtorem插件
自动将样式中的px单位转换为rem单位,可通过设置PX(单位大写)禁止转换。
npm i -D postcss-pxtorem
  1. 配置postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
    'postcss-pxtorem': {      
        rootValue: 392 / 10,  //根据设计图    
        unitPrecision: 5,      
        propList: ['*'],      
        selectorBlackList: [/^\.html/],      
        exclude: /(node_module)/,      
        replace: true,      
        mediaQuery: false,      
        minPixelValue: 0,    
    }
  }
}
  1. 引入[flexible.js](https://github.com/amfe/lib-flexible/tree/master)
    <html lang="en">
      <head>
        <script src="http://g.tbcdn.cn/mtb/lib-flexible/0.3.2/??flexible_css.js,flexible.js"></script>
      </head>
      <body>{children}</body>
    </html>

FAQs:

  • 引入flexible.js为什么不使用next/script内置<Script>标签,而使用<script>

next/script内置<Script>标签针对脚本加载进行了优化,采用的是异步加载;

  • 异步加载flexible.js的话,ssr页面初始化加载时html标签采用浏览器默认的样式,没有指定fontsize样式;
  • 加载flexible.js执行阶段,依据客户端deviceWidth动态计算html#fontsize,引发页面整体样式的变化,浏览器会重新绘制页面,导致页面闪屏;

<script>是同步加载,会阻塞DOM树的渲染;

  • 同步加载flexible.js的话,ssr页面渲染之前flexible已经加载执行完毕,不会出现页面重新渲染而导致的闪屏。
  • 大字体情况下,flexible.js会受到影响,布局变大
禁止大字体,用户修改手机字号时页面不受影响
function rectifyNonstandardFontSize() {
  var $dom = document.createElement('div');
  $dom.style = 'font-size:20px;';
  document.body.appendChild($dom);
  var scaledFontSize = parseInt(
    window.getComputedStyle($dom, null).getPropertyValue('font-size')
  );
  document.body.removeChild($dom);
  var scaleFactor = 20 / scaledFontSize;
  var originRootFontSize = parseInt(
    window
      .getComputedStyle(document.documentElement, null)
      .getPropertyValue('font-size')
  );
  document.documentElement.style.fontSize =
    originRootFontSize * scaleFactor * scaleFactor + 'px';
}
rectifyNonstandardFontSize();
  • windows环境下,部分场景下flexible导致布局出现问题,代码报错:
使用flexible必须判断client环境,使用typeof window !== 'undefined'
  • tailwindcss在IE上无法使用

一般来说,Tailwind CSS v3.0专为Chrome、Firefox、Edge和Safari的最新稳定版本而设计,并在这些浏览器上进行了测试。它不支持任何版本的 IE,包括 IE 11,支持Edge。

若需要支持IE,可以使用Tailwind CSS v1.9,具体支持哪些浏览器完全取决于样式的使用,而不是框架。

  • 若使用 Tailwind 的 Flexbox 工具构建的网格,它只能在 IE10 以上版本中运行,因为IE9 不支持 Flexbox;
    <div class="flex">
      <div class="w-1/3"><!-- ... --></div>
      <div class="w-1/3"><!-- ... --></div>
      <div class="w-1/3"><!-- ... --></div>
    </div>
  • 如果需要支持 IE9,可以使用浮点来构建网格,因为几乎所有浏览器都支持浮点;
<div class="clearfix">
  <div class="float-left w-1/3"><!-- ... --></div>
  <div class="float-left w-1/3"><!-- ... --></div>
  <div class="float-left w-1/3"><!-- ... --></div>
</div>
  • tailwindcss不会自动为其任何样式添加供应商前缀,需要手动添加自动前缀器。
# Using npm
npm install autoprefixer

# Using Yarn
yarn add autoprefixer
// postcss.config.js
module.exports = {
  plugins: [
    require('tailwindcss'),
    require('autoprefixer'),
  ]
}

多主题

根据条件定义主题

const adaptMultipleThemes = () => {
  const isDark = getCookie('darkMode') === 'true'
  isDark && document && document.documentElement.classList.add('isDark')
}

scss声明主题样式

// scss主题设置
$linkColor: #3482FF!default;
$linkDarkColor: red;
@mixin mixDarkTheme () {
  & {
    @content;
  }
  @at-root :global(.isDark) { // :global脱离css module
    $linkColor: $linkDarkColor!global;
  }
  @at-root :global(.isDark) & {
    @content;
  };
}

主题样式应用

  1. 在入口文件中调用adaptMultipleThemes
  2. 在样式中使用@include mixDarkTheme
.title {
  @include mixDarkTheme(){
    color: $linkColor;
  }
}

站点配置

每个Page独立配置title

  1. 给每个页面增加layout.js
  2. layout.js中配置metadata

拆分next.config.mjs

next.config.js默认仅支持commonjs规范,若使用ES Module规范,更改后缀名为.mjs

.mjs配置为例进行拆分:

  1. 拆分需要使用.mjs后缀文件
  2. .mjs文件使用ES Module格式书写
  3. 引入需要携带.mjs后缀

全局依赖

在Next.js中通过<Script>引入像Swiper这种全局变量,在使用时会报Swiper is not defined这种错误。
  1. 定义类型声明文件
// next-env.d.ts
declare global {
  var Swiper: any;
  interface Window {
    Swiper: any
  }
}
declare interface Window {
  Swiper: any
}
  1. 在使用全局变量的地方引入类型声明文件
import { Swiper } from '@/types/globals';
// 底屏swiper
useEffect(() => {
  const myswiper = new Swiper('.swiper-container', {
    slidesPerView: 1.05,
    spaceBetween: 16
  });
  return () => {
    myswiper.destroy(true);
  };
}, []);

环境变量

CLI

  1. 通过命令行配置运行环境
// package.json
  "scripts": {
    "dev": "next dev",
    "build": "cross-env RUN_ENV=production next build && cross-env RUN_ENV=production npm run gennginx",
    "build:dev": "cross-env RUN_ENV=staging next build && cross-env RUN_ENV=staging npm run gennginx",
  },
  1. next.config.js#env读取环境变量写入代码

    • process.env.*只能node环境使用;
    • next.config.js#env会直接打入包内,serverclient都可以使用;

import Analyzer from '@next/bundle-analyzer';
import withImages from 'next-images';

import { domains } from './constants/domains.mjs';

const withBundleAnalyzer = Analyzer({
  enabled: process.env.ANALYZE === 'true'
});

const nextConfig = withImages(
  withBundleAnalyzer({
    // output: 'export', // 开启CSR,需要去除rewrite配置
    assetPrefix:
      process.env.RUN_ENV == void 0
        ? domains.STATIC_URL_PREFIX
        : `${domains.STATIC_URL_PREFIX}${domains.ASSET_PATH_PRFIX}`, // 静态资源前缀
    images: {
      loader: 'custom',
      loaderFile: './src/image-loader/index.mjs'
    },
    env: {
      RUN_ENV: process.env.RUN_ENV
    },
    webpack: (config) => {
      // 可自定义webpack配置
      return config;
    },
  })
);

export default nextConfig;

Caveat:

  • next.config.mjs引入自定义模块只能是.mjs文件,无法访问*.ts文件,会报错。
  • next.config.mjs#env配置变量的访问限制:

    • 不能解构:const { RUN_ENV } = process.env;
    • 不能使用变量:const env = process.env[$var];
  1. 通过process.env.*声明环境配置
// ./constants/domains.mjs
const dev = {
  HOST: '',
  STATIC_URL_PREFIX: '',
  ASSET_PATH_PRFIX: '/assets',
};
const staging = {
  HOST: 'https://<staging.host>.com',
  STATIC_URL_PREFIX: 'https://<staging.static.host.com>',
  ASSET_PATH_PRFIX: '/assets',
};
const prod = {
  HOST: 'https://<prod.host>.com',
  STATIC_URL_PREFIX: 'https://<prod.static.host.com>',
  ASSET_PATH_PRFIX: '/assets',
};
const env = process.env.RUN_ENV;
function getDomains() {
  switch (env) {
    case 'production':
      return prod;
    case 'staging':
      return staging;
    default:
      return dev;
  }
}
const domains = getDomains();
export { domains };

Good to know:

  • 借助import ... from ...的缓存特性,多次引用,只有第一次引用会执行代码,后续使用的是对象引用,而非拷贝。
  1. 消费者调用
// app/page.tsx
import { domains } from '@/constants/domains.mjs'; //必须携带mjs后缀
console.log(domains.Host)

Good to knows

.env文件

  • 带Next_Public_开头的变量,服务端、客户端都可以访问;
  • 不带Next_Public_开头的变量,服务端可以访问,客户端无法访问;
  • 通过.env文件配置的环境变量无法自定义环境关键字;
  • 通过.env文件配置的环境变量不会写入代码中,可考虑存储敏感信息(待验证)

读取环境变量写入代码有三种方案:

  1. env配置,具体如上

    const nextConfig = withImages(
        ...
        env: {
          RUN_ENV: process.env.RUN_ENV
        },
      })
    );
    
  2. webpack配置

    const nextConfig = withImages(
      ...
      webpack: (config, options) => {
        config.plugins.push(new options.webpack.DefinePlugin({
           'process.browser': JSON.stringify(typeof window !== 'undefined')
           'process.env': JSON.stringify(process.env)
        }));
    
        return config;
      }
    );
  3. babelrc配置

    • 安装插件:

      npm install babel-plugin-transform-define --save-dev
    • .babelrc 或 Babel 配置中添加插件配置:

      {
        "presets": ["next/babel"],
        "plugins": [
          ["transform-define", { "process.browser": "typeof window !== 'undefined'" }]
        ]
      }

参考文档:

https://nextjs.org/docs/messages/non-standard-node-env

https://www.51cto.com/article/773164.html

路由守护

中间件允许你在请求完成前运行代码。然后,您可以根据接收到的请求,通过重写、重定向、修改请求或响应标头或直接响应等方式修改响应。

中间件在缓存内容和路径匹配之前运行。

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}

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

多级状态管理Reducer

useReducer:

  1. useReducer可以管理复杂的、具有多个相关状态的组件状态;

    • 如不想通过useState定义多排状态,可以考虑useReducer替代;
    • 可以进行新旧状态数据对比,类同useState((prev) => {})
  2. useReducer & useContext实现多级状态同步;
  1. useReducer & useContext多级状态管理封装
import {
  createContext,
  Dispatch,
  useMemo,
  PropsWithChildren,
  useContext,
  useReducer
} from 'react';
import { useSearchParams } from 'react-router-dom';

interface ContextStateDef {
  login: {
    loginToken: string;
    openId: string;
  }
}
enum ActionsEnum {
  setState = 1,
}
interface ContextActionDef {
  type: ActionsEnum.setState;
  payload: ContextStateDef['login'];
}

const initialState = { // 定义初始上下文
  login: {
    loginToken: '',
    openId: ''
  }
};
type ActionDef = ContextActionDef;
const RootContextContainer = createContext<{  // 定义上下文,对应Provider的value必须符合当前类型;
  state: ContextStateDef;
  dispatch: Dispatch<ActionDef>;
}>({
  state: initialState,
  dispatch: (() => {}) as Dispatch<ActionDef>
});

const reducer = (state: ContextStateDef, action: ActionDef) => {
  const { type, payload } = action;
  switch (type) {
    case ActionsEnum.setState:
      return {
        ...state,
        login: payload
      };
    default:
      return {
        ...state
      } as never;
  }
};
const useRootContext = () => {  // 封装Consumer hooks方法,避免原始上下文暴露,提供调试信息
  const rootContext = useContext(RootContextContainer);
  if (!rootContext) {
    throw new Error('useRootContext must be used within a RootProvider');
  }
  const updateContext = useMemo(() => {  // 避免context引发的无限渲染循环
    return rootContext.dispatch;
  }, [rootContext]);
  const context = useMemo(() => {  // 避免context引发的无限渲染循环
    return rootContext.state;
  }, [rootContext]);
  return {
    context,
    updateContext
  };
};
const RootProvider = ({ children }: PropsWithChildren) => {
  const [state, dispatch] = useReducer(reducer, initialState);
  const [searchParams] = useSearchParams();
  // Provider实现多级传递上下文
  return (
    <RootContextContainer.Provider value={{ state, dispatch }}>
      {children}
    </RootContextContainer.Provider>
  );
};
export type { ContextStateDef };
export { ActionsEnum, useRootContext };
export default RootProvider;
  1. Provider调用
<RootProvider>
  <HeaderNavigation
    above
    defaultTheme={ThemeEnum.light}
    themeChangable
    transparent
  />
  {children}
  <FooterSiteMap />
</RootProvider>
  1. Consumer调用
  // 使用时,通过useCallback、useMemo承接,创建缓存,避免引发无限渲染循环;
  const updateScroller = useCallback(() => {
    updateContext({
      type: ActionsEnum.setScroller,
      payload: scrollContainerRef
    });
  }, [updateContext, scrollContainerRef]);
  useEffect(() => {
    updateScroller();
  }, [updateScroller]);

无限渲染循环

有时可能会遇到无限循环的问题。这通常是因为在使用时,依赖项没有正确设置,导致组件在每次渲染时都会重新创建新的上下文。

使用上下文时,需要通过useCallback、useMemo承接,组件使用memo()包裹,创建缓存,避免引发无限渲染循环

降级CSR

将Next应用打包为CSR,在服务崩溃时,通过Nginx负载均衡()指向客户端渲染。

  1. CSR导出配置
//next.config.js
const nextConfig = {  
  // https://nextjs.org/docs/app/building-your-application/deploying/static-exports
  output: 'export', 
} 
module.exports = nextConfig
  • next14导出静态资源时,只需要修改next.config.js即可,无需像v13版本使用next export
  • 执行next build,生成包含HTML/CSS/JS的out目录。
  • 通过browser-sync start --cors -s "./out"即可访问导出的静态页面。

CSR针对部分next特性不支持,详见https://nextjs.org/docs/app/building-your-application/deployi...

  1. 部署

    • 部署SSR
    • 部署CSR
  2. 宕机自动降级CSR
http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;
    upstream backend {
        server host.ssr.com max_fails=3 fail_timeout=30s; // 在30s内重试3次都失败,则标记当前服务不可用,会走下边的bak.com服务。
        server bak.csr.com backup;  // 当上游服务器都不可用时,该服务接收Nginx请求。
    }  // 如果所有配置的上游服务器都无法响应,Nginx将返回错误给客户端。默认情况下,这通常是 502 Bad Gateway 错误。
    server {
        listen  80;                                                         
        server_name domain.com;
        index  index.php index.html index.htm;

        location / {
            proxy_pass http://backend;  //将请求domain.com的请求代理到upstream服务集群中,按负载均衡策略访问不同的服务器。 
            proxy_set_header Host $host;
        }
    }
}

FAQs

  1. 静态资源加载失败
查看next.config#assetPrefix字段配置,该配置会影响js/css静态资源加载路径。
  1. 跳转失败,404
程序内的页面跳转需要使用next内置的组件next/link,使用a无法正常跳转。

参考文档

https://juejin.cn/post/7338280070304809010

https://blog.csdn.net/javaboyweng/article/details/97612605

浏览器兼容

ES6+垫片

  1. 安装相关依赖

    npm i -S core-js regenerator-runtime
    npm i -D @babel/core @babel/preset-env
  2. 根文件引入垫片

    // src/app/layout.tsx
    import 'core-js'
    import 'regenerator-runtime/runtime' // 支持async、await
  3. 配置.babelrc

    {
      "presets": [
        [
          "next/babel", 
          {
            "preset-env": { // 配置同@babel/preset-env
              "debug": true, // 开启调试模式,在命令行输出当前详细配置&执行。
              /**
                * 设置为 "entry" 时,Babel 会根据 core-js 版本和 targets 配置,自动引入目标环境所需的所有 polyfills。这需要在入口文件中手动引入 core-js/stable(包含 ECMAScript 标准特性)和 regenerator-runtime/runtime(如果你使用了异步函数)。
                * 使用 useBuiltIns: "usage" 时,不需要在代码中手动引入 core-js 或 regenerator-runtime,Babel 会根据需要自动处理这些引入。
              */
              "useBuiltIns": "entry",
              "corejs": {
                "version": "3.36", // Warning! Recommended to specify used minor core-js version, like corejs: '3.36', instead of corejs: 3, since with corejs: 3 will not be injected modules which were added in minor core-js releases.
                // "proposals": true
              },
              "targets": { 
              /**
               * 如果在 Babel 的配置文件(如 .babelrc、babel.config.js)中直接指定了 targets,这将具有最高优先级。Babel 会忽略 .browserslistrc 文件或 package.json 中的 browserslist 配置,仅使用 targets 选项的设置。
               * 两种设置模式:
                 * browserslist兼容模式
                 * browserslist模式
              */
              // "chrome": "49" // browserslist兼容模式
                "browsers": [ // browserslist模式
                  ">0.02%", // 包含chrome 48+
                  "not op_mini all",
                  "not op_mob >= 1"
                ]
              }
            },
            "transform-runtime": {},
            "styled-jsx": {},
            "class-properties": {}
          }
        ]
      ],
      "plugins": []
    }
  4. 配置next.config.mjs
const nextConfig = withImages(
  ...
  webpack: (config, options) => {
    config.plugins.push(new options.webpack.DefinePlugin({
       'process.env': JSON.stringify(process.env)
    }));

    return config;
  }
);
  1. 配置browserslist【必须】

    可通过npx browserslist查看当前支持的浏览器
    // package.json 
    {
      ...
      /**
       * 配置了.babelrc#targets,这个也需要配置,否则会报错。
       * 配置了browserslist,可以不用配置.babelrc#targets
      */
      "browserslist": [
        ">0.02%", // 包含chrome 48+
        "not op_mini all",
        "not op_mob >= 1"
      ]
    }

FAQs

  • globalThis is not defined.

报错代码回源:

node_modules/next/dist/client/components/async-local-storage.js

30L

const maybeGlobalAsyncLocalStorage = globalThis.AsyncLocalStorage;

解决方案:

https://www.npmjs.com/package/globalthis

修改代码库,追加下述代码:
var globalThis = require('globalthis')()

  • Illegal Constructor error.

报错代码回源:

node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js

new ReadableStream

解决方案:

https://github.com/MattiasBuelens/web-streams-polyfill/tree/masterIllegal

在html顶部追加:

<script src="`https://unpkg.com/web-streams-polyfill/dist/polyfill.js`"></script>

参考文档

https://www.jnielson.com/demystifying-babel-preset-env

https://github.com/vercel/next.js/issues/44250

https://cloud.tencent.com/developer/ask/sof/106622781


米花儿团儿
1.3k 声望75 粉丝