gatsby建站详细教程及避坑攻略

3,288 阅读9分钟

这篇文章算是对这段时间折腾的一个东西的技术总结,在之前文章博客迁移中,我已经总结了项目的背景,技术选型等形而上的内容,这篇文章主要是技术总结,并且帮助大家能从我的视角了解gatsby,并且快速的上手实践。

Gatsby是什么,为什么要用Gatsby

Gatsby实际上是是一个react的静态化框架。React本身实际上能完成所有gatsby的功能。但是有一个问题就是,react是一个单页应用(SPA - single page application),本身实际上是在前端动态的生成页面。这样就会有两个问题,一个是首屏加载时间很长。第二个因为页面是动态生成,所以搜索引擎不知道每个页面的内容,对SEO很不友好。对于这些问题,react的服务端渲染框架(SSR - server side render)应运而生。而SSR本身也有很多不同的实现方案,有些是直接搭一个渲染服务器多一个中间层,而有一些就比较极端,比如gatsby,直接在编译的时候就把整个网站静态化。这样上面的问题就迎刃而解了。

gatsby如何工作

react吸引人的特点之一就是她的动态特性,如果在编译的时候就把整个网站静态化,那么这些动态的特性该如何保证呢?Gatsby给出的方案就是程序化生成页面。具体的说,gatsby的页面生成有两种方式,一个是直接把react组件写在/src/pages目录下,比如/src/pages/tags.js对应的页面就是/tags。第二种就是通过gatsby的api动态的构建页面了,官方的说明文档在这里.简而言之就是

  1. 创建页面模板
  2. 通过graphQL获取用于生成页面的数据
  3. 在gatsby-node.js中通过createPageonCreateNode两个API生成页面

实战

实战部分手把手教你创建一个和我一样的博客

创建项目

首先你需要安装gatsby及其依赖

然后通过博客模板创建项目

$ gatsby new my-blog-starter https://github.com/gatsbyjs/gatsby-starter-blog

然后

$ cd my-blog-stater && npm start 就可以打开浏览器输入localhost:8000看到主页了

20200227175924.png

增加tags相关页面

如果你想像我的主页一样拥有tags页面的话。可以通过以下操作

增加tag标签

要增加tags界面,首先得有tags。要做的很简单,就是在你的markdown文章中增加tags标签如图

20200227191105.png
20200227191128.png
文章在/content/blog/{title}这个路径中

生成网页

tags相关的页面有两个,一个是/tags,另一个是/tag/{tag},前一个用于展示所有的tag,其中点击任意一个tag就是跳转到对应tag的文章列表,也就是后面的页面。

因为/tags依赖/tag/{tag},所以我们首先我们生成/tag/{tag},根据前面介绍gatsby如何工作

首先我们需要创建一个页面模板/src/templates/tag-posts.js

import React from "react"
import { graphql, Link } from "gatsby"

const TagPost = ({data, pageContext}) => {
  const posts = data.allMarkdownRemark.edges

  return (
    
      <div>
      <h1>Posts for tag: {pageContext.targetTag}</h1>
      <p>{posts.length + " posts"}</p>
      <hr />
      <ul>
      {posts.map(({ node }) => {
        const title = node.frontmatter.title || node.fields.slug
        return (
          <li><Link to={node.fields.slug}>{title}</Link></li> 
        )
      })}
      </ul>
      </div>

      
  )
}

export default TagPost

export const pageQuery = graphql`
  query($targetTag: String!) {
    site {
      siteMetadata {
        title
      }
    }
    allMarkdownRemark(filter: {frontmatter: {tags: {eq: $targetTag}}}, sort: {fields: frontmatter___date, order: DESC}) {
      edges {
        node {
          excerpt(truncate: true)
          fields {
            slug
          }
          frontmatter {
            date(formatString: "MMMM DD, YYYY")
            title
            tags
          }
        }
      }
    }
  }
`

然后按照第二部,在gatsby-node.js中生成页面,添加如下代码

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

  /*
  此处省略生成blog-post的代码
  */

  // start generate tag posts
  const tagPost = path.resolve(`./src/templates/tag-posts.js`)
  const tagPostResult = await graphql(
    `
    {
      allMarkdownRemark {
        nodes {
          frontmatter {
            tags
          }
        }
      }
    }
    `
  )

  if (tagPostResult.errors) {
    throw tagPostResult.errors
  }

  const nodes = tagPostResult.data.allMarkdownRemark.nodes

  //extract distict tags of all posts
  var tagSet = new Set()
  nodes.forEach(node => node.frontmatter.tags.forEach(tag => tagSet.add(tag)))

  // gen page for each tag
  tagSet.forEach( tag => createPage({
    path: "tag/" + tag,
    component: tagPost,
    context: {
      targetTag : tag
    },
  }))

}

之后你重新npm start,然后输入网址http://localhost:8000/tag/tag2,你就能看到下面的页面

20200227191308.png

现在我们要生成/tags的页面,这个不是一个动态路径,所以我们在/src/pages添加一个js文件就可以了/src/pages/tags.js,里面的代码如下

import React from "react"
import { Link, graphql } from "gatsby"


const Tags = ({ data }) => {
  const posts = data.allMarkdownRemark.edges
  var tagMap = new Map()


  for (const post of posts) {
    for (const tag of post.node.frontmatter.tags) {
      if (tagMap.has(tag)) {
        tagMap.set(tag, tagMap.get(tag) + 1)
      } else {
        tagMap.set(tag, 1)
      }
    }
  }
  var tagPair = Array.from(tagMap)
  tagPair.sort((left, right) => right[1] - left[1])

  console.log(tagPair)

  return (
    <div>
      <h1>All Tags</h1>
      <p>Click the tag to read related articles</p>
      <hr />
      <ul>
        {tagPair.map(([tag, count]) => {
          return (
                <li><Link to={"tag/" + tag} className="tag">{tag + " | " + count + " posts"}</Link></li>
          )
        })}
      </ul>
    </div>
  )
}

export default Tags

export const pageQuery = graphql`
  query {
    allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
      edges {
        node {
          frontmatter {
            tags
          }
        }
      }
    }
  }
`

然后再浏览器输入http://localhost:8000/tags,就可以看到下面的页面

20200227195057.png

这两类页面就覆盖了两种页面的生成方式,其他的页面就靠大伙举一反三了

优化

增加css效果

刚才生成的页面中,都是光秃秃的文字,为了页面更美观,需要加上css效果使其更美观,博客本身的css框架我并不是很喜欢,所以我换了一个我比较喜欢另一个轻便的css框架BULMA

这个框架的特点就是简单轻巧,只有css,没有js,不容易和其他的组件产生冲突。而且非常的易用。

官方有介绍如何接入的文档

具体如何使用,或者想直接抄作业可以直接看我的源码

组件化

组件就是很多页面需要公用的模块就会抽象成组件,这样会使代码变得优雅易懂。

比如每个页面都要页眉页脚,这些就可以抽象出Layout组件。每个页面又要不同的元信息做SEO,这就抽象出了SEO组件。比如我觉得展示文章需要用统一的卡片样式,我自己就抽象出了PostCard组件。还有每个页面可能都需要的Comment组件。

因为gatsby本身就是从react扩展出来的框架,自然也可以使用组件来优雅的开发界面。组件所在的路径是/src/components/。里面已经有了LayoutSEO等组件。相关的语法需要查阅一下react的文档。依葫芦画瓢写还是比较简单的。

具体的怎么写可以直接看我的源码

第三方评论

之前我的hexo博客用的是国内的多说,但是多说已经不运营了。但是我又不想自己额外维护一个评论数据库。所以我调研了一圈,对比了一番发现几个可能可用的

  • Disqus - 这个是世界上最大也是最成熟的,但是在中国被墙了。pass
  • gitcomment - 这个是通过github issue来实现的,强制用github登录。对于非码农的评论者不友好,而且github在中国api的效率也存疑。pass
  • levere(来必力) - 这个是韩国的一个第三方模块,对中国的本地化支持的很好,支持简单回复和包括微信在内的各种第三方账号登录。看他的客户也不像会倒的样子,所以就用他了。

levere有个问题就是在移动端打开的时候无法微信登录,这个我咨询了他们的客服,他们回复这个就是这样,因为微信得扫码,所以无法移动端登录。无解。而且在集成的时候还碰到了一些坑,见下文。

托管

整个网页托管到了github page上,具体怎么托管,请翻阅文档

自动化

因为gatsby出色的架构,整个网站现在只需要增加markdown文章,其他的任何内容都不需要更改就会自动的构建并发布新网站。但是我简直懒到了家,为了实现hexo一样,只要hexo new就可以写新文章的便捷,我写了一个脚本\gen-new-post.js,然后再/package.jsonscript中增加一条命令

"new": "node gen-new-post.js $*",

这样,我们就可以用npm run new {newtitle}自动生成markdown文件啦。

踩坑

graphQL相关

你的每个md文件都要加上tags:[], 不然graphQL读出来的话这个属性就是null而不会是空的array,那样的话tags.foreachtags.map()就会报错。

Link 和 a,两种不同的跳转

官方介绍说Link是用来跳转到应用内的页面的,而a则是用来跳转到外链的。但是这样其实是有一个问题,一开始我的评论模块时通过helmet模块把第三方评论的脚本写到每个post的页面的header里面。第一次点击文章没有问题,但是从一篇文章通过点击下一篇文章的链接跳转的时候,评论模块就消失了。在网上查了一下,里面有一个解决方案是吧Link改成a。确实有效。但是我进一步探究了一下为什么。

查了很多资料后我知道了原因,gatsby的Link实际上和react的Route类似,会做优化做预加载,加快跳转速度。通过Link跳转的话,其实用的是react特有的diff渲染,这种渲染只会渲染两个页面不同的部分。因为两篇post的header部分几乎完全相同,所以在渲染的时候评论模块的script是不会重新生成的,而评论模块时script脚本动态生成的,所以就消失了。

使用a的话,会强制整个页面进行更新,这个自然是可行的,但是就没有了预加载的优势。页面的跳转就不会快如闪电,不够优雅。

其实这个可以通过一个优雅的方式解决。就是react的hook。在Comment组件中增加一个hook - useEffect。

const Comment = () => {
    useEffect(() => {
        var j, e = document.getElementsByTagName("script")[0];

       if (typeof LivereTower === 'function') { return; }

       j = document.createElement("script");
       j.src = 'https://cdn-city.livere.com/js/embed.dist.js';
       j.async = true;

       e.parentNode.insertBefore(j, e);
    });
  return (
    <div id="lv-container" data-id="city" data-uid="xxxxx">
    </div>
  )
}

useEffect 的作用是每次在组件加载和更新的时候,会调用一次这个方法。而comment组件又是嵌在tag-post中的,所以每次切换文章,tag-post肯定需要重新加载,所以comment也需要重新加载,而useEffect就会动态的插入一个script,这个新插入的script就会动态生成一个第三方评论框,问题解决。