前後端分離
現在 Web 工程可以說日漸複雜,有許多大型系統為了使開發人員專注於改善客戶體驗與嚴謹的商業邏輯,專業的前後端分工已經是趨勢。本章不介紹兩者工作的細節或差異,重點會放在前後端整合的問題上,並使用前端最紅的函式庫 React.js 與強大的後端 ASP.NET Web API。
以下介紹,會假設您已經對這兩套軟體有初步的認識。
在整合之前,我們必須要有清楚的認知「.NET 與 React 都擁有各自完整的生態系」,如果整合的工作需要徹底的改變他們彼此的開發習慣或流程,不僅成本過高更是是緣木求魚。該如何以影響最低的前提下整合,是成敗的關鍵。
Webpack
首先我以前端的角度切入,大多數開發 React 的專案都會使用 Webpack 作為網站的打包工具。
通常會搭配 webpack-dev-server 作為暫時運行的 web server。藉由這個環境我們思考一下幾個問題:
- webpack 打包後輸出成靜態的成品的檔案路徑
- 使用 reacte-router 必須處理 server 的 router 規則與行為
- 測試使用 webpack-dev-server 但成品部署到 IIS 不需要改 code
由於是前後端分離的架構,後端完全不需要進行渲染 html 等工作,僅需要將資料 (JSON) 輸出即可,因此只要後端準備一個跟 webpack-dev-server 一樣的環境,就可以無痛整合。
前後端協議
為了達成這個目的,前後端必須先將規則與需求定義清楚,前後端在共同遵守這個需求即可。
以下協議為參考範例,您可以視專案狀況調整
- 在 http://hostname/dist/... 下的檔案,如果為實體檔案,則回應實體檔案
- webpack 輸出的資源統一放在 http://hostname/dist/ 下 (e.g. bundle.js, xxx.png...)
- API 的請求 URL 統一以 "api" 為 prefix (e.g. http://hostname/api/book, http://hostname/api/book/10...)
- 其於 request 一律回應 dist 下的 index.html
(假設您有 SPA 的需求,譬如使用 React-router 作為解決方案 e.g. http://hostname/home)
在這個協議下,前端開發人員可以依照原先的開發環境,且不影響後端人員。等到開發完成或告一段落後,使用 webpack 輸出到後端指定的 dist 資料夾下,就可以無痛整合。(前端開發與測試時使用 webpack-dev-server 跑 web,完成後用 IIS 跑 web)
Route 規則
以下用幾個案例說明以上規範實際的樣子:
- http://hostname/api/login (API 回應)
- http://hostname/api/book/10 (API 回應)
- http://hostname/dist/bundle.js (回應 js 檔案)
- http://hostname/dist/pic.jpg (回應 jpg 檔案)
- http://hostname/abc (回應 index.html 檔案)
- http://hostname/abc/cde/fgh (回應 index.html 檔案)
- http://hostname/ (回應 index.html 檔案)
修改 webpack-dev-server 設定
1. 在 http://hostname/dist/... 下的檔案,如果為實體檔案,則回應實體檔案
在您的
var bundler = new webpackDevServer(compiler, { ... hot: true, stats: { colors: true } }) // 加入下面這一行 bundler.app.use('/dist',express.static('path/to/dist'));
2. webpack 輸出的資源統一放在 http://hostname/dist/ 下 (e.g. bundle.js, xxx.png...)
在您的
,其中
var bundler = new webpackDevServer(compiler, { // 加上這兩行 publicPath: '/dist/', contentBase: 'path/to/dist', ... })
另外,為了日後專案完成,離開 webpack-dev-server 的環境時,依舊能保持資源路徑不變,
module.exports = { entry: { app: ['./app/index.jsx'] }, output: { filename: 'bundle.js', path: 'path/to/dist', // 加上下面這一行 publicPath: '/dist/' }, ... }
3. 其於 request 一律回應 dist 下的 index.html
為了日後使用 react-router 製作 SPA 的應用,這邊我們需要加入一個 route 規則,將所有的 request 導向給 dist 下的 index.html
bundler.app.use('/dist',express.static('path/to/dist')); // 加入下面這一段規則 bundler.app.get(/^\/[-\w\/]*$/,(req,res)=>{ res.sendFile('path/to/dist/index.html')) })
使用 react-router 這類的套件時,所有資源請求都會從根目錄發起,以確保使用絕對路徑。e.g. <script src="/dist/bundle.js"></script> ,或會增加 <base> 標籤來明確定義。
修改 ASP.NET Web API 設定
前端修改完了,接下來也要讓後端能符合前端的運作環境。前端只需要將 webpack 打包好的成品輸出到指定的目錄下,就可以運作順暢。
注意:下面看到的網址均為 IIS Server 的環境,非 webpack-dev-server
1. 在 http://hostname/dist/... 下的檔案,如果為實體檔案,則回應實體檔案
首先我們需要在
public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true; routes.IgnoreRoute("dist/{*file}"); ... }
如此一來,當 Request 為 http://hostname/dist/... 就不會進入任何 Router Controller ,整個專案的目錄與檔案會轉交給 IIS 進行解析,如果專案目錄下的 dist 目錄下也存在檔案,就會回應該檔案。
換句話說,前端人員就必須將 webpack 打包好的結果輸出到專案目錄下的 /dist 目錄。
2. API 的請求 URL 統一以 "api" 為 prefix
我們只需要在每一個 API Controller 類別加上
[RoutePrefix("api")] public class LoginController : ApiController { [HttpPost] [Route("login")] public HttpResponseMessage login() { ... } }
3. 其於 request 一律回應 dist 下的 index.html
首先我們必須將原先在
public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true; routes.IgnoreRoute("Dist/{*file}"); routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); // 把 route 規則改成這樣 routes.MapRoute( name: "Default", url: "{*url}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); }
其中,
public class HomeController : Controller { private const string INDEX_PATH = @"~\dist\index.html"; public ActionResult Index() { string path = Server.MapPath(INDEX_PATH); if (System.IO.File.Exists(path)) return Content(System.IO.File.ReadAllText(path)); else return new HttpNotFoundResult(); } }
其中,我們會去讀取 dist\index.html 檔案,作為輸出。如果該檔案不存在,就回傳錯誤碼 404 Not Found。
結論
在這個架構下,原先撰寫前端程式的開發人員,可以繼續使用他熟悉的工作流程繼續開發。後端人員也不需要修改任何邏輯或開發流程,API 也可以照舊繼續開發,且互相不會影響。
在實際運作的狀況下,後端人員不需要一定把 API 撰寫好前端才可以工作,雙方僅需要將 API 的服務、回應的格式、錯誤狀態碼等規範定義清楚,雙方就可以同步進行。待 API 開發完畢後,前端再將 API 串回實際的後端即可。且可以部屬在同一台 Server 下,而不是 Node.js 與 IIS 兩個 Web Server。
前端人員串回後端 OK 後,就可以使用 webpack 將結果輸出到後端指定的 dist 目錄下,改以 IIS 作為正式服務。
其他問題
在開發或測試時,API與前端還是分開運作的,呼叫後端API屬於跨域存取。後端 API 必須在標頭 (Header) 加上
,下面提供一個一勞永逸的作法,您可以在
<system.webServer> ... <httpProtocol> <customHeaders> <add name="Access-Control-Allow-Origin" value="http://your_front_end_test_server" /> <add name="Access-Control-Allow-Methods" value="GET,POST,PUT,DELETE,OPTIONS" /> <add name="Access-Control-Allow-Credentials" value="true"/> </customHeaders> </httpProtocol> </system.webServer>
如此一來,所有 API 回應結果時,都會在 Header 被加上以上三個設定。
Reference
- Enabling Cross-Origin Requests in ASP.NET Web API 2
- Asp.Net WebApi2 Enable CORS
- WEBPACK DEV SERVER
- Support webpack-dev-server historyApiFallback
- Route all Web API requests to one controller method
- ASP.NET MVC 開發心得分享 (21):Routing 觀念與技巧