react-static 静态网站搭建(二):react-static 介绍

2019-03-17

前言

上一篇介绍了静态网站,在本篇中我们来看系列的主角——react-static 静态网站生成器。在本文中,我们将对它进行一个全面的介绍。

react-static 是一个 React 的静态网站生成器。

通俗来说就是:

  • 我们用 React 来编写网站(比如博客),这个过程与普通编写 React 网站一样
  • 区别在于网页数据的提供(如博文内容)
    • 对于普通 React 网站,可能需要运行时通过一个接口获取文章
    • 在 react-static 中,这个过程发生在构建时,Webpack 构建的时候就会把博文传入 React 文章页
  • 这样,react-static 构建出的博客,每篇文章都对应一个预渲染的网页,也就是静态化了

当然,不是说任何 React 网站通过它都能变静态。react-static 是一个框架,它定义了一些规则。React 应用必须按照他定义的概念、规则来编写,才能工作。

如果你对上面的过程有困惑,可以先看下上一篇

在本文中我们就来详细介绍这些规则。

特性

我们先来看下 react-static 所提供的特性:

特性说明
100% React原汁原味 React 开发,不会额外添加奇奇怪怪的约束
超快的构建速度构建速度是真的快,还十分可靠
超快的性能预加载功能开箱即用,网页秒跳
多样的数据来源向页面传入的数据来源由自己决定(自己写 JS 脚本),API,数据库,json……
自动代码和数据分离代码是代码,数据是数据,相互隔离,架构十分清晰
通过 PRPL 预加载秒跳转比较新的一种 PWA 页面预加载模式,我还没有研究过,不过开箱即用,已经先用上了 🤣
Progressive Enhancement + Graceful Fallbacks框架作者本行就是搞 SPA 的 SEO 的。构建出的静态站的 SEO 完全正常
React-firstReact 优先,类似 Python 的 Pythonic 概念,这里推崇的是 Reactic 😆
无痛项目设置与迁移亲身经历,我将本站从一个自己写的框架迁移到 react-static,迁移成本真的很低
100% 支持 React 生态CSS-in-JS、GraphQL,连 Redux 都支持
开箱即用热加载代码写完一保存,界面就立刻更新了,所见即所得,开发效率高
扩展性静态生成器一个很重要的指标是网站生成时间与数据规模的关系。我之前写过一个框架放弃了,原因就是生成时间与数据规模成正比……react-static 则完全感受不到生成变慢

数据与模板

静态网站主要分为两部分:数据与模板

以博客文章页为例:

  • 模板负责文章页的框架,除了文章部分没有填充外,它的样子就是文章页的样子
  • 数据就是博客文章的数据,每篇博客文章都对应一份数据
  • 将任意一份数据与模板进行结合,就得到了最终的博客数据,它就是最终访客看到的样子

在 react-static 中这两部分如下图:

其中:

  • 数据在构建时提供
    • 可以有多种来源
    • 通过 static.config.js 中的一个 js 方法实现
    • 这个方法是我们自己写的,因此想怎么拿数据就怎么拿数据
  • 模板对应于图中的右侧
    • 是一个普通的 React 应用
    • 模板页是 React HOC,页面数据通过参数传入,我们 return 出对应的 React 组件
    • 模板填充数据的过程也是遵循 React 的函数式思想

前面说过,不是说任何 React 网站通过它都能变静态。react-static 是一个框架,它定义了一些规则。React 应用必须按照他定义的概念、规则来编写,才能工作。

下面是时候深入水下啦!

static.config.js

static.config.js 是基于 react-static 的静态网站的核心配置文件,各种规则大部分都在这里定义。

它位于项目的根目录。我们以 maxiee.github.io 的配置文件实例来看下它的主要内容(完整选项可参见文档):

// 博客画作板块的数据提供方法
// 画作位于 maxieePaintPath 变量对应的目录下,里面是一系列图片文件
// getMaxieePaints 方法获取这些图片的路径
const getMaxieePaints = () => {
    return getAllFiles(maxieePaintPath)
        .map(v => getFileName(v))
}

export default {
    plugins: ['react-static-plugin-typescript'],    // 开启 TypeScript 支持
    entry: path.join(__dirname, 'src', 'index.tsx'),// 指定入口文件
    siteRoot: "https://maxiee.github.io",           // 网站域名
    getSiteData: () => ({                           // 网站全局数据
        title: 'Maxiee Blog'                        // getSiteData 提供的数据
    }),                                             // 在模板中通过特定 HOC 能获取到
    // getRoutes 方法最为重要,它定义了你的网站中都有哪些路径
    // 这些路径使用什么组件(模板)进行展示
    // 也定义了如何向模板提供数据
    // 这里以博客的画作板块为例
    getRoutes: async ({dev}) => {
        let maxieePaints = getMaxieePaints()        // 获取所有画作路径
        
        return [                                    // 数组,内部包含多个路径模板
            {
                path: '/',                          // 定义首页
                getData: () => ({                   // 首页模板所传入的数据
                    blogList: posts                 // 这里传入的是所有帖子的列表
                }),
                children: posts                     // children 表示首页子页面,这里是文章页
                    .sort((a, b) => { ... }         // 按照发布时间排序
                    .map(post => {                  // 读取 markdown 
                        let content = fs.readFileSync(  // 读取 markdown 文件内容
                            "./content/blog/posts/" + post.link, 'utf8')
                        let retData = Object.assign(// 将文章内容放入 retData 结构
                            {},
                            post,
                            {article: content}
                        )
                        
                        return {                    // 返回路由结构
                            path: `/post/${post.link}/`,        // 路径
                            component: 'src/containers/Post',   // 指定渲染模板组件
                            getData: () => ({                   // 向模板提供数据
                                post: retData,
                                dev: dev
                            })
                        }
                    })  
            },
            {
                path: '/gallery/',                  // 定义路径
                getData: () => ({                   // getData 中定义的数据结构就是
                    paints: maxieePaints,           // 传入模板的数据结构
                    dev: dev                        // 这里把所有画作的路径传入
                })
            },
            ...
        ]
    }
}

上面就是 react-static 核心配置文件 static.config.js 的一个实例,其中:

  • 它是一个 js 文件,我们可以在其中定义方法
  • 实际中,除了 getMaxieePaints 外,我还定义了很多函数,用来获取博客文章、书签数据
  • getSiteData 提供的是网站全局数据
  • getRoutes 提供的是路由数据
  • / 对应于网站首页,传入的数据是博客的列表,会被框架与 src/pages/index.tsx 自动关联
  • / 下有一个子路由,即 /post/${post.link}/ 这是博客文章,它的渲染组件是我们指定的,为 src/containers/Post
  • gallery 是画作板块,'/gallery/' 会被框架与 src/pages/gallery.tsx 自动关联
  • 路由这块可能会感觉有些复杂,框架文档中对这块有更加详细的介绍,可以参见文档进行深入了解。同时这个项目也提供了许多官方示例,可以将实例 clone 下来研究一下,就一目了然了

首页

接下来我们就进入网站的视图(模板)部分。所有视图、模板都是 React 高阶组件,下面我们以首页( src/pages/index.tsx )为例:

import { withRouteData } from 'react-static'

// 首页
export default withRouteData(({blogList} : {blogList : any[]}) => (
    <Fragment>
        <Helmet>
                <meta charSet="UTF-8" />
                <title>{BLOG_NAME} | Maxiee 的知识积累</title>
        </Helmet>
        <PostListView posts={blogList}/>
    </Fragment>
))

其中:

  • withRouteData 是 react-static 中定义的一个高阶组件
  • 高阶组件参数中的 blogList 眼熟吗?这就是我们在 static.config.js 中用 getData 提供的数据
  • 这里接收到博客列表,传入 PostListView 组件进行展示

画作板块

下面我们再来看博客的画作(/gallery/)板块:

import { withRouteData } from 'react-static'

export default withRouteData(({paints, dev} : {paints : [string], dev : boolean}) => {
    return <GalleryComponent paints={paints.reverse()} dev={dev}/>
})

其中:

  • 同样也是使用 withRouteData,获取到两个参数 paints 和 dev
  • paints 为所有画作的路径
  • GalleryComponent 是实际进行展示的 React 组件

通过这两个页面我们能够感觉到,这的确是原汁原味的 React 开发体验。

侧边栏、导航栏

也许你会有疑问,上面两个模板都只有正文部分的内容,侧边栏、顶部导航栏是在哪里定义的呢?

他们位于 src/App.tsx 下:

import React from 'react'
import {Root, Routes} from 'react-static'
import 'semantic-ui-css/semantic.min.css'
import { Grid } from 'semantic-ui-react';
import SideBar from './components/sidebar/SideBar'
import NavBar from '@components/NavBar';
function App() {
    
    return (
        <Root>
            <Grid stackable stretched relaxed>
                <Grid.Column width={3} style={{paddingRight: 0}}>
                    <SideBar />
                </Grid.Column>
                <Grid.Column width={13} style={{paddingLeft: 0}}>
                    <NavBar selected=''/>
                    <Routes />
                </Grid.Column>
            </Grid>
        </Root>
    )
}

export default App

其中:

  • Root 与 Routes 来自于 react-static
  • Routes 部分就是网站的正文部分
  • 我们前面的两个页面都被填充进正文部分

官方示例

也许看了上面这些,还是会觉得认识不够透彻。这很正常,因为 react-static 这种开发方式比较新颖。

彻底认识透彻的方法也很简单,react-static 官方提供了大量示例,只需要把他们 clone 下来跑一跑看一看,getting hands dirty。

具体包括如下:

其中,在 react-static 项目首页的 Sites Built with React-Static 一节中,还有很多使用 react-static 构建的网站是提供源码的,这也是很好的学习资源。

Getting Start

我将 Getting Start 放在最后,因为自己当时在初学的时候,因为对理论的不熟悉,怎么都 Getting Start 不起来。

最终当我将 react-static 的概念理解透彻后(也就是前文的内容),自然而然就上手了。

react-static 自身是一个命令行工具,安装方法:

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

创建项目:

$ react-static create

运行开发服务器:

$ yarn start # or react-static start

构建项目:

$ yarn build # or react-static build

本地托管:

$ yarn serve

小结

react-static 首页中有大量的学习资源,包括框架作者的 YouTube 演讲,强烈建议观看。

如果你想搭建一个静态网站,想要自己从头搭建,同时又使用 React,那么 react-static 绝对是首选之一。

在后续的文章中,我会继续更新 react-static 更多的使用技巧。欢迎关注~