2023年2月

在我们实际业务开发中,从头开发一个应用系统,不管是基于BS的前端项目,还是基于Winform的CS应用系统,都是由容易到复杂,逐步演化的一个开发过程,如果我们基于一定基础上,并配合一些配套的开发工具,那么这个应用系统的开发就会变得容易很多,毕竟现在每一个应用系统都是要求要五脏俱全的系统,不仅要长得漂亮,还要有内涵。对于一个基于CS模式的Winform应用系统,开发起来相对比较容易一些,依赖的东西的边界比较清晰一些,因此大多数可以使用独立、模块化的方式来开发系统。

1、Winform的应用系统的模块化场景

我们开发一个系统的时候,往往需要对各种资源进行各种处理,如硬件API接口(摄像头、读卡器、采集器等)、数据库访问(SQLServer、Oracle、Mysql、SQLite、PostgreSQL、MongoDB)、文件读写支持(XML、JSON、文本文件、图片等)、缓存处理(MemoCache、Redis等),以及第三方的一些接口支持(如微信支付、支付宝、抖音等),当然还有自己业务内部的接口支持,涉及的范围很大也很广,不过我们一般来说,一个系统的主要业务还是对数据库的存取进行处理,因此诞生了很多基于数据库业务的基础模块,如基于通用字典读取和设置的字典模块、权限管理模块等,这些应用模块,基本上是各个业务系统都需用到的,不同的系统,他们的规则和显示方式可能有所差异。

如下是一个基础Winform框架支持的一些模块图示。

如通用字典管理着整个系统的各种下拉列表或者参考信息。

权限管理模块则定义好整个组织机构、用户、角色、权限分配、菜单资源定义、字段权限、数据权限、黑白名单等配置信息。

权限模块中,是相对复杂一点的模块,其中还需要管理各级组织机构的信息。

权限管理系统模块,可以单独调用登陆后进行展示,也可以通过界面整合的方式,无缝的整合到应用系统中去,作为一个整体应用对待使用。

而工作流模块则管理着日常流程表单的处理。

业务模块则是基于我们某个业务范畴开发的一个独立模块,可能应用到了很多通用的模块,这个也是模块化的精髓。

如下是整合以上一些基础模块的CRM客户关系管理系统的内容,也可以简单的把CRM当成一个复杂一点的业务应用模块。

模块化类似小孩子搭积木的方式构建一个所要的形状,虽然软件和建筑在这方面肯定更加复杂化,但是模块化系统是大势所趋,也是简化开发、易于维护、提高系统健壮性的重要举措。

随着应用模块的不断开发,有一个好的组织架构,纵向统一管理模块中各个局部分层,会更加方便,因此引入了一个框架的设计模式,以便更好统一各个分层的管理。

Winform开发架构如下所示。

一般框架都支持常见的关系型数据库,并提供他们对应的支持切换。

另外,一方面,我们可以收集WInform开发中常常涉及到的界面控件使用,类库使用,常规组件使用等一系列的代码片段、难点疑点进行整合管理,整理一个综合的案例提供各种各样的开发测试案例代码,用来辅助开发学习了解。

以及一些杂项的内容收集。

力求在各个方面能够快速辅助系统的开发。

2、Winform的应用系统的开发

在我们实际不断的迭代开发中,往往会不断的累积出各个完善的应用模块出来,不过这个过程开发,可以基于我们的配套开发工具进行快速的开发,由于各个模块是基于相同的架构分层,以及对整套系统的一个固定的开发模式,我们可以采用代码生成工具来根据数据库信息,生成对应分层的项目代码,并把它们之间的关系处理好,生成一个完整的解决方案,生成即可编译通过的完善项目模块。

对于Winform界面来说,有时候,我们需要精细化的定义各种界面部分的内容,因此工具提供配置进行定义,然后再生成。

如在代码生成工具里面,设置主从表的界面生成如下所示。

有了系统框架的通用基础模块支持,以及基于固定架构的代码生成工具辅助开发,事半功倍,而且开发的系统模块更加统一和稳定,并且内置的支持多种数据库的接入, 基本上能够覆盖大多数应用系统的需要。

基于Vue的前端框架有很多,这几年随着前端技术的官方应用,总有是学不完的前端知识在等着我们,一个人的精力也是有限,不可能一一掌握,不过我们学习很大程度都会靠兴趣驱动,或者目标导向,最终是可以以点破面,逐步掌握各种前端知识的。本篇随笔主要以实际应用场景为例介绍一些Vue前端技术的拓展,供大家参考学习。

基于Vue的前端知识,不管在移动端,还是桌面端管理后台,都有着很多可供选择的前端框架或者组件。

移动端开发各种小程序应用和App都综合一起了,一站化处理,可以同时开发微信小程序、支付宝小程序、抖音以及百度等小程序,如UniApp、UView等都是以Vue为开发语言的,并提供了便于开发和部署的IDE工具,虽然很多也可以基于业界使用的VSCode来开发,不过定制化的工具,效率也会显得更优一些。而随着移动端小程序开发的活跃,京东凹凸实验室也近两年推出了Taro的前端框架,和UniApp等一样,也是可以同时开发微信小程序、支付宝小程序、抖音以及百度等小程序,对标各种小程序的开发的。

桌面端开发,基于Vue的桌面后台管理框架也有很多,如Element、AntDesign-Vue等,随着Vue3的推广及覆盖,会有越来越多的前端框架的推出。另外的还有
Electronjs
以及基于Vue的Electron框架,可以系统平台无关的基于浏览器的应用,相当于自带浏览器的应用程序了。

基于React前端语言的开发也是一个方向,如最早的AntDesign就是React的,后来才推出Vue版本,京东的Taro早期也是React版本,现在同时推出带有Vue语言版本的框架。本篇随笔不详细讨论各种前端的技术,主要就是介绍基于Vue开发的门户网站展示和后台数据管理系统。

1、门户网站展示和后台数据管理的需求和采用组件框架

我们不管是个人团队,还是企业,一般总有开发推广网站的需求,虽然可以制作静态的网站宣传,不过动态的内容管理更新会更加方便,也同时突显更好的交互体验,而伴随着动态的内容发布,内容管理同时也需要一个后台进行处理。

因此结合两者的特点,我们前端门户发布,采用
Bootstrap-vue
组件(中文站点:
https://code.z01.com/bootstrap-vue/
),BootstrapVue 是基于 Bootstrap v4 + Vue.js 的前端 UI 框架。它是流行的
Bootstrap 框架
与 Vue.js 的集成。使用 BootstrapVue,任何人都可以从 Vanilla.js 或 jQuery 切换到 Vue.js,而无需担心 Bootstrap 对 jQuery 的严重依赖,甚至无法找到解决方法。这就是 BootstrapVue 的救援方式。它有助于弥补这一差距,并允许 Vue 开发人员能够轻松地在他们的项目中使用 Bootstrap。BootstrapVue不依赖Jquery。

GitHub库的地址:
https://github.com/topics/bootstrapvue

BootstrapVue的官网地址(可能受限无法访问):
https://bootstrap-vue.org/

BootstrapVue的中文网站地址如下:
https://code.z01.com/bootstrap-vue/

通过在Vue项目中引入对应的 BootstrapVue,那么它的相关组件使用就参考官网的介绍了解即可。BootstrapVue中有很多和Bootstrap一样的组件,不过标签前缀需要加上b-

例如对于常用的按钮界面代码处理,如下所示。

<div>
  <b-button>Button</b-button>
  <b-buttonvariant="danger">Button</b-button>
  <b-buttonvariant="success">Button</b-button>
  <b-buttonvariant="outline-primary">Button</b-button>
</div>

Bootstrap的CSS已经广泛应用了很多年了,有着很成熟的技术体系,因此Bootstrap的页面资源也非常的多。我们在网上搜一下Bootstrap,就可以找到很多相关Bootstrap的模板提供网站,从中我们可以下载或者借鉴很多设计良好的界面,从而利用来开发属于自己的公司门户网站内容。

后端管理,我们采用比较广泛的Vue+Element的后台框架,使用
Element
强大的组件能力,是我们更加方便的管理门户网站后台的数据,目前Element的版本已经推出基于Vue3.x的
Element-Plus
版本了。Element提供了非常丰富的数据管理组件,能够为我们的后台管理提供非常高效、便捷的解决方案。

门户网站前端一般负责合理展示内容,而后端就需要整合数据模型,尽可能的把内容管理得更加方便一些,我们把几个模块的内容一起综合管理,根据不同的类别进行区分即可,因为大多数情况内容模型的格式是相似的。

我们创建了几个管理菜单入口,用于维护管理门户网站的内容信息,如下导航所示。

利用Vue和Element的友好协调性,我们可以快速基于这些门户网站的数据表进行业务模块的开发管理。

公司门户动态网站预览地址:
http://www.iqidi.com:8000

2、前端门户网站的模块设计及界面

一般的公司门户网站,都可能包含一些相关的产品、解决方案、客户或合作伙伴、图片展示、公司介绍、公司新闻等等。首页里面可能放置一些简略的综合内容,如下所示

门户网站一般菜单放在水平的放在顶部,如下所示,这样比较容易阅读。

在BootstrapVue组件库里面,提供了很多对Bootstrap同等类似的组件封装,其中图片轮播可以采用b-carousel-slide组件实现,而有一些小的图片,如客户/合作伙伴Logo或者友情连接什么的,则可以使用一个滑动图片展示的vue-awesome-swiper组件。

这样我们运行的时候 ,效果如下所示。

不同的轮播,我们指定不同的背景图片,以及文字,就很好的达到展示产品信息的目的。

有一些小的图片,如客户/合作伙伴Logo或者友情连接什么的,我们可能希望以图片的方式展示一个图片墙即可,如果内容比较多,那么占用版面比较大,而采用 vue-awesome-swiper 则可以避免这个问题,它是在展示图片的基础上,定时动态移动图片浏览,可以在更小的空间上放置更多的图片。

我们来看看具体的展示效果,其中单击图片,可以打开合作伙伴的网站地址。

这个面板在可视范围内,展示固定的几张图片,然后动态滑动,可以放置很多,又能拖动查看,效果还是非常不错的。

除了这两种,有时候我们的产品可能需要展示更多的图片,就是图片画廊的意思,那么我们就可以综合展示图片了,如下效果所示。

其中包括产品、框架等产品的介绍,维护内容差不多,只是展示方式有所差异。

框架产品的介绍,和软件产品的类似,不过这里采用了大图展示的方式,栅格的大小也有所变化。

以上这些网站门户的内容,由于采用了BootstrapVue,因此也是自适应的窗口布局的,我们可以切换到手机屏幕模式,那么可以看到正常、合理的展示,而不会仅仅是给电脑桌面端的展示。

我们可以在实现代码的时候,合理设置栅格的大小,从而让内容按照自己的需要展示。

对于门户网站,我们大概可以了解到门户网站的一些常用模块,如网站菜单、网站轮播广告、网站内容介绍(如公司信息、产品信息、合作伙伴、公司客户等)、网站图片展览、公司新闻等等模块,以及网站的一些常规设置信息,如Logo、网站名称、网站底部信息(包括公司地址、备案信息等等)。

这些模块有些可以综合一起管理,因为内容大多数格式差不多,如网站内容介绍(如公司信息、产品信息、合作伙伴、公司客户等),有些这需要独立设计管理,如网站菜单、网站轮播广告
等,
另外一些内容管理如新闻和模块内容类似,我们为了方便也将它独立进行维护即可。

综合以上一些内容,我们可以设计一些表来承载这些信息,然后在后端提供对应对口,供前端使用即可。根据需要我们设计了后端存储的数据表,如下所示

按照后端ABP框架或者Web API所需的框架类,使用代码生成工具生成代码后,集成在项目中,常规的增删改查、分页管理等基础接口就可以直接使用了。

后端API接口通过Swagger查看对应公开接口,如下所示。

由于门户网站不需要用户登录,或者授权信息,那么可以公开部分接口作为匿名访问,如访问菜单集合的信息作为匿名接口访问,如下所示。

再次定义一个API类,这个是直接访问后端接口获取数据的,如下所示

这样我们如果需要使用实际接口获取数据的话,导入对应的API类即可,如下所示。

import portal2 from '@/api/system/portal'

网站关于公司客户的动态数据展示效果如下所示。

3、后台管理模块的设计和界面

后台管理模块,主体框架界面采用的是基于后台配置的菜单动态生成,左侧是菜单,右边顶部是特定导航条和内容区

网站信息,主要就是维护LOGO、网站名称、网站底部信息的一些内容的。

网站菜单管理,综合管理门户网站中的菜单连接信息。

网站新闻,是维护新闻内容列表的。

这个是属于前端、后端完全分离的架构设计,后端采用基于Asp.net的Web API技术,并提供按域来管理API的分类,Web API如下架构所示。

而Vue&Element的前端的架构设计,也借鉴了我们ABP框架的前端管理部分,Vue&Element的前端的架构设计如下所示。

引入了前后端分离的Vue + Element 作为前端技术路线,那么前后端的边界则非常清晰,前端可以在通过网络获取对应的JSON就可以构建前端的应用了。 一般来说,我们页面模块可能会涉及到Store模块,用来存储对应的状态信息,也可能是直接访问API模块,实现数据的调用并展示。在页面开发过程中,多数情况下,不需要Store模块进行交互,一般只需要存储对应页面数据为全局数据状态的情况下,才可能启用Store模块的处理。通过WebProxy代理的处理,我们可以很容易在前端中实现跨域的处理,不同的路径调用不同的域名地址API都可以,最终转换为本地的API调用,这就是跨域的处理操作。

公司门户动态网站预览地址:
http://www.iqidi.com:8000

pinia是一个vue的状态存储库,你可以使用它来存储、共享一些跨组件或者页面的数据,使用起来和vuex非常类似。pina相对Vuex来说,更好的ts支持和代码自动补全功能。本篇随笔介绍pinia的基础用法以及持久化存储的一些用法,供参考学习。

pinia在2019年11月开始时候是一个实验项目,目的就是重新设计一个与组合API匹配的vue状态存储。基本原则和原来还是一样的,pinia同时支持vue2和vue3,且不要求你必须使用Vue3的组合API。不管是使用vue2或者vue3,pinia的API是相同的,文档是基于vue3写的。

Pinia 是 Vuex4 的升级版,也就是 Vuex5; Pinia 极大的简化了Vuex的使用,是 Vue3的新的状态管理工具;Pinia 对 ts的支持更好,性能更优, 体积更小,无 mutations,可用于 Vue2 和 Vue3;Pinia支持Vue Devtools、 模块热更新和服务端渲染。

1、pinia的安装和使用

安装pinia(
https://pinia.vuejs.org/
)

npm install pinia

在main.j或者main.ts中引入使用

import { createPinia } from 'pinia'app.use(createPinia())

下面就是使用pinia的一个例子。这样你就创建了一个状态存储。

//stores/counter.js
import { defineStore } from 'pinia'export const useCounterStore= defineStore('counter', {
state: ()
=>{return { count: 0}
},
//也可以这样定义状态 //state: () => ({ count: 0 }) actions: {
increment() {
this.count++},
},
})

在组件中使用:

import { useCounterStore } from '@/stores/counter'exportdefault{
setup() {
const counter
=useCounterStore()

counter.count
++ //编辑器会有代码提示 counter.$patch({ count: counter.count + 1})//也可以使用action来代替 counter.increment()
},
}
如果你不是很喜欢setup函数和组合API,pinia也有类似vuex的map的功能。你可以用上面的方式定义你的store,但是使用时用mapStores(), mapState(),或者 mapActions():
const useCounterStore = defineStore('counter', {
state: ()
=> ({ count: 0}),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++}
}
})

const useUserStore
= defineStore('user', {//... })

export
default{
computed: {
//其他计算属性 //... //可以使用 this.counterStore 和 this.userStore获取 ...mapStores(useCounterStore, useUserStore)//可以使用 this.count 和this.double获取 ...mapState(useCounterStore, ['count', 'double']),
},
methods: {
//可以使用 this.increment()调用 ...mapActions(useCounterStore, ['increment']),
},
}

与vue4之前的版本相比,pinia的API是有很多不同的,即:

  • 去掉了mutation。因为好多人认为mutation是多余的。以前它方便devtools集成,现在这不是个问题了。
  • 不用在写复杂的ts类型包装,所有的都是有类型的,API设计的都是尽量符合ts的类型推断
  • 不再使用一个莫名其妙的字符串了,只需要导入一个函数,调用他们就行了,同时还有代码自动补全
  • 不需要动态添加store了,因为它们现在本来就是动态。如果你想,你随时可以手动去写一个store。
  • 没有复杂的嵌套模块了。你仍然可以在一个store中导入其他的store来实现嵌套模块,但是pinia还是推荐使用一个扁平的结构。但是即使你使用循环依赖也没关系。
  • 不再需要命名空间了。因为现在store本来就是扁平结构了。你也可以理解为所有的store本来就有命名空间了。
你的应用中的全局数据需要保存在store中。在很多地方你都要使用这些数据,比如说,用户信息需要在导航栏中显示,也需要在个人中心显示。还有些数据,需要暂存起来,比如一个需要分好几页填写的表单。
在pinia中,store是通过defineStore()方法定义的,它的第一个参数就是一个唯一的名字:
import { defineStore } from 'pinia'export const useStore= defineStore('main', {//other options...
})

上面只是定义了store,在setup函数中调用了useStore()时,才会创建store:

import { useStore } from '@/stores/counter'exportdefault{
setup() {
const store
=useStore()return{//你可以返回store这个对象,然后就可以在template中使用了 store,
}
},
}

在store实例化以后,你就可以调用到store中定义的state、getters和actions了。为了让解构的值还保持响应式,你需要用到storeToRefs()方法。它会给响应式的数据创建ref。

import { storeToRefs } from 'pinia'exportdefaultdefineComponent({
setup() {
const store
=useStore()//`name` 和 `doubleCount` 是响应式的 //插件增加的属性也会创建ref //但是会自动跳过action或者不是响应性的属性 const { name, doubleCount } =storeToRefs(store)return{
name,
doubleCount
}
},
})

默认情况下,你可以在store实例上直接获取或者修改state:

const store =useStore()
store.counter
++

也可以调用$reset()方法来把state恢复为初始值:

const store =useStore()
store.$reset()

除了直接修改store里的值store.counter++,你也可以是用$patch方法。你可以同时修改多个值:

store.$patch({
counter: store.counter
+ 1,
name:
'Abalam',
})

或者$patch接收一个函数作为参数,来简化改变数组的写法:

store.$patch((state) =>{
state.items.push({ name:
'shoes', quantity: 1})
state.hasChanged
= true})

2、pinia的持久化存储处理

你可以用$subscribe()来侦听state的改变,持久化一般存储在
localStorage和sessionStorage。

localStorage和sessionStorage差别

localStorage和sessionStorage一样都是用来存储客户端临时信息的对象。

他们均只能存储字符串类型的对象(虽然规范中可以存储其他原生类型的对象,但是目前为止没有浏览器对其进行实现)。

localStorage生命周期是永久,这意味着除非用户显示在浏览器提供的UI上清除localStorage信息,否则这些信息将永远存在。

sessionStorage生命周期为当前窗口或标签页,一旦窗口或标签页被永久关闭了,那么所有通过sessionStorage存储的数据也就被清空了。

不同浏览器无法共享localStorage或sessionStorage中的信息。相同浏览器的不同页面间可以共享相同的 localStorage(页面属于相同域名和端口),但是不同页面或标签页间无法共享sessionStorage的信息。这里需要注意的是,页面及标 签页仅指顶级窗口,如果一个标签页包含多个iframe标签且他们属于同源页面,那么他们之间是可以共享sessionStorage的。

JSON对象提供的parse和stringify将其他数据类型转化成字符串,再存储到storage中就可以了,操作的方式:

存:

var obj = {"name":"xiaoming","age":"16"}

localStorage.setItem("userInfo",JSON.stringify(obj));

取:

var user = JSON.parse(localStorage.getItem("userInfo"))

删除:

localStorage.remove("userInfo);

清空:

localStorage.clear();

pnia 使用订阅机制subscribe来实现数据的持久化存储的代码如下所示。

const instance =useMainStore();//订阅数据变化,变化时存储 instance.$id 这是storeId
instance.$subscribe((mutation, state) =>{
localStorage.setItem(instance.$id, JSON.stringify(state));
});
//init 初始的时候获取 const val =localStorage.getItem(instance.$id);if(val) {
instance.$state
=JSON.parse(val);
}

也可以通过watch实现

watch(
pinia.state,
(state)
=>{//persist the whole state to the local storage whenever it changes localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep:
true}
)

但是需要注意,这种方式持久化会提示pinia未安装挂载,所以需要在pinia挂载后再调用,这里可以将它封装成方法导出,在挂载后调用

xport const initStore = () =>{
const instance
=useMainStore();//订阅数据变化,变化时存储 instance.$id 这是storeId instance.$subscribe((mutation, state) =>{
localStorage.setItem(instance.$id, JSON.stringify(state));
});
//init 初始的时候获取 const val =localStorage.getItem(instance.$id);if(val) {
instance.$state
=JSON.parse(val);
}

}
默认情况下,state侦听会和组件绑定在一起(如果store是在组件的setup中)。这意味着,当组件卸载时,侦听会自动被移除。如果你需要在组件被卸载时,侦听仍然保持,需要给$subscribe()方法传递第二个参数true:
export default{
setup() {
const someStore
=useSomeStore()//组件卸载后,侦听也会有 someStore.$subscribe(callback, true)//... },
}

或者watch状态的变化

watch(
pinia.state,
(state)
=>{//在state改变时,保存在localStorage中 localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep:
true}
)

3、使用pinia插件持久化存储

pinia plugin persist官方网站:
pinia-plugin-persist

持久化存储也可以通过安装插件的方式,安装 pinia-plugin-persist 来实现。

npm i pinia-plugin-persist --save

使用main.js

import { createPinia } from 'pinia'import piniaPluginPersist from'pinia-plugin-persist'const store=createPinia()
store.use(piniaPluginPersist)
createApp(App).use(store).mount(
'#app')

在对应的store中开启,数据默认存在 sessionStorage 里,并且会以 storeId 作为 key

import { defineStore } from 'pinia'
//'main' 是storeId
export const useMainStore = defineStore('main', {
state: ()
=>({
counter:
2,
name:
'Eduardo',
isAdmin:
true}),//…… //开启数据缓存 persist: {
enabled:
true}
})

如果需要自定义key和存储位置,则修改参数即可。

persist: {
enabled:
true,
strategies: [
//使用插件自定义存储 {
key:
'settings', //key可以自己定义,不填的话默认就是这个store的ID storage: localStorage,
}
]
},

4、在实际项目中使用pinia

一般项目开发,实际上存储的内容会比较多,可能根据不同的键值模块进行区分,因此把它们放在一个store/modules里面,方便的使用引用它来存取设置数据即可。

我们这里简单以一个settings的配置信息进行介绍,其中index.ts是一个统一的创建pinia的对象并挂接到全局App上的。

其中index.ts的代码如下所示。

import type { App } from "vue";
import { createPinia } from
"pinia";
import piniaPluginPersist from
'pinia-plugin-persist';//使用插件持久化 const store=createPinia();
store.use(piniaPluginPersist)
//使用插件持久化 exportfunction setupStore(app: App<Element>) {
app.use(store);
}

export { store };

因此在main.js里面引入并挂接pinia即可。

import { createApp } from 'vue'import ElementPlus from'element-plus'import'element-plus/dist/index.css'import'normalize.css' //css初始化
import App from'./App.vue'import { setupStore } from "/@/store";

const app
=createApp(App)
setupStore(app)
app.use(ElementPlus)
app.mount(
'#app')

这样我们就可以再次定义一个模块化的配置信息,以便于管理存储各种不同类型的内容。

如下面我们定义一个程序配置信息setttings.ts

import { defineStore } from "pinia";
import { store } from
"/@/store";

export type settingType
={
title: string;
fixedHeader:
boolean;
hiddenSideBar:
boolean;
};

export const useSettingStore
=defineStore({
id:
"settings",
state: (): settingType
=>({
title:
"Vue3 + TypeScript + Element",
fixedHeader:
false,
hiddenSideBar:
false}),
persist: {
enabled:
true,
strategies: [
//使用插件自定义存储 {
key:
'settings', //key可以自己定义,不填的话默认就是这个store的ID storage: localStorage,
}
]
},

getters: {
getTitle() {
return this.title;
},
getFixedHeader() {
return this.fixedHeader;
},
getHiddenSideBar() {
return this.HiddenSideBar;
}
},
actions: {
CHANGE_SETTING({ key, value }) {
//eslint-disable-next-line no-prototype-builtins if (this.hasOwnProperty(key)) {this[key] =value;
}
},
changeSetting(data) {
this.CHANGE_SETTING(data);
}
}
});

export
functionuseSettingStoreHook() {returnuseSettingStore(store);
}

然后在组件视图vue或者app.vue中使用即可

<scriptlang="ts">import { defineComponent } from"vue";

import { useSettingStoreHook } from
"/@/store/modules/settings";
import { storeToRefs } from
"pinia";

export
defaultdefineComponent({
name:
"app",
components: {
},
setup() {
const store
=useSettingStoreHook();

const { fixedHeader, title }
=storeToRefs(store);return{
fixedHeader,
title,
};
},
methods: {
setTitle() {
this.title= "Vue3 + TypeScript + Element + Edit";
console.log(
this.title);
},
},
});
</script>

查看数据修改后,存储在本地存储空间中的内容,如下所示。

由于我们有时候需要在基于.net framework的项目上使用(如Winform端应用),有时候有需要在.net core的项目上使用(如.net core的WebAPI),那么我们把基于SQLSugar的基础模块封装,编译为.net standard就很有必要,而且由于.net framework和.net core在配置文件上的差异,我们需要对基础配置信息进行不同环境的兼容处理,以便实现基础模块支持.net FrameWork和.net core的项目调用。

1、基于.netStandard的类库模块

在上篇随笔《
基于SqlSugar的数据库访问处理的封装,支持多数据库并使之适应于实际业务开发中
(1)》中介绍了对SqlSugar 基础模块的封装处理,为了兼容不同类型的框架,我们可以把它们封装为.net Standard类库。

但是 为了基础模块能够顺利加载不同.net框架下的配置文件信息来初始化数据库连接,我们需要配置不同的加载处理方式来读取处理。

如.net Framework的使用App.config中读取配置信息,而.net core项目中使用读取 appSettings.json里面的配置信息。

因此需要让它们兼容,我们需要修改项目文件中的TargetFrameworks,让它根据不同的框架生成不同的DLL类库,从而达到支持不同环境下配置文件的读取处理。

这样我们查看项目属性,就可以看到类库是支持多种目标框架的了。

接下来我们对DbContext辅助类进行改动,让它根据不同的目标框架条件来读取配置信息。

统一入口就是调用ConfigHelper辅助类来隔离处理操作。

        publicDbContext()
{
this.DbSetting =ConfigHelper.GetDbSettings();
Init(
this.DbSetting.DbConfigName);
}
public DbContext(stringdbConfigName)
{
if (this.DbSetting == null)
{
this.DbSetting =ConfigHelper.GetDbSettings();
}
Init(dbConfigName);
}

以替代原先的处理代码。原先的只能从.net framework的App.config中读取,现在我们需要根据框架来判断处理。

        publicDbContext()
{
//默认采用配置项名//appSettings/DefaultDb 配置项为指定连接字符串的name var dbConfigName = ConfigurationManager.AppSettings["DefaultDb"];
Init(dbConfigName);
}

我们先来定义一个承载数据库信息的实体类对象。

    /// <summary>
    ///数据库配置信息/// </summary>
    public classDbSetting
{
/// <summary> ///默认指定的连接字符串集合的配置项名称/// </summary> public string DbConfigName { get; set; }/// <summary> ///数据库类型,默认为SQLServer/// </summary> public string DbType { get; set; } = "sqlserver";/// <summary> ///连接字符串/// </summary> public string ConnectionString { get; set; }

}

在目标框架为.net framwork的时候,我们的App.Config配置信息是下面的。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <!--Sqlserver数据库的连接字符串-->
    <addname="sqlserver"providerName="System.Data.SqlClient"connectionString="Persist Security Info=False;Data Source=(local);Initial Catalog=WinFramework;Integrated Security=SSPI" />
    <!--MySQL数据库的连接字符串-->
    <addname="mysql"providerName="MySql.Data.MySqlClient"connectionString="Server=localhost;Database=winframework;Uid=root;Pwd=123456;SslMode=none" />
    <!--sqlite数据库字符串,路径符号|DataDirectory|代表当前运行目录-->
    <addname="sqlite"providerName="System.Data.SQLite"connectionString="Data Source=|DataDirectory|\WinFramework.db;Version=3;" />
    <!--PostgreSQL数据库的连接字符串-->
    <addname="npgsql"providerName="Npgsql"connectionString="Server=localhost;Port=5432;Database=winframework;User Id=postgres;Password=123456" />
    <!--不受驱动影响,32位64位均可使用-->
    <addname="oracle"providerName="OracleManaged"connectionString="Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orcl)));User ID=win;Password=win" />
    <!--达梦数据库的连接字符串-->
    <addname="Dm"providerName="Dm"connectionString="Server=localhost;User ID=SYSDBA;PWD=SYSDBA;Database=WINFRAMEWORK;" />
  </connectionStrings>
  
  <appSettings>
    <!--指定默认的数据库类型,如果不指定则使用第一个连接字符串-->
    <addkey="DefaultDb"value="sqlserver" />
    <!--字典、权限组件的数据库类型:mysql、npgsql、oracle、sqlite、sqlserver等,默认为sqlserver可不写-->
    <addkey="ComponentDbType"value="sqlserver" />
  </appSettings>
  <startup>
    <supportedRuntimeversion="v4.0"sku=".NETFramework,Version=v4.8" />
  </startup>

而如果是基于.net core的情况下,读取的是appSettings.json里面的配置信息,配置文件信息如下所示。

{"ConnectionStrings": {"Default": "Server=.; Database=WeixinBootstrap2; Trusted_Connection=True;","Oracle": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orcl)));User ID=C##ABP;Password=abp","MySql": "Server=localhost;Database=myprojectdb;Uid=root;Pwd=123456;","PostgreSQL": "Server=localhost;Port=5432;Database=myprojectdb;User Id=postgres;Password=123456"},"DbSetting": {"DefaultDb": "Default","ComponentDbType": "sqlserver"},

...........

}

其中 DefaultDb 指向的是默认的连接字符串配置节点名称,而ComponentDbType为它的数据库类型,如sqlserver,mysql这样的标识。

2、根据不同目标框架读取配置信息

有了上面不同目标框架下的配置信息节点的格式,我们就可以根据目标框架的不同来相应读取配置信息,从而实现不同的初始化处理操作。

        /// <summary>
        /// 根据承载环境是netframework或者是.netcore,读取配置信息。
        /// 在.netframework中的app.config的appSettings中配置DefaultDb,ComponentDbType,连接字符串在connectionStrings中配置
        /// 在.netcore中的appSettings.json的DbSetting节点中配置DefaultDb,ComponentDbType,连接字符串在ConnectionStrings中配置
        /// </summary>
        /// <returns></returns>
public static DbSetting GetDbSettings()
{
//初始化承载配置信息对象 var dbSetting = newDbSetting();

#
ifNETFRAMEWORK//基于.net frameowork下读取app.config的配置 dbSetting.DbType = ConfigurationManager.AppSettings["ComponentDbType"] ?? "sqlserver";
dbSetting.DbConfigName
= ConfigurationManager.AppSettings["DefaultDb"];var setting = ConfigurationManager.ConnectionStrings[1];//默认第一个连接字符串 if (!string.IsNullOrWhiteSpace(dbSetting.DbConfigName))
{
//如果配置节点名称存在,则读取它的连接字符串 setting =ConfigurationManager.ConnectionStrings[dbSetting.DbConfigName];
}
if (setting != null)
{
dbSetting.ConnectionString
=setting.ConnectionString;
}
#
else //基于.net core下的读取appsettings.json的配置信息 dbSetting.DbType = GetSectionValue("DbSetting:ComponentDbType") ?? "sqlserver";
dbSetting.DbConfigName
= GetSectionValue("DbSetting:DefaultDb");var connectionStringKey = string.Format("ConnectionStrings:{0}", dbSetting.DbConfigName);
dbSetting.ConnectionString
=GetSectionValue(connectionStringKey);
#endif
returndbSetting;
}

我们可以从类的顶部来选择对应的分类,从而实现代码的加亮显示,便于代码的编写。

另外,我们根据配置信息的数据库类型,通过遍历判断的方式来转换为SqlSugar对应的数据库类型即可。

这样DbContext初始化的时候,就能够顺利适用于不同的目标框架中了,我们在SQLSugar封装的基类就可以不管它的具体处理,只需要初始化DbContext即可,如下代码所示。

基类调用来处理常规的对象返回操作,代码如下所示。

        /// <summary>
        /// 获取所有记录
        /// </summary>
        public virtual async Task<ListResultDto<TEntity>>GetAllAsync()
{
var list =await EntityDb.GetListAsync();return new ListResultDto<TEntity>()
{
Items
=list
};
}

而如果我们需要联合多表来实现联合查询,也可以使用基类的对象进行处理。

如对于字典来说,根据字典大类名称来获取字典项目信息,而字典项目表里面,只有字典大类的ID,那么就需要联合字典大类和字典项目两个表进行关联查询了,如下代码所示。

        /// <summary>
        ///根据字典类型名称获取所有该类型的字典列表集合(Key为名称,Value为值)/// </summary>
        /// <param name="dictTypeName">字典类型名称</param>
        /// <returns></returns>
        public async Task<Dictionary<string, string>> GetDictByDictType(stringdictTypeName)
{
var query = this.dbContent.Client.Queryable<DictDataInfo, DictTypeInfo>(
(d, t)
=> d.DictType_ID == t.Id && t.Name ==dictTypeName)
.Select(d
=> d); //联合条件获取对象 query= query.OrderBy(d => d.DictType_ID).OrderBy(d => d.Seq);//排序 var list = await query.ToListAsync();//获取列表 var dict = new Dictionary<string, string>();foreach (var info inlist)
{
if (!dict.ContainsKey(info.Name))
{
dict.Add(info.Name, info.Value);
}
}
returndict;
}

这样就可以实现联合表的查询处理。

至此,我们就可以无差别的在不同的目标框架上,根据不同的配置文件来初始化我们的DbContext类,从而无差别的使用基于SqlSugar的数据库访问处理的基类,简化了框架的处理。

相关随笔列表如下所示。

基于SqlSugar的数据库访问处理的封装,支持.net FrameWork和.net core的项目调用
(本篇随笔)

我前面几篇随笔介绍了关于几篇关于SqlSugar的基础封装,已经可以直接应用在Winform项目开发上,并且基础接口也通过了单元测试,同时测试通过了一些Winform功能页面;本篇随笔继续深化应用开发,着手在在.net6框架的Web API上开发应用,也就是基于.net core的Web API应用开发,这样可以应用在不同的前端接入上。本篇随笔主要介绍基于.net6框架的Web API的相关整合开发内容,内容涉及到Swagger的整合支持、SeriLog的支持、JWT鉴权和用户身份信息缓存、基类控制器封装、自动注入接口对象、统一结果封装、统一异常处理等方面。

1、创建.netcore WebApi项目并添加相关支持

本篇随笔主要从基础框架开发创建,因此使用VS2022添加一个基于.net core6的WebAPI项目,如下所示。

我们在生成的项目中,看到有一个Program.cs的代码文件,里面代码比较简洁,我们逐步调整并添加自己的相关代码即可。

在其中可以看到

builder.Services.AddSwaggerGen();

这个是简单的Swagger注释支持,我们如果需要定义更多的信息,可以采用下面的代码。

#region 添加swagger注释

//builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerGen(c=>{
c.SwaggerDoc(
"v1", newOpenApiInfo
{
Version
= "v1",
Title
= "Api"});
c.IncludeXmlComments(Path.Combine(basePath,
"SugarWebApi.xml"), true);//WebAPI项目XML文件 c.IncludeXmlComments(Path.Combine(basePath, "SugarProjectCore.xml"), true);//其他项目所需的XML文件 c.AddSecurityDefinition("Bearer", newOpenApiSecurityScheme
{
Description
= "Value: Bearer {token}",
Name
= "Authorization",
In
=ParameterLocation.Header,
Type
=SecuritySchemeType.ApiKey,
Scheme
= "Bearer"});
c.AddSecurityRequirement(
newOpenApiSecurityRequirement()
{
{
newOpenApiSecurityScheme
{
Reference
= newOpenApiReference
{
Type
=ReferenceType.SecurityScheme,
Id
= "Bearer"},Scheme= "oauth2",Name = "Bearer",In =ParameterLocation.Header,
},
new List<string>()
}
});
});
#endregion

上面的代码除了添加对应控制器的接口信息外,还增加了一个相关服务类的接口定义,便于我们查看详细的xml信息,如下所示得到很详细的接口注释。

然后调整Swagger UI支持的代码如下所示。

//Configure the HTTP request pipeline.
if(app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

另外,我们的Web API控制器,需要集成JWT Bear 认证的处理的,添加认证代码如下所示。

//JWT Bear 认证
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>{
options.TokenValidationParameters
= newTokenValidationParameters
{
//非固定可选可加 ValidateIssuer = true,
ValidIssuer
= builder.Configuration["Jwt:Issuer"],
ValidateAudience
= true,
ValidAudience
= builder.Configuration["Jwt:Audience"],

ValidateLifetime
= true,//时间 ClockSkew =TimeSpan.Zero,
IssuerSigningKey
= new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"]))
};
});

这里面的代码读取配置信息的,我们可以在appSettings.json中配置JWT的一些键值。

  "Jwt": {"Secret": "your-256-bit-secret","Issuer": "iqidi.com","Audience": "api"}

另外,为了在.net core中输出日志,可以使用SeriLog组件进行处理。

添加相关的nuget类库,如下所示。

然后在Program.cs中添加初始化日志代码即可。

//初始化日志
Log.Logger = newLoggerConfiguration()
.MinimumLevel.Debug()
//最小记录级别//对其他日志进行重写,除此之外,目前框架只有微软自带的日志组件 .MinimumLevel.Override(source: "Microsoft", minimumLevel: Serilog.Events.LogEventLevel.Error)

.Enrich.FromLogContext()
//记录相关上下文信息 .WriteTo.Console() //输出到控制台 .WriteTo.File("logs/log.txt", rollingInterval: RollingInterval.Day) //输出到本地文件 .CreateLogger();

2、统一结果封装和异常处理

在Web API的控制器返回信息中,我们为了方便使用JSON信息,往往需要对返回结果进行封装,让它们返回指定格式的数据,如下所示。

正常结果200:

未授权结果401:

关于接口数据格式的统一封装,我们定义一个WrapResultFilter,以及需要一个不封装的属性标识DontWrapResultAttribute,默认是统一封装返回的结果。

    /// <summary>
    ///禁用封装结果/// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]public classDontWrapResultAttribute : Attribute
{
}

而统一封装的处理,需要继承ActionFilterAttribute并重写OnResultExecuting处理操作。

里面的主要逻辑就是对结果内容进行统一的再次封装,如下所示主要的逻辑代码。

if (context.Result isObjectResult objRst)
{
if (objRst.Value isAjaxResponse)return;

context.Result
= new ObjectResult(newAjaxResponse
{
Success
= true,
Error
= null,
TargetUrl
= string.Empty,
UnAuthorizedRequest
= false,
Result
=objRst.Value
});
}

除了常规的正常返回内容进行封装,也需要对异常进行拦截,并对结果进行封装,因此需要继承ExceptionFilterAttribute并添加一个异常处理的过滤器进行处理,并重写OnException的操作即可。

    /// <summary>
    ///自定义异常处理/// </summary>
    public classGlobalExceptionFilter : ExceptionFilterAttribute
{
/// <summary> ///异常处理封装/// </summary> /// <param name="context"></param> public override voidOnException(ExceptionContext context)
{
if (!context.ExceptionHandled)
{
//异常返回结果包装 var content = newAjaxResponse()
{
Success
= false,
Error
= new ErrorInfo(0, context.Exception.Message, context.Exception.StackTrace),
TargetUrl
= string.Empty,
UnAuthorizedRequest
= false,
Result
= null};//日志记录 Log.Error(context.Exception, context.Exception.Message);

context.ExceptionHandled
= true;
context.Result
= newApplicationErrorResult(content);
context.HttpContext.Response.StatusCode
= (int)HttpStatusCode.InternalServerError;
}
}

因此为了拦截相关的处理,我们在Program.cs中添加以下代码进行拦截。

//控制器添加自定义过滤器
builder.Services.AddControllers(options=>{
options.Filters.Add
<WrapResultFilter>(); //统一结果封装处理 options.Filters.Add<GlobalExceptionFilter>();//自定义异常处理 });//所有控制器启动身份验证 builder.Services.AddMvc(options =>{
options.Filters.Add(
new AuthorizeFilter());//所有MVC服务默认添加授权标签 });

并调整代码,添加认证和授权验证的代码处理。

app.UseAuthentication();

app.UseAuthorization();

对于一些系统异常的处理(如401未授权、400未找到接口、500系统错误)等,默认是没有进行处理的

我们如果要拦截,就另外需要添加一个中间件的方式来处理信息流,如下所示。

其中在Invoke的函数处理中,统一处理不同的异常即可。

public asyncTask Invoke(HttpContext context)
{
try{awaitnext(context);
}
catch(Exception ex)
{
var statusCode =context.Response.StatusCode;if (ex isArgumentException)
{
statusCode
= 200;
}
awaitHandleExceptionAsync(context, statusCode, ex.Message);
}
finally{var statusCode =context.Response.StatusCode;var msg = "";if (statusCode == 401)
{
msg
= "未授权" + context.Response.Headers["WWW-Authenticate"];

}
else if (statusCode == 404)
{
msg
= "未找到服务";
}
else if(statusCode == 500)
{
msg
= "系统错误";
}
else if (statusCode == 502)
{
msg
= "请求错误";
}
else if (statusCode != 200)
{
msg
= "未知错误";
}
if (!string.IsNullOrWhiteSpace(msg))
{
awaitHandleExceptionAsync(context, statusCode, msg);
}
}
}

并添加一个扩展类方法,用于快速使用中间件方式调用。

    /// <summary>
    ///自定义错误处理的扩展方法/// </summary>
    public static classErrorHandlingExtensions
{
public static IApplicationBuilder UseErrorHandling(thisIApplicationBuilder builder)
{
return builder.UseMiddleware<ErrorHandlingMiddleware>();
}
}

最后在program.cs代码中添加使用代码即可,注意添加位置。

另外,为了把用户身份信息缓存起来,我们可以使用Redis进行缓存处理,因此在项目中使用CRedis的封装类库进行操作Redis

通过连接字符串(读取配置信息)初始化Redis的代码如下所示。

//初始化Redis
RedisHelper.Initialization(new CSRedisClient(builder.Configuration["CSRedis:ConnectString"]));

其中appSettings.json信息如下所示。

{
"ConnectionStrings": {
"Default": "Server=.; Database=WeixinBootstrap2; Trusted_Connection=True;",
"Oracle": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orcl)));User ID=C##ABP;Password=abp",
"MySql": "Server=localhost;Database=myprojectdb;Uid=root;Pwd=123456;",
"PostgreSQL": "Server=localhost;Port=5432;Database=myprojectdb;User Id=postgres;Password=123456"
},
"DbSetting": {
"DefaultDb": "Default",
"ComponentDbType": "sqlserver"
},
"CSRedis": {
"ConnectString": "127.0.0.1:6379"
},
"Jwt": {
"Secret": "your-256-bit-secret",
"Issuer": "iqidi.com",
"Audience": "api"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
}

如果允许登录授权请求成功的话,那么对应的用户省份缓存也就记录在Redis中了。

3、接口对象的依赖注入处理

我们在.net core的Web API中调用相关处理,我们往往是使用接口来调用相关的处理的。

启动Web API的时候通过手工或者自动注入接口对象的方式,然后在控制器里面的构造函数中通过依赖注入方式使用接口即可。

如果是手工注入,那么你确定在Web API项目中所有用到的业务访问接口,都需要提取注入。

    services.AddSingleton<IDictDataService, DictDataService>();
services.AddSingleton
<IDictTypeService, DictTypeService>();
services.AddSingleton
<ICustomerService, CustomerService>();
services.AddScoped
<IUserService, UserService>();

但是这样接口实现一旦很多,手工加入肯定繁琐而且低效了,因此需要考虑自动注入所有相关的服务接口为佳。

为了实现这个自动注入的目标,首先我们先定义几个不同生命周期的接口声明。

    //用于定义这三种生命周期的标识接口

    public interfaceIDependency
{
}
/// <summary> ///瞬时(每次都重新实例)/// </summary> public interfaceITransientDependency : IDependency
{
}
/// <summary> ///单例(全局唯一)/// </summary> public interfaceISingletonDependency : IDependency
{

}
/// <summary> ///一个请求内唯一(线程内唯一)/// </summary> public interfaceIScopedDependency : IDependency
{
}

然后通过遍历相关的DLL,然后实现所有实现指定接口的类对象,统一加入即可,如下代码所示。

            var baseType = typeof(IDependency);var path = AppDomain.CurrentDomain.RelativeSearchPath ??AppDomain.CurrentDomain.BaseDirectory;var getFiles = Directory.GetFiles(path, "*.dll").Where(Match);  //.Where(o=>o.Match())
            var referencedAssemblies = getFiles.Select(Assembly.LoadFrom).ToList();  //.Select(o=> Assembly.LoadFrom(o))

            var ss = referencedAssemblies.SelectMany(o => o.GetTypes());

然后进一步进行对接口的判断,如下所示。

    var types =referencedAssemblies
.SelectMany(a
=>a.DefinedTypes)
.Select(type
=>type.AsType())
.Where(x
=> x != baseType &&baseType.IsAssignableFrom(x)).ToList();var implementTypes = types.Where(x =>x.IsClass).ToList();var interfaceTypes = types.Where(x =>x.IsInterface).ToList();foreach (var implementType inimplementTypes)
{
if (typeof(IScopedDependency).IsAssignableFrom(implementType))
{
var interfaceType = interfaceTypes.FirstOrDefault(x =>x.IsAssignableFrom(implementType));if (interfaceType != null)
services.AddScoped(interfaceType, implementType);
}
else if (typeof(ISingletonDependency).IsAssignableFrom(implementType))
{
var interfaceType = interfaceTypes.FirstOrDefault(x =>x.IsAssignableFrom(implementType));if (interfaceType != null)
services.AddSingleton(interfaceType, implementType);
}
else{var interfaceType = interfaceTypes.FirstOrDefault(x =>x.IsAssignableFrom(implementType));if (interfaceType != null)
services.AddTransient(interfaceType, implementType);
}
}

然后统一调用即可。

//配置依赖注入访问数据库
ServiceInjection.ConfigureRepository(builder.Services);

这样我们在对应的WebAPI 控制器中就可以方便的使用接口的构造函数注入方式了。

    /// <summary>
    ///客户信息的控制器对象/// </summary>
    public class CustomerController : BusinessController<CustomerInfo, string, CustomerPagedDto>{privateICustomerService _customerService;/// <summary>
        ///构造函数,并注入基础接口对象/// </summary>
        /// <param name="customerService"></param>
        public CustomerController(ICustomerService customerService) :base(customerService)
{
this._customerService =customerService;
}
}

以上就是我们在创建.net Core项目的Web API项目中碰到的一些常见问题的总结,希望对大家有所帮助。

相关系类文章如下所示。

基于SqlSugar的数据库访问处理的封装,在.net6框架的Web API上开发应用
(本随笔)