一杯茶的時間,上手 Gatsby 搭建我的博客

本文由圖雀社區認證做者 crimx[1] 寫做而成,點擊閱讀原文查看做者的博客,感謝做者的優質輸出,讓咱們的技術世界變得更加美好😆前端


爲何選 Gatsby

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

後來基於 Node.js 的 Hexo[4] 橫空出世,我便重構了博客[5]對其深刻整合,還爲其寫了一個 emoji 插件[6]。在編寫過程當中發現其 API 設計比較不成熟,調試體驗也不是很好,閱讀其它插件代碼時發現不少都須要用到未公開接口。同時資源管理須要藉助其它 Task runner,如當時比較流行的 Grunt 和 Gulp 。這樣下來直接依賴了大量包,衝突不可避免的產生。node

在一次換系統以後,項目終於構建不了了,包衝突處理起來很是頭疼,也影響到了寫博文的興致。git

拖延了一段時間後,終於開始考慮更換框架。這時 React Angular Vue 生態已比較成熟,因此就不必考慮其它的模板引擎。github

首先注意到的是新星 VuePress[7] 。然而考察事後發現其正在 v1 到 v2 的更替期,v1 功能比較簡陋,v2 還在 alpha 期不穩定。且 VuePress 目前仍是針對靜態文檔優化比較多,做爲博客依然比較簡陋。web

這時 @unicar[8] 正好推薦了基於 React 的 Gatsby[9]。發現其生態很強大,再搭配 React 龐大的生態,確實很是吸引人。算法

並且在瞭解過程當中還發現了 Netlify CMS[10] 這個內容管理平臺,如此一來,文章數據徹底能夠存在 Github 中,同時能夠便捷地編輯文章。數據庫

Gatsby 項目結構

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

完整的 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',
      valueBoolean(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


      

● 一杯茶的時間,上手 React 框架開發

● 前端學習數據結構與算法系列(一):初識數據結構與算法

● 用動畫和實戰打開 React Hooks(一):useState 和 useEffect



·END·

圖雀社區

匯聚精彩的免費實戰教程



關注公衆號回覆 z 拉學習交流羣


喜歡本文,點個「在看」告訴我

本文分享自微信公衆號 - 圖雀社區(tuture-dev)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索