React Helmet:使用 React Helmet 管理 React 网站的文档头部

Avatar of Darrell Huffman
Darrell Huffman 发表于

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

文档头部可能不是网站中最引人注目的部分,但其中包含的内容对于网站的成功而言,与其用户界面同等重要。毕竟,在这里,您可以告诉搜索引擎有关您的网站的信息,并将其与 Facebook 和 Twitter 等第三方应用程序集成,更不用说您加载并在其中初始化的资产了,这些资产从分析库到样式表不等。

React 应用程序存在于其挂载到的 DOM 节点中,考虑到这一点,如何使文档头的内容与您的路由保持同步并不明显。一种方法可能是使用 componentDidMount 生命周期方法,如下所示

componentDidMount() {
  document.title = "Whatever you want it to be";
}

但是,您不仅希望更改文档的标题,还希望修改元标签和其他标签的数组,并且很快您就会得出结论,以这种方式管理文档头的内容很快就会变得乏味且容易出错,更不用说您最终得到的代码将毫无语义可言。显然必须有更好的方法来使文档头与您的 React 应用程序保持最新。正如您可能根据本教程的主题所怀疑的那样,有一个简单易用的组件称为 React Helmet,该组件由美国国家橄榄球联盟(!)开发并维护。

在本教程中,我们将探讨 React Helmet 的许多常见用例,这些用例从设置文档标题到向文档主体添加 CSS 类。等等,文档主体?本教程不应该关于如何使用文档头部吗?好吧,我有好消息要告诉你:React Helmet 也允许你使用 <html><body> 标签的属性;并且不用说,我们也必须研究如何做到这一点!

查看代码库

本教程的一个重要注意事项是,我将要求您安装 Gatsby——一个构建在 React 之上的静态网站生成器——而不是 Create React App。这是因为 Gatsby 开箱即用地支持 服务器端渲染SSR),如果我们确实想要充分利用 React Helmet 的强大功能,我们将不得不使用 SSR!

您可能会问自己,为什么 SSR 重要到足以在关于管理 React 应用程序的文档头的教程中引入一个完整的框架?答案在于搜索引擎和社交媒体爬虫在抓取通过异步 JavaScript 生成的内容方面做得非常糟糕。这意味着,在没有 SSR 的情况下,文档头内容是否与 React 应用程序保持最新并不重要,因为 Google 将不知道它。幸运的是,您会发现,开始使用 Gatsby 并不比开始使用 Create React App 更复杂。我很有信心地说,如果这是您第一次遇到 Gatsby,那将不是最后一次!

开始使用 Gatsby 和 React Helmet

像这样的教程通常会先安装我们将要使用的依赖项。

让我们首先安装 Gatsby 命令行界面

npm i -g gatsby-cli

虽然 Gatsby 的 入门库 包含大量提供大量内置功能的项目,但我们将限制自己使用最基本的入门项目,即 Gatsby 的 Hello World 项目

在您的终端中运行以下命令

gatsby new my-hello-world-starter https://github.com/gatsbyjs/gatsby-starter-hello-world

my-hello-world-starter 是您项目的名称,因此,如果您想将其更改为其他名称,请随时这样做!

安装完入门项目后,通过在终端中运行 cd [name of your project]/ 导航到其根目录,然后在那里运行 gatsby develop。您的站点现在正在 http://localhost:8000 运行,如果您打开并编辑 src/pages/index.js,您会注意到您的站点会立即更新:Gatsby 会处理我们所有的热重载需求,而无需我们考虑——更不用说触碰——Webpack 配置文件。就像 Create React App 一样!虽然我建议所有 JavaScript 开发人员学习如何使用 Webpack 设置和配置项目,以便深入了解其工作原理,但将所有 Webpack 样板代码抽象出来确实很好,这样我们就可以专注于学习 React Helmet 和 Gatsby!

接下来,我们将安装 React Helmet

npm i --save react-helmet

之后,我们需要安装 Gatsby Plugin React Helmet 以启用使用 React Helmet 添加的数据的服务器端渲染

npm i --save gatsby-plugin-react-helmet

当您想在 Gatsby 中使用插件时,您始终需要将其添加到项目目录根目录下的 gatsby-config.js 文件中的 plugins 数组中。Hello World 入门项目没有附带任何插件,因此我们需要自己创建此数组,如下所示

module.exports = {
  plugins: [`gatsby-plugin-react-helmet`]
}

太好了!我们所有的依赖项现在都已就绪,这意味着我们可以继续进行业务方面的事情了。

第一次使用 React Helmet

我们需要回答的第一个问题是 React Helmet 应该放在应用程序的什么位置。由于我们将在所有页面上使用 React Helmet,因此将其与页面头部和页脚组件嵌套在一起是有意义的,因为它们也将用于我们网站的每个页面。此组件将包装我们所有页面上的内容。在 React 术语中,这种类型的组件通常称为“布局”组件。

src 目录中,创建一个名为 components 的新目录,并在其中创建一个名为 layout.js 的文件。完成后,将下面的代码复制粘贴到此文件中。

import React from "react"
import Helmet from "react-helmet"

export default ({ children }) => (
  <>
    <Helmet>
      <title>Cool</title>
    </Helmet>
    <div>
      <header>
        <h1></h1>
        <nav>
          <ul>
          </ul>
        </nav>  
      </header>
      {children}
      <footer>{`${new Date().getFullYear()} No Rights Whatsoever Reserved`}</footer>
    </div>
  </>
)

让我们分解一下这段代码。

首先,如果您不熟悉 React,您可能会问自己,包装 React Helmet 组件和头部和页脚元素的空标签是怎么回事。答案是,如果您尝试从组件中返回多个元素,React 会变得很疯狂并抛出错误,并且很长一段时间以来,除了将元素嵌套在父元素(通常是 div)中之外别无选择,这导致了元素检查器体验明显令人不快,充满了毫无意义的 div。这些空标签是声明 Fragment 组件的简写方式,作为 React 解决此问题的一种方法而被引入。它们允许我们从组件中返回多个元素,而无需添加不必要的 DOM 膨胀。

这是一个很大的绕道,但如果您像我一样,不介意适量地了解代码相关的琐事。无论如何,让我们继续代码的 <Helmet> 部分。正如您可能从粗略一瞥中推断出的那样,我们在这里设置文档的标题,并且我们使用的方式与在普通 HTML 文档中完全相同;与我在本教程引言中编写的笨拙方法相比,这是一个很大的改进!但是,标题是硬编码的,我们希望能够动态设置它。在我们看看如何做到这一点之前,我们将使用我们花哨的 Layout 组件。

转到 src/pages/ 并打开 ìndex.js。用以下内容替换现有代码

import React from "react"
import Layout from "../components/layout"

export default () => 
  <Layout>
    <div>I live in a layout component, and life is pretty good here!</div>
  </Layout>

这将 Layout 组件导入应用程序并为其提供标记。

使内容动态化

在 React 中硬编码内容没有多大意义,因为 React 的主要卖点之一是它可以轻松创建可重用的组件,这些组件可以通过向其传递 props 来进行自定义。当然,我们希望能够使用 props 来设置文档的标题,但我们到底希望标题是什么样子?通常,文档标题以网站名称开头,后跟分隔符,最后以您所在的页面的名称结尾,例如 网站名称 | 页面名称 或类似内容。您可能说得对,在思考中,我们可以为此使用 模板字面量,您是对的!

假设我们正在为一家名为 Cars4All 的公司创建网站。在下面的代码中,您会看到 Layout 组件现在接受一个名为 pageTitle 的 prop,并且文档标题(现在使用模板字面量呈现)使用它作为占位符值。设置文档标题不会比这更困难了!

import React from "react"
import Helmet from "react-helmet"

export default ({ pageTitle, children }) => (
  <>
    <Helmet>
      <title>{`Cars4All | ${pageTitle}`}</title>
    </Helmet>
    <div>
      <header>
        <h1>Cars4All</h1>
        <nav>
          <ul>
          </ul>
        </nav>  
      </header>
      {children}
      <footer>{`${new Date().getFullYear()} No Rights Whatsoever Reserved`}</footer>
    </div>
  </>
)

让我们通过将 pageTitle 设置为“主页”来相应地更新 ìndex.js

import React from "react"
import Layout from "../components/layout"

export default () => 
  <Layout pageTitle="Home">
    <div>I live in a layout component, and life is pretty good here!</div>
  </Layout>

如果您在浏览器中打开 http://localhost:8000,您会看到文档标题现在是 Cars4All | 主页。胜利!但是,如引言中所述,我们希望在文档头部执行的操作不仅仅是设置标题。例如,我们可能希望包含字符集、描述、关键词、作者和视口元标签。

我们该如何做到这一点?答案与我们设置文档标题的方式完全相同

import React from "react"
import Helmet from "react-helmet"

export default ({ pageMeta, children }) => (
  <>
    <Helmet>
      <title>{`Cars4All | ${pageMeta.title}`}</title>
      
      {/* The charset, viewport and author meta tags will always have the same value, so we hard code them! */}
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <meta name="author" content="Bob Trustly" />

      {/* The rest we set dynamically with props */}
      <meta name="description" content={pageMeta.description} />
      
      {/* We pass an array of keywords, and then we use the Array.join method to convert them to a string where each keyword is separated by a comma */}
      <meta name="keywords" content={pageMeta.keywords.join(',')} />
    </Helmet>
    <div>
      <header>
        <h1>Cars4All</h1>
        <nav>
          <ul>
          </ul>
        </nav>  
      </header>
      {children}
      <footer>{`${new Date().getFullYear()} No Rights Whatsoever Reserved`}</footer>
    </div>
  </>
)

您可能已经注意到,Layout 组件不再接受 pageTitle 属性,而是使用 pageMeta 属性代替,这是一个封装页面所有元数据的对象。您不必将所有页面数据都这样捆绑在一起,但我非常反对属性膨胀。如果数据存在共同点,我总是会像这样将其封装起来。无论如何,让我们用相关数据更新 index.js

import React from "react"
import Layout from "../components/layout"

export default () => 
  <Layout
    pageMeta={{
      title: "Home",
      keywords: ["cars", "cheap", "deal"],
      description: "Cars4All has a car for everybody! Our prices are the lowest, and the quality the best-est; we are all about having the cake and eating it, too!"
    }}
  >
    <div>I live in a layout component, and life is pretty good here!</div>
  </Layout>

如果您再次打开 http://localhost:8000,启动 DevTools 并深入到文档头部,您会看到我们添加的所有元标记都在那里。无论您是否想添加更多元标记、规范 URL 还是使用 Open Graph Protocol 将您的网站与 Facebook 集成,这都是您应该了解的方法。我想指出的一件事是:如果您需要向文档头部添加脚本(可能是因为您想通过包含一些 结构化数据 来增强网站的 SEO),那么您必须像这样在花括号内将脚本呈现为字符串

<script type="application/ld+json">{`
{
  "@context": "http://schema.org",
  "@type": "LocalBusiness",
  "address": {
  "@type": "PostalAddress",
  "addressLocality": "Imbrium",
  "addressRegion": "OH",
  "postalCode":"11340",
  "streetAddress": "987 Happy Avenue"
  },
  "description": "Cars4All has a car for everybody! Our prices are the lowest, and the quality the best-est; we are all about having the cake and eating it, too!",
  "name": "Cars4All",
  "telephone": "555",
  "openingHours": "Mo,Tu,We,Th,Fr 09:00-17:00",
  "geo": {
  "@type": "GeoCoordinates",
  "latitude": "40.75",
  "longitude": "73.98"
  }, 			
  "sameAs" : ["https://#/your-profile",
  "http://www.twitter.com/your-profile",
  "http://plus.google.com/your-profile"]
}
`}</script>

有关您可以在文档头部放置的所有内容的完整参考,请查看 Josh Buchea 的精彩 概述

逃生舱

无论出于何种原因,您可能需要覆盖已使用 React Helmet 设置的值——在这种情况下该怎么办?React Helmet 背后的聪明人已经考虑到了这种特定用例,并为我们提供了一个逃生舱:在组件树中更下层的组件中设置的值始终优先于在组件树中更上层的组件中设置的值。通过利用这一点,我们可以覆盖现有值。

假设我们有一个看起来像这样的虚构组件

import React from "react"
import Helmet from "react-helmet"

export default () => (
  <>
    <Helmet>
      <title>The Titliest Title of Them All</title>
    </Helmet>
    <h2>I'm a component that serves no real purpose besides mucking about with the document title.</h2>
  </>
)

然后我们想将此组件包含在 ìndex.js 页面中,如下所示

import React from "react"
import Layout from "../components/layout"
import Fictitious from "../components/fictitious"

export default () => 
  <Layout
    pageMeta={{
      title: "Home",
      keywords: ["cars", "cheap", "deal"],
      description: "Cars4All has a car for everybody! Our prices are the lowest, and the quality the best-est; we are all about having the cake and eating it, too!"
    }}
  >
    <div>I live in a layout component, and life is pretty good here!</div>
    <Fictitious />
  </Layout>

由于 Fictitious 组件位于我们组件树的底层,因此它能够劫持文档标题并将其从“首页”更改为“史上最棒的标题”。虽然我认为这个逃生舱的存在是一件好事,但我建议除非确实没有其他方法,否则不要使用它。如果其他开发人员接手您的代码并且不了解您的 Fictitious 组件及其作用,那么他们可能会怀疑代码闹鬼,我们不想吓到我们的同事开发人员!毕竟,战斗机确实配备了弹射座椅,但这并不是说战斗机飞行员可以随意使用它们。

走出文档头部

如前所述,我们还可以使用 React Helmet 更改 HTML 和 body 属性。例如,声明网站的语言始终是一个好主意,您可以使用 HTML 的 lang 属性来实现。使用 React Helmet 设置如下

<Helmet>

  /* Setting the language of your page does not get more difficult than this! */
  <html lang="en" />
    
  /* Other React Helmet-y stuff...  */
</Helmet>

现在让我们真正发挥 React Helmet 的强大功能,让 Layout 组件的 pageMeta 属性接受一个添加到文档 body 的自定义 CSS 类。到目前为止,我们的 React Helmet 工作仅限于一个页面,因此我们可以通过为 Cars4All 网站创建另一个页面并使用 Layout 组件的 pageMeta 属性传递自定义 CSS 类来真正丰富内容。

首先,我们需要修改我们的 Layout 组件。请注意,由于我们的 Cars4All 网站现在将包含多个页面,因此我们需要使网站访问者能够在这些页面之间导航:Gatsby 的 Link 组件 来救援!

使用 Link 组件并不比将其 to 属性设置为构成您要链接到的页面的文件名更难。因此,如果我们想为 Cars4All 出售的汽车创建一个页面,并将页面文件命名为 cars.js,则链接到它就像输入 <Link to="/cars/">我们的汽车</Link> 一样简单。当您在“我们的汽车”页面上时,应该能够导航回 ìndex.js 页面,我们将其称为首页。这意味着我们也需要将 <Link to="/">首页</Link> 添加到我们的导航中。

在下面的新 Layout 组件代码中,您可以看到我们正在从 Gatsby 导入 Link 组件,并且头部元素中以前为空的无序列表现在已填充了我们页面的链接。在 Layout 组件中唯一剩下的工作是添加以下代码段

<body className={pageMeta.customCssClass ? pageMeta.customCssClass : ''}/>

…到 <Helmet> 代码中,如果使用 pageMeta 属性传递了 CSS 类,则此代码段会将 CSS 类添加到文档 body 中。哦,鉴于我们将要传递一个 CSS 类,我们当然必须创建一个。让我们回到 src 目录并创建一个名为 css 的新目录,在其中创建一个名为 main.css 的文件。最后但并非最不重要的是,我们必须将其导入到 Layout 组件中,因为否则我们的网站将不知道它的存在。然后将以下 CSS 添加到文件中

.slick {
  background-color: yellow;
  color: limegreen;
  font-family: "Comic Sans MS", cursive, sans-serif;
}

现在用我们刚刚讨论的新 Layout 代码替换 src/components/layout.js 中的代码

import React from "react"
import Helmet from "react-helmet"
import { Link } from "gatsby"
import "../css/main.css"

export default ({ pageMeta, children }) => (
  <>
    <Helmet>
      {/* Setting the language of your page does not get more difficult than this! */}
      <html lang="en" />
      
     {/* Add the customCssClass from our pageMeta prop to the document body */}
     
     <body className={pageMeta.customCssClass ? pageMeta.customCssClass : ''}/>
      
      <title>{`Cars4All | ${pageMeta.title}`}</title>
      
      {/* The charset, viewport and author meta tags will always have the same value, so we hard code them! */}
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <meta name="author" content="Bob Trustly" />

      {/* The rest we set dynamically with props */}
      <meta name="description" content={pageMeta.description} />
      
      {/* We pass an array of keywords, and then we use the Array.join method to convert them to a string where each keyword is separated by a comma */}
      <meta name="keywords" content={pageMeta.keywords.join(',')} />
    </Helmet>
    <div>
      <header>
        <h1>Cars4All</h1>
        <nav>
          <ul>
            <li><Link to="/">Home</Link></li>
            <li><Link to="/cars/">Our Cars</Link></li>
          </ul>
        </nav>  
      </header>
      {children}
      <footer>{`${new Date().getFullYear()} No Rights Whatsoever Reserved`}</footer>
    </div>
  </>
)

我们只会在 cars.js 页面中将自定义 CSS 类添加到文档 body 中,因此无需对 ìndex.js 页面进行任何修改。在 src/pages/ 目录中,创建一个名为 cars.js 的文件,并将下面的代码添加到其中。

import React from "react"
import Layout from "../components/layout"

export default () => 
  <Layout
    pageMeta={{
      title: "Our Cars",
      keywords: ["toyota", "suv", "volvo"],
      description: "We sell Toyotas, gas guzzlers and Volvos. If we don't have the car you would like, let us know and we will order it for you!!!",
      customCssClass: "slick"
    }}
  >
    <h2>Our Cars</h2>
    <div>A car</div>
    <div>Another car</div>
    <div>Yet another car</div>
    <div>Cars ad infinitum</div>
  </Layout>

如果您转到 http://localhost:8000,您会看到您现在可以在页面之间导航。此外,当您到达 cars.js 页面时,您会注意到有些地方看起来有点不对劲……嗯,难怪我会称自己为 Web 开发人员而不是 Web 设计师!让我们打开 DevTools,切换文档头部并导航回 ìndex.js 页面。更改路由时内容会更新!

锦上添花

如果您检查页面的源代码,您可能会感觉有点被欺骗了。我承诺了一个 SSR React 网站,但我们的 React Helmet 好处都没有在源代码中找到。

您可能会问,我强迫您使用 Gatsby 的目的是什么?好吧,年轻的见习武士,耐心点!在站点的根目录下,在终端中运行 gatsby build,然后运行 gatsby serve

Gatsby 会告诉您该站点现在正在 http://localhost:9000 上运行。转到那里,再次检查页面的源代码。瞧,都在那里!您现在拥有一个网站,它具有 React SPA 的所有优点,而无需放弃 SEO 或与第三方应用程序集成等等。Gatsby 非常棒,我真诚地希望您能继续探索 Gatsby 提供的功能。

就此而言,编码愉快!