毛盾的博客

step by step.

理解闭包:把大象放进冰箱

前言

在很多高级语言中,都有闭包(closure)这个概念。刚开始接触的时候,在网上看了一些资料, 总觉得讲的很抽象,很难理解,一直也没有搞清楚。这段时间在学习一个php框架,里面也有 用到闭包。通过里面的例子,重新整理了一下思路,这里把我的心得体会记录下来。

记得很多年前有一个小品,里面有个问题是:把大象放进冰箱需要几个步骤?答案是只需要3步:

  1. 把冰箱门打开

  2. 把大象放进冰箱

  3. 把冰箱门关上

其实这个故事很像是我们在写代码处理具体的业务场景时的思考过程。面对一个复杂的业务场景 (把大象放进冰箱),我们通常都是将其进行层层分解,然后再分层实现。比如针对这个问题, 我们可以定义3个函数,分别对应上面的3个步骤,然后再按顺序调用,就可以解决了。嗯,典型 的面向过程的思维。到这里,好像跟闭包都没有什么关系。没关系,我们先把上面这个例子放一 边,来看另外一个例子。

例子:脱衣服

现在的场景是,一个人身上穿了3件衣服,从里到外分别是:内衣,带纽扣的衣服,带拉链的衣服, 他现在要去洗澡,要把身上的衣服都脱掉。很自然的,我们也会想到分步来解决问题。

  1. 脱掉最外层的衣服。衣服上面有附件拉链,需要先解开拉链,再脱掉衣服。

  2. 脱掉中间层的衣服。衣服上面有附件纽扣,需要先解开纽扣,再脱掉衣服。

  3. 脱掉里层的内衣。内衣上面没有附件,直接脱掉。

处理的过程跟上面的把大象放进冰箱的例子完全类似。但是在这个例子里面,3个步骤的每一步 都很类似,都是脱掉一件衣服,区别是每件衣服的附件不同,需要有不同的动作来处理。我们 再设想一下,如果这3件衣服都是一样的衣服,会是一个什么场景呢?这不就是一个典型的递归 问题嘛。我们需要做的就是一件一件的脱衣服,如果里面还有衣服,就继续脱下一件,直到里面 没有衣服为止。使用递归的思想,我们可以更加抽象的来思考解决问题。但是,现在的问题是这 3件衣服不一样,脱掉每件衣服的处理是有差异的。我们是不是可以用类似递归的办法来处理呢? 答案是可以的,用闭包。先把代码贴上,后面再来解释。代码是用php写的(用什么语言并不是 关键,主要目的是帮助我们理解概念)。

<?php

function constructClosure($closure, $accessories)
{
    return function()  use ($closure, $accessories)
    {
        echo "解开附件" . $accessories . ",";
        echo "脱掉衣服\n";
        $closure();
    };
}

$accessories = array(
                              "拉链",
                              "纽扣"
                             );

$takeOffUnderWear = function(){
    echo "脱掉内衣 \n";
};

$accessories = array_reverse($accessories);
$takeOffClothes = array_reduce($accessories, constructClosure,  $takeOffUnderWear);
$takeOffClothes();

?>

到这里终于跟闭包扯上关系了。那么闭包到底是什么?最简单的解释,闭包是匿名函数。例如在 上面的代码中,constructClosure函数中,return后面的部分,就是一个闭包。

一个典型的闭包

我们先来看看闭包长啥样。跟普通的函数挺像的是吧。跟普通函数的区别主要是没有函数名 (use ($closure, $accessories)这部分先不管),所以叫做匿名函数。但是仅仅只是知道闭包是 匿名函数对于我们理解闭包是远远不够的。我们再来看看,上面的代码中,闭包作为一个整体, 作为函数constructClosure的返回值,被return了。在我们以往的认识中,通常return的都是一 个变量,而这里返回的却是一个闭包。这里可以看出闭包的第一个特点,那就是闭包可以像一个 变量一样,可以作为值来传递,包括:可以作为函数返回值,可以作为函数入参(constructClosure 的入参$closure也是闭包),也可以把闭包赋值给一个变量。总之就是,我们可以把闭包当作一 个变量一样来使用。可能有人会问,既然把闭包看作是变量,那么这个变量是什么类型呢?可以把 闭包看作是一种特殊类型的变量。

我们来看一下constructClosure函数,这个函数是在干嘛呢?这个函数有2个入参,其中一个是 闭包,函数的返回值也是一个闭包。也就是说,这个函数是通过一个闭包(入参),形成了一个 新的闭包(返回值)。这不是正好符合迭代的特点嘛。而上面的代码也的确是在做迭代。

$takeOffClothes = array_reduce($accessories, constructClosure,  $takeOffUnderWear);

这里做了2次迭代。(关于array_reduce函数,可以参考php的手册)

第一次迭代

constructClosure的入参,$closure = $takeOffUnderWear,$accessories = “纽扣"(注意数组 $accessories经过了反转,所以数组的第一个值是"纽扣")。传入的初始的闭包$takeOffUnderWear, 做了什么呢?我们看它的函数体,很简单,就是把内衣脱掉。

$takeOffUnderWear = function(){
    echo "脱掉内衣 \n";
};

经过第一次迭代,形成了一个新的闭包,我们可以把这个新的闭包用变量$closure1来表示(注意 上面提到的闭包的第一个特点,可以当作变量来使用)。那么这个新的闭包做了什么呢?我们看它 的函数体部分

function()  use ($closure, $accessories)
{
    echo "解开附件" . $accessories . ",";
    echo "脱掉衣服\n";
    $closure();
};

函数体里面用到了函数体外部的两个变量$closure, $accessories(注意这两个变量并非是闭包的 入参,在这里闭包的入参为空)。这里可以看出闭包的第二个特点,也就是闭包可以使用作用域外 的变量(php里面使用关键字use来继承外部的变量,其他语言的语法可能有点不同)。这个新 形成的闭包$closure1执行的动作包括:解开纽扣(继承自外部变量$accessories的值是纽扣), 脱掉衣服,然后再调用初始的闭包$takeOffUnderWear(因为闭包本质是匿名函数,所以我们可以 调用它)。可以看到,因为可以继承外部变量,所以闭包的行为(函数体部分)可以根据外部变量 的不同而产生差异。

第二次迭代

constructClosure的入参,$closure = $closure1(第一次迭代的结果),$accessories = “拉链"。 又形成了一个新的闭包,这个闭包被赋值给了变量$takeOffClothes。这个闭包执行的动作包括: 解开 拉链(继承自外部变量$accessories的值是拉链),脱掉衣服,再调用上一次迭代形成的闭包 $closure1。可以看到,这次形成的闭包其行为又有了不同。

我们可以这么来理解这两次迭代。一开始,我们需要处理的场景很简单,只穿了内衣,只需要脱掉 这一件衣服。我们用初始的闭包$takeOffUnderWear就可以处理。然后,问题变复杂了一点,外面 又套了一件带纽扣的衣服,我们需要脱掉2件衣服。这时,我们可以在初始闭包的基础上,再套上一 层,形成一个新的闭包。这对应了上面的第一次迭代。再后来,问题又复杂了一点,外面又套上了 一件带拉链的衣服,我们需要脱掉3件衣服。这时,我们又可以在上次的闭包基础上,再套上一层, 形成一个更大的闭包。这对应了上面的第二次迭代。

闭包的迭代

可以看到,一个比较复杂的处理过程,我们通过闭包的迭代进行了抽象和简化,而闭包的两个特点 可以支持我们进行这样的抽象和简化。最终形成的闭包$takeOffClothes就好像是从里到外穿上3件 不同的衣服,描述好了我们需要解决的场景。

$takeOffClothes = array_reduce($accessories, constructClosure,  $takeOffUnderWear);

上面的语句,我们只是得到了一个闭包,相当于得到了一个解决方案,但是我们只是得到了方案, 还没有解决问题。再想想闭包的本质是什么?对,匿名函数,函数就需要调用执行。最后我们需 要调用一下最终形成的闭包$takeOffClothes,这样就大功告成了。

运行结果如下。

运行结果

通过这个例子,我们再来重新认识一下闭包这个词。闭包是匿名函数,包的是什么呢?包的是函数体 部分,也就是代码部分。除此之外呢,还有继承自作用域外的变量。当形成闭包的时候(“穿衣服”), 闭包函数体部分的这些作用域外的变量,继承自外部。在这之后,这些作用域外的变量就再也不可见 了。当我们执行闭包的时候(“脱衣服”),这些作用域外的变量既不是闭包的输入,也不是输出, 就好像是函数的内部代码一样,外部是看不见的。这么看来,叫“闭包”还是比较贴切的。

例子:把大象放进冰箱

现在再回过头来看一下开头提到的把大象放进冰箱的问题。把大象放进冰箱的3个步骤看起来没有太多 的相似之处,也可以用闭包来解决吗?答案是可以。

<?php

interface MIddleware
{
    public static function handle(Closure $next);
}

class Fridge implements MIddleware
{
    public static function handle(Closure $next)
    {
        echo "把冰箱门打开\n";
        $next();
        echo "把冰箱门关上\n";
    }
}

function constructClosure($closure, $pipe)
{
    return function()  use ($closure, $pipe)
    {
        $pipe::handle($closure);
    };
}

$pipes = array("Fridge");
$putElephantIntoFridge = function() {
    echo "把大象放进冰箱\n";
};

$procedures = array_reduce($pipes, constructClosure,  $putElephantIntoFridge);
$procedures();

?>

说明一下,上面的代码中把接口MIddleware和类Fridge的函数定义为static是为了不用实例化类。 代码是模仿参考文档1中的代码。这里其实用到了设计模式中的装饰着模式,是用闭包来实现的。 运行结果如下。

运行结果

看到这里,可能不免会问,这是不是有点简单问题复杂化。把大象放进冰箱,写3个函数就好了, 需要整闭包这些复杂的东西吗?其实举这个例子,只是方便解释闭包的概念,理解一下闭包的使用。 跟传统的解决方案相比,闭包可以把解决问题的各个环节,动态组合起来,提供了一种动态的灵活 性,这是闭包的优势。但是,闭包也不是没有缺点(因为我也是刚接触闭包,对这一点没有太多 认识,可以参考其他的资料)。针对具体的场景,闭包是否是好的选择,还需要我们多积累经验, 多思考分析。

总之,闭包这个概念,一开始我是被网上的各种资料给绕晕了。感觉还是结合具体的例子,会比较 好理解一点。知乎上面有一个问题是关于设计闭包(Closure)的初衷,我觉得对于认识闭包也比较 有帮助。

参考文档

[1]Laravel框架关键技术解析 P104-P110

Ubuntu搭建pxe实现网络安装操作系统

前言

办公室有一台服务器需要安装Centos操作系统 。一开始准备用移动硬盘来安装,但是在将操作 系统镜像文件写入硬盘映像时会清除移动硬盘里的数据,所以决定使用其他方式来安装。网上 搜索了一番,发现可以配置pxe来实现网络安装Centos操作系统,于是决定试一试。

在开始之前,先简单介绍一下pxe的工作原理。pxe(Pre-boot Execution Enviroment,预启动 执行环境)是由Intel公司开发的技术,工作于Client/Server的网络模式,支持工作站通过网络从 远端服务器下载映像,并由此支持通过网络启动操作系统。在启动过程中,终端要求服务器分 配ip地址,再用tftp(trivial file transfer protocol)或mtftp(multicast trivial file transfer protocol) 协议下载pxe引导文件与所要安装操作系统的内核文件,来引导操作系统的安装。

严格来说,pxe并不是一种安装方式,而是一种引导方式。进行pxe安装的必要条件是在安装的 计算机中必须包含一个pxe支持的网卡,即网卡中必须要有pxe client。pxe协议可以使计算机通 过网络启动。此协议分为client端和server端,而pxe client则在网卡的rom中。当计算机引导时, bios把pxe client调入内存中执行,然后由pxe client将放置在远端的文件通过网络下载到本地运 行。运行pxe协议需要设置dhcp服务器和tftp服务器。dhcp服务器会给pxe client(将要安装系统 的主机)分配一个ip地址,由于是给pxe client分配ip地址,所以在配置dhcp服务器时需要增加相 应的pxe设置。此外,在pxe client的rom中,已经存在了tftp client,那么它就可以通过tftp协议 到tftp server上下载所需的文件了。

pxe的工作过程:

  1. pxe client从自己的pxe网卡启动,向本网络中的dhcp服务器获取ip

  2. dhcp服务器返回分配给客户机的ip以及pxe文件的放置位置(该文件一般是放在一台tftp服务器 上)

  3. pxe client 向本网络中的tftp服务器获取pxelinux.0文件

  4. pxe client执行pxelinux.0(类似boot loader,是一个开机管理程序)

  5. 根据pxelinux.0的执行结果,通过tftp服务器加载内核和文件系统

  6. 进入安装画面,开始进行系统安装,所需要的文件可以通过http、ftp、nfs几种方式之一获取 (本文通过http来获取)

详细工作流程,参考下面这张图(盗图一张,来源于参考文档1)

pxe工作原理

准备工作

了解了pxe的工作原理,我们大概知道需要做哪些准备工作了。这里的准备工作主要是在pxe服务 器端(我用的是一台笔记本电脑,系统是ubuntu14.04)安装相应的服务,并准备好需要的文件。

  • 关闭iptables与Selinux
  • 安装dhcp服务并进行配置
  • 安装tftp服务并进行配置
  • 安装httpd服务并进行配置
  • 准备安装树
  • 准备kick start文件

关闭iptables与Selinux

sudo ufw disable

我的环境中并没有安装Selinux,所以关闭Selinux这步略过

安装dhcp服务并进行配置

sudo apt-get install isc-dhcp-server

我们还需要将服务器的网卡设置成静态ip

sudo vi /etc/network/interfaces

添加eth0的配置

auto eth0
iface eth0 inet static
address 192.168.3.2
netmask 255.255.255.0
broadcast 192.168.3.255
gateway 192.168.3.1

之后重启网口(重启除还回口之外的网口)

sudo ifdown --exclude=lo -a && sudo ifup --exclude=lo -a

修改dhcp配置文件

sudo vi /etc/dhcp/dhcpd.conf

在末尾添加

subnet 192.168.3.0 netmask 255.255.255.0 {
    range 192.168.3.10 192.168.3.254;        #地址池范围
    option routers 192.168.3.2;              #dhcp网关地址,这里填写为本机eth0地址
    next-server 192.168.3.2;                 #tftp服务器地址
    filename "pxelinux.0";                   #指定的pxe文件
}

重启dhcp服务

sudo /etc/init.d/isc-dhcp-server restart

看看67端口是否已启用

ss -unl

安装tftp服务并进行配置

sudo apt-get install tftpd-hpa
sudo apt-get install tftp-hpa

修改配置文件

sudo vi /etc/default/tftpd-hpa

配置如下

TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/var/lib/tftpboot"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="--secure"
RUN_DAEMON="yes"
OPTION="-l -s /var/lib/tftpboot"

重启tftp服务

sudo /etc/init.d/tftpd-hpa start

看看69端口是否已启用

ss -unl

此外,还需要在tftp目录/var/lib/tftpboot下准备好需要的文件。首先挂载操作系统的镜像文件。

sudo mount -o loop CentOS-6.9-x86_64-bin-DVD1.iso /media/cdrom

进行拷贝

sudo cp /media/cdrom/images/pxeboot/vmlinuz /var/lib/tftpboot/
sudo cp /media/cdrom/images/pxeboot/initrd.img /var/lib/tftpboot/
sudo cp /media/cdrom/isolinux/boot.msg /var/lib/tftpboot/
sudo cp /media/cdrom/isolinux/splash.jpg /var/lib/tftpboot/
sudo cp /media/cdrom/isolinux/vesamenu.c32 /var/lib/tftpboot/

这里简单说明一下这几个文件

  • vmlinuz:内核文件
  • initrd.img:内核模块参数
  • vesamenu.c32:显示图形界面的程序
  • splash.jpg:图形界面背景图片
  • boot.msg:提供额外的信息,让使用者了解选项意义

图形界面显示的菜单在isolinux.cfg文件中配置。我们再新建一个pxelinux.cfg目录,将其拷贝到 该目录下,重命名为default。

sudo mkdir pxelinux.cfg
sudo cp /media/cdrom/isolinux/isolinux.cfg /var/lib/tftpboot/pxelinux.cfg/default

可以看到,该文件的每一个label都对应图形界面上的一个启动选项。

label linux
  menu label ^Install or upgrade an existing system
  menu default
  kernel vmlinuz
  append initrd=initrd.img
label vesa
  menu label Install system with ^basic video driver
  kernel vmlinuz
  append initrd=initrd.img nomodeset
label rescue
  menu label ^Rescue installed system
  kernel vmlinuz
  append initrd=initrd.img rescue
label local
  menu label Boot from ^local drive
  localboot 0xffff
label memtest86
  menu label ^Memory test
  kernel memtest
  append -

等一等,是不是还少了个最重要的文件。没错,我们还需要pxelinux.0。这个文件需要通过安装syslinux 才能获取到,然后将其拷贝到tftp目录下。

apt-get install syslinux
sudo cp /usr/lib/syslinux/pxelinux.0 /var/lib/tftpboot/

安装httpd服务并进行配置

sudo apt-get install apache2

启动httpd服务

sudo service apache2 start

查看httpd服务状态

sudo service apache2 status

准备安装树

安装树是一个目录,包括了完整的系统安装文件和相关程序。我们之前下载好的Centos镜像文件,进行 挂载后(目录为/media/cdrom)就可以作为安装树。同时,我们需要httpd服务来向外提供安装树,因 此需要把安装树放在httpd服务路径/var/www/html目录下。

sudo mount --bind /media/cdrom/ /var/www/html/centos6.9/

准备kick start文件

相信大家都有过不少安装操作系统的经验了。在安装系统的时候,安装程序会与我们进行交互,要求我们 完成很多设定,比如区域,语言,时间,用户名密码以及磁盘分区等。那么有没有可能安装程序自动完成 这些设定而不需要与我们进行交互呢?答案是肯定的,通过kick start可以实现。我们可以把我们想要的 设定预先保存在一个kick start文件中,当安装程序需要完成系统的各种设定时,它不再与我们进行交互, 而是直接读取kick start文件中的配置。ubuntu系统下,通过system-config-kickstart来生成kick start文件。

安装system-config-kickstart

sudo apt-get install system-config-kickstart

使用system-config-kickstart进行配置

system-config-kickstart &

配置界面如下

kickstart配置界面1

在安装方法中,我们选择http,http服务器地址填写为之前配置的静态ip地址,目录填写安装树的目录。

kickstart配置界面2

其余配置可以借鉴参考文档2

配置好以后,我们把这些配置保存成文件ks.cfg,放在/var/www/html/目录下。生成的ks.cfg文件内容如下

#Generated by Kickstart Configurator
#platform=x86

#System language
lang en_US
#Language modules to install
lang en_US
#System keyboard
keyboard us
#System mouse
#mouse
#System timezone
timezone Asia/Shanghai
#Root password
rootpw --iscrypted $1$Cl5Hj3DM$YK2YmnSGhWlS3Ikoll2jf.
#Initial user
#user xxk --fullname "xxk" --iscrypted --password $1$hidfUjGt$AGwC0WU3InS1HG5/kRfD41
#Reboot after installation
reboot
#Use text mode install
text
#Install OS instead of upgrade
install
#Use Web installation
url --url http://192.168.3.2/centos6.9
#System bootloader configuration
bootloader --location=mbr 
#Clear the Master Boot Record
zerombr yes
#Partition clearing information
clearpart --all --initlabel 
#Disk partitioning information
part /home --fstype ext4 --size 50000 
part / --fstype ext4 --size 400000 
part /boot --fstype ext4 --size 1000 
part swap --size 2000 
#System authorization infomation
auth  --useshadow  --enablemd5 
#Network information
network --bootproto=dhcp --device=eth0
#Firewall configuration
firewall --disabled 
#Do not configure the X Window System
skipx

需要注意的是,因为我是在ubuntu系统下生成kick start文件,而我要安装的是Centos系统,不同系统的kick start文件会有少许区别。比如我生成的ks.cfg文件中,关于鼠标的设定以及普通用户(非超级用户)的设定在 Centos下就不支持,需要把它们注释掉。

此外,我们还需要告诉安装程序ks.cfg文件的位置。修改/var/lib/tftpboot/pxelinux.cfg/default,第一个label下, append initrd=initrd.img后面增加ks=http://192.168.3.2/ks.cfg

label linux
  menu label ^Install or upgrade an existing system
  menu default
  kernel vmlinuz
  append initrd=initrd.img ks=http://192.168.3.2/ks.cfg

安装过程

  • 硬件环境
  • 修改bios
  • 开始安装

硬件环境

将pxe服务器端(一台笔记本电脑)和pxe客户端(待安装Centos的一台服务器)连接在同一个子网内。最简单的 办法是使用hub,但是由于办公室里面没有hub,只有无线路由器,于是将笔记本和服务器都用网线连接到路由器, 这样也可以保证它们在一个子网内。

现在的无线路由器都自带了dhcp的功能,按道理应该需要将路由器的dhcp关闭。但是我没有关闭路由器的dhcp服务 (因为不知道路由器admin账号的密码),也并没有影响。

修改bios

修改待安装Centos系统的服务器的bios,将启动方式改为从网络启动。(并不是所有bios都支持这种启动方式,如果 不支持,那么就不能通过网络进行安装)。

开始安装

修改bios后,重启待安装系统的服务器,出现安装界面,选择第一项,后面的整个安装过程就不需要我们干预了, 我们就可以去做别的事情了。等整个安装过程结束,系统会自动重启,我们再进入bios,把启动方式修改为从硬盘 启动,整个安装过程就结束了。如果我们有多台电脑需要安装同样的操作系统,那么我们配置好pxe服务器端以后, 只要保证待安装系统的电脑处于同一子网内,这些电脑就会自动进行下载,安装,大大减轻了工作量。

安装过程中出现的图形界面(网上找了一张Centos6.5的界面,6.9的和这类似)

图形界面

遇到的问题

  1. 其实和本文所讲的内容没有关系。我之前在办公室台式机上搭建了一个octopress环境,在那上面写博客,现在 想在笔记本上也搭一个octopress。安装好octopress和相关组件后,运行rake setup_github_pages将远端仓库origin 关联到我github上的repo。此时运行git branch -a,并不能看到我github上repo里面的两个分支。

    运行命令

    git fetch origin
    

    可以将远端仓库origin的分支同步到本地,再用命令git branch -a,就可以看到origin仓库的远端分支了。再将远端 分支与本地分支关联起来。

    git branch -u origin/master master
    git branch -u origin/source source
    

    这样就可以进行git pull,git push等操作了。

参考文档

[1]http://www.linuxidc.com/Linux/2014-11/109074.htm
[2]http://linuxme.blog.51cto.com/1850814/871839
[3]http://www.tuicool.com/articles/meM7Nb
[4]http://linux.vbird.org/linux_enterprise/0120installation.php

Octopress + Github Pages 搭建博客的过程

前言

一直以来都想找个地方把工作中遇到的问题,学习总结记录下来,但在以前的公司因为商业涉密 的原因,很多博客网站都被屏蔽,也就把这事搁置了。这段时间正好有空,网上了解了下有什么 比较适合程序员写技术博客的地方,看了别人推荐的Octopress + Github Pages来写博客 的方法,有种相见恨晚的感觉,就像一位博主说的那样,像写代码一样写博客,所有的东西都是 代码化的,一切尽在掌握中。本文参考网上的文章,记录下自己搭建的过程。

这里先简单介绍下标题中的两个东西。Github想必大家都很熟悉了,它提供一个可以托管我们 的项目和代码的一个仓储。而Github Pages呢,它是Github提供的用于介绍我们的项目或者 个人的主页,一个Github账号可以有多个项目主页,但是只能有一个个人主页,我们可以在个 人主页中存放技术文档,相当于一个技术博客(插点题外话,一开始关于Github Pages用作 个人博客这种行为是否正当网上有过争论,后来看到有了官方的介绍中是允许把Github Pages 作为博客,现在好像大家都接受了,我个人觉得用来写技术文章应该是符合Github Pages 的本意的,技术无关的东西还是放在其他的博客平台比较好)。Octopress是一个Jekyll的博 客框架,对Jekyll做了封装,使用更加方便,我们可以用它来很方便的写和上传博文。简单的 说,Github Pages提供了一个个人主页可以被用作我们的博客,而Octopress则是我们编写 和上传博文的工具。

准备工作

  • 安装git
  • 安装ruby (1.9.3以上版本)
  • Markdown编辑器,支持中文
  • 注册一个Github账号,创建博客的代码仓储

安装git

我的系统是ubuntu 14.04,64位系统,直接 apt-get install git 安装。

安装ruby

这里只介绍下ubuntu下的安装。如果直接用 apt-get install 的方式安装,会发现安装的 ruby版本并不是最新的版本。我们需要1.9.3以上的ruby版本,因此先要安装一个ruby的版本 管理工具,常用的两个工具是rbenv和RVM,我用的是rbenv。

安装版本管理工具rbenv及build插件:

cd ~
git clone git://github.com/sstephenson/rbenv.git .rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
source ~/.bash_profile

接下来安装新版本的ruby,我安装的是2.3.1版本,安装完后需要把本地的ruby版本切换到新版本

rbenv install 2.3.1
rbenv local 2.3.1
rbenv rehash

Markdown编辑器

Octopress博文采用Markdown语法来书写,因此我们需要一款好用的Markdown编辑器,我用 的是Sublime Text 3,安装过程很简单,这里就不再赘述。因为我们书写博客需要用中文,还 需要安装一个中文输入法,比如搜狗输入法linux版本。另外再安装Sublime的Markdown插件, Markdown Editing。

注册一个Github账号,创建博客的代码仓储

注册的过程就不用多介绍了。注册好之后,我们还需要创建一个仓储(repo)用来存放博文。比如 如果你的账号是username,那么你需要创建一个名为username.github.io的repo。注意在 创建repo的时候,不要为repo创建README文档,让repo保持为空,否则在后面发布博文的时候 会出现错误。

搭建过程

  • 安装Octopress及依赖
  • 关联Octopress到Github Pages
  • 基本配置
  • 开始写博客
  • 高级配置

安装Octopress及依赖

克隆Octopress

git clone git://github.com/imathis/octopress.git octopress

安装依赖

cd octopress
gem install bundler
rbenv rehash
bundle install

安装默认主题

rake install

到此,Octopress就安装好了。

关联Octopress到Github Pages

前面说到,Octopress是编写和上传博文的工具,Github Pages提供了存储博文的仓储。这 一步就是把这两者联系起来,也就是当我们用Octopress发布博文的时候,Octopress就知道 把博文上传到我们指定的仓储。

rake setup_github_pages

运行命令后,会要求输入Github Pages的repo地址,把下面的username替换成你的账户名

git@github.com:username/username.github.io.git

参考文档2中解释了这个命令的处理过程,这里简单说明一下。我们从Github上clone了 Octopress的代码,在没有执行这个命令之前,octopress目录下的分支对应的远端仓库(远端仓 库名一般默认是origin)就是octopress这个项目的repo。当我们做了修改,再提交,那么我们 就是提交到octopress这个项目的repo中。而我们的目的是想用ocotpress来写博文,并把我们 的博文提交到前面我们创建的博客的repo(名为username.github.io的repo)。因此我们需要 把octopress关联到我们自己的repo。在执行这个命令后,默认的远端仓库origin被指向了我们 创建的博客repo,同时创建了一个新的名为octopress的远端仓库用来指向octopress项目的 repo。

在octopress目录下运行命令

git remote -v

你将会看到两个远端仓库。

基本配置

现在可以对Octopress作一下简单的配置,主要是对_config.yml文件进行修改。

title: 毛盾的博客
subtitle: step by step.
author: 毛盾

开始写博客

主要是下面的几个命令

rake new_post["title"]  //在/source/_posts目录下生成博文框架,为markdown格式
rake generate           //把markdown文件转换成静态页面,在public目录下
rake preview            //在http://localhost:4000/ 预览博文

如果一切顺利,你就会看到你的博客了,此时博客里只有一篇刚刚创建的博文,内容为空。此时你 就可以用Markdown编辑器打开/source/_posts目录下的markdown文档,往里面填充内容了。 关于Markdown语法,可以参考这里。当你写完一篇博文,想要上传到Github Pages你的个人主页,就用下面的命令来发布。

rake deploy             //把public目录下的内容拷贝到_deploy目录下,上传到博客repo

此外,我们还需要提交博客的源文件

git add .
git commit -m 'comment'
git push origin source

以上就是写博客的基本流程。参考文档2中介绍了Octopress的工作原理,这里作下 简单说明。Octopress建立了两个分支,一个分支是master分支,对应_deploy目录,也就 是我们最终发布的静态页面。另一个分支是source分支,包括博文的markdown文件,以及生 成博文的插件,主题和脚本等。执行rake deploy命令,会将master分支进行提交。如果我 们要提交源文件,则需要使用git命令手动提交source分支。

高级配置

社交分享

有很多第三方库提供社交分享的功能,我用的是jiathis。

  1. 在_config.yml中,增加

     social_share: true
    
  2. 修改source/_includes/post/sharing.html,增加下面的内容,记得把所有的 \去掉

       \{\% if site.social_share \%\}
           \{\% include post/social_media.html \%\}
       \{\% endif \%\}
    
  3. 新建文件source/_includes/post/social_media.html,访问jiathis, 获取分享的代码,放入新建的文件中。最好先注册一个jiathis账号后再获取代码,这 样获取的代码会带有你的uid。

添加文章评论

有很多第三方的评论平台,这里使用多说。

  1. 打开多说主页,在主页上点击“我要安装”,之后选择一个社交工具的账号 来登录,然后开始创建站点,站点地址就填写你的博客地址username.github.io, 然后填写你的多说域名(你填写的部分就是你的多说short_name,后面会用到)。

  2. 在_config.yml中,增加

     duoshuo_comments: true
     duoshuo_short_name: yourname    //你的多说short_name
    
  3. 在source/layouts/post.html中,增加

     {% if site.duoshuo_short_name and site.duoshuo_comments == true and page.comments == true %}
      <section>
       <h1>Comments</h1>
       <div id="comments" aria-live="polite">{% include post/duoshuo.html %}</div>
      </section>
     {% endif %}
    
  4. 新建文件source/_includes/post/duoshuo.html,将下面的代码模板复制到新建的 文件中,记得把yourname替换成你自己的多说short_name。

     <!-- Duoshuo COMMENT BEGIN -->
     <div class="ds-thread"></div>
     <script type="text/javascript">
         var duoshuoQuery = {short_name: "yourname"};
         (function() {
             var ds = document.createElement('script');
             ds.type = 'text/javascript';
             ds.async = true;
             ds.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') + '//static.duoshuo.com/embed.js';
             ds.charset = 'utf-8';
             (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(ds);
         })();
     </script>
     <!-- Duoshuo COMMENT END -->
    

其他高级配置

比如配置独立域名,添加about页等,参考文档里面讲的比较详细,这里就不多介绍了。

遇到的问题

  1. 执行命令rake preview,提示Could not find a JavaScript runtime.可能是你 没有安装js运行环境,如果是ubuntu系统,直接apt-get install nodejs。

  2. 如果你在为博客创建repo的时候,repo不为空,比如添加了一个README文档,那么在执行 rake deploy时可能会报错,解决方法参考这里。

  3. 如果你修改了博文,rake generate,然后rake preview预览,发现博文没有更新,把 浏览器的缓存清理一下,再试试。

参考文档

[1]http://toughcoder.net/blog/2014/10/16/blogging-like-a-hacker-with-github-pages/ [2]http://blog.leichunfeng.com/blog/2014/11/11/use-octopress-plus-github-pages-to-setup-a-personal-blog/
[3]http://blog.devtang.com/2012/02/10/setup-blog-based-on-github/ [4]http://www.jianshu.com/p/1a117ef0e534