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.ts
和test-page.ts
去掉export
和import
,直接在网页上引入脚本:
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脚本都要重写一个没有export
和import
的版本显然是不现实的。
因此,需要有一个工具把可执行的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