用 vue 写一个单页的 hexo 主题

2018 年 5 月 5 日更新: 因为我突然发现如果采用单页的话,就很难处理 SEO 的问题,所以我现在已经基本不再考虑在 hexo 的基础上考虑这个主题。所以我现在也已经在使用别的主题。在未来,我会考虑采用 vuepress 或者 peco 搭建我的博客。


大概一年以前,我发布过一个主题,名为 hexo-theme-mls。这个主题是我当时在学完如何写一个 hexo 主题后完成的。后来在学了 vue 之后,构思着再写一个主题。我非常想把它写成一个单页应用,可是一直没有思绪。直到后来看到了 hexo-theme-one,我才有了思绪,于是开始写一个单页的主题。

restful

首先一个问题就是,如何生成 json 文件,用于前端的异步数据请求。hexo-generator-restful 提供了很好的思路,hexo 在执行的时候,会载入主题文件夹下的 scripts 下的文件并执行。而 hexo 提供了一个接口,可以用于在读取所有博客内容之后,生成所有文件之前,添加需要生成的文件。

generator 接口即是需要的接口。注册一个函数,这个函数可以在执行完后返回一个数组,数组每一个元素即是一个将要生成的文件的信息,这个信息包括:

1
2
3
4
5
{
path: post.path, // 路径
data: post, // 这个文件的内容
layout: 'post' // 布局
}

一般文件生成后会在 public 文件夹下,那么这个路径只要是相对 public 即可,比如 api/posts.json,则会在 public/api/posts.json。至于文件内容,因为这个 data 一般要求是字符串或流 (stream),所以我选择用 JSON.stringify() 将需要的数据 (对象) 转化为字符串。

那么最终需要生成哪些 json 文件呢?

所有的 posts 数据

一个 posts.json 记录了这个博客所有的博客大概信息 (每篇博客的标题,包含的标签,发布日期等),但不包含博客文章的具体内容,因为如果将所有文章内容放进一个文件的话,那么这个文件可能非常大。因而,准备再生成一个 post/ 文件夹,下面存放了各个博客文章的具体信息。如果一篇博文标题为 Foo。那么请求 api/post/Foo.json 可以拿到这篇博文的具体信息。

所有的 tags 数据

类似于 posts 数据,一个 tags 用于说明这个博文下所有的 tag,一个 tag/ 文件夹包含每一个 tag 的具体信息(这个 tag 下有哪一些文章)。

其它

一些配置信息,比如 siteConfig,和 themeConfig。另外如果有自定义 page 的话,再放一些信息于 page/ 文件夹下。

404 处理

这个是我当时最头疼的一个问题,毕竟 hexo 当初又不是为单页设计的,估计也考虑不到这个问题。

我有一个路由是 /posts。但在 hexo g 运行完后,在 public 目录下面是没有 posts/index.html 这个文件的。因此如果运行 hexo s 的时候,如果直接访问 localhost:4000。那么 hexo 会直接报错,提示 Can not get /posts。如果要解决这个问题,那么我必须在所有可能的路由下生成 index.html。如果我有 100 个路由,那么我需要生成在 100 个文件夹下生成共计 100 个 index.html

可是这很明显不符合我对 SPA 的预期。按照最初的想法,我只需要一个 index.html。无论浏览器访问哪个路由,后端都能返回同一个 index.html。这一点可以参考 vue-router 官网上对 404 重定向的一个说明 – HTML5 History 模式

当然我上面这么说的自然原因是我采用了 HTML5 History 模式。我当然可以选择放弃这种模式,采用 URL hash 的方式。也就是说,我的博客地址会变成 /#/post/foo 的样子。但这里又出现了另外一个问题,disqus 对评论的获取是不考虑 # 之后的内容。换句话说,如果用户访问 /#/post/foo/#/post/bar 时,disqus 都会去拿 / 路由对应的评论,所有的页面评论就全部一样了!

因此我不得不再继续思考如何采用上 HTML5 History 模式。如果只考虑部署的话,GitHub Pages 有 404 重定向的方式 (建立 404.html) 以及 nginx 也有类似功能。所有在部署之后是不会有这问题,我需要做的,是对 hexo s 的扩充。

hexo 确实开放了对 hexo s 的扩充的接口,结合这个 ISSUE: https://github.com/hexojs/hexo/issues/1030。我可以在 hexo s 中实现在 404 时的重定向了。但有两种方案:

方案一:

1
2
3
4
5
6
7
8
hexo.extend.filter.register('server_middleware', function _404middleware (app) {
app.use(function handle404(req, res, next) {
const s = fs.createReadStream(
path.resolve(__dirname, '../../../', hexo.config.public_dir, './index.html')
)
s.pipe(res)
}, 99)
})

这种方法,就是直接读取生成好的 index.html 返回。这种方案在大部分情况下可行。

方案二:

1
2
3
4
5
6
7
8
9
10
11
12
13
hexo.extend.filter.register('server_middleware', function _404middleware (app) {
app.use(function handle404(req, res, next) {
const { pathname } = url.parse(req.url)
if (!pathname.endsWith('.json')) {
res.writeHead(302, {
'Location': url.resolve(hexo.config.root, '404.html?redirect=' + req.url)
})
res.end()
} else {
next()
}
}, 99)
})

其中 99 代表 priority。默认 server 的值为 10。priority 越小越先执行。让默认服务先运行,如果出现 404,那么上面的代码才会执行,进行重定向。注意我增加了一个 redirect 的请求参数,专门用于在前端继续跳转一次。如果不跳转,前端 url 就变成了 /404.html。但我想在当前 URL 下也能显示 404 页面,比如在 /post/xx 页面下显示 404 的内容,而不是跳转到 /404.html 显示。其中注意不要对 json 文件进行处理,仅对一般请求处理即可。

方案二,和方案一相比,两者都不算美观,并不能说是我最满意的方案。但相比之下,方案二,还好一点,原因是方案一一个明显的问题就是,它的成功运行是基于 public 目录下存在 index.html 的前提下。如果这个前提不满足,那么就无法继续。因此我暂时采用了方案二。

页面设计

上面讲的,其实大多是 hexo 相关内容。至于页面设计就简单多了,因为后端路由已经解决了。前端实际上只是样式的问题。因为当时想做的简单一点,于是才用了 milligram 这么一个纯 css 的极简框架。

加上 vue 全家桶,以及 axios 用作异步请求工具,前端开发似乎并没有遇到什么大困难。

唯一可能算的上问题的或许是字体问题吧。原本字体用的是 google fonts 的链接,而总所周知,这在国内体验很不好,因此花了点时间,把字体从网上下载,并写成 css 引用的方式,打包成为主题的一部分。

其它

我考虑过要不要加上多语言支持,也就是 i18n。不过感觉没什么必要,并没有那么多需要翻译的。所以这个坑就先留着吧。

在我写这篇文章的时候,hexo.io/themes 也有了一些其它单页应用的主题,或许未来应该有更多的吧。可能 hexo 未来会对单页应用的博客做一些相关的支持,也可能会出现一款为单页应用博客而定制的博客生成器吧。

另外,记得刚把这个发布并用在自己博客上的时候,有伙伴吐槽 “有点丑呀”。或许吧,和其它主题比起来,确实“朴素”了太多。不过不知道为什么,现在的我确实喜欢这样的“朴素”了呢。

顺便贴一下项目地址,hexo-theme-only。欢迎使用和提供批评意见。


散华礼弥散华礼弥