從JavaScript到TypeScript - 模塊化和構(gòu)建
TypeScript 帶來(lái)的***好處就是靜態(tài)類型檢查,所以在從 JavaScript 轉(zhuǎn)向 TypeScript 之前,一定要認(rèn)識(shí)到添加類型定義會(huì)帶來(lái)額外的工作量,這是必要的代價(jià)。不過(guò),相對(duì)于靜態(tài)類型檢查帶來(lái)的好處,這些代價(jià)是值得的。當(dāng)然,TypeScript 允許不定義類型或者將所有類型定義為 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)格的按照依賴順序引入生成的 .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ò)代碼邏輯去檢查模塊間的依賴關(guān)系。默認(rèn)情況下它會(huì)按文件名的字母序逐個(gè)轉(zhuǎn)譯 .ts 文件,除非源文件中通過(guò) /// <reference path="..." /> 明確指定了依賴項(xiàng)。
ES6 模塊
在 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)檢查依賴項(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();
常用的構(gòu)建配置
我們?cè)缙陧?xiàng)目中使用 TypeScript 的命名空間,不過(guò)最近幾乎都重構(gòu)成 ES6 模塊方式了。由于會(huì)用到 async 函數(shù),所以一般會(huì)配置 TypeScript 輸出 ES2017 代碼,再通過(guò) Babel 轉(zhuǎn)譯成 ES5 代碼,***由 Webpack 打包輸出。
tsconfig.json
- {
- "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 類型庫(kù)即可。
webpack.config.js
這里使用了 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 task
如果還使用 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)入的)。
需要的 Node 模塊
從上面的構(gòu)建配置中不難總結(jié)出構(gòu)建過(guò)程需要安裝的 Node 模塊,有這樣一些
- gulp
- gulp-util
- webpack-stream
- webpack
- ts-loader
- typescript
- babel-loader
- babel-core
- babel-preset-es2015
- babel-preset-stage-3
在 Node.js 環(huán)境直接運(yùn)行 .ts
在 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)建腳本的用戶來(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 的功能依賴于 typescript,所以別忘了安裝這兩個(gè)模塊。