问题背景
在前端开发中,我们经常会遇到一些UI组件的交互不够友好的情况。最近,我们的产品经理提出了一个关于饿了么UI(Element UI)月份范围选择器的交互问题:
现状问题
当使用Element UI的月份范围选择器,并且设置为只能选择历史月份时,默认显示的是当前年份在左边,下一年在右边。这导致了一个问题:当业务需要选择去年的月份时,用户需要多点一下"去年"按钮,操作不够直观。
期望效果
产品经理希望优化这个交互,让默认显示效果变为:左边显示去年,右边显示当前年,这样用户就可以直接选择去年的月份,无需额外操作。
解决方案探索
这个问题看似简单,但实际上Element UI并没有暴露相关的配置属性来控制这个行为。我们有几种可能的解决方案:
-
直接复制一份源码:这是最直接的方法,但对于一个高级前端开发来说,这种方式不够优雅,而且难以维护。
-
使用CSS或JS hack:通过CSS或JavaScript来修改组件的行为,但这种方式可能会在组件版本更新时失效。
-
使用pnpm打补丁:这是一种更优雅的方式,通过修改依赖包的源码并创建补丁,既可以解决问题,又可以在依赖更新时重新应用补丁。
我们选择了第三种方案,使用pnpm来给Element UI源码打补丁。下面是详细的实现过程。
准备工作
1. 环境要求
在开始之前,确保你的电脑上已经安装了以下软件:
- Node.js 14+(可以在Node.js官网下载安装)
- pnpm 6+(可以通过
npm install -g pnpm命令安装) - Element UI 2.15.14(我们将在后面的步骤中安装)
2. 安装依赖
首先,打开命令行工具(Windows系统可以使用CMD或PowerShell,Mac系统可以使用Terminal),进入你的项目目录,然后运行以下命令安装Element UI:
# 进入项目目录
cd 你的项目目录
# 安装Element UI 2.15.14
pnpm add element-ui@2.15.14
安装完成后,你可以在项目的 package.json 文件中看到Element UI已经被添加到依赖中。
调试Element UI源码
1. 定位相关代码
要修改月份范围选择器的默认显示行为,我们需要找到相关的源码。
首先,打开你的项目文件夹,找到 node_modules 目录,然后进入 element-ui 文件夹,再进入 lib 目录,找到 element-ui.common.js 文件。这个文件包含了Element UI的所有组件代码,但它是压缩过的,看起来会比较乱。
不过别担心,我们可以使用文本编辑器的搜索功能来找到我们需要的代码。打开 element-ui.common.js 文件,然后搜索 month-range 或者 calcDefaultValue,你会找到月份范围选择器的相关代码。
2. 分析代码逻辑
找到 calcDefaultValue 函数后,我们来分析一下它的逻辑:
var month_rangevue_type_script_lang_js_calcDefaultValue = function calcDefaultValue(val) {
var _calcDefaultValue = calcDefaultValue(val),
left = _calcDefaultValue[0],
right = _calcDefaultValue[1];
this.leftDate = left;
this.rightDate = val && val[1] && left.getFullYear() !== right.getFullYear() && this.unlinkPanels ? right : Object(date_util_["nextYear"])(this.leftDate);
};
这段代码的逻辑是:
- 计算默认值,得到左右两个日期
- 将左边日期(
this.leftDate)设置为当前日期 - 将右边日期(
this.rightDate)设置为下一年
这就是为什么默认显示当前年在左边,下一年在右边的原因。
3. 确定修改方案
我们需要修改这段代码,添加一个 history 属性,当该属性为 true 时,默认将左侧日期设置为上一年,右侧日期设置为当前年。这样,当用户打开月份范围选择器时,就可以直接看到去年和今年的月份,无需额外点击"去年"按钮。
打补丁过程
1. 安装patch-package
首先,我们需要安装 patch-package 工具,用于创建和应用补丁。打开命令行工具,进入项目目录,运行以下命令:
# 进入项目目录
cd 你的项目目录
# 安装patch-package作为开发依赖
pnpm add -D patch-package
安装完成后,你可以在项目的 package.json 文件中看到 patch-package 已经被添加到开发依赖中。
2. 修改源码
现在,我们需要修改Element UI的源码。按照之前的步骤,找到 node_modules/element-ui/lib/element-ui.common.js 文件,定位到 calcDefaultValue 函数,然后进行修改:
修改前的代码:
var month_rangevue_type_script_lang_js_calcDefaultValue = function calcDefaultValue(val) {
var _calcDefaultValue = calcDefaultValue(val),
left = _calcDefaultValue[0],
right = _calcDefaultValue[1];
this.leftDate = left;
this.rightDate = val && val[1] && left.getFullYear() !== right.getFullYear() && this.unlinkPanels ? right : Object(date_util_["nextYear"])(this.leftDate);
};
修改后的代码:
var month_rangevue_type_script_lang_js_calcDefaultValue = function calcDefaultValue(val) {
var _calcDefaultValue = calcDefaultValue(val),
left = _calcDefaultValue[0],
right = _calcDefaultValue[1];
if (val) {
this.leftDate = this.history ? Object(date_util["prevYear"])(left) : left;
this.rightDate = val && val[1] && left.getFullYear() !== right.getFullYear() && this.unlinkPanels ? right : Object(date_util["nextYear"])(this.leftDate);
} else {
if (!this.history) return;
// 确保value存在且是数组
if (!this.value || !Array.isArray(this.value)) return;
const [start, end] = this.value.map(date => new Date(date).getFullYear());
if (start !== end) return;
this.leftDate = Object(date_util["prevYear"])(this.leftDate);
this.rightDate = Object(date_util["nextYear"])(this.leftDate);
}
};
3. 创建补丁
修改完成后,保存文件,然后回到命令行工具,运行以下命令创建补丁:
# 创建补丁
pnpm patch element-ui
运行这个命令后,pnpm会提示你输入补丁的描述,你可以输入一个简短的描述,比如"优化月份范围选择器默认显示行为",然后按回车键。
这会创建一个补丁文件,位于 patches/element-ui@2.15.14.patch。
4. 补丁产物比对
补丁文件是一个文本文件,它记录了我们对Element UI源码的修改。打开 patches/element-ui@2.15.14.patch 文件,你会看到类似以下的内容:
diff --git a/lib/element-ui.common.js b/lib/element-ui.common.js
index 5631486796d454e650722ee90ed15472cc703643..851671b16f4b79d632a7113aed92f89b18560ef2 100644
--- a/lib/element-ui.common.js
+++ b/lib/element-ui.common.js
@@ -20394,8 +20394,14 @@ var month_rangevue_type_script_lang_js_calcDefaultValue = function calcDefaultVa
left = _calcDefaultValue[0],
right = _calcDefaultValue[1];
- this.leftDate = left;
+ this.leftDate = this.history ? Object(date_util["prevYear"])(left) : left;
this.rightDate = val && val[1] && left.getFullYear() !== right.getFullYear() && this.unlinkPanels ? right : Object(date_util["nextYear"])(this.leftDate);
+ }else{
+ if(!this.history) return;
+ const [strat, end] = this.value.map(date => new Date(date).getFullYear());
+ if(strat!==end) return;
+ this.leftDate = Object(date_util["prevYear"])(this.leftDate);
+ this.rightDate = Object(date_util["nextYear"])(this.leftDate);
+ }
}
},
这个补丁文件清晰地展示了我们的修改:
- 我们添加了一个
history属性的判断 - 当
history为true时,将左侧日期设置为上一年 - 当
history为true且值的年份相同时,也会将左侧日期设置为上一年,右侧日期设置为下一年
通过补丁文件,我们可以清楚地看到修改前后的代码差异,这对于后续的维护和版本更新非常重要。
5. 配置自动应用补丁
为了确保每次安装依赖时都能自动应用补丁,我们需要在 package.json 文件中添加一个 postinstall 脚本:
打开项目的 package.json 文件,找到 scripts 部分,添加以下代码:
"scripts": {
"postinstall": "patch-package"
}
这样,当运行 pnpm install 时,补丁会自动应用。
6. 使用修改后的组件
现在,我们可以在代码中使用修改后的月份范围选择器,并通过 history 属性来控制默认显示行为:
修改后的demo代码:
<template>
<div class="app">
<h1>月份范围选择器优化示例</h1>
<!-- 普通模式 -->
<div class="demo-item">
<h3>普通模式(默认)</h3>
<el-date-picker
v-model="dateRange1"
type="monthrange"
range-separator="至"
start-placeholder="开始月份"
end-placeholder="结束月份"
:picker-options="{ disabledDate: (time) => time.getTime() > Date.now() }"
/>
</div>
<!-- 历史模式 -->
<div class="demo-item">
<h3>历史模式(优化后)</h3>
<el-date-picker
v-model="dateRange2"
type="monthrange"
:history="true" <!-- 启用历史月份模式 -->
range-separator="至"
start-placeholder="开始月份"
end-placeholder="结束月份"
:picker-options="{ disabledDate: (time) => time.getTime() > Date.now() }"
/>
</div>
</div>
</template>
<script>
export default {
data() {
return {
dateRange1: [],
dateRange2: []
};
}
};
</script>
<style scoped>
.app {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.demo-item {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #eaeaea;
border-radius: 4px;
}
h1 {
text-align: center;
margin-bottom: 30px;
}
h3 {
margin-bottom: 15px;
color: #333;
}
</style>
效果对比:
- 普通模式:默认显示当前年在左边,下一年在右边
- 历史模式:默认显示去年在左边,当前年在右边
现在,当你打开月份范围选择器时,就会看到左边显示去年,右边显示今年,这样就可以直接选择去年的月份,无需额外点击"去年"按钮了。
使用npm和yarn打补丁
除了使用pnpm,我们也可以使用npm或yarn来打补丁。下面是使用npm和yarn打补丁的方法:
使用npm打补丁
-
安装patch-package:
npm install --save-dev patch-package -
修改源码: 找到
node_modules/element-ui/lib/element-ui.common.js文件,定位到calcDefaultValue函数,进行修改(修改内容与之前相同)。 -
创建补丁:
npx patch-package element-ui -
配置自动应用补丁: 在
package.json文件中添加postinstall脚本:"scripts": { "postinstall": "patch-package" }
使用yarn打补丁
-
安装patch-package:
yarn add --dev patch-package -
修改源码: 找到
node_modules/element-ui/lib/element-ui.common.js文件,定位到calcDefaultValue函数,进行修改(修改内容与之前相同)。 -
创建补丁:
yarn patch-package element-ui -
配置自动应用补丁: 在
package.json文件中添加postinstall脚本:"scripts": { "postinstall": "patch-package" }
无论使用哪种包管理器,打补丁的基本流程都是相同的:修改源码 -> 创建补丁 -> 配置自动应用补丁。
遇到的问题及解决方法
在打补丁的过程中,你可能会遇到一些问题,下面是一些常见问题的解决方法:
1. 重新下载依赖后补丁失效
问题:当我们重新运行 pnpm install 时,补丁可能会失效,因为依赖会被重新下载,覆盖我们的修改。
解决方法:这就是为什么我们需要在 package.json 中添加 postinstall 脚本的原因。这个脚本会在每次安装依赖后自动运行,重新应用我们的补丁。
2. 补丁文件是压缩的文件,修改起来困难
问题:Element UI的源码在npm包中是压缩的,看起来非常混乱,修改起来比较困难。
解决方法:
- 使用文本编辑器的搜索功能:大多数现代文本编辑器(如VS Code、Sublime Text等)都有强大的搜索功能,你可以使用它来快速找到你需要的代码。
- 使用在线代码格式化工具:如果你觉得压缩的代码实在难以阅读,可以使用在线代码格式化工具(如 JS Beautifier)来格式化代码,使其更易读。
- 直接搜索关键词:你可以直接搜索
calcDefaultValue或month-range等关键词,找到相关的代码。
3. 版本兼容性问题
问题:当Element UI版本更新时,补丁可能会失效,因为新版本的代码结构可能会发生变化。
解决方法:
- 在更新Element UI版本后,重新创建补丁:当你更新Element UI版本时,需要重新修改源码并创建新的补丁。
- 记录补丁的作用和修改的内容:在项目文档中记录补丁的作用和修改的内容,以便后续维护。这样,当Element UI版本更新时,你可以快速找到需要修改的部分。
- 关注Element UI的更新日志:定期查看Element UI的更新日志,了解是否有相关的功能更新或bug修复。
4. 找不到相关的代码
问题:在 element-ui.common.js 文件中找不到 calcDefaultValue 函数。
解决方法:
- 确保你使用的是Element UI 2.15.14版本:不同版本的Element UI代码结构可能会有所不同,确保你使用的是与本文相同的版本。
- 尝试搜索其他关键词:如果找不到
calcDefaultValue函数,可以尝试搜索month-range、leftDate或rightDate等关键词。 - 查看Element UI的源码仓库:如果你仍然找不到相关的代码,可以查看Element UI的源码仓库,找到
month-range.vue文件,查看其中的calcDefaultValue函数。
代码解析
让我们来详细解析一下我们的修改,以便你能更好地理解代码的逻辑:
1. 添加history属性
我们添加了一个 history 属性,用于控制是否默认显示上一年和当前年。当这个属性为 true 时,月份范围选择器会默认显示去年在左边,今年在右边;当这个属性为 false 时,保持原来的行为,即默认显示今年在左边,明年在右边。
2. 修改默认值计算逻辑
我们修改了 calcDefaultValue 函数的逻辑,添加了一个条件判断:
- 当有值传入时:如果
history为true,则将左侧日期设置为上一年;否则,保持原来的行为。 - 当没有值传入时:如果
history为true且当前值的年份相同,则将左侧日期设置为上一年,右侧日期设置为下一年。
3. 保持向后兼容
我们的修改保持了向后兼容性,当 history 为 false 时,组件的行为与原来一致。这样,即使你不添加 history 属性,组件也能正常工作,不会影响现有的代码。
4. 代码执行流程
当你使用修改后的月份范围选择器时,代码的执行流程如下:
- 当你打开月份范围选择器时,
calcDefaultValue函数会被调用。 - 函数会检查是否传入了
history属性。 - 如果
history为true,则将左侧日期设置为上一年,右侧日期设置为今年。 - 如果
history为false或未设置,则保持原来的行为,将左侧日期设置为今年,右侧日期设置为明年。
这样,当你需要选择历史月份时,就可以直接看到去年和今年的月份,无需额外点击"去年"按钮了。
打补丁的好处与团队协作
使用打补丁的方式进行插件二次开发,有以下几个明显的好处:
1. 保持代码的可维护性
- 避免直接修改源码:直接修改
node_modules中的源码会在依赖更新时被覆盖,而打补丁的方式可以在依赖更新后重新应用补丁。 - 清晰的修改记录:补丁文件记录了所有的修改内容,使代码变更更加透明和可追溯。
- 版本兼容性:当插件版本更新时,我们可以根据需要重新创建补丁,确保修改在新版本中仍然有效。
2. 团队协作优势
- 共享修改:补丁文件可以提交到版本控制系统中,团队成员可以共享和复用这些修改。
- 统一环境:通过
postinstall脚本,确保所有团队成员在安装依赖后都能自动应用补丁,避免环境不一致的问题。 - 代码审查:补丁文件可以作为代码审查的对象,团队成员可以清楚地看到修改的内容和目的。
- 减少冲突:当多个团队成员需要修改同一个插件时,通过补丁的方式可以减少代码冲突的可能性。
3. 其他优势
- 灵活性:可以根据项目的具体需求对插件进行定制化修改,而不受插件原有功能的限制。
- 安全性:修改仅限于项目内部,不会影响其他项目或插件的正常使用。
- 学习机会:通过分析和修改插件源码,可以深入了解插件的工作原理,提高自己的技术水平。
总结
通过使用pnpm/npm/yarn打补丁的方式,我们成功优化了Element UI月份范围选择器的交互体验,使其在选择历史月份时更加直观。这种方法不仅解决了问题,而且保持了代码的可维护性,是一种优雅的解决方案。
技术要点
-
使用patch-package创建和应用补丁:这是一种管理依赖包修改的有效方法,能够在依赖更新后自动重新应用补丁。
-
调试压缩的源码:通过搜索和分析压缩的源码,找到需要修改的部分,是前端插件二次开发的必备技能。
-
保持向后兼容:在修改依赖包时,要确保修改不会破坏现有功能,保证代码的稳定性。
-
文档化修改:记录补丁的作用和修改的内容,以便后续维护和团队协作。
-
代码安全性:在修改源码时,要注意添加必要的安全检查,确保代码在各种情况下都能正常运行。
后续建议
-
提交PR:如果你的修改对其他开发者也有帮助,可以考虑向Element UI官方仓库提交PR,为开源社区贡献力量。
-
封装组件:可以将修改后的月份范围选择器封装成一个独立的组件,方便在项目中使用,提高代码的复用性。
-
监控依赖更新:定期检查Element UI的更新,确保补丁在新版本中仍然有效,及时更新补丁以适应新版本的变化。
-
分享经验:将你的经验分享给团队成员,提高团队的整体技术水平,促进团队协作。
通过这种方法,我们不仅解决了产品提出的交互问题,还学习了如何使用pnpm/npm/yarn打补丁来修改依赖包,这是一种非常实用的前端开发技能。希望这篇文章对你有所帮助!