0.参考说明

本文参考自以下文章

1.gulp简介

gulp将开发流程中让人痛苦或耗时的任务自动化,从而减少你所浪费的时间、创造更大价值。

简单来说,gulp就是一个自动化工具,在本文中将体现以下功能:

  1. 压缩静态资源(js/css/html/font
  2. 监控自定义js/css的改变,合并成一个(这么做有一定的弊端!)
  3. 扩展:引入Stylus,编译并合并

2.gulp安装

先决条件:Node.js,若未安装,请查看《Node.js安装教程》

安装gulp命令行工具

1
npm install --global gulp-cli

创建项目目录并进入

1
2
3
mkdir jinyu_hexo_gulp

cd jinyu_hexo_gulp

提示:

  • 可以在Hexo根目录下创建,或完全新建一个项目
  • 之后将使用[gulp_root]代指本项目的根目录
  • 此处设定gulp项目(jinyu_hexo_gulp)和Hexo项目(jinyu_hexo_blog)是并列关系:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    Project
    ├── jinyu_hexo_blog
    │ ├── ...
    │ ├── public
    │ ├── scaffolds
    │ ├── source
    │ │ ├── ...
    │ │ ├── js //用于放置自定义js整合文件
    │ │ ├── css //用于放置自定义css整合文件
    │ │ └── custom //未处理的自定义静态资源
    │ │ ├── fonts
    │ │ ├── js
    │ │ ├── css
    │ │ └── styl
    │ └── themes
    │ └── butterfly
    │ └── ...
    └── jinyu_hexo_gulp
    └── ...

在项目目录下创建package.json文件

1
npm init

提示:(一直按回车)

安装gulp,作为开发时依赖项

1
npm install --save-dev gulp

检查gulp版本

1
gulp --version

确保输出与下面的屏幕截图匹配,否则你可能需要执行本指南中的上述步骤。

创建gulpfile文件

[gulp_root]创建文件gulpfile.jsGulpfile.js(请确保名称正确),然后在里面先写一个HelloWorld试试:

1
2
3
4
5
6
7
function defaultTask(cb) {
// place code for your default task here
console.log('HelloWorld!');
cb();
}

exports.default = defaultTask

说明:cb:CallBack的缩写,具体请看异步执行

测试

[gulp_root]路径下,打开终端输入:

1
gulp

输出结果

3.gulpfile.js解析

创建任务

gulpfile.js中,创建函数即可创建一个任务,如上面的HelloWorld例子:

1
2
3
4
5
6
7
8
//此处创建了一个名为defaultTask的任务
function defaultTask(cb) {
console.log('HelloWorld!');
cb();
}

//导出任务
exports.default = defaultTask

然后下面的exports.default = defaultTask指的是导出了defaultTask任务,并命名为default

exports.<task_name>的任务称为public任务,可以在命令行中使用gulp <task_name>执行,未导出的任务则是private任务,不可在命令行中使用gulp <task_name>来执行。

default是默认任务名称,所以在命令行中使用gulp <task_name>时可以省略任务名,只需要输入gulp,等效于gulp default

多个任务

再创建两个测试任务ab,且都导出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//此处创建了一个名为defaultTask的任务
function defaultTask(cb) {
console.log('HelloWorld!');
cb();
}

function a(cb) {
console.log('a');
cb();
}

function b(cb) {
console.log('b');
cb();
}

//导出任务
exports.default = defaultTask
exports.a = a
exports.b = b

此时可以在命令行中使用gulp a b来执行多个任务,即:可以输入gulp <task_name0> <task_name1> ...来执行多个任务,若还要执行default任务,则必须写上,不可省略。

组合任务

gulp提供了两个强大的组合方法:series()parallel(),允许将多个独立的任务组合为一个更大的操作。这两个方法都可以接受任意数目的任务(task)函数或已经组合的操作。series()parallel()可以互相嵌套至任意深度。

串行series()

如果需要让任务(task)按顺序执行,请使用series()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//需要引入series方法
const { series } = require('gulp');

//此处创建了一个名为defaultTask的任务
function defaultTask(cb) {
console.log('HelloWorld!');
cb();
}

function a(cb) {
console.log('a');
cb();
}

function b(cb) {
console.log('b');
cb();
}

//导出任务
exports.default = defaultTask
exports.c = series(a, b)

此时执行在命令行中执行gulp c将会顺序执行ab两个任务。

并行parallel()

对于希望以最大并发来运行的任务(tasks),可以使用parallel()方法将它们组合起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//需要引入parallel方法
const { parallel } = require('gulp');

//此处创建了一个名为defaultTask的任务
function defaultTask(cb) {
console.log('HelloWorld!');
cb();
}

function a(cb) {
console.log('a');
cb();
}

function b(cb) {
console.log('b');
cb();
}

//导出任务
exports.default = defaultTask
exports.c = parallel(a, b)

此时执行在命令行中执行gulp c将会并发执行ab两个任务。

结论

使用series()还是parallel()要看任务之间是否有依赖关系,比如:

  1. 编译stylus为css的任务和压缩css任务就有先后关系,需要使用series()来执行。
  2. 压缩css任务和压缩js任务之间就没有依赖关系,可以使用parallel()来执行。

4.Gulpfile分割

大部分用户起初是将所有业务逻辑都写到一个gulpfile.js文件中。随着文件的变大,可以将此文件重构为数个独立的文件。
每个任务(task)可以被分割为独立的文件,然后导入(import)到gulpfile.js文件中并组合。这不仅使事情变得井然有序,而且可以对每个任务(task)进行单独测试,或者根据条件改变组合。

例:在[gulp_root]下新建文件夹tasks,新建任务say_hello.jssay_world.js,此时的目录格式如下:

1
2
3
4
5
6
7
jinyu_hexo_gulp
├── gulpfile.js
├── package-lock.json
├── package.json
└── tasks
├── say_hello.js
└── say_world.js

say_hello.js

1
2
3
4
5
6
const sayHello = (cb) => {
console.log("hello");
cb();
}

exports.sayHello = sayHello;

say_world.js

1
2
3
4
5
6
const sayWorld = (cb) => {
console.log("world");
cb();
}

exports.sayWorld = sayWorld;

gulpfile.js

1
2
3
4
5
const {series} = require('gulp');
const {sayHello} = require('./tasks/say_hello.js');
const {sayWorld} = require('./tasks/say_world.js');

exports.default = series(sayHello, sayWorld);

这样按文件区分不同的任务,然后在gulpfile.js中引入的方式,便于任务的管理和分类以及日后的维护。

命令行中执行gulp的结果:

5.插件

gulp有很多插件来达成各式各样的功能,如压缩静态资源(js/css/html/font)、监控文件的变化等。

下文用到的插件列表

插件名作用版本
gulp-terser压缩js^2.1.0
gulp-clean-css压缩css^4.3.0
gulp-htmlclean压缩html^2.7.22
gulp-html-minifier-terser压缩html,可以压缩html中的ES6语法^7.1.0
gulp-fontmin压缩字体^0.7.4
gulp-concat合并静态资源^2.6.1
gulp-stylus编译Stylus文件^3.0.0

引入path.js

[gulp_root]下新建path.js,用于定义后面任务需要用到的路径,比如自定义js文件的源路径、压缩合并后的目的路径等:

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
custom_js: {
src: '../jinyu_hexo_blog/source/custom/js/*.js',
concat_name: 'jinyu.js',
dest: '../jinyu_hexo_blog/source/js'
},
custom_css: {
src: '../jinyu_hexo_blog/source/custom/css/*.css',
concat_name: 'jinyu.css',
dest: '../jinyu_hexo_blog/source/css'
}
};

合并自定义静态资源并监测

安装插件:

1
npm install --save-dev gulp-concat

tasks文件夹新建任务custom_js.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const {src, dest, watch} = require('gulp');
const {custom_js} = require('../paths.js');

const concat = require('gulp-concat'); //合并

//合并自定义js
const concatJs = () =>
src([custom_js.src])
.pipe(concat(custom_js.concat_name))
.pipe(dest(custom_js.dest));

//监控自定义js的变动,{ignoreInitial: false}表示执行watch时初始化一次watch监视的任务
const watchJs = () =>
watch(custom_js.src, {ignoreInitial: false}, concatJs);

exports.concatJs = concatJs;
exports.watchJs = watchJs;

tasks文件夹新建任务custom_css.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const {src, dest, watch} = require('gulp');
const {custom_css} = require('../paths.js');

const concat = require('gulp-concat'); //合并

//合并自定义css
const concatCss = () =>
src([custom_css.src])
.pipe(concat(custom_css.concat_name))
.pipe(dest(custom_css.dest));

//监控自定义css的变动,{ignoreInitial: false}表示执行watch时初始化一次watch监视的任务
const watchCss = () =>
watch(custom_css.src, {ignoreInitial: false}, concatCss);

exports.concatCss = concatCss;
exports.watchCss = watchCss;

修改gulpfile.js

1
2
3
4
5
6
7
const {parallel, series} = require('gulp');

const {concatJs, watchJs} = require('./tasks/custom_js.js');
const {concatCss, watchCss} = require('./tasks/custom_css.js');

exports.concat = parallel(concatJs, concatCss);
exports.watch = parallel(watchJs, watchCss);

gulp concat

在命令行输入gulp concat,执行效果:

  1. jinyu_hexo_blog/source/custom/js/下的所有js文件合并成jinyu.js,并放到jinyu_hexo_blog/source/js/下。
  2. jinyu_hexo_blog/source/custom/css/下的所有css文件合并成jinyu.css,并放到jinyu_hexo_blog/source/css/下。

修改Hexo的_config.yml

jinyu_hexo_blog/source/custom下存放的是未处理的自定义静态资源文件,在执行hexo g时,public里是不需要这些文件的,所以在_config.yml(不是主题的_config.yml)中把此文件夹忽略。

1
2
3
4
5
6
# Include / Exclude file(s)
## include:/exclude: options only apply to the 'source/' folder
include:
exclude:
ignore:
- '**/source/custom'

注意:在部署Hexo时,命令gulp concat需要放在hexo clhexo g之间。

gulp watch

在命令行输入gulp watch,执行效果:

  1. jinyu_hexo_blog/source/custom/js/下的任意js文件在编辑后,会被gulp监测到,然后执行custom_js.js中的concatJs任务:jinyu_hexo_blog/source/custom/js/下的所有js文件合并成jinyu.js,并放到jinyu_hexo_blog/source/js/下。
  2. jinyu_hexo_blog/source/custom/css/下的任意css文件在编辑后,会被gulp监测到,然后执行custom_css.js中的concatCss任务:jinyu_hexo_blog/source/custom/css/下的所有css文件合并成jinyu.css,并放到jinyu_hexo_blog/source/css/下。

修改主题的_config.yml

_config.butterfyl.yml_config.yml(主题的_config.yml)中找到inject,然后把合并后的自定义文件引入即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Inject
# Insert the code to head (before '</head>' tag) and the bottom (before '</body>' tag)
# 插入代码到头部 </head> 之前 和 底部 </body> 之前
inject:
head:
# - <link rel="stylesheet" href="/xxx.css">
# 自定义css
# 静态文件后面加个"?任意内容"可以在每次更新之后让用户自动重新请求。
# 例如添加"?1",在修改此文件后改成"?2",用户访问你的网站时,不会使用本地的缓存,而是请求新的内容,没修改的话就不用动。
- <link rel="stylesheet" media="defer" onload="this.media='all'" href="/css/jinyu.css?v=202301151121">
bottom:
# - <script src="xxxx"></script>
# 自定义js
- <script async data-pjax src="/js/jinyu.js?v=202301151121"></script>

注意:命令gulp watch的作用通常是在本地开发时使用,本地hexo s后,另开一个终端执行gulp watch。在修改自定义js/css后,会更新合并后的文件jinyu.js/jinyu.css

编译、合并自定义Stylus并监测

若对于自定义css依旧想使用原生的css来实现,可跳过这一节。或想要使用其他css预处理器,可以参考此节。

关于Stylus的介绍,请查看

安装插件:

1
npm install --save-dev gulp-stylus

paths.js中添加关于stylus的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = {
custom_js: {
src: '../jinyu_hexo_blog/source/custom/js/*.js',
concat_name: 'jinyu.js',
dest: '../jinyu_hexo_blog/source/js'
},
// 使用stylus,不再使用css
// custom_css: {
// src: '../jinyu_hexo_blog/source/custom/css/*.css',
// concat_name: 'jinyu.css',
// dest: '../jinyu_hexo_blog/source/css'
// },
custom_stylus: {
src: '../jinyu_hexo_blog/source/custom/styl/*.styl',
concat_name: 'jinyu.css',
dest: '../jinyu_hexo_blog/source/css'
},
};

tasks文件夹新建任务custom_stylus.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const {src, dest, watch} = require('gulp');
const {custom_stylus} = require('../paths.js');

const concat = require('gulp-concat'); //合并
const stylus = require("gulp-stylus"); //编译stylus

//编译并合并自定义stylus
const concatStylus = () =>
src([custom_stylus.src])
.pipe(stylus())
.pipe(concat(custom_stylus.concat_name))
.pipe(dest(custom_stylus.dest));

//监控自定义stylus的变动,{ignoreInitial: false}表示执行watch时初始化一次watch监视的任务
const watchStylus = () =>
watch(custom_stylus.src, {ignoreInitial: false}, concatStylus);

exports.concatStylus = concatStylus;
exports.watchStylus = watchStylus;

修改gulpfile.js

1
2
3
4
5
6
7
8
9
const {parallel, series} = require('gulp');

const {concatJs, watchJs} = require('./tasks/custom_js.js');
// 使用stylus,不再使用css
// const {concatCss, watchCss} = require('./tasks/custom_css.js');
const {concatStylus, watchStylus} = require('./tasks/custom_stylus.js');

exports.concat = parallel(concatJs, concatStylus);
exports.watch = parallel(watchJs, watchStylus);

gulp concatgulp watch的效果没变,监视css变成了监视styl文件的修改;已修改的Hexo_config.yml和主题的_config.butterfly.yml也不需要改动。

压缩静态资源

安装以下插件:

1
npm install --save-dev gulp-terser gulp-clean-css gulp-htmlclean gulp-html-minifier-terser gulp-fontmin

paths.js中添加关于minify的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
module.exports = {
custom_js: {
src: '../jinyu_hexo_blog/source/custom/js/*.js',
concat_name: 'jinyu.js',
dest: '../jinyu_hexo_blog/source/js'
},
// 使用stylus,不再使用css
// custom_css: {
// src: '../jinyu_hexo_blog/source/custom/css/*.css',
// concat_name: 'jinyu.css',
// dest: '../jinyu_hexo_blog/source/css'
// },
custom_stylus: {
src: '../jinyu_hexo_blog/source/custom/styl/*.styl',
concat_name: 'jinyu.css',
dest: '../jinyu_hexo_blog/source/css'
},
minify: {
js: {
src: [
'../jinyu_hexo_blog/public/**/*.js',
'!../jinyu_hexo_blog/public/**/*.min.js'
],
dest: '../jinyu_hexo_blog/public'
},
css: {
src: '../jinyu_hexo_blog/public/**/*.css',
dest: '../jinyu_hexo_blog/public'
},
html: {
src: '../jinyu_hexo_blog/public/**/*.html',
dest: '../jinyu_hexo_blog/public'
},
font: {
src: '../jinyu_hexo_blog/public/fonts/*.ttf',
html: '../jinyu_hexo_blog/public/**/*.html',
dest: '../jinyu_hexo_blog/public/fonts_dest/'
},
}
};

tasks文件夹新建任务minify.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
const {src, dest, parallel} = require('gulp');
const {minify} = require('../paths.js');

const terser = require('gulp-terser'); //压缩js
const cleanCSS = require('gulp-clean-css'); //压缩css
const htmlClean = require('gulp-htmlclean'); //压缩html
const htmlMinifierTerser = require('gulp-html-minifier-terser'); //压缩html,可以压缩HTML中的ES6语法
const fontMin = require('gulp-fontmin'); //压缩字体

//压缩js
const minifyJS = () =>
src(minify.js.src)
.pipe(terser({keep_fnames: true, mangle: false}))
.pipe(dest(minify.js.dest));

//压缩css
const minifyCSS = () =>
src([minify.css.src])
.pipe(cleanCSS({compatibility: 'ie11'}))
.pipe(dest(minify.css.dest));

//压缩html
const minifyHTML = () =>
src(minify.html.src)
.pipe(htmlClean())
.pipe(htmlMinifierTerser({
removeComments: true, //清除html注释
collapseWhitespace: true, //压缩html
collapseBooleanAttributes: true,
//省略布尔属性的值,例如:<input checked="true"/> ==> <input />
removeEmptyAttributes: true,
//删除所有空格作属性值,例如:<input id="" /> ==> <input />
removeScriptTypeAttributes: true,
//删除<script>的type="text/javascript"
removeStyleLinkTypeAttributes: true,
//删除<style>和<link>的 type="text/css"
minifyJS: true, //压缩页面 JS
minifyCSS: true, //压缩页面 CSS
minifyURLs: true //压缩页面URL
}))
.pipe(dest(minify.html.dest));

//压缩字体
function minifyFontInner(text, cb) {
src(minify.font.src) //原字体所在目录
.pipe(fontMin({
text: text
}))
.pipe(dest(minify.font.dest)) //压缩后的输出目录
.on('end', cb);
}

const minifyFont = (cb) => {
let buffers = [];
src([minify.font.html]) //HTML文件所在目录请根据自身情况修改
.on('data', function (file) {
buffers.push(file.contents);
})
.on('end', function () {
let text = Buffer.concat(buffers).toString('utf-8');
minifyFontInner(text, cb);
});
}

exports.minify = parallel(minifyJS, minifyCSS, minifyHTML, minifyFont);

修改gulpfile.js

1
2
3
4
5
6
7
8
9
10
11
const {parallel, series} = require('gulp');

const {concatJs, watchJs} = require('./tasks/custom_js.js');
// 使用stylus,不再使用css
// const {concatCss, watchCss} = require('./tasks/custom_css.js');
const {concatStylus, watchStylus} = require('./tasks/custom_stylus.js');
const {minify} = require('./tasks/minify.js');

exports.concat = parallel(concatJs, concatStylus);
exports.watch = parallel(watchJs, watchStylus);
exports.minify = minify;

gulp minify

在命令行输入gulp minify,执行效果:
并行执行minifyJS, minifyCSS, minifyHTML, minifyFont四个任务,压缩public下的所有js/css/html文件;压缩后的字体文件会在public/fonts_dest生成,在css中引用时需要注意路径。
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@font-face {
font-family: 'ResourceHanRoundedCN';
font-display: swap;
font-style: normal;
font-weight: normal;
src: url('/fonts_dest/ResourceHanRoundedCN-Bold.eot'); /*IE9*/
src: url('/fonts_dest/ResourceHanRoundedCN-Bold.eot') format('embedded-opentype'), /* IE6-IE8 */ /* url('.woff2') format('woff2'), */
url('/fonts_dest/ResourceHanRoundedCN-Bold.woff') format('woff'), /*chrome、firefox */
url('/fonts_dest/ResourceHanRoundedCN-Bold.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
url('/fonts_dest/ResourceHanRoundedCN-Bold.svg') format('svg'); /* iOS 4.1- */
}
body {
font-family: "ResourceHanRoundedCN", sans-serif !important;
}

注意:gulp minify压缩的是public目录下的文件,所以在执行完hexo g执行gulp minify

6.使用

此处分享一下平常用的管理脚本,来自张洪Heo《通过alias自定义终端命令实现Hexo博客的高效使用,简化你的终端命令》Peach的评论,并做了一些修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@echo off
set blog_home=E:\Project\jinyu_hexo_blog
set gulp_home=E:\Project\jinyu_hexo_gulp
:begin
cd %blog_home%
echo 项目路径:%blog_home%
echo gulp路径:%gulp_home%
echo ====================================
echo 欢迎使用 Hexo 命令助手 请选择命令
echo 本地测试或上传网站时,将自动清理缓存
echo ====================================
echo 1. 本地启动
echo 2. 通过VsCode打开项目
echo 3. 清理缓存
echo 4. 更新追番页数据
echo 5. 新建文章
echo 6. 新建页面
echo 7. 上传网站
echo 8. 访问服务器博客
echo 0. 退出
set /p input="请选择命令并按下回车:"
if %input%==1 goto A
if %input%==2 goto B
if %input%==3 goto C
if %input%==4 goto D
if %input%==5 goto E
if %input%==6 goto F
if %input%==7 goto G
if %input%==8 goto H
if %input%==0 goto Z

@REM order: 1. 本地启动
:A
cls
start cmd.exe /k "@echo off && hexo cl && hexo g && hexo s"
start cmd.exe /k "@echo off && cd %gulp_home% && gulp watch"
goto begin

@REM order: 2. 通过VsCode打开项目
:B
cls
start cmd.exe /c "@echo off && code %blog_home% && exit"
start cmd.exe /c "@echo off && code %gulp_home% && exit"
goto begin

@REM order: 3. 清理缓存
:C
cls
start cmd.exe /k "@echo off && hexo cl && echo 已清理缓存 && pause && exit"
goto begin

@REM order: 4. 更新追番页数据
:D
cls
start cmd.exe /k "@echo off && hexo bangumi -u && pause && exit"
goto begin

@REM order: 5. 新建文章
:E
set /p newFileName="请输入文章文件名:"
cls
start cmd.exe /k "hexo n %newFileName% && echo. && echo 已在 source/_posts 目录下生成文件:%newFileName%.md && pause && exit"
cls
goto begin

@REM order: 6. 新建页面
:F
set /p newPageName="请输入独立页面文件名:"
cls
start cmd.exe /k "hexo n page %newPageName% && echo. && echo 已在 source 目录下生成文件夹: %newPageName%,并在文件夹内创建文件: index.md && pause && exit"
goto begin

@REM order: 7. 上传网站
:G
cls
start cmd.exe /k "@echo off && hexo cl && cd %gulp_home% && gulp concat && cd %blog_home% && hexo bangumi -u && hexo g && cd %gulp_home% && gulp minify && cd %blog_home% && hexo d && pause && exit"
goto begin

@REM order: 8. 访问服务器博客
:H
cls
start https://jinyu.host
goto begin

@REM order: 0. 退出
:Z
exit

pause

开发

可以看到,在平常开发时,会启动两个命令窗口,一个执行hexo cl && hexo g && hexo s,另一个则是使用gulp进行监测:gulp watch

部署

在部署的时候,顺序如下:
hexo cl -> gulp concat -> hexo bangumi -u -> hexo g -> gulp minify -> hexo d

说明:hexo bangumi -u是更新番剧页面的数据,若没有引入,可以去掉