在網頁前端的世界中,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 GARBAGE></script> <script GARBAGE></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 的型別定義時,給予很多學習上的指引與觀念上的指導。若文章中有敘述或觀念錯誤的地方,還請聯繫我讓我了解、改進與修正,謝謝大家的多多指教與包含。