本文会围绕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
导出目录TypeScript
模块支持export =
语法以支持传统的CommonJS
和AMD
的工作流模型TypeScript
提供的特定语法 import module = require("module")
.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。
tsdeclare 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);
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))
外部命名空间常用来给给第三方库写类型声明文件,实现编辑器代码提示功能。
tsdeclare 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) // 报错
tsnamespace 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
tsenum 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
模块的?
Classic
或Node
可以使用--moduleResolution
标记来指定使用哪种模块解析策略。--module AMD | System | ES2015
时的默认值为Classic
,其它情况时则为Node
。
假设文件路径是这样txtroot └── src └──moduleA.ts
如果是相对路径引入
ts// moduleA.ts
import { b } from "./moduleB"
会按下面的查找顺序查找
如果是非相对路径引入
ts// moduleA.ts
import { b } from "moduleB"
会按下面的查找顺序查找
非相对导入按如下顺序查找
node_modules
文件夹,一直找到根目录moduleB
、 pagckage.json
、 index
文件.ts
、.tsx
、.d.ts
的文件
有时为了引入方便 在tsconfig.json
中配置baseUrl
和paths
表示路径映射。
比如配置
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'
它告诉编辑器所有匹配*
(所有的值)模式的模块导入会在以下两个位置查找
比如,有下面的工程结构
tssrc
└── views
└── view1.ts (imports './template1')
└── view2.ts
generated
└── templates
└── views
└── template1.ts (imports './view2')
可以使用rootDirs
来告诉编译器, src/views
和 generated/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
--noResolve
编译选项告诉编译器不要添加任何不是在命令行上传入的文件到编译列表。 编译器仍然会尝试解析模块,但是只要没有指定这个文件,那么它就不会被包含在内。
exclude
或files
,那么根目录下所有文件夹里的所有文件都会在编译列表里。files
指定编译的文件 或 使用 exclude
排除需要编译的文件。tsconfig.json
自动加入的(模块解析策略)。import
或使用了/// <reference path="..." />
指令的文件。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';
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)
});
临时方案: 如果你还不熟悉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
}
})
tsc与babel编译TS的异同
Babel
的劣势(@babel/preset-typescript
)
.d.ts
文件
npx tsc --declaration -p ./ -t esnext --emitDeclarationOnly --outDir types
TypeScript
不推荐的旧的语法,因为类型检查太重要了,所以可以忽略这一点)Babel
的优势
Babel
能够根据目标环境自动添加 polyfill
;TS 编译器则不处理API需要手动引入corejs
或runtime
Babel
有插件机制,有丰富的插件生态,TS
编译器没有插件系统TS
只支持最新的es规范,和部分提案,想要支持新语法就要升级TS版本;Babel
通过插件支持es句法
API
及 提案 babel
不支持的ts
语法综上:Babel支持TS是最好的选择,@babel/preset-typescript
也是TS
开发团队的人实现的, Webpack
、Rollup
等 都是该方案
参考资料:
后续有东西再补充。。。
本文作者:郭敬文
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!