TypeScript 学习之路第十六章:模组

作者:孙宇晨 来源:www.5idf.cn 2020-02-29   阅读:

我们先前所练习的TypeScript程式几乎都只是把程式叙述写在index.ts档案中,虽然我们已经会使用函数、类别来分割不同功能的程式,但当程式愈写愈多的时候,这样的作法还是会让程式变得难以维护。这时就需要用到TypeScript提供的「模组」系统了。

浅谈JavaScript的模组系统

试想一下,若要在一个网页中加入很多个JavaScript档案进来,每个JavaScript档案都是一个JavaScript模组,都拥有独立的功能。这些JavaScript档案要怎么样撰写才不会被彼此影响?

举例来说,a.js档案提供了一个f函数,b.js档案也提供了一个f函数。

 
function f () {
 
console .log( 'A' );
 
}
 
function f () {
 
console .log( 'B' );
 
}

那么以下网页中的JavaScript程式应该会怎么执行?


 
<!DOCTYPE html>
 
< html >
 
< head >
 
< meta charset = UTF-8 >
 
< script src = "a.js" > </ script >
 
< script src = "b.js" > </ script >
 
</ head >
 
< body >
 
< script >
 
f();
 
</ script >
 
</ body >
 
</ html >

答案是,当浏览器载入以上HTML的第10行时,最后(最近)被载入的f函数会被执行到。以上网页,b.js档案会比a.js档案还要晚被载入,所以会执行到b.js档案的f函数。

但如果将网页改成:


 
<!DOCTYPE html>
 
< html >
 
< head >
 
< meta charset = UTF-8 >
 
< script src = "a.js" > </ script >
 
< script src = "b.js" defer > </ script >
 
</ head >
 
< body >
 
< script >
 
f();
 
</ script >
 
</ body >
 
</ html >

以上网页,当浏览器载入以上HTML的第10行时,最后(最近)被载入的f函数是a.js档案的f函数,因为b.js档案被加上defer属性,它需要等到整个网页都读取完成后才会被载入。

由此可见,以上的a.js档案和b.js档案是会互相影响的,所以我们需要用不同的方式来实作它们的程式才行。最直觉的方式,就是将每个模组中的内容都用一层函数包起来,然后把要暴露(export)出去的属性都放进一个物件回传出来,再把这个物件被指派给一个变数来储存,该变数的名称便是模组名称。

例如:

 
function aFunction () {
 
return {
 
f : function f () {
 
console .log( 'A' );
 
}
 
};
 
}
 
 
 
var A = aFunction();
 
function bFunction () {
 
return {
 
f : function f () {
 
console .log( 'B' );
 
}
 
};
 
}
 
 
 
var B = bFunction();

 
<!DOCTYPE html>
 
< html >
 
< head >
 
< meta charset = UTF-8 >
 
< script src = "a.js" > </ script >
 
< script src = "b.js" > </ script >
 
</ head >
 
< body >
 
< script >
 
Af();
 
Bf();
 
</ script >
 
</ body >
 
</ html >

以上的作法,可不理会a.js档案和b.js档案的载入顺序,因为两个f函数此时已有各自的「命名空间」(namespace),并不会发生冲突。

事实上,我们还可以将aFunction函数和bFunction函数「匿名化」,如下:

 
var A = ( function () {
 
return {
 
f : function f () {
 
console .log( 'A' );
 
}
 
};
 
})();
 
var B = ( function () {
 
return {
 
f : function f () {
 
console .log( 'B' );
 
}
 
};
 
})();

这样的函数又被称为「立即被呼叫的函式」(Immediately Invoked Function Expression, IIFE),可以确保原先的aFunction函数和bFunction函数不能直接被外部程式呼叫,也不会与外部程式发生冲突。这种JavaScript的模组撰写方式称为「模组样式」(Module Pattern)。

不过「模组样式」顶多就是切分命名空间罢了,并无法处理模组与模组之间的相依关系(如重复引用等)。所以后来JavaScript生态圈又演化出了CommonJS和AMD(Asynchronous Module Definition)两种主要的模组系统,前者直接被Node.js内建(我们在前面的章节用的require函数就是CommonJS的东西),后者则是可以在网页浏览器引用额外的JavaScript档案来支援。

ES6出来之后,使用TypeScript来开发JavaScript程式,可以完全抛开模组样式、CommonJS和AMD,因为TypeScript可以直接将ES6的模组系统编译成其它的模组系统。换句话说,我们只需要学会ES6的模组系统就好了!

ES6的模组系统

export关键字

利用ES6之后加入的export关键字,我们可以将一个TypeScript的.ts档案变成一个TypeScript模组。export关键字可以被用在最外层的varletconstfunctionclassinterfaceenumtype关键字前,使该项目暴露到模组外(换句话说就是可以被模组外的程式使用)。

例如:

 
export function f () {
 
console .log( 'A' );
 
}

export关键字也可以不必与上述的varlet等关键字搭配使用,而是可以直接在其后接上一组大括号{},来放入要暴露的名称,如果有多个名称就用逗号,隔开。

例如:

 
export function f () {
 
console .log( 'A1' );
 
}
 
 
 
function f2 () {
 
console .log( 'A2' );
 
}
 
 
 
function f3 () {
 
console .log( 'A3' );
 
}
 
 
 
function f4 () {
 
console .log( 'A4' );
 
}
 
 
 
export {
 
f2
 
};
 
 
 
export {
 
f3,
 
f4
 
};

这边要注意的是,export关键字在一个.ts档案中是可以被使用多次的!

如果要改变暴露出去的名称,可以在大括号{}中用as关键字来设定,设定方式如下:

 
export function f () {
 
console .log( 'A1' );
 
}
 
 
 
function f2 () {
 
console .log( 'A2' );
 
}
 
 
 
function f3 () {
 
console .log( 'A3' );
 
}
 
 
 
function f4 () {
 
console .log( 'A4' );
 
}
 
 
 
export {
 
f2 as ff
 
};
 
 
 
export {
 
f3 as fff,
 
f4 as ffff
 
};

import关键字

我们在前面的章节学的import关键字用法其实并不属于ES6的用法,而是TypeScript针对ES5以下使用CommonJS模组系统提供的用法,姑且先把它忘了吧!

利用ES6之后加入的import关键字,我们可以在TypeScript中引用TypeScript(或是具有TypeScript的.d.ts定义档的JavaScript)的模组。

引用方式如下:

import * as自定义模组名称from  '模组档案路径(不含.ts副档名)' ;

注意这边的路径,如果是相对路径的话,要用./../来开头,不然会被当作是要引用JavaScript环境中的模组。

例如以下这个a.ts

 
export function f () {
 
console .log( 'A1' );
 
}
 
 
 
function f2 () {
 
console .log( 'A2' );
 
}
 
 
 
function f3 () {
 
console .log( 'A3' );
 
}
 
 
 
function f4 () {
 
console .log( 'A4' );
 
}
 
 
 
export {
 
f2 as ff
 
};
 
 
 
export {
 
f3 as fff,
 
f4 as ffff
 
};

可以这样来引用:

 
import * as A from './a' ;
 
 
 
Af();
 
A.ff();
 
A.fff();
 
A.ffff();

由于Node.js尚未完整支援ES6的模组系统,因此如果TypeScript的编译目标是设定为ES6或是以上的版本的话,还需要在tsconfig.json设定档中的compilerOptions栏位中加上module栏位,并将栏位值设为CommonJS字串,才能够让编译出来的JavaScript模组在Node.js中正常使用。

以上程式执行时会输出:

A1  A2  A3  A4

提醒一下,这边的引用a.ts档案的路径是填./a,而不是a另外,我们不需要将a.ts加进tsconfig.json设定档中的files栏位的阵列中,因为TypeScript会自动编译import关键字引用的.ts档案。

如果我们只是想引用模组中的部份项目的话,可以将* as 自定義模組名稱改写为一组大括号{},里面填入要引用的名称,如果有多个名称就用逗号,隔开。

例如:

 
import {f, ff} from './a' ;
 
 
 
f();
 
ff();

如果想要改变名称,就使用as关键字,用法如下:

 
import {f as f1, ff as f2} from './a' ;
 
 
 
f1();
 
f2();

import关键字也可以用来引用非模组的TypeScript档案。

例如以下这个b.ts

 
function f () {
 
console .log( 'B' );
 
}
 
 
 
f();

可以这样来引用:

 
import './b' ;

这边要注意的是,在index.ts中,是无法呼叫b.tsf函数的哦!另外,像这样import关键字没有搭配from关键字来使用时,除了能够引用任何的(模组或是非模组)TypeScript程式外,还可以引用任何的JavaScript程式。

重复暴露(Re-export)

export关键字也可以搭配from关键字来用,可以直接将指定的模组提供的名称再暴露出去,无法直接用* as 自定義模組名稱例如:

 
import {f as f1, ff as f2} from './a' ;
 
export {fff as f3, ffff as f4} from './a' ;
 
 
 
f1();
 
f2();

如果想要将整个模组更换名称后再暴露,要写成以下这个样子:

 
import {f as f1, ff as f2} from './a' ;
 
import * as A from './a' ;
 
 
 
f1();
 
 
 
export {A};

预设暴露(Default Export)

作用在function关键字前或是class关键字前的export关键字,其后可以加上default关键字,来将该函数或是类别当作预设的暴露对象。一个TypeScript模组只能有一个预设的暴露对象。

例如:

 
export function f () {
 
console .log( 'A' );
 
}

替函数f加上default关键字,如下:

 
export default function f () {
 
console .log( 'A' );
 
}

当我们要引用TypeScript模组的预设暴露对象时,就用如下的import关键字语法:

 
import f from './a' ;
 
 
 
f();

再举一个例子:

 
export default function f () {
 
console .log( 'A1' );
 
}
 
 
 
function f2 () {
 
console .log( 'A2' );
 
}
 
 
 
function f3 () {
 
console .log( 'A3' );
 
}
 
 
 
function f4 () {
 
console .log( 'A4' );
 
}
 
 
 
export {
 
f2 as ff
 
};
 
 
 
export {
 
f3 as fff,
 
f4 as ffff
 
};
 
import * as A from './a' ;
 
import f from './a' ;
 
 
 
f();
 
A.ff();
 
A.fff();
 
A.ffff();

注意这边由于a.ts的函数f是用export default来暴露,因此它并不会包含在index.tsA模组名称之下。

除了函数和类别之外,export default关键字也可以用在定数或者任意名称上。

例如:

 
export default 'magiclen.org' ;
 
import m from './c' ;
 
 
 
console .log(m);

以上程式会输出magiclen.org

再例如:

 
let n: number = 123456 ;
 
 
 
export default n;
 
import m from './c' ;
 
 
 
console .log(m);

以上程式会输出magiclen.org

在网页上使用TypeScript编译出来的JavaScript模组

现在有以下两个档案:

 
export function f () {
 
console .log( 'A' );
 
}
 
export function f () {
 
console .log( 'B' );
 
}

ES6 模组系统

若将a.tsb.ts档案编译成ES6的JavaScript模组,要如何直接在网页浏览器上使用它们呢?请看以下的HTML网页范例:


 
<!DOCTYPE html>
 
< html >
 
< head >
 
< meta charset = "utf-8" />
 
</ head >
 
< body >
 
< script type = "module" >
 
import * as A from './a.js' ;
 
import * as B from './b.js' ;
 
 
 
Af();
 
Bf();
 
</ script >
 
</ body >
 
</ html >

在HTML网页中新增script元素,并加上type="module"属性,接着就可以在这个script元素中撰写ES6的JavaScript程式了。直接用import关键字即可引用JavaScript模组。

AMD 模组系统

若要使网页相容比较旧,不支援ES6的网页浏览器,可以将a.tsb.ts档案编译成AMD的JavaScript模组,只要修改tsconfig.json设定档案中compilerOptions栏位的module栏位的值为AMD字串即可。当然,记得也要将编译目标改为ES5或是以下的版本。

如何在网页浏览器上使用AMD的JavaScript模组呢?请看以下的HTML网页范例:


 
<!DOCTYPE html>
 
< html >
 
< head >
 
< meta charset = "utf-8" />
 
< script src = "https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js" > </ script >
 
</ head >
 
< body >
 
< script >
 
requirejs([ './a.js' , './b.js' ], function ( A, B ) {
 
Af();
 
Bf();
 
});
 
</ script >
 
</ body >
 
</ html >

利用require.min.js提供的requirejs函数,可以帮我们引用AMD的JavaScript模组。

UMD(Universal Module Definition) 模组系统

理想的UMD模组应该要可以同时支援CommonJS、AMD以及root(即网页浏览器的window或Node.js的global),所以若将a.tsb.ts档案编译成UMD的JavaScript模组,它们应该要可以像以下这样来被使用:


 
<!DOCTYPE html>
 
< html >
 
< head >
 
< meta charset = "utf-8" />
 
< script src = "a.js" > </ script >
 
< script src = "b.js" > </ script >
 
</ head >
 
< body >
 
< script >
 
Af();
 
Bf();
 
</ script >
 
</ body >
 
</ html >

不过现阶段的TypeScript尚未实作这部份的功能,这个issue也还在讨论当中,咱们就先等等吧!

替任意JavaScript模组加上TypeScript的.d.ts档案

TypeScript的importfrom关键字无法直接引用JavaScript模组,它必须要有.d.ts档案才行。

例如以下这个JavaScript模组:

 
"use strict" ;
 
Object .defineProperty(exports, "__esModule" , { value : true });
 
function f () {
 
console .log( 'A' );
 
}
 
exports.f = f;

我们若想要将a.js引用至TypeScript程式中,就必须要先撰写正确的a.d.ts档案。内容如下:

 
export declare function f (): void ;

TypeScript官方有提供.d.ts档案的自动产生工具,有兴趣的读者们可以参考看看。

替TypeScript编译出来的JavaScript模组加上.d.ts档案

如果需要让TypeScript在编译程式的时候顺便产生出.d.ts档案,可以修改tsconfig.json设定档案中compilerOptions栏位的declaration栏位的值为true如此一来,被编译到的.ts档案都会产生出.d.ts档案。

用ES6的import关键字来引用Node.js的模组

至此我们已经学会了ES6的import关键字的用法了!来看看上一章提供的例子:


 
import fs = require ( 'fs' );
 
import path = require ( 'path' );
 
import util = require ( 'util' );
 
 
 
const INPUT_PATH = 'input' ;
 
const OUTPUT_FOLDER = 'folder' ;
 
const OUTPUT_NAME = 'output' ;
 
 
 
const pStat = util.promisify(fs.stat);
 
 
 
const pMkdir = util.promisify(fs.mkdir);
 
 
 
const pCopyFile = util.promisify(fs.copyFile);
 
 
 
const pUnlink = util.promisify(fs.unlink);
 
 
 
async function main () {
 
try {
 
const stats = await pStat(INPUT_PATH);
 
console .log(stats);
 
 
 
await pMkdir(OUTPUT_FOLDER, {recursive: true });
 
 
 
let outputPath = path.join(OUTPUT_FOLDER, OUTPUT_NAME);
 
await pCopyFile(INPUT_PATH, outputPath);
 
 
 
await pUnlink(INPUT_PATH);
 
 
 
console .log( 'Done!' );
 
} catch (err) {
 
console .error(err);
 
}
 
}
 
 
 
main();

在先前的章节有提到,import fs = require('fs');这样的import关键字用法用于TypeScript的编译目标是设为ES6或是以上的版本时会有问题,这是因为当我们把TypeScript的编译目标设为ES6或是以上的版本时,module栏位的预设值会变成ES6由于ES6的import关键字并不能被这样使用,所以就会导致程式编译失败。若要解决这个问题,只需要明确设定module栏位的值为CommonJS等别种模组系统就好了。

不过我们既然已经学会ES6的模组系统了,就应该要尽量去用它。以上程式可以用ES6的import关键字修改成这样:


 
import * as fs from 'fs' ;
 
import * as path from 'path' ;
 
import * as util from 'util' ;
 
 
 
const INPUT_PATH = 'input' ;
 
const OUTPUT_FOLDER = 'folder' ;
 
const OUTPUT_NAME = 'output' ;
 
 
 
const pStat = util.promisify(fs.stat);
 
 
 
const pMkdir = util.promisify(fs.mkdir);
 
 
 
const pCopyFile = util.promisify(fs.copyFile);
 
 
 
const pUnlink = util.promisify(fs.unlink);
 
 
 
async function main () {
 
try {
 
const stats = await pStat(INPUT_PATH);
 
console .log(stats);
 
 
 
await pMkdir(OUTPUT_FOLDER, {recursive: true });
 
 
 
let outputPath = path.join(OUTPUT_FOLDER, OUTPUT_NAME);
 
await pCopyFile(INPUT_PATH, outputPath);
 
 
 
await pUnlink(INPUT_PATH);
 
 
 
console .log( 'Done!' );
 
} catch (err) {
 
console .error(err);
 
}
 
}
 
 
 
main();

总结

在这个章节中我们学会了TypeScript的模组制作和引用的方式。下一个章节要来介绍TypeScript提供的命名空间(namespace)功能。

分享给小伙伴们:
如果本文侵犯了您的权利, 请联系本网立即做出处理,谢谢。
当前位置:孙宇晨博客 > 技术 > 《TypeScript 学习之路第十六章:模组转载请注明出处。
相关文章
  • 什么是机器学习?定义,类型

    什么是机器学习?定义,类型

  • TypeScript 学习之路第十七章:命名空间(namespace)

    TypeScript 学习之路第十七章:命名空间(namespace)

  • TypeScript学习之路第十八章:装饰器(Decorator)

    TypeScript学习之路第十八章:装饰器(Decorator)

  • 如何在Webpack中使用TypeScript?

    如何在Webpack中使用TypeScript?