Hexo 优化 - 利用 Gulp 压缩代码

Gulp 压缩代码

版本说明

主要模块的版本号分别为: gulp 3.9.x,gulp-babel 7.x,babel-core 6.x

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"babel-core": "^6.26.3",
"babel-loader": "^7.1.5",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"gulp": "^3.9.1",
"gulp-babel": "^7.0.1",
"gulp-cache": "^1.1.1",
"gulp-changed": "^3.2.0",
"gulp-clean": "^0.4.0",
"gulp-debug": "^4.0.0",
"gulp-htmlclean": "^2.7.22",
"gulp-htmlmin": "^5.0.1",
"gulp-imagemin": "^5.0.3",
"gulp-minify-css": "^1.2.4",
"gulp-uglify": "^3.0.2",
"gulp-util": "^3.0.8",
"imagemin-pngquant": "^7.0.0"

安装 NPM 模块

全局安装:

1
2
3
4
5
# 全局安装 Gulp 3.9
$ npm install -g gulp@3.9.1

# 全局查看 Gulp 的版本号
$ gulp --version

局部安装:

1
2
3
4
5
6
7
8
9
# 局部安装 Gulp 3.9.1
$ npm install gulp@3.9.1

# 局部安装 Babel 6
$ npm install gulp-babel@7 babel-core babel-preset-env babel-loader babel-preset-es2015 --save

# 局部安装其他模块
1)将上面其他模块的依赖信息添加到博客根目录下的package.json文件中
2)然后在博客的根目录下,执行局部安装命令:npm install

创建 Gulp 配置文件

在 Hexo 博客的根目录下创建 gulpfile.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
var gulp = require('gulp');
var gutil = require('gulp-util');
var clean = require('gulp-clean');
var debug = require('gulp-debug');
var cache = require('gulp-cache');
var babel = require('gulp-babel');
var uglify = require('gulp-uglify');
var changed = require('gulp-changed');
var htmlmin = require('gulp-htmlmin');
var imagemin = require('gulp-imagemin');
var htmlclean = require('gulp-htmlclean');
var minifycss = require('gulp-minify-css');
var pngquant = require('imagemin-pngquant');

// 压缩css文件
gulp.task('minify-css', function() {
return gulp.src('./public/css/**/*.css')
.pipe(minifycss())
.pipe(gulp.dest('./public/css'));
});

// 压缩js文件,支持将ES6代码转换成ES5代码
gulp.task('minify-js', function() {
return gulp.src('./public/lib/**/*.js')
.pipe(babel({
presets: ['es2015']
}))
.pipe(uglify())
.pipe(gulp.dest('./public/lib'));
});

// 压缩html文件
gulp.task('minify-html', function() {
return gulp.src('./public/**/*.html')
.pipe(htmlclean())
.pipe(htmlmin({
removeComments: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
}))
.pipe(gulp.dest('./public'));
});

// 压缩图片(深度压缩)
gulp.task('minify-images', function() {
gulp.src('./public/asset/**/*.{png,jpg,gif,ico}')
.pipe(cache(imagemin({ //启用缓存,只压缩发生变化的图片
progressive: true, //是否无损压缩jpg图片
interlaced: false, //是否隔行扫描gif进行渲染
svgoPlugins: [{removeViewBox: false}], //是否移除svg的viewbox属性
multipass: false, //是否多次优化svg直到完全优化
optimizationLevel: 5, //优化等级,取值范围:0-7,默认值:3
use: [pngquant()] //使用pngquant深度压缩png图片的imagemin插件
})))
.pipe(gulp.dest('./public/asset'));
});

// gulp3的写法
gulp.task('default', ['minify-css', 'minify-js', 'minify-images', 'minify-html']);

Gulp 执行压缩任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 进入博客根目录
$ cd /blogroot

# 执行指定的压缩任务(如压缩CSS)
$ gulp minify-css

# 执行全部压缩任务
$ gulp
[09:35:19] Using gulpfile /blogroot/gulpfile.js
[09:35:19] Starting 'minify-css'...
[09:35:19] Starting 'minify-js'...
[09:35:19] Starting 'minify-images'...
[09:35:19] Finished 'minify-images' after 6.59 ms
[09:35:19] Starting 'minify-html'...
[09:35:20] Finished 'minify-css' after 739 ms
[09:35:27] Finished 'minify-js' after 7.78 s
[09:40:51] Finished 'minify-html' after 5.52 min
[09:40:51] Starting 'default'...
[09:40:51] Finished 'default' after 34 μs
[09:40:52] gulp-imagemin: Minified 122 images

Gulp 使用优化

Gulp 只压缩修改过的文件

  • 上面的代码,默认会压缩扫描得到的所有文件(除了图片以外),效率和性能都非常低。此时可以安装 gulp-changed 模块,控制 Gulp 只压缩修改过的文件(代码如下)。
  • 每次运行下面的代码之后,如果接着执行 hexo clean 命令,再执行压缩操作会发现 Gulp 依然会压缩所有文件。建议配合使用 gulp-cache 或者 gulp-cached 模块来解决压缩缓存的问题,也可以自行实现压缩缓存机制(内存缓存 + 磁盘缓存)。
  • 如果压缩缓存方案依赖文件的 Hash 值,且 Hexo 使用了 Yilia 主题,则必须保证 Hexo 生成的标签(Tag)列表是有顺序的,否则 Hexo 每次执行 cleangenerate 操作后,HTML 文件的 Hash 值都不一样,最终导致每次都会压缩 HTML 文件;建议修改 Hexo 主题的 JS 代码,使每次生成的标签列表都一样。
  • 如果安装了第三方插件,可能也会影响文件的 Hash 值,例如:文章加密插件。
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
90
91
var gulp = require('gulp');
var gutil = require('gulp-util');
var clean = require('gulp-clean');
var debug = require('gulp-debug');
var cache = require('gulp-cache');
var babel = require('gulp-babel');
var uglify = require('gulp-uglify');
var changed = require('gulp-changed');
var htmlmin = require('gulp-htmlmin');
var imagemin = require('gulp-imagemin');
var htmlclean = require('gulp-htmlclean');
var minifycss = require('gulp-minify-css');
var pngquant = require('imagemin-pngquant');

// 压缩css源文件(只压缩修改过的源文件)
gulp.task('generate-minify-css', function() {
return gulp.src('./public/css/**/*.css')
.pipe(changed('/tmp/blog/css/', {extension:'.css', hasChanged: changed.compareContents}))
.pipe(minifycss())
.pipe(gulp.dest('/tmp/blog/css/'))
.pipe(debug({title: '压缩css源文件:'}));
});

// 拷贝经压缩的css源文件(只拷贝修改过的压缩源文件)
gulp.task('copy-minify-css', ['generate-minify-css'], function() {
return gulp.src('/tmp/blog/css/**/*.css')
.pipe(changed('./public/css/', {extension:'.css', hasChanged: changed.compareContents}))
.pipe(gulp.dest('./public/css/'))
.pipe(debug({title: '拷贝经压缩的css源文件:'}));
});

// 压缩js源文件(只压缩修改过的源文件),支持将ES6代码转换成ES5代码
gulp.task('generate-minify-js', function() {
return gulp.src('./public/lib/**/*.js')
.pipe(changed('/tmp/blog/lib/', {extension:'.js', hasChanged: changed.compareContents}))
.pipe(babel({presets: ['es2015']}))
.pipe(uglify())
.pipe(gulp.dest('/tmp/blog/lib/'))
.pipe(debug({title: '压缩js源文件:'}));
});

// 拷贝经压缩的js源文件(只拷贝修改过的压缩源文件)
gulp.task('copy-minify-js', ['generate-minify-js'], function() {
return gulp.src('/tmp/blog/lib/**/*.js')
.pipe(changed('./public/lib/', {extension:'.js', hasChanged: changed.compareContents}))
.pipe(gulp.dest('./public/lib/'))
.pipe(debug({title: '拷贝经压缩的js源文件:'}));
});

// 压缩html源文件(只压缩修改过的源文件)
gulp.task('generate-minify-html', function() {
return gulp.src('./public/**/*.html')
.pipe(changed('/tmp/blog/', {extension:'.html', hasChanged: changed.compareContents}))
.pipe(htmlclean())
.pipe(htmlmin({
removeComments: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
}))
.pipe(gulp.dest('/tmp/blog/'))
.pipe(debug({title: '压缩html源文件:'}));
});

// 拷贝经压缩的html源文件(只拷贝修改过的压缩源文件)
gulp.task('copy-minify-html', ['generate-minify-html'], function() {
return gulp.src('/tmp/blog/**/*.html')
.pipe(changed('./public/', {extension:'.html', hasChanged: changed.compareContents}))
.pipe(gulp.dest('./public/'))
.pipe(debug({title: '拷贝经压缩的html源文件:'}));
});

// 压缩图片(深度压缩)
gulp.task('minify-images', function() {
gulp.src('./public/asset/**/*.{png,jpg,gif,ico}')
.pipe(cache(imagemin({ //启用缓存,只压缩发生变化的图片
progressive: true, //是否无损压缩jpg图片
interlaced: false, //是否隔行扫描gif进行渲染
svgoPlugins: [{removeViewBox: false}], //是否移除svg的viewbox属性
multipass: false, //是否多次优化svg直到完全优化
optimizationLevel: 5, //优化等级,取值范围:0-7,默认值:3
use: [pngquant()] //使用pngquant深度压缩png图片的imagemin插件
})))
.pipe(gulp.dest('./public/asset'))
.pipe(debug({title: '压缩图片:'}));
});

// gulp3的写法
gulp.task('minify-js', ['generate-minify-js', 'copy-minify-js']);
gulp.task('minify-css', ['generate-minify-css', 'copy-minify-css']);
gulp.task('minify-html', ['generate-minify-html', 'copy-minify-html']);

Gulp 直接调用 Hexo-Cli 命令

如果需要 Gulp 直接调用 Hexo 的命令(hexo-cli),即执行 Hexo 的 clean、generate、server、deploy 等操作,可以参考以下代码:

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
var Hexo = require('hexo');
var hexo = new Hexo(process.cwd(), {});

gulp.task('hexo-clean', function() {
return hexo.init().then(function() {
return hexo.call('clean', {
watch: false
}).then(function() {
return hexo.exit();
}).catch(function(err) {
return hexo.exit(err);
});
});
});

gulp.task('hexo-generate', function() {
return hexo.init().then(function() {
return hexo.call('generate', {
watch: false
}).then(function() {
return hexo.exit();
}).catch(function(err) {
return hexo.exit(err);
});
});
});

gulp.task('hexo-server', function() {
return hexo.init().then(function() {
return hexo.call('server', {});
}).catch(function(err) {
console.log(err);
});
});

gulp.task('hexo-deploy', function() {
return hexo.init().then(function() {
return hexo.call('deploy', {
watch: false
}).then(function() {
return hexo.exit();
}).catch(function(err) {
return hexo.exit(err);
});
});
});

显示 Gulp 的详细构建日志信息

为了方便调试 Gulp 的构建,可以安装 gulp-debug 模块,然后添加 JS 代码将构建日志信息打印出来,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
var debug = require('gulp-debug');

gulp.task('minify-js', function() {
return gulp.src('./lib/*.js')
.pipe(babel({
presets: ['es2015']
}))
.pipe(uglify())
.pipe(gulp.dest('./build'))
.pipe(debug({title: '压缩js文件:'}));
});

显示 Gulp 构建失败时的错误日志信息

默认情况下,如果 Gulp 执行构建任务出错,不一定都能显示详细的错误日志信息。为了方便定位问题,可以安装 gulp-util 模块,然后添加 JS 代码将错误日志信息打印出来,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
var gutil = require('gulp-util');

gulp.task('minify-js', function() {
return gulp.src('./lib/*.js')
.pipe(babel({
presets: ['es2015']
}))
.pipe(uglify())
.on('error', function(err) {
gutil.log(gutil.colors.red('[Error]'), err.toString());
})
.pipe(gulp.dest('./build'));
});

升级 Gulp 的版本

Gulp 版本升级

将 Gulp 从版本 3.9.1 升级到 4.0.2,可以使用以下命令:

1
2
3
# 全局安装版本检测、版本升级工具
$ npm install -g npm-check
$ npm install -g npm-upgrade
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 进入博客的根目录
$ cd /blog-root

# 检测Hexo哪些模块可以升级
$ npm-check

# 删除package-lock.json
# rm -rf package-lock.json

# 更新package.json
$ npm-upgrade

# 更新Hexo的模块
$ npm update --save

# 若出现依赖的问题,用以下命令检查一下,然后把报错的统一修复一下即可
$ npm audix

# 或者强制更新
$ npm update --save --force

Babel 版本升级

将 Babel 从版本 6 升级到 7,官方版本升级说明看这里

1
2
3
4
5
# 卸载 Babel 6
$ npm uninstall babel-core babel-loader babel-preset-env babel-preset-es2015 --save

# 安装 Babel 7
$ npm install gulp-babel @babel/core @babel/preset-env babel-preset-es2015 babel-core@6 --save

若 Gulp + Babel 执行文件压缩时,提示 [BABEL] Note: The code generator has deoptimised the styling of "xxx/xxx.js" as it exceeds the max of "500KB",可以添加 compact": false 参数来忽略该提示,示例代码如下:

1
2
3
4
5
6
7
8
9
gulp.task('minify-js', function() {
return gulp.src('./public/lib/**/*.js')
.pipe(babel({
"compact": false,
presets: ['es2015']
}))
.pipe(uglify())
.pipe(gulp.dest('./public/lib'));
});

Gulp 版本升级后的代码兼容处理

当 Gulp 从版本 3 升级到 4 之后,执行 Gulp 命令时若出现 Gulp error: The following tasks did not complete: Did you forget to signal async completion? 错误,则需要参考以下方式更改代码。Gulp 4 最大的变化就是不能像以前那样传递一个依赖任务列表,即不能再用 Gulp3 的方式指定依赖任务,需要配合使用 gulp.seriesgulp.parallel,因为 Gulp 任务现在只有两个参数。在 Gulp4 中必须明确通知 Gulp 任务已经完成,而在 Gulp3 中,通常不必要这么做,因为如果没有发出异步完成信号,那么当任务返回时,Gulp3 会认为它已经完成。通知 Gulp 任务已完成的另一个常见方法是 Return 一个流或者 Promise,异步任务还可以利用回调函数来通知 Gulp 任务已完成,相关资料可参考这里。下面是使用回调函数来通知 Gulp4 任务已完成的示例代码:

1
2
3
4
5
6
7
8
9
gulp.task('generate-minify-css', gulp.series(function(cb){
// 异步操作
gulp.src('./public/css/**/*.css')
.pipe(changed('/tmp/blog/css/', {extension:'.css', hasChanged: changed.compareContents}))
.pipe(minifycss())
.pipe(gulp.dest('/tmp/blog/css/'))
.pipe(debug({title: '压缩css源文件:'}));
cb();
}));

Gulp 与 Babel 版本升级后的完整依赖项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"@babel/core": "^7.14.8",
"@babel/preset-env": "^7.14.9",
"babel-core": "^6.26.3",
"babel-preset-es2015": "^6.24.1",
"gulp": "^4.0.2",
"gulp-babel": "^7.0.1",
"gulp-callback": "0.0.3",
"gulp-changed": "^4.0.2",
"gulp-clean": "^0.4.0",
"gulp-debug": "^4.0.0",
"gulp-htmlclean": "^2.7.22",
"gulp-htmlmin": "^5.0.1",
"gulp-imagemin": "^7.1.0",
"gulp-minify-css": "^1.2.4",
"gulp-uglify": "^3.0.2",
"gulp-util": "^3.0.8",
"imagemin-pngquant": "^9.0.2",

常见问题

压缩图片出错

执行 gulp generate-minify-images 任务(压缩图片)时,终端输出如下的错误信息:

1
2
3
4
5
6
7
8
[13:55:24] Finished 'generate-minify-images' after 1.2 s
(node:3702) UnhandledPromiseRejectionWarning: Error: spawn /usr/local/hexo-blog/node_modules/mozjpeg/vendor/cjpeg ENOENT
at Process.ChildProcess._handle.onexit (internal/child_process.js:269:19)
at onErrorNT (internal/child_process.js:465:16)
at processTicksAndRejections (internal/process/task_queues.js:80:21)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:3702) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:3702) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

可以执行以下命令来解决,这里强烈建议在本地连接上 VPN 后,再执行 npm install 命令,这样可以确保所有下载的文件就都是完整的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 文件授权
$ chmod 755 -R ~/.npm

# 进入博客的根目录
$ cd /blog-root

# 删除所有模块文件
$ rm -rf node_modules

# 重建
$ npm rebuid

# 安装模块
$ npm install