把 blog 从 octopress 迁移到 hexo

之前用的一直用 octopress 来写 blog。对于我来说 octopress 有个最大的缺点,就是它是 ruby 写的。导致我经常就要去搞搞 ruby 的版本啊、gem 啊什么杂七杂八的事情。今天无意间看到了一个 node 写的博客工具 hexo,功能类似于 octorepss 但是部署成本比 octopress 低了不少,于是花了 15 分钟把整个博客从 octopress 迁移到了 hexo 上面。

和 octopress 类似,hexo 也是一个静态页面生成工具。它允许用 markdown 语法来写文章,并且生成并上传整个网站到 github。它和 octopress 有着完全一样的文章格式,所以整个迁移过程非常轻松。

安装 hexo

1
2
# 国内网速慢可以尝试加上 --registry=http://r.cnpmjs.org 来使用国内镜像加速
npm install -g hexo

初始化目录

1
hexo init blog && cd blog

迁移 octopress 的文章

1
2
rm -rf source/_posts
cp -a OCTOPRESS_BLOG/source/_posts source/

修改链接规则

需要修改 _config.ymlnew_post_name 一行:

1
new_post_name: :year-:month-:day-:title.md

修改博客信息

例如博客名称、网站地址等等,这些内容都在 _config.yml 中。另外还要将配置文件最后的 deploy 信息修改成 github 地址:

1
2
3
4
deploy:
  repo: git@github.com:USER/REPO
  type: github
  brandh: master

如果博客用了顶级域名,还需要在 source 目录下新建一个 CNAME 文件:

1
echo "lostjs.com" > source/CNAME

预览效果

1
2
hexo generate
hexo server

然后浏览器访问本地 4000 端口即可。hexo server 和 octopress 的 rake preview 命令类似,会自动监听本地文件修改,但是如果修改的是 _config.yml 的话就需要重新 hexo generate 才能看到效果。

发布

1
hexo deploy

迁移完毕,浏览器中试一下,原先的 url 都能正常访问。

整体感受

hexo 给我的第一感受就是简单,不像 octopress 一样,hexo init 后生成的文件非常少,一眼看过去清除明了。在任何一台机器上只需要 npm install -g hexo 之后就能使用,不像 ruby 一样还需要搞 rbenv 并编译 ruby 之类的事情。另外一个感受就是速度快,不管是 generate 还是 deploy 都比 octopress 快了不少。整体来说 hexo 是一个非常值得一试的博客工具。

- FIN -

异步、promise 与缓存

异步操作缓存

在 Node 开发过程中要常常和异步调用打交道,例如发送 http 请求、读取文件内容等。有时我们需要将这些异步 I/O 操作的结果缓存下来,使得程序运行的速度更快,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var fileContent = null;
function readFile(callback) {
    if (fileContent !== null) {
        process.nextTick(function () {
            callback(null, fileContent);
        });
        return;
    }

    fs.readFile(FILE_PATH, function (err, content) {
        if (err) {
            callback(err);
            return;
        }

        fileContent = content;
        callback(null, content);
    });
}

上面的代码用 fileContent 变量做了一个简单的内存缓存,在 readFile 函数中,如果发现缓存中存在内容,则跳过文件读取操作。

这是个非常简单的缓存应用案例,我们可以将代码中的缓存逻辑抽出来,与业务逻辑分离,成为一个通用的缓存方法 cacheAsync:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function cacheAsync(logical) {
    var cache = null;
    return function (callback) {
        if (cache) {
            process.nextTick(function () {
                callback(null, cache);
            });
            return;
        }

        logical(function (err, result) {
            if (err) {
                logical(err);
                return;
            }

            cache = result;
            callback(null, result);
        });
    };
}

var readFile = cacheAsync(function (callback) {
    fs.readFile(FILE_PATH, callback);
});

readFile(function (err, content) {
    if (err) {
        throw err;
        return;
    }

    console.log('file:', content);
});

使用 cacheAsync 方法可以很容易的给一个异步回调函数加上缓存。

promise

promise 是和上面代码中示范的 callback 异步回调方式截然不同的另一种异步范式。promise 范式下的异步函数不再接收一个 callback 函数,而是返回一个 promise 对象。promise 对象通过 then 方法来绑定回调函数,通过 catch 方法绑定错误处理函数。

nodejs 目前有很多个 promise 异步范式的库,最流行的是 q。这里以 q 为例,示范一个简单的 promise 异步范式:

1
2
3
4
5
6
7
8
9
10
11
12
13
var readFile = function () {
    // q.nfcall 使一个 callback 风格的异步函数返回一个 promise 对象
    return q.nfcall(fs.readFile, FILE_PATH);
};

// 下面这行看起来就和同步调用一样
var fileContent = readFile();

fileContent.then(function (content) {
    console.log('file:', content);
}).catch(function (err) {
    console.log(err);
});

看起来是不是感觉代码更复杂了?除了能看到异常逻辑和正常流程被分离了以外,似乎没有太多好处?那么看一个更复杂的例子,这个例子尝试从两个数据源读取用户信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
function getUserSync() {
    var user = fetchUserFromWeiboSync();

    if (!user) {
        user = fetchUserFromRenrenSync();
    }

    if (!user) {
        throw new Error(404);
    }

    return user;
}

callback 回调版本,一个常见的 callback hell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getUserAsync(callback) {
    fetchUserFromWeiboAsync(function (err) {
        if (err) {
            fetchUserFromRenrenAsync(function (err, user) {
                if (err) {
                    callback(new Error(404));
                } else {
                    callback(null, user);
                }
            });
        } else {
            callback(null, user);
        }
    })
}

promise 版本,代码之少令人惊讶:

1
2
3
function getUserQ() {
    return fetchUserFromWeiboQ().catch(fetchUserFromRenrenQ);
}

promise 与缓存

给一个 promise 范式下的异步函数加缓存是一件非常轻松的事情,因为你不需要处理任何的异常流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var q = require('q');

function cacheQ(logical) {
    var cache = null;

    return function () {
        if (cache !== null) {
            return q(cache);
        }

        // 这里不需要再处理任何的异常,缓存方法和程序逻辑进一步解耦
        return logical().then(function (result) {
            cache = result;
            // 这里必须 return,因为这里的返回值会作为参数传递给下一个 then 方法绑定的回调函数
            return result;
        });
    };
}

var readFile = cacheQ(function () {
    // q.nfcall 使一个 callback 风格的异步函数返回一个 promise 对象
    return q.nfcall(fs.readFile, FILE_PATH);
});

更灵活的缓存

在实际应用中,一个缓存函数还应当支持以下特性:

  1. 缓存应该是一个 key-value 存储,而不是只能缓存一个值
  2. 避免使用一个简单的内存对象来做缓存,更好的选择有 lru-cache、redis 以及 memcached 等
  3. 缓存函数应该对逻辑函数透明,即逻辑函数是否被缓存,不应该影响整个程序的执行结果

以下给一个实际项目中应用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var q = require('q');
var LRU = require('lru-cache');

function cacheQ(logical, key) {
    var cache = new LRU({
        max: 100, // 最大缓存数量,防止内存泄露
        maxAge: 60000 // 一分钟过期
    });

    return function () {
        var args = arguments;
        key = typeof key === 'function' ? key.apply(this, args) : '__default__';

        // 这里支持 key 作为一个 promise 对象,所以需要用 q 来包装这个对象
        return q(key).then(function (key) {
            if (cache.has(key)) {
                return cache.get(key);
            }

            return logical.apply(this, args).then(function (result) {
                cache.set(key, result);
                return result;
            });
        }.bind(this)); // 注意 this 的传递
    };
}

var readFileCached = cacheQ(function (path) {
    return q.nfcall(fs.readFile, path);
}, function (path) {
    return path;
});

- FIN -