TypeScript是強型別的世界,透過預先宣告物件、屬性、方法、介面,在編輯階段提供Intellisense提示、Go Definition、Find All References、Rename... 等編譯式語言才有的功能,而編譯時可預先抓出參數、型別、方法錯誤,降低執行階段發現修復的高昂成本。

開始使用TypeScript一段時間後,一定會發現一項困擾: 雖然DefinitelyTyped計劃已匯集許多常用JavaScript程式庫的TypeScript定義檔,但畢竟無法涵蓋你會用到的每一個JavaScript程式庫,以我自己為例,馬上面對的問題的便是: 我找不到jQuery BlockUI的定義檔。

於是,在程式中要引用block(),blockUI()會出現紅蚯蚓無法編譯。

有一個取巧解法,只要不在全域範圍,在module、interface、class內部可以透過declare指令重新將jQuery、$定義成any型別,declare的變數並不會出現在JavaScript中,純粹讓編譯器假設該變數存在。如此jQuery變成任意型別物件,不管加上什麼屬性、呼叫任何方法都視為合法,但這得付出代價 – 與jQuery相關的Intellisense與編輯檢查從此失效,退回JavaScript時代。

我曾想到一個投機做法,declare var $就好,當需要強型別時寫jQuery("div"),需要用無定義檔方法時寫$("div")。不過身為程式魔人,一直偷雞摸狗下去也不是辦法,還是乖乖學會怎麼為jQuery Plugin寫定義檔吧!

這裡先試寫一個簡單但無聊的jQuery Plugin當成練習目標,為了涵蓋常遇到的各式情境,我刻意加入多種存取API:

$("…").fill(); 元素塗色(用預設顏色)
$("...").fill({ color: "red" }); 元素塗色(指定顏色)
$.fill(); 網頁塗色(用預設顏色)
$.fill({ color: "red" }); 網頁塗色(指定顏色)
$.fill.options.color = "blue"; 修改預設顏色
$.title(); 取得網頁標題
$.title("…"); 設定網頁標題

TypeScript程式碼如下:

/** jquery.fill options */
interface JQFillOptions {
    /** fill color */
    color?: string;
}
 
(function ($) {
    var defaultOptions: JQFillOptions = {
        color: "red"
    };
    //merge option and default option to get fill color
    function getColor(options?: JQFillOptions) {
        return $.extend({}, defaultOptions, options).color;
    }
    //fill background to document.body
    $.fill = function (options?: JQFillOptions) {
        $("body").css("background-color", getColor(options));
    }
    //global options
    $.fill.options = defaultOptions;
    //fill background for element
    $.fn.fill = function (options?: JQFillOptions) {
        return this.each(function () {
            $(this).css("background-color", getColor(options));
        });
    };
    //get and set document.title
    $.title = function (title?: string) {
        if (title)
            document.title = title;
        else
            return document.title;
    };
})(jQuery);

寫個網頁測試:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>jQuery Fill Plugin</title>
    <style>
        div { 
            margin: 12px; padding: 6px; width: 100px; 
            color: white;
        }
    </style>
</head>
<body>
    <div>Test</div>
    <div>Test</div>
    <script src="../Scripts/jquery-2.1.1.js"></script>
    <script src="jquery.fill.js"></script>
    <script>
        $.fill.options.color = "yellow";
        $.fill();
        $("div").first().fill({ color: "blue" })
        .end().last().fill({ color: "green" });
        $.title($.title() + "$$");
    </script>
 
</body>
</html>

測試成功!

接著我們把JavaScript抽出來改寫為TypeScript,一如預料,馬上遇到JQuery、JQueryStatic未定義fill、title的錯誤,無法編譯。

為了讓TypeScript認識我們的Plugin,我們需在jquery.fill.ts加入interface JQuery及interface JQueryStatic宣告,TypeScript會將它們合併到JQuery定義檔的同名interface中,如此JQuery會多了.fill()以支援$("…").fill()語法、JQueryStatic會增加.fill()、.title()以支援$.fill()、$.title()。不過有個地方比較麻煩,由於要同時支援$.fill()、$.fill.options兩種存取方式,需多宣告一個interface JQFillStatic,其中包含一個方法(options?: JQFillOptions)以及一個屬性options,而JQueryStatic interface的fill型別為JQFillStatic,如此才能讓$.fill()與$.fill.options都有效。宣告程式如下:

/** jquery.fill options */
interface JQFillOptions {
    /** fill color */
    color?: string;
}
interface JQuery {
    /**
     * 將元素填滿背景色
     * 
     * @param options 顏色設定,未提供時依預設值
     */
    fill(options?: JQFillOptions): JQuery;
}
interface JQFillStatic {
    /**
     * 將document.body填滿背景色
     * 
     * @param options 顏色設定,未提供時依預設值
     */
    (options?: JQFillOptions);
    /** 顏色預設值 */
    options?: JQFillOptions;
}
interface JQueryStatic {
    fill?: JQFillStatic;
    /** 
     * 取得或設定document.title
     * @param title 要設定的網頁標題,未提供時傳回現有標題
     */
    title(title?: string);
}
 
(function ($) {
    ///...省略...
})(jQuery);
 

如此,TypeScript就認得我們的Plugin囉~

在以上案例,JQuery Plugin用TypeScript開發,因此JQuery、JQueryStatic宣告直接寫入同一TypeScript檔即可。如果是第三方JavaScript,做法則比照scripts/typings/jquery/jquery.d.ts,要為該JavaScript檔寫一個同檔名的.d.ts。更進一步,還可將你寫好的定義檔貢獻到DefinitelyTyped,分享給開發社群,這才是新時代的好男兒! (遠目)

劍及履及,我的第一個TypeScript定義檔已經被Merge到DefintelyTyped,DefinitelyTyped會自動將其包成NuGet Package,所以jQuery BlockUI現在有TypeScript定義檔囉~

關於撰寫定義檔,TypeScript CodePlex上有篇教學: Writing Definition (.d.ts) Files,如果你也有心參與DefinitelyTyped的定義檔補完計劃,可以參考貢獻指南


Comments

# by Jerry

不好意思, 我想請問。有的plugin使用方法是 $.fn.pluginname.fun(); 我根據提供的API文件寫定義檔 但是不曉得怎麼定義 $.fn.pluginname 這應該是個物件 目前是這樣做的: interface JQuery { pluginname: { ... fun: () => void; } } 但是在 visual studio 上寫測試的typescript時, autocomplete(是這樣說嗎?)的選單裡不是我註解的jsdoc內容 雖然沒有錯誤,但是這樣沒有type checking了吧? 請問我是否漏掉了甚麼? 謝謝!

# by Jeffrey

to Jerry, 請參考我的測試 <img src="http://i.imgur.com/8dScTjV.gif" alt="" />

# by Jerry

有耶。 謝謝! 我了解一下jQuery.fn 它是jQuery.prototype的alias 建立基於jQuery的plugin,透過擴增fn,即增加新的成員函式 所以jQuery 物件都可使用到 如: jQuery([selector]).pluginname.fun(); 或 #([selector]).pluginname.fun();

# by alert('2333')

alert('2333')

# by Jeffrey

to alert('2333'), 感謝協助XSS測試。

# by Kyle.M

你好,我是angular的新手, 請問當我執行了以下安裝指令後,如何可以在component 層面使用? 謝謝! npm i @types/jquery.blockui --save

# by Jeffrey

to Kyle.M, NG我只用到AngluarJS,從Angular2之後就脫節了,推薦你可到 https://www.facebook.com/groups/augularjs.tw/ 問看看。

Post a comment