Gulp

本快速入门指南将教你如何使用 gulp 构建 TypeScript,然后将 BrowserifyterserWatchify 添加到 gulp 流程中。本指南还展示了如何使用 Babelify 添加 Babel 功能。

🌐 This quick start guide will teach you how to build TypeScript with gulp and then add Browserify, terser, or Watchify to the gulp pipeline. This guide also shows how to add Babel functionality using Babelify.

我们假设你已经在使用 Node.jsnpm

🌐 We assume that you’re already using Node.js with npm.

最小项目

🌐 Minimal project

让我们先创建一个新目录。我们暂且把它命名为 proj,但你可以随意更改名字。

🌐 Let’s start out with a new directory. We’ll name it proj for now, but you can change it to whatever you want.

shell
mkdir proj
cd proj

首先,我们将按以下方式构建我们的项目:

🌐 To start, we’re going to structure our project in the following way:

proj/
├─ src/
└─ dist/

TypeScript 文件将从你的 src 文件夹开始,通过 TypeScript 编译器处理后,最终输出到 dist

🌐 TypeScript files will start out in your src folder, run through the TypeScript compiler and end up in dist.

让我们把它搭建起来:

🌐 Let’s scaffold this out:

shell
mkdir src
mkdir dist

初始化项目

🌐 Initialize the project

现在我们将把这个文件夹变成一个 npm 包。

🌐 Now we’ll turn this folder into an npm package.

shell
npm init

你将收到一系列提示。你可以使用默认设置,除了你的入口点。对于你的入口点,请使用 ./dist/main.js。你随时可以返回并在为你生成的 package.json 文件中更改这些设置。

🌐 You’ll be given a series of prompts. You can use the defaults except for your entry point. For your entry point, use ./dist/main.js. You can always go back and change these in the package.json file that’s been generated for you.

安装我们的依赖

🌐 Install our dependencies

现在我们可以使用 npm install 来安装软件包。首先全局安装 gulp-cli(如果你使用的是 Unix 系统,可能需要在本指南中的 npm install 命令前加上 sudo)。

🌐 Now we can use npm install to install packages. First install gulp-cli globally (if you use a Unix system, you may need to prefix the npm install commands in this guide with sudo).

shell
npm install -g gulp-cli

然后在你的项目开发依赖中安装 typescriptgulpgulp-typescriptGulp-typescript 是一个用于 TypeScript 的 gulp 插件。

🌐 Then install typescript, gulp and gulp-typescript in your project’s dev dependencies. Gulp-typescript is a gulp plugin for TypeScript.

shell
npm install --save-dev typescript gulp@4.0.0 gulp-typescript

写一个简单的例子

🌐 Write a simple example

让我们编写一个 Hello World 程序。 在 src 中,创建文件 main.ts

🌐 Let’s write a Hello World program. In src, create the file main.ts:

ts
function hello(compiler: string) {
console.log(`Hello from ${compiler}`);
}
hello("TypeScript");

在项目根目录 proj 中,创建文件 tsconfig.json

🌐 In the project root, proj, create the file tsconfig.json:

{
"": ["src/main.ts"],
"": true,
"": "es5"
}
}

创建 gulpfile.js

🌐 Create a gulpfile.js

在项目根目录下,创建文件 gulpfile.js

🌐 In the project root, create the file gulpfile.js:

js
var gulp = require("gulp");
var ts = require("gulp-typescript");
var tsProject = ts.createProject("tsconfig.json");
gulp.task("default", function () {
return tsProject.src().pipe(tsProject()).js.pipe(gulp.dest("dist"));
});

测试生成的应用

🌐 Test the resulting app

shell
gulp
node dist/main.js

程序应该打印“Hello from TypeScript!”

🌐 The program should print “Hello from TypeScript!“.

在代码中添加模块

🌐 Add modules to the code

在我们进入 Browserify 之前,先把我们的代码搭建出来并加入模块。这是你在实际应用中更可能使用的结构。

🌐 Before we get to Browserify, let’s build our code out and add modules to the mix. This is the structure you’re more likely to use for a real app.

创建一个名为 src/greet.ts 的文件:

🌐 Create a file called src/greet.ts:

ts
export function sayHello(name: string) {
return `Hello from ${name}`;
}

现在将 src/main.ts 中的代码更改为从 greet.ts 导入 sayHello

🌐 Now change the code in src/main.ts to import sayHello from greet.ts:

ts
import { sayHello } from "./greet";
console.log(sayHello("TypeScript"));

最后,将 src/greet.ts 添加到 tsconfig.json

🌐 Finally, add src/greet.ts to tsconfig.json:

{
"": ["src/main.ts", "src/greet.ts"],
"": true,
"": "es5"
}
}

通过运行 gulp 然后在 Node 中进行测试,确保模块正常工作:

🌐 Make sure that the modules work by running gulp and then testing in Node:

shell
gulp
node dist/main.js

请注意,尽管我们使用了 ES2015 模块语法,TypeScript 仍然生成了 Node 使用的 CommonJS 模块。我们将在本教程中继续使用 CommonJS,但你可以在选项对象中设置 module 来更改这一点。

🌐 Notice that even though we used ES2015 module syntax, TypeScript emitted CommonJS modules that Node uses. We’ll stick with CommonJS for this tutorial, but you could set module in the options object to change this.

Browserify

现在让我们把这个项目从 Node 移到浏览器。要做到这一点,我们希望将所有模块打包成一个 JavaScript 文件。幸运的是,这正是 Browserify 所做的。更棒的是,它允许我们使用 Node 所使用的 CommonJS 模块系统,这也是 TypeScript 默认的输出方式。这意味着我们的 TypeScript 和 Node 设置基本上可以不变地迁移到浏览器。

🌐 Now let’s move this project from Node to the browser. To do this, we’d like to bundle all our modules into one JavaScript file. Fortunately, that’s exactly what Browserify does. Even better, it lets us use the CommonJS module system used by Node, which is the default TypeScript emit. That means our TypeScript and Node setup will transfer to the browser basically unchanged.

首先,安装 browserify、tsify 和 vinyl-source-stream。tsify 是一个 Browserify 插件,类似于 gulp-typescript,能让你使用 TypeScript 编译器。vinyl-source-stream 让我们可以将 Browserify 的文件输出转换回 gulp 可识别的格式,称为 vinyl

🌐 First, install browserify, tsify, and vinyl-source-stream. tsify is a Browserify plugin that, like gulp-typescript, gives access to the TypeScript compiler. vinyl-source-stream lets us adapt the file output of Browserify back into a format that gulp understands called vinyl.

shell
npm install --save-dev browserify tsify vinyl-source-stream

创建一个页面

🌐 Create a page

src 中创建一个名为 index.html 的文件:

🌐 Create a file in src named index.html:

html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World!</title>
</head>
<body>
<p id="greeting">Loading ...</p>
<script src="bundle.js"></script>
</body>
</html>

现在将 main.ts 改为更新页面:

🌐 Now change main.ts to update the page:

ts
import { sayHello } from "./greet";
function showHello(divName: string, name: string) {
const elt = document.getElementById(divName);
elt.innerText = sayHello(name);
}
showHello("greeting", "TypeScript");

调用 showHello 会调用 sayHello 来更改段落的文本。 现在将你的 gulpfile 改成如下内容:

🌐 Calling showHello calls sayHello to change the paragraph’s text. Now change your gulpfile to the following:

js
var gulp = require("gulp");
var browserify = require("browserify");
var source = require("vinyl-source-stream");
var tsify = require("tsify");
var paths = {
pages: ["src/*.html"],
};
gulp.task("copy-html", function () {
return gulp.src(paths.pages).pipe(gulp.dest("dist"));
});
gulp.task(
"default",
gulp.series(gulp.parallel("copy-html"), function () {
return browserify({
basedir: ".",
debug: true,
entries: ["src/main.ts"],
cache: {},
packageCache: {},
})
.plugin(tsify)
.bundle()
.pipe(source("bundle.js"))
.pipe(gulp.dest("dist"));
})
);

这会添加 copy-html 任务,并将其作为 default 的依赖。这意味着每次运行 default 时,必须先运行 copy-html。我们还将 default 改为使用 tsify 插件调用 Browserify,而不是 gulp-typescript。方便的是,它们都允许我们向 TypeScript 编译器传递相同的选项对象。

🌐 This adds the copy-html task and adds it as a dependency of default. That means any time default is run, copy-html has to run first. We’ve also changed default to call Browserify with the tsify plugin instead of gulp-typescript. Conveniently, they both allow us to pass the same options object to the TypeScript compiler.

在调用 bundle 之后,我们使用 source(我们为 vinyl-source-stream 设置的别名)来命名我们的输出包 bundle.js

🌐 After calling bundle we use source (our alias for vinyl-source-stream) to name our output bundle bundle.js.

通过运行 gulp 测试页面,然后在浏览器中打开 dist/index.html。 你应该能在页面上看到“Hello from TypeScript”。

🌐 Test the page by running gulp and then opening dist/index.html in a browser. You should see “Hello from TypeScript” on the page.

请注意,我们将 debug: true 指定给了 Browserify。这会导致 tsify 在打包后的 JavaScript 文件中生成源映射。源映射让你可以在浏览器中调试原始的 TypeScript 代码,而不是打包后的 JavaScript。你可以通过打开浏览器的调试工具并在 main.ts 内设置断点来测试源映射是否工作。当你刷新页面时,断点应该会暂停页面,并让你调试 greet.ts

🌐 Notice that we specified debug: true to Browserify. This causes tsify to emit source maps inside the bundled JavaScript file. Source maps let you debug your original TypeScript code in the browser instead of the bundled JavaScript. You can test that source maps are working by opening the debugger for your browser and putting a breakpoint inside main.ts. When you refresh the page the breakpoint should pause the page and let you debug greet.ts.

Watchify、Babel 和 Terser

🌐 Watchify, Babel, and Terser

现在我们将我们的代码与 Browserify 和 tsify 打包在一起,我们可以使用 browserify 插件将各种功能添加到我们的构建中。

🌐 Now that we are bundling our code with Browserify and tsify, we can add various features to our build with browserify plugins.

  • Watchify 会启动 gulp 并保持运行,每次保存文件时都会增量编译。这让你可以在浏览器中持续进行编辑-保存-刷新循环。
  • Babel 是一个非常灵活的编译器,可以将 ES2015 及更高版本的代码转换为 ES5 和 ES3。这让你能够添加 TypeScript 不支持的广泛且自定义的转换。
  • Terser 压缩你的代码,以减少下载时间。

Watchify

我们将从 Watchify 开始提供后台编译:

🌐 We’ll start with Watchify to provide background compilation:

shell
npm install --save-dev watchify fancy-log

现在将你的 gulpfile 更改为以下内容:

🌐 Now change your gulpfile to the following:

js
var gulp = require("gulp");
var browserify = require("browserify");
var source = require("vinyl-source-stream");
var watchify = require("watchify");
var tsify = require("tsify");
var fancy_log = require("fancy-log");
var paths = {
pages: ["src/*.html"],
};
var watchedBrowserify = watchify(
browserify({
basedir: ".",
debug: true,
entries: ["src/main.ts"],
cache: {},
packageCache: {},
}).plugin(tsify)
);
gulp.task("copy-html", function () {
return gulp.src(paths.pages).pipe(gulp.dest("dist"));
});
function bundle() {
return watchedBrowserify
.bundle()
.on("error", fancy_log)
.pipe(source("bundle.js"))
.pipe(gulp.dest("dist"));
}
gulp.task("default", gulp.series(gulp.parallel("copy-html"), bundle));
watchedBrowserify.on("update", bundle);
watchedBrowserify.on("log", fancy_log);

这里基本上有三个更改,但它们需要你稍微重构一下代码。

🌐 There are basically three changes here, but they require you to refactor your code a bit.

  1. 我们将 browserify 实例封装在对 watchify 的调用中,然后保留了结果。
  2. 我们调用了 watchedBrowserify.on('update', bundle);,这样每当你的 TypeScript 文件发生变化时,Browserify 就会运行 bundle 函数。
  3. 我们调用 watchedBrowserify.on('log', fancy_log); 来记录到控制台。

(1)和(2)意味着我们必须将对 browserify 的调用从 default 任务中移出。 而且我们必须为 default 的函数命名,因为 Watchify 和 Gulp 都需要调用它。 使用(3)添加日志是可选的,但对于调试你的设置非常有用。

🌐 Together (1) and (2) mean that we have to move our call to browserify out of the default task. And we have to give the function for default a name since both Watchify and Gulp need to call it. Adding logging with (3) is optional but very useful for debugging your setup.

现在当你运行 Gulp 时,它应该会启动并保持运行状态。尝试修改 main.ts 中的 showHello 代码并保存它。你应该会看到类似这样的输出:

🌐 Now when you run Gulp, it should start and stay running. Try changing the code for showHello in main.ts and saving it. You should see output that looks like this:

shell
proj$ gulp
[10:34:20] Using gulpfile ~/src/proj/gulpfile.js
[10:34:20] Starting 'copy-html'...
[10:34:20] Finished 'copy-html' after 26 ms
[10:34:20] Starting 'default'...
[10:34:21] 2824 bytes written (0.13 seconds)
[10:34:21] Finished 'default' after 1.36 s
[10:35:22] 2261 bytes written (0.02 seconds)
[10:35:24] 2808 bytes written (0.05 seconds)

Terser

首先安装 Terser。由于 Terser 的目的是混淆你的代码,我们还需要安装 vinyl-buffer 和 gulp-sourcemaps 来保持源映射的正常工作。

🌐 First install Terser. Since the point of Terser is to mangle your code, we also need to install vinyl-buffer and gulp-sourcemaps to keep sourcemaps working.

shell
npm install --save-dev gulp-terser vinyl-buffer gulp-sourcemaps

现在将你的 gulpfile 更改为以下内容:

🌐 Now change your gulpfile to the following:

js
var gulp = require("gulp");
var browserify = require("browserify");
var source = require("vinyl-source-stream");
var terser = require("gulp-terser");
var tsify = require("tsify");
var sourcemaps = require("gulp-sourcemaps");
var buffer = require("vinyl-buffer");
var paths = {
pages: ["src/*.html"],
};
gulp.task("copy-html", function () {
return gulp.src(paths.pages).pipe(gulp.dest("dist"));
});
gulp.task(
"default",
gulp.series(gulp.parallel("copy-html"), function () {
return browserify({
basedir: ".",
debug: true,
entries: ["src/main.ts"],
cache: {},
packageCache: {},
})
.plugin(tsify)
.bundle()
.pipe(source("bundle.js"))
.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(terser())
.pipe(sourcemaps.write("./"))
.pipe(gulp.dest("dist"));
})
);

注意 terser 本身只有一个调用 —— 对 buffersourcemaps 的调用是为了确保 sourcemaps 继续有效。 这些调用为我们提供了一个单独的 sourcemap 文件,而不是像以前那样使用内联 sourcemap。 现在你可以运行 Gulp 并检查 bundle.js 是否被压缩成不可读的混乱文件:

🌐 Notice that terser itself has just one call — the calls to buffer and sourcemaps exist to make sure sourcemaps keep working. These calls give us a separate sourcemap file instead of using inline sourcemaps like before. Now you can run Gulp and check that bundle.js does get minified into an unreadable mess:

shell
gulp
cat dist/bundle.js

Babel

首先安装 Babelify 和用于 ES2015 的 Babel 预设。像 Terser 一样,Babelify 会混淆代码,所以我们需要 vinyl-buffer 和 gulp-sourcemaps。默认情况下,Babelify 只会处理扩展名为 .js.es.es6.jsx 的文件,因此我们需要将 .ts 扩展名作为选项添加到 Babelify 中。

🌐 First install Babelify and the Babel preset for ES2015. Like Terser, Babelify mangles code, so we’ll need vinyl-buffer and gulp-sourcemaps. By default Babelify will only process files with extensions of .js, .es, .es6 and .jsx so we need to add the .ts extension as an option to Babelify.

shell
npm install --save-dev babelify@8 babel-core babel-preset-es2015 vinyl-buffer gulp-sourcemaps

现在将你的 gulpfile 更改为以下内容:

🌐 Now change your gulpfile to the following:

js
var gulp = require("gulp");
var browserify = require("browserify");
var source = require("vinyl-source-stream");
var tsify = require("tsify");
var sourcemaps = require("gulp-sourcemaps");
var buffer = require("vinyl-buffer");
var paths = {
pages: ["src/*.html"],
};
gulp.task("copy-html", function () {
return gulp.src(paths.pages).pipe(gulp.dest("dist"));
});
gulp.task(
"default",
gulp.series(gulp.parallel("copy-html"), function () {
return browserify({
basedir: ".",
debug: true,
entries: ["src/main.ts"],
cache: {},
packageCache: {},
})
.plugin(tsify)
.transform("babelify", {
presets: ["es2015"],
extensions: [".ts"],
})
.bundle()
.pipe(source("bundle.js"))
.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(sourcemaps.write("./"))
.pipe(gulp.dest("dist"));
})
);

我们还需要将 TypeScript 的目标设置为 ES2015。然后 Babel 会从 TypeScript 生成的 ES2015 代码中输出 ES5。让我们修改 tsconfig.json:

🌐 We also need to have TypeScript target ES2015. Babel will then produce ES5 from the ES2015 code that TypeScript emits. Let’s modify tsconfig.json:

{
"": ["src/main.ts"],
"": true,
"": "es2015"
}
}

对于这样一个简单的脚本,Babel 的 ES5 输出应该与 TypeScript 的输出非常相似。

🌐 Babel’s ES5 output should be very similar to TypeScript’s output for such a simple script.