Jade Dungeon

Typescript

创建TypeScript项目

项目结构

study-typescript/
▾ src/
  ▾ html/
      test.html
  ▾ scripts/ts/
    ▾ module01/
        greeter-page.ts
        greeter.ts
      test-page.ts
      test.ts
▸ target/
  gulpfile.js
  tsconfig.json

可执行程序的代码

module01.greeter是一个模块,用export导出类Student和方法greeter()

export class Student {

	fullName: string;

	constructor(
		public firstName    : string,
		public middleInitial: string,
		public lastName     : string)
	{
		this.fullName = firstName + " " + middleInitial + " " + lastName;
	}

}

interface Person {
	firstName: string;
	lastName: string;
}

export function greeter(person: Person) {
	return "Hello, " + person.firstName + " " + person.lastName;
};

程序的入口test.ts使用import导入之前module01/greeter导出的内容:

import { greeter, Student } from "./module01/greeter";

let user = new Student("Jane", "M.", "User");
 
console.log(greeter(user));

网页的代码

浏览器并不支持export,所以先用另一个版本的greeter-page.tstest-page.ts 去掉exportimport,直接在网页上引入脚本:

class Student {

	fullName: string;

	constructor(
		public firstName    : string,
		public middleInitial: string,
		public lastName     : string)
	{
		this.fullName = firstName + " " + middleInitial + " " + lastName;
	}

}

interface Person {
	firstName: string;
	lastName: string;
}

function greeter(person: Person) {
	return "Hello, " + person.firstName + " " + person.lastName;
};

直接在网页上引入脚本:

let user = new Student("Jane", "M.", "User");
 
console.log(greeter(user));

document.body.textContent = greeter(user);
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Typescript Sample</title>
</head>
<body>
	 
</body>
<script src="./scripts/ts/module01/greeter-page.js"></script>
<script src="./scripts/ts/test-page.js"></script>
</html>

使用Gulp构建

指定nodejs的版本:

$ echo 'v16.15.0' > .nvmrc
$ nvm use
Found '/home/student/study-typescript/.nvmrc' with version <v16.15.0>
Now using node v16.15.0 (npm v8.5.5)

安装相关的组件:

npm install --save-dev typescript gulp gulp-cli gulp-typescript gulp-jshint gulp-clean

编译配置tsconfig.json,只指定编译相关的参数。 编译的源文件与输出文件由Gulp来管理;

{
  "compilerOptions": {
    "target": "es2015",
    "strict": true,
    "module": "commonjs",
    "noImplicitAny": true,
    "sourceMap": true
  }
}

Gulp的配置文件gulpfile.js指定源文件目录与输出目录:

const gulp   = require('gulp');
const ts     = require("gulp-typescript");
const jshint = require('gulp-jshint');  //js检查
const clean  = require('gulp-clean');   //清空文件夹
 
// 复制html文件
gulp.task('clean-html', () => {
	return gulp.src(["./target/" + '**/*.html'], {read: false})
		.pipe(clean());
});
 
gulp.task('copy-html', gulp.series('clean-html', async (callback) => {
	return gulp.src(["./src/html/" + "**/*.html"])
		.pipe(gulp.dest("./target/"));
}));
 
// 编译typescript
gulp.task('clean-ts', () => {
		return gulp.src(["./target/scripts/ts/"], 
			{read: false, allowEmpty: true}).pipe(clean());
});
 
const tsProject = ts.createProject("tsconfig.json");
 
gulp.task('build-ts', gulp.series('clean-ts', () => {
	return gulp.src("./src/scripts/ts/" + '**/*.ts')
		.pipe(tsProject())
		.js
		.pipe(gulp.dest("./target/scripts/ts/"));
}));

// 命令的组合,并行执行
gulp.task('default', gulp.parallel('copy-html', 'build-ts'));

执行命令构建:

gulp

# 或者:

gulp default

构建完成后,可以运行编译后的可执行脚本:

$ node target/scripts/ts/test.js
Hello, Jane User

也可以可以打开target/index.html网页查看网页的效果, 但是如果每个typescript脚本都要重写一个没有exportimport 的版本显然是不现实的。

因此,需要有一个工具把可执行的js文件转为浏览器可以识别的js。

转化为网页可用

把可执行的js文件转为浏览器可以识别的js的gulp插件为browserify, 安装方式:

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

转化单个文件

修改gulpfile.js

// 打包typescript输出为浏览器可用的版本
var browserify = require("browserify");
var source = require("vinyl-source-stream");
var tsify = require("tsify");

gulp.task("browserify-ts", gulp.series('clean-ts', () => {
	return browserify({
		basedir: ".",
		debug: true,
		entries: [
			"./src/scripts/ts/" + "test-browserify.ts"
		],
		cache: {},
		packageCache: {}
	})
	.plugin(tsify)
	.bundle()
	.pipe(source("test-browserify-bundle.js"))
	.pipe(gulp.dest("./target/scripts/ts/"));
}));
 
// 命令的组合,并行执行
// 去掉原来的`build-ts`步骤,改为`browserify-ts`步骤
// gulp.task('default', gulp.parallel('copy-html', 'build-ts'));
gulp.task('default', gulp.parallel('copy-html', 'browserify-ts'));

在新的步骤browserify-ts里指定了把./src/scripts/ts/test-browserify.ts 编译为浏览器可用的./target/scripts/ts/test-browserify-bundle.js

其实browserify插件已经包含了编译typescript的工作, 但是关联的范围不同:

  • browserify插件只编译entries参数指定的源代码所引用到的其他文件。
  • gulp-typescript插件会按配置的路径./src/scripts/ts/**/*.ts 编译所有的typescript(即有网页到到的代码,也有可执行脚本用到的代码)。

所以如果是纯网页脚本,可以去掉gulp-typescript的步骤。

在这里我们添加程序文件test-browserify.ts给页面使用:

import { greeter, Student } from "./module01/greeter";

let user = new Student("Jane", "M.", "User");
  
console.log(greeter(user));
 
document.body.textContent = greeter(user);  // 在页面上显示内容

网页用的test-browserify.ts与可执行脚本的test.js区别就是加了最后一句在页面上显示内容的代码。

最后网页上直接引用test-browserify-bundle.js就可以了:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Typescript Sample</title>
</head>
<body>
</body>
<script src="./scripts/ts/test-browserify-bundle.js"></script>
</html>

转化多个文件

一般来说一个网站上会有多个页面,每个页面都有不同的脚本, 所以改进程序为可以给多个文件转化为多个文件:

// 打包typescript输出为浏览器可用的版本
const browserify = require("browserify");
const source = require("vinyl-source-stream");
const tsify = require("tsify");

let browserifyTs = (filename) => {
	let dstFilename = filename.replace(/\.ts$/,'-bundle.js');
	return browserify({
		basedir: ".",
		debug: true,
		entries: [
			"./src/scripts/ts/" + filename,
		],
		cache: {},
		packageCache: {}
	}).plugin(tsify)
		.bundle()
		.on("error", fancyLog)
		.pipe(source(dstFilename))
		.pipe(gulp.dest("./target/scripts/ts/"));
};

let browserifyTasks = [  ];

// 遍历每一个要被网页引用的ts,生成任务
["test-browserify-01.ts",
	"test-browserify-02.ts"
].forEach((o) => {
	browserifyTasks.push(() => { return browserifyTs(o); });
});

// 多个转化任务组合为一个
gulp.task("browserify-ts", gulp.parallel('build-ts', browserifyTasks));

监控文件修改

gulp-watch插件用来监控文件的改动:

npm install --save-dev gulp-watch

对每个不同的任务指定不同的更新任务:

// 观察文件更新
const watch = require('gulp-watch');

// 针对每个目录调用不同的构建任务
let watchTask = (path, task) => {
	return watch(path, () => {
		console.log(`watch file change: ${path} `);
		gulp.parallel(task)(); 
	});
};

// 把所有要监控的目录都加上去
gulp.task('watching', () => {
	watchTask("./src/html/" + "**/*.html", "copy-html");
	watchTask("./src/scripts/ts/" + "**/*.ts", "browserify-ts");
});

// 命令的组合,并行执行
gulp.task('develop', gulp.parallel('copy-html', 'browserify-ts', 'watching'));

注意:现在的方案有缺陷,就是同时只能触发一个任务。 当同时保存多个文件时,如果前一个任务没有构建完, 就触发了下一个构建任务,会报错。

使用Terser压缩脚本

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

把压缩的步骤加在编译后面:

var terser       = require("gulp-terser");
var sourcemaps   = require("gulp-sourcemaps");
var buffer       = require("vinyl-buffer");

let browserifyTs = (filename) => {
	let dstFilename = filename.replace(/\.ts$/,'-bundle.js');
	return browserify({
		basedir: ".",
		debug: true,
		entries: [
			"./src/scripts/ts/" + filename,
		],
		cache: {},
		packageCache: {}
	}).plugin(tsify)
		.bundle()
		.pipe(source(dstFilename))
		.pipe(buffer())
		.pipe(sourcemaps.init({ loadMaps: true }))
		.pipe(terser())
		.pipe(sourcemaps.write("./"))
		.on("error", fancyLog)
		.pipe(gulp.dest("./target/scripts/ts/"));
};

使用Babel编译为老版本

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.

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

babel的操作放在编译后面、压缩的前面:

let browserifyTs = (filename) => {
	let dstFilename = filename.replace(/\.ts$/,'-bundle.js');
	return browserify({
		basedir: ".",
		debug: true,
		entries: [
			"./src/scripts/ts/" + filename,
		],
		cache: {},
		packageCache: {}
	}).plugin(tsify)
		.transform("babelify", {
			presets: ["es2015"],
			extensions: [".ts"],
		})
		.bundle()
		.pipe(source(dstFilename))
		.pipe(buffer())
		.pipe(sourcemaps.init({ loadMaps: true }))
		.pipe(terser())
		.pipe(sourcemaps.write("./"))
		.on("error", fancyLog)
		.pipe(gulp.dest("./target/scripts/ts/"));
};

确保tsconfig.json里的版本是es2015

{
  "compilerOptions": {
    "target": "es2015",
    "strict": true,
    "module": "commonjs",
    "noImplicitAny": true,
    "sourceMap": true
  }
}

httpserver

为了方便测试,我们要启动一个http服务,通过http请求来读取target目录下的文件:

const fs   = require("fs");
const path = require("path");
const http = require("http");
const url  = require("url");

const port = 8000;

const showHTML = (res, content) => {
	res.writeHead(200,{'Content-Type': 'text/html'});
	res.end(content);
};

const show404 = (res, content) => {
	res.writeHead(200,{'Content-Type': 'text/html'});
	res.end("File Not Found");
};

const loadFile = (res, filename, contentType) => {
	console.debug(`load file  : ${filename}`);
	fs.readFile(filename.replace(/\?.*$/, ''), "binary", (err, file) => {
		if (err) {
			res.writeHead(500, {'Content-Type': 'text/plain'});
			res.write(err + "\n");
			res.end();
		} else {
			res.writeHead(200, {'Content-Type': contentType});
			res.write(file, "binary");
			res.end();
		}
	});
};

let server = http.createServer((req, res) => {
	let reqUrl = url.parse(req.url, true);
	console.debug(`request url: ${reqUrl.pathname}`);
	if (reqUrl.pathname === "/") {
		showHTML(res,['<!DOCTYPE html>',
			'<html lang="en">',
			'<head>',
			'<meta charset="UTF-8">',
			'<title>Hello World</title>',
			'</head>',
			'<body>',
			'<a target="_blank" href="/target/test.html">test page 01</a><br/>',
			'<a target="_blank" href="/target/test-browserify.html">test page 02</a><br/>',
			'</body>',
			'</html>'
		].join('\r\n'));
	} else if (/^\/target\/images\/.+/.test(reqUrl.pathname)) {
		loadFile(res, "." + reqUrl.pathname, "image/jpg");
	} else if (
		/^\/target\/styles\/.+/.test(reqUrl.pathname) ||
		/^\/target\/3rd\/styles\/.+/.test(reqUrl)) 
	{
		loadFile(res, "." + reqUrl.pathname, "text/css");
	} else if (
		/^\/target\/scripts\/.+/.test(reqUrl.pathname) ||
		/^\/target\/3rd\/scripts\/.+/.test(reqUrl)) 
	{
		loadFile(res, "." + reqUrl.pathname, "application/javascript;charset=utf-8");
	} else if (/^\/target\/.+/.test(reqUrl.pathname)) {
		loadFile(res, "." + reqUrl.pathname, "text/html");
	} else {
		show404(res);
	}
});
server.listen(port);

直接用node运行这个脚本就可以:

node ./httpServer.js

浏览器通过地址http://localhost:8000/就可以访问。

Debug与调试

直接使用Chrome调试

调试的参数和node调试的参数一样:

  • --inspect:打开调试,遇到断点停下。
  • --inspect-brk:打开调试,并暂停在程序开头。

调试时启动node程序,把Typescript程序node_modules/ts-node/dist/bin.js 作为node的启动的入口,再把要调用的脚本放在后面。

$ node --inspect-brk ./node_modules/ts-node/dist/bin.js ./src/scripts/app/main.ts

Debugger listening on ws://127.0.0.1:9229/328b224f-f79c-4845-81a5-23e890075878
For help, see: https://nodejs.org/en/docs/inspector
Debugger attached.

在chrome浏览器中输入地址http://127.0.0.1:9229/json/list可看可以连接的调试程序:

[ {
  "description": "node.js instance",
  "devtoolsFrontendUrl": "chrome-devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:9229/328b224f-f79c-4845-81a5-23e890075878",
  "devtoolsFrontendUrlCompat": "chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/328b224f-f79c-4845-81a5-23e890075878",
  "faviconUrl": "https://nodejs.org/static/favicon.ico",
  "id": "328b224f-f79c-4845-81a5-23e890075878",
  "title": "._node_modules_ts-node_dist_bin.js",
  "type": "node",
  "url": "file://D:_workspace_study_vimwiki-theme_templates_blog_node_modules_ts-node_dist_bin.js",
  "webSocketDebuggerUrl": "ws://127.0.0.1:9229/328b224f-f79c-4845-81a5-23e890075878"
} ]

在浏览器中打开devtoolsFrontendUrl中定义的地址,就可以打开chrome调试窗口, --inspect-brk让程序直接停在开头。在chrome调试窗口的sources 标签页中可以关联源代码的位置,之后的调试操作就和调试网页上的JS一样了。

结合VSCode调试

注意在调试过程中入口是Typescript的启动入口ts-node/dist/bin.js, 然后把要测试的脚本作为参数args传递给它。否则无法进入调试。

.vscode/launch.json是配置启动参数:

  • 第一个调试配置launch-current-ts调试当前在VSCode中打开并正focus的文件。
  • 第二个调度配置launch-test-file调试指定的文件。如:src/scripts/app/main.ts
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "launch-current-ts",
            "type": "node",
            "request": "launch",
            "program": "${workspaceRoot}/node_modules/ts-node/dist/bin.js",
            "args": [
                "${relativeFile}"
            ],
            "cwd": "${workspaceRoot}",
            "protocol": "inspector"
        }, {
            "name": "launch-test-file",
            "type": "node",
            "request": "launch",
            "program": "${workspaceRoot}/node_modules/ts-node/dist/bin.js",
            "args": [
                "${workspaceRoot}/src/scripts/app/main.ts"
            ],
            "cwd": "${workspaceRoot}",
            "protocol": "inspector"
        }
    ]
}

依赖文件数量限制

万一npm的包太多了超过了linux的上限:

Error: ENDSPC: System limit for number of file watchers reached

如果是 Webpack 引起的错误, 可以通过在webpack.config.js文件中设置watchOptions属性, 尝试排除node_modules文件夹。

module.exports = {
  //...
  watchOptions: {
    ignored: /node_modules/,
  },
};

或者直接改系统的上限:

方法一:增加 inotify watchers 的上限数量(临时)

可以临时修改 inotify 中 watcher 的上先数量,提高数量上限,方法如下:

sudo sysctl fs.inotify.max_user_watches=65535
sudo sysctl -p
cat /proc/sys/fs/inotify/max_user_watches
65535

方法二:增加 inotify watchers 的上限数量(永久)

但上述方法只是临时修改了 inotify 的 watcher 上限数量,但重启后就会恢复 inotify 的默认设置。可以用以下方法永久修改:

echo fs.inotify.max_user_watches=65535 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
cat /proc/sys/fs/inotify/max_user_watches
65535