背景
因主技术栈使用Vue,部分服务端渲染的项目就自然的选择了使用 Nuxt.js 框架进行开发。近期在尝试进行技术栈更新,所以选择使用 Next.js进行页面开发。目前采用 Next.js 12.13.1 版本,官方最近版本迭代到了 Next.js 13.4
Next.js 是基于React 的服务端渲染框架。
在传统的SPA项目中,例如使用 Vue-cli 创建的项目,最终build生成的静态文件,是基于浏览器渲染的,即所谓的 CSR (Client-side Rendering)。
CSR往往都是单页面应用,即一个HTML文件和若干个js、css文件。打开build后的HTML文件,发现代码很简单,页面和组件的元素都是放在了js里,由js动态渲染到HTML中。CSR模式是目前前端开发项目中应用最为广泛的。
但有些也场景,特别是需要SEO优化的时候,CSR就不太合适了,所以服务端(Server-side Rendering))渲染应运而生,SSR是由服务器将用户请求的页面DOM组装好后,再返回给浏览器,因此通过“查看网页源代码”,是可以看到完整的页面DOM的。
而SSG (Static Site Generation)顾名思义就是静态网站生成,也就是常说的“网页静态化”,除了适合SEO,还很方便CDN加速,比较适合内容相对比较固定的资讯发布类网站。
以下是关于CSR、SSR、SSG的简单对比
先说明依赖包版本
Node.js 16.17.0
next.js 12.13.1
react 18.2.0
react-dom 18.2.0
axios 0.27.2
sass 1.55.0
redux 4.2.0
创建Next.js项目
首先我们先对 Next.js 的项目进行初始化,Next.js 提供了脚手架来帮助我们初始化项目,我们可以执行下面的命令来初始化项目:
npx create-next-app@latest --typescript
其中 next-env.d.ts 是 Next.js 的类型文件,可以保证 ts 选择 Next.js 相关的类型,通常我们不需要对它进行修改,可以在提交后加到 .gitignore 中。next.config.js 是 Next.js 的构建配置,底层也是基于 Webpack 去打包的,我们可以在默认的配置上加上下面的配置来提供别名的能力。
// next.config.js
const path = require("path");
module.exports = {
reactStrictMode: true,
swcMinify: true,
webpack: (config) => {
config.resolve.alias = {
...config.resolve.alias,
"@": path.resolve(__dirname),
};
return config;
},
};
tsconfig.json 中我们也需要加一下对应的别名解析识别(baseurl , paths)。
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
到这里项目其实就已经初步初始化完成了,我们执行npm run dev打开http://localhost:3000 就可以看到一个 Next.js 的默认服务器端渲染页面
配置项目
设置路径别名
Next的官方脚手架没有src目录,但是考虑src目录是普遍存在大多数脚手架工程中,所以Next也对src目录做了支持,我们可以新建一个 src 目录,将 pages、styles、api 目录移动到src 目录里,在执行 npm run dev,项目依旧正常运行
关于src目录,官方的规则如下:
1.如果根目录下有 pages,则src/pages 将被忽略
2.public 目录以及 next.config.js、tsconfig.json 等配置相关文件,不能放在src目录里
修改 tsconfig.json 别名解析
"paths": {
"@/*": ["src/*"]
}
这样在代码中引用 src 目录下的文件,可以直接使用 @表示src目录,不需要 "../" 了。
修改 tsconfig.json 需要重启项目才能生效。
按照以上目录设置后,项目的入口文件变为 src/pages/_app.js。
配置 SourceMap
development 环境是开启sourceMap的,production 环境默认不开启。如果需要在prod 环境中开启,可以在next.config.js 中进行配置,为了不暴露项目源码,不建议进行配置
module.exports = {
productionBrowserSourceMaps: !!isDev,
}
设置页面Title
设置页面的title很简单,修改 src/pages/_app.js
import '../styles/globals.css' // 引入全局样式
import Head from 'next/head';
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return (
<>
页面标题
>
)
}
运行项目,发现页面的title 已经修改成功
设置HTML框架代码
新建 src/pages/_document.js,代码如下:
import Document, { Html, Head, Main, NextScript } from 'next/document'; import Script from 'next/script'; class MyDocument extends Document { render() { return (
); } } MyDocument.getInitialProps = async (ctx) => { const initialProps = await Document.getInitialProps(ctx); return { ...initialProps }; }; export default MyDocument;
_document.js 也是Next.js 的指定文件名,且必须在pages目录下才生效。
那么 _document.js 和 _app.js 有什么区别呢?
自定义 App,可以对App 组件进行重构
- 页面切换之间保持布局的持久化
- 切换页面时保持状态(state)
- 使用 componentDidCatch 自定义错误处理
- 向页面(pages)注入额外的数据
- 添加全局CSS
自定义Document
只有在服务端渲染的时候才会被调用,主要用来修改服务端渲染的文档内容,通常服务端渲染会使用一些 css-in-js 库,它在_document.js 中定义。
在next/document中提供的并不仅是Document组件,还有一些跟HTML标签对应的组件,在重写的时候要记得都要写上。
官方不建议把 title 放到_document.js中,如果在_document.js 中的head 设置了title,在build 的时候会收到 warning。所以我们将 head 的内容放在 _app.js 中设置。
设置错误页面
Next.js 自带了 404、500页面,但是比较简陋,我们可以自定义页面覆盖掉自带错误页面
可以自定义 404.tsx、500.tsx或者 _error.tsx。
环境变量
- .env.development
- .env.production
- .env.test
CSS预处理及使用
集成Sass
Next.js 允许你导入(import)具有 .scss 和 .sass 扩展名的 Sass 文件。 你可以通过 CSS 模块以及 .module.scss 或 .module.sass 扩展名来使用组件及的 Sass。
npm install sass
Postcss 配置
为了适配移动端,采用 px-to-vw 方案,项目中引入
npm install postcss postcss-px-to-viewport-8-plugin autoprefixer css-loader cssnano tailwindcss --save-dev
新增 postcss.config.js
module.exports = {
plugins: {
'postcss-px-to-viewport-8-plugin': {
// 视窗的宽度,对应的是我们设计稿的宽度,我们公司用的是750
viewportWidth: 750,
// 指定`px`转换为视窗单位值的小数位数
unitPrecision: 3,
// 指定需要转换成的视窗单位,建议使用vw
viewportUnit: 'vw',
// 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
selectorBlackList: ['.ignore'],
// 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
minPixelValue: 1,
// 允许在媒体查询中转换`px`
mediaQuery: false,
// exclude: undefined
},
// 自动补全
autoprefixer: {
overrideBrowserslist: [
'Android 4.1',
'iOS 7.1',
'Chrome > 31',
'ff > 31',
'ie >= 8',
],
grid: true,
},
tailwindcss: {},
...(process.env.NODE_ENV === 'development' ? {} : { cssnano: {} }),
},
};
配置全局样式
新建 src/styles/globals.scss,内部进行样式初始化。然后在入口文件引入 globals.scss。参考_app.tsx
配置页面样式
需要注意的是,在Next.js 项目中,组件的代码中是无法引入全局样式的,对于组件自身的样式,只能使用 CSS Module 来引用,即文件名为 ***.module.scss。否则会报错。组件中的className,也需要按CSS Module的方式进行设置。
注意一下,这里使用 CSS Module 的方式加载样式,因此生成的HTML中,className 会自动加上随机字符串后缀,对应的css也会自动添加相应的字符串,正因为 CSS Module 这个机制,同名样式互相污染的问题也就不存在了。
页面路由
基于文件系统的路由
Next.js 的优势是 基于文件系统的路由,对应的只需要在 src/pages 下创建的对应页面即可。
在pages/ 下创建 /about.tsx 文件,重新启动项目,
next/link
Next.js 项目内的页面跳转使用, 该组件会预先加载引用的页面。如果是项目外部链接,则使用
next/router
Next.js 自带的router,通过 useRouter 来获取当前页面的pathname等信息
图片引用
使用原生标签引入图片
本地图片资源,存放在 public/img/*** 下,图片会跟随项目进行打包,图片根路径地址与项目跟路径地址一致
引用方式一

publicRuntimeConfig.basePath 是根据当前环境,获取的 basePath 路径
使用 next/image 引用图片
Next.js 自带的 的升级版,提供了非常方便的尺寸适配,加载等属性,会根据客户端的情况,进行图片的动态优化处理,但也会自动增加很多样式,会影响原生的
样式,所以要根据情况使用。
如果要使用,需要在
module.exports = {
images: {
domains: ['qq.com'],
path: `${basePath}/_next/image`,
},
}
项目中开始部分网络图片采用 标签进行加载。
接口请求
CSR/SSR/SSG三种API请求方式
在Next.js 项目中,API请求有三种方式。但是根据项目的部署方式,最多可以同时有两种,即:
- CSR+SSR
- CSR+SSG
CSR 的API请求,就是常规的前端项目中的请求方式,即:由客户端浏览器发起请求,拿到数据后渲染到页面
SSR的API请求,是由服务端(Next.js的Node.js)发起请求,拿到数据后,组装到HTML里,然后将组装好的HTML返回给客户端浏览器
SSG的API请求,于SSR的API请求类似,也是由服务发起请求并把数据组装到HTML里,然后进行静态化输出。 但由于是完全静态化的,所以当API数据发生变化时,必须重新静态化才能更新页面。
getInitialProps (SSR)
getInitialProps是在渲染页面之前就会运行的API。 如果该路径下包含该请求,则执行该请求,并将所需的数据作为props传递给页面。
- 只能在pages文件夹内的文件中使用。getInitialProps是SSR专用的API,这是误解。
- 直接访问后,getInitialProps将在服务器端运行。
- 使用next/link进行客户端路由时,在客户端执行。
getServerSideProps (SSR)
getServerSideProps 每次访问时请求数据
- 方法只会在服务端运行,每次请求都运行一遍 getServerSideProps 方法
- 如果页面通过浏览器端Link组件导航而来,Next会向服务端发一个请求,然后在服务端运行getServerSideProps方法,然后返回JSON到浏览器
- getServerSideProps 方法主要是升级了9.3之前的 getInitialProps 方法。9.3之前的 getInitialProps 方法有一个很大的缺陷是在浏览器中req和 res 对象会是 undefined 。也就是使用它的页面,如果是浏览器渲染你需要在组件内再显示地请求一次,开发体验不太好
getStaticProps(SSG)
所谓的SSG也就是静态站点生成,在build阶段将页面构建成静态的html文件,这样线上直接访问HTML文件,性能极高。
- 不会在客户端上运行,始终在服务器端运行
- getStaticProps 是用于在构建时预先执行 getInitialProps 进行的处理并预先生成静态文件的API
- 如果是动态路由的页面,使用 getStaticPaths 方法来返回所有的路由参数,以及是否需要回落机制
getStaticPaths (SSG)
用于在使用动态路由时生成静态文件。
- getStaticPaths 方法返回的fallback很有用:如果fallback是false,访问该方法没有返回的路由会404
- 但是如果不想或者不方便在build阶段拿到路由参数,可以设置fallback为true,Next在访问build中没有的动态路由时候,先浏览器loading,然后服务端开始build该页面的信息,然后再返回浏览器渲染,再次访问该路由该缓存就会生效,很强大!!!
- 静态缓存目前没办法很灵活的更新!例如内容在build或者fallback生效之后发生更改,目前没办法很方便的替换缓存。
如何选择SSR还是SSG?
- 如果页面内容真动态(例如,来源数据库,且经常变化), 使用getServerSideProps方法的SSR。
- 如果是静态页面或者伪动态(例如,来源数据库,但是不变化),可以酌情使用SSG。
axios
使用 axios,并进行的简单的封装,进行了请求拦截及响应拦截。
开发过程中的优化方案
上面提到的本地图片引用,因为项目采用Docker构建,图片如果跟随项目一起打包,每次更新上线后,图片的缓存会失效,页面会重新加载图片,体验上很不好,后来修改为将本地图片上传至CDN。需修改配置文件中的 assetPrefix 配置为CDN文件路径,需结合next-compose-plugins,next-optimized-images 使用
修改页面图片引用方式
npm install --save next-compose-plugins next-optimized-images
// next.config.js
const withPlugins = require('next-compose-plugins');
const optimizedImages = require('next-optimized-images');
const nextConfig = {
assetPrefix: isDev ? '' : 'https://图片cdn地址',
distDir: 'build', // 项目文件打包路径
basePath: '',
sassOptions: {
includePaths: [path.join(__dirname, 'src/styles')],
},
...
}
module.exports = withPlugins([optimizedImages], nextConfig);
module.exports = nextConfig;
修改图片的目录,原目录在public/img/*** 下,修改为src/assets/images/***
页面/组件引用方式修改
// 导入本地图片
import demoImg from '@/assets/image/demoImg.png';
// 使用
项目构建,执行 npm run build 后,会在项目目录下生成 build 文件夹,该文件夹下就是项目的构建结果。其中 build/static/media 文件夹存放的是项目引用的图片,并且文件名已经经过hash 处理,可通过工具上传至CDN目录。
Next.js 实践记录
原文链接:
https://juejin.cn/post/7232596270551711799