asp.net Web開發框架 (2) - 基本SPA架構


前面
我們談過了非技術的背景原因之後,我們在這篇會正式開始跟各位介紹我這幾年在團隊中最常使用的開發架構。在大部分(超過九成五)的Web開發情境下,最近幾年我們多半採用SPA(Single Page Application)架構。這邊我們先定義一件事情,有些人覺得(認定)SPA就一定只有一個html page,整個應用程式就都在這個page當中運行,所有的功能都在這個頁面上呈現,所以也沒有換頁這件事,但,對我們來說並不是這樣的。

SPA概念在.NET或非.NET陣營上都有著不同的定義,但對於我們團隊而言,其重點在於『沒有從伺服器端動態Render HTML到前端』的這個動作,伺服器端與前端Browser之間的交互,只有幾個行為:

  1. 下載靜態HTML Pages(大多時候都不只一頁)
  2. Client(Browser)與Server(WebAPI)之間的Http Call與JSON傳遞

前端頁面也幾乎沒有Form Submit這個動作,取而代之的是AJAX非同步呼叫。

that’s all.

這樣的規範(你要說是限制也行)讓開發架構變得非常單純,應用程式很自然地分成了三層:

  1. Presentation Layer(view),採用HTML+javaScript , 運行於用戶端Browser
  2. Service Layer,採用asp.net WebAPI或PageMethods,運行於伺服器端IIS
  3. Business Logic Layer,採用C#寫成的class,運行於伺服器端Application Server或IIS(和Service Layer佈署在一起)

為何不用任何『從伺服器端動態Render HTML到前端』的機制?

伺服器端只對前端傳遞JSON與靜態HTML(而非透過C#程式碼動態生成的HTML),這是一種限制,但這個限制利大於弊,所有透過伺服器端往前端Render HTML的技術(ASP、PHP、ASP.NET WebForms、ASP.NET MVC View),一不小心都很容造成相依性、並且導致重用性降低,因為開發人員會忍不住把Business Logic寫在其實是Render View的展示層程式碼中(如果你曾經在MVC的Controller或是WebForm的Page裡面寫存取資料庫或撰寫運算的code,那就是了)。你很難在上述狀況下誘導開發人員把前後端抽離,讓程式碼的職責清晰。這在只有一個人開發時可能不會覺得有什麼問題,但在團隊開發的時候,這導致分工、維護、交接都很困難。且如此一來,要能達成如同SPA架構一樣,讓Business Logic和Service可以被任何種類的前端重用(例如行動裝置、Desktop Application…etc),也近乎不可能。

同時,動態Render到前端的HTML是一個黑箱,我們不知道那些HTML Code怎麼產生的(嚴格說起來不是真的不知道,而是無法由自己掌控),在開發階段也很難看到預覽的效果(你非得有個伺服器跑起來才看的到效果)。另外一個問題是設計師很難與程式開發人員互動,一旦你把設計師設計好的純html+css改成razor view或是asp.net WebForm(或是更傳統的PHP/JSP/ASP),基本上這個檔案別想讓設計師再維護了。綜合了許許多多的原因,我們的Web專案幾乎讓大部分的html page都保持原始(設計師能夠持續維護)的狀態,前端永遠只是加上javascript,便於與WebAPI所撰寫的Service Layer互動,透過javascript把後端傳遞來的JSON資料呈現在頁面上。

因此,要持守這個架構,採用SPA對我們來說是最理想的選擇。此外前面提過另一個優點,當我們透過SPA開發,很自然地變成了View <--> Service <--> Business Logic這樣的結構,在現今動不動客戶就要Mobile解決方案的狀況下,我們可以輕易地把View換成Native App,對於後端核心邏輯來說,完全不需要調整。

如何輕鬆地使用SPA開發Web應用程式

既然是團隊開發,我們當然要讓開發人員可以很容易的follow這個開發架構,因此我們透過Nuget來簡化使用此開發框架的流程,現在,開發人員只要建立一個最基本的asp.net Web Application,並且引入isRock.Framework.Web套件即可(是的,我們將其公開在Nuget上了,因此讀者也可以使用),步驟如下:

  1. 建立最基本(Empty)的Web Application(別忘了至少要勾選WebAPI)
  2. 建立好的專案如下,然後請加入 bootstrap、jQuery、Vue、isRock.Framework.Web套件(重點當然是最後這個,其他幾個是順便。或是你也可以直接引入一個Nuget套件isRock.Framework.Web.AllPackages,它會幫你引入其他所有需要的Nuget套):
  3. 套件都安裝完之後,我們打開自動幫我們產生的ExampleController…

    這是一個已經寫好的樣板WebAPI,它的功能是擔任Service Layer,被前端的JavaScript以AJAX方式呼叫,它吐回前端的Always是一組JSON。

    請留意它的Route,呼叫的方式會是 api/Example/{MethodName} ,這什麼意思呢? 請先看它的Code,在第12-15行的部分,assemblyLauncher.ExecuteCommand是我們的重點,這個類別可以幫我們運行特定的Business Logic Component。程式碼中的TestClassA當然就是我們示範用的Business Logic Component。

    這一段需要解釋一下。為什麼這麼設計呢?

    首先,這個開發架構很清楚地把Business Logic Component與Services Layer分開,因此每一隻WebAPI的APIController,在設計上,我們都希望它代表著一塊模組(或一組功能,或一組API),而WebAPI只是一層Service介面,實際上系統核心功能的實現,是透過後面的Business Logic Component,也就是上面程式碼中的TestClassA。所以,你會看到我們的ExampleController,設計成讓呼叫端(JavaScript)可以直接要求某一個Method,然後該WebAPI就幫我們去執行對應的Business Logic Component(TestClassA)中的該Method,取得回傳值之後,再透過WebAPI以JSON的形式回傳給前端的JavaScript。

    知道這個概念之後,您就不難理解,上面的程式碼主要是透過WebAPI撰寫的Service Layer,可以透過assemblyLauncher.ExecuteCommand運行到我們的Business Logic Component(範例中的TestClassA)。接著,我們來看,Presentation(View)的部分。
  4. 請在專案中加入一個index.html,並且撰寫底下內容(這是一個從身高體重計算BMI的範例):

     
  5. 上面這段html很簡單,就是輸入身高體重的UI,我們讓用戶輸入身高體重,透過後端的Business Logic算出BMI值。當然,後端也應該要有一個計算身高體重的類別,因為我們在專案中建立一個BO資料夾,在其中撰寫一個類別如下(其實,更嚴格的作法是將BO獨立撰寫成一個專案,而非放在同一個專案中,這部分容後再敘)。注意BMI方法傳入身高體重當作參數,計算出BMI值(體重kg/身高m的平方)回傳:
  6. 你可能發現了上面這個類別繼承自BusinessLogicBase,這有很多用途,以後我們再解釋,請留意我們做了一個計算BMI值的Method(名稱為BMI),有一個傳入參數(bodyInfo物件),其回傳型別是ExecuteCommandDefaultResult ,另外請注意我們把核心運算(以後包含資料庫存取)都撰寫在這個類別中,最後真正的計算結果我們透過ExecuteCommandDefaultResult 的Data屬性回傳。
    當然,如此一來,我們在Service Layer(WebAPI)那邊也需要跟著做一些調整(把原本的測試用BO(TestClassA)換成正式的BO:

    注意我們把原本測試的TestClassA換成了真正的Business Logic Component (BO.Health),這樣一來,當我們從用戶端透過javaScript呼叫 /api/Example/BMI 時,就會運行到BO.Health這個類別的BMI方法。
  7. 最後,我們再調整一下前端的Code,加入JavaScript(同樣的,理想狀況應該是獨立的.js檔案,這是範例為求單純因此我們塞在HTML頁面中),讓靜態的HTML可以和WebAPI撰寫的Service Layer互動:
    我們加入了JavaScript來抓取用戶在頁面上輸入的值(14行),並且透過ExecuteAPI這個由isRock.Framework.Web框架提供的JavaScript function來呼叫伺服器端的WebAPI(16行傳入的參數表示要呼叫ExampleController所對應的類別的BMI Method),並且將參數para傳遞到伺服器端。伺服器端的BO(Health.BMI)會自動被執行(前端的Para參數會自動被帶入BMI Method的bodyInfo參數),執行後會把回傳值回傳到前端,透過前端18行的alert顯示出來…
  8. 執行結果如下:

請留意,在這個架構當中,擔任Service Layer的WebAPI,我們要求開發人員,不得在其中存取資料庫,當然也不得在其中撰寫Business Logic,它只有一個任務,就是呼叫Business Logic Component,並且pass相關的參數(未來Service Layer還會擴充成負責安全控管、權限、Logging、例外處理…等與商業邏輯沒有直接關係的infrastucture 機制,但目前我們先不談這部分)。所以你會發現,所有核心的運算與資料庫存取,應該透過Business Logic Components(也就是C#寫成的Class)來開發與運行。

這使得整個架構中核心的部分幾乎都在Business Logic(BO),由於這部分是很單純的Method,因此也可以輕易地進行Unit Test或TDD,而Services Layer的部分,在專案開發過程中,我們只讓Project/Team Leader做必要的調整(一般來說幾乎不改寫,只會依照系統模塊相對應的增加APIController),如此一來,當架構確定之後,前後端開發人員都專注於撰寫各自的程式碼,前端是View(JavaScript/HTML)、後端是Business Logic(C#),責任歸屬非常明確,每一個開發方式和風格也都相同,後續的重用性和維護便利性也大幅提升。

同時,你會發現View的撰寫永遠是HTML/CSS/JavaScript,設計師不管做任何調整,開發人員都可以接手繼續進行開發,反之亦同。而切割開來的Services Layer(WebAPI),可以給任何異質系統呼叫,不管是行動裝置,桌面應用,或是其他平台的API,都可輕易透過標準的HTTP+JSON進行API的調用。而後端的BO(Business Logic Component)則被保護得很好,並維持單純且統一的撰寫方式。在BO的部分我們一般要求要進行Unit Test,確保核心邏輯的正確性。

前面說過,這樣的架構,含括了近九成五以上的Web開發專案,長期下來再也沒有混雜不堪的程式碼,同時交接與維運或程式碼的重用性都大幅提高。

上面只是一個很簡單的範例,後面我們會繼續介紹,如果面對更複雜的需求該如何開發。

後記 : 千萬別以為,我們團隊這樣做,就表示我們覺得只能這樣做,解決方案只有當下的最適解,並沒有永遠的最佳解,時空不同往往答案就不同,團隊或客戶種種因素都可能影響架構師或開發人員的判斷依據。(白話:以上架構僅供參考)

source code :
https://github.com/isdaviddong/isRockWebFxBasicSample

--------------------------------------------
上一篇: asp.net Web開發框架 (1) - 天下武功,唯快不破
下一篇: asp.net Web開發框架 (3) - 如何讓asp.net WebForms也能搭配Vue.js和bootstrap並享有SPA開發架構?

留言

匿名表示…
感謝老師分享WebForm跟WebAPI協同運作的架構,另外想要請教的是如果是採用MVC架構,那麼是否有類似SPA的結構可以參考?感覺導向ViewPage就已經要把所有的準備資料打到前端了,是否有可能在這個中間的過程截斷之類的,改由前端回CALL某網址的JSON,若有解法還請老師指教。
isDavid寫道…
微軟的MVC本身就支援SPA,但微軟官方的SPA和我在意的點不同,如果你對MVC做SPA有興趣,可以參考:
https://www.asp.net/single-page-application/videos/pluralsight-single-page-apps-jumpstart
匿名表示…
您好,我之前試用isRock框架的WebAPI方式Demo,用vs調試運行沒有問題,但是我發佈到IIS時調用API時就會報錯誤:IIS10.0 詳細錯誤 -404.0 - Not Found,不知何解?
isDavid寫道…
Jacky Lam,

很難評估,資訊不夠。
但如果要猜的話,我會覺得可能跟你的routing或Web Api的呼叫有關...因為整個框架的基礎建立在WebAPI上...

你可以試著google 『web api 404.0 - Not Found』...
應該有很多資訊
Rick表示…
請問老師,這種方式似乎沒辦法自動產生postman或者是swagger UI API文件
請問實務上都怎麼跟前端協作溝通這一塊呢 ?
isDavid寫道…
@rick,

兩者角度不同。

之所以能用postman或者是swagger 產生API文件時,是意味著Services Layer本身就是提供對外呼叫的標準介面,這時WebAPI走標準的Restful很合理,因此postman或者是swagger 可以假設API符合restful格式,然後產生對應的文檔。

但本文中的狀況,其實是抽掉了這層服務層(或是把服務層視為單一規格),然後讓前端直接藉由標準的服務層對BO溝通,在這個情境裡,不需要服務層的API list,需要的是對外公開的BO的Method List,基本上用任何類似sandcastle 之類的工具,列舉出BO的methods即可。
Unknown寫道…
老師您好 小弟有個問題vuejs 如何能加入npm plugin 給vue.js 使用呢 是否能指個方向 萬分感謝!
Unknown寫道…
不好意思 忘了說 是給webform 用的 感謝!
Richer寫道…
那個...關於佈署到 IIS 後, 前端 JavaScript 在呼叫 API 時, 會出現 404 的錯誤, 其實我也有遇到; 後來發現是 ExecuteAPI 裡 $.ajax 的 url 參數, 預設即為 /api 的緣因, 如果佈署到 IIS 時, 程式佈署的所在的位置, 不是在 IIS 的 default web site 下, 而是底下的子網站時, 就會形成 url 是錯誤的情形...
感謝 David 老師無私的分享

這個網誌中的熱門文章

使用LM Studio輕鬆在本地端以API呼叫大語言模型(LLM)

VS Code的字體大小

使用 Dify 建立企業請假機器人

使用Qdrant向量資料庫實作語意相似度比對

使用C#開發LineBot(3) - 使用LineBotSDK發送Line訊息