从JavaScript到TypeScript - 模块化和构建

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,其它比较常用的还有 amd、system 等。

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

但是,如果原来的 JS 文件没有使用任何模块框架的情况下,转换为采用 ES6 模块写法的 TS
代码,在构建的时候就会麻烦一点。这种情况下即使构建成单一输出文件,仍然会需要模块化框架的支持,比如需要 AMD 的 define 和
require,或者需要 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 注入对象(或函数、值等)的方法也很简单,分两步:申明、赋值,比如:


  1. import MyApi from "./myapi"; 
  2.  
  3. declare global { 
  4.     interface Window { 
  5.         mime: MyApi; 
  6.     } 
  7.  
  8. window.mime = new MyApi();  

常用的构建配置

我们早期项目中使用 TypeScript 的命名空间,不过最近几乎都重构成 ES6 模块方式了。由于会用到 async
函数,所以一般会配置 TypeScript 输出 ES2017 代码,再通过 Babel 转译成 ES5 代码,最后由 Webpack
打包输出。

tsconfig.json


  1.     "compilerOptions": { 
  2.         "module": "commonjs", 
  3.         "target": "es2017", 
  4.         "lib": [ 
  5.             "dom", 
  6.             "es6", 
  7.             "dom.iterable", 
  8.             "scripthost", 
  9.             "es2017" 
  10.         ], 
  11.         "noImplicitAny": false, 
  12.         "sourceMap": false 
  13.     } 
  14. }  

在 target 为 es5 或 es6 的时候,TypeScript 会有默认的 lib 列表,这在官方文档中有详细说明。target
定义为 es2017 是为了支持 async 函数,但这个配置没有默认 lib 列表,所以参考官方文档对 --target es6 使用的
lib 列表,补充 es2017 类型库即可。

webpack.config.js

这里使用了 Webpack2 的配置格式。


  1. module.exports = { 
  2.     entry: { 
  3.         index: "./js/index" 
  4.     }, 
  5.     output: { 
  6.         filename: "[name].js" 
  7.     }, 
  8.     devtool: "source-map", 
  9.     resolve: { 
  10.         extensions: [".ts"] 
  11.     }, 
  12.     module: { 
  13.         rules: [ 
  14.             { 
  15.                 test: /\.ts$/, 
  16.                 use: [ 
  17.                     { 
  18.                         loader: "babel-loader", 
  19.                         options: { 
  20.                             presets: ["es2015", "stage-3"] 
  21.                         } 
  22.                     }, 
  23.                     "ts-loader" 
  24.                 ], 
  25.                 exclude: /node_modules/ 
  26.             } 
  27.         ] 
  28.     } 
  29. };  

gulp task

如果还使用 gulp,任务是这样写的


  1. const gulp = require("gulp"); 
  2. const gutil = require("gulp-util"); 
  3.  
  4. // 转译JavaScript 
  5. gulp.task("webpack", () => { 
  6.     const webpack = require("webpack-stream"); 
  7.     const config = require("./webpack.config.js"); 
  8.     return gulp.src("./js/**/*.ts") 
  9.         .pipe(webpack(config, require("webpack"))) 
  10.         .on("error", function(err) { 
  11.             gutil.log(err); 
  12.             this.emit("end"); 
  13.         }) 
  14.         .pipe(gulp.dest("../www/js")); 
  15. });  

这里需要注意的是 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 代码)中添加一句


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

或者


  1. require('ts-node/register') 

因为 Node.js 7.6 开始已经直接支持 async 函数语法,所以即使用到了这个语法,也不用担心 ts-node 在内存的转译结果不能运行。

入口文件仍然必须是 .js 文件,这是个小小的遗憾,不过对于使用 Node.js 写构建脚本的用户来说,有两个好消息:gulp 和
webpack 都直接支持 .ts 入口(或配置)文件。比如以 gulp 为例,可以定义 gulpfile.ts (注意扩展名是 .ts)
如下


  1. import * as gulp from "gulp"; 
  2.  
  3. gulp.task("hello", () => { 
  4.     console.log("hello gulp"); 
  5. });  

不过 gulp 也是通过 ts-node 模块来实现使用 TypeScript 的,而 ts-node 的功能依赖于 typescript,所以别忘了安装这两个模块。

作者:边城

来源:51CTO

时间: 2025-01-29 18:11:01

从JavaScript到TypeScript - 模块化和构建的相关文章

Javascript和HTML5利用canvas构建Web五子棋游戏实现算法

这只是一个简单的JAVAscript和HTML5小程序,没有实现人机对战. 五子棋棋盘落子点对应的二维数组.数组的元素对应落子点.比如数组元素值为0表示该元素对应的落子点没有棋子,数组元素值为1表示该元素对应的落子点有白棋子,数组元素值为2表示该元素对应的落子点有黑棋子: 判断五子棋赢棋的算法是通过对五子棋棋盘落子点对应的二维数组的操作来实现的. 判断五子棋赢棋算法 下边的函数可以实现判断五子棋赢棋的算法,也可以按照教材中相应的算法实现. 其中函数的参数xx.yy为数组下标,chess数组实现五

javascript和HTML5利用canvas构建猜牌游戏实现算法

让我猜猜你心中的牌,先随机生成27张牌,不能重复列出三列牌,然后记住其中一张,然后点击牌所在的列,多次就可以猜出你想的牌,具体实现如下,感兴趣的朋友可以参考下哈   让我猜猜你心中的牌,先随机生成27张牌,不能重复列出三列牌,然后记住其中一张,然后点击牌所在的列,多次就可以猜出你想的牌. 如果是9张只要猜2次,如果是27张就是猜3次. 实现方法(27张): 如果点击了第三列,那就是说牌一定在这9张里面,就把第三列的9张牌平均给每列分3张,假设编号为123,456,789 再点击一次,如果点击第二

Javascript和HTML5利用canvas构建Web五子棋游戏实现算法_javascript技巧

这只是一个简单的JAVAscript和HTML5小程序,没有实现人机对战. 五子棋棋盘落子点对应的二维数组.数组的元素对应落子点.比如数组元素值为0表示该元素对应的落子点没有棋子,数组元素值为1表示该元素对应的落子点有白棋子,数组元素值为2表示该元素对应的落子点有黑棋子: 判断五子棋赢棋的算法是通过对五子棋棋盘落子点对应的二维数组的操作来实现的. 判断五子棋赢棋算法 下边的函数可以实现判断五子棋赢棋的算法,也可以按照教材中相应的算法实现. 其中函数的参数xx.yy为数组下标,chess数组实现五

javascript和HTML5利用canvas构建猜牌游戏实现算法_javascript技巧

让我猜猜你心中的牌,先随机生成27张牌,不能重复列出三列牌,然后记住其中一张,然后点击牌所在的列,多次就可以猜出你想的牌. 如果是9张只要猜2次,如果是27张就是猜3次. 实现方法(27张): 如果点击了第三列,那就是说牌一定在这9张里面,就把第三列的9张牌平均给每列分3张,假设编号为123,456,789 再点击一次,如果点击第二列,那么猜的牌就在456里面,再分到三列,4,5,6 再点击一次,就可以知道牌是哪个了. 实现算法: 我是使用一维数组实现,第一次猜第三列就把第三列的数据和0,1,2

Javascript模块化编程(一)模块的写法

随着网站逐渐变成"互联网应用程序",嵌入网页的Javascript代码越来越庞大,越来越复杂. 网页越来越像桌面程序,需要一个团队分工协作.进度管理.单元测试等等......开发者不得不使用软件工程的方法,管理网页的业务逻辑. Javascript模块化编程,已经成为一个迫切的需求.理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块. 但是,Javascript不是一种模块化编程语言,它不支持"类"(class),更遑论"模块&q

JavaScript的模块化:封装(闭包),继承(原型) 介绍

在复杂的逻辑下, JavaScript 需要被模块化,模块需要封装起来,只留下供外界调用的接口.闭包是 JavaScript 中实现模块封装的关键,也是很多初学者难以理解的要点   虽然 JavaScript 天生就是一副随随便便的样子,但是随着浏览器能够完成的事情越来越多,这门语言也也越来越经常地摆出正襟危坐的架势.在复杂的逻辑下, JavaScript 需要被模块化,模块需要封装起来,只留下供外界调用的接口.闭包是 JavaScript 中实现模块封装的关键,也是很多初学者难以理解的要点.最

JavaScript的模块化:封装(闭包),继承(原型) 介绍_javascript技巧

虽然 JavaScript 天生就是一副随随便便的样子,但是随着浏览器能够完成的事情越来越多,这门语言也也越来越经常地摆出正襟危坐的架势.在复杂的逻辑下, JavaScript 需要被模块化,模块需要封装起来,只留下供外界调用的接口.闭包是 JavaScript 中实现模块封装的关键,也是很多初学者难以理解的要点.最初,我也陷入迷惑之中.现在,我自信对这个概念已经有了比较深入的理解.为了便于理解,文中试图封装一个比较简单的对象. 我们试图在页面上维护一个计数器对象 ticker ,这个对象维护一

Javascript必须掌握的js库

JavaScript脚本库是一个预先用JavaScript语言写好的库.使用JavaScript库可以更轻松地开发基于JavaScript的应用程序,尤其是对于AJAX和其他以Web为中心的技术.当我们在为一个项目选定开发技术的时候,选择一个明星框架当然很不错,但是有些库文件太大了.当你想要为一个特定的任务寻找解决方案的时候,你可以选择一个更有针对性,更轻量级的框架. 本文整理了2015年1月15个可提高编程效率的JavaScript库,下面这些JavaScript库都是非常实用的,尤其是对于有

Top 10 JavaScript编辑器,你在用哪个?

对于JavaScript程序员来说,目前有很多很棒的工具可供选择.本文将会讨论10个优秀的支持JavaScript,HTML5和CSS开发,并且可以使用Markdown进行文档编写的文本编辑器.为什么使用编辑器而不是IDE进行JavaScript编程?原因就是速度快. 编辑器和IDE之间的本质区别在于:IDE不但可以调试,并且可以对代码进行概要分析,IDE还支持应用程序的生命周期管理(ALM)系统.我们在这里讨论的许多编辑器至少支持一个版本控制系统,通常是Git,现在IDE和编辑器之间的区别也越