从 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,因此别忘了安装这两个模块。

扩展阅读


关注做者的公众号“边城客栈” →

相关文章
相关标签/搜索