1,模块化
模块化是指自上而下把一个复杂问题(功能)划分成若干模块的过程,在编程中就是指通过某种规则对程序(代码)进行分割、组织、打包,每个模块完成一个特定的子功能,再把所有的模块按照某种规则进行组装,合并成一个整体,最终完成整个系统的所有功能
从基于 Node.js 的服务端 commonjs 模块化,到前端基于浏览器的 AMD、CMD 模块化,再到 ECMAScript2015 开始原生内置的模块化, JavaScript 的模块化方案和系统日趋成熟。
TypeScript 也是支持模块化的,而且它的出现要比 ECMAScript模块系统标准化要早,所以在 TypeScript 中即有对 ECMAScript 模块系统的支持,也包含有一些自己的特点。。
2,模块化历程
- CommonJS
- AMD
- UMD
- ESM
无论是那种模块化规范,重点关注:保证模块独立性的同时又能很好的与其它模块进行交互
- 如何定义一个模块与模块内部私有作用域
- 通过何种方式导出模块内部数据
- 通过何种方式导入其它外部模块数据
3,基于服务端、桌面端的模块化
CommonJS
在早期,对于运行在浏览器端的 JavaScript 代码,模块化的需求并不那么的强烈,反而是偏向 服务端、桌面端 的应用对模块化有迫切的需求(相对来说,服务端、桌面端程序的代码和需求要复杂一些)。CommonJS 规范就是一套偏向服务端的模块化规范,它为非浏览器端的模块化实现制定了一些的方案和标准,NodeJS 就采用了这个规范。
独立模块作用域
一个文件就是模块,拥有独立的作用域
导出模块内部数据
通过 module.exports 或 exports 对象导出模块内部数据
// a.js
let a = 1;
let b = 2;
module.exports = {
x: a,
y: b
}
// or
exports.x = a;
exports.y = b;
导入外部模块数据
通过 require 函数导入外部模块数据
// b.js
let a = require('./a');
a.x;
a.y;
这些代码可以使用node去执行它
4,基于浏览器的模块化
AMD
因为 CommonJS 规范一些特性(基于文件系统,同步加载),它并不适用于浏览器端,所以另外定义了适用于浏览器端的规范
AMD(Asynchronous Module Definition)异步模块定义
注意:在AMD模块化中并不是一个文件表示一个模块,也就意味着在一个文件中可以定义多个模块(define),当时推荐一个文件中添加一个某块就够了。
浏览器并没有具体实现该规范的代码,我们可以通过一些第三方库来解决
requireJS(库)
// 1.html
//data-main 表明你要导入文件的入口文件,以为通常会出现模块引用着另外一个模块的情况,要把它们都导进来就需要确定入口文件
<script data-main="./js/a.s" src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js"></script>
独立模块作用域
通过一个 define 方法来定义一个模块,在该方法内部模拟模块独立作用域
// b.js
//与CommonJS规范不同得是,CommonJS规范对整个文件在运行过程中会参数独立得作用域
//在前端如果直接使用let a = 2 ...来写代码,那么会在加载过程中将其变为全局作用域
define(function() {
// 模块内部代码
let a = 1;
let b = 2;
//这样在这个文件define外部是不能调用let a 和 let b 的
})
导出模块内部数据(return)
通过 return 导出模块内部数据
// b.js
define(function() {
let a = 1;
let b = 2;
return {
x: a,
y: b
}
})
导入外部模块数据
通过前置依赖列表导入外部模块数据
// a.js
// 定义一个模块,并导入 ./b 模块
//它会将导入的数据默认的传到function的参数里
define(['./b'], function(b) {
console.log(b);
})
requireJS的 CommonJS 风格
require.js 也支持 CommonJS 风格的语法
导出模块内部数据
// b.js
define(function(require, exports, module) {
let a = 1;
let b = 2;
//module.exports === exports true
module.exports = {
x: a,
y: b
}
export.x=a;
export.y=b;
})
导入外部模块数据
// a.js
define(function(require, exports, module) {
let b = require('./b')
console.log(b);
})
UMD
严格来说,UMD 并不属于一套模块规范,它主要用来处理 CommonJS、AMD、CMD 的差异兼容,是模块代码能在前面不同的模块环境下都能正常运行。随着 Node.js 的流行,前端和后端都可以基于 JavaScript 来进行开发,这个时候或多或少的会出现前后端使用相同代码的可能,特别是一些不依赖宿主环境(浏览器、服务器)的偏低层的代码。我们能实现一套代码多端适用(同构),其中在不同的模块化标准下使用也是需要解决的问题,UMD 就是一种解决方式
(function (root, factory) {
//通过判断modle下有没有“object"对象,如果有就是CommonJS环境
if (typeof module === "object" && typeof module.exports === "object") {
// Node, CommonJS-like
module.exports = factory();
}
else if (typeof define === "function" && define.amd) {
// AMD 模块环境下 浏览器环境
define(factory);
} else {
// 不使用任何模块系统,直接挂载到全局
root.kkb = factory();
}
//下面()中的为向上面函数传递的参数
//this环境,如果在windows中为windows
}(this, function () {
let a = 1;
let b = 2;
// 模块导出数据
return {
x: a,
y: b
}
}));
这样子node和前端环境使用同样段代码就不需要写两次,而是通过写一个逻辑判断来看看是处于node环境还是前端环境
5,ESM
从 ECMAScript2015/ECMAScript6 开始,JavaScript 原生引入了模块概念,而且现在主流浏览器也都有了很好的支持,同时在 Node.js 也有了支持,所以未来基于 JavaScript 的程序无论是在前端浏览器还是在后端 Node.js 中,都会逐渐的被统一
独立模块作用域
一个文件就是模块,拥有独立的作用域,且导出的模块都自动处于 严格模式 下,即:'use strict'
ESM使用要求:script 标签需要声明 type="module"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="js/a.js" type="module"></script>
</body>
</html>
注意:通过这样导入文件,不能通过文件的在浏览器方式打开(这样导入的js是无效的),要通过服务器插件什么打开
导出模块内部数据
使用 export 语句导出模块内部数据
// 导出单个特性
export let name1, name2, …, nameN;
export let name1 = …, name2 = …, …, nameN;
export function FunctionName(){...}
export class ClassName {...}
// 导出列表
export { name1, name2, …, nameN };
// 重命名导出
export { variable1 as name1, variable2 as name2, …, nameN };
// 默认导出
export default expression;
export default function (…) { … }
export default function name1(…) { … }
export { name1 as default, … };
// 模块重定向导出
export * from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
export { default } from …;
import v {x,y} from "./b.js" //相当与结构,将b.js文件中导出的x,和y导出来
console.log(x,y,v) //1,2,3
//b.js
let a = 1;
let b = 2;
export var x =a;
export var y =b;
export default a+b;
导入外部模块数据
导入分为两种模式(与vue-router中的路由懒加载同理)
- 静态导入
- 动态导入
静态导入
使用 import 语句导入模块,这种方式称为:静态导入
静态导入方式不支持延迟加载,import 必须在模块的最开始
import defaultExport from "module-name";
import * as name from "module-name";
import { export } from "module-name";
import { export as alias } from "module-name";
import { export1 , export2 } from "module-name";
import { foo , bar } from "module-name/path/to/specific/un-exported/file";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";
document.onclick = function () {
// import 必须放置在当前模块最开始加载
// import m from './m.js'
// console.log(m);
}
动态导入
此外,还有一个类似函数的动态 import(),它不需要依赖 type="module" 的 script 标签。
关键字 import 可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个 promise
import('./m.js')
.then(m => {
//...
});
// 也支持 await
let m = await import('./m.js');
通过
import()方法导入返回的数据会被包装在一个对象中,即使是default也是如此
6,Ts中的模块化
TypeScript 也支持模块化,而且它的出现比 ESM 还要早,TypeScript 的模块化实现也有一些地方与上述其它一些模块化系统有所差异,但是随着 TypeScript 的更新,同时 ESM 标准本身也越来越成熟,所以当下和未来 TypeScript 的模块化也会与 ESM 越来越接近
TS 模块系统
虽然早期的时候,TypeScript 有一套自己的模块系统实现,但是随着更新,以及 JavaScript 模块化的日趋成熟,TypeScript 对 ESM 模块系统的支持也是越来越完善
模块
无论是 JavaScript 还是 TypeScript 都是以一个文件作为模块最小单元
- 任何一个包含了顶级
import或者export的文件都被当成一个模块 - 相反的一个文件不带有顶级的
import或者export,那么它的内容就是全局可见的
全局模块
如果一个文件中没有顶级 import 或者 export ,那么它的内容就是全局的,整个项目可见的
//a.ts和b.ts为与src目录下
// a.ts
let a1 = 100;
let a2 = 200;
// b.ts
// ok, 100因为a1为全局的
console.log(a1);
// error a2是已经定义的全局变量
let a1 = 300
不推荐使用全局模块,因为它会容易造成代码命名冲突(全局变量污染)
文件模块
任何一个包含了顶级 import 或者 export 的文件都会当做一个模块,在 TypeScript 中也称为外部模块。
// a.ts
let a1 = 100; //局部的
let a2 = 200;
export default {}; //有了这个a.ts则为模块文件
// b.ts
let a1 = 300 // ok
模块语法
TypeScript 与 ESM 语法类似
导出模块内部数据
使用 export 导出模块内部数据(变量、函数、类、类型别名、接口……)
导入外部模块数据
使用 import 导入外部模块数据
模块编译
TypeScript 编译器也能够根据相应的编译参数,把代码编译成指定的模块系统使用的代码
module 选项
在tsconfig.json文件中配置
在 TypeScript 编译选项中,module 选项是用来指定生成哪个模块系统的代码,可设置的值有:"none"、"commonjs"、"amd"、"udm"、"es6"/"es2015/esnext"、"System"
target=="es3" or "es5":默认使用commonjs- 其它情况,默认
es6
模块导出默认值的问题
如果一个模块没有默认导出
// m1.ts
//没有export default默认导出
export let obj = {
x: 1
}
则在引入该模块的时候,需要使用下列一些方式来导入
// main.ts
// error: 提示 m1 模块没有默认导出
//默认导入
import v from './m1'
// 可以简单的使用如下方式
import {obj} from './m1'
console.log(obj.x)
// ok
import * as m1 from './m1'
console.log(m1.obj.x)
加载非TS文件
有的时候,我们需要引入一些 js 的模块,比如导入一些第三方的使用 js 而非 ts 编写的模块,默认情况下 tsc 是不对非 ts 模块文件进行处理的
我们可以通过 allowJs 选项开启该特性:在config.json文件中:添加"allowJs":true
// m1.js
export default 100;
// main.ts
import m1 from './m1.js'
console.log(m1)
非 ESM 模块中的默认值问题
在 ESM 中模块可以设置默认导出值
export default '哈哈哈';
但是在 CommonJS 、AMD 中是没有默认值设置的,它们导出的是一个对象(exports)
//m1.js
module.exports.obj = {
x: 100
}
在 TypeScript 中导入这种模块的时候会出现 模块没有默认导出的错误提示。
简单一些的做法:
import * as m from './m1.js'
通过配置选项解决:
allowSyntheticDefaultImports
设置为:true,允许从没有设置默认导出的模块中默认导入。
虽然通过上面的方式可以解决编译过程中的检测问题,但是编译后的具体要运行代码还是有问题的
esModuleInterop
设置为:true,则在编译的同时生成一个 __importDefault 函数,用来处理具体的 default 默认导出
注意:以上设置只能当
module不为es6+的情况下有效
以模块的方式加载 JSON 格式的文件
TypeScript 2.9+ 版本添加了一个新的编译选项:resolveJsonModule,它允许我们把一个 JSON 文件作为模块进行加载
resolveJsonModule
设置为:true ,可以把 json 文件作为一个模块进行解析
data.json
{
"name": "zMouse",
"age": 35,
"gender": "男"
}
ts文件
import * as userData from './data.json';
console.log(userData.name);
7,模块解析策略
什么是模块解析
模块解析是指编译器在查找导入模块内容时所遵循的流程。
相对与非相对模块导入
根据模块引用是相对的还是非相对的,模块导入会以不同的方式解析。
相对导入
相对导入是以 /、./ 或 ../ 开头的引用
// 导入根目录下的 m1 模块文件
import m1 from '/m1'
// 导入当前目录下的 mods 目录下的 m2 模块文件
import m2 from './mods/m2'
// 导入上级目录下的 m3 模块文件
import m3 from '../m3'
非相对导入
所有其它形式的导入被当作非相对的
import m1 from 'm1'
模块解析策略
为了兼容不同的模块系统(CommonJS、ESM),TypeScript 支持两种不同的模块解析策略:Node、Classic,当 --module 选项为:AMD、System、ES2015 的时候,默认为 Classic ,其它情况为 Node
--moduleResolution 选项
除了根据 --module 选项自动选择默认模块系统类型,我们还可以通过 --moduleResolution 选项来手动指定解析策略
// tsconfig.json
{
...,
"moduleResolution": "node"
}
Classic 模块解析策略
该策略是 TypeScript 以前的默认解析策略,它已经被新的 Node 策略所取代,现在使用该策略主要是为了向后兼容
相对导入
// /src/m1/a.ts
import b from './b.ts'
解析查找流程:
- src/m1/b.ts
默认后缀补全
// /src/m1/a.ts
import b from './b'
解析查找流程:
-
/src/m1/b.ts
-
/src/m1/b.d.ts
非相对导入
// /src/m1/a.ts
import b from 'b'
对于非相对模块的导入,则会从包含导入文件的目录开始依次向上级目录遍历查找,直到根目录为止
-
/src/m1/b.ts
-
/src/m1/b.d.ts
-
/src/b.ts
-
/src/b.d.ts
-
/b.ts
-
/b.d.ts
Node 模块解析策略
该解析策略是参照了 Node.js 的模块解析机制
相对导入
// node.js
// /src/m1/a.js
import b from './b'
在 Classic 中,模块只会按照单个的文件进行查找,但是在 Node.js 中,会首先按照单个文件进行查找,如果不存在,则会按照目录进行查找(比如如果找,没有找到,那么就会去找叫b的文件夹)
- /src/m1/b.js
- /src/m1/b/package.json中'main'中指定的文件
- /src/m1/b/index.js
文件夹查找机制:先找文件,文件找不到就去找目录名同的文件,如果里面有index.js那么它将会引用它,如果不叫index.js/ts文件,那么可以在词文件夹下创建一个package.json文件来配置其默认导出的文件名,添加"main": xxx
非相对导入
// node.js
// /src/m1/a.js
import b from 'b'
对于非相对导入模块,解析是很特殊的,Node.js 会这一个特殊文件夹 node_modules 里查找,并且在查找过程中从当前目录的 node_modules 目录下逐级向上级文件夹进行查找
- /src/m1/node_modules/b.js
- /src/m1/node_modules/b/package.json中'main'中指定的文件
- /src/m1/node_modules/b/index.js
- /src/node_modules/b.js
- /src/node_modules/b/package.json中'main'中指定的文件
- /src/node_modules/b/index.js
- /node_modules/b.js
- /node_modules/b/package.json中'main'中指定的文件
- /node_modules/b/index.js