Gatsby + realworld 案例实践 - 03 文章列表页面

本文介绍了如何在兼顾SEO和快速访问的同时,实现实时更新文章列表。静态生成确保初始加载速度,服务器端动态获取新文章,提升用户体验。

文章列表页

实现思路

文章列表不会频繁变化,但是当发布文章时,应该向用户展示最新数据。

为了 SEO 和访问速度的兼顾,以及用户体验的提升,实现思路如下:

  1. 在构建应用时,先将文章现有的数据进行静态生成,这样当用户访问文章列表页面的时候就不需要等待,并且有助于 SEO 爬虫工具抓取内容。
    • 但是如果有用户发布文章,新发布的文章不会立即显示在页面中
  2. 当页面加载时动态从服务器获取数据,并将最新的数据显示在页面中。
    • 这样的结果是,用户会立即看到静态页面中的数据,然后再看到最新的数据。

具体步骤

  1. 创建一个数据源插件,用于获取所有的外部源数据
  2. 将获取到的文章列表数据,放置到数据层中
  3. 以编程的方式创建带分页的文章列表页面
  4. 在组件中从数据层中查询组件需要展示的数据,并展示
  5. 动态从服务器端获取最新的数据,用最新的数据替换事先生成好的静态数据

初始化数据源插件

在项目更目录下创建文件夹 plugins/gatsby-source-list,在文件夹中创建 gatsby-node.js

# 生成 package.json
npm init -y
# 安装依赖
npm i axios gatsby-node-helpers

配置插件:

// 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", // 请求基准地址
      },
    },
  ],
}

获取外部文章列表数据

// plugins\gatsby-source-list\gatsby-node.js
const axios = require("axios")

// 获取外部文章数据添加到数据层
exports.sourceNodes = async ({}, { apiURL }) => {
  const articles = await loadArticles(apiURL)
  console.log(articles)
}

// 获取文章列表
async function loadArticles(apiURL) {
  const limit = 100 // 查询条数(realworld 允许一次最多获取100条数据)
  let offset = 0 // 从第几条开始查询
  const result = [] // 获取的数据

  await load()

  async function load() {
    const { data } = await axios.get(`${apiURL}/articles`, {
      params: { limit, offset },
    })

    result.push(...data.articles)

    if (result.length < data.articlesCount) {
      // 继续获取数据
      offset += limit
      await load()
    }
  }

  return result
}

将数据添加到数据层

安装插件依赖(在当前插件文件夹下安装):

npm i gatsby-node-helpers
// plugins\gatsby-source-list\gatsby-node.js
const axios = require("axios")
const { createNodeHelpers } = require("gatsby-node-helpers")

// 获取外部文章数据添加到数据层
exports.sourceNodes = async (
  { actions, createNodeId, createContentDigest },
  { apiURL }
) => {
  const articles = await loadArticles(apiURL)

  const { createNodeFactory } = createNodeHelpers({
    typePrefix: "articles",
    createNodeId,
    createContentDigest,
  })

  const createNodeObject = createNodeFactory("list")

  const { createNode } = actions

  articles.forEach(article => {
    // realworld 文章数据没有 id,需要通过唯一标识 slug 生成唯一 id
    article.id = createNodeId(article.slug)
    createNode(createNodeObject(article))
  })
}

// 获取文章列表
async function loadArticles(apiURL) {...}

根据文章列表数据创建带分页的文章列表页面

安装插件

创建带分页的列表页面需要用到插件 gatsby-awesome-pagination

在当前插件文件夹下安装 npm i gatsby-awesome-pagination

// plugins\gatsby-source-list\gatsby-node.js
const axios = require("axios")
const { createNodeHelpers } = require("gatsby-node-helpers")
const { paginate } = require("gatsby-awesome-pagination")

// 获取外部文章数据添加到数据层
exports.sourceNodes = async (
  { actions, createNodeId, createContentDigest },
  { apiURL }
) => {...}

// 获取文章列表
async function loadArticles(apiURL) {...}

// 创建文章列表页面
exports.createPages = async ({ actions, graphql }) => {
  const { createPage } = actions

  // 传递给插件的数组
  // 插件只是依据这个数组去循环创建分页
  // 并不需要其中的全部数据
  // 所以可以只查询一个字段获取一个正确数量的数组即可
  const { data } = await graphql(`
    query {
      allArticlesList {
        totalCount
      }
    }
  `)

  const items = Array(data.allArticlesList.totalCount).fill(0)

  // Create your paginated pages
  paginate({
    createPage, // Gatsby 的 createPage 方法
    items, // 要分页的数据
    itemsPerPage: 10, // 每页的条数
    pathPrefix: "/list", // 访问地址 默认第一页 页码作为 param 拼接到地址后面 如 /list/2
    component: require.resolve("../../src/templates/list.js"), // 页面模板绝对路径
  })
}

关于 gatsby-awesome-pagination 插件

传递的数据源

插件依据循环数据数组创建分页,所以数据不需要真实的数据。

本例使用的是依据查询的文章总条数创建的数组。

也可以查询具体字段,如:

const { data } = await graphql(`
  query {
    allArticlesList {
      nodes {
        id # 任意字段
      }
    }
  }
`)
const items = data.allArticlesList.nodes
访问地址

pathPrefix 定义的访问地址,是第一页的访问地址,其它页访问使用 <pathPrefix>/<页码>

例如 pathPrefix: '/list',第一页访问地址是 /list,第二页访问地址是 /list/2

注意,插件并没有为 /list/1 地址创建页面,访问它只会进入 404。

创建列表页面模板文件

创建 src/templates/list.js 文件,内容可以复制 src/page/index.js

修改文件,查询文章列表数据并显示到页面:

// src\templates\list.js
import React from "react"
import Banner from "../components/Banner"
import Toggle from "../components/Toggle"
import Sidebar from "../components/Sidebar"
import { graphql } from "gatsby"

export default function List({ data }) {
  return (
    <div className="home-page">
      <Banner />
      <div className="container page">
        <div className="row">
          <div className="col-md-9">
            <Toggle />
            <Lists articles={data.allArticlesList.nodes} />
          </div>
          <div className="col-md-3">
            <Sidebar />
          </div>
        </div>
      </div>
    </div>
  )
}

function Lists({ articles }) {
  return articles.map(article => (
    <div key={article.slug} className="article-preview">
      <div className="article-meta">
        <a href="profile.html">
          <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-outline-primary btn-sm pull-xs-right">
          <i className="ion-heart" /> {article.favoritesCount}
        </button>
      </div>
      <a className="preview-link">
        <h1>{article.title}</h1>
        <p>{article.description}</p>
        <span>Read more...</span>
      </a>
    </div>
  ))
}

// 插件提供的查询命令
// $skip:从第几条开始查询
// $limit:每页查询的条数
export const pageQuery = graphql`
  query ($skip: Int!, $limit: Int!) {
    allArticlesList(skip: $skip, limit: $limit) {
      nodes {
        slug
        author {
          image # 作者头像
          username # 作者名称
        }
        title # 标题
        createdAt # 创建日期
        description # 描述
        favoritesCount # 点赞数
      }
    }
  }
`

动态获取文章列表数据

在页面加载的时候获取文章的最新数据,替换页面中的静态数据。

如果还没构建发布文章页面,可以访问官方搭建的 Demo 地址发布:Home — Conduit (realworld.io)

获取最新数据存放在 Store 中

// src\store\sagas\article.saga.js
import { takeEvery, put } from "redux-saga/effects"
import axios from "axios"

function* loadArticles({ limit, offset }) {
  const { data } = yield axios.get("/articles", {
    params: {
      limit,
      offset,
    },
  })

  yield put({ type: "loadArticlesSuccess", payload: data.articles })
}

export default function* articleSaga() {
  yield takeEvery("loadArticles", loadArticles)
}

// src\store\sagas\root.saga.js
import { all } from "redux-saga/effects"
import counterSaga from "./counter.saga"
import authSaga from "./auth.saga"
import articleSaga from "./article.saga"

export default function* rootSaga() {
  yield all([counterSaga(), authSaga(), articleSaga()])
}

// src\store\reducers\article.reducer.js
export default function (state = {}, action) {
  switch (action.type) {
    case "loadArticlesSuccess":
      return {
        articles: action.payload,
      }
      break

    default:
      return state
      break
  }
}

// src\store\reducers\root.reducer.js
import { combineReducers } from "redux"
import counterReducer from "./counter.reducer"
import authReducer from "./auth.reducer"
import articleReducer from "./article.reducer"

export default combineReducers({
  counterReducer,
  authReducer,
  articleReducer,
})

在组件中获取文章数据

// src\templates\list.js
import React, { useEffect } from "react"
import { useDispatch, useSelector } from "react-redux"
import Banner from "../components/Banner"
import Toggle from "../components/Toggle"
import Sidebar from "../components/Sidebar"
import { graphql } from "gatsby"

export default function List({ data, pageContext }) {
  // pageContext 是 gatsby-awesome-pagination 插件传递给组件的分页信息
  const { skip, limit } = pageContext

  const dispatch = useDispatch()
  const articleReducer = useSelector(state => state.articleReducer)

  useEffect(() => {
    dispatch({
      type: "loadArticles",
      limit,
      offset: skip,
    })
  }, [])

  return (
    <div className="home-page">
      <Banner />
      <div className="container page">
        <div className="row">
          <div className="col-md-9">
            <Toggle />
            <Lists
              articles={articleReducer.articles || data.allArticlesList.nodes}
            />
          </div>
          <div className="col-md-3">
            <Sidebar />
          </div>
        </div>
      </div>
    </div>
  )
}

function Lists({ articles }) {...}

// 插件提供的查询命令
// $skip:从第几条开始查询
// $limit:每页查询的条数
export const pageQuery = graphql`...`

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值