Do.net.5 Core MVC 專案整合 Vue 的開發,是近期於公司上遇到的一項挑戰。由於能參照的範本實屬稀少,於是這邊筆記下整備工作。至於日後在本機端模擬真實上線的步驟 ( 透過 IIS ),則留到下一篇。
重點摘要
- 採用 MVC 架構,因此 Vue-cli 改使用多頁輸出。換言之,每一個頁面都是一個獨立的 Vue Instance
- 後端控制路由和 Controller
- 前端開發時,先運行 dotnet run 將後端伺服器打開 https://localhost:5001 再開始
- 前端開發時,有兩種方法可使用:
1. 進入 client-app 下,使用 vue-cli 提供的 npm run serve 加速開發,並搭配 proxy 自根目錄轉址到 https://localhost:5001
2. 在專案根目錄下 dotnetMultiVues/src/dotnetMultiVues.Web/,使用 env=dev dotnet watch run來開發,不過這樣每次前端有修改存檔時,都得花上時間等候較長的 npm run build - dotnet publish –configuration Release後,會在 bin 資料夾下生成 Release 資料夾和一系列的檔案。最後,將 bin/Release.net5.0/publish 作為 IIS 伺服器上的網站資料夾 ( 實體路徑 )
- 不同的 Vue 實體間,可以利用如 vuex-persistedstate 來維持 state 的狀態。 不過在每次的初始載入前,要視情況決定是否載入儲存於 localStorage 的值。
Github Repo
https://github.com/andy922200/dotnet-mvc-multi-vue-template
Prerequisites
Step by Step
1. 前往 https://dotnet.microsoft.com/download 安裝好 Do.net 5.0 SDK
2. 用 Dotnet new 的指令開啟一個 MVC 專案
1 2 3 |
mkdir dotnetMultiVues cd dotnetMultiVues dotnet new mvc --name dotnetMultiVues.Web -o src/dotnetMultiVues.Web |
3. 在根目錄中用 dotnet sln 來記錄下此根目錄下有哪些專案。若根目錄沒有 .sln,要先新增一個,並將專案加入這個 sln 清單內
1 2 3 |
// dotnetMultiVues dotnet new sln --name dotnetMultiVues dotnet sln dotnetMultiVues.sln add src/dotnetMultiVues.Web/ |
4. 在根目錄下,先行配置好 .gitignore 、 .vscode/settings.json 和 vetur.config.js 檔案
.gitignore
.vscode/settings.json
1 2 3 4 5 6 |
{ "eslint.format.enable": true, "eslint.workingDirectories": [ "./src/DotnetVues.Web/client-app" ] } |
vetir.config.js
1 2 3 4 5 6 7 8 9 10 |
// vetur.config.js module.exports = { settings: { "vetur.useWorkspaceDependencies": true, "vetur.experimental.templateInterpolationService": true }, projects: [ './src/dotnetMultiVues.Web/client-app' ] } |
5. 清除掉 dotnetMultiVues\src\dotnetMultiVues.Web\wwwroot裡的所有東西,因為這些會被 vue-cli build 出來的檔案給取代掉
6. 當你安裝好 npm 和 Vue CLI 後,執行以下指令來產生前端開發環境
1 2 |
// dotnetMultiVues\src\dotnetMultiVues.Web vue create client-app |
你可以根據你的情境,安裝 Vue2 / Vue3 / Typescript / Vuex / Vue-Router … 等功能,以下是這次選擇的配置情況。
1 2 3 4 5 6 7 8 9 |
? Please pick a preset: Manually select features ? Check the features needed for your project: Choose Vue version, Babel, TS, Vuex, CSS Pre-processors, Linter ? Choose a version of Vue.js that you want to start the project with 3.x ? Use class-style component syntax? No ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes ? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass) ? Pick a linter / formatter config: Standard ? Pick additional lint features: Lint on save ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files |
7. 在 client-app 中,打開顯示隱藏的資料夾,將其中的 .git 資料夾給全數刪除。因為在根目錄下已經有版本控制的 .git 了。
8. 在 client-app 下建立 vue.config.js,並調整為多頁輸出模式。若有兩頁名稱為 demo 和 dashboard 的話,可以調整成以下:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// vue.config.js module.exports = { publicPath: process.env.NODE_ENV === 'production' ? `./${process.env.BASE_URL}`: '/', outputDir: '../wwwroot', css: { extract: true, sourceMap: process.env.NODE_ENV !== 'production' }, filenameHashing: false, productionSourceMap: process.env.NODE_ENV !== 'production', configureWebpack: config => { config.devtool = 'source-map' if (process.env.NODE_ENV === 'production') { // config for production... } else { config.devServer = { port: 8080, proxy: { '/': { target: 'https://localhost:5001' } } } } }, pages: { demo: { entry: 'src/pages/demo/main.ts', template: 'public/index.html', filename: 'demo.html', title: 'demo Page', dom: 'demo', chunks: ['chunk-vendors', 'chunk-common', 'demo'] }, dashboard: { entry: 'src/pages/dashboard/main.ts', template: 'public/index.html', filename: 'dashboard.html', title: 'dashboard Page', dom: 'dashboard', chunks: ['chunk-vendors', 'chunk-common', 'dashboard'] } } } |
9. 調整前端頁面的設定檔和資料結構:
public/index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<!-- client-app/public/index.html 此段為 webpack - html-webpack-plugin 打包時會參照的 --> <!DOCTYPE html> <html lang=""> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <title> <%= htmlWebpackPlugin.options.title %> </title> </head> <body> <noscript> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="<%= htmlWebpackPlugin.options.dom %>"></div> <!-- built files will be auto injected --> </body> </html> |
client-app/src
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
src │ shims-vue.d.ts │ ├─assets │ logo.png │ ├─components │ HelloWorld.vue │ ├─pages │ ├─dashboard │ │ App.vue │ │ main.ts │ │ │ └─demo │ App.vue │ main.ts │ └─store index.ts |
.eslintrc.js
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
module.exports = { root: true, env: { node: true }, extends: [ 'plugin:vue/vue3-essential', '@vue/standard', '@vue/typescript/recommended' ], parserOptions: { ecmaVersion: 2020 }, rules: { '@typescript-eslint/indent': ['error', 4], '@typescript-eslint/no-this-alias': 'off', indent: 'off', quotes: [2, 'single'], semi: [2, 'never'], 'no-use-before-define': [2, 'nofunc'], 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'comma-spacing': [2, { before: false, after: true }], 'key-spacing': [1, { beforeColon: false, afterColon: true }], 'import/first': [0], 'object-property-newline': [2, { allowAllPropertiesOnSameLine: false }], 'object-curly-newline': [1, { ObjectExpression: 'always', ImportDeclaration: 'always', ExportDeclaration: 'never' }], 'vue/html-indent': [1, 4, { attribute: 1, baseIndent: 1, closeBracket: 0, alignAttributesVertically: true, ignores: [] }], 'vue/html-closing-bracket-newline': [2, { singleline: 'never', multiline: 'always' }], 'vue/html-closing-bracket-spacing': [2, { selfClosingTag: 'always' }] }, overrides: [ { files: [ '**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)' ], env: { jest: true } } ] } |
.stylelintrc.json
pages/{folder}/*.vue
package.json
startup.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddMvc(option => option.EnableEndpointRouting = false); // add this } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ignore app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); // Whenever a request to the Spa Controller doesnt match a defined Action, return the Index.cshtml and let the front-end handle it // please remember to change the controller name // routes.MapRoute( // name: "spa-route", // template: "{controller=Spa}/{*anything=Index}", // defaults: new { action = "Index" }); }); } |
<project’s name>.csproj
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
" ><Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> <MpaRoot>client-app</MpaRoot> <DefaultItemExcludes>$(DefaultItemExcludes);$(MpaRoot)\node_modules\**</DefaultItemExcludes> </PropertyGroup> <ItemGroup> <!-- extends watching group if you would like to use dotnet watch run--> <Watch Include="$(MpaRoot)\public\*;$(MpaRoot)\src\**\*" Exclude="$(MpaRoot)\node_modules\**\*;obj\**\*;bin\**\*" /> </ItemGroup> <Target Name="NpmRunBuildForDev" BeforeTargets="Build" Condition="'$(env)' == 'dev'"> <Exec WorkingDirectory="$(MpaRoot)" Command="npm run build-for-dev"/> </Target> <Target Name="NpmRunBuild" BeforeTargets="Build" Condition="'$(env)' != 'dev'"> <Exec WorkingDirectory="$(MpaRoot)" Command="npm run build"/> </Target> </Project> |
views/shared/_layout.cshtml
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - DotnetVues.Web</title> <link rel="icon" href="/favicon.ico"> <environment include="Development,Staging,Production"> <link rel="stylesheet" href="~/css/chunk-vendors.css" /> </environment> @RenderSection("Styles", required: false) </head> <body> <header> <nav class="navbar" role="navigation" aria-label="main navigation"> <div class="navbar-brand"> <a class="navbar-item brand-text" href="/">DotnetVues</a> <span aria-hidden="true"></span> <span aria-hidden="true"></span> <span aria-hidden="true"></span> </div> <div class="navbar-menu"> <div class="navbar-end"> <a asp-controller="Dashboard" asp-action="Index" class="navbar-item">Dashboard</a> <a asp-controller="Demo" asp-action="Index" class="navbar-item">Demo</a> </div> </div> </nav> </header> <main role="main" class="pb-3"> @RenderBody() </main> <footer class="border-top footer text-muted"> <div class="container"> &copy; 2021 - DotnetVues.Web - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> </div> </footer> <!-- Add the common js portions here --> <environment include="Development,Staging,Production"> <script src="~/js/chunk-common.js"></script> <script src="~/js/chunk-vendors.js"></script> </environment> @RenderSection("Scripts", required: false) </body> </html> |
views / asp-controller_name / asp-action_name
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// 這邊會對應到 Controllers 裡面的中 IActionResult asp-action_name 的 return 為 VIEW() 的選項 // 以 /dashboard/index 為例子 (簡寫為 /dashboard) // index.cshtml @{ ViewData["Title"] = "MultiVue-Dashboard"; } @section Styles { <environment include="Development,Production,Staging"> <link rel="stylesheet" href="~/css/dashboard.css"/> </environment> } @section scripts { <environment include="Development,Production,Staging"> <script src="~/js/dashboard.js" asp-append-version="true"></script> </environment> } <div id="dashboard"></div> |
controllers
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// Controllers/Dashboard.cs 為例子 using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using dotnetMultiVues.Web.Models; namespace DotnetVues.Web.Controllers { public class DashboardController : Controller { public IActionResult Index() { return View(); // Dashboard/index,會去找同名的 .cshtml } public IActionResult AnotherPage() { return View(); // Dashboard/AnotherPage,會去找同名的 .cshtml } } [Route("api/[controller]")] [ApiController] public class DashboardAPIController : ControllerBase { // GET: api/dashboardAPI [HttpGet] public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } // GET: api/dashboardAPI/5 [HttpGet("{id}", Name = "Get")] public string Get(int id) { return $"This is your id: {id}"; } } } |
Reference
- Vue Multi Pages in Dotnet Core
- What routes do I add so I can have mixed MVC and Web API with both default Get and additional actions?
- Vue-CLI 製作 SPA 很方便,那需要多頁面的時候呢?