从 JavaScript 到 TypeScript - 模块化和构建

2017/04/10 10:32
阅读数 64

TypeScript 带来的最大好处就是静态类型检查,所以在从 JavaScript 转向 TypeScript 之前,一定要认识到添加类型定义会带来额外的工作量,这是必要的代价。不过,相对于静态类型检查带来的好处,这些代价是值得的。当然,TypeScript 允许不定义类型或者将所有类型定义为 any,但如果这样做,TypeScript 带来的大部分静态检查功能都会失去作用,换言之,也就没必要使用 TypeScript 了。

模块化

在转换之前还要注意的一个问题就是模块化。早期的 JavaScript 代码基本上是每个 HTML 页面对应一个或几个 JavaScript 脚本,那时候的 JavaScript 代码中很少有模块化的概念。不过随着 Web 2.0 的兴起,大量的工作从后端移到前端,JavaScript 程序变得越来越复杂,模块化成为刚需,大量的模块化框架随之而来,其中比较有名的有 RequestJS 及其带来的 AMD 标准,还有 SeaJS 带来的 CMD 标准。而随着 Node.js 的兴起以及 JavaScript 的全栈化,又有了 CommonJS 标准。之后又出现了广为使用的 SystemJS。当然少不了 ES6 的模块化标准,虽然到目前为止 Node.js 和大部分浏览器都还不支持它。

TypeScript 本身支持两种模块化方式,一种是对 ES6 的模块的微小扩展,另一种是在 ES6 发布之前本身模仿 C# 的命名空间。大部分使用命令空间的场景都可以使用 ES6 模块化标准来代替。我们先来看一看两种模块化方式区别。

命名空间

使用命令空间写的 TS 脚本在转译成 JS 后,可以不使用任何模块加载框架,直接在页面中加载即可使用。不过很遗憾,这种方式转义出来的 JS 程序不能直接在 Node.js 中使用。因为 tsc 不为会命名空间形式的模块生成 modules.exports 对象以及 require 语句。

有一种情况例外。将所有 .ts 文件转译成一个 .js,假设叫 all.js,那么它可以通过 node all 来运行。这种情况下不需要任何模块的导入导出。

不过在浏览器环境中,严格的按照依赖顺序引入生成的 .js 文件是可行的。早期没有使用模块化的 JS 文件就可以使用“命名空间”形式的模块化写法,甚至可以将原来成百上千行的大型 JS 源文件,拆分成若干小的 TS 文件,再通过 tsc --outfile 输出单一 JS 文件来使用,这样既能实现模块化重构,又能不改变原有的 HTML(或其它动态页面文件)的代码。

还有一点需要注意的是,在指定生成单一输出文件的情况下,TypeScript 不会通过代码逻辑去检查模块间的依赖关系。默认情况下它会按文件名的字母序逐个转译 .ts 文件,除非源文件中通过 /// <reference path="..." /> 明确指定了依赖项。

ES6 模块

在 TypeScript 使用 ES6 模块语法来实现模块化的情况下,tsc 允许通过 module 参数来指定生成的 .js 会应用于何种模块化框架,默认的是 commonjs,其它比较常用的还有 amdsystem 等。

显然,如果原来的 JS 程序使用了 AMD 框架,在转换成 TS 的时候,就可以使用 ES6 模块写法,并通过 tsc --module amd 来输出对应的 JS 文件,同样不需要修改原来的页面文件。

但是,如果原来的 JS 文件没有使用任何模块框架的情况下,转换为采用 ES6 模块写法的 TS 代码,在构建的时候就会麻烦一点。这种情况下即使构建成单一输出文件,仍然会需要模块化框架的支持,比如需要 AMD 的 definerequire,或者需要 System 的 API 支持。

为了避免引入模块化框架,可以考虑以 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 模块,有这样一些

  • gulp

  • gulp-util

  • webpack-stream

  • webpack

  • ts-loader

  • typescript

  • babel-loader

  • babel-core

  • babel-preset-es2015

  • babel-preset-stage-3

在 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,所以别忘了安装这两个模块。




本文分享自微信公众号 - 边城客栈(fancyidea-full)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部