注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

uni-app实现微信小程序蓝牙打印

web
打印流程 小程序连接蓝牙打印大致可以分为九步:1.初始化蓝牙模块、2.开始搜索附近的蓝牙设备、3.获取搜索到的蓝牙列表、4.监听寻找到新设备的事件、5.连接蓝牙设备、6.关闭搜索蓝牙设备事件、7.获取蓝牙设备的所有服务、8.获取服务的所有特征值、9.向蓝牙设备...
继续阅读 »

打印流程


小程序连接蓝牙打印大致可以分为九步:1.初始化蓝牙模块、2.开始搜索附近的蓝牙设备、3.获取搜索到的蓝牙列表、4.监听寻找到新设备的事件、5.连接蓝牙设备、6.关闭搜索蓝牙设备事件、7.获取蓝牙设备的所有服务、8.获取服务的所有特征值、9.向蓝牙设备写入数据


1.初始化蓝牙模块 uni.openBluetoothAdapter


注意:其他蓝牙相关 API 必须在 uni.openBluetoothAdapter 调用之后使用。


uni.openBluetoothAdapter({
success(res) {
console.log(res)
}
})

2.开始搜索附近的蓝牙设备 uni.startBluetoothDevicesDiscovery


此操作比较耗费系统资源,请在搜索并连接到设备后调用 uni.stopBluetoothDevicesDiscovery 方法停止搜索。


uni.startBluetoothDevicesDiscovery({
success(res) {
console.log(res)
}
})

3.获取搜索到的蓝牙列表 uni.getBluetoothDevices


获取在蓝牙模块生效期间所有已发现的蓝牙设备。包括已经和本机处于连接状态的设备(不是很准确,有时会获取不到)。


uni.getBluetoothDevices({
success(res) {
console.log(res)
}
})

4.监听寻找到新设备的事件 uni.onBluetoothDeviceFound


监听寻找到新设备的事件,跟第三步一起使用,确保能获取附近所有蓝牙设备。


uni.onBluetoothDeviceFound(function (devices) {
console.log(devices)
})

5.连接蓝牙设备 uni.createBLEConnection


若APP在之前已有搜索过某个蓝牙设备,并成功建立连接,可直接传入之前搜索获取的 deviceId 直接尝试连接该设备,避免用户每次都要连接才能打印,省略二三四步减少资源浪费。


uni.createBLEConnection({
deviceId:获取到蓝牙的deviceId,
success(res) {
console.log(res)
}
})

6.关闭搜索蓝牙设备事件 uni.stopBluetoothDevicesDiscovery


停止搜寻附近的蓝牙外围设备。若已经找到需要的蓝牙设备并不需要继续搜索时,建议调用该接口停止蓝牙搜索。


uni.stopBluetoothDevicesDiscovery({
success(res) {
console.log(res)
}
})

7.获取蓝牙设备的所有服务 uni.getBLEDeviceServices


uni.getBLEDeviceServices({
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId,
success(res) {
console.log('device services:', res.services)
}
})

8.获取服务的所有特征值 uni.getBLEDeviceCharacteristics


uni.getBLEDeviceCharacteristics({
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId,
success(res) {
console.log('device getBLEDeviceCharacteristics:', res.characteristics)
}
})
三种不同特征值的id
for (var i = 0; i < res.characteristics.length; i++) {
if (!notify) {
notify = res.characteristics[i].properties.notify;
if (notify) readId = res.characteristics[i].uuid;
}
if (!indicate) {
indicate = res.characteristics[i].properties.indicate;
if (indicate) readId = res.characteristics[i].uuid;
}
if (!write) {
write = res.characteristics[i].properties.write;
writeId = res.characteristics[i].uuid;
}
if ((notify || indicate) && write) {
/* 获取蓝牙特征值uuid */
success &&
success({
serviceId,
writeId: writeId,
readId: readId,
});
finished = true;
break;
}

9.向蓝牙设备写入数据 uni.writeBLECharacteristicValue


向低功耗蓝牙设备特征值中写入二进制数据。注意:必须设备的特征值支持 write 才可以成功调用。


并行调用多次会存在写失败的可能性。


APP不会对写入数据包大小做限制,但系统与蓝牙设备会限制蓝牙4.0单次传输的数据大小,超过最大字节数后会发生写入错误,建议每次写入不超过20字节。


若单次写入数据过长,iOS 上存在系统不会有任何回调的情况(包括错误回调)。


安卓平台上,在调用 notifyBLECharacteristicValueChange 成功后立即调用 writeBLECharacteristicValue 接口,在部分机型上会发生 10008 系统错误


// 向蓝牙设备发送一个0x00的16进制数据
const buffer = new ArrayBuffer(1)
const dataView = new DataView(buffer)
dataView.setUint8(0, 0)
uni.writeBLECharacteristicValue({
// 这里的 deviceId 需要在 getBluetoothDevices 或 onBluetoothDeviceFound 接口中获取
deviceId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId,
// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
characteristicId,
// 这里的value是ArrayBuffer类型
value: buffer,
success(res) {
console.log('writeBLECharacteristicValue success', res.errMsg)
}
})

写在最后


DEMO地址:gitee.com/zhou_xuhui/… (plus可能会报错,demo中注释掉就好,不影响流程)


打印机CPCL编程参考手册(CPCL 语言):http://www.docin.com/p-2160

作者:我是真的菜呀
来源:juejin.cn/post/7246264754141773885
10502…

收起阅读 »

LeCun再爆金句:ChatGPT?连条狗都比不上!语言模型喂出来的而已

【新智元导读】 LeCun昨天在一场辩论中再贬ChatGPT!形容这个AI模型的智力连狗都不如。 图灵三巨头之一的LeCun昨日又爆金句。 「论聪明程度,ChatGPT可能连条狗都不如。」 这句话来自本周四LeCun在Vivatech上和Jacques Att...
继续阅读 »
【新智元导读】 LeCun昨天在一场辩论中再贬ChatGPT!形容这个AI模型的智力连狗都不如。

图灵三巨头之一的LeCun昨日又爆金句。


「论聪明程度,ChatGPT可能连条狗都不如。」


这句话来自本周四LeCun在Vivatech上和Jacques Attalie的一场辩论,可谓精彩纷呈。


图片


CNBC甚至直接把这句话放到了标题里,而LeCun也在之后火速转推。


“ChatGPT和狗:比不了一点”


LeCun表示,当前的AI系统,哪怕是ChatGPT,根本就不具备人类的智能水平,甚至还没有狗聪明。


要知道,在AI爆炸发展的今天,无数人已经为ChatGPT的强大性能所折服。在这种情况下,LeCun的这句话可谓惊世骇俗。


图片


不过,LeCun一贯的观点都是——不必太过紧张,如今的AI智能水平远远没到我们该担忧的地步。


而其他的科技巨头则基本和LeCun持截然相反的意见。


比如同为图灵三巨头的Hinton和Bengio,以及AI届人士由Sam Altman挑头签的公开信,马斯克的危机言论等等。


在这种大环境下,LeCun一直「不忘初心」,坚定认为现在真没啥可担心的。


LeCun表示,目前的生成式AI模型都是在LLM上训练的,而这种只接受语言训练的模型聪明不到哪去。


「这些模型的性能非常有限,他们对现实世界没有任何理解。因为他们纯粹是在大量文本上训练的。」


而又因为大部分人类所拥有的知识其实和语言无关,所以这部分内容AI是捕捉不到的。


LeCun打了个比方,AI现在可以通过律师考试,因为考试内容都停留在文字上。但AI绝对没可能安装一个洗碗机,而一个10岁的小孩儿10分钟就能学会怎么装。


图片


这就是为什么LeCun强调,Meta正尝试用视频训练AI。视频可不仅仅是语言了,因此用视频来训练在实现上会更加艰巨。


LeCun又举了个例子,试图说明什么叫智能上的差别。


一个五个月大的婴儿看到一个漂浮的东西,并不会想太多。但是一个九个月大的婴儿再看到一个漂浮的物体就会感到非常惊讶。


因为在九个月大的婴儿的认知里,一个物体不该是漂浮着的。


LeCun表示,如今我们是不知道如何让AI实现这种认知能力的。在能做到这一点之前,AI根本就不可能拥有人类智能,连猫猫狗狗的都不可能。


图片


Attali:我也要签公开信


在这场讨论中,法国经济和社会理论家Jaques Attali表示,AI的好坏取决于人们如何进行利用。


然而他却对未来持悲观态度。他和那些签公开信的AI大牛一样,认为人类会在未来三四十年内面临很多危险。


他指出,气候灾难和战争是他最为关注的问题,同时担心AI机器人会「阻挠」我们。


Attali认为,需要为AI技术的发展设置边界,但由谁来设定、设定怎么样的边界仍是未知的。


这和前一阵子签的两封公开信所主张的内容相同。


图片


图片


当然,公开信LeCun也是压根没搭理,发推高调表示哥们儿没签。


图片


LeCun炮轰ChatGPT——没停过


而在此之前,LeCun针对ChatGPT不止讲过过一次类似的话。


就在今年的1月27日,Zoom的媒体和高管小型聚会上,LeCun对ChatGPT给出了一段令人惊讶的评价——


「就底层技术而言,ChatGPT并不是多么了不得的创新。虽然在公众眼中,它是革命性的,但是我们知道,它就是一个组合得很好的产品,仅此而已。」


图片


「除了谷歌和Meta之外,还有六家初创公司,基本上都拥有非常相似的技术。」


此外,他还表示,ChatGPT用的Transformer架构是谷歌提出的,而它用的自监督方式,正是他自己提倡的,那时OpenAI还没诞生呢。


当时闹得更大,Sam Altman直接在推上给LeCun取关了。


1月28日,LeCun梅开二度,继续炮轰ChatGPT。


他表示,「大型语言模型并没有物理直觉,它们是基于文本训练的。如果它们能从庞大的联想记忆中检索到类似问题的答案,他们可能会答对物理直觉问题。但它们的回答,也可能是完全错误的。」


而LeCun对LLM的看法一以贯之,从未改变。从昨天的辩论就可以看出,他觉得语言训练出来的东西毫无智能可言。


今年2月4日,LeCun直白地表示,「在通往人类级别AI的道路上,大型语言模型就是一条歪路」。


图片


「依靠自动回归和响应预测下一个单词的LLM是条歪路,因为它们既不能计划也不能推理。」


当然,LeCun是有充分的理由相信这一点的。


ChatGPT这种大语言模型是「自回归」。AI接受训练,从一个包含多达14000亿个单词的语料库中提取单词,预测给定句子序列中的最后一个单词,也就是下一个必须出现的单词。


图片


Claude Shannon在上个世纪50年代开展的相关研究就是基于这一原则。


原则没变,变得是语料库的规模,以及模型本身的计算能力。


LeCun表示,「目前,我们无法靠这类模型生成长而连贯的文本,这些系统不是可控的。比如说,我们不能直接要求ChatGPT生成一段目标人群是13岁儿童的文本。


其次,ChatGPT生成的文本作为信息来源并不是100%可靠的。GPT的功能更像是一种辅助工具。就好比现有的驾驶辅助系统一样,开着自动驾驶功能,也得把着方向盘。


而且,我们今天所熟知的自回归语言模型的寿命都非常短,五年算是一个周期,五年以后,过去的模型就没有人再会用了。


而我们的研究重点,就应该集中在找到一种是这些模型可控的办法上。换句话说,我们要研究的AI,是能根据给定目标进行推理和计划的AI,并且得能保证其安全性和可靠性的标准是一致的。这种AI能感受到情绪。」


图片


要知道,人类情绪的很大一部分和目标的实现与否有关,也就是和某种形式的预期有关。


而有了这样的可控模型,我们就能生成出长而连贯的文本。


LeCun的想法是,未来设计出能混合来自不同工具的数据的增强版模型,比如计算器或者搜索引擎。


像ChatGPT这样的模型只接受文本训练,因此ChatGPT对现实世界的认识并不完整。而想要在此基础上进一步发展,就需要学习一些和整个世界的感官知觉、世界结构有关的内容。


然而好玩儿的是,Meta自己的模型galactica.ai上线三天就被网友喷的查无此人了。


图片


原因是胡话连篇。


笑。


参考资料:http://www.cnbc.com/2023/0

作者:新智元
来源:juejin.cn/post/7246334166950150202
6/15/…

收起阅读 »

悟了两星期终于悟了,移动端适配核心思想——没讲懂揍我

web
移动端开发与pc端适配的不同 pc端布局常用方案 所谓适配,就是指我们的项目开发出来用户在不同虚拟环境、不同硬件环境等等各种情况下有一个稳定、正常的展示(简单理解就是不会排版混乱) 先来回顾一下pc端的项目我们如何写页面做到适配的,大部分页面都是采用版心布局的...
继续阅读 »

移动端开发与pc端适配的不同


pc端布局常用方案


所谓适配,就是指我们的项目开发出来用户在不同虚拟环境、不同硬件环境等等各种情况下有一个稳定、正常的展示(简单理解就是不会排版混乱)


先来回顾一下pc端的项目我们如何写页面做到适配的,大部分页面都是采用版心布局的形式。也就是说所有的内容都写在版心容器盒子里,这个容器盒子设置:margin: 0 auto; & min-width: <版心宽度> & width: <版心宽度>就可以保证:




  • 当用户的屏幕(浏览器)宽度很大,或者缩放浏览器到很小比例时,此时浏览器的宽度大于版心盒子的width,版心容器会自动生成margin-left & margin-right,总会保证版心容器处于页面的正中心。


    这里可以提一嘴pc端浏览器缩放的原理:页面所有元素的css宽高都不会改变,只是css像素的在屏幕上展示的大小缩水了,具体点来说,原本700px * 700px的盒子在浏览器上用10cm * 10cm面积(的物理像素)渲染,但现在用原本<浏览器缩放比率> * 10cm * 10cm面积(的物理像素)渲染。




  • 当用户的屏幕小于版心盒子的width,出现横向滚动条,版心盒子的左右margin为0,width内的内容可滑动滚动条完整查看。




可以参考大淘宝pc端官网就是版心布局的实践。


好了,那么问题来了,移动端为啥不能照搬pc端的这种适配方案呢?


我们有必要先梳理一下移动端对页面进行渲染展示的逻辑:


移动端页面渲染的逻辑


<meta name="viewport">的情况


在html文档里没有<meta name="viewport">标签配置的情况下(通过对比即可理解<meta>标签的意义):


plus:如下整个流程篇口语话主要是梳理核心思路,没有一字一板的细节考究



  1. 我们项目中布局写的所有dom元素的css大小都正常(完全按照css大小的预期)在一个非常大的空间进行渲染,这个空间可能不是无限大,但是为了帮助理解,因为这个空间的大小一般不影响我们项目的正常布局,所以我们可以理解为无限大,这是第一步,即项目页面就像在pc端一样完全按照css写的大小以及布局进行渲染。



  1. 因为我们的移动端设备没有电脑屏幕那么大,所以会把第一步在“很大空间”渲染的页面进行缩小,直至缩小到我们的大页面宽度正好与手机屏幕的宽度一样即可。所以第二步相当于为了让用户把页面看全,手机自动把页面缩小至屏幕内。


为了帮助大家理解,也验证我上面的说法,我写了如下的pc端的版心布局的页面:


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Document</title>
</head>
<style>
.container {
   width: 1200px;
   min-width: 1200px;
   margin: 0 auto;
   border: 2px solid red;
   background: yellow;
}
.container .content {
   height: 3000px;
}
</style>
<body>
   <div class="container">
       <div class="content">内容</div>
   </div>
</body>
</html>

我把上面的页面部署到osrc(一个国内的免费部署网站,类似于vercel)上了(不可用chrome浏览器的移动端去模拟移动端访问的场景,chrome浏览器只是模拟了屏幕大小,而并没有模拟移动端环境,也就是说根本不会处理<meta>标签,所以这里我们需要部署),大家可以自行用pc端和移动端访问体验(实践一下绝对秒懂我上面的文字),为了照顾没有双端设备的读者,我截一下图(直接喂饭到胃哈哈)


pc端访问:


pc端访问版心布局.png


移动端访问:


移动端访问版心布局.jpg


清晰了吧兄弟们,我们写死的1200px宽的container盒子因为手机本身没这么大,所以缩小之后塞进了手机屏幕中,仔细看手机中的文字,已经被缩小的看不清了。


配置<meta name="viewport">的情况


暂时只给我们的index.html<meta name="viewport">添加一个content="width=device-width, initial-scale=1.0"


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Document</title>
</head>
<style>
.container {
   width: 1200px;
   min-width: 1200px;
   margin: 0 auto;
   border: 2px solid red;
   background: yellow;
}
.container .content {
   height: 3000px;
}
</style>
<body>
   <div class="container">
       <div class="content">内容</div>
   </div>
</body>
</html>

重新部署后访问查看效果,有meta的页面,部署地址<meta>标签是针对移动端的,所以pc端完全没影响,跟上面一样。现在我们访问移动端效果如下,我没有缩小图片,注意观察页面底部出现滚动条了(纵向滚动条有滚动所以文字没展示,不重要):


设置meta后移动端访问效果.jpg


解释一下content="width=device-width, initial-scale=1.0"的作用。


解读<meta> & dip & 布局视口(自认为最精华,全网少数不带偏新人的解释)


其实相当于我们在content字段中进行了两项配置,第一项是width=device-width,第一个width是指布局视口的宽度,引出概念,何为布局视口?还记得我们上面说的在没有<meta>时的那个非常大的布局空间嘛,就是它!我们让这个空间的宽度等于device-widthdevice-width就是指dip即设备独立像素,第二个概念,何为dip(device independent piexl设备独立像素)呢?听我的,完全不要被网上各种乱七八糟的解释弄迷糊了,什么dpr,什么物理像素,我只能说那些东西与我们开发者并没有直接关系,笔者读了几户所有能搜到的各种移动端入门文章,一言难尽... ,我来给出对于dip的理解,每一个型号的移动设备都具有的一个大小,这个大小是用设备独立像素dip来描述的,所以它仅仅是一个描述大小的单位,但是这个单位究竟是多大呢,换句话说dip有何特殊性呢?


在移动端不缩放的情况下,一个css像素等于一个设备独立像素dip

(chrome浏览器的移动端开发工具里显示的就是dip大小)也就是说,我们让布局视口的宽度等于设备的dip宽度,这里注意:布局视口由原来的”无限大“现在改为一个具体的数值,并不会影响页面的正常布局,页面还是会完整渲染, 只是最后不用缩小放进屏幕了,因为我们缩小的目的就是让布局视口完整的展现在屏幕中。因为屏幕不能展示完整整个页面的布局,所以底部出现滚动条。用户可以滚动访问页面全部内容。


其实这里initial-scale=1.0的作用就是让移动端浏览器不自行缩放,不然的话浏览器会把如上页面再缩小,然后放到手机屏幕里去。


关于<meta name="viewport">的最佳实践


简简单单如下:


<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

为什么说这是最佳实践,论证它其实还缺少一个关键点,也就是移动端css单位的选取,概括来说,width=device-width配置与移动端css单位的选取两者相辅相成,共同构成了“最佳实践”。


先说一下css单位选取——以vw为例:vw是相对屏幕大小的一个长度单位,1vw等于移动设备屏幕宽度的1%


如何理解“最佳实践”?


首先width=device-width保证了无论何种机型的移动设备,我们开发时写页面的布局视口始终等于屏幕宽度,但看这一点,确实没啥用。如果再来一个条件:页面中所有长度单位用vw来表达。细品!


如何细品?别忘了初心,我们的目标是在不同的移动设备上都能有统一的展示效果,开品:我们用不同dip宽度的设备打开网页,首先布局视口的大小会随着设备dip的不同而不同,也就是始终等于dip宽度:


布局视口宽度 === 设备dip宽度,

并且我们的所有元素大小单位都是vw,也就是说页面中所有元素大小都以屏幕宽度为参照物。最终的效果就是,一个dip宽度超级小的设备打开网页,与一个dip宽度非常大的设备打开网页,看到的页面内容是完全相似的,也就是每个元素在页面中所占的比例不同设备都一样(不同点就在于屏幕本身的大小不一样)!


一般<meta>标签的content中还会设置initial-scale=1.0, maximum-scale=1.0, user-scalable=no,即不让页面进行缩放,感觉这个看需求吧,不让缩小应该是必须的,因为可以想一想,用户缩小完全没有意义呐!(需要大家自己去理解,属于只可意会),至于让不让放大,应该是看情况吧,反正移动端淘宝官网是允许放大的。


移动端适配方案理解


主流的有vw方案、flexible + rem方案,总而言之,把元素的大小用rem来表示或者vw表示,本质都是以手机屏幕宽度为参考,vw比较直接,表达的意思就是1vw等于手机屏幕宽度的百分之一;rem比较间接,通过flexible.js先把得知屏幕宽度是多少px,然后设置<html>font-size,进而所有元素的rem其实还是表达占屏幕宽度的百分之多少。


当然两种方案都有一些技术细节问题需要解决,比如1px问题、安全区域问题等等。这里就不多说了。


相信能一步一步走到这里的同志,对移动端适配绝对有了一个清晰的把握。


2023.6.19,3: 59。

作者:荣达
来源:juejin.cn/post/7246001188448731196
更文不易,点个赞吧!

收起阅读 »

像支付宝那样“致敬”第三方开源代码

前言 通常我们在App中会使用第三方的开源代码,按照许可协议,我们应该在App中公开使用的开源代码并且附上对应的开源协议。当然,实际上只有少部分注重合规性的大厂才会这么干,比如下图是支付宝的关于界面的第三方信息。当然,对于小企业,基本上都不会放使用的第三方开源...
继续阅读 »

前言


通常我们在App中会使用第三方的开源代码,按照许可协议,我们应该在App中公开使用的开源代码并且附上对应的开源协议。当然,实际上只有少部分注重合规性的大厂才会这么干,比如下图是支付宝的关于界面的第三方信息。当然,对于小企业,基本上都不会放使用的第三方开源代码的任何信息。
image.png


不过,作为一个有“追求”的码农,我们还是想对开源软件致敬一下的,毕竟,没有他们我都不知道怎么写代码。然而,我们的 App 里用了那么多第三方开源插件,总不能一个个找出来一一致敬吧?怎么办?其实,Flutter 早就为我们准备好了一个组件,那就是本篇要介绍的 AboutDialog


AboutDialog 简介


AboutDialog 是一个对话框,它可以提供 App 的基本信息,如 Icon、版本、App 名称、版权信息等。
image.png


同时,AboutDialog还提供了一个查看授权信息(View Licenses)的按钮,点击就可以查看 App 里所有用到的第三方开源插件,并且会自动收集他们的 License 信息展示。所以,使用 AboutDialog 可以让我们轻松表达敬意。怎么使用呢?非常简单,我们点击一个按钮的时候,调用 showAboutDialog 就搞定了,比如下面的代码:


IconButton(
onPressed: () {
showAboutDialog(
context: context,
applicationName: '岛上码农',
applicationVersion: '1.0.0',
applicationIcon: Image.asset('images/logo.png'),
applicationLegalese: '2023 岛上码农版权所有'
);
},
icon: const Icon(
Icons.info_outline,
color: Colors.white,
),
),

参数其实一目了然,具体如下:



  • context:当前的 context

  • applicationName:应用名称;

  • applicationVersion:应用版本,如果要自动获取版本号也可以使用 package_info_plus 插件。

  • applicationIcon:应用图标,可以是任意的 Widget,通常会是一个App 图标图片。

  • applicationLegalese:其他信息,通常会放置应用的版权信息。


点击按钮,就可以看到相应的授权信息了,点击一项就可以查看具体的 License。我看了一下使用的开源插件非常多,要是自己处理还真的很麻烦。
image.png


可以说非常简单,当然,如果你直接运行还有两个小问题。


按钮本地化


AboutDialog 默认提供了两个按钮,一个是查看授权信息,一个是关闭,可是两个按钮 的标题默认是英文的(分别是VIEW LICENSES和 CLOSE)。
image.png


如果要改成本地话的,还需要做一个自定义配置。我们扒一下 AboutDialog 的源码,会发现两个按钮在DefaultMaterialLocalizations中定义,分别是viewLicensesButtonLabelcloseButtonLabel。这个时候我们自定义一个类集成DefaultMaterialLocalizations就可以了。


class MyMaterialLocalizationsDelegate
extends LocalizationsDelegate<MaterialLocalizations>
{
const MyMaterialLocalizationsDelegate();

@override
bool isSupported(Locale locale) => true;

@override
Future<MaterialLocalizations> load(Locale locale) async {
final myTranslations = MyMaterialLocalizations(); // 自定义的本地化资源类
return Future.value(myTranslations);
}

@override
bool shouldReload(
covariant LocalizationsDelegate<MaterialLocalizations> old) =>
false;
}

class MyMaterialLocalizations extends DefaultMaterialLocalizations {
@override
String get viewLicensesButtonLabel => '查看版权信息';

@override
String get closeButtonLabel => '关闭';

}

然后在 MaterialApp 里指定本地化localizationsDelegates参数使用自定义的委托类对象就能完成AboutDialog两个按钮文字的替换。


return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const AboutDialogDemo(),
localizationsDelegates: const [MyMaterialLocalizationsDelegate()],
);

添加自定义的授权信息


虽然 Flutter 会自动收集第三方插件,但是如果我们自己使用了其他第三方的插件的话,比如没有在 pub.yaml 里引入,而是直接使用了源码。那么还是需要手动添加一些授权信息的,这个时候我们需要自己手动添加了。添加的方式也不麻烦,Flutter 提供了一个LicenseRegistry的工具类,可以调用其 addLicense 方法来帮我们添加授权信息。具体使用如下:


LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(
['关于岛上码农'],
'我是岛上码农,微信公众号同名。\f如有问题可以加本人微信交流,微信号:island-coder。',
);
});

这个方法可以在main方法里调用。其中第一个参数是一个数组,是因为可以允许多个开源代码共用一份授权信息。同时,如果一份开源插件有多个授权信息,可以多次添加,只要名称一致,Flutter就会自动合并,并且会显示该插件的授权信息条数,点击查看时,会将多条授权信息使用分割线分开,代码如下所示:


void main() {
runApp(const MyApp());
LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(
['关于岛上码农'],
'我是岛上码农,微信公众号同名。如有问题可以加本人微信交流,微信号:island-coder。',
);
});

LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(
['关于岛上码农'],
'使用时请注明来自岛上码农、。',
);
});
}

image.png


总结


本篇介绍了在 Flutter 中快速展示授权信息的方法,通过 AboutDialog 就可以轻松搞定,各位“抄代码”的码农们,赶紧用起来向大牛们致敬吧!



我是岛上码农,微信公众号同名。如有问题可以加本人微信交流,微信号:island-coder


👍🏻:觉得有收获请点个赞鼓励一下!


🌟:收藏文章,方便回看哦!


💬:评论交流,互相进步!


作者:岛上码农
来源:juejin.cn/post/7246328828837871677

收起阅读 »

值得学习的JavaScript调试技巧

web
引言 最近老大在cr我代码时,我就静静坐在他旁边看他装逼。他一顿断点+抓包+各种骚操作给我看楞了,我没忍住就让他手把手教我,他就和我在一个小屋子里xxxx了几个小时,他手法太快了俺看一遍就忘了,于是乎就靠回忆和资料查找整理了哈高频的js调试技巧,希望能帮助到各...
继续阅读 »

引言


最近老大在cr我代码时,我就静静坐在他旁边看他装逼。他一顿断点+抓包+各种骚操作给我看楞了,我没忍住就让他手把手教我,他就和我在一个小屋子里xxxx了几个小时,他手法太快了俺看一遍就忘了,于是乎就靠回忆和资料查找整理了哈高频的js调试技巧,希望能帮助到各位。


一:console.dir


在打印dom节点时,普通的console.log是纯文本格式,而dir的打印是以对象的方式。因此在输出dom节点时,务必使用dir打印


<div id="main">
<div class="box1">
<p>p1</p>
</div>

</div>

let oD = document.querySelector('.box1')
console.log(oD)//普通的log输出
console.dir(oD)//dir输出方式

image.png


二:二次发起请求


在调试接口时,通常我们会刷新页面然后观察network的接口信息,如果项目加载时间过长,刷新页面查看接口的效率是十分低的。



  1. 对接口请求右键

  2. 选择Relpy xhr发送请求


image.png


三:接口请求参数修改


借助浏览器控制台可以不用修改代码就可以发送不同参数的新请求了。具体操作如下



  1. 对接口请求右键

  2. 选择copy。

  3. 再选择copy as fetch。

  4. 在console区域粘贴上面的请求信息,然后修改请求体参数。

  5. 然后切换到networkl查看最新请求的结果


效果展示


24.gif


四:css查看伪类hover,active样式


在控制台右侧选择:hov可以选择对应dom各种伪类状态下的css样式,十分的便捷


image.png


五:css样式跳转到对应文件查看


选择css样式,按住alt点击就可以跳到对应文件查看具体代码


25.gif


六:控制台输出选择的dom


首先在页面选择指定的位置dom,然后在在控制台使用$0就表示当前选中的dom了


26.gif


七:展开全部dom


有时候我们在页面查找一个dom时,它嵌套层级特别深。这巨他妈蛋疼一层层展开,这个时候我们就需要找到一键全部展开来帮助我们解决这个问题了。


27.gif


右键选择expand就可以展开选择的dom了。


八:断点调试


断点调试是本节最后一个内容了,它也是最核心的内容了,玩的6的是真的6,老大说我搞懂断点调试和对应的堆栈上下文就可以毕业了。(毕业=辞退?还是。。。)下面我列举的仅仅是入门级别的断点调试,只是说明如何上手操作,里面许多东西还望大家多多探索。


1. 打断点方式


代码中:debugger


在需要断点的地方写入debugger,此时程序运行后代码就会卡在这里,等待主人的安排


let a = 10
debugger
a++

浏览器中:



  1. 选择sources

  2. 在指定代码行左侧单击


image.png


2. 断点间调试


第一种断点调试是十分常用的方式,代码会从当前断点直接运行到下一个断点处执行,中间经过代码都默认被执行且跳过。如下图红色按钮就是断点间调试。


image.png


例子演示


28.gif


我们在上图中打了3个断点,逐个点击,首先从断点15行直接跳到断点17行,最后跳到19行。由于异步最后执行,所以最后又跳到断点15行结束。断点经过的地方鼠标移动到变量上可以查看其内部数据。


3. 逐步调试


逐步调试很明显就是字面意思,从当前断点位置开始一行一行的运行代码,稍微注意的是,遇到函数不进入函数的内部,而是直接执行完函数。


image.png


例子演示


29.gif


4. 进入与进出函数调试


逐步调试遇到函数是不进入函数内部的,因此需要借助进入和进出调试方式控制函数的访问


image.png


例子演示


30.gif


5. 逐步调试详细版


上面讲述了第一种逐步调试方式,其遇到函数是不进入函数内部的,而是直接执行函数。因此下面这种方式是逐步调试的详细版,它也是从断点位置逐步的调试运行,遇到函数也会进入函数的内部进行逐步执行。


image.png


九:React/Vue中尝试


有吊毛说react和vue咋调试?嗯,那个吊毛其实就是我,其实也很简单滴。



  1. 在需要调试的代码位置插入debugger

  2. 在浏览器控制台需要查看变量的地方插入断点

  3. 使用各种调试连招一顿操作就行。


代码例子


例如下面的例子,页面最后显示的num是多少?最后是101,不了解批量setState的开始肯定蒙,我们调试看看


import React,{useEffect, useState} from "react";
const Home = () => {
const [num,setNum] = useState(1)
useEffect(()=>{
debugger
setNum(100)
setTimeout(() => {
setNum(num+100)
}, 0);
},[])
return (
<div>num:{num}</div>
)
}
export default Home;

调试演示
根据调试发现,进入定时器的时候num还未更新,还是1。


31.gif


作者:前端兰博
来源:juejin.cn/post/7246376735838060603
收起阅读 »

前端时钟翻页效果,一看就会,一写就fei

web
最近阅读了不少实现翻页效果的文章,受益匪浅,在此写个学习笔记。 一、元素拆解 从侧面来观察这个翻页过程,能看到承载文字内容的主要有三个面板:一个静止在上半部分的面板,显示旧文字的上半部分;一个静止在下半部分的面板,显示新文字的下半部分;第三个是旋转面板,一...
继续阅读 »

最近阅读了不少实现翻页效果的文章,受益匪浅,在此写个学习笔记。


22.gif


一、元素拆解


动画拆解.png


从侧面来观察这个翻页过程,能看到承载文字内容的主要有三个面板:一个静止在上半部分的面板,显示旧文字的上半部分;一个静止在下半部分的面板,显示新文字的下半部分;第三个是旋转面板,一面显示旧文字的下半部分,另一面显示新文字的上半部分。翻转的动画,我们考虑采用FLIP的思想:



  1. 先实现【动画结束帧】的样式;

  2. 再从【动画开始帧】播放。


二、实现结束帧样式


准备工作:用vue脚手架创建一个模板项目,并添加一个div容器:


image.png


<!-- App.vue -->
<template>
<div id="app">
<test-comp/>
</div>
</template>


<!-- Test.vue -->
<template>
<div class="card"></div>
</template>


<style lang="less" scoped>
.card {
position: relative;
border: solid 4px black;
width: 400px;
height: 400px;
perspective: 1000px;
}
</style>


2.1 实现静止的上半面板


image.png


<template>
<div class="card">
<div class="half-card top-half"></div>
<!-- <div class="half-card bottom-half">财</div> -->
</div>

</template>

<style lang="less" scoped>
/* ... */
.half-card {
position: absolute;
width: 100%;
height: 50%;
overflow: hidden;
background-color: #2c292c;
color: white;
font-size: 320px;
}
.top-half {
line-height: 400px;
}
</style>


我们知道line-height配合font-size可以控制文字在垂直方向的位置,大多数情况下,文字顶部与容器顶部的距离公式为(line-height - font-size) / 2。


记容器高度h,文字大小f,容器只显示文字上半部分的情况下,上述距离的值为h - f / 2,即(line-height - f) / 2 = h - f / 2,所以line-height为2h(400px)。


2.2 实现静止的下半面板


image.png


<template>
<div class="card">
<!-- <div class="half-card top-half">发</div> -->
<div class="half-card bottom-half"></div>
</div>

</template>

<style lang="less" scoped>
/* ... */
.bottom-half {
top: 50%;
line-height: 0;
}
</style>


在容器只显示文字下半部分的情况下,完整的文字顶部距离容器顶部的距离是-f / 2,那么就有(line-height - f) / 2 = - f / 2,即line-height = 0;


2.3 实现旋转面板


2.3.1 旋转面板的正面————新文字的上半部分


image.png


<template>
<div class="card">
<!-- <div class="half-card top-half">发</div> -->
<!-- <div class="half-card bottom-half">财</div> -->
<div class="rotating-half">
<div class="half-card front-side"></div>
<!-- <div class="half-card back-side">发</div> -->
</div>
</div>

</template>

<style lang="less" scoped>
/* ... */
.rotating-half {
position: absolute;
width: 100%;
height: 50%;
.half-card {
height: 100%;
}
}
.front-side {
line-height: 400px;
}
</style>


2.3.2 旋转面板的背面————旧文字的下半部分


怎么让一个div背对我们?只要让它绕着自己的腰部横线翻转180度即可(翻跟斗)。


image.png
image.png


<template>
<div class="card">
<!-- <div class="half-card top-half">发</div> -->
<!-- <div class="half-card bottom-half">财</div> -->
<div class="rotating-half">
<!-- <div class="half-card front-side">财</div> -->
<div class="half-card back-side"></div>
</div>
</div>

</template>

<style lang="less" scoped>
/* ... */
.back-side {
line-height: 0;
transform: rotateX(180deg); // !!!!!!!!!!!
}
</style>


现在,如果把正面也加上,会发现这样一个问题:两个面的位置是重叠的,在模板中后声明的背面元素(即使它是背对着我们)会覆盖正面元素。我们想让这两个面在背对我们的状态下都不显示,这就需要到如下的css属性:backface-visibility: hidden。


此外,现在一个旋转面板中带有两个“面”,我们想要这两个面随着父元素面板的3d旋转一起旋转,也就是保持相对静止,这就需要设置旋转面板【将子元素纳入自己的3d变换空间】:transform-style: preserve-3d。


加上css后,让旋转面板简单地旋转一下,看看效果(效果图有点慢):


<style lang="less" scoped>
/* ... */
.rotating-half {
/* ... */
transform-style: preserve-3d;
.half-card {
/* ... */
backface-visibility: hidden;
}
/* to delete */
transition: transform 1s;
&:hover { transform: rotateX(-180deg); }
}
/* ... */
</style>

2.gif


至此,三个面板静态效果已经完成:


image.png


三、播放动画


在第二节已经得到了动画结束时的状态。接下来需要从动画开始的状态进行播放。


3.1 设置好旋转轴


在目标动画中,旋转面板应该是绕着底边进行旋转的。把【变换原点】设置为底边的中点,这样,经过这个点的X轴就和底边所在的直线重合,绕X轴旋转就等价于绕底边旋转:


<style lang="less" scoped>
/* ... */
.rotating-half {
/* ... */
transform-origin: center bottom;
}
/* ... */
</style>

3.2 找到动画开始帧,使用animate播放动画


动画开始时,旋转面板在主面板的下半区域。要从上半区域(无变换状态)到达下半区域,需要绕着底边逆时针旋转180度,因此开始帧所处于的变换状态就是rotateX(-180deg),从而得到动画的关键帧:


【transform: rotateX(-180deg)】->【transform: none】。


我们给旋转面板加上ref,然后在组件挂载完毕时播放即可:


<script>
export default{
mounted() {
this.$refs.rotate?.animate?.(
[
{ offset: 0, transform: 'rotateX(-180deg)' },
// { offset: 1, transform: 'none' },
],
{
duration: 1000,
easing: 'ease-in-out',
},
);
},
};
</script>

2.gif


四、应用


这样的UI组件可能会用于记录时间、比赛分数变化啥的,自然是不能把值写死。考虑如下的应用场景:


<!-- App.vue -->
<template>
<div id="app" class="flex-row">
<test-comp :value="scoreLGD"/>
<h1>VS</h1>
<test-comp :value="scoreLiquid"/>
</div>
</template>


<script>
import TestComp from './Test';
export default {
components: { TestComp },
data() { return {
scoreLGD : 15,
scoreLiquid: 13,
};
},
mounted() {
setInterval(() => {
this.scoreLGD = this.randomInt(99);
this.scoreLiquid = this.randomInt(99);
}, 5000);
},
/* ... */
};

在该场景下,翻页组件需要在更新时而不是挂载时执行动画(因为没有上一个值)。因此我们在组件内部维护一个记录上一个值的状态,然后把动画从挂载阶段移动到更新阶段:


<template>
<div class="card">
<!-- 旧文字上 -->
<div
v-if="staleValue !== undefined"
class="half-card top-half">

{{ staleValue }}
</div>
<!-- 新文字下 -->
<div class="half-card bottom-half">{{ value }}</div>
<!-- 旋转面板 -->
<div ref="rotate" class="rotating-half">
<!-- 新文字上 -->
<div class="half-card front-side">{{ value }}</div>
<!-- 旧文字下 -->
<div
v-if="staleValue !== undefined"
class="half-card back-side">

{{ staleValue }}
</div>
</div>
</div>

</template>

<script>
export default {
props: ['value'],
data() { return { staleValue: undefined }; },
watch: {
value(_, old) { this.staleValue = old; },
},
updated() {
this.$refs.rotate?.animate?.(
[{ offset: 0, transform: 'rotateX(-180deg)' }],
{ duration: 1000, easing: 'ease-in-out' },
);
},
};
</script>


基本完成:


22.gif


总结一下


实现翻页效果 = 实现两块静态面板 + 实现一块双面旋转面板 + 播放旋转动画。


这里用vue写了demo, react应该也差不多,将updated换成layoutEffect等等。


另外,动画也可以用类名加css实现,当元素不在视口可以不播放,一些样式可以改成props配置。总之应该有不少地方还可以迭代优化下。


参考文章如下,分析思路基本一致,代码实现上有差异:

【1】优雅的时钟翻页效果,让你的网页时钟与众不同!

【2】原生JS实现

一个翻页时钟

收起阅读 »

程序员有没有必要成为业务领域专家 ?

看到这个知乎问题时,我的思绪纷飞,往事一幕幕闪现在脑海里,等平静下来,内心变得很笃定。 于是,我做了如下的回答: 非常有必要。 1997年,乔布斯刚刚回归苹果不久,在开发者大会上,一名程序员当众质疑乔布斯不懂技术。 乔布斯,你是一个聪明又有影响力的人。但是很...
继续阅读 »


看到这个知乎问题时,我的思绪纷飞,往事一幕幕闪现在脑海里,等平静下来,内心变得很笃定。


于是,我做了如下的回答:


非常有必要


1997年,乔布斯刚刚回归苹果不久,在开发者大会上,一名程序员当众质疑乔布斯不懂技术。



乔布斯,你是一个聪明又有影响力的人。但是很遗憾也很明显,很多时候你根本不知道自己在做什么。我希望你能用清楚的语言解释一下 Java 编程语言以及其变种是如何阐述 OpenDoc(开源技文档)内置的一些想法。等你说完以后,你能不能跟我们说一说你自己过去七年都干了些什么?



面对这样犀利的提问,乔布斯平静的喝了一口水,低头沉思了几秒,开口这样回答道:



有时候你能取悦一部分的人,但是当你想要作出改变的时候,最难的是某些事情别人做的是对的。我相信 OpenDoc 肯定有一些功能,没有任何其他东西能做到。我其实也不太懂,我相信你肯定能做一些样品出来,可能是一个小型的 app 来展示它的功能,最难的部分是如何将那些功能塞进更大的愿景里面,例如让你每年一个产品能够卖百八十亿美元。


我经常发现,你得从用户体验出发,倒推用什么技术,你不能从技术出发,然后去想如何才能卖出去。在座的没有人比我犯过更多这样的错误,我也搞到伤痕累累,我知道这就是原因,当我们尝试去为苹果思考战略和愿景,都是从能为用户带来什么巨大利益出发,我们可以给用户带来什么,而不是先找一群工程师,大家坐下来,看看我们有什么吊炸天的技术,然后怎么把它卖出去。



我非常认同乔布斯的话。


程序员有的时候沉迷在自己的世界里,执拗的以为“代码就是全部”


但现实并非如此,编码的目的是创造产品或者提供服务,从而在这个商业社会实现更大的价值


而程序员成长为业务领域专家,能够更加深刻的理解公司的产品或者服务,从而更有优势为公司做出贡献。当个人的贡献上升时,公司的认同和利益也会随之而来。




这个回答一天内得到不少赞同,也是我意想不到的,因为我并不觉得我回答得好,看来很多同学都认可这个观点。



熟悉我的朋友都知道 ,我对技术非常有激情,曾经也认为技术意味着一切。


只是后来,工作中遇到越来越多的挫折,很多好朋友也友善的提醒我,不要太执着于技术,我也越来越认识到自己认知的局限性


我不断的去读书、听演讲、思考,依稀之间得到一个结论:"一个 IT 公司的成功 ,技术固然是重要的一环,而公司的产品、用户人群、经营模式是另一个我很少关注且非常重要的维度"。


偶然间我看了乔布斯的一个视频,视频的两句话让我醍醐灌顶。




  • 我相信你肯定能做一些样品出来,可能是一个小型的 app 来展示它的功能,最难的部分是如何将那些功能塞进更大的愿景里面




  • 你得从用户体验出发,倒推用什么技术,你不能从技术出发,然后去想如何才能卖出去




懂业务是一种认知模式,人的能力是多层次的,技术和懂业务并非互斥的关系。


亲爱的程序员朋友,技术是我们的立身之本,但是业务同样重要 , 真诚的希望你做一个既懂技术又懂业务的工程师。




如果我的文章对你有所帮助,还请帮忙点赞、在看、转发一下,你的支持会激励我输出更高质

作者:勇哥java实战分享
来源:juejin.cn/post/7246224746005954616
量的文章,非常感谢!

收起阅读 »

腾讯视频技术团队偷懒了?!

腾小云导读 PC Web 端、手机 H5 端、小程序端、App 安卓端、App iOS 端......在多端时代,一个应用往往需要支持多端。若每个端都独立开发一套系统来支持,将消耗巨大的人力和经费!腾讯视频团队想到一个“偷懒”的方法——能不能只开发一套基础系统...
继续阅读 »


动图封面


腾小云导读


PC Web 端、手机 H5 端、小程序端、App 安卓端、App iOS 端......在多端时代,一个应用往往需要支持多端。若每个端都独立开发一套系统来支持,将消耗巨大的人力和经费!腾讯视频团队想到一个“偷懒”的方法——能不能只开发一套基础系统,通过兼容不同平台的特性,来快速编译出不同平台的应用呢?本篇特邀腾讯视频团队为你分享快速编译出支持多端的应用、一套代码行走天下的“偷懒”历程。欢迎阅读。


目录


1 背景


2 设计思路


3 具体实现


4 总结


01、 背景


腾讯视频搜索在多个端都存在:安卓 App 端搜索、iOS App 端搜索、H5 端搜索、小程序端搜索、PC Web 端、PC 客户端搜索。每个端,除了个别模块的样式有细微差异之外,其他都一样,如下面的图片所示。



按照以前的现状,安卓 App 端搜索一套代码、iOS App 端搜索一套代码、手机 H5 端一套代码、小程序端搜索一套代码、PC 客户端一套代码、PC Web 端一套代码......每套代码都是独立开发,独立维护,成本非常高。并且,后端的搜索接口以前也是分散在多个不同的协议中,有的平台是 jce 协议的接口,有的是 PB 协议的接口,也是五花八门。


随着业务增长的需求,我们已经没有足够的时间来维护各自一套独立的系统,我们打算进行升级改革!治理的办法就是:收敛!把后端不同平台的接口都归一到同一个接口中,通过平台号来区分;前端也将不同平台的代码,收敛归一成一套代码,通过条件编译来兼容适配不同平台的差异性,不同的平台,在蓝盾流水线中配置不同的参数来上线,从而达到多合一的效果。


总体来说,我们团队就实现一个“多端合一的万能模板”的想法达成一致。并且,我们希望使用 hippy-vue 技术栈。


理由有以下:


Hippy 是公司级别的中台框架,有专门的团队在进行问题的修复和功能的迭代开发,并且广泛应用到了很多公司级的应用中,暂时不会出现“荒芜丢弃”的局面; Hippy 是为了抹平 iOS、Android 双端差异,提供接近 Web 的开发体验而生,在上层支持了 React 和 Vue 两套界面框架,前端开发人员可以通过它,将前端代码转换为终端的原生指令,进行原生终端 App 开发; Hippy 在底层进行了大量的优化,使利用 Hippy 框架开发的终端 App 应用,在启动速度,可复用列表组件、渲染效率、动画速度、网络通信等方面都提供了业内顶尖的性能表现,值得信赖; Hippy 在上层支持 Vue 技术栈,正好我们团队目前所有的前端项目也都统一为 Vue 技术栈,开发人员上手毫无违和感。

02、设计思路


系统的架构图如下所示:



通用模版为了简化开发、提高开发效率,在模版中集成了大量现有组件和工具包,具体可以分为以下三层:



  • 第三方工具层


在通用工具库中,模版包装并提供了很多常用方法,比如 cookie 的设置和 cookie 的获取方法;DOM 的操作方法;Cache 的设置,Cache 的获取,Cache 的过期时间等。在第三方接入库中,模版已经接好了 Aegis 监控,Tab 实验的实验值获取,大同上报等;在打包编译库中,模版提供了通用的 Hippy App 打包安卓脚本和 IOS 脚本、H5 的打包脚本、小程序地打包脚本、一套代码,运行不同的打包命令,执行不同的编译打包脚本,就可以生成不同平台对应的发布包。编译打包在后续还会详细讲解。



  • 数据管理层


在这层中,模版集成了跟数据处理相关的模块。


在 Store 层,由于该模块是基于 Vue2 实现的(Vue3 会在下一个版本中提供),模版已经集成好了 Vuex、State、Getters、Mutations、Actions 等,并且都有实例代码(该模版是基于 Vue2 实现的,Vue3 会在下一个版本中提供);


在 Model 层,模版提供了一套将 PB 文件转化为 TS 类文件的方法,方便快速接入后端PB协议接口请求;同时,还包装了接口通用请求方法,以及全局的统一错误处理上报方法;在数据配置中,模版提供了全局的常量配置文件,应用的版本配置文件(版本的配置对 Hippy App 的应用非常实用),以及 UI 样式的配置(正常模式样式还是暗黑模式样式,宽屏,窄屏等)。



  • UI 层


为了提高开发速度,提高开发效率,模版提供了示例页面代码。同时,根据脚手架来选择是否需要路由,来动态添加应用的路由;以及常用的基础组件库。这些组件库中的组件,是从众多 Hippy 应用中提取出来,实用又高效。


03、具体实现


本文将从 Hippy App 端实现,Hippy H5 端的实现和 Hippy 微信小程序端端实现来分别展开介绍。


下图是 Hippy App 端实现逻辑。



App 端的入口文件为 main-native.ts。在里面,声明了一个 App 实例,指定 phone 下的一些属性设置,比如状态栏、背景色等等。同时,需要用到的 native 组件,都需要在 main-native 中进行声明绑定,才可以在页面中使用。


例如:下图示例中注册声明了两个 native 组件,LottieView 和 VideoView,在页面中就可以直接使用这两个 native 组件。


Vue.registerElement('LottieView');
Vue.registerElement('VideoView', {
component: {
name: 'VideoView',
processEventData(event: any, nativeEventName: string, nativeEventParams: any) {
// To do something for the native component event
return event;
},
},});

main-native 中还有一个重要的方法:app.$start() 方法。


该方法为 Hippy 引擎初始化完成后回调到 Hippy 前端的方法;Hippy 端跟 App 方法进行通信,通过 jsbridge 来进行,模版中已经封装好了具体方法;Hippy 请求后端接口,通过 fetch 协议,也有具体的协议方法封装;Hippy 在 App 内部的跳转,是通过伪协议跳转来实现的。


Hippy App 应用的部署分为以下三种情况:



  • 本地调试


本地调试是通过 Hippy + Chrome Devtools 来完成,通过 WS 通道转发消息,具体流程如下图。




  • 部署测试环境


模版中引入了环境变量参数,同时在代码模版中做了大量环境变量的兼容逻辑,比如测试环境用测试环境的接口,正式环境用正式环境的接口;测试环境用测试环境的 CDN,静态文件上传到测试环境,测试环境部署测试环境的离线包等;测试环境的调试我们是通过离线包的方式来实现的,有专门的测试环境流水线接入使用,只需要稍微做少许调整即可,有需要的可以私聊。



  • 部署正式环境


正式环境流程会做这样几件事情:正式环境接口、正式环境的 CDN、正式环境的日志上、部署正式环境的离线包平台、图片的特殊处理。因为 App 端是采用离线包的形式,如果所有本地图片都打包到离线包中,会导致离线包包体积很大,会影响到 App 的整体体积大小和离线包的下载速度。模版中做了针对图片的特殊脚本处理:引入了图片编译大小变量:STATIC_SIZE_LIMIT。当大于该限制条件的图片都一律上传到 CDN,如果想保留的,则需要增加特殊声明:inline。


具体流程如下图所示:



Hippy H5 的实现流程如下图所示。



Hippy H5 的实现跟 App 的实现流程类似,但是差异如下:


App 的入口文件为 main-native.ts,h5 的入口文件为 main.ts 文件。H5 的入口文件中,没有关于 iphone 的设置,跟 Web 的设置一样;H5 的路由用 vue-router,页面中的路由跳转都是 H5 超链接,不是伪协议;H5 的本地调试很简单,跟 Vue Web 一样,都是在本地起 http-server 来测试;测试环境的部署和正式环境的部署都是采用的服务器来部署,不是离线包。

这里重点讨论一下大同上报的实现。大同上报在 App 端的上报参数声明跟 H5 端的上报参数声明不一致,如何统一这些差异?模版中的解决方案是:封装自定义标签 Directive。


具体实现如下:在 Directive 标签中兼容 App 和 H5 的不一致。


/**
* @example
*
* <element v-report="elementReportInfo" />
* <element v-report="{ eid, ...extra }" />
* <page v-report="{ pgid, ...extra }" />
* <page v-report.page="assertPageReport" />
*/

Vue.directive('report', {
bind(el) {
el.addEventListener('layout', throttledForceReport);
},
unbind(el) {
el.removeEventListener('layout', throttledForceReport);
},
inserted: setReport,
update: setReport,
} as DirectiveOptions);

很多人可能会问,Hippy App 跟 Hippy H5 有很多不同的地方,如果写两套代码,会不会导致代码的体积变得很大?答案是一定的,为了解决以上问题,该万能模版提供了条件编译。引入环境变量:isNative。然后,根据该条件,进行条件编译,不同的平台,生成不同平台的代码,避免了生成大量冗余代码。


Hippy 微信小程序的实现流程如下图所示:



小程序的实现是基于 Taro Vue 框架。该框架跟 Hippy Vue 框架天然兼容,但是也有一些小程序的特殊地方:


小程序的入口文件约定为 app.ts,创建 app 实例是在 app.ts 中来完成;小程序的主页面文件为 app.vue,在其中定义小程序的状态栏,标题栏,页面等;小程序的全局配置在 app.config.ts 中;小程序的构建脚本在 script 中的 index.js。小程序的代码是基于 Hippy Vue 的代码通过 Taro 自动构建转化而成,很多配置都是自动生成的,只需要在开发的时候,遵循约定的命名规范即可。

为了一套代码能够同时支持 App,H5 和微信小程序,需要遵循一些约定的规范,否则在从 Hippy Vue 转化为 Taro Vue 的时候会遇到一些问题:


文件夹命名规范:全部小写加“-”, 例如:node-redis, agent-base 等,不要用大驼峰等;文件的命名规范:跟文件夹命名规范一致,全部小写加“-”,例如:eslint-recomment.js;属性的命名:也采用小写加“-”, 例如:data-url。

04、总结


目前该模版已经在腾讯视频的搜索场景落地,并且上线应用,但是,还是有一些需要共同打磨的地方:


Vue3 的支持:目前我们是基于 Hippy Vue2 来实现的。随着 Vue3 的广泛应用,后续我们需要升级到 Vue3。


组件丰富:通用组件的种类还不是特别丰富,只是基于我们腾讯视频搜索场景进行的封装,后续可以补充更多更丰富的组件。


迭代升级:通用组件目前还是通过源代码的方式存放在代码模版中,不利于后续组件的升级迭代。计划后续会把组件给迁移到我们的应用组件库平台 Athena,该平台我们会后续发专文介绍,大家敬请期待。


以上是本次分享全部内容,如果觉得文章还不错的话欢迎分享~


-End-


原创作者|熊才刚


技术责编|陈恕胜



你有什么开发提效小技巧?欢迎在腾讯云开发者公众号评论区中分享你的经验和看法。我们将选取1则最有意义的分享,送出腾讯云开发者-文化衫1件(见下图)。6月21日中午12点开奖。



图片


图片
图片
图片


作者:腾讯云开发者
来源:juejin.cn/post/7246056370624495671
收起阅读 »

2023—疫情、毕业、两次离职、失恋、遇到新的自己

就业之前 ​ 应该大三、大二或者大四的你都会有这么一个焦虑:该怎么去选择,考公务员、考研、还是就业? ​ 我也曾经是这其中的一员,从大三上就开始陷入焦虑。 ​ 首先是排除了考公务员,因为家里的姐姐就是毕业2年仍然在考公,算是陷入其中,我自己也觉得应该不会...
继续阅读 »

就业之前


​ 应该大三、大二或者大四的你都会有这么一个焦虑:该怎么去选择,考公务员、考研、还是就业?


​ 我也曾经是这其中的一员,从大三上就开始陷入焦虑。


​ 首先是排除了考公务员,因为家里的姐姐就是毕业2年仍然在考公,算是陷入其中,我自己也觉得应该不会好到哪儿去。


​ 然后是考研,这是个纠结了很久的问题,甚至在2023年过年的时候,我仍然有想去考研的想法,但是多数是受到了旁边的人干扰。(当然能考到一个好的学校还是很好的),但是我自认是学渣,所以,最最多也就一个双非二本研究生,期间还得附上3-4年的时间,可能是读书太久了,所以最后选择了直接就业。


​ 高考完选了大数据专业,当初选择大数据以为是新兴专业,而且在贵州,快毕业了才发现,所谓的大数据只有大公司才有,小公司基本就前端+后端这样的模式,甚至都是全干工程师。我是从2022年前开始学习的前端,6月暑假找的实习。其实我很佩服自己那半年的时间,从js到vue和小程序,期间在小破站上学习的视频还是蛮多的。


第一段实习 — 贵阳



早9晚5.30,双休,2.5k



​ 2022年后,当时觉得自己身上有用不完的干劲,觉得毕业后非大厂莫属。当时学校一门课程就是做小程序+后台+后端,我和室友们做了一个关于项目管理的项目,期间也大概学会了git、接口对接、项目配合这些东西,后面项目也获得了学院的作品展示。现在看起来做的什么玩意儿啊,哈哈哈。在2月到4月那段时间就是学习+做项目,期间的收获很大。


​ 到了五月开始投简历,也是我最焦虑的一段时间,因为带来的反差真的很大,背了很多八股文,信心满满的却得不到一点回复。有几次线上面试也都凉了,其中有一次鹅厂的实习,被按在地上摩擦。运气比较好的是得到了一家线下的面试(我和室友都进了),后面在20多个人中也是我俩拿下了前端实习的2个名额,2.5k。然后开始了合租之旅...


​ 很清楚的记得租房的时候,被中介差点骗了300块,但是最后遇到一个很好的房东。去的第一天,根本睡不着,那个月也是疯狂爆痘。在公司的3个月其实学到了好些东西,因为之前没机会去接触这么大的项目,对于git和项目配合的理解更深了。而且在空余时间也有机会学习新的知识(ts、react等),合租的时候我的室友做饭我就洗碗(他做的饭真的好吃,就是口味重了点)就这样到10月,迎来了第一波疫情,很清楚的记得是在中秋节之前开始的,疫情的时候,每天想的是怎么买到菜,到快结束的时候,3个菜都是发的白萝卜,太残忍了。坚持到10月底疫情结束,由于疫情和公司接的政府的外包,贵阳的财政情况(懂的都懂,拖欠工资),所以我开始投简历,准备下一家,最后去了一家重庆的音乐公司(因为当时女朋友也是在重庆工作)。


第二段 — 重庆



995,3k



​ 11月初,说走就走,当时前一天得到offer,后两天我就去了重庆,实习3.5k。刚到公司,就2个人!!一个淘宝运营,一个财务,还有一个老板和总监出差去了。如果不是用的vue3+ts,估计我当时就会走。就这样就开始做起了(还有一个实习的后端)。好处是有一个技术顾问,给了整体的框架技术的搭建建议。最后选择用vue3+ts+quasar搭建的后台管理系统。运气不好的是,又一波疫情来了,在家里居家办公了近1个月。那是最阴暗的一个月了,每天在房间里都是一个人,没有人可以说话,心情好的时候写一写代码,不好的就打游戏。当时一个人也想了很多,也有了想去考研的想法。所以在疫情结束的时候,1月初,离职了,准备回家过年。


现在 - 贵阳



6.5k+300补贴+住宿
大小周,早8.30晚5.30



在这2023年过年2个月的时间里,自己想通了很多,其实自己没有那么特别,就接受了自己的平凡,当时抱着试试的态度也投了一些沿海的城市。最后得到了贵阳目前这家公司的面试。很搞笑的记得当时顺道去重庆,拖着行李箱去面试的。最后得到offer:6.5k+300补贴+住宿。


面试的时候挺简单的,没什么太难的点。入职后做的原生小程序开发,业务倒是挺麻烦,对于组件的封装和代码规范是我目前觉得最值得学习的,对于原生好处就是,可以了解更多底层一点的东西,不用组件。坏处就是:开发速度会有所降低,样式也可能没有组件的好看。



感受:工作氛围挺好,非外包的项目,也挺清闲,大概有1/2 +的时间没事做,挺适合养老。
还有就是:遇到不会的别一个人死磕,多问问。




坏处:自控不好的话容易摆烂,周末空闲时间比较少



关于感情


我们是从高中一直到现在,因为异地+她忘不了从前喜欢的,在入职后几天,我提的分手。


说实话挺难受的,但是也没有必要在继续了。但是在这段情感中也学到了很多,成长了很多,懂得了自爱。


分手之后,感觉回归到了自由,也舍得给自己花钱了(从前我是很拮据的那种)。


3月,买了人生中第一台相机:尼康D750,后来也用它拍了很多照片。其实也可以用手机拍,但是觉得相机的意义就是可以多出去走走,还有女生好像对这个很感兴趣(哈哈哈),学会很加分。


5月,认识新的朋友,去了大理、丽江。虽然像是去踩坑的,但是,也学到了一点人像拍照的技巧。感谢同行小姐姐宽宏大量(我拍的贼丑)还鼓励我。



顺便给你们避避坑:


1.景区租服饰拍照的:其实不怎么专业,精修的图还没她们自己批的好看。


2.旅游之前做好攻略!!!(我就是当天决定当天走的)


3.丽江的 茶马古道 x ,日照金山√,玉龙雪山需要预约


4.大理的洱海√,基本上玩的都是环洱海



附上几组图片:


DSC_3061.JPG


DSC_3106.JPG


最后


马上毕业了,很庆幸自己能找到这份工作。随着工作的清闲,感觉自己变得闲鱼了,还是得支棱起

作者:ibeen
来源:juejin.cn/post/7232175144219148349
来。
希望越来越好!

收起阅读 »

用js脚本下载某书的所有文章

web
前言 在某书上的写了好几年的文章,发现某书越来越烂了,全是广告,各种擦边标题党文章和小说等,已经不适合技术人员了。 想把某书上的文章全部下载下来整理一下,某书上是有一个下载所有文章功能的,用了以后发现下载功能现在有问题,无法下载个人账号里所有文章,不知道是不是...
继续阅读 »

前言


在某书上的写了好几年的文章,发现某书越来越烂了,全是广告,各种擦边标题党文章和小说等,已经不适合技术人员了。


想把某书上的文章全部下载下来整理一下,某书上是有一个下载所有文章功能的,用了以后发现下载功能现在有问题,无法下载个人账号里所有文章,不知道是不是下载功能根据日期什么判断了,还是bug了,试了好几次都这样,官方渠道只能放弃了。


手动一篇一篇粘贴的成本太高了,不仅有发布的文章,还有各种没有发布的笔记在里面,各种文章笔记加起来好几百篇呢,既然是工程师,就用工程师思维解决实际问题,能用脚本下载个人账号的下的所有文章吗?


思考.gif


思路梳理


由于是下载个人账号下的所有文章,包含发布的和未发布的,来看下个人账号的文章管理后台


文集模式.png


根据操作以及分析浏览器控制台 网络 请求得知,文章管理后台逻辑是这样的,默认查询所有文集(文章分类列表), 默认选中第一个文集,查询第一个文集下的所有文章,默认展示第一篇文章的内容,点击文集,获取当前文集下的所有文章,默认展示文集中的第一篇文章,点击文章获取当前文章数据,来分析一下相关的接口请求


获取所有文集


https://www.jianshu.com/author/notebooks 这个 Get 请求是获取所有 文集,用户信息是放在 cookie


分析请求模式.png


来看下返回结果


[
    {
        "id": 51802858,
        "name": "思考,工具,痛点",
        "seq": -4
    },
    {
        "id": 51783763,
        "name": "安全",
        "seq": -3
    },
    {
        "id": 51634011,
        "name": "数据结构",
        "seq": -2
    },
    ...
]

接口返回内容很简单,一个 json 数据,分别是:id、文集名称、排序字段。


获取文集中的所有文章


https://www.jianshu.com/author/notebooks/51802858/notes 这个 Get 请求是根据 文集id 获取所有文章,51802858"思考,工具,痛点" 文集的id, 返回数据如下


[
    {
        "id": 103888430, // 文章id
        "slug": "984db49de2c0",
        "shared": false,
        "notebook_id": 51802858, // 文集id
        "seq_in_nb": -4,
        "note_type": 2,
        "autosave_control": 0,
        "title": "2022-07-18", // 文章名称
        "content_updated_at": 1658111410,
        "last_compiled_at": 0,
        "paid": false,
        "in_book": false,
        "is_top": false,
        "reprintable": true,
        "schedule_publish_at": null
    },
    {
        "id": 98082442,
        "slug": "6595bc249952",
        "shared": false,
        "notebook_id": 51802858,
        "seq_in_nb": -3,
        "note_type": 2,
        "autosave_control": 3,
        "title": "架构图",
        "content_updated_at": 1644215292,
        "last_compiled_at": 0,
        "paid": false,
        "in_book": false,
        "is_top": false,
        "reprintable": true,
        "schedule_publish_at": null
    },
    ...
]

接口返回的 json 数据里包含 文章id文集名称,这是接下来需要的字段,其他字段暂时忽略。


获取文章内容


https://www.jianshu.com/author/notes/98082442/content 这个 Get 请求是根据 文章id 获取文章 Markdown 格式内容, 98082442《架构图》 文章的id, 接口返回为 Markdown 格式的字符串


{"content":"![微服务架构图 (3).jpg](https://upload-images.jianshu.io/upload_images/6264414-fa0a7893516725ff.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n"}

现在,我们了解清楚了文集,文章,以及文档内容的获取方式,接下来开始脚本实现。


代码实现


由于我是前端攻城狮,优先考虑使用 js 来实现下载,还有一个考虑因素是,接口请求里面的用户信息是通过读取 cookie 来实现的,js 脚本在浏览器的控制台执行发起请求时,会自动读取 cookie,很方便。


如果要下载个人账号下所有文章的话,根据梳理出来的思路编写代码就行


获取所有文集id


fetch("https://www.jianshu.com/author/notebooks")
  .then((res) => res.json())
  .then((data) => {
    // 输出所有文集
    console.log(data);
  })

使用fetch函数进行请求,得到返回结果,上面的代码直接在浏览器控制台执行即可,控制台输出效果如下


输出所有文集.png


根据文集数据获取所有文章


上一步得到了所有文集,使用 forEach 循环所有文集,再根据 文集id 获取对应文集下的所有文章,依然使用 fetch 进行请求


...
let wenjiArr = [];
wenjiArr = data; // 文集json数据
let articleLength = 0;
wenjiArr.forEach((item, index) => {
  // 根据文集获取文章
  fetch(`https://www.jianshu.com/author/notebooks/${item.id}/notes`)
    .then((res2) => res2.json())
    .then((data2) => {
      console.log("输出文集下的所有文章:", data2);
    });
});

根据文章id获取文章内容,并下载 Markdown 文件


有了文章 id, 根据 id 获取内容,得到的内容是一个对象,对象中的 content 属性是文章的 Markdown 字符串,使用 Blob 对象和 a 标签,通过 click() 事件实现下载。


在这里的代码中使用 articleLength 变量记录了一下文章数量,使用循环中的文集名称和文章名称拼成 Markdown 文件名 item.name - 《item2.title》.md


...
console.log(item.name + " 文集中的文章数量: " + data2.length);
articleLength = articleLength + data2.length;
console.log("articleLength: ", articleLength);
data2.forEach(async (item2, i) => {
// 根据文章id获取Markdown内容
fetch(`https://www.jianshu.com/author/notes/${item2.id}/content`)
.then((res3) => res3.json())
.then((data3) => {
console.log(data3);
const blob = new Blob([data.content], {
type: "text/markdown",
});
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = item.name + " - 《" + item2.title + `》.md`;
link.click();
});
});

代码基本完成,运行


在浏览器控制台中运行写好的代码,浏览器下方的下载提示嗖嗖的显示,由于没有做任何处理,当前脚本执行过程中报错了,文章下载了几十个以后就停止了,提示 429



HTTP 请求码 429 表示客户端发送了太多的请求,服务器无法处理。这种错误通常表示服务器被攻击或过载。



文章内容太多了,意料之中的情况,需要改进代码


思路改进分析


根据问题分析,脚本里的代码是循环套循环发请求的,这部分改造一下试试效果。


把每个循环里面发送 fetch 请求的外面是加个 setTimeout, 第一个循环里面的 setTimeout 延迟参数设置为 1000 * indexindex 为当前循环的索引,第一个请求0秒后执行,后面每一次都加1秒后执行,由于文集的数量不多,大约20个,这一步这样实现是没问题的。


重点是第二个循环,根据文集获取所有文章,每个文集里多的文章超过50篇,少的可能2,3篇,这里面的 setTimeout 延迟参数这样设置 2000 * (i + index)i 为第二个循环的索引,这样保证在后面的请求中避免了某个时间段发送大量请求,导致丢包的问题。


再次执行代码,对比控制台输出的文章数量和下载目录中的文章(项目)数量,如果一致,说明文章都下载好了


390.png


下载.png


改造后的完整代码地址


github.com/gywgithub/F…


思考


整体来看,文章下载脚本的逻辑并不复杂,接口参数也简单明确,两个 forEach 循环,三个 fetch 请求,把获取到的文章内容实用 a 标签下载下来就行了。关于大量请求发送导致 429 或者请求丢失的问题,脚本中使用了一种方案,当时还想到了另外两种方案:


请求同步执行


通过同步的方式先得到所有文集下的所有文章,再根据文章列表数组同步发请求下载,请求一个个发,文章一篇篇下载


Promise.all


使用 Promise.all() 分批发送请求,避免一次请求发送太多



也可能还有其他的解决方案,欢迎大家评论区讨论交流,一起学习共同进步 ^-^



作者:草帽lufei
来源:juejin.cn/post/7245184987531018300

收起阅读 »

这道面试题真的很变态吗?😱

web
最近帮公司招聘,主要负责一面,所以基本上问的基础多一点。但是我在问这样一道面试题的时候,很少有人答对。不少人觉得我问这道题多少有点过分了😭,当然了面试还是奔着相互沟通相互学习的目的,并不是说这道题不会就被刷掉,单纯的觉得这道题有意思。话不多说,我们直接上题 题...
继续阅读 »

最近帮公司招聘,主要负责一面,所以基本上问的基础多一点。但是我在问这样一道面试题的时候,很少有人答对。不少人觉得我问这道题多少有点过分了😭,当然了面试还是奔着相互沟通相互学习的目的,并不是说这道题不会就被刷掉,单纯的觉得这道题有意思。话不多说,我们直接上题


题目


var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()


function foo() {
console.log("foo1")
}
foo()

function foo() {
console.log("foo2")
}
foo()

这里我会要求面试者从上到下依次说出执行结果。普遍多的面试者给出的答案是:foo1、foo2、foo1、foo2。虽然在我看来这是一道简单的面试题,但是也不至于这么简单吧😱~~~


当然面试本来就是一个相互讨论的过程,那就和面试者沟通下这道题我的理解,万一我理解错了呢😂


解答


拆分函数表达式


首先我会让面试者先看前面两个函数


var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()

这时候大部分人基本上都可以答对了,是:foo1、foo2。再有很少数的人答不对那就只能”施主,出门右转“😌。接着根据我当时的心情可能会稍作追问(美女除外🙂):


foo()
var foo = function () {
console.log("foo1")
}

这时候又有一部分的人答不上来了。这毫无疑问是肯定会报错的啊


image.png


我们都知道用var定义的变量会变量提升,所以声明会被拿到函数或全局作用域的顶部,并且输出undefined。所以当执行foo()的时候,foo还是undefined,所以会报错。由于js从按照顺序从上往下执行,所以当执行foo = function(){}的时候,才对foo进行赋值为一个函数。我们重新看拆分之后的代码


var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()

foo首先会变量提升,然后进行赋值为function。所以当执行第一个foo的时候,此时foo就是我们赋值的这个函数。接着执行第二个foo的赋值操作,由于函数作用域的特性,后面定义的函数将覆盖前面定义的函数。
由于在调用函数之前就进行了函数的重新定义,所以在调用函数时,实际执行的是最后定义的那个函数。所以上面的代码会打印:foo1、foo2。


这种定义函数的方式,我们称为函数表达式。函数表达式是将函数作为一个值赋给一个变量或属性


函数表达式我们拆分完了,下面就看看函数声明吧。


拆分函数声明


function foo() {
console.log("foo1")
}
foo()

function foo() {
console.log("foo2")
}
foo()

大部分人其实都卡在了这里。函数声明会在任何代码执行之前先被读取并添加到执行上下文,也就是函数声明提升。说到这里其实大多数人就已经明白了。这里使用了函数声明定义了两个foo函数,由于函数声明提升,第二个foo会覆盖第一个foo,所以当调用第一个foo的时候,其实已经被第二个foo覆盖了,所以这两个打印的都是foo2。


当两段代码结合


当开始解析的时候,函数声明就已经提升了,第四个foo会覆盖第三个foo。然后js开始从上往下执行,第一个赋值操作之后执行foo()后,打印了”foo1“,第二个赋值之后执行foo(),打印了"foo2"。下面两个foo的执行其实是第二个赋值了的foo,因为函数声明开始从刚开始就被提升了,而下面的赋值会覆盖foo。


总结


我们整体分析代码的执行过程



  1. 通过函数表达式定义变量foo并赋值为一个匿名函数,该函数在被调用时打印"foo1"。

  2. 接着,通过函数表达式重新定义变量foo,赋值为另一个匿名函数,该函数在被调用时打印"foo2"。

  3. 使用函数声明定义了两个名为foo的函数。函数声明会在作用域中进行提升。后面的会覆盖前面的,由于声明从一开始就提升了,而又执行了两个赋值操作,所以此时foo是第二个赋值的函数。

  4. 然后调用foo(),输出"foo2"。

  5. 再调用foo(),也输出"foo2"。


其实就一个点: 函数表达式相对于函数声明的一个重要区别是函数声明在代码解析阶段就会被提升(函数声明提升),而函数表达式则需要在赋值语句执行到达时才会创建函数对象


小伙伴们,以上是我的理解,欢迎在评论区留言,大家相互讨论相互学习。


之前的描述确实有点不妥,所以做了改动,望大家谅解,还

作者:翰玥
来源:juejin.cn/post/7237051958993469496
是本着相互学习的态度

收起阅读 »

别再无聊地显示隐藏了,Vue 中使用过渡动画让你的网页更有活力

web
Vue 是一款流行的前端框架,支持过渡动画的实现是其中的一项重要特性。在 Vue 中,使用过渡动画可以为用户提供更加友好的用户体验。下面我将为大家介绍一下子如何在 Vue 中实现过渡动画。 1. 你知道什么是过渡动画吗 过渡动画是指在 DOM 元素从一个状态到...
继续阅读 »

Vue 是一款流行的前端框架,支持过渡动画的实现是其中的一项重要特性。在 Vue 中,使用过渡动画可以为用户提供更加友好的用户体验。下面我将为大家介绍一下子如何在 Vue 中实现过渡动画。


1. 你知道什么是过渡动画吗


过渡动画是指在 DOM 元素从一个状态到另一个状态发生变化时,通过添加过渡效果使得这个变化看起来更加平滑自然的动画效果。在 Vue 中,过渡动画可以应用到以下几个场景中:



  • 显示和隐藏元素

  • 动态添加或删除元素

  • 元素位置的变化


2. Vue 过渡动画的实现方法


2.1 CSS 过渡


Vue 提供了 transition 组件来支持过渡动画。我们可以在需要应用过渡动画的元素外层包裹一个 transition 组件,并通过设置 CSS 样式或绑定动态 class 来实现过渡动画的效果。


Vue 的过渡动画通过添加 CSS 类名来实现。我们可以通过为需要过渡的元素添加 v-ifv-show 指令来控制元素的显示和隐藏,然后使用 transition 组件进行动画效果的设置。


下面我写个示例给大家参考一下,我将给按钮添加过渡动画效果:


<template>
<button @click="show=!show">Toggle</button>
<transition name="fade">
<div v-if="show">Hello, World!</div>
</transition>
</template>
<script>
export default {
data() {
return {
show: false
};
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity .5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

在上面的代码思路中,我们在 transition 包裹的 div 元素上使用了 v-if 指令来控制元素的显示和隐藏。同时,我们给 transition 组件添加了一个 name 属性,并使用 CSS 样式来定义过渡动画的效果。其中,.fade-enter-active.fade-leave-active 分别表示进入和离开时的过渡动画,而 .fade-enter.fade-leave-to 则分别表示进入和离开时元素的样式。


2.2 JS 过渡


除了使用 CSS 过渡外,在 Vue 中也可以使用 JavaScript 过渡来实现动画效果。JS 过渡相比于 CSS 过渡的优势在于它可以更加灵活地控制过渡动画。


它与 CSS 过渡不同,Javascript 过渡可以更加灵活地控制过渡动画,可以实现更加丰富的效果。Vue 提供了事件钩子函数,使得我们可以自定义过渡动画的效果。


image.png


Vue 中提供了以下事件钩子函数:



  • before-enter

  • enter

  • after-enter

  • enter-cancelled

  • before-leave

  • leave

  • after-leave

  • leave-cancelled


我们可以使用 transition 组件的 mode 属性来设置过渡的模式,如果使用了 mode 属性,Vue 将会自动调用对应的钩子函数,我们可以通过这些钩子函数来自定义过渡效果。


下面是我写的一个基于 JS 过渡的演示Demo,我们将为按钮添加自定义的过渡动画:


<template>
<button @click="show=!show">Toggle</button>
<transition :css="false" @before-enter="beforeEnter" @enter="enter" @leave="leave">
<div v-if="show">Hello, World!</div>
</transition>
</template>
<script>
export default {
data() {
return {
show: false
};
},
methods: {
beforeEnter(el) {
el.style.opacity = 0;
el.style.transformOrigin = 'left';
},
enter(el, done) {
anime({
targets: el,
opacity: 1,
translateX: [20, 0],
easing: 'easeInOutQuad',
duration: 500,
complete: done
});
},
leave(el, done) {
anime({
targets: el,
opacity: 0,
translateX: [-20, 0],
easing: 'easeInOutQuad',
duration: 500,
complete: done
});
}
}
};
</script>

在上面的前端页面中,我们通过设置 transition 组件的 css 属性为 false 来禁用 CSS 过渡,然后我们使用了 before-enterenterleave 等钩子函数来自定义过渡动画。在这个示例代码中,我们使用了第三方动画库 Anime.js 来实现元素进入和离开时的动画效果,同时在 anime 动画完成后,我们还需要手动调用 done 函数来告知 Vue 过渡动画已经完成。


3. 小结一下


通过我写的这篇文章的介绍,可以让大家多了解了 Vue 过渡动画的基本概念,并且掌握了如何在 Vue 中实现过渡动画。不论是使用 CSS 过渡还是 JavaScript 过渡,都可以帮助我们为用户提供更加友好的用户体验。我希望本文对您有所帮助,如果您有任何疑问或建议,欢迎在评论区留言。


作者:Cosolar
来源:juejin.cn/post/7241874482574114875
收起阅读 »

for循环的代价

web
for循环的可控性比forEach好,但是它的代价却鲜为人知,这篇文章就是简单的探讨下这背后的代价。 先定一个基调,我们从作用域的角度去分析,也会按需讲一些作用域的知识。 作用域是什么? 要解释这个问题,先从程序的角度来看,几乎所有编程语言上来就会介绍自己的变...
继续阅读 »

for循环的可控性比forEach好,但是它的代价却鲜为人知,这篇文章就是简单的探讨下这背后的代价。 先定一个基调,我们从作用域的角度去分析,也会按需讲一些作用域的知识。


作用域是什么?


要解释这个问题,先从程序的角度来看,几乎所有编程语言上来就会介绍自己的变量类型,因为如果没有变量程序只能执行一些简单的任务。但是引入变量之后程序怎么才能准确的找到自己需要的变量。这就需要建立一套规则让程序能够准确的找到需要的变量,这样的规则被称为作用域。


块级作用域


块级作用域如同全局作用域和函数作用域一样,只不过块级作用域由花括号({})包裹的代码块创建的。在块级作用域内声明的变量只能在该作用域内访问,可以使用 let 或 const 关键字声明变量,可以在块级作用域内创建变量。
所以引擎在编译时是通过花括号({})包裹和声明关键字判断是否创建块级作用域,因此绝大多数的语句是没有作用域的,同时从语言设计的角度来说越少作用域的执行环境调度效率也就越高,执行时的性能也就越好。
基于这个原则,switch语句被设计为有且仅有一个作用域,无论它有多少个case语句,其实都是运行在一个块级作用域环境中的。
一些简单的、显而易见的块级作用域包括:


// 例1
try {
// 作用域1
}
catch (e) { // 表达式e位于作用域2
// 作用域2
}
finally {
// 作用域3
}

// 例2
//(注:没有使用大括号)
with (x) /* 作用域1 */; // <- 这里存在一个块级作用域

// 例3, 块语句
{
// 作用域1

除此之外,按上述理解,for语句也可以满足上述的条件。


for循环作用域


并不是所有的for循环都有自己的作用域,有且仅有


for ( <let/const> ...) ...

这个语法有自己的块级作用域。当然,这也包括相同设计的for await和for .. of/in ..。例如:


for await ( <let/const> x of ...) ...
for ( <let/const> x ... in ...)
for ( <let/const> x ... of ...) ...

已经注意到了,这里并没有按照惯例那样列出“var”关键字。简单理解就是不满足创建的条件。Js引擎在编译时,会对标识符进行登记,而为了兼容,将标识符分为了两类varNames 和 lexicalNames。以前 var 声明、函数声明将会登记在varNames,为了兼容varNames只有全局作用域和函数作用域两种,所以编译时会就近登记在全局作用域和函数作用域中且变量有“提升”效果。Es6新增的声明关键词将登记在lexicalNames,编译时会就近创建块级作用或就近登记在函数作用域中。



varNames 和 lexicalNames属性只是一个用于记录标识符的列表,是通过词法作用域分析,在当前作用域中做登记的。它们记录了当前作用域中的变量和函数的名称,以及它们的作用域信息,帮助 JavaScript 引擎在代码执行时正确地解析标识符的作用域。



关于作用域还有一点要说明,JavaScript采用词法作用域,这意味着变量的作用域在代码编写时就确定了,而不是在运行时确定。这与动态作用域不同,动态作用域是根据函数的调用栈来确定变量的作用域。
举个例子:


function foo() {
console.log( a ); // 2
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();

词法作用域下log的结果是2,动态作用域下log的是3。



词法作用域是指 JavaScript 引擎在编译时如何确定变量的作用域。在 JavaScript 中,词法作用域是指变量的作用域是由代码的位置(词法)决定的,而不是由运行时的作用域链决定的。



变量的作用域和可见性是由词法作用域和作用域链来决定的,作用域链是基于词法作用域和函数作用域来确定的,这也证明了 JavaScript 采用的是词法作用域。


for循环隐藏作用域


首先,必须要拥有至少一个块级作用域。如之前讲到的,满足引擎创建的条件。但是这一个作用域貌似无法解释下面这段代码


for(let i=0;i<10;i++){
let i=1;
console.log(i) // 1
}

这段代码时可以正常运行的,而我们知道let语句的变量不能重复声明的,所以对for循环来说一个作用域是满足了这个场景的。
但是这段代码依然可以执行,那JS引擎是如何处理的呢?
只能说明循环体又创建了一个块级作用域,事实如你所见,JS引擎确实对for循环的每个循环体都创建了一个块级作用域。
举个栗子,以下代码中使用 let 声明变量 i


for (let i = 0; i < 5; i++) {
console.log(i);
}

在编译时,JavaScript 引擎会将循环体包裹在一个块级作用域中,类似于以下代码:


{
let i;
for (i = 0; i < 5; i++) {
console.log(i);
}
}

每次循环都会创建一个新的块级作用域,因此,在循环中声明的变量 i 只能在当前块级作用域中访问,不会污染外部作用域的变量。而通过作用域链每个循环体内都可以访问外层变量i。
而我们知道从语言设计的角度来说越少作用域的执行环境调度效率也就越高,执行时的性能也就越好,所以这也算是代价之一吧。
就算如此设计还是无法解释下面这段代码。


for (let i = 0; i < 5; i++) {
setTimeout(()=>{console.log(i)})
}

如果按上述的理解,那最后log时访问的都是外层的变量i,最后的结果应该都是4,可事实却并非如此。当定时器被触发时,函数会通过它的闭包来回溯,并试图再次找到那个标识符i。然而,当定时器触发时,整个for迭代都已经结束了。这种情况下,访问i,获取到的也是上层作用域中的i,此刻i的值应该是最后一次赋。
之所能按我们预想的输出1,2,3,4,那是因为JavaScript 引擎在创建循环体作用域的时候,会在该作用域中声明一个新的变量 i,并将其初始化为当前的迭代次数,这个新的变量 i 会覆盖外层的变量 i。这个过程是由 JavaScript 引擎自动完成的,我们并不需要手动

作者:chtty
来源:juejin.cn/post/7245641209913360445
创建或赋值这个变量。

收起阅读 »

前端如何破解 CRUD 的循环

web
据说,西西弗斯是一个非常聪明的国王,但他也非常自负和狂妄。他甚至敢欺骗神灵,并把死者带回人间。为此,他被宙斯(Zeus)惩罚,被迫每天推着一块巨石上山,但在接近山顶时,巨石总是会滚落下来,他不得不重新开始推石头,永远困在这个循环中… 很多开发工作也如此单调而乏...
继续阅读 »

据说,西西弗斯是一个非常聪明的国王,但他也非常自负和狂妄。他甚至敢欺骗神灵,并把死者带回人间。为此,他被宙斯(Zeus)惩罚,被迫每天推着一块巨石上山,但在接近山顶时,巨石总是会滚落下来,他不得不重新开始推石头,永远困在这个循环中…


很多开发工作也如此单调而乏味,比如今天要讲的中后台开发的场景。中后台业务基本上就是一些数据的增删改查、图表,技术含量不高,比较容易范式化。


前端如何破除这种 CRUD 的单调循环呢?








低代码


过去几年前端的低代码很火,这些低代码平台通常支持创建数据模型后,一键生成对应的增删改查页面:




模型驱动生成页面





💡 本文提及的低代码是狭义低代码,你可以认为就是可视化搭建平台





低代码在过去几年就是 「雷声大,雨点小」,跟现在的 AI 颇为相似。


不管是大厂还是小厂都在搞低代码,包括笔者也参与过几个低代码项目,但是小厂支撑不起来这样的资源投入,最后都胎死腹中。我相信很多读者也经历过这种情况。
大部分公司只是尾随市场营销噱头,盲目跟风,压根就没有做这种低代码平台资源准备和沉淀。


作为前端,能参与到低代码项目的开发是一件非常兴奋的事情,毕竟是少数前端能主导的项目,架构、组件设计、编辑器的实现可玩性很高,可以跟同行吹很久。


作为用户(开发者)呢?可能会排斥和质疑,不管怎么说,它并没有发挥市场所期望的价值。




最主要的原因是:它解决不了复杂的问题




低代码直观、门槛低, 前期开发确实很爽,可视化数据建模、拖拉拽生成页面、流程编排,很快就可以把一些简单的业务开发出来。


然而软件编码本身占用研发流程的比例,据 ChatGPT 估算大约只有 20% ~ 30%。而且业务持续变化,代码也需要持续迭代。试想一下如何在这些低代码平台上进行重构和检索?






总的来说,有一些缺点:




  • 复杂的业务逻辑用低代码可能会更加复杂。低代码应该是特定领域问题的简化和抽象,如果只是单纯将原有的编码工作转换为 GUI 的模式,并没有多大意义。


    例如流程编排,若要用它从零搭建一个复杂的流程,如果照搬技术语言去表达它,那有可能是个地狱:


    流程编排


    理想的流程编排的节点应该是抽象程度更高的、内聚的业务节点,来表达业务流程的流转。然而这些节点的设计和开发其实是一件非常有挑战性的事情。




  • 软件工程是持续演进的,在可维护性方面,目前市面上的低代码平台并不能提供可靠的辅助和验证。因此企业很难将核心的稳态业务交给这些平台。




  • 还有很多… 平台锁定,缺乏标准,性能问题、复用、扩展性、安全问题、黑盒,可迁移性,研发成本高,可预测性/可调试性差,高可用,版本管理,不能自动化…








当然,低代码有低代码的适用场景,比如解决特定领域问题(营销活动页面,海报,数据大屏,表单引擎、商城装修、主页),POC 验证。即一些临时的/非核心的敏态业务



💡 目前有些低代码平台也有「出码能力」,让二开有了一定的可行性。




💡 AI 增强后的低代码可能会更加强大。但笔者依旧保持观望的态度,毕竟准确地描述软件需求,本身就是就是软件研发的难题之一,不然我们也不需要 DDD中的各种方法论,开各种拉通会,或许也不需要需求分析师,产品…


非专业用户直接描述需求来产出软件,大多是不切实际的臆想









中间形态


有没有介于可视化低代码平台和专业代码之间的中间形态?既能保持像低代码平台易用性,同时维持代码的灵活性和可维护性。


我想那就是 DSL(domain-specific language) 吧? DSL 背后体现的是对特定领域问题的抽象,其形式和语法倒是次要的。



💡 DSL 的形式有很多,可以创建一门新的微语言(比如 SQL, GraphQL);可以是一个 JSON 或者 YAML 形式;也可以基于一门现有的元语言(比如 Ruby、Groovy,Rust…)来创建,这些元语言,提供的元编程能力,可以简洁优雅地表达领域问题,同时能够复用元语言 本身的语言能力和基础设施。



严格上可视化低代码平台也是一种‘可视化’ 的 DSL,笔者认为它的局限性更多还是来源‘可视化’,相对的,它优点也大多来源’可视化‘



这又牵扯到了持续了半个多世纪的: GUI vs CLI(程序化/文本化) 之争。这个在《UNIX 编程艺术》中有深入的探讨。命令行和命令语言比起可视化接口来说,更具表达力,尤其是针对复杂的任务。另外命令行接口具有高度脚本化的能力。缺点就是需要费劲地记忆,易用性差,透明度低。当问题规模变大、程序的行为日趋单一、过程化和重复时, CLI 也常能发挥作用。

如果按照友好度和问题域的复杂度/规模两个维度来划分,可以拉出以下曲线:

友好曲线


中间会出现一个交叉点,在这个交叉点之后,命令行的简要行和表达力变得要比避免记忆负担更有价值。


《反 Mac 接口》一书中也进行了总结:可视化接口在处理小数量物体简单行为的情况下,工作的很好,但是当行为或物体的数量增加是,直接操作很快就编程机械重复的苦差…



也就是说,DSL 的形式会约束 DSL 本身的表达能力。




正如前文说的,如果‘低代码’仅仅是将原本的编码工作转换为 GUI 形式,其实并没有多大意义,因为没有抽象。


反例:JSON GUI vs JSON


JSON GUI vs JSON






正例: VSCode 案例


setting in json


setting in gui


充分利用 GUI 的优势,提供更好的目录组织、文本提示、数据录入的约束和校验。






我们可能会说 GUI 形式用户体验更好,门槛低更低,不用关心底层的细节。其实并不一定是 GUI 带来的,而是抽象后的结果。GUI 只不过是一种接口形式




回到正题,为了摆脱管理后台 CRUD 的 「西西弗斯之石」: 我们可以创建一个 DSL,这个 DSL 抽象了管理端的各种场景,将繁琐的实现细节、重复的工作封装起来,暴露简洁而优雅的用户接口(User Interface)。



💡 小结。DSL 是可视化低代码与 pro code 之间的中间中间形态,权衡了易用性/灵活性和实现成本。DSL 的形式会直接影响它的表达能力,但比形式更重要的是 DSL 对特定问题域的抽象。


我们不必重新发明一门语言,而是复用元语言的能力和生态,这基本上是零成本。











抽象过程


典型的增删改查页面:


CRUD


分析过程:



  1. 后端增删改查主要由两大组件组成: 表单表格

  2. 而表单和表格又由更原子的’字段’组成。字段的类型决定了存储类型、录入方式、和展示方式

  3. 字段有两种形态:编辑态预览态。表格列、详情页通常是预览态,而表单和表格筛选则使用编辑态。




预览态和编辑态


借鉴低代码平台的组件库/节点库,我们可以将这些‘字段’ 提取出来, 作为表单和表格的‘原子’单位, 这里我们给它取个名字,就叫原件(Atomic)吧。


低代码平台


原件将取代组件库里面的表单组件,作为我们 CRUD 页面的最小组成单位。它有且只有职责:


原件



  • 数据类型和校验。原件代表的是一种数据类型,可以是基础类型,比如数字、字符串、布尔值、枚举;也可以是基础类型上加了一些约束和交互,比如邮件、手机号码、链接;甚至可能有业务属性,比如用户,商品,订单,二维码。

  • 数据的预览。

  • 数据的录入,严格约束为 value/onChange 协议。好处是方便进行状态管理,可能保证原件实现的统一性。






接着组合原件来实现表单和表格组件,满足 CRUD 场景:


CRUD


理想状态下,我们仅需声明式地指定表格的列和原件类型,其余的技术细节应该隐藏起来。表格伪代码示例:


# 创建包含 名称、创建时间、状态三列的表格,其中可以搜索名称和创建时间
Table(
columns(
column(名称,name, queryable=true)
column(创建时间, created, data-range, queryable=true)
column(状态, status, select, options=[{label: 启用,value: 1, {label: 禁用, value: 0}}])
)
)



表单伪代码示例:


# 创建包含 名称、状态、地址的表单
Form(
item(名称,name, required=true)
item(状态,status, select, options=[{label: 启用,value: 1, {label: 禁用, value: 0}}])
item(地址, address, address)
)



如上所示,本质上,开发者就应该只关注业务数据本身,而应该忽略掉前端技术实现的噪音(比如状态管理、展示风格、分页、异常处理等等)。






表格和表单为了适应不同的需求,还会衍生出不同的展现形式:


概览图


原件 + 核心的表单/表格能力 + 场景/展示形式,一套「组合拳」下来,基本就可以满足常见的后台 CRUD 需求了。








约定大于配置


前端的在研发流程中相对下游,如果上游的产品定义,UI 设计,后端协议没有保持一致性,就会苦于应付各种混乱的差异,复用性将无从谈起。


为了最小化样板代码和沟通成本,实现开箱即用的效果。我们最好拉通上下游,将相关的规范确定下来,前端开发者应该扮演好串联的角色。




这些规范包含但不限于:



  • 页面的布局

  • UI 风格

  • 提示语

  • 验证规则

  • 数据的存储格式

  • 通用的接口(比如文件上传,导入导出)



概览图


组件库可以内置这些约定,或者提供全局的配置方式。这些规范固化后,我们就享受开箱即用的快感了。








实现示例


基于上述思想,我们开发了一套组件库(基于 Vue 和 element-ui),配合一套简洁的 DSL,来快速开发 CRUD 页面。





💡 这套组件库耦合了我们自己的约定。因此可能不适用于外部通用的场景。本文的意义更多是想启发读者,去构建适合自己的一套解决方案。



列表页定义:


表格示例


import { defineFatTable } from '@wakeadmin/components'

/**
* 表格项类型
*/

export interface Item {
id: number
name: string
createDate: number
}

export const MyTable = defineFatTable<Item>(({ column }) => {
// 可以在这里放置 Vue hooks
return () => ({
async request(params) {
/* 数据获取,自动处理异常和加载状态 */
},
// 删除操作
async remove(list, ids) {
/*列删除*/
},
// 表格列
columns: [
// queryable 标记为查询字段
column({ prop: 'name', label: '名称', queryable: true }),
column({ prop: 'createDate', valueType: 'date-range', label: '创建时间', queryable: true }),
column({
type: 'actions',
label: '操作',
actions: [{ name: '编辑' }, { name: '删除', onClick: (table, row) => table.remove(row) }],
}),
],
})
})

语法类似于 Vue defineComponent,传入一个’setup’, 。这个 setup 中可以放置一些逻辑和状态或者 Vue hooks,就和 Vue defineComponent 定义一样灵活。


返回关于表格结构的”声明”。最优的情况下,开发者只需要定义表格结构和后端接口,其余的交由组件库处理。


当然复杂的定制场景也能满足,这里可以使用 JSX,监听事件,传递组件支持的任意 props 和 slots。






表单页示例:


表单示例


import { defineFatForm } from '@wakeadmin/components'
import { ElMessageBox } from 'element-plus'

export default defineFatForm<{
// 🔴 这里的泛型变量可以定义表单数据结构
name: string
nickName: string
}>(({ item, form, consumer, group }) => {
// 🔴 这里可以放置 Vue Hooks

// 返回表单定义
return () => ({
// FatForm props 定义
initialValue: {
name: 'ivan',
nickName: '狗蛋',
},

submit: async (values) => {
await ElMessageBox.confirm('确认保存')
console.log('保存成功', values)
},

// 🔴 子节点
children: [
item({ prop: 'name', label: '账号名' }),
item({
prop: 'nickName',
label: '昵称',
}),
],
})
})


💡 和 tailwind 配合食用更香。我们假设整体的页面是符合UI规范的,细微的调整使用 tw 会很方便







全局配置:


import { provideFatConfigurable } from '@wakeadmin/components'
import { Message } from 'element-ui'

export function injectFatConfigurations() {
provideFatConfigurable({
// ...
// 统一处理 images 原件上传
aImagesProps: {
action: '/upload',
},
// 统一 date-range 原件属性
aDateRangeProps: {
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'yyyy-MM-dd',
shortcuts: [
{
text: '最近一周',
onClick(picker: any) {
picker.$emit('pick', getTime(7))
},
},
{
text: '最近一个月',
onClick(picker: any) {
picker.$emit('pick', getTime(30))
},
},
{
text: '最近三个月',
onClick(picker: any) {
picker.$emit('pick', getTime(90))
},
},
],
},
})
}





更多示例和深入讲解见这里








更多实现


前端社区有很多类似的产品,比如:



  • XRender。中后台「表单/表格/图表」开箱即用解决方案

  • Antd ProComponents。ProComponents 是基于 Ant Design 而开发的模板组件,提供了更高级别的抽象支持,开箱即用。可以显著的提升制作 CRUD 页面的效率,更加专注于页面

  • 百度 Amis 。 用 JSON 作为 DSL,来描述界面


读者不妨多参考参考。








总结


简单来说,我们就是从提供「毛坯房」升级到了「精装房」,精装房的设计基于我们对市场需求的充分调研和预判。目的是对于 80% 的用户场景,可以实现拎包入住,当然也允许用户在约束的范围内改装。


本文主要阐述的观点:



  • 低代码平台的高效和易用大多来源于抽象,而不一定是 GUI,GUI ≠ 低代码。

  • 摆脱「西西弗斯之石」 考验的是开发者的抽象能力,识别代码中固化/重复的逻辑。将模式提取出来,同时封装掉底层的实现细节。最终的目的是让开发者将注意力关注到业务本身,而不是技术实现细节。

  • 用声明式、精简、高度抽象 DSL 描述业务 。DSL 的形式会约束他的表达能力,我们并不一定要创建一门新的语言,最简单的是复用元语言的生态和能力。

  • 约定大于配置。设计风格、交互流程、数据存储等保持一致性,才能保证抽象收益的最大化。因此规范很重要。这需要我们和设计、产品、后端深入沟通,达成一致。

  • 沉淀原件。低代码平台的效率取决于平台提供的组件能力、数量和粒度。比如前端的组件库,亦或者流程引擎的节点,都属于原件的范畴。

  • 要求不要太高,没有万精油方案,我们期望能满足 80% 常见的场景,这已经是一个很好的成绩。至于那 20% 的个性需求,还是从毛坯房搞起吧。








扩展阅读


收起阅读 »

你的代码着色好看吗?来这里看看吧!

web
如果你想在网页上展示一些代码,你可能会遇到一个问题:代码看起来很单调,没有任何颜色或格式,这样的代码不仅不美观,也不利于阅读和理解。 那么,有没有什么办法可以让代码变得更漂亮呢?答案是有的,而且很简单。 你只需要使用一个叫做 highlight.js 的第三方...
继续阅读 »

如果你想在网页上展示一些代码,你可能会遇到一个问题:代码看起来很单调,没有任何颜色或格式,这样的代码不仅不美观,也不利于阅读和理解。


那么,有没有什么办法可以让代码变得更漂亮呢?答案是有的,而且很简单。


你只需要使用一个叫做 highlight.js 的第三方库,就可以轻松实现代码着色的效果。



highlight.js 是一个非常强大和流行的库,它可以自动识别和着色超过 190 种编程语言。


它支持多种主题和样式,让你可以根据自己的喜好选择合适的配色方案。


在本文中,子辰将向你介绍如何使用 highlight.js 来为你的代码着色,以及它的基本原理和优势。


让我们开始吧!


如何使用 highlight.js


使用 highlight.js 的方法有两种:一种是通过 npm 下载并安装到你的项目中,另一种是通过 CDN 引入到你的网页中。


这里我们以 CDN 的方式为例,如果你想使用 npm 的方式,可以参考官方文档。


首先,我们需要在网页中引入 highlight.js 的 JS 文件和 CSS 文件。


JS 文件是核心文件,负责识别和着色代码,CSS 文件是样式文件,负责定义代码的颜色和格式。



我们可以从 CDN 中选择一个合适的 JS 文件和 CSS 文件。


highlight.js 提供了多个 CDN 服务商,你可以根据自己的需求选择一个,这里我们以 jsDelivr 为例。


JS 文件的链接如下:


<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/highlight.min.js"></script>

CSS 文件的链接则需要根据你想要的主题来选择。


highlight.js 提供了很多主题,你可以在官网上预览每个主题的效果,并找到对应的 CSS 文件名,这里我们以 github-dark 为例。


CSS 文件的链接如下:


<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/github-dark.min.css">

将上面两个链接分别放到网页的 head 标签中,就完成了引入 highlight.js 的步骤。


接下来,我们需要在网页中写一些代码,并用 pre 标签和 code 标签包裹起来。


pre 标签用于保留代码的格式,code 标签用于标识代码内容。例如:


<pre>
<code id="code-area">
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
height: 100vh;
width: 100vw;
}
</code>
</pre>

注意,我们给 code 标签添加了一个 id 属性,方便后面通过 JS 获取它。


最后,我们需要在网页中添加一些 JS 代码,来调用 highlight.js 的方法,实现代码着色的功能。


highlight.js 提供了两个主要的方法:highlightElement 和 highlight。


这两个方法都可以实现代码着色的效果,但是适用于不同的场景。


highlightElement


highlightElement 方法适用于当你的代码是直接写在网页中的情况。


这个方法接受一个元素作为参数,并将该元素内部的文本内容进行着色处理。例如:


// 获取 code 元素
const codeEle = document.getElementById("code-area");
// 调用 highlightElement 方法,传入 code 元素
hljs.highlightElement(codeEle);

如果一切顺利,你应该能看到类似下图的效果:



代码已经被着色了,并且你可以看到代码被替换成了一个个标签,标签被加上了样式。


在最后的原理里我们在详细的说一下。


highlight


highlight 方法适用于当你的代码是通过 Ajax 请求获取到的纯文本数据的情况。


这个方法接受一个字符串作为参数,并返回一个对象,包含着色后的文本内容和代码的语言。例如:


<script>
const codeEle = document.getElementById('code-area')
// 比如说现在 code 就是 Ajax 返回的数据,lang 就是代码语言,content 就是代码内容
const code = {
lang: 'css',
content: `
* {
margin: 0;
padding: 0;
}`

}
// 我们接下来可以使用 hljs.highlight,将代码内容与代码语言传入进去
const result = hljs.highlight(code.content, {
language: code.lang
})
// 它会返回一个结果,我们打印到控制台看看
console.log('result >>> ', result)
</script>


我们可以看到,打印出来的是一个对象,code 是它原始的代码,language 是它的语言,而 value 就是它着色后的代码。


那么现在要做的就是将 value 添加到 code 元素里边去。


<script>
const code = {
lang: 'css',
content: `
* {
margin: 0;
padding: 0;
}`

}
const result = hljs.highlight(code.content, {
language: code.lang
})
const codeEle = document.getElementById('code-area')
codeEle.innerHTML = result.value
</script>


我们可以看到,代码确实被着色了,但是和之前的有所差别,我们看一下是什么原因。



打开控制后我们发现,用这种方式 code 元素就没有办法被自动加上类样式了,所以说我们就需要手动给 code 加上类样式才可以。


// 通过 className 为 code 手动添加类样式,并添加类的语言
codeEle.className = `hljs language-${code.lang}`

highlight.js 的语言支持


无论使用哪种方法,都需要注意指定代码所属的语言。


如果不指定语言,highlight.js 会尝试自动识别语言,并可能出现错误或不准确的结。


指定语言可以通过两种方式:



  • 在 code 标签中添加 class 属性,并设置为 language-xxx 的形式,其中 xxx 是语言名称。

  • 在调用 highlightElement 或 highlight 方法时,在第二个参数中传入一个对象,并设置 language 属性为语言名称。



上图是 highlight.js 支持的语言,可以看到有很多种,需要用其他语言的时候,language 设置成指定的语言名称就可以了。


原理


它的原理你可能已经猜到了,在 highlightElement 里我们简单说了一下,现在再看下图:



之所以可以实现着色,其实就是查找和替换的过程,将原来的纯文本替换为元素标签包裹文本,元素是可以加上样式的,而样式就是我们引入的 css 文件。


这就是它的基本原理了。


总结


其实有时候我们去设置 Mackdown 的自定义样式呢,在代码区域设置的时候也是这样设置的,当然类样式的名字呢,基本上都是标准的格式。


好了,这个库分享介绍给你了,库的原理也为你做了简单的科普,希望对你有所帮助。


如果你有什么问题或建议,请在评论区留言,如果你觉得这篇文章有用,请点赞收

作者:子辰Web草庐
来源:juejin.cn/post/7245584147456507965
藏或分享给你的朋友!

收起阅读 »

前端没了?也许是刚开始

前段时间社区上大肆讨论「前端已死」,各种唱衰前端的言论此起彼伏,真是闻者落泪,听者伤心。 最近又听说某大厂取消大前端部门,前端被拆分到各个业务组。很多前端高 P 或离职,或被裁,或转后端。 这是前端的落日? 今天就瞎聊聊,聊到哪算哪。 前端技术发展趋于稳定 过...
继续阅读 »

前段时间社区上大肆讨论「前端已死」,各种唱衰前端的言论此起彼伏,真是闻者落泪,听者伤心。


最近又听说某大厂取消大前端部门,前端被拆分到各个业务组。很多前端高 P 或离职,或被裁,或转后端。


这是前端的落日?


今天就瞎聊聊,聊到哪算哪。


前端技术发展趋于稳定


过去前端最被开发者诟病的是发展太快了,三天不学前端就跟不上了。


在我个人的前端经历中,技术栈从 Jquery 到 Angular,再到 React,前端技术快速切换,同时配套的前端研发体系也在不断推倒重建。


仅仅 React,从 V14 到 V15,再到 V16,一路快速迭代,每一个版本都有质的飞跃。尤其是 V16.8 hooks 出来之后,各种生态工具又得重建。


但最近几年,前端技术发展似乎慢了下来,React 16.8 版本还是 19 年发布的,距离现在已经四年多时间了,在这四年多时间内,React 虽然发布了 V17、V18,但并没有什么质的提升。对于我而言,只要能用 Hooks,16/17/18 哪个版本无所谓。


前端生态趋于稳定,是好事还是坏事?


我以前很羡慕 JAVA 开发同学,学习一套技术体系可以用到老。现在似乎前端也可以这样了,我认为这是一件非常好的事情,是前端成熟的标志。


只有前端技术体系不再发展,前端工程师才能 100% 精力投入在产品建设上。不用总是分精力去学习新技术,新轮子,最大程度复用过去的学习成果和基础设施。


比如现在有个需求是计算 43✖️5 的结果,我们会使用现有的方法,计算出结果,而不是要去思考有没有更好的计算方法。


image.png


本质上是我们的计算方法已经非常成熟稳定了,我们只要专注需求本身。前端什么时候稳定到这个程度,就皆大欢喜了。


公司中前端部门的发展


再聊聊一个公司中,前端团队定位的问题。



  1. 公司发展初期,产品建设高速期,前端的首要目标是建设产品,这时候前端通常在业务部门中,大家全力做产品。

  2. 随着公司发展,产品越来越多,各个业务部门的前端交流少,使用的技术五花八门,重复造轮子。这时候公司通常会搞一个大前端部门,使其研制出来一套在公司内成熟的前端解决方案,统一各种基建,提高人均效能。

  3. 当前端解决方案统一到一定程度之后,大前端部门的使命就结束了,这时候前端又会被拆分到业务部门中。


这里说说阿里大前端部门拆分,我认为原因有两个:



  1. 阿里前端基建相对已经成熟,拆分之后对前端研发效能影响不大。

  2. 公司不挣钱,不吃大锅饭了。以前是各个业务部门一起挣钱,一起养着大前端团队。现在公司不赚钱了,要改革,要求各个业务线自己挣钱自己花,所以前端开发也要回归到业务团队中。


最近并不只是「前端已死」了,我认为各个工种都在面临裁员潮,原罪是公司不赚钱了。在发展阶段啥问题都没有,一旦发展停滞,那什么都是问题,一个一个开刀。


对于个体来讲,如果想在这个公司发展,就要尽力帮助公司成长。另外就是居安思危,提升个人竞争力,只有跑的比别人快,才能在逆风中活下来,才能比别人更容易找到工作。


这里不得不吐槽下,很多人乐意躺平,你说要学习,要往前走一步,他会喷你卷,喷你带坏了风气,喷你是资本主义的走狗。大无语~


一个前端创业机会


之前在蚂蚁,前端基建非常完善,开发者真正的只用关心业务,不用去思考技术的东西。在公司内部基本上做到了只用关心 43✖️5 这个需求,不用考虑该用哪种计算方法。


从蚂蚁离开后,非常怀念蚂蚁的前端解决方案,但在社区上,并没有类似的收费或者免费方案。
据我所知,各个大公司,都会有自己的前端解决方案。但中小公司,基本上没有能力去自建一套类似解决方案。


阿里的后端解决方案,有商业化的产品「云效」。


蚂蚁的后端解决方案,有商业化的产品「SOFAStack」。


微软的后端解决方案,有商业化的产品「Azure DevOps」。


为什么后端有,前端没有呢?


我认为是后端技术栈稳定,基于这一套技术栈的生态也就稳定,发展多年下来势必会有成熟的解决方案。大公司产出商业化解决方案,给中小公司用。


前端技术栈过去发展快,生态不稳定,大公司内部的解决方案也是最近一两年才出来,所以前端目前还没有类似的解决方案。


所以我认为前端解决方案,是未来一个确定性的机会。


这个解决方案覆盖前端研发的整个生命流程,包括不限于:



  • 产品:埋点、数据分析等等

  • 开发:迭代协作、脚手架、组件库、逻辑库、联调、国际化、跨端等等

  • 上线:灰度、回滚、CDN、监控告警、性能、SEO、SSR、SSG、离线包、安全等等


希望未来前端解决方案,像乘法解决方案一样,能成为行业统一规范,让开发者真正的只用关心业务,不用关心技术。


前端的未来


说几点自己的思考:



  1. 前端技术趋于稳定,开始有商业化的前端解决方案出来。

  2. 前端不会消失,但门槛会进一步降低,低端前端饱和。

  3. 高级前端依旧紧缺,因为高级前端并不是只是看前端技能,而是综合考虑技术能力、业务能力、沟通能力、情商、职业素养、工作经验等等,这个过去现在未来都不会变。

  4. 前端工程师可能会变成用户体验工程师,后端考虑存储并发等,前端考虑用户体验,为整体用户体验负责。

  5. 前端会进一步蚕食桌面端开发、移动端开发的生存空间。

  6. AI 可能会改变前端研发模式。


总结


也许前端技术栈稳定之后,才是真正的开始。


文中所有观点未经论证,纯属 YY,欢迎理性讨论。

作者:前端技术砖家
来源:juejin.cn/post/7245874747390083109

收起阅读 »

996.ICU发起人勾结境外势力,被判颠覆国家政权罪刑拘!

996  ICU996.ICU指工作996、生病ICU,也就是工作从早上9点上班到晚上9点下班,每周工作6天,生病了就住进ICU。2019年3月27日,一个名为996ICU的项目在GitHub上传开。程序员们揭露“996ICU”互联网公司,...
继续阅读 »

996  

ICU


996.ICU指工作996、生病ICU,也就是工作从早上9点上班到晚上9点下班,每周工作6天,生病了就住进ICU。2019年3月27日,一个名为996ICU的项目在GitHub上传开。程序员们揭露“996ICU”互联网公司,抵制互联网公司的996工作制度。 [1-2]  

2019年4月8日,反996许可发布满一周,已有104个项目采用该许可。 [3] 

2019年4月11日,人民日报针对“996工作制”发表评论员文章《强制加班不应成为企业文化》 [4]  ;

马老师的"996是福报”更加让996广为社会传播和诟病

今天中国BAT这些公司能够996,我认为是我们这些人修来的福报。这个世界上,我们每一个人都希望成功,都希望美好生活,都希望被尊重,我请问大家,你不付出超越别人的努力和时间,你怎么能够实现你想要的成功?今天我们拥有这么多资源,我们带着巨大的使命,希望在未来能够让天下没有难做的生意,你不付出可以吗?不可以。 [5]  (马云评)

然后,ToB技术社区:qidao123.com臭名昭著的“996 ICU”谁曾料到这居然是有人和境外势力勾结导演传播的呢?








天网恢恢疏而不漏

始作俑者最终等待啊他的是牢狱之灾!

来源:https://mp.weixin.qq.com/s/YQLQPlyF_ljSrV0UPNgtTA

收起阅读 »

优雅的使用位运算,省老多事了!!!

web
你好,我是泰罗凹凸曼,今天我们来一篇JS中的位运算科普,经常在源码中看到的位运算符,和用其定义的一系列状态到底有什么优势? 位运算符号的基本了解 首先,我们应该要简单了解位运算符,常用的位运算符大概有以下几种,我们可以在JS中使用 toString 将数字转换...
继续阅读 »

你好,我是泰罗凹凸曼,今天我们来一篇JS中的位运算科普,经常在源码中看到的位运算符,和用其定义的一系列状态到底有什么优势?


位运算符号的基本了解


首先,我们应该要简单了解位运算符,常用的位运算符大概有以下几种,我们可以在JS中使用 toString 将数字转换为二进制查看,也可以通过 0b 开头来手动创建一个二进制数字:


(3).toString(2) // 11
0b00000011 // 3 前面的几位0可以省略,可以简写为 0b11

1. 与 &


按位对比两个二进制数,如果对应的位都为 1,则结果为 1,否则为 0


console.log((1 & 3) == 1) // true

对比图例如下所示:



2. 或 |


按位对比两个二进制数,如果对应的位有一个 1,则结果为 1,否则为 0


console.log((1 | 3) == 3) // true

对比图例如下所示:



3. 异或 ^


按位对比两个二进制数,如果对应的位有且只有一个 1,则结果为 1,否则为 0


console.log((1 ^ 3) == 2) // true

对比图例如下所示:



4. 非 ~


按位对操作的二进制数取反,即 1 变 0,0 变 1,任何数的非运算符计算结果都是 -(x + 1)


const a = -1 // ~a = -(-1 + 1) = 0
console.log(~a) // 0
const b = 5 // ~b = -(5 + 1) = -6
console.log(~b) // -6

一个数和它的取反数相加的结果总为 -1


5. 左移 <<


左移会将二进制值的有效位数全部左移指定位数,被移出的高位(最左边的数字)丢弃,但符号会保留,低位(最右边的数字)会自动补0


console.log(1 << 2) // 4

图例如下所示:



6. 右移 >>


和左移相反的操作,将二进制的操作数右移指定位数,高位补0,低位丢弃!


console.log(4 >> 2) // 1

参考资料均来自 MDN,除了这些常用的符号之外,文档还标注了所有的JS操作符号,感兴趣的同学可以看一看!


有什么用?


说了这么多符号,对于操作符的影响是加深了,但是有什么用呢?二进制数字难理解,位操作符也难理解,二进制和十进制的互转不写个代码都心算不了,相信各位同学肯定有如此费解,我们先来看一段 Vue 的源代码,其中定义了很多状态类的字段!


源码位置戳这里



以及 Vue 中对其的使用,源码位置戳这里



我们可以看到,Vue 定义了一系列状态列标识一个 Dom 是属于什么类型,并用 VNode 中的一个字段 shapeFlag 来完成存储和判断,对状态的存储只用到了一个字段一个数字,就可以进行多种状态的判断!


我们尝试着设计一种类似的判断结构出来如何?


我有N个权限


假设系统中的用户我们规定其有增删改查四个权限,我们可以设计一个枚举类来标识拥有的四个权限:


enum UserPerm {
CREATE = 1 << 0,
DELETE = 1 << 1,
UPDATE = 1 << 2,
SELECT = 1 << 3,
}

我们设计的时候,完全不必在意上述的二进制的十进制值是什么,只需要清楚的是,上述枚举的 1 在二进制位的哪个位置,如 1 的 二进制为 00000001,将其左移 1(1 << 1), 就变成了 00000010, 依次类推,我们用一个二进制串中的每一位来标识一个权限,这样一个字符串中只要出现对应位置的 1, 则该用户就拥有对应位置的权限,如图:



有什么好处呢?


我们知道二进制是可以转换为十进制的,这样子我们就可以用一个数字来表示多个权限,如一个用户完整的拥有四个权限,那他的二进制为 0b1111, 那么其状态为数字 15


如果一个用户只有 CREATESELECT 的权限,那么二进制表达为 0b1001,十进制数字为 9


后端数据库中,前端用户信息中,接口返回都只有一列一个字段就可以表示,那么用户信息应该是下面的形式:


const userInfo = {
name: '泰罗凹凸曼',
phone: '15888888888',
perm: 9, // 代表其只有 CREATE 和 SELECT 两种权限
}

权限的判断


如何判断这个用户是否具备某一个权限呢?那就需要请出我们的 与运算符(&),参考 Vue 的做法:


console.log(userInfo.perm & UserPerm.CREATE) // 9 & (1 << 0) = 1

console.log(userInfo.perm & UserPerm.UPDATE) // 返回 0, 0代表不通过

如果 userInfo.perm 中包含 CREATE,就会返回 CREATE 的值,否则返回 0,在JS中,任何非0的数字都可以通过 if 判断,所以我们只需要一个判断就足够了!


if (userInfo.perm & UserPerm.CREATE) {
console.log('有创建权限')
} else {
console.log('没有创建权限')
}

什么原理?我们之前给过与运算符的图例,接下来我们看一下如上两句代码的图例所示:



我们看到,上下的符号位如果对不上的话,返回的结果都是 0,这样子我们就轻松实现了权限的判断


权限的增删


那么我们如何实现对一个用户的权限更新呢,比如给上面的用户新增一个 UPDATE 权限,这个时候我们就需要 或运算符(|)


比如:


userInfo.perm | UserPerm.UPDATE // 1001 | 0100 = 1101 = 13

这样子我们就对一个用户权限进行了增加,或的规则我们上面也给过图例,这里大家可以自己尝试理解一下,无非是两个二进制数 10010100 之间的或运算,只有其中一位为 1 则为 1,这两个数字计算的结果自然是 1101


那么如何实现权限删除呢?异或运算符(^)给你答案!有且只有一个 1,返回 1,否则为 0,删除对我们刚刚添加的 UPDATE 权限的方法:


userInfo.perm ^ UserPerm.UPDATE // 1101 ^ 0100 = 1001

非常简单是吧?看到这里,相信你已经完全理解位运算符在权限系统的妙用了,如果我这个时候需要添加一个新的权限,如分享权限,那么我只有用第五位的1来表示这个权限就可以啦


enum UserPerm {
SHARE = 1 << 5
}

// 添加分享权限
userInfo.perm | UserPerm.SHARE

以前的方案


我们以前在做用户标识的时候,通常会定义一个数组来表示,然后执行数组判断来进行权限的判断


const userPerm = ['CREATE', 'UPDATE', 'DELETE', 'SELECT']

// 判断有无权限
if (userPerm.includes('CREATE')) {
// ...
}

// 增加权限
user.perm.push('UPDATE')

// 删除权限
user.perm.splice(user.perm.indexOf('UPDATE'), 1)

相信大家也可以看出来,无论是从内存占用,效率,便捷程度来说位运算符的形式都是完胜,这也是会被各大项目使用的原因之一!快去你的项目中实践吧,记得写好注释哦!


结语


今天带大家认识了位运算符在权限系统的妙用,小伙伴们还有什么使用位运算符的巧妙思路,可以在评论中给出来哦!继续加油吧,快去实践少年!


祝大家越来越牛逼!


去探索,不知道的东西还多着呢,我是泰罗凹凸曼,M78星云最爱写代码的,我们下一篇再会!


作者:泰罗凹凸曼
来源:juejin.cn/post/7244809939838844984
收起阅读 »

晋升涨薪?不,晋升要命!

最近有个朋友来问我晋升的事,其实他没什么产出,只不过因为做的那块业务还不错,赚了好几个小目标,然后顺理成章的被提名了。所以他慌的要死! 你猜他来问什么内容?他什么都想问,又什么都问不出来,因为我也没晋升过。不过这并不妨碍我写点什么,谁让我会编呢? 我要不要提...
继续阅读 »

最近有个朋友来问我晋升的事,其实他没什么产出,只不过因为做的那块业务还不错,赚了好几个小目标,然后顺理成章的被提名了。所以他慌的要死!


你猜他来问什么内容?他什么都想问,又什么都问不出来,因为我也没晋升过。不过这并不妨碍我写点什么,谁让我会编呢?



我要不要提名晋升


我要不要提名晋升?你要是能问出这问题,要不刚毕业,要不加班加迷糊了!


晋升意味着涨薪,涨薪意味着每个月能多吃几顿海底捞,多看几部电影,这世道和谁过不去都不能和钱过不去。


但晋升往往是领导说了才算,除非你是向园,还有个董事长爷爷。所以,怎么说服他就成了一道槛。


工作三要素:A-能力;B-岗位;C-环境(其他人和事),而晋升基本只和 AB 有关。 果你在自己岗位上,已经承担了下一级该承担的责任。然后能力又达到了下一级所要求的水平,再不提名晋升就没天理了


例如我是P6,但是我一直在做P7的事情,同时在抗P7的责任,并且表现不错。那么我对标P7不就是既定事实嘛,既然是事实谁又能阻止你提名?


另外还有 C,如果把晋升与环境挂钩,晋升的理由变成了诸如  “如果我在他的位置上,我能做得比他更好”、“为什么他是P9而我是P8”,以这些理由提名晋升,属实是自寻烦恼,说不定明年你就成为人才输送给社会了。


从这个角度上看,我那位朋友已经晋升失败了。不过你们也不要太关注这个,缘分这个东西不是说有就有。就像你能恰好看见我这篇帖子,然后顺手点赞、收藏、在看一样



提名之后,如何准备答辩


晋升靠的是硬实力,以及10%的运气。 你想去吹牛也不是不行,就怕到时候下不来台。仅仅是 P6 升 P7 的答辩,上面坐着的都是P9级别的大佬和砖家。


PPT 以真实、简朴为主,凡是在 PPT 上花费超过10小时的,我觉得都有耍流氓的嫌疑。这些内容应该是这段时间你所积累的工作成果。平时没事拿个小笔记记一下,关键时刻它能像宋江一样救你的命。【推荐你用语雀,真的很好用】


有了 PPT,你得去讲出来吧。讲话作为一门艺术,对于我们理工科的同学还是有一定难度的,所以我建议你有空去参加下吐槽大会。没有条件?那就创造条件,公司里找几个段子手还不是轻而易举。相比之下,产品经理的优势比我们大多了。



如何把实力讲透?这里面是有一定技巧的,3分讲结果,7分讲过程。光讲结果不讲过程,30分钟的答辩,你5分钟就完成了,还是包含自我介绍的那种。


3分成效如何讲?——把我在当前岗位上,如何把手里的工作做上了一个新台阶 这种感觉讲出来,就是,因为你的努力而带来了什么改变?


7分过程如何讲?——把事情的复杂度、岗位的挑战、面临的困难讲清楚,把你做事的匠心讲清楚,你把你的做事的方法、思路讲清楚。说白了,就是“我解了一个挺难的题,我是这样那样解的”;


关于答辩与专家评委


评委扮演的角色很简单,评审的过程,就是评委向答辩人学习的过程。每个人都存在未涉及的领域,你看 ChatGPT 用了上亿的数据训练,花了几十亿美金,现在连小学数学题都解不出来


三个评委,花45分钟与答辩人进行深度交流,如果评委们都表示没有收获,学不到东西(无论是学到知识还是方法或者心态),那么答辩人晋升不通过,也不冤枉。


这个道理够简单吧,神雕侠侣里黄老邪为什么会和杨过拜把子,一方面是杨过的性格和黄老邪很像,另一方面是因为能从杨过那学到点东西。


回到现实,我们每个人都有直接的体会。如果有个大牛(至少他在当前的工作中是专业的)跟我们交流,我们一定有收获。如果对方十分平庸(或者在工作中能力一般),我们收获就比较少。


晋升通过,意味着什么


意味着加薪,年终奖多了点


意味着岗位(B)对你的能力(A)要求更大了一些


意味着你离 3.25 更近了一些,我知道的几个同事,每次晋升之后的第一个季度或半年度,都会拿一次3.25。


晋升不通过,意味着什么


恭喜你,终于松了口气!


结尾


如果这篇文章对您有所帮助,可以关注我的公众号。



我是车辙,掘金小册《SkyWalking》作者

作者:车辙cz
来源:juejin.cn/post/7244783947820449853
,一个神奇的程序员。

收起阅读 »

js的垃圾回收机制

web
概论 对于js的垃圾回收,很多人的理解还停留在引用计数和标记清除的阶段。 有人会说,学习这个,对业务代码开发没啥作用。但是我想说,了解了这些基础的东西之后,才能更好地组织代码,在写代码的时候,才能做到心中能有个框架,知道浏览器到底发生了什么。 我也不是科班出身...
继续阅读 »

概论


对于js的垃圾回收,很多人的理解还停留在引用计数和标记清除的阶段。


有人会说,学习这个,对业务代码开发没啥作用。但是我想说,了解了这些基础的东西之后,才能更好地组织代码,在写代码的时候,才能做到心中能有个框架,知道浏览器到底发生了什么。


我也不是科班出身,很多东西不清不楚的。但我感觉计算机行业有个很好的地方,就是学了的知识很快就能得到验证,就能在生产上应用。这种成就感是我当年干机械的时候所无法体验到的。


前几天就有个报障说有个项目越用越卡,但是排查不出问题,我最近正好在学习垃圾回收内存泄漏,就立马能分析出来是不是内存不足产生的影响,就很开心。


本文我会采用图解的方式,尽量照着js垃圾回收的演变历史讲解。


一,什么是垃圾回收


GCGarbage Collection,也就是我们常说的垃圾回收。


我们知道,js是v8引擎编译执行的,而代码的执行就需要内存的参与,内存往往是有限的,为了更好地利用内存资源,就需要把没用的内存回收,以便重新使用。


比如V8引擎在执行代码的过程中遇到了一个函数,那么我们会创建一个函数执行上下文环境并添加到调用栈顶部,函数的作用域里面包含了函数中所有的变量信息,在执行过程中我们分配内存创建这些变量,当函数执行完毕后函数作用域会被销毁,那么这个作用域包含的变量也就失去了作用,而销毁它们回收内存的过程,就叫做垃圾回收。


如下代码:


var testObj1={
a:1
}
testObj1={
b:2
}

对应的内存情况如下:


1,垃圾的产生.drawio.png
其中堆中的{a:1}就变成了垃圾,需要被GC回收掉。


在C / C++中,需要开发者跟踪内存的使用和管理内存。而js等高级语言,代码在执行期间,是V8引擎在为我们执行垃圾回收。


那么既然已经有v8引擎自动给我们回收垃圾了,为啥我们还需要了解V8引擎的垃圾回收机制呢?这是因为依据这个机制,还有些内存无法回收,会造成内存泄漏。具体的表现就是随着项目运行时间的变成,系统越来越卡滞,需要手动刷新浏览器才能恢复。


了解V8的垃圾回收机制,才能让我们更好地书写代码,规避不必要的内存泄漏。


二,内存的生命周期


如上所说,内存应该存在这样三个生命周期:




  1. 分配所需要的内存:在js代码执行的时候,基本数据类型存储在栈空间,而引用数据类型存储在堆空间。




  2. 使用分配的空间:可能对对应的值做一些修改。




  3. 不需要时将其释放回收。


    如下代码:


    function fn(){
    //创建对象,分配空间
    var testObj={
    a:1
    }
    //修改内容
    testObj.a=2
    }
    fn()//调用栈执行完毕,垃圾回收

    对应的内存示意图:




2,内存的生命周期.drawio.png


三,垃圾回收的策略


当函数执行完毕,js引擎是通过移动ESP(ESP:记录当前执行状态的指针)来销毁函数保存在栈当中的执行上下文的,栈顶的空间会被自动回收,不需要V8引擎的垃圾回收机制出面。


然而,堆内存的大小是不固定的,那堆内存中的数据是如何回收的呢?


这就引出了垃圾回收的策略。


通常用采用的垃圾回收有两种方法:引用计数(reference counting)标记清除(mark and sweep)


3.1,引用计数(reference counting)


如上文第二节中所说,testObj对象存放在堆空间,我们想要使用的时候,都是通过指针来访问,那么是不是只要没有额外的指针指向它,就可以判定为它不再被使用呢?


基于这个想法,人们想出了引用计数的算法。


它工作原理是跟踪每个对象被引用的次数,当对象的引用次数变为 0 时,则判定该对象为无用对象, 可以被垃圾回收机制进行回收。


    function fn(){
//创建对象,分配空间
var testObj1={
a:1
}//引用数:1
var testObj2=testObj1//引用数:2
var testObj3=testObj1//引用数:3
var testObj4={
b:testObj1
}//引用数:4
testObj1=null//引用数:3
testObj2=null//引用数:2
testObj3=null//引用数:1
testObj4=null//引用数:1
}
fn()//调用栈执行完毕,垃圾回收

如上代码,引用次数变成0后,堆内存中的对应内存就会被GC。


如下图,当testObj1-4都变成null后,原来的testObj4引用数变成0,而{a:1}这时候的引用数还为1(有一个箭头指向它),而{b:1002}被回收后,它的引用数就变成0,故而最后也被垃圾回收。


3,引用计数的计数数量.drawio.png


引用计数的优点:


引用计数看起来很简单,v8引擎只需要关注计数器即可,一旦对象的引用数变成0,就立即回收。

但是很明显的,引用计数存在两个缺点:


1,每个对象都需要维护一个计数器去记录它的引用数量。
2,如果存在相互循环引用的对象,因为各自的引用数量无法变成0(除非手动改变),因而无法被垃圾回收。

对于第二点,如下代码:


function fn(){
//创建对象,分配空间
var testObj1={
a:testObj2
}
var testObj2={
b:testObj1
}
}
fn()

当fn执行完毕后的内存情况如下,因为两个对象相互引用,导致引用数到不了0,就无法被GC:


4.循环引用.drawio.png


因为引用计数的弊端,后续的浏览器开始寻找新的垃圾回收机制,从2012年起,所有现代浏览器都使用了标记清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记清除算法的改进。


3.2,标记清除(mark and sweep)


标记清除是另一种常见的垃圾回收机制。


它工作原理是找出所有活动对象并标记它们,然后清除所有未被标记的对象


其实现步骤如下:



  1. 根节点:垃圾回收机制的起点是一组称为根的对象(有很多根对象),根通常是引擎内部全局变量的引用,或者是一组预定义的变量名,例如浏览器环境中的 Window 对象和 Document 对象。

  2. 遍历标记:从根开始遍历引用的对象,将其标记为活动对象。每个活动对象的所有引用也必须被遍历并标记为活动对象。

  3. 清除:垃圾回收器会清除所有未标记的对象,并使空间可用于后续使用。


因为能从根节点开始被遍历到的(有被使用到的),就是有用的活动对象,而剩余不能被链接到的则是无用的垃圾,需要被清除。


对于前文引用计数中循环引用的例子,就因为从根对象触发,无法遍历到堆空间中的那两个循环引用的对象,就会把它判定为垃圾对象,从而回收。


如下代码:


var obj1={
a:{
b:{
c:3
}
}
}
var obj2={
d:1
}
obj2=null

如下图,从根节点无法遍历到obj2了,就会把d垃圾回收。


5,标记清除.png


按照这个思路,标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,会导致空闲内存空间是不连续的,出现了 内存碎片(如下图),并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题:当新对象需要空间存储时,需要遍历空间以找到能够容纳对象大小size的区域:


6,标记清除新增对象.png


这样效率比较低,因而又有了标记整理(Mark-Compact)算法 ,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会先将活动对象向内存的一端移动,然后再回收未标记的垃圾内存:


7,标记整理算法.png


四,V8引擎的分代回收


如上文所说,在每次垃圾回收时都要检查内存中所有的对象,这样的话对于一些占用空间大、存活时间长的对象,要是和占用空间小、存活时间短的对象一起检查,那不是平白浪费很多不必要的检查资源嘛。


因为前者需要时间长并且不需要频繁进行清理,后者恰好相反,那怎么优化呢?


类似于信誉分,信誉分高的,检查力度就应该小一些嘛。把信誉分抽象一下,其实说的就是分层级管理,于是就有了弱分代假设。


4.1,弱分代假设(The Weak Generational Hypothesis)



  1. 多数对象的生命周期短

  2. 生命周期长的对象,一般是常驻对象


V8的GC也是基于假设将对象分为两代: 新生代和老生代。


对不同的分代执行不同的算法可以更有效的执行垃圾回收。


V8 的垃圾回收策略主要基于分代式垃圾回收机制,将堆内存分为新生代和老生代两区域,采用不同的策略来管理垃圾回收。


他们的内存大小如下:


64位操作系统32位操作系统
V8内存大小1.3G(1432MB)0.7g(716MB)
新生代空间32MB16MB
老生代空间1400MB700MB

4.2,新生代的垃圾回收策略


新生代对象是通过一个名为 Scavenge 的算法进行垃圾回收。


在JavaScript中,任何对象的声明分配到的内存,将会先被放置在新生代中,而因为大部分对象在内存中存活的周期很短,所以需要一个效率非常高的算法。Scavenge算法是一个典型的牺牲空间换取时间的复制算法,在占用空间不大的场景上非常适用。


Scavenge 算法中将新生代内存一分为二,Semi space FromSemi space To,新生区通常只支持 1~8M 的容量。这块区域使用副垃圾回收器来回收垃圾。


工作方式也很简单:


1,等From空间满了以后,垃圾回收器就会把活跃对象打上标记。
2,把From空间已经被标记的活动对象复制到To空间。
3,将From空间的所有对象垃圾回收。
4,两个空间交换,To空间变成From空间,From变成To空间。以此往复。

而判断是否是活跃对象的方法,还是利用的上文说的从根节点遍历,满足可达性则是活跃对象。


具体流程如下图所示,假设有蓝色指针指向的是From空间,没有蓝色指针指向的是To空间:


8,新生代的垃圾回收策略.drawio.png


从上图可以明显地看到,这种方式解决了上文垃圾回收后内存碎片不连续的问题,相当于是利用空间换时间。


现在新生代空间的垃圾回收策略已经了解,那新生代空间中的对象又如何进入老生代空间呢?


4.3,新生代空间对象晋升老生代空间的条件


1,复制某个对象进入to区域时,如果发现内存占用超过to区域的25%,则将其晋升老生代空间。(因为互换空间后要留足够大的区域给新创建对象)
2,经过两次fromto互换后,还存活的对象,下次复制进to区域前,直接晋升老生代空间。

4.4,老生代空间的垃圾回收策略


老生代空间最初的回收策略很简单,这在我们上文也讲过,就是标记整理算法。


1,先根据可达性,给所有的老生代空间中的活动对象打上标记。
2,将活动对象向内存的一端移动,然后再回收未标记的垃圾内存。

这样看起来已经很完美了,但是我们知道js是个单线程的语言,就目前而言,我们的垃圾回收还是全停顿标记:js是运行在主线程上的,一旦垃圾回收生效,js脚本就会暂停执行,等到垃圾回收完成,再继续执行


这样很容易造成页面无响应的情况,尤其是在多对象、大对象、引用层级过深的情况下。


于是在这基础上,又有了增量标记的优化。


五,V8优化


5.1,增量标记


前文所说,我们给老生代空间中的所有对象打上活动对象的标记,是从一组根节点出发,根据可达性遍历而得。这就是全量地遍历,一次性完成,


但因为js是单线程,为了避免标记导致主线程卡滞。于是人们想出来和分片一样的思路:主线程每次遍历一部分,就去干其他活,然后再接着遍历。如下图:


9,增量标记.png


增量标记就是将一次 GC 标记的过程,分成了很多小步,每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成一轮 GC 标记,这样就算页面卡滞,因为时间很短,使用者也感受不到,体验就好了很多。


但是这又引发了一个新的问题:每次遍历一部分节点就停下来,下次继续怎么识别到停顿点,然后继续遍历呢?


V8又引入了三色标记法。


5.2,三色标记法


首先要明确初心:三色标记法要解决的问题是遍历节点的暂停与恢复。


使用两个标志位编码三种颜色:白色(00),灰色(10)和黑色(11)。


白色:指的是未被标记的对象,可以回收


灰色:指自身被标记,成员变量(该对象的引用对象)未被标记,即遍历到它了,但是它的下线还没遍历。不可回收


黑色:自身和成员变量都被标记了,是活动对象。不可回收


1,从已知对象开始,即roots(全局对象和激活函数), 将所有非root对象标记置为白色
2,将root对象变黑,同时将root的直接引用对象abc标记为灰色
3,将abc标记为黑色,同时将它们的直接引用对象标记为灰色
4,直到没有可标记灰色的对象时,开始回收所有白色的对象

10,三色标记法.drawio.png


如上图所示,如果第一次增量标记只标记到(2),下次开始时,只要找到灰色节点,继续遍历标记即可。


而遍历标记完成的标志就是内存中不再有灰色的。于是这时候就可以把白色的垃圾回收掉。


那这样就解决了遍历节点的暂停与恢复问题,同时支持增量标记。


(ps:其实这里我有个疑惑,暂停后重新开始的时候,不也要遍历寻找灰色节点嘛,每次恢复都要遍历找灰色节点,不是也耗时嘛?)


5.3,写屏障


按照上文对标记的描述,其实有一个前提条件:在标记期间,代码运行不会变更对象的引用情况。


比如说我采用的是增量标记,前脚刚做好的标记,后脚就被js脚本修改了引用关系,那不是会导致标记结果不可信嘛?如下图:


11,写屏障.drawio.png


就像上图一样,D已经被判定成垃圾了,但是下一个分片的js又引用了它,这时候如果删除,必然不对,所以V8 增量使用 写屏障 (Write-barrier) 机制,即一旦有黑色对象引用白色对象,该机制会强制将引用的白色对象改为灰色,从而保证下一次增量 GC 标记阶段可以正确标记,这个机制也被称作 强三色不变性


那在我们上图的例子中,将对象 B 的指向由对象 C 改为对象 D 后,白色对象 D 会被强制改为灰色。


这样一来,就不会将D判定为垃圾 ,并且图中新增的垃圾C在本轮垃圾回收中也不会回收,而是在下一轮回收了。


5.4,惰性清理


上文的增量标记和三色标记法以及写屏障只是对标记方式的优化。目的是采用分片的思想将标记的流程碎片化。


而清理阶段同样可以利用这个思想。


V8的懒性清理,也称为惰性清理(Lazy Sweeping),是一种垃圾回收机制,用于延迟清理未标记对象所占用的内存空间,以减少垃圾回收期间的停顿时间。


当增量标记结束后,假如当前的可用内存足以让我们快速的执行代码,其实我们是没必要立即清理内存的,于是可以将清理的过程延迟一下,让JavaScript逻辑代码先执行;也无需一次性清理完所有非活动对象内存,垃圾回收器可以按需逐一进行清理,直到所有的页都清理完毕。


六,垃圾回收总结


6.1,初始的垃圾回收策略:从引用计数到标记清除


对于js的垃圾回收,最开始的时候,是采用引用计数的算法,但是因为引用计数存在循环引用导致垃圾无法清除,于是又引入了标记清除算法,而标记清除算法存在碎片空间问题,于是又优化成标记整理算法。


随着技术的发展,v8引擎的垃圾回收机制也在不断完善。


6.2,弱分代假设,划分新老生代空间采用不同策略


第一次完善是采用弱分代假设,为了让内存占用大、存活时间长的对象减少遍历,采用分代模型,分成了新分代和老分代空间,垃圾回收采取不同的策略。


新生代空间以空间换时间,拆分成from和to空间互换位置,解决垃圾回收后内存不连续的问题。


将满足条件的对象晋升到老生代空间。而老生代空间采用标记整理算法。


6.3,从全停顿到引入分片思想


因为js是单线程,如果垃圾回收耗时过长,就会阻塞页面响应。


为了解决标记阶段的全停顿问题,引入了增量标记算法。但是非黑即白的标记算法在下一次重新开始标记时无法找到上次的中断点,所以使用三色标记法。此外,为了避免增量标记过程中js脚本变更引用关系,v8又增加了写屏障。


同样的,为了解决清理阶段的全停顿问题,引入了惰性清理。


七,本系列其他文章


最近在整理js基础,下面是已经完成的文章:


js从编译到执行过程 - 掘金 (juejin.cn)


从异步到promise - 掘金 (juejin.cn)


从promise到await - 掘金 (juejin.cn)


浅谈异步编程中错误的捕获 - 掘金 (juejin.cn)


作用域和作用域链 - 掘金 (juejin.cn)


原型链和原型对象 - 掘金 (juejin.cn)


this的指向原理浅谈 - 掘金 (juejin.cn)


js的函数传参之值传递 - 掘金 (juejin.cn)


js的事件循环机制 - 掘金 (juejin.cn)


从作用域链和内存角度重新理解闭包 - 掘金 (juejin.cn)


八,本文参考文章:


「硬核JS」你真的了解垃圾回收机制吗 - 掘金 (juejin.cn)


一文带你快速掌握V8的垃圾回收机制 - 掘金 (juejin.cn)


[深入浅出]JavaScript GC 垃圾回收机制 - 掘金 (juej

in.cn)

收起阅读 »

23美团一面:双检锁单例会写吗?(总结所有单例模式写法)

面试经历 (后来别人跟我说这种写法nacos里也常见) 记录一次面试经历 2023.06.01 美团海笔的,本来以为笔的情况也不咋好 看到牛客网上一堆ak说没面试机会的 结果也不知怎地到了一面 (略过自我介绍和项目介绍~) 面试官:会写单例吗,写个单例看看 ...
继续阅读 »

面试经历


(后来别人跟我说这种写法nacos里也常见)


记录一次面试经历


2023.06.01


美团海笔的,本来以为笔的情况也不咋好 看到牛客网上一堆ak说没面试机会的


结果也不知怎地到了一面


image.png


(略过自我介绍和项目介绍~)


面试官:会写单例吗,写个单例看看


我:


// 饿汉式
public class SingleObject {
private static SingleObject instance = new SingleObject();

//让构造函数为 private
private SingleObject(){}

public static SingleObject getInstance(){
return instance;
}
}

面试官:嗯 你这个单例在没有引用的时候就创建了对象?优化一下


我:应该是懒汉模式!


public class Singleton {  
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
// 多了一个判断是否为null的过程
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

面试官:你这个线程不安全啊 再优化一下?


我:那就加个锁吧


public class Singleton {  
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

面试官:这种写法 多线程来了的话会阻塞在一起 能否再优化?


我:。。。 不会了


面试官:回去看看双检锁单例


···


之后问了数据库事务



  1. 读未提交(read uncommitted)

  2. 读已提交(read committed)

  3. 可重复读(repeatable read)

  4. 序列化(serializable)以及默认是哪个(repeatable read) 、


数据库的范式了解吗 等等


不出意外:


image.png


还是非常可惜的 这次机会 再加油吧


单例模式整理


学习自菜鸟教程


1.饿汉式


懒加载 no
多线程安全 yes
缺点:没有实现懒加载,即还未调用就创建了对象


public class Singleton {  
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}

2.懒汉式(线程不安全)


懒加载 yes
多线程安全 no


public class Singleton {  
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

3.懒汉式(线程安全)


懒加载 yes
多线程安全 yes
缺点:和面试官说的那样,多线程访问会阻塞


public class Singleton {  
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

4.双检锁/双重校验锁(DCL,即 double-checked locking)


public class Singleton {  
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

这段代码是一个单例模式的实现,使用了双重检查锁定的方式来保证线程安全和性能。双重检查锁定是指在加锁前后都进行了一次判空的操作,以避免不必要的加锁操作。


而为了保证双重检查锁定的正确性,需要使用volatile关键字来修饰singleton变量,以禁止指令重排序优化。(JUC的知识串起来了!)如果没有volatile关键字修饰,可能会出现一个线程A执行了new Singleton()但是还没来得及赋值给singleton,而此时另一个线程B进入了第一个if判断,判断singleton不为null,于是直接返回了一个未初始化的实例,导致程序出错。


使用volatile关键字可以确保多线程环境下的可见性和有序性,即一个线程修改了singleton变量的值,其他线程能够立即看到最新值,并且编译器不会对其进行指令重排序优化。这样就能够保证双重检查锁定的正确性。


学到了 !!!


后来别人提醒:


image.png


image.png


(牛逼。。)


5.登记式/静态内部类


public class Singleton {  
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

这段代码是一个单例模式的实现,使用了静态内部类的方式来保证线程安全和性能。静态内部类是指在外部类中定义一个静态的内部类,该内部类可以访问外部类的所有静态成员和方法,但是外部类不能访问内部类的成员和方法。


在这个单例模式的实现中,SingletonHolder就是一个静态内部类,它里面定义了一个静态的、final的、类型为Singleton的变量INSTANCE。由于静态内部类只有在被调用时才会被加载,因此INSTANCE对象也只有在getInstance()方法被调用时才会被初始化,从而实现了懒加载的效果。


由于静态内部类的加载是线程安全的,因此不需要加锁就可以保证线程安全。同时,由于INSTANCE是静态final类型的,因此保证了它只会被实例化一次,并且在多线程环境下也能正确地被发布和共享。


这种方式相对于双重检查锁定来说更加简单和安全,因此在实际开发中也比较常用。


6. 枚举


public enum Singleton {  
INSTANCE;
public void whateverMethod() {
}
}

这段代码是使用枚举类型实现单例模式的一种方式。在Java中,枚举类型是天然的单例,因为枚举类型的每个枚举值都是唯一的,且在类加载时就已经被初始化。


在这个示例代码中,Singleton是一个枚举类型,其中只定义了一个枚举值INSTANCE。由于INSTANCE是一个枚举值,因此它在类加载时就已经被初始化,并且保证全局唯一。在使用时,可以通过Singleton.INSTANCE来获取该单例对象。


需要注意的是,虽然枚举类型天然的单例特性可以保证线程安全和反序列化安全,但是如果需要延迟初始化或者有其他特殊需求,仍然需要使用其他方式来实现单例模式。


7. 容器式单例


Java中可以使用容器来实现单例模式,比如使用Spring框架中的Bean容器。下面是一个使用Spring框架实现单例的示例代码:



  1. 定义一个单例类,比如MySingleton:


public class MySingleton {
private static MySingleton instance;
private MySingleton() {}
public static MySingleton getInstance() {
if (instance == null) {
instance = new MySingleton();
}
return instance;
}
public void doSomething() {
// do something
}
}


  1. 在Spring的配置文件中定义该类的Bean:


<bean id="mySingleton" class="com.example.MySingleton" scope="singleton"/>


  1. 在Java代码中获取该Bean:


ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
MySingleton mySingleton = (MySingleton) context.getBean("mySingleton");
mySingleton.doSomething();

在上面的代码中,我们在Spring的配置文件中定义了一个名为mySingleton的Bean,它的类是com.example.MySingleton,作用域为singleton(即单例)。然后在Java代码中,我们通过ApplicationContext获取该Bean,并调用它的doSomething()方法。


使用Spring框架可以方便地管理单例对象,同时也可以很容易地实现依赖注入和控制反转等功能。


总结完成 继续加油!!!


作者:ovO
来源:juejin.cn/post/7244820297290465317
收起阅读 »

什么,产品让我实现自动播放?

web
前言 最近,在逛一些技术群时,看到有人在吐槽,这个video媒体标签设置autoplay属性怎么不生效。不生效就算了,为什么我在dom渲染完成时去获取video元素(假设获取到的元素为el),然后执行el.paly()也不生效,why???? 那为什么我通过控...
继续阅读 »

前言


最近,在逛一些技术群时,看到有人在吐槽,这个video媒体标签设置autoplay属性怎么不生效。不生效就算了,为什么我在dom渲染完成时去获取video元素(假设获取到的元素为el),然后执行el.paly()也不生效,why????


那为什么我通过控制台去执行这两行代码的时候,它又生效了!!!!???


带着满脸疑惑,我们来了解一下浏览器的自动播放策略。


1、浏览器的自动播放策略


以谷歌浏览器为例:


在某些特定的情况下,浏览器是允许自动播放的:




  1. 静音状态下始终允许自动播放




  2. 有声音自动播放时:



    • 用户进行了页面点击等与界面发生交互行为后。

    • 达到媒体参与指数, 也就是用户之前在本站播放过有声音的视频。

    • 用户将页面添加到移动设备的主屏幕上或者在PC上安装了PWA




  3. 主站可以将自动播放权限委托给它们的 iframe,以允许自动播放声音。




2、静音播放。


在静音状态下,浏览器是允许自动播放的,代码如下:


<video controls class="videoItem" width="100%" autoplay muted loop="loop" src="./tets.mp4"></video>

属性说明:


muted: 是否静音播放,默认为false


autoplay:是否自动播放,默认为false


control:控制器是否显示,默认为false(可不写)。


loop:是否循环播放,默认为false(可不写)。


唉,好像可以自动播放了,于是我拿去应付产品,产品给我泼了一盆冷水,说能让它播放起来有声音吗?

于是,又开始了我们的与非静音状态自动播放功能的探索。


3、非静音自动播放


我们在上面有了解到非静音自动播放有四种情况:



  1. 用户进行了页面点击等与界面发生交互行为后。

  2. 达到媒体参与指数MEI.

  3. 用户将页面添加到移动设备的主屏幕上或者在PC上安装了PWA

  4. 主站可以将自动播放权限委托给它们的 iframe。


3.1 用户进行了页面交互行为


这个就可以理解成用户触碰了页面之后,我们就可以进行有声音的自动播放了。看代码


HTML部分:


<video controls class="videoItem" width="100%"  loop="loop" src="./tets.mp4"></video>

JS部分:


const vdo = document.querySelector('video')
// 播放函数
async function playAudio() {
const res = await vdo.play()
}
// 监听用户点击
document.addEventListener('click',playAudio)
// 监听媒体播放
vdo.addEventListener('play', function () { //播放开始执行的函数
console.log("开始播放");
// 移除点击监听事件
document.removeEventListener("click",playAudio)
})

这里我们就有很大的发挥空间了,比如说视频不完全在可见区,或者用户看视频前给他弹个框?


3.2 达到媒体参与指数MEI


MEI是浏览器根据我们对一些网站的浏览行为打分,越高,就表示我们喜欢观看这个网站的视频,可以通过about://media-engagement来查询,不可更改。
注意:该策略只对PC端浏览器有效。


3.3 用户将页面添加到移动设备的主屏幕上或者在PC上安装了PWA


用户将页面添加到移动设备的主屏幕上或者在PC上安装了PWA,这个我们基本可以不用去考虑了,在国内是比较少见的,感兴趣的可以去百度。


3.4 主站可以将自动播放权限委托给它们的 iframe。


这个就是通过iframe来控制媒体的自动播放。代码如下:


    <!-- 允许自动播放 -->
<iframe src="跨源地址" allow="autoplay">

<!-- 允许自动播放和设置全屏 -->
<iframe src="跨源地址" allow="autoplay; fullscreen">

需要注意的是,我们可能需要做一些本地代理,不然可能会出现跨域问题。


4 补充


上面还有一点是没有提到的,就是同源下点击页面跳转后可实现自动播放。


比如:我在http://www.bilibili.com页面点击一个视频跳转到了一个新页面,就可以实现自动播放(注意:不能打开新窗口,否则非

作者:清_秋
来源:juejin.cn/post/7244818202214416443
静音自动播放将失效)

收起阅读 »

作为一个普通人,我是如何在浮躁的时代让自己减少焦虑的

前言 来自一个普通人的思考和分享,由于水平有限,认知有限,活动受限,观点可能也有个人偏见,不喜勿喷哈 欢迎JYM一起交流探讨人生、生人、上天,入地、活着、归去、等所有 法律允许, 社区允许 的开放性话题 自我介绍 一个普通人,坐标帝都,小厂前端攻城狮,每...
继续阅读 »

前言



来自一个普通人的思考和分享,由于水平有限,认知有限,活动受限,观点可能也有个人偏见,不喜勿喷哈


欢迎JYM一起交流探讨人生、生人、上天,入地、活着、归去、等所有 法律允许社区允许 的开放性话题



自我介绍



一个普通人,坐标帝都,小厂前端攻城狮,每天奋斗在一线,时不时加班1-3个小时,工作地在西北 4 环,家住东 6 环,由于每天 10 环极限通勤, 下班到家就是一副有气无力的死狗样,妥妥的一个社畜。


第二天早上又是元气满满的一天,精力充沛的出门去打拼,都是为了活着……



像牲口一样活下去.webp


基于周围的一些社会现状,分享一些自己的思考




  1. 追剧看视频的人,好多人使用的 1.25倍速,1.5倍速;漫无目的不停的刷着各种短视频,不停的快速的往下翻着;在手机上不停的翻看着微博,朋友圈,群记录等。



    仿佛什么都看了,又仿佛什么都没看 😟





  2. 经常手机上明明没有任何消息,总是打开翻两下,再息屏。



    似乎不想错过一些联系,又仿佛没有人联系。手机让我们通信更方便了,仿佛也让我们更孤独了 😕





  3. 多少人每天睡醒的第一件事就是打开手机,从起床开始,看手机朋友圈,看热搜,看头条,看推送,看信息,看邮件,吃饭看手机,走路看手机,去上班路上看手机,站着看,躺着看,坐着看,拉便便也看。 最终得到了什么?得到了焦虑、暴躁、前倾的颈椎、渐厚的镜片,还有熬夜留下的黑眼圈......




  4. 我们常常抽时间刷微博、看热搜和刷短视频,但却难以每天保持锻炼身体、系统学习和读几页书,以及坚持做一些微小的事情去发现自己的兴趣爱好。



    智能手机的发展,让我们变的浮躁,静不下来,似乎也停不下来 🧐


    这是一个互联网高度发达的时代,一个被算法支配的时代,一个缺少自我主动支配时间的时代,智能手机的发展给我们的生活带来了翻天覆地的变化,方便了出行、支付、交流等等,在带来极大便利的同时,我们也付出了代价,好多时候我感觉自己就是一副没有灵魂的躯壳。



    行尸走肉.png




  5. 有些社区文章和公众号文章的标题,短短几个字,让人莫名产生焦虑感。




    • 最新大厂热乎面经,已拿offer




    • 前某大厂某高级别大牛带你xxxx




    • 大厂是怎么xxx




    • 都202x年了,你还不会xxx





    曾几何时,看到这种标题,我抱着学习提升跟上时代的想法,点击进去,发现好多是广告,卖课的,要么就是一些同质化的内容,无语翻倍...


    这是最好的时代,也是最坏的时代,好多人靠贩卖焦虑就能赚大钱,新闻自媒体靠一个标题就能吸引流量


    不知道什么时候想通了,承认自己是个普通人,工作只是我们活着的一种手段,每天过的开不开心,家庭幸不幸福和在哪上班收入多少有啥关系?


    该吃吃,该喝喝,有事儿别往心里搁,哟!今天天气真不错,开心 😁





  6. 在疫情时代,看着工作机会减少了,生意不好做了,时代在变化,疫情时代干掉了一些工作,但是疫情核酸检测员,核酸检查站多了,保安也多了,疫情相关产业链上的工作岗位增多了。




  7. ChatGPT、AIGC 爆火的这段时间,好多文章以及社区之类的,大家都在讨论 AI,这时候各种 AI 峰会,线下交流会哪哪都爆满,AI 相关的培训也热的不行,仿佛搭不上 AI 这趟车,我们就会被马上淘汰一样。



    任何一个时代的变革,淘汰一些工作的同时,催生了大量新的岗位,跟上时代,时刻 "刷新" 自己,我已经想好了,什么时候 AI 能完全替代我这个前端工程师了,那时候我也不敲代码了,解脱!


    AI 快来解放我吧 😜





总的来看,我的焦虑感是由环境因素和心理因素共同作用的结果


既然知道了焦虑感产生的原因,也就相对好解决了,毕竟只是尽量减少自己的焦虑感


我尝试做了这些有趣而正向的东西,发现效果还不错



  1. 例如有时会用碎片时间记个单词,dogsbody n.打工人;杂役 累的像狗,身体被掏空的就是打工人了, 是不是一下子就记住了呢?


截屏2023-06-15 13.44.32.png



  1. 每周抽时间运动,目前是每周末早上去跑步,一般5-8公里。偶尔也在公司的健身区举举铁。个人比较喜欢跑步,跑到一定公里数的时候,心脏加快跳动,血液流动更畅通,还有夏天室外奔跑汗流浃背的感觉。感觉整个人都更轻松了,心情也会变好。我愿意坚持运动,享受运动过程中的愉悦感和得到放松的身体。


来看看我今年的跑步数据:


2.png



  1. 无意间发现了一款乐器 -- 非洲手鼓,当时被这个手鼓颜值给吸引了,然后就果断入手一只。目前可以跟着《桥边姑娘》的节奏拍下来, 暖阳下 我迎芬芳 是谁家的姑娘,我走在了那座小桥上 你抚琴奏忧伤…… ,是不是有画面了,我跟着节奏去敲手鼓的时候,使我沉浸在这个音乐中,这首音乐有了参与感,对音乐有了更近一步的了解。


feizhoushougu




  1. 在家的时候,如果没什么事情,我会把手机“藏”起来,让自己不那么容易看到手机,有空就看会书或者望着窗外发呆,刚看完村上春树的《当我谈跑步时我谈些什么》,看书的过程会感到很平静




  2. 地铁路上由于通勤时间太久,有时候会闭目养神,尽量让自己少看手机,想一下今天有什么工作没完成,或者计划做点什么事情,让大脑活跃一下。我还把手机上总是发广告的那种公众号,总贩卖焦虑的那些自媒体取关了,不管他(她)是什么行业大佬,还是什么专业XXX,尝试远离他(她)们,发现世界慢慢变的更清净了





最最重要的——坚持,真正值得做的,绝对绝对不会轻而易举


最后,祝每位看官都能活出自己想要的样子 ^-^


作者:草帽lufei
来源:juejin.cn/post/7244748771061219385

收起阅读 »

你的优缺点怎么说?

前言 哈咯,大家好,我是沐华。关于优缺点,我想大家肯定有遇到过:在面试前填的表中有这个问题,或者面试过程中被问到这个问题,还有的人还会在自己的简历自评上写上自己的优点。不知道这个问题的答案,你是怎么写的呢? 我的优点是实战经验比较丰富,技术能力比较强,比如我一...
继续阅读 »

前言


哈咯,大家好,我是沐华。关于优缺点,我想大家肯定有遇到过:在面试前填的表中有这个问题,或者面试过程中被问到这个问题,还有的人还会在自己的简历自评上写上自己的优点。不知道这个问题的答案,你是怎么写的呢?


我的优点是实战经验比较丰富,技术能力比较强,比如我一个人能干三个人的活,而且只收两份钱


缺点是比较喜欢吹牛比,比如上一条,嘿嘿


这个问题重点无非就是想看你是否真诚自信、是否有一个清楚的自我认知、以及一些沟通表达能力


废话不说了,上答案


你觉得你的缺点/劣势是什么?


说缺点,有几个需要注意的点:不能是会伤害到公司或工作的缺点、不能和人家已经提出来的要求相悖,比如人家 jd 上说了要求能加班/抗压能力强,你说你缺点是不能加班/抗压能力不行,那就不行


小技巧是:说缺点的时候可以有些吞吞吐吐的,不会显得那么理直气壮 理所当然,最好加上应对办法或咱们已经采取的行动,比如:




  • 我缺点倒是有一堆,比如对项目全局把控能力不够啊,开始项目不大的时候很多地方自己能设想得到,到后面迭代越做越大的时候,才发现前面做的有些东西就不行了没考虑到,前段时间买了一些架构方面的课程,现在也是在学习中




  • 我会有些较真,有时候跟同事讨论问题,意见不一致时就总忍不住用自己的想法去说服别人,有时候就导致同事不太开心,毕竟一个在一起工作,友好合作才是最重要的,应该控制自己的情绪,摆正心态的,讨论问题应该是各抒已见,畅所欲言的嘛,好在自己意识到这个问题,现在也是在努力改正中




  • 我不太爱说话(上班社恐,下班社牛),表述能力不够吧,因为那些年一个人惯了,一个人吃一个人睡,一个人想念一个人醉,一个人苦一个人累,一个人悲欢一个人体会




  • 可能我不大会拒绝我同事的要求,就是在职场关系处理上,不太会拒绝同事的要求我觉得这个是一个缺点吧,有次就因为帮同事处理问题,结果自己负责的需求耽误了进度,结果被领导说,毕竟自己还有本职工作,还是应该要先做完自己手上的工作,再去帮别人的




  • 我比较容易焦虑,所以我会每件事情之前制定一个计划,规划好每一步,确保充分考虑和准备,尽量不去出现意外状况啥啥的




那我缺点说些和职场无关的行不行?


比如:我的缺点是做饭水平差一些,成品都是这样的,





平时倒还好,最得劲的是拿来招待客人的时候,那是把家里所有房间都改成卫生间都不够用啊,而且客人越多时上菜还越快,都不知道我是怎么做到的,你说难受不。而且除了做饭水平差点之外,我的缺点还有挑食、我胆子小、我怕黑、我恐高...啥啥的


这是不是不太合适?


你觉得你的优点/优势是什么?


优点嘛,或者问业余爱好,这不就是上才艺的时刻嘛,包括咱人生中的高光时刻都可以拿来说


小技巧是:最好是加一些事例哈,而且说的时候这个就不能吞吞吐吐的了,要自信点,最好是拿出装比时的那种气势,然后就像拿捏猫的后颈肉那样,拿捏住,别松了就行。比如:




  • 我学习能力强上手快、我执行力强,(事例:)比如上次领导说后面准备升级 Vue3,给我们两个月时间学习,结果我花了一个月不光学完了 Vue3,而且还自己用 Vue3 开发了一个新系统小项目,然后领导很高兴,还帮我申请了个 500 块钱奖励




  • 我喜欢钻研技术吧,可能还有点强迫症吧,(事例:)有时候遇到问题,就算半夜搞到很晚也要把问题解决掉,比如上次上周我遇到个带 plus 的问题,搞了个通宵愣是解决了,不然要是没解决都睡不着觉




  • 我很会做 PPT,(事例:)每次年中述职、年终述职的时候,同事们都是等我做完了,然后再拿我的去参考,或者让我帮忙看怎么修改。还有会议、有同事转正述职的时候之类的等等所有会用到 PPT 的时候我就成了香饽饽




  • 我喜欢做笔记记录、我善于总结、善于分享,(事例:)并且有在一些技术博客和公众号上发一些文章,其实很多都是自己平时学习中或工作中遇到的




  • 我移动端经验丰富、有全栈开发经验、有管理带人经验、有过超大型项目经验(大厂背景)、有各种证书、很会处理人际关系左右逢源,等等这些都可以




优点不比缺点,说一些与职场无关的,是完全可以的,比如:



  • 我很会做菜,不仅做菜速度快,而且摆盘还好看,(事例:)我家里面一直都是我做饭的,有时候做菜发个朋友圈,或者发个掘金,都有好多人点赞,比如正在看文章的你


最后


如果你还是有些懵懵懂懂的,再给你个小技巧,就是用反义词:



  • 把优点反过来说,就是缺点了

  • 把缺点反过来说,就是优点了


谢谢你宝贵的时间,如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【】都是我创作的最大动力 ^_^


更多前端文章,或者加入前端交流群,欢迎关注公众号【沐华说技术】,大家一起共同交流和进步呀


往期精彩


【保姆级】Vue3 开发文档


【由浅到深】聊聊 Vue 和 React 的区别,看看你在哪个段位


TS 泛型进阶


深入浅出虚拟 DOM 和 Diff 算法源码,及 Vue2 与 Vue3 中的区别


上手 Vue 新的状态管理 Pinia,一篇文章就够了


作者:沐华
来源:juejin.cn/post/7244761504560595002
收起阅读 »

这份简历,大学生看了要落泪

本文首发自公粽hao「林行学长」,欢迎来撩,免费领取20个求职工具资源包。 了解校招、分享校招知识的学长来了! 今天学长被一份小学生简历震惊到了。 不仅仅是小学生已经开始涉猎简历这回事,更是小学生的简历内容震撼到我了。 就是感叹,现在卷到小学了吗! 当大学生...
继续阅读 »

本文首发自公粽hao「林行学长」,欢迎来撩,免费领取20个求职工具资源包。


了解校招、分享校招知识的学长来了!


今天学长被一份小学生简历震惊到了。


不仅仅是小学生已经开始涉猎简历这回事,更是小学生的简历内容震撼到我了。



就是感叹,现在卷到小学了吗!


当大学生还在为四六级烦恼,一些小学生已经开始接触托福雅思,这学习水平,简直就是“满级人类”啊!


当然,咱们普通人只能看看,有的内容也许咱们根本不用呢(酸言酸语)


不过,简历这回事,还得是大人们的专属烦恼。



毕竟,小学生又不用找工作……


看完了小学生的简历,咱们来说说怎么写才不会把自己的简历写成真·小学生简历。


01 能力跟后面的“举例”要匹配。


大家都很喜欢说“协作能力”,那么协作能力到底是什么呢?


协作能力是指个人或团队在共同完成某项任务时,能够有效地进行沟通、合作和协调的能力。


不要简单地把所有团队合作都当做是“协作能力”的体现。



如果后面体现的明明创意能力,那么跟前面的“协作能力”的表述不一致,就会显得不够专业。


有时候换一种表述方法,也能给自己更多的亮点和竞争力。


02 顺序很重要


不同的工作,有不同的核心。


例如,销售工作中应该用销售额说话,直接把销售成果放在第一条更加抢眼,也更能体现自己的能力。


顺序的排放除了要跟自己相应的工作相匹配之外,还应该跟自己想要申请的职位相匹配。



问问自己:我想申请的职位看重什么能力?需要什么能力?我有什么经历可以体现这样的能力?


然后把这样的能力突出给 HR 看,这样才能事半功倍。


03 词语选择要准确。


动词选择要有力,要实质性;形容词选择不要“感性”,要“理性”。


开头要用动词,动词要能体现你的主动性和积极性,例如:“组织”就比“协助”好。


从形容词来看,感性的、判断性的词不要用,因为主观词语会让人觉得你不太专业。


你所能做的,是要用事实和数字来证明自己,自己的工作“有效”,然后让 HR 自己来判断。


04 拒绝抽象,拒绝小学生作文,用数字说话。


这是老生常谈了,不少同学简历语言一不留神就会不够职业化,显得非常像“小学生作文”。



简历上用数字说话,用结果说话,用事实说话。去证明,而不是去告诉。

作者:林行学长
来源:juejin.cn/post/7244099508212383805

收起阅读 »

面试:(简单粗暴点)百度一面,直接问痛我

web
前言 这次的百度面试挺紧张的,在写算法题的时候脑子都有点空白,还是按照脑海中那点残存的算法技巧才写出来,不至于太尴尬,以及第一次面试百度这种级别的公司,难免出现了一些平常不至于出现的问题或没注意的缺点,在这里分享给大家。 百度一面 1. 如何用chatgpt提...
继续阅读 »

前言


这次的百度面试挺紧张的,在写算法题的时候脑子都有点空白,还是按照脑海中那点残存的算法技巧才写出来,不至于太尴尬,以及第一次面试百度这种级别的公司,难免出现了一些平常不至于出现的问题或没注意的缺点,在这里分享给大家。


百度一面


1. 如何用chatgpt提升前端开发效率



因为我嘴贱,平时习惯了使用chatgpt,然后自我介绍说了一句,由于之前面得公司都没问过,导致我没怎么往这方面准备,以至于答得时候牛头不对马嘴,所以说不愧是大厂啊。




  1. 问题解答和指导ChatGPT可以帮助回答与前端开发相关的问题。当你在编写代码的时候,当一时忘记了某个API怎么用,就可以向ChatGPT提问,并获得解答和指导,甚至还会给出一些更加深入且性能更好的应用。这可以帮助更快地解决问题和理解前端开发中的概念。

  2. 代码片段和示例ChatGPT可以帮助你生成常见的前端代码片段和示例。你可以描述你想要实现的功能或解决的问题,然后向ChatGPT请求相关代码片段。这样,您可以更快地获得一些基础代码,从而加快开发速度。

  3. 自动生成文档ChatGPT可以帮助你生成前端代码的文档。你可以描述一个函数、组件或类,并向ChatGPT请求生成相关的文档注释。这可以帮助您更轻松地为你的代码添加文档,提高代码的可读性和可维护性。

  4. 问题排查和调试:在开发过程中,您可能会遇到问题或错误。您可以向ChatGPT描述您遇到的问题,或者直接把代码交给它,并请求帮助进行排查和调试。ChatGPT可以提供一些建议和指导,帮助您更快地找到问题的根本原因并解决它们。

  5. 学习资源和最新信息ChatGPT可以为你提供关于前端开发的学习资源和最新信息。你可以向ChatGPT询问关于前端开发的最佳实践、最新的框架或库、前端设计原则等方面的问题。这可以帮助我们不断学习和更新自己的前端开发知识,从而提高效率。


2. [1, 2, 3, 4, 5, 6, 7, 8, 9] => [[1, 2, 3],[4, 5, 6],[7, 8, 9]],把一个一维数组变成三个三个的二维数组


在JavaScript中,可以使用数组的slice方法和一个循环来将一个一维数组转换为一个二维数组。下面是一个示例代码:


    function convertTo2DArray(arr, chunkSize) {
var result = [];
for (var i = 0; i < arr.length; i += chunkSize) {
result.push(arr.slice(i, i + chunkSize));
}
return result;
}

var inputArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var outputArray = convertTo2DArray(inputArray, 3);

console.log(outputArray);

输出结果将是:


    [[1, 2, 3], [4, 5, 6], [7, 8, 9]]


slice 不会修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组,不信的话自己可以编译一下。



这段代码中的convertTo2DArray函数接受两个参数:arr表示输入的一维数组,chunkSize表示每个子数组的大小。它使用slice方法来从输入数组中提取每个子数组,并使用循环来遍历整个数组并构建输出二维数组。最后,它返回生成的二维数组。


3. 输出结果,为什么?


    const obj3 = {a: 1};
const obj4 = {b: 2};
console.log(obj3 == obj4); // false
console.log(obj3 === obj4); // false

结果:


false,false


原因:


在这段代码中,obj3obj4分别是两个独立的对象,它们开辟的堆内存地址是完全不一样。==运算符用于比较两个操作数是否相等,而===运算符用于比较两个操作数是否严格相等。


根据对象的比较规则,当使用==运算符比较两个对象时,它们将会进行类型转换后再进行比较。由于obj3obj4是不同的对象,即使它们的属性值相同,它们的引用也不同,因此在进行类型转换后,它们会被视为不相等的对象。因此,console.log(obj3 == obj4);的输出结果将会是false


而在使用===运算符比较两个对象时,不会进行类型转换,而是直接比较两个操作数的值和类型是否完全相同。由于obj3obj4是不同的对象,且类型也不同,即使它们的属性值相同,它们也不会被视为严格相等的对象。因此,console.log(obj3 === obj4);的输出结果同样会是false



总结起来,无论是使用==运算符还是===运算符,obj3obj4都不会被视为相等或严格相等的对象,因为它们是不同的对象。



4. this有关 输出结果,为什么?


    const obj1 = {
fn: () => {
  return this
}
}
const obj2 = {
fn: function(){
  return this
}
}

console.log(obj1.fn());
console.log(obj2.fn());

输出结果:



  1. window || undefined

  2. obj2


原因是:


在箭头函数 fn 中的 this 关键字指向的是定义该函数的上下文,而不是调用该函数的对象。因此,当 obj1.fn() 被调用时,由于箭头函数没有它自己的this,当你调用fn()函数时,this指向会向上寻找,因此箭头函数中的 this 指向的是全局对象(在浏览器环境下通常是 window 对象),因此返回的是 undefined


而在普通函数 fn 中的 this 关键字指向的是调用该函数的对象。在 obj2.fn() 中,函数 fn 是作为 obj2 的方法被调用的,所以其中的 this 指向的是 obj2 对象本身,因此返回的是 obj2


需要注意的是,在严格模式下,普通函数中的 this 也会变为 undefined,因此即使是 obj2.fn() 也会返回 undefined。但在示例中没有明确指定使用严格模式,所以默认情况下运行在非严格模式下。


5. Promise有关输出结果,为什么?


    console.log('1');
function promiseFn() {
return new Promise((resolve, reject) => {
  setTimeout(()=> {
    console.log('2');
  })
  resolve('3');
  console.log('4')
})
}

promiseFn().then(res => {
console.log(res);
});

输出结果: 1 4 3 2


原因是:



  1. 首先,代码从上往下执行,把console.log('1')放入同步任务

  2. 再调用promiseFn(),因为new Promise是同步任务,所以放入同步任务,继续执行

  3. 遇到setTimout这个宏任务,放入宏任务队列中

  4. 遇到resolve('3'),把res返回

  5. 之后再执行.then(),因为promise.then是微任务,所以放入微任务队列

  6. 代码是先执行同步任务,再执行微任务,之后再是宏任务

  7. 所以输出结果为1 4 3 2



这里涉及到了EventLoop的执行机制,如果不是太清楚可以看看我的面试题:小男孩毕业之初次面试第二家公司第一题



6. 实现斐波那契的第N个值(从0开始),要求时间复杂度为O(n)



首先,说到斐波那契第一个想到的肯定是如下的算法,但这可是百度啊,如果只是这种程度的话如何能和同样面相同岗位的人竞争呢,所以我们得想到如下算法有什么缺点,然后如何优化



function fib(n) {
if (n == 0 || n === 1) return 1;
return fib(n - 1) + fib(n - 2);
};

console.log(fib(3)); // 5
console.log(fib(5)); // 8

单纯的使用递归看似没什么问题,也能运算出结果,但是里面有个致命的问题,首先,时间复杂度就不对,递归思想的复杂度为 O(2^n) ,它不为O(n),然后还有会重复计算,比如计算n=3时,会计算fib(1) + fib(2),再次计算fib(4)时,会先算fib(3) = fib(1) + fib(2),然后再计算fib(4) = fib(1) + fib(2) + fib(3),在这里,fib(1)和fib(2)重复计算了两次,对于性能损耗极大。此时的你如果对动态规划敏感的话,就会从中想到动态规划其中最关键的特征——重叠子问题



因此,使用动态规划来规避重复计算问题,算是比较容易想到较优的一种解法,并且向面试官展现了你算法能力中有动态规划的思想,对于在面试中的你加分是极大的。



以下是动态规划思路的算法,状态转移方程为dp[i] = dp[i-1] + dp[i-2]


function fibonacci(n) { 
if (n <= 1) return n;
let fib = [0, 1]; // 保存斐波那契数列的结果
for (let i = 2; i <= n; i++) {
fib[i] = fib[i - 1] + fib[i - 2]; // 计算第i个斐波那契数
}
return fib[n];
}



当然,你可能会说,在面试中怎么可能一下子就能想到动态规划,所以在面试前你需要背一背相关的状态转移方程,当你对算法问题分析到一定程度时,就能够记忆起这些状态转移方程,提高你写算法的速度。



在面试中,动态规划的常用状态转移方程可以根据问题的具体情况有所不同。以下是几个常见的动态规划问题和它们对应的状态转移方程示例:




  1. 斐波那契数列(Fibonacci Sequence):



    • dp[i] = dp[i-1] + dp[i-2],其中 dp[i] 表示第 i 个斐波那契数。




  2. 爬楼梯问题(Climbing Stairs):



    • dp[i] = dp[i-1] + dp[i-2],其中 dp[i] 表示爬到第 i 级楼梯的方法数。




  3. 背包问题(Knapsack Problem):



    • dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]),其中 dp[i][j] 表示在前 i 个物品中选择总重量不超过 j 的最大价值,weight[i] 表示第 i 个物品的重量,value[i] 表示第 i 个物品的价值。




  4. 最长递增子序列(Longest Increasing Subsequence):



    • dp[i] = max(dp[j] + 1, dp[i]),其中 dp[i] 表示以第 i 个元素结尾的最长递增子序列的长度,j0i-1 的索引,且 nums[i] > nums[j]




  5. 最大子数组和(Maximum Subarray Sum):



    • dp[i] = max(nums[i], nums[i] + dp[i-1]),其中 dp[i] 表示以第 i 个元素结尾的最大子数组和。




  6. 最长公共子序列(Longest Common Subsequence):




    • 如果 str1[i] 等于 str2[j],则 dp[i][j] = dp[i-1][j-1] + 1




    • 否则,dp[i][j] = max(dp[i-1][j], dp[i][j-1]),其中 dp[i][j] 表示 str1 的前 i 个字符和 str2 的前 j 个字符的最长公共子序列的长度。






  7. 编辑距离(Edit Distance):




    • 如果 word1[i] 等于 word2[j],则 dp[i][j] = dp[i-1][j-1]




    • 否则,dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1,其中 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作次数。






  8. 打家劫舍(House Robber):



    • dp[i] = max(dp[i-1], dp[i-2] + nums[i]),其中 dp[i] 表示前 i 个房屋能够获得的最大金额,nums[i] 表示第 i 个房屋中的金额。




  9. 最大正方形(Maximal Square):




    • 如果 matrix[i][j] 等于 1,则 dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1




    • 否则,dp[i][j] = 0,其中 dp[i][j] 表示以 matrix[i][j] 为右下角的最大正方形的边长。






7. 手写EventBus


当需要手动实现一个简单的 EventBus 时,你可以创建一个全局的事件总线对象,并在该对象上定义事件的订阅和发布方法。


class EventBus {
constructor() {
this.events = {}; // 存储事件及其对应的回调函数列表
}

// 订阅事件
subscribe(eventName, callback) {
this.events[eventName] = this.events[eventName] || []; // 如果事件不存在,创建一个空的回调函数列表
this.events[eventName].push(callback); // 将回调函数添加到事件的回调函数列表中
}

// 发布事件
publish(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
callback(data); // 执行回调函数,并传递数据作为参数
});
}
}

// 取消订阅事件
unsubscribe(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); // 过滤掉要取消的回调函数
}
}
}

使用上述 EventBus 类,你可以执行以下操作:


// 创建全局事件总线对象
const eventBus = new EventBus();

const callback1 = data => {
console.log('Callback 1:', data);
};

const callback2 = data => {
console.log('Callback 2:', data);
};

// 订阅事件
eventBus.subscribe('event1', callback1);
eventBus.subscribe('event1', callback2);

// 发布事件
eventBus.publish('event1', 'Hello, world!');

// 输出:
// Callback 1: Hello, world!
// Callback 2: Hello, world!

// 取消订阅事件
eventBus.unsubscribe('event1', callback1);

// 发布事件
eventBus.publish('event1', 'Goodbye!');

// 输出:
// Callback 2: Goodbye!

在上述示例中,我们创建了一个 EventBus 类,该类具有 subscribepublishunsubscribe 方法。subscribe 方法用于订阅事件,publish 方法用于发布事件并触发相关的回调函数,unsubscribe 方法用于取消订阅事件。我们使用全局的 eventBus 对象来执行订阅和发布操作。


这个简单的 EventBus 实现允许你在不同的组件或模块之间发布和订阅事件,以实现跨组件的事件通信和数据传递。你可以根据需要对 EventBus 类进行扩展,添加更多的功能,如命名空间、一次订阅多个事件等。



当问到EventBus时,得预防面试官问到EvnetEmitter,不过当我在网上查找相关的资料时,发现很多人似乎都搞混了这两个概念,虽然我在这里的手写原理似乎也差不多,但在实际使用中,两者可能在细节上有所不同。因此,在具体场景中,你仍然需要根据需求和所选用的实现来查看相关文档或源码,以了解它们的具体实现和用法。



下面是一个简单的 EventEmitter 类实现的基本示例:


class EventEmitter {
constructor() {
this.events = {}; // 用于存储事件及其对应的回调函数列表
}

// 订阅事件
on(eventName, callback) {
this.events[eventName] = this.events[eventName] || []; // 如果事件不存在,创建一个空的回调函数列表
this.events[eventName].push(callback); // 将回调函数添加到事件的回调函数列表中
}

// 发布事件
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
callback(data); // 执行回调函数,并传递数据作为参数
});
}
}

// 取消订阅事件
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); // 过滤掉要取消的回调函数
}
}

// 添加一次性的事件监听器
once(eventName, callback) {
const onceCallback = data => {
callback(data); // 执行回调函数
this.off(eventName, onceCallback); // 在执行后取消订阅该事件
};
this.on(eventName, onceCallback);
}
}

使用上述 EventEmitter 类,你可以执行以下操作:


    const emitter = new EventEmitter();

const callback1 = data => {
console.log('Callback 1:', data);
};

const callback2 = data => {
console.log('Callback 2:', data);
};

// 添加一次性事件监听器
const onceCallback = data => {
console.log('Once Callback:', data);
};

// 订阅事件
emitter.on('event1', callback1);
emitter.on('event1', callback2);
emitter.once('event1', onceCallback);

// 发布事件
emitter.emit('event1', 'Hello, world!');

// 输出:
// Callback 1: Hello, world!
// Callback 2: Hello, world!
// Once Callback: Hello, world!

// 取消订阅事件
emitter.off('event1', callback1);

// 发布事件
emitter.emit('event1', 'Goodbye!');

// 输出:
// Callback 2: Goodbye!

在上述示例中,EventEmitter 类具有 onemitoffonce 方法。on 方法用于订阅事件,emit 方法用于发布事件并触发相关的回调函数,off 方法用于取消订阅事件,once方法用于添加一次性的事件监听器。你可以根据需求对 EventEmitter 类进行扩展,添加更多的功能,比如一次订阅多个事件、取消所有事件订阅等。


eventBus,eventEmitter的区别


EventBusEventEmitter 都是用于实现事件发布-订阅模式的工具,但它们在实现和使用上有一些区别。




  1. 实现方式:



    • EventBusEventBus 是一个全局的事件总线,通常是作为一个单例对象存在,用于在不同组件或模块之间传递事件和数据。在 Vue.js 中,Vue 实例可以充当 EventBus 的角色。

    • EventEmitterEventEmitter 是一个基于类的模块,通常是作为一个实例对象存在,用于在单个组件或模块内部实现事件的发布和订阅。




  2. 使用范围:



    • EventBusEventBus 的作用范围更广泛,可以跨越不同组件、模块或文件进行事件的发布和订阅。它可以实现多个组件之间的通信和数据传递。

    • EventEmitterEventEmitter 主要用于单个组件或模块内部,用于实现内部事件的处理和通信。




  3. 依赖关系:



    • EventBusEventBus 通常需要一个中央管理的实例,因此需要在应用程序的某个地方进行创建和管理。在 Vue.js 中,Vue 实例可以用作全局的 EventBus

    • EventEmitterEventEmitter 可以在需要的地方创建实例对象,并将其用于内部事件的发布和订阅。




  4. 命名空间:



    • EventBusEventBus 可以使用不同的事件名称来进行事件的区分和分类,可以使用命名空间来标识不同类型的事件。

    • EventEmitterEventEmitter 通常使用字符串作为事件的名称,没有直接支持命名空间的概念。





总结起来,EventBus 主要用于实现跨组件或模块的事件通信和数据传递,适用于大型应用程序;而 EventEmitter 主要用于组件或模块内部的事件处理和通信,适用于小型应用程序或组件级别的事件管理。选择使用哪种工具取决于你的具体需求和应用场景。



8. (场景题)在浏览器中一天只能弹出一个弹窗,如何实现,说一下你的思路?


要在浏览器中实现一天只能弹出一个弹窗的功能,可以使用本地存储(localStorage)来记录弹窗状态。下面是一种实现方案:



  1. 当页面加载时,检查本地存储中是否已存在弹窗状态的标记。

  2. 如果标记不存在或者标记表示上一次弹窗是在前一天,则显示弹窗并更新本地存储中的标记为当前日期。

  3. 如果标记存在且表示上一次弹窗是在当天,则不显示弹窗。


以下是示例代码:


    // 检查弹窗状态的函数
function checkPopupStatus() {
// 获取当前日期
const currentDate = new Date().toDateString();

// 从本地存储中获取弹窗状态标记
const popupStatus = localStorage.getItem('popupStatus');

// 如果标记不存在或者标记表示上一次弹窗是在前一天
if (!popupStatus || popupStatus !== currentDate) {
// 显示弹窗
displayPopup();

// 更新本地存储中的标记为当前日期
localStorage.setItem('popupStatus', currentDate);
}
}

// 显示弹窗的函数
function displayPopup() {
// 在这里编写显示弹窗的逻辑,可以是通过修改 DOM 元素显示弹窗,或者调用自定义的弹窗组件等
console.log('弹出弹窗');
}

// 在页面加载时调用检查弹窗状态的函数
checkPopupStatus();

在这个实现中,checkPopupStatus 函数会在页面加载时被调用。它首先获取当前日期,并从本地存储中获取弹窗状态的标记。如果标记不存在或者表示上一次弹窗是在前一天,就会调用 displayPopup 函数显示弹窗,并更新本地存储中的标记为当前日期。


通过这种方式,就可以确保在同一天只能弹出一个弹窗,而在后续的页面加载中不会重复弹窗。


9. 项目中的性能优化?




  1. 对组件和图片进行懒加载对暂时未使用的组件和图片使用懒加载可以显著地减少页面加载时间,比如在我的项目中路由配置中除了需要频繁切换的页面组件外,其他的组件都使用箭头函数引入组件进行懒加载,以及一些没有展现在界面的图片也进行了一个VueLazy的懒加载。




  2. 减少HTTP请求数量由于频繁的请求会对后端服务器造成极大的负担,所以应该减少不必要的请求,比如在我的项目中的搜索界面,对于搜索按钮增加了防抖功能




  3. 使用缓存使用浏览器缓存可以减少资源请求,从而提高页面加载速度。项目中我会把用户的一些需要持久化的信息存入本地存储。




  4. 异步请求使用Promise.all:异步请求可以在后台加载资源,从而避免阻塞页面加载。在请求数据时,我会使用Promise.all一次性并行的请求类似的数据,而不需要一个一个的请求,较少了请求时间。




  5. 图片优化使用适当的图片格式和大小可以减少页面的资源请求和加载时间,项目中我会把图片转化成base64的格式和webp格式,这样可以使图片大小更小




  6. 使用CDN加速:使用CDN可以提高资源的访问速度,从而加快页面加载速度。我项目中的一些第三方资源有时需要请求,因此我会使用CDN内容分发网络来提高访问速度。




  7. 骨架屏(Skeleton Screen):它可以提升用户感知的加载速度和用户体验。虽然骨架屏本身并不直接影响代码性能,但它可以改善用户对应用程序的感知,提供更好的用户体验。




10. 项目中遇到的难点,如何解决


1. 数据状态管理


前端登录状态管理



  • 我在一个练手的项目中做前端登录功能的时候, 碰到了购物车需要登录判断的功能,比如用isLogin来判断有没有登录,当时由于没有深入了解vuex,所以我一开始想着把这个isLogin通过组件与组件的传值方法,把这个值传给相应的组件,然后在需要登录组件中进行判断,但后来发现这个方法太麻烦了

  • 后来通过学习了解,使用了vuex这个全局状态管理的方法, 通过使用createStore这个vuex中的API创建了一个全局的登录状态,再通过actions mutations实现登录判断和登录状态共享


组件数据状态管理



  • 我项目中一开始首页、详情页等其他页面越来越多的状态放在同一个store上,虽然感觉有点乱,但实现了数据流和组件开发的分离,使得我更能够专注于数据的管理

  • 但随着数据的增多,感觉实在太乱了,然后得知vuex中可以使用 modules 来进行分模块,相应的页面放入相应的模块状态中,之后再用actions,mutations,state,getters这四件套, 更好的模块化管理数据,能够知道哪些状态是全局共享的(登录), 哪些状态是模块共享的

  • 然后在新的项目中,也就是现在简历上的项目里,尝试使用pinia来管理,因为我发现它更简单(没有mutations),模块化更好,让我对组件状态管理的更加得心应手,学习起来也更加的方便。


node的错误处理



  • 一开始用node写后端的时候,一堆错误,比如路由没配置,数据库报错。使得后面的代码都无法运行,写着写着就感觉写不下去,经常一个错误就需要反复的在脑海中想最后依靠那一丝的灵光一闪才解决

  • 之后我就在app.js这个后端入口文件的最后,添加一个统一的错误处理的中间件,向前端返回状态码和相应的信息后,直接使用next()向后继续执行,这样虽然服务器报了错,但仍然可以执行后续的代码。


跨域问题



  • 在我写完前端项目的时候,想要提升一下自己,就转去学习了Koa,在搭建了大致的服务器,写了一个简单的接口并运行服务器后,我想当然的就在前端直接请求后端的端口,结果报了一个跨域的错误,由于当时初学后端,不怎么了解跨域,所以找了很多的解答并逐个在项目中进行尝试,比如跨域中的scriptpostMessagehtml本身的Websocket

  • 但发现最实用的还是在服务器中配置Access-Control-Allow-Origin来控制跨域请求的url地址,以及其他一些Access-Control-Allow头来控制跨域请求方法等,然后跨域请求url的白名单我放入了.env这个全局环境变量中。


axios响应拦截



  • 在后端返回数据的时候,我返回数据有一个状态码以及添加到data这个需要返回的数据(代码如下),这导致我在获取接口里的数据时需要多.data(引用一层data),当时我没意识到,结果一直获取不到数据。之后输出获取的数据才发现在数据外面包了一层,虽然这个时候解决了服务器那边数据返回的问题,但后面每次获取数据时都需要在往里再获取,非常的麻烦。

  • 最后在学习了并在项目中使用axios进行请求和响应后,就在响应的时候设置一个拦截器,对响应进行一番处理之后就可以直接拿到后端接口返回的值,而不会导致接口返回的值不会有太多的嵌套了。


11. 如何学习前端的,学了几年?



这个就看个人情况了,但其中,你得展现出你的学习积极性和对前端的热爱,让面试官能够欣赏你



我大致说说我回答的,仅作参考


我从大二开始就对前端很感兴趣,当时正好学校也分了Web前端的方向,于是就跟着学校的课程开始学习基本的html,css,js三剑客,但之后感觉到老师教的很慢,就自己到B站上学习了,之后由于参加过一次蓝桥杯,就看到了蓝桥云课上有相关的基于html,css,js比较基础项目,接着我还学习了一些行内大牛写的一些博客文章,比如阮一峰,张鑫旭,廖雪峰等这些老师。之后又学习了vue并且在GitHub上学习相关的设计理念,根据GitHub上项目中不懂的东西又逐渐学习了各种UI组件库和数据请求方式,最后又学习了Nodejs中的Koa,用Vue和Koa仿写了一个全栈型项目,目前正在学习一些typescript的基本用法并尝试着运用到项目中,并在学习Vue的一些底层源码。


结语及吐槽


大厂的面试终归到底还是和我之前面的公司不一样,它们更加看重的是代码底层的实现和你的算法基础,终归到底,这次面试只是一次小尝试,想要知道自己的水平到底在哪里,并且能够借此完善自己的能力,努力的提升自己,希望能够给

作者:吃腻的奶油
来源:juejin.cn/post/7240751116701728805
大家带来一些正能量。

收起阅读 »

优化图片和视频的加载过程,提升用户体验

web
展示效果 (因为掘金不能上传视频,所以转成动图之后分辨率比较低,还望多包涵) 展示都是基于 Slow 3G 弱网下的效果。 优化前 这种体验交较差,在图片下载完之前,本应该展示图片的区域会长时间空白。 优化后 图片下载过程中显示模糊的图片占位符,直到图片下...
继续阅读 »

展示效果


(因为掘金不能上传视频,所以转成动图之后分辨率比较低,还望多包涵)


展示都是基于 Slow 3G 弱网下的效果。


优化前


before.gif


这种体验交较差,在图片下载完之前,本应该展示图片的区域会长时间空白。


优化后


eeeee.gif


图片下载过程中显示模糊的图片占位符,直到图片下载完成再切换展示。


原理


首先先贴出页面的代码 index.html:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<style>
html,
body {
margin: 0;
padding: 0;
}

@keyframes pulse {
0% {
opacity: 0;
}
50% {
opacity: 0.1;
}
100% {
opacity: 0;
}
}

.container {
width: 50vw;
background-repeat: no-repeat;
background-size: cover;
}

.container.loaded::before {
animation: none;
content: none;
}

.container::before {
content: '';
position: absolute;
inset: 0;
opacity: 0;
animation: pulse 2.5s infinite;
background-color: var(--text-color);
}

.container img,
.container video {
opacity: 0;
transition: opacity 250ms ease-in-out;
}

.container.loaded img,
.container.loaded video {
opacity: 1;
}
</style>
<body>
<!-- container容器加载一个体积非常小的低分辨率图片 -->
<div class="container" style="background-image: url(http://localhost:3000/uploads/10007/fox-small.jpeg);">
<!-- 图片延时加载 loading: lazy -->
<img
src="http://localhost:3000/uploads/10007/fox.jpeg"
loading="lazy"
style="width: 50vw"
/>

</div>

<br/>

<video
id="video"
autoplay
controls="controls"
style="width: 50vw"
poster="http://localhost:3000/uploads/10007/big_buck_bunny-small.png"
src="http://localhost:3000/uploads/10007/big_buck_bunny.mp4"
>
</video>
</body>
<script>
const blurredImageDiv = document.querySelector('.container');
const img = blurredImageDiv.querySelector('img');
function loaded() {
// 图片下载完之后 再展示
blurredImageDiv.classList.add('loaded');
}

if (img.complete) {
loaded();
} else {
img.addEventListener('load', loaded);
}

var poster = new Image();
poster.onload = function () {
// 加载完之后替换 poster url 不会重复请求
const video = document.querySelector('#video');
video.poster = 'http://localhost:3000/uploads/10007/big_buck_bunny.png';
};
poster.src = 'http://localhost:3000/uploads/10007/big_buck_bunny.png';
</script>
</html>

其实原理就是基于原图片生成出一个低分辨率体积非常小的图片(因为体积小,下载会很快),然后作为占位符显示,直到原图片完全下载之后再替换展示原图片。


那么如何生成一个超低分辨率的占位图片呢,可以使用 ffmpeg,需要本地提前安装,我是用的MacOS系统,所以直接通过 brew install ffmpeg 安装了。


如果是服务使用 Docker 部署的话,可参考:


FROM node:16 AS deps
WORKDIR /app
COPY . .
RUN wget https://www.johnvansickle.com/ffmpeg/old-releases/ffmpeg-4.4.1-arm64-static.tar.xz &&\
tar xvf ffmpeg-4.4.1-arm64-static.tar.xz &&\
mv ffmpeg-4.4.1-arm64-static/ffmpeg /usr/bin/ &&\
mv ffmpeg-4.4.1-arm64-static/ffprobe /usr/bin/
#RUN apt install -y ffmpeg
RUN yarn install
RUN yarn build
EXPOSE 3000
ENV PORT 3000
CMD [ "node", "dist/index.js" ]

ffmpeg -i sourcePath.jpg -vf scale=width:height outputPath.jpg
// 约束比例压缩
// width/height 为压缩之后图片的宽高 当其中一个值为 -1 的时候将保持原来的尺寸比例压缩

那么我们可以有如下命令:


ffmpeg -i sourcePath.jpg -vf scale=20:-1 outputPath.jpg
// 压缩之后生成 20 像素宽的图片用于做占位符展示

我们可以写个文件上传的服务,上传图片之后,服务端自动生成一个低分辨率的图片版本,然后将两者的地址url都返回过来。比如 Node 中我们可以使用 fluent-ffmpeg,那么以上命令就对应成代码:


import * as ffmpeg from 'fluent-ffmpeg';
import { FfmpegCommand } from 'fluent-ffmpeg';

export const runFfmpegCmd = (command: FfmpegCommand) =>
new Promise<void>((resolve, reject) => {
command
.on('error', (error) => {
reject(error);
})
.on('end', () => {
resolve();
})
.run();
});


public async uploadImage(user: User.UserInfo, file: Express.Multer.File) {
console.log(file);
const path = join(this.uploadPath, `${user.id}`, '/');

await ensureDir(path);

const { originalname, path: filePath } = file;
const finalPath = path + originalname;
const name = originalname.split('.');
const smallPath = path + name[0] + '-small.' + name[1];
console.log(smallPath);
await rename(filePath, finalPath);

// size 对应 scale=20:-1
await runFfmpegCmd(ffmpeg(finalPath).size('20x?').output(smallPath));

return {
statusCode: HttpStatus.OK,
data: {
path: finalPath,
smallPath,
},
};
}

public async uploadVideo(user: User.UserInfo, file: Express.Multer.File) {
console.log(file);
const path = join(this.uploadPath, `${user.id}`, '/');

await ensureDir(path);

const { originalname, path: filePath } = file;
const finalPath = path + originalname;
const name = originalname.split('.');
const shotName = name[0] + '.png';
const smallName = name[0] + '-small.png';

await rename(filePath, finalPath);

// 生成两个不同分辨率的缩略图
await Promise.all([
runScreenShotCmd(
ffmpeg(finalPath).screenshot({
count: 1,
filename: shotName,
folder: path,
}),
),
runScreenShotCmd(
ffmpeg(finalPath).screenshot({
count: 1,
filename: smallName,
folder: path,
size: '20x?',
}),
),
]);

return {
statusCode: HttpStatus.OK,
data: {
path: finalPath,
shotPath: path + shotName,
smallPath: path + smallName,
},
};
}

代码在自己的github上:im_server


自己本地的 swagger 界面的上传截图:


图片
image.png


视频
image.png


那么我们就可以得到一个超低分辨率的图片了,由于体积非常小,所以下载很快(特别是弱网情况下)。


补充


关于 img 标签的 lazy load 可参考:浏览器IMG图片原生懒加载loading=”lazy”实践指南


使用 imgsrcset 属性可实现根据不同屏幕分辨率加载不同尺寸的图片,进一步提升用户体验,而且没必要在小屏幕中加载超大分辨率的图片:响应式图片


结论


通过使用超低分辨率的占位符图片可以优化用户体验,特别是一些图片素材网站,再结合 img 标签的 loading="lazy"

作者:梦想很大很大
来源:juejin.cn/post/7244352006814679100
code> 懒加载。

收起阅读 »

记一次雪花算法遇到的 生产事故!

你好,我是悟空。 最近生产环境遇到一个问题: 现象:创建工单、订单等地方,全都创建数据失败。 初步排查:报错信息为duplicate key,意思是保存数据的时候,报主键 id 重复,而这些 id 都是由雪花算法生成的,按道理来说,雪花算法是生成分布式唯一 I...
继续阅读 »

你好,我是悟空。


最近生产环境遇到一个问题:


现象:创建工单、订单等地方,全都创建数据失败。


初步排查:报错信息为duplicate key,意思是保存数据的时候,报主键 id 重复,而这些 id 都是由雪花算法生成的,按道理来说,雪花算法是生成分布式唯一 ID,不应该生成重复的 ID。


大家可以先猜猜是什么原因。


有的同学可能对雪花算法不熟悉,这里做个简单的说明。


一、雪花算法


snowflake(雪花算法):Twitter 开源的分布式 id 生成算法,64 位的 long 型的 id,分为 4 部分:


snowflake 算法



  • 1 bit:不用,统一为 0

  • 41 bits:毫秒时间戳,可以表示 69 年的时间。

  • 10 bits:5 bits 代表机房 id,5 个 bits 代表机器 id。最多代表 32 个机房,每个机房最多代表 32 台机器。

  • 12 bits:同一毫秒内的 id,最多 4096 个不同 id,自增模式


优点:



  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。

  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。

  • 可以根据自身业务特性分配bit位,非常灵活。


缺点:



  • 强依赖机器时钟,如果机器上时钟回拨(可以搜索 2017 年闰秒 7:59:60),会导致发号重复或者服务会处于不可用状态。


看了上面的关于雪花算法的简短介绍,想必大家能猜出个一二了。


雪花算法和时间是强关联的,其中有 41 位是当前时间的时间戳,


二、排查


2.1 雪花算法有什么问题?


既然是雪花算法的问题,那我们就来看下雪花算法出了什么问题:


(1)What:雪花算法生成了重复的 ID,这些 ID 是什么样的?


(2)Why:雪花算法为什么生成了重复的 key


第一个问题,我们可以通过报错信息发现,这个重复的 ID 是 -1,这个就很奇怪了。一般雪花算法生成的唯一 ID 如下所示,我分别用二进制和十进制来表示:


十进制表示:2097167233578045440

二进制表示:0001 1101 0001 1010 1010 0010 0111 1100 1101 1000 0000 0010 0001 0000 0000 0000

找到项目中使用雪花算法的工具类,生成 ID 的时候有个判断逻辑:



当前时间小于上次的生成时间就会返回 -1,所以问题就出在这个逻辑上面。(有的雪花算法是直接抛异常)



if (timestamp < this.lastTimestamp) {
return -1;
}


由于每次 timestamp 都是小于 lastTimeStamp,所以每次都返回了 -1,这也解释了为什么生成了重复的 key。


2.2 时钟回拨或跳跃


那么问题就聚焦在为什么当前时间还会小于上次的生成时间


下面有种场景可能发生这种情况:


首先假定当前的北京时间是 9:00:00。另外上次生成 ID 的时候,服务器获取的时间 lastTimestamp=10:00:00,而现在服务器获取的当前时间 timestamp=09:00:00,这就相当于服务器之前是获取了一个未来时间,现在突然跳跃到当前时间。


而这种场景我们称之为时钟回拨时钟跳跃


时钟回拨:服务器时钟可能会因为各种原因发生不准,而网络中会提供 NTP 服务来做时间校准,因此在做校准的时候,服务器时钟就会发生时钟的跳跃或者回拨问题。


2.3 时钟同步


那么服务器为什么会发生时钟回拨或跳跃呢?



我们猜测是不是服务器上的时钟不同步后,又自动进行同步了,前后时间不一致。



首先我们的每台服务器上都安装了 ntpdate 软件,作为 NTP 客户端,会每隔 10 分钟NTP 时间服务器同步一次时间。


如下图所示,服务器 1 和 服务器 2 部署了应用服务,每隔 10 分钟向时间服务器同步一次时间,来保证服务器 1 和服务器 2 的时间和时间服务器的时间一致。



每隔 10 分钟同步的设置:


*/10 * * * * /usr/sbin/ntpdate <ip>

另外时间服务器会向 NTP Pool同步时间,NTP Pool 正在为世界各地成百上千万的系统提供服务。 它是绝大多数主流Linux发行版和许多网络设备的默认“时间服务器”。(参考ntppool.org)


那问题就是 NTP 同步出了问题??


2.4 时钟不同步


我们到服务器上查看了下时间,确实和时钟服务器不同步,早了几分钟。


当我们执行 NTP 同步的命令后,时钟又同步了,也就是说时间回拨了。


ntpdate  <时钟服务器 IP>

在产生事故之前,我们重启过服务器 1。我们推测服务器重启后,服务器因网络问题没有正常同步。而在下一次定时同步操作到来之前的这个时间段,我们的后端服务已经出现了因 ID 重复导致的大量异常问题。


这个 NTP 时钟回拨的偶发现象并不常见,但时钟回拨确实会带了很多问题,比如润秒 问题也会带来 1s 时间的回拨。


闰秒就是通过给“世界标准时间”加(或减)1秒,让它更接近“太阳时”。例如,两者相差超过0.9秒时,就在23点59分59秒与00点00分00秒之间,插入一个原本不存在的“23点59分60秒”,来将时间调慢一秒钟。


为了预防这种情况的发生,网上也有一些开源解决方案。


三、解决方案


(1)方式一:使用美团 Leaf方案,基于雪花算法。


(2)方式二:使用百度 UidGenerator,基于雪花算法


(3)方式三:用 Redis 生成自增的分布式 ID。弊端是 ID 容易被猜到,有安全风险。


3.1 美团的 Leaf 方案


美团的开源项目 Leaf 的方案:采用依赖 ZooKeeper 的数据存储。如果时钟回拨的时间超过最大容忍的毫秒数阈值,则程序报错;如果在可容忍的范围内,Leaf 会等待时钟同步到最后一次主键生成的时间后再继续工作


重点就是需要等待时钟同步!



3.2 百度 UidGenerator 方案


百度UidGenerator方案不在每次获取 ID 时都实时计算分布式 ID,而是利用 RingBuffer 数据结构,通过缓存的方式预生成一批唯一 ID 列表,然后通过 incrementAndGet() 方法获取下一次的时间,从而脱离了对服务器时间的依赖,也就不会有时钟回拨的问题。


重点就是预生成一批 ID!


Github地址:


https://github.com/baidu/uid-generator

四、总结


本篇通过一次偶发的生产事故,引出了雪花算法的原理、雪花算法的不足、对应的开源解决方案。


雪花算法强依赖服务器的时钟,如果时钟产生了回拨,就会造成很多问题。


我们的系统虽然做了 NTP 时钟同步,但也不是 100% 可靠,而且润秒这种场景也是出现过很多次。鉴于此,美团和百度也有对应的解决方案。


最后,我们的生产环境也是第一次遇到因 NTP 导致的时钟回拨,而且系统中用到雪花算法的地方并不多,所以目前并没有采取以上的替换方案。


https://github.com/Jackson0714/PassJava-Platform/blob/master/passjava-common/src/main/java/com/jackson0714/passjava/common/utils/SnowflakeUtilV2.java

参考资料:


time.geekbang.org/dailylesson…


blog.csdn.net/liangcsdn11…


http://www.jianshu.com/p/2911

作者:悟空聊架构
来源:juejin.cn/post/7244339465559408695
10ca6…

收起阅读 »

AIGC明星独角兽爆雷!7亿融资烧大半,拖欠员工工资,创始人被扒得千疮百孔

Stable Diffusion背后团队创始人,被曝巨大丑闻! 这两天,福布斯发布的一则长新闻在网上病毒式传播: 30多位前员工+投资人现身说法,细数Stability AI老板Emad Mostaque(伊玛德·莫斯塔克)9大罪证—— 学历造假,根本没有取...
继续阅读 »

Stable Diffusion背后团队创始人,被曝巨大丑闻!


这两天,福布斯发布的一则长新闻在网上病毒式传播:


30多位前员工+投资人现身说法,细数Stability AI老板Emad Mostaque(伊玛德·莫斯塔克)9大罪证——



  • 学历造假,根本没有取得牛津大学硕士学位;

  • 把Stable Diffusion的10亿代码成果“据为己有”;

  • 拖欠大量员工工资,甚至不交工资税;

  • ……


据爆料,这家所谓刚融资1亿美元(折合人民币约7亿)、估值10亿美元晋升独角兽的“明星公司”,其创始人不仅夸大了Stability AI公司的收入,其后续融资也不顺利,只是对外表示“成功”。


图片


在Stability AI工作过的前员工Eric Hallahan在推特阐述亲身经历:



和我的感受一样,伊玛德擅长把别人的作品写上自己的名字,或者做一些你无法确认是否属实的事情。



图片


甚至有网友看完后给出了有点耸人听闻的结论:



与Sam Bankman Fried(币圈诈骗被捕)、Bernie Madoff、Elizabeth Holmes(滴血验癌女王)等人类似,伊玛德·莫斯塔克也是这样的骗子。


显然,这标志着Stability AI的终结



图片

图片


这究竟是怎么一回事?


Stability AI创始人原来是个大忽悠?


福布斯曝光的9大具体罪状,按照严重程度先后为:


01、窃取Stable Diffusion成果


这起纠纷还得追溯到去年10月。当时Stability AI刚刚拿到1.01亿美元,一举成为估值10亿独角兽。


前脚刚融资完,后脚一家叫做Runway的AI初创公司就突然“截胡”,发布了一个Stable Diffusion新版本,并表示自己才是Stable Diffusion的原作者。


图片


原来,让Stability AI声名鹊起的Stable Diffusion,核心代码是由慕尼黑大学、海德堡大学和Runway共同完成的,Stability AI只是作为“金主”,提供了计算资源。


但是Runway等作者认为,Stability AI直接将该成果据为己有,比如在Stable Diffusion去年8月正式发布之时,Stability AI的新闻稿直接将伊玛德写成“有史以来最受欢迎的开源软件”的首席布道者。


没过多久,Stability AI的筹款平台还直接称呼“Stability AI是Stable Diffusion背后的公司”、“它是我们的模型”,根本没有提到Runway等作者。


图片


来自慕尼黑大学、海德堡大学的教授其实也想宣传他们的贡献,奈何大学的新闻办公室当时正在休假。他表示,伊玛德在关键信息中误导了公众:



显然,如果你拥有一家自带庞大新闻部的公司,你就可以根据自己的兴趣重塑历史。



此外,伊玛德在去年的投资者介绍中,还将Midjourney描述为其“生态系统”的一部分,声称Stability AI“共同创造”了该产品并“组织”了其用户社区。


但Midjourney创始人告诉福布斯,伊玛德只是提供了“非常小”的一部分财务捐赠,除此之外与Midjourney没有任何关系。


图片


02、拖欠员工工资、不交税,明目张胆开上“夫妻店”


有多少人惊叹过Stability AI如此一个快速做大做强的公司,居然只有11个初始员工?


其实后来Stability AI快速扩张,员工数量达到了140名,其中不乏曾任英伟达董事的研发副总裁、来自谷歌大脑的负责人,以及Ommer实验室的3位博士生。


但伊玛德被曝对员工很“苛刻”。


有8名前雇员向福布斯称,公司内部一再拖延或未支付他们的工资超过一个月,以及该上交的工资税也没交,导致去年英国税务机构都威胁要没收公司资产了。


就在这期间,伊玛德的老婆作为公司的公关主管(后来成为董事会成员),竟然还从公司账户中挪走了数万英镑


图片


他们的理由是,我们夫妻俩最开始一直在通过贷款帮助Stability AI运营(现在该还了)。


Ps. “欠款”已在2022年底之前已经结清。


另外,伊玛德的老婆还被曝经常在办公室骂员工,甚至把他们骂哭


最骇人的还是今年1月她曾表示要辞去董事会的职位,回归家庭和个人事业。然而5月,在Stability AI的一份组织架构图中,她又被列为“基金会负责人”,处于公司最高层,与丈夫伊玛德相当。


03、学历造假


说回伊玛德,今年他40岁,是一位在英国长大的孟加拉人,2005年本科毕业于牛津大学数学和计算机专业。


然而他对外声称自己拥有牛津大学的硕士学历。


图片


04、工作经历造假


伊玛德毕业后进入金融行业,后加入对冲基金公司Capricorn。


他声称自己因为重新“盘活”这家一度陷入困境的公司而获得殊荣,然而Capricorn联创表示:



他确实是公司的联合首席投资官,然而由于业绩不佳,这只3.3亿美元的基金很快就结束了仅两年的运营。



当初引荐他进入Capricorn的大佬则表示:



伊玛德非常擅长编故事。



基金事业失败之后,伊玛德开启了创业,经历一系列失败点子之后,终于在2019年创立了Stability AI,不过当时这家公司是作为一个AI驱动的数据中心,来帮助全球机构做出有关新冠疫情方面的决策。


后来直到他注意到了Stable Diffusion团队。


05、夸大公司收入


伊玛德去年10月公开声称,Stability AI年收入已超过1000万美元,但知情人士表示,公司销售额没有改善。


06、融资困难,遭遇投资人怀疑


尽管Stability AI收入并不可观,伊玛德最初还是通过各种办法搞到了约1亿美元的融资。


但现在,他遭到了很多投资人的质疑。


许多投资人告诉福布斯,伊玛德后面一直在努力想要以大约40亿美元的估值再融资数亿美元,却在如今这个对初创AI公司相当利好的风口中一直都没有成功。


07、夸大与亚马逊的合作,拖欠数百万美元账单


伊玛德的夸大宣传还不止如此。


他告诉投资人Stability AI正在组装世界上最大的10台超算之一,然后亚马逊以80%的折扣卖给他计算资源服务,因为Stability AI“跟亚马逊具有战略合作伙伴关系”。


然而实际上,只要是任何跟亚马逊有长期租赁合作的客户都能享受这一标准折扣。


另外,据三名前Stability AI员工表示,公司还曾因为拖欠数月账单(高达数百万美元),导致亚马逊威胁说要撤销该公司对其部分GPU的访问权限。


08、和世卫组织等套近乎


伊玛德在几个筹款平台上都宣称经合组织、世界卫生组织和世界银行是Stability AI的合作伙伴,但三者全部予以否认。


09、还有版权官司在身


最后,伊玛德还被指控Stability AI违反版权法训练模型,遭到了盖蒂图片社和艺术家的集体诉讼。


消息一出,网友无不震惊又失望:原来当今最牛AIGC公司之一的创始人是如此阴暗之人。


图片


有人称,伊玛德为了能让自己成为亿万富翁不择手段。


图片


AI技术本身也躺枪:



果然和“它的主人”一样,取得的成果是从别人(人类艺术家)那里剽窃而来的。



图片


一时之间,舆论哗然。


事实究竟如何呢?


本人火速回应:大量不实指控


面对这一系列近乎“不留情面”的指控,伊玛德24小时内火速给出了回应。


在博客上,他po出了自己给Stability AI团队针对福布斯报道写的长信,算是给团队作出一个解释,同时也对外界的种种传言进行澄清。


1、学历造假


伊玛德在长信中确认了自己没有硕士学位的说法,甚至连学士学位也没有。不过他表示,这仅仅是因为他没有参加毕业典礼的缘故:



我已经支付了60英镑,并将在下个月通过邮寄方式收取学位证书。



2、关于剽窃Stable Diffusion成果


这份长信称,Stability AI已多次公开声明是Stable Diffusion第一个版本的合作者,其他合作者来自慕尼黑大学(LMU)和另一家AI创业公司Runway。


图片


伊玛德还表示,目前这篇论文中5位作者的3位,都在Stability AI工作,其中两位是共同第一作者:


图片


不过,这份声明中好像忽略了海德堡大学?原论文是这样的:


图片


3、关于拖欠员工工资


伊玛德承认之前存在延期发工资的情况,但后面都补上了,而且“向受影响的员工支付了额外的工资”。


同时自2021年底以来,在“正常运营中”没有任何工资支付问题。


4、与Midjourney的关系


伊玛德表示,Midjourney一直是Stability AI支持的一个“独立组织”,后者帮助它做了不少扩展,包括引入审美评分和偏好学习等,并为它提供战略和模式支持。


至于究竟捐了多少钱,伊玛德没有澄清也没有回应。


5、关于夸大亚马逊的战略合作


关于夸大亚马逊计算资源的合作,伊玛德没有提到价格,但表示AWS专门为Stability AI构建了一个罕见的专用计算集群,包含4000张A100,“独一无二”。


这里面一些支付上的问题(主要是付款时间问题),都已经在集群搭建好的当月(2022年8月)被解决。


除此之外,伊玛德还回应了自己和公司的一些信息。


关于他自己,他主要澄清了之前的工作。例如,他曾经在对冲基金工作,但后来这家基金倒闭了,原因并非在他。毕竟他就职期间,这家公司不仅拿到了奖项,还挽回了业绩;此外,他还与联合国有不少合作,并参与了自闭症相关研究等。


关于Stability AI,他则强调了目前“公司经营良好”,不仅努力加强和改进人力资源流程,而且2022年底前已结清和Zehra公司欠款。至于投资的情况,目前公司还没有向签署了MNDA的投资人开放Data Room(数据室)。


最后,伊玛德还刻意强调“公司所做的一切都会受到严格审查”:


图片


在发布这篇文章后,他接受了VentureBeat的采访,表示很难过:



我们的团队和福布斯澄清了一些事实,但他们显然选择忽视其中许多问题的真相。



对于这一回应,部分人并不买帐。因为关于文章中一些很关键的点,伊玛德并没有澄清:


图片


例如前员工Eric Hallahan就指出了其中一个模糊的地方:



正式员工工资可能是按时支付的,但这可不包括非正式员工和外包员工们的工资!



图片


但也有网友认为,这篇新闻报道充斥着恶意,伊玛德做过的好事不应该被忽略:


图片


不过在创始人被曝丑闻这一事件之外,Stability AI自身也面临严重问题——更早时候,它就被曝出疯狂烧钱、管理混乱等问题。


“尚未有明确盈利途径”


虽然去年10月拿到了1亿美元融资,不过Stability AI走得一直不算顺利。


前不久,记者Reed Albergotti就在Semafor上爆料了Stability AI目前面临的一些问题。


一方面,Stability AI目前缺少像微软之于OpenAI这样“财力雄厚”的合作伙伴,其AI模型训练的昂贵成本究竟如何处理是一个未知数。


前段时间Stability AI宣称要推出开源类ChatGPT产品,然而推出后效果并不好。


另一方面,伊玛德设想的两种商业模式目前都不是很明确。


一种是搞咨询、提供AI工具的模式,另一种则是和相关基金合作的模式。


目前后一种模式已经被放弃,前一种模式的运转也并不顺利,因为员工分散在各地办公,而这实际上减缓了产品开发流程、增加了成本。据Semafor称,一些员工已经对于伊玛德的领导风格失去信心,认为管理非常混乱。


显然,Stability AI的商业模式仍旧不确定。


目前它需要向投资人证明两点,一点是它的技术足以开发顶尖AI模型;另一点则是它是否具有“赚钱”的能力——


Stability AI去年融到的1亿美元,如今已经烧掉了大部头。


而就在更早的时候,据《商业内幕》介绍,伊玛德还曾经警告Stability AI员工这一行业竞争的激烈性:



你们都会在2023年死去。



图片


但也不止是盈利问题,AI绘画的不确定性,让这个领域也面临版权等更长期的问题。


有网友表示,Stability AI之事只是导火索:


图片



现在看来,这家公司是起诉的最合适选项。


但Midjourney和DeviantArt最好也小心点,因为他们很可能就是下一个。



参考链接:

[1]http://www.forbes.com/sites/kenri…

[2]twitter.com/EMostaque/s…

[3]emad.posthaven.com/on-setting-…

[4]twitter.com/EricHallaha…

[5]venturebeat.com/ai/stabilit…

[6]http://www.businessinsider.com/stability-a…


— 完<

作者:量子位
来源:juejin.cn/post/7244466524559376445
/strong> —

收起阅读 »

优雅的时钟翻页效果,让你的网页时钟与众不同!

web
你有没有想过给你的网页时钟添加翻页效果,让它更引人注目,更酷炫吗?如果是的话,你来对地方了! 这篇文章将教你如何通过简单的步骤,为你的网页时钟添加翻页效果。 无论你是 web 开发初学者或是有一定经验的开发者,这篇文章都将为你提供有用的实现技巧和原理解释。 ...
继续阅读 »

你有没有想过给你的网页时钟添加翻页效果,让它更引人注目,更酷炫吗?如果是的话,你来对地方了!


13.gif


这篇文章将教你如何通过简单的步骤,为你的网页时钟添加翻页效果。


无论你是 web 开发初学者或是有一定经验的开发者,这篇文章都将为你提供有用的实现技巧和原理解释。


来,跟着子辰一起开始吧!


思考


01.gif


通过上图可以看到,由 3 翻到 4,其实是 3 的上半部分,与 4 的下半部分,一起翻下来的。


为了便于理解将翻页的过程通过侧面角度展示,解析成下图中所示的样子。


02.png


我们先来看一下 3 是如何呈现的。


03.png


那么由 3 的呈现我们可以知道,4 其实一开始是对折的,然后 4B 翻下来后形成完整的 4。


04.png


那么现在我们将 3 与 4 结合在一起看看。


05.png


由上可知,下一个数字都是对折的,在呈现时,都是有由前一个数字的上半部与下一个数字的上半部,翻转得到新的数字。


既然数字翻页的秘密我们知道了,接下来就是实现了。


06.png


实现翻页


容器背景


首先我们要实现一个承载数字的容器,中间使用伪元素做分割线,这就是时钟的底盘。


<div class="card-container"></div>

.card-container {
background: #2c292c;
width: 200px;
height: 200px;
position: relative;
perspective: 500px;
}

.card-container::before {
z-index: 99;
content: " ";
position: absolute;
left: 0;
top: 50%;
background: #120f12;
width: 100%;
height: 6px;
margin-top: -3px;
}

07.png


下层数字上半部分


接下来我们先来实现背后的下一层的 4,因为 4 分为上下两部分,我们先实现上半部分。


<div class="card-container">
<div class="card1 card-item">4</div>
</div>

/* 因为所有的数字都有公共部分,我们提取出来 */
.card-item {
position: absolute;
width: 100%;
/* 因为每个卡片只有半个数字,所以高度只有百分之50 */
height: 50%;
left: 0;
top: 0;
overflow: hidden;
background: #2c292c;
}

.card1 {
line-height: 200px
}

08.png


下层数字下半部分


<div class="card-container">
<div class="card1 card-item">4</div>
<div class="card2 card-item">4</div>
</div>

.card2 {
top: 50%;
}

首先我们写出来是这样的。


09.png


但是我们要求的是 4 的下半部分向上对折覆盖在 4 的上半部分之上。
所以我们看到的应该是 4 下半部分的背面,通过中线向上对折,并且因为是背面,所以我们不应该看到他。


.card2 {
z-index: 2;
top: 50%;
line-height: 0;
/* 变换原点为上边的中部 */
transform-origin: center top;
/* 对折 180 度 */
transform: rotateX(180deg);
/* 通过这个属性让元素的背面隐藏 */
backface-visibility: hidden;
}

08.png


现在看上去好像和只有上半部分没什么区别,所以我们给他加个 hover 加个过渡效果让它翻转下来看看。


11.gif


这样就看出来了。


上层数字


上层数字的原理就比较简单了,我们参考下层数字的逻辑写。


<div class="card-container">
<div class="card1 card-item">4</div>
<div class="card2 card-item">4</div>
<div class="card3 card-item">3</div>
<div class="card4 card-item">3</div>
</div>

.card3 {
line-height: 200px;
transform-origin: center bottom;
backface-visibility: hidden;
z-index: 2
}

.card4 {
top: 50%;
line-height: 0
}

12.png


现在就是这样效果,同样的,我们给它加个 hover,3 的翻页过渡要与 4 的保持同步。


01-13.gif


现在我们就实现了单个数字的过渡。


翻页时钟


时钟无非就是三个翻页效果加上自动翻页,我们去实现一下。


<!-- data-number 用于存储上一次的时间,来和即将改变的时间对比 -->
<div class="card-container flip" id="card-h" data-number="00">
<div class="card1 card-item">00</div>
<div class="card2 card-item">00</div>
<div class="card3 card-item">00</div>
<div class="card4 card-item">00</div>
</div>
<div class="card-container flip" id="card-m" data-number="00">
<div class="card1 card-item">00</div>
<div class="card2 card-item">00</div>
<div class="card3 card-item">00</div>
<div class="card4 card-item">00</div>
</div>
<div class="card-container flip" id="card-s" data-number="00">
<div class="card1 card-item">00</div>
<div class="card2 card-item">00</div>
<div class="card3 card-item">00</div>
<div class="card4 card-item">00</div>
</div>

/* etc... */

.flip .card2 {
transform: rotateX(0);
}

.flip .card3 {
transform: rotateX(-180deg);
}

// 获取 dom
const hour = document.getElementById("card-h");
const minute = document.getElementById("card-m");
const second = document.getElementById("card-s");

function setHTML(dom, time) {
// 下一次要显示的时间
const nextValue = time.toString().padStart(2, "0");
// 上一次的时间
const curValue = dom.dataset.number;
// 如果下次要显示的时间和上一次的一样,直接退出。比如在同一分钟或同一小时内。
if (nextValue === curValue) {
return;
}
// 重置时分秒的 dom
dom.innerHTML = `<div class="card1 card-item">${nextValue}</div>
<div class="card2 card-item">${nextValue}</div>
<div class="card3 card-item">${curValue}</div>
<div class="card4 card-item">${curValue}</div>`
;
// 移除 flip 属性再次添加以触发过渡再次执行
dom.classList.remove("flip");
dom.clientHeight;
dom.classList.add("flip");
// 时间不同时重置 dataset.number
dom.dataset.number = nextValue;
}

// 获取时分秒并分别设置
function setNumbers() {
var now = new Date();
var h = now.getHours();
var m = now.getMinutes();
var s = now.getSeconds();
setHTML(hour, h);
setHTML(minute, m);
setHTML(second, s);
}

setNumbers();

setInterval(setNumbers, 1000);

13.gif


至此我们就完成了时钟翻页的效果了,你学会了吗?


总结


子辰详细介绍了如何通过简单的步骤,为网页时钟添加翻页效果。


文章从思考开始,通过分析数字翻页的秘密来解决问题。


接着,详细讲解了实现翻页的具体方法和原理,并给出了相应的代码实现。


最后,通过组合多个翻页效果,实现了完整的时钟翻页效果。


如果你认真读完了这篇文章,那么以下这几点都是你所学到的:



  1. 提高对 CSS3 属性的理解和掌握,例如 perspective、transform、backface-visibility 等。

  2. 掌握实现元素翻转动画的基本方法和技巧,包括旋转轴心、变换原点、背面可见性等。

  3. 了解如何通过数据属性(data-*)存储和比较数据,避免不必要的重复操作。

  4. 学会如何通过 JavaScript 操作 DOM 元素,实现网页中的动态效果。


其实实现的效果并不难,代码也都是基础的代码,难的是思考翻页的过程,好的思维方法,才是前端进阶的基础,更是关键。


多看好的文章,多看好的思考过程,都是

作者:子辰Web草庐
来源:juejin.cn/post/7244351125448458296
提升思维的一种方式。

收起阅读 »

移动端网页开发有感

web
前段时间参与了一个移动端页面开发的需求,开发时明显感觉与 pc 端开发相比,移动端页面的限制会更多😭 需求结束后思考了一下究竟是哪些方面感觉不舒服?有没有对应的解决方法?以便下次开发移动端页面时能提升开发效率和体验。 移动端网页容易出现布局问题 🤦‍♂️ 因为...
继续阅读 »

前段时间参与了一个移动端页面开发的需求,开发时明显感觉与 pc 端开发相比,移动端页面的限制会更多😭


需求结束后思考了一下究竟是哪些方面感觉不舒服?有没有对应的解决方法?以便下次开发移动端页面时能提升开发效率和体验。


移动端网页容易出现布局问题 🤦‍♂️


因为页面空间小,容易出现元素重叠、挤压、换行等样式问题,怎么在不同尺寸的设备上合适地展示页面?


解决办法:




  1. 使用 <meta name="viewport"> 标签 ✨


    这个标签想必做过移动端页面开发的同学都不陌生吧?它就是专门为移动端展示优化而增加的标签。


    先来看看它的作用是什么?


    它可以设置移动端页面宽度、缩放比例、是否允许用户缩放网页等


    它的基本属性有哪些?


    属性名含义取值范围
    width控制视口大小具体数值或 'device-width'
    initial-scale页面初始缩放比例0.0 ~ 10
    minimum-scale控制页面允许被缩小的倍数0.0 ~ 10
    maximum-scale控制页面允许被大的倍数0.0 ~ 10
    user-scalable控制是否允许放大和缩小页面yes 或 no

    需要注意的是在移动设备上默认页面宽度为 980px:




Luban_16853778753692bc31648-c60e-4ebc-845c-3bac272f7393.jpg


假如我们希望页面的视口宽度与设备宽度相同,同时初始缩放比例为 1,可以在 <head> 里增加这个的 meta 标签
<meta name="viewport" content="width=device-width,initial-scale=1">


Luban_1685377875383f6d72d0e-c360-43aa-807a-739399af01fe.jpg


这样页面的展示就符合我们的预期了




  1. 使用 vw、vh 视口单位
    vw、vh 都是视口单位


    简而言之:100vw = 视口宽度,100vh = 视口高度 (够简单吧 😅




  2. 使用 rem 相对单位




rem 在移动端开发中很重要,因为不同移动设备有着不同的尺寸和分辨率,如果我们用固定的 px 作为元素大小单位会发现不够适用


而 rem 是相对单位大小,它相对的是根元素 html 的字体大小,比如:


<html>
<head>
<style>
html {
font-size: 14px; // 这里将 html 的字体大小设为 14px
}
.content {
font-size: 2rem; // 在页面展示时将会被计算成 14 * 2 = 28px
}
</style>
</head>
<body>
<div class="content">rem</div>
</body>
</html>

所以我们可以根据设备大小动态设置根元素大小,从而成比例地更改页面里其它元素的大小


    const BASE_PAGE_WIDTH = 370
const BASE_SIZE = 16

function setRem() {
const scale = document.documentElement.clientWidth / BASE_PAGE_WIDTH
document.documentElement.style.fontSize = `${scale * BASE_SIZE}px`
}

setRem()

真机调试比较麻烦 😌


尽管可以在电脑浏览器上模拟移动设备的展示情况,但并不够真实,在开发和测试阶段仍然需要在真机上调试;


同时可能我们的测试环境需要连接 vpn 或添加特定的请求头才能访问到,所以在手机上需要通过电脑代理才能访问测试环境,从而进行开发测试;


最后,即使能在手机上访问到本地开发和测试环境的页面,你会发现当页面报错的时候你压根就看不到 log 日志输出或网络请求,这种干看着页面有问题却不能进一步排查的感觉就很难受 😖


还好有一些工具可以帮我们化解这些难题 🥳


解决办法:



  1. 首先我们可以使用 whistlecharles 来连接电脑代理,这里以 whistle 为例:

    • 电脑安装并启动 whistle

    • 手机和电脑在同一局域网下

    • 手机设置网络代理

    • 手机安装 https 证书





具体操作可以访问官方文档的详细步骤哈



Done!现在手机会通过电脑作为代理来访问网络,可以直接访问开发地址或测试环境地址啦~



  1. 然后我们可以使用 VConsole 在移动设备上进行调试,它相当于在页面上加了一个控制台,从而让我们可以查看页面上的日志输出、网络请求等,它的用法也很简单:


// 使用 npm 安装
npm install vconsole

import VConsole from 'vconsole'

new VConsole()

// 使用 CDN 安装
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
// VConsole will be exported to `window.VConsole` by default.
const vConsole = new window.VConsole();
</script>


然后你会发现页面右下角多了一个 vConsole 的按钮:


Screenshot_2023-06-12-23-46-33-090_mark.via.jpg


我们可以测试一下打印日志:


    document.addEventListener('click', handlePageClick)

function handlePageClick() {
console.log('Daniel Yang')
}

在点击页面后再点击 vConsole 按钮会发现在展开的面板里 log 一栏已经显示出 log 的内容:


Screenshot_2023-06-12-23-48-35-778_mark.via.jpg


同时我们也可以在 VConsole 面板上查看页面元素结构、客户端存储、网络请求等,总之非常的 nice 🤗


以上就是自己对一次移动端网页开发过程中遇到的问题小小的总结,如果你在移动端开发中有遇到其它印象深刻的坑,欢迎一起留言讨论哦


006APoFYly8hesgm67dwpj30hs0hbmxv.jpeg

作者:卡布奇诺有点苦
来源:juejin.cn/post/7243757233666195515
th="50%"/>

收起阅读 »

百分百空手接大锅

web
背景 愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅...
继续阅读 »

背景


愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅,都怪我们前端,没有做好前端监控,导致线上问题持续两天才发现。原本以为运营会把推辞一下说不,锅是她们的,可惜人家不太懂人情世故,这锅就扣在了技术部头上。虽然但是,我还是静下心来把前端异常监控搞了出来,下次一定不要主动接锅,希望看到本文的朋友们也不要随便心软接锅^_^


监控


因为之前基于sentry做了埋点处理,基础已经打好,支持全自动埋点、手动埋点和数据上报。相关的原理可以参考之前的一篇文章如何从0-1构建数据平台(2)- 前端埋点。本次监控的数据上报也基于sentry.js。那么如何设计整个流程呢。具体步骤如下:




  1. 监控数据分类




  2. 监控数据定义




  3. 监控数据收集




  4. 监控数据上报




  5. 监控数据输出




  6. 监控数据预警




数据分类


我们主要是前端的数据错误,一般的异常大类分为逻辑异常和代码异常。基于我们的项目,由于涉及营收,我们就将逻辑错误专注于支付异常,其他的代码导致的错误分为一大类。然后再将两大异常进行细分,如下:




  1. 支付异常


    1.1 支付成功


    1.2 支付失败




  2. 代码异常


    2.1 bindexception


     2.1.1  js_error

    2.1.2 img_error

    2.1.3 audio_error

    2.1.4 script_error

    2.1.5 video_error



  3. unhandleRejection


    3.1 promise_unhandledrejection_error


    3.2 ajax_error




  4. vueException




  5. peformanceInfo




数据定义


基于sentry的上报数据,一般都包括事件与属性。在此我们定义支付异常事件为“page_h5_pay_monitor”,定义代码异常事件为“page_monitor”。然后支付异常的属性大概为:



pay_time,

pay_orderid,

pay_result,

pay_amount,

pay_type,

pay_use_coupon,

pay_use_coupon_id,

pay_use_coupon_name,

pay_use_discount_amount,

pay_fail_reason,

pay_platment


代码异常不同的错误类型可能属性会有所区别:



// js_error

monitor_type,

monitor_message,

monitor_lineno,

monitor_colno,

monitor_error,

monitor_stack,

monitor_url

// src_error

monitor_type,

monitor_target_src,

monitor_url

// promise_error

monitor_type,

monitor_message,

monitor_stack,

monitor_url

// ajax_error

monitor_type,

monitor_ajax_method,

monitor_ajax_data,

monitor_ajax_params,

monitor_ajax_url,

monitor_ajax_headers,

monitor_url,

monitor_message,

monitor_ajax_code

// vue_error

monitor_type,

monitor_message,

monitor_stack,

monitor_hook,

monitor_url

// peformanceInfo 为数据添加 loading_time 属性,该属性通过entryTypes获取

try {

const observer = new PerformanceObserver((list) => {

for (const entry of list.getEntries()) {

if (entry.entryType === 'paint') {

sa.store.set('loading_time', entry.startTime)

}
}

})

observer.observe({ entryTypes: ['paint'] })

} catch (err) {

console.log(err)

}


数据收集


数据收集通过事件绑定进行收集,具体绑定如下:


import {

BindErrorReporter,

VueErrorReporter,

UnhandledRejectionReporter

} from './report'

const Vue = require('vue')


// binderror绑定

const MonitorBinderror = () => {

window.addEventListener(

'error',

function(error) {

BindErrorReporter(error)

},true )

}

// unhandleRejection绑定 这里由于使用了axios,因此ajax_error也属于promise_error

const MonitorUnhandledRejection = () => {

window.addEventListener('unhandledrejection', function(error) {

if (error && error.reason) {

const { message, code, stack, isAxios, config } = error.reason

if (isAxios && config) {

// console.log(config)

const { data, params, headers, url, method } = config

UnhandledRejectionReporter({

isAjax: true,

data: JSON.stringify(data),

params: JSON.stringify(params),

headers: JSON.stringify(headers),

url,

method,

message: message || error.message,

code

})

} else {

UnhandledRejectionReporter({

isAjax: false,

message,

stack

})

}

}

})

}

// vueException绑定

const MonitorVueError = () => {

Vue.config.errorHandler = function(error, vm, info) {

const { message, stack } = error

VueErrorReporter({

message,

stack,

vuehook: info

})

}

}

// 输出绑定方法

export const MonitorException = () => {

try {

MonitorBinderror()

MonitorUnhandledRejection()

MonitorVueError()

} catch (error) {

console.log('monitor exception init error', error)

}

}


数据上报


数据上报都是基于sentry进行上报,具体如下:



/*

* 异常监控库 基于sentry jssdk

* 监控类别:

* 1、window onerror 监控未定义属性使用 js资源加载失败问题

* 2、window addListener error 监控未定义属性使用 图片资源加载失败问题

* 3、unhandledrejection 监听promise对象未catch的错误

* 4、vue.errorHandler 监听vue脚本错误

* 5、自定义错误 包括接口错误 或其他diy错误

* 上报事件: page_monitor

*/


// 错误类别常量

const ERROR_TYPE = {

JS_ERROR: 'js_error',

IMG_ERROR: 'img_error',

AUDIO_ERROR: 'audio_error',

SCRIPT_ERROR: 'script_error',

VIDEO_ERROR: 'video_error',

VUE_ERROR: 'vue_error',

PROMISE_ERROR: 'promise_unhandledrejection_error',

AJAX_ERROR: 'ajax_error'

}

const MONITOR_NAME = 'page_monitor'

const PAY_MONITOR_NAME = 'page_h5_pay_monitor'

const MEMBER_PAY_MONITOR_NAME = 'page_member_pay_monitor'

export const BindErrorReporter = function(error) {

if (error) {

if (error.error) {

const { colno, lineno } = error

const { message, stack } = error.error

// 过滤

// 客户端会有调用calljs的场景 可能有一些未知的calljs

if (message && message.toLowerCase().indexOf('calljs') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else if (error.target) {

const type = error.target.nodeName.toLowerCase()

const monitorType = type + '_error'

const src = error.target.src

sa.track(MONITOR_NAME, {

//属性

})

}

}

}

export const UnhandledRejectionReporter = function({

isAjax = false,

method,

data,

params,

url,

headers,

message,

stack,

code

}
) {

if (!isAjax) {

// 过滤一些特殊的场景

// 1、自动播放触发问题

if (message && message.toLowerCase().indexOf('user gesture') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else {

sa.track(MONITOR_NAME, {

//属性

})

}

}

export const VueErrorReporter = function({ message, stack, vuehook }) {

sa.track(MONITOR_NAME, {

//属性

})

}

export const H5PayErrorReport = ({

isSuccess = true,

amount = 0,

type = -1,

couponId = -1,

couponName = '',

discountAmount = 0,

reason = '',

orderid = 0,

}
) => {

// 事件名:page_member_pay_monitor

sa.track(PAY_MONITOR_NAME, {

//属性

})

}


以上,通过sentry的sa.track进行上报,具体不作展开


输出与预警


数据被上报到大数据平台,被存储到hdfs中,然后我们直接做定时任务读取hdfs进行一定的过滤通过钉钉webhook输出到钉钉群,另外如果有需要做数据备份可以通过hdfs到数据仓库再到kylin进行存储。


总结


数据监控对于大的,特别是涉及营收的平台是必要的,我们在设计项目的时候一定要考虑到,最好能说服服务端,让他们服务端也提供相应的代码监控。ngnix层或者云端最好也来一层。严重的异常可以直接给你打电话,目前云平台都有相应支持。这样有异常及时发现,锅嘛,接到手里

作者:CodePlayer
来源:juejin.cn/post/7244363578429030459
就可以精准扔出去了。

收起阅读 »

Redis性能瓶颈揭秘:如何优化大key问题?

1. 什么是Redis大key问题 Redis大key问题指的是某个key对应的value值所占的内存空间比较大,导致Redis的性能下降、内存不足、数据不均衡以及主从同步延迟等问题。 到底多大的数据量才算是大key? 没有固定的判别标准,通常认为字符串类型的...
继续阅读 »

1. 什么是Redis大key问题


Redis大key问题指的是某个key对应的value值所占的内存空间比较大,导致Redis的性能下降、内存不足、数据不均衡以及主从同步延迟等问题。


到底多大的数据量才算是大key?


没有固定的判别标准,通常认为字符串类型的key对应的value值占用空间大于1M,或者集合类型的k元素数量超过1万个,就算是大key。


Redis大key问题的定义及评判准则并非一成不变,而应根据Redis的实际运用以及业务需求来综合评估。例如,在高并发且低延迟的场景中,仅10kb可能就已构成大key;然而在低并发、高容量的环境下,大key的界限可能在100kb。因此,在设计与运用Redis时,要依据业务需求与性能指标来确立合理的大key阈值。


2. 大key带来的影响



  1. 内存占用过高。大Key占用过多的内存空间,可能导致可用内存不足,从而触发内存淘汰策略。在极端情况下,可能导致内存耗尽,Redis实例崩溃,影响系统的稳定性。

  2. 性能下降。大Key会占用大量内存空间,导致内存碎片增加,进而影响Redis的性能。对于大Key的操作,如读取、写入、删除等,都会消耗更多的CPU时间和内存资源,进一步降低系统性能。

  3. 阻塞其他操作。某些对大Key的操作可能会导致Redis实例阻塞。例如,使用DEL命令删除一个大Key时,可能会导致Redis实例在一段时间内无法响应其他客户端请求,从而影响系统的响应时间和吞吐量。

  4. 网络拥塞。每次获取大key产生的网络流量较大,可能造成机器或局域网的带宽被打满,同时波及其他服务。例如:一个大key占用空间是1MB,每秒访问1000次,就有1000MB的流量。

  5. 主从同步延迟。当Redis实例配置了主从同步时,大Key可能导致主从同步延迟。由于大Key占用较多内存,同步过程中需要传输大量数据,这会导致主从之间的网络传输延迟增加,进而影响数据一致性。

  6. 数据倾斜。在Redis集群模式中,某个数据分片的内存使用率远超其他数据分片,无法使数据分片的内存资源达到均衡。另外也可能造成Redis内存达到maxmemory参数定义的上限导致重要的key被逐出,甚至引发内存溢出。


3. 大key产生的原因



  1. 业务设计不合理。这是最常见的原因,不应该把大量数据存储在一个key中,而应该分散到多个key。例如:把全国数据按照省行政区拆分成34个key,或者按照城市拆分成300个key,可以进一步降低产生大key的概率。

  2. 没有预见value的动态增长问题。如果一直添加value数据,没有删除机制、过期机制或者限制数量,迟早出现大key。例如:微博明星的粉丝列表、热门评论等。

  3. 过期时间设置不当。如果没有给某个key设置过期时间,或者过期时间设置较长。随着时间推移,value数量快速累积,最终形成大key。

  4. 程序bug。某些异常情况导致某些key的生命周期超出预期,或者value数量异常增长 ,也会产生大key。


4. 怎样排查大key


4.1 SCAN命令


通过使用Redis的SCAN命令,我们可以逐步遍历数据库中的所有Key。结合其他命令(如STRLEN、LLEN、SCARD、HLEN等),我们可以识别出大Key。SCAN命令的优势在于它可以在不阻塞Redis实例的情况下进行遍历。


4.2 bigkeys参数


使用redis-cli命令客户端,连接Redis服务的时候,加上 —bigkeys 参数,可以扫描每种数据类型数量最大的key。



redis-cli -h 127.0.0.1 -p 6379 —bigkeys



4.3 Redis RDB Tools工具


使用开源工具Redis RDB Tools,分析RDB文件,扫描出Redis大key。


例如:输出占用内存大于1kb,排名前3的keys。



rdb —commond memory —bytes 1024 —largest 3 dump.rbd



5. 怎么解决大key



  1. 拆分成多个小key。这是最容易想到的办法,降低单key的大小,读取可以用mget批量读取。

  2. 数据压缩。使用String类型的时候,使用压缩算法减少value大小。或者是使用Hash类型存储,因为Hash类型底层使用了压缩列表数据结构。

  3. 设置合理的过期时间。为每个key设置过期时间,并设置合理的过期时间,以便在数据失效后自动清理,避免长时间累积的大Key问题。

  4. 启用内存淘汰策略。启用Redis的内存淘汰策略,例如LRU(Least Recently Used,最近最少使用),以便在内存不足时自动淘汰最近最少使用的数据,防止大Key长时间占用内存。

  5. 数据分片。例如使用Redis Cluster将数据分散到多个Redis实例,以减轻单个实例的负担,降低大Key问题的风险。

  6. 删除大key。使用UNLINK命令删除大key,UNLINK命令是DEL命令的异步版本,它可以在后台删除Key,避免阻塞Redis实例。


6. 总结


大Key问题是Redis中常见的问题之一,可能导致性能下降、内存占用过高、阻塞其他操作以及主从同步延迟等问题。本文详细介绍了大Key产生的原因、影响、检测方法和解决方案。通过优化数据结构设计、设定合理的数据过期策略、优化系统架构和配置,以及渐进式删除大Key等方法,我们可以有效地解决和预防大Key问题,从而提高Redis系统的稳定性和性能。



我是「一灯架构」,如果本文对你有帮助,欢迎各位小伙伴点赞、评论和关注,感谢各位老铁,我们下期见


作者:一灯架构
链接:https://juejin.cn/post/7222825885203218491
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

微信扫码登录,做了又好像没做

这两天,和微信扫码登录这个功能杠上了。 为什么??? 大家都知道,个人网站我做了很多,而我的每个网站都有登录/注册相关的功能。无一例外我的网站都是通过账号和密码这种形式进行登录/注册,而前两天有人和我说这样太麻烦了,搞个扫码登录多方便啊! 你还别说,我以前是真...
继续阅读 »

这两天,和微信扫码登录这个功能杠上了。


为什么???


大家都知道,个人网站我做了很多,而我的每个网站都有登录/注册相关的功能。无一例外我的网站都是通过账号和密码这种形式进行登录/注册,而前两天有人和我说这样太麻烦了,搞个扫码登录多方便啊!


你还别说,我以前是真考虑过,但那会不是赶工期嘛,就没去过多的了解扫码登录这回事。而这次再次被提及,那就顺势这两天给他办了吧!


G8JkY.gif


考虑到现在的用户都喜欢使用微信,而微信扫码登录这个功能也是非常普遍的,所以一拍即合,我也整个微信扫码登录。


经过周末两天的摸索,现在个人开发者要给自己的网站加上微信扫码登录这个功能有三个途径:



  1. 利用微信扫码,关注公众号实现微信扫码登录

  2. 利用微信开放平台提供的网站应用提供的获取用户信息接口,实现扫码登录

  3. 利用小程序做中间过渡实现的微信扫码登录


然而,这里我要给你们泼一盆凉水了,这三个方案,我都试过,对于个人而言,无法实现,或者说无法满足我对扫码登录功能的期望



期望:微信扫码,用户授权,获取用户头像及昵称等信息



先来说说公众号,为啥不行。


首先,我有公众号,但是我的公众号仅仅是个人,而不是企业认证的公众号,这就表明我只能发信息,而没有获取和支付等这种高级的功能。


就比如获取用户信息:


Snipaste_2023-06-11_19-04-17.jpg


可能有人会说去认证一个不就行了,我只能说,你们可以去试试(认证费几千块 + 其他材料)。


再来说说微信开放平台,为啥不行。


其实这个平台的对接功能写的非常通俗易懂,而且也相对简单,但是,和上面的问题也是一样,个人想要获取用户相关的功能,也要认证。


那我再看一下他的认证条件吧:


Snipaste_2023-06-11_19-04-117.jpg


这个认证条件倒是简单明了,就 300 块钱,你认证了就给你获取用户的功能权限。


虽然 300 也不是什么大数目,但是我一想我做的那几个项目用户都还没几个,就把 300 花出去,不太划算,所以这个方向也 pass。


那就来看看最后的小程序方案吧!


查看了小程序的接口文档,发现还真有获取用户信息的 wx 接口,文档如下:


Snipaste_2023-06-11_19-47-46.jpg


有了这个接口就好办了,而且个人是可以开发小程序且不需要任何认证流程的,有微信号就 ok。这不就是我想要的方案嘛,有手就行了,扫码登录。


此时我还没意识到,这个有手就行和我想象的不太一样。


敲定好方案之后,我就在脑子里构思如何通借助小程序来实现微信扫码登录,大致思考了一会,我设计了如下登录方案:


Snipaste_2023-06-11_20-06-39.jpg


图中的主要步骤就两个:



  1. 获取小程序二维码,给用户扫码

  2. 微信扫码,获取用户信息,进行后续登录流程


之后通过编码,居然发现最重要的两个信息(头像、昵称)拿不到,我也是醉了。而我去翻看微信的官方文档,原来是腾讯把 wx.getUserProfile 接口给下线了,这给我整无语了。


Snipaste_2023-06-11_21-20-30.jpg


咋办?


小程序代码我都写好上线了,后端的相关接口我也写好了,就差网站页面联调了。你说这功能是联调,还是不联调呢!


算了,先这样吧!功能先保留,等以后要做新的项目了,再把这个功能放上去。现在整个的流程问题比较大的就是获取不到用户的头像和昵称,但是,扫码登录这个基本流程是已经走通的。等新项目对接这个功能的时候,再让用户通过网站提供的功能再来完善用户头像和昵称等信息吧!


Snipaste_2023-06-11_21-29-44.jpg


吐槽完了!!!


最后,感谢各位看官看到这里,如果大家有啥好的微信扫码登录方案,欢迎评论区留言。


作者:J3code
链接:https://juejin.cn/post/7243413799347601464
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android-策略模式与状态模式:形似而行不似

引言: 在软件开发中,我们经常面临着需求的变化和复杂的业务逻辑。为了解决这些问题,设计模式应运而生。其中,策略模式和状态模式是两种常用的设计模式。本文将带你走进它们的世界,一起探索它们的魅力和应用场景。 一、策略模式 在生活中,我们经常会遇到需要根据不同情况采...
继续阅读 »

引言: 在软件开发中,我们经常面临着需求的变化和复杂的业务逻辑。为了解决这些问题,设计模式应运而生。其中,策略模式和状态模式是两种常用的设计模式。本文将带你走进它们的世界,一起探索它们的魅力和应用场景。


一、策略模式


在生活中,我们经常会遇到需要根据不同情况采取不同策略的情况。软件开发也存在类似的情况。策略模式就是为了解决这类问题而生的。它允许在运行时根据需要选择算法的行为。


应用场景: 想象一下,你是一位游戏开发者,你的游戏中有多种角色,每个角色都有不同的攻击方式。使用策略模式,你可以为每个角色创建一个独立的攻击策略,然后在运行时根据角色的选择来决定使用哪种策略。这样,你就能灵活地扩展和切换角色的攻击方式,而无需修改大量的代码。


代码示例: 让我们以一个游戏的例子来说明策略模式的使用。假设你正在开发一个角色扮演游戏,其中有两个角色:战士(Warrior)和法师(Mage)。每个角色都有自己的攻击方式。首先,我们需要定义一个策略接口,表示不同的攻击策略:

interface AttackStrategy {
fun attack()
}

接下来,我们创建两个具体的策略类,分别代表战士和法师的攻击方式:

class WarriorAttackStrategy : AttackStrategy {
override fun attack() {
println("战士使用剑攻击")
}
}

class MageAttackStrategy : AttackStrategy {
override fun attack() {
println("法师使用魔法攻击")
}
}

最后,我们创建一个角色类,其中包含一个攻击策略的引用:

class Character(private val attackStrategy: AttackStrategy) {
fun attack() {
attackStrategy.attack()
}
}

现在,我们可以创建战士和法师的实例,并分别调用它们的攻击方法:

val warrior = Character(WarriorAttackStrategy())
warrior.attack() // 输出:战士使用剑攻击

val mage = Character(MageAttackStrategy())
mage.attack() // 输出:法师使用魔法攻击

通过使用策略模式,我们可以轻松地为角色添加新的攻击方式,而无需修改角色类的代码。


策略模式的优势:



  • 灵活性:策略模式允许在运行时动态切换算法,使系统更具灵活性。

  • 可扩展性:通过添加新的策略类,我们可以轻松地扩展系统的功能。


缺点:



  • 增加了类的数量:引入策略模式会增加类的数量,特别是当策略类较多时。过度使用策略模式可能会导致类爆炸。


二、状态模式


在某些情况下,对象的行为取决于其内部状态的变化。状态模式可以帮助我们更好地管理对象的状态,并基于不同的状态执行相应的行为。


应用场景: 以电梯为例,电梯在不同的状态下有不同的行为。比如,当电梯在运行状态下,按下楼层按钮不会有任何反应;而当电梯在停止状态下,按下楼层按钮会触发电梯移动到相应楼层的行为。使用状态模式,我们可以更好地管理电梯的状态和行为,提高系统的可维护性和扩展性。


代码示例: 让我们以电梯的例子来说明状态模式的使用。首先,我们定义一个电梯状态接口:

interface ElevatorState {
fun pressFloorButton(floor: Int)
}

接下来,我们创建两个具体的状态类,分别代表电梯的运行状态和停止状态:

class RunningState : ElevatorState {
override fun pressFloorButton(floor: Int) {
// 运行状态下不响应按钮按下事件
}
}

class StoppedState : ElevatorState {
override fun pressFloorButton(floor: Int) {
println("电梯移动到楼层 $floor")
}
}

然后,我们创建一个电梯类,其中包含一个状态的引用:

class Elevator(private var state: ElevatorState) {
fun setState(state: ElevatorState) {
this.state = state
}

fun pressFloorButton(floor: Int) {
state.pressFloorButton(floor)
}
}

现在,我们可以创建一个电梯的实例,并模拟按下楼层按钮的事件:

val elevator = Elevator(StoppedState())
elevator.pressFloorButton(5) // 输出:电梯移动到楼层 5

elevator.setState(RunningState())
elevator.pressFloorButton(7) // 没有任何输出

通过使用状态模式,我们可以根据电梯的状态来决定是否响应按钮按下事件,并执行相应的行为。


状态模式的优势:



  • 易于扩展:通过添加新的状态类,我们可以轻松地扩展系统的行为。

  • 可维护性:状态模式使状态转换和行为与特定状态相关,使代码更具可读性和可维护性。


缺点:



  • 增加了类的数量:引入状态模式会增加类的数量,特别是当状态较多时。过度使用状态模式可能会导致类爆炸。


策略模式与状态模式的区别:



  • 目的不同:策略模式关注的是算法的选择和使用,用于根据不同的策略执行不同的行为;而状态模式关注的是对象内部状态的变化,用于根据不同的状态执行不同的行为。

  • 职责不同:策略模式将不同的算法封装成独立的策略类,客户端主动选择使用哪种策略;而状态模式将不同的状态封装成独立的状态类,并由环境类根据当前状态执行相应的行为。

  • 关注点不同:策略模式关注的是行为的选择和灵活性,用于动态地切换算法;而状态模式关注的是状态的变化和可维护性,用于根据状态执行相应的行为。


结语: 策略模式和状态模式是两种常用的设计模式,它们分别解决了根据不同策略和状态执行不同行为的问题。通过灵活地选择和管理策略或状态,我们可以提高系统的灵活性、可扩展性和可维护性。在实际开发中,我们可以根据具体的需求和场景选择适合的设计模式,以优化代码结构和提高系统的可维护性。希望本文能帮助你更好地理解和应用策略模式和状态模式,让你在设计中游刃有余,解决你的设计困扰!


作者:潜行中的小角色
链接:https://juejin.cn/post/7243828459288395835
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

两年半的Android程序员の上半年总结

你好,你好们好~ 首先是感谢@张拭心,@林栩link以及各位大佬,jym的熏陶,我属实是21届,到目前为止也就两年半~ 技术上必然有待提高,但是文案书写上,是可以同步跟上的;这是我学到最核心的点;工作完成与否是义务责任,学会如何汇报是在强化自己的能力;所以我也...
继续阅读 »

你好,你好们好~
首先是感谢@张拭心,@林栩link以及各位大佬,jym的熏陶,我属实是21届,到目前为止也就两年半~


技术上必然有待提高,但是文案书写上,是可以同步跟上的;这是我学到最核心的点;工作完成与否是义务责任,学会如何汇报是在强化自己的能力;所以我也在想着自发性的去做总结;


一是觉得这样会有更好的健康的提升。


二是觉得:



人们无法预知片刻的价值,直到片刻永远变成了回忆



先从工作部分开始说吧


wallhaven-wyzxvr_1920x1080.png


工作

1、应用层进入Framework层



从年后开始,陆陆续续能在工作上接触到一些简单的系统层的业务,当然是简单的,比如View的焦点控制,网络SDK部分,系统截图适配等等;这其实算是一种能力被认可的预兆,可以被放到一些看似无关紧要的业务去了,对于我来说,我是非常感兴趣的;这样我会更有动力去接触驱动层,适配芯片;当然岗位的大部分内容还是跟UI相关,处理用户交互逻辑,适配数据接口等等,完全进化应该还需要很长一段时间。


2、接触到底层消息框架


硬件公司核心都在底层,C & C++ 应该都是最核心的部分;领导决策开放新项目的所有部门的源码,任何更新都可以可以看到【当然权限是领导给的,本项目的同事】,这意味着我可以在任何时间节点上,调试所有新功能,对比前后差别,思考为何这么实现?换做是我能会怎么做?规避逻辑,扩展思路怎么加?这些我都可以自己想,自己完成;领导英明!


3、上海分公司转去苏州总部


申请去总部的原因实际上有很多,无非是想省下更多的钱,既为梦想也为生活,当然上海的时候也过得滋润,但不免会想到以后的事儿,虽然现在这个盼望破灭了,在我还没有写完这个文章时候【下面生活的部分会说到】,但对于工作的发展肯定是有利的;努力吧




工作部分总结,内容真的不多,新入社会各处都需要学习,各处都需要认真,切勿松懈;既为自己也为将来;正反馈永远都会在你做出选择那一刻就开始显现的,即使不明显;


d804058cd0e6b476d57f7eccda390d0.jpg


生活

1、和女友分手



在对双方家人完全公开学历职业,身体状况,家庭背景以后,女方家庭不太愿意许这份亲事,理由是不够般配;以很多承诺,房车礼金孩子都不是问题以后,依旧无动于衷;女友也被说服,压力太大,扛不住,最后选择分开;我知道我的原因我的态度肯定是占大部分的,没做好自己该做的情感支撑;只能说,害,遗憾终将困其一生;


47f54f952e5ab441533ff3d48d1cde2.png




点亮更多的城市


没有忘记自己一直以来喜欢做的事情,就是旅游;游山玩水,闲云野鹤;
上半年去了几个城市,太原,西安,长沙,苏州;最后还转去了苏州哈哈哈


0c8c589f228141599b450e900d91fb4.jpg


0da20b2d67eaa5d8a1b309517fd50b2.jpg


0e8e0836c99009e3433b0ff21f1a606.jpg


1efc74fa9d338f1084e8bcfe0fd14a9.jpg


2cf8c88ba518d13e75854fbf3bcaf40.jpg


18a079711d5a9fee5630234a6808a7f.jpg


72c721f34630897b022e3c64eea5a60.jpg


79a503a738f10d784bd7c07c22e037a.png


201f6cd0491dfa13b7baf215c0e023e.jpg


2820dc3c8eee657c6786c5401f4d5a0.jpg


28003994205371e78b317719cc8a203.jpg


ad129f5e32b2663f42ecbd699bc6382.jpg


b0b0b80c9cc29bb9addb31de096a2d7.jpg


d8af16d1467abd26102e93e7420dad7.jpg


dbe11b157d1a75c7830b68700298fbd.jpg


e6919033d19f70baa1097f81aab7945.jpg


ed7ffd420892a8946f704f6adcca9ef.jpg


f0f14a836a16abbac3c91ca3dda4315.jpg


fa69a14a886061e753aad7cebc4e8c3.jpg


fffb41b013f68a1f2e9bd3e85d5709d.jpg




坚持读实体书


总觉得电子书我看不下去,即使看了也没有特别多的思考过程,反而拿到实体书的时候,我一看就是一下午,或者我会反复琢磨一小段他想要描述的故事;当然目前还只是停留在一些悬疑小说,休闲生活的书籍,名著,心理学等等经验的书籍我还是看得少。怎么说呢,我觉得应该是故事导出对生活的思考,千古无同局,有的都是处理问题的心态和思维;
又或者我也就只是拿它打发时间。


02fd964c67fb27fe801e187632fc0cb.jpg


41fe086f97e437b26050fc9ce8117b7.jpg


73ebab8ff382770c8e2556da83a1a3c.jpg


875807d2a0549b2ab110f75a2747fb1.jpg


a29d817254dde31d77e50d43dc71cc1.jpg


b46c9643e2fc6a341955d8a516971da.jpg




PC游戏


奥日依旧没有通关,它会是我很喜欢的画风,想要细细聘问;


饥荒也在玩,即使过了十年,存活的技巧我依然不会掌握,但是开心就开心在收集;


LOL偶尔会和三五好友一起打几把,大乱斗居多,下棋也会玩儿;


地平线是日常,排气声能让我很舒缓。





我属于一个重度的赛车迷,毕业两年多自己攒了钱就立马买了一辆二手马自达6,人生第一台车;


我也不知道是不是最近两年火起来的JDM风,激起我对跑山,跑环湖的兴趣,一个月都是去一到两次;现在好像马自达6也有点满足不了我了,也在计划换新的车计划,我现在属于那种拿买房的钱去买车来取悦自己的人,也许会不够成熟吧,又或许开心比什么都重要;


31ba1f26ae7210b98e3d6a8df3c55dd.jpg


a7c8663ad4c67b71d2ad84c6ec919cc.jpg


林拜先生说过:



人活着是一门艺术,


创造这门艺术需要你有高超的手艺,


人都有强项,都有短板,


成就成在扬长避短上。



是的,应该承认。


今天的晚霞很美,虽然它跟昨天、前天、明天相比没有什么不同,但依旧是未完待续...


744d2d66f21da7c78e035ef2719e60f.jpg


作者:Kuye
链接:https://juejin.cn/post/7244007111423803450
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

天马行空使用适配器模式

1. 前言 因为最近没有什么比较好的技术想要分享,所以来水一下文章,啊不对,是分享一下一些思路,和大家交流一下想法,没准能产生一些新的想法。这篇文章不具备权威,不一定正确,简单来说全都是我瞎吹。 开发这么久,多多少少也有了解过适配器模式。我还是一如既往的建议学...
继续阅读 »

1. 前言


因为最近没有什么比较好的技术想要分享,所以来水一下文章,啊不对,是分享一下一些思路,和大家交流一下想法,没准能产生一些新的想法。这篇文章不具备权威,不一定正确,简单来说全都是我瞎吹。


开发这么久,多多少少也有了解过适配器模式。我还是一如既往的建议学习设计模式,不能光靠看别人的文章,得靠自己的积累和理解源码的做法来得更切实际。


2. 浅谈适配器


我们都知道,适配器模式是一种结构型模式,结构型模式我的理解都有一个特点,简单来说就是封装了一些操作,就是隐藏细节嘛,比如说代理就是隐藏了通过代理调用实体的细节。


我还是觉得我们要重头去思考它是怎样的一个思路。如果你去查“适配器模式”,相信你看到很多关于插头的说法,而且你会觉得这东西很简单,但好像看完之后又感觉学了个寂寞。


还是得从源码中去理解。先看看源码中最经典的适配器模式的使用地方,没错,RecyclerView的Adapter。但是它又不仅仅只使用Adapter模式,如果拿它的源码来做分析反而会有些绕。但是我们可以进行抽象。想想Adapter其实主要的流程就是输入数据然后输出View给RecyclerView


然后又可以大胆的去思考一下,如果不定义一个Adapter,没有ViewHolder,要怎么实现这个效果?写一个循环,然后在循环里面做一些逻辑判断,然后创建对应的子View,添加到父View中 ,大概会这样做吧。那其实Adapter就帮我们做了这一步,并且还做了很多比较经典的优化操作,其实大概就是这样。


然后从这样的模型中,我大概是能看出了一些东西,比如使用Adapter是为了输入一些东西,也可以说是为了让它封装一些逻辑,然后达到某些目的,如果用抽象的眼光去看,我不care你封装的什么东西,我只要你达到我的目的(输出我的东西),RecyclerView的adapter就是输出View。这是其一,另外,在这个模型中我有一个使用者去使用这个Adaper,这个Apdater在这里不是一个概念,而是一个对象,我这个使用者通过Adaper这个对象给它一些输入,以此来实现我的某些目标


ok,结合Adapter模式的结构图看看(随便从网上找一张图)


image.png


可以看到这个模型中有个Client,有个Tagrget,有个Adapter。拿RecyclerView的Adapter来说,Client是RecyclerView (当然具体的addView操作是在LayoutManager中,这里是抽象去看),Adapter就是RecyclerView.Adapter,而Tagrget抽象去看就是对view的操作。


3. 天马行空的使用


首先一般使用官方的RecyclerView啊这些,都会提供自己的Adapter,但是会让人会容易觉得Adapter就是在复用View的情况下使用。而我的理解是,RecyclerView的复用是ViewHolder的思想,不是Adapter的思想,比如早期的ListView也有Adapter啊,当时出RecyclerView之前也没有说用到ViewHolder的这种做法(这是很多年前的事),我说这个是想要表达不要把Adapter和RecyclerView绑一起,这样会让思维受到局限。


对我而言,Adapter在RecyclerView中的作用是 “Data To View” ,适配数据而产出对应的View。


那我可以把Apdater的作用理解成“Object To Object” ,对于我来说,Object它可以是Data,可以是View,甚至可以是业务逻辑。


所以当我跳出RecyclerView这种传统的 “Data To View” 的思维模式之后,Adapter适配器模式可以做到的场景就很多,正如上面我理解的,Adapter简单来说就是 “Data To View” ,那我可以用适配器模式去做 “Data To Data” ,可以去做 “View To Data” ,可以去做 “Business To Business” , “Business To View” 等等,实现多种效果。


假设我这里做个Data To Data的场景 (强行举的例子可能不是很好)


我请求后台拿到个人的基本信息数据

data class PeopleInfo(
var name: String?,
var sex: Int?,
......
)

然后通过个人的ID,再请求服务端另外一个接口拿到成绩数据

data class ScoreInfo(
var language : Int,
var math : Int,
......
)

然后我有个数据竞赛的报名表对象。

data class MathCompetition(
var math : Int,
var name : String
)

然后一般我们使用到的时候就会这样赋值,假设这段代码在一个Competition类中进行

val people = getPeopleInfo()
val score = getScoreInfo()
val mathTable = MathCompetition(
score.math?,
people.name?,
......
)

就是深拷贝的一种赋值,我相信很多人的代码里面肯定会有

两个对象 A 和 B
A.1 = B.1
A.2 = B.2
A.3 = B.3
......

这样的代码,然后对象的熟悉多的话这个代码就会写得很长。当然这不会造成什么大问题,但是你看着看着,总觉得缺少一些美感,但是好像这玩意没封装不起来这种感觉。


如果用适配器来做的话,首先明确要做的流程是从Competition这个类中,将所有数据整合成MathCompetition对象,目标就是输出MathCompetition对象。那从适配器模式的模型上去看,client就是Competition,是它的需求,Taget就是getMathCompetition,输出mathTable,然后我们可以写一个Adapter。

class McAdapter {

var mathCompetition : MathCompetition? = null

init {
// 给默认值
mathCompetition = MathCompetition(0, "name", ......)
}

fun setData(people : PeopleInfo? = null, score : ScoreInfo? = null){
people?.let {
mathCompetition?.name = it.name
......
}

score?.let {
mathCompetition?.math = it.math
......
}
}

fun getData() : MathCompetition?{
return mathCompetition
}

}

然后在Competition中就不需要直接引用MathCompetition,而是设置个setAdapter方法,然后需要拿数据时再调用adapter的getData()方法,这样就恰到好处,不会把这些深拷贝方式的赋值代码搞得到处都是。 这个Demo看着好像没什么,但是真碰到了N合1这样的数据场景的时候,使用Adapter显然会更安全。


我再简单举一个Business To View的例子吧。假设你的app中有很几套EmptyView,比如你有嵌入到页面的EmptyView,也有做弹窗类型的EmptyView,我们一般的做法就是对应的页面的xml文件中直接写EmptyView,那这些EmptyView的代码就会很分散是吧。OK,你也想整合起来,所以你会写个EmptyHelper,大概是这个意思,用单例写一个管理EmptyView的类,然后里面统一封装对EmptyView的操作,一般都会这样写。 其实如果你让我来做,我可能就会用适配器模式去实现。 ,当然也有其他办法能很好的管理,这具体的得看心情。


写一个Adapter,我这里写一段伪代码,应该比较容易能看懂

class EmptyAdapter() {
// 这样语法要伴生,懒得写了,看得懂就行
const val STATUS_NORMAL = 0
const val STATUS_LOADING = 1
const val STATUS_ERROR = 2

private var type: Int = 0
var parentView: ViewGroup? = null
private var mEmptyView: BaseEmptyView? = null
private var emptyStatus = 0 // 有个状态,0是不显示,1是显示加载中,2是加载失败......

init {
createEmptyView()
}

private fun createEmptyView() {
// 也可以判断是否有parentView决定使用哪个EmptyView等逻辑
mEmptyView = when (type) {
0 -> AEmptyView
1 -> BEmptyView
2 -> CEmptyView
else -> {
AEmptyView
}
}
}

fun setData(status: Int) {
when (status) {
0 -> parentView?.removeView(mEmptyView)
1 -> mEmptyView?.showLoading()
2 -> mEmptyView?.showError()
}
}

fun updateType(type: Int) {
setData(0)
this.type = type
createEmptyView()
}
}

然后在具体的Activity调用的时候,可以

val emptyAdapter = EmptyAdapter(getContentView())
// 然后在每次要loading的时候去设置adapter的的状态
emptyAdapter.setData(EmptyAdapter.STATUS_LOADING)
emptyAdapter.setData(EmptyAdapter.STATUS_NORMAL)
emptyAdapter.setData(EmptyAdapter.STATUS_ERROR)

可以看出这样做就有几个好处,其一就是不用每个xml都写EmptyView,然后也能做到统一的管理,和一些人写的Helper这种的效果类似,最后调用的方法也很简单,你只需要创建一个Adaper,然后调用它的setData就行,我们的RecyclerView也是这样的,在外层去创建然后调用Adapter就行。


4. 总结


写这篇文章主要是为了水,啊不对,是为了想说明几个问题:

(1)开发时要善于跳出一些限制去思考,比如RecyclerView你可能觉得适配器模式就和它绑定了,就和View绑定了,有View的地方才能使用适配器模式,至少我觉得不是这样。

(2)学习设计模式,只去看一些介绍是很难理解的,当然要知道它的一个大致的思想,然后要灵活运用到开发中,这样学它才有用。

(3)我对适配器模式的理解就是 Object To Object,我可以去写ViewAdapter,可以去写DataAdapter,也可以去写BusinessAadpter,可以用这个模式去适配不同的场景,利用这个思想来使代码更加合理。


当然最后还是要强调一下,我不敢保证我说的就是对的,我肯定不是权威的。但至少我使用这招之后的的新代码效果要比一些旧代码更容易维护,更容易扩展。


作者:流浪汉kylin
链接:https://juejin.cn/post/7242623772301459517
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

何谓实事求是地工作?

提到实事求是,大家第一时间会想到什么?我想大部分是客观,事实,脚踏实地?这么一想,大家都会觉得,自己挺实事求是的呀,没毛病。但是,我会经常在工作中感受到不是那么实事求是的行为,比如张嘴就来,不带思考,做事全靠猜的行为,真太多了。 随着我这两年的学习和总结,我越...
继续阅读 »

提到实事求是,大家第一时间会想到什么?我想大部分是客观,事实,脚踏实地?这么一想,大家都会觉得,自己挺实事求是的呀,没毛病。但是,我会经常在工作中感受到不是那么实事求是的行为,比如张嘴就来,不带思考,做事全靠猜的行为,真太多了。


随着我这两年的学习和总结,我越发觉得实事求是非常重要,并把它视为我做事情和成长的基石。对于实事求是,我主要有以下 3 层理解。


首先,尊重客观事实,探寻真理。我们要承认事实,即使这个事实有多么的难以置信,但存在即是合理,我们首先要尊重它,承认它。然后我们还要积极主动地面对它,探寻事实背后的真理,获得真知,这样才能真正的成长,并有可能寻得机会。当某个事情的进展超出自己预期的时候,我们正确的态度应该是思考为什么会这样,而不是去想对错得失。


其次,数据说话,数据驱动。事实如何去量化?答案是数据。使用数据去表达事实,是我们程序员应该有的技能。工作的本质就是解决问题,之前的文章有讲解,问题就是理想状态和现实状态之间的差别,因此,我们在工作当中做的每一项决策的依据、制定的每一个目标,都应该用数据说话。我们应该使用数据表达现状,并使用数据衡量目标,驱动自己去工作。一些沟通的细节就能够体现出他是不是在实事求是地工作,比如“这个页面加载太慢了,需要优化”。那这个页面加载到底有多慢?业界标准或者竞品的加载耗时是多少?优化的目标值是多少?


最后,从客观事实中获取反馈,不断迭代。工作中想要获得成功和成长,最核心的一个环节是反馈。很多人没有意识到这点。更多的人没有意识到的是,获取反馈其实很简单,随处都是。敏捷开发、精益创业、增长黑客,这些理论的底层核心都是基于事实和数据的反馈,不断迭代改进自己的产品,从而获得成功。对于个人成长来说也是一样的,我们要从客观事实中获取反馈,思考总结,不断迭代自己的能力。


总结一下,实事求是地工作有 3 个层次,首先,要正视事实,并主动探究真理;然后我们慢慢地开始用数据驱动自己的工作;最后让数据驱动变成循环,不断迭代,并把这种循环融入到各个方面,包括工作和个人成长,让它成为自己下意识的动作。


我在努力学习和践行实事求是地工作,我也希望我的团队可以用实事求是的态度来工作,以此文共勉!


作者:潜龙在渊灬
链接:https://juejin.cn/post/7241394138260160568
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

🐞 如何成为一名合格的“中级开发”

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️ 在这个系列里面的上一篇文章中,我跟大家分享了怎么做一个专业的开发者,还有工作中要注意什么事情。 这是我们人生很重要的一步,因为只有学会怎么开始,才能慢慢变优秀,才能一步步往上进步。 如果你是第一次看这个系列,我强烈建...
继续阅读 »

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️


在这个系列里面的上一篇文章中,我跟大家分享了怎么做一个专业的开发者,还有工作中要注意什么事情。


这是我们人生很重要的一步,因为只有学会怎么开始,才能慢慢变优秀,才能一步步往上进步。


如果你是第一次看这个系列,我强烈建议你回去看看我之前写的两篇文章,说不定能对你有帮助。




  1. 🎖️怎么知道我的能力处于什么水平?我该往哪里努力?

  2. 🚗我毕业/转行了,怎么适应我的第一份开发工作?



其实我想写这篇文章已经很久了,可是一直想不出来怎么写,找了很多资料也没用。


确实憋不出来,中间还水了一篇“JavaScript冷饭”文章。天可是天天炒冷饭不好吃啊,写那些水文总会心生愧疚,感觉对不起你们哈哈。


今天,我们继续聊一聊,当我们进入这个角色一两年后,该怎么摆脱“初级”头衔,迈入“中级”阶段呢?😎


🎖️ 中级开发的显著特点:“骄傲”



注意事项:


我接下来提及的内容可能很多大佬跟我的意见是不同的。


也有可能我的知识有限,我只涵盖了前端开发工程师的部分,对其他岗位的开发工程师不了解,可能我说的指标并不一定能和贵公司考核时所授予给的职称相对应。


我这里说的是衡量开发人员技能、知识和整体能力的一般指标


它会根据所在的领域而变化,比如前端、后端、数据等等都不太一样。


虽然具体的工具、技术甚至架构知识可能有所不同,但是我说的一般原则应该是可以广泛适用的。


如果觉得我说错了,请在评论区交流。😊



“骄傲”他拥有的知识


我这里要说一个很有意思的特点,这个特点一定能够显著的提醒你,你肯定到了“中级”级别。


可能你觉得我要说的是开玩笑,但是我是认真的,我觉得大部分的“中级开发”都经历过这个事情。


如果你认真看过我的这篇文章,我希望你可以在评论区互动,在评论区跟大家说一声:你不是一个人😊


当你到了中级水平,你心里一定有一个想法。那就是:


我已经学会了我现在做的事情,以及要用的所有东西了!


再说得清楚一点就是:


“我已经完全会用JavaScript了,我对HTML很熟悉了,我对数据库没问题!”


“我已经完全会用Vue了,我也会用Angular开发”


这个时候的“中级开发”,觉得他已经有了这个领域需要的能力了。



当然啦,这不是一种傲慢,我想表达的“骄傲”不是贬义词。


这个阶段只是我们成长中必须经历的一个阶段。



“骄傲”不是一件坏事


其实刚刚我也 tips 了一下,这个是我们一定会经历的阶段。


说明你真的要开始要进入“中级开发“这个角色了。


就像我们小时候我们都会觉得,爸爸妈妈什么都不知道,我们才更明白一样


当你拥有”骄傲“,你才开始真正走自己的路。这个时候你才真正开始独立思考。


这意味着你已经积累了足够的知识和经验,可以继续精进设计模式、最佳实践等这些学科以拔高你的知识。


简单的东西已经不能吸引你了。


🚩 中级开发应该掌握什么?


现在你是中级开发了,你需要看看自己是不是能做到下面这些事情。


这些“新”的东西可以让中级开发更有经验,也更能帮助团队。


编程能力:



  1. 很清楚不同的系统(API、模块、包等)怎么互相连接

  2. 熟练使用编程工具(IDE、GIT等)

  3. 知道怎么实现一般的需求

  4. 遇到bug的时候,知道从哪里找原因和解决办法

  5. 知道怎么优化代码和重构代码

  6. 知道怎么提高性能

  7. 知道怎么用面向对象的程序设计

  8. 知道常用的软件架构模式(MVC、MVVM、MVP、MVI等)

  9. 知道编程语言的一些特点(函数式编程)

  10. 知道怎么部署系统应用

  11. 知道怎么用数据库索引

  12. 知道怎么用数据库表迁移

  13. 知道怎么用数据库分片技术


社会能力:



  1. 可以偶尔跟产品经理(客户)沟通

  2. 是团队的主力


开始优雅:



  1. 代码模块开始按照设计模式来写

  2. 对烂代码有敏感度和重构能力


等等


📌 对中级开发的一些建议


也许现在在读文章的你已经是一位中级开发的存在了,我现在有一些建议想要分享给你!


找一个自己感兴趣的开发者社区加入


为什么我们常说“好的团队创造个人”呢


因为当你真的参与到了重要或高价值的项目时,你真的比一个人漫无目的地学习更快地获得经验。


而且当你真正在团队中贡献力量地时候,你地团队,你的组长,你的领导都会知道,把事情交给你,你就能把自己做好。


在这个过程中,你能积累经验并在你的团队中声名鹊起(这不是名气,而是知名度),那么当新的机会出现时,你就能很快地把握住。


跳出舒适区


跟我上一篇提到的给初级开发的建议类似,你一定要经常的跳出自己的舒适区,不然你不会有毅力坚持学习。


而且,特别是在互联网行业,学习能力是个硬性指标,如果无法坚持下去,很容易就会被淘汰。


这样做可以开阔你的眼界,让你的知识面更广。最终,你会逐渐掌握开发的技巧,面对这些全新的知识领域时,能更快、更准确地找到重点并掌握它们。


但是只要你坚持下去,未来的你一定会与其他人拉开差距。


找到你的导师


这一点在上一篇我也强调过了。你的开发生涯,不能只靠你自己摸索。


你需要有人给你提供想法并能够从中学习。特别是在“中级开发”阶段。


导师可以帮助你不会在某些技术问题或者人生问题上钻牛角尖,他可以拉你一把,避免你浪费很多时间。


这个人可以是你团队中的某个人。


也可以是网络上开发者社区中认识的某位博主。


找到你信任的人(或者更可能是一群人),你可以跟他们问问题和说想法!


找到可以指导你的导师,让你能够突破当前的认知。你的未来将逐步变得清晰起来。


持续学习


这个没什么好说的,在这内卷的社会中,如果没有润的资本和能力,不如在持续学习中等待破局的机会!




🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?当你处于这个阶段时,你发现什么对你帮助最大?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨


作者:道长王jj
链接:https://juejin.cn/post/7243203041872412731
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

程序员创业:从技术到商业的转变

作为一名程序员,我们通常会聚焦于编程技能和技术能力的提升,这也是我们日常工作的主要职责。但是,随着技术的不断发展和市场的变化,仅仅依靠技术能力已经不足以支撑我们在职场上的发展和求职竞争力了。所以,作为一名有远大理想的程序员,我们应该考虑创业的可能性。 为什么程...
继续阅读 »

作为一名程序员,我们通常会聚焦于编程技能和技术能力的提升,这也是我们日常工作的主要职责。但是,随着技术的不断发展和市场的变化,仅仅依靠技术能力已经不足以支撑我们在职场上的发展和求职竞争力了。所以,作为一名有远大理想的程序员,我们应该考虑创业的可能性。


为什么程序员要创业?


创业其实并非只适用于商学院的毕业生或者有创新理念的企业家。程序员在业内有着相当高的技术储备和市场先知,因此更容易从技术角度前瞻和切入新兴市场,更好地利用技术储备来实现创业梦想。


此外,创业可以释放我们的潜力,同时也可以让我们找到自己的定位和方向。在创业的过程中,我们可能会遇到各种挑战和困难,但这些挑战也将锻炼我们的意志力和决策能力,让我们更好地发挥自己的潜力。


创业需要具备的技能


作为一名技术人员,创业需要具备更多的技能。首先是商业和运营的技能:包括市场分析、用户研究、产品策划、项目管理等。其次是团队管理和沟通能力,在创业的过程中,人才的招聘和管理是核心问题。


另外,还需要具备跨界合作的能力,通过开放性的合作与交流,借助不同团队的技术和资源,完成创业项目。所以我们应该将跨界合作看作是创业过程中的重要选择,选择和加强自己的跨界交流和合作能力,也能为我们的企业注入活力和创新精神。


如何创业?


从技术到商业的转变,从最初想法的诞生到成熟的企业的创立,都需要一个创业的路线图。以下是一些需要注意的事项:




  1. 研究市场:了解市场趋势,分析需求,制定产品策略。可以去参加行业论坛,争取到专业意见和帮助。




  2. 制定商业计划:包括产品方案、市场营销、项目管理、团队建设等。制定一个系统的商业计划是投资者和团队成员对创业企业的认可。




  3. 招募团队:由于我们一般不是经验丰富的企业家,团队的选择尤为重要。要找的不仅要是技能和经验匹配的团队,更要找能一起携手完成创业项目的合作者。




  4. 行动计划:从实现规划步入到实战行动是创业项目的关键。按部就班地完成阶段性任务,控制实施进度和途中变化,在完成一个阶段后可以重新评估计划。




  5. 完成任务并分析:最后,团队成员需要根据企业进展,完整阶段性的目标,做自己的工作。及时完成考核任务并一起分享数据分析、事件解决和项目总结等信息,为项目下一阶段做出准确预测。




结语


创业是一条充满挑战性和机遇的路线,也是在我们的技术和业务的进一步升级中一条非常良好的通道。越来越多的技术人员意识到了自己的潜力,开始考虑自己创业的可能性。只要学会逐步掌握创业所需的技能和知识,并制订出详细的创业路线图,大可放手去尝试,才能最终实现自己心中的创业梦想。


作者:郝学胜
链接:https://juejin.cn/post/7240465997002047547
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin特性实现高拓展性Logger

前言 在Android中记录日志一般是使用Android自带的Log工具, 但是实际项目中,我们经常需要统一控制项目的日志开关或过滤等级,有时还需要将日志写入到文件中。 目前除了自己实现外,基本都是通过第三方的Log日志库比如JakeWharton/timbe...
继续阅读 »

前言


在Android中记录日志一般是使用Android自带的Log工具,


但是实际项目中,我们经常需要统一控制项目的日志开关过滤等级,有时还需要将日志写入到文件中。


目前除了自己实现外,基本都是通过第三方的Log日志库比如JakeWharton/timberorhanobut/logger ,但是有些情况仍然实现起来困难。


比如:



  • 多模块或封装SDK的情况,子模块也用到日志框架,如何单独管理子模块的日志开关?

  • Java library中没有Android Log类引入日志模块报错,同理使用JUnit做单元测试的时候日志模块报错。


并且替换项目中的Log是痛苦的,因此就需要一个可拓展的日志框架尤为重要。


和大多数流行的Logger框架不同,本文实现的Logger不是对各种数据进行美化输出,本文充分利用Kotlin的拓展函数实现了一个灵活配置的轻量Logger,它具有以下特性:



  • 控制模块中的Logger配置

  • 输出到文件等多个目标,控制输出线程

  • 支持拦截器,实现过滤,格式化

  • 支持Jvm使用,支持JUnit


项目地址 Logger


原理和


核心只有3个类




  1. Logger 日志的操作入口:主要保证稳定简单易用,用作门面。




  2. LogPrinter 日志输出目标:配置日志输出。




  3. LogLevel 日志输出等级:(VERBOSE,DEBUG,INFO,WARNING,ERROR,WTF)




Logger.png


日志记录


通过Logger 可以获取全局实例。


输出日志有许多等级,和android的Log等级是一致的:

Logger.v("Test verbose")
Logger.d("Test debug")
Logger.i("Test info")
Logger.w("Test warning")
Logger.e("Test error")
Logger.wtf("Test wtf")

Logger.v { "Test verbose" }
Logger.d { "Test debug" }
Logger.i { "Test info" }
Logger.w { "Test warning" }
Logger.e { "Test error" }
Logger.wtf { "Test wtf" }


通过Logger["subTag"]可以生成子Logger
默认情况,子LoggerlevellogPrinter均继承自父Logger,tag"$父tag-$subTag"

Logger.tag = "App" //全局Logger的tag设置为“APP”
class XxActivity {
val logger = Logger["XxActivity"] //二级Logger的tag为“APP-XxActivity”
val logger = loggerForClass() //使用当前类名生成二级Logger的tag为“APP-XxActivity”

inner class XxFragment {
val fragmentLogger = logger["XxFragment"]//三级Logger的tag为“APP-XxActivity-XxFragment”
}
}


日志配置


Logger总共只有4个属性:



  • level 过滤等级(VERBOSE,DEBUG,INFO,WARNING,ERROR,WTF)

  • tag 日志TAG

  • logPrinter 日志输出目标,比如Android Logcat,文件,标注输出流,Socket等。

  • loggerFactory 生产子Logger的工厂,默认情况,子LoggerlevellogPrinter均继承自父Logger,tag"$父tag-$subTag"


如下示例配置了顶级Loggerleveltag,输出目标为AndroidLogcat,同时在子线程把WARNING等级的日志按一定格式输出到文件"warning.log"中,
同时"SubModule"Logger的输出等级设置为ERROR等级。

Logger.level = LogLevel.VERBOSE
Logger.tag = "AppName"
Logger.logPrinter = AndroidLogPrinter()
.logAlso(FileLogPrinter({ File("warning.log") })
.format { _, _, messageAny, _ -> "【${Thread.currentThread().name}】:$messageAny" }
.logAt(Executors.newSingleThreadExecutor())
.filterLevel(LogLevel.WARNING)
)
Logger.loggerFactory = { subTag ->
Logger("$tag-$it", level, logPrinter).also { child ->
if (child.tag == "SubModule") {
logger.level = LogLevel.ERROR
}
}
}


实现


Logger功能实现:


因为Logger是门面,所以提供便捷的方法来使用,而真正的写入日志代理给LogPrinter

open class Logger(
var tag: String = "LOG",
var level: LogLevel = LogLevel.VERBOSE,
var logPrinter: LogPrinter = createPlatformDefaultLogPrinter(),
) {
fun v(message: Any?) = log(LogLevel.VERBOSE, message, null)
fun log(level: LogLevel, message: Any?, throwable: Throwable? = null) {
if (this.level <= level) {
logPrinter.log(level, tag, message, throwable)
}
}
//省略其他等级...

var loggerFactory: (childTag: String) -> Logger = ::defaultLoggerFactory

/**
* 创建子Logger
* @param subTag 次级tag,一般为模块名
*/
operator fun get(subTag: String): Logger = loggerFactory(subTag)

companion object INSTANCE : Logger(tag = "Logger")

}
enum class LogLevel(val shortName: String) {
VERBOSE("V"),
DEBUG("D"),
INFO("I"),
WARNING("W"),
ERROR("E"),
WTF("WTF")
}
fun interface LogPrinter {
fun log(level: LogLevel, tag: String, messageAny: Any?, throwable: Throwable?)
}

LogPrinter拓展实现


首先实现对LogPrinter进行拦截,后续的功能都通过拦截器实现。


拦截器

/**
* 拦截器
* logPrinter 被拦截对象
*/
typealias LogPrinterInterceptor = (logPrinter: Logger.LogPrinter, level: Logger.LogLevel, tag: String, messageAny: Any?, throwable: Throwable?) -> Unit

inline fun Logger.LogPrinter.intercept(crossinline interceptor: LogPrinterInterceptor) =
Logger.LogPrinter { level, tag, messageAny, throwable ->
interceptor(this@intercept, level, tag, messageAny, throwable)
}

添加额外的LogPrinter


添加一个额外的LogPrinter,也可看作将2个LogPrinter合并成1个。想要添加多个输出目标时使用。

fun Logger.LogPrinter.logAlso(other: Logger.LogPrinter) =
intercept { logPrinter, level, tag, messageAny, throwable ->
logPrinter.log(level, tag, messageAny, throwable)
other.log(level, tag, messageAny, throwable)
}


设置日志记录线程


控制LogPrinter的输出线程

fun Logger.LogPrinter.logAt(executor: Executor) =
intercept { logPrinter, level, tag, messageAny, throwable ->
executor.execute {
logPrinter.log(level, tag, messageAny, throwable)
}
}

格式化


控制LogPrinter的输出的格式,比如csv格式,Json格式等。

typealias LogFormatter = (level: Logger.LogLevel, tag: String, messageAny: Any?, throwable: Throwable?) -> String

fun Logger.LogPrinter.format(formatter: LogFormatter) =
intercept { logPrinter, level, tag, messageAny, throwable ->
val formattedMessage = formatter(level, tag, messageAny, throwable)
logPrinter.log(level, tag, formattedMessage, throwable)
}

日志过滤


LogPrinter中输出的日志进行过滤,可以根据tagmessagelevelthrowable进行组合判断来过滤。

fun Logger.LogPrinter.filter(
predicate: (
level: Logger.LogLevel,
tag: String,
messageAny: Any?,
throwable: Throwable?
) -> Boolean
) =
intercept { logPrinter, level, tag, messageAny, throwable ->
if (predicate(level, tag, messageAny, throwable)) {
logPrinter.log(level, tag, messageAny, throwable)
}
}


Logger拷贝


Logger为原型拷贝一个新Logger,和生成子Logger不同,它并不是通过loggerFactory生成的,并且tag也是拷贝的。

/**
* 拷贝
*/
fun Logger.copy(
tag: String = this.tag,
level: Logger.LogLevel = this.level,
logPrinter: Logger.LogPrinter = this.logPrinter,
loggerFactory: (childTag: String) -> Logger = ::defaultLoggerFactory,
) = Logger(tag, level, logPrinter).also { it.loggerFactory = loggerFactory }

Json格式化


因为并没有引入任何Android类和Json序列化库,所以没有内置。在此提供Gson示例


方式1,使用LogPrinter拓展


适用于该Logger所有日志都需要转Json的情况

val gson = GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create()

fun Logger.jsonLogger() =
copy(logPrinter = logPrinter.format { _, _, messageAny, _ -> gson.toJson(messageAny) })

//使用
fun testLogJsonLogger() {
val logger = Logger.jsonLogger()
logger.d {
arrayOf("hello", "world")
}
logger.i {
mapOf(
"name" to "tom",
"age" to 19,
)
}
}

方式2,拓展Logger方法


通过拓展Logger方法实现,适用于Logger的部分数据需要输出为Json模式。

inline fun Logger.logJson(level: Logger.LogLevel = Logger.LogLevel.INFO, any: () -> Any) {
log(level, block = { gson.toJson(any()) })
}

//使用
fun testLogJsonExt() {
Logger.logJson {
mapOf(
"name" to "tom",
"age" to 19,
)
}
}


拓展使用


这些拓展方法可以连续调用,就像使用RxJava一样。

Logger.logPrinter = ConsoleLogPrinter()
.format { _, tag, messageAny, _ -> "$tag : $messageAny\n" }
.logAlso(ConsoleLogPrinter()
.format { _, tag, messageAny, _ ->//添加分割线 tag,时间,message转json,同时加上堆栈信息
"""---------------------------\n $tag ${currentTime()} ${Json.toJson(messageAny)} \n${Thread.currentThread().stackTrace.contentToString()}"""
}
.filterLevel(LogLevel.INFO))//仅记录level在INFO及以上的
.logAlso(ConsoleLogPrinter()
.format { _, tag, messageAny, _ -> "$tag :《$messageAny》\n" }
.filter { _, tag, _, _ -> tag.contains("CHILD") })//仅记录tag包含CHILD

混淆


如果通过混淆去除日志信息,可按如下配置。

-assumenosideeffects class me.lwb.logger.Logger {
public *** d(...);

public *** e(...);

public *** i(...);

public *** v(...);

public *** log(...);

public *** w(...);

public *** wtf(...);

}

总结


本文主要使用了Kotlin拓展和高阶函数实现了一个拓展性高的Logger库,通过拓展方法实现线程切换,多输出,格式化等,同时通过配置全局logFactory的方法可以在不修改子模块代码的情况下去控制子模块Logger的level等信息。


该库十分精简,加上拓展和默认实现总代码小于300行,不依赖Android库第三方库,可以在纯Jvm程序中使用,也可在Android程序中使用。


作者:丨小夕
链接:https://juejin.cn/post/7244002241845166138
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

关于晋升的一点思考

晋升是一个极好的自我review的机会,不,应该是最好,而且没有之一。 晋升是最好的自我review的一次机会,不管有没有晋升成功,只要参加了晋升,认真准备过PPT,就已经包赚不赔了。 总的来说,晋升的准备工作充分体现出了——功夫在平时。平时要是没有两把刷子,...
继续阅读 »

晋升是一个极好的自我review的机会,不,应该是最好,而且没有之一。

晋升是最好的自我review的一次机会,不管有没有晋升成功,只要参加了晋升,认真准备过PPT,就已经包赚不赔了。


总的来说,晋升的准备工作充分体现出了——功夫在平时。平时要是没有两把刷子,光靠答辩准备的一两个月,是绝无可能把自己“包装”成一个合格的候选人的。

下面整体剖析一下自己在整个准备过程中的观察、思考、判断、以及做的事情和拿到的结果。


准备工作


我做的第一件事情并不是动手写PPT,而是搜集信息,花了几天在家把网上能找到的所有关于晋升答辩的文章和资料全撸了一遍,做了梳理和总结。


明确了以下几点:



  • 晋升是在做什么

  • 评委在看什么

  • 候选人要准备什么

  • 评判的标准是什么

  • 常见的坑有哪些


首先要建立起来的,是自己对整个晋升的理解,形成自己的判断。后面才好开展正式的工作。



写PPT


然后开始进入漫长而又煎熬的PPT准备期,PPT的准备又分为四个子过程,并且会不断迭代进行。写成伪代码就是下面这个样子。

do {
确认思路框架;
填充内容细节;
模拟答辩;
获取意见并判断是否还需要修改;
} while(你觉得还没定稿);

我的PPT迭代了n版,来来回回折腾了很多次,思路骨架改了4次,其中后面三次均是在准备的后半段完成的,而最后一次结构大改是在最后一周完成的。这让我深深的觉得前面准备的1个月很多都是无用功。


迭代,迭代,还是迭代


在筹备的过程中,有一个理念是我坚持和期望达到的,这个原则就是OODA loop ( Boyd cycle)


OODA循环是美军在空战中发展出来的对敌理论,以美军空军上校 John Boyd 为首的飞行员在空战中驾驶速度慢火力差的F-86军刀,以1:10的击落比完胜性能火力俱佳的苏联米格-15。而Boyd上校总结的结论就是,不是要绝对速度快,而是要比对手更快的完成OODA循环


而所谓的OODA循环,就是指 observe(观察)–orient(定位)–decide(决策)–act(执行) 的循环,是不是很熟悉,这不就是互联网的快速迭代的思想雏形嘛。


相关阅读 what is OODA loop

wiki.mbalib.com/wiki/包以德循环 (from 智库百科)

en.wikipedia.org/wiki/OODA_l… (from Wikipedia)


看看下图,PPT应该像第二排那样迭代,先把框架确定下来,然后找老板或其他有经验的人对焦,框架确定了以后再填充细节。如果一开始填充细节(像第一排那样),那么很有可能越改越乱,最后一刻还在改PPT。


btw,这套理论对日常工作生活中的大部分事情都适用。


一个信息论的最新研究成果


我发现,程序员(也有可能是大部分人)有一个倾向,就是show肌肉。明明很简单明了的事情,非要搞得搞深莫测,明明清晰简洁的架构,非要画成“豆腐宴”。


晋升述职核心就在做一件事,把我牛逼的经历告诉评委,并让他们相信我牛逼。


所以,我应该把各种牛逼的东西都堆到PPT里,甚至把那些其实一般的东西包装的很牛逼,没错吧?


错。


这里面起到关键作用的是 “让他们相信我牛逼” ,而不是“把我牛逼的故事告诉评委”。简单的增大的输出功率是不够的,我要确保评委能听进去并且听懂我说的东西,先保证听众能有效接收,再在此基础上,让听众听的爽。


How?


公式:喜欢 = 熟悉 + 意外


从信息论的角度来看,上面的公式说的就是旧信息和新信息之间要搭配起来。那么这个搭配的配比应该是多少呢?


这个配比是15.87% ——《科学美国人》


也就是说,你的内容要有85%是别人熟悉的,另外15%是能让别人意外的,这样就能达到最佳的学习/理解效果。这同样适用于心流、游戏设计、神经网络训练。所以,拿捏好这个度,别把你的PPT弄的太高端(不知所云),也别搞的太土味(不过尔尔)。


能够否定自己,是一种能力


我审视自己的时候发现,很多时候,我还保留一张PPT或是还持续的花心思做一件事情,仅仅是因为——舍不得。你有没有发现,我们的大脑很容易陷入“逻辑自洽”中,然后越想越对,越想越兴奋。


千万记得,沉没成本不是成本,经济学里成本的定义是放弃了的最大价值,它是一个面向未来的概念,不面向过去。


能够否定和推翻自己,不留恋于过去的“成就” ,可以帮助你做出更明智的决策。


我一开始对好几页PPT依依不舍,觉得自己做的特牛逼。但是后来,这些PPT全被我删了,因为它们只顾着自己牛逼,但是对整体的价值却不大,甚至拖沓。


Punchline


Punchline很重要,这点我觉得做的好的人都自觉或不自觉的做到了。想想,当你吧啦吧啦讲的时候,评委很容易掉线的,如果你没有一些点睛之笔来高亮你的成果和亮点的话,别人可能就糊里糊涂的听完了。然后呢,他只能通过不断的问问题来挖掘你的亮点了。


练习演讲


经过几番迭代以后,PPT可以基本定稿,这个时候就进入下一个步骤,试讲。


可以说,演讲几乎是所有一线程序员的短板,很多码农兄弟们陪电脑睡的多了,连“人话”有时候都讲不利索了。我想这都要怪Linus Torvalds的那句


Talk is cheap. Show me the code.


我个人的经验看来,虽然成为演讲大师长路漫漫不可及,但初级的演讲技巧其实是一个可以快速习得的技能,找到几个关键点,花几天时间好好练几遍就可以了,演讲要注意的点主要就是三方面:



  • 形象(肢体语言、着装等)

  • 声音(语速、语调、音量等)

  • 文字(逻辑、关键点等)



演讲这块,我其实也不算擅长,我把仅有的库存拿出来分享。


牢记表达的初衷


我们演讲表达,本质上是一个一对多的通信过程,核心的目标是让评委或听众能尽可能多的接受到我们传达的信息


很多程序员同学不善于表达,最明显的表现就是,我们只管吧啦吧啦的把自己想说的话说完,而完全不关心听众是否听进去了。


讲内容太多


述职汇报是一个提炼的过程,你可能做了很多事情,但是最终只会挑选一两件最有代表性的事情来展现你的能力。有些同学,生怕不能体现自己的又快又猛又持久,在PPT里塞了太多东西,然后又讲不完,所以只能提高语速,或者囫囵吞枣、草草了事。


如果能牢记表达的初衷,就不应该讲太多东西,因为听众接收信息的带宽是有限的,超出接收能力的部分,只会转化成噪声,反而为你的表达减分。


过度粉饰或浮夸


为了彰显自己的过人之处,有时候会自觉或不自觉的把不是你的工作也表达出来,并没有表明哪些是自己做的,哪些是别人做的。一旦被评委识破(他本身了解,或问问题给问出来了),那将会让你陈述的可信度大打折扣。


此外,也表达的时候也不要过分的浮夸或张扬,一定的抑扬顿挫是加分的,但过度浮夸会让人反感。


注意衔接


作为一个演讲者,演讲的逻辑一定要非常非常清晰,让别人能很清晰明了的get到你的核心思路。所以在演讲的时候要注意上下文之间的衔接,给听众建设心理预期:我大概会讲什么,围绕着什么展开,分为几个部分等等。为什么我要强调这个点呢,因为我们在演讲的时候,很容易忽略听众的感受,我自己心里有清楚的逻辑,但是表达的时候却很混乱,让人一脸懵逼。


热情


在讲述功能或亮点的时候,需要拿出自己的热情和兴奋,只有激动人心的演讲,才能抓住听众。还记得上面那个分布图吗?形象和声音的占比达到93%,也就是说,你自信满满、热情洋溢的说“吃葡萄不吐葡萄皮”,也能打动听众。


第一印象


这个大家都知道,就是人在最初形成的印象会对以后的评价产生影响 。

这是人脑在百万年进化后的机制,可以帮助大脑快速判断风险和节省能耗——《思考,快与慢》

评委会刻意避免,但是人是拗不过基因的,前五分钟至关重要,有经验的评委听5分钟就能判断候选人的水平,一定要想办法show出你的与众不同。可以靠你精心排版的PPT,也可以靠你清晰的演讲,甚至可以靠一些小 trick(切勿生搬硬套)。


准备问题


当PPT准备完,演讲也练好了以后,不出意外的话,应该没几天了。这个时候要进入最核心关键的环节,准备问题。


关于Q&A环节,我的判断是,PPT和演讲大家都会精心准备,发挥正常的话都不会太差。这就好像高考里的语文,拉不开差距,顶多也就十几分吧。而Q&A环节,则是理综,优秀的和糟糕的能拉开50分的差距,直接决定总分。千万千万不可掉以轻心。


问题准备我包含了这几个模块:



  • 业务:业务方向,业务规划,核心业务的理解,你做的事情和业务的关系,B类C类的差异等

  • 技术:技术难点,技术亮点,技术选型,技术方案的细节,技术规划,代码等

  • 数据:核心的业务数据,核心的技术指标,数据反映了什么等等

  • 团队:项目管理经验,团队管理经验

  • 个人:个人特色,个人规划,自己的反思等等


其中业务、技术和数据这三块是最重要的,需要花80%的精力去准备。我问题准备大概花了3天时间,整体还是比较紧张的。准备问题的时候,明显的感觉到自己平时的知识储备还不太够,对大业务方向的思考还不透彻,对某些技术细节的把控也还不够到位。老话怎么说的来着,书到用时方恨少,事非经过不知难。


准备问题需要全面,不能有系统性的遗漏。比如缺少了业务理解或竞品分析等。


在回答问题上,也有一些要点需要注意:


听清楚再回答


问题回答的环节,很多人会紧张,特别是一两道问题回答的不够好,或气氛比较尴尬的时候,容易大脑短路。这个时候,评委反复问你一个问题或不断追问,而自己却觉得“我说的很清楚了呀,他还没明白吗”。我见过或听说过很多这样的案例,所以这应该是时有发生的。


为了避免自己也踩坑,我给自己定下了要求,一定要听清楚问题,特别是问题背后的问题。如果觉得不清楚,就反问评委进行doubel check。并且在回答的过程中,要关注评委的反映,去确认自己是否答到点子上了。


问题背后的问题


评委的问题不是天马行空瞎问的,问题的背后是在考察候选人的某项素质,通过问题来验证或挖掘候选人的亮点。这些考察的点都是公开的,在Job Model上都有。


我认为一个优秀的候选人,应当能识别出评委想考察你的点。找到问题背后的问题,再展开回答,效果会比单纯的挤牙膏来的好。


逻辑自洽、简洁明了


一个好的回答应该是逻辑自洽的。这里我用逻辑自洽,其实想说的是你的答案不一定要完全“正确”(其实往往也没有标准答案),但是一定不能自相矛盾,不能有明显的逻辑漏洞。大部分时候,评委不是在追求正确答案,而是在考察你有没有自己的思考和见解。当然,这种思考和见解几乎都是靠平时积累出来的,很难临时抱佛脚。


此外,当你把逻辑捋顺了以后,简洁明了的讲出来就好了,我个人是非常喜欢能把复杂问题变简单的人的。一个问题的本质是什么,核心在那里,关键的几点是什么,前置条件和依赖是什么,需要用什么手段和资源去解决。当你把这些东西条分缕析的讲明白以后,不用再多啰嗦一句,任何人都能看出你的牛逼了。


其他


心态调整


我的心态经历过过山车般的起伏,可以看到



在最痛苦最难受的时候,如果身边有个人能理解你陪伴你,即使他们帮不上什么忙,也是莫大的宽慰。如果没有这样的人,那只能学会自己拥抱自己,自己激励自己了。


所以,平时对自己的亲人好一点,对朋友们好一点,他们绝对是你人生里最大的财富。


关于评委


我从一开始就一直觉得评委是对手,是来挑战你的,对你的汇报进行证伪。我一直把晋升答辩当作一场battle来看待,直到进入考场的那一刻,我还在心理暗示,go and fight with ths giants。


但真实的经历以后,感觉评委更多的时候并不是站在你的对立面。评委试图通过面试找到你的一些闪光点,从而论证你有能力晋升到下一个level。从这个角度来讲,评委不但不是“敌人”,更像是友军一般,给你输送弹药(话题)。


一些教训




  • 一定要给自己设置deadline,并严格执行它。如果自我push的能力不强,就把你的deadline公开出来,让老板帮你监督。




  • 自己先有思考和判断,再广开言路,不要让自己的头脑成为别人思想的跑马场。




  • 坚持OODA,前期千万不要扣细节。这个时候老板和同事是你的资源,尽管去打扰他们吧,后面也就是一两顿饭的事情。




附件


前期调研



参考文章


知乎


作者:酒红
链接:https://juejin.cn/post/7240805459288162360
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

北美 2023 被裁员的感悟

不贩卖焦虑,就事论事,希望能帮助到有需要的朋友。 很多人觉得在裁员之前是没有任何迹象的,其实真的不是这样。 公司在裁员的过程中有很多要裁员的迹象,我会在另外一篇文章中对我遇到的一些裁员信号设置一些雷区和警告,当你遇到上面的这些信号的时候,直觉告诉你需要马上考虑...
继续阅读 »

不贩卖焦虑,就事论事,希望能帮助到有需要的朋友。


很多人觉得在裁员之前是没有任何迹象的,其实真的不是这样。


公司在裁员的过程中有很多要裁员的迹象,我会在另外一篇文章中对我遇到的一些裁员信号设置一些雷区和警告,当你遇到上面的这些信号的时候,直觉告诉你需要马上考虑寻找下一个替代方案了。


因为当这些信号的任何一个或者多个同时出现的时候就意味着裁员在进行中了,通常会在 3 到 6 个月左右发生。


WeChat Image_20230602102637


在公司的职位


在被裁公司的职位是 Tech Lead。


虽然这个职位并不意味着你对其他同事而言能够获得更多的有效信息,但是通常可能会让自己与上级有更好的沟通管道。


但是,非常不幸的是这家公司的沟通渠道非常有问题。


因为负责相关开发部分的副总是从 PHP 转 Java 的,对 Java 的很多情况都不非常明确,所以他非常依赖一个架构师。


但,公司大部分人都认为这个架构师的要求是错误的,并且是非常愚蠢的。


比如说要求代码必须要放在一行上面,导致代码上面有不少行甚至超过了 1000 个字符。


所有开发都对这个要求非常不理解,并且多次提出这种要求是无理并且愚蠢的,我们组是对这个要求反应最激烈,并且抵触最大的(也有可能是因为我的原因,我不希望在明显错误的地方让步;我可以让步,但是需要给一个能说服的理由)。


然而,这个所谓的架构师就利用 PR 合并的权力,不停的让我们的组员进行修改。


裁员之前


正是因为在公司的职位和上面说到的和架构师直接的冲突。


在 6 个月之前,我就已经和组里的同事说要准备进行下一步了,你们该面试的就面试了,不要拖延。


在这个中间过程中,我的领导还找我谈过一次。领导的意思就是他非常同意我们的有关代码 PR 的要求,也觉得这些要求都是狗屁。


但,负责开发的副总,认为我们组现在是所有组里面最差的。


可能当时没有认真理解这句话的意思,我们组从所有组里面最好的,变成最差的只用了 2 周(一个 Sprint)的时间。


在这次谈话后,我更加坚信让我的组员找下一家的信息了,对他们中途要面试其他公司我都放开一面。


非常不幸的,我自己那该死的拖延症,我是把我自己的简历准备好了,但是还没有来得及投就等来了真正裁员的这一天。


深刻的教训和学到的经验:


如果公司的运营或者管理让感觉到不舒服,并且已经有开始寻找下家的想法的时候,一定要尽快准备,马上实施,不要拖延


这就是我在上面标黑马上的原因。


裁员过程


裁员过程非常简单和迅速,并且在毫不知情的情况下进行。


在周四的时候,公司的高层提示所有的会议今天全部取消,并且把应该 11 点开的全公司会议提前到了 9 点。


因为很多人都没有准备,所以很多人也没有参加。


后来才知道,9 点就是宣布裁员的开始,事后知道裁员比率为 40%。


然后就是各个部门找自己的被裁的员工开会,这个会议通常首先是一个 Zoom 的 Group 会议,说了一堆屁话,就是什么这是不得已的决定呀,什么乱七八糟的东西。


当然,在这个时候你还需要或者期待公司给你什么合理的理由呢?


然后就是 P&C 人员说话,基本上就是每个人 15 分钟的时间单独 Zoom。


这个 15 分钟,主要就是读下文件了,至于 2 个会议上是不是开摄像头,随意。


你愿意开也行,不愿意开也行,反正上面的所有人都心不在焉。我是懒得开,因为和你谈话的人,你都根本不认识。


第二个会议就是 P&C,这个会议主要就是和你说说被裁员后可以有的一些福利和什么的,对我个人来说我更关注的是补偿。


至于 401K 和里面的资金都是可以自行转的,也没啥需要他们说的,了解到补偿就是 6 周工资,不算多也凑合能接受。


负责裁员的人说,还有什么需要问的,我简单的回答了下 All Set 然后 have a nice day 就好了。毕竟他们只是具体做事的人,问他们也问不出个所以然,这有啥的。


裁员之后


裁员之后,感觉所有认识的被裁的同事都是懵的。


开完 15 分钟的 P&C 会议后,基本上电脑和邮箱马上就不能用了。公司貌似说电脑可以自己留着,但是上面的数据会被远程清理掉。


留在公司里面的东西会有人收拾后寄到家里。


我在公司里的办公桌就属于离职型办公桌,简单的来说,上面只有一台不属于我的显示器,另外就是从其他地方拿过来的一盒消毒湿巾,公司里面压根没有我需要的东西。


很多人认为公司禁用账户有点太不讲人情,其实从技术层面来说根本没有什么,因为所有的管理都是 LDAP,直接在 LDAP 上禁用你账户就好了,没啥稀奇的。


中午的时候,被裁的同事都互相留下了手机号码,感觉大家因为我在裁员列表里面感觉有点扯。另外更扯的同事在这个公司工作了 7 年了,也在列表里面(所有 PHP 的基础架构都是他写的和建立的)。


虽然最开始和这个同事有过一些摩擦,但是这个印度的同事真的挺好的,我们都觉得他挺不错,也愿意和他一起共事。


很多人,包括我。都对这个同事感觉不值,也觉得这很扯。


奈何公司的选择就是一些阿谀奉承,天天扯淡的人,比如说那个奇葩的架构师。


没多久,被裁的同事建了一个群,然后把我给拉进去了,主要还是我们组里面的同事,大家希望能够分享一些面试经验和机会,偶尔吐槽下。


在晚上的时候,突然收到另外一个同事的 LinkedIn 好友邀请,他不在这次裁员内。


但是他也被降职了,他本来是 Sr 开发人员和小组长,后来被提拔成架构师了,现在连小组长都不是了。


他和我说,如果需要帮助的话,他会尽量帮忙,并且还给他的一些曾经的招聘专员账号推送给了我。


我也非常感谢他们,虽然经历过,但是也收获了一些朋友,虽然说在美国职场比较难收获朋友,但是也并不是完全这样的。


没有了利益的纠葛,更容易说点实话。


作者:honeymoose
链接:https://juejin.cn/post/7240052076624035901
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android开发中的MVC_MVP_MVVM

前言 MVC、MVP和MVVM是Android开发中常见的架构模式,这三种架构模式都有其独特的优点和适用场景。它们可以帮助开发者更好地组织和管理代码,提高应用程序的可维护性和可测试性,同时也能够提升开发效率和团队协作能力。选择适合的架构模式取决于项目的需求和开...
继续阅读 »

前言


MVC、MVP和MVVM是Android开发中常见的架构模式,这三种架构模式都有其独特的优点和适用场景。它们可以帮助开发者更好地组织和管理代码,提高应用程序的可维护性和可测试性,同时也能够提升开发效率和团队协作能力。选择适合的架构模式取决于项目的需求和开发团队的经验,理解和掌握这些架构模式对于Android开发者来说是非常重要的。现通过一个案例需求分析三种框架


案例需求



查询用户账号信息:用户输入账号,点击按钮可进行查询账号信息,如果查询数据成功,则将数据展示在界面上;如果查询数据失败,则在界面上提示获取数据失败



MVC框架模型


为什么学习MVC?


如果不使用任何框架来实现这个案例需求,则需要实现以下功能:



  • 获取用户输入的信息

  • 展示获取信息成功界面

  • 展示获取信息失败界面

  • 查询用户数据

  • 业务逻辑


代码示例


Bean对象

//账号信息
public class Account {
private String name; //账号名称
private int level; //账号登记

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getLevel() {
return level;
}

public void setLevel(int level) {
this.level = level;
}
}

回调接口

public interface MCallback {
void onSuccess(Account account);
void onFailed();
}

整体业务逻辑

public class NormalActivity extends AppCompatActivity implements View.OnClickListener {

private EditText mEtAccount;
private TextView mTvResult;
private Button mBtGetAccount;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_normal);

initView();
}

private void initView() {
mEtAccount = findViewById(R.id.et_account);
mTvResult = findViewById(R.id.tv_result);
mBtGetAccount = findViewById(R.id.btn_getAccount);
mBtGetAccount.setOnClickListener(this);
}

@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_getAccount:
String userInput = getUserInput();
getAccountData(userInput, new MCallback() {
@Override
public void onSuccess(Account account) {
showSuccessPage(account);
}

@Override
public void onFailed() {
showFailedPage();
}
});
}
}

//获取用户输入的信息
private String getUserInput() {
return mEtAccount.getText().toString();
}

//展示获取数据成功的界面
private void showSuccessPage(Account account) {
mTvResult.setText("用户账号:" + account.getName() + "|"
+ "用户等级:" + account.getLevel());
}

//展示获取数据失败的界面
private void showFailedPage() {
mTvResult.setText("获取数据失败");
}

//模拟查询账号数据
private void getAccountData(String accountName, MCallback callback) {
Random random = new Random();
boolean isSuccess = random.nextBoolean();
if (isSuccess) {
Account account = new Account();
account.setName(accountName);
account.setLevel(100);
callback.onSuccess(account);
} else {
callback.onFailed();
}
}
}

MVC模型简介


MVC是一种经典的架构模式,将应用程序分为三个主要组成部分:模型(Model)、视图(View)和控制器(Controller)。模型负责数据管理和业务逻辑,视图负责用户界面展示,控制器处理用户输入和协调模型与视图之间的交互。


截屏2023-06-07 15.27.41.png



  • Controller:Activity、Fragment

  • View:layout、View控件

  • Model:数据处理(网络请求、SQL等)


MVC代码演练


使用MVC框架实现案例需求的话,需要实现以下功能:



  • MVCActivity(C层):业务逻辑处理、获取用户输入、展示成功页面、展示失败页面

  • MVCModel层(M层):查询账号数据

  • View层(V层):layout



  1. 将数据的获取与界面的展示分离(将查询账号数据从Acitity中分离到Model中即可)

  2. 解决各层之间通信问题(Activity通知Model获取数据,Model通知Activity更新界面)


将查询账号数据抽离到MVCModel中:

public class MVCModel {
//模拟查询账号数据
public void getAccountData(String accountName, MCallback callback) {
Random random = new Random();
boolean isSuccess = random.nextBoolean();
if (isSuccess) {
Account account = new Account();
account.setName(accountName);
account.setLevel(100);
callback.onSuccess(account);
} else {
callback.onFailed();
}
}
}

MVC的优缺点



  • 优点:
    一定程度上实现了Model与View的分离,降低了代码的耦合度。

  • 缺点:
    Controller与View难以完全解耦,并且随着项目复杂度的提升,Controller将越来越臃肿。


MVP框架模型


MVP模型简介


MVP模式在MVC的基础上做了一些改进,将视图和模型之间的直接交互改为通过一个中间层——Presenter来完成。Presenter负责处理视图的逻辑和用户交互,并将数据获取和处理的任务委托给模型。
截屏2023-06-12 16.34.57.png



  1. Model与View不再直接进行通信,而是通过中间层Presenter来实现

  2. Activity的功能被简化,不再充当控制器,主要负责View层面的工作


MVP代码实战


使用MVP框架实现案例需求的话,需要实现以下功能:



  • MVPActivity(V层):获取用户输入、展示成功界面、展示失败界面

  • MVPPresenter(P层):业务逻辑处理

  • MVPModel(M层):查询账号数据



  1. MVPctivity负责提供View层面的功能(采用实现接口的方式)

  2. MVPModel负责提供数据方面的功能

  3. Model与View不再进行直接通信,通过Presenter来实现


IMVPView接口设计

public interface IMVPView {
String getUserInput();
void showSuccessPage(Account account);
void showFailedPage();
}

MVPModel设计

public class MVPModel {
//模拟查询账号数据
public void getAccountData(String accountName, MCallback callback) {
Random random = new Random();
boolean isSuccess = random.nextBoolean();
if (isSuccess) {
Account account = new Account();
account.setName(accountName);
account.setLevel(100);
callback.onSuccess(account);
} else {
callback.onFailed();
}
}
}

MVPPresenter设计

public class MVPPresenter {
private IMVPView imvpView;
private MVPModel mvpModel;

public MVPPresenter(IMVPView imvpView) {
this.imvpView = imvpView;
mvpModel = new MVPModel();
}

public void getData(String accountName) {
mvpModel.getAccountData(accountName, new MCallback() {
@Override
public void onSuccess(Account account) {
imvpView.showSuccessPage(account);
}

@Override
public void onFailed() {
imvpView.showFailedPage();
}
});
}
}

MVP的优缺点



  • 优点:解决了MVC中Controller与View过度耦合的缺点,职责划分明显,更加易于维护

  • 缺点:接口数量多,项目复杂度升高。随着项目复杂度的提升,Presenter层将越来越臃肿。


使用MVP的建议:



  1. 接口规范化(封装父类接口以减少接口的使用量)

  2. 使用第三方插件自动生成MVP代码

  3. 对于一些简单的界面,可以选择不使用框架

  4. 根据项目复杂度,部分模块可以选择不使用接口


MVVM框架模型


MVVM模型简介


MVVM模式进一步改进了MVP模式,引入了一个新的组件——ViewModel。ViewModel与视图进行双向绑定,负责处理视图的状态和逻辑,同时也能够监听模型的变化。这种双向绑定的机制使得视图与数据的同步更加方便,减少了手动更新视图的代码量。


截屏2023-06-13 11.27.10.png



  1. 减少了接口数量

  2. 告别繁琐findViewById操作


DataBinding学习


DataBinding是谷歌官方发布的一个实现数据绑定的框架(实现数据与视图的双向绑定),DataBinding可以帮助我们在安卓中更好的实现MVVM模式。


DataBinding使用步骤



  1. 启用DataBinding

  2. 修改布局文件为DataBinding布局

  3. 数据绑定


DataBinding实战


在 build.gradle(app)的android中启动DataBinding

dataBinding {
enabled = true
}

alt+enter或option+enter修改布局为DataBinding布局

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>

<variable
name="account"
type="com.example.mdemo.bean.Account" />
<variable
name="activity"
type="com.example.mdemo.databinding.DemoActivity" />
</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".databinding.DemoActivity">

<TextView
android:id="@+id/tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:gravity="center"
android:text="@{account.name+'|'+account.level}" />

<Button
android:id="@+id/btn_addLevel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:onClick="@{activity.onclick}"
android:text="账号等级+1" />
</LinearLayout>
</layout>

数据绑定

public class DemoActivity extends AppCompatActivity {

private Account account;
private ActivityDemoBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_demo);
account = new Account();
account.setName("TEST");
account.setLevel(100);
binding.setAccount(account);
binding.setActivity(this);
}

public void onclick(View view) {
Toast.makeText(this, "点击了", Toast.LENGTH_SHORT).show();
int level = account.getLevel();
account.setLevel(level + 1);
binding.setAccount(account);
}
}

代码优化


为减少binding.setAccount赋值实现数据更新的代码冗余操作,可借助BaseObservable接口、Bindable注解及notifyPropertyChanged实现数据自动更新操作

public class Account extends BaseObservable {
private String name; //账号名称
private int level; //账号登记

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Bindable
public int getLevel() {
return level;
}

public void setLevel(int level) {
this.level = level;
notifyPropertyChanged(BR.level);
}
}

MVVM代码实战


使用MVVM框架实现案例需求的话,需要实现以下功能:



  • layout(V层):获取用户输入、展示成功界面、展示失败界面

  • MVVMViewModel(VM层):业务逻辑处理、数据更新

  • MVVMModel层(M层):查询账号数据



  1. 提供View、ViewModel以及Model三层

  2. 将布局修改为DataBinding布局

  3. View与ViewModel之间通过DataBinding进行通信

  4. 获取数据并展示在界面上


MVVMDataBinding布局

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>
<variable
name="viewModel"
type="com.example.mdemo.mvvm.MVVMViewModel" />

</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".mvvm.MVVMActivity">

<EditText
android:id="@+id/et_account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入要查询的账号"
android:layout_marginTop="30dp"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:text="@={viewModel.userInput}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

<Button
android:id="@+id/btn_getAccount"
android:text="查询用户信息"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:onClick="@{viewModel.getData}"
app:layout_constraintTop_toTopOf="@id/et_account"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

<TextView
android:id="@+id/tv_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.result}"
app:layout_constraintTop_toTopOf="@+id/btn_getAccount"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="120dp" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

MVVMViewModel设计

public class MVVMViewModel extends BaseObservable {
private MVVMModel mvvmModel;
private String userInput;
private ActivityMvvmactivityBinding binding;
private String result;

//一般需要传入Application对象,方便在ViewModel中使用application,
//比如sharedpreferences需要使用
public MVVMViewModel(Application application) {
mvvmModel = new MVVMModel();
}

public MVVMViewModel(Application application, ActivityMvvmactivityBinding binding) {
mvvmModel = new MVVMModel();
this.binding = binding;
}

@Bindable
public String getResult() {
return result;
}

public void setResult(String result) {
this.result = result;
notifyPropertyChanged(BR.result);
}

@Bindable
public String getUserInput() {
return userInput;
}

public void setUserInput(String userInput) {
this.userInput = userInput;
notifyPropertyChanged(BR.userInput);
}


public void getData(View view) {
// String userInput = binding.etAccount.getText().toString();
mvvmModel.getAccountData(userInput, new MCallback() {
@Override
public void onSuccess(Account account) {
String info = account.getName() + "|" + account.getLevel();
setResult(info);
}

@Override
public void onFailed() {
setResult("获取数据失败");
}
});
}
}

MVVMActivity

public class MVVMActivity extends AppCompatActivity {

private ActivityMvvmactivityBinding binding;
private MVVMViewModel mvvmViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this,R.layout.activity_mvvmactivity);
mvvmViewModel = new MVVMViewModel(getApplication(),binding);
binding.setViewModel(mvvmViewModel);
}
}

LiveData+ViewModel



  • LiveData是一个可以被观察的数据持有者,它可以通过添加观察者的方式来让其他组件观察它的变更

  • LiveData遵从应用程序的生命周期(如果LiveData的观察者已经是销毁状态,LiveData就不会通知该观察者)


MVVM的优缺点



  • 优点:实现了数据和视图的双向绑定,极大的简化代码

  • 缺点:bug难以调试,并且dataBinding目前还存在一些编译问题


总结



  • MVC:学习简单但是解耦不够彻底

  • MVP:解耦更加彻底,学习相对简单,但是代码相对繁琐

  • MVVM:代码逻辑简介,但是学习成本较大

作者:Coward881
链接:https://juejin.cn/post/7244050872127193146
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

从Kotlin中return@forEach了个寂寞

今天在Review(copy)同事代码的时候,发现了一个问题,想到很久之前,自己也遇到过这个问题,那么就来看下吧。首先,我们抽取最小复现代码。(1..7).forEach { if (it == 3) { return@forEach ...
继续阅读 »

今天在Review(copy)同事代码的时候,发现了一个问题,想到很久之前,自己也遇到过这个问题,那么就来看下吧。首先,我们抽取最小复现代码。

(1..7).forEach {
if (it == 3) {
return@forEach
}
Log.d("xys", "Num: $it")
}

�很简单的代码,我相信很多人都这样写过,实际上就是遍历的过程中,满足条件后就退出遍历,那么上面的代码,能实现这样的需求吗?我们来看下执行结果。

Num: 1
Num: 2
Num: 4
Num: 5
Num: 6
Num: 7

很遗憾,即使等于3之后就return了,但是然并卵,遍历依然继续执行了。相信很多写Kotlin的开发者都遇到过这个问题,其原因,还是在于语法的思维定势,我们在Kotlin的文档上,可以找到非常明确的解释。
kotlinlang.org/docs/return…


我们先来看下Kotlin中forEach的源码。

/**
* Performs the given [action] on each element.
*/
@kotlin.internal.HidesMembers
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}

�我们来提取下关键信息:



  • 内联函数

  • 高阶函数


发现了吗,由于高阶函数的存在,当你在高阶函数的闭包内「return」时,是结束的整个函数,当你使用「return@forEach�」时,是结束当前的闭包,所以,如果你像这样写:

(1..7).forEach {
if (it == 3) {
return
}
Log.d("xys", "Num: $it")
}

那么等于3之后,整个函数就被return了,那么如果你像文章开头这样写,那么等效于continue,因为你结束了当前的闭包,而这个闭包只是其中的一次遍历过程。那么我们要如何实现我们最初的需求呢?看到这样,答案其实已经呼之欲出了,那就是要return整个遍历的闭包。所以,官方也给出了解决方案,那就是外面套一层闭包:

run loop@{
(1..7).forEach {
if (it == 3) {
return@loop
}
Log.d("xys", "Num: $it")
}
}

写起来确实是麻烦一点,但这却是必不可少的过程,是引入闭包所带来的一点副作用。



当然这里不仅限于run,任何闭包都是可以的。



欢迎大家关注我的公众号——【群英传】,专注于「Android」「Flutter」「Kotlin」
我的语雀知识库——http://www.yuque.com/xuyisheng


作者:xuyisheng
链接:https://juejin.cn/post/7243819009866235964
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

[Android] 弱网检测示例

使用ConnectivityManager类来检查当前的网络状态和连接类型 使用以下代码来检查当前是否有可用的网络连接:ConnectivityManager cm = (ConnectivityManager) getSystemService(Contex...
继续阅读 »

使用ConnectivityManager类来检查当前的网络状态和连接类型


使用以下代码来检查当前是否有可用的网络连接:

ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
boolean isConnected = activeNetwork != null && activeNetwork.isConnectedOrConnecting();

您还可以使用ConnectivityManager的getNetworkCapabilities()方法来检查当前网络的能力。例如,您可以使用以下代码来检查当前网络是否支持较高的下载速度:

ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkCapabilities nc = cm.getNetworkCapabilities(cm.getActiveNetwork());
if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
// 这是一个蜂窝网络
if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_LTE)) {
// 快
} else {
// 慢
}
}

使用Ping或者Traceroute命令来检测网络质量


然后,您可以使用Java的ProcessBuilder类来执行Ping命令。例如,您可以使用以下代码来Ping http://www.baidu.com

// ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", "www.baidu.com");
ProcessBuilder pb = new ProcessBuilder("traceroute", "-n", "www.baidu.com");
pb.redirectErrorStream(true);
Process process = pb.start();

// 读取命令的输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
// todo:处理输出行
}

请注意,需要在应用的主线程之外运行此代码,因为它会阻塞线程。您可以使用协程或其他方法来在后台线程上执行Ping/Traceroute命令。


执行Ping命令后,您可以从输出中提取有关网络质量的信息。例如,您可以查找"time="字符串来获取每个Ping请求的延迟(单位是毫秒)。您还可以查找"packet loss"字符串来获取丢包率。


执行Traceroute命令后,您可以从输出中提取有关网络路径的信息。例如,每行输出都包含一个路由器的IP地址和延迟(单位是毫秒)。您可以使用此信息来了解数据在网络中的传输路径,以及在路径上的延迟。


最后,请注意,使用Ping/Traceroute命令进行网络测量可能会对网络造成轻微负载,并且在某些情况下可能会被防火墙阻止。因此,您应该谨慎使用此方法。


为了在网络状况较差的情况下优化Android应用的性能,您可以考虑以下几点:



  1. 使用缓存:缓存常用的数据和资源可以帮助您的应用在没有网络连接的情况下也能工作。您可以使用Android提供的缓存API,或者使用第三方库来管理缓存。

  2. 使用离线模式:如果您的应用依赖于网络连接来提供内容,那么您可以考虑让用户在离线时也能使用应用。这可以通过在线时下载内容并在离线时使用来实现。

  3. 使用后台同步:如果您的应用需要定期与服务器同步数据,那么您可以使用Android的后台同步机制来实现。这样,当网络状况较差时,您的应用就可以在后台自动同步数据,而不会影响用户体验。

  4. 优化网络请求:对于使用网络加载数据的应用,可以考虑优化网络请求。这可以通过减少不必要的请求,使用压缩传输,以及使用较快的网络连接来实现。

  5. 使用较少的数据:尽量减少应用使用的数据量可以帮助您的应用在网络状况较差的情况下更好地运行。这可以通过优化图像大小,使用缓存和离线模式,以及减少不必要的数据传输来实现。

  6. 使用较快的网络连接:如果您的应用需要使用网络,那么使用较快的网络连接可以帮助您的应用在网络状况较差的情况下更好地运行。例如,您可以使用Wi-Fi而不是移动数据连接,或者在有多个可用网络连接时选择速度较快的连接。

  7. 使用网络状态监测:通过监测网络状态,您的应用可以根据当前的网络状态调整其行为。例如,您可以在网络状态较差时显示提示信息,或者在网络不可用时禁用某些功能。

  8. 尝试使用新的网络技术:最新的移动设备和操作系统通常都会提供更多的网络优化功能。例如,Android 11引入了网络请求优化工具,可以帮助您的应用在网络状况较差的情况下更好地运行

作者:3_7
链接:https://juejin.cn/post/7239340461235142715
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

浅析一下:kotlin委托背后的实现机制

大家好,kotlin的属性委托、类委托、lazy等委托在日常的开发中,给我们提供了很大的帮助,我之前的文章也是有实战过几种委托。不过对比委托实现的背后机制一直都没有分析过,所以本篇文章主要是带领大家分析下委托的实现原理,加深对kotlin的理解。 一. laz...
继续阅读 »

大家好,kotlin的属性委托、类委托、lazy等委托在日常的开发中,给我们提供了很大的帮助,我之前的文章也是有实战过几种委托。不过对比委托实现的背后机制一直都没有分析过,所以本篇文章主要是带领大家分析下委托的实现原理,加深对kotlin的理解。


一. lazy委托


这里我们不说用法,直接说背后的实现原理。


先看一段代码:


val content: String by lazy {
"oiuytrewq"
}

fun main() {
println(content.length)
}

我们看下反编译后的java代码:




  1. 首先会通过DelegateDemoKt静态代码块饿汉式的方式创建一个Lazy类型的变量content$delegate,命名的规则即代码中定义的原始变量值拼接上$delegate,我们原始定义的content变量就会从属性定义上消失,但会生成对应的get方法,即getContent()



  1. 当我们在main方法中调用content.length时,其实就是调用getContent().length(),而getContent()最终是调用了content$delegate.getValue方法;



  1. 这个lazy类型的变量是调用了LazyKt.lazy()方法创建,而真正的核心逻辑——该方法具体参数的传入,在反编译的java代码中并没有体现;


java代码既然看不到,我们退一步看下字节码:



上面是DelegateDemoKt类构造器对应的字节码,其中就是获取了DelegateDemoKt$content$2作为参数传入了LazyKt.lazy()方法。


我们看下DelegateDemoKt$content$2类的实现字节码:



DelegateDemoKt$content$2类实现了Function0接口,所以上面lazy的真正实现逻辑就是DelegateDemoKt$content$2类的invoke方法中,上图的字节码红框圈出的地方就很直观的看出来了。


二. 属性委托


属性委托的委托类就是指实现了ReadWritePropertyReadOnlyProperty接口的类,像官方提供的Delegates.observable()Delegates.vetoable()这两个api也是借助前面两个接口实现的。这里我们就以支持读写的ReadWriteProperty委托接口进行举例分析。


先看一段例子代码:


var age: Int by object : ReadWriteProperty<Any?, Int> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
return 10
}

override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
val v = value * value
println("setValue: $v")
}
}

fun main() {
age = 4
println(age)
}

我们看下反编译的java代码:


public final class DelegateDemoKt {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{Reflection.mutableProperty0(new MutablePropertyReference0Impl(DelegateDemoKt.class, "age", "getAge()I", 1))};
@NotNull
private static final <undefinedtype> age$delegate = new ReadWriteProperty() {
@NotNull
public Integer getValue(@Nullable Object thisRef, @NotNull KProperty property) {
Intrinsics.checkNotNullParameter(property, "property");
return 10;
}

public void setValue(@Nullable Object thisRef, @NotNull KProperty property, int value) {
Intrinsics.checkNotNullParameter(property, "property");
int v = value * value;
String var5 = "setValue: " + v;
System.out.println(var5);
}
};

public static final int getAge() {
return age$delegate.getValue((Object)null, $$delegatedProperties[0]);
}

public static final void setAge(int var0) {
age$delegate.setValue((Object)null, $$delegatedProperties[0], var0);
}

public static final void main() {
setAge(4);
int var0 = getAge();
System.out.println(var0);
}
}


  1. 和lazy有些类似,会生成一个实现了ReadWriteProperty接口的匿名类变量age$delegate,命名规则和lazy相同,通过还帮助我们生成了对应的getAgesetAge方法;



  1. 当我们在代码中执行age = 4就会调用setAge(4)方法,最终会调用age$delegate.setValue()方法;类似的调用age就会调用getAge(),最终调用到age$delegate.getValue()方法;



  1. 编译器还通过反射帮助我们生成了一个KProperty类型的$$delegatedProperties变量,主要是ReadWritePropertysetValuegetValue方法都需要传入这样一个类型的对象,通过$$delegatedProperties变量我们可以访问到具体的变量名等信息;




类似的还有一种属性委托,我们看下代码:


val map = mutableMapOf<String, Int>()

val name: Int by map

上面代码的意思是:当访问name时,就会从map这个散列表中获取key为"name"的value值并返回,不存在就直接抛异常,接下来我们看下反编译后的java代码:


public final class DelegateDemoKt {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{Reflection.property0(new PropertyReference0Impl(DelegateDemoKt.class, "name", "getName()I", 1))};
@NotNull
private static final Map map = (Map)(new LinkedHashMap());
@NotNull
private static final Map name$delegate;

static {
name$delegate = map;
}

public static final int getName() {
Map var0 = name$delegate;
Object var1 = null;
KProperty var2 = $$delegatedProperties[0];
return ((Number)MapsKt.getOrImplicitDefaultNullable(var0, var2.getName())).intValue();
}
}


  1. 生成一个Map类型的name$delegate变量,这个变量其实就是我们定义的map散列表;



  1. 通过反射生成了一个KProperty类型对象变量$$delegatedProperties,通过这个对象的getName()我们就能拿到变量名称,比如这里的"name"变量名;



  1. 最终调用了MapsKt.getOrImplicitDefaultNullable方法,去map散列表去查找"name"这个key对应的value;



PS:记得kotlin1.6还是1.7的插件版本对应委托进行了优化,这个后续的文章会再进行讲解。



三. 类委托


类委托实现就比较简单了,这里我们看下样例代码:


fun interface Fruit {
fun type(): Int
}

class FruitProxy(private val model: Fruit) : Fruit by model

fun main() {
val proxy: FruitProxy = FruitProxy {
-1
}
println(proxy.type())
}

反编译成java代码看下:





首先我们看下FruitProxy这个类,其实现了Fruit接口,借助属性委托特性,编译器会自动帮助我们生成type() 接口方法的实现,并再其中调用构造方法传入的委托类对象modeltype()方法,类委托的核心逻辑就这些。


再main()方法中构造FruitProxy时,我们也无法知晓具体的构造参数对象是啥,和上面的lazy一样,我们看下字节码:



其实FruitProxy方法就传入了一个DelegateDemoKt$main$proxy$1类型的对象,并实现了Fruit接口重写了type方法。


总结


本篇文章主要是讲解了三种委托背后的实现原理,有时候反编译字节码看不出来原理的,可以从字节码中寻找答案,希望本篇文章能对你有所帮助。


历史文章


这里是我整理的过往kotlin特性介绍的历史文章,大家感兴趣可以阅读下:


Kotlin1.9.0-Beta,它来了!!


聊聊Kotlin1.7.0版本提供的一些特性


聊聊kotlin1.5和1.6版本提供的一些新特性


kotlin密封sealed class/interface的迭代之旅


优化@BuilderInference注解,Kotlin高版本下了这些“毒手”!


@JvmDefaultWithCompatibility优化小技巧

,了解一下~

收起阅读 »

父母在家千万注意别打开“共享屏幕”,银行卡里的钱一秒被转走......

打开屏幕共享,差点直接被转账 今天和爸妈聊天端午回家的事情,突然说到最近AI诈骗的事情,千叮咛万嘱咐说要对方说方言才行,让他们充分了解一下现在骗子诈骗的手段,顺便也找了一下骗子还有什么其他的手段,打算一起和他们科普一下,结果就发现下面这一则新闻: 在辽宁大连务...
继续阅读 »

打开屏幕共享,差点直接被转账


今天和爸妈聊天端午回家的事情,突然说到最近AI诈骗的事情,千叮咛万嘱咐说要对方说方言才行,让他们充分了解一下现在骗子诈骗的手段,顺便也找了一下骗子还有什么其他的手段,打算一起和他们科普一下,结果就发现下面这一则新闻:


在辽宁大连务工的耿女士接到一名自称“大连市公安局民警”的电话,称其涉嫌广州一起诈骗案件,让她跟广州警方对接。耿女士在加上所谓的“广州警官”的微信后,这位“警官”便给耿女士发了“通缉令”,并要求耿女士配合调查,否则将给予“强制措施”。随后,对方与耿女士视频,称因办案需要,要求耿女士提供“保证金”,并将所有存款都集中到一张银行卡上,再把钱转到“安全账户”。


图片


期间,通过 “屏幕共享”,对方掌握了耿女士银行卡的账号和密码。耿女士先后跑到多家银行,取出现金,将钱全部存到了一张银行卡上。正当她打算按照对方指示,进行下一步转账时,被民警及时赶到劝阻。在得知耿女士泄露了银行卡号和密码后,银行工作人员立即帮助耿女士修改了密码,幸运的是,银行卡的近6万元钱没有受到损失。


就这手段,我家里的老人根本无法预防,除非把手机从他们手里拿掉,与世隔绝还差不多,所以还是做APP的各大厂商努力一下吧!


希望各大厂商都能看看下面这个防劫持SDK,让出门在外打工的我们安心一点。


防劫持SDK


一、简介


防劫持SDK是具备防劫持兼防截屏功能的SDK,可有效防范恶意程序对应用进行界面劫持与截屏的恶意行为。


二、iOS版本


2.1 环境要求


条目说明
兼容平台iOS 8.0+
开发环境XCode 4.0 +
CPU架构armv7, arm64, i386, x86_64
SDK依赖libz, libresolv, libc++

2.2 SDK接入


2.2.1 DxAntiHijack获取

官网下载SDK获取,下面是SDK的目录结构


1.png


DXhijack_xxx_xxx_xxx_debug.zip 防劫持debug 授权集成库 DXhijack_xxx_xxx_xxx_release.zip 防劫持release 授权集成库




  • 解压DXhijack_xxx_xxx_xxx_xxx.zip 文件,得到以下文件




    • DXhijack 文件夹



      • DXhijack.a 已授权静态库

      • Header/DXhijack.h 头文件

      • dx_auth_license.description 授权描述文件

      • DXhijackiOS.framework 已授权framework 集成库






2.2.2 将SDK接入XCode

2.2.2.1 导入静态库及头文件

将SDK目录(包含静态库及其头文件)直接拖入工程目录中,或者右击总文件夹添加文件。 或者 将DXhijackiOS.framework 拖进framework存放目录


2.2.2.2 添加其他依赖库

在项目中添加 libc++.tbd 库,选择Target -> Build Phases,在Link Binary With Libraries里点击加号,添加libc++.tbd


2.2.2.3 添加Linking配置

在项目中添加Linking配置,选择Target -> Build Settings,在Other Linker Flags里添加-ObjC配置


2.3 DxAntiHijack使用


2.3.1 方法及参数说明

@interface DXhijack : NSObject

+(void)addFuzzy; //后台模糊效果
+(void)removeFuzzy;//后台移除模糊效果
@end

2.3.2 使用示例

在对应的AppDelegate.m 文件中头部插入


#import "DXhijack.h"

//在AppDelegate.m 文件中applicationWillResignActive 方法调用增加
- (void)applicationWillResignActive:(UIApplication *)application {
[DXhijack addFuzzy];
}

//在AppDelegate.m 文件中applicationDidBecomeActive 方法调用移除
- (void)applicationDidBecomeActive:(UIApplication *)application {
[DXhijack removeFuzzy];
}


三、Android版本


3.1 环境要求


条目说明
开发目标Android 4.0+
开发环境Android Studio 3.0.1 或者 Eclipse + ADT
CPU架构ARM 或者 x86
SDK三方依赖

3.2 SDK接入


3.2.1 SDK获取


  1. 访问官网,注册账号

  2. 登录控制台,访问“全流程端防控->安全键盘SDK”模块

  3. 新增App,填写相关信息

  4. 下载对应平台SDK


3.2.2 SDK文件结构



  • SDK目录结构 android-dx-hijack-sdk.png



    • dx-anti-hijack-${version}.jar Android jar包

    • armeabiarmeabi-v7aarm64-v8ax86 4个abi平台的动态库文件




3.2.3 Android Studio 集成

点击下载Demo


3.2.3.1 Android Studio导入jar, so

把dx-anti-hijack-x.x.x.jar, so文件放到相应模块的libs目录下


android-dx-hijack-as.png



  • 在该Module的build.gradle中如下配置:


 android{
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}

repositories{
flatDir{
dirs 'libs'
}
}
}


dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}



3.2.3.2 权限声明

Android 5.0(不包含5.0)以下需要在项目AndroidManifest.xml文件中添加下列权限配置:


<uses-permission android:name="android.permission.GET_TASKS"/>

3.2.3.3 混淆配置

-dontwarn *.com.dingxiang.mobile.**
-dontwarn *.com.mobile.strenc.**
-keep class com.security.inner.**{*;}
-keep class *.com.dingxiang.mobile.**{*;}
-keep class *.com.mobile.strenc.**{*;}
-keep class com.dingxiang.mobile.antihijack.** {*;}

3.3 DxAntiHijack 类使用


3.3.1 方法及参数说明

3.3.1.1 初始化


建议在Application的onCreate下調用


/**
* 使用API前必須先初始化
* @param context
*/

public static void init(Context context);

3.3.1.2 反截屏功能


/**
* 反截屏功能
* @param activity
*/

public static void DGCAntiHijack.antiScreen(Activity activity);

/**
* 反截屏功能
* @param dialog
*/

public static void DGCAntiHijack.antiScreen(Dialog dialog);

3.3.1.3 反劫持检测


/**
* 调用防劫持检测,通常现在activity的onPause和onStop调用
* @return 是否存在被劫持风险
*/

public static boolean DGCAntiHijack.antiHijacking();

3.3.2 使用示例

//使用反劫持方法
@Override
protected void onPause() {
boolean safe = DXAntiHijack.antiHijacking();
if(!safe){
Toast.makeText(getApplicationContext(), "App has entered the background", Toast.LENGTH_LONG).show();
}
super.onPause();
}

@Override
protected void onStop() {
boolean safe = DXAntiHijack.antiHijacking();
if(!safe){
Toast.makeText(getApplicationContext(), "App has entered the background", Toast.LENGTH_LONG).show();
}
super.onStop();
}



//使用反截屏方法
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DXAntiHijack.antiScreen(MainActivity.this);
}

以上。


结语


这种事情层出不穷,真的不是吾等普通民众能解决的,最好有从上至下的政策让相应的厂商(尤其是银行和会议类的APP)统一做处理,这样我们在外

作者:昀和
来源:juejin.cn/post/7242145254057312311
打工的人才能安心呀。

收起阅读 »

Spring Boot定时任务详解与案例代码

概述 Spring Boot是一个流行的Java开发框架,它提供了许多便捷的特性来简化开发过程。其中之一就是定时任务的支持,让开发人员可以轻松地在应用程序中执行定时任务。本文将详细介绍如何在Spring Boot中使用定时任务,并提供相关的代码示例。 实际案例...
继续阅读 »

image.png





概述


Spring Boot是一个流行的Java开发框架,它提供了许多便捷的特性来简化开发过程。其中之一就是定时任务的支持,让开发人员可以轻松地在应用程序中执行定时任务。本文将详细介绍如何在Spring Boot中使用定时任务,并提供相关的代码示例。


实际案例


在Spring Boot中,使用定时任务非常简单。首先,需要在应用程序的入口类上添加@EnableScheduling注解,以启用定时任务的支持。该注解将告诉Spring Boot自动配置并创建一个线程池来执行定时任务。


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

一旦启用了定时任务支持,就可以在任何Spring管理的Bean中创建定时任务。可以通过在方法上添加@Scheduled注解来指定定时任务的执行规则。下面是一个简单的示例,演示了每隔一分钟执行一次的定时任务:


import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class MyScheduledTask {

@Scheduled(cron = "0 * * * * *") // 每分钟执行一次
public void executeTask() {
// 在这里编写定时任务的逻辑
System.out.println("定时任务执行中...");
}
}

在上面的示例中,我们创建了一个名为MyScheduledTask的组件,并在其中定义了一个名为executeTask的方法。通过使用@Scheduled(cron = "0 * * * * *")注解,我们指定了该方法应该每分钟执行一次。当定时任务触发时,executeTask方法中的逻辑将被执行。


需要注意的是,@Scheduled注解支持不同的任务触发方式,如基于固定延迟时间、固定间隔时间或cron表达式等。可以根据实际需求选择适合的方式。


以上就是使用Spring Boot进行定时任务的基本示例。通过简单的注解配置,您可以轻松地在应用程序中添加和管理定时任务。希望本文能对您理解和使用Spring Boot定时任务提供帮助。


总结


Spring Boot提供了便捷的方式来实现定时任务。通过添加@EnableScheduling注解来启用定时任务支持,并使用@Scheduled注解来指定任务的执行规则。可以根据需求选择不同的触发方式。


除了上述基本示例外,Spring Boot还提供了更多高级功能和配置选项,以满足更复杂的定时任务需求。



  1. 方法参数和返回值:您可以在定时任务方法中添加参数和返回值,Spring Boot会自动注入合适的值。例如,可以将java.util.Date类型的参数添加到方法中,以获取当前时间。返回值可以是voidjava.util.concurrent.Futurejava.util.concurrent.CompletableFuture等类型。

  2. 并发执行和线程池配置:默认情况下,Spring Boot的定时任务是串行执行的,即每个任务完成后再执行下一个任务。如果需要并发执行任务,可以通过配置线程池来实现。可以在application.propertiesapplication.yml文件中设置相关的线程池属性,如核心线程数、最大线程数和队列容量等。

  3. 异常处理:定时任务可能会抛出异常,因此需要适当处理异常情况。您可以使用@Scheduled注解的exceptionHandler属性来指定异常处理方法,以便在任务执行过程中捕获和处理异常。

  4. 动态调度:有时需要根据运行时的条件来动态调整定时任务的触发时间。Spring Boot提供了TaskScheduler接口和CronTrigger类,您可以使用它们来在运行时动态设置定时任务的执行规则。

  5. 集群环境下的定时任务:如果应用程序部署在多个节点的集群环境中,可能会遇到定时任务重复执行的问题。为了避免这种情况,可以使用分布式锁机制,如Redis锁或数据库锁,来确保只有一个节点执行定时
    作者:百思不得小赵
    来源:juejin.cn/post/7244089396567638072
    任务。

收起阅读 »

从 数据工程 到 Prompt 工程

动动发财的小手,点个赞吧! 数据工程构成了数据科学过程的很大一部分。在 CRISP-DM 中,这个过程阶段称为“数据准备”。它包括数据摄取、数据转换和数据质量保证等任务。在本文章中,我们使用 ChatGPT 和 Python 解决了典型的数据工程任务。通过这...
继续阅读 »

动动发财的小手,点个赞吧!



数据工程构成了数据科学过程的很大一部分。在 CRISP-DM 中,这个过程阶段称为“数据准备”。它包括数据摄取、数据转换和数据质量保证等任务。在本文章中,我们使用 ChatGPT 和 Python 解决了典型的数据工程任务。通过这样做,我们探索了数据工程与提示工程新学科之间的联系。


简介


2022 年 5 月,斯蒂芬·沃尔夫勒姆 (Stephen Wolfram) 和莱克斯·弗里德曼 (Lex Fridman) 发表了题为“编程已死吗?”的深刻见解。他们讨论了未来开发者是否还会使用高级语言。根据 Wolfram 的说法,许多编程任务可以通过大型语言模型 (LLM) 实现自动化。在撰写本文时,此类模型最突出的例子是 ChatGPT。自 2022 年底推出以来,它产生了惊人的效果。指定要由 LLM 执行的操作称为“提示工程”。如果 Wolfram 是对的,至少软件开发的一部分将从编写代码转向编写提示。


在数据科学方面,数据准备可能是一项耗时且乏味的任务。那么,为什么不尝试使用 LLM 使其自动化呢?在以下部分中,我们将使用 ChatGPT 和 Python 解决不同的数据工程问题。我们没有自己编写 Python 代码,而是使用提示工程来生成它。我们的实验是在 2023 年 5 月 19 日基于当时最新的免费 ChatGPT 版本 (GPT-3.5) 进行的。


创建数据框


让我们从一个简单的问题开始,并从样本数据集创建一个 Pandas 数据框。表 1 包含例如世界银行提供的国家指标。


image-20230524153840794


为了从上面的示例中创建数据框,我们开始了与 ChatGPT 的新对话并发布了以下提示:


Create a Pandas table with the following data:
Continent, Country, Year, Category, Variable, Value
Asia, China, 2022, Economy, GDP, 18.1
Asia, China, 2022, Demographics, Population, 1412
North America, USA, 2022, Economy, GDP, 25.5
North America, USA, 2022, Demographics, Population, 333
Europe, Germany, 2022, Economy, GDP, 3.9
Europe, Germany, 2022, Demographics, Population, 84

作为响应,该模型生成了以下 Python 代码:


import pandas as pd

data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}

df = pd.DataFrame(data)
print(df)

运行脚本时,我们看到输出符合我们的规范:



尽管我们在实验中使用了示例,但下一节中生成的代码原则上也可以处理更大的数据集。


旋转数据框


对于数据科学,我们在第一个提示中创建的表结构并不是最优的。相反,所谓的“平板”会更方便。在表 1 中,指标“GDP”和“人口”分别用名为“Variable”和“Value”的两列表示。相比之下,平面表格将为每个指标包含单独的列,如表 2 所示。



将表 1 的格式转换为表 2 的格式称为“透视”。让我们尝试使用 ChatGPT 来解决这个任务。为此,我们在对话中发布了第二个提示:


Transform the table into the following format:
Continent, Country, Year, GDP, Population
Asia, China, 2022, 18.1, 1412
North America, USA, 2022, 25.5, 333
Europe, Germany, 2022, 3.9, 84

上述消息基于所需数据格式的示例。或者,我们也可以纯口头表达我们的要求。与经典编程一样,在提示工程中有实现目标的替代解决方案。在不久的将来,寻找最佳方式将是一项激动人心的任务。根据我们的提示,ChatGPT对Python代码进行了如下调整:


import pandas as pd

data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}

df = pd.DataFrame(data)

# Pivot the table
pivot_df = df.pivot_table(index=['Continent', 'Country', 'Year'], columns='Category', values='Value').reset_index()

# Rename the columns
pivot_df.columns.name =
pivot_df.columns = [column if column != 'Variable' else for column in pivot_df.columns]

print(pivot_df)

然而,运行脚本的第二个版本显示输出不是我们所要求的:


该表包含名为“人口统计”和“经济”的两列,而不是预期的“GDP”和“人口”列。为了解决这个问题,我们发布了下一个提示:


The columns are not correct, you should have used 
the variables GDP and population

作为响应,模型纠正了错误并生成了另一个版本的 Python 代码:


import pandas as pd

data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}

df = pd.DataFrame(data)

# Pivot the table
pivot_df = df.pivot_table(index=['Continent', 'Country', 'Year'], columns='Variable', values='Value').reset_index()

# Rename the columns
pivot_df.columns.name =
pivot_df.columns = ['Continent', 'Country', 'Year', 'GDP', 'Population']

print(pivot_df)

运行脚本表明这次旋转已正确实现:



富集数据框


另一个典型的数据工程任务是用附加信息丰富数据集。为此,我们要求 ChatGPT 通过发布以下提示将 ISO 国家代码添加到我们的数据中:


For each country, in the table above, add its ISO code

结果,模型调整了响应并返回了以下代码片段:


import pandas as pd

data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}

iso_codes = {
'China': 'CN',
'USA': 'US',
'Germany': 'DE'
}

df = pd.DataFrame(data)

# Add ISO code column
df['ISO Code'] = df['Country'].map(iso_codes)

# Pivot the table
pivot_df = df.pivot_table(index=['Continent', 'Country', 'ISO Code', 'Year'], columns='Variable', values='Value').reset_index()

# Rename the columns
pivot_df.columns.name =
pivot_df.columns = ['Continent', 'Country', 'ISO Code', 'Year', 'GDP', 'Population']

print(pivot_df)

运行脚本显示一个带有 ISO 代码的新列已添加到数据框中。



派生列


接下来让我们通过将一个国家的 GDP 除以其人口规模来得出一个新的列“人均 GDP”。为此,我们发送了以下提示:


Derive a new column "GDP per capita" by dividing the GDP 
of a country by its population size

对此,模型修改了代码如下:


import pandas as pd

data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}

iso_codes = {
'China': 'CN',
'USA': 'US',
'Germany': 'DE'
}

df = pd.DataFrame(data)

# Add ISO code column
df['ISO Code'] = df['Country'].map(iso_codes)

# Pivot the table
pivot_df = df.pivot_table(index=['Continent', 'Country', 'ISO Code', 'Year'], columns='Variable', values='Value').reset_index()

# Calculate GDP per capita
pivot_df['GDP per capita'] = pivot_df['GDP'] / pivot_df['Population']

# Rename the columns
pivot_df.columns.name =
pivot_df.columns = ['Continent', 'Country', 'ISO Code', 'Year', 'GDP', 'Population', 'GDP per capita']

print(pivot_df)

输出表明脚本产生了预期的结果:



分析数据


一旦数据转换完成,实际的分析就可以开始了。最后让我们计算一下各国GDP与人口的相关系数。为此,我们贴出最后一个提示:


Use the table above to calculate the correlation coefficient between 
the GDP and the population of the countries

结果,模型返回了以下代码片段:


import pandas as pd

data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}

df = pd.DataFrame(data)

# Pivot the table
pivot_df = df.pivot_table(index=['Continent', 'Country', 'Year'], columns='Variable', values='Value').reset_index()

# Calculate correlation coefficient
correlation_coefficient = pivot_df['GDP'].corr(pivot_df['Population'])

print("Correlation Coefficient: {:.3f}".format(correlation_coefficient))

运行脚本时,我们发现两个变量之间存在很强的正相关性,这并不奇怪:



总结


不可否认,我们的实验是基于一个简单的数据集。然而,结果是显著的。我们执行了几项数据工程任务,而没有编写一行代码。 ChatGPT 不仅能够在大多数情况下正确执行我们的提示。但即使模型犯了错误,它也能够反映和修复错误。与软件开发一样,生成的代码必须经过测试。此外,它可能需要重构和优化。在 AI 时代使用 pylint 仍然是一个好主意。然而,总而言之,我们必须同意 Wolfram 的观点:在未来,数据工程的重要部分将从编码转向提示工程。这种新方法不会取代数据工程师,但会提高他们的效率。


作者:冷冻工厂
来源:juejin.cn/post/7244174500773118009

收起阅读 »