手摸手从零到一开发一个灵活的Todolist便签项目

本文介绍了一个从零开始开发的Todolist项目,旨在解决项目中硬编码颜色值的问题。通过配色表转换、颜色查找、颜色距离计算和颜色替换四个步骤,实现一键将颜色值替换成变量。文中详细阐述了每个步骤的实现方案,并提供了代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、背景

前段时间在公司项目中推进全局换肤之后,发现有个后遗症。项目中存在大量硬编码的颜色值,导致部分场景无法达到动态换肤的效果,需要把这些颜色值全部找出来并替换成变量。再加上公司正在风风火火的实行UI规范大一统,对这个问题有迫切的解决需求。然而公司中现存项目数量众多,在上万个文件中找出所有硬编码的颜色值,无异于大海捞针。且在各业务线紧张的迭代中,还要花费大量的人力去查找替换这些色值明显是不太现实的

二、目标

通过工具化的方式,一键将项目中硬编码的颜色值全部替换成相应的变量(less 变量或者 css3 变量),前提条件是公司必须有一套标准的配色表

三、实现方案

1、配色表转换

首先,需要将 UI 设计师提供的配色表转成对应的变量(本篇文章以 less 为例,其他预处理器同理)。例如设计师提供的配色表,如下

色号色值
1号蓝\color{#e6f7ff}{1号蓝}1号蓝#e6f7ff
2号蓝\color{#bae7ff}{2号蓝}2号蓝#bae7ff
3号蓝\color{#91d5ff}{3号蓝}3号蓝#91d5ff
4号蓝\color{#69c0ff}{4号蓝}4号蓝#69c0ff
5号蓝\color{#40a9ff}{5号蓝}5号蓝#40a9ff
6号蓝\color{#1890ff}{6号蓝}6号蓝#1890ff
7号蓝\color{#096dd9}{7号蓝}7号蓝#096dd9
8号蓝\color{#0050b3}{8号蓝}8号蓝#0050b3
9号蓝\color{#003a8c}{9号蓝}9号蓝#003a8c
10号蓝\color{#002766}{10号蓝}10号蓝#002766

转换成 less 变量之后,形式如下,姑且把这个文件命名为:color-table.less

@blue-1: #e6f7ff; 
@blue-2: #bae7ff; 
@blue-3: #91d5ff; 
@blue-4: #69c0ff; 
@blue-5: #40a9ff; 
@blue-6: #1890ff; 
@blue-7: #096dd9; 
@blue-8: #0050b3; 
@blue-9: #003a8c; 
@blue-10: #002766; 
2、找颜色

前面准备工作做好之后,接下来就是怎么把颜色找出来了。嗯?那么多文件怎么找?很容易想到,可以通过 fs.readdirfs.readFile 配合正则表达式进行查找匹配,进而可以得出以下表达式(可精确匹配 rgbahex 格式的颜色值,暂不考虑英文字面量颜色与hsl格式,无他,项目中基本不用)

/(#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3}))|([rR][gG][Bb][Aa]?[\(]([\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?),){2}[\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?),?[\s]*(0\.\d{1,2}|1|0)?[\)]{1})/g 

这么长一串看着晕吗?我也是这种感觉!那咱们把它可视化一下,最后长这样:

666,瞬间清晰多了

正则表达式有了之后,需要把它利用起来才能找到颜色值,这一块逻辑的实现代码如下:

// 匹配 rgba + hex 格式颜色值正则
const HEX_AND_RGB_COLOR_REG =/(#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3}))|([rR][gG][Bb][Aa]?[\(]([\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?),){2}[\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?),?[\s]*(0?\.\d{1,2}|1|0)?[\)]{1})/g;

// 读取文件
const readFile = (filePath) => {fs.readFile(filePath, function (err, data) {if (err) {return err;}let str = data.toString();// 匹配出来的颜色值const matchColor = str.match(HEX_AND_RGB_COLOR_REG);console.log(matchColor);});
};

// 读取文件夹
const readDir = (filePath) => {//遍历目标文件夹fs.readdir(filePath + "", function (err, files) {if (err) {console.log(err);return err;}if (files.length !== 0) {files.forEach((item) => {const fileP = path.resolve(filePath, item);//判断文件的状态,用于区分文件名/文件夹fs.stat(fileP, function (err, status) {if (err) {return err;}const isFile = status.isFile(); //是文件const isDir = status.isDirectory(); //是文件夹if (isFile) {readFile(fileP);}if (isDir) {readDir(fileP);}});});}});
} 
3、计算颜色距离

配色表有了,项目中的颜色也找出来了。接下来到了最关键的一步:把颜色之间的相似度计算出来。也就是说需要知道两个颜色之间的“距离”才能知道从项目中找出来的这个颜色值需要被替换成配色表中的哪个变量

在这之前需要把配色表文件 color-table.less 处理一下,使之成为 key-vlaue 的对象形式,方便后续的匹配操作

// 读取配色表中的内容
const colorTableStr = fs.readFileSync("./color-table.less", "utf-8");
// 去除行末的分号
const colorTableStrNext = colorTableStr.replace(/;/g, "");
// 将每一行数据装进数组,形如:['@blue-1: #e6f7ff', '@blue-2: #bae7ff']
const colorTableArr = colorTableStrNext.split("\n").filter((item) => !!item);

// 配色表对象,形如:{'#e6f7ff': '@blue-1', '#bae7ff': '@blue-2'}
const colorVarObj = {};
colorTableArr.forEach((item) => {item = item.toString();if (item) {const [key, value] = item.split(":");// 去除空格const val = value.replace(/\s/g, "");colorVarObj[val] = key;}
}); 

经过上述处理之后的配色表对象 colorVarObj 格式为:{'#e6f7ff': '@blue-1', '#bae7ff': '@blue-2'}

既然是计算,那肯定得有一套计算逻辑,首先想到的计算方法是:

  • 将颜色全部转换为 RGB 格式;
  • 结合公式 ΔRGB=(R2−R1)2+(G2−G1)2+(B2−B1)2\Delta RGB = \sqrt{(R_2 - R_1)^2 + (G_2 - G_1)^2 + (B_2 - B_1)^2}ΔRGB=(R2​−R1​)2+(G2​−G1​)2+(B2​−B1​)2​ 进行计算

这种计算方法对于大部分颜色来讲是没问题的,但是对于某些颜色就行不通了,假设有下面这一组颜色

color1:rgb(150,150,150)\color{#969696}{color1: rgb(150,150,150)}color1:rgb(150,150,150)

color2:rgb(120,120,120)\color{#787878}{color2: rgb(120,120,120)}color2:rgb(120,120,120)

color3:rgb(170,130,130)\color{#AA8282}{color3: rgb(170,130,130)}color3:rgb(170,130,130)

从计算结果来看,color3color1 的色差 34.64,明显小于 color2color1 的色差 51.96。但是从视觉效果上来看,color2color3 更接近 color1,所以这个方法行不通

经过查阅相关资料,找到了一个相对比较完美的计算方案:

  • 将颜色全部转成 LAB 格式
  • 结合 CIEDE2000 公式 ΔE00∗=(ΔL′KLSL)2+(ΔC′KCSC)2+(ΔH′KHSH)2+RTΔC′KCSCΔH′KHSH\Delta E_{00}^* = \sqrt{ (\frac{\Delta L\prime}{K_LS_L})2 + (\frac{\Delta C\prime}{K_CS_C})2 + (\frac{\Delta H\prime}{K_HS_H})2 + R_T\frac{\Delta C^\prime}{K_CS_C}\frac{\Delta H^\prime}{K_HS_H} }ΔE00∗​=(KL​SL​ΔL′​)2+(KC​SC​ΔC′​)2+(KH​SH​ΔH′​)2+RT​KC​SC​ΔC′​KH​SH​ΔH′​​ 来计算,这个方案用 js 代码实现之后,跟 Photoshop 的色差计算结果相差无几,基本上可以说是一模一样。

上述公式中各字母代表的含义及计算方法:

接下来,咱们来稍微实现一下这个公式。。。可惜啊,已经有大佬实现了这个计算公式,秉着不重复造轮子观念,就直接用这个库吧(其实就是能力不行,实现不了😄)。传送门:DeltaE

不过呢,需要注意的是,这个公式接收的颜色格式为 LAB,所以需要对颜色进行转换,找了一圈类似的 js 库,经测试发现转换得并不精准。那咱们自己实现一下?上代码:

// rgb转为lab
const rgb2lab = function ({ r, g, b }) {r /= 255.0; // rgb range: 0 ~ 1g /= 255.0;b /= 255.0;// gamma 2.2if (r > 0.04045) {r = Math.pow((r + 0.055) / 1.055, 2.4);} else {r = r / 12.92;}if (g > 0.04045) {g = Math.pow((g + 0.055) / 1.055, 2.4);} else {g = g / 12.92;}if (b > 0.04045) {b = Math.pow((b + 0.055) / 1.055, 2.4);} else {b = b / 12.92;}// sRGBlet X = r * 0.436052025 + g * 0.385081593 + b * 0.143087414;let Y = r * 0.222491598 + g * 0.71688606 + b * 0.060621486;let Z = r * 0.013929122 + g * 0.097097002 + b * 0.71418547;// XYZ range: 0~100X = X * 100.0;Y = Y * 100.0;Z = Z * 100.0;// Reference White Pointconst ref_X = 96.4221;const ref_Y = 100.0;const ref_Z = 82.5211;X = X / ref_X;Y = Y / ref_Y;Z = Z / ref_Z;// Labif (X > 0.008856) {X = Math.pow(X, 1 / 3.0);} else {X = 7.787 * X + 16 / 116.0;}if (Y > 0.008856) {Y = Math.pow(Y, 1 / 3.0);} else {Y = 7.787 * Y + 16 / 116.0;}if (Z > 0.008856) {Z = Math.pow(Z, 1 / 3.0);} else {Z = 7.787 * Z + 16 / 116.0;}const lab_L = 116.0 * Y - 16.0;const lab_A = 500.0 * (X - Y);const lab_B = 200.0 * (Y - Z);return [lab_L, lab_A, lab_B];
}; 

看到这里是不是有点疑惑,上面那一堆数字怎么来的,你咋知道是这些?附上参考资料:Conversion from RGB to lab

结合 DeltaE 公式,实现逻辑如下:

// 计算颜色距离
const calDistance = (current, source) => {const [cl, ca, cb] = rgb2lab(tinyColor(current).toRgb());const [sl, sa, sb] = rgb2lab(tinyColor(source).toRgb());const distance = DeltaE.getDeltaE00({ L: cl, A: ca, B: cb },{ L: sl, A: sa, B: sb });return distance;
}; 

还是使用前面那一组颜色进行测试这个新公式的计算效果。color3color1 的色差 16.11,是大于 color2color1 的色差 10.82。计算结果与视觉效果完全一致。

4、替换颜色

一切准备就绪,替换颜色就很简单了。但是,要注意以下三点:

  • less 文件:直接将相应的颜色值替换为 color-table.less 中的 less 变量
  • tsx 文件:由于 tsx 文件不支持 less 变量,所以 tsx 文件中硬编码的颜色值,将被替换为 color-table.lessless 变量对应的色值或 css3 变量
  • 透明度不为 1 的 rgba 颜色值:由于 LAB 色彩空间没有透明度信息,所以经过转换之后,原色值的透明度会丢失,导致被转换为错误的变量。此时,需要结合 less 的 fade 方法还原丢失的透明度fade(${less变量}, ${透明度 * 100}%)

代码实现如下:

// 根据颜色距离,取出最接近的颜色
const nearColor = (color) => {const colors = Object.keys(colorVarObj);const distance = colors.map((item) => {return {distance: calDistance(color, item),color: item,};});let resColor = color;let minDis = Number.MAX_SAFE_INTEGER;distance.forEach((item) => {if (item.distance < minDis) {minDis = item.distance;resColor = item.color;}});// 替换为less变量return [colorVarObj[resColor], resColor];
};

// 替换颜色
const replaceNearColor = (str, type) => {let [colorVar, colorRes] = nearColor(str);let realColor = colorTableObj[colorVar] ? colorTableObj[colorVar] : str;// 透明度不为1的颜色特殊处理const alp = tinyColor(str).getAlpha();if (alp !== 1) {const color = replaceCssVarFn(realColor);colorVar = `fade(${color}, ${alp * 100}%)`;}// 处理非less文件中的颜色值if (type !== 'less') {if (alp !== 1) {realColor = tinyColor(realColor).setAlpha(alp).toRgbString();}return realColor;}return colorVar;
}; 

最后

整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值