使用 React 构建 WordPress 单页应用主题的实践

梨子2018-11-10,更新于 2019-8-22
本文最后更新于 1369 天前(2019-8-22),其中的信息可能已经有所发展或者不再适用于现阶段。
本文全长 4717 字,全部读完大约需要 14 分钟。

以下将要介绍的就是你现在看到的这个主题喵!

2019-08-22 更新:换主题了,不是这个了

背景

想要开坑写自己的博客这个历史由来已久。

最初我们的博客是用的 WordPress,为什么呢,因为那时候只知道 WordPress。“又一个 WordPress 站点!”从官网上下载的安装包,轻松几部就搭建了一个“self host”的博客。早年的时候,我和 bs 有一天踩了很多坑以后,说“我们应该开一个博客,记录下这些坑给别人看”,就在某台租的虚拟主机(还不是 VPS)上搭建了这个博客。

(啊真遗憾我好像没有留图,只能靠描述了,要是以后找到了补给大家)这个博客最初的时候是用的一个两栏主题,左边约是 40% 是导航栏,右边是文章,背景是悬浮在空中的 2645 工作室的标志。后来改成了左边约 80% 都是文章,右边导航栏比较小,主题色是红色的一个主题。这个主题用了很长时间,老读者们应该都有印象。我们刚刚从主机屋的虚拟空间把博客迁到阿里云青岛的时候换的这个主题,当时兔子跟我说,比原来好看多了!

后来听说静态博客很好,便跟风去 GitHub Pages 上学习了一番,用 jekyll 搭建了静态博客。在 GitHub 上编辑就可以实时更新,听起来好像很方便的样子!但是实际上用了一段时间以后发现,感觉体验不是很好。编辑的时候没有像博客后台这样的可视化编辑器,在 GitHub 上想 Preview 还得切个按钮然后等好久。另外每次编辑都会出现一条 commit,令人不爽。如果不想这样的话就得每次正经地打开电脑在工作环境写博客,感觉也挺不方便的。

学习了前端以后,咱迷上了 SPA。于是就想搞一个 SPA 的博客。起初是想自己写的,那时候也是我刚刚学了 go,用 go 写了个简单的博客后端,kotori。写完以后觉得诶很好!但是过段时间以后发现,啊,功能好少。于是一顿重新设计以后设计了一个特别复杂的 kotori-ng。然而在我每天苦恼于这个东西该怎么实现的时候,有一天 GitHub 被微软收购了。在满世界的热烈讨论中,我看到这样一张图,一个某 CMS 的 GitHub 仓库页面中浮现出一枚 Word 里那种曲别针,它说了一句类似这样的话

Looks like you're trying to build a CMS, we've found 11 clinics for you.

我就顿悟了。做一个 CMS 是一项极其复杂的工作,我没有必要尝试踩这么一个坑。

后来一打听,果然 WordPress 已经有了 RESTful API。于是我立即决定写一个自己的 WordPress 的 SPA 主题!

后续:

我发现了 peco 这个神奇的东西。它有很多惊艳的功能:它提供了 out of box 的预渲染功能,即,你像写 Hexo 博客一样写,编译的时候直接编译出一堆网页同时还是 SPA。体验无比的好。更厉害的是,它可以在 Markdown 里直接引用 Vue 组件,同时在 frontmatter 里传入 data。哇我感觉这个功能帅爆了。不仅别人看我的博客的时候不会因为翻到下一篇文章而停止音乐播放,而且我还可以直接在 frontmatter 里写 yaml 来指定这篇文章播放哪首歌。但是我在写 peco 主题的时候发现这东西完成度太低了,很多 api 都没有(相比 hexo)。而且最近作者说要重写了但是一直在咕。。

Gatsby (不知道怎么读的朋友,就是了不起的盖茨比的那个盖茨比)这个东西呢,基于 react 的和 peco 类似的功能。但是强行植入了一个 GQL,有什么用没看出来,写起来麻烦麻烦不少,而且直接导致了不能像 peco 一样灵活地利用 frontmatter。研究过一阵以后觉得没什么意思,还是坐等 peco 吧。另外 Gatsby 也是可以直接用 WordPress 作为后端的,但是由于 WordPress 的迷之 API 设计(也许符合 RESTful 规范的 API 就应该是这样的吧),导致那个能用的功能非常少,我一会在介绍我写的这个的时候会详细讲。

致谢

这个主题,在设计上主要参考,或者说 inspired by 这几位前辈的博客。

总之,向这几位前辈致谢。

Blessing Studio

主题 printempw 说叫 Seventeen,可是链接已经打不开了。大部分的视觉风格都是从这里抄来的。其中我特别喜欢 text-shadow,写出的文章看上去很有感觉。

空樱酱的博客

不知道主题是谁写的,也没查到叫什么名字。这里主要是参考了顶部、移动导航栏的效果和评论框的排版。

空樱酱超可爱哦!原谅我直接把小鸟的点心那句话搬过来了,太可爱了。

梦笔记

主题叫向日葵(Himawari),吟梦自己写的,但似乎没有公开源代码。这里主要是参考了文章内容的一些样式。

技术栈

这个博客所用的技术:

React 16 (create-react-app-ts)

在写这个博客之前我刚学了 Vue,我对前端的兴趣其实有一部分原因也是吟梦和空樱酱的博客太好看了www。

因为看了吟梦的 这篇文章 所以很想学一下 React。这次就尝试顺便学了下 React。

到今天写这篇文章的时候,我甚至已经用过了 Angular。感想的话,React 功能非常不全。很多功能都没有,比如说 Vue 的 v-model,Angular 的 ngModel,在 React 这甚至要自己实现。还有就是 vue-router 自带一个 keep-alive 功能,react-router 没有!还有 vue-router 和 Angular 都有导航守卫的功能,react-router 也没有。

另外 vue 和 react 对比(先不说不用虚拟 DOM 的 Angular),vue 有精确地控制 state 的功能,不需要每次改了什么都要调用一次 this.setState 这个反人类的东西。另外得益于 vue 这个对自身状态有感知的特性,vue 也要比 setState 要高效。在设置数组或者对象这些引用变量的时候,vue 能够知道这变量内部更改了哪些东西而不用替换整个变量。

但是尽管如此,React 仍然成为了我最喜欢的前端组件(框架)。

首先它很优雅。它是 react、vue、angular 中最组件的一个。它只做自己作为一个 lib 应该做的事。我可以像用一个 js 库一样去用它。比如我 import 了一个 react 组件,我使用 React.render 把它渲染到 DOM 里。不需要刻意按照某种框架,某种模式,某种文件格式来书写。我完全知道我在做什么,我就是在用一个普普通通的 js 库。简单,清晰,优雅。

第二它很规范。

vue 真是魔改的一塌糊涂,完全不按照 javascript 常理来。虽然降低了新人的门槛(我入门的时候超喜欢 vue 来着),但是学过 javascript 以后就会倍感困扰。譬如说,

private _username = '';
public get username () {
  return this._username;
}
public set username (username) {
  console.log(username);
  this._username = username;
}

在 vue 里 这段代码用不了,而且是只有 getter 好使 setter 不好使。而且那个 getter 的用法还出现在文档里了,作为计算属性的类风格写法。这段代码你能看见输出,但是 this._username 不会更新,也不会有错误或者提示。要想让他工作你必须得用 v-model(或者其他绑定了事件的方法),然后把副作用写在这里

@watch(_username)
private onUsernameChanged() {
  console.log(this._username)
}

这就有点令人爆炸了。

Angular 虽然没有做 Vue 这么多的魔法(vue 里你一个 object 都有些奇怪的魔法),但是也还有一些魔法的。比如通过 Component 装饰器,可以把类的私有成员作为 state。即,类的私有成员更改了,视图就会更新,这一点跟 vue 一样。而 React 则老老实实的是 this.props,this.state,虽然看上去多写了不少没用的东西。但是实际上我能很好地知道哪些变量是跟视图有关的,哪些是无关的。无关的那些,我就不需要放到 state 里,它就是一个普普通通的类成员,我不用担心它有什么多余的行为。

第三呢,它的不足,往往有强大的生态来弥补。比如说上面说的不如 vue 的地方,可以用 mobx,用起来爽得飞起。

最后,也是 React 最棒的一点,那就是它的高阶组件(HoC)。高阶组件实在是太棒了,以至于我反复搜索 Angular 和 Vue 如何实现 HoC。很遗憾,至少 Angular 和 Vue 都是没有官方实现的。

什么是高阶组件?高阶组件是一个函数,它能够接收一个组件作为参数,返回给你另一个组件。加上 jsx,真是完美的组合。我可以任意地来抽象我的类。例如在这个项目里,我的文章列表、文章和归档都是用同一个高阶组件装饰的。它们虽然显示的不一样,但是内在逻辑都是一样的,拉取文章、评论等等。这样通过组合类的形式,一方面能够避免 mixin 的各种问题,另一方面我可以自由地抽象、封装渲染视图的逻辑。

Typescript

自从用了 typescript 以后,我再也离不开它了。一方面懒得配置 babel 那堆东西,一方面 typescript 的语法补全更强,一方面 tslint 真的是好用。

Honoka

在写这个东西之前,我甚至还不是很会用 Promise。所以如果你真的想看看这个代码的话一定轻喷多指教啊、

在一个夜里,我花了一个通宵来学习什么是 Promise。我还记得的那天早上我非常开心,我被 Promise 卓越精巧的设计所折服。还有空樱酱写的 honoka 这个库,用了以后要感动哭了,太好用了。

那天早上我踏着清晨去食堂吃了油条和豆腐脑,超级开心。

Monaco Editor

monaco-editor 是我在这个项目里语法高亮的解决方案。它是微软从 vscode 里抽出的代码,用来在网页上运行的。

在这个项目里是我第一次尝试用,主要是为了代码高亮。本来想用 webpack 打包的,上网找了一个 react-monaco 的库,用了之后打包出来的贼大,就删了换成微软的 cdn 了。M$ release 的版本比我自己编译的小多了。

后来我在实习的公司做一个字幕编辑器,太好用了,语法高亮错误提示自动补全,API 超级简单,文档特别清楚,而且 issue 里作者态度极好,有问必答。从此成为了一名软粉。

如果你还在用 CodeMirror 或者 Ace 的话,你该试一下 Monaco。

我常常想,做这么个东西对微软有什么好处呢。养着这么多开发,都是爱呀qwq

Typed.js

这个东西很好看。但是用起来有不少问题。

首先 CSS 文件好像它的 JS 不会注入,得自己想办法从官方示例里抠出来。不然你就没有光标。

另外它还有严重的 bug,这是我提的 issue

i18next

这个国际化的解决方案貌似是最新最好的了。

如果你想用这个主题然后想改一些文字的话,可以直接去改 locale。

WordPress

WordPress 的 REST API 是这个样子的:

declare module 'wordpress' {
  export interface Post {
    id: number;
    title: Content;
    categories: number[];
    tags: number[];
    date: string;
    date_gmt: string;
    modified: string;
    modified_gmt: string;
    slug: string;
    excerpt: Content;
    content: Content;
    comment_status: string;
  }
}

看,它什么关联的字段都没给你!categories,tags 都是 number!!

好,那么你现在想要显示目录是什么怎么办,你就得一个一个文章地去查他那个 number 究竟是什么鬼东西。

如果说这个还有方法优化的话(把整个列表当前页的文章的 categories 给 merge 起来,然后查一次),如果你想显示每篇文章的评论数,你就彻底没办法了。只能一个一个地查。如果你这一页上有 10 篇文章的话,你就得请求 10 次,100 篇就 100 次。

那么上面提到的 Gatsby 是怎么做的呢?没错!它只显示文章,别的什么都显示不了。看看这个 Gatsby + WordPress 的博客。

后来我发现其实它可以显示目录和 tag 的, 具体怎么实现的,中间应该是有个缓存层,大概这就是 GraphQL 的典型应用场景吧(

所以怎么办呢。在异步拉取目录、标签、评论数(先把文章显示出来再慢慢加载,要不太慢了)的基础上, 我设计了一个超级缓存功能。你在浏览中的一切下载的数据都会被缓存到一个本地的 map 里,列表、文章、目录、tag、评论数等等。访问的时候先从本地缓存里查看看有没有,如果有就先显示,然后再异步地拉取更新。包括上一篇下一篇都会先尝试从本地缓存中计算出来,如果处于页面的边界才会去拉 api。

缓存功能可以在选项里配置。

主题色

主题我是用的 μ's 的主题色(奇迹的颜色!),然后主题的名字是她们 solo 的专辑名(以颜色命名)。

背景是我从 ぷちぐるラブライブ!(宇宙第一消消乐)里截图出来的。本来在游戏里那个排列是斜 30°,我研究了好久都没想出来怎么用 CSS 让一张小图片斜 30° 平铺。网上查背景怎么旋转都说不行,要旋转整个 div。但是旋转 div 的话就很难盖住视口,尤其是在网页长度还不确定的情况下。

最后还是向 45° 低头了,也就是你们看到的样子。

如何安装该主题

首先你必须要装一个 WordPress 插件来启用 REST API 不登录评论和把文章原始 HTML 通过 REST API 传到前端的功能。安装的步骤很简单,就一个 php 文件扔进插件目录然后在仪表盘里启用就行了。

然后克隆该项目,如果你需要修改一些配置(如菜单栏啊什么的),它们在 src/config.js

然后编译它

npm install
npm run build

最后配置 http 服务器,我这里给出一个示例 nginx 配置。

# Virtual Host configuration for example.com
#
# You can move that to a different file under sites-available/ and symlink that
# to sites-enabled/ to enable it.

server {
        listen 80;
        listen [::]:80;
        server_name blog.cool2645.com;

        rewrite ^(.*) https://blog.cool2645.com$1 permanent;
}

server {
        listen 443;
        listen [::]:443;
        server_name blog.cool2645.com;

        root /var/www/blog;
        index index.htm index.html index.php;

        ssl on;
        ssl_certificate /etc/letsencrypt/live/www.cool2645.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/www.cool2645.com/privkey.pem;
        ssl_trusted_certificate /etc/letsencrypt/live/www.cool2645.com/chain.pem;

        rewrite /wp-admin$ $scheme://$host$uri/ permanent;
        location / {

                root /var/www/blog/orange-cheers;

                try_files $uri $uri/ /index.html;

        }
        location ~ \\.php$ {
                include snippets/fastcgi-php.conf;
                # With php7.2-fpm:
                fastcgi_pass unix:/run/php/php7.2-fpm.sock;
        }
        location ~ /feed|wp-.*/ {
                try_files $uri $uri/ =404;
                if (-f $request_filename/index.html){  
                        rewrite (.*) $1/index.html break;  
                }  
                if (-f $request_filename/index.php){  
                        rewrite (.*) $1/index.php;  
                }  
                if (!-f $request_filename){  
                        rewrite (.*) /index.php;  
                }  
        }
}

注意几个问题:

  1. 重写 404 页面到 /index.html,这是所有 SPA 都要的
  2. 注意把 feed 和 wp-.* 仍然分发给原来的 WordPress,保证 RSS feed、后台、以及 REST API 能正常访问到

注意事项

如果你开了会影响文章内容的插件(如代码显示插件),你需要把它关上。

部分插件会导致 WordPress 无法正常保存没有转义过的原始 HTML,如果你存在这个问题,把奇怪的插件和主题都禁用了。

为了保证向后兼容性,你需要在使用 Markdown 书写的部分前后加上 pre 标签:

<pre lang="markdown" class="lang:markdown">
  # markdown goes here
</pre>

注意 Markdown 里不能再有 HTML 标签了!

如果需要在 Markdown 里使用 HTML 的话,可以先关闭 pre 再书写 HTML。

其他

万一你想用这个主题的话,

源代码在这里

如果你遇到了什么问题,欢迎联系我。

除特殊说明以外,本网站文章采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。