先说个真实感受:用过 Element UI 的人,十个里有八个在el-date-picker上卡过壳,尤其是“限制选择范围”这种需求。刚接手时觉得简单——不就加个 disabled 嘛,实际一写才发现边界条件多到怀疑人生:开始日期选了之后结束日期没跟着变、禁用逻辑在切换月份时失效、时间戳格式把日期判断带偏……这篇文章我把这些年攒下的踩坑记录、封装思路和排查套路整理出来,从最基础的 disabled-date 原理讲到复杂联动场景,新手能照着抄,老手也能从中排查到自己忽略的细节。
1. 先搞懂禁用日期的底层逻辑
1.1 disabled-date 到底是个什么东西
el-date-picker 的日期禁用能力,官方文档只给了一句“某个日期是否被禁用”,但很多人忽略了一个关键前提:它是一个回调函数,组件渲染每一个日期格子时都会调用它。回调接收一个 Date 对象作为参数,返回 true 表示该日期不可选,返回 false 表示可选。
理解这一点非常重要。有些人误以为 disabled-date 只在打开面板时执行一次,实际上它会在以下几种时机反复触发:
- 首次打开面板渲染当前月份所有日期时
- 点击上一月/下一月切换月份时
- 选择开始日期后,重新校准结束日期可选范围时
- 清空已选日期后恢复默认状态时
这解释了为什么代码里如果写了复杂的计算逻辑(比如依赖某个响应式变量),那个变量变化时面板会自动重新校验。但也带来了性能隐患:如果你的禁用逻辑里做了大量耗时的计算,每次渲染都会卡顿。
// 基础模板 <el-date-picker v-model="dateValue" type="date" :disabled-date="disabledDate" placeholder="选择日期" />// 最简单的禁用逻辑:只允许今天和今天之前 const disabledDate = (date) => { return date.getTime() > Date.now(); };这里有个很多人没发现的细节:date 对象的时间是当前时区的零点。比如你选 2025-03-15,传入 disabledDate 的日期对象是2025-03-15 00:00:00,但Date.now()是当前这一刻的毫秒时间戳。如果用户在当前日期的下午操作,2025-03-15 00:00:00的 getTime() 一定小于Date.now(),所以今天仍然可选。但如果你比较的是date.getTime() < Date.now()而逻辑写反成了<,今天就会被禁用。这种边界问题靠眼睛看很难发现,必须用不同时段实测。
1.2 时间戳比较是最不容易出错的方式
很多新手喜欢比较年月日:
// 不推荐:字符串拼出来比较,格式一旦变化就翻车 const disabledDate = (date) => { const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); const dateStr = `${year}-${month}-${day}`; return dateStr < '2025-01-01'; };字符串比较规则极其脆弱。'2025-1-1'和'2025-01-01'在字典序上就不同,而且如果后端返回的时间格式不是YYYY-MM-DD,这种判断瞬间失效。
我推荐统一用时间戳:
const disabledDate = (date) => { // 零点对齐:先把目标日期转成当天零点 const today = new Date(); today.setHours(0, 0, 0, 0); return date.getTime() < today.getTime(); // 禁用今天之前的日期 };核心思路:对比前先把毫秒、秒、分、小时全部归零,只保留年月日维度。这个操作能规避掉几乎所有时间颗粒度不一致的坑。
重要提示:如果你用
new Date('2025-03-15')这种方式构造日期,某些浏览器会解析成 UTC 时间而非本地时间,导致日期偏移一天。稳妥做法是new Date(2025, 2, 15),注意月份从 0 开始。
2. 五个高频业务场景的完整实现
2.1 只允许选择过去日期(历史数据录入场景)
业务背景:比如某个系统的补录功能,只能补录已经发生的数据,不允许选择未来日期作为业务发生时间。
export const disabledPastDate = (date) => { const today = new Date(); today.setHours(0, 0, 0, 0); return date.getTime() < today.getTime(); };如果需求是“只能选昨天及更早”,把今天零点和昨天零点比较一下:
const disabledDate = (date) => { const today = new Date(); today.setHours(0, 0, 0, 0); const yesterday = today.getTime() - 24 * 60 * 60 * 1000; return date.getTime() >= today.getTime() || date.getTime() < yesterday; };这里要注意,不要直接对today调用setDate(today.getDate() - 1)再比较,因为有夏令时等时区因素的极端场景存在时,直接减 24 小时也可能有偏差。不过国内项目基本不涉及夏令时,用 24 小时毫秒数的做法安全可靠。
2.2 只允许选择未来日期(预约/排期场景)
预约类系统很常见的需求:不允许用户选择已经过去的时间。单独使用日期类型比较简单:
const disabledDate = (date) => { const today = new Date(); today.setHours(0, 0, 0, 0); return date.getTime() < today.getTime(); };但注意预约类系统往往还带时间选择,也就是 type 是datetime。此时问题变得复杂:假设现在时间是 2025-03-15 14:30,用户选择的日期是今天,但时间选了上午 09:00,这个时间已经过去了。限制日期只能保证日期维度合法,无法限制时间维度。
处理办法是把日期时间合并判断:
const disabledDateTime = (date) => { // date 是 Date 类型,包含时分秒 return date.getTime() < Date.now(); };但disabled-date对 datetime 类型处理的是日期粒度,不能精确禁用“今日已过的某个时间点”,只能在点击对应时间格时通过selectable-range来限制。Element UI 的 datetime 类型有:disabled-time这个属性:
const disabledTime = (date) => { if (date && date.getDate() === new Date().getDate()) { return { disabledHours: () => range(0, new Date().getHours()), disabledMinutes: () => range(0, new Date().getMinutes()), disabledSeconds: () => range(0, new Date().getSeconds()) }; } return {}; };这里逻辑的核心是:先判断日期是否是今天,再分别禁用已经过去的时、分、秒。需要注意边界情况,比如当前是 14:30:45,那么 14 点整不能禁用,因为 14:00 已经过去了,但如果允许选择 14:30 之后的分钟,规则又要调整。实际项目里最常见的处理是禁用“已经过去的整个小时”,避免分钟秒的复杂判断。
2.3 限制在一个固定区间内
比如只能选择 2025-01-01 到 2025-12-31 之间的日期。
const disabledDate = (date) => { const start = new Date(2025, 0, 1).getTime(); const end = new Date(2025, 11, 31).getTime(); const current = date.getTime(); return current < start || current > end; };有一个重要经验:把开始和结束时间戳在 data 里缓存,而不是在 disabledDate 回调里重复构造。因为回调在渲染每个格子时都会触发,如果面板一次渲染 42 个格子(6 周),那就是 42 次new Date(),性能损耗叠加起来很明显。定义在 computed 或 data 里只会计算一次。
2.4 动态联动:开始日期不能晚于结束日期
这是最常见也是最容易写错的需求。两个日期选择器,A 选择开始日期,B 选择结束日期,B 的可选范围要动态跟随 A 的值变化。
先给一个常见的反面教材:
// 错误示范 <el-date-picker v-model="startDate" @change="startChange" /> <el-date-picker v-model="endDate" :disabled-date="endDisabled" /> endDisabled(date) { return date.getTime() < this.startDate.getTime(); }这个写法问题很多:@change只会在日期变化且校验通过后触发,如果清空了日期就不会触发;而且this.startDate可能是格式化后的字符串'2025-03-15',直接调getTime()会报错。
正确写法:
<template> <div> <el-date-picker v-model="form.startDate" type="date" placeholder="开始日期" :disabled-date="startDisabled" @change="handleStartChange" /> <el-date-picker v-model="form.endDate" type="date" placeholder="结束日期" :disabled-date="endDisabled" /> </div> </template> <script> export default { data() { return { form: { startDate: '', endDate: '' }, minDate: null, // 用来动态缓存开始日期 }; }, computed: { startDisabled() { // 这里返回一个函数,注意不能直接给 disabled-date 传箭头函数再访问 this return (date) => { // 开始日期可以限制今天之后,根据业务需求调整 return false; }; } }, watch: { 'form.startDate': function (newVal) { if (newVal) { // 统一转为 Date 类型保存 this.minDate = new Date(newVal); // 如果开始日期晚于当前结束日期,清空结束日期 if (this.form.endDate && new Date(newVal) > new Date(this.form.endDate)) { this.form.endDate = ''; } } else { this.minDate = null; } } }, methods: { endDisabled(date) { if (this.minDate) { return date.getTime() < this.minDate.getTime(); } return false; } } }; </script>这里有几个关键决策:
- 用 watch 而不是 @change:watch 能覆盖清空操作,@change 只在用户完成选择时触发一次,清空时不会触发。这是很多人踩的坑。
- minDate 单独缓存:直接用
this.form.startDate也行,但需要注意它可能是字符串格式。单独缓存成 Date 对象,后续判断更稳定。 - 联动清空结束日期:这是业务上必须的处理,否则用户先选了开始日期 2025-05-10,又选了结束日期 2025-01-01,结束日期已经超出可选范围,但组件不会自动清空,后面提交表单校验时就乱了。
2.5 排除特定日期(节假日、周末、已约日期)
这种需求在选座、预约、会议室预订系统里很常见:周末不能选,某些特殊日期(比如已经约满的日期)不能选。
const disabledDates = [new Date(2025, 2, 18), new Date(2025, 2, 20)]; // 模拟已约满 const disabledDate = (date) => { const target = date.getTime(); // 周末禁用 const day = date.getDay(); if (day === 0 || day === 6) return true; // 特殊日期禁用 const hit = disabledDates.some(item => item.getTime() === target); return hit; };需要注意:disabledDates 里的日期要精确到天零点,否则同一日期存了不同时间点,用 getTime 比对就会出错。用new Date(2025, 2, 18)构造的日期默认是零点,服务端返回的日期字符串'2025-03-18'如果直接 new Date 解析,在部分浏览器中是 UTC 零点,换算成本地时区可能变成前一天下午四点多,判断时会出现奇怪偏差。建议用new Date(item.replace(/-/g, '/'))这种兼容做法,或手动 split 解析。
3. 表格里的多个日期选择器,避免闭包陷阱
3.1 现象:每一行的日期禁用逻辑全乱了
表格每行都有一个日期选择器,且每行的限制范围不同(比如行 A 限制从 2025-01-01 开始,行 B 限制从 2025-02-01 开始)。直接在循环里写:
<el-table-column v-for="item in list" :key="item.id"> <el-date-picker :disabled-date="(date) => date.getTime() < new Date(item.minDate).getTime()" /> </el-table-column>这里的作用域闭包其实没问题,但潜在隐患是item.minDate变化后,模板重新渲染,每个选择器的禁用逻辑都会重新声明,组件可能会复用一个日期面板,导致一些状态错乱。另外匿名函数每次渲染都是新引用,Element UI 内部某些版本对处理不严谨时会出现“面板打开了,但禁用不生效”的怪现象。
3.2 正确做法:用统一方法传入行数据
<el-table-column v-for="item in list" :key="item.id"> <el-date-picker :disabled-date="(date) => tableDisabledDate(date, item)" /> </el-table-column>methods: { tableDisabledDate(date, row) { if (!row.minDate) return false; return date.getTime() < new Date(row.minDate).getTime(); } }但这里还是每次渲染生成新函数,性能上依然不太好。更彻底的做法是用工厂函数,在 data 初始化时一次性生成禁用函数:
created() { this.list = this.list.map(item => { const disabledDate = (date) => { if (!item.minDate) return false; return date.getTime() < new Date(item.minDate).getTime(); }; return { ...item, disabledDate }; }); }模板中绑定:disabled-date="item.disabledDate",函数引用固定,不会因为模板重新渲染而变化。
实测中这种写法稳定性非常高,尤其表格行数据多、翻页刷新频繁的场景,明显能减少面板重新计算的次数。
4. 实战中的坑与排查方案
4.1 默认时间是当天零点,比较结果和你想象的不一样
这是出现频率最高的“灵异事件”。面板上明明显示 15 号是灰色禁用的,代码逻辑看起来也没问题,打开控制台一调试才发现:date 对象是2025-03-15 00:00:00,而你的限制条件是date.getTime() < someTime,如果 someTime 是2025-03-15 08:30:00,那么 15 号零点小于 8 点半,被禁用了。
场景复现:你希望“今天 8 点半之后不能选今天”,但按日期纬度处理,发现今天整个被禁用了。
排查思路:在 disabledDate 里临时加点日志:
const disabledDate = (date) => { console.log('current date:', date, 'timestamp:', date.getTime()); console.log('limit timestamp:', this.deadline); return date.getTime() > this.deadline; };看到打印出来的粒度差异,基本就明白问题了。解决方式前面提过:比较前统一归零,或者明确业务上按小时还是按天作为粒度。
4.2 value-format 引发的类型混乱
Element UI 的value-format指定 v-model 的值格式,比如YYYY-MM-DD。一旦设置了这个属性,v-model 拿到的就是字符串,而不是 Date 对象。此时在 disabledDate 里做date.getTime()没问题,因为回调里的 date 永远是 Date,但如果你在 watch 里对 v-model 值调用getTime(),就会直接报错xxx.getTime is not a function。
建议的规范做法:v-model 存储层统一用字符串格式,便于后端直接接收;但在计算和比较时一律 new Date 转一次。同时注意new Date('2025-03-15')在 iOS 上有解析兼容问题(会返回 Invalid Date),需要替换横杠:
const parseDate = (str) => new Date(str.replace(/-/g, '/'));这个方法虽然看起来土,但在各种 Web 环境里实测最稳,比dayjs(str)还省事。这个坑在移动端尤其明显,PC 浏览器基本没问题,因为标准实现统一了。
4.3 短时间切换面板月份导致“禁用延迟”
表格里多个日期选择器,用户快速切换月份,偶尔出现某些日期没被禁用,刷新页面后又正常。这种问题根源在于 disabled-date 是异步配合面板渲染的,快速操作时函数引用重新绑定,渲染过程丢帧。
最直接的规避方式:避免在 disabledDate 中依赖动态变化且计算成本高的表达式,把它改成 data 里预计算好的数组或布尔值。比如:
data() { // 比如把禁用范围一次性算好 const blockedRanges = [...]; return { blockedRanges }; }, methods: { disabledDate(date) { const ts = date.getTime(); return this.blockedRanges.some(range => ts >= range.start && ts <= range.end); } }这样 disabledDate 每次只做一次数组遍历,不存在响应式依赖,执行极快且稳定。
4.4 联动重置时结束日期没清空
前面联动场景提到过,开始日期改变后,旧的结束日期可能超出新范围。如果不清空,数据看板上会出现“开始日期晚于结束日期”的非法组合。
处理时要考虑清空策略:
- 如果新开始日期晚于当前结束日期,清空结束日期。
- 如果新开始日期早于当前结束日期,保留结束日期,因为仍然合法。
- 如果清空了开始日期,结束日期连带清空。
watch: { 'form.startDate'(newVal) { if (!newVal) { this.form.endDate = ''; return; } if (this.form.endDate) { if (new Date(newVal) > new Date(this.form.endDate)) { this.form.endDate = ''; } } } }4.5 校验规则与 disabled-date 双重拦截
很多项目会给日期字段加表单校验(必填、范围等),但发现 disabled-date 限制了日期后,原来的范围校验反而触发了,导致报错提示“请输入有效日期”。原因是用户在禁用状态下无法选到非法值,但组件内部校验链仍然在走。
处理办法是:表单校验规则里不要再写范围限制(比如validator: checkDateRange),因为 disabledDate 已经在前端做了强控制。如果后端还有一层校验(比如提交后校验日期是否重复),前端表单的校验应该只负责“必填”和“格式”。否则用户选不到非法日期,却一直收到非法提示,体验极差。这个同步问题很难在代码层面定位,经验上优先排查表单 rule 和 disabledDate 是否功能重复。
5. 进阶技巧:让限制范围的体验更顺滑
5.1 配合 shortcus 提供合规的快捷选项
限制范围之后,用户手动翻日期到目标月份很麻烦。例如“只能选最近 7 天”,如果用户不在当前月份,要往前翻很久。这时候自定义 shortcuts 体验会好很多,而且要注意快捷选项生成的值必须落在限制范围内。
const shortcuts = [ { text: '今天', onClick(picker) { const today = new Date(); picker.$emit('pick', today); } }, { text: '最近3天', onClick(picker) { const today = new Date(); const start = new Date(today); start.setDate(start.getDate() - 2); picker.$emit('pick', [start, today]); } } ];如果快捷选项生成的日期不在 disabledDate 的允许范围内,Element UI 不会主动拦截,选上后再次打开面板日期会显示为红色或异常状态。所以自定义快捷选项前必须反复确认它的范围与 disabledDate 限制一致。
5.2 使用 onPick 解决范围选择器的体验问题
如果用的是 type="daterange" 这种双输入框的模式,限制范围是一个整体区间,比如只允许选择过去的连续区间。此时有两个思路:
- 用 range-separator 和快捷选项辅助。
- 用
onPick回调,在用户点选了开始日期之后,临时改变结束日期可选范围。
Element UI 在 daterange 模式下,element 内部的禁用逻辑由组件自己管理,我们再加 disabledDate 时要注意:disabledDate 对开始日期和结束日期都会生效。如果你限制了“不能选未来”,那么用户点选开始日期在今天之前,结束日期也只能在今天之前,会导致“无法跨过今天”的诡异现象。
例如:今天是 2025-03-15,用户想选择 3 月 10 日到 3 月 18 日,因为 18 日是未来,结束日期被禁用,导致这段区间永远选不了。业务上这可能不是想要的——往往希望开始日期限制在过去,但结束日期可以是未来。
这种需求就不能用 disabledDate 统一拦截,需要 split 成两个单独的 date-picker 来处理,或者对日期范围做“开始不晚于今天”的校验,而不是“全部日期不能选未来”。这块要结合业务想清楚:限制的是区间两端,还是区间内的每一个点。这个差别决定了实现方案完全不同。
5.3 非连续禁用改成标记提示,而非硬禁
有些场景不适合硬性禁用,比如排班系统,某个时间段已经被占用了,但如果直接禁用,用户体验上容易困惑(为什么这个日期不能选?)。此时更适合在面板下方或 tooltip 中给出说明文字。
Element UI 官方贴吧/社区里常用方案是:不用 disabledDate,而是在选中后校验,再用picker-options下方加一段自定义提示。虽然不能全局控制提示,但可以在表单校验消息中写明“该日期已被约满,请选择其他日期”,比看灰掉更友好。
5.4 性能优化:日期范围大到几千天时
有一种限制是“可提前 365 天预约”,年份跨度大。面板翻动时 disabledDate 计算量很大。实测下来,用Array.prototype.some遍历一个较大数组在低端移动端上会有明显卡顿。优化方向:
- 将禁用日期集合转成一个 Set,存时间戳,判断时用
has。 - 把日期范围判断简化为
>= start && <= end,不要用 Array。 - 若禁用规则是动态的,考虑用 computed 缓存中间结果。
const disabledSet = computed(() => { const set = new Set(); // 假设从接口拿到的 disabledList disabledList.value.forEach(item => set.add(item.getTime())); return set; }); const disabledDate = (date) => disabledSet.value.has(date.getTime());实测数据:500 个禁用日期的场景,用 Set 之后面板切换渲染速度从 300ms 左右降到 30ms 以内,体感完全不一样。
6. 常见问题排查速查表
这里把实际项目里反复出现过的问题做一个速查表,方便直接对照检查:
| 表现 | 可能原因 | 解决方向 |
|---|---|---|
| 今天被误禁用 | 时间比较未归零或日界线错误 | 统一 setHours(0,0,0,0) |
| 禁用逻辑不生效 | disabled-date 函数引用每次变化 | 改为 data/computed 定义 |
| v-model 报 getTime 错误 | value-format 声明为字符串 | new Date 转换后再比较 |
| iOS 下 Invalid Date | 日期字符串含横杠 | replace(/-/g, '/') |
| 表格多行禁用乱 | 模板匿名闭包函数 | 提前在数据中绑定 disabledDate |
| 开始日期晚于结束日期 | change 事件未覆盖清空 | watch + 联动清空 |
| 快捷选项落在禁用区 | shortcuts 与 disabledDate 不一致 | 生成前校验范围 |
| 面板切换卡顿 | 禁用判断逻辑复杂 | Set 缓存 + 简化条件 |
还有一个特别隐蔽的情况:如果 disabledDate 中依赖的某个响应式变量在渲染过程中被修改(比如异步请求返回了新的禁用列表),面板正在打开状态时,列表更新会触发组件重新渲染,此时可能出现面板关闭又瞬间打开,或者日期点击后没有反应。遇到这种情况,先确认是否有异步更新,如果有,用nextTick延迟处理。
这个坑我在某个智能预约系统里排查了整整两天。当时的场景是从服务端拉取会议室预定时间列表,返回后赋值给 blockedDates,然后用户点击日期输入框瞬间,面板渲染逻辑刚跑完一半,响应式数据变了,组件重置了选择状态。最终解决办法是在赋值前判断一下面板是否处于打开状态,或者干脆在打开面板的下一个 tick 再赋值,规避渲染竞争。
7. 封装一个通用限制指令还是组件
7.1 项目里多个页面用了相同限制逻辑
如果你多个页面都遇到“开始不晚于今天”“开始不晚于结束”这种需求,别再每次复制粘贴了。我倾向于封装一个基于 el-date-picker 的轻量组件,只暴露出业务需要的属性:
<template> <el-date-picker v-model="innerValue" type="date" :disabled-date="mergedDisabledDate" :placeholder="placeholder" @change="handleChange" /> </template> <script> export default { name: 'LimitedDatePicker', props: { value: String, minDate: Date, maxDate: Date, placeholder: String, // 扩展禁用规则 extraDisabled: Function }, computed: { innerValue: { get() { return this.value; }, set(val) { this.$emit('input', val); } }, mergedDisabledDate() { return (date) => { if (this.minDate && date.getTime() < this.minDate.getTime()) { return true; } if (this.maxDate && date.getTime() > this.maxDate.getTime()) { return true; } if (this.extraDisabled && this.extraDisabled(date)) { return true; } return false; }; } }, methods: { handleChange(val) { this.$emit('change', val); } } }; </script>这个封装思路最大的好处是:把禁用判定收敛到一个函数里,新增规则只加一行,不会带偏原有逻辑。另外提醒一下,组件的 v-model 转发要注意类型一致,父组件如果是字符串格式,内部不要偷偷转成 Date,否则提交时数据格式会不一致。
7.2 大量表单项时的公共参数统一
在大型项目中,可能几十个日期字段都是“只能选过去日期”。建议把常用的限制函数抽到一个公共文件统一导出一个配置工厂:
// dateLimitFactory.js export const onlyPast = () => { return (date) => { const today = new Date(); today.setHours(0, 0, 0, 0); return date.getTime() < today.getTime(); }; }; export const onlyFuture = () => { return (date) => { const today = new Date(); today.setHours(0, 0, 0, 0); return date.getTime() > today.getTime(); }; };这样在 template 中写:
<el-date-picker :disabled-date="onlyPast()" />但要注意:模板里直接调用函数会每次渲染都执行工厂函数,产生的匿名函数引用每次不同,又可能踩回前面“函数引用变化”的问题。正确做法是在数据里先定义好:
data() { return { disabledPast: onlyPast() }; }模板绑定:disabled-date="disabledPast",这样只执行一次。
8. 我个人的几个实操习惯
最后分享几个长期形成的习惯,不一定适合所有团队,但在我做过的项目里确实省了不少事。
第一,所有日期限制函数一律返回布尔值,不做隐式转换。有人喜欢写return date.getTime() < start && date.getTime() > end这种,逻辑看着没问题,但一旦 start 或 end 是字符串、null 或 undefined,表达式会静默变成 NaN 比较,结果是 false,日期全部可点。接口字段定义不规范时这种 bug 特别难查。保险做法是先把 minDate 和 maxDate 统一校验一遍,非法值直接不限制。
第二,限制条件优先从业务视角写注释。日期限制逻辑往往藏着业务规则,比如“为什么不能选 3 月 18 号”,如果没有注释,半年之后自己回来看代码,很可能把这个判断当成 bug 删掉。我在每次禁用函数上方都会写清楚业务原因的注释,至少包含业务场景和特殊例外。
第三,和 UI 确认清楚禁用后的交互预期。有的产品经理希望禁用日期隐藏(不显示),有的希望置灰,有的希望有 tooltip 提示原因。Element UI 的 disabled-date 只能置灰,没有隐藏或提示能力。如果需要提示,就要另加自定义渲染,或者换成第三方组件。这里一定要提前沟通确认,避免开发完成才发现交互不符合预期,白白返工。
日期范围限制这块,看起来只是几十行代码的小功能,但实际上牵扯到组件渲染机制、响应式更新时机、时区处理、业务边界定义、性能优化等多个层面。把这些经验沉淀成模板和公共函数,后续再遇到类似需求,基本就是复制粘贴加微调的事,这也是最初写这篇文章的初衷——让后来的人少趟几个我看过的坑。