.Net Core应用框架Util介绍(五)

摘要:
第一部分简要介绍Angular Ts中Util的封装。本文介绍Angular封装的另一部分,即Html的封装。AngularMaterial是Google以材质设计风格开发的UI组件库<Mat form field><inputmatInputplaceholder=“Favoritefoot”value=“Sushi”></Mat form field>您可以看到AngularMaterial文本框不是输入标记,输入标记嵌套在Mat form field标记中。Angular主张将数据访问与组件分离,这是Angular Material标准组件库遵循的设计理念。低效验证是业务健壮性的基本保证。AngularMaterial表单组件提供基本

上篇简要介绍了Util在Angular Ts方面的封装情况,本文介绍Angular封装的另一个部分,即Html的封装。

标准组件与业务组件

对于管理后台这样的表单系统,你通常会使用Angular Material或Ng-Zorro这样的UI组件库,它们提供了标准化的UI组件

标准组件将Ts封装起来,以特定标签和属性的方式提供使用。

业务组件使用标准组件拼凑页面,并从服务端API获取数据绑定到页面上。

可以看出,标准组件是业务开发的基础,我们必须将标准组件的开发效率提升到极致。

使用标准组件的问题

直接使用原生标准组件有什么问题呢?

复杂的Html结构

现代流行的UI组件库,为了构造美观大气的视觉效果及增强组件的功能特性,一个组件需要组装多个Html元素来表达。

在带来美观视觉体验的同时,也导致了Html结构变得很复杂。

Angular Material是Google以Material设计风格开发的UI组件库。

我们来看一个Angular Material文本框的例子。

<mat-form-field>
    <input matInput placeholder="Favorite food"value="Sushi">
</mat-form-field>

你看到了Angular Material文本框并不是一个input标签,input标签嵌套在mat-form-field标签内。

这看上去并不算复杂,不过它只是最简单的情况,让我们增加两个特性。

<mat-form-field>
    <input matInput placeholder="测试一下"[(ngModel)]="value" >
    <mat-hint>哈哈</mat-hint>
    <button mat-button *ngIf="value"matSuffix mat-icon-button (click)="value=''">
        <mat-icon>close</mat-icon>
    </button>
</mat-form-field>

我们在文本框的下方添加了提示文本,并在文本框右侧加了个按钮,你可以点击这个按钮清空文本框的内容。

.Net Core应用框架Util介绍(五)第1张

你应该观察到Html结构变得稍微复杂了,让我们再添加两个特性。

<mat-form-field>
    <input matInput  #testControl="ngModel"name="test"placeholder="金额"[(ngModel)]="value"required max="10">
    <span matPrefix>$ &nbsp;</span>
    <span matSuffix></span>
    <mat-hint>充值金额</mat-hint>
    <button mat-button *ngIf="value"matSuffix mat-icon-button (click)="value=''">
        <mat-icon>close</mat-icon>
    </button>
    <mat-error *ngIf="testControl.hasError('max') && !testControl.hasError('required')">
        最大金额不能超过10元
    </mat-error>
    <mat-error *ngIf="testControl.hasError('required')">
        这是一个必填项
    </mat-error>
</mat-form-field>

现在在文本框的左侧加了一个美元符号,在文本框右侧添加了后缀“元”,另外添加了必填和最大值验证。

.Net Core应用框架Util介绍(五)第2张

这还只是一个不太复杂的文本框,Html居然这么长。  

组件标签结构成为前端业务开发的第一个关注点

繁琐的数据绑定

如果要绑定一些可选项到下拉列表,一种办法是硬编码。

<mat-form-field>
    <mat-select placeholder="请选一个吧">
        <mat-option value="1">A</mat-option>
        <mat-option value="2">B</mat-option>
        <mat-option value="3">C</mat-option>
    </mat-select>
</mat-form-field> 

这是具有三个选项的下拉列表。

.Net Core应用框架Util介绍(五)第3张

如果我们要绑定56个民族,就需要硬编码56个选项,这确实可行,不过一个下拉框就60几行,占地太广,复制粘贴也不方便。

另外,下拉选项可能是动态的,这些可选值存储在数据库中。

数据绑定大多从服务端获取数据,绑定到组件上。

Angular提倡将数据访问与组件分离,这个设计理念被Angular Material这些标准组件库所遵循。

为了绑定数据,你首先需要发送一个Http请求,从服务端获取Json数据,转换为Ts对象,然后通过Angular提供的循环语法绑定上去。

<mat-form-field>
    <mat-select placeholder="Favorite food">
        <mat-option *ngFor="let food of foods"[value]="food.value">
            {{food.viewValue}}
        </mat-option>
    </mat-select>
</mat-form-field>

Angular Material下拉列表能够分组,它与普通下拉列表的Html结构不同,如果服务端返回的数据格式不太友好,绑定起来将更加困难。

<mat-form-field>
    <mat-select placeholder="Pokemon"[formControl]="pokemonControl">
        <mat-option>-- None --</mat-option>
        <mat-optgroup *ngFor="let group of pokemonGroups"[label]="group.name"
                        [disabled]="group.disabled">
            <mat-option *ngFor="let pokemon of group.pokemon"[value]="pokemon.value">
                {{pokemon.viewValue}}
            </mat-option>
        </mat-optgroup>
    </mat-select>
</mat-form-field>

.Net Core应用框架Util介绍(五)第4张

下拉列表并不是唯一需要数据绑定的组件,还有一些组件也需要,且它们更加复杂,比如树型控件,表格控件,树型表格控件等。

数据绑定成为前端业务开发的第二个关注点

低效的验证

验证是业务健壮性的基本保障,Angular Material表单组件提供了基本的验证方法。

<mat-form-field>
    <input matInput name="test"[(ngModel)]="value"required>
</mat-form-field>

上面演示了设置必填项的方法,它相当简单,只要把required加到input标签上就好了。

遗憾的是,文本框虽然得到了验证,但却没有显示出任何错误提示消息。

通过添加一个mat-error标签,可以显示指定错误提示。

<mat-form-field>
    <input matInput  #control="ngModel"name="test"[(ngModel)]="value"required>
    <mat-error *ngIf="control.hasError('required')">
        这是一个必填项
    </mat-error>
</mat-form-field>

如果组件上有两个验证条件,你需要添加两个mat-error标签。

<mat-form-field>
    <input matInput  #control="ngModel"name="test"[(ngModel)]="value"required max="10">
    <mat-error *ngIf="control.hasError('max') && !control.hasError('required')">
        最大值不能超过10
    </mat-error>
    <mat-error *ngIf="control.hasError('required')">
        这是一个必填项
    </mat-error>
</mat-form-field>

注意,为了让提示消息只在特定验证条件失败时才显示,你需要在mat-error标签上进行验证状态判断。

如果现在组件包含5个验证条件,mat-error和它上面的判断条件将变得相当复杂。

另一方面,客户端脚本验证只是为了提升用户体验,用户可以绕过界面直接请求你的服务端,所以真正的验证必须在服务端完成。

这样一来,验证需要在客户端和服务端编写两次,这造成了双倍的工作量。

当需求发生变动,服务端和客户端的验证很难同步更新,维护变得更加困难。

验证成为前端业务开发的第三个关注点

解决方案

如果开发的时候,既不用关心Html的结构,又不用关注数据怎么绑定,验证还能自动完成,甚至连标签和它上面的属性也不用记忆,这就最理想不过了,该如何实现呢?

用Angular组件包装标准组件

首先我们需要用Angular组件对标准组件进行包装,以方便功能扩展,这个自定义组件称为包装器

  • 封装Html复杂结构

我们把标准组件的Html标签包装起来,以属性的形式提供访问。

<mat-form-field>
    <input matInput placeholder="测试一下"[(ngModel)]="value" >
    <mat-hint>哈哈</mat-hint>
    <button mat-button *ngIf="value"matSuffix mat-icon-button (click)="value=''">
        <mat-icon>close</mat-icon>
    </button>
</mat-form-field>

把上面标签包装后变成这样。

<mat-textbox-wrapperplaceholder="测试一下"[(ngModel)]="value"hint="哈哈"showClearButton="true"></mat-textbox-wrapper>

mat-hint标签现在被转换为hint属性,通过showClearButton属性来控制是否显示清空按钮,大幅提升了组件的易用性。

  • 约定前后端数据格式

不论下拉列表,还是表格,甚至树型控件这些需要数据绑定的组件,都有一定规律可循。

当你一遍又一遍的复制粘贴,仔细观察这个机械乏味的绑定过程,不难抽取出公共元素,形成前后端数据绑定的通用数据格式。

一旦抽取出前后端通用数据格式,你只需将业务数据转换为通用格式,发送到客户端就自动绑定完成。

  • 将数据操作内置到包装器

如果你曾经使用过EasyUi这样的组件库,定会发现它的数据绑定功能十分强大,这是因为它把数据操作内置到了标准组件中。

基于Angular低耦合设计原则,Angular Material标准组件并不会直接请求服务端,任何数据绑定工作都需要你手工完成,不过我们可以将数据操作内置到包装器。

一旦封装完成,数据绑定变得非常简单,比如设置一个url属性即可,服务端返回约定的数据格式。

<mat-select-wrapper url="/api/test"></mat-select-wrapper>

用TagHelper封装Angular组件

Angular包装器组件,大幅简化了标准组件的使用,但它提供的依然是Html,而自定义Html标签和属性没有什么提示,这意味着你如果记不住这些API,就需要随时欣赏API文档。

TagHelper终于闪亮登场。

  • 强类型代码提示和编译时检查

一旦把Html标签封装成TagHelper,就可以跟API文档拜拜了,把代码提示点出来,慢慢选,只要你知道该组件确实有这功能,哪怕印象有点模糊也没关系。

Html标签和属性的拼写错误也将与你无缘,VS大哥会为你把关,代码健壮性将大幅提升。

.Net Core应用框架Util介绍(五)第5张

  • Lambda表达式元数据解析

很多人已经认识到HtmlHelper或TagHelper的好处是强类型提示,不过这个认识还很肤浅。

TagHelper真正的威力来自Lambda表达式元数据解析,它提供了一个统一的抽象方式,自动设置表单组件的常规属性、验证,甚至数据绑定。

对于Angular Material表单组件,通常需要设置以下常规属性:

    • 控件名称 name
    • 占位文本 placeholder
    • 双向绑定 ngModel

常规验证:

    • 必填项验证 required
    • Email验证 email
    • 最小长度验证 minlength
    • 最大长度验证 maxlength
    • 最小值验证 min
    • 最大值验证 max
    • 正则验证 pattern

几乎所有表单组件都需要设置这三个常规属性,而文本框更需要进行多种验证,虽然这些操作并不复杂,但由于一个表单界面包含很多组件,每个组件都要挨个设置,既浪费时间又枯燥乏味。

如果能够自动化设置这些常规属性和验证属性,虽然从单个组件看并不起眼,但从整个项目的角度,能大幅提升生产力。

Lambda表达式元数据解析,通过读取C#属性的类型信息以及相关的特性,能够自动化设置三大常规属性,以及对文本框实施多种验证,还解决了客户端与服务端验证无法同步的难题。

一旦用上Lambda表达式,界面标签将变得干净整洁,你的关注点将迅速转移到业务上。

.Net Core应用框架Util介绍(五)第6张

上面的TagHelper标签生成的结果Html如下。

<mat-textbox-wrapper name="code"placeholder="应用程序编码"requiredMessage="应用程序编码不能为空"[(model)]="model&&model.code"[maxLength]="60"[required]="true"></mat-textbox-wrapper>

for指向了ApplicationDto对象的Code属性,下面是Code属性的定义。

.Net Core应用框架Util介绍(五)第7张

从Code属性定义可以解析出该组件需要设置的常规属性和验证属性。

.Net Core应用框架Util介绍(五)第8张

上面演示了文本框组件,对于单选框,多选框,下拉框等表单组件,都可以使用相同的方式,一个for属性,基础工作已经完成。

封装的弊端

看了前面的解决方案,你知道经过几层高强度封装后,组件将变得简单易用,不过在将这些方法应用到你的项目之前,你需要对这些方法有更深的了解。

任何事物都有其两面性,所谓此消彼长,在组件变得更加简单易用的同时,它的灵活性也在降低

包装器组件将Html结构封装起来,这会导致组件不再支持模板化,如果某个功能在你的包装器中未实现,那么不能通过在包装器标签内嵌套HTML的方式组合出新的功能。

封装包装器组件有相当多的讲究,特别是Angular Material这样的组件库在功能上几乎无法与EasyUi或Ext等企业级UI库相提并论,你必须在易用性和灵活性间进行平衡,对于像表格这样的重量级组件,很难封装到完全满足业务需求,这种情况下,你必须为其保留模板化能力。

另一方面,封装后的傻瓜式TagHelper,很容易把程序员惯坏,开发常用功能风升水起,一碰到超出框架范围的需求就变得束手无策,因为他们从来没有学习过原生的知识。

你团队的主力开发人员必须对原生技术有系统了解。

一旦功能超出框架范围,你必须有能力扩展框架,在必要的时候,直接使用原生Html进行开发,这时候你更能体会到TagHelper与Html混合编程的好处,既提升了常规功能的开发效率,又满足了复杂功能对操作体验的需求。

Util组件介绍

下面简要介绍Util中封装的几个常用组件,它们来自Angular Material或PrimeNg组件库。

文本框

前面已经展示过文本框的用法,除了常规属性设置和验证以外,for指向属性的数据类型会影响生成的文本框类型,比如属性为日期类型,文本框会变成一个日期选择控件。

        /// <summary>
        ///创建时间
        /// </summary>
        [Display( Name = "创建时间")]
        public DateTime? CreationTime { get; set; } 
<util-textbox for="CreationTime"></util-textbox>

生成的Html结果如下。

<mat-datepicker-wrapper name="creationTime"placeholder="创建时间"[(model)]="model&&model.creationTime"></mat-datepicker-wrapper>

.Net Core应用框架Util介绍(五)第9张

下面再演示一下数值类型,添加了最大和最小值验证,并设置前缀文本和后缀图标。当属性为数值类型时,文本框只能输入数字。

        /// <summary>
        ///金额
        /// </summary>
        [Required( ErrorMessage = "必须填写金额")]
        [Range(10,50,ErrorMessage = "有效金额在10到50之间")]
        [Display( Name = "金额")]
        public decimal Money { get; set; }

.Net Core应用框架Util介绍(五)第10张

生成的Html结果如下。

    <mat-textbox-wrapper type="number"name="money"placeholder="金额"[(model)]="model&&model.money"
        startHint="a"endHint="b"prefixText="$"suffixFontAwesomeIcon="fa-apple"
        [required]="true"requiredMessage="必须填写金额"
        [min]="10"[max]="50"minMessage="有效金额在10到50之间"maxMessage="有效金额在10到50之间">
    </mat-textbox-wrapper>

来看看执行效果。

.Net Core应用框架Util介绍(五)第11张

下拉列表

下拉列表的封装,重点在于数据绑定

  • 绑定枚举

下面演示如何把民族枚举绑定到下拉列表。

C#代码如下。

1    /// <summary>
2     ///民族
3     /// </summary>
4     public enumNation {
5         /// <summary>
6         ///汉族        
7         /// </summary>
8         [Description( "汉族")]
9         Hz = 0,
10         /// <summary>
11         ///蒙古族        
12         /// </summary>
13         [Description( "蒙古族")]
14         Mgz = 1,
15         /// <summary>
16         ///回族        
17         /// </summary>
18         [Description( "回族")]
19         HuiZ = 2,
20         /// <summary>
21         ///藏族        
22         /// </summary>
23         [Description( "藏族")]
24         Zz = 3,
25         /// <summary>
26         ///维吾尔族        
27         /// </summary>
28         [Description( "维吾尔族")]
29         Wwez = 4,
30         /// <summary>
31         ///苗族        
32         /// </summary>
33         [Description( "苗族")]
34         Mz = 5,
35         /// <summary>
36         ///彝族        
37         /// </summary>
38         [Description( "彝族")]
39         Yz = 6,
40         /// <summary>
41         ///壮族        
42         /// </summary>
43         [Description( "壮族")]
44         ZhuangZ = 7,
45         /// <summary>
46         ///布依族        
47         /// </summary>
48         [Description( "布依族")]
49         Byz = 8,
50         /// <summary>
51         ///朝鲜族        
52         /// </summary>
53         [Description( "朝鲜族")]
54         Cxz = 9,
55         /// <summary>
56         ///满族        
57         /// </summary>
58         [Description( "满族")]
59         ManZ = 10,
60         /// <summary>
61         ///侗族        
62         /// </summary>
63         [Description( "侗族")]
64         Tz = 11,
65         /// <summary>
66         ///瑶族        
67         /// </summary>
68         [Description( "瑶族")]
69         YaoZ = 12,
70         /// <summary>
71         ///白族        
72         /// </summary>
73         [Description( "白族")]
74         Bz = 13,//baizu
75         /// <summary>
76         ///土家族        
77         /// </summary>
78         [Description( "土家族")]
79         Tjz = 14,
80         /// <summary>
81         ///哈尼族        
82         /// </summary>
83         [Description( "哈尼族")]
84         Hnz = 15,
85         /// <summary>
86         ///哈萨克族        
87         /// </summary>
88         [Description( "哈萨克族")]
89         Hskz = 16,
90         /// <summary>
91         ///傣族        
92         /// </summary>
93         [Description( "傣族")]
94         Dz = 17,
95         /// <summary>
96         ///黎族        
97         /// </summary>
98         [Description( "黎族")]
99         Lz = 18,
100         /// <summary>
101         ///傈僳族        
102         /// </summary>
103         [Description( "傈僳族")]
104         Lsz = 19,
105         /// <summary>
106         ///佤族        
107         /// </summary>
108         [Description( "佤族")]
109         Wz = 20,
110         /// <summary>
111         ///畲族        
112         /// </summary>
113         [Description( "畲族")]
114         Sz = 21,
115         /// <summary>
116         ///高山族        
117         /// </summary>
118         [Description( "高山族")]
119         Gsz = 22,
120         /// <summary>
121         ///拉祜族        
122         /// </summary>
123         [Description( "拉祜族")]
124         Lhz = 23,
125         /// <summary>
126         ///水族        
127         /// </summary>
128         [Description( "水族")]
129         ShuiZ = 24,
130         /// <summary>
131         ///东乡族        
132         /// </summary>
133         [Description( "东乡族")]
134         Dxz = 25,
135         /// <summary>
136         ///纳西族        
137         /// </summary>
138         [Description( "纳西族")]
139         Nxz = 26,
140         /// <summary>
141         ///景颇族        
142         /// </summary>
143         [Description( "景颇族")]
144         Jpz = 27,
145         /// <summary>
146         ///柯尔克孜族        
147         /// </summary>
148         [Description( "柯尔克孜族")]
149         Kekzz = 28,
150         /// <summary>
151         ///土族        
152         /// </summary>
153         [Description( "土族")]
154         TuZ = 29,
155         /// <summary>
156         ///达斡尔族        
157         /// </summary>
158         [Description( "达斡尔族")]
159         Dwez = 30,
160         /// <summary>
161         ///仫佬族        
162         /// </summary>
163         [Description( "仫佬族")]
164         Mlz = 31,
165         /// <summary>
166         ///羌族        
167         /// </summary>
168         [Description( "羌族")]
169         Qz = 32,
170         /// <summary>
171         ///布朗族        
172         /// </summary>
173         [Description( "布朗族")]
174         Blz = 33,
175         /// <summary>
176         ///撒拉族        
177         /// </summary>
178         [Description( "撒拉族")]
179         Slz = 34,
180         /// <summary>
181         ///毛南族        
182         /// </summary>
183         [Description( "毛南族")]
184         Mnz = 35,
185         /// <summary>
186         ///仡佬族        
187         /// </summary>
188         [Description( "仡佬族")]
189         Ylz = 36,
190         /// <summary>
191         ///锡伯族        
192         /// </summary>
193         [Description( "锡伯族")]
194         Xbz = 37,
195         /// <summary>
196         ///阿昌族        
197         /// </summary>
198         [Description( "阿昌族")]
199         Acz = 38,
200         /// <summary>
201         ///普米族        
202         /// </summary>
203         [Description( "普米族")]
204         Pmz = 39,
205         /// <summary>
206         ///塔吉克族        
207         /// </summary>
208         [Description( "塔吉克族")]
209         Tjkz = 40,
210         /// <summary>
211         ///怒族        
212         /// </summary>
213         [Description( "怒族")]
214         Nz = 41,
215         /// <summary>
216         ///乌孜别克族        
217         /// </summary>
218         [Description( "乌孜别克族")]
219         Wzbkz = 42,
220         /// <summary>
221         ///俄罗斯族        
222         /// </summary>
223         [Description( "俄罗斯族")]
224         Elsz = 43,
225         /// <summary>
226         ///鄂温克族        
227         /// </summary>
228         [Description( "鄂温克族")]
229         Ewkz = 44,
230         /// <summary>
231         ///德昂族        
232         /// </summary>
233         [Description( "德昂族")]
234         Daz = 45,
235         /// <summary>
236         ///保安族        
237         /// </summary>
238         [Description( "保安族")]
239         Baz = 46,
240         /// <summary>
241         ///裕固族        
242         /// </summary>
243         [Description( "裕固族")]
244         Ygz = 47,
245         /// <summary>
246         ///京族        
247         /// </summary>
248         [Description( "京族")]
249         Jz = 48,
250         /// <summary>
251         ///塔塔尔族        
252         /// </summary>
253         [Description( "塔塔尔族")]
254         Ttrz = 49,
255         /// <summary>
256         ///独龙族        
257         /// </summary>
258         [Description( "独龙族")]
259         Dlz = 50,
260         /// <summary>
261         ///鄂伦春族        
262         /// </summary>
263         [Description( "鄂伦春族")]
264         Elcz = 51,
265         /// <summary>
266         ///赫哲族        
267         /// </summary>
268         [Description( "赫哲族")]
269         Hzz = 52,
270         /// <summary>
271         ///门巴族        
272         /// </summary>
273         [Description( "门巴族")]
274         Mbz = 53,
275         /// <summary>
276         ///珞巴族        
277         /// </summary>
278         [Description( "珞巴族")]
279         Lbz = 54,
280         /// <summary>
281         ///基诺族        
282         /// </summary>
283         [Description( "基诺族")]
284         Jnz = 55
285     }
民族枚举
        /// <summary>
        ///民族
        /// </summary>
        [Required( ErrorMessage = "必须选择一个民族")]
        [Display( Name = "民族")]
        [DataMember]
        public Nation Nation { get; set; }

TagHelper代码如下。

<util-select for="Nation"></util-select>

生成的Html如下,可以看出,民族可选项被硬编码到Html标签中。

    <mat-select-wrapper name="nation"placeholder="民族"requiredMessage="必须选择一个民族"[(model)]="model&&model.nation"
        [dataSource]="[{'text':'汉族','value':0,'sortId':0},{'text':'蒙古族','value':1,'sortId':1},{'text':'回族','value':2,'sortId':2},{'text':'藏族','value':3,'sortId':3},{'text':'维吾尔族','value':4,'sortId':4},{'text':'苗族','value':5,'sortId':5},{'text':'彝族','value':6,'sortId':6},{'text':'壮族','value':7,'sortId':7},{'text':'布依族','value':8,'sortId':8},{'text':'朝鲜族','value':9,'sortId':9},{'text':'满族','value':10,'sortId':10},{'text':'侗族','value':11,'sortId':11},{'text':'瑶族','value':12,'sortId':12},{'text':'白族','value':13,'sortId':13},{'text':'土家族','value':14,'sortId':14},{'text':'哈尼族','value':15,'sortId':15},{'text':'哈萨克族','value':16,'sortId':16},{'text':'傣族','value':17,'sortId':17},{'text':'黎族','value':18,'sortId':18},{'text':'傈僳族','value':19,'sortId':19},{'text':'佤族','value':20,'sortId':20},{'text':'畲族','value':21,'sortId':21},{'text':'高山族','value':22,'sortId':22},{'text':'拉祜族','value':23,'sortId':23},{'text':'水族','value':24,'sortId':24},{'text':'东乡族','value':25,'sortId':25},{'text':'纳西族','value':26,'sortId':26},{'text':'景颇族','value':27,'sortId':27},{'text':'柯尔克孜族','value':28,'sortId':28},{'text':'土族','value':29,'sortId':29},{'text':'达斡尔族','value':30,'sortId':30},{'text':'仫佬族','value':31,'sortId':31},{'text':'羌族','value':32,'sortId':32},{'text':'布朗族','value':33,'sortId':33},{'text':'撒拉族','value':34,'sortId':34},{'text':'毛南族','value':35,'sortId':35},{'text':'仡佬族','value':36,'sortId':36},{'text':'锡伯族','value':37,'sortId':37},{'text':'阿昌族','value':38,'sortId':38},{'text':'普米族','value':39,'sortId':39},{'text':'塔吉克族','value':40,'sortId':40},{'text':'怒族','value':41,'sortId':41},{'text':'乌孜别克族','value':42,'sortId':42},{'text':'俄罗斯族','value':43,'sortId':43},{'text':'鄂温克族','value':44,'sortId':44},{'text':'德昂族','value':45,'sortId':45},{'text':'保安族','value':46,'sortId':46},{'text':'裕固族','value':47,'sortId':47},{'text':'京族','value':48,'sortId':48},{'text':'塔塔尔族','value':49,'sortId':49},{'text':'独龙族','value':50,'sortId':50},{'text':'鄂伦春族','value':51,'sortId':51},{'text':'赫哲族','value':52,'sortId':52},{'text':'门巴族','value':53,'sortId':53},{'text':'珞巴族','value':54,'sortId':54},{'text':'基诺族','value':55,'sortId':55}]"
        [required]="true">
    </mat-select-wrapper>

执行效果如下。

.Net Core应用框架Util介绍(五)第12张

  • 绑定服务端数据

为了绑定服务端数据,必须约定通用数据格式,对于下拉列表,服务端C#是由Util.Item来完成的。

1 usingSystem;
2 usingNewtonsoft.Json;
3 
4 namespaceUtil {
5     /// <summary>
6     ///列表项
7     /// </summary>
8     public class Item : IComparable<Item>{
9         /// <summary>
10         ///初始化
11         /// </summary>
12         /// <param name="text">文本</param>
13         /// <param name="value"></param>
14         /// <param name="sortId">排序号</param>
15         /// <param name="group"></param>
16         /// <param name="disabled">禁用</param>
17         public Item( string text, object value, int? sortId = null, string group = null, bool? disabled = null) {
18             Text =text;
19             Value =value;
20             SortId =sortId;
21             Group =group;
22             Disabled =disabled;
23 }
24 
25         /// <summary>
26         ///文本
27         /// </summary>
28         [JsonProperty( "text", NullValueHandling =NullValueHandling.Ignore )]
29         public string Text { get; }
30 
31         /// <summary>
32         ///33         /// </summary>
34         [JsonProperty( "value", NullValueHandling =NullValueHandling.Ignore )]
35         public object Value { get; }
36 
37         /// <summary>
38         ///排序号
39         /// </summary>
40         [JsonProperty( "sortId", NullValueHandling =NullValueHandling.Ignore )]
41         public int? SortId { get; }
42 
43         /// <summary>
44         ///45         /// </summary>
46         [JsonProperty( "group", NullValueHandling =NullValueHandling.Ignore )]
47         public string Group { get; }
48 
49         /// <summary>
50         ///禁用
51         /// </summary>
52         [JsonProperty( "disabled", NullValueHandling =NullValueHandling.Ignore )]
53         public bool? Disabled { get; }
54 
55         /// <summary>
56         ///比较
57         /// </summary>
58         /// <param name="other">其它列表项</param>
59         public intCompareTo( Item other ) {
60             return string.Compare( Text, other.Text, StringComparison.CurrentCulture );
61 }
62 }
63 }
Util.Item

客户端Typescript定义了对应的结构。

1 //============== 列表=============================
2 //Copyright 2018 何镇汐
3 //Licensed under the MIT license
4 //================================================
5 import { ISort, sort } from '../core/sort';
6 import { util } from '../index';
7 
8 /**
9 * 列表
10  */
11 export class Select {
12     /**
13 * 初始化列表
14 * @param items 列表项集合
15      */
16 constructor(private items: SelectItem[]) {
17 }
18 
19     /**
20 * 转换为下拉列表项集合
21      */
22 toOptions(): SelectOption[] {
23         return this.getSortedItems().map(value => newSelectOption(value));
24 }
25 
26     /**
27 * 获取已排序的列表项集合
28      */
29 private getSortedItems() {
30         return sort(this.items);
31 }
32 
33     /**
34 * 转换为下拉列表组集合
35      */
36 toGroups(): SelectOptionGroup[] {
37         let result: SelectOptionGroup[] = new Array<SelectOptionGroup>();
38         let groups = util.helper.groupBy(this.getSortedItems(), t =>t.group);
39         groups.forEach((items, key) =>{
40             result.push(new SelectOptionGroup(key, items.map(item => new SelectOption(item)), false));
41 });
42         returnresult;
43 }
44 
45     /**
46 * 是否列表组
47      */
48     isGroup(): boolean{
49         return this.items.every(value => !!value.group);
50 }
51 }
52 
53 /**
54 * 列表项
55  */
56 export class SelectItem implements ISort {
57     /**
58 * 文本
59      */
60 text: string;
61     /**
62 * 值
63      */
64 value;
65     /**
66 * 禁用
67      */
68     disabled?: boolean;
69     /**
70 * 排序号
71      */
72     sortId?: number;
73     /**
74 * 组
75      */
76     group?: string;
77 }
78 
79 /**
80 * 下拉列表项
81  */
82 export class SelectOption {
83     /**
84 * 文本
85      */
86 text: string;
87     /**
88 * 值
89      */
90 value;
91     /**
92 * 禁用
93      */
94     disabled?: boolean;
95 
96     /**
97 * 初始化下拉列表项
98 * @param item 列表项
99      */
100 constructor(item: SelectItem) {
101         this.text =item.text;
102         this.value =item.value;
103         this.disabled =item.disabled;
104 }
105 }
106 
107 /**
108 * 下拉列表组
109  */
110 export class SelectOptionGroup {
111     /**
112 * 初始化下拉列表组
113 * @param text 文本
114 * @param value 值
115 * @param disabled 禁用
116      */
117     constructor(public text: string, public value: SelectOption[], public disabled?: boolean) {
118 }
119 }
SelectItem

下面来演示一下用法。

先把Nation属性的类型改成int。

        /// <summary>
        ///民族
        /// </summary>
        [Required( ErrorMessage = "必须选择一个民族")]
        [Display( Name = "民族")]
        [DataMember]
        public int Nation { get; set; }

在WebApi控制器中,添加一个方法,用来获取民族枚举可选项。

通过Util.Helpers.Enum.GetItems方法可以提取出枚举项列表,返回值为List<Item>,这正是我们约定的标准格式,如果返回的是业务类型列表,应转换为List<Item>。

Success方法用来将List<Item>转换为前后端约定的标准结果类型Result。

        /// <summary>
        ///获取民族可选项列表
        /// </summary>
        [HttpGet( "nationItems")]
        publicIActionResult GetNationItems() {
            List<Item> items = Util.Helpers.Enum.GetItems<Util.Biz.Enums.Nation>();
            returnSuccess( items );
        }

再来看TagHelper标签,for属性承包了常规的机械工作,你将注意力集中在业务上,通过手工设置url属性来加载远程数据。

<util-select for="Nation"url="/api/test/nationItems"></util-select>

效果跟直接绑定枚举一样,不过生成的Html简单很多。

<mat-select-wrapper name="nation"placeholder="民族"requiredMessage="必须选择一个民族"url="/api/application/nationItems"[(model)]="model&&model.nation"[required]="true"></mat-select-wrapper>

上面演示的下拉列表并未分组,我们来改造一下,让它以分组显示。

        /// <summary>
        ///获取民族可选项列表
        /// </summary>
        [HttpGet( "nationItems")]
        publicIActionResult GetNationItems() {
            var result = Util.Helpers.Enum.GetItems<Util.Biz.Enums.Nation>()
                .GroupBy( t => Util.Helpers.String.PinYin( t.Text.Substring( 0,1) ) )
                .SelectMany( t => t.ToList().Select( item => newItem( item.Text, item.Value, item.SortId, t.Key ) ) );
            returnSuccess( result );
        }

Util包含大量有用的Helper,Util.Helpers.String.PinYin方法能够将汉字转换为拼音首字母缩写,使用GroupBy方法将民族拼音首字母进行分组,并转换为Item标准格式。

执行效果如下。

.Net Core应用框架Util介绍(五)第13张

TagHelper没有任何变化,Angular Material下拉列表是否分组,其原生Html格式完全不同,但封装以后,你根本感觉不到它们的区别,你不需要编写任何一行Ts代码,就完成了分组下拉列表的绑定,你应该已经体会到封装的强大之处。

单选按钮

单选按钮和下拉列表类似,下面演示一下枚举绑定。

C#代码如下。

    /// <summary>
    ///性别
    /// </summary>
    public enumGender {
        /// <summary>
        ////// </summary>        
        [Description( "女士")]
        Female = 1,
        /// <summary>
        ////// </summary>
        [Description( "先生")]
        Male = 2
    }
        /// <summary>
        ///性别
        /// </summary>
        [Display( Name = "性别")]
        public Gender Gender { get; set; }

TagHelper标签如下。

<util-radio for="Gender"></util-radio>

生成的Html标签如下。

    <mat-radio-wrapper label="性别"name="gender"[(model)]="model&&model.gender"[dataSource]="[{'text':'女士','value':1,'sortId':1},{'text':'先生','value':2,'sortId':2}]">
    </mat-radio-wrapper>

执行效果如下。

.Net Core应用框架Util介绍(五)第14张

复选框

复选框用于操作布尔类型。

C#代码如下。

        /// <summary>
        ///启用
        /// </summary>
        [Display( Name = "启用")]
        [DataMember]
        public bool? Enabled { get; set; }

TagHelper标签如下。

<util-checkbox for="Enabled"></util-checkbox>

生成的Html标签如下。

<mat-checkbox name="gender"[(ngModel)]="model&&model.gender">性别</mat-checkbox>

执行效果如下。

.Net Core应用框架Util介绍(五)第15张

滑动开关

滑动开关与复选框功能相同,但长像更具现代化气质。

TagHelper标签如下。

<util-slide-toggle for="Enabled"></util-slide-toggle>

生成的Html标签如下。

<mat-slide-toggle name="enabled"[(ngModel)]="model&&model.enabled">启用</mat-slide-toggle>

执行效果如下。

.Net Core应用框架Util介绍(五)第16张

表格

Angular Material表格提供了一套模板化机制,你需要任何功能,往表格标签中添加元素就好了。

像序号,多选,分页等常规功能都没有内置到Angular Material表格中,Angular Material官网以Demo的形式提供了参考样例,如果你直接使用它来进行业务开发,将导致十分低效的开发效率。

Util将自动生成序号,多选,分页,排序等常见功能以及数据绑定能力封装到表格包装器组件中,同时,Util保留了Angular Material表格的模板化能力,你依然可以通过往表格标签中添加元素的方式扩展功能。

由于大多表格都需要分页,约定的后台数据格式由PagerList承载,Ts也定义了类似的分页列表对象。

下面演示一个简单的表格示例。

服务端已经封装了通用的查询方法,留待下篇介绍。

先看看TagHelper代码。

1 <util-table id="tableApplication"query-param="queryParam"base-url="application"
2 sort="CreationTime"sort-direction="Desc"max-height="500">
3     <util-table-column type="Checkbox"></util-table-column>
4     <util-table-column type="LineNumber"></util-table-column>
5     <util-table-column for="Code"sort="true"></util-table-column>
6     <util-table-column for="Name"sort="true"></util-table-column>
7     <util-table-column for="Enabled"sort="true"></util-table-column>
8     <util-table-column for="RegisterEnabled"sort="true"></util-table-column>
9     <util-table-column for="CreationTime"sort="true"></util-table-column>
10     <util-table-column title="操作"column="operation">
11         <util-table-cell>
12             <util-a styles="Icon"tooltip="编辑"bind-link="['update',row.id]">
13                 <util-icon material-icon="Edit"></util-icon>
14             </util-a>
15             <util-button styles="Icon"menu-id="menu">
16                 <util-icon material-icon="More_Vert"></util-icon>
17                 <util-menu id="menu">
18                     <util-menu-item label="删除"material-icon="Delete"on-click="delete(row.id)"></util-menu-item>
19                     <util-menu-item label="查看详细"material-icon="Visibility"bind-link="['detail',row.id]"></util-menu-item>
20                 </util-menu>
21             </util-button>
22         </util-table-cell>
23     </util-table-column>
24 </util-table>

生成的Html如下。

1 <mat-table-wrapper #tableApplication=""baseUrl="application"key="application"maxHeight="500"[(queryParam)]="queryParam"><mat-table matSort=""matSortActive="CreationTime"matSortDirection="desc"matSortDisableClear=""[dataSource]="tableApplication.dataSource"[style.max-height]="tableApplication.maxHeight?tableApplication.maxHeight+'px':null"[style.min-height]="tableApplication.minHeight?tableApplication.minHeight+'px':null">
2     <ng-container matColumnDef="selectCheckbox"><mat-header-cell *matHeaderCellDef=""><mat-checkbox (change)="$event?tableApplication.masterToggle():null"[checked]="tableApplication.isMasterChecked()"[disabled]="!tableApplication.dataSource.data.length"[indeterminate]="tableApplication.isMasterIndeterminate()"></mat-checkbox></mat-header-cell><mat-cell *matCellDef="let row"><mat-checkbox (change)="$event?tableApplication.checkedSelection.toggle(row):null"(click)="$event.stopPropagation()"[checked]="tableApplication.checkedSelection.isSelected(row)"></mat-checkbox></mat-cell></ng-container>
3     <ng-container matColumnDef="lineNumber"><mat-header-cell *matHeaderCellDef="">ID</mat-header-cell><mat-cell *matCellDef="let row">{{ row.lineNumber }}</mat-cell></ng-container>
4     <ng-container matColumnDef="code"><mat-header-cell *matHeaderCellDef=""mat-sort-header="">应用程序编码</mat-header-cell><mat-cell *matCellDef="let row">{{ row.code }}</mat-cell></ng-container>
5     <ng-container matColumnDef="name"><mat-header-cell *matHeaderCellDef=""mat-sort-header="">应用程序名称</mat-header-cell><mat-cell *matCellDef="let row">{{ row.name }}</mat-cell></ng-container>
6     <ng-container matColumnDef="enabled"><mat-header-cell *matHeaderCellDef=""mat-sort-header="">启用</mat-header-cell><mat-cell *matCellDef="let row"><mat-icon *ngIf="row.enabled">check</mat-icon><mat-icon *ngIf="!row.enabled">clear</mat-icon></mat-cell></ng-container>
7     <ng-container matColumnDef="registerEnabled"><mat-header-cell *matHeaderCellDef=""mat-sort-header="">启用注册</mat-header-cell><mat-cell *matCellDef="let row"><mat-icon *ngIf="row.registerEnabled">check</mat-icon><mat-icon *ngIf="!row.registerEnabled">clear</mat-icon></mat-cell></ng-container>
8     <ng-container matColumnDef="creationTime"><mat-header-cell *matHeaderCellDef=""mat-sort-header="">创建时间</mat-header-cell><mat-cell *matCellDef="let row">{{ row.creationTime | date:"yyyy-MM-dd" }}</mat-cell></ng-container>
9     <ng-container matColumnDef="operation"><mat-header-cell *matHeaderCellDef="">操作</mat-header-cell>
10         <mat-cell *matCellDef="let row">
11             <a mat-icon-button=""matTooltip="编辑"[routerLink]="['update',row.id]">
12                 <mat-icon>edit</mat-icon>
13             </a>
14             <button mat-icon-button=""type="button"[matMenuTriggerFor]="menu">
15                 <mat-icon>more_vert</mat-icon>
16                 <mat-menu #menu="matMenu"><ng-template matMenuContent="">
17                     <button (click)="delete(row.id)"mat-menu-item=""><mat-icon>delete</mat-icon><span>删除</span></button>
18                     <button mat-menu-item=""[routerLink]="['detail',row.id]"><mat-icon>visibility</mat-icon><span>查看详细</span></button>
19                 </ng-template></mat-menu>
20             </button>
21         </mat-cell>
22     </ng-container>
23 <mat-header-row *matHeaderRowDef="['selectCheckbox','lineNumber','code','name','enabled','registerEnabled','creationTime','operation'];sticky:true"></mat-header-row><mat-row (click)="tableApplication.selectedSelection.select(row)"*matRowDef="let row;columns:['selectCheckbox','lineNumber','code','name','enabled','registerEnabled','creationTime','operation']"class="mat-row-hover"[class.selected]="tableApplication.selectedSelection.isSelected(row)"></mat-row></mat-table></mat-table-wrapper>

可以看见,Html比TagHelper代码要复杂得多,这还是封装过后的情况,如果完全没有封装,折腾一个表格将会耗费你大量精力,且Bug遍地,难以维护。

Ts代码几乎看不见,你只需设置base-url属性,数据绑定就完成了。

base-url是一个基地址,根据约定创建服务端请求地址/api/baseUrl,如果你的请求地址不同,可以改为设置url属性。

执行效果如下。

.Net Core应用框架Util介绍(五)第17张

树型表格

树型层次关系是业务常见操作之一。

Util Angular Material的封装主要是在Angular Material 5.x之前完成的,Angular Material 6.x才提供了树型控件,所以Util尚未封装树型控件,不过为了解决编辑树型层次困难的局面,我从PrimeNg组件库Copy了一个树型表格过来。

PrimeNg是另一个开源的Angular组件库,它的树型表格功能非常弱,我花了数天时间来修改它的源码,以满足我的基本需求。

由于树型包含同步加载,异步加载,上移,下移,单选,多选等操作,封装树型表格比普通表格要复杂得多。

服务端提供了PrimeTreeControllerBasePrimeTreeNode等对象类型来实现与客户端通信,不过它们都还相当具体化,待后续封装Ng-Zorro树型组件时再来重构。

一旦封装完成,它用起来就跟Angular Material表格几乎没什么区别。

来看个示例。

TagHelper代码如下。

<util-tree-table id="treeTable_role"base-url="role"query-param="queryParam"selection-mode="Multiple"key="treeTable_role" >
    <util-tree-table-column for="Name"></util-tree-table-column>
    <util-tree-table-column for="Code"></util-tree-table-column>
    <util-tree-table-column for="Enabled"></util-tree-table-column>
    <util-tree-table-column for="SortId"></util-tree-table-column>
    <util-tree-table-column title="操作">
        <util-a styles="Icon"tooltip="添加下级角色"link="create"query-params="{id:row.data.id}">
            <util-icon material-icon="Add"></util-icon>
        </util-a>
        <util-button id="btnMoveUp"styles="Icon"tooltip="上移"ng-if="!isFirst(row)"on-click="moveUp(row,btnMoveUp,$event)">
            <util-icon material-icon="Arrow_Upward"></util-icon>
        </util-button>
        <util-button id="btnMoveDown"styles="Icon"tooltip="下移"ng-if="!isLast(row)"on-click="moveDown(row, btnMoveDown,$event)">
            <util-icon material-icon="Arrow_Downward"></util-icon>
        </util-button>
        <util-button styles="Icon"menu-id="menu"on-click="selectRow(row,$event)">
            <util-icon material-icon="More_Vert"></util-icon>
            <util-menu id="menu">
                <util-menu-item label="编辑"material-icon="Edit"bind-link="['update',row.data.id]"></util-menu-item>
                <util-menu-item label="禁用"material-icon="Lock"on-click="disable(row)"></util-menu-item>
                <util-menu-item label="启用"material-icon="Lock_Open"on-click="enable(row)"></util-menu-item>
                <util-menu-item label="删除"material-icon="Delete"on-click="delete(row)"></util-menu-item>
                <util-menu-item label="详细"material-icon="Visibility"bind-link="['detail',row.data.id]"></util-menu-item>
            </util-menu>
        </util-button>
    </util-tree-table-column>
</util-tree-table>

生成的Html如下。

<p-tree-table #treeTable_role=""baseUrl="role"key="treeTable_role"selectionMode="checkbox"[(queryParam)]="queryParam">
    <p-column field="name"header="角色名称"></p-column>
    <p-column field="code"header="角色编码"></p-column>
    <p-column field="enabled"header="启用"><ng-template let-first="first"let-i="index"let-last="last"let-row="rowData"><mat-icon *ngIf="row.data.enabled">check</mat-icon><mat-icon *ngIf="!row.data.enabled">clear</mat-icon></ng-template></p-column>
    <p-column field="sortId"header="排序号"></p-column>
    <p-column header="操作"><ng-template let-first="first"let-i="index"let-last="last"let-row="rowData">
        <a mat-icon-button=""matTooltip="添加下级角色"routerLink="create"[queryParams]="{id:row.data.id}">
            <mat-icon>add</mat-icon>
        </a>
        <mat-button-wrapper #btnMoveUp=""(onClick)="moveUp(row,btnMoveUp,$event)"*ngIf="!isFirst(row)"style="mat-icon-button"tooltip="上移"><ng-template>
            <mat-icon>arrow_upward</mat-icon>
        </ng-template></mat-button-wrapper>
        <mat-button-wrapper #btnMoveDown=""(onClick)="moveDown(row, btnMoveDown,$event)"*ngIf="!isLast(row)"style="mat-icon-button"tooltip="下移"><ng-template>
            <mat-icon>arrow_downward</mat-icon>
        </ng-template></mat-button-wrapper>
        <button (click)="selectRow(row,$event)"mat-icon-button=""type="button"[matMenuTriggerFor]="menu">
            <mat-icon>more_vert</mat-icon>
            <mat-menu #menu="matMenu"><ng-template matMenuContent="">
                <button mat-menu-item=""[routerLink]="['update',row.data.id]"><mat-icon>edit</mat-icon><span>编辑</span></button>
                <button (click)="disable(row)"mat-menu-item=""><mat-icon>lock</mat-icon><span>禁用</span></button>
                <button (click)="enable(row)"mat-menu-item=""><mat-icon>lock_open</mat-icon><span>启用</span></button>
                <button (click)="delete(row)"mat-menu-item=""><mat-icon>delete</mat-icon><span>删除</span></button>
                <button mat-menu-item=""[routerLink]="['detail',row.data.id]"><mat-icon>visibility</mat-icon><span>详细</span></button>
            </ng-template></mat-menu>
        </button>
    </ng-template></p-column>
</p-tree-table>

看上去Html比TagHelper没有复杂多少,那是因为已经将功能内置到树型表格组件内部,不得不承认,有时候修改源码比在外围扩展要省很多力气。

执行效果如下。

.Net Core应用框架Util介绍(五)第18张

在完成了异步加载,多选,上移,下移,搜索,删除行,刷新,分页等一系列功能后,一行Ts都没有,是否感觉到很清爽呢。

其它组件

Util还封装了颜色拾取器,菜单,侧边栏等组件,限于篇幅,就不一一介绍。

小结

本文简要介绍了Angular标准组件的封装手法,它能够大幅提升业务开发的生产力,同时也提醒你,必须系统学习原生技术,否则碰上稍微复杂点的问题就无法解决。

本文更多的是介绍封装思路,而封装思想与具体UI技术无关,一旦你了解了封装背后的动机和技巧,不论Angular还是Vue,或者Android组件,甚至小程序都可以通过封装来提升开发效率。

未完待续,C#服务端CRUD的封装将在下篇介绍。

写文需要动力,请大家多多支持,点下推荐,Github点下星星

Util应用框架交流一群: 24791014(已满)

Util应用框架交流二群: 184097033

Util应用框架地址:https://github.com/dotnetcore/util

免责声明:文章转载自《.Net Core应用框架Util介绍(五)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇SQL Server中如何生成GUID C#.NET中如何生成和使用GUID手把手教你搭建织女星开发板RISC-V开发环境下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

使用 dumi 打包 React 组件库并生成文档站点

对于前端团队来说,公共组件库是必须的,紧接着就是完善组件库的文档 社区里关于快速生成文档的工具有很多,如 StoryBook、Docz、Gatsby 在调研了几种文档工具之后,最终我选择了 umi 家族的另一个成员:dumi 因为它集成了 docz,以及打包工具father-build,同时支持创建自己的Markdown 组件 当然最重要的是,我的项目是基...

yum命令Header V3 RSA/SHA1 Signature, key ID c105b9de: NOKEY

yum命令Header V3 RSA/SHA1 Signature, key ID c105b9de: NOKEY 博客分类:linux 三种解决方案我采取第三种方案解决的第一种:linux 使用rpm安装软件时,遇到"warning: rpmts_HdrFromFdno: Header V3 RSA/SHA256 Signature, key ID...

前端框架Vue自学之Vue组件化开发(三)

终极目标:掌握和使用Vue(全家桶:Core+Vue-router+Vuex) 本博客目的:记录Vue学习的进度和心得(Vue组件化开发) 内容:通过官网说明,掌握Vue组件化开发。 正文: Vue组件化开发 一、认识组件化 1、什么是组件化? 任何一个人处理信息的逻辑能力都是有限的,所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。但...

文本超过长度后隐藏,显示省略号

table{   table-layout:fixed; } td{    white-space:nowrap;   overflow:hidden;   text-overflow:ellipsis; }   在table和td分别新增这几个样式就可以了,效果如下: 本方法用于解决表格单元格内容过多时的美观问题,主要涉及到4句CSS样式: 1....

我们是如何做好前端工程化和静态资源管理

随着互联网的发展,我们的业务也日益变得更加复杂且多样化起来,前端工程师也不再只是做简单的页面开发这么简单,我们需要面对的十分复杂的系统性问题,例如,业务愈来愈复杂,我们要如何清晰地梳理;团队人员愈来愈多,我们要如何更好地进行团队协作;功能愈来愈多,我们要如何保证页面的性能不至于下降,等等。所有的这些都可以归结为如何提升开发体验和性能问题。 提升开发体验...

web前端学习笔记(CSS固定宽度布局)

一、单列布局:       这是最简单的一种布局方式,之所以给出该示例,还是为了保证本篇博客的完整性。 <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>固定宽度布局</title> <style type="text/css"&g...