.Net Core應用框架Util介紹(三)

  上篇介紹了Util的開發環境,並讓你把Demo運行起來。本文將介紹該Demo的前端Angular運行機制以及目錄結構。

 

目錄結構

    在VS上打開Util Demo,會看見如下的目錄結構。

  

  現代前端通常採用VS Code開發,不過我們爲了使用TagHelper,需要採用VS開發,這爲你提供了更多的選擇。 

  你可以將WebApi和Angular應用放在同一個項目中,就像現在看見的那樣。也可以分別把WebApi和Angular應用放到不同項目中。 

  如果你已經習慣了VS Code開發,這同樣沒問題,不過你將放棄TagHelper帶來的強類型代碼提示和編譯時檢查特性。 

  對於Angular,它提供了ng cli命令行工具,你可以用ng cli來創建項目結構。

  前文已簡要介紹了TagHelper,它是用來提升Angular視圖頁面開發效率的利器。爲了使用TagHelper,不得不放棄ng cli,因爲它不支持在Angular組件上配置服務端動態地址。

  下面介紹這個項目中包含的目錄和文件。

 

Apis目錄

  這個目錄用來存放Web Api控制器。

  ApplicationController演示了普通CRUD操作,RoleController演示了樹型層次的CRUD操作。

  你暫時不要關心Web Api CRUD操作,我會在後續介紹。

 

 Areas目錄

   用過Asp.Net Mvc的同學可能知道,Areas就是區域,它的作用是提供模塊化管理。我們把不同的模塊用Areas的區域分隔開,這樣在項目規模變大時,還能迅速找到相關頁面。

  與傳統Asp.Net Mvc應用不同,Util的Areas控制器並不進行任何操作,只是簡單的返回視圖頁面,cshtml僅起到代碼生成器的作用。

  一個更好的選擇是使用RazorPage,它把控制器和頁面合併了,將來會使用這種方式。 

Configs目錄

   你並不需要它,我在Demo中用來放測試配置,項目上我通常把Configs目錄放在應用層類庫。

Controllers目錄

   Controllers目錄是用來放置與首頁相關的控制器。

Datas目錄  

  Util引入了DDD經典架構,Datas位於基礎設施層,一些人把它叫倉儲層。

  Datas通常放在單獨的類庫,爲了演示簡單,我放在該WEB項目的目錄中。

DbScripts目錄

  這個目錄提供了Sql Server建庫腳本。

  一些人可能很驚訝,什麼年代了,還在使用Db First開發。

  在多年的開發實戰中,我摸索到一套以PowerDesigner數據建模配合CodeSmith代碼生成的開發模式。對於CRUD,它具有快速高效的特點,同時你還能擁有清晰的數據字典以供未來查閱。

  對於具備面向對象編程能力的人,這種方式並不會降低代碼質量和設計水平,在將代碼生成出來以後,通過手工調整就可達到與Code First相同的代碼水平。

  我會在未來某個合適的時候介紹這種開發模式。

 Domains目錄

  DDD經典架構中領域層相關的目錄,實際開發中將放到單獨的類庫。

Services目錄

  DDD經典架構中應用層相關的目錄,實際開發中將放到單獨的類庫。

Typings目錄

   Angular相關的所有東西都在這裏。

 

  app目錄用來存放與業務相關的項目資源,比如Angular組件,指令,服務等。

  值得注意的是,該目錄包含組件對應的.html文件,這些.html文件和.cshtml文件是怎樣的關係?

 

 

  如果你從未運行過Util Demo項目,打開app目錄,並未找到任何.html文件。

  你可能已經猜到了,.html文件是由.cshtml文件生成的

  你永遠都不應該手工編輯這些.html文件,因爲在調試運行時將被覆蓋。 

  test目錄包含Ts單元測試,我僅對極少數Helper進行單元測試。通過下面的npm命令把測試運行起來。

npm test

 

  util目錄包含對Angular常用API和Angular Material組件的封裝。

  

 

  Angular組件由視圖和控制器兩部分構成。視圖即模板頁,包含html標籤。控制器用來編寫邏輯,包含Ts代碼。換句話說,Angular應用開發主要是編寫html和ts(當然還有css,暫時不要管它)。

  TagHelper並不是Util封裝Angular的唯一手段,對於Angular控制器,Util採用鏈式封裝手法,將Angular常用Api封裝得更加簡單易用,使你對Angular Api只要有一個模糊的印象就可以開發了。

  對於Angular視圖頁面,並不能直接採用TagHelper簡單包裝,這樣會導致TagHelper過於複雜,另外很多功能需要在運行時進行判斷,TagHelper只在開發調試階段存在,所以採用兩層封裝會更加省力。

  首先採用Angular組件或指令對Material組件進行封裝,然後採用TagHelper提供強類型提示。  

  對於希望採用VS Code開發的同學,Typings/util目錄中封裝的代碼同樣可以使用,它跟TagHelper沒有什麼關係,你可以把它Copy到你的項目,我尚未把它發佈到npm。 

Views目錄

  Views目錄包含首頁。

appsettings.json文件

  它是一個配置文件,數據庫連接字符串在這裏。

nlog.config文件

  它是NLog日誌組件的配置文件,Util 採用NLog輸出開發調試和錯誤日誌,默認位置是c:\log目錄。

package.json文件

  它是npm包管理器的配置文件。

Program.cs文件

  它是Asp.Net Core程序入口點文件。

Startup.cs文件

  它是Asp.Net Core啓動文件,在這裏配置依賴注入和中間件請求管道。

tsconfig.json文件

  它是Typescript語言配置文件。

webpack.config.js文件

  它是Webpack自動化構建工具的配置文件。

  還有兩個配置文件隱藏在webpack.config.js下,它們對util和第三方Js框架進行處理。

 

運行機制

  對於沒有前端基礎的同學,可能很難理解這個Demo是如何運行起來的,下面爲你介紹這個Demo的運行機制,我們從npm包還原開始。 

 npm還原

  當你輸入yarncnpm install node-sass,它會找到package.json文件的dependencies節,然後把需要的文件下載到node_modules目錄中。

 執行Webpack構建

  然後輸入npm run dev,這裏發生了什麼?

  npm run是npm的一個命令,它會查找package.jsonscripts定義的命令。

 

  • npm run dev 

  dev就是npm run要查找的命令名,它是一個約定俗成的名稱,代表開發階段配置,即develop,當然你不一定用這個名字,叫abc也可以。

  npm run dev查找到package.json文件scripts節定義的dev命令,它的內容是npm run vendor && npm run app,這個命令是由兩個npm run命令組成的。

  •  npm run vendor

  npm run vendor的內容是webpack --config webpack.config.vendor.js,這將對webpack.config.vendor.js執行構建操作。

  webpack命令默認查找webpack.config.js文件,現在要查找的是webpack.config.vendor.js,所以需要添加參數—config。

  我們來看看webpack.config.vendor.js包含什麼內容。 

  1 const pathPlugin = require('path');
  2 const webpack = require('webpack');
  3 var Extract = require("extract-text-webpack-plugin");
  4 
  5 //第三方Js庫
  6 const jsModules = [
  7     'reflect-metadata',
  8     'zone.js',
  9     'moment',
 10     '@angular/animations',
 11     '@angular/common',
 12     '@angular/common/http',
 13     '@angular/compiler',
 14     '@angular/core',
 15     '@angular/forms',
 16     '@angular/elements',
 17     '@angular/platform-browser',
 18     '@angular/platform-browser/animations',
 19     '@angular/platform-browser-dynamic',
 20     '@angular/router',
 21     '@angular/cdk/esm5/collections.es5',
 22     '@angular/flex-layout',
 23     '@angular/material',
 24     'primeng/primeng',
 25     'lodash',
 26     "echarts-ng2"
 27 ];
 28 
 29 //第三方Css庫
 30 const cssModules = [
 31     '@angular/material/prebuilt-themes/indigo-pink.css',
 32     'material-design-icons/iconfont/material-icons.css',
 33     'font-awesome/css/font-awesome.css',
 34     'primeicons/primeicons.css',
 35     'primeng/resources/themes/omega/theme.css',
 36     'primeng/resources/primeng.min.css'
 37 ];
 38 
 39 module.exports = (env) => {
 40     //是否開發環境
 41     const isDev = !(env && env.prod);
 42     const mode = isDev ? "development" : "production";
 43 
 44     //將css提取到單獨文件中
 45     const extractCss = new Extract("vendor.css");
 46 
 47     //獲取路徑
 48     function getPath(path) {
 49         return pathPlugin.join(__dirname, path);
 50     }
 51 
 52     //打包第三方Js庫
 53     let vendorJs = {
 54         mode: mode,
 55         entry: { vendor: jsModules },
 56         output: {
 57             publicPath: 'dist/',
 58             path: getPath("wwwroot/dist"),
 59             filename: "[name].js",
 60             library: '[name]'
 61         },
 62         resolve: {
 63             extensions: ['.js']
 64         },
 65         devtool: "source-map",
 66         plugins: [
 67             new webpack.DllPlugin({
 68                 path: getPath("wwwroot/dist/[name]-manifest.json"),
 69                 name: "[name]"
 70             }),
 71             new webpack.ContextReplacementPlugin(/\@angular\b.*\b(bundles|linker)/, getPath('./Typings')),
 72             new webpack.ContextReplacementPlugin(/angular(\\|\/)core(\\|\/)@angular/, getPath('./Typings')),
 73             new webpack.IgnorePlugin(/^vertx$/)
 74         ]
 75     }
 76 
 77     //打包css
 78     let vendorCss = {
 79         mode: mode,
 80         entry: { vendor: cssModules },
 81         output: {
 82             publicPath: './',
 83             path: getPath("wwwroot/dist"),
 84             filename: "[name].css"
 85         },
 86         devtool: "source-map",
 87         module: {
 88             rules: [
 89                 { test: /\.css$/, use: extractCss.extract({ use: isDev ? 'css-loader' : 'css-loader?minimize' }) },
 90                 {
 91                     test: /\.(png|jpg|gif|woff|woff2|eot|ttf|svg)(\?|$)/, use: {
 92                         loader: 'url-loader',
 93                         options: {
 94                             limit: 20000,
 95                             name: "[name].[ext]",
 96                             outputPath: "images/"
 97                         }
 98                     }
 99                 }
100             ]
101         },
102         plugins: [
103             extractCss
104         ]
105     }
106     return isDev ? [ vendorJs, vendorCss] : [vendorCss];
107 }
webpack.config.vendor.js

  vendorJs 對象用於配置將哪些第三方Js框架文件進行打包,vendorCss 對象用於配置需要打包的第三方框架提供的Css文件。 

  entry屬性指定了需要打包的入口文件,output屬性則指定輸出的位置和文件名。 

  當webpack.config.vendor.js執行完畢,會在Util.Samples.Webs項目的wwwroot目錄創建一個dist子目錄,並生成vendor.jsvendor.css兩個文件。

  注意vendor.js僅在開發調試階段使用,所以並沒有對它進行壓縮,正式發佈並不需要執行vendorJs對象。

  該腳本的最後一行證明了這一點。

return isDev ? [ vendorJs, vendorCss] : [vendorCss];
  •  npm run app 

  npm run app又包含兩個命令,用於執行webpack.config.util.jswebpack.config.js

webpack --config webpack.config.util.js && webpack

  先來看看webpack.config.util.js

 1 const pathPlugin = require('path');
 2 const webpack = require('webpack');
 3 
 4 module.exports = (env) => {
 5     //是否開發環境
 6     const isDev = !(env && env.prod);
 7     const mode = isDev ? "development" : "production";
 8 
 9     //獲取路徑
10     function getPath(path) {
11         return pathPlugin.join(__dirname, path);
12     }
13 
14     //打包util腳本庫
15     return {
16         mode: mode,
17         entry: { util: [getPath("Typings/util/index.ts")] },
18         output: {
19             publicPath: 'dist/',
20             path: getPath("wwwroot/dist"),
21             filename: "[name].js",
22             library: '[name]'
23         },
24         resolve: {
25             extensions: ['.js', '.ts']
26         },
27         devtool: "source-map",
28         module: {
29             rules: [
30                 { test: /\.ts$/, use: ['awesome-typescript-loader?silent=true'] }
31             ]
32         },
33         plugins: [
34             new webpack.DllReferencePlugin({
35                 manifest: require('./wwwroot/dist/vendor-manifest.json')
36             }),
37             new webpack.DllPlugin({
38                 path: getPath("wwwroot/dist/[name]-manifest.json"),
39                 name: "[name]"
40             })
41         ]
42     }
43 }
webpack.config.util.js

  它將查找Util.Samples.Webs項目下Typings/util/index.ts文件,這是util默認導出文件,所有在外部需要訪問的類型都會從這裏導出。

  當webpack.config.util.js執行完畢,會在dist目錄創建util.js文件。

  同樣的,util.js文件僅用於開發調試階段 

  下面看webpack.config.js

 1 const pathPlugin = require('path');
 2 const webpack = require('webpack');
 3 var Extract = require("extract-text-webpack-plugin");
 4 const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
 5 
 6 module.exports = (env) => {
 7     //是否開發環境
 8     const isDev = !(env && env.prod);
 9     const mode = isDev ? "development" : "production";
10 
11     //將css提取到單獨文件中
12     const extractCss = new Extract("app.css");
13 
14     //獲取路徑
15     function getPath(path) {
16         return pathPlugin.join(__dirname, path);
17     }
18 
19     //打包js
20     let jsConfig = {
21         mode: mode,
22         entry: { app: getPath("Typings/main.ts") },
23         output: {
24             publicPath: 'dist/',
25             path: getPath("wwwroot/dist"),
26             filename: "[name].js",
27             chunkFilename: '[id].chunk.js'
28         },
29         resolve: {
30             extensions: ['.js', '.ts']
31         },
32         devtool: "source-map",
33         module: {
34             rules: [
35                 { test: /\.ts$/, use: isDev ? ['awesome-typescript-loader?silent=true', 'angular-router-loader'] : ['@ngtools/webpack'] },
36                 { test: /\.js$/, loader: '@angular-devkit/build-optimizer/webpack-loader', options: { sourceMap: false } },
37                 { test: /\.html$/, use: 'html-loader?minimize=false' }
38             ]
39         },
40         plugins: [
41             new webpack.DefinePlugin({
42                 'process.env': { NODE_ENV: isDev ? JSON.stringify("dev") : JSON.stringify("prod") }
43             })
44         ].concat(isDev ? [
45             new webpack.DllReferencePlugin({
46                 manifest: require('./wwwroot/dist/vendor-manifest.json')
47             }),
48             new webpack.DllReferencePlugin({
49                 manifest: require('./wwwroot/dist/util-manifest.json')
50             })
51         ] : [
52                 new AngularCompilerPlugin({
53                     tsConfigPath: 'tsconfig.json',
54                     entryModule: "Typings/app/app.module#AppModule"
55                 })
56             ])
57     }
58 
59     //打包css
60     let cssConfig = {
61         mode: mode,
62         entry: { app: getPath("wwwroot/css/main.scss") },
63         output: {
64             publicPath: './',
65             path: getPath("wwwroot/dist"),
66             filename: "[name].css"
67         },
68         resolve: {
69             modules: ['wwwroot']
70         },
71         devtool: "source-map",
72         module: {
73             rules: [
74                 {
75                     test: /\.scss$/, use: extractCss.extract({
76                         use: isDev ? ['css-loader', { loader: 'postcss-loader', options: { plugins: [require('autoprefixer')] } }, 'sass-loader']
77                             : ['css-loader?minimize', { loader: 'postcss-loader', options: { plugins: [require('autoprefixer')] } }, 'sass-loader']
78                     })
79                 },
80                 {
81                     test: /\.(png|jpg|gif|woff|woff2|eot|ttf|svg)(\?|$)/, use: {
82                         loader: 'url-loader',
83                         options: {
84                             limit: 20000,
85                             name: "[name].[ext]",
86                             outputPath: "images/"
87                         }
88                     }
89                 }
90             ]
91         },
92         plugins: [
93             extractCss
94         ]
95     }
96     return [jsConfig, cssConfig];
97 }
webpack.config.js

  webpack.config.js查找Typings目錄下的main.ts,main.ts是angular項目的入口文件。

  webpack通過遞歸依賴查找main.ts,將除了util.js和vendor.js以外所有引用到的ts或js文件打包到dist/app.js文件中。

  注意,正式發佈時,app.js將採用angular官方提供的webpack編譯插件@ngtools/webpack進行AOT編譯並打包生成。

 

  現在dist目錄生成了如下文件。

  0.chunk.js是由angular子模塊生成的js文件,當路由配置對子模塊啓用了延遲加載,每個子模塊都會生成一個獨立的js文件。

 

  loadChildren以延遲加載的方式來配置SystemModule子模塊。

 運行機制

  現在運行angular應用的js文件已經就緒,讓我們把它運行起來,在VS上F5啓動項目。 

  注意:你應該使用Google Chrome來打開它,IE瀏覽器,可以通過啓用polyfill來勉強支持,不過由於效果不佳,我已經把它扔掉了。 

  當瀏覽器打開首頁http://localhost:5200,Asp.Net Core啓動文件Startup.cs中配置的默認路由將被激活,從而將請求發送到HomeController控制器的Index方法。

   Index方法直接返回了Views目錄下Index.cshtml首頁。

 

  environment標籤是一個環境判斷條件,用於設置開發及上線等不同階段的內容。

  <environment include="Development">用於開發階段,<environment exclude="Development">用於發佈階段,可以看出,在發佈後並不需要vendor.js和util.js文件,因爲app.js會包含它們。 

  好,現在瀏覽器加載了Index首頁,Angular應用是如何運行起來的呢?

  • Angular的引導過程

   還記得Angular應用入口文件main.ts嗎,來看看它包含什麼內容。

 

  platformBrowserDynamic是爲瀏覽器平臺提供的JIT動態編譯服務,它將引導AppModule根模塊的啓動。

  AppModule是Angular應用的根模塊,它的主要任務之一就是啓動AppComponent根組件。

  AppComponent是整個Angular應用的根組件,所有其它組件都將被加載到根組件中。

 

  selector用於指定組件的自定義標籤,這裏將根組件標籤定義爲<app></app>,你發現它已經被放置在Index.cshtml中。 

  AppComponent根組件準備啓動了,由於是JIT編譯,所以它需要獲取視圖 

  組件的視圖由templateUrl屬性指定。

templateUrl: env.prod() ? './app.component.html' : '/home/main'

  我們希望開發階段通過訪問服務端控制器來獲取視圖,這樣在編輯TagHelper時就能更方便,只需刷新頁面就能看見效果。 

  env是一個環境檢測對象,prod方法如果返回true表明當前爲正式環境,將從app.component.html靜態文件獲取視圖,如果是開發調試環境,則訪問服務端HomeController控制器的Main方法獲取視圖。 

  Main方法上的Html特性,是用來幫助.cshtml生成.html靜態文件的輔助工具。 

  一般情況下,你並不需要手工設置Html特性來生成html文件,Util提供了ViewControllerBase控制器基類,當你的視圖控制器繼承它,所有html文件就會生成到約定的目錄中。

 

  由Template屬性設置的路徑可知,Typings/app中的項目結構也採用模塊化組織,與區域模塊相對應。

   現在來看根組件的視圖。

   這是你第一次看見Util封裝的TagHelper標籤,以<util-打頭的標籤都是Util TagHelper,它們以粗體顯示,這是由於安裝了Resharper的原因。

  TagHelper在運行時會把html輸出到頁面,它們把弱類型的html封裝成了具有強類型提示的標籤。

  如何知道某個TagHelper到底輸出了什麼html呢?

  一種辦法是打開它生成的.html文件來查找,不過當頁面很複雜時,這種辦法有點吃力。

  另一種辦法是查看日誌,Util TagHelper的每個組件都提供了write-log屬性,當設置爲true,就會在C盤log目錄生成日誌。

 

  main.cshtml視圖中最關鍵的部分就是<router-outlet></router-outlet>標籤。

  router-outlet是Angular路由的佔位符,當根模塊AppModule中配置的路由激活時,相關的Angular組件就會被放進這個佔位符中。

  根模塊中的路由配置被拆分到一個單獨的模塊AppRoutingModule中,路由配置如下。

 

  通過路由配置可以發現,當打開首頁時,命中路由第二項path:’’,會跳轉到/systems/application路徑,systems是一個子模塊,我們來查看它的路由配置。

 

 

  /systems/application將激活ApplicationIndexComponent組件,並把它加載到根組件的<router-outlet></router-outlet>中。

  ApplicationIndexComponent組件請求服務端地址/view/systems/application獲取視圖。

 

   /view打頭的地址將匹配到Areas區域控制器,這是在MVC路由配置中設置的。

 

  控制器ApplicationControllerIndex方法將返回視圖。

 

  Angular JIT編譯會在系統啓動時請求服務端URL,在Chrome瀏覽器F12調出開發者工具,刷新頁面,會觀察到頁面請求了Areas中的控制器。

  

  所以你在開發階段運行項目會感覺比較慢,在正式發佈後就沒這些開銷了。

小結  

  本文簡要介紹了Util Demo的目錄結構和運行機制,如果你沒有Angular基礎,估計還是很難看懂,建議你閱讀Angular中文網https://angular.cn  

  未完待續,下一篇將對Util Demo的Angular封裝進行介紹,本來是準備這篇介紹的,不過限於篇幅,放到下篇,我知道,太長的文章既難寫更難讀。

  寫文需要動力,請大家多多支持,點下推薦,Github點下星星。

  Util應用框架交流一羣: 24791014

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章