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