跳到主要内容
  1. Skills/
  2. Hugo 使用指南/

动态的hugo - 监控数据变化并重新生成页面

··字数 3538·8 分钟
有悟方法

使用 hugo 来生成静态网页,速度就像子弹一样,会飞。hugo 提供了 getJSON 和 getCSV 两个函数,用于生成页面文件时,获取外部的数据,这样就可以为页面添加外部内容。 但如果这些外部数据,按照某一种频率更新,那么 hugo 就需要相应的更新。

但是 hugo 的开发模式并不监控这些外部资源,怎么办?

有一点是需要澄清的,hugo 是静态网站生成器,它只会按照模板要求与数据,生成静态页面html 文件,当然可以与 像 vue或 react 这些构建动态网页的 js 框架结合,但模板会变成非常复杂。本文所说的动态 hugo,是指让 hugo 可以在数据变化以后重新自动生成页面文件。

在文章

中已经介绍了 getJSON、getCSV 不仅可以获取远程的外部数据,同时可以用来获取本地 hugo 工作目录下(非data目录)的数据文件。

┌──────────────┐             remote   
│hugo          │  getJSON  ┌──────────┐
│  ┌─────────┐ ◀──getCSV───┤ json,csv │
│  │ content │ │           └──────────┘
│  └─────────┘ │                       
│  ┌─────────┐ │                       
│  │  data   │ │                       
│  └─────────┘ │                       
│  ┌─────────┐ │           ┌──────────┐
│  │ static  │ │generate──▶│  public  │
│  └─────────┘ │           └──────────┘
│  ┌─────────┐ │                       
│  │ layouts │ │                       
│  └─────────┘ │                       
│  ┌─────────┐ │                       
│  │ config  │ │                       
│  └─────────┘ │                       
└──────────────┘                       

改变获取位置 #

如果把远程外部数据变成本地数据,hugo 就可以实现监控数据文件变化来重新生成页面文件。那么问题也就分解为:

  1. 远程数据同步到本地
  2. hugo 监控本地数据,然后自动重新生成页面。

这种方案,可以说是代价最小,因为 hugo 进程时刻活动着,重新生成页面时无须再另外启动,也不需要额外的管理器。唯一需要做的,就是把更新的数据送到 hugo 可以识别的目录下。

┌────────────────────────┐                      
│hugo                    │                      
│┌─────────┐  ┌─────────┐│          ┌──────────┐
││ config  │  │ content ││──rebuild▶│  public  │
│└─────────┘  └─────────┘│          └──────────┘
│             ┌─────────┐│                      
│             │ layouts ││                      
│             └─────────┘│                      
│┌─────────┐  ┌─────────┐│                      
││ static  │  │  data   ││                      
│└────▲────┘  └────▲────┘│                      
└─────│────────────│─────┘                      
      └──────┬─────┘                            
             │                                  
       ┌──────────┐                             
remote │ json,csv │                             
       └──────────┘                             

为了实现本方案,需要一种手段可以将远程外部数据同步到本地。实现这种的手段实在太多。如

  • 编写一段 shell,让其间隔一段时间,从远程下载 json 或者 csv 到本地目录。不停的循环。
  • 使用 cron 执行器,执行一段数据获取脚本
  • 使用爬虫程序抓取数据
  • 。。。

大家熟悉什么用什么。以上这些,除获取数据需要放在 hugo 的工作目录下能被读取到之外,其它均与 hugo 无关。

至此,好像结束了?

不,有几个细节需要注意,因为有个问题一直困扰着有悟,直到有悟把它弄清楚。

hugo 的变化监控与相应的机制 #

如上节图中所示,hugo 的目录下有 layouts、content、config、data、static 等五种类型的数据目录。在 hugo 监控到它们发生变化时所作出的反应是不完全一样的。

  • layouts,当模板变化时,页面重新生成
  • content,当文章内容变化时,页面重新生成
  • config,当配置变化时,页面重新生成
  • data,当数据变化时,页面重新生成
  • static,当静态文件发生变化时,这些文件会被同步到 public 下的对应路径

其中,值得注意的是 static 目录。

┌──────────────┐                  
│hugo watch    │                  
│  ┌─────────┐ │                  
│  │ content │─┼─┐                
│  └─────────┘ │ │                
│  ┌─────────┐ │ │                
│  │  data   │─┼─┤                
│  └─────────┘ │ │                
│  ┌─────────┐ │ │ rebuild        
│  │ config  │─┼─┤                
│  └─────────┘ │ │                
│  ┌─────────┐ │ │    ┌──────────┐
│  │ layouts │─┼─┴───▶│  public  │
│  └─────────┘ │      └──────────┘
│  ┌─────────┐ │            ▲     
│  │ static  │─┼────────────┘     
│  └─────────┘ │  sync            
└──────────────┘                  

json、csv 数据文件放在 static 作静态文件时,这些数据文件的更新会触发 hugo rebuild,这些数据文件也会被同步发布到 public 目录下。但 hugo 并不会更新对它们的引用,包括使用 os.Stat 读取文件状态,不能读取到变化后的时间。意味着如果模板中引用到这些文件中的数据来不断生成新的页面,所得到的页面内容数据不会是最新的(经实际测试,页面始终为第一次启动时的状态,不管这些文件变化多少次)。不知道这是 hugo 的 bug,还是有意为之。

因此,这就涉及到数据文件存放的选择问题。在文章 hugo 中的数据管理 中谈到,data 目录是用来保存除控制参数以外的配置数据,会随 hugo 驻留在内存中。

页面使用到的本地 json 数据文件,可以有三种存放位置:

  1. data 目录下,但缺点是这些数据以驻留在内存的方式存在,当数据比较多时 hugo 会占用过多的内存
  2. 在 content 页面的目录下,做为页面的 page bundle resource 存在。缺点是当这些数据不能在多个页面之间共享,并且存储位置不统一,不便于维护管理。
  3. 放在 static 目录下。缺点就是它的单独更新,不会触发 hugo 页面 rebuild.

static 下的数据变化如何触发 hugo rebuild #

接上节中的问题,本节主要谈谈如何让 hugo 在 static 目录下的 json 文件发生变化时也会触发页面 rebuild,从而确保网站的页面内容最新。

如果你已经按照有悟所介绍的,已经准备了另外的数据更新程序,那么你就差最后一步,在线触发 hugo 的页面 rebuild。

也许你已经想到了,上节已经提到,当 hugo 监控到 layouts、content、config、data 这些目录中的内容发生变化后,会触发整站的页面 rebuild。本节所描述的方法就是利用这个机制,在 static 目录中的 json 数据更新后,让以上四个目录之一发生某些变化,从而让 hugo 感知,接着页面就会更新。

经过一番搜索,发现在 vscode 完善支持 golang 开发之前,golang 本身缺少一项 代码保存后重新编译 的开发者功能,在 github 上有几个实现了这个简单的 watch and build 的开源小工具。有悟挑选了其中的 modd 来触发 hugo rebuild,从这个代码库的 release 下载对应的操作系统版本。

这个 watch and build 工具本身,与 hugo 一样,具有 监控文件变化 的能力,利用它来监控 static目录,当 json 文件发生变化后,向变化能引起 hugo rebuild 的目录layouts、content、config、data中的任一个更新一个空文件,这样傻傻地就可以触发 hugo rebuild(需要注意的,data 目录下的数据只能是 yaml、json、csv格式,因为这些数据会被 hugo 解析后加载到内存,所以不符合格式的内容会引起 hugo 报错,需要小心)


  1. 配置 modd

在 hugo site 目录下新增一个文件 modd.conf:

static/data/** {
    prep: echo "rebuild: true" > data/hub.yml
}
static/data/**
表示监控 static 下的数据目录
echo "rebuild: true" > data/hub.yml
modd 会在检测到目录变化向,在 site/data 目录下生成一个 hub.yml 文件,内容为 rebuild: true。文件名与内容可根据需要来定,只要是可执行的 shell 命令。prep 后所执行的命令,只要能触发 hugo rebuild 的 shell 都可以。
  1. 启动 modd

进入 hugo 工作目录 site 后,使用 路径/modd -f modd.conf,modd 启动后就会监控 static/data 目录变化,根据配置中的命令,从而触发 hugo rebuild。

  1. 将 modd 配置成系统服务

如果你的 hugo 是部署在服务器上的在线服务,那么 modd 就需要与 hugo 一同不间断运行。这时使用 systemd 或者 supervisord 来让 modd 变成常驻的进程。

[Unit]
Description=modd server
Documentation=https://github.com/cortesi/modd
After=network.target nss-lookup.target

[Service]
User=ubuntu
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
NoNewPrivileges=true
WorkingDirectory=/home/public/youwu.today/site
ExecStart=/home/ubuntu/bin/modd -f modd.conf 
Restart=on-failure
RestartPreventExitStatus=23

[Install]
WantedBy=multi-user.target

通过这种变相的方法,有悟把需要更新的数据,定期的更新到 static/data 目录下,使用 modd 来监控这个变化,然后当变化发生时,向 hugo 工作目录 site/data 中写入一个数据文件来触发 hugo rebuid,从而确保静态页面最新。

有悟翻遍 hugo 的命令行说明,也没见到有可以实现本功能的参数选项。 如果 hugo 对待 static 目录下的内容变化也能像对待 layouts、content、config、data 这四个目录一样,那么本节就是多余的。

结论 #

如果数据比较少,数据文件比较小,可以让数据同步程序或者脚本将最新的数据存放在 data 目录下。 如果数据没有共享性,只是某个页面使用,那么可以同步到 content 页面的目录下,作为该页面的 resource。 如果数据量比较大,并且可能多个页面共用,那么应该放在 static 目录下,以确保 hugo 可以获取到。再使用另外的手段来触发 hugo 页面 rebuild。