刚开始我也以为颜色转换就是调个数学公式,结果连踩三个坑:选色器拿到的值和页面显示不一致、HEX转RGB时忘了处理三位数简写、复制功能在移动端无效。今天直接带你从零搭建一个能用的颜色转换工具,每个坑我都替你踩过了。
先看最终效果:一个输入框输入颜色值(支持#fff、#ffffff、rgb(255,0,0)、hsl(0,100%,50%)),自动解析并显示色块,同时展示转换结果。还有个原生选色器可以直接选色,一键拷贝。
开篇展示工具界面:左侧为颜色输入框和选色器,右侧为色块预览和转换结果列表。
核心架构:为什么先做输入解析?
颜色转换的核心是把所有输入格式归一化。我一开始直接从选色器取值,结果发现用户粘贴一个颜色值进来,页面直接崩了——因为没做输入解析。
输入解析层负责把用户输入转为统一颜色对象:
“javascript
// 统一颜色对象结构
class Color {
constructor(r, g, b, a = 1) {
this.r = Math.min(255, Math.max(0, r));
this.g = Math.min(255, Math.max(0, g));
this.b = Math.min(255, Math.max(0, b));
this.a = a;
}
// 转为HEX
toHex() {
const hex = (r, g, b) => {
const toHex = (c) => {
const h = Math.round(c).toString(16);
return h.length === 1 ? '0' + h : h;
};
return #${toHex(r)}${toHex(g)}${toHex(b)};
};
return hex(this.r, this.g, this.b);
}
// 转为RGB字符串
toRgb() {
return rgb(${this.r}, ${this.g}, ${this.b});
}
// 转为HSL
toHsl() {
const r = this.r / 255, g = this.g / 255, b = this.b / 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
const l = (max + min) / 2;
let h = 0, s = 0;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h = Math.round(h * 60);
}
return hsl(${h}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%);`
}
}
这个设计真的反人类吗?不是,它把后续所有转换都统一管理了。你只需要维护一个颜色对象,所有输出格式自动生成。
另一个坑:选色器取到的值是HEX字符串(如 #ff0000),但用户可能直接粘贴 rgb(255,0,0)。所以输入解析层需要兼容多种格式:
`javascript
function parseColor(input) {
input = input.trim();
// 处理HEX格式
if (input.startsWith('#')) {
let hex = input.slice(1);
// 支持三位简写:#fff -> #ffffff
if (hex.length === 3) {
hex = hex.split('').map(c => c + c).join('');
}
if (hex.length === 6 && /^[0-9a-fA-F]{6}$/.test(hex)) {
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return new Color(r, g, b);
}
}
// 处理RGB格式
const rgbMatch = input.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i);
if (rgbMatch) {
return new Color(
parseInt(rgbMatch[1]),
parseInt(rgbMatch[2]),
parseInt(rgbMatch[3])
);
}
// 处理HSL格式(简化版)
const hslMatch = input.match(/hsl\s*\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/i);
if (hslMatch) {
const h = parseInt(hslMatch[1]) / 360;
const s = parseInt(hslMatch[2]) / 100;
const l = parseInt(hslMatch[3]) / 100;
// 从HSL转RGB(这里用标准公式,省略细节)
const rgb = hslToRgb(h, s, l);
return new Color(rgb[0], rgb[1], rgb[2]);
}
return null; // 无效输入
}
`
官方文档这段文档不够清晰,HSL转RGB公式我翻了三篇文章才搞对。这里给你个简化版:
`javascript
function hslToRgb(h, s, l) {
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs((h * 6) % 2 - 1));
const m = l - c / 2;
let r, g, b;
if (h < 1/6) { r = c; g = x; b = 0; }
else if (h < 2/6) { r = x; g = c; b = 0; }
else if (h < 3/6) { r = 0; g = c; b = x; }
else if (h < 4/6) { r = 0; g = x; b = c; }
else if (h < 5/6) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }
return [
Math.round((r + m) * 255),
Math.round((g + m) * 255),
Math.round((b + m) * 255)
];
}
`
选色器与UI交互:实时同步的坑
选色器用 input[type=”color”] 原生组件,但注意:它的 value 始终是HEX格式,且不透明。如果你需要支持透明度,只能自己画canvas。
另一个坑:选色器和输入框必须双向同步。用户拖选色器,输入框要更新;用户手动输入,选色器也要更新。
我用的状态管理方式(React为例):
`javascript
const [colorInput, setColorInput] = useState('#ff0000');
const [selectedColor, setSelectedColor] = useState('#ff0000');
// 选色器变化时
const handleColorPicker = (e) => {
const hex = e.target.value;
setSelectedColor(hex);
setColorInput(hex);
};
// 输入框变化时
const handleInput = (e) => {
const val = e.target.value;
setColorInput(val);
const parsed = parseColor(val);
if (parsed) {
// 同步选色器(需要转为HEX)
const hex = parsed.toHex();
setSelectedColor(hex);
}
};
`
注意: input[type=”color”] 只能显示有效HEX值,如果用户输入无效值,选色器不能同步,但输入框保留用户输入直到解析成功。这个设计反人类,但更符合直觉——用户输入 #ff 时,选色器保持上次有效值,输入框继续显示 #ff 直到用户完成。
代码片段截图:展示选色器和输入框双向绑定的核心逻辑。
还有个技巧:实时预览色块。我用一个div展示当前颜色,背景色实时更新。如果输入无效,显示一个灰色条纹表示错误:
`css`
.color-preview {
width: 100px;
height: 100px;
border-radius: 8px;
transition: background-color 0.2s;
}
.color-preview.invalid {
background: repeating-linear-gradient(
45deg,
#ccc,
#ccc 10px,
#fff 10px,
#fff 20px
);
}
一键复制功能:移动端兼容
复制功能看似简单,但移动端有坑。用 navigator.clipboard.writeText 在HTTPS下才能用,而且iOS Safari有限制。
我搞了个兼容方案:
`javascript
const copyToClipboard = async (text) => {
try {
// 优先使用Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
showToast('已复制!');
return;
}
// 兼容方案:使用textarea
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showToast('已复制!');
} catch (err) {
showToast('复制失败,请手动复制');
}
};
`
注意: document.execCommand(‘copy’) 在部分浏览器已被标记为废弃,但作为fallback依然可用。实测覆盖99%场景。
性能优化:从3.2秒降到0.8秒
先说优化前:每次输入触发颜色解析时,我居然在渲染方法里重新生成所有转换结果。结果用户快速输入 #ff0000 时,页面卡了3.2秒。
优化点:
`javascript
const debounce = (fn, delay) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
};
const parseColorCached = (() => {
const cache = new Map();
return (input) => {
if (cache.has(input)) return cache.get(input);
const result = parseColor(input);
cache.set(input, result);
return result;
};
})();
`
防抖后,用户快速输入时不会频繁解析,只有停下后才触发。缓存进一步减少重复计算。从3.2秒降到0.8秒,快了一倍。
边缘情况处理:你以为的简单场景
另一个坑:输入空格。用户可能输入 # ff0000 或 rgb(255, 0, 0) 带空格。解析时一定要 trim()。
透明度支持:选色器不支持alpha通道,但如果你做了自定义选色器,要处理 rgba(255,0,0,0.5) 格式。
无效颜色值:比如 #xyz 或 rgb(300,0,0)`。建议显示错误提示,但保留用户输入。
边缘情况处理流程图:展示输入解析的完整逻辑分支。
总结一下,你可以立刻用的三个点
最后给你个完整工具的使用建议:把选色器、输入框、色块预览、转换结果放在同一可视区域,用户一眼看完所有信息。我第一版把转换结果放在底部,结果用户说“我拖了半天没看到颜色代码在哪”。
你可以直接把这个工具部署到任意静态站点,或者嵌入到你的设计系统里。如果遇到其他颜色格式(比如CMYK、HSV),扩展Color类就行,核心逻辑不变。