Grunt 生态系统非常庞大,并且一直在增长。由于拥有数量庞大的插件可供选择,因此,你可以利用 Grunt 自动完成任何事,并且花费最少的代价。如果找不到你所需要的插件,那就自己动手创造一个 Grunt 插件,然后将其发布到 npm 上吧。
Grunt和 Grunt 插件是通过 npm 安装并管理的,npm是 Node.js 的包管理器。
Grunt 0.4.x 必须配合 Node.js >= 0.8.0
版本使用。
在安装 Grunt 前,请确保当前环境中所安装的 npm 已经是最新版本,执行 npm update -g npm
指令进行升级(在某些系统中可能需要 sudo
指令)。
安装 CLI
在继续学习前,你需要先将Grunt命令行(CLI)安装到全局环境中。安装时可能需要使用sudo(针对OSX、*nix、BSD等系统中)权限或者作为管理员(对于Windows环境)来执行以下命令。
npm install -g grunt-cli
上述命令执行完后,grunt
命令就被加入到你的系统路径中了,以后就可以在任何目录下执行此命令了。
注意,安装grunt-cli
并不等于安装了 Grunt!Grunt CLI的任务很简单:调用与Gruntfile
在同一目录中 Grunt。这样带来的好处是,允许你在同一个系统上同时安装多个版本的 Grunt。
这样就能让多个版本的 Grunt 同时安装在同一台机器上。
CLI如何工作的
每次运行grunt
时,他就利用node提供的require()
系统查找本地安装的 Grunt。正是由于这一机制,你可以在项目的任意子目录中运行grunt
。
如果找到一份本地安装的 Grunt,CLI就将其加载,并传递Gruntfile
中的配置信息,然后执行你所指定的任务。
拿一份现有的 Grunt 项目练手
假定Grunt CLI已经正确安装,并且已经有一份配置好package.json
和 Gruntfile
文件的项目了,接下来就很容易拿Grunt练手了:
- 将命令行的当前目录转到项目的根目录下。
- 执行
npm install
命令安装项目依赖的库。 - 执行
grunt
命令。
OK,就是这么简单。还可以通过grunt --help
命令列出所有已安装的Grunt任务(task),但是一般更建议去查看项目的文档以获取帮助信息。
Gruntfile: 此文件被命名为 Gruntfile.js
或 Gruntfile.coffee
,用来配置或定义任务(task)并加载Grunt插件的。 此文档中提到的 Gruntfile
其实说的是一个文件,文件名是 Gruntfile.js
或 Gruntfile.coffee
。
package.json
package.json
应当放置于项目的根目录中,与Gruntfile
在同一目录中,并且应该与项目的源代码一起被提交。在上述目录(package.json
所在目录)中运行npm install
将依据package.json
文件中所列出的每个依赖来自动安装适当版本的依赖。
下面列出了几种为你的项目创建package.json
文件的方式:
- 大部分 grunt-init 模版都会自动创建特定于项目的
package.json
文件。 - npm init命令会创建一个基本的
package.json
文件。 - 复制下面的案例,并根据需要做扩充,参考此说明.
{
"name": "my-project-name",
"version": "0.1.0",
"devDependencies": {
"grunt": "~0.4.5",
"grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-nodeunit": "~0.4.1",
"grunt-contrib-uglify": "~0.5.0"
}
}
安装Grunt 和 grunt插件
向已经存在的package.json
文件中添加Grunt和grunt插件的最简单方式是通过npm install <module> --save-dev
命令。此命令不光安装了<module>
,还会自动将其添加到devDependencies 配置段中,遵循tilde version range格式。
例如,下面这条命令将安装Grunt最新版本到项目目录中,并将其添加到devDependencies内:
npm install grunt --save-dev
同样,grunt插件和其它node模块都可以按相同的方式安装。下面展示的实例就是安装 JSHint 任务模块:
npm install grunt-contrib-jshint --save-dev
在 Grunt 插件 页面可以看到当前可用的 Grunt 插件,他们可以直接在项目中安装并使用。
安装插件之后,请务必确保将更新之后的 package.json
文件提交到项目仓库中。
Gruntfile
Gruntfile.js
或 Gruntfile.coffee
文件是有效的 JavaScript 或 CoffeeScript 文件,应当放在你的项目根目录中,和package.json
文件在同一目录层级,并和项目源码一起加入源码管理器。
Gruntfile由以下几部分构成:
- "wrapper" 函数
- 项目与任务配置
- 加载grunt插件和任务
- 自定义任务
Gruntfile文件案例
在下面列出的这个 Gruntfile
中,package.json
文件中的项目元数据(metadata)被导入到 Grunt 配置中, grunt-contrib-uglify 插件中的uglify
任务(task)被配置为压缩(minify)源码文件并依据上述元数据动态生成一个文件头注释。当在命令行中执行 grunt
命令时,uglify
任务将被默认执行。
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
uglify: {
options: {
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
},
build: {
src: 'src/<%= pkg.name %>.js',
dest: 'build/<%= pkg.name %>.min.js'
}
}
});
// 加载包含 "uglify" 任务的插件。
grunt.loadNpmTasks('grunt-contrib-uglify');
// 默认被执行的任务列表。
grunt.registerTask('default', ['uglify']);
};
前面已经向你展示了整个 Gruntfile
,接下来将详细解释其中的每一部分。
"wrapper" 函数
每一份 Gruntfile
(和grunt插件)都遵循同样的格式,你所书写的Grunt代码必须放在此函数内:
module.exports = function(grunt) {
// Do grunt-related things in here
};
项目和任务配置
大部分的Grunt任务都依赖某些配置数据,这些数据被定义在一个object内,并传递给grunt.initConfig方法。
在下面的案例中,grunt.file.readJSON('package.json')
将存储在package.json
文件中的JSON元数据引入到grunt config中。 由于<% %>
模板字符串可以引用任意的配置属性,因此可以通过这种方式来指定诸如文件路径和文件列表类型的配置数据,从而减少一些重复的工作。
你可以在这个配置对象中(传递给initConfig()方法的对象)存储任意的数据,只要它不与你任务配置所需的属性冲突,否则会被忽略。此外,由于这本身就是JavaScript,你不仅限于使用JSON;你可以在这里使用任意的有效的JS代码。如果有必要,你甚至可以以编程的方式生成配置。
与大多数task一样,grunt-contrib-uglify 插件中的uglify
任务要求它的配置被指定在一个同名属性中。在这里有一个例子, 我们指定了一个banner
选项(用于在文件顶部生成一个注释),紧接着是一个单一的名为build
的uglify目标,用于将一个js文件压缩为一个目标文件。
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
uglify: {
options: {
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
},
build: {
src: 'src/<%= pkg.name %>.js',
dest: 'build/<%= pkg.name %>.min.js'
}
}
});
加载 Grunt 插件和任务
像 concatenation、[minification]、grunt-contrib-uglify 和 linting这些常用的任务(task)都已经以grunt插件的形式被开发出来了。只要在 package.json
文件中被列为dependency(依赖)的包,并通过npm install
安装之后,都可以在Gruntfile
中以简单命令的形式使用:
// 加载能够提供"uglify"任务的插件。
grunt.loadNpmTasks('grunt-contrib-uglify');
注意: grunt --help
命令将列出所有可用的任务。
自定义任务
通过定义 default
任务,可以让Grunt默认执行一个或多个任务。在下面的这个案例中,执行 grunt
命令时如果不指定一个任务的话,将会执行uglify
任务。这和执行grunt uglify
或者 grunt default
的效果一样。default
任务列表数组中可以指定任意数目的任务(可以带参数)。
// Default task(s).
grunt.registerTask('default', ['uglify']);
如果Grunt插件中的任务(task)不能满足你的项目需求,你还可以在Gruntfile
中自定义任务(task)。例如,在下面的 Gruntfile
中自定义了一个default
任务,并且他甚至不依赖任务配置:
module.exports = function(grunt) {
// A very basic default task.
grunt.registerTask('default', 'Log some stuff.', function() {
grunt.log.write('Logging some stuff...').ok();
});
};
特定于项目的任务不必在 Gruntfile
中定义。他们可以定义在外部.js
文件中,并通过grunt.loadTasks 方法加载。
gulp
1.gulp是什么?
Gulp是基于node的一个自动化构建工具,开发者可以使用它构建自动化工作流程(前端集成开发环境)。例如:网页自动刷新,CSS预处理,代码检测,图片压缩等功能,只需要简单的命令行就可以全部完成。使用它,可以简化工作,让你把重点放在功能的开发上,同时减少人为失误,提高开发的效率和质量。
Gulpfile 详解
gulpfile 是项目目录下名为 gulpfile.js
(或者首字母大写 Gulpfile.js
,就像 Makefile 一样命名)的文件,在运行 gulp
命令时会被自动加载。在这个文件中,你经常会看到类似 src()
、dest()
、series()
或 parallel()
函数之类的 gulp API,除此之外,纯 JavaScript 代码或 Node 模块也会被使用。任何导出(export)的函数都将注册到 gulp 的任务(task)系统中。
Gulpfile 转译
你可以使用需要转译的编程语言来书写 gulpfile 文件,例如 TypeScript 或 Babel,通过修改 gulpfile.js
文件的扩展名来表明所用的编程语言并安装对应的转译模块。
- 对于 TypeScript,重命名为
gulpfile.ts
并安装 ts-node 模块。 - 对于 Babel,重命名为
gulpfile.babel.js
并安装 @babel/register 模块。
针对此功能的高级知识和已支持的扩展名的完整列表,请参考 gulpfile 转译 文档。
Gulpfile 分割
大部分用户起初是将所有业务逻辑都写到一个 gulpfile 文件中。随着文件的变大,可以将此文件重构为数个独立的文件。
每个任务(task)可以被分割为独立的文件,然后导入(import)到 gulpfile 文件中并组合。这不仅使事情变得井然有序,而且可以对每个任务(task)进行单独测试,或者根据条件改变组合。
Node 的模块解析功能允许你将 gulpfile.js
' 文件替换为同样命名为 gulpfile.js
的目录,该目录中包含了一个名为 index.js
的文件,该文件被当作 gulpfile.js
使用。并且,该目录中还可以包含各个独立的任务(task)模块。
导出任务
任务(tasks)可以是 public(公开) 或 private(私有) 类型的。
- 公开任务(Public tasks) 从 gulpfile 中被导出(export),可以通过
gulp
命令直接调用。 - 私有任务(Private tasks) 被设计为在内部使用,通常作为
series()
或parallel()
组合的组成部分。
一个私有(private)类型的任务(task)在外观和行为上和其他任务(task)是一样的,但是不能够被用户直接调用。如需将一个任务(task)注册为公开(public)类型的,只需从 gulpfile 中导出(export)即可。
const { series } = require('gulp');
// `clean` 函数并未被导出(export),因此被认为是私有任务(private task)。
// 它仍然可以被用在 `series()` 组合中。
function clean(cb) {
// body omitted
cb();
}
// `build` 函数被导出(export)了,因此它是一个公开任务(public task),并且可以被 `gulp` 命令直接调用。
// 它也仍然可以被用在 `series()` 组合中。
function build(cb) {
// body omitted
cb();
}
exports.build = build;
exports.default = series(clean, build);
在以前的 gulp 版本中,task()
方法用来将函数注册为任务(task)。虽然这个 API 依旧是可以使用的,但是 导出(export)将会是主要的注册机制,除非遇到 export 不起作用的情况。
组合任务
Gulp 提供了两个强大的组合方法: series()
和 parallel()
,允许将多个独立的任务组合为一个更大的操作。这两个方法都可以接受任意数目的任务(task)函数或已经组合的操作。series()
和 parallel()
可以互相嵌套至任意深度。
如果需要让任务(task)按顺序执行,请使用 series()
方法。
任务(task)完成通知
当从任务(task)中返回 stream、promise、event emitter、child process 或 observable 时,成功或错误值将通知 gulp 是否继续执行或结束。如果任务(task)出错,gulp 将立即结束执行并显示该错误。
当使用 series()
组合多个任务(task)时,任何一个任务(task)的错误将导致整个任务组合结束,并且不会进一步执行其他任务。当使用 parallel()
组合多个任务(task)时,一个任务的错误将结束整个任务组合的结束,但是其他并行的任务(task)可能会执行完,也可能没有执行完。
返回 stream
const { src, dest } = require('gulp');
function streamTask() {
return src('*.js')
.pipe(dest('output'));
}
exports.default = streamTask;
返回 promise
function promiseTask() {
return Promise.resolve('the value is ignored');
}
exports.default = promiseTask;
返回 event emitter
const { EventEmitter } = require('events');
function eventEmitterTask() {
const emitter = new EventEmitter();
// Emit has to happen async otherwise gulp isn't listening yet
setTimeout(() => emitter.emit('finish'), 250);
return emitter;
}
exports.default = eventEmitterTask;
返回 child process
const { exec } = require('child_process');
function childProcessTask() {
return exec('date');
}
exports.default = childProcessTask;
返回 observable
const { Observable } = require('rxjs');
function observableTask() {
return Observable.of(1, 2, 3);
}
exports.default = observableTask;
使用 callback
如果任务(task)不返回任何内容,则必须使用 callback 来指示任务已完成。在如下示例中,callback 将作为唯一一个名为 cb()
的参数传递给你的任务(task)。
function callbackTask(cb) {
// `cb()` should be called by some async work
cb();
}
exports.default = callbackTask;
如需通过 callback 把任务(task)中的错误告知 gulp,请将 Error
作为 callback 的唯一参数。
function callbackError(cb) {
// `cb()` should be called by some async work
cb(new Error('kaboom'));
}
exports.default = callbackError;
然而,你通常会将此 callback 函数传递给另一个 API ,而不是自己调用它。
const fs = require('fs');
function passingCallback(cb) {
fs.access('gulpfile.js', cb);
}
exports.default = passingCallback;
gulp 不再支持同步任务(Synchronous tasks)
gulp 不再支持同步任务(Synchronous tasks)了。因为同步任务常常会导致难以调试的细微错误,例如忘记从任务(task)中返回 stream。
当你看到 "Did you forget to signal async completion?" 警告时,说明你并未使用前面提到的返回方式。你需要使用 callback 或返回 stream、promise、event emitter、child process、observable 来解决此问题。
使用 async/await
如果不使用前面提供到几种方式,你还可以将任务(task)定义为一个 async 函数,它将利用 promise 对你的任务(task)进行包装。这将允许你使用 await
处理 promise,并使用其他同步代码。
const fs = require('fs');
async function asyncAwaitTask() {
const { version } = fs.readFileSync('package.json');
console.log(version);
await Promise.resolve('some result');
}
exports.default = asyncAwaitTask;
处理文件
gulp 暴露了 src()
和 dest()
方法用于处理计算机上存放的文件。
src()
接受 glob 参数,并从文件系统中读取文件然后生成一个 Node 流(stream)。它将所有匹配的文件读取到内存中并通过流(stream)进行处理。
由 src()
产生的流(stream)应当从任务(task)中返回并发出异步完成的信号,就如 创建任务(task) 文档中所述。
const { src, dest } = require('gulp');
exports.default = function() {
return src('src/*.js')
.pipe(dest('output/'));
}
流(stream)所提供的主要的 API 是 .pipe()
方法,用于连接转换流(Transform streams)或可写流(Writable streams)。
const { src, dest } = require('gulp');
const babel = require('gulp-babel');
exports.default = function() {
return src('src/*.js')
.pipe(babel())
.pipe(dest('output/'));
}
dest()
接受一个输出目录作为参数,并且它还会产生一个 Node 流(stream),通常作为终止流(terminator stream)。当它接收到通过管道(pipeline)传输的文件时,它会将文件内容及文件属性写入到指定的目录中。gulp 还提供了 symlink()
方法,其操作方式类似 dest()
,但是创建的是链接而不是文件( 详情请参阅 symlink() )。
大多数情况下,利用 .pipe()
方法将插件放置在 src()
和 dest()
之间,并转换流(stream)中的文件。
向流(stream)中添加文件
src()
也可以放在管道(pipeline)的中间,以根据给定的 glob 向流(stream)中添加文件。新加入的文件只对后续的转换可用。如果 glob 匹配的文件与之前的有重复,仍然会再次添加文件。
这对于在添加普通的 JavaScript 文件之前先转换部分文件的场景很有用,添加新的文件后可以对所有文件统一进行压缩并混淆(uglifying)。
const { src, dest } = require('gulp');
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
exports.default = function() {
return src('src/*.js')
.pipe(babel())
.pipe(src('vendor/*.js'))
.pipe(uglify())
.pipe(dest('output/'));
}
分阶段输出
dest()
可以用在管道(pipeline)中间用于将文件的中间状态写入文件系统。当接收到一个文件时,当前状态的文件将被写入文件系统,文件路径也将被修改以反映输出文件的新位置,然后该文件继续沿着管道(pipeline)传输。
此功能可用于在同一个管道(pipeline)中创建未压缩(unminified)和已压缩(minified)的文件。
const { src, dest } = require('gulp');
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const rename = require('gulp-rename');
exports.default = function() {
return src('src/*.js')
.pipe(babel())
.pipe(src('vendor/*.js'))
.pipe(dest('output/'))
.pipe(uglify())
.pipe(rename({ extname: '.min.js' }))
.pipe(dest('output/'));
}
模式:流动(streaming)、缓冲(buffered)和空(empty)模式
src()
可以工作在三种模式下:缓冲(buffering)、流动(streaming)和空(empty)模式。这些模式可以通过对 src()
的 buffer
和 read
参数 进行设置。
- 缓冲(Buffering)模式是默认模式,将文件内容加载内存中。插件通常运行在缓冲(buffering)模式下,并且许多插件不支持流动(streaming)模式。
- 流动(Streaming)模式的存在主要用于操作无法放入内存中的大文件,例如巨幅图像或电影。文件内容从文件系统中以小块的方式流式传输,而不是一次性全部加载。如果需要流动(streaming)模式,请查找支持此模式的插件或自己编写。
- 空(Empty)模式不包含任何内容,仅在处理文件元数据时有用。
Glob 详解
glob 是由普通字符和/或通配字符组成的字符串,用于匹配文件路径。可以利用一个或多个 glob 在文件系统中定位文件。
src()
方法接受一个 glob 字符串或由多个 glob 字符串组成的数组作为参数,用于确定哪些文件需要被操作。glob 或 glob 数组必须至少匹配到一个匹配项,否则 src()
将报错。当使用 glob 数组时,将按照每个 glob 在数组中的位置依次执行匹配 - 这尤其对于取反(negative) glob 有用。
字符串片段与分隔符
字符串片段(segment)是指两个分隔符之间的所有字符组成的字符串。在 glob 中,分隔符永远是 /
字符 - 不区分操作系统 - 即便是在采用 \\
作为分隔符的 Windows 操作系统中。在 glob 中,\\
字符被保留作为转义符使用。
如下, * 被转义了,因此,* 将被作为一个普通字符使用,而不再是通配符了。
'glob_with_uncommon_\\*_character.js'
避免使用 Node 的 path
类方法来创建 glob,例如 path.join
。在 Windows 中,由于 Node 使用 \\
作为路径分隔符,因此将会产生一个无效的 glob。还要避免使用 __dirname
和 __filename
全局变量,由于同样的原因,process.cwd()
方法也要避免使用。
const invalidGlob = path.join(__dirname, 'src/*.js');
特殊字符: * (一个星号)
在一个字符串片段中匹配任意数量的字符,包括零个匹配。对于匹配单级目录下的文件很有用。
下面这个 glob 能够匹配类似 index.js
的文件,但是不能匹配类似 scripts/index.js
或 scripts/nested/index.js
的文件。
'*.js'
特殊字符: ** (两个星号)
在多个字符串片段中匹配任意数量的字符,包括零个匹配。 对于匹配嵌套目录下的文件很有用。请确保适当地限制带有两个星号的 glob 的使用,以避免匹配大量不必要的目录。
下面这个 glob 被适当地限制在 scripts/
目录下。它将匹配类似 scripts/index.js
、scripts/nested/index.js
和 scripts/nested/twice/index.js
的文件。
'scripts/**/*.js'
在上面的示例中,如果没有 scripts/
这个前缀做限制,node_modules
目录下的所有目录或其他目录也都将被匹配。
特殊字符: ! (取反)
由于 glob 匹配时是按照每个 glob 在数组中的位置依次进行匹配操作的,所以 glob 数组中的取反(negative)glob 必须跟在一个非取反(non-negative)的 glob 后面。第一个 glob 匹配到一组匹配项,然后后面的取反 glob 删除这些匹配项中的一部分。如果取反 glob 只是由普通字符组成的字符串,则执行效率是最高的。
['script/**/*.js', '!scripts/vendor/']
如果任何非取反(non-negative)的 glob 跟随着一个取反(negative) glob,任何匹配项都不会被删除。
['script/**/*.js', '!scripts/vendor/', 'scripts/vendor/react.js']
取反(negative) glob 可以作为对带有两个星号的 glob 的限制手段。
['**/*.js', '!node_modules/']
在上面的示例中,如果取反(negative)glob 是 !node_modules/**/*.js
,那么各匹配项都必须与取反 glob 进行比较,这将导致执行速度极慢。
匹配重叠(Overlapping globs)
两个或多个 glob 故意或无意匹配了相同的文件就被认为是匹配重叠(overlapping)了。如果在同一个 src()
中使用了会产生匹配重叠的 glob,gulp 将尽力去除重叠部分,但是在多个 src()
调用时产生的匹配重叠是不会被去重的。
文件监控
gulp api 中的 watch()
方法利用文件系统的监控程序(file system watcher)将 globs 与 任务(task) 进行关联。它对匹配 glob 的文件进行监控,如果有文件被修改了就执行关联的任务(task)。如果被执行的任务(task)没有触发 异步完成 信号,它将永远不会再次运行了。
此 API 的默认设置是基于通常的使用场景的,而且提供了内置的延迟和排队机制。
警告:避免同步任务
就像注册到任务系统中的任务(task)一样,与文件监控程序关联的任务(task)不能是同步任务(synchronous taks)。如果你将同步任务(task)关联到监控程序,则无法确定任务(task)的完成情况,任务(task)将不会再次运行(假定当前正在运行)。
由于文件监控程序会让你的 Node 进程保持持续运行,因此不会有错误或警告产生。由于进程没有退出,因此无法确定任务(task)是否已经完成还是运行了很久很久了。
可监控的事件
默认情况下,只要创建、更改或删除文件,文件监控程序就会执行关联的任务(task)。 如果你需要使用不同的事件,你可以在调用 watch()
方法时通过 events
参数进行指定。可用的事件有 'add'
、'addDir'
、'change'
、'unlink'
、'unlinkDir'
、'ready'
、'error'
。此外,还有一个 'all'
事件,它表示除 'ready'
和 'error'
之外的所有事件。
const { watch } = require('gulp');
// 所有事件都将被监控
watch('src/*.js', { events: 'all' }, function(cb) {
// body omitted
cb();
});
初次执行
调用 watch()
之后,关联的任务(task)是不会被立即执行的,而是要等到第一次文件修之后才执行。
如需在第一次文件修改之前执行,也就是调用 watch()
之后立即执行,请将 ignoreInitial
参数设置为 false
。
const { watch } = require('gulp');
// 关联的任务(task)将在启动时执行
watch('src/*.js', { ignoreInitial: false }, function(cb) {
// body omitted
cb();
});
队列
watch()
方法能够保证当前执行的任务(task)不会再次并发执行。当文件监控程序关联的任务(task)正在运行时又有文件被修改了,那么所关联任务的这次新的执行将被放到执行队列中等待,直到上一次关联任务执行完之后才能运行。每一次文件修改只产生一次关联任务的执行并放入队列中。
如需禁止队列,请将 queue
参数设置为 false
。
const { watch } = require('gulp');
// 每次文件修改之后关联任务都将执行(有可能并发执行)
watch('src/*.js', { queue: false }, function(cb) {
// body omitted
cb();
});
延迟
文件更改之后,只有经过 200 毫秒的延迟之后,文件监控程序所关联的任务(task)才会被执行。这是为了避免在同使更改许多文件时(例如查找和替换操作)过早启动任务(taks)的执行。
如需调整延迟时间,请为 delay
参数设置一个正整数。
const { watch } = require('gulp');
// 文件第一次修改之后要等待 500 毫秒才执行关联的任务
watch('src/*.js', { delay: 500 }, function(cb) {
// body omitted
cb();
});
使用监控程序实例
你可能不会使用到此功能,但是如果你需要对被修改的文件进行完全的掌控 (例如访问文件路径或元数据)请使用从 watch()
返回的 chokidar 实例。
注意: 返回的 chokidar 实例没有队列、延迟和异步完成(async completion)这些功能。
可选的依赖项
Gulp 有一个名为 fsevents 的可选依赖项,他是一个特定于 Mac 系统的文件监控程序。如果你看到安装 fsevents 时出现的警告信息 - "npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents" - 这并不是什么问题,忽略即可。 如果跳过 fsevents 的安装,将使用一个备用文件监控程序,后续在 gulpfile 中产生的任何错误都将与此警告无关。
npm
一、npm是什么
npm(node package manager)为你和你的团队打开了连接整个JavaScript天才世界的一扇大门。它是世界上最大的软件注册表,每星期大约有30亿次的下载量,包含超过600000个包(即代码模块)。来自各大洲的开源软件开发者使用npm互相分享和借鉴。包的结构使您能够轻松跟踪依赖项和版本。
下面是关于npm的快速介绍:
npm由三个独立的部分组成:
网站
注册表(registry)
命令行工具(CLI)
网站是开发者查找包(package)、设置参数以及管理npm使用体验的主要途径。
注册表是一个巨大的数据库,保存了每个包的信息。
CLI通过命令行或终端运行。开发者通过CLI与npm打交道。
二、用npm可以...
使软件包适应您的应用程序,或者将它们合并成现在的样子。
下载可以立即使用的独立工具。
运行包而不使用npx下载。
与任何npm用户,任何地方共享代码。
将代码限制为特定的开发人员。
组建虚拟团队。
管理多个版本的代码和代码依赖项。
更新基础代码时,可以轻松更新应用程序。
发现解决同一难题的多种方法。
查找其他正在处理类似问题的开发人员
pnpm
当使用 npm 或 Yarn 时,如果你有100个项目使用了某个依赖(dependency),就会有100份该依赖的副本保存在硬盘上。 而在使用 pnpm 时,依赖会被存储在内容可寻址的存储中,所以:
- 如果你用到了某依赖项的不同版本,那么只会将有差异的文件添加到仓库。 例如,如果某个包有100个文件,而它的新版本只改变了其中1个文件。那么
pnpm update
时只会向存储中心额外添加1个新文件,而不会因为仅仅一个文件的改变复制整新版本包的内容。 - 所有文件都会存储在硬盘上的某一位置。 当软件包被被安装时,包里的文件会硬链接到这一位置,而不会占用额外的磁盘空间。 这允许你跨项目地共享同一版本的依赖。
因此,您在磁盘上节省了大量空间,这与项目和依赖项的数量成正比,并且安装速度要快得多!
webpack
本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。
入口(entry)
入口起点(entry point) 指示 webpack 应该使用哪个模块,来作为构建其内部 依赖图(dependency graph) 的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
默认值是 ./src/index.js,但你可以通过在 webpack configuration 中配置 entry 属性,来指定一个(或多个)不同的入口起点。例如:
webpack.config.js
module.exports = {
entry: './path/to/my/entry/file.js',
};
输出(output)
output 属性告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js
,其他生成文件默认放置在 ./dist
文件夹中。
你可以通过在配置中指定一个 output
字段,来配置这些处理过程:
webpack.config.js
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js',
},
};
在上面的示例中,我们通过 output.filename
和 output.path
属性,来告诉 webpack bundle 的名称,以及我们想要 bundle 生成(emit)到哪里。可能你想要了解在代码最上面导入的 path 模块是什么,它是一个 Node.js 核心模块,用于操作文件路径
loader
webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。
Warning
webpack 的其中一个强大的特性就是能通过 import
导入任何类型的模块(例如 .css
文件),其他打包程序或任务执行器的可能并不支持。我们认为这种语言扩展是很有必要的,因为这可以使开发人员创建出更准确的依赖关系图。
在更高层面,在 webpack 的配置中,loader 有两个属性:
test
属性,识别出哪些文件会被转换。use
属性,定义出在进行转换时,应该使用哪个 loader。
webpack.config.js
const path = require('path');
module.exports = {
output: {
filename: 'my-first-webpack.bundle.js',
},
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
};
以上配置中,对一个单独的 module 对象定义了 rules
属性,里面包含两个必须属性:test
和 use
。这告诉 webpack 编译器(compiler) 如下信息:
loader
webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。
Warning
webpack 的其中一个强大的特性就是能通过 import
导入任何类型的模块(例如 .css
文件),其他打包程序或任务执行器的可能并不支持。我们认为这种语言扩展是很有必要的,因为这可以使开发人员创建出更准确的依赖关系图。
在更高层面,在 webpack 的配置中,loader 有两个属性:
test
属性,识别出哪些文件会被转换。use
属性,定义出在进行转换时,应该使用哪个 loader。
webpack.config.js
const path = require('path');
module.exports = {
output: {
filename: 'my-first-webpack.bundle.js',
},
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
};
以上配置中,对一个单独的 module 对象定义了 rules
属性,里面包含两个必须属性:test
和 use
。这告诉 webpack 编译器(compiler) 如下信息:
“嘿,webpack 编译器,当你碰到「在
require()
/import
语句中被解析为 '.txt' 的路径」时,在你对它打包之前,先 use(使用)raw-loader
转换一下。”
Warning
重要的是要记住,在 webpack 配置中定义 rules 时,要定义在 module.rules
而不是 rules
中。为了使你便于理解,如果没有按照正确方式去做,webpack 会给出警告。
Warning
请记住,使用正则表达式匹配文件时,你不要为它添加引号。也就是说,/\.txt$/
与 '/\.txt$/'
或 "/\.txt$/"
不一样。前者指示 webpack 匹配任何以 .txt 结尾的文件,后者指示 webpack 匹配具有绝对路径 '.txt' 的单个文件; 这可能不符合你的意图。
在使用 loader 时,可以阅读 loader 章节 查看更深入的自定义配置。
“嘿,webpack 编译器,当你碰到「在
require()
/import
语句中被解析为 '.txt' 的路径」时,在你对它打包之前,先 use(使用)raw-loader
转换一下。”
Warning
重要的是要记住,在 webpack 配置中定义 rules 时,要定义在 module.rules
而不是 rules
中。为了使你便于理解,如果没有按照正确方式去做,webpack 会给出警告。
Warning
请记住,使用正则表达式匹配文件时,你不要为它添加引号。也就是说,/\.txt$/
与 '/\.txt$/'
或 "/\.txt$/"
不一样。前者指示 webpack 匹配任何以 .txt 结尾的文件,后者指示 webpack 匹配具有绝对路径 '.txt' 的单个文件; 这可能不符合你的意图。
在使用 loader 时,可以阅读 loader 章节 查看更深入的自定义配置。
想要使用一个插件,你只需要 require()
它,然后把它添加到 plugins
数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new
操作符来创建一个插件实例。
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 用于访问内置插件
module.exports = {
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};
在上面的示例中,html-webpack-plugin
为应用程序生成一个 HTML 文件,并自动将生成的所有 bundle 注入到此文件中。
模式(mode)
通过选择 development
, production
或 none
之中的一个,来设置 mode
参数,你可以启用 webpack 内置在相应环境下的优化。其默认值为 production
。
module.exports = {
mode: 'production',
};