背景
因主技术栈使用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的简单对比
CSR | SSR | SSG | |
---|---|---|---|
运行端 | 浏览器 | 服务器 | 服务器 |
静态文件 | 单页面 | 由服务器即时生成 | 多个页面 |
SEO | 不适合 | 适合 | 适合 |
静态文件CDN | 适合 | 不适合 | 适合 |
适用场景 | 活动页面、中后台产品、对SEO无要求的页面 | 信息展示型网站、对SEO有要求的页面 | 内容较为固定的资讯类网站 |
先说明依赖包版本
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目录,官方的规则如下:
- 如果根目录下有 pages,则src/pages 将被忽略
- 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 (
<>
<Head>
<title>页面标题</title>
</Head>
<Component {...pageProps} />
</>
)
}
运行项目,发现页面的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 (
<Html>
<Head />
<body>
<Main />
<NextScript />
<Script
src="***.js"
strategy="beforeInteractive"
></Script>
</body>
</Html>
);
}
}
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: {} }),
},
};
tailwindcss 是个好东西,请参考后续文章。
配置全局样式
新建 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 文件,重新启动项目,即可访问 http://localhost:3000/about
next/link
Next.js 项目内的页面跳转使用<Link>
, 该组件会预先加载引用的页面。如果是项目外部链接,则使用<a>
next/router
Next.js 自带的router,通过 useRouter 来获取当前页面的pathname等信息
图片引用
使用原生标签引入图片
本地图片资源,存放在%20public/img/***
%20下,图片会跟随项目进行打包,图片根路径地址与项目跟路径地址一致
引用方式一
<img
%20%20src={`${publicRuntimeConfig.basePath}/img/modelTitle/moreIcon.png`}
width="5"
height="9"
alt=""
/>
publicRuntimeConfig.basePath
是根据当前环境,获取的 basePath 路径
使用 next/image 引用图片
Next.js 自带的<Image>
可以认为是<img>
的升级版,提供了非常方便的尺寸适配,加载等属性,会根据客户端的情况,进行图片的动态优化处理,但也会自动增加很多样式,会影响原生的<img>
样式,所以要根据情况使用。
如果要使用,需要在<Image>
包裹一个父容器,并为父容器定义样式。<Image>
会自动适配父容器的大小,因为可以不用为<Image>
特意设置宽高。需要注意的是,如果使用<Image>
加载图片,因为 Next.js 的安全机制,还需要在 next.config.js文件中设置图片域属性,例如设置图片域名属性 qq.com
module.exports = {
images: {
domains: ['qq.com'],
path: `${basePath}/_next/image`,
},
}
项目中开始部分网络图片采用<Image>
加载,Next 的确会做懒加载等处理,但是在安卓低端机下存在兼容问题,无法展示,所以后来统一使用原生<img>
标签进行加载。
接口请求
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';
// 使用
<img src={demoImg.src} alt="" />
项目构建,执行 npm run build
后,会在项目目录下生成 build
文件夹,该文件夹下就是项目的构建结果。其中 build/static/media
文件夹存放的是项目引用的图片,并且文件名已经经过hash 处理,可通过工具上传至CDN目录。
参考资料
- Next.js
- 必会的next.js自定义App和Document - 编程
- 使用 React 和 Next.js 构建博客 - 编程
- SSR 实战:官网开发指南 - 祯民 - 编程小册