注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

瘫痪8年后,马斯克的首个脑机接口人类植入者,正在用念力玩文明6

一名因潜水事故导致肩部以下瘫痪八年的 29 岁男子,正在借助脑机接口设备重温在线国际象棋和杀时间大作游戏《文明 6》。 这是脑机接口公司 Neuralink 最新一场直播的内容,迅速吸引了五百万多人围观。 在九分钟的简短直播中,Neuralink 首位人体...
继续阅读 »

一名因潜水事故导致肩部以下瘫痪八年的 29 岁男子,正在借助脑机接口设备重温在线国际象棋和杀时间大作游戏《文明 6》。


图片


这是脑机接口公司 Neuralink 最新一场直播的内容,迅速吸引了五百万多人围观。


图片


在九分钟的简短直播中,Neuralink 首位人体受试者 Noland Arbaugh 先是进行了自我介绍,并表示自己可以使用 Neuralink 设备玩在线国际象棋和视频游戏《文明》。


Arbaugh 使用一把特制的椅子坐在笔记本电脑前。当他试图控制一盘棋时,双手仍然放在椅子的扶手上:


图片


「这并不完美。我想说我们遇到了一些问题。我不想让人们认为这是旅程的终点,还有很多工作要做,」Arbaugh 在 Neuralink 工程师 Bliss Chapman 旁边说道。但脑机接口已经为他的生活带来了许多改善,比如终于不用依赖家人就能玩几个小时的视频游戏了。


原本的身体情况限制了他参与最喜欢的电子游戏《文明 6》的能力,因为每次只能玩几个小时,然后需要家人的帮助来重新调整坐姿。


「我基本上已经放弃玩那个游戏了,」他补充说这是一个「大型游戏」,需要很多时间坐着不动。


有了脑机芯片之后,躺在床上玩几小时的视频游戏不成问题。如果说仍有限制,那就是在连续玩 8 个小时的视频游戏后,必须再次为设备充电。这对于经常「一局到天亮」的《文明 6》来说,确实还不太够。


在直播中,Arbaugh 描述了学习如何使用脑机接口的过程:「我会尝试移动,比如说,我的右手向左、向右、向前、向后移动,从那时起,我觉得开始想象光标移动变得很直观。」


他说:「如果你们能看到光标在屏幕上移动,那或许就是我。」


虽然直播中包含的细节相对较少,但 Neuralink 工程师在视频中表示,未来几天公司会发布更多信息。


脑机接口研究的重要一步


Neuralink 由马斯克在 2016 年创立,目前正在开发一种被称为脑机接口的系统,它可以从大脑信号中解码运动意图。该公司的初步目标是让瘫痪者只用意念就能控制光标或键盘。


此次直播,使 Neuralink 成为了真正发布人脑植入证据的公司之一。另外发布过证据的两家公司 Blackrock Neurotech 和 Synchron 领先多年,三家公司各有不同的做法,同时这一赛道也涌入了不少初创公司。 


比如,Neuralink 的一位联合创始人于 2021 年离开公司,创办了竞争对手 Precision Neuroscience,去年 6 月开始了一项人体临床研究。 


而 Neuralink 遭遇了严格的审查,部分原因是其创始人马斯克也是特斯拉和 SpaceX 的首席执行官,且是世界上最富有的人之一。 


Neuralink 去年获得了美国食品和药物管理局(US Food and Drug Administration)的绿灯,可以继续进行初步人体试验,并在秋季开始招募瘫痪者来测试该设备。


但到目前为止,Neuralink 公司几乎没有透露这项研究进展的细节。


马斯克在 1 月份的一篇 X 帖子中宣布,第一个人体试验对象已经接受了 Neuralink 的植入物,并且「恢复良好」。


图片


2 月 19 日,马斯克在 X 上的 Spaces 音频对话中回答了有关参与者情况的问题:「进展良好,患者似乎已完全康复,没有出现我们所知的不良影响。患者只需通过思考就能在屏幕上移动鼠标。」


Neuralink 的设备通过该公司开发的手术机器人植入大脑;一旦植入成功,它在外观上是不可见的。为了分析大脑信号并将其转化为输出命令来控制外部设备,Neuralink 还设计了专门的软件。


Arbaugh 的此次直播似乎打消了人们对设备安全的顾虑:「我想,没什么好害怕的。手术非常简单,一天后我就真的出院了。」他还表示手术后没有认知障碍。


争议中前行的脑机接口


一些神经科学家和伦理学家批评 Neuralink 之前的试验缺乏透明度。2021 年,Neuralink 发布了一段视频,展示一只植入其设备的猴子通过心灵感应玩电子游戏,引起巨大轰动。美国动物保护组织 PCRM 曾对 Neuralink 提起诉讼,指控其「虐待」试验中使用的猴子。


图片


Neuralink 回应称:「多只猴子在参加试验之前健康状况就已经不佳,即将被实施安乐死。所有新的医疗设备都必须先在动物身上进行测试,然后再在人体上进行测试。这是 Neuralink 无法逃避的规则。但我们绝对致力于以尽可能人道和道德的方式与动物合作。」


相比于动物,人类受试者参与试验在伦理方面会带来更大的挑战。Neuralink 尚未透露将参加此次初步人体试验的受试者数量、试验地点或将进行的评估。


值得注意的是,Neuralink 尚未在 ClinicalTrials.gov (一个包含涉及人类受试者的医学研究信息的政府存储库)上注册。


据专家称,即使脑机接口设备被证明可安全用于人体,Neuralink 仍可能需要十多年的时间才能获得商业使用许可。


除了 Neuralink,其他几家公司也在竞相将脑机接口商业化。例如,Synchron 正在开发一种类似支架的装置,试图将其插入颈静脉并向上移动,使其紧贴大脑。相比之下,Synchron 的血管介入方式有着比 Neuralink 更高的安全性,Neuralink 需要切入人体颅骨进行设备植入。


Synchron 曾为 ALS 患者植入其脑机接口设备 ——Stentrode。接受 Stentrode 植入物后,参与者可以使用计算机通过文本进行交流并完成日常任务,比如在线购物和办理银行业务。


图片


Synchron 的临床参与者通过使用他的思想以数字方式控制他的计算机来进行交流。


然而,FDA 尚未批准任何脑机接口设备,它们都还处于实验阶段。


参考链接:http://www.wired.com/story/neura…


http://www.sohu.com/a/535904499…


作者:机器之心
来源:juejin.cn/post/7348640468005273615
收起阅读 »

ES2024即将发布!5个可能大火的JS新方法

web
Hello,大家好,我是 Sunday。 ECMAScript 2024(ES15) 即将发布(2024年6月),新的版本带来了非常多全新的特性。其中有 5 个全新的 JS 方法,可以大幅度提升我们的工作效率,从而让我们得到更多的摸鱼时间。咱们一起来看看吧! ...
继续阅读 »

Hello,大家好,我是 Sunday。


ECMAScript 2024(ES15) 即将发布(2024年6月),新的版本带来了非常多全新的特性。其中有 5 个全新的 JS 方法,可以大幅度提升我们的工作效率,从而让我们得到更多的摸鱼时间。咱们一起来看看吧!


01:Promise.withResolvers


这个功能引入了一个新方法来创建一个 promise,直接返回 resolve 和 reject 的回调。使用 Promise.withResolvers ,我们可以创建直接在其执行函数之外 resolve 和 reject


const [promise, resolve, reject] = Promise.withResolvers();

setTimeout(() => resolve('Resolved after 2 seconds'), 2000);

promise.then(value => console.log(value));

02:Object.groupBy()


Object.groupBy() 方法是一项新添加的功能,允许我们按照特定属性将数组中的 对象分组,从而使数据处理变得更加容易。


const pets = [
{ gender: '男', name: '张三' },
{ gender: '女', name: '李四' },
{ gender: '男', name: '王五' }
];

const res = Object.groupBy(pets, pet => pet.gender);
console.log(res);
// 输出:
// {
// 女: [{ gender: '女', name: '李四' }]
// 男: [{ gender: '男', name: '张三' }, { gender: '男', name: '王五' }],
// }

03:Temporal


Temporal提案引入了一个新的API,以更直观和高效的方式 处理日期和时间。例如,Temporal API提供了新的日期、时间和持续时间的数据类型,以及用于创建、操作和格式化这些值的函数。


const today = Temporal.PlainDate.from({ year: 2023, month: 11, day: 19 });
console.log(today.toString()); // 输出: 2023-11-19

const duration = Temporal.Duration.from({ hours: 3, minutes: 30 });
const tomorrow = today.add(duration);
console.log(tomorrow.toString()); // 输出: 2023-11-20

04:Records 和 Tuples


Records 和 Tuples 是全新的数据结构,提供了一种更简洁和类型安全的方式来创建对象和数组。



  • Records 类似于对象,但具有具体类型的固定属性集。

  • Tuples 类似于数组,但具有固定长度,每个元素可以具有不同类型。


let record = #{
id: 1,
name: "JavaScript",
year: 2024
};
console.log(record.name); // 输出: JavaScript

05:装饰器(Decorators)


装饰器(Decorators)是一种提议的语法,用于添加元数据或修改类、函数或属性的行为。装饰器可用于实现各种功能,如日志记录、缓存和依赖注入。


function logged(target, key, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${key} with`, args);
return original.apply(this, args);
};
return descriptor;
}

class Example {
@logged
sum(a, b) {
return a + b;
}
}

const e = new Example();
e.sum(1, 2); // 输出:[1, 2]

其他


ES15 还提供了很多其他的新提案,比如:新的正则v、管道符|>String.prototype.isWellFormed()ArrayBuffer.prototype.resize 等等。大家有兴趣的同学可以额外到 mdn 网站上进行了解~~



前端训练营:1v1私教,终身辅导计划,帮你拿到满意的 offer 已帮助数百位同学拿到了中大厂 offer。欢迎来撩~~~~~~~~



作者:程序员Sunday
来源:juejin.cn/post/7349410765525483555
收起阅读 »

💀填好个税,一年多给几千块 ~ 聊聊个人所得税,你该退税还是补税?写一个个税的计算器(退税、补税、个税)

前言 一年一度个税年度综合汇算清缴的时间又到了,作为开发者的你,肯定过了起征点了吧。🫤 去年退税退了 5676 ,今年看这个估计得补好几千,但是个税年度汇算清缴还没有预约到,抓紧提前算算金额,做做心理建设。\同时,了解个税都扣在哪了,才可以让我们合理避税~ 下...
继续阅读 »

前言


一年一度个税年度综合汇算清缴的时间又到了,作为开发者的你,肯定过了起征点了吧。🫤


去年退税退了 5676 ,今年看这个估计得补好几千,但是个税年度汇算清缴还没有预约到,抓紧提前算算金额,做做心理建设。\同时,了解个税都扣在哪了,才可以让我们合理避税~


下面我们简单聊聊 补税预缴 ,顺便讲讲专项附加扣除应该怎么填。


以及带大家写一个个税计算器。你可以通过码上掘金查看 在线 svelte(无UI) 版 ,后续也会推出其他框架版。


为什么你需要补税?


大多数情况下,公司发工资会替你把税交了,这个行为叫预缴。


为什么预缴呢?因为国家规定:



《个人所得税扣缴申报管理办法(试行)》(国家税务总局公告2018年第61号发布)

第六条:扣缴义务人向居民个人支付工资、薪金所得时,应当按照累计预扣法计算预扣税款,并按月办理扣缴申报。



这也就是我们每个月发工资都会扣税的原因。


那为什么需要补税呢?因为预缴是根据你在当前公司的收入进行缴税,公司会计算你的累进税率,你会发现每到年底税交的越来越高了,这是累进预缴导致的。


有些人在年中换了工作了,新公司不知道你之前已经交到哪个阶段的个税了,因此预缴时计税金额会重新累计。


因此补税的原因不外乎:



  • 工作变更

  • 公司主体变更(如:公司拆分)


为什么说预缴是天才发明?


预缴制简直是个天才发明,不但会大大减少逃税人数,而且能减轻税务工作量(转移至各公司),且可以让缴税的人对税率的感知没有那么强烈。


达成这种效果主要原因有两点,分别是 损失厌恶心理账户


损失厌恶


人们对损失的敏感程度通常远远大于对同等价值的收益的敏感程度

人们对损失的敏感程度通常远远大于对同等价值的收益的敏感程度

人们对损失的敏感程度通常远远大于对同等价值的收益的敏感程度


牢记这句话。


一个最简单的例子,短视频中经常会出现的 最有效的 6 条学习方式,最后一条最重要 。这种放大损失的语言,常常能诱发更高的完播率。



虽然我很讨厌以这种方式留住用户,但常常在刷到这类视频时,也忍不住多看一样,虽然知道它最终可能也没什么实质内容。



还有一种就是我们常常刷掉一个视频,又返回去看一眼,又刷掉又返回去。我常常会有这种心理,这个视频我是不是应该看一看的纠结。


个税也是同理,个税预缴是减少我们的收益,而个税年终汇算则是直接让我们从口袋中掏钱。


就算汇算综合到月度计算,同样也是,一种是公司扣完发给你,另一种是发给你之后你再掏出来一部分。大家感受一下这其中的区别。


心理账户


人们可能会将个税缴纳视作开销,而且是意外开销,意外开销总是让人痛苦的。


比如我每个月 1w 块,其中 3k 拿来租房,3k 拿来吃饭, 2k 拿来娱乐,2k 拿来缴五险一金。


这时候到年终汇算时,人们则容易苦不堪言。


且这种带来的直接后果是,我想把税留到最后一天交,同时最后一天也很容易忘记交,因为大脑也不想要这种意外支出。


最终则导致 漏交、拒交 个税的人数大大增加。


专项附加扣除严谨度



  • 子女教育(未婚,无接触)

  • 赡养老人(容易被查)

  • 继续教育 - 学历提升(基本不查)

    • 学历提升可以选择一个对应学历,每个学历 4 年,共 16 年左右抵税



  • 继续教育 - 证书获取(基本不查)

    • 证书获取有人一个证书可以一直抵税,建议: 营养师证、焊工证等



  • 租房买房(基本不查)

  • 大病医疗(未填过,未知)


开发


首先咱们先写个个税计算器的 class ,个人所得税英文简称 IIT (Individual Income Tas)


class IITCalulator {}

添加需要计算的内容


众所周知,

个税计算法:应缴税款 * 对应税率 - 速算扣除

应缴税款计算法:工资 - 五险一金缴纳额 - 专项附加扣除


因此我们先添加 工资 、 五险一金 、 专项附加扣除 的属性。


工资


我们工资有两个组成部分,分别是 固定工资 和 年终奖(如果有的话)。


class IITCalulator {
private salary: {
monthlySalary: number;
yearEnd: number;
} = {
monthlySalary: 0,
yearEnd: 0,
};

/**
* @description 添加工资(通过工资计算年薪)
*/

addSalary(
monthlySalary,
yearEnd?: { value: number; type: "month" | "amount" }
) {
this.salary.monthlySalary = monthlySalary;
if (yearEnd) {
this.salary.yearEnd =
yearEnd.type === "amount"
? yearEnd.value
: monthlySalary * yearEnd.value;
}
}
}

五险一金


这里直接给了固定金额,可以通过查看每月扣除得知。



考虑到有人不太清楚自己的五险一金缴纳基数,这里直接用了固定金额,后续可以扩展出通过缴纳比例自动计算



class IITCalulator {
private socialInsuranceMonthlyAmount = 0;

/**
* @description 添加五险一金,计算年五险一金缴纳额
* @param {number} monthlyAmount 月度缴纳金额
*/

addSocialInsurance(monthlyAmount) {
this.socialInsuranceMonthlyAmount = monthlyAmount;
}
}

专项附加扣除


专项附加扣除通过数组的方式存储扣除项。



  1. 子女教育

  2. 赡养老人

  3. 继续教育(学校)

  4. 继续教育(证书)

  5. 住房贷款

  6. 大病医疗


// 专项附加扣除类型
type SpecialDeductionType =
| "children"
| "elder"
| "education-school"
| "education-certificate"
| "housing"
| "medical";

class IITCalulator {
private specialDeductionTypes: Array<SpecialDeductionType> = [];
private medicalAmount = 0;

/**
* @description 添加专项附加扣除
* @param {string} type 专项附加扣除类型
*/

addSpecialDeduction(
SpecialDeductionType: SpecialDeductionType,
medicalAmount?: number
) {
this.specialDeductionTypes.some((t) => t !== SpecialDeductionType) &&
this.specialDeductionTypes.push(SpecialDeductionType);

if (medicalAmount) {
this.medicalAmount = medicalAmount;
}
}
}

计算 工资 、 五险一金 、 专项附加扣除


我们添加了基础属性,可以根据基础属性计算出对应金额。


工资


工资 = 月薪 * 12 + 年终奖


getYearSalary() {
return this.salary.monthlySalary * 12 + this.salary.yearEnd;
}

五险一金


五险一金 = 月缴纳额 * 12


getYearSocialInsurance() {
return this.socialInsuranceMonthlyAmount * 12;
}

专项附加扣除


专项附加扣除 = 扣除项的扣除金额合集



需要注意的是:大病扣除项是固定金额的



这里直接采用 reduce 进行累加。


/**
* @description 计算专项附加扣除
*/

private getSpecialDeduction() {
return this.specialDeductionTypes.reduce((r, v) => {
switch (v) {
case "children":
return r + 2000 * 12;
case "elder":
return r + 3000 * 12;
case "education-school":
return r + 400 * 12;
case "education-certificate":
return r + 3600;
case "housing":
return r + 1500 * 12;
case "medical":
return r + this.medicalAmount;
default:
return r;
}
}, 0);
}

计算纳税金额


我们基础数据都有了,就只差计算了。先通过基础数据计算应纳税所得额,再通过应纳税所得额计算个税。


计算应纳税所得额


calcIIT() {
// 计算年薪
const yearSalary = this.getYearSalary();
// 五险一金缴纳金额
const yearSocialInsurance = this.getYearSocialInsurance();
// 专项附加扣除金额
const specialDeduction = this.getSpecialDeduction();
// 计算需要缴纳个税的金额
let taxableAmount =
yearSalary - yearSocialInsurance - specialDeduction - 60000;
// 计算个税
return this.calcTaxableAmount(taxableAmount);
}

计算应缴个税


个税计算参考:


image.png


// 计算个税(金额 * 税率 - 速算扣除)
private calcTaxableAmount(taxableAmount: number) {
if (taxableAmount <= 36000) {
return taxableAmount * 0.03;
} else if (taxableAmount <= 144000) {
return taxableAmount * 0.1 - 2520;
} else if (taxableAmount <= 300000) {
return taxableAmount * 0.2 - 16920;
} else if (taxableAmount <= 420000) {
return taxableAmount * 0.25 - 31920;
} else if (taxableAmount <= 660000) {
return taxableAmount * 0.3 - 52920;
} else if (taxableAmount <= 960000) {
return taxableAmount * 0.35 - 85920;
} else {
return taxableAmount * 0.45 - 181920;
}
}

完整代码:


// 专项附加扣除类型
// 1. 子女教育
// 2. 赡养老人
// 3. 继续教育(学校)
// 4. 继续教育(证书)
// 5. 住房贷款
// 6. 大病医疗
type SpecialDeductionType =
| "children"
| "elder"
| "education-school"
| "education-certificate"
| "housing"
| "medical";

class IITCalculator {
private salary: {
monthlySalary: number;
yearEnd: number;
} = {
monthlySalary: 0,
yearEnd: 0,
};
private socialInsuranceMonthlyAmount = 0;

private specialDeductionTypes: Array<SpecialDeductionType> = [];
private medicalAmount = 0;

constructor() {}

/**
* @description 添加工资(通过工资计算年薪)
*/

addSalary(
monthlySalary,
yearEnd?: { value: number; type: "month" | "amount" }
) {
this.salary.monthlySalary = monthlySalary;
if (yearEnd) {
this.salary.yearEnd =
yearEnd.type === "amount"
? yearEnd.value
: monthlySalary * yearEnd.value;
}
}

getYearSalary() {
return this.salary.monthlySalary * 12 + this.salary.yearEnd;
}

/**
* @description 添加五险一金,计算年五险一金缴纳额
* @param {number} monthlyAmount 月度缴纳金额
*/

addSocialInsurance(monthlyAmount) {
this.socialInsuranceMonthlyAmount = monthlyAmount;
}

getYearSocialInsurance() {
return this.socialInsuranceMonthlyAmount * 12;
}

/**
* @description 添加专项附加扣除
* @param {string} type 专项附加扣除类型
*/

addSpecialDeduction(
SpecialDeductionType: SpecialDeductionType,
medicalAmount?: number
) {
this.specialDeductionTypes.some((t) => t !== SpecialDeductionType) &&
this.specialDeductionTypes.push(SpecialDeductionType);

if (medicalAmount) {
this.medicalAmount = medicalAmount;
}
}

/**
* @description 计算专项附加扣除
*/

private getSpecialDeduction() {
return this.specialDeductionTypes.reduce((r, v) => {
switch (v) {
case "children":
return r + 2000 * 12;
case "elder":
return r + 3000 * 12;
case "education-school":
return r + 400 * 12;
case "education-certificate":
return r + 3600;
case "housing":
return r + 1500 * 12;
case "medical":
return r + this.medicalAmount;
default:
return r;
}
}, 0);
}

calcIIT() {
// 计算年薪
const yearSalary = this.getYearSalary();
// 年终奖是否单独计税

// 五险一金缴纳金额
const yearSocialInsurance = this.getYearSocialInsurance();
// 专项附加扣除金额
const specialDeduction = this.getSpecialDeduction();
// 计算需要缴纳个税的金额
let taxableAmount =
yearSalary - yearSocialInsurance - specialDeduction - 60000;
// 计算个税
return this.calcTaxableAmount(taxableAmount);
}

// 计算个税(金额 * 税率 - 速算扣除)
private calcTaxableAmount(taxableAmount: number) {
if (taxableAmount <= 36000) {
return taxableAmount * 0.03;
} else if (taxableAmount <= 144000) {
return taxableAmount * 0.1 - 2520;
} else if (taxableAmount <= 300000) {
return taxableAmount * 0.2 - 16920;
} else if (taxableAmount <= 420000) {
return taxableAmount * 0.25 - 31920;
} else if (taxableAmount <= 660000) {
return taxableAmount * 0.3 - 52920;
} else if (taxableAmount <= 960000) {
return taxableAmount * 0.35 - 85920;
} else {
return taxableAmount * 0.45 - 181920;
}
}
}

最后


我最开始尝试写一个 UI 版。但后续感觉,UI 版对于不同语言的用户,会看起来很痛苦。

因此我通过纯 JS 实现,大家可以通过不同的 UI 调用该类,可以在各个框架中使用。

同时也通过 svelte 做了一个简略 UI 版,大家可以直接尝试。


最后,点赞、关注、收藏 ,祝大家多多退税~~


作者:sincenir
来源:juejin.cn/post/7342511044290789430
收起阅读 »

如果我贷款买一套 400W 的房子,我要给银行多送几辆迈巴赫?

买房攻略 2023 年至今,上海房价一跌再跌。俺已经蠢蠢欲动了,磨刀霍霍向"买房"。但是奈何手里钞票不够,只能向天再借 500 年打工赚钱。但是作为倔强的互联网打工人,想知道自己会被银行割多少韭菜。于是就写了个程序,用于计算我贷款买房需要多给银行还多少钱。这样...
继续阅读 »

买房攻略


2023 年至今,上海房价一跌再跌。俺已经蠢蠢欲动了,磨刀霍霍向"买房"。但是奈何手里钞票不够,只能向天再借 500 年打工赚钱。但是作为倔强的互联网打工人,想知道自己会被银行割多少韭菜。于是就写了个程序,用于计算我贷款买房需要多给银行还多少钱。这样我就能知道银行割我的韭菜,能省下几辆迈巴赫的钱了。


贷款利率



  • 公积金的贷款利率



    • 首房:贷款时间 <=5 年,利率为 2.6% ;贷款时间 >= 5 年,利率为 3.1%

    • 非首房:贷款时间 <=5 年,利率为 3.025% ;贷款时间 >= 5 年,利率为 3.575%




image.png



  • 商业险贷款利率



    • 贷款时间 <=5 年,利率为 3.45% ;贷款时间 >= 5 年,利率为 3.95%




image.png


代码实现



  • 以下代码,实现了:我贷款买房需要多给银行还多少钱


public class LoanAmountCalculation {

   //首套住房5年以内公积金贷款利率
   private static final double FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_WITHIN_FIVE_YEARS = 2.6;
   //首套住房5年以上公积金款利率
   private static final double FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_MORE_FIVE_YEARS = 3.1;
   //二房5年以内公积金贷款利率
   private static final double NOT_FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_WITHIN_FIVE_YEARS = 3.025;
   //二房5年以上公积金款利率
   private static final double NOT_FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_MORE_FIVE_YEARS = 3.575;
   //5年以内商业贷款利率
   private static final double COMMERCIAL_LOAN_RATE_WITHIN_FIVE_YEARS = 3.45;
   //5年以上商业贷款利率
   private static final double COMMERCIAL_LOAN_RATE_MORE_FIVE_YEARS = 3.95;

   public static void main(String[] args) {
       Scanner scanner = new Scanner(System.in);

       double houseAmount = getInputValue(scanner, "请输入预计买房金额(单位:W):", "请输出正确的买房金额(>0)!");
       double principal = getInputValue(scanner, "请输入您的本金(单位:W):", "请输出正确的买房金额(>0)!");
       if (principal >= houseAmount) {
           System.out.println("全款买房,崇拜大佬!");
           return;
      }

       double accumulationFundLoanAmount = getInputValue(scanner, "请输入公积金贷款金额(单位:W):", "请输出正确的公积金贷款金额(>0)!");

       double commercialLoanAmount = houseAmount - principal - accumulationFundLoanAmount;
       if(commercialLoanAmount <= 0){
           System.out.println("您的本金+公积金贷款已经够买房啦,恭喜大佬!");
           return;
      }else{
           System.out.println("您的本金+公积金贷款还不够买房哦,需要商业贷款金额为(单位:W):" + commercialLoanAmount + "\n");
      }

       int accumulationFundLoanYears = getInputIntValue(scanner, "请输入公积金贷款年份(单位:年):");
       int commercialLoanAmountYears = getInputIntValue(scanner, "请输入商业贷款年份(单位:年):");

       int isFirstHouse = getInputIntValue(scanner, "请输入是否首房(0:否,1:是):");

       LoanAmount loanAmount = calculateLoanAmount(
               accumulationFundLoanAmount, accumulationFundLoanYears,
               commercialLoanAmount, commercialLoanAmountYears, isFirstHouse);
       System.out.println("详细贷款信息如下:" + "\n" + loanAmount);
  }

   /**
    * 获取double类型的输入
    * @param scanner:Java输入类
    * @param prompt:提示信息
    * @param errorMessage:输入错误的提示信息
    * @return 一个double类型的输入
    */

   private static double getInputValue(Scanner scanner, String prompt, String errorMessage) {
       double value;
       while (true) {
           System.out.println(prompt);
           if (scanner.hasNextDouble()) {
               value = scanner.nextDouble();
               if (value > 0) {
                   break;
              } else {
                   System.out.println(errorMessage);
              }
          } else {
               scanner.next();
               System.out.println(errorMessage);
          }
      }
       return value;
  }

   /**
    * 获取int类型的输入
    * @param scanner:Java输入类
    * @param prompt:提示信息
    * @return 一个int类型的输入
    */

   private static int getInputIntValue(Scanner scanner, String prompt) {
       int value;
       while (true) {
           System.out.println(prompt);
           if (scanner.hasNextInt()) {
               value = scanner.nextInt();
               if (value > 0) {
                   break;
              } else {
                   System.out.println("请输入正确的年份(>0)!");
              }
          } else {
               scanner.next();
               System.out.println("请输入正确的年份(>0)!");
          }
      }
       return value;
  }

   /**
    * 功能:贷款金额计算
    * 入参:
    * 1.accumulationFundLoanAmount:公积金贷款金额 2.accumulationFundLoanYears:公积金贷款年份;
    * 3.commercialLoanAmount:商业贷款金额;       4.commercialLoanAmountYears:商业贷款年份
    * 5.isFirstHouse:是否首房
    */

   private static LoanAmount calculateLoanAmount(double accumulationFundLoanAmount, int accumulationFundLoanYears,
                                                          double commercialLoanAmount, int commercialLoanAmountYears, int isFirstHouse)
{
       LoanAmount loanAmount = new LoanAmount();
       //公积金贷款还款金额
       double accumulationFundRepaymentAmount;
       if(isFirstHouse == 1){
           accumulationFundRepaymentAmount = accumulationFundLoanYears <= 5 ?
                   accumulationFundLoanAmount * Math.pow((100 + FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_WITHIN_FIVE_YEARS) / 100, accumulationFundLoanYears)
                  : accumulationFundLoanAmount * Math.pow((100 + FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_MORE_FIVE_YEARS) / 100, accumulationFundLoanYears);
      }else{
           accumulationFundRepaymentAmount = accumulationFundLoanYears <= 5 ?
                   accumulationFundLoanAmount * Math.pow((100 + NOT_FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_WITHIN_FIVE_YEARS) / 100, accumulationFundLoanYears)
                  : accumulationFundLoanAmount * Math.pow((100 + NOT_FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_MORE_FIVE_YEARS) / 100, accumulationFundLoanYears);
      }
       loanAmount.setAccumulationFundRepaymentAmount(String.format("%.2f", accumulationFundRepaymentAmount));

       //公积金贷款每年还款金额
       loanAmount.setAccumulationFundAnnualRepaymentAmount(String.format("%.2f", accumulationFundRepaymentAmount / accumulationFundLoanYears));

       //商业贷款还款金额
       double commercialRepaymentAmount = commercialLoanAmountYears <= 5 ?
               commercialLoanAmount * Math.pow((100 + COMMERCIAL_LOAN_RATE_WITHIN_FIVE_YEARS) / 100, commercialLoanAmountYears)
              : commercialLoanAmount * Math.pow((100 + COMMERCIAL_LOAN_RATE_MORE_FIVE_YEARS) / 100, commercialLoanAmountYears);
       loanAmount.setCommercialRepaymentAmount(String.format("%.2f", commercialRepaymentAmount));

       //商业贷款每年还款金额
       loanAmount.setCommercialAnnualRepaymentAmount(String.format("%.2f", commercialRepaymentAmount / commercialLoanAmountYears));

       //公积金贷款超出金额
       loanAmount.setAccumulationFundLoanExceedAmount(String.format("%.2f", accumulationFundRepaymentAmount - accumulationFundLoanAmount));

       //商业贷款超出金额
       loanAmount.setCommercialLoanExceedAmount(String.format("%.2f", commercialRepaymentAmount - commercialLoanAmount));

       loanAmount.setTotalExceedLoanAmount(String.format("%.2f", accumulationFundRepaymentAmount - accumulationFundLoanAmount + commercialRepaymentAmount - commercialLoanAmount));
       return loanAmount;
  }
   @Data
   static class LoanAmount{
       /**
        * 公积金贷款还款金额
        */

       private String accumulationFundRepaymentAmount;
       /**
        * 公积金贷款每年还款金额
        */

       private String accumulationFundAnnualRepaymentAmount;
       /**
        * 商业贷款还款金额
        */

       private String commercialRepaymentAmount;
       /**
        * 商业贷款每年还款金额
        */

       private String commercialAnnualRepaymentAmount;
       /**
        * 公积金贷款超出金额 = 公积金贷款还款金额 - 公积金贷款金额
        */

       private String accumulationFundLoanExceedAmount;
       /**
        * 商业贷款超出金额 = 商业贷款还款金额 - 商业贷款金额
        */

       private String commercialLoanExceedAmount;

       /**
        * 总共贷款超出金额
        */

       private String totalExceedLoanAmount;

       @Override
       public String toString() {
           return "1.公积金贷款还款金额=" + accumulationFundRepaymentAmount + "万元\n" +
                   "2.商业贷款还款金额=" + commercialRepaymentAmount + "万元\n" +
                   "3.公积金贷款每年还款金额=" + accumulationFundAnnualRepaymentAmount + "万元\n" +
                   "4.商业贷款每年还款金额=" + commercialAnnualRepaymentAmount + "万元\n" +
                   "5.公积金贷款超出金额=" + accumulationFundLoanExceedAmount + "万元\n" +
                   "6.商业贷款超出金额=" + commercialLoanExceedAmount + "万元\n" +
                   "7.总共贷款超出金额=" + totalExceedLoanAmount + "万元\n";
      }
  }
}

代码输入,输出示例


6f9e90ab10c92d0a673777569a64f75.png


由上图可知,我要贷款买一套 400w 的房子,本金只有 120w,使用组合贷:公积金贷款 120w(10年),商业贷款 160w(20年)。最终我需要多还银行 230.07w,相当于买两辆迈巴赫的钱了,巨亏!


以上就是全部内容了,如果涉及到真实场景,还是需要根据具体的情况计算的!


作者:一只野生的八哥
来源:juejin.cn/post/7346385551366684722
收起阅读 »

用马斯克五步工作法重构支付宝商家账单

本文作者是蚂蚁集团数据研发工程师惠勒,将马斯克五步工作法应用在了实际项目中,实现了支付宝商家账单的重构,希望本文对想要降低系统复杂度的同学或者项目有所帮助。0. 概述支付宝中国数据团队在过去的一年里应用马斯克的五步工作法重构了有 10 年历史之久的支付宝商家账...
继续阅读 »

本文作者是蚂蚁集团数据研发工程师惠勒,将马斯克五步工作法应用在了实际项目中,实现了支付宝商家账单的重构,希望本文对想要降低系统复杂度的同学或者项目有所帮助。

0. 概述

支付宝中国数据团队在过去的一年里应用马斯克的五步工作法重构了有 10 年历史之久的支付宝商家账单,整体复杂度减少 60%,时效性提升 1 小时,计存成本降低 30%,理解和运维成本大幅下降。复杂度是很多问题的根源,既会增加运维的成本,又降低了支撑业务的效率。 账单重构的经验表明,相当大比例的复杂度是没有必要的,我们应该致力于把复杂的事情变简单,而不是倒过来做“防御性编程”。希望本文对想要降低系统复杂度的同学或者项目有所帮助。

1. 重构背景

1.1 什么是商家账单

商家通过支付宝发生业务,我们对他们提供相应的流水单或者凭证,这就是商家账单。商户可以到B站下载账单和他们自己的业务记录及资金变动期望逐一比对,确认所有业务和资金都按正确的期望的方式完成了处置,这个过程称为商家对账

支付宝目前提供了丰富账单类型,包括资金流水,交易订单,资产凭证,营销动账,费用账单以及一些列个性化定制账单。实现方式上则有在线实时账单以及基于 odps 的离线的日/月账单,其中在线账单主要用于业务查询,而离线账单则主要用于商家对账,本文所指商家账单主要指离线账单

图1:B站里的商家账单

1.2 为什么要重构

一句话概括:历时 10 年,积重难返

商家账单作为支付宝收单业务配套的基础产品,主要的服务对象是商家。和所有 To B 产品一样,其面临着“千人千面的个性化诉求和成本可控的快速支撑”的核心矛盾。在实现过程中,要么在原有逻辑上打个补丁,更多的时候是出于稳定性等因素考虑,不敢动原有的逻辑,于是就新起炉灶搞个新的字段。历时 10 年,资金流水账单搞出了上百个字段,很多字段的加工链路极其复杂。目前整个账单大概有几千个任务,近万的依赖关系,平均加工深度 20 多层,各种横向域之间的耦合,纵向层之间的调用层出不强,用一团乱麻来形容也不为过!

图 2:真实的账单血缘图

image.png

图 3:账单架构混乱的示意图

1.3 为什么是现在

主要是因为逻辑过于复杂,当前用于保障账单准确出账时效的成本已经过于高昂。

离线账单是拿去对账的,这就像有上百万商家拿着放大镜在找问题一样,不仅像金额,时间等字段不能有问题,各种订单号,门店 ID 等字段也偏差不得。而当前账单过于复杂,经常出现变更了这里漏了那里,或者是改了上游影响了好几层以外的下游。目前每年流转到二线研发同学的咨询就有几百例,一线外包和马力等同学接到的账单类问题更是以万计。

时效性方面的压力则有过之而无不及。由于需要对标竞对 T+1 10 点的出账时效,支付宝目前对客承诺 T+1 9 点出账,扣减掉在线账单文件生成和预留的异常处理时间,基本上要求离线账单需要 T+1 5 点 30 产出。作为蚂蚁唯二的两条最高级基线之一,运维同学承担了极大的压力,从 2023 年 9 月-12 月,运维同学在夜间共计响应 150+ 起电话告警,涉及天数 67 天,值班起夜比例为 67/122=54.9%。虽然引发起夜的因素有集群计算资源以及 odps 软件等外部因素,但根子上还是因为加工链路太长,给基线预留的余量不够。

为了彻底解决上述问题,我们决心重构支付宝商家账单,通过降低复杂度的方式,既提升用户体验又降低运维成本。

2. 重构目标

通过降低 50% 的复杂度,达到以下 5 点业务效果

  • 准确的

每个字段的含义是明确的,账单数据内部是一致的

  • 高时效的

    账单产出提前 1 小时
  • 好运维的

    重大问题能够快速一键重跑的(72h 降低到 12h 以内),日常的异常情况能够快速处理(1h以内),代码结构是好理解的(模块化的分层架构)
  • 易扩展的

    可扩展性强,对于各种业务需求的响应速度较快,不需要对代码逻辑大幅改动。有灰度环境的全链路回归链路,减少变更风险
  • 低成本的

    在保证回刷要求的前提下尽可能降低存储成本(降低 1/3 的存储成本),减少任务数量,降低计算成本(降低 1/3 的计算成本)

3. 应用五步工作法重构账单

马斯克在特斯拉和 spaceX 的成功经验告诉我们,应用五步工作法可以把复杂的事情变得简单,把高昂的成本打下来。所谓五步工作法,主要是

  1. 质疑,推敲需求,不要有愚蠢的需求;
  2. 删减,简化流程,精简部件或工艺流程;
  3. 优化,在前面两步的基础上做优化;
  4. 加速,在前三步的基础上加快迭代时间;
  5. 替换,在完成前四步之后做自动化替换。 商家账单的重构工作也或多或少借鉴了五步工作法。

3.1 质疑

第一步是质疑:为什么有那么多字段,为什么每个字段有那么多逻辑,为什么加工链路需要那么长。 带着这几个为什么,我们开始做字段梳理工作,核心工作是两项

  1. 梳理这些字段哪些是有人用的,哪些是没人用的。有人用的话有多少人在用,都是哪些商户
  2. 从末端表的字段出发,自下而上的梳理加工链路,穿透到最上游,看字段最终来源于哪些领域。

以最多商户使用的资金流水账单为例,上百个字段中仅有不到三分之一是核心字段,一半左右是个性化字段(使用商户数 100 以下),剩余大几十个都是无人使用字段;字段来源方面则集中在账务,交易,支付,计收费,结算,充转提等几个领域。从这些数字中我们得出以下两个观点

  1. 不需要那么多字段,可以先集中攻克核心字段
  2. 我们可以分域处理信息,再拼起来集中加工使用

3.2 删减

带着第一步质疑的观点,我们开始做删减,以核心字段为目标,落地如下架构设计 image.png

图4:重构的账单架构图

核心的工作有那么几项

  1. 把最终的一个对客账单字段,拆解为几个不同领域的字段加工。 如账单字段商户订单号,可以简化为如下规则:如果有交易号的话,则取交易号,否则使用账务域的外部流水号兜底。这样一来,一个账单字段就拆解为了一个交易域的字段和一个账务域的字段。对其余核心账单字段如法炮制,最终可以归到账务,交易,支付,计收费,结算,充转提等6-7个领域中去。
  2. 每个领域按照面向领域建模的方式进行中间层的构建,把需要的领域内的字段提前加工处理好
  3. 把(2)的结果拼接起来变成账单因子层宽表,再根据每个出账字段的加工规则,清洗出最后的账单字段
  4. 清洗出账规则,过滤(3)的结果,产出最终的日明细账单。日汇总,月账单等都基于日明细账单加工。

    特别需要注意的是,在这个阶段并不需要特别拘泥于细节,因为后面还会把删多的逻辑补回来,按照五步工作法的说法,如果最后没有补回来 10% 的逻辑,说明这个阶段删减的不够。

3.3 优化

在整个账单重构过程中,有几个难点我们专门提出来成立了专项进行优化

  1. 出账范围
  2. 商家账单中最基本的一个问题是给谁出账。老账单里起了好几十个任务在处理这个问题,经常会出现商户来问为什么没有出账,往往需要查半天才解释的清楚。重构时优化了该问题,明确了必须是签约了指定产品才能出账单,任务数相应减少到 10 个以下。
  3. 关联 jar 包

    商家账单中经常出现一笔昨日的流水需要关联多日之前的商品信息或者交易信息的诉求。这种跨天关联在离线odps的批处理框架下是比较麻烦且性价比很低的,在需要关联多日数据时面临着需要申请大量资源导致任务无法调起的问题。商家账单当前是使用一种jar包的方案来实现的,它的本质还是离线跨天关联,只是优化了并发逻辑,把一个大任务拆分成多个子任务发到 odps 跑。

    这次重构,我们提出离在线融合方案,利用在线可以笔笔查,性能好速度快的优势,使用 udf 调用在线http接口进行查询。考虑到在线查询可能会有失败的可能,对于失败的少数数据再用离线跨天关联的方式进行查漏补缺。

    目前这个方案还在研发中,预计可以大幅降低相关任务的计算成本,进一步提高出账的时效性。

  4. 离线灰度方案

    作为一个面客类产品,如何在离线实现像在线一样的变更三板斧从而减少对客影响,是一直困扰着所有离线同学的一个难题。本次重构中,我们做了一点尝试。我们的核心思想是:离线任务只跑一次但算两套值,并通过一个控制模块来控制哪些账号取原始值,哪些账号取灰度值。

    如下图所示,我们跑一次任务,把线上值记录在因子宽表的强字段收入金额和指出金额中,把灰度值记录在灰度扩展字段中,不同的值经过相同或者不同的加工规则,产出两个账单对客值净收入金额和灰度扩展字段中的净收入金额,最后通过一个控制模块来决定哪些账号要用灰度值。

    image.png

图5:离线灰度方案

  1. 稳定性自愈方案

    商家账单有几千个任务,但我们依赖的这套离线批处理的模式里又有很多不确定的因素:odps 的软件问题,底层计算集群的机器抖动,槽位占用,在线压制等,所以每个月总有那么几十个报错或者变慢的任务需要人工处理。在本次重构过程中,我们联合蚂蚁大数据部相关团队上线了报错自动重跑和变慢自愈恢复等两个稳定性自愈方案。

    报错自动重跑方案的核心是系统自动识别运行日志中的关键词,除非是明确不可重跑的报错(如数据质量问题,权限问题等),都会由调度系统拉起来重跑,实际运行过程中还需要综合考虑基线余量等要素。目前报错自动重跑方案每个月可以减少商家账单几十次的报错处理,减少 4-5 天的起夜值班。

    变慢自愈方案的核心思想是识别相关变慢任务并自动 copy 到双跑链路执行。若双跑任务执行期间原链路恢复,则不做处理;若双跑链路执行完成,原链路仍未完成,则杀死原链路任务,将双跑结果注入原表,将任务置成功。

图 6:变慢自愈方案

3.4 加速

在第二步删减模块我们只关注于核心字段的核心逻辑,一方面存在核心字段逻辑删多了的问题,另外一方面需要把其余个性化字段也补上。在第四步加速中,我们需要通过一定的方式开始回补,并且这种回补相比第二步而言要高效的多。

我们的办法是找一个牵引指标,小步快跑,快速迭代。当主逻辑搭建的差不多了以后尽快把脚本发布上线,并起一个新老账单对比任务,通过对比来发现新账单逻辑上存在的问题,对于缺失的部分快速补上。这里面的核心是要有一个牵引指标,在账单重构过程中,我们引入了商户可切流比例这样一个指标,计算公式如下

商户可切流比例=所有流水的所有字段都比对通过的商户数/总商户数*100%

以资金账单为例,商户可切流比例从第一版的 41% 经过大概 10 次迭代上升到了 85%,后又经过一段时间的精细化调整,目前为 99.3%。在这个过程中,我们一方面修复了代码中的 bug,另外也基本上把删多了的有用逻辑进行了回补。这种目标驱动的针对性查漏补缺的做法相比第二步的正向推进要高效的多。

图 7:资金账单的迭代次数和商户可切流比例

3.5 替换

五步工作法的最后一步是自动化替换人工,在账单场景中我们姑且取替换之意。在商户可切流比例高于 95%,且新账单的时效性等问题基本优化完全之后,我们就开启了新老账单的替换工作。优先切换核对通过且经常有下载使用的商户,这样做一方面可以得到商户的反馈,另一方面不因为长尾的逻辑优化影响切流。

4. 重构效果

重构的效果总的来说是满足预期的。以下为几个方面的效果

  • 复杂度

我们用任务数来衡量复杂度,资金流水账单的复杂度下降了 60% 以上,交易订单的复杂度也下降了47%

  • 时效性

资金流水账单时效性较老账单提升 1.5 小时,交易订单提升 1 小时

  • 成本

    存储和计算成本较老账单下降 1/3 左右,每年节省上百万的计存成本

  • 准确性

汇总,月,历史等账单均从日明细加工而得,不再会有内部不统一的问题

  • 运维/理解成本

代码耦合度大幅降低,账单整体的理解成本从半年到1年下降到 1 个月左右

5. 总结反思

站在现在这个时间点,我们认为有那么几点是值得总结的

1) 复杂度是各种问题的源泉

商家账单的各种准确性,时效性以及高可用问题,归根到底是因为业务逻辑过多,架构腐化,导致整体复杂度急剧飙升,造成今日想改也改不动,想维持也维持不了的尴尬局面。我们必须在日常迭代中高度重视复杂度控制问题,不图一时之快,尽量不给后人留坑。

2) 很多复杂度是没有必要的

账单重构的经验表明,对于一个发展了多年的系统,很多复杂度是没有必要的。今日之所以可以降低复杂度,一方面是因为过去很多逻辑现在已经失效了,另一方面是因为后人对于业务的理解有了新的认知,可以用新的方法来复现来重做相关功能。

复杂度的降低带来的好处是方方面面的,就商家账单而言,最直观的就是时效性的提升,从而带来基线稳定性的提升,降低起夜压力。其次复杂度的减少降低了运维和编码成本,可以进一步减少准确性问题。当然,用现在流行的话讲,这不是一种“防御性编程”,不再需要半年到1年的时间才能熟悉账单的工作,一个略有经验的同学,可能1个月左右就能上手账单业务。那是否要担心因此工作就会被替代呢?我们想是没有必要杞人忧天的,如果一个工作要通过刻意把事情变复杂才能形成壁垒和核心竞争力,那么这样的工作是不值得为之奉献的。

3) 拆解事情很重要,节奏感更重要

重构商家账单是一件有些复杂的事情,面对这样一团乱麻般的代码,从何处下手,要拆解出哪几件事情来,每件事件的交付物应该是什么,这样的拆解固然很重要。但更重要的是节奏感的把握,同样是做这些事情,哪个阶段先重点做什么工作,是细致做还是粗略做,这种节奏感的把握更为重要。因为如果节奏不对,就会在某个阶段发现事情无法快速推进下去,时间一久就会受到一线同学以及主管等质疑。要是节奏对了,每个阶段都能看到一些希望,那么一件复杂的事情便会推行的比较顺利。

4) 需要进一步降低重构这件事的成本

尽管如此,我们还是耗费了几百人日来做账单的重构。我们有那么多的场景值得重构,整个社会上也有非常多的公司有这种重构降本增效的诉求。是否有可能把重构的过程流程化,甚至是产品化,从而大幅降低重构这件事的成本?这是重构这件事给我们的遗留命题。

5) 数仓同学未来的两个发展方向

在转岗到支付宝数据部门的时候,有一位高年级的同学曾经问我如何看待数仓同学的上升空间/发展路径问题。经过这一年多在商家账单的一线工作,尤其是这大半年来专职投入重构工作的经历,我想可能有如下两个方向

a) 专业的数据技术专家

数据技术专家主要专注于通过代码优化,架构优化等方法,降低一套数仓任务的复杂度,获取时效,准确,计存成本等方面的收益。商家账单重构就是典型这类场景,这个发展方向的价值收益比较好衡量。未来的核心竞争力在于是否可以借助AI的力量把重构优化这件事的成本降低,提升效率。

b) 全局的数据架构师

全局的数据架构师更关注的是信息架构的问题,要解决的是数据生产者和数据消费者之间的信息差问题。我们常常可以听到业务同学抱怨数据不准,不好用,听到算法同学说没有优质的数据供给。殊不知其实要做一份好的数据资产其实是有比较高的门槛,需要对业务,架构,信息流转的方式等都有比较深入的理解,才有可能做出一份好的数据。但在实践中我们发现,造成这种门槛的一个重要原因是因为作为数据生产者的系统研发同学并不了解作为数据消费者的数仓同学的痛点,有些问题其实在数据生产者略作改动就可以给数据消费者减少极大的成本。数据架构师就需要致力于解决这样的问题。我们很难具象的衡量这样的改动带来的收益,但可以肯定的是这些细小的改动会润物细无声的降低做一份好的数据资产的门槛,进而真正的发挥数据要素乘数效应,助力业务发展。


作者:支付宝体验科技
来源:juejin.cn/post/7347912334700789779
收起阅读 »

答应我,在vue中不要滥用watch好吗?

web
前言 上周五晚上8点,开开心心的等着产品验收完毕后就可以顺利上线。结果产品突然找到我说要加需求,并且维护这一块业务的同事已经下班走了,所以只有我来做。虽然内心一万头草泥马在狂奔,但是嘴里还是一口答应没问题。由于这一块业务很复杂并且我也不熟悉,加上还饿着肚子,在...
继续阅读 »

前言


上周五晚上8点,开开心心的等着产品验收完毕后就可以顺利上线。结果产品突然找到我说要加需求,并且维护这一块业务的同事已经下班走了,所以只有我来做。虽然内心一万头草泥马在狂奔,但是嘴里还是一口答应没问题。由于这一块业务很复杂并且我也不熟悉,加上还饿着肚子,在梳理代码逻辑的时候我差点崩溃了。需要修改的那个vue文件有几千行代码,迭代业务对应的ref变量有10多个watch。我光是梳理这些watch的逻辑就搞了很久,然后小心翼翼的在原有代码上面加上新的业务逻辑,不敢去修改原有逻辑(担心搞出线上bug背锅)。


滥用watch带来的问题


首先我们来看一个例子:


<template>
{{ dataList }}
</template>

<script setup lang="ts">
import { ref, watch } from "vue";

const dataList = ref([]);
const props = defineProps(["disableList", "type", "id"]);
watch(
() => props.disableList,
() => {
// 根据disableList逻辑很复杂同步计算出新list
const newList = getListFromDisabledList(dataList.value);
dataList.value = newList;
},
{ deep: true }
);
watch(
() => props.type,
() => {
// 根据type逻辑很复杂同步计算出新list
const newList = getListFromType(dataList.value);
dataList.value = newList;
}
);
watch(
() => props.id,
() => {
// 从服务端获取dataList
fetchDataList();
},
{ immediate: true }
);
</script>

上面这个例子在template中渲染了dataList,当props.id更新时和初始化时从服务端异步获取dataList。当props.disableListprops.type更新时,同步的计算出新的dataList。


代码逻辑流程图是这样的:


bad-code.png


乍一看上面的代码没什么问题,但是当一个不熟悉这一块业务的新同学接手这一块代码时问题就出来了。


我们平时接手一个不熟悉的业务首先要找一个切入点,对于前端业务,切入点肯定是浏览器渲染的页面。在 Vue 中,页面由模板渲染而来,找到模板中使用的响应式变量和他的来源,就能理解业务逻辑。以 dataList 变量为例,梳理dataList的来源基本就可以理清业务逻辑。


在我们上面的这个例子dataList的来源就是发散的,有很多个来源。首先是watchprops.id从服务端异步获取。然后是watchprops.disableListprops.type,同步更新了dataList。这个时候一个不熟悉业务的同学接到产品需求要更新dataList的取值逻辑,他需要先熟悉dataList多个来源的取值逻辑,熟悉完逻辑后再分析我到底应该是在哪个watch上面去修改业务逻辑完成产品需求。


但是实际上我们维护别人的代码时(特别是很复杂的代码)一般都不愿意去改代码,而是在原有代码的基础上再去加上我们的代码。因为去改别人的复杂代码很容易搞出线上bug,然后背锅。所以在这里我们的做法一般都是再加一个watch,然后在这个watch中去实现产品最新的dataList业务逻辑。


watch(
() => props.xxx,
() => {
// 加上产品最新的业务逻辑
const newList = getListFromXxx(dataList.value);
dataList.value = newList;
}
);

迭代几次业务后这个vue文件里面就变成了一堆watch,屎山代码就是这样形成的。当然不排除有的情况是故意这样写的,为的就是稳定自己在团队里面的地位,因为离开了你这坨代码没人敢动。


关注公众号:前端欧阳,解锁我更多vue干货文章,并且可以免费向我咨询vue相关问题。


使用computed解决问题


我们看了上面的反例,那么一个易维护的代码是怎么样的呢?我认为应该是下面这样的:


line.png


dataListtemplate中渲染,然后同步更新dataList,最后异步从服务端异步获取dataList,整个过程能够被穿成一条线。此时新来一位同学要去迭代dataList相关的业务,那么他只需要搞清楚产品的最新需求是应该在同步阶段去修改代码还是异步阶段去修改代码,然后在对应的阶段去加上对应的最新代码即可。


我们来看看上面的例子应该怎么优化成易维护的代码,上面的代码中dataList来源主要分为同步来源和异步来源。异步来源这一块我们没法改,因为从业务上来看props.id更新后必须要从服务端获取最新的dataList。我们可以将同步来源的代码全部摞到computed中。优化后的代码如下:


<template>
{{ renderDataList }}
</template>

<script setup lang="ts">
import { ref, computed, watch } from "vue";

const props = defineProps(["disableList", "type", "id"]);
const dataList = ref([]);

const renderDataList = computed(() => {
// 根据disableList计算出list
const newDataList = getListFromDisabledList(dataList.value);
// 根据type计算出list
return getListFromType(newDataList);
});

watch(
() => props.id,
() => {
// 从服务端获取dataList
fetchDataList();
},
{
immediate: true,
}
);
</script>

我们在template中渲染的不再是dataList变量,而是renderDataListrenderDataList是一个computed,在这个computed中包含了所有dataList同步相关的逻辑。代码逻辑流程图是这样的:


good-code.png


此时一位新同学接到产品需求要迭代dataList相关的业务,因为我们的整个业务逻辑已经变成了一条线,新同学就可以很快的梳理清楚业务逻辑。再根据产品的需求看到底应该是修改同步相关的逻辑还是异步相关的逻辑。下面这个是修改同步逻辑的demo:


const renderDataList = computed(() => {
// 加上产品最新的业务逻辑
const xxxList = getListFromXxx(dataList.value);
// 根据disableList计算出list
const newDataList = getListFromDisabledList(xxxList);
// 根据type计算出list
return getListFromType(newDataList);
});

总结


这篇文章介绍了watch主要分为两种使用场景,一种是当watch的值改变后需要同步更新渲染的dataList,另外一种是当watch的值改变后需要异步从服务端获取要渲染的dataList。如果不管同步还是异步都一股脑的将所有代码都写在watch中,那么后续接手的维护者要梳理dataList相关的逻辑就会非常痛苦。因为到处都是watch在更新dataList的值,完全不知道应该在哪个watch中去加上最新的业务逻辑,这种时候我们一般就会再新加一个watch然后在新的watch中去实现最新的业务逻辑,时间久了代码中就变成了一堆watch,维护性就变得越来越差。我们给出的优化方案是将那些同步更新dataListwatch代码全部摞到一个名为renderDataListcomputed,后续维护者只需要判断新的业务如果是同步更新dataList,那么就将新的业务逻辑写在computed中。如果是要异步更新dataList,那么就将新的业务逻辑写在watch中。


作者:前端欧阳
来源:juejin.cn/post/7340573783744102435
收起阅读 »

H5、小程序商品加入购物车的抛物线动画如何实现

web
H5、小程序商品加入购物车的抛物线动画如何实现 电商类 H5、小程序把商品加入到购物车时,常常有一个抛物线动画。比如麦当劳小程序,当你点击加购按钮时,会看到有一个小汉堡从卡片汉堡上抛出,然后掉落到购物袋里。 这种动画该怎么做呢?如果你也想实现它,看完这篇文章...
继续阅读 »

H5、小程序商品加入购物车的抛物线动画如何实现


电商类 H5、小程序把商品加入到购物车时,常常有一个抛物线动画。比如麦当劳小程序,当你点击加购按钮时,会看到有一个小汉堡从卡片汉堡上抛出,然后掉落到购物袋里。


mcdonald's.gif


这种动画该怎么做呢?如果你也想实现它,看完这篇文章,你一定有所收获。我会先说明抛物线动画的原理,再解释实现它的关键代码,最后给出完整的代码示例。代码效果如下:


parabola.gif


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的前端武学。


抛物线动画的原理


高中物理告诉我们,平抛运动、斜抛运动可以分解为水平方向的匀速直线运动、竖直方向自由落体运动(匀加速直线运动)。


principle.png


同理,我们可以把抛物线动画分解为水平的匀速动画、竖直的匀加速动画。


水平匀速动画很容易实现,直接 animation-timing-function 取值 linear 就行。


竖直的匀加速直线运动,严格实现非常困难,我们可以近似实现。因为匀加速直线运动,速度是越来越快的,所以我们可以用一个先慢后快的动画替代,你可能立刻就想到给 animation-timing-function 设置 ease-in。不过 ease-in 先慢后快的效果不是很明显。针对这个问题,张鑫旭大佬提供了一个贝塞尔曲线 cubic-bezier(0.55, 0, 0.85, 0.36);1。当然,你也可以用 cubic-bezier 自己调一个先慢后快的贝塞尔曲线。


关键代码实现


我们把代码分为两部分,第一部分是布局代码、第二部分是动画代码。


布局代码


首先是 HTML 代码,代码非常简单。下图中小球代表商品、长方形代表购物车。


ball-and-cart.png


<div class="ball-box">
<div class="ball"></div>
</div>
<div class="cart"></div>

你可能比较好奇,小球用一个 ball 元素就可以实现,为什么我要用 ball 和 ball-box 两个元素呢?因为 animation 只能给一个元素定义一个动画效果,而我们需要给小球添加两个动画效果。于是我们将动画分解,给 ball-box 添加水平运动的动画,给 ball 添加竖直运动的动画。


动画代码


再看动画代码,moveX 是水平运动动画,moveY 是竖直动画,动画时间均为 1s。为了让效果更加明显,我还特意将动画设置成无限循环的动画。


.ball-box {
/* ... */
animation: moveX 1s linear infinite;
}

.ball {
/* ... */
animation: moveY 1s cubic-bezier(0.55, 0, 0.85, 0.36) infinite;
}

@keyframes moveX {
to {
transform: translateX(-250px);
}
}

@keyframes moveY {
to {
transform: translateY(250px);
}
}

代码示例



总结


本文我们介绍了抛物线动画的实现方法,我们可以将抛物线动画拆分为水平匀速直线动画和竖直从慢变快的动画,水平动画我们可以使用 linear 来实现,竖直动画我们可以使用一个先慢后快的贝塞尔曲线替代。设置动画时,我们还需要注意,一个元素只能设置一个动画。


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的前端武学。


Footnotes




作者:小霖家的混江龙
来源:juejin.cn/post/7331607384933220390
收起阅读 »

面试官:在连续请求过程中,如何取消上次的请求?

web
前言 这个问题想必很多朋友都遇到过,我再详细说一下场景! 如 Boss 搜索框所示: 先输入1 再输入2 再输入3 再输入123  请求参数依次为:1 12 123 123123 请求参数通过右侧的 query 参数也可以看到,一共请求了...
继续阅读 »

前言


这个问题想必很多朋友都遇到过,我再详细说一下场景!


如 Boss 搜索框所示:




先输入1




再输入2




再输入3




再输入123 




请求参数依次为:1 12 123 123123



请求参数通过右侧的 query 参数也可以看到,一共请求了四次。


不难发现,这里已经做了基本的防抖,因为我们连续输入123的时候,只发了一次请求。


好了,现在看完基本场景,我们回到正题!


从上面的演示中不难发现我们一共发送了4次请求,顺序依次为1、12、123、123123。


问题


面试官现在问题如下:



我先输入的 1,已经发送请求了,紧接着输入了 2,3,123,如果在我输入最后一次 123 的时候,我第一次输入的 1 还没有请求成功,那请求 query 为 1 的这个接口将会覆盖 query 为 123123 的搜索结果,因为当 1 成功的时候会将最后一次请求的结果覆盖掉,当然这个概率很小很小,现在就这个bug,说一下你的解决思路吧!



解决


看到这个问题我们首先应该思考的是如何保证后面的请求不被前面的请求覆盖掉,首先说一下防抖是不行的,防抖只是对连续输入做了处理,并不能解决这个问题,上面的演示当中应该不难发现。


如何保证后面的请求不被前面的请求覆盖掉?


我们思路是否可以转化为:只需要保证后面的每次接口请求都是最新的即可?


简单粗暴一点就是,我们后续请求接口时直接把前面的请求干掉即可!


那如何在后续请求时,直接干掉之前的请求?


关键:使用 AbortController


MDN 解释如下:



AbortController 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。


AbortController.abort(),中止一个 尚未完成 的 Web(网络)请求。



MDN 文档如下:



AbortController - Web API 接口参考 | MDN (mozilla.org)



我们可以借助 AbortController 直接终止还 未完成 的接口请求,注意这里说的是还未完成的接口,如果接口已经请求成功就没必要终止了。


代码实现


参考代码如下:


    let currentAbortController = null;
function fetchData(query) {
// 取消上一次未完成的请求
if (currentAbortController) {
currentAbortController.abort();
}

// 创建新的 AbortController
currentAbortController = new AbortController();

return fetch(`/api/search?q=${query}`, {
signal: currentAbortController.signal
})
.then(response => response.json())
.then(data => {
// 处理请求成功的数据
updateDropdownList(data);
})
.catch(error => {
// 只有在请求未被取消的情况下处理错误
if (!error.name === 'AbortError') {
handleError(error);
}
});
}

借用官方的解释:



当 fetch 请求初始化时,我们将 AbortSignal 作为一个选项传递进入请求的选项对象中(下面的 {signal: currentAbortController.signal})。这将 signal 和 controller 与 fetch 请求相关联,并且允许我们通过调用 AbortController.abort() 去中止它!



这就意味着我们将 signal 作为参数进行传递,当我们调用 currentRequest.abort() 时就可以终止还未完成的接口请求,从而达到我们的需要。


我们在每次重新调用这个接口时,判断是否存在 AbortController 实例,如果存在直接中止掉该实例即可,这样就可以保证我们每次的请求都可以拿到最新的数据。


    if (currentAbortController) {
currentAbortController.abort();
}

总结


我们再来理一下这个逻辑:


首先是第一次调用时为接口请求添加 AbortSignal 参数


之后在每次进入都判断是否存在 AbortController 实例,有的话直接取消掉


取消只会针对还未完成的请求,已经完成的不会取消


通过这样就可以达到我们每次都会使用最新的请求接口作为数据来源,因为后面的接口会将前面的干掉


如果这道面试题这样回答,是不是还不错?


作者:JacksonChen
来源:juejin.cn/post/7347395265836924938
收起阅读 »

10 天的开发量,老板让我 1 天完成,怎么办?

大家好,我是树哥! 昨天,我在文章《业务开发做到零 bug 有多难?》和大家聊了下影响零 bug 的一些因素。其中,我提到了开发时被压缩工时,应该怎么做。今天,我们就来聊聊这个话题。 只要工作过几年的小伙伴,必然会遇到过背压工时的情况。面对这种情况,不同的工作...
继续阅读 »

大家好,我是树哥!


昨天,我在文章《业务开发做到零 bug 有多难?》和大家聊了下影响零 bug 的一些因素。其中,我提到了开发时被压缩工时,应该怎么做。今天,我们就来聊聊这个话题。


只要工作过几年的小伙伴,必然会遇到过背压工时的情况。面对这种情况,不同的工作年限、在不同的公司、不同的团队氛围下,都会有不同的反应。如果你是一个刚刚毕业的萌新开发,很大情况下你会选择自己加班服从。甚至加班都完不成的情况下,你还吭哧吭哧不出声。


最后等待你的结果就是 —— 成为被复盘的对象,被批评。那么如果遇到了开发时间被压缩,或者被质疑的情况下,我们除了默默加班接受之外,还能做些什么来让自己没那么苦逼吗?


在我看来,自己一个人傻傻加班是下下签,是最后实在没办法才做的无奈之举。一旦有其他选择,你都不应该提出自己加班加点做完。那么,到底有什么办法可以解决工时被压缩这一问题呢?


解释工时构成


如果你的开发时间被压缩,那么较大可能是 leader 质疑你评估出的工时。假设你的工时评估并没有问题,那么就是你考虑到了一些风险点,而你的 leader 并没有考虑到。毕竟这也很正常,对于一个很久没有写代码的管理者来说,其会习惯性地忽略一些细节性的东西。


这个时候,你要做的不是胆怯地接受。而是要主动去找 leader ,跟他解释工时是怎么评估出来的。你考虑到了某些风险点,为什么是这么多工时。如果你的 leader 不是傻子,那么相信他会接受你的解释。但这里要注意的是,解释的时候记得要语气好些,不要怒气冲冲地找别人,不然话没说然就吵起来了。


减少需求内容


假设你和 leader 已经进行了友好地沟通, leader 也认可了你的评估时间。但是他说:没办法,老板就要求那个时间点做完,没办法给你更多时间了!


这时候,萌新小白就会老老实实回去座位上加班,最后还是干不完被批斗。但对于职场老油条来说,他就学会与 leader 以及产品沟通 —— 能不能少点需求内容。例如:我们要做一个员工列表,那是不是只做列表就可以,不用做筛选和搜索了?员工详情是不是也可以先不走了?


老板可以指定最终完成的时间,但是他基本不会干涉到具体的细节上面。这时候就给我们留下了沟通的空间,这也就是作为开发的你可以争取的东西。对于一个有经验的产品经理来说,如果研发给他提出了减少非核心功能的诉求,他一般也会答应的。


申请更多资源


如果产品说:不行,我们每个功能点都得做,一点需求都少不了!


这时候你可以再向你的老板提出诉求 —— 申请更多资源。


前面你也解释过工时的构成,做这么多功能确实需要这么多时间。如果最终上线时间不能推迟,那么就只能投入更多的资源了。


在这种情况下,如果公司还有富裕的研发资源,那自然会优先考虑你这边的诉求。对于你来说,你的研发压力也自然变小了。


分摊开发压力


如果实在又申请不到更多资源,这个项目又只能由你们团队 5 个人来完成,怎么办?


很多时候,不同开发人员的开发压力不一样。可能你开发压力比较大,其他人开发压力比较小,这时候你可以提出 —— 是否可以让其他小伙伴帮忙做点工作,这样可以减少一些压力?


我想,如果你的 leader 也应该会考虑你的诉求。千万不要自己明明完成不了,还要硬抗。到最后加班干了几个星期,需求还是完成不了,不仅辛苦的付出得不到理解,还被批斗。那可就真的是赔了夫人又折兵啊!


千万不要觉得这种情况不会发生,在我去年工作的时候,就发生了这样一个事情,我也是很同情那位同学的。如果那位同学能看到这篇文章,那么或许他后面就不会踩坑了吧。


推迟上线时间


上面说得是最终上线时间无法变更的情况,但很多时候并没有这种倒排需求。很多需求的上线时间并不是一成不变的,你只要给出足够合理的解释,也都是可以沟通的。


因此,如果在上面的沟通方法都行不通的情况下,你也可以沟通看看是否可以推迟上线时间。毕竟相对于研发速度来说,研发质量肯定更加重要。


终极绝招


大多数情况下,如果你能合理应用上面提到的几种沟通方式,被压缩工时的问题一般都能解决。但有些小伙伴会问:那如果真的所有办法都失效了呢?那怎么办?


其实,大多数情况下,不太可能到了需要使用绝招的地步。但如果真的到了这一步,那你就做好「殊死一搏」的准备,用上这个绝招吧 —— 调预期、表态度。


调预期,就是给你的 leader 打预防针,提前告诉他这样做的后果就是 —— 质量差、很难做得完。如果他还是这么坚定地推进,那么如果真的做不完,相信他也理解,不会太过于责怪你。


表态度,就是得加加班。如果你之前已经说了压力很大,甚至加班都做不完。那么你至少还是得表表态度,不能像往常一样早早下班,这样即使最后搞砸了。由于你态度还算端正,还不至于被责怪得太狠。但如果你要是又说做不完,又每天早早下班,那别人就觉得是你态度问题了。


走到这一步,实属是无奈,但这也是最后的保命之举了,除非你不想在这干了。


总结


今天分享了几种沟通解决「被压缩工时」的方法,包括:



  • 解释工时构成

  • 减少需求内容

  • 申请更多资源

  • 分摊开发压力

  • 推迟上线时间


本质上来说,就是不要自己一个人傻傻地抗压力,不要让自己背负着太大压力。我们要明白自己能做到什么程度,而且不要早早把「自己加班」这一最后的保命、卖惨利器祭出。他应该是自己的保命技能,而不是为别人锦上添花的技能。


特别是,不要因为赶进度、赶工时,而去牺牲开发质量。因为如果你这么做了,后果就是你付出了很多时间和精力,最后你会在项目复盘会上检讨 —— 为什么你的功能代码质量这么差。这是另一个话题了,后续有时间我们继续聊。


希望大家都能够活学活用,下次在和 leader 以及产品沟通的时候,用上这些沟通技巧吧!希望大家都不要加班,准时下班!


如果你觉得今天的文章对你有帮助,欢迎点赞转发评论支持树哥,你的支持对于我很重要,感谢大家!


作者:树哥聊编程
来源:juejin.cn/post/7348289379055140903
收起阅读 »

业务开发做到零 bug 有多难?

大家好,我是树哥,好久不见啦。作为一个工作了 10 多年的开发,写业务代码总是写了不少的。但你想过做到零 bug 吗?我可是想过的,毕竟我还是有点追求的。不然每天都是浑浑噩噩地过,多没意思啊。大概在一年多前,我给自己立下一个目标 —— 尽量将自己经手的业务需求...
继续阅读 »

大家好,我是树哥,好久不见啦。

作为一个工作了 10 多年的开发,写业务代码总是写了不少的。但你想过做到零 bug 吗?我可是想过的,毕竟我还是有点追求的。不然每天都是浑浑噩噩地过,多没意思啊。

大概在一年多前,我给自己立下一个目标 —— 尽量将自己经手的业务需求做到零 bug。不试不知道,一试吓一跳,原来零 bug 还真的还不容易。今天,树哥就跟大家分享关于「业务开发零 bug」的一些思考。

要做到业务开发零 bug,其实挺难的。这涉及到非常多方面,有些方面可能还不只是你能控制的,例如:产品 PRD 详尽程度,产研组织的稳定性等等。经过一段时间的思考与摸索,我自己总结出一些影响因素,分别是:

  1. 产品需求文档的清晰程度
  2. 需求的复杂程度
  3. 开发人员的细心程度
  4. 开发人员是否详细自测过
  5. 开发人员对项目的熟悉程度
  6. 开发人员开发时间是否充足

针对上面说到的影响因素,我们一个个详细聊聊。

需求文档清晰程度

对于研发、测试人员来说,他们获取信息的源头就是产品的 PRD 文档。因此,需求文档是否写得清晰、明确,就显得非常重要。

如果产品自己对功能都不了解,那么输出的需求文档肯定「缺斤少两」,到时候就是边开发边补充需求,甚至是在测试过程中补充需求。遇到这种情况,想要做到零 bug 真的非常难。

因此,清晰明确的需求文档,是我们实现业务开发零 bug 的重要前提。如果这个前提保证不了,那要做到零 bug 真的很难。毕竟想做成啥样都不知道,程序员又不是神仙,咋能猜出你想要什么。但这块内容,更多是对于产品人员专业能力的要求,开发人员无法控制。

在一些公司,会再需求评审之前先对需求文档进行一次初审,筛除那些有明显重大问题的需求,这样可以减少一部分劣质需求。

但初审的作用还是有限的,它没办法对功能的细节做较多的判断。很多时候恰恰就是一些功能细节的缺失,导致了一些 bug 的诞生。

需求的复杂程度

需求的复杂程度,对于实现业务开发零 bug 也有很大的影响。举个简单地例子:一个改文案的需求,和一个完全重新做的功能。

这样的两个需求,其复杂程度差别很大,肯定是改文案的需求实现业务开发零 bug 的难度低很多。对于一个完全重新做的功能,要做到完全零 bug,对于开发人员的要求非常高。

对于越复杂的项目,零 bug 的可能性就越低。因此,很多项目为了追求产出功能的高质量,会采用将功能点拆得非常细的方式,来减少单个需求的复杂度。

笔者公司在去年做过这个尝试,确实是可以较大地提高产出功能的质量。

细心程度

前面说到需求文档的清晰程度很重要,这取决于产品人员对于业务的理解程度,以及对于对于功能的熟悉程度。开发人员的细心,就像是一个质检关卡一样,在开发之前就对产品的需求内容进行详尽的思考与提问。

对于粗心的开发人员来说,其可能不看需求文档就直接参加需求评审,等到开发的时候边写代码边看需求文档,其写得代码也是一边熟悉需求一边改。这样写出来的系统功能是比较差的,没有一个统一、全局的设计与思考,很容易在细节处发生问题。

一个细心的开发人员,其会在评审之前就详细阅读需求文档,甚至会前前后后翻阅好几次。他甚至会逐字逐句地阅读,弄懂每个文字、句子的意思,甚至有时候会让你觉得他是在玩文字游戏(但不得不说,确实有必要细致一些)。

最后会联系上下文思考功能的合理性。如果发现一些不合理的地方,他会积极与产品沟通反馈,以确保其对于需求的理解,与产品经理对于需求的理解是一致的。

通过对比,我们知道细心的开发人员对于产品经理来说,是一个莫大的帮助,可以帮助他查漏补缺,让其对于功能的考虑更加细致、严谨。

这里的开发人员不仅仅指的是后端开发人员,也包括前端开发、移动端开发,他们都会从不同角度提出问题。

对于后端开发人员来说,他们可能会提出性能问题。对于前端开发以及移动端开发同学,他们可能会提出交互问题、样式统一等问题。

简单地说,细心的开发人员可以弥补需求文档的缺陷,从而让大家对于需求的理解更趋于一致,从而减少 bug 的发生。因此,开发人员的细心程度也是决定业务开发能否实现零 bug 的关键因素!

是否详细自测过

即使写过 10 多年代码的开发人员,刷 Leetcode 也不敢说 bug free 一把过,对于更加复杂的业务代码更是如此。因此,要做到业务开发零 bug,其中一个很重要的操作便是 —— 自测。

自测可以帮你再次检查可能出现的问题,从而提高零 bug 的概率。对于我而言,我习惯性在自测的时候再次对照一遍需求文档,从而避免自己遗漏一些功能的细节点。

对于自测而言,业界有很多种自测方法,包括:单测、集成测试、功能测试。一般情况,建议自己选择适合自己的自测方法。

很多时候,功能测试是相对来说性价比较高的方式。除此之外,自测的详细程度也根据实际情况有所不同,例如有些人只会测试正常情况,但有些老手会测试一些边界情况、异常情况。

毫无疑问,你越能像测试人员一样测试,你的提测质量肯定就越高,bug 当然也就越少。

对项目的熟悉程度

这里说的项目熟悉程度,既指技术层面的熟悉程度,也指业务功能层面的熟悉程度。

技术层面的熟悉程度,指的是项目之间是用什么技术栈搭建的,你对这些技术是否都熟悉。举个很简单的例子,项目中采用了微服务的方式进行调用,那么你是否清楚是什么微服务调用?

如果采用了 ElasticSearch 进行搜索,那么你是否对 ElasticSearch 有一些了解,知道一些基本使用及最佳实践?等等。

这些算是技术层面的熟悉程度,你对这些越熟悉,你在技术层面发生问题的可能性就越小。

业务功能层面的熟悉程度,指的是你对项目其他模块的业务是否熟悉。例如你经常负责 A 模块的功能,你对 A 模块肯定很熟悉。

但下个迭代你就要去做 B 迭代的需求了,这时候你肯定不是很熟,相对来说出错的可能性就更大一些。

无论是技术层面,还是业务层面的熟悉程度,都会随着你做了更多的需求,变得更加熟悉。到了后面某个阶段,你基本上就不存在踩坑的问题了,也为你业务开发零 bug 奠定了基础。如果你是一个刚刚进入公司的新手,那么做到零 bug 还是很难的。

开发时间是否充足

开发时间是否充足,决定了你是否有充足的时间去熟悉需求,去和产品经理确定细节。有了充足的时间,你也才能有一定时间去进行更详细的自测。更为关键的一点,有充足的时间,你写代码才能写得更好。因此,开发时间是否充足是很重要的。

在实际的开发过程中,会因为各种各样的原因,其实并没有办法给你留出特别理想的开发时间。这时候该怎么办?有些人选择接受,去压缩自己的时间。

有些人则会选择去沟通,或者协调资源,保证自己有充足的时间。其实,正确的做法还是第二种,这样会更好一些。

这需要开发人员有更强的综合能力(沟通、协调能力),但并不是每个开发人员都具备的。关于这点,又是可以聊的一个话题 —— 当你的需求被压缩工时的时候,你应该怎么做?这里暂不展开,后续有时间可以聊聊。

简单来说,开发时间是基础,没有合理、充足的时间保障的话,要做到业务开发零 bug 是不可能的事情。

总结

要做到业务开发零 bug,其实就是要消除功能开发过程中的所有不确定性,包括:需求功能的不确定性、自己写错代码的不确定性等等。而发生这些不确定性的地方,可能就有:

  1. 产品需求文档的清晰程度
  2. 需求的复杂程度
  3. 开发人员的细心程度
  4. 开发人员是否详细自测过
  5. 开发人员对项目的熟悉程度
  6. 开发人员开发时间是否充足

除了上面说到的 6 个影响业务开发零 bug 的因素之外,肯定还有其他影响因素。

你能想到什么影响业务开发零 bug 的因素吗?

欢迎在评论区留言与大家分享。

好了,今天的分享就到此为止。如果你觉得今天的文章对你有帮助,欢迎点赞转发评论,你的转发对于我很重要,感谢大家!


作者:树哥聊编程
来源:mp.weixin.qq.com/s/XqCD9-epYHstWD3CS7hLEg
收起阅读 »

APP与H5通信-JsBridge

背景 在移动开发领域,原生应用嵌入网页(H5)可以实现一套代码多端使用,那么原生应用(APP)和网页(H5)之间的通信就非常重要。 JsBridge作为一种实现此类通信的工具,用于实现原生应用和嵌入其中的网页之间的通信。 H5与native交互,本质上来说就两...
继续阅读 »

背景


在移动开发领域,原生应用嵌入网页(H5)可以实现一套代码多端使用,那么原生应用(APP)和网页(H5)之间的通信就非常重要。


JsBridge作为一种实现此类通信的工具,用于实现原生应用和嵌入其中的网页之间的通信。


H5与native交互,本质上来说就两种调用:



  1. JavaScript 调用 native 方法

  2. native 调用 JavaScript 方法


JavaScript调用native方法有两种方式:



  1. 注入,native 往 webview 的 window 对象中添加一些原生方法,h5可以通过注入的方法来调用 app 的原生能力

  2. 拦截,H5通过与 native 之间的协议发送请求,native拦截请求再去调用 app 原生能力


本文主要介绍H5端与App(android和ios)之间通信使用方式。


代码实现


实现步骤:


这段代码实现的是 APP(Android 和 iOS) 和 H5 之间的通信。这个通信过程主要依赖于 WebViewJavascriptBridge 这个桥接库。这里是具体的流程:



  1. 初始化 WebViewJavascriptBridge 对象:



    • 对于 Android,如果 WebViewJavascriptBridge 对象已经存在,则直接使用;如果不存在,则在 'WebViewJavascriptBridgeReady' 事件触发时获取 WebViewJavascriptBridge 对象。

    • 对于 iOS,如果 WebViewJavascriptBridge 对象已经存在,直接使用;如果不存在,则创建一个隐藏的 iframe 来触发 WebViewJavascriptBridge 的初始化,并在初始化完成后通过 WVJBCallbacks 回调数组来获取 WebViewJavascriptBridge 对象。



  2. 注册事件:


    提供了 callHandlerregisterHandler 两个方法,分别用于在 JS 中调用 APP 端的方法和注册供 APP 端调用的 JS 方法。


  3. 调用方法:


    当 APP 或 JS 需要调用对方的方法时,只需调用 callHandlerregisterHandler 方法即可。



const { userAgent } = navigator;
const isAndroid = userAgent.indexOf('android') > -1; // android终端

/**
* Android 与安卓交互时:
* 1、不调用这个函数安卓无法调用 H5 注册的事件函数;
* 2、但是 H5 可以正常调用安卓注册的事件函数;
* 3、还必须在 setupWebViewJavascriptBridge 中执行 bridge.init 方法,否则:
* ①、安卓依然无法调用 H5 注册的事件函数
* ①、H5 正常调用安卓事件函数后的回调函数无法正常执行
*
* @param {*} callback
*/

function androidFn(callback) {
if (window.WebViewJavascriptBridge) {
callback(window.WebViewJavascriptBridge);
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady',
() => {
callback(window.WebViewJavascriptBridge);
},
false,
);
}
}

/**
* IOS 与 IOS 交互时,使用这个函数即可,别的操作都不需要执行
*/

function iosFn(callback) {
if (window.WebViewJavascriptBridge) { return callback(window.WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
const WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__BRIDGE_LOADED__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(() => { document.documentElement.removeChild(WVJBIframe); }, 0);
}

/**
* 注册 setupWebViewJavascriptBridge 方法
* 之所以不将上面两个方法融合成一个方法,是因为放在一起,那么就只有 iosFuntion 中相关的方法体生效
*/

const setupWebViewJavascriptBridge = isAndroid ? androidFn : iosFn;

/**
* 这里如果不做判断是不是安卓,而是直接就执行下面的方法,就会导致
* 1、IOS 无法调用 H5 这边注册的事件函数
* 2、H5 可以正常调用 IOS 这边的事件函数,并且 H5 的回调函数可以正常执行
*/

if (isAndroid) {
/**
* 与安卓交互时,不调用这个函数会导致:
* 1、H5 可以正常调用 安卓这边的事件函数,但是无法再调用到 H5 的回调函数
*
* 前提 setupWebViewJavascriptBridge 这个函数使用的是 andoirFunction 这个,否则还是会导致上面 1 的现象出现
*/

setupWebViewJavascriptBridge((bridge) => {
console.log('打印***bridge', bridge);
// 注册 H5 界面的默认接收函数(与安卓交互时,不注册这个事件无法接收回调函数)
bridge.init((message, responseCallback) => {
responseCallback('JS 初始化');
});
});
}

export default {
// js调APP方法 (参数分别为:app提供的方法名 传给app的数据 回调)
callHandler(name, params, callback) {
setupWebViewJavascriptBridge((bridge) => {
bridge.callHandler(name, params, callback);
});
},

// APP调js方法 (参数分别为:js提供的方法名 回调)
registerHandler(name, callback) {
setupWebViewJavascriptBridge((bridge) => {
bridge.registerHandler(name, (data, responseCallback) => {
callback(data, responseCallback);
});
});
},
};

使用 JSBridge 总结:


1、跟 IOS 交互的时候,只需要且必须注册 iosFuntion 方法即可,不能在 setupWebViewJavascriptBridge 中执行 bridge.init 方法,否则 IOS 无法调用到 H5 的注册函数;


2、与安卓进行交互的时候



  • 使用 iosFuntion,就可以实现 H5 调用 安卓的注册函数,但是安卓无法调用 H5 的注册函数,
    并且 H5 调用安卓成功后的回调函数也无法执行

  • 使用 andoirFunction 并且要在 setupWebViewJavascriptBridge 中执行 bridge.init 方法,
    安卓才可以正常调用 H5 的回调函数,并且 H5 调用安卓成功后的回调函数也可以正常执行了


H5使用


h5获取app返回的数据:


jsBridge.callHandler('getAppUserInfo', { title: '首页' }, (data) => {
console.log('获取app返回的数据', data);
});

app获取h5返回的数据:


 jsBridge.registerHandler('getInfo', (data, responseCallback) => {
console.log('打印***get app data', data);
responseCallback('我是返回的数据');
});


两者都可通信,只要一方使用registerHandler注册了事件,另一方通过callHandler接受数据


总结


主要介绍了原生应用嵌入网页(H5)与APP(android和ios)之间的通信实现方法。


这个通信过程主要依赖于 WebViewJavascriptBridge 这个桥接库。通过在JavaScript中调用native方法和native调用JavaScript方法,实现APP和H5的互通。


主要通过提供了 callHandlerregisterHandler 两个方法,分别用于在 JS 中调用 APP 端的方法和注册供 APP 端调用的 JS 方法。


更简单方式: APP与H5通信-postMessage


参考资料:


ios-webview


android-webview


参考案例


作者:一诺滚雪球
来源:juejin.cn/post/7293728293768855587
收起阅读 »

面试官:为什么不用 index 做 key?

web
Holle 大家好,我是阳阳羊,在前两天的面试中,面试官问了这样一个问题:“在 Vue 中,我们在使用 v-for 渲染列表的时候,为什么要绑定一个 key?能不能用 index 做 key?” 在聊这个问题之前我们还得需要知道 Vue 是如何操作 DOM 结...
继续阅读 »

Holle 大家好,我是阳阳羊,在前两天的面试中,面试官问了这样一个问题:“在 Vue 中,我们在使用 v-for 渲染列表的时候,为什么要绑定一个 key?能不能用 indexkey?”


在聊这个问题之前我们还得需要知道 Vue 是如何操作 DOM 结构的。


虚拟DOM


我们知道,Vue 不可以直接操作 DOM 结构,而是通过数据驱动、指令等机制来间接操作 DOM 结构。当我们修改模版中的数据时,Vue 会触发重新渲染过程,调用render函数,它会返回一个 虚拟 DOM 树,它描述了整个组件模版的结构。


举个栗子🌰:


<template>
<ul class="list">
<li v-for="item in list" :key="item.index" class="item">{{ item }}</li>
</ul>
</template>

<script setup>
import { ref } from 'vue';
const list = ref(['html', 'css', 'js'])
</script>

Vue 在渲染这个列表时,就会调用render函数,它会返回一个类似下面这个虚拟 DOM 树。


let VDom = {
tagName: 'ul',
props: {
class: 'list'
},
chilren: [
{
tagName: 'li',
props: {
class: 'item'
},
chilren: ['html']
},
{
tagName: 'li',
props: {
class: 'item'
},
chilren: ['css']
},
{
tagName: 'li',
props: {
class: 'item'
},
chilren: ['js']
}
]
}

虚拟 DOM 的每个节点对应于真实 DOM 树中的一个节点。


当我们修改数据时,Vue 又会触发重新渲染的过程。


const list = ref(['html', 'css', 'vue']) //修改列表第三项'js'->'vue'

Vue 又会生成一个新的虚拟DOM树:


let VDom = {
tagName: 'ul',
props: {
class: 'list'
},
chilren: [
{
tagName: 'li',
props: {
class: 'item'
},
chilren: ['html']
},
{
tagName: 'li',
props: {
class: 'item'
},
chilren: ['css']
},
{
tagName: 'li',
props: {
class: 'item'
},
chilren: ['vue']
}
]
}

注意观察,这里最后一个节点的子节点为'vue',发生了数据变化,Vue内部又会返回一个新的虚拟 DOM。那么 Vue 是如何将这个变化响应给页面的呢?


摆在面前的有两条路


要么重新渲染这个新的虚拟 DOM ,要么只新旧虚拟 DOM 之间改变的地方。


显而易见,只渲染修改了的地方是不是会更节省性能。


巧了,尤雨溪也是这样想的,于是便有了“ Diff 算法 ”。


Diff 算法


Vue 将新生成的新虚拟 DOM 与上一次渲染时生成的旧虚拟 DOM 进行比较,对比出是哪个虚拟节点更改了,找出这个虚拟节点,并只更新这个虚拟节点所对应的真实节点,而不用更新其他数据没发生改变的节点。


我自己总结了一下Diff算法的过程,由于代码过多,就不在此展示了:




  1. 新旧虚拟DOM对比的时候,Diff 算法比较只会在同层级进行,不会跨层级比较。

  2. 首先比较两个节点的类型,如果类型不同,则废弃旧节点并用新节点替代。

  3. 对于相同类型的节点,进一步比较它们的属性。记录属性差异,以便生成相应的补丁。

  4. 如果两个节点相同,继续递归比较它们的子节点,直到遍历完整个树。

  5. 如果节点有唯一标识,可以通过这些标识来快速定位相同标识的节点。

  6. 如果节点的相同,只是顺序变化,不会执行不必要的操作。



面试官:为什么不用 index 做 key?


平常v-for循环渲染的时候,为什么不建议用 index 作为循环项的 key 呢?


举个栗子🌰:


<div id="app">
<ul>
<li v-for="item in list" :key="item.index">{{item}}</li>
</ul>
<button @click="add">添加</button>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(['html', 'css', 'js']);
const add=()=> {
list.value.unshift('阳阳羊');
}
return {
list,
add
}
}
}).mount('#app')
</script>

这里用 indexkey渲染这个列表,我们通过 add 方法在列表的前面添加一项。


GIF 2024-3-5 23-57-41.gif


我们发现添加操作导致的整个列表的重新渲染,按道理来说,Diff 算法会复用后面的三项,因为它们只是位置发生了变化,内容并没有改变。但是我们回过头来发现,我们在前面添加了一项,导致后面三项的 index 变化,从而导致 key 值发生变化。Diff 算法失效了?


那我们可以怎么解决呢?其实我们只要使用一个独一无二的值来当做key就行了


<div id="app">
<ul>
<li v-for="item in list" :key="item.id">{{item.name}}</li>
</ul>
<button @click="add">添加</button>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(
[
{ name: "html", id: 1 },
{ name: "css", id: 2 },
{ name: "js", id: 3 },
]);
const add=()=> {
list.value.unshift({ name: '阳阳羊', id: 4 });
}
return {
list,
add
}
}
}).mount('#app')
</script>

GIF 2024-3-6 0-09-39.gif


这样,key就是永远不变的,更新前后都是一样的,并且又由于节点的内容本来就没变,所以 Diff 算法完美生效,只需将新节点添加到真实 DOM 就行了。


最后


看到这里,希望你已经对Diff 算法有了初步的了解,想要深入了解,可以自行查看Diff 源码。总的来说,Diff 算法是一项关键的技术,为构建响应式和高效的用户界面提供了基础。最后,祝你面试顺利,学习进步!



如果你正在面临春招,或者对面试有所疑惑,欢迎评论 / 私信,我们报团取暖!




技术小白记录学习过程,有错误或不解的地方还请评论区留言,如果这篇文章对你有所帮助请 “点赞 收藏+关注” ,感谢支持!!



作者:阳阳羊
来源:juejin.cn/post/7342793254096109583
收起阅读 »

深入理解 CSS:基础概念、注释、选择器及优先级

在构建网页的过程中,我们不仅需要HTML来搭建骨架,还需要CSS来装扮我们的网页。那么,什么是CSS呢?本文将带大家了解css的基础概念,注释、选择器及优先级。一、CSS简介1.1 什么是CSSCSS,全称为Cascading Style Sheets(层叠样...
继续阅读 »

在构建网页的过程中,我们不仅需要HTML来搭建骨架,还需要CSS来装扮我们的网页。那么,什么是CSS呢?本文将带大家了解css的基础概念,注释、选择器及优先级。

一、CSS简介

1.1 什么是CSS

CSS,全称为Cascading Style Sheets(层叠样式表),是一种用于描述网页上的信息格式化和显示方式的语言。它的主要功能是控制网页的视觉表现,包括字体、颜色、布局等样式结构。

Description

通过CSS,开发者可以将文档的内容与其表现形式分离,这样不仅提高了网页的可维护性,还使得样式更加灵活和多样化。

CSS的应用非常广泛,它可以用来控制网页中几乎所有可见元素的样式,包括但不限于文本的字体、大小、颜色,元素的位置、大小、背景色,以及各种交互效果等。

CSS样式可以直接写在HTML文档中,也可以单独存储在样式单文件中,这样可以被多个页面共享使用。无论是哪种方式,样式单都包含了将样式应用到指定类型的元素的规则。

1.2 CSS 语法规范

所有的样式,都包含在

<head>
 <style>
 h4 {
 color: blue;
 font-size: 100px;
 }
 </style>
</head>

1.3 CSS 的三大特性

Css有三个非常重要的特性:层叠性、继承性、优先级。

层叠性

相同选择器给设置相同的样式,此时一个样式就会覆盖(层叠)另一个冲突的样式。层叠性主要解决样式冲突的问题。

层叠性原则:

  • 样式冲突,遵循的原则是就近原则,哪个样式离结构近,就执行哪个样式
  • 样式不冲突,不会层叠

继承性

CSS中的继承:子标签会继承父标签的某些样式,如文本颜色和字号。恰当地使用继承可以简化代码,降低 CSS 样式的复杂性子元素可以继承父元素的样式(text-,font-,line-这些元素开头的可以继承,以及color属性)。

行高的继承性:

body {
 font:12px/1.5 Microsoft YaHei;
}
  • 行高可以跟单位也可以不跟单位
  • 如果子元素没有设置行高,则会继承父元素的行高为 1.5
  • 此时子元素的行高是:当前子元素的文字大小 * 1.5
  • body 行高 1.5 这样写法最大的优势就是里面子元素可以根据自己文字大小自动调整行高

优先级

当同一个元素指定多个选择器,就会有优先级的产生。选择器相同,则执行层叠性,选择器不同,则根据选择器权重执行。

Description

  • 权重是有4组数字组成,但是不会有进位。
  • 可以理解为类选择器永远大于元素选择器, id选择器永远大于类选择器,以此类推…
  • 等级判断从左向右,如果某一位数值相同,则判断下一位数值。
  • 可以简单记忆法:通配符和继承权重为0, 标签选择器为1,类(伪类)选择器 为 10,id选择器 100, 行内样式表为1000,!important 无穷大。
  • 继承的权重是0, 如果该元素没有直接选中,不管父元素权重多高,子元素得到的权重都是 0。

权重叠加:如果是复合选择器,则会有权重叠加,需要计算权重。

1.4 Css注释的使用

在CSS中,注释是非常重要的一部分,它们可以帮助你记录代码的意图,提供有关代码功能的信息。CSS注释以/开始,以/结束,注释内容在这两个标记之间。例如:

/* 这是一个注释 */
body {
    background-color: #f0f0f0; /* 背景颜色设置为浅灰色 */
}

在上面的例子中,"/* 这是一个注释 */"是注释内容,它不会影响网页的显示效果。

二、CSS选择器

在CSS中,选择器是核心组成部分,它定义了哪些HTML元素将会被应用对应的样式规则。以下是一些常用的CSS选择器类型:

2.1 基础选择器

基础选择器是由单个选择器组成的,包括:标签选择器、类选择器、id 选择器和通配符选择器。

2.1.1 标签选择器

标签选择器(元素选择器)是指用 HTML 标签名称作为选择器,按标签名称分类,为页面中某一类标签指定统一的 CSS 样式。

标签名{
 属性1: 属性值1;
 属性2: 属性值2;
 ...
}

标签选择器可以把某一类标签全部选择出来,比如所有的 <div> 标签和所有的 <span> 标签。

Description

优点:能快速为页面中同类型的标签统一设置样式。
缺点:不能设计差异化样式,只能选择全部的当前标签。

2.1.2 类选择器

想要差异化选择不同的标签,单独选一个或者某几个标签,可以使用类选择器,类选择器在 HTML 中以 class 属性表示,在 CSS 中,类选择器以一个点“.”号显示。

.类名 {
 属性1: 属性值1;
 ...
}

在标签class 属性中可以写多个类名,多个类名中间必须用空格分开。

2.1.3 id选择器

id 选择器可以为标有特定 id 的 HTML 元素指定特定的样式。
HTML 元素以 id 属性来设置 id 选择器,CSS 中 id 选择器以“#" 来定义。

#id名 {
 属性1: 属性值1;
 ...
}

注意:id 属性只能在每个 HTML 文档中出现一次。

2.1.4 通配符选择器

在 CSS 中,通配符选择器使用“*”定义,它表示选取页面中所有元素(标签)。

* {
 属性1: 属性值1;
 ...
}

2.1.5 基础选择器小结

Description

2.2 复合选择器

常用的复合选择器包括:后代选择器、子选择器、并集选择器、伪类选择器等等。

2.2.1 后代选择器

后代选择器又称为包含选择器,可以选择父元素里面子元素。其写法就是把外层标签写在前面,内层标签写在后面,中间用空格分隔。当标签发生嵌套时,内层标签就成为外层标签的后代。

元素1 元素2 { 样式声明 }
  • 元素1 和 元素2 中间用空格隔开
  • 元素1 是父级,元素2 是子级,最终选择的是元素2
  • 元素2 可以是儿子,也可以是孙子等,只要是元素1 的后代即可
  • 元素1 和 元素2 可以是任意基础选择器

2.2.2 子选择器

子元素选择器(子选择器)只能选择作为某元素的最近一级子元素。简单理解就是选亲儿子元素。

元素1 > 元素2 { 样式声明 }
  • 元素1 和 元素2 中间用 大于号 隔开
  • 元素1 是父级,元素2 是子级,最终选择的是元素2
  • 元素2 必须是亲儿子,其孙子、重孙之类都不归他管,也可以叫他亲儿子选择器

2.2.3 并集选择器

并集选择器是各选择器通过英文逗号(,)连接而成,任何形式的选择器都可以作为并集选择器的一部分。

元素1,元素2 { 样式声明 }

2.2.4 伪类选择器

伪类选择器用于向某些选择器添加特殊的效果,比如给链接添加特殊效果,或选择第1个,第n个元素。伪类选择器书写最大的特点是用冒号(:)表示,比如 :hover 、 :first-child 。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!
2.2.4.1 链接伪类选择器

为了确保生效,请按照 LVHA 的循顺序声明 :link-:visited-:hover-:active。因为 a 链接在浏览器中具有默认样式,所以我们实际工作中都需要给链接单独指定样式。

 /* a 是标签选择器 所有的链接 */ 
a {
 color: gray; 

/* :hover 是链接伪类选择器 鼠标经过 */
 a:hover {
 color: red; /* 鼠标经过的时候,由原来的 灰色 变成了红色 */
 }
2.2.4.2 :focus 伪类选择器

:focus 伪类选择器用于选取获得焦点的表单元素。焦点就是光标,一般情况 <input> 类表单元素才能获取,因此这个选择器也主要针对于表单元素来说。

input:focus {
 background-color:yellow;
}

2.2.5 复合选择器小结

Description
以上就是常用的css选择器的相关知识了,正确并灵活地运用各种选择器,可以精准地对页面中的任何元素进行样式设定。

通过这篇文章,相信你现在已经对CSS有了基础的了解,它是如何作为网页设计的基础,以及如何使用注释、选择器和优先级来精确控制你的网页样式。记住,CSS是一门艺术,也是一种科学,掌握它,你就能创造出无限可能的网页体验。

收起阅读 »

干货|AI浸入社交领域,泛娱乐APP如何抓住新风口?

2023年是大模型技术蓬勃发展的一年,自ChatGPT以惊艳姿态亮相以来,同年年底多模态大模型技术在国内及全球范围内的全面爆发,即模型能够理解并生成包括文本、图像、视频、音频等多种类型的内容。例如,基于大模型的文本到图像生成工具如DALL-E 2、Imagen...
继续阅读 »

2023年是大模型技术蓬勃发展的一年,自ChatGPT以惊艳姿态亮相以来,同年年底多模态大模型技术在国内及全球范围内的全面爆发,即模型能够理解并生成包括文本、图像、视频、音频等多种类型的内容。例如,基于大模型的文本到图像生成工具如DALL-E 2、Imagen等以及文生视频模型Sora的发布标志着这一领域的重要突破。这些动态表明,AI 领域的竞争日益激烈,呈现出百模争流的局面。


本文将深入剖析AI对社交领域的应用带来了哪些新的机遇和挑战。

+AI和AI+ 深入社交领域

AI 在社交领域的应用,当下主要集中在 +AI和 AI+ 两种形式。+AI 主要解决什么会更好,即在原先成熟的产品中,添加了 AI 功能,触发新场景和新玩法,包括如下场景:

  • 社交约会:通过虚拟分身,减少破冰投入,增加匹配度,代表 APP 如 snack、Synclub

  • 社交游戏:AI 与人类混合社区,比如一个由 AI 生成的 Instagram 或 X 平台,而人类可以随时参与其中

AI+ 主要解决什么会出现,包括现在以大模型为基础的生成式机器人、虚拟伴侣等,包括如下场景:

  • AI伴聊:character.ai、Poe、replika、talkie、星野、筑梦岛

  • AI助手:chatGPT、豆包、文心一言

AI 渗透社交领域 机遇与挑战并存

AI 在社交领域的广泛使用,为企业和开发者在提升个性化体验、提高平台管理效率及内容生成管理等方面均带来了不少机遇。利用 AI 的数据分析能力,为用户提供更加个性化的内容推荐、社交互动等服务,提高用户参与度和满意度;AI 可以用于内容审核、虚假信息检测等,减轻人工负担,提高平台的安全性和可靠性;AI可以自动识别和分类文本、图像和视频内容,辅助内容创作者进行创作,并能快速处理大量用户生成的数据。

机遇不少,但挑战并存。企业和开发者还面临着数据隐私和安全、算法偏见和歧视等问题,以及如何恰当处理人机关系的变化。AI 应用需要大量的数据支持,这可能引发用户数据隐私和安全方面的担忧;由于训练数据的不均衡或算法设计的缺陷,AI 可能会产生偏见和歧视,影响公平性;AI 的广泛应用可能会改变人与人之间的互动方式,引发社会结构和人际关系的变化。

未来何去何从?以下这些趋势洞察也许可以给你带来新的思考

AI 聊天应用相对比较稳定,占据保持位置


深入挖掘垂直场景才具备竞争力

除了少量产品之外其他产品都有自研 AI 大模型,在头部产品功能越来越全面的当下,靠兼容多款大模型、多种功能的第三方 AI 产品的生存空间越来越小了,面向 C 端用户,单纯套壳+做薄的应用意义不大,需要深入挖掘垂直场景才具备竞争力。


市场饱和?布局出海是大方向

一些企业将 ChatGPT、Claude 等前沿大模型 API 与特定场景相结合,快速研发出垂直细分市场的社交应用,如北美市场上线的 talkie、coze,百度在日本等地上线名为“SynClub”的AI社交产品,标志着中国公司在海外市场创造了新的商业模式和服务模式,深受海外市场用户和企业的认可与接纳。随着AI技术的进一步发展和完善,预计这一领域的创新应用将更加深入和广泛。

加强监管与自律

AI 技术在社交领域的应用仍面临一些挑战。一方面,AI 可能导致隐私泄露问题,用户的个人信息和数据可能被不当利用。另一方面,由于算法的局限性,AI 可能存在偏差和误判,影响社交互动的质量。为了实现 AI 与人类的共同发展,在技术创新的同时,关注伦理和社会影响。通过建立透明的算法和数据管理机制,保障用户隐私和数据安全,同时 AI 本身也将用于自我监管,强化平台的自治能力。

IM+多种大模型 聊天体验更顺畅、高效

环信作为国内即时通讯云领域的开创者,率先将IM(即时通讯)和多种大模型服务结合在社交领域中,可以为用户提供更加顺畅、高效和个性化的聊天体验,同时也有望在社交应用程序中实现更多的智能化功能,创新更多社交新玩法,从而帮助APP提高活跃度、用户满意度和忠诚度。


海量并发,稳定可靠的平台能力

支持多重备份、灾备恢复、回调容灾等技术手段,单日数十亿级别的消息传输和处理,SLA99.99%,持续保障系统高可用性和可靠性。

国际化加速,提升出海使用体验

提供快速、准确的消息传递和响应,全球平均时延小于100ms,使得用户交互过程流畅自然,提升应用的竞争力和用户满意度。

易开发,方案快速上线

开发者可以通过调用API等方式快速构建智能交互功能,提供开箱即用的场景化demo,最快1天实现方案快速验证。

内容审核,为应用安全保驾护航

基于先进的算法和AI技术,在保证高效性和准确性的同时,自动检测和屏蔽不合规信息,确保聊天环境的健康和安全。

安全合规,保障用户隐私安全

支持国、内外不同区域合规要求,根据最小化和公开透明处理原则,保护不同区域的网络安全、数据安全及用户隐私安全

卓越服务,助力战略愿景落地

支持全球范围内的企业级客户服务,具备丰富的行业标杆客户案例,提供专属方案咨询、集成顾问、营销推广及客户成功保障服务。

AI 对社交领域的影响是深远而广泛的。它为人们提供了更加便捷、高效的社交方式,同时也带来了一些挑战。在未来的发展中,我们需要关注技术的发展趋势,用审核的眼光分析AI技术的优劣势,判断AI+社交领域的产品是否做到了“扬长避短”,同时也期待2024年,环信携手各行业客户打造Killer Apps。

相关文档:

收起阅读 »

【Harmony OS 鸿蒙下载三方依赖 ohpm环境搭建】

前言:ohpm(One Hundred Percent Mermaid )是一个集成了Mermaid的命令工具,可以用于生成关系图、序列图、等各种图表。我们可以使用ohpm来生成漂亮且可读性强的图表。本期教大家如何搭建ophm环境:一、在DevEco Stud...
继续阅读 »

前言:ohpm(One Hundred Percent Mermaid )是一个集成了Mermaid的命令工具,可以用于生成关系图、序列图、等各种图表。我们可以使用ohpm来生成漂亮且可读性强的图表。

本期教大家如何搭建ophm环境:

一、在DevEco Studio中,将依赖放到指定的 oh-package-json5的dependencies 内

二、打开 Terminal 执行 :“ohpm install”
(1).成功会提示
如果提示成功则该ohpm配置是正确的

(2).失败会提示
ohpm not found ! 大概意思就是找不到这个ohpm
三、解决小标题(2)的失败提示:
1.查阅我们的ohpm地址 一定要记住这个地址
Mac端找到该位置路径 点击DevEco Studio ->Preferences

2.打开终端命令行 注:如果不需要以下图文引导 请向下拉 下方有无图文引导方式
输入:echo $SHELL 输入后 单击回车

3.提示/bin/zsh
(1)执行: vi ~/.zshrc 后点击回车

(2)进入到该页面 输入 i

(3)拷贝:export OHPM_HOME=/Users/xxx/Library/Huawei/ohpm
export PATH=${PATH}:${OHPM_HOME}/bin
1.中间的 xxx 输入 在标题三的1图中 /Users 每个用户名都不一样 不要直接填xxx 按照路径给的填写即可。

(4)编辑完成后,单击ESC ,即可退出编辑模式,然后输入“:wq!”,单击回车保存


5)输入: source ~/.zshrc;
以下是无图文引导方式:
(1)执行: vi ~/.zshrc(2)输入 i
(3)export OHPM_HOME=/Users/xxx/Library/Huawei/ohpm
export PATH=${PATH}:${OHPM_HOME}/bin
中间的 xxx 输入 在
标题三的1图中 /Users 每个用户名都不一样 不要直接填xxx 按照路径给的填写即可。
(4)编辑完成后,单击ESC ,即可退出编辑模式,然后输入“:wq!”,单击回车保存
(5)输入: source ~/.zshrc;
4.提示/bin/base
(1)执行: vi ~/.bash_profile
(2)输入 i
(3)export OHPM_HOME=/Users/xxx/Library/Huawei/ohpm
export PATH=${PATH}:${OHPM_HOME}/bin
中间的 xxx 输入 在
标题三的1图中 /Users 每个用户名都不一样 不要直接填xxx 按照路径给的填写即可
(4)编辑完成后,单击ESC ,即可退出编辑模式,然后输入“:wq!”,单击回车保存
(5)输入: source ~/.bash_profile
四、检验 ohpm环境是否配置成功:
命令行输入 export 查验是否有 ohpm
五、检验方式第二种 输入 ohpm -v 会显示你的 版本




收起阅读 »

【手把手教Android聊天室uikit集成-kotlin 第一期】

前言:环信提供一个开源的 ChatroomUIKit 示例项目,演示了如何使用该 UIKit 快速搭建聊天室页面,实现完整业务。本文展示如何编译并运行 Android 平台的聊天室 UIKit 示例项目。#一、详细步骤导入uikit二、遇到集成报错解决&nbs...
继续阅读 »

前言:环信提供一个开源的 ChatroomUIKit 示例项目,演示了如何使用该 UIKit 快速搭建聊天室页面,实现完整业务。

本文展示如何编译并运行 Android 平台的聊天室 UIKit 示例项目。

一、详细步骤导入uikit
二、遇到集成报错解决 
1. 从github下载的附件我们打开以后 会有两个 一个是ChatRoomService ,另外一个是ChatroomUIKit

2.先倒入UIkit的本地库(引导的内容可以参考标题1. 的绿色箭头第二个文件夹)

3.然后在导入ChatRoomservice 选择文件后也点击Finish 注: 一共两个文件 都需要导入
4.填写settings.gradle
include(":ChatroomUIKit")
include(":ChatroomService")

5.添加:build.gradle(app)
implementation(project(mapOf("path" to ":ChatroomUIKit")))

6.如果遇到该报错如下:
遇到报错如下:
Dependency 'androidx.activity:activity:1.8.0' requires libraries and applications that depend on it to compile against version 34 or later of the Android APIs.
:app is currently compiled against android-33.
Also, the maximum recommended compile SDK version for Android Gradle
plugin 7.4.2 is 33.
Recommended action: Update this project's version of the Android Gradle
plugin to one that supports 34, then update this project to use
compileSdkVerion of at least 34.
Note that updating a library or application's compileSdkVersion (which
allows newer APIs to be used) can be done separately from updating
targetSdkVersion (which opts the app in to new runtime behavior) and
minSdkVersion (which determines which devices the app can be installed
解决方案: 注意一下自己app的 targetSDK版本号以及compilesdk 都给到 34 大概在报错信息也能提示到是 需要强制到34
7.初始化UIkit

(1)appkey管理后台位置

8.客户端登录调用
ChatroomUIKitClient.getInstance().login("4","YWMtFTJV-OXGEe6LxEWLvu_JdPqlsNlfrUUAh3km7oObq2HVh7Pgj9ER7JuEZ0XLQ13UAwMAAAGOVbV_AAWP1AB9sFv_7oIlDyK7Jay0Coha-HnF5o0PnXttL7r4gxryCA", onSuccess = {
val intent = Intent(this@MainActivity, As::class.java)
startActivity(intent)

}, onError = {
code, error ->


})


(1)参数管理后台具体位置 ,每次点击查看token的token内容都是不同的,这个不必担心。


(2)跳转到Asactivity 后遇到了一个问题!
继承ComponentActivity() 无法拿到setContent
解决办法:将这个依赖升级到 1.8.0 刚才用了1.7.0版本 无法拿到这个setContent
implementation("androidx.activity:activity-compose:1.8.0")
9.展示进入聊天室逻辑
class As : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent{
ComposeChatroom(roomId = "242681589596161",roomOwner = UserInfoProtocol)
}
(1)参数roomId 在管理后台可以查看

(2)roomOwner 为 UserInfoProtocol 类型 ,可以自己定义编辑属性将参数存入方法内


收起阅读 »

做好离职管理,享受非凡人生

近年来,职场竞争越来越激烈,每个员工都希望通过努力工作,赢得领导的认可和提拔机会。 然后,不可否认的是,有人粉墨登场,就有人卸妆离场。离职是职业生涯中一个重要的转折点,它不仅是结束一段工作关系,也是展示个人素质和职业态度的重要时刻。 人们常说“离职见人品”,这...
继续阅读 »

近年来,职场竞争越来越激烈,每个员工都希望通过努力工作,赢得领导的认可和提拔机会。


然后,不可否认的是,有人粉墨登场,就有人卸妆离场。离职是职业生涯中一个重要的转折点,它不仅是结束一段工作关系,也是展示个人素质和职业态度的重要时刻。


人们常说“离职见人品”,这句话凸显了离职时人品的重要性。而对于某些管理岗位,往往在招聘时,都是会背调的。


离职时的表现往往能够反映出一个人的职业素养和道德水准。


在离职过程中,一个有良好人品的人会以积极、负责的态度对待工作交接,尽力确保工作流程的顺畅进行,不给公司和团队带来不必要的麻烦。


他们会与上级和同事进行充分的沟通,表达对公司的感激之情,并保持良好的合作关系。


相反,一些人在离职时可能会表现出不良的行为。


他们可能会消极对待工作交接,甚至故意隐瞒重要信息或破坏工作进度,给公司和团队带来困扰。不要认为你在这家公司做的不爽,你就也想让公司不爽,这样的行为不仅缺乏职业道德,也可能对个人的声誉和未来的职业发展产生负面影响。


我身边一个真实的案例,我的前同事(主管级别)因为不服空降领导,在持续了三个月与领导发生冲突和争吵后,愤然离职甩脸色,再其离职后,仍发邮件给公司高层打报告。现在他基本在杭州很难混下去,因为背调基本上打电话过来给目前的领导,询问其人品,结果可想而知了。上个月还听到领导跟我交流,说某某公司找他背调前同事。


那么,在离职时如何展现良好的人品呢?我们如何体面的告别自己的工作呢?


01. 提前通知


在离职前,提前向上级和同事发出离职通知是一种负责任的行为。


这样做可以给予公司足够的时间来安排工作交接,确保工作流程的顺畅进行。


通过提前通知,公司可以有足够的时间找到合适的人选来接替你的职位,避免因你的离职而导致工作的中断或延误。


同时,这也为你的同事提供了准备和适应的时间,以便他们能够顺利接手你的工作职责。这样的做法不仅展现了你的职业素养,也有助于维护良好的人际关系。


2. 积极配合


在工作交接期间,积极配合同事和上级,确保工作流程的顺畅进行。比如:
1、制定详细的交接计划:与上级和同事一起制定详细的交接计划,明确交接的内容、时间和责任人。这样可以确保交接工作有条不紊地进行。
2、分享工作文档和资料:将你的工作文档、资料和相关信息整理好,并与同事和上级分享。这将帮助他们更好地了解工作的细节和进展,以便顺利接手。
3、提供培训和指导:如果同事需要,你可以提供培训和指导,帮助他们熟悉工作流程和项目。这将有助于他们更快地适应新的工作职责。
4、积极回答问题:在同事和上级有疑问或需要帮助时,积极回答问题并提供支持。保持沟通畅通,确保他们能够顺利进行工作。
5、参与重要会议和项目:如果可能的话,参与一些重要的会议和项目,以便更好地了解工作的最新情况,并在必要时提供帮助。


3. 保持联系


离职时,你要向公司领导和同事表达感激之情,感谢他们在工作中给予的支持和帮助。不要因为工作的事情,搞得自己不开心。
还有你一定要清楚,离职了不是永别了,在你工作中积累的同事和领导关系,都是你日后在这个社会中的潜在资源,离职时把关系维护好,说不定哪天,他们能在某个地方帮助到你。


离职后,与前同事和上级保持联系,建立良好的人际关系。


edc24cf0a4794418a044541d72cb2cc4_3.png


4. 不抱怨不诋毁


静坐当思己过,闲谈莫论人非。在离职过程中,不要抱怨公司或同事,也不要在背后诋毁他们。抱怨和诋毁只会让自己显得狭隘和小气,对于未来的职业发展也没有任何好处。即使在离职过程中存在一些不愉快的事情,我们也应该以成熟的方式处理,保持良好的沟通和合作。


5.结语


总之,“离职见人品”这句话提醒我们,在职业生涯中,我们的行为和态度不仅影响着当前的工作环境,也会对未来的发展产生深远的影响。在离职过程中,我们应该保持积极的态度,不要抱怨或诋毁公司或同事。学会感恩,以成熟的方式处理事情,保持良好的人际关系,这些都是我们未来职业发展的宝贵财富。因此,无论离职与否,我们都应该始终保持良好的职业素养和道德标准,以展现自己的优秀品质。


676153ad0a7146a796cc55819a5999bd_1.png


作者:陆理手记
来源:juejin.cn/post/7347910130711035913
收起阅读 »

号外:小程序获取手机号要付费了 0.03元/次 !😒

前言 今天无意中得知了 v2ex.com 打不开的原因,原来规则判断走的是国内线路,需要启用全局连接,切换后就第一次成功访问该站点,没想到就在头条发现了这么一条吐槽微信小程序的消息 获取手机号要收费了,一度以为是假消息,经过验证,发现竟然是真的。 从微信开发...
继续阅读 »

前言


今天无意中得知了 v2ex.com 打不开的原因,原来规则判断走的是国内线路,需要启用全局连接,切换后就第一次成功访问该站点,没想到就在头条发现了这么一条吐槽微信小程序的消息 获取手机号要收费了,一度以为是假消息,经过验证,发现竟然是真的。


截屏2023-06-26 21.59.04.png


从微信开发文档中确实发现了这条收费信息,貌似是今天刚更新的,然后就在刚刚,收到了官方消息通知。


截屏2023-06-26 19.53.48.png


关键信息


目前的获取手机号组件,将从 8月26号 开始,以 0.03元/条 的价格收费,每调用成功计数一次。


每个小程序总共赠送 1000 次免费额度(不是每月送一次哦),注意这 1000 次是包含开发版、体验版、正式版的。


除了现在的获取手机号组件外,腾讯又推出了一个新的组件,也是用来获取手机号的,区别在于是实时获取用户最新的,可能之前的组件过去的是有一段时间内的缓存数据?这个组件价格为 0.04元/次


以上两个组件具体收费,貌似是可以按照套餐来购买的,具体方案可以在小程序后台的付费模块查看。


充值或者购买后,可以在小程序后台付费菜单模块,查看额度,以及每日的使用情况,当费用达到临界值时候,也会通过消息提醒你充值续费的。nnd真贴心


截屏2023-06-26 19.49.30.png


截屏2023-06-26 19.51.38.png


总结下


还有两个月时间,可以开始梳理自己公司的小程序了,然后找老板讨论方案,该掏钱掏钱,该改方案就改方案吧;


其实想想,企业项目的话公司花钱,自己也不用心疼,个人项目的话,估计也没几个用户,更花不了几个钱,对程序员来说,没啥影响;


不过腾讯这波操作,感觉像缴人头税一样,难道是经济不好,开始拓展创收渠道了?会不会以后每一个微信生态 api 都要单独给钱才能用呢 😒?


据说现在全网小程序已经突破 700万个了,假设每个先充值 1000 块,那就是70亿啊!!这波操作,属实做到了我曾经想做而做不到的事,全国人民给我1块钱,我立马成亿万富翁了,只能说 666, 服了。




作者:Ethan_Zhou
来源:juejin.cn/post/7248909699961634853
收起阅读 »

4年零4天,我从毕业后的第一家公司离职了

写在前面 从上家公司离职已经有一段时间了,当我打开备忘录翻看以前每天的todolist,成长历程历历在目,也觉得有必要写一篇文章对我毕业后的第一份工作做一个阶段性的总结。 离职原因 我的第一家公司是杭州某大型车企旗下的一家网约车公司,19年11月份拿到校招o...
继续阅读 »

写在前面


从上家公司离职已经有一段时间了,当我打开备忘录翻看以前每天的todolist,成长历程历历在目,也觉得有必要写一篇文章对我毕业后的第一份工作做一个阶段性的总结。


image-20240224093538751.png


离职原因


我的第一家公司是杭州某大型车企旗下的一家网约车公司,19年11月份拿到校招offer就开始进入公司实习了,2020年6月份正式转正,在2023年11月底离职,所以满打满算应该在这家公司呆了有四年的时间。


至于为什么离职,有以下几点原因:



  1. 首先是感觉到个人成长受限。不知道大家有没有同感,在一家公司时间呆久了之后,就有一种温水煮青蛙的感觉,看到一些老青蛙,就像看到了未来的自己,会有危机感。

  2. 其次是我个人原因,想进大厂。由于自己的学历在市场上不是很有竞争力,希望通过大厂经历能够稍微弥补一下,方便为自己以后长远的职业生涯做打算。

  3. 当然公司的原因也有,首先是公司的管理层和公司文化发生了变化。简单来说,就是从之前的车企文化变成了"福报文化",就让我有一种“在哪儿卷不是卷?”的想法。


    其次,公司的战略也让我感觉到一种危机感,从各方面的信息来看,现在是一种破釜沉舟的心态,但是破釜沉舟了这么久,也没有看到成效,反而是公司的一部分员工因为所谓的战略,需要将办公地点迁到苏州。虽然还没有轮到我们部门,那么如果真的有那么一天,我还是得准备离职,还不如早做打算。



成长历程


在这家公司一共有过两次晋升,一次是在2022年,一次是在2023年,算上普调,应该是有过三次涨薪。


刚开始进入公司的时候,是以实习生的身份,那时候我刚从一家几百人的小公司实习结束,也是我第一次亲身经历了比较正规的研发流程和研发规范。刚开始的时候,每天都有很多低级问题要问,和测试、产品同学沟通起来也是十分的不流畅,加上当时刚好赶上了疫情,在家办公了一段时间,好在实习的表现还算满意,我的4个月实习让我免去了试用期,拿到毕-业-证以后直接转正了。


毕业后分配到了营销小组,这是我第一次做To C的业务,经常会出一些线上问题,收到用户的投诉,当时的老板对于线上问题的忍耐程度很低,很小的问题都会被无限放大,导致我当时在做需求,需求的上线都是处于一种极度紧张的状态,一旦出现线上问题,都会默默的打开Boss 直聘,做好找下一份工作的准备。


在之后的1 ~ 3的工作经历中,我渐渐找到了工作的节奏,以及如何应对一些人际关系,处理起线上问题也比以前镇定多了。第二年和第三三的绩效都比较好,而且后来的某一段时间内,公司优化掉了大部分的测试同学,我们研发写的代码需要研发进行自测,我们的需求研发周期变成了研发时间 + 自测时间,我觉得这给了我们研发更多的Buffer,能把一个需求做的更好,甚至我经常能留出多余的时间来做一些okr相关的项目。


在离开公司的前半年时间,我的工作又发生了很大的变化,在完成日常的工作之外,我还积极参与了公司的游泳社团、羽毛球社团、篮球社团。分别在周二、周四、周五跟着公司的小伙伴一起参加体育运动,这对我的身体状况有了很大的改善。


这三年半有什么成长?



  1. 技术上的成长:技术上的成长当然是首位的。刚来实习的时候,这会写一些简单的jsx语法,后来在能完成公司的正常业务迭代之外,还参与了公司脚手架的建设,帮助解决一些框架上带来的问题。同时还有机会接触到APM监控系统的核心流程,对APM这个功能进行迭代。另外还有一些简单的BFF开发、低代码平台的开发、埋点核心链路的监控、微信/支付宝小程序...

  2. 心态上的成长:刚毕业的时候,心态是很脆弱的,经常会因为一些小事情,心里就默念:“不干了!”。现在的心态是:“挣钱嘛,受点委屈怎么了?”

  3. 时间管理上的成长:由于要在工作之外还要保证自己的健康状态、和个人成长。所以在时间管理上,我渐渐有了一套自己的管理模式。比如每周要锻炼多少次,最近一段时间要把这个知识点复习完毕,这个年度要去几个城市旅游,今年要学会游泳,等等。


    我甚至学会了利用地铁的通勤时间做一些知识点的复习、阅读书籍、做一些今天的时间规划之类的事情,我觉得是一个很大的成长。


  4. 学习能力的成长:毕业之前,我只会通过看视频来学习,看文字,看文档,我都很难学习到知识,经常遗漏一些关键点。现在我甚至很排斥看视频这种方式,觉得很浪费时间,很啰嗦...


健康状况


除了我的胃,其他都没有什么大毛病,这里要告诫一下大家一定要注意保护自己的胃,胃病真的想象的那么简单。


由于疫情期间在家里没怎么活动,加上暴饮暴食,饱腹后继续喝茶... 以及后来复工之后在公司天天吃外卖,导致我的胃经常胀气。中午午睡的时候顶的难受睡不着觉,晚上睡觉的时候肚子也胀胀的,需要起身好几次打几个嗝才能睡得着,对我的睡眠造成了很大的困扰。


期间也去医院看过,做过胃镜,吃过中药,但是效果不是那么明显。后来很长一段时间没有吃辣的,也按时吃饭,不吃夜宵,经常跳绳锻炼一下,渐渐有所好转。


面试历程


其实跳槽这件事情是我在一两年前就计划好的。在22年底的时候,其实就出去面试过,不过当时没有准备的特别充分,加上当时太天真,还是一副刚毕业时年轻气盛的样子,导致很多面试官对我的印象都不是特别好。




一些感谢的人


首先感谢一下我上家公司的几位直系上司。我呆过两个组,两个组长都对我十分不错,一位在我实习期间给了我很多引导,帮我快速熟悉公司的业务。另外一位则是在我成为正式员工之后,帮我担下了很多的责任,并且帮助我快速的成长,成功获得了两次晋升的机会,并且都顺利通过。


另外感谢一下我的专家,真的是一位特别好的人,从来不会PUA下属,总是会以过来人的角度给你一些很好的建议。


有没有什么要吐槽的


如果非有什么需要吐槽的,还是公司的文化吧。并不是说现在的文化有什么问题,我想吐槽的是变化。因为曾经美好过,所以当注入新鲜血液之后的变化,其实让很多经历过美好的人会有一些失望😂😂😂


写在最后


感觉这四年时间过的很快,从一个职场新人变成了拥有三年多经验的打工人。昨天晚上还在家里翻出之前入职培训时的一些合影,当时我记得校招入职的时候有54人,算上被优化和自己主动离职的,目前应该还剩下不到10人。


希望老东家越来越好,早点实现盈利上市!!!


也希望自己在下一份工作中能够快速适应,能够给N年后的自己也交上一张满意的答卷。


作者:枣仁
来源:juejin.cn/post/7339042131468697640
收起阅读 »

春天的痛与爱,教会了我善良

看了看天气预报,从明天开始天气就变暖了,路两旁的花也开了,有些树也冒出了新芽,再过一段时间,就可以真正意义上感受春天的气息了。昨晚做了一个梦,梦到了好多小伙伴,大家一起去爬山,因为还有几个妹子一起,所以我就像孙悟空一样这棵树爬一下,那棵树吊一下,以至于今天早上...
继续阅读 »

看了看天气预报,从明天开始天气就变暖了,路两旁的花也开了,有些树也冒出了新芽,再过一段时间,就可以真正意义上感受春天的气息了。图片昨晚做了一个梦,梦到了好多小伙伴,大家一起去爬山,因为还有几个妹子一起,所以我就像孙悟空一样这棵树爬一下,那棵树吊一下,以至于今天早上醒来后,全身酸痛,差点班都不想去上,此刻,打着字,手依然是酸痛的。


我感到有点后悔,因为现实中,我算是已经很少去装X了,但是梦里居然还是那个德行,不过想一想,这怪我了?梦啥我能决定吗?瞬间心里好受了一点,就当锻炼了吧,平时很少锻炼,梦里能锻炼一下也不错。


大概从明天开始后就不会再有多过分冷的天气了,所以这个梦就当做真正意义上春天的开场曲了,春夏秋冬这四个季节,我最喜欢春天,因为这个季节里有美好的回忆,也有令人痛苦的回忆。


我爷爷家门口有一片竹林,春天的时候,竹林里面会长很多竹蛋,竹蛋开花后就是竹荪,是很不错的美味,竹林下面是一条小沟,整个村子农作物灌溉的水都需要从这里过,而那会我们经常要去堵水,堵水的意思就是去几公里外的源头抢一点水,然后让水流向往村子里的小沟,途中不断有支流,直到流到自己家的田里。


从源头到我家田里大概有四五公里的路程,那会我经常顺着小沟走到田里,因为是丘陵地带,所以一直在半山腰上走,而山腰下面全是农田,到处都是一片绿油油的,那时候家里养了一只小狗,因为比较贪吃,名字叫做馋瓢,我偶尔还会带着它顺着小狗走,它经常还会捉到一些小野生动物。


那会对外界没有什么概念,连乡镇都没去过,只是在我爷爷家墙上的报纸上能了解一点,所以每当看到田野,高山的时候,心中都会产生一些幻想。


三年级是在乡镇上读书,那会拳皇97和三国战记2007特别风靡,于是那会我经常在游戏厅里打上几个小时,三国经常一个币打到通关,那会父母在浙江打工,后面他们回来直到我经常去打游戏,于是经常被骂,后面就很少去了。


不过狗改不了吃屎,虚拟的我玩不了就不玩,但但是现实中我可不放过,于是和一个比我小几级的同村小伙伴,两个用木棒自己做武器,我喜欢孔明,于是做了一把剑,他喜欢赵云,于是搞了一杆长枪。


那时候正是春天,我们两个每天都去别人家地里将菜和秸秆当作小兵,疯狂冲杀,于是经常被别人骂上好几个小时,现在回想起来,看着别人心疼的捡起碎了的蔬菜,真的觉得自己做了好大的孽了。


四年级的时候,捡到了一个类似于MP3那种小玩意,可以收音,于是我在租房子过去的坟头上收音,那会信号是很弱的,需要来回走动才能收到信号,就听了一些什么国际形势的东西,但是啥也不懂,只是觉得能够收入来自于外面的声音,就觉得很幸福,因为小镇上没有网吧,每次都会在坟头来回走几百次,坟头草都给别人踩没了,不过还好,春天里的生命力是无比顽强的,没了又会快速长出来。


我只在小镇待了一年半,但是那段发生在春天里的故事至今无法忘怀,因为都有点作孽,所以教学了以后我要善良一点。


到了县城读书后,因为南方的雨水比较多,春天和夏天经常水淹县城,那会我妈在街上买水果,每当下大雨的时候,我都赶快去给她收摊,记得有一次雨特别大,我给她收完摊子,看到路上好多被雨淋着的背篼(贵州的一种职业)。


于是回到家里,我搞了两把雨伞,冒着大雨就出门了,在街上遇到一个六十多岁的背篼,我就主动给他伞,然后送他回家,送他到家后,我就原路返回回家了,一路上水淹到了膝盖,还有电闪雷鸣,特别吓人。


回到家后,我全身湿透了,被骂了一顿,我记得我妈说:关你什么事,这么危险,你要是被水淹死了怎么办。


我当时哭着说:我还不是希望有一天你们遇到这种事情的事情,有人也能帮助你们,我有错吗?


那会特别委屈,多年后我才意识到,父母不是反对你做这种事情,而是太危险了,要是真的运气不好,被水冲走了怎么办。并且在我说出那句话时,她明显带有微笑。


后面她继续卖了四五年的水果,后面又卖烧饼,不知道在我没看到的时候,有没有人帮她推过三轮车,不过这一切都不重要了。


在后来初高中岁月里,我遇上了第一个喜欢的女孩子,并且也谈恋爱了,也是在春天,凌晨六点我家在她家门口等她,然后拉着她在东山下面跑步,呐喊,不过相处的时光依然很短,后面她辍学了,打了一个电话给我爸,叫我去她家,于是分手了。


那天夜里,我在东山下面转了很多圈才回去,后面的几个月里,也比较颓废,不过在那个年纪一切都情有可原。


几年后,我顺着火车路跑步到她家,然后做了一碗面条给我吃,瞎聊了很久,过了一年她就结婚,也是在春天,我不再顺着火车路跑步去她老家,而是骑着摩托车过去了,从那会起,我才算真正的放下。


后面,就没有再联系过了,东山的春天依然没变,只是那片土地变成了一个风景区,那些小路与菜地已经楼阁挺立,迎接了来自外地的游客,越来越多的人进来跑步,我刻在那颗核桃树上的字现在应该已经结痂了。


春是那么让人着谜,不单单是因为气候,更多的是因为它能带给我许多东西,让我从中反思,收获!


作者:苏格拉的底牌
来源:juejin.cn/post/7341282461203365922
收起阅读 »

中年程序员写给37岁的自己

笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪,胡乱写一通,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。 你好,37岁👇 37岁的自己你好,接下来的几个“重要”是36岁的自己在过去一年中所得到的感悟,希望能够帮助...
继续阅读 »


笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪,胡乱写一通,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。


你好,37岁👇


37岁的自己你好,接下来的几个“重要”是36岁的自己在过去一年中所得到的感悟,希望能够帮助37岁的自己更好的前行。


人生就是不停的打怪升级的过程,从2009年到今天,已经是第15个年头了,如果说程序员的职业生涯有20年的话,从现在算起至少还有5个年头可以继续拼搏。


春节快乐


在这里先祝各位春节快乐!


还有2天就到春节了,和前两年一样,还是一直在公司坚守到最后一天,也是利用最后比较清闲的时间,总结一下自己过去一年,把感想写下来,希望能够帮助自己更好的看清内心。


说真的每一年总结的时候,都会有不一样的收获。


运气很重要



再回头看自己15年成长的过程,运气和努力其实是各占一半,5分是不断努力的提升自己生存的技能,5分是在每个阶段也是很幸运遇到了能够帮助自己的贵人。但是这几年也很有感触,随着年龄的不断增长,运气很有可能是往后几年职业生涯是否能顺利的决定性因素。那什么是运气?比如你是否进入了一个没有被社会抛弃的行业,你是否正处于公司的重要赛道,你是否能和你的领导惺惺相惜,你正在做的事情是否能够发挥你的自身价值等等,以上这些当然和前期的努力是有关系的,通过你的努力让你有可能接受这一份运气的可能性,但是最终做决定的并不是自己。加入正好某个政策出现拯救了行业、正好公司的战略调整让你们部门还能招人、正好你领导看你顺眼并委以重任...那么请珍惜眼前,走好眼前每一步,努力付出,不辜负这一份运气!


写这篇文章的时候,正好在这家厂干满3年,这三年真是各种心酸,经历了整个公司业务的缩编裁撤,跌宕起伏,经历了团队连换3个领导,在最艰难,最焦虑的时候,选择了放下焦虑,在团队动荡的时候,更需要努力找机会向隔级领导证明自己,努力寻找过程中的那点不起眼的运气成分,同时做好准备,最坏的情况无非就是拿大礼包。但是如果就此躺平,那运气只会飘向其他人。终于在第三个领导的时候,运气来了,惺惺相惜的感觉出现了,逐渐被委以重任,这个时候就只管抱紧大腿往前冲就行了。


谨言慎行很重要


懂得在什么场合说什么样的话,以及能够听得懂别人的话外话,分得清对什么样的人应该说什么样的话,做到不给自己惹麻烦,不给领导惹麻烦。


程序员都是比较单纯的,和机器打交道最多,有一说一,不会拐弯抹角,这也使得我们在沟通或者有利用冲突的时候,经常是处于弱势的那一方,一不小心就会被别人当枪使,但我遇到的大部分同事都很nice的,也会有一些个卧龙凤雏,目前公司里确实遇到了一两个,有时候因为想用真心换真心,换来的却是冷枪暗箭,但俗话说吃一见长一智,以后和他们说话多留个心眼,不能被同一个坑埋两次吧。


这里举个例子,有一次我们有个研发主动去找产品同学,希望他们能够在下个需求时能从用户体验优化一些体验差的产品逻辑,我们可以加人优化,这个时候如果是靠谱的产品肯定是会一起配合梳理,但是我们的这个产品的领导真是个卧龙,这件事情传到他耳朵里,就变成了我们有资源也不给他们排期某某需求,反手就向上面老板投诉我们研发工作不饱和,真是能给人气死。这里并不是说我们同学没有情商,而是因为程序员所处的环境造就了我们,直面目标和结果的推进方式,容易全盘托出。我们喜欢沉浸在技术的世界里,这里所见即所得,绝对的平等,没有小人得志,没有尔虞我诈。


所以这里需要做到对人下菜,踩过的坑要记得,该设防的时候得设防,不能啥都说,拿不定主意时,少说多做。


身体健康很重要


作为家里的顶梁柱,可不能倒下,拼归拼,但身体健康还是要放在首位。35岁以后注意养生吧,现在发量真的比之前少了很多,头皮明显暴露在阳光下了,都是上有老下有小,没天的睡眠质量真的是大不如前。这个时候保持心情愉悦很重要,如果一只处于焦虑烦躁的状态,很容易导致身心疲惫,各种毛病就会接踵而至。由于睡眠不足,导致眼睛血丝严重,眼角膜出现问题,去年有大半年的时候都是往医院跑,在医生的叮嘱下,及时调整作息时间,工作生活保持适当躺平,保持心情愉悦,总算是没有往恶化的方面发展,以后只需要注意养生即可。


还是要保证适当的锻炼,但是不能过量,比如跳绳、跑步,对于我来说过量会膝盖疼,所以也不是很适合,每天适当的散散步是个不错的选择。


家庭和睦很重要


我们每天努力工作,就是为了多挣钱,为了给家人一个幸福美满的生活。在工作中难免会遇到很多委屈,心中自然会有很多不满,这时就需要自己找到发泄不满的方式,如果说家里人可以倾听,那可以和家里倾诉,如果不希望让家里知道,那可以通过刷剧、钓鱼、打球等方式,非常管用。


在过去这两三年,我媳妇是我很大的精神支柱,在21、22年团队业务动荡的时候,作为家里的顶梁柱,那时候压力山大,但她成为了我的坚强后盾,她鼓励我坚持学习充实自己,事情并没有到最坏的时候,只要时刻做好准备,其他的看运气,大不了就是失业,或许年纪大了可能会经历很长一段时间的空窗期,但慢慢找总能再找到工作的。她真正做到了陪伴、倾听、鼓励,没有不满、没有抱怨,这就是夫妻同心,其利断金。没有了后顾之忧,少了很多自身的精神内耗,学习效率、工作效率都会有很大的提升,随着公司业务的回暖,目前至少也算是在不断的稳步前进中。


现在媳妇有了、娃有了、房子有了、车子有了,这不就是人生最好的运气么,接下来我的目标其实就是努力经营好这个家,为他们遮风挡雨,家庭健康和睦比啥都重要。


适当旅游很重要


我们每天生活在高度紧张、快节奏的环境里,很容易把自己搞得紧张兮兮的,这个时候需要定期找时间出去放松一下,带上家人来一趟说走就走的旅行。慢节奏的旅行,不仅不累,还能促进家庭和睦,千万不要吝啬,该花钱的地方就得大胆的花。


今年一直想着找时间去一趟海南,所以等老大考试完第二天,立马就出发了。不过也许是许久没坐飞机了,竟然还忘记了45分钟不让值机的事情,也没有提前值机,一直排队托运,结果过时了没有完成托运和值机搞得还改签加钱体验了一把头等舱,但说实话北京坐飞机一路躺着去海南也是挺舒服的,空姐真的是服务周到。



1月初去海南温度刚刚好,完美的体验了一把反季节的旅行。


夕阳下的美好,这感觉又回到了十几年前弹恋爱的感觉:)。




作者:冲_破
来源:juejin.cn/post/7332381790341136384
收起阅读 »

外行转码农,焦虑到躺平

介绍自己 本人女,16年本科毕业,学的机械自动化专业,和大部分人一样,选专业的时候是拍大腿决定的。 恍恍惚惚度过大学四年,考研时心比天高选了本专业top5学校,考研失败,又不愿调剂,然后就参加校招大军。可能外貌+绩点优势,很顺利拿到了很多工厂offer,然后欢...
继续阅读 »

介绍自己


本人女,16年本科毕业,学的机械自动化专业,和大部分人一样,选专业的时候是拍大腿决定的。


恍恍惚惚度过大学四年,考研时心比天高选了本专业top5学校,考研失败,又不愿调剂,然后就参加校招大军。可能外貌+绩点优势,很顺利拿到了很多工厂offer,然后欢欢喜喜拖箱带桶进厂。


每天两点一线生活,住宿吃饭娱乐全在厂区,工资很低但是也没啥消费,住宿吃饭免费、四套厂服覆盖春夏秋冬。


我的岗位是 inplan软件维护 岗位,属于生产资料处理部门,在我来之前6年该岗位一直只有我师傅一个人,岗位主要是二次开发一款外购的软件,软件提供的api是基于perl语言,现在很少有人听过这个perl吧。该岗位可能是无数人眼里的神仙岗位吧,我在这呆了快两年,硬是没写过一段代码...


inplan软件维护 岗位的诞生就是我的师傅开创的,他原本只是负责生产资料处理,当大家只顾着用软件时,他翻到了说明书上的API一栏,然后写了一段代码,将大家每日手工一顿操作的事情用一个脚本解决了,此后更是停不下来,将部门各种excel数据处理也写成了脚本,引起了部门经理的注意,然后就设定了该岗位。


然而,将我一个对部门工作都不了解的新人丢在这个岗位,可想我的迷茫。开始半年师傅给我一本厚厚的《perl入门到精通》英文书籍,让我先学会 perl 语言。(ps:当时公司网络不连外网,而我也没有上网查资料的习惯,甚至那时候对电脑操作都不熟练...泪目)


师傅还是心地很善良很单纯的人,他隔一段时间会检查我的学习进度,然而当他激情澎拜给我讲着代码时,我竟控制不住打起了瞌睡,然后他就不管我了~~此后我便成了部门透明人物,要是一直透明下去就好了。我懒散的工作态度引起了部门主管的关注,于是我成了他重点关注的对象,我的工位更是移到了他身后~~这便是我的噩梦,一不小心神游时,主管的脸不知啥时凑到了我的电脑屏幕上~~~😱


偶然发现我的师傅在学习 php+html+css+js,他打算给部门构建一个网站,传统的脚本语言还是太简陋了。我在网上翻到了 w3scool离线文档 ,这一下子打开了我的 代码人生。后面我的师傅跳槽了,我在厂里呆了两年觉得什么都没学到,也考虑跳槽了。


后面的经历也很魔幻,误打误撞成为了一名前端开发工程师。此时是2018年,算是前端的鼎盛之年吧,各种新框架 vue/react/angular 都火起来了,各种网站/手机端应用如雨后春笋。我的前端之路还算顺利吧,下面讲讲我的经验吧


如何入门


对于外行转码农还是有一定成本的,省心的方式就是报班吧,但是个人觉得不省钱呀。培训班快则3个月,多的几年,不仅要交上万的培训费用,这段时间0收入,对于家境一般的同学,个人不建议报班。


但是现在市场环境不好,企业对你的容忍度不像之前那么高。之前几年行业缺人,身边很多只懂皮毛的人都可以进入,很多人在岗位半年也只能写出简单的页面,逻辑复杂一点就搞不定~~即使被裁了,也可以快速找到下家。这样的日子应该一去不复返了,所以我们还是要具备的实力,企业不是做慈善的,我们入职后还是要对的起自己的一份工资。


讲讲具体怎么入门吧


看视频:


b站上有很多很多免费的视频,空闲之余少刷点段子,去看看这些视频。不要问我看哪个,点击量大的就进去看看,看看过来人的经验,看看对这个行业的介绍。提高你的信息量,普通人的差距最大就在信息量的多少


还是看视频:


找一个系统的课程,系统的学习 html+css+js+vue/react,我们要动手写一些demo出来


做笔记:


对于新人来说,就是看了视频感觉自己会了,但是写起来很是费力。为啥呢?因为你不知道也记不住有哪些api,所以我们在看视频学习中,有不知道的语法就记下来。

我之前的经验就是手动抄写,最初几年抄了8个笔记本,但是后面觉得不是很方便,因为笔记没有归纳,后续整理笔记困难,所以我们完全可以用电子档的形式,这方便后面的归纳修改。我更多是将它当成一个手册吧,我自己也经常遗忘一些API,所以时不时会去翻翻。


回顾:


我们的笔记做了就要经常的翻阅,温故而知新,经常翻阅我们的笔记,经常去总结,突然有一天你的思维就上升了一个高度。



  • 慢慢你发现写代码就是不停调用api的过程

  • 慢慢你会发现程序里的美感,一个设计模式、一种新思维。我身边很多人都曾经深深沉迷过写代码,那种成就感带来的心流,这是物质享受带来不了的


输出:


就是写文章啦,写文章让我们总结回顾知识点,发现知识的盲区,在这个过程中进行了深度思考。更重要的是,对于不严谨的同学来说,研究一个知识点很容易浅尝则止,写文章驱动自己去更深层系统挖掘。不管对于刚入行的还是资深人士,我觉得输出都是很重要的。


持续提升


先谈谈学历歧视吧,现在很多大厂招聘基本条件就是211、985,对此很是无奈,但是我内心还是认可这种要求的,我对身边的本科985是由衷的佩服的。我觉得他们高考能考上985,身上都是有过人之处的,学习能力差不了。


见过很多工作多年的程序员,但是他们的编码能力无法描述,不管是逻辑能力、代码习惯、责任感都是很差的,写代码完全是应付式的,他们开发的代码如同屎山。额,但是我们也不要一味贬低他人,后面我也学会了尊重每一个人,每个人擅长的东西不一样,他可能不擅长写代码,但是可能他乐观的心态是很多人不及的、可能他十分擅长交际...


但是可能的话,我们还是要不断提高代码素养



  • 广度:我们实践中,很多场景没遇到,但是我们要提前去了解,不要等需要用、出了问题才去研究。我们要具备一定的知识面覆盖,机会是给有准备的人的。

  • 深度:对于现在面试动不动问源码的情况,很多人是深恶痛绝的,曾经我也是,但是当我沉下心去研究的时候,才发现这是有道理的。阅读源码不仅挺高知识的广度,更多让我们了解代码的美感


具体咋做呢,我觉得几下几点吧。(ps:我自己也做的不好,道理都懂,很难做到优秀呀~~~)



  • 扩展广度:抽空多看看别人的文章,留意行业前沿技术。对于我们前端同学,我觉得对整个web开发的架构都要了解,后端同学的mvc/高并发/数据库调优啥的,运维同学的服务器/容器/流水线啥的都要有一定的了解,这样可以方便的与他们协作

  • 提升深度:首先半路出家的同学,前几年不要松懈,计算机相关知识《操作系统》《计算机网络》《计算机组成原理》《数据结构》《编译原理》还是要恶补一下,这是最基础的。然后我们列出自己想要深入研究的知识点,比如vue/react源码、编译器、低代码、前端调试啥啥的,然后就沉下心去研究吧。


职业规划


现在整个大环境不好了,程序员行业亦是如此,身边很多人曾经的模式就是不停的卷,卷去大厂,跳一跳年薪涨50%不是梦,然而现在不同了。寒风凌凌,大家只想保住自己的饭碗(ps:不同层次情况不同呀,很多大厂的同学身边的同事还是整天打了鸡血一般)


曾经我满心只有工作,不停的卷,背面经刷算法。22年下半年市场明显冷下来,大厂面试机会都没有了,年过30,对大厂的执念慢慢放下。


我慢慢承认并接受了自己的平庸,然后慢慢意识到,工作只是生活的一部分。不一定要担任ceo,才算走上人生巅峰。最近几年,我爱上了读书,以前只觉得学理工科还是实用的,后面慢慢发现每个行业有它的美感~


最后引用最近的读书笔记结尾吧,大家好好体会一下论语的“知天命”一词,想通了就不容易焦虑了~~~



自由就是 坦然面对生活,看清了世界的真相依然热爱生活。宠辱不惊,闲看庭前花开花落。去留无意,漫随天外云卷云舒。



image.png




作者:chengliu0508
来源:juejin.cn/post/7343138429860347945
收起阅读 »

缓存把我坑惨了..

故事 春天,办公室外的世界总是让人神往的,小猫带着耳机,托着腮帮,望着外面美好的春光神游着... 一声不和谐的座机电话声打破这份本该属于小猫的宁静,“hi,小猫,线上有个客户想购买A产品规格的商品,投诉说下单总是失败,帮忙看一下啥原因。”客服部小姐姐甜美的声音...
继续阅读 »

故事


春天,办公室外的世界总是让人神往的,小猫带着耳机,托着腮帮,望着外面美好的春光神游着...


一声不和谐的座机电话声打破这份本该属于小猫的宁静,“hi,小猫,线上有个客户想购买A产品规格的商品,投诉说下单总是失败,帮忙看一下啥原因。”客服部小姐姐甜美的声音从电话那头传来。“哦哦,好,我看一下,把商品编号发一下吧......”


由于前一段时间的系统熟悉,小猫对现在的数据表模型已经了然于胸,当下就直接定位到了商品规格信息表,发现数据库中客户想购买的规格已经被下架了,但是前端的缓存好像并没有被刷新。


小猫在系统中找到了之前开发人员留的后门接口,直接curl语句重新刷新了一下接口,缓存问题搞定了。


关于商品缓存和数据库不一致的情况,其实小猫一周会遇到好几个这样的客诉,他深受DB以及缓存不一致的苦,于是他下定决心想要从根本上解决问题,而不是curl调用后门接口......


写在前面


小猫的态度其实还是相当值得肯定的,当他下定决心从根本上排查问题的时候开始,小猫其实就是一名合格而且负责的研发,这也是我们每一位软件研发人员所需要具备的处理事情的态度。


在软件系统演进的过程中,只有我们在修复历史遗留的问题的时候,才是真正意义上地对系统进行了维护,如果我们使用一些极端的手段(例如上述提到的后门接口curl语句)来保持古老而陈腐的代码继续工作的时候,这其实是一种苟且。一旦系统有了问题,我们其实就需要及时进行优化修复,否则会形成不好的示范,更多的后来者倾向于类似的方式解决问题,这也是为什么FixController存在的原因,这其实就是系统腐化的标志。


言归正传,关于缓存和DB不一致相信大家在日常开发的过程中都有遇到过,那么我们接下来就和大家好好盘一盘,缓存和DB不一致的时候,咱们是如何去解决的。接下来,大家会看到解决方案以及实战。


缓存概要


常规接口缓存读取更新


常规缓存读取


看到上面的图,我们可以清晰地知道缓存在实际场景中的工作原理。



  1. 发生请求的时候,优先读取缓存,如果命中缓存则返回结果集。

  2. 如果缓存没有命中,则回归数据库查询。

  3. 将数据库查询得到的结果集再次同步到缓存中,并且返回对应的结果集。


这是大家比较熟悉的缓存使用方式,可以有效减轻数据库压力,提升接口访问性能。但是在这样的一个架构中,会有一个问题,就是一份数据同时保存在数据库和缓存中,如果数据发生变化,需要同时更新缓存和数据库,由于更新是有先后顺序的,并且它不像数据库中多表事务操作满足ACID特性,所以这样就会出现数据一致性的问题。


DB和缓存不一致方案与实战DEMO


关于缓存和DB不一致,其实无非就是以下四种解决方案:



  1. 先更新缓存,再更新数据库

  2. 先更新数据库,再更新缓存

  3. 先删除缓存,后更新数据库

  4. 先更新数据库,后删除缓存


先更新缓存,再更新数据库(不建议)


cache02.png


这种方案其实是不提倡的,这种方案存在的问题是缓存更新成功,但是更新数据库出现异常了。这样会导致缓存数据与数据库数据完全不一致,而且很难察觉,因为缓存中的数据一直都存在。


先更新数据库,再更新缓存


先更新数据库,再更新缓存,如果缓存更新失败了,其实也会导致数据库和缓存中的数据不一致,这样客户端请求过来的可能一直就是错误的数据。


cache03.png


先删除缓存,后更新数据库


这种场景在并发量比较小的时候可能问题不大,理想情况是应用访问缓存的时候,发现缓存中的数据是空的,就会从数据库中加载并且保存到缓存中,这样数据是一致的,但是在高并发的极端情况下,由于删除缓存和更新数据库非原子行为,所以这期间就会有其他的线程对其访问。于是,如下图。


cache04.png


解释一下上图,老猫罗列了两个线程,分别是线程1和线程2。



  1. 线程1会先删除缓存中的数据,但是尚未去更新数据库。

  2. 此时线程2看到缓存中的数据是空的,就会去数据库中查询该值,并且重新更新到缓存中。

  3. 但是此时线程1并没有更新成功,或者是事务还未提交(MySQL的事务隔离级别,会导致未提交的事务数据不会被另一个线程看到),由于线程2快于线程1,所以线程2去数据库查询得到旧值。

  4. 这种情况下最终发现缓存中还是为旧值,但是数据库中却是最新的。


由此可见,这种方案其实也并不是完美的,在高并发的情况下还是会有问题。那么下面的这种总归是完美的了吧,有小伙伴肯定会这么认为,让我们一起来分析一下。


先更新数据库,后删除缓存


先说结论,其实这种方案也并不是完美的。咱们通过下图来说一个比较极端的场景。


cache05.png


上图中,我们执行的时间顺序是按照数字由小到大进行。在高并发场景下,我们说一下比较极端的场景。


上面有线程1和线程2两个线程。其中线程1是读线程,当然它也会负责将读取的结果集同步到缓存中,线程2是写线程,主要负责更新和重新同步缓存。



  1. 由于缓存失效,所以线程1开始直接查询的就是DB。

  2. 此时写线程2开始了,由于它的速度较快,所以直接完成了DB的更新和缓存的删除更新。

  3. 当线程2完成之后,线程1又重新更新了缓存,那此时缓存中被更新之后的当然是旧值了。


如此,咱们又发现了问题,又出现了数据库和缓存不一致的情况。


那么显然上面的这四种方案其实都多多少少会存在问题,那么究竟如何去保持数据库和缓存的一致性呢?


保证强一致性


如果有人问,那我们能否保证缓存和DB的强一致性呢?回答当然是肯定的,那就是针对更新数据库和刷新缓存这两个动作加上锁。当DB和缓存数据完成同步之后再去释放,一旦其中任何一个组件更新失败,我们直接逆向回滚操作。我们可能还得做快照便于其历史缓存重写。那这种设计显然代价会很大。


其实在很大一部分情况下,要求缓存和DB数据强一致大部分都是伪需求。我们可能只要达到最终尽量保持缓存一致即可。有缓存要求的大部分业务其实也是能接受数据在短期内不一致的情况。所以我们就可以使用下面的这两种最终一致性的方案。


错误重试达到最终一致


如下示意图所示:


cache06.png


上面的图中我们看到。当然上述老猫只是画了更新线程,其实读取线程也一样。



  1. 更新线程优先更新数据,然后再去更新缓存。

  2. 此时我们发现缓存更新失败了,咱们就将其重新放到消息队列中。

  3. 单独写一个消费者接收更新失败记录,然后进行重试更新操作。


说到消息队列重试,还有一种方式是基于异步任务重试,咱们可以把更新缓存失败的这个数据保存到数据库,然后通过另外的一个定时任务进而扫描待执行任务,然后去做相关的缓存更新动作。


当然上面我们提到的这两种方案,其实比较依赖我们的业务代码做出相对应的调整。我们当然也可以借助Canal组件来监控MySQL中的binlog的日志。通过数据库的 binlog 来异步淘汰 key,利用工具(canal)将 binlog日志采集发送到 MQ 中,然后通过 ACK 机制确认处理删除缓存。先更新DB,然后再去更新缓存,这种方式,被称为 Cache Aside Pattern,属于缓存更新的经典设计模式之一。


cache07.png


上述我们总结了缓存使用的一些方案,我们发现其实没有一种方案是完美的,最完美的方案其实还是得去结合具体的业务场景去使用。方案已经同步了,那么如何去撸数据库以及缓存同步的代码呢?接下来,和大家分享的当然是日常开发中比较好用的SpringCache缓存处理框架了。


SpringCache实战


SpringCache是一个框架,实现了基于注解缓存功能,只需要简单地加一个注解,就能实现缓存功能。
SpringCache提高了一层抽象,底层可以切换不同的cache实现,具体就是通过cacheManager接口来统一不同的缓存技术,cacheManager是spring提供的各种缓存技术抽象接口。


目前存在以下几种:



  • EhCacheCacheManager:将缓存的数据存储在内存中,以提高应用程序的性能。

  • GuavaCaceManager:使用Google的GuavaCache作为缓存技术。

  • RedisCacheManager:使用Redis作为缓存技术。


配置


我们日常开发中用到比较多的其实是redis作为缓存,所以咱们就可以用RedisCacheManager,做一下代码演示。咱们以springboot项目为例。


老猫这里拿看一下redisCacheManager来举例,项目开始的时候我们当忽然要在pom文件依赖的时候就肯定需要redis启用项。如下:


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--使用注解完成缓存技术-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

因为我们在application.yml中就需要配置redis相关的配置项:


spring:
redis:
host: localhost
port: 6379
database: 0
jedis:
pool:
max-active: 8 # 最大链接数据
max-wait: 1ms # 连接池最大阻塞等待时间
max-idle: 4 # 连接线中最大的空闲链接
min-idle: 0 # 连接池中最小空闲链接
cache:
redis:
time-to-live: 1800000

常用注解


关于SpringCache常用的注解,整理如下:


cache08.png


针对上述的注解,咱们做一下demo用法,如下:


用法简单盘点


@Slf4j
@SpringBootApplication
@ServletComponentScan
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class);
}
}

在service层我们注入所需要用到的cacheManager:


@Autowired
private CacheManager cacheManager;

/**
* 公众号:程序员老猫
* 我们可以通过代码的方式主动清除缓存,例如
**/

public void clearCache(String productCode) {
try {
RedisCacheManager redisCacheManager = (RedisCacheManager) cacheManager;

Cache backProductCache = redisCacheManager.getCache("backProduct");
if(backProductCache != null) {
backProductCache.evict(productCode);
}
} catch (Exception e) {
logger.error("redis 缓存清除失败", e);
}
}

接下来我们看一下每一个注解的用法,以下关于缓存用法的注解,我们都可以将其加到dao层:


第一种@Cacheable


在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中。


@Cacheable 注解中的核心参数有以下几个:



  • value:缓存的名称,可以是一个字符串数组,表示该方法的结果可以被缓存到哪些缓存中。默认值为一个空数组,表示缓存到默认的缓存中。

  • key:缓存的 key,可以是一个 SpEL 表达式,表示缓存的 key 可以根据方法参数动态生成。默认值为一个空字符串,表示使用默认的 key 生成策略。

  • condition:缓存的条件,可以是一个 SpEL 表达式,表示缓存的结果是否应该被缓存。默认值为一个空字符串,表示不考虑任何条件,缓存所有结果。

  • unless:缓存的排除条件,可以是一个 SpEL 表达式,表示缓存的结果是否应该被排除在缓存之外。默认值为一个空字符串,表示不排除任何结果。


上述提及的SpEL是是Spring Framework中的一种表达式语言,此处不展开,不了解的小伙伴可以自己去查阅一下相关资料。


代码使用案例:


@Cacheable(value="picUrlPrefixDO",key="#id")
public PicUrlPrefixDO selectById(Long id) {
PicUrlPrefixDO picUrlPrefixDO = writeSqlSessionTemplate.selectOne("PicUrlPrefixDao.selectById", id);
return picUrlPrefixDO;
}

第二种@CachePut


表示将方法返回的值放入缓存中。
注解的参数列表和@Cacheable的参数列表一致,代表的意思也一样。
代码使用案例:


@CachePut(value = "userCache",key = "#users.id")
@GetMapping()
public User get(User user){
User users= dishService.getById(user);
return users;
}

第三种@CacheEvict


表示从缓存中删除数据。使用案例如下:


@CacheEvict(value="picUrlPrefixDO",key="#urfPrefix")
public Integer deleteByUrlPrefix(String urfPrefix) {
return writeSqlSessionTemplate.delete("PicUrlPrefixDao.deleteByUrlPrefix", urfPrefix);
}

上述和大家分享了一下SpringCache的用法,对于上述提及的三个缓存注解中,老猫在日常开发过程中用的比较多的是@CacheEvict以及@Cacheable,如果对SpringCache实现原理感兴趣的小伙伴可以查阅一下相关的源码。


使用缓存的其他注意点


当我们使用缓存的时候,除了会遇到数据库和缓存不一致的情况之外,其实还有其他问题。严重的情况下可能还会出现缓存雪崩。关于缓存失效造成雪崩,大家可以看一下这里【糟糕!缓存击穿,商详页进不去了】。


另外如果加了缓存之后,应用程序启动或服务高峰期之前,大家一定要做好缓存预热从而避免上线后瞬时大流量造成系统不可用。关于缓存预热的解决方案,由于篇幅过长老猫在此不展开了。不过方案概要可以提供,具体如下:



  • 定时预热。采用定时任务将需要使用的数据预热到缓存中,以保证数据的热度。

  • 启动时加载预热。在应用程序启动时,将常用的数据提前加载到缓存中,例如实现InitializingBean 接口,并在 afterPropertiesSet 方法中执行缓存预热的逻辑。

  • 手动触发加载:在系统达到高峰期之前,手动触发加载常用数据到缓存中,以提高缓存命中率和系统性能。

  • 热点预热。将系统中的热点数据提前加载到缓存中,以减轻系统压力。5

  • 延迟异步预热。将需要预热的数据放入一个队列中,由后台异步任务来完成预热。

  • 增量预热。按需预热数据,而不是一次性预热所有数据。通过根据数据的访问模式和优先级逐步预热数据,以减少预热过程对系统的冲击。


如果小伙伴们还有其他的预热方式也欢迎大家留言。


总结


上述总结了关于缓存在日常使用的时候的一些方案以及坑点,当然这些也是面试官最喜欢提问的一些点。文中关于缓存的介绍老猫其实并没有说完,很多其实还是需要小伙伴们自己去抽时间研究研究。不得不说缓存是一门以空间换时间的艺术。要想使用好缓存,死记硬背策略肯定是行不通的。真实的业务场景往往要复杂的多,当然解决方案也不同,老猫上面提及的这些大家可以做一个参考,遇到实际问题还是需要大家具体问题具体分析。


作者:程序员老猫
来源:juejin.cn/post/7345729950458282021
收起阅读 »

完美解决html2canvas + jsPDF导出pdf分页echarts内容截断问题

web
想直接看解决方案的可跳过我的絮絮叨叨 有段时间没有更新内容了,一方面是自己在沉淀,二是前段时间学着剪vlog想着把自己上下班及中午锻炼的碎片整理出来发到网上,咱也做一个自媒体博主,实现时间自由,财富自由,走上巅峰,登上福布斯,弹劾小日子,哈哈哈,当然有想法是好...
继续阅读 »

想直接看解决方案的可跳过我的絮絮叨叨


有段时间没有更新内容了,一方面是自己在沉淀,二是前段时间学着剪vlog想着把自己上下班及中午锻炼的碎片整理出来发到网上,咱也做一个自媒体博主,实现时间自由,财富自由,走上巅峰,登上福布斯,弹劾小日子,哈哈哈,当然有想法是好的,别管最后咋样先动起来,在行动的这个过程中总会有意想不到的收获,沉淀的过程中我也尝试着写过一些无厘头的文,巴拉巴拉我在说什么,先搞正事,完事在絮叨。


image.png


事件起因


像往常一样,我在霹雳吧啦的敲着26个字母,产品大佬过来说,小帅咱们客户反映线上导出的数据统计有问题,我想不对啊,数据有问题?不可能吧,数据有问题,应该是去找后端吧,找我干啥,是需要我跟进这个问题嘛?原来是历史问题啊~!页面数据涉及到 柱状图、饼状图、折线图和一些数据的展示呈现出现了中间断裂/截断问题,导致导出的pdf格式打印出来不美观,影响用户体验


image.png


简单分析



  1. html2canvas + jsPDF现状导出pdf是一个整体,以a4的高度进行分页,问题的主要原因

  2. 需要对页面元素进行计算,1 + 2大于 a4的高度就另起一页 简单说干就干

  3. 打开百度一搜,why?为啥都没有完美的解决方法,倔友的一些方法也都试了,多少都存在问题不能解决。得,还是得自己搞。


核心代码 代码经过测试 可直接使用


我知道大家进来都想直接找解决问题的方法,因为我带着问题去找答案也一样,先解决了再听他们絮叨。上才艺展示,如果能帮到你请回来看我絮叨。


import html2Canvas from 'html2canvas'
import { jsPDF } from 'jspdf'

// pdfDom 页面dom , spacingHeight 留白间距 fileName 文件名
export function html2Pdf(pdfDom,spacingHeight,fileName){


// 获取元素的高度
function getElementHeight(element) {
return element.offsetHeight;
}

// A4 纸宽高
const A4_WIDTH = 592.28,A4_HEIGHT = 841.89;
// 获取元素去除滚动条的高度
const domScrollHeight = pdfDom.scrollHeight;
const domScrollWidth = pdfDom.scrollWidth;

// 保存当前页的已使用高度
let currentPageHeight = 0;
// 获取所有的元素 我这儿是手动给页面添加class 用于计算高度 你也可以动态添加 这个不重要,主要是看逻辑
let elements = pdfDom.querySelectorAll('.element');
// 代表不可被分页
let newPage = 'new-page'

// 遍历所有内容的高度
for (let element of elements) {
let elementHeight = getElementHeight(element);
console.log(elementHeight, '我是页面上的elementHeight'); // 检查
// 检查添加这个元素后的总高度是否超过 A4 纸的高度
if (currentPageHeight + elementHeight > A4_HEIGHT) {
// 如果超过了,创建一个新的页面,并将这个元素添加到新的页面上
currentPageHeight = elementHeight;
element.classList.add(newPage);
console.log(element, '我是相加高度大于A4纸的元素');
}
currentPageHeight += elementHeight
}
// 根据 A4 的宽高等比计算 dom 页面对应的高度
const pageWidth = pdfDom.offsetWidth;
const pageHeight = (pageWidth / A4_WIDTH) * A4_HEIGHT;
// 将所有不允许被截断的子元素进行处理
const wholeNodes = pdfDom.querySelectorAll(`.${newPage}`);
console.log(wholeNodes, '将所有不允许被截断的子元素进行处理')
// 插入空白块的总高度
let allEmptyNodeHeight = 0;
for (let i = 0; i < wholeNodes.length; i++) {
// 判断当前的不可分页元素是否在两页显示
const topPageNum = Math.ceil(wholeNodes[i].offsetTop / pageHeight);
const bottomPageNum = Math.ceil((wholeNodes[i].offsetTop + wholeNodes[i].offsetHeight) / pageHeight);

// 是否被截断
if (topPageNum !== bottomPageNum) {
// 创建间距
const newBlock = document.createElement('div');
newBlock.className = 'spacing-node';
newBlock.style.background = '#fff';

// 计算空白块的高度,可以适当留出空间,根据自己需求而定
const _H = topPageNum * pageHeight - wholeNodes[i].offsetTop;
newBlock.style.height = _H + spacingHeight + 'px';

// 插入空白块
wholeNodes[i].parentNode.insertBefore(newBlock, wholeNodes[i]);

// 更新插入空白块的总高度
allEmptyNodeHeight = allEmptyNodeHeight + _H + spacingHeight;
}
}
pdfDom.setAttribute(
'style',
`height: ${domScrollHeight + allEmptyNodeHeight}px; width: ${domScrollWidth}px;`,
);

}


以上我们就完成 dom 层面的分页,下面就进入常规操作转为图片进行处理


 return html2Canvas(pdfDom, {
width: pdfDom.offsetWidth,
height: pdfDom.offsetHeight,
useCORS: true,
allowTaint: true,
scale: 3,
}).then(canvas => {

// dom 已经转换为 canvas 对象,可以将插入的空白块删除了
const spacingNodes = pdfDom.querySelectorAll('.spacing-node');

for (let i = 0; i < spacingNodes.length; i++) {
emptyNodes[i].style.height = 0;
emptyNodes[i].parentNode.removeChild(emptyNodes[i]);
}

const canvasWidth = canvas.width,canvasHeight = canvas.height;
// html 页面实际高度
let htmlHeight = canvasHeight;
// 页面偏移量
let position = 0;

// 根据 A4 的宽高等比计算 pdf 页面对应的高度
const pageHeight = (canvasWidth / A4_WIDTH) * A4_HEIGHT;

// html 页面生成的 canvas 在 pdf 中图片的宽高
const imgWidth = A4_WIDTH;
const imgHeight = 592.28 / canvasWidth * canvasHeight
// 将图片转为 base64 格式
const imageData = canvas.toDataURL('image/jpeg', 1.0);

// 生成 pdf 实例

const PDF = new jsPDF('', 'pt', 'a4', true)

// html 页面的实际高度小于生成 pdf 的页面高度时,即内容未超过 pdf 一页显示的范围,无需分页
if (htmlHeight <= pageHeight) {

PDF.addImage(imageData, 'JPEG', 0, 0, imgWidth, imgHeight);

} else {

while (htmlHeight > 0) {
PDF.addImage(imageData, 'JPEG', 0, position, imgWidth, imgHeight);

// 更新高度与偏移量
htmlHeight -= pageHeight;
position -= A4_HEIGHT;

if (htmlHeight > 0) {
// 在 PDF 文档中添加新页面
PDF.addPage();
}
}

}
// 保存 pdf 文件
PDF.save(`${fileName}.pdf`);
}).catch(err => {
console.log(err);
}
);


})



到这儿 htmlToPdf.js这个文件逻辑就处理完毕了,页面引入就可以正常使用了。


import  { html2Pdf }  from '@/utils/htmlToPdf'

// this.$refs 或 id
html2Pdf(this.$refs.viewReportCon)


如果能帮到你那最好不过了,最近天气回暖,换季期间干燥,多方因素易发生感冒,请各位 彦祖 务必保重身体。


作者:攀登的牵牛花
来源:juejin.cn/post/7346808829298262050
收起阅读 »

HTML表单标签详解:如何用HTML标签打造互动网页?

在互联网的世界中,表单是用户与网站进行互动的重要桥梁。无论是注册新账号、提交反馈、还是在线购物,表单都扮演着至关重要的角色。在网页中,我们需要跟用户进行交互,收集用户资料,此时就需要用到表单标签。HTML提供了一系列的表单标签,使得开发者能够轻松地创建出功能丰...
继续阅读 »

在互联网的世界中,表单是用户与网站进行互动的重要桥梁。无论是注册新账号、提交反馈、还是在线购物,表单都扮演着至关重要的角色。在网页中,我们需要跟用户进行交互,收集用户资料,此时就需要用到表单标签。

HTML提供了一系列的表单标签,使得开发者能够轻松地创建出功能丰富的表单。今天我们就来深入探讨这些标签,了解它们的作用以及如何使用它们来构建一个有效的用户界面。

一、表单的组成

在HTML中,一个完整的表单通常由表单域、表单控件(表单元素)和提示信息三个部分构成。

表单域

  • 表单域是一个包含表单元素的区域
  • 在HTML标签中,<form>标签用于定义表单域,以实现用户信息的收集和传递
  • <form>会把它范围内的表单元素信息提交给服务器

表单控件

这些是用户与表单交云的各种元素,如<input>(用于创建不同类型的输入字段)、<textarea>(用于多行文本输入)、<button>(用于提交表单或执行其他操作)、<select><option>(用于创建下拉列表)等。

提示信息

这些信息通常通过<label>标签提供,它为表单控件提供了描述性文本,有助于提高可访问性。<label>标签通常与<input>标签一起使用,并且可以通过for属性与<input>标签的id属性关联起来。

这三个部分共同构成了一个完整的HTML表单,使得用户可以输入数据,并通过点击提交按钮将这些数据发送到Web服务器进行处理。

二、表单元素

在表单域中可以定义各种表单元素,这些表单元素就是允许用户在表单中输入或者选择的内容控件。下面就来介绍HTML中常用的表单元素。

1、<form>标签:基础容器

作用:定义一个表单区域,用户可以在其中输入数据进行提交。

<form action="submit.php" method="post">

其中action属性指定了数据提交到的服务器端脚本地址,method属性定义了数据提交的方式(通常为GET或POST)。

2、<input>标签:数据输入

<input>标签是一个单标签,用于收集用户信息。允许用户输入文本、数字、密码等。

<input type="text" name="username" placeholder="请输入用户名">

type属性决定了输入类型,name属性定义了数据的键名,placeholder属性提供了输入框内的提示文本。

<input>标签的属性

Description

下面举个例子来说明:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form>
               用户名:<input type="text" value="请输入用户名"><br> 

               密码:<input type="password"><br>

      性别:男<input type="radio" name="sex" checked="checked"><input type="radio" name="sex"><br>

               爱好:吃饭<input type="checkbox"> 睡觉<input type="checkbox"> 打豆豆<input type="checkbox"><br>

                <input type="submit" value="免费注册">
                <input type="reset" value="重新填写">
                <input type="button" value="获取短信验证码"><br>
                上传头像:<input type="file">
    </form>
</body>
</html>

Description

3、<label>标签:关联说明

它与输入字段如文本框、单选按钮、复选框等关联起来,以改善网页的可用性和可访问性。<label>标签有两种常见的用法:

1)包裹方式:

在这种用法中,<label>标签直接包裹住关联的表单元素。例如:

<label>用户名:<input type="text" name="username"></label>

这样做的好处是用户点击标签文本时,关联的输入字段会自动获取焦点,从而提供更好的用户体验。

2)使用for属性关联:

在这种用法中,<label>标签通过for属性与目标表单元素建立关联,for属性的值应与目标元素的id属性相匹配。例如:

<label for="username">用户名:</label><input type="text" id="username" name="username">

这样做的优势是单击标签时,相关的表单元素会自动选中(获取焦点),从而提高可用性和可访问性。

4、<select>和<option>标签:下拉选择

在页面中,如果有多个选项让用户选择,并且想要节约页面空间时,我们可以使用标签控件定义下拉列表。

注意点:

  • <select>中至少包含一对<option>
  • 在<option>中定义selected=“selected”时,当前项即为默认选中项
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form>
        籍贯:
        <select>
            <option>山东</option>
            <option>北京</option>
            <option>西安</option>
            <option selected="selected">火星</option>
        </select>
    </form>
</body>
</html>

Description

5、<textarea>标签:多行文本输入

当用户输入内容较多的情况下,我们可以用表单元素标签替代文本框标签。

  • 允许用户输入多行文本。
<textarea name="message" rows="5" cols="30">默认文本</textarea>

rows和cols属性分别定义了文本区域的行数和列数。

Description

6、<button>标签:按钮控件

创建一个可点击的按钮,通常用于提交或重置表单。它允许用户放置文本或其他内联元素(如<i><b><strong><br><img>等),这使得它比普通的具有更丰富的内容和更强的功能。

<button type="submit">提交</button>

type属性为submit时表示这是一个提交按钮。

7、<fieldset>和<legend>标签:分组和标题

通常用于在HTML表单中对相关元素进行分组,并提供一个标题来描述这个组的内容。

<fieldset>标签: 该标签用于在表单中创建一组相关的表单控件。它可以将表单元素逻辑分组,并且通常在视觉上通过围绕这些元素绘制一个边框来区分不同的组。这种分组有助于提高表单的可读性和易用性。

<legend>标签: 它总是与<fieldset>标签一起使用。<legend>标签定义了<fieldset>元素的标题,这个标题通常会出现在浏览器渲染的字段集的边框上方。<legend>标签使得用户更容易理解每个分组的目的和内容。

代码示例:

<form>
  <fieldset>
    <legend>个人信息</legend>
    <label for="name">姓名:</label>
    <input type="text" id="name" name="name"><br><br>
    <label for="email">邮箱:</label>
    <input type="email" id="email" name="email"><br><br>
  </fieldset>
  <fieldset>
    <legend>兴趣爱好</legend>
    <input type="checkbox" id="hobby1" name="hobby1" value="music">
    <label for="hobby1">音乐</label><br>
    <input type="checkbox" id="hobby2" name="hobby2" value="sports">
    <label for="hobby2">运动</label><br>
    <input type="checkbox" id="hobby3" name="hobby3" value="reading">
    <label for="hobby3">阅读</label><br>
  </fieldset>  <input type="submit" value="提交">
</form>

在这个示例中,我们使用了两个<fieldset>元素来组织表单的不同部分。第一个<fieldset>包含姓名和邮箱字段,而第二个<fieldset>包含三个复选框,用于选择用户的兴趣爱好。每个<fieldset>都有一个<legend>元素,用于提供标题。这样,用户在填写表单时可以更清晰地了解每个部分的内容。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

8、<datalist>标签:预定义选项列表

<datalist>标签是HTML5中引入的一个新元素,它允许开发者为输入字段提供预定义的选项列表。当用户在输入字段中输入时,浏览器会显示一个下拉菜单,其中包含与用户输入匹配的预定义选项。

使用<datalist>标签可以提供更好的用户体验,因为它可以帮助用户选择正确的选项,而不必手动输入整个选项。此外,<datalist>还可以与<input>元素的list属性结合使用,以将预定义的选项列表与特定的输入字段关联起来。

下面是一个使用<datalist>标签的代码示例:

<form>
  <label for="color">选择你喜欢的颜色:</label>
  <input type="text" id="color" name="color" list="colorOptions">
  <datalist id="colorOptions">
    <option value="红色">
    <option value="蓝色">
    <option value="绿色">
    <option value="黄色">
    <option value="紫色">
  </datalist>
  <input type="submit" value="提交">
</form>

9、<output>标签:计算结果输出

<output>标签是HTML5中引入的一个新元素,它用于显示计算结果或输出。该标签通常与JavaScript代码结合使用,通过将计算结果赋值给<output>元素的value属性来显示结果。

<output>标签可以用于各种类型的计算和输出,例如数学运算、字符串处理、数组操作等。它可以与<input>元素一起使用,以实时更新计算结果。

下面是一个使用<output>标签的示例:

<form>
  <label for="num1">数字1:</label>
  <input type="number" id="num1" name="num1" oninput="calculate()"><br><br>
  <label for="num2">数字2:</label>
  <input type="number" id="num2" name="num2" oninput="calculate()"><br><br>
  <label for="result">结果:</label>
  <output id="result"></output>
</form>

<script>
function calculate() {
  var num1 = parseInt(document.getElementById("num1").value);
  var num2 = parseInt(document.getElementById("num2").value);
  var result = num1 + num2;  document.getElementById("result").value = result;
}
</script>

10、<progress>标签:任务进度展示

<progress>标签是HTML5中用于表示任务完成进度的一个新元素。它通过value属性和max属性来表示进度,其中value表示当前完成的值,而max定义任务的总量或最大值。

示例:

<!DOCTYPE html>
<html>
<head>
  <title>Progress Example</title>
</head>
<body>
  <h1>File Download</h1>
  <progress id="fileDownload" value="0" max="100"></progress>
  <br>
  <button onclick="startDownload()">Start Download</button>

  <script>
    function startDownload() {
      var progress = document.getElementById("fileDownload");
      for (var i = 0; i <= 100; i++) {
        setTimeout(function() {
          progress.value = i;
        }, i * 10);
      }
    }
  </script>
</body>
</html>

Description

在上面的示例中,我们创建了一个名为"fileDownload"的<progress>元素,并设置了初始值为0,最大值为100。我们还添加了一个按钮,当用户点击该按钮时,会触发名为"startDownload"的JavaScript函数。这个函数模拟了一个文件下载过程,通过循环逐步增加<progress>元素的value属性值,从而显示下载进度。

11、<meter>标签:度量衡指示器

<meter>标签在HTML中用于表示度量衡指示器,它定义了一个已知范围内的标量测量值或分数值,通常用于显示磁盘使用情况、查询结果的相关性等。例如:

<p>CPU 使用率: <meter value="0.6" min="0" max="1"></meter> 60%</p>
<p>内存使用率: <meter value="0.4" min="0" max="1"></meter> 40%</p>

在这个示例中,我们使用了两个<meter>标签来分别显示CPU和内存的使用率。value属性表示当前的测量值,min和max属性分别定义了测量范围的最小值和最大值。通过这些属性,<meter>标签能够清晰地显示出资源的使用情况。

需要注意的是,<meter>标签不应该用来表示进度条,对于进度条的表示,应该使用<progress>标签。

12、<details><summary>标签:详细信息展示

<details><summary>标签是HTML5中新增的两个元素,用于创建可折叠的详细信息区域。

<details>标签定义了一个可以展开或折叠的容器,其中包含一些额外的信息。它通常与<summary>标签一起使用,<summary>标签定义了<details>元素的标题,当用户点击该标题时,<details>元素的内容会展开或折叠。

示例:

<details>
  <summary>点击查看详细信息</summary>
  <p>这里是一些额外的信息,用户可以点击标题来展开或折叠这些信息。</p>
</details>

在这个示例中,我们使用了<details>标签来创建一个可折叠的容器,并在其中添加了一个<summary>标签作为标题。当用户点击这个标题时,容器的内容会展开或折叠。

总结:

HTML表单标签是构建动态网页的基石,它们使得用户能够与网站进行有效的交互。通过合理地使用这些标签,开发者可以创建出既美观又功能强大的表单,从而提升用户体验和网站的可用性。所以说,掌握这些标签的使用,对于前端开发者来说是至关重要的。

收起阅读 »

如何从Button.vue到Button.js

web
Vue的插件系统提供了一种灵活的方式来扩展Vue。Element UI作为一个基于Vue的UI组件库,其使用方式遵循Vue的插件安装模式,允许通过Vue.use()方法全局安装或按需加载组件。本文以Button组件为例,深入探讨Vue.use()方法的工作原理...
继续阅读 »

Vue的插件系统提供了一种灵活的方式来扩展Vue。Element UI作为一个基于Vue的UI组件库,其使用方式遵循Vue的插件安装模式,允许通过Vue.use()方法全局安装或按需加载组件。本文以Button组件为例,深入探讨Vue.use()方法的工作原理,以及如何借助这一机制实现Element UI组件的动态加载。

1. Vue.use()的工作原理

Vue.use(plugin)方法用于安装Vue插件。其基本工作原理如下:

  1. 参数检查Vue.use()首先检查传入的plugin是否为一个对象或函数,因为一个Vue插件可以是一个带有install方法的对象,或直接是一个函数。
  2. 安装插件:如果插件是一个对象,Vue会调用该对象的install方法,传入Vue构造函数作为参数。如果插件直接是一个函数,Vue则直接调用此函数,同样传入Vue构造函数。
  3. 避免重复安装:Vue内部维护了一个已安装插件的列表,如果一个插件已经安装过,Vue.use()会直接返回,避免重复安装。

Vue.use方法是Vue.js框架中用于安装Vue插件的一个全局方法。它提供了一种机制,允许开发者扩展Vue的功能,包括添加全局方法和实例方法、注册全局组件、通过全局混入来添加全局功能等。接下来,我们深入探讨Vue.use的工作原理。

1.1 详细步骤

Vue.use(plugin, ...options)方法接受一个插件对象或函数作为参数,并可选地接受一些额外的参数。Vue.use的基本工作流程如下:

  1. 检查插件是否已安装:Vue内部维护了一个已安装插件的列表。如果传入的插件已经在这个列表中,Vue.use将不会重复安装该插件,直接返回。
  2. 执行插件的安装方法

    • 如果插件是一个对象,Vue将调用该对象的install方法。
    • 如果插件本身是一个函数,Vue将直接调用这个函数。

    在上述两种情况中,Vue构造函数本身和Vue.use接收的任何额外参数都将传递给install方法或插件函数。

1.2 插件的install方法

插件的install方法是实现Vue插件功能的关键。这个方法接受Vue构造函数作为第一个参数,后续参数为Vue.use提供的额外参数。在install方法内部,插件开发者可以执行如下操作:

  • 注册全局组件:使用Vue.component注册全局组件,使其在任何新创建的Vue根实例的模板中可用。
  • 添加全局方法或属性:通过直接在VueVue.prototype上添加方法或属性,为Vue添加全局方法或实例方法。
  • 添加全局混入:使用Vue.mixin添加全局混入,影响每一个之后创建的Vue实例。
  • 添加Vue实例方法:通过在Vue.prototype上添加方法,使所有Vue实例都能使用这些方法。

1.3 示例代码

考虑一个简单的插件,它添加了一个全局方法和一个全局组件:

const MyPlugin = {
install(Vue, options) {
// 添加一个全局方法
Vue.myGlobalMethod = function() {
// 逻辑...
}

// 添加一个全局组件
Vue.component('my-component', {
// 组件选项...
});
}
};

// 使用Vue.use安装插件
Vue.use(MyPlugin);

Vue.use(MyPlugin)被调用时,Vue会执行MyPlugininstall方法,传入Vue构造函数作为参数。MyPlugin利用这个机会向Vue添加一个全局方法和一个全局组件。

1.4 小结

Vue.use方法是Vue插件系统的核心,它为Vue应用提供了极大的灵活性和扩展性。通过Vue.use,开发者可以轻松地将外部库集成到Vue应用中,无论是UI组件库、工具函数集合,还是提供全局功能的插件。理解Vue.use的工作原理对于有效地利用Vue生态系统中的资源以及开发自定义Vue插件都至关重要。

2. Element UI的动态加载

Element UI允许用户通过全局方式安装整个UI库,也支持按需加载单个组件以减少应用的最终打包体积。按需加载的实现,本质上是利用了Vue的插件安装机制。

以按需加载Button组件为例,步骤如下:

  1. 安装babel插件:首先需要安装babel-plugin-component,这个插件可以帮助我们在编译过程中自动将按需加载的组件代码转换为完整的导入语句。
  2. 配置.babelrc或babel.config.js:在.babelrcbabel.config.js配置文件中配置babel-plugin-component,指定需要按需加载的Element UI组件。
{
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
  1. 在Vue项目中按需加载:在Vue文件中,可以直接导入Element UI的Button组件,并使用Vue.use()进行安装。
import Vue from 'vue';
import { Button } from 'element-ui';

Vue.use(Button);

上述代码背后的实现逻辑如下:

  • babel-plugin-component插件处理这段导入语句时,它会将按需加载的Button组件转换为完整的导入语句,并且确保相关的样式文件也被导入。
  • Button组件对象包含一个install方法。这个方法的作用是将Button组件注册到全局,使其在Vue应用中的任何位置都可使用。
  • Vue.use(Button)调用时,Vue内部会执行Buttoninstall方法,将Button组件注册到Vue中。

在Vue中,如果一个组件(如Element UI的Button组件)需要通过Vue.use()方法进行按需加载,这个组件应该提供一个install方法。这个install方法是Vue插件安装的核心,它定义了当使用Vue.use()安装插件时Vue应该如何注册这个组件。接下来,我们来探讨一个具有install方法的Button组件应该是什么样的。

3. 仿Button组件

一个设计得当的Button组件,用于按需加载时,大致应该遵循以下结构:

// Button.vue


<script>
export default {
name: 'ElButton',
// 组件的其他选项...
};
script>

为了使上述Button组件可以通过Vue.use(Button)方式安装,我们需要在组件外层包裹一个对象或函数,该对象或函数包含一个install方法。这个方法负责将Button组件注册为Vue的全局组件:

// index.js 或 Button.js
import Button from './Button.vue';

Button.install = function(Vue) {
Vue.component(Button.name, Button);
};

export default Button;

这里,Button.install方法接收一个Vue构造函数作为参数,并使用Vue.component方法将Button组件注册为全局组件。Button.name用作全局注册的组件名(在这个例子中是ElButton),确保了组件可以在任何Vue实例的模板中通过标签来使用。

使用场景

当开发者在其Vue应用中想要按需加载Button组件时,可以这样实现加载:

import Vue from 'vue';
import Button from 'path-to-button/index.js'; // 或者直接指向包含`install`方法的文件

Vue.use(Button);

通过这种方式,Button组件就被注册为了全局组件,可以在任何组件的模板中直接使用,而无需在每个组件中单独导入和注册。

小结

拥有install方法的Button组件使得它可以作为一个Vue插件来按需加载。这种模式不仅优化了项目的打包体积(通过减少未使用组件的引入),还提供了更高的使用灵活性。开发者可以根据需要,选择性地加载Element UI库中的组件,而无需加载整个UI库。这种按需加载的机制,结合Vue的插件安装系统,极大地增强了Vue应用的性能和可维护性。

结尾

通过上述分析,我们可以看到,Vue.use()方法为Vue插件和组件的安装提供了一种标准化的方式。Element UI通过结合Vue的插件系统和Babel插件,实现了组件的按需加载,既方便了开发者使用,又优化了应用的打包体积。


作者:慕仲卿
来源:juejin.cn/post/7346134710132129830
收起阅读 »

电话背调,我给他打了8分

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。 离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息...
继续阅读 »

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。


离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息,但自始至终,他也没打一声招呼,让给个好评。


离职最后一天,办完手续,没跟任何人打一个招呼,不知什么时候就消失了。


当初他刚入职一周时,其实大家都已经看出他在沟通上有很大问题,还想着如何对他有针对性的安排工作和调整,发挥他的长处,避免他的短处。但没想到这么快就离职了。在他提离职时,虽没过多挽留,但给了一些过来人的建议,很明显也听不进去。


站在旁观者的角度来看,他的职业生涯或即将面临到的事几乎能看得清清楚楚,但他有自己的坚持,别人是没办法的。


就着这事,聊聊最近对职场上关于沟通的一些思考:


第一,忌固执己见


职场中最怕遇到的一种人就是固执己见的人。大多数聪明人,在遇到固执己见的人时,基本上都会在三言两语之后停止与其争辩。因为,人一旦在自己的思维层次形成思维闭环,是很难被说服的。


而对于固执己见的人,失去的是新的思维、新的思想、纠错学习的机会,甚至是贵人的相助。试想一下,本来别人好像给你提建议,指出一条更好的路,结果换来的是争辩,是抬杠,聪明人都会敬而远之,然后默默地在旁边看着你掉坑里。


真正牛的人,基本上都是兼听则明,在获得各类信息、建议之后,综合分析,为己所用。


第二,不必说服,尊重就好


站在另外一个方面,如果一件事与己无关,别人有不同的意见,或者这事本身就是别人负责,那么尊重就好,不必强行说服对方,不必表现自己。


曾看到两个都很有想法的人,为一件事争论好几天,谁也无法说服谁。一方想用权力压另一方,另一方也不care,把简单的事情激化,急赤白脸的。


其实争论的核心只是展现形式不同而已,最终只是在争情绪、争控制感、争存在感而已,大可不必。


对于成年人,想说服谁都非常难的。而工作中的事,本身就没有对错,只有优劣,大多数时候试一下就知道了。


有句话说的非常好,“成年人的世界只做筛选,不做教育”。如果说还能做点什么,那就是潜移默化的影响别人而已。


第三,不懂的领域多听少说


如果自己对一个领域不懂,最好少发表意见,多虚心学习、请教即可。任正非辞退写《万言书》的员工的底层逻辑就是这个,不懂,不了解情况,还草率提建议,只是哗众取宠、浪费别人时间。


如果你不懂一个领域,没有丰富的背景知识和基础理论支撑,在与别人沟通的过程中,强行提建议,不仅露怯,还会惹人烦。即便是懂,也需要先听听别人的看法和视角解读。


站在另一个角度,如果一个不懂的人来挑战你的权威,质疑你的决定,笑一笑就好,不必与其争辩。


郭德纲的一段相声说的好:如果你跟火箭专家说,发射火箭得先抱一捆柴,然后用打火机把柴点着,发射火箭。如果火箭专家看你一眼,就算他输。


第四,没事多夸夸别人


在新公司,学到的最牛的一招就是夸人。之前大略知道夸人的效果,但没有太多的去实践。而在新公司,团队中的几个大佬,身体力行的在夸人。


当你完成一件事时,夸“XXX,真牛逼!”,当你解决一个问题时,夸“还得是XXX,不亏是这块的专家”。总之,每当别人有好的表现时,总是伴随着夸赞和正面响应。于是整个团队的氛围就非常好。


这事本身也不需要花费什么成本,就是随口一句话的事,而效果却非常棒。与懂得“人捧人,互相成就彼此,和气生财”的人相处,是一种非常愉悦的体验。


前两天看到一条视频,一位六七岁的小姑娘指派正在玩游戏的父亲去做饭,父亲答应了。她妈妈问:你是怎么做到的?她说:夸他呀。


看看,这么小的小孩儿都深谙的人性,我们很多成人却不懂,或不愿。曾经以为开玩笑很好,现在发现“夸”才是利器,同时一定不要开贬低性的玩笑。


其实,职场中还有很多基本的沟通规则,比如:分清无效沟通并且及时终止谈话、适当示弱、认真倾听,积极反馈、少用反问等等。


当你留意和思考这些成型的规则时,你会发现它们都是基于社会学和心理学的外在呈现。很有意思,也很有用。


作者:程序新视界
来源:juejin.cn/post/7265978883123298363
收起阅读 »

git 如何撤回已push的代码

在日常的开发中,我们经常使用Git来进行版本控制。有时候,我们可能会不小心将错误的代码 Push 到远程仓库,或者想要在本地回退到之前的某个版本重新开发。 或者像我一样,写了一些感觉以后很有用的优化方案push到线上,又接到了一个新的需求。但是呢,项目比较重要...
继续阅读 »



在日常的开发中,我们经常使用Git来进行版本控制。有时候,我们可能会不小心将错误的代码 Push 到远程仓库,或者想要在本地回退到之前的某个版本重新开发。


或者像我一样,写了一些感觉以后很有用的优化方案push到线上,又接到了一个新的需求。但是呢,项目比较重要,没有经过测试的方案不能轻易上线,为了承接需求只能先把push上去的优化方案先下掉。


现在我的分支是这样的,我想要在本地和远程仓库中都恢复到help文档提交的部分。


image.png

1.基础的手动操作(比较笨,不推荐)



这样的操作非常不推荐,但是如果你不了解git,确实是我们最容易理解的方式。



如果你的错误代码不是很多,那么你其实可以通过与你想要恢复到的commit进行对比,然后手动删除错误代码,然后删除不同的代码。


image.png

按住 ctrl 选择想要对比的两个commit,然后选择 Compare Versions 就能通过对比删除掉你想要删除的代码。



这个方案在代码很简单时时非常有效的,甚至还能通过删除后最新commit和想要退回的commit在Compare一下保障代码一致。


但是这个方法对于代码比较复杂的情况来说就不太好处理了,如果涉及到繁杂的配置文件,那更是让人头疼。只能通过反复的Compare Version来进行对比。


这样的手动操作显然显得有些笨拙了,对此git有一套较为优雅的操作流程,同样能解决这个问题。


2. git Revert Commit(推荐)


image.png

同样的,我第三次提交了错误代码,并且已经push到远程分支。想要撤回这部分代码,只需要右键点击错误提交记录


image.png

git自动产生一个Revert记录,然后我们会看到git自动将我第三次错误提交代码回退了,这个其实就相当于git帮我们手动回退了代码。


image.png

后续,只需要我们将本次改动push到远程,即可完成一次这次回退操作,


image.png

revert相当于自动帮我们进行版本回退操作,并且留下改动记录,非常安全。这也是评论区各位大佬非常推荐的。



但是revert还是存在一点不足,即一次仅能回退一次push。如果我们有几十次甚至上百次的记录,一次次的单击回退不仅费时费力而且还留下了每次的回退记录,我个人觉得revert在这种情况下又不太优雅。


3. 增加新分支(推荐撤回较多情况下使用)


如果真的需要回退到上百次提交之前的版本,我的建议是直接新建个分支。


在想要回到的版本处的提交记录右键,点击new branch


image.png
image.png
image.png

新建分支的操作仅仅增加了一个分支,既能保留原来的版本,又能安全回退到想要回退的版本,同时不会产生太多的回退记录。


但是此操作仍然建议慎用,因为这个操作执行多了,分支管理就又成了一大难题。



4. Reset Current Branch 到你想要恢复的commit记录(不太安全,慎用)


image.png


这个时候会跳出四个选项供你选择,我这里是选择hard


其他选项的含义仅供参考,因为我也没有一一尝试过。




  1. Soft:你之前写的不会改变,你之前暂存过的文件还在暂存。

  2. Mixed:你之前写的不会改变,你之前暂存过的文件不会暂存。

  3. Hard:文件恢复到所选提交状态,任何更改都会丢失。
    你已经提交了,然后你又在本地更改了,如果你选hard,那么提交的内容和你提交后又本地修改未提交的内容都会丢失。

  4. keep:任何本地更改都将丢失,文件将恢复到所选提交的状态,但本地更改将保持不变。
    你已经提交了,然后你又在本地更改了,如果你选keep,那么提交的内容会丢失,你提交后又本地修改未提交的内容不会丢失。



image.png

image.png


image.png


然后,之前错误提交的commit就在本地给干掉了。但是远程仓库中的提交还是原来的样子,你要把目前状态同步到远程仓库。也就是需要把那几个commit删除的操作push过去。


打开push界面,虽然没有commit需要提交,需要点击Force Push,强推过去。
image.png


需要注意的是对于一些被保护的分支,这个操作是不能进行的。需要自行查看配置,我这里因为不是master分支,所以没有保护。


image.png

可以看到,远程仓库中最新的commit只有我们的help文档。在其上的三个提交都没了。


image.png

注意:以上使用的是2023版IDEA,如果有出入的话可以考虑搜索使用git命令。


作者:DaveCui
来源:juejin.cn/post/7307066452290043958
收起阅读 »

faceApi-人脸识别和人脸检测

web
需求:浏览器通过模型检测前方是否有人(距离和正脸),检测到之后拍照随机保存一帧 实现步骤: 获取浏览器的摄像头权限 创建video标签并通过video标签展示摄像头影像 创建canvas标签并通过canvas标签绘制摄像头影像并展示 将canvas的当前帧转...
继续阅读 »

需求:浏览器通过模型检测前方是否有人(距离和正脸),检测到之后拍照随机保存一帧


实现步骤:



  1. 获取浏览器的摄像头权限

  2. 创建video标签并通过video标签展示摄像头影像

  3. 创建canvas标签并通过canvas标签绘制摄像头影像并展示

  4. 将canvas的当前帧转成图片展示保存


pnpm install @vladmandic/face-api 下载依赖


pnpm install @vladmandic/face-api


下载model模型


将下载的model模型放到项目的public文件中 如下图


image.png


创建video和canvas标签


      <video ref="videoRef" style="display: none"></video>
<template v-if="!picture || picture == ''">
<canvas ref="canvasRef" width="400" height="400"></canvas>
</template>
<template v-else>
<img ref="image" :src="picture" alt="" />
</template>
</div>

  width: 400px;
height: 400px;
border-radius: 50%;
overflow: hidden;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}

.video_box {
position: fixed;
width: 400px;
height: 400px;
border-radius: 50%;
overflow: hidden;
}

@keyframes moveToTopLeft {
0% {
right: 50%;
top: 50%;
transform: translate(-50%, -50%);
}

100% {
right: -68px;
top: -68px;
transform: scale(0.5);
}
}

.video_box {
animation: moveToTopLeft 2s ease forwards;
}


介绍分析

video 类选择器 让视频流居中

picture变量 判断是否转成照片

video_box视频流的某一帧转成照片后 动态移动到屏幕右上角



主要逻辑代码 主要逻辑代码 主要逻辑代码!!!


import * as faceApi from '@vladmandic/face-api'

const videoRef = ref()
const options = ref(null)
const canvasRef = ref(null)
let timeout = null
// 初始化人脸识别
const init = async () => {
await faceApi.nets.ssdMobilenetv1.loadFromUri("/models") //人脸检测
// await faceApi.nets.tinyFaceDetector.loadFromUri("/models") //人脸检测 人和摄像头距离打开
await faceApi.nets.faceLandmark68Net.loadFromUri("/models") //特征检测 人和摄像头距离必须打开
// await faceApi.nets.faceRecognitionNet.loadFromUri("/models") //识别人脸
// await faceApi.nets.faceExpressionNet.loadFromUri("/models") //识别表情,开心,沮丧,普通
// await faceApi.loadFaceLandmarkModel("/models");

options.value = new faceApi.SsdMobilenetv1Options({
minConfidence: 0.5, // 0.1 ~ 0.9
});
await cameraOptions()
}

// 打开摄像头
const cameraOptions = async() => {
let constraints = {
video: true
}
// 如果不是通过loacalhost或者通过https访问会将报错捕获并提示
try {
if (navigator.mediaDevices) {
navigator.mediaDevices.getUserMedia(constraints).then((MediaStream) => {
// 返回参数
videoRef.value.srcObject = MediaStream;
videoRef.value.play();
recognizeFace()
}).catch((error) => {
console.log(error);
});
} else {
console.log('浏览器不支持开启摄像头,请更换浏览器')
}

} catch (err) {
console.log('非https访问')
}
}

// 检测人脸
const recognizeFace = async () => {
if (videoRef.value.paused) return clearTimeout(timeout);
canvasRef.value.getContext('2d', { willReadFrequently: true }).drawImage(videoRef.value, 0, 0, 400, 400);
// 直接检测人脸 灵敏较高
// const results = await new faceApi.DetectAllFacesTask(canvasRef.value, options.value).withFaceLandmarks();
// if (results.length > 0) {
// photoShoot()
// }
// 计算人与摄像头距离和是否正脸
const results = await new faceApi.detectSingleFace(canvasRef.value, options.value).withFaceLandmarks()
if (results) {
// 计算距离
const { positions } = results.landmarks;
const leftPoint = positions[0];
const rightPoint = positions[16];
// length 可以代替距离的判断 距离越近 length值越大
const length = Math.sqrt(
Math.pow(leftPoint.x - rightPoint.x, 2) +
Math.pow(leftPoint.y - rightPoint.y, 2),
);
// 计算是否正脸
const { roll, pitch, yaw } = results.angle
//roll水平角度 pitch上下角度 yaw 扭头角度
console.log(roll, pitch, yaw, length)
if (roll >= -10 && roll <= 10 && pitch >= -10 && pitch <= 10 && yaw>= -20 && yaw <= 20 && length >= 90 && length <= 110) {

photoShoot()

}

}


timeout = setTimeout(() => {
return recognizeFace()
}, 0)
}
const picture = ref(null)
const photoShoot = () => {
// 拿到图片的base64
let canvas = canvasRef.value.toDataURL("image/png");
// 停止摄像头成像
videoRef.value.srcObject.getTracks()[0].stop()
videoRef.value.pause()
if(canvas) {
// 拍照将base64转为file流文件
let blob = dataURLtoBlob(canvas);
let file = blobToFile(blob, "imgName");
// 将blob图片转化路径图片
picture.value = window.URL.createObjectURL(file)

} else {
console.log('canvas生成失败')
}
}
/**
* 将图片转为blob格式
* dataurl 拿到的base64的数据
*/
const dataURLtoBlob = (dataurl) => {
let arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while(n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], {
type: mime
});
}
/**
* 生成文件信息
* theBlob 文件
* fileName 文件名字
*/
const blobToFile = (theBlob, fileName) => {
theBlob.lastModifiedDate = new Date().toLocaleDateString();
theBlob.name = fileName;
return theBlob;
}

// 判断是否在区间
const isInRange = (number, start, end) => {
return number >= start && number <= end
}
export { init, videoRef, canvasRef, timeout, picture }

作者:发量浓郁的程序猿
来源:juejin.cn/post/7346121373113647167
收起阅读 »

神奇!一个命令切换测试和线上环境?

大家好,我是喜欢折腾,热爱分享的“一只韩非子”。 关注微信公众号:会编程的韩非子 添加微信号:Hanfz0712 免费加入问答群/知识交流群,一起交流技术难题与未来,让我们Geek起来! 今天跟大家分享一个小Tips,让大家能够更快的切换测试和线上环境。 1...
继续阅读 »

大家好,我是喜欢折腾,热爱分享的“一只韩非子”。

关注微信公众号:会编程的韩非子

添加微信号:Hanfz0712

免费加入问答群/知识交流群,一起交流技术难题与未来,让我们Geek起来!



今天跟大家分享一个小Tips,让大家能够更快的切换测试和线上环境。


1.前因


不知道大家会不会在开发中经常遇到需要切换测试环境和线上环境。比如本地开发完成后需要部署到测试环境查看,然后就需要在我们的主机上配置测试环境的DNS,从而使得相同的域名能够从线上指向到测试环境。


image.png


image.png
我们发现,需要点啊点啊点,真的太头痛了。估计配置玩这个,代码已经忘记写到哪一行了。


下载.jpeg


所以我们有没有更简单的方式来配置DNS呢,经过小韩一顿小脑瓜的思考,想起来那我们能不能够通过命令行代码的方式来解决这个问题呢。哎,你别说还真可以。

一般系统级的配置除了可视化操作外,会有对应的命令行代码(划重点喽)


2.上命令


又是熟悉的一顿Goole后,终于让我找到了,出来吧,My Code!


# 配置DNS
# Mac
networksetup -setdnsservers Wi-Fi x.x.x.x

# Windows
# WiFi
netsh interface ip set dns name="Wi-Fi" static x.x.x.x
# 网线 具体网线连接的名称例:本地连接、以太网...
netsh interface ip set dns name="具体网线连接的名称" static x.x.x.x

哎没错,就是上面这几个。但是!但是!但是!这他喵的也真的太长了!!!我还不如点啊点啊点!好好好把我骗进来杀是吧。


下载 (1).jpeg


别急别急,知道好兄弟记不住,所以我还有一招。那就是别名

友情提示:记忆力非常不错手速又特别快的好兄弟,可以点击左侧页面第一个大拇指和第三个小星星,然后退出群聊了。


3.什么是别名?


那么什么是别名呢?,让我们Google一波,找到你了。


image.png


啊?不对不对,搜索的姿势不对,让我们换个姿势。


image.png
这下姿势就对了,我们得到了我们的答案,原来别名就是用一个简单的命令替代完整的命令,好兄弟们有福了。


4.我要用别名!


别名需要存放在我们的配置文件中,文件的地址是:

Mac:~/.zshrc~/.bashrc。可以通过命令echo $SHELL查看默认使用的是zsh还是bash,来选择对应的配置文件。

Windows:查看文末


在Mac下我们别名的语法为:
alias 别名名称='具体的命令'

名称选择一个自己喜欢的即可,但是注意不要与已经注册的别名重复了,我们可以输入alias命令查看已经注册的别名。

所以我们最终的配置为:


# 别名配置
# 配置测试环境DNS
alias dtest='networksetup -setdnsservers Wi-Fi x.x.x.x'
# 清除测试环境DNS
alias dclear='networksetup -setdnsservers Wi-Fi empty'

然后我们输入dtest就可以进入测试环境,输入dclear就可以回到线上环境了,你也可以继续配置自己的预发环境等等。这简直太妙了。


634da736e9f24u06.gif







最后的最后,留给好兄弟们一个小作业

检索一下Windows下如何配置别名:别名的家在哪里,语法是什么


images.jpeg


作者:一只韩非子
来源:juejin.cn/post/7347161048572968975
收起阅读 »

领导:我有个需求,你把我们项目的技术栈升级一下

web
故事的开始 在一个风和日丽的下午,我正喝着女神请的9.9咖啡,逛着掘金摸着🐟,一切的一切都是这么的美好。 霎那间,只见耳边响起了声音,”系统觉醒中,请。。“,啊?我都外挂到账了? 呸,是”帅哥,领导叫你去开会“。 ”哦“,某位帅哥站了起来,撇了撇帅气的刘海,走...
继续阅读 »

故事的开始


在一个风和日丽的下午,我正喝着女神请的9.9咖啡,逛着掘金摸着🐟,一切的一切都是这么的美好。


霎那间,只见耳边响起了声音,”系统觉醒中,请。。“,啊?我都外挂到账了?


呸,是”帅哥,领导叫你去开会“。


”哦“,某位帅哥站了起来,撇了撇帅气的刘海,走向了办公室。


胖虎00002-我真的很不错.gif


会议室情节


”咦,不是开会吗,怎么就只有领导一个人“。


昏暗的灯光,发着亮光的屏幕,在幽闭的空间里气氛显得有那么一丝的~~~暧昧,不对,是紧张。


”帅哥你来了,那我直接说事情吧“,领导说到。


突然我察觉到那么一丝不安,但是现在走好像来不及了,房门紧闭,领导又有三头六臂,凭着我这副一米八五,吴彦祖的颜值的身躯根本就逃不了。


”是这样的,上面有个新需求,我看完之后发现我们目前的项目技术包袱有点重,做起来比较麻烦,看看你做个项目技术栈升级提高的方案吧“。


听到这里我松了一口气,还好只是升级提高的方案,不是把屁股抬高的方案。


哆啦A梦00009-噢那你要试试看么.png


进入正题


分析了公司项目当前的技术栈,确实存在比较杂乱的情况,一堆的技术包袱,惨不忍睹,但还是能跑的,人和项目都能跑。



技术栈:vue2全家桶 + vuetify + 某位前辈自己开发的组件库 + 某位前辈搞的半成品bff



我用了一分钟的时间做足了思想功课,这次的升级决定采用增量升级的方案,为什么用增量升级,因为线上的项目,需求还在开发,为了稳定和尽量少的投入人力,所以采取增量升级。



这里会有很多小伙伴问,什么是增量升级啊,我会说,自己百度



又经过了一分钟的思想斗争,决定使用微前端的方案进行技术升级,框架选择阿里的qiankun



这里又会有小伙伴说,为什么用qiankun啊,我会说,下面会说



胖虎00004我大雄今天就是要刁难你胖虎.gif


微前端---qiankun


为什么使用微前端,最主要是考虑到他与技术栈无关,能够忽略掉一些历史的包袱。


为什么要用qiankun,最主要还是考虑到稳定问题,qiankun目前的社区比较大,方案也多,出了问题能找到方案,本人之前也有使用过的经验,所以这次就决定是它。



技术为业务服务,在面对技术选型的时候要考虑到现实的问题,不能一味的什么都用新的,稳是第一位



那么应该如何在老项目中使用微前端去升级,我给出了我的方案步骤



  1. 对老项目进行改造(路由,登录,菜单)

  2. 编写子应用的开发模板

  3. 逐个逐个模块进行重构


下面会和大家分析一下三个步骤的内容


66b832f0c3f84bc99896d7f5c4367021_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


老项目的改造(下面大多数为代码)


第一步,我们要将老项目改造成适合被子应用随便进入的公交车。🚗


先对老项目进行分析,老项目是一个后台项目,大多数的后台项目布局都是上中左布局,头部导航栏,左边菜单栏,中间内容。那么我们只需要当用户选择的菜单属于微前端模块时,将中间内容变成微前端容器就好了。


image.png


那我们现在制作一个layout,里面的UI库我懒得换了,讲究看吧🌈


// BasicLayout.vue
<a-layout>
<a-layout-sider collapsible>
//菜单
</a-layout-sider>

<a-layout>
<a-layout-header>
//头部
</a-layout-header>
<a-layout-content>
//内容
<router-view/>
<slot></slot>
</a-layout-content>
</a-layout>

</a-layout>

然后对App.vue进行一定的修改


// App.vue
<a-config-provider locale="zh-cn">
<component v-if="layout" :is="layout">
<!-- 微前端子应用的容器,插槽紧跟着view-router -->
<div id="SubappViewportWrapper"></div>
</component>

// 初始化子应用时传入容器,这个容器不能后续修改,目前方案是将下面的容器动态append到SubappViewportWrapper
<div id="SubappViewport"></div>
</a-config-provider>

import { BasicLayout, UserLayout } from '@/layouts'
import { MICRO_APPS } from './qiankun'
import { start } from 'qiankun';
export default defineComponent({
components: {
BasicLayout,
UserLayout
},
data () {
return {
locale: zhCN,
layout: ''
}
},
methods: {
isMicroAppUrl (url) {
let result = false;
MICRO_APPS.forEach(mUrl => {
if (url.includes(mUrl)) {
result = true;
}
});
return result;
},
checkMicroApp (val) {
if (isMicroAppUrl(val.fullPath)) {
// 展示微前端容器
console.log('是微前端应用....');
document.body.classList.toggle(cName, false);
console.log(document.body.classList);
} else {
// 隐藏微前端容器
console.log('不是微前端应用');
document.body.classList.toggle(cName, true);
}
const oldLayout = this.layout;
this.layout = val.meta.layout || 'BasicLayout';
if (oldLayout !== this.layout) {
const cNode = document.getElementById('SubappViewport');
this.$nextTick(function () {
const pNode = document.getElementById('SubappViewportWrapper');
if (pNode && cNode) {
pNode.appendChild(cNode);
}
});
}
}
},
watch: {
$route (val) {
this.checkMicroApp(val);
}
},
mounted () {
start()
}
})
</script>

<style lang="less">
</style>



修改目的,判断路由中是否为微前端模块,如果是的话,就插入微前端模块容器。


然后新建一个qiankun.js文件


// qiankun.js
import { registerMicroApps, initGlobalState } from 'qiankun';
export const MICRO_APPS = ['test-app']; // 子应用列表
const MICRO_APPS_DOMAIN = '//localhost:8081'; // 子应用入口域名
const MICRO_APP_ROUTE_BASE = '/test-app'; // 子应用匹配规则

const qiankun = {
install (app) {
// 加载子应用提示
const loader = loading => console.log(`加载子应用中:${loading}`);
const registerMicroAppList = [];
MICRO_APPS.forEach(item => {
registerMicroAppList.push({
name: item,
entry: `${MICRO_APPS_DOMAIN}`,
container: '#SubappViewport',
loader,
activeRule: `${MICRO_APP_ROUTE_BASE}`
});
});
// 注册微前端应用
registerMicroApps(registerMicroAppList);

// 定义全局状态
const { onGlobalStateChange, setGlobalState } = initGlobalState({
token: '', // token
});
// 监听全局变化
onGlobalStateChange((value, prev) => {
console.log(['onGlobalStateChange - master'], value);
});
}
};
export default qiankun;

这个文件我们引入了qiankun并对使用它的API进行子应用的注册,之后直接在main.js注册,


// main.js
...
import qiankun from './qiankun'
Vue.use(qiankun)
...

然后我们只需要对路由做一点点的修改就可以用了


新建RouteView.vue页面,用于接受微前端模块的内容


//RouteView.vue
<template>
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>

</template>

修改路由配置


//router.js
const routes = [
{
path: '/home',
name: 'Home',
meta: {},
component: () => import('@/views/Home.vue')
},
// 当是属于微前端模块的路由, 使用RouteView组件
{
path: '/test-app',
name: 'test-app',
meta: {},
component: () => import('@/RouteView.vue')
}
]

最后就新增一个名为test-app的子应用就可以了。


关于子应用的内容,这次先不说了,码字码累了,下次再说吧。


哆啦A梦00007-嘛你慢慢享受吧.png


下集预告(子应用模板编写)


你们肯定会说,不要脸,还搞下集预告。


哼,我只能说,今天周五,准备下班,不码了。🌈


胖虎00014-怎么了-我胖虎说的有毛病吗.gif


作者:小酒星小杜
来源:juejin.cn/post/7307469610423664655
收起阅读 »

H5、小程序中四个反向圆角的图片如何实现

web
H5、小程序中四个反向圆角的图片如何实现 最近我逛热风小程序(一个卖鞋的小程序)时,发现了一个奇特的图片样式。图片的四个圆角是反向的,和常规图片不一样。 思索一番后,我发现想实现这个效果,需要的 CSS 知识还挺多,于是整理这篇文章。 下面我会先介绍如何实现...
继续阅读 »

H5、小程序中四个反向圆角的图片如何实现


最近我逛热风小程序(一个卖鞋的小程序)时,发现了一个奇特的图片样式。图片的四个圆角是反向的,和常规图片不一样。


hotwind.jpg


思索一番后,我发现想实现这个效果,需要的 CSS 知识还挺多,于是整理这篇文章。


下面我会先介绍如何实现四个反向圆角的矩形,再介绍如何把特殊矩形作为遮罩、得到和热风小程序的图片效果。接着,我会给出完整的代码。最后,我会给做一个简单的总结。


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的前端武学。


如何实现四个反向圆角的矩形


不难想到,四个反向圆角的矩形,就是四个边角都被圆遮挡的矩形。


rect-and-circle.png


径向渐变


知道圆遮挡矩形的原理后,我们最容易想到的办法是 —— 先用 50% 的 border-radius 得到圆,再改变圆形的定位去遮挡矩形。


不过这种方法需要多个元素,并不优雅,我将介绍另一种更巧妙的办法。


我们需要先了解一个 CSS 函数 —— radial-gradientradial-gradient 中文名称是径向渐变,它可以指定渐变的中心、形状、颜色和结束位置。语法如下:


/*
* 形状 at 位置
* 渐变颜色 渐变位置
* ...
* 渐变颜色 渐变位置
*/

background-image:
radial-gradient(
circle at center,
transparent 0,
transparent 20px,
#ddd 20px
);

利用径向渐变在矩形中心画圆


光看语法比较抽象,我们用代码写一个实际的例子。如图所示,我们要在矩形中心画圆:


rect0.png


下面是关键代码:


background: radial-gradient(
circle at center,
transparent 0,
transparent 20px,
#ddd 20px
);

其中:



  • radial-gradient 函数用于创建渐变效果。

  • circle at center 指定了圆形渐变,并且渐变的中心在矩形的中心。

  • transparent 0 指定了第一个渐变颜色为透明,位置是从中心开始。

  • transparent 20px 指定了第二个渐变颜色也为透明,位置距离中心 20px。

  • #ddd 20px 指定了第三个渐变颜色为淡灰色,位置距离中心 20px,第三个渐变颜色之后的颜色都是淡灰色。


通过 radial-gradient,我们成功让矩形中心、半径为 20px 的圆变透明,超过半径 20px 的地方颜色都变为灰色,这样看起来就是矩形中心有一个圆。


矩形左上角画圆


不难想到,只要我们把圆的中心从矩形中心移动到矩形的左上角,就可以让圆挡住矩形左上角,得到一个反向圆角。


rect1.png


关键代码如下,我们可以把 circle at center 改写为 circle at left top


background: radial-gradient(
circle at left top,
transparent 0,
transparent 20px,
#ddd 20px
);

矩形四个角画圆


我们已经知道 radial-gradient 如何实现 1 个反向圆角,接下来再看如何实现 4 个反向圆角。继续之前的思路,我们很容易想到给 background 设置多个反向渐变。


多个渐变之间可以用逗号分隔、且它们会按照声明的顺序依次堆叠。于是我们会写出如下关键代码:


background: radial-gradient(
circle at left top,
transparent 0,
transparent 20px,
#ddd 20px
),
/* ... */
radial-gradient(
circle at right bottom,
transparent 0,
transparent 20px,
#ddd 20px
);

遗憾的是,上述代码运行后我们看不到四个反向圆角,而是看到一个矩形。


rect.png


这是因为四个矩形互相堆叠,反而把反向圆角给遮住了。


overlay.png


遮挡怎么解决呢?我们可以分四步来解决。


设置背景宽度和高度


我们先单独看一个径向渐变。


第一步是设置 background-size: 50% 50%;,它设置了背景图像的大小是容器宽度和高度的 50%。代码运行后效果如下:


background-size.png


可以看到左上角有反向圆角的矩形重复出现了四次。


设置不允许重复


第二步是设置 background-repeat: no-repeat;。我们需要去除第一步中出现的重复。代码运行后效果如下:


no-repeat.png


给每个径向渐变设置位置


第三步是设置不允许重复时,应该保留的背景图像位置。


第一步中左上角是反向圆角的矩形出现了四次,第二步不允许重复时默认保留了左上角的矩形。事实上我们可以选择保留四个矩形中的任何一个,比如我们可以选择保留右下角的矩形。


background: 
radial-gradient(
circle at left top,
transparent 0,
transparent 20px,
#ddd 20px
)
right bottom;

代码运行后效果如下:


no-repeat-right-bottom.png


组合前三个步骤


第四步是组合前三个步骤的语法。


看完第三步后,一个很自然的想法,就是用四个渐变分别形成四个特殊矩形,然后把四个特殊矩形分别放在左上、右上、左下和右下角,最后得到有四个反向圆角的矩形。


关键代码如下,为了区分四个径向渐变,我给左上、右上、左下、右下分别设置了红、绿、蓝、黄四种颜色:


background: radial-gradient(
circle at left top,
transparent 0,
transparent 20px,
red 20px
)
left top,
radial-gradient(
circle at right top,
transparent 0,
transparent 20px,
green 20px
)
right top,
radial-gradient(
circle at left bottom,
transparent 0,
transparent 20px,
blue 20px
)
left bottom,
radial-gradient(
circle at right bottom,
transparent 0,
transparent 20px,
yellow 20px
)
right bottom;
background-repeat: no-repeat;
background-size: 50% 50%;

代码运行效果如下:


rect2.png


不难想到,只要把红、绿、蓝、黄都换为灰色,就可以得到一个全灰、有四个反向圆角的矩形。


rect2-gray.png


把背景改写为遮罩


知道四个反向圆角的矩形如何实现后,我们可以:



  • background-size 改写为 mask-size

  • background-repeat 改写为 mask-repeat;

  • background 改写为 mask


这样就可以得到四个反向圆角的矩形遮罩。


mask: radial-gradient(
circle at left top,
transparent 0,
transparent 20px,
#ddd 20px
)
left top,
radial-gradient(
circle at right top,
transparent 0,
transparent 20px,
red 20px
)
right top,
radial-gradient(
circle at left bottom,
transparent 0,
transparent 20px,
red 20px
)
left bottom,
radial-gradient(
circle at right bottom,
transparent 0,
transparent 20px,
red 20px
)
right bottom;
mask-size: 50% 50%;
mask-repeat: no-repeat;

我们可以把四个反向圆角的矩形覆盖在一张背景图片上,就得到了和热风小程序一样的效果:


rect3.png


需要注意的是,部分手机浏览器不支持 mask 语法,所以我们有必要再设置一份 -webkit-mask-webkit-mask-size-webkit-mask-repeat


代码示例



总结


本文我们介绍了如何实现四个反向圆角的图片。


我们可以利用径向渐变,实现四个反向圆角的矩形。然后我们把这个矩形作为遮罩,覆盖在背景图片上,这样就实现了四个反向圆角的图片。




作者:小霖家的混江龙
来源:juejin.cn/post/7338280070303350834
收起阅读 »

回顾我这三年,都是泡沫

昨天,一个在掘金认识的小伙伴,进入了美团专门做 IDE 的基建组,心底真是替他高兴,这本来就是他应得的。 刚认识的时候还是一个工作一年的小毛孩,整天逮着我问各种问题,模板引擎、Babel、Electron、Jest、Rollup… 虽然没见过面,不知道他长什么...
继续阅读 »

朋友圈


昨天,一个在掘金认识的小伙伴,进入了美团专门做 IDE 的基建组,心底真是替他高兴,这本来就是他应得的。


刚认识的时候还是一个工作一年的小毛孩,整天逮着我问各种问题,模板引擎、Babel、Electron、Jest、Rollup…


虽然没见过面,不知道他长什么样,在我脑海里,他就是两样放着光,对技术充满好奇心、自我驱动力很强小伙子。


我就知道他能成,因为我多少也是这样子的,尽管我现在有些倦怠。


后来,随着工作越来越忙,博客也停更了,我们便很少联系了。


不过,后面我招人,尤其是校招生或者初级开发,我都是按照他这个范本来的。我也时常跟别人提起,我认识北京这样一个小伙子。


也有可能我们这边庙太小了,这样的小伙伴屈指可数。


平台和好奇心一样重要


大部分人智商条件不会有太多的差距,尤其是程序员这个群体,而好奇心可以让你比别人多迈出一步,经过长时间的积累就会拉开很大的差距。


而平台可以让你保持专注,与优秀的人共事,获得更多专业的经验和知识、财富,建立自己的竞争壁垒。








回到正题。


我觉得是时候阶段性地总结和回望回顾我过去这三年,却发现大部分都是泡沫。跨端、业务、质量管理、低代码、领域驱动设计... 本文话题可能会比较杂




2020 年七月,口罩第二年。我选择了跳槽,加入了一家创业公司




跨端开发的泡沫


2020 年,微信小程序已经成为国内重要的流量入口,事实也证明,我们过去几年交付的 C 端项目几乎上都是小程序。更严谨的说,应该是微信小程序,尽管很多巨头都推出了自己的小程序平台,基本上都是陪跑的。




Taro 2.x


进来后接手的第一个项目是原生小程序迁移到 Taro。


那时候,我们的愿景是“一码多端”,期望一套程序能够跑在微信小程序、支付宝小程序等小程序平台、H5、甚至是原生 App。


那时候 Taro 还是 2.x 版本,即通过语法静态编译成各端小程序的源码。


我们迁移花了不少的时间,尽管 Taro 官方提供了自动转换的工具,但是输出的结果是不可靠的,我们仍需要进行全量的回归测试,工作量非常大。 期间我也写了一个自动化代码迁移 CLI 来处理和 Lint 各种自动迁移后的不规范代码。




重构迁移只是前戏。难的让开发者写好 Taro,更难的是写出跨端的 Taro 代码。




我总结过,为什么 Taro(2.x) 这么难用:



  • 很多初级开发者不熟悉 React。在此之前技术栈基本是 Vue

  • 熟悉 React 的却不熟悉 Taro 的各种约束。

  • 即使 Taro 宣称一码多端,你还是需要了解对应平台/端的知识。 即使是小程序端,不同平台的小程序能力和行为都有较大的区别。而 Taro 本身在跨端上并没有提供较好的约束,本身 Bug 也比较多。

  • 如果你有跨端需求,你需要熟知各端的短板,以进行权衡和取舍。强调多端的一致和统一会增加很多复杂度, 对代码的健壮性也是一个比较大的考验。

  • 我们还背着历史包袱。臃肿、不规范、难以维护、全靠猜的代码。




在跨端上,外行人眼里‘一码多端’就是写好一端,其他端不用改就可以直接运行起来,那有那么简单的事情?


每个端都有自己的长板和短板:


短板效应


我们从拆分两个维度来看各端的能力:


维度




放在一个基线上看:


对比


跨端代码写不好,我们不能把锅扔给框架,它仅仅提供了一种通用的解决方案,很多事情还是得我们自己去做。




实际上要开发跨平台的程序,最好的开发路径就是对齐最短的板,这样迁移到其他端就会从而很多,当然代价就是开发者负担会很重:


路径


为了让开发者更好的掌握 Taro, 我编写了详细的 Wiki, 阐述了 React 的各种 trickTaro 如何阉割了 ReactTaro 的原理、开发调试、跨端开发应该遵循的各种规范






Taro 3.0


我们的 Taro 项目在 2020 年底正式在生产使用,而 Taro 3.0 在 2020 年 / 7 月就正式发布了,在次年 5 月,我们决定进行升级。


技术的发展就是这么快,不到 5 个月时间,Taro 2.x 就成为了技术债。


Taro 2.x 官方基本停止了新功能的更新、bug 也不修了,最后我们不得不 Fork Taro 仓库,发布在私有 npm 镜像库中。




Taro 2.x 就是带着镣铐跳舞,实在是太痛苦,我写了一篇文档来历数了它的各种‘罪行’:



  • 2.x 太多条条框框,学习成本高

  • 这是一个假的 React

  • 编译慢

  • 调试也太反人类







Taro 3.x 使用的是动态化的架构,有很多优势:


3.x 架构 和数据流


3.x 架构 和数据流



  • 动态化的架构。给未来远程动态渲染、低代码渲染、使用不同的前端框架(支持 Vue 开发)带来了可能

  • 不同端视图渲染方式差异更小,更通用,跨端兼容性更好。

  • 2.x 有非常多的条条框框,需要遵循非常多的规范才能写出兼容多端的代码。3.x 使用标准 React 进行开发,有更好的开发体验、更低的学习成本、更灵活的代码组织。

  • 可以复用 Web 开发生态。




使用类似架构的还有 Remax、Alita、Kbone, 我之前写过一篇文章实现的细节 自己写个 React 渲染器: 以 Remax 为例(用 React 写小程序)




而 Taro 不过是新增了一个中间层:BOM/DOM,这使得 Taro 不再直接耦合 React, 可以使用任意一种视图框架开发,可以使用 Vue、preact、甚至是 jQuery, 让 Web 生态的复用成为可能。




升级 3.x 我同样通过编写自动化升级脚本的形式来进行,这里记录了整个迁移的过程。








重构了再重构


我在 2B or not 2B: 多业态下的前端大泥球 讲述过我们面临的困境。


21 年底,随着后端开启全面的 DDD 重构(推翻现有的业务,重新梳理,在 DDD 的指导下重新设计和开发),我们也对 C 端进行了大规模的重构,企图摆脱历史债务,提高后续项目的交付效率




C 端架构


上图是重构后的结果,具体过程限于篇幅就不展开了:





  • 基础库:我们将所有业务无关的代码重新进行了设计和包装。

    • 组件库:符合 UI 规范的组件库,我们在这里也进行了一些平台差异的抹平

    • api: Taro API 的二次封装,抹平一些平台差异

    • utils: 工具函数库

    • rich-html、echart:富文本、图表封装

    • router:路由导航库,类型安全、支持路由拦截、支持命名导航、简化导航方法…




  • 模块化:我们升级到 Taro 3.x 之后,代码的组织不再受限于分包和小程序的约束。我们将本来单体的小程序进行了模块的拆分,即 monorepo 化。按照业务的边界和职责拆分各种 SDK

  • 方案:一些长期积累开发痛点解决方案,比如解决分包问题的静态资源提取方案、解决页面分享的跳板页方案。

  • 规范和指导实现。指导如何开发 SDK、编写跨平台/易扩展的应用等等




巨头逐鹿的小程序平台,基本上是微信小程序一家独大


跨端框架,淘汰下来,站稳脚跟的也只有 taro 和 uniapp


时至今日,我们吹嘘许久的“一码多端”实际上并没有实现;








大而全 2B 业务的泡沫


其实比一码多端更离谱的事情是“一码多业态”。


所谓一码多业态指的是一套代码适配多个行业,我在 2B or not 2B: 多业态下的前端大泥球 中已经进行了深入的探讨。


这是我过去三年经历的最大的泡沫,又称屎山历险记。不要过度追求复用,永远不要企图做一个大而全的 2B 产品






低代码的泡沫


2021 年,低代码正火,受到的资本市场的热捧。


广义的低代码就是一个大箩筐,什么都可以往里装,比如商城装修、海报绘制、智能表格、AI 生成代码、可视化搭建、审核流程编排…


很多人都在蹭热点,只要能粘上一点边的,都会包装自己是低代码,包括我们。在对外宣称我们有低代码的时候,我们并没有实际的产品。现在 AI 热潮类似,多少声称自己有大模型的企业是在裸泳呢?




我们是 2B 赛道,前期项目交付是靠人去堆的,效率低、成本高,软件的复利几乎不存在。


低代码之风吹起,我们也期望它能破解我们面临的外包难题(我们自己都在质疑这种软件交付方式和外包到底有什么区别)。


也有可能是为了追逐资本热潮,我们也规划做自己的 PaaS、aPaaS、iPaaS… 各种 “aaS”(不是 ass)。


但是我们都没做成,规划和折腾了几个月,后面不了了之,请来的大神也送回去了。




在我看来,我们那时候可能是钱多的慌。但并没有做低代码的相关条件,缺少必要的技术积累和资源。就算缩小范围,做垂直领域的低代码,我们对领域的认知和积累还是非常匮乏。




在这期间, 我做了很多调研,也单枪匹马撸了个 “前端可视化搭建平台”:


低代码平台


由于各种原因, 这个项目停止了开发。如今社区上也有若干个优秀的开源替代物,比如阿里的低代码引擎、网易云的 Tango、华为云的 TinyEngine。如果当年坚持开发下去,说不定今天也小有成就了。




不管经过这次的折腾,我越坚信,低代码目前还不具备取代专业编程的能力。我在《前端如何破解 CRUD 的循环》也阐述过相关的观点。


大型项目的规模之大、复杂度之深、迭代的周期之长,使用低代码无疑是搬石头砸自己的脚。简单预想一下后期的重构和升级就知道了。




低代码的位置


低代码是无代码和专业编码之间的中间形态,但这个中间点并不好把握。比如,如果倾向专业编码,抽象级别很低,虽然变得更加灵活,但是却丧失了易用性,最终还是会变成专业开发者的玩具。


找对场景,它就是一把利器。不要期望它能 100% 覆盖专业编码,降低预期,覆盖 10%?20%?再到 30%? 已经是一个不错的成就。


低代码真正可以提效不仅在于它的形式(可视化),更在于它的生态。以前端界面搭建为例,背后开箱即用的组件、素材、模板、应用,才是它的快捷之道。


在我看来,低代码实际上并不是一个新技术,近年来火爆,更像是为了迎合资本的炒作而稍微具象化的概念。


而今天,真正的’降本增效‘的大刀砍下来,又有多少’降本增效‘的低代码活下来了呢?








质量管理的泡沫


2021 年四月,我开始优化前端开发质量管理,设计的开发流程如下:


流程


开发环境:



  • 即时反馈:通过 IDE 或者构建程序即时对问题进行反馈。

  • 入库前检查:这里可以对变动的源代码进行统一格式化,代码规范检查、单元测试。如果检查失败则无法提交。


集成环境:



  • 服务端检查:聪明的开发者可能绕过开发环境本地检查,在集成环境我们可以利用 Gerrit + Jenkins 来执行检查。如果验证失败,该提交会被拒绝入库。

  • CodeReview:CodeReview 是最后一道防线,主要用于验证机器无法检验的设计问题。

  • 自动化部署:只有服务端检查和 CodeReview 都通过才能提交到仓库

    • 测试环境:即时部署,关闭安全检查、开启调试方便诊断问题

    • 生产环境:授权部署




生产环境:


前端应用在客户端中运行,我们通常需要通过各种手段来监控和上报应用的状态,以便更快地定位和解决客户问题。






原则一:我认为“自动化才是秩序”:


文档通常都会被束之高阁,因此单靠文档很难形成约束力。尤其在迭代频繁、人员构造不稳定的情况。规范自动化、配合有效的管理才是行之有效的解决办法。



  • 规范自动化。能够交给机器去执行的,都应该交给机器去处理, 最大程度降低开发者的心智负担、犯错率。可以分为以下几个方面:

    • 语言层面:类型检查,比如 Typescript。严格的 Typescript 可以让开发者少犯很多错误。智能提示对开发效率也有很大提升。

    • 风格层面:统一的代码格式化风格。例如 Prettier

    • 规范层面:一些代码规范、最佳实践、反模式。可以遵循社区的流行规范, 例如 JavaScript Standard

    • 架构层面:项目的组织、设计、关联、流程。可以通过脚手架、规范文档、自定义 ESLint 规则。



  • 管理和文化: 机器还是有局限性,更深层次的检查还是需要人工进行。比如单元测试、CodeReview。这往往需要管理来驱动、团队文化来支撑。这是我们后面需要走的路。






原则二:不要造轮子


我们不打算造轮子,建立自己的代码规范。社区上有很多流行的方案,它们是集体智慧的结晶,也最能体现行业的最佳实践:


社区规范


没必要自己去定义规则,因为最终它都会被废弃,我们根本没有那么多精力去维护。






实现


企业通知 Code Review


企业通知 Code Review






我们这套代码质量管理体系,主要基于以下技术来实现:



  • Jenkins: 运行代码检查、构建、通知等任务

  • Gerrit:以 Commit 为粒度的 CodeReview 工具

  • wkfe-standard: 我们自己实现渐进式代码检查 CLI






如果你想了解这方面的细节,可以查看以下文档:





我推崇的自动化就是秩序目的就是让机器来取代人对代码进行检查。然而它只是仅仅保证底线。


人工 CodeReview 的重要性不能被忽略,毕竟很多事情机器是做不了的。


为了推行 CodeReview,我们曾自上而下推行了 CCC(简洁代码认证) 运动,开发者可以提交代码让专家团队来 Code Review,一共三轮,全部通过可以获得证书,该证书可以成为绩效和晋升的加分项;除此之外还有代码规范考试…


然而,这场运动仅仅持续了几个月,随着公司组织架构的优化、这些事情就不再被重视。


不管是多么完善的规范、工作流,人才是最重要的一环,到最后其实是人的管理






DDD / 中台的泡沫


近年来,后端微服务、中台化等概念火热,DDD 也随之而起。


DDD 搜索趋势


上图的 DDD Google 趋势图,一定程度可以反映国内 DDD 热度的现实情况:



  • 在 14 年左右,微服务的概念开始被各方关注,我们可以看到这年 DDD 的搜索热度有明显的上升趋势

  • 2015 年,马某带领阿里巴巴集团的高管,去芬兰的赫尔辛基对一家名叫 supercell 的游戏公司进行商务拜访,中台之风随着而起,接下来的一两年里,DDD 的搜索热度达到了顶峰。

  • 2021 ~ 2022 年,口罩期间,很多公司业务几乎停摆,这是一个’内修‘的好时机。很多公司在这个阶段进行了业务的 DDD 重构,比较典型的代表是去哪儿业务瘦身 42%+效率提升 50% :去哪儿网业务重构 DDD 落地实践)。




上文提到,我们在 2021 年底也进行了一次轰轰烈烈的 DDD 重构战役,完全推翻现有的项目,重新梳理业务、重新设计、重新编码。


重构需要投入了大量的资源,基本公司 1 / 3 的研发资源都在里面了,这还不包括前期 DDD 的各种预研和培训成本。


在现在看来,这些举措都是非常激进的。而价值呢?现在还不’好说‘(很难量化)






DDD 落地难


其实既然开始了 DDD 重构, 就说明我们已经知道 ’怎么做 DDD‘ 了,在重构之前,我们已经有了接近一年的各种学习和铺垫,且在部分中台项目进行了实践。


但我至今还是觉得 DDD 很难落地,且不说它有较高的学习成本,就算是已落地的项目我们都很难保证它的连续性(坚持并贯彻初衷、规范、流程),烂尾的概率比较高。


为了降低开发者对 DDD 的上手门槛,我们也进行了一些探索。






低代码 + DDD?


可视化领域建模


可视化领域建模


2022 下半年,我们开始了 ’DDD 可视化建模‘ 的探索之路,如上图所示。


这个平台的核心理念和方法论来源于我们过去几年对 DDD 的实践经验,涵盖了需求资料的管理、产品愿景的说明、统一语言、业务流程图、领域模型/查询模型/领域服务的绘制(基于 CQRS),数据建模(ER)、对象结构映射(Mapper)等多种功能,覆盖了 DDD 的整个研发流程。


同时它也是一个知识管理平台,我们希望在这里聚合业务开发所需要的各种知识,包括原始需求资料、统一语言、领域知识、领域建模的结果。让项目的二开、新团队成员可以更快地入手。


最终,建模的结果通过“代码生成器”生成代码,真正实现领域驱动设计,而设计驱动编码。


很快我们会完全开源这套工具,可以关注我的后续文章。






DDD 泡沫


即使我们有’低代码‘工具 + 代码自动生成的加持,实现了领域驱动设计、设计驱动编码,结果依旧是虎头蛇尾,阻止不了 DDD 泡沫的破裂。




我也思考了很多原因,为什么我们没有’成功‘?





  • DDD 难?学习曲线高

  • 参与的人数少,DDD 受限在后端开发圈子里面,其他角色很少参与进来,违背了 DDD 的初衷

  • 重术而轻道。DDD 涵括了战略设计和战术设计,如果战略设计是’道‘、战术设计就是’术‘,大部分开发者仅仅着眼于术,具体来说他们更关注编码,思维并没有转变,传统数据建模思维根深蒂固

  • 中台的倒台,热潮的退去


扩展阅读:







一些零碎的事


过去三年还做了不少事情,限于篇幅,就不展开了:







过去三年经历时间轴:



  • 2020 年 7 月,换了公司,开始接手真正迁移中的 Taro 项目

  • 2020 年 10 月,Taro 2.x 小程序正式上线

  • 2020 年 10 月 ~ 11 月 优化代码质量管理体系,引入开发规范、Gerrit Code Review 流程

  • 2020 年 12 月 ~ 2021 年 4 月,业务开发

  • 2021 年 1 月 博客停更

  • 2021 年 5 月 Taro 3.x 升级

  • 2021 年 7 月 ~ 10 月 前端低代码平台开发

  • 2021 年 11 月 ~ 2022 年 5 月, DDD 大规模重构,C 端项目重构、国际化改造

  • 2022 年 6 月 ~ 2022 年 11 月,B 端技术升级,涉及容器化改造、微前端升级、组件库开发等

  • 2022 年 12 月~ 2023 年 4 月,可视化 DDD 开发平台开发

  • 2023 年 5 月 ~ 至今。业务开发,重新开始博客更新








总结


贝尔实验室


我们都有美好的愿望


重构了又重构,技术的债务还是高城不下


推翻了再推翻,我们竟然是为了‘复用’?


降本增效的大刀砍来


泡沫破碎,回归到了现实


潮水退去,剩下一些裸泳的人


我又走到了人生的十字路口,继续苟着,还是换个方向?


作者:荒山
来源:juejin.cn/post/7289718324857880633
收起阅读 »

你的年终奖怎么算个税?你的算法对吗?

又到了一年一度的报税阶段。相信现在很多人都是把年终奖分出来单独计税的,但是最近在计算年终奖交税的时候,突然觉得有些怪怪的,网上一查,不止我一个人发现有问题,这里记录一下,也听听大伙有什么看法。 前置工作,我们先来看看个税是怎么计算的。 单独计算 应纳税额 = ...
继续阅读 »

又到了一年一度的报税阶段。相信现在很多人都是把年终奖分出来单独计税的,但是最近在计算年终奖交税的时候,突然觉得有些怪怪的,网上一查,不止我一个人发现有问题,这里记录一下,也听听大伙有什么看法。


前置工作,我们先来看看个税是怎么计算的。


单独计算


应纳税额 = 年终奖金额 × 适用税率 - 速算扣除数



出处:财政部税务总局公告2023年第30号,自己百度



假设我拿了 40000 年终奖,而且设置单独计算,按照 2023 年税法规定,年终奖适用的七档分阶段税率以及速算扣除数如下(税表三):



全年一次性奖金单独计税的优惠政策曾在2021年12月延续2年,原本将于2023年年底到期。23年发布的《关于延续实施全年一次性奖金个人所得税政策的公告》将该政策再延4年



image.png



来源:http://www.gerensuodeshui.cn/index_gsslb…



适用税率使用的是年终奖总额除以12,分摊到12个月里的均值:40000 / 12 = 3333.33..., 查表后显示我在第 2 档,速算扣除数是 210,税率 10%.


按照计算公式,我的应交税额是:40000 * 10% - 210 = 3790.


合并计算


综合所得应纳税额 = {累计综合所得收入(含全年一次性奖金)-累计减除费用-累计专项扣除-累计专项附加扣除-累计依法确定的其他扣除-捐赠} × 适用税率-速算扣除数


顾名思义,就是将年终奖并入工资计算,这就适合税率表一了:


image.png


比如你每月工资 10000,允许扣除的三险一金每月为 2000 元,专项附加每月扣除 2000 元。并入以后全年收入所得:10000 * 12 + 40000 = 160000,查表可得在第三档。税率 20%:(160000 - 2000×12 - 2000×12)* 20% - 16920 = 5480.


交的税明显多多了,你会想,只有傻子才会合并计算啊!


单独 or 合并 ?


但是也有例外,比如你一个工资 5000,年终奖 500000,允许扣除的三险一金每月为 2000 元,专项附加每月扣除2000元,没有其他扣除项目。则单独计税时,年终奖缴纳:500000 * 30% - 4410 = 145590,若是合并计算(全年 56w 收入,在表一的第五档)需缴纳:560000 - 2000 × 12 - 2000 × 12)× 30% - 52920 = 100680,这时候合并计算反而划算了。


大多数情况,当你工资比较多,年终奖相对较少时选择单独计算会更好;当你的年终奖在全年收入中占大头时,合并计算更有利。


临界点问题


你以为这就完了?问题才刚开始,看一下知乎有人的帖子:为什么年终奖个税会存在BUG?


讲的是在现行的税法和相关文件中,年终奖(全年一次性奖金)的个人所得税存在人为设计的「临界点问题」。这个问题新华网也证实了:新华网关于税费计算,即:高收入者的税后收入反而会比低收入者的税后收入还要低的问题


我们以单独计算为例,假设我年终奖拿了 36000,那么交税:36000 * 3% - 0 = 1080,那么我到手 34920. 假设老板发了善心,给我长了一块的工资,现在是 36001,那么查表可得,在表三中我跨档了,要交税:36001 * 10% - 210 = 3390.1, 此时我到手:32610.9,反而到手还低了!?


网上说是计算公式”弄错“了速算扣除数。速算扣除数并非税法规定,而是根据税法推算出来的一个方便计算的系数。至于速算扣除数怎么来的,可以看这篇知乎文章,写的很详细。我这里总结一下,就是速算扣除数是反推计算的:



  • 税率3%对应速算扣除数为0

  • 税率10%对应速算扣除数为3000*(10%-3%)+0=210

  • 税率20%对应速算扣除数为12000*(20%-10%)+210=1410

  • 税率35%对应速算扣除数为25000*(25%-20%)+555=2660

  • ...


按我们这个例子:


36,000元,除以12得3000.0000,对应税率3%,速算扣除数0,


税金: 1080 元;


36,001元,除以12得3000.0833,对应税率10%,速算扣除数210,


税金: 1590.1;


据那篇文章所说,问题出现在速算扣除数上,这里的速算扣除数应该用年的,而公式只用了月的


如果你还是不清楚,我们不用速算扣除数计算,我们按照税法规定的:



  • 不超过1500元的 3

  • 超过1500元至4500元的部分 10

  • 超过4500元至9000元的部分 20

  • 超过9000元至35000元的部分 25

  • ...


按照税法规定,36,000 年终奖,月平均是 3000,按照他“修订”的公式,应该缴纳:(3000 * 3% + 0 * 10%) * 12 = 1080,36,001 年终奖,月平均 3000.0833, 按照他“修订”的公式,应该缴纳:(3000 * 3% + 0.0833 * 10%) * 12 = 1080.1,等价于现行算法 (3000.0833 * 10% - 210) * 12 相对合理?


文章指出现行算法是这样的: 36001 * 10% - 210 = 3390.1。只减了一个210,就是说每个月都多算了 7%。


总结


按照推论,现有的速算扣除数 * 12 才是真正的速算扣除数。所以计算公式应该是:


应纳税额 = (年终奖金额 / 12 × 适用税率 - 速算扣除数) * 12


按照这个公式,上面36,000 年终奖,月平均是 3000,应该缴纳 (36000 / 12 * 3% - 0) * 12 = 1080,36,000 年终奖,月平均是 3000,应该缴纳 (36001 / 12 * 10% - 210) * 12 = 1080.1



依据: 财政部税务总局公告2023年第30号





所以,写到这里,我也糊涂了,年终奖交税到底要怎么算?谁能告诉我😓


作者:小肚肚肚肚肚哦
来源:juejin.cn/post/7346720393414492199
收起阅读 »

H5、小程序 Tab 如何滚动居中

web
H5、小程序 Tab 如何滚动居中 Tab 在 PC 端、移动端应用都上很常见,不过 Tab 在移动端 比 PC 端更复杂。为什么呢?移动端设备屏幕较窄,一般仅能展示 4 ~ 7 个 Item。考虑到用户体验,UI 往往要求程序员实现一个功能——点击 Item...
继续阅读 »

H5、小程序 Tab 如何滚动居中


Tab 在 PC 端、移动端应用都上很常见,不过 Tab 在移动端 比 PC 端更复杂。为什么呢?移动端设备屏幕较窄,一般仅能展示 4 ~ 7 个 Item。考虑到用户体验,UI 往往要求程序员实现一个功能——点击 Item 后,Item 滚动到屏幕中央,拼多多的 Tab 就实现了这个功能。


pdd.gif


如果你也想实现这个功能,看了这篇文章,你一定会有所收获。我会先说明 Tab 滚动的本质,分析出滚动距离的计算公式,接着给出伪代码,最后再给出 Vue、React 和微信小程序的示例代码。


Tab 滚动的本质


Tab 滚动,本质是包裹着 Item 的容器在滚动。


如下图,竖着的虚线长方形代表手机屏幕,横着的长方形代表 Tab 的容器,标着数字的小正方形代表一个个 Tab Item。


左半部分中,Tab 容器紧贴手机屏幕左侧。右半部分中,Item 4 位于屏幕中央,两部分表示 Item 4 从屏幕右边滚动到屏幕中央。


scroll-left.png


不难看出,Item 4 滚动居中,其实就是容器向左移动 distance。此时容器滚动条到容器左边缘的距离也是 distance。


换句话说,让容器向左移动 distance,Item 4 就能居中。 因此只要我们能找出计算 distance 的公式,就能控制某个 Item 居中。


计算 distance 的公式


该如何计算 distance 呢?我们看下方这张更细致的示意图。


屏幕中央有一条线,它把 Item 4 分成了左右等宽的两部分,也把手机屏幕分成了左右等宽的两部分。你可以把 Item 4 一半的宽度记为 halfItemWidth,把手机屏幕一半的宽度记为 halfScreenWidth。再把 Item 4 左侧到容器左侧的距离记为 itemOffsetLeft


calculate-scroll-left.png


不难看出,这四个值满足如下等式:


distance + halfScreenWidth = itemOffsetLeft + halfItemWidth

简单推导一下,就得到了计算 distance 的公式。


distance = itemOffsetLeft + halfItemWidth - halfScreenWidth

公式的伪代码实现


现在开始解释公式的代码实现。


先看下 itemOffsetLefthalfItemWidthhalfScreenWidth 如何获取。



  • itemOffsetLeft 是 Item 元素到容器左侧的距离,你可以用 HTMLElement.offsetLeft 作它的值。

  • halfItemWidth 是 Item 元素一半的宽度。HTMLElement.offsetWidth 是元素的整体宽度,你可以用 offsetWidth / 2 作它的值,也可以先用 Element.getBoundingClientRect() 获取一个 itemRect 对象,再用 itemRect.width / 2 作它的值。

  • halfScreenWidth 是手机屏幕一半的宽度。 window.innerWidth 是手机屏幕的整体宽度,你可以用 innerWidth / 2 作它的值。


再看下如何把 distance 设置到容器上。


在 HTML 中,我们可以使用 Element.scrollLeft 来读取和设置元素滚动条到元素左边的位置。因此,你只需要容器的 scrollLeft 赋值为 distance,就可以实现 Item 元素滚动居中。


现在给出点击 tab 的函数的伪代码:


const onClick = () => {
const itemOffsetLeft = item.offsetLeft;
const halfItemWidth = item.offsetWidth / 2;
const halfScreenWidth = window.innerWidth / 2;
tabContainer.scrollLeft = itemOffsetLeft + halfItemWidth - halfScreenWidth
}

代码示例


Vue


Tab 滚动居中 | Vue


React


Tab 滚动居中 | React


微信小程序


Tab 滚动居中 | 微信小程序


小程序的 API 和浏览器的 API 有差异。



  • itemOffsetLeft ,你需要从点击事件的 event.currentTarget 中获取。

  • halfItemWidth,你需要先用 wx.createSelectorQuery() 选取到 Item 后,从 exec() 的执行结果中获取到 Item 整体宽度,然后再除以 2。

  • halfScreenWidth,你需要先用 wx.getSystemInfoSync() 获取屏幕整体宽度,然后再除以 2。


至于把 distance 设置到容器上,微信小程序 scroll-view 组件中,有 scroll-left 这个属性,你可以把 distance 赋值给 scroll-left




作者:小霖家的混江龙
来源:juejin.cn/post/7322730720732921867
收起阅读 »

身在职场,必须面对的两点焦虑

今天听到了堂弟考编上岸的消息,挺为他感到高兴的,因为准备了很久,也失败了多次,最终取得了胜利的果实,真的可以用苦尽甘来来形容。 对于小县城家境一般的孩子,如果不去职场历练,那么唯一的路就是就是考编,但是这条路是充满艰辛的。 去年和一30几岁的同事聊天,他说很羡...
继续阅读 »

今天听到了堂弟考编上岸的消息,挺为他感到高兴的,因为准备了很久,也失败了多次,最终取得了胜利的果实,真的可以用苦尽甘来来形容。


对于小县城家境一般的孩子,如果不去职场历练,那么唯一的路就是就是考编,但是这条路是充满艰辛的。


去年和一30几岁的同事聊天,他说很羡慕那些考上编制的,即使编制内的工作强度和职场上的工作强度不相上下,但是也是让人望尘莫及。


我思考了下,在职场中为什么感觉累?其实就是两点。



1.对未来的不确定而感动担忧。




2.对当下的不稳定性而焦虑。



对未来的不确定而感动担忧


对于大多数人而言,在职场中就是充当一颗螺丝钉,没有啥核心竞争力,工作所获得的报酬也不可观,面对职场上越来越高的要求,而自己的能力却在原地踏步,坑位越来越少,但是新人却不断在涌入。


这时候就会产生对未来的担忧,这种担忧是对未来是否能在职场里混下去,如果混不下去,那自己又能做什么?


如果现在自己尚未成家立业,那么这种担忧可能不会很强烈,但是如果已经成家,并且身上的担子也不轻,那么这种担忧就会愈加强烈。


这个担忧里面还可以剖解出两个点,职业是否有第二曲线接受现实的能力


第一,如果自己已经想好,并且规划好了路线,也有收获了,那么其实是不会有太大的担忧,充其量就是离开平台,换到一个自己有利用价值得平台,或者自己干,这就是职业得第二曲线。


比如年龄大了写不了代码了,那么是否具备做销售的能力,或者是否具有跨行业去做和现在收入不相上下的能力。


第二,是否具有接受现实的能力,比如离开当前的行业,是否愿意跨行去做其它行业并且收入会下降很多,这也和个人消费情况有关。


比如之前的一个同事,因为快40岁了,在职场上已经没有竞争力了,于是他就跨行业去做建材销售,收入肯定比之前的行业低了不少,但是我和他聊的时候,他说其实自己早就意识到这个问题,所以今天走到这一步,其实是在自己的预料里,所以是能坦然接受,不会有很大的心理负担。


那么这种情况其实也不会过于焦虑,就像我们读书的时候,有些同学每天都在玩,你问他如果考不上好的大学怎么办,他会说:我已经准备好去读专科了。那么最后去读专科已经在他的意料中,所以当下自然不会焦虑。


但是有的人又不想学,但是又想考一本,那么当下自然就会焦虑,因为自己希望变好,但是又不去行动。


对当下的不稳定性而焦虑


对于当下的不稳定在我们这个时代其实更加明显,因为到处都在裁员,找工作的人大把。


如果自己所在的公司效益不好,一段时间就会裁员一波人,而自己对于目前的市场又不太了解,那么就会焦虑。


因为今天自己没有拿大礼包,不代表明天不拿,明天不拿,不代表后天不拿,只要效益一日不变好,那么就会提心吊胆的。


所以这就是为什么那些多人击破头脑去考公务员的原因,其实就是不敢面对不稳定性,不敢面对市场的跌宕起伏,并且也意识到自己能力很平庸,没有啥竞争力。


因为就目前来说,考上编制,即使工资不如自己之前,但是他是确定性的,只要你不犯大的错误,那么呆一辈子其实是可能的。


只要你没有太大的支出,并且没有太大的消费欲望,那么其实是可以安安稳稳过日子的,这就是体制的魔力。




以上两点,基本就能概括职场中感到累的原因。


但是我们多数人是考不上编制的,你可以去报一个名试试,相信你会怀疑人生。


那么面对职场的焦虑,这是就需要去做一些实质上的行动和心态上的调整。


实质上的行动就是深耕领域,提升竞争力,或者发展第二曲线,俗话说得好,闲时养兵,战时亮剑,有些现在看似没有用的东西,实际上会在某个时间点发挥作用。


前段时间和几个朋友聊的时候,一朋友说自己还在医院上班的时候,虽然只是个小护士,但是医院里面的机器总是会坏,每次坏的时候,都要等好几天别人才来修,并且也要花费几万块钱,于是在别人修的时候自己就偷学,后面机器坏了,自己就悄悄修好,自己也不说,后面领导发现是他修的,于是对他更加器重。


那么这样的人,即使不做这一行,到了别的行业,他也能从中找到自己的竞争力,也会发光发亮,事实也是如此,他现在转行了,估计也是做得不错。


心态上的调整就是避免去想很多还没有发生的东西,即使知道这一天早晚要来,心态上也要若无其事,因为总是去想那么暂时还没有发生的事情一点卵用都没有,只会把心态搞崩。


其实如果我们仔细观察,我们会发现那些厉害的人,都比较乐观。


因为只有拥有乐观的心态,自己做事情的时候效率才会更高,才不会去想这想那,专注才是进步最好的基石,而专注的前提是乐观。



今天的分享就到这里,感谢你的观看。



作者:苏格拉的底牌
来源:juejin.cn/post/7345071716680679424
收起阅读 »

写个 Mixin 来自动维护 loading 状态吧

web
Vue 中处理页面数据有两种交互方式: 骨架屏:加载时提供骨架屏,加载失败展示错误页面和重试按钮,需要维护加载状态数据,适用于注重用户体验的精细页面 消息弹窗:加载过程中展示 loading 遮罩,失败时弹出错误消息提示,不需要维护加载状态数据,适用于后台管...
继续阅读 »

Vue 中处理页面数据有两种交互方式:



  • 骨架屏:加载时提供骨架屏,加载失败展示错误页面和重试按钮,需要维护加载状态数据,适用于注重用户体验的精细页面

  • 消息弹窗:加载过程中展示 loading 遮罩,失败时弹出错误消息提示,不需要维护加载状态数据,适用于后台管理系统等不太看重用户体验的页面,或者提交数据的场景


本文适用于骨架屏类的页面数据加载场景。


痛点描述


我们日常加载页面数据时,可能需要维护 loading 状态,就像这样:


<template>
<el-table v-loading="loading" :data="tableData"></el-table>
</template>
<script>
export default {
data() {
return {
tableData: [],
loading: false,
}
},
methods: {
async getTableData() {
this.loading = true
try {
this.tableData = await this.$http.get("/user/list");
} finally {
this.loading = false;
}
},
},
}
</script>

其实加载函数本来可以只有一行代码,但为了维护 loading 状态,让我们的加载函数变得复杂。如果还要维护成功和失败状态的话,加载函数还会变得更加复杂。


export default {
data() {
return {
tableData: [],
loading: false,
success: false,
error: false,
errmsg: "",
}
},
methods: {
async getTableData() {
this.loading = true;
this.success = false;
this.error = false;
try {
this.user = await this.$http.get("/user/list");
this.success = true;
} catch (err) {
this.error = true;
this.errmsg = err.message;
} finally {
this.loading = false;
}
},
},
}

如果页面有多个数据要加载,比如表单页面中有多个下拉列表数据,那么这些状态属性会变得特别多,代码量会激增。


export default {
data() {
return {
yearList: [],
yearListLoading: false,
yearListLoaded: false,
yearListError: false,
yearListErrmsg: "",
deptList: [],
deptListLoading: false,
deptListLoaded: false,
deptListError: false,
deptListErrmsg: "",
tableData: [],
tableDataLoading: false,
tableDataLoaded: false,
tableDataError: false,
tableDataErrmsg: ""
}
}
}

其实我们可以根据加载函数的状态来自动维护这些状态数据,这次我们要实现的目标就是自动维护这些状态数据,并将它们放到对应函数的属性上。看看这样改进后的代码:


<template>
<div v-if="tableData.success">
<!-- 显示页面内容 -->
</div>
<div v-else-if="tableData.loading">
<!-- 显示骨架屏 -->
</div>
<div v-else-if="tableData.error">
<!-- 显示失败提示 -->
</div>
</template>
<script>
export default {
data() {
return {
tableData: []
}
},
methods: {
async getTableData() {
this.tableData = await this.$http.get("/user/list");
},
}
}
</script>

加载函数变得非常纯净,data 中也不需要定义一大堆状态数据,非常舒适。


Mixin 设计


基本用法


我们需要指定一下 methods 中的哪些方法是用来加载数据的,我们只需要对这些加载数据的方法添加状态属性。根据我之前在文章《我可能发现了Vue Mixin的正确用法——动态Mixin》中的看法,可以使用函数形式的 mixin 来指定。


export default {
mixins: [asyncStatus('getTableData')],
methods: {
async getTableData() {},
},
}

指定多个方法


也可以用数组指定多个方法名。


export default {
mixins: [asyncStatus([
'getDeptList',
'getYearList',
'getTableData'
])],
}

自动扫描所有方法


如果不传参数,则通过遍历的方式,给所有组件实例方法加上状态属性。


export default {
mixins: [asyncStatus()]
}

全局注入


虽然给所有的组件实例方法加上状态属性是没必要的,但也不影响。而且这有个好处,就是可以注册全局 mixin。


Vue.mixin(asyncStatus())

默认注入的属性


我们默认注入的状态字段有4个:



  • loading 是否正在加载中

  • success 是否加载成功

  • error 是否加载失败

  • exception 加载失败时抛出的错误对象


指定注入的属性名


当然,为了避免命名冲突,可以传入第二个参数,来指定添加的状态属性名。


export default {
mixins: [asyncStatus('getTableData', {
// 注入的加载状态属性名是 isLoading
loading: 'isLoading',
// 注入的错误状态属性名是 hasError
error: 'hasError',
// 错误对象的属性名是 errorObj
exception: 'errorObj',
// 不注入 success 属性
success: false,
})]
}

随意传入参数


由于第一个参数和第二个参数的形式没有重叠,所以省略第一个参数也是可行的。


export default {
mixins: [asyncStatus({
loading: 'isLoading',
error: 'hasError',
exception: 'errorObj',
success: false,
})]
}

总结


总结一下,我们需要使用函数形式来实现 mixin,函数接收两个参数,并且两个参数都是可选的。


/**
* @/mixins/async-status.mixin.js
* 维护异步方法的执行状态,为组件中的指定方法添加如下属性:
* - loading {boolean} 是否正在执行
* - success {boolean} 是否执行成功
* - error {boolean} 是否执行失败
* - exception 方法执行失败时抛出的异常
* @param {string|string[]} [methods] 方法名,可指定多个
* @param {Alias} [alias] 为注入的属性指定属性名,或将某个属性设置成false跳过注入
*
* @typedef Alias
* @type {object}
* @prop {boolean|string} [loading=true]
* @prop {boolean|string} [success=true]
* @prop {boolean|string} [error=true]
* @prop {boolean|string} [exception=true]
*/

export function asyncStatus(methods, alias) {}

函数返回真正的 mixin 对象,为组件中的异步方法维护并注入状态属性。


Mixin 实现


注入属性的时机


实现这个 mixin 是有一定难度的,首先要找准注入属性的时机。我们希望尽可能早往方法上注入属性,至少在执行 render 函数之前,以便在加载状态变化时可以重现渲染,但又需要在组件方法初始化之后。


所以,你需要熟悉 Vue 的组件渲染流程。在 Vue2 的源码中有这样一段组件初始化代码:


Vue.prototype._init = function (options?: Object) {
// ...
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, "beforeCreate");
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, "created");
// ...
};

而其中的 initState 方法的源码如下:


export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

所以总结一下 Vue 组件的初始化流程:



  • 执行 beforeCreate

  • 挂载 props

  • 挂载 methods

  • 执行并挂载 data

  • 挂载 computed

  • 监听 watch

  • 执行 created


我们必须在 methods 初始化之后开始注入属性,否则方法还没挂载到组件实例上。可以选择的是 data 或者 created。为了尽早注入,我们应该选择在 data 中注入。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias) {
return {
data() {
// 在这里为方法注入状态属性
return {}
}
}
}

处理参数


由于参数的形式比较自由,我们需要处理并统一一下参数形式。我们把 methods 处理成数组形式,并取出 alias 中指定注入的状态属性名。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias = {}) {
// 只传入 alias 的情况
if (typeof methods === 'object' && !Array.isArray(methods)) {
alias = methods
}
// 将 methods 规范化成数组形式
if (typeof methods === 'string') {
methods = [methods]
}
if (!Array.isArray(methods)) {
// TODO: 这里应该换成遍历出的所有方法名
methods = []
}
// 获取注入的状态属性名
const getKey = (name) =>
typeof alias[name] === 'string' || alias[name] === false
? alias[name]
: name
const loadingKey = getKey('loading')
const successKey = getKey('success')
const errordKey = getKey('error')
const exceptionKey = getKey('exception')
}

遍历组件方法


没有传入 methods 的时候,需要遍历组件上定义的所有方法。办法是遍历 this.$options.methods 上的所有属性名,这样遍历出的结果会包含从 mixins 中引入的方法。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias = {}) {
return {
data() {
if (!Array.isArray(methods)) {
// 遍历出的所有方法名,注意这段代码需要在 data 中执行
methods = Object.keys(this.$options.methods)
}
return {}
}
}
}

维护加载状态


需要注意的是,只有响应式对象上的属性才会被监听,也就是说,只有响应式对象上的属性值变化才能引起组件的重新渲染。所以我们必须创建一个响应式对象,把加载状态维护进去。这可以通过 Vue.observable() 这个API来创建。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias = {}) {
return {
data() {
for (const method of methods) {
if (typeof this[method] === 'function') {
// 存储状态值
const status = Vue.observable({})
loadingKey && Vue.set(status, loadingKey, false)
successKey && Vue.set(status, successKey, false)
errorKey && Vue.set(status, errorKey, false)
exceptionKey && Vue.set(status, exceptionKey, false)
// 设置状态值
const setStatus = (key, value) => key && (status[key] = value)
}
}
return {}
}
}
}

我们把加载状态维护到 status 中。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias = {}) {
return {
data() {
for (const method of methods) {
if (typeof this[method] === 'function') {
const fn = this[method]
// 用于识别是否最后一次调用
let loadId = 0
// 替换原始方法
this[method] = (...args) => {
// 生成本次调用方法的标识
const currentId = ++loadId
setStatus(loadingKey, true)
setStatus(successKey, false)
setStatus(errorKey, false)
setStatus(exceptionKey, null)
try {
// 这里调用原始方法,this 为组件实例
const result = fn.call(this, ...args)
// 兼容同步和异步方法
if (result instanceof Promise) {
return result
.then((res) => {
// 最后一次加载完成时才更新状态
if (loadId === currentId) {
setStatus(loadingKey, false)
setStatus(successKey, true)
}
return res
})
.catch((err) => {
// 最后一次加载完成时才更新状态
if (loadId === currentId) {
setStatus(loadingKey, false)
setStatus(errorKey, true)
setStatus(exceptionKey, err)
}
throw err
})
}
setStatus(loadingKey, false)
setStatus(successKey, true)
return result
} catch (err) {
setStatus(loadingKey, false)
setStatus(errorKey, true)
setStatus(exceptionKey, err)
throw err
}
}
}
}
return {}
}
}
}

注入状态属性


其实需要注入的属性都在 status 中,可以把它们作为访问器属性添加到对应的方法上。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias = {}) {
return {
data() {
for (const method of methods) {
if (typeof this[method] === 'function') {
// 存储状态值
const status = Vue.observable({})
// 替换原始方法
this[method] = (...args) => {}
// 注入状态值到方法中
Object.keys(status).forEach((key) => {
Object.defineProperty(this[method], key, {
get() {
return status[key]
}
})
})
Object.setPrototypeOf(this[method], fn)
}
}
return {}
}
}
}

完整代码


最后整合一下完整的代码。


import Vue from 'vue'

/**
* @/mixins/async-status.mixin.js
* 维护异步方法的执行状态,为组件中的指定方法添加如下属性:
* - loading {boolean} 是否正在执行
* - success {boolean} 是否执行成功
* - error {boolean} 是否执行失败
* - exception 方法执行失败时抛出的异常
* @param {string|string[]|Alias} [methods] 方法名,可指定多个
* @param {Alias} [alias] 为注入的属性指定属性名,或将某个属性设置成false跳过注入
*
* @typedef Alias
* @type {object}
* @prop {boolean|string} [loading=true] 加载状态的属性名
* @prop {boolean|string} [success=true] 加载成功状态的属性名
* @prop {boolean|string} [error=true] 加载失败状态的属性名
* @prop {boolean|string} [exception=true] 加载失败时存储错误对象的属性名
*
* @example
* <template>
* <el-table v-loading="getTableData.loading" />
* </template>
* <script>
* export default {
* mixins: [
* asyncMethodStatus('goFetchData')
* ],
* methods: {
* async getTableData() {
* this.tableData = await this.$http.get('/user/list');
* }
* }
* }
* </script>
*/

export default function asyncMethodStatus(methods, alias = {}) {
// 规范化参数
if (typeof methods === 'object' && !Array.isArray(methods)) {
alias = methods
}
if (typeof methods === 'string') {
methods = [methods]
}
const getKey = (name) =>
typeof alias[name] === 'string' || alias[name] === false
? alias[name]
: name
const loadingKey = getKey('loading')
const successKey = getKey('success')
const errorKey = getKey('error')
const exceptionKey = getKey('exception')
return {
data() {
if (!Array.isArray(methods)) {
// 默认为所有方法注入属性
methods = Object.keys(this.$options.methods)
}
for (const method of methods) {
if (typeof this[method] === 'function') {
const fn = this[method]
let loadId = 0
const status = Vue.observable({})
loadingKey && Vue.set(status, loadingKey, false)
successKey && Vue.set(status, successKey, false)
errorKey && Vue.set(status, errorKey, false)
exceptionKey && Vue.set(status, exceptionKey, false)
const setStatus = (key, value) => key && (status[key] = value)
this[method] = (...args) => {
const currentId = ++loadId
setStatus(loadingKey, true)
setStatus(successKey, false)
setStatus(errorKey, false)
setStatus(exceptionKey, null)
try {
const result = fn.call(this, ...args)
if (result instanceof Promise) {
return result
.then((res) => {
if (loadId === currentId) {
setStatus(loadingKey, false)
setStatus(successKey, true)
}
return res
})
.catch((err) => {
if (loadId === currentId) {
setStatus(loadingKey, false)
setStatus(errorKey, true)
setStatus(exceptionKey, err)
}
throw err
})
}
setStatus(loadingKey, false)
setStatus(successKey, true)
return result
} catch (err) {
setStatus(loadingKey, false)
setStatus(errorKey, true)
setStatus(exceptionKey, err)
throw err
}
}
Object.keys(status).forEach((key) => {
Object.defineProperty(this[method], key, {
get() {
return status[key]
}
})
})
Object.setPrototypeOf(this[method], fn)
}
}
return {}
}
}
}

作者:cxy930123
来源:juejin.cn/post/7249724085147254845
收起阅读 »

移动端vh适配短屏幕手机,兼容一屏显示问题

web
rem适配 在日常的移动端开发中,设计稿一般为750物理像素,而我平时开发的时候,习惯以屏幕宽度为375,高度为724为标准( iPhone X 在微信内置浏览器的宽高) ,如下图所示: 该页面是使用 rem 进行适配,此时该图片宽度为 533px,正常我们...
继续阅读 »

rem适配


在日常的移动端开发中,设计稿一般为750物理像素,而我平时开发的时候,习惯以屏幕宽度为375,高度为724为标准( iPhone X 在微信内置浏览器的宽高) ,如下图所示:


1710350110240.png


该页面是使用 rem 进行适配,此时该图片宽度为 533px,正常我们需要设置其宽度为5.33rem ,当屏幕高度为724时,可以正常一屏显示完全。


 <body>
   <div class="content">其他内容</div>
   <img class="pic" src="./images/1.png" />
 </body>

 * {
   margin: 0;
   padding: 0;
 }
 body {
   background-color: skyblue;
 }
 .content {
   margin-top: 8rem
 }
 .pic {
  width: 5.33rem;
 }

短屏手机显示


而当我模拟短屏幕手机进行预览时,设置屏幕高度为667,此时屏幕宽度没有变化,那么根元素 htmlfont-size 也不会发生变化,那么造成的结果就是短屏幕手机上会出现滚动条,无法一屏显示。


1710350616617.png


但是需求是要求内容一屏能显示完全,此时 rem 适配已经没法做到了,在屏幕宽度不变,但是高度变化的情况下,这该怎么进行适配呢?


没错,这里我想到的是 vh 单位,不使用百分比是因为百分比适配是根据父级的宽高进行计算,而 vh 是根据整个屏幕的高度进行计算。


修改 css 如下所示:


 .content {
   margin-top: 55.249vh;
   /* margin-top: 8rem; */
 }
 .pic {
   /* width: 5.33rem; */
   width: auto;
   height: 28.66vh;
   max-height: 4.15rem;
 }

vh高度适配


利用 vh 对高度进行适配,但是这个 55.249vh28.66vh 是如何这算出来的呢?


首先我是基于 375*724 进行布局,在724的高度下,图片宽度 5.33rem,高度没设置,那就是使用了图片533px 时的高度,为 415px


1710351128534.png


724的高度下,图片高度使用了 415px,那么在屏幕上显示的应该是207.5px,那如果使用 415px 进行vh 换算,应该是 415 / (724x2) x 100,得出的结果约为28.66,这个就是对应的 vh 高度。


那么 55.249vh 同理,原来设置的 8rem,也就是相当于 800px,经过换算后得出结果。


而对图片设置 max-height 是为了不让图片一直随着高度变大得拉伸,以免造成图片变形。


此时在短屏幕手机上显示的效果如下图所示,当然 font-size 我这里没处理,有时候 font-size 也可以使用 vh 适配。


1710351669836.png


作者:一如彷徨
来源:juejin.cn/post/7345729950458724389
收起阅读 »

为什么可以通过process.env.NODE_ENV来区分环境

web
0.背景 通常我们在开发中需要区分当前代码的运行环境是dev、test、prod环境,以便我们进行相对应的项目配置,比如是否开启sourceMap,api地址切换等。而我们区分环境一般都是通过process.env.NODE_ENV,那么为什么process....
继续阅读 »

0.背景


通常我们在开发中需要区分当前代码的运行环境是dev、test、prod环境,以便我们进行相对应的项目配置,比如是否开启sourceMap,api地址切换等。而我们区分环境一般都是通过process.env.NODE_ENV,那么为什么process.env.NODE_ENV可以区分环境呢?是我们给他配置的,还是他可以自动识别呢?


1.什么是process.env.NODE_ENV


process.env属性返回一个包含用户环境信息的对象。


在node环境中,当我们打印process.env时,发现它并没有NODE_ENV这一个属性。实际上,process.env.NODE_ENV是在package.json的scripts命令中注入的,也就是NODE_ENV并不是node自带的,而是由用户定义的,至于为什么叫NODE_ENV,应该是约定成俗的吧。


2.通过package.json来设置node环境中的环境变量


如下为在package.json文件的script命令中设置一个变量NODE_ENV


{
"scripts": {
"dev": "NODE_ENV=development webpack --config webpack.dev.config.js"
}
}

执行对应的webpack.config.js文件


// webpack.config.js
console.log("【process.env】", process.env.AAA);

但是在index.jsx中也就是浏览器环境下的文件中打印process.env就会报错,如下:
image.png
可以看到NODE_ENV被赋值为development,当执行npm run dev时,我们就可以在 webpack.dev.config.js脚本中以及它所引入的脚本中访问到process.env.NODE_ENV,而无法在其它脚本中访问。原因就是前文提到的peocess.env是Node环境的属性,浏览器环境中index.js文件不能够获取到。


3.使用webpack.DefinePlugin插件在业务代码中注入环境变量


这个时候我们就存在一个解决方法,通过webpack中的DefinePlugin来设置一个全局变量,这样所有的打包的js文件都可以访问到这个全局变量了。


const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"development"'
})
]
}



使用DefinePlugin注意点
webpack.definePlugins本质上是打包过程中的字符串替换,比如我们刚才定义的__WEBPACK__ENV:JSON.stringify('packages')
在打包过程中,如果我们代码中使用到了__WEPBACK__ENVwebpack会将它的值替换成为对应definePlugins中定义的值,本质上就是匹配字符串替换,并不是传统意义上的环境变量process注入。

如下图所示:
image.png
由上图可知:仔细对比这两段代码第一个问题的答案其实已经很明了了,针对definePlugin这个插件我们使用它定义key:value全局变量时,他会将value进行会直接替换文本。所以我们通常使用JSON.stringify('pacakges')或者"'packages'"



作者:会飞的特洛伊
来源:juejin.cn/post/7345760019319390248
收起阅读 »

HTML常用基础标签:图片与超链接标签全解!

HTML图片标签和超链接标签是网页开发中常用的两种标签,它们分别用于在网页中插入图片和创建超链接。我们每天都在互联网世界中与各种形式的信息打交道。你是否好奇过,当你点击一篇文章中的图片或链接时,是什么神奇的力量让你瞬间跳转到另一个页面?今天,就让我们一起揭开H...
继续阅读 »

HTML图片标签和超链接标签是网页开发中常用的两种标签,它们分别用于在网页中插入图片和创建超链接。

我们每天都在互联网世界中与各种形式的信息打交道。你是否好奇过,当你点击一篇文章中的图片或链接时,是什么神奇的力量让你瞬间跳转到另一个页面?

今天,就让我们一起揭开HTML图片标签和超链接标签的神秘面纱。

一、HTML图片标签

HTML图片标签是一种特殊的标记,它可以让网页显示图像。通过使用图片标签,我们可以在网页上展示各种图片,从而让网页更加生动有趣。

Description

1、语法结构

HTML图片标签的语法结构非常简单,只需要使用标签,并在其中添加src属性,指定图片的路径即可。例如:

<img src="image.jpg" alt="描述图片的文字">

2、图片格式

HTML支持多种图片格式,包括JPEG、PNG、GIF等。不同的图片格式具有不同的特点,可以根据需要选择合适的格式。

3、图片属性

除了src属性外,HTML图片标签还有其他一些常用的属性,如:

  • alt属性用于描述图片的内容,当图片无法显示时,会显示该属性的值;
  • width和height属性用于设置图片的宽度和高度;
  • title属性用于设置鼠标悬停在图片上时显示的提示信息。

4、网络图片的插入

当需要插入网络上的图片时,可以将图片的URL地址作为src属性的值。例如:

<img src="https://www.example.com/images/pic.jpg" alt="示例图片">

5、本地图片的插入

当需要插入本地图片时,可以将图片的相对路径或绝对路径作为src属性的值。

6、相对路径与绝对路径

在这里再给大家介绍两个概念,相对路径与绝对路径,搞懂它们,我们在插入本地图片时也能得心应手。

Description

相对路径:
相对于当前HTML文件所在目录的路径,包含Web的相对路径(HTML中的相对目录)。例如,如果图片文件位于与HTML文件相同的目录中,可以直接使用文件名作为路径:

<img src="pic.jpg" alt="本地图片">

绝对路径:
图片文件在计算机上的完整路径(URL和物理路径)。例如:

<img src="C:/Users/username/Pictures/pic.jpg" alt="本地图片">

二、HTML超链接标签

超链接标签是HTML中另一个重要的元素,它可以实现网页之间的跳转。通过使用超链接标签,我们可以将文本、图片等内容设置为可点击的链接,方便用户在不同页面之间自由切换。

Description

1、语法结构

超链接标签使用<a>标签表示,需要在href属性中指定链接的目标地址。

<a href="目标地址" title="标题">文本内容</a>

例如:

<a href="https://www.ydcode.cn/">点击访问示例网站</a>

示例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>示例网站</title>
</head>
<body>
    <h1>欢迎来到云端源想!</h1>
    <p>这是一个简单的HTML页面,用于展示一个网站的结构和内容。</p>
    <a href="https://www.ydcode.cn/">点击访问示例网站</a>
</body>
</html>

Description

2、链接目标

超链接可以链接到不同的目标,包括其他网页、电子邮件地址、文件下载等。通过设置href属性的值,可以实现不同的链接目标。

3、链接属性

超链接标签还有一些其他常用的属性,如:

  • target属性用于设置链接打开的方式,可以选择在新窗口或当前窗口打开链接;
  • title属性用于设置鼠标悬停在链接上时显示的提示信息;
  • rel属性用于设置链接的关系,例如设置nofollow值可以告诉搜索引擎不要跟踪该链接。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

4、锚点链接标签

锚点标签用于在网页中创建一个可以点击的锚点,以便用户可以通过点击锚点跳转到页面中的其他部分。如下图中电子书的章节切换。

Description

锚点标签的语法为:

<a name="锚点名称"></a>

例如,可以在页面中的一个段落前添加一个锚点:

<a name="section1"></a>
<p>这是一个段落。</p>

然后,可以在页面的其他位置创建一个指向该锚点的超链接:

<a href="#section1">跳转到第一节</a>

当用户点击“跳转到第一节”链接时,页面将滚动到名为“section1”的锚点所在的位置。

示例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>示例网站</title>
</head>
<body>
    <h1>欢迎来到云端源想!</h1>
    <p><a href="#section1">跳转到第一节</a></p>
    <p>这是一个段落。</p>
    <p>这是另一个段落。</p>
    <p>这是第三个段落。</p>
    <a name="section1"></a>
    <p>这是第一节的内容。</p>
</body>
</html>

Description

三、总结

HTML图片标签和超链接标签是构建网页的两个重要元素,它们不仅丰富了网页的内容,还为网页添加了动态和互动性。

通过学习和掌握这两个标签的使用方法,我们可以创建更加丰富和互动的网页,为用户提供更好的浏览体验。无论是展示精美的图片,还是实现页面之间的跳转,HTML图片标签和超链接标签都能帮助我们实现更多的创意和功能。

让我们一起探索HTML的奇妙世界,创造出更加精彩的网页吧!

收起阅读 »

UNIAPP开发电视app教程

目前开发安卓TV的方法相对开说是十分的少的,使用uniapp开发相对来说几乎是没有的,因此,写下这篇文章供大家参考。 开发难点 如何方便的开发调试 如何使需要被聚焦的元素获取聚焦状态 如何使被聚焦的元素滚动到视图中心位置 如何在切换路由时,缓存聚焦的状态 如...
继续阅读 »

目前开发安卓TV的方法相对开说是十分的少的,使用uniapp开发相对来说几乎是没有的,因此,写下这篇文章供大家参考。


开发难点



  1. 如何方便的开发调试

  2. 如何使需要被聚焦的元素获取聚焦状态

  3. 如何使被聚焦的元素滚动到视图中心位置

  4. 如何在切换路由时,缓存聚焦的状态

  5. 如何启用wgt和apk两种方式的升级


一、如何方便的开发调试


之前我在论坛看到人家说,没办法呀,电脑搬到电视,然后调试。


其实大可不必,安装android studio里边创建一个模拟器就可以了。


注意:最好安装和电视系统相同的版本号,我这里是长虹电视,安卓9所以使用安卓9的sdk


二、如何使需要被聚焦的元素获取聚焦状态


uniapp的本质上是webview, 因此我们可以在它的元素上添加tabIndex, 就可以获取焦点了。


  <view class="card" tabindex="0">
<image :src="`${VITE_URL}${props.image}`" fade-show lazy-load mode="aspectFill"></image>
<view class="bottom">
<text class="name">{{ props.name }}</text> <text class="remark">{{ props.remark }}</text>
<div class="footer">
<view class="tags">
<text class="tag" v-for="tag in tags" :key="tag">{{ tag }}</text>
</view>
<text class="price">&yen; {{ props.price }}</text>
</div>
</view>
</view>


.card {
border-radius: 1.25vw;
overflow: hidden;
}
.card:focus {
box-shadow: 0 0 0 0.3vw #fff, 0 0 1vw 0.3vw #333;
outline: none;
transform: scale(1.03);
transition: box-shadow 0.3s ease, transform 0.3s ease;
}


三、如何使被聚焦的元素滚动到视图中心位置


使用renderjs进行实现如下


<script  module="homePage" lang="renderjs">
export default {
mounted() {
let isScrolling = false; // 添加一个标志位,表示是否正在滚动
document.body.addEventListener('focusin', e => {
if (!isScrolling) {
// 检查是否正在滚动
isScrolling = true; // 设置滚动标志为true
requestAnimationFrame(() => {
// @ts-ignore
e.target.scrollIntoView({
behavior: 'smooth', // @ts-ignore
block: e.target.dataset.index ? 'end' : 'center'
});
isScrolling = false; // 在滚动完成后设置滚动标志为false
});
}
});
}
};
</script>

就可以使被聚焦元素滚动到视图中心,requestAnimationFrame的作用是缓存


四、如何在切换路由时,缓存聚焦的状态


通过设置tabindex属性为0和1,会有不同的效果:



  1. tabindex="0":将元素设为可聚焦,并按照其在文档中的位置来确定焦点顺序。当使用Tab键进行键盘导航时,tabindex="0"的元素会按照它们在源代码中的顺序获取焦点。这可以用于将某些非交互性元素(如
    等)设为可聚焦元素,使其能够被键盘导航。

  2. tabindex="1":将元素设为可聚焦,并将其置于默认的焦点顺序之前。当使用Tab键进行键盘导航时,tabindex="1"的元素会在默认的焦点顺序之前获取焦点。这通常用于重置焦点顺序,或者将某些特定的元素(如重要的输入字段或操作按钮)置于首位。


需要注意的是,如果给多个元素都设置了tabindex属性,那么它们的焦点顺序将取决于它们的tabindex值,数值越小的元素将优先获取焦点。如果多个元素具有相同的tabindex值,则它们将按照它们在文档中的位置来确定焦点顺序。同时,负数的tabindex值也是有效的,它们将优先于零和正数值获取焦点。


我们要安装缓存插件,如pinia或vuex,需要缓存的页面单独配置


import { defineStore } from 'pinia';
export const useGlobalStore = defineStore('global', {
state: () => ({ home_active_tag: 'active0', hot_active_tag: 'hot0', dish_active_tag: 'dish0' })
});


更新一下业务代码


组件区域
<view class="card" :tabindex="home_active_tag === 'packagecard' + props.id ? 1 : 0">
<image :src="`${VITE_URL}${props.image}`" fade-show lazy-load mode="aspectFill"></image>
<view class="bottom">
<text class="name">{{ props.name }}</text> <text class="remark">{{ props.remark }}</text>
<div class="footer">
<view class="tags">
<text class="tag" v-for="tag in tags" :key="tag">{{ tag }}</text>
</view>
<text class="price">&yen; {{ props.price }}</text>
</div>
</view>

</view>

const { home_active_tag } = storeToRefs(useGlobalStore());

页面区域

<view class="content">
<FoodCard
v-for="_package in list.dishes"
@click="goShopByFood(_package)"
:id="_package.id"
:name="_package.name"
:image="_package.image"
:tags="_package.tags"
:price="_package.price"
:shop_name="_package.shop_name"
:shop_id="_package.shop_id"
:key="_package.id"
></FoodCard>
<image
class="card"
@click="goMore"
:tabindex="home_active_tag === 'more' ? 1 : 0"
style="width: 29.375vw; height: 25.9375vw"
src="/static/home/more.png"
mode="aspectFill"
/>
</view>

const goShopByFood = async (row: Record<string, any>) => {
useGlobalStore().home_active_tag = 'foodcard' + row.id;
uni.navigateTo({
url: `/pages/shop/index?shop_id=${row.shop_id}`,
animationDuration: 500,
animationType: 'zoom-fade-out'
});
};


如果,要设置启动默认焦点 id和index可默认设置,推荐启动第一个焦点组用index,它可以确定


  <view class="active">
<image
v-for="(active, i) in list.active"
:key="active.id"
@click="goActive(active, i)"
:tabindex="home_active_tag === 'active' + i ? 1 : 0"
:src="`${VITE_URL}${active.image}`"
data-index="0"
fade-show
lazy-load
mode="aspectFill"
class="card"
></image>
</view>

import { defineStore } from 'pinia';
export const useGlobalStore = defineStore('global', {
state: () => ({
home_active_tag: 'active0', //默认选择
hot_active_tag: 'hot0',
dish_active_tag: 'dish0'
})
});


对于多层级的,要注意销毁,在前往之前设置默认焦点


const goHot = (index: number) => {
useGlobalStore().home_active_tag = 'hotcard' + index;
useGlobalStore().hot_active_tag = 'hot0';
uni.navigateTo({ url: `/pages/hot/index?index=${index}`, animationDuration: 500, animationType: 'zoom-fade-out' });
};


五、如何启用wgt和apk两种方式的升级


pages.json


{
"path": "components/update/index",
"style": {
"disableScroll": true,
"backgroundColor": "#0068d0",
"app-plus": {
"backgroundColorTop": "transparent",
"background": "transparent",
"titleNView": false,
"scrollIndicator": false,
"popGesture": "none",
"animationType": "fade-in",
"animationDuration": 200
}
}
}


组件


<template>
<view class="update">
<view class="content">
<view class="content-top">
<text class="content-top-text">发现版本</text>
<image class="content-top" style="top: 0" width="100%" height="100%" src="@/static/bg_top.png"> </image>
</view>
<text class="message"> {{ message }} </text>
<view class="progress-box">
<progress
class="progress"
border-radius="35"
:percent="progress.progress"
activeColor="#3DA7FF"
show-info
stroke-width="10"
/>

<view class="progress-text">
<text>安装包正在下载,请稍后,系统会自动重启</text>
<text>{{ progress.totalBytesWritten }}MB/{{ progress.totalBytesExpectedToWrite }}MB</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app';
import { reactive, ref } from 'vue';
const message = ref('');
const progress = reactive({ progress: 0, totalBytesExpectedToWrite: '0', totalBytesWritten: '0' });
onLoad((query: any) => {
message.value = query.content;
const downloadTask = uni.downloadFile({
url: `${import.meta.env.VITE_URL}/${query.url}`,
success(downloadResult) {
plus.runtime.install(
downloadResult.tempFilePath,
{ force: false },
() => {
plus.runtime.restart();
},
e => {}
);
}
});
downloadTask.onProgressUpdate(res => {
progress.progress = res.progress;
progress.totalBytesExpectedToWrite = (res.totalBytesExpectedToWrite / Math.pow(1024, 2)).toFixed(2);
progress.totalBytesWritten = (res.totalBytesWritten / Math.pow(1024, 2)).toFixed(2);
});
});
</script>
<style lang="less">
page {
background: transparent;
.update {
/* #ifndef APP-NVUE */
display: flex; /* #endif */
justify-content: center;
align-items: center;
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.65);
.content {
position: relative;
top: 0;
width: 50vw;
height: 50vh;
background-color: #fff;
box-sizing: border-box;
padding: 0 50rpx;
font-family: Source Han Sans CN;
border-radius: 2vw;
.content-top {
position: absolute;
top: -5vw;
left: 0;
image {
width: 50vw;
height: 30vh;
}
.content-top-text {
width: 50vw;
top: 6.6vw;
left: 3vw;
font-size: 3.8vw;
font-weight: bold;
color: #f8f8fa;
position: absolute;
z-index: 1;
}
}
}
.message {
position: absolute;
top: 15vw;
font-size: 2.5vw;
}
.progress-box {
position: absolute;
width: 45vw;
top: 20vw;
.progress {
width: 90%;
border-radius: 35px;
}
.progress-text {
margin-top: 1vw;
font-size: 1.5vw;
}
}
}
}
</style>


App.vue


import { onLaunch } from '@dcloudio/uni-app';
import { useRequest } from './hooks/useRequest';
import dayjs from 'dayjs'; onLaunch(() => {
// #ifdef APP-PLUS
plus.runtime.getProperty('', async app => { const res: any = await useRequest('GET', '/api/tv/app'); if (res.code === 2000 && res.row.version > (app.version as string)) { uni.navigateTo({ url: `/components/update/index?url=${res.row.url}&type=${res.row.type}&content=${res.row.content}`, fail: err => { console.error('更新弹框跳转失败', err); } }); } });
// #endif
});

如果要获取启动参数


plus.android.importClass('android.content.Intent');
const MainActivity = plus.android.runtimeMainActivity();
const Intent = MainActivity.getIntent();
const roomCode = Intent.getStringExtra('roomCode');
if (roomCode) {
uni.setStorageSync('roomCode', roomCode);
} else if (!uni.getStorageSync('roomCode') && !roomCode) {
uni.setStorageSync('roomCode', '8888');
}

作者:Rjl_CLI
来源:juejin.cn/post/7272348543625445437
收起阅读 »

有效封装WebSocket,让你的代码更简洁!

web
前言 在现代 Web 应用中,实时通信已经成为越来越重要的一部分。而 WebSocket 技术的出现,使得实时通信变得更加高效和便捷。 WebSocket 协议是一种基于 TCP 协议的双向通信协议,它能够在客户端和服务器之间建立起持久性的连接,从而实现实时通...
继续阅读 »

前言


在现代 Web 应用中,实时通信已经成为越来越重要的一部分。而 WebSocket 技术的出现,使得实时通信变得更加高效和便捷。


WebSocket 协议是一种基于 TCP 协议的双向通信协议,它能够在客户端和服务器之间建立起持久性的连接,从而实现实时通信。


在前端开发中,为了更好地利用 WebSocket 技术,我们通常会对其进行封装,以便于全局调用并根据自己的业务做不同的预处理。


本文将介绍如何有效封装一个 WebSocket 供全局使用,并根据自己的业务做不同的预处理,实现更方便的调用,减少重复代码。


具体实现


我们将基于 Web API 提供的 WebSocket 类,封装一个 Socket 类,该类将提供以下功能:



  1. 建立 WebSocket 连接,并支持发送 query 参数。

  2. 发送、接收消息,支持对 WebSocket 的事件进行监听。

  3. 断开 WebSocket 连接。

  4. 支持心跳检测。

  5. 可以根据业务需要,对发送和接收的消息进行预处理。


下面是实现代码:


// socket.js
import modal from '@/plugins/modal'
const baseURL = import.meta.env.VITE_APP_BASE_WS;
const EventTypes = ['open', 'close', 'message', 'error', 'reconnect'];
const DEFAULT_CHECK_TIME = 55 * 1000; // 心跳检测的默认时间
const DEFAULT_CHECK_COUNT = 3; // 心跳检测默认失败重连次数
const DEFAULT_CHECK_DATA = { Type: 1, Parameters: ['alive'] }; // 心跳检测的默认参数 - 跟后端协商的
const CLOSE_ABNORMAL = 1006; // WebSocket非正常关闭code码

class EventMap {
deps = new Map();
depend(eventType, callback) {
this.deps.set(eventType, callback);
}
notify(eventType, event) {
if (this.deps.has(eventType)) {
this.deps.get(eventType)(event);
}
}
}

class Socket extends WebSocket {
heartCheckData = DEFAULT_CHECK_DATA;
heartCheckTimeout = DEFAULT_CHECK_TIME;
heartCheckInterval = null;
heartCheckCount = DEFAULT_CHECK_COUNT
constructor(options, dep, reconnectCount = 0) {
let _baseURL = baseURL
const { url, protocols, query = {}, greet = null, customBase = null } = options;
const _queryParams = Object.keys(query).reduce((str, key) => {
if (typeof query[key] !== 'object' && typeof query[key] !== 'function') {
return str += str.length > 0 ? `&${key}=${query[key]}` : `${key}=${query[key]}`;
} else {
return str;
}
}, '');
if (customBase) {
_baseURL = customBase
}
super(`${_baseURL}${url}?${_queryParams}`, protocols);
this._currentOptions = options;
this._dep = dep;
this._reconnectCount = reconnectCount;
greet && Object.assign(this, {
heartCheckData: greet
})
this.initSocket();
}

// 初始化WebSocket
initSocket() {
// 监听webSocket的事件
this.onopen = function (e) {
this._dep.notify('open', e);
this.heartCheckStart();
}
this.onclose = function (e) {
this._dep.notify('close', e);
// 如果WebSocket是非正常关闭 则进行重连
if (e.code === CLOSE_ABNORMAL) {
if (this._reconnectCount < this.heartCheckCount) {
this._reconnectCount++;
const _socket = new Socket(this._currentOptions, this._dep, this._reconnectCount);
this._dep.notify('reconnect', _socket);
} else {
return modal.msgError('WebSocket重连失败, 请联系技术客服!');
}
}
}
this.onerror = function (e) {
this._dep.notify('error', e);
}
this.onmessage = function (e) {
// 如果后端返回的是二进制数据
if (e.data instanceof Blob) {
const reader = new FileReader()
reader.readAsArrayBuffer(e.data)
reader.onload = (ev) => {
if (ev.target.readyState === FileReader.DONE) {
this._dep.notify('message', ev.target?.result);
}
}
} else {
// 处理普通数据
try {
const _parseData = JSON.parse(e.data);
this._dep.notify('message', _parseData);
} catch (error) {
console.log(error)
}
}
}

}

// 订阅事件
subscribe(eventType, callback) {
if (typeof callback !== 'function') throw new Error('The second param is must be a function');
if (!EventTypes.includes(eventType)) throw new Error('The first param is not supported');
this._dep.depend(eventType, callback);
}

// 发送消息
sendMessage(data, options = {}) {
const { transformJSON = true } = options;
let result = data;
if (transformJSON) {
result = JSON.stringify(data);
}
this.send(result);
}

// 关闭WebSocket
closeSocket(code, reason) {
this.close(code, reason);
}

// 开始心跳检测
heartCheckStart() {
this.heartCheckInterval = setInterval(() => {
if (this.readyState === this.OPEN) {
let transformJSON = typeof this.heartCheckData === 'object'
this.sendMessage(this.heartCheckData, { transformJSON });
} else {
this.clearHeartCheck();
}
}, this.heartCheckTimeout)
}

// 清除心跳检测
clearHeartCheck() {
clearInterval(this.heartCheckInterval);
}

// 重置心跳检测
resetHeartCheck() {
clearInterval(this.heartCheckInterval);
this.heartCheckStart();
}
}
// 默认的配置项
const defaultOptions = {
url: '',
protocols: '',
query: {},
}

export const useSocket = (options = defaultOptions) => {
if (!window.WebSocket) return modal.msgWarning('您的浏览器不支持WebSocket, 请更换浏览器!');
const dep = new EventMap();
const reconnectCount = 0;
return new Socket(options, dep, reconnectCount);
}

接下来我们从实际使用的角度解释一下上面的代码,首先我们暴露了一个 useSocket 函数,该函数接收一个 options 配置项参数,支持的参数有:



  • url:要连接的 WebSocket URL;

  • protocols:一个协议字符串或者一个包含协议字符串的数组;

  • query:可以通过 URL 传递给后端的查询参数;

  • greet:心跳检测的打招呼信息;

  • customBase:自定义的 baseURL ,否则默认使用环境变量中定义的 env.VITE_APP_BASE_WS


在调用该函数后,我们首先会判断当前用户的浏览器是否支持 WebSocket,如果不支持给予用户提示。


然后我们实例化了一个 EventMap 类的实例对象 dep,你可以把它当作是一个依赖收集桶,当用户订阅了某个 WebSocket 事件时,我们将收集这个事件对应的回调作为依赖,在事件触发时,再通知该依赖,然后调用该事件对应的回调函数。


接下来我们定义了一个初始的重连次数记录值 reconnectCount 为 0,每当这个 WebSocket 重连时,该值会自增。


之后我们实例化了自己封装的 Socket 类,并传入了我们上面的三个参数。
Socket 类的构造函数 constructor 中,我们先取出配置项,把 query 内的参数拼接在 URL 上,然后使用 super 调用父类的构造函数进行建立 WebSocket 连接。


之后我们缓存了当前 Socket 实例化时的参数,再调用 initSocket() 方法去进行 WebSocket 事件的监听:



  • onopen:触发 depopen 对应的回调函数并且打开心跳检测;

  • onclose:触发 depclose 对应的回调函数并且对关闭的 code 码进行判断,如果是非正常关闭连接,将会进行重连,如果重连次数达到阈值,则通知给用户;

  • onerror:触发 deperror 对应的回调函数;

  • onmessage:接收到服务端返回的数据,可以先根据自身业务做一些预处理,比如我就根据不同的数据类型进行了数据解析的预处理,之后再触发 depmessage 对应的回调函数并传入处理过后的数据。


我们也暴露了一些成员方法以供实例对象使用:



  • subscribe:订阅 WebSocket 事件,传入事件类型并须是 EventTypes 内的类型之一,第二个参数则是回调函数;

  • sendMessage:同样的,我们在给服务端发送数据之前也可以根据自身业务做一些预处理,比如我将需要转成 JSON 的数据,在这里统一转换后再发送给服务端;

  • closeSocket:关闭 WebSocket 连接;

  • heartCheckStart:开始心跳检测,会创建一个定时器,在一定时间之后(默认是55s)给服务端发送信息确认连接是否正常;

  • clearHeartCheck:清除心跳检测定时器(如果当前 WebSocket 连接已经关闭,则自动清除);

  • resetHeartCheck:重置心跳检测定时器。


如何使用


让我们看下如何使用这个封装好的 useSocket 函数,以在 Vue3中使用为例:


// xx.jsx or xx.vue
import { useSocket } from './socket.js'
const socket = ref(null) // WebSocket实例
const initWebSocket = () => {
const options = {
url: '/<your url>',
query: {
// something params
},
}
socket.value = useSocket(options)
socket.value.subscribe('open', () => {
console.log('WebSocket连接成功!')
const greet = 'hello'
// 发送打招呼消息
socket.value.sendMessage(greet)
})
socket.value.subscribe('close', reason => {
console.log('WebSocket连接关闭!', reason)
})
socket.value.subscribe('message', result => {
console.log('WebSocket接收到消息:', result)
})
socket.value.subscribe('error', err => {
console.log('WebSocket捕获错误:', err)
})
socket.value.subscribe('reconnect', _socket => {
console.log('WebSocket断开重连:', _socket)
socket.value = _socket
})
}
initWebSocket()

最后,如果想 debug 我们的心跳检测是否有效,可以使用下面这段代码:


// 测试心跳检测重连 手动模拟断开的情况
if (this._reconnectCount > 0) return;
const tempTimer = setInterval(() => {
this.close();
if (this._reconnectCount < 3) {
console.log('重连');
this._reconnectCount++;
const _socket = new Socket(this._currentOptions, this._dep, this._reconnectCount);
this._dep.notify('reconnect', _socket);
} else {
return clearInterval(tempTimer);
}
}, 3 * 1000)

initSocket() 方法中的 this.onopen 事件的回调函数内的最后添加上面这段代码即可。


总结


至此,我们实现了一个 WebSocket 类的封装,提供了连接、断开、消息发送、接收和心跳检测等功能,并可以根据业务需要对消息进行预处理。同时,我们还介绍了如何使用封装好的 useSocket 函数。


WebSocket 封装的好处在于可以让我们在全局范围内方便地使用 WebSocket,提高代码的可读性和可维护性,降低代码的复杂度和重复性。在实际开发过程中,我们可以结合自己的业务需求,对封装的 WebSocket 类进行扩展和优化,以达到更好的效果。


尽管我在文中尽可能地详细介绍了每一个步骤和细节,但是难免会存在一些错误和不足之处。如果您在使用本文中介绍的方法时发现了任何错误或者有更好的方法,非常欢迎您指正并提出建议,以便我能够不断改进和提升文章的质量。


我是荼锦,一个兴趣使然的开发者。非常感谢您阅读本文,希望本文对您有所帮助!


作者:荼锦
来源:juejin.cn/post/7231481633671757861
收起阅读 »

吾辈楷模!国人开源的Redis客户端被Redis官方收购了!

不久前开源圈子里的一则消息在网上引起了一阵关注和讨论。 一个由国人开发者所打造的开源项目被 Redis 公司官方给收购了,作者自己也发了动态,表示感谢项目9年以来的陪伴,同时也希望她未来一切都好。 这个开源项目的名字叫做:ioredis,相信不少小伙伴也用过...
继续阅读 »

不久前开源圈子里的一则消息在网上引起了一阵关注和讨论。


一个由国人开发者所打造的开源项目被 Redis 公司官方给收购了,作者自己也发了动态,表示感谢项目9年以来的陪伴,同时也希望她未来一切都好。



这个开源项目的名字叫做:ioredis,相信不少小伙伴也用过。



目前在GitHub上我们可以看到,ioredis项目的开源地址已经被迁移至 Redis 官方旗下了。



iosredis是国人开发者所打造的一个Redis客户端,基于TypeScript所编写,以健壮性、高性能以及功能强大为特色,并且被很多大公司所使用。



截止到目前,该项目在GitHub上已累计获得超过 13000 个 Star标星和 1000+ Fork。


作者自己曾表示,自己创建这个开源项目的初衷也很简单,那就是当年在这方面并没有找到一个令自己满意的开源库,于是决定自己动手来打造一个,于是就利用闲暇时间,自己从零开发并开源了 ioredis 。


直到2022 年 8 月 30 日,历时整整7年,ioredis 成为了 Node.js 最流行的 Redis 客户端。



而直到如今,这个项目从个人的 side project 到被开源公司官方收购,作者9 年的坚持属实令人佩服,吾辈楷模啊!


而拜访了这位开发者的GitHub后我们会发现,作者非常热衷于创造工具,除了刚被收购的名作ioredis之外,主页还有非常多的开源项目,并且关注量都不低。



而且从作者发的一些动态来看,这也是一个热爱生活的有趣灵魂。



有一说一,个人开源作者真的挺不容易的,像上面这样的个人开源项目被官方收购的毕竟是个例,其实好多个人的开源项目到后期由于各种主客观原因,渐渐都停止更新和维护了。


大家都知道,伴随着这两年互联网行业的寒意,软件产业里的不少环节也受到了波动。行业不景气,连开源项目的主动维护也变得越来越少了。


毕竟连企业也要降本增效,而开源往往并不能带来快速直接的实际效益。付出了如果没有回报,便会很难坚持下去。


而对于一名学习者而言,参与开源项目的意义是不言而喻的,之前咱们这里也曾多次提及。


参与开源项目除了可以提升自身技术能力,收获项目开发经验之外,还可以让自己保持与开源社区其他优秀开发者之间的联系与沟通,并建立自己的技术影响力,另外参与优秀开源项目的经历也会成为自己求职简历上的一大亮点。


所以如果精力允许,利用业余时间来参与或维护一些开源项目,这对技术开发者来说,也是一段难得的经历!


作者:CodeSheep
来源:juejin.cn/post/7345746216150876198
收起阅读 »

一个高并发项目到落地的心酸路

前言 最近闲来没事,一直在掘金上摸鱼,看了不少高并发相关的文章,突然有感而发,想到了几年前做的一个项目,也多少和高并发有点关系。 这里我一边回忆落地细节一边和大家分享下,兴许能给大家带来点灵感。 正文 需求及背景 先来介绍下需求,首先项目是一个志愿填报系统,既...
继续阅读 »

前言


最近闲来没事,一直在掘金上摸鱼,看了不少高并发相关的文章,突然有感而发,想到了几年前做的一个项目,也多少和高并发有点关系。

这里我一边回忆落地细节一边和大家分享下,兴许能给大家带来点灵感。


正文


需求及背景


先来介绍下需求,首先项目是一个志愿填报系统,既然会扯上高并发,相信大家也能猜到大致是什么的志愿填报。

核心功能是两块,一是给考试填报志愿,二是给老师维护考生数据。

本来这个项目不是我们负责,奈何去年公司负责这个项目的组遭到了甲方严重的投诉,说很多考生用起来卡顿,甚至把没填上志愿的责任归到系统上。

甲方明确要求,如果这年再出现这种情况,公司在该省的所有项目将面临被替换的风险。

讨论来讨论去,最后公司将任务落到我们头上时,已经是几个月后的事了,到临危受命阶段,剩下不到半年时间。

虽然直属领导让我们不要有心理负担,做好了表扬,做不好锅也不是我们的,但明显感觉到得到他的压力,毕竟一个不小心就能上新闻。


分析


既然开始做了,再说那些有的没的就没用了,直接开始分析需求。

首先,业务逻辑并不算复杂,难点是在并发和数据准确性上。与客户沟通后,大致了解了并发要求后,于是梳理了下。



  1. 考生端登录接口、考生志愿信息查询接口需要4W QPS

  2. 考生保存志愿接口,需要2W TPS

  3. 报考信息查询4W QPS

  4. 老师端需要4k QPS

  5. 导入等接口没限制,可以异步处理,只要保证将全部信息更新一遍在20分钟以内即可,同时故障恢复的时间必须在20分钟以内(硬性要求)

  6. 考生端数据要求绝对精准,不能出现遗漏、错误等和考生操作不一致的数据

  7. 数据脱敏,防伪

  8. 资源是有限的,提供几台物理机

    大的要求就这么多,主要是在有限资源下需要达到如此高的并发确实需要思考思考,一般的crud根本达不到要求。


方案研讨


接下来我会从当时我们切入问题的点开始,从前期设计到项目落地整个过程的问题及思考,一步步去展示这个项目如何实现的

首先,我们没有去设计表,没有去设计接口,而是先去测试。测试什么?测试我们需要用到或可能用到的中间件是否满足需求


MySQL


首先是MySQL,单节点MySQL测试它的读和取性能,新建一张user表。

向里面并发插入数据和查询数据,得到的TPS大概在5k,QPS大概在1.2W。

查询的时候是带id查询,索引列的查询不及id查询,差距大概在1k。

insert和update存在细微并发差距,但基本可以忽略,影响更新性能目前最大的问题是索引。

如果表中带索引,将降低1k-1.5k的TPS。

目前结论是,mysql不能达到要求,能不能考虑其他架构,比如mysql主从复制,写和读分开。

测试后,还是放弃,主从复制结构会影响更新,大概下降几百,而且单写的TPS也不能达到要求。

至此结论是,mysql直接上的方案肯定是不可行的


Redis


既然MySQL直接查询和写入不满足要求,自然而然想到加入redis缓存。于是开始测试缓存,也从单节点redis开始测试。

get指令QPS达到了惊人的10w,set指令TPS也有8W,意料之中也惊喜了下,仿佛看到了曙光。

但是,redis容易丢失数据,需要考虑高可用方案


实现方案


既然redis满足要求,那么数据全从redis取,持久化仍然交给mysql,写库的时候先发消息,再异步写入数据库。

最后大体就是redis + rocketMQ + mysql的方案。看上去似乎挺简单,当时我们也这样以为 ,但是实际情况却是,我们过于天真了。

这里主要以最重要也是要求最高的保存志愿信息接口开始攻略


故障恢复

第一个想到的是,这些个节点挂了怎么办?

mysql挂了比较简单,他自己的机制就决定了他即使挂掉,重启后仍能恢复数据,这个可以不考虑。

rocketMQ一般情况下挂掉了可能会丢失数据,经过测试发现,在高并发下,确实存在丢消息的现象。

原因是它为了更加高效,默认采用的是异步落盘的模式,这里为了保证消息的绝对不丢失,修改成同步落盘模式。

然后是最关键的redis,不管哪种模式,redis在高并发下挂掉,都会存在丢失数据的风险。

数据丢失对于这个项目格外致命,优先级甚至高于并发的要求。

于是,问题难点来到了如何保证redis数据正确,讨论过后,决定开启redis事务。

保存接口的流程就变成了以下步骤:

1.redis 开启事务,更新redis数据

2.rocketMQ同步落盘

3.redis 提交事务

4.mysql异步入库

我们来看下这个接口可能存在的问题。

第一步,如果redis开始事务或更新redis数据失败,页面报错,对于数据正确性没有影响

第二步,如果rocketMQ落盘报错,那么就会有两种情况。

情况一,落盘失败,消息发送失败,好像没什么影响,直接报错就可。

情况二,如果发送消息成功,但提示发送失败(无论什么原因),这时候将导致mysql和redis数据的最终不一致。

如何处理?怎么知道是redis的有问题还是mysql的有问题?出现这种情况时,如果考生不继续操作,那么这条错误的数据必定无法被更新正确。

考虑到这个问题,我们决定引入一个时间戳字段,同时启动一个定时任务,比较mysql和redis不一致的情况,并自主修复数据。

首先,redis中记录时间戳,同时在消息中也带上这个时间戳并在入库时记录到表中。

然后,定时任务30分钟执行一次,比较redis中的时间戳是否小于mysql,如果小于,便更新redis中数据。如果大于,则不做处理。

同时,这里再做一层优化,凌晨的时候执行一个定时任务,比较redis中时间戳大于mysql中的时间戳,连续两天这条数据都存在且没有更新操作,将提示给我们手动运维。

然后是第三步,消息提交成功但是redis事务提交失败,和第二步处理结果一致,将被第二个定时任务处理。

这样看下来,即使redis崩掉,也不会丢失数据。


第一轮压测


接口实现后,当时怀着期待,信息满满的去做了压测,结果也是当头棒喝。

首先,数据准确性确实没有问题,不管突然kill掉哪个环节,都能保证数据最终一致性。

但是,TPS却只有4k不到的样子,难道是节点少了?

于是多加了几个节点,但是仍然没有什么起色。问题还是想简单了。


重新分析


经过这次压测,之后一个关键的问题被提了出来,影响接口TPS的到底是什么???

一番讨论过后,第一个结论是:一个接口的响应时间,取决于它最慢的响应时间累加,我们需要知道,这个接口到底慢在哪一步或哪几步?

于是用arthas看了看到底慢在哪里?

结果却是,最慢的竟然是redis修改数据这一步!这和测试的时候完全不一样。于是针对这一步,我们又继续深入探讨。

结论是:

redis本身是一个很优秀的中间件,并发也确实可以,选型时的测试没有问题。

问题出在IO上,我们是将考生的信息用json字符串存储到redis中的(为什么不保存成其他数据结构,因为我们提前测试过几种可用的数据结构,发现redis保存json字符串这种性能是最高的),

而考生数据虽然单条大小不算大,但是在高并发下的上行带宽却是被打满的。

于是针对这种情况,我们在保存到redis前,用gzip压缩字符串后保存到redis中。

为什么使用gzip压缩方式,因为我们的志愿信息是一个数组,很多重复的数据其实都是字段名称,gzip和其他几个压缩算法比较后,综合考虑到压缩率和性能,在当时选择了这种压缩算法。

针对超过限制的字符串,我们同时会将其拆封成多个(实际没有超过三个的)key存储。


继续压测


又一轮压测下来,效果很不错,TPS从4k来到了8k。不错不错,但是远远不够啊,目标2W,还没到它的一半。

节点不够?加了几个节点,有效果,但不多,最终过不了1W。

继续深入分析,它慢在哪?最后发现卡在了rocketMQ同步落盘上。

同步落盘效率太低?于是压测一波发现,确实如此。

因为同步落盘无论怎么走,都会卡在rocketMQ写磁盘的地方,而且因为前面已经对字符串压缩,也没有带宽问题。

问题到这突然停滞,不知道怎么处理rocketMQ这个点。

同时,另一个同事在测试查询接口时也带来了噩耗,查询接口在1W2左右的地方就上不去了,原因还是卡在带宽上,即使压缩了字符串,带宽仍被打满。

怎么办?考虑许久,最后决定采用较常规的处理方式,那就是数据分区,既然单个rocketMQ服务性能不达标,那么就水平扩展,多增加几个rocketMQ。

不同考生访问的MQ不一样,同时redis也可以数据分区,幸运的是正好redis有哈希槽的架构支持这种方式。

而剩下的问题就是如何解决考生分区的方式,开始考虑的是根据id进行求余的分区,但后来发现这种分区方式数据分布及其不均匀。

后来稍作改变,根据正件号后几位取余分区,数据分布才较为均匀。有了大体解决思路,一顿操作后继续开始压测。


一点小意外


压测之后,结果再次不如人意,TPS和QPS双双不增反降,继续通过arthas排查。

最后发现,redis哈希槽访问时会在主节点先计算key的槽位,而后再将请求转到对应的节点上访问,这个计算过程竟然让性能下降了20%-30%。

于是重新修改代码,在java内存中先计算出哈希槽位,再直接访问对应槽位的redis。如此重新压测,QPS达到了惊人的2W,TPS也有1W2左右。

不错不错,但是也只到了2W,在想上去,又有了瓶颈。

不过这次有了不少经验,马上便发现了问题所在,问题来到了nginx,仍然是一样的问题,带宽!

既然知道原因,解决起来也比较方便,我们将唯一有大带宽的物理机上放上两个节点nginx,通过vip代理出去,访问时会根据考生分区信息访问不同的地址。


压测


已经记不清第几轮压测了,不过这次的结果还算满意,主要查询接口QPS已经来到了惊人的4W,甚至个别接口来到6W甚至更高。

胜利已经在眼前,唯一的问题是,TPS上去不了,最高1W4就跑不动了。

什么原因呢?查了每台redis主要性能指标,发现并没有达到redis的性能瓶颈(上行带宽在65%,cpu使用率也只有50%左右)。

MQ呢?MQ也是一样的情况,那出问题的大概率就是java服务了。分析一波后发现,cpu基本跑到了100%,原来每个节点的最大链接数基本占满,但带宽竟然还有剩余。

静下心来继续深入探讨,连接数为什么会满了?原因是当时使用的SpringBoot的内置容器tomcat,无论如何配置,最大连接数最大同时也就支持1k多点。

那么很简单的公式就能出来,如果一次请求的响应时间在100ms,那么1000 * 1000 / 100 = 10000。

也就是说单节点最大支持的并发也就1W,而现在我们保存的接口响应时间却有300ms,那么最大并发也就是3k多,目前4个分区,看来1W4这个TPS也好像找到了出处了。

接下来就是优化接口响应时间的环节,基本是一步一步走,把能优化的都优化了一遍,最后总算把响应时间控制在了100ms以内。

那么照理来说,现在的TPS应该会来到惊人的4W才对。


再再次压测


怀着忐忑又激动的心情,再一次进入压测环节,于是,TPS竟然来到了惊人的2W5。

当时真心激动了一把,但是冷静之后却也奇怪,按正常逻辑,这里的TPS应该能达到3W6才对。

为了找到哪里还有未发现的坑(怕上线后来惊喜),我们又进一步做了分析,最后在日志上找到了些许端倪。

个别请求在链接redis时报了链接超时,存在0.01%的接口响应时间高于平均值。

于是我们将目光投向了redis连接数上,继续一轮监控,最终在业务实现上找到了答案。

一次保存志愿的接口需要执行5次redis操作,分别是获取锁、获取考生信息、获取志愿信息、修改志愿信息、删除锁,同时还有redis的事务。

而与之相比,查询接口只处理了两次操作,所以对于一次保存志愿的操作来看,单节点的redis最多支持6k多的并发。

为了验证这个观点,我们尝试将redis事务和加锁操作去掉,做对照组压测,发现并发确实如预期的一样有所提升(其实还担心一点,就是抢锁超时)。


准备收工


至此,好像项目的高并发需求都已完成,其他的就是完善完善细节即可。

于是又又又一次迎来了压测,这一次不负众望,重要的两个接口均达到了预期。

这之后便开始真正进入业务实现环节,待整个功能完成,在历时一个半月带两周的加班后,终于迎来了提测。


提测后的问题


功能提测后,第一个问题又又又出现在了redis,当高并发下突然kill掉redis其中一个节点。

因为用的是哈希槽的方式,如果挂掉一个节点,在恢复时重新算槽将非常麻烦且效率很低,如果不恢复,那么将严重影响并发。

于是经过讨论之后,决定将redis也进行手动分区,分区逻辑与MQ的一致。

但是如此做,对管理端就带来了一定影响,因为管理端是列表查询,所以管理端获取数据需要从多个节点的redis中同时获取。

于是管理端单独写了一套获取数据分区的调度逻辑。

第二个问题是管理端接口的性能问题,虽然管理端的要求没考生端高,但扛不住他是分页啊,一次查10个,而且还需要拼接各种数据。

不过有了前面的经验,很快就知道问题出在了哪里,关键还是redis的连接数上,为了降低链接数,这里采用了pipeline拼接多个指令。


上线


一切准备就绪后,就准备开始上线。说一下应用布置情况,8+4+1+2个节点的java服务,其中8个节点考生端,4个管理端,1个定时任务,2个消费者服务。

3个ng,4个考生端,1个管理端。

4个RocketMQ。

4个redis。

2个mysql服务,一主一从,一个定时任务服务。

1个ES服务。

最后顺利上线,虽然发生了个别线上问题,但总体有惊无险,

而真是反馈的并发数也远没有到达我们的系统极限,开始准备的水平扩展方案也没有用上,无数次预演过各个节点的宕机和增加分区,一般在10分钟内恢复系统,不过好在没有排上用场。


最后


整个项目做下来感觉越来越偏离面试中的高并发模式,说实在的也是无赖之举,

偏离的主要原因我认为是项目对数据准确性的要求更高,同时需要完成高并发的要求。

但是经过这个项目的洗礼,在其中也收获颇丰,懂得了去监控服务性能指标,然后也加深了中间件和各种技术的理解。

做完之后虽然累,但也很开心,毕竟在有限的资源下去分析性能瓶颈并完成项目要求后,还是挺有成就感的。

再说点题外话,虽然项目成功挽回了公司在该省的形象,也受到了总公司和领导表扬,但最后也就这样了,

实质性的东西一点没有,这也是我离开这家公司的主要原由。不过事后回想,这段经历确实让人难忘,也给我后来的工作带来了很大的帮助。

从以前的crud,变得能去解决接口性能问题。这之前一遇上,可能两眼茫然或是碰运气,现在慢慢的会根据蛛丝马迹去探究优化方案。

不知道我在这个项目的经历是否能引起大家共鸣?希望这篇文章能对你有所帮助。


作者:青鸟218
来源:juejin.cn/post/7346021356679675967
收起阅读 »

在 vite 工程化中手动分包

web
项目搭建我们使用 vite 搭建一个 vue3 工程,执行命令:pnpm create vite vue3-demo --template vue-ts 安装 lodash 依赖包,下载依赖:pnpm...
继续阅读 »

项目搭建

  1. 我们使用 vite 搭建一个 vue3 工程,执行命令:
pnpm create vite vue3-demo --template vue-ts
  1. 安装 lodash 依赖包,下载依赖:
pnpm add lodash

pnpm install
  1. 完成后的工程目录结构是这样的: 


业务场景

我们先首次构建打包,然后修改一下代码再打包,对比一下前后打包差异:


可以看到,代码改动后,index-[hash].js 的文件指纹发生了变化,这意味着每次打包后,用户就要重新下载新的 js,而这个文件里面包含了这些东西:vuelodash业务代码,其中像 vuelodash 这些依赖包是固定不变的,有变动的只是我们的业务代码,基于这个点我们就可以在其基础上打包优化。

打包优化

我们需要在打包上优化两个点:

  1. 把第三方依赖库单独打包成一个 js 文件
  2. 把我们的业务代码单独打包成一个 js 文件

这块需要我们对 vite 工程化知识有一定的了解,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源,可以通过配置 build.rollupOptions.output.manualChunks 来自定义 chunk 分割策略。

更改 vite 配置

  1. 打开 vite.config.ts,加入配置项:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor':['vue', 'lodash'], // 这里可以自己自定义打包名字和需要打包的第三方库
}
}
}
}
})
  1. 执行打包命令,我们可以看到打包文件中多了一个 verdor-[hash].js 的文件,这个就是刚才配置分包的文件: 


  1. 这样的好处就是,将来如果我们的业务代码有改动,打包的第三方库的文件指纹就不会变,用户就会直接读取浏览器缓存,这是一种比较优的解决办法: 


  1. 但这样需要我们每次都手动填写第三方库,那也显得太呆了,我们可以把 manualChunks 配置成一个函数,每次去加载这个模块的时候,它就会运行这个函数,打印看下输出什么: 


  1. 我们会发现依赖包都是在 node_modules 目录下,接下来我们就修改一下配置:
 import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor'
}
}
}
}
}
})
  1. 我们再看下打包结果: 


总结

分包(Code Splitting)是一种将应用程序代码拆分为不同的块或包的技术,从而在需要时按需加载这些包。这种技术带来了许多优势,特别是在构建大型单页应用(Single Page Application,SPA)时。

  • 减小初始加载时间: 将应用程序分成多个小块,可以减少初始加载时需要下载的数据量。这意味着用户可以更快地看到初始内容,提高了用户体验。
  • 优化资源利用: 分包可以根据用户的操作行为和需要进行按需加载。这样,在用户访问特定页面或功能时,只会加载与之相关的代码,而不会加载整个应用程序的所有代码,从而减少了不必要的资源消耗。
  • 并行加载: 分包允许浏览器并行加载多个包。这可以加速页面加载,因为浏览器可以同时请求多个资源,而不是等待一个大文件下载完成。
  • 缓存优化: 分包使得缓存管理更加灵活。如果应用程序的一部分发生变化,只需重新加载受影响的分包,而不必重新加载整个应用程序。
  • 减少内存占用: 当用户访问某个页面时,只有该页面所需的代码被加载和执行。这有助于减少浏览器内存的使用,尤其是在应用程序变得复杂时。
  • 按需更新: 当应用程序的某个部分需要更新时,只需要重新发布相应的分包,而不必重新发布整个应用程序。这可以减少发布和部署的复杂性。
  • 代码复用和维护: 分包可以按功能或模块来划分,从而鼓励代码的模块化和复用。这样,不同页面之间可以共享相同的代码块,减少了重复编写和维护代码的工作量。


作者:白雾茫茫丶
来源:juejin.cn/post/7346031272919072779
收起阅读 »

H5推送,为什么都用WebSocket?

web
       大家好,我是石头~        最近大盘在3000点附近磨蹭,我也随大众去网上浏览了下感兴趣的几只股票,看下行情怎样。        看了一会,还是垃圾行情,不咋地,不过看着页面上的那些实时刷新分时图和五档行情,倒是想起公司以前就因为这个实时数...
继续阅读 »

       大家好,我是石头~


       最近大盘在3000点附近磨蹭,我也随大众去网上浏览了下感兴趣的几只股票,看下行情怎样。
       看了一会,还是垃圾行情,不咋地,不过看着页面上的那些实时刷新分时图和五档行情,倒是想起公司以前就因为这个实时数据刷新的问题,差点引起一次生产事故。


HTTP轮询差点导致生产事故


       那是一个给用户展示实时数据的需求,产品的要求是用户数据发生变动,需要在30秒内给客户展示出来。


       当时由于数据展示的页面入口较深,负责的后端开发就让H5通过轮询调用的方式来实现数据刷新。


       然而,由于客户端开发的失误,将此页面在APP打开时就进行了初始化,导致数据请求量暴涨,服务端压力大增,差点就把服务端打爆了。


fa7049166c79454eb87f3890d1aa6f4b.webp


H5推送,应该用什么?


       既然用HTTP做实时数据刷新有风险,那么,应该用什么方式来实现?


       一般要实现服务端推送,都需要用到长连接,而能够做到长连接的只有WebSocket、UDP和TCP,而且,WebSocket是在TCP之上构建的一种高级应用层协议。大家觉得我们应该用哪一种?


       其实,大家只要网上查一下,基本都会被推荐使用WebSocket,那么,为什么要用WebSocket?


u=2157318451,827303453&fm=253&fmt=auto&app=138&f=JPEG.webp


为什么要用WebSocket?


       这个我们可以从以下几个方面来看:



  • 易用性与兼容性:WebSocket兼容现代浏览器(HTML5标准),可以直接在H5页面中使用JavaScript API与后端进行交互,无需复杂的轮询机制,而且支持全双工通信。而TCP层级的通信通常不适合直接在纯浏览器环境中使用,因为浏览器API主要面向HTTP(S)协议栈,若要用TCP,往往需要借助Socket.IO、Flash Socket或其他插件,或者在服务器端代理并通过WebSocket、Comet等方式间接与客户端通信。

  • 开发复杂度与维护成本:WebSocket已经封装好了一套完整的握手、心跳、断线重连机制,对于开发者而言,使用WebSocket API相对简单。而TCP 开发则需要处理更多的底层细节,包括但不限于连接管理、错误处理、协议设计等,这对于前端开发人员来说门槛较高。

  • 资源消耗与性能:WebSocket 在建立连接之后可以保持持久连接,减少了每次请求都要建立连接和断开连接带来的资源消耗,提升了性能。而虽然TCP连接也可以维持长久,但如果是自定义TCP协议,由于没有WebSocket的标准化复用和优化机制,可能在大规模并发场景下,资源管理和性能控制更为复杂。

  • 移动设备支持:WebSocket在移动端浏览器上的支持同样广泛,对于跨平台的H5应用兼容性较好。若采用原生TCP,移动设备上的兼容性和开发难度会进一步加大。


websocket01.jpg


结论


       综上所述,H5实时数据推送建议使用WebSocket,但是在使用WebSocket的时候,大家对其安全机制要多关注,避免出现安全漏洞。


作者:石头聊技术
来源:juejin.cn/post/7345404998164955147
收起阅读 »

面试官:前端请求如何避免明文传输?谁沉默了,原来是我

web
如果你也在准备春招,欢迎加微信shunwuyu。这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。 前言 连夜肝文,面试以来最尴尬的一次,事情是这样的,最近有开始面稍微有难度一点岗位,本文的主题出自北京某一小厂的正式...
继续阅读 »

如果你也在准备春招,欢迎加微信shunwuyu。这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。



前言


连夜肝文,面试以来最尴尬的一次,事情是这样的,最近有开始面稍微有难度一点岗位,本文的主题出自北京某一小厂的正式岗面试题,薪资水平大概开在10k-12k。之前一直是投的比较小的公司比较简单的实习岗位,这个是无意间投出去的一个,由于是 0 年经验小白*1,结果没想到简历过筛,硬着头皮上了。


结果很惨,40分钟的面试有 80% 不会回答,像大文件上传、缓存优化、滑动 text-area标签用什么属性(话说为什么有这么冷的题)等等,有一个算一个,都没答出来。


2.jpg


重点来了,在两个面试官问到前端请求如何避免明文传输的时候,在我绞尽脑汁思考五秒之后,现场气氛非常凝重,这道题也成为了这次面试的最后一题。


在此提醒各位小伙伴,如果你的简历或者自我介绍中有提到网络请求,一定要注意了解一下有关数据加密处理,出现频率巨高!!!


最后,下午四点面试,六点hr就通知了我面试结果,凉凉


微信图片_20240224002007.jpg


如何避免前端请求明文传输


要在前端发送请求时做到不明文,有以下几种方法:



  1. HTTPS 加密传输: 使用 HTTPS 协议发送请求,所有的数据都会在传输过程中进行加密,从而保护数据不以明文形式传输。这样即使数据被截获,黑客也无法直接获取到数据的内容。

  2. 数据加密处理: 在前端对敏感数据进行加密处理,然后再发送请求。可以使用一些加密算法,如 AES、RSA 等,将敏感数据进行加密后再发送到服务器。这样即使数据在传输过程中被截获,也无法直接获取其内容。

  3. 请求签名验证: 在发送请求之前,前端对请求参数进行签名处理,并将签名结果和请求一起发送到服务器。服务器端根据事先约定的签名算法和密钥对请求参数进行验证,确保请求的完整性和可靠性。

  4. Token 验证: 在用户登录时,后端会生成一个 Token 并返回给前端,前端在发送请求时需要将 Token 添加到请求头或请求参数中。后端在接收到请求后,验证 Token 的有效性,以确保请求的合法性。

  5. 请求头加密处理: 在发送请求时,可以将请求头中的一些关键信息进行加密处理,然后再发送到服务器。服务器端需要在接收到请求后对请求头进行解密,以获取其中的信息。


HTTPS 加密传输


HTTPS(HyperText Transfer Protocol Secure)是HTTP协议的安全版本,它通过在HTTP和TCP之间添加一层TLS/SSL加密层来实现加密通信。


HTTPS加密传输的具体细节:



  1. TLS/SSL握手过程: 客户端与服务器建立HTTPS连接时,首先进行TLS/SSL握手。在握手过程中,客户端和服务器会交换加密算法和密钥信息,以协商出双方都支持的加密算法和密钥,从而确保通信的安全性。

  2. 密钥交换: 在握手过程中,客户端会向服务器发送一个随机数,服务器使用该随机数以及自己的私钥生成一个对称密钥(即会话密钥)。该对称密钥用于加密和解密后续的通信数据。

  3. 证书验证: 在握手过程中,服务器会向客户端发送自己的数字证书。客户端会验证服务器的数字证书是否有效,包括检查证书的颁发机构、有效期等信息,以确认与服务器建立连接的真实性。

  4. 加密通信: 客户端和服务器在握手成功后,就会使用协商好的加密算法和密钥进行通信。客户端和服务器之间传输的所有数据都会被加密,包括HTTP请求和响应内容、URL、请求头等信息。

  5. 完整性保护: 在通信过程中,TLS/SSL还会使用消息认证码(MAC)来保护通信的完整性,防止数据在传输过程中被篡改。MAC是通过将通信内容和密钥进行哈希计算得到的,用于验证数据的完整性。


通过以上步骤,HTTPS这种加密通信方式在保护用户隐私、防止数据被窃取或篡改方面起到了重要作用。


数据加密处理


数据加密处理是指在前端对敏感数据进行加密处理,以确保数据在传输过程中的安全性。


数据加密处理的一般步骤和具体方法:



  1. 选择加密算法: 首先需要选择合适的加密算法,常见的包括对称加密算法(如AES)和非对称加密算法(如RSA)。对称加密算法使用相同的密钥进行加密和解密,而非对称加密算法使用公钥和私钥进行加密和解密。

  2. 生成密钥: 对于对称加密算法,需要生成一个密钥,用于加密和解密数据。对于非对称加密算法,需要生成一对公钥和私钥,公钥用于加密数据,私钥用于解密数据。

  3. 加密数据: 在前端,使用选择好的加密算法和密钥对敏感数据进行加密处理。例如,对用户的密码、个人信息等敏感数据进行加密处理,确保在数据传输过程中不被窃取或篡改。

  4. 传输加密数据: 加密后的数据可以作为请求的参数发送到服务器。在发送请求时,可以将加密后的数据作为请求体或请求参数发送到服务器,确保数据在传输过程中的安全性。

  5. 解密数据(可选): 在服务器端接收到加密数据后,如果需要对数据进行解密处理,则需要使用相同的加密算法和密钥对数据进行解密操作。这样可以得到原始的明文数据,进一步进行业务处理。


总的来说,数据加密处理通过选择合适的加密算法、安全地管理密钥,以及正确地使用加密技术,可以有效地保护用户数据的安全性和隐私性。


请求签名验证


请求签名验证是一种验证请求完整性和身份验证的方法,通常用于确保请求在传输过程中没有被篡改,并且请求来自于合法的发送方。


请求签名验证的一般步骤:



  1. 签名生成: 发送请求的客户端在发送请求之前,会根据事先约定好的签名算法(如HMAC、RSA等)以及密钥对请求参数进行签名处理。签名处理的结果会作为请求的一部分发送到服务器。

  2. 请求发送: 客户端发送带有签名的请求到服务器。签名可以作为请求头、请求参数或请求体的一部分发送到服务器。

  3. 验证签名: 服务器接收到请求后,会根据事先约定好的签名算法以及密钥对请求参数进行签名验证。服务器会重新计算请求参数的签名,然后将计算得到的签名和请求中的签名进行比较。

  4. 比较签名: 服务器会将计算得到的签名和请求中的签名进行比较。如果两者一致,则说明请求参数没有被篡改,且请求来自于合法的发送方;否则,说明请求可能被篡改或来自于非法发送方,服务器可以拒绝该请求或采取其他适当的处理措施。

  5. 响应处理(可选): 如果请求签名验证通过,服务器会处理请求,并生成相应的响应返回给客户端。如果请求签名验证不通过,服务器可以返回相应的错误信息或拒绝请求。


通过请求签名验证,可以确保请求在传输过程中的完整性和可靠性,防止数据被篡改或伪造请求。这种方法经常用于对 API 请求进行验证,保护 API 服务的安全和稳定。


Token 验证


Token 验证是一种常见的用户身份验证方式,通常用于保护 Web 应用程序的 API 端点免受未经授权的访问。


Token验证的一般步骤:



  1. 用户登录: 用户使用用户名和密码登录到Web应用程序。一旦成功验证用户的凭据,服务器会生成一个Token并将其返回给客户端。

  2. Token生成: 服务器生成一个Token,通常包括一些信息,如用户ID、角色、过期时间等,然后将Token发送给客户端(通常是作为响应的一部分)。

  3. Token发送: 客户端在每次向服务器发送请求时,需要将Token作为请求的一部分发送到服务器。这通常是通过HTTP请求头的Authorization字段来发送Token,格式可能类似于Bearer Token。

  4. Token验证: 服务器在接收到请求时,会检查请求中的Token。验证过程包括检查Token的签名是否有效、Token是否过期以及用户是否有权限执行请求的操作。

  5. 响应处理: 如果Token验证成功,服务器会处理请求并返回相应的数据给客户端。如果Token验证失败,服务器通常会返回401 Unauthorized或其他类似的错误代码,并要求客户端提供有效的Token。

  6. Token刷新(可选): 如果Token具有过期时间,客户端可能需要定期刷新Token以保持登录状态。客户端可以通过向服务器发送刷新Token的请求来获取新的Token。


在Token验证过程中,服务器可以有效地识别和验证用户身份,以确保API端点仅允许授权用户访问,并保护敏感数据不被未经授权的访问。


请求头加密处理


请求头加密处理是指在前端将请求头中的一些关键信息进行加密处理,然后再发送请求到服务器。


请求头加密处理的一般步骤:



  1. 选择加密算法: 首先需要选择适合的加密算法,常见的包括对称加密算法(如AES)和非对称加密算法(如RSA)。根据安全需求和性能考虑选择合适的加密算法。

  2. 生成密钥: 对于对称加密算法,需要生成一个密钥,用于加密和解密请求头中的信息。对于非对称加密算法,需要生成一对公钥和私钥,公钥用于加密数据,私钥用于解密数据。

  3. 加密请求头: 在前端,使用选择好的加密算法和密钥对请求头中的关键信息进行加密处理。可以是请求中的某些特定参数、身份验证信息等。确保加密后的请求头信息无法直接被识别和篡改。

  4. 发送加密请求: 加密处理后的请求头信息作为请求的一部分发送到服务器。可以是作为请求头的一部分,也可以是作为请求体中的一部分发送到服务器。

  5. 解密处理(可选): 在服务器端接收到加密请求头信息后,如果需要对请求头进行解密处理,则需要使用相同的加密算法和密钥对数据进行解密操作。这样可以得到原始的请求头信息,服务器可以进一步处理请求。


请求头加密处理这种方法可以有效地防止请求头中的敏感信息被窃取或篡改,并提高了数据传输的安全性。


请求头加密处理和数据加密处理的区别


请求头加密处理和数据加密处理在概念和步骤上非常相似,都是为了保护数据在传输过程中的安全性。


要区别在于加密的对象和处理方式:



  1. 加密对象:



    • 请求头加密处理: 主要是对请求头中的一些关键信息进行加密处理,例如身份验证信息、授权信息等。请求头中的这些信息通常是用来授权访问或识别用户身份的关键数据。

    • 数据加密处理: 主要是对请求体中的数据或响应体中的数据进行加密处理,例如用户提交的表单数据、API请求中的参数数据等。这些数据通常是需要保护隐私的用户输入数据或敏感业务数据。



  2. 处理方式:



    • 请求头加密处理: 一般来说,请求头中的关键信息通常较少,并且不像请求体中的数据那样多样化。因此,请求头加密处理可以更加灵活,可以选择性地对请求头中的特定信息进行加密处理,以提高安全性。

    • 数据加密处理: 数据加密处理通常是对请求体中的整体数据进行加密处理,以保护整体数据的安全性。例如,对表单数据进行加密处理,或对API请求参数进行加密处理,确保数据在传输过程中不被窃取或篡改。




结论:
请求头加密处理和数据加密处理都是为了保护数据在传输过程中的安全性,但针对的对象和处理方式有所不同。


请求头加密处理主要针对请求头中的关键信息进行加密,而数据加密处理主要针对请求体中的数据进行加密。


作者:知了知了__
来源:juejin.cn/post/7338702103882399744
收起阅读 »

保守点,90%的程序员不适合做独立开发

近两年互联网行业不景气,很多程序员都在寻找新出路。很自然的,独立开发成为一个充满吸引力的选择 —— 背靠自己的开发技能,不用看老板脸色,靠产品养活自己,想想就很美好。 但恕我直言,保守点说,90%的程序员不适合做独立开发。 这篇文章全是大实话,虽然会打破一些人...
继续阅读 »

近两年互联网行业不景气,很多程序员都在寻找新出路。很自然的,独立开发成为一个充满吸引力的选择 —— 背靠自己的开发技能,不用看老板脸色,靠产品养活自己,想想就很美好。


但恕我直言,保守点说,90%的程序员不适合做独立开发。


这篇文章全是大实话,虽然会打破一些人的幻想,但也提供解决方案,希望对迷茫的同学有些帮助。


独立开发赚钱么?


如果你满足如下画像:



  • 程序员工作多年,编程水平不错

  • 收入完全来源于工资

  • 日常学习的目的是提升技术


那对你来说,独立开发是不赚钱的。不赚钱并不是说做这事儿一分钱赚不到,满足以上画像的大部分独立开发者在持续经营半年到一年产品后,还是能稳定获得几刀~几十刀收益的。只是相比于付出的心血来说,这点收益实在是低。


以至于出海独立开发圈儿在谈收益时的语境都不是我开发了1年,现在每月能赚50刀,而是我开发了1年,现在拥有了等效于3w刀年化2%的货基(3w * 2% / 12 = 50)


这么一换算,欣慰了许多。


为什么不赚钱?因为独立开发的重点并不在于开发,叫独立产品会更准确些。


对于一款形成稳定变现闭环的产品,有3个最重要的环节:



  • 流量获取

  • 运营转化

  • 产品交付


程序员只是产品交付环节下的一个工种,与你同处产品交付环节的工种还包括产品经理、QA、项目经理、运维......


独立开发的本质就是你一个人抗下上述所有工种。


话又说回来,如果你即会编程又会流量获取,会运营转化,这样的复合人才在公司根本不用担心被裁,也没必要做独立开发。


所以,对于满足以上画像的同学,我劝你不要把独立开发当作失业后的救命稻草。


认识真实的商业世界


虽然我不建议你all in独立开发,但我建议有空闲时间的同学都去尝试下独立开发。


尝试的目的并不是赚钱,而是更具象的感知流量获取 -> 运营转化 -> 产品交付的路径。


大部分互联网产品往简单了说,都是表格 + 表单的形式,比如推特就是2个大表单(推荐流、关注流)以及描述用户之间关系的表格。


既然如此,当我们有了独立开发的想法时,首先考虑的应该是 —— 我的产品能不能用表格 + 表单 + 高效沟通实现,比如腾讯/飞书文档 + 微信群交流


像多抓鱼(做二手书业务)早期验证需求时,就是几个用户群 + 保存二手书信息的excel表组成。


如果你发现需求靠微信群交流就能解决,付款靠微信转账就能解决,那还有必要写代码开发项目,对接微信支付API么?


当聊到微信交流时,其实就触碰到另一个工种的工作范围了 —— 私域运营。在私域运营看来,通过微信(或其他社交软件)成交是再正常不过的商业模式,但很多程序员是不知道的。


这就是为什么我不建议你把独立开发当作被裁后的救命稻草,但建议有空闲时间的同学都去尝试下独立开发 —— 涉猎其他工种的工作范围,认识真实的商业世界。


当达到这一步后,我们再考虑下一步 —— 发掘你的长处。


发掘你的长处


当我们认识到一款完整的产品有3个最重要的环节:



  • 流量获取

  • 运营转化

  • 产品交付


就应该明白 —— 如果我们想显著提高独立开发的成功率,最好的方式是找到自己最擅长的环节,再和擅长其他环节的人合作。


这里很多程序员有个误区,会认为程序员擅长的肯定就是产品交付下的开发。


实际上,就我交流过的,或者亲自带出来的跑通变现闭环的程序员中,很多人有编程之外的天赋,只是他们没有意识到罢了。


举几个非常厉害的能力(或者说天赋):



  1. 向上突破的能力


有一类同学敢于把自己放到当前可能还不胜任的位置,然后通过不断学习让自己完成挑战。举几个例子:



  • 在不懂地推的时候,参与到校园外卖团队做地推,学习市场和推广的知识

  • 在只看了一本HTML书的情况下,敢直接接下学校建设国际会议网站的任务

  • 在不懂做运营的时候,有老板找他当公司运营负责人,他也接下来,并也做得很好


这类同学很容易跑出有自己特色的非标服务,再包装成产品售卖。



  1. 源源不断的心力支持


有位同学看短视频趋势不错,正好大学也玩过一段时间单反,就买了一套专业的影视设备,准备一边学做饭一边拍短视频,想做一名美食博主。


每天下班拍视频、剪辑加后期的,每个视频都需要花 10+ 个小时。熬了半年多,数据一直不行,就放弃了。


虽然他失败了,但很少有人能在没有正反馈的事上坚持半年,这种源源不断的心力支持其实是一种天赋。


靠这个天赋,只要踩到合适的赛道,成功是迟早的事儿。



  1. 链接人的能力


有些同学特别喜欢在群里唠嗑,与大佬聊天也不犯怵。这就是链接人的天赋


在如今的时代,有价值的信息通常是在小圈子中传播,再慢慢破圈到大众视野中。这类同学靠链接人的天赋,可以:



  1. 从小圈子获得有价值的信息,做信息差生意

  2. 做中间人整合资源


假设你探寻一圈后发现 —— 自己最拿得出手的就是编程能力,那你的当务之急不是发掘需求


以咱们普通程序员的产品sense,也就能想出笔记应用Todo List应用这类点子了......


你需要做的,是多认识其他圈子的人,向他们展示你的编程能力,寻找潜在的需求方


以我在运营的Symbol社区举例,这是个帮程序员发展第二曲线的社群。


之前社群有个痛点:每天社群会产生大量有价值的碎片知识,但这些知识分散在大量聊天消息中,爬楼看消息很辛苦。


基于这个痛点出发,我作为产品经理和群里两位小伙伴合作开发了识别、总结、打标签、分发有价值聊天记录的社群机器人



作为回报,这两位小伙伴将获得付费社群的收入分成。


总结


对于满足如下画像的程序员:



  • 程序员工作多年,编程水平不错

  • 收入完全来源于工资

  • 日常学习的目的是提升技术


不要把独立开发当作被裁后的救命稻草,而应该将其作为认识真实商业世界分工的途径,以及发掘自身优势的手段。


拍脑袋想没有用,只有真正在事儿上修,才能知道自己喜欢什么、擅长什么。


当认清自身优势后,与有其他优势的个体合作,一起构建有稳定收益闭环的产品。




作者:魔术师卡颂
来源:juejin.cn/post/7345756317557047306
收起阅读 »

快速从0-1完成聊天室开发——环信ChatroomUIKit功能详解

聊天室是当下泛娱乐社交应用中最经典的玩法,通过调用环信的 IM SDK 接口,可以快速创建聊天室。如果想根据自己业务需求对聊天室应用的 UI界面、弹幕消息、礼物打赏系统等进行自定义设计,最高效的方式则是使用环信的 ChatroomUIKit 。文档地址:htt...
继续阅读 »

聊天室是当下泛娱乐社交应用中最经典的玩法,通过调用环信的 IM SDK 接口,可以快速创建聊天室。如果想根据自己业务需求对聊天室应用的 UI界面、弹幕消息、礼物打赏系统等进行自定义设计,最高效的方式则是使用环信的 ChatroomUIKit 。

文档地址:https://doc.easemob.com/uikit/chatroomuikit/ios/roomuikit_overview.html


环信 ChatroomUIKit v1.0.0,提供了 UIKit 的各种组件,可根据实际业务需求快速上线聊天室功能。通过该 UIKit,聊天室中的用户可实时交互,发送普通弹幕消息,并支持打赏消息和全局广播等功能。

环信ChatroomUIKit 亮点功能

1、模块化、组件化、API驱动三层架构—开发更灵活、自由

环信 ChatroomUIKit 采用模块化、组件化和 API 驱动的三层架构,提供高完成度和高自定义能力的聊天室解决方案。

  • 模块化:聊天室的整个页面被划分为不同的模块,例如弹幕区、输入区、成员列表和礼物列表等。我们可以自由重写每个模块,而不影响其他模块的可用性。
  • 组件化:每个模块都由多个组件组成。可以灵活定制每个组件,以满足个性化需求。
  • API 驱动:环信 ChatroomUIKit 提供了一套稳定可靠的 API,可用于自定义聊天室的各种属性。

2、全平台覆盖 跨平台开发更简易

在移动应用开发领域,React Native 和 Flutter 是两个备受欢迎的跨平台 UI 框架,业内很多 IM UIKit 目前还不支持上述两个跨平台框架,而环信 ChatroomUIKit 实现了突破!


在覆盖 iOS、Android、Web 三大原生平台的同时,环信ChatroomUIKit 还支持 Flutter 和 React Native 跨平台框架,助力开发者快速实现跨平台开发。

3、UI界面、功能组件可自定义、快速上线

环信 ChatroomUIKit 采用最新的 UI 框架和开发语言,可以快速上手。同时,该 UIKit 对标国际主流的社交应用,提供开箱即用的社交组件,一方面,可以快速将其集成到自己的应用程序中,另一方面还支持对功能组件进行自定义,灵活定制符合自身需求的聊天室应用。

4、界面明暗主题快速切换

明暗主题界面是目前极为主流的 App 界面设置,环信 ChatroomUIKit 默认风格为明亮模式,支持明暗主题一键切换,我们可以简单快速地对界面中所有元素的明亮/暗黑风格进行设置,产出舒适的视觉体验。

  • 浅色主题

  • 深色主题

5、自定义弹幕消息

弹幕消息是聊天室最为核心的功能。环信 ChatroomUIKit 支持业内主流的消息样式,包括发送时间显示、用户身份标识、用户头像、昵称等元素,提供极为灵活的弹幕消息自定义能力。


还可以根据业务需要,通过开关控件控制所有元素的显示或隐藏,如,是否隐藏对话框中的用户头像和昵称等。同时,也可以对消息气泡、颜色、字体等属性进行灵活快速的调整。

6、完整的打赏模块

对于主播和直播平台而言,打赏是主要收入来源之一,是直播/社交类 App 的重要功能。环信 ChatroomUIKit 支持完整的打赏流程,包含礼物赠送和打赏消息两部分。支持自定义礼物的样式、名称和金额等属性,也能够拓展礼物类型,如普通观众和会员观众礼物。同时,还可以选择特殊的打赏消息样式,突出展示打赏行为。


7、丰富的消息管理功能 支持全局广播、消息翻译

环信 ChatroomUIKit 具备丰富的消息管理功能,例如,支持用户向 App 内所有聊天室在线观众发送消息、通知以及大额打赏等重要信息;支持聊天室内的消息撤回和消息举报功能,所有用户只能撤回自己发送的消息。


同时,为满足出海用户的业务需要,环信 ChatroomUIKit 还支持消息的翻译功能,用户可以将聊天室中的单条消息从一种语言转换成另一种语言。

除了以上功能,环信 ChatroomUIKit 还支持成员管理等更多功能,进一步了解咨询或体验Demo可参考 ChatroomUIKit文档中心

相关文档:

收起阅读 »

HTML常用布局标签:提升网页颜值!不可不知的HTML布局技巧全解析!

在HTML的世界里,一切都是由容器和内容构成的。容器,就如同一个个盒子,用来装载各种元素;而内容,则是这些盒子里的珍宝。理解了这一点,我们就迈出了探索HTML布局的第一步。在HTML中,布局标签主要用于控制页面的结构和样式。本文将介绍一些常用的布局标签及其使用...
继续阅读 »

在HTML的世界里,一切都是由容器和内容构成的。容器,就如同一个个盒子,用来装载各种元素;而内容,则是这些盒子里的珍宝。理解了这一点,我们就迈出了探索HTML布局的第一步。

在HTML中,布局标签主要用于控制页面的结构和样式。本文将介绍一些常用的布局标签及其使用方法,并通过代码示例进行演示。

一、理解布局的重要性

布局在我们前端开发中担任什么样的角色呢?想象一下,你面前有一堆散乱的积木,无序地堆放在那里。

Description

而你的任务,就是将这些积木按照图纸拼装成一个精美的模型。HTML布局标签的作用就像那张图纸,它指导浏览器如何正确、有序地显示内容和元素,确保网页的结构和外观既美观又实用。

下面我们就来看看在HTML中常用的基础布局标签有哪些,如何使用这些布局标签完成我们的开发目标。

二、常用的布局标签

1、div标签

div标签是一个块级元素,它独占一行,用于对页面进行区域划分。它可以包含其他HTML元素,如文本、图片、链接等。通过CSS样式可以设置div的布局和样式。

示例代码:

<!DOCTYPE html>
<html>
<head>
<style>
  .box {
    width: 200px;
    height: 200px;
    background-color: red;
  }
</style>
</head>
<body>

<div>这是一个div元素

</div>

</body>
</html>

运行结果:

Description

2、span标签

span标签是一个内联元素,它不独占一行,用于对文本进行区域划分。它主要用于对文本进行样式设置,如字体、颜色等。与div类似,span也可以包含其他HTML元素。
示例代码:

<!DOCTYPE html>
<html>
<head>
<style>
  .text {
    color: blue;
    font-size: 20px;
  }
</style>
</head>
<body>

<p>这是一个<span>span元素</span></p>

</body>
</html>

运行结果:

Description

3、table标签

table标签用于创建表格,它包含多个tr(行)元素,每个tr元素包含多个td(单元格)或th(表头单元格)元素。

<table> 定义一个表格,<tr> 定义表格中的行,而 <td> 则定义单元格。通过这三个标签,我们可以创建出整齐划一的数据表,让信息的展示更加直观明了。

需要注意的是:

  • <table></table>标记着表格的开始和结束。
  • <tr></tr>标记着行的开始和结束,几组表示该表格有几行。
  • <td></td>标记着单元格的开始和结束,表示这一行中有几列。

示例代码:

<!DOCTYPE html>
<html>
<head>
<style>
  table, th, td {
    border: 1px solid black;
  }
</style>
</head>
<body>
<table>
  <tr>
    <th>姓名</th>
    <th>年龄</th>
  </tr>
  <tr>
    <td>张三</td>
    <td>25</td>
  </tr>
  <tr>
    <td>李四</td>
    <td>30</td>
  </tr>
</table>
</body>
</html>

运行结果:

Description

4、form标签

<form>标签的主要作用是定义一个用于用户输入的HTML表单。这个表单可以包含各种输入元素,如文本字段、复选框、单选按钮、提交按钮等。

<form>元素可以包含以下一个或多个表单元素:<input><textarea><button><select><option><optgroup><fieldset><label><output>等。

示例代码:

<!DOCTYPE html>
<html>
<head>
<style>
  form {
    display: flex;
    flex-direction: column;
  }
</style>
</head>
<body>

<form>
  <label for="username">用户名:</label>
  <input type="text" id="username" name="username">
  <br>
  <label for="password">密码:</label>
  <input type="password" id="password" name="password">
  <br>
  <input type="submit" value="提交">
</form>

</body>
</html>

运行结果:
Description

5、列表标签

1)无序列表

  • 指没有顺序的列表项目
  • 始于<ul>标签,每个列表项始于<li>
  • type属性有三个选项:disc实心圆、circle空心圆、square小方块。 默认属性是disc实心圆。

示例代码:

<!DOCTYPE html>
<htmml>
<head>
<meta charst = "UTF-8">
<title>html--无序列表</title>
</head>
<body>
<ul>
<li>默认的无序列表</li>
<li>默认的无序列表</li>
<li>默认的无序列表</li>
</ul>
<ul>
<li type = "circle">添加circle属性</li>
<li type = "circle">添加circle属性</li>
<li type = "circle">添加circle属性</li>
</ul>
<ul>
<li type = "square">添加square属性</li>
<li type = "square">添加square属性</li>
<li type = "squaare">添加square属性</li>
</ul>
</body>
</html>

运行结果:
Description
也可以使用CSS list-style-type属性定义html无序列表样式。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

2)有序列表

  • 指按照字母或数字等顺序排列的列表项目。
  • 其结果是带有前后顺序之分的编号,如果插入和删除一个列表项,编号会自动调整。
  • 始于<ol>标签,每个列表项始于<li>

示例代码:

<ol>
<li>默认的有序列表</li>
<li>默认的有序列表</li>
<li>默认的有序列表</li>
</ol>
<ol type = "a" start = "2">
<li>第1项</li>
<li>第2项</li>
<li>第3项</li>
<li value ="20">第四项</li>
</ol>
<ol type = "Ⅰ" start = "2">
<li>第1项</li>
<li>第2项</li>
<li>第3项</li>
</ol>

运行结果:
Description
同样也可以使用CSS list-style-type属性定义html有序列表样式。

3)自定义列表

  • 自定义列表不仅仅是一列项目,而是项目及其注释的组合。
  • <dl>标签开始。每个自定义列表项以<dt>开始。每个自定义列表项的定义以<dd>开始。
  • 用于对术语或名词进行解释和描述,自定义列表的列表项前没有任何项目符号。
    基本语法:
<dl>
<dt>名词1</dt>
<dd>名词1解释1</dd>
<dd>名词1解释2</dd>

<dt>名词2</dt>
<dd>名词2解释1</dd>
<dd>名词2解释2</dd>
</dl>

<dl>即“definition list(定义列表)”,
<dt>即“definition term(定义名词)”,
<dd>即“definition description(定义描述)”。

示例代码:

<dl>
<dt>计算机</dt>
<dd>用来计算的仪器</dd>

<dt>显示器</dt>
<dd>以视觉方式显示信息的装置</dd>
</dl>

运行结果:
Description
以上就是HTML中常用的布局标签及其使用方法。在实际开发中,还可以结合CSS和JavaScript来实现更复杂的布局和交互效果。

掌握了这些HTML常用布局标签,你已经拥有了构建网页的基础工具。记住,好的布局不仅需要技术,更需要创意和对细节的关注。现在,打开你的代码编辑器,开始你的布局设计之旅吧!

收起阅读 »