Vue 中的 defineComponent


defineComponent 本身的功能很簡單,但是最主要的功能是為了 ts 下的類型推到。對於一個 ts 文件,如果我們直接寫
export default {}
復制代碼
這個時候,對於編輯器而言,{} 只是一個 Object 的類型,無法有針對性的提示我們對於 vue 組件來說 {} 里應該有哪些屬性。但是增加一層 defineComponet 的話,
export default defineComponent({})
復制代碼
這時,{} 就變成了 defineComponent 的參數,那么對參數類型的提示,就可以實現對 {} 中屬性的提示,外還可以進行對參數的一些類型推導等操作。

但是上面的例子,如果你在 vscode 的用 .vue 文件中嘗試的話,會發現不寫 defineComponent 也一樣有提示。這個其實是 Vetur 插件進行了處理。

下面看 defineComponent 的實現,有4個重載,先看最簡單的第一個,這里先不關心 DefineComponent 是什么,后面細看。
// overload 1: direct setup function
// (uses user defined props interface)
export function defineComponent<Props, RawBindings = object>(
setup: (
props: Readonly ,
ctx: SetupContext
) => RawBindings | RenderFunction
): DefineComponent<Props, RawBindings>
復制代碼
defineComponet 參數為 function, function 有兩個參數 props 和 ctx,返回值類型為 RawBindings 或者 RenderFunction。defineComponet 的返回值類型為 DefineComponent<Props, RawBindings>。這其中有兩個泛型 Props 和 RawBindings。Props 會根據我們實際寫的時候給 setup 第一個參數傳入的類型而確定,RawBindings 則根據我們 setup 返回值來確定。一大段話比較繞,寫一個類似的簡單的例子來看:

類似 props 用法的簡易 demo 如下,我們給 a 傳入不同類型的參數,define 返回值的類型也不同。這種叫 Generic Functions
declare function define (a: Props): Props

const arg1:string = '123'
const result1 = define(arg1) // result1:string

const arg2:number = 1
const result2 = define(arg2) // result2: number
復制代碼

類似 RawBindings 的簡易 demo如下: setup 返回值類型不同,define 返回值的類型也不同
declare function define (setup: ()=>T): T

const arg1:string = '123'
const resul1 = define(() => {
return arg1
})

const arg2:number = 1
const result2 = define(() => {
return arg2
})
復制代碼

由上面兩個簡易的 demo,可以理解重載1的意思,defineComponet 返回類型為DefineComponent<Props, RawBindings>,其中 Props 為 setup 第一個參數類型;RawBindings 為 setup 返回值類型,如果我們返回值為函數的時候,取默認值 object。從中可以掌握一個 ts 推導的基本用法,對於下面的定義
declare function define (a: T): T
復制代碼
可以根據運行時傳入的參數,來動態決定 T 的類型 這種方式也是運行時類型和 typescript 靜態類型的唯一聯系,很多我們想通過運行時傳入參數類型,來決定其他相關類型的時候,就可以使用這種方式。
接着看 definComponent,它的重載2,3,4分別是為了處理 options 中 props 的不同類型。看最常見的 object 類型的 props 的聲明
export function defineComponent<
// the Readonly constraint allows TS to treat the type of { required: true }
// as constant instead of boolean.
PropsOptions extends Readonly ,
RawBindings,
D,
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = Record<string, any>,
EE extends string = string

(
options: ComponentOptionsWithObjectProps<
PropsOptions,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
EE

): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>
復制代碼
和上面重載1差不多的思想,核心思想也是根據運行時寫的 options 中的內容推導出各種泛型。在 vue3 中 setup 的第一個參數是 props,這個 props 的類型需要和我們在 options 傳入的一致。這個就是在ComponentOptionsWithObjectProps中實現的。代碼如下
export type ComponentOptionsWithObjectProps<
PropsOptions = ComponentObjectPropsOptions,
RawBindings = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
Props = Readonly<ExtractPropTypes >,
Defaults = ExtractDefaultPropTypes

= ComponentOptionsBase<
Props,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
EE,
Defaults
& {
props: PropsOptions & ThisType
} & ThisType<
CreateComponentPublicInstance<
Props,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
Props,
Defaults,
false

export interface ComponentOptionsBase<
Props,
RawBindings,
D,
C extends ComputedOptions,
M extends MethodOptions,
Mixin extends ComponentOptionsMixin,
Extends extends ComponentOptionsMixin,
E extends EmitsOptions,
EE extends string = string,
Defaults = {}

extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
ComponentInternalOptions,
ComponentCustomOptions {
setup?: (
this: void,
props: Props,
ctx: SetupContext<E, Props>
) => Promise | RawBindings | RenderFunction | void
//...
}
復制代碼
很長一段,同樣的先用一個簡化版的 demo 來理解一下:
type TypeA<T1, T2, T3> = {
a: T1,
b: T2,
c: T3
}
declare function define<T1, T2, T3>(options: TypeA<T1, T2, T3>): T1
const result = define({
a: '1',
b: 1,
c: {}
}) // result: string
復制代碼
根據傳入的 options 參數 ts 會推斷出 T1,T2,T3的類型。得到 T1, T2, T3 之后,可以利用他們進行其他的推斷。稍微改動一下上面的 demo,假設 c 是一個函數,里面的參數類型由 a 的類型來決定:
type TypeA<T1, T2, T3> = TypeB<T1, T2>
type TypeB<T1, T2> = {
a: T1
b: T2,
c: (arg:T1)=>{}
}
const result = define({
a: '1',
b: 1,
c: (arg) => { // arg 這里就被會推導為一個 string 的類型
return arg
}
})
復制代碼
然后來看 vue 中的代碼,首先 defineComponent 可以推導出 PropsOptions。但是 props 如果是對象類型的話,寫法如下
props: {
name: {
type: String,
//... 其他的屬性
}
}
復制代碼
而 setup 中的 props 參數,則需要從中提取出 type 這個類型。所以在 ComponentOptionsWithObjectProps 中
export type ComponentOptionsWithObjectProps<
PropsOptions = ComponentObjectPropsOptions,
//...
Props = Readonly<ExtractPropTypes >,
//...

復制代碼
通過 ExtracPropTypes 對 PropsOptions 進行轉化,然后得到 Props,再傳入 ComponentOptionsBase,在這個里面,作為 setup 參數的類型
export interface ComponentOptionsBase<
Props,
//...

extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
ComponentInternalOptions,
ComponentCustomOptions {
setup?: (
this: void,
props: Props,
ctx: SetupContext<E, Props>
) => Promise | RawBindings | RenderFunction | void
復制代碼
這樣就實現了對 props 的推導。

this 的作用
在 setup 定義中第一個是 this:void 。我們在 setup 函數中寫邏輯的時候,會發現如果使用了 this.xxx IDE 中會有錯誤提示

Property 'xxx' does not exist on type 'void'

這里通過設置 this:void來避免我們在 setup 中使用 this。
this 在 js 中是一個比較特殊的存在,它是根據運行上上下文決定的,所以 typescript 中有時候無法准確的推導出我們代碼中使用的 this 是什么類型的,所以 this 就變成了 any,各種類型提示/推導啥的,也都無法使用了(注意:只有開啟了 noImplicitThis 配置, ts 才會對 this 的類型進行推導)。為了解決這個問題,typescript 中 function 的可以明確的寫一個 this 參數,例如官網的例子:
interface Card {
suit: string;
card: number;
}

interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}

let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// NOTE: The function now explicitly specifies that its callee must be of type Deck
createCardPicker: function (this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);

  return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
};

},
};

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);
復制代碼
明確的定義出在 createCardPicker 中的 this 是 Deck 的類型。這樣在 createCardPicker 中 this 下可使用的屬性/方法,就被限定為 Deck 中的。
另外和 this 有關的,還有一個 ThisType。

ExtractPropTypes 和 ExtractDefaultPropTypes
上面提到了,我們寫的 props
{
props: {
name1: {
type: String,
require: true
},
name2: {
type: Number
}
}
}
復制代碼
經過 defineComponent 的推導之后,會被轉換為 ts 的類型
ReadOnly<{
name1: string,
name2?: number | undefined
}>
復制代碼
這個過程就是利用 ExtractPropTypes 實現的。
export type ExtractPropTypes = O extends object
? { [K in RequiredKeys ]: InferPropType<O[K]> } &
{ [K in OptionalKeys ]?: InferPropType<O[K]> }
: { [K in string]: any }
復制代碼
根據類型中清晰的命名,很好理解:利用 RrequiredKeys 和 OptionsKeys 將 O 按照是否有 required 進行拆分(以前面props為例子)
{
name1
} & {
name2?
}
復制代碼
然后每一組里,用 InferPropType<O[K]> 推導出類型。

InferPropType
在理解這個之前,先理解一些簡單的推導。首先我們在代碼中寫
props = {
type: String
}
復制代碼
的話,經過 ts 的推導,props.type 的類型是 StringConstructor。所以第一步需要從 StringConstructor/ NumberConstructor 等 xxConstrucror 中得到對應的類型 string/number 等。可以通過 infer 來實現
type a = StringConstructor
type ConstructorToType = T extends { (): infer V } ? V : never
type c = ConstructorToType // type c = String
復制代碼
上面我們通過 ():infer V 來獲取到類型。之所以可以這樣用,和 String/Number 等類型的實現有關。javascript 中可以寫
const key = String('a')
復制代碼
此時,key 是一個 string 的類型。還可以看一下 StringConstructor 接口類型表示
interface StringConstructor {
new(value?: any): String;
(value?: any): string;
readonly prototype: String;
fromCharCode(...codes: number[]): string;
}
復制代碼
上面有一個 ():string ,所以通過 extends {(): infer V} 推斷出來的 V 就是 string。
然后再進一步,將上面的 a 修改成 propsOptions 中的內容,然后把 ConstructorToType 中的 infer V 提到外面一層來判斷
type a = StringConstructor
type ConstructorType = { (): T }
type b = a extends {
type: ConstructorType
required?: boolean
} ? V : never // type b = String
復制代碼
這樣就簡單實現了將 props 中的內容轉化為 type 中的類型。
因為 props 的 type 支持很多中寫法,vue3 中實際的代碼實現要比較復雜
type InferPropType = T extends null
? any // null & true would fail to infer
: T extends { type: null | true }
? any
// As TS issue
https://github.com/Microsoft/TypeScript/issues/14829 // somehow ObjectConstructor when inferred from { (): T } becomes any // BooleanConstructor when inferred from PropConstructor(with PropMethod) becomes Boolean
// 這里單獨判斷了 ObjectConstructor 和 BooleanConstructor
: T extends ObjectConstructor | { type: ObjectConstructor }
? Record<string, any>
: T extends BooleanConstructor | { type: BooleanConstructor }
? boolean
: T extends Prop<infer V, infer D> ? (unknown extends V ? D : V) : T

// 支持 PropOptions 和 PropType 兩種形式
type Prop<T, D = T> = PropOptions<T, D> | PropType
interface PropOptions<T = any, D = T> {
type?: PropType | true | null
required?: boolean
default?: D | DefaultFactory | null | undefined | object
validator?(value: unknown): boolean
}

export type PropType = PropConstructor | PropConstructor []

type PropConstructor<T = any> =
| { new (...args: any[]): T & object } // 可以匹配到其他的 Constructor
| { (): T } // 可以匹配到 StringConstructor/NumberConstructor 和 () => string 等
| PropMethod // 匹配到 type: (a: number, b: string) => string 等 Function 的形式

// 對於 Function 的形式,通過 PropMethod 構造成了一個和 stringConstructor 類型的類型
// PropMethod 作為 PropType 類型之一
// 我們寫 type: Function as PropType<(a: string) => {b: string}> 的時候,就會被轉化為
// type: (new (...args: any[]) => ((a: number, b: string) => {
// a: boolean;
// }) & object) | (() => (a: number, b: string) => {
// a: boolean;
// }) | {
// (): (a: number, b: string) => {
// a: boolean;
// };
// new (): any;
// readonly prototype: any;
// }
// 然后在 InferPropType 中就可以推斷出 (a:number,b:string)=> {a: boolean}
type PropMethod<T, TConstructor = any> =
T extends (...args: any) => any // if is function with args
? {
new (): TConstructor;
(): T;
readonly prototype: TConstructor
} // Create Function like constructor
: never
復制代碼

RequiredKeys
這個用來從 props 中分離出一定會有值的 key,源碼如下
type RequiredKeys = {
[K in keyof T]: T[K] extends
| { required: true }
| { default: any }
// don't mark Boolean props as undefined
| BooleanConstructor
| { type: BooleanConstructor }
? K
: never
}[keyof T]
復制代碼
除了明確定義 reqruied 以外,還包含有 default 值,或者 boolean 類型。因為對於 boolean 來說如果我們不傳入,就默認為 false;而有 default 值的 prop,一定不會是 undefined

OptionalKeys
有了 RequiredKeys, OptionsKeys 就很簡單了:排除了 RequiredKeys 即可
type OptionalKeys = Exclude<keyof T, RequiredKeys >
復制代碼

ExtractDefaultPropTypes 和 ExtractPropTypes 類似,就不寫了。
推導 options 中的 method,computed, data 返回值, 都和上面推導 props 類似。
emits options
vue3 的 options 中新增加了一個 emits 配置,可以顯示的配置我們在組件中要派發的事件。配置在 emits 中的事件,在我們寫 $emit 的時候,會作為函數的第一個參數進行提示。
對獲取 emits 中配置值的方式和上面獲取 props 中的類型是類似的。$emit的提示,則是通過 ThisType 來實現的(關於 ThisType 參考另外一篇文章介紹)。下面是簡化的 demo
declare function define (props:{
emits: T,
method?: {[key: string]: (...arg: any) => any}
} & ThisType<{
$emits: (arg: T) => void
}>):T

const result = define({
emits: {
key: '123'
},
method: {
fn() {
this.$emits(/這里會提示:arg: {
key: string;
}
/)
}
}
})
復制代碼
上面會推導出 T 為 emits 中的類型。然后 & ThisType ,使得在 method 中就可以使用 this.$emit。再將 T 作為 $emit 的參數類型,就可以在寫 this.$emit的時候進行提示了。
然后看 vue3 中的實現
export function defineComponent<
//... 省卻其他的
E extends EmitsOptions = Record<string, any>,
//...

(
options: ComponentOptionsWithObjectProps<
//...
E,
//...

): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>

export type ComponentOptionsWithObjectProps<
//..
E extends EmitsOptions = EmitsOptions,
//...

= ComponentOptionsBase< // 定義一個 E 的泛型
//...
E,
//...
& {
props: PropsOptions & ThisType
} & ThisType<
CreateComponentPublicInstance< // 利用 ThisType 實現 $emit 中的提示
//...
E,
//...

// ComponentOptionsWithObjectProps 中 包含了 ComponentOptionsBase
export interface ComponentOptionsBase<
//...
E extends EmitsOptions, // type EmitsOptions = Record<string, ((...args: any[]) => any) | null> | string[]
EE extends string = string,
Defaults = {}

extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
ComponentInternalOptions,
ComponentCustomOptions {
//..
emits?: (E | EE[]) & ThisType // 推斷出 E 的類型
}

export type ComponentPublicInstance<
//...
E extends EmitsOptions = {},
//...

= {
//...
$emit: EmitFn // EmitFn 來提取出 E 中的 key
//...
}
復制代碼
在一邊學習一邊實踐的時候踩到一個坑。踩坑過程:將 emits 的推導過程實現了一下
export type ObjectEmitsOptions = Record<
string,
((...args: any[]) => any) | null

export type EmitsOptions = ObjectEmitsOptions | string[];

declare function define<E extends EmitsOptions = Record<string, any>, EE extends string = string>(options: E| EE[]): (E | EE[]) & ThisType
復制代碼
然后用下面的方式來驗證結果
const emit = ['key1', 'key2']
const a = define(emit)
復制代碼
看 ts 提示的時候發現,a 的類型是 const b: string[] & ThisType ,但是實際中 vue3 中寫同樣數組的話,提示是 const a: (("key1" | "key2")[] & ThisType ) | (("key1" | "key2")[] & ThisType )
糾結好久,最終發現寫法的不同:用下面寫法的話推導出來結果一致
define(['key1', 'key2'])
復制代碼
但是用之前的寫法,通過變量傳入的時候,ts 在拿到 emit 時候,就已經將其類型推導成了 string[],所以 define 函數中拿到的類型就變成了 string[],而不是原始的 ['key1', 'key2']
因此需要注意:在 vue3 中定義 emits 的時候,建議直接寫在 emits 中寫,不要提取為單獨的變量再傳給 emits
真的要放在單獨變量里的話,需要進行處理,使得 '[key1', 'key2'] 的變量定義返回類型為 ['key1', 'key2'] 而非 string[]。可以使用下面兩種方式:

方式一
const keys = ["key1", "key2"] as const; // const keys: readonly ["key1", "key2"]
復制代碼
這種方式寫起來比較簡單。但是有一個弊端,keys 為轉為 readonly 了,后期無法對 keys 進行修改。
參考文章2 ways to create a Union from an Array in Typescript

方式二
type UnionToIntersection = (T extends any ? (v: T) => void : never) extends (v: infer V) => void ? V : never
type LastOf = UnionToIntersection<T extends any ? () => T : never> extends () => infer R ? R : never
type Push<T extends any[], V> = [ ...T, V]

type UnionToTuple<T, L = LastOf , N = [T] extends [never] ? true : false> = N extends true ? [] : Push<UnionToTuple<Exclude<T, L>>, L>

declare function tuple (arr: T[]): UnionToTuple

const c = tuple(['key1', 'key2']) // const c: ["key1", "key2"]
復制代碼
首先通過 arr: T[] 將 ['key1', 'key2'] 轉為 union,然后通過遞歸的方式, LastOf 獲取 union 中的最后一個,Push到數組中。

mixins 和 extends
vue3 中寫在 mixins 或 extends 中的內容可以在 this 中進行提示。對於 mixins 和 extends 來說,與上面其他類型的推斷有一個很大的區別:遞歸。所以在進行類型判斷的時候,也需要進行遞歸處理。舉個簡單的例子,如下
const AComp = {
methods: {
someA(){}
}
}
const BComp = {
mixins: [AComp],
methods: {
someB() {}
}
}
const CComp = {
mixins: [BComp],
methods: {
someC() {}
}
}
復制代碼
對於 CComp 中的 this 的提示,應該有方法 someB 和 someA。為了實現這個提示,在進行類型推斷的時候,需要一個類似下面的 ThisType
ThisType<{
someA
} & {
someB
} & {
someC
}>
復制代碼
所以對於 mixins 的處理,就需要遞歸獲取 component 中的 mixins 中的內容,然后將嵌套的類型轉化為扁平化的,通過 & 來鏈接。看源碼中實現:
// 判斷 T 中是否有 mixin
// 如果 T 含有 mixin 那么這里結果為 false,以為 {mixin: any} {mixin?: any} 是無法互相 extends 的
type IsDefaultMixinComponent = T extends ComponentOptionsMixin
? ComponentOptionsMixin extends T ? true : false
: false

//
type IntersectionMixin = IsDefaultMixinComponent extends true
? OptionTypesType<{}, {}, {}, {}, {}> // T 不包含 mixin,那么遞歸結束,返回 {}
: UnionToIntersection<ExtractMixin > // 獲取 T 中 Mixin 的內容進行遞歸

// ExtractMixin(map type) is used to resolve circularly references
type ExtractMixin = {
Mixin: MixinToOptionTypes
}[T extends ComponentOptionsMixin ? 'Mixin' : never]

// 通過 infer 獲取到 T 中 Mixin, 然后遞歸調用 IntersectionMixin
type MixinToOptionTypes = T extends ComponentOptionsBase<
infer P,
infer B,
infer D,
infer C,
infer M,
infer Mixin,
infer Extends,
any,
any,
infer Defaults

? OptionTypesType<P & {}, B & {}, D & {}, C & {}, M & {}, Defaults & {}> &
IntersectionMixin &
IntersectionMixin
: never
復制代碼
extends 和 mixin 的過程相同。然后看 ThisType 中的處理
ThisType<
CreateComponentPublicInstance<
Props,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
Props,
Defaults,
false
>

export type CreateComponentPublicInstance<
P = {},
B = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = {},
PublicProps = P,
Defaults = {},
MakeDefaultsOptional extends boolean = false,
// 將嵌套的結構轉為扁平化的
PublicMixin = IntersectionMixin & IntersectionMixin ,
// 提取 props
PublicP = UnwrapMixinsType<PublicMixin, 'P'> & EnsureNonVoid

,
// 提取 RawBindings,也就是 setup 返回的內容
PublicB = UnwrapMixinsType<PublicMixin, 'B'> & EnsureNonVoid,
// 提取 data 返回的內容
PublicD = UnwrapMixinsType<PublicMixin, 'D'> & EnsureNonVoid ,
PublicC extends ComputedOptions = UnwrapMixinsType<PublicMixin, 'C'> &
EnsureNonVoid ,
PublicM extends MethodOptions = UnwrapMixinsType<PublicMixin, 'M'> &
EnsureNonVoid ,
PublicDefaults = UnwrapMixinsType<PublicMixin, 'Defaults'> &
EnsureNonVoid

= ComponentPublicInstance< // 上面結果傳給 ComponentPublicInstance,生成 this context 中的內容
PublicP,
PublicB,
PublicD,
PublicC,
PublicM,
E,
PublicProps,
PublicDefaults,
MakeDefaultsOptional,
ComponentOptionsBase<P, B, D, C, M, Mixin, Extends, E, string, Defaults>

復制代碼
以上就是整體大部分的 defineComponent 的實現,可以看出,他純粹是為了類型推導而生的,同時,這里邊用到了很多很多類型推導的技巧,還有一些這里沒有涉及,感興趣的同學可以去仔細看下 Vue 中的實現。.markdown-body pre,.markdown-body pre>code.hljs{color:#333;background:#f8f8f8}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:700}.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:teal}.hljs-doctag,.hljs-string{color:#d14}.hljs-section,.hljs-selector-id,.hljs-title{color:#900;font-weight:700}.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type{color:#458;font-weight:700}.hljs-attribute,.hljs-name,.hljs-tag{color:navy;font-weight:400}.hljs-link,.hljs-regexp{color:#009926}.hljs-bullet,.hljs-symbol{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999;font-weight:700}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700} 分類: 前端 標簽: Vue.jsTypeScript源碼前端


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM