2023-06-20
TypeScript
00
请注意,本文编写于 538 天前,最后修改于 279 天前,其中某些信息可能已经过时。

目录

面试题
模块
导入导出
外部模块
模块声明通配符
UMD (AMD + CommonJS + Global变量)
模块的扩展
扩展全局对象及其属性和方法
命名空间
解决同名变量冲突问题
命名空间分离多个文件
外部命名空间
别名
命名空间的声明合并
命名空间和类的合并
命名空间合并函数
命名空间合并枚举
模块与命名空间
模块解析
Classic
Node
路径映射
复杂映射
虚拟目录映射
虚拟目录映射案例-国际化
跟踪模块解析
--noResolve
为什么在exclude列表里的模块还会被编译器使用
项目实战案例
TS项目引用JS文件
定义ajax如入参和出参
Vue3项目vuex如何使用TS
工程化应用
编译TS代码用babel还是tsc?
参考资料

本文会围绕TS工程化应用讲述TS相关知识点,包括但不限于以下内容 模块、命名空间、TS配置文件、TS与前端工程化。 作为TS文章,只讲TS知识点,小编默认你已经掌握了JS模块化(esm、umd、commonjs、amd、ohters)和了解了前端工程化(webpack/babel/eslint)相关知识。

面试题

  • 如何扩展Window对象
  • 如何给一类库编写声明文件,UMD类库如何写声明文件
  • 如何扩展一个类库、扩展一个全局变量或全局变量增加属性?
  • 编译TS代码用babel还是tsc?两者有什么区别?
  • 命名空间可以声明合并吗?

带着问题开始本篇文章的阅读吧

模块

导入导出

  • 语法同ESM的导入导出 export import
  • 一般我们用TS写一个简单的类库,tsc命令可以为自动生成声明文件.d.ts
    • npx tsc --declaration -p ./ -t esnext --emitDeclarationOnly --outDir types
    • --declaration // 生成声明文件
    • -p ./ 选定了项目根目录,按照包的查找顺序
    • -t esnext选择esnext即不对es6+语法打补丁,至于原因我后面讲
    • --emitDeclarationOnly 仅生成声明文件
    • --outDir types 导出目录
  • 对CJS与AMD的支持
    • TypeScript模块支持export =语法以支持传统的CommonJSAMD的工作流模型
    • 使用时必须使用TypeScript提供的特定语法 import module = require("module")

外部模块

  • 外部模块,有很多库是用JS书写的,我们在TS项目中,如果想引入一个JS库(也没有第三方声明文件),需要额外写一份 .d.ts文件
ts
// 文件名 path.d.ts // 这里我是故意将 模块名与文件名 写成不一致,引入是模块名引入 declare module "CustomPath" { export function normalize(p: string): string; export function join(...paths: any[]): string; export let sep: string; } // 如何使用 /// <reference path="../types/path.d.ts"/> import {normalize} from 'CustomPath' // 或使用 import {normalize} = require("CustomPath"); // 有些时候我们想偷懒,让我用就行了,不需要代码提示 declare module "hot-new-module"; // 简写模块里所有导出的类型将是any。

模块声明通配符

  • 某些模块加载器如SystemJS 和 AMD支持导入非JavaScript内容。 它们通常会使用一个前缀或后缀来表示特殊的加载语法。 模块声明通配符可以用来表示这些情况。
ts
declare module "*!text" { const content: string; export default content; } // Some do it the other way around. declare module "json!*" { const value: any; export default value; } // 现在你可以就导入匹配"*!text"或"json!*"的内容了。 import fileContent from "./xyz.txt!text"; import data from "json!http://example.com/data.json"; console.log(data, fileContent);

UMD (AMD + CommonJS + Global变量)

  • 按照TS中文网给你案例不通, 经过多伦询问chatGPT得到可能方案如下
ts
// 定义 declare namespace MyLibrary { // 声明模块的结构 export function myFunction(): void; export const myVariable: string; } // 使用 // 方式一 import { myFunction, myVariable } from 'my-library'; myFunction(); // 方式二 MyLibrary.myFunction()

模块的扩展

  • 我们给一些类库扩展一些功能的同时如何编写声明文件?
ts
// Calculator.ts export default class Calculator { // ... } // ProgrammerCalculator.ts import Calculator from "./Calculator"; export default class ProgrammerCalculator extends Calculator { say(){} } // 使用 import Calculator from './ProgrammerCalculator'; // 这也是一种设计模式,叫做适配器模式

扩展全局对象及其属性和方法

ts
// 利用interface可重复声明扩展window,该文件需要配置到tsconfig.json中的includes字段 declare interface Window { add(a:number, b:number): number; } window.add(1, 2) // ok // 声明全局变量 declare const wx: any; // 全局对象的方法扩展 // 方案一: 该方式会丢失之前的扩展 export class Observable<T> { // ... still no implementation ... } declare global { interface Array<T> { toObservable(): Observable<T>; } } // 方案二 // 使用 [].toObservable() interface Array<T> { toObservable(): void; }

命名空间

随着项目规模的扩大,为了避免同名变量或函数命名冲突,引入了命名空间的概念

解决同名变量冲突问题

如下示例:

ts
// date.ts namespace DateUtil { export const format = (arg: Date): string => { return 'YYYY-MM-DD hh:mm:ss' } } // string.ts namespace StringUtil { export const format = (price: number): string => { return price.toFixed(2); } } // 使用 // test.ts DateUtil.format(new Date); StringUtil.format(18);

有随着项目的发展,单个命名空间文件太大, 这里可以分离为多个文件

命名空间分离多个文件

ts
// add.ts namespace MyMath { export interface add { (n1: number, n2: number): number; } } // multi.ts /// <reference path="add.ts" /> namespace MyMath { export interface multi { (n1: number, n2: number): number; } } // test.ts /// <reference path="add.ts" /> /// <reference path="multi.ts" /> const add: MyMath.add = (n1, n2) => n1 + n2 const multi: MyMath.multi = (n1, n2) => n1 * n2 console.log(add(3, 5)) console.log(multi(3, 5))

外部命名空间

外部命名空间常用来给给第三方库写类型声明文件,实现编辑器代码提示功能。

ts
declare namespace D3 { export interface Selectors { select: { // 是函数重载吗? (selector: string): Selection; (element: EventTarget): Selection; }; } export interface Event { x: number; y: number; } export interface Base extends Selectors { event: Event; } } declare var d3: D3.Base; d3.select("123"); // d3.select(123) // 报错

别名

ts
namespace Shapes { export namespace Polygons { export class Triangle { } export class Square { } } } import polygons = Shapes.Polygons; let sq = new polygons.Square(); // Same as "new Shapes.Polygons.Square()"

命名空间的声明合并

  • 与接口声明合并相似,同名命名空间会合并成员
  • 但是未导出的变量只能在当前文件使用,不能在同名的命名空间文件中使用

命名空间和类的合并

ts
// album.ts class Album { label: Album.AlbumLabel; } namespace Album { export class AlbumLabel { } } // 使用 /// <reference path="./album.ts"/> var album: Album = new Album(); album.label; // ok var albumLabel = new Album.AlbumLabel(); albumLabel; // ok

命名空间合并函数

image.png

命名空间合并枚举

ts
enum Color { red = 1, green = 2, } namespace Color { export function mixColor(colorName: string) { if (colorName == "yellow") { return Color.red + Color.green; } } }

模块与命名空间

  • 引入模块 import
  • 引入命名空间 /// <reference>

模块解析

import { a } from "moduleA";是如何查找moduleA模块的?

  • TS编译器有两种策略 ClassicNode 可以使用--moduleResolution标记来指定使用哪种模块解析策略。
  • 若未指定,那么在使用了 --module AMD | System | ES2015时的默认值为Classic,其它情况时则为Node。 假设文件路径是这样
txt
root └── src └──moduleA.ts

Classic

如果是相对路径引入

ts
// moduleA.ts import { b } from "./moduleB"

会按下面的查找顺序查找

  • /root/src/moduleB.ts
  • /root/src/moduleB.d.ts

如果是非相对路径引入

ts
// moduleA.ts import { b } from "moduleB"

会按下面的查找顺序查找

  • /root/src/moduleB.ts
  • /root/src/moduleB.d.ts
  • /root/moduleB.ts
  • /root/moduleB.d.ts
  • /moduleB.ts
  • /moduleB.d.ts

Node

  • 这个解析策略试图在运行时模仿Node.js模块解析机制。 相对导入按如下顺序查找模块
  • /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

非相对导入按如下顺序查找

  1. 会依次往上层查找node_modules文件夹,一直找到根目录
  2. 依次查找找moduleBpagckage.jsonindex文件
  3. 依次查找后缀名为 .ts.tsx.d.ts的文件 image.png

路径映射

有时为了引入方便 tsconfig.json中配置baseUrlpaths表示路径映射

比如配置

json
{ "compilerOptions": { "baseUrl": "./src", // This must be specified if "paths" is. "paths": { "jquery": ["../node_modules/jquery/dist/jquery"] // 此处映射是相对于"baseUrl" } } }

那么 import $ from "jquery" 将在 root/node_modules/jquery/dist/jquery 查找jquery

复杂映射

paths还可以指定复杂映射,包括制定多个回退位置 比如如下配置

ts
{ "compilerOptions": { "baseUrl": ".", "paths": { "*": [ "*", "generated/*" ] } } }

import 'folder1/file2'

它告诉编辑器所有匹配*(所有的值)模式的模块导入会在以下两个位置查找

  • 'projectRoot/folder1/file2.ts'
  • 'projectRoot/generated/folder1/file2.ts'

虚拟目录映射

比如,有下面的工程结构

ts
src └── views └── view1.ts (imports './template1') └── view2.ts generated └── templates └── views └── template1.ts (imports './view2')

可以使用rootDirs来告诉编译器, src/viewsgenerated/templates/views 是同一个目录

json
{ "compilerOptions": { "rootDirs": [ "src/views", "generated/templates/views" ] } }

虚拟目录映射案例-国际化

设想这样一个国际化的场景,构建工具自动插入特定的路径记号来生成针对不同区域的捆绑,比如将#{locale}做为相对模块路径./#{locale}/messages的一部分。在这个假定的设置下,工具会枚举支持的区域,将抽像的路径映射成./zh/messages./de/messages等。

利用rootDirs我们可以让编译器了解这个映射关系,从而也允许编译器能够安全地解析./#{locale}/messages,就算这个目录永远都不存在。比如,使用下面的tsconfig.json

ts
{ "compilerOptions": { "rootDirs": [ "src/zh", "src/de", "src/#{locale}" ] } }

编译器现在可以将import messages from './#{locale}/messages'解析为import messages from './zh/messages'用做工具支持的目的,并允许在开发时不必了解区域信息。

跟踪模块解析

tsc --traceResolution

image.png

--noResolve

--noResolve编译选项告诉编译器不要添加任何不是在命令行上传入的文件到编译列表。 编译器仍然会尝试解析模块,但是只要没有指定这个文件,那么它就不会被包含在内。

image.png

为什么在exclude列表里的模块还会被编译器使用

  • 一个“TS工程” 如果不指定任何 excludefiles,那么根目录下所有文件夹里的所有文件都会在编译列表里。
  • 可以使用files指定编译的文件 或 使用 exclude排除需要编译的文件。
  • 有些是被tsconfig.json自动加入的(模块解析策略)。
  • 因此,要从编译列表中排除一个文件,你需要在排除它的同时,还要排除所有对它进行import或使用了/// <reference path="..." />指令的文件。

项目实战案例

TS项目引用JS文件

ts
// /src/test.js export default function(...args) { console.log(...args) } // /src/main.js // 确保 webpack中resolve.alias中配置了 '@':'./src' import test from '@/test.js' test(1,2,3); // global.d.ts // 确定该文件已被tsconfig.json中的include字段的glob模式包含 declare module '@/test.js' { export default any; } // 等价于 // declare module '@/test.js';

定义ajax如入参和出参

ts
// 以axios为例定义ajax入参及出参数据结构 import axios, {AxiosRequestConfig} from 'axios'; interface AjaxResult<T = any> { // 后端定义的接口响应数据结构 code: number; // 错误码(如 0正常 1登录过期) data: T; // 接口正常响应后的业务数据结构 message: string; // 错误信息提示,code !== 0时, 前端直接toast } interface AjaxRequestConfig extends AxiosRequestConfig { notToast: boolean; } function createService(baseURL?: string) { const service = axios.create({ baseURL: baseURL, withCredentials: true, timeout: 8000, }); return { get<Req = any, Res = any>(url: string, params: Req, options?: AjaxRequestConfig): Promise<AjaxResult<Res>> { return service.get(url, { params: params, ...options }); } }; } // 业务层使用 interface ReqData { test: string; } interface ResData { login: string; id: number; avatar_url: string; // [propName: string]: any; } createService('https://api.github.com') .get<ReqData, ResData>('/users/vuejs', {test: 'test'}) .then(res => { console.log('vuejs作者的github头像是: ' + res.data.avatar_url) });

Vue3项目vuex如何使用TS

临时方案: 如果你还不熟悉Composition Api,工期也赶,想继续使用Options Api this.$store的写法

ts
// 1. 配置 tsconfig.json中的include字段 // 2. 新建 vue.d.ts export {}; declare module 'vue' { interface ComponentCustomProperties { $store: Store<any>, } }

官方推荐方案-API编码

ts
// 1. 编写个store src/store/index.ts import {createStore, Store} from 'vuex'; import {InjectionKey} from 'vue'; interface StoreState { // ... } export const storeKey: InjectionKey<Store<StoreState>> = symbol(); export const store = createStore<StoreState>({ state: { //.... }, // ... }) // 2. src/main.ts 安装插件 import {store, storeKey} from './store' createApp(App) .use(store, storeKey) // 这一行是新增的代码 .mount('#app') // 3. composition Api中使用 import {useStore} from 'vuex'; import {storeKey} from '/src/store'; export default defineComponent({ setup() { // 以下两行是新增的代码 const $store = useStore(storeKey); // $store.state.xxx } })

工程化应用

编译TS代码用babel还是tsc?

tsc与babel编译TS的异同

  1. Babel的劣势(@babel/preset-typescript
    • 无法做到类型检查也就是生成 .d.ts文件
      • 解决方案: 单独使用tsc生成声明文件 npx tsc --declaration -p ./ -t esnext --emitDeclarationOnly --outDir types
    • 部分语法不支持(主要是一些 TypeScript 不推荐的旧的语法,因为类型检查太重要了,所以可以忽略这一点)
  2. Babel的优势
    • 灵活性:Babel能根据目标浏览器转译指定语法;而TS只能配置指定的ecma版本
    • polyfill (es6API): Babel 能够根据目标环境自动添加 polyfillTS 编译器则不处理API需要手动引入corejsruntime
    • 插件系统:Babel有插件机制,有丰富的插件生态,TS编译器没有插件系统
    • TS只支持最新的es规范,和部分提案,想要支持新语法就要升级TS版本;Babel通过插件支持es句法 API 及 提案 babel不支持的ts语法

综上:Babel支持TS是最好的选择@babel/preset-typescript 也是TS开发团队的人实现的, WebpackRollup等 都是该方案

参考资料:

参考资料

后续有东西再补充。。。

本文作者:郭敬文

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!