JavaScript >> Javascript 文檔 >  >> Node.js

函數式編程:適用於 AWS Lambda 的類型安全節點路由器 - 第 1 部分

Lambda 函數基本上有三種常見的設計模式:Single Purpose Function、Fat Lambda 和 Lambda-lith。在這篇文章中,我們將討論 Lambda-lith 模式的輕量級版本。下圖顯示了 lith 模式的基本輪廓。

Lith 模式適用於沒有太多路由或太複雜業務邏輯的小型、有限的 API 和微服務。一切都適合單個 lambda 函數,所有路由都集中到該函數,該函數確定如何處理傳入請求。在 Node 中,路由通常由 Express 或 Koa 等框架處理。這種模式很簡單,允許您創建一個相同的本地開發環境,因為您的 Lith 本質上只是一個容器化的 Express/Koa/任何服務器。

但是,如果我們不想使用框架來處理一些路由的開銷呢? Express 和其他類似的框架迫使我們處理 request , response , 和 next 如果我們只是將路由與處理它的函數匹配,通常遠遠超過我們需要的對象。

此外,作為函數式程序員,使用這個 API 會讓我們感到不舒服,因為它不是類型安全的,沒有考慮函數純度,而且它讓我們傳遞對 Request 的可變引用 和 Response 對象。讓我們看看我們是否可以提出自己的路由方法來考慮這些 FP 規則。最終結果應該是一個輕量級模式,用於使用 AWS lambda 設計簡單但健壯的微服務。

TypeScript 中的函數式編程

這篇文章在很大程度上依賴於 fp-ts 庫。教授 fp-ts 超出了這篇文章的範圍,但是有很多資源可用於學習 TypeScript 和一般情況下的函數式編程。無論如何,即使你不理解每一行代碼,你也應該能夠跟隨。

對於路由,我們將使用構建在 fp-ts 之上的驚人的 fp-ts-routing 庫。

基本上,這個庫允許我們將表示路由的路徑字符串解析為包含在該路由中編碼的數據的預定義類型。讓我們看一下 GitHub 自述文件中的示例並逐步完成每個步驟。

//
// Locations
//

interface Home {
  readonly _tag: 'Home'
}

interface User {
  readonly _tag: 'User'
  readonly id: number
}

interface Invoice {
  readonly _tag: 'Invoice'
  readonly userId: number
  readonly invoiceId: number
}

interface NotFound {
  readonly _tag: 'NotFound'
}
// (1)
type Location = Home | User | Invoice | NotFound

const home: Location = { _tag: 'Home' }

const user = (id: number): Location => ({ _tag: 'User', id })

const invoice = (userId: number, invoiceId: number): Location => ({ _tag: 'Invoice', userId, invoiceId })

const notFound: Location = { _tag: 'NotFound' }

// matches (2)
const defaults = end
const homeMatch = lit('home').then(end)
const userIdMatch = lit('users').then(int('userId'))
const userMatch = userIdMatch.then(end)
const invoiceMatch = userIdMatch
  .then(lit('invoice'))
  .then(int('invoiceId'))
  .then(end)

// router (3)
const router = zero<Location>()
  .alt(defaults.parser.map(() => home))
  .alt(homeMatch.parser.map(() => home))
  .alt(userMatch.parser.map(({ userId }) => user(userId)))
  .alt(invoiceMatch.parser.map(({ userId, invoiceId }) => invoice(userId, invoiceId)))

// helper
const parseLocation = (s: string): Location => parse(router, Route.parse(s), notFound)

import * as assert from 'assert'


//
// parsers (4)
//

assert.strictEqual(parseLocation('/'), home)
assert.strictEqual(parseLocation('/home'), home)
assert.deepEqual(parseLocation('/users/1'), user(1))
assert.deepEqual(parseLocation('/users/1/invoice/2'), invoice(1, 2))
assert.strictEqual(parseLocation('/foo'), notFound)

  1. 我們要做的第一件事是定義一個表示 API 端點的 sum 類型。在這種情況下,有四個可能的端點代表 Location .每個端點都是一個標記/區分的聯合,包含相應端點所需的數據。例如路線 /users/1 將被表示為符合 User 的對象 接口 const user1 = {_tag: 'User', id: 1}
    我們還為每個 Location 定義了輔助構造函數 .

  2. 現在我們將真正開始使用 Fp-ts-routing 的功能。對於每條路線,我們需要構建一個 Matcher 使用提供的組合器。每個matcher組合器的簡要說明:

    -> lit 匹配文字字符串值,例如lit('home') 匹配 /home

    -> int 匹配一個整數值並將其存儲在提供的字符串值中,例如int('userId') 匹配 '/10202' 解析器會返回 {userId: 10202} .

    -> then 允許我們將 Matchers 鏈接在一起,因此
    逐步為我們的路線構建解析器。例如我們
    希望我們的用戶路由為 /users/:id 我們可以為那個 const userIdMatch = lit('users').then(int('userId')) 構建一個匹配器

    -> end 只匹配路線 / 的結尾 .如果沒有像 lit('home') 這樣的組合匹配器 將匹配 '/home/otherstuff' 而不僅僅是 '/home'

  3. 一旦我們構建了匹配器,我們就可以將它們拼湊成一個路由器,該路由器會將路徑字符串解析為 Location .我們使用 alt 構造“空”(“零”)解析器後的方法,確保傳入總和類型(Location 在這種情況下),我們正在解析。 alt 方法接受 Parser 每個匹配器的實例並返回一個新的解析器,該解析器包含所有“替代”解析器的合併。

  4. 最後我們可以使用解析器和輔助函數將路徑字符串解析成我們的 Location 總和類型。

好的,很多。讓我們稍微談談為什麼這個看起來很簡單的代碼實際上非常強大。

類型安全

首先我們應該提到的是我們的 router 是類型安全的。也就是說,我們知道並且可以解釋 parseLocation 的所有可能狀態 函數將返回。查看簽名 parseLocation(s: string): Location 我們可以很容易地確定這個函數接受一個字符串並返回一個位置。鑑於 Location 是 sum 類型,我們知道所有可能的有效狀態。這使我們可以使用模式匹配來編寫非常簡單的控制邏輯。

例如,我們可能想用 Location 做一些事情 從路徑字符串解析的對象。我們可以在 _tag 上進行模式匹配 Location 的屬性 目的。 TypeScript 告訴我們 Location 的所有可能值 ! 通過以這種方式編寫代碼,我們減輕了跟踪控制邏輯中需要考慮的狀態這一巨大的容易出錯的負擔。

事實上,如果我們正確地建模了我們的領域,那麼只有在我們的 Location 中明確存在的狀態 sum-type 是可能的,這意味著根本不代表非法狀態。換句話說,我們的代碼保證是正確的。我不了解你,但這讓我對最終產品更有信心。

詳盡性檢查

通常,在為生產編寫代碼時,一個重要的考慮因素是理解你需要讓其他人(或“未來的你”)讀懂的內容。編寫類型安全的代碼使維護和代碼擴展更加容易。函數簽名和類型定義充當內置文檔,並將編譯器變成您自己的個人正確性檢查器。

一個例子是窮舉檢查。考慮上面的路由處理程序示例。如果我們不考慮所有 Location 會發生什麼 類型?我們可能會遇到意外行為或運行時錯誤,導致一切崩潰。但是由於我們已經使用 sum-types 對我們的 API 進行了建模,我們可以使用強大的窮舉檢查模式。

如果我們定義一個函數如下:

const assertExhaustive = (param: never) => {}

這個函數所做的只是接受 never 類型的參數 然後……什麼也不做。那是因為這個函數的存在只是為了保證我們的switch中的所有case 報表已入賬。觀看下面的 gif 以了解其工作原理。

通過添加 assertExhaustive 如果我們沒有處理可能的 Location 類型,我們強制編譯器通知我們 switch 語句的函數 .這是非常強大的,因為它可以確保中斷更改(例如添加新的端點位置)不會編譯,除非它們被顯式處理。 編譯器迫使我們編寫正確的代碼。

結論

到目前為止,我們已經介紹了 fp-ts-routing,我們已經了解瞭如何將我們的 API 域定義為 sum-type,並且我們理解了為什麼這允許我們使用編譯器來強制代碼正確性。在下一篇文章中,我們將討論 可組合性 以及我們的路由解析器的一元性質。我們將看到如何從 fp-ts/fp-ts-routing 為我們提供的簡單構建塊中構建一個類型安全且強大的路由器來支持我們的 AWS lambda 微服務。


Tutorial JavaScript 教程
  1. 函數式編程。

  2. SvelteKit:Scss 不起作用。

  3. 如何使用 RegExp 和 Object Evaluator 動態驗證表單輸入正則表達式 |表單驗證

  4. 禁用詳細信息/摘要

  5. 群組視頻聊天:ReactJS 和 WebRTC

  6. 了解 Vue.js nextTick

  7. 在下一個項目中使用 Redis 的 5 種方法

  1. javascript中的循環和迭代示例

  2. CanJS 2.3 發布

  3. JavaScript 可選鏈 `?.` 解釋 - 它是如何工作的以及何時使用它

  4. 引擎蓋下的 Node.js #1 - 了解我們的工具

  5. 創建您自己的自定義 JS 錯誤類型🤔

  6. 掃描數組js對象

  7. JavaScript 幻燈片 |帶有手動導航按鈕的響應式自動播放

  1. Array Reverse 對我不起作用……

  2. 從輸入字段讀取屬性時 HTML 編碼丟失

  3. CSS 變量與 ThemeContext

  4. 在 React 中創建一個聊天應用程序