书签

你还没有保存任何书签. 要将某篇文章加入书签, 请点击

  • 拥抱 Gatsby,用 React 搭建完整博客系统(三)——渲染 Markdown 文件并生成页面

  • 前言

    在本系列的前两篇中,我们已经使用了官方基础模板搭建好了我们的 Gatsby 博客,并通过数据源插件实现了目录文件的读取。

    但数据源插件只能够获取到文件本身的信息,却无法读取到文件内容,要读取文件内容,就需要数据源转换插件的帮助了。

    本篇就将以 markdown 文件为例,讲解一下数据源转换插件的使用。

    添加示例文档

    我们在 pages 目录下新建一个 markdown 文档:markdown-demo.md,内容可以随意,以下是一个示例:

    ---
    title: "示例 Markdown 文档"
    date: "2020-01-02"
    ---
    
    ## 这是一个标题
    这是一段话,这是**加粗文本**,这是*斜体*,这是行内代码`Javascript`。
    
    > 这是一个引用
    
    这是一个列表:
    * 第一项内容
    * 第二项内容
    * 第三项内容
    

    之后,再次访问 http://localhost:8080/myfiles ,可以看到数据源插件已经获取到了该文件,这一过程是即时的,只要我们添加了文件,gatsby-source-filesystem 插件就能够将其添加到 Gatsby 的数据源中:
    gatsby-from-zero-3-1.png

    下面我们要做的,就是读取该 md 文件内容,并将其转换为 HTML 页面。

    安装 Markdown 转换插件

    gatsby-transformer-remark 插件可以实现将对 Markdown 文件的读取,我们安装该插件:

    yarn add gatsby-transformer-remark
    

    之后就像我们安装 gatsby-source-filesystem 插件之后所做的一样,将该插件添加到 gatsby-config.js 文件中去,你的 gatsby-config.js 文件看起来将是这样的:

    plugins: [
        `gatsby-plugin-react-helmet`,
        {
          resolve: `gatsby-source-filesystem`,
          options: {
            name: `src`,
            path: `${__dirname}/src`,
          },
        },
        {
          resolve: `gatsby-source-filesystem`,
          options: {
            name: `src`,
            path: `${__dirname}/src`,
          },
        },
        `gatsby-transformer-remark`,
        ...
    

    之后重启开发服务器,再次打开 GraphiQL 就可以看到插件已经将 Markdown 文件的内容提到到了数据源中:
    gatsby-from-zero-3-2.png

    我们可以通过 GraphQL 查询查看插件读取到的文件内容:
    gatsby-from-zero-3-3.png

    为博客添加一个文章列表页

    我们最终的目标是实现一个博客,而博客一般都会拥有一个包含所有文章的列表页,通过上文内容,我们已经能够获取到目录下所有文件,并能够读取到 Markdown 文件的内容,通过这些,我们已经可以实现一个简单的列表页了。

    修改 pages 目录下的 index.js 文件:

    import React from "react"
    import { graphql } from "gatsby"
    import Layout from "../components/layout"
    
    const Index = ({ data }) => {
      console.log(data)
      return (
        <Layout>
          <div>
            <h1>文章列表</h1>
            <h4>共{data.allMarkdownRemark.totalCount} 篇文章</h4>
            {data.allMarkdownRemark.edges.map(({ node }) => (
              <div key={node.id}>
                <h3>
                  {node.frontmatter.title}{" "}
                  <span>
                    — {node.frontmatter.date}
                  </span>
                </h3>
                <p>{node.excerpt}</p>
              </div>
            ))}
          </div>
        </Layout>
      )
    }
    
    export default Index
    
    export const query = graphql`
      query {
        allMarkdownRemark {
          totalCount
          edges {
            node {
              id
              frontmatter {
                title
                date(formatString: "DD MMMM, YYYY")
              }
              excerpt
            }
          }
        }
      }
    `
    

    再次访问 http://localhost:8000 就能看到我们的页面已经发生了变化:
    gatsby-from-zero-3-4.png

    我们继续向 pages 目录中添加 Markdown 文件,首页也会继续发生变化:
    gatsby-from-zero-3-5.png

    将 Markdown 文件渲染为对应的 HTML 页面

    前文中,我们通过 gatsby-transformer-remark 插件将获取到了所有的 Markdown 文件,并且将所有文件和内容生成了一个文章列表页,但作为一个博客,需要在点击每一个标题时,跳转到对应的页面,这该如何实现呢?

    很容易想到的自然是手动添加对应的 js 文件,比如我们可以添加一个 markdown-demo.js,然后借助于 GraphQL 查询获取到 Markdown 文件对应的 HTML 内容,渲染为页面。

    这种方法自然是可行的,但是如果每添加一篇文章都需要这样处理那也未免太麻烦了,本部分就将讲解如何动态的将 GraphQL 的查询结果映射为页面。

    整个过程中需要两个步骤:创建路径生成内容

    为页面创建 slug 或路径

    slug 为官方文档中所使用的单词,可以理解为别名,在这里大致和路径类似,本单词本文将不翻译直接使用

    本部分中我们将使用多个 Gatsby 提供的 API, Gatsby 提供的所有 API 可以查看官方文档

    首先用到的是 onCreateNode 这一API,其中的方法将在每个节点创建或更新时调用,要使用该 API,我们需要编辑项目根目录下的 gatsby-node.js 文件,在其中添加以下内容:

    exports.onCreateNode = ({node}) => {
      console.log(node)
    }
    

    之后重启服务,可以看到在终端中打印出了所有节点:
    gatsby-from-zero-3-7.png

    从打印内容中我们可以看到能获取到的信息,我们需要做的是为所有 Markdown 类型的节点生成路径

    要实现这一目标,最方便的方式是使用 gatsby-source-filesystem 插件,其提供了 createFilePath 方法,我们修改 onCreateNode 方法:

    const { createFilePath } = require(`gatsby-source-filesystem`) 
    
    exports.onCreateNode = ({ node, getNode }) => {
      if (node.internal.type === `MarkdownRemark`) {
        console.log(createFilePath({ node, getNode, basePath: `pages` }))
      }
    }
    

    重启开发服务器,就可以看到,我们创建的两个 Markdown 文件的路径被打印出来了:
    gatsby-from-zero-3-8.png

    将 slug 信息添加到数据源

    我们已经获取到了每一个 Markdown 文档的 slug,那当我们访问这一路径时,如何知道他们对应的是哪一个文件?一个可行的处理方式是将 slug 信息添加到数据源对应的条目中,这样我们就可以通过 GraphQL 查询通过 slug 找到对应的文件了。

    我们需要使用使用 createNodeField 这一 API,其功能是扩展某节点,向一个节点添加任意信息,新添加的信息将被存储在 fields 属性下。

    改造我们的 onCreateNode 方法:

    const { createFilePath } = require(`gatsby-source-filesystem`)
    
    exports.onCreateNode = ({ node, getNode, actions }) => {
      const { createNodeField } = actions // highlight-line
      if (node.internal.type === `MarkdownRemark`) {
        const slug = createFilePath({ node, getNode, basePath: `pages` })
        createNodeField({
          node,
          name: `slug`,
          value: slug,
        })
      }
    }
    

    重启开发服务器,打开 GraphiQL 浏览器,就可以找到新添加的属性了:
    gatsby-from-zero-3-9.png

    我们可以通过 GraphQL 查询到 slug 对应的节点内容:
    gatsby-from-zero-3-10.png

    有了这些,下一步就是创建页面了。

    创建页面

    创建页面需要使用 createPages API,该 API 将在数据源初始化完成,数据源转换插件执行完成后执行,因此可以在其中执行 GraphQL 查询语句。

    创建页面的主要操作如下:

    • 通过 GraphQL 查询到所有的 slug
    • 使用 createPages 所提供的 createPage 方法为每个 slug 生成页面

    createPage 接受几个主要参数:

    • path: 要生成的页面路径,在页面及页面 GraphQL 查询中均可用
    • component: 生成页面的模板
    • context: 可选项,要传递的额外信息,传递的信息将在页面 GraphQL 查询中可用

    所以,我们先创建一个最简单的模板/src/templates/post.js,暂不渲染内容,只将传递到页面中的 props 打印出来:

    import React from "react"
    import Layout from "../components/layout"
    
    export default (props) => {
      console.log(props)
      return (
        <Layout>
          <div>Hello blog post</div>
        </Layout>
      )
    }
    

    之后,再创建页面,我们修改 gatsby-node.js 方法,增加以下内容:

    exports.createPages = async ({ graphql, actions }) => {
      const { createPage } = actions
      const result = await graphql(`
        query {
          allMarkdownRemark {
            edges {
              node {
                fields {
                  slug
                }
              }
            }
          }
        }
      `)
    
      result.data.allMarkdownRemark.edges.forEach(({ node }) => {
        createPage({
          path: node.fields.slug,
          component: path.resolve(`./src/templates/post.js`),
          context: {
            slug: node.fields.slug,
          },
        })
      })
    }
    

    我们通过 GraphQL 查询到所有 Markdown 文件的 slug,然后为每一个 slug 生成了一个页面。

    重启开发服务器,访问相关页面,可以看到页面已经创建好了,在控制台也打印出了传递到页面中的信息:
    gatsby-from-zero-3-11.png

    渲染页面内容

    我们已经为每一个 Markdown 文档生成了页面,但所有页面都只显示了模板内容,我们需要将 Markdown 文档的内容渲染到页面中。

    我们已经将 slug 信息等传递到了每一个页面,只需要在模板文件中根据传递的信息进行查询,即可获得文件内容,之后使用获得的内容动态渲染模板文件即可。

    我们修改模板文件 /src/templates/post.js 内容:

    import React from "react"
    import { graphql } from "gatsby"
    import Layout from "../components/layout"
    
    export default ({ data }) => {
      const post = data.markdownRemark
      return (
        <Layout>
          <div>
            <h1>{post.frontmatter.title}</h1>
            <div dangerouslySetInnerHTML={{ __html: post.html }} />
          </div>
        </Layout>
      )
    }
    
    export const query = graphql`
      query($slug: String!) {
        markdownRemark(fields: { slug: { eq: $slug } }) {
          html
          frontmatter {
            title
          }
        }
      }
    `
    

    注意上面的 GraphQL 查询,在 createPage 方法的说明中我们已经提及了,path 变量以及在 context 中的变量均可在这里使用,所以如果我们在这里将 $slug 换成 $path 也是可行的。

    再次打开页面,此时已经正常渲染了 Markdown 文件内容:
    gatsby-from-zero-3-12.png

    为文章列表页添加链接

    为了方便跳转,给我们前面创建的文章列表页增加跳转链接,修改 pages/index.js 文件内容,增加 GraphQL 查询内容,并将文章标题内容使用 Link 标签包裹:

    import React from "react"
    import { Link, graphql } from "gatsby"
    import Layout from "../components/layout"
    
    const Index = ({ data }) => {
      return (
        <Layout>
          <div>
            <h1>文章列表</h1>
            <h4>共{data.allMarkdownRemark.totalCount} 篇文章</h4>
            {data.allMarkdownRemark.edges.map(({ node }) => (
              <div key={node.id}>
                <Link to={node.fields.slug}>
                <h3>
                  {node.frontmatter.title}{" "}
                  <span>
                    — {node.frontmatter.date}
                  </span>
                </h3>
                 </Layout>
                <p>{node.excerpt}</p>
                </Link>
              </div>
            ))}
          </div>
      )
    }
    
    export default Index
    
    export const query = graphql`
      query {
        allMarkdownRemark {
          totalCount
          edges {
            node {
              id
              frontmatter {
                title
                date(formatString: "DD MMMM, YYYY")
              }
              fields {
                slug
              }
              excerpt
            }
          }
        }
      }
    `
    

    再次打开首页,已经可以跳转到生成的页面了:
    gatsby-from-zero-3-13.png

    总结

    通过本篇内容,我们了解了 Gatsby 如何利用数据源转换插件来读取文件内容并将其加入数据源,以及如何利用数据源中的内容动态生成页面。到目前为止,我们已经实现了一个基本的博客功能,现在只要在 pages 目录下添加 Markdown 文件,就会自动生成对应的页面,并加入到首页的文章列表中。

    当然,目前我们的博客还比较简陋,因为我们未添加任何样式,在本系列的后续内容中,将会有专门学习 Gatsby 样式相关的知识,并一起来进行网站的美化。在下一部分,我们将了解 Strapi 相关的知识,了解如何从远程 API 获得数据并进行渲染。