---
# System prepended metadata

title: 在 ASP.NET MVC 中使用 React 元件 (1)
tags: [React]

---

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