简介:《Web界面设计规范(Web Form的UI设计)》是一份面向Web前端、UI设计及表单类项目开发者的界面规范参考文档,适合需要统一页面风格、降低协作沟通成本的团队使用。压缩包内为1个doc文件,约199KB,内容围绕Web Form的UI设计展开,涵盖界面设计目的与适用范围、文件与控件命名、控件外观,以及字体、颜色、边距、尺寸单位、表格排版和网站目录结构等细项。文档以规则条目和约束建议组织,便于读者快速对照检查现有界面,也可作为团队制定设计规范、评审页面稿和沉淀组件样式标准的底稿。已有2518人学习,适合初中级开发者查漏补缺,也能为项目负责人统一多页面视觉与交互细节提供可复用的参考框架。
1. 一份 2005 年的 WebForm 规范,今天翻出来还能治什么病
手上这份《Web 界面设计规范》,2005 年 5 月底定稿,6 月初补了一版控件外观和表格排版,一共 11 页,服务对象是 .NET Web Form 时代的 aspx / ascx 页面。它管的事情很窄:文件怎么命名、控件前缀叫什么、Label 和 TextBox 默认多宽、表格怎么缩进、正文字体用几号。但恰恰是这种窄,让它在今天依然能直接套用——后台管理系统的界面设计一致性,八成坏在“没人规定过默认值”,而不是坏在审美水平。这份规范本质上是一张缺省值清单:24 个控件前缀、一套宽高默认值、9pt 基准字号、3px 最小边距。下面按“怎么照着落地”的顺序拆开讲,命名、外观、表格、坑,一层层往下走。
2. 文件与控件命名:24 个前缀怎么真正落到工程里
命名规范是这份文档里最容易被人翻过去、也最容易在半年后救命的章节。首页、列表页、编辑页,一个项目几十个页面,如果控件 ID 全是TextBox1、Button2,重构的时候没人敢动——你根本不知道哪个 ID 绑了校验、哪个 ID 被前端脚本取过值。前缀规范的价值不是好看,是让“看到 ID 就知道类型和用途”变成默认行为。
2.1 扩展名与文件名:先把 aspx 和 ascx 的边界划清
文档第三节只有一行有效信息,但这一行把项目结构定死了:.aspx是 Web 页面,.ascx是用户自定义控件,后者的前缀统一用wuc。这条规则在 2005 年是常识,放到今天很多还在维护的老系统里依然是硬约束——页面和控件的区别不是“哪个能直接访问”,而是“哪个有<%@ Page %>指令、哪个靠宿主页面加载”。
顺着这条规则往下补,我在实际项目里会额外压三条:
- 文件名全部小写英文 + 数字 + 下划线,不带汉字、空格、特殊字符。文件名带汉字在 IIS 上能跑,但一进版本库、一上部署脚本、一走日志分析就开始出玄学问题。
- 文件名表达“业务模块 + 动作”,比如
ar_invoice_list.aspx、ar_invoice_edit.aspx,而不是page1.aspx、newpage.aspx。这一条文档里没写,但同一份规范里“目录按栏目划分”的思路已经暗含了这个要求。 - 用户控件文件名带
wuc_前缀,跟页面文件在目录里一眼区分开。规范只规定扩展名和前缀符号,落地时我一般会把前缀直接写进文件名,比如wuc_grid_pager.ascx。
这里有个取舍要说清楚:文件名前缀和控件 ID 前缀是两套东西,别混。文件名解决“找文件”,控件前缀解决“找 DOM 元素和服务端变量”,混在一起以后做全局搜索会很痛苦。
2.2 控件前缀表:按“类型 → 前缀 → 实例”逐条对照
文档第四节是整份规范里信息密度最高的一页,给了一张完整的控件前缀表。我按原表整理成下面这样,加了“典型用途”一列,方便对照自己项目里的控件做映射:
| 控件类型 | 前缀 | 例子 | 典型用途 |
|---|---|---|---|
| Label | lbl | lblTip | 静态提示、字段名 |
| TextBox | txt | txtName | 单行输入 |
| Button | btn | btnOK | 提交、取消 |
| LinkButton | lbtn | lbtnDelete | 行内操作 |
| ImageButton | ibtn | ibtnSearch | 图标按钮 |
| HyperLink | hlk | hlkDetail | 跳转链接 |
| DropDownList | ddl | ddlStatus | 枚举选择 |
| ListBox | lst | lstSelected | 多选列表 |
| DataGrid | grd | grdInvoice | 表格数据 |
| DataList | dlst | dlstItems | 重复布局 |
| Repeater | rep | repRows | 轻量重复渲染 |
| CheckBox | chk | chkEnabled | 单个开关 |
| CheckBoxList | chklst | chklstRoles | 多选组 |
| RadioButtonList | rdolst | rdolstType | 单选组 |
| RadioButton | rdo | rdoDefault | 单个单选 |
| Image | img | imgLogo | 图片展示 |
| Panel | pan | panFilter | 容器分组 |
| PlaceHolder | plh | plhContent | 占位容器 |
| Calendar | cld | cldStart | 日期选择 |
| AdRotator | adr | adrBanner | 轮播广告位 |
| Table | tbl | tblHeader | 服务端表格 |
| RequiredFieldValidator | rfv | rfvName | 必填校验 |
| CompareValidator | cpv | cpvPwd | 比较校验 |
| RangeValidator | rgv | rgvAmount | 区间校验 |
| RegularExpressionValidator | rev | revEmail | 正则校验 |
| CustomValidator | cstv | cstvCode | 自定义校验 |
| ValidationSummary | vls | vlsForm | 校验汇总 |
| Xml | xml | xmlConfig | XML 数据源 |
| Literal | ltl | ltlHtml | 原样输出 HTML |
| CrystalReportViewer | crv | crvReport | 报表展示 |
这张表的关键不在于前缀字母选得好不好,而在于“一个控件类型只对应一个前缀”。前端取值、后端找控件、脚本里document.getElementById,全靠这层映射。一旦有人写了个btnName的 TextBox,半年后接手的人会盯着btnName.Text怀疑人生。
2.3 把前缀表变成能跑的检查
规范写在文档里,靠人记一定会走样。我的做法是用一个几十行的脚本扫一遍 aspx,把不符合前缀的 ID 全列出来,提交前跑一次。
import re from pathlib import Path # 来自规范第四节的 控件标签 -> 规定前缀 PREFIX = { "asp:Label": "lbl", "asp:TextBox": "txt", "asp:Button": "btn", "asp:LinkButton": "lbtn", "asp:ImageButton": "ibtn", "asp:HyperLink": "hlk", "asp:DropDownList": "ddl", "asp:ListBox": "lst", "asp:DataGrid": "grd", "asp:DataList": "dlst", "asp:Repeater": "rep", "asp:CheckBox": "chk", "asp:CheckBoxList": "chklst", "asp:RadioButtonList": "rdolst", "asp:RadioButton": "rdo", "asp:Image": "img", "asp:Panel": "pan", "asp:PlaceHolder": "plh", "asp:Calendar": "cld", "asp:AdRotator": "adr", "asp:Table": "tbl", "asp:RequiredFieldValidator": "rfv", "asp:CompareValidator": "cpv", "asp:RangeValidator": "rgv", "asp:RegularExpressionValidator": "rev", "asp:CustomValidator": "cstv", "asp:ValidationSummary": "vls", } # 只匹配 “标签和 id 出现在同一行” 的写法,够用且误报少 TAG_RE = re.compile(r"<(?P<tag>asp:[A-Za-z]+)\b[^>]*?\bid=\"(?P<id>[^\"]+)\"", re.I) def check(path: Path) -> list[str]: bad = [] text = path.read_text(encoding="utf-8", errors="ignore") for lineno, line in enumerate(text.splitlines(), 1): for m in TAG_RE.finditer(line): tag, cid = m.group("tag"), m.group("id") want = PREFIX.get(tag) if want and not cid.startswith(want): bad.append(f"{path.name}:{lineno} <{tag}> id={cid} 应以 {want} 开头") return bad if __name__ == "__main__": for p in Path(".").rglob("*.aspx"): for msg in check(p): print(msg)逻辑上分三步:先建立标签与前缀的映射,再用正则从每一行里抽出asp:xxx标签和它的id,最后比较前缀。几个参数要按自己项目改——PREFIX表就是规范第四节,少登一个控件类型就少一条检查;TAG_RE里限定了“同一行”,是因为 Web Form 的标签如果跨行写(<asp:TextBox换行再写id=),这个正则匹配不到,会静默漏检,如果你项目里流行跨行写标签,得先把text.splitlines()换成整文件匹配再回溯行号。
还有两个边界:正则不认识服务器端注释<%-- --%>,被注释掉的控件也会被报出来;runat="server"之外的纯 HTML 元素不在检查范围。前一个问题我一般手动忽略,后一个正好符合规范——静态内容本来就不该用服务端控件。
3. 控件外观与视觉基准:9pt 字体、3px 边距和一套宽高默认值
到第五节,规范开始处理真正影响观感的东西:每个控件该多宽、多高。原文档给了一个有点绕但很严谨的写法{50px|文本宽度},意思是这个参数可以取50px,也可以取“随文本自适应”。这种写法看着像模板占位符,实际上是给“不确定宽度”留了合法出口——否则开发者会随便拍一个数字。
3.1 字体与颜色为什么全部收进 CSS
规范第六节开门见山:所有 Web 界面基准字号一律9pt,字体序列Verdana, Arial, Helvetica, Sans-Serif,并且“所有字体设定应在 CSS 中完成”。颜色同理,“以清爽简洁为准”,也全部落 CSS。
这条在今天听起来像废话,但放进 2005 年的 Web Form 语境里是有针对性的。那时候常见写法是在控件上直接挂Font-Size="9pt",一个页面几十个控件各挂一遍,改基准字号要改几十处,漏一处就出现 10pt 和 9pt 混排。收进 CSS 之后,基准字号只有一个来源。
body { font-size: 9pt; font-family: Verdana, Arial, Helvetica, Sans-Serif; line-height: 150%; } p { text-indent: 2em; /* 首行缩进走样式,不用 */ margin: 0 0 6px 0; } .field { margin: 3px; /* 规范规定的最小边距 3px */ }逻辑说明:font-size: 9pt是所有文字的基准,子元素不显式声明就继承;line-height: 150%对应规范第七节建议的 120%/150% 两档行距,正文长文本用 150%,密集表格用 120%;text-indent: 2em对应规范里“禁止用 或全角空格做首行缩进”的硬要求。
参数上要注意9pt这个单位本身。pt 是物理单位,浏览器会按 96dpi 换算成 12px,视觉上偏小但清晰;如果团队现在统一用 px,别直接改成12px就完事——9pt在缩放和打印时的表现跟12px不完全一样,迁移时最好整个项目一起换,不要一半 pt 一半 px。
3.2 pt 给文字,px 给对象:尺寸单位的分工
6.4 节那条规则很短但很管用:所有字体尺寸一律用pt,所有对象和线条的尺寸一律用px。这条的分界线是“这段度量描述的是文字还是盒子”。字体用 pt,边框、宽高、间距用 px。
我见过最常见的违规是把表格宽度写成width="90%"又同时给<td>写死width="600px",父子互相打架,最后到底谁生效取决于浏览器——这就是规范 6.5.2 节反复强调“不出现多于一个控制同一单元格大小的 width/height”的现实来源。
3.3 控件宽高默认值表与自适应规则
第五节那张表是整份文档最实用的部分。核心规则可以归纳成三类:
| 类别 | 控件 | 宽度 | 高度 |
|---|---|---|---|
| 自适应文本类 | Label、LinkButton、HyperLink、CheckBox、RadioButton | 适应文本 | 适应文本或默认 |
| 固定输入类 | TextBox | 150px 或 100% 或按内容 | 默认值 |
| 固定按钮类 | Button | 50px 或文本宽度 | 默认值 |
| 容器类 | Panel、PlaceHolder | 适应内部控件 | 适应内部控件 |
| 列表类 | DropDownList | 150px 或 100% 或适应文本 | 默认值 |
| 列表类 | ListBox | 150px 或 100% | 100px 或按项目数 |
| 数据展示类 | DataGrid、DataList、Repeater | 按需 | 按需 |
| 校验类 | 各种 Validator、ValidationSummary | 适应文本或默认 | 默认 |
“默认值”这三个字是留给 CSS 的,规范没有把 20px 还是 22px 写死,只要求“由 CSS 统一定义”。这一点很关键:文档只锁相对关系(Label 跟文本走、TextBox 默认 150px、Button 50px 起),绝对像素值交给样式表,项目换皮的时候只改一处。
<!-- 按规范写:宽度只在需要偏离默认值时才显式声明 --> <asp:TextBox id="txtUserName" runat="server" Width="150px" /> <asp:Button id="btnOK" runat="server" Text="确定" Width="50px" /> <asp:Label id="lblTip" runat="server" Text="请输入用户名" /> <asp:DropDownList id="ddlStatus" runat="server" Width="150px" />这里有个参数层面的坑:Width 写成150px是硬宽度,遇到窄屏或长文本会溢出;写成100%则完全跟随父容器,在 Panel 里很稳,在裸<td>里会撑满整列。我的习惯是输入框默认150px,只有在筛选栏这种横向排列的场景才用100%,并且一定配合父容器的固定宽度使用。
4. 表格排版:缩进、宽高声明与空单元格的三条硬约束
第六节 6.5 是整份规范里最“较真”的部分,专门管<table>。在 CSS Grid 和 Flexbox 普及之前,Web Form 的页面骨架全靠表格搭,嵌套三四层是常态,于是表格的写法直接决定了页面加载观感和后期维护成本。这些约束今天依然有价值——大量存量系统还在用表格布局,而且表格布局的很多问题(空白、抖动、错位)在 div 布局里换个形式照样出现。
4.1 缩进与</td>不换行:一个半角空格的蝴蝶效应
规范 6.5.1 给了一条非常具体的规则:<table>和<tr>对齐,<td>缩进一个 TAB;如果<td>里没有嵌套表格,</td>必须和<td>在同一行,不能换行。
原因写得很清楚:浏览器把换行当作一个半角空格。换行写在<td>和</td>之间,等于给单元格内容前后各加了一个看不见的空格,图片会偏移一两个像素,文字会莫名多出一点左边距——这类问题排查起来纯属玄学,因为源代码里什么都看不出来。
<!-- 不规范:</td> 换行,等于在图片后面塞了一个半角空格 --> <td><img src="../images/sample.gif" alt="" /> </td> <!-- 规范:无嵌套表格时,结束标记与 <td> 同行 --> <td><img src="../images/sample.gif" alt="" /></td> <!-- 确实需要空格时,显式写出来 --> <td><img src="../images/sample.gif" alt="" /> </td>逻辑说明:第一段代码里,图片和</td>之间的换行会被解析成一个空白文本节点,在某些浏览器和字号组合下会让单元格宽度多出 3~4px,表现是列宽对不齐;第三段是“我真需要一个空格”的合法写法,把空格显式放在</td>前面,意图明确,后来人看得懂。
参数上,缩进用 TAB 还是 4 个空格不重要,重要的是同一项目内统一——混用会导致 diff 里全是空白变更,代码评审时真正的改动被淹没。
4.2 width 和 height 只写一次:一个单元格只有一个控制源
6.5.2 给的规则是这套规范里最容易被忽略、也最容易埋雷的一条:width 和 height 不要重复声明。只有一列的表格,width 写在<table>标签上;只有一行的表格,height 写在<table>上;多行多列的表格,宽高写在第一行或第一列的<td>上。
判断标准很实用:改动代码里任何一个 width/height 数值,都应该在浏览器里看到变化。如果你改了没反应,说明有另一个声明在压着它,这个重复声明就是垃圾代码。
<!-- 多行多列表格:宽高只写在第一行的 <td> 上 --> <table border="0" cellspacing="0" cellpadding="0"> <tr> <td width="120" height="24">单据编号</td> <td>INV-2024-0001</td> </tr> <tr> <td>开票日期</td> <td>2024-03-18</td> </tr> </table>同时规范还禁了两件事:同一层级的<table>必须左对齐;不允许存在空的单元格。空单元格的处理方式是塞一张 1×1 的透明 gif,业界习惯叫null.gif,因为部分浏览器认为空<td>非法,会直接不渲染或者塌陷高度,导致表格上下错位。
这条“左对齐”的约束放到今天有点绝对,但在表格嵌套布局里它是保命条款:一旦某一层表格写了align="center",内层所有单元格的基准线都会跟着漂,外层想对齐就得再加一层包裹,嵌套层数瞬间超标。
4.3 三层嵌套与 tbody 分块:把首屏空白时间压下去
6.5.3 提了两条结构性建议:表格嵌套尽量控制在三层以内;避免用一整张大表格包住整页内容,如果必须这么做,用<tbody>分块。
第二条的理由写得很直白,也是我踩过最疼的一次:浏览器解析表格是“整表就绪才渲染”,一整页塞在一张大表格里,用户会先看到长时间空白,然后所有内容一次性弹出来。这不是性能问题,是感知问题——同样的加载耗时,分块渲染会让用户觉得“页面在动”。
<tbody>的作用是给这张大表切段,浏览器可以先渲染已解析完的 tbody。三层嵌套的上限则是经验值:超过三层之后,任何一次列宽调整都会引发连锁位移,维护成本指数上升。
配套的还有第七节的目录结构规范,它和表格布局是一套的:根目录下设images、common、temp三个必选子目录,按需加media。images放全站公用图(logo、banner、菜单、按钮),common放 css、js、include 等公共文件,temp放客户陆续提供的原始资料,media放 flash、avi 这类多媒体。栏目各自建目录,栏目下再按需开images和media;temp里的资料按日期建子目录归档。
这条规范里最值得抄走的是“路径全部使用相对路径”和“目录缺省文件不写全名”:<a href="aboutus/">而不是<a href="aboutus/index.htm">。好处是换服务器、换默认文档名时不用全站替换链接。
5. 常见问题排查:五个真实翻车现场
规范类文档最大的问题是“看起来都对,用起来全错”,因为条款之间会互相打架。下面这五条是照着这份文档落地时最容易撞上的,每条按现象、原因、解决三步走。
5.1 视觉类的三个坑
现象一:同一行的 Label 和 TextBox 永远差两三个像素,怎么调都对不齐。原因基本是</td>换行留下的半角空格,或者 Label 用了适应文本宽度而 TextBox 用了固定150px,两者的基线计算方式不同。解决方法是先按 4.1 节把所有无嵌套表格的</td>收回同一行,再给这一行的 Label 显式加vertical-align: middle,让文本基线和输入框居中对齐,而不是各按各的算。
现象二:页面加载时图片区域先是塌陷,图片加载完整个页面往下一跳。这是规范 7.2 节明确写过的副作用:为了“发挥浏览器自身排版能力”,建议<img>不写 width 和 height,代价是图片未加载时没有占位,尺寸大的图会造成页面抖动。解决办法不是无脑加宽高,而是分场景:图片在固定尺寸表格里就不用管;图片在流式布局里,就要在最后补上 width 和 height,或者用 CSS 给容器一个最小高度。
现象三:中文和英文混排时字间距忽宽忽窄,括号位置也很怪。规范 7.2 节第 4 条给的是排版细则:不同语种文字之间加一个半角空格,避头符号之前、避尾符号之后除外;汉字之间的标点用全角,英文字母和数字周围的括号用半角。这条最难的是执行一致性,我的做法是把它写进代码评审清单,而不是指望每个人记住。
5.2 结构与状态类的两个坑
现象四:改了<td width="120">的数值,浏览器里毫无变化。原因是同一个单元格的宽度被多处声明:外层<table>有 width,<tr>里有 height,<td>里又写了 width,浏览器按自己的优先级挑了一个生效,你改的那个正好没被采纳。解决方法是按 4.2 节的原则排查,保证一个维度只有一个控制源,改完再验证一次“改数字能看到变化”。
现象五:页面 ViewState 越来越大,一个列表页的隐藏字段能到几百 KB。规范 7.3 节对此有明确要求:所有不需要 PostBack 后取值的控件一律关闭 ViewState,仅作单向显示的控件(如 Label)可以关,静态文本直接用 HTML 标记而不要用服务端控件。很多人以为 ViewState 是框架自动管理的黑匣子,实际上它就是把控件状态序列化进页面,控件越多、层级越深,体积越大。解决办法在下一章展开。
还有一条容易被当成小事的:客户端脚本一律用 JavaScript,编码规则参照 C# 规范。这条约束的实际意义是“别在同一页面混用多种脚本风格”,至于具体用哪种,在那个技术栈里其实没有第二个选项。
6. 上线前的一道自检:ViewState 与脚本约束怎么收口
规范最后一节讲状态管理,只有短短一段,但它决定了页面体积的上限。规则可以拆成三句:不需要 PostBack 后取值的控件,一律EnableViewState="false";仅作单向显示的控件(典型是 Label)可以关;所有静态文本直接写 HTML 标记,不要用服务端控件包一层。
落地时我一般按下面的方式写,并且把“关 ViewState”当成新建页面时的默认动作,而不是性能优化阶段的补救:
<%@ Page Language="C#" EnableViewState="false" %> <!-- 页面级先关,需要保持状态的控件单独打开 --> <asp:TextBox id="txtKeyword" runat="server" EnableViewState="true" /> <asp:Label id="lblTitle" runat="server" Text="单据列表" /> <!-- 纯静态文案直接写 HTML,不走服务端控件 --> <h2 class="page-title">单据列表</h2>页面级EnableViewState="false"是总闸,然后给真正需要跨回传保持值的输入控件单独开;静态标题这类内容改用<h2>,直接不进 ViewState 序列化。参数上要注意,页面级关闭之后,GridView 这类控件的分页、排序状态也会丢,需要显式打开或者改用查询字符串传参——这也是规范说“如有可能关闭整个页面的 ViewState”而不是“必须关闭”的原因。
验证环节我习惯用两条命令加一次肉眼回看:
# 1) 找出还在用 <font> 标签或给 <img> 硬写尺寸的地方 grep -rn --include=*.aspx -E '<font size=|<img[^>]+(width|height)=' . # 2) 找出用连续 做缩进或占位的段落 grep -rn --include=*.aspx -E '( ){2,}' .第一条对应规范里“禁止<font size=?>、字号一律走样式表”和“谨慎给 img 写死尺寸”,第二条对应“禁止连续多个 、首行缩进用 text-indent”。grep 只能捞明显违规,像“Label 用了服务端控件但内容其实是静态的”这种,工具判断不了,只能靠回看页面源码里__VIEWSTATE的长度——我的经验值是列表页超过 100KB 就该动手拆了。
血泪经验是:这套规范真正难的不是理解,是执行的一致性。有次接手一个模块,前缀表、字号、边距全都对,唯独表格嵌套了五层,改一个列宽要往下追四层<td>,最后只能推倒重排。从那以后我每次新建页面都强制走一遍三件事——控件 ID 用脚本扫一遍、CSS 里确认 9pt 基准和 3px 边距没被覆盖、表格嵌套数一遍不超过三层。希望帮到你。
本文还有配套的精品资源,点击获取