react-static 静态网站搭建(三):React 静态博客 Step by Step

2019-03-18

前言

经过前面两篇文章,相信大家已经对静态博客和 react-static 有了充分的认识。在本节我们来一个实战环节,手把手教你如何用 react-static 搭建一个静态博客。

安装

首先需要安装 react-static :

$ yarn global add react-static
# or
$ npm install -g react-static

其中:

  • react-static 作为全局命令进行安装

创建工程

在你想创建工程的目录执行以下命令:

$ react-static create

命令行会提示几个问题:

① 项目叫什么名字:

? What should we name this project? (my-static-site)

输入一个自己想要的名称。

② 选择哪种模板:

? Select a template below... (Use arrow keys or type to search)
❯ README.md
  basic
  blank
  stress-test
  typescript
  Local Directory...
  GIT Repository...

这里我选择了 typescript,因为我是 typescript 党,在这里顺道安利一下 😆

之后就开始项目创建过程,react-static 会调用包管理器创建工程、安装依赖,因此需要等待几分钟时间。

需要注意的是:我使用的包管理器是 Yarn,之前我在用 npm 创建工程的时候失败过一次。

依赖安装好后,会提示你如何开始:

=> [✓] Project "react-static-blog-demo" created (89.7s)

  => To get started:

    cd "react-static-blog-demo"

    yarn start - Start the development server
    yarn build - Build for production
    yarn serve - Test a production build locally

这几个命令都很有用,需要牢记。

下面 cd "react-static-blog-demo" 进入工程,开搞!

Hello world!

首先我们把项目先跑起来。在项目根目录,执行下面命令:

yarn start

终端会显示以下信息:

yarn run v1.13.0
$ react-static start
=> Building Routes...
=> [✓] Routes Built (1.2s)
=> Building Templates
=> [✓] Templates Built
=> Building App Bundle...
Starting type checking service...
Using 1 worker with 2048MB memory limit
=> [✓] Build Complete (10.3s)
=> [✓] App serving at http://localhost:3000
=> File changed: /artifacts/react-static-templates.js
=> Updating build...
=> [✓] Build Updated (0.5s)

等了半天你会奇怪,为啥浏览器还没有打开呢?

因为构建成功是不会自动打开浏览器的!!需要自己手动打开 🤣

在浏览器中输入地址 http://localhost:3000,会打开项目首页:

其中,我们发现:

  • 示例工程已经为我们搭建好一个博客站点了
  • 这个站点包括首页、关于、博客,完全满足一般的博客需求
  • 这也意味着我们不用从头开始

进入 Blog 板块(/blog)看看:

其中:

  • 实例工程还插入了很多 mock 数据
  • 后面我们会讲到,这些数据都是通过 API 接口在构建时获取的

文章详情页(/blog/post/1/):

项目工程

关于项目的整体结构,首先请参考 react-static 静态网站搭建(二):react-static 介绍

在此我们主要看工程目录结构:

.
├── README.md
├── artifacts           // react-static 自动生成的项目描述
│   ├── react-static-browser-plugins.js
│   └── react-static-templates.js
├── node_modules
├── package.json        // 工程描述文件
├── public              // 公共资源目录
│   └── robots.txt
├── src                 // 代码目录
│   ├── App.tsx         // 网站整体结构定义
│   ├── app.css         // 网站全局 css
│   ├── components      // React 组件
│   ├── containers      // 容器:Post.tsx 帖子页
│   ├── index.tsx       // 网站入口,react-static 初始化相关,导入 App
│   ├── pages           // 页面:404.tsx、about.tsx、blog.tsx、index.tsx
│   └── types.ts        // 数据类型定义
├── static.config.js    // react-static 配置文件
├── tmp
│   └── dev-server
├── tsconfig.json       // TypeScript 配置脚本
└── yarn.lock

其中,我们关心的有:

src/App.tsx

它定义了网站的整体框架:顶部导航栏、侧边栏,以及划定内容区的范围。各个页面都展示在内容区当中。

import React from 'react'
import { Root, Routes } from 'react-static'
import { Link } from '@reach/router'
import './app.css'
import FancyDiv from '@components/FancyDiv'

function App() {
  return (
    <Root>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/blog">Blog</Link>
      </nav>
      <div className="content">
        <FancyDiv>
          <Routes />
        </FancyDiv>
      </div>
    </Root>
  )
}

export default App

其中:

  • Root 和 Routes 都来自 react-static,Routes 表示内容区
  • 项目使用了 @reach/router 的 Link 组件,用来做站内跳转

src/index.tsx

这个文件是网站的入口,主要用于 react-static 初始化,一般不需要改动:

import React from 'react'
import ReactDOM from 'react-dom'

// Your top level component
import App from './App'

// Export your top level component as JSX (for static rendering)
export default App

// Render your app
if (typeof document !== 'undefined') {
  const renderMethod = module.hot
    ? ReactDOM.render
    : ReactDOM.hydrate || ReactDOM.render

  const render = (Comp: Function) => {
    renderMethod(<Comp />, document.getElementById('root'))
  }

  // Render!
  render(App)

  // Hot Module Replacement
  if (module.hot) {
    module.hot.accept('./App', () => render(require('./App').default))
  }
}

其中:

  • 导入了 App.tsx
  • 初始化了热加载

src/pages/index.tsx

这个 index 不要与上一节搞混,它是网站的首页。

import React from 'react'
import { withSiteData } from 'react-static'

export default withSiteData(() => (
  <div style={{ textAlign: 'center' }}>
    <h1>
      Welcome to React-Static <br /> + TypeScript
    </h1>
    <p>
      Learn{' '}
      <a href="https://github.com/sw-yx/react-typescript-cheatsheet">
        React + TypeScript
      </a>
    </p>
    <p>
      <a href="https://twitter.com/swyx">Report issues with this template</a>
    </p>
  </div>
))

其中:

  • 通过 withSiteData 这个高阶组件,能够拿到 static.config.js 中提供的 getSiteData,不过这里很简单,并没有从中获取数据

src/pages/blog.tsx

这里对应的是博客列表页:

import React from 'react'
import { withRouteData } from 'react-static'
import { Link } from '@reach/router'
import { Post } from '../types'

export default withRouteData(({ posts }: { posts: Post[] }) => (
  <div>
    <h1>It's blog time.</h1>
    <br />
    All Posts:
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          <Link to={`/blog/post/${post.id}/`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  </div>
))

其中:

  • 使用 withRouteData 这个高阶组件,拿到的是 static.config.js 向这个路径提供的数据,即博客列表

src/containers/Post.tsx

这对应的是文章详情页。

眼尖的同学会发现代码路径变到了 containers 下,这是因为在 react-static 中,一级路由会自动映射到 src/pages/*.tsx 下,而二级路由需要手动指定组件。当然,containers 的命名没有限制,改名叫 src/subPage/Post.tsx 也没问题,只要在 static.config.js 配置二级路由组建时写对路径就行。

文章详情页的代码为:

import React from 'react'
import { withRouteData } from 'react-static'
import { Link } from '@reach/router'
import { Post } from '../types'

export default withRouteData(({ post }: { post: Post }) => (
  <div>
    <Link to="/blog/">{'<'} Back</Link>
    <br />
    <h3>{post.title}</h3>
    <p>{post.body}</p>
  </div>
))

其中可以看出,与博客列表页所使用的套路是完全一致的,都是通过 withRouteData 这个高阶组件,来拿去对应的数据。

从这里可以看出,react-static 一单适应它定义的规则,使用起来是非常简单的! 🎉

数据提供

下面我们来看 static.config.js 中,是如何向这些页面提供数据的:

import axios from 'axios'
import path from 'path'

export default {
  plugins: ['react-static-plugin-typescript'],          // 使用 TypeScript
  entry: path.join(__dirname, 'src', 'index.tsx'),      // 指定入口文件
  getSiteData: () => ({                                 // 指定 SiteData
    title: 'React Static',
  }),
  getRoutes: async () => {                              // 定义路由
    const { data: posts } = await axios.get(            // 数据准备阶段
      'https://jsonplaceholder.typicode.com/posts',     // 通过这个 API 拉取数据
    )
    return [                                            // 返回路由表
      {
        path: '/blog',                                  // 博客列表页
        getData: () => ({                               // 提供博客列表页的 RouteData
          posts,
        }),
        children: posts.map(post => ({                  // 子页面:各个文章详情页
          path: `/post/${post.id}`,                     // 路径
          component: 'src/containers/Post',             // 指定组件
          getData: () => ({                             // 提供文章详情页的 RouteData
            post,
          }),
        })),
      },
    ]
  },
}

是不是非常简单直观!其中:

  • 数据提供是我们直接写 js 代码来获取的
  • 你会发现:src/pages/index.tsx、src/pages/about.tsx 都没再上面出现,这是因为 react-static 中有一个约定,src/pages/ 下的页面(高阶组件),会自动为他们配置路径。但是如果你要向它们提供数据的话,还得在上面来指定,如 /blog。

数据从何处来?

在示例工程中,数据是调用 API 接口来获取的。

有一点需要弄清的是,这里从 API 获取数据发生在构建时,只在构建时执行一次。react-static 会拿这一次获取到的数据生成静态网站。

这也就是说,当一次构建发布后,如果 API 有了更新,线上的网站不会更新,除非再执行一次构建过程。别忘了我们是静态网站!

能不能不从 API 中获取呢?当然可以。

上面代码中 getRoutes 就是个 node.js 的方法,怎么获取数据完全由你编写的 js 代码控制。

我们开下脑洞,可以如何获取数据呢?

  • 扫描某个文件夹下的 Markdown (maxiee.github.io 就是这种方法)
  • 读取某个 SQLite 数据库
  • 读取 MySQL、MongoDB
  • 读取 OrgMode 笔记
  • ……

数据的 json 保存

react-static 采取数据与模板分离的模式。

我们在 react-static 中提供数据,在模板中通过 withRouteData 接收数据。

这些数据在构建时会被保存为 json 文件。

我们下面来验证这个过程,首先构建项目:

yarn build

经历了 25.02s 之后,我完成了构建。

你会想一个空的工程为何构建需要这么多时间呢?这是因为要启动 Pyppeteer 进行 DOM snapshot。

我在实践中发现,当我有上百篇文章需要静态化的时候,构建时间也不过 80.44s。平均一个页面增长 0.5s,这个速度是非常令人满意的!

说到这我在多说两句,在使用 react-static 之前,我独立开发了一个静态网站生成器。一开始用着挺好,可它的致命缺陷是,生成时间随着页面数量急速上升,同时稳定性也下降,开始出现构建失败的情况。

这件事给我的经验就是,没有两把刷子就不要造车轮,先提高自己的姿势水平。所以我现在不自己瞎折腾了,改为翻译优秀文章,收获与提高更大!

构建完成后的静态站点位于 dist 目录下:

├── 404
│   └── routeInfo.json
├── 404.html
├── about
│   ├── index.html
│   └── routeInfo.json
├── blog
│   ├── index.html
│   ├── post
│   └── routeInfo.json
├── index.html
├── main.6643724f.js
├── main.6643724f.js.map
├── robots.txt
├── routeInfo.json
├── static.4e7b9259.js
├── static.4e7b9259.js.map
├── styles.97353e12.css

我们主要来看 blog/post/ 下,上面的结构中没有展开这块,我们进去后继续看:

.
├── 1
│   ├── index.html
│   └── routeInfo.json
├── 10
│   ├── index.html
│   └── routeInfo.json
├── 100
│   ├── index.html
│   └── routeInfo.json
├── 11
│   ├── index.html
│   └── routeInfo.json
├── 12
│   ├── index.html
│   └── routeInfo.json
├── 13
│   ├── index.html
│   └── routeInfo.json
├── 14
│   ├── index.html
│   └── routeInfo.json
……

我们会看到如下结构,其中:

  • 以数字命名的目录自然想到就是我们的 postID
  • 每个目录下都包含两个文件
    • index.html:静态化网页,文章内容被固化在 HTML 里面了,待会儿我们详细看一看
    • routeInfo.json:static.config.js 中为每个帖子提供的数据被保存为 routeInfo.json

routeInfo.json 是干嘛的

我们首先会有疑问,routeInfo.json 是干嘛的?index.html 中把帖子数据固化进去了,为啥外面还要存一份 routeInfo.json 呢?

让我们设想一种场景,从 1 号文章跳转到 2 号文章,如何做呢?

  • 我们可以从 1 号的 index.html 跳转到 2 号的 index.html
  • 但是这样会把 2 号的 index.html,重新下载一遍
  • 我们已经知道,1 号与 2 号的 index.html 除了内容区外都是一样的,如果重新下载一遍,除了内容外的数据下来都是浪费
  • 能不能只下载内容呢?
  • react-static 说可以,只需要下载 2 号的 routeInfo.json 就可以了

所以说,react-static 构建出来的站点速度非常非常快!⚡️⚡️而 routeInfo.json 只是其针对速度的优化之一!

我们以 1 号博客为例,看看他的 routeInfo.json:

{
    "template": "../src/containers/Post",
    "sharedHashesByProp": {},
    "data": {
        "post": {
            "userId": 1,
            "id": 1,
            "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
            "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
        }
    },
    "path": "blog/post/1"
}

可以看出:

  • 这就是我们在 static.config.js 中提供的数据,附带了一些 meta 信息,比如指定模板路径

dist/blog/post/1/index.html

下面我们再来看看 1 号博客的静态 HTML:

<!DOCTYPE html>
<html lang="en">

<head>
    <link rel="preload" as="script" href="/templates/src-containers-Post.634b5ebb.js" />
    <link rel="preload" as="script" href="/templates/styles.97353e12.js" />
    <link rel="preload" as="script" href="/templates/vendors~main.9dfd262a.js" />
    <link rel="preload" as="script" href="/main.6643724f.js" />
    <link rel="preload" as="style" href="/styles.97353e12.css" />
    <link rel="stylesheet" href="/styles.97353e12.css" />
    <meta charSet="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, shrink-to-fit=no" />
</head>

<body>
    <div id="root">
        <div style="outline:none" tabindex="-1" role="group">
            <nav><a href="/">Home</a><a href="/about">About</a><a href="/blog">Blog</a></nav>
            <div class="content">
                <div style="border:1px solid red">
                    <div><a href="/blog/">&lt;
                            <!-- --> Back</a><br />
                        <h3>sunt aut facere repellat provident occaecati excepturi optio reprehenderit</h3>
                        <p>quia et suscipit
                            suscipit recusandae consequuntur expedita et cum
                            reprehenderit molestiae ut ut quas totam
                            nostrum rerum est autem sunt rem eveniet architecto</p>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script type="text/javascript">
        window.__routeInfo = { "template": "../src/containers/Post", "sharedHashesByProp": {}, "data": { "post": { "userId": 1, "id": 1, "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" } }, "path": "blog/post/1", "siteData": { "title": "React Static" } };</script>
    <script defer="" type="text/javascript" src="/templates/src-containers-Post.634b5ebb.js"></script>
    <script defer="" type="text/javascript" src="/templates/styles.97353e12.js"></script>
    <script defer="" type="text/javascript" src="/templates/vendors~main.9dfd262a.js"></script>
    <script defer="" type="text/javascript" src="/main.6643724f.js"></script>
</body>

</html>

其中:

  • head 去加载了一堆 js,带有 preload 属性,是预加载的 js
  • 在 body 末尾也加载一堆 js
  • 这些 js 我们先不去管它
  • 中间的 body 部分是已经固化的文章,这就意味着,就算这些 js 都没加载完,只要 HTML 下载下来,文章就已经展示出来了
  • 我们会发现有一个奇怪的 window.__routeInfo 脚本,这里面的内容一看就是上一节的 routeInfo.json,这是怎么回事呢?
  • 原来,HTML 下载下来,文章展示出来后并没有结束。js 还在继续下载着,一旦下载完成,会运行 React 框架,运行你的 APP,运行你的 1 号文章的组件代码,传入 window.__routeInfo,又把你这篇文章的代码重新跑了一遍,并再次输出到网页上
  • 但是由于前后页面的 DOM 是完全一样的,用户对此是无感知的(背后竟然干了这种事情!)

为什么要再跑一次呢?

请参见我 react-static 静态网站搭建(一):什么是 React 静态网站? 中的最后一节 One more thing

一句话概括就是:就是让用户用访问静态网站的速度,得到 SPA 的丰富体验。

之后呢

示例工程我们已经分析地十分透彻了。之后做什么呢?

之后就是该 Getting your hands dirty 了:

  • 添加或者自己编写一个好看的 UI 库
  • 修改各个页面的模板
  • 选择一种适合自己工作流的数据提供方式
  • 开始编写自己的博客吧!

结论

经过这三篇的系列文章,我们已经明白了什么是静态网站,什么是 react-static,如何创建静态站点,尤其是静态博客。

从下一篇开始,我将介绍各种用 react-static 创建网站的实用技巧。欢迎持续关注!