Gatsby + realworld 案例实践 - 04 文章详情页面

本文介绍如何使用Gatsby创建博客文章详情页,包括本地插件的创建、页面模板编写及动态文章详情页的实现方法。

文章详情页

文章详情页要使用编程的方式去创建,并且要通过本地插件去实现。

创建本地插件

创建 plugins/gatsby-plugin-article/gatsby-node.js,在 plugins/gatsby-plugin-article 插件文件夹下生成 package.jsonnpm init -y)。

配置插件

// gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: "gatsby-plugin-create-client-paths",
      options: {
        prefixes: ["/app/*"], // 指定客户端专用路由匹配规则
      },
    },
    {
      resolve: "gatsby-source-list",
      options: {
        apiURL: "https://conduit.productionready.io/api", // 请求基准地址
      },
    },
    // gatsby-plugin-article 一定要配置在 gatsby-source-list 后面
    // 因为只有文章数据添加到数据层
    // 才能基于数据层的数据创建详情页
    "gatsby-plugin-article",
  ],
}

编写插件

// plugins\gatsby-plugin-article\gatsby-node.js
exports.createPages = async ({ graphql, actions }) => {
  const { data } = await graphql(`
    query MyQuery {
      allArticlesList {
        nodes {
          # 基于 slug 构建详情页
          # 用作地址
          # 用作详情页查询数据的依据
          slug
        }
      }
    }
  `)

  const { createPage } = actions

  data.allArticlesList.nodes.forEach(article => {
    createPage({
      path: `/article/${article.slug}`, // 访问地址
      component: require.resolve("../../src/templates/article.js"), // 模板绝对路径
      context: {
        slug: article.slug, // 传递给页面的参数
      },
    })
  })
}

编写页面模板

页面模板使用 src/pages/article.js 文件,将其移动到 src/templates 下。

// src\templates\article.js
import React from "react"
import { graphql } from "gatsby"

export default function Article({ data }) {
  const article = data.articlesList

  return (
    <div className="article-page">
      <div className="banner">
        <div className="container">
          <h1>{article.title}</h1>
          <div className="article-meta">
            <a>
              <img src={article.author.image} />
            </a>
            <div className="info">
              <a className="author">{article.author.username}</a>
              <span className="date">{article.createAt}</span>
            </div>
            <button className="btn btn-sm btn-outline-secondary">
              <i className="ion-plus-round" />
              &nbsp; {article.author.username}{" "}
              <span className="counter">({article.following})</span>
            </button>
            &nbsp;&nbsp;
            <button className="btn btn-sm btn-outline-primary">
              <i className="ion-heart" />
              &nbsp; Favorite Post{" "}
              <span className="counter">({article.favoritesCount})</span>
            </button>
          </div>
        </div>
      </div>
      <div className="container page">
        <div className="row article-content">
          <div dangerouslySetInnerHTML={{ __html: article.body }}></div>
        </div>
        <hr />
        {/* 点赞&关注区域 */}
        <div className="article-actions">...</div>
        {/* 评论区域 */}
        <div className="row">...</div>
      </div>
    </div>
  )
}

export const query = graphql`
  query ($slug: String) {
    articlesList(slug: { eq: $slug }) {
      author {
        image # 作者头像
        username # 作者名称
        following # 作者关注数量
      }
      title # 标题
      body # 文章内容
      description # 描述
      createdAt # 创建日期
      favoritesCount # 点赞数
    }
  }
`

在列表页添加详情链接

// src\templates\list.js
// 引入 Link 组件
import { graphql, Link } from "gatsby"

// 修改 Lists 组件
<Link to={`/article/${article.slug}`} className="preview-link">
  <h1>{article.title}</h1>
  <p>{article.description}</p>
  <span>Read more...</span>
</Link>

当前列表页加载后获取了最新文章数据,新的文章点击查看详情页面,由于没有构建对应的静态页面,可能会报 404 页面。

可以先把获取最新数据的代码注释掉再测试。

创建动态文章详情页面

存在的问题

列表页面最先显示的是 Gatsby 构建的静态页面,接着动态获取文章最新数据,替换静态数据。

但是获取的最新文章并没有在 Gatsby 构建应用时生成对应的静态页面。

所以当点击列表上的最新文章页面的时候(如果静态数据中没有该文章),则会进入 404 页面。

所以需要实现:无论用户访问的是静态生成的详情页还是动态生成的详情页,都能正常访问。

解决方法

自定义应用的 404 页面:

  1. 在 404 页面可以获取到页面的访问路径。
  2. 检查路径是否以 /article 开头,如果是就获取路径上的 slug
  3. 然后将 slug 传递给 article.js 组件
  4. article.js 组件中先判断是否有静态数据,如果有则显示静态数据
  5. 如果没有则通过 slug 动态获取数据,然后将获取的数据显示在页面中

创建 404 页面

新建 src/pages/404.js 文件,用于自定义 404 页面。

// src\pages\404.js
import React from "react"

export default function NotFount({ location }) {
  const { pathname } = location
  return <div>{pathname}</div>
}

现在 404 页面仍然显示的是应用默认的内容。

点击 404 页面上面的 Preview custom 404 page按钮可以查看自定义的内容。

在这里插入图片描述

将文章数据作为状态存储

注意文章数据初始值是 {},需要对更深的取值使用可选链(?.

// src\templates\article.js
import React, { useState, useEffect } from "react"
import { graphql } from "gatsby"
import axios from "axios"

export default function Article({ data, slug }) {
  const [article, setArticle] = useState({})

  useEffect(() => {
    if (data) {
      setArticle(data.articlesList)
      return
    }
    ;(async function () {
      const { data } = await axios.get(`/articles/${slug}`)
      setArticle(data.article)
    })()
  }, [])

  return (
    <div className="article-page">
      <div className="banner">
        <div className="container">
          <h1>{article.title}</h1>
          <div className="article-meta">
            <a>
              <img src={article.author?.image} />
            </a>
            <div className="info">
              <a className="author">{article.author?.username}</a>
              <span className="date">{article.createAt}</span>
            </div>
            <button className="btn btn-sm btn-outline-secondary">
              <i className="ion-plus-round" />
              &nbsp; {article.author?.username}{" "}
              <span className="counter">({article.following})</span>
            </button>
            &nbsp;&nbsp;
            <button className="btn btn-sm btn-outline-primary">
              <i className="ion-heart" />
              &nbsp; Favorite Post{" "}
              <span className="counter">({article.favoritesCount})</span>
            </button>
          </div>
        </div>
      </div>
      <div className="container page">
        <div className="row article-content">
          <div dangerouslySetInnerHTML={{ __html: article.body }}></div>
        </div>
        <hr />
        {/* 点赞&关注区域 */}
        <div className="article-actions">...</div>
        {/* 评论区域 */}
        <div className="row">...</div>
      </div>
    </div>
  )
}

export const query = graphql`
  query ($slug: String) {
    articlesList(slug: { eq: $slug }) {
      author {
        image # 作者头像
        username # 作者名称
        following # 作者关注数量
      }
      title # 标题
      body # 文章内容
      description # 描述
      createdAt # 创建日期
      favoritesCount # 点赞数
    }
  }
`

禁用 Gatsby 404 页面

开发环境下(gatsby develop),当 Gatsby 进入 404 时,会先进入 dev-404-page,点击 Preview custom 404 page 才会进入默认创建的 404 页面或开发者自定义的 404 页面。

所以导致现在访问动态数据的文章详情页,页面会显示两个 Layout 内容。

可以使用 Gatsby Node APIs 在创建完页面后删除开发环境的 dev-404-page 页面,就可以直接访问自定义的 404 页面了。

可以直接在应用的 gatsby-node.js 中配置,也可以创建自定义插件。

创建自定义插件plugins/gatsby-plugin-disable-404/gatsby-node.js

在插件目录下生成 package.jsonnpm init -y

在应用中配置插件

// gatsby-config.js
module.exports = {
  plugins: [
    ...
    "gatsby-plugin-disable-404",
  ],
}

编写插件:

// plugins\gatsby-plugin-disable-404\gatsby-node.js
exports.onCreatePage = ({ page, actions }) => {
  const { deletePage, createPage } = actions
  if (page.path === "/dev-404-page/") {
    // 删除开发环境的 dev-404-page 页面
    deletePage(page)
  } else {
    createPage(page)
  }
}

该插件参考 gatsby-disable-404插件,源码中删除了 /404/ 页面,也就是将自定义的 404 页面也删除了,不能满足当前需求,所以本例重新实现。

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.onCreatePage = function (_a) {
 var page = _a.page, actions = _a.actions;
 var deletePage = actions.deletePage, createPage = actions.createPage;
 if (page.path === '/404/' || page.path === '/dev-404-page/') {
     deletePage(page);
 } else {
     createPage(page);
 }
};

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值