当前位置: 首页 > 科技观察

从JavaScript到TypeScript——模块化与构建

时间:2023-03-17 21:52:23 科技观察

TypeScript最大的好处就是静态类型检查,所以在从JavaScript转到TypeScript之前,一定要意识到增加类型定义会带来额外的工作量,这是必须的代价。然而,这些成本值得静态类型检查带来的好处。当然,TypeScript不允许定义类型或将所有类型定义为any,但如果你这样做,TypeScript自带的静态检查功能大部分都会失效。换句话说,不需要使用TypeScript。模块化转换前需要注意的另一个问题是模块化。早期的JavaScript代码基本上是每个HTML页面对应一个或几个JavaScript脚本,当时的JavaScript代码几乎没有模块化的概念。但是随着Web2.0的兴起,很多工作从后端转移到了前端,JavaScript程序变得越来越复杂,模块化成为刚需,大量的模块化框架也随之而来,其中比较出名的有RequestJS及其SeaJS带来的AMD标准,以及SeaJS带来的CMD标准。随着Node.js的兴起和JavaScript的完整堆栈,出现了CommonJS标准。然后是广泛使用的SystemJS。当然,ES6模块化标准是少不了的,虽然目前Node.js和大部分浏览器都不支持。TypeScript本身支持两种模块化方式,一种是对ES6模块的小扩展,另一种是在ES6发布之前模仿C#的命名空间本身。大多数使用命名空间的场景都可以用ES6模块化标准来替代。我们先来看看这两种模块化方式的区别。写在namespace中的TS脚本翻译成JS后,可以直接在页面加载,不需要使用任何模块加载框架。遗憾的是,这样转义出来的JS程序在Node.js中是不能直接使用的。因为tsc不会为命名空间的模块生成modules.exports对象和require语句。有一个例外。将所有的.ts文件翻译成一个.js,假设叫all.js,那么就可以通过nodeall运行了。在这种情况下,不需要导入和导出任何模块。但是在浏览器环境下,严格按照依赖顺序导入生成的.js文件是可行的。早期可以不使用模块化的JS文件,而是使用“命名空间”形式的模块化写法,甚至可以将几百、几千行的大型JS源文件拆分成几个小的TS文件,然后通过tsc——-outfile输出单个JS文件供使用,可以实现模块化重构,无需改变原有HTML(或其他动态页面文件)的代码。另外需要注意的是,TypeScript在指定生成单个输出文件时,不会通过代码逻辑检查模块之间的依赖关系。默认情况下,它按文件名的字母顺序一个接一个地转译.ts文件,除非在源文件中通过///明确指定依赖项。ES6模块当TypeScript使用ES6模块语法实现模块化时,tsc允许您通过module参数指定生成的.js将应用到哪个模块化框架。默认是commonjs,其他常用的还有amd和systemwait。显然,如果原JS程序使用AMD框架,转TS时,可以使用ES6模块写法,使用tsc--moduleamd输出对应的JS文件,不需要修改原文件页面文件。但是,如果原始JS文件没有使用任何模块框架,转成ES6模块编写的TS代码,构建时会有点麻烦。在这种情况下,即使构建成单个输出文件,也需要模块化框架的支持,比如AMD的defineandrequire,或者System的API支持。为了避免引入模块化框架,可以考虑以commonjs标准输出JS,然后使用Webpack将所有生成的JS打包成一个文件。由于这里使用了Webpack,构建配置可以更加灵活,因为Webpack可以指定多入口多输出,通过require(...)翻译成import...会自动检查依赖关系。而且Webpack还可以使用ts-loader直接处理.ts文件而不使用tsc先翻译它们。如果在TS中使用更高版本的ECMAScript语法,比如async/await,还可以通过babel-loader加一层处理……非常灵活。但是这里经常会出现一个问题,生成的.js中的所有定义都不在全局范围内,那么脚本导入网页后如何使用其中定义的内容呢?这就需要使用全局对象window——这里不需要考虑Node.js的全球化对象global,因为在Node.js下一般都是以模块化的方式引入的,不需要往里面注入什么全局对象。向窗口注入对象(或函数、值等)的方法也很简单,分为声明和赋值两步,例如:importMyApifrom"./myapi";declareglobal{interfaceWindow{mime:MyApi;}}window.mime=newMyApi();常用的构建配置我们在早期的项目中使用了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}}当target为es5或es6时,TypeScript会有一个默认的lib列表,官方文档对此有详细说明。target定义为es2017以支持async函数,但是这个配置没有默认的lib列表,所以参考官方文档--targetes6使用的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/}]}};gulptask如果还是用gulp的话,task是这样写的constgulp=require("gulp");constgutil=require("gulp-util");//翻译JavaScriptgulp.task("webpack",()=>{constwebpack=require("webpack-stream");constconfig=require("./webpack.config.js");returngulp.src("./js/**/*.ts").pipe(webpack(config,require("webpack"))).on("error",function(err){gutil.log(err);this.emit("结束");}).pipe(gulp.dest("../www/js"));});这里需要注意的是webpack-stream默认使用webpack1,而我们的配置需要webpack2,所以为它指定第二个参数,具体版本的webpack实例(通过require("webpack")导入)。需要的Node模块从上面的构建配置不难总结出构建过程中需要安装的Node模块,比如gulpgulp-utilwebpack-streamwebpackts-loadertypescriptbabel-loaderbabel-corebabel-preset-es2015babel-preset-stage-3直接运行在Node.js环境中。ts可以通过ts-node包直接在Node.js中运行TypeScript代码。您需要做的就是在入口代码文件(当然是.js代码)中添加一个require('ts-node').register({/*options*/})或require('ts-node/register'))因为Node.js7.6已经直接支持了asyncfunction语法,所以即使使用这种语法也不用担心ts-node在内存中的翻译结果无法运行。入口文件还是必须是.js文件,有点遗憾,但是对于使用Node.js编写构建脚本的用户来说,有两个好消息:gulp和webpack都直接支持.ts入口(或配置)文件。例如,以gulp为例,你可以这样定义gulpfile.ts(注意扩展名是.ts)import*asgulpfrom"gulp";gulp.task("hello",()=>{console.log(“hellogulp”);});不过gulp也是通过ts-node模块使用TypeScript实现的,而ts-node的功能依赖于typescript,所以不要忘记安装这两个模块。