.NET Core应用框架Util介绍(三)

2018 年 9 月 19 日 DotNet

(点击上方蓝字,可快速关注我们)


来源:何镇汐

.cnblogs.com/xiadao521/p/Util-Introduction-3.html


《.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还原


当你输入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包含什么内容。 


const pathPlugin = require('path');

const webpack = require('webpack');

var Extract = require("extract-text-webpack-plugin");


//第三方Js库

const jsModules = [

    'reflect-metadata',

    'zone.js',

    'moment',

    '@angular/animations',

    '@angular/common',

    '@angular/common/http',

    '@angular/compiler',

    '@angular/core',

    '@angular/forms',

    '@angular/elements',

    '@angular/platform-browser',

    '@angular/platform-browser/animations',

    '@angular/platform-browser-dynamic',

    '@angular/router',

    '@angular/cdk/esm5/collections.es5',

    '@angular/flex-layout',

    '@angular/material',

    'primeng/primeng',

    'lodash',

    "echarts-ng2"

];


//第三方Css库

const cssModules = [

    '@angular/material/prebuilt-themes/indigo-pink.css',

    'material-design-icons/iconfont/material-icons.css',

    'font-awesome/css/font-awesome.css',

    'primeicons/primeicons.css',

    'primeng/resources/themes/omega/theme.css',

    'primeng/resources/primeng.min.css'

];


module.exports = (env) => {

    //是否开发环境

    const isDev = !(env && env.prod);

    const mode = isDev ? "development" : "production";


    //将css提取到单独文件中

    const extractCss = new Extract("vendor.css");


    //获取路径

    function getPath(path) {

        return pathPlugin.join(__dirname, path);

    }


    //打包第三方Js库

    let vendorJs = {

        mode: mode,

        entry: { vendor: jsModules },

        output: {

            publicPath: 'dist/',

            path: getPath("wwwroot/dist"),

            filename: "[name].js",

            library: '[name]'

        },

        resolve: {

            extensions: ['.js']

        },

        devtool: "source-map",

        plugins: [

            new webpack.DllPlugin({

                path: getPath("wwwroot/dist/[name]-manifest.json"),

                name: "[name]"

            }),

            new webpack.ContextReplacementPlugin(/\@angular\b.*\b(bundles|linker)/, getPath('./Typings')),

            new webpack.ContextReplacementPlugin(/angular(\\|\/)core(\\|\/)@angular/, getPath('./Typings')),

            new webpack.IgnorePlugin(/^vertx$/)

        ]

    }


    //打包css

    let vendorCss = {

        mode: mode,

        entry: { vendor: cssModules },

        output: {

            publicPath: './',

            path: getPath("wwwroot/dist"),

            filename: "[name].css"

        },

        devtool: "source-map",

        module: {

            rules: [

                { test: /\.css$/, use: extractCss.extract({ use: isDev ? 'css-loader' : 'css-loader?minimize' }) },

                {

                    test: /\.(png|jpg|gif|woff|woff2|eot|ttf|svg)(\?|$)/, use: {

                        loader: 'url-loader',

                        options: {

                            limit: 20000,

                            name: "[name].[ext]",

                            outputPath: "images/"

                        }

                    }

                }

            ]

        },

        plugins: [

            extractCss

        ]

    }

    return isDev ? [ vendorJs, vendorCss] : [vendorCss];

}


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。


const pathPlugin = require('path');

const webpack = require('webpack');


module.exports = (env) => {

    //是否开发环境

    const isDev = !(env && env.prod);

    const mode = isDev ? "development" : "production";


    //获取路径

    function getPath(path) {

        return pathPlugin.join(__dirname, path);

    }


    //打包util脚本库

    return {

        mode: mode,

        entry: { util: [getPath("Typings/util/index.ts")] },

        output: {

            publicPath: 'dist/',

            path: getPath("wwwroot/dist"),

            filename: "[name].js",

            library: '[name]'

        },

        resolve: {

            extensions: ['.js', '.ts']

        },

        devtool: "source-map",

        module: {

            rules: [

                { test: /\.ts$/, use: ['awesome-typescript-loader?silent=true'] }

            ]

        },

        plugins: [

            new webpack.DllReferencePlugin({

                manifest: require('./wwwroot/dist/vendor-manifest.json')

            }),

            new webpack.DllPlugin({

                path: getPath("wwwroot/dist/[name]-manifest.json"),

                name: "[name]"

            })

        ]

    }

}


它将查找Util.Samples.Webs项目下Typings/util/index.ts文件,这是util默认导出文件,所有在外部需要访问的类型都会从这里导出。


当webpack.config.util.js执行完毕,会在dist目录创建util.js文件。


同样的,util.js文件仅用于开发调试阶段。 


下面看webpack.config.js。


const pathPlugin = require('path');

const webpack = require('webpack');

var Extract = require("extract-text-webpack-plugin");

const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;


module.exports = (env) => {

    //是否开发环境

    const isDev = !(env && env.prod);

    const mode = isDev ? "development" : "production";


    //将css提取到单独文件中

    const extractCss = new Extract("app.css");


    //获取路径

    function getPath(path) {

        return pathPlugin.join(__dirname, path);

    }


    //打包js

    let jsConfig = {

        mode: mode,

        entry: { app: getPath("Typings/main.ts") },

        output: {

            publicPath: 'dist/',

            path: getPath("wwwroot/dist"),

            filename: "[name].js",

            chunkFilename: '[id].chunk.js'

        },

        resolve: {

            extensions: ['.js', '.ts']

        },

        devtool: "source-map",

        module: {

            rules: [

                { test: /\.ts$/, use: isDev ? ['awesome-typescript-loader?silent=true', 'angular-router-loader'] : ['@ngtools/webpack'] },

                { test: /\.js$/, loader: '@angular-devkit/build-optimizer/webpack-loader', options: { sourceMap: false } },

                { test: /\.html$/, use: 'html-loader?minimize=false' }

            ]

        },

        plugins: [

            new webpack.DefinePlugin({

                'process.env': { NODE_ENV: isDev ? JSON.stringify("dev") : JSON.stringify("prod") }

            })

        ].concat(isDev ? [

            new webpack.DllReferencePlugin({

                manifest: require('./wwwroot/dist/vendor-manifest.json')

            }),

            new webpack.DllReferencePlugin({

                manifest: require('./wwwroot/dist/util-manifest.json')

            })

        ] : [

                new AngularCompilerPlugin({

                    tsConfigPath: 'tsconfig.json',

                    entryModule: "Typings/app/app.module#AppModule"

                })

            ])

    }


    //打包css

    let cssConfig = {

        mode: mode,

        entry: { app: getPath("wwwroot/css/main.scss") },

        output: {

            publicPath: './',

            path: getPath("wwwroot/dist"),

            filename: "[name].css"

        },

        resolve: {

            modules: ['wwwroot']

        },

        devtool: "source-map",

        module: {

            rules: [

                {

                    test: /\.scss$/, use: extractCss.extract({

                        use: isDev ? ['css-loader', { loader: 'postcss-loader', options: { plugins: [require('autoprefixer')] } }, 'sass-loader']

                            : ['css-loader?minimize', { loader: 'postcss-loader', options: { plugins: [require('autoprefixer')] } }, 'sass-loader']

                    })

                },

                {

                    test: /\.(png|jpg|gif|woff|woff2|eot|ttf|svg)(\?|$)/, use: {

                        loader: 'url-loader',

                        options: {

                            limit: 20000,

                            name: "[name].[ext]",

                            outputPath: "images/"

                        }

                    }

                }

            ]

        },

        plugins: [

            extractCss

        ]

    }

    return [jsConfig, cssConfig];

}


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点下星星。


相关文章:


《.NET Core应用框架Util介绍(一)》


《.NET Core应用框架Util介绍(二)》


看完本文有收获?请转发分享给更多人

关注「DotNet」,提升.Net技能 

登录查看更多
0

相关内容

Bundle tool for the front-end
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
117+阅读 · 2020年5月10日
【强化学习资源集合】Awesome Reinforcement Learning
专知会员服务
93+阅读 · 2019年12月23日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
95+阅读 · 2019年12月4日
【精通OpenCV 4】Mastering OpenCV 4 - Third Edition 随书代码
专知会员服务
39+阅读 · 2019年11月13日
Python用于NLP :处理文本和PDF文件
Python程序员
4+阅读 · 2019年3月27日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
去哪儿网开源DNS管理系统OpenDnsdb
运维帮
21+阅读 · 2019年1月22日
WebAssembly在QQ邮箱中的一次实践
IMWeb前端社区
13+阅读 · 2018年12月19日
.NET Core 环境下构建强大且易用的规则引擎
开源巨献:阿里巴巴最热门29款开源项目
算法与数据结构
5+阅读 · 2017年7月14日
Deep learning for cardiac image segmentation: A review
Arxiv
21+阅读 · 2019年11月9日
Continual Unsupervised Representation Learning
Arxiv
7+阅读 · 2019年10月31日
Area Attention
Arxiv
5+阅读 · 2019年5月23日
Arxiv
6+阅读 · 2018年5月18日
VIP会员
相关VIP内容
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
117+阅读 · 2020年5月10日
【强化学习资源集合】Awesome Reinforcement Learning
专知会员服务
93+阅读 · 2019年12月23日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
95+阅读 · 2019年12月4日
【精通OpenCV 4】Mastering OpenCV 4 - Third Edition 随书代码
专知会员服务
39+阅读 · 2019年11月13日
相关资讯
Python用于NLP :处理文本和PDF文件
Python程序员
4+阅读 · 2019年3月27日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
去哪儿网开源DNS管理系统OpenDnsdb
运维帮
21+阅读 · 2019年1月22日
WebAssembly在QQ邮箱中的一次实践
IMWeb前端社区
13+阅读 · 2018年12月19日
.NET Core 环境下构建强大且易用的规则引擎
开源巨献:阿里巴巴最热门29款开源项目
算法与数据结构
5+阅读 · 2017年7月14日
Top
微信扫码咨询专知VIP会员