typescript的模块
在开始介绍typescript的模块之前,笔者认为很有必要对es6新增的module语法进行一个介绍,其中包含了module和CommJs、AMD的比较:
ES6的module
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 、AMD 两种。前者用于服务器,后两者用于浏览器。
1、CommonJs
CommonJS是服务器端js模块化的规范,而NodeJS是这种规范的实现。 根据CommonJS规范,每一个文件就是一个模块,拥有自己独立的作用域,变量,以及方法等,对其他的模块都不可见。CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。require方法用于加载模块。
//定义一个module.js文件加粗样式
var A = function() {
console.log('我是定义的模块');
}
//导出这个模块
//1.第一种返回方式 module.exports = A;
//2.第二种返回方式 module.exports.test = A
//3.第三种返回方式 exports.test = A;
exports.test = A;
//再写一个test.js文件,去调用刚才定义好的模块,这两个文件在同一个目录下
var module = require("./module"); //加载这个模块
//调用这个模块,不同的返回方式用不同的方式调用
//1.第一种调用方式 module();
//2.第二种调用方式 module.test();
//3.第三种调用方式 module.test();
module.test();
//接下来我们去执行这个文件,前提是你本地要安装node.js,不多说了,自己百度安装。
//首先打开cmd, cd到这两个文件所在的目录下,执行: node test.js
node test.js
//输出结果:我是定义的模块
CommonJS 加载模块是同步的,所以只有加载完成才能执行后面的操作。像Node.js主要用于服务器的编程,加载的模块文件一般都已经存在本地硬盘,所以加载起来比较快,不用考虑异步加载的方式,所以CommonJS规范比较适用。但如果是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。所以就有了 AMD CMD 解决方案。
2、AMD
- AMD 全称为 Asynchromous Module Definition(异步模块定义) 。
- AMD 通过异步加载模块。模块加载不影响后面语句的运行。所有依赖某些模块的语句均放置在回调函数中
- AMD 规范只定义了一个函数 define,通过 define 方法定义模块。该函数的描述如下:
下面来看一下AMD的书写例子
(1)引入require.js
require.js可在官网requirejs.org/下载,并直接使用script引入
上面的代码使用了data-main属性,目的是为了等到require.js加载完成以后才去加载index.js,同时还使用了async和defer,async属性表明这个文件需要异步加载,避免网页失去响应。IE不支持这个属性,只支持defer,所以把defer写上
(2)使用define创建模块文件
// dependence.js
define(function() {
var minus = (x, y) => x - y;
return {
minus,
}
})
同时模块之间可以相互引用
// utils.js,和dependence.js在同一个文件夹底下
define(['./dependence'], function(dependence) {
var sum = (x, y) => x + y;
return {
sum,
minus: dependence.minus,
}
})
(3)使用require引入模块
可以用require.config方法来定义引入的js路径以及对应的别名,最后require方法中引入就行
// index.js
require.config({
paths: {
utils: './utils',
dependence: './dependence',
}
})
require(["utils", "dependence"], function (utils, dependence) {
console.log(utils.sum(10, 20), utils.minus(20, 10), dependence.minus(30, 10));
});
// 控制台输出30,10,20
require.js对一些非规范的模块可以使用require.config方法中的shim属性来进行处理,有兴趣的小伙伴可以自行到官网了解,这里不展开讨论
3、ES6的module
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。 模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。下面来看一下它们的用法:
(1)export
a、export + 变量声明
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
在这里需要注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系,下面这种写法是错误的:
// 报错
export 1;
// 报错
var m = 1;
export m;
b、export + { 变量名组合 }
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export { firstName, lastName, year };
还可以使用as关键字,重命名变量的对外输出名称
c、export default + 变量
// 正确
export var a = 1;
// 正确
var a = 1;
export default a;
// 错误
export default var a = 1
需要注意的是,函数和类的输出写法可以是两种不同的写法:
// 第一组
export default function crc32() { // 输出
// ...
}
import crc32 from 'crc32'; // 输入
// 第二组
export function crc32() { // 输出
// ...
};
import {crc32} from 'crc32'; // 输入
(2)import
使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。import语法具有以下几个特点:
a、import输入的变量都是只读的
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
b、import命令具有提升效果
foo(); // correct
import { foo } from 'my_module';
上面的代码不会报错,因为import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。
c、import语句会执行所加载的模块
// index.js
import { judge } from './components/test/import';
console.log(judge);
import './components/test/import'; // 如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。
// /components/test/import.js
console.log(123); // import引入以后,这个console会执行
export const judge: boolean = true;
d、import是静态执行(编译阶段),不能使用表达式和变量
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
(3)import()
前面说到,import和export语句只能在编译阶段执行,所以import和export语句只能在模块的顶层,不能在代码块之中(比如,在if代码块之中,或在函数之中)。这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。 我们来看一下commonjs的动态加载:
const path = './' + fileName;
const myModual = require(path); // require到底加载哪一个模块,只有运行时才知道。import命令做不到这一点。
为了解决这个问题,ES2020提案 引入import()函数,支持动态加载模块,并返回一个promise对象。import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。下面来看一下它的用法:
setTimeout(() => {
import('./components/test/import').then( module => {
console.log(module.judge);
})
}, 3000);
由于import()返回的是一个promise对象,所以promise相关的api都可以使用,例如使用Promise.all同时加载多个模块:
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});
或者用在async函数中
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
main();
4、ESModule VS CommonJs
前面我们介绍了Commonjs、AMD以及ESModule的相关知识,由于Commonjs和ESModule的使用范围较广,这个章节我们主要介绍ESModule以及Commonjs的区别:
(1)两者输出的值不相同
a、CommonJS 模块输出的是值的拷贝
值的拷贝,意味着一旦输出一个值,模块内部的变化就影响不到这个值
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
b、ESModule输出的是值的引用
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。 因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
(2)两者的加载时机不同
a、Commonjs在运行时加载
运行时加载意味着我们可以按需加载、条件加载、获得动态的模块路径,而ESModule的import()方法就是为了一统江湖,取代Node的require方法才诞生的。
b、ESModule在编译时加载
运行时加载意味着需要加载整个模块对象,但是编译时加载可以做到只加载模块的部分内容,webpack中的tree shaking就是基于这个来实现的。 在这里我们需要弄清楚的是,上面说到的运行时按需加载或者条件加载,针对的是多个模块根据条件选择适合当前使用的某个模块;而这里说的tree shaking,指的是不完整加载某个模块的全部内容,只加载引用的部分。 下面是webpack对于tree shaking的说明
typescript的module
TypeScript与ECMAScript 2015一样,任何包含顶级import或者export的文件都被当成一个模块。相反地,如果一个文件不带有顶级的import或者export声明,那么它的内容被视为全局可见的(因此对模块也是可见的)。 typescript的export和import用法与上面ESModule的用法完全一样,这里不展开描述。我们主要看一下typescript新增了什么特性:
1、export = 和 import = require()
CommonJS和AMD的exports都可以被赋值为一个对象, 这种情况下其作用就类似于 es6 语法里的默认导出,即 export default语法了。虽然作用相似,但是 export default 语法并不能兼容CommonJS和AMD的exports。 为了支持CommonJS和AMD的exports, TypeScript提供了export =以及import module = require("module")语法。
// src/components/test/commonImport.ts
const judge: boolean = true;
export = judge;
// src/index.ts
import { sum } from './components/test/import';
import judge = require('./components/test/commonImport');
console.log(sum(10, 20), judge);
根据编译时指定的模块目标参数,编译器会生成相应的供Node.js (CommonJS),Require.js (AMD),UMD,SystemJS或ECMAScript 2015 native modules (ES6)模块加载系统使用的代码。 为了编译,我们必需要在命令行上指定一个模块目标。对于Node.js来说,使用--module commonjs; 对于Require.js来说,使用--module amd。下面是转化的结果:
// commonjs
"use strict";
exports.__esModule = true;
var import_1 = require("./components/test/import");
var judge = require("./components/test/commonImport");
console.log(import_1.sum(10, 20), judge);
// amd
define(["require", "exports", "./components/test/import", "./components/test/commonImport"], function (require, exports, import_1, judge) {
"use strict";
exports.__esModule = true;
console.log(import_1.sum(10, 20), judge);
});
2、模块的解析
模块解析是指编译器在查找导入模块内容时所遵循的流程。假设有一个导入语句 import { a } from "moduleA"; 为了去检查任何对 a的使用,编译器需要准确的知道它表示什么,并且需要检查它的定义moduleA。
(1)根据路径定位文件
共有两种可用的模块解析策略:Node和Classic。 你可以使用 --moduleResolution标记来指定使用哪种模块解析策略。若未指定,那么在使用了 --module AMD | System | ES2015时的默认值为Classic,其它情况时则为Node。 a、classic
这种策略在以前是TypeScript默认的解析策略。 现在,它存在的理由主要是为了向后兼容。下面以相对导入和非相对导入模块为例进行介绍
- 相对导入的模块是相对于导入它的文件进行解析的
举个例子,/root/src/folder/A.ts文件里的import { b } from "./moduleB"会使用下面的查找流程:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
- 对于非相对模块的导入,编译器则会从包含导入文件的目录开始依次向上级目录遍历
举个例子,/root/src/folder/A.ts文件里的import { b } from "moduleB"会使用下面的查找流程:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts
b、node
这个解析策略试图在运行时模仿Node.js模块解析机制。 完整的Node.js解析算法可以在nodejs.org/api/modules…找到。 我们先来看一下nodejs是如何解析模块的,通常,在Node.js里导入是通过 require函数调用进行的。 Node.js会根据 require的是相对路径还是非相对路径做出不同的行为。
- 相对路径
假设有一个文件路径为 /root/src/moduleA.js,包含了一个导入var x = require("./moduleB"); Node.js以下面的顺序解析这个导入:
/root/src/moduleB.js
/root/src/moduleB/package.json(如果指定了main属性,nodejs就会引用main属性定义的文件)
/root/src/moduleB/index.js
- 非相对路径
非相对模块名的解析是个完全不同的过程。 Node会在一个特殊的文件夹 node_modules里查找你的模块。 node_modules可能与当前文件在同一级目录下,或者在上层目录里。 Node会向上级目录遍历,查找每个 node_modules直到它找到要加载的模块。 假设有一个文件路径为 /root/src/moduleA.js,包含了一个导入var x = require("moduleB"); Node.js以下面的顺序解析这个导入:
/root/src/node_modules/moduleB.js
/root/src/node_modules/moduleB/package.json (如果指定了"main"属性)
/root/src/node_modules/moduleB/index.js
/root/node_modules/moduleB.js
/root/node_modules/moduleB/package.json (如果指定了"main"属性)
/root/node_modules/moduleB/index.js
/node_modules/moduleB.js
/node_modules/moduleB/package.json (如果指定了"main"属性)
/node_modules/moduleB/index.j
TypeScript是模仿Node.js运行时的解析策略来在编译阶段定位模块定义文件。 因此,TypeScript在Node解析逻辑基础上增加了TypeScript源文件的扩展名( .ts,.tsx和.d.ts)。 同时,TypeScript在 package.json里使用字段"types"来表示类似"main"的意义 - 编译器会使用它来找到要使用的"main"定义文件。 同样的,我们也从相对路径和非相对路径进行举例
- 相对路径
假设有一个导入语句import { b } from "./moduleB"在/root/src/moduleA.ts里,会以下面的流程来定位"./moduleB":
/root/src/moduleB.ts
/root/src/moduleB.tsx
/root/src/moduleB.d.ts
/root/src/moduleB/package.json (如果指定了"types"属性)
/root/src/moduleB/index.ts
/root/src/moduleB/index.tsx
/root/src/moduleB/index.d.ts
- 非相对路径
类似地,非相对的导入会遵循Node.js的解析逻辑,首先查找文件,然后是合适的文件夹。 因此 /root/src/moduleA.ts文件里的import { b } from "moduleB"会以下面的查找顺序解析:
/root/src/node_modules/moduleB.ts
/root/src/node_modules/moduleB.tsx
/root/src/node_modules/moduleB.d.ts
/root/src/node_modules/moduleB/package.json (如果指定了"types"属性)
/root/src/node_modules/moduleB/index.ts
/root/src/node_modules/moduleB/index.tsx
/root/src/node_modules/moduleB/index.d.ts
/root/node_modules/moduleB.ts
/root/node_modules/moduleB.tsx
/root/node_modules/moduleB.d.ts
/root/node_modules/moduleB/package.json (如果指定了"types"属性)
/root/node_modules/moduleB/index.ts
/root/node_modules/moduleB/index.tsx
/root/node_modules/moduleB/index.d.ts
/node_modules/moduleB.ts
/node_modules/moduleB.tsx
/node_modules/moduleB.d.ts
/node_modules/moduleB/package.json (如果指定了"types"属性)
/node_modules/moduleB/index.ts
/node_modules/moduleB/index.tsx
/node_modules/moduleB/index.d.ts
(2)路径映射
a、baseUrl和paths
TypeScript编译器通过使用tsconfig.json文件里的"baseUrl"和"paths"来支持声明映射。 假设现在项目结构如下,
projectRoot
├── src
│ ├── app
│ │ ├── a.js
│ │ └── b.js
│ └── assets
│ ├── a.js
│ └── b.js
└── tsconfig.json
我们一般设置baseUrl为".",意思是找寻文件的根目录与tsconfig.json文件同级(即项目根目录)。当然,我们也可以根据项目实际情况进行调整。 根据上面的目录结构,可以在tsconfig.json文件这么设置paths:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@app/*": [ "src/app/*" ],
"@assets/*": [ "src/assets/*" ],
}
},
}
或者
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@app/*": [ "app/*" ],
"@assets/*": [ "assets/*" ],
}
},
}
进行设置后我们可以在根目录下的任意文件底下这么访问src/app/a.js文件
import **** from '@app/a.js';
b、rootDirs
有时多个目录下的工程源文件在编译时会进行合并放在某个输出目录下。利用rootDirs,可以告诉编译器生成这个虚拟目录的roots; 因此编译器可以在“虚拟”目录下解析相对模块导入,就 好像它们被合并在了一起一样。 比如,有下面的工程结构:
src
└── views
└── view1.ts (imports './template1')
└── view2.ts
generated
└── templates
└── views
└── template1.ts (imports './view2')
src/views里的文件是用于控制UI的用户代码。 generated/templates是UI模版,在构建时通过模版生成器自动生成。 构建中的一步会将 /src/views和/generated/templates/views的输出拷贝到同一个目录下。 在运行时,视图可以假设它的模版与它同在一个目录下,因此可以使用相对导入 "./template"。 可以使用"rootDirs"来告诉编译器。 "rootDirs"指定了一个roots列表,列表里的内容会在运行时被合并。 因此,针对这个例子, tsconfig.json如下:
{
"compilerOptions": {
"rootDirs": [
"src/views",
"generated/templates/views"
]
}
}
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!