知海

Gulp 教程

TypeScript-dev指南与教程

Gulp 教程

本指南将教你如何使用 Gulp 构建 TypeScript 项目,并演示如何在 Gulp 管道中集成 BrowserifyUglifyWatchify。此外,还会介绍如何通过 Babelify 为项目添加 Babel 功能。

本教程假设你已经安装了 Node.jsnpm

创建简单工程

首先创建一个新目录,命名为 proj(或任意你喜欢的名字)。

text 复制代码
mkdir proj
cd proj

我们将以如下目录结构开始工程:

text 复制代码
proj/
   ├─ src/
   └─ dist/

TypeScript 源文件放在 src 目录下,编译器生成的目标文件输出到 dist 目录。

创建目录:

text 复制代码
mkdir src
mkdir dist

初始化工程

将当前目录初始化为 npm 包:

text 复制代码
npm init

按照提示操作,除入口文件外,其余均可使用默认值。入口文件设置为 ./dist/main.js。你也可以随时在生成的 package.json 中修改配置。

安装依赖项

全局安装 gulp-cli(如果你使用 Unix 系统,可能需要 sudo):

text 复制代码
npm install -g gulp-cli

然后安装 typescriptgulpgulp-typescript 作为开发依赖。gulp-typescript 是 TypeScript 的 Gulp 插件。

text 复制代码
npm install --save-dev typescript gulp@4.0.0 gulp-typescript

编写一个简单示例

src 目录下创建 main.ts

typescript 复制代码
function hello(compiler: string) {
  console.log(`Hello from ${compiler}`);
}
hello('TypeScript');

在工程根目录 proj 下创建 tsconfig.json

json 复制代码
{
  "files": [
    "src/main.ts"
  ],
  "compilerOptions": {
    "noImplicitAny": true,
    "target": "es5"
  }
}

创建 gulpfile.js

在工程根目录下创建 gulpfile.js

javascript 复制代码
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'));
});

测试应用

text 复制代码
gulp
node dist/main.js

程序应输出 Hello from TypeScript!

向代码中添加模块

在使用 Browserify 之前,我们先调整代码结构,引入模块。这个结构更接近真实应用。

创建 src/greet.ts

typescript 复制代码
export function sayHello(name: string) {
  return `Hello from ${name}`;
}

修改 src/main.ts,从 greet.ts 导入 sayHello

typescript 复制代码
import { sayHello } from './greet';

console.log(sayHello('TypeScript'));

更新 tsconfig.json,将 src/greet.ts 加入 files

json 复制代码
{
  "files": [
    "src/main.ts",
    "src/greet.ts"
  ],
  "compilerOptions": {
    "noImplicitAny": true,
    "target": "es5"
  }
}

运行 gulp 并在 Node.js 下验证模块是否工作:

text 复制代码
gulp
node dist/main.js

注意:虽然我们使用了 ES2015 模块语法,TypeScript 仍会生成 Node.js 使用的 CommonJS 模块。本教程中我们继续使用 CommonJS 模块,你可以通过修改 compilerOptions.module 来调整模块生成方式。

集成 Browserify

现在将工程从 Node.js 环境迁移到浏览器环境,我们需要将模块打包为单个 JavaScript 文件。Browserify 支持 Node.js 的 CommonJS 模块,而 TypeScript 默认生成的正是 CommonJS 格式,因此无需修改 TypeScript 和 Node.js 的配置即可直接移植。

安装 Browserify、tsify 和 vinyl-source-stream:

text 复制代码
npm install --save-dev browserify tsify vinyl-source-stream
  • tsify 是 Browserify 的插件,相当于 gulp-typescript 的角色,为 Browserify 提供 TypeScript 编译器支持。
  • vinyl-source-stream 将 Browserify 的输出转换为 Gulp 可识别的 vinyl 流格式。

创建页面

src 目录下创建 index.html

markup 复制代码
<!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 以更新页面内容:

typescript 复制代码
import { sayHello } from './greet';

function showHello(divName: string, name: string) {
  const elt = document.getElementById(divName);
  elt.innerText = sayHello(name);
}

showHello('greeting', 'TypeScript');

更新 gulpfile.js

javascript 复制代码
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 改用 Browserify 搭配 tsify 插件,而不是 gulp-typescript。两者向 TypeScript 编译器传递的参数对象完全兼容。

调用 bundle() 后,通过 source(vinyl-source-stream 的别名)将输出文件命名为 bundle.js

测试页面:

text 复制代码
gulp

然后在浏览器中打开 dist/index.html,页面上应显示 Hello from TypeScript

我们为 Browserify 指定了 debug: true,这会生成 source maps。source maps 允许浏览器直接调试 TypeScript 源码,而不是压缩后的 JavaScript 文件。可以在浏览器调试器中对 main.ts 设置断点验证,刷新页面后代码会停在断点处,同时可以调试 greet.ts

集成 Watchify、Babel 和 Uglify

现在已通过 Browserify 和 tsify 完成了打包,接下来可以为构建流程添加更多特性:

  • Watchify:启动 Gulp 后保持监听,文件保存时自动编译,加速编辑-保存-刷新循环。
  • Babel:灵活的编译器,可将 ES2015+ 转换为 ES5/ES3,支持大量自定义转换器。
  • Uglify:压缩代码,减小下载体积。

集成 Watchify

安装依赖:

text 复制代码
npm install --save-dev watchify fancy-log

修改 gulpfile.js

javascript 复制代码
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);

主要变化有三点:

  1. browserify 实例包裹在 watchify 中,用于监听文件变化。
  2. 通过 watchedBrowserify.on('update', bundle) 在 TypeScript 文件发生变化时自动执行 bundle 函数。
  3. 通过 watchedBrowserify.on('log', fancy_log) 将日志输出到控制台。

由于 Watchify 和 Gulp 都需要调用 bundle,我们将 Browserify 的创建移出了 default 任务并命名为独立函数。第 3 点虽然不是必须的,但对于调试很有帮助。

现在运行 gulp,它会保持进程监听。尝试修改 main.ts 中的代码并保存,控制台会输出类似信息:

text 复制代码
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)

集成 Uglify

安装 Uglify 以及用于支持 source maps 的辅助包:

text 复制代码
npm install --save-dev gulp-uglify vinyl-buffer gulp-sourcemaps

修改 gulpfile.js

javascript 复制代码
var gulp = require('gulp');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var tsify = require('tsify');
var uglify = require('gulp-uglify');
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(uglify())
      .pipe(sourcemaps.write('./'))
      .pipe(gulp.dest('dist'));
  })
);

buffer() 将流转换为缓冲,sourcemaps.init({ loadMaps: true }) 加载已有的 source maps,uglify() 压缩代码,最后 sourcemaps.write('./') 输出独立的 sourcemap 文件,替代之前的内嵌方式。

运行 gulp 并检查压缩后的 bundle.js

text 复制代码
gulp
cat dist/bundle.js

集成 Babel

安装 Babelify 和 ES2015 预设。与 Uglify 类似,Babelify 也会混淆代码,因此需要 vinyl-buffergulp-sourcemaps。Babelify 默认只处理 .js.es.es6.jsx 文件,需要添加 .ts 扩展名。

text 复制代码
npm install --save-dev babelify@8 babel-core babel-preset-es2015 vinyl-buffer gulp-sourcemaps

修改 gulpfile.js

javascript 复制代码
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

json 复制代码
{
  "files": [
    "src/main.ts"
  ],
  "compilerOptions": {
    "noImplicitAny": true,
    "target": "es2015"
  }
}

对于简单的示例代码,Babel 生成的 ES5 输出与 TypeScript 直接生成的结果类似。

帮助我们改进文档

发现翻译问题或内容错误?请告诉我们。