
攻城狮一枚; 乐于分享新知识
回顾 今天来晚辣,给公司做了一个小项目,一个瀑布流+动态视频控制的DEMO,有需要的可以联系我,公司的项目就不对外展示了(一个后端程序员真的要干前端了哈哈哈)。 书接上文,昨天正式的开始了Vue的代码的学习,简单的通过一些假的数据来展示了下个人博客的首页列表,不知道大家是否还记得昨天讲的什么,如果不太清楚呢,可以再回顾下《[从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 十七 ║Vue基础:使用Vue.js 来画博客首页(一)](https://www.jianshu.com/p/067493c96a53)》,我们主要说到了,Vue的核心语法是什么,MVVM体现在哪些方面,如何简单安装Vue环境(通过直接引用Vue.js文件的形式),以及常用的十天指令的前五个,并且通过讲解,咱们做了一个小DEMO,就是一个个人的博客系统的首页(这里是盗取网友的一个样式,因为我看着着实比较喜欢哈哈)。其实主要记住一点,用Vue这类MVVM框架开发,一定要摆脱之前的DOM操作的习惯,改成操作数据来控制View,如果你Vue这一块会的话,那学习微信小程序开发就是分分钟的事儿,嗯~~~ 关于后边的实战环节,我还没有确定要用什么样式的博客,如果大家看到有好的,可以留言下,咱们以它为基础可以扩展,如果不行的话,我就自己写一个简单的吧,当然还是那句话,我只是一个抛砖引玉的作用,也给看到这篇文章的小伙伴们一丢丢的动力,这个时候要说下QQ群里的小伙伴,都已经开始用Vue,配合着前边的教程和自己的.Net Core项目进行开发页面了,我感觉也是很开心的!至少可以在平时的时间,一起学点儿东西也是不错的。虽然不能手把手吧,但是一些建议还是尽量给提问题的小伙伴的。哈哈,{{ 强硬收回话题 }},今天我们接着上一篇的内容,继续往下走,主要是:把 基本指令 介绍完,说下计算属性和侦听器,Class 与 Style 绑定,主要是这三部分,在博客页面上设计 添加文章,删除文章,筛选文章等功能。 零、今天完成左下角浅蓝色的部分 一、VUE 常用的一些指令总结 ( 下 ) 1、补充下 v-once 指令 —— 禁止修改 看名字就可以知道,这个是 一次 的意思,也就是说在第一次渲染以后,后期的无论数据的如何修改都不会影响该节点,只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。 注意:但是,我想说的是,这个指令一般情况不要使用,除非是含有大量的静态数据,不想每次加载的时候占用时间,如果过多的使用该指令会出现很多意想不到的问题,因为数据的不刷新,不适合刚入门的小伙伴使用。 2、v-bind ( : )指令 —— 动态属性 在昨天的博客首页的联系中,我们其实已经用到了这个指令,大家应该没有注意到,就是文章列表陪着 href 属性的时候。 v-bind 指令的作用和用法,它用于动态绑定DOM元素的属性,比较常见的比如:标签的href属性,标签的src属性。 <ul class="post-list non-style-list"> <li v-for='item in list' class="post-list-item"> <!--这里用到了 v-bind 指令--> <a v-bind:href="'https://www.cnblogs.com/laozhang-is-phi/p/'+ item.id +'.html'">{{item.name}}</a> <!--还可以这样写 这里的linkUrl 是一个变量--> <a :href="linkUrl">{{item.name}}</a> <span class="post-list-date">({{item.date}})</span> </li> </ul> 还可以:动态地绑定一个或多个特性,或一个组件 prop 到表达式(这里要记得,是动态的绑定,就是指在特性中存在变量)。 <!-- 绑定一个有属性的对象 --> <div v-bind="{ id: someProp, 'other-attr': otherProp }"></div> <!-- 通过 prop 修饰符绑定 DOM 属性 --> <div v-bind:text-content.prop="text"></div> 3、v-on ( @ )指令——事件触发 绑定事件监听器。事件类型由参数指定。表达式可以是一个方法的名字或一个内联语句,如果没有修饰符也可以省略。 用在普通元素上时,只能监听原生 DOM 事件。用在自定义元素组件上时,也可以监听子组件触发的自定义事件。相当于绑定事件的监听器,绑定的事件触发了,可以指定事件的处理函数。 我们可以简单说个栗子,在我们的博客首页的头像上,增加一个点击事件(就是之前的click事件), <div v-on:click="alert('我是老张的哲学的头像')"> </div> //注意 v-on 可以用@代替,比如 //<div @:click="alert('我是老张的哲学的头像')"> </div> 在我们的页面里,我们可以用来触发:添加、筛选功能 a、新建一个 input 标签,添加一个 回车 事件 <input @keydown.enter="addArticle" type="text" class="edit" placeholder="按回车添加文章"> b、在 vue 实例的 methods 中,统一添加我们的 addArticle 方法。 //我们的方法都统一写到这里 methods: { //添加事件 addArticle: function () { //将文章存入list数组,注意 this指向! //将数据反转 this.list = this.list.reverse(); this.list.push(this.task); this.list = this.list.reverse(); //存入list[]后,重置task this.task = { name: '',//内容为空 id: 1000, date: " Just Now ", finished: false,//未完成 deleted: false//未删除 } } }, c、这个时候,差最后一步,就是获取 input 的指(这个时候可千万不要再像以前那样,根据id来获取结果了) 还记得咱们前几帐将基本语法和Vue的核心功能的时候,说到了其中的一个很大的特性就是数据驱动 —— 双向数据绑定,不仅我们可以给 Data 赋值,还可以通过在 DOM 操作的时候,将指获取到 Data,没错就是下边的这个指令,v-model。 4、v-model 指令 —— 双向数据绑定 这是一个我认为很重要,也是经常使用到的指令,主要是表单操作,它可以很容易的实现表单控件和数据的双向绑定,相对以前的手动更新DOM,这个上边也说到了。 在之前的 input 输入框中,添加 v-model 指令 <input @keydown.enter="addArticle" type="text" class="edit" v-model="task.name" placeholder="按回车添加文章"> 这个时候,我们的博客添加的功能就好了(当然现在是最低端最low的,只是为了讲解 v-model 指令用,博客添加到时候会用 express 后台管理)。 好啦,常用的 vue 指令已经讲解完成,还有其他的一些不常用的几个大家可以用到的时候了解下。 二、计算属性 Computed 1、计算属性的原理 在模板内使用表达式很便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如: <div id="example"> {{ message.split('').reverse().join('') }} </div> 在这个地方,模板不再是简单的声明式逻辑。你必须看一段时间才能意识到,这里是想要显示变量 message 的翻转字符串。当你想要在模板中多次引用此处的翻转字符串时,就会更加难以处理,然后如果大量的使用这样的表达式,会使得整个页面不仅不好看,还很繁重。 所以,对于任何复杂逻辑,你都应当使用计算属性。 就比如上边的栗子,我们就可以写成这样: <div id="example"> //1、这里是我们在 computed中定义的值,而不是在data中 <p>message: "{{ reversedMessage }}"</p> </div> var vm = new Vue({ el: '#example', data: { message: 'Hello' }, computed: { // 计算属性的 getter reversedMessage: function () { // 注意 !`this` 指向 vm 实例 return this.message.split('').reverse().join('') } } }) 这样看起来就清晰明了,减轻页面的负重。这里你可以会好奇,这就像一个data的中间件一样,不用做任何的其他操作,都可以实现这个逻辑,就好像data的影子一样,没错!计算属性就是一个getter器。 你可以像绑定普通属性一样在模板中绑定计算属性。Vue 知道 vm.reversedMessage 依赖于 vm.message,因此当 vm.message 发生改变时,所有依赖 vm.reversedMessage 的绑定也会更新。而且最妙的是我们已经以声明的方式创建了这种依赖关系:计算属性的 getter 函数是没有副作用 (side effect) 的,这使它更易于测试和理解。 2、知道了他的原理和如何使用,那么我们就可以在我们的项目中使用 计算属性 来达到我们的动态查询文章的功能 我们首先添加一个计算属性来过滤我们的文章list数据 //通过计算属性过滤数据 computed: { listSearch: function () { //为什么要存这个this呢,因为filter过滤器会改变this的指向 let that = this; return this.list.filter(function (item) { //简单的 判断文章name是否包含 input中的值,因为双向绑定,所以也就是 task.name return item.name.indexOf(that.$data.task.name) >= 0; }); } } 接下来,我们就需要把我们的计算属性 listSearch 替换掉view中的 list,从而达到过滤: <li v-for='item in listSearch' class="post-list-item"> <span class="post-list-date">({{item.date}})</span> </li> 最后我们可以看看效果: 注意:计算属性默认只有 getter ,不过在需要时你也可以提供一个 setter : computed: { fullName: { // getter get: function () { return this.firstName + ' ' + this.lastName }, // setter set: function (newValue) { var names = newValue.split(' ') this.firstName = names[0] this.lastName = names[names.length - 1] } } } 现在再运行 vm.fullName = '老张 哲学' 时,setter 会被调用,vm.firstName 和 vm.lastName 也会相应地被更新。 三、侦听器 (不建议多使用) 虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的(这个要强调,是异步操作,或者开销较大的操作)。 在这里,我们监听下我们的 input 输入的数据变化,也就是 task.name 的值 一般的写法是这样的: new Vue({ data: { author: "老张的哲学", task: { name: '',//内容为空 id: 100, date: " Just Now ", finished: false,//未完成 deleted: false//未删除 }, }, watch: { author: function (newval, oldVal) { console.log("author 变化辣,") } } }) 但是在我们的栗子中,是监听一个对象的中某个属性,也就是 task.name, 所以我们就会这么写: watch: { task.name() { //这里面可以执行一旦监听的值发生变化你想做的操作 } }, 但是,这样写是不符合规则的,必须是一个变量,因此会报错: 所以我们就需要还是用到 计算属性来定义,还记得计算属性是干什么的么,它就像一个数据的中间件,把原始数据再封装一下, 那正好,我们可以把 task.name 给封装下,最终会是这样的: new Vue({ data: { author: "老张的哲学", task: { name: '',//内容为空 id: 100, date: " Just Now ", finished: false,//未完成 deleted: false//未删除 }, }, watch: { author: function (newval, oldVal) { console.log("author 变化辣,") }, nameCpt() : function (newval, oldVal) { console.log("task.name 变化辣,") }, }, //通过计算属性来操作我们需要用到的任何数据 computed: { nameCpt() { return this.task.name } } }) 注意:虽然Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性。但是监听是特别浪费资源的,当我们有一些数据需要随着其它数据变动而变动时,我们很容易滥用 watch,因此通常更好的做法是使用计算属性而不是命令式的 watch 回调。 四、匆匆结语 今天时间晚了些,动态Class 与 Style 绑定没有说到,那我们就下次再说吧!今天呢,我们主要说了常用的指令,主要的是 v-model、v-bind、v-on三个指令,然后还说了计算属性和侦听器,我在开发的过程中,计算属性是使用较多的,但是某些时候,watch 侦听器会发挥不一样的作用!好啦,下次咱们继续说说 动态Class 与 Style 绑定 和 很重要的 生命周期讲解 吧。 五、CODE https://github.com/anjoy8/Blog.Vue
缘起 昨天说到了《[从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 十五 ║ Vue前篇:JS对象&字面量&this](https://www.cnblogs.com/laozhang-is-phi/p/9580807.html)》,通过总体来看,好像大家对这一块不是很感兴趣,嗯~~这一块确实挺枯燥的,不能直接拿来代码跑一下那种,不过还是得说下去,继续加油吧!如果大家对昨天的小demo练习的话,相信现在已经对JS的面向对象写法很熟悉了,如果嵌套字面量定义函数,如何使用this关键字指向。今天呢,主要说一下ES6中的一些特性技巧,然后简单说一下模块化的问题,好啦,开始今天的讲解~ 还是老规矩,一言不合就是上代码 str1 = 'Hello JS!'; function fun1() { var str1 = 'Hello C#!'; } fun1(); alert(str1); 大家猜猜,最后会弹出来哪一句话? 零、今天要完成浅紫色的部分 一、什么是传说中的ES6 这些定义网上一大堆,不过还是粘出来,大家可以统一看一下,简单了解了解: 1、定义 ECMAScript 6是JavaScript语言的下一代标准,在2015年6月正式发布。它的目标,是使得JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。 标准的制定者有计划,以后每年发布一次标准,使用年份作为标准的版本。因为当前版本的ES6是在2015年发布的,所以又称ECMAScript 2015。也就是说,ES6就是ES2015,下一年应该会发布小幅修订的ES2016。 2、有哪些新的变化 编程语言JavaScript是ECMAScript的实现和扩展,由ECMA(一个类似W3C的标准组织)参与进行标准化。ECMAScript定义了: 语言语法 – 语法解析规则、关键字、语句、声明、运算符等。 类型 – 布尔型、数字、字符串、对象等。 原型和继承 内建对象和函数的标准库 – JSON、Math、数组方法、对象自省方法等。 ECMAScript标准不定义HTML或CSS的相关功能,也不定义类似DOM(文档对象模型)的Web API,这些都在独立的标准中进行定义。ECMAScript涵盖了各种环境中JS的使用场景,无论是浏览器环境还是类似node.js的非浏览器环境。 3、ECMAScript和JavaScript的关系 1996年11月,JavaScript的创造者Netscape公司,决定将JavaScript提交给国际标准化组织ECMA,希望这种语言能够成为国际标准。次年,ECMA发布262号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为ECMAScript,这个版本就是1.0版。 该标准从一开始就是针对JavaScript语言制定的,但是之所以不叫JavaScript,有两个原因。一是商标,Java是Sun公司的商标,根据授权协议,只有Netscape公司可以合法地使用JavaScript这个名字,且JavaScript本身也已经被Netscape公司注册为商标。二是想体现这门语言的制定者是ECMA,不是Netscape,这样有利于保证这门语言的开放性和中立性。 因此,ECMAScript和JavaScript的关系是,前者是后者的规格,后者是前者的一种实现 二、var、let 与 const 块作用域 这里先说下,作用域的问题 1、ES6之前,JavaScript 并没有块级作用域,所谓的块,就是大括号里面的语句所组成的代码块,比如 function blog(bl) { if (bl) { var foo = "Blog"; } console.log(foo); } blog(true); //=> Blog 2、虽然变量变量foo 位于 if 语句的代码块中,但是 JavaScript 并没有块级作用域的概念,因此被添加到了当前的执行环境 - 即函数中,在函数内都可以访问到。 因此:var 定义的变量是函数级作用域,作用范围是在函数开始阶段和函数执行完成之前内都是存在的; 并且如果该函数内部还存在匿名函数等特殊函数,这个 var 出的变量在匿名函数中任然可以用; 3、在ES出现后,定义了一个新的命名方式 let function Blog(bool) { if (bool) { let foo = "Blog"; } else { console.log(foo); } } Blog(false); //这里会报错 Uncaught ReferenceError: foo is not defined 因此,使用 let,上述问题完全解决,let出的变量作用域是 块作用域,在离开某一代码块,该变量就会被销毁不存在 应当尽可能的避免用 var,用 let 来代替,除非你需要用到变量提升。 4、随着面向对象思维的出现,JS也出现了常量的定义 const const 与 let 的基本用法相同,定义的变量都具有块级作用域,也不会发生变量提升。不同的地方在于,const 定义的变量,只能赋值一次。 const foo='Blog'; function Blog(bool) { if (bool) { foo = "Vue"; } else { console.log(foo); } } Blog(true); //这里会报错 Identifier 'foo' has already been declared 因此const多用作不发生变化的变量定义,比如定义月份,或者,星期等:const months = []; 三、箭头函数 还记得昨天的那个小demo么,今天再说一个地方 var obj = { data: { books: "", price: 0, bookObj: null }, bind() {//**注意!**ES6 中,可以使用这种方法简写函数,等价于 bind: function () { var that = this; //普通函数 //$(".ok").click(function () { // console.log(this);//这个时候,this,就是 .ok 这个Html标签 // var bookItem = that.data.bookObj; // var _parice = $(bookItem).data("price"); // var _book = $(bookItem).data("book"); // that.data.books += _book + ","; // that.data.price += parseInt(_parice); // that.show(); //}); //箭头函数 $(".ok").click(() => { var bookItem = this.data.bookObj;//在箭头函数中,this指向的是定义函数时所在的对象 var _parice = $(bookItem).data("price"); var _book = $(bookItem).data("book"); this.data.books += _book + ","; this.data.price += parseInt(_parice); this.show(); $(".bg,.popupbox").hide(); }); }, } 在普通的click函数中 this 指向对象 $(".ok") ,因此,我们如果想要获取定义的对象中的数据(obj.data),那我们只能在 click 方法前,就去用一个 that 自定义变量来保存这个 this , 但是在箭头函数中就不一样了,this 始终指向定义函数时所在的对象(就是 obj 对象); 是不是更方便些! 在Vue中,也经常使用 vue实例,或者this来获取相应的值 var vm = new Vue({ el:'#root', data:{ tasks:[] }, mounted(){ axios.get('/tasks') .then(function (response) { vm.tasks = response.data;//使用Vue实例 }) }, mounted2(){ axios.get('/tasks') .then(response => this.tasks = response.data);//箭头函数 this } }); 四、参数默认值 && rest参数 1、 在ES6中,可以像C#那样定义默认参数 function buyBook(price, count = 0.9){ return price * count; } buyBook(100); //甚至可以将方法的值赋给参数 function buyBook(price, count =GetCount()){ return price * count; } function GetCount(){ return 100; } buyBook(200); 2、不仅如此,还可以快速获取参数值 //ES6之前是这样的 function add(a,b,c){ let total = a + b + c; return total; } add(1, 2, 3); //ES6你可以这么操作,提供了 rest 参数来访问多余变量 function sum(...num) { let total = 0; for (let i = 0; i < num.length; i++) { total = total + num[i]; } return total; } sum(1, 2, 3, 4, 6); 五、ES6中的表达式 1、字符串表达式 在之前我们都是这样使用字符串表达式 var name = 'id is ' + bid+ ' ' + aid + '.' var url = 'http://localhost:5000/api/values/' + id 在ES6中我们有了新语法,在反引号包裹的字符串中,使用${NAME}语法来表示模板字符: var name = `id is ${aid} ${bid}` var url = `http://localhost:5000/api/values/${id}`//注意是反引号,英文输入下下的,Tab键上边的那个 2、还有就是多行表达式的写法 //之前我们都是这么写的 var roadPoem = '这个是一个段落' + '换了一行' + '增加了些内容' + 'dddddddddd' //但是在ES6中,可以使用反引号 var roadPoem = `这个是一个段落 换了一行 增加了些内容, dddddddddd 结尾,` 六、模块化定义 1、什么是模块化开发 模块化开发是基于一定的语法规范,通过代码书写设计,使代码耦合度降低,模块化的意义在于最大化的设计重用,以最少的模块、零部件,更快速的满足更多的个性化需求。因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。 用阮一峰大神的说法就是: 今天的Web网页越来越像桌面程序,网页上加载的javascript也越来越复杂,前端工程师不得不开始用软件工程的思维去管理自己的代码。Javascript模块化编程,已经成为一个非常迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。但是,Javascript不是一种模块化编程语言,它不支持"类"(class),更别提"模块"(module)了。(正在制定中的ECMAScript标准第六版将正式支持"类"和"模块",但还需要很长时间才能投入实用 就这样,Node.js 就出现了,一个用来开发服务器端的js框架,基于commonJs的模块化。当然中间还有CMD,AMD(这个东西我还需要慢慢研究下); 2、模块化在代码中是如何体现的呢 1、首先我们先看看普通的定义一个类是如何写的 新建一个index.js 文件 class Student { constructor(homework= []) { this.homework= homework; } study() { console.log(this.homework); } } const st = new Student ([ 'blog', 'api', 'vue' ]); st.study(); 然后新建一个index.html页面,引用该js文件 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <script src="index.js"></script> </body> </html> 然后就可以得到结果: 这是一个很简单的,定义一个Student 类,然后定义一个方法,通过传递一个数组参数,来实例化。 这样虽然很简单,但是却无法复用,无法作为一个零件来使用。而且如果有一个地方要修改,多处都需要修改,这个面向对象的思想,没有发挥出来; 这个时候你可能会说,把这个拆成两个问题,就可以复用了,嗯~试试 2、我们把这两个文件分开 新建一个Student.js ,定义处理Student类;然后新建一个main.js方法,来调用实例化该类,就可以使用 然后在 index.html 页面里去引用这两个文件 <body> <script src="Student.js"></script> <script src="main.js"></script> </body> 当然结果是一样的,这样虽然实现了分隔开,也可以去不同的地方调用; 但是,从上文中你也看的出,如果不是自己写的代码,一般不太容易看的明白,到底是什么意思,直观性不是很好,我们将无法看到彼此间的关联(main.js 加载 Student.js), 3、我们用模块的写法设计这个调用 ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。 我们直接修改之前的代码 然后在 index.html 页面中,只需要引用 就行 4、因为浏览器现在还不能直接运行模块化代码,所以我们需要打包,打包工具有很多,比如 webpack 注意:这里用到打包概念,之后会讲到,这里就先略过,以后会讲到,步骤是 首先安装npm,或者阿里镜像 cnpm(npm其实是Node.js的包管理工具,这个在我们之后的Node.js环境配置中,自动随带安装)全局安装 rollup.js $ cnpm install --global rollup cd 当前文件夹 $ rollup main.js --format iife --output bundle.js 然后只需要引用生成的 5、这里我因为测试,已经生成好了,打包出来的bundle.js 是这样的,是不是兜兜转转又回到了之前的写法,其实ES6的模块开发,就是导入的代码块儿 (function () { 'use strict'; class TaskCollection { constructor(tasks = []) { this.tasks = tasks; } dump() { console.log(this.tasks); } } const tc = new TaskCollection([ 'blog', 'api', 'vue' ]); tc.dump(); }()); 总结来说:模块化的好处和问题 可维护性 灵活架构,焦点分离 方便模块间组合、分解 方便单个模块功能调试、升级 多人协作互不干扰 可测试性,可分单元测试; 性能损耗 系统分层,调用链会很长 模块间通信,模块间发送消息会很耗性能 其实说白了,就是JS在作为一个开发语言来说,越来越靠近了后端服务器语言。 七、每天一个小Demo 这里是一个特别特别简单的关于ES6的留言板,大家可以看一看 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h2>简易留言板</h2> <input type="text" placeholder="请输入内容" size="30" id="msg"> <input type="button" value="留言" id="btn"> <div id="msg-div"></div> <script> //let 定义块级变量 let oBtn = document.getElementById('btn'); let msg = document.getElementById('msg'); let content = document.getElementById('msg-div'); oBtn.onclick = () => { let ovalue = msg.value; let ali = document.createElement('p'); //ES6模板字符串 //多行表达式 ali.innerHTML = `${ovalue}<span style="color:red;"> 删除</span>`; var aspan = content.getElementsByTagName('p'); if (aspan.length > 0) { content.insertBefore(ali, aspan[0]); } else { content.appendChild(ali); } msg.value = ''; var oSpan = content.getElementsByTagName('span'); for (let i = 0; i < oSpan.length; i++) { //ES6箭头函数 oSpan[i].onclick = function () { content.removeChild(this.parentNode);//注意this的指向 }; } }; </script> </body> </html> 八、结语 通过这两天的学习,大家了解到了,JS的一些特性和变化:嵌套字面量的定义,面向对象的封装,类和模块化的使用,ES6的日益成熟,通过打包进行发布等等,都能表现出JS在向一个服务器端语言快速迈进的冲动,也是极大的推动了,MVVM的到来,从而实现像Node.js 这种,可以脱离浏览器环境也能运行的不一样视角。好啦,关于JS高阶,这两讲已经差不多了,当然还有其他的,大家可以自行学习了解,其实这两篇都懂的化,已经差不多了,明天咱们就开始正式进入Vue入门篇,通过引用Vue.js 实现栗子。
缘起 书接上文《[从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 十四 ║ VUE 计划书 & 我的前后端开发简史](https://www.cnblogs.com/laozhang-is-phi/p/9577805.html)》,昨天咱们说到了以我的经历说明的web开发经历的几个阶段,而且也说到了Vue系列需要讲到的知识点,今天就正式开始Code,当然今天的代码都特别简单,希望大家慢慢的学习,今天主要讲的是JS高级——关于面向对象的语法。 磨刀不误砍柴工,当然,我在写这篇之前,或者是写Vue之前,都在考虑要从何处入手,怎么写,因为大家肯定有一部分是全栈工程师,都很懂,那我写的您就可以一目十行,不用看代码,也有一部分还是专注于后端,前端只是会一些Javascript,或者Jquery,进行一些Dom操作等,最后还有一部分小伙伴是CS模式开发的,我们的QQ群我就了解到一些,所以呢,我就去各个平台看大家是如何讲Vue,或者是讲MVVM前后端分离的,嗯~~~,我也都能看懂,但是就是不想和他们一样 [ 这样会拉仇恨么哈哈 ],当然,不是说我写的好,因为好多人都是文章直接一上来就Vue-cli搞起来,直接Hello World跑起来,然后就是搞一个TodoList,基本的就这么过去了,感觉既然是写了,就要从基础写起,所以我就想到了先从JS面向对象讲起,做个铺垫,也给大家一个缓冲。大家放心,Vue-cli,Vue-router,Axios,Vuex等等,咱们都有,还有hello world,哈哈哈哈! 这个时候,一定会有好多小伙伴问,既然是Vue,为什么要说JS呢,请看我写的简单的代码(这是Vue定义一个组件的语法): 不要怕看不懂,因为过一段时间就会了 <script> import myHeader from '../components/header.vue' import myFooter from '../components/footer.vue' export default { components: {myHeader, myFooter}, data() { return { id: this.$route.params.id, dat: {}, isShow: true } }, created() { this.getData() }, methods: { getData() { var that = this this.$api.get('Blog/Get/' + this.id, null, r => { r.data.bCreateTime = that.$utils.goodTime(r.data.bCreateTime) this.dat = r.data this.isShow = false }) } }, watch: { '$route'(to, from) { this.dat={} this.isShow = true this.id = to.params.id this.getData() } } } </script> 零、今天要完成浅绿色的部分 一、JS和C#一样,一切都是对象 1、关于JS函数对象方法的写法,目前有两种,不知道大家平时喜欢用哪种,请看: //这种是直接函数声明的方法 var id=9; var brandid=1; GetGoods(); GetDetail(); GetUsers(); function GetGoods() { var tmplTrips = new StringBuilder(); $('#DifCountry .cur').removeClass("cur"); //... } function GetDetail() { var tmplTrips = new StringBuilder(); $('#DifCountry .cur').removeClass("cur"); //... } function GetUsers() { var tmplTrips = new StringBuilder(); $('#DifCountry .cur').removeClass("cur"); //... } //这一种是对象的写法 Object.defineProperties(a, { "property": { set property(newValue) { console.log("set property"); return this._property = newValue; }, get property() { console.log("getgsd property"); return this._property; }, enumerable: true, configurable: true }, "name":{ value: "maotr", writable: true, enumerable: true, configurable: true } }); 2、对象包含哪些元素 包含 属性,方法,数据类型(字符串,数字,布尔,数组,对象),在C#中,随随便便就是写出几个对象,JS中也是如此。 二、定义对象的四种方式 1、通过new关键字 格式:new Object(); var obj=new Object(); obj.Name='blog'; obj.address='beijing'; 2、嵌套字面量的方法,键值对的形式 格式:objectName = {property1:value1, property2:value2,…, propertyN:valueN} 。property是对象的属性 这个我使用的较多,只要是用来POST传值的时候。 var obj = { name: 'blog', address: 'beijing', say: function () { alert("hello world"); } } 3、函数表达式,和函数声明很类似 注意!函数表达式必须先定义,再使用,有上下顺序之分;而函数声明没有。 //函数声明(定义和使用没有先后之分) function obj(){ alert('hi,我是函数声明'); } //函数表达式(必须先定义声明,再使用) var obj=function(){ //other things... alert('hi,我是函数表达式方法') } 4、构造函数声明 function Blog(title,font) { this.title = title, this.font = font, this.read = function () { alert('Hi ,Blog'); } } var blog1 = new Blog('JS学习', 100); alert(blog1.title); blog1.read(); 总体来说, 构造函数,和普通函数都是平时用的较多的写法 //构造函数 function Egperson (name,age) { this.name = name; this.age = age; this.sayName = function () { alert(this.name); } } var person = new Egperson('mike','18'); //this-->person person.sayName(); //'mike' //普通函数 function egPerson (name,age) { this.name = name; this.age = age; this.sayName = function () { alert(this.name); } } egPerson('alice','23'); //this-->window window.sayName(); //'alice' 但是要注意,构造函数和普通函数的区别: 1、在命名规则上,构造函数一般是首字母大写,普通函数则是遵照小驼峰式命名法 2、构造函数内部的this指向是新创建的person实例,而普通函数内部的this指向调用函数的对象(如果没有对象调用,默认为window) 3、构造函数内部会创建一个实例,调用普通函数时则不会创建新的对象 这个时候你一定会问了,看着两种方法很相似呀??? 这里引用网友的说法: 我感觉创造实例的原因是因为new ,而不是因为他是“构造函数”,构造函数的名称首字母大写是约定。浏览器并不会因为函数的首字母是否大写来判断这个函数是不是构造函数。普通函数也通过New创建也依旧可以打上构造函数的效果。如果不想用new,也可以加一个return语句。补充:构造函数返回都是对象。也可以在构造函数中显示调用return.如果return的值是一个对象,它会代替新创建的对象实例返回。如果返回的值是一个原始类型,它会被忽略,新创建的实例(对象)会被返回。 三、剪不断理还乱的 this关键字 1、这里有一个常见面试题,相信有很多人会乱: function foo() { var f2 = new foo2(); console.log(f2); //{a: 3} console.log(this); //window return true; } function foo2() { console.log(this); //foo2类型的对象 不是foo2函数 return { a: 3 }; } var f1 = foo(); console.log(f1); // true 大概思路是这样的; 1、f1调用foo(),然后foo(),又实例化对象foo2,所以,这个时候,f2就是一个foo2()的一个对象,被打印出来; 2、然后foo2(),返回值一个对象{a:3},被赋值给了f2,被打印出来; 3、这个this,就是调用者,也就是window; 4、这个时候foo()的返回值 true,被赋值给f1,被打印出来; 2、深入理解 js this关键字 栗子1: var name = 'blog.vue'; function showName(){ return this.name; } alert(showName()); //blog.vue 大家都知道是指向window全局对象的属性(变量)name,因为调用showName()实际上就相当于window.showName(),所以this指向window对象。 这里看着有些简单,但是如果复杂点儿呢,所以这里先引入三个概念: 1、函数名是一个指向函数的指针。 2、函数执行环境(this什么时候初始化):当某个函数第一次被调用时,会创建一个执行环境,并使用this/arguments和其他命名参数的值来初始化函数的活动对象。 3、初始化指向谁:在Javascript中,this关键字永远都指向函数(方法)的所有者。 栗子2 var name = 'The window'; var object = { name:'The object', getNameFunc:function(){ return object1.getNameFunc(); } }; var object1 = { name:'The object1', getNameFunc:function(){ return this.name; } }; alert(object.getNameFunc());//The object1 说明:object1.getNameFunc()相当于是调用object1对象的getNameFunc方法,此时会初始化this关键字,而getNameFunc方法的所有者就是object1,this指向object1,所以调用object1.getNameFunc()返回的是object1的name属性,也就是"The object1"。 3、总结 函数的几种调用方式: 1.普通函数调用 2.作为方法来调用 3.作为构造函数来调用 4.使用apply、call方法来调用 5.Function.prototype.bind方法 6.es6箭头函数 请记住一点:谁调用这个函数或方法,this关键字就指向谁。 如果你对this以及函数的嵌套字面量有一定的了解了,可以看看下边这个小Demo。 四、面向对象实例Demo 1、我简单做了一个小Demo,大家可以看看,当然也可以自己动手试试,特别简单的,就是鼠标滑过,显示按钮,弹窗。 这个代码我已经到了Git上,地址是,以后每次的小Demo我都会放上去,以备不时之需,Git代码在文末。 var obj = { data: { books: "", price: 0, bookObj: null }, init: function () { this.bind(); this.popup(); this.show(); }, show: function () { $(".content-title").html(""); $(".content-title").html(this.data.books); $(".content-price").html(this.data.price); }, bind: function () { var that = this; $(".content .content-list ").hover(function () { $(this).find(".see").show(); }, function () { $(this).find(".see").hide(); }); $(".see").click(function () { $(".bg,.popupbox").show(); that.data.bookObj = $(this); }); $(".btn3,.cancel").click(function () { $(".bg,.popupbox").hide(); }); $(".ok").click(function () { var bookItem = that.data.bookObj; var _parice = $(bookItem).data("price"); var _book = $(bookItem).data("book"); that.data.books += _book + ","; that.data.price += parseInt(_parice); that.show(); }); }, popup: function () { var box = $(".popupbox"); var _width = document.documentElement.clientWidth; var _height = document.documentElement.clientHeight; var $width = (_width - box.width()) / 2; var $height = (_height - box.height()) / 2; box.css({ "left": $width, "top": $height }); }, watch: { } }; $(function () { obj.init(); }); 这个栗子,就是我们上文中的第二节的第2个方法——通过定义嵌套字面量的形式来写的,只不过稍微复杂一丢丢(颜值不好,大家多担待)。 这个时候,你再把这个代码和上文的开篇的那个Vue写法对比下:哦!是不是有点儿相近的意思了。嗯!如果你找到了其中的类似处,那恭喜,你已经基本入门啦! 2、单向数据传输 从上边的代码和动图你应该能看到,目前这种开发都是单向数据的(是通过Data来控制Dom的展示的),那如何通过操作Dom来控制Data呢?诶~如果你能这么想,那Vue已经事半功倍了。 其实MVVM的核心功能之一就是双向数据传输。 五、结语 今天也是简简单单的说了一下,面向对象的一些知识,还有就是如何定义嵌套字面量,this到底如何归属的,讲的稍微浅显了一些,后期还需要大量内容丰富,如果你自己写一遍,你应该已经掌握了如何定义一个对象方法了。这样可以为以后学习Vue打下基础。好啦今天的讲解暂时到这里辣~明天继续JS高级的属性获取和ES6的一些内容。 六、CODE https://github.com/anjoy8/Blog.Vue
代码已上传Github+Gitee,文末有地址 番外:时间真快,今天终于到了系统打包的日子,虽然项目还是有很多问题,虽然后边还有很多的内容要说要学,但是想着初级基本的.Net Core 用到的基本至少就这么多了(接口文档,项目框架,持久化ORM,依赖注入,AOP,分布式缓存,CORS跨域等等),中高级的,比如在Linux高级发布,Nginx代理,微服务,Dockers等等,这个在以后的更新中会慢慢提到,不然的话,Vue就一直说不到了 [哭笑哈哈],其实我还有很多要总结的,比如 Power BI系列(没用过的点击看看),比如C#7.0系列等文章,都在慢慢酝酿中,希望能坚持下来,不过这两个系列目前还不会写到,如果有需要用或学微软Power BI(https://docs.microsoft.com/zh-cn/power-bi/sample-customer-profitability)的,可以加QQ群联系我,我在微软项目中已经用到了。还是打算从下周一开始转战Vue的文章,当然后端也会一直穿插着,这里要说下,我们的QQ群已经有一些小伙伴了,每天可以一起交流心得和问题,感觉还是很不错的,如果你有什么问题,或者其他技术上的需要讨论,咱们的群是可以试试哟,我和其他小伙伴会一直在线给大家解答(咋感觉像一个广告哈哈,大家随意哈)。 正传:好啦,书接上文,昨天说到了《从壹开始前后端分离【 .NET Core2.0 +Vue2.0 】框架之十二 || 三种跨域方式比较,DTOs(数据传输对象)初探》,因为下午时间的问题,只是讲解了四种跨域方法,没有讲解完DTO,其实这个东西很简单,说白了,就是把两个实体类进行转换,不用人工手动去一一赋值,今天呢,就简单说下常见DTO框架AutoMapper的使用,然后做一个打包处理,发布到我的windows服务器里,今天刚刚买了一个Ubuntu Linux服务器,因为如果开发.Net Core,一定会接触到Linux服务器,等各种,因为它跨域了,就是酱紫。但是还没有配置好,所以会在下边留下位置,慢慢补充在Ubuntu部署的讲解。 零、今天完成右下角的深蓝色部分 一、在项目中使用添加一个案例使用AutoMapper 1、在接口 IBlogArticleServices.cs和 类BlogArticleServices.cs中,添加GetBlogDetails()方法,返回类型是BlogViewModels 请看这两个类 /// <summary> /// 博客文章实体类 /// </summary> public class BlogArticle { /// <summary> /// /// </summary> public int bID { get; set; } /// <summary> /// 创建人 /// </summary> public string bsubmitter { get; set; } /// <summary> /// 博客标题 /// </summary> public string btitle { get; set; } /// <summary> /// 类别 /// </summary> public string bcategory { get; set; } /// <summary> /// 内容 /// </summary> public string bcontent { get; set; } /// <summary> /// 访问量 /// </summary> public int btraffic { get; set; } /// <summary> /// 评论数量 /// </summary> public int bcommentNum { get; set; } /// <summary> /// 修改时间 /// </summary> public DateTime bUpdateTime { get; set; } /// <summary> /// 创建时间 /// </summary> public System.DateTime bCreateTime { get; set; } /// <summary> /// 备注 /// </summary> public string bRemark { get; set; } } ------------------------------------------------- /// <summary> /// 博客信息展示类 /// </summary> public class BlogViewModels { /// <summary> /// /// </summary> public int bID { get; set; } /// <summary>/// 创建人 /// </summary> public string bsubmitter { get; set; } /// <summary>/// 博客标题 /// </summary> public string btitle { get; set; } /// <summary>/// 摘要 /// </summary> public string digest { get; set; } /// <summary> /// 上一篇 /// </summary> public string previous { get; set; } /// <summary> /// 上一篇id /// </summary> public int previousID { get; set; } /// <summary> /// 下一篇 /// </summary> public string next { get; set; } /// <summary> /// 下一篇id /// </summary> public int nextID { get; set; } /// <summary>/// 类别 /// </summary> public string bcategory { get; set; } /// <summary>/// 内容 /// </summary> public string bcontent { get; set; } /// <summary> /// 访问量 /// </summary> public int btraffic { get; set; } /// <summary> /// 评论数量 /// </summary> public int bcommentNum { get; set; } /// <summary>/// 修改时间 /// </summary> public DateTime bUpdateTime { get; set; } /// <summary> /// 创建时间 /// </summary> public System.DateTime bCreateTime { get; set; } /// <summary>/// 备注 /// </summary> public string bRemark { get; set; } } 两个实体类字段还基本可以,不是很多,但是我曾经开发一个旅游网站的系统,有一个表字段都高达30多个,当然还有更多的,额,如果我们一个个赋值是这样的 BlogViewModels models = new BlogViewModels() { bsubmitter=blogArticle.bsubmitter, btitle = blogArticle.btitle, bcategory = blogArticle.bcategory, bcontent = blogArticle.bcontent, btraffic = blogArticle.btraffic, bcommentNum = blogArticle.bcommentNum, bUpdateTime = blogArticle.bUpdateTime, bCreateTime = blogArticle.bCreateTime, bRemark = blogArticle.bRemark, }; 所以这个方法的全部代码是: 接口层也要添加: public interface IBlogArticleServices :IBaseServices<BlogArticle> { Task<List<BlogArticle>> getBlogs(); Task<BlogViewModels> getBlogDetails(int id); } /// <summary> /// 获取视图博客详情信息 /// </summary> /// <param name="id"></param> /// <returns></returns> public async Task<BlogViewModels> getBlogDetails(int id) { var bloglist = await dal.Query(a => a.bID > 0, a => a.bID); var idmin = bloglist.FirstOrDefault() != null ? bloglist.FirstOrDefault().bID : 0; var idmax = bloglist.LastOrDefault() != null ? bloglist.LastOrDefault().bID : 1; var idminshow = id; var idmaxshow = id; BlogArticle blogArticle = new BlogArticle(); blogArticle = (await dal.Query(a => a.bID == idminshow)).FirstOrDefault(); BlogArticle prevblog = new BlogArticle(); while (idminshow > idmin) { idminshow--; prevblog = (await dal.Query(a => a.bID == idminshow)).FirstOrDefault(); if (prevblog != null) { break; } } BlogArticle nextblog = new BlogArticle(); while (idmaxshow < idmax) { idmaxshow++; nextblog = (await dal.Query(a => a.bID == idmaxshow)).FirstOrDefault(); if (nextblog != null) { break; } } blogArticle.btraffic += 1; await dal.Update(blogArticle, new List<string> { "btraffic" }); BlogViewModels models = new BlogViewModels() { bsubmitter=blogArticle.bsubmitter, btitle = blogArticle.btitle, bcategory = blogArticle.bcategory, bcontent = blogArticle.bcontent, btraffic = blogArticle.btraffic, bcommentNum = blogArticle.bcommentNum, bUpdateTime = blogArticle.bUpdateTime, bCreateTime = blogArticle.bCreateTime, bRemark = blogArticle.bRemark, }; if (nextblog != null) { models.next = nextblog.btitle; models.nextID = nextblog.bID; } if (prevblog != null) { models.previous = prevblog.btitle; models.previousID = prevblog.bID; } return models; } View Code 想了想这才是一个方法,一般的系统都会有少则几十,多则上百个这样的方法,这还不算,大家肯定遇到过一个情况,如果有一天要在页面多显示一个字段,噗!不是吧,首先要存在数据库,然后在该实体类就应该多一个,然后再在每一个赋值的地方增加一个,而且也没有更好的办法不是,一不小心就少了一个,然后被产品测试说咱们不细心,心塞哟,别慌!神器来了,一招搞定。 2、先来引入DTO讲解,以及它的原理 在学习EF的时候我们知道了ORM(Object Relational Mapping)映射,是一种对象关系的映射,对象-关系映射(ORM)系统一般以中间件的形式存在,主要实现程序对象到关系数据库数据的映射。 而Automapper是一种实体转换关系的模型,AutoMapper是一个.NET的对象映射工具。主要作用是进行领域对象与模型(DTO)之间的转换、数据库查询结果映射至实体对象。 下边的是基本原理,大家喵一眼就行: Ø 什么是DTO? 数据传输对象(DTO)(DataTransfer Object),是一种设计模式之间传输数据的软件应用系统。数据传输目标往往是数据访问对象从而从数据库中检索数据。数据传输对象与数据交互对象或数据访问对象之间的差异是一个以不具有任何行为除了存储和检索的数据(访问和存取器)。 Ø 为什么用? 它的目的只是为了对领域对象进行数据封装,实现层与层之间的数据传递。为何不能直接将领域对象用于数据传递?因为领域对象更注重领域,而DTO更注重数据。不仅如此,由于“富领域模型”的特点,这样做会直接将领域对象的行为暴露给表现层。 需要了解的是,数据传输对象DTO本身并不是业务对象。数据传输对象是根据UI的需求进行设计的,而不是根据领域对象进行设计的。比如,Customer领域对象可能会包含一些诸如FirstName, LastName, Email, Address等信息。但如果UI上不打算显示Address的信息,那么CustomerDTO中也无需包含这个 Address的数据”。 Ø 什么是领域对象? 领域模型就是面向对象的,面向对象的一个很重要的点就是:“把事情交给最适合的类去做”,即:“你得在一个个领域类之间跳转,才能找出他们如何交互”。在我们的系统中Model(EF中的实体)就是领域模型对象。领域对象主要是面对业务的,我们是通过业务来定义Model的。 以上的这些大家简单看看原理即可,意思大家肯定都懂,下边开始讲解如何使用 3、在Blog.Core.Services项目中引用Nuget包,AutoMapper 和 AutoMapper.Extensions.Microsoft.DependencyInjection AutoMapper.Extensions.Microsoft.DependencyInjection,这个是用来配合依赖注入的,看名字也能看的出来吧,大家回忆下,整个项目中,都是使用的依赖注入,所以尽量不要用new 来实例化,导致层耦合。 4、基于上边原理,在接口层Blog.Core 中,添加文件夹AutoMapper,然后添加映射配置文件 CustomProfile.cs,用来匹配所有的映射对象关系 public class CustomProfile : Profile { /// <summary> /// 配置构造函数,用来创建关系映射 /// </summary> public CustomProfile() { CreateMap<BlogArticle, BlogViewModels>(); } } 大家看下F12这个CreateMap方法 public IMappingExpression<TSource, TDestination> CreateMap<TSource, TDestination>(); 第一个参数是原对象,第二个是目的对象,所以,要想好,是哪个方向转哪个,当然可以都写上,比如 CreateMap<BlogArticle, BlogViewModels>(); CreateMap<BlogViewModels, BlogArticle>(); 5、修改上边服务层BlogArticleServices.cs 中getBlogDetails 方法中的赋值,改用AutoMapper,并用构造函数注入 最终的代码是 IBlogArticleRepository dal; IMapper IMapper; public BlogArticleServices(IBlogArticleRepository dal, IMapper IMapper) { this.dal = dal; base.baseDal = dal; this.IMapper = IMapper; } public async Task<BlogViewModels> getBlogDetails(int id) { var bloglist = await dal.Query(a => a.bID > 0, a => a.bID); var idmin = bloglist.FirstOrDefault() != null ? bloglist.FirstOrDefault().bID : 0; var idmax = bloglist.LastOrDefault() != null ? bloglist.LastOrDefault().bID : 1; var idminshow = id; var idmaxshow = id; BlogArticle blogArticle = new BlogArticle(); blogArticle = (await dal.Query(a => a.bID == idminshow)).FirstOrDefault(); BlogArticle prevblog = new BlogArticle(); while (idminshow > idmin) { idminshow--; prevblog = (await dal.Query(a => a.bID == idminshow)).FirstOrDefault(); if (prevblog != null) { break; } } BlogArticle nextblog = new BlogArticle(); while (idmaxshow < idmax) { idmaxshow++; nextblog = (await dal.Query(a => a.bID == idmaxshow)).FirstOrDefault(); if (nextblog != null) { break; } } blogArticle.btraffic += 1; await dal.Update(blogArticle, new List<string> { "btraffic" }); //注意就是这里 BlogViewModels models = IMapper.Map<BlogViewModels>(blogArticle); if (nextblog != null) { models.next = nextblog.btitle; models.nextID = nextblog.bID; } if (prevblog != null) { models.previous = prevblog.btitle; models.previousID = prevblog.bID; } return models; } 6、老规矩,还是在Startup中,注入服务 services.AddAutoMapper(typeof(Startup));//这是AutoMapper的2.0新特性 7、修改BlogController.cs中的 Get(int id)方法,运行项目,断点调试,发现已经成功了,是不是很方便,你也可以反过来试一试 [HttpGet("{id}", Name = "Get")] public async Task<object> Get(int id) { var model = await blogArticleServices.getBlogDetails(id);//调用该方法 var data = new { success = true, data = model }; return data; } 8、好啦,DTOs就到这里了,我们的ConfigureServices也基本告一段落了,主要有这些 二、Blog.Core项目打包发布在IIS 1、在项目Blog.Core中,右键,发布,选择文件,相信大家都会,不会的可以联系我 注意: 这里有一个坑,还记得我们用swagger中使用的两个xml文件么,编译的时候有,但是.net core官方限制了在发布的时候包含xml文件,所以我们需要处理下 在项目工程文件WebApplication1.csproj中,增加 <PropertyGroup> <GenerateDocumentationFile>true</GenerateDocumentationFile></PropertyGroup> ---------------------------------------------------------------------- 当然我们还可以基于CLI的Publish命令进行发布,只需切换到Light.API根目录下,输入以下命令即可: dotnet publish --framework netcoreapp1.1 --output "E:\Publish" --configuration Release framework表示目标框架,output表示要发布到的目录文件夹,configuration表示配置文件,等同于和上面我们通过管理器来发布的操作 具体的大家可以自行实验 2、一定要在服务器中安装.Net Core SDK (已安装则跳过): 地址:https://www.microsoft.com/net/download 在CMD命令窗口下,输入 dotnet 查看 3、安装WindowsHosting(已安装则跳过) IIS安装服务器上安装DotNetCore.X.X.X-WindowsHosting安装成功后重启IIS服务器。根据版本选择下载 下载地址:https://www.microsoft.com/net/download/windows 4、安装AspNetCoreModule托管模块(已安装则跳过), 下载地址:点击我下载 5、应用池配置为无托管代码 (网上解释:ASP.NET Core不再是由IIS工作进程(w3wp.exe)托管,而是使用自托管Web服务器(Kestrel)运行,IIS则是作为反向代理的角色转发请求到Kestrel不同端口的ASP.NET Core程序中,随后就将接收到的请求推送至中间件管道中去,处理完你的请求和相关业务逻辑之后再将HTTP响应数据重新回写到IIS中,最终转达到不同的客户端(浏览器,APP,客户端等)。而配置文件和过程都会由些许调整,中间最重要的角色便是AspNetCoreModule,它是其中一个的IIS模块,请求进入到IIS之后便立即由它转发,并迅速重定向到ASP.NET Core项目中,所以这时候我们无需设置应用程序池来托管我们的代码,它只负责转发请求而已) 6、如果需要读写根目录权限,要更改应用池 ApplicationPoolIdentity 7、如果没有出现正常的页面,你可以打开错误日志 在发布的时候,会有一个web.config出现,通过修改web.config 启用错误日志查看详细错误信息 将stdoutLogEnabled的修改为 true,并在应用程序根目录添加 logs 文件夹 一定要手动添加logs文件,不然会不出现 8、只要本地能通过,常见的错误就是生成的文件不全导致的,大家可以自行看看,如果有问题,也可以大家一起解决 9、在IIS中启动项目,或者直接输入服务器IP地址,加端口调试 注意:这里有一个小问题,因为发布以后,默认启动页是在开发环境中重定向到了swagger,但是在服务器部署以后,不能跳转,大家打开后会这样,404找不到,不要怕, 只需要在后边加上Swagger就行了 三、项目在Liunx Ubuntu中部署(简单版,慢慢完善) 1、在腾讯云购买Ubuntu服务器后,登陆,然后进入命令页面 2、部署Linux系统中的微软环境 继续执行下面的命令 Register the trusted Microsoft signature key: curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg 继续 sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg 根据系统版本,执行下面的命令 sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-xenial-prod xenial main" > /etc/apt/sources.list.d/dotnetdev.list' 好了,环境部署完毕,下面我们安装 SDK 3、部署.Ne Core 环境 sudo apt-get install apt-transport-https sudo apt-get update sudo apt-get install dotnet-sdk-2.1.4 安装成功后,输入命令 dotnet 证明安装成功啦 4、安装代码上传工具,Fillzila或者winSCP都可以,(我用的是winSCP) 软件下好打开后界面是这样的,我们需要填的就是主机名(你服务器的公网IP)、用户名(服务器的用户名)、密码(你买服务器时设置的密码),那个文件协议就是SFTP,不用改变 5、登陆进去默认是 /Home/ubuntu 文件夹,我们都在这里操作 6、下面我们在服务器新建一个控制台项目测试一下 dotnet new console -o myApp 然后就在winSCP发现多了一个项目 7、然后运行我们刚刚创建的项目 cd myAppdotnet run 代码一起正常! 8、把我们的项目发布上去,注意这里不是咱们发布的版本!不是发布的版本! 因为我们本地发布的是windows版本的,如果把publish打包版本发布上去,会报错,各种错 所以应该是把整个解决方法提交上去,当然git就别提交了 然后呢,进入到我们要发布的接口层项目 cd Blog.Core,然后再cd Blog.Core 最后执行 dotnet run 即可 四、结语 今天暂时就先写到这里,我们学到了如何用AutoMapper来实现DTO数据对象映射,也学会了在windows下的IIS中发布项目,最后就是Linux系统中,搭建环境和运行.net core 。以后呢我还会讲到如何桌面话Linux系统,Nginx代理等等,大家拭目以待吧 五、CODE https://github.com/anjoy8/Blog.Core https://gitee.com/laozhangIsPhi/Blog.Core
更新反馈 1、博友@落幕残情童鞋说到了,Nginx反向代理实现跨域,因为我目前还没有使用到,给忽略了,这次记录下,为下次补充。 代码已上传Github+Gitee,文末有地址 今天忙着给小伙伴们提出的问题解答,时间上没把握好,都快下班了,赶紧发布:书说上文《从壹开始前后端分离【 .NET Core2.0 +Vue2.0 】框架之十一 || AOP自定义筛选,Redis入门 11.1》,昨天咱们说到了分布式缓存键值数据库,主要讲解了如何安装,使用,最后遗留了一个问题,同步+Redis缓存还是比较简单,如何使用异步泛型存取Redis,还是一直我的心结,希望大家有会的,可以不吝赐教,本系列教程已经基本到了尾声,今天就说两个小的知识点,既然本系列是讲解前后端分离的,那一定会遇到跨域的问题,没错,今天将说下跨域!然后顺便说一下DTOs(数据传输对象),这些东西大家都用过,比如,在MVC中定义一个ViewModel,是基于Model实体类的,然后做了相应的变化,以适应前端需求,没错,就是这个,如果大型的实体类,一个个复杂的话会稍显费力,今天就是用一个自动映射工具——AutoMapper。 零、今天完成左下角的深紫色部分 一、为什么会出现跨域的问题 跨域问题由来已久,主要是来源于浏览器的”同源策略”。 何为同源?只有当协议、端口、和域名都相同的页面,则两个页面具有相同的源。只要网站的 协议名protocol、 主机host、 端口号port 这三个中的任意一个不同,网站间的数据请求与传输便构成了跨域调用,会受到同源策略的限制。 同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。浏览器的同源策略,出于防范跨站脚本的攻击,禁止客户端脚本(如 JavaScript)对不同域的服务进行跨站调用(通常指使用XMLHttpRequest请求)。 所以说我们在web中,我们无法去获取跨域的请求,常见的就是无法通过js获取接口(这里要说下我的以前使用的经验:在同源系统下,前端js去调用后端接口,然后后端C#去调取跨域接口,这是我以前采用的办法,但是前后端分离,这个办法肯定就是不行了,因为那时候已经没有了前后端之分,是两个项目),所以我们只要合理使用同源策略,就可以达到跨域访问的目的。 二、如何达到跨域的目的——三种跨域方式 之JsonP 我自己建立了一个一个静态页面,用来模拟前端访问,具体如下步骤: 1、新建一个Html页面,使用Jquery来发送请求(文件在项目的WWW文件夹下,大家可以自己下载,或者Copy下边代码)。 一共三种跨域方法 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Blog.Core</title> <script src="https://cdn.bootcss.com/jquery/1.10.2/jquery.min.js"></script> <style> div { margin: 10px; word-wrap: break-word; } </style> <script> $(document).ready(function () { $("#jsonp").click(function () { $.getJSON("http://localhost:58427/api/Login/jsonp?callBack=?", function (data) { $("#data-jsonp").html("数据: " + data.value); }); }); $("#cors").click(function () { $.get("http://localhost:58427/api/Login/Token", function (data, status) { $("#status-cors").html("状态: " + status); $("#data-cors").html("数据: " + data); }); }); }); </script> </head> <body> <h3>通过JsonP实现跨域请求</h3> <button id="jsonp">发送一个 GET </button> <div id="status-jsonp"></div> <div id="data-jsonp"></div> <hr /> <h3>添加请求头实现跨域</h3> <hr /> <h3>通过CORS实现跨域请求,另需要在服务器段配置CORE</h3> <button id="cors">发送一个 GET </button> <div id="status-cors"></div> <div id="data-cors"></div> <hr /> </body> </html> 注意:这里一定要注意jsonp的前端页面请求写法,要求很严谨 2、将这个页面部署到自己的IIS中(拷贝到文件里,直接在iis添加该文件,访问刚刚的Html文件目录就行) 3、在我们的项目 LoginController 中,设计Jsonp接口,Core调用的接口我们已经有了,就是之前获取Token的接口GetJWTStr [HttpGet] [Route("jsonp")] public void Getjsonp(string callBack, long id = 1, string sub = "Admin", int expiresSliding = 30, int expiresAbsoulute = 30) { TokenModel tokenModel = new TokenModel(); tokenModel.Uid = id; tokenModel.Sub = sub; DateTime d1 = DateTime.Now; DateTime d2 = d1.AddMinutes(expiresSliding); DateTime d3 = d1.AddDays(expiresAbsoulute); TimeSpan sliding = d2 - d1; TimeSpan absoulute = d3 - d1; string jwtStr = BlogCoreToken.IssueJWT(tokenModel, sliding, absoulute); //重要,一定要这么写 string response = string.Format("\"value\":\"{0}\"", jwtStr); string call = callBack + "({"+response+"})"; Response.WriteAsync(call); } 注意:这里一定要注意jsonp的接口写法,要求很严谨 4、点击”通过JsonP实现跨域请求“按钮,发现已经有数据了,证明Jsonp跨域已经成功,你可以换成自己的域名试一试,但是Cors的还不行 三、如何达到跨域的目的——三种跨域方式 之添加请求头实现跨域 这里我没有写到代码里,是在一般处理程序里之前用到的 后端 public void ProcessRequest(HttpContext context) { //接收参数 string uName = context.Request["name"]; string data = "{\"name\":\"" + uName + "\",\"age\":\"18\"}"; //只需在服务端添加以下两句 context.Response.AddHeader("Access-Control-Allow-Origin", "*"); //跨域可以请求的方式 context.Response.AddHeader("Access-Control-Allow-Methods", "POST,GET"); context.Response.Write(data); } 前端 function ashxRequest() { $.post("http://localhost:5551/ashxRequest.ashx", { name: "halo" }, function (data) { for (var i in data) { alert(data[i]); } }, "json") } 大家感兴趣可以自己实验下。有问题请留言 四、如何达到跨域的目的——三种跨域方式 之 高效CORS 1、前端的代码在jsonp的时候已经写好,请往上看第二节,后端接口也是Token接口 剩下的就是配置跨域了,很简单! 2、在ConfigureServices中添加 #region CORS services.AddCors(c => { //↓↓↓↓↓↓↓注意正式环境不要使用这种全开放的处理↓↓↓↓↓↓↓↓↓↓ c.AddPolicy("AllRequests", policy => { policy .AllowAnyOrigin()//允许任何源 .AllowAnyMethod()//允许任何方式 .AllowAnyHeader()//允许任何头 .AllowCredentials();//允许cookie }); //↑↑↑↑↑↑↑注意正式环境不要使用这种全开放的处理↑↑↑↑↑↑↑↑↑↑ //一般采用这种方法 c.AddPolicy("LimitRequests", policy => { policy .WithOrigins("http://localhost:8020", "http://blog.core.xxx.com","")//支持多个域名端口 .WithMethods("GET", "POST", "PUT", "DELETE")//请求方法添加到策略 .WithHeaders("authorization");//标头添加到策略 }); }); #endregion 基本注释都有,大家都能看的懂,就这么简单! 3、在需要跨域的controller上,增加特性(本文因为在LoginController,所以在这个控制器里),注意名称要写对 LimitRequests [Produces("application/json")] [Route("api/Login")] [EnableCors("LimitRequests")]//就是这里 public class LoginController : Controller { //.... } 4、好啦运行调试,一切正常 至此,跨域的问题已经完成辣 五、其他跨域方法补充 nginx是一个高性能的web服务器,常用作反向代理服务器。nginx作为反向代理服务器,就是把http请求转发到另一个或者一些服务器上。 通过把本地一个url前缀映射到要跨域访问的web服务器上,就可以实现跨域访问。 对于浏览器来说,访问的就是同源服务器上的一个url。而nginx通过检测url前缀,把http请求转发到后面真实的物理服务器。并通过rewrite命令把前缀再去掉。这样真实的服务器就可以正确处理请求,并且并不知道这个请求是来自代理服务器的。 简单说,nginx服务器欺骗了浏览器,让它认为这是同源调用,从而解决了浏览器的跨域问题。又通过重写url,欺骗了真实的服务器,让它以为这个http请求是直接来自与用户浏览器的。 这样,为了解决跨域问题,只需要动一下nginx配置文件即可。 六、结语 三种办法其实都能达到目的,但是优缺点也很明显 1、手动创建JSONP跨域 优点:无浏览器要求,可以在任何浏览器中使用此方式 缺点:格式要求很严格,只支持get请求方式,请求的后端出错不会有提示,造成不能处理异常 2、添加请求头实现跨域 优点:支持任意请求方式,并且后端出错会像非跨域那样有报错,可以对异常进行处理 缺点:兼容性不是很好,IE的话 <IE10 都不支持此方式 虽然CORS的方法有点儿类似请求头,但是封装,兼容性,灵活性都要好的很多,强烈推荐。 七、初探DTOs 请看以下实体类 //数据库实体类 public class Author { public string Name { get; set; } } public class Book { public string Title { get; set; } public Author Author { get; set; } } //页面实体类 public class BookViewModel { public string Title { get; set; } public string Author { get; set; } } //api调用 BookViewModel model = new BookViewModel { Title = book.Title, Author = book.Author.Name } 上面的例子相当的直观了,我们平时也是这么用的基本,但是问题也随之而来了,我们可以看到在上面的代码中,如果一旦在Book对象里添加了一个额外的字段,而后想在前台页面输出这个字段,那么就需要去在项目里找到每一处有这样BookViewModel转换字段的地方,这是非常繁琐的。另外,BookViewModel.Author是一个string类型的字段,但是Book.Author属性却是Author对象类型的,我们用的解决方法是通过Book.Auther对象来取得Author的Name属性值,然后再赋值给BookViewModel的Author属性,这样看起行的通,但是想一想,如果打算在以后的开发中把Name拆分成两个-FisrtName和LastName,我的天呐!我们得去把原来的ViewModel对象也拆分成对应的两个字段,然后在项目中找到所有的转换,然后替换。 那么有什么办法或者工具来帮助我们能够避免这样的情况发生呢?AutoMapper正是符合要求的一款插件。 只需一键操作,就能一劳永逸,解决所有问题,然后通过依赖注入,快速使用: //AutoMapper自动映射 //Mapper.Initialize(cfg => cfg.CreateMap<BlogArticle, BlogViewModels>()); //BlogViewModels models = Mapper.Map<BlogArticle, BlogViewModels>(blogArticle); BlogViewModels models = IMapper.Map<BlogViewModels>(blogArticle);//就这一句话完全搞定所有转换 今天因为时间的关系,没有说到Automapper,明天再见吧~ 八、CODE https://github.com/anjoy8/Blog.Core https://gitee.com/laozhangIsPhi/Blog.Core
大神留步 先说下一个窝心的问题,求大神帮忙,如何在Task异步编程中,使用Redis存、取Task<List<T>>泛型,有偿帮助,这里谢谢,文末有详细问题说明,可以留言或者私信都可以。 当然我也会一直思考,大家持续关注本帖,如果我想到好办法,会及时更新,并通知大家。 代码已上传Github+Gitee,文末有地址 书说上文《从壹开始前后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存》,昨天咱们说到了AOP面向切面编程,简单的举出了两个栗子,不知道大家有什么想法呢,不知道是否与传统的缓存的使用有做对比了么? 传统的缓存是在Controller中,将获取到的数据手动处理,然后当另一个controller中又使用的时候,还是Get,Set相关操作,当然如果小项目,有两三个缓存还好,如果是特别多的接口调用,面向Service服务层还是很有必要的,不需要额外写多余代码,只需要正常调取Service层的接口就行,AOP结合Autofac注入,会自动的查找,然后返回数据,不继续往下走Repository仓储了。 昨天我发布文章后,有一个网友提除了一个问题,他想的很好,就是如果面向到了Service层,那BaseService中的CURD等基本方法都被注入了,这样会造成太多的代理类,不仅没有必要,甚至还有问题,比如把Update也缓存了,这个就不是很好了,嗯,我也发现了这个问题,所以需要给AOP增加验证特性,只针对Service服务层中特定的常使用的方法数据进行缓存等。这样既能保证切面缓存的高效性,又能手动控制,不知道大家有没有其他的好办法,如果有的话,欢迎留言,或者加群咱们一起讨论,一起解决平时的问题。 零、今天完成的大红色部分 一、给缓存增加验证筛选特性 1、在解决方案中添加新项目Blog.Core.Common,然后在该Common类库中添加 特性文件夹 和 特性实体类,以后特性就在这里 //CachingAttribute /// <summary> /// 这个Attribute就是使用时候的验证,把它添加到要缓存数据的方法中,即可完成缓存的操作。注意是对Method验证有效 /// </summary> [AttributeUsage(AttributeTargets.Method, Inherited = true)] public class CachingAttribute : Attribute { //缓存绝对过期时间 public int AbsoluteExpiration { get; set; } = 30; } 2、添加Common程序集引用,然后修改缓存AOP类方法 BlogCacheAOP=》Intercept,简单对方法的方法进行判断 //qCachingAttribute 代码 //Intercept方法是拦截的关键所在,也是IInterceptor接口中的唯一定义 public void Intercept(IInvocation invocation) { var method = invocation.MethodInvocationTarget ?? invocation.Method; //对当前方法的特性验证 var qCachingAttribute = method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(CachingAttribute)) as CachingAttribute; //如果需要验证 if (qCachingAttribute != null) { //获取自定义缓存键 var cacheKey = CustomCacheKey(invocation); //根据key获取相应的缓存值 var cacheValue = _cache.Get(cacheKey); if (cacheValue != null) { //将当前获取到的缓存值,赋值给当前执行方法 invocation.ReturnValue = cacheValue; return; } //去执行当前的方法 invocation.Proceed(); //存入缓存 if (!string.IsNullOrWhiteSpace(cacheKey)) { _cache.Set(cacheKey, invocation.ReturnValue); } } else { invocation.Proceed();//直接执行被拦截方法 } } 可见在invocation参数中,包含了几乎所有的方法,大家可以深入研究下,获取到自己需要的数据 3、在制定的Service层中的某些类的某些方法上增加特性(一定是方法,不懂的可以看定义特性的时候AttributeTargets.Method) /// <summary> /// 获取博客列表 /// </summary> /// <param name="id"></param> /// <returns></returns> [Caching(AbsoluteExpiration = 10)]//增加特性 public async Task<List<BlogArticle>> getBlogs() { var bloglist = await dal.Query(a => a.bID > 0, a => a.bID); return bloglist; } 4、运行项目,打断点,就可以看到,普通的Query或者CURD等都不继续缓存了,只有咱们特定的 getBlogs()方法,带有缓存特性的才可以 5、当然,这里还有一个小问题,就是所有的方法还是走的切面,只是增加了过滤验证,大家也可以直接把那些需要的注入,不需要的干脆不注入容器,我之所以需要都经过的目的,就是想把它和日志结合,用来记录Service层的每一个请求,包括CURD的调用情况。 二、什么是Redis,为什么使用它 我个人有一个理解,关于Session或Cache等,在普通单服务器的项目中,很简单,有自己的生命周期等,想获取Session就获取,想拿啥就拿傻,但是在大型的分布式集群中,有可能这一秒的点击的页面和下一秒的都不在一个服务器上,对不对!想想如果普通的办法,怎么保证session的一致性,怎么获取相同的缓存数据,怎么有效的进行消息队列传递? 这个时候就用到了Redis,这些内容,网上已经到处都是,但是还是做下记录吧 Redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。它内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。在此基础上,Redis支持各种不同方式的排序。为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。 也就是说,缓存服务器如果意外重启了,数据还都在,嗯!这就是它的强大之处,不仅在内存高吞吐,还能持久化。 Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得Redis可执行单层树复制。存盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。 Redis也是可以做为消息队列的,与之相同功能比较优秀的就是Kafka Redis还是有自身的缺点: Redis只能存储key/value类型,虽然value的类型可以有多种,但是对于关联性的记录查询,没有Sqlserver、Oracle、Mysql等关系数据库方便。Redis内存数据写入硬盘有一定的时间间隔,在这个间隔内数据可能会丢失,虽然后续会介绍各种模式来保证数据丢失的可能性,但是依然会有可能,所以对数据有严格要求的不建议使用Redis做为数据库。 关于Redis的时候,看到网上一个流程图: 1、保存数据不经常变化 2、如果数据经常变化,就需要取操作Redis和持久化数据层的动作了,保证所有的都是最新的,实时更新Redis 的key到数据库,data到Redis中,但是要注意高并发 三、Redis的安装和调试使用 1.下载最新版redis,选择.msi安装版本,或者.zip免安装 (我这里是.msi安装) 下载地址:https://github.com/MicrosoftArchive/redis/releases 2.双击执行.msi文件,一路next,中间有一个需要注册服务,因为如果不注册的话,把启动的Dos窗口关闭的话,Redis就中断连接了。 3.如果你是免安装的,需要执行以下语句 启动命令:redis-server.exe redis.windows.conf 注册服务命令:redis-server.exe --service-install redis.windows.conf 去服务列表查询服务,可以看到redis服务默认没有开启,开启redis服务(可以设置为开机自动启动) 四、创建appsettings.json数据获取类 如果你对.net 获取app.config或者web.config得心应手的话,在.net core中就稍显吃力,因为不支持直接对Configuration的操作 前几篇文章中有一个网友说了这样的方法,在Starup.cs中的ConfigureServices方法中,添加 Blog.Core.Repository.BaseDBConfig.ConnectionString = Configuration.GetSection("AppSettings:SqlServerConnection").Value; 当然这是可行的,只不过,如果配置的数据很多,比如这样的,那就不好写了。 { "Logging": { "IncludeScopes": false, "Debug": { "LogLevel": { "Default": "Warning" } }, "Console": { "LogLevel": { "Default": "Warning" } } }, //用户配置信息 "AppSettings": { //Redis缓存 "RedisCaching": { "Enabled": true, "ConnectionString": "127.0.0.1:6379" }, //数据库配置 "SqlServer": { "SqlServerConnection": "Server=.;Database=WMBlogDB;User ID=sa;Password=123;", "ProviderName": "System.Data.SqlClient" }, "Date": "2018-08-28", "Author": "Blog.Core" } } 当然,我受到他的启发,简单做了下处理,大家看看是否可行 0、将上面代码添加到appsettings.json文件中 1、在Blog.Core.Common类库中,新建Helper文件夹,新建Appsettings.cs操作类,然后引用 Microsoft.Extensions.Configuration.Json 的Nuget包 /// <summary> /// appsettings.json操作类 /// </summary> public class Appsettings { static IConfiguration Configuration { get; set; } static Appsettings() { //ReloadOnChange = true 当appsettings.json被修改时重新加载 Configuration = new ConfigurationBuilder() .Add(new JsonConfigurationSource { Path = "appsettings.json", ReloadOnChange = true }) .Build(); } /// <summary> /// 封装要操作的字符 /// </summary> /// <param name="sections"></param> /// <returns></returns> public static string app(params string[] sections) { try { var val = string.Empty; for (int i = 0; i < sections.Length; i++) { val += sections[i] + ":"; } return Configuration[val.TrimEnd(':')]; } catch (Exception) { return ""; } } } 2、如何使用呢,直接引用类库,传递想要的参数就行(这里对参数是有顺序要求的,这个顺序就是json文件中的层级) /// <summary> /// 获取博客列表 /// </summary> /// <returns></returns> [HttpGet] [Route("GetBlogs")] public async Task<List<BlogArticle>> GetBlogs() { var connect=Appsettings.app(new string[] { "AppSettings", "RedisCaching" , "ConnectionString" });//按照层级的顺序,依次写出来 return await blogArticleServices.getBlogs(); } 3、注意:!!把appsettings.json文件添加到bin生成文件中!! 如果直接运行,会报错,提示没有权限, 操作:右键appsettings.json =》 属性 =》 Advanced =》 复制到输出文件夹 =》 永远复制 =》应用,保存 4、这个时候运行项目,就可以看到结果了 五、创建Redis缓存接口以及类,并在Controller中测试 1、在Blog.Core.Common的Helper文件夹中,添加SerializeHelper.cs 对象序列化操作,以后再扩展 public class SerializeHelper { /// <summary> /// 序列化 /// </summary> /// <param name="item"></param> /// <returns></returns> public static byte[] Serialize(object item) { var jsonString = JsonConvert.SerializeObject(item); return Encoding.UTF8.GetBytes(jsonString); } /// <summary> /// 反序列化 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="value"></param> /// <returns></returns> public static TEntity Deserialize<TEntity>(byte[] value) { if (value == null) { return default(TEntity); } var jsonString = Encoding.UTF8.GetString(value); return JsonConvert.DeserializeObject<TEntity>(jsonString); } } 2、在Blog.Core.Common类库中,新建Redis文件夹,并新建IRedisCacheManager接口和RedisCacheManager类,并引用Nuget包StackExchange.Redis public interface IRedisCacheManager { /// <summary> /// 获取 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="key"></param> /// <returns></returns> TEntity Get<TEntity>(string key); //设置 void Set(string key, object value, TimeSpan cacheTime); //判断是否存在 bool Get(string key); //移除 void Remove(string key); //清除 void Clear(); } 因为在开发的过程中,通过ConnectionMultiplexer频繁的连接关闭服务,是很占内存资源的,所以我们使用单例模式来实现 public class RedisCacheManager : IRedisCacheManager { private readonly string redisConnenctionString; public volatile ConnectionMultiplexer redisConnection; private readonly object redisConnectionLock = new object(); public RedisCacheManager() { string redisConfiguration = Appsettings.app(new string[] { "AppSettings", "RedisCaching", "ConnectionString" });//获取连接字符串 if (string.IsNullOrWhiteSpace(redisConfiguration)) { throw new ArgumentException("redis config is empty", nameof(redisConfiguration)); } this.redisConnenctionString = redisConfiguration; this.redisConnection = GetRedisConnection(); } /// <summary> /// 核心代码,获取连接实例 /// 通过双if 夹lock的方式,实现单例模式 /// </summary> /// <returns></returns> private ConnectionMultiplexer GetRedisConnection() { //如果已经连接实例,直接返回 if (this.redisConnection != null && this.redisConnection.IsConnected) { return this.redisConnection; } //加锁,防止异步编程中,出现单例无效的问题 lock (redisConnectionLock) { if (this.redisConnection != null) { //释放redis连接 this.redisConnection.Dispose(); } this.redisConnection = ConnectionMultiplexer.Connect(redisConnenctionString); } return this.redisConnection; } /// <summary> /// 清除 /// </summary> public void Clear() { foreach (var endPoint in this.GetRedisConnection().GetEndPoints()) { var server = this.GetRedisConnection().GetServer(endPoint); foreach (var key in server.Keys()) { redisConnection.GetDatabase().KeyDelete(key); } } } /// <summary> /// 判断是否存在 /// </summary> /// <param name="key"></param> /// <returns></returns> public bool Get(string key) { return redisConnection.GetDatabase().KeyExists(key); } /// <summary> /// 获取 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="key"></param> /// <returns></returns> public TEntity Get<TEntity>(string key) { var value = redisConnection.GetDatabase().StringGet(key); if (value.HasValue) { //需要用的反序列化,将Redis存储的Byte[],进行反序列化 return SerializeHelper.Deserialize<TEntity>(value); } else { return default(TEntity); } } /// <summary> /// 移除 /// </summary> /// <param name="key"></param> public void Remove(string key) { redisConnection.GetDatabase().KeyDelete(key); } /// <summary> /// 设置 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <param name="cacheTime"></param> public void Set(string key, object value, TimeSpan cacheTime) { if (value != null) { //序列化,将object值生成RedisValue redisConnection.GetDatabase().StringSet(key, SerializeHelper.Serialize(value), cacheTime); } } } 代码还是很简单的,网上都有很多资源,就是普通的添加,获取 3、将redis接口和类 在ConfigureServices中 进行注入,(注意是构造函数注入)然后在controller中添加代码测试 services.AddScoped<IRedisCacheManager, RedisCacheManager>(); IAdvertisementServices advertisementServices; IBlogArticleServices blogArticleServices; IRedisCacheManager redisCacheManager;//Reids缓存 /// <summary> /// 构造函数 /// </summary> /// <param name="advertisementServices"></param> /// <param name="blogArticleServices"></param> /// <param name="redisCacheManager"></param> public BlogController(IAdvertisementServices advertisementServices, IBlogArticleServices blogArticleServices, IRedisCacheManager redisCacheManager) { this.advertisementServices = advertisementServices; this.blogArticleServices = blogArticleServices; this.redisCacheManager = redisCacheManager; } /// <summary> /// 获取博客列表 /// </summary> /// <returns></returns> [HttpGet] [Route("GetBlogs")] public async Task<List<BlogArticle>> GetBlogs() { var connect=Appsettings.app(new string[] { "AppSettings", "RedisCaching" , "ConnectionString" });//按照层级的顺序,依次写出来 List<BlogArticle> blogArticleList = new List<BlogArticle>(); if (redisCacheManager.Get<object>("Redis.Blog") != null) { blogArticleList = redisCacheManager.Get<List<BlogArticle>>("Redis.Blog"); } else { blogArticleList = await blogArticleServices.Query(d => d.bID > 5); redisCacheManager.Set("Redis.Blog", blogArticleList, TimeSpan.FromHours(2));//缓存2小时 } return blogArticleList; } 4、运行,执行Redis缓存,看到结果 六、心结 今天的讲解就到里了,是不是有一种草草收场的感觉,是的!本来后来应该最后一节。细心的你应该发现了,我们是在controller进行测试,Redis缓存的是List泛型,但是呢,AOP切面缓存还是基于内存缓存,昨天我本想合并下,奈何AOP切面是通过异步编程,获取到的Task的List泛型,在Redis中需要序列化,鄙人表示不是很懂,希望看到的大神帮忙解决下, 如何把异步返回的Task<List<T>>结果,缓存到Redis,并能通过泛型取出来,有偿服务。感谢! 七、CODE https://github.com/anjoy8/Blog.Core https://gitee.com/laozhangIsPhi/Blog.Core
代码已上传Github+Gitee,文末有地址 上回《从壹开始前后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之九 || 依赖注入IoC学习 + AOP界面编程初探》咱们说到了依赖注入Autofac的使用,不知道大家对IoC的使用用怎样的感觉,我个人表示还是比较可行的,至少不用自己再关心一个个复杂的实例化服务对象了,直接通过接口就满足需求,当然还有其他的一些功能,我还没有说到,抛砖引玉嘛,大家如果有好的想法,欢迎留言,也可以来群里,大家一起学习讨论。昨天在文末咱们说到了AOP面向切面编程的定义和思想,我个人简单使用了下,感觉主要的思路还是通过拦截器来操作,就像是一个中间件一样,今天呢,我给大家说两个小栗子,当然,你也可以合并成一个,也可以自定义扩展,因为我们是真个系列是基于Autofac框架,所以今天主要说的是基于Autofac的Castle动态代理的方法,静态注入的方式以后有时间可以再补充。 时间真快,转眼已经十天过去了,感谢大家的鼓励,批评指正,希望我的文章,对您有一点点儿的帮助,哪怕是有学习新知识的动力也行,至少至少,可以为以后跳槽增加新的谈资 [哭笑],这些天我们从面向对象OOP的开发,后又转向了面向接口开发,到分层解耦,现在到了面向切面编程AOP,往下走将会是,分布式,微服务等等,技术真是永无止境啊!好啦,马上开始动笔。 零、今天完成的深红色部分 一、面向切面编程AOP实现日志记录接口使用情况功能(服务层) 首先想一想,如果有一个需求(这个只是我的一个想法,真实工作中可能用不上),要记录整个项目的接口和调用情况,当然如果只是控制器的话,还是挺简单的,直接用一个过滤器或者一个中间件,还记得咱们开发Swagger拦截权限验证的中间件么,那个就很方便的把用户调用接口的名称记录下来,当然也可以写成一个切面,但是如果想看下与Service或者Repository层的调用情况呢,好像目前咱们只能在Service层或者Repository层去写日志记录了,那样的话,不仅工程大(当然你可以用工厂模式),而且耦合性瞬间就高了呀,想象一下,如果日志要去掉,关闭,修改,需要改多少地方!您说是不是,好不容易前边的工作把层级的耦合性降低了。别慌,这个时候就用到了AOP和Autofac的Castle结合的完美解决方案了。 经过这么多天的开发,几乎每天都需要引入Nuget包哈,我个人表示也不想再添加了,现在都已经挺大的了(47M当然包括全部dll文件),今天不会辣!其实都是基于昨天的两个Nuget包中已经自动生成的Castle组件。请看以下步骤: 1、在IBlogArticleServices.cs定义一个获取博客列表接口 ,并在BlogArticleServices实现该接口 public interface IBlogArticleServices :IBaseServices<BlogArticle> { Task<List<BlogArticle>> getBlogs(); } public class BlogArticleServices : BaseServices<BlogArticle>, IBlogArticleServices { IBlogArticleRepository dal; public BlogArticleServices(IBlogArticleRepository dal) { this.dal = dal; base.baseDal = dal; } /// <summary> /// 获取博客列表 /// </summary> /// <param name="id"></param> /// <returns></returns> public async Task<List<BlogArticle>> getBlogs() { var bloglist = await dal.Query(a => a.bID > 0, a => a.bID); return bloglist; } } 2、在API层中添加对该接口引用(注意RESTful接口路径命名规范,我这么写只是为了测试) /// <summary> /// 获取博客列表 /// </summary> /// <returns></returns> [HttpGet] [Route("GetBlogs")] public async Task<List<BlogArticle>> GetBlogs() { return await blogArticleServices.getBlogs(); } 3、在Blog.Core新建文件夹AOP,并添加拦截器BlogLogAOP,并设计其中用到的日志记录Logger方法或者类 关键的一些知识点,注释中已经说明了,主要是有以下:1、继承接口IInterceptor2、实例化接口IINterceptor的唯一方法Intercept3、void Proceed();表示执行当前的方法和object ReturnValue { get; set; }执行后调用,object[] Arguments参数对象4、中间的代码是新建一个类,还是单写,就很随意了。 /// <summary> /// 拦截器BlogLogAOP 继承IInterceptor接口 /// </summary> public class BlogLogAOP : IInterceptor { /// <summary> /// 实例化IInterceptor唯一方法 /// </summary> /// <param name="invocation">包含被拦截方法的信息</param> public void Intercept(IInvocation invocation) { //记录被拦截方法信息的日志信息 var dataIntercept = $"{DateTime.Now.ToString("yyyyMMddHHmmss")} " + $"当前执行方法:{ invocation.Method.Name} " + $"参数是: {string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())} \r\n"; //在被拦截的方法执行完毕后 继续执行当前方法 invocation.Proceed(); dataIntercept += ($"被拦截方法执行完毕,返回结果:{invocation.ReturnValue}"); #region 输出到当前项目日志 var path = Directory.GetCurrentDirectory() + @"\Log"; if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } string fileName = path + $@"\InterceptLog-{DateTime.Now.ToString("yyyyMMddHHmmss")}.log"; StreamWriter sw = File.AppendText(fileName); sw.WriteLine(dataIntercept); sw.Close(); #endregion } } 4、添加到Autofac容器中,实现注入 还记得昨天的容器么,先把拦截器注入,然后对程序集的注入方法中添加拦截器服务即可 builder.RegisterType<BlogLogAOP>();//可以直接替换其他拦截器!一定要把拦截器进行注册 var assemblysServices = Assembly.Load("Blog.Core.Services"); //builder.RegisterAssemblyTypes(assemblysServices).AsImplementedInterfaces();//指定已扫描程序集中的类型注册为提供所有其实现的接口。 builder.RegisterAssemblyTypes(assemblysServices) .AsImplementedInterfaces() .InstancePerLifetimeScope() .EnableInterfaceInterceptors()//引用Autofac.Extras.DynamicProxy; .InterceptedBy(typeof(BlogLogAOP));//可以直接替换拦截器 注意其中的俩个方法 .EnableInterfaceInterceptors()//对目标类型启用接口拦截。拦截器将被确定,通过在类或接口上截取属性, 或添加 InterceptedBy () .InterceptedBy(typeof(BlogLogAOP));//允许将拦截器服务的列表分配给注册。 说人话就是,将拦截器加上要注入容器的的接口或者类上 5、运行项目,嗯,你就看到这根目录下生成了一个Log文件夹,里边有日志记录,当然记录很简陋,里边是获取到的实体类,大家可以自己根据需要扩展 这里,面向服务层的日志记录就完成了,大家感觉是不是很平时的不一样? 二、需求2.面向切面编程AOP实现接口数据的缓存功能 想一想,如果我们要实现缓存功能,一般咱们都是将数据获取到以后,定义缓存,然后在其他地方使用的时候,在根据key去获取当前数据,然后再操作等等,平时都是在API接口层获取数据后进行缓存,今天咱们可以试试,在接口之前就缓存下来。 1、老规矩,定义一个缓存类和接口,你会问了,为什么上边的日志没有定义,因为我会在之后讲Redis的时候用到这个缓存接口 /// <summary> /// 简单的缓存接口,只有查询和添加,以后会进行扩展 /// </summary> public interface ICaching { object Get(string cacheKey); void Set(string cacheKey, object cacheValue); } /// <summary> /// 实例化缓存接口ICaching /// </summary> public class MemoryCaching : ICaching { //引用Microsoft.Extensions.Caching.Memory;这个和.net 还是不一样,没有了Httpruntime了 private IMemoryCache _cache; //还是通过构造函数的方法,获取 public MemoryCaching(IMemoryCache cache) { _cache = cache; } public object Get(string cacheKey) { return _cache.Get(cacheKey); } public void Set(string cacheKey, object cacheValue) { _cache.Set(cacheKey, cacheValue, TimeSpan.FromSeconds(7200)); } } 2、定义一个拦截器还是继承IInterceptor,并实现Intercept /// <summary> /// 面向切面的缓存使用 /// </summary> public class BlogCacheAOP : IInterceptor { //通过注入的方式,把缓存操作接口通过构造函数注入 private ICaching _cache; public BlogCacheAOP(ICaching cache) { _cache = cache; } //Intercept方法是拦截的关键所在,也是IInterceptor接口中的唯一定义 public void Intercept(IInvocation invocation) { //获取自定义缓存键 var cacheKey = CustomCacheKey(invocation); //根据key获取相应的缓存值 var cacheValue = _cache.Get(cacheKey); if (cacheValue != null) { //将当前获取到的缓存值,赋值给当前执行方法 invocation.ReturnValue = cacheValue; return; } //去执行当前的方法 invocation.Proceed(); //存入缓存 if (!string.IsNullOrWhiteSpace(cacheKey)) { _cache.Set(cacheKey, invocation.ReturnValue); } } //自定义缓存键 private string CustomCacheKey(IInvocation invocation) { var typeName = invocation.TargetType.Name; var methodName = invocation.Method.Name; var methodArguments = invocation.Arguments.Select(GetArgumentValue).Take(3).ToList();//获取参数列表,最多三个 string key = $"{typeName}:{methodName}:"; foreach (var param in methodArguments) { key += $"{param}:"; } return key.TrimEnd(':'); } //object 转 string private string GetArgumentValue(object arg) { if (arg is int || arg is long || arg is string) return arg.ToString(); if (arg is DateTime) return ((DateTime)arg).ToString("yyyyMMddHHmmss"); return ""; } } 注释的很清楚,基本都是情况 3、ConfigureServices不用动,只需要改下拦截器的名字就行 注意: //将 TService 中指定的类型的范围服务添加到实现 services.AddScoped<ICaching, MemoryCaching>();//记得把缓存注入!!! 4、运行,你会发现,首次缓存是空的,然后将Repository仓储中取出来的数据存入缓存,第二次使用就是有值了,其他所有的地方使用,都不用再写了,而且也是面向整个程序集合的 三、还有其他的一些问题需要考虑 1、可以针对某一层的指定类的指定方法进行操作,这里就不写了,大家可以自己实验 配合Attribute就可以只拦截相应的方法了。因为拦截器里面是根据Attribute进行相应判断的!! builder.RegisterAssemblyTypes(assembly) .Where(type => typeof(IQCaching).IsAssignableFrom(type) && !type.GetTypeInfo().IsAbstract) .AsImplementedInterfaces() .InstancePerLifetimeScope() .EnableInterfaceInterceptors() .InterceptedBy(typeof(QCachingInterceptor)); 2、时间问题,阻塞,浪费资源问题等 定义切面有时候是方便,初次使用会很别扭,使用多了,可能会对性能有些许的影响,因为会大量动态生成代理类,性能损耗,是特别高的请求并发,比如万级每秒,还是不建议生产环节推荐。所以说切面编程要深入的研究,不可随意使用,我说的也是九牛一毛,大家继续加油吧! 3、静态注入 基于Net的IL语言层级进行注入,性能损耗可以忽略不计,Net使用最多的Aop框架PostSharp(好像收费了;)采用的即是这种方式。 大家可以参考这个博文:https://www.cnblogs.com/mushroom/p/3932698.html 四、结语 今天的讲解就到了这里了,通过这两个小栗子,大家应该能对面向切面编程有一些朦胧的感觉了吧,感兴趣的可以深入的研究,也欢迎一起讨论,刚刚在缓存中,我说到了缓存接口,就引入了下次的讲解内容,Redis的高性能缓存框架,内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。下次再见咯~ 五、CODE https://github.com/anjoy8/Blog.Core https://gitee.com/laozhangIsPhi/Blog.Core
更新 1、感谢@dongfo博友的提醒,目前是vue-cli脚手架是3.0.1,vue的版本还是2.5.17,下文已改,感谢纠错! 2、代码已经同步到码云https://gitee.com/laozhangIsPhi/Blog.Core,欢迎纠错 代码已上传Github+Gitee,文末有地址 说接上文,上回说到了《从壹开始前后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之八 || API项目整体搭建 6.3 异步泛型+依赖注入初探》,后来的标题中,我把仓储两个字给去掉了,因为好像大家对这个模式很有不同的看法,嗯~可能还是我学艺不精,没有说到其中的好处,现在在学DDD领域驱动设计相关资料,有了好的灵感再给大家分享吧。 到目前为止我们的项目已经有了基本的雏形,后端其实已经可以搭建自己的接口列表了,框架已经有了规模,原本应该说vue了,但是呢,又听说近来Vue-cli已经从2.0升级到了3.0了,还变化挺大,前端大佬们,都不停歇呀。当然我还在学习当中,我也需要了解下有关3.0的特性,希望给没有接触到,或者刚刚接触到的朋友们,有一些帮助,当然我这个不是随波逐流,只是在众多的博文中,给大家一个入门参考,届时说3.0的时候,还是会说2.0的相关问题的。 虽然项目整体可以运行了,但是我还有几个小知识点要说下,主要是1、依赖注入和AOP相关知识;2、跨域代理等问题(因为Vue是基于Node开发的,与后台API接口不在同一个地址);3、实体类的DTO相关小问题;4、Redis缓存等;5、部署服务器中的各种坑;虽然都是很小的知识点,我还是都下给大家说下的,好啦,开始今天的讲解; 零、今天完成的绿色部分 一、依赖注入的理解和思考 说到依赖,我就想到了网上有一个例子,依赖注入和工厂模式中的相似和不同: (1)原始社会里,没有社会分工。须要斧子的人(调用者)仅仅能自己去磨一把斧子(被调用者)。相应的情形为:Java程序里的调用者自己创建被调用者。(2)进入工业社会,工厂出现。斧子不再由普通人完毕,而在工厂里被生产出来,此时须要斧子的人(调用者)找到工厂,购买斧子,无须关心斧子的制造过程。相应Java程序的简单工厂的设计模式。(3)进入“按需分配”社会,须要斧子的人不须要找到工厂,坐在家里发出一个简单指令:须要斧子。斧子就自然出如今他面前。相应Spring的依赖注入。 在上篇文章中,我们已经了解到了,什么是依赖倒置、控制反转(IOC),什么是依赖注入(DI),网上这个有很多很多的讲解,我这里就不说明了,其实主要是见到这样的,就是存在依赖 public class A : D { public A(B b) { // do something } C c = new C(); } 就比如我们的项目中的BlogController,只要是通过new 实例化的,都是存在依赖 public async Task<List<Advertisement>> Get(int id) { IAdvertisementServices advertisementServices = new AdvertisementServices(); return await advertisementServices.Query(d => d.Id == id); } 使用依赖注入呢,有以下优点: 传统的代码,每个对象负责管理与自己需要依赖的对象,导致如果需要切换依赖对象的实现类时,需要修改多处地方。同时,过度耦合也使得对象难以进行单元测试。 依赖注入把对象的创造交给外部去管理,很好的解决了代码紧耦合(tight couple)的问题,是一种让代码实现松耦合(loose couple)的机制。 松耦合让代码更具灵活性,能更好地应对需求变动,以及方便单元测试。 举个栗子,就是关于日志记录的 日志记录:有时需要调试分析,需要记录日志信息,这时可以采用输出到控制台、文件、数据库、远程服务器等;假设最初采用输出到控制台,直接在程序中实例化ILogger logger = new ConsoleLogger(),但有时又需要输出到别的文件中,也许关闭日志输出,就需要更改程序,把ConsoleLogger改成FileLogger或者NoLogger, new FileLogger()或者new SqlLogger() ,此时不断的更改代码,就显得心里不好了,如果采用依赖注入,就显得特别舒畅。 我有一个个人的理解,不知道恰当与否,比如我们平时食堂吃饭,都是食堂自己炒的菜,就算是配方都一样,控制者(厨师)还是会有些变化,或者是出错,但是肯德基这种,由总店提供的,店面就失去了控制,就出现了第三方(总店或者工厂等),这就是实现了控制反转,我们只需要说一句,奥尔良鸡翅,嗯就拿出来一个,而且全部分店的都一样,我们不用管是否改配方,不管是否依赖哪些调理食材,哈哈。 二、常见的IoC框架有哪些 Autofac:貌似目前net下用的最多吧Ninject:目前好像没多少人用了Unity:也是较为常见 其实.Net Core 有自己的轻量级的IoC框架, ASP.NET Core本身已经集成了一个轻量级的IOC容器,开发者只需要定义好接口后,在Startup.cs的ConfigureServices方法里使用对应生命周期的绑定方法即可,常见方法如下 services.AddTransient<IApplicationService,ApplicationService>//服务在每次请求时被创建,它最好被用于轻量级无状态服务(如我们的Repository和ApplicationService服务) services.AddScoped<IApplicationService,ApplicationService>//服务在每次请求时被创建,生命周期横贯整次请求 services.AddSingleton<IApplicationService,ApplicationService>//Singleton(单例) 服务在第一次请求时被创建(或者当我们在ConfigureServices中指定创建某一实例并运行方法),其后的每次请求将沿用已创建服务。如果开发者的应用需要单例服务情景,请设计成允许服务容器来对服务生命周期进行操作,而不是手动实现单例设计模式然后由开发者在自定义类中进行操作。 当然.Net Core自身的容器还是比较简单,如果想要更多的功能和扩展,还是需要使用上边上个框架。 三、较好用的IoC框架使用——Autofac 首先呢,我们要明白,我们注入是要注入到哪里——Controller API层。然后呢,我们看到了在接口调用的时候,如果需要其中的方法,需要using两个命名空间 [HttpGet("{id}", Name = "Get")] public async Task<List<Advertisement>> Get(int id) { IAdvertisementServices advertisementServices = new AdvertisementServices();//需要引用两个命名空间Blog.Core.IServices;Blog.Core.Services; return await advertisementServices.Query(d => d.Id == id); } 接下来我们就需要做处理: 1、在Nuget中引入两个:Autofac.Extras.DynamicProxy(Autofac的动态代理,它依赖Autofac,所以可以不用单独引入Autofac)、Autofac.Extensions.DependencyInjection(Autofac的扩展) 2、让Autofac接管Starup中的ConfigureServices方法,记得修改返回类型IServiceProvider public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc(); #region 配置信息 //Blog.Core.Repository.BaseDBConfig.ConnectionString = Configuration.GetSection("AppSettings:SqlServerConnection").Value; #endregion #region Swagger services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Version = "v0.1.0", Title = "Blog.Core API", Description = "框架说明文档", TermsOfService = "None", Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "Blog.Core", Email = "Blog.Core@xxx.com", Url = "https://www.jianshu.com/u/94102b59cc2a" } }); //就是这里 #region 读取xml信息 var basePath = PlatformServices.Default.Application.ApplicationBasePath; var xmlPath = Path.Combine(basePath, "Blog.Core.xml");//这个就是刚刚配置的xml文件名 var xmlModelPath = Path.Combine(basePath, "Blog.Core.Model.xml");//这个就是Model层的xml文件名 c.IncludeXmlComments(xmlPath, true);//默认的第二个参数是false,这个是controller的注释,记得修改 c.IncludeXmlComments(xmlModelPath); #endregion #region Token绑定到ConfigureServices //添加header验证信息 //c.OperationFilter<SwaggerHeader>(); var security = new Dictionary<string, IEnumerable<string>> { { "Blog.Core", new string[] { } }, }; c.AddSecurityRequirement(security); //方案名称“Blog.Core”可自定义,上下一致即可 c.AddSecurityDefinition("Blog.Core", new ApiKeyScheme { Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入{token}\"", Name = "Authorization",//jwt默认的参数名称 In = "header",//jwt默认存放Authorization信息的位置(请求头中) Type = "apiKey" }); #endregion }); #endregion #region Token服务注册 services.AddSingleton<IMemoryCache>(factory => { var cache = new MemoryCache(new MemoryCacheOptions()); return cache; }); services.AddAuthorization(options => { options.AddPolicy("Admin", policy => policy.RequireClaim("AdminType").Build());//注册权限管理,可以自定义多个 }); #endregion #region AutoFac //实例化 AutoFac 容器 var builder = new ContainerBuilder(); //注册要通过反射创建的组件 builder.RegisterType<AdvertisementServices>().As<IAdvertisementServices>(); //将services填充到Autofac容器生成器中 builder.Populate(services); //使用已进行的组件登记创建新容器 var ApplicationContainer = builder.Build(); #endregion return new AutofacServiceProvider(ApplicationContainer);//第三方IOC接管 core内置DI容器 } 这个时候我们就把AdvertisementServices的new 实例化过程注入到了Autofac容器中 3、通过依赖注入的三种方式(构造方法注入、setter方法注入和接口方式注入)中的构造函数方式实现注入 在BlogController中,添加构造函数,并在Get方法中,去掉实例化过程; IAdvertisementServices advertisementServices; /// <summary> /// 构造函数 /// </summary> /// <param name="advertisementServices"></param> public BlogController(IAdvertisementServices advertisementServices) { this.advertisementServices = advertisementServices; } [HttpGet("{id}", Name = "Get")] public async Task<List<Advertisement>> Get(int id) { //IAdvertisementServices advertisementServices = new AdvertisementServices();//需要引用两个命名空间Blog.Core.IServices;Blog.Core.Services; return await advertisementServices.Query(d => d.Id == id); } 4、然后运行调试,发现在断点刚进入的时候,接口已经被实例化了,达到了注入的目的。 5、这个时候,我们发现已经成功的注入了,Advertisement实体类到接口中,但是项目中有那么多的类,都要一个个手动添加么,答案当然不是滴~ 四、通过反射将Blog.Core.Services和Blog.Core.Repository两个程序集的全部方法注入 修改如下代码,注意这个时候需要在项目依赖中,右键,添加引用Blog.Core.Services到项目,或者把dll文件拷贝到Blog.Core的bin文件夹中,否则反射会找不到。 var builder = new ContainerBuilder(); //注册要通过反射创建的组件 //builder.RegisterType<AdvertisementServices>().As<IAdvertisementServices>(); var assemblysServices = Assembly.Load("Blog.Core.Services"); builder.RegisterAssemblyTypes(assemblysServices).AsImplementedInterfaces();//指定已扫描程序集中的类型注册为提供所有其实现的接口。 var assemblysRepository = Assembly.Load("Blog.Core.Repository"); builder.RegisterAssemblyTypes(assemblysRepository).AsImplementedInterfaces(); //将services填充到Autofac容器生成器中 builder.Populate(services); //使用已进行的组件登记创建新容器 var ApplicationContainer = builder.Build(); 其他不变,运行项目,一切正常,换其他接口也可以 到这里,Autofac依赖注入已经完成,基本的操作就是这样,别忙!现在还没有真正的完成哟!现在只是把Service和API层解耦了,Service和Repository还没有! 注意:文中和Git代码中,因为为了说明的方便,没有把Api层的Service 层 和 Repository 层给去掉,大家手动去掉,我已经更新到git上了。 1、最终的效果是这样的:工程只是依赖接口层 2、把service.dll 和 Repository.dll 两个文件拷贝到项目 的 bin \ debug \netcoreapp2.1 下 3、然后在Autofac依赖注入的时候,出现加载程序集失败的情况,可以修改如下: var basePath = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath;//获取项目路径 var servicesDllFile = Path.Combine(basePath, "Blog.Core.Services.dll");//获取注入项目绝对路径 var assemblysServices = Assembly.LoadFile(servicesDllFile);//直接采用加载文件的方法 还记得Blog.Core.Services中的BaseServices.cs么,它还是通过new 实例化的方式在创建,仿照contrller,修改BaseServices并在全部子类的构造函数中注入: public class BaseServices<TEntity> : IBaseServices<TEntity> where TEntity : class, new() { //public IBaseRepository<TEntity> baseDal = new BaseRepository<TEntity>(); public IBaseRepository<TEntity> baseDal;//通过在子类的构造函数中注入,这里是基类,不用构造函数 //... } public class AdvertisementServices : BaseServices<Advertisement>, IAdvertisementServices { IAdvertisementRepository dal; public AdvertisementServices(IAdvertisementRepository dal) { this.dal = dal; base.baseDal = dal; } } 好啦,现在整个项目已经完成了相互直接解耦的功能,以后就算是Repository和Service如何变化,接口层都不用修改,因为已经完成了注入,第三方Autofac会做实例化的过程。 五、 当然,你也可以直接将一个类进行注入,而不一定要继承接口的方式; //1. 定义一个服务,包含一个方法 public class PeopleService { public string Run(string m) { return m; } } //2. 写一个扩展方法用来注入服务,与直接在ConfigureServices返回是一个道理 namespace Haos.Develop.CoreTest { public static class Extension { public static IServiceCollection AddTestService(this IServiceCollection service) { return service.AddScoped(factory => new PeopleService()); } } } //3. 回到Startup类中找到ConfigureServices方法添加如下代码 public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddTestService();//将上边的方法注入 } //4.我们可以采用构造函数方式来注入和直接获取 public PeopleService People; public HomeController(PeopleService people)//构造函数 { People = people; } public JsonResult Test() { People.Run("111"); return Json(""); } 上边这个方法采用的是单独注册一个扩展方法来注入服务,和我们的直接return是一样的,感兴趣的可以试一试。 同时可以参考这个网友的: https://www.cnblogs.com/stulzq/p/6880394.html 六、简单了解通过AOP切面实现日志记录 什么是AOP?引用百度百科:AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。实现AOP主要由两种方式, 一种是编译时静态植入,优点是效率高,缺点是缺乏灵活性,.net下postsharp为代表者(好像是付费了。。)。 另一种方式是动态代理,优点是灵活性强,但是会影响部分效率,动态为目标类型创建代理,通过代理调用实现拦截。 AOP能做什么,常见的用例是事务处理、日志记录等等。 常见的AOP都是配合在Ioc的基础上进行操作,上边咱们讲了Autofac这个简单强大的Ioc框架,下面就讲讲Autofac怎么实现AOP。Autofac的AOP是通过Castle(也是一个容器)项目的核心部分实现的,名为Autofac.Extras.DynamicProxy,顾名思义,其实现方式为动态代理。当然AOP并不一定要和依赖注入在一起使用,自身也可以单独使用。 网上有一个博友的图片,大概讲了AOP切面编程 七、结语 今天的文章呢,主要说了依赖注入IoC在项目中的使用,从上边的说明中,可以看到,最大的一个优点就是实现解耦的作用,最明显的就是,在Controller中,不用在实例服务层或者业务逻辑层了,不过还是有些缺点的,缺点之一就是会占用一定的时间资源,效率稍稍受影响,不过和整体框架相比,这个影响应该也是值得的。 明天,我们继续将面向切面编程AOP中的,日志记录和面向AOP的缓存使用。 八、CODE https://github.com/anjoy8/Blog.Core https://gitee.com/laozhangIsPhi/Blog.Core
代码已上传Github+Gitee,文末有地址 番外:在上文中,也是遇到了大家见仁见智的评论和反对,嗯~说实话,积极性稍微受到了一丢丢的打击,不过还好,还是有很多很多很多人的赞同的,所以会一直坚持下去,欢迎提出各种建议,问题,意见等,我这个系列呢,只是一个抛砖引玉的文章,大家可以自定义的去扩展学习,比如你看了.net core api,可以自学.net core mvc呀;看了sqlsugar,可以自学EF,Deppar呀;看了vue,可以自学React、Angular呀,我也是个小白,大家进步是本系列文章的唯一目标。 说接上文,《从壹开始前后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之七 || API项目整体搭建 6.2 轻量级ORM》,在文中,我们提到了Sqlsugar,该框架呢我也是咨询了身边的一些大神,他们给我说法是: Sqlsugar和EF一样,表达式树,不用写sql,但是支持sql,支持多种类型数据库(MSSQL,Oracle,Mysql,SQLite),配置简单; 仅仅是一个数据访问层,100k轻量级,方便迁移; 关于速率呢,我简单的做了一个测试,使用公司的数据表,一共4千万条数据,我遍历全表,并提取前一万条(当然数据库有主键索引,一般分页100条也够多了吧),一共1.6s,截图如下: 网友互动 昨天也收到了很多人的问题和反馈,关于DbConfig连接字符串,@君子不器_万物有灵(这里把您的昵称放出来,不会侵权吧,可以私信我,我删掉),有新的方法,我很感谢他,把他的方法写出来 可参考网文地址:配置和选项 1、在appsettings.json 中添加 "AppSettings": { "SqlServerConnection": "Server=.;Database=BlogDB;User ID=sa;Password=sa;", "ProviderName": "System.Data.SqlClient" }, 2、在ConfigureServices(IServiceCollection services) 方法中添加 //数据库配置 BaseDBConfig.ConnectionString = Configuration.GetSection("AppSettings:SqlServerConnection").Value; 3、修改BaseDBConfig.cs public static string ConnectionString { get; set; } 我在之后的项目中会使用他的这个方法,并且做一个扩展,这里先写上,如果大家都有好的意见或建议,我都会在下一篇文章中写出来,大家一起学习。 好啦,昨天已经总结好了,开始今天的讲解。 缘起 在上一节中,我们实现了仓储层的构造,并联通了数据库,调取了数据,整理搭建已经有了一个雏形,今天就继续往下探讨,写一个异步泛型仓储 使用异步Async/Aswait呢,还是很方便的,不过,如果要使用异步,就要异步到底,不然就会阻塞,变成了同步,还是鼓励大家练习下,就算是出现错误了,那就证明学习到了,哈哈哈[哭笑]; 泛型呢,用到的是接口和基类,可以极大的减少工作量; 零、今天完成的浅紫色部分 一、在Blog.Core.IRepository 层中添加BASE文件夹,并添加接口 IBaseRepository.cs 我自己从SimpleClient中,抽取了基本的常见方法做封装,你也可以自己自定义封装扩展,有人问我,既然Sqlsugar都已经封装好了SimpleClient,为什么我还要在仓储中再一次封装,我是这么想的,如果一个项目中来了一个新人,之前用过EF或者Sqlhelper,那他来了,需要在重新学习一遍Sqlsugar了,这个时间上,至少一两天吧,所以封装基本的接口后,他只需要按照之前的开发方法使用就行,不需要了解底层,当然这个还是个小栗子,其实大公司都是这样的,更新迭代很快,没有精力从新学习,所以这也就是面向接口编程的好处,我曾经在微软的项目中就充分的感受到这个境况。 注意:我这里没有封装多表查询,其实是可以写的,参考地址 多表查询, ,如果各位会封装的话,可以留言,感激不尽。 public interface IBaseRepository<TEntity> where TEntity : class { Task<TEntity> QueryByID(object objId); Task<TEntity> QueryByID(object objId, bool blnUseCache = false); Task<List<TEntity>> QueryByIDs(object[] lstIds); Task<int> Add(TEntity model); Task<bool> DeleteById(object id); Task<bool> Delete(TEntity model); Task<bool> DeleteByIds(object[] ids); Task<bool> Update(TEntity model); Task<bool> Update(TEntity entity, string strWhere); Task<bool> Update(TEntity entity, List<string> lstColumns = null, List<string> lstIgnoreColumns = null, string strWhere = ""); Task<List<TEntity>> Query(); Task<List<TEntity>> Query(string strWhere); Task<List<TEntity>> Query(Expression<Func<TEntity, bool>> whereExpression); Task<List<TEntity>> Query(Expression<Func<TEntity, bool>> whereExpression, string strOrderByFileds); Task<List<TEntity>> Query(Expression<Func<TEntity, bool>> whereExpression, Expression<Func<TEntity, object>> orderByExpression, bool isAsc = true); Task<List<TEntity>> Query(string strWhere, string strOrderByFileds); Task<List<TEntity>> Query(Expression<Func<TEntity, bool>> whereExpression, int intTop, string strOrderByFileds); Task<List<TEntity>> Query(string strWhere, int intTop, string strOrderByFileds); Task<List<TEntity>> Query( Expression<Func<TEntity, bool>> whereExpression, int intPageIndex, int intPageSize , string strOrderByFileds); Task<List<TEntity>> Query(string strWhere, int intPageIndex, int intPageSize , string strOrderByFileds); Task<List<TEntity>> QueryPage(Expression<Func<TEntity, bool>> whereExpression, int intPageIndex = 0, int intPageSize = 20, string strOrderByFileds = null); } 二、在Blog.Core.IRepository 层中,将其他的接口,继承Base 还记得前几天我们一直用的IAdvertisementRepository.cs么,终于可以卸到自身代码了,继承Base 然后将其他所有的方法都继承该基类方法,不细说,可以去Github查看。 三、在Blog.Core.Repository 层中,添加BASE文件夹,并添加 BaseRepository.cs 基类 基本写法和前几天的AdvertisementRepository.cs很类似,代码如下: //BaseRepository.cs namespace Blog.Core.Repository.Base { public class BaseRepository<TEntity> : IBaseRepository<TEntity> where TEntity : class, new() { private DbContext context; private SqlSugarClient db; private SimpleClient<TEntity> entityDB; public DbContext Context { get { return context; } set { context = value; } } internal SqlSugarClient Db { get { return db; } private set { db = value; } } internal SimpleClient<TEntity> EntityDB { get { return entityDB; } private set { entityDB = value; } } public BaseRepository() { DbContext.Init(BaseDBConfig.ConnectionString); context = DbContext.GetDbContext(); db = context.Db; entityDB = context.GetEntityDB<TEntity>(db); } public async Task<TEntity> QueryByID(object objId) { return await Task.Run(() => db.Queryable<TEntity>().InSingle(objId)); } /// <summary> /// 功能描述:根据ID查询一条数据 /// 作 者:Blog.Core /// </summary> /// <param name="objId">id(必须指定主键特性 [SugarColumn(IsPrimaryKey=true)]),如果是联合主键,请使用Where条件</param> /// <param name="blnUseCache">是否使用缓存</param> /// <returns>数据实体</returns> public async Task<TEntity> QueryByID(object objId, bool blnUseCache = false) { return await Task.Run(() => db.Queryable<TEntity>().WithCacheIF(blnUseCache).InSingle(objId)); } /// <summary> /// 功能描述:根据ID查询数据 /// 作 者:Blog.Core /// </summary> /// <param name="lstIds">id列表(必须指定主键特性 [SugarColumn(IsPrimaryKey=true)]),如果是联合主键,请使用Where条件</param> /// <returns>数据实体列表</returns> public async Task<List<TEntity>> QueryByIDs(object[] lstIds) { return await Task.Run(() => db.Queryable<TEntity>().In(lstIds).ToList()); } /// <summary> /// 写入实体数据 /// </summary> /// <param name="entity">博文实体类</param> /// <returns></returns> public async Task<int> Add(TEntity entity) { var i = await Task.Run(() => db.Insertable(entity).ExecuteReturnBigIdentity()); //返回的i是long类型,这里你可以根据你的业务需要进行处理 return (int)i; } /// <summary> /// 更新实体数据 /// </summary> /// <param name="entity">博文实体类</param> /// <returns></returns> public async Task<bool> Update(TEntity entity) { //这种方式会以主键为条件 var i = await Task.Run(() => db.Updateable(entity).ExecuteCommand()); return i > 0; } public async Task<bool> Update(TEntity entity, string strWhere) { return await Task.Run(() => db.Updateable(entity).Where(strWhere).ExecuteCommand() > 0); } public async Task<bool> Update(string strSql, SugarParameter[] parameters = null) { return await Task.Run(() => db.Ado.ExecuteCommand(strSql, parameters) > 0); } public async Task<bool> Update( TEntity entity, List<string> lstColumns = null, List<string> lstIgnoreColumns = null, string strWhere = "" ) { IUpdateable<TEntity> up = await Task.Run(() => db.Updateable(entity)); if (lstIgnoreColumns != null && lstIgnoreColumns.Count > 0) { up = await Task.Run(() => up.IgnoreColumns(it => lstIgnoreColumns.Contains(it))); } if (lstColumns != null && lstColumns.Count > 0) { up = await Task.Run(() => up.UpdateColumns(it => lstColumns.Contains(it))); } if (!string.IsNullOrEmpty(strWhere)) { up = await Task.Run(() => up.Where(strWhere)); } return await Task.Run(() => up.ExecuteCommand()) > 0; } /// <summary> /// 根据实体删除一条数据 /// </summary> /// <param name="entity">博文实体类</param> /// <returns></returns> public async Task<bool> Delete(TEntity entity) { var i = await Task.Run(() => db.Deleteable(entity).ExecuteCommand()); return i > 0; } /// <summary> /// 删除指定ID的数据 /// </summary> /// <param name="id">主键ID</param> /// <returns></returns> public async Task<bool> DeleteById(object id) { var i = await Task.Run(() => db.Deleteable<TEntity>(id).ExecuteCommand()); return i > 0; } /// <summary> /// 删除指定ID集合的数据(批量删除) /// </summary> /// <param name="ids">主键ID集合</param> /// <returns></returns> public async Task<bool> DeleteByIds(object[] ids) { var i = await Task.Run(() => db.Deleteable<TEntity>().In(ids).ExecuteCommand()); return i > 0; } /// <summary> /// 功能描述:查询所有数据 /// 作 者:Blog.Core /// </summary> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query() { return await Task.Run(() => entityDB.GetList()); } /// <summary> /// 功能描述:查询数据列表 /// 作 者:Blog.Core /// </summary> /// <param name="strWhere">条件</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query(string strWhere) { return await Task.Run(() => db.Queryable<TEntity>().WhereIF(!string.IsNullOrEmpty(strWhere), strWhere).ToList()); } /// <summary> /// 功能描述:查询数据列表 /// 作 者:Blog.Core /// </summary> /// <param name="whereExpression">whereExpression</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query(Expression<Func<TEntity, bool>> whereExpression) { return await Task.Run(() => entityDB.GetList(whereExpression)); } /// <summary> /// 功能描述:查询一个列表 /// 作 者:Blog.Core /// </summary> /// <param name="whereExpression">条件表达式</param> /// <param name="strOrderByFileds">排序字段,如name asc,age desc</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query(Expression<Func<TEntity, bool>> whereExpression, string strOrderByFileds) { return await Task.Run(() => db.Queryable<TEntity>().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(whereExpression != null, whereExpression).ToList()); } /// <summary> /// 功能描述:查询一个列表 /// </summary> /// <param name="whereExpression"></param> /// <param name="orderByExpression"></param> /// <param name="isAsc"></param> /// <returns></returns> public async Task<List<TEntity>> Query(Expression<Func<TEntity, bool>> whereExpression, Expression<Func<TEntity, object>> orderByExpression, bool isAsc = true) { return await Task.Run(() => db.Queryable<TEntity>().OrderByIF(orderByExpression != null, orderByExpression, isAsc ? OrderByType.Asc : OrderByType.Desc).WhereIF(whereExpression != null, whereExpression).ToList()); } /// <summary> /// 功能描述:查询一个列表 /// 作 者:Blog.Core /// </summary> /// <param name="strWhere">条件</param> /// <param name="strOrderByFileds">排序字段,如name asc,age desc</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query(string strWhere, string strOrderByFileds) { return await Task.Run(() => db.Queryable<TEntity>().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(!string.IsNullOrEmpty(strWhere), strWhere).ToList()); } /// <summary> /// 功能描述:查询前N条数据 /// 作 者:Blog.Core /// </summary> /// <param name="whereExpression">条件表达式</param> /// <param name="intTop">前N条</param> /// <param name="strOrderByFileds">排序字段,如name asc,age desc</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query( Expression<Func<TEntity, bool>> whereExpression, int intTop, string strOrderByFileds) { return await Task.Run(() => db.Queryable<TEntity>().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(whereExpression != null, whereExpression).Take(intTop).ToList()); } /// <summary> /// 功能描述:查询前N条数据 /// 作 者:Blog.Core /// </summary> /// <param name="strWhere">条件</param> /// <param name="intTop">前N条</param> /// <param name="strOrderByFileds">排序字段,如name asc,age desc</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query( string strWhere, int intTop, string strOrderByFileds) { return await Task.Run(() => db.Queryable<TEntity>().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(!string.IsNullOrEmpty(strWhere), strWhere).Take(intTop).ToList()); } /// <summary> /// 功能描述:分页查询 /// 作 者:Blog.Core /// </summary> /// <param name="whereExpression">条件表达式</param> /// <param name="intPageIndex">页码(下标0)</param> /// <param name="intPageSize">页大小</param> /// <param name="intTotalCount">数据总量</param> /// <param name="strOrderByFileds">排序字段,如name asc,age desc</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query( Expression<Func<TEntity, bool>> whereExpression, int intPageIndex, int intPageSize, string strOrderByFileds) { return await Task.Run(() => db.Queryable<TEntity>().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(whereExpression != null, whereExpression).ToPageList(intPageIndex, intPageSize)); } /// <summary> /// 功能描述:分页查询 /// 作 者:Blog.Core /// </summary> /// <param name="strWhere">条件</param> /// <param name="intPageIndex">页码(下标0)</param> /// <param name="intPageSize">页大小</param> /// <param name="intTotalCount">数据总量</param> /// <param name="strOrderByFileds">排序字段,如name asc,age desc</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query( string strWhere, int intPageIndex, int intPageSize, string strOrderByFileds) { return await Task.Run(() => db.Queryable<TEntity>().OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds).WhereIF(!string.IsNullOrEmpty(strWhere), strWhere).ToPageList(intPageIndex, intPageSize)); } public async Task<List<TEntity>> QueryPage(Expression<Func<TEntity, bool>> whereExpression, int intPageIndex = 0, int intPageSize = 20, string strOrderByFileds = null) { return await Task.Run(() => db.Queryable<TEntity>() .OrderByIF(!string.IsNullOrEmpty(strOrderByFileds), strOrderByFileds) .WhereIF(whereExpression != null, whereExpression) .ToPageList(intPageIndex, intPageSize)); } } } View Code 然后呢,同样在在Blog.Core.Repository 层中,将其他的接口,继承BaseRepository,这里略过,按下不表。 这里要说下,昨天有人问我DbContext.cs内容讲一下,其实呢,这个类里,很简单,主要是1、获取SqlSugarClient实例,2、CodeFirst(根据实体类生成数据库表);3、DbFirst(根据数据库表生成实体类)。只不过都是用到的同名方法重载,可能看上去比较累,可以调用一次就能明白了。 这里简单说下DbFirst吧,其他的可以自行研究下,也可以右侧扫码加QQ群,我们一对一讨论。DbFirst是一个根据数据库表生成实体类的过程,前提是要有系统表的权限,否则无法读取表的结构,框架在底层封装了很多模板,将真实值填充进去,特别像是动软代码生成器或者T4模板。 四、同样在Blog.Core.IServices 和 Blog.Core.Services 层中,分别添加基接口,并实现基类 public interface IBaseServices<TEntity> where TEntity : class { Task<TEntity> QueryByID(object objId); Task<TEntity> QueryByID(object objId, bool blnUseCache = false); Task<List<TEntity>> QueryByIDs(object[] lstIds); Task<int> Add(TEntity model); Task<bool> DeleteById(object id); Task<bool> Delete(TEntity model); Task<bool> DeleteByIds(object[] ids); Task<bool> Update(TEntity model); Task<bool> Update(TEntity entity, string strWhere); Task<bool> Update(TEntity entity, List<string> lstColumns = null, List<string> lstIgnoreColumns = null, string strWhere = ""); Task<List<TEntity>> Query(); Task<List<TEntity>> Query(string strWhere); Task<List<TEntity>> Query(Expression<Func<TEntity, bool>> whereExpression); Task<List<TEntity>> Query(Expression<Func<TEntity, bool>> whereExpression, string strOrderByFileds); Task<List<TEntity>> Query(Expression<Func<TEntity, bool>> whereExpression, Expression<Func<TEntity, object>> orderByExpression, bool isAsc = true); Task<List<TEntity>> Query(string strWhere, string strOrderByFileds); Task<List<TEntity>> Query(Expression<Func<TEntity, bool>> whereExpression, int intTop, string strOrderByFileds); Task<List<TEntity>> Query(string strWhere, int intTop, string strOrderByFileds); Task<List<TEntity>> Query( Expression<Func<TEntity, bool>> whereExpression, int intPageIndex, int intPageSize, string strOrderByFileds); Task<List<TEntity>> Query(string strWhere, int intPageIndex, int intPageSize, string strOrderByFileds); Task<List<TEntity>> QueryPage(Expression<Func<TEntity, bool>> whereExpression, int intPageIndex = 0, int intPageSize = 20, string strOrderByFileds = null); } //BaseServices.cs namespace Blog.Core.Services.BASE { public class BaseServices<TEntity> : IBaseServices<TEntity> where TEntity : class, new() { public IBaseRepository<TEntity> baseDal = new BaseRepository<TEntity>(); public async Task<TEntity> QueryByID(object objId) { return await baseDal.QueryByID(objId); } /// <summary> /// 功能描述:根据ID查询一条数据 /// 作 者:AZLinli.Blog.Core /// </summary> /// <param name="objId">id(必须指定主键特性 [SugarColumn(IsPrimaryKey=true)]),如果是联合主键,请使用Where条件</param> /// <param name="blnUseCache">是否使用缓存</param> /// <returns>数据实体</returns> public async Task<TEntity> QueryByID(object objId, bool blnUseCache = false) { return await baseDal.QueryByID(objId, blnUseCache); } /// <summary> /// 功能描述:根据ID查询数据 /// 作 者:AZLinli.Blog.Core /// </summary> /// <param name="lstIds">id列表(必须指定主键特性 [SugarColumn(IsPrimaryKey=true)]),如果是联合主键,请使用Where条件</param> /// <returns>数据实体列表</returns> public async Task<List<TEntity>> QueryByIDs(object[] lstIds) { return await baseDal.QueryByIDs(lstIds); } /// <summary> /// 写入实体数据 /// </summary> /// <param name="entity">博文实体类</param> /// <returns></returns> public async Task<int> Add(TEntity entity) { return await baseDal.Add(entity); } /// <summary> /// 更新实体数据 /// </summary> /// <param name="entity">博文实体类</param> /// <returns></returns> public async Task<bool> Update(TEntity entity) { return await baseDal.Update(entity); } public async Task<bool> Update(TEntity entity, string strWhere) { return await baseDal.Update(entity, strWhere); } public async Task<bool> Update( TEntity entity, List<string> lstColumns = null, List<string> lstIgnoreColumns = null, string strWhere = "" ) { return await baseDal.Update(entity, lstColumns, lstIgnoreColumns, strWhere); } /// <summary> /// 根据实体删除一条数据 /// </summary> /// <param name="entity">博文实体类</param> /// <returns></returns> public async Task<bool> Delete(TEntity entity) { return await baseDal.Delete(entity); } /// <summary> /// 删除指定ID的数据 /// </summary> /// <param name="id">主键ID</param> /// <returns></returns> public async Task<bool> DeleteById(object id) { return await baseDal.DeleteById(id); } /// <summary> /// 删除指定ID集合的数据(批量删除) /// </summary> /// <param name="ids">主键ID集合</param> /// <returns></returns> public async Task<bool> DeleteByIds(object[] ids) { return await baseDal.DeleteByIds(ids); } /// <summary> /// 功能描述:查询所有数据 /// 作 者:AZLinli.Blog.Core /// </summary> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query() { return await baseDal.Query(); } /// <summary> /// 功能描述:查询数据列表 /// 作 者:AZLinli.Blog.Core /// </summary> /// <param name="strWhere">条件</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query(string strWhere) { return await baseDal.Query(strWhere); } /// <summary> /// 功能描述:查询数据列表 /// 作 者:AZLinli.Blog.Core /// </summary> /// <param name="whereExpression">whereExpression</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query(Expression<Func<TEntity, bool>> whereExpression) { return await baseDal.Query(whereExpression); } /// <summary> /// 功能描述:查询一个列表 /// 作 者:AZLinli.Blog.Core /// </summary> /// <param name="whereExpression">条件表达式</param> /// <param name="strOrderByFileds">排序字段,如name asc,age desc</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query(Expression<Func<TEntity, bool>> whereExpression, Expression<Func<TEntity, object>> orderByExpression, bool isAsc = true) { return await baseDal.Query(whereExpression, orderByExpression, isAsc); } public async Task<List<TEntity>> Query(Expression<Func<TEntity, bool>> whereExpression, string strOrderByFileds) { return await baseDal.Query(whereExpression, strOrderByFileds); } /// <summary> /// 功能描述:查询一个列表 /// 作 者:AZLinli.Blog.Core /// </summary> /// <param name="strWhere">条件</param> /// <param name="strOrderByFileds">排序字段,如name asc,age desc</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query(string strWhere, string strOrderByFileds) { return await baseDal.Query(strWhere, strOrderByFileds); } /// <summary> /// 功能描述:查询前N条数据 /// 作 者:AZLinli.Blog.Core /// </summary> /// <param name="whereExpression">条件表达式</param> /// <param name="intTop">前N条</param> /// <param name="strOrderByFileds">排序字段,如name asc,age desc</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query(Expression<Func<TEntity, bool>> whereExpression, int intTop, string strOrderByFileds) { return await baseDal.Query(whereExpression, intTop, strOrderByFileds); } /// <summary> /// 功能描述:查询前N条数据 /// 作 者:AZLinli.Blog.Core /// </summary> /// <param name="strWhere">条件</param> /// <param name="intTop">前N条</param> /// <param name="strOrderByFileds">排序字段,如name asc,age desc</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query( string strWhere, int intTop, string strOrderByFileds) { return await baseDal.Query(strWhere, intTop, strOrderByFileds); } /// <summary> /// 功能描述:分页查询 /// 作 者:AZLinli.Blog.Core /// </summary> /// <param name="whereExpression">条件表达式</param> /// <param name="intPageIndex">页码(下标0)</param> /// <param name="intPageSize">页大小</param> /// <param name="intTotalCount">数据总量</param> /// <param name="strOrderByFileds">排序字段,如name asc,age desc</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query( Expression<Func<TEntity, bool>> whereExpression, int intPageIndex, int intPageSize, string strOrderByFileds) { return await baseDal.Query( whereExpression, intPageIndex, intPageSize, strOrderByFileds); } /// <summary> /// 功能描述:分页查询 /// 作 者:AZLinli.Blog.Core /// </summary> /// <param name="strWhere">条件</param> /// <param name="intPageIndex">页码(下标0)</param> /// <param name="intPageSize">页大小</param> /// <param name="intTotalCount">数据总量</param> /// <param name="strOrderByFileds">排序字段,如name asc,age desc</param> /// <returns>数据列表</returns> public async Task<List<TEntity>> Query( string strWhere, int intPageIndex, int intPageSize, string strOrderByFileds) { return await baseDal.Query( strWhere, intPageIndex, intPageSize, strOrderByFileds); } public async Task<List<TEntity>> QueryPage(Expression<Func<TEntity, bool>> whereExpression, int intPageIndex = 0, int intPageSize = 20, string strOrderByFileds = null) { return await baseDal.QueryPage(whereExpression, intPageIndex = 0, intPageSize, strOrderByFileds); } } } View Code 五、运行项目,并调试接口 这个时候,需要把接口改成异步请求方式: // GET: api/Blog/5 /// <summary> /// 根据id获取数据 /// </summary> /// <param name="id">参数id</param> /// <returns></returns> [HttpGet("{id}", Name = "Get")] public async Task<List<Advertisement>> Get(int id) { IAdvertisementServices advertisementServices = new AdvertisementServices(); return await advertisementServices.Query(d => d.Id == id); } Http返回200,一切正常。 六、初探依赖注入 首先,我们需要了解下什么是控制反转IOC,举个栗子,我在之前开发简单商城的时候,其中呢,订单模块,有订单表,那里边肯定有订单详情表,而且呢订单详情表中还有商品信息表,商品信息表还关联了价格规格表,或者其他的物流信息,商家信息,当然,我们可以放到一个大表里,可是你一定不会这么做,因为太庞大,所以必定分表,那必定会出现类中套类的局面,这就是依赖,比如上边的,订单表就依赖了详情表,我们在实例化订单实体类的时候,也需要手动实例详情表,当然,EF框架中,会自动生成。不过倘若有一个程序员把详情表实体类改错了,那订单表就崩溃了,哦不!我是遇到过这样的情景。 怎么解决这个问题呢,就出现了控制反转。网上看到一个挺好的讲解: 1、没有引入IOC之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,A直接使用new关键字创建B的实例,程序高度耦合,效率低下,无论是创建还是使用B对象,控制权都在自己手上。 2、软件系统在引入IOC容器之后,这种情形就完全改变了,由于IOC容器的加入,对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。 3、依赖注入,是指程序运行过程中,如果需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部的注入。Spring的依赖注入对调用者和被调用者几乎没有任何要求,完全支持对POJO之间依赖关系的管理。依赖注入通常有两种:·设值注入。·构造注入。 这个就是依赖注入的方式。 什么是控制反转(IoC) Inversion of Control,英文缩写为IoC,不是什么技术,而是一种设计思想。 简单来说就是把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦,如下图: 大家看到了吧,由于引进了中间位置的“第三方”,也就是IOC容器,使得A、B、C、D这4个对象没有了耦合关系,齿轮之间的传动全部依靠“第三方”了,全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。 我们再来做个试验:把上图中间的IOC容器拿掉,然后再来看看这套系统: 我们现在看到的画面,就是我们要实现整个系统所需要完成的全部内容。这时候,A、B、C、D这4个对象之间已经没有了耦合关系,彼此毫无联系,这样的话,当你在实现A的时候,根本无须再去考虑B、C和D了,对象之间的依赖关系已经降低到了最低程度。所以,如果真能实现IOC容器,对于系统开发而言,这将是一件多么美好的事情,参与开发的每一成员只要实现自己的类就可以了,跟别人没有任何关系! 作者地址:https://blog.csdn.net/zlts000/article/details/51533459 因为时间和篇幅的关系,今天在项目中,暂时不引入Autofac了,下周我们继续深入了解。 七、结语 写文章原来也是一个体力活,嗯加油!今天终于将后端框架补充了下,实现了基本的功能,重点讲解了如何在仓储模式中,使用基类泛型,当然这是一个思想,你也可以在开发的过程中,多使用抽象类,接口编程; 然后呢,又简单的使用了异步编程,现在也是很流行的一直写法,我也是刚使用,大家欢迎批评指正; 简单的了解了下,IOC控制反转和DI依赖注入,为下次做准备; 当然,现在才仅仅是一个雏形,以后还会用到AOP的日志,异常记录;Redis的缓存等,慢慢来吧,给自己加加油! 八、CODE https://github.com/anjoy8/Blog.Core.git https://gitee.com/laozhangIsPhi/Blog.Core
代码已上传Github+Gitee,文末有地址 书接上文:《从壹开始前后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之六 || API项目整体搭建 6.1 仓储》,我们简单的对整体项目进行搭建,用到了项目中常见的仓储模式+面向接口编程,核心的一共是六层,当然你也可以根据自己的需求进行扩展,比如我在其他的项目中会用到Common层,当然我们这个项目接下来也会有,或者我还会添加Task层,主要是作为定时项目使用,我之前用的是Task Schedule,基本能满足需求。 缘起 在上一节中,我们最后提出了两个问题,不知道大家是否还记得,这里还重新说明一下: 1、如果每个仓储文件都需要把一个一个写出来,至少是四遍,会不会太麻烦,而且无法复用,失去了面向接口编程的意义; 2、每次接口调用的时候,需要引入很多命名空间,比如Blog.Core.IServices;Blog.Core.Services;Blog.Core.Repository等等 对就是这两个问题,相信聪明的大家也都能看懂,或许还能给出相应的解决办法,比如泛型仓储,比如依赖注入,当然,如果你有更好的办法,欢迎留言,我会把你的想法写下了,让大家一起进步。这里先简单说下问题1中为什么要四遍,仓储模式的基本就是如何将持久化动作和对象获取方式以及领域模型Domain Model结合起来,进一步:如何更加统一我们的语言(Ubiquitous Language),一个整合持久化技术的好办法是仓储Repositories。明白了这个问题,你就知道,定义仓储,首先需要定义IRepository接口(1),然后再Repository中实现(2),接着在IService层中引用这些接口,同时也可以自定义扩展业务逻辑接口(3),最后在Service层中去实现(4),这就是四层。 问题明白了,我们就要动手做起来,思考了下,如果干巴巴直接写泛型仓储,会比较干涩,所以我考虑今天先把数据持久化做出来,一个轻量级的ORM框架——SqlSugar。 零、今天完成的蓝色部分 一、在Blog.Core.IRepository 层中添加CURD接口 还记得昨天我们实现的Sum接口么,今天在仓储接口 IAdvertisementRepository.cs 添加CURD四个接口,首先需要将Model层添加引用,这个应该都会,以后不再细说,如下: namespace Blog.Core.IRepository { public interface IAdvertisementRepository { int Sum(int i, int j); int Add(Advertisement model); bool Delete(Advertisement model); bool Update(Advertisement model); List<Advertisement> Query(Expression<Func<Advertisement, bool>> whereExpression); } } 编译项目,提示错误,别慌!很正常,因为我们现在只是添加了接口,还没有实现接口。 二、在Blog.Core.Repository 层中实现Blog.Core.IRepository 所定义的CURD接口 当然,我们还是在AdvertisementRepository.cs文件中操作,这里我有一个小技巧,不知道大家是否用到过,因为我比较喜欢写接口,这样不仅可以不暴露核心代码,而且也可以让用户调用的时候,直接看到简单的接口方法列表,而不去管具体的实现过程,这样的设计思路还是比较提倡的,如下图: 你先看到了继承的接口有红色的波浪线,证明有错误,然后右键该接口,点击 Quick Actions and Refactorings...,也就是 快速操作和重构 ,你就会看到VS的智能提示,双击左侧的Implement interface,也就是实现接口,如下图: Visual Studio真是宇宙第一IDE,没的说 [手动点赞],然后就创建成功了,你就可以去掉throw处理,自定义代码编写了,当然,如果你不习惯或者害怕出错,那就手动写吧,也是很快的。 namespace Blog.Core.Repository { public class AdvertisementRepository : IAdvertisementRepository { public int Add(Advertisement model) { throw new NotImplementedException(); } public bool Delete(Advertisement model) { throw new NotImplementedException(); } public List<Advertisement> Query(Expression<Func<Advertisement, bool>> whereExpression) { throw new NotImplementedException(); } public int Sum(int i, int j) { return i + j; } public bool Update(Advertisement model) { throw new NotImplementedException(); } } } 这个时候我们重新编译项目,嗯!意料之中,没有错误,但是具体的数据持久化如何写呢? 三、引用轻量级的ORM框架——SqlSugar 首先什么是ORM, 对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系数据库中。这些概念我就不细说了,自从开发这些年,一直在讨论的问题就是用ADO.NET还是用ORM框架,还记得前几年面试的时候,有一个经理问: 如果一个项目,你是用三层架构ADO,还是用ORM中的EF? 大家可以自由留言,我表示各有千秋吧,一个产品的存在即有合理性,我平时项目中也有ADO,也有EF,不过本系列教程中基于面向对象思想,面向接口思想,当然还有以后的面向切面编程(AOP),还是使用ORM框架,不过是一个轻量级的,EF比较重,我在我其他的项目中用到了.Net MVC 6.0 + EF Code First 的项目,如果大家需要,我也开源出去,方法Github上,请文末留言吧~ 关于ORM有一些常见的框架,如SqlSugar、Dapper、EF、NHeberneit等等,这些我都或多或少的了解过,使用过,至于你要问我为啥用SqlSugar,只要一个原因,作者是中国人,嗯!没错,这里给他打个广告,本系列中的前端框架Vue,也是我们中国的,Vue作者尤雨溪,这里也祝福大家都能有自己的成绩,为国人争光! 扯远了,开始动手引入框架: 开始,我们需要先向 Repository 层中引入SqlSugar,如下: 1)直接在类库中通过Nuget引入 sqlSugarCore,一定是Core版本的!,我个人采用这个办法,因为项目已经比较成型 2)Github下载源码,然后项目引用(点击跳转到Github下载页) 注意:为什么要单独在仓储层来引入ORM持久化接口,是因为,降低耦合,如果以后想要换成EF或者Deper,只需要修改Repository就行了,其他都不需要修改,达到很好的解耦效果。 编译一切正常,继续 首先呢,你需要了解下sqlsugar的具体使用方法,http://www.codeisbug.com/Doc/8,你先自己在控制台可以简单试一试,这里就不细说了,如果大家有需要,我可以单开一个文章,重点讲解SqlSugar这一块。 1、在Blog.Core.Repository新建一个sugar文件夹,然后添加两个配置文件,BaseDBConfig.cs 和 DbContext.cs ,这个你如果看了上边的文档,那这两个应该就不是问题。 namespace Blog.Core.Repository { public class BaseDBConfig { public static string ConnectionString = File.ReadAllText(@"D:\my-file\dbCountPsw1.txt").Trim(); //正常格式是 //public static string ConnectionString = "server=.;uid=sa;pwd=sa;database=BlogDB"; //原谅我用配置文件的形式,因为我直接调用的是我的服务器账号和密码,安全起见 } } //DbContext.cs,一个详细的上下文类,看不懂没关系,以后我会详细讲解 namespace Blog.Core.Repository { public class DbContext { private static string _connectionString; private static DbType _dbType; private SqlSugarClient _db; /// <summary> /// 连接字符串 /// Blog.Core /// </summary> public static string ConnectionString { get { return _connectionString; } set { _connectionString = value; } } /// <summary> /// 数据库类型 /// Blog.Core /// </summary> public static DbType DbType { get { return _dbType; } set { _dbType = value; } } /// <summary> /// 数据连接对象 /// Blog.Core /// </summary> public SqlSugarClient Db { get { return _db; } private set { _db = value; } } /// <summary> /// 数据库上下文实例(自动关闭连接) /// Blog.Core /// </summary> public static DbContext Context { get { return new DbContext(); } } /// <summary> /// 功能描述:构造函数 /// 作 者:Blog.Core /// </summary> private DbContext() { if (string.IsNullOrEmpty(_connectionString)) throw new ArgumentNullException("数据库连接字符串为空"); _db = new SqlSugarClient(new ConnectionConfig() { ConnectionString = _connectionString, DbType = _dbType, IsAutoCloseConnection = true, IsShardSameThread = true, ConfigureExternalServices = new ConfigureExternalServices() { //DataInfoCacheService = new HttpRuntimeCache() }, MoreSettings = new ConnMoreSettings() { //IsWithNoLockQuery = true, IsAutoRemoveDataCache = true } }); } /// <summary> /// 功能描述:构造函数 /// 作 者:Blog.Core /// </summary> /// <param name="blnIsAutoCloseConnection">是否自动关闭连接</param> private DbContext(bool blnIsAutoCloseConnection) { if (string.IsNullOrEmpty(_connectionString)) throw new ArgumentNullException("数据库连接字符串为空"); _db = new SqlSugarClient(new ConnectionConfig() { ConnectionString = _connectionString, DbType = _dbType, IsAutoCloseConnection = blnIsAutoCloseConnection, IsShardSameThread = true, ConfigureExternalServices = new ConfigureExternalServices() { //DataInfoCacheService = new HttpRuntimeCache() }, MoreSettings = new ConnMoreSettings() { //IsWithNoLockQuery = true, IsAutoRemoveDataCache = true } }); } #region 实例方法 /// <summary> /// 功能描述:获取数据库处理对象 /// 作 者:Blog.Core /// </summary> /// <returns>返回值</returns> public SimpleClient<T> GetEntityDB<T>() where T : class, new() { return new SimpleClient<T>(_db); } /// <summary> /// 功能描述:获取数据库处理对象 /// 作 者:Blog.Core /// </summary> /// <param name="db">db</param> /// <returns>返回值</returns> public SimpleClient<T> GetEntityDB<T>(SqlSugarClient db) where T : class, new() { return new SimpleClient<T>(db); } #region 根据数据库表生产实体类 /// <summary> /// 功能描述:根据数据库表生产实体类 /// 作 者:Blog.Core /// </summary> /// <param name="strPath">实体类存放路径</param> public void CreateClassFileByDBTalbe(string strPath) { CreateClassFileByDBTalbe(strPath, "Km.PosZC"); } /// <summary> /// 功能描述:根据数据库表生产实体类 /// 作 者:Blog.Core /// </summary> /// <param name="strPath">实体类存放路径</param> /// <param name="strNameSpace">命名空间</param> public void CreateClassFileByDBTalbe(string strPath, string strNameSpace) { CreateClassFileByDBTalbe(strPath, strNameSpace, null); } /// <summary> /// 功能描述:根据数据库表生产实体类 /// 作 者:Blog.Core /// </summary> /// <param name="strPath">实体类存放路径</param> /// <param name="strNameSpace">命名空间</param> /// <param name="lstTableNames">生产指定的表</param> public void CreateClassFileByDBTalbe( string strPath, string strNameSpace, string[] lstTableNames) { CreateClassFileByDBTalbe(strPath, strNameSpace, lstTableNames, string.Empty); } /// <summary> /// 功能描述:根据数据库表生产实体类 /// 作 者:Blog.Core /// </summary> /// <param name="strPath">实体类存放路径</param> /// <param name="strNameSpace">命名空间</param> /// <param name="lstTableNames">生产指定的表</param> /// <param name="strInterface">实现接口</param> public void CreateClassFileByDBTalbe( string strPath, string strNameSpace, string[] lstTableNames, string strInterface, bool blnSerializable = false) { if (lstTableNames != null && lstTableNames.Length > 0) { _db.DbFirst.Where(lstTableNames).IsCreateDefaultValue().IsCreateAttribute() .SettingClassTemplate(p => p = @" {using} namespace {Namespace} { {ClassDescription}{SugarTable}" + (blnSerializable ? "[Serializable]" : "") + @" public partial class {ClassName}" + (string.IsNullOrEmpty(strInterface) ? "" : (" : " + strInterface)) + @" { public {ClassName}() { {Constructor} } {PropertyName} } } ") .SettingPropertyTemplate(p => p = @" {SugarColumn} public {PropertyType} {PropertyName} { get { return _{PropertyName}; } set { if(_{PropertyName}!=value) { base.SetValueCall(" + "\"{PropertyName}\",_{PropertyName}" + @"); } _{PropertyName}=value; } }") .SettingPropertyDescriptionTemplate(p => p = " private {PropertyType} _{PropertyName};\r\n" + p) .SettingConstructorTemplate(p => p = " this._{PropertyName} ={DefaultValue};") .CreateClassFile(strPath, strNameSpace); } else { _db.DbFirst.IsCreateAttribute().IsCreateDefaultValue() .SettingClassTemplate(p => p = @" {using} namespace {Namespace} { {ClassDescription}{SugarTable}" + (blnSerializable ? "[Serializable]" : "") + @" public partial class {ClassName}" + (string.IsNullOrEmpty(strInterface) ? "" : (" : " + strInterface)) + @" { public {ClassName}() { {Constructor} } {PropertyName} } } ") .SettingPropertyTemplate(p => p = @" {SugarColumn} public {PropertyType} {PropertyName} { get { return _{PropertyName}; } set { if(_{PropertyName}!=value) { base.SetValueCall(" + "\"{PropertyName}\",_{PropertyName}" + @"); } _{PropertyName}=value; } }") .SettingPropertyDescriptionTemplate(p => p = " private {PropertyType} _{PropertyName};\r\n" + p) .SettingConstructorTemplate(p => p = " this._{PropertyName} ={DefaultValue};") .CreateClassFile(strPath, strNameSpace); } } #endregion #region 根据实体类生成数据库表 /// <summary> /// 功能描述:根据实体类生成数据库表 /// 作 者:Blog.Core /// </summary> /// <param name="blnBackupTable">是否备份表</param> /// <param name="lstEntitys">指定的实体</param> public void CreateTableByEntity<T>(bool blnBackupTable, params T[] lstEntitys) where T : class, new() { Type[] lstTypes = null; if (lstEntitys != null) { lstTypes = new Type[lstEntitys.Length]; for (int i = 0; i < lstEntitys.Length; i++) { T t = lstEntitys[i]; lstTypes[i] = typeof(T); } } CreateTableByEntity(blnBackupTable, lstTypes); } /// <summary> /// 功能描述:根据实体类生成数据库表 /// 作 者:Blog.Core /// </summary> /// <param name="blnBackupTable">是否备份表</param> /// <param name="lstEntitys">指定的实体</param> public void CreateTableByEntity(bool blnBackupTable, params Type[] lstEntitys) { if (blnBackupTable) { _db.CodeFirst.BackupTable().InitTables(lstEntitys); //change entity backupTable } else { _db.CodeFirst.InitTables(lstEntitys); } } #endregion #endregion #region 静态方法 /// <summary> /// 功能描述:获得一个DbContext /// 作 者:Blog.Core /// </summary> /// <param name="blnIsAutoCloseConnection">是否自动关闭连接(如果为false,则使用接受时需要手动关闭Db)</param> /// <returns>返回值</returns> public static DbContext GetDbContext(bool blnIsAutoCloseConnection = true) { return new DbContext(blnIsAutoCloseConnection); } /// <summary> /// 功能描述:设置初始化参数 /// 作 者:Blog.Core /// </summary> /// <param name="strConnectionString">连接字符串</param> /// <param name="enmDbType">数据库类型</param> public static void Init(string strConnectionString, DbType enmDbType = SqlSugar.DbType.SqlServer) { _connectionString = strConnectionString; _dbType = enmDbType; } /// <summary> /// 功能描述:创建一个链接配置 /// 作 者:Blog.Core /// </summary> /// <param name="blnIsAutoCloseConnection">是否自动关闭连接</param> /// <param name="blnIsShardSameThread">是否夸类事务</param> /// <returns>ConnectionConfig</returns> public static ConnectionConfig GetConnectionConfig(bool blnIsAutoCloseConnection = true, bool blnIsShardSameThread = false) { ConnectionConfig config = new ConnectionConfig() { ConnectionString = _connectionString, DbType = _dbType, IsAutoCloseConnection = blnIsAutoCloseConnection, ConfigureExternalServices = new ConfigureExternalServices() { //DataInfoCacheService = new HttpRuntimeCache() }, IsShardSameThread = blnIsShardSameThread }; return config; } /// <summary> /// 功能描述:获取一个自定义的DB /// 作 者:Blog.Core /// </summary> /// <param name="config">config</param> /// <returns>返回值</returns> public static SqlSugarClient GetCustomDB(ConnectionConfig config) { return new SqlSugarClient(config); } /// <summary> /// 功能描述:获取一个自定义的数据库处理对象 /// 作 者:Blog.Core /// </summary> /// <param name="sugarClient">sugarClient</param> /// <returns>返回值</returns> public static SimpleClient<T> GetCustomEntityDB<T>(SqlSugarClient sugarClient) where T : class, new() { return new SimpleClient<T>(sugarClient); } /// <summary> /// 功能描述:获取一个自定义的数据库处理对象 /// 作 者:Blog.Core /// </summary> /// <param name="config">config</param> /// <returns>返回值</returns> public static SimpleClient<T> GetCustomEntityDB<T>(ConnectionConfig config) where T : class, new() { SqlSugarClient sugarClient = GetCustomDB(config); return GetCustomEntityDB<T>(sugarClient); } #endregion } } View Code 2、然后在刚刚我们实现那四个方法的AdvertisementRepository.cs中,重写构造函数,编辑统一Sqlsugar实例方法,用到了私有属性,为以后的单列模式做准备。 private DbContext context; private SqlSugarClient db; private SimpleClient<Advertisement> entityDB; internal SqlSugarClient Db { get { return db; } private set { db = value; } } public DbContext Context { get { return context; } set { context = value; } } public AdvertisementRepository() { DbContext.Init(BaseDBConfig.ConnectionString); context = DbContext.GetDbContext(); db = context.Db; entityDB = context.GetEntityDB<Advertisement>(db); } 3、正式开始写持久化逻辑代码(注意:我在Model层中,添加了全局的数据类型转换方法,UtilConvert,这样就不用每次都Convert,而且也解决了为空转换异常的bug) public static class UtilConvert { /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <returns></returns> public static int ObjToInt(this object thisValue) { int reval = 0; if (thisValue == null) return 0; if (thisValue != null && thisValue != DBNull.Value && int.TryParse(thisValue.ToString(), out reval)) { return reval; } return reval; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <param name="errorValue"></param> /// <returns></returns> public static int ObjToInt(this object thisValue, int errorValue) { int reval = 0; if (thisValue != null && thisValue != DBNull.Value && int.TryParse(thisValue.ToString(), out reval)) { return reval; } return errorValue; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <returns></returns> public static double ObjToMoney(this object thisValue) { double reval = 0; if (thisValue != null && thisValue != DBNull.Value && double.TryParse(thisValue.ToString(), out reval)) { return reval; } return 0; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <param name="errorValue"></param> /// <returns></returns> public static double ObjToMoney(this object thisValue, double errorValue) { double reval = 0; if (thisValue != null && thisValue != DBNull.Value && double.TryParse(thisValue.ToString(), out reval)) { return reval; } return errorValue; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <returns></returns> public static string ObjToString(this object thisValue) { if (thisValue != null) return thisValue.ToString().Trim(); return ""; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <param name="errorValue"></param> /// <returns></returns> public static string ObjToString(this object thisValue, string errorValue) { if (thisValue != null) return thisValue.ToString().Trim(); return errorValue; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <returns></returns> public static Decimal ObjToDecimal(this object thisValue) { Decimal reval = 0; if (thisValue != null && thisValue != DBNull.Value && decimal.TryParse(thisValue.ToString(), out reval)) { return reval; } return 0; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <param name="errorValue"></param> /// <returns></returns> public static Decimal ObjToDecimal(this object thisValue, decimal errorValue) { Decimal reval = 0; if (thisValue != null && thisValue != DBNull.Value && decimal.TryParse(thisValue.ToString(), out reval)) { return reval; } return errorValue; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <returns></returns> public static DateTime ObjToDate(this object thisValue) { DateTime reval = DateTime.MinValue; if (thisValue != null && thisValue != DBNull.Value && DateTime.TryParse(thisValue.ToString(), out reval)) { reval = Convert.ToDateTime(thisValue); } return reval; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <param name="errorValue"></param> /// <returns></returns> public static DateTime ObjToDate(this object thisValue, DateTime errorValue) { DateTime reval = DateTime.MinValue; if (thisValue != null && thisValue != DBNull.Value && DateTime.TryParse(thisValue.ToString(), out reval)) { return reval; } return errorValue; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <returns></returns> public static bool ObjToBool(this object thisValue) { bool reval = false; if (thisValue != null && thisValue != DBNull.Value && bool.TryParse(thisValue.ToString(), out reval)) { return reval; } return reval; } } View Code 最终的仓储持久化是: public class AdvertisementRepository : IAdvertisementRepository { private DbContext context; private SqlSugarClient db; private SimpleClient<Advertisement> entityDB; internal SqlSugarClient Db { get { return db; } private set { db = value; } } public DbContext Context { get { return context; } set { context = value; } } public AdvertisementRepository() { DbContext.Init(BaseDBConfig.ConnectionString); context = DbContext.GetDbContext(); db = context.Db; entityDB = context.GetEntityDB<Advertisement>(db); } public int Add(Advertisement model) { //返回的i是long类型,这里你可以根据你的业务需要进行处理 var i = db.Insertable(model).ExecuteReturnBigIdentity(); return i.ObjToInt(); } public bool Delete(Advertisement model) { var i = db.Deleteable(model).ExecuteCommand(); return i > 0; } public List<Advertisement> Query(Expression<Func<Advertisement, bool>> whereExpression) { return entityDB.GetList(whereExpression); } public int Sum(int i, int j) { return i + j; } public bool Update(Advertisement model) { //这种方式会以主键为条件 var i = db.Updateable(model).ExecuteCommand(); return i > 0; } } 四、在 Blog.Core.IServices 层设计CURD接口,和仓储接口一样,在Blog.Core.Services去实现 这里不细说,记得添加引用,最终的代码是: namespace Blog.Core.IServices { public interface IAdvertisementServices { int Sum(int i, int j); int Add(Advertisement model); bool Delete(Advertisement model); bool Update(Advertisement model); List<Advertisement> Query(Expression<Func<Advertisement, bool>> whereExpression); } } namespace Blog.Core.Services { public class AdvertisementServices : IAdvertisementServices { public IAdvertisementRepository dal = new AdvertisementRepository(); public int Sum(int i, int j) { return dal.Sum(i, j); } public int Add(Advertisement model) { return dal.Add(model); } public bool Delete(Advertisement model) { return dal.Delete(model); } public List<Advertisement> Query(Expression<Func<Advertisement, bool>> whereExpression) { return dal.Query(whereExpression); } public bool Update(Advertisement model) { return dal.Update(model); } } } 都是很简单,如果昨天的Sum方法你会了,这个肯定都会。 五、Controller测试接口,数据库Sql生成语句在wwwroot文件中 实现工作,根据id获取数据 这里为了调试方便,我把权限验证暂时注释掉 //[Authorize(Policy ="Admin")] 然后修改我们的其中一个Get方法,根据id获取信息 // GET: api/Blog/5 /// <summary> /// /// </summary> /// <param name="id"></param> /// <returns></returns> [HttpGet("{id}", Name = "Get")] public List<Advertisement> Get(int id) { IAdvertisementServices advertisementServices = new AdvertisementServices(); return advertisementServices.Query(d => d.Id == id); } 接下来运行调试,在我们接口文档中,直接点击调试 得到的结果是如果,虽然是空的,但是返回结果http代码是200,因为表中没数据嘛 六、结语 好啦,今天的讲解就到这里,你简单的了解了什么是ORM,以及其中的SqlSugar,然后呢,仓储模式的具体使用,最后还有真正的连接数据库,获取到数据,下一节中,我们继续来解决两大问题,来实现泛型仓储。 七、CODE https://github.com/anjoy8/Blog.Core.git https://gitee.com/laozhangIsPhi/Blog.Core
代码已上传Github+Gitee,文末有地址 书接上文:前几回文章中,我们花了三天的时间简单了解了下接口文档Swagger框架,已经完全解放了我们的以前的Word说明文档,并且可以在线进行调试,而且当项目开始之中,我们可以定义一些空的接口,或者可以返回假数据,这样真正达到了前后端不等待的缺陷,还是很不错的,当然,这离我说的前后端分离还是相差甚远,今天呢,我们就简单搭建下我们的项目架构。 本项目是我自己的一个真实项目,数据都是真实的,之前搭建过一个MVC + EF Code First的项目,本项目就是基于这个了,前一段时间我已经搭建起来了,是这样的,本系列教程会重新开始。 零、本期完成的思维脑图中的粉色部分 一、创建.net Core 类库,Model数据层 其中,Models文件夹中,存放的是整个项目的数据库表实体类,这里是手动创建的,当然也可以自动创建,在以后的文章中我会提到,用到的是SqlSugar的T4创建,这里先买一个伏笔。 然后,VeiwModels文件夹,是存放的DTO实体类,在开发中,一般接口需要接受数据,返回数据,我之前都是这么红果果的使用的,后来发现弊端很大,不仅把重要信息暴露出去(比如手机号等),还对数据造成冗余(比如我需要接受用户的生日,还需要具体的年、月、日这就是三个字段,当然您也可以手动拆开,这只是一个栗子,所以不能直接用数据库实体类接受),就用到了DTO类的转换,但是频繁的转换又会麻烦,别慌,以后的文章中,我们会引用AutoMapper来自动转换,这里再买一个伏笔。 最后的是MessageModel和TableModel,大家也基本一看就能明白,因为在前端接口中,需要固定的格式,以及操作,不能把数据直接发出去,会报错,在以后的Vue开发中,会提到这个,这里又买了一个伏笔。 如下: /// <summary> /// 通用返回信息类 /// </summary> public class MessageModel<T> { /// <summary> /// 操作是否成功 /// </summary> public bool Success { get; set; } /// <summary> /// 返回信息 /// </summary> public string Msg { get; set; } /// <summary> /// 返回数据集合 /// </summary> public List<T> Data { get; set; } } 整个项目运行,没错,继续创建下一层。 二、创建Blog.Core.IRepository和 Blog.Core.Repository 仓储层 这里简单说下仓储层:repository就是一个管理数据持久层的,它负责数据的CRUD(Create, Read, Update, Delete) service layer是业务逻辑层,它常常需要访问repository层。有网友这么说:Repository(仓储):协调领域和数据映射层,利用类似与集合的接口来访问领域对象。Repository 是一个独立的层,介于领域层与数据映射层(数据访问层)之间。它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接口提供给领域层进行领域对象的访问。Repository 是仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。 我们定义了IRepository层,提供了所有的操作接口,今天搭建框架,我简单地写一个实例,明天我们将把所有的方法嵌套进去。 在 IAdvertisementRepository.cs 中,添加一个求和接口 public interface IAdvertisementRepository { int Sum(int i, int j); } 然后再在 AdvertisementRepository.cs 中去实现该接口,记得要添加引用,这个应该都会,就不细说了。 public class AdvertisementRepository : IAdvertisementRepository { public int Sum(int i, int j) { return i + j; } } 运行项目,一起正常,继续往下。 三、创建 Blog.Core.IServices 和 Blog.Core.Services 业务逻辑层,就是和我们平时使用的三层架构中的BLL层很相似 Service层只负责将Repository仓储层的数据进行调用,至于如何是与数据库交互的,它不去管,这样就可以达到一定程度上的解耦,加入以后数据库要换,比如MySql,那Service层就完全不需要修改即可,至于真正意义的解耦,还是得靠依赖注入,这下一节我们会讲到。 这里在 IAdvertisementServices 中添加接口 namespace Blog.Core.IServices { public interface IAdvertisementServices { int Sum(int i, int j); } } 然后再在 AdvertisementServices 中去实现该接口 public class AdvertisementServices : IAdvertisementServices { IAdvertisementRepository dal = new AdvertisementRepository(); public int Sum(int i, int j) { return dal.Sum(i, j); } } 注意!这里是引入了三个命名空间 using Blog.Core.IRepository;using Blog.Core.IServices; using Blog.Core.Repository; 四、创建 Controller 接口调用 将系统默认的ValueController删除,手动添加一个BlogController控制器,可以选择一个空的,也可以选择一个带有默认读写实例的。如下: [Produces("application/json")] [Route("api/Blog")] [Authorize(Policy ="Admin")] public class BlogController : Controller { // GET: api/Blog [HttpGet] public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } // GET: api/Blog/5 [HttpGet("{id}", Name = "Get")] public string Get(int id) { return "value"; } // POST: api/Blog [HttpPost] public void Post([FromBody]string value) { } // PUT: api/Blog/5 [HttpPut("{id}")] public void Put(int id, [FromBody]string value) { } // DELETE: api/ApiWithActions/5 [HttpDelete("{id}")] public void Delete(int id) { } } 接下来,在应用层添加服务层的引用 using Blog.Core.IServices;using Blog.Core.Services; 然后,改写Get方法 ·······// GET: api/Blog /// <summary> /// Sum接口 /// </summary> /// <param name="i">参数i</param> /// <param name="j">参数j</param> /// <returns></returns> [HttpGet] public int Get(int i,int j) { IAdvertisementServices advertisementServices = new AdvertisementServices(); return advertisementServices.Sum(i,j); } F5 运行项目,调试如下: 天呀!出错辣!别慌,还记得昨天咱们加的权限么,嗯!就是那里,手动模拟登陆,获取Token,注入,不会的可以看上一篇,然后再执行,结果: 五、结语 好啦,今天的工作暂时到这里了,你可以看到整体项目的搭建,结构,如何引用,如何测试等,当然,这里还是有很多小问题,比如: ·如果每个仓储都需要这么写,至少是四遍,会不会太麻烦; ·每次Controller接口调用,需要引入很多命名空间 ·等等等等 这些问题,下一节我们都会带大家一起去慢慢解决! 六、Code https://github.com/anjoy8/Blog.Core.git https://gitee.com/laozhangIsPhi/Blog.Core
群友反馈: 群里有小伙伴反馈,在Swagger使用的时候报错,无法看到列表,这里我说下如何调试和主要问题: 1、如果遇到问题,这样的: 请在浏览器 =》 F12 ==》 console 控制台 ==》点击错误信息地址 或者直接链接http://localhost:xxxxx/swagger/v1/swagger.json,就能看到错误了 2、主要问题就是同一个controller中的同一个请求特性(注意[HttpGet]和[HttpGet("{id}")]是两个) ,不能同名 主要修改下路由,然后配合修改名字就行 [Route("api/[controller]/[action]")] 4、或者直接在方法上增加路由 [HttpPost] [Route("newPost")] public void Post([FromBody]string value) { } WHY 书接上文,在前边的两篇文章中,我们简单提到了接口文档神器Swagger,《从零开始搭建自己的前后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之三 || Swagger的使用 3.1》、《从零开始搭建自己的前后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之四 || Swagger的使用 3.2》,两个文章中,也对常见的几个问题做了简单的讨论,最后还剩下一个小问题, 如何给接口实现权限验证? 其实关于这一块,我思考了下,因为毕竟我的项目中是使用的vue + api 搭建一个前台展示,大部分页面都没有涉及到权限验证,本来要忽略这一章节,可是犹豫再三,还是给大家简单分析了下,个人还是希望陪大家一直搭建一个较为强大的,只要是涉及到后端那一定就需要 登陆=》验证了,本文主要是参考网友https://www.cnblogs.com/RayWang/p/9255093.html的思路,我自己稍加改动,大家都可以看看。 根据维基百科定义,JWT(读作 [/dʒɒt/]),即JSON Web Tokens,是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。它是一种用于双方之间传递安全信息的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的、自包含的方法,从而使通信双方实现以JSON对象的形式安全的传递信息。 以上是JWT的官方解释,可以看出JWT并不是一种只能权限验证的工具,而是一种标准化的数据传输规范。所以,只要是在系统之间需要传输简短但却需要一定安全等级的数据时,都可以使用JWT规范来传输。规范是不因平台而受限制的,这也是JWT做为授权验证可以跨平台的原因。 如果理解还是有困难的话,我们可以拿JWT和JSON类比: JSON是一种轻量级的数据交换格式,是一种数据层次结构规范。它并不是只用来给接口传递数据的工具,只要有层级结构的数据都可以使用JSON来存储和表示。当然,JSON也是跨平台的,不管是Win还是Linux,.NET还是Java,都可以使用它作为数据传输形式。 1)客户端向授权服务系统发起请求,申请获取“令牌”。 2)授权服务根据用户身份,生成一张专属“令牌”,并将该“令牌”以JWT规范返回给客户端 3)客户端将获取到的“令牌”放到http请求的headers中后,向主服务系统发起请求。主服务系统收到请求后会从headers中获取“令牌”,并从“令牌”中解析出该用户的身份权限,然后做出相应的处理(同意或拒绝返回资源) 一、通过Jwt获取Token,并通过缓存记录,配合中间件实现验证 在之前的搭建中,swagger已经基本成型,其实其功能之多,不是我这三篇所能写完的,想要添加权限,先从服务开始 在ConfigureServices中,增加以下代码 #region Token绑定到ConfigureServices //添加header验证信息 var security = new Dictionary> { { "Blog.Core", new string[] { } }, }; c.AddSecurityRequirement(security); //方案名称“Blog.Core”可自定义,上下一致即可 c.AddSecurityDefinition("Blog.Core", new ApiKeyScheme { Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入{token}\"", Name = "Authorization",//jwt默认的参数名称 In = "header",//jwt默认存放Authorization信息的位置(请求头中) Type = "apiKey" }); #endregion View Code 最终的是这样的 /// <summary> /// ConfigureServices 方法 /// </summary> /// <param name="services"></param> public void ConfigureServices(IServiceCollection services) { services.AddMvc(); #region Swagger services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Version = "v0.1.0", Title = "Blog.Core API", Description = "框架说明文档", TermsOfService = "None", Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "Blog.Core", Email = "Blog.Core@xxx.com", Url = "https://www.jianshu.com/u/94102b59cc2a" } }); //就是这里 #region 读取xml信息 var basePath = PlatformServices.Default.Application.ApplicationBasePath; var xmlPath = Path.Combine(basePath, "Blog.Core.xml");//这个就是刚刚配置的xml文件名 var xmlModelPath = Path.Combine(basePath, "Blog.Core.Model.xml");//这个就是Model层的xml文件名 c.IncludeXmlComments(xmlPath, true);//默认的第二个参数是false,这个是controller的注释,记得修改 c.IncludeXmlComments(xmlModelPath); #endregion #region Token绑定到ConfigureServices //添加header验证信息 //c.OperationFilter<SwaggerHeader>(); var security = new Dictionary<string, IEnumerable<string>> { { "Blog.Core", new string[] { } }, }; c.AddSecurityRequirement(security); //方案名称“Blog.Core”可自定义,上下一致即可 c.AddSecurityDefinition("Blog.Core", new ApiKeyScheme { Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入{token}\"", Name = "Authorization",//jwt默认的参数名称 In = "header",//jwt默认存放Authorization信息的位置(请求头中) Type = "apiKey" }); #endregion }); #endregion #region Token服务注册 services.AddSingleton<IMemoryCache>(factory => { var cache = new MemoryCache(new MemoryCacheOptions()); return cache; }); services.AddAuthorization(options => { options.AddPolicy("Admin", policy => policy.RequireClaim("AdminType").Build());//注册权限管理,可以自定义多个 }); #endregion } View Code 然后执行代码,就可以看到效果 图 1 图 2 它的作用就是,每次请求时,从Header报文中,获取密钥token,这里根据token可以进一步判断相应的权限等。 接下来,就是在项目中添加五个文件,如下图 ,图 3 具体来说: 1:BlogCoreMemoryCache 这里是简单的一个缓存的使用,在以后的Redis中,也可以配合使用,这里是先借鉴大神的,以后我会扩展,然后并结合Redis,具体看Git上的代码,这里不做详细说明 public class RayPIMemoryCache { public static MemoryCache _cache = new MemoryCache(new MemoryCacheOptions()); /// <summary> /// 验证缓存项是否存在 /// </summary> /// <param name="key">缓存Key</param> /// <returns></returns> public static bool Exists(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } object cached; return _cache.TryGetValue(key, out cached); } /// <summary> /// 获取缓存 /// </summary> /// <param name="key">缓存Key</param> /// <returns></returns> public static object Get(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } return _cache.Get(key); } /// <summary> /// 添加缓存 /// </summary> /// <param name="key">缓存Key</param> /// <param name="value">缓存Value</param> /// <param name="expiresSliding">滑动过期时长(如果在过期时间内有操作,则以当前时间点延长过期时间)</param> /// <param name="expiressAbsoulte">绝对过期时长</param> /// <returns></returns> public static bool AddMemoryCache(string key, object value, TimeSpan expiresSliding, TimeSpan expiressAbsoulte) { if (key == null) { throw new ArgumentNullException(nameof(key)); } if (value == null) { throw new ArgumentNullException(nameof(value)); } _cache.Set(key, value, new MemoryCacheEntryOptions() .SetSlidingExpiration(expiresSliding) .SetAbsoluteExpiration(expiressAbsoulte) ); return Exists(key); } } View Code 2:BlogCoreToken,主要方法,获取JWT字符串并存入缓存中 public class BlogCoreToken { public BlogCoreToken() { } /// <summary> /// 获取JWT字符串并存入缓存 /// </summary> /// <param name="tm"></param> /// <param name="expireSliding"></param> /// <param name="expireAbsoulte"></param> /// <returns></returns> public static string IssueJWT(TokenModel tokenModel, TimeSpan expiresSliding, TimeSpan expiresAbsoulte) { DateTime UTC = DateTime.UtcNow; Claim[] claims = new Claim[] { new Claim(JwtRegisteredClaimNames.Sub,tokenModel.Sub),//Subject, new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),//JWT ID,JWT的唯一标识 new Claim(JwtRegisteredClaimNames.Iat, UTC.ToString(), ClaimValueTypes.Integer64),//Issued At,JWT颁发的时间,采用标准unix时间,用于验证过期 }; JwtSecurityToken jwt = new JwtSecurityToken( issuer: "Blog.Core",//jwt签发者,非必须,自定义 audience: tokenModel.Uname,//jwt的接收该方,非必须 claims: claims,//声明集合 expires: UTC.AddHours(12),//指定token的生命周期,unix时间戳格式,非必须 signingCredentials: new Microsoft.IdentityModel.Tokens .SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes("Blog.Core's Secret Key")), SecurityAlgorithms.HmacSha256)); var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); RayPIMemoryCache.AddMemoryCache(encodedJwt, tokenModel, expiresSliding, expiresAbsoulte);//将JWT字符串,令牌实体,存入缓存 return encodedJwt; } } View Code 3:TokenAuth,这是一个中间件,每次网络请求的时候,都走这里,作为报文获取判断,并防篡改 public class TokenAuth { /// <summary> /// /// </summary> private readonly RequestDelegate _next; /// <summary> /// /// </summary> /// <param name="next"></param> public TokenAuth(RequestDelegate next) { _next = next; } /// <summary> /// /// </summary> /// <param name="httpContext"></param> /// <returns></returns> public Task Invoke(HttpContext httpContext) { var headers = httpContext.Request.Headers; //检测是否包含'Authorization'请求头,如果不包含返回context进行下一个中间件,用于访问不需要认证的API if (!headers.ContainsKey("Authorization")) { return _next(httpContext); } var tokenStr = headers["Authorization"]; try { string jwtStr = tokenStr.ToString().Trim(); //如何存在Authorization,但是和缓存的不一样,那就是被篡改了 if (!RayPIMemoryCache.Exists(jwtStr)) { return httpContext.Response.WriteAsync("非法请求"); } TokenModel tm = ((TokenModel)RayPIMemoryCache.Get(jwtStr)); //提取tokenModel中的Sub属性进行authorize认证 List<Claim> lc = new List<Claim>(); Claim c = new Claim(tm.Sub+"Type", tm.Sub); lc.Add(c); ClaimsIdentity identity = new ClaimsIdentity(lc); ClaimsPrincipal principal = new ClaimsPrincipal(identity); httpContext.User = principal; return _next(httpContext); } catch (Exception) { return httpContext.Response.WriteAsync("token验证异常"); } } } View Code 4:上边的方法中都会用到一个TokenModel,自己简单写一个,也可以是你登陆的时候的用户实体类,或者其他, /// /// 令牌类 /// public class TokenModel { publicTokenModel() { this.Uid = 0; } /// /// 用户Id /// public long Uid { get; set; } /// /// 用户名 /// public string Uname { get; set; } /// /// 手机 /// public string Phone { get; set; } /// /// 头像 /// public string Icon { get; set; } /// /// 昵称 /// public stringUNickname { get; set; } /// /// 签名 /// public string Sub { get; set; } View Code 5:将四个文件都添加好后,最后两步 1、然后再Startup的Configure中,将TokenAuth注册中间件 图 6 2、在需要加权限的页面中,增加特性 这个时候,你运行项目,发现之前写的都报错了, 图 7 别慌!是因为每次操作请求,都会经过TokenAuth 中的Invoke方法,方法中对Header信息进行过滤,因为现在Header中,并没有相应的配置信息,看到这里,你就想到了,这个特别像我们常见的[HttpGet]等特性,没错!在.Net Core 中,到处都可以看到AOP编程,真的特别强大。 这个时候我们就用到了最开始的那个权限按钮 ,图 8 没错就是这里,但是我们方法写好了,那Token如何获取呢,别急,我们新建一个LoginController,来模拟一次登陆操作,简单传递几个参数,将用户角色和缓存时间传递,然后生成Token,并生成到缓存中,为之后做准备。 [HttpGet] [Route("Token")] public JsonResult GetJWTStr(long id=1, string sub="Admin", int expiresSliding = 30, int expiresAbsoulute = 30) { TokenModel tokenModel = new TokenModel(); tokenModel.Uid = id; tokenModel.Sub = sub; DateTime d1 = DateTime.Now; DateTime d2 = d1.AddMinutes(expiresSliding); DateTime d3 = d1.AddDays(expiresAbsoulute); TimeSpan sliding = d2 - d1; TimeSpan absoulute = d3 - d1; string jwtStr = BlogCoreToken.IssueJWT(tokenModel, sliding, absoulute); return Json(jwtStr); } View Code 这个时候我们就得到了我们的Token 图 9 然后粘贴到我们的上图权限窗口中,还记得么 图 10 接下来,你再调用窗口,就发现都可以辣! 二、通过认证授权的形式,实现验证,去掉缓存(此部分代码周一Git更新) 1、还是和上一种方法类似,做了封装,在项目文件夹AuthHelper下,新建一个OverWrite文件夹,然后新建JwtHelper类 这个类,主要是是生成Token,和解析Token namespace Blog.Core.AuthHelper { public class JwtHelper { public static string secretKey { get; set; } = "sdfsdfsrty45634kkhllghtdgdfss345t678fs"; /// <summary> /// 颁发JWT字符串 /// </summary> /// <param name="tokenModel"></param> /// <returns></returns> public static string IssueJWT(TokenModelJWT tokenModel) { var dateTime = DateTime.UtcNow; var claims = new Claim[] { new Claim(JwtRegisteredClaimNames.Jti,tokenModel.Uid.ToString()),//Id new Claim("Role", tokenModel.Role),//角色 new Claim(JwtRegisteredClaimNames.Iat,dateTime.ToString(),ClaimValueTypes.Integer64) }; //秘钥 var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtHelper.secretKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var jwt = new JwtSecurityToken( issuer: "Blog.Core", claims: claims, //声明集合 expires: dateTime.AddHours(2), signingCredentials: creds); var jwtHandler = new JwtSecurityTokenHandler(); var encodedJwt = jwtHandler.WriteToken(jwt); return encodedJwt; } /// <summary> /// 解析 /// </summary> /// <param name="jwtStr"></param> /// <returns></returns> public static TokenModelJWT SerializeJWT(string jwtStr) { var jwtHandler = new JwtSecurityTokenHandler(); JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr); object role = new object(); ; try { jwtToken.Payload.TryGetValue("Role", out role); } catch (Exception e) { Console.WriteLine(e); throw; } var tm = new TokenModelJWT { Uid = long.Parse(jwtToken.Id), Role = role.ToString(), }; return tm; } } /// <summary> /// 令牌 /// </summary> public class TokenModelJWT { /// <summary> /// Id /// </summary> public long Uid { get; set; } /// <summary> /// 角色 /// </summary> public string Role { get; set; } } } 2、还是在OverWrite文件夹中,新建中间件JwtTokenAuth.cs,主要作用和之前一样 public class JwtTokenAuth { /// <summary> /// /// </summary> private readonly RequestDelegate _next; /// <summary> /// /// </summary> /// <param name="next"></param> public JwtTokenAuth(RequestDelegate next) { _next = next; } /// <summary> /// /// </summary> /// <param name="httpContext"></param> /// <returns></returns> public Task Invoke(HttpContext httpContext) { //检测是否包含'Authorization'请求头 if (!httpContext.Request.Headers.ContainsKey("Authorization")) { return _next(httpContext); } var tokenHeader = httpContext.Request.Headers["Authorization"].ToString(); TokenModelJWT tm = JwtHelper.SerializeJWT(tokenHeader); //授权 var claimList = new List<Claim>(); var claim = new Claim(ClaimTypes.Role, tm.Role); claimList.Add(claim); var identity = new ClaimsIdentity(claimList); var principal = new ClaimsPrincipal(identity); httpContext.User = principal; return _next(httpContext); } } 3、在项目启动类的配置服务ConfigureService中,新增认证部分 #region 认证 services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = "Blog.Core", ValidAudience = "wr", IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(JwtHelper.secretKey)), RequireSignedTokens = true, // 将下面两个参数设置为false,可以不验证Issuer和Audience,但是不建议这样做。 ValidateAudience = false, ValidateIssuer = true, ValidateIssuerSigningKey = true, // 是否要求Token的Claims中必须包含 Expires RequireExpirationTime = true, // 是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比 ValidateLifetime = true }; }); #endregion 登陆,就是验证用户登陆以后,通过个人信息(用户名+密码),调取数据库数据,根据权限,生成一个令牌 认证,就是根据登陆的时候,生成的令牌,检查其是否合法,这个主要是证明没有被篡改 授权,就是根据令牌反向去解析出的用户身份,回应当前http请求的许可,表示可以使用当前接口,或者拒绝访问 4、修改services.AddAuthorization方法,通过增加角色的方式来配置 services.AddAuthorization(options => { options.AddPolicy("Client", policy => policy.RequireRole("Client").Build()); options.AddPolicy("Admin", policy => policy.RequireRole("Admin").Build()); options.AddPolicy("AdminOrClient", policy => policy.RequireRole("Admin,Client").Build()); }); 5、记得修改configure中的中间件 app.UseMiddleware<JwtTokenAuth>(); 如果没有权限会是这样的 WHAT 这一篇呢,写的比较潦草,主要是讲如何使用,具体的细节知识,还是大家摸索,还是那句话,这里只是抛砖引玉的作用哟,通过阅读本文,你会了解到,什么是JWT,如何添加配置.net core 中间件,如何使用Token验证,在以后的项目里你就可以在登陆的时候,调用Token,返回客户端,然后判断是否有相应的接口权限。 NEXT 好啦!项目准备阶段就这么结束了,以后咱们就可以直接用swagger来调试了,而不是没错都用F5运行等,接下来我们就要正式开始搭建项目了,主要采用的是泛型仓储模式 Repository+Service,也是一种常见的模式。 CODE https://github.com/anjoy8/Blog.Core.git
WHY 书接上文《从零开始搭建自己的前后端分离【 .NET Core2.0 Api + Vue 2.0 】框架之三 || Swagger的使用 3.1》,上文中只是简单的对如何使用Swagger作了介绍,而且最后也提出了几个问题,这里再重温下那几个问题 BEFORE 为何直接 F5 运行,首页还是无法加载? 接口虽有,但是却没有相应的文字说明? 项目开发中的实体类是如何在Swagger中展示的? 对于接口是如何加权限验证的? HOW 1、设置swagger ui页面为首页 在上一回中我们提到,我们直接F5运行项目,出现了系统默认页, 虽然可以在输入/swagger后,顺利的访问swagger ui页,但是我们发现每次运行项目,都会默认访问api/values这个接口,我想要将启动页设为swagger(或者是任意一个页面),你就需要用到了 **设置文件launchSettings.json **了: 然后你再一次F5 运行,就会发现不一样了,其他的配置,以及以后部署中的设置,我们会在以后的文章中都有提到。 2、接下来,我们就需要解决第二个问题,如何增加文字说明,就是传说中的注释 右键项目名称=>属性=>生成,勾选“输出”下面的“xml文档文件”,系统会默认生成一个,当然老规矩,你也可以自己起一个名字 .net core 2.1 新不同 生成的默认路径不一样了,没有了netcoreapp2.0文件夹 这个时候,先别忙着运行项目,作为老司机的我,只要是改代码或者配置文件,保存后,第一件事就是看看有没有错误,一看,咦~~~果然,虽然是警告,可以强迫症呀,一看还挺多 别慌!一看,哦!原来是swagger把一些接口方法都通过xml文件配置了,就是刚刚上文提到的,所以我们只需要加上方法注释就可以辣,可以左斜杠/,连续三下即可 现在呢,配置好了xml文件,接下来需要让系统启动的时候,去读取这个文件了,重新编辑Startup.cs,修改ConfigureServices函数: public void ConfigureServices(IServiceCollection services) { services.AddMvc(); #region Swagger services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Version = "v0.1.0", Title = "Blog.Core API", Description = "框架说明文档", TermsOfService = "None", Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "Blog.Core", Email = "Blog.Core@xxx.com", Url = "https://www.jianshu.com/u/94102b59cc2a" } }); //就是这里 var basePath = PlatformServices.Default.Application.ApplicationBasePath; var xmlPath = Path.Combine(basePath, "Blog.Core.xml");//这个就是刚刚配置的xml文件名 c.IncludeXmlComments(xmlPath,true);//默认的第二个参数是false,这个是controller的注释,记得修改 }); #endregion } View Code .Net Core 2.1 新不同 感谢网友反馈@风格不同,@dannyjyc 获取项目路径的方式和2.0版本不一样 var basePath = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath; var xmlPath = Path.Combine(basePath, "Blog.Core.xml");//这个就是刚刚配置的xml文件名 c.IncludeXmlComments(xmlPath, true);//默认的第二个参数是false,这个是controller的注释,记得修改 然后F5 运行,都加上了,感觉前端大佬再也不会说看不懂接口了,哈哈哈哈 接下来开始第三个问题:添加实体类说明注释 新建一个.net core 类库Blog.Core.Model,注意是 .net core的类库 新建一个Love的实体类 /// <summary> /// 这是爱 /// </summary> public class Love { /// <summary> /// id /// </summary> public int Id { get; set; } /// <summary> /// 姓名 /// </summary> public string Name { get; set; } /// <summary> /// 年龄 /// </summary> public int Age { get; set; } } 然后就是刚添加的类库中,在Model层项目,属性,中添加xml路径,添加注释(这里的步骤和API项目的添加方法一致,不会的请留言),然后在API项目中添加引用 image 改写注入方法,并在控制器中参数引用 public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); #region Swagger services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Version = "v0.1.0", Title = "Blog.Core API", Description = "框架说明文档", TermsOfService = "None", Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "Blog.Core", Email = "Blog.Core@xxx.com", Url = "https://www.jianshu.com/u/94102b59cc2a" } }); //就是这里 var basePath = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath; var xmlPath = Path.Combine(basePath, "Blog.Core.xml");//这个就是刚刚配置的xml文件名 c.IncludeXmlComments(xmlPath, true);//默认的第二个参数是false,这个是controller的注释,记得修改 var xmlModelPath = Path.Combine(basePath, "Blog.Core.Model.xml");//这个就是Model层的xml文件名 c.IncludeXmlComments(xmlPath, true);//默认的第二个参数是false,这个是controller的注释,记得修改 c.IncludeXmlComments(xmlModelPath); }); #endregion } /// <summary> /// post /// </summary> /// <param name="love">model实体类参数</param> [HttpPost] public void Post(Love love) { } .net core 2.1 新不同 注意,不能再HttpGet中,用实体类做参数,会报错 dang dang dang,就出来了 .net core 2.1 新不同 NEXT 对于接口是如何加权限验证的? 让我们带着这些问题,继续浏览下一篇吧,Swagger 3.3 权限 CODE https://github.com/anjoy8/Blog.Core.git https://gitee.com/laozhangIsPhi/Blog.Core
WHY 上文中已经说到,单纯的项目接口在前后端开发人员使用是特别不舒服的,那所有要推荐一个,既方便又美观的接口文档说明框架,当当当,就是Swagger,随着互联网技术的发展,现在的网站架构基本都由原来的后端渲染,变成了:前端渲染、后端分离的形态,而且前端技术和后端技术在各自的道路上越走越远。 前端和后端的唯一联系,变成了API接口;API文档变成了前后端开发人员联系的纽带,变得越来越重要,swagger就是一款让你更好的书写API文档的框架。 没有API文档工具之前,大家都是手写API文档的,在什么地方书写的都有,有在confluence上写的,有在对应的项目目录下readme.md上写的,每个公司都有每个公司的玩法,无所谓好坏。 书写API文档的工具有很多,但是能称之为“框架”的,估计也只有swagger了。 HOW 下面开始引入swagger插件 方法有两个: 1)可以去swagger官网或github上下载源码,然后将源码(一个类库)引入自己的项目; 2)直接利用NuGet包添加程序集应用(这里就是前边说的 在以后的开发中,Nuget无处不在)。 右键项目中的 Dependencies -- > Manage Nuget Packags --> Browse --> Search "Swashbuckle.AspNetCore" --> Install 然后就在项目的Nuget依赖里看到刚刚引入的Swagger 图 2 这个时候,你可以试一下,当然是不可以的,还记得上文说的,.Net Core 都需要一个程序入口么,对就是Startup.cs文件 1、打开Startup.cs类,编辑ConfigureServices类 public void ConfigureServices(IServiceCollection services) { services.AddMvc(); #region Swagger services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Version = "v0.1.0", Title = "Blog.Core API", Description = "框架说明文档", TermsOfService = "None", Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "Blog.Core", Email = "Blog.Core@xxx.com", Url = "https://www.jianshu.com/u/94102b59cc2a" } }); }); #endregion } 2、编辑Configure类 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } #region Swagger app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "ApiHelp V1"); }); #endregion app.UseMvc(); } 3、到这,已经完成swagger的添加,F5 运行调试,在域名后面输入/swagger,http://localhost:54067/swagger/index.html 点击回车,当当当 出来啦 图 3 图 4 既美观又快捷,这样以后发布出去,前后端开发人员就可以一起开发了,嗯!不错! WHAT 好啦,本节基本就是这里了,你简单浏览后,会了解到,什么是Swagger,它如何创建使用,如何运行的,但是,细心的你会发现一些问题 NEXT 如何直接F5运行,首页无法加载? 接口虽有,但是却没有文字文档说明? 对于接口是如何加权限验证的? 如何发布到服务器,大家一起接口开发呢? 项目开发中的实体类是如何在Swagger中展示的? 让我们带着这些问题,继续浏览下一篇吧,Swagger 3.2 配置 ღ 网友反馈ღ @BlueDr提出:可以将Swagger的UI页面配置在Configure的开发环境之中 public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); #region Swagger app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "ApiHelp V1"); }); #endregion } app.UseMvc(); } CODE https://github.com/anjoy8/Blog.Core.git https://gitee.com/laozhangIsPhi/Blog.Core
WHY 至于为什么要搭建.Net Core 平台,这个网上的解释以及铺天盖地,想了想,还是感觉重要的一点,跨平台,嗯!没错,而且比.Net 更容易搭建,速度也更快,所有的包均有Nuget提供,不再像以前的单纯引入组件,比如是这样的: 已经没有了之前的Assemblies和COM的引入,初次使用感觉会很别扭,不过使用多了,发现还是很方便的,所以你一定要会使用Nuget,真的很强大,这点儿设计思路感觉更像Linux了。 HOW 说了从零开始,就得从零开始,老生常谈,开始。 当然,前提是你得安装.Net Core,VS 2015也是可以,只不过需要单独安装.Net Core,首先你得装个vs2015 并且保证已经升级至 update3及以上。 我的VS是2017,我这里只说2017,有不会的网友可以留言,只要在Visual Studio Installer 中安装下图中的Core 平台即可。 1、File --> Project (记得文件名不要是中文,不然,你懂的) 2、然后选择.Net Core 版本和项目类型,我选择相对稳定的ASP.NET Core 2.0,然后选择API的项目类型 至于其他的,大家可以自己玩一玩,还有就是是否Docker支持,这两年Docker着实很火,我也会在以后的时间里,补上这块儿的使用。。。 Duang ,然后就出现了,特别简单的一个.Net Core API就这么诞生了,嗯不错,基本的分成这几个部分,是不是特别像一个控制台程序?而且真是简洁了不少 点开Controllers --> ValuesController 文件,你会发现四个方法,并且每个方法也有各自的特性,分别是HttpGet,HttpPost,HttpPut,HttpDelete,这四个就是传说中的RESTful风格的编程。 为什么会有这种风格呢: RESTful 风格接口实际情况是,我们在前后端在约定接口的时候,可以约定各种风格的接口,但是,RESTful 接口是目前来说比较流行的,并且在运用中比较方便和常见的接口。 虽然它有一些缺陷,目前 github 也在主推 GraphQL 这种新的接口风格,但目前国内来说还是 RESTful 接口风格比较普遍。并且,在掌握了 RESTful 接口风格之后,会深入的理解这种接口的优缺点,到时候,你自然会去想解决方案,并且在项目中实行新的更好的理念,所以,我这系列的博文,依然采用 http://cnodejs.org/ 网站提供的 RESTful 接口来实战。 了解程序开发的都应该知道,我们所做的大多数操作都是对数据库的四格操作 “增删改查” 对应到我们的接口操作分别是:post 插入新数据delete 删除数据put 修改数据get 查询数据 注意,这里是我们约定,并非这些动作只能干这件事情。从表层来说,除get外的其他方法,没有什么区别,都是一样的。从深层来说包括 get在内的所有方法都是一模一样的,没有任何区别。但是,我们约定,每种动作对应不同的操作,这样方便我们统一规范我们的所有操作。 假设,我们的接口是 /api/v1/love 这样的接口,采用 RESTful 接口风格对应操作是如下的:get 操作 /api/v1/love获取 /api/v1/love 的分页列表数据,得到的主体,将是一个数组,我们可以用数据来遍历循环列表post 操作 /api/v1/love我们会往 /api/v1/love 插入一条新的数据,我们插入的数据,将是JOSN利用对象传输的。get 操作 /api/v1/love/1我们获取到一个 ID 为 1 的的数据,数据一般为一个对象,里面包含了 1 的各项字段信息。put 操作 /api/v1/love/1我们向接口提交了一个新的信息,来修改 ID 为 1 的这条信息delete 操作 /api/v1/love/1我们向接口请求,删除 ID 为 1 的这一条数据 由上述例子可知,我们实现了5种操作,但只用了两个接口地址, /api/v1/love 和 /api/v1/love/1 。所以,采用这种接口风格,可以大幅的简化我们的接口设计。 .Net Core 2.1 新不同 然后 F5 运行,就会看到接口地址,以及对应的内容,你可以根据自己的需要进行各种配置合适的路由, 这里要注意下,如果出现特性相同,方法同名,参数一样的,编译会报错,起名字很重要。 还有,这里会自动跳转到默认地址 api/values,当然是可以配置的,就在 Properties --> launchSettings.json 中 接下来点开 appsettings.json 文件,这里就是整个系统app的配置地址,更类似以前的web.config,以后大家会用到 继续往下,打开Startup.cs 文件这里是整个项目的启动文件,所有的启动相关的都会在这里配置,比如 依赖注入,跨域请求,Redis缓存等,更多详情在以后的文章中都会有所提起 , WHAT 好啦,项目搭建就这么愉快的解决了,而且你也应该简单了解了.Net Core API是如何安装,创建,各个文件的意义以及如何运作,如何配置等,但是既然是接口,那一定是要前后端一起进行配置,使用,交流的平台,从上文看出,每次都特别麻烦,而且不直观,UI 不友好,怎么办呢? NEXT 下一节我们就使用一个神器 Swagger,一个快速,轻量级的项目RESTFUL接口的文档在线自动生成+功能测试功能软件。 CODE https://github.com/anjoy8/Blog.Core.git NOTE 如何不会使用Git,可以参考 https://www.jianshu.com/p/2b666a08a3b5
缘起 作为一个.Net攻城狮已经4年有余了,一直不温不火,正好近来项目不是很忙,闲得无聊,搞一搞新技术,一方面是打发无聊的时间,一方面也是督促自己该学习辣!身边的大神都转行的转行,加薪的加薪,本人比较懒,只想搞技术 [哭笑] ,也是怀着小小的梦想,做一个系列文章可以和大家一起进步,讨论,希望总阅读数能上1万,嗯,哈哈哈哈 技术 本系列文章只是对现有的一些技术做一个简单说明或者是引入,只是一个抛砖引玉的作用,主要的还是希望和志同道合的大神们一起切磋武艺。 系统环境 windows 10、SQL server 2012、Visual Studio 2017、Windows Server 2008 R2 后端技术: * .Net Core 2.0 API(因为想单纯搭建前后端分离,因此就选用的API,如果想了解.Net Core MVC,也可以交流) * Async和Await 异步编程 * Repository + Service 仓储模式编程 * Swagger 前后端文档说明,基于RESTful风格编写接口 * Cors 简单的跨域解决方案 数据库技术 * SqlSugar 轻量级ORM框架 * Autofac 轻量级IoC和DI依赖注入 * AutoMapper 自动对象映射 分布式缓存技术 * Redis 轻量级分布式缓存 前端技术 * Vue 2.0 框架全家桶 Vue2 + VueRouter2 + Webpack + Axios * ElementUI 基于Vue 2.0的组件库 结语 这里再一次说明,仅仅是简单的特别简单的入门使用,如果对于上边的技术,你从来没有听过,或者听过没用过,嗯,你可以简单花点儿时间看一看,但是如果你都已经用过或者有一定的技术,请帮忙监督指正。 致谢 感谢有两位朋友提供思路,或者说动力,才使我萌发了想写的冲动,特别感谢李大爷,嗯就是哈哈,的默默支持,才使我有了继续写下去的动力。