2017年3月23日 星期四

React 整合 ASP.NET Web API

由於在工作上屬於全端開發(前後端都要寫),以下簡單分享自己整合 React 與 ASP.NET Web API 的過程。希望幫助有這方面需求的朋友。


前後端分離

現在 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/... 下的檔案,如果為實體檔案,則回應實體檔案

在您的server.js檔案中加入靜態檔案設定

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...)

在您的webpackDevServer設定中加入publicPathcontentBase兩個參數
,其中publicPath表示 Webpack 打包後,webpack-dev-server 會將資源輸出到 http://hostname/dist/... (e.g. http://hostname/dist/bundle.js)

var bundler = new webpackDevServer(compiler, {
 // 加上這兩行
 publicPath: '/dist/',
 contentBase: 'path/to/dist',
 ...
})


另外,為了日後專案完成,離開 webpack-dev-server 的環境時,依舊能保持資源路徑不變,publicPath也需要加到webpack.config.js的設定中。

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/... 下的檔案,如果為實體檔案,則回應實體檔案

首先我們需要在App_Start/RouteConfig.cs中,增加以下語法:

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")],這麼一來,所有 API 的請求都必須以 /api/... 為開頭呼叫。就像是以下這個範例:

[RoutePrefix("api")]
public class LoginController : ApiController
{
    [HttpPost]
    [Route("login")]
    public HttpResponseMessage login()
    {
        ...
    }
}



3. 其於 request 一律回應 dist 下的 index.html

首先我們必須將原先在App_Start\RouteConfig.cs中的routes.MapRoute改掉,就像是這樣:


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 }
    );   
}

其中,url被設置為匹配任何規則的{*url},也就是說除了可以被匹配到的 http://hostname/api/... 之外的 request 都會被導引到HomeController,此時我們必須加入一個純 ASP.NET MVC 的 Controller 來處理,而非使用 API 的 ApiController。
HomeController.cs就像是這樣:

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) 加上 Control-Allow-Origin 才可以被測試呼叫。一般撰寫 React 的開發人員會使用 Fetch API 或是 isomorphic-fetch 作為 AJAX 呼叫。後端必須配合 Fetch API 的規範,明確定義Control-Allow-Origin為何(不可為*),且Access-Control-Allow-Credentials必須設定為true
,下面提供一個一勞永逸的作法,您可以在Web.config檔案中加上 httpProtocol 的設定。(僅在測試時使用)

<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