×

SEO Next.js SSR

手把手教你搭建SEO项目:利用Next.js SSR实战,小白入门教学

元智汇电子 元智汇电子 发表于2023-11-12 10:37:42 浏览794 评论0

抢沙发发表评论

引言

你是否好奇浏览器是如何渲染一个页面的呢?

image.png

浏览器渲染网页的过程可以简单分为以下几个步骤:

  1. HTML解析: 在进行HTML解析之前,浏览器进行DNS解析和TCP握手等网络协议操作,与用户要访问的域名服务器建立连接。域名服务器返回一个HTML文本,用于后续的渲染(这一点非常关键,请注意)。

  2. 渲染树构建: 浏览器客户端接收到服务端返回的HTML文本后,对HTML文本进行解析。DOM用于生成DOM树,决定页面的布局结构,而CSS则生成CSSOM树,决定页面元素的样式。在这个过程中,如果遇到脚本或静态资源,会执行预加载以提前请求静态资源,最终形成一个渲染树。

    image.png

  3. 布局: 浏览器在获得渲染树后进行布局操作,以确定页面上每个对象的大小和位置,然后进行渲染。

  4. 渲染: 电脑视图是通过GPU的图像帧显示的。渲染的过程是将获得的渲染树转换为GPU的图像帧来显示。浏览器首先进行栅格化,将页面按行列划分成对应的层,例如12栅格。然后根据布局树的位置得到一个合成帧,包括文本、颜色、边框等。最终将合成帧升级到GPU的图像帧,显示在页面上,从而呈现在我们的电脑上。

服务器端渲染对C端网站的优势主要在于它获得的HTML不同。这样的差异会为Web应用带来不同的表现。如果你想深入了解搭建SEO项目以及Next.js SSR实战,我们将在下文中为小白提供详尽的入门教学。


SSR 优势

易传播性:提高搜索引擎匹配准确度

搜索引擎可以被视为一种爬虫,负责爬取指定页面的 HTML,并根据用户输入的关键词对页面内容进行排序和检索,最终呈现给用户。在页面渲染过程中,对于高级爬虫,它们可能会等待页面渲染完成后提取数据并进行关键词匹配。相反,低级爬虫则可能直接从服务器拉取HTML。在这一过程中,服务器端拉取的HTML中包含的实际页面关键词和数据越多,搜索引擎匹配的精度也越高。

image.png

SSR在服务器端完成对页面数据的请求,将相应数据注入DOM并一同返回,产生一个包含实际数据的完整可预览的HTML。以掘金首页为例,服务器端拉取的HTML包含即将展示的实际数据。

image.png

相对于客户端渲染,SSR的数据拉取过程在服务器端完成,而客户端渲染则在客户端执行。客户端渲染页面的服务器响应的HTML通常是一个包含执行脚本但不包含实际数据的空HTML,这也可参考B端管理平台的HTML响应。由于服务器端渲染和客户端渲染存在差异,决定了在爬虫关键词爬取的精准度上,服务器端渲染明显优于客户端渲染,使站点更容易在相关关键词排名上获得更高的位置。

交互稳定性:提升效率

在交互稳定性方面,SSR表现更为高效。客户端渲染需要在执行脚本后请求数据,而SSR在服务器端已完成数据请求,因此具有更短的首屏时间。这使得SSR在维持交互稳定性方面更具优势,因为无需等待脚本执行和数据请求,可以更快地呈现页面内容,提高用户体验并确保页面加载效率。


项目初始化

  • 创建项目

    yarn create next-app --typescript

image.png


配置别名

// 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": {    // ...其他设置    "baseUrl": ".",    "paths": {      "@/*": ["src/*"],      "@public/*": ["public/*"]    }  },  // ...其他设置}

目录结构

image.png

项目代码地址


ESLint


  • 安装依赖

yarn add -D eslint eslint-plugin-react @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-simple-import-sort npx eslint --init

image.png

  • 创建 .eslintrc.js

// .eslintrc.js

module.exports = {  env: {    browser: true,    commonjs: true,    es2021: true,    node: true,  },  extends: [    

"eslint:recommended",    

"plugin:react/recommended",    

"plugin:@typescript-eslint/recommended",    

"plugin:import/recommended",  ],  parser: "@typescript-eslint/parser",  parserOptions: {    ecmaFeatures: {      jsx: true,    },    ecmaVersion: "latest",  },  plugins: ["react", "@typescript-eslint", "eslint-plugin-simple-import-sort"],  rules: {    

// ...其他规则  }, };

在原有配置的基础上,添加对 commonjs 全局变量的识别。

  • React 17 不再需要显式引入 React

    在 React 17 之后,JSX 的页面已经不再需要显式引入 React,因此我们去掉了 react/jsx-uses-react 和 react/react-in-jsx-scope 这两个 lint 规则。

  • 禁用对 require 的使用

    为了避免使用 require 来定义,我们禁用了 @typescript-eslint/no-var-requires 这个规则。考虑到 Node.js 中很多相关的依赖没有对 ES Module 的定义,我们做了这个调整。

  • 关闭组件定义缺少显示名称的警告

    我们关闭了 react/display-name 规则,以消除组件定义缺少显示名称的警告。在某些情况下,显示名称可能并非必需,因此我们根据实际情况进行了调整。

  • 使用 eslint-plugin-simple-import-sort 进行 import 排序

    为了提高代码的可维护性,我们引入了 eslint-plugin-simple-import-sort,以实现 import 语句的自动排序。这样可以确保导入语句的一致性,使代码更易于理解。

  • 处理重复导入问题

    通过安装 eslint-plugin-import 插件,并添加 plugin:import/recommended,我们关闭了 no-duplicate-imports 规则,以自动修正 import 重复导入的问题。

  • 关闭无法解析导入的警告

    为了消除无法解析导入的警告,我们关闭了 import/no-unresolved 规则。这有助于避免在项目中出现导入路径无法解析的问题。


Commit

  • 安装依赖

yarn add -D @commitlint/config-conventional @commitlint/cli

  • 新增 commitlint.config.js

module.exports = {  extends: ["@commitlint/config-conventional"],  rules: {    

"type-enum": [2, "always", ["feat", "fix", "revert"]],    

"subject-max-length": [1, "always", 30],  }, };

其中,type-enum 指定了 commit 正文的前缀,通常使用三种类型:Feat、Fix、Revert。subject-max-length 对应实际 commit 长度(不包括前缀),这里设置为30。


Husky

  • 安装依赖

yarn add -D husky npx husky install npx husky add .husky/pre-commit

  • .husky/pre-commit

#!/usr/bin/env sh.

"$(dirname -- "$0")/_/husky.sh"

npx commitlint --edit $1

npm run lint


Prettier

  • 在 VSCode 安装 Prettier - Code formatter 插件。

  • 安装依赖


yarn add -D prettier eslint-plugin-prettier

  • 创建 .prettierrc.js

module.exports = {  singleQuote: false,  trailingComma: "all",  printWidth: 80,  htmlWhitespaceSensitivity: "ignore", };

  • .vscode/settings.json

{  "editor.formatOnSave": true,  "editor.defaultFormatter": "esbenp.prettier-vscode",  "editor.codeActionsOnSave": {    "source.fixAll.eslint": true,    "source.fixAll.stylelint": true  },  "stylelint.validate": ["css", "less", "scss", "vue"],  "[scss]": {    "editor.defaultFormatter": "esbenp.prettier-vscode"  }}

  • 添加 .editorconfig 定义编码风格

# top-most EditorConfig file

root = true

[*]

indent_style = space

indent_size = 2

end_of_line = lf

charset = utf-8

trim_trailing_whitespace = false

insert_final_newline = false


Stylelint

  • 安装依赖

yarn add -D stylelint stylelint-config-clean-order stylelint-config-prettier stylelint-config-standard stylelint-config-standard-scss stylelint-prettier

  • 创建 stylelint.config.js

module.exports = {  processors: [],  extends: [    

"stylelint-config-standard-scss",    

"stylelint-config-standard",    

"stylelint-prettier/recommended",    

"stylelint-config-prettier",    

"stylelint-config-clean-order",  ],  rules: {    

// ...其他规则  }, };

仓库地址


部署和打包环境搭建

在项目开发过程中,为了及时发现潜在问题并减少上线后的并发问题,定期进行静态文件的打包和本地预览是一种有效的做法,尤其在引入组件库之前更显得重要。

  • 安装 cross-env

为了区分不同的环境变量,我们使用 cross-env 这个工具,它可以在不同平台上设置和使用环境变量的脚本。


yarn add -D cross-env

  • 配置环境变量

添加 .env.development、.env.test、.env.production 文件,并写入相应的配置。注意,为了在浏览器环境中访问这些变量,需要添加前缀 NEXT_PUBLIC_,否则打包后将无法在页面中访问。


NEXT_PUBLIC_HOST = https://junfeng530.xyz

  • 添加打包脚本

在 package.json 中添加以下脚本命令,分别用于测试环境和生产环境的打包。

"scripts": {  "build": "cross-env NODE_ENV=test next build",  "export:test": "cross-env NODE_ENV=test next build && next export",  "export:prod": "cross-env NODE_ENV=production next build && next export"

}

  • 安装 http-server

全局安装 http-server 用于本地预览静态文件。

npm install -g http-server

  • 查看静态文件

运行以下命令进行静态文件的导出和本地预览:

yarn export:test

cd out http-server


样式、模块化代码提示

  • 安装 sass 依赖,注意如果使用 Next.js 版本超过 13.1.1,可能会出现报错,因此需要将 Next.js 版本锁定在 13.1.1。

yarn add -D sass

  • 在 next.config.js 中配置自定义页面扩展名,指定项目将打包指定后缀的文件作为页面。

const path = require('path');

module.exports = {  

pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'],  

reactStrictMode: true,  

swcMinify: true,  

webpack: (config) => {    config.resolve.alias = {      ...config.resolve.alias,      

'@': path.resolve(__dirname),    };    

return config;  }, };

  • 将文件名从 index.ts 修改为 index.page.tsx,_app.tsx 修改为 _app.page.tsx。


修改 index.page.tsx 文件:

// @/pages/index.page.tsx

import styles from './home/index.module.scss';

export default function Home() {  

return <div className={styles.home}>官网实战</div>; }

在 .eslintrc.js 中关闭组件缺少 DisplayName 的规则:

'react/display-name': 'off'

  • 修改页面目录结构

将首页仍保留为 index.page.tsx,使用 home 目录下的文件,每个页面都包括 api.ts、index.module.scss、index.page.tsx、components 等文件。

  • 添加样式代码提示

在页面中只能使用 cssModule 的方式引入样式,全局样式放到 @/styles 文件目录下,并在 _app.tsx 中引入。

  • 安装 VSCode 插件以获得 CSS Modules 的代码提示。


在 next.config.js 中修改配置以兼容驼峰风格:

const path = require("path");

const nextConfig = {  

pageExtensions: ["page.tsx", "page.ts", "page.jsx", "page.js"],  

reactStrictMode: true,  

images: {    

loader: "akamai",    

path: "/",  },  

webpack: (config) => {    config.resolve.alias = {      ...config.resolve.alias,

     "@": path.resolve(__dirname),  

 };    

const rules = config.module.rules      .find((rule) => typeof rule.oneOf === "object")      .oneOf.filter((rule) => Array.isArray(rule.use));    rules.forEach((rule) => {      rule.use.forEach((moduleLoader) => {

       if (

         moduleLoader.loader !== undefined &&          moduleLoader.loader.includes("css-loader") &&

         typeof moduleLoader.options.modules === "object"        ) {          moduleLoader.options = {            ...moduleLoader.options,

           modules: {              ...moduleLoader.options.modules,

             exportLocalsConvention: "camelCase",            },          };        }      });    });

   return config;  }, };module.exports = nextConfig;

在引入样式时,使用 styles. 将触发代码提示。

image.png


响应式布局配置

  • 安装 postcss-px-to-viewport

使用以下命令安装 postcss-px-to-viewport,这是一个用于将 px 单位转换为视口单位的 PostCSS 插件。

yarn add -D postcss-px-to-viewport

  • 配置 postcss.config.js

在项目根目录下创建 postcss.config.js 文件,并添加以下配置,以实现对 px 单位的转换:

module.exports = {

 plugins: {

   "postcss-px-to-viewport": {

     unitToConvert: "px",

     viewportWidth: 1920,

     unitPrecision: 5,

     propList: ["*"],

     viewportUnit: "vw",

     fontViewportUnit: "vw",

     selectorBlackList: [],

     minPixelValue: 1,

     mediaQuery: false,

     replace: true,

     exclude: undefined,

     include: undefined,

     landscape: false,

     landscapeUnit: "vw",

     landscapeWidth: 568,    },  }, };


媒体查询

对于一些小尺寸的弹窗等元素,我们可能不希望进行响应式布局。可以利用 postcss 的 mediaQuery 特性,通过添加媒体查询来避免对 vw 的转换。比如:

@media (min-width: 1px) {

 /* your styles here */

}

其他移动端适配的媒体查询则按照常规方式添加即可。


设备判断

为了更精准地判断设备,推荐使用 react-use 库中的 useWindowSize 钩子,结合用户代理字符串来判断设备类型。

  • 安装 react-use

yarn add react-use

  • 封装 useDevice 钩子

创建 useDevice.ts 文件,封装用于设备判断的自定义钩子:

import { useEffect, useState } from "react";

import { useWindowSize } from "react-use";

export const useDevice = () => {

 const [isMobile, setMobile] = useState(true);

 const size = useWindowSize();

 useEffect(() => {

   const userAgent =

     typeof window.navigator === "undefined" ? "" : navigator.userAgent;

   const mobile =

     /Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.test(        userAgent,      );    setMobile(size.width <= 750 || mobile);  }, [size.width]);

 return {    isMobile,  }; };

  • 在页面中使用

在页面中引入该钩子,并根据设备类型进行不同的布局:

import { useDevice } from "@/hooks/useDevice";

const YourComponent = () => {

 const { isMobile } = useDevice();

 return (

   <>      {!isMobile && <div>pc端布局</div>}      {isMobile && <div>移动端布局</div>}

   </>  ); };


引入 Ant Design

  • 安装 Ant Design

使用最新版本的 Ant Design(5.0),该版本采用 CSS-in-JS,本身支持按需加载,不再需要 babel-plugin-import 插件。只需安装依赖,然后引入即可。详细信息请查阅 Ant Design 官方文档

yarn add antd

为了在旧浏览器上保持样式兼容性(例如在安卓微信中打开时),您可以使用 @ant-design/cssinjs 的 StyleProvider 来避免性能下降操作。

  • 在 _app.page.tsx 中引入 Ant Design

import type { AppProps } from "next/app";

import "@/styles/globals.scss";

import { StyleProvider } from "@ant-design/cssinjs";

import { ConfigProvider } from "antd";

import zhCN from "antd/locale/zh_CN";

export default function App({ Component, pageProps }: AppProps) {

 return (

   <StyleProvider hashPriority="high">      <ConfigProvider locale={zhCN}>        <Component {...pageProps} />      </ConfigProvider>    </StyleProvider>  ); }

如果需要引入 Ant Design 默认样式文件,可以选择在 _app.page.tsx 中添加以下引入:

import 'antd/dist/reset.css';

在页面中使用 Ant Design 组件

例如,在 index.page.tsx 中使用 Ant Design 按钮:

import { Button } from "antd";

<Button type="primary">Ant Design 按钮</Button>

修复 Ant Design 5.0 在打包 test 环境下的样式丢失

在 _document.page.tsx 中进行修改,具体解决方案请参考这里

import { createCache, extractStyle, StyleProvider } from "@ant-design/cssinjs";

import Document, {

 DocumentContext,

 Head,

 Html,

 Main,

 NextScript, } from "next/document";

export default class MyDocument extends Document {

 static async getInitialProps(ctx: DocumentContext) {

   const cache = createCache();

   const originalRenderPage = ctx.renderPage;    ctx.renderPage = () =>      originalRenderPage({

       enhanceApp: (App) => (props) =>          (            <StyleProvider cache={cache}>              <App {...props} />            </StyleProvider>          ),      });

   const initialProps = await Document.getInitialProps(ctx);

   return {      ...initialProps,

     styles: (

       <>          {initialProps.styles}

         <style            data-test="extract"            dangerouslySetInnerHTML={{ __html: extractStyle(cache) }}          />        </>      ),    };  }  render() {

   return (

     <Html lang="en">        <Head />        <body>          <Main />          <NextScript />        </body>      </Html>    );  } }


引入 Ant Design Mobile

  • 安装 Ant Design Mobile

yarn add antd-mobile

  • 配置 next.config.js

为了支持 Ant Design Mobile 的引入,可以通过两种方式配置 next.config.js。您可以选择其中一种方式,两者效果相同。

方式一:添加实验性配置,但可能会出现一些警告。

module.exports = {

 experimental: {

   transpilePackages: ["antd-mobile"],  },

 // 其他 Next.js 配置};

方式二:建议使用 next-transpile-modules 插件。

yarn add -D next-transpile-modules

修改 next.config.js:

const withTM = require('next-transpile-modules')(['antd-mobile']);

module.exports = withTM({

 // 其他项目中的 Next.js 配置

});

去除 postcss-px-to-viewport 对 Ant Design Mobile 的转换

在 postcss.config.js 中添加排除规则,以防止对 Ant Design Mobile 的样式进行 px 到 viewport 单位的转换。

module.exports = {

 plugins: {

   "postcss-px-to-viewport": {

     // 其他配置...      exclude: [/antd-mobile/],    },  }, };

在页面中引入 Ant Design Mobile 组件

在 index.page.tsx 中引入 Ant Design Mobile 的按钮组件:

import { Button as ButtonMobile } from "antd-mobile";

<ButtonMobile size="large" color="primary">  Ant Design Mobile 按钮

</ButtonMobile>


封装 Axios

  • 安装 Axios

yarn add axios

  • 创建请求封装文件 utils/request.ts

import { notification } from "antd";

import type { AxiosError, AxiosRequestConfig } from "axios";

import axios from "axios";

const instance = axios.create({

 timeout: 30 * 1000, });

// 请求拦截

instance.interceptors.request.use(

 (config) => config,

 (error) => Promise.reject(error) );

// 响应拦截

instance.interceptors.response.use(

 (res) => {

   if (      res.data.code !== undefined &&      res.data.code !== 0 &&      res.data.code !== 200 &&      !(res.config as AxiosRequestConfig & { skipErrorHandler?: boolean }).skipErrorHandler    ) {      notification.error({

       message: '异常',

       description: res.data.msg || res.data.message,      });

     return Promise.reject(res.data);    }

   return Promise.resolve(res.data);  },

 (error: AxiosError<{ code: number; message?: string; msg?: string }>) => {

   const { skipErrorHandler } = error.config as AxiosRequestConfig & {      skipErrorHandler?: boolean    };

   if (error.response?.status === 401 && !skipErrorHandler) {

     return;    }

   if (!skipErrorHandler) {      notification.error({

       message: '异常',

       description: error.response?.data?.message || error.response?.data?.msg || error.message,      });    }

   return Promise.reject(error);  } );

type Request = <T = unknown>(  config: AxiosRequestConfig & { skipErrorHandler?: boolean }

) => Promise<T>;

export const request = instance.request as Request;


搭建 Mock 环境

  • 在根目录下创建 mock 文件夹,并添加 data.json 和 routes.json 两个文件。

image.png

{    "indexStore": {        "store": {            "深圳": [                {                    "name": "坂田店",                    "address": "福田街道xxx",                    "marker": [                        114.294773,                        22.587251                    ]                },                {                    "name": "坂田店",                    "address": "福田街道xxx",                    "marker": [                        114.294773,                        22.587251                    ]                }            ],            "广州": [                {                    "name": "天河店",                    "address": "天河街道xxx",                    "marker": [                        114.294773,                        22.587251                    ]                },                {                    "name": "天河店",                    "address": "天河街道xxx",                    "marker": [                        114.294773,                        22.587251                    ]                }            ],            "佛山": [                {                    "name": "好地方店",                    "address": "而得到街道xxx",                    "marker": [                        114.294773,                        22.587251                    ]                },                {                    "name": "好地方店",                    "address": "而得到街道xxx",                    "marker": [                        114.294773,                        22.587251                    ]                }            ]        },        "seo": {            "content": "坂田店、福田街道xxx、天河店、天河街道xxx、好地方店、而得到街道xxx"        }    }}

routes.json:

{    "/api/*": "/$1"

}

  • 安装 json-server

yarn add -D json-server

  • 安装 concurrently:

yarn add -D concurrently

  • 添加命令:

"dev:mock": "concurrently  \"yarn mock\" \"next dev\"",

"mock": "cd ./mock && json-server --watch data.json --routes routes.json --port 4000"

通过同时运行 mock 和 next dev 两个终端,您可以使用 mock 环境进行开发。


获取服务器端接口数据

Next.js 提供了 getStaticProps 方法,允许我们在项目构建时获取服务器端的静态数据。需要注意的是,该方法仅在构建时执行一次,数据必须在发布时进行更新,且仅能在页面级别上使用。

请注意,在实际发布时,应切换至真实数据,而仅在本地调试时使用模拟数据。

  • 创建接口调用文件

在 @/home/api.ts 中,我们可以定义一个获取模拟数据的函数:

import { request } from "@/utils/request";

export interface IMockData {

 store: {    [key: string]: {

     name: string;

     address: string;

     marker: number[];    }[];  };

 seo: string; }

export function fetchMockData() {

 return request<IMockData>({

   url: `${process.env.NEXT_PUBLIC_HOST}/api/indexStore`,

   method: "GET",  }); }

  • 在页面中使用接口数据

在 @/pages/index.page.tsx 中,我们可以调用上述接口,并将数据传递给页面组件:

import { Button } from "antd";

import { Button as ButtonMobile } from "antd-mobile";

import { fetchMockData, IMockData } from "./home/api";

import Layout from "../components/layout";

import styles from "./home/index.module.scss";

export default function Home(props: { mockData: IMockData }) {

 console.log("mockData", props.mockData);

 const headSeo = {

   keywords: "sso、nextjs、antd、jiang",

   description: `seo实践 ${props.mockData.seo}`,

   title: "nextJs 官网 SSR 实战",  };

 return (

   <Layout seo={headSeo}>      <div>        <Button type="primary">antd 按钮</Button>        <ButtonMobile color="primary">antd-mobile 按钮</ButtonMobile>        <div className={styles["home-container"]}>官网实战</div>;

     </div>    </Layout>  ); }export async function getStaticProps() {

 // 获取模拟数据  const res = await fetchMockData();

 const mockData = res;

 return {

   props: { mockData },  }; }


封装通用布局组件

为了提高代码的可维护性,我们可以将一些通用的布局组件进行封装。

image.png

  • Footer 组件

// @/components/footer/index.tsx

import styles from "./index.module.scss";

export default function Footer() {

 return (

   <div id="footer" className={styles["footer-container"]}>      底部

   </div>  ); }

  • HeadSeo 组件

// @/components/headSeo/index.tsx

import Head from "next/head";

export default function HeadSeo(seo: { content: { keywords: string; description: string; title: string } }) {

 return (

   <Head>      <meta charSet="UTF-8" />      <meta        name="viewport"        content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;"      />      <meta name="keywords" content={seo.content.keywords} />      <meta name="description" content={seo.content.description} />      <meta name="robots" content="index, follow" />      <meta name="applicable-device" content="pc,mobile" />      <meta name="format-detection" content="telephone=no" />      <title>{seo.content.title}</title>    </Head>  ); }

  • Layout 组件

// @/components/layout/index.tsx

import Footer from "../footer";

import HeadSeo from "../headSeo";

import Navbar from "../navbar";export default function Layout(props: {  children: React.ReactNode;  seo: { keywords: string; description: string; title: string }; }) {

 return (

   <>      <HeadSeo content={props.seo} />      <Navbar />      <main>{props.children}</main>      <Footer />    </>  ); }

  • Navbar 组件

// @/components/navbar/index.tsx

import styles from "./index.module.scss";

export default function Navbar() {

 return (

   <div id="footer" className={styles["navbar-container"]}>      头部

   </div>  ); }

通过这样的组件封装,我们可以在页面中更清晰地呈现内容,提高代码的可读性和可维护性。

优化图片加载性能是提升网站速度的有效手段之一。

  • 封装 useWebp Hooks (@/hooks/useWebp.ts)

import { useEffect, useState } from "react";

export const useWebp = () => {

 const [isSupportWebp, setIsSupportWebp] = useState(true);

 useEffect(() => {

   if (typeof window !== "undefined") {

     const supportWebp =

       window.document          .createElement("canvas")          .toDataURL("image/webp")          .indexOf("data:image/webp") > -1;

     setIsSupportWebp(supportWebp);    }  }, []);

 return {    isSupportWebp,  }; };

  • 封装 useOss Hooks (@/hooks/useOss.ts)

import { useCallback } from "react";

import { useWebp } from "./useWebp";

export const useOSS = () => {

 const { isSupportWebp } = useWebp();

 const getOssImage = useCallback(

   (option: {      originUrl: string;      notSupportWebpWidth?: number;      notSupportWebpHeight?: number;      width?: number;      height?: number;    }) => {

     let process = "";

     if ((option.notSupportWebpWidth && !isSupportWebp) || option.width) {        process = `w_${option.notSupportWebpWidth || option.width},`;      }      if ((option.notSupportWebpHeight && !isSupportWebp) || option.height) {        process = `${process}h_${option.notSupportWebpHeight || option.height},`;      }      if (process) {        process = `x-oss-process=image/resize,m_fill,limit_0,${process},`;      }      if (isSupportWebp && process) {        process = `${process}/format,webp`;      }      if (isSupportWebp && !process) {        process = `x-oss-process=image/format,webp`;      }      return `${option.originUrl}?${process}`;    },    [isSupportWebp],  );

 return { getOssImage }; };

  • 封装 OssImage 组件 (@/components/OssImage/index.tsx)

import { useOSS } from "@/hooks/useOss";

type Props = React.DetailedHTMLProps<

 React.ImgHTMLAttributes<HTMLImageElement>,

 HTMLImageElement

> & {  notSupportWebpWidth?: number;  notSupportWebpHeight?: number;  ossWidth?: number;  ossHeight?: number; };

export default function OssImage(props: Props) {

 const { getOssImage } = useOSS();

 return (

   <img      {...props}

     src={getOssImage({        originUrl: props.src || "",

       notSupportWebpWidth: props.notSupportWebpWidth,

       notSupportWebpHeight: props.notSupportWebpHeight,

       width: props.ossWidth,

       height: props.ossHeight,      })}

     loading="lazy"    />  ); }

  • 在页面中使用 OssImage 组件 (@/pages/index.page.tsx)

import OssImage from "@/components/OssImage";

// 使用 oss,自动判断是否支持 webp

<OssImage  style={{    background: "beige",  }}

 src="https://img.alicdn.com/tfs/TB11B9iM7voK1RjSZPfXXXPKFXa-338-80.png"  notSupportWebpWidth={338}  notSupportWebpHeight={80}/>

// 不使用 oss,正常传宽高

<OssImage  style={{    background: "beige",  }}

 src="https://img.alicdn.com/tfs/TB11B9iM7voK1RjSZPfXXXPKFXa-338-80.png"  width={338}  height={80}

/>

通过这些封装,你可以更轻松地在项目中使用 WebP 格式和 CDN 进行图片优化。


数据渲染

在 Next.js 中,我们可以通过 getStaticProps 和 getServerSideProps 两个函数来实现数据的渲染。这两者只能存在一个,它们都在浏览器渲染之前调用。在开发过程中,可以在终端查看数据。


1. getStaticProps - SSG (静态生成)

getStaticProps 在项目构建打包时调用,生成静态 HTML 文件。这个过程只在构建时进行一次,适用于不经常变化的数据,具有较好的 SEO。

// 静态 SSG

export async function getStaticProps() {

 // 获取 mock 数据  const res = await fetchMockData();

 const mockData = res;

 return {

   props: { mockData },  }; }


2. getServerSideProps - SSR (服务端渲染)

getServerSideProps 在服务器接收到请求时更新,适用于经常改变的数据,但不利于 SEO。

export async function getServerSideProps() {

 const data = await fetchMockData();

 console.log("data", data);

 if (!data) {

   return {

     redirect: {

       destination: "/",

       permanent: false,      },    };  }  return {

   props: { data },  }; }

3. getStaticPaths - 生成多页面

可以使用 getStaticPaths 生成多个页面,例如生成新闻页面,搭配 getStaticProps 构造不同页面的数据。

@/static-path/[id].page.tsx

export default function ({ post }: { post: string }) {

 return (

   <div>      <h1>Post: {post}</h1>    </div>  ); }

export async function getStaticPaths() {

 const paths = new Array(10).fill(0).map((_, i) => ({

   params: { id: i + 1 + "" },  }));

 console.log("paths", paths);

 return { paths, fallback: false }; }

export async function getStaticProps({ params }: { params: { id: string } }) {

 console.log("params", params);

 return { props: { post: `post ${params.id}` } }; }


总结

通过 getStaticProps 和 getServerSideProps,我们可以在 Next.js 中实现静态生成和服务端渲染,使项目更具弹性和性能。在服务器部署方面,可以参考相关文章。将来,我们还计划加入埋点、监控系统、后台管理系统等功能,并考虑将自己的博客切换到 SSR 方式。


希望这篇文章对你有所帮助,后续会持续更新。



群贤毕至

访客