《进击吧!Blazor!》第一章 5.组件开发
zhezhongyun 2025-05-05 20:10 18 浏览
《进击吧!Blazor!》是本人与张善友老师合作的Blazor零基础入门系列视频,此系列能让一个从未接触过Blazor的程序员掌握开发Blazor应用的能力。
视频地址:
https://space.bilibili.com/483888821/channel/detail?cid=151273
本系列文章是基于《进击吧!Blazor!》直播内容编写,升级.Net5,改进问题,讲解更全面。因为篇幅有限,文章中省略了部分代码,完整示例代码:
https://github.com/TimChen44/Blazor-ToDo
作者:陈超超
Ant Design Blazor 项目贡献者,拥有十多年从业经验,长期基于.Net技术栈进行架构与开发产品的工作,现就职于正泰集团。
邮箱:timchen@live.com
欢迎各位读者有任何问题联系我,我们共同进步。
这次分享我么要聊聊Blazor的精髓,也是我个人认为Blazor框架体系中最优秀的特性——组件。
组件
组件(Component)是对数据和方法的简单封装。几乎所有UI相关的框架都有组件(控件)的概念。
早期的Delphi组件叫做VCL(Visual Component Library),它采用自身嵌套的方式组合成所需的用户界面,并提供属性,方法,事件与组件外部进行交互,自身有着独立的生命周期,在必要的时候进行销毁。
之后.Net的WinForms和WPF组件相对于Delphi虽然设计实现上完全不同,但是对组件的定义和用途上几乎一致。
现在Web前端框架Angular中也采用了组件的概念,整体理念依旧相似。
有些框架根据是否可见将组件分为,组件(Component)不可见,控件(Control)可见,比如Delphi,WinForms
纵观这些框架的组件设计,可以提炼出组件包含以下特性。
Blazor应用也是使用组件构建的。组件是自包含的用户界面 (UI) 块,例如页、对话框或窗体。 组件包含插入数据或响应 UI 事件所需的 HTML 标记和处理逻辑。 组件非常灵活且轻量。 可在项目之间嵌套、重复使用和共享。
1.参数(属性)
提供组件外部向组件内部传递数据的方式。
在Blazor中我们称组件的属性(Property)叫参数(Parameter),参数本身就是一个属性,但是为了让Blazor框架能区分两者,所以我们在属性上增加 [Parameter]特性来声明属性为组件的参数。
[Parameter]
public string Text { get; set; }
组件参数
组件参数可以接收来在razor页面中给与的值,支持简单类型,也可以支持复杂类型。
<!--组件代码-->
<h1>Blazor is @Text!</h1>
@code {
[Parameter]
public string Text { get; set; }
}
123456
<!--组件使用-->
<Component Title="Superior">
12
上例就是将Superior通过参数传入组件,组件中就会输出Blazor is Superior!
路由参数
组件可以接收来自 @page 指令所提供的路由模板的路由参数。 路由器使用路由参数来填充相应的组件参数。参数类型受限于路由规则,只支持几个基本类型。
<!--页面代码-->
@page "/RouteParameter/{text}"
<h1>Blazor is @Text!</h1>
@code {
[Parameter]
public string Text { get; set; }
}
1234567
当使用/RouteParameter/Superior地址进行路由时,跳转到上例中的页面,并且页面输出Blazor is Superior!
级联参数
在某些情况下,使用组件参数将数据从祖先组件流向子代组件不太方便,尤其是在有多个组件层时。 级联值和参数提供了一种方便的方法,使祖先组件为其所有子代组件提供值,从而解决了此问题。
祖先组件中使用CascadingValue设定需要向下传递的级联值,子代组件中使用 [CascadingParameter] 特性来声明级联参数用于接收级联值。
本文后续会有详细的Demo来讲解此特性,此处暂不展开了。
2.事件
事件是一种由组件内部发起,由组件外部处理的一种机制。
对于原始的Html元素与Razor组件在事件的使用上有一些细微差别,下面分开介绍。
Html 元素
对HTML 元素的事件采用@on{EVENT}格式(例如 @onclick)处理事件,Razor 组件将此属性的值视为事件处理程序。
<h1>Blazor is @Text!</h1>
<button @onclick="OnClick">Button</button>
@code
{
private string Text { get; set; }
void OnClick(MouseEventArgs e)
{
Text = "Superior";
}
}
12345678910
点击Button按钮后就触发@onclick事件,然后设置Text的值,最后组件输出Blazor is Superior!
每一个事件都会返回一个参数,@onclick事件返回MouseEventArgs参数,更多详见事件参数类型
Razor 组件
跨组件公开事件,可以使用 EventCallback。父组件可向子组件的 EventCallback 分配回调方法,由子组件完成调用。
<!--子组件-->
<button @onclick="OnBtnClick">Button</button>
@code {
[Parameter]
public EventCallback<string> OnClick { get; set; }
void OnBtnClick(MouseEventArgs e)
{
if (OnClick.HasDelegate)
OnClick.InvokeAsync("Superior");
}
}
123456789101112
<!--父组件-->
<h1>Blazor is @Text!</h1>
<Component OnClick="OnClick"></Component>
@code
{
private string Text { get; set; }
void OnClick(string e)
{
Text = e;
}
}
1234567891011
EventCallback<string> OnClick 定义了一个名为OnClick的事件,EventCallback的泛型参数就是事件的参数类型。
OnClick.InvokeAsync("Superior") 调用这个事件,让注册的方法执行,注意事件调用前通过OnClick.HasDelegate判断事件是否有被注册,如果没有任何方法注册此事件,那么调用会发生异常。
OnClick="OnClick" 将OnClick方法注册给事件。
3.方法
组件对外暴露的方法,提供外部组件调用。
<!--组件代码-->
<h1>Blazor is @Text!</h1>
@code
{
private string Text { get; set; }
public void SetText(string text)
{
Text = text;
StateHasChanged();
}
}
1234567891011
<!--组件使用-->
<Component @ref="@component"></Component>
<button @onclick="OnClick">Button</button>
@code
{
private Component component;
void OnClick(MouseEventArgs e)
{
component.SetText("Superior");
}
}
1234567891011
当点击Button按钮触发@onclick事件,通过Component组件的SetText方法设置组件的Text值,组件就输出Blazor is Superior!
@ref 想要获得某个组件的实例,可以使用@ref特性,在这里他会把Component组件的实例填充到component变量中。此处注意,@ref的应用只有在组件完成呈现后才完成。
4.数据绑定
参数只提供了外部组件向组件单向赋值,数据绑定就是双向赋值。
对于原始的Html元素与Razor组件在数据绑定的使用上有一些细微差别,下面分开介绍。
Html 元素
使用通过名为 @bind 的 Html 元素特性提供了数据绑定功能。
<h4>Blazor is @Text!</h4>
<input @bind="Text" />
@code
{
private string Text;
}
123456
把Text变量绑定到input组件,当input中完成输入且离开焦点后输出Blazor is Superior!。
如果我们想要输入时立即显示输入的内容,我们可以通过带有 event 参数的 @bind:event 属性将绑定指向 oninput 事件。
<h4>Blazor is @Text!</h4>
<input @bind="Text" @bind:event="oninput"/>
@code
{
private string Text;
}
123456
Html元素绑定实现原理
Html元素本身并不支持双向属性绑定机制,当我们使用@bind后,Blazor帮我们生成了value="@Text"实现向Html元素赋值,再生成@onchange事件实现Html元素向绑定变量赋值。
<input value="@Text"
@onchange="@((ChangeEventArgs __e) => Text = __e.Value.ToString())" />
@code {
private string Text { get; set; }
}
1234567
5.嵌套
组件嵌套就是允许一个组件成为另一组件的容器,通过父与子的层层嵌套实现各种复杂的界面,在这过程中我们也能提炼出相似的组件,加以重复使用和共享。
下面是“我的一天”界面的代码以及他们组件的嵌套结构
子内容
组件可以设置自己的某一个位置插入其他组件的内容。
<!--组件代码-->
<h1>Blazor is @ChildContent</h1>
@code{
[Parameter] public RenderFragment ChildContent { get; set; }
}
12345
<!--组件使用-->
<Component>
<strong>Superior!</strong>
</Component>
1234
Component具有一个类型为 RenderFragment 的 ChildContent 属性,RenderFragment表示要呈现的 UI 段。
ChildContent 的值是从父组件接收的UI段。
在组件中需要呈现ChildContent内容的地方放置@ChildContent标记。
ChildContent属性命名为固定名字,下面是完整写法,上面是简略写法。
<Component>
<ChildContent>
<strong>Superior!</strong>
</ChildContent>
</Component>
12345
模板
可以通过指定一个或多个 RenderFragment 类型的组件参数来接收多个UI段。
<!--组件代码-->
<h1>@Title is @Quality</h1>
@code{
[Parameter] public RenderFragment Title { get; set; }
[Parameter] public RenderFragment Quality { get; set; }
}
1234567
<!--组件使用-->
<Component>
<Title>
<strong>Blazor</strong>
</Title>
<Quality>
<strong>Superior!</strong>
</Quality>
</Component>
123456789
模板参数
可以定义 RenderFragment<TValue> 类型的组件参数来定义支持参数的模板。
<!--组件代码-->
@foreach (var item in Items)
{
<h4>@Title(item) is Superior!</h4>
}
@code{
[Parameter] public RenderFragment<string> Title { get; set; }
[Parameter] public IReadOnlyList<string> Items { get; set; }
}
123456789
<!--组件使用-->
<Component Items="items">
<Title Context="item">
<strong>@item</strong>
</Title>
</Component>
@code{
List<string> items = new List<string> { ".Net", "C#", "Blazor" };
}
123456789
组件使用时通过IReadOnlyList<string> Items属性将内容传入组件,组件内部使用@foreach (var item in Items)将集合循环呈现,@Title(item)确定了插入位置,且给模板传入item的值,再外部通过Context="item"接收参数,最终实现模板的呈现。
6.生命周期
Blazor 框架包括同步和异步生命周期方法。一般情况下同步方法会先与异步方法执行。
我们可以重写生命周期方法的,以在组件初始化和呈现期间对组件执行其他操作。
组件初始化
组件状态改变
组件销毁
ToDo应用组件化改造
任务信息
重要任务不论是否是今天,我们都需要便捷地查看,所以我们需要做一个“重要任务”的页面。
这个页面显示内容和“我的一天”非常相似,所以我们可以抽象出一个TaskItem.razor组件,组件的Html以及样式基本是从ToDay.razor组件迁移过来。
<Card Bordered="true" Size="small" Class="task-card">
<div class="task-card-item">
@{
var finishClass = new ClassMapper().Add("finish").If("unfinish", () => Item.IsFinish == false);
}
<div class="@(finishClass.ToString())" @onclick="OnFinishClick">
<Icon Type="check" Theme="outline" />
</div>
<div class="title" @onclick="OnCardClick">
@if (TitleTemplate != null)
{
@TitleTemplate
}
else
{
<AntDesign.Text Strong> @Item.Title</AntDesign.Text>
<br />
<AntDesign.Text Type="@TextElementType.Secondary">
@Item.Description
</AntDesign.Text>
}
</div>
<div class="del" @onclick="OnDelClick">
<Icon Type="rest" Theme="outline" />
</div>
<div class="date">
@Item.PlanTime.ToShortDateString()
<br />
@{
int? days = (int?)Item.Deadline?.Subtract(DateTime.Now.Date).TotalDays;
}
<span style="color:@(days switch { _ when days > 3 => "#ccc", _ when days > 0 => "#ffd800", _ => "#ff0000" })">
@Item.Deadline?.ToShortDateString()
</span>
</div>
@if (ShowStar)
{
<div class="star" @onclick="OnStarClick">
<Icon Type="star" Theme="@(Item.IsImportant ? "fill" : "outline")" />
</div>
}
</div>
</Card>
1234567891011121314151617181920212223242526272829303132333435363738394041424344
public partial class TaskItem
{
//任务内容
[Parameter] public TaskDto Item { get; set; }
//完成图标事件
[Parameter] public EventCallback<TaskDto> OnFinish { get; set; }
public async void OnFinishClick()
{
if (OnFinish.HasDelegate)
await OnFinish.InvokeAsync(Item);
}
//条目点击事件
[Parameter] public EventCallback<TaskDto> OnCard { get; set; }
public async void OnCardClick()
{
if (OnCard.HasDelegate)
await OnCard.InvokeAsync(Item);
}
//删除图标事件
[Parameter] public EventCallback<TaskDto> OnDel { get; set; }
public async void OnDelClick()
{
if (OnDel.HasDelegate)
await OnDel.InvokeAsync(Item);
}
//重要图标事件
[Parameter] public EventCallback<TaskDto> OnStar { get; set; }
public async void OnStarClick()
{
if (OnStar.HasDelegate)
await OnStar.InvokeAsync(Item);
}
//是否相似重要图标
[Parameter] public bool ShowStar { get; set; } = true;
//支持标题模板
[Parameter] public RenderFragment TitleTemplate { get; set; }
}
@if (TitleTemplate != null) 如果外部传入了模板,那么就是显示模板,否则就使用默认格式显示。
新建任务
在“重要任务”和“我的一天”中均有添加任务的功能,我们也将他们抽象成NewTask.razor组件。
<Divider Text="新任务"></Divider>
@if (newTask != null)
{
<Spin Spinning="isNewLoading">
<div class="task-input">
<DatePicker Picker="@DatePickerType.Date" @bind-Value="@newTask.PlanTime" />
<Input @bind-Value="@newTask.Title" OnkeyUp="OnInsertKey" />
@if(ChildContent!=null )
{
@ChildContent(newTask)
}
</div>
</Spin>
}
1234567891011121314
public partial class NewTask
{
[Inject] public MessageService MsgSrv { get; set; }
[Inject] public HttpClient Http { get; set; }
[Parameter] public EventCallback<TaskDto> OnInserted { get; set; }
[Parameter] public Func<TaskDto> NewTaskFunc { get; set; }
[Parameter] public RenderFragment<TaskDto> ChildContent { get; set; }
//新的任务
TaskDto newTask { get; set; }
private bool isNewLoading { get; set; }
protected override void OnInitialized()
{
newTask = NewTaskFunc?.Invoke();
base.OnInitialized();
}
async void OnInsertKey(KeyboardEventArgs e)
{
if (e.Code == "Enter")
{
if (string.IsNullOrWhiteSpace(newTask.Title))
{
MsgSrv.Error(#34;标题必须填写");
return;
}
isNewLoading = true;
var result = await Http.PostAsJsonAsync<TaskDto>(#34;api/Task/SaveTask", newTask);
if (result.IsSuccessStatusCode)
{
newTask.TaskId = await result.Content.ReadFromJsonAsync<Guid>();
await Task.Delay(1000);
if (OnInserted.HasDelegate) await OnInserted.InvokeAsync(newTask);
newTask = NewTaskFunc?.Invoke();
}
else
{
MsgSrv.Error(#34;请求发生错误 {result.StatusCode}");
}
isNewLoading = false;
StateHasChanged();
}
}
}
EventCallback<TaskDto> OnInserted 不同场景下插入后需要做的事情可能不同,所以通过这个事件由外部进行处理。
Func<TaskDto> NewTaskFunc 不同场景下对TaskDto初始化要求不同,所以用这个函数来调用初始化。
RenderFragment<TaskDto> ChildContent 使用模板实现额外的表单进行扩展输入内容。
重要任务
创建Star.razor文件作为重要任务的页面文件,代码如下
@page "/star"
<PageHeader Title="@("重要的任务")" Subtitle="@(#34;数量:{taskDtos?.Count}")"></PageHeader>
<Spin Spinning="@isLoading">
@foreach (var item in taskDtos)
{
<TaskItem Item="item" OnFinish="OnFinish" OnCard="OnCardClick" OnDel="OnDel" OnStar="OnStar" ShowStar="false">
</TaskItem>
}
<NewTask OnInserted="OnInsert" NewTaskFunc="() => new TaskDto() { PlanTime = DateTime.Now.Date, IsImportant = true }"></NewTask>
</Spin>
123456789101112
public partial class Star
{
// 1、 列出当天的所有代办工作
[Inject] public HttpClient Http { get; set; }
bool isLoading = true;
private List<TaskDto> taskDtos = new List<TaskDto>();
protected async override Task OnInitializedAsync()
{
isLoading = true;
taskDtos = await Http.GetFromJsonAsync<List<TaskDto>>("api/Task/GetStarTask");
isLoading = false;
await base.OnInitializedAsync();
}
//2、 添加代办
public MessageService MsgSrv { get; set; }
async void OnInsert(TaskDto item)
{
taskDtos.Add(item);
}
//3、 编辑抽屉
[Inject] public TaskDetailServices TaskSrv { get; set; }
async void OnCardClick(TaskDto task)
{
TaskSrv.EditTask(task, taskDtos);
await InvokeAsync(StateHasChanged);
}
//4、 修改重要程度
private async void OnStar(TaskDto task)
{
var req = new SetImportantReq()
{
TaskId = task.TaskId,
IsImportant = !task.IsImportant,
};
var result = await Http.PostAsJsonAsync<SetImportantReq>("api/Task/SetImportant", req);
if (result.IsSuccessStatusCode)
{
task.IsImportant = req.IsImportant;
StateHasChanged();
}
}
//5、 修改完成与否
private async void OnFinish(TaskDto task)
{
var req = new SetFinishReq()
{
TaskId = task.TaskId,
IsFinish = !task.IsFinish,
};
var result = await Http.PostAsJsonAsync<SetFinishReq>("api/Task/SetFinish", req);
if (result.IsSuccessStatusCode)
{
task.IsFinish = req.IsFinish;
StateHasChanged();
}
}
//6、 删除代办
[Inject] public ConfirmService ConfirmSrv { get; set; }
public async Task OnDel(TaskDto task)
{
if (await ConfirmSrv.Show(#34;是否删除任务 {task.Title}", "删除", ConfirmButtons.YesNo, ConfirmIcon.Info) == ConfirmResult.Yes)
{
taskDtos.Remove(task);
}
}
}
TaskItem
OnFinish="OnFinish" OnCard="OnCardClick" OnDel="OnDel" OnStar="OnStar" 绑定不同的操作函数
此处完全可以使用上一节介绍服务将这些方法提取到一个独立的服务中,这里我就偷懒不改了。
ShowStar="false" 不显示重要图标
NewTask
NewTaskFunc="() => new TaskDto() { PlanTime = DateTime.Now.Date, IsImportant = true }" 重要初始化时默认将IsImportant设置成true
我的一天
我们将“我的一天”也进行适当改造
@page "/today"
<PageHeader Title="@("我的一天")" Subtitle="@DateTime.Now.ToString("yyyy年MM月dd日")"></PageHeader>
<Spin Spinning="@isLoading">
@foreach (var item in taskDtos)
{
<TaskItem @key="item.TaskId" Item="item" OnFinish="OnFinish" OnCard="OnCardClick" OnDel="OnDel" OnStar="OnStar">
<TitleTemplate>
<AntDesign.Text Strong Style="@(item.IsFinish?"text-decoration: line-through;color:silver;":"")"> @item.Title</AntDesign.Text>
<br />
<AntDesign.Text Type="@TextElementType.Secondary">
@item.Description
</AntDesign.Text>
</TitleTemplate>
</TaskItem>
}
<NewTask OnInserted="OnInsert" NewTaskFunc="()=> new TaskDto() {PlanTime=DateTime.Now.Date }">
<ChildContent Context="newTask">
<RadioGroup @bind-Value="newTask.IsImportant">
<Radio RadioButton Value="true">重要</Radio>
<Radio RadioButton Value="false">普通</Radio>
</RadioGroup>
</ChildContent>
</NewTask>
</Spin>
123456789101112131415161718192021222324252627
C#代码因为变化很小,所以不在此处贴出
TaskItem
TitleTemplate 通过模板重写了标题的显示方式,支持当完成后标题增加删除线
NewTask
ChildContent 重写了子内容,提供了重要度的选择。
次回预告
自己的待办当然只有自己能看了啦,所以登录,权限啥的都给安排上,请关注下一节——安全
学习资料
更多关于Blazor学习资料:https://aka.ms/LearnBlazor
相关推荐
- 不看必后悔!15个三星GoodLock隐藏小技巧~(上)
-
很多刚用三星手机的星粉们,一定对三星GoodLock这个宝藏App还不是很了解,今天就带大家一起详细去看看GoodLock这个大家族究竟有多少宝藏功能及成员呢?让我们一起来看看吧~由于Good...
- 惊魂幻象理智值监控WA!大字体+范围提示一目了然
-
一个WA助你轻松监控大幻象理智值,不再错过恢复时机这个WA用大字体实时同步我们的理智值下面的图标提示恢复宝珠的剩余次数上面的图标高亮则意味着我们在宝珠的恢复范围内我们可以将图标随意移动到适合自己观察的...
- 盘点十个超炫的jQuery插件(jquery插件是干什么的)
-
“DevExpress14.2新版发布会”即将推出。心动不如行动,赶快报名吧!我们期待与您相约。今天小编为大家搜罗了十个超酷的jQuery插件,这可以使你的网站界面更加友好。jQuery创造了令人难...
- Google官方梳理,Android 多返回栈技术详解
-
用户通过系统返回按钮导航回去的一组页面,在开发中被称为返回栈(backstack)。多返回栈即一堆"返回栈",对多返回栈的支持是在Navigation2.4.0-alpha0...
- 说三星手机系统不好用,因为没有玩明白三星,三星Diy功能超强大
-
都说三星手机系统不好用,其实真正用起来,挺好用的三星手机系统像块没打磨的玉,默认设置是基础款,用着普通。但你要是肯花点时间,它能变得特别顺手。关键就在一个叫GoodLock的工具,它是三星自家出的...
- Sam Helper三星手机用户必装神器(三星手机必装app)
-
SamHelper这款软件集合了三星手机主题工具当前主题o主题路径o主题安装系统设置屏幕模式o状态栏o文件o频段o暗码Lock&LabsGoodLockoGalaxyLa...
- 外卖套餐搭配的探索和应用(外卖套餐搭配技巧)
-
本文系外卖美食知识图谱系列的第三篇文章,从技术层面我们会介绍外卖套餐搭配的技术方案,包括离线、实时的套餐搭配的迭代,套餐质量评估方案,同时会介绍套餐搭配的业务应用。1.背景让用户更方便快捷地选购到满...
- 用几行原生JS就可以实现丝滑的元素过渡效果
-
作者:ConardLi大家可以看下下面这个应用的页面切换体验,是不是很丝滑~做过体验优化的朋友应该都清楚,如果用原生的CSS或者JS动画去实现,想要实现出类似的效果,不会特别简单,而且也要考...
- 速腾车主RNS315固件及2016年6月地图升级详细教程
-
本来打算买个大众原厂的地图卡,后来在网上看其实不需要原厂地图卡也可以升级,于是开始在网上收集资料,开始天真的以为只要有密匙文件修改一下就可以免费升级了,其实最主要的还是破解的固件,不知道为什么,网上找...
- 学习一个母词act,一次解析一串关联、复合、衍生词族
-
首先形义解读一下act的原始意象,A是力量与行动的开始C是范围的覆盖T是目标目的的接触与刻度合在一起,行动行为艺术力量的复合行为以下是关于act的复合词、词根词缀衍生词及其变形后的词根衍生词的详细...
- 优迈系统(一体化控制柜)快车调试(八)
-
逻辑故障和驱动故障分析段码管上显示警告码和故障码对照表警告码操作模式故障码操作模式或驱动故障AL000EPC(紧急电源管制模式)ER100DTC(门在设定时间内不能关到位)AL001COR(复位模式)...
- Layui简单实现左侧菜单和Tab选项卡动态操作
-
<!DOCTYPEhtml><html><head><metacharset="utf-8">...
- 码农如何快速打造一个有设计感的网站
-
像我这样的程序员来说经常被“设计”这个词吓到,因为我是一名程序员而不是设计师,我拥有的是计算机学位证,另外我对ComicSans字体并不介意。(注:ComicSans字体是Win95附带...
- vue3 新特性 computed、watch、watchEffect 看完就会
-
1、watchEffectwatchEffect侦听器是一个副作用函数,不需要指定监听的某个属性,监视的回调中用到哪个属性,就会监听哪个属性,一旦运行就会立即执行。watchEffect与com...
- 10个冷门但非常实用前端开发者很少用的CSS规则
-
许多开发者只学了基础——比如修改颜色、设置字体或创建弹性布局——就止步不前。但CSS是一门精深而多用途的语言,掌握后能帮你构建优雅、高效且无障碍的界面。无论你是从零搭建还是微调设计系统,掌握一些高...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML常用标签 (29)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)
- HTML button formtarget 属性 (30)
- CSS 水平对齐 (Horizontal Align) (30)