Gulp:Web 开发人员最大化网站速度的秘密武器
已发表: 2022-03-11我们中的许多人必须处理用于生产的基于 Web 的项目,这些项目向公众提供各种服务。 在处理此类项目时,能够快速构建和部署我们的代码非常重要。 快速做某事通常会导致错误,特别是如果一个过程是重复的,因此尽可能自动化这样一个过程是一个好习惯。
在这篇文章中,我们将着眼于一个工具,它可以成为我们实现这种自动化的一部分。 这个工具是一个名为 Gulp.js 的 npm 包。 为了熟悉本文中使用的基本 Gulp.js 术语,请参阅我们的 Toptal 开发人员之一 Antonios Minas 之前在博客上发布的“使用 Gulp 进行 JavaScript 自动化简介”。 我们将假设您对 npm 环境有基本的了解,因为它在本文中广泛用于安装包。
服务前端资产
在继续之前,让我们退后几步,大致了解一下 Gulp.js 可以为我们解决的问题。 许多基于 Web 的项目具有前端 JavaScript 文件,这些文件提供给客户端,以便为网页提供各种功能。 通常还有一组 CSS 样式表也提供给客户端。 有时在查看网站或 Web 应用程序的源代码时,我们可以看到如下代码:
<link href="css/main.css" rel="stylesheet"> <link href="css/custom.css" rel="stylesheet"> <script src="js/jquery.min.js"></script> <script src="js/site.js"></script> <script src="js/module1.js"></script> <script src="js/module2.js"></script>
这段代码有一些问题。 它引用了两个独立的 CSS 样式表和四个独立的 JavaScript 文件。 这意味着服务器总共要向服务器发出六个请求,并且每个请求都必须在页面准备好之前单独加载一个资源。 这对于 HTTP/2 来说不是什么问题,因为 HTTP/2 引入了并行性和标头压缩,但这仍然是一个问题。 它增加了加载此页面所需的总流量并降低了用户体验的质量,因为加载文件需要更长的时间。 在 HTTP 1.1 的情况下,它还会占用网络并减少可用请求通道的数量。 将 CSS 和 JavaScript 文件合并到一个单独的包中会更好。 这样一来,总共只有两个请求。 提供这些文件的缩小版本也很好,这些文件通常比原始文件小得多。 如果任何资产被缓存,我们的 Web 应用程序也可能会中断,并且客户端会收到过时的版本。
解决其中一些问题的一种原始方法是使用文本编辑器手动将每种类型的资产组合成一个包,然后通过压缩器服务运行结果,例如 http://jscompress.com/。 事实证明,在开发过程中不断地这样做是非常乏味的。 一个轻微但值得怀疑的改进是使用 GitHub 上可用的软件包之一托管我们自己的 minifier 服务器。 然后我们可以做一些看起来有点类似于以下的事情:
<script src="min/f=js/site.js,js/module1.js"></script>
这将向我们的客户端提供缩小文件,但它不会解决缓存问题。 它还会导致服务器上的额外负载,因为我们的服务器本质上必须在每次请求时重复地连接和缩小所有源文件。
使用 Gulp.js 实现自动化
当然,我们可以比这两种方法中的任何一种做得更好。 我们真正想要的是自动化捆绑并将其包含在我们项目的构建阶段。 我们希望最终得到已经缩小并准备好提供服务的预构建资产包。 我们还希望强制客户端在每次请求时接收我们捆绑资产的最新版本,但我们仍然希望尽可能利用缓存。 对我们来说幸运的是,Gulp.js 可以处理这个问题。 在本文的其余部分,我们将构建一个利用 Gulp.js 的强大功能来连接和缩小文件的解决方案。 当有更新时,我们还将使用插件来破坏缓存。
我们将在示例中创建以下目录和文件结构:
public/ |- build/ |- js/ |- bundle-{hash}.js |- css/ |- stylesheet-{hash}.css assets/ |- js/ |- vendor/ |- jquery.js |- site.js |- module1.js |- module2.js |- css/ |- main.css |- custom.css gulpfile.js package.json
gulpfile.js 文件是我们定义 Gulp 将为我们执行的任务的地方。 npm 使用package.json
来定义我们应用程序的包并跟踪我们将安装的依赖项。 公共目录应该配置为面向网络。 assets 目录是我们将存储源文件的地方。 要在项目中使用 Gulp,我们需要通过 npm 安装它并将其保存为项目的开发人员依赖项。 我们还想从 Gulp 的concat
插件开始,它允许我们将多个文件连接成一个。
要安装这两个项目,我们将运行以下命令:
npm install --save-dev gulp gulp-concat
接下来,我们将要开始编写 gulpfile.js 的内容。
var gulp = require('gulp'); var concat = require('gulp-concat'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(gulp.dest('public/build/js')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(gulp.dest('public/build/css')); }); gulp.task('default', ['pack-js', 'pack-css']);
在这里,我们正在加载 gulp 库及其 concat 插件。 然后我们定义三个任务。
第一个任务 ( pack-js
) 定义了将多个 JavaScript 源文件压缩到一个包中的过程。 我们列出了源文件,它们将按照指定的顺序进行全局、读取和连接。 我们通过管道将其输入到 concat 插件中,以获得一个名为bundle.js
的最终文件。 最后,我们告诉 gulp 将文件写入public/build/js
。
第二个任务 ( pack-css
) 与上面做同样的事情,但针对 CSS 样式表。 它告诉 Gulp 将连接的输出作为stylesheet.css
存储在public/build/css
中。
第三个任务( default
)是 Gulp 在我们不带参数调用它时运行的任务。 在第二个参数中,我们传递了默认任务运行时要执行的其他任务的列表。
让我们使用我们通常使用的任何源代码编辑器将此代码粘贴到 gulpfile.js 中,然后将文件保存到应用程序根目录。
接下来,我们将打开命令行并运行:
gulp
如果我们在运行这个命令后查看我们的文件,我们会发现两个新文件: public/build/js/bundle.js
和public/build/css/stylesheet.css
。 它们是我们源文件的串联,解决了部分原始问题。 但是,它们没有被缩小,并且还没有缓存破坏。 让我们添加自动缩小。
优化内置资产
我们将需要两个新插件。 要添加它们,我们将运行以下命令:
npm install --save-dev gulp-clean-css gulp-minify
第一个插件用于缩小 CSS,第二个插件用于缩小 JavaScript。 第一个使用 clean-css 包,第二个使用 UglifyJS2 包。 我们将首先在 gulpfile.js 中加载这两个包:
var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css');
然后,在将输出写入磁盘之前,我们需要在任务中使用它们:
.pipe(minify()) .pipe(cleanCss())
gulpfile.js 现在应该如下所示:
var gulp = require('gulp'); var concat = require('gulp-concat'); var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(minify()) .pipe(gulp.dest('public/build/js')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(cleanCss()) .pipe(gulp.dest('public/build/css')); }); gulp.task('default', ['pack-js', 'pack-css']);
让我们再次运行 gulp。 我们将看到文件stylesheet.css
以缩小格式保存,而文件bundle.js
保存。 我们会注意到我们现在还有 bundle-min.js,它被缩小了。 我们只需要缩小文件,并且希望将其保存为bundle.js
,因此我们将使用附加参数修改我们的代码:

.pipe(minify({ ext:{ min:'.js' }, noSource: true }))
根据 gulp-minify 插件文档(https://www.npmjs.com/package/gulp-minify),这将为缩小版本设置所需的名称,并告诉插件不要创建包含原始源的版本。 如果我们删除构建目录的内容并再次从命令行运行 gulp,我们最终会得到两个缩小的文件。 我们刚刚完成了构建过程的缩小阶段。
缓存破坏
接下来,我们将要添加缓存破坏,我们需要为此安装一个插件:
npm install --save-dev gulp-rev
并在我们的 gulp 文件中要求它:
var rev = require('gulp-rev');
使用插件有点棘手。 我们必须首先通过插件管道缩小输出。 然后,我们必须在将结果写入磁盘后再次调用插件。 该插件重命名文件,以便用唯一的哈希标记它们,并且它还创建一个清单文件。 清单文件是一个映射,我们的应用程序可以使用它来确定我们应该在 HTML 代码中引用的最新文件名。 在我们修改 gulp 文件后,它最终应该是这样的:
var gulp = require('gulp'); var concat = require('gulp-concat'); var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css'); var rev = require('gulp-rev'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(minify({ ext:{ min:'.js' }, noSource: true })) .pipe(rev()) .pipe(gulp.dest('public/build/js')) .pipe(rev.manifest()) .pipe(gulp.dest('public/build')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(cleanCss()) .pipe(rev()) .pipe(gulp.dest('public/build/css')) .pipe(rev.manifest()) .pipe(gulp.dest('public/build')); }); gulp.task('default', ['pack-js', 'pack-css']);
让我们删除构建目录的内容并再次运行 gulp。 我们会发现我们现在有两个文件,每个文件名都附加了标签,还有一个 manifest.json 保存到public/build
。 如果我们打开清单文件,我们会看到它只引用了我们的缩小和标记文件之一。 发生的情况是每个任务都编写一个单独的清单文件,其中一个最终覆盖另一个。 我们将需要使用其他参数修改任务,这些参数将告诉他们查找现有清单文件并将新数据合并到其中(如果存在)。 它的语法有点复杂,所以让我们看看代码应该是什么样子,然后再过一遍:
var gulp = require('gulp'); var concat = require('gulp-concat'); var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css'); var rev = require('gulp-rev'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(minify({ ext:{ min:'.js' }, noSource: true })) .pipe(rev()) .pipe(gulp.dest('public/build/js')) .pipe(rev.manifest('public/build/rev-manifest.json', { merge: true })) .pipe(gulp.dest('')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(cleanCss()) .pipe(rev()) .pipe(gulp.dest('public/build/css')) .pipe(rev.manifest('public/build/rev-manifest.json', { merge: true })) .pipe(gulp.dest('')); }); gulp.task('default', ['pack-js', 'pack-css']);
我们首先将输出传递给rev.manifest()
。 这将创建标记文件,而不是我们之前拥有的文件。 我们提供rev-manifest.json
的所需路径,并告诉rev.manifest()
合并到现有文件(如果存在)。 然后我们告诉 gulp 将清单写入当前目录,此时该目录将是 public/build。 路径问题是由 GitHub 上更详细讨论的错误引起的。
我们现在有自动缩小、标记文件和清单文件。 所有这些将使我们能够更快地将文件交付给用户,并在我们进行修改时破坏他们的缓存。 不过只剩下两个问题了。
第一个问题是,如果我们对源文件进行任何修改,我们将获得新标记的文件,但旧的文件也会保留在那里。 我们需要一些方法来自动删除旧的缩小文件。 让我们使用一个允许我们删除文件的插件来解决这个问题:
npm install --save-dev del
我们将在我们的代码中需要它并定义两个新任务,一个用于每种类型的源文件:
var del = require('del'); gulp.task('clean-js', function () { return del([ 'public/build/js/*.js' ]); }); gulp.task('clean-css', function () { return del([ 'public/build/css/*.css' ]); });
然后,我们将确保新任务在我们的两个主要任务之前完成运行:
gulp.task('pack-js', ['clean-js'], function () { gulp.task('pack-css', ['clean-css'], function () {
如果我们在修改后再次运行gulp
,我们将只有最新的缩小文件。
第二个问题是我们不想在每次进行更改时都继续运行 gulp。 为了解决这个问题,我们需要定义一个观察者任务:
gulp.task('watch', function() { gulp.watch('assets/js/**/*.js', ['pack-js']); gulp.watch('assets/css/**/*.css', ['pack-css']); });
我们还将更改默认任务的定义:
gulp.task('default', ['watch']);
如果我们现在从命令行运行 gulp,我们会发现它在调用时不再构建任何东西。 这是因为它现在调用 watcher 任务,该任务将监视我们的源文件是否有任何更改,并且仅在它检测到更改时才构建。 如果我们尝试更改任何源文件,然后再次查看控制台,我们将看到pack-js
和pack-css
任务及其依赖项自动运行。
现在,我们所要做的就是在我们的应用程序中加载 manifest.json 文件并从中获取标记的文件名。 我们如何做到这一点取决于我们特定的后端语言和技术堆栈,并且实现起来非常简单,所以我们不会详细讨论它。 但是,一般的想法是我们可以将清单加载到数组或对象中,然后定义一个帮助函数,允许我们以类似于以下方式从模板调用版本化资产:
gulp('bundle.js')
一旦我们这样做了,我们就不必再担心文件名中的标签更改了,我们将能够专注于编写高质量的代码。
本文的最终源代码以及一些示例资产可以在这个 GitHub 存储库中找到。
结论
在本文中,我们讨论了如何为我们的构建过程实现基于 Gulp 的自动化。 我希望这对您有所帮助,并允许您在自己的应用程序中开发更复杂的构建过程。
请记住,Gulp 只是可用于此目的的工具之一,还有许多其他工具,例如 Grunt、Browserify 和 Webpack。 它们的目的和可以解决的问题范围各不相同。 有些可以解决 Gulp 无法解决的问题,例如将 JavaScript 模块与可以按需加载的依赖项捆绑在一起。 这被称为“代码拆分”,它是对在每个页面上提供一个大文件的想法的改进。 这些工具非常复杂,但将来可能会被覆盖。 在接下来的文章中,我们将讨论如何自动部署我们的应用程序。