--- robots: noindex, nofollow tags: React --- # 在 ASP.NET MVC 中使用 React 元件 (1) 決定要逐步換用 React 元件後,第一件會想到的事情是靠 webpack [設定多個 entry points 的功能][multi-page-application],來為現存頁面提供個別的 js 檔,並將共用的 library 放到 vendor.js 中。 本想靠 [react-app-rewired][react-app-rewired] 覆寫 [create-react-app][create-react-app](CRA) 的 webpack 設定檔,但 CRA 做了太多我們現在還不需要做的事情(例如分割打包 js 後自動加上避免 browser cache 的 hash 、自動處理影像)。 js 和影像目前還是由 ASP.NET 處理,所以還是選擇自己操作 webpack 。再將打包好的 js 交給 [ReactJS.NET][reactjs-dotnet] 使用。 [multi-page-application]: https://webpack.js.org/concepts/entry-points/#multi-page-application [react-app-rewired]: https://www.npmjs.com/package/react-app-rewired [create-react-app]: https://create-react-app.dev/ [reactjs-dotnet]: https://reactjs.net/ ## ReactJS.NET ### 安裝 要使用 ReactJS.NET 得先從 NuGet 安裝: * `React.Core` * `React.Web` * `React.Web.Mvc4` * `System.Web.Optimization.React` 為了使用它提供的 server-side rendering 功能(它會使用指定的 js engine ,在繪製 view 時,執行你提供的 script ),還得裝: * `JavaScriptEngineSwitcher.Core` 再安裝某個 js engine (這次我選了 V8 ,也有人用 Chakra ): * `JavaScriptEngineSwitcher.V8` * `JavaScriptEngineSwitcher.V8.Native.win-x64` * `JavaScriptEngineSwitcher.V8.Native.win-x86` (看團隊的開發環境決定要不要 x64, x86 兩種架構都裝,記得把 IIS 切到對應的架構,不然 ReactJS.NET 跑不起來) 要注意的是,某些套件(例如 `System.Web.Optimization.React` )會用到舊版的 `JavaScriptEngineSwitcher.Core` ,所以得在 `Web.config` 中 remap 舊版至新版: ```xml= <dependentAssembly> <assemblyIdentity name="JavaScriptEngineSwitcher.Core" publicKeyToken="c608b2a8cc9e4472" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-3.3.0.0" newVersion="3.3.0.0" /> </dependentAssembly> ``` ### 設定 ReactJS.NET 的設定檔放在 `App_Start/ReactConfig.cs` : ```csharp= using JavaScriptEngineSwitcher.Core; using JavaScriptEngineSwitcher.V8; using React; [assembly: WebActivatorEx.PreApplicationStartMethod(typeof(YourApp.ReactConfig), "Configure")] namespace YourApp { public static class ReactConfig { public static void Configure() { ReactSiteConfiguration.Configuration .SetReuseJavaScriptEngines(true) .SetAllowJavaScriptPrecompilation(true) .SetBabelVersion(BabelVersions.Babel7) .AddScript("~/Content/React/Test.jsx"); JsEngineSwitcher.Current.DefaultEngineName = V8JsEngine.EngineName; JsEngineSwitcher.Current.EngineFactories.AddV8(); } } } ``` 這個檔告訴 ReactJS.NET 該跑哪隻 js 來 SSR React 元件,過程中能以 [Babel][babel] 替我們 transpile js ,所以還要設定 Babel 版本。 為了使用它提供的 `BabelBundle` ,還得修改 `BundleConfig.cs` : ```diff= +using System.Web.Optimization.React; namespace YourApp { // 略 + bundles.Add(new BabelBundle("~/bundles/React").Include( + "~/Content/React/Test.jsx")); // 略 } ``` 再修改 `Web.config` 讓 bundle 發生作用: ```diff= <system.web> <!-- Set compilation debug="true" to insert debugging symbols into the compiled page. Because this affects performance, set this value to - <compilation debug="true" targetFramework="4.7.2"/> + <compilation debug="false" targetFramework="4.7.2"/> <httpRuntime targetFramework="4.7.2" executionTimeout="300" maxRequestLength="204800"/> ``` [babel]: https://babeljs.io/ ### 繪製 React 元件 最後準備一個 React 元件,例如 `Test.jsx` : ```javascript= function Test() { const [counter, setCounter] = React.useState(0); return ( <div className="test"> <span>count: {counter}</span> &nbsp; <button onClick={() => setCounter(x => x + 1)}> up </button> </div> ); } ``` 並在 view 加上: ```cshtml= @using System.Web.Optimization @using React.Web.Mvc <body> @Html.React("Test", new {}) <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> @Scripts.Render("~/bundles/React") @Html.ReactInitJavaScript() </body> ``` 就能看見 ASP.NET MVC 在 HTML 中繪製好 UI 元件,並正確地把元件控制權交給 browser 。 ### 差強人意之處 只靠 Babel 做 transpiling ,而不靠 webpack 打包我們的 js ,會讓我們無法使用 ES6 module 、不能 `import` 或是 `require` 其他 React 元件。 而且 `BabelBundle` 一定要在關閉 debug 的環境中才能運作,增加開發難度。 接著我們會自己打包 js ,並修改設定讓 ReactJS.NET 直接用打包好的 js 。 ## 使用 webpack 打包 js ### 準備打包環境 [安裝 yarn][yarn-installation] : ```shell= # 不用切換到最新的 berry(yarn2) 版 npm i -g yarn ``` 另外準備一個目錄來放需要打包的 js ,結構如下: ``` . ├── package.json ├── src │   ├── components │   │   ├── Test.jsx │   │   └── index.js │   └── index.js └── webpack.config.js ``` 再安裝需要的套件: ```shell= yarn add --dev babel-loader @babel/core @babel/preset-env @babel/preset-react yarn add --dev webpack webpack-cli ``` [yarn-installation]: https://yarnpkg.com/getting-started/install ### 設定 我們現在還在使用外部的(不是由 npm 安裝的) React 和 ReactDOM ,所以要寫明 `externals` 告訴 webpack 這些 library 可以從全域取得: ```javascript= externals: { 'react': 'React', 'react-dom': 'ReactDOM', } ``` 為了讓 ReactJS.NET 可以取用 entry point js 暴露的 React 元件: ```javascript= output: { path: yourAppScriptPath, filename: 'client.bundle.js', libraryTarget: 'global', library: 'VitalUI', globalObject: 'this', // 如果不寫這行, globalObject 會變成 self 導致 SSR 失敗 } ``` 在 `output` 中得特別加上 `libraryTarget: 'global'`, `library: 'VitalUI'`, `globalObject: 'this'` ,這樣在執行 `client.bundle.js` 時,就能從 `VitalUI` 這個全域物件拿到 React 元件。 完整的 `webpack.config.js` 內容如下: ```javascript= const path = require('path'); const yourAppScriptPath = path.resolve(__dirname, '../src/YourApp/Scripts'); module.exports = { mode: 'development', entry: './src/index.js', output: { path: yourAppScriptPath, filename: 'client.bundle.js', libraryTarget: 'global', library: 'VitalUI', globalObject: 'this', }, externals: { 'react': 'React', 'react-dom': 'ReactDOM', }, resolve: { extensions: ['.js', '.jsx'], }, module: { rules: [ { test: /\.m?jsx?$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: [ '@babel/preset-env', '@babel/preset-react', ], }, }, }, ], }, }; ``` ### 準備開發與建置 scripts 我們的目的是將 js 打包到特定目錄,而 .Net MVC 不像 .Net Core 有 `SpaApplicationBuilderExtensions` 可以用,所以我們不像 CRA 或是一般的 React SPA 專案那樣,跑自己的 `webpack-dev-server` 。 `package.json` 裡的 `scripts` 段落則寫成: ```json= "scripts": { "start": "webpack --config ./webpack.config.js --watch", "build": "webpack --config ./webpack.config.js" } ``` 這樣開發時只要啟動 .Net 應用程式,並在前端目錄下跑 `yarn start` 即可。 ### 修改 ReactJS.NET 相關設定 現在我們可以拿掉 `BabelBundle` : ```diff= - bundles.Add(new BabelBundle("~/bundles/React").Include( - "~/Content/React/Test.jsx")); ``` 在 `App_Start/ReactConfig.cs` 裡關掉 Babel ,並使用打包完的 `client.bundle.js` : ```diff= ReactSiteConfiguration.Configuration .SetReuseJavaScriptEngines(true) .SetAllowJavaScriptPrecompilation(true) - .SetBabelVersion(BabelVersions.Babel7) - .AddScript("~/Content/React/Test.jsx"); + .SetLoadBabel(false) + .AddScriptWithoutTransform("~/Scripts/client.bundle.js"); ``` 並更新 view 的程式碼: ```diff= <body> - @Html.React("Test", new {}) + @Html.React("VitalUI.Test", new {}) <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> - @Scripts.Render("~/bundles/React") + @Scripts.Render("~/Scripts/client.bundle.js") @Html.ReactInitJavaScript() </body> ``` 這樣要 SSR 時, `@Html.React` 會取用全域物件 `VitalUI` 裡的 `Test` 元件。 `@Script.Render` 則在 client-side 引用打包好的 js 。 ## 下一步 按計劃,要做到: * 靠 TypeScript 描述資料間的關係 * 切分我們自己的 js 和來自第三方的 js * 前端工程師仍可以啟動 Storybook 、和設計師一起準備 UI 元件 * 後端工程師可以準備 [server-side data][server-side-data] 讓 UI 元件取用 我們將在後續的文件中,繼續說明怎麼完成這些事。 [server-side-data]: https://reactjs.net/tutorials/aspnet4.html#server-side-data ## 參考資料 * ReactJS.NET 的 [Tutorial (ASP.NET 4.x)](https://reactjs.net/tutorials/aspnet4.html) * [Use the React project template with ASP.NET Core](https://docs.microsoft.com/zh-tw/aspnet/core/client-side/spa/react?view=aspnetcore-5.0&tabs=visual-studio) - 讓我們比較在 .NET Core 和 .NET MVC 使用 React 時有什麼差異 * ReactJS.NET 的 [React.Sample.Mvc4](https://github.com/reactjs/React.NET/tree/main/src/React.Sample.Mvc4) - 展示了一個完整的 ReactJS.NET MVC 專案長什麼樣子 * [Server + Client side React components in Asp.NET MVC 4 using Create-React-App and Parcel](https://github.com/johot/reactjs.net-asp-mvc4-cra-typescript-parcel) - 這個 repo 展示了怎麼樣自己打包 js 給 ReactJS.NET 使用 * [webpack 官方文件](https://webpack.js.org/concepts/)