React & Gatsby | 用Markdown、React和对象存储构建博客

2021 04 19, Mon

用React写简单的前端也有些时间了,大部分时候都是用react + router + nginx try_file来设置web服务,多多少少还是有些不方便。

20年知道了Gatsby,但是那个时候没多少精力,就没有尝试。前一段时间因为脚骨折行动不方便,在家办公之余把博客也都彻底改成了Gatsby构建的。真爽。

Gatsby

Gatsby是一个基于NodeJS/React/GraphQL构建的静态网站生成器,可以快速的根据JavaScript代码和数据源生成出来一个基于React的静态网站。为了降低这种复杂性,也有非常多的模版/StarterKit可以用。

和Hugo、Hexo、Jekyll相比,Gatsby更复杂一些,需要自己写JavaScript指定生成网站的逻辑,但是相对来说换取了非常高的加载体验和灵活性,同时也提供了更多的数据源。相对于React + HashRouter,提高了SEO的能力,相对于React + BrowserRouter降低了Web服务的要求,并且比较好的解决了404页面的问题。

通过jsx直接写页面

定义上来说Gatsby是根据React Component来生成网站,我们可以在/src/pages/index.jsx中用JSX写首页逻辑。Gatsby提供了Link之类的React Component用来在页面间跳转,我们只要写好路径就行。

构建过程中,我们可以使用GraphQL来获取数据。数据通常是用插件提供的,比如说 gatsby-source-filesystem 提供了对本地文件的访问能力,gatsby-transformer-remark 可以把markdown转换成html并提供GraphQL的数据源。

写好的页面会根据相对的位置自动生成一个html页面和对应的js。

通过js告诉gatsby要生成一个页面

除了直接写JSX以外,我们还可能需要根据GraphQL的结果,利用写好的模版生成其他的页面。比如说我们有了 index.jsx 列举博客的条目,也需要把 markdown 根据 jsx 转换成具体的页面。这个时候就需要告诉Gatsby我们要生成新的页面。Gatsby提供了一个 gatsby-node.js 文件来写 hook 函数,每当一些特定的动作被执行这个文件导出的特定函数就会执行一次。

比如说我们可以导出 onCreateNode({ node, getNode, actions }) 函数,每当插件建立了一个Node,这个函数都会调用一次。这个Node可以是一个文件,可以是一个Markdown节点,也可以是别的,通过node.internal.type区分。类似的是创建页面有一个 async createPages({ graphql, actions }) ,这个hook则是在准备创建页面的时候调用的,graphql是一个异步函数用来查询GraphQL结果,查询到结果以后,就可以根据结果用 actions.createPage 告诉Gatsby要用某个模版,也就是React Component,来创建页面了。

比如说这个博客的gatsby-node.js是这样的:

const { createFilePath } = require(`gatsby-source-filesystem`);
const path = require(`path`);

exports.onCreateNode = ({ node, getNode, actions }) => {
  const { createNodeField } = actions;
  if (node.internal.type === `MarkdownRemark` || node.internal.type === `Mdx`) {
    const slug = createFilePath({ node, getNode, basePath: `pages` });
    createNodeField({
      node,
      name: `slug`,
      value: slug,
    });
    if (slug.startsWith("/posts/")) {
      createNodeField({
        node,
        name: `kind`,
        value: "post",
      });
    } else {
      createNodeField({
        node,
        name: `kind`,
        value: "page",
      });
    }
  }
};

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions;
  // **Note:** The graphql function call returns a Promise
  // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise for more info
  var result = await graphql(`
    query {
      allMdx(filter: { fields: { draft: { eq: false } } }) {
        edges {
          node {
            fields {
              slug
              kind
            }
            frontmatter {
              tags
            }
          }
        }
      }
    }
  `);

  let tags = new Set();
  let processNode = ({ node }) => {
    if (node.fields.kind == "post") {
      (node.frontmatter.tags || ["uncataloged"]).forEach((tag) => {
        tags.add(tag);
      });
    }
    createPage({
      path: node.fields.slug,
      component: path.resolve(`./src/templates/post/index.tsx`),
      context: {
        slug: node.fields.slug,
      },
    });
  };

  result.data.allMdx.edges.forEach(processNode);

  tags.forEach((tag) => {
    createPage({
      path: `/tags/${tag}`,
      component: path.resolve(`./src/templates/tags/index.tsx`),
      context: {
        tag: tag,
      },
    });
  });
};

网站信息

除了博客啊什么的这些以外,我们还可能在很多地方用到一些通用的信息,这些信息通常叫 Site Metadata,网站元信息,比如说网站的名字啊、标题栏的项目啊什么的。以及Gatsby的配置,比如说,用什么插件,数据源用什么、在哪里。

这些信息通常在gatsby-config.js中。比如说

let metadata = {
  title: `从CentOS开始的Linux之旅`,
};

module.exports = {
  siteMetadata: {
    title: metadata.title,
    navbar: [
      {
        route: "/",
        title: "Home",
      },
      {
        route: "/me",
        title: "Me",
      },
    ],
  },
  plugins: [
      //...
  ],
};

graphql

graphql类似于SQL,但是形式和结果都不大一样。SQL是从数据库中检索数据的语言,而graphql是从自定义的数据集中检索数据的语言。

gatsby利用插件和graphql创建了一种比较简单灵活的方法来检索数据。我们只要在插件中定义数据源、处理数据的插件,就可以在JSX代码中检索出来我们需要的数据。

比如说博客。我们写博客一般是用markdown,gatsby中有对应的插件叫remark,把markdown转换成html。但是我们今天用mdx,markdown的一个拓展,在md中直接写jsx,比较方便灵活。

虽然mdx和jsx非常相近,但是博客一般不用单独的jsx来写,不是说不可以,但是直接用jsx写需要自己维护非常多的信息,又回到了手写网站每一个页面的年代。我们作为懒人没有必要。只要设置filesystem类型的数据源,目录设置为我们放置mdx文件的目录,加上mdx插件,就像这样:

let metadata = {
  title: `从CentOS开始的Linux之旅`,
};

module.exports = {
  siteMetadata: {
    title: metadata.title,
    navbar: [
      {
        route: "/",
        title: "Home",
      },
      {
        route: "/me",
        title: "Me",
      },
    ],
  },
  plugins: [
    // ...
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        path: `${__dirname}/content/`,
      },
    },
    {
      resolve: "gatsby-plugin-mdx",
      options: {
        extensions: [".mdx", ".md"],
      },
    },
    // ...
  ],
};

假如说我写了一篇博客content/posts/setup-blog-with-gatsby.mdx

---
title:  React & Gatsby | 用Markdown、React和对象存储构建博客
date:  2021-04-19T12:51:50.000+08
tags:  ['react', 'typescript']
draft:  false
author: guochao
summary: 用React构建静态网站
featuredimage: "/images/gatsby-logo.svg"
---

用React写简单的前端也有些时间了,大部分时候都是用react + router + nginx try_file来设置web服务,多多少少还是有些不方便。

20年知道了Gatsby,但是那个时候没多少精力,就没有尝试。前一段时间因为脚骨折行动不方便,在家办公之余把博客也都彻底改成了Gatsby构建的。真爽。

我们就可以在首页src/pages/index.tsx中,通过graphql,检索所有的博客内容了:

import React from "react";

import { graphql } from "gatsby";

import { Container } from "react-bootstrap";

import DefaultLayout from "../components/layout";
import { PostCard } from "../components/cards";

export const query = graphql`
  query {
    allMdx(
      filter: { fields: { kind: { eq: "post" }, draft: { eq: false } } }
      sort: { fields: frontmatter___date, order: DESC }
    ) {
      nodes {
        frontmatter {
          title
          date
          summary
          featuredimage {
            publicURL
          }
        }
        id
        timeToRead
        fields {
          draft
          slug
        }
      }
    }
  }
`;

type PageNode = {
  frontmatter: {
    title?: string;
    summary?: string;
    tags?: string[];
    date: Date;
    featuredimage?: {
      publicURL: string;
    };
  };
  id: string;
  timeToRead: number;
  fields: {
    slug: string;
    draft: boolean;
  };
};

type IndexPageProps = {
  data: {
    allMdx: {
      nodes: PageNode[];
    }
  };
};

export default class IndexPage extends React.Component<IndexPageProps> {
  constructor(prop: IndexPageProps) {
    super(prop);
  }
  render() {
    return (
      <DefaultLayout>
        <Container>
          {this.props.data.allMdx.nodes.filter((node) => {
            return !node.fields.draft;
          }).map((node) => {
            return (
              <PostCard
                key={node.fields.slug}
                title={node.frontmatter.title || "No Title"}
                date={node.frontmatter.date}
                image={node.frontmatter.featuredimage?.publicURL}
                summary={node.frontmatter.summary}
                slug={node.fields.slug}
              />
            );
          })}
        </Container>
      </DefaultLayout>
    );
  }
}

不止可以检索内容,还可以有文章自己的各种信息:标题,类别、时间、摘要、封面图、目录、正文HTML、正文的语法解析图……blablabla。甚至在拿到这些信息的时候让gatsby给你处理好,图片要合适的大小,日期要合适的格式。

如果一年到头没有几篇博客,单独写JSX,然后自己一点点维护各种列表、Sitemap还好。但是如果写的多了,维护这些东西非常的费力不讨好。gatsby的graphql非常适合让我们从这些工作里面解脱出来。