2019-03-18
经过前面两篇文章,相信大家已经对静态博客和 react-static 有了充分的认识。在本节我们来一个实战环节,手把手教你如何用 react-static 搭建一个静态博客。
首先需要安装 react-static :
$ yarn global add react-static
# or
$ npm install -g 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"
进入工程,开搞!
首先我们把项目先跑起来。在项目根目录,执行下面命令:
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)看看:
其中:
文章详情页(/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
其中,我们关心的有:
它定义了网站的整体框架:顶部导航栏、侧边栏,以及划定内容区的范围。各个页面都展示在内容区当中。
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
其中:
这个文件是网站的入口,主要用于 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)) } }
其中:
这个 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> ))
其中:
这里对应的是博客列表页:
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> ))
其中:
这对应的是文章详情页。
眼尖的同学会发现代码路径变到了 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, }), })), }, ] }, }
是不是非常简单直观!其中:
在示例工程中,数据是调用 API 接口来获取的。
有一点需要弄清的是,这里从 API 获取数据发生在构建时,只在构建时执行一次。react-static 会拿这一次获取到的数据生成静态网站。
这也就是说,当一次构建发布后,如果 API 有了更新,线上的网站不会更新,除非再执行一次构建过程。别忘了我们是静态网站!
能不能不从 API 中获取呢?当然可以。
上面代码中 getRoutes 就是个 node.js 的方法,怎么获取数据完全由你编写的 js 代码控制。
我们开下脑洞,可以如何获取数据呢?
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
……
我们会看到如下结构,其中:
我们首先会有疑问,routeInfo.json 是干嘛的?index.html 中把帖子数据固化进去了,为啥外面还要存一份 routeInfo.json 呢?
让我们设想一种场景,从 1 号文章跳转到 2 号文章,如何做呢?
所以说,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" }
可以看出:
下面我们再来看看 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/">< <!-- --> 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>
其中:
window.__routeInfo
脚本,这里面的内容一看就是上一节的 routeInfo.json,这是怎么回事呢?window.__routeInfo
,又把你这篇文章的代码重新跑了一遍,并再次输出到网页上为什么要再跑一次呢?
请参见我 react-static 静态网站搭建(一):什么是 React 静态网站? 中的最后一节 One more thing。
一句话概括就是:就是让用户用访问静态网站的速度,得到 SPA 的丰富体验。
示例工程我们已经分析地十分透彻了。之后做什么呢?
之后就是该 Getting your hands dirty 了:
经过这三篇的系列文章,我们已经明白了什么是静态网站,什么是 react-static,如何创建静态站点,尤其是静态博客。
从下一篇开始,我将介绍各种用 react-static 创建网站的实用技巧。欢迎持续关注!