跳到主要内容
HTML input 标签 type 属性详解与实战避坑指南 | 极客日志
HTML / CSS 大前端
HTML input 标签 type 属性详解与实战避坑指南 综述由AI生成 详细解析了 HTML input 标签的多种 type 属性,包括 text、password、email、number 等常见类型及 date、color、file 等特殊类型。重点阐述了不同浏览器和移动端的兼容性差异、键盘适配问题、表单校验陷阱以及最佳实践方案。通过真实项目案例,提供了防抖搜索、密码显示切换、文件上传校验等实用技巧,并强调了后端二次验证的重要性。
岁月神偷 发布于 2026/4/6 更新于 2026/5/20 24 浏览
HTML input 标签 type 属性详解与实战避坑指南
引言:那天我差点被一个 input 搞自闭了
这事儿说起来有点丢人。去年接了个急活儿,客户要个简单的"手机号 + 验证码"登录页。我心想这有啥难的,不就是两个输入框一个按钮嘛,咔咔咔半小时搞定,自信满满地交了差。
结果测试妹子拿着她的古董安卓机过来,一脸疑惑地问我:"为啥我点手机号输入框,弹出来的是全键盘啊?我同事 iPhone 上就是数字键盘诶?"
我当场愣住。赶紧拿她手机一看,好家伙,我写的 type="text",人家 iOS 可能智能识别给换了数字键盘,但这部老安卓就老老实实给我弹了个全键盘。用户得切来切去才能输手机号,体验稀碎。
那一刻我才意识到,我对 input 的认知可能还停留在"就是个框框让用户打字"的原始人阶段。后来一查,光是 type 属性就有二十多种,每种在不同浏览器、不同设备上的表现都天差地别。有的能调键盘,有的能校验格式,有的能弹出原生组件,有的…在 IE 里直接装死。
所以今天这篇文章,咱们就把 input 这个"看似简单实则深不见底"的玩意儿彻底扒干净。不管你是刚入行的小白,还是写了几年代码但从来没认真看过 MDN 的老油条(比如我),相信都能捞到点干货。咱不搞那种"官方文档翻译版"的枯燥罗列,就聊我在真实项目里踩过的坑、流过泪的教训,以及那些"原来还能这么玩"的骚操作。
input 到底是个啥玩意儿
说实话,HTML 里比 input 更"表里不一"的标签真不多。你看它标签名就叫 input(输入),感觉就是个被动接收用户打字内容的容器。但实际上,它更像是浏览器提供的一个"功能入口",type 属性就是决定这个入口通向哪里的钥匙。
同样是 <input>,type 改成 text 它就是普通文本框;改成 file 它就变成文件选择器,能调系统的资源管理器;改成 color 它甚至能唤出系统的调色板。这已经不是"输入"的范畴了,简直是个万能接口。
更微妙的是,浏览器对不同类型的 input 有着完全不同的处理逻辑。比如:
渲染层 :date 类型在某些浏览器里会渲染成自带日历图标的复合组件,而 text 就是光秃秃一个框
交互层 :number 类型在桌面端可能有上下箭头微调按钮,在移动端则优先唤起数字键盘
校验层 :email 和 url 类型在表单提交时会自动做格式校验,虽然这校验逻辑有时候挺智障的
数据层 :但最坑的是,无论 type 是什么,input.value 返回的永远是字符串 。对,你没看错,哪怕是 number 类型,你拿到的也是 "123" 而不是 123。这个后面会详细吐槽。
还有一个很多人忽略的点:移动端键盘适配 。iOS 和 Android 会根据 input 的 type 来决定弹出什么键盘。这个细节在移动端表单体验里至关重要,毕竟全键盘和数字键盘的切换成本,对用户的打断感还是很强的。
所以理解 input 的关键在于:它不仅仅是一个"输入框",而是浏览器封装好的、带有特定功能的交互组件。选对 type,相当于免费获得了浏览器和操作系统层面的优化支持;选错了,轻则体验打折,重则功能直接崩掉。
type 值全家桶大起底
好了,进入正题。下面我把常用的 type 值一个个拎出来唠唠,每个都会给完整的代码示例 ,包括 HTML 结构、CSS 样式和 JavaScript 交互。毕竟光讲概念不写代码就是耍流氓。
text:最老实的打工人
这是最基础的类型,也是默认类型(如果你不写 type,浏览器就当它是 text)。它真的就是"你敲啥它显示啥",没有任何特殊处理,也不带任何校验。
但正因为它的"纯洁",有时候反而成了坑。比如前面说的手机号输入,如果你无脑用 text,移动端就不会自动切数字键盘。
基础用法:
用户名
<label for ="username" >
</label >
<input type ="text" id ="username" name ="username" placeholder ="请输入用户名" maxlength ="20" >
<div class ="input-wrapper" >
<input type ="text" id ="search" name ="q" placeholder ="搜索商品..." autocomplete ="off" autocapitalize ="off" autocorrect ="off" spellcheck ="false" enterkeyhint ="search" >
<button type ="button" id ="clear-btn" style ="display: none;" > ×</button >
</div >
<script >
const input = document .getElementById ('search' );
const clearBtn = document .getElementById ('clear-btn' );
input.addEventListener ('input' , (e ) => {
clearBtn.style .display = e.target .value ? 'block' : 'none' ;
});
clearBtn.addEventListener ('click' , () => {
input.value = '' ;
clearBtn.style .display = 'none' ;
input.focus ();
});
</script >
看到没,就算是普通的 text,也有很多细节可以优化。特别是 enterkeyhint 这个属性,可以让移动端键盘的右下角按钮显示"搜索"、"发送"、"下一步"等文字,而不是默认的"换行"或"前往",体验提升立竿见影。
password:表面神秘,其实只是把字符藏起来 这个大家都熟,输入内容会变成小黑点或星号。但很多人不知道的是,浏览器对 password 输入框有额外的安全处理,比如禁止自动填充(虽然现代浏览器为了用户体验,有时候会智能地提示保存密码)。
<label for ="pwd" > 密码</label >
<input type ="password" id ="pwd" name ="password" minlength ="6" maxlength ="20" required >
<div class ="password-wrapper" style ="position: relative; width: 300px;" >
<input type ="password" id ="password" placeholder ="请输入密码" style ="width: 100%; padding: 10px 40px 10px 12px; box-sizing: border-box;" >
<button type ="button" id ="togglePwd" aria-label ="显示密码" style ="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; padding: 5px;" >
<svg id ="eyeIcon" width ="20" height ="20" viewBox ="0 0 24 24" fill ="none" stroke ="currentColor" >
<path d ="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" > </path >
<circle cx ="12" cy ="12" r ="3" > </circle >
</svg >
</button >
</div >
<script >
const pwdInput = document .getElementById ('password' );
const toggleBtn = document .getElementById ('togglePwd' );
const eyeIcon = document .getElementById ('eyeIcon' );
let isVisible = false ;
toggleBtn.addEventListener ('click' , () => {
isVisible = !isVisible;
pwdInput.type = isVisible ? 'text' : 'password' ;
toggleBtn.setAttribute ('aria-label' , isVisible ? '隐藏密码' : '显示密码' );
if (isVisible) {
eyeIcon.innerHTML = `<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path> <line x1="1" y1="1" x2="23" y2="23"></line>` ;
} else {
eyeIcon.innerHTML = `<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path> <circle cx="12" cy="12" r="3"></circle>` ;
}
});
window .addEventListener ('beforeunload' , () => {
pwdInput.type = 'password' ;
isVisible = false ;
});
</script >
切换按钮要用 type="button",不然在 form 里会触发表单提交
加上 aria-label 给读屏软件用,这是无障碍的基本要求
密码框的 autocomplete 建议保留默认值或设为 current-password/new-password,让密码管理器能正常工作
email:自带格式校验,但别太信它 email 类型在桌面端看起来和 text 没啥区别,但它有两个隐藏特性:一是提交表单时会自动校验格式(必须包含 @ 和域名),二是移动端会调出带有 @ 和 . 符号的优化键盘。
<form id ="loginForm" >
<label for ="email" > 邮箱</label >
<input type ="email" id ="email" name ="email" placeholder ="[email protected] " required multiple >
<button type ="submit" > 提交</button >
<span id ="errorMsg" style ="color: red; display: none;" > </span >
</form >
<script >
const form = document .getElementById ('loginForm' );
const emailInput = document .getElementById ('email' );
const errorMsg = document .getElementById ('errorMsg' );
emailInput.addEventListener ('invalid' , (e ) => {
e.preventDefault ();
if (emailInput.validity .valueMissing ) {
errorMsg.textContent = '邮箱不能为空' ;
} else if (emailInput.validity .typeMismatch ) {
errorMsg.textContent = '邮箱格式不对,检查一下?' ;
} else if (emailInput.validity .tooShort ) {
errorMsg.textContent = `邮箱太短,至少 ${emailInput.minLength} 个字符` ;
}
errorMsg.style .display = 'block' ;
});
emailInput.addEventListener ('input' , () => {
if (emailInput.validity .valid ) {
errorMsg.style .display = 'none' ;
}
});
form.addEventListener ('submit' , (e ) => {
if (!emailInput.validity .valid ) {
e.preventDefault ();
emailInput.focus ();
}
});
</script >
但是! 浏览器的 email 校验非常宽松。比如 a@b 这种明显不合法的邮箱,很多浏览器认为是有效的(因为技术上 b 可以是局域网域名)。还有 abc@123 这种,浏览器可能也觉得 OK。
所以永远不要完全依赖前端的 type="email" 校验 ,后端必须再做一次严格验证。前端校验只是为了即时反馈,提升体验,防君子不防小人。
number:弹出数字键盘,但小心它返回字符串 number 类型在移动端是个神器,因为它能唤起数字键盘。但这也是个天坑,因为很多人以为 type="number" 就能保证拿到数字,结果 input.value 返回的是字符串 "123",而且如果用户输入了非法字符(比如字母),不同浏览器处理方式还不一样。
<label for ="age" > 年龄</label >
<input type ="number" id ="age" name ="age" min ="0" max ="150" step ="1" placeholder ="18" >
<script >
const ageInput = document .getElementById ('age' );
ageInput.addEventListener ('change' , (e ) => {
console .log ('value 类型:' , typeof e.target .value , '值:' , e.target .value );
console .log ('valueAsNumber 类型:' , typeof e.target .valueAsNumber , '值:' , e.target .valueAsNumber );
if (isNaN (e.target .valueAsNumber )) {
console .log ('没输入有效数字' );
}
});
ageInput.addEventListener ('keydown' , (e ) => {
if (e.key === '.' || e.key === 'e' || e.key === 'E' ) {
e.preventDefault ();
}
});
</script >
<label for ="price" > 价格(元)</label >
<input type ="number" id ="price" inputmode ="decimal" min ="0.01" max ="999999.99" step ="0.01" placeholder ="0.00" >
<script >
const priceInput = document .getElementById ('price' );
priceInput.addEventListener ('blur' , (e ) => {
let val = parseFloat (e.target .value );
if (!isNaN (val)) {
val = Math .max (0.01 , Math .min (999999.99 , val));
e.target .value = val.toFixed (2 );
}
});
function getPriceValue ( ) {
const val = priceInput.valueAsNumber ;
return isNaN (val) ? null : val;
}
</script >
Safari 桌面版 :不会阻止用户输入字母,只是提交时校验失败
Chrome :输入非数字字符时,value 会变成空字符串
Firefox :行为类似 Chrome,但 UI 表现略有不同
value 永远是 string ,记得用 parseFloat、parseInt 或 valueAsNumber 转换
tel:电话专用,iOS 安卓都给你调数字拨号盘 tel 类型是我修复前面那个"古董安卓机不弹数字键盘" bug 的救星。它专门用于电话号码输入,移动端会唤起纯数字键盘(通常还有 * 和 # 键)。
<label for ="mobile" > 手机号</label >
<input type ="tel" id ="mobile" name ="mobile" pattern ="1[3-9]\d{9}" maxlength ="11" placeholder ="13800138000" autocomplete ="tel" required >
<script >
const mobileInput = document .getElementById ('mobile' );
mobileInput.addEventListener ('input' , (e ) => {
let value = e.target .value .replace (/\D/g , '' );
if (value.length > 11 ) value = value.slice (0 , 11 );
if (value.length > 7 ) {
value = `${value.slice(0 , 3 )} ${value.slice(3 , 7 )} ${value.slice(7 )} ` ;
} else if (value.length > 3 ) {
value = `${value.slice(0 , 3 )} ${value.slice(3 )} ` ;
}
e.target .value = value;
});
mobileInput.addEventListener ('change' , (e ) => {
const pureNumber = e.target .value .replace (/\s/g , '' );
console .log ('纯数字:' , pureNumber);
});
</script >
注意 :tel 类型没有内置的格式校验 (不像 email 和 url),所以必须用 pattern 属性或 JS 正则来验证。它唯一的作用就是调起合适的键盘。
url:输入网址时自动补 http?想多了,它只校验格式 url 类型和 email 类似,主要做两件事:一是提交时校验格式(必须包含协议如 http:// 或 https://),二是移动端键盘会优化(通常会有 .com、/ 等快捷按键)。
<label for ="website" > 个人网站</label >
<input type ="url" name ="website" placeholder ="https://example.com" pattern ="https?://.+" >
<script >
const urlInput = document .getElementById ('website' );
urlInput.addEventListener ('blur' , (e ) => {
let val = e.target .value .trim ();
if (val && !/^https?:\/\//i .test (val)) {
e.target .value = 'https://' + val;
}
});
</script >
常见误区 :很多人以为 type="url" 会自动给输入的网址加 http://,其实不会,它只是校验。自动补全得自己写 JS。
search:带小×清空按钮,细节控狂喜 search 类型在视觉上和 text 几乎一样,但 WebKit 内核的浏览器(Chrome、Safari、Edge)会给它添加一个内置的"×"清空按钮,鼠标悬停或输入时显示,点击一键清空。这个细节虽然小,但对用户体验很友好。
<form role ="search" id ="searchForm" >
<input type ="search" id ="siteSearch" name ="q" placeholder ="搜索站内文章..." results ="5" autosave ="site-search" >
<button type ="submit" > 搜索</button >
</form >
<style >
input [type="search" ] ::-webkit-search-cancel-button {
-webkit-appearance : none;
appearance : none;
height : 20px ;
width : 20px ;
background : url ('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23999"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>' ) no-repeat center;
cursor : pointer;
opacity : 0.6 ;
transition : opacity 0.2s ;
}
input [type="search" ] ::-webkit-search-cancel-button:hover {
opacity : 1 ;
}
input [type="search" ] {
-webkit-appearance : textfield;
appearance : textfield;
border : 1px solid #ccc ;
padding : 8px 12px ;
border-radius : 4px ;
}
</style >
<script >
const searchInput = document .getElementById ('siteSearch' );
const searchForm = document .getElementById ('searchForm' );
let debounceTimer;
searchInput.addEventListener ('input' , (e ) => {
clearTimeout (debounceTimer);
const query = e.target .value .trim ();
if (query.length < 2 ) return ;
debounceTimer = setTimeout (() => {
console .log ('执行搜索:' , query);
performSearch (query);
}, 300 );
});
searchForm.addEventListener ('submit' , (e ) => {
e.preventDefault ();
const query = searchInput.value .trim ();
if (query) {
performSearch (query);
}
});
function performSearch (query ) {
console .log ('正在搜索:' , query);
}
</script >
注意 :results 和 autosave 是 Safari 的私有属性,其他浏览器不支持。如果需要跨浏览器一致的"最近搜索"功能,得用 localStorage 自己实现。
date / time / datetime-local:时间选择器三兄弟,兼容性一言难尽 这三个是 HTML5 新增的日期时间类型,浏览器会渲染成原生的日期选择器。听起来很美好,但现实很骨感——它们的兼容性特别是样式自定义能力,简直是前端噩梦。
<label for ="birthday" > 生日</label >
<input type ="date" id ="birthday" name ="birthday" min ="1900-01-01" max ="2024-12-31" >
<label for ="meetingTime" > 会议时间</label >
<input type ="time" id ="meetingTime" name ="meetingTime" min ="09:00" max ="18:00" step ="1800" >
<label for ="appointment" > 预约时间</label >
<input type ="datetime-local" id ="appointment" name ="appointment" min ="2024-01-01T00:00" max ="2024-12-31T23:59" >
<script >
document .getElementById ('birthday' ).valueAsDate = new Date ();
const appointmentInput = document .getElementById ('appointment' );
function setDateTimeInput (input, date ) {
const pad = (n ) => n.toString ().padStart (2 , '0' );
const year = date.getFullYear ();
const month = pad (date.getMonth () + 1 );
const day = pad (date.getDate ());
const hour = pad (date.getHours ());
const minute = pad (date.getMinutes ());
input.value = `${year} -${month} -${day} T${hour} :${minute} ` ;
}
setDateTimeInput (appointmentInput, new Date ());
appointmentInput.addEventListener ('change' , {
date = (e. . );
. ( , date);
. ( , date. ());
});
</script >
Firefox :在 macOS 上 date 和 time 的样式极其简陋,datetime-local 直到 2021 年才支持
Safari 桌面版 :对 datetime-local 的支持也比较晚,老版本直接退化成 text
IE :全军覆没,直接当 text 处理
移动端 :iOS 和 Android 通常能唤起系统原生的日期选择器,体验反而比桌面端好
<input type ="date" id ="birthday" placeholder ="YYYY-MM-DD" onchange ="console.log(this.value)" >
<script >
function isDateInputSupported ( ) {
const input = document .createElement ('input' );
input.setAttribute ('type' , 'date' );
return input.type === 'date' ;
}
if (!isDateInputSupported ()) {
console .log ('浏览器不支持原生 date,需要加载第三方日期库如 flatpickr' );
loadDatePickerPolyfill ();
}
</script >
month / week:冷门但有用,比如做财务报表或排班系统 这两个类型比较冷门,但在特定场景下很方便。month 选择年月,week 选择年周(第几周)。
<label for ="reportMonth" > 报表月份</label >
<input type ="month" id ="reportMonth" name ="reportMonth" min ="2024-01" max ="2024-12" >
<label for ="workWeek" > 工作周</label >
<input type ="week" id ="workWeek" name ="workWeek" >
<script >
const monthInput = document .getElementById ('reportMonth' );
const weekInput = document .getElementById ('workWeek' );
monthInput.addEventListener ('change' , (e ) => {
const [year, month] = e.target .value .split ('-' );
console .log (`选择了 ${year} 年 ${month} 月` );
const firstDay = new Date (year, month - 1 , 1 );
const lastDay = new Date (year, month, 0 );
console .log ('当月范围:' , firstDay, '至' , lastDay);
});
weekInput.addEventListener ('change' , (e ) => {
const value = e.target .value ;
const [year, weekStr] = value.split ('-W' );
const week = parseInt (weekStr);
console .log (`${year} 年第 ${week} 周` );
const firstDayOfYear = new Date (year, 0 , 1 );
const dayOfWeek = firstDayOfYear.getDay ();
const daysToFirstMonday = dayOfWeek <= 1 ? 1 - dayOfWeek : 8 - dayOfWeek;
const firstMonday = new Date (year, 0 , 1 + daysToFirstMonday);
const startOfWeek = new Date (firstMonday);
startOfWeek.setDate (firstMonday.getDate () + (week - 1 ) * 7 );
const endOfWeek = new Date (startOfWeek);
endOfWeek.setDate (startOfWeek.getDate () + 6 );
console .log ('该周从' , startOfWeek, '到' , endOfWeek);
});
</script >
兼容性 :这两个比 date 更惨,IE 和旧版 Safari 都不支持。如果项目需要兼容老浏览器,建议直接用 select 下拉框或第三方组件。
color:点一下弹出调色板,设计师看了直呼内行 color 类型会唤起系统的颜色选择器,返回十六进制颜色值(如 #ff0000)。这个在需要用户自定义主题色、背景色或标注颜色的场景下很有用。
<label for ="themeColor" > 主题色</label >
<input type ="color" id ="themeColor" name ="themeColor" value ="#1890ff" >
<div id ="preview" style ="width: 100px; height: 100px; background: #1890ff; margin-top: 10px;" > 预览区域 </div >
<script >
const colorInput = document .getElementById ('themeColor' );
const preview = document .getElementById ('preview' );
colorInput.addEventListener ('input' , (e ) => {
const color = e.target .value ;
preview.style .backgroundColor = color;
preview.textContent = color;
console .log ('选择的颜色:' , color);
});
</script >
返回值固定为十六进制 ,不能直接获取 RGB 或 HSL,需要转换
无法设置透明度 (没有 alpha 通道),如果需要透明色,得额外加 range 滑块
样式几乎无法自定义 ,颜色选择器是系统原生的,不同操作系统样子完全不同
<div class ="color-picker" >
<input type ="color" id ="baseColor" value ="#1890ff" >
<input type ="range" id ="alpha" min ="0" max ="1" step ="0.01" value ="1" >
<span id ="rgbaValue" > rgba(24, 144, 255, 1)</span >
</div >
<script >
const baseColor = document .getElementById ('baseColor' );
const alpha = document .getElementById ('alpha' );
const rgbaValue = document .getElementById ('rgbaValue' );
function hexToRgb (hex ) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i .exec (hex);
return result ? { r : parseInt (result[1 ], 16 ), g : parseInt (result[2 ], 16 ), b : parseInt (result[3 ], 16 ) } : null ;
}
function updateColor ( ) {
const rgb = hexToRgb (baseColor.value );
const a = alpha.value ;
const rgba = `rgba(${rgb.r} , ${rgb.g} , ${rgb.b} , ${a} )` ;
rgbaValue.textContent = rgba;
document .body .style .backgroundColor = rgba;
}
baseColor.addEventListener ('input' , updateColor);
alpha.addEventListener ('input' , updateColor);
</script >
file:上传入口,accept 属性能限制文件类型 file 类型是最复杂的 input 类型之一,因为它涉及文件系统访问。用户点击后会唤起系统的文件选择对话框,选中文件后可以通过 JavaScript 读取文件信息或内容。
<label for ="avatar" > 上传头像</label >
<input type ="file" id ="avatar" name ="avatar" accept ="image/*" >
<label for ="documents" > 上传资料(可多选)</label >
<input type ="file" id ="documents" name ="documents" multiple accept =".pdf,.doc,.docx" >
<label for ="folder" > 上传文件夹</label >
<input type ="file" id ="folder" webkitdirectory directory >
<script >
const avatarInput = document .getElementById ('avatar' );
const documentsInput = document .getElementById ('documents' );
avatarInput.addEventListener ('change' , (e ) => {
const file = e.target .files [0 ];
if (!file) return ;
console .log ('文件名:' , file.name );
console .log ('文件大小:' , (file.size / 1024 ).toFixed (2 ), 'KB' );
console .log ('文件类型:' , file.type );
if (file.type .startsWith ('image/' )) {
const reader = new FileReader ();
reader.onload = (event ) => {
const img = document .createElement ('img' );
img.src = event.target .result ;
img.style .maxWidth = '200px' ;
document .body .appendChild (img);
};
reader.readAsDataURL (file);
}
});
documentsInput.addEventListener ('change' , (e ) => {
const files = Array .from (e.target .files );
console .log (`选择了 ${files.length} 个文件` );
files.forEach (file => {
const maxSize = 10 * 1024 * 1024 ;
if (file.size > maxSize) {
alert (`${file.name} 超过 10MB,跳过` );
return ;
}
console .log ('处理文件:' , file.name );
});
});
</script >
accept="image/*" 只是提示性的,不能阻止用户选择其他类型文件 (在文件对话框里可以手动切换显示所有文件)
真正校验必须在 JS 里检查 file.type 或文件名后缀
MIME 类型有时候不靠谱(比如 Windows 上某些 .jpg 文件可能被识别为空字符串)
<div id ="dropZone" style ="border: 2px dashed #ccc; padding: 40px; text-align: center;" >
<p > 拖拽文件到这里,或 <label for ="fileUpload" style ="color: blue; cursor: pointer;" > 点击选择</label > </p >
<input type ="file" id ="fileUpload" multiple accept ="image/*" style ="display: none;" >
<ul id ="fileList" > </ul >
</div >
<script >
const dropZone = document .getElementById ('dropZone' );
const fileInput = document .getElementById ('fileUpload' );
const fileList = document .getElementById ('fileList' );
dropZone.addEventListener ('click' , (e ) => {
if (e.target !== fileInput) {
fileInput.click ();
}
});
dropZone.addEventListener ('dragover' , (e ) => {
e.preventDefault ();
dropZone.style .borderColor = '#1890ff' ;
dropZone.style .backgroundColor = '#f0f8ff' ;
});
dropZone.addEventListener ('dragleave' , () => {
dropZone.style .borderColor = '#ccc' ;
dropZone.style .backgroundColor = 'transparent' ;
});
dropZone.addEventListener ('drop' , (e ) => {
e.preventDefault ();
dropZone.style .borderColor = '#ccc' ;
dropZone.style .backgroundColor = 'transparent' ;
const files = Array .from (e.dataTransfer .files );
handleFiles (files);
});
fileInput.addEventListener ('change' , (e ) => {
handleFiles (Array .from (e.target .files ));
fileInput.value = '' ;
});
function handleFiles (files ) {
files.forEach (file => {
if (!file.type .startsWith ('image/' )) {
alert (`${file.name} 不是图片文件` );
return ;
}
const li = document .createElement ('li' );
li.textContent = `${file.name} (${(file.size / 1024 ).toFixed(1 )} KB)` ;
fileList.appendChild (li);
});
}
</script >
checkbox 和 radio:老熟人了,但你真会用 label 绑定吗? 这两个是选择型输入,和前面的文本型完全不同。它们的状态是 checked(布尔值),而不是 value。
<fieldset >
<legend > 选择你的兴趣爱好(可多选):</legend >
<label > <input type ="checkbox" name ="hobby" value ="coding" > 写代码 </label >
<label > <input type ="checkbox" name ="hobby" value ="reading" > 阅读 </label >
<label > <input type ="checkbox" name ="hobby" value ="gaming" > 打游戏 </label >
<label > <input type ="checkbox" name ="hobby" value ="sleeping" > 睡觉 </label >
</fieldset >
<button onclick ="getHobbies()" > 获取选择</button >
<script >
function getHobbies ( ) {
const checkedBoxes = document .querySelectorAll ('input[name="hobby"]:checked' );
const values = Array .from (checkedBoxes).map (cb => cb.value );
console .log ('选中的爱好:' , values);
}
function toggleAll (checked ) {
document .querySelectorAll ('input[name="hobby"]' ).forEach (cb => {
cb.checked = checked;
});
}
document .querySelectorAll ('input[name="hobby"]' ).forEach (cb => {
cb.addEventListener ('change' , (e ) => {
console .log (`${e.target.value} 的状态变为:${e.target.checked} ` );
});
});
</script >
<fieldset >
<legend > 选择支付方式:</legend >
<label > <input type ="radio" name ="payment" value ="alipay" checked > 支付宝 </label >
<label > <input type ="radio" name ="payment" value ="wechat" > 微信支付 </label >
<label > <input type ="radio" name ="payment" value ="card" > 银行卡 </label >
</fieldset >
<script >
function getPayment ( ) {
const selected = document .querySelector ('input[name="payment"]:checked' );
return selected ? selected.value : null ;
}
document .querySelectorAll ('input[name="payment"]' ).forEach (radio => {
radio.addEventListener ('change' , (e ) => {
if (e.target .checked ) {
console .log ('切换到:' , e.target .value );
showPaymentForm (e.target .value );
}
});
});
</script >
很多人写 checkbox 和 radio 时不包 label,或者乱用 id/for,导致点击文字无法切换,体验很差。正确的做法有两种:
<label class ="checkbox-wrapper" >
<input type ="checkbox" name ="agree" >
<span > 我已阅读并同意用户协议</span >
</label >
<input type ="checkbox" id ="agree" name ="agree" >
<label for ="agree" > 我已阅读并同意用户协议</label >
方法 1 的好处是结构更紧凑,且点击文字和点击 checkbox 本身都能触发,不需要额外的 CSS 扩大点击区域。
hidden:默默传值的小透明,后端最爱 hidden 类型不会在页面上显示任何 UI,但它的值会随表单一起提交。常用于传递一些不需要用户看到但需要后端知道的参数,比如用户 ID、表单版本号、CSRF Token 等。
<form id ="orderForm" action ="/submit-order" method ="POST" >
<label > 商品名称:<input type ="text" name ="productName" value ="iPhone 15" > </label >
<label > 数量:<input type ="number" name ="quantity" value ="1" > </label >
<input type ="hidden" name ="userId" value ="12345" >
<input type ="hidden" name ="csrfToken" value ="a1b2c3d4e5f6" >
<input type ="hidden" name ="timestamp" id ="timestamp" >
<button type ="submit" > 提交订单</button >
</form >
<script >
document .getElementById ('timestamp' ).value = Date .now ();
const form = document .getElementById ('orderForm' );
const hiddenInput = document .createElement ('input' );
hiddenInput.type = 'hidden' ;
hiddenInput.name = 'source' ;
hiddenInput.value = 'mobile_web' ;
form.appendChild (hiddenInput);
form.addEventListener ('submit' , (e ) => {
const userId = form.querySelector ('[name="userId"]' ).value ;
if (!userId || userId !== '12345' ) {
e.preventDefault ();
alert ('非法请求' );
}
});
</script >
重要提醒 :hidden 字段只是"看不见",不是"防篡改"。任何懂浏览器的用户都能在开发者工具里修改 hidden 的值。所以永远不要依赖 hidden 字段做安全校验 ,后端必须重新验证这些值是否合法。
submit / reset / button:表单控制三剑客,现在基本被 JS 取代了 这三个按钮类型的 input 在早期的 HTML 表单里很常见,但现代开发中,我们更倾向于用 <button> 标签,因为它更灵活(可以包含图标、文字、HTML 结构),而且不容易和表单提交行为搞混。
<input type ="submit" value ="提交表单" >
<input type ="reset" value ="重置" >
<input type ="button" value ="普通按钮" onclick ="alert('hello')" >
<button type ="submit" > 提交表单</button >
<button type ="reset" > 重置</button >
<button type ="button" onclick ="doSomething()" > 普通按钮</button >
<button type ="submit" >
<svg > </svg >
<span > 提交订单</span >
<small > 预计 2 秒完成</small >
</button >
注意 :如果在 form 里用 <button> 而不写 type,它会默认变成 submit,这可能不是你想要的。所以总是显式写 type="button" 或 type="submit" ,避免意外提交表单。
image:用图片当提交按钮?复古但还能用 这个类型允许你用一张图片作为提交按钮,点击时会提交表单,同时发送点击的坐标(x, y)给后端。听起来很酷,但实际上现在基本没人用了,因为用 CSS 给 button 加背景图更灵活。
<input type ="image" src ="submit-button.png" alt ="提交" width ="100" height ="40" >
<button type ="submit" style ="background:url('submit-button.png') no-repeat; width: 100px; height: 40px; border: none; text-indent: -9999px;" > 提交 </button >
image 类型提交时,URL 会变成 ?x=123&y=45,表示用户点击图片的位置。这个特性在某些特殊场景(比如地图标记)可能有用,但一般表单真用不上。
这些 type 看似好用,其实坑不少 前面讲每个 type 的时候已经穿插了一些坑,这里再集中吐槽几个最让人头大的。
number 在 Safari 里的迷惑行为 Safari 桌面版(特别是 macOS)对 number 类型的处理一直很迷。它不会阻止你在 number 输入框里输入字母,输入框也不会变红或提示错误,但当你尝试获取 value 时,它会返回空字符串。
const input = document .getElementById ('num' );
console .log (input.value );
console .log (input.validity .valid );
这导致如果你不做额外校验,可能会拿到空值却误以为用户没输入。更坑的是,Safari 不会给输入框添加视觉错误状态,用户也不知道自己输错了。
function getNumberValue (inputId ) {
const input = document .getElementById (inputId);
const value = input.valueAsNumber ;
if (input.value === '' || isNaN (value) || !/^\d+(\.\d+)?$/ .test (input.value )) {
return null ;
}
return value;
}
datetime-local 在 Firefox 里的退化 直到 Firefox 93 版本(2021 年 10 月),Firefox 桌面版才支持 datetime-local 类型。在那之前,它直接退化成普通的 text 输入框,用户得手动输入 "2024-01-15T14:30" 这种格式,体验极差。
如果你需要支持老版本 Firefox,必须准备 polyfill:
function checkDateTimeSupport ( ) {
const input = document .createElement ('input' );
input.setAttribute ('type' , 'datetime-local' );
if (input.type !== 'datetime-local' ) {
loadPolyfill ();
}
}
email 不拦"abc@123"这种假邮箱 前面提过,浏览器的 email 校验非常宽松。abc@123 这种没有有效顶级域名的邮箱,很多浏览器认为是合法的(因为 123 可以是内网域名)。
<input type ="email" id ="email" >
<script >
document .getElementById ('email' ).value = 'abc@123' ;
console .log (document .getElementById ('email' ).validity .valid );
</script >
所以前端校验只能防手滑,后端必须严格校验 。后端应该用更严格的正则,比如要求至少有一个点号和后缀。
移动端键盘的诡异差异 同样是 type="number",iOS 会唤起纯数字键盘(带小数点),但某些安卓机(特别是国产定制系统)可能会唤起包含符号的数字键盘,甚至全键盘。
更诡异的是 type="tel",理论上应该唤起电话键盘(带 * 和 #),但 iPad 上有时会唤起普通数字键盘,没有 * 和 #。
建议 :如果键盘类型对体验至关重要(比如纯数字验证码),除了设置正确的 type,还可以加 inputmode 属性作为双重保险:
<input type ="text" inputmode ="numeric" pattern ="[0-9]*" maxlength ="6" >
浏览器自动填充的噩梦 现代浏览器的密码管理器和表单自动填充功能,有时候会让前端开发者崩溃。比如:
浏览器会自动给 input 加黄色背景(表示已填充)
即使用户没点击,浏览器也可能自动填充保存的账号密码
某些情况下,浏览器会把 text 输入框当成用户名来填充
<input type ="text" autocomplete ="off" >
<input type ="password" autocomplete ="new-password" >
<input type ="password" autocomplete ="current-password" >
<input type ="text" autocomplete ="one-time-code" >
但说实话,autocomplete="off" 在现代浏览器里也不是 100% 管用,因为浏览器认为自动填充是"为用户好",有时候会无视这个属性。
真实项目里怎么用才不翻车 好了,吐槽完坑,来点实用的。下面是我从真实项目里总结的几个常见场景的最佳实践。
电商筛选:search + debounce 商品列表页的搜索框,需要即时搜索但又要避免频繁请求。
<div class ="search-box" >
<input type ="search" id ="productSearch" placeholder ="搜索商品名称..." autocomplete ="off" >
<span id ="loading" style ="display: none;" > 搜索中...</span >
</div >
<ul id ="results" > </ul >
<script >
const searchInput = document .getElementById ('productSearch' );
const loading = document .getElementById ('loading' );
const results = document .getElementById ('results' );
let debounceTimer;
let abortController;
searchInput.addEventListener ('input' , (e ) => {
const query = e.target .value .trim ();
clearTimeout (debounceTimer);
if (abortController) {
abortController.abort ();
}
if (query.length === 0 ) {
results.innerHTML = '' ;
return ;
}
if (query.length < 2 ) return ;
debounceTimer = setTimeout (() => {
performSearch (query);
}, 300 );
});
async function performSearch (query ) {
loading.style .display = 'inline' ;
abortController = new AbortController ();
try {
const response = await fetch (`/api/search?q=${encodeURIComponent (query)} ` , {
signal : abortController.signal
});
const data = await response.json ();
renderResults (data);
} catch (err) {
if (err.name === 'AbortError' ) {
console .log ('请求被取消(正常)' );
} else {
console .error ('搜索失败:' , err);
results.innerHTML = '<li>搜索出错,请重试</li>' ;
}
} finally {
loading.style .display = 'none' ;
}
}
function renderResults (data ) {
results.innerHTML = data.map (item => `
<li>
<img src="${item.image} " alt="${item.name} ">
<span>${item.name} </span>
<strong>¥${item.price} </strong>
</li>
` ).join ('' );
}
</script >
登录页:password 加"显示密码"切换 这个前面代码里写过,但值得再强调。移动端输入密码时,因为键盘小、容易输错,给用户一个"显示明文"的选项,能大幅降低输错概率。
另外,iOS 的密码管理器在检测到 type="password" 时会自动提示生成强密码或填充已保存密码,这时候如果你的输入框 name 或 id 不规范,可能会导致填充错误。
密码框的 name 设为 password 或 current-password
新密码(注册页)设为 new-password
不要给密码框加奇怪的 id 比如 pwd-input-123,这会让密码管理器困惑
移动端表单:优先用 tel/email/number 触发合适键盘 移动端表单的黄金法则:每减少一次键盘切换,转化率就能提升一点 。
<input type ="tel" inputmode ="tel" pattern ="[0-9]*" >
<input type ="text" inputmode ="numeric" pattern ="[0-9]*" maxlength ="6" >
<input type ="text" inputmode ="decimal" pattern ="[0-9]*[.]?[0-9]*" >
<input type ="email" inputmode ="email" >
注意验证码用了 type="text" 而不是 number,因为 number 在 iOS 上会带加减按钮,而且长按会显示数字选择器,体验不如纯 text 配合 inputmode="numeric" 好。
上传头像:accept="image/*" 防止用户乱传 <input type ="file" id ="avatar" accept ="image/png,image/jpeg" capture ="user" >
<script >
const avatarInput = document .getElementById ('avatar' );
avatarInput.addEventListener ('change' , async (e) => {
const file = e.target .files [0 ];
if (!file) return ;
const validTypes = ['image/jpeg' , 'image/png' ];
if (!validTypes.includes (file.type )) {
alert ('只支持 JPG 和 PNG 格式' );
avatarInput.value = '' ;
return ;
}
const maxSize = 2 * 1024 * 1024 ;
if (file.size > maxSize) {
alert ('图片不能超过 2MB' );
avatarInput.value = '' ;
return ;
}
const img = new Image ();
img.src = URL .createObjectURL (file);
await new Promise ((resolve ) => {
img.onload = resolve;
});
if (img.width < 200 || img.height < 200 ) {
alert ('图片尺寸至少 200x200 像素' );
avatarInput.value = '' ;
URL .revokeObjectURL (img.src );
return ;
}
console .log ('校验通过,准备上传' );
URL .revokeObjectURL (img.src );
});
</script >
capture="user" 属性在移动端会唤起摄像头直接拍照,而不是从相册选择,适合需要实时拍摄的场景(比如身份证上传)。
遇到奇怪问题?先问这几句灵魂拷问 写了这么多年表单,我总结了一套自检清单。遇到 input 相关 bug 时,按这个顺序排查,能解决 90% 的问题。
用户输的是数字,为啥取出来是字符串? 因为 value 永远是 string! 这是 HTML 规范定的,不管 type 是什么。
const numInput = document .getElementById ('age' );
numInput.value = 25 ;
console .log (typeof numInput.value );
const age = parseInt (numInput.value , 10 );
const age2 = numInput.valueAsNumber ;
安卓上 date 选择器没反应?
机型太老 :Android 4.4 及以下对 date 类型支持很差,需要降级为 text 并引入第三方日期库
WebView 环境 :如果是在 App 的 WebView 里,可能禁用了某些原生组件,需要和原生开发沟通
CSS 问题 :某些样式(如 -webkit-appearance: none)可能会隐藏掉原生选择器的触发按钮
function initDatePicker ( ) {
const input = document .getElementById ('date' );
if (input.type !== 'date' ) {
flatpickr (input, {
dateFormat : 'Y-m-d' ,
minDate : '1900-01-01'
});
}
}
点了 file 没弹窗? 绝对是因为在非用户交互事件里触发的! 浏览器的安全策略要求 file 选择器必须由真实的用户行为(如点击事件)触发,不能在 setTimeout、Promise 回调或异步请求成功后自动触发。
setTimeout (() => {
document .getElementById ('file' ).click ();
}, 1000 );
fetch ('/api/check' ).then (() => {
document .getElementById ('file' ).click ();
});
document .getElementById ('uploadBtn' ).addEventListener ('click' , () => {
document .getElementById ('file' ).click ();
});
如果业务逻辑必须先请求接口再弹窗,那就只能把请求放到弹窗之后,或者引导用户再点一次。
radio 选了却没生效? 99% 是因为 name 属性没统一 。radio 的互斥逻辑是靠相同的 name 实现的,name 不同就不会互斥。
<input type ="radio" name ="pay1" value ="alipay" > 支付宝
<input type ="radio" name ="pay2" value ="wechat" > 微信
<input type ="radio" name ="payment" value ="alipay" > 支付宝
<input type ="radio" name ="payment" value ="wechat" > 微信
另外 1% 是因为 JS 动态创建的 radio 没有正确设置 name,或者放在不同的 form 里(虽然 name 相同,但 form 不同也不会互斥,不过这种情况很少见)。
几个骚操作提升体验 最后分享几个我在项目里用过的、能明显提升体验的小技巧。
用 CSS 隐藏 file 默认样式,自定义上传按钮 原生的 file 输入框样式丑到哭,而且不同浏览器长得完全不一样。通常的做法是隐藏它,用一个好看的 button 来代理。
<div class ="custom-upload" >
<input type ="file" id ="realFile" accept ="image/*" >
<button type ="button" id ="fakeBtn" class ="upload-btn" >
<svg > </svg > 选择图片
</button >
<span id ="fileName" > 未选择文件</span >
</div >
<style >
.custom-upload {
position : relative;
display : inline-flex;
align-items : center;
gap : 10px ;
}
#realFile {
position : absolute;
left : 0 ;
top : 0 ;
width : 100% ;
height : 100% ;
opacity : 0 ;
cursor : pointer;
z-index : 1 ;
}
.upload-btn {
padding : 10px 20px ;
background : #1890ff ;
color : white;
border : none;
border-radius : 4px ;
cursor : pointer;
display : flex;
align-items : center;
gap : 5px ;
transition : background 0.3s ;
}
.upload-btn :hover {
background : #40a9ff ;
}
.custom-upload .has-file .upload-btn {
background : #52c41a ;
}
</style >
<script >
const realFile = document .getElementById ('realFile' );
const fakeBtn = document .getElementById ('fakeBtn' );
const fileName = document .getElementById ('fileName' );
const wrapper = document .querySelector ('.custom-upload' );
realFile.addEventListener ('change' , (e ) => {
if (e.target .files .length > 0 ) {
fileName.textContent = e.target .files [0 ].name ;
wrapper.classList .add ('has-file' );
fakeBtn.innerHTML = '<svg><!-- 成功图标 --></svg> 重新选择' ;
}
});
</script >
关键点:opacity: 0 隐藏 file 输入框,但保留它的点击区域,这样点击漂亮的 button 实际上是点击了透明的 file 输入框。这比用 JS 代理 click 事件更可靠,因为某些浏览器(比如 Safari)对 JS 触发的 file.click() 有限制。
password 输入框加个"眼睛"图标切换明文 前面已经写过完整代码,这里再补充一个细节:切换明文/密文时,应该保持焦点在输入框里,别让用户再点一次。
toggleBtn.addEventListener ('click' , () => {
const isPassword = pwdInput.type === 'password' ;
pwdInput.type = isPassword ? 'text' : 'password' ;
const cursorPos = pwdInput.selectionStart ;
pwdInput.focus ();
pwdInput.setSelectionRange (cursorPos, cursorPos);
});
这个细节很小,但对用户体验影响很大,特别是长密码输到一半想看一下有没有输错的时候。
search 框监听 input 事件实现即时搜索 前面电商筛选的例子里已经用了 debounce,这里再补充一个 UX 细节:搜索结果应该高亮匹配的关键词。
function renderResults (data, query ) {
const regex = new RegExp (`(${escapeRegex(query)} )` , 'gi' );
results.innerHTML = data.map (item => {
const highlightedName = item.name .replace (regex, '<mark>$1</mark>' );
return `
<li>
<span>${highlightedName} </span>
<span>¥${item.price} </span>
</li>
` ;
}).join ('' );
}
function escapeRegex (string ) {
return string.replace (/[.*+?^${}()|[\]\\]/g , '\\$&' );
}
mark 标签是 HTML5 新增的,专门用于标记高亮文本,默认有黄色背景,也可以用 CSS 自定义样式。
用 :placeholder-shown 伪类玩转动态 label 动画 Material Design 风格的浮动标签(Floating Label)现在很流行,就是输入框里的 placeholder 在聚焦或输入时,缩小并移动到输入框上方。这个效果纯 CSS 就能实现,不需要 JS。
<div class ="floating-label" >
<input type ="text" id ="username" placeholder ="" >
<label for ="username" > 用户名</label >
</div >
<style >
.floating-label {
position : relative;
margin-top : 20px ;
}
.floating-label input {
width : 100% ;
padding : 12px ;
border : 1px solid #ccc ;
border-radius : 4px ;
font-size : 16px ;
outline : none;
}
.floating-label label {
position : absolute;
left : 12px ;
top : 50% ;
transform : translateY (-50% );
color : #999 ;
font-size : 16px ;
pointer-events : none;
transition : all 0.2s ease;
background : white;
padding : 0 4px ;
}
.floating-label input :focus ~ label ,
.floating-label input :not (:placeholder-shown ) ~ label {
top : 0 ;
font-size : 12px ;
color : #1890ff ;
}
.floating-label input :focus {
border-color : #1890ff ;
}
</style >
原理::placeholder-shown 伪类在 input 的 placeholder 可见时匹配。我们把 placeholder 设为空格(placeholder=" "),这样:
当 input 为空且失焦时,placeholder 显示(虽然是空格),label 在中间
当 input 有内容或获得焦点时,placeholder 不显示,label 上移
这比用 JS 监听 focus/blur/input 事件要简洁得多,而且性能更好。
最后说句实在话 写到这儿,估计你也看出来了,input 这玩意儿看着就是个小标签,但水深得很。二十多种 type,每种都有兼容性陷阱;移动端和桌面端表现不一样;不同浏览器还有自己的"个性";再加上键盘适配、自动填充、无障碍访问这些现代要求,想把一个表单做得体验好,真没那么简单。
我这些年踩过的坑包括但不限于:以为 type="number" 就万事大吉结果被字符串类型坑了;在安卓机上测试得好好的,到 iOS 上键盘弹不出来;信了浏览器的 email 校验,后端没做二次验证结果被灌了一堆脏数据;还有那个经典的"点击 file 没反应"调试半天发现是 setTimeout 里触发的…
第一,别光背 type 列表,多在真机上跑。 手头常备几部测试机,或者至少用 BrowserStack 这类工具测测。Chrome 开发者工具的设备模拟只能模拟尺寸,模拟不了真正的键盘行为和系统组件。
第二,渐进增强,做好降级方案。 想用 date 选择器?没问题,但先检测支不支持,不支持就加载第三方库。想用 color 调色板?可以,但留一个 text 输入框作为后备,让用户也能手动输十六进制色值。
第三,后端校验永远是最后一道防线。 前端做校验是为了即时反馈、提升体验,但永远不要相信前端传上去的数据。所有从 input.value 拿到的字符串,后端都要重新校验类型、格式、范围。
第四,关注无障碍。 给输入框加上 label,给错误提示加上 aria-live 区域,给图标按钮加上 aria-label。这些对普通用户没影响,但对使用读屏软件的视障用户就是能不能用的区别。
下次再遇到产品经理说"这个输入框很简单,半天能做完吧",你就把这篇文章甩他脸上——开玩笑的,别真甩,毕竟还得继续合作。但你可以心平气和地给他讲讲这里面的门道,说不定还能争取到更合理的排期。
毕竟,咱们前端工程师的尊严,有时候就藏在这些"看似简单"的细节里。
(e ) =>
const
new
Date
target
value
console
log
'选择的日期时间:'
console
log
'时间戳:'
getTime
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online