前言
本静态站点用于演示之用,使用 Hugo 构建,以及 Markdown 供应内容。
流行的静态站点框架有以下几个:
- Jekyll (基于 Ruby 容易上手) https://www.jekyll.com.cn/docs/home/
- Hexo (基于 Node.js 容易上手) https://hexo.io/docs/
- Hugo (基于 Go) https://gohugo.io/documentation/
演示站点有两个访问入口:
- https://jimbowhy.gitee.io/
- https://jimboyeah.github.io/
此站点提供了一篇关于 Hugo 静态站点生成框架的入门教程。
- Hugo 不完美教程 - IX: Menus 菜单组织
- Hugo 不完美教程 - VIII: Functions 内置函数
- Hugo 不完美教程 - VII: Variables 对象变量
- Hugo 不完美教程 - VI: Multilingual 多语言支持
- Hugo 不完美教程 - V: Templates 模板机制
- Hugo 不完美教程 - V: Templates 其它模板
- Hugo 不完美教程 - IV: Hugo Pipes 管道处理
- Hugo 不完美教程 - III: Hugo Modules 模块
- Hugo 不完美教程 - II: Hugo 目录组织
- Hugo 不完美教程 - I: Hugo Web Framework
代码仓库地址如下,查看 hugo-project 分支是原文件,master 分支是发布的静态站点文件:
- https://github.com/jimboyeah/jimboyeah.github.io/tree/hugo-project
- https://gitee.com/jimbowhy/jimbowhy/tree/hugo-project/
title: "IV: Hugo Pipes 管道处理"
description: "坚果的 Hugo 教程"
date: 2020-08-06T20:14:08-04:00
featured_image_: "/assets/IMG_20181101_233654_s.jpg"
thumb_image_: "/assets/micro_s.png"
summary: Hugo Pipes 是一组织处理资源目录下的文件函数集,资源目录可以通过 assetDir 配置项指定,默认是 assets,这些资源通过管道处理生成最终需要的文件,比如 SCSS 通过管道的工具处理生成 CSS,其中一个工具就是 PostCSS。
tags: ["hugo"]
目录:
[TOC]
Hugo Pipes 管道处理
Hugo Pipes 是一组织处理资源目录下的文件函数集,资源目录可以通过 assetDir 配置项指定,默认是 assets。
涉及处理的内容:
- 样式脚本 SASS / SCSS
- 构建后期 PostProcess
- 样式加工 PostCSS
- 脚本转译 JavaScript Building
- 脚本转译 Babel
- 资源压缩 Asset minification
- 资源打包 Asset bundling
- 资源指纹 Fingerprinting and SRI
- 资源生成 Resource from Template
- 资源生成 Resource from String
管道处理还应用于模板中串联函数的调用,如,生成 5 个数再将顺序打乱:
{{ shuffle (seq 1 5) }}
使用管道的语法:
{{ (seq 1 5) | shuffle }}
资源对象的提供的属性变量或方法参考 Page Resources 文档:
{{
属性 | 说明 |
---|---|
ResourceType | 资源 MIME 类型,如 image/jpeg 对应 ResourceType image |
Name | 资源文件名,相对于当前页面,可以在 front matter 设置 |
Title | 默认和 .Name 一样,也可以在 front matter 设置 |
Permalink | 资源绝对 URL,对于 page 资源空值 |
RelPermalink | 资源相对 URL,对于 page 资源空值 |
Content | 资源内容,通常是字符串内容 |
MediaType | MIME 类型,如 image/jpeg |
MediaType.MainType | 主要 MIME 类型,如 application/pdf 的 MainType 就是 application |
MediaType.SubType | 次要 MIME 类型,上面 pdf 的 SubType 是 pdf ,而 PPT 文件是 vnd.mspowerpoint |
MediaType.Suffixes | 可能 MIME 列表,切片数据类型 |
{{
如果,有 Go 语言基础,可以试着读 Hugo 源代码,这也是开源的一大好片处,似乎不搞源代码开源就没有意义了:
// Resource represents a linkable resource, i.e. a content page, image etc.
type Resource interface {
ResourceTypeProvider
MediaTypeProvider
ResourceLinksProvider
ResourceMetaProvider
ResourceParamsProvider
ResourceDataProvider
}
使用资源对象方法:
如果,文件不一定会存在,那么就需要加条件判断:
{{ $style := resources.Get "theme/css/main.css" | resources.PostCSS }}
{{ if $css }}
{{ printf $style.Content }}
{{ end }}
处理 SCSS 样式脚本资源
先将资源文件读入使用:
{{ $style := resources.Get "sass/main.scss" }}
在 resources.Get 函数读取 SCSS 样式脚本后,就要使用扩展将其转换为 CSS。
资源文件会被打包到 /public 目录下,如果使用了 .Permalink 或 .RelPermalink,即意味使用了资源,Hugo 就会将将其打包。
{{ $style := resources.Get "sass/main.scss" | resources.ToCSS | resources.Minify | resources.Fingerprint }}
{{ $style := resources.Get "sass/main.scss" | toCSS | minify | fingerprint }}
每个 Hugo Pipes 资源转换方法都使用驼峰式 camelCased 别名,如 toCSS 表示 resources.ToCSS,非转换方法就没有这样的别名,如 resources.Get, resources.FromString, resources.ExecuteAsTemplate, resources.Concat 等。
整个 Hugo Pipes 管道链是基于缓存的,即前一级生成的内容缓存后进入下一级处理:
{{ $mainJs := resources.Get "js/main.js" | js.Build "main.js" | minify | fingerprint }}
在 Hugo 构建站点时,管道链在首次调用后建立,所有资源都会从缓存中获取,不必担心模板执行反反复复地使用资源而引起构建性能问题。
通过 toCSS 进行转换时,可以设置以下参数:
{{
选项 | 类型 | 说明 |
---|---|---|
targetPath | [string] | 指定输出路径,默认只修改原 SASS/SCSS 的扩展名为 .css; |
outputStyle | [string] | 默认是 nested,其它输出风格有 expanded, compact, compressed 等; |
precision | [int] | 浮点值处理精度 |
enableSourceMap | [bool] | 是不使用 source map 调试信息映射文件 |
includePaths | [string slice] | 添加 SCSS/SASS 包含目录,注意要使用工程目录中的相对路径 |
{{
示范,给 SCSS/SASS 添加 includePaths 路径参数,注意使用了 dict 字典对象:
{{ $options := (dict "targetPath" "style.css" "outputStyle" "compressed" "enableSourceMap" true "includePaths" (slice "node_modules/myscss")) }}
{{ $style := resources.Get "sass/main.scss" | resources.ToCSS $options }}
设置 outputStyle 为 compressed 可以获得比 resources.Minify 更好的 SASS/SCSS 压缩效果。
SASS / SCSS 作为 CSS 的升级版,实现以样式脚本的方式来定义样式表,掌握它们可以极大提高效率。但是学习曲线陡峭,另一个选择是使用 PostCSS 工具。
构建后期 PostProcess
允许在构建生成后延迟资源到 /public 的转移,使用 resources.PostProcess 标记资源资源后,延迟转移生成后的任何文件,通常转换链中的一个或多个步骤取决于生成的结果。
一个基本的后期处理是使用 PostCSS 对样式进行清理,当前有两个限制:
- 只能处理 .html 模板文件;
- 不可以操纵由资源对象方法返回的值;
例如,示例中 upper 函数的调用就不能正常得到正确结果:
{{ $css := resources.Get "css/main.css" }}
{{ $css = $css | resources.PostCSS | minify | fingerprint | resources.PostProcess }}
{{ $css.RelPermalink | upper }}
配合 PostCSS 清理 CSS,有多种方法实现,考虑简单的实现方法,避免使用 resources.PostProcess 从模板中提取关键字,可以参考 tailwindcss 文档示例。
下面的配置写入项目根目录下的 hugo_stats.json,如果只想在发布时有效,可以将其保存到 config/production 目录。
[build]
writeStats = true
配置脚本:
const purgecss = require('@fullhuman/postcss-purgecss')({
content: [ './hugo_stats.json' ],
defaultExtractor: (content) => {
let els = JSON.parse(content).htmlElements;
return els.tags.concat(els.classes, els.ids);
}
});
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
...(process.env.HUGO_ENVIRONMENT === 'production' ? [ purgecss ] : [])
]
};
上面配置为发布时清理,那么在页面模板中也要相应使用条件进行环境判断:
{{ $css := resources.Get "css/main.css" }}
{{ $css = $css | resources.PostCSS }}
{{ if hugo.IsProduction }}
{{ $css = $css | minify | fingerprint | resources.PostProcess }}
{{ end }}
样式加工 PostCSS
Hugo Pipes 可以使用 PostCSS 处理样式文件,这是一个非常实用的 CSS 工具。
PostCSS 官方介绍插件功能特性:
- 增加代码可读性 → autoprefixer
- 使用先进的 CSS 样式,Use tomorrow's CSS, today! → postcss-cssnext
- 全局样式 Global CSS 终结者 → postcss-modules
- 保证样式正确性 → stylelint
- 强大的 grid CSS → LostGrid
一些支持的功能:
- 片断引入 partial imports
- 变量 variables
- 嵌套 nesting
- 混合宏 mixins
- 扩展 extend
- 占位符 placeholder classes
- 颜色函数 darken and rgba color functions
- 压缩 compression
语法支持参考各个插件,大数预处理器由 Syntaxes 语法扩展而来。事实上,Sass、Stylus 和 LESS 很多功能都可以通过 PostCSS 语言的扩展实现,比如说添加 mixin,变量,条件,循环,嵌套和扩展等。
例如,定义以下一个样式:
:fullscreen { ... }
经过 PostCSS autoprefixer 处理后,自动添加了浏览器前缀:
:-webkit-full-screen { ... }
:-ms-fullscreen { ... }
:fullscreen { ... }
Lost Grid 是一个强大的 PostCSS 网格系统,可与任何预处理器甚至是原生 CSS 一起使用。
在这里有非常好的 demo 展示:http://lostgrid.org/lostgrid-example.html
以下例子根据不同的设备屏幕大小来调整网格的每行格子数,小屏幕一行一格,中小屏幕一行三格,大屏幕一行六格:
.ColumnSection__grid div {
lost-column: 1/1;
}
@media (min-width: 400px) {
.ColumnSection__grid div {
lost-column: 1/3;
}
}
@media (min-width: 900px) {
.ColumnSection__grid div {
lost-column: 1/6;
}
}
postcss-nested 实现类似 Sass 功能:
.phone {
&_title {
width: 500px;
@media (max-width: 500px) { width: auto; }
body.is_dark & { color: white; }
}
img { display: block; }
}
.title {
font-size: var( --font );
@at-root html { --font: 16px }
}
@at-root 相应为上级节点定义一个新的样式,var (--font) 这样的表示引用变量,只有上级节点的值才能有效引用。
转译生成:
.phone_title { width: 500px; }
@media (max-width: 500px) {
.phone_title { width: auto; }
}
body.is_dark .phone_title { color: white; }
.phone img { display: block; }
.title { font-size: var(--font); }
html { --font: 16px }
postcss-nested & postcss-mixins 结合实现 Sass 中最常用的特性:
@define-mixin clearfix{
&:after{
display: table;
clear: both;
content: " ";
}
}
.column-container{
color: #333;
@mixin clearfix;
}
编译后:
.column-container{
color: #333;
}
.column-container:after{
display: table;
clear: both;
content: " ";
}
postcss-cssnext 语法:
:root {
--fontSize: 1rem;
--mainColor: #12345678;
--centered: {
display: flex;
align-items: center;
justify-content: center;
};
}
body {
color: var(--mainColor);
font-size: var(--fontSize);
line-height: calc(var(--fontSize) * 1.5);
padding: calc((var(--fontSize) / 2) + 1px);
}
.centered {
@apply --centered;
}
生成浏览器可用语法:
body {
color: rgba(18, 52, 86, 0.47059);
font-size: 16px;
font-size: 1rem;
line-height: 24px;
line-height: 1.5rem;
padding: calc(0.5rem + 1px);
}
.centered {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
}
可以在 baseof.html 模板中引入样式资源:
{{ $css := resources.Get "css/main.css" }}
{{ $style := $css | resources.PostCSS }}
或者指定配置:
{{ $style := resources.Get "css/main.css" | resources.PostCSS (dict "config" "customPostCSS.js" "noMap" true) }}
{{
属性 | 类型 | 说明 |
---|---|---|
config | [string] | 指定 PostCSS 配置文件,默认 postcss.config.js |
noMap | [bool] | 默认 true 不生成调试映射文件 |
inlineImports | [bool] | 默认 false,启用 @import "..." 或 @import url("...") 语句 |
use | [string] | 使用的 PostCSS 插件列表 |
parser | [string] | 指定 PostCSS parser |
stringifier | [string] | 指定 PostCSS stringifier |
syntax | [string] | 指定 postcss syntax |
{{
安装 postcss-cli 或相应插件模块,以下为全局安装,建议在工程中安装,即去掉 -g 参数:
npm install -g postcss-cli
npm install -g autoprefixer postcss-cssnext postcss-import postcss-apply postcss-nested postcss-mixin postcss-sass
使用 Hugo Snap package 则需要在项目中安装 PostCSS 而不是全局安装:
npm install postcss-cli
安装后可以按以下命令格式试试样式的编译:
postcss --use autoprefixer -c options.json -o main.css css/*.css
postcss input.css -o output.css
postcss src/**/*.css --base src --dir build
cat input.css | postcss -u autoprefixer > output.css
可以在 postcss.config.js 进行配置,通过 Node 上下文指定环境。
module.exports = {
plugins: [
require('autoprefixer'),
require('postcss-cssnext'),
require('postcss-nested'),
...process.env.HUGO_ENVIRONMENT === 'production'
? [purgecss]
: []
]
}
参考 https://github.com/postcss/postcss#usage
JavaScript Building 脚本打包
Hugo Pipes 使用 ESBuild 来转译 JavaScript 脚本,Tree Shaking 算法可以有效清除死代码,这是一个高效 JavaScript 转译器:
{{ $built := resources.Get "js/index.js" | js.Build "main.js" }}
可以通过 target [string] 指定 es5, es2015, es2016, es2017, es2018, es2019, es2020, esnext 行目标输出规范,默认是 esnext。
或使用其它选项,使用 dict 关键字定义两个字典来传入参数:
{{ $externals := slice "react" "react-dom" }}
{{ $defines := dict "process.env.NODE_ENV" `"development"` }}
{{ $opts := dict "targetPath" "main.js" "externals" $externals "defines" $defines }}
{{ $built := resources.Get "scripts/main.js" | js.Build $opts }}
Babel 脚本转译
Hugo Pipes 也可以通过 Babel 来转译脚本,任意版本的 JavaScript 可以转译为另一个版本规范。
Babel 使用了 babel cli,需要先进行安装,全局安装或作为工程依赖安装:
npm install -g @babel/cli @babel/core
npm install @babel/preset-env --save-dev
如果使用了 Hugo Snap 包则需要在工程中安装,而不是全局安装:
npm install @babel/cli @babel/core --save-dev
{{- $transpiled := resources.Get "scripts/main.js" | babel -}}
{{ $opts := dict "noComments" true }}
{{- $transpiled := resources.Get "scripts/main.js" | babel $opts -}}
默认地,Babel 会使用工程中 babel.config.js 作为配置文件。
Asset minification
Hugo Pipes 可以使用 resources.Minify 压缩 CSS, JS, JSON, HTML, SVG, XML 等资源:
{{ $css := resources.Get "css/main.css" }}
{{ $style := $css | resources.Minify }}
如果需要压缩最终输出到 /public 目录的 HTML 文件,可以使用 hugo --minify 命令。
Asset bundling
Hugo Pipes 可以将任意资源打包在一起,相同的 MIME 类型文件就只可以打包为一个文件以减少浏览器请求。
{{ $plugins := resources.Get "js/plugins.js" }}
{{ $global := resources.Get "js/global.js" }}
{{ $js := slice $plugins $global | resources.Concat "js/bundle.js" }}
为资源文件生成指纹 Fingerprinting
通过 resources.Fingerprint 方法生成 sha256 哈希摘要,可以指定其它,如 sha384, sha512, md5 等。
处理后的资源对象会在 .Data.Integrity 属性保存摘要数据,由生成摘要的函数名和摘要数据的 Base64 编码用连字符拼接组成。
{{ $js := resources.Get "js/global.js" }}
{{ $secureJS := $js | resources.Fingerprint "sha512" }}
SRI - Subresource Integrity 子资源完整性,用它可以确保站点在客户端运行时,加载的是未经篡改的原始资源。
大部分运营商被劫持,都是因为插入广告代码的需求。如果网站启用了 SRI,篡改后的文件就无法执行,这很可能让页面变得完全不可用。所以 SRI 给我的感觉是:宁为玉碎不为瓦全。
使用 CSP - Content Security Policy 外链白名单机制可以在现代浏览器下减小 XSS 风险。但针对 CDN 内容被篡改而导致的 XSS,CSP 并不能防范,因为网站所使用的 CDN 域名,肯定在 CSP 白名单之中。而 SRI 通过对资源进行摘要签名机制,保证外链资源的完整性。
例如,要引入以下这个资源,并启用 SRI 策略:
https://example.com/static/js/other/zepto.js
可以使用 sha256 算法生成摘要签名,并进行 Base64 编码:
curl https://example.com/static/js/other/zepto.js | openssl dgst -sha256 -binary | openssl enc -base64 -A
b/TAR5GfYbbQ3gWQCA3fxESsvgU4AbP4rZ+qu1d9CuQ=
最终的代码如下:
浏览器拿到资源内容之后,会使用 integrity 所指定的签名算法计算结果,并与 integrity 提供的摘要签名比对,如果二者不一致,就不会执行这个资源。
动态加载的资源使用 SRI 也是类似的,需要指定 crossOrigin 和 integrity 属性。例如:
var s = document.createElement('script');
s.crossOrigin = 'anonymous';
s.integrity = 'sha256-b/TAR5GfYbbQ3gWQCA3fxESsvgU4AbP4rZ+qu1d9CuQ=';
s.src = 'https://example.com/static/js/other/zepto.js';
document.head.appendChild(s);
从字符串中创建资源
示范生成 JS 脚本:
{{ $string := (printf "var rootURL: '%s'; var apiURL: '%s';" (absURL "/") (.Param "API_URL")) }}
{{ $targetPath := "js/vars.js" }}
{{ $vars := $string | resources.FromString $targetPath }}
{{ $global := resources.Get "js/global.js" | resources.Minify }}
从模板中获取创建资源
使用 resources.ExecuteAsTemplate 只将资源作为模板执行:
// assets/sass/template.scss
$backgroundColor: {{ .Param "backgroundColor" }};
$textColor: {{ .Param "textColor" }};
body{
background-color:$backgroundColor;
color: $textColor;
}
// [...]
// some md file
{{ $sassTemplate := resources.Get "sass/template.scss" }}
{{ $style := $sassTemplate | resources.ExecuteAsTemplate "main.scss" . | resources.ToCSS }}