Nextjs之app router应用
项目初始化
- 创建项目
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
- 创建
.editorconfig
统一代码风格
# top-most EditorConfig file
root = true
# 针对所有文件
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
- 创建
.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
方案
- 安装
tailwindcss
npm i -D tailwindcss postcss autoprefixer
npx tailwindcss init
该操作会自动以下操作:
-
更新
package.json
; -
创建
tailwind.config.js
- 更新
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;
- 引入
tailwindcss
内置样式组件
/* app/global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
- 创建
postcss.config.js
文件
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
- 测试可用性
// 更新app/page.tsx
export default function Home() {
return (
<main>
<div className="text-3xl text-white bg-black">22222</div>
</main>
);
}
- 特定断点配置
// 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
单位为rem
,html#fontsize
以浏览器默认字号为基准(通用为16px) -
移动端rem基础由设计图&设备宽度确定,是动态的
- 定制化
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中使用,才会被打包;否则,自定义颜色不起作用;
- 安装
postcss-pxtorem
插件
自动将样式中的px单位转换为rem单位,可通过设置PX(单位大写)禁止转换。
npm i -D postcss-pxtorem
- 配置
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,
}
}
}
- 引入
[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;
};
}
主题样式应用
-
在入口文件中调用
adaptMultipleThemes
-
在样式中使用
@include mixDarkTheme
.title {
@include mixDarkTheme(){
color: $linkColor;
}
}
站点配置
每个Page独立配置title
-
给每个页面增加
layout.js
-
在
layout.js
中配置metadata
拆分next.config.mjs
next.config.js
默认仅支持commonjs
规范,若使用ES Module
规范,更改后缀名为.mjs
以.mjs
配置为例进行拆分:
-
拆分需要使用
.mjs
后缀文件 -
.mjs
文件使用ES Module
格式书写 -
引入需要携带
.mjs
后缀
全局依赖
在Next.js中通过
<Script>
引入像Swiper这种全局变量,在使用时会报Swiper is not defined
这种错误。
- 定义类型声明文件
// next-env.d.ts
declare global {
var Swiper: any;
interface Window {
Swiper: any
}
}
declare interface Window {
Swiper: any
}
- 在使用全局变量的地方引入类型声明文件
import { Swiper } from '@/types/globals';
// 底屏swiper
useEffect(() => {
const myswiper = new Swiper('.swiper-container', {
slidesPerView: 1.05,
spaceBetween: 16
});
return () => {
myswiper.destroy(true);
};
}, []);
环境变量
CLI
- 通过命令行配置运行环境
// 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",
},
-
next.config.js#env
读取环境变量写入代码-
process.env.*
只能node环境使用; -
next.config.js#env
会直接打入包内,server
、client
都可以使用;
-
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];
-
- 通过
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 ...
的缓存特性,多次引用,只有第一次引用会执行代码,后续使用的是对象引用,而非拷贝。
- 消费者调用
// app/page.tsx
import { domains } from '@/constants/domains.mjs'; //必须携带mjs后缀
console.log(domains.Host)
Good to knows
.env文件
-
带Next_Public_开头的变量,服务端、客户端都可以访问;
-
不带Next_Public_开头的变量,服务端可以访问,客户端无法访问;
-
通过
.env
文件配置的环境变量无法自定义环境关键字; -
通过
.env
文件配置的环境变量不会写入代码中,可考虑存储敏感信息(待验证)
读取环境变量写入代码有三种方案:
-
env配置,具体如上
const nextConfig = withImages( ... env: { RUN_ENV: process.env.RUN_ENV }, }) );
-
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; } );
-
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:
useReducer可以管理复杂的、具有多个相关状态的组件状态;
如不想通过useState定义多排状态,可以考虑useReducer替代;
可以进行新旧状态数据对比,类同
useState((prev) => {})
;useReducer & useContext实现多级状态同步;
- 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;
- Provider调用
<RootProvider>
<HeaderNavigation
above
defaultTheme={ThemeEnum.light}
themeChangable
transparent
/>
{children}
<FooterSiteMap />
</RootProvider>
- Consumer调用
// 使用时,通过useCallback、useMemo承接,创建缓存,避免引发无限渲染循环;
const updateScroller = useCallback(() => {
updateContext({
type: ActionsEnum.setScroller,
payload: scrollContainerRef
});
}, [updateContext, scrollContainerRef]);
useEffect(() => {
updateScroller();
}, [updateScroller]);
无限渲染循环
有时可能会遇到无限循环的问题。这通常是因为在使用时,依赖项没有正确设置,导致组件在每次渲染时都会重新创建新的上下文。
使用上下文时,需要通过useCallback、useMemo承接,组件使用memo()包裹,创建缓存,避免引发无限渲染循环
降级CSR
将Next应用打包为CSR,在服务崩溃时,通过Nginx负载均衡()指向客户端渲染。
- 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/deploying/static-exports#unsupported-features
-
部署
-
部署SSR
-
部署CSR
-
-
宕机自动降级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
- 静态资源加载失败
查看
next.config#assetPrefix
字段配置,该配置会影响js/css静态资源加载路径。
- 跳转失败,404
程序内的页面跳转需要使用next内置的组件
next/link
,使用a
无法正常跳转。
参考文档
https://juejin.cn/post/7338280070304809010
https://blog.csdn.net/javaboyweng/article/details/97612605
浏览器兼容
ES6+垫片
-
安装相关依赖
npm i -S core-js regenerator-runtime npm i -D @babel/core @babel/preset-env
-
根文件引入垫片
// src/app/layout.tsx import 'core-js' import 'regenerator-runtime/runtime' // 支持async、await
-
配置
.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": [] }
-
配置next.config.mjs
const nextConfig = withImages(
...
webpack: (config, options) => {
config.plugins.push(new options.webpack.DefinePlugin({
'process.env': JSON.stringify(process.env)
}));
return config;
}
);
-
配置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