发布于 2026-01-06 6 阅读
0

Pentagram 页面过渡 – Gatsby.js 深度解析:动态生成项目页面 #1;设计和实现项目页面 #2;添加页面过渡效果 #3;设计增强 #4;添加 Dato CMS #5

Pentagram 页面过渡 – Gatsby.js 深度解析

功能:动态生成项目页面 #1

任务:设计并实施项目页面 #2

功能:添加页面过渡效果 #3

功能:设计改进 #4

功能:添加 Dato CMS #5

简而言之,在这篇博文中,我们使用 Gatsby.js 构建了一个 Pentagram 精美作品集网站的浅克隆版本。页面过渡效果丰富,采用无头 CMS 系统,并大量运用了 React 技术。

https://gatsbygram-clone.netlify.com/
https://github.com/mattrothenberg/gatsbygram


有人说“模仿是最高形式的赞美”。

在我作为设计师/开发人员的整个职业生涯中,我一直尝试将模仿作为一种教育工具,剖析、分析和重新创作那些对我产生影响的图像、网站和应用程序。

请放心,这并非是对剽窃他人作品的某种抽象辩解。恰恰相反,我模仿的初衷并非将他人的作品冒充为自己的作品,而是将其作为灵感和学习的来源。

我之所以提起这件事,是因为今天我们要“模仿”一下我近几年见过的最精美的网站之一——Pentagram.com——的一些细节。如果你不熟悉Pentagram,他们是一家设计公司,为各大客户提供一流的设计服务。

具体来说,我们将使用Gatsby(我们都熟知和喜爱的静态网站生成器)来重现您在项目之间导航时那些漂亮的页面过渡效果,从而学习一些有价值的 Gatsby 技能:

  • 从零开始搭建和配置项目
  • 用一些最新的热门元素来设计网站,例如styled-components
  • 使用来自远程源(例如,无头CMS)的数据填充网站
  • 使用 Gatsby 内置的图像组件和插件系统来渲染优化后的图像

友情提示:我会分享大量代码。不必逐行复制粘贴。我已经尽力将我们将要构建的各种“功能”拆分成gatsbygram 仓库中的分支,并在每个部分的末尾提供相关提交/PR的链接。

为我们的盖茨比项目搭建脚手架

首先,让我们安装 Gatsby CLI(如果您还没有安装的话),创建一个新的项目目录,并安装一些依赖项。

yarn global add gatsby-cli
mkdir gatsbygram
cd gatsbygram
yarn init
yarn add react-dom react gatsby
Enter fullscreen mode Exit fullscreen mode

在此基础上,我们可以添加一些脚本,package.json以便在本地运行开发服务器并构建项目。

"scripts": {
  "develop": "gatsby develop",
  "serve": "gatsby serve",
  "build": "gatsby build"
}
Enter fullscreen mode Exit fullscreen mode

添加页面

目前,我们的网站功能还不太完善。这是因为我们还没有告诉 Gatsby 要渲染哪些页面。

在 Gatsby 的世界里,src/pages目录是特殊的。该目录中的 JS 模块代表我们网站上的离散“路由”(例如,src/pages/index->“首页”,src/pages/about->“关于”页面)。

例如,如果我们向网站添加一个简单的 React 组件,当我们运行并访问http://localhost:8000src/pages/index.js启动网站时,我们将看到它被渲染yarn develop

// src/pages/index.js
import React from 'react'

const Home = () => <div>Hello world</div>

export default Home
Enter fullscreen mode Exit fullscreen mode

然而,对于我们的网站来说,我们无法预先定义页面。我们作品集中的项目都有不同的标题和别名,因此我们需要使用一种机制来动态生成这些页面。幸运的是,Gatsby 为我们提供了一个绝佳的解决方案gatsby-node.js

gatsby-node.js

Gatsby 为插件和网站构建器提供了许多 API,用于控制您的网站。

在[此处gatsby-node.js应填写具体平台名称],我们可以直接与此类 API 进行交互。就我们的目的而言,我们将使用这个createPagesAPI。正如您可能已经猜到的,这个 API 允许我们动态创建页面(例如,根据我们从远程源获取的数据)。

在后续步骤中,我们将返回此文件并从远程数据源(DatoCMS)提取数据,但现在让我们先来思考一下“项目”的架构,并将驱动我们作品集网站的数据结构硬编码到代码中。为了尽快搭建网站,我们也尽量保持简洁。

// gatsby-node.js
const projects = [
  {
    title: 'Project #1',
    slug: 'project-1',
  },
  {
    title: 'Project #2',
    slug: 'project-2',
  },
  // etcetera
]
Enter fullscreen mode Exit fullscreen mode

我们的存根项目数据集已经完成,接下来让我们深入了解一下该createPages函数的实际工作原理。

文件:gatsby-node.js

你会注意到,我们已经对传递给函数的参数进行了解构createPages,从中提取出一个actions包含createPage单个函数的对象。正是这个函数将实现将我们的存根项目数据集转换为实际页面的神奇功能。

实际上,createPage它需要一些值才能执行这种转换。

  1. path您正在尝试构建的页面(例如,/projects/project-1
  2. 我们希望在用户访问此路径时呈现的内容component(可以将其视为一个“模板”,我们将把项目数据放入其中)。
  3. context传递给此组件的 props

例如,它可能看起来像这样……

// The path to our template component
const projectTemplate = path.resolve(`./src/templates/project.js`)

projects.forEach(project => {
  // Rudimentary way to get the "next" project so we can show a preview at the bottom of each project
  const next = projects[index === projects.length - 1 ? 0 : index + 1]

  createPage({
    path: `/projects/${project.slug}`,
    component: projectTemplate,
    context: {
      ...project,
      next,
    },
  })
})
Enter fullscreen mode Exit fullscreen mode

其中component,我们的模板是另一个简单的 React 组件。

import React from 'react'

const Project = ({ pageContext: project }) => <div>{project.title}</div>

export default Project
Enter fullscreen mode Exit fullscreen mode

createPages脚本准备就绪后,我们可以重启开发服务器(通过yarn develop),然后访问http://localhost:8000/projects/project-1。虽然不太优雅,但确实能完成任务。

我们现在拥有了动态生成的页面!是时候让项目页面动起来了。

视觉与交互设计

接下来是精彩的部分!在本节中,我们将安装和配置用于网站样式设计的工具集。

我个人非常喜欢Rebass,以及相关的styled-components。让我们来安装这些依赖项。

yarn add rebass gatsby-plugin-styled-components styled-components babel-plugin-styled-components
Enter fullscreen mode Exit fullscreen mode

你会注意到其中一个依赖项是[此处应填写依赖项名称gatsby-plugin-styled-components]。Gatsby 的另一个强大之处在于其一流的插件 API,开发者可以利用它来扩展库的核心功能。这里,我稍微挥挥手,我们添加了一些代码,用于配置和支持[此处应填写styled-components依赖项名称]。但是,安装依赖项只是整个过程中的一步。我们还需要让 Gatsby 知道该如何使用它。

进入gatsby-config.js

gatsby-config.js

这是另一个“神奇”的配置文件(类似于gatsby-node.js我们在上一步中看到的),但本质上它是一个 Gatsby 网站正在使用的所有插件的清单。我们只需要在这里指定我们的插件,然后继续下一步即可。

module.exports = {
  plugins: [`gatsby-plugin-styled-components`],
}
Enter fullscreen mode Exit fullscreen mode

布局

如今大多数网站都采用熟悉的组织结构,将任意的“主要内容”夹在页眉和页脚之间。

// Layout.jsx
const Layout = () => (
  <>
    <Header />
    {
      // main content
    }
    <Footer />
  </>
)
Enter fullscreen mode Exit fullscreen mode

我们将遵循类似的模式,但原因有二——

  1. Header我们的视觉/交互设计要求项目页面之间保持一致。
  2. 我们需要一个组件,该组件styled-components使用我们网站的主题进行初始化,并将此主题信息向下传递给其子组件。

Layout.jsx所以,让我们对上面那个不太严谨的例子稍作修改——

import React from 'react'
import { ThemeProvider } from 'styled-components'
import { Box } from 'rebass'

// A very simple CSS reset
import '../style/reset.css'

const theme = {
  fonts: {
    sans: 'system-ui, sans-serif',
  },
  colors: {
    grey: '#999',
    black: '#1a1a1a',
    red: '#e61428',
  },
}

const Layout = ({ children }) => (
  <ThemeProvider theme={theme}>
    <React.Fragment>
      <Box as="header">silly header</Box>
      <Box as="main">{children}</Box>
    </React.Fragment>
  </ThemeProvider>
)

export default Layout
Enter fullscreen mode Exit fullscreen mode

至于我们自己的项目theme,我浏览了一下 Pentagram 的网站,并从中提取了一些设计细节——

  • 他们使用的是漂亮的Neue Haas Grotesk字体,但我们将改用系统字体。
  • 他们网站上真正使用的“颜色”只有blackgreyred。照片本身已经传达了大部分视觉复杂性。

项目标题组件

Pentagram 网站上的每个项目页面似乎都遵循以下结构——

const Project = () => (
  <>
    {/* Title + description + category + hero image */}
    <ProjectHeader />

    {/* Photo grid */}
    <ProjectPhotos />

    {/* Title + description + category + truncated preview of hero image of NEXT project */}
    <ProjectHeader truncated />
  </>
)
Enter fullscreen mode Exit fullscreen mode

请注意,它ProjectHeader出现了两次。您可能会问,为什么会出现两次?这是为了方便您点击下一个项目预览(位于任何项目页面的底部)时看到的流畅页面过渡效果。我们稍后会详细介绍这种过渡效果,但现在,我们只需知道,我们的ProjectHeader元素需要存在于两种状态——

  1. 项目的默认状态title,其中description,,categoryhero图像将可见。
  2. truncated我们隐藏了图像的大部分,作为hero列表中下一个项目的预告。

我设想我们的ProjectHeader组件大概会是这样的。我们会利用 `<style>`FlexBox`<style>` 辅助组件rebass,并使用它们styled-components为页面上的各个排版元素添加一些视觉样式(例如,字体粗细、字体大小和颜色)。

const ProjectHeader = ({ project, truncated }) => (
  <Box>
    <Flex>
      <Box>
        <Title as="h1">title goes here</Title>
        <Box>
          <Category as="h3">category goes here</Category>
        </Box>
      </Box>
      <Box>
        <Box>
          <Description as="h2">description goes here...</Description>
        </Box>
      </Box>
    </Flex>
    <Hero truncated={truncated} />
  </Box>
)
Enter fullscreen mode Exit fullscreen mode

不过请注意,我们将truncatedprop 一直传递到了Hero组件,目前该组件渲染的是一个 8:5 宽高比的灰色方框。通过传递这个 prop,我们可以将组件渲染成ProjectHeader前面提到的两种状态:默认状态和“截断”状态。

const HeroWrap = styled(Box)`
  ${props =>
    props.truncated &&
    css`
      max-height: 200px;
      overflow: hidden;
    `}
`

const Hero = ({ truncated }) => (
  <HeroWrap mt={[4, 5]} truncated={truncated}>
    <AspectRatioBox ratio={8 / 5} />
  </HeroWrap>
)
Enter fullscreen mode Exit fullscreen mode

别担心,我们会在教程的后续部分对这个组件进行调整。不过现在,我们已经具备了开始所需的条件。

相关提交:7f0ff3f

项目内容

夹在这两个ProjectHeader组件之间的,正是项目内容!

鉴于我们目前没有任何“真实”的项目数据可供参考,我们将完全用占位符来模拟这部分内容。我们将用这种方式搭建一个漂亮的占位符网格。

import React from 'react'
import { Box } from 'rebass'
import styled from 'styled-components'

import AspectRatioBox from './aspect-ratio-box'

const Grid = styled(Box)`
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-gap: ${props => props.theme.space[4]}px;
`

const ProjectContent = () => (
  <Box my={4}>
    <Grid>
      <AspectRatioBox ratio={8 / 5} />
      <AspectRatioBox ratio={8 / 5} />
      <AspectRatioBox ratio={8 / 5} />
      <AspectRatioBox ratio={8 / 5} />
    </Grid>
  </Box>
)

export default ProjectContent
Enter fullscreen mode Exit fullscreen mode

不错!

让我们回到Project模板组件,添加这些元素,然后提交更改。

const Project = ({ pageContext: project }) => (
  <Layout>
    <ProjectHeader project={project} />
    <ProjectContent />
    <ProjectHeader project={project.next} truncated />
  </Layout>
)
Enter fullscreen mode Exit fullscreen mode

过渡

虽然我们还有很多工作要做,但让我们先从有趣的部分开始:实现我们在 Pentagram 网站上看到的页面过渡效果。

在深入代码部分之前,让我们先来梳理一下这个过渡过程中究竟发生了什么。

  1. 消失ProjectContent得相对较快(几百秒内ms)。
  2. 内容淡出后,ProjectHeader下一个项目的截断部分会滑到页面的“顶部”,有效地过渡到ProjectHeader即将加载的页面。

很简单,对吧?细节决定成败😈。

但幸运的是,很多繁重的工作已经有人替我们完成了。让我们使用一个名为 的出色库gatsby-plugin-transition-link,它:

它提供了一种通过 Link 组件的 props 来描述页面过渡的简单方法。对于进入和退出页面,您可以指定多个时间值,将状态传递给这两个页面,并分别触发一个函数。

yarn add gatsby-plugin-transition-link
Enter fullscreen mode Exit fullscreen mode

正如我们之前看到的,让我们把这个插件添加到我们的……gatsby-config.js

module.exports = {
  plugins: [`gatsby-plugin-styled-components`, `gatsby-plugin-transition-link`],
}
Enter fullscreen mode Exit fullscreen mode

现在,为了开始使用这个库,我们需要对Project模板组件进行一些修改。

实际上,这个过渡插件的工作原理是,它公开了一个TransitionLink组件,我们可以用它来代替 Gatsby 的内置Link组件(该组件有一些神奇的功能,但实际上只是提供了一种在页面之间进行路由的机制)。

import TransitionLink from 'gatsby-plugin-transition-link'

const Project = ({ pageContext: project }) => {
  const nextProjectUrl = `/projects/${project.next.slug}`

  return (
    <Layout>
      <ProjectHeader project={project} />
      <ProjectContent />
      <TransitionLink to={nextProjectUrl}>
        <ProjectHeader project={project.next} truncated />
      </TransitionLink>
    </Layout>
  )
}
Enter fullscreen mode Exit fullscreen mode

请注意,通过将ProjectHeader组件包裹在 `<div>` 标签中TransitionLink,我们实际上将其变成了指向作品集中下一个项目的超链接。而且它确实有效!但它并没有触发上面 GIF 动画中展示的那种漂亮的页面过渡效果。为此,我们需要一些其他工具。

我们首先需要的是gatsby-plugin-transition-linkTransitionState 中的另一个组件。

简而言之,这个组件的作用是暴露一个transitionStatus属性,用于描述过渡动画当前所处的状态,可以是:exiting、、或。这非常有用,因为它提供了我们声明过渡动画工作方式所需的信息。虽然这个插件也提供了一些钩子,允许我们以更命令式的方式执行过渡动画exited例如,使用类似这样的库),但我更倾向于这种声明式方法,原因将在接下来的几个步骤中阐明。enteringenteredgsap

Project为了开始使用这些数据,让我们按以下方式重构模板组件。

  1. 将模板中的展示元素移至功能组件中,ProjectInner
  2. 引入<TransitionState>,它接受一个“函数作为子对象”,并向其传递一个transitionStatus属性,该属性描述了转换在其生命周期中的位置。
const ProjectInner = ({ transitionStatus, project }) => {
  const nextProjectUrl = `/projects/${project.next.slug}`
  return (
    <Layout>
      <ProjectHeader project={project} />
      <ProjectContent />
      <TransitionLink to={nextProjectUrl}>
        <ProjectHeader project={project.next} truncated />
      </TransitionLink>
    </Layout>
  )
}

const Project = ({ pageContext: project }) => {
  return (
    <TransitionState>
      {({ transitionStatus }) => (
        <ProjectInner transitionStatus={transitionStatus} project={project} />
      )}
    </TransitionState>
  )
}
Enter fullscreen mode Exit fullscreen mode

就这样,我们ProjectInner现在可以使用transitionStatusprop 来声明我们在本节开头概述的过渡步骤(例如,淡入内容、向上滑动标题)。

如前所述,我非常欣赏React 所倡导的声明式编程思维。回想过去jQuery,我们用命令式的方式告诉程序addClass该做什么或fadeOut该做什么,React 这种声明式编程方式——只需声明我们想要做什么,其余的都由库来处理——简直令人耳目一新。

话虽如此,声明式编程风格也可能让人完全摸不着头脑,尤其是在动画方面。如果你和我一样,你可能用过TweenMaxGreensock 库之类的工具来学习动画。总的来说,TweenMax它遵循的是一种非常命令式(当然也很强大)的方法。例如,我们或许可以用这样的代码来实现过渡效果:

// Fade out the main content
TweenMax.to(mainContentEl, 1, { opacity: 0 })

// Slide up the header
TweenMax.to(nextHeaderEl, 1, { y: nextYPos, delay: 250 })

// Profit 😎
Enter fullscreen mode Exit fullscreen mode

今天,我们将摒弃这种方法,转而采用声明式方法。为此,我们将使用我最喜欢的 React 库之一Pose

Pose需要我们使用以下 API “声明”我们的转换。

const FadingBox = posed.div({
  visible: { opacity: 1 },
  hidden: { opacity: 0 },
})
Enter fullscreen mode Exit fullscreen mode

然后,我们可以FadingBox像使用其他 React 组件一样使用它。不同之处在于,它FadingBox公开了一个prop,我们可以向其传递一个字符串值。如果该字符串值与 Pose 实例上定义的某个键(在本例中为 `position`或 `position: 'state'`)pose匹配,则组件将自动触发到该特定状态的过渡。visiblehidden

<!-- Now you see me 👀 -->
<FadingBox pose="visible" />

<!-- Now you don't 🙈 -->
<FadingBox pose="hidden" />
Enter fullscreen mode Exit fullscreen mode

那我为什么要解释这么多背景信息呢?你可能还记得,我们现在组件里有一个特殊的transitionStatus属性ProjectInner,它用来声明过渡动画所处的状态。让我们用这个属性来实现过渡动画的第一步:淡出主要内容。

我们首先要做的是构建Pose实例。

// Transition to {opacity: 0} when pose === 'exiting'
const FadingContent = posed.div({
  exiting: { opacity: 0 },
})
Enter fullscreen mode Exit fullscreen mode

然后我们将当前项目的头部和内容包裹在实例中。

<FadingContent pose={transitionStatus}>
  <ProjectHeader project={project} />
  <ProjectContent />
</FadingContent>
Enter fullscreen mode Exit fullscreen mode

但你会注意到,目前实际上什么都没发生。这是因为我们需要告诉组件各个转换应该持续TransitionLink多长时间,以及描述这些转换开始和结束时我们希望发生什么。entryexit

// For now, let's use this as a magic number that describes how long our transition should take
const TRANSITION_LENGTH = 1.5

const exitTransition = {
  length: TRANSITION_LENGTH, // Take 1.5 seconds to leave
  trigger: () => console.log('We are exiting'),
}

const entryTransition = {
  delay: TRANSITION_LENGTH, // Wait 1.5 seconds before entering
  trigger: () => console.log('We are entering'),
}

// Let's pass these hooks as props to our TransitionLink component
<TransitionLink
  to={nextProjectUrl}
  exit={exitTransition}
  entry={entryTransition}
/>
Enter fullscreen mode Exit fullscreen mode

保存并刷新浏览器。恭喜,您刚刚实现了第一个(虽然有点生硬的)过渡效果!

接下来我们来看下一个过渡部分,这确实有点棘手。首先,我们需要移除页面底部添加的text-decoration样式,因为除了被截断的首页图片之外,这个页眉应该看起来和上面的页眉完全一样。TransitionLinkProjectHeader

<TransitionLink
  style={{
    textDecoration: 'none',
    color: 'inherit',
  }}
/>
Enter fullscreen mode Exit fullscreen mode

接下来,让我们定义pose滑动的规则<ProjectHeader />

const SlidingHeader = posed.div({
  exiting: {
    y: ({ element }) => {
      // This is an alternative API that allows us to dynamically generate a "y" value.

      // When scrolling back to the top, how far should we actually go? Let's factor the height of our site's header into the equation.
      const navbar = document.querySelector('header')
      const navbarDimensions = navbar.getBoundingClientRect()
      const distanceToTop =
        element.getBoundingClientRect().top - navbarDimensions.height

      // And return that aggregate distance as the dynamic "y" value.
      return distanceToTop * -1
    },
    transition: {
      ease: [0.59, 0.01, 0.28, 1], // Make the transition smoother
      delay: 250, // Let's wait a tick before starting
      duration: TRANSITION_LENGTH * 1000 - 250, // And let's be sure not to exceed the 1.5s we have allotted for the entire animation.
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

这段代码块包含了很多内容。关键在于,你可以动态生成转换状态。你无需将这些值硬编码,尤其是在触发转换之前Pose需要执行某种计算(例如我们的计算)的情况下。distanceToTop

不过,我们还需要念诵一些其他的咒语。

首先,让我们将截断的部分包裹ProjectHeader在我们新创建的 Pose 中。

<SlidingHeader pose={transitionStatus}>
  <ProjectHeader project={project.next} truncated={shouldTruncate} />
</SlidingHeader>
Enter fullscreen mode Exit fullscreen mode

truncated你会注意到,我们不再为 `<project_name>` 硬编码一个真值,而是传递一个名为 `<project_name>` 的变量。这样做是因为我们现在只想在页面切换到下一页后才shouldTruncate截断下一个项目的首页图片。在页面切换期间,我们希望显示截断后的内容,这样可以使切换更加自然流畅。

const shouldTruncate = ['entering', 'entered'].includes(transitionStatus)
Enter fullscreen mode Exit fullscreen mode

最后,我们需要在组件中添加一些必要的细节(我知道这很虚伪)TransitionLink,以使实际过渡更加平滑。

const exitTransition = {
  length: TRANSITION_LENGTH,
  trigger: () => {
    if (document) {
      // Preventing overflow here make the animation smoother IMO
      document.body.style.overflow = 'hidden'
    }
  },
}

const entryTransition = {
  delay: TRANSITION_LENGTH,
  trigger: () => {
    if (document && window) {
      // Ensuring we're at the top of the page when the page loads
      // prevents any additional JANK when the transition ends.
      window.scrollTo(0, 0)
      document.body.style.overflow = 'visible'
    }
  },
}
Enter fullscreen mode Exit fullscreen mode

就是这样。

抛光

为了尽快添加这些漂亮的页面过渡效果,我们忽略了一些设计细节。

  • 我们的<header>标题太小了,页面加载时不会淡入,而且还显示着“愚蠢的标题”字样🙈
  • 我们省略了在过渡开始时逐渐淡出的截断页脚上方的“下一个项目”标题。

Logo 和标题过渡

让我们在项目中添加一个廉价且丑陋的 Pentagram 标志复制品(对不起,Pentagram)/src/components/logo.svg。然后我们可以将其导入并添加到Layout组件中。不过,趁此机会,我们再创建一个 Pose 实例,以便在页面过渡时实现标题的淡入淡出效果。

import { Box, Image } from 'rebass'

const FadingHeader = posed.header({
  exiting: { opacity: 0 },
  exited: { opacity: 0 },
  entering: { opacity: 1 },
  entered: { opacity: 1 },
})

const Layout = ({ children, transitionStatus }) => (
  <ThemeProvider theme={theme}>
    <React.Fragment>
      <FadingHeader pose={transitionStatus}>
        <Box px={[3, 5]} py={4}>
          <Image src={Logo} alt="Gatsbygram Logo" height={32} />
        </Box>
      </FadingHeader>
      <Box as="main" px={[3, 5]}>
        {children}
      </Box>
    </React.Fragment>
  </ThemeProvider>
)
Enter fullscreen mode Exit fullscreen mode

等等……这个值是怎么transitionStatus作为属性传递给这个组件的?还记得我们Layout在组件模板中是如何使用这个组件的吗Project?从那里,我们可以简单地将这个值作为属性传递下去,然后让它Layout随意处理它。

<Layout transitionStatus={transitionStatus}>project content goes here</Layout>
Enter fullscreen mode Exit fullscreen mode

“下一个项目”标题和过渡

既然我们已经是经验丰富的过渡专家,那就让我们通过在截断的上方添加一个额外的设计元素来展示我们的技能吧ProjectHeader

// next-project-heading.jsx

import React from 'react'
import { Box, Text } from 'rebass'
import styled from 'styled-components'

const Heading = styled(Text)`
  color: ${props => props.theme.colors.red};
  font-family: ${props => props.theme.fonts.sans};
  font-size: ${props => props.theme.fontSizes[3]}px;
  font-weight: normal;
`

const Rule = styled.hr`
  background: #e3e4e5;
  height: 1px;
  border: 0;
`

const NextProjectHeading = () => (
  <Box mb={5}>
    <Rule />
    <Heading pt={3}>Next Project</Heading>
  </Box>
)

export default NextProjectHeading
Enter fullscreen mode Exit fullscreen mode

最后,让我们把它插入到我们的Project模板组件中。

const FadingNextProjectHeading = posed.div({
  exiting: { opacity: 0 },
})

<TransitionLink
  style={{
    textDecoration: 'none',
    color: 'inherit',
  }}
  to={nextProjectUrl}
  exit={exitTransition}
  entry={entryTransition}
>
  <FadingNextProjectHeading pose={transitionStatus}>
    <NextProjectHeading />
  </FadingNextProjectHeading>
  <SlidingHeader pose={transitionStatus}>
    <ProjectHeader project={project.next} truncated={shouldTruncate} />
  </SlidingHeader>
</TransitionLink>
Enter fullscreen mode Exit fullscreen mode

通过 DatoCMS 进行远程数据传输

我目前最喜欢的CMS之一是DatoCMS。你可能会问为什么?

  1. 他们提供相当慷慨的免费套餐。
  2. 他们有一个一流的 Gatsby 插件(用于获取数据)。
  3. 它与Gatsby Preview兼容

使用免费帐户,我们将能够用来自无头 CMS 的实际数据替换硬编码的项目数据集!

创建账户后,登录并创建一个Project模型,其架构类似于这样。

const project = {
  title: 'single-line-string',
  description: 'multiple-paragraph-text',
  featuredPhoto: 'single-file',
  photos: 'multiple-files',
  slug: 'seo',
  category: 'link',
}
Enter fullscreen mode Exit fullscreen mode

模型搭建完成后,就可以添加几个不同的项目了。不必追求完美,只需添加几个,并确保填写标题、描述、类别,并添加照片即可。

添加几个项目之后,我们就可以开始利用这些远程数据构建我们的 Gatsby 网站了。

yarn add gatsby-source-datocms gatsby-transformer-sharp gatsby-plugin-sharp gatsby-image
Enter fullscreen mode Exit fullscreen mode

现在,在gatsby-config.js添加此插件之前,我们需要在根目录中添加一个 ` .env.development.dato.conf` 和 `.dato.conf`.env.production文件,并确保这些文件被版本控制系统忽略(以免意外泄露 DatoCMS 凭据)。请获取您的 Dato API 密钥和站点 URL,并将这些值添加到.env您创建的相应文件中。目前,开发环境和生产环境的凭据相同,但您可以随时生成不同的凭据。

DATO_API_TOKEN=""
Enter fullscreen mode Exit fullscreen mode

然后,在顶部gatsby-config.js,让我们引入dotenv库,以便将这些环境变量提取到内存中,并在模块中使用。

require('dotenv').config({
  path: `.env.${process.env.NODE_ENV}`,
})

module.exports = {
  plugins: [
    `gatsby-transformer-sharp`, // for eventual image manipulation
    `gatsby-plugin-sharp`, // for eventual image manipulation
    {
      resolve: `gatsby-source-datocms`,
      options: {
        apiToken: process.env.DATO_API_TOKEN,
        apiUrl: 'https://site-api.datocms.com',
      },
    },
    ...etcetera,
  ],
}
Enter fullscreen mode Exit fullscreen mode

下次运行程序时yarn develop,请格外注意输出结果,该结果显示:

View GraphiQL, an in-browser IDE, to explore your site's data and schema
  http://localhost:8000/___graphql
Enter fullscreen mode Exit fullscreen mode

Gatsby 内置了一个 GraphQL 浏览器(功能丰富,包括强大的自动补全功能),我们可以用它来构建查询,获取项目数据。将以下查询语句粘贴到左侧,即可看到 DatoCMS 数据呈现在您眼前!

{
  projects: allDatoCmsProject {
    edges {
      node {
        title
        slug
      }
      next {
        title
        slug
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

但我们的网站目前还没有使用这些数据来动态创建项目页面。为了实现这一点,我们需要返回gatsby-node.js并进行一些调整。

首先,让我们把解构graphql作为createPages方法的一个附加参数。

exports.createPages = async ({ graphql, actions }) => {}
Enter fullscreen mode Exit fullscreen mode

接下来,我们迭代并保存上面提到的 GraphQL 查询结果,将其作为局部变量。注意,我们并没有获取每个项目的所有信息(例如,描述、特色照片等)。我非常喜欢将这类数据获取操作延迟到Project组件模板中。稍后会详细介绍。

const query = `{
  projects:allDatoCmsProject {
    edges {
      node {
        title
        slug
        description
      }
      next {
        title
        slug
      }
    }
  }
}`
Enter fullscreen mode Exit fullscreen mode

最后,让我们实际运行该 Graphql 查询,并调整我们的页面创建逻辑,以适应 DatoCMS 返回的数据的新形状/结构。

// Run the query
const result = await graphql(query)

// Abort if there were errors
if (result.errors) {
  throw new Error(result.errors)
}

const projects = result.data.projects.edges

const createProjectPage = project => {
  // Our GraphQL response actually tells us what the "next" node is, which is great! In the case of the last project in the list, let's default "next" to the very first project so we have a nice "carousel" of projects on our site.
  const next = project.next || projects[0].node

  createPage({
    path: `/projects/${project.node.slug}`,
    component: projectTemplate,
    context: {
      nextSlug: next.slug,
      ...project.node,
    },
  })
}

projects.forEach(createProjectPage)
Enter fullscreen mode Exit fullscreen mode

如果一切顺利,该程序应该可以无错误地运行,并且您应该能够打开浏览器访问http://localhost:8000/projects/SLUG,其中 slug 是 DatoCMS 根据您的项目标题自动生成的 slug 之一。

我们离终点线越来越近了!但我们还需要获取项目剩余的详细信息。现在就去Project组件模板看看吧!

你可能会问:“等等,为什么我们不直接在这里获取所有数据呢?” 我认为,获取项目其余数据还有更好的方法gatsby-node.js。实际上,这个文件的主要作用是构建网站的“框架”,列出各个页面,而无需获取每个页面所需的所有数据。你可以把我们刚才的操作想象成构建网站的“外壳”。之后,我们可以返回Project模板组件,获取实现页面所需的数据。

接下来project.js,我们导入 Gatsby 的内置graphql函数。然后在文件末尾,我们编写并导出另一个查询,以获取我们需要的剩余数据。

import { graphql } from 'gatsby'

// component boilerplate
export const query = graphql`
  query($slug: String!, $nextSlug: String!) {
    project: datoCmsProject(slug: { eq: $slug }) {
      description
      category {
        title
      }
      featuredPhoto {
        fluid {
          ...GatsbyDatoCmsFluid
        }
      }
      photos {
        fluid {
          ...GatsbyDatoCmsFluid
        }
      }
    }
    next: datoCmsProject(slug: { eq: $nextSlug }) {
      title
      slug
      description
      category {
        title
      }
      featuredPhoto {
        fluid {
          ...GatsbyDatoCmsFluid
        }
      }
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

几点说明。

  • 我撒谎了。实际上我们写了两个查询。一个用于获取当前项目的所有数据,另一个用于获取渲染队列中下一个项目所需的信息。注意这两个查询分别用ProjectHeader别名表示project:next:
  • GatsbyDatoCmsFluid这是一个 GraphQL 片段(实际上是一段共享的查询逻辑),它来自 Gatsby gatsby-source-datocms。该片段返回一个对象,该对象可以无缝地嵌入到 Gatsby 的Img组件中,该组件负责渲染图像。你可能会问,为什么要使用这个库?嗯……

它结合了 Gatsby 的原生图像处理功能和先进的图像加载技术,可以轻松全面地优化网站的图像加载。gatsby-image 使用 gatsby-plugin-sharp 来驱动其图像转换功能。

所以,我们已经导出了这段详细的 GraphQL 查询,但仍然没有任何反应!这是因为在底层,Gatsby 将一个dataprop 注入到了我们的页面组件中,project.js但我们实际上并没有对它进行任何操作。接下来,让我们创建一个project包含来自远程 DatoCMS 数据的真实页面,并将传递给 Gatsby ProjectInner

const Project = ({ pageContext: projectShell, data }) => {
  const { project, next } = data // GraphQL results
  const aggregateProject = {
    ...projectShell,
    ...project,
    next,
  }

  return (
    <TransitionState>
      {({ transitionStatus }) => (
        <ProjectInner
          transitionStatus={transitionStatus}
          project={aggregateProject}
        />
      )}
    </TransitionState>
  )
}
Enter fullscreen mode Exit fullscreen mode

鉴于我们的project数据结构已经发生变化,我们需要相应地重构一些展示组件。让我们从……开始ProjectHeader

之前,我们对 `<value>`、`<value>` 和图像进行了硬编码categorydescription对于hero前两个值,我们可以直接从 prop 中提取字段project,例如:

<Category as="h3">{project.category.title}</Category>
Enter fullscreen mode Exit fullscreen mode

但是,对于我们的hero图像,我们实际上需要将其project.featuredPhoto作为 prop 传递,并利用库Img中的组件gatsby-image来渲染实际的图像。

const Hero = ({ photo, truncated }) => {
  return (
    <HeroWrap mt={[4, 5]} truncated={truncated}>
      <AspectRatioBox ratio={8 / 5}>
        <Img fluid={photo.fluid} />
      </AspectRatioBox>
    </HeroWrap>
  )
}
Enter fullscreen mode Exit fullscreen mode

接下来我们需要修复的组件是 `<div>` ProjectContent,因为它目前被硬编码为返回一个 8:5 的占位符网格。我们需要将project.photos`<div>` 作为 prop 传递,遍历集合,并Img据此渲染组件。

const ProjectContent = ({ photos }) => {
  return (
    <Box my={4}>
      <Grid>
        {photos.map((photo, index) => {
          return (
            <AspectRatioBox key={index} ratio={8 / 5}>
              <Img fluid={photo.fluid} />
            </AspectRatioBox>
          )
        })}
      </Grid>
    </Box>
  )
}
Enter fullscreen mode Exit fullscreen mode

就这样,我们的项目页面就完成了。

添加首页

我们的网站目前唯一缺少的就是一个漂亮的首页,用来列出所有项目。幸运的是,我们现在都是 Gatsby 专家了,应该能轻松搭建一个首页来实现这个目标。

我们先来编写一个 GraphQL 查询,获取我们需要的所有数据。

export const query = graphql`
  {
    projects: allDatoCmsProject {
      edges {
        node {
          slug
          title
          featuredPhoto {
            fluid {
              ...GatsbyDatoCmsFluid
            }
          }
        }
      }
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

接下来,只需遍历我们的数据集,并在页面上渲染一些项目即可!

const Home = ({ data }) => {
  const projects = data.projects.edges
  return (
    <Layout>
      <Grid>
        {projects.map(project => (
          <ProjectGridItem key={project.node.title} project={project.node} />
        ))}
      </Grid>
    </Layout>
  )
}
Enter fullscreen mode Exit fullscreen mode
const ProjectGridItem = ({ project }) => {
  return (
    <AniLink
      style={{ textDecoration: 'none' }}
      fade
      to={`/projects/${project.slug}`}
      duration={0.2}
    >
      <Box>
        <Img fluid={project.featuredPhoto.fluid} />
        <Box mt={3}>
          <Description>{project.title}</Description>
        </Box>
      </Box>
    </AniLink>
  )
}
Enter fullscreen mode Exit fullscreen mode

这一次,我选择使用AniLink来自 的组件gatsby-plugin-transition-link

AniLink 是 TransitionLink 组件的封装,提供了四种默认过渡效果:淡入淡出、滴画、滑动和覆盖。

在底层,它用于gsap执行实际的过渡效果。

如果您想为您的网站(或至少网站的某些部分)添加简单的页面过渡效果,我绝对推荐使用AniLinkover 。TransitionLink

提交:https://github.com/mattrothenberg/gatsbygram/commit/ffdcc67ad1aa02f2d6ca85ea58ebdc900bb2c0fc

结论

呼,真有趣!我希望我们这次从零开始搭建 Gatsby 网站的教程能让你更有信心去搭建一个属于你自己的独特网站!

非常感谢您能就如何改进这篇文章/使其更有帮助提出宝贵意见!

我也很想知道你们还希望我写些关于 Gatsby 的其他主题!

文章来源:https://dev.to/mattrothenberg/recreating-pentagram-com-a-deep-dive-with-gatsby-js-h75