Next.js 实践记录

lxf2023-05-15 00:49:18

背景

因主技术栈使用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的简单对比

CSRSSRSSG
运行端浏览器服务器服务器
静态文件单页面由服务器即时生成多个页面
SEO不适合适合适合
静态文件CDN适合不适合适合
适用场景活动页面、中后台产品、对SEO无要求的页面信息展示型网站、对SEO有要求的页面内容较为固定的资讯类网站

Next.js 实践记录 先说明依赖包版本

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.js 实践记录

其中 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.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 (
    <>
      <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页面,但是比较简陋,我们可以自定义页面覆盖掉自带错误页面

Next.js 实践记录

可以自定义 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等信息

图片引用

使用原生Next.js 实践记录标签引入图片

本地图片资源,存放在%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请求有三种方式。但是根据项目的部署方式,最多可以同时有两种,即:

  1. CSR+SSR
  2. 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-pluginsnext-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 实战:官网开发指南 - 祯民 - 编程小册
本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!