[TypeScript] 編程實踐之1: Google的TypeScript代碼風格1:介紹

版本1.8

2016年1月

Microsoft自2012年10月1日起根據Open Web Foundation最終規範協議版本1.0(“ OWF 1.0”)提供此規範。OWF 1.0可以從http://www.openwebfoundation.org/legal/the-owf-1-0-agreements/owfa-1-0獲得。

TypeScript是Microsoft Corporation的註冊商標。

1 介紹

諸如Web電子郵件,地圖,文檔編輯和協作工具之類的JavaScript應用程序已成爲日常計算中越來越重要的部分。我們設計TypeScript來滿足構建和維護大型JavaScript程序的JavaScript編程團隊的需求。 TypeScript幫助編程團隊定義軟件組件之間的接口,並深入瞭解現有JavaScript庫的行爲。 TypeScript還使團隊可以通過將其代碼組織到可動態加載的模塊中來減少命名衝突。 TypeScript的可選類型系統使JavaScript程序員能夠使用高效的開發工具和實踐:靜態檢查,基於符號的導航,語句完成和代碼重構。

TypeScript是JavaScript的語法糖。 TypeScript語法是ECMAScript 2015(ES2015)語法的超集。每個JavaScript程序也是TypeScript程序。 TypeScript編譯器僅對TypeScript程序執行文件本地轉換,並且不對TypeScript中聲明的變量重新排序。這將導致JavaScript輸出與TypeScript輸入非常匹配。 TypeScript不會轉換變量名,從而使直接調試生成的JavaScript變得容易。 TypeScript可以選擇提供源映射,從而啓用源級別的調試。 TypeScript工具通常在保存文件時生成JavaScript,從而保留了JavaScript開發中通常使用的測試,編輯,刷新週期。

TypeScript語法包含ECMAScript 2015的所有功能,包括類和模塊,並提供將這些功能轉換爲ECMAScript 3或5兼容代碼的功能。

類使程序員能夠以標準方式表達常見的面向對象的模式,從而使繼承等功能更易讀和可互操作。模塊使程序員可以將代碼組織到組件中,同時避免命名衝突。 TypeScript編譯器提供了模塊代碼生成選項,這些選項支持靜態或動態加載模塊內容。

TypeScript還爲JavaScript程序員提供了可選類型註釋的系統。這些類型註釋類似於在Closure系統中找到的JSDoc註釋,但是在TypeScript中,它們直接集成到語言語法中。這種集成使代碼更具可讀性,並減少了將類型註釋與其相應變量同步的維護成本。

TypeScript類型系統使程序員能夠表達對JavaScript對象功能的限制,並使用強制執行這些限制的工具。爲了最大程度地減少工具變得有用所需的註釋數量,TypeScript類型系統廣泛使用類型推斷。例如,從以下語句中,TypeScript將推斷變量’i’具有number類型。

var i = 0;

TypeScript將根據以下函數定義推斷函數f具有string返回類型。

function f() {  
    return "hello";  
}

爲了從這種推斷中受益,程序員可以使用TypeScript語言服務。 例如,代碼編輯器可以合併TypeScript語言服務,並使用該服務來查找字符串對象的成員,如以下屏幕截圖所示。
在這裏插入圖片描述
在此示例中,程序員無需提供類型註釋即可受益於類型推斷。 但是,某些有益的工具確實需要程序員提供類型註釋。 在TypeScript中,我們可以像下面的代碼片段中那樣表達參數要求。

function f(s: string) {  
    return s;  
}

f({});       // Error  
f("hello");  // Ok

參數s上的此可選類型註釋使TypeScript類型檢查器知道程序員期望參數s的類型爲“字符串”。 在函數“ f”的主體內,工具可以假定“ s”的類型爲“字符串”,並提供與該假設一致的操作類型檢查和成員變量自動完成。 工具也可以在第一次調用“ f”時生成錯誤信號,因爲“ f”需要字符串而不是對象作爲參數。 對於函數“ f”,TypeScript編譯器將生成以下JavaScript代碼:

function f(s) {  
    return s;  
}

在JavaScript輸出中,所有類型註釋均已刪除。 通常,TypeScript在生成JavaScript之前會擦除所有類型信息。

1.1 環境聲明

環境聲明將變量引入TypeScript範圍,但對生成的JavaScript程序的影響爲零。 程序員可以使用環境聲明來告訴TypeScript編譯器某些其他組件將提供變量。 例如,默認情況下,TypeScript編譯器將爲使用未定義變量打印錯誤。 要添加瀏覽器定義的一些公共變量,TypeScript程序員可以使用環境聲明。 下面的示例聲明瀏覽器提供的“document”對象。 因爲聲明未指定類型,所以推斷類型爲“ any”。 “any”類型意味着工具不能對document對象的形狀或行爲承擔任何責任。 下面的一些示例將說明程序員如何使用類型來進一步表徵對象的預期行爲。

declare var document;  
document.title = "Hello";  // Ok because document has been declared

對於“document”,TypeScript編譯器會自動提供一個聲明,因爲默認情況下,TypeScript包含一個文件“ lib.d.ts”,該文件爲內置JavaScript庫以及文檔對象模型提供了接口聲明。

TypeScript編譯器默認不包括jQuery的接口,因此要使用jQuery,程序員可以提供如下聲明:

declare var $;

第1.3節提供了一個更廣泛的示例,說明程序員如何爲jQuery和其他庫添加類型信息。

1.2 函數類型

函數表達式是JavaScript的強大功能。 它們使函數定義能夠創建閉包:從圍繞函數定義的詞法範圍中捕獲信息的函數。 當前,閉包是JavaScript強制執行數據封裝的唯一方法。 通過捕獲和使用環境變量,閉包可以保留無法從閉包外部訪問的信息。 JavaScript程序員經常使用閉包來表示事件處理程序和其它異步回調,其中另一個軟件組件(例如DOM)將通過處理程序函數回調JavaScript。

TypeScript函數類型使程序員可以表達函數的預期簽名。 函數簽名是參數類型加返回類型的序列。 下面的示例使用函數類型來表達異步vote機制的回調簽名要求。

function vote(candidate: string, callback: (result: string) => any) {  
   // ...  
}

vote("BigPig",  
     function(result: string) {  
         if (result === "BigPig") {  
            // ...  
         }  
     }  
);

在此示例中,“ vote”的第二個參數具有函數類型

(result: string) => any

這意味着第二個參數是一個返回“ any”類型的函數,該函數具有一個名爲“ result”的“ string”類型的單個參數。第3.9.2節提供了有關函數類型的其他信息。

1.3 對象類型

TypeScript程序員使用對象類型來聲明他們對對象行爲的期望。 以下代碼使用對象類型文字來指定’MakePoint’函數的返回類型。

var MakePoint: () => {  
    x: number; y: number;  
};

程序員可以給對象類型起名字; 我們稱爲命名對象類型接口。 例如,在以下代碼中,接口聲明一個必填字段(name)和一個可選字段(favoriteColor)。

interface Friend {  
    name: string;  
    favoriteColor?: string;  
}

function add(friend: Friend) {  
    var name = friend.name;  
}

add({ name: "Fred" });  // Ok  
add({ favoriteColor: "blue" });  // Error, name required  
add({ name: "Jill", favoriteColor: "green" });  // Ok

TypeScript對象類型對JavaScript對象可以表現的行爲多樣性進行建模。 例如,jQuery庫定義一個對象“ $”,該對象具有諸如“ get”(發送Ajax消息)之類的方法以及諸如“ browser”(向瀏覽器供應商提供信息)之類的字段。 但是,jQuery客戶端也可以將“ $”作爲函數調用。 該函數的行爲取決於傳遞給該函數的參數的類型。

以下代碼片段捕獲了jQuery行爲的一小部分,足以以一種簡單的方式使用jQuery。

interface JQuery {  
    text(content: string);  
}  
  
interface JQueryStatic {  
    get(url: string, callback: (data: string) => any);     
    (query: string): JQuery;  
}

declare var $: JQueryStatic;

$.get("http://mysite.org/divContent",  
      function (data: string) {  
          $("div").text(data);  
      }  
);

“ JQueryStatic”接口引用另一個接口:“ JQuery”。 此接口表示一個或多個DOM元素的集合。 jQuery庫可以對這樣的集合執行許多操作,但是在此示例中,jQuery客戶端僅需要知道它可以通過將字符串傳遞給’text’方法來設置集合中每個jQuery元素的文本內容。 “ JQueryStatic”接口還包含“ get”方法,該方法對提供的URL執行Ajax get操作,並安排在收到響應後調用提供的回調。

最後,“ JQueryStatic”接口包含一個裸函數簽名

(query: string): JQuery;

裸簽名表示該接口的實例是可調用的。 此示例說明TypeScript函數類型只是TypeScript對象類型的特例。 具體來說,函數類型是包含一個或多個調用簽名的對象類型。 因此,我們可以將任何函數類型編寫爲對象類型文字。 下面的示例使用兩種形式描述相同的類型。

var f: { (): string; };  
var sameType: () => string = f;     // Ok  
var nope: () => number = sameType;  // Error: type mismatch

上面我們提到,’$'函數的行爲取決於其參數的類型。 到目前爲止,我們的jQuery輸入僅捕獲以下行爲之一:傳遞字符串時返回類型爲’JQuery’的對象。 爲了指定多種行爲,TypeScript支持在對象類型中重載函數簽名。 例如,我們可以向’JQueryStatic’接口添加一個附加的呼叫簽名。

(ready: () => any): any;

此簽名表示可以將一個函數作爲’$‘函數的參數傳遞。 當函數傳遞給’$'時,當DOM文檔準備就緒時,jQuery庫將調用該函數。 因爲TypeScript支持重載,所以工具可以使用TypeScript來顯示所有可用的函數簽名及其文檔提示,並在使用特定簽名調用函數後提供正確的文檔。

典型的客戶不需要添加任何其他類型,而只需使用社區提供的類型來發現(通過帶有文檔提示的語句完成)並(通過靜態檢查)驗證庫的正確使用,如以下屏幕截圖所示。
在這裏插入圖片描述
第3.9.2節提供了有關對象類型的其他信息。

1.4 結構子類型化

對象類型會進行結構化的比較。 例如,在下面的代碼片段中,類“ CPoint”與接口“ Point”匹配,因爲“ CPoint”具有“ Point”的所有必需成員。 一個類可以選擇聲明它實現了一個接口,以便編譯器將檢查該聲明的結構兼容性。 該示例還說明,對象類型可以匹配從對象文字推斷出的類型,只要該對象文字提供了所有必需的成員。

interface Point {  
    x: number;  
    y: number;  
}

function getX(p: Point) {  
    return p.x;  
}

class CPoint {  
    x: number;  
    y: number;  
    constructor(x: number,  y: number) {  
        this.x = x;  
        this.y = y;  
    }  
}

getX(new CPoint(0, 0));  // Ok, fields match

getX({ x: 0, y: 0, color: "red" });  // Extra fields Ok

getX({ x: 0 });  // Error: supplied parameter does not match

有關類型比較的更多信息,請參見第3.3節

1.5 上下文類型推斷

通常,TypeScript類型推斷是“自下而上”的:從表達式樹的葉子到其根。 在下面的示例中,TypeScript通過在返回表達式中自底向上流動類型信息來推斷“ number”作爲函數“ mul”的返回類型。

function mul(a: number, b: number) {  
    return a * b;  
}

對於沒有類型註釋或默認值的變量和參數,TypeScript推斷類型爲“ any”,以確保編譯器不需要有關函數調用位置的非本地信息即可推斷函數的返回類型。 通常,這種自下而上的方法爲程序員提供了有關類型信息流的清晰直覺。

但是,在某些有限的上下文中,推斷是從表達式的上下文“自頂向下”進行的。 發生這種情況的地方稱爲上下文類型化。 當程序員使用一種類型但可能不知道該類型的所有詳細信息時,上下文類型化幫助工具提供了出色的信息。 例如,在上面的jQuery示例中,程序員將函數表達式作爲第二個參數提供給’get’方法。 在鍵入該表達式期間,工具可以假定函數表達式的類型與“ get”簽名中給出的類型相同,並且可以提供包含參數名稱和類型的模板。

$.get("http://mysite.org/divContent",  
      function (data) {  
          $("div").text(data);  // TypeScript infers data is a string  
      }  
);

上下文類型對於寫出對象文字也很有用。 當程序員鍵入對象文字時,上下文類型提供的信息使工具能夠爲對象成員名稱提供補全。

第4.23節提供了有關上下文類型表達式的更多信息。

1.6 類

JavaScript實踐有兩種非常常見的設計模式:模塊模式和類模式。粗略地說,模塊模式使用閉包來隱藏名稱並封裝私有數據,而類模式使用原型鏈來實現面向對象繼承機制的許多變體。諸如“ prototype.js”之類的庫是這種做法的典型。 TypeScript的命名空間是模塊模式的形式化形式。 (現在,術語“模塊模式”有點不幸,因爲ECMAScript 2015以與模塊模式規定的方式不同的形式正式支持模塊。因此,TypeScript使用術語“命名空間”來表示其模塊模式的形式化。)

本節和下面的命名空間部分將展示TypeScript在爲類和命名空間生成ECMAScript 3或5兼容代碼時如何生成一致的慣用JavaScript。 TypeScript轉換的目標是確切地傳達程序員在實現類或工具所沒有的命名空間時鍵入的內容。本節還將描述TypeScript如何爲每個類聲明推斷類型。我們將從一個簡單的BankAccount類開始。

class BankAccount {  
    balance = 0;  
    deposit(credit: number) {  
        this.balance += credit;  
        return this.balance;  
    }  
}  

此類生成以下JavaScript代碼。

var BankAccount = (function () {  
    function BankAccount() {  
        this.balance = 0;  
    }  
    BankAccount.prototype.deposit = function(credit) {  
        this.balance += credit;  
        return this.balance;  
    };  
    return BankAccount;  
})();

此TypeScript類聲明創建一個名爲“ BankAccount”的變量,其值是“ BankAccount”實例的構造函數。 此聲明還會創建一個具有相同名稱的實例類型。 如果我們將這種類型編寫爲接口,則如下所示。

interface BankAccount {  
    balance: number;  
    deposit(credit: number): number;  
}

如果我們要爲“ BankAccount”構造函數變量寫出函數類型聲明,它將具有以下形式。

var BankAccount: new() => BankAccount;

函數簽名以關鍵字“ new”爲前綴,指示必須將“ BankAccount”函數作爲構造函數調用。 函數的類型可能同時具有調用和構造函數簽名。 例如,內置的JavaScript Date對象的類型包括兩種簽名。

如果要使用初始餘額啓動銀行帳戶,則可以向“ BankAccount”類添加構造函數聲明。

class BankAccount {  
    balance: number;  
    constructor(initially: number) {  
        this.balance = initially;  
    }  
    deposit(credit: number) {  
        this.balance += credit;  
        return this.balance;  
    }  
}

此版本的’BankAccount’類要求我們引入一個構造函數參數,然後將其分配給’balance’字段。 爲了簡化這種常見情況,TypeScript接受以下簡略語法。

class BankAccount {  
    constructor(public balance: number) {  
    }  
    deposit(credit: number) {  
        this.balance += credit;  
        return this.balance;  
    }  
}

“ public”關鍵字表示構造函數參數將保留爲字段。 public是類成員的默認可訪問性,但是程序員也可以爲類成員指定private或protected的可訪問性。 可訪問性是設計時的構造; 它在靜態類型檢查期間強制執行,但並不意味着任何運行時強制執行。

TypeScript類還支持繼承,如以下示例所示。* *

class CheckingAccount extends BankAccount {  
    constructor(balance: number) {  
        super(balance);  
    }  
    writeCheck(debit: number) {  
        this.balance -= debit;  
    }  
}

在此示例中,類“ CheckingAccount”派生自類“ BankAccount”。 “ CheckingAccount”的構造函數使用“ super”關鍵字調用類“ BankAccount”的構造函數。 在生成的JavaScript代碼中,“ CheckingAccount”的原型將鏈接到“ BankAccount”的原型。

TypeScript類也可以指定靜態成員。 靜態類成員成爲類構造函數的屬性。

第8節提供了有關類的其他信息。

1.7 枚舉類型

使用TypeScript,程序員可以將一組數字常量總結爲一個枚舉類型。 下面的示例創建一個枚舉類型來表示計算器應用程序中的運算符。

const enum Operator {  
    ADD,  
    DIV,  
    MUL,  
    SUB  
}

function compute(op: Operator, a: number, b: number) {  
    console.log("the operator is" + Operator[op]);  
    // ...  
}

在此示例中,計算功能使用枚舉類型的功能記錄運算符“ op”:從枚舉值(op)到與該值對應的字符串的反向映射。 例如,“運算符”的聲明會自動將從零開始的整數分配給列出的枚舉成員。 第9節介紹了程序員還可以如何將整數顯式分配給枚舉成員,以及如何使用任何字符串來命名枚舉成員。

當使用const修飾符聲明枚舉時,TypeScript編譯器將爲該枚舉成員生成與該成員的分配值相對應的JavaScript常量(帶有註釋)。 這樣可以提高許多JavaScript引擎的性能。

例如,“計算”功能可能包含如下的switch語句。

switch (op) {  
    case Operator.ADD:  
        // execute add  
        break;  
    case Operator.DIV:  
        // execute div  
        break;  
    // ...  
}

對於此switch語句,編譯器將生成以下代碼。

switch (op) {  
    case 0 /* Operator.ADD */:  
        // execute add  
        break;  
    case 1 /* Operator.DIV */:  
        // execute div  
        break;  
    // ...  
}

JavaScript實現可以使用這些顯式常量爲該switch語句生成有效的代碼,例如,通過構建一個由case值索引的跳轉表。

1.8 字符串參數重載

TypeScript的一個重要目標是爲現有的JavaScript編程模式提供準確而直接的類型。 爲此,TypeScript包括將在下一節中討論的泛型類型,以及本節的主題字符串參數的重載。

JavaScript編程接口通常包含一些函數,這些函數的行爲可以通過傳遞給該函數的字符串常量來區分。 文檔對象模型大量使用了這種模式。 例如,下面的屏幕快照顯示’document’對象的’createElement’方法具有多個簽名,其中一些簽名標識了將特定字符串傳遞給該方法時返回的類型。
在這裏插入圖片描述
以下代碼片段使用此功能。 因爲推斷’span’變量的類型爲’HTMLSpanElement’,所以代碼可以無靜態錯誤地引用’span’的’isMultiline’屬性。

var span = document.createElement("span");  
span.isMultiLine = false;  // OK: HTMLSpanElement has isMultiline property

在下面的屏幕快照中,一個編程工具將字符串參數重載的信息與上下文類型相結合,以推斷變量’e’的類型爲’MouseEvent’,因此’e’具有’clientX’屬性。
在這裏插入圖片描述
第3.9.2.4節節提供了有關如何在函數簽名中使用字符串文字的詳細信息。

1.9 通用類型和功能

類似於字符串參數的重載,泛型類型使TypeScript更容易準確地捕獲JavaScript庫的行爲。 因爲它們使類型信息能夠從客戶端代碼,庫代碼再流回到客戶端代碼,所以泛型類型可以比任何其它TypeScript功能做更多的事情來支持詳細的API描述。

爲了說明這一點,讓我們看一下內置JavaScript數組類型的TypeScript接口的一部分。 您可以在TypeScript發行版隨附的’lib.d.ts’文件中找到此接口。

interface Array<T> {  
    reverse(): T[];  
    sort(compareFn?: (a: T, b: T) => number): T[];  
    // ...   
}

與上面的定義類似,接口定義可以具有一個或多個類型參數。 在這種情況下,“數組”接口只有一個參數“T”,用於定義數組的元素類型。 ‘reverse’方法返回具有相同元素類型的數組。 sort方法採用一個可選參數’compareFn’,其類型是一個函數,該函數採用兩個類型爲’T’的參數並返回一個數字。 最後,sort返回元素類型爲“T”的數組。

函數也可以具有通用參數。 例如,數組接口包含一個“map”方法,定義如下:

map<U>(func: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];

在元素類型爲“ T”的數組“ a”上調用的map方法將對每個“ a”元素應用函數“ func”,並返回類型爲“ U”的值。

TypeScript編譯器通常可以推斷通用方法參數,從而使程序員無需顯式提供它們。 在下面的示例中,編譯器推斷map方法的參數’U’具有類型’string’,因爲傳遞給map的函數返回一個字符串。

function numberToString(a: number[]) {  
    var stringArray = a.map(v => v.toString());  
    return stringArray;  
}

在此示例中,編譯器推斷’numberToString’函數返回字符串數組。

在TypeScript中,類也可以具有類型參數。 以下代碼聲明一個類,該類實現“T”類型的項目的鏈接列表。 此代碼說明程序員如何限制類型參數以擴展特定類型。 在這種情況下,列表上的項目必須擴展“ NamedItem”類型。 這使程序員可以實現“日誌”功能,該功能可以記錄項目的名稱。

interface NamedItem {  
    name: string;  
}

class List<T extends NamedItem> {  
    next: List<T> = null;

    constructor(public item: T) {  
    }

    insertAfter(item: T) {  
        var temp = this.next;  
        this.next = new List(item);  
        this.next.next = temp;  
    }

    log() {  
        console.log(this.item.name);  
    }

    // ...  
}

第3.7節提供了有關泛型類型的更多信息。

1.10 命名空間

類和接口通過提供一種機制來描述如何使用可以與該組件的實現分離的軟件組件,從而支持大規模JavaScript開發。 TypeScript在設計時(通過限制使用私有成員和受保護成員)在類中實施實現的封裝,但由於在運行時可以訪問所有對象屬性,因此無法在運行時實施封裝。 JavaScript的未來版本可能會提供private名稱,這將使private成員和protected成員的運行時實施成爲可能。

在JavaScript中,在運行時強制執行封裝的一種非常常見的方法是使用模塊模式:使用閉包變量封裝私有字段和方法。模塊模式是通過在軟件組件周圍繪製邊界來提供組織結構和動態加載選項的自然方法。模塊模式還可以提供引入命名空間的能力,從而避免對大多數軟件組件使用全局命名空間。

以下示例說明了JavaScript模塊模式。

(function(exports) {  
    var key = generateSecretKey();  
    function sendMessage(message) {  
        sendSecureMessage(message, key);  
    }  
    exports.sendMessage = sendMessage;  
})(MessageModule);

此示例說明了模塊模式的兩個基本元素:模塊關閉和模塊對象。模塊關閉是封裝模塊實現的函數,在這種情況下,變量爲“ key”,函數爲“ sendMessage”。模塊對象包含導出的變量和模塊功能。簡單的模塊可以創建並返回模塊對象。上面的模塊將模塊對象作爲參數“ exports”,並將“ sendMessage”屬性添加到模塊對象。這種擴充方法簡化了模塊的動態加載,還支持將模塊代碼分成多個文件。

該示例假定外部詞彙範圍定義了函數’generateSecretKey’和’sendSecureMessage’;它還假定外部作用域已將模塊對象分配給變量“ MessageModule”。

TypeScrip的命名空間提供了一種簡潔表達模塊模式的機制。在TypeScript中,程序員可以通過在外部命名空間中嵌套命名空間和類來將模塊模式與類模式組合。

以下示例顯示了簡單命名空間的定義和使用。

namespace M {  
    var s = "hello";  
    export function f() {  
        return s;  
    }  
}

M.f();  
M.s;  // Error, s is not exported

在此示例中,變量“ s”是命名空間的私有功能,但是函數“ f”是從命名空間導出的,並且可以在命名空間之外的代碼中訪問。 如果要用接口和變量來描述命名空間“ M”的作用,我們將寫

interface M {  
    f(): string;  
}

var M: M;

接口“ M”概括了命名空間“ M”的外部可見行爲。 在此示例中,我們可以爲接口使用與初始化變量相同的名稱,因爲在TypeScript中,類型名和變量名不會衝突:每個詞法作用域都包含變量聲明空間和類型聲明空間(有關更多詳細信息,請參見第2.3節)。

TypeScript編譯器爲命名空間生成以下JavaScript代碼:

var M;  
(function(M) {  
    var s = "hello";  
    function f() {  
        return s;  
    }  
    M.f = f;  
})(M || (M = {}));

在這種情況下,編譯器假定命名空間對象位於全局變量“ M”中,該變量可能已初始化或尚未初始化爲所需的命名空間對象。

1.11 模塊

TypeScript還支持ECMAScript 2015模塊,這些模塊是包含頂級導出和導入指令的文件。 對於這種類型的模塊,TypeScript編譯器可以生成ECMAScript 2015兼容代碼以及下級ECMAScript 3或5兼容代碼,以用於各種模塊加載系統,包括CommonJS,異步模塊定義(AMD)和通用模塊定義(UMD) 。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章