整合vitejs + vue3搭配.NET Core網站開發練習

這篇文章主要在練習如何整合前後端網站,在開發時期階段的體驗及發佈出去的方法,最終將網站容器化的一個範例

Intro

這篇文章主要是針對 Youtube:AspNetCore Api + Vitejs + Vuejs Setup for Dev & Prod - Anton Wieslander所做的練習及心得,建議有時間的人可以花 20 分鐘看一下

example repos

  1. LAB-vite: 因為喜歡在 rider 開發,因此新建一個方案檔將前後端專案都拉進來
  2. Lab-vite-vue-dotnetcore: 多了 dockerfile 的部分,其餘大同小異

網站開發框架的選擇

webpack 很好沒錯,但專案變大後,不管是在產生 production build 還是在開發時期的編譯時間,都需要很久的時間才能運作,如果再加上開發較複雜的專案,可能同時主機就會需要開好幾個 IDE,有時為了做 PoC 還會需要弄個 docker 架架資料庫
在這樣的背景之下如果還要等待 webpack 是非常難以忍受的事情;同理,在先前就已經了解到了 vue2vue3 的差異,並且希望能夠練習一下 vue3 所帶來的開發體驗,而我又是慣常開發 C# 的人,採用.NET6 mvc也是很自然的一件事情,因此就有了下列的技術架構

  1. 前端採用 vite.js + vue3.js
  2. 後端採用 .NET6 mvc

vite.js 能幹嘛

  1. 可以拿來打包程式碼
  2. 開發模式的時候,速度挺快 (利用 瀏覽器支援Native ESM進行運作)
  3. 正式環境的時候,打包程式碼跟 webpack 一樣,都是將所需要的模組全部包在一起 (為了避免因依賴鍊過長,造成瀏覽器持續的請求資源,這當中前端 JS 是停在那邊等待的,畫面會卡)

vue3.js 的好處

相比於 vue2 的差異,最主要的感受應該是提供了 Composition API 的寫法,還有效能的提升,差異的部分網路上很多,我覺得這一篇vue3 對比 Vue2.x 差異性、注意點、整體梳理,與 React hook 比又如何挺不錯,專業的評比還是交給專業的來,我轉發一下就好

對我而言,最主要的就是一系列的效能優化改善,以及最直觀的語法上提供了 Composition API 所帶來的優點,也就是關注點分離,使邏輯、資料可以集中存放維護,也便於 reUse

前後端分離開發

因為實際上佈署出去的環境,也會影響到開發網站的選擇,這邊因為只是練習,所以打算是將前後端 deploy 到同一個站台發佈出去,之後可以再包裝成 docker 直接容器化,感覺想法還行,那麼就是實作來驗證看看開發體驗順不順,因為是前後端分離專案,兩邊開發,之所以這麼考量,主要還是覺得前端後端其實都有其專業程度,一個專案內如果前後端包在一起,那勢必就會需要開始學習自己不熟悉的部分,有些人喜歡這樣,但也有的人不喜歡,認為為甚麼開發前端,我還要去了解後端的 razor 頁面、生命週期阿啥的東西;或者是反過來開發後端,我還需要去學習 webpacknpm、指令列那些的東西,不能就用 VS2022 IDE 介面上點一點就好了嗎?

我覺得有興趣的話,東西都放在同一個 sln 內,想看可以去看;如果不感興趣,直接開自己的頁面,前端就確定好假資料的 API 自己刻;後端因為也只提供前端呼叫,所以確認好 API 介面後自己單元測試做一做,都挺好的,反正不管怎麼說,前後端始終都有自己的技術堆疊,切割開來會是比較好的選擇

但是兩個專案跑起來兩個站台,假設前端 3000後端5000好了,前端跟後端索取資料,很自然地就會需要 fetch 給網址,像是 http://localhost:5000/Api/Test,但是最終又是會放在一起 deploy 出去的,所以 production 出去的時候,網址應該是打/Api/Test,那有沒有辦法可以解決呢

應該有吧,說不定可以弄一個設定檔之類的東西,看 deploy 出去是 development 還是 production 來切換吧?嗯,可能吧,但想想就有點累,有沒有更方便一點的?

Microsoft.AspNetCore.SpaServices.Extensions

還真的有,這東西就叫做 Microsoft.AspNetCore.SpaServices.Extensions,但是必須要抱歉的是,我對它不熟悉,而且似乎網上的介紹很少,但藉由一些練習我大概摸索出來的用途如下

  1. 它能夠將發給後端的請求,代理轉發到 SPA 那邊去,所以我們必須要再判斷當前為開發時期的時候才這樣做,因為在正式環境,東西都被我們佈署在一起了,就不需要做代理這件事情
  2. 它能夠指定網站的RootPath,所以當佈署在一起的時候,網站吃的首頁就是該路徑

LAB:建立前後端網站

準備工作大概完成了,底下就列出練習的步驟及重點

建立前端網站

建立目錄並利用官方 Getting Started的指令建立一個快速的啟動專案

1
2
3
mkdir Lab-vite-vue-dotnetcore
cd Lab-vite-vue-dotnetcore
npm create vite@latest

接著會要求輸入專案名稱,選擇技術框架、採用的語言

完成後可以進入該目錄先安裝套件,並跑一次看看網站是否正常啟動

1
2
3
cd front-end
npm install
npm run dev

建立後端網站

回到上層目錄接著開始透過 dotnet cli 建立新的 mvc 專案,完成後一樣測試看看網站是否正常啟動

1
2
3
dotnet new web -n back-end
cd back-end
dotnet run

LAB:前端索取資料

這邊我們練習從 Vue Component 裡面去撈後端資料,重點在 fetch 之後,要透過 reactive 包裝,這樣資料才會響應式的隨著變動

底下就列出關鍵的部分

後端給予資料

做一個 API 提供前端呼叫,當路由符合 /api/test就回應一個字串,因此做一個測試的 TestController

1
2
3
4
5
6
7
8
9
10
11
12
13
// Controller/TestController.cs
using Microsoft.AspNetCore.Mvc;

namespace back_end.Controller;

[Route("/api/[controller]")]
public class TestController : Microsoft.AspNetCore.Mvc.Controller
{
public IActionResult Index()
{
return Ok("Test Result");
}
}

當然也因為測試環境是兩個站台,所以後端為了測試要允許CORS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseCors(b => b.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
}

app.UseRouting();
app.UseEndpoints(e => e.MapDefaultControllerRoute());


app.Run();

前端取得資料

在前端 SFC 透過 fetch 拿資料後將結果指派回資料,經由 vue 渲染於畫面上,後端路由:https://localhost:7156/api/Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup>
// /src/components/HelloWorld.vue
import { reactive, ref } from "vue"

defineProps({
msg: String,
})

const count = ref(0)
// === new code begin
const state = reactive({
message: "empty",
})

fetch("https://localhost:7156/api/Test")
.then((r) => r.text())
.then((t) => (state.message = t))
// === new code end
</script>

LAB:Microsoft.AspNetCore.SpaServices.Extensions

新增套件並設定

後端加入套件Microsoft.AspNetCore.SpaServices.Extensions並設置,因為目前我採用.NET6,所以安裝的版本是 6.0.11 版

1
dotnet add package Microsoft.AspNetCore.SpaServices.Extensions --version 6.0.11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// 此處指定 dist 是因為之後我們要佈署的時候,前端是把 build 目錄 dist 整個都複製過來
builder.Services.AddSpaStaticFiles(config => config.RootPath = "dist");
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
// 因為採用套件,開發時期用 proxy 代理,已經不再需要 CORS
// app.UseCors(b => b.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
}

app.UseRouting();
app.UseEndpoints(e => e.MapDefaultControllerRoute());

// 使用 SPA 的靜態檔案
app.UseSpaStaticFiles();
app.UseSpa(b =>
{
if (app.Environment.IsDevelopment())
{
// 開發階段採用 proxy 指定前端網站
b.UseProxyToSpaDevelopmentServer("http://localhost:5173/");
}
});
app.Run();

修改前端 fetch 網址

因為請求已經被代理,所以路由就改成相對路徑

1
2
3
fetch("/api/Test")
.then((r) => r.text())
.then((t) => (state.message = t))

接著直接開啟後端網站,就可以看到前端SPA,呼叫後端API的結果

這個時候,如果你同時有開啟 Hot Reload , HMR,前後端的程式碼修改後都可以直接看到瀏覽器會去自動更新畫面,當然後端的部分 Hot Reload 有一些條件,但到目前為止整體的開發流程無疑被改善很多了

發佈網站

1
2
// 發布後端網站到上層的output目錄
dotnet publish -c Release -o ..\output

前端的部分直接讓他發佈到 output 就好

1
2
3
4
5
6
7
8
9
10
// vite.config.js
import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"

export default defineConfig({
plugins: [vue()],
build: {
outDir: "../output/dist",
},
})
1
npm run build

測試發佈版本

目前專案目錄如下

進入 output 目錄,這邊存放的是剛才前後端輸出的結果

直接透過 dotnet back-end.dll執行

將應用程式容器化

建立 dockerfile

因為不想要把建立搞得太麻煩,所以 build 都在外面先做好,image 就是直接拿 output 複製進去,所以指令變得很單純

1
2
3
4
5
6
7
FROM mcr.microsoft.com/dotnet/aspnet:6.0

COPY output .

EXPOSE 80

ENTRYPOINT ["dotnet", "back-end.dll"]

產生 image

在專案根目錄下執行指令建立 docker image

1
docker build -t demo .

產生 container

當然還需要將 image 透過指令來產生 container,並且測試將 port:7000 指給 container

1
docker run -d --name=lab -p 7000:80 demo



結論

經由這一次的練習,大概可以學到,或者說是複習了

  1. Vite.js + Vue3.js 的威力展示 (HMR)
  2. .NET6 mvc
  3. 前後端整合開發
  4. production 發佈方式
  5. 容器化

可以為之後日益複雜的軟體架構打一些基礎,而過去幾年的技術與現在相比,在開發時期階段更是進步了很多,但是如果舊網站跟不上技術迭代的腳步,可以想見的是維護的人力會越來越辛苦,也越來越少。像是以前的 classic asp 應該已經不會有人想要維護,而傳統的 js 開發方式也很難被現在的開發者接受,舊專案除非已經穩定沒有維護需求那就還好,技術背景停滯在開發當下的階段,如果還要求持續更新專案功能或是除錯維護,無疑是非常挑戰開發人員的一件事情,要嘛弄一些 workAround 避掉,透過增加整體架構的複雜度、犧牲可維護性;要嘛就是乖乖地想辦法把應用程式的技術棧一層層慢慢更新上去,但這其中也有很大的可能性會受限於
客戶端硬體、系統。只能說真的看運氣了,不論如何,軟體開發的演進總是越來越進步,身為開發人員也必須要能夠持續學習成長,才能說是專業從業人員啊 (技術沒有很強至少要有學習心態啊~)

補充:如果你也喜歡用 Rider

我非常喜歡使用 Rider 來開發,所以我做了另外一個 Repo:LAB-vite,主要就是為了在 Rider 裡面可以方便的開發,這邊列出一些我碰到的問題還有解法,下面的步驟我就再做一次在新的練習專案:Lab-vite-vue-dotnetcore

我想要利用 IDE 的搜尋功能,找到前端檔案

首先幫前端專案複製一個專案檔過去,並修改一下,檔案命名為 front-end.csproj

1
2
3
4
5
6
7
8
9
10
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>front_end</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>

根目錄下建立方案檔,並將專案加入

1
2
3
dotnet new sln
dotnet sln .\Lab-vite-vue-dotnetcore.sln add .\back-end\back-end.csproj
dotnet sln .\Lab-vite-vue-dotnetcore.sln add .\front-end\front-end.csproj



最後透過開啟方案檔的方式,就可以透過 Search EveryWhere 找到前端檔案了

測試前後端都要分別執行,有沒有 one click 搞定的方法

有的,但是仍舊需要做一些前置作業,前端需要一個任務去執行 npm run dev、後端需要一個任務去執行dotnet watch,最後在透過內建的Compound執行兩個任務,

首先我們設定前端的部分,指定好前端路徑

後端的部分則透過.NET Watch Run Configuration,可以很方便的執行

如果一開始是用開啟目錄的方式打開,而不是透過開啟解決方案、專案的話,是沒有辦法設定 dotnet watch外掛的

新增 Compound 任務如下,選擇另外兩個任務就好

點一下之後等待兩個任務跑完,瀏覽器自動開出來

測試一下前端修改之後,HMR 正確執行

後端的部分修改完畢後會自動重載頁面

既然都有一鍵執行了,那一鍵發佈呢

本質都是一樣的東西啊,把指令包裝一下就好了

再新增一個執行 output 目錄下 exe 的任務

測試一下發佈的網站結果

那容器化也來一個吧,我真的懶得打指令了

新增任務照圖設定

如果想要順便把容器也建立起來,就填一下必要的設定值

測試一下 docker container 的網站