我们先前所练习的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程式应该会怎么执行?
|
|
|
< 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
函数。
但如果将网页改成:
|
|
|
< 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(); |
|
|
|
< 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
关键字可以被用在最外层的var
、let
、const
、function
、class
、interface
、enum
、type
关键字前,使该项目暴露到模组外(换句话说就是可以被模组外的程式使用)。
例如:
|
export function f () { |
|
console .log( 'A' ); |
|
} |
export
关键字也可以不必与上述的var
、let
等关键字搭配使用,而是可以直接在其后接上一组大括号{}
,来放入要暴露的名称,如果有多个名称就用逗号,
隔开。
例如:
|
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.ts
的f
函数的哦!另外,像这样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.ts
的A
模组名称之下。
除了函数和类别之外,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.ts
和b.ts
档案编译成ES6的JavaScript模组,要如何直接在网页浏览器上使用它们呢?请看以下的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.ts
和b.ts
档案编译成AMD的JavaScript模组,只要修改tsconfig.json
设定档案中compilerOptions
栏位的module
栏位的值为AMD
字串即可。当然,记得也要将编译目标改为ES5
或是以下的版本。
如何在网页浏览器上使用AMD的JavaScript模组呢?请看以下的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.ts
和b.ts
档案编译成UMD的JavaScript模组,它们应该要可以像以下这样来被使用:
|
|
|
< 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的import
与from
关键字无法直接引用JavaScript模组,它必须要有.d.ts
档案才行。
例如以下这个JavaScript模组:
|
; |
|
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)功能。