從 JavaScript 到 TypeScript - 模塊化和構建

TypeScript 帶來的最大好處就是靜態類型檢查,因此在從 JavaScript 轉向 TypeScript 以前,必定要認識到添加類型定義會帶來額外的工做量,這是必要的代價。不過,相對於靜態類型檢查帶來的好處,這些代價是值得的。固然,TypeScript 容許不定義類型或者將全部類型定義爲 any,但若是這樣作,TypeScript 帶來的大部分靜態檢查功能都會失去做用,換言之,也就不必使用 TypeScript 了。javascript

模塊化

在轉換以前還要注意的一個問題就是模塊化。早期的 JavaScript 代碼基本上是每一個 HTML 頁面對應一個或幾個 JavaScript 腳本,那時候的 JavaScript 代碼中不多有模塊化的概念。不過隨着 Web 2.0 的興起,大量的工做從後端移到前端,JavaScript 程序變得愈來愈複雜,模塊化成爲剛需,大量的模塊化框架隨之而來,其中比較有名的有 RequestJS 及其帶來的 AMD 標準,還有 SeaJS 帶來的 CMD 標準。而隨着 Node.js 的興起以及 JavaScript 的全棧化,又有了 CommonJS 標準。以後又出現了廣爲使用的 SystemJS。固然少不了 ES6 的模塊化標準,雖然到目前爲止 Node.js 和大部分瀏覽器都還不支持它。html

TypeScript 自己支持兩種模塊化方式,一種是對 ES6 的模塊的微小擴展,另外一種是在 ES6 發佈以前自己模仿 C# 的命名空間。大部分使用命令空間的場景均可以使用 ES6 模塊化標準來代替。咱們先來看一看兩種模塊化方式區別。前端

命名空間

使用命令空間寫的 TS 腳本在轉譯成 JS 後,能夠不使用任何模塊加載框架,直接在頁面中加載便可使用。不過很遺憾,這種方式轉義出來的 JS 程序不能直接在 Node.js 中使用。由於 tsc 不爲會命名空間形式的模塊生成 modules.exports 對象以及 require 語句。java

有一種狀況例外。將全部 .ts 文件轉譯成一個 .js,假設叫 all.js,那麼它能夠經過 node all 來運行。這種狀況下不須要任何模塊的導入導出。node

不過在瀏覽器環境中,嚴格的按照依賴順序引入生成的 .js 文件是可行的。早期沒有使用模塊化的 JS 文件就可使用「命名空間」形式的模塊化寫法,甚至能夠將原來成百上千行的大型 JS 源文件,拆分紅若干小的 TS 文件,再經過 tsc --outfile 輸出單一 JS 文件來使用,這樣既能實現模塊化重構,又能不改變原有的 HTML(或其它動態頁面文件)的代碼。webpack

還有一點須要注意的是,在指定生成單一輸出文件的狀況下,TypeScript 不會經過代碼邏輯去檢查模塊間的依賴關係。默認狀況下它會按文件名的字母序逐個轉譯 .ts 文件,除非源文件中經過 /// <reference path="..." /> 明確指定了依賴項。es6

ES6 模塊

在 TypeScript 使用 ES6 模塊語法來實現模塊化的狀況下,tsc 容許經過 module 參數來指定生成的 .js 會應用於何種模塊化框架,默認的是 commonjs,其它比較經常使用的還有 amdsystem 等。web

顯然,若是原來的 JS 程序使用了 AMD 框架,在轉換成 TS 的時候,就可使用 ES6 模塊寫法,並經過 tsc --module amd 來輸出對應的 JS 文件,一樣不須要修改原來的頁面文件。typescript

可是,若是原來的 JS 文件沒有使用任何模塊框架的狀況下,轉換爲採用 ES6 模塊寫法的 TS 代碼,在構建的時候就會麻煩一點。這種狀況下即便構建成單一輸出文件,仍然會須要模塊化框架的支持,好比須要 AMD 的 definerequire,或者須要 System 的 API 支持。npm

爲了不引入模塊化框架,能夠考慮以 commonjs 標準輸出 JS,而後經過 Webpack 來把全部生成的 JS 打包成單一文件。這裏既然用到了 Webpack,構建配置就能夠更靈活了,由於 Webpack 能夠指定多個 entry,能夠有多個輸出,它會經過 import ... 轉譯成的 require(...) 自動檢查依賴項。並且 Webpack 還可使用 ts-loader 直接處理 .ts 文件而不須要先使用 tsc 來進行轉譯。若是在 TS 中用到了高版本 ECMAScript 語法,好比 async/await,還能夠經過 babel-loader 來增長一層處理……很是靈活。

但這裏每每會有一個問題,生成的 .js 中全部定義都不在全局範圍,那麼腳本引入網頁以後,如何使用其中定義的內容?這須要藉助全局對象 window——這裏不須要考慮 Node.js 的全局對象 global,由於在 Node.js 下通常是採用模塊化的方式引入,不須要向全局對象注入什麼東西。

window 注入對象(或函數、值等)的方法也很簡單,分兩步:申明、賦值,好比:

import MyApi from "./myapi";

declare global {
    interface Window {
        mime: MyApi;
    }
}

window.mime = new MyApi();

經常使用的構建配置

咱們早期項目中使用 TypeScript 的命名空間,不過最近幾乎都重構成 ES6 模塊方式了。因爲會用到 async 函數,因此通常會配置 TypeScript 輸出 ES2017 代碼,再經過 Babel 轉譯成 ES5 代碼,最後由 Webpack 打包輸出。

tsconfig.json

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es2017",
        "lib": [
            "dom",
            "es6",
            "dom.iterable",
            "scripthost",
            "es2017"
        ],
        "noImplicitAny": false,
        "sourceMap": false
    }
}

targetes5es6 的時候,TypeScript 會有默認的 lib 列表,這在官方文檔中有詳細說明。target 定義爲 es2017 是爲了支持 async 函數,但這個配置沒有默認 lib 列表,因此參考官方文檔對 --target es6 使用的 lib 列表,補充 es2017 類型庫便可。

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,任務是這樣寫的

const gulp = require("gulp");
const gutil = require("gulp-util");

// 轉譯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 默認使用的是 webpack1,而咱們的配置須要 webpack2,因此爲它指定第二個參數,一個特定版本的 webpack 實例 (由 require("webpack") 導入的)。

須要的 Node 模塊

從上面的構建配置中不難總結出構建過程須要安裝的 Node 模塊,有這樣一些

在 Node.js 環境直接運行 .ts

在 Node.js 中能夠經過 ts-node 包來直接運行 TypeScript 代碼。須要作的只是在入口代碼文件(固然是個 .js 代碼)中添加一句

require('ts-node').register({ /* options */ })

或者

require('ts-node/register')

由於 Node.js 7.6 開始已經直接支持 async 函數語法,因此即便用到了這個語法,也不用擔憂 ts-node 在內存的轉譯結果不能運行。

入口文件仍然必須是 .js 文件,這是個小小的遺憾,不過對於使用 Node.js 寫構建腳本的用戶來講,有兩個好消息:gulp 和 webpack 都直接支持 .ts 入口(或配置)文件。好比以 gulp 爲例,能夠定義 gulpfile.ts (注意擴展名是 .ts) 以下

import * as gulp from "gulp";

gulp.task("hello", () => {
    console.log("hello gulp");
});

不過 gulp 也是經過 ts-node 模塊來實現使用 TypeScript 的,而 ts-node 的功能依賴於 typescript,因此別忘了安裝這兩個模塊。

擴展閱讀


關注做者的公衆號「邊城客棧」 →

相關文章
相關標籤/搜索