为什么要迁移到Hexo
WordPress的存在,让我们很容易的搭建出属于自己的功能完善的博客系统,并且提供丰富的插件、主题。当然还存在一些问题例如:
- 运行时需要PHP与MySQL,个人小站服务器性能孱弱,内存小,这几个占用资源相对较多。让我印象深刻的是Docker中部署MySQL容器,启动后,剩余内存从500MB到不足100MB,很是吓人。
- 自从开始用Markdown写文章,就爽的停不下来,WordPress需要第三方插件来支持Markdown,效果一般,对表格支持有问题。
- 最重要的一点是维护成本,WordPress的功能强大,导致维护成本较高,比如备份和迁移,一直用BackWPup来做备份,当恢复导入sql时,会遇到诸如数据库不同版本支持的编码不同,从而导入失败,要手动改sql自动编码格式。解了一个问题还有一系列各种问题,鼓捣鼓捣几个小时就过去了,不优雅。
- 希望能有这样的方案,用Markdown来编辑内容,用Git来做备份和发布,本地编辑完内容后,一条命令,自动部署到服务器并发布。哪怕服务器挂掉,我的内容仓库在本地或云端始终有备份,只需重新发布即可。
内容与展示的耦合
后来就找到了Hexo,当然还有一些类似的博客系统。看完Hexo官网介绍,心中出现一种声音:就是你了。
Hexo的搭建比较顺利,中间也遇到一些问题,我把问题都总结在本文最后一节。其实不论WordPress也好,Hexo也好,博客最重要的是内容,我们使用的各种博客系统,可以看做是一种内容的展示方式、形式。今天我迁移到了Hexo,也许过一阵子,就有其他的框架更能满足我的需求,可能会再次迁移,由于不同框架的结构不同,设计理念也不同,就会带来额外的迁移维护成本。
我的设想是,对于内容,自己定义一套存储维护方法:每篇文章使用Markdown写作,图片等资源放入同目录下资源文件夹。因为Markdown是纯文本,可以很方便的使用Git来做版本控制或云端备份。设计一个adapter,用来生成与目标框架匹配的内容。如此,在内容上,我只需要维护自己的这套内容管理方法,如果要发布到WordPress,我就写一个adapter程序来适配WordPress的发布方法;如果要发布到Hexo,就写一个adapter程序适配Hexo的发布方法。通过这个适配器模式,来实现内容维护与展示形式的解耦。

当本地编辑完成一篇文章,Git提交,并推送到远程仓库,可以设置多个远程仓库,其中一个就放在网站服务器上,服务器Git通过Git Hooks检测到有新文章推送来时,触发Adapter转换程序,开始一系列转换,完成后发布给博客框架。
解耦方法
- 本地内容结构
- Hexo中source目录结构
- 搭建Git服务端以及Git Hooks设置
- 针对Hexo的Adapter
本地内容结构:
articles/
|---artical1
| |---artical1.md
| |---src/
| |---pic1.jpg
| |---pic2.jpg
|
|---artical2/
|---artical2.md
|---src/
|---pic1.jpg
|---pic2.jpg
在artical1.md中,若需要插入图片,可直接引用,本地编辑过程中即可预览图片。
Hexo中source目录结构:
source/_posts/
|---artical1.md
|---artical2.md
|---artical1/
| |---src/
| |---pic1.jpg
| |---pic2.jpg
|
|---artical2/
|---src/
|---pic1.jpg
|---pic2.jpg
由于Hexo的目录机制,在生成public后,首页的图片地址生成有误,需要安装hexo-asset-image插件,并且在_config.yml中设置post_asset_folder: true,
这样来安排Hexo中的目录结构,md中引用图片的路径不需要修改。
在服务器端,搭建Git仓库,用于接收文章push:
git init --bare post.git
就会在当前目录下生成post.git文件夹,使用--bare参数来初始化Git,是创建一个空仓库,这个空仓库没有工作区,在此目录下只能用来接收pull推送,不能对仓库进行pull、checkout、commit等操作。
在本地,可以通过push来推送提交:
git remote add origin2 user@domain.com:/path/to/post.git
git push origin2 master
user是用来登录服务器的用户名,domain.com是服务器域名,也可用IP代替。
因为我们要在服务器端监听push动作来触发Adapter程序,所以在post.git/hooks/下,创建post-update文件,实际上post-update可以是个shell脚本,Git会在push后在hooks文件夹下自动根据文件名称去执行。
我并不擅长写shell,于是在post-update里执行了一段Python脚本,这个exe.py脚本就是Adapter。
#!/bin/bash
unset $(git rev-parse --local-env-vars)
export NVM_DIR="/usr/local/nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
python hooks/exe.py
实际上只有最后一行才是执行exe.py,其他几行的用处放在最后面问题总结里面。
这里Adapter的作用是将我们自定义的内容结构,转换成不同博客框架的文件结构,下面附上针对我自己的内容维护结构转Hexo框架所需文件结构的的Adapter:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import subprocess
import shutil
__author__ = "xzxiao"
# 1.清理hexo/source/_post
# 2.clone post.git 到hexo/git/pull/文件夹下
# 3.复制pull根目录下文章文件夹到_posts下
# 4.遍历子目录,把_posts文件夹中的所有md文件移动到_posts下
# 5.hexo clean
# 6.hexo g
HEXO_PATH = "/path/to/hexo"
SOURCE_PATH = HEXO_PATH + "/source"
POST_PATH = SOURCE_PATH + "/_posts"
SRC_PATH = "/src"
GIT_DIR = "/path/to/post.git"
GIT_PULL_DIR = "/path/to/pull"
def clean_post():
print "clean_post: "
path = POST_PATH + os.sep + "*"
print path
subprocess.Popen("rm -rf " + path, shell=True, executable='/bin/bash').wait()
def clone():
print "clone:"
if os.path.exists(GIT_PULL_DIR + "/.git"):
print ".git exist, pulling"
subprocess.Popen("git pull", shell=True, executable='/bin/bash', cwd=GIT_PULL_DIR).wait()
else:
print ".git not exist, cloning"
subprocess.Popen("git clone " + GIT_DIR + " ./", shell=True, executable='/bin/bash', cwd=GIT_PULL_DIR).wait()
def cp_post():
print "cp_post:"
for root, dirs, files in os.walk(GIT_PULL_DIR):
if root != GIT_PULL_DIR:
break
for dir_name in dirs:
src = os.path.join(root, dir_name)
if not src.__contains__("hook") and not src.__contains__(".git"):
print "cp " + src + " to " + os.path.join(POST_PATH, dir_name)
shutil.copytree(src, os.path.join(POST_PATH, dir_name))
def mv_md():
print "mv_md:"
for root, dirs, files in os.walk(POST_PATH):
for file_name in files:
if file_name.endswith(".md"):
print "mv " + os.path.join(root, file_name) + " to " + POST_PATH
shutil.move(os.path.join(root, file_name), POST_PATH)
def clean():
print "clean:"
subprocess.Popen("hexo clean", shell=True, executable='/bin/bash', cwd=HEXO_PATH).wait()
def g():
print "g:"
subprocess.Popen("hexo g", shell=True, executable='/bin/bash', cwd=HEXO_PATH).wait()
if __name__ == "__main__":
clean_post()
clone()
cp_post()
mv_md()
clean()
g()
开头的注释说明了主要做的6个步骤,当服务器接收到新文章的push后,就会执行此脚本,最终完成Hexo的重新发布。
我没有用Hexo自带的服务器程序,而是用的Nginx,配置规则后直接跳转到Hexo生成的public目录,其实Hexo就是一个把Markdown文档按照一定规则和主题样式,转换成静态html的框架。
问题总结
在Git Hooks中执行git pull命令,会出现Not a git repository: '.'错误,当排除脚本里的目录问题后,问题由于hooks在运行的过程中,GIT_DIR和GIT_WORK_TREE环境变量会被设置,导致git pull的目录并不是你指定的那一个,所以需要在git pull之前,执行unset $(git rev-parse --local-env-vars),重设环境变量。
脚本中执行hexo命令提示command not found。在当前用户的.bashrc中,nvm会自动添加NVM_DIR变量,用户登录进shell环境,就会自动加载nvm环境变量,如果在hooks中命令需要用到nvm,执行前需要手动设置环境变量
export NVM_DIR="/usr/local/nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
Python的subprocess.Popen方法,用于替代os.popen(),可以在Python中执行shell命令,此方法参数cwd用于指定当前工作目录,executable用于指定shell,若要等到一条命令执行完后再执行下一个命令,可以在方法最后调用.wait(),阻塞。
Git Hooks下的post-update文件要给执行权限
nvm要全局安装,如果只安装给了root,切换到其他用户是找不到nvm的。