写点什么

TS 导入导出那些事

作者:这我可不懂
  • 2023-07-21
    福建
  • 本文字数:3451 字

    阅读完需:约 11 分钟

一、前言

最近用 TypeScript 写 npm 包,各种模块、命名空间、全局定义等等扰得我睡不着觉。我便苦心研究,总结了几个比较冷门的,国内貌似基本上找不到资料的导入导出用法,顺便在其中又插入一些不那么冷门的用法,于是本篇文章来了。

二、导入模块

导入模块主要分为 2 种:

导入 ESM 模块

导入平常的 ES Module 中的东西相信大家都不陌生。唯一需要注意的便是默认导出与“星号”式导入的区别。

“星号”式导入:

import * as Mod from './mod';
复制代码


// 类似于const Mod = require('./mod');
复制代码

导入模块的默认导出:

import ModDef from './mod';
复制代码


// 类似于const { default: ModDef } = require('./mod');
复制代码

命名的方式导入模块:

import ModDef, { a, b } from './mod';
复制代码


// 类似于const {	default: ModDef,	a, b} = require('./mod');
复制代码

导入 CJS 模块

导入 CommonJS 模块和 ES Module 稍有不同。主要是因为面向 CJS 的模块会有导出分配。

如果最终模块是个对象,则可以使用同 ESM 一样的方式导入。

import * as Mod from './mod';
复制代码

如果模块是个函数或者基本数据类型之类的,则要用以下这种方式

import Mod = require('./mod');
复制代码

实际上具体使用哪种还是有点混乱的,主旨思想是只要引入不报错就行。

三、在模块中导出

在模块中导出东西相信大家也不陌生,不过这里还是详细讲解一下。

在模块中导出东西有很多种方法。导出总共可分为 4 类:

命名导出

命名导出有两种方法,一种是声明着导出

export namespace A { }export function b() { }export class C { }export const d = 123;export let { e } = { e: 'hh' };
复制代码

一种是声明后导出

namespace A { }function b() { }class C { }const d = 123;let { e } = { e: 'hh' };
export { A, b, C, d, e };
复制代码

声明后导出比声明着导出更灵活,能合并,也能重命名

namespace A { }export { A };
function b() { }export { b as c };
class C { }export { C as B };
const d = 123;let { e } = { e: 'hh' };export { d, e };
复制代码

命名导出编译成 Common JS 后类似这样

exports.xxx = xxx;
复制代码

需要注意的是其他人无法修改任何你导出的东西。即使是使用 let 声明也一样

/* mod.ts */
export let a = 123;

复制代码


/* others.ts */
import { a } from './mod';a = 321; // 报错:ts(2632)

复制代码

不过对于上面的代码,你可以随便修改所导出的 a 。因为其他人每次读取 a 时都会重新从你的导出对象上访问一次 a 属性,不用担心其他人无法接收到你的修改。具体可以查看编译后的 JS 文件

import { a } from './mod';const b = a + 123;console.log(a);
复制代码


// 编译后var mod_1 = require("./mod");var b = mod_1.a + 123;console.log(mod_1.a);
复制代码

默认导出

默认导出可以理解为一种特殊的命名导出。

默认导出的名字是 default 。但是你不能搞个名字叫 default 的变量然后导出,你必须得用 export default 或者在导出时重命名

export let default = 123; // 报错:ts(1389)
export default 123; // 正确
export let a = 123;export { a as default }; // 正确
复制代码

星号导入搭配默认导出,可以达到默认导出即为星号导出的效果

/* mod.ts */
import * as Self from './mod';export default Self;// 或者export * as default from './mod';

复制代码


/* others.ts */
import * as Mod from './mod';import ModDef from './mod';console.log(Mod === ModDef); // true

复制代码

导出分配

导出分配就是把 Common JS 的导出搬到了 TS 中。写法也差不多

export = 'hh';// 相当于module.export = 'hh';
复制代码

导出分配也可以指定默认导出,只需要有 default 属性就可以

/* mod.ts */
export = { default: 123 };

复制代码


/* others.ts */
import mod from './mod';console.log(mod); // 123

复制代码

需要注意的是采用了导出分配后便不能再使用其他导出方法。

导出其他模块

导出其他模块一般分为两种。

第一种是星号导出,把其他模块的东西全导出到自己里头,就像

export * from './xxx'
复制代码

具体效果是 xxx 中导出的东西,也可以通过你访问到。

/* xxx.ts */
export let a = { hh: 'hh' };

复制代码


/* mod.ts */
export * from './xxx.ts';

复制代码


/* others.ts */
import { a } from './xxx';import { a } from './mod';console.log(a === a); // true

复制代码

第二种是挂到自己模块下面,既可以是这样

export * as xxx from './xxx';
复制代码


// 编译后exports.xxx = require('./xxx');
复制代码

也可以是这样

export import xxx = require('./xxx');
复制代码


// 编译后var xxx = require('./xxx');exports.xxx = xxx;
复制代码

二者主要的区别在于前者仅仅是导出了模块,其他什么都没干;而后者实际上还引入了模块,为模块注册了标识符,让你在导出的同时还可以通过 xxx 访问到 ./xxx 模块。

如果只想把另一个模块的一部分东西导出出去,还有这种方法

export {  alpha,  default as beta,} from './xxx';
复制代码

四、导入命名空间

虽然现在命名空间相较于模块并不是特别常用,但它还是有比较完整的导入导出功能的。

导入命名空间中的东西很简单,像这样

import xxx = Space.xxx;
复制代码

不论你是在模块中导入全局命名空间,还是在命名空间中导入其他命名空间,都是适用的。

import Err = globalThis.Error;throw Err('hh');
namespace A { import Process = NodeJS.Process; let proce: Process;}
复制代码

较为可惜的是命名空间貌似没有星号导入,也不支持解构导入。

五、在命名空间中导出

在一般 TS 中,命名空间只有两种方法导出。

第一种方法是声明着导出,类似于模块

namespace A {	export const a = 123;}
复制代码

第二种方法是导入着导出,可以用来导出其他命名空间的东西

namespace A {	export import Err = globalThis.Error;}
复制代码

而对于不一般的 TS ——也就是类型声明中,命名空间还可以采用像模块一样的导出对象

declare namespace A {	const a = 123;	const b = 'hh';	export { a, b };}
复制代码

六、使用全局定义

全局定义一般有三种:

  1. 内置在 TS 中的全局定义。比如 setTimeout 、 Error 等。对于这种全局定义,直接拿来用就可以了。

  2. 位于环境模块中的全局定义。比如 NodeJS.Process 等。

    包括位于 node_modules/@types 文件夹中的自动引入的环境模块,都可以通过三斜杠注释来引入。

    你可以通过 path 直接指定文件路径

    /// <reference path="./types.d.ts" />

  3. 位于模块中的全局定义。

    这种全局定义只需要引入一下模块,表示你已经运行此模块,即可

    import '@babel/core';

    或者你也可以通过三斜杠注释,通过 types 指定模块

    /// <reference types="@babel/core" />

    需要注意的是,不论你采用 import 还是三斜杠注释,甚至只是在类型中使用了一个 typeof import('xxx') ,只要你在一个 TS 文件中引入了这个模块所定义的全局类型,那这个类型就会永远存在下去,污染你的 globalThis 。唯一在不污染全局域的情况下运行模块的方法是使用 import() 函数动态引入,但这样子你也拿不到你需要的类型。

七、进行全局定义

进行全局定义一般有三种方法。

第一种是直接写环境模块。不带任何 import 和 export 一般就会让编译器把这当成一个环境模块。所以,如果你需要防止一个 TS 文件变成环境模块导致类型泄露的话,你可以加一个安全无公害的 export { }; 。

第二种是在模块中定义,只需要把类型定义写到 declare global 里头就行

declare global {	const a = 123;	let b: {};}
a; // 123b; // {}
复制代码

第三种是通过合并 globalThis 命名空间来定义,好处是可以使用命名空间的“导入着导出”方法,将模块或者其他命名空间的局部变量变成全局变量

import _Mod from './mod';
declare global { namespace globalThis { const a = 123; export import Mod = _Mod; }}
a; // 123Mod; // typeof import('./mod')
复制代码


相关内容拓展:(技术前沿)


近 10 年间,甚至连传统企业都开始大面积数字化时,我们发现开发内部工具的过程中,大量的页面、场景、组件等在不断重复,这种重复造轮子的工作,浪费工程师的大量时间。


针对这类问题,低代码把某些重复出现的场景、流程,具象化成一个个组件、api、数据库接口,避免了重复造轮子。极大的提高了程序员的生产效率。


推荐一款程序员都应该知道的软件 JNPF 快速开发平台,采用业内领先的 SpringBoot 微服务架构、支持 SpringCloud 模式,完善了平台的扩增基础,满足了系统快速开发、灵活拓展、无缝集成和高性能应用等综合能力;采用前后端分离模式,前端和后端的开发人员可分工合作负责不同板块,省事又便捷。


免费体验官网:https://www.jnpfsoft.com/?infoq

 

还没有了解低代码这项技术可以赶紧体验学习!

发布于: 刚刚阅读数: 3
用户头像

低代码技术追随者,为全民开发而努力 2023-02-15 加入

大家好,我是老王,专注于分享低代码图文知识,感兴趣的伙伴就请关注我吧!

评论

发布
暂无评论
TS 导入导出那些事_typescript_这我可不懂_InfoQ写作社区