专栏首页图雀社区一杯茶的时间,上手 Gatsby 搭建个人博客

一杯茶的时间,上手 Gatsby 搭建个人博客

本文由图雀社区认证作者 crimx[1] 写作而成,点击阅读原文查看作者的博客,感谢作者的优质输出,让我们的技术世界变得更加美好?

为什么选 Gatsby

我的博客最初是用 Github Pages 默认的 Jekyll[2] 框架,其使用的 Liquid[3] 模板引擎在使用上有诸多不便。

后来基于 Node.js 的 Hexo[4] 横空出世,我便重构了博客[5]对其深入整合,还为其写了一个 emoji 插件[6]。在编写过程中发现其 API 设计比较不成熟,调试体验也不是很好,阅读其它插件代码时发现很多都需要用到未公开接口。同时资源管理需要借助其它 Task runner,如当时比较流行的 Grunt 和 Gulp 。这样下来直接依赖了大量包,冲突不可避免的产生。

在一次换系统之后,项目终于构建不了了,包冲突处理起来非常头疼,也影响到了写博文的兴致。

拖延了一段时间后,终于开始考虑更换框架。这时 React Angular Vue 生态已比较成熟,所以就没必要考虑其它的模板引擎。

首先注意到的是新星 VuePress[7] 。然而考察过后发现其正在 v1 到 v2 的更替期,v1 功能比较简陋,v2 还在 alpha 期不稳定。且 VuePress 目前还是针对静态文档优化比较多,作为博客依然比较简陋。

这时 @unicar[8] 正好推荐了基于 React 的 Gatsby[9]。发现其生态很强大,再搭配 React 庞大的生态,确实非常吸引人。

而且在了解过程中还发现了 Netlify CMS[10] 这个内容管理平台,如此一来,文章数据完全可以存在 Github 中,同时可以便捷地编辑文章。

Gatsby 项目结构

建议使用 Starter 修改着理解 Gatsby,我用的是 Gatsby + Netlify CMS Starter[11]

完整的 Gatsby 项目结构可以看文档[12],这里针对搭建博客用到的功能说明一下。

  • /src/pages 目录下的组件会被生成同名页面。
  • /src/templates 目录下放渲染数据的模板组件,如渲染 Markdown 文章,在其它博客系统中一般叫 layout
  • /src/components 一般放其它共用的组件。
  • /static 放其它静态资源,会跳过 Webpack 直接复制过去。

接下来是两个比较常用的配置文件,需要修改时参考 Starter 改即可。

  • /gatsby-config.js 基本用来配置两个东西:
    1. siteMetadata 放一些全局信息,这些信息在每个页面都可以通过 GraphQL 获取到。
    2. plugins 配置插件,这个按用到时按该插件文档说明弄即可。
  • /gatsby-node.js 可以调用 Gatsby node APIs[13] 干一些自动化的东西。一般有两个常用场景:
    1. 添加额外的配置,比如为 Markdown 文章生成自定义路径。
    2. 生成 /src/pages 以外的页面文件,如为每个 Markdown 文章生成页面文件。

此外还有两个不那么常用的配置文件。

  • /gatsby-browser.js 可以调用 Gatsby 浏览器 APIs[14],一般插件才会用到,如滚动到特定位置。
  • /gatsby-ssr.js 服务器渲染的配置,一般也是插件才用到。

这就是搭建 Gatsby 博客的基本结构了,可以看到非常简单,且因为其丰富的生态,其它底层接口基本不需要用到。但接下来还是会有一些小坑,第一个便是 GraphQL,我们将马上来分析。

为什么用 GraphQL

在上一节介绍了选择 Gatsby 的原因,其中提到了 Gatsby 使用 GraphQL 。大家可能会有疑惑,不是建静态博客么,怎么会有 GraphQL?难道还要部署服务器?

其实这里 GraphQL 并不是作为服务器端部署,而是作为 Gatsby 在本地管理资源的一种方式。

通过 GraphQL 统一管理实际上非常方便,因为作为一个数据库查询语言,它有非常完备的查询语句,与 JSON 相似的描述结构,再结合 Relay 的 Connections 方式处理集合,管理资源不再需要自行引入其它项目,大大减轻了维护难度。

带魔法的 GraphQL

这里也是 Gatsby 的第一个坑。在 Gatsby 中,根据 js 文件的位置不同,使用 GraphQL 有两种形式,且 Gatsby 对其做了魔法,在 src/pages 下的页面可以直接 export GraphQL 查询,在其它页面需要用 StaticQuery 组件[15]或者 useStaticQuery[16] hook。

这里面查询语句虽然写的是字符串,但其实这些查询语句不会出现在最终的代码中,Gatsby 会先对其抽取[17]

个人其实不太喜欢魔法,因为会增加初学者的理解难度。但不得不承认魔法确实很方便,就是用了魔法的项目应该在文档最显眼的地方说明一遍。

快速上手 GraphQL

GraphQL 结构跟最终数据很相似,基本语法也非常简单,看看官方文档即可。一个快速上手的方式是访问项目开发时(默认 http://localhost:8000)的 /___graphql 页面,通过 GraphiQL 编辑器右侧可以浏览所有能够查询的资源。

另一个需要理解的是 Relay 的 Connections 概念,你会发现 Gatsby 里所有的数据集合都是以这种方式查询。推荐阅读 Apollo 团队分享的文章[18]

对 Connections 细致的理解往往是实现分页等底层需求时才需要,而这些均有插件完成。一般使用时只需要知道集合里每个项目的数据在 edges.node 中,同时通过 GraphiQL 浏览其它可以使用的数据。如对于 Markdown 文章,相应插件提供了字数统计以及阅读时长等数据,均可通过 GraphQL 直接获取。

Debug GraphQL

Gatsby 魔法带来的另外一个坑是 GraphQL 报错信息不全,可能会默默被吞掉,也可能无法定位到最终文件。

我在修改 starter 时踩到一个坑是复制组件时忘了修改 static query 查询语句的名称,导致重名报错。

避免错误最好方式是在 GraphiQL 编辑器中写好运行无误再复制到组件中。

Remark 插件坑

Gatsby 中处理 markdown 最常用也是默认的插件是 gatsby-transformer-remark。这个插件对 markdown 文件解析后会生成 MarkdownRemark GraphQL 节点,其中 front matters 数据也会被解析出来。同时 MarkdownRemark 的集合对应为 allMarkdownRemark connections。

对于 connections 节点我们一般可以用 sortfilter 来筛选处理数据(可在 GraphiQL 编辑器中浏览),这里有一个坑便是如果要处理 front matters 数据,它们必须存在所有查询的 markdown 文件上并且具有相同的类型,插件才会生成相应的 fields,否则可能会抛出异常或者更糟糕的,默默失败了。

避免方式同上,先在 GraphiQL 编辑器中运行一遍,看看筛选的结果是否正确。

另外一种处理方式是在 /gatsby-node.js 中通过 onCreateNode 钩子,在生成 markdown 相关节点时手工处理,确保节点存在。

这在实现草稿和上下篇的时候会用到,具体例子我会在后续章节中提到。

为什么选择 Netlify CMS

搭建 Gatsby 博客其实不需要 CMS 都是可以的,编写 Markdown 然后 build 即可。但这么做还是略嫌不便,通过 CMS 一般可以在一个可视化的在线环境中编辑文章,然后一键即可发布。

Gatsby 主流的两个 CMS 是 Contentful 和 Netlify CMS。

对于 Contentful 来说,文章是放在 Contentful 的服务器上的,管理也是通过 Contentful 提供的工具。当然其质量还是不错的,喜欢的可以参照官方的教程[19]搭建。

Netlify CMS 是跟项目一起发布的,默认是在 /admin 页面下。文章也是存在源项目中,就是原来默认的 Markdown 文件。Netlify CMS 借助 Oauth 把写好的 Markdown 文件推送到项目源码的仓库上,再配合 Netlify 检测仓库变动自动构建发布。当然后者也不是必须的,可以换其它方式自动构建。

Netlify CMS 的优点是开源免费,文章跟项目源码在一起,界面可以高度自定义,甚至可以自行扩充 React 组件,基本满足简单的博客编写需求。

配置 Netlify CMS

如果用官方的 starter[20] 配置将会非常简单。此 starter 默认使用 Github 作为仓库,Netlify 作为自动构建服务器。

配置 Widgets

默认的 /static/admin/config.yml 已经配置好了大部分,如果对文章 Markdown 添加了自定义的 front matters 则需要再做些细调。

Widgets 代表了在 CMS 中可输入的模块,官方[21]为常见的类型都提供了默认的 widgets ,没有满足的也可以自定义[22]

如我的博客[23]中每篇文章都有一个 quote 域放些引用文字,那么在配置[24]中添加上

fields:
  - label: "Quote"
    name: "quote"
    widget: "object"
    fields:
      - {label: "Content", name: "content", widget: "text", default: "", required: false}
      - {label: "Author", name: "author", widget: "string", default: "", required: false}
      - {label: "Source", name: "source", widget: "string", default: "", required: false}

如此即可在 CMS 中填写相关信息。

配置预览

CMS 中提供了文章预览界面,如果需要自定义只需修改 /src/cms/ 下相应的文件即可,就是简单的 React 组件。

以上便是 Netlify CMS 最常用的配置,只需简单的修改博客现在就能跑起来了。接下来我们会通过实现草稿模式和上下篇文章来深入理解 Gatsby 的机制。

迁移博客需要考虑的一个重要问题便是路径兼容。我们当然不希望迁移后原有的链接无法访问,这不仅影响到 SEO ,更带来了不好的用户访问体验。本文将聊聊怎么让 Gatsby 兼容 Jekyll 式路径。

Gatsby 如何生成特定页面

一般来说,在 /src/pages/ 目录下的组件会自动生成相应路径的页面,但如果是其它类型的文件就不会了。我们可以通过 Gatsby 的 Node APIs 来生成特定页面。

/gatsby-node.js 中配置 Gatsby Node APIs,如果项目是基于 starter 的话你很可能会发现里面已经有相应的配置。

我们通过声明 exports.createPages 钩子来配置页面生成,在回调中通过调用 actions.createPage 来生成某个指定页面。这个方法接受一个配置参数,其中的 path 域代表了生成页面的路径。

exports.createPages = ({ actions, graphql }) => {
  actions.createPage({
    path,
    // ...
  })
}

指定博客页面

那么我们怎么知道该生成哪些页面呢?这里通过 exports.createPages 回调中的 graphql 来查询 Markdown 文件。

exports.createPages = ({ actions, graphql }) => {
  const { createPage } = actions

  return graphql(`
    {
      allMarkdownRemark(sort: { order: ASC, fields: [frontmatter___date] }) {
        edges {
          node {
            id
            fields {
              slug
            }
            frontmatter {
              path
              title
              layout
            }
          }
        }
      }
    }
  `).then(result => {
    if (result.errors) {
      result.errors.forEach(e => console.error(e.toString()))
      return Promise.reject(result.errors)
    }
    
    // ...
  })
}

Netlify CMS 会在 Markdown front matters 中的 path 域生成路径。根据默认的 /static/admin/config.yml 我们的路径应该是 /blog/{{year}}-{{month}}-{{day}}-{{slug}}/,这个可能跟旧博客不一样,如 Jekyll 是 /{{year}}/{{month}}/{{day}}/{{slug}}/

修改 Markdown 节点

在 Remark 插件生成的 Markdown 节点中,我们可以往 fields 域放一些自定义的变量。这里我们把自定义的路径存到 fields.slug 中。

通过 /gatsby-node.js 中的 exports.onCreateNode 钩子我们可以在生成节点的时候进行拦截处理。你可能发现文件里面已经有一些配置的代码了,我们这里只关注 Markdown 相关的。

exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions

  if (node.internal.type === `MarkdownRemark`) {
    // Jeykll style post path
    const filepath = createFilePath({ node, getNode })
    createNodeField({
      node,
      name: 'slug',
      value: filepath.replace(
        /^\/blog\/([\d]{4})-([\d]{2})-([\d]{2})-/,
        '/$1/$2/$3/'
      )
    })
  }
}

我们把原有的路径值换成了自定义值并存在了 fileds.slug 中。

创建页面

回到我们前面的查询[25],得到需要的数据之后只需要对每个页面调用 actions.createPage 即可。

exports.createPages = ({ actions, graphql }) => {
  const { createPage } = actions

  return graphql(`
    {
      allMarkdownRemark(sort: { order: ASC, fields: [frontmatter___date] }) {
        edges {
          node {
            id
            fields {
              slug
            }
            frontmatter {
              path
              title
              layout
            }
          }
        }
      }
    }
  `).then(result => {
    if (result.errors) {
      result.errors.forEach(e => console.error(e.toString()))
      return Promise.reject(result.errors)
    }
    
    const { edges } = result.data.allMarkdownRemark

    const options = edges.map(edge => ({
      path: edge.node.fields.slug,
      title: edge.node.frontmatter.title,
      component: path.resolve(
        `src/templates/${edge.node.frontmatter.layout}.js`
      ),
      // additional data can be passed via context
      context: {
        id: edge.node.id
      }
    }))
    
    options.forEach(option => createPage(option))
  })
}

也许你会问为什么不在这里直接计算自定义路径而是要存到 fields.slug 中。这是因为这个路径我们可能还会在其它地方用到,存起来就不必多处计算了。

上面代码中可以注意到还有个 context 域,这个域中的数据会被传到 component 的 props 中。这样我们在模板组件中通过 pageContext.id 便可判断当前渲染的文件。

通过实现自定义路径基本上可以了解 Gatsby 页面生成的方式了。下节中我会继续谈谈其它个性化的配置,如草稿模式和显示上下篇博文。

草稿模式

草稿模式即可以将文章保存为草稿而不被渲染出来。方式是在 front matters 中设置一个 draft 布尔域,以此域作为渲染参考。

这里有一个地方需要注意,前面文章提过,Markdown 插件需要所有文章中都有 draft 域且都是布尔类型才会生成相应的 GraphQL 查询。如果是新的博客这个问题不大,如果是迁移过来的,有两个解决方式,第一个是手动写个脚本给文章都补上域,另一个是利用 Gatsby 的 Node APIs 在 fields 上生成特定域,鲁棒性更好些。

自动生成域

观察 Remark 插件生成的 GraphQL 类型,我们可以发现,front matters 都被放在 frontmatter 域中,而与之同级的有一个前面文章提到过的 fields 域,用来放自定义生成的数据。

Gatsby 在生成 GraphQL 节点时提供了钩子 onCreateNode,我们利用这个钩子往 fields 中放自定义的数据。

编辑 /gatsby-node.js,如果是用了 starter 的话这里很可能已经有其它的代码,已有的不需要动,添加我们需要的即可。

exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions

  if (node.internal.type === `MarkdownRemark`) {
    createNodeField({
      node,
      name: 'draft',
      value: Boolean(node.frontmatter.draft)
    })
  }
}

如此 fields 中就保证了会有 draft 这个域了。

过滤草稿

有了标记之后,在生成页面的地方我们就需要过滤草稿。

首先是普通的文章页面生成,这个是在 createPages 钩子中,如果你的博客只有文章用到 Markdown 的话,可以在 GraphQL 查询中直接过滤,否则我们用前面文章的方法,先取所有 Markdown 文件再根据渲染的模板来分别处理各种类型的文章。

注意我把模板域的名字换成了自己更习惯的 layout,原来的 starter 中应该叫 templateKey。修改其实也很简单,搜索所有文件替换关键字即可。

options
  .filter(
    (_, i) =>
      !(
        edges[i].node.frontmatter.layout === 'blog-post' &&
        edges[i].node.fields.draft
      )
  )
  .forEach(option => createPage(option))

我在主页中也列举了最近的几篇文章,这里也需要过滤草稿,可以直接在 GraphQL 中过滤。

query IndexQuery {
  latestPosts: allMarkdownRemark(
    sort: { order: DESC, fields: [frontmatter___date] }
    filter: {
      fields: { draft: { ne: true } }
      frontmatter: { layout: { eq: "blog-post" } }
    }
    limit: 5
  ) {
    edges {
      node {
        excerpt(pruneLength: 200)
        id
        fields {
          slug
        }
        frontmatter {
          title
          description
          layout
          date(formatString: "MMMM DD, YYYY")
        }
      }
    }
  }
}

其它地方同理。

上下篇

在文章页面中我们通常会加入上下篇来引导继续浏览。这里我们同样在 createPages 钩子中处理,但这回我们添加到 context 域中,这个域里的数据会作为 props 传到模板组件中。

createPage 生成文章页面前添加处理代码[26]计算上下篇:

options
  .filter(
    (_, i) =>
      edges[i].node.frontmatter.layout === 'blog-post' &&
      !edges[i].node.fields.draft
  )
  .forEach((option, i, blogPostOptions) => {
    option.context.prev =
      i === 0
        ? null
        : {
          title: blogPostOptions[i - 1].title,
          path: blogPostOptions[i - 1].path
        }
    option.context.next =
      i === blogPostOptions.length - 1
        ? null
        : {
          title: blogPostOptions[i + 1].title,
          path: blogPostOptions[i + 1].path
        }
  })

然后在文章的 /src/templates/blog-post.js 组件里,接收 pageContext props,就可以使用上面传入的数据了。这是[27]我的例子。

通过实现这几个功能我们了解了 Gatsby 页面生成的方式以及其 Node APIs 的基本使用。Gatsby 的功能远不止这些,官方文档写得非常详细,需要实现其它功能建议先去看看有无现有的例子。本系列到这里暂告一段落,谢谢你的阅读,希望能对你搭建 Gatsby 博客有所帮助。

参考资料

[1]

crimx: https://www.crimx.com/

[2]

Jekyll: https://jekyllrb.com

[3]

Liquid: https://shopify.github.io/liquid/

[4]

Hexo: https://hexo.io

[5]

博客: https://blog2018.crimx.com

[6]

emoji 插件: https://github.com/crimx/hexo-filter-github-emojis

[7]

VuePress: https://vuepress.vuejs.org/

[8]

@unicar: https://twitter.com/unicar9

[9]

Gatsby: https://www.gatsbyjs.org/

[10]

Netlify CMS: https://netlifycms.org

[11]

Gatsby + Netlify CMS Starter: https://github.com/netlify-templates/gatsby-starter-netlify-cms

[12]

文档: https://www.gatsbyjs.org/docs/gatsby-project-structure/

[13]

Gatsby node APIs: https://www.gatsbyjs.org/docs/node-apis/

[14]

Gatsby 浏览器 APIs: https://www.gatsbyjs.org/docs/browser-apis/

[15]

StaticQuery 组件: https://www.gatsbyjs.org/docs/static-query/

[16]

useStaticQuery: https://www.gatsbyjs.org/docs/use-static-query/

[17]

先对其抽取: https://www.gatsbyjs.org/docs/page-query#how-does-the-graphql-tag-work

[18]

Apollo 团队分享的文章: https://blog.apollographql.com/explaining-graphql-connections-c48b7c3d6976

[19]

教程: https://www.contentful.com/r/knowledgebase/gatsbyjs-and-contentful-in-five-minutes/

[20]

starter: https://github.com/netlify-templates/gatsby-starter-netlify-cms

[21]

官方: https://www.netlifycms.org/docs/widgets/

[22]

自定义: https://www.netlifycms.org/docs/custom-widgets/

[23]

博客: https://blog.crimx.com

[24]

配置: https://github.com/crimx/blog-2019/blob/3af6a9706e2c1e7f7c1a3c1dac0ad981d5603693/static/admin/config.yml#L14-L28

[25]

前面的查询: https://github.com/crimx/blog-2019/blob/d7c8c6bbbe73ef455f70bc629d153b836482f788/gatsby-node.js#L71-L79

[26]

添加处理代码: https://github.com/crimx/blog-2019/blob/d7c8c6bbbe73ef455f70bc629d153b836482f788/gatsby-node.js#L47-L68

[27]

这是: https://github.com/crimx/blog-2019/blob/1b2f63a60448a502c632d120c798009b2960b19f/src/templates/blog-post.js#L123-L160

本文分享自微信公众号 - 图雀社区(tuture-dev),作者:crimx

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 从一道面试题引发的原理性探究

    key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速。对于简单列表页渲染来说 diff 节点也更快,但会产生一些...

    一只图雀
  • 你所需要的跨域问题的全套解决方案都在这里啦!(升级版)

    随着RESTful架构风格成为主流,以及Vue.js、React.js和Angular.js这三大前端框架的日益强大,越来越多的开发者开始由传统的MVC架构转向...

    一只图雀
  • 从零到部署:用 Vue 和 Express 实现迷你全栈电商应用(三)

    这篇文章中,我们将讲解 Vue 实例的 Props 和 Methods,接着我们又讲解了最常见的 Vue 模板语法,并通过实例的方式将这些模板语法都实践了一番,...

    一只图雀
  • ADO.NET Entity Framework

    作为下一代 ADO.NET 3.0 的开发框架,Entity Framework 让我们从复杂的关系数据模型中解脱出来,使用更加符合面向对象的实体数据模型(En...

    张善友
  • iOS App冷启动治理:来自美团外卖的实践

    冷启动时长是App性能的重要指标,作为用户体验的第一道“门”,直接决定着用户对App的第一印象。美团外卖iOS客户端从2013年11月开始,历经几十个版本的迭代...

    iOS_林夕
  • iOS App冷启动治理:来自美团外卖的实践

    冷启动时长是App性能的重要指标,作为用户体验的第一道“门”,直接决定着用户对App的第一印象。美团外卖iOS客户端从2013年11月开始,历经几十个版本的迭代...

    美团技术团队
  • iOS App冷启动治理:来自美团外卖的实践

    冷启动时长是App性能的重要指标,作为用户体验的第一道“门”,直接决定着用户对App的第一印象。美团外卖iOS客户端从2013年11月开始,历经几十个版本的迭代...

    美团技术团队
  • 绘图系列|R-wordcloud2包绘制词云

    前段时间读完大刘的流浪地球,本来想着写点东西... 结果“懒癌”了,今天就先弄个词云凑合吧( ╯□╰ )。

    西游东行
  • 如何爬取美团网美食

    工作需求需要采集OTA网站的美食数据,某个城市的饭店类型情况等。对于老饕来说这不算个事。。。然而最后的结果是中午晚饭都没有时间去吃了。。。情况如下

    sergiojune
  • 我爬取分析美团网,原来北京上海Top10美食是它们

    作者:Kying,西二旗程序单身汪一枚。从事智慧旅游、数据挖掘。新晋python 小白,希望与志同道合者一起煮酒论英雄,数据森麟特邀作者。

    Python中文社区

扫码关注云+社区

领取腾讯云代金券