在網頁前端的世界中,jQuery 曾主宰著這塊領域,不管是其可以彈性地操作 HTML 元素,或是提供多樣化的 API 等特色,讓 jQuery 在許多專案中都扮演著重要角色。
隨著前端不斷的快速演進,有許多函示庫、前端框架、甚至是可能撼動 JavaScript 地位的新技術等等不斷出現,即使如此,在許多專案依舊可以看到 jQuery 的影子。
在一些比較早期開發的專案自然不用說會出現 jQuery,在新函示庫或前端框架的專案中,也許基於一些考量(例如:想使用的套件尚未支援此種新框架等),仍然會出現 jQuery。
在工作中,有大量使用到 jQuery Widget 的情境。最近想要在既有 webpack 與 TypeScript 的建置環境下,使用 TypeScript 客製 jQuery Widget 供專案使用。雖然對很多前端先進們而言,這已經是基本常識,在這邊還是向大家分享一下在整合 webpack、TypeScript 與 jQuery 的一些歷程。若有觀念錯誤的地方,還請大家多多包含與指教。
為簡化問題並縮限範圍,這邊會以簡單的步驟與範例進行說明。
由於原本的專案已經有引入 jQuery 與 jQuery UI 的函示庫了,因此在這邊不參考完整的套件,僅在開發環境中加入型別定義套件。
為了讓 TypeScript 認得懂 jQuery UI 各式各樣的 API,這裡使用到了 DefinitelyTyped 專案 所提供的定義;另外,此套件會相依於 jQuery 定義套件。
在專案環境中,執行以下指令:
npm install @types/jqueryui --save-dev
若專案使用yarn,執行以下指令:
yarn add @types/jqueryui --dev
新增accounts.widget.ts檔案,並加入以下程式碼:
(($: JQueryStatic) => {
$.widget('test.AccountsWidget', {
options: {
getAccountsApi: 'api/v1/accounts'
},
_create: function () {
this._super();
},
_init: function () {
this._super();
},
_destroy: function () {
this._super();
},
searchAccounts: function (criteria: any) {
return [];
}
});
})(jQuery);
雖然 Widget 的型別定義並未提供_create()、_init()、_destroy()等方法,但你仍然可以使用這些 jQuery UI Widget 所提供的生命週期方法。另外,此範例中的 Widget 提供了searchAccounts(criteria)方法供外部使用。
為使用到 TypeScript 強型別的特性(這邊的範例大量使用 Structural Type System 特性),新增了一些型別定義。
AccountsWidget與AccountsWidgetOptions型別。 調整accounts.widget.ts檔案:
+ interface AccountsWidget {
+ options: AccountsWidgetOptions;
+ searchAccounts(criteria: any): Array<any>;
+ }
+ interface AccountsWidgetOptions {
+ getAccountsApi: string;
+ }
- (($: JQueryStatic) => {
+ (($: JQueryStatic) => <AccountsWidget>{
...
Account型別。 新增account.d.ts檔案,並加入以下程式碼:
type Account = {
id: number;
name: string;
};
調整accounts.widget.ts檔案:
interface AccountsWidget {
options: AccountsWidgetOptions;
- searchAccounts(criteria: any): Array<any>;
+ searchAccounts(criteria: any): Array<Account>;
}
AccountsCriteria型別。 調整accounts.widget.ts檔案:
interface AccountsWidget {
options: AccountsWidgetOptions;
- searchAccounts(criteria: any): Array<Account>;
+ searchAccounts(criteria: AccountsCriteria): Array<Account>;
}
...
+interface AccountsCriteria {
+ name: string;
+ page: number;
+ pageSize: number;
+}
...
$.widget('test.AccountsWidget', <AccountsWidget>{
...
- searchAccounts: function (criteria: any) {
+ searchAccounts: function (criteria: AccountsCriteria) {
...
_accounts資料物件,並簡易實作查詢方法searchAccounts(criteria: AccountsCriteria)。 調整accounts.widget.ts檔案:
...
(($: JQueryStatic) => {
+ const _accounts: Array<Account> = [{
+ id: 1,
+ name: 'Cash'
+ }, {
+ id: 2,
+ name: 'Account Receivable'
+ }, {
+ id: 3,
+ name: 'Account Payable'
+ }];
$.widget('test.AccountsWidget', <AccountsWidget>{
...
searchAccounts: function (criteria: AccountsCriteria) {
+ criteria ??= {
+ name: '',
+ page: 1,
+ pageSize: 10,
+ };
+ const lower: number = (criteria.page - 1) * criteria.pageSize;
+ const upper: number = criteria.page * criteria.pageSize;
+ // TODO: Use `GET accounts API` to get accounts by criteria.
- return [];
+ return [
+ ..._accounts.filter(a => !criteria.name || a.name.indexOf(criteria.name) >= 0)
+ .filter((a, i) => i >= lower && i < upper)
+ ];
},
...
到這邊為止,我們已經準備好了AccountsWidget,與平時用 JavaScript 開發 jQuery Widget 的流程相似,不同之處僅再加上一些 TypeScript 的型別定義,另外使用了較新的語法,例如:Arrow Function、Spread Syntax、Nullish Coalescing with Short-Circuiting Assignment Operators 等等。
有時候會有開發人員將常用、共用的 JavaScript 方法放到獨立的.js檔案中,讓其他地方直接呼叫的情況。為了讓 TypeScript 認得懂此方法,需要定義此方法。
mapAccount(account)的外部方法。 新增original-project-function.d.ts檔案,並加入以下程式碼:
declare function mapAccount(account: any): any;
要注意的是,由於這個方法並非在 TypeScript 內自己實作,比較保守的作法,必須注意此方法是否確實存在。
searchAccounts(criteria: AccountsCriteria)方法中,使用此方法。 調整accounts.widget.ts檔案:
...
searchAccounts: function (criteria: AccountsCriteria) {
...
return [
..._accounts.filter(a => !criteria.name || a.name.indexOf(criteria.name) >= 0)
.filter((a, i) => i >= lower && i < upper)
+ .map(a => typeof mapAccount === 'function'
+ ? mapAccount({...a})
+ : {...a})
];
},
...
在這邊新增了一個index.ts檔案,初始化並使用上述準備好的AccountsWidget,此外,此檔案亦作為 webpack 的進入點。
AccountsWidget,以及使用其所提供的searchAccounts(criteria)方法,藉由 TypeScript Declaration Merging 的特性,擴充 jQuery 型別定義。 調整accounts.widget.ts檔案:
+interface JQuery {
+ AccountsWidget(options: AccountsWidgetOptions): void;
+ AccountsWidget<TResult>(functionName: string): TResult;
+ AccountsWidget<TResult, TParams>(functionName: string, params: TParams): TResult;
+}
interface AccountsWidget {
...
AccountsWidget,並查詢兩次資料,顯示在瀏覽器 console 中。 新增index.ts檔案,並加入以下程式碼:
import './accounts.widget';
$(() => {
const app: JQuery<HTMLElement> = $('#app');
app.AccountsWidget({
getAccountsApi: 'api/v2/accounts'
});
const accounts: Array<Account> = app.AccountsWidget('searchAccounts');
const accounts2: Array<Account> = app.AccountsWidget('searchAccounts', {
name: 'Account',
page: 1,
pageSize: 2
});
console.log({ accounts, accounts2 });
});
為實際查看 webpack 依據index.ts打包出來的.js檔,在瀏覽器 console 所顯示的內容是否符合預期,在這邊僅簡易的新增了一個index.html來測試。
新增index.html檔案,並加入以下程式碼:
(註:在這邊使用圖片顯示程式碼內容,原因是如果使用程式碼區塊的方式,會被轉譯為下方的樣式。第一個 script 引用會被調整其他內容,此外,index.bundle.js的相對路徑也會被調整。)
<!DOCTYPE html> <html> <head> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> <script src="./index.bundle.js"></script> </head> <body> <div id="app"></div> </body> </html>
在上述程式碼中,引入了外部的 jQuery 與 jQuery UI 函示庫,並引入了 webpack 打包出來的index.bundle.js檔案;HTML 元素的部分,僅有一個id為 app 的div元素。
另外,先前提到呼叫其他.js檔案中的方法mapAccount(account),我們在此模擬與實作,直接加入至index.html當中。
調整index.html檔案:
...
+ <script>
+ function mapAccount(account) {
+ account.name += ' Mapped';
+ return account;
+ }
+ </script>
</head>
...
我們預期查詢出來的結果,會有兩個Account[],accounts包含所有資料,accounts2包含id為 2 與 3 的資料;此外,所有Account的name皆會受到mapAccount(account)影響,串上「 Mapped」後綴。
實際執行結果如下所示,符合預期結果:
希望這段整合過程,能對專案的開發與效率帶來助益。雖然這篇屬於新手向文章,也希望能幫助到一些有需要的人。在此特別感謝 Isaac 大師的幫忙,在我摸索 TypeScript 的型別定義時,給予很多學習上的指引與觀念上的指導。若文章中有敘述或觀念錯誤的地方,還請聯繫我讓我了解、改進與修正,謝謝大家的多多指教與包含。
When you subscribe to the blog, we will send you an e-mail when there are new updates on the site so you wouldn't miss them.
評論