阅读视图

发现新文章,点击刷新页面。

如何用 Node.JS 和 Canvas 自动生成图片

在本指南中,我将展示如何用 Node.JS 生成文章缩略图。以下是我用这种方法生成的一张图片:

本文的完整代码可以在 Git Gist 中找到。

由于 Node.JS ,它本身并不具备 canvas 功能。我们使用一个名为 canvas 的组件,将其导入到我们的 Node.JS 项目中。可以通过运行 npm i canvas 来安装它。

如何在 Canvas 中使用 Emoji

对于我生成的图片,我还想使用 Emoji。因此,我使用了该包的一个分支,名为 @napi-rs/canvas,它支持 Emoji。我使用的版本是 0.1.14,所以如果你在本指南操作时遇到问题,尝试用 npm i @napi-rs/canvas@0.1.14 命令安装它。

现在我们已经了解了基础知识,让我们开始吧。首先,让我们导入所有需要的包。在这里我导入了几个东西:

  • canvas — 这是我们创建图片的方式。
  • fs — 将图片写入服务器并保存。
  • cwebp — 这是我们将图片保存为 webp 文件的方式,这样它就能针对网络进行优化。
  • fonts — 导入 3 种字体——其中两种是 Inter 的不同版本,这是一种很好的字体,最后一种是 Apple Emoji 字体。你可以在 Inter 字体页面 找到 Inter 字体,在 Apple Emoji 字体页面 找到 Apple Emoji 字体。
import canvas from '@napi-rs/canvas' // 用于创建画布。
import fs from 'fs' // 用于为我们的图片创建文件。
import cwebp from 'cwebp' // 用于将图片转换为 webp 格式。

// 加载我们需要的字体
GlobalFonts.registerFromPath('./fonts/Inter-ExtraBold.ttf', 'InterBold');
GlobalFonts.registerFromPath('./fonts/Inter-Medium.ttf','InterMedium');
GlobalFonts.registerFromPath('./fonts/Apple-Emoji.ttf', 'AppleEmoji');

如何用 JavaScript 生成文章缩略图

当我们在 HTML 画布上书写文本时,它通常不会自动换行。相反,我们需要创建一个函数来测量容器的宽度,并决定是否换行。注释后的函数如下所示

// 这个函数接受 6 个参数:
// - ctx: 画布的上下文
// - text: 我们想要换行的文本
// - x: 文本的起始 x 坐标
// - y: 文本的起始 y 坐标
// - maxWidth: 最大宽度,即容器的宽度
// - lineHeight: 每行的高度(由我们定义)
const wrapText = function(ctx, text, x, y, maxWidth, lineHeight) {
    // 首先,按空格分割单词
    let words = text.split(' ');
    // 然后我们创建几个变量来存储行的信息
    let line = '';
    let testLine = '';
    // wordArray 是我们将要返回的数组,它将保存
    // 行文本的信息,以及它的 x 和 y 起始位置
    let wordArray = [];
    // totalLineHeight 将保存行高的信息
    let totalLineHeight = 0;

    // 接下来,我们遍历每个单词
    for(var n = 0; n < words.length; n++) {
        // 测试它的长度
        testLine += `${words[n]} `;
        var metrics = ctx.measureText(testLine);
        var testWidth = metrics.width;
        // 如果太长,则我们开始新的一行
        if (testWidth > maxWidth && n > 0) {
            wordArray.push([line, x, y]);
            y += lineHeight;
            totalLineHeight += lineHeight;
            line = `${words[n]} `;
            testLine = `${words[n]} `;
        }
        else {
            // 否则我们只有一行!
            line += `${words[n]} `;
        }
        // 当所有单词完成后,我们将剩余的内容推入数组
        if(n === words.length - 1) {
            wordArray.push([line, x, y]);
        }
    }

    // 返回包含单词的数组,以及总行高
    // 总行高将是 (总行数 - 1) * 行高
    return [ wordArray, totalLineHeight ];
}

现在我们开始编写 generateMainImage 函数。这个函数将接受我们提供的所有信息,并为你的文章或网站生成一张图片。

在这个函数中,你可以传入任何你想要的颜色,选择权在你手中。

// 这个函数接受 5 个参数:
// canonicalName: 这是我们用来保存图片的名字
// gradientColors: 一个包含两种颜色的数组,例如 [ '#ffffff', '#000000' ],用于我们的渐变
// articleName: 你希望在图片中显示的文章或网站的标题
// articleCategory: 该文章所属的类别——或者文章的副标题
// emoji: 你希望在图片中显示的 emoji
const generateMainImage = async function(canonicalName, gradientColors, articleName, articleCategory, emoji) {

    articleCategory = articleCategory.toUpperCase();
    // gradientColors 是一个数组 [ c1, c2 ]
    if(typeof gradientColors === "undefined") {
        gradientColors = [ "#8005fc", "#073bae"]; // 备用值
    }

    // 创建画布
    const canvas = createCanvas(1342, 853);
    const ctx = canvas.getContext('2d')

    // 添加渐变——我们使用 createLinearGradient 来实现这一点
    let grd = ctx.createLinearGradient(0, 853, 1352, 0);
    grd.addColorStop(0, gradientColors[0]);
    grd.addColorStop(1, gradientColors[1]);
    ctx.fillStyle = grd;
    // 填充我们的渐变
    ctx.fillRect(0, 0, 1342, 853);

    // 在画布上书写我们的 Emoji
    ctx.fillStyle = 'white';
    ctx.font = '95px AppleEmoji';
    ctx.fillText(emoji, 85, 700);

    // 添加我们的标题文本
    ctx.font = '95px InterBold';
    ctx.fillStyle = 'white';
    let wrappedText = wrapText(ctx, articleName, 85, 753, 1200, 100);
    wrappedText[0].forEach(function(item) {
        // 我们将填充数组中的文本 item[0],在坐标 [x, y]
        // x 是数组中的 item[1]
        // y 是数组中的 item[2],减去行高(wrappedText[1]),再减去 emoji 的高度(200px)
        ctx.fillText(item[0], item[1], item[2] - wrappedText[1] - 200); // 200 是 emoji 的高度
    })

    // 将我们的类别文本添加到画布上
    ctx.font = '50px InterMedium';
    ctx.fillStyle = 'rgba(255,255,255,0.8)';
    ctx.fillText(articleCategory, 85, 553 - wrappedText[1] - 100); // 853 - 200 用于 emoji,-100 用于 1 行的行高

    if(fs.existsSync(`./views/images/intro-images/${canonicalName}.png`)) {
        return '图片已存在!我们没有创建任何图片'
    }
    else {
        // 将画布设置为 png 格式
        try {
            const canvasData = await canvas.encode('png');
            // 保存文件
            fs.writeFileSync(`./views/images/intro-images/${canonicalName}.png`, canvasData);
        }
        catch(e) {
            console.log(e);
            return '这次无法创建 png 图片。'
        }
        try {
            const encoder = new cwebp.CWebp(path.join(__dirname, '../', `/views/images/intro-images/${canonicalName}.png`));
            encoder.quality(30);
            await encoder.write(`./views/images/intro-images/${canonicalName}.webp`, function(err) {
                if(err) console.log(err);
            });
        }
        catch(e) {
            console.log(e);
            return '这次无法创建 webp 图片。'
        }

        return '图片已成功创建!';
    }
}

用 Node.JS 生成文章图片

让我们仔细观察一下这个函数,以便完全理解其中的原理。我们首先准备数据——将类别转换为大写,并设置一个默认渐变。然后我们创建画布,并使用 getContext 初始化一个绘制的空间。

articleCategory = articleCategory.toUpperCase();
// gradientColors 是一个数组 [ c1, c2 ]
if(typeof gradientColors === "undefined") {
    gradientColors = [ "#8005fc", "#073bae"]; // 备用值
}

// 创建画布
const canvas = createCanvas(1342, 853);
const ctx = canvas.getContext('2d')

然后绘制渐变:

// 添加渐变——我们使用 createLinearGradient 来实现这一点
let grd = ctx.createLinearGradient(0, 853, 1352, 0);
grd.addColorStop(0, gradientColors[0]);
grd.addColorStop(1, gradientColors[1]);
ctx.fillStyle = grd;
// 填充我们的渐变
ctx.fillRect(0, 0, 1342, 853);

图片上绘制 emoji 文本。

// 在画布上书写我们的 Emoji
ctx.fillStyle = 'white';
ctx.font = '95px AppleEmoji';
ctx.fillText(emoji, 85, 700);

现在我们使用我们的换行函数 wrapText。我们将传入相当长的 articleName,并从图片底部附近的 85, 753 开始。由于 wrapText 返回一个数组,我们将遍历该数组以确定每行的坐标,并将它们绘制到画布上:

    // 添加我们的标题文本
    ctx.font = '95px InterBold';
    ctx.fillStyle = 'white';
    let wrappedText = wrapText(ctx, articleName, 85, 753, 1200, 100);
    wrappedText[0].forEach(function(item) {
        // 我们将填充数组中的文本 item[0],在坐标 [x, y]
        // x 是数组中的 item[1]
        // y 是数组中的 item[2],减去行高(wrappedText[1]),再减去 emoji 的高度(200px)
        ctx.fillText(item[0], item[1], item[2] - wrappedText[1] - 200); // 200 是 emoji 的高度
    })

    // 将我们的类别文本添加到画布上
    ctx.font = '50px InterMedium';
    ctx.fillStyle = 'rgba(255,255,255,0.8)';
    ctx.fillText(articleCategory, 85, 553 - wrappedText[1] - 100); // 853 - 200 用于 emoji,-100 用于 1 行的行高

将画布图片保存到服务器

好了,现在我们已经创建了图片,让我们将它保存到服务器上:

  • 首先,我们将检查文件是否存在。如果存在,我们将返回图片。
  • 如果文件不存在,我们将尝试使用 canvas.encode 创建 png 版本,并使用 fs.writeFileSync 保存它。
  • 如果一切顺利,我们将使用 cwebp 保存一个.webp 版本的文件,这比 .png 版本小得多。
  if(fs.existsSync(`./views/images/intro-images/${canonicalName}.png`)) {
      return '图片已存在!我们没有创建任何图片'
  }
  else {
      // 将画布设置为 png 格式
      try {
          const canvasData = await canvas.encode('png');
          // 保存文件
          fs.writeFileSync(`./views/images/intro-images/${canonicalName}.png`, canvasData);
      }
      catch(e) {
          console.log(e);
          return '这次无法创建 png 图片。'
      }
      try {
          const encoder = new cwebp.CWebp(path.join(__dirname, '../', `/views/images/intro-images/${canonicalName}.png`));
          encoder.quality(30);
          await encoder.write(`./views/images/intro-images/${canonicalName}.webp`, function(err) {
              if(err) console.log(err);
          });
      }
      catch(e) {
          console.log(e);
          return '这次无法创建 webp 图片。'
      }

      return '图片已成功创建!';
  }

要运行这个文件:

node index.js

以下是通过这种方式生成的一张图片的示例:

原文:fjolt.com/article/jav…

6 个常见的 React 反模式,正在损害你的代码质量

当我刚开始使用 React 时,一切似乎都很简单 —— 只有几个组件、一些 props 和状态。但随着项目的增长,我开始遇到一些问题。我意识到这些问题其实是伪装成模式的反模式 —— 不是好的那种,而是有害的。在这里,我主要指的是我在许多代码库中看到的代码模式,这些代码是由不同经验水平的开发者编写的,并不一定是真正意义上的“模式”。

1. Props 钻透

问题: Props 钻透发生在你将 props 从顶级组件传递到中间组件,最终到达实际需要它们的组件。当你的组件树很深,且 prop 只在链的末端被一个组件使用时,这会显得尤为成问题。

为什么不好: 这会导致组件紧密耦合,难以重构。如果 prop 的需求发生变化,你可能需要更新从顶层到底层之间的每个组件。整个系统会变得脆弱且难以维护。

想象一个 SearchableList 组件,它通过 ListonItemClick 函数传递给 ListItem。每个中间组件都必须处理 props,即使它们实际上并不需要它们。这为了一点点收益增加了大量的复杂性。

代码示例 — 不好的方法:

function SearchableList({ items, onItemClick }) {
  return (
    <div className="searchable-list">
      <List items={items} onItemClick={onItemClick} />
    </div>
  );
}

function List({ items, onItemClick }) {
  return (
    <ul className="list">
      {items.map((item) => (
        <ListItem key={item.id} data={item} onItemClick={onItemClick} />
      ))}
    </ul>
  );
}

function ListItem({ data, onItemClick }) {
  return (
    <li className="list-item" onClick={() => onItemClick(data.id)}>
      {data.name}
    </li>
  );
}

你必须将 onItemClickSearchableList 一路传递到 ListItem,中间的所有组件都包含这个 prop 和函数,但它们什么也不做。

这在人们试图“修复”问题时非常常见,或者他们没有时间去思考影响会有多大。

2. 在组件内进行数据转换

问题: 直接在组件的 useEffect 或渲染函数中转换数据,通常感觉是最简单的做法。你获取数据,按需转换,然后设置状态 —— 全部在一个地方完成。

为什么不好: 这会在组件内混合关注点,使其承担多个职责 —— 获取、转换和渲染。这也会使测试变得困难,并限制转换逻辑的可重用性。随着转换变得越来越复杂,这会使组件更难以理解和维护。

考虑一个 UserProfile 组件,它获取用户数据并转换它,例如合并名字和姓氏。将所有这些逻辑放在 useEffect 中意味着每次更改都需要获取和转换 —— 效率不高。

代码示例 — 在组件内转换:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(response => response.json())
      .then(data => {
        // 在组件内转换数据
        const transformedUser = {
          name: `${data.firstName} ${data.lastName}`,
          age: data.age,
          address: `${data.addressLine1}, ${data.city}, ${data.country}`
        };
        setUser(transformedUser);
      });
  }, [userId]);

  return (
    <div>
      {user && (
        <>
          <p>Name: {user.name}</p>
          <p>Age: {user.age}</p>
          <p>Address: {user.address}</p>
        </>
      )}
    </div>
  );
}

这个示例展示了如何在 useEffect 中直接数据获取和转换,导致组件紧密耦合,使测试变得困难。

3. 在视图中包含复杂逻辑

问题: 你是否曾经因为“只是一小段”而直接在组件中包含一些业务逻辑?但很快组件就充满了条件语句和计算。

为什么不好: 组件应该专注于呈现 UI,而不是实现业务规则。你在组件中放入的逻辑越多,重用它们就越困难。这会导致组件臃肿,难以测试和理解。

想象一个组件,它不仅显示订单详情,还计算折扣、运费和预估税 —— 这种逻辑如果放在单独的服务函数或钩子中更具可重用性。

4. 缺乏测试

问题: 跳过测试可能感觉节省时间,尤其是在截止日期时。但 React 组件通常处理复杂功能 —— 如管理表单状态或 API 调用 —— 这可能导致难以诊断的错误。

为什么不好: 没有适当的单元或集成测试,当重构或添加功能时,没有安全网来捕获错误。每次更改都成为一项冒险,你会发现自己做了很多手动测试,但仍无法覆盖所有场景。

我还记得在某个功能中,购物车在某些边缘情况下未能更新。适当的单元测试本可以在问题进入生产环境之前捕获这些问题。

5. 重复代码

问题: 复制粘贴一段代码通常是 easiest solution —— 你已经写过了,为什么不直接重用呢?问题在于,每个重复实例都是维护负担。

为什么不好: 当需求变化时,你需要更新每个重复实例,遗漏其中一个可能会导致错误和不一致。这是确保你的逻辑保持集中且易于修改的问题。

想象一个 formatDate() 函数,它出现在多个组件中,因为你每次需要时都粘贴了它。当格式要求变化时,这会变成一项搜索并希望你找到了所有实例的任务。

代码示例 — 重复代码:

function AdminList(props) {
  const filteredUsers = props.users.filter(user => user.isAdmin);
  return <List items={filteredUsers} />;
}

function ActiveList(props) {
  const filteredUsers = props.users.filter(user => user.isActive);
  return <List items={filteredUsers} />;
}

这个示例展示了如何在不同组件中过滤用户逻辑,导致代码重复。

6. 责任过多的长组件

问题: 你可能会想到一个像 OrderContainer 这样的组件,它管理与订单相关的所有内容 —— 验证、错误处理、获取数据和渲染 UI。

为什么不好: 组件应该遵循单一职责原则(SRP)。当它们有太多职责时,它们会变得复杂且难以调试、理解和扩展。如果某部分逻辑依赖于另一部分,它们也会非常难以测试。

在一个项目中,我有一个表单组件,它处理验证、提交、错误显示,甚至管理全局状态。将其拆分为更小的组件并提取不同任务的钩子,使代码更易于处理。

原文:training.shikshatech.in/6-common-re…

🌟 正确管理深层嵌套的 React 组件

在 React 应用开发中,深层嵌套的组件结构如果处理不当,很容易变得难以维护。

本文将探讨一种架构模式,确保在处理嵌套组件时,提高应用的可扩展性、可维护性和代码清晰度。这种模式遵循单一职责原则,让子组件专注于自身的逻辑,而父组件则负责处理所有外部操作。

👀 查看演示

👉 演示

📂 查看完整项目

👉 GitHub 仓库

🏗️ 深层嵌套组件的问题

  • 组件紧密耦合:子组件处理了本不应由它们处理的操作。
  • 调试困难:业务逻辑分散在多个组件中。
  • 可复用性降低:组件难以提取和复用。

🎯 目标

我们希望实现以下目标:

  • ✅ 保持子组件纯净(只关注 UI 和内部状态)。
  • ✅ 将逻辑集中到父组件(处理如保存到数据库等外部操作)。
  • ✅ 使用验证工具确保数据完整性。

🛠️ 实现模式

父组件:处理所有操作

父组件负责以下任务:

  1. 管理应用状态。
  2. 在保存前验证数据。
  3. 处理来自子组件的更新。
import { useCallback, useState } from "react";
import { toast } from "react-toastify";
import { Data } from "../data/Data";
import { Child1 } from "./child1";
import { Child2 } from "./child2";


export const Parent = () => {
  const [data, setData] = useState<Data | undefined>({
    child1: undefined,
    child2: undefined,
    grandChild: undefined,
  });

  const isDataComplete = useCallback(
    (incomingData: Partial<Data> | undefined): incomingData is Data => {
      return (
        !!incomingData?.child1 &&
        incomingData?.child1.trim().length > 0 &&
        !!incomingData?.child2 &&
        !!incomingData?.grandChild
      );
    },
    []
  );

  const onSave = useCallback(
    (data: Partial<Data> | undefined) => {
      if (!isDataComplete(data)) {
        toast("Please fill all fields first", { style: { color: "black", backgroundColor: "#f9b6af" } });
        return;
      }
      toast("You filled all your fields!", { style: { color: "black", backgroundColor: "lightgreen" } });
    },
    [isDataComplete]
  );

  const onUpdate = useCallback(
    (incomingData: Partial<Data>) => {
      setData(prev => ({ ...prev, ...incomingData }));
    },
    []
  );

  return (
    <>
      <Child1 data={data} onUpdate={onUpdate} />
      <Child2 data={data} onUpdate={onUpdate} onSave={onSave} />
    </>
  );
};

子组件 1:委托更新

子组件仅管理自身的输入,并将更新委托给父组件。

import { useState } from "react";
import { Data } from "../data/Data";

export const Child1 = ({ data, onUpdate }: { data: Data | undefined; onUpdate: (parentData: Partial<Data>) => void; }) => {
  const [child1Input, setChild1Input] = useState(data?.child1);

  return (
    <>
      <label>Child 1 input</label>
      <input
        value={child1Input}
        onChange={(e) => {
          setChild1Input(e.target.value);
          onUpdate({ child1: e.target.value });
        }}
        />
    </>
  );
};

子组件 2:嵌套子组件和保存操作

孙子组件使用委托逻辑进行按钮操作。

import { useState } from "react";
import { GrandChild } from "./grandChild";

export const Child2 = ({ data, onUpdate, onSave }) => {
  const [child2Input, setChild2Input] = useState(data?.child2);

  return (
    <>
      <label>Child 2 input</label>
      <input
        value={child2Input}
        onChange={(e) => {
          setChild2Input(e.target.value);
          onUpdate({ child2: e.target.value });
        }}
        />
      <GrandChild onUpdate={onUpdate} data={data} onSave={onSave} />
    </>
  );
};

自定义逻辑和父组件逻辑

import { useState } from "react";

export const GrandChild = ({ onUpdate, data, onSave }) => {
  const [grandChildInput, setGrandChildInput] = useState(data?.grandChild);

  return (
    <>
      <label>Grandchild input</label>
      <input
        value={grandChildInput}
        onChange={(e) => {
          setGrandChildInput(e.target.value);
          onUpdate({ grandChild: e.target.value });
        }}
        />
      <button onClick={() => onSave(data)}>Save</button>
    </>
  );
};

如果子组件或孙子组件在更新父组件之前需要包含自定义逻辑,可以定义一个自定义回调,然后调用委托的父组件操作。例如:

jsxconst customOnUpdate = (value: string, onUpdate: (data: Partial<Data>) => void) => {
  console.log("Custom logic before updating:", value);

  // Then execute parent logic
  onUpdate();

  console.log("Custom logic after updating:", transformedValue);
};

🎯 为什么这样有效

  • 关注点分离:子组件仅处理自身的逻辑。
  • 单一数据源:父组件管理并验证数据。
  • 提高可复用性:任何组件都可以独立复用。
  • 更好的可维护性:调试和扩展功能更加容易。

🚀 编程愉快!

原文:www.yuque.com/fengjutian/… 《🌟 Managing Deeply Nested React Components the Right Way》

React 中的静态渲染 SSG

渲染模式

从我们关于服务器端渲染(SSR)的讨论中得知,较高的请求处理会影响首字节时间(TTFB)。同样地,客户端渲染(CSR)中,较大的 JavaScript 包会因下载和处理脚本所需的时间而对应用的首次内容绘制(FCP)、最大内容绘制(LCP)和交互时间(TTI)产生不利影响。

静态生成(SSG)试图通过在网站构建时预先渲染 HTML 内容来解决这些问题。

此处为语雀视频卡片,点击链接查看:static-generation-1.webm

每个用户访问的路由都会提前生成一个静态 HTML 。这些静态 HTML 文件可能存储在服务器或CDN上,在客户端请求时获取。

此处为语雀视频卡片,点击链接查看:static-generation-2.webm

静态文件还可以被缓存,从而提供更大的弹性。由于 HTML 是提前生成的,服务器上的处理时间可以忽略不计,从而实现更快的首字节时间(TTFB)。在理想情况下,客户端的 JavaScript 应该最少,静态页面在被客户端接收后应尽快变得可交互。

基本结构

顾名思义,静态渲染适用于静态内容,页面无需根据登录用户进行自定义(例如个性化推荐)。因此,网站的“关于我们”、“联系我们”、博客页面或电子商务应用的产品页面等静态页面是静态渲染的理想选择。像 Next.js、Gatsby 和 VuePress 这样的框架支持静态生成。

让我们从这个简单的 Next.js 示例开始。

Next.js:

// pages/about.js

export default function About() {
  return (
    <div>
    <h1>关于我们</h1>
  {/* ... */}
  </div>
  );
}

当网站构建(使用 next build)时,此页面将被预渲染成一个 HTML 文件 about.html,可通过路由 /about 访问。

带数据的 SSG

像“关于我们”这样的静态内容可能无需获取数据即可渲染。然而,对于个人博客页面,需要将数据与特定模板合并,然后在构建时渲染成 HTML。

生成的 HTML 页面数量取决于博客文章的数量。这些场景可以使用 Next.js 静态渲染来解决。我们可以根据可用项目生成列表页面。让我们看看如何操作。

列表页面 - 所有Items

生成列表页面是一个内容依赖于数据的场景。数据在构建时从数据库获取来构建页面。在 Next.js 中,这可以通过在页面组件中导出 getStaticProps() 函数来实现。该函数在服务器上调用获取数据。然后数据可以传递给页面的 props。以下是生成产品列表页面的代码,最初分享在这篇文章

// 此函数在构建服务器上于构建时运行
export async function getStaticProps() {
  return {
    props: {
      products: await getProductsFromDatabase(),
    },
  };
}

// 页面组件在构建时从 getStaticProps 接收 products prop
export default function Products({ products }) {
  return (
    <>
    <h1>产品</h1>
    <ul>
    {products.map((product) => (
      <li key={product.id}>{product.name}</li>
                 ))}
</ul>
  </>
);
}

该函数不会包含在客户端 JavaScript 中,因此可以直接从数据库获取数据。

个人详情页面 - 每个Item

在上述示例中,我们在列表页面上为每个产品提供一个详细的个人页面。这些页面可以通过点击列表页面访问。

假设我们有产品 ID 为 101102103 等的产品。我们需要它们的路由是 /products/101/products/102/products/103 。为了在 Next.js 中实现这一点,我们可以结合使用 getStaticPaths() 函数和动态路由

我们需要为此创建一个通用的页面组件 products/[id].js,并在其中导出 getStaticPaths() 函数。该函数将返回所有可能的产品 ID,这些 ID 用于在构建时预渲染个人产品页面。以下 Next.js 代码显示了如何构建此代码。

// pages/products/[id].js

// 在 getStaticPaths() 中,你需要返回希望在构建时预渲染的产品页面(/products/[id])的 ID 列表。为此,可以从数据库获取所有产品。
export async function getStaticPaths() {
  const products = await getProductsFromDatabase();

  const paths = products.map((product) => ({
    params: { id: product.id },
  }));

  // fallback: false 表示没有正确 ID 的页面将显示 404 错误。
  return { paths, fallback: false };
}

// params 将包含每个生成页面的 ID。
export async function getStaticProps({ params }) {
  return {
    props: {
      product: await getProductFromDatabase(params.id),
    },
  };
}

export default function Product({ product }) {
  // 渲染产品
}

产品页面的详细信息可以通过产品 ID 的 getStaticProps 函数在构建时填充。注意这里使用了 fallback: false 。它表示如果特定路由或产品 ID 没有对应的页面,将显示 404 错误页面。

因此,我们可以使用 SSG 预渲染多种不同类型的页面。

SSG - 关键考虑因素

如前所述,SSG 通过减少客户端和服务器所需的处理,为网站带来了出色的性能。由于内容已经存在且可以被网络爬虫轻松渲染,这些网站还对搜索引擎优化(SEO)友好。尽管 SSG 是一个出色的渲染模式,但在评估 SSG 对特定应用的适用性时,需要考虑以下因素。

  1. 大量的 HTML 文件: 需要为用户访问的每个路由生成单独的 HTML 。例如,当将其用于博客时,每篇博客文章都会生成一个 HTML 文件。对任何文章的编辑都需要重新构建才能更新。维护大量的 HTML 文件可能具有挑战性。
  2. 托管依赖: 为了使 SSG 网站响应迅速,用于存储和托管平台也必须出色。如果一个 SSG 网站托管在多个 CDN 上,利用边缘缓存,那么可以实现卓越的性能。
  3. 动态内容: 每当内容发生变化时,SSG 网站都需要重新构建和重新部署。如果在内容更改后网站未重新构建,显示的内容可能会过时。这使得 SSG 不适合高度动态的内容。

原文:www.patterns.dev/react/stati…

❌