注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

前端如何用密文跟后端互通?原来那么简单!

web
后端:密码得走密文哇! 我:base64?md5? 后端:这次不行哇,新来的测试不好糊弄呢!必须要国密sm2加密捏 我: 好吧,看我的。 我这边使用的是sm-crypto,当然也有很多优秀的库如:forge,我在业务上搭配jszip做过上传zip文件内藏加密后...
继续阅读 »

后端:密码得走密文哇!

我:base64?md5?

后端:这次不行哇,新来的测试不好糊弄呢!必须要国密sm2加密捏

我: 好吧,看我的。


我这边使用的是sm-crypto,当然也有很多优秀的库如:forge,我在业务上搭配jszip做过上传zip文件内藏加密后的私钥进行证书登录,还是不错的支持,但是文档社区不是很友好,所以推荐sm-crypto,接下来让我们一起使用它吧。


sm-crypto是一个基于Node.js的密码学库,专为与国密算法(中国密码算法标准)兼容而设计。它提供了各种加密、解密、签名和验证功能。


sm-crypto包含多种密码算法的实现,例如:



  • SM1:对称加密算法,其加密强度与AES相当,但该算法不公开,调用时需要通过加密芯片的接口进行调用。

  • SM2:非对称加密算法,基于ECC(椭圆曲线密码学)。该算法已公开,且由于基于ECC,其签名速度与秘钥生成速度都快于RSA。此外,ECC 256位(SM2采用的就是ECC 256位的一种)的安全强度比RSA 2048位高,但运算速度快于RSA。

  • SM3:消息摘要算法,可以用MD5作为对比理解,其校验结果为256位。

  • SM4:无线局域网标准的分组数据算法,属于对称加密,密钥长度和分组长度均为128位。


sm-crypto内部方法介绍


1.SM2加密与解密


SM2是一种基于椭圆曲线密码学的非对称加密算法。sm-crypto提供了SM2的密钥生成、加密、解密等功能。通过调用相关方法,开发者可以轻松地生成SM2密钥对,并使用公钥进行加密、私钥进行解密。


const { sm2 } = require('sm-crypto');  
const keyPair = sm2.generateKeyPairHex(); // 生成密钥对
const publicKey = keyPair.publicKey; // 公钥
const privateKey = keyPair.privateKey; // 私钥

const message = 'Hello, SM2!'; // 待加密的消息
const encrypted = sm2.doEncrypt(message, publicKey, { hash: true }); // 使用公钥加密
const decrypted = sm2.doDecrypt(encrypted, privateKey, { hash: true, raw: true }); // 使用私钥解密

console.log('加密结果:', encrypted);
console.log('解密结果:', decrypted.toString()); // 输出原始消息

image.png


2.SM3摘要算法


SM3是一种密码杂凑算法,用于生成消息的摘要值。sm-crypto提供了SM3的摘要计算功能,开发者可以通过调用相关方法计算任意消息的SM3摘要值。


const { sm3 } = require('sm-crypto');  
const message = 'Hello, SM3!'; // 待计算摘要的消息
const digest = sm3(message); // 计算SM3摘要值

console.log('SM3摘要值:', digest);

image.png


3.SM4分组加密算法


SM4是一种分组密码算法,适用于无线局域网等场景。sm-crypto提供了SM4的加密与解密功能,开发者可以使用SM4密钥对数据进行加密和解密操作。


const sm4 = require('sm-crypto').sm4;                                               |
const sm4 = require('sm-crypto').sm4;
const key = '0123456789abcdeffedcba9876543210'; // 16字节的SM4密钥
const message = 'Hello, SM4!'; // 待加密的消息
const encrypted = sm4.encrypt(Buffer.from(message), Buffer.from(key, 'hex')); // 加密
const decrypted = sm4.decrypt(encrypted, Buffer.from(key, 'hex')); // 解密

console.log('加密结果:', encrypted.toString('hex'));
console.log('解密结果:', decrypted.toString()); // 输出原始消息

image.png


4、签名/验签


签名(Sign)


const { sm2 } = require('sm-crypto'); 
const keyPair = sm2.generateKeyPairHex(); // 生成密钥对
const publicKey = keyPair.publicKey; // 公钥
const privateKey = keyPair.privateKey; // 私钥

const message = '这是要签名的消息'; // 替换为实际要签名的消息
// 使用私钥对消息进行签名
let sigValueHex = sm2.doSignature(message, privateKey);
console.log('签名结果:', sigValueHex);

image.png
验签(Verify Signature)


const message = '这是要验证签名的消息'; // 应与签名时使用的消息相同  
const sigValueHex = '签名值'; // 替换为实际的签名值字符串,即签名步骤中生成的sigValueHex

// 使用公钥验证签名是否有效
let verifyResult = sm2.doVerifySignature(message, sigValueHex, publicKey);

console.log('验签结果:', verifyResult); // 如果验证成功,应输出true;否则输出false


image.png


实战例子


登录注册,对用户密码进行加密



注意:前端是不储存任何涉及安全的密钥(公钥是直接拿后端生成的)。



新建个工具文件,专门存放加密逻辑,我这用的是SM2


// smCrypto.js
import { sm2 } from 'sm-crypto' // 引入加密库

export const doEncrypt = ( // 加密
data,
pKey = publicKey,
cipherMode = 0
) =>
sm2.doEncrypt(
typeof data === 'object'
? JSON.stringify(data) : data,
pKey,
cipherMode
)
export const encryptionPwd = async data => { // 加密密码高阶
let servePublicKey = ''
await user.getSm2Pkeys()
.then(res => {
servePublicKey = res.data.content
})
return doEncrypt(
data,
servePublicKey
)
}

sm-crypto作为一款基于Node.js的国密算法库,为开发者提供了丰富的密码学功能。通过调用sm-crypto的内部方法,开发者可以轻松地实现SM2加密与解密、SM3摘要计算以及SM4分组加密等操作。这些功能在保障数据安全、构建安全应用等方面发挥着重要作用。同时,开发者在使用sm-crypto时,也需要注意遵循最佳的安全实践,确保密钥的安全存储和管理,以防止潜在的安全风险。


作者:大码猴
来源:juejin.cn/post/7350168797637558272
收起阅读 »

浏览器无痕模式就真的无痕了吗?不一定哦!

web
概述 无痕模式,有些浏览器也叫隐身模式,隐私模式。该模式下所有cookie、缓存是失效的,也就是所有原来的登录信息都会消失,那么是否你打开一个网站,网站平台就真的不确定你是谁了吗? 不一定哦。这个世界上有一种技术叫浏览器指纹技术,不需要你登录,它就可以根据你的...
继续阅读 »

概述


无痕模式,有些浏览器也叫隐身模式,隐私模式。该模式下所有cookie、缓存是失效的,也就是所有原来的登录信息都会消失,那么是否你打开一个网站,网站平台就真的不确定你是谁了吗? 不一定哦。这个世界上有一种技术叫浏览器指纹技术,不需要你登录,它就可以根据你的特定标志来区分,从而跟踪你的所有操作记录。今天我们就来看看这种技术的原理,还有用途。


一、原理


浏览器指纹可以在用户没有任何登录的情况下仍然知道你谁,比如你在登录了一下网站A,现在开启无痕模式,再次打开网站A, 那么网站A大概率还是能区分现在操作网站的人是谁。 因为在这个世界上,用户的浏览器环境极小概率才能相同,考虑的因素包括浏览器版本、浏览器型号、屏幕分辨率、系统语言、本地时间、CPU架构、电池状态、网络信息、已安装的浏览器插件等等各种各样。浏览器指纹技术就是用这些因素来综合计算一个哈希值,这个哈希值大概率是唯一的


二、展示


今天我们就展示一下单单利用Canvas画布这个功能来确定用户的唯一标识,因为Canvas API专为通过JavaScript和HTML绘制图形而设计,而图形在画布上的呈现方式可能因Web浏览器、操作系统、显卡和其他因素而不一样,从而产生可用于创建指纹的唯一图像,我们利用下方代码展示一个图像。


// 输入一个带有小写、大写、标点符号的文本
var txt = "BrowserLeaks,com <canvas> 1.0";
ctx.textBaseline = "top";
ctx.font = "14px 'Arial'";
ctx.textBaseline = "alphabetic";
ctx.fillStyle = "#f60";
ctx.fillRect(125,1,62,20);
ctx.fillStyle = "#069";
ctx.fillText(txt, 2, 15);
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
ctx.fillText(txt, 4, 17);

下面动画GIF图展示了虽然JavaScript代码相同,但是由于图像在不同系统上的呈现方式不同,因此每一个用户所显示的图像都有细微的差别。这种差别大多数时候人眼通常无法识别,但通过对生成图像的数据进行分析就能发现不一样。



我们直接看成品,利用普通模式和无痕模式打开这个测试网站,就能发现2个哈希值是完全一样的,并且是每15万个用户中有2个人可能是相同的哈希值,唯一性达到了99.99%。


三、用途


1、广告


广告联盟通过浏览器指纹技术,不需要你登录,就可以知道你看过哪些记录,然后在其他站点给你推送你感兴趣的广告。所以有时候我们经常会碰到在淘宝或者百度搜索了一个商品,然后去到其他网站,马上给你推送你搜索过的商品的广告。


2、防刷


针对一些商品的浏览量、内容的阅读量、投票等等各种类似平台,往往会针对用户做一个限制,比如只能浏览一次。这种时候往往也都是用浏览器指纹技术,虽然你可能注册了很多用户账号,但是你的浏览器指纹都是相同的,可以判断你就是同一个人。不过上有政策,下有对策,有个东西叫做指纹浏览器,这类浏览器里面可以随意的切换用户的指纹。 所以这个技术针对广大的普通用户防刷还是有效的,对专业的刷量工作室就没什么效果了。


作者:明同学
来源:juejin.cn/post/7347958786050637875
收起阅读 »

勇闯天涯-程序员小哥哥,来婚介所试一下嘛(七千字长文预警!)

前言 今天技术博主不聊技术,聊聊生活聊聊情感。先叠个甲,我纯纯乐子人,对婚介所无任何恶意,可能后面还得用上,本次纯纯试探下婚介所是个什么路数!事件源于上周五的一通红娘电话,我一听要给我介绍有钱有颜的小姐姐,直接挂断,这天底下哪有这样的好事。当天下午的时候,又给...
继续阅读 »

前言


今天技术博主不聊技术,聊聊生活聊聊情感。先叠个甲,我纯纯乐子人,对婚介所无任何恶意,可能后面还得用上,本次纯纯试探下婚介所是个什么路数!事件源于上周五的一通红娘电话,我一听要给我介绍有钱有颜的小姐姐,直接挂断,这天底下哪有这样的好事。当天下午的时候,又给我来了一通电话,仿佛不知道我给她挂断了一样,基于一些奇妙的想法,我想尝试以及找找素材,我决定让她微信跟我聊。然后因为博主工作比较忙,打了招呼后,就没理她,最关键是到晚上下班后,我也忘了这档子事。周六的下午四点左右,一通电话开启了本次的故事,即使315曝光了相关产业,但是博主依然要冲,就是头铁。


阳光下午的微醺时光


接到电话的时候,我挺疑惑的,这次上来就问我现在是不是有空,出于礼貌,并且我也听出了她是昨天那个红娘,我就没有挂断电话。上来就说,弟弟,我是XX老师,今天来电话主要是聊一下,看能不能进一步推荐小姐姐。说她那里看资料我是97年的程序员,年收入XX,对吗?然后我说是,她就跟我说这里有个小姐姐挺合适的,北京XX工作的,身高165身高,本地的,收入XX。哥们一听就懵了,本地的,心里OS是,那咋遭得住,咱也不是高富帅,纯纯普男。红娘一听就问我有没有考虑常驻北京,然后就问我老家哪里的,我说四川的,她就在资料库里找了找,说真有个四川在北京的,快速给我念了一遍女生的资料。


接着就离谱了,问我怎么称呼?我说姓王,哪里工作,海淀XX公司?红娘直接薄纱,诶,没听过耶( •̀ ω •́ )y,咱们一般服务百度、阿里和字节的比较多。好好好,北京人中龙凤就是多,哥们就是菜中菜,脸都绿了。接着问我加班如何,公司福利啥的,然后说我这个181cm的身高在四川挺高的,她知道的四川男生都不太高来着。然后聊到了哥们的毕业学校,说前两天有个大连理工的女孩,真绝了,大家都这么爱走婚介所吗?我感觉聊到啥,都有女孩能配上,后面说到工作,又聊到一个大二的姑娘还没工作就来找对象了,好好好,走上了人生快车道。


然后聊到了我父母的职业,就说哥们的条件在当地不错,怎么没找上对象?我当时就说到了我的前对象,网恋的,就在掘金找的,一说到这个,好兄弟们,掘金真的能找对象的,不骗人。我的失败不是网恋的问题,纯属我自己的问题,第一次谈吧,不知道咋谈,自己也是摸不准进度,达不到对象的期待,慢慢地就淡了。我也是无脑纯爱战士,这是红娘听了之后的评价,哈哈,不过我觉得前对象确实挺不错的,当时要是硬扛着也能继续,但是我感觉可能是热情燃尽了,她也算是太忙了太累了,无论是生活还是工作,我都挺佩服,主动退出,我感觉应该是我被甩了。不后悔,男人就该当断即断,她也不小了,继续耗着,对她不公平。说远了啊,核心意思就是,网恋也有靠谱的,掘金这质量高的离谱,大家多去相亲角看看。


说着说着,突然红娘聊到了星座,说我应该了解了解星座,也说到了一些星座的小知识。然后顺嘴提到了应该门当户对,不只是能力,还有三观,阶层之类的。接着说道,我条件还行,为啥这么晚谈恋爱,是不是打游戏了?我说姐,没毛病,游戏真好玩!又接着问我单身多久了,身边还有没有介绍的呀,同事父母啥的资源。接着就进入正题了,就说小弟弟你呀,身边没有资源,姐姐这里有呀,你又有点慢热,男人花期这么短,这不得抓紧时间。你没谈就是没有遇到合适的人,圈子太小,北京这边初婚太晚了,25岁才准备开始,你想想四川重庆那边20左右就开始准备结婚了,你回去那就是大龄青年了,不是市场上最合适的那一批了。姐姐之前认识一个大哥,一年给女生花了七八万,环球影城去哈尔滨各种礼物砸下去结果手都没牵上。我???这不是大冤种嘛,移动ATM。红娘就跟我说,弟弟你啊,现在这恋爱能力,跟女生比划比划半年结果嘴都亲不上,咱们也不要浪费时间,选对人比胡乱去试强多了。姐姐我这啊,适合你的资源还蛮多的呀,你是B站来的资源,这种比较少,一般只占15%,我们婚介所大部分都是线下和国企政府合作的,所以需要你提供一定的身份和征信资料,因为我们这的姑娘都是认证过的,要对大家负责。接着又说觉得我性格比较好,很坦诚,就问我对女生有没有什么要求,我说年龄上不超过我三岁吧,不抽烟不去酒吧夜店,然后红娘跟我说,你这就没别的要求啦,那还挺低的,然后就问我三观、家庭情况之类的要求,问的很细。然后谈笑间,红娘给我猛推三个女生,嘎嘎读资料,听得哥们心里发慌,动不动就是大厂,家里有矿,问女生条件是否符合我的要求.......我真蚌埠住了,哥们真的配嘛,这条件谷歌,父母地方小领导或者开公司,问我符不符合,好好好,我能傍富婆不?


有一说一,专业红娘就是能聊,我俩聊了得有一小时,最后说让我去线下谈,给我预约了她明早10点半的时间。她想要了解下我这个人的衣着品位、谈吐举止、再做下深度的了解,以及一些信息的认证,同时也给我一些情感上的建议,让我少走些弯路。哥们听到情感上的建议,真的心动了,教练,俺就缺这个,确认了下这次聊天不收钱,就是个单纯的信息认证,我就决定去了。


小雨霖霖的春日午前


因为约好的时间是10点半,我住在石景山,婚介所在朝外SOHO那里,差不多一小时十几分钟的路程,我就九点出发了。到了地方之后,周围有好几个SOHO,没有明显指示牌,而且感觉周日早上那里人也少,当时就觉得奇怪了。因为事前的时候,跟亲友和群里的小伙伴下过军令状,不掏一分钱,实时播报进度,没消息就赶紧救我,所以我倒也没有特别慌。我的想法就很纯粹,想通过红娘的眼睛来认识我在相亲市场中是什么样的,因为我接触的人相对少嘛,所以也算是练练胆,特别是这种情感上的,多积累积累经验总没错。没找到地方,我就拍了个附件的标识给红娘老师,她就指挥我上了楼,上楼之后还蛮吓人的,左边黑的,右边就她家灯亮的。


进门之后有个助理来接待,给了我一张表单去填,相对详细哈,但是身-份-证这敏感信息可以选择不填,其实这会儿我觉得还挺不错,放下了防备,但是打开了录音,哈哈。把我领到一个逼仄的会议室,大概两平米吧,首先给我拍了一张坐着的照片,然后在我填表的时候,说要看下我的征信,简单点的就看芝麻信用就行,看了我的八百多分吧,就说你这还蛮高的。又试探性的问了能不能看下花呗借呗之类的,我寻思着也没多少额度,那就看呗,他一看就挺奇怪的,就说按你这个分数应该有好几万才对呀,你是自己调了吗?我说是呀,没事搞那么多干啥,又问我有没有信用卡,我说就拿花呗当信用卡。又说想看我支付宝里的资产,我寻思着没几个钱,那就看呗。他一看就说,你没炒基金股票啥的嘛,我说没,他就顺着话说,昨天有个姑娘和你一样,也是这样的,她就喜欢这种稳赚不赔的。浅聊了一下就说,去给我叫红娘老师,说红娘老师特别看重我,昨天周六来了好多姑娘,很多都不错的,让我跟红娘老师好好学学。哥们一听这个就来劲了,好啊,就是要这个,我不就缺这套恋爱情商嘛。


热情似火的夏日狂欢


红娘老师进来首先点评了我的头发,有点长还有点凌乱,说怎么还没理发呀。这一点说的挺对,我快一个月没理了,因为头发烫过,又有点长,显得乱。然后又说我好白呀,南方人皮肤就是好,因为我有痘痘嘛,就聊了聊油性皮肤。然后就跟我说想看看我短发的照片,说比较难想象我短发的照片,又说我这个是不是渣男锡纸烫,认出来了我这是前刺发型,感谢我的四川托尼,摩根前刺音容犹在。然后又说我字写的还不错,我说着就没必要硬夸了吧,她说跟其他人对比算是工整了。因为看到我手机壳了,就问我是个什么图案,我给她看了是二次元,她就问我是不是老爱玩B站,是不是博主,我说我哪是啊,只是喜欢看。她就说你声音还蛮好听的有点像肖战,可以整点视频啥的,说不定还能火。我说没那颜值,哈哈,谈笑间我就给她发了几张我的生活照,还有全身照片。她就说没有很胖呀,看着还好,而且家里还蛮干净的,弟弟习惯不错。


我也比较能说嘛,我就说红娘老师怎么不开个号啥的,吐吐槽啥的,比如B站上的XX说媒啥的。她跟我说她们上班很忙的,周一周二休息,平时都是快九点才下班,哪有时间开视频吐槽,都是朋友之间聊聊。然后她就很好奇我之前电话里说的前对象嘛,就聊了聊我前对象的事。因为我是抱着想要咨询的目的来的,所以我也说的相对比较详细,红娘比较在意的点是各阶段时间点、怎么认识的、告白怎么做的、准备了什么样的礼物和GG的理由。大致说了一遍,我觉得我前女友挺好的,无奈博主高攀不上,也没有达到人家的期望,就选择了默默退出。前女友说实话很不错的,情侣头像是我先换她才换的,我说话她也会回我,是我感觉被甩了,也是她有点冷吧,就放弃了。说这些呢,也是跟前面一样,说一下兄弟们要勇敢冲,掘金相亲角也靠谱,哪有那么多渣女,大部分还是好女孩,希望大家都把握得住,找到合适的另一半!


红娘老师跟我说了几个之前来聊的哥们,就说谈恋爱不能太晚,有一些大哥因为圈子比较小,导致来婚介所就想找个合适的姑娘一步到位。但是哪有这么合适的呀,一点攻略和目标都没有,怎么可能一口吃个胖子!我也附和老师说,对啊,心急吃不了热豆腐。然后就跟我说,昨天来了两哥们,一个京东一个百度的,还有00年来找对象的,我一听都懵了,这不都是人中龙凤嘛,这还愁对象,有没有大厂哥们来现身说法的,让小弟我长长见识。聊着聊着,就跟我看了个小姐姐,大四左右吧,想找个大五岁左右工作稳定的哥们,本身条件很优秀的,北交大硕士进国企,所以想要个稳定的生活。反正红娘老师意识就是说,现在女生吧,恋商普遍比男生多个三五年,现在这么卷,考虑得都比较现实。然后就说现在女生都是要求有钱有颜,小弟弟你也没钱,正在发展期嘛,颜的话还算周正,需要打理打理。然后就说想看看我之前的照片,要看我手机,emmm,这时候我没发现这是个套路,应该之前有哥们也做过这事儿。我有开录音嘛,左上角有小标,很烦,红娘应该是特意盯了这个地方,一开始问我怎么不给她看手机,而是直接发给她。我也没在意,她就老找理由看我手机,然后我也不小心被她看见了,她就问我是不是忘了关导航,哈哈,我打了个马虎眼就过了,然后后面过了一阵又看到了,就直接问我是不是录音。我非常坦诚的告诉她就是在录音,有顾虑,然后我也当着她面关了,我俩嘻嘻哈哈也就把这事过了。


想要摸一下秋天的老虎


聊了聊我的家庭,跟我聊了聊她最近咨询过的男生,有成功也有失败案例,反正都是会员。比较隐晦地指出了我这个水平在北京不够看,在成都也不算特别优秀地类型,但是结合家庭和年龄来讲,也算是还不错,至少还有操作空间。我一开始就想知道我在婚恋市场是个什么档次,以及从红娘视角来看我是什么样的,所以我就直球问她了。或许是基于职业素养,或者我问的有点子收费味道了,她顿了一下,从两方面给了我建议,一是从我初恋这段历程总结出来的,说我就是没有技巧的纯爱战士,需要提高情商,反正套话居多。二是不要乱撒网,要精准定位,选择自己合适的人,基于星座啥的跟我聊了半天,我这金牛座适合什么样的人。然后就说我这个改是来不及了,27也不小了,没救了,就从选对人开始,然后就开始围绕她们的服务开始聊。


总共聊了得有三小时左右吧,前一个半小时唠家常,根据你的资料聊细节,时不时说说女生的资料。后面就开始猛攻,围绕中介所的两项核心服务,一是心动女生匹配筛选,二是全程跟踪指导,结合真实案例开始跟我介绍。比如女生这个,真的是拿了五六份女生的资料,就跟简历似的,问我有没有眼缘,又跟我说这几天来了什么什么姑娘,有还没毕业的,有点子真实案例掺点焦虑的感觉。比如全程指导,给我看了她手下成功的案例,放了点截图和视频啥的,让我看效果。


中间插一下哈,聊聊我对红娘的看法,真的很强。能干这行的都是社牛,还有很强的气场,我确实能感觉出来她是很快乐地在撮合,觉得是在做正确的事,收的钱理所应当。说真的,要不是我去之前就抱着分文不出的想法,她一顿吹我真给了,业务能力太强了。她聊天的话,目的性很强,总是往婚介服务,这个筛选方向去带,会弱化你的劣势,不会很明显地贬低你,但也不会特别夸耀你的长处,就把你当作一个朋友一样,所有都是围绕我们能给你找一个天造地设的对象这一核心点展开。说实在的,动不动认个亲弟弟这操作也太骚了,见面第一次,这社交强度直接拉满,很热情。


聊聊收费啥的,好像每个人都不一样,给我的标准是1年49990,半年29990,3个月19990,后面我说不用了,拉扯半小时,认我做亲弟弟,然后给我亲属价对赌协议,先交10100等成功了再补9890或者给她介绍2个就不用给尾款了。最后博主一是领导在催干活,嘎嘎电话,朋友在催吃饭,也微信电话不停,就想润了,红娘就说要不先交500定金,不行再退,我确实没这想法,就发了50红包给姐买奶茶。反正这吃相一出来就有点尬住了,我这确实是很好说话,没有一甩脸子直接走了,就这么耗着。说实在的,我确实觉得人家说了3小时,这总得有点收益,50块钱在北京不是什么大钱,但是我本来也是抱着聊聊的想法,没想到上来就这么狠,直接2W起步,真恐怖,润了润了。


又到了白色相簿的季节


最后聊聊博主的收获,有用信息不多哈,大部分是一些套话,这大概就是免费内容吧。她给我看了客户的跟踪记录,红娘会全程指导男生的行为,会推一些书或者情感内容啥的,当然,这都是收费内容。怎么说呢,这一次三小时的激烈battle,我也能感觉出来,我还是该在情商上做一些提升,红娘有提到男女之间的情商和朋友之间的情商是两码事,说我上一次挂了就是技巧不足,竟然半年连嘴都没亲,这还是人???跟我说她们那里的都是一个月速通,我这样的已经没救了,好好好,这年头纯爱战士就是鲨BEE。


我其实对这家婚介所没啥意见,我也是想着既然聊上了那就去看看呗,没想到第一次这强度就这么大,哈哈,离谱。我看了下女生资源都很不错,男生也很厉害,都是人中龙凤,不知真假。XX爱恋,她跟我说主要还是线下,我的资料是从B站过去的,就离谱,不知道我填啥了,一开始就知道我的性别年龄和手机号,其他一概不知,可能是我资料被卖了吧,根据年龄给我推送的。喔,对了,说是每周给政府或者国企办公益相亲会,我之前倒是没怎么了解,不知道她家是不是在北京属于厉害的了,还是纯诈骗,摸不清楚,这也是我没交钱的理由之一。因为我在B站上看过XX说媒嘛,我就问她,怎么不开个自媒体号吐吐槽,还能挣点外快。哈哈,这姐非常真实,说剪辑、化妆、文案太累了,线下就够赚了,没必要混线上。


写着写着,突然想起来这姐还说过我一个坏毛病,说我没有底线,有点舔狗,女生一般不喜欢姿态放低的男生,希望我有点神秘感,或者说装一点。哈哈,下一个更好,咱们有的是资源,看得出姐是相当洒脱,成年人不改变只选择这话倒是诠释的清清楚楚。还说我应该有个底,不要一上来就跟人托底,比如她一看我就是个逗比的人,结果跟我一聊还真是个嘻嘻哈哈的乐天派,这样就没有神秘感,她说女生一般不太喜欢这种。


最后的最后,她把经理叫来了,做个回访,我是很欣赏红娘老师的,因为我这次没谈成业绩我也是相当抱歉,所以当着经理面说了很多红娘的好话,没有谈成也确实是我的问题。哈哈,经理直接问我,免费给我介绍同意不,我都懵了,这太狠了,我也没答应,这种形式还是太超前了。相亲说实话还是摆条件,看见女生资料搁面前一遍遍刷,人中龙凤太多啦,哈哈,不由得想起了我初恋也就是我前对象,我也得好好努力呀!


锵锵!我闪亮登场!


今天突然想到,好像真是在B站一个UP那里填过资料的,是我错怪人家了。红娘这姐姐挺有意思的,我是真的觉得她干这行很快乐,她还问我对这行有没有兴趣,哈哈。这种撮合有情人就能收钱的事可太行了,我也蛮喜欢这种做好事的感觉。不过骗人的也不少,315曝光过了,大家也长个心眼吧,学会识人。


最后做个总结吧,博主从昨年开始琢磨男女大事,经历了人生第一次相亲会、初恋、婚介所,虽然都只有一次,但都是挺有意思的。哎,母胎SOLO真的太难了,不会谈啊,盲目冲锋的纯爱战士真的不是版本答案了,红娘老师也跟我说,女生感情上比男生早熟三四年以上,难难难,但是博主依旧对生活抱有期望,哈哈,毕竟我是个乐子人。


我的人生信条是,我要让这痛苦压抑的世界绽放幸福快乐之花,向美好的世界献上祝福!!!


作者:云雨雪
来源:juejin.cn/post/7350123500936986674
收起阅读 »

祖传屎山代码平时不优化,一重构就翻天覆地

写作背景 写背景之前先放一张网图,侵删。 有一个活跃应用包含了2个相似业务场景,所以共用了底层模型。 前期在开发过程中,强行将两波研发组正在研发的产品底层模型和能力统一。底层能力统一遇到了挺多问题,比如数据库字段适配、转换、冗余;repo 层 SQL...
继续阅读 »



写作背景



写背景之前先放一张网图,侵删。



image.png


有一个活跃应用包含了2个相似业务场景,所以共用了底层模型。




  1. 前期在开发过程中,强行将两波研发组正在研发的产品底层模型和能力统一。底层能力统一遇到了挺多问题,比如数据库字段适配、转换、冗余;repo 层 SQL 条件拼接用了大量 if else,导致建索引困难…等等。

  2. 一些历史原因,该应用经手了十多个研发,代码是垒了又垒,出现一个很有规律的现象,大家都是只增代码不减代码。

  3. 代码性能随着数据规模增加不断降低,靠着优化补丁缝缝补补支撑着,业务高峰期经常被运维同学拿着 SQL 光顾。



重构该项目想法不止10次,想逮着机会拉着各方大佬商讨重构事项,因为重构对业务是没有收益的,并且重构难度相当大,所以迟迟没有下定决心。


最近刚好产品需要打磨下一个版本,需要挺长时间,几个后端研发商讨要不重构吧。嗯,我想可以,于是我找上前端负责人沟通拉他入伙,找上前端之前测试已经同意了。


于是一场重构拉开序幕了。




  1. A 同学负责梳理和收敛模型、数据订正、向上提供能力。

  2. B 同学负责梳理前端接口,编排底层能力,提供原子接口给前端(最复杂,直接面向Web端业务,接口有很多特殊逻辑)。

  3. C 同学负责引擎层,和一些计算类逻辑,另外就是打打杂。



重构


大型重构耗时不说还费人力,搞不好重构完你拿不到业务结果,所以重构前你要明确收益是啥?无非就是下面几种




  1. 性能提升产品体验更好;

  2. 简化架构并提升架构扩展性(后面迭代基于重构后架构能快速开发上线);

  3. 历史债清理,历史代码可读性差维护费劲(大部分程序员看别人代码都是这样吧)。



我们是三种情况都中,下面简单总结重构的思路吧。


模型梳理和能力收敛


底层模型我认为最重要,要可靠、稳定且变动少,如果在迭代中你的模型变来变去,上层业务根本开发不了或者边开发边改,到项目收尾就是另一坨屎山。


上层业务是根据底层模型长出来的,所以一定要跟产品讨论确定最终模型,若有好的竞品参考更好了,你的设计可能会看的更远,以防过度设计,架构设计满足未来1-2年迭代即可。


模型设计需要预估数据规模,数据规模决定是否采用分库分表/分区表。如果不好预估采用简单原则先上线看看业务效果,但基础框架这些能力一定要预留好,上量后能快速开发上线。


底层模型和能力收敛了,上层业务编排对能力的复用性更高。ps:这次模型梳理我们干掉了 2 张千万级表。


数据订正


模型梳理和收敛一般会涉及数据订正,特指线上模型对应的数据割接在新模型。一般会有下面几种方式




  1. 写脚本从数据库捞数据订正数据,一般会先 select 查询到内存中,重新组装数据再 insert 新模型。数据量小场景完全可行,但数据量大是跑不动(已踩过坑,线上数据几天没跑完最后发布失败)。

  2. 写 SQL 直接操作数据表,简单的数据处理场景、数据量小场景可行,数据量大场景不可靠,容易超时并且会有数据库稳定性风险,另外订正逻辑复杂是搞不定的(已踩过坑,线上跑数据失败导致发布失败)。

  3. oplog、binlog.... 日志采集同步到消息队列(kafka、pulsar 等),启消费组消费订正数据(我最常用也是最可靠的),数据量大的场景特别爽,处理存量数据的同时还能保证增量数据同步处理。



数据订正是清理过期数据最佳时期。假若平台过期数据体量大,这部分数据不迁移新表,留在历史表中当备份就行,亦可快速恢复。ps:本次重构过期数据预估是千万级别。


API 接口


接口是你对外的门面,应该提前规划明确,不能新增需求就干一个接口,需求迭代到后期,大大小小接口加起来几十上百个维护成本是很高的。


我们一般会按照下面几个原则:




  1. 按操作分类比如:增、删、改、查是一类,只会定义4个接口上游业务方调用需传入 source 区分调用源。

  2. 接口保持简洁,不耦合非当前业务的复杂数据。比如业务上需要回显组织架构数据(员工名称、部门、员工上级等),这类数据需要业务方自行编排组织架构 byids 接口。

  3. 接口具备降级能力,不能因为接口内部编排的非重要接口、逻辑报错导致整个接口不可用。
    降级指将某些业务或者接口的功能降低,可以是只提供部分功能,也可以是完全停掉所有功能。



这次重构 B 同学和前端面临了巨大压力,接口多、混乱、逻辑不清晰,决定梳理业务逻辑按照上面 3 个原则重写接口。


代码重构技巧


代码重构也是本次重构重点,经过长时间迭代已经闻到了坏代码味道。怎么重构早就心中有数,很早就盘点和推演了,下面是我常用的一些重构技巧,这些技巧都是非常经典的,如果看过「重构改善既有代码设计」应该都不陌生。


内联临时变量

项目里有一些临时变量,只被简单赋值了一次。将这些临时变量的赋值语句直接嵌入到使用它们的地方,而不是创建一个新的变量来存储这个临时值。


func Publish() error {
// ... 省略一部分代码
err = Producer(context.TODO()).ProducerOne(&obj)
if err != nil {
return err
}
return nil
}

临时变量内联改造后👇👇👇


func Publish() error {
// ... 省略一部分代码
return Producer(context.TODO()).ProducerOne(&obj)
}

魔幻数字"(Magic Number)

指代码中使用未经解释或定义的常数值,这些值通常没有命名并且没有给出其含义或用途。这样的数字使代码难理解和维护,项目里面很多魔幻数字使用。


func Update(ids []string, nodeID string) {
// ... 省略一部分代码
Report(context.TODO(), nodeID, "6", ids)
}

要解决魔幻数字比较简单,只需你把业务逻辑理解定义成枚举就可以了,这个数字6表示朋友圈类型,魔幻数字改造后👇👇👇


type TargetType int

const (
QWMoment TargetType = 6
)

func Update(ids []string, nodeID string) {
// ... 省略一部分代码
Report(context.TODO(), nodeID, QWMoment, ids)
}

删除注释、未引用代码「俗称死代码」

根据我 review 代码的经验,不少研发同学会把已注释、未引用代码保留,这部分代码是非常影响后面维护者思路的,我们在重构过程中遇到不少这类代码,来来回回找测试和研发确认为什么会保留,哪些业务常用在用?带来了不小负担。(尤其是越上层的死代码引用了一堆下层代码,比如controller 引用 service,service 再引用 repo ,若重写 repo 非常上头)


所以我强烈建议,一旦代码不用了,应该立刻删除。若删除这部分代码后面迭代可能会使用,我建议重新开发。我列一些删除代码后的收益。




  1. 清晰度和简洁性;

  2. 减少维护成本;

  3. 减少冗余和混乱;

  4. 避免误导。



卫语句取代条件表达式

卫语是用来提前结束方法执行的结构。通常情况下,卫语句用来检查某些前置条件是否满足,如果条件不满足,则立即退出方法执行,以避免进入后续的代码块。有助于减少代码嵌套深度,增加代码的可读性和可维护性。


按照我的经验,卫语句应该有下面 2 种情况:




  1. 两个条件分支都属于正常行为;

  2. 有一个条件分支是正常行为,另一个分支则是异常的情况。



我们review过的代码一般是第二种情况比较严重。


func Recall(exclusion constant.ExclusionType)error  {
if exclusion == constant.OnlyOneExec {
if detail.TargetID == "" {
return nil
}
_, err := repo.Update(ctx,....)
if err != nil {
return xerrors.Wrapf(err, "Update")
}
}
return nil
}

调整后代码👇👇👇


func Recall(exclusion constant.ExclusionType)error  {
if exclusion != constant.OnlyOneExec {
return nil
}

if detail.TargetID == "" {
return nil
}
_, err := repo.Update(ctx,....)
return err
}

变量改名

好的命名能让读者一目了然,变量名可以很好的解释一段代码干了什么。我发现项目里面很多字段名、类名、包名很模糊,很难理解具体的业务(包括我自己也经常命名错)。


下段代码是我整理的坏的命名


TaskCommand 是 Kafka 消费者依赖的实体,收到消息后根据 Type 和 Status 撤回数据。但你看 struct 名 跟撤回没有任何关系。


// TaskCommand 任务相关命令
type TaskCommand struct {
Type   int8 `json:"type"`   // 执行类型
Status int8 `json:"status"` 
}

所以我选择把 TaskCommand 替换成跟业务更贴切的名称👇👇👇


type RecallDataParam struct {
Type   int8 `json:"type"` // 执行类型
Status int8 `json:"status"`
}

引入参数对象

以一个对象取代一些参数,可以改善代码的可读性和维护性,尤其是在函数参数列表较长或者参数之间存在复杂关系的情况下。将一组相关的参数封装到一个对象中,将该对象作为函数的参数传递,简化函数签名并提高代码的清晰度。


在一些历史比较久的代码里过长参数真的很常见,从 controller 透传到 service 再透传到 repo 层,代码复用性也非常低。


type AppImpl struct {
}

func (app *AppImpl) List(tp []int, status, page, pageSize int, keyword string, domain string) ([]interface{}, error) {
// .... 省略业务逻辑
return nil, nil
}

上段代码我一般会在 controller 和 service 中间抽一个 dto 实体。👇👇👇


type AppImpl struct {
}

func (app *AppImpl) List(listDTO *ListDTO) ([]interface{}, error) {
// .... 省略业务逻辑
return nil, nil
}

type ListDTO struct {
tp []int
status, page, pageSize int
keyword, domain string
}

提炼类

一个类应该是一个明确的抽象,它的职责是单一的,只处理一些明确的职责。


提炼类一般是下面两种情况




  1. 需求是在不停变化和累加,你会这儿加一个函数,那儿加一个方法。导致某些文件或者类非常臃肿。

  2. 相似的能力,散落在不同的业务板块,涉及的开发都在重复建设,有一个需求建一个烟囱。



典型案例是项目中事件上报能力,本应该是一个通用能力集中收敛上报代码,据我梳理代码散落在多处,上报触点有 10 来个,每个触点都在写同样的上报代码,假设某一天上报逻辑变化必须在这 10 多处做出许多小修改。


所以我决定把上报能力收敛在一个类,将复杂逻辑封装到该类,定义有限参数露出给使用方。


上报通用能力封装在 EventTracking。👇👇👇


// Tracker 埋点上报接口
type Tracker[T any] interface {
EventTracking(in T) error
}

type CMSReachDTO struct {
}

type CMSReachTracking[T any] struct {
ctx context.Context
}

func NewCMSReachTracking[T any](ctx context.Context) Tracker[*CMSReachDTO] {
return &CMSReachTracking[T]{ctx: ctx}
}

func (t *CMSReachTracking[T]) EventTracking(in *CMSReachDTO) error {
// ....逻辑省略

return nil
}

提炼超类

如果两个类在做相似的事,可以利用基本的继承/组合(GO 只有组合)机制把它们的相似之处提炼到超类。一般会把字段、方法都搬移过去。


我遇到的 case 在 entity 上会多一些,比如下面这两个 struct。


type Task struct {
ID string `gorm:"column:id"`
Tenant string `gorm:"column:tenant"`
SubDomain string `gorm:"column:sub_domain"`
IsDel int8   `gorm:"column:is_del;default:1" `
CreateAt int64  `gorm:"column:create_at;autoCreateTime:milli"`
UpdateAt int64  `gorm:"column:update_at;autoUpdateTime:milli"`
CreateUserID string `gorm:"column:create_uid"`
UpdateUserID string `gorm:"column:update_uid"`
// ... 省略其他字段
}

type TaskDetail struct {
ID string `gorm:"column:id"`
TargetID string `gorm:"column:target_id"`
Tenant string `gorm:"column:tenant"`
SubDomain string `gorm:"column:sub_domain"`
IsDel int8   `gorm:"column:is_del;default:1" `
CreateAt int64  `gorm:"column:create_at;autoCreateTime:milli"`
UpdateAt int64  `gorm:"column:update_at;autoUpdateTime:milli"`
CreateUserID string `gorm:"column:create_uid"`
UpdateUserID string `gorm:"column:update_uid"`
// ... 省略其他字段
}

上面这段代码他们都有共性的代码,并且我非常熟悉业务是不可能更改的,所以我会提炼一个超类。👇👇👇


type SuperParty struct {
Tenant string `gorm:"column:tenant"`
SubDomain string `gorm:"column:sub_domain"`
IsDel int8   `gorm:"column:is_del;default:1" `
CreateAt int64  `gorm:"column:create_at;autoCreateTime:milli"`
UpdateAt int64  `gorm:"column:update_at;autoUpdateTime:milli"`
CreateUserID string `gorm:"column:create_uid"`
UpdateUserID string `gorm:"column:update_uid"`
}

type Task struct {
ID string `gorm:"column:id"`
SuperParty
// ... 省略其他字段
}

type TaskDetail struct {
ID string `gorm:"column:id"`
TargetID string `gorm:"column:target_id"`
SuperParty
// ... 省略其他字段
}

当然某些场景还有一些配套的方法,也可以一并搬迁到 SuperParty 里面。


提炼方法/函数

提炼函数/方法是我常用的一种手段,我不喜欢长函数/方法。我看过一个说法,一个函数/方法应该能在一屏中显示,我一直奉为经典语录(我写的代码函数/方法基本不会超过一百行);另外只要有一段代码不止被用一次,我就会把他们单独放进一个函数。


有这样一个场景,调用外部 byids 查询员工信息获取 externalId 执行业务逻辑,封装外部接口调用。


type Client struct {
ctx context.Context
}

func (c *Client) GetByIDs(id []string) ([]*User, error) {
// ....省略业务逻辑
return []*User{}, nil
}

type User struct {
ID         string `json:"id"`
ExternalID string `json:"externalId"`
}

下面是业务方使用 GetByIDs() 方法


func TestGetUserByIDs(t *testing.T) {
ids := []string{"1""2"}

client := &Client{}
users, err := client.GetByIDs(ids)
if err != nil {
panic(err)
}

l := make([]string0len(users))
for _, v := range users {
l = append(l, v.ExternalID)
}

// ...执行业务逻辑
}

 业务方调用 GetByIDs() 方法,遍历 users 获取 ExternalID 执行业务逻辑。在业务上这种操作还真不少,所以决定优化复用一部分代码。👇👇👇




  1. 定义 Users 切片。

  2. GetByIDs() 方法返回 Users。

  3. Users 提供 GetExternalIDs 方法。



type Client struct {
ctx context.Context
}

func (c *Client) GetByIDs(id []string) (Users, error) {
// .... 省略业务逻辑
return Users{}, nil
}

type Users []*User

func (u Users) GetExternalIDs() []string {
out := make([]string0len(u))
for _, v := range u {
out = append(out, v.ExternalID)
}

return out
}

type User struct {
ID         string `json:"id"`
ExternalID string `json:"externalId"`
}

下面是业务方使用 GetByIDs() 方法


func TestGetUserByIDs(t *testing.T) {
ids := []string{"1""2"}

client := &Client{}
users, err := client.GetByIDs(ids)
if err != nil {
panic(err)
}

l := users.GetExternalIDs()
// 执行业务逻辑
   // ....
}

代码空行

我非常非常不喜欢代码从头到尾写下来没有任何空行,难以阅读让读者很难提起兴趣。空行在我看来是必不可少的,在代码中使用空行来分隔不同功能或逻辑块之间的代码,空行使得代码更易读。


下面段代码是没有任何空行的,代码比较短阅读起来可能并不费劲。
image.png


适当进行空行优化后👇👇👇
企业微信截图_25861264-b69d-48eb-bd5c-eb2e4e9f87aa.png


上段代码先不关注逻辑,优化后可读性更强了,代码分为3段逻辑,每段逻辑都有各自的职责。


空行是用来区分不同逻辑块的,过度空行也会影响代码阅读,如下:
image.png


引入设计模式

设计模式是被大佬们验证过的、开发经验的总结,可以帮助我们更好地组织和管理代码,并提高代码的可维护性、可读性、可扩展性和可重用性。下面链接是我最常用的设计模式,也在这次重构过程中全部用上了,有兴趣可以看看。


最后总结




  1. 如果你的项目不是外包项目(交付了就完事儿),一定要多回头看看自己写的代码,跟着版本迭代持续优化和改进,你才能进步。另外对代码一定要有洁癖。

  2. 重构是持续的过程,如果是重要项目,每个版本我们都会推进代码优化,保证代码可维护性、可扩展性、另外就是高性能。千万别堆积最后,那可是大工程到后面很多人是没有决心干这个事儿的,所以大家应该平时迭代中不断优化和完善,才可持续性。

  3. 大型重构时,一定要明确收益并且是可量化的,比如重构后 qps 提升了10%,应用消耗资源降低了…等等,你才有跟老板谈判的筹码。



作者:彭亚川Allen
来源:juejin.cn/post/7344290391989485578
收起阅读 »

2024年的安卓现代开发

大家好 👋🏻, 新的一年即将开始, 我不想错过这个机会, 与大家分享一篇题为《2024年的安卓现代开发》的文章. 🚀 如果你错过了我的上一篇文章, 可以看看2023年的安卓现代开发并回顾一下今年的变化. 免责声明 📝 本文反映了我的个人观点和专业见解, 并参考...
继续阅读 »


大家好 👋🏻, 新的一年即将开始, 我不想错过这个机会, 与大家分享一篇题为《2024年的安卓现代开发》的文章. 🚀


如果你错过了我的上一篇文章, 可以看看2023年的安卓现代开发并回顾一下今年的变化.


免责声明


📝 本文反映了我的个人观点和专业见解, 并参考了 Android 开发者社区中的不同观点. 此外, 我还定期查看 Google 为 Android 提供的指南.


🚨 需要强调的是, 虽然我可能没有明确提及某些引人注目的工具, 模式和体系结构, 但这并不影响它们作为开发 Android 应用的宝贵替代方案的潜力.


Kotlin 无处不在 ❤️



Kotlin是由JetBrains开发的一种编程语言. 由谷歌推荐, 谷歌于 2017 年 5 月正式发布(查看这里). 它是一种与 Java 兼容的现代编程语言, 可以在 JVM 上运行, 这使得它在 Android 应用开发中的采用非常迅速.


无论你是不是安卓新手, 都应该把 Kotlin 作为首选, 不要逆流而上 🏊🏻 😎, 谷歌在 2019 年谷歌 I/O 大会上宣布了这一做法. 有了 Kotlin, 你就能使用现代语言的所有功能, 包括协程的强大功能和使用为 Android 生态系统开发的现代库.


请查看Kotlin 官方文档


Kotlin 是一门多用途语言, 我们不仅可以用它来开发 Android 应用, 尽管它的流行很大程度上是由于 Android 应用, 我们可以从下图中看到这一点.




KotlinConf ‘23


Kotlin 2.0 要来了


另一个需要强调的重要事件是Kotlin 2.0的发布, 它近在眼前. 截至本文发稿之日, 其版本为 2.0.0-beta4



新的 K2 编译器 也是 Kotlin 2.0 的另一个新增功能, 它将带来显著的性能提升, 加速新语言特性的开发, 统一 Kotlin 支持的所有平台, 并为多平台项目提供更好的架构.


请查看 KotlinConf '23 的回顾, 你可以找到更多信息.


Compose 🚀




Jetpack Compose 是 Android 推荐的用于构建本地 UI 的现代工具包. 它简化并加速了 Android 上的 UI 开发. 通过更少的代码, 强大的工具和直观的 Kotlin API, 快速实现你的应用.




Jetpack Compose 是 Android Jetpack 库的一部分, 使用 Kotlin 编程语言轻松创建本地UI. 此外, 它还与 LiveData 和 ViewModel 等其他 Android Jetpack 库集成, 使得构建具有强反应性和可维护性的 Android 应用变得更加容易.


Jetpack Compose 的一些主要功能包括



  1. 声明式UI

  2. 可定制的小部件

  3. 与现有代码(旧视图系统)轻松集成

  4. 实时预览

  5. 改进的性能.


资源:



Android Jetpack ⚙️




Jetpack是一套帮助开发者遵循最佳实践, 减少模板代码, 编写在不同Android版本和设备上一致运行的代码的库, 这样开发者就可以专注于他们的业务代码.


_ Android Jetpack 文档



其中最常用的工具有:



Material You / Material Design 🥰



Material You 是在 Android 12 中引入并在 Material Design 3 中实现的一项新的定制功能, 它使用户能够根据个人喜好定制操作系统的视觉外观. Material Design 是一个由指南, 组件和工具组成的可调整系统, 旨在坚持UI设计的最高标准. 在开源代码的支持下, Material Design 促进了设计师和开发人员之间的无缝协作, 使团队能够高效地创造出令人惊叹的产品.



目前, Material Design 的最新版本是 3, 你可以在这里查看更多信息. 此外, 你还可以利用Material Theme Builder来帮助你定义应用的主题.


代码仓库


使用 Material 3 创建主题


SplashScreen API



Android 中的SplashScreen API对于确保应用在 Android 12 及更高版本上正确显示至关重要. 不更新会影响应用的启动体验. 为了在最新版本的操作系统上获得一致的用户体验, 快速采用该 API 至关重要.


Clean架构



Clean架构的概念是由Robert C. Martin提出的. 它的基础是通过将软件划分为不同层次来实现责任分离.


特点



  1. 独立于框架.

  2. 可测试.

  3. 独立于UI

  4. 独立于数据库

  5. 独立于任何外部机构.


依赖规则


作者在他的博文Clean代码中很好地描述了依赖规则.



依赖规则是使这一架构得以运行的首要规则. 这条规则规定, 源代码的依赖关系只能指向内部. 内圈中的任何东西都不能知道外圈中的任何东西. 特别是, 外圈中声明的东西的名称不能被内圈中的代码提及. 这包括函数, 类, 变量或任何其他命名的软件实体.




安卓中的Clean架构:


Presentation 层: Activitiy, Composable, Fragment, ViewModel和其他视图组件.
Domain层: 用例, 实体, 仓库接口和其他Domain组件.
Data层: 仓库的实现类, Mapper, DTO 等.


Presentation层的架构模式


架构模式是一种更高层次的策略, 旨在帮助设计软件架构, 其特点是在一个可重复使用的框架内为常见的架构问题提供解决方案. 架构模式与设计模式类似, 但它们的规模更大, 解决的问题也更全面, 如系统的整体结构, 组件之间的关系以及数据管理的方式等.


在Presentation层中, 我们有一些架构模式, 我想重点介绍以下几种:



  • MVVM

  • MVI


我不想逐一解释, 因为在互联网上你可以找到太多相关信息. 😅


此外, 你还可以查看应用架构指南.



依赖注入


依赖注入是一种软件设计模式, 它允许客户端从外部获取依赖关系, 而不是自己创建依赖关系. 它是一种在对象及其依赖关系之间实现控制反转(IoC)的技术.



模块化


模块化是一种软件设计技术, 可将应用划分为独立的模块, 每个模块都有自己的功能和职责.



模块化的优势


可重用性: 拥有独立的模块, 就可以在应用的不同部分甚至其他应用中重复使用.


严格的可见性控制: 模块可以让你轻松控制向代码库其他部分公开的内容.


自定义交付: Play特性交付 使用应用Bundle的高级功能, 允许你有条件或按需交付应用的某些功能.


可扩展性: 通过独立模块, 可以添加或删除功能, 而不会影响应用的其他部分.


易于维护: 将应用划分为独立的模块, 每个模块都有自己的功能和责任, 这样就更容易理解和维护代码.


易于测试: 有了独立的模块, 就可以对它们进行隔离测试, 从而便于发现和修复错误.


改进架构: 模块化有助于改进应用的架构, 从而更好地组织和结构代码.


改善协作: 通过独立的模块, 开发人员可以不受干扰地同时开发应用的不同部分.


构建时间: 一些 Gradle 功能, 如增量构建, 构建缓存或并行构建, 可以利用模块化提高构建性能.


更多信息请参阅官方文档.


网络



序列化


在本节中, 我想提及两个我认为非常重要的工具: 与 Retrofit 广泛结合使用的Moshi, 以及 JetBrains 的 Kotlin 团队押宝的Kotlin Serialization.



MoshiKotlin Serialization 是用于 Kotlin/Java 的两个序列化/反序列化库, 可将对象转换为 JSON 或其他序列化格式, 反之亦然. 这两个库都提供了友好的接口, 并针对移动和桌面应用进行了优化. Moshi 主要侧重于 JSON 序列化, 而 Kotlin Serialization 则支持包括 JSON 在内的多种序列化格式.


图像加载



要从互联网上加载图片, 有几个第三方库可以帮助你处理这一过程. 图片加载库为你做了很多繁重的工作; 它们既处理缓存(这样你就不用多次下载图片), 也处理下载图片并将其显示在屏幕上的网络逻辑.


_ 官方安卓文档




响应/线程管理




说到响应式编程和异步进程, Kotlin协程凭借其suspend函数和Flow脱颖而出. 然而, 在 Android 应用开发中, 承认 RxJava 的价值也是至关重要的. 尽管协程和 Flow 的采用率越来越高, 但 RxJava 仍然是多个项目中稳健而受欢迎的选择.


对于新项目, 请始终选择Kotlin协程❤️. 可以在这里探索一些Kotlin协程相关的概念.


本地存储


在构建移动应用时, 重要的一点是能够在本地持久化数据, 例如一些会话数据或缓存数据等. 根据应用的需求选择合适的存储选项非常重要. 我们可以存储键值等非结构化数据, 也可以存储数据库等结构化数据. 请记住, 这一点并没有提到我们可用的所有本地存储类型(如文件存储), 只是提到了允许我们保存数据的工具.



建议:



测试 🕵🏼



软件开发中的测试对于确保产品质量至关重要. 它能检测错误, 验证需求并确保客户满意. 下面是一些最常用的测试工具::



工具文档的测试部分


截屏测试 📸



Android 中的截屏测试涉及自动捕获应用中各种 UI 元素的屏幕截图, 并将其与基线图像进行比较, 以检测任何意外的视觉变化. 它有助于确保不同版本和配置的应用具有一致的UI外观, 并在开发过程的早期捕捉视觉回归.



R8 优化


R8 是默认的编译器, 可将项目的 Java 字节码转换为可在 Android 平台上运行的 DEX 格式. 通过缩短类名及其属性, 它可以帮助我们混淆和减少应用的代码, 从而消除项目中未使用的代码和资源. 要了解更多信息, 请查看有关 缩减, 混淆和优化应用 的 Android 文档. 此外, 你还可以通过ProGuard规则文件禁用某些任务或自定义 R8 的行为.




  • 代码缩减

  • 缩减资源

  • 混淆

  • 优化


第三方工具



  • DexGuard


Play 特性交付





Google Play 的应用服务模式称为动态交付, 它使用 Android 应用Bundle为每个用户的设备配置生成并提供优化的 APK, 因此用户只需下载运行应用所需的代码和资源.




自适应布局



随着具有不同外形尺寸的移动设备使用量的增长, 我们需要一些工具, 使我们的 Android 应用能够适应不同类型的屏幕. 因此, Android 为我们提供了Window Size Class, 简单地说, 就是三大类屏幕格式, 它们是我们开发设计的关键点. 这样, 我们就可以避免考虑许多屏幕设计的复杂性, 从而将可能性减少到三组, 它们是 Compat, MediumExpanded.


Window Size Class




支持不同的屏幕尺寸


我们拥有的另一个重要资源是Canonical Layout, 它是预定义的屏幕设计, 可用于 Android 应用中的大多数场景, 还为我们提供了如何将其适用于大屏幕的指南.



其他相关资源



Form-Factor培训


本地化 🌎



本地化包括调整产品以满足不同地区不同受众的需求. 这包括翻译文本, 调整格式和考虑文化因素. 其优势包括进入全球市场, 增强用户体验, 提高客户满意度, 增强在全球市场的竞争力以及遵守当地法规.


注: BCP 47 是安卓系统使用的国际化标准.


参考资料



性能 🔋⚙️



在开发 Android 应用时, 我们必须确保用户体验更好, 这不仅体现在应用的开始阶段, 还体现在整个执行过程中. 因此, 我们必须使用一些工具, 对可能影响应用性能的情况进行预防性分析和持续监控:



应用内更新



当你的用户在他们的设备上不断更新你的应用时, 他们可以尝试新功能, 并从性能改进和错误修复中获益. 虽然有些用户会在设备连接到未计量的连接时启用后台更新, 但其他用户可能需要提醒才能安装更新. 应用内更新是 Google Play 核心库的一项功能, 可提示活跃用户更新你的应用*.


运行 Android 5.0(API 等级 21)或更高版本的设备支持应用内更新功能. 此外, 应用内更新仅支持 Android 移动设备, Android 平板电脑和 Chrome OS 设备.


- 应用内更新文档




应用内评论



Google Play 应用内评论 API 可让你提示用户提交 Play Store 评级和评论, 而无需离开你的应用或游戏.


一般来说, 应用内评论流程可在用户使用应用的整个过程中随时触发. 在流程中, 用户可以使用 1-5 星系统对你的应用进行评分, 并添加可选评论. 一旦提交, 评论将被发送到 Play Store 并最终显示出来.


*为保护用户隐私并避免 API 被滥用, 你的应用应严格遵守有关何时请求应用内评论评论提示的设计的规定.


- 应用内评论文档




可观察性 👀



在竞争日益激烈的应用生态系统中, 要获得良好的用户体验, 首先要确保应用没有错误. 确保应用无错误的最佳方法之一是在问题出现时立即发现并知道如何着手解决. 使用 Android Vitals 来确定应用中崩溃和响应速度问题最多的区域. 然后, 利用 Firebase Crashlytics 中的自定义崩溃报告获取更多有关根本原因的详细信息, 以便有效地排除故障.


工具



辅助功能



辅助功能是软件设计和构建中的一项重要功能, 除了改善用户体验外, 还能让有辅助功能需求的人使用应用. 这一概念旨在改善的不能包括: 有视力问题, 色盲, 听力问题, 灵敏性问题和认知障碍等.


考虑因素:



  • 增加文字的可视性(颜色对比度, 可调整文字大小)

  • 使用大而简单的控件

  • 描述每个UI元素


更多详情请查看辅助功能 - Android 文档


安全性 🔐



在开发保护设备完整性, 数据安全性和用户信任的应用时, 安全性是我们必须考虑的一个方面, 甚至是最重要的方面.



  • 使用凭证管理器登录用户: 凭据管理器 是一个 Jetpack API, 在一个单一的 API 中支持多种登录方法, 如用户名和密码, 密码匙和联合登录解决方案(如谷歌登录), 从而简化了开发人员的集成.

  • 加密敏感数据和文件: 使用EncryptedSharedPreferencesEncryptedFile.

  • 应用基于签名的权限: 在你可控制的应用之间共享数据时, 使用基于签名的权限.


android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
name="my_custom_permission_name"
android:protectionLevel="signature" />


  • 不要将密钥, 令牌或应用配置所需的敏感数据直接放在项目库中的文件或类中. 请使用local.properties.

  • 实施 SSL Pinning: 使用 SSL Pinning 进一步确保应用与远程服务器之间的通信安全. 这有助于防止中间人攻击, 并确保只与拥有特定 SSL 证书的受信任服务器进行通信.


res/xml/network_security_config.xml


"1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.comdomain>
<pin-set expiration="2018-01-01">
<pin digest="SHA-256">ReplaceWithYourPinpin>

<pin digest="SHA-256">ReplaceWithYourPinpin>
pin-set>
domain-config>
network-security-config>


  • 实施运行时应用自我保护(RASP): 这是一种在运行时保护应用免受攻击和漏洞的安全技术. RASP 的工作原理是监控应用的行为, 并检测可能预示攻击的可疑活动:



    • 代码混淆.

    • 根检测.

    • 篡改/应用钩子检测.

    • 防止逆向工程攻击.

    • 反调试技术.

    • 虚拟环境检测

    • 应用行为的运行时分析.




想了解更多信息, 请查看安卓应用中的运行时应用自我保护技术(RASP). 还有一些 Android 安全指南.


版本目录


Gradle 提供了一种集中管理项目依赖关系的标准方式, 叫做版本目录; 它在 7.0 版中被试验性地引入, 并在 7.4 版中正式发布.


优点:



  • 对于每个目录, Gradle 都会生成类型安全的访问器, 这样你就可以在 IDE 中通过自动补全轻松添加依赖关系.

  • 每个目录对构建过程中的所有项目都是可见的. 它是声明依赖项版本的中心位置, 并确保该版本的变更适用于每个子项目.

  • 目录可以声明 dependency bundles, 即通常一起使用的 "依赖关系组".

  • 目录可以将依赖项的组和名称与其实际版本分开, 而使用版本引用, 这样就可以在多个依赖项之间共享一个版本声明.


请查看更多信息


Secret Gradle 插件


Google 强烈建议不要将 API key 输入版本控制系统. 相反, 你应该将其存储在本地的 secrets.properties 文件中, 该文件位于项目的根目录下, 但不在版本控制范围内, 然后使用 Secrets Gradle Plugin for Android 来读取 API 密钥.


日志


日志是一种软件工具, 用于记录程序的执行信息, 重要事件, 错误调试信息以及其他有助于诊断问题或了解程序运行情况的信息. 日志可配置为将信息写入不同位置, 如日志文件, 控制台, 数据库, 或将信息发送到日志记录服务器.



Linter / 静态代码分析器



Linter 是一种编程工具, 用于分析程序源代码, 查找代码中的潜在问题或错误. 这些问题可能是语法问题, 代码风格不当, 缺乏文档, 安全问题等, 它们会对代码的质量和可维护性产生影响.



Google Play Instant



Google Play Instant 使本地应用和游戏无需安装即可在运行 Android 5.0(API 等级 21)或更高版本的设备上启动. 你可以使用 Android Studio 构建这类体验, 称为即时应用即时游戏. 通过允许用户运行即时应用或即时游戏(即提供即时体验), 你可以提高应用或游戏的发现率, 从而有助于吸引更多活跃用户或安装.



新设计中心



安卓团队提供了一个新的设计中心, 帮助创建美观, 现代的安卓应用, 这是一个集中的地方, 可以了解安卓在多种形态因素方面的设计.


点击查看新的设计中心


人工智能



GeminiPalM 2是谷歌开发的两个最先进的人工智能(AI)模型, 它们将改变安卓应用开发的格局. 这些模型具有一系列优势, 将推动应用的效率, 用户体验和创新.



人工智能编码助手工具


Studio Bot



Studio Bot 是你的 Android 开发编码助手. 它是 Android Studio 中的一种对话体验, 通过回答 Android 开发问题帮助你提高工作效率. 它采用人工智能技术, 能够理解自然语言, 因此你可以用简单的英语提出开发问题. Studio Bot 可以帮助 Android 开发人员生成代码, 查找相关资源, 学习最佳实践并节省时间.


Studio Bot



Github Copilot


GitHub Copilot 是一个人工智能配对程序员. 你可以使用 GitHub Copilot 直接在编辑器中获取整行或整个函数的建议.


Amazon CodeWhisperer


这是亚马逊的一项服务, 可根据你当前代码的上下文生成代码建议. 它能帮助你编写更高效, 更安全的代码, 并发现新的 API 和工具.


Kotlin Multiplatform 🖥 📱⌚️ 🥰 🚀



最后, 同样重要的是, Kotlin Multiplatform 🎉 是本年度最引人注目的新产品. 它是跨平台应用开发领域的有力竞争者. 虽然我们的主要重点可能是 Android 应用开发, 但 Kotlin Multiplatform 为我们提供了利用 KMP 框架制作完全本地 Android 应用的灵活性. 这一战略举措不仅为我们的项目提供了未来保障, 还为我们提供了必要的基础架构, 以便在我们选择多平台环境时实现无缝过渡. 🚀


如果你想深入了解 Kotlin Multiplatform, 我想与你分享这几篇文章. 在这些文章中, 探讨了这项技术的现状及其对现代软件开发的影响:



作者:bytebeats
来源:juejin.cn/post/7342861726000791603
收起阅读 »

H5唤起APP路子

web
前一段时间在做一些H5页面,需求中落地页占比较大,落地页承担的职责就是引流。引流有两种形式,同时也是我们对唤端的定义:引导已下载用户打开APP,引导未下载用户下载APP。 引导已下载用户打开APP,从数据上说用户停留在APP中的时间更多了,是在提高用户粘性;从...
继续阅读 »

前一段时间在做一些H5页面,需求中落地页占比较大,落地页承担的职责就是引流。引流有两种形式,同时也是我们对唤端的定义:引导已下载用户打开APP,引导未下载用户下载APP。


引导已下载用户打开APP,从数据上说用户停留在APP中的时间更多了,是在提高用户粘性;从体验上说,APP体验是要比H5好的。引导未下载用户下载APP,可以增加我们的用户量。


上面其实分别解释了 什么是唤端 以及 为什么要唤端,也就是 3W法则 中的 What 和 Why,那么接下来我们就要聊一聊 How 了,也就是 如何唤端


我们先来看看常见的唤端方式以及他们适用的场景:


唤端媒介


URL Scheme


来源


我们的手机上有许多私密信息,联系方式、照片、银彳亍卡信息...我们不希望这些信息可以被手机应用随意获取到,信息泄露的危害甚大。所以,如何保证个人信息在设备所有者知情并允许的情况下被使用,是智能设备的核心安全问题。


对此,苹果使用了名为 沙盒 的机制:应用只能访问它声明可能访问的资源。但沙盒也阻碍了应用间合理的信息共享,某种程度上限制了应用的能力。


因此,我们急需要一个辅助工具来帮助我们实现应用通信, URL Scheme 就是这个工具。


URL Scheme 是什么


我们来看一下 URL 的组成:

[scheme:][//authority][path][?query][#fragment]

我们拿 https://www.baidu.com 来举例,scheme 自然就是 https 了。


就像给服务器资源分配一个 URL,以便我们去访问它一样,我们同样也可以给手机APP分配一个特殊格式的 URL,用来访问这个APP或者这个APP中的某个功能(来实现通信)。APP得有一个标识,好让我们可以定位到它,它就是 URL 的 Scheme 部分。


常用APP的 URL Scheme


APP微信支付宝淘宝微博QQ知乎短信
URL Schemeweixin://alipay://taobao://sinaweibo://mqq://zhihu://sms://

URL Scheme 语法


上面表格中都是最简单的用于打开 APP 的 URL Scheme,下面才是我们常用的 URL Scheme 格式:

     行为(应用的某个功能)    
|
scheme://[path][?query]
| |
应用标识 功能需要的参数

Intent


安卓的原生谷歌浏览器自从 chrome25 版本开始对于唤端功能做了一些变化,URL Scheme 无法再启动Android应用。 例如,通过 iframe 指向 weixin://,即使用户安装了微信也无法打开。所以,APP需要实现谷歌官方提供的 intent: 语法,或者实现让用户通过自定义手势来打开APP,当然这就是题外话了。


Intent 语法

intent:
HOST/URI-path // Optional host
#Intent;
package=[string];
action=[string];
category=[string];
component=[string];
scheme=[string];
end;

如果用户未安装 APP,则会跳转到系统默认商店。当然,如果你想要指定一个唤起失败的跳转地址,添加下面的字符串在 end; 前就可以了:

S.browser_fallback_url=[encoded_full_url]

示例


下面是打开 Zxing 二维码扫描 APP 的 intent。

intent:
//scan/
#Intent;
package=com.google.zxing.client.android;
scheme=zxing;
end;

打开这个 APP ,可以通过如下的方式:

 <a href="intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;S.browser_fallback_url=http%3A%2F%2Fzxing.org;end"> Take a QR code a>

Universal Link


Universal Link 是什么


Universal Link 是苹果在 WWDC2015 上为 iOS9 引入的新功能,通过传统的 HTTP 链接即可打开 APP。如果用户未安装 APP,则会跳转到该链接所对应的页面。


为什么要使用 Universal Link


传统的 Scheme 链接有以下几个痛点:



  • 在 ios 上会有确认弹窗提示用户是否打开,对于用户来说唤端,多出了一步操作。若用户未安装 APP ,也会有一个提示窗,告知我们 “打不开该网页,因为网址无效”

  • 传统 Scheme 跳转无法得知唤端是否成功,Universal Link 唤端失败可以直接打开此链接对应的页面

  • Scheme 在微信、微博、QQ浏览器、手百中都已经被禁止使用,使用 Universal Link 可以避开它们的屏蔽( 截止到 18年8月21日,微信和QQ浏览器已经禁止了 Universal Link,其他主流APP未发现有禁止 )


如何让 APP 支持 Universal Link


有大量的文章会详细的告诉我们如何配置,你也可以去看官方文档,我这里简单的写一个12345。



  1. 拥有一个支持 https 的域名

  2. 开发者中心 ,Identifiers 下 AppIDs 找到自己的 App ID,编辑打开 Associated Domains 服务。

  3. 打开工程配置中的 Associated Domains ,在其中的 Domains 中填入你想支持的域名,必须以 applinks: 为前缀

  4. 配置 apple-app-site-association 文件,文件名必须为 apple-app-site-association不带任何后缀

  5. 上传该文件到你的 HTTPS 服务器的 根目录 或者 .well-known 目录下


Universal Link 配置中的坑


这里放一下我们在配置过程中遇到的坑,当然首先你在配置过程中必须得严格按照上面的要求去做,尤其是加粗的地方。



  1. 跨域问题


    IOS 9.2 以后,必须要触发跨域才能支持 Universal Link 唤端。


    IOS 那边有这样一个判断,如果你要打开的 Universal Link 和 当前页面是同一域名,ios 尊重用户最可能的意图,直接打开链接所对应的页面。如果不在同一域名下,则在你的 APP 中打开链接,也就是执行具体的唤端操作。


  2. Universal Link 是空页面


    Universal Link 本质上是个空页面,如果未安装 APP,Universal Link 被当做普通的页面链接,自然会跳到 404 页面,所以我们需要将它绑定到我们的中转页或者下载页。



如何调用三种唤端媒介


通过前面的介绍,我们可以发现,无论是 URL Scheme 还是 Intent 或者 Universal Link ,他们都算是 URL ,只是 URL Scheme 和 Intent 算是特殊的 URL。所以我们可以拿使用 URL 的方法来使用它们。


iframe

<iframe src="sinaweibo://qrcode">

在只有 URL Scheme 的日子里,iframe 是使用最多的了。因为在未安装 app 的情况下,不会去跳转错误页面。但是 iframe 在各个系统以及各个应用中的兼容问题还是挺多的,不能全部使用 URL Scheme。


a 标签

<a href="intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;end"">扫一扫</a>

前面我们提到 Intent 协议,官方给出的用例使用的就是使用的 a 标签,所以我们跟着一起用就可以了


使用过程中,对于动态生成的 a 标签,使用 dispatch 来模拟触发点击事件,发现很多种 event 传递过去都无效;使用 click() 来模拟触发,部分场景下存在这样的情况,第一次点击过后,回到原先页面,再次点击,点击位置和页面所识别位置有不小的偏移,所以 Intent 协议从 a 标签换成了 window.location。


window.location


URL Scheme 在 ios 9+ 上诸如 safari、UC、QQ浏览器中, iframe 均无法成功唤起 APP,只能通过 window.location 才能成功唤端。


当然,如果我们的 app 支持 Universal Link,ios 9+ 就用不到 URL Scheme 了。而 Universal Link 在使用过程中,我发现在 qq 中,无论是 iframe 导航 还是 a 标签打开 又或者 window.location 都无法成功唤端,一开始我以为是 qq 和微信一样禁止了 Universal Link 唤端的功能,其实不然,百般试验下,通过 top.location 唤端成功了。


判断唤端是否成功


如果唤端失败(APP 未安装),我们总是要做一些处理的,可以是跳转下载页,可以是 ios 下跳转 App Store... 但是Js 并不能提供给我们获取 APP 唤起状态的能力,Android Intent 以及 Universal Link 倒是不用担心,它们俩的自身机制允许它们唤端失败后直接导航至相应的页面,但是 URL Scheme 并不具备这样的能力,所以我们只能通过一些很 hack 的方式来实现 APP 唤起检测功能。

// 一般情况下是 visibilitychange 
const visibilityChangeProperty = getVisibilityChangeProperty();
const timer = setTimeout(() => {
const hidden = isPageHidden();
if (!hidden) {
cb();
}
}, timeout);

if (visibilityChangeProperty) {
document.addEventListener(visibilityChangeProperty, () => {
clearTimeout(timer);
});

return;
}

window.addEventListener('pagehide', () => {
clearTimeout(timer);
});

APP 如果被唤起的话,页面就会进入后台运行,会触发页面的 visibilitychange 事件。如果触发了,则表明页面被成功唤起,及时调用 clearTimeout ,清除页面未隐藏时的失败函数(callback)回调。


当然这个事件是有兼容性的,具体的代码实现时做了事件是否需要添加前缀(比如 -webkit- )的校验。如果都不兼容,我们将使用 pagehide 事件来做兜底处理。


没有完美的方案


透过上面的几个点,我们可以发现,无论是 唤端媒介调用唤端媒介 还是 判断唤端结果 都没有一个十全十美的方法,我们在代码层上能做的只是在确保最常用的场景(比如 微信、微博、手百 等)唤端无误的情况下,最大化的兼容剩余的场景。


好的,我们接下来扯一些代码以外的,让我们的 APP 能够在更多的平台唤起。




  • 微信、微博、手百、QQ浏览器等。


    这些应用能阻止唤端是因为它们直接屏蔽掉了 URL Scheme 。接下来可能就有看官疑惑了,微信中是可以打开大众点评的呀,微博里面可以打开优酷呀,那是如何实现的呢?


    它们都各自维护着一个白名单,如果你的域名在白名单内,那这个域名下所有的页面发起的 URL Scheme 就都会被允许。就像微信,如果你是腾讯的“家属”,你就可以加入白名单了,微信的白名单一般只包含着“家属”,除此外很难申请到白名单资质。但是微博之类的都是可以联系他们的渠道童鞋进行申请的,只是条件各不相同,比如微博的就是在你的 APP 中添加打开微博的入口,三个月内唤起超过 100w 次,就可以加入白名单了。




  • 腾讯应用宝直接打开 APP 的某个功能


    刚刚我们说到,如果你不是微信的家属,那你是很难进入白名单的,所以在安卓中我们一般都是直接打开腾讯应用宝,ios 中 直接打开 App Store。点击腾讯应用宝中的“打开”按钮,可以直接唤起我们的 APP,但是无法打开 APP 中的某个功能(就是无法打开指定页面)。


    腾讯应用宝对外开放了一个叫做 APP Link 的申请,只要你申请了 APP Link,就可以通过在打开应用宝的时候在应用宝地址后面添加上 &android_schema={your_scheme} ,来打开指定的页面了。




开箱即用的callapp-lib


信息量很大!各种问题得自己趟坑验证!内心很崩溃!


不用愁,已经为你准备好了药方,只需照方抓药即可😏 —— npm 包 callapp-lib


你也可以通过 script 直接加载 cdn 文件:

<script src="https://unpkg.com/callapp-lib"></script>

它能在大部分的环境中成功唤端,而且炒鸡简单啊,拿过去就可以用啊,还支持很多扩展功能啊,快来瞅瞅它的 文档 啊~~~


作者:阳光多一些
链接:juejin.cn/post/7348249728939130907
收起阅读 »

Mysql中Varchar(50)和varchar(500)区别是什么?

Mysql中Varchar(50)和varchar(500)区别是什么? 一. 问题描述 我们在设计表结构的时候,设计规范里面有一条如下规则: 对于可变长度的字段,在满足条件的前提下,尽可能使用较短的变长字段长度。 为什么这么规定,我在网上查了一下,主要基...
继续阅读 »

Mysql中Varchar(50)和varchar(500)区别是什么?


一. 问题描述


我们在设计表结构的时候,设计规范里面有一条如下规则:



  • 对于可变长度的字段,在满足条件的前提下,尽可能使用较短的变长字段长度。


为什么这么规定,我在网上查了一下,主要基于两个方面



  • 基于存储空间的考虑

  • 基于性能的考虑


网上说Varchar(50)和varchar(500)存储空间上是一样的,真的是这样吗?


基于性能考虑,是因为过长的字段会影响到查询性能?


本文我将带着这两个问题探讨验证一下


二.验证存储空间区别


1.准备两张表


CREATE TABLE `category_info_varchar_50` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(50) NOT NULL COMMENT '分类名称',
`is_show` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否展示:0 禁用,1启用',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '序号',
`deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_name` (`name`) USING BTREE COMMENT '名称索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分类';

CREATE TABLE `category_info_varchar_500` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(500) NOT NULL COMMENT '分类名称',
`is_show` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否展示:0 禁用,1启用',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '序号',
`deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_name` (`name`) USING BTREE COMMENT '名称索引'
) ENGINE=InnoDB AUTO_INCREMENT=288135 DEFAULT CHARSET=utf8mb4 COMMENT='分类';

2.准备数据


给每张表插入相同的数据,为了凸显不同,插入100万条数据


DELIMITER $$
CREATE PROCEDURE batchInsertData(IN total INT)
BEGIN
DECLARE start_idx INT DEFAULT 1;
DECLARE end_idx INT;
DECLARE batch_size INT DEFAULT 500;
DECLARE insert_values TEXT;

SET end_idx = LEAST(total, start_idx + batch_size - 1);

WHILE start_idx <= total DO
SET insert_values = '';
WHILE start_idx <= end_idx DO
SET insert_values = CONCAT(insert_values, CONCAT('(\'name', start_idx, '\', 0, 0, 0, NOW(), NOW()),'));
SET start_idx = start_idx + 1;
END WHILE;
SET insert_values = LEFT(insert_values, LENGTH(insert_values) - 1); -- Remove the trailing comma
SET @sql = CONCAT('INSERT INTO category_info_varchar_50 (name, is_show, sort, deleted, create_time, update_time) VALUES ', insert_values, ';');

PREPARE stmt FROM @sql;
EXECUTE stmt;
SET @sql = CONCAT('INSERT INTO category_info_varchar_500 (name, is_show, sort, deleted, create_time, update_time) VALUES ', insert_values, ';');
PREPARE stmt FROM @sql;
EXECUTE stmt;

SET end_idx = LEAST(total, start_idx + batch_size - 1);
END WHILE;
END$$
DELIMITER ;

CALL batchInsertData(1000000);

3.验证存储空间


查询第一张表SQL


SELECT
table_schema AS "数据库",
table_name AS "表名",
table_rows AS "记录数",
TRUNCATE ( data_length / 1024 / 1024, 2 ) AS "数据容量(MB)",
TRUNCATE ( index_length / 1024 / 1024, 2 ) AS "索引容量(MB)"
FROM
informati0n—schema.TABLES
WHERE
table_schema = 'test_mysql_field'
and TABLE_NAME = 'category_info_varchar_50'
ORDER BY
data_length DESC,
index_length DESC;

查询结果


image.png


查询第二张表SQL


SELECT
table_schema AS "数据库",
table_name AS "表名",
table_rows AS "记录数",
TRUNCATE ( data_length / 1024 / 1024, 2 ) AS "数据容量(MB)",
TRUNCATE ( index_length / 1024 / 1024, 2 ) AS "索引容量(MB)"
FROM
informati0n—schema.TABLES
WHERE
table_schema = 'test_mysql_field'
and TABLE_NAME = 'category_info_varchar_500'
ORDER BY
data_length DESC,
index_length DESC;

查询结果


image.png


4.结论


两张表在占用空间上确实是一样的,并无差别


三.验证性能区别


1.验证索引覆盖查询


select name from category_info_varchar_50 where name = 'name100000'
-- 耗时0.012s
select name from category_info_varchar_500 where name = 'name100000'
-- 耗时0.012s
select name from category_info_varchar_50 order by name;
-- 耗时0.370s
select name from category_info_varchar_500 order by name;
-- 耗时0.379s

通过索引覆盖查询性能差别不大


1.验证索引查询


select * from category_info_varchar_50 where name = 'name100000'
--耗时 0.012s
select * from category_info_varchar_500 where name = 'name100000'
--耗时 0.012s
select * from category_info_varchar_50 where name in('name100','name1000','name100000','name10000','name1100000',
'name200','name2000','name200000','name20000','name2200000','name300','name3000','name300000','name30000','name3300000',
'name400','name4000','name400000','name40000','name4400000','name500','name5000','name500000','name50000','name5500000',
'name600','name6000','name600000','name60000','name6600000','name700','name7000','name700000','name70000','name7700000','name800',
'name8000','name800000','name80000','name6600000','name900','name9000','name900000','name90000','name9900000')
-- 耗时 0.011s -0.014s
-- 增加 order by name 耗时 0.012s - 0.015s

select * from category_info_varchar_50 where name in('name100','name1000','name100000','name10000','name1100000',
'name200','name2000','name200000','name20000','name2200000','name300','name3000','name300000','name30000','name3300000',
'name400','name4000','name400000','name40000','name4400000','name500','name5000','name500000','name50000','name5500000',
'name600','name6000','name600000','name60000','name6600000','name700','name7000','name700000','name70000','name7700000','name800',
'name8000','name800000','name80000','name6600000','name900','name9000','name900000','name90000','name9900000')
-- 耗时 0.012s -0.014s
-- 增加 order by name 耗时 0.014s - 0.017s

索引范围查询性能基本相同, 增加了order By后开始有一定性能差别;


3.验证全表查询和排序


全表无排序


image.png


image.png


全表有排序


select * from category_info_varchar_50 order by  name ;
--耗时 1.498s
select * from category_info_varchar_500 order by name ;
--耗时 4.875s

image.png
image.png


结论:


全表扫描无排序情况下,两者性能无差异,在全表有排序的情况下, 两种性能差异巨大;


分析原因


varchar50 全表执行sql分析

1711426760869.jpg
我发现86%的时花在数据传输上,接下来我们看状态部分,关注Created_tmp_files和sort_merge_passes
1711426760865.jpg


image.png
Created_tmp_files为3

sort_merge_passes为95


varchar500 全表执行sql分析

image.png


增加了临时表排序


image.png
image.png
Created_tmp_files 为 4

sort_merge_passes为645


关于sort_merge_passes, Mysql给出了如下描述:



Number of merge passes that the sort algorithm has had to do. If this value is large, you may want to increase the value of the sort_buffer_size.



其实sort_merge_passes对应的就是MySQL做归并排序的次数,也就是说,如果sort_merge_passes值比较大,说明sort_buffer和要排序的数据差距越大,我们可以通过增大sort_buffer_size或者让填入sort_buffer_size的键值对更小来缓解sort_merge_passes归并排序的次数。


四.最终结论


至此,我们不难发现,当我们最该字段进行排序或者其他聚合操作的时候,Mysql会根据该字段的设计的长度进行内存预估, 如果设计过大的可变长度, 会导致内存预估的值超出sort_buffer_size的大小, 导致mysql采用磁盘临时文件排序,最终影响查询性能;


作者:向显
来源:juejin.cn/post/7350228838151847976
收起阅读 »

华为自研的前端框架是什么样的?

web
大家好,我卡颂。 最近,华为开源了一款前端框架 —— openInula。根据官网提供的信息,这款框架有3大核心能力: 响应式API 兼容ReactAPI 官方提供6大核心组件 并且,在官方宣传视频里提到 —— 这是款大模型驱动的智能框架。 ...
继续阅读 »

大家好,我卡颂。


最近,华为开源了一款前端框架 —— openInula。根据官网提供的信息,这款框架有3大核心能力:



  1. 响应式API




  1. 兼容ReactAPI




  1. 官方提供6大核心组件



并且,在官方宣传视频里提到 —— 这是款大模型驱动智能框架


那么,这究竟是款什么样的前端框架呢?我在第一时间体验了Demo,阅读了框架源码,并采访了框架核心开发者。本文将包括两部分内容:



  1. 对框架核心开发者陈超涛的采访

  2. 卡颂作为一个老前端,阅读框架源码后的一些分析

采访核心开发者


开发Inula的初衷是?


回答:


华为内部对于业务中强依赖的软件,考虑到竞争力,通常会开发一个内部使用的版本。


Inula在华为内部,从立项到现在两年多,基本替换了公司内绝大部分React项目。



卡颂补充背景知识:Inula兼容React 95% API,最初开发的目的就是为了替换华为内部使用的React。为了方便理解,你可以将Inula类比于华为内部的React



为什么开源?


回答:


华为对于自研软件的公司策略,只要是公司内部做的,觉得还ok的自研都会开源。



接下来的提问涉及到官网宣传的内容



宣传片提到的大模型赋能、智能框架是什么意思?


回答:


这主要是Inula团队与其他外部团队在AI低代码方向的一些探索。比如:



  1. 团队与上海交大的一个团队在探索大模型赋能chrome调试业务代码方面有些合作,目的是为了自动定位问题

  2. 团队与华为内部的大模型编辑器团队合作,探索框架与编辑器定制可能性


以上还都属于探索阶段。


Inula未来有明确的发展方向么?


回答:


团队正在探索引入响应式API,相比于React的虚拟DOM方案,响应式API能够提高运行时性能。24年可能会从Vue composition API中寻求些借鉴。


新的发展方向会在项目仓库以RFC的形式展开。



补充:RFCRequest for Comments的缩写。这是一种协作模式,通常用于提出新的特性、规范或者改变现有的一些规则。RFC的目的是收集不同的意见和反馈,以便在最终确定一个决策前,考虑尽可能多的观点和影响。



为什么要自研核心组件而不用社区成熟方案?



卡颂补充:所谓核心组件,是指状态管理、路由、国际化、请求库、脚手架这样的框架生态相关的库。既然Inula兼容React,为什么不直接用React生态的成熟产品,而要自研呢?毕竟,这些库是没有软件风险的。




回答:


主要还是丰富Inula生态,根据社区优秀的库总结一套Inula官方推荐的最佳实践。至于开发者怎么选择,我们并不强求。


卡颂的分析


以上是我对Inula核心开发者陈超涛的采访。下面是我看了Inula源码后的一些分析。


要分析一款前端框架,最重要的是明白他是如何更新视图的?这里我选择了两种触发时机来分析:



  1. 首次渲染


触发的方式类似如下:


Inula.render(<App />, document.getElementById("root"));


  1. 执行useState的更新方法触发更新


触发的方式类似如下:


function App() {
const [num, update] = useState(0);
// 触发更新
update(xxx);
// ...
}

顺着调用栈往下看,他们都会执行两步操作:



  1. 创建名为update的数据结构

  2. 执行launchUpdateFromVNode方法


比如这是首屏渲染时:



这是useState更新方法执行时:



launchUpdateFromVNode方法会向上遍历到根结点(源码中遍历的节点叫VNode),再从根节点开始遍历树。由此可以判断,Inula的更新机制与React类似。


所有主流框架在触发更新后,都不会立刻执行更新,中间还有个调度流程。这个流程的存在是为了解决:



  1. 哪些更新应该被优先执行?

  2. 是否有些更新是冗余的,需要合并在一块执行?


Vue中,更新会在微任务中被调度并统一执行,在React中,同时存在微任务(promise)与宏任务(MessageChannel)的调度模式。


Inula中,存在宏任务的调度模式 —— 当宿主环境支持MessageChannel时会使用它,不支持则使用setTimeout调度:



同时,与这套调度机制配套的还有个简单的优先级算法 —— 存在两种优先级,其中:



  • ImmediatePriority:对应正常情况触发的更新

  • NormalPriority:对应useEffect回调


每个更新会根据更新的ID(一个自增的数字)+ 优先级对应的数字 作为优先级队列中的排序依据,按顺序执行。


假设先后触发2次更新,优先级分别是ImmediatePriorityNormalPriority,那么他们的排序依据分别是:



  1. 100(假设当前ID到100了)- 1(ImmediatePriority对应-1) = 99

  2. 101(100自增到101)+ 10000(NormalPriority对应10000)= 10101


99 < 10101,所以前者会先执行。


需要注意的是,Inula中对更新优先级的控制粒度没有React并发更新细,比如对于如下代码:


useEffect(function cb() {
update(xxx);
update(yyy);
})

React中,控制的是每个update对应优先级。在Inula中,控制的是cb回调函数与其他更新所在回调函数之间的执行顺序。


这意味着本质来说,Inula中触发的所有更新都是同步更新,不存在React并发更新中高优先级更新打断低优先级更新的情况。


这也解释了为什么Inula兼容 95% 的React API,剩下 5% 就是并发更新相关API(比如useTransitionuseDeferredvalue)。


现在我们已经知道Inula的更新方式类似React,那么官网提到的响应式API该如何实现呢?这里存在三条路径:



  1. 一套外挂的响应式系统,类似ReactMobx的关系

  2. 内部同时存在两套更新系统(当前一套,响应式一套),调用不同的API使用不同的系统

  3. 重构内部系统为响应式系统,通过编译手段,使所有API(包括当前的React API与未来的类 Vue Composition API)都走这套系统



其中第一条路径比较简单,第二条路径应该还没框架使用,第三条路径想象空间最大。不知道Inula未来会如何发展。


总结


当前,Inula是一款类React的框架,功能上可以类比为React并发更新之前的版本


下一步,Inula会引入响应式API,目的是提高渲染效率。


对于未来的发展,主要围绕在:



  • 探索类 Vue Composition API的可能性

  • 迭代官方核心生态库


对于华为出的这款前端框架,你怎么看?


现在开放原子开源基金会搞了个开源大赛,奖金有35w,有两个选题:



  1. 基于openInula实现社区生态库,比如组件库、图表库、Rust基建、SSR、跨平台、高性能响应式更新方案...

  2. 基于openInula实现的AI应用


由于 openInulaReact API基本一致,说白了只要你把自己的 React 项目改下依赖适配下就能报名,有奖金拿,还有华为背书,这波属于稳赚不赔。感兴趣的朋友可以搜openInula前端框架生态与AI创新挑战赛报名。


作者:魔术师卡颂
来源:juejin.cn/post/7307451255432249354
收起阅读 »

告别混乱布局:用CSS盒子模型为你的网页穿上完美外衣!

在网络设计的世界里,盒子模型是构建网页布局的基石,只有理解了盒子模型,我们才能更好的进行网页布局。HTML中的每一个元素都可以看成是一个盒子,拥有盒子一样的外形和平面空间,它不可见、不直观,但无处不在,所以初学者很容易在这上面出问题。今天就让我们来深入了解一下...
继续阅读 »

在网络设计的世界里,盒子模型是构建网页布局的基石,只有理解了盒子模型,我们才能更好的进行网页布局。

HTML中的每一个元素都可以看成是一个盒子,拥有盒子一样的外形和平面空间,它不可见、不直观,但无处不在,所以初学者很容易在这上面出问题。今天就让我们来深入了解一下盒子模型。

一、盒子模型是什么?

首先,我们来理解一下什么是CSS盒子模型。

简单来说,CSS盒子模型是CSS用来管理和布局页面上每一个元素的一种机制。每个HTML元素都可以被想象成一个矩形的盒子,这个盒子由内容(content)、内边距(padding)、边框(border)和外边距(margin)四个部分组成。

Description

这四个部分共同作用,决定了元素在页面上的最终显示效果。

二、盒子模型的组成部分

一个盒子由外到内可以分成四个部分:margin(外边距)、border(边框)、padding(内边距)、content(内容)。

Description

其中margin、border、padding是CSS属性,因此可以通过这三个属性来控制盒子的这三个部分。而content则是HTML元素的内容。

下面来一一介绍盒子模型的各个组成部分:

2.1 内容(Content)

内容是盒子模型的中心,它包含了实际的文本、图片等元素。内容区域是盒子模型中唯一不可或缺的部分,其他三部分都是可选的。

内容区的尺寸由元素的宽度和高度决定,但可以通过设置box-sizing属性来改变这一行为。

下面通过代码例子来了解一下内容区:

<!DOCTYPE html>
<html>
<head>
<style>
.box {
width: 200px;
height: 100px;
background-color: lightblue;
border: 2px solid black;
padding: 10px;
margin: 20px;
box-sizing: content-box; /* 默认值 */
}
</style>
</head>
<body>


<div>这是一个盒子模型的例子。</div>


</body>
</html>

Description

在这个例子中,.box类定义了一个具有特定样式的<div>元素。这个元素的宽度为200px,高度为100px,背景颜色为浅蓝色。边框为2像素宽的黑色实线,内边距为10像素,外边距为20像素。

由于我们设置了box-sizing: content-box;(这是默认值),所以元素的宽度和高度仅包括内容区的尺寸。换句话说,元素的宽度是200px,高度是100px,不包括内边距、边框和外边距。

如果我们将box-sizing属性设置为border-box,则元素的宽度和高度将包括内容区、内边距和边框,但不包括外边距。这意味着元素的总宽度将是234px(200px + 2 * 10px + 2 * 2px),高度将是124px(100px + 2 * 10px + 2 * 2px)。

总之,内容区是CSS盒子模型中的一个核心概念,它表示元素的实际内容所在的区域。通过调整box-sizing属性,您可以控制元素尺寸是否包括内容区、内边距和边框。

2.2 内边距(Padding)

内边距是内容的缓冲区,它位于内容和边框之间。通过设置内边距,我们可以在内容和边框之间创建空间,让页面看起来不会太过拥挤。

内边距是内容区和边框之间的距离,会影响到整个盒子的大小。

  • padding-top: ; 上内边距
  • padding-left:; 左内边距
  • padding-right:; 右内边距
  • padding-bottom:; 下内边距

代码示例:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
<style type="text/css">
/*
1、 padding-top: ; 上内边距
padding-left:; 左内边距
padding-right:; 右内边距
padding-bottom:; 下内边距
2、padding简写 可以跟多个值
四个值 上 右 下 左
三个值 上 左右 下
二个值 上下 左右
一个值 上下左右


*/

.box1 {
width: 200px;
height: 200px;
background-color: #bfa;
/* padding-top:30px ;
padding-left: 30px;
padding-right: 30px;
padding-bottom: 30px; */

padding: 40px;
border: 10px transparent solid;
}
.box1:hover {
border: 10px red solid;
}

/*
* 创建一个子元素box2占满box1,box2把内容区撑满了
*/

.box2 {
width: 100%;
height: 100%;
background-color: yellow;
}
</style>
</head>
<body>
<div>
<div></div>
</div>
</body>
</html>

Description

2.3 边框(Border)

边框围绕在内边距的外围,它可以是实线、虚线或者其他样式。边框用于定义内边距和外边距之间的界限,同时也起到了美化元素的作用。

边框属于盒子边缘,边框里面属于盒子内部,出了边框都是盒子的外部,设置边框必须指定三个样式 边框大小、边框的样式、边框的颜色

  • 边框大小:border-width
  • 边框样式:border-style
  • 边框颜色:border-color

代码示例:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<style type="text/css">


.box {
width: 0px;
height: 0px;
/* background-color: rgb(222, 255, 170); */
/* 边框的大小 如果省略,有默认值,大概1-3px ,不同的浏览器默认大小不一样
border-width 后可跟多个值
四个值 上 右 下 左
三个值 上 左右 下
二个值 上下 左右
一个值 上下左右

单独设置某一边的边框宽度
border-bottom-width
border-top-width
border-left-width
border-right-width
*/

border-width: 20px;
/* border-left-width:40px ; */
/*
边框的样式
border-style 可选值
默认值:none
实线 solid
虚线 dashed
双线 double
点状虚线 dotted
*/

border-style: solid;
/* 设置边框的颜色 默认值是黑色
border-color 也可以跟多个值
四个值 上 右 下 左
三个值 上 左右 下
二个值 上下 左右
一个值 上下左右
对应的方式跟border-width是一样
单独设置某一边的边框颜色
border-XXX-color: ;
*/

border-color: transparent transparent red transparent ;
}
.box1{
width: 200px;
height: 200px;
background-color: turquoise;
/* 简写border
1、 同时设置边框的大小,颜色,样式,没有顺序要求
2、可以单独设置一个边框
border-top:; 设置上边框
border-right 设置右边框
border-bottom 设置下边框
border-left 设置左边框
3、去除某个边框
border:none;
*/

border: blue solid 10px;
border-bottom: none;
/* border-top:10px double green ; */

}
</style>
</head>
<body>
<div></div>
<div></div>
</body>
</html>

Description

2.4 外边距(Margin)

外边距是元素与外界的间隔,它决定了元素与其他元素之间的距离。通过调整外边距,我们可以控制元素之间的相互位置关系,从而影响整体布局。

  • margin-top:; 正值 元素向下移动 负值 元素向上移动
  • margin-left:; 正值 元素向右移动 负值 元素向左移动
  • margin-bottom:; 正值 元素自己不动,其靠下的元素向下移动,负值 元素自己不动,其靠下的元素向上移动
  • margin-right: ; 正值负值都不动

代码示例:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
<style type="text/css">
/* 外边距 不会影响到盒子的大小
可以控制盒子的位置
margin-top:; 正值 元素向下移动 负值 元素向上移动
margin-left:; 正值 元素向右移动 负值 元素向左移动
margin-bottom:; 正值 元素自己不动,其靠下的元素向下移动,负值 元素自己不动,其靠下的元素向上移动
margin-right: ; 正值负值都不动
简写 margin 可以跟多个值
规则跟padding一样
*/

.box1 {
width: 200px;
height: 200px;
background-color: #bfa;
border: 10px solid red;
/* margin-top: -100px;
margin-left: -100px;
margin-bottom: -100px;
margin-right: -100px; */

margin: 40px;
}


.box2 {
width: 200px;
height: 200px;
background-color: yellow;
}
</style>
</head>
<body>
<div></div>
<div></div>
</body>
</html>

Description

三、盒子的大小

盒子的大小指的是盒子的宽度和高度。大多数初学者容易将宽度和高度误解为width和height属性,然而默认情况下width和height属性只是设置content(内容)部分的宽和高。

盒子真正的宽和高按下面公式计算

  • 盒子的宽度 = 内容宽度 + 左填充 + 右填充 + 左边框 + 右边框 + 左边距 + 右边距
  • 盒子的高度 = 内容高度 + 上填充 + 下填充 + 上边框 + 下边框 + 上边距 + 下边距

我们还可以用带属性的公式表示:

  • 盒子的宽度 = width + padding-left + padding-right + border-left + border-right + margin-left + margin-right
  • 盒子的高度 = height + padding-top + padding-bottom + border-top + border-bottom + margin-top + margin-bottom

上面说到的是默认情况下的计算方法,另外一种情况下,width和height属性设置的就是盒子的宽度和高度。盒子的宽度和高度的计算方式由box-sizing属性控制。

Description

box-sizing属性值

content-box:默认值,width和height属性分别应用到元素的内容框。在宽度和高度之外绘制元素的内边距、边框、外边距。

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

点这里前往学习哦!

border-box:为元素设定的width和height属性决定了元素的边框盒。就是说,为元素指定的任何内边距和边框都将在已设定的宽度和高度内进行绘制。通过从已设定的宽度和高度分别减去 边框 和 内边距 才能得到内容的宽度和高度。

  • 当box-sizing:content-box时,这种盒子模型成为标准盒子模型;
  • 当box-sizing: border-box时,这种盒子模型称为IE盒子模型。

四、盒子模型应用技巧

掌握了盒子模型的基本概念后,我们就可以开始创造性地应用它来设计网页。以下是一些技巧:

  • 使用内边距来创建呼吸空间,不要让内容紧贴边框,这样可以让页面看起来更加舒适。

  • 巧妙运用边框来分隔内容区域,或者为特定的元素添加视觉焦点。

  • 利用外边距实现元素间的对齐和分组,保持页面的整洁和组织性。

  • 考虑使用负边距来实现重叠效果,创造出独特的层次感和视觉冲击力。

CSS盒子模型是前端开发的精髓之一,它不仅帮助我们理解和控制页面布局,还为我们提供了无限的创意空间。现在,你已经掌握了盒子模型的奥秘,是时候在你的项目中运用这些知识,创造出令人惊叹的网页设计了。

记住,每一个细节都可能是打造卓越用户体验的关键。开启你的CSS盒子模型之旅,让我们一起构建更加精彩、更加互动的网页世界!

收起阅读 »

爱猫程序员给自家小猪咪做了一个上门喂养小程序

web
🐱前言 每次一到节假日,都好想和好朋友们一起出去玩,但是心里总放不下家里的小猪咪,于是心想能不能找一个喂养师上门喂养呢。于是找了几个上门喂养的平台,并最终下单了服务。 真的不得不说,上门喂养的小姐姐真的非常用心和专业。她们来到我家期间,全情投入地照顾着我的毛孩...
继续阅读 »

🐱前言


每次一到节假日,都好想和好朋友们一起出去玩,但是心里总放不下家里的小猪咪,于是心想能不能找一个喂养师上门喂养呢。于是找了几个上门喂养的平台,并最终下单了服务。


真的不得不说,上门喂养的小姐姐真的非常用心和专业。她们来到我家期间,全情投入地照顾着我的毛孩子,让它吃得饱饱的,看起来胖乎乎的。更令我感动的是,她们还全程录像了照料过程,并将视频发送给我,让我能够时刻了解小猪咪的情况。


分享下小猪咪美照👇



🤔️思考


我也是程序员,为什么我不能自己也做一个上门喂养的小程序呢,于是经过调研,发现了其他的几个平台各有各的弊端和优点,然后诞生出了最初的想法如何做一个把其他平台的弊端去除掉,做一个最好的上门喂养平台。


🎨开始设计


于是开始琢磨figma~~~


因为任何c端都是通过首页再去衍生出其他页面整体样式的,所以先着手制作首页,只要首页定好了其他页面都好说。
image.png


一周后....开始着手设计🤔️...思考...参考....初版定稿


由于刚入门设计一开始丑丑的,不忍直视~~~



再经过几天的琢磨思考...改版...最终确定首页


经过不断的练习琢磨参考最后定稿,给大家推荐下我经常参考我素材网站花瓣



N天之后......其他页面陆续出炉


由于页面太多了就不一一展示啦~~~


image.png


最满意的设计页面


给各大掘友分享一下我最满意的设计页面


签到


结合了猫咪的元素统一使用了同一只猫咪素材~整体效果偏向手绘风格。


image.png


抽奖扭蛋


这个扭蛋机真是一笔一画画了一天才出来的,真的哭😭啦~,由于AE动画太过麻烦所以每一个扭蛋球球的滚动都用代码去实现~~


image.png



💻编程


技术选型


uniapp + nestjs + mysql


NestJS是一个基于Node.js的开发框架,它提供了一种构建可扩展且模块化的服务端应用程序的方式,所以对于前端而言是最好上手的一门语法。


Nestjs学习推荐


给各大掘友推荐一下本人从0到1上手nestjs的教程,就是一下小册就是以下这本,初级直接上手跟着写一遍基本就会啦


image.png


建议学习到 61章左右就可以开始写后端项目啦



小程序端基本使用逻辑



  • 用户下单-服务人员上门服务完成-用户检查完成后确认订单完成-订单款项打款到服务人员钱包

  • 用户注册成为服务人员-设置服务范围-上线开始服务-等待用户给服务人员下单


下单流程


选择服务地点-选择服务人员-点击预约-添加服务宠物-付款


image.png


服务人员认证流程


根据申请流程逐步填写,由于服务人员是平台与用户产生信任的标准,所以我们加大了通过审核的门槛,把一些只追求利益,而不是真正热爱宠物的人员拒之门外,保护双方利益。


image.png


后端Nestjs部署


后端代码写完之后我们需要把服务部署到腾讯云,以下是具体步骤


1.腾讯云创建镜像仓库


前往腾讯云创建容器镜像服务,这样我们就可以把本地docker镜像推送到腾讯云中了,这个容器镜像服务个人版是免费的


image.png


2.打包Nestjs


通过执行docker命令部署到本地的docker


image.png


👇以下是具体docker代码


FROM --platform=linux/amd64 node:18-alpine3.14 as build-stage

WORKDIR /app

COPY package.json .
COPY cert .
COPY catdogship.com_nginx .
COPY ecosystem.config.js .

RUN npm config set registry https://registry.npmmirror.com/

RUN npm install

COPY . .

# 第一个镜像执行 build
RUN npm run build

# FROM 继承 node 镜像创建一个新镜像
FROM --platform=linux/amd64 node:18-alpine3.14 as production-stage

# 通过 COPY --from-build-stage 从那个镜像内复制 /app/dist 的文件到当前镜像的 /app 下
COPY --from=build-stage /app/package.json /app/package.json
COPY --from=build-stage /app/ecosystem.config.js /app/ecosystem.config.js

COPY --from=build-stage /app/dist /app/src/
COPY --from=build-stage /app/cert /app/cert/
COPY --from=build-stage /app/public /app/public/
COPY --from=build-stage /app/static /app/static/
COPY --from=build-stage /app/catdogship.com_nginx /app/catdogship.com_nginx/

WORKDIR /app

# 切到 /app 目录执行 npm install --production 只安装 dependencies 依赖
RUN npm install --production

RUN npm install pm2 -g

EXPOSE 443

CMD ["pm2-runtime", "/app/ecosystem.config.js"]

3.推送到腾讯云


本地打包完成之后我们需要把本地的docker推送到腾讯云中,所以我们本地写一个sh脚本执行推送


#!/bin/bash

# 生成当前时间
timestamp=$(date +%Y-%m-%d-%H-%M)

# Step 1: 构建镜像
docker build -t hello:$timestamp .

# Step 2: 查找镜像的标签
image_id=$(docker images -q hello:$timestamp)

# Step 3: 为镜像添加新的标签
docker tag $image_id 你的腾讯云镜像地址:$timestamp

docker push 你的腾讯云镜像地址:$timestamp

4.部署到服务器


由于我使用的是轻量级应用服务器,所以直接使用自动化助手去进行部署(PS:可能有一些小伙伴会问为什么用轻量级应用服务器呢,因为目前用户量不是很多,轻量级应用服务器足够支撑,后面用户量起来会考虑转为k8s集群


image.png


然后我们去创建一个自动化执行命令,去执行服务器的docker部署


image.png


创建命令


image.png


执行命令


image.png


👇以下是命令代码


# 停止服务
docker stop hello

# 删除容器
docker rm hello

# 拉取镜像
docker pull 你的腾讯云镜像地:{{key}}

#读取image名称
image_id=$(docker images -q 你的腾讯云镜像地:{{key}})

# 运行容器
docker run -d -p 443:443 -e TZ=Asia/Shanghai --name hello $image_id

5.部署完成


命令返回执行结果显示执行完成,说明已经部署成功了


image.png


6.Nestjs服务器端的管理


由于node是一个单线程,所以我们使用的是pm2去进行管理node,它可以把node变成一个多线程并进行管理


由于nestjs中使用到了定时任务,而定时任务只需要开一条线程去做就好了,所以我增加了一个环境变量NODE_ENV来对定时任务进行管理


module.exports = {
apps: [
{
name: 'wx-applets',
// 指定要运行的应用程序的入口文件路径
script: '/app/src/main.js',
exec_mode: 'cluster',
// 集群模式下的实例数-启动了2个服务进程
instances: 4,
// 如果设置为 true,则避免使用进程 ID 为日志文件添加后缀
combine_logs: true,
// 如果服务占用的内存超过300M,会自动进行重启。
// max_memory_restart: '1000M',
env: {
NODE_ENV: 'production',
},
},
{
name: 'wx-applets-scheduled-tasks',
script: '/app/src/main.js',
instances: 1,
// 采用分叉模式,创建一个单独的进程
exec_mode: 'fork',
env: {
NODE_ENV: 'tasks',
},
},
],
};

后端总结


到目前为止前台的业务接口都写完了做了个统计一共有179个接口


image.png


后期版本更新


预计这个月上线毛孩子用品盲盒抽奖,有兴趣的友友们也可关注下哦


Frame 2608921.png


后期展望,帮助更多的流浪动物有一个温暖的家


image.png

小程序上线


目前小程序已经上线啦~,友友们可以前往小程序搜索 喵汪舰 前往体验,
或者扫描一下二维码前往


开业海报.jpg

我的学习路线


因为我是一个前端开发,所以对于设计感觉还是挺好的,所以上手比较快。
一条学习建议路线:前端-后端-设计-产品,最终形成了一个完整的产品产出。


以下的链接是这个项目中我经常用到的素材网站:


freepik国外素材网站-可以找到大部份的插画素材


figma自带社区-获取参考的产品素材


花瓣国内素材参考网站-涵盖了国内基本的产品素材


pinterest国外大型素材网-你想到的基本都有


总结


一个产品的产出不仅仅依靠代码,还要好的用户体验,还需要不断的优化迭代,


最后给一起并肩前行的创业者们的一段话:


在创业的道路上,我们正在追逐梦想,挑战极限,为自己和世界创造新的可能性。这个旅程充满了风险和不确定性,但也蕴藏着无限的机遇和成就,不要害怕失败,勇于面对失败,将其视为成功的必经之路。


作者:热爱vue的小菜鸟
来源:juejin.cn/post/7348363812948033575
收起阅读 »

业务开发做到零 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 的因素吗?欢迎在评论区留言与大家分享。


好了,今天的分享就到此为止,希望大家都能做到业务开发零 bug,业务开发代码一把过!


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


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

聊一聊定时任务重复执行以及解决方案

大家好,我是小趴菜,关于定时任务大家都有接触过,项目中肯定也使用过,只需要在项目中的启动类上加上 @EnableScheduling 注解就可以实现了 现在是单个节点部署,倒是没什么问题。如果是多节点部署呢? 假设现在我们系统要每天给用户新增10积分,那么更新...
继续阅读 »

大家好,我是小趴菜,关于定时任务大家都有接触过,项目中肯定也使用过,只需要在项目中的启动类上加上 @EnableScheduling 注解就可以实现了


现在是单个节点部署,倒是没什么问题。如果是多节点部署呢?


假设现在我们系统要每天给用户新增10积分,那么更新的SQL如下


  update user set point = point + 10 where id = 1;

这时候你的服务部署了两台,那么每一台都会来执行这条更新语句,那么用户的积分就不是+10了。而是+20。


当然这里我们只是举了个例子来引出 @EnableScheduling 的定时任务会存在定时任务重复执行的问题。而且有可能会因为重复执行导致数据不一致的问题


使用数据库乐观锁


在使用乐观锁的时候,首先我们会新增一个字段,也就是更新日期,但是这个更新日期不是指我们修改数据的那个更新时间,比如说今天是2024-03-25,那么到了明天,第一台机器更新成功了,这个值就更新成2024-03-26,其它机器的线程来更新判断这个值是否是2024-03-25,如果不是,说明已经有线程更新了,那么就不需要再次更新了


  update user set point = point + 10,modifyTime = 2023-03-26 where id = 1 and modifyTime = 2024-03-25

基于乐观锁的方式有什么缺点呢??


现在我们只有两台服务器,那如果有1千台,1万台呢,对于同一条数据的,那么这1万台服务器都会去执行更新操作,但是其实在这1万次更新操作中,只有一次操作是成功的,其余的操作都是不需要执行的


所以使用这种方式当机器数量很多的时候,对数据库的压力是非常大的


分布式锁


我们还可以使用分布式锁的方式来实现,比如要执行这个定时任务之前要先获取一把锁,这个锁是对每一条记录都分别有对应的一把锁


当线程来更新某一条数据的时候,首先要获取这条记录的一个分布式锁,拿到锁了就可以去更新了,没有拿到锁的也不要去等待获取锁了,就直接更新下一条数据即可,同样的步骤,只有拿到某条数据的锁,才可以更新


image.png


但是这里有一个注意的点,就是比如服务-1先获取到id=100的这条记录的锁,然后执行更新,但是此时因为某些原因,导致某台服务器过了一会才来执行id=100的这条数据,因为服务-1已经执行完了,所以会释放掉这把锁,所以这台服务器来就能获取到锁,那么也会执行更新操作


所以在更新的时候,还是做一下判断,判断这条记录是否已经被更新了,如果已经更新了,那么就不要再次更新了


分布式锁相对于乐观锁来说,减少了大量的无用的更新操作,但还是会存在极少量的重复更新操作,但是相对来说,对数据库的压力就减少了很多


但是与此同时,这就依赖于Redis,要保证Redis的服务可用


消息队列


我们可以将要更新的数据提前加载到消息队列中去,然后每台服务就是一个消费者,保证一条记录只能让一个消费者消费,这样也就可以避免重复更新的问题了


但是消费失败的记录不要重回队列,可以在数据库记录,让人工进行处理


使用消息队列会有什么问题呢?


如果你的消息数据特别多,比如有上亿条,那么消息队列就会被堆满,,而且每天都要把数据库的数据都加载到消息队列中去


或许有人说,数据量大我可以多弄几个消息队列,这样确实可以解决一个消息队列堆积消息过多的问题,但是你要如何控制某些服务器只访问某个队列呢?不能每台服务都循环获取每一个队列中的消息吧


而且如果你的消息队列是单点的,那么服务宕机那么所有数据都没法更新了,这时候你还要去弄一个集群
,这成本就有点大了


所以不推荐使用这种方式


分布式任务调度-xxl-job


最后就是使用分布式定时任务调度框架 xxl-job了,关于xxl-job的使用大家可以网上自己搜一下资料。


XXL-JOB是一个开源的分布式任务调度框架,它主要用于解决大规模分布式任务的调度和执行问题。该框架提供了任务调度中心执行器任务日志等组件,支持任务的定时调度、动态添加和删除、执行情况监控和日志记录等功能。


总结


以上就是为大家提供的定时任务重复执行的解决方案,大家可以根据自己的实际情况来选择不同的方案来实现


作者:我是小趴菜
来源:juejin.cn/post/7350167062364979226
收起阅读 »

职场上的人情世故 - 初入公司的第一个项目

说下背景 来公司的第二周,上面下发了新需求,也是我参与公司的第一个项目。 我先说下公司一个项目的大体配置 产品经理 1~2人 一般中小版本就1人 UI 1~2人 研发前端 1~3人 研发后端 1~5人(包含协同团队) ps:其中后端中1...
继续阅读 »

说下背景


来公司的第二周,上面下发了新需求,也是我参与公司的第一个项目。


我先说下公司一个项目的大体配置


产品经理 1~2人  一般中小版本就1人
UI 1~2人
研发前端 1~3人
研发后端 1~5人(包含协同团队)
ps:其中后端中1人为项目负责人,负责整体项目进度和协调资源
测试 1~2人

好的,故事由此开始。先铺垫下,该项目是我从业以来最无力最绝望最想扯呼的一个。


需求评审


周一刚来公司,就被公司给我安排的导师L 叫到身边


导师L:「一会儿有个需求评审,我文档先发你,你先抓紧时间看看」


懵懂的我:「好的,我先熟悉下任务」


说实话文档看起来真的有点头疼,主要是上面的一行字我真的无语至极【与线上保持一致】。公司的项目还是比较大的,我上哪儿去了解保持一致是个啥意思......问下导师吧,他也在排查线上问题;问下其他同事吧,也刚来没多久不了解这块业务....


好吧时间应该够,我先打开系统,看看这功能到底在哪里。


「走,A2评审会」 导师L给了我一个眼神


大哥,我才看了不到20分钟,功能我都没看完......


我的情况稍微有点特殊,一般来说,大家跟随导师的节奏,问题都不大
看不懂的需求,一定要整懂了在动手,否则返工风险很大
时间不允许?那要再考虑下该公司是不是适合你了哦!!!总不能说能跑起来就行吧

过程我全程梦游,大家都在提问题:各种数据的兼容方案、功能冲突怎么兼容、有类似功能是否能套用、数据权限是否考虑、特殊逻辑的兜底方案.......


开始还能记一下笔记,后面发现 跟不上,根本跟不上 直接 弃笔,从容 面对现实了


从会议室走出来,嗯~~ 怎么形容心情呢。我高考语文作文那一页名字忘了写出考场都没这么忧虑过(当然作文最后还是有得分)。


分配任务


直接上对话详情,后端加上我一共3人


导师L:「任务我这边大概分配了下,你们先选吧」


我内心很慌,我一个都看不明白要怎么做


不知所措的我:「要不我拿剩下的」


同事P:「那把这个、这个、这个给我吧,剩下的你们分?」


导师L看着我:「那就这样分,你做那个、那个、那个,有问题随时找我」


不知所措的我:「好的,我主要是不熟悉这块业务,有问题我先整理出来再一起问下你呗」


这里有个关键点,整理完问题再一起咨询,切记遇到一个问一个
态度摆正,表现得自信点,第一次在同事面前“刷脸”,印象要留好一些

研发阶段


我的任务主要是客户群统计模块


导师给我开了两个服务的权限,让我先看看代码


懵逼的我:「L哥,要不大概说下在那部分写的统计数据?我应该从哪些表出结果?」


导师L:「你先看代码,里面都有逻辑,你看得懂」


无语的我:「好的」 内心(这么玩儿是吧)


知道那代码有多难懂吗?全都是A队列推到B队列,判断后再推C队列,并且还有了很多观察者模式。可能有兄弟姐妹问:「看不懂注释?」。注释少得可怜,也就一些字段含义,不过看单词也能看出来。


队列和队列之间很难看懂,并且很多跨服务的队列,给我开的两个服务根本不够,前后一共加上4个服务才把统计的逻辑串起来,我还专门画了个流程图出来


不管代码设计和质量如何,作为新人别公开喷前辈的代码
前期不要加入太多个性化的代码和设计,每个公司的要求不一致,最好的方式还是模仿
逻辑复杂的话,建议画一个流程图or时序图,架构图的话还是问下老同事,自己画可能不准确
不要轻易下手编码,确保自己很明确整体逻辑了再跟熟悉的同事复述一遍先

好吧,我终于找到逻辑线了(花了1天半时间,留给我开发的时间很近很近),但我发现,结果表很多很多,有AAA_statistics、BBB_statistics、CCC_statistics...... 我拿不定主意,刚好我也对这几个服务熟悉得差不多了,于是我拿着这些成果去问导师L


诚恳的我:「L哥,帮忙看看这几张表,我感觉有一些数据是重复的,还对不上数,帮忙看看呢?」


导师L:「逻辑都捋清楚了吗?」


我直接掏出了我的流程图


导师L:「这是JW(我们组的TL)给你的吗?」


求知的我:「我自己画的,不晓得准确性怎么样,帮忙check下呢」


导师L:「主要是我这还有点事,你再看看吧,晚点我们过一下」


前期难免会有很多问题,但自己也要有点准备,否则被问成麻瓜,印象分直接拉低
不知道的不要说"不知道",可以说"我还不太熟悉,我先去看看逻辑,X哥要不跟我说下大概位置或则有啥文档?"

到了晚上7:00左右,我发现L哥不见了,后来才晓得那天他溜得挺早的。只剩我原地爆炸,明天要联调了,我关键的数据组装service还只有一行//TODO。没招了,我按照我的想法和数据指标,找了几张最合适的表,把数据产出了。


第二天


我早早地来公司,重新check了下代码并又调试了几次,除了数据对不上没啥其他问题。导师L带着早饭来到公司,我还等到吃完早饭再去问了下。


疲惫的我:「L哥,你看我用这张统计表去输出功能有问题没?」


导师L:「就那么几张表,你造几条数据不就看出来了」


疲惫的我:「我造了几条数据,但是昨天的数据我看今天才过来,昨天忘了记录过程」


导师L:「怎么可能,你怎么造的数据,算了等会儿我看一下」


......


过程就不细讲了,总之,不好受...... 最后问题也定位到了,是导师L当初做测试,把定时任务往后延了1天。好吧,这还有定时任务的事儿?我只能看代码看到一堆数据同步的接口,真的是应征了那句话:【最怕的是:我不知道我'不知道'】。


时间允许的话,着手开工前还是留一份概要设计的存档,别把坑都留给后面的人了
流程图、时序图、架构图等,前期多花时间,少走弯路
心态摆好,我们是来打工挣钱的,交不交朋友是其次而已,关系不恶化怎么着都行

而后,比较顺利地进行到提测阶段,bug真的是多得要命,主要是以下几点:


1、数据权限没加(就是prd文档上写的跟xxx模块保持一致逻辑)


2、数据同步时好时坏,排查后发现是定时任务不稳定,大家都在一个环境提测版本,都在修改功能


3、未做改造的功能点测出历史bug(功能类似,故测试同学也纳入了测试范围)


4、服务被其他版本覆盖


导致最终版本交付时,我个人产生了大概20+个bug。周五下午TL专门找我谈话,约谈此事,大概意思是版本复杂度不高,但bug数过多,对我的版本质量不满意。对此,我也阐述了我的解释,这里我就不细说解释的话,大概从以下几点去阐述


不要丢锅,先把由自己粗心产生的bug讲一下,记得提一下数据,因自身代码质量产生了X个bug,后续会加强编码质量,多自测
也不要背锅,说下由于环境问题导致了X个bug的产生,并给出自己的建议,是否能做环境隔离,一个环境只能容纳服务不冲突的1~2个版本
注意下,TL直接对话的机会并不是很多,可以结合自身入职以来的经历,提一些建议(不用管是否重要)
总之,事事多总结,否则像这种情况下TL突然找咱们约谈,说不出个123,印象分会很差

项目总结


在项目研发中,不一定能得到公司前辈们的"关照",这时候就需要先自己憋一下(注意合理安排时间),怎么着也能憋出点内容,那这这些内容再去咨询,效果会好很多。像我这种被孤立的情况,我也不好说是不是常态,我个人还是认为好同事还是居多的。总之还是与同事搞好关系,或者关系一般遇事能帮忙也行,不要跟任何同事把关系闹僵(哪怕脾气差、技术菜、背后勾心斗角)。


还是那句话,遇事不要慌,先调整好心态(道理都看得懂,但希望大家都能做得到),尽力吃透项目再开始施工,否则返工或者低质量产出不好避免。


多做总结,不甩锅不背锅,不管是项目总结还是转正答辩还是晋升答辩,都能有个有条理有依据的总结出来。这点在职场中至关重要。


作者:snowlover
来源:juejin.cn/post/7347542133610364980
收起阅读 »

低谷期可能是在救你

怎么来定义低谷期? 我觉得真正的低谷期是迷茫,挫败,痛苦,自我怀疑等等,反正一定是比肉体上的痛苦更煎熬! 可能你会觉得暂时性找不到工作,面试受挫,分手等是你的低谷期,实则不然,这大概是一个不敢面对自己的借口。 因为多数人找到工作后,只要能在温饱线上,那么90%...
继续阅读 »

怎么来定义低谷期?


我觉得真正的低谷期是迷茫,挫败,痛苦,自我怀疑等等,反正一定是比肉体上的痛苦更煎熬!


可能你会觉得暂时性找不到工作,面试受挫,分手等是你的低谷期,实则不然,这大概是一个不敢面对自己的借口。


因为多数人找到工作后,只要能在温饱线上,那么90%的人就会心安理得摆烂,和男女朋友复合后,依然是维持原样,不会被爱情去刺激自己成长。


如果用工作和分手这两件事情来描述真正的低谷期,那么我觉得应该是这样描述的:


自己付出了很多,每天都像一条疯狗干,工作和学习的时长维持在18个小时左右,坚持了很久,但是没有收获自己想要的东西,希望还是如此渺茫,于是产生严重的自我怀疑,挫败,深圳绝望!


自己一直在努力,在慢慢成长,目标越来越近,但是自己爱得死去活来的女朋友实在等不了自己成长,自己变强,所以无情的选择离开。


经过上面的描述,成为了毋庸置疑的低谷期。


因为没经过描述之前其实你是没有任何斗志的,目的就是能安心混日子,可以用摆烂来形容,和女朋友在一起,自己也不需要啥成本,她也没有啥诉求,所以算得上是互相将就。


但是经过描述后,实际上你的努力近乎疯狂,对于成功是无比的渴望,但是迟迟没降临到你的身上,而女朋友是在你身上压了筹码的,而你又非常爱她,但是迟迟不能让她压的筹码赚个翻倍,所以离开成了定局。


这下就一目了然了吧。


所以这时候你再去回想一下自己所谓的低谷期,是否真的是低谷期?还是自己矫情?


从上面的场景我们能看到深刻二字,第一个是根本谈不上深刻,而第二个是无比的深刻,如果你经历过,那么你自然能理解!


只有让自己刻骨铭心和痛苦的事情才能让自己成长,其他的基本不能。


就像你比较肥胖,三高找上你,家里人叫你不要再吃垃圾食品,不要喝饮料,但是这时候你还没真正躺在病床上,你基本上不会去听。


但是如果此刻你躺在病床上,插着呼吸管,医生给你说,如果你再不注意饮食,那么你很快就会死掉。你看你还会不会再去吃垃圾食品,喝饮料。


所以说,只有让自己脱一层皮,自己才会意识到问题的所在。


那么低谷期里,也自然会脱一层皮,甚至还会换骨头,那么你觉得这难道不是最佳的成长期吗?


所以一个人能遇上真正的低谷期,其实是一种幸运,应该要感谢这个遭遇。


就像一个朋友,因为一些原因导致自己不能再继续读书,家里为了自己的事情而负债累累,自己承受巨大的压力,想死的心都有了。


但是在这段堪称被恶魔惩罚的时期,他没有一蹶不振,反而置之死地而后生,最后无论是财富还是个人能力,阅历都有很大的提升!


后面和他聊天的时候,他说那是自己最痛苦,最无助的时候,但是自己庆幸没有放弃,才造就了一身钢筋铁骨?


所以你说这个低谷期是不幸的吗?


不是,这算的上是恩赐,因为并不是谁都有这种机会。


所以我们在低谷期的时候,如果能有把它作为翻盘的机会的思想,那么它一定是很难得的机会!


但是如果把它作为上天对自己的惩罚,从而一蹶不振,那么其实是错失一次很好的机会,并且也学让自己越陷越深!


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

探索个人IP&副业第三个月的思考:未经审视的人生不值得过

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 个人IP探索的第三个月,不再是一往无前的埋头苦干,一腔热血之余,对自己的能力探索与兴趣挖掘,成为了日常生活的主线。 为了更好的坚持下去,希望自己能够找到自己最擅长、做起来最愉悦、...
继续阅读 »

前言


Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。


个人IP探索的第三个月,不再是一往无前的埋头苦干,一腔热血之余,对自己的能力探索与兴趣挖掘,成为了日常生活的主线。


为了更好的坚持下去,希望自己能够找到自己最擅长、做起来最愉悦、自身价值观又认可的事情。但最近一段时间,我意识到,我其实并不了解我自己。想认识自己,也不是一件简单的事情。



古希腊哲学家苏格拉底曾说:“未经审视的人生不值得过。“



话不多说,看看最近一段时间,我身边发生的事情与思考吧。


经历AI的诱惑,坚定内心的方向


其实在23年年底刚开始写作的时候,自己就对自己列了几个方向


其中,AI一直是我自己认为一个很重要的方向,时代大趋势下,所有行业只要加上AI,似乎都变得有些不一样。


image.png


24年年初,我第一次试着去了解AI的工具,是因为那时看到一个圈友,做了一个AI艺术二维码。于是我也下载下了stable-diffusion,准备大展身手一把。


本来是这样想的,发个朋友圈,如果有想要做艺术二维码的,我5块钱给他生成一个,怎么着也算开启了副业收入对吧。


结果人家做出来的是这样的,蛮好看。


image.png


我生成出来的,丑就不说了,问题是扫都扫不出来,就很搞笑。


苦于周边暂时没有大佬支持,也有着别的事情需要做,就暂时被我搁置了。


我的内心其实对AI感兴趣,但实际上,还是以一个程序员思维来考虑的。举一个例子,就想现在.NET的就业环境已经逐渐减少,如果我毕业时选择了一份.NET的工作,那么市场需求一定是很小的。


所以我才会对AI感兴趣,我的思路还是,或许5年后,Java已经不行了,但是算法开发、模型开发可能成为市场需求主流,为了找到工作,不得不去学习AI相关。


这件事情埋在我心里,虽没干成,但毕竟没有搞出来,心里还是有点不甘心的。


从过年伊始,就通过各种渠道,了解到了AI破局社群,即将迎来拉新。那时候我了解到,原来搞AI类应用,其实是有一个专门的社群的,里面也有各种各样的项目让你去落地,还有专业的人给你指导,我之前没跑出来的AI二维码,只要加入,遇到的问题肯定就能解决。


面对着铺天盖地的宣传,自己内心也逐渐有些动摇,一边是追求热点,迅速满足。一边是继续学习写作,延迟满足,看着许多圈友入局,内心还是挺焦虑的。


最终还是克服了自己短期尝试AI的想法,两个原因:



  1. 自己在尝试副业的伊始,就决定2024上半年,我不会思考变现问题,主业还能养活自己。

  2. 既然决定先好好锻炼写作能力,那就应该持续内化基本功,不追求短期目标。


人的精力有限,需要放在有用的地方。


关于写作


上了热榜,占据首页


经过一周时间的打磨,之前经历的一篇文章,竟然上了热榜第一,6000多字的碎碎念能被大家看到,不少朋友给我说能够感同深受,一切的努力都值得了。


image.png


被更多的人看到,固然是好事,但在写这篇文章的过程中,也发现了不少问题,其中低效、内耗的问题,最为严重。


首先,6000多字的文章,完全是个人经历与思考,纯手写的话,如何组织语言,安排好顺序,就是一个最大的问题。


其次,如何把结构梳理好,能够更有连贯性,才能让大家在看的过程中,不枯燥乏味,我花费了非常多的时间。


最后,因为事情涉及到不少真实经历,受限于篇幅,不能完全的呈现给大家,如何选择最能让大家共鸣,最能给大家带来价值的地方,也蛮头疼的。


与现在利用AI工具,一天能够四五篇文章的作者比起来,效率简直低的可怕,虽然带来的价值不一样,但是流量就是这么多,想让更多人看到,效率如何提高,是我接下来要重点思考的事情。


关于输出的“困扰”


经过3个月的输出,我不出意外的遇到了“缺少话题”的情况,这也是最近更新频率变低的最主要的原因。


在这段时间,虽然输出频率降低,但话题的收集没有落下,每当有一些小点子,我就会先记录下来,每天翻一翻,去看看哪些是最想写的。


但实际上,即使收集了很多素材,如果你没有太多想说的话,无疑也只是罗列旧知识,没有新观点,我只想说点新颖的,所以迟迟没有动笔。


为了解决这个问题,两个方法和大家分享下


先立后破


先立后破,虽然话题素材减少,但强制要求自己,每周都要有所输出。


为了达成这个任务,我被迫搜集很多素材,先丰富自己的素材库,然后选择自己最有表达欲的主题,详细去写。


为什么要先立后破


任何事情都有一个逻辑,就是先别管技巧或者合理性,也别管效果如何,先给它规定一个数量,把数量做到了,慢慢的,你就会因为数量而倒逼自己去琢磨如何研究质量。


写作这件事也一样,即使我不知道写什么,也要强迫自己动笔去写,自己在输出这件事上,依然坚信着一句话。



数量不一定能决定质量,但一定能决定重量。



定扩联思维


先确定


能输出的点有很多,技术、副业、写作、复盘,先把内容方向定下来,比如我今天选择的方向就是复盘。


后扩展


扩展的话就好说了,既然是复盘,每一件事情都可以展开来讲,AI的一些经历。关于输出的困扰,输出一些方法论。


再联想


根据自己阅读、学习过的内容,进行发散。比如我深入思考的个人定位问题,最近的所学所想,都可以随着复盘输出出来。


个人定位


这个月花费最多的时间,就是在思考如何有一个清晰的定位。思考过程中问自己的几个问题,也在此罗列出来,如果你也想找到个人定位,也不妨对着这几个问题问一下自己。


职业与专业知识


7年后端研发工程师,北漂回二线城市的大厂程序员。毕业半年裸辞北漂,但经过几年北漂后,为了追求内心、照顾家庭,6年后回到二线城市发展。


北漂阶段,从30人小公司,最终进入大厂。大厂期间面试人数50+,对于求职面试、职业规划有着不少宝贵经验。


完成北漂回归二线城市,完成了城市选择,公司判断,薪资谈判,心里建设等一系列流程,各种有些许挫折,但都一一完成拆解。


善于在复杂项目中抽丝剥茧,但缺乏创造力。经手的项目无论难度多大,都可以极快的完成梳理与上手。但每当新业务开展,思路总会比较局限。


喜欢的博主


个人非常喜欢的博主是,半佛仙人,18年从公众号关注至今,文章几乎一篇不落。


image.png


为什么喜欢?我简单的想了下,有一下几个原因。



  1. 有一定的专业性,瑞幸等一些文章,业内出名,如果大家听说过半佛,大概也看过那些非常出名的文章。

  2. 够肝。热点时事跟踪及时,比较重要的热点新闻,时事动向,他一定参与进来。虽然从作者的角度来说,肯定是要跟着热点和流量走,但对于我这种几乎不看微博和新闻的人来说,能让我省下很多无效时间,还能了解到很多最新的新闻。

  3. 够骚,从他的ID就看出来了:banfoSB,他的文风是我关注的所有博主里,最有特点的。


以上,羡慕。在我眼中,能毫无顾及的表达出自己的看法,让这么多人看到,对于一个爱写东西的人来讲,无疑是一件幸福的事情了。如果这件事情还能赚到钱,那更是锦上添花。


业余爱好


大学业余时间除了打游戏,就是谈恋爱,一上高数就睡觉,下课铃响就起床,


直到看了一本书《皮囊》之后,开始热爱上了阅读。虽然自己生在城市,完全没有农村经历,但对《皮囊》《活着》《兄弟》等很多农村作品,阅读的时候却有着特别大的感触,文中主人工经历越苦难,我的感受越心酸。


后来发现,老读这种文章,人都快看emo了(许多穷苦的事情,自己又没有能力改变什么)。


后来大学期间看了《嫌疑人x的现身》(短篇小说,非常精彩),了解了东野圭吾,对悬疑类作品有了极大兴趣,花了半年的时间就把他大多数重要的作品都看了一遍。印象最深的是《白夜行》,从第一次打开《白夜行》到最终看完,几乎过去了半年的时间,但是《嫌疑人x的现身》只需花了2天时间我就看完了。


但最终读完《白夜行》后,其中的精彩程度让我认为这是东野圭吾小说系列在我心中的No.1。这本书也让我意识到长篇小说的精彩之处,前面之前认为的枯燥的铺垫,到最后都是拍手称绝的伏笔。让我对延时满足这四个字,有个更深的体会。


image.png


后来又听说了村上春树,《挪威的森林》目前为止读了四遍,闲下来仍然会打开翻一翻。里面一些句子,一直在潜移默化的影响着我



不要同情自己,同情自己是卑劣懦夫干的勾当。


所谓绅士,就是做自己该做之事,而不是想做之事。



擅长的事情


我的盖洛普第二名和第五名,是和谐和体谅,遇见冲突解决冲突,并能够设身处地的体会他人的感受。所以之前有很多朋友和我说过,和我聊天,即使问题解决不了,心理上也能舒服很多。


image.png


或许你想找对象,我能帮你发现一些你潜在的问题,帮你去想应该怎么做,才能得到妹妹。


或许你面临裁员、换工作,我能根据你现有的能力与过往的经验,帮你规划提升方向。


或许你面临职场困惑,我也可以根据过往经验给出一些意见。


如果你有类似的问题,也欢迎你来找我聊聊。


说在最后


这次的复盘就先聊到这里了,有小的高光时刻,也有阻碍和自己的思考,感谢你能看到这里。




作者:东东拿铁
来源:juejin.cn/post/7349856694996074550
收起阅读 »

歌词滚动效果

web
需求 实现歌词滚动效果,类似网易云音乐播放界面。 实现 1.准备数据 后台一般会返回这样一个歌词数据,每个时间都对应当前这个歌词。 因为是字符串不方便直接使用,我们把他转化为对象格式。封装一个utils工具传入歌词把lyric转化为对象。 // 处理后端返...
继续阅读 »

需求


实现歌词滚动效果,类似网易云音乐播放界面。



实现


1.准备数据

后台一般会返回这样一个歌词数据,每个时间都对应当前这个歌词。



因为是字符串不方便直接使用,我们把他转化为对象格式。封装一个utils工具传入歌词把lyric转化为对象。


// 处理后端返回歌词
export const HandleLyric = (lyric) => {
    function convertToSeconds(timeArray) {
        // 获取分钟和秒(包括毫秒)  
        const minutes = parseFloat(timeArray[0]); // 使用parseFloat来确保即使分钟有小数部分也能正确处理  
        const secondsWithMilliseconds = parseFloat(timeArray[1]);
        // 计算总秒数  
        const totalSeconds = minutes * 60 + secondsWithMilliseconds;
        return totalSeconds;
    }
    let line = lyric.split('\n')
    let value1 = []
    for (let i = 0; i < line.length; i++) {
        let str = line[i]
        // 把字符串分割为数组
        let part = str.split(']')
        let timestr = part[0].substring(1)
        let parts = timestr.split(':')
        let obj = {
            time: convertToSeconds(parts),
            word: part[1]
        }
        value1.push(obj)
    }
    return value1
}


2.计算偏移量

准备audio标签


  <div class="audio">
      <el-button>立即播放/暂停</el-button>
      <audio :src="audio_info['url']" ref="audio" class="audio-sty">11</audio>
  </div>

调用audio标签的currentTime可以获取当前歌曲播放到第几秒,遍历歌词的时间和当前时间(currentTime)比较,返回的i就是当前歌词的在第几行。


const changeplay = () => {
    let audio = document.querySelector('.audio-sty')
     // 找到当前这一句歌词的索引
        function FindIndex() {
            let currentTime = audio.currentTime
            for (var i = 0; i < store.lyicWords.length; i++) {
                if (currentTime < store.lyicWords[i].time) {
                    return i - 1
                }
            }
            return store.lyicWords.length - 1
        } 
}

计算偏移量


        // 计算偏移量 
        /**
         * 偏移量
         * @containerHeight //容器高度
         * @PHeight   //单个歌词高度
         */
        function Setoffset() {
            var index = FindIndex()
            var offset = index * store.PHeight + store.PHeight / 2 - store.containerHeight / 2
            if (offset < 0) {
                offset = 0
            }
            store.index = index
            store.Offset = offset
        }


用当前歌词所偏移的大小加上单个歌词Height1/2的大小,就可以实现歌词偏移,如果想让每次高亮的歌词在中间,需要再减去container盒子自身Height的一半。




调用audio的timeupadte时间触发计算偏移函数


  // audio 时间变化事件
audio.addEventListener('timeupdate', Setoffset)

3.添加样式

<div class="center" ref="center">
            <h1>歌词</h1>
<el-card class="box-center">
      <div class="center-ci" style="overflow: auto;" ref="lyricHeight">
      <p v-for="(item, index) in  songList['txt']" v-if="songList['txt'].length != 0"
                        :class="[index == store.index ? 'active' : '']">
{{item }}</p>
       <p v-else>纯音乐,无歌词</p>
       </div>
</el-card>
</div>

使用transform: translateY(-50px);控制偏移

使用transform: scale(1.2);控制文字大小


     .center-ci {
                transformtranslateY(-50px);
                display: block;
                height100%;
                width100%;

                p {
                    transition0.2s;
                    font-size20px;
                    text-align: center;
                }

                .active {
                    transformscale(1.2);
                    color: goldenrod;
                }
            }

给歌词容器设置transform就可以实现歌词偏移了



// 根据偏移量滚动歌词 
lyricHeight.value.style.transform = `translateY(-${store.Offset}px)`

4.效果


~谢谢观看


作者:remember_me
来源:juejin.cn/post/7336538738749849600
收起阅读 »

微信小程序:优雅处理分页功能

web
背景 在公司的项目中,分页功能十分常见,以前就是简单的复制粘贴,每次来回切换切面,还经常复制错位置或复制到其他页面去了,半天找不到原因(内心:#*! $%^&@ 1)。 核心思路 分页原理: 属性 page来控制分页数,初始值为1;moreFlag有更多数...
继续阅读 »

背景



在公司的项目中,分页功能十分常见,以前就是简单的复制粘贴,每次来回切换切面,还经常复制错位置或复制到其他页面去了,半天找不到原因(内心:#*! $%^&@ 1)。



核心思路


分页原理:


属性 page来控制分页数,初始值为1;moreFlag有更多数据的标志,初始值为true;pageSize页面自行定义,可以默认设置为10


组件 原生scroll-viewrecycle-view,但内容页面定制化(循环detailList)


方法 bindscrolltolowerbindscrolltoUpper 调取接口获取更多数据,若结果集中的数据量小于pageSize时,moreFlag更新为false,界面激活“暂无更多”标识;bindrefresherrefresh时刷新重置变量为初始值。


后端接口对接 一般接口都会用Promise异步封装,但一般页面中的请求和处理都各不相同,所以不能作为共同点


graph TD
getListChangeStatus初始化 --> getList加载数据 --> getRemoteList调接口输出处理好的结果集 --> getList合并结果集 --> 下拉刷新重置pulldownRefresh

混入


根据上面的分页原理,大都为逻辑处理,所以考虑使用类似Vue里的混入,而在小程序中有一个类似的语法:Behavior

Behavior


image.png


behaviors 是用于组件间代码共享的特性,类似于一些编程语言中的 “mixins” 或 “traits”。


每个 behavior 可以包含一组属性、数据、生命周期函数和方法。组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。  每个组件可以引用多个 behavior ,behavior 也可以引用其它 behavior


定义Behavior


和组件的配置参数差不多,点击查看详情


注意 即使运用在Page里,方法也需放在methods里;同时注意一下同名字段和生命周期的规则,如下图所示:


image.png
以 scroll-view 按照分页原理来定义分页的Behavior


module.exports = Behavior({
data: {
page: 1,
moreFlag: true,
detailList: [],
refreshFlag: false
},
methods: {
// 初始化
async getListChangeStatus() {
this.setData({
page: 1,
detailList: [],
moreFlag: true
})
await this.getList()
},
// 获取列表
async getList() {
if (!this.data.moreFlag) {
... // 可以增加处理,例如吐司等
return
}
let { detailList,fun } = await this.getRemoteList()
if (detailList) {
let detailListTem = this.data.detailList
if (this.data.page == 1) {
detailListTem = []
}
if (detailList.length < this.data.pageSize) {
//表示没有更多数据了
this.setData({
detailList: detailListTem.concat(detailList),
moreFlag: false
})
} else {
this.setData({
detailList: detailListTem.concat(detailList),
moreFlag: true,
page: this.data.page + 1
})
}
// 可能需要一些处理,例如获取容器的高度之类的,又或者scroll-int0-view对应id
if(fun && fun instanceof Function) fun()
}
},
// 到达底部
reachBottom() {
console.log('--reachBottom--')
this.getList()
},
// 下拉刷新重置
pullDownRefresh() {
this.setData({
page: 1,
hasMoreData: true,
detailList: []
})
this.getList()
setTimeout(() => {
this.setData({
refreshFlag: false
})
}, 500)
}
}
})

引入方法


在页面或组件中(如果页面多个需要分页的地方建议用组件)使用behaviors: [myBehavior]


import pagination from '../../behaviors/pagination.js'
Page({
data: {
pageSize: 10
},
behaviors: [pagination],
onShow() {
this.getListChangeStatus()
},
async getRemoteList() {
let { page, pageSize } = this.data
const returnObj = {}
const res = await XXX // 请求接口
... // 处理数据
returnObj.detailList = data
return returnObj
}
})


<scroll-view scroll-y refresher-enabled refresher-triggered="{{refreshFlag}}" bindrefresherrefresh="pullDownRefresh" bindscrolltolower="reachBottom" style="height:100%;">

scroll-view>



作者:HuaHua的世界
来源:juejin.cn/post/7267417634478719036
收起阅读 »

宏观经济的一些判断

估计谁都没想到2023年,疫情恢复的第一年竟然如此:大厂裁员、楼市继续下跌、年底股市暴跌等。 这篇文章说下笔者对宏观经济的一些理解和判断,以试图解释过去一年发生的一些事情。 宏观经济周期 笔者看来,由于美元具备全球货币属性,美元加息是全球的麻烦。所以总体而言,...
继续阅读 »

估计谁都没想到2023年,疫情恢复的第一年竟然如此:大厂裁员、楼市继续下跌、年底股市暴跌等。


这篇文章说下笔者对宏观经济的一些理解和判断,以试图解释过去一年发生的一些事情。


宏观经济周期


笔者看来,由于美元具备全球货币属性,美元加息是全球的麻烦。所以总体而言,全球经济处于美元加息影响下的结果中,也就是处于美元回流,全球货币紧缩,全球经济处于下行周期中


但每个主要经济体又有其自身的宏观经济周期,比如说我们国家,疫情三年为了人民的生命健康,我们国家没有像西方打开方便之门,大肆放水,而是选择严守死防。


三年疫情,我们国家保护了人民的生命健康,但我们也封闭了三年,紧缩了三年。我们国家处于疫情恢复周期中,处于上行周期中。但始作俑者的美国因为超发货币,自身经济过热,所以需要降低过热经济,所以宏观经济也处于下行周期中。


这里说一个题外话,宏观经济是通缩好还是通货膨胀好呢?实际上,过分的通缩和过分的通货膨胀都不好,适度的通胀最好,一般来说维持在2%左右是比较好的,这也就是美国一直想通过加息实现的通胀水平。


不管是通缩还是通胀代表的都是一种趋势,通缩经济逐渐下行,规模逐渐减小,反之通胀经济处于上行,规模逐渐扩大,这样才能实现GDP的增长,实现经济的发展。


回来继续说宏观经济周期,当下国家要做的就是让经济处于扩张区间,或者说实现经济的适度通胀。


2023年CPI和PPI一直不温不火,其实有一点通缩的迹象,所以国家在出台各种手段改善这种迹象:



  1. 对楼市逐渐降低首付比例,逐渐放开楼市,逐渐降息,逐渐降低存款准备金等

  2. 对股市降低印花税,转融通暂停,加大逆回购力度,证监会新管理层上任等


但改变趋势谈何容易,同时还要考虑美国的加息节奏。所以出台的手段并不是那么尽如人意,也因为受到美国加息节奏的影响,为了维持外汇的稳定性,出台的时机也并未尽如人意。


笔者觉得正因为上述原因:美元加息周期、国内降息周期导致2023大厂裁员、楼市继续下跌、年底股市暴跌。


2024年宏观经济


目前美国已经暂停加息半年,加息周期已经到顶,预期5、6月份后降息。也正因为暂停加息,加息到顶我国央行今天进一步降低5年期贷款利率。


2024年的国内的宏观经济依然是将经济恢复为适度通胀的水平。笔者的判断是在汇率稳定的前提下,稳定楼市,因为楼市涉及的面太大。稳定了楼市才能稳定经济稳定预期,才能稳定股市。


完全有可能的是央行在美国降息后选择再一次降息贷款利息的方式,加强人们的信心,加强人们对国家进一步搞活经济的决心,给人们信心。


稳定楼市而不是大力发展楼市。笔者认为未来国家的发展会逐渐向高科技产业倾斜,从依靠楼市带动GDP增长改为依靠科技带动。


综上,一些配套的改革一定也会有:比如真正支持优秀公司发展的证券市场,比如政府职能转变为服务好企业,比如更加开放的区域市场(现在的浦东新区)等等。


而笔者比较关心的股票市场,隶属于证券市场,笔者觉得未来中国的股市一定会越来越好,尤其优秀的科技公司。


(本文完)


作者:通往自由之路
来源:juejin.cn/post/7337227407365963803
收起阅读 »

打工人应避免被过度体制化

《肖申克的救赎》是一部令人震撼的电影。其给我留下深刻印象的不止男主的筹谋,还有终获假释,却选择上吊自杀的老布。老布是一个悲剧人物。 老布被关押监狱五十年。半个多世纪,老布从青年变为白发苍苍的老头,他最终熟悉了监狱的一切,适应了监狱的一切,也和监狱捆绑在了一起。...
继续阅读 »

《肖申克的救赎》是一部令人震撼的电影。其给我留下深刻印象的不止男主的筹谋,还有终获假释,却选择上吊自杀的老布。老布是一个悲剧人物。


老布被关押监狱五十年。半个多世纪,老布从青年变为白发苍苍的老头,他最终熟悉了监狱的一切,适应了监狱的一切,也和监狱捆绑在了一起。用电影中的话来说,老布被监狱“体制化”了,包括他的自我认同,自我习惯。他依赖这个监狱。


也正因为此,出狱后的老布因为无法适应、融入发生巨变的社会环境,选择结束自己的一生。


体制化是指是个人或群体在某种环境中逐渐适应并接受其中的规则、习惯、意识和氛围,以至于这些成为其思考和行为的一部分。比如说销售人员更会自我激励,司机师傅更容易记下位置路线,财务人员更细致……


现代社会高度发展,每个人都不可避免的依赖某个组织或企业。为了融入组织或企业,被体制化是必然。但过度被体制化,比如长期生活在农村的老人,长期在高速口收费的收费员,长期无法晋升的公司老白兔……不利于人的发展,应该主动避免。


分工


现代企业建立的基础之一是大规模生产下的分工,分工可以提高生产效率,但分工也让人失去独立性。就拿上面长期在高速口收费的收费员举例,几年前河北高速一个收费员被辞退,就闹了起来,原因是他别的什么都不会了。


长期单一职责一旦被离职很容易陷入困境,轻者生活质量下降;重者长期失业;更重者,如老布一般选择终结自己的生命,事实上前几年就发生了这种事……


企业机会狼多肉少


几年前读过一句话:我们的社会实际是一个精英筛选系统,大多数人存在的意义是辅助筛选。这句话套用在企业也适用,企业是一个小型的精英筛选系统。


这些年互联网行业流行的所谓35岁职场危机,背后实际是部分企业认定35岁没能成为精英,也就不具人力性价比,选择不再对其招聘。


35岁大部分都有了家庭,要是35岁没有成为精英,相关企业招聘的意愿又不强,只能等着被辞退。要是在被过度体制化,被拴在一家企业,那无疑是雪上加霜。


体制化是放弃整个森林


我对企业有很多定义,其中之一企业是社会当中满足一系列特定需求的组织。一系列特定需求实际是局部市场。在局部市场里,企业注定机会有限。而市场本身比企业拥有更多机会,被过度体制化会被动放弃拥有更多机会的可能。


积极培养第二曲线甚至第三曲线


随着技术的提高,经济的发展,现代职业的生命周期在缩短。我认为每个人应该积极培养第二甚至第三职业。第二职业可以是自媒体,比如写作、比如up主、再比如小红书博主……可以是其他擅长的,可以利用下班时间去做的事业。


第三职业可以是投资人,投资是坐顺风车的学问,重要的是识别顺风车。做投资可以先从二级市场,也就是股票市场开始(投资需谨慎)。要能够借国家发展的大势,享受国家发展带来的红利。


我之前写过一篇关于宏观经济的文章《宏观经济的一些判断》。目前,我国经济处于上行周期中,受房地产拖累,经济爬坡有些缓慢。上个月CPI同比上涨0.7%,消费意愿在加强;上个月社融处于增长态势,经济活动相对活跃,上个月规模以上企业工业增加值显著增长……


种种迹象表明经济在好转,我也相信经济一定会好起来,经济好起来叠加国家对证券市场的进一步规范,不久以后股市完全有可能成为新的国民财富蓄水池。


结尾


总之,在工作过程中要识别哪些规则、习惯、意识和氛围,长期来讲是真正的机会,哪些不是。到了一定年龄,看不到成为公司精英的希望,应发展更多社会生存技能,在社会这个更大市场中生根发芽,创造财富,获取财富。


也希望能够直接借到国家发展大势,直接享受国家发展红利。


作者:通往自由之路pro
来源:juejin.cn/post/7348013957732941876
收起阅读 »

微信小程序商城分类滚动列表锚点

web
一、需求背景最近接了个商城小程序的项目,在做商品分类页面的时候,一开始是普通分类列表,但是客户觉得效果不理想,想要滚动列表的效果,需要实现以下功能:列表滑动效果;滑动切换分类;点击分类跳转到相应的分类位置。思路是用使用官方组件scroll-view,给每个分类...
继续阅读 »

一、需求背景

最近接了个商城小程序的项目,在做商品分类页面的时候,一开始是普通分类列表,但是客户觉得效果不理想,想要滚动列表的效果,需要实现以下功能:

  1. 列表滑动效果;
  2. 滑动切换分类;
  3. 点击分类跳转到相应的分类位置。

思路是用使用官方组件scroll-view,给每个分类(子元素)添加锚点,然后记录每个分类项的高度,监听scroll-view组件滚动事件,计算分类的跳转

二、效果演示

录制_2023_04_18_11_25_56_701.gif

三、核心代码实现

下面要使用到的方法都来自于查阅微信小程序官方文档

  1. 创建一个scoll-view 并配置需要用到的属性scroll-int0-view 根据文档描述此属性是子元素的id,值为哪个就跳到那个子元素。为了使跳转不显得生硬,再添加scroll-with-animation属性,然后创建动态生成分类的dom元素并为每个子元素添加相应的id

image.png

        <view class="content">

<scroll-view scroll-y scroll-with-animation class="left" style="height:{{height}}rpx;" scroll-int0-view='{{leftId}}'>
<view id='left{{index}}' class="left-item {{activeKey===index?'active':''}}" wx:for="{{navData}}" data-index='{{index}}' wx:key='id' bindtap="onChange">
<text class='name'>{{item.name}}text>
view>
scroll-view>

<scroll-view class="right" scroll-y scroll-with-animation scroll-int0-view="{{selectedId}}" bindscroll="changeScroll" style='height:{{height}}rpx;'>

<view class="item" wx:for="{{goodslist}}" wx:key="id" id='type{{index}}'>

<view class="type">【{{item.name}}】view>

<view class="item-list">
<navigator class="list-item" wx:for="{{item.list}}" wx:for-item='key' wx:key="id" url='/pages/goods/goods?id={{key.id}}'>
<image style="width: 100%; height: 180rpx;" src="{{key.imgurl}}" />
<view class="item-name">{{key.goods_name}}view>
navigator>
view>
<view wx:if="{{item.list.length===0}}" class="nodata">
暂无商品
view>
view>
scroll-view>
view>

css部分

这里用到了吸顶效果position: sticky;

        .content {
width: 100%;
height: calc(100% - 108rpx);
overflow-y: hidden;
display: flex;

.left {
height: 100%;
overflow-y: scroll;
.left-item {
width: 100%;
padding: 20rpx;
box-sizing: border-box;

.name {
word-wrap: break-word;
font-size: 28rpx;
color: #323233;
}
}

.active {
border-left: 6rpx #ee0a24 solid;
background-color: #fff;
}
}

.right {
flex: 1;

.item {
position: relative;
padding: 20rpx;

.type {
margin-bottom: 10rpx;
padding: 5rpx;
position: sticky;
top: 0;
background-color: #fff;
}

.item-list {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 20rpx;
text-align: center;

.item-name {
color: #3a3a3a;
font-size: 26rpx;
margin-top: 10rpx;
}
}

.nodata{
padding: 20rpx;
color: #ccc;
}
}
}
}

2. 在列表渲染完成之后计算出每个分类的高度并且保存成一个数组

// 用到的data
data:{
// 分类列表
navData:[],
// 商品列表
goodslist:[],
// 左侧分类选中项 分类列表数组的下标
activeKey:0,
// 计算出的锚点的位置
heightList:[],
// 右侧子元素的锚点
selectedId: 'type0',
// 左侧分类的锚点
leftId:'left0',
// scroll-view 的高度
height:0
},
onShow() {
let Height = 0;
wx.getSystemInfo({
success: (res) => {
Height = res.windowHeight
}
})
const query = wx.createSelectorQuery();
query.selectAll('.search').boundingClientRect()
query.exec((res) => {
// 计算滚动列表的高度 视口高度减去顶部高度 *2是因为拿到的是px 虽然也可以 但是我们通常使用的是rpx
this.setData({
height: (Height - res[0][0].height) * 2
})
})
},

//计算右侧每个锚点的高距离顶部的高
selectHeight() {
let h = 0;
const query = wx.createSelectorQuery();
query.exec((res) => {
console.log('res', res)
let arr=res[0].map((item,index)=>{
h+ = item.height
return h
})
this.setData({
heightList: arr,
})
console.log('height', this.data.heightList)
})
},

使用到的相关API image.png

  1. 监听scroll-view的滚动事件,通过滚动位置计算当前是哪个分类。
changeScroll(e) {
// 获取距离顶部的距离
let scrollTop = e.detail.scrollTop;
// 当前分类选中项,分类列表下标
let {activeKey,heightList} = this.data;
// 防止超出分类 判断滚动距离是否超过当前分类距离顶部高度
if (activeKey + 1 < heightList.length && scrollTop >= heightList[activeKey]) {
this.setData({
// 左侧分类选中项改变
activeKey: activeKey + 1,
// 左侧锚点对应位置
leftId: `left${activeKey + 1}`
})
}
if (activeKey - 1 >= 0 && scrollTop < heightList\[activeKey - 1]) {
this.setData({
activeKey: activeKey - 1,
leftId: `left${activeKey - 1}`
})
}
},

4. 监听分类列表点击事件,点击分类跳转相应的分类商品列表

onChange(event) {
let index = event.currentTarget.dataset.index
this.setData({
activeKey: index,
selectId: "item" + index
});
},

四、总结

左侧分类一开始是用的vantUI的滚动列表,但是分类过多就不会随着滑动分类滚动到可视位置,所以改成自定义组件,反正也很简单。

最初是想根据右侧滚动位置给左侧的scroll-view添加scroll-top,虽然实现,但是有时会有一点小问题目前没有想好怎么解决,改用右侧相同方法实现可以解决。

css部分使用scss编写,使用的是vscode的easy scss插件,具体方法百度一下,很简单。


作者:丝网如风
来源:juejin.cn/post/7223211123960660028

收起阅读 »

如何防止网站信息泄露(复制/水印/控制台)

web
一、前言 中午休息的时候,闲逛公司内网,看到一个url,就想复制一下url,看看url对应的内容是啥,习惯性使用ctrl+c,然后ctrl+v,最后得到是 禁止复制,哦,原来是禁用了复制。这能难倒一个前端开发吗?当然不能。于是打开了控制台,这时,发现页面自动执...
继续阅读 »

一、前言


中午休息的时候,闲逛公司内网,看到一个url,就想复制一下url,看看url对应的内容是啥,习惯性使用ctrl+c,然后ctrl+v,最后得到是 禁止复制,哦,原来是禁用了复制。这能难倒一个前端开发吗?当然不能。于是打开了控制台,这时,发现页面自动执行了一段立即执行函数,函数体里面是debugger代码,然后手动跳过debugger后,页面已经变成一个空白页面了。


本文将简单讲解禁止复制、水印和禁止打开控制台三个功能点的实现。


前面几节是分功能讲解,最后一节将会写出完整的代码,如何防止网站信息泄露。


二、禁止复制


现在有的复制网页(常规网页用户,不开控制台的情况)的内容方式有:



  1. 选择 -> ctrl+c(command + c)

  2. 选择 -> 鼠标右键 -> 复制


js拦截


相比user-select无法做到某一些内容可以被选中


document.addEventListener('contextmenu', function(e) {  
e.preventDefault();
}, false);

document.addEventListener('selectstart', function(e) {
e.preventDefault();
}, false);


user-select


不难发现,当我们复制内容的时候,首选是选择目标内容,那我们可以禁用用户的选择目标内容。


css属性user-select用于控制用户是否能够选择(即复制)文本和其他页面元素。它的作用是指定用户在浏览网页时是否能够选择和复制页面上的文本和其他元素。


<h3>user-select: none;</h3>
<div style="user-select: none;">我是捌玖,欢迎关注我哟,这儿是利用css样式,测试能否禁用复制</div>
<div style="user-select: none;">哈哈哈,当然,这种方式是无效的,我只是玩下</div>

那user-select和pointer-event的区别是啥?
pointer-events  指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件的 target (en-US)。通俗一点讲,例如我们给某个元素设置了pointer-events: none,当我们点击这个元素的时候,是不会触发click事件,包括我们即使通过鼠标也无法选中该元素。

user-select 用于控制用户是否可以选择文本。这不会对作为浏览器用户界面(即 chrome)的一部分的内容加载产生任何影响,除非是在文本框中。



注意:user-select是无法拦截input中的选中(鼠标/ctrl+a)



拦截ctrl + a


每个人(系统)使用使用组合键进行复制,可能键是存在差异的,就拿我电脑是mac,默认是command + a为复制,当然是可以修改的,看个人使用习惯,所以我们要同时拦截掉ctrl + a 和 command + a。


document.keyDown = function(event) {
const {ctrlKey, metaKey, keyCode} = event;
if((ctrlKey || metaKey) && keyCode === 65) {
return false;
}
}

拦截ctrl+c(command + c)


每个人(系统)使用使用组合键进行复制,可能键是存在差异的,就拿我电脑是mac,默认是command + c为复制,当然是可以修改的,看个人使用习惯,所以我们要同时拦截掉ctrl + c 和 command + c的。不可以直接拦截c键的输入,会影响到input框的输入


document.keyDown = function(event) {
const {ctrlKey, metaKey, keyCode} = event;
if((ctrlKey || metaKey) && keyCode === 67) {
return false;
}
}

直接拦截鼠标右键


该方法直接拦截了右键菜单的打开,主要用于拦截图片的复制,菜单中的复制图片的方法有多种(复制图片、复制图片地址等),暂时没找到合适的方法拦截菜单中具体的某一项。
image.png


document.oncontextmenu = function(event){
if(event.srcElement.tagName=="IMG"){
alert('图片直接右键');
return false;
}
};

三、水印


网站水印的主要作用是版权保护和网站标识展示。具体来说,它可以在图片上加上作者的信息或标识,防止他人未经授权使用,有助于保护图片的版权安全。同时,它也可以在网页中展示特定的标识或信息,如网站的名称、网址、版权信息等,有助于提高网站的知名度和品牌形象。


此外,网站水印还可以用于追踪网站的非法使用和侵权行为。当发现某个网站上出现了未经授权的水印,就可以通过水印的信息追踪到该网站的使用者,从而采取相应的法律措施。


// 创建Canvas元素  
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');

// 设置Canvas尺寸和字体样式
canvas.width = 100;
canvas.height = 100;
context.font = '30px Arial';
context.fillStyle = 'rgba(0,0,0,0.1)';

// 绘制文字到Canvas上
context.fillText('捌玖', 10, 50);

// 生成水印图像的URL
const watermarkUrl = canvas.toDataURL();

// 在页面上显示水印图像(或进行其他操作)
const divDom = document.createElement('div');
divDom.style.position = 'fixed';
divDom.style.zIndex = '99999';
divDom.style.top = '-10000px';
divDom.style.bottom = '-10000px';
divDom.style.left = '-10000px';
divDom.style.right = '-10000px';
divDom.style.transform = 'rotate(-45deg)';
// 避免对用户的交互产生影响
divDom.style.pointerEvents = 'none';
divDom.style.backgroundImage = `url(${watermarkUrl})`;
document.body.appendChild(divDom);

四、禁止打开控制台


禁止用户打开控制台



  1. 防止代码被窃取:在控制台中,用户可以查看和修改网页的源代码,这可能会导致恶意用户窃取您的代码或敏感信息。通过禁止打开控制台,可以保护您的代码和数据不被未经授权的用户访问或篡改。

  2. 提高安全性:控制台是网页与用户之间进行交互的主要渠道之一,但也是潜在的安全风险之一。恶意用户可以利用控制台执行恶意代码、进行钓鱼攻击等。禁止用户打开控制台可以减少这些潜在的安全风险。

  3. 保护系统资源:在控制台中,用户可以执行各种操作,例如创建新文件、删除文件等,这可能会对系统资源造成不必要的占用和破坏。禁止打开控制台可以限制用户的操作范围,保护系统资源不被滥用。

  4. 防止误操作:控制台允许用户进行各种操作,但这也增加了误操作的风险。禁止打开控制台可以减少用户误操作的可能性,避免不必要的损失和风险。


let firstTime;
let lastTime;
setInterval(() => {
firstTime = Date.now()
debugger
lastTime = Date.now()
if (lastTime - firstTime > 10) {
window.location.href = "about:blank";
}
}, 100)

五、总结


    // 防止用户选中
function disableSelect() {
// 方式一,js拦截
// 缺点: 无法做到某一些内容可以选中
document.onselectstart = function(event){
e.preventDefault();
};


// 方式:给body设置样式
document.body.style.userSelect = 'none';


// 禁用input的ctrl + a
document.keyDown = function(event) {
const {ctrlKey, metaKey, keyCode} = event;
if((ctrlKey || metaKey) && keyCode === 65) {
return false;
}
}
};

// 禁用键盘的复制
function disableCopy() {
const {ctrlKey, metaKey, keyCode} = event;
if((ctrlKey || metaKey) && keyCode === 67) {
return false;
}
}

// 禁止复制图片
function disableCopyImg() {
document.oncontextmenu = function(event){
if(event.srcElement.tagName=="IMG"){
alert('图片直接右键');
return false;
}
};
};

// 生成水印
function generateWatermark(keyword = '捌玖') {
// 创建Canvas元素
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');

// 设置Canvas尺寸和字体样式
canvas.width = 100;
canvas.height = 100;
context.font = '30px Arial';
context.fillStyle = 'rgba(0,0,0,0.1)';

// 绘制文字到Canvas上
context.fillText(keyword, 10, 50);

// 生成水印图像的URL
const watermarkUrl = canvas.toDataURL();

// 在页面上显示水印图像(或进行其他操作)
const divDom = document.createElement('div');
divDom.style.position = 'fixed';
divDom.style.zIndex = '99999';

// 因为div旋转了45度,所以div需要足够的大
divDom.style.top = '-10000px';
divDom.style.bottom = '-10000px';
divDom.style.left = '-10000px';
divDom.style.right = '-10000px';
divDom.style.transform = 'rotate(-45deg)';

// 防止对用户的交互产生影响
divDom.style.pointerEvents = 'none';
divDom.style.backgroundImage = `url(${watermarkUrl})`;
document.body.appendChild(divDom);
}

// 禁止打开控制台
function disbaleConsole() {
let firstTime
let lastTime
setInterval(() => {
firstTime = Date.now()
debugger
lastTime = Date.now()
if (lastTime - firstTime > 10) {
window.location.href = "about:blank";
}
}, 100);
}

disableSelect();
disableCopy();
disableCopyImg();
generateWatermark();
disbaleConsole();

作者:捌玖ki
来源:juejin.cn/post/7292416512333332534
收起阅读 »

瘫痪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环境:1.Harmony 安装 三...
继续阅读 »

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

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

1.Harmony 安装 三方依赖 在DevEco Studio 将依赖放到指定的 oh-package-json5 的 dependencies 内

2.然后打开 Terminal 执行 :“ohpm install”
1.成功会提示

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

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

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

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

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

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


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