2023年2月

在混合开发框架模式中,有时候我们在处理树形节点的时候,需要很多关联的处理,可能需要结合用户配置信息,属性字典,以及表的字段分类等信息来展示一个结构树,那么在处理的时候就可能会频繁的对这些接口API进行调用,而如果我们使用Web API一次性的获取树形节点信息,然后统一加载的话,性能会提升很多,本篇随笔介绍通过封装一个总的树形结构列表数据返回的Web API,从而在Winform客户端一次性展示的方式,实现性能的优化处理。

1、树形结构展示效果

如下面的CRM客户关系管理系统中,我们需要展示很多客户相关的树形节点,以方便快捷查询相关类型的客户信息。

那么这个树列表就需要结合很多属性来处理了,包括了客户的字段信息,客户配置显示信息,每个字段类型的对应的字典信息,如客户状态、客户类型等等。

因此如果在客户端整合逻辑,那么需要对几个不同的处理接口进行调用并处理,这种解析起来比较慢,而且也会导致处理效率问题。

一般情况下,我们云端的服务器性能会比客户端的性能更好一些,这些对数据库处理的逻辑封装在Web API的后盾会更加方便,也就是瘦客户端的方式更有效率了。

2、Web API端封装处理逻辑

例如我们定义一个以下的接口来获取数据。

        /// <summary>
        ///获取客户树形类别的数据/// </summary>
        /// <param name="userId">当前用户ID</param>
        /// <param name="companyId">所属公司ID</param>
        /// <param name="dataFilter">数据过滤条件</param>
        /// <param name="shareUserCondition">分配用户ID条件</param>
        /// <returns></returns>
        List<TreeNodeInfo> GetCustomerTree(string userId, string companyId, string dataFilter, string shareUserCondition);

其中TreeNodeInfo对象是我们自己定义的一个对象,用来承载具有层级信息的列表信息。

具体这个类的代码如下所示。

    /// <summary>
    ///用来承载TreeNode的信息/// </summary>[Serializable]
[DataContract]
public classTreeNodeInfo
{
/// <summary> ///子对象集合/// </summary> [DataMember]public List<TreeNodeInfo> Nodes { get; set; }/// <summary> ///节点名称/// </summary> [DataMember]public string Text { get; set; }/// <summary> ///节点标签/// </summary> [DataMember]public string Tag { get; set; }/// <summary> ///图标序号/// </summary> [DataMember]public int IconIndex { get; set; }/// <summary> ///是否展开/// </summary> [DataMember]public bool IsExpanded { get; set; }/// <summary> ///前景色/// </summary> [DataMember]public string ForeColor { get; set; }/// <summary> ///默认构造函数/// </summary> publicTreeNodeInfo() {this.Nodes = new List<TreeNodeInfo>();
}
/// <summary> ///参数构造函数/// </summary> /// <param name="text">节点名称</param> /// <param name="iconIndex">图标序号</param> /// <param name="tag">节点标签</param> public TreeNodeInfo(string text, int iconIndex, string tag = "") : this()
{
this.Text =text;this.IconIndex =iconIndex;this.Tag =tag;
}
}

Web API端的控制器方法如下所示。

最后具体在客户端界面绑定显示数据的逻辑如下所示。

        /// <summary>
        ///使用Json对象创建列表树/// </summary>
        private voidInitTree()
{
//清空节点信息 this.treeView1.Nodes.Clear();//通过Web API方式获取树对象列表结构 var list = CallerFactory<ICustomerService>.Instance.GetCustomerTree(LoginUserInfo.ID, this.SelectedCompanyID,this.DataFilterCondition, this.ShareUserCondition);if (list != null && list.Count > 0)
{
//遍历每个节点,生成对应的TreeView对象节点 foreach (var node inlist)
{
//构建TreeView对象节点信息 TreeNode parentNode = newTreeNode(node.Text, node.IconIndex, node.IconIndex);
parentNode.Tag
=node.Tag;if (!string.IsNullOrEmpty(node.ForeColor))
{
//如果节点颜色有值,则修改前景色 parentNode.ForeColor =ColorTranslator.FromHtml(node.ForeColor);
}
//递归处理树形列表 InitTreeNode(node.Nodes, parentNode);if (parentNode.Text != "标记颜色")
{
parentNode.Expand();
//选择性的展开部分一级节点 }//把根节点加入到树对象里面显示 this.treeView1.Nodes.Add(parentNode);
}
}
}
/// <summary> ///递归处理树形列表/// </summary> /// <param name="nodes">树节点信息对象</param> /// <param name="pNode">TreeView根节点</param> private void InitTreeNode(List<TreeNodeInfo>nodes, TreeNode pNode)
{
foreach (TreeNodeInfo node innodes)
{
TreeNode subNode
= newTreeNode(node.Text, node.IconIndex, node.IconIndex);
subNode.Tag
=node.Tag;if (!string.IsNullOrEmpty(node.ForeColor))
{
//如果节点颜色有值,则修改前景色 subNode.ForeColor =ColorTranslator.FromHtml(node.ForeColor);
}
//递归调用 InitTreeNode(node.Nodes, subNode);
pNode.Nodes.Add(subNode);
}
}

这里基本不会涉及很多逻辑,我们只需要对树形节点的结构进行遍历展示即可。

其实后端已经给我们处理好很多数据了,包括对节点构建、数据字典的处理,以及每个条件的数量处理都合并一起,它的逻辑还是很多的。

这个部分的逻辑由于代码量比较大,我们可以简化抽取出来一个辅助类处理,这样在需要的地方直接调用辅助类进行处理就可以了。

抽取辅助类后,对处理逻辑的调用简单了很多。

 CustomerHelper helper = newCustomerHelper();var result =helper.GetCustomerTree(userId, companyId, dataFilter, shareUserCondition);

这部分有300多行代码,具体就不再一一介绍了,主要就是对各个接口的处理,获取数据并组装起来。

这种在服务器端对主要逻辑进行封装,简化客户端的处理逻辑,是我们推荐的方式,可以极大的提高界面响应效率,减少不必要的网络延迟损耗,从而提高用户的体验效果,对于具有较高运算速度的服务器,更是物尽其用。

随着时间的推移,Winform也算是能够坚持下来最久的技术之一了,它的昔日辉煌和现今的依旧活跃,导致了它依旧拥有者很庞大的用户群体,虽然目前很多技术日新月异的,曾经的ASP、ASP.NET WebForm、Asp.NET MVC、WPF等技术基本上淡出了视野,而迎来了.NET Core、UWP等技术应用,.NET Core也给.NET迎来了一次重要的涅槃重生的契机,可以更高效的运行在各种平台上,从而激发了.NET的又一春。Winform的技术虽然基本上已经压缩在一定的范围内,不过由于的用途广泛,微软也无法完全舍弃,据说在即将到来的.NET core 3.0里面,会支持Winform,那真是非常不错的一次转变。

1、Winform的应用场景

我自己也是一个Winform开发的拥趸,基本上十几年来一直用着Winform开发各种各样的应用(虽然我也做很多相关的Web开发),从最早的一些小工具,小共享软件什么的,到后面给客户开发一些数据管理系统、业务管理系统等等,因此在这方面使用还算有一定的背景,可以对WInform这个技术应用做一个个人的概括。

1)用户体验

在Winform应用里面,和其他Web系统比起来,它的用户体验是最好的,而且界面响应速度也比Web界面来的快捷一些,由于很多情况下,用户考虑使用方便性,如一些报表的展示、打印、导入导出文件的处理等常规的操作,都还是习惯使用Winform这种定制型非常好的界面来处理,毕竟大多数情况下,单位都有一套业务和数据的管理系统来处理这些业务。

2)数据敏感

另外很多情况下,如一些事业单位、机构什么,他们的数据是比较敏感的,不希望对外公开,网络的引入会提供数据外泄的可能,另外它们也是经常处于内网的环境下,因此一个单机版的程序就可以搞定他们的日常业务处理了,这种特别的业务环境,注定了使用Winform来处理会更胜一筹。

3)开发便利

Winform开发的程序,发布共享比较容易,直接安装就可以使用,可以不需要部署在云端(虽然我的混合框架方式可以访问Web API、WCF等服务获取数据,透明的数据处理);而且Winform的界面开发起来非常方便,结合界面套件,可以做出非常棒的界面效果。另外从开发角度上讲,Web前端的技术淘汰非常快,Winform的技术积累反而是在逐步加固的过程,因此对于一些开发人员来说,迭代Winform开发的应用会更加方便,也更加熟练,因此只要客户在用,系统兼容,这种Winform的程序会一直保留下去。

2、Winform开发的过程

1)界面开发

Winform开发对比其他有不少优点,主要的特点还是开发方便,基于一定的框架,可以快速开发特定的业务管理系统。如下是我客户关系管理系统的界面效果。主界面是采用了DevExpress套件,可以让界面看起来非常统一漂亮,另外对于界面的开发,我们可以基于数据库信息的基础上,通过工具快速生成常规的列表展示界面,以及编辑界面,从而进行一定的调整即可。

对于列表界面,常规的就是包含数据的分页展示、查询、高级查询、导入、导出、打印等这些常规的功能,这些都可以通过定义好的界面模板进行统一生成,生成后进行一定的调整(如加入左侧树形列列表)即可。

如这个编辑界面,也是基于数据库信息的生成后进行一定的调整即可。我们可以快速的修改控件的类型,如修改为下来列表类型,备注类型等,而在代码中进行字典类型绑定就可以显示字典数据了。

2)后台代码开发

对于一个新建的业务表,我们需要开发的需要底层的实现和界面层的展示,这些工作量也是非常巨大的,如果基于控件细粒度的处理,也是非常繁琐的工作,因此基于这些开发过程的考虑,我们引入了提高效率开发的代码生成工具Database2Sharp,专门为我们基于开发框架基础上的框架实现代码开发,和业务界面展示的快速开发。

代码生成工具,不仅能够让它生成我们常规开发的界面层以下的实现代码(包括BLL、DAL、Entity、IDAL等层,以及混合框架的WCF、Web API的实现层和调用封装层),以及界面层的调用代码。

有了这些的处理,我们可极大减轻工作量。

生成的项目中,我们已经有了对应框架支持的实现层了。

普通Winform框架的分层架构图。

3)底层数据库支持

在实际需求中,你往往不能决定客户需要用什么数据库,那么需要根据实际需求或者环境进行数据库类型的选型,如果是单机版为了方便可以使用SQLite,如果是已有业务系统或者需要响应速度快一些的,那么考虑使用SQLServer或者Mysql、有些历史原因的可能会用PostgreSQL或者Oracle等等。那么框架的弹性就需要支持多种数据库的了,这种支持不能导致太大的工作量最好,否则会弄得焦头烂额的。

框架底层数据库访问采用了微软企业库实现,因此在处理多种数据库访问的时候,能够提供统一的访问处理操作,同时对不同的数据库支持操作也是非常不错的。下图是框架底层数据库的支持情况。

采用了微软企业库Enterprise Library作为我们底层的数据库访问模块后,对于多种数据库的访问操作,就会统一采用这个企业库的数据库访问对象,操作起来非常一致,为了对不同数据库的常规增删改查等一些操作进行进一步的封装,以达到简化代码的目的,因此我们可以为每个不同的数据库定义一个数据访问操作基类,以便实现一些不同数据库差异性的处理,但是它们还是有一个共同的数据访问基类。

采用不同的数据库,我们需要为不同数据库的访问层进行生成处理,如为SQLServer数据的表生成相关的数据访问层DALSQL,里面放置各个表对象的内容,不过由于采用了相关的继承类处理和基于数据库的代码生成,需要调整的代码很少。

4)数据集中的云端模式

在很多业务系统中,有很多需求是希望部署在云端服务器中,这种方式可以实现数据的几种管理,也有利于安全。因此我们整合了WCF和Web API两种服务访问方式,而在开发界面基础上,不需要太大的变化即可接入,这就是我们的混合开发框架。

混合框架的多种方式支持

而对于WCF或者Web API的封装,我们是通过接口适配的方式,调用层需要对业务接口进行封装,从而产生封装的代码量。因此可以利用代码生成工具生成对应业务模块的接口适配代码,可以极大减轻对这部分的开发效率损耗。

混合框架的架构如下所示。

代码生成工具Database2Sharp,生成整体性的混合型框架项目如下所示,只是没有下图的界面部分,这部分在实际开发过程中,结合我的混合型框架案例进行整合即可,另外也可以界使用Database2Sharp进行Winform界面的开发,这样整体性就非常方便操作了:

Winform调用Web API的过程,这个过程可以通过下面这个图示进行讲解。

5)模块化的框架结构

在开发Winform应用的时候,我们除了希望简化代码外,其实很多常规的业务,我们希望不希望都要重新开发,如权限管理系统、字典管理、附件管理等,这些是很多业务都涉及到的模块,我们应该在一定粒度上实现整合现有模块即可,这样可以降低我们开发的难度和减少开发时间,我们就可以把重要的时间花在具体的业务领域里面,快速响应客户的需求开发。

混合型框架可以看成是Winform框架高级版本,除了它本身是一个完整的业务系统外,它外围的所有辅助性模块均(如通用权限、通用字典、通用附件管理、通用人员管理。。。。)都实现了这种混合型的框架,因此使用非常方便,整个框架如果简化来看,就是在原有的Winform界面层,用接口调用方式,避免和业务逻辑类的紧耦合关系。

最近这些年,随着前端应用技术突飞猛进,产生了很多新的前端框架,当然也引入了数不胜数的前端技术概念,前端不在是早期Web Form的拖拉处理方式,也不再是Ajax+HTML那么简单,随着前端技术的发展,前端的JS越来越重要,也越来越复杂,而为了开发的方便,引入了很多可以对JS+CSS进行编译的框架,而在发布的时候按需编译处理,从而增强了整个前端的开发过程,这些前端的技术包括AngularJS、React、Vue等等,这些前端技术应用框架又囊括了很多相关的技术,包括了MVVM(Model-View-ViewModel)、ES6、Babel、dva、umi、less等技术或概念。前端技术越滚越大,范围也越来越广,大有日新月异的感觉。

1、前端技术的自我回顾和展望

记得在上大学时候,开始玩asp的年代,前端和后端糅合一起的困境;也曾记得WebForm开发的乐趣和无奈,快捷但是很丑很笨重;而现在还在继续做着Ajax + HTML的这种前端的处理,痛并快乐着。技术总是一步步的推进则,但是眼光一旦聚焦在某个技术范畴,日月如梭,抬头间很快就会发现世界又多了新的前端技术,从开始的犹豫和不确信的停留这段时间后,发现整个前端的世界也已经渐成格局,包括Angular、React、Vue等技术应用已经日趋成熟,而且拥有着庞大的拥趸群体,也有着丰富的资源可供学习和了解。

下面是Angular、React、Vue几个技术框架的一些介绍。

AngularJS诞生于2009年,由Misko Hevery 等人创建,后为Google所收购。是一款优秀的前端JS框架,已经被用于Google的多款产品当中。AngularJS有着诸多特性,最为核心的是:MVC(Model–view–controller)、模块化、自动化双向数据绑定、语义化标签、依赖注入等等。Angular开发在全球开发人员中广泛流行,并被谷歌,福布斯,WhatsApp,Instagram,healthcare.gov和许多财富500强公司等大型组织使用。

React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。 由于 React 的设计思想极其独特,属于革命性创新,性能出众,代码逻辑却非常简单。所以,越来越多的人开始关注和使用,认为它可能是将来 Web 开发的主流工具。

Vue.js是讨论最多且发展最快的JavaScript框架之一。它由前谷歌员工Evan You创建,他在担任Google员工时曾在Angular工作过。您可以认为它是成功的,因为它能够使用HTML,CSS和JavaScript构建有吸引力的UI。

这些技术各有优点,很难片面的说明谁优谁劣,它们都各自有自己的生存土壤和大批的拥趸,而我开始选型做前端技术更新的时候,主要看中的是阿里巴巴的Ant-Design开发框架,这个它是使用了React的技术框架,因此也就自然而然的研究学习起React和Ant-Design来,虽然之前对前端的一些技术有所涉猎,但是真正等你想要进入Ant-Design的开发大门的时候,还是感觉自己像进入了一个前端技术的大观园,一个个新概念接踵而来,一种种代码的写法迎面冲击,教程看了几遍还是一头雾水,真的开始怀疑人生了,不过学习新技术还是需要很多平静的心态,调整好,一步一个脚印相信还是有所斩获的,偶尔看到阮一峰的大牛介绍在学习研究React的时候,也曾花了几个月的时候,虽然他的高度难以看齐,但是学习的韧劲和毅力,是值得我们学习的。学习新的东西,从技术角度,可以满足好奇心,提高技术水平;从职业角度,有利于求职和晋升,有利于参与潜力大的项目(摘自阮一峰笔记)。

2、React的技术学习

接触一些新的东西,就必然需要投入精力来学习掌握。对于学习Ant-Desin,虽然这个框架本身提供了很多教程介绍,但是我们一些技术点,还是需要更细节的学习,首推还是阮一峰的技术日志吧。

1、ECMAScript 6 入门

2、React 入门实例教程

3、Redux 入门教程(一):基本用法

4、Redux 入门教程(二):中间件与异步操作

5、Redux 入门教程(三):React-Redux 的用法

6、
Redux 文档基础教程

7、DvaJS快速上手

下面有些内容在学习的时候,掌握的不是很好,摘录并作为一个回顾吧。

模块的 Import 和 Export

import
用于引入模块,
export
用于导出模块。

//引入全部
import dva from 'dva';//引入部分
import { connect } from 'dva';
import { Link, Route } from
'dva/router';//引入全部并作为 github 对象 import * as github from './services/github';//导出默认 export defaultApp;//部分导出,需 import { App } from './file'; 引入 export class App extend Component {};

析构赋值

析构赋值让我们从 Object 或 Array 里取部分数据存为变量。

//对象
const user = { name: 'guanguan', age: 2};
const { name, age }
=user;
console.log(`${name} : ${age}`);
//guanguan : 2 //数组 const arr = [1, 2];
const [foo, bar]
=arr;
console.log(foo);
//1

我们也可以析构传入的函数参数。

const add = (state, { payload }) =>{returnstate.concat(payload);
};
//析构时还可以配 alias,让代码更具有语义 const add = (state, { payload: todo }) =>{returnstate.concat(todo);
};

对象展开运算符(Object Spread Operator)

//可用于组装数组。
const todos = ['Learn dva'];
[...todos,
'Learn antd']; //['Learn dva', 'Learn antd'] //也可用于获取数组的部分项。 const arr = ['a', 'b', 'c'];
const [first, ...rest]
=arr;
rest;
//['b', 'c'] //With ignore const [first, , ...rest] =arr;
rest;
//['c'] //还可收集函数参数为数组。 functiondirections(first, ...rest) {
console.log(rest);
}
directions(
'a', 'b', 'c'); //['b', 'c']; //代替 apply。 functionfoo(x, y, z) {}
const args
= [1,2,3];//下面两句效果相同 foo.apply(null, args);
foo(...args);
//对于 Object 而言,用于组合成新的 Object const foo ={
a:
1,
b:
2,
};
const bar
={
b:
3,
c:
2,
};
const d
= 4;

const ret
= { ...foo, ...bar, d }; //{ a:1, b:3, c:2, d:4 }

propTypes

JavaScript 是弱类型语言,所以请尽量声明 propTypes 对 props 进行校验,以减少不必要的问题。

functionApp(props) {return <div>{props.name}</div>;
}
App.propTypes
={
name: React.PropTypes.string.isRequired,
};

内置的 prop type 有:

  • PropTypes.array
  • PropTypes.bool
  • PropTypes.func
  • PropTypes.number
  • PropTypes.object
  • PropTypes.string

DVA数据流向

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State。

Reducer和effects

reducer 是一个函数,接受 state 和 action,返回老的或新的 state 。即:
(state, action) => state

app.model({
namespace:
'todos',
state: [],
reducers: {
add(state, { payload: todo }) {
returnstate.concat(todo);
},
remove(state, { payload: id }) {
return state.filter(todo => todo.id !==id);
},
update(state, { payload: updatedTodo }) {
return state.map(todo =>{if (todo.id ===updatedTodo.id) {return{ ...todo, ...updatedTodo };
}
else{returntodo;
}
});
},
},
};

建议最多一层嵌套,以保持 state 的扁平化,深层嵌套会让 reducer 很难写和难以维护。

app.model({
namespace:
'app',
state: {
todos: [],
loading:
false,
},
reducers: {
add(state, { payload: todo }) {
const todos
=state.todos.concat(todo);return{ ...state, todos };
},
},
});

effects示例

app.model({
namespace:
'todos',
effects: {
*addRemote({ payload: todo }, { put, call }) {
yield call(addTodo, todo);
yield put({ type:
'add', payload: todo });
},
},
});

put用于触发 action,call用于调用异步逻辑,支持 promise。

异步请求

异步请求基于 whatwg-fetch,API 详见:
https://github.com/github/fetch

GET 和 POST

import request from '../util/request';//GET
request('/api/todos');//POST
request('/api/todos', {
method:
'POST',
body: JSON.stringify({ a:
1}),
});

统一错误处理

假如约定后台返回以下格式时,做统一的错误处理。

{
status:
'error',
message:
'',
}

编辑
utils/request.js
,加入以下中间件:

functionparseErrorMessage({ data }) {
const { status, message }
=data;if (status === 'error') {throw newError(message);
}
return{ data };
}

然后,这类错误就会走到
onError
hook 里。

Subscription

subscriptions
是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。格式为
({ dispatch, history }) => unsubscribe

异步数据初始化

比如:当用户进入
/users
页面时,触发 action
users/fetch
加载用户数据。

app.model({
subscriptions: {
setup({ dispatch, history }) {
history.listen(({ pathname })
=>{if (pathname === '/users') {
dispatch({
type:
'users/fetch',
});
}
});
},
},
});
react dva 的 connect 与 @connect
connect的作用是将组件和models结合在一起。将models中的state绑定到组件的props中。并提供一些额外的功能,譬如dispatch

connect 的使用

connect 方法返回的也是一个 React 组件,通常称为容器组件。因为它是原始 UI 组件的容器,即在外面包了一层 State。

connect 方法传入的第一个参数是 mapStateToProps 函数,该函数需要返回一个对象,用于建立 State 到 Props 的映射关系。

简而言之,connect接收一个函数,返回一个函数。

第一个函数会注入全部的models,你需要返回一个新的对象,挑选该组件所需要的models。

export default connect(({ user, login, global = {}, loading }) =>({
currentUser: user.currentUser,
collapsed: global.collapsed,
fetchingNotices: loading.effects[
'global/fetchNotices'],
notices: global.notices
}))(BasicLayout);
//简化版 export defaultconnect(
({ user, login, global
= {}, loading }) =>{return{
currentUser: user.currentUser,
collapsed: global.collapsed,
fetchingNotices: loading.effects[
'global/fetchNotices'],
notices: global.notices
}
}
)(BasicLayout);

@connect的使用

其实只是connect的装饰器、语法糖罢了。

//将 model 和 component 串联起来
export default connect(({ user, login, global = {}, loading }) =>({
currentUser: user.currentUser,
collapsed: global.collapsed,
fetchingNotices: loading.effects[
'global/fetchNotices'],
notices: global.notices,
menuData: login.menuData,
redirectData: login.redirectData, }))(BasicLayout);
//改为这样(export 的不再是connect,而是class组件本身。),也是可以执行的,但要注意@connect必须放在export default class前面://将 model 和 component 串联起来
@connect(({ user, login, global = {}, loading }) =>({
currentUser: user.currentUser,
collapsed: global.collapsed,
fetchingNotices: loading.effects[
'global/fetchNotices'],
notices: global.notices,
menuData: login.menuData,
redirectData: login.redirectData,
}))

export
defaultclass BasicLayout extends React.PureComponent {//... }
export default connect(从 model 的 state 中获取数据)(要将数据绑定到哪个组件)

以上部分内容摘自 https://blog.csdn.net/zhangrui_web/article/details/79651812

2、Ant-Design的框架

这款基于React开发的UI框架,界面非常简洁美观,是阿里巴巴旗下蚂蚁金融服务集团(旗下拥有支付宝、余额宝等产品)所设计的一个前端UI组件库。目前支持了React, 并且有一个对Vue支持的测试版本。

学习和使用Ant-Design,我们可以使用VSCode来对项目代码进行维护和编辑,这样可以在Mac和Window环境同样的开发体验和操作模式,非常方便。

如果需要掌握Ant-Design框架,我们需要了解model,namespace,connect,dispatch,action,reducer ,effect这些概念。

DVA 的 model 对象有几个基本的属性介绍。

  1. namespace
    :model 的命名空间,只能用字符串。一个大型应用可能包含多个 model,通过
    namespace
    区分。
  1. state
    :当前 model 状态的初始值,表示当前状态。
  1. reducers
    :用于处理同步操作,可以修改
    state
    ,由
    action
    触发。reducer 是一个纯函数,它接受当前的 state 及一个 action 对象。action 对象里面可以包含数据体(payload)作为入参,需要返回一个新的 state。
  1. effects
    :用于处理异步操作(例如:与服务端交互)和业务逻辑,也是由 action 触发。但是,它不可以修改 state,要通过触发 action 调用 reducer 实现对 state 的间接操作。
  1. action
    :action 就是一个普通 JavaScript 对象,是 reducers 及 effects 的触发器,形如
    { type: 'add', payload: todo }
    ,通过 type 属性可以匹配到具体某个 reducer 或者 effect,payload 属性则是数据体,用于传送给 reducer 或 effect。

整体的数据流向见下图:

在Reducer里面,不要修改传入的
state
。 使用
Object.assign()
新建了一个副本。不能这样使用
Object.assign(state, { visibilityFilter: action.filter })
,因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。

function todoApp(state =initialState, action) {switch(action.type) {caseSET_VISIBILITY_FILTER:returnObject.assign({}, state, {
visibilityFilter: action.filter
})
default:returnstate
}
}

或者使用使用对象展开运算符(Object Spread Operator)来处理,从而使用
{ ...state, ...newState }
达到相同的目的。

reducers: {
save(state, action) {
return{
...state,
...action.payload,
};
},
},


default
情况下返回旧的
state
。遇到未知的 action 时,一定要返回旧的
state

每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的
state
参数都不同,分别对应它管理的那部分 state 数据。

下面两种合成 reducer 方法完全等价:

const reducer =combineReducers({
a: doSomethingWithA,
b: processB,
c: c
})
function reducer(state ={}, action) {return{
a: doSomethingWithA(state.a, action),
b: processB(state.b, action),
c: c(state.c, action)
}
}

dva封装了redux,减少很多重复代码比如action reducers 常量等,dva所有的redux操作是放在models目录下,通过namespace作为key,标识不同的模块state,可以给state设置初始数据。

reducers跟传统的react-redux写法一致,所有的操作放在reducers对象内

Effect 被称为副作用,在我们的应用中,最常见的就是异步操作,
Effects
的最终流向是通过
Reducers
改变
State

其中上面的effects里面,call, put其实是saga的写法,dva集成了saga,可以参考上图中的saga内容

DVA 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,DVA 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。

DVA 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装,没有引入任何新概念,全部代码不到 100 行。

在Ant-Design的Pages/.umi目录里面,有一个initDva.js文件,就是用来统一批量处理 DVA 的引入的,如下所示。

在有 DVA 之前,我们通常会创建
sagas/products.js
,
reducers/products.js

actions/products.js
,然后在这些文件之间来回切换。

有了 DVA 后,它最核心的是提供了
app.model
方法,用于把 reducer, initialState, action, saga 封装到一起,这样我们在书写代码的时候,把它主要内容,和加载分离出来。如果建立的Model比较多,每次开始的时候需要加入这一句好像也是挺麻烦的,如果可以自动把这个model批量加入,应该会更好吧,不过不知道是基于什么考量。

在给客户开发一个信息发送功能的时候,需要涉及到短信的发送,短信发送一般不同的厂商提供的接口不同,处理方式也不太一样,之前用的一个厂商的,提供了一个封装类就很容易发送短息,因此都是基于HTTP协议做的一个数据发送而已,接触阿里云的短信服务器后,发现阿里云还增加了非常多的参数,其中包括一些秘钥和签名的内容。短信发送由于比较敏感原因,大多数应用场景是验证码或者一些固定的信息提醒,因此厂商都要求客户按预定的模板来发送,这样限制了短信的应用场景,只能根据业务进行消息定制了。本篇随笔主要介绍阿里云的短信服务的发送处理。

1、短信发送的处理介绍

在短信发送中,阿里云提供自己的SDK封装,以降低使用的难度,不过需要引入它提供的SDK类库;本篇随笔主要介绍基于HTTP方式进行自行的封装处理,这部分代码我从网上摘录并进行一定的调整,测试成功。

使用阿里云的短信服务,需要注册登录自己的阿里云控制台,然后进入AccessKeys的处理界面

然后系统会提示需要创建一个新的Key(如果没有的话就创建,否则使用已有的即可)

这里我们获取到AccessKey ID 和Access Key Secret两个关键信息,需要用在数据签名的里面的。

另外我们需要创建一个SignName,也就是签名,一般为我们短信提示的公司名称,如【广州爱奇迪】这样的字样。

短信是基于模板的,阿里云不能发送随意的内容,因此只能基于模板发送,如验证码或者业务消息,有点类似微信的模板消息了,因此里面可以添加变量发送的。

记得我以前写过一个关于动态变量的信息发送的文章《
一个包含动态变量的短信模板设计分析
》,就是介绍如何处理变量模板消息的。

阿里云默认提供了一些基础模板,如下所示。

一般我们业务可能还需要定制一些业务消息,那么需要审核通过才可以使用新增的模板消息。

短信的发送可以利用API接口进行发送,如下所示是它的API说明

如果需要采集用户的回复信息,如一些随访记录,那么需要做一个接口的处理,如下所示。

发送短信的API接口详细说明如下所示。

其实请求信息比上面列出的信息多很多,包括秘钥和数据加密信息等的处理,下面详细给出代码说明。

2、模板消息发送

有了上面的信息介绍,我们大概了解了短信消息发送的处理规则了。

实际上,发送信息的时候,我们可能需要添加很多参数信息,如下代码所示。

            Dictionary<string, string> keyValues = new Dictionary<string, string>();//声明一个字典//1.系统参数
            keyValues.Add("SignatureMethod", "HMAC-SHA1");
keyValues.Add(
"SignatureNonce", Guid.NewGuid().ToString());
keyValues.Add(
"AccessKeyId", AccessKeyId);
keyValues.Add(
"SignatureVersion", "1.0");
keyValues.Add(
"Timestamp", nowDate);
keyValues.Add(
"Format", "Json");//可换成xml//2.业务api参数 keyValues.Add("Action", "SendSms");
keyValues.Add(
"Version", "2017-05-25");
keyValues.Add(
"RegionId", "cn-hangzhou");
keyValues.Add(
"PhoneNumbers", mobile);
keyValues.Add(
"SignName", SignName);
keyValues.Add(
"TemplateParam", "{\"code\":\"" + code + "\"}");
keyValues.Add(
"TemplateCode", templateCode);
keyValues.Add(
"OutId", "123");

具体我们来贴出不用SDK的短信发送辅助类,如下代码所示。

    /// <summary>
    ///阿里短信发送/// </summary>
    public classSmsHelper
{
private const string endpoint = "dysmsapi.aliyuncs.com";private const string AccessKeyId = "你的秘钥键";private const string AccessKeySecret = "你的秘钥值";private const string SignName = "广州爱奇迪";/// <summary> ///短信验证码/// </summary> /// <param name="mobile"></param> /// <param name="code"></param> /// <returns></returns> public static string SendSms(string mobile, int code, string templateCode = "SMS_126645400")
{
string nowDate = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'");//GTM时间 Dictionary<string, string> keyValues = new Dictionary<string, string>();//声明一个字典//1.系统参数 keyValues.Add("SignatureMethod", "HMAC-SHA1");
keyValues.Add(
"SignatureNonce", Guid.NewGuid().ToString());
keyValues.Add(
"AccessKeyId", AccessKeyId);
keyValues.Add(
"SignatureVersion", "1.0");
keyValues.Add(
"Timestamp", nowDate);
keyValues.Add(
"Format", "Json");//可换成xml//2.业务api参数 keyValues.Add("Action", "SendSms");
keyValues.Add(
"Version", "2017-05-25");
keyValues.Add(
"RegionId", "cn-hangzhou");
keyValues.Add(
"PhoneNumbers", mobile);
keyValues.Add(
"SignName", SignName);
keyValues.Add(
"TemplateParam", "{\"code\":\"" + code + "\"}");
keyValues.Add(
"TemplateCode", templateCode);
keyValues.Add(
"OutId", "123");//3.去除签名关键字key if (keyValues.ContainsKey("Signature"))
{
keyValues.Remove(
"Signature");
}
//4.参数key排序 Dictionary<string, string> ascDic = keyValues.OrderBy(o => o.Key).ToDictionary(o => o.Key, p =>p.Value.ToString());//5.构造待签名的字符串 StringBuilder builder = newStringBuilder();foreach (var item inascDic)
{
if (item.Key == "SignName")
{
}
else{
builder.Append(
"&").Append(specialUrlEncode(item.Key)).Append("=").Append(specialUrlEncode(item.Value));
}
if (item.Key == "RegionId")
{
builder.Append(
"&").Append(specialUrlEncode("SignName")).Append("=").Append(specialUrlEncode(keyValues["SignName"]));
}
}
string sorteQueryString = builder.ToString().Substring(1);

StringBuilder stringToSign
= newStringBuilder();
stringToSign.Append(
"GET").Append("&");
stringToSign.Append(specialUrlEncode(
"/")).Append("&");
stringToSign.Append(specialUrlEncode(sorteQueryString));
string Sign = MySign(AccessKeySecret + "&", stringToSign.ToString());//6.签名最后也要做特殊URL编码 string signture =specialUrlEncode(Sign);//最终打印出合法GET请求的URL string url = string.Format("http://{0}/?Signature={1}{2}", endpoint, signture, builder);string result =GetHtmlFormUrl(url);returnresult;
}
/// <summary> ///短信接口C#调用方法/// </summary> /// <param name="url"></param> /// <returns></returns> private static string GetHtmlFormUrl(stringurl)
{
string strRet = null;if (url == null || url.Trim().ToString() == "")
{
returnstrRet;
}
string targeturl =url.Trim().ToString();try{
HttpWebRequest hr
=(HttpWebRequest)WebRequest.Create(targeturl);
hr.UserAgent
= "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)";
hr.Method
= "GET";
hr.Timeout
= 30 * 60 * 1000;
WebResponse hs
=hr.GetResponse();
Stream sr
=hs.GetResponseStream();
StreamReader ser
= newStreamReader(sr, Encoding.UTF8);

strRet
=MessageHandle(ser.ReadToEnd());
}
catch(Exception ex)
{
strRet
= "短信发送失败!" +ex.Message;
}
returnstrRet;
}
/// <summary> ///验证手机号码是否合法/// </summary> /// <param name="mobile">电话号码</param> /// <returns></returns> public static bool IsMobile(stringmobile)
{
return System.Text.RegularExpressions.Regex.IsMatch(mobile, @"^1[3|4|5|7|8][0-9]\d{8}$");
}
/// <summary> ///URL编码/// </summary> /// <param name="value"></param> /// <returns></returns> private static string specialUrlEncode(stringtemp)
{
StringBuilder stringBuilder
= newStringBuilder();for (int i = 0; i < temp.Length; i++)
{
string t =temp[i].ToString();string k =HttpUtility.UrlEncode(t, Encoding.UTF8);if (t ==k)
{
stringBuilder.Append(t);
}
else{
stringBuilder.Append(k.ToUpper());
}
}
return stringBuilder.ToString().Replace("+", "%20").Replace("*", "%2A").Replace("%7E", "~");
}
/// <summary> ///HMACSHA1签名/// </summary> /// <param name="accessSecret"></param> /// <param name="stringToSign"></param> /// <returns></returns> private static string MySign(string accessSecret, stringstringToSign)
{
try{var hmacsha1 = newHMACSHA1(Encoding.UTF8.GetBytes(accessSecret));var dataBuffer =Encoding.UTF8.GetBytes(stringToSign);var hashBytes =hmacsha1.ComputeHash(dataBuffer);string stringbyte = BitConverter.ToString(hashBytes, 0).Replace("-", string.Empty).ToLower();byte[] bytes =strToToHexByte(stringbyte);returnConvert.ToBase64String(bytes);
}
catch(Exception ex)
{
throwex;
}
}
/// <summary> ///字符串转16进制字节数组/// </summary> /// <param name="hexString"></param> /// <returns></returns> private static byte[] strToToHexByte(stringhexString)
{
hexString
= hexString.Replace(" ", "");if ((hexString.Length % 2) != 0)
hexString
+= " ";byte[] returnBytes = new byte[hexString.Length / 2];for (int i = 0; i < returnBytes.Length; i++)
returnBytes[i]
= Convert.ToByte(hexString.Substring(i * 2, 2), 16);returnreturnBytes;
}
/// <summary> ///消息处理机制/// </summary> /// <param name="str"></param> /// <returns></returns> private static string MessageHandle(stringstr)
{
MessageModel message
= JsonConvert.DeserializeObject<MessageModel>(str);string result = "";switch(message.Code)
{
case "OK":
result
= "短信发送成功!";break;case "isp.RAM_PERMISSION_DENY":
result
= "RAM权限DENY";break;case "isv.OUT_OF_SERVICE":
result
= "业务停机";break;case "isv.PRODUCT_UN_SUBSCRIPT":
result
= "未开通云通信产品的阿里云客户";break;case "isv.PRODUCT_UNSUBSCRIBE":
result
= "产品未开通";break;case "isv.ACCOUNT_NOT_EXISTS":
result
= "账户不存在";break;case "isv.ACCOUNT_ABNORMAL":
result
= "账户异常";break;case "isv.SMS_TEMPLATE_ILLEGAL":
result
= "短信模板不合法";break;case "isv.SMS_SIGNATURE_ILLEGAL":
result
= "短信签名不合法";break;case "isv.INVALID_PARAMETERS":
result
= "参数异常";break;case "isv.MOBILE_NUMBER_ILLEGAL":
result
= "非法手机号";break;case "isv.MOBILE_COUNT_OVER_LIMIT":
result
= "手机号码数量超过限制";break;case "isv.TEMPLATE_MISSING_PARAMETERS":
result
= "模板缺少变量";break;case "isv.BUSINESS_LIMIT_CONTROL":
result
= "业务限流";break;case "isv.INVALID_JSON_PARAM":
result
= "JSON参数不合法,只接受字符串值";break;case "isv.PARAM_LENGTH_LIMIT":
result
= "参数超出长度限制";break;case "isv.PARAM_NOT_SUPPORT_URL":
result
= "不支持URL";break;case "isv.AMOUNT_NOT_ENOUGH":
result
= "账户余额不足";break;case "isv.TEMPLATE_PARAMS_ILLEGAL":
result
= "模板变量里包含非法关键字";break;
}
returnresult;
}

}
internal classMessageModel
{
public string RequestId { get; set; }public string Code { get; set; }public string Message { get; set; }
}

上面代码不是我原创,声明一下,我做了一些修改调整而已,方便辅助类的使用,我们输入我们的企业的秘钥键值,然后发送测试短信即可。

            string tel = "18620292076";string result = SmsHelper.SendSms(tel, 123456);
Console.WriteLine(result);

发送测试,3~5秒就可以收到验证码信息的提示了,如下所示。

以上就是短信消息的发送,希望对使用阿里云短信服务的开发人员有所帮助,辅助类直接就可以使用了。

在我们做客户关系管理系统的Winform界面的时候,需要对进展阶段这个属性进行一个方便的动态切换和标记处理,如我们根据不同的进展阶段显示不同的相关信息,也可以随时保存当前的阶段信息。其实也是一个比较常见的功能,我们可以把字典列表扁平化动态展示在控件上,然后根据用户选择的阶段位置进行切换即可,本篇随笔就是在客户的需求基础上完善这个功能。

1、进展阶段的动态展示和处理

我们来看看界面的大致情况

其实这部分是根据字典列表进行动态展示的,也就是使用一个用户控件进行处理即可。

为了实现这个功能,我们先创建一个用户控件,如下界面所示,保留一个按钮,这个我们让它先占着位置,最后还是把它追加到最后的位置上即可。

为了展示所有阶段,并记录当前阶段,我们设置了两个变量,放在用户控件里面

        /// <summary>
        ///阶段列表/// </summary>
        public List<CListItem> StageList { get; set; }/// <summary>
        ///当前阶段的值/// </summary>
        public double CurrentStage { get; set; }

然后为了在切换和保存两个事件触发外部处理,我们添加两个事件处理,如下所示

        /// <summary>
        ///选中某个阶段的处理事件/// </summary>
        public EventHandler OnSelectedStageHandler { get; set; }/// <summary>
        ///设置阶段完成的处理事件/// </summary>
        public EventHandler OnSetCompleteStage { get; set; }

这样用户控件看起来就像是这样子的代码了。

    /// <summary>
    ///阶段控件显示/// </summary>
    public partial classStageControl : BaseUserControl
{
/// <summary> ///阶段列表/// </summary> public List<CListItem> StageList { get; set; }/// <summary> ///当前阶段的值/// </summary> public double CurrentStage { get; set; }/// <summary> ///选中某个阶段的处理事件/// </summary> public EventHandler OnSelectedStageHandler { get; set; }/// <summary> ///设置阶段完成的处理事件/// </summary> public EventHandler OnSetCompleteStage { get; set; }

为了动态展示控件的信息,我们需要使用一个自定义函数来对控件按钮的位置进行判断并绘制,这样可以根据需要进行相关样式的定义,也可以动态变化阶段的列表内容。

        /// <summary>
        ///初始化控件/// </summary>
        public voidInit()
{
this.Controls.Clear();//清空界面//根据阶段列表数量计算每个选项的大小 if (StageList != null && StageList.Count > 0)
{
var count =StageList.Count;//计算每项的宽度、高度 var width = (this.Width-150) * 0.8 / (count * 1.0);var height = this.Height * 0.8;//计算间隔位置,默认为0,最大不超过20宽度 double space = 0;if ((count - 1) > 0)
{
space
= (this.Width * 0.2) / ((count - 1) * 1.0);
}
space
= (space > 20) ? 20 : space; //限定最大间隔20//根据列表项目,动态构建按钮显示项目 int i = 0;foreach (CListItem item inStageList)
{
double value =Convert.ToDouble(item.Value);
SimpleButton button
= newSimpleButton();
button.Text
= value.ToString("P0");//显示百分比 button.ToolTip =item.Text;
button.Tag
=value;
button.ButtonStyle
=BorderStyles.HotFlat;
button.Appearance.Options.UseBackColor
= true;//根据所处阶段设置背景色 if(CurrentStage >=value)
{
button.Appearance.BackColor
=Color.SkyBlue;
}
else{
button.Appearance.BackColor
=Color.Transparent;
}
//按钮的单击事件,触发对外部的处理 button.Click += (s,e)=>{var currentStep =(SimpleButton)s;
CurrentStage
=Convert.ToDouble(currentStep.Tag);if(OnSelectedStageHandler != null)
{
OnSelectedStageHandler(s, e);
}
Init();

};
//根据计算好的信息,设置按钮大小和位置 button.Size = new Size((int)width, (int)height);
button.Location
= new Point(i * (int)(width + space), 0);this.Controls.Add(button);

i
++;
}
this.btnSetComplete.Location = new Point(this.Width-145, 4);this.Controls.Add(btnSetComplete);
}
}

如果是要保存状态,也交由事件处理

        /// <summary>
        ///完成操作,触发外部对状态的保存/// </summary>
        private void btnSetComplete_Click(objectsender, EventArgs e)
{
if(OnSetCompleteStage != null)
{
OnSetCompleteStage(sender, e);
}
}

2、外部窗体使用自定义控件

创建好用户控件后,在外部窗体使用这个用户控件的时候,我们把它拖到窗体界面里面,如下设计界面效果所示。

在这个窗体里面,初始化控件的事件处理,用来做选择的变化处理和保存状态处理。

            this.stageControl1.OnSelectedStageHandler += (s, e) =>{this.txtStage.SetComboBoxItem(string.Concat(this.stageControl1.CurrentStage));
};
this.stageControl1.OnSetCompleteStage += (s, e) =>{if (!string.IsNullOrEmpty(ID))
{
this.txtStage.SetComboBoxItem(string.Concat(this.stageControl1.CurrentStage));var result = CallerFactory<ISaleChanceService>.Instance.UpdateStage(tempInfo.ID, this.stageControl1.CurrentStage);
ShowMessageAutoHide(result.Success
? "设置成功" : "设置失败");
ProcessDataSaved(
null, null);
}
};

我们在调用窗体使用这个进展阶段的控件的时候,需要给它初始化数据,如下是对字典信息的绑定给它。

        /// <summary>
        ///初始化数据字典/// </summary>
        private voidInitDictItem()
{
//初始化代码 this.txtStatus.BindDictItems("机会状态");this.txtSource.BindDictItems("机会来源");this.txtChanceType.BindDictItems("机会类别");this.txtCompetitiveIndex.BindDictItems("机会竞争指数");this.txtConfidenceIndex.BindDictItems("机会信心指数");this.txtStage.BindDictItems("机会进展阶段");var listItem = DictItemUtil.GetDictByDictType("机会进展阶段");
this.stageControl1.StageList =
listItem;
}

然后在界面显示的时候,调用Init函数即可,如下代码所示。

            //初始化显示控件
            this.stageControl1.Init();

实际项目运行的整体效果如下所示。