React & Gatsby | 用Markdown、React和对象存储构建博客
用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非常适合让我们从这些工作里面解脱出来。