文最初發表于STX Next博客網站,經原作者 Sebastian Buczyński 同意由 InfoQ 中文站翻譯分享。
現在,每個人都在關注 API。API 最早開始流行于大約 20 年前,2000 年,Roy Fielding 在他的博士論文中首次提出了 REST 這個術語。同年,Amazon、Salesforce 和 eBay 向全世界的開發者介紹了他們的 API,永遠改變了我們構建軟件的方式。
在 REST 之前,Roy Fielding 論文中的原則被稱為“HTTP 對象模型”,隨后你會明白這為何非常重要。
隨著閱讀的深入,你還會看到如何確定你的 API 是否成熟,好 API 的主要品質是什么以及為何在構建 API 的時候,要注重適應性。
REST 代表表述性狀態轉移(Representational State Transfer),由 Roy Fielding 在他的博士論文中定義,長期以來,它就是服務 API 的圣杯。它并不是構建 API 的唯一方式,但是由于它的流行,即便是非開發人員也知道這種標準。
RESTful 軟件有如下六種特點:
但是,對日常使用來說,這過于理論化了。我們需要更具操作性的東西,這也就是 API 成熟度模型。
該模型是由 Leonard Richardson 提出的,它將 RESTful 開發原則結合成四個簡單易行的步驟。
在模型中的位置越高,就越接近 Roy Fielding 所定義的 RESTful 原始理念。
Level 0 的 API 是一組簡單 XML 或 JSON 的描述。在前文中,我曾經說過在 Fielding 的論文之前,RESTful 原則被稱為“HTTP 對象模型”。
這是因為 HTTP 是 RESTful 開發中最重要的組成部分。REST 要盡可能多地使用 HTTP 固有屬性中的理念。
在 Level 0,沒有使用任何這樣的東西。我們只是構建自己的協議并把它作為一個專有層。這種架構被稱為遠程過程調用(Remote Procedure Call,RPC),適用于遠程過程/命令。
通常我們會有一個端點,可以對它進行調用以獲取一堆 XML。在這方面,一個典型的例子就是 SOAP 協議:
另外一個很好的例子就是 Slack API。它有些多樣化,有多個端點,但依然是 RPC 風格的 API。它暴露了 Slack 的各種功能,中間沒有附加任何特性。如下的代碼展示了如何向一個特定的通道發送消息:
雖然按照 Richardson 的模型,這是一個 Level 0 的 API,但是這并不意味著它是不好的。只要它是可用的,并且恰當地服務于業務需求,那它就是很棒的 API。
為了構建 Level 1 的 API,我們需要找出系統中的名詞并將它們通過不同的 URL 暴露出來,如下面的樣例所示:
其中,“/api/books”能讓我訪問一個通用的圖書目錄,“/api/profile”能夠讓我訪問這些書的作者的基本信息。為了獲取某個資源的第一個特定實例,我可以在 URL 中添加 ID(或其他引用)。
在 URL 中還可以嵌套資源,這展示了它們是以層級結構的形式組織的。
回到 Slack 的樣例,如下展示了按照 Level 1 API,它們會是什么樣子的:
現在,URL 發生了變化,從原先的“/api/chat.postMessage”變成了現在的“/api/channels/general/messages”。
信息中“channel”部分從請求體轉移到了 URL 中。從字面就能看出,通過使用這個 URL,我們可以預期有條消息發布到了“general”通道上。
Level 2 利用 HTTP 動作(verb)來添加更多的含義和意圖。在這方面可用的動作比較多,我這里只用到一個基礎的子集:PUT / DELETE / GET / POST。
借助這些動作,我們可以預期包含它們的 URL 有不同的行為:
以上面提到的“/api/books”為例:
那這里的“安全”和“冪等”又是什么意思呢?
“安全”的方法指的是永遠不會改變數據的方法。REST 建議 GET 方法只能用來獲取數據,所以在上面的集合中,它是唯一一個安全的方法。不管你調用多少次基于 REST 的 GET 方法,它永遠不會改變數據庫中的任何東西。但是,這并不是該動作的固有特性,而是關系到你該如何實現它,所以我們需要確保它是這樣運行的。所有其他的方法都會以不同的方式改變數據,不能隨意使用。在 REST 中,GET 方法既是安全的,又是冪等的。
“冪等”的方法指的是多次使用不會產生不同結果的方法。按照 REST,DELETE 方法應該是冪等的,如果刪除了某個資源,然后針對相同的資源再次調用 DELETE,它不會改變任何東西。資源應該早就已經消失了。在 REST 規范中,POST 是唯一一個非冪等的方法,所以我們可以對相同的資源多次調用 POST 方法,這樣我們會得到重復的資源。
我們重新看一下 Slack 樣例,如果我們使用 HTTP 動作來進行更多的操作會是什么樣子:
我們可以使用 POST 方法發送消息到通用的通道,我們也可以使用 GET 方法從通用通道獲取消息。我們還可以使用 DELETE 方法和特定的 ID 刪除消息,這里比較有意思的一點在于,消息并不是與特定通道關聯的,所以我可以設計一個單獨的 API 來刪除資源。這個例子表明,設計 API 并不總是那么簡單,這方面有很多可選項和權衡。
還記得純文字、沒有任何圖像的電腦游戲嗎?我們只能看到一些文本,描述了你在哪里,以及接下來能干什么。為了取得進展,我們必須要輸入自己的選擇。在一定程度上來講,HATEOAS 就是做這件事情的。
HATEOAS 指的是“超媒體作為應用狀態引擎(Hypermedia as the Engine of Application State)”。
有了 HATEOAS 之后,當其他人使用你的 API 的時候,他們就能看到通過 API 還能做哪些其他的事情。HATEOAS 回答了“從這里出發,我還能去哪里?”的問題。
但這還不是所有的內容。HATEOAS 還可以對數據關系進行建模。我們可能會有一個關于圖書的資源,并且在 URL 中沒有將作者信息嵌套進來,但是我們可以包含它們的鏈接,如果有人對作者感興趣的話,那么他們可以訪問這些鏈接并探索相關的數據。
HATEOAS 不像其他成熟度模型的等級那樣流行,但是有些開發人員確實在使用它。其中一個樣例就是 Jira,如下是它們的搜索 API 的響應:
他們將鏈接嵌入到了其他我們可以探索的資源中,以及該 issue 的狀態過渡列表。
另外一個使用 HATEOAS 的樣例是 Artsy。他們的 API 嚴重依賴 HATEOAS,并且還使用了 JSON Plus 調用規范,按照該規范強制要求使用一種特殊的約定來構建鏈接。下面是一個分頁的例子,這是使用 HATEOAS 最酷的樣例之一:
我們可以提供到下一頁、上一頁、第一頁和最后一頁的鏈接,還可以按照需要添加其他頁面的鏈接。這樣簡化了 API 的消費,因為這樣不需要在客戶端添加 URL 的解析邏輯,也不需要追加頁碼的方法。我們只需要在客戶端使用已經實現結構化的鏈接就可以了。
我們已經介紹完了 Richardson 模型,但這并不是實現好的 API 的全部內容。其他重要的品質還有什么呢?
我對自己使用的 API 的基本期望之一就是,需要有一種明確的方式來判斷是否有錯誤或異常。我想要知道請求是否得到了處理。
HTTP 有一種簡單的方式來實現這一點:HTTP 狀態碼。
管理狀態碼的基本規則是:
我們的 API 至少要提供 4xx 和 5xx 狀態碼。有時候,5xx 是自動生成的。例如,客戶端發送了一些內容到服務器端,但是這非法的請求,而我們的校驗是有缺陷的,從而導致這個問題繼續在代碼中執行了下去,最終導致出現了異常,這樣就會返回一個 5xx 的狀態碼。
如果你想要承諾使用特定的狀態碼,那么你會遇到“哪種狀態碼最適合當前情況?”的問題。這樣的問題并不總是那么容易回答,我推薦你去閱讀聲明這些狀態碼的 RFC,它們給出了比其他來源更廣泛的解釋,并且告訴了你何時使用這些狀態碼更合適等。幸運的是,網上有些資源可以幫助我們做出選擇,比如Mozilla的HTTP狀態碼指南。
優秀的 API 必須要有優秀的文檔。在文檔方面,最大的問題在于,隨著 API 的發展需要找人同步更新文檔。有個更好的方案是不脫離代碼自更新文檔。
例如,注釋與代碼的脫節。當代碼發生變化的時候,注釋依然保持不變,這樣的話,注釋就過時了。這甚至會比根本就沒有任何注釋更糟糕,因為在隨后的一段時間內,它們會提供錯誤的信息。注釋不會自動更新,所以開發人員需要記得在維護代碼的時候同時維護它們。
自更新的文檔工具可以解決這個問題。在這方面,一個流行的工具就是 Swagger,它是基于 OpenAPI 構建的工具,可以很容易地描述你的 API。
Swagger 很酷的一點在于它是可執行的,所以如果你嘗試修改 API,能立即看到它的作用和變化。
為了給 Swagger 添加自動更新功能,我們需要使用其他的插件和工具。在 Python 中,有針對大多數主流框架的插件。它們能生成 API 請求該如何組織的描述,并定義數據的輸入和輸出。
如果你不想要使用 Swagger,而是想使用更簡單的工具,那該怎么辦呢?有個流行的替代方案是Slate。
還有一些值得推薦的中間方案,如widdershins和api2html的組合,它允許我們從 Swagger 的定義中生成類似 Slate 的文檔。
在有些系統中,緩存可能并不是什么大問題。這樣的系統可能沒有很多的數據可供緩存,所有的數據都在不斷地發生變化,或者系統根本沒有很大的流量。
但是,在大多數情況下,緩存對于良好的性能至關重要。它與 RESTful API 密切相關,因為 HTTP 協議在緩存方面做了很多事情,比如 HTTP 頭信息允許我們控制緩存的行為。
你可能想要在客戶端緩存東西,或者如果有注冊表或值存儲的話,那么你可能想要在應用程序中緩存數據。但是,HTTP 讓我們能夠基本上免費就可以獲得一個很好的緩存,所以如果可能的話,請不要錯過這個免費的午餐。
同時,因為緩存是 HTTP 規范的一部分,所以很多涉及 HTTP 的技術都知道如何進行緩存:瀏覽器原生支持緩存,客戶端和服務器之間的中間技術也是如此。
構建 API 以及現代軟件最重要的部分就是適應性。如果沒有適應性,開發就會變慢,在合理的時間發布特性就會變得更加困難,當面對最后截止時間的時候更是如此。
“軟件架構”在不同的上下文語境中有不同的含義,不過我們現在采用這個定義:
軟件架構一種行為/藝術,能夠避免會阻礙未來變化的決策。
記住了這一點,在設計軟件的時候,當你必須要在具有相似優點的方案中做出選擇時,你應該始終選擇更多考慮到未來的方案。
好的實踐并不是萬能的。按照正確的方式構建錯誤的東西并不是你想要的結果。最好采取一種成長的心態,接受變化是不可避免的,尤其是如果你的項目要持續發展的話更是如此。
要想讓你的 API 更具適應性,其中很關鍵的一點就是保持盡可能薄的 API 層,真正的復雜性應該往下層轉移。
公開的 API 發布之后,它就已經完成了,是不可改變的,你就不能再去觸碰它了。如果你已經有了一個設計古怪的 API,除了接受現狀之外,還能做些什么呢?
你應該不斷尋找簡化實現的方法。有時候,你可以通過一個特定的 HTTP 頭信息來控制 API 響應的格式,相對于構建另外一個叫做 v2 的新 API,這是一種更簡單的解決方案。
API 只是另外一層的抽象。它們不應該決定如何實現,為了避免這種問題,我們可以采用如下幾種開發模式。
這是一種類似于門面的開發模式。如果你要把一個單體結構拆分為一組微服務,并且希望向外部暴露一些功能的話,那么你只需要構建一個類似門面的 API 網關。
它將為不同的微服務提供一個統一的接口(這些微服務可能有不同的 API,使用不同的錯誤格式等等)。
如果你必須要構建一個 API 來滿足一堆不同的客戶端的話,那么這可能會非常困難。針對某個客戶端所作出的決策可能會影響其他客戶端的功能。
按照適用于前端的后端(backend for frontend)理念,如果你有不同的客戶端,它們喜歡不同形式的 API,比如移動應用可能會喜歡使用 GraphQL,那么就單獨為它們構建吧。
只有當你的 API 是一層抽象,并且這個抽象層很薄的時候,這種方式才有效。如果它與你的數據庫耦合,或者太大,具有太多的邏輯,那么就無法這樣做了。
很多人都在熱炒 GraphQL。它是一項新興的技術,但是已經有了很多粉絲,以至于有些開發者聲稱它將取代 REST。
盡管 GraphQL 比 RESTful 要新的多,但是它們有很多相似之處。GraphQL 最大的不足之處在于它的緩存,它必須要在客戶端或應用程序中實現。現在,有內置的實現了緩存功能的客戶端庫(比如Apollo),但是這仍然要比使用 HTTP 提供的幾乎免費的緩存功能要困難。
從技術講,GraphQL 位于 Richardson 模型的 Level 0 層級,但是它具有良好 API 的特質。我們可能無法同時使用多個 HTTP 的功能,但是 GraphQL 的出現就是解決這一問題的。
GraphQL 的殺手锏就是聚合不同的 API,并將它們作為一個 GraphQL API 暴露出來。
GraphQL 在處理數據抓取不足和數據過量抓取方面有很好的效果,而這些問題是 REST API 很難進行管理的。這兩個問題都與性能有關,如果數據抓取不足,那說明你沒有高效地使用 API,所以必須要進行大量的調用。如果數據過量抓取的話,那么 API 調用的數據傳輸會比必要的數據傳輸更大,這是對帶寬的一種浪費。
借助 REST 與 GraphQL 的比較,我們能夠總結出一個好的 API 最重要的品質。
好的 API 的特性
我們需要一個清晰的數據表述方式:RESTful 以資源的方式提供了表述。我們需要有一種方式顯示有哪些可用的操作:RESTful 通過組合資源和 HTTP 動作實現這一點。我們需要有一種方式來確認是否存在錯誤/異常:HTTP 狀態碼可以實現這一點,可能還會包含闡述它們的響應信息。最好能夠提供 API 發現和導航的功能:在 RESTful 中,HATEOAS 負責實現這一點。有好的文檔是非常重要的:在這方面,可執行、自更新的文檔可以解決這個問題,這超出了 RESTful 規范的范圍。最后,但同樣重要的是,優秀的 API 應該具有緩存功能,除非你的特定情況認為它是不必要的。
REST 和 GraphQL 之間最大的區別是它們處理緩存性的方式。當我們使用 REST 方式構建 API 的時候,我們基本上可以免費獲得 HTTP 的緩存功能。如果選擇 GraphQL 的話,你需要自行負責為客戶端或應用程序添加緩存。
原文鏈接:
https://www.stxnext.com/blog/how-to-build-a-good-api-that-wont-embarrass-you
延伸閱讀:
關注我并轉發此篇文章,即可獲得學習資料~若想了解更多,也可移步InfoQ官網,獲取InfoQ最新資訊~
Excel VBA中可以引用Excel工作表函數,也可以引用Excel VBA函數,還可以引用用戶自己定義的函數或過程。除此以外,還可以調用Windows API。
Windows API包括幾千個可調用的函數,這些函數是Windows提供給應用程序與操作系統的接口,他們猶如“積木塊”一樣,可以搭建出各種界面豐富,功能靈活的應用程序。它們大致可以分為以下幾個大類:
基本服務; 組件服務; 用戶界面服務; 圖形多媒體服務; 消息和協作; 網絡; Web服務。
Windows API使用 .lib 文件。.lib 文件也就是庫文件,分為靜態庫文件和動態庫文件(DLL)
靜態鏈接就是把靜態鏈接庫中的函數直接復制到程序中,成為程序的一部分。
動態鏈接是指在程序運行時將已駐留在內存中的動態鏈接庫中的函數在需要時鏈接起來一起運行。
API 的 dll 在 windows 系統的 system32 目錄下, 圖形界面相關的 API 在 USER32.dll 里,進程、文件之類的操作在 kernel32.dll 里。MSDN 的每個函數都會說明它在哪個頭文件, 哪個 lib, 哪個 dll 里的。
動態鏈接庫中的函數可以被VBA調用,下面就以一個實例來說明。
在kernel32.dll中有一個 GetSystemDirectory()函數,可以到到Windows的系統路徑。以下就是通過一個過程來調用這個函數的實例:
Private Declare Function GetSystemDirectory Lib "kernel32" Alias "GetSystemDirectoryA" _ (ByVal lpBuffer As String, ByVal nSize As Long) As Long Sub info() Dim sPath As String * 260, lLen As Long lLen=GetSystemDirectory(sPath, 260) Text3=Left(sPath, lLen) VBA.MsgBox (Text3) End Sub
效果如下:
1.1查詢如何傳遞參數到DLL函數的Declare語句格式
為了調用Windows API中的函數,需要描述這些可用的函數的文檔規范,如何在VBA中聲明這些函數,以及如何調用它們。下面是兩個有用的資源:
I 可以使用API Viewer加載宏查找和復制需要的Declare語句。可以在下面的站點下載API聲明查看器:
http://www.activevb.de/rubriken/apiviewer/index-apiviewereng.html
登錄以下頁面后在以下點擊下載:
安裝后打開需要的文件:
即可查看可以使用的API及聲明:
II Microsoft Platform SDK,包含復雜的Windows API文檔。可以在下面的地址中查看:http://www.office-cn.net/t/api/index.html?apihelp.htm
可以使用的API函數類別:
1.2 使用Declare語句
在從VBA中調用DLL里的函數之前,必須為VBA提供在哪里找到函數以及如何調用該函數的信息:
Declare語句是一個定義,告訴VBA在哪里找到特定的DLL函數以及如何調用該函數。在代碼中添加Declare語句最簡單的辦法是使用API Viewer加載宏,其中包含Windows API中大多數函數的Declare語句,也包含一些函數所需要的常量和類型定義。
Declare語句聲明的形式如下:
[Public|Private]Declare Sub name Lib "libname" [Alias "aliasname"][([arglist])] [Public|Private]Declare Function name Lib "libname" [Alias "aliasname"] [([arglist])] [As type]
下面是GetTempPath函數的Declare語句的示例,該函數返回Windows臨時文件夾的路徑(默認為C:\Windows\Temp):
Private Declare Function GetTempPath Lib "kernel32" _ Alias "GetTempPathA" (ByVal nBufferLength As Long, _ ByVal lpBuffer As String) As Long
I 關鍵字Declare告訴VBA在工程中要包含的DLL函數的定義。在標準模塊中的Declare語句可以是公共的或私有的,取決于你希望API函數僅用于單個模塊還是整個工程。在類模塊中,Declare語句必須是私有的。
II 在關鍵字Function之后是函數的名字,具體地說,是從VBA中調用該函數時使用的名字。這個名字可以與API函數本身的名字相同,也可以在Declare語句中使用關鍵字Alias指定打算在VBA中通過不同的名字(別名)調用該函數。
III 在上面的示例中,在DLL中API函數的名字是GetTempPathA,從VBA中調用該函數時使用的名字是GetTempPath。注意,DLL函數的實際名字出現在關鍵字Alias之后,同時也注意到GetTempPath是Win32API.txt文件用于該函數的別名(有充分必要使用別名),但你可以將其改變為任何你想要的名字。
IV 關鍵字Lib指定包含函數的DLL。
1.3 在過程中調用API函數。
有如下任務:判斷一個文件框如果內容為空,則顯示一個提示文本框進行提示,并在3秒后自動消息。
我們知道,VBA提供以下代碼的提示對話框,但它需要用戶自己點擊關閉按鈕才可以關閉,不能滿足我們的需要。
MsgBox ("提示:編號不能為空!")
2.1 插入用戶窗體,并添加兩個文本框,如下:
2.2 在VBE中插入模塊,并添加以下代碼:
Private Declare Function MsgBoxTimeout Lib "user32" _ Alias "MessageBoxTimeoutA" ( _ ByVal hwnd As Long, _ ByVal lpText As String, _ ByVal lpCaption As String, _ ByVal wType As VbMsgBoxStyle, _ ByVal wlange As Long, _ ByVal dwTimeout As Long) _ As Long Sub PopupMsgbox(Optional prompt As String="OK", Optional title As String="友情提示", Optional seconds As Long=300) MsgBoxTimeOut 0, prompt, title, 64, 0, seconds End Sub
2.3 為文本框添加事件代碼:
Private Sub TextBox1_Exit(ByVal Cancel As MSForms.ReturnBoolean) If TextBox1.Value="" Then 'MsgBox ("提示:編號不能為空!") PopupMsgbox "編號不能為空!", "提示", 1500 '1.5秒后提示窗口自動關閉 End If End Sub
2.4 當文本框為空,鼠標光標點擊到文本框以外的其它位置時,就會彈出提示對話框,并在1.5秒后自動關閉,如下:
MsgBoxTimeout()是一個未公開的 API 函數。
此函數的參數如下:
總結上例使用API的流程:
I Declare MsgBoxTimeOut
II 定義PopupMsgbox過程,在此過程中調用了MsgBoxTimeOut
III 在其他過程中調用PopupMsgbox()
也可以聲明后直接調用:
Dim i As Long i=MsgBoxTimeout(0, "編號不能為空!", "提示", vbYesNo, 0, 1500)
-End-
束驗證 DOM 方法
Property | Description |
---|---|
checkValidity() | 如果 input 元素中的數據是合法的返回 true,否則返回 false。 |
setCustomValidity() | 設置 input 元素的 validationMessage 屬性,用于自定義錯誤提示信息的方法。使用 setCustomValidity 設置了自定義提示后,validity.customError 就會變成true,則 checkValidity 總是會返回false。如果要重新判斷需要取消自定義提示,方式如下:setCustomValidity('') setCustomValidity(null) setCustomValidity(undefined) |
以下實例如果輸入信息不合法,則返回錯誤信息:
checkValidity() 方法
<inputid="id1"type="number"min="100"max="300"required><buttononclick="myFunction()">驗證</button><pid="demo"></p><script>
functionmyFunction(){varinpObj=document.getElementById("id1"); if(inpObj.checkValidity()==false){document.getElementById("demo").innerHTML=inpObj.validationMessage; }}
</script>
*請認真填寫需求信息,我們會在24小時內與您取得聯系。