TypeScript 帶來(lái)的最大好處就是靜態(tài)類(lèi)型檢查,所以在從 JavaScript 轉(zhuǎn)向 TypeScript 之前,一定要認(rèn)識(shí)到添加類(lèi)型定義會(huì)帶來(lái)額外的工作量,這是必要的代價(jià)。不過(guò),相對(duì)于靜態(tài)類(lèi)型檢查帶來(lái)的好處,這些代價(jià)是值得的。當(dāng)然,TypeScript 允許不定義類(lèi)型或者將所有類(lèi)型定義為 any
,但如果這樣做,TypeScript 帶來(lái)的大部分靜態(tài)檢查功能都會(huì)失去作用,換言之,也就沒(méi)必要使用 TypeScript 了。
在轉(zhuǎn)換之前還要注意的一個(gè)問(wèn)題就是模塊化。早期的 JavaScript 代碼基本上是每個(gè) HTML 頁(yè)面對(duì)應(yīng)一個(gè)或幾個(gè) JavaScript 腳本,那時(shí)候的 JavaScript 代碼中很少有模塊化的概念。不過(guò)隨著 Web 2.0 的興起,大量的工作從后端移到前端,JavaScript 程序變得越來(lái)越復(fù)雜,模塊化成為剛需,大量的模塊化框架隨之而來(lái),其中比較有名的有 RequestJS 及其帶來(lái)的 AMD 標(biāo)準(zhǔn),還有 SeaJS 帶來(lái)的 CMD 標(biāo)準(zhǔn)。而隨著 Node.js 的興起以及 JavaScript 的全?;钟辛?CommonJS 標(biāo)準(zhǔn)。之后又出現(xiàn)了廣為使用的 SystemJS。當(dāng)然少不了 ES6 的模塊化標(biāo)準(zhǔn),雖然到目前為止 Node.js 和大部分瀏覽器都還不支持它。
TypeScript 本身支持兩種模塊化方式,一種是對(duì) ES6 的模塊的微小擴(kuò)展,另一種是在 ES6 發(fā)布之前本身模仿 C# 的命名空間。大部分使用命令空間的場(chǎng)景都可以使用 ES6 模塊化標(biāo)準(zhǔn)來(lái)代替。我們先來(lái)看一看兩種模塊化方式區(qū)別。
使用命令空間寫(xiě)的 TS 腳本在轉(zhuǎn)譯成 JS 后,可以不使用任何模塊加載框架,直接在頁(yè)面中加載即可使用。不過(guò)很遺憾,這種方式轉(zhuǎn)義出來(lái)的 JS 程序不能直接在 Node.js 中使用。因?yàn)?tsc
不為會(huì)命名空間形式的模塊生成 modules.exports
對(duì)象以及 require
語(yǔ)句。
有一種情況例外。將所有
.ts
文件轉(zhuǎn)譯成一個(gè).js
,假設(shè)叫all.js
,那么它可以通過(guò)node all
來(lái)運(yùn)行。這種情況下不需要任何模塊的導(dǎo)入導(dǎo)出。
不過(guò)在瀏覽器環(huán)境中,嚴(yán)格的按照依賴(lài)順序引入生成的 .js
文件是可行的。早期沒(méi)有使用模塊化的 JS 文件就可以使用“命名空間”形式的模塊化寫(xiě)法,甚至可以將原來(lái)成百上千行的大型 JS 源文件,拆分成若干小的 TS 文件,再通過(guò) tsc --outfile
輸出單一 JS 文件來(lái)使用,這樣既能實(shí)現(xiàn)模塊化重構(gòu),又能不改變?cè)械?HTML(或其它動(dòng)態(tài)頁(yè)面文件)的代碼。
還有一點(diǎn)需要注意的是,在指定生成單一輸出文件的情況下,TypeScript 不會(huì)通過(guò)代碼邏輯去檢查模塊間的依賴(lài)關(guān)系。默認(rèn)情況下它會(huì)按文件名的字母序逐個(gè)轉(zhuǎn)譯 .ts
文件,除非源文件中通過(guò) /// <reference path="..." />
明確指定了依賴(lài)項(xiàng)。
在 TypeScript 使用 ES6 模塊語(yǔ)法來(lái)實(shí)現(xiàn)模塊化的情況下,tsc
允許通過(guò) module
參數(shù)來(lái)指定生成的 .js
會(huì)應(yīng)用于何種模塊化框架,默認(rèn)的是 commonjs
,其它比較常用的還有 amd
、system
等。
顯然,如果原來(lái)的 JS 程序使用了 AMD 框架,在轉(zhuǎn)換成 TS 的時(shí)候,就可以使用 ES6 模塊寫(xiě)法,并通過(guò) tsc --module amd
來(lái)輸出對(duì)應(yīng)的 JS 文件,同樣不需要修改原來(lái)的頁(yè)面文件。
但是,如果原來(lái)的 JS 文件沒(méi)有使用任何模塊框架的情況下,轉(zhuǎn)換為采用 ES6 模塊寫(xiě)法的 TS 代碼,在構(gòu)建的時(shí)候就會(huì)麻煩一點(diǎn)。這種情況下即使構(gòu)建成單一輸出文件,仍然會(huì)需要模塊化框架的支持,比如需要 AMD 的 define
和 require
,或者需要 System 的 API 支持。
為了避免引入模塊化框架,可以考慮以 commonjs 標(biāo)準(zhǔn)輸出 JS,然后通過(guò) Webpack 來(lái)把所有生成的 JS 打包成單一文件。這里既然用到了 Webpack,構(gòu)建配置就可以更靈活了,因?yàn)?Webpack 可以指定多個(gè) entry
,可以有多個(gè)輸出,它會(huì)通過(guò) import ...
轉(zhuǎn)譯成的 require(...)
自動(dòng)檢查依賴(lài)項(xiàng)。而且 Webpack 還可以使用 ts-loader
直接處理 .ts
文件而不需要先使用 tsc
來(lái)進(jìn)行轉(zhuǎn)譯。如果在 TS 中用到了高版本 ECMAScript 語(yǔ)法,比如 async/await
,還可以通過(guò) babel-loader
來(lái)增加一層處理……非常靈活。
但這里往往會(huì)有一個(gè)問(wèn)題,生成的 .js
中所有定義都不在全局范圍,那么腳本引入網(wǎng)頁(yè)之后,如何使用其中定義的內(nèi)容?這需要借助全局對(duì)象 window
——這里不需要考慮 Node.js 的全局對(duì)象 global
,因?yàn)樵?Node.js
下一般是采用模塊化的方式引入,不需要向全局對(duì)象注入什么東西。
向 window
注入對(duì)象(或函數(shù)、值等)的方法也很簡(jiǎn)單,分兩步:申明、賦值,比如:
import MyApi from "./myapi";
declare global {
interface Window {
mime: MyApi;
}
}
window.mime = new MyApi();
我們?cè)缙陧?xiàng)目中使用 TypeScript 的命名空間,不過(guò)最近幾乎都重構(gòu)成 ES6 模塊方式了。由于會(huì)用到 async 函數(shù),所以一般會(huì)配置 TypeScript 輸出 ES2017 代碼,再通過(guò) Babel 轉(zhuǎn)譯成 ES5 代碼,最后由 Webpack 打包輸出。
{
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"lib": [
"dom",
"es6",
"dom.iterable",
"scripthost",
"es2017"
],
"noImplicitAny": false,
"sourceMap": false
}
}
在 target
為 es5
或 es6
的時(shí)候,TypeScript 會(huì)有默認(rèn)的 lib
列表,這在官方文檔中有詳細(xì)說(shuō)明。target
定義為 es2017
是為了支持 async 函數(shù),但這個(gè)配置沒(méi)有默認(rèn) lib
列表,所以參考官方文檔對(duì) --target es6
使用的 lib
列表,補(bǔ)充 es2017
類(lèi)型庫(kù)即可。
這里使用了 Webpack2 的配置格式。
module.exports = {
entry: {
index: "./js/index"
},
output: {
filename: "[name].js"
},
devtool: "source-map",
resolve: {
extensions: [".ts"]
},
module: {
rules: [
{
test: /\.ts$/,
use: [
{
loader: "babel-loader",
options: {
presets: ["es2015", "stage-3"]
}
},
"ts-loader"
],
exclude: /node_modules/
}
]
}
};
如果還使用 gulp,任務(wù)是這樣寫(xiě)的
const gulp = require("gulp");
const gutil = require("gulp-util");
// 轉(zhuǎn)譯JavaScript
gulp.task("webpack", () => {
const webpack = require("webpack-stream");
const config = require("./webpack.config.js");
return gulp.src("./js/**/*.ts")
.pipe(webpack(config, require("webpack")))
.on("error", function(err) {
gutil.log(err);
this.emit("end");
})
.pipe(gulp.dest("../www/js"));
});
這里需要注意的是 webpack-stream 默認(rèn)使用的是 webpack1,而我們的配置需要 webpack2,所以為它指定第二個(gè)參數(shù),一個(gè)特定版本的 webpack 實(shí)例 (由 require("webpack")
導(dǎo)入的)。
從上面的構(gòu)建配置中不難總結(jié)出構(gòu)建過(guò)程需要安裝的 Node 模塊,有這樣一些
在 Node.js 中可以通過(guò) ts-node 包來(lái)直接運(yùn)行 TypeScript 代碼。需要做的只是在入口代碼文件(當(dāng)然是個(gè) .js
代碼)中添加一句
require('ts-node').register({ /* options */ })
或者
require('ts-node/register')
因?yàn)?Node.js 7.6 開(kāi)始已經(jīng)直接支持 async 函數(shù)語(yǔ)法,所以即使用到了這個(gè)語(yǔ)法,也不用擔(dān)心 ts-node 在內(nèi)存的轉(zhuǎn)譯結(jié)果不能運(yùn)行。
入口文件仍然必須是 .js
文件,這是個(gè)小小的遺憾,不過(guò)對(duì)于使用 Node.js 寫(xiě)構(gòu)建腳本的用戶(hù)來(lái)說(shuō),有兩個(gè)好消息:gulp 和 webpack 都直接支持 .ts
入口(或配置)文件。比如以 gulp 為例,可以定義 gulpfile.ts
(注意擴(kuò)展名是 .ts
) 如下
import * as gulp from "gulp";
gulp.task("hello", () => {
console.log("hello gulp");
});
不過(guò) gulp 也是通過(guò) ts-node 模塊來(lái)實(shí)現(xiàn)使用 TypeScript 的,而 ts-node 的功能依賴(lài)于 typescript,所以別忘了安裝這兩個(gè)模塊。
敬請(qǐng) 掃碼 關(guān)注〔邊城〕的公眾號(hào):邊城客棧
更多建議: