From:https://segmentfault.com/a/1190000018249137?utm_source=tag-newest
- Github - allowSyntheticDefaultImports should be the default?
- exports、module.exports和export、export deault到底是咋回事
JavaScript 中有多种 export 的方式,而 TypeScript 中针对这种情况做了多种 import 语法,最常见的就是 import * as path from 'path' 这种。下面来讲解 TypeScript 中不同的 import 具有什么意义。
1、从 export 说起
关于 TypeScript 中不同 import
的含义,最典型的就是下面的 import
语法:
import * as path from 'path'
不少人疑问这句代码究竟是什么意思,这里要先从 js 的 export
开始说。
首先,JavaScript 的模块化方案,在历史的演进中,有多种导出模块的方式:
exports
module.exports
export
export default
在 nodejs 中内置的模块遵循的都是 CommonJS 规范,语法为 module.exports
和 exports
。
// 模块中的 exports 变量指向 module.exports
// 这篇文章不会深入讲解 module.exports 和 exports 的关系module.exports = function () { }
exports.site = 'https://tasaid.com'
module.exports.name = 'linkFly'
例如 nodejs
内置的 events 模块的源码:
在 ECMAScript 6 中又新增了语法 export
和 export default
:
export default function () { }
export const site = 'https://tasaid.com'
export const name = 'linkFly'
到这里画风还比较正常,而大名鼎鼎的 JavaScript 转码编译器 babel 针对 ECMAScript 6 新增的 export default
语法,搞了个 babel-plugin-transform-es2015-modules-commonjs 的转换插件,用于将 ECMAScript 6 转码为 CommonJs 规范的语法:
源码:export default 42;
编译后:
Object.defineProperty(exports, "__esModule", {value: true
});exports.default = 42;
到这里,我们看到有三种 export
默认值的语法:
module.exports = function () {} // commonjs
exports.default = function () {} // babel 转码
export default function () {} // es6
2、JavaScript 中的 import
:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import
静态的import
语句用于导入由另一个模块导出的绑定。无论是否声明了 strict mode ,导入的模块都运行在严格模式下。在浏览器中,import
语句只能在声明了 type="module"
的 script
的标签中使用。
此外,还有一个类似函数的动态 import()
,它不需要依赖 type="module"
的script标签。
在 script 标签中使用 nomodule
属性,可以确保向后兼容。
在您希望按照一定的条件或者按需加载模块的时候,动态import()
是非常有用的。而静态型的 import
是初始化加载依赖项的最优选择,使用静态 import
更容易从代码静态分析工具和 tree shaking 中受益。
语法
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";var promise = import("module-name"); //这是一个处于第三阶段的提案。
defaultExport
导入模块的默认导出接口的引用名。module-name
要导入的模块。通常是包含目标模块的.js
文件的相对或绝对路径名,可以不包括.js
扩展名。某些特定的打包工具可能允许或需要使用扩展或依赖文件,它会检查比对你的运行环境。只允许单引号和双引号的字符串。name
导入模块对象整体的别名,在引用导入模块时,它将作为一个命名空间来使用。export, exportN
被导入模块的导出接口的名称。alias, aliasN
将引用指定的导入的名称。
2.1 描述
name 参数是 "导入模块对象" 的名称,它将用一种名称空间来引用导入模块的接口。export 参数指定单个的命名导出,而 import * as name 语法导入所有导出接口,即导入模块整体。
导入整个模块的内容 ( import * as myModule from 'xxx.js' )
这将 myModule 插入当前作用域,其中包含来自位于 /modules/my-module.js 文件中导出的所有接口。
import * as myModule from '/modules/my-module.js';
在这里,访问导出接口意味着使用模块名称(在本例为 "myModule" )作为命名空间。例如,如果上面导入的模块包含一个接口 doAllTheAmazingThings(),你可以这样调用:
myModule.doAllTheAmazingThings();
导入单个接口 ( import {myExport} from 'xxx.js'; )
给定一个名为 myExport 的对象或值,它已经从模块 my-module 导出(因为整个模块被导出)或显式地导出(使用 export 语句),将 myExport 插入当前作用域。
import {myExport} from '/modules/my-module.js';
导入多个接口 ( import {foo, bar} from 'xxx.js'; )
这将 foo 和 bar 插入当前作用域。
import {foo, bar} from '/modules/my-module.js';
导入带有别名的接口 ( import {x as y} from 'zzz.js'; )
可以在导入时重命名接口。例如,将 shortName 插入当前作用域。
import {reallyLongModuleExportName as shortName} from '/modules/my-module.js';
导入时重命名多个接口
使用别名导入模块的多个接口。
import {reallyReallyLongModuleMemberName as shortName,anotherLongModuleName as short
} from '/modules/my-module.js';
仅为副作用而导入一个模块 ( import 'xxx.js'; )
整个模块仅为副作用(中性词,无贬义含义)而导入,而不导入模块中的任何内容(接口)。 这将运行模块中的全局代码, 但实际上不导入任何值。
import '/modules/my-module.js';
导入默认值
引入模块可能有一个 default export(无论它是对象,函数,类等)可用。然后可以使用 import 语句来导入这样的默认接口。最简单的用法是直接导入默认值:
import myDefault from '/modules/my-module.js';
也可以同时将 default 语法与上述用法(命名空间导入或命名导入)一起使用。在这种情况下,default 导入必须首先声明。 例如:
import myDefault, * as myModule from '/modules/my-module.js';
// myModule used as a namespace
或者
import myDefault, {foo, bar} from '/modules/my-module.js';
// specific, named imports
当用动态导入的方式导入默认导出时,其工作方式有所不同。你需要从返回的对象中解构并重命名 "default" 键。
(async () => {if (somethingIsTrue) {const { default: myDefault, foo, bar } = await import('/modules/my-module.js');}
})();
动态 import
标准用法的 import 导入的模块是静态的,会使所有被导入的模块,在加载时就被编译(无法做到按需编译,降低首页加载速度)。有些场景中,你可能希望根据条件导入模块或者按需导入模块,这时你可以使用动态导入代替静态导入。下面的是你可能会需要动态导入的场景:
- 当静态导入的模块很明显的降低了代码的加载速度且被使用的可能性很低,或者并不需要马上使用它。
- 当静态导入的模块很明显的占用了大量系统内存且被使用的可能性很低。
- 当被导入的模块,在加载时并不存在,需要异步获取
- 当导入模块的说明符,需要动态构建。(静态导入只能使用静态说明符)
- 当被导入的模块有副作用(这里说的副作用,可以理解为模块中会直接运行的代码),这些副作用只有在触发了某些条件才被需要时。(原则上来说,模块不能有副作用,但是很多时候,你无法控制你所依赖的模块的内容)
请不要滥用动态导入(只有在必要情况下采用)。静态框架能更好的初始化依赖,而且更有利于静态分析工具和 tree shaking 发挥作用
关键字 import 可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个 promise
import('/modules/my-module.js').then((module) => {// Do something with the module.});
这种使用方式也支持 await
关键字。
let module = await import('/modules/my-module.js');
2.2 示例
标准导入
下面的代码将会演示如何从辅助模块导入以协助处理 AJAX JSON 请求。
模块:file.js
function getJSON(url, callback) {let xhr = new XMLHttpRequest();xhr.onload = function () {callback(this.responseText)};xhr.open('GET', url, true);xhr.send();
}export function getUsefulContents(url, callback) {getJSON(url, data => callback(JSON.parse(data)));
}
主程序:main.js
import { getUsefulContents } from '/modules/file.js';getUsefulContents('http://www.example.com',data => { doSomethingUseful(data); });
动态导入
此示例展示了如何基于用户操作去加载功能模块到页面上,在例子中通过点击按钮,然后会调用模块内的函数。当然这不是能实现这个功能的唯一方式,import()
函数也可以支持await
。
const main = document.querySelector("main");
for (const link of document.querySelectorAll("nav > a")) {link.addEventListener("click", e => {e.preventDefault();import('/modules/my-module.js').then(module => {module.loadPageInto(main);}).catch(err => {main.textContent = err.message;});});
}
2.3 规范
Specification |
---|
"function-like" dynamic import() proposal |
ECMAScript (ECMA-262)Imports |
Report problems with this compatibility data on GitHub
3、TypeScript 中的 import
在 TypeScript 中,也有多种 import
的方式。
import * as xx from 'xx' // commonjs 模块
import xx from 'xx' // es6 模块
import xx = require('xx') // commonjs 模块,类型声明为 export = xx
const xx = require('xx') // 没有类型声明,默认导入 any 类型
在 tsconfig.json
中,allowSyntheticDefaultImports
会影响到 import 语法的类型检查规则,这个下面再说。
import * as xx from 'xx'
import * as xx from 'xx'
的语法来一般都是用来导入使用 module.exports
导出的模块。
import * as path from 'path'
因为 nodejs 中的模块大部分都是通过 module.exports
、exports.xx
语法进行导出的。
import xx from 'xx'
默认情况下,import xx from 'xx'
的语法只适用于 ECMAScript 6 的 export default
导出:
模块 foo:
export default function () { console.log('https://tasaid.com')
}
ES6 模块的导入:
import foo from './foo'
foo()
而前面我们说了,babel
会将 es6 的模块的 export default
语法编译为 exports.default
语法。
而 TypeScript 默认是不识别这种语法的,如果一个模块的导出是 exports.default
导出,如果使用 import xx from 'xx'
的语法导入是会报错的。
所以在 tsconfig.json
中,有个 allowSyntheticDefaultImports
选项,就是针对这种语法做兼容。
如果设定 allowSyntheticDefaultImports
为 true
,则检测导入的模块是否是 ES6 模块,如果不是,则查找模块中是否有 exports.default
导出。
从而达到针对 exports.default
的兼容。
效果参见这个动画:
allowSyntheticDefaultImports
选项的,一般情况下我采取的方式是将 deafult 重新命名:
import { default as foo } from 'foo'
import xx = require('xx')
import xx = require('xx')
是用来导入 commonjs 模块的库,特殊的地方在于这个库的类型声明是 export = xx
这种方式导出的:
foo.js 源码:
module.exports = () => { console.log('https://tasaid.com')
}
foo.d.ts 类型声明文件源码:
declare function foo(): void;
export = foo
bar.ts 引用:
import foo = require('./foo')foo()
在 《[JavaScript 和 TypeScript 交叉口 —— 类型定义文件(*.d.ts)
](https://tasaid.com/blog/20171...》中讲述过 TypeScript 类型声明文件对导入导出的影响
const xx = require('xx')
当一个模块没有类型声明文件的时候,可以使用 commonjs 原始的 require()
方式来导入模块,这样会默认该模块为 any。
总 结
最后我们整体总结下,在 TypeScript 中,有多种 import 的方式,分别对应了 JavaScript 中不同的 export。
import * as xx from 'xx' // commonjs 模块
import xx from 'xx' // 标准 es6 模块
import xx = require('xx') // commonjs 模块,类型声明为 export = xx
const xx = require('xx') // 没有类型声明,默认导入 any 类型
针对 babel
编译出来的 exports.default
语法,ts 提供了 allowSyntheticDefaultImports
选项可以支持,只不过个人不太推荐。
个人建议将 default
重命名。
import { default as foo } from 'foo'
关于 TypeScript 中类型声明文件(*.d.ts) 对 import 和 export 的影响,参考 《[JavaScript 和 TypeScript 交叉口 --- 类型定义文件](https://tasaid.com/blog/20171...》。