選單
GSS 技術部落格
在這個園地裡我們將從技術、專案管理、客戶對談面和大家分享我們多年的經驗,希望大家不管是喜歡或是有意見,都可以回饋給我們,讓我們有機會和大家對話並一起成長!
若有任何問題請來信:gss_crm@gss.com.tw
9 分鐘閱讀時間 (1734 個字)

jQuery with typescript

jQuery with typescript

 如影隨形的 jQuery

  在網頁前端的世界中,jQuery 曾主宰著這塊領域,不管是其可以彈性地操作 HTML 元素,或是提供多樣化的 API 等特色,讓 jQuery 在許多專案中都扮演著重要角色。

  隨著前端不斷的快速演進,有許多函示庫、前端框架、甚至是可能撼動 JavaScript 地位的新技術等等不斷出現,即使如此,在許多專案依舊可以看到 jQuery 的影子。

  在一些比較早期開發的專案自然不用說會出現 jQuery,在新函示庫或前端框架的專案中,也許基於一些考量(例如:想使用的套件尚未支援此種新框架等),仍然會出現 jQuery。

  在工作中,有大量使用到 jQuery Widget 的情境。最近想要在既有 webpack 與 TypeScript 的建置環境下,使用 TypeScript 客製 jQuery Widget 供專案使用。雖然對很多前端先進們而言,這已經是基本常識,在這邊還是向大家分享一下在整合 webpack、TypeScript 與 jQuery 的一些歷程。若有觀念錯誤的地方,還請大家多多包含與指教。

  為簡化問題並縮限範圍,這邊會以簡單的步驟與範例進行說明。

 加入 jQuery UI 定義

  由於原本的專案已經有引入 jQuery 與 jQuery UI 的函示庫了,因此在這邊不參考完整的套件,僅在開發環境中加入型別定義套件。

  為了讓 TypeScript 認得懂 jQuery UI 各式各樣的 API,這裡使用到了 DefinitelyTyped 專案 所提供的定義;另外,此套件會相依於 jQuery 定義套件。

  在專案環境中,執行以下指令:

  npm install @types/jqueryui --save-dev

  若專案使用yarn,執行以下指令:

   yarn add @types/jqueryui --dev

 新增 Widget

  新增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 特性),新增了一些型別定義。

    • 定義了AccountsWidgetAccountsWidgetOptions型別。

  調整 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 等等。

 使用其他 .js 檔案中的方法

  有時候會有開發人員將常用、共用的 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})
  ];
},
... 

 使用 AccountsWidget

  在這邊新增了一個index.ts檔案,初始化並使用上述準備好的AccountsWidget,此外,此檔案亦作為 webpack 的進入點。

  • 為了讓 TypeScript 初始化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 的資料;此外,所有Accountname皆會受到mapAccount(account)影響,串上「 Mapped」後綴。

  實際執行結果如下所示,符合預期結果:

圖一、瀏覽器 console 中顯示的執行結果與預期結果相符

 結語

  希望這段整合過程,能對專案的開發與效率帶來助益。雖然這篇屬於新手向文章,也希望能幫助到一些有需要的人。在此特別感謝 Isaac 大師的幫忙,在我摸索 TypeScript 的型別定義時,給予很多學習上的指引與觀念上的指導。若文章中有敘述或觀念錯誤的地方,還請聯繫我讓我了解、改進與修正,謝謝大家的多多指教與包含。

let’s encrypt ssl憑證程式certbot不支援ubuntu 14 04
Angular #6 - Services and Components[3]

相關文章

 

評論

尚無評論
已經注冊了? 這裡登入
Guest
2024/05/12, 週日

Captcha 圖像