1. 程式人生 > >在 .NET Core 5 中整合 Create React app

在 .NET Core 5 中整合 Create React app

> 翻譯自 Camilo Reyes 2021年2月22日的文章 [《Integrate Create React app with .NET Core 5》](https://www.red-gate.com/simple-talk/dotnet/net-tools/integrate-create-react-app-with-net-core-5/) [^1] > > Camilo Reyes 演示瞭如何將 Create React app 與 .NET Core 整合,以生成一個移除了幾個依賴項的腳手架。 [^1]: Integrate Create React app with .NET Core 5 *Create React app* 是社群中建立一個全新 React 專案的首選方式。該工具生成了基礎的腳手架用於開始編寫程式碼,並抽象出了許多具有挑戰性的依賴項。webpack 和 Babel 之類的 React 工具被集中到一個單獨的依賴項中,使得 React 開發者可以專注於手頭的工作,這降低了構建*單頁應用*的必要門檻。 不過問題依然存在,雖然 React 解決了客戶端的問題,但服務端呢?.NET 開發者在使用 Razor、伺服器端配置,並通過 session cookie 處理 ASP.NET 使用者會話(session)方面有著悠久的歷史。在本文中,我將向您展示如何通過兩者之間的良好整合來實現兩全其美的效果。 本文提供了一種動手實踐的方式,因此您可以依照自上而下的順序,獲得更佳的閱讀效果。如果您更喜歡隨著程式碼學習,可以[從 GitHub 上獲取原始碼](https://github.com/beautifulcoder/integrate-dotnet-core-create-react-app.git)[^GitHub],使閱讀更愉快。 [^GitHub]:
示例程式碼 一般的解決方案涉及兩個主要部分——前端和後端。後端是一個典型的 ASP.NET MVC 應用,任何人都可以在 .NET Core 5 中啟動。請確保您已安裝 .NET Core 5,並將專案的目標設定為 .NET Core 5,只要執行了此操作也便開啟了 C# 9 特性。隨著整合的進行,我還將新增更多的部分。前端會有 React 專案和輸出像 *index.html* 之類靜態資產的 NPM 工具。我將假定您具有 .NET 和 React 的工作知識,因此我不會深究諸如在開發機上設定 .NET Core 或 Node 的基礎。也就是說,請注意下面一些有用的 using 語句,以便後面使用: ```csharp using Microsoft.AspNetCore.Http; using System.Net; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using System.Text.RegularExpressions; ``` ## 初始化專案設定 好訊息是,微軟提供了一個基礎的腳手架模板,用於啟動新的帶有 React 前端的 ASP.NET 專案。該 ASP.NET React 專案具有一個客戶端應用,它輸出可以託管在任何地方的靜態資產;以及一個 ASP.NET 後端應用,它可以通過呼叫 API Endpoints 獲取 JSON 資料。這裡的一個優點是,整個解決方案可以作為一個整體同時部署,而無需將前後兩端拆分成單獨的部署管道。 要安裝基礎的腳手架,請執行以下操作: ```bash mkdir integrate-dotnet-core-create-react-app cd integrate-dotnet-core-create-react-app dotnet new react --no-https dotnet new sln dotnet sln add integrate-dotnet-core-create-react-app.csproj ``` 有了這些,就可以在 Visual Studio 或 VS Code 中開啟解決方案檔案。您可以執行 `dotnet run` 來啟動專案,看看該腳手架都為您做了些什麼。請注意命令 `dotnet new react`,這是我用於該 React 專案的模板。 下面是初始模板的樣子: ![initial react template](https://img2020.cnblogs.com/blog/2074831/202103/2074831-20210325013244295-26240886.png) 如果您在使用 React 時遇到任何問題,只需將目錄更改為 `ClientApp` 並執行 `npm install`,即可啟動並執行 Create React App。整個 React 應用程式在客戶端渲染,而不需要伺服器渲染。它有一個具有三個不同頁面的 `react-router`:一個計數器、一個獲取天氣資料的頁面和一個主頁。如果您看一下控制器,會發現 `WeatherForecastController` 有一個 API Endpoint 來獲取天氣資料。 該腳手架已經包含了一個 Create React App 專案。為了驗證這一點,請開啟 *ClientApp* 資料夾中的 *package.json* 檔案進行檢查。 這就是它的證據: ```json { "scripts": { "start": "rimraf ./build && react-scripts start", "build": "react-scripts build", } } ``` 找到 *react-scripts*,這是像 webpack 一樣封裝所有其他 React 依賴項的單一依賴項。若要在將來升級 React 和它的依賴項,您只需升級這一依賴項。它抽象化了可能有潛在危險的升級以保持最新狀態,因此這才是 React App 的真正魔力。 *ClientApp* 中的整個資料夾結構遵循常規的 Create React App 專案,在其周圍包裹著 ASP.NET 專案。 資料夾結構如下所示: ![dotnet react app folder structure](https://img2020.cnblogs.com/blog/2074831/202103/2074831-20210325013343861-1626704129.png) 該 React 應用程式有很多優點,但是它缺少一些重要的 ASP.NET 功能: - 沒有通過 Razor 進行的服務端渲染,使任何其他 MVC 頁面像一個單獨的應用程式一樣工作 - 很難從 React 客戶端訪問 ASP.NET 服務端配置資料 - 它不會整合由 session cookie 實現的 ASP.NET 使用者會話 隨著整合的推進,我將逐一解決這些問題。好在這些理想的功能是可以使用 Create React App 和 ASP.NET 實現的。 為了跟蹤整合更改,我將使用 Git 提交初始腳手架。假設 Git 已安裝,請執行 `git init`、`git add` 和 `git commit` 來提交這個初始專案。檢視提交歷史是跟蹤整合所需更改的一種很好的方法。 現在,建立以下對此整合很有用的資料夾和檔案。我建議使用 Visual Studio 右鍵單擊建立控制器 、類或 View: - **/Controllers/HomeController.cs**:服務端主頁,它將覆蓋 Create React App 的 **index.html** 入口頁 - **/Views/Home/Index.cshtml**:Razor 檢視,它渲染服務端元件和來自 React 專案的解析過的 **index.html** - **/CreateReactAppViewModel.cs**:主要的整合檢視模型,它將抓取 **index.html** 靜態資產並將其解析出來以供 MVC 使用 有了這些資料夾和檔案後,請終止當前正在執行的應用程式,並通過 `dotnet watch run` 以監視模式啟動該應用程式。此命令跟蹤前端和後端的更改,甚至在需要時重新整理頁面。 其餘的必要更改將放入腳手架現有的檔案中。這好極了,因為可以最大限度地減少必要的程式碼調整來充實這個整合。 是時候擼起袖子,做個深呼吸,處理這個整合的主要部分了。 ## CreateReactAppViewModel 整合 我將從建立一個執行大部分整合工作的檢視模型開始。開啟 `CreateReactAppViewModel` 並放入以下程式碼: ```csharp public class CreateReactAppViewModel { private static readonly Regex _parser = new( @"(?.*)
\s*(?.*)", RegexOptions.IgnoreCase | RegexOptions.Singleline); public string HeadContent { get;set;} public string BodyContent { get;set;} public CreateReactAppViewModel(HttpContext context) { var request = WebRequest.Create( context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase + "/index.html"); var response = request.GetResponse(); var stream = response.GetResponseStream(); var reader = new StreamReader( stream ?? throw new InvalidOperationException( "The create-react-app build output could not be found in " + "/ClientApp/build. You probably need to run npm run build. " + "For local development, consider npm start.")); var htmlFileContent = reader.ReadToEnd(); var matches = _parser.Matches(htmlFileContent); if (matches.Count != 1) { throw new InvalidOperationException( "The create-react-app build output does not appear " + "to be a valid html file."); } var match = matches[0]; HeadContent = match.Groups["HeadContent"].Value; BodyContent = match.Groups["BodyContent"].Value; } } ``` 這段程式碼乍一看可能有點嚇人,但它只做了兩件事:從開發伺服器獲取輸出的 **index.html** 檔案,並解析出 `head` 和 `body` 標籤。這使得 ASP.NET 中的消費方應用程式可以訪問 HTML,該 HTML 連結到來自 Create React App 的靜態資產。這些資產是靜態檔案,其中包含帶有 JavaScript 和 CSS 的程式碼包。例如,JavaScript 包 *js\main.3549aedc.chunk.js* 或 CSS 包 *css\2.ed890e5e.chunk.css*。這就是在 React 中 webpack 接收所編寫的程式碼並將其呈現到瀏覽器的方式。 我選擇直接向開發伺服器發起一個 `WebRequest`,是因為在開發模式下,Create React App 不會生成 ASP.NET 可訪問的任何實際檔案。這對於本地開發來說足夠了,因為它可以與 webpack 開發伺服器很好地配合。客戶端上的任何更改都將自動更新到瀏覽器。在監視模式下進行的任何後端更改也會重新整理到瀏覽器。因此,您可以在兩全其美的環境中獲得最佳的生產力。
在生產環境中,將會通過 `npm run build` 建立靜態資產。我建議執行檔案 IO,並從 *ClientApp/build* 中的實際位置讀取 index 檔案。另外,在生產模式下,最好在靜態資產部署到託管伺服器之後快取該檔案的內容。 為了讓您有一個更好的概念,下面是一個 build 後的 **index.html** 檔案的樣子: ![built index.html looks like](https://img2020.cnblogs.com/blog/2074831/202103/2074831-20210325013422208-1098461995.png) 我高亮顯示了消費方 ASP.NET 應用需要解析的 `head` 和 `body` 標籤。有了這些原始的 HTML,剩下的就簡單多了。 檢視模型就緒後,就該花點時間處理 home 控制器了,它將覆蓋來自 React 的 *index.html*。 開啟 `HomeController` 並新增以下程式碼: ```csharp public class HomeController : Controller { public IActionResult Index() { var vm = new CreateReactAppViewModel(HttpContext); return View(vm); } } ``` 在 ASP.NET 中,該控制器是預設路由,它會在服務端渲染的支援下覆蓋 Create React App。這就是解鎖整合的訣竅,從而可以兩全其美。 接著,把下面的 Razor 程式碼放入 *Home/Index.cshtml* 中: ```html @model integrate_dotnet_core_create_react_app.CreateReactAppViewModel @Html.Raw(Model.HeadContent) @Html.Raw(Model.BodyContent)

Server-side rendering

``` React 應用程式使用 `react-router` 來定義客戶端的路由。如果在瀏覽器處於非 home 路由時重新整理頁面,它將恢復為靜態的 *index.html*。 要解決這種不一致性,請在 `Startup` 中定義下面的服務端路由,路由是在 `UseEndpoints` 中定義的: ```csharp endpoints.MapControllerRoute( "default", "{controller=Home}/{action=Index}"); endpoints.MapControllerRoute( "counter", "/counter", new { controller = "Home", action = "Index"}); endpoints.MapControllerRoute( "fetch-data", "/fetch-data", new { controller = "Home", action = "Index"}); ``` 此時,看一下瀏覽器,現在它將通過 **h2** 顯示這個服務端“元件”。這看起來似乎有點愚蠢,因為它只是在頁面上渲染的一些簡單 HTML,但其潛力是無窮的。ASP.NET Razor 頁面可以具有完整的應用程式外殼,其中包含選單、品牌和導航,它可以在多個 React 應用之間共享。如果有任何舊版 MVC Razor 頁面,這個閃亮的新 React 應用能夠無縫整合。 ## 伺服器端應用程式配置 接下來,假如我想顯示此應用上來自 ASP.NET 的服務端配置,比如 HTTP 協議、主機名和 base URL。我選擇這些主要是為了保持簡單,不過這些配置值可以來自任何地方,它們可以是 *appsettings.json* 設定,或者甚至可以是來自配置資料庫中的值。 要使服務端設定可以被 React 客戶端訪問,請將其放在 *Index.cshtml* 中: ```html

@Context.Request.Protocol @Context.Request.Scheme://@[email protected]

``` 這裡在全域性 `window` 瀏覽器物件中設定來自伺服器的任意配置值。React 應用可以輕而易舉地檢索這些值。我選擇在 Razor 中渲染這些相同的值,主要是為了演示它們與客戶端應用將看到的是相同的值。 在 React 中,開啟 *components\NavMenu.js* 並新增下面的程式碼段;其中大部分將放在 `Navbar` 中: ```js import { NavbarText } from 'reactstrap'; {window.SERVER_PROTOCOL} {window.SERVER_SCHEME}://{window.SERVER_HOST}{window.SERVER_PATH_BASE} ``` 這個客戶端應用現在將顯示通過全域性 `window` 物件設定的伺服器端配置。它不需要觸發 Ajax 請求來載入這些資料,也不需要以某種方式讓 `index.html` 靜態資產可以訪問它。 假如您使用了 Redux,這會變得更加容易,因為可以在應用程式初始化 store 時進行設定。初始化狀態值可以在客戶端渲染任何內容之前傳遞到 store 中。 例如: ```js const preloadedState = { config: { protocol: window.SERVER_PROTOCOL, scheme: window.SERVER_SCHEME, host: window.SERVER_HOST, pathBase: window.SERVER_PATH_BASE } }; const store = createStore(reducers, preloadedState, applyMiddleware(...middleware)); ``` 為了簡潔起見,我選擇不使用 Redux store,而是通過 `window` 物件的方式實現,這只是一個粗略的想法。這種方法的好處是,整個應用都可以保持單元可測試的狀態,而不會受到類似 `window` 物件的瀏覽器依賴項的汙染。 ## .NET Core 使用者會話整合 最後,作為主菜,現在我將這個 React 應用與 ASP.NET 使用者會話(Session)整合在一起。我將鎖定獲取天氣資料的後端 API,並僅在使用有效會話時顯示此資訊。這意味著當瀏覽器觸發 Ajax 請求時,它必須包含一個 ASP.NET session cookie。否則,該請求將被拒絕,並重定向以指示瀏覽器必須先登入。 要在 ASP.NET 中啟用使用者會話支援,請開啟 *Startup* 檔案並新增: ```csharp public void ConfigureServices(IServiceCollection services) { services .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.Cookie.HttpOnly = true; }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // 將下面程式碼放在 UseRouting 和 UseEndpoints 之間 app.UseAuthentication(); app.UseAuthorization(); } ``` 請務必保留其餘的腳手架程式碼,只是在恰當的方法中新增上面的程式碼段。啟用了身份驗證和授權後,轉到 `WeatherForecastController` 並給該控制器新增一個 `Authorize` 屬性。這將有效地將其鎖定,從而需要由 cookie 實現的 ASP.NET 使用者會話來獲取資料。 `Authorize` 屬性假定使用者可以登入到該應用。回到 `HomeController` 並新增 Login 和 Logout 方法,記得新增 using `Microsoft.AspNetCore.Authentication`、`Microsoft.AspNetCore.Authentication.Cookies` 和 `Microsft.AspNetCore.Mvc`。 這是建立然後終止使用者會話的一種方法: ```csharp public async Task Login() { var userId = Guid.NewGuid().ToString(); var claims = new List { new(ClaimTypes.Name, userId) }; var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var authProperties = new AuthenticationProperties(); await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties); return RedirectToAction("Index"); } public async Task Logout() { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return RedirectToAction("Index"); } ``` 請注意,通常使用重定向和 ASP.NET session cookie 來建立使用者會話。我添加了一個 `ClaimsPrincipal`,它帶有一個設定為隨機 **Guid** 的使用者 ID,使其看起來更加真實。[^Claims] 在實際應用中,這些 Claims 可能來自資料庫或者 JWT。 [^Claims]: 用 Cookie 代表一個通過驗證的主體,它包含 Claims, ClaimsIdentity, ClaimsPrincipal 三部分資訊,其中 ClaimsPrincipal 相當於持有證件的人,ClaimsIdentity 就是持有的證件,Claims 是證件上的資訊。 要將此功能公開給客戶端,請開啟 *components\NavMenu.js* 並將下面的連結新增到 `Navbar`: ```xml Log In Log Out ``` 最後,我希望客戶端應用處理請求失敗的情況,並給終端使用者提供一些提示,指出出了點問題。開啟 *components\FetchData.js* 並用下面的程式碼段替換 `populateWeatherData`: ```js async populateWeatherData() { try { const response = await fetch( 'weatherforecast', { redirect: 'error' }); const data = await response.json(); this.setState({ forecasts: data, loading: false }); } catch { this.setState({ forecasts: [{ date: 'Unable to get weather forecast' }], loading: false }); } } ``` 我調整了一下 `fetch`,以使它不會用重定向跟蹤失敗的請求,而是返回一個錯誤響應。當 Ajax 請求獲取資料失敗時,ASP.NET 中介軟體將以重定向到登入頁的方式響應。在實際的應用中,我建議將其自定義為 401 (Unauthorized) 狀態碼,以便客戶端可以更優雅地處理此問題;或者,設定某種方式來輪詢後端並檢查活動會話,然後通過 `window.location` 進行相應地重定向。 完成後,dotnet 監視程式應該會在重新整理瀏覽器時跟蹤前後兩端的更改。為了進行測試,我將首先訪問 Fetch Data 頁,請注意會請求失敗,然後登入,並使用有效的會話再次嘗試獲取天氣資料。我將開啟“Network”選項卡,以在瀏覽器中顯示 Ajax 請求。 ![ajax request with valid session](https://img2020.cnblogs.com/blog/2074831/202103/2074831-20210325013501378-572527028.png) 請注意當我第一次獲取天氣資料時的 302 重定向,它失敗了。接著,來自登入頁的後續重定向建立了一個會話。檢視一下瀏覽器的 cookies,會顯示這個名為 `AspNetCore.Cookies` 的 cookie,它是一個 session cookie,正是它讓後面的 Ajax 請求工作正常了。 ## 結論 .NET Core 5 和 React 不必獨立存在。通過出色的整合,便可以在 React 中解鎖服務端渲染、服務端配置資料和 ASP.NET 使用者會話。
> 作者 : Camilo Reyes > 譯者 : 技術譯民 > 出品 : [技術譯站](https://ittranslator.cn/) > 連結 : [英文原文](https://www.red-gate.com/simple-talk/dotnet/net-tools/integrate-create-react-app-with-net-c