文章列表页
实现思路
文章列表不会频繁变化,但是当发布文章时,应该向用户展示最新数据。
为了 SEO 和访问速度的兼顾,以及用户体验的提升,实现思路如下:
- 在构建应用时,先将文章现有的数据进行静态生成,这样当用户访问文章列表页面的时候就不需要等待,并且有助于 SEO 爬虫工具抓取内容。
- 但是如果有用户发布文章,新发布的文章不会立即显示在页面中
- 当页面加载时动态从服务器获取数据,并将最新的数据显示在页面中。
- 这样的结果是,用户会立即看到静态页面中的数据,然后再看到最新的数据。
具体步骤
- 创建一个数据源插件,用于获取所有的外部源数据
- 将获取到的文章列表数据,放置到数据层中
- 以编程的方式创建带分页的文章列表页面
- 在组件中从数据层中查询组件需要展示的数据,并展示
- 动态从服务器端获取最新的数据,用最新的数据替换事先生成好的静态数据
初始化数据源插件
在项目更目录下创建文件夹 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`...`
本文介绍了如何在兼顾SEO和快速访问的同时,实现实时更新文章列表。静态生成确保初始加载速度,服务器端动态获取新文章,提升用户体验。
410

被折叠的 条评论
为什么被折叠?



