Gulp 构建流水线手记

这个站点的构建核心只有一条 Gulp 4 流水线:Pug → Less → Babel → 压缩。听起来很「老派」,但对于一个以静态页为主、JS 体量很小的个人主页来说,这种直白的多任务模型反而最好维护——改哪个目录,就跑哪个 task,不用在配置文件里绕圈子。

后来站点长出了 Blog、About、Contact 三个子页面,构建流水线也跟着扩展。但原则没变:主页行为不受影响,新能力通过增量任务接入

整体构建顺序

一次完整 npm run build 的执行顺序如下:

  1. clean — 清理 dist/cssdist/js(注意:不会删掉已编译的 HTML 和 assets)
  2. assets — 复制 src/assetsdist/assets
  3. pug — 编译首页 src/index.pug,注入 config.json
  4. css — 编译 Less,走 autoprefixer、clean-css、cssnano
  5. js — Babel 转译后 uglify
  6. html — 压缩首页 HTML
  7. pages — 构建所有子页面(并行编译 + 子 HTML 压缩)

开发态的 npm run dev 会先 build 一次,再进入 watch。子页面的 HTML 在 watch 下是未压缩的,只有完整 build 才会走 sub-html 压缩——这点和首页行为一致。

子页面任务拆分

pages 任务聚合了六个子任务,其中四个负责编译,两个负责资源与压缩:

  • blog-list — 编译 src/blog/index.pugdist/blog/index.html
  • blog-posts — 批量编译 src/blog/posts/*.pugdist/blog/posts/*.html
  • about / contact — 各自的单页模板
  • sub-assets — 复制各模块 assets/ 子目录(允许为空)
  • sub-html — 对已生成的子页面 HTML 做 htmlclean + htmlmin

子页面 Pug 编译和首页最大的区别是数据注入方式不同。首页只读 config.json;子页面在此基础上叠加模块私有 JSON,并生成导航高亮数据。

数据加载与注入

每个模块在 src/<module>/_data/<module>.json 维护自己的内容。gulpfile 顶部的 loadData(mod) 负责读取:

const loadData = (mod) => {
    const p = `./src/${mod}/_data/${mod}.json`
    return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : {}
}

博客列表页还会额外处理分类导航数据——从 categoriesposts 计算出每个分类的文章数量,供侧栏筛选使用。编译时通过展开运算符合并进 Pug 局部变量:

pug({ data: { ...config, ...loadData('blog'), categoryNav, nav: navData('blog') } })

模板里因此可以同时访问 config.main.name(全局)和 posts(模块私有),不必维护两套配置源。

路径与导航约定

子页面深度不同,资源路径也不一样。每篇 Pug 在顶部声明 base 变量:

  • 列表页 / 关于 / 联系:base = '../'
  • 博客文章页:base = '../../'

顶栏 nav.pug${base} 拼接链接,避免写死 ../blog/ 导致文章页跳转错误。这是静态多层级站点里很基础但容易忽略的细节。

watch 监听范围

开发时改文件能否自动重建,取决于 watch 的 glob。除了首页原有的监听外,子页面扩展后新增了:

  • src/blog/**/*.pugsrc/blog/_data/*.json
  • src/about/**src/contact/** 同理
  • src/components/nav.pug 变更会触发所有子页面重建

一个已知坑:config.json 变更不会被 watch 捕获,改完需要重启 npm run dev。如果模板里开关型字段(比如是否加载 WebGL 背景)读的是 config,重启前会看到旧值。

为什么不用 Webpack / Vite

个人主页的 JS 体量很小:首页动画依赖 CDN 上的 anime.js,副屏只有一个 Canvas 类,子页面几乎纯静态。引入现代打包器带来的收益有限,成本却很实在:

  • 多一层配置和依赖树,升级时容易牵扯一整片
  • dev server 和 HMR 对这个项目不是刚需——Gulp + livereload 已经够用
  • 产物体积是卖点之一,README 里写着 CSS+JS 总计控制在很小的范围

Gulp 的任务模型足够直白:新增一个子页面,就加一个 task,挂到 pages 里,完事。不需要理解 chunk 分割、tree-shaking 规则才能改一个 Pug 模板。

部署到 GitHub Pages 的注意点

最终部署的是 dist/ 目录。子页面以文件夹形式存在(blog/about/),链接用相对路径,在 username.github.io 根域名下可以直接工作。如果将来挂到自定义域名的子路径,需要统一检查 base 变量——这是静态站点扩展时迟早会碰到的问题。

工具选型没有高下,只有是否匹配场景。

这条流水线不算「最佳实践」,但对当前这个站点来说刚好够用。它最大的优点是:半年没碰,回来再看 gulpfile 仍然知道从哪里改起——这对个人项目来说,比「技术栈先进」重要得多。