上篇介紹了Util的開發環境,並讓你把Demo運行起來。本文將介紹該Demo的前端Angular運行機制以及目錄結構。 目錄結構 在VS上打開Util Demo,會看見如下的目錄結構。 現代前端通常採用VS Code開發,不過我們為了使用TagHelper,需要採用VS開發,這為你提供了更多的選擇。 ...
上篇介紹了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還原
當你輸入yarn和cnpm install node-sass,它會找到package.json文件的dependencies節,然後把需要的文件下載到node_modules目錄中。
執行Webpack構建
然後輸入npm run dev,這裡發生了什麼?
npm run是npm的一個命令,它會查找package.json中scripts定義的命令。
- 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.js和vendor.css兩個文件。
註意:vendor.js僅在開發調試階段使用,所以並沒有對它進行壓縮,正式發佈並不需要執行vendorJs對象。
該腳本的最後一行證明瞭這一點。
return isDev ? [ vendorJs, vendorCss] : [vendorCss];
- npm run app
npm run app又包含兩個命令,用於執行webpack.config.util.js和webpack.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路由配置中設置的。
控制器ApplicationController的Index方法將返回視圖。
Angular JIT編譯會在系統啟動時請求服務端URL,在Chrome瀏覽器F12調出開發者工具,刷新頁面,會觀察到頁面請求了Areas中的控制器。
所以你在開發階段運行項目會感覺比較慢,在正式發佈後就沒這些開銷了。
小結
本文簡要介紹了Util Demo的目錄結構和運行機制,如果你沒有Angular基礎,估計還是很難看懂,建議你閱讀Angular中文網https://angular.cn。
未完待續,下一篇將對Util Demo的Angular封裝進行介紹,本來是準備這篇介紹的,不過限於篇幅,放到下篇,我知道,太長的文章既難寫更難讀。
寫文需要動力,請大家多多支持,點下推薦,Github點下星星。
Util應用框架交流一群: 24791014