论golang Timer Reset方法使用的正确姿势

2016年,Go语言Tiobe编程语言排行榜上位次的大幅蹿升(2016年12月份Tiobe榜单:go位列第16位,Rating值:1.939%)。与此同时,我们也能切身感受到Go语言在世界范围蓬勃发展,其在中国地界儿上的发展更是尤为猛烈^0^:For gopher们的job变多了、网上关于Go的资料也大有“汗牛充栋”之势。作为职业Gopher^0^,要为这个生态添砖加瓦,就要多思考、多总结,关键还要做到“遇到了问题,就要说出来,给出你的见解”。每篇文章都有自己的切入角度和关注重点,因此Gopher们也无需过于担忧资料的“重复”。

这次,我来说说在使用Go标准库中Timer的Reset方法时遇到的问题。

一、关于Timer原理的一些说明

网络编程方面,从用户视角看,golang表象上是一种“阻塞式”网络编程范式,而支撑这种“阻塞式”范式的则是内置于go编译后的executable file中的runtime。runtime利用网络IO多路复用机制实现多个进行网络通信的goroutine的合理调度。goroutine中的执行函数则相当于你在传统C编程中传给epoll机制的回调函数。golang一定层度上消除了在这方面“回调”这种“逆向思维”给你带来的心智负担,简化了网络编程的复杂性。

但长时间“阻塞”显然不能满足大多数业务情景,因此还需要一定的超时机制。比如:在socket层面,我们通过显式设置net.Dialer的Timeout或使用SetReadDeadline、SetWriteDeadline以及SetDeadline;在应用层协议,比如http,client通过设置timeout参数,server通过TimeoutHandler来限制操作的time limit。这些timeout机制,有些是通过runtime的网络多路复用的timeout机制实现,有些则是通过Timer实现的。

标准库中的Timer让用户可以定义自己的超时逻辑,尤其是在应对select处理多个channel的超时、单channel读写的超时等情形时尤为方便。

1、Timer的创建

Timer是一次性的时间触发事件,这点与Ticker不同,后者则是按一定时间间隔持续触发时间事件。Timer常见的使用场景如下:

场景1:

t := time.AfterFunc(d, f)

场景2:

select {
    case m := <-c:
       handle(m)
    case <-time.After(5 * time.Minute):
       fmt.Println("timed out")
}

或:
t := time.NewTimer(5 * time.Minute)
select {
    case m := <-c:
       handle(m)
    case <-t.C:
       fmt.Println("timed out")
}

从这两个场景中,我们可以看到Timer三种创建姿势:

t:= time.NewTimer(d)
t:= time.AfterFunc(d, f)
c:= time.After(d)

虽然姿势不同,但背后的原理则是相通的。

Timer有三个要素:

* 定时时间:也就是那个d
* 触发动作:也就是那个f
* 时间channel: 也就是t.C

对于AfterFunc这种创建方式而言,Timer就是在超时(timer expire)后,执行函数f,此种情况下:时间channel无用。

//$GOROOT/src/time/sleep.go

func AfterFunc(d Duration, f func()) *Timer {
    t := &Timer{
        r: runtimeTimer{
            when: when(d),
            f:    goFunc,
            arg:  f,
        },
    }
    startTimer(&t.r)
    return t
}

func goFunc(arg interface{}, seq uintptr) {
    go arg.(func())()
}

注意:从AfterFunc源码可以看到,外面传入的f参数并非直接赋值给了内部的f,而是作为wrapper function:goFunc的arg传入的。而goFunc则是启动了一个新的goroutine来执行那个外部传入的f。这是因为timer expire对应的事件处理函数的执行是在go runtime内唯一的timer events maintenance goroutine: timerproc中。为了不block timerproc的执行,必须启动一个新的goroutine。

//$GOROOT/src/runtime/time.go
func timerproc() {
    timers.gp = getg()
    for {
        lock(&timers.lock)
        ... ...
            f := t.f
            arg := t.arg
            seq := t.seq
            unlock(&timers.lock)
            if raceenabled {
                raceacquire(unsafe.Pointer(t))
            }
            f(arg, seq)
            lock(&timers.lock)
        }
        ... ...
        unlock(&timers.lock)
   }
}

而对于NewTimer和After这两种创建方法,则是Timer在超时(timer expire)后,执行一个标准库中内置的函数:sendTime。sendTime将当前当前事件send到timer的时间Channel中,那么说这个动作不会阻塞到timerproc的执行么?答案肯定是不会的,其原因就在下面代码中:

//$GOROOT/src/time/sleep.go
func NewTimer(d Duration) *Timer {
    c := make(chan Time, 1)
    t := &Timer{
        C: c,
        ... ...
    }
    ... ...
    return t
}

func sendTime(c interface{}, seq uintptr) {
    // Non-blocking send of time on c.
    // Used in NewTimer, it cannot block anyway (buffer).
    // Used in NewTicker, dropping sends on the floor is
    // the desired behavior when the reader gets behind,
    // because the sends are periodic.
    select {
    case c.(chan Time) <- Now():
    default:
    }
}

我们看到NewTimer中创建了一个buffered channel,size = 1。正常情况下,当timer expire,t.C无论是否有goroutine在read,sendTime都可以non-block的将当前时间发送到C中;同时,我们看到sendTime还加了双保险:通过一个select判断c buffer是否已满,一旦满了,直接退出,依然不会block,这种情况在reuse active timer时可能会遇到。

2、Timer的资源释放

很多Go初学者在使用Timer时都会担忧Timer的创建会占用系统资源,比如:

有人会认为:创建一个Timer后,runtime会创建一个单独的Goroutine去计时并在expire后发送当前时间到channel里。
还有人认为:创建一个timer后,runtime会申请一个os级别的定时器资源去完成计时工作。

实际情况并不是这样。恰好近期gopheracademy blog发布了一篇 《How Do They Do It: Timers in Go》,通过对timer源码的分析,讲述了timer的原理,大家可以看看。

go runtime实际上仅仅是启动了一个单独的goroutine,运行timerproc函数,维护了一个”最小堆”,定期wake up后,读取堆顶的timer,执行timer对应的f函数,并移除该timer element。创建一个Timer实则就是在这个最小堆中添加一个element,Stop一个timer,则是从堆中删除对应的element。

同时,从上面的两个Timer常见的使用场景中代码来看,我们并没有显式的去释放什么。从上一节我们可以看到,Timer在创建后可能占用的资源还包括:

  • 0或一个Channel
  • 0或一个Goroutine

这些资源都会在timer使用后被GC回收。

综上,作为Timer的使用者,我们要做的就是尽量减少在使用Timer时对最小堆管理goroutine和GC的压力即可,即:及时调用timer的Stop方法从最小堆删除timer element(如果timer 没有expire)以及reuse active timer。

BTW,这里还有一篇讨论go Timer精度的文章,大家可以拜读一下。

二、Reset到底存在什么问题?

铺垫了这么多,主要还是为了说明Reset的使用问题。什么问题呢?我们来看下面的例子。这些例子主要是为了说明Reset问题,现实中很可能大家都不这么写代码逻辑。当前环境:go version go1.7 darwin/amd64。

1、example1

我们的第一个example如下:

//example1.go

func main() {
    c := make(chan bool)

    go func() {
        for i := 0; i < 5; i++ {
            time.Sleep(time.Second * 1)
            c <- false
        }

        time.Sleep(time.Second * 1)
        c <- true
    }()

    go func() {
        for {
            // try to read from channel, block at most 5s.
            // if timeout, print time event and go on loop.
            // if read a message which is not the type we want(we want true, not false),
            // retry to read.
            timer := time.NewTimer(time.Second * 5)
            defer timer.Stop()
            select {
            case b := <-c:
                if b == false {
                    fmt.Println(time.Now(), ":recv false. continue")
                    continue
                }
                //we want true, not false
                fmt.Println(time.Now(), ":recv true. return")
                return
            case <-timer.C:
                fmt.Println(time.Now(), ":timer expired")
                continue
            }
        }
    }()

    //to avoid that all goroutine blocks.
    var s string
    fmt.Scanln(&s)
}

example1.go的逻辑大致就是 一个consumer goroutine试图从一个channel里读出true,如果读出false或timer expire,那么继续try to read from the channel。这里我们每次循环都创建一个timer,并在go routine结束后Stop该timer。另外一个producer goroutine则负责生产消息,并发送到channel中。consumer中实际发生的行为取决于producer goroutine的发送行为。

example1.go执行的结果如下:

$go run example1.go
2016-12-21 14:52:18.657711862 +0800 CST :recv false. continue
2016-12-21 14:52:19.659328152 +0800 CST :recv false. continue
2016-12-21 14:52:20.661031612 +0800 CST :recv false. continue
2016-12-21 14:52:21.662696502 +0800 CST :recv false. continue
2016-12-21 14:52:22.663531677 +0800 CST :recv false. continue
2016-12-21 14:52:23.665210387 +0800 CST :recv true. return

输出如预期。但在这个过程中,我们新创建了6个Timer。

2、example2

如果我们不想重复创建这么多Timer实例,而是reuse现有的Timer实例,那么我们就要用到Timer的Reset方法,见下面example2.go,考虑篇幅,这里仅列出consumer routine代码,其他保持不变:

//example2.go
.... ...
// consumer routine
    go func() {
        // try to read from channel, block at most 5s.
        // if timeout, print time event and go on loop.
        // if read a message which is not the type we want(we want true, not false),
        // retry to read.
        timer := time.NewTimer(time.Second * 5)
        for {
            // timer is active , not fired, stop always returns true, no problems occurs.
            if !timer.Stop() {
                <-timer.C
            }
            timer.Reset(time.Second * 5)
            select {
            case b := <-c:
                if b == false {
                    fmt.Println(time.Now(), ":recv false. continue")
                    continue
                }
                //we want true, not false
                fmt.Println(time.Now(), ":recv true. return")
                return
            case <-timer.C:
                fmt.Println(time.Now(), ":timer expired")
                continue
            }
        }
    }()
... ...

按照go 1.7 doc中关于Reset使用的建议:

To reuse an active timer, always call its Stop method first and—if it had expired—drain the value from its channel. For example:

if !t.Stop() {
        <-t.C
}
t.Reset(d)

我们改造了example1,形成example2的代码。由于producer行为并未变更,实际example2执行时,每次循环Timer在被Reset之前都没有expire,也没有fire a time to channel,因此timer.Stop的调用均返回true,即成功将timer从“最小堆”中移除。example2的执行结果如下:

$go run example2.go
2016-12-21 15:10:54.257733597 +0800 CST :recv false. continue
2016-12-21 15:10:55.259349877 +0800 CST :recv false. continue
2016-12-21 15:10:56.261039127 +0800 CST :recv false. continue
2016-12-21 15:10:57.262770422 +0800 CST :recv false. continue
2016-12-21 15:10:58.264534647 +0800 CST :recv false. continue
2016-12-21 15:10:59.265680422 +0800 CST :recv true. return

和example1并无二致。

3、example3

现在producer routine的发送行为发生了变更:从以前每隔1s发送一次数据变成了每隔7s发送一次数据,而consumer routine不变:

//example3.go

//producer routine
    go func() {
        for i := 0; i < 10; i++ {
            time.Sleep(time.Second * 7)
            c <- false
        }

        time.Sleep(time.Second * 7)
        c <- true
    }()

我们来看看example3.go的执行结果:

$go run example3.go
2016-12-21 15:14:32.764410922 +0800 CST :timer expired

程序hang住了。你能猜到在哪里hang住的吗?对,就是在drain t.C的时候hang住了:

           // timer may be not active and may not fired
            if !timer.Stop() {
                <-timer.C //drain from the channel
            }
            timer.Reset(time.Second * 5)

producer的发送行为发生了变化,Comsumer routine在收到第一个数据前有了一次time expire的事件,for loop回到loop的开始端。这时timer.Stop函数返回的不再是true,而是false,因为timer已经expire,最小堆中已经不包含该timer了,Stop在最小堆中找不到该timer,返回false。于是example3代码尝试抽干(drain)timer.C中的数据。但timer.C中此时并没有数据,于是routine block在channel recv上了。

在Go 1.8以前版本中,很多人遇到了类似的问题,并提出issue,比如:

time: Timer.Reset is not possible to use correctly #14038

不过go team认为这还是文档中对Reset的使用描述不够充分导致的,于是在Go 1.8中对Reset方法的文档做了补充Go 1.8 beta2中Reset方法的文档改为了:

Resetting a timer must take care not to race with the send into t.C that happens when the current timer expires. If a program has already received a value from t.C, the timer is known to have expired, and t.Reset can be used directly. If a program has not yet received a value from t.C, however, the timer must be stopped and—if Stop reports that the timer expired before being stopped—the channel explicitly drained:

if !t.Stop() {
        <-t.C
}
t.Reset(d)

大致意思是:如果明确time已经expired,并且t.C已经被取空,那么可以直接使用Reset;如果程序之前没有从t.C中读取过值,这时需要首先调用Stop(),如果返回true,说明timer还没有expire,stop成功删除timer,可直接reset;如果返回false,说明stop前已经expire,需要显式drain channel。

4、example4

我们的example3就是“time已经expired,并且t.C已经被取空,那么可以直接使用Reset ”这第一种情况,我们应该直接reset,而不用显式drain channel。如何将这两种情形合二为一,很直接的想法就是增加一个开关变量isChannelDrained,标识timer.C是否已经被取空,如果取空,则直接调用Reset。如果没有,则drain Channel。

增加一个变量总是麻烦的,RussCox也给出一个未经详尽验证的方法,我们来看看用这种方法改造的example4.go:

//example4.go

//consumer
    go func() {
        // try to read from channel, block at most 5s.
        // if timeout, print time event and go on loop.
        // if read a message which is not the type we want(we want true, not false),
        // retry to read.
        timer := time.NewTimer(time.Second * 5)
        for {
            // timer may be not active, and fired
            if !timer.Stop() {
                select {
                case <-timer.C: //try to drain from the channel
                default:
                }
            }
            timer.Reset(time.Second * 5)
            select {
            case b := <-c:
                if b == false {
                    fmt.Println(time.Now(), ":recv false. continue")
                    continue
                }
                //we want true, not false
                fmt.Println(time.Now(), ":recv true. return")
                return
            case <-timer.C:
                fmt.Println(time.Now(), ":timer expired")
                continue
            }
        }
    }()

执行结果:

$go run example4.go
2016-12-21 15:38:16.704647957 +0800 CST :timer expired
2016-12-21 15:38:18.703107177 +0800 CST :recv false. continue
2016-12-21 15:38:23.706665507 +0800 CST :timer expired
2016-12-21 15:38:25.705314522 +0800 CST :recv false. continue
2016-12-21 15:38:30.70900638 +0800 CST :timer expired
2016-12-21 15:38:32.707482917 +0800 CST :recv false. continue
2016-12-21 15:38:37.711260142 +0800 CST :timer expired
2016-12-21 15:38:39.709668705 +0800 CST :recv false. continue
2016-12-21 15:38:44.71337522 +0800 CST :timer expired
2016-12-21 15:38:46.710880007 +0800 CST :recv false. continue
2016-12-21 15:38:51.713813305 +0800 CST :timer expired
2016-12-21 15:38:53.713063822 +0800 CST :recv true. return

我们利用一个select来包裹channel drain,这样无论channel中是否有数据,drain都不会阻塞住。看似问题解决了。

5、竞争条件

如果你看过timerproc的代码,你会发现其中的这样一段代码:

// go1.7
// $GOROOT/src/runtime/time.go
            f := t.f
            arg := t.arg
            seq := t.seq
            unlock(&timers.lock)
            if raceenabled {
                raceacquire(unsafe.Pointer(t))
            }
            f(arg, seq)
            lock(&timers.lock)

我们看到在timerproc执行f(arg, seq)这个函数前,timerproc unlock了timers.lock,也就是说f的执行并没有在锁内。

前面说过,f的执行是什么?

对于AfterFunc来说,就是启动一个goroutine,并在这个新goroutine中执行用户传入的函数;
对于After和NewTimer这种创建姿势创建的timer而言,f的执行就是sendTime的执行,也就是向t.C中send 当前时间。

注意:这时候timer expire过程中sendTime的执行与“drain channel”是分别在两个goroutine中执行的,谁先谁后,完全依靠runtime调度。于是example4.go中的看似没有问题的代码,也可能存在问题(当然需要时间粒度足够小,比如ms级的Timer)。

如果sendTime的执行发生在drain channel执行前,那么就是example4.go中的执行结果:Stop返回false(因为timer已经expire了),显式drain channel会将数据读出,后续Reset后,timer正常执行;
如果sendTime的执行发生在drain channel执行后,那么问题就来了,虽然Stop返回false(因为timer已经expire),但drain channel并没有读出任何数据。之后,sendTime将数据发到channel中。timer Reset后的Timer中的Channel实际上已经有了数据,于是当进入下面的select执行体时,”case <-timer.C:”瞬间返回,触发了timer事件,没有启动超时等待的作用。

这也是issue:*time: Timer.C can still trigger even after Timer.Reset is called #11513中问到的问题。

go官方文档中对此也有描述:

Note that it is not possible to use Reset's return value correctly, as there is a race condition between draining the channel and the new timer expiring. Reset should always be invoked on stopped or expired channels, as described above. The return value exists to preserve compatibility with existing programs.

三、真的有Reset方法的正确使用姿势吗?

综合上述例子和分析,Reset的使用似乎没有理想的方案,但一般来说,在特定业务逻辑下,Reset还是可以正常工作的,就如example4那样。即便出现问题,如果了解了Reset背后的原理,问题解决起来也是会很快很准的。

文中的相关代码可以在这里下载

四、参考资料

Golang官方有关Timer的issue list:

runtime: special case timer channels #8898
time:timer stop ,how to use? #14947
time: document proper usage of Timer.Stop #14383
*time: Timer.Reset is not possible to use correctly #14038
Time.After doesn’t release memory #15781
runtime: timerproc does not get to run under load #15706
time: time.After uses memory until duration times out #15698
time:timer stop panic #14946
*time: Timer.C can still trigger even after Timer.Reset is called #11513
time: Timer.Stop documentation incorrect for Timer returned by AfterFunc #17600

相关资料:

  1. go中的定时器timer
  2. Go内部实现之timer
  3. Go定时器
  4. How Do They Do It: Timers in Go
  5. timer在go可以有多精确

给女儿搭建一个博客站点

时光荏苒。转眼间女儿已经成为一名小学生了,依稀还记得当年果果呱呱坠地的情景,独自回味,感慨万千。

果果3岁前,都是我来记录她的生活点滴和成长历程,那个时候她是我们生活舞台的主角。3岁后,果果学会了说话,上了幼儿园,开始学习各种知识、技能以及各种才艺。尤其是在幼儿园中班之后,她学会了写字、组词、造句和写日记,果果完全可以自己用文字来表达自己了! 我觉得是时候让她自己来记录她的成长历程了,我和她妈妈只是辅助和指导就好了。这种想法日益迫切,尤其是果果今年上了小学后,果果的成长更快了。我觉得迫切需要给她一个平台去表达她自己和记录她的成长。传统手段不能满足需求,于是我就想到给她搭建了一个博客站点,辅助她用网络文字、图片的形式记录自6岁上学之后的成长历程。于是这个周末就花了些时间,给女儿搭了一个博客站点。

下面以“流水账”的形式,记录一下这个站点的搭建过程,也许能给和我有同样需求的家长们带来一些帮助^_^。

一、选型和准备工作

博客站点,我首选静态页面的。静态页面,又要快速搭建,我首选github page。github page一般情况下在国内访问相对较为稳定,访问速度也不错,ping延迟一般在100多ms,比我独立购买的Digital Ocean的主机的延迟低很多。还有另外一个原因就是市面上几乎所有主流静态页面生成工具都对github pages有着不错的支持。由于采用静态页面,即便将来迁移到VPS,也几乎是无缝的。于是给果果在github上申请了账号

用与搭建博客、个人站点的静态页面生成工具很多,比如:jekylloctopresshexo以及hugo,用哪个呢?作为Gopher,我首选hugo。接下来,我们来看看用hugo是否能搭建出满足我们需求的基于github page的博客站点吧。

hugo的安装参考hugo github主页上的说明即可。由于hugo import了很多第三方package,有些package可能在墙外,因此配置上加速器是更好的、更快的^_^。

二、基于hugo搭建博客站点

去年曾写过一篇《使用Hugo搭建静态站点(http://tonybai.com/2015/09/23/intro-of-gohugo/)》,讲述如何通过hugo这个golang开发的工具搭建一个属于自己的静态站点(static websites)。不过那篇文章并没有谈到hugo如何与github page结合。

hugo官方文档中,对如何使用hugo创建基于github page站点有着较为详尽的描述,这是由一位名为Spencer Lyon的外国开发者贡献的文章,并且Spencer Lyon给出hugo github page的工程template: hugo-gh-blog。我这里就直接使用了该工程模板,并基于hugo_gh_blog做一些定制化修改,比如“汉化”之类的。

下面是详细的步骤:

1、clone hugo_gh_blog

我们首先将Spencer Lyon的hugo_gh_blog代码库clone到本地,这是我们博客搭建的基础:

$mkdir GuoGuoBlog
$cd GuoGuoBlog
$git clone https://github.com/spencerlyon2/hugo_gh_blog.git
Cloning into 'hugo_gh_blog'...
remote: Counting objects: 489, done.
remote: Total 489 (delta 0), reused 0 (delta 0), pack-reused 489
Receiving objects: 100% (489/489), 84.50 KiB | 24.00 KiB/s, done.
Resolving deltas: 100% (232/232), done.
Checking connectivity... done.
$cd hugo_gh_blog/
$ls
LICENSE        README.md    config.yaml    content/    deploy.sh*    static/        themes/

2、编辑config.yaml和本地调试

进入hugo_gh_blog目录,编辑config.yaml,设置站点的一些元数据:

---
contentdir: "content"
layoutdir: "layouts"
publishdir: "public"
indexes:
  category: "categories"
baseurl: "http://baisibei.github.io"
title: "果果的成长历程"
canonifyurls: true
theme: "Lanyon"
...

接下来,我们来生成我们的静态博客页面:

$hugo
0 draft content
0 future content
2 pages created
0 paginator pages created
2 categories created
in 48 ms

hugo将创建public目录,并将生成的页面放入该目录:

$ls public
404.html    categories/    css/        favicon.ico    img/        index.html    index.xml    posts/        sitemap.xml

public/index.html就是站点首页。

我们在本地可以启动hugo server,并查看生成的站点情况:

$hugo server -t Lanyon
0 draft content
0 future content
2 pages created
0 paginator pages created
2 categories created
in 54 ms
Serving pages from /Users/tony/GuoGuoBlog/hugo_gh_blog/public
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)

打开浏览器,输入localhost:1313。不出意外,你就可以看到类似下面的站点:

img{512x368}

3、创建github page repository

默认情况,github账号xxxx对应的github page repository是xxxx.github.io。于是我在女儿的github账号下创建public repository:baisibei.github.io。

接下来,我们需要将上步中生成的静态页面push到baisibei.github.io这个repository中。在本地进入到hugo_gh_blog/public目录下,执行:

$git init
$git add -A
$git commit -m"initial commit" .
$git remote add origin https//github.com/baisibei.github.io.git
$git push -u origin master

如果你是用自己的github账号替孩子提交,那么请在该repository下设置collaborator。

push一旦成功后,你就可以直接访问:https://xxxx.github.io查看站点页面了。我这里要访问的是baisibei.github.io。

4、样式问题

问题出现了。在本地样式良好的首页,一旦push到github page上,再用浏览器(chrome)打开,我发现样式全部丢失了,首页被render为“全文字”版本。我一开始怀疑css文件路径不对或无法访问到某个css文件,通过“显示网页源代码”,单独试着访问所有css文件,发现这些文件都是可访问的。还有一个现象是:通过mac safari浏览器、手机上的ucweb、微信内置浏览器浏览,均没有样式问题,显示一切正常(Firefox、IE也均有问题)。将hugo_gh_blog放在我的VPS上,并用hugo作为web server,任何浏览器访问都是没有问题的。

针对这个问题,谷歌和度娘了半天,也没有解决掉。于是我有了换工具的想法。在搜索其他工具资料的过程中,我发现了基于hexo的maupassant theme!没错,就是那个和我目前博客同源的主题:maupassant。这个主题采用响应式的设计,对不同屏幕的访问均有很好的适配。

之前我的博客为了适应智能终端的浏览,采用了WPtouch插件,效果差强人意。这次我特地停用了该插件,直接用手机访问我的博客,发现maupassant的显示效果是棒棒的。于是下一步,我将hugo更换为hexo,主题由Lanyon更换为maupassant。

hugo_gh_blog和baisibei.github.io依然保留在github.com上,后者的名字被rename为baisibei.github.io.using.hugo。

三、基于hexo搭建博客站点

1、安装hexo相关工具

第一次用hexo,安装hexo过程需要一些耐心:

$npm install hexo-cli -g
{耐心的等待... ...}

$which hexo
/usr/local/bin/hexo

$hexo -v
hexo-cli: 1.0.2
os: Darwin 13.1.0 darwin x64
http_parser: 2.7.0
node: 6.9.1
v8: 5.1.281.84
uv: 1.9.1
zlib: 1.2.8
ares: 1.10.1-DEV
icu: 57.1
modules: 48
openssl: 1.0.2j

2、创建blog

使用hexo init在本地创建blog repository目录:

$hexo init hexo_gh_blog
{耐心等待...}
... ...
INFO  Start blogging with Hexo!

进入hexo_gh_blog目录:

$cd hexo_gh_blog
$ls
_config.yml    node_modules/    package.json    scaffolds/    source/        themes/

没完,我们还需要install一下相关的依赖:

$npm install
{耐心等待....}

通过”hexo g”命令生成blog文件:

$hexo g
INFO  Start processing
INFO  Files loaded in 270 ms
INFO  Generated: index.html
INFO  Generated: archives/index.html
INFO  Generated: fancybox/blank.gif
INFO  Generated: fancybox/jquery.fancybox.css
INFO  Generated: fancybox/fancybox_sprite.png
INFO  Generated: fancybox/fancybox_loading.gif
INFO  Generated: fancybox/fancybox_overlay.png
INFO  Generated: fancybox/fancybox_loading@2x.gif
INFO  Generated: fancybox/jquery.fancybox.pack.js
INFO  Generated: fancybox/jquery.fancybox.js
INFO  Generated: fancybox/fancybox_sprite@2x.png
INFO  Generated: archives/2016/12/index.html
INFO  Generated: css/fonts/fontawesome-webfont.eot
INFO  Generated: css/fonts/fontawesome-webfont.svg
INFO  Generated: css/style.css
INFO  Generated: css/fonts/fontawesome-webfont.ttf
INFO  Generated: fancybox/helpers/fancybox_buttons.png
INFO  Generated: css/fonts/FontAwesome.otf
INFO  Generated: js/script.js
INFO  Generated: fancybox/helpers/jquery.fancybox-buttons.css
INFO  Generated: archives/2016/index.html
INFO  Generated: css/fonts/fontawesome-webfont.woff
INFO  Generated: fancybox/helpers/jquery.fancybox-media.js
INFO  Generated: fancybox/helpers/jquery.fancybox-buttons.js
INFO  Generated: fancybox/helpers/jquery.fancybox-thumbs.css
INFO  Generated: fancybox/helpers/jquery.fancybox-thumbs.js
INFO  Generated: css/images/banner.jpg
INFO  Generated: 2016/12/16/hello-world/index.html
INFO  28 files generated in 867 ms

$ls
_config.yml    db.json        node_modules/    package.json    public/        scaffolds/    source/        themes/

和hugo命令类似,hexo g也创建了public目录,并将站点的静态文件生成在这个目录下面。

通过hexo s可以启动一个web server,在本地查看生成的静态站点:

$hexo s
INFO  Start processing
INFO  Hexo is running at http://localhost:4000/. Press Ctrl+C to st

hexo自带的landscape theme真的是不咋好看。

3、更换主题为maupassant

先清理一下生成文件,再clone maupassant主题:

$hexo clean
INFO  Deleted database.
INFO  Deleted public folder.

$git clone https://github.com/tufu9441/maupassant-hexo themes/maupassant
Cloning into 'themes/maupassant'...
remote: Counting objects: 1310, done.
remote: Total 1310 (delta 0), reused 0 (delta 0), pack-reused 1309
Receiving objects: 100% (1310/1310), 562.88 KiB | 382.00 KiB/s, done.
Resolving deltas: 100% (747/747), done.
Checking connectivity... done.

$ls themes/
landscape/    maupassant/

编辑hexo_gh_blog/_config.yml文件,修改theme为:maupassant

//_config.xml
theme: landscape

=>

theme: maupassant

重新生成站点静态文件之前,我们还需要安装下面两个工具,否则hexo生成出来的静态页面也是不可用的:

$ npm install hexo-renderer-jade --save
$ npm install hexo-renderer-sass --save

hexo g和hexo s后,你就可以在本地:localhost:4000地址上看到生成的静态页面了:

img{512x368}

仿效上面章节中的步骤,将public目录push到baisibei.github.io repository中,看看我们上传后的站点通过公网访问是否还有“失真”现象,结果:一切正常。

4、定制站点

a) 定制hexo_gh_blog/_config.yml

这个_config.xml中的配置都是站点全局范畴的,这里仅我将我修改过的一些定制属性贴出来:

# Site
title: Amy Bai
subtitle: 果果的成长历程
description: 记录一个小女孩儿在学习、生活、家庭、情感方面的成长经历
author: Amy Bai
language: zh-CN
timezone: Asia/Shanghai
since: 2016
avatar: https://avatars0.githubusercontent.com/u/24524343?v=3&s=400

# URL
## If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/'
url: http://daughter.tonybai.com

默认情况下,maupassant主题的menu中包含rss菜单项(站点的订阅feed),对应的访问路径是/atom.xml,但要生成atom.xml,需要安装另外两个plugins:hexo-generator-feed和hexo-generator-sitemap:

在_config.xml中添加:

plugins:
- hexo-generator-feed
- hexo-generator-sitemap

安装这两个插件;

$npm install hexo-generator-feed --save
hexo-site@0.0.0 /Users/tony/GuoGuoblog/hexo_gh_blog
└── hexo-generator-feed@1.2.0

$npm install hexo-generator-sitemap --save
hexo-site@0.0.0 /Users/tony/GuoGuoblog/hexo_gh_blog
└── hexo-generator-sitemap@1.1.2

安装后,执行hexo g,会看到atom.xml的生成。不过由于hexo版本似乎与feed插件有兼容性问题,当执行hexo s时,命令报错。我暂时在_config.xml中先注释掉这两个插件,待后期看是否能解决,不过这不影响站点的主要功能。

b) about页面

在maupassant主题的menu中默认还包含了about菜单项,但在生成的站点静态页面中点击about菜单项,将返回失败页面。如何给站点添加about页面呢?

在hexo_gh_blog/source下创建about目录,进入about目录,创建index.md文件,内容诸如:

---
title: 关于我
---
我是Amy Bai,小名果果。

这样hexo g和hexo s后,你就有about页面可供访问了。

5、写post

hexo通过hexo new 命令来创建一篇post,我更喜欢简单粗暴,直接再hexo_gh_blog/source/_post创建一个xxx.md文件,这就是一篇post。post内的markdown格式和很多工具都是类似的:

以initial-post.md为例:
---
title: "第一篇(待写)"
date: "2016-12-15"
description: "第一篇博文,敬请期待^O^"
categories:
    - "日记"
    - "感悟"
---

## 标题一

第一篇文章,敬请期待

### 子标题

## 小结

^O^

hexo按md文件头中的date对post进行排序。title就是显示在文章中的标题。description是文章摘要。默认情况下,maupassant主题在首页只是展示文章摘要而不是全文。

四、域名绑定

还没有申请顶级域名下的二级域名,目前打算绑定daughter.tonybai.com这个子域名。怎么做呢?

在public目录下,创建CNAME文件,文件内容:daughter.tonybai.com。然后将文件Push到github上去。

在你的域名管理站点,创建”daughter.tonybai.com”子域名,并将其CNAME值设置为”baisibei.github.io”。生效后,打开浏览器,访问”daughter.tonybai.com”,你就可以看到你刚刚生成的新站点了。

五、小结

站点搭建好了!用各种终端访问,感觉效果还不错。post发布也很方便,如果你想自动发布,定义一下hexo deploy即可。我个人习惯手动提交,也就没这个步骤了。

接下来,把内容创作的任务就交给果果了^_^。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats