注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

VUE3基础学习(二)模板语法,简单响应式,计算属性

说明:Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。一、模板语法{{}}使用<span>Message: {{ msg }}</span> {{}} 标识的数据将会从...
继续阅读 »

说明:Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。

一、模板语法

{{}}使用

<span>Message: {{ msg }}</span> {{}} 标识的数据将会从数据中获取,同时每次 msg 属性更改时它也会同步更新。

v-html

<p>Using v-html directive: <span v-html="rawHtml"></span></p>

若想插入 HTML,你需要使用 v-html 指令

Attribute 绑定

<div v-bind:id="dynamicId"></div>

简写 <div :id="dynamicId"></div>

布尔型 Attribute

<button :disabled="isButtonDisabled">Button</button>

当 isButtonDisabled 为[真值]或一个空字符串 (即 <button disabled="">) 时,元素会包含这个 disabled attribute。而当其为其他[假值]时 attribute 将被忽略。

动态绑定多个值

如果你有像这样的一个包含多个 attribute 的 JavaScript 对象:

data() {
return {
objectOfAttrs: {
id: 'container',
class: 'wrapper'
}
}
}

通过不带参数的 v-bind,你可以将它们绑定到单个元素上:

template

<div v-bind="objectOfAttrs"></div>

使用 JavaScript 表达式

注意:仅仅支持简单的表达式

{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div :id="`list-${id}`"></div>

二、 响应式

如下例子:


<script>
export default {
created() {
// 生命周期钩子中创建
},
unmounted() {
// 最好是在组件卸载时
},
data() {
return {
count: 1
}
},
methods :{
increment(){
this.count ++
console.log(this.count)
}
},

// `mounted` is a lifecycle hook which we will explain later
mounted() {
// `this` refers to the component instance.
console.log(this.count) // => 1
this.increment()
}
}
</script>

<template>
<div>
<span>Count is: {{ count }}</span>
<button @click="increment">{{ count }}</button>
</div>
</template>

1、 data 选项来声明组件的响应式状态 2、要为组件添加方法,我们需要用到 methods 选项。

计算属性

说明:模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。这时候我们需要用计算属性了。 实例:

export default {
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
}
}
},
computed: {
// 一个计算属性的 getter
publishedBooksMessage() {
// `this` 指向当前组件实例
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
}

template

<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>

计算属性 与 方法 对比

如上同样用方法也可以实现,但其中有什么区别么?

<p>{{ calculateBooksMessage() }}</p>

js

// 组件中
methods: {
calculateBooksMessage() {
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}

若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果,而不用重复执行 getter 函数。


作者:王二蛋与他的张大花
链接:https://juejin.cn/post/7254043722945675321
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

经济持续低迷环境下,女全栈程序员决定转行了

引言疫情这几年,社会问题层出不穷,而在疫情放开之后,最头疼的就是民生就业问题,大厂裁员,小厂倒闭,每年大批量的应届毕业生也涌入就业市场。近几日,统计局也发布了就业相关数据,全国失业青年达600多万,面对此数据,我们能想到的是实际的失业人数肯定会比公布的数据要多...
继续阅读 »

引言

疫情这几年,社会问题层出不穷,而在疫情放开之后,最头疼的就是民生就业问题,大厂裁员,小厂倒闭,每年大批量的应届毕业生也涌入就业市场。

近几日,统计局也发布了就业相关数据,全国失业青年达600多万,面对此数据,我们能想到的是实际的失业人数肯定会比公布的数据要多很多,尤其是表示 “一周工作一小时以上” 也纳入了就业范围。

image.png

而从我自己的判断来说,记得我自己在去年8月份被裁之后就在xhs发布了一篇关于个人如何交社保的教程,去年年底,观看浏览量不是特别多,而在今年(从年初至今)浏览量以及收藏量蹭蹭往上涨,几乎是每天都有人浏览和收藏我的帖子,抛去网上数据到底如何,光从我自己的感受来看,今年失业人数比去年更多!

image.png

个人只是随手发了一个帖子,将自己如何交社保的步骤记录下来,就有持续的搜索流量,这绝不是一件好事!说明了哀鸿遍野。

一面广大青少年正值青春鼎盛却面临着就业危机,另一方面还要忍受各种开支的骤增,比如深圳统租房的出现,大批人发声:微棠gun出深圳!

曾经破旧拥挤的城中村,为每一位打工人开启了大城市的入口,虽然这个入口短暂,且在关上门的时候,会毫不犹豫抹去你所有的痕迹。
而今这个入口,它不会再破旧拥挤,但会吸取你身上的最后一滴血。

个人经历

1.行政岗转前端

自己曾经拿着一个一本工科学历,因为厌倦行政岗位的勾心斗角,从而挑灯夜战每天在公司加班学习前端到11点,半路出家转行做了前端程序员。

2.刚转行遇吸血领导

而刚转行,又遇到了极其吸血的创业公司(大小周、从0到1项目,双周迭代迭代加班到2点)。

当时不敢辞职,不外乎有几个原因:

  • 刚转行,自己认为技术还比较菜,不敢辞职,被裁了之后才发现外面一大片天地
  • 真的很忙,根本没有时间提升自我与准备面试。因为呆了两年,我自己上了一次救护车,后来离职之后也发现自己因此得了疲劳综合症
  • 比较会吃苦,当时看来觉得可以忍一忍

关于这家公司呢,我想说,我这领导是真的狗,领导是我大一届的学长,曾经担任了大厂某知名项目的组长,号称协同领域的专家,关于此人是我生活中见过最资本的一个人:

  • 针对刚毕业的新人,不培养下属却对下属有着超乎大厂的要求(毫不夸张,你没经历过就不要觉得我是在夸张)
  • 技术部的同事都是很年轻的,做事都兢兢业业,不甩锅,不摸鱼,很多事都是自发的去解决,关于技术水平,我很客观的评价,不菜
  • 在裁我的时候,我呆的时间是13个月,也就是差一个月满2年,但他忽悠我说法律都规定只能给我1+1,我还不满2年,当时对方忽悠毫不脸红,又本着学长+平时看起来正人君子的偏见,在当时就签署了合同,失去的1个月补偿金还好,最伤人的是利用了你的信任,杀人诛心。

3.持续学习

从吸血公司出来之后,进入了相对比较wlb的公司,也清楚认识到自己在程序员领域,女性并不吃香,因此自己也是一直在学习前端技术。

  • 比如自己也曾在掘金发布了上百的技术文章
  • 买教学课程
  • 从零学算法,刷Leetcode
  • github持续输出代码
  • 建立自己的技术博客

image.png

image.png

4.努力不代表有收获

曾经相信自己勤能补拙,后来发现,比你拙的一大批还比你工资高;
曾经熟悉React技术栈,却在失业时找前端兼职时因不会vue而被刷;
曾经将网上的八股文背了再背,面试一二面对答如流,却倒在了三面面试官深问你项目经验; 曾经以为深耕项目经验,学性能优化、前端工程化、架构,却因为面试不会吹牛且遇上近几年经济低迷环境,工资还是那样。
曾经以为,自己努力点,自己性格好点,不断提升,会迎来比较好的人生。
曾经以为,男女平等,男生不应该一人承担经济压力,所以放弃了沉迷貌美如花,选择了与男生一样扛水桶,挑重活,但事实是,那些每天开开心心负责貌美如花的女生比我这种埋头搞钱的女生要幸福很多,对于像花一样的女生,谁不怜爱宽容呢,谁会去宽容一个扎在程序员堆里放弃自己容貌的黄脸婆呢。(看到这里,也许有人觉得我是因为自己长的太丑了,所以才选择搞钱,然而客观来分析,我自己并不丑,虽然说不是校花班花级别,但也可以在普通人群里说的是中上,颜控党眼里也能过得去,不是普信)

然后事实是,有些人,不用长得漂亮,不用能力强,不用对外提供情绪价值或其他价值,他站在那里,就有好的收获,就有人包容就有人爱。

在经历过上述的心理历程之后,明白了职场规则,以及社会运作规律,在大环境下,每个人都在尽自己的努力维持着公平,这个世界,因为有些人经历坎坷,未能坚守住自己底线,从而世界才会有坏人的存在。但大部分情况是,没有绝对的坏人,比如你觉得领导对自己很吸血,但可能领导背后的压力是整个公司的生存(虽然我的领导真的就是单纯的吸血),比如你觉得有些人对自己戾气重,可能当时人家真的内心极其痛苦,而你刚好撞到了枪口上,比如有些人因为诸多原因对你坏,但可能对别人好。

So,个人而言,还是做好自己,看淡所有的行为,同时能有自己的盾和矛。

决定转行

明白自己确实不适合长久做程序员,因此跟大家一样,网上搜了很多搞副业赚钱的路子,排除了偏门以及刑法上的路子,结合我自己的情况,目前已经开始正式着手Vlog自媒体之路了。

  • 买拍摄工具
  • 打造自己的IP
  • 整理自己的衣着、居住环境
  • 学习自媒体知识、拍摄技巧

总的而言,作为一个硬件工科出身的妹子,一直觉得自己更喜欢软件,比如硬件我要调试半天的电路我才能把一个灯泡💡点亮,而计算机,我写一行代码就可以得到反馈,即使是错误的,也能快速做出调整。

但也不可否认,女生在敲代码方面确实跟男生比没有那么大的天赋,就好比玩游戏,大部分女生会玩游戏,但是如果说要打的特别好,男生还是居多。

所以自己也很佩服那些在代码这条路上走的很坚定的女程序员。一起加油吧。

最后,我给各位女程序猿一个小建议,如果没有很高的学历背景或比较好的人脉资源运气,我觉得趁早搞一个副业,但是绝对不要裸辞去搞副业。程序员这个岗位虽然目前已经卷的不行,但瘦死的骆驼比马大,比某些天坑行业还是好很多,我觉得我们还是很幸运的。

image.png


作者:傲娇的萌
链接:https://juejin.cn/post/7246304095375097915
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

微信图片防撤回

了解需求实际生活中,由于好奇朋友撤回的微信图片信息,但直接去要又怎会是我的性格呢。由此萌生出做一个微信防撤回程序(已向朋友说明)。当前网络上其实存在一些微信防撤回程序,不过担心不正规软件存在漏洞,泄漏个人信息,这里也就不考虑此种方法。解决方案思路由于当前微信不...
继续阅读 »

了解需求

实际生活中,由于好奇朋友撤回的微信图片信息,但直接去要又怎会是我的性格呢。由此萌生出做一个微信防撤回程序(已向朋友说明)。

当前网络上其实存在一些微信防撤回程序,不过担心不正规软件存在漏洞,泄漏个人信息,这里也就不考虑此种方法。

解决方案

思路

由于当前微信不支持微信网页版登陆,因此使用itchat的方法不再适用。

后来了解到电脑端微信图片会先存储在本地,撤回后图片再从本地删除,因此只要在撤回前将微信本地图片转移到新文件夹即可。

在此使用Python的watchdog包来监视文件系统事件,例如文件被创建、修改、删除、移动,我们只需监听创建文件事件即可。

安装watchdog包:    pip install watchdog
我的python环境为python3.9版本

实现

1.首先进行文件创建事件监听,在监听事件发生后的事件处理对象为复制微信图片到新文件夹。具体代码如下。

需要注意的是微信在2022.05前,图片存储在images目录下;在2022.05后,图片存储在MsgAttach目录下,并按微信对象分别进行存储。

# 第一步:加载路径,并实时读取JPG信息
import os
import shutil
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

def mycopyfile(srcfile,dst_dir):
if not os.path.isfile(srcfile):
print ("%s not exist!"%(srcfile))
else:
fpath,fname=os.path.split(srcfile) # 分离文件名和路径
if fname.endswith('.jpg') or fname.endswith('.png') or fname.endswith('.dat'):
dst_path = os.path.join(dst_dir, fname)
shutil.copy(srcfile, dst_path) # 复制文件

class MyEventHandler(FileSystemEventHandler):
# 文件移动
# def on_moved(self, event):
# print("文件移动触发")
# print(event)


def on_created(self, event):
# print("文件创建触发")
print(event)
mycopyfile(event.src_path, dst_dir)


# def on_deleted(self, event):
# print("文件删除触发")
# print(event)
#
# def on_modified(self, event):
# print("文件编辑触发")
# print(event)

if __name__ == '__main__':

dst_dir = r"E:\03微信防撤回\weixin" #TODO:修改为自己的保存文件目录
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)

observer = Observer() # 创建观察者对象
file_handler = MyEventHandler() # 创建事件处理对象
listen_dir = r"C:\Users\hc\Documents\WeChat" #TODO:修改为自己的监听目录
observer.schedule(file_handler, listen_dir, True) # 向观察者对象绑定事件和目录
observer.start() # 启动
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()

2.由于微信保存文件以.dat格式保存,因此需要对微信文件格式进行解码,具体解码代码如下。

# weixin_Image.dat 破解
# JPG 16进制 FF D8 FF
# PNG 16进制 89 50 4e 47
# GIF 16进制 47 49 46 38
# 微信.bat 16进制 a1 86----->jpg ab 8c----jpg dd 04 --->png
# 自动计算异或 值
import os

into_path = r'E:\03微信防撤回\weixin' # 微信image文件路径
out_path = r'E:\03微信防撤回\image'

def main(into_path, out_path):

dat_list = Dat_files(into_path) # 把路径文件夹下的dat文件以列表呈现
lens = len(dat_list)
if lens == 0:
print('没有dat文件')
exit()

num = 0
for dat_file in dat_list: # 逐步读取文件
num += 1
temp_path = into_path + '/' + dat_file # 拼接路径:微信图片路径+图片名
dat_file_name = dat_file[:-4] # 截取字符串 去掉.dat
imageDecode(temp_path, dat_file_name, out_path) # 转码函数
value = int((num / lens) * 100) # 显示进度
print('正在处理--->{}%'.format(value))


def Dat_files(file_dir):
"""
:param file_dir: 寻找文件夹下的dat文件
:return: 返回文件夹下dat文件的列表
"""
dat = []
for files in os.listdir(file_dir):
if os.path.splitext(files)[1] == '.dat':
dat.append(files)
return dat

def imageDecode(temp_path, dat_file_name, out_path):
dat_read = open(temp_path, "rb") # 读取.bat 文件
xo, j = Format(temp_path) # 判断图片格式 并计算返回异或值 函数

if j == 1:
mat = '.png'
elif j == 2:
mat = '.gif'
else:
mat = '.jpg'

out = out_path + '/' + dat_file_name + mat # 图片输出路径
png_write = open(out, "wb") # 图片写入
dat_read.seek(0) # 重置文件指针位置

for now in dat_read: # 循环字节
for nowByte in now:
newByte = nowByte ^ xo # 转码计算
png_write.write(bytes([newByte])) # 转码后重新写入


def Format(f):
"""
计算异或值
各图片头部信息
png:89 50 4e 47
gif: 47 49 46 38
jpeg:ff d8 ff
"""
dat_r = open(f, "rb")

try:
a = [(0x89, 0x50, 0x4e), (0x47, 0x49, 0x46), (0xff, 0xd8, 0xff)]
for now in dat_r:
j = 0
for xor in a:
j = j + 1 # 记录是第几个格式 1:png 2:gif 3:jpeg
i = 0
res = []
now2 = now[:3] # 取前三组判断
for nowByte in now2:
res.append(nowByte ^ xor[i])
i += 1
if res[0] == res[1] == res[2]:
return res[0], j
except:
pass
finally:
dat_r.close()


# 运行
if __name__ == '__main__':
main(into_path, out_path)

作者:空气猫
链接:https://juejin.cn/post/7221376169370583101
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

北美 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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

2023年35大龄程序员最后的挣扎

一、自身情况我非科班出身,年龄到了35岁。然后剧本都差不多,2022年12月各种裁员,失业像龙卷风一样席卷社会各个角落。其实30岁的时候已经开始焦虑了,并且努力想找出路。提升技术,努力争增加自己的能力。努力争取进入管理层,可是卷无处不在,没有人离开这个坑位,你...
继续阅读 »

一、自身情况

我非科班出身,年龄到了35岁。然后剧本都差不多,2022年12月各种裁员,失业像龙卷风一样席卷社会各个角落。

  1. 其实30岁的时候已经开始焦虑了,并且努力想找出路。
  2. 提升技术,努力争增加自己的能力。
  3. 努力争取进入管理层,可是卷无处不在,没有人离开这个坑位,你努力的成效很低。
  4. 大环境我们普通人根本改变不了。
  5. 自己大龄性价比不高,中年危机就是客观情况。
  6. 无非就是在本赛道继续卷,还是换赛道卷的选择了。

啊Q精神:我还不是最惨的那一批,最惨的是19年借钱买了恒大的烂尾楼,并且在2021年就失业的那拨人。简直不敢想象,那真是绝望啊。心里不够坚强的,想不开轻生的念头都会有。我至少拿了点赔偿,手里还有些余粮,暂时饿不死。

image.png

二、大环境情况

  1. 大环境不好已经不是秘密了,整个经济走弱。大家不敢消费,对未来信心不足已经是板上钉钉的事了。

  2. 这剧本就是30年前日本的剧本,不敢说一摸一样。可以说大差不差了,互联网行业的薪资会慢慢的回归平均水平,或者技术要求在提升一个等级。

  3. 大部分普通人,还是做应用层拧螺丝,少部分框架师能造轮子也就是2:8理论。

  4. 能卷进这20%里,就能在上一层楼。也不是说这行就不行了,只不过变成了存量市场,而且坑位变少,人并没有变少还增加了。

  5. 不要怀疑自己的能力,这也不是你的问题了,是外部环境导致的市场萎缩。我们能做的就是,脱下孔乙己的长衫,先保证生活。努力干活,不违法乱纪做什么都是光荣了,不要带有色眼镜看待任何人。

三、未来出路

未来的出路在哪里?

这个我也很迷惑,因为大佬走的路,并不是我们这些普通的不能在普通的人能够走的通的。当然也有例外的情况,这些就是幸存者偏差了。

我先把chartGPT给的答应贴出来:

image.png

可以看到chartGPT还是给出,相对可行有效的方案。当然这些并不是每个人都适用。

我提几个普通人能做的建议(普通人还是围绕生存在做决策):

  1. 有存款的,并且了解一些行业的可以开店,比如餐饮店,花店,水果店等。
  2. 摆摊,国家也都改变政策了。
  3. 超市,配送员,外卖员。
  4. 开滴滴网约车。
  5. 有能力的,可以润出G。可以吸一吸GW“free的air”,反正都是要被ZBJ榨取的。

以上都是个人不成熟的观点,jym多多包涵。

每个行业都卷,没有很好的建议都是走一步算一步,保持学习,减少精神内耗


作者:可乐泡枸杞
链接:https://juejin.cn/post/7230656455808335930
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

为什么React一年不发新版了?

web
大家好,我卡颂。 遥想前几年,不管是React还是Vue,都在快速迭代版本,以至于很多同学抱怨学不动了。 而现在,React已经一年没更新稳定release了。 甚至有人认为,这就是前端已死最直接的证据: 那么,React最近一年为什么不发版了呢?是因为前...
继续阅读 »

大家好,我卡颂。


遥想前几年,不管是React还是Vue,都在快速迭代版本,以至于很多同学抱怨学不动了


而现在,React已经一年没更新稳定release了。


上一次发版还是22年6月


甚至有人认为,这就是前端已死最直接的证据:



那么,React最近一年为什么不发版了呢?是因为前端框架领域已经没有新活儿可整了么?React v19是不是遥遥无期了?


欢迎围观朋友圈、加入人类高质量前端交流群,带飞


最近一年React活跃吗?


不想看长文章的同学,这里一句话总结本文观点:



React之所以一年没发版,并不是因为无活可整,而是在完成框架从UI库到元框架的转型



首先,我们来看看,最近这一年React的更新活跃度是否降低?


从代码push量来看,最近一年甚至比release产出较多的前几年更活跃:



既然更活跃,那React这段时间到底在做什么呢?从代码增删行数可以一窥端倪,其中:




  • 绿色柱状代表代码增加行数




  • 红色柱状代表代码减少行数




  • 红色折线代表代码行数总体趋势





代码量变化来看,React历史上大体分为四个时期:




  • 13年开源,到17年之前的功能迭代期




  • 持续到18年的重构期(重构React Fiber架构)




  • 18~22年基于Fiber架构的新功能迭代期




  • 22年至今的重构期




功能迭代期重构期的区别在于:




  • 前者主要是在稳定的架构上迭代新特性




  • 后者一般重构底层架构的同时,重构老特性




剧烈的代码量波动通常发生在重构期。比如,在最近的重构期内,PR #25774删除了3w行代码。




这个PR主要改变React对于同一个子包,同时拥有.new.old两个文件的开发模式



最近一年React都在干啥?


明确了React最近一年处于重构期。那么,究竟是重构什么呢?


答案是 —— 将RSCReact Server Component,服务端组件)接入当前React体系内。


有同学会问:RSC只是个类似SSR的特性,为什么要实现他还涉及重构?


这是因为RSC不仅是一个特性,更是React未来主要的发展方向,其意义不亚于Hooks。所以,围绕RSC的迭代涉及大量代码的重构。比如:




  • SSR相关代码需要修改




  • SSR代码修改导致Suspense组件代码修改




  • Suspense的修改又牵扯到useEffect回调触发时机的变化




可以说是牵一发而动全身了。


RSC为什么重要


为什么RSCReact这么重要?要回答这个问题,得从开源项目的发展聊起。


开源项目要想获得成功,一定需要满足目标用户(开发者)的需求。


早期,React作为前端框架,满足了UI开发的需求。在此期间,React团队的迭代方向主要是:




  • 摸索更清晰的开发范式(发布了Error BoundraySuspenseHooks




  • 修补代码(发布新的Context实现)




  • 优化开发体验(发布CRA




  • 底层优化(重构Fiber架构)




可以发现,这些迭代内容中大部分(除了底层优化)都是直接面向普通开发者的,所以React文档(文档也是面向开发者的)中都有体现,开发者通过文档能直观的感受到React不断迭代。


随着前端领域的发展,逐渐涌现出各种业务开发的最佳实践,比如:




  • 状态管理的最佳实践




  • 路由的最佳实践




  • SSR的最佳实践




一些框架开始整合这些最佳实践(比如Next.jsRemix,或者国内的Umijs...)


到了这一时期,开发者更多是通过使用这些框架间接使用React


感受到这一变化后,React团队的发展方向逐渐变化 —— 从面向开发者的前端框架变为面向上层框架的元框架。


发展方向变化最明显的表现是 —— 文档中新出的特性普通开发者很少会用到,比如:




  • useTransition




  • useId




  • useMutableSource




这些特性都是作为元框架,给上层框架(或库)使用的。


上述特性虽然普通开发者很少用到,但至少文档中提及了。但随着React不断向元框架方向发展,即使出了新特性,文档中已经不再提及了。比如:




  • useOptimistic




  • useFormStatus




上述两个Hook想必大部分同学都没听过。他们是React源码中切实存在的Hook。但由于是元框架理念下的产物,所以React文档并未提及。相反,Next.js文档中可以看到使用介绍。


总结


React之所以已经一年没有发布稳定release,是因为发展方向已经从面向开发者转型为面向上层框架


在此期间的更新都是面向上层框架,所以开发者很难感知到React的变化。


但这并不能说明React停止迭代了,也不能据此认为前端发展的停滞。


如果一定要定量观察React最近一年的发展,距离React v19里程碑,已经大体过半了:


收起阅读 »

手把手教你从零开始集成声网音视频功能(iOS版)

说明1.环信音视频和声网音视频 是两个不同的系统,所以如果要切换的话,需要集成声网的sdk,环信音视频的sdk可以直接废弃2.文章会介绍如何用声网的音视频跑通demo,可以了解整个音视频通话的流程,3.文章会介绍已经集成了环信im功能如何在集成声网添加音视频功...
继续阅读 »

说明

1.环信音视频和声网音视频 是两个不同的系统,所以如果要切换的话,需要集成声网的sdk,环信音视频的sdk可以直接废弃

2.文章会介绍如何用声网的音视频跑通demo,可以了解整个音视频通话的流程,

3.文章会介绍已经集成了环信im功能如何在集成声网添加音视频功能

前提条件

1.有环信开发者账号和声网的开发者账号

2.macOS系统,安装了xcode集成环境

跑通Demo

1.下载iOS Demo 地址:https://www.easemob.com/download/im

2.我这边下载的是4.0.3 版本,如果你的Xcode 版本运行demo报错的话,先找到podfile文件打开注释,并加上:config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0’,如下图 ,在pod install

3.为了测试方便可以先把这个appkey 配置成自己的

4.连续点击版本号,切换成账号密码登录,到此im部分完成

搭建App Server生成声网token

2.如果出现Starting server at port 8082 说明搭建成功

3.在下图这里替换成自己声网的appid

4.在callDidRequestRTCTokenForAppId 这个方法做一下修改,主要是换成你自己的服务器生成的token,

5.以上修改完成就可以进行音视频通话了,如果通话正常可以去声网的控制台,看到通话记录。

到此恭喜你跑通Demo

把声网集成到已有项目中

说明:如果你之前集成环信的音视频,那么就直接废弃掉,从头集成声网音视频,我这边从新建项目开始

1.新建项目,并添加相应的库,pod install 一下,添加麦克风和摄像头权限

2.AppDelegate 文件里面进行环信初始化

3.使用xib 创建几个控件,并进行绑定

4.在 login点击事件调登录操作,登录成功之后进行EaseCallManager 类的初始化

注意:EaseCallManager只能在登录成功之后才能初始化,要不然发起通话会报错


5.实现EaseCallDelegate代理方法,需要在callDidRequestRTCToken回调中,获取APPserver的token,并设置,如下图

6.在call方法发起一对一视频通话,如下图

至此 代码完成,可以运行在两台设备上查看效果,如果能正常进行视频通话,那么恭喜你集成成功

总结

1.在环信控制台创建im项目,拿到appkey

2.在声网控制台创建音视频项目拿到appid 和 appCertificate

3.参考声网给的go语言的APPserver示例,全部复制下来,填上声网的appid 和 appCertificate,就直接运行

4.创建iOS项目,集成

pod 'AgoraRtcEngine_iOS/RtcBasic' //声网音视频库

pod 'HyphenateChat', '~> 4.0.3' //环信im库

pod 'EaseCallKit' //环信IMSDK作为信令封装的声网音视频SDK通话带UI组件库

这三个库

5.AppDelegate 文件里面进行环信初始化填上环信的appkey

6.登录成功的方法里面初始化EaseCallManager

7.发起视频通话邀请

8.邀请方和被邀请方都会走 func callDidRequestRTCToken(forAppId aAppId: String, channelName aChannelName: String, account aUserAccount: String, uid aAgoraUid: Int)
这个加入音视频通话频道前触发该回调,在这个回调函数里面获取各自的声网token,然后调用setRTCToken:channelName:方法将token设置进来

完毕

参考链接

收起阅读 »

用Kotlin开发时,如何优化一下Lambda的开销

前言 在Kotlin中声明一个Lambda表达式,在编译字节码中会产生一个匿名类。此匿名类中有一个 invoke方法,为Lambda的调用方法,每次调用会创建一个新匿名类对象。可想而知,Lambda语法虽简洁,但额外增加的开销也不少。还有,若Lambda捕捉某...
继续阅读 »

前言


在Kotlin中声明一个Lambda表达式,在编译字节码中会产生一个匿名类。此匿名类中有一个 invoke方法,为Lambda的调用方法,每次调用会创建一个新匿名类对象。可想而知,Lambda语法虽简洁,但额外增加的开销也不少。还有,若Lambda捕捉某个变量,每次调用时都会创建一个新对象,会导致效率较低。


在Kotlin中采取优化Lambda额外开销的方式就是:内联函数。


回顾Java中采取的优化方式:invokedynamic


invokedynamic技术是Java7后提出,在运行期才产生相应翻译代码。
invokedynamic被首次调用时,会触发产生一个匿名类来替换中间码invokedynamic,后续调用会直接采用该匿名类代码。这种做的好处主要有:



  • 具体的转换实现是在运行时产生,在字节码中只有一个固定的invokedynamic,所以需要静态生成的类的个数及字节码大小都显著减少。

  • 与编译时写死在字节码中的策略不同,利用invokedynamic可把实际的翻译策略隐藏在JDK库的实现, 极大提高了灵活性,在确保向后兼容性的同时,后期可以继续对编译策略不断优化升级

  • JVM天然支持针对该方式的Lambda表达式的翻译和优化,开发者在书写Lambda表达式时,可以完全不用关心这个问题,极大地提升了开发体验。


Kotlin中采取的优化方式:内联函数


Kotlin拥抱内联函数,在C++、C#等语言中也支持这种特性。可以用 inline 关键字来修饰函数,这些函数就称为内联函数。它的函数体在编译期被嵌入到每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。


内联函数的工作原理并不复杂,就是Kotlin编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样就不存在运行时的开销。


若想在用Kotlin开发时获得尽可能良好的性能支持,以及控制匿名类的生成数量,就来学习下内联函数:


如以下示例:

fun main() {
foo {
println("dive into Kotlin...")
}
}

fun foo(block: () -> Unit) {
println("before block")
block()
println("end block")
}

以上声明一个高阶函数foo,接受一个Lambda 参数为 () -> Unit,最后在main函数中调用它。下面是通过字节码反编译的Java代码:

public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
foo((Function0)null.INSTANCE);
}

public static final void foo(@NotNull Function0 block) {
Intrinsics.checkParameterIsNotNull(block, "block");
String var1 = "before block";
System.out.println(var1);
block.invoke();
var1 = "end block";
System.out.println(var1);
}

调用foo会产生一个Function()类型的block类,然后通过 invovke() 来执行,这样会增加额外生成类和调用开销。下面给foo函数加上inline修饰符:

inline fun foo(block: () -> Unit) {
println("before block")
block()
println("end block")
}

看看相应Java代码:

public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
String va1 = "before block";
System.out.println(var1);
// block函数体在这里开始粘贴
String var2 = "dive into Kotlin...";
System.out.println(var2);
// block函数体在这里结束粘贴
var1 = "end block";
System.out.println(var1);
}

public static final void foo(@NotNull Function0 block) {
Intrinsics.checkParameterIsNotNull(block, "block");
String var2 = "before block";
System.out.println(var2);
block.invoke();
var2 = "end block";
System.out.println(var2);
}

如上面所说,foo 函数体代码及被调用的Lambda代码都粘贴到了相应调用的位置。试想下,若是一个公共方法,或被嵌套在一个循环调用中,该方法势必会被调用很多次。通过inline函数,可以消除这种额外调用,从而节省开销。


内联函数一个典型应用场景就是Kotlin集合类。Kotlin 中集合函数式API,如map、filter都是被定义成内联函数:

inline fun <T, R> Array<out T>.map {
transform: (T) -> R
}: List<R>

inline fun <T> Array<out T>.filter {
predicate: (T) -> Boolean
}: List<T>

很容易理解,因这些方法都接收Lambda表达式参数,需要对集合元素进行遍历操作,因此把相应的实现进行内联无疑是适合的。


但内联函数不是万能的,以下情况应避免使用内联函数:



  • JVM对普通函数已经能够根据实际情况智能判断是否进行内联优化,因此并不需要对其使用Kotlin的inline语法,否则只会让字节码变得更加复杂

  • 尽量避免对具有大量函数体的函数进行内联,会导致过多的字节码数量

  • 一个函数被定义为内联函数,就不能获取闭包类的私有成员,除非把它声明为internal

作者:沐小琪吖
链接:https://juejin.cn/post/7233809825204568124
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

朝花夕拾 - 卷王的自白(光头祭天,法力无边)

一、震撼开场 做一个卷王 ta 有什么错,无非就是 —— 「秃」了那么一点点!!! 【震惊】更加震撼的削发视频 咳咳咳,一一回复: 自愿的 没有想不开 没有考到寺庙 心态正常 …… 如果非要给这次的行为贯穿一个理由,那就是「下周四就 28 岁了」「头...
继续阅读 »

一、震撼开场


做一个卷王 ta 有什么错,无非就是 ——


04-10-01.jpg


「秃」了那么一点点!!!



咳咳咳,一一回复:



  • 自愿的

  • 没有想不开

  • 没有考到寺庙

  • 心态正常

  • ……


如果非要给这次的行为贯穿一个理由,那就是「下周四就 28 岁了」「头发长长太快想凉快点」!


二、“正文”


Hello 小伙伴们早上、中午、下午、晚上和深夜好,这里是 jsliang~


鸽了近一个月,终于能恢复正常作息,和小伙伴们一起折腾、聊天吹水、学习啦!


04-10-02.png


当然,这次我想在 jsliang 的「朝花夕拾」频道,和小伙伴们聊的,不仅仅是鸽了的这个月的内容。


更多的,咱们要从年初,乃至年前聊起。


虽然可能会聊到很多内容,但是咱们尽可能长话短说!



  1. 这几个月怎么过来的?

  2. 你觉得有意思的事情是什么?

  3. 你觉得很失败/颓的事情是什么?

  4. 这几个月发生的事情有哪些让你关注的?

  5. 后面想做啥事情?


三、话痨小剧场


本篇图文尽可能不涉及技术等硬通货,纯粹想聊天吹水交朋友,感兴趣的小伙伴可 + VX: Liang123Gogo 或者关注公众号 「飘飞的心灵」


z-small-wechat.jpeg



朋友圈每天更新丰富内容哦~



3.1 这几个月怎么过来的?


从 2 月过完年回来后,996 甚至偶尔 997 呀,然后喜提「卷王」称号。


大概故事线就是:



  1. jsliang 想挑战下自己

  2. 然后转开发组,挑战新项目

  3. 不巧作为几年老开发,评估需求失败,3 周的需求,愣是做了 10 周

  4. 于是「自愿」加班,顶着压力 996 乃至 997


Q:你干嘛哎哟!这么一说这几个月可歌可泣的故事,你一点都没体现啊!


04-10-03.png


A:这你就不懂了吧,这里不说的简单点,小伙伴们怎么往下看~


3.2 你觉得有意思的事情是什么?


这几个月感觉蛮有意思的事情,大概有那么几个。


首先,能学到一些没玩过的技术啦!


对于一些「浅薄」的技术,我总是那么沉迷,比如这一次学习了:



  1. Formily。阿里面向中后台复杂场景的表单解决方案

  2. Slate / tinymce。这 2 款不同的富文本编辑器,一种是 React,一种是 Vue,在 2 个不同项目都接触了


04-10-04.png


然后,就是对旧知识的巩固啦!


没错,就是你 React Hook~


有一说一每次写 Hook 我都很沉迷


—— 为什么这个组件多次渲染,为什么这个组件又不渲染了!


回头写一篇小作文吧,在这里吐槽占篇幅太大了。


接着,就是对升华身心的一种挑战。


这几个月被大佬吐槽过能力不行、需求管理没做好;


这几个月碰到过技术问题,经常辗转反侧想不通;


这几个月熬夜有点多,脸色蜡黄黑眼圈……


04-10-05.png


不过还好,每天坚持跑步和跟小伙伴吐槽,让我挺过来了,很有意思,下次别喊我体验了,哈哈~


最后,就是通过直播、发视频、写文章认识了更多的小伙伴。


现在 VX 有 2000+ 好友,距离 30 岁的 5000 好友又近了一步!


不同小伙伴让我认识了世界不一样的精彩~


3.3 你觉得很失败/颓的事情是什么?


这个算糗事集合了吧哈哈~


Round 1


小丑鸭碰到白天鹅的尴尬~


04-10-06.png


Round 2


如 3.2 所说,因为瞎排期,明明做了 10 周的需求,被我在需求稿上写成了 3 周。


然后,我就被骂了哈哈哈,没人撑我,甚是失败。


当时有想过跑路……


Round 3


赚的钱还是那么卑微而少,但是肉眼可见爸妈年轻不再。


有时候会迷茫啥时候能回家买地皮自己起楼,2023 年的机遇我到底抓住了没有。


04-10-07.png


以上,简单说说 3 件印象比较深刻的,后面有更多失败和令人颓废的事咱们再唠嗑唠嗑~


3.4 这几个月发生的事情有哪些让你关注的?


有 2 个:



  • 人工智能

  • 前端已死


当时本来想蹭热度去写写,后面自己把自己说服了,按表不发,等后面逐个打通(如果有小伙伴感兴趣听我吐槽的话)


目前只能说保持对这 2 个话题的关注,提升自己对这 2 块相关技能的学习。


其他的,慢慢来啦,咱走个长期攻略。


3.5 后面想做啥事情?


先补全一些遗憾:



  • CSS 系列更新到第 8 篇(当前第 5 篇,尽量补充几个实用性的)

  • 出门旅游 1 次

  • 补充文章+视频:人工智能

  • 补充文章+视频:前端已死

  • 恢复每日晚上 9:00-11:00 的直播


长期上,可能会关注一下「自由职业」和「独立能力」,毕竟真不能想象自己还能在一线做开发多久~


那么,就酱啦!


周六下午要加班,晚上我会把剃光头的视频和本期内容整合成一个小视频,晚上回去直播,欢迎关注光头前端!


See you tonight~




不折腾的前端,和咸鱼有什么区别!


觉得文章不错的小伙伴欢迎点赞/点 Star。


如果小伙伴需要联系 jsliang



个人联系方式存放在 Github 首页,欢迎一起折腾~


争取打造自己成为一个充满探索欲,喜欢折腾,乐于扩展自己知识面的终身学习斜杠程序员。


作者:jsliang
链接:https://juejin.cn/post/7223417872596697148
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

毕业两年,我的生活发生了翻天覆地的变化

去年从大学毕业的我大概没想到自己仅仅毕业两年,生活、心态会发生这么多变化吧。 我学会接受了自己的平凡 大概大家都做过这样的梦吧,毕业我要月入几万,我要做职场最靓的仔,我要去大城市闯荡出属于自己的天地。结果这几年经济状况不太好,很多企业都在裁员,校招名额明显减...
继续阅读 »

去年从大学毕业的我大概没想到自己仅仅毕业两年,生活、心态会发生这么多变化吧。


c129b212a0f596a998f904adaf8772c.jpg
我学会接受了自己的平凡


大概大家都做过这样的梦吧,毕业我要月入几万,我要做职场最靓的仔,我要去大城市闯荡出属于自己的天地。结果这几年经济状况不太好,很多企业都在裁员,校招名额明显减少,于是校招、找工作的时候就认清现实,好像是个offer就去。


毕业后,我从中选了最满意的一个offer,前往深圳。


在公司的一年,我浑浑噩噩,每天感觉时间像不够用似的。每天升级打怪,学到了很多,后面因为公司业务原因,跳槽了。但是很开心的是在这里认识到了很多小伙伴,大家现在也有联系,时不时出来喝个酒。也和公司技术很牛的大佬成了朋友。也学会接受了自己的平凡,原来真的有那种写代码很轻松,把写代码当游戏,把工作当乐趣的人呀,真的是降维打击我这个小菜鸡。


78840c794394308c40b286f3321073b.jpg


人生就是不断的坍塌,然后自我重建。


一年过去了,我好像没了刚出社会的冲劲,偶尔下班也会学习,但是没有像刚毕业一样有很多的学习热情,闲暇时间就会多刷技术贴,技术文章。


跳槽跑路了,结果我发现我从刀山跑到了火海。入职后我才知道我所在部门的前端再过几天就要跑路了,相当于就我一个啥也不熟悉的来接锅了。组长带人和我交接时用了两小时,然后留下一脸蒙蔽的我。总之,后面度过了艰难的两个月,好歹算是背着锅缓慢前行。后面公司又出了不少幺蛾子,挺了7个月,忍不了又跑路了。但是这几个月吃的苦也让我的工作能力上涨,技能增多,抗压能力增强。于是我发现“人生就是不断的坍塌,然后自我重建,最后堆成了现在的我”。


相亲也不是不行


话说我年纪也不大,但是不知道为什么毕业两年,时间飞逝,居然就开始有点年龄焦虑。工作后也没什么渠道去认识女孩子。办公室一屋子男生,问他们对象哪儿来的,都说自己的对象是new来的。


也不是没被家里人拉去相亲过,第一年我觉得自己还小,也考虑到在家乡相亲的岂不是要异地啊,无比抗拒。第二年我成熟了,(不是,被毒打了)发现工作后是真难找对象啊。


转折点在某个风和日丽的下午,大数据都知道我单身了。我刷脉脉看到了相亲贴,然后知道了大厂相亲角公众号这个平台,这个公众号标榜用户都是阿里、字节、百度、腾讯、华为等大厂的单身同学。因为注册需要企业认证,最开始不太信任平台,就没注册。先进了他们的单身群观望,后面群里面每天都发一些嘉宾推荐,然后想到这种有门槛的,用户都是经过审核的,岂不是更可靠。感觉确实还蛮靠谱的,于是就注册了。被拉到上墙群后发现上墙群里的群友们都好优秀,小厂的我夹缝求生。


我算是发现了,人的观念是会一直变的,想当初我怎么也想不到自己会去相亲吧。


66b61a33c9903edc332893e26b27945.jpg


总结


毕业两年,我的生活好像变了很多,又好像没啥变化,曾经我不能接受的,现在又行了。曾经觉得自己可以了,现在也认清现实了。哈哈哈哈哈。


作者:苍苍尔
链接:https://juejin.cn/post/7158708534471819278
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

从小到大为何一谈学习就愁眉苦脸

谈“学习”色变 从小到大,我们总有许多东西要学习:除了各个科目如数学、英语的学习,还有一些被爸妈安排学习的舞蹈、钢琴等等。 总之,我们必须持续不断地学习,如果不学习就会学习成绩落后,别无他法。 因此,谈到“学习”这个词,总会可能给人一些不舒服的感觉,不信你看看...
继续阅读 »

谈“学习”色变


从小到大,我们总有许多东西要学习:除了各个科目如数学、英语的学习,还有一些被爸妈安排学习的舞蹈、钢琴等等。


总之,我们必须持续不断地学习,如果不学习就会学习成绩落后,别无他法。


因此,谈到“学习”这个词,总会可能给人一些不舒服的感觉,不信你看看:


爷爷奶奶会说:“把电视关了,快去学习。”


老师会说:“你家孩子啥都好,就是不爱学习。”


每次我爸送我出门,也绝对不会忘记加一句“学习努力一点。”


工作了,老板会说:“一会开个会,组织学习。”公司安排各种培训,也是为了学习...


于是,“学习”这个词会带给我们一些不舒服的感觉,不是让我们想起年轻时埋头于做题参加各种考试的岁月,就是给人一种单调枯燥的“复读机”式的培训、会议之类的低质量教育活动。


其实以上种种都是被动学习,我们一开始就没体会到真正的学习。


羊浸式培训



羊浸(现实中)是指把毫无防备的羊浸到一个大水箱里面做清洗,去除它们身上的寄生虫。



羊排成一队,你抓起一只浸到水箱里,让它感受一次强烈的、陌生的、中毒性的清洗经历。但是,药性会逐渐失效,所以过段时间你不得不对它们再次做清洗。


这种模式,在公司可能会很流行,也称为羊浸式培训。比如召集一大堆不知情的员工,在一个陌生的环境中通过密集的方式,花三到五天的时间培训他们,然后培训完颁发一个证书,宣布他们获得了什么优秀头衔。但培训的效果会逐渐减弱,于是第二年必需再来一次“进修”课程。


说一个自己的故事吧。


在我刚进大一那会,就有学校的师兄师姐向我们推销英语学习,到后面才知道是在培训机构支持下的俱乐部。还加入了 4 天 3 夜的训练营,来自不同学校的学生聚集到一起,其中感受到各种励志的故事,觉得很受用,也的确获得了优秀营员的证书,发誓今后一定要改头换面,学好英语。


但是,现在我还是没能学好口语。是的,"羊浸式"培训不起作用。


殊不知,我们容易把受教育的过程,当做学习者被动接受的过程。我们会被灌输各种知识,而不是自己主动进行学习。


再说一个身边的例子,在我们大三的时候,面临找工作或者考研的两难问题时,培训机构出现了。某 Java 培训机构告诉我们,学 Java 非常好找工作,毫无编程基础也可以快速入门。只要学好基础,会点数据库和SQL基础,学会网络编程,编写 JSP 页面,就可以找到一份 Java 编程的工作了;如果再下点功夫,学习 Structs、Hibernate、Spring 等流行框架,就可以找到好工作了,至少月薪 10K ...。


在我们那个时候,这样的条件和出路是多么诱人。本来是不确定与迷茫的大三时期,突然有这么好的机会摆在面前,不抓住怎么能行?于是,专业半数以上的同学都选择了这个培训班。于是,一个专业通过突击一下子拥有了大片的 Java 开发人员,或者说“快餐式”程序员。


但是,几年下来。当我再问曾经上过培训班的同学,迫于无奈,好几个已经退出 Java 开发人员的队列了。


脱离学校教育之后,我们再谈“学习”


为什么羊浸式方法没有用呢?



  • 学习不是强加于你的,而应该是你主动做的事情

  • 仅仅掌握知识,而不是实践,没有用。要学以致用

  • 随机的方法,没有目标和反馈,往往会导致随机的结果


单纯密集的课堂教育最多只能给你正确的方向,而且紧紧掌握知识的提纲,并不会提高专业水平。


于是,我们不仅该问,脱离学校教育之后,我们该如何学习?


1、 你需要持续的详细目标。



  1. 无论是在职业生涯,还是个人生活中。为了学习和成长,需要设定一些目标,比如我要学好 Python、我要减肥。

  2. 但是这些目标有点泛,我们需要详细一点:什么时候开始学 Python,学好 Python 用来爬虫;要减多少斤才算减肥,是通过控制食量还是增加运动?


2、 持续的反馈以了解你的进展


比如,这个月减了多少斤,这个月学了哪些章节 Python 知识点,只有确切的数字带给我们真实的反馈。



你不必看清你去往何处,不必看清你的目的地和沿途的一切。你只需看清面前的一两米即可。



3 更加主动全面的学习目标


除了个人的目标,还可以指定更大背景下的目标:比如事业、家庭、财务...


将学习变成一件我们必须掌握的事情,用科学的方式。更多关于目标的制定方法可以参考 SMART 模型。


总结


脱离学校后,为了满足我们的兴趣和需求,主动学习才是破解之道。


作者:宇宙之一粟
链接:https://juejin.cn/post/7214427834994982971
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一位普通前端程序媛转行创业阶段小结

背景介绍 大家好,我是 Gopal。在我自己的年中总结——「我在 Shopee 工作这两年」 中提到,假如赞破 200,就更新女朋友的年中总结。 一开始,感觉破 200 很难,没想到大家那么给力。 但是她最近比较忙(也可能是比较 lan),所以我们最后以采访的...
继续阅读 »

背景介绍


大家好,我是 Gopal。在我自己的年中总结——「我在 Shopee 工作这两年」 中提到,假如赞破 200,就更新女朋友的年中总结。


一开始,感觉破 200 很难,没想到大家那么给力。


但是她最近比较忙(也可能是比较 lan),所以我们最后以采访的方式,分享给大家。


上篇我们提到,我女朋友的身份从一个「前端开发」转变成「创业者」,所以本篇文章重点突出创业后她的一些思考。


采访者:Gopal。受访者:QPQ。


以下标题均为问题,内容均为回答。




采访开始分割线




创业方向


抖音电商(抖店)。


具体的工作就是:前期确定类目,选货,进货(商品+打包袋),做商品链接,上链接卖货,谈物流,打包发货,售后,客服,日常运营等。


创业跟「打工」最大的区别是?


最大的区别就是创业需要自己缴社保,打工公司给缴哈哈哈。


然后目前由于是一个人在创业,所以一切的事物都需要自己做包括选货,进货(商品+打包袋),做商品链接,上链接卖货,打包发货,售后,客服,日常运营等, 也就是自己是自己的老板也是自己的员工。


之前工作呢是前端开发,分工明确做前端开发就好。现在是整条链路都需要自己经手,也是一种不一样的体验。


不过这个得话看个人,我自己是乐在其中,虽然所有纷繁杂乱的事情都得自己做,但很有成就感。另外就是身份上的转变,从之前的技术开发关注功能与交互转变为了现在直接接触用户,跟用户产生利益关系的这么一个身份,一种不太一样的感觉。


创业后的生活节奏?


生活节奏呢有时候忙起来真的可以做到废寝忘食,自从做抖店以来直接从50多公斤降到最低时47,减肥很轻松哈哈哈哈。


不过充实的一天忙完之后是最开心轻松的时刻,一般这个时间段我会用来逛公园,吃自己喜欢的。


但是也时长有流量冷淡,生意冷淡的时候,会比较忧桑和焦虑,尤其是突然从很高的流量和单量降到很低的流量和单量的时候心情会很忧桑,也会很焦虑。


另外处理售后也是门考验人的技术活,有时候碰到一些难搞的售后,难受一整天都不在话下,此处心疼所有的客服小姐姐一秒钟。不过通过售后也能发现一些问题,从开店到现在通过不断调整优化目前已经减少了很大一部分售后问题可喜可贺。


创业有遇到什么困难么?


人货场是硬伤。



  1. 人手问题。有时候生意特别好但是人货场跟不上真的很可惜,浪费了流量。这个真是硬伤目前无法解决,只能后面如果越做越好了有能力雇个助理看看。

  2. 场地和囤货问题。



  • 第一是场地受限放货和找货都很艰难,这是硬伤,只能后面有能力了整个仓库试下。

  • 第二有时候有一些产品卖爆了会再搞一大批,回来后发现并没有那么畅销了,就会存在囤货压力。爆款补货真的不能搞多,饥饿营销也是不错的策略,多了就不香了。

  • 第三目前所在地区域距离货品工厂挺远的,拿货也是一个硬伤,相比在货品工厂附近的店家就很劣势。



  1. 经常性会遇到瓶颈期,如何破局有点难搞。目前也正在经历,也做了很多尝试但暂时所有的尝试都还没有突破瓶颈,还需要不断的学习同行和不断的尝试不同的方式来处理这个问题。


未来计划


期望明年的这个时候能有仓库和员工哈哈哈哈,美丽的愿望。




采访结束分割线




最后,我自己总结一下。


首先,我们是深思熟虑之后才做出来的决定,并不是冲动。



  • 我女朋友一直有创业的想法。

  • 我们还年轻,现阶段负担还没那么大。设想,假如到 30+ 岁,那时候,做出一个选择,可能需要考虑的事情就更多了。也就是试错的代价可能没那么大。

  • 我们两个人,起码保证一人还是有稳定收入的前提下。

  • ...


另外,创业真的很艰难,作为一个「陪跑者」,还是有所体会。


最后一点,我觉得只要我们做的事情不违法,不违反道德,不对他人造成伤害,那你想做就去做吧


共勉!


作者:Gopal
链接:https://juejin.cn/post/7123914177168703518
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

路才走了一半,为何停下?(2023 年中总结)

这篇文章姑且算的上是我 2023 年的年中总结。 我最近看到了这样一句话:“网站的流量是由于先前写的文章,你现在的成就是由于之前的努力或者有远见的选择。” 我现在的生活在我看来是舒适安逸的,有着不重的课业,单身,实习并拿着对学生来说不错的薪水,可预见的会到一个...
继续阅读 »

这篇文章姑且算的上是我 2023 年的年中总结。


我最近看到了这样一句话:“网站的流量是由于先前写的文章,你现在的成就是由于之前的努力或者有远见的选择。”


我现在的生活在我看来是舒适安逸的,有着不重的课业,单身,实习并拿着对学生来说不错的薪水,可预见的会到一个不错的公司,并且一直摆烂好像也有一个光明的未来,这份答卷是两年前的我,选定方向,然后持之以恒的努力所带来的。


然后呢,我现在的现状就是,得过且过,然后愉快的摆烂,长期躺尸带来的空虚感以及身体的虚弱感让我开始思考,如果我现在就丢下了笔,我半年后的答卷会在那里? 是依旧保持现状,被身后的小伙伴追上?还是在某一个瞬间醒悟,然后开始改变?


我认为我在前行路上丢掉了很多的东西,所以我想写篇文章来记录,来反思,这篇文章就是如此,所以这篇文章并不会有太多经历的回忆,当然,由于我这半年几乎一直在蔚来实习,其实生活也是相当的单调,接下来让我们进入正题吧。


image.png


Part One 人一旦停下了思考,未来的结果也就注定了。


看一下微信读书的记录吧:


一月份读了9h39min,二月份读了11小时21min,三月份只读了5h28min,四月份呢,也只有11h56min,五月份稍微好些,到了22h46min,六月份第一周,你只读了3min。


我知道你读了很多网络小说,比如《诡秘之主》这类小说,他确实有很多精妙的设计,你不需要动脑子,读起来当然舒适,我曾和一个朋友聊过纯粹的读书是什么,他说:“那本质上是一种玩物丧志。”我无意争论这些书是否会和我吃的饭一样融入血肉,只是对我个人而言,我希望他们留下些什么,每本书至少要让我学到一个道理,否则和沉迷某一件事物无法控制自己有什么区别?我不否认这很功利,但自我提升就是如此。


所以对前半年的第一个反思,就是真真正正的重启思考,为什么项目中的这个事情你没有考虑到,为什么你对边缘案例不够敏感?为什么一个月了这个事情你还没做完,尤其是当这个事情并不困难的情况下?哪些微小的习惯能使得你变得更坚韧?


我向来是一个行动者,但行动的往往太快了,太急于拿到这个结果,在一定程度上要学会思考,克服无脑的鲁莽。



要紧的是果敢的迈出第一步, 对与错先都不管,自古就没有把一切都设计好再开步的事。别想把一切都弄清楚,再去走路。鲁莽者要学会思考, 善思者要克服的是犹豫。 目的渴求完美,举步之际则无需周全。



共勉!


Part Two 像重病时一样珍重自己


在前两周我第一次阳了,高烧之下,身体极其难受,但反而想做些什么,打开莫言的《生死疲劳》,边读边想。


很奇怪,在这种难受的时刻,我把我的时间看的更加重要了,我不愿意去刷短视频,看没用的小说,我的清醒的时间并不多,在有限的时间中,我总先想把时间要用的更有价值。


有位朋友曾向我表明过他的一种人生态度:“我经常会想象,死亡过后是一种什么状态,虚无?黑暗?所以我每一天,都很感恩,我活着,所以我格外珍惜每一天,和我相处的每个人。”


当时生了病才明白,只有当我们意识到我们时间的珍贵,才会去珍惜当下的时间,以及相处的人。


那为什么不从现在开始,保持感恩,然后用好每天的时间呢?去见新的人也好,去学习也好,去锻炼也好,总之做我觉得有价值而非产生快感的事情。


image.png


Part Three 想想下一本读什么?


真正的阅读者每天给自己规定的读书时间是多少?3h?4h?8h?


是 1 min。


只有当开始不那么困难的时候,我们才会轻松的坚持下来,这个说的是开始。


而当我们开始阅读 1min,我们当然不会满足于此,我们还会继续去阅读。


读书是如此,背单词同样是如此,开启学习也是如此。


当我们读书/做事的时候,如果我们不想着下一件/下一本是什么,我们会做什么?


对于我个人来说,我会干完这个事情之后立刻觉得累了,躺在床上开始刷手机,一刷就是1h+,玩完了意识到自己的空虚,然后开始后悔,这就是我个人的惯性,或者说是习惯,怎么克服呢?


想想下一本读什么!


image.png


Part Four 几个道理,再开始吧


这里做了一个相当大的知识导图,我先放出来一部分截图吧~


总的来说就是觉得自己可以做的事情相当的多。


image.png


image.png


作者:阳树阳树
链接:https://juejin.cn/post/7247776651688706103
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin和Swift的前世一定是兄

Swift介绍 Swift这门编程语言主要用于iOS和MacOS的开发,可以说是非常流行的一门编程语言,我只想说,如果你会Kotlin,那么你学习Swift会非常容易,反之亦然。下载XCode,然后你就可以创建Playground练习Swift语法了。 pl...
继续阅读 »

Swift介绍


Swift这门编程语言主要用于iOS和MacOS的开发,可以说是非常流行的一门编程语言,我只想说,如果你会Kotlin,那么你学习Swift会非常容易,反之亦然。下载XCode,然后你就可以创建Playground练习Swift语法了。
截屏2023-06-26 22.39.18.png
playground这个名字起的好,翻译成中文就是操场,玩的地方,也就是说,你可以尽情的测试你的Swift代码。


声明变量和常量


Kotlin的写法:

var a: Int = 10
val b: Float = 20f

Swift的写法:

var a: Int = 10
let b: Float = 20.0

你会发现它俩声明变量的方式一模一样,而常量也只是关键字不一样,数据类型我们暂不考虑。


导包


Kotlin的写法:

import android.app.Activity

Swift的写法:

import SwiftUI

这里kotlin和swift的方式一模一样。


整形


Kotlin的写法:

val a: Byte = -10
val b: Short = 20
val c: Int = -30
val d: Long = 40

Swift的写法:

let a: Int8 = -10
let b: Int16 = 20
let c: Int32 = -30
let d: Int = -30
let e: Int64 = 40
let f: UInt8 = 10
let g: UInt16 = 20
let h: UInt32 = 30
let i: UInt = 30
let j: UInt64 = 40

Kotlin没有无符号整型,Swift中Int32等价于Int,UInt32等价于UInt。无符号类型代表是正数,所以没有符号。


基本运算符


Kotlin的写法:

val a: Int = 10
val b: Float = 20f
val c = a + b

Swift的写法:

let a: Int = 10
let b: Float = 20
let c = Float(a) + b

Swift中没有隐式转换,Float类型不用写f。这里Kotlin没那么严格。


逻辑分支


Kotlin的写法:

val a = 65
if (a > 60) {
}

val b = 1
when (b) {
1 -> print("b等于1")
2 -> print("b等于2")
else -> print("默认值")
}

Swift的写法:

let a = 65
if a > 60 {
}

let b = 1
switch b {
case 1:
print("b等于1")
case 2:
print("b等于2")
default:
print("默认值")
}

Swift可以省略if的括号,Kotlin不可以。switch的写法倒是有点像Java了。


循环语句


Kotlin的写法:

for (i in 0..9) {
}

Swift的写法:

for var i in 0...9 {
}
// 或
for var i in 0..<10 {
}

Kotlin还是不能省略括号。


字符串


Kotlin的写法:

val lang = "Kotlin"
val str = "Hello $lang"

Swift的写法:

let lang = "Swift"
let str = "Hello \(lang)"

字符串的声明方式一模一样,拼接方式略有不同。


数组


Kotlin的写法:

val arr = arrayOf("Hello", "JYM")
val arr2 = emptyArray<String>()
val arr3: Array<String>

Swift的写法:

let arr = ["Hello", "JYM"]
let arr2 = [String]()
let arr3: [String]

数组的写法稍微有点不同。


Map和Dictionary


Kotlin的写法:

val map = hashMapOf<String, Any>()
map["name"] = "张三"
map["age"] = 100

Swift的写法:

let dict: Dictionary<String, Any> = ["name": "张三", "age": 100]

Swift的字典声明时必须初始化。Map和Dictionary的本质都是哈希。


函数


Kotlin的写法:

fun print(param: String) : Unit {
}

Swift的写法:

func print(param: String) -> Void {
}

func print(param: String) -> () {
}

除了关键字和返回值分隔符不一样,其他几乎一模一样。


高阶函数和闭包


Kotlin的写法:

fun showDialog(build: BaseDialog.() -> Unit) {
}

Swift的写法:

func showDialog(build: (dialog: BaseDialog) -> ()) {
}

Kotlin的高阶函数和Swift的闭包是类似的概念,用于函数的参数也是一个函数的情况。


创建对象


Kotlin的写法:

val btn = Button(context)

Swift的写法:

let btn = UIButton()

这里kotlin和swift的方式一模一样。


类继承


Kotlin的写法:

class MainPresenter : BasePresenter {
}

Swift的写法:

class ViewController : UIViewController {
}

这里kotlin和swift的方式一模一样。


Swift有而Kotlin没有的语法


guard...else的语法,通常用于登录校验,条件不满足,就执行else的语句,条件满足,才执行guard外面的语句。

guard 条件表达式 else {
}

另外还有一个重要的语法就是元组。元祖在Kotlin中没有,但是在一些其他编程语言中是有的,比如Lua、Solidity。元组主要用于函数的返回值,可以返回一个元组合,这样就相当于函数可以返回多个返回值了。
Swift的元组:

let group = ("哆啦", 18, "全宇宙最强吹牛首席前台")

Lua的多返回值:

function group() return "a","b" end

Solidity的元组:

contract MyContract {
mapping(uint => string) public students;

function MyContract(){
students[0] = "默认姓名";
students[1] = "默认年龄";
students[2] = "默认介绍";
}

function printInfo() constant returns(string,uint,string){
return("哆啦", 18, "全宇宙最强吹牛首席前台");
}
}

总结


编程语言很多地方都是相通的,学会了面向对象编程,你学习其他编程语言就会非常容易。学习一门其他编程语言的语法是很快的,但是要熟练掌握,还需要对该编程语言的API有大量的实践。还是那句话,编程语言只是工具,你的编程思维的高度才是决定你水平的重要指标。所以我给新入行互联网的同学的建议是,你可以先学习面向对象的编程思想,不用局限于一门语言,可以多了解下其他的编程语言,选择你更喜欢的方向。选择好后,再深耕一门技术。每个人的道不一样,可能你就更适合某一个方向。


作者:dora
链接:https://juejin.cn/post/7248962809023316028
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter这么火为什么不了解一下呢?(上)

Flutter这么火为什么不了解一下呢?(上) Flutter是Google移动UI框架,用以创建高质量的native接口,真正跨平台,同时在iOS和Android上运行。Flutter是免费开源的,全球开发者及组织均可以使用。 Flutter有又几个特点...
继续阅读 »

Flutter这么火为什么不了解一下呢?(上)



Flutter是Google移动UI框架,用以创建高质量的native接口,真正跨平台,同时在iOS和Android上运行。Flutter是免费开源的,全球开发者及组织均可以使用。



Flutter有又几个特点:

1.快速开发 毫秒级的热加载快速地将修改应用到app。使用丰富的可完全自定义的组件在几分钟内就可以构建native界面。


2.极具表现力,灵活的UI 快速地将特性集中到native终端用户体验。利用分层结构可以完整地自定义UI,进而完成快速绘制及灵活的设计。


3.native性能 Flutter组件包含了所有平台的关键差异,例如滚动,导航,图标和字体。使得Flutter在iOS和Android上使用可以获得完全的native性能体验。


快速开发

Flutter热加载技术有助于你快速且简单地进行试验,构建UI,增加特性,并且快速修复bug。体验不到一秒的重新加载体验。


img


漂亮的UI

Flutter内置MD设计风格及iOS组件,更有丰富的手势API,流畅的滚动体验和平台认同感会让用户感到愉悦。


img


img


现代的响应式框架(Modern,reactive framework)

利用Flutter响应式框架和丰富的平台,布局和功能组件是的UI构建非常简单。使用灵活并且强大的API(2D,动画,手势,性能等)可以解决在UI上各种问题。

  int counter = 0;

 void increment() {
   // Tells the Flutter framework that state has changed,
   // so the framework can run build() and update the display.
   setState(() {
     counter++;
  });
}

 Widget build(BuildContext context) {
   // This method is rerun every time setState is called.
   // The Flutter framework has been optimized to make rerunning
   // build methods fast, so that you can just rebuild anything that
   // needs updating rather than having to individually change
   // instances of widgets.
   return new Row(
     children: <Widget>[
       new RaisedButton(
         onPressed: increment,
         child: new Text('Increment'),
      ),
       new Text('Count: $counter'),
    ],
  );
}
}

使用native特性和SDKs

我们使用平台APIs,第三方SDKs和native代码开发APP。Flutter可以让你在iOS和Android继续使用Java,Swift,Objective-C代码并且使用native特性。


访问平台特性很简单。下边的代码片段开始:

    var batteryLevel = 'unknown';
   try {
       int result = await methodChannel.invokeMethod('getBatteryLevel');
       batteryLevel = 'Battery level: $result%';
    } on PlatformException {
       batteryLevel = 'Failed to get battery level.';
    }
     setState(() {
       _batteryLevel = batteryLevel;
    });
}

学习如何使用包(packages),或者写platform channels,使用native代码,APIs和SDKs。


统一的开发标准



Flutter拥有工具及库帮助你简单快速地在iOS和Android上实现你的想法。若你还没有任何移动开发经验,那么Flutter将会是你构建漂亮的移动APP的一种简单快速的额方式。若你是有经验的iOS或者Android开发人员,那么你可以使用Flutter组件,并且继续使用已有的Java/Objective-C/Swift程序。



构建 漂亮的APP UI 丰富的2D GPU加速APIs 响应式框架 动画/动作 APIs 兼容Android Material组件及苹果组件样式


流程的编码体验 急速热加载技术 IntelliJ:重构,自动补足功能等 Dart语言及核心库 包管理


拥有App所有特性 与移动OS APIs&SDKs互操作性 Maven/Java Cocoapods/ObjC/Swift


优化 测试 Unit测试 继承测试 无设备测试


Debug IDE debug 基于网络debug 异步/唤醒感知 表达式求值程序


配置 时间线 CPU和内存 应用性能图标


部署 编译 Native ARM程序 消除无效代码


发布 App市场 Play Store


标题安装Flutter



在国内安装Flutter需要首先需要一个值得信任的国内镜像。在镜像上边保存着Flutter需要的依赖及相关库,包等。为了使用Flutter,需要使用一个备用存储位置,我们需要配置环境变量。 配置环境变量名: PUB_HOSTED_URL 和 FLUTTER_STORAGE_BASE_URL。



在windows系统中,需要在环境变量设置中添加:


PUB_HOSTED_URL : pub.flutter-io.cn FLUTTER_STORAGE_BASE_URL : storage.flutter-io.cn


然后运行Git命令(前提是安装了GitBash工具):

git clone -b dev https://github.com/flutter/flutter.git Flutter


Flutter文件夹需要注意:文件夹存放的路径上不要出现空格,否则在IDE中进行工程创建后会有警告,SDK环境路径上存在分隔符。



在clone完成之后,即Flutter Sdk下载完毕,还需要配置Flutter环境: xxxx/Flutter/bin目录下。


重新打开一个命令行,在其中输入命令

flutter doctor

进行环境及缺失的依赖检查,并下载需要的依赖。 运行效果如下图:


img


在环境及相关依赖检查完成之后,可以开始在Android Studio中进行创建工程行为。



注意:Android Studio 预览版中无法保证运行Flutter成功。因此需要使用稳定版AS,且需要3.0版本以上。



Android Studio中需要安装Flutter Plugin,Dart Plugin两个插件。


Dart SDK也需要手动安装,直接下载zip包免安装。


成功准备好IDE环境之后,就可以创建Flutter Project了,默认创建Flutter Application就可以了,按照IDE创建提示一直到最终完成。



需要注意:同样由于网络环境,直接运行Flutter Project是不可行的,UI会一直停留在Gradle正在初始化工程。这时需要修改build.gradle配置中的中央Maven库到一个可信赖的公共Maven库。 这里我修改成Ali的Maven库

buildscript {
   ext.kotlin_version = '1.1.51'
   repositories {
       maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
       google()
  }
   // ......
}

// ......

allprojects {
   repositories {
       maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
  }
   google()
}
// ......


然后再次sync工程,进行运行。


img


首个创建的Flutter Project工程结构如下:


img



作者:花海blog
链接:https://juejin.cn/post/7250288616533049381
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

github365天保持常绿的“旁门左道”

目录 前言 项目初始化 自动提交的原理 编写核心代码 仓库的设置 总结 一、前言 作为一名程序员,我们肯定有自己的github账号,在github个人主页会展示你最近十二个月的提交情况,用绿色的深浅表示你当日提交次数的多少。如果你将个人的github主页链...
继续阅读 »

github365天保持常绿的“旁门左道”.png


目录



  1. 前言

  2. 项目初始化

  3. 自动提交的原理

  4. 编写核心代码

  5. 仓库的设置

  6. 总结


一、前言


作为一名程序员,我们肯定有自己的github账号,在github个人主页会展示你最近十二个月的提交情况,用绿色的深浅表示你当日提交次数的多少。如果你将个人的github主页链接放在简历上,面试官在面试时便可能会点进去,首先映入眼帘的便是这提交次数展示区域。但学生党平时要忙于个人的学业、社畜党要忙于自己的工作,几乎没有多少时间来做自己的项目、建设自己的github仓库,这样则可能会导致这片区域是空白的,像下面一样:



如果这片区域是空白的或者近十二个月只有零零散散的提交记录,说明个人github的活跃度不够,进而说明对技术探索学习的积极性也不够,这会给面试官留下不好的印象。


如果你在为你的github提交记录担忧,那么就可阅读本篇文章。本篇文章带大家借助GitHub Actions实现github每日自动提交,让你的github365天保持常绿,像下面这样:



当然这只是偷懒的做法,正如文章标题所说的“旁门左道”,可供娱乐和学习,但还是希望大家能有效的提交代码,这样才是真正巩固和学习技术的有效途径。


二、项目初始化


首先在自己的github上创建一个名为autocommit-robot的仓库,然后git clone 项目地址到本地打开即可,此处不赘述。



在项目根目录下执行npm init命令(根据自己实际需要进行配置,否则一直按回车键用默认配置即可)进行npm初始化。同时发现此时项目并没有.gitignore文件,可在项目根目录下新建一个.gitignore文件,配置按下面来即可:


.DS_Store
.vite-ssg-dist
.vite-ssg-temp
*.local
dist
dist-ssr
node_modules
.idea/
*.log

至此,项目的初始化工作便已完成,项目的目录如下所示:



三、自动提交的原理


本文章介绍的github自动提交功能是借助GitHub Actions实现的,Github Actions是最近几年新加进来的功能,可以用于给项目做CI/CD。原先你如果有这个需求,可能要借助Travis等三方网站。新建目录和配置文件 Github Actions使用yml格式做配置。首先要在你的项目中建立一下目录:.github/workflows/在workflows目录中新增yaml配置文件,文件名任意。通过目录名可以看出Github将此类任务称为“工作流”。其它关于Github Actions的基础使用,可看这篇文章:【白嫖Github Action做定时任务】。下面是我的.github\workflows\robot.yml内容:


name: autocommit-robot

on:
schedule:
- cron: "0 0 * * *"

jobs:
bots:
runs-on: ubuntu-latest
steps:
- name: "Checkout code"
uses: actions/checkout@v1

- name: "Set node"
uses: actions/setup-node@v1
with:
node-version: 16.x

- name: "Install"
run: npm install

- name: "Run bash"
run: node index.js

- name: "Commit"
uses: EndBug/add-and-commit@v4
with:
author_name: XC0703
author_email: example@qq.com
message: "feat: save robot"
add: "pictures/*"

env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}

上面这段内容定义了一个名为autocommit-robot的github工作流,其作用是每天自动将一张图片添加到Git仓库中。具体来说,它包含以下步骤:



  1. 设置GitHub Actions的运行时间为每天0点0分

  2. 在Ubuntu操作系统上运行该脚本

  3. 使用Node.js安装所需的依赖项

  4. 运行index.js文件,该文件中定义了从外部API获取一张图片并将其保存到pictures文件夹中的逻辑

  5. 使用add-and-commit操作自动将新添加的图片提交到Git仓库,并指定提交信息为feat: save robot


总的来说,这个脚本是一段自动化流程,能够帮助我们实现每天自动将一张图片添加到Git仓库并提交的功能。


注意:GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}为仓库范围的访问令牌,按照上面链接文章中所讲的步骤去申请一个个人令牌,然后将个人令牌的值存储在GitHub Secrets中(下面会讲怎么设置这个令牌)。这里后面的那个ACCESS_TOKEN为笔者自己的token名称,根据自己的实际情况进行替换,不要照搬。


四、仓库的设置


4.1 配置 GitHub Secrets


GitHub Secrets是 GitHub 提供的一种功能,用于存储和管理敏感数据、密钥和凭据。它们可用于在 GitHub Actions 或其他 CI/CD(持续集成/持续部署)流程中安全地访问这些敏感信息。


GitHub Secrets 的主要作用如下:



  1. 存储敏感数据:可以使用 GitHub Secrets 存储敏感数据,例如 API 密钥、数据库凭据、访问令牌等,而不是将它们明文写入代码或配置文件中。

  2. 保护代码安全:通过将敏感数据存储在 Secrets 中,可以避免将其提交到代码库中,从而降低了泄露敏感信息的风险。

  3. 安全访问凭据:在 CI/CD 流程中,可以使用 Secrets 来访问所需的敏感数据。例如,在构建和部署过程中,可以在脚本中使用 Secrets 来进行身份验证,而无需明文写入凭据。

  4. 多环境支持:Secrets 可以在不同的分支或环境中设置不同的值,这样可以轻松管理不同环境所需的不同凭据。

  5. 灵活的权限控制:GitHub Secrets 具有灵活的权限控制机制,可以根据需要对不同角色或团队进行访问限制,以确保敏感数据的安全性。


通俗地讲就是执行工作流的过程中,需要使用一些敏感数据(如个人令牌、服务器账号密码等),不能直接写在文件中,通过key/value的形式配置在 GitHub 中,使用的时候只需要输入key就能找到对应的value,这样就避免了将敏感数据直接暴露在文件中。


配置过程如下:




4.2 开启工作流权限


如果没有开启,执行工作流会报错。


配置如下:




五、编写核心代码


在第三步中提到每日要自动运行index.js文件,这个文件中定义了从外部API获取一张图片并将其保存到pictures文件夹中的逻辑。实现这个逻辑要借助requestpathfs三个依赖包,三个包作用如下:



  • request 模块用于向指定 URL 发送 HTTP 请求,并获取响应结果。它提供了对常见的 HTTP 动词(如 GET、POST 等)以及请求头、响应头等信息的控制。在这里,request 模块用于从 Robohash API 获取一张随机图片,将其作为响应结果返回。

  • path 模块提供了用于处理文件路径的相关方法,例如path.resolve() 用于将多个路径拼接成一个绝对路径,path.dirname() 用于获取一个路径的目录名部分,path.extname() 用于获取文件扩展名等。在这里,path 模块path.resolve() 方法被用于生成绝对路径,path.join() 方法可以将多个参数拼接成一个路径字符串。

  • fs 模块则提供了文件系统相关的操作方法,例如读写文件、创建文件夹、删除文件等。在这里,fs 模块的 fs.createWriteStream() 方法被用于创建可写流,将获取到的图片流写入到本地文件中。


npm install request path fs

index.js文件如下:


const request = require("request");
const path = require("path");
const fs = require("fs");
const id = (~~(Math.random() * 100000)).toString(); // 获取小于10w的数字
const url = `https://robohash.org/${id}`;
const dirPath = path.resolve(__dirname, "pictures");
// 这一步的处理,因为github获取的时间时区是美国时区,所以获取到的时间格式是6/16/2023,我们可以先split后将年份放到数组第一位,这样就是需要的文件名格式了
const dateArr = new Date().toLocaleDateString().split("/"); // 本地调试时用.toLocaleDateString("en")
dateArr.unshift(dateArr.pop());
const date = dateArr.join("-");
request(url).pipe(fs.createWriteStream(`${dirPath}/${date}.png`));

这段代码主要实现了从Robohash API获取一张随机生成的图片,并将其保存到本地指定文件夹中。具体来说,它包含以下步骤:



  1. 引入必要的模块:requestpathfs

  2. 生成一个小于10万的随机数字 id

  3. 使用 robohash.org 的API,构造访问地址 url,id作为参数的一部分

  4. 使用 path.resolve() 方法创建 dirPath 文件夹路径,用于存储图片文件

  5. 获取当前日期,并按照年-月-日的格式拼接成字符串 date

  6. 使用 request(url) 发起GET请求,获取图片资源流。可以理解为通过这个 url 去获得一张随机图片。

  7. 将获取到的图片资源流通过 pipe() 方法管道流到 fs.createWriteStream() 中,将其写入到 ${dirPath}/${date}.png 文件中,这样就将获取到的图片保存到了指定的本地文件夹。


最后,每当这段代码执行一次,就会从 robohash.org 获取一张随机的图片,并将其保存到本地的 ${dirPath}/${date}.png 文件中。


此时,项目开发完成,将本地更新推送到远程github仓库即可。


六、总结


本篇文章带大家借助GitHub Actions实现github每日自动提交的功能,项目源码地址如下:


github.com/XC0703/auto…


若不想自己重新开发一份,也可以直接fork我的仓库,这样我每日的自动提交也会同步到你们的仓库中。


最后,感谢您的阅读,如对您有

作者:明远湖之鱼
来源:juejin.cn/post/7253673249159200825
帮助,欢迎点赞收藏。

收起阅读 »

MySQL:我的从库竟是我自己!?

本文将通过复制场景下的异常分析,介绍手工搭建MySQL主从复制时需要注意的关键细节。 作者:秦福朗 爱可生 DBA 团队成员,负责项目日常问题处理及公司平台问题排查。热爱互联网,会摄影、懂厨艺,不会厨艺的 DBA 不是好司机,didi~ 本文来源:原创投稿 ...
继续阅读 »

本文将通过复制场景下的异常分析,介绍手工搭建MySQL主从复制时需要注意的关键细节。



作者:秦福朗


爱可生 DBA 团队成员,负责项目日常问题处理及公司平台问题排查。热爱互联网,会摄影、懂厨艺,不会厨艺的 DBA 不是好司机,didi~


本文来源:原创投稿




  • 爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。


背景


有人反馈装了一个数据库,来做现有库的从库。做好主从复制关系后,在现有主库上使用 show slave hosts; 管理命令去查询从库的信息时,发现从库的 IP 地址竟是自己的 IP 地址,这是为什么呢?


因生产环境涉及 IP,端口等保密信息,以下以本地环境来还原现象。


本地复现


基本信息


主库从库
IP10.186.65.3310.186.65.34
端口66076607
版本8.0.188.0.18

问题现象


不多说,先上图,以下为在主库执行 show slave hosts; 出现的现象:



可以看到这里的 Host 是主库的 IP 地址。


我们登陆从库查看一下 show slave status\G


mysql> show slave status\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 10.186.65.33
Master_User: universe_op
Master_Port: 6607
Connect_Retry: 60
Master_Log_File: mysql-bin.000002
Read_Master_Log_Pos: 74251749
Relay_Log_File: mysql-relay.000008
Relay_Log_Pos: 495303
Relay_Master_Log_File: mysql-bin.000002
Slave_IO_Running: Yes
Slave_SQL_Running: Yes

我们看到确实从库是在正常运行的,且复制的源就是主库。


为什么执行 show 命令看到的 Host 和实际的情况对不上呢?


查阅资料


首先查阅官方文档,关于 show slave hosts; 语句的解释:




  • 首先说明 8.0.22 之后版本的 show slave hosts 语句被废弃(可执行),改为 show replicas,具体机制还是一样的。

  • 这里说明了各个数据的来源,多数来源于 report-xxxx 相关参数,其中 Host 的数据来自于从库的 report_host 这个参数。


然后,我们测试在从库执行 show variables like "%report%";


mysql> show variables like "%report%";
+-----------------+--------------+
| Variable_name | Value |
+-----------------+--------------+
| report_host | 10.186.65.33 |
| report_password | |
| report_port | 6607 |
| report_user | |
+-----------------+--------------+
4 rows in set (0.01 sec)

可以看到这里显示的就是主库的 IP。


我们再查询 report_host 的参数基本信息:



可以看到该参数非动态配置,在从库注册时上报给主库,所以主库上执行 show slave hosts; 看到的是 IP 是从这里来的,且无法在线修改。


最后也通过查看从库上的 my.cnf 上的 report_port 参数,证实确实是主库的 IP:



结论


经了解,生产上的从库是复制了主库的配置文件来部署的,部署时没有修改 report_host 这个值,导致启动建立复制后将 report_host 这个 IP 传递给主库,然后主库查询 show slave hosts 时就出现了自己的 IP,让主库怀疑自己的从库竟然是自己。


生产上大部分人知道复制主库的配置文件建立新库要修改 server_id 等相关 ID 信息,但比较容易忽略掉 report_ipreport_port 等参数的修改,这个需要引起注意,虽然错误之后看起来对

作者:爱可生开源社区
来源:juejin.cn/post/7254043722946199609
复制运行是没影响的。

收起阅读 »

短链的原理

1. 什么是短链? 在使用网址访问访问网站的时候,可能都会遇到一个很长的链接,多则可能几百个字符,这样的链接看起来非常的长 https://www.google.com/search?q=%E9%95%BF%E9%93%BE%E9%93%BE%E6%8E%A5...
继续阅读 »

1. 什么是短链?


在使用网址访问访问网站的时候,可能都会遇到一个很长的链接,多则可能几百个字符,这样的链接看起来非常的长


https://www.google.com/search?q=%E9%95%BF%E9%93%BE%E9%93%BE%E6%8E%A5&rlz=1C5GCEM_enCN1065&oq=%E9%95%BF%E9%93%BE%E9%93%BE%E6%8E%A5&aqs=chrome..69i57j0i13i512j0i10i13i512l2j0i13i30j0i10i13i30j0i13i30l4.4703j0j15&sourceid=chrome&ie=UTF-8


像以上这种,就是长链链接,看起来非常的臃肿,虽然说链接里面携带的一些参数使链接变长是不可避免的,但是我们可以使用另一种方法,既能使链接携带参数也能使链接为短链接,后面会讲到短链的原理和实现方式。


https://juejin.cn/post/7254039051588599864


像以上这种就是短链,使用短链有以下几种优点。



  1. 相比长链更加简洁。

  2. 便于使用,粘贴复制分享给别人时较为便捷,有些平台对分享内容的长度有所限制(微博只能发140字),这个时候使用短链可以输入更多的内容。

  3. 短链生成的二维码更容易识别。


2. 短链的原理


实现短链的核心原理就是链接映射表和302临时重定向


当我们用户用短链访问服务器时,服务器会将用户携带的短链在服务器中的链接映射表中寻找唯一与之对应的长链接,寻找到后返回重定向,重定向至长链接的网页,这样用户就可以不直接携带长链接,而是携带一个短链接,间接的去访问长链接,这个中间层就是服务器,服务器会在中间做一个链接映射和重定向的操作。


image.png


3. 短链的实现方式


3.1. 自增id法


自增ID的方法也叫做永不重复法,即采用发号器原理来实现,每一个url对应一个数字,然后自增,可以理解为ID,然后将ID进行相应的转换(比如进制转换),由于ID是唯一的,所以转换出来的结果也是唯一的。短网址的长度一般设为 6 位,而每一位是由 [a - z, A - Z, 0 - 9] 总共 62 个字母组成的,所以 6 位的话,总共会有 62^6 ~= 568亿种组合,基本上够用了。


3.2. 算法生成


将长链接做一次哈希算法之后就可以生产一个短链接。


我们都知道哈希算法是一种摘要算法,它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。


我们常见的哈希算法有:MD5、SHA-1、SHA-256、SHA-512 算法等。但我们最好还是使用另一种叫做 MurmurHash 的哈希算法。


因为 MD5 和 SHA 哈希算法,它们都是加密的哈希算法,也就是说我们无法从哈希值反向推导出原文,从而保证了原文的保密性。


但对于我们这个场景而言,我们并不关心安全性,我们关注的是运算速度以及哈希冲突。而 MurmurHash 算法是一个非加密哈希算法,所以它的速度回更快。


哈希冲突


学过 HashMap 的同学都知道,哈希冲突是哈希算法不可避免的问题。而解决哈希冲突的方式有两种,分别是:链表法和重哈希法。HashMap 使用了链表法,但我们这里使用的是重哈希法。


所谓的重哈希法,指的是当发生哈希冲突的时候,我们在原有长链后面加上固定的特殊字符,后续拿

作者:1in
来源:juejin.cn/post/7254039051588599864
出长链时再将其去掉.

收起阅读 »

5分钟,带你迅速上手“Markdown”语法

web
本篇将重点讲解:Markdown的 “语法规范” 与 “上手指南”。 一、Markdown简介 Markdown是一种文本标记语言,它容易上手、易于学习,排版清晰明了、直观清晰。常用于撰写 “技术文档” 、 “技术博客” 、 “开发文档” 等等。 总之,如...
继续阅读 »

本篇将重点讲解:Markdown的 “语法规范”“上手指南”





一、Markdown简介


Markdown是一种文本标记语言,它容易上手、易于学习,排版清晰明了、直观清晰。常用于撰写 “技术文档”“技术博客”“开发文档” 等等。
总之,如果你是一名开发者,并且你有写博客的欲望与想法时,使用Markdown是你不二的选择。




二、Markdown语法


接下来,我们来看一下Markdown“标准语法”


我们看下大纲,其中包括:



1、标题


  • 标准语法:使用1~6“#”符 + “空格” + “你的标题”。


# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题


  • 效果图解:




注:#和「标题」之间有一个空格,这是最标准的语法格式。
有些编辑器做了兼容,有的并没有。所以最好要加上空格。



2、列表


  • 标准语法:使用-符,在文本前加入-符即可。


- 文本1
- 文本2
- 文本3

如果你希望有序,在文本前加上1. 2. 3. 4. ...


1. 文本1
2. 文本2
3. 文本3


注:-1. 2. 等和文本之间要保留一个字符的空格。




  • 效果图解:



3、超链接



  • 标准语法:[链接名](链接url)




  • 效果图解:





4、图片


  • 标准语法:


![图片名](链接url)


  • 效果图解:



5、引用



  • 标准语法:> 文本




  • 效果图解:





6、斜体、加粗



  • 标准语法
    斜体*文本*
    加粗**文本**
    斜体&加粗***文本***




  • 效果图解:





7、代码块



  • 标准语法:
    ``` 你的代码 ```(前面3个点,后面3个点)




  • 效果图解:







8、表格


  • 标准语法:


dog | bird | cat
----|------|----
foo | foo | foo
bar | bar | bar
baz | baz | baz


  • 效果图解:





9、特殊标记


  • 标准语法:``


`特殊样式`


  • 效果图解:





10、分割线



  • 标准语法:--- 最少3个




  • 效果图解:





11、常用html标记

注意:html标记只适合辅助使用,不一定所有编辑器都能生效。



  • 标准语法:


换行符:<br/> (或者使用Markdown标准语法:空格+空格+回车,但我感觉不是很直观)
上:<sup>文本</sup>
下:<sub>文本</sub>



  • 效果图解:





三、Markdown优点



  • 纯文本,所以兼容性极强,可以用所有文本编辑器打开。

  • 让作者更专注于写作而不是排版。(大家都是技术人员嘛..)

  • 格式转化方便,markdown文本可以很轻松转成htmlpdf等等。(图个方便嘛)

  • 语法简单

  • 可读性强,配合表格、引用、代码块等等,让读者瞬间“懂
    作者:齐舞647
    来源:juejin.cn/post/7254107670012510245
    你”。

收起阅读 »

🤣泰裤辣!这是什么操作,自动埋点,还能传参?

web
前言 在上篇文章讲了如何通过手写babel插件自动给函数埋点之后,就有同学问我,自动插入埋点的函数怎么给它传参呢?这篇文章就来解决这个问题我讲了通过babel来实现自动化埋点,也讲过读取注释给特定函数插入埋点代码,感兴趣的同学可以来这里 给所有函数都添加埋...
继续阅读 »


前言


在上篇文章讲了如何通过手写babel插件自动给函数埋点之后,就有同学问我,自动插入埋点的函数怎么给它传参呢?这篇文章就来解决这个问题
我讲了通过babel来实现自动化埋点,也讲过读取注释给特定函数插入埋点代码,感兴趣的同学可以来这里





效果是这样的
源代码:


//##箭头函数
//_tracker
const test1 = () => {};

const test1_2 = () => {};

转译之后:


import _tracker from "tracker";
//##箭头函数
//_tracker
const test1 = () => {
_tracker();
};

const test1_2 = () => {};

代码中有两个函数,其中一个//_tracker的注释,另一个没有。转译之后只给有注释的函数添加埋点函数。
要达到这个效果就需要读取函数上面的注释,如果注释中有//_tracker,我们就给函数添加埋点。这样做避免了僵硬的给每个函数都添加埋点的情况,让埋点更加灵活。




那想要给插入的埋点函数传入参数应该怎么做呢?
传入参数可以有两个思路,



  • 一个是将参数也放在注释里面,在babel插入代码的时候读取下注释里的内容就好了;

  • 另一个是将参数以局部变量的形式放在当前作用域中,在babel插入代码时读取下当前作用域的变量就好;


下面我们来实现这两个思路,大家挑个自己喜欢的方法就好


参数放在注释中


整理下源代码


import "./index.css";

//##箭头函数
//_tracker,_trackerParam={name:'gongfu', age:18}
const test1 = () => {};

//_tracker
const test1_2 = () => {};


代码中,有两个函数,每个函数上都有_tracker的注释,其中一个注释携带了埋点函数的参数,待会我们就要将这个参数放到埋点函数里



关于如何读取函数上方的注释,大家可以这篇文章:(),我就不赘述了




准备入口文件


index.js


const { transformFileSync } = require("@babel/core");
const path = require("path");
const tracker = require("./babel-plugin-tracker-comment.js");

const pathFile = path.resolve(__dirname, "./sourceCode.js");

//transform ast and generate code
const { code } = transformFileSync(pathFile, {
plugins: [[tracker, { trackerPath: "tracker", commentsTrack: "_tracker",commentParam: "_trackerParam" }]],
});

console.log(code);



和上篇文章的入口文件类似,使用了transformFileSyncAPI转译源代码,并将转译之后的代码打印出来。过程中,将手写的插件作为参数传入plugins: [[tracker, { trackerPath: "tracker", commentsTrack: "_tracker"}]]。除此之外,还有插件的参数



  • trackerPath表示埋点函数的路径,插件在插入埋点函数之前会检查是否已经引入了该函数,如果没有引入就需要额外引入。

  • commentsTrack标识埋点,如果函数前的注释有这个,就说明函数需要埋点。判断的标识是动态传入的,这样比较灵活

  • commentParam标识埋点函数的参数,如果注释中有这个字符串,那后面跟着的就是参数了。就像上面源代码所写的那样。这个标识不是固定的,是可以配置化的,所以放在插件参数的位置上传进去


编写插件


插件的功能有:



  • 查看埋点函数是否已经引入

  • 查看函数的注释是否含有_tracker

  • 将埋点函数插入函数中

  • 读取注释中的参数


前三个功能在上篇文章(根据注释添加埋点)中已经实现了,下面实现第四个功能


const paramCommentPath = hasTrackerComments(leadingComments, options.commentsTrack);
if (paramCommentPath) {
const param = getParamsFromComment(paramCommentPath, options);
insertTracker(path, param, state);
}

//函数实现
const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}

try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};

const insertTracker = (path, param, state) => {
const bodyPath = path.get("body");
if (bodyPath.isBlockStatement()) {
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
bodyPath.node.body.unshift(ast);
} else {
const ast = template.statement(`{
${state.importTackerId}(${param});
return BODY;
}`
)({ BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};


上述代码的逻辑是检查代码是否含有注释_tracker,如果有的话,再检查这一行注释中是否含有参数,最后再将埋点插入函数。
在检查是否含有参数的过程中,用到了插件参数commentParam。表示如果注释中含有该字符串,那后面的内容就是参数了。获取参数的方法也是简单的字符串切割。如果没有获取到参数,就一律返回null



获取参数的复杂程度,取决于。先前是否就有一个规范,并且编写代码时严格按照规范执行。
像我这里的规范是埋点参数commentParam和埋点标识符_tracker必须放在一行,并且参数需要是对象的形式。即然是对象的形式,那这一行注释中就不允许就其他的大括号符号"{}"
遵从了这个规范,获取参数的过程就变的很简单了。当然你也可以有自己的规范



在执行插入逻辑的函数中,会校验参数param是否为null,如果是null,生成ast的时候,就不传入param了。



当然你也可以一股脑地传入param,不会影响结果,顶多是生成的埋点函数会收到一个null的参数,像这样_tracker(null)



第四个功能也实现了,来看下完整代码


完整代码


const { declare } = require("@babel/helper-plugin-utils");
const { addDefault } = require("@babel/helper-module-imports");
const { template } = require("@babel/core");
//judge if there are trackComments in leadingComments
const hasTrackerComments = (leadingComments, comments) => {
if (!leadingComments) {
return false;
}
if (Array.isArray(leadingComments)) {
const res = leadingComments.filter((item) => {
return item.node.value.includes(comments);
});
return res[0] || null;
}
return null;
};

const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}

try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};

const insertTracker = (path, param, state) => {
const bodyPath = path.get("body");
if (bodyPath.isBlockStatement()) {
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
bodyPath.node.body.unshift(ast);
} else {
const ast = template.statement(`{
${state.importTackerId}(${param});
return BODY;
}`
)({ BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};


const checkImport = (programPath, trackPath) => {
let importTrackerId = "";
programPath.traverse({
ImportDeclaration(path) {
const sourceValue = path.get("source").node.value;
if (sourceValue === trackPath) {
const specifiers = path.get("specifiers.0");
importTrackerId = specifiers.get("local").toString();
path.stop();
}
},
});

if (!importTrackerId) {
importTrackerId = addDefault(programPath, trackPath, {
nameHint: programPath.scope.generateUid("tracker"),
}).name;
}

return importTrackerId;
};

module.exports = declare((api, options) => {

return {
visitor: {
"ArrowFunctionExpression|FunctionDeclaration|FunctionExpression|ClassMethod": {
enter(path, state) {
let nodeComments = path;
if (path.isExpression()) {
nodeComments = path.parentPath.parentPath;
}
// 获取leadingComments
const leadingComments = nodeComments.get("leadingComments");
const paramCommentPath = hasTrackerComments(leadingComments,options.commentsTrack);

// 如果有注释,就插入函数
if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;

const param = getParamsFromComment(paramCommentPath, options);
insertTracker(path, param, state);
}
},
},
},
};
});



运行代码


现在可以用入口文件来使用这个插件代码了


node index.js

执行结果
image.png



运行结果符合预期



可以看到我们设置的埋点参数确实被放到函数里面了,而且注释里面写了什么,函数的参数就会放什么,那么既然如此,可以传递变量吗?我们来试试看


import "./index.css";

//##箭头函数
//_tracker,_trackerParam={name, age:18}
const test1 = () => {
const name = "gongfu2";
};

const test1_2 = () => {};

在需要插入的代码中,声明了一个变量,然后注释的参数刚好用到了这个变量。
运行代码看看效果
image.png
可以看到,插入的参数确实用了变量,但是引用变量却在变量声明之前,这肯定不行🙅。得改改。
需要将埋点函数插入到函数体的后面,并且是returnStatement的前面,这样就不会有问题了


const insertTrackerBeforeReturn = (path, param, state) => {
//blockStatement
const bodyPath = path.get("body");
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
if (bodyPath.isBlockStatement()) {
//get returnStatement, by body of blockStatement
const returnPath = bodyPath.get("body").slice(-1)[0];
if (returnPath && returnPath.isReturnStatement()) {
returnPath.insertBefore(ast);
} else {
bodyPath.node.body.push(ast);
}
} else {
ast = template.statement(`{ ${state.importTackerId}(${param}); return BODY; }`)({ BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};

这里将insertTracker改成了insertTrackerBeforeReturn
其中关键的逻辑是判断是否是一个函数体,



  • 如果是一个函数体,就判断有没有return语句,

    • 如果有return,就放在return前面

    • 如果没有return,就放在整个函数体的后面



  • 如果不是一个函数体,就直接生成一个函数体,然后将埋点函数放在return的前面


再来运行插件:
image.png



很棒,这就是我们要的效果😃




完整代码


const { declare } = require("@babel/helper-plugin-utils");
const { addDefault } = require("@babel/helper-module-imports");
const { template } = require("@babel/core");
//judge if there are trackComments in leadingComments
const hasTrackerComments = (leadingComments, comments) => {
if (!leadingComments) {
return false;
}
if (Array.isArray(leadingComments)) {
const res = leadingComments.filter((item) => {
return item.node.value.includes(comments);
});
return res[0] || null;
}
return null;
};

const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}

try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};

const insertTrackerBeforeReturn = (path, param, state) => {
//blockStatement
const bodyPath = path.get("body");
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
if (bodyPath.isBlockStatement()) {
//get returnStatement, by body of blockStatement
const returnPath = bodyPath.get("body").slice(-1)[0];
if (returnPath && returnPath.isReturnStatement()) {
returnPath.insertBefore(ast);
} else {
bodyPath.node.body.push(ast);
}
} else {
ast = template.statement(`{ ${state.importTackerId}(${param}); return BODY; }`)({BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};


const checkImport = (programPath, trackPath) => {
let importTrackerId = "";
programPath.traverse({
ImportDeclaration(path) {
const sourceValue = path.get("source").node.value;
if (sourceValue === trackPath) {
const specifiers = path.get("specifiers.0");
importTrackerId = specifiers.get("local").toString();
path.stop();
}
},
});

if (!importTrackerId) {
importTrackerId = addDefault(programPath, trackPath, {
nameHint: programPath.scope.generateUid("tracker"),
}).name;
}

return importTrackerId;
};

module.exports = declare((api, options) => {

return {
visitor: {
"ArrowFunctionExpression|FunctionDeclaration|FunctionExpression|ClassMethod": {
enter(path, state) {
let nodeComments = path;
if (path.isExpression()) {
nodeComments = path.parentPath.parentPath;
}
// 获取leadingComments
const leadingComments = nodeComments.get("leadingComments");
const paramCommentPath = hasTrackerComments(leadingComments,options.commentsTrack);

// 如果有注释,就插入函数
if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;

const param = getParamsFromComment(paramCommentPath, options);
insertTrackerBeforeReturn(path, param, state);
}
},
},
},
};
});


参数放在局部作用域中


这个功能的关键就是读取当前作用域中的变量。


在写代码之前,来定一个前提:当前作用域的变量名也和注释中参数标识符一致,也是_trackerParam


准备源代码


import "./index.css";

//##箭头函数
//_tracker,_trackerParam={name, age:18}
const test1 = () => {
const name = "gongfu2";
};

const test1_2 = () => {};

//函数表达式
//_tracker
const test2 = function () {
const age = 1;
_trackerParam = {
name: "gongfu3",
age,
};
};

const test2_1 = function () {
const age = 2;
_trackerParam = {
name: "gongfu4",
age,
};
};

代码中,准备了函数test2test2_1。其中都有_trackerParam作为局部变量,但test2_1没有注释//_tracker


编写插件


if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;

//check if have tackerParam
const hasTrackParam = path.scope.hasBinding(options.commentParam);
if (hasTrackParam) {
insertTrackerBeforeReturn(path, options.commentParam, state);
return;
}

const param = getParamsFromComment(paramCommentPath, options);
insertTrackerBeforeReturn(path, param, state);
}

这个函数的逻辑是先判断当前作用域中是否有变量_trackerParam,有的话,就获取该声明变量的初始值。然后将该变量名作为insertTrackerBeforeReturn的参数传入其中。
我们运行下代码看看
image.png



运行结果符合预期,很好




完整代码


const { declare } = require("@babel/helper-plugin-utils");
const { addDefault } = require("@babel/helper-module-imports");
const { template } = require("@babel/core");
//judge if there are trackComments in leadingComments
const hasTrackerComments = (leadingComments, comments) => {
if (!leadingComments) {
return false;
}
if (Array.isArray(leadingComments)) {
const res = leadingComments.filter((item) => {
return item.node.value.includes(comments);
});
return res[0] || null;
}
return null;
};

const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}

try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};

const insertTrackerBeforeReturn = (path, param, state) => {
//blockStatement
const bodyPath = path.get("body");
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
if (bodyPath.isBlockStatement()) {
//get returnStatement, by body of blockStatement
const returnPath = bodyPath.get("body").slice(-1)[0];
if (returnPath && returnPath.isReturnStatement()) {
returnPath.insertBefore(ast);
} else {
bodyPath.node.body.push(ast);
}
} else {
ast = template.statement(`{${state.importTackerId}(${param}); return BODY;}`)({BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};


const checkImport = (programPath, trackPath) => {
let importTrackerId = "";
programPath.traverse({
ImportDeclaration(path) {
const sourceValue = path.get("source").node.value;
if (sourceValue === trackPath) {
const specifiers = path.get("specifiers.0");
importTrackerId = specifiers.get("local").toString();
path.stop();
}
},
});

if (!importTrackerId) {
importTrackerId = addDefault(programPath, trackPath, {
nameHint: programPath.scope.generateUid("tracker"),
}).name;
}

return importTrackerId;
};

module.exports = declare((api, options) => {

return {
visitor: {
"ArrowFunctionExpression|FunctionDeclaration|FunctionExpression|ClassMethod": {
enter(path, state) {
let nodeComments = path;
if (path.isExpression()) {
nodeComments = path.parentPath.parentPath;
}
// 获取leadingComments
const leadingComments = nodeComments.get("leadingComments");
const paramCommentPath = hasTrackerComments(leadingComments,options.commentsTrack);

// 如果有注释,就插入函数
if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;

//check if have tackerParam
const hasTrackParam = path.scope.hasBinding(options.commentParam);
if (hasTrackParam) {
insertTrackerBeforeReturn(path, options.commentParam, state);
return;
}

const param = getParamsFromComment(paramCommentPath, options);
insertTrackerBeforeReturn(path, param, state);
}
},
},
},
};
});


总结:


这篇文章讲了如何才埋点的函数添加参数,参数可以写在注释里,也可以写在布局作用域中。支持动态传递,非常灵活。感兴趣的金友们可以拷一份代码下来跑一跑,相信你们会很有成就感的。


下篇文章来讲讲如何在create-reate-app中使用我们手写的babel插件。



相关文章:



  1. 通过工具babel,给函数都添加埋点

  2. 通过工具babel,根据注释添加埋点


作者:慢功夫
来源:juejin.cn/post/7254032949229764669

收起阅读 »

👀SSO单点登录 知多少

为撒我们要使用单点登录 通常情况下我们开发一套系统时,我们都会写一套登录页面。当用户需要访问系统时需要先进行账号登录然后进入系统使用。但是当我们有多个系统时难道需要用户登录多次吗?显然这种方式是不优雅的😶‍🌫️,此时就轮到单点登录登场了。本文将主要以前端的视角...
继续阅读 »

为撒我们要使用单点登录


通常情况下我们开发一套系统时,我们都会写一套登录页面。当用户需要访问系统时需要先进行账号登录然后进入系统使用。但是当我们有多个系统时难道需要用户登录多次吗?显然这种方式是不优雅的😶‍🌫️,此时就轮到单点登录登场了。本文将主要以前端的视角来聊一聊该如何落地单点登录。



单点登录的英文名叫做:Single Sign On(简称SSO)😶‍🌫️



单点登录能干嘛


简单粗暴的说单点登录就是



一处登录 处处登录 🤡



单点登录在前端的系统模型


单点登录实施方案


单点登录目前有两种方案



  • cookie 共享方案(子域名方案)

  • ticket 方案(凭证交换)


这两个方案是有共同点的,我们看看它们有哪些共同的部分



  • 需要一个独立的登录系统

  • 通用重定向的方式完成子系统登录



结合以上可以发现,这两个方案其实在前端的差异性并不大,主要就是需要考虑如何实现登录态共享。



cookie 共享方案


传统的登录方式是用户在登录页完成登录,然后后端通过 set-cookie 将 token 写入到浏览器的 cookie 当中之后的每次请求都会携带token完成身份验证。此时的登录模块和业务模块是同一系统下的不同页面。



单点登录模型下的登录模块和业务模块是独立的不同系统




结合以上流程图我们来分析一波,图中的Asso都是example.com的子域名,当用户访问A-client时会向A-server发送身份验证请求检验当前请求是否携带有效token,如无效则返回特定 code A-client接到返回后根据 code 做出不同响应。



  • A-server返回 token 有效,则继续保持对A-client的访问。

  • A-server返回 token 无效。


    • A-client重定向至SSO-client并携带redirect参数http://sso.example.com?redirect=a.example.com




    • 当用户在SSO-client完成登录后(SSO-server 通过 set-cookie 设置 token 为子域名可访问),再通过redirect参数重定向回A-client




    • 回到A-clientA-client再次向A-server发起身份验证,这次请求会默认携带上domain=example.com的cookie.token,A-server收到token检验有效后,返回用户信息继续保持用户对A-client的有效访问。






至此单点登录的核心逻辑就梳理完成了。



注意:以上是以每套系统都有独立的后端服务(同源)做出的流程,但是有时候我们的后端服可能是同一个,这时候我们在 ABC系统中的请求可能会有跨域/跨站等问题。
解决办法也很多,需要看是跨域,还是跨站问题。
这里贴一个跨域的处理方法 note.youdao.com/s/Tjuuv8Mv 也可以用反向代理去规避跨域问题。




同源:协议+主机名+端口 都相同

同站:二级域名+顶级域名 相同即可



ticket 临时凭证


ticket 方案总体上和 cookie 方案总体差不多,主要区别在于获取 token 的形式不同。

cookie 方案主要是依赖浏览的特性来完成 token 共享。

ticket 则是由各系统携带 ticket 这个临时凭证去换取token



从上图可以看出 ticket 方案不同的地方在于:




  • 用户首次访问A-client时 token 不存在A-client会重定向至SSO-client,当用户在SSO-client登录成功后SSO-server会 set-cookie 将 token 注入到浏览器中(7天免登录),并返回 ticket 参数SSO-client收到登录成功结果后再次带着 ticket 参数重定向回A-client (http://a.example.com?ticket=xxxxxxx)




  • 此时A-client时会多一个 ticket 参数。




  • A-client中的 token 过期时或不存在时,如果 ticket 存在A-server就会带着ticket去请求SSO-server检验ticket是否有效,当SSO-server返回有效的话就表示用户在SSO中是有效登录的。




  • 验证成功后A-server下发 token set-cookie 进A-client的cookie中。




至此完成登录。



以上多是以前端的角度在分析单点登录的实现逻辑,还有很多可以修改的地方。例如后端是同一套服务,token是放在cookie,还是local storage或是JS内存中都是根据业务需求改变的。




以及安全问题,这里就不展开讨论啦



作者:Wanp
来源:juejin.cn/post/7254027262885855291
收起阅读 »

作为一名前端给自己做一个算命转盘不过分吧

web
算命转盘 前言 给自己做一个算命转盘,有事没事算算命,看看运势挺好的(虽然我也看不懂)。 这个算命转盘我是实现在了自己的个人博客中的这里是地址,感兴趣可以点进去看看。 实现过程 开发技术:react + ts 该转盘主要是嵌套了三层 圆形滚动组件 来实现的,...
继续阅读 »

算命转盘


zodiac.gif

前言


给自己做一个算命转盘,有事没事算算命,看看运势挺好的(虽然我也看不懂)。


这个算命转盘我是实现在了自己的个人博客中的这里是地址,感兴趣可以点进去看看。


实现过程


开发技术:react + ts


该转盘主要是嵌套了三层 圆形滚动组件 来实现的,再通过 ref 绑定组件,调用其中的 scrollTo 方法即可使组件发生指定的滚动,再传入随机数,即可实现随机旋转效果,通过嵌套三层该组件实现三层的随机旋转,模拟“算命”效果。


// 这是精简后的代码
export default () => {
const onScrollCircle = () => {
const index = Math.floor(Math.random() * zodiacList.length)
scrollCircleRef.current?.scrollTo({index, duration: 1000})
}
return (
<>
<ScrollCircle ref={scrollCircleRef}></ScrollCircle>
<button onClick={() => onScrollCircle}>点击旋转</button>
</>

)
}

三层大致结构如下:具体代码可以看码上掘金



  • 转盘的第一层


export default () => {
return (
<ScrollCircle>
{list.map((item, index) => (
<ScrollCircle.Item
key={index}
index={index}
>

<CircleItem />
</ScrollCircle.Item>
))}
</ScrollCircle>

)
}


  • 转盘的第二层


const CircleItem = () => {
return (
<ScrollCircle>
{list.map((item, index) => (
<ScrollCircle.Item
key={index}
index={index}
>

<CircleItemChild />
</ScrollCircle.Item>
))}
</ScrollCircle>

)
}


  • 转盘的第三层


const CircleItemChild = () => {
return (
<ScrollCircle>
{list.map((item, index) => (
<ScrollCircle.Item
key={index}
index={index}
>

<div>
内容
</div>
</ScrollCircle.Item>
))}
</ScrollCircle>

)
}

圆形滚动组件


现在的 圆形滚动组件 支持展示到上下左右中各个方向上,要是大家使用过程中有什么意见可以提一下,我尽力实现,当然能提 pr 最好了(∪^ェ^∪)。


组件源码地址


线上Demo演示地址


image.png

主要是在旧版的基础上不断完善而来的,旧版圆形滚动组件的 往期文章


props等使用文档


ScrollCircle


属性名描述类型默认值
listLength传入卡片的数组长度number(必选)
width滚动列表的宽度string"100%"
height滚动列表的高度string"100%"
centerPoint圆心的位置"center" , "auto" , "left" , "right" , "bottom" , "top""auto (宽度大于高度时在底部,否则在右侧)"
circleSize圆的大小"inside" , "outside""outside (圆溢出包裹它的盒子)"

其他的属性...(篇幅问题就不全放上来了,可以直接去线上Demo演示地址查看)


centerPoint


主要通过该属性,将圆心控制到上下左右中间位置。


属性名描述
auto自动适应,当圆形区域宽度大于高度时,圆心会自动在底部,否则在右边
center建议搭配 circleSize='inside' 一起使用(让整个圆形在盒子内部)
left让圆心在左边
top让圆心在顶部
right让圆心在右边
bottom让圆心在底部

作者:滑动变滚动的蜗牛
来源:juejin.cn/post/7254014646779428922
收起阅读 »

前端行业:是否已走到了十字路口?

一、一些迹象 逛社区,偶然看到了这张图片: 嗯……我眉头一皱,久久不语,心想,有这么夸张吗,假的吧? 突然想到,最近我在社区发了个前端招聘的信息,结果简历漫天纷飞,塞爆邮箱。 莫非,前端这个岗位真的不再是供不应求了?🤔 二、原因分析 我细想下,也差不多到时候...
继续阅读 »

一、一些迹象


逛社区,偶然看到了这张图片:



嗯……我眉头一皱,久久不语,心想,有这么夸张吗,假的吧?


突然想到,最近我在社区发了个前端招聘的信息,结果简历漫天纷飞,塞爆邮箱。


莫非,前端这个岗位真的不再是供不应求了?🤔


二、原因分析


我细想下,也差不多到时候了。


从16年到现在,算算,7年的时间了。


前端大火就是从16年开始的,多种原因,包括:


移动互联网的兴起,传统行业的数字化转型,大前端技术的普及等。


紧接着是Vue为代表的前端框架和工具的兴起,使得前端开发的门槛进一步降低,前端也成为进入互联网圈子的最快最容易的跳板,促使前端圈进一步繁荣。


然而,连王菲都知道,没有什么是长盛不衰的。



发展,稳定,衰落是亘古不变的事物发展规律。


各种迹象表明,无论是有意还是无意,目前互联网的发展似乎进入了平稳期,这也意味着岗位的需求也开始变得平稳,而涌入这个行业的新人却没有停止,这就必然导致到了某个时间点,前端从业人员会达到饱和,于是那些没有竞争力的人就会遇到求职困境。


遇困的人多了,在社区的声音多了,自然也就会出现“前端已死”这样的言论。


三、破局之道


想要改变这种现状,只能是下面两种方法。


一是烧香拜佛,祈祷互联网大环境好转,最好再来一波生产力或生产环境的变革,让前端行业再赶上一波发展的春风,催生更大的岗位需求,何愁就业?


但显然,寄希望于大环境是不靠谱的,生产力虽然一定是往上走的,但说不定不是助力行业的发展,而是革了行业的命。


比方说现在很火的chatGPT,你说是会增加前端岗位呢,还是空窗加倍绝绝子?


所以,要想前端碗端得稳,前端饭吃得香,还是得靠下面这个方法,也就是想办法提高个人的核心竞争力。


提高核心竞争力


所谓核心竞争力,说白了,就是你能干别人干不了的活,能做别人做不了的事情。


更直白一点,就是你能给团队创造比别人更多的价值。


很普通的一句话,对不对?但是意识到和意识不到,那可是天差地别。


最近虽然收到了很多简历,但是看完之后都只能无奈摇头,不能说一模一样嘛,可以说极其雷同,缺少区分度。


专业技能均是全覆盖,工作描述均是自己用了什么前端框架,做了什么什么工作。


没有任何吸引人的信息,给人感觉,就是个普通的前端从业人员,领导安排个需求,然后接受,排期,完成开发,上线,这种。



这就……对吧,不是不给机会,实在是给不了。


一百份简历竞争一个招聘HC,肯定是把面试机会留给那些有突出亮点的人的。


拿工作描述举例,你一个一个罗列你做的项目,用了哪些技术有什么用?所有投简历的人都有做项目,都有使用前端技术,你的这些描述完全就是废话,简历扔垃圾箱的那种。


不需要扯那么多,你就说你比别人牛在什么地方!


注意,这个牛,不一定就是技术水平或者业务成果,任何亮点都可以,只要是能够做到别人做不到的事情,同时是对团队有帮助的,都可以。


举几个例子:


– 我参与了团队所有项目的开发,“所有”就是亮点,隐约让人觉得你是可信任的。


– 我是团队下班最晚的,工作最积极的。也是亮点,可以提,工时越长,通常产出越多,性价比就越高。


– 我在团队里做了很多看不见的工作。亮点,主动承担边缘工作不是所有人都可以做到的。


– 我是团队内分享(面授或文章都可以)次数第一。亮点,加分,帮助团队成长也是一种价值产出。


– 我连续获得四星五星荣誉,或者优秀员工称号,加分,公司的认可比自己在简历上吹上天都有用。


甚至是工作以外的特长都可以,我是钓鱼大佬,我是跑步达人,我是综艺专家,我是健身狂人,都可以,因为一个人能坚持自己的爱好并做到出众,也是不简单的。



可偏偏问题就在于,能够获得面试机会的亮点如此简单,很多人却没有,一个也没有。


因为在日常工作中就没有这种意识,就是我要做得比别人更好、我要强化我的优势、我要想办法让团队变得更好的意识。


平时工作就是浑浑噩噩的状态,等需求,写代码,上线,拿钱,一切都是在被动进行,仅把前端当作职业而非事业,总是希望干活少,拿钱多。


所以做事难以精益求精,也不会为了更好的未来努力让当下的自己变得更好,也不会主动做那些工作以外的对团队有帮助的事情,典型的被网上的躺平言论给忽悠瘸了。


弄错了因果,即,我给老板加班,又不会给我涨薪,我为什么要加班?我学习更底层的技术,平时又用不到,我为什么要学?我平时工作那么忙,还要我去写文档做分享,我为什么要做?


所以,找不到工作就不要怨天尤人了,也别说什么“前端已死”,前端行业好着呢,优秀的前端不知道多缺,年薪不知道有多高!


框架的能力


很多人做开发非常熟练,各种得心应手,于是就会觉得自己是个挺有竞争力的前端开发人员。



高启强没有说话,只是呵呵一笑。


这是不小心把框架的能力当作自己的能力了。


大家不妨冷静想一想,借助一个成熟的框架,开发出一个合格的Web应用,他的难度有多高?


更具体点,我们经常使用的各种小程序和快应用,让一个培训班里培训了3个月的新人,以及充足的时间,他能不能捣鼓出来?


答案显而易见,肯定可以,至少绝大多数人都可以。


因为使用一个东西的难度要比创造一个东西的难度低多了。


也就是,基于Vue等前端框架的开发,它是需要技术的,但是,它并不需要的很高的技术。


这种状态最容易迷惑人,所谓满瓶不动半瓶摇。


如果不能跳出自己所处的环境,正在更高的视角看待自己,非常容易对自己在行业所处的层次造成误判,譬如,我明明干活很利索,怎么没有面试机会,一定是我们这个行业出问题了。


这就是误判,有问题的不是行业,而是自己的竞争力不足。


我再说一遍,希望大家不要嫌啰嗦,使用工具的能力,并不能作为核心竞争力,因为现在学习资料很丰富,社区很活跃,什么问题都可以找到解决方案,你能做到的别人也能做到,没有任何优势,不属于竞争力。


反而是下面这些能力有足够的区分度。



  • 比他人涉猎更广,例如音视频处理、图形表现实现或者Node开发有较多经验;

  • JS、CSS等前端基本功扎实,积累深厚,各种API特性了然于心,最佳实践信手捏来;

  • 具有设计审美或者产品嗅觉灵敏,开发的产品体验非常好,干活很细。


拥有这些能力或特质,并在简历上表现出来,最好有材料佐证,那找到一份满意的工作是非常轻松的事情。


就怕一年经验十年用,从此外卖天天送。



当然,不可否认,虽说框架与工具让很多人陷入了温床,但对于国家整个数字化转型和互联网的发展是做出了重大贡献的。


在巨大需求出现的时候,有足够多的人力迅速投身这个行业,带动整个行业的发展。


只是,潮水终会退去,只有那些真正会游泳的才能继续在大海中徜徉。


四、未来如何


常常有人问我,旭哥,我应该学什么才有前途?


每当看到这样的问题,我都会眉头紧锁,过于功利的心态,在技术这条路上注定难有大成。


这就有点类似于养殖业,比如说前两年养鲈鱼很赚钱,结果很多养殖户改养鲈鱼,造成今年鲈鱼泛滥,市场存量是过去数倍,根本卖不出价格,最后赔得裤衩都不剩了。


技术其实也是类似,有人一看前端就业形势大好,都去搞前端,结果“前端已死”。


技术栈也是一样,妄图学完之后自己就成了香饽饽,可能吗?人是趋利性的动物,就算你眼光独到,命运垂怜,抢得先机,但数年之后呢?


所以,其实重要的不是学了什么,而是学得怎么样。


心无旁骛,专注自身,无论学什么,从事哪个职业,只要自己足够有竞争力,都有前途。


无论是历史悠久的后端开发,还是巅峰期早已过去的客户端开发,亦或者是开始进入稳定期的前端开发,均是如此。


前端的未来


随着消费和广告行业的慢慢复苏,前端的就业情况会有所好转。但是……


首先,这个好转不会很快,而是很缓慢那种,因为当一个事物陷入低谷再要起来,前期都是缓慢的,需要升到某一个临界点之后,才会明显加速。


其次,就算前端的就业情况有所恢复,也不可能恢复到疫情之前的那种火热,那个时候遍地都是前端培训班,非常夸张。


至于前端是否会死,这个完全不要担心。


只要互联网还在,前端这个职业就不会消失,因为无论设备介质如何变化,用户的交互行为都不会消失,而前端就是一个处理人机交互的职业。


而人工智能的兴起,确实会对前端这个职业产生影响,是危机但也是机遇,如果你安于现状,则是危机,如果你勤于学习,则人工智能是机遇,会让你的产出更加高效。


这么看来,最核心的竞争力应该是学习的能力!



原文地址:blog.csdn.net/m0_60961651…



作者:嚣张农民
来源:juejin.cn/post/7254097346762211385
收起阅读 »

Android 实现单指滑动、双指缩放照片

一、前景提示 最近接到一个查看大图的需求,现在图片展示还不够大,要求还要能缩小能放大还能保存照片。直接开始Google实现方式。 二、实现功能 根据查询到的结果分为两种,一个是使用手势监听来实现,第二种监听触摸事件来实现 手势监听-- ScaleGestur...
继续阅读 »

一、前景提示


最近接到一个查看大图的需求,现在图片展示还不够大,要求还要能缩小能放大还能保存照片。直接开始Google实现方式。


二、实现功能


根据查询到的结果分为两种,一个是使用手势监听来实现,第二种监听触摸事件来实现



  • 手势监听-- ScaleGestureDetector Google提供的手势监听类

  • 触摸事件--OnTouchListener 自己监听触摸事件自己实现放大缩小的逻辑


2.1 手势监听


先写布局文件

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_example"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="Hello World!"
android:scaleType="fitCenter"
android:src="@drawable/muffin_7870491_1920"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

再去实现手势监听方法

class MainActivity : AppCompatActivity() {
private lateinit var mScaleGestureDetector: ScaleGestureDetector
private var mScaleFactor: Float = 1.0f
private lateinit var mImageView: AppCompatImageView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mImageView = findViewById(R.id.iv_example)
mScaleGestureDetector = ScaleGestureDetector(this, ScaleGestureListener())
mImageView.setOnTouchListener { _, event ->
mScaleGestureDetector.onTouchEvent(event)
true
}
}


private inner class ScaleGestureListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
mScaleFactor *= detector.scaleFactor
// 限制缩放因子在0.1到10.0
mScaleFactor = mScaleFactor.coerceIn(0.1f, 10.0f)
mImageView.scaleX = mScaleFactor
mImageView.scaleY = mScaleFactor
return true
}
}
}

代码很简单直接使用ScaleGestureDetector去监听触摸事件,手势本质也是Google内部监听事件判断再回调给我们使用。当然我们这里不去查看源码,只看实现过程。
在使用过程中发现这种缩放并不平滑,而且响应有点慢,有延迟。猜想内部是由很多其他的判断吧。那我们只想简单一点怎么搞呢,那就是自己去判断缩放,还有实现单指滑动用手势也不太好实现的样子。所以我们试试第二种方式实现也就是触摸事件。


2.2 触摸事件


首先我们实现一下缩放,我们还是沿用上次使用onTouchListener来处理我们的触摸事件,布局文件中需要把imageView的缩放属性改为矩阵 android:scaleType="matrix"

private var startMatrix = Matrix()
mImageView.setOnTouchListener { _, event ->
when(event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_POINTER_DOWN -> {
// 记录双指按下的位置和距离
startDistance = getDistance(event)
if (startDistance > 10f) {
startMatrix.set(mImageView.imageMatrix)
mode = 2
}
return@setOnTouchListener true
}
}
true
}

没有自己处理过触摸事件的小伙伴可能会好奇MotionEvent.ACTION_MASK是什么,其实这个是为了处理多点触摸事件加的一个flag和action做and操作,我们就能处理ACTION_POINTER_DOWN和ACTION_POINTER_UP这两个多点触摸事件。
看下代码逻辑,我们先计算两个手指的距离,如果距离大于10就证明是缩放操作,设置成我们自己定义的模式,再把imageView的矩阵保存,后续对照片移动,缩放都是通过变换矩阵来实现的。
至于计算两个手指之间的距离用的勾股定理,来个示意图,大家就明白了。


yuque_diagram.jpg


计算如下。

 private fun getDistance(event: MotionEvent): Float {
val dx = event.getX(0) - event.getX(1)
val dy = event.getY(0) - event.getY(1)
return sqrt(dx * dx + dy * dy)
}

通过计算能得到直角边和邻边,对他们使用勾股定理就能得到斜边的值,也就是两个手指之间的距离。
有做过触摸事件监听的同学就应该知道,我们下一步要监听移动事件了也就是MotionEvent.ACTION_MOVE。

mImageView.setOnTouchListener { _, event ->
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_POINTER_DOWN -> {
// 记录双指按下的位置和距离
startDistance = getDistance(event)
if (startDistance > 10f) {
startMatrix.set(mImageView.imageMatrix)
mode = 2
}
return@setOnTouchListener true
}
MotionEvent.ACTION_MOVE -> {
if (mode == 2) {
// 双指缩放
val currentDistance = getDistance(event)
if (currentDistance > 10f) {
val scale = currentDistance / startDistance
mImageView.imageMatrix = startMatrix.apply {
postScale(scale, scale, getMidX(event), getMidY(event))
}
}
}
return@setOnTouchListener true
}
MotionEvent.ACTION_POINTER_UP -> {
mode = 0
return@setOnTouchListener true
}

else -> return@setOnTouchListener true

}

}

这里在move事件中我们也需要对手指之间的距离进行计算,如果距离超过10,就开始计算缩放倍数,通过postScale进行矩阵变换。
在MotionEvent.ACTION_POINTER_UP事件中对mode值进行复位操作,毕竟还有个单指拖动操作。
如果大家把上面的代码运行过就会发现怎么图片没有居中显示,这是因为我们的缩放属性被改为矩阵也就是android:scaleType="matrix",那么想要图片居中显示怎么操作呢,只需要在触摸时去改变缩放属性,其他的时候不变即可。
我们把imageView恢复成android:scaleType="fitCenter",在onTouchListener中加入(放在when前即可)

mImageView.scaleType = ImageView.ScaleType.MATRIX

这样一开始就可以保持图片在中央了。
这样缩放功能实现了,下面实现单指拖动功能,思路很简单记录第一次按下的位置,在移动过程中计算应该需要偏移的距离,再记录下当前的位置,以便于下次计算。

private var lastX = 0f
private var lastY = 0f
mImageView.setOnTouchListener { _, event ->
mImageView.scaleType = ImageView.ScaleType.MATRIX
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
// 记录单指按下的位置
lastX = event.x
lastY = event.y
mode = 1
startMatrix.set(mImageView.imageMatrix)
return@setOnTouchListener true
}
MotionEvent.ACTION_POINTER_DOWN -> {
// 记录双指按下的位置和距离
startDistance = getDistance(event)
if (startDistance > 10f) {
startMatrix.set(mImageView.imageMatrix)
mode = 2
}
return@setOnTouchListener true
}
MotionEvent.ACTION_MOVE -> {
if (mode == 1) {
// 单指拖动
val dx = event.x - lastX
val dy = event.y - lastY
mImageView.imageMatrix = startMatrix.apply {
postTranslate(dx, dy)
}
lastX = event.x
lastY = event.y
} else if (mode == 2) {
// 双指缩放
val currentDistance = getDistance(event)
if (currentDistance > 10f) {
val scale = currentDistance / startDistance
mImageView.imageMatrix = startMatrix.apply {
postScale(scale, scale, getMidX(event), getMidY(event))
}
}
}
return@setOnTouchListener true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
mode = 0
return@setOnTouchListener true
}

else -> return@setOnTouchListener true

}

}

代码实现和思路一样,我们还需要在MotionEvent.ACTION_UP中复位模式,调用postTranslate进行偏移。
这样基本上功能我们都简单实现了。下面我们就需要优化了代码,如果各位跟着实现了,就会发现缩放倍数太大了导致轻轻动一下就会放很大,还有别的都是需要我们优化的。


三、功能优化


3.1 优化缩放倍数太大问题


其实这个问题和我们处理move事件有关系,熟悉Android事件机制都知道一个完整的事件流程就是down->move.....move->up。知道了这个之后,再仔细看我们的代码

val currentDistance = getDistance(event)
if (currentDistance > 10f) {
val scale = currentDistance / startDistance
mImageView.imageMatrix = startMatrix.apply {
postScale(scale, scale, getMidX(event), getMidY(event))
}
}

在move事件中我们这样处理的,计算缩放倍数然后缩放,大体一看是没有什么问题的。但是,我们的move事件不止执行一次,这就导致我们的缩放不止执行一次,每次都是在原来的基础上放大或者缩小。所以轻轻移动倍数就会很多。
最简单的办法就是我们记录一下move过程中累计的倍数,如果到达最大值或者最小值就不让放大或者缩小了。代码如下。

if (scale > 1.0f) {
sumScale += scale
} else {
sumScale -= scale
}
if (sumScale >= maxScale || sumScale <= minScale) {
return@setOnTouchListener true
}

简单但是有效的方式。其中max和min,可以自己赋值。


3.2 保持原图不缩小


实现起来也很简单,需要先定义一个变量记录当前缩放之后的倍数。大家测试就会发现,如果是放大操作那么倍数就会大于1如果是缩小倍数就会比1 小。我们就可以利用这点来处理我们的逻辑。

private var lastScaleFactor = 1f
if (scale * lastScaleFactor > 1.0f) {
if (sumScale >= maxScale || sumScale <= minScale) {
return@setOnTouchListener true
}
sumScale += scale
mImageView.imageMatrix = startMatrix.apply {
postScale(scale, scale, getMidX(event), getMidY(event))
lastScaleFactor *= scale
}
} else {
sumScale -= scale
}

demo在这里点我点我


tips:demo好像不是放大不是很顺畅,但是在项目里用Gilde加载后很流畅,猜测是照片大小问题。但是思路是一样的问题不大。


作者:风罗伊曼
链接:https://juejin.cn/post/7224426218929012797
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android动态权限申请从未如此简单

前言 注:只想看实现的朋友们可以直接跳到最后面的最终实现 大家是否还在为动态权限申请感到苦恼呢?传统的动态权限申请需要在Activity中重写onRequestPermissionsResult方法来接收用户权限授予的结果。试想一下,你需要在一个子模块中申请权...
继续阅读 »

前言


注:只想看实现的朋友们可以直接跳到最后面的最终实现


大家是否还在为动态权限申请感到苦恼呢?传统的动态权限申请需要在Activity中重写onRequestPermissionsResult方法来接收用户权限授予的结果。试想一下,你需要在一个子模块中申请权限,那得从这个模块所在的ActivityonRequestPermissionsResult中将结果一层层再传回到这个模块中,相当的麻烦,代码也相当冗余和不干净,逼死强迫症。


使用


为了解决这个痛点,我封装出了两个方法,用于随时随地快速的动态申请权限,我们先来看看我们的封装方法是如何调用的:

activity.requestPermission(Manifest.permission.CAMERA, onPermit = {
//申请权限成功 Do something
}, onDeny = { shouldShowCustomRequest ->
//申请权限失败 Do something
if (shouldShowCustomRequest) {
//用户选择了拒绝并且不在询问,此时应该使用自定义弹窗提醒用户授权(可选)
}
})

这样是不是非常的简单便捷?申请和结果回调都在一个方法内处理,并且支持随用随调。


方案


那么,这么方便好用的方法是怎么实现的呢?不知道小伙伴们在平时开发中有没有注意到过,当你调用startActivityForResult时,AS会提示你该方法已被弃用,点进去看会告诉你应该使用registerForActivityResult方法替代。没错,这就是androidx给我们提供的ActivityResult功能,并且这个功能不仅支持ActivityResult回调,还支持打开文档,拍摄照片,选择文件等各种各样的回调,同样也包括我们今天要说的权限申请


其实Android在官方文档 请求运行时权限 中就已经将其作为动态权限申请的推荐方法了,如下示例代码所示:

val requestPermissionLauncher =
registerForActivityResult(RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// Permission is granted. Continue the action or workflow in your
// app.
} else {
// Explain to the user that the feature is unavailable because the
// feature requires a permission that the user has denied. At the
// same time, respect the user's decision. Don't link to system
// settings in an effort to convince the user to change their
// decision.
}
}

when {
ContextCompat.checkSelfPermission(
CONTEXT,
Manifest.permission.REQUESTED_PERMISSION
) == PackageManager.PERMISSION_GRANTED -> {
// You can use the API that requires the permission.
}
shouldShowRequestPermissionRationale(...) -> {
// In an educational UI, explain to the user why your app requires this
// permission for a specific feature to behave as expected, and what
// features are disabled if it's declined. In this UI, include a
// "cancel" or "no thanks" button that lets the user continue
// using your app without granting the permission.
showInContextUI(...)
}
else -> {
// You can directly ask for the permission.
// The registered ActivityResultCallback gets the result of this request.
requestPermissionLauncher.launch(
Manifest.permission.REQUESTED_PERMISSION)
}
}

说到这里,可能有小伙伴要质疑我了:“官方文档里都写明了的东西,你还特地写一遍,还起了这么个标题,是不是在水文章?!”


莫急,如果你遵照以上方法这么写的话,在实际调用的时候会直接发生崩溃:

java.lang.IllegalStateException: 
LifecycleOwner Activity is attempting to register while current state is RESUMED.
LifecycleOwners must call register before they are STARTED.

这段报错很明显的告诉我们,我们的注册工作必须要在Activity声明周期STARTED之前进行(也就是onCreate时和onStart完成前),但这样我们就必须要事先注册好所有可能会用到的权限,没办法做到随时随地有需要时再申请权限了,有办法解决这个问题吗?答案是肯定的。


绕过生命周期检测


想解决这个问题,我们必须要知道问题的成因,让我们带着问题进到源码中一探究竟:

public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull ActivityResultContract<I, O> contract,
@NonNull ActivityResultCallback<O> callback) {
return registerForActivityResult(contract, mActivityResultRegistry, callback);
}

public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultRegistry registry,
@NonNull final ActivityResultCallback<O> callback) {
return registry.register(
"activity_rq#" + mNextLocalRequestCode.getAndIncrement(), this, contract, callback);
}

public final <I, O> ActivityResultLauncher<I> register(
@NonNull final String key,
@NonNull final LifecycleOwner lifecycleOwner,
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {

Lifecycle lifecycle = lifecycleOwner.getLifecycle();

if (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
throw new IllegalStateException("LifecycleOwner " + lifecycleOwner + " is "
+ "attempting to register while current state is "
+ lifecycle.getCurrentState() + ". LifecycleOwners must call register before "
+ "they are STARTED.");
}

registerKey(key);
LifecycleContainer lifecycleContainer = mKeyToLifecycleContainers.get(key);
if (lifecycleContainer == null) {
lifecycleContainer = new LifecycleContainer(lifecycle);
}
LifecycleEventObserver observer = new LifecycleEventObserver() { ... };
lifecycleContainer.addObserver(observer);
mKeyToLifecycleContainers.put(key, lifecycleContainer);

return new ActivityResultLauncher<I>() { ... };
}

我们可以发现,registerForActivityResult实际上就是调用了ComponentActivity内部成员变量的mActivityResultRegistry.register方法,而在这个方法的一开头就检查了当前Activity的生命周期,如果生命周期位于STARTED后则直接抛出异常,那我们该如何绕过这个限制呢?


其实在register方法的下面就有一个同名重载方法,这个方法并没有做生命周期的检测:

public final <I, O> ActivityResultLauncher<I> register(
@NonNull final String key,
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {
registerKey(key);
mKeyToCallback.put(key, new CallbackAndContract<>(callback, contract));

if (mParsedPendingResults.containsKey(key)) {
@SuppressWarnings("unchecked")
final O parsedPendingResult = (O) mParsedPendingResults.get(key);
mParsedPendingResults.remove(key);
callback.onActivityResult(parsedPendingResult);
}
final ActivityResult pendingResult = mPendingResults.getParcelable(key);
if (pendingResult != null) {
mPendingResults.remove(key);
callback.onActivityResult(contract.parseResult(
pendingResult.getResultCode(),
pendingResult.getData()));
}

return new ActivityResultLauncher<I>() { ... };
}

找到这个方法就简单了,我们将registerForActivityResult方法调用替换成activityResultRegistry.register调用就可以了


当然,我们还需要注意一些小细节,检查生命周期的register方法同时也会注册生命周期回调,当Activity被销毁时会将我们注册的ActivityResult回调移除,我们也需要给我们封装的方法加上这个逻辑,最终实现就如下所示。


最终实现

private val nextLocalRequestCode = AtomicInteger()

private val nextKey: String
get() = "activity_rq#${nextLocalRequestCode.getAndIncrement()}"

fun ComponentActivity.requestPermission(
permission: String,
onPermit: () -> Unit,
onDeny: (shouldShowCustomRequest: Boolean) -> Unit
) {
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
onPermit()
return
}
var launcher by Delegates.notNull<ActivityResultLauncher<String>>()
launcher = activityResultRegistry.register(
nextKey,
ActivityResultContracts.RequestPermission()
) { result ->
if (result) {
onPermit()
} else {
onDeny(!ActivityCompat.shouldShowRequestPermissionRationale(this, permission))
}
launcher.unregister()
}
lifecycle.addObserver(object : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
launcher.unregister()
lifecycle.removeObserver(this)
}
}
})
launcher.launch(permission)
}

fun ComponentActivity.requestPermissions(
permissions: Array<String>,
onPermit: () -> Unit,
onDeny: (shouldShowCustomRequest: Boolean) -> Unit
) {
var hasPermissions = true
for (permission in permissions) {
if (ContextCompat.checkSelfPermission(
this,
permission
) != PackageManager.PERMISSION_GRANTED
) {
hasPermissions = false
break
}
}
if (hasPermissions) {
onPermit()
return
}
var launcher by Delegates.notNull<ActivityResultLauncher<Array<String>>>()
launcher = activityResultRegistry.register(
nextKey,
ActivityResultContracts.RequestMultiplePermissions()
) { result ->
var allAllow = true
for (allow in result.values) {
if (!allow) {
allAllow = false
break
}
}
if (allAllow) {
onPermit()
} else {
var shouldShowCustomRequest = false
for (permission in permissions) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
shouldShowCustomRequest = true
break
}
}
onDeny(shouldShowCustomRequest)
}
launcher.unregister()
}
lifecycle.addObserver(object : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
launcher.unregister()
lifecycle.removeObserver(this)
}
}
})
launcher.launch(permissions)
}

总结


其实很多实用技巧本质上都是很简单的,但没有接触过就很难想到,我将我的开发经验分享给大家,希望能帮助到大家。


作者:dreamgyf
链接:https://juejin.cn/post/7225516176171188285
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

IDEA 常用配置指南

1、外观配置 1.1 基本配置图 1.1-1 修改更改主题 + 背景图片 如果IDEA版本是2023.1.2以后的版本可以开启 newUI 体验新版的UI界面,我个人是挺喜欢的🌝 1.2 快捷键配置图1.2-1 修改快捷键 2、配置开发环境 2.1 配置G...
继续阅读 »

1、外观配置


1.1 基本配置

图 1.1-1 修改更改主题 + 背景图片
image-20230705155717189.png


如果IDEA版本是2023.1.2以后的版本可以开启 newUI 体验新版的UI界面,我个人是挺喜欢的🌝



1.2 快捷键配置

图1.2-1 修改快捷键
image-20230705160242097.png

2、配置开发环境


2.1 配置GIT

图2.1-1配置git
image-20230705161247492.png

2.2 配置maven



2.3 配置JDK
















图 2.3-1 配置项目的JDK
image-20230707163308798.png
image-20230707163311898.png


配置项目的语言版本指的是设置 JDK 版本,比如有些比较老的项目需要JDK-6启动,此时不需要安装JDK-6,可以把语言版本设置为6即可运行项目。



3、编辑器设置


3.1 基本配置













图 3.1-1 代码补全提示去掉匹配规则
image-20230705162157455.png












图 3.1-2 配置字体样式和大小
image-20230705162514296.png


字体建议使用等宽字体,如 Consolas 或者 JetBrains Mono,最近英特尔开源的 intel-one-mono 字体也挺好用的,喜欢的话可以安装一下。














图3.1-3 配置行号显示和方法分割
image-20230706084729859.png

3.3 编码风格配置













图 3.3-1 Java引入折叠
image-20230705162853383.png












图 3.3-2 Java代码超字符数换行
image-20230706090037457.png

3.4 配置代码模板


可以生成常见的代码模板,方便开发使用



  1. 配置文件模板,如图3.4-1所示;

  2. 配置文件的所有者信息,新建文件后会添加在类的头部,如图3.4-2所示;

  3. 代码生成模板,输入关键词,点击回车后即可触发生成代码,配置如图 3.4-3所示,使用效果如图3.4-4所示;













图 3.4-1 文件生成模板
image-20230705163048097.png












图 3.4-2 文件头设置
image-20230705163545871.png


















图 3.4-3 配置代码生成模板
image-20230705165216751.png
image-20230705164753838.png
image-20230705164825676.png


代码生成模板建议建立新的分组后,在新的分组内编写代码生成模板。

















图 3.4-4 代码模板使用效果
image-20230705165006840.png
image-20230705165017812.png

3.5 配置编辑器编码格式


建议都设置为 UTF-8 避免出现文件乱码问题













3.5-1 配置编辑器编码格式
image-20230705163642870.png

3.6 配置忽略的文件和文件夹


建议在忽略文件和文件夹内配置IDEA编辑器生成的文件,防止git提交时提交IDEA配置文件被打🦉













图 3.6-1 配置忽略的文件和文件夹
image-20230705165811144.png

4、好玩的插件

插件名称
Alibaba Java Coding阿里巴巴代码规范检查插件,根据《Java开发手册》进行代码规范性检查
Easy Code代码生成器,配置好代码模板后,可以从数据库一键生成从dao层到service层的代码
GenerateSerialVersionUID一键生成SerivalVersionUID
Jrebel and XRebel热部署插件,代码更改后点击build可以免重启热部署
JSON ParserJSON 格式化,在IDEA内增加JSON格式化窗口
LeetCode Editor实现IDEA内刷LeetCod题目
MyBatisX方便找到mapper和XML的映射
Rainbow Brackets彩虹括号,方便查看代码
Star Wars Progress Bar星球大战主题进度条
Translation翻译插件,支持翻译代码、注释等等
camelCase快捷的从全大写、下划线、大驼峰、小驼峰命名之间切换
作者:RealPluto
链接:https://juejin.cn/post/7253743476390740005
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

这么好的Android开发辅助工具App不白嫖可惜了

过年期间闲来没事,手撸了一个辅助Android开发调试的工具App,适合Android开发者和测试同学使用。 Github地址下载, Gitee地址下载(需要登录gitee) 或者去Google Play安装 功能概览 对我这样的懒人开发者来说,反复的做同样一...
继续阅读 »

过年期间闲来没事,手撸了一个辅助Android开发调试的工具App,适合Android开发者和测试同学使用。


Github地址下载,
Gitee地址下载(需要登录gitee)


或者去Google Play安装


功能概览


对我这样的懒人开发者来说,反复的做同样一件事简直太煎熬了,因此我把我平时开发中需要反复操作的命令和一些繁琐的操作整理成了一个工具。


废话不多说, 先上图了解下工具的大概功能有哪些(内容比截图丰富,欢迎下载体验)












CodeCrafts的核心是一个可拖动的侧边栏的悬浮窗,悬浮窗可以折叠或展开,悬浮窗中包含5大块功能分别对应一个TAB, 这5大块功能分别是应用控制、开发者选项、常用功能,常用系统设置和全局功能


请看视频预览:


floating-bar.gif


高清原图 introduction-floating-bar.gif


功能明细


1. 应用控制


应用控制能力将一些日常开发过程中对应用的一些繁琐的操作或者命令行指令转变为可视化的操作,而且还有自动收集和整理Crash, ANR日志,并且可以自动关联Logcat日志


文字太繁琐, 请直接看视频


application-control.gif


高清原图 introduction-application-controls.gif


2. 开发者选项


这里的开发者选项功能是将系统的开发者选项中一些最常用的开关放在悬浮窗中, 随时启用或关闭。
优势是不需要频繁去系统的开发者选项中去找对应开关,一键开闭。


我调研了其他有类似能力的工具App,都是引导用户去开发者选项中去开启或关闭功能。CodeCrafts一键开闭,无需跳转到系统开发者选项页面。


请看视频预览:


developer-options.gif


p2.gif


3. 最常用功能


没什么好介绍的,略。


4. 常用系统设置页面


这里承载了一些开发过程中经常需要打开的系统设置页面的快捷按钮,没什么好介绍的,略


5. 全局功能


这里的全局是相对于应用控制的,应用控制可以选择你正在开发的任意一款App, 然后应用控制中的所有能力都是对你的这个App的操作。 而全局控制中的功能不针对选中的App,所有App都适用


5.1 实时数据(Realtime data)


实时数据会随着当前页面变化或者系统事件实时变化



(以上图为例介绍, 实时数据的内容不仅仅只有这些)

内容含义用途
org.chromium.chrome.browser.firstrun.FirstRunActivity当前Activity的类名代码定位
launch time: 208ms当前Activity的冷启动耗时启动优化
com.android.chrome当前Activity所在应用的包名常用信息
Chrome(uid: 10163)当前Activity所在应用的名称和UID常用信息
pid: 23017当前Activity的进程ID常用信息
192.168.2.56,...当前系统的IP地址,可能有多个adb connect等
system当前应用是系统应用
allowBackUp当前应用有allowBackUp属性告警
实时数据未来还会有更多的扩展内容

5.2 不锁定屏幕


不会进入锁屏状态,也不会灭屏,避免开发过程中老是自动锁屏。


和系统开发者选项中的功能类似,区别是无论是否插入USB线都有效,开发者选项中的拔掉USB线后就无效了。
都可以用,具体选择看你的使用场景。


5.3 Latest Crashes


显示缓存中最近发生的Crash的调用堆栈,可能为空也可能不止一个Crash堆栈, 需要自行查看是否是你关注的Crash。


使用说明


CodeCrafts的很多功能依赖Shell权限, 如果发现存在功能不可用的情况,一般都是shell权限获取失败了, 只需要通过在电脑终端输入adb命令"adb tcpip 5555"指令, CodeCrafts就可以自动获取shell权限了。


image.png


adb tcpip 5555



  1. 第一次使用,连接电脑终端发送"adb tcpip 5555" 或

  2. 手机断电重启,连接电脑终端发送"adb tcpip 5555" 或

  3. 莫名其妙功能不能用了,连接电脑终端发送"adb tcpip 5555"


新增功能


有不少人反馈对CodeCrafts的实现原理感兴趣,后面新增的功能尽量配上实现原理



  1. CodeCrafts之断点调试 (1.0.15新增)


建设中



  1. 文件沙盒, 快速浏览App的文件目录

  2. 自动化,自动化点击,输入(比如自动跳广告,自动输入账号密码?)

  3. 组件检查, 快速查看View的类型, id, 颜色等

  4. ...


后期规划



  1. 悬浮窗的tab和内容可动态配置

  2. 应用控制增加应用性能数据

  3. 提供外部SDK接口,外部应用可接入CodeCrafts进行定制化改造


CodeCrafts持续更新中...


Github地址下载,
Gitee地址下载(需要登录gitee)


或者去Google Play安装


作者:小码哥哥
链接:https://juejin.cn/post/7194736298521788472
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

聊聊自己进入大厂前,创业公司的经历,我学到了什么?

前言 自从毕业开始工作之后,我坚持写了 2 年多的日记,基本节奏是每半年会写一篇,目的是为了记录自己成长的痕迹。 未来某个时间自己再回首的时候,能回味到此时此刻自己在做什么。当时的选择是否正确,是否在此时此刻,看到了未来发展的趋势,跟上了正确的车。 现在是 2...
继续阅读 »

前言


自从毕业开始工作之后,我坚持写了 2 年多的日记,基本节奏是每半年会写一篇,目的是为了记录自己成长的痕迹。


未来某个时间自己再回首的时候,能回味到此时此刻自己在做什么。当时的选择是否正确,是否在此时此刻,看到了未来发展的趋势,跟上了正确的车。


现在是 23 年 6 月,我第一次羊了,上一轮虽然逃过去了,但是这一次没能幸免,现在还在发着低烧。


距离上次写日记又过去了半年多,自己也步入 25 岁,这半年发生了很多事情,体会到时间变得更快了。


工作第一年:误打误撞加入创业公司



在来到字节之前,经历了校招被毁约,来到了创业公司,在创业公司工作了一年,作息是大小周,早 9 晚 9 ,对我来说这是一段比较记忆深刻的经历,教会了我很多互联网行业的东西。



校招被毁约


19 年的时候签约了一家金融公司,也是临近 20 年毕业前几十天,才被公司以疫情经济情况不好的理由,告知单方面毁约。


那个时候的我还是很害怕的,一点是临近毕业,大多校招已停止,第二点是自己有一段时间没有复习,很多知识已经遗忘。


一个朋友得知了我的情况,内推我到一家创业公司,也是内推第二天一大早就去公司面试,还好我还有一些基础在脑子里,现场面了两轮,当场就通过了,成为了前端的实习生一枚,公司也是答应毕业之后就转正。


面试通过之后,第二天就去公司上班了。第一天入职就拿到了 Macbook,也是第一次用 Mac 办公,想象那时的自己,就跟刚进城的人一样。


公司的结构


一个公司,总共就 50 来个人,做着 DAU 百万的产品。由于公司不大,所以做各种不同事情的人(销售、CEO、内容运营、新媒体运营、算法、产品),都坐在一起。敲代码的时候还能听到销售打电话的声音,因为是教育产品,所以销售的合作方大多是猿辅导、好未来、作业帮、学霸君等教育头部公司。


虽然公司小,但是产品各个环节非常全面:



  • ABTest

  • 算法

  • Hive


公司小的好处是,决策层级少,一个想法想要落地是很快的。在公司 1 年期间,快速上线了 3 款小程序,也快速从 0 到 1 上线了金融产品。PC、H5、小程序开发都有涉及。


整个团队的氛围是非常 Open 的,可以试验各种新技术,不像大厂内部会封装一些框架,有内部标准,不好去实践一些开源的框架。


与普通公司不同的地方


个人扮演的角色


我觉得最大不同的地方是角色,我是一名前端,但不仅扮演着前端的角色。要对产品体验负责,也要自己设计页面 UI。


公司没有测试,所以在日常开发中,每个研发和产品都是 QA,要对 C 端百万 DAU 产品对应的用户体验负责。


每个人都对需求负责,一个需求上线之后,可以自己写 SQL 去 Hive 里捞埋点数据,验证 AB 的可行性。


可以深入感受互联网不同角色发挥的作用


在创业公司,因为每周都有公司周会,大家会聚在一起聊每周各部门的进展,在会上,也可以全流程的了解到一个产品的完整生命周期。




  • 内容运营视角:



    • 做公众号,发文章,在存在粉丝基础的情况下,头版收入是很可观的,可以达到 6 位数+。




  • 产品运营视角:



    • 做竞品分析,看出对方哪方面做的好,我们要抄袭哪快的功能。

    • 做电话回访,了解用户的痛点,尤其针对停留时间较长的重度 C 端用户。




  • 销售视角:



    • 通过和大型教育机构合作,由于家长是很愿意为孩子进行教育付费的,所以通过弹窗收集信息配合电销,可以达到很可观的收入。




  • 算法视角:



    • 通过 AB 试验调整算法策略,可以优化广告收入,另外也可以提前计算预测插入广告可能带来的收益。

    • 调整算法策略,也可以优化用户停留时长,增强用户粘性。




  • 数据分析:



    • 通过 Hive 离线数据计算,可以生成一些报表数据,给提供用户信息聚合查看功能。




  • 产品视角:



    • 在产品基本功能打磨完毕后,要尽可能往付费方向引导。




  • 运维视角:



    • 将服务迁移到 K8S 集群,可以降低维护成本。




  • 后端视角:



    • 和算法、数分团队配合,另外还需要负责机器运维相关。




  • 老板视角:



    • 关注一些重要事项的进展,以及查看上线后的数据,是否符合产品预期。

    • 最终产品需要自负盈亏,功能不能一步设计到位,也需要把一些功能做成付费使用的。

    • 关注公司每个方向资金支出情况,控制公司收支,避免快速花光融资资金。保持自己的股份不被过度稀释。




我的直观感受是,自己虽然初入茅庐,但通过这一年的感知,深入理解了 C 端产品的全流程。这对我来说也是一笔很大的财富。


营收方面


App 内广告占大头,开屏广告>插屏广告>贴片广告,其次公众号文章等也是赚钱的利器,销售带来的收益远不如以上两个。总共这些一个月7位数还是有的。实际上最大的开销除了人力成本,还是服务器的成本,这个成本逼近7位数。


创业公司的生命周期



  1. 公司在快速发展期,有很多功能需要开发,这时是需要人的时候,会无限招人。

  2. 在产品 2-3 年之后,如没有新的大方向,进入一段停滞期,指的是 DAU 的不增长或下降。

  3. 产品稳定期,不再需要人,核心骨干退出团队,HC 缩减,产品转向以盈利为目标。

  4. 自负盈亏,break even。不再为公司资金发愁,不再需要融资。

  5. 保持公司运作,通过手段维持 DAU 和用户付费意愿,通过一些预消费手段留住用户,扩大收益。


快速验证


快速验证是 CEO 经常提的一个点,不过在王兴和张一鸣成功的经验来看,这也是正确的。


快速验证是说快速从 0 到 1 上线一个产品,冷启动或硬广,在短期查看一个产品的数据,如果产品数据不够理想,便放弃产品。试验下一个风口上的题材。


像美团,或者字节现在也在使用这种策略,快速上线 App 并试错,留下那些抓住用户的产品。


公司的瓦解


一个产品的瓶颈


当一个产品被打磨到 3 年之后,一般来说主流程就比较完善了,换句话说是用户需要的功能,产品都有了。这个时候也就过了 PM 发力 RD 开发的时期,在这之后即便这个公司只有运营,也可以保持产品正常运行。


创业公司的问题


CEO 的话语权会很大


一个人带来的决策不一定合理,当产品的发展不再合理时。大家会出现不满情绪,久而久之大家也不再团结协作,在快速上线几个小程序无果之后,3 个月内 50% 的研发团队成员纷纷离职了,不过大伙也很厉害,离职之后大多都去了大厂。


转变方向为营收优先


通过缩减一系列支出,想方设法让公司达到赚钱的状态。


手段有:砍 HC,团建,下线产品不需要的费钱的功能。


另外我也是一步步看着,公司从半层多楼的工区面积,变成 5分之二,4分之一的大小,最后工区被卖掉,撤离北京。


我的离开


我的离开也是必然,在后期被拉到老板的新产品线帮忙做产品从 0 到 1 建设。对当时工作还不到 1 年的我来说,还是很有压力,独自 own 一个私人银行项目。


在长时间宣传下,仍是没有用户使用,我能明显的感受到,新产品前景是渺茫的,只是老板的一厢情愿。另外新产品线的研发非常少,只是一番的催活,其实过程也决定了结果,产品是做不成的。在这种情况下,我提出了离职。


不过我也很感谢这段经历,能让我对从 0 到 1 创业有新的理解,另外也锻炼了我的抗压能力,增强了技术积累。


最近的工作


工作上


工作上在建设插件市场,提供了一种能快速开发页面组件的方式,能直接嵌入组件到前端中,类似动态执行模块联邦注入的组件。是一块很有意思的功能,类似于 Chrome 应用商城,其实开发工具建设一直是我比较喜欢也擅长的方向,未来也会继续在这方面努力,学习其它语言,做更快更高效的工具,为开发提效。


详细可以看这篇我今年写的文章 带你从 0 到 1 实现前端「插件市场架构」


能力提升方面


编程技能


学习并实战了以下技能:



  • VSCode 插件开发


  • Rush.js

    • 大型项目构建管理。



  • Golang (MySQL / Redis / Kafka)

    • 主要还是 API 层面的熟悉,目的还是为了能用非 NodeJS 语言写一写后端,以及了解更多的后端知识。



  • Rust
    稍微了解了一下语法,之前也写了一篇文章:以 JS 的视角带你入门 Rust 语言,快速上手


开源库


轻量的模块联邦


非编程相关


最近这 2 年,锻炼了画图、写 PRD、拉通对齐的能力,大厂更加专精一个方面,这让我能静下心来,不再像创业公司一样,受老板的影响,不再做快速迭代的事情,而是把产品打磨好,更加以用户角度出发思考用户需要什么,补齐什么功能。


愿望


毕业之后,由于疫情一直都是在国内旅游。还没出过国,希望疫情后每年自己都能出去走走,行万里路。把最好的景色都记录下来,拓宽眼界,放松心情。我很喜欢大海,尤其是四环环海的小岛,看着大海能让自己的心平静下来。接下来还有几个非常想去的地方、意大利、冰岛、新西兰、瑞士,夏威夷,希望能在 30 岁之前达成目标。


作者:EricLee
链接:https://juejin.cn/post/7243252896392314937
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

鹅厂七年半,写在晋升失败的不眠之夜

夜半惊醒 看了下时间,凌晨4:37,辗转反侧。3.7号大领导告诉我这次10升11没通过,这是我知道晋级失败的第4个夜晚,从震惊到否认,到愤怒,到麻木,再到冷静。我的技术能力、思考深度、攻坚能力明明是小组里突出的一个,但在此次技术职级的晋升上,却名落孙山,落在同...
继续阅读 »

夜半惊醒


看了下时间,凌晨4:37,辗转反侧。3.7号大领导告诉我这次10升11没通过,这是我知道晋级失败的第4个夜晚,从震惊到否认,到愤怒,到麻木,再到冷静。我的技术能力、思考深度、攻坚能力明明是小组里突出的一个,但在此次技术职级的晋升上,却名落孙山,落在同组小伙伴之后。技术人员,终究困于技术的窠臼。


工作经历


浑浑噩噩的四年


我毕业就进入鹅厂工作,15年,正是鹅厂大杀四方的一年,至今犹记得当时拿到鹅厂 offer 时的兴奋和同学眼中的艳羡。来了公司后,跟着项目组开始创新项目,一眼望到头的挣不来钱,浑浑噩噩的干了3年,和一起进公司的小伙伴们收入差距越来越大。


好在我的驱力不错,既然挣不到钱,那就好好积累技术能力,提升自己。那段时间,周围没有伙伴,没有对比,没有好的机会,只剩下我一个人慢慢悠悠的学习积累,我以为自己足够努力,没有荒废时间;后来才发现自己其实是井底之蛙,一起入职的小伙伴付出了百倍的努力和数不清的不眠之夜,1年多就已经能 cover 当时的业务了,没过2年就已经提干了。


2018年底,我满怀信心去答辩后台 3.1,没过。我当时还觉得评委装逼,挑刺,后来回头看写的都是s,一点含量都没有。


时来运转


2019年3月,答辩刚结束。部门业务调整,我们被整组划到了一个相对挣钱的业务上,leader 也调整成后台 leader。


新业务还是挣钱,凭着第一个项目积累的技术和项目经验,我得到了新领导的认可,第一年干的轻轻松松就挣到了远远超出之前的收入,和之前累死累活拿温饱线形成鲜明对比,可见大厂里跟对项目是多么的重要。


只是没想到第一年就是巅峰,后面随着贸易战、反垄断,鹅厂形势越来越差,我们业务收入也受到极大影响,人员也越来越冗余。挣的钱少了,分蛋糕的人还多了,可想而知,收入越来越差。


在这里,我成功从8级升到了10级工程师。说起来我们这一届实名惨,第一批毕业要等1年才能晋级,最后一批8升9需要公司范围内通道评审,第一批9升10走BG内评审,第一批10升11走部门内评审,全程试验品,一样没落下。


10升11


去年年底公司晋升改革,10升11评审下放部门,职级和待遇不再挂钩,不仅看重”武功“,还看中”战功“,同时传闻这次晋升名额极少。大领导找我谈话,要不就别去了?我想了想这两年的技术和项目积累,不甘心。


整个中心,就我和同组的小伙伴两个人一起去,我挑的是个一直在做的有技术挑战、持续优化的项目,小伙伴挑的是挣钱的项目。我准备了几个周末,小伙伴临时抱佛脚,结果小伙伴过了,我没过。评委说,我过度设计(优化过头)、没有实战演练容灾。


我和大领导说这个和我预期差太多了,我和小伙伴一起预答辩过,都知道讲的啥,咱是技术评审,我的项目技术含量、架构和挑战明明更好,为啥是我没过?大领导说你是认知偏差,人家讲的确实好。不忿,遂找评委 battle,咱要真按这个说法,能不能对参加评审的项目一致对待,不要双标?且不说我是不是过度设计,凭啥对我的项目就要求容灾演练,对别人的项目就视而不见?评委不语,你的项目对部门价值没产生什么收益和价值。


部门400+人,4个晋升11级名额,大概率是一个中心一个。一个技术评审,糅合了许多超过技术方面的考量,业务挣不挣钱和技术有啥关系?技术好的业务就能挣钱?业务挣钱多的是技术好的原因?


我以前也晋级失败过,但是我认输,因为相对而言,整个BG一起评审,你没过就是你技术差,其他因素影响没有这么大。这次明明是我更有技术深度、更投心思在优化上,却事与愿违。


反思与感悟


反思一下。千头万绪,毛主席说,那是没有抓住主要矛盾。


大的方面上讲:这两年大环境差,公司收入减少,需要降本增效。我要是管理层,也会对内部薪资成本、晋升等要求控制增速;政策制定上,也会将资源更倾斜于现金流业务,控制亏损业务的支出。在这个大的背景下,公司不愿意养“能力强,战功少”的技术骨干了;更愿意养“能力强,战功多”的人员。希望员工把精力都聚焦在业务挣钱上。


部门层面上讲:和公司政策一脉相承。晋升名额少,那紧着挣钱的中心、挣钱的小组、挣钱的个人给。这个意义上讲,职级体系已经不是衡量技术能力的标尺了。你技术能力强,没用。你得是核心,得是领导认可的人。


中心层面上讲:要争取名额,否则怎么团结手底下人干活。而且名额要给业务干活干的最出色的那个人,其他人就往后稍稍。我要是我领导,年前也会劝退我的。毕竟我技术能力虽然强,更多的是问题专家的角色,而不是业务核心。


且预测一下,中心之间肯定进行了各种资源置换。评审估计流于形式。慢说我被挑出来了问题,就是挑不出来问题也得给你薅下来。我就是和小伙伴互换项目去讲,估计还是小伙伴过,到时候评委就该说我“你做这东西有啥难度,我叫个毕业生都能来做”了。此处我想起了一副对联,不太合适,就是很搞笑。“说你行你就行不行也行,说你不行你就不行行也不行”,横批是“不服不行”。



从个人角度讲,付出和收获不成正比,难受吗?那肯定的。但谁让方向一开始就错了呢?这世界不公平的事情多了,不差这一个。



重新出发


综上来说,短期内,不改变发力方向到业务上,后面可以说晋升无望。以前老是觉得自己技术能力强,心高气傲,心思没在业务上,勤于术而懒于道,实在是太过幼稚。做了几年都对很多东西不甚了了。


今后,需要趁着还能拼的时候拼一下,潜心业务。编码和开发只是很小一部分,更要从:



  1. 业务大局出发,实时数据驱动,监控、统计一定要完善,要有看板,否则无法衡量。

  2. 主动探索业务,给产品经理出主意,一起合力把产品做上去做大,提升对业务的贡献,探索推荐、探索推广渠道等等。产品做不上去,个人也没机会发展。

  3. 多做向上管理,和领导、大领导多沟通。做好安排的每一件小事,同时主动汇报,争取重获信任。

  4. 主动承担,做一个领导眼里靠谱放心的人。

  5. 多思考总结,多拔高提升,不要做表面的勤奋工作,看似没有浪费时间,实则每分每秒都在浪费时间。

  6. 多社交,多沟通,多交流,打破技术人员的牢笼。


凡事都有两面性,福兮祸所依,祸兮福所倚。多受挫折、早受挫折不是坏事。


2023,就立个 flag 在这里吧。


作者:醉梦星河
链接:https://juejin.cn/post/7208907027840630840
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

从互联网到国企、从一线城市到三线省会

6月的北京格外的闷热,比起内蒙真的热了不少,整整四个月没来北京了,晚上出高铁来到清河站时还是那么的熟悉,挤上13号线路过五道口、知春路,去西直门换乘2号线,再换上5号线到了宋家庄,最后换上回村的亦庄线,从北京的最西北边走到最东南角。看着地铁上的疲惫的人们,这次...
继续阅读 »

6月的北京格外的闷热,比起内蒙真的热了不少,整整四个月没来北京了,晚上出高铁来到清河站时还是那么的熟悉,挤上13号线路过五道口、知春路,去西直门换乘2号线,再换上5号线到了宋家庄,最后换上回村的亦庄线,从北京的最西北边走到最东南角。看着地铁上的疲惫的人们,这次回来自己更像是一个游客的视角,观察着以前的“自己”。从3月初离职一直没有记录过这段经历,但这次去北京让我觉得有必要写一些自己的感受和体会。


离职前的纠结


意外通过的面试


毕业四年一直从事Java开发,在京东两年左右,2月份很偶然的看到内蒙的一则国企招聘,本着今年大概率要回去工作的想法,顺便就报名了,又很顺利的通过了笔试和面试,面试时特意请假一天从北京跑回内蒙,下午等待面试的时候手机被收走四个小时,四个小时没处理工作消息差点爆炸,各种报警和需求沟通群里被@,面试完急匆匆的坐高铁回北京继续上班。本来只是想试下机会,莫名就通过了,这下轮到自己开始纠结了。


无时无刻的报警&下不了的班


在京东工作应该是我做开发这些年达到的事业最高峰,从之前写简单逻辑的小菜鸟一下子开阔了视野,见到从未了解的新领域,对流量并发有了新的认识。但这份工作确实很辛苦,我们几乎是7*24小时待命,每天都要保持手机开机,随时都会有接口报警,一定要第一时间响应处理,核心接口还要配置语音报警,即使晚上也会直接打电话进行通知,如果不接电话就会被系统记录,还有一些产品运营的问题也会随时发生,某些定位的门店或者商品不展示了,都要及时给人家反馈处理。这两年我们真的不管去哪里都要带着电脑,出去旅游或者逛街都是如此,脑子里的那根弦一直紧紧绷着,就像是悬在头上的达摩克利斯之剑。


还有每天忙碌的工作,写不完的需求,开不完的会,解决不完的问题,下不了的班,从早上九点去了就开始忙碌,经常晚上十点多才可以下班,很多人可能会待到12点甚至更久,但我确实是卷不动了,身上压着的三座大山,需求排期、日常报警、绩效目标,每个月的发版上线是不可能变得,再多事情也得把需求开发完和前后端联调完,再让测试验证通过,而这期间有报警问题也要第一时间处理,不然会记录个人的问题处理能力,如果报警拖久了变成事故,那就是全部人背锅了。每个季度的绩效目标也要完成,否则到了季度末绩效考核验证时,即使需求都写完,报警都处理了,绩效目标没完成也是不合格。时间就是那么多,任何事情的优先级都很高,只能自己不断加班去做。京东工作这两年都没有写过自己的博客,因为确实是没有时间,这些以后想写一个京东工作系列再详细记录下来。


坚持还是放弃


即使吐槽了很多,但压力确实让人成长,这些年是对我职业生涯的重新铸造,就像炼铁般一锤一锤反复敲打,从思维逻辑到开发能力、沟通交流等方面都有了很大的改变,自己逐渐成长为了部门最能背锅的,顶住了网关这个问题爆炸源。此时走难免不甘心,上个季度末刚拿到了A+的绩效,国企面试通过的同时也通过了京东内部晋升评审,正如自己一直喜欢开发这行,眼前也是事业逐渐越来越好,朝着预期的目标不断靠近,此时真的要激流勇退吗。


这个问题真的思考了很久很久,在北京很快乐也很痛苦,做着最喜欢的事情,但这么多年也只有自己,我慢慢认为生活不应该是这样的,生活不应该只有工作,工作是为了生活,但生活不是为了工作。回去之后的问题:


一是:工资大幅度缩水,降到生活快要不能自理,有种刚毕业的感觉。


二是:技术这方面基本就不会再有大的进步。第二点真的是让我最难以接受的,看着京东的神灯社区里面各种技术文章,职业生涯的巅峰就此打住真是非常不甘心。回去之后就有了更多的时间,不再全部投入到技术上,去找朋友,同学,家人,放下背负了太久太久的压力。


在北京感觉自己就是一节电池,现在的我有90%的电量,但如此大的压力不可能一直保持冲劲,等到了互联网人退休年龄,我还能有机会再体面的回去吗,对于北京,年轻人都是一茬一茬的韭菜,


最终还是选择了离开,带着遗憾和不舍,带着对新生活的期待。


国企的压力


与之前每次在北京换工作的压力不同,国企的压力是找不到目标,每天两点一线的生活,早上喝茶下午喝咖啡,看看文档看看资料,写一些工作总结,催一催开发进度,这些就是一天的工作内容,开始的一两个月真的有些迷失,这些就是我想要的吗,安逸圈也未免太安逸了,当你突然从强压状态下换到清闲的环境里,一时之间非常不适应,总感觉人生就要如此荒废过去,前一两个月我总想看别的工作机会,想让自己重新忙起来,以前在井底只能看到那片蓝天,现在好不容易出来拥有了大片蓝天,却又想赶紧再找到自己的井。


学习不止工作


四个月过去了,逐渐开始看清楚自己的目标,也有了一些简单的规划,现在的时间越来越多,其实完全可以做更多自己想做的事情,互联网的知识不再像以前那么集中,需要你有更大的耐心和毅力去坚持学习,可以学到的理论更多,但实践的机会比较少,以前站在巨人的肩膀上用海量的数据和流量来验证,现在还在吃过去的老本,扩充了知识的广度,而深度还停滞在那里。


目前只是摆平了自己的心态,逐渐认清形势再改变还需要时间,时间会让一切都变得更好,只要你愿意的话。以前所有的生活都给了工作,现在工作只是生活的一部分,用生活之余学到的东西继续反哺工作,提高本就不多工作的效率,也顺便去学习业务,技术在三线城市不再是唯一,而究竟如何平衡二者的关系,让自己还能继续拥有竞争力,这是我目前还看不清的。


人际关系


没有绝对的公平,但在北京是有相对的公平,而回到三线城市的国企,公平变得妙不可言,人际关系成为了重中之重,小小的部门内部已然是派系林立,十几个人的关系层级更是深不可测,想起在jd,工位后面坐着小领导,对面最在平台部负责人,管理几百人的领导也是和我们一样坐在一起,同事们经常说:在互联网公司比你大好几级的领导,和你就是平级。而在国企所有的工作,事情都有条条框框去限制,你永远看不清里面的水有多深,同时生活也被同事关系所入侵,大家经常吃饭喝酒聊工作,即使在开怀畅饮的时候也要时刻谨慎提防,说错话和做错事要比想的更加严重。在互联网公司争吵是必不可少的,不吵就说不清楚需求,甩不了锅,而回到这里,所有人都客客气气的,所有人都慈眉善目和你微笑,只是面具背后的脸很难看到。


做技术的本身比较呆板,不会八面玲珑也不会左右逢源,我只想做好自己的事情,做一个不出声的小透明,不争不抢,做自己喜欢的事情。


所见所想


记忆拉回现实,看着北京地铁上的众生相,感觉大家都很疲惫但眼里还有希望,曾经北漂的我现在只想逃离,虽然只回去四个月但依然接受不了快节奏的北京,紧绷了四年的弦已经彻底放松,去总部和过往的同事吃了个饭,大家坐着聊聊天,为他们还能坚持在北京奋斗而加油,每个人都有自己的选择,我的退缩也需要勇气,时至今日也乐得接受自己的选择的路。


如今互联网的大潮正在褪去,可能越来越多的人面临这样的选择,假如我们还有的选的话,其实生活中大部分事情我们是没得选的,生活一步一步推着你往前走。


如果问我,刚毕业选择来大城市后悔吗,我坚信自己不后悔,这里让我看到学到也付出了太多。


如果问我,现如今离开大城市后悔吗,我也坚信自己不后悔,这里没有家没有归宿,我终究要回去,只怕走的越远越迷茫。


作者:AlgoRain
链接:https://juejin.cn/post/7253115535482437689
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

人需借以虚名而成事

前言 自从我看冯唐老师推荐的《资治通鉴》之后,对历史有那么一丢丢的兴趣,直到在短视频里面刷到《百家讲坛》里面几位老师的讲解片段,以及跟自己经历结合,瞬间满满的共鸣,是啊时代一直在变化,而里面的为人处事、社会规则凡此种种却从来没有太大变化,这就是我们需要去学习...
继续阅读 »

前言




自从我看冯唐老师推荐的《资治通鉴》之后,对历史有那么一丢丢的兴趣,直到在短视频里面刷到《百家讲坛》里面几位老师的讲解片段,以及跟自己经历结合,瞬间满满的共鸣,是啊时代一直在变化,而里面的为人处事、社会规则凡此种种却从来没有太大变化,这就是我们需要去学习历史的原因。


所以有了以下感慨。



<<百家讲坛>> 不愧是经典,整本历史歪歪斜斜写着两字:权谋,谋事、谋人,加上不确定因素,天时地利人和,最终变成命运。
人的一生就像书中一行字轻描淡写,绘成一笔~



人需借以虚名而成事




这里解释一下,人需要倚靠外部的这些名号、头衔从而能有比较大的作为。为什么这么讲呢,听我细细道来~


人生需要四行


这是王立群老师讲的,人生要想有点成就需要四行,首先你自己行,你自身本身够硬,能够做好事,其次是有人说你行,从古代的推荐机制,比如说优秀的人推荐其他优秀的人才对不对,李白的大名远扬,唐玄宗在公主引荐之下见了皇帝,第三个说你行的人要行,推荐你的那个人本身能力要强,不然没有说服力,第四是身体行,很多机会是熬出来的,比如司马懿伺候了3位君主,兢兢业业,最终才能完成自己的大业。


何为借以虚名?


1、刘备的例子,他在比较年长的时候还无法实现自己的基业,寄人篱下,他登场的时候是怎样的呢,名号叫刘皇叔,翻开长长的祖谱,八辈子打不到杆哈哈哈,不过因为当时被曹操挟持,所以需要有汉族帮忙自己,所以顺理成章的提拔上来。


2、项羽出生在一个贵族家庭。他的父亲项梁是楚国将领,曾经在楚汉战争中和刘邦一起作战。而他的母亲也是楚国的贵族,是楚王的女儿。


相比之下,刘邦的出身则比较低微。他的祖父是一个农民,父亲是一个小县官。他自己也曾经当过一个普通的县尉,但因为功绩不够而被罢免。


尽管项羽出身高贵,但他的性格比较豁达,喜欢豪情壮志和享乐人生,行事大胆不拘小节。刘邦则更加谨慎稳重,注重政治规划和长远发展。


3、王立群老师讲述过他自身的故事,小学的时候成绩优秀得到保送,后面名额被学校给了别人,自己去了私立的学校,半工半读,到了大学还是遇到了同样的遭遇,后面就到比较好的大学教书,但是有一天校长跟他讲他的成绩还是出身问题需要去另一所低一点的学校教书,他当时就哭了,命运比较坎坷的。


4、工作中,我们常常会听到,某位领导自我介绍某某大厂经历,还有某某大学毕业,还有当你投一些大厂的核心部门的时候,都会对你学历进行考核。


所以我们从上面3个例子来看,人生确实正如王立群老师讲的,需要四行,当你自己不行的时候,即使有机会也会最终流失掉;当没有推荐你的时候,也会埋没在茫茫人海中,并不是说是金子总会发光,现实是伯乐更加重要;当说你行的人没有能力的时候也不行,名牌大学、大厂经历也是这样,通过一个有名声的物体从而表示你也可以,也很厉害哈哈;最后身体是本钱,这个就不用过多的讲解了。


读历史的目的,从前人的故事、经历中,学习到点什么,化为自身的经验、阅历。


我发现现在很多人都喜欢出去玩,说的开阔视野,扩展见识,在我看来读历史才是真正的拓宽自己的眼界,原来几百年前也有人跟我一样遇到同样的问题,也有那些胸怀大志的三国名将郁郁不得志。


历史是一本书籍,它可以很薄,几句话、几行字就概括了一个朝代,它也可以很厚,厚到整个朝代的背景,比如气候情况、经济情况、外交情况、内政情况,各个出名的人物事迹,那岂是几页纸能讲得完的,是多么的厚重。


如果人不能借以虚名怎么做事?


修炼两件事情




同样百家讲坛也给出了一个比较好的见解,一个人想要成事,需要两个明,一个是高明、一是精明。高明是你有足够的眼光去发现,发现机会也好,发现危机也好,往往人生的成就需要这种高瞻远瞩的;另一个精明,是指在做事上打磨,细心主动,王阳明讲的知行合一,里面很关键的一点就是事上练,方法论在实战中去验证总结。


心态上练




李连杰在采访中说了这么一个耐人寻味的故事,他发现不管是有钱的人还是没有钱的人都会生气,比如一些老板在为几百万损失破口大骂,也有人因为几万的丢失是否生气,也有人丢了几块钱很愤怒。他觉得人都会愤怒,只是那个导致他愤怒的级别不一样而已。


他早年给自己定目标,发现当达到目标之后,发现还是有人比自己厉害,所以即使达到目标也没有想象快乐;即使有钱人无法解决痛苦的问题,比如生老病死。


所以人生意义在哪里?


我认为如果你奔着成事的想法,多修炼上面两件事,如果没有发展机会,不如躺平,积累自己的能力,就像道德经里面的无为而为,而心态的修炼是人生终极修炼,因为生死才是人生最大的难关,人应该怎么脱离时间束缚,不受衰老影响。


恰恰历史给了我们启示,要有所作为,能够为人类历史上留下精神粮食,做出浓厚贡献,就像我们都没有见过孔子、老子吧,但是他们的精神至今都在影响着我们。


作者:大鸡腿同学
链接:https://juejin.cn/post/7252251628090490917
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

央国企也开始欠薪停薪了

上海某院自6月29日起,全员停工三个月,理由是现阶段已经没有新的设计项目需要完成。去年该公司还对疫情后行业复苏充满期待,连续在两地新开了分公司。今年的市场让人猝不及防。近日,网上曝出“某建筑企业工资停了”,引发众人热议。有网友表示“半年没发工资了”“已经好几年...
继续阅读 »

上海某院自6月29日起,全员停工三个月,理由是现阶段已经没有新的设计项目需要完成。去年该公司还对疫情后行业复苏充满期待,连续在两地新开了分公司。今年的市场让人猝不及防。



近日,网上曝出“某建筑企业工资停了”,引发众人热议。有网友表示“半年没发工资了”“已经好几年只交保险不发工资了。”......



有人说某铁和某建都发不出工资了,震惊。


有人说上班以来就没有发过100%的工资,以后也不会补。


有的小朋友被吓到了,说考虑安稳才选的国有企业,已经开始担心这种情况了。

评论区的前辈也是毫不留情:“我本来也是求稳去了国企,现在也发不出来了”、“市属国企、半年没发工资了。”、“能拿一天是一天”



还有人预测国企也顶不住了,开始要裁员了。


由于行情不好,许多设计师都倾向于跳槽去国企,大家的心态都是:“虽然给的薪水不如民企,但是至少能旱涝保收。“然而,现在建筑行业的国企也被曝出停发工资,实在令人惊愕。

房企、建企欠薪被处罚

水泥内参了解到,近期多地公布建筑工程领域企业欠薪行为,至少有164家建企、房企被通报或处罚。

江苏省住建厅公布拖欠工资引发群体性事件被限制市场准入及通报批评企业和人员名单。对126家企业予以全省范围内限制建筑市场准入;对44家企业予以全省通报批评;对42名从业人员予以全省限制建筑市场准入;对86名从业人员予以全省通报批评。


上海市对2023年建筑工程领域发生的拖欠农民工工资事件中负有相应责任的企业和项目负责人进行通报前公示,17家建筑工程企业被通报

济南市住房和城乡建设局发布关于公布涉访拖欠农民工工资项目专项检查情况的通知,9家房地产企业未按月足额拨付人工费,移送市城管局进行处罚


烟台市住建局对存在拖欠农民工工资、消极对待清欠工作行为,并造成不良社会影响的32家开发施工企业,以及私自截留、挪用农民工工资的55名内部承包人,实施暂停网签备案、列入信用管理黑名单、农民工工资支付监管平台公示曝光等多重处罚。

义乌市通报近期因欠薪违法行为被市人社部门行政处罚的有关企业,11家建筑领域企业被列入信用惩戒名单。


云南省人力资源和社会保障厅向社会公布了《2023年第二批重大劳动保障违法案件和2023年第一批拖欠农民工工资失信联合惩戒对象名单》。云南宝栋建设工程有限公司拖欠192名劳动者劳动报酬8743759元;云南齐优建筑劳务有限公司拖欠88名劳动者劳动报酬3417179.00元;陕西凯得瑞建设工程有限公司拖欠9名劳动者劳动报酬135050元;山东美达建工集团股份有限公司因拖欠83名劳动者工资1499378.30元等4家企业被列入拖欠农民工工资失信联合惩戒对象名单

台州市通报建筑工程领域农民工工资支付责任履行不到位的10家典型建筑施工企业:广东禾润建设工程有限公司、浙江普川装饰工程有限公司、江苏中南建筑产业集团有限责任公司、浙江广优建设有限公司、中铁北京工程局集团第五工程有限公司、中国二十二冶集团有限公司、浙江祥生建设工程有限公司、杭州庆强建设有限公司、浙江海大建设有限公司、九华恒业建设有限公司。

马鞍山市通报全市工程建设领域工资支付诚信等级评定情况,对存在拖欠工资、违法发包(转包)等行为的28家企业评为B级,给予通报批评


合肥市通报拖欠农民工工资失信联合惩戒对象名单,7家建筑公司及负责人被处罚

湖北省人力资源和社会保障厅公布2023年第二批拖欠劳动报酬典型案件,2家建筑相关企业被通报... ...


凉山州会理船城牌水泥厂欠薪1739.62万元

2月13日,凉山州会理船城牌水泥有限责任公司管理人发布职工债权公示称,截至2023年2月10日,船城水泥公司职工债权总额为人民币17,396,229.04元(包括欠付职工工资、补偿金、职工垫缴的基本养老保险和基本医疗保险费用、伤残等。会理市人民法院于2022年4月21日作出裁定受理船城水泥公司的破产清算。

拉萨某水泥厂欠薪536万元

西藏自治区人民检察院网站2023年1月18日消息,拉某某等人与拉萨市某水泥有限公司签订运输合同,并按合同约定拉某某等人承运该公司的运输项目。2021年6月起,该公司因资金周转问题,导致拉某某等225户878人运输款未得到支付,未支付运输款金额为536万元。达孜区人民检察院认为,水泥公司与878人存在合同关系,水泥公司应按照合同约定支付相关的款项。

此外,烟台磐龙水泥有限公司被曝光拖欠员工工资3年,约70-80人为此上访。目前,厂里的设备已经被老板卖掉。

武宣县桐岭镇人民政府消息,2022年7月至2023年4月期间,叶某等12人在桐岭镇华润水泥厂从事工程建设相关项目,完工后何老板仍拖欠其5万元多工资未支付,多次追讨无果。

崇左市某建材水泥厂被责令停产后,造成全体职工失业。水泥厂长期拖欠工资,让部分职工生活陷入困境。河市白塔镇显德汪创业水泥拖欠工资据悉,员工不干了,最后一个月工资不给,而且还是压一个工资的。

据悉,除了拖欠工资外,近几年由于企业效益不好,水泥厂裁员降薪的事情也时有发生,有业内人员表示,别说降薪了,现在能按时发工资就已经超越很多人了......

来源:http://www.163.com/dy/article/I8VT204K0552CAGB.html

收起阅读 »

vue3 表单封装遇到的一个有意思的问题

web
前言 最近在用 vue3 封装 element 的表单时遇到的一个小问题,这里就简单记录一下过程。话不多说直接上代码!!! 正文 部分核心代码 import { ref, defineComponent, renderSlot, type PropType, ...
继续阅读 »

前言


最近在用 vue3 封装 element 的表单时遇到的一个小问题,这里就简单记录一下过程。话不多说直接上代码!!!


正文


部分核心代码


import { ref, defineComponent, renderSlot, type PropType, type SetupContext } from 'vue';
import { ElForm, ElFormItem, ElRow, ElCol } from 'element-plus';
import type { RowProps, FormItemProps, LabelPosition } from './types';
import formItemRender from './CusomFormItem';
import { pick } from 'lodash-es';

const props = {
formRef: {
type: String,
default: 'customFormRef',
},
modelValue: {
type: Object as PropType<Record<string, unknown>>,
default: () => ({}),
},
rowProps: {
type: Object as PropType<RowProps>,
default: () => ({
gutter: 24,
}),
},
formData: {
type: Array as PropType<FormItemProps[]>,
default: () => [],
},
labelPosition: {
type: String as PropType<LabelPosition>,
default: 'right',
},
labelWidth: {
type: String,
default: '150px',
},
};

const elFormItemPropsKeys = [
'prop',
'label',
'labelWidth',
'required',
'rules',
// 'error',
// 'showMessage',
// 'inlineMessage',
// 'size',
// 'for',
// 'validateStatus',
];

export default defineComponent({
name: 'CustomForm',
props,
emits: ['update:modelValue'],
setup(props, { slots, emit, expose }: SetupContext) {
const customFormRef = ref();

const mValue = ref({ ...props.modelValue });

watch(
mValue,
(newVal) => {
emit('update:modelValue', newVal);
},
{
immediate: true,
deep: true,
},
);

// 表单校验
const validate = async () => {
if (!customFormRef.value) return;
return await customFormRef.value.validate();
};

// 表单重置
const resetFields = () => {
if (!customFormRef.value) return;
customFormRef.value.resetFields();
};

// 暴漏方法
expose({ validate, resetFields });

// col 渲染
const colRender = () => {
return props.formData.map((i: FormItemProps) => {
const formItemProps = { labelWidth: props.labelWidth, ...pick(i, elFormItemPropsKeys) };
return (
<ElCol {...i.colProps}>
<ElFormItem {...formItemProps}>
{i.formItemType === 'slot'
? renderSlot(slots, i.prop, { text: mValue.value[i.prop], props: { ...i } })
: formItemRender(i, mValue.value)}
</ElFormItem>
</ElCol>

);
});
};

return () => (
<ElForm ref={customFormRef} model={mValue} labelPosition={props.labelPosition}>
<ElRow {...props.rowProps}>
{colRender()}
<ElCol>
<ElFormItem labelWidth={props.labelWidth}>{renderSlot(slots, 'action')}</ElFormItem>
</ElCol>
</ElRow>
</ElForm>

);
},
});

<script setup lang="ts">
import CustomerForm from '/@/components/CustomForm';
const data = ref([
{
formItemType: 'input',
prop: 'name',
label: 'Activity name',
placeholder: 'Activity name',
rules: [
{
required: true,
message: 'Please input Activity name',
trigger: 'blur',
},
{ min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
],
},
{
formItemType: 'select',
prop: 'region',
label: 'Activity zone',
placeholder: 'Activity zone',
options: [
{
label: 'Zone one',
value: 'shanghai',
},
{
label: 'Zone two',
value: 'beijing',
},
],
},
{
formItemType: 'inputNumber',
prop: 'count',
label: 'Activity count',
placeholder: 'Activity count',
},
{
formItemType: 'date',
prop: 'date',
label: 'Activity date',
type: 'datetime',
placeholder: 'Activity date',
},
{
formItemType: 'radio',
prop: 'resource',
label: 'Resources',
options: [
{ label: 'Sponsorship', value: '1' },
{ label: 'Venue', value: '2' },
],
},
{
formItemType: 'checkbox',
prop: 'type',
label: 'Activity type',
options: [
{ label: 'Online activities', value: '1', disabled: true },
{ label: 'Promotion activities', value: '2' },
{ label: 'Offline activities', value: '3' },
{ label: 'Promotion activities', value: '4' },
{ label: 'Simple brand exposure', value: '5' },
],
},
{
formItemType: 'input',
prop: 'desc',
type: 'textarea',
label: 'Activity form',
placeholder: 'Activity form',
},
{
formItemType: 'slot',
prop: 'test',
label: 'slot',
},
]);
const model = reactive({
name: '',
region: '',
count: 0,
date: '',
resource: '',
type: [],
desc: '',
test: '1111',
});
const formRef = ref();
const submitForm = () => {
const valid = formRef.value.validate();
if (valid) {
console.log(model);
} else {
return false;
}
};

const resetForm = () => {
formRef.value.resetFields();
};
</script>

<template>
<div class="wrap">
<CustomerForm
ref="formRef"
:v-model="model"
:formData="data"
>

<template #test="scope">
{{ scope.text }}
</template>
<template #action>
<el-button type="primary" @click="submitForm()">Create</el-button>
<el-button @click="resetForm()">Reset</el-button>
</template>
</CustomerForm>
</div>
</template>


<style scoped>
.wrap {
margin: 30px auto;
width: 600px;
height: auto;
}
</style>



问题现象


代码其实非常简单,运行起来也很正常很流畅😀😀😀,但是当我填写完表单后点击提交按钮,打印model的值时,发现值全没给上。


微信截图_20230709120015.png


原因分析


这里经过两年半的尝试,终于发现在定义model时,将const model = reactive({xxx}) 改为 const model = ref({xxx}) 后就正常了。思考了一下 ref 定义的对象,源码上最后通过 toReactive 还是被转化为 reactive,ref 用法上需要 .value, 数据上这两者应该没有什么不同。然后我就去把 reactive、ref 又看了看也没发现问题。在emit('update:modelValue', newVal) 处打印也是正常的。


watch( mValue,
(newVal) => {
console.log('newVal>>>', newVal)
emit('update:modelValue', newVal);
},
{ immediate: true, deep: true, }
);

最后有意思的是,我把 const model 改成 let model tmd居然也正常了,这就让我百思不得其解了😕😕😕


解决


其实上面 debugger 后,就确定了方向 肯定是emit('update:modelValue', newVal)这里出问题了,回到使用组件,把v-model 拆解一下,此时还看不出来问题。


1688879457596.jpg


换成:modelValue="model" @update:model-value="update(e)"问题立马出现了,ts已经提示了 model是常量!


微信截图_20230709131250.png


这样问题就非常明了了,这就解释了 let 可以 const 不行,但你好歹报个错啊 😤😤😤 坑死人不偿命,可见即使在 template 里面这样写@update:model-value="model = $event" ts 也无能为力!
回过头再来看看 ref 为啥可行呢?当改成ref时,


  const update = (e) => {
model.value = e;
};

update是要.value 的,修改常量对象里面属性是正常的。再想想 ref 的变量在 template 中 vue 已经帮我们解过包了,v-model 语法糖拿着属性直接赋值并不会产生问题。而常量 reactive 则不能修改,也可以在在里面再包裹一层对象,但这样就有点冗余了。


总结


总结起来就是,const 定义的 reactive 对象,v-model 去更新整个对象的时候失败,常量不能更改,也没有给出任何报错或提示!


唉!今年太难了。前端路漫漫其修远兮,还需

作者:Pluto5280
来源:juejin.cn/post/7253453908039123005
更加卷地而行!😵😵😵

收起阅读 »

极致舒适的Vue弹窗使用方案

web
一个Hook让你体验极致舒适的Dialog使用方式! Dialog地狱 为啥是地狱? 因为凡是有Dialog出现的页面,其代码绝对优雅不起来!因为一旦你在也个组件中引入Dialog,就最少需要额外维护一个visible变量。如果只是额外维护一个变量这也不是不...
继续阅读 »

一个Hook让你体验极致舒适的Dialog使用方式!


image.png


Dialog地狱


为啥是地狱?


因为凡是有Dialog出现的页面,其代码绝对优雅不起来!因为一旦你在也个组件中引入Dialog,就最少需要额外维护一个visible变量。如果只是额外维护一个变量这也不是不能接受,可是当同样的Dialog组件,即需要在父组件控制它的展示与隐藏,又需要在子组件中控制。


为了演示我们先实现一个MyDialog组件,代码来自ElementPlus的Dialog示例


<script setup lang="ts">
import { computed } from 'vue';
import { ElDialog } from 'element-plus';

const props = defineProps<{
visible: boolean;
title?: string;
}>();

const emits = defineEmits<{
(event: 'update:visible', visible: boolean): void;
(event: 'close'): void;
}>();

const dialogVisible = computed<boolean>({
get() {
return props.visible;
},
set(visible) {
emits('update:visible', visible);
if (!visible) {
emits('close');
}
},
});
</script>

<template>
<ElDialog v-model="dialogVisible" :title="title" width="30%">
<span>This is a message</span>
<template #footer>
<span>
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="dialogVisible = false"> Confirm </el-button>
</span>
</template>
</ElDialog>
</template>

演示场景


就像下面这样:


Kapture 2023-07-07 at 22.44.55.gif


示例代码如下:


<script setup lang="ts">
import { ref } from 'vue';
import { ElButton } from 'element-plus';

import Comp from './components/Comp.vue';
import MyDialog from './components/MyDialog.vue';

const dialogVisible = ref<boolean>(false);
const dialogTitle = ref<string>('');

const handleOpenDialog = () => {
dialogVisible.value = true;
dialogTitle.value = '父组件弹窗';
};

const handleComp1Dialog = () => {
dialogVisible.value = true;
dialogTitle.value = '子组件1弹窗';
};

const handleComp2Dialog = () => {
dialogVisible.value = true;
dialogTitle.value = '子组件2弹窗';
};
</script>

<template>
<div>
<ElButton @click="handleOpenDialog"> 打开弹窗 </ElButton>
<Comp text="子组件1" @submit="handleComp1Dialog"></Comp>
<Comp text="子组件2" @submit="handleComp2Dialog"></Comp>
<MyDialog v-model:visible="dialogVisible" :title="dialogTitle"></MyDialog>
</div>
</template>

这里的MyDialog会被父组件和两个Comp组件都会触发,如果父组件并不关心子组件的onSubmit事件,那么这里的submit在父组件里唯一的作用就是处理Dialog的展示!!!🧐这样真的好吗?不好!


来分析一下,到底哪里不好!


MyDialog本来是submit动作的后续动作,所以理论上应该将MyDialog写在Comp组件中。但是这里为了管理方便,将MyDialog挂在父组件上,子组件通过事件来控制MyDialog


再者,这里的handleComp1DialoghandleComp2Dialog函数除了处理MyDialog外,对于父组件完全没有意义却写在父组件里。


如果这里的Dialog多的情况下,简直就是Dialog地狱啊!🤯


理想的父组件代码应该是这样:


<script setup lang="ts">
import { ElButton } from 'element-plus';

import Comp from './components/Comp.vue';
import MyDialog from './components/MyDialog.vue';

const handleOpenDialog = () => {
// 处理 MyDialog
};
</script>

<template>
<div>
<ElButton @click="handleOpenDialog"> 打开弹窗 </ElButton>
<Comp text="子组件1"></Comp>
<Comp text="子组件2"></Comp>
</div>
</template>

在函数中处理弹窗的相关逻辑才更合理。


解决之道


🤔朕观之,是书之文或不雅,致使人之心有所厌,何得无妙方可解决?


依史记之辞曰:“天下苦Dialog久矣,苦楚深深,望有解脱之道。”于是,诸位贤哲纷纷举起讨伐Dialog之旌旗,终“命令式Dialog”逐渐突破困境之境地。


image.png


没错现在网上对于Dialog的困境,给出的解决方案基本上就“命令式Dialog”看起来比较优雅!这里给出几个网上现有的命令式Dialog实现。


命令式一


codeimg-facebook-shared-image (5).png


吐槽一下~,这种是能在函数中处理弹窗逻辑,但是缺点是MyDialog组件与showMyDialog是两个文件,增加了维护的成本。


命令式二


基于第一种实现的问题,不就是想让MyDialog.vue.js文件合体吗?于是诸位贤者想到了JSX。于是进一步的实现是这样:


codeimg-facebook-shared-image (7).png


嗯,这下完美了!🌝


doutub_img.png


完美?还是要吐槽一下~



  • 如果我的系统中有很多弹窗,难道要给每个弹窗都写成这样吗?

  • 这种兼容JSX的方式,需要引入支持JSX的依赖!

  • 如果工程中不想即用template又用JSX呢?

  • 如果已经存在使用template的弹窗了,难道推翻重写吗?

  • ...


思考


首先承认一点命令式的封装的确可以解决问题,但是现在的封装都存一定的槽点。


如果有一种方式,即保持原来对话框的编写方式不变,又不需要关心JSXtemplate的问题,还保存了命令式封装的特点。这样是不是就完美了?


那真的可以同时做到这些吗?


doutub_img (2).png


如果存在一个这样的Hook可以将状态驱动的Dialog,转换为命令式的Dialog吗,那不就行了?


它来了:useCommandComponent


image.png


父组件这样写:


<script setup lang="ts">
import { ElButton } from 'element-plus';

import { useCommandComponent } from '../../hooks/useCommandComponent';

import Comp from './components/Comp.vue';
import MyDialog from './components/MyDialog.vue';

const myDialog = useCommandComponent(MyDialog);
</script>

<template>
<div>
<ElButton @click="myDialog({ title: '父组件弹窗' })"> 打开弹窗 </ElButton>
<Comp text="子组件1"></Comp>
<Comp text="子组件2"></Comp>
</div>
</template>

Comp组件这样写:


<script setup lang="ts">
import { ElButton } from 'element-plus';

import { useCommandComponent } from '../../../hooks/useCommandComponent';

import MyDialog from './MyDialog.vue';

const myDialog = useCommandComponent(MyDialog);

const props = defineProps<{
text: string;
}>();
</script>

<template>
<div>
<span>{{ props.text }}</span>
<ElButton @click="myDialog({ title: props.text })">提交(需确认)</ElButton>
</div>
</template>

对于MyDialog无需任何改变,保持原来的样子就可以了!


useCommandComponent真的做到了,即保持原来组件的编写方式,又可以实现命令式调用


使用效果:


Kapture 2023-07-07 at 23.44.25.gif


是不是感受到了莫名的舒适?🤨


不过别急😊,要想体验这种极致的舒适,你的Dialog还需要遵循两个约定!


两个约定


如果想要极致舒适的使用useCommandComponent,那么弹窗组件的编写就需要遵循一些约定(其实这些约定应该是弹窗组件的最佳实践)。


约定如下:



  • 弹窗组件的props需要有一个名为visible的属性,用于驱动弹窗的打开和关闭。

  • 弹窗组件需要emit一个close事件,用于弹窗关闭时处理命令式弹窗。


如果你的弹窗组件满足上面两个约定,那么就可以通过useCommandComponent极致舒适的使用了!!



这两项约定虽然不是强制的,但是这确实是最佳实践!不信你去翻所有的UI框看看他们的实现。我一直认为学习和生产中多学习优秀框架的实现思路很重要!



如果不遵循约定


这时候有的同学可能会说:哎嘿,我就不遵循这两项约定呢?我的弹窗就是要标新立异的不用visible属性来控制打开和关闭,我起名为dialogVisible呢?我的弹窗就是没有close事件呢?我的事件是具有业务意义的submitcancel呢?...


doutub_img.png


得得得,如果真的没有遵循上面的两个约定,依然可以舒适的使用useCommandComponent,只不过在我看来没那么极致舒适!虽然不是极致舒适,但也要比其他方案舒适的多!


如果你的弹窗真的没有遵循“两个约定”,那么你可以试试这样做:


<script setup lang="ts">
// ...
const myDialog = useCommandComponent(MyDialog);

const handleDialog = () => {
myDialog({
title: '父组件弹窗',
dialogVisible: true,
onSubmit: () => myDialog.close(),
onCancel: () => myDialog.close(),
});
};
</script>

<template>
<div>
<ElButton @click="handleDialog"> 打开弹窗 </ElButton>
<!--...-->
</div>
</template>

如上,只需要在调用myDialog函数时在props中将驱动弹窗的状态设置为true,在需要关闭弹窗的事件中调用myDialog.close()即可!


这样是不是看着虽然没有上面的极致舒适,但是也还是挺舒适的?


源码与实现


实现思路


对于useCommandComponent的实现思路,依然是命令式封装。相比于上面的那两个实现方式,useCommandComponent是将组件作为参数传入,这样保持组件的编写习惯不变。并且useCommandComponent遵循单一职责原则,只做好组件的挂载和卸载工作,提供足够的兼容性



其实useCommandComponent有点像React中的高阶组件的概念



源码


源码不长,也很好理解!在实现useCommandComponent的时候参考了ElementPlus的MessageBox


源码如下:


import { AppContext, Component, ComponentPublicInstance, createVNode, getCurrentInstance, render, VNode } from 'vue';

export interface Options {
visible?: boolean;
onClose?: () => void;
appendTo?: HTMLElement | string;
[key: string]: unknown;
}

export interface CommandComponent {
(options: Options): VNode;
close: () => void;
}

const getAppendToElement = (props: Options): HTMLElement => {
let appendTo: HTMLElement | null = document.body;
if (props.appendTo) {
if (typeof props.appendTo === 'string') {
appendTo = document.querySelector<HTMLElement>(props.appendTo);
}
if (props.appendTo instanceof HTMLElement) {
appendTo = props.appendTo;
}
if (!(appendTo instanceof HTMLElement)) {
appendTo = document.body;
}
}
return appendTo;
};

const initInstance = <T extends Component>(
Component: T,
props: Options,
container: HTMLElement,
appContext: AppContext | null = null
) =>
{
const vNode = createVNode(Component, props);
vNode.appContext = appContext;
render(vNode, container);

getAppendToElement(props).appendChild(container);
return vNode;
};

export const useCommandComponent = <T extends Component>(Component: T): CommandComponent => {
const appContext = getCurrentInstance()?.appContext;

const container = document.createElement('div');

const close = () => {
render(null, container);
container.parentNode?.removeChild(container);
};

const CommandComponent = (options: Options): VNode => {
if (!Reflect.has(options, 'visible')) {
options.visible = true;
}
if (typeof options.onClose !== 'function') {
options.onClose = close;
} else {
const originOnClose = options.onClose;
options.onClose = () => {
originOnClose();
close();
};
}
const vNode = initInstance<T>(Component, options, container, appContext);
const vm = vNode.component?.proxy as ComponentPublicInstance<Options>;
for (const prop in options) {
if (Reflect.has(options, prop) && !Reflect.has(vm.$props, prop)) {
vm[prop as keyof ComponentPublicInstance] = options[prop];
}
}
return vNode;
};

CommandComponent.close = close;

return CommandComponent;
};

export default useCommandComponent;

除了命令式的封装外,我加入了const appContext = getCurrentInstance()?.appContext;。这样做的目的是,传入的组件在这里其实已经独立于应用的Vue上下文了。为了让组件依然保持和调用方相同的Vue上下文,我这里加入了获取上下文的操作!


基于这个情况,在使用useCommandComponent时需要保证它在setup中被调用,而不是在某个点击事件的处理函数中哦~


最后


如果你觉得useCommandComponent对你在开发中有所帮助,麻烦多点赞评论收藏😊


如果useCommandComponent对你实现某些业务有所启发,麻烦多点赞评论收藏😊


如果...,麻烦多点赞评论收藏😊


如果大家有其他弹窗方案,欢迎留言交流哦!


1632388279060.gif

收起阅读 »

前端业务代码,怎么写测试用例?

web
为什么前端写测试用例困难重重 关于不同测试的种类,网上有很多资料,比如单元、集成、冒烟测试,又比如 TDD BDD 等等,写测试的好处也不用多说,但是就前端来说,写测试用例,特别是针对业务代码测试用例写还是不太常见的事情。我总结的原因有如下几点: 搭建测试环...
继续阅读 »

为什么前端写测试用例困难重重


关于不同测试的种类,网上有很多资料,比如单元、集成、冒烟测试,又比如 TDD BDD 等等,写测试的好处也不用多说,但是就前端来说,写测试用例,特别是针对业务代码测试用例写还是不太常见的事情。我总结的原因有如下几点:



  • 搭建测试环境比较麻烦,什么 jest config、mock 这个、mock 那个,有那个时间写完 mock,都能写完业务代码了

  • 网上能找到的测试教程资料都是简单的 demo,与真实业务场景不匹配,看了这些 demo,还是不知道怎么写测试

  • 网上很难找到合适的模版项目,像 antd 这种都是针对公共 UI 组件的测试用例,对我们写业务逻辑的测试用例没有太大的参考价值

  • 业务需求改动频繁,导致维护测试用例的成本高


我最近在做一个 React Native 项目,想践行 TDD 开发,所以我花了几天时间,梳理了市面上常见的前端测试工具,看了 N 个前端测试实践的文章,最终选择了大道至简,只用下面两个库:



  • jest,不多说,最流行的类 react 项目的测试框架

  • react-test-renderer,用于测试组件 UI,搭配 jest 的快照功能一起使用,让测试 UI 变得不再繁琐


业务代码的测试用例之心法


不要这样写业务代码的测试用例


不要面向实现写测试用例,比如针对某个组件,把每个 props 都写一个测试用例,而 props 很有可能因为业务改动或重构等原因改动,导致我们也要改动相应的测试用例代码,尽管测试用例本身没有错误。


页面跳转、没有任何交互的静态页面、兼容性、


业务代码要怎么写测试


为了平衡开发时间和写测试用例的时间,我认为对于业务代码来说,测试用例不需要面面俱到,什么逻辑都写个测试用例。我们只需要关注用户交互相关的逻辑,具体来说,我会重点关注以下方面:



  • pure 组件的 UI 是否有对应的测试用例

  • 面向功能测试,比如用户输入、点击按钮、加载数据时的 UI、数据为空时的 UI

  • 针对工具函数的各种输入输出测试


写测试用例所需的成本由低到高依次是:

reducer → pure component → business component → DOM testing → e2e

其中 pure component 指的是只有 props 的,只负责渲染的 dummy component。Business compoent 指的是包含 store dispatcher、api fetch、副作用等业务逻辑的业务组件。


程度越靠后,测试的成本越高,所以我们可以花多些精力在测试组件和 reducer 上,少花时间在 DOM 测试和 e2e 测试上。而对于 reducer、pure component、business component 来说,它们的测试用例是相辅相成的,因为 business component 里就包括了 reducer 的使用和 pure component 的渲染,

所以测 business compoent,就等于侧面测到了 reducer 和 pure component。这个测试方法在 Redux 官网也有提到:

完全避免直接测试任何 Redux 代码,将其视为实现细节cn.redux.js.org/usage/writi…


案例:如何测试 pure component


Dumb Component 只用来接收 props 并进行展示,所以它更易于测试,我们只需要 mock 父组件传来的 props 即可,然后搭配 Jest 的 snapshot 快照来判断测试用例是否通过。


比如我们要测试 Tag Component,这个组件的功能很简单,就是展示标签 UI:


Pasted image 20230708154654.png


我们可以用快照测试来记录下这个组件的 UI,如果以后 UI 有改动,这条测试用例就会报错。比如我们现在多了一个业务逻辑,需要每个标签都自动带上 [],好比之前标签展示的是 text,根据业务逻辑,现在标签展示的是 [text]


我们修改 Tag 组件,添加相应的业务逻辑:


Pasted image 20230708155008.png


这时候跑测试用例,可以发现用例报错,而且我们可以报错结果知道组件的 UI 进行了哪些改动,如果这个改动是符合我们期待的,那么直接更新 snapshot 即可:


Pasted image 20230708155111.png


同时,提交代码的时候,这条测试用例对应的 snapshot 也会跟着一起 commit,在 Code Review 阶段我们可以根据 snapshot 来直观的看到组件 UI 进行了哪些改动,美滋滋啊。


如何对 Reducer 进行测试


用 Redux 作为状态管理工具时,一种比较好的编程范式是,让 Store 提供数据,组件只负责渲染数据。组件 UI 可能会因为业务变动而频繁的更改,而 Redux 中的数据逻辑不会经常更改,所以在没有任何像上面那种组件 UI 的快照测试时,可以优先测试 Redux,后期补上组件的快照测试。


工作流:



  1. 先写测试用例,开一个 snapshot

  2. 开启 jest --watch,编写 action 和 reducer 相关代码

  3. 当 snapshot 是我们期待的值,就保存这个 snapshot

  4. 完成测试用例
    作者:Kz
    来源:juejin.cn/post/7253102401452032055
    的编写

收起阅读 »

为什么谷歌搜索不支持无限分页

这是一个很有意思却很少有人注意的问题。 当我用Google搜索MySQL这个关键词的时候,Google只提供了13页的搜索结果,我通过修改url的分页参数试图搜索第14页数据,结果出现了以下的错误提示: 百度搜索同样不提供无限分页,对于MySQL关键词,百度...
继续阅读 »

这是一个很有意思却很少有人注意的问题。


当我用Google搜索MySQL这个关键词的时候,Google只提供了13页的搜索结果,我通过修改url的分页参数试图搜索第14页数据,结果出现了以下的错误提示:


Google不能无限分页


百度搜索同样不提供无限分页,对于MySQL关键词,百度搜索提供了76页的搜索结果。


百度不能无限分页


为什么不支持无限分页


强如Google搜索,为什么不支持无限分页?无非有两种可能:



  • 做不到

  • 没必要


「做不到」是不可能的,唯一的理由就是「没必要」。


首先,当第1页的搜索结果没有我们需要的内容的时候,我们通常会立即更换关键词,而不是翻第2页,更不用说翻到10页往后了。这是没必要的第一个理由——用户需求不强烈。


其次,无限分页的功能对于搜索引擎而言是非常消耗性能的。你可能感觉很奇怪,翻到第2页和翻到第1000页不都是搜索嘛,能有什么区别?


实际上,搜索引擎高可用和高伸缩性的设计带来的一个副作用就是无法高效实现无限分页功能,无法高效意味着能实现,但是代价比较大,这是所有搜索引擎都会面临的一个问题,专业上叫做「深度分页」。这也是没必要的第二个理由——实现成本高。


我自然不知道Google的搜索具体是怎么做的,因此接下来我用ES(Elasticsearch)为例来解释一下为什么深度分页对搜索引擎来说是一个头疼的问题。


为什么拿ES举例子


Elasticsearch(下文简称ES)实现的功能和Google以及百度搜索提供的功能是相同的,而且在实现高可用和高伸缩性的方法上也大同小异,深度分页的问题都是由这些大同小异的优化方法导致的。


什么是ES


ES是一个全文搜索引擎。


全文搜索引擎又是个什么鬼?


试想一个场景,你偶然听到了一首旋律特别优美的歌曲,回家之后依然感觉余音绕梁,可是无奈你只记得一句歌词中的几个字:「伞的边缘」。这时候搜索引擎就发挥作用了。


使用搜索引擎你可以获取到带有「伞的边缘」关键词的所有结果,这些结果有一个术语,叫做文档。并且搜索结果是按照文档与关键词的相关性进行排序之后返回的。我们得到了全文搜索引擎的定义:



全文搜索引擎是根据文档内容查找相关文档,并按照相关性顺序返回搜索结果的一种工具



2022-06-08-085125.png


网上冲浪太久,我们会渐渐地把计算机的能力误以为是自己本身具备的能力,比如我们可能误以为我们大脑本身就很擅长这种搜索。恰恰相反,全文检索的功能是我们非常不擅长的。


举个例子,如果我对你说:静夜思。你可能脱口而出:床前明月光,疑是地上霜。举头望明月,低头思故乡。但是如果我让你说出带有「月」的古诗,想必你会费上一番功夫。


包括我们平时看的书也是一样,目录本身就是一种符合我们人脑检索特点的一种搜索结构,让我们可以通过文档ID或者文档标题这种总领性的标识来找到某一篇文档,这种结构叫做正排索引


目录就是正排索引


而全文搜索引擎恰好相反,是通过文档中的内容来找寻文档,诗词大会中的飞花令就是人脑版的全文搜索引擎。


飞花令就是全文搜索


全文搜索引擎依赖的数据结构就是大名鼎鼎的倒排索引(「倒排」这个词就说明这种数据结构和我们正常的思维方式恰好相反),它是单词和文档之间包含关系的一种具体实现形式。


单词文档矩阵


打住!不能继续展开了话题了,赶紧一句话介绍完ES吧!



ES是一款使用倒排索引数据结构、能够根据文档内容查找相关文档,并按照相关性顺序返回搜索结果的全文搜索引擎



高可用的秘密——副本(Replication)


高可用是企业级服务必须考虑的一个指标,高可用必然涉及到集群和分布式,好在ES天然支持集群模式,可以非常简单地搭建一个分布式系统。


ES服务高可用要求其中一个节点如果挂掉了,不能影响正常的搜索服务。这就意味着挂掉的节点上存储的数据,必须在其他节点上留有完整的备份。这就是副本的概念。


副本


如上图所示,Node1作为主节点,Node2Node3作为副本节点保存了和主节点完全相同的数据,这样任何一个节点挂掉都不会影响业务的搜索。满足服务的高可用要求。


但是有一个致命的问题,无法实现系统扩容!即使添加另外的节点,对整个系统的容量扩充也起不到任何帮助。因为每一个节点都完整保存了所有的文档数据。


因此,ES引入了分片(Shard)的概念。


PB级数量的基石——分片(Shard)


ES将每个索引(ES中一系列文档的集合,相当于MySQL中的表)分成若干个分片,分片将尽可能平均地分配到不同的节点上。比如现在一个集群中有3台节点,索引被分成了5个分片,分配方式大致(因为具体如何平均分配取决于ES)如下图所示。


分片


这样一来,集群的横向扩容就非常简单了,现在我们向集群中再添加2个节点,则ES会自动将分片均衡到各个节点之上:


横向扩展


高可用 + 弹性扩容


副本和分片功能通力协作造就了ES如今高可用支持PB级数据量的两大优势。


现在我们以3个节点为例,展示一下分片数量为5,副本数量为1的情况下,ES在不同节点上的分片排布情况:


主分片和副分片的分布


有一点需要注意,上图示例中主分片和对应的副本分片不会出现在同一个节点上,至于为什么,大家可以自己思考一下。


文档的分布式存储


ES是怎么确定某个文档应该存储到哪一个分片上呢?



通过上面的映射算法,ES将文档数据均匀地分散在各个分片中,其中routing默认是文档id。


此外,副本分片的内容依赖主分片进行同步,副本分片存在意义就是负载均衡、顶上随时可能挂掉的主分片位置,成为新的主分片。


现在基础知识讲完了,终于可以进行搜索了。


ES的搜索机制


一图胜千言:


es搜索



  1. 客户端进行关键词搜索时,ES会使用负载均衡策略选择一个节点作为协调节点(Coordinating Node)接受请求,这里假设选择的是Node3节点;

  2. Node3节点会在10个主副分片中随机选择5个分片(所有分片必须能包含所有内容,且不能重复),发送search request;

  3. 被选中的5个分片分别执行查询并进行排序之后返回结果给Node3节点;

  4. Node3节点整合5个分片返回的结果,再次排序之后取到对应分页的结果集返回给客户端。



注:实际上ES的搜索分为Query阶段Fetch阶段两个步骤,在Query阶段各个分片返回文档Id和排序值,Fetch阶段根据文档Id去对应分片获取文档详情,上面的图片和文字说明对此进行了简化,请悉知。



现在考虑客户端获取990~1000的文档时,ES在分片存储的情况下如何给出正确的搜索结果。


获取990~1000的文档时,ES在每个分片下都需要获取1000个文档,然后由Coordinating Node聚合所有分片的结果,然后进行相关性排序,最后选出相关性顺序在990~100010条文档。


深度分页


页数越深,每个节点处理的文档也就越多,占用的内存也就越多,耗时也就越长,这也就是为什么搜索引擎厂商通常不提供深度分页的原因了,他们没必要在客户需求不强烈的功能上浪费性能。


<
作者:蝉沐风
来源:juejin.cn/post/7136009269903622151
hr/>

完。

收起阅读 »

关于浏览器缓存策略这件事儿

web
前言 我们打开百度这个网站并刷新多次时时,注意到百度的logo是没有每次都加载一遍的。我们知道图片是img标签中的src属性加载出来的,这也需要浏览器去请求图片资源的,那么为什么刷新多次浏览器只请求了一次图片资源呢?这就涉及到了浏览器的缓存策略了,这张图片被浏...
继续阅读 »

前言


我们打开百度这个网站并刷新多次时时,注意到百度的logo是没有每次都加载一遍的。我们知道图片是img标签中的src属性加载出来的,这也需要浏览器去请求图片资源的,那么为什么刷新多次浏览器只请求了一次图片资源呢?这就涉及到了浏览器的缓存策略了,这张图片被浏览器缓存下来了!


正文


一、为什么要有浏览器的缓存策略?



  • 提升用户体验,减少页面重复的http请求


二、为什么通过浏览器url地址栏访问的html页面不缓存?



  • 强制刷新页面浏览器url地址栏访问资源 时,浏览器默认会在请求头中设置Cache-control: no-cache,如设置该属性浏览器就会忽略响应头中的 Cache-control


如何优化网络资源请求的时间呢?有以下三种方式。


三、CDN网络分发



CDN:CDN会通过负载均衡技术,将用户的请求定向到最合适缓存服务器上去获取内容。



比如说,北京的用户,我们让他访问北京的节点,深圳的用户,我们让他访问深圳的节点。通过就近访问,加速用户对网站的访问,进而解决Internet网络拥堵状况,提高用户访问网络的响应速度。


四、强缓存



强缓存是浏览器的缓存策略,后端设置响应头中的属性值就能设置文件资源在浏览器的缓存时间过了缓存的有效期再次访问时,文件资源需再次加载



强缓存有两种方式来控制资源被浏览器缓存的时长:



  1. 后端设置响应头中的 Cache-control: max-age=3600 来控制缓存时长(为一个小时)

  2. 后端设置响应头中的 Expires:xxx 来控制缓存的截止日期(截止日期为xxx)


我们直接上代码让你更好理解,我们需要实现一个页面,页面上需展现一个标题一张图片


<!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>
<body>
<h1>Earth</h1>
<img src="assets/image/earth.jpeg" alt="">
</body>
</html>

const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('mime'); //接收一个文件后缀,返回一个文件类型

const server = http.createServer((req, res) => {
const filePath = path.resolve(__dirname, `www/${req.url}`) //resolve合并地址
if (fs.existsSync(filePath)) { //判断路径是否有效
const stats = fs.statSync(filePath) //获取文件信息
const isDir = stats.isDirectory() //是否是文件夹
if (isDir) {
filePath = path.join(filePath, 'index.html')
}
//读取文件
if (!isDir || fs.existsSync(filePath)) {

//判断前端请求的路径的后缀名是图片还是文本
const { ext } = path.parse(filePath) //.html .jpeg

const time = new Date(Date.now() + 3600000).toUTCString() //定义时间 作为缓存时间的有效期

let status = 200

res.writeHead(status, {
'Content-Type': `${mime.getType(ext)};charset=utf-8`,
'Cache-control': 'max-age=3600', //缓存时长为一小时
// 'expires': time //截止日期 缓存一小时后过期
})

if (status === 200) {
const fileStream = fs.createReadStream(filePath) //将文件读成流类型
fileStream.pipe(res) //将文件流导入响应体
}else{
res.end();
}

}
}
})

server.listen(3000, () => {
console.log('listening on port 3000');
})

第一次运行:
image.png


刷新页面后,可以看到图片资源没有重新加载:
image.png


三、协商缓存


我们想象这样的场景:当我们偷偷把图片偷偷换成另一张图片,图片名依然和之前那张一样,会是什么结果呢?

操作后,刷新页面发现图片还是之前那张图片,并没有换成新的!那这就出事儿了,后端图片换了,用户看到的还是老图片,有一种方案是改变图片资源的名字,直接请求最新图片资源,但这并不是最优方案,终极方案是需要协商缓存的帮忙。



协商缓存也是浏览器的缓存策略,它也有两种方式辅助强缓存,来判断文件资源是否被修改



1. 后端设置响应头中的 last-modified: xxxx



  • 辅助强缓存,让URL地址栏请求的资源也能被缓存

  • 辅助强缓存,借助请求头中的if-modified-since来判断资源文件是否被修改,如果被修改则返回新的资源,否则返回304状态码,让前端读取本地缓存


代码如下:


const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('mime'); //接收一个文件后缀,返回一个文件类型

const server = http.createServer((req, res) => {
const filePath = path.resolve(__dirname, `www/${req.url}`) //resolve合并地址
if (fs.existsSync(filePath)) { //判断路径是否有效
const stats = fs.statSync(filePath) //获取文件信息
const isDir = stats.isDirectory() //是否是文件夹
if (isDir) {
filePath = path.join(filePath, 'index.html')
}
//读取文件
if (!isDir || fs.existsSync(filePath)) {

//判断前端请求的路径的后缀名是图片还是文本
const { ext } = path.parse(filePath) //.html .jpeg

const time = new Date(Date.now() + 3600000).toUTCString() //定义时间 作为缓存时间的有效期

const timeStamp = req.headers['if-modified-since'] //请求头的if-modified-since字段
let status = 200

//判断文件是否修改过
if (timeStamp && Number(timeStamp) === stats.mtimeMs) { //timeStamp为字符串 转换为number类型判断
status = 304
}

res.writeHead(status, {
'Content-Type': `${mime.getType(ext)};charset=utf-8`,
'Cache-control': 'max-age=3600', //缓存时长为一小时 //max-age=0或no-cache不需要缓存
// 'expires': time //截止日期 缓存一小时后过期
'last-modified': stats.mtimeMs //文件最后一次修改时间
})

if (status === 200) {
const fileStream = fs.createReadStream(filePath) //将文件读成流类型
fileStream.pipe(res) //将文件流导入响应体
}else{
res.end();
}

}
}
})

server.listen(3000, () => {
console.log('listening on port 3000');
})

我们只要看last-modified这个字段的值有无变化即可:
image.png


2. Etag:文件的标签



  • 请求头中会被携带If--Match

  • Etag保证了每一个资源是唯一的,资源变化都会导致Etag变化。服务器根据If--Match值来判断是否命中缓存。 当服务器返回304的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。


image.png

代码如下:


const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('mime'); //接收一个文件后缀,返回一个文件类型
const md5 = require('crypto-js/md5');

const server = http.createServer((req, res) => {
const filePath = path.resolve(__dirname, `www/${req.url}`) //resolve合并地址
if (fs.existsSync(filePath)) { //判断路径是否有效
const stats = fs.statSync(filePath) //获取文件信息
const isDir = stats.isDirectory() //是否是文件夹
if (isDir) {
filePath = path.join(filePath, 'index.html')
}
//读取文件
if (!isDir || fs.existsSync(filePath)) {

//判断前端请求的路径的后缀名是图片还是文本
const { ext } = path.parse(filePath) //.html .jpeg
const content = fs.readFileSync(filePath);
let status = 200

//判断文件是否被修改过
if (req.headers['if-none-match'] == md5(content)) {
status=304
}

res.writeHead(status, {
'Content-Type': `${mime.getType(ext)};charset=utf-8`,
'Cache-control': 'max-age=3600', //缓存时长为一小时 //max-age=0或no-cache不需要缓存
'Etag': md5(content) //文件资源的md5值
})

if (status === 200) {
const fileStream = fs.createReadStream(filePath) //将文件读成流类型
fileStream.pipe(res) //将文件流导入响应体
} else {
res.end();
}

}
}
})

server.listen(3000, () => {
console.log('listening on port 3000');
})


最后附上一张图便于更好理解浏览器的缓存策略:


image.png

收起阅读 »

什么是圈复杂度?如何降低圈复杂度?

圈复杂度:理解和降低代码复杂性 在软件开发中,代码的复杂性是一个重要的考量因素。圈复杂度是一种用于衡量代码复杂性的指标,它可以帮助开发者评估代码的可读性、可维护性和可测试性。本文将详细介绍圈复杂度的概念,并提供几种降低圈复杂度的方法。同时,我们还将探讨如何在前...
继续阅读 »

圈复杂度:理解和降低代码复杂性


在软件开发中,代码的复杂性是一个重要的考量因素。圈复杂度是一种用于衡量代码复杂性的指标,它可以帮助开发者评估代码的可读性、可维护性和可测试性。本文将详细介绍圈复杂度的概念,并提供几种降低圈复杂度的方法。同时,我们还将探讨如何在前端开发中使用ESLint和VS Code工具来设置和检测圈复杂度。


什么是圈复杂度?


圈复杂度是由Thomas J. McCabe于1976年提出的一种软件度量指标,用于衡量程序中的控制流程复杂性。它通过计算代码中的判断语句和循环语句的数量来评估代码的复杂性。圈复杂度的值越高,代码的复杂性就越高,理解和维护代码的难度也就越大。


圈复杂度的计算方法是通过构建程序的控制流图,然后统计图中的节点数和边数来得出结果。每个判断语句(如if语句)和循环语句(如for循环)都会增加控制流图中的节点数和边数。圈复杂度的值等于图中边数减去节点数,再加上2。这个值表示了代码中独立路径的数量,即代码执行的可能路径数。


圈复杂度的计算方式可以通过以下步骤进行:




  1. 首先,将程序转换为控制流图(Control Flow Graph,CFG)。控制流图是一种图形表示方法,用于描述程序中的控制流程,包括各种条件和循环语句。




  2. 在控制流图中,每个节点表示程序中的一个基本块(Basic Block),即一组连续的语句序列,没有分支或跳转语句。




  3. 接下来,计算控制流图中的节点数量(N)和边数量(E)。节点数量即为程序中的基本块数量,边数量表示基本块之间的控制流转移关系。




  4. 根据以下公式计算圈复杂度(V):

    V = E - N + 2


    公式中的2表示程序的入口和出口节点,因为每个程序都至少有一个入口和一个出口。




为什么要降低圈复杂度?


高圈复杂度的代码往往难以理解和维护。当代码的复杂性增加时,开发者需要花费更多的时间和精力来理解代码的逻辑和执行路径。这不仅增加了开发和调试的难度,还可能导致代码中隐藏的逻辑错误。


圈复杂度代码状况可测性维护成本
1-10清晰、结构化
10-20复杂
20-30非常复杂
>30不可读不可测非常高

降低圈复杂度有助于提高代码的可读性和可维护性。简化代码结构可以使代码更易于理解,减少错误的引入,并提高代码的可测试性。此外,降低圈复杂度还有助于改善代码的性能,因为简单的代码通常执行更快。


如何降低圈复杂度?


以下是几种降低圈复杂度的常用方法:


1. 减少条件语句的嵌套


条件语句的嵌套是导致圈复杂度增加的常见原因之一。当条件语句嵌套层级过多时,代码的可读性和可维护性都会受到影响。为了降低圈复杂度,可以考虑使用早期返回(early return)的方式来减少条件语句的嵌套。通过在函数内部尽早返回结果,可以避免深层嵌套的条件判断。


function calculateGrade(score) {
if (score >= 90) {
return 'A';
}
if (score >= 80) {
return 'B';
}
if (score >= 70) {
return 'C';
}
return 'D';
}

2. 拆分复杂函数


函数的复杂性是导致圈复杂度升高的另一个常见原因。当一个函数包含过多的逻辑和操作时,它往往难以理解和维护。为了降低圈复杂度,可以将复杂的函数拆分成多个小函数,每个函数只负责一个特定的任务。这样可以提高代码的可读性和可维护性,并且使得每个函数的圈复杂度更低。


function calculateGrade(score) {
if (score >= 90) {
return 'A';
}
return calculateGradeForLowerScores(score);
}

function calculateGradeForLowerScores(score) {
if (score >= 80) {
return 'B';
}
if (score >= 70) {
return 'C';
}
return 'D';
}

3. 使用循环和迭代替代重复的代码块


重复的代码块会增加代码的复杂性和重复性。为了降低圈复杂度,可以使用循环和迭代来替代重复的代码块。通过将重复的逻辑抽象成一个函数,并在循环中调用该函数,可以减少代码的重复性和复杂性。


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

4. 使用适当的数据结构和算法


选择适当的数据结构和算法可以帮助降低代码的复杂性和提高性能。例如,使用哈希表可以减少查找操作的复杂度,使用排序算法可以提高搜索和比较的效率。通过选择合适的数据结构和算法,可以降低代码的圈复杂度并提高代码的执行效率。


使用ESLint检测圈复杂度


ESLint是一个流行的JavaScript代码检查工具,它可以帮助开发者发现和修复代码中的问题,包括圈复杂度。ESLint提供了许多规则和插件,可以配置和检测圈复杂度。


在ESLint中,可以使用complexity规则来设置圈复杂度的阈值。通过在配置文件中设置适当的阈值,可以在代码检查过程中发现圈复杂度过高的代码段,并及时进行优化和重构。


// .eslintrc.js
module.exports = {
rules: {
complexity: ['error', 15], // 设置圈复杂度阈值为15
},
};

使用VS Code工具检测圈复杂度


VS Code是一款流行的代码编辑器,它提供了许多插件和工具,可以帮助开发者提高代码质量和效率。在VS Code中,可以使用插件如ESLint、CodeMetrics等来检测圈复杂度。


安装ESLint插件后,可以在VS Code的设置中配置圈复杂度的阈值,并在编辑器中实时检测代码的圈复杂度。通过设置合适的阈值,可以在开发过程中及时发现和解决代码复杂性问题。


结论


圈复杂度是衡量代码复杂性的重要指标,通过降低圈复杂度可以提高代码的可读性、可维护性和可测试性。在前端开发中,使用ESLint和VS Code工具可以帮助我们设置和检测圈复杂度,并及时发现和解决代码中的复杂性问题。通过合理的代码设计和优化,我们可以编写出更简洁、高效和易于维护的代码。


希望本文对你理解圈复杂度以及降低

作者:jungang
来源:juejin.cn/post/7253291161397559353
代码复杂性有所帮助!

收起阅读 »

js十大手撕代码

web
前言 js中有很多API贼好用,省下了很多工夫,你知道它的原理吗?这篇文章对它们做一个总结。 正文 一、手撕instanceof instanceof的原理:通过判断对象的原型是否等于构造函数的原型来进行类型判断 代码实现: const myInstanc...
继续阅读 »

前言


js中有很多API贼好用,省下了很多工夫,你知道它的原理吗?这篇文章对它们做一个总结。


正文


一、手撕instanceof



  • instanceof的原理:通过判断对象的原型是否等于构造函数的原型来进行类型判断

  • 代码实现:


const myInstanceOf=(Left,Right)=>{
if(!Left){
return false
}
while(Left){
if(Left.__proto__===Right.prototype){
return true
}else{
Left=Left.__proto__
}
}
return false
}

//验证
console.log(myInstanceOf({},Array)); //false

二、手撕call,apply,bind


call,apply,bind是通过this的显示绑定修改函数的this指向


1. call


call的用法:a.call(b) -> 将a的this指向b

我们需要借助隐式绑定规则来实现call,具体实现步骤如下:

往要绑定的那个对象(b)上挂一个属性,值为需要被调用的那个函数名(a),在外层去调用函数。


function foo(x,y){
console.log(this.a,x+y);
}

const obj={
a:1
}

Function.prototype.myCall=function(context,...args){
if(typeof this !== 'function') return new TypeError('is not a function')
const fn=Symbol('fn') //使用Symbol尽可能降低myCall对其他的影响
context[fn]=this //this指向foo
const res=context[fn](...args) //解构,调用fn
delete context[fn] //不要忘了删除obj上的工具函数fn
return res //将结果返回
}

//验证
foo.myCall(obj,1,2) //1,3

2. apply


apply和call的本质区别就是接受的参数形式不同,call接收零散的参数,而apply以数组的方式接收参数,实现思路完全一样,代码如下:


function foo(x,y){
console.log(this.a,x+y);
}

const obj={
a:1
}

Function.prototype.myApply=function(context,args){
if(typeof this !== 'function') return new TypeError('is not a function')
const fn=Symbol('fn') //尽可能降低myCall对其他的影响
context[fn]=this
context[fn](...args)
delete context[fn]
}

//验证
foo.myApply(obj,[1,2]) //1,3

3. bind


bind和call,apply的区别是会返回一个新的函数,接收零散的参数

需要注意的是,官方bind的操作是这样的:



  • 当new了bind返回的函数时,相当于new了foo,且new的参数需作为实参传给foo

  • foo的this.a访问不到obj中的a


function foo(x,y,z){
this.name='zt'
console.log(this.a,x+y+z);
}

const obj={
a:1
}


Function.prototype.myBind=function(context,...args){

if(typeof this !== 'function') return new TypeError('is not a function')

context=context||window

let _this=this

return function F(...arg){
//判断返回出去的F有没有被new,有就要把foo给到new出来的对象
if(this instanceof F){
return new _this(...args,...arg) //new一个foo
}
_this.apply(context,args.concat(arg)) //this是F的,_this是foo的 把foo的this指向obj用apply
}
}

//验证
const bar=foo.myBind(obj,1,2)
console.log(new bar(3)); //undefined 6 foo { name: 'zt' }


三、手撕深拷贝


这篇文章中详细记录了实现过程
【js手写】浅拷贝与深拷贝


四、手撕Promise


思路:



  • 我们知道,promise是有三种状态的,分别是pending(异步操作正在进行), fulfilled(异步操作成功完成), rejected(异步操作失败)。我们可以定义一个变量保存promise的状态。

  • resolve和reject的实现:把状态变更,并把resolve或reject中的值保存起来留给.then使用

  • 要保证实例对象能访问.then,必须将.then挂在构造函数的原型上

  • .then接收两个函数作为参数,我们必须对所传参数进行判断是否为函数,当状态为fulfilled时,onFulfilled函数触发,并将前面resolve中的值传给onFulfilled函数;状态为rejected时同理。

  • 当在promise里放一个异步函数(例:setTimeout)包裹resolve或reject函数时,它会被挂起,那么当执行到.then时,promise的状态仍然是pending,故不能触发.then中的回调函数。我们可以定义两个数组分别存放.then中的两个回调函数,将其分别在resolve和reject函数中调用,这样保证了在resolve和reject函数触发时,.then中的回调函数即能触发。


代码如下:


const PENDING = 'pending'
const FULFILLED = 'fullfilled'
const REJECTED = 'rejected'

function myPromise(fn) {
this.state = PENDING
this.value = null
const that = this
that.resolvedCallbacks = []
that.rejectedCallbacks = []

function resolve(val) {
if (that.state == PENDING) {
that.state = FULFILLED
that.value = val
that.resolvedCallbacks.map((cb)=>{
cb(that.value)
})
}
}
function reject(val) {
if (that.state == PENDING) {
that.state = REJECTED
that.value = val
that.rejectedCallbacks.map((cb)=>{
cb(that.value)
})
}
}

try {
fn(resolve, reject)
} catch (error) {
reject(error)
}

}

myPromise.prototype.then = function (onFullfilled, onRejected) {
const that = this
onFullfilled = typeof onFullfilled === 'function' ? onFullfilled : v => v
onRejected= typeof onRejected === 'function' ? onRejected : r => { throw r }

if(that.state===PENDING){
that.resolvedCallbacks.push(onFullfilled)
that.resolvedCallbacks.push(onRejected)
}
if (that.state === FULFILLED) {
onFullfilled(that.value)
}
if (that.state === REJECTED) {
onRejected(that.value)
}
}

//验证 ok ok
let p = new myPromise((resolve, reject) => {
// reject('fail')
resolve('ok')
})

p.then((res) => {
console.log(res,'ok');
}, (err) => {
console.log(err,'fail');
})

五、手撕防抖,节流


这篇文章中详细记录了实现过程
面试官:什么是防抖和节流?如何实现?应用场景?


六、手撕数组API


1. forEach()


思路:



  • forEach()用于数组的遍历,参数接收一个回调函数,回调函数中接收三个参数,分别代表每一项的值、下标、数组本身。

  • 要保证数组能访问到我们自己手写的API,必须将其挂到数组的原型上


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

//代码实现
Array.prototype.my_forEach = function (callback) {
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this)
}
}

//验证
arr.my_forEach((item, index, arr) => { //111 111
if (item.age === 18) {
item.age = 17
return
}
console.log('111');
})


2. map()


思路:



  • map()也用于数组的遍历,与forEach不同的是,它会返回一个新数组,这个新数组是map接收的回调函数返回值

    代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_map=function(callback){
const res=[]
for(let i=0;i<this.length;i++){
res.push(callback(this[i],i,this))
}
return res
}

//验证
let newarr=arr.my_map((item,index,arr)=>{
if(item.age>18){
return item
}
})
console.log(newarr);
//[
// undefined,
// { name: 'aa', age: 19 },
// undefined,
// { name: 'cc', age: 21 }
//]

3. filter()


思路:



  • filter()用于筛选过滤满足条件的元素,并返回一个新数组


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_filter = function (callback) {
const res = []
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this) && res.push(this[i])
}
return res
}

//验证
let newarr = arr.my_filter((item, index, arr) => {
return item.age > 18
})
console.log(newarr); [ { name: 'aa', age: 19 }, { name: 'cc', age: 21 } ]

4. reduce()


思路:



  • reduce()用于将数组中所有元素按指定的规则进行归并计算,返回一个最终值

  • reduce()接收两个参数:回调函数、初始值(可选)。

  • 回调函数中接收四个参数:初始值 或 存储上一次回调函数的返回值、每一项的值、下标、数组本身。

  • 若不提供初始值,则从第二项开始,并将第一个值作为第一次执行的返回值


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_reduce = function (callback,...arg) {
let pre,start=0
if(arg.length){
pre=arg[0]
}
else{
pre=this[0]
start=1
}
for (let i = start; i < this.length; i++) {
pre=callback(pre,this[i], i, this)
}
return pre
}

//验证
const sum = arr.my_reduce((pre, current, index, arr) => {
return pre+=current.age
},0)
console.log(sum); //76


5. fill()


思路:



  • fill()用于填充一个数组的所有元素,它会影响原数组 ,返回值为修改后原数组

  • fill()接收三个参数:填充的值、起始位置(默认为0)、结束位置(默认为this.length-1)。

  • 填充遵循左闭右开的原则

  • 不提供起始位置和结束位置时,默认填充整个数组


代码实现:


Array.prototype.my_fill = function (value,start,end) {
if(!start&&start!==0){
start=0
}
end=end||this.length
for(let i=start;i<end;i++){
this[i]=value
}
return this
}

//验证
const arr=new Array(7).my_fill('hh',null,3) //往数组的某个位置开始填充到哪个位置,左闭右开
console.log(arr); //[ 'hh', 'hh', 'hh', <4 empty items> ]


6. includes()


思路:



  • includes()用于判断数组中是否包含某个元素,返回值为 true 或 false

  • includes()提供第二个参数,支持从指定位置开始查找


代码实现:


const arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_includes = function (item,start) {
if(start<0){start+=this.length}
for (let i = start; i < this.length; i++) {
if(this[i]===item){
return true
}
}
return false
}

//验证
const flag = arr.my_includes('c',3) //查找的元素,从哪个下标开始查找
console.log(flag); //false


7. join()


思路:



  • join()用于将数组中的所有元素指定符号连接成一个字符串


代码实现:


const arr = ['a', 'b', 'c']

Array.prototype.my_join = function (s = ',') {
let str = ''
for (let i = 0; i < this.length; i++) {
str += `${this[i]}${s}`
}
return str.slice(0, str.length - 1)
}

//验证
const str = arr.my_join(' ')
console.log(str); //a b c

8. find()


思路:



  • find()用于返回数组中第一个满足条件元素,找不到返回undefined

  • find()的参数为一个回调函数


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_find = function (callback) {
for (let i = 0; i < this.length; i++) {
if(callback(this[i], i, this)){
return this[i]
}

}
return undefined
}

//验证
let j = arr.my_find((item, index, arr) => {
return item.age > 19
})
console.log(j); //{ name: 'cc', age: 21 }

9. findIndex()


思路:



  • findIndex()用于返回数组中第一个满足条件索引,找不到返回-1

  • findIndex()的参数为一个回调函数


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_findIndex = function (callback) {
for (let i = 0; i < this.length; i++) {
if(callback(this[i], i, this)){
return i
}
}
return -1
}


let j = arr.my_findIndex((item, index, arr) => {
return item.age > 19
})
console.log(j); //3

10. some()


思路:



  • some()用来检测数组中的元素是否满足指定条件。

  • 有一个元素符合条件,则返回true,且后面的元素会再检测。


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_some = function (callback) {
for (let i = 0; i < this.length; i++) {
if(callback(this[i], i, this)){
return true
}
}
return false
}

//验证
const flag = arr.some((item, index, arr) => {
return item.age > 20
})
console.log(flag); //true

11. every()


思路:



  • every() 用来检测所有元素是否都符合指定条件。

  • 有一个不满足条件,则返回false,后面的元素都会再执行。


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_every = function (callback) {
for (let i = 0; i < this.length; i++) {
if(!callback(this[i], i, this)){
return false
}
}
return true
}

//验证
const flag = arr.my_every((item, index, arr) => {
return item.age > 16
})
console.log(flag); //true


七、数组去重


1. 双层for循环 + splice()


let arr = [1, 1, '1', '1', 2, 2, 2, 3, 2]
function unique(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1)
j-- //删除后j向前走了一位,下标需要减一,避免少遍历一位
}
}
}
return arr
}

console.log(unique(arr)) //[ 1, '1', 2, 3 ]

2. 排序后做前后比较


let arr = [1, 1, '1', '1', 2, 2, 2, 3, 2]

function unique(arr) {
let res = []
let seen //记录上一次比较的值
let newarr=[...arr] //解构出来,开辟一个新数组
newarr.sort((a,b)=>a-b) //sort会影响原数组 n*logn
for (let i = 0; i < newarr.length; i++) {
if (newarr[i]!==seen) {
res.push(newarr[i])
}
seen=newarr[i]
}
return res
}

console.log(unique(arr)) //[ 1, '1', 2, 3 ]

3. 借助include


let arr = [1, 1, '1', '1', 2, 2, 2, 3, 2]

function unique(arr) {
let res = []
for (let i = 0; i < arr.length; i++) {
if(!res.includes(arr[i])){
res.push(arr[i])
}
}
return res
}

console.log(unique(arr)) //[ 1, '1', 2, 3 ]

4. 借助set


let arr = [1, 1, '1', '1', 2, 2, 2, 3, 2]
const res1 = Array.from(new Set(arr));
console.log(res1); //[ 1, '1', 2, 3 ]

八、数组扁平化


1. 递归


let arr1 = [1, 2, [3, 4, [5],6]]

function flatter(arr) {
let len = arr.length
let result = []
for (let i = 0; i < len; i++) { //遍历数组每一项
if (Array.isArray(arr[i])) { //判断子项是否为数组并拼接起来
result=result.concat(flatter(arr[i]))//是则使用递归继续扁平化
}
else {
result.push(arr[i]) //不是则存入result
}
}
return result
}

console.log(flatter(arr1)) //[ 1, 2, 3, 4, 5, 6 ]

2. 借助reduce (本质也是递归)


let arr1 = [1, 2, [3, 4, [5],6]]

const flatter = arr => {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, [])
}
console.log(flatter(arr1)) //[ 1, 2, 3, 4, 5, 6 ]

3. 借助正则


let arr1 = [1, 2, [3, 4, [5],6]]

const res = JSON.parse('[' + JSON.stringify(arr1).replace(/\[|\]/g, '') + ']');
console.log(res) //[ 1, 2, 3, 4, 5, 6 ]

九、函数柯里化


思路:



  • 函数柯里化是只传递给函数一部分参数调用它,让它返回一个函数去处理剩下的参数

  • 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数,小于则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数


代码实现:


const my_curry = (fn, ...args) => 
args.length >= fn.length
? fn(...args)
: (...args1) => curry(fn, ...args, ...args1);

function adder(x, y, z) {
return x + y + z;
}
const add = my_curry(adder);
console.log(add(1, 2, 3)); //6
console.log(add(1)(2)(3)); //6
console.log(add(1, 2)(3)); //6
console.log(add(1)(2, 3)); //6

十、new方法


思路:



  • new方法主要分为四步:

    (1) 创建一个新对象

    (2) 将构造函数中的this指向该对象

    (3) 执行构造函数中的代码(为这个新对象添加属性

    (4) 返回新对象


function _new(obj, ...rest){
// 基于obj的原型创建一个新的对象
const newObj = Object.create(obj.prototype);

// 添加属性到新创建的newObj上, 并获取obj函数执行的结果.
const result = obj.apply(newObj, rest);

// 如果执行结果有返回值并且是一个对象, 返回执行的结果, 否则, 返回新创建的对象
return typeof result === 'object' ? result : newObj;
}



总结不易,

作者:zt_ever
来源:juejin.cn/post/7253260410664419389
动动手指给个赞吧!💗

收起阅读 »

抛开场景,一味的吹捧路由懒加载就像在耍流氓🤣

异步路由长啥样? 原理不做过多介绍(大家都知道),直接上代码了 一般来说,只有SPA需要异步路由。当配置了异步路由,通过split chunk,打包后都会生成单独的chunk,以webpack为例,建议添加魔法注释,并开启prefetch,以进一步提升体验im...
继续阅读 »

异步路由长啥样?


原理不做过多介绍(大家都知道),直接上代码了


一般来说,只有SPA需要异步路由。当配置了异步路由,通过split chunk,打包后都会生成单独的chunk,以webpack为例,建议添加魔法注释,并开启prefetch,以进一步提升体验

import loadable from '@loadable/component';

const XXXA = loadable(() => import('@/views/xx/XXXA'/* webpackChunkName: 'XXXA', webpackPrefetch: true */));

const XXXB = loadable(() => import('@/views/xx/XXXB'/* webpackChunkName: 'XXXB', webpackPrefetch: true */));

// ...

const allRoutes: IRoute[] = [
{
path: '/xxx-a',
component: XXXA,
},
{
path: '/xxx-b',
component: XXXB,
},
// ...
];

// ...

SPA应用,白屏现象能不能解决??


首先我们要搞明白的是,异步路由给我们解决的痛点是啥?


balabala....


Yes, 你打的很对👍🏾,就是为了缩减🐷包的大小,减少资源加载的时间。


但是,结合页面的加载过程,我先给您下个结论:


“SPA无论怎么优化,都无法避免白屏的现象产生,哪怕网速快,一闪而过(实际上你调成3g或带宽更低的网络,白屏一直伴随着你🤮)”


为啥🤬?



  • SPA的入口是index.html, 初始dom,只有div#app,并且一般都没有任何样式

  • 页面加载需要先加载必要js,比如main.[hash].js

  • main.[hash].js加载并解析成功,页面才会正常展示,so,空档期一定存在,这就是白屏现象的原因所在(注意:此时和是否有其他路由没有任何关系


为什么说异步路由不是100%保险??



问题的根本在于这句话:异步路由能提升首屏用户体验 👀



但是,很多人不知道,这里的首屏并不是单指index.html,举个例子:


对于移动端混合应用开发,首屏可能变成SPA中的任何一个路由路径。原因也很简单,APP首页中有很多菜单:每个菜单都可以配置路径,这些路径很可能来自同一个SPA。 我们暂且把这些路径叫做一级路由(不包括index.html)


对于以上场景,首屏不再单一。


开始划重点



  • 上述一级路由(不包括index.html)请不要配置成异步加载,使用普通的import即可, 让它打进主包中。

  • 对于其他非一级路由,只是通过push、replace进行跳转,那么配置为异步路由就很合适


实际案例分析


还是先下个结论:


比如有个路由/xx-a,它作为一级路由配置在了APP菜单中,那么它就变成了"所谓的首屏页面",如果我们还是使用异步路由,就会延长白屏的时间

import loadable from '@loadable/component';

const XxxA = loadable(() => import('@/views/xx/XxxA'/* webpackChunkName: 'XxxA', webpackPrefetch: true */));

// ...

const allRoutes: IRoute[] = [
{
path: '/xxx-a',
component: XXXA,
},
// ...
];

// ...

此时,build目录中存在



  • main.[hash].js (包含react路由逻辑

  • XxxA.[hash].js


此时,打卡devtools,就会知道,network中资源加载顺序如下



  1. index.html

  2. main.js

  3. 如果index.html中引入了其他资源, 比如jquery, lodash...,也会优先download这些资源(哪怕你配置的defer或者async)

  4. 当main加载并执行后,才会触发路由逻辑,并开始加载XxxA.[hash].js

  5. 加载XxxA.[hash].js成功,开始解析执行XxxA,XxxA页面才会被正常渲染


以上过程中,白屏的开始是main的加载和执行(包含了路由逻辑)的时间消耗产生的,而XxxA.[hash].js的加载解析和执行,又无疑增加了空档期,这样白屏时间也就被延长了


若index.html中引入了若干其他script资源,并且处于http1.1的服务器环境中,这个现象就会变得特别明显



  • 因为1.1多路复用对于资源的请求数量有限制,chrome下6个作为一组


我们可以借助network和performance进行实际演示说明:


为了演示,我们将网络调慢些(实际上客户的网络环境也有这样的情况


image.png


network



后续每组都需等待前一组加载完成,才开始加载



image.png


只有main加载成功并执行,才会触发路由逻辑,从而开始加载XxxA脚本。如果网络慢,main.js加载过程就会更长,从而间接导致XxxA脚本的加载和解析执行被推迟, 这无疑也就延长了白屏时间!!🦮


performance截图 (异步路由)


image.png


performance截图 (非异步路由)



此时代码逻辑被打进了main.js中,直接第一波解析执行即可,很明显缩短了白屏时间。



image.png


对比一下,就可以一目了然,哎,什么也不说了~


总结


异步路由并不能100%缩短白屏时间,最关键的是我们要知道“首屏”这个词的意义,它并不是单指index.html入口,它可以是SPA中的任何一个路由(结合混合移动开发场景就知道了)


So:



  • 如果SPA中的“首屏”只有一个,不存在移动混合开发场景或者路径分享场景(比如分享微信,支付宝等),那所有路由都可以进行异步加载

  • 如果存在移动混合开发场景或者路径分享场景,那对应的路由请不要异步加载,使用import即可。


好了,到此结束,如果对您有帮助,还望点个小💖💖


作者:jerrywus
链接:https://juejin.cn/post/7216213764777328697
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

学前端必读的从输入url到页面渲染全过程

从输入 URL 到页面展示这中间到底发生了什么?,这是一道非常经典的面试题,这一过程涉及到了计算机网络、操作系统、Web等一系列知识,如果对这一过程有非常好的了解,对以后的开发甚至是程序的优化都是非常有益的。现将过程梳理如下: 1. 解析URL 分析所需要使用...
继续阅读 »

从输入 URL 到页面展示这中间到底发生了什么?,这是一道非常经典的面试题,这一过程涉及到了计算机网络、操作系统、Web等一系列知识,如果对这一过程有非常好的了解,对以后的开发甚至是程序的优化都是非常有益的。现将过程梳理如下:


1. 解析URL


分析所需要使用的传输协议和请求的资源路径。如果url中的协议或主机名不合法,将会把地址栏中输入的内容传递给搜索引擎,如果没有问题,浏览器会检查url中是否出现了非法字符,如果存在,对非法字符(空格、汉字等双字节字符)进行转义后在进行下一过程。


编码和解码:



  • encodeURI()/decodeURI():encodeURI()函数只会把参数中的空格编码为%20,汉字进行编码,其余特殊字符不会转换

  • encodeURIComponent()/decodeURIComponent():由于这个方法对:/都进行了编码,所以不能用它来对网址进行编码,适合对URL中的参数进行编码

  • escape()/unescape():将字符的unicode编码转化为16进制序列,不对ASCII字母和数字进行编码,也不会对' * @ - _ + . / '这些ASCII符号进行编码,用于服务器与服务器端传输多
let url = "http://www.baidu.com/test /中国"
console.log(encodeURI(url)); // http://www.baidu.com/test%20/%E4%B8%AD%E5%9B%BD
let url1 = `https://www.baidu.com/from=${encodeURIComponent('http://wwws.udiab.com')}`
console.log(url1); // https://www.baidu.com/from=http%3A%2F%2Fwwws.udiab.com
console.log(escape(url)); // http%3A//www.baidu.com/test%20/%u4E2D%u56FD

1.1 URL地址格式


传统格式:scheme://host:port/path?query#fragment。例:http://www.urltest.cn/system/user…



  • scheme(必写):协议http(超文本传输协议)、https(安全超文本传输协议)、ftp(文件传输协议,用于将文件下载或上传至网站)、file(计算机上的文件)

  • host(必写):域名或IP地址

  • port(可省略):端口号,http默认80,https默认443

  • path:路径,例如 /system/user

  • query:参数,例如 username=falcon&age=18

  • fragment:锚点(哈希hash),用于定位页面的某个位置


Restful格式:可以通过不同的请求方式(get、post、put、delete)来实现不同的效果



2. 缓存判断


浏览器缓存就是浏览器将用户请求过的资源存储到本地电脑。当浏览器再次访问时就可以直接从本地加载,不需要去服务器请求,能有效减少不必要的数据传输,减轻服务器负担,提升网站性能,提高客户端网页打开速度。浏览器缓存一般分为强缓存和协商缓存


image.png



  • 浏览器在请求某一资源时,会先获取该资源缓存的header信息,判断是否命中强缓存(cache-control、expires信息),命中则直接从缓存中获取资源信息,不会向服务器发起请求;

  • 如果没有命中强缓存,浏览器会发送请求(携带该资源缓存的第一次请求返回的header字段信息,Last-Modified/If-Modified-Since、Etag/If-None-Match)到服务器,由服务器根据请求中携带的相关header字段进行对比来判断是否命中协商缓存,命中则返回新的header信息更新缓存中对应的header信息,不返回资源内容,浏览器直接从缓存中获取;否则返回最新的资源内容


2.1 强缓存



  • expires,绝对时间字符串,发送请求在这个时间之前有效,之后无效


catch-control,几个比较常用的字段如下:



  • max-age=number,相对字段,利用资源第一次请求时间和Cache-Control设定的有效期,计算出一个资源过期时间,然后再进行比较

  • no-cache,不使用本地缓存,需要使用协商缓存

  • no-store,禁止浏览器缓存数据,每次用户都需要向服务器发送请求获取完整资源

  • public,可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器

  • private,只能被终端用户浏览器缓存


【注】



  • 强缓存如何重新加载缓存过的资源?使用强缓存,不用向服务器发送请求就可以获取到资源,如果在强缓存期间,资源发生了变化,浏览器就一直得不到最新的资源,如何操作:通过更新页面中引用的资源路径,让浏览器主动放弃缓存,加载新的资源

  • 如果二者同时存在,cache-control的优先级高于expires


2.2 协商缓存


Last-Modified/If-Modified-Since



  • 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,会在response的header上加上Last-Modified(表示资源在服务器上的最后修改时间)字段

  • 浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since(值就是上一次请求返回的Last-Modified值)字段,来判断是否发生变化,没变化返回304,从缓存中加载,也不会重新在response的header上添加Last-Modified;发生变化则直接从服务器加载,并且更新Last-Modified值


Etag/If-None-Match



  • 这两个值是由服务器生成的每个资源的唯一标识字符串,只要资源变化值就会发生改变

  • 判断过程与上面一组逻辑类似

  • 不同的是,当服务器返回304时,由于Etag重新生成过,response的header还是会把这个Etag返回


【注】



  • 为什么需要Etag?一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变修改时间);某些文件修改非常频繁(例如在秒以下的时间进行修改);某些服务器不能精确得到文件的最后修改时间等这些情况下,利用Etag能够更加精确的控制缓存

  • 两者可以一起使用,服务器会优先验证Etag,在一致的情况下,才会继续比对Last-Modified


2.3 用户行为对缓存的影响


image.png


3. DNS解析


获取输入url中的域名对应的IP地址。



  • 第一步,检查浏览器缓存中是否缓存过该域名对应的IP地址;

  • 第二步,检查本地的hosts文件(系统缓存)

  • 第三步,本地域名解析服务器进行解析;

  • 第四步,根域名解析服务器进行解析;

  • 第五步,gTLD服务器进行解析(顶级域名)

  • 第六步,权威域名服务器进行解析,最终获得域名IP地址;


3.1 域名层级结构图


image.png



  • 根域:位于域名空间最顶层,一般用一个点“.”表示

  • 顶级域:一般表示一种类型的组织机构或者国家地区。.net(网络供应商) .com(工商企业) .org(团体组织) .edu(教育机构) .gov(政府部门) .cn(中国国家域名)

  • 二级域:用来标明顶级域内一个特定的组织。.com.cn .net.cn .edu.cn

  • 子域:二级域下所创建的各级域名,各个组织或用户可以自由申请注册

  • 主机:位于域名空间最下层,一台具体的计算机。完整格式域:http://www.sina.com.cn


3.2 递归查询、迭代查询


用户向本地DNS服务器发起请求属于递归请求;本地DNS服务器向各级域名服务器发起请求属于迭代请求
image.png



  • 递归查询:以本地DNS服务器为中心,客户端发出请求报文后就一直处于等待状态,直到本地DNS服务器发来最终查询结果

  • 迭代查询:DNS服务器如有客户端请求数据则返回正确地址;没有则返回一个指针;按指针继续查询


4. TCP三次握手建立连接


image.png



  • 第一步,客户端发送SYN包(seq=x)到服务器,等待服务器确认

  • 第二步,服务器收到SYN包,确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(seq=y),即SYN+ACK包

  • 第三步,客户端收到SYN+ACK包,向服务器发送确认包ACK(ack=y+1)


三次握手完成,客户端和服务器正式开始传递数据


4.1 TCP、UDP



  • TCP 面向连接的协议,只有建立后才可以传递数据

  • UDP 无连接的协议,可以直接传送数据,传输效率较高,但不能保证数据的完整性


5. 发起http、https请求


5.1 http1.0、http1.1、http2.0区别



5.2 http、https



  • https需要CA申请证书,一般需要交费

  • http运行在TCP之上,明文传输;https运行在SSL/TLS之上,SSL/TLS运行在TCP之上,加密传输

  • http默认端口80,https默认端口443

  • https可以有效的防止运营商劫持


5.3 XSS、CSRF


XSS


XSS(Cross-site Scripting)。跨域脚本攻击,指通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序等。


分类如下:


image.png



  • 反射型。发出请求时,xss代码出现在URL中,作为输入提交到服务器端,服务器端解析后响应,xss代码随响应内容一起传回浏览器,最后浏览器解析执行xss代码。例如:"http://www.a.com/xss/reflect…"
    image.png

  • 存储型。和反射型的差别是提交的代码会存储在服务器端(数据库、内存、文件系统等),下次请求目标页面时不用再提交XSS代码。例如:留言板xss,用户提交留言到数据库,目标用户查看留言板时,留言的内容会从数据库查询出来并显示,浏览器解析执行,触发xss攻击

  • DOM型。不需要服务器的参与,触发xss靠的是浏览器端的DOM解析,完全是客户端触发


防御措施:



  • 过滤。对用户的输入(和URL参数)进行过滤。移除用户输入的和事件相关的属性,如onerror、onclick等;移除用户输入的Style节点、Script节点(一定要特别注意,它是支持跨域的)、Iframe节点

  • 编码。对输出进行html编码。对动态输出到页面的内容进行html编码,使脚本无法再浏览器中执行

  • 服务端设置会话Cookie的HTTP Only属性,这样客户端的JS脚本就不能获取cookie信息了


CSRF


CSRF(Cross-site request forgery)。跨站请求伪造,攻击者通过伪造用户的浏览器请求,向用户曾经认证访问过的网站发送出去,使目标网站接收并误以为是用户的真实操作而去执行命令。常用于转账、盗号、发送虚假消息等
image.png


防御措施:



  • token验证。服务器返回给客户端一个token信息,客户端带着token发送请求,如果token不合法,服务器拒绝这个请求

  • 隐藏令牌。将token隐藏在http的header中

  • referer验证。页面请求来源验证,只接受本站的请求,其他进行拦截


5.4 get、post


GET



  • 一般用于获取数据;

  • 参数放在url中;

  • 浏览器回退或刷新无影响;

  • 请求可被缓存;

  • 请求的参数放url上,有长度限制;

  • 请求的参数只能是ASCII码;

  • url对所有人可见,安全性差;

  • get产生一个tcp数据包,hearder、data一起发送一次请求,服务器返回200


POST



  • 一般用于向后台传递数据、创建数据;

  • 参数放在body里;

  • 浏览器回退或刷新数据会被需重提交;

  • 请求不会被缓存;

  • 请求的参数放body上,无长度限制;

  • 请求的参数类型无限制,允许二进制数据;

  • 请求参数不会被保存在浏览器历史或web服务器日志中,相对更安全;

  • post产生两个tcp数据包,先发送header,返回100 continue,再发送data,服务器返回200,但不是绝对的,Firefox只发送一次


5.5 状态码


1XX - 通知



  • 100 -- 客户端继续发送请求

  • 101 -- 切换协议


2XX - 成功



  • 200 -- 请求成功,一般应用与 get 或 post 请求

  • 201 -- 请求成功并创建新的资源,


3XX - 重定向



  • 301 -- 永久移动,请求的资源已被永久移动到新的 url,返回信息包括新的 url,浏览器会自动定向到新的 url,今后所有的请求都是新的 url

  • 302 -- 临时移动,资源只是临时移动,客户端应继续使用旧的 url

  • 304 -- 所请求的资源未修改,不会返回任何资源。浏览器请求的时候,会先访问强缓存,没有则访问协商缓存,协商缓存命中,资源未修改,返回 304


4XX - 客户端错误



  • 400 -- 客户端请求的语法错误,服务器无法理解(z 字段类型,或对应的值类型不一致;或者没有进行 JSON.toStringfy 的转换)

  • 401 -- 请求需要用户的认证

  • 403 -- 服务器理解客户端请求,但是拒绝执行

  • 404 -- 服务器无法根据客户端的请求找到资源


5XX - 服务器端错误



  • 500 -- 服务器内部错误,无法完成资源的请求

  • 501 -- 服务器不支持请求功能,无法完成资源请求

  • 502 -- 网关或代理服务器向远程服务器发送请求返回无效


5.6 跨域



  • 跨域:浏览器不能执行其他网站的脚本,这是由于同源策略(同协议、同域名、同端口)限制造成的

  • 同源策略限制的行为:cookie,localstorage和IndexDB无法读取;DOM无法获取;Ajax请求不能发送


跨域的几种解决方式:



  • jsonp,实现原理是<script>标签的src可以发跨域请求,不受同源策略限制,缺点是只能实现get一种请求

  • document.domain + iframe跨域domain属性可返回下载当前文档的服务器域名,此方案仅限主域相同,子域不同的跨域场景

  • 跨域资源共享(CORS)只服务端设置Access-Control-Allow-Origin即可,前端无需设置;若要带cookie请求,前后端都需要设置

  • nginx反向代理跨域,项目中常用的一种方案

  • html5的postMessage(跨文档消息传输),WebSocket(全双工通信、实时通信)


6. 返回数据


当页面请求发送到服务器端后,服务器端会返回一个html文件作为响应


7. 页面渲染


7.1 加载过程



  • HTML会被渲染成DOM树。HTML是最先通过网址请求过来的,请求过来之后,HTML本身会由一个字节流转化成一个字符流,浏览器端拿到字符流,之后通过词法分析,将相应的词法分析成相应的token,转化不同的token tag,然后通过token类型append到DOM树

  • 遇到link token tag,去请求css,然后对css进行解析,生成CSSOM树

  • DOM树和CSSOM树结合形成Render Tree,再进行布局和渲染

  • 遇到script tag,然后去请求JS相关的web资源,请求回来的js交给浏览器的v8引擎进行解析
    image.png


7.2 加载特点



  • html文档解析,对tag依次从上到下解析,顺序执行

  • html中可能会引入很多css,js的web资源,这些资源在浏览器中是并发加载的。

  • DOM树和CSSOM树通常是并行构建的, 所以CSS加载不会阻塞DOM的解析;Render树依赖DOM树和CSSOM树进行,所以CSS加载会阻塞DOM的渲染css会阻塞js文件执行,但不会阻塞js文件下载,因为GUI渲染线程与JavaScript线程互斥,JS有可能影响样式;js会阻塞DOM的解析(把js文件放在最下面),也就会阻塞DOM的渲染,同时js顺序执行,也会阻塞后续js逻辑的执行

  • 依赖关系。页面渲染依赖于css的加载;js的执行顺序依赖关系;js逻辑对于dom节点的依赖关系,有些js需要去获取dom节点

  • 引入方式。直接引入,不会阻塞页面渲染;defer不会阻塞页面渲染,顺序执行;async不会阻塞页面渲染,先到先执行,不保证顺序;异步动态js,需要的时候引入


【注】css样式置顶;js脚本置底;用link代替import;合理使用js异步加载


资源加载完成后,通过样式计算、布局设置、分层、绘制等过程,将页面呈现出来


7.3 重绘回流


根据渲染树,浏览器可以计算出网页中有哪些节点,各节点的CSS以及从属关系,发生回流;根据渲染树以及回流得到的节点信息,计算出每个节点在屏幕中的位置,发生重绘



  • 元素的规模尺寸、布局、显隐性发生变化时,发生回流,每个页面至少产生一次回流,第一次加载

  • 元素的外观、风格、颜色等发生变化而不影响布局,发生重绘

  • 回流一定发生重绘,重绘不一定发生回流


【避免措施】


样式设置



  • 避免使用层级较深的选择器

  • 避免使用 css 表达式

  • 元素适当的定义高度或最小高度

  • 给图片设置尺寸

  • 不要使用 table 布局

  • 能 css 实现的,尽量不要使用 js 实现


渲染层



  • 将需要多次重绘的元素独立为 render layer,如设置 absolute,可以减少重绘范围

  • 对于一些动画元素,使用硬件渲染


DOM 优化



  • 缓存 DOM

  • 减少 DOM 深度及 DOM 数量

  • 批量操作 DOM

  • 批量操作 CSS 样式

  • 在内存中操作 DOM

  • DOM 元素离线更新

  • DOM 读写分离

  • 事件代理

  • 防抖和节流

  • 及时清理环境


TCP四次挥手断开连接


image.png



  • 客户端发送一个FIN(seq=u)数据包到服务器,用来关闭客户端到服务器的数据连接

  • 服务器接受FIN数据包,发送ACK(seq=u+1)数据包到客户端

  • 服务器关闭与客户端的连接并发送一个FIN(seq=w)数据包到客户端,请求关闭连接

  • 客户端发送ACK(seq=w+1)数据包到服务器,服务器在收到ACK数据包后进行CLOSE状态,客户端在一定时间没有收到服务器的回复证明其关闭后,也进入关闭状态

作者:可乐冰冰冰
链接:https://juejin.cn/post/7252869090549268538
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

VUE3基础学习(一)环境搭建与简单上手

VUE是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型。 个人学习感受:构建模板,通过数据就可以生成展示的html,上手简单,快速。 引用:VUE官网 ...
继续阅读 »

VUE是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型。


个人学习感受:构建模板,通过数据就可以生成展示的html,上手简单,快速。


引用:VUE官网



  • 声明式渲染:Vue 基于标准 HTML 拓展了一套模板语法,使得我们可以声明式地描述最终输出的 HTML 和 JavaScript 状态之间的关系。

  • 响应性:Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM。


开始:


1.安装环境


1.node.js (已安装 16.0 或更高版本的 Node.js)


说明:Node.js是一种基于Chrome V8引擎的JavaScript运行环境,是一个可以在服务器端运行JavaScript的开源工具。
NodeJS已经集成了npm,所以npm也一并安装好了。
验证测试: node -v npm -v


1688628179293(1).png


2.cnpm


说明 :由于npm的服务器在海外,所以访问速度比较慢,访问不稳定 ,cnpm的服务器是由淘宝团队提供 服务器在国内cnpm是npm镜像。但是一般cnpm只用于安装时候,所以在项目创建与卸载等相关操作时候我们还是使用npm。


全局安装cnpm


npm install -g cnpm --registry=https://registry.npm.taobao.org


验证测试: cnpm -v


1688628363694(1).png


1.IDE与简单上手


1.IDE:webstorm


说明:我个人一直在用jetbrains 旗下的各种 IDA ,我使用起来比较熟练。


配置IDE:


f3a5978c8419300747f83d9ce163328.png


fc016670459439a8f9bdbb6448d5936.png


80a50b2d56df3c9a4a9c6941dd60d89.png


baa93f92dd8b8e24326e4d17888d9b7.png


2.简单上手:


到需要创建项目的文件目录


npm init vue@latest


这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。你将会看到一些诸如 TypeScript 和测试支持之类的可选功能提示:


1688629335156.png


如果不确定是否要开启某个功能,你可以直接按下回车键选择 No。在项目被创建后,通过以下步骤安装依赖并启动开发服务器:


之后执行:


npm install


1688629446854(1).png


执行完安装后执行启动:


npm run dev


1688629550944(1).png


执行成功: 打开网页 http://localhost:5173/


1688629581157.png


当你准备将应用发布到生产环境时,请运行:


npm run build


此命令会在 ./dist 文件夹中为你的应用创建一个生产环境的构建版本。


简单例子 与 说明:


<script setup>
defineProps({
msg: {
type: String,
required: true
}
})
</script>

<!--模板区域-->
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3> You’ve successfully created a project with <a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> + <a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
</h3>
</div>
</template>
<!--样式区域-->
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
</style>

作者:王二蛋与他的张大花
链接:https://juejin.cn/post/7252537738276208699
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Vue 为什么要禁用 undefined?

vue
Halo Word!大家好,我是大家的林语冰(挨踢版)~ 今天我们来伪科普一下——Vue 等开源项目为什么要禁用/限用 undefined? 敏感话题 我们会讨论几个敏感话题,包括但不限于—— 测不准的 undefined 如何引发复合 BUG? 薛定谔的...
继续阅读 »

Halo Word!大家好,我是大家的林语冰(挨踢版)~


今天我们来伪科普一下——Vue 等开源项目为什么要禁用/限用 undefined




敏感话题


我们会讨论几个敏感话题,包括但不限于——



  1. 测不准的 undefined 如何引发复合 BUG?

  2. 薛定谔的 undefined 如何造成二义性?

  3. 未定义的 undefined 为何语义不明?


懂得都懂,不懂关注,日后再说~




1. 测不准的 undefined 如何引发复合 BUG?


一般而言,开源项目对 undefined 的使用有两种保守方案:



  • 禁欲系——能且仅能节制地使用 undefined

  • 绝育系——禁用 undefined


举个粒子,Vue 源码就选择用魔法打败魔法——安排黑科技 void 0 重构 undefined


vue-void.png


事实上,直接使用 undefined 也问题不大,毕竟 undefined 表面上还是比较有安全感的。


readonly-desc.gif


猫眼可见,undefined 是一个鲁棒只读的属性,表面上相当靠谱。


虽然 undefined 自己问题不大,但最大的问题在于使用不慎可能会出 BUG。undefined 到底可能整出什么幺蛾子呢?


你知道的,不同于 null 字面量,undefined 并不恒等于 undefined 原始值,比如说祂可以被“作用域链截胡”。


举个粒子,当 undefined 变身成为 bilibili,同事的内心是崩溃的。


bilibili.png


猫眼可见,写做 undefined 变量,读做 'bilbili' 字符串,这样的代码十分反人类。


这里稍微有点违和感。机智如你可能会灵魂拷问,我们前面不是已经证明了 undefined 是不可赋值的只读属性吗?怎么祂喵地一言不合说变就变,又可以赋值了呢?来骗,来偷袭,不讲码德!


这种灵异现象主要跟变量查找的作用域链机制有关。读写变量会遵循“就近原则”优先匹配,先找到谁就匹配谁,就跟同城约会一样,和樱花妹异地恋的优先级肯定不会太高,所以当前局部作用域的优先级高于全局作用域,于是乎 JS 会优先使用当前非全局同名变量 undefined


换而言之,局部的同名变量 undefined 屏蔽(shadow,AKA“遮蔽”)了全局变量 globalThis.undefined


关于作用域链这种“远亲不如近邻”的机制,吾愿赐名为“作用域链截胡”。倘若你不会搓麻将,你也可以命名为“作用域链抢断”。倘若你不会打篮球,那就叫“作用域链拦截”吧。


globalThis.undefined 确实是只读属性。虽然但是,你们重写非全局的 undefined,跟我 globalThis.undefined 有什么关系?


周树人.gif


我们总以为 undefined 短小精悍,但其实 globalThis.undefined 才能扬长避短。


当我们重新定义了 undefinedundefined 就名不副实——名为 undefined,值为任意值。这可能会在团队协作中引发复合 BUG。


所谓“复合 BUG”指的是,单独的代码可以正常工作,但是多人代码集成就出现问题。


举个粒子,常见的复合 BUG 包括但不限于:



  • 命名冲突,比如说 Vue2 的 Mixin 就有这个瑕疵,所以 Vue3 就引入更加灵活的组合式 API

  • 作用域污染,ESM 模块之前也有全局作用域污染的老毛病,所以社区有 CJS 等模块化的轮子,也有 IIFE 等最佳实践

  • 团队协作,Git 等代码版本管理工具的开发冲突


举个粒子,undefined 也可能造成类似的问题。


complex-bug.png


猫眼可见,双方的代码都问题不大,但放在一起就像水遇见钠一般干柴烈火瞬间爆炸。


这里分享一个小众的冷知识,这样的代码被称为“Jenga Code”(积木代码)。


Jenga 是一种派对益智积木玩具,它的规则是,先把那些小木条堆成一个规则的塔,玩家轮流从下面抽出一块来放在最上面,谁放上之后木塔垮掉了,谁就 GG 了。


jenga.gif


积木代码指的是一点点的代码带来了亿点点的 BUG,一行代码搞崩整个项目,码农一句,可怜焦土。


换而言之,这样的代码对于 JS 运行时是“程序正义”的,对于开发者却并非“结果正义”,违和感拉满,可读性和可为维护性十分“赶人”,同事读完欲哭无泪。


所谓“程序正义”指的是——JS 运行时没有“阳”,不会抛出异常,直接挂掉,浏览器承认你的代码 Bug free,问题不大。


祂敢报错吗?祂不敢。虽然但是,无症状感染也是感染。你敢这么写吗?你不敢。除非忍不住,或者想跑路。


举个粒子,“离离原上谱”的“饭圈倒牛奶”事件——



  • 有人鞠躬尽瘁粮食安全

  • 有人精神饥荒疯狂倒奶


这种行为未必违法,但是背德,每次看到只能无视,毕竟语冰有“傻叉恐惧症”。


“程序正义”不代表“结果正义”,代码能 run 不代表符合“甲方肝虚”,不讲码德可能造成业务上的技术负债,将来要重构优化来还债。所谓“前猫拉屎,后人铲屎”大抵也是如此。


综上所述,要警惕测不准的 undefined 在团队开发中造成复合 BUG。




2. 薛定谔的 undefined 如何造成二义性?


除了复合 BUG,undefined 还可能让代码产生二义性。


代码二义性指的是,同一行代码,可能有不同的语义。


举个粒子,JS 的一些代码解读就可能有歧义。


mistake.png


undefined 也可能造成代码二义性,除了上文的变量名不副实之外,还很可能产生精神分裂的割裂感。


举个粒子,代码中存在两个一龙一猪的 undefined


default.png


猫眼可见,undefined 的值并不相同,我只觉得祂们双标。


undefined 变量之所以是 'bilibili' 字符串,是因为作用域链就近屏蔽,cat 变量之所以是 undefined 原始值,是因为已声明未赋值的变量默认使用 undefined 原始值作为缺省值,所以没有使用局部的 undefined 变量。


倘若上述二义性强度还不够,那我们还可以写出可读性更加逆天的代码。


destruct.png


猫眼可见,undefined 有没有精神分裂我不知道,但我快精神分裂了。


代码二义性还可能与代码的执行环境有关,譬如说一猫一样的代码,在不同的运行时,可能有一龙一猪的结果。


strict-mode.png


猫眼可见,我写你猜,谁都不爱。


大家大约会理直气壮地反驳,我们必不可能写出这样不当人的代码,var 是不可能 var 的,这辈子都不可能 var


问题在于,墨菲定律告诉我们,只要可能有 BUG,就有可能有 BUG。说不定你的猪队友下一秒就给你来个神助攻,毕竟不是每个人都像你如此好学,既关注了我,还给我打 call。


语冰以前也不相信倒牛奶这么“离离原上谱”的事件,但是写做“impossible”,读做“I M possible”。


事实上,大多数教程一般不会刻意教你去写错误的代码,这其实恰恰剥夺了我们犯错的权利。不犯错我们就不会去探究为什么,而对知识点的掌握只停留在表面是什么,很多人知错就改,下次还敢就是因为缺少了试错的成就感和多巴胺,不知道 BUG 的 G 点在哪里,没有形成稳固的情绪记忆。


请相信我,永远写正确的代码本身就是一件不正确的事情,你会看到这期内容就是因为语冰被坑了气不过,才给祂载入日记。


语冰很喜欢的一部神作《七龙珠》里的赛亚人,每次从濒死体验中绝处逢生战斗力就会增量更新,这个设定其实蛮科学的,譬如说我们身边一些“量变到质变”的粒子,包括但不限于:



  • 骨折之后骨头更加坚硬了

  • 健身也是肌肉轻度撕裂后增生

  • 记忆也是不断复习巩固


语冰并不是让大家在物理层面去骨折,而是鼓励大家从 BUG 中学习。私以为大神从来不是没有 BUG,而是 fix 了足够多的 BUG。正如爱迪生所说,我没有失败 999 次,而是成功了 999 次,我成功证明了那些方法完全达咩。


综上所述,undefined 的二义性在于可能产生局部的副作用,一猫一样的代码在不同运行时也可以有一龙一猪的结果,最终导致一千个麻瓜眼中有一千个哈利波特,读码人集体精神分裂。




3. 未定义的 undefined 为何语义不明?


除了可维护性感人的复合 BUG 和可读性感人的代码二义性,undefined 自身的语义也很难把握。


举个粒子,因为太麻烦就全写 undefined 了。


init.png


猫眼可见,原则上允许我们可以无脑地使用 undefined 初始化任何变量,万物皆可 undefined


虽然但是,绝对的光明等于绝对的黑暗,绝对的权力导致绝对的腐败。undefined 的无能恰恰在于祂无所不能,语冰有幸百度了一本书叫《选择的悖论》,这大约也是 undefined 的悖论。


代码是写给人看的,代码的信息越具体明确越好,偏偏 undefined 既模糊又抽象。你知道的,我们接触的大多数资料会告诉我们 undefined 的意义是“未定义/无值”。


虽然但是,准确而无用的观念,终究还是无用的。undefined 的正确打开方式就是无为,使用 undefined 的最佳方式是不使用祂。




免责声明



本文示例代码默认均为 ESM(ECMAScript Module)筑基测评,因为现代化前端开发相对推荐集成 ESM,其他开发环境下的示例会额外注释说明,edge cases 的解释权归大家所有。



今天的《ES6 混合理论》合集就讲到这里啦,我们将在本合集中深度学习若干奇奇怪怪的前端面试题/冷知识,感兴趣的前端爱好者可以关注订阅,也欢迎大家自由言论和留言许愿,共享 BUG,共同内卷。


吾乃前端的虔信徒,传播 BUG 的福音。


我是大家的林语冰,我们一期一会,不散不见,掰掰~


作者:大家的林语冰
链接:https://juejin.cn/post/7240483867123220540
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

老菜鸟为什么喜欢代码重构

“花有重开日,人无再少年”,诸位,如果再给你们一次机会还会选择做个码农吗? “今年花胜去年红,可惜明年花更好”,诸位,对码农生涯还有多少期盼? “君看今日树头花,不是去年枝上朵”,诸位,可曾想过:如果在几年前便拥有现在的技术、薪资,面对生活、谈女朋友会不会是另...
继续阅读 »

“花有重开日,人无再少年”,诸位,如果再给你们一次机会还会选择做个码农吗?


“今年花胜去年红,可惜明年花更好”,诸位,对码农生涯还有多少期盼?


“君看今日树头花,不是去年枝上朵”,诸位,可曾想过:如果在几年前便拥有现在的技术、薪资,面对生活、谈女朋友会不会是另外一番滋味呢...


好了!扯多了,还是谈一谈正题:老菜鸟为什么喜欢代码重构!


屎山上挖呀挖


最近几个月流行的"花园种花"跟大家分享一下:


222213.jpg

小朋友们准备开始
在小小的花园里面 ~ 挖呀挖呀挖 ~ 种小小的种子 ~ 开小小的花
在大大的花园里面 ~ 挖呀挖呀挖 ~ 种大大的种子 ~ 开大大的花
在特别大的花园里面 ~ 挖呀挖呀挖 ~ 种特别大的种子 ~ 开特别大的花
在什么样的花园里面 ~ 挖呀挖呀挖 ~ 种什么样的种子 ~ 开什么样的花
在特别大的花园里面 ~ 挖呀挖呀挖 ~ 种特别大的种子 ~ 开特别大的花


每天对着自己项目中的代码,真的想改编一下:

在小小的屎山里面   ~ 挖呀挖呀挖 ~ 
在大大的屎山里面 ~ 挖呀挖呀挖 ~
在特别大的屎山里面 ~ 挖呀挖呀挖 ~
在特别大的屎山里面 ~ 挖呀挖呀挖 ~

没被铲走的屎山就是好屎山



诸位,想不想一睹屎山真容?----去你自己项目上找吧,哈哈; 重构并不一定是因为屎山,屎山可能不一定需要重构





  1. 于公司、项目而言,能正常运行、满足客户需要的代码就是OK的,甭管它屎山还是花园,能帮公司赚到钱的就是王道也!




  2. 为什么会有屎山的存在,源于项目早期的设计、架构不合理、中后期编码不规范、或者说压根就没有架构、没有规范;




  3. 很多大厂或一些比较讲究的公司,有自己的架构师、质量团队,那他们产出的项目质量就会非常高,就算是重构也可能是跨代、技术层面的升级,绝非屎山引起的重构!




  4. 爷爷都是从孙子过来的,谁还没个小菜成长记呢,谁小时候(初级阶段)还没(写过一些垃圾代码)在裤子上拉过屎呢;能接受别人的喷说明你心智成熟了,能发现项目中的糟糕代码说明你技术成长了;




  5. 最重要的一点,用发展的眼光看问题,以当下的技术、潮流去审视多年前的项目,还是要充满敬意,而不只是吐槽!




优化


你需要可能是优化


重构并不一定是因为屎山,可能是技术自身的转换升级; 屎山可能不一定要重构,或许只需要做代码的优化; 倘若项目的技术栈还比较年轻,那我们面对的可能是优化,而不是重构;



那糟糕的代码源自何处呢?


  • 曾经的你? 曾经梦想仗剑走天涯,如今大肚秃顶撸代码;

  • 已经离职的同事?人走茶凉,雁过拔毛;


话说谁还不是从小菜成长起来的呢,当你发现项目上诸多不合理时,说明你的技术已经成长、提高了不少;



  • 可能你接手了一个旧的项目,看到的代码是公司几年前的产品设计

  • 可能你自己今年前写的代码,因为项目赚钱了,又要升级

  • 可能你接手了(不太讲究的)同事的代码

  • 可能就是因为赶进度,只要功能实现


如果仅仅是代码不够友好,我们需要的或许只是长期优化了...


网友谈重构



当你看到眼前的屎山会作何感想? TMD,怎么会会会有如此的代码呢,某某某真**,ε=(´ο`*)))唉? 正常,如果你没有这样的感慨,下文就不用看了,直接吐槽就行了...



看看网友回复



  • 看心情




  • 别自己找trouble




  • 又不是不能用,你领导怕成本太高吧,新项目可以用新架构




  • 除非你自己愿意花时间去重构,不然哪个老板舍得花这个钱和时间




  • 应该选择成为领导,让底下人996用新技术重构
    .... 下面的更绝




  • 小伙子还年轻吧,动不动就重构




  • 代码和你 有一个能跑就行
    哈哈,太多了,诸位,你会怎么想,面对糟糕的代码你会重构吗?




cg3.jpg


问题



当你发现问题时说明你用心了;当你吐槽槽糕代码时,说明你技术提升了;当你想爆粗口时说明你对美好生活是充满向往的;



那么,你的项目上可能有哪些问题呢(以前端代码为例)?



  • 技术栈过于古老

  • 架构设计不合理

  • 技术选型、规范不合理

  • 不合理的三目运算符

  • 过多的if嵌套

  • 回调地狱

  • 冗余的方法、函数

  • 全局变量混乱

  • 项目结构混乱

  • 路由管理糟糕

  • 状态数据管理混乱

  • CSS样式样式混乱

  • .....
    .... 哪些该重构,哪些该优化?


sikao6.jpg


机会



“祸兮福之所倚,福兮祸之所伏”, 问题的背后往往就是机会;



路人甲: 明明就是一座屎山,又何来机会一说?有扒拉屎山的功夫,搞点新技能,搞点原创不开心吗?答案是肯定的


路人乙: 解决屎 OR 新项目搭建,会选择哪个呢? 我想脑子正常点的人应该会选择重新搭建吧!


个人觉得对于经验比较丰富的开发当然选容易的,对于经验不丰富的开发而言当然也选容易的!对于有一定基础 && 想要快速提升综合能力者,解决屎山或许别有一番滋味,未尝不是一件闻起来臭吃起来香的(臭豆腐)幸事;



  • 理解老旧项目的初始架构、设计有助于了解、理解技术发展的脉络

  • 有很多老旧项目的设计、架构是非常优秀的,值得去深入学习背后的思想

  • 重构的整个过程也是不断审视自己不足的过程,查漏补缺,提升最短的那块板

  • 一切技术服务于业务,重构的过程也是深入理解业务的过程

  • 有机会重构是一种幸福,面对诸多压力本身就是是一种心智的磨练,不经历风云怎能见彩虹; 大成功者必是大磨难者!


挑战



问题的背后是机会, 机会的身边往往伴随着诸多挑战;面对重构不去争取,那老油条和小菜鸟有有什么区别(什么价值),不要等到退出行业了,再说: “曾经有一次重构的机会摆在我面前,我没有珍惜,等到了失去的时候才后悔莫及,尘世间最痛苦的事莫过于此。如果老板可以再给我一个再来一次的机会的话,我会说:重构!重构! 重构!。如果一定要加一个期限的话,我希望是公司设定的时间之前!”



QQ截图20230628143530.jpg



  • 时间:领导、公司会不会给予足够的时间深入重构,万一没有如期完成呢?

  • 回报:重构是否能达到预期,是否能带来质变级的体验,万一重构后没成功呢?

  • 能力:自身能力是否能承担得起重构的重任,是否具备一定的抗压能力

  • 博弈:重构也是一种资源的博弈,考虑如何让自己的利益最大化的


重构




  • 重构的机会应该是去争取的,而不是被赋予的




  • 生活中处处有压力,又总是压得人喘不过气,如果连个代码重构都不敢想、不敢搞,那生活中的种种不如意又当如何?正如那句话:“做人如果没有梦想,和咸鱼有什么区别?”




QQ截图20230628143731.jpg


收获


其实,个人感觉收获最大的还是心智的磨练; 其次是技术的提升;


结语


生活已经够累了,跟大家闲扯一下,放松!放松!放松! 最重要的天热了要多喝水,多吃蔬菜和水果,适度的体育活动!欢迎大家说说自己的看法


作者:风雪中的兔子
链接:https://juejin.cn/post/7249586143011291192
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

为了摸鱼,我开发了一个工具网站

       大家好,我是派大星,由于前段时间实习入职,所以把时间以及精力都放在熟悉公司业务以及从工作中提升自己的业务逻辑,空余时间也是放在了学习新技术上,到目前为止也是参与了公司3个项目的开发团队中,参与过程中犯过错,...
继续阅读 »

       大家好,我是派大星,由于前段时间实习入职,所以把时间以及精力都放在熟悉公司业务以及从工作中提升自己的业务逻辑,空余时间也是放在了学习新技术上,到目前为止也是参与了公司3个项目的开发团队中,参与过程中犯过错,暴露出了很多的不足,丧失过信心,学生时期所带的傲气也是被一点一点的慢慢的打磨掉,正是因为这些,带给我的成长是巨大的。好了,闲言少叙,下面让我们进入今天的主题。


创作背景


       因为”懒“得走路,所以发明了汽车飞机,因为”懒“得干苦力活,所以发明了机器帮助我们做,很早之前看到过这个梗,而我这次同样也是因为”懒“,所以才开发出了这个工具。这里先卖个关子,先不说这个工具的作用,容我向大家吐槽一下这一段苦逼的经历。在我实习刚入职不久,就迎来了自己第一个任务,由于自己对所参与的项目的业务并不太了解,所以只能先做一些类似测试的工作,比如就像这次,组长给了我一份Json 文件,当我打开文件后看到数据都是一些地区名称,但当我随手的将滚动条往下一拉,瞬间发现不对劲,因为这个小小的文件行数竟然达到了1w+❗❗❗❗


在这里插入图片描述


不禁让我脊背发凉,但是这时我的担心还没达到最坏的地步,毕竟我还对具体的任务不了解。但当组长介绍任务内容,主要是让我将这些数据添加到数据库对应的表中,由于没有sql脚本,只有这个json 文件,需要手动去操作,而且给我定的任务周期是两天。听到这个时间时内心的慌张瞬间消失了,因为在之前我就了解过Navicat支持Json格式的文件直接导入数据,一个这么简单的任务给我两天时间,这不是非要让我带薪学习。


猥琐流口水表情包_表情包大全_尼凸图片网_一个图片、头像、表情包分享的网站


当我接下任务自信打开Navicat的导入功能时发现了一个重要问题,虽然它支持字段映射,但是给的Json数据是省市区地址名称,里面包含着各种嵌套,说实话到想到这里我已经慌了,而且也测试了一下字段只能单个的批量导入,而且不支持嵌套的类型,突然就明白为什么 给我两天的时间。在这里插入图片描述这时候心里只能默默祈祷已经有大神开发出了能处理这种数据的工具网站,但是经过一个小时的艰苦奋斗,但最终依旧是没有结果,网上有很多JsonSQl的工具网站,但是很多都支持简单支持一下生成创建表结构的语句,当场心如死灰,跑路的心都有了。但最终还是咬着牙 手动初始化数据,其过程中的“趣味” 实属无法用语言表达……


        上述就是这个工具的开发背景,也是怕以后再给我分配这么“有趣” 的任务。那么下面就给大家分享一下我自制的 Json转译SQL 工具,而且它也是一个完全免费的工具网站,同时这次也是将项目进行了 开源分享,大家也可以自己用现成的代码完成本地部署测试,感兴趣的同学可以自行拉取代码!



开源地址:github.com/pdxjie/sql-…



项目简介


Sql-Translation (简称ST)是一个 Json转译SQL 工具,在同类工具的基础上增强了功能,为节省时间、提高工作效率而生。并且遵循 “轻页面、重逻辑” 的原则,由极简页面来处理复杂任务,且它不仅仅是一个项目,而是以“降低时间成本、提高效率”为目标的执行工具。
在这里插入图片描述


技术选型


前端:



  • Vue

  • AntDesignUI组件库

  • MonacoEditor 编辑器

  • sql-formatter SQL格式化


后端:



  • SpringBoot

  • FastJson


项目特点



  • 内置主键JSON块如果包含id字段,在选择建表操作模式时内部会自动为id设置primary key

  • 支持JSON数据生成建表语句:按照内置语法编写JSON,支持生成创建表的SQL语句

  • 支持JSON数据生成更新语句:按照内置语法编写JSON,支持生成创更新的SQL语句,可配置单条件、多条件更新操作

  • 支持JSON数据生成插入语句:按照内置语法编写JSON,支持生成创插入的SQL语句,如果JSON中包含 多层 (children)子嵌套,可按照相关语法指定作为父级id的字段

  • 内置操作语法:该工具在选取不同的操作模式时,内置特定的使用语法规范

  • 支持字段替换:需转译的JSON中字段与对应的SQL字段不一致时可以选择字段替换

  • 界面友好:支持在线编辑JSON代码,支持代码高亮、语法校验、代码格式化、查找和替换、代码块折叠等,体验良好


解决痛点


下面就让我来给大家介绍一下Sql-Translation 可以解决哪些痛点问题:




  • 需要将大量JSON中的数据导入到数据库中,但是JSON中包含大量父子嵌套关系 ——> 可以使用本站




  • 在进行JSON数据导入数据库时,遇到JSON字段与数据库字段不一致需要替换字段时 ——> 可以使用本站




  • 根据Apifox工具来实现更新或新增接口(前提是对接口已经完成了设计工作),提供了Body体数据,而且不想手动编写SQL时 ——> 可以使用本站




对上述三点进行进行举例说明(按照顺序):


第一种情况:

{
"id": "320500000",
"text": "苏州工业园区",
"value": "320500000",
"children": [
{
"id": "320505006",
"text": "斜塘街道",
"value": "320505006",
"children": []
},
{
"id": "320505007",
"text": "娄葑街道",
"value": "320505007",
"children": []
},
....
]
}

第二种情况:
在这里插入图片描述


第三种情况:
在这里插入图片描述


以上内容就是该工具的简单介绍,由于该工具内置了部分语法功能,想要了解本工具全部工具以及想要动手操作的的同学请点击前往操作文档 ,该操作文档中包含了具体的语法介绍以及每种转换的具体示例数据 提供测试使用。


地址传送门



作者:派同学
链接:https://juejin.cn/post/7168285867160076295
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

大喊一声Fuck!代码就能跑了是什么体验?

大喊一声Fuck!代码就能跑了是什么体验? 1 前言 大家好,我是心锁,23届准毕业生。 程序员的世界,最多的不是代码,而是💩山和bug。 近期我在学习过程中,在github找到了这么一个项目,能在我们输错命令之后,大喊一声Fuck即可自动更正命令,据说喊得越...
继续阅读 »

大喊一声Fuck!代码就能跑了是什么体验?


1 前言


大家好,我是心锁,23届准毕业生。


程序员的世界,最多的不是代码,而是💩山和bug。


近期我在学习过程中,在github找到了这么一个项目,能在我们输错命令之后,大喊一声Fuck即可自动更正命令,据说喊得越大声效果越好。


c37237b03e45fed8c2828c6f7abb93b9


2 项目基本介绍


thefuck是一个基于Python编写的项目,它能够自动纠正你在命令行中输入的错误命令。如果你输错了一个命令,只需要在命令行中输入“fuck”,thefuck就会自动纠正你的错误。该项目支持众多的终端和操作系统,包括Linux、macOS和Windows。


43885f5e1f8c7ff2b3392d297c855609


2.1 环境要求



  • python环境(3.4+)


2.2 安装方式


thefuck支持brew安装,非常方便,在macOS和Linux上都可以通过brew安装。

brew install thefuck

也支持通过pip安装,便携性可以说是一流了。

pip3 install thefuck

2.3 配置环境变量


建议将下边的代码配置在环境变量中(.bash_profile.bashrc.zshrc),不要问为什么,问就是有经验。

eval $(thefuck --alias)
eval $(thefuck --alias FUCK)
eval $(thefuck --alias fuck?)
eval $(thefuck --alias fuck?)

接着运行source ~/.bashrc(或其他配置文件,如.zshrc)确认更改立即可用。


3 使用效果


Untitled


03cf7e926946b7d8a3da902841c3c5b1


4 thefuck的工作原理


thefuck的工作原理非常简单。当你输入一个错误的命令时,thefuck会根据你输入的命令和错误提示自动推测你想要输入的正确命令,并将其替换为正确的命令。thefuck能够自动推测正确的命令是因为它内置了大量的规则,这些规则能够帮助thefuck智能地纠正错误的命令。


所以,该项目开放了自定义规则。


4.1 创建自己的规则


如果thefuck内置的规则不能够满足你的需求,你也可以创建自己的规则。thefuck的规则是由普通的Python函数实现的。你可以在~/.config/thefuck/rules目录下创建一个Python脚本,然后在其中定义你的规则函数。


以创建一个名为my_rule的规则为例,具体步骤如下:


4.1.1 创建rule.py文件


~/.config/thefuck/rules目录下创建一个Python脚本,比如my_rules.py


4.1.2 遵循的规则


在自定义脚本中,必须实现以下两个函数,match显然是用来匹配命令是否吻合的函数,而get_new_command则会在match函数返回True时触发。

match(command: Command) -> bool
get_new_command(command: Command) -> str | list[str]

同时可以包含可选函数,side_effect的作用是开启一个副作用,即除了允许原本的命令外,你可以在side_effect做更多操作。

side_effect(old_command: Command, fixed_command: str) -> None

5 yarn_uninstall_to_remove


以创建一个名为yarn_uninstall_to_remove的规则为例,该规则会在我们错误使用yarn uninstall …命令时,自动帮助我们修正成yarn remove … 。具体步骤如下:


5.1 创建yarn_uninstall_to_move.py文件


~/.config/thefuck/rules目录下创建一个Python脚本,yarn_uninstall_to_remove.py


5.2 编写代码

from thefuck.utils import for_app

@for_app('yarn')
def match(command):
return 'uninstall' in command.script

def get_new_command(command):
return command.script.replace('uninstall', 'remove')

priority=1 # 优先级,数字越小优先级越高

5.3 效果


Untitled


6 总结


世界之大,无奇不有。不得不说的是,伴随着AI的逐渐发展,类似这种项目未来一定是优先接入AI者才可以继续发展。


友情提示,喊fuck的时候先设置后双击control打开听写功能,喊完再点击一下control完成输入。


Untitled


作者:源心锁
链接:https://juejin.cn/post/7213651072145244221
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一次线上事故,我顿悟了异步的精髓

在高并发的场景下,异步是一个极其重要的优化方向。 前段时间,生产环境发生一次事故,笔者认为事故的场景非常具备典型性 。 写这篇文章,笔者想和大家深入探讨该场景的架构优化方案。希望大家读完之后,可以对异步有更深刻的理解。 1 业务场景 老师登录教研平台,会看到课...
继续阅读 »

在高并发的场景下,异步是一个极其重要的优化方向。


前段时间,生产环境发生一次事故,笔者认为事故的场景非常具备典型性


写这篇文章,笔者想和大家深入探讨该场景的架构优化方案。希望大家读完之后,可以对异步有更深刻的理解。


1 业务场景


老师登录教研平台,会看到课程列表,点击课程后,课程会以视频的形式展现出来。



访问课程详情页面,包含两个核心动作:




  1. 读取课程视频信息 :


    从缓存服务器 Redis 获取课程的视频信息 ,返回给前端,前端通过视频组件渲染。




  2. 写入课程观看行为记录 :


    当教师观看视频的过程中,浏览器每隔3秒发起请求,教研服务将观看行为记录插入到数据库表中。而且随着用户在线人数越多,写操作的频率也会指数级增长。




上线初期,这种设计运行还算良好,但随着在线用户的增多,系统响应越来越慢,大量线程阻塞在写入视频观看进度表上的 Dao 方法。上。


首先我们会想到一个非常直观的方案,提升写入数据库的能力



  1. 优化 SQL 语句;

  2. 提升 MySQL 数据库硬件配置 ;

  3. 分库分表。


这种方案其实也可以满足我们的需求,但是通过扩容硬件并不便宜,另外写操作可以允许适当延迟和丢失少量数据,那这种方案更显得性价比不足。


那么架构优化的方向应该是: “减少写动作的耗时,提升写动作的并发度” , 只有这样才能让系统更顺畅的运行。


于是,我们想到了第二种方案:写请求异步化



  • 线程池模式

  • 本地内存 + 定时任务

  • MQ 模式

  • Agent 服务 + MQ 模式


2 线程池模式


2014年,笔者在艺龙旅行网负责红包系统相关工作。运营系统会调用红包系统给特定用户发送红包,当这些用户登录 app 后,app 端会调用红包系统的激活红包接口 。


激活红包接口是一个写操作,速度也比较快(20毫秒左右),接口的日请求量在2000万左右。


应用访问高峰期,红包系统会变得不稳定,激活接口经常超时,笔者为了快速解决问题,采取了一个非常粗糙的方案:


"控制器收到请求后,将写操作放入到独立的线程池中后,立即返回给前端,而线程池会异步执行激活红包方法"。


坦率的讲,这是一个非常有效的方案,优化后,红包系统非常稳定。


回到教研的场景,见下图,我们也可以设计类似线程池模型的方案:



使用线程池模式,需要注意如下几点:



  1. 线程数不宜过高,避免占用过多的数据库连接池 ;

  2. 需要考虑评估线程池队列的大小,以免出现内存溢出的问题。


3 本地内存 + 定时任务


开源中国统计浏览数的方案非常经典。


用户访问过一次文章、新闻、代码详情页面,访问次数字段加 1 , 在 oschina 上这个操作是异步的,访问的时候只是将数据在内存中保存,每隔固定时间将这些数据写入数据库。



示例代码如下:



我们可以借鉴开源中国的方案 :



  1. 控制器接收请求后,观看进度信息存储到本地内存 LinkedBlockingQueue 对象里;

  2. 异步线程每隔1分钟从队列里获取数据 ,组装成 List 对象,最后调用 Jdbc batchUpdate 方法批量写入数据库;

  3. 批量写入主要是为了提升系统的整体吞吐量,每次批量写入的 List 大小也不宜过大 。


这种方案优点是:不改动原有业务架构,简单易用,性能也高。该方案同样需要考虑内存溢出的风险。


4 MQ 模式


很多同学们会想到 MQ 模式 ,消息队列最核心的功能是异步解耦,MQ 模式架构清晰,易于扩展。



核心流程如下:



  1. 控制器接收写请求,将观看视频行为记录转换成消息 ;

  2. 教研服务发送消息到 MQ ,将写操作成功信息返回给前端 ;

  3. 消费者服务从 MQ 中获取消息 ,批量操作数据库 。


这种方案优点是:



  1. MQ 本身支持高可用和异步,发送消息效率高 , 也支持批量消费;

  2. 消息在 MQ 服务端会持久化,可靠性要比保存在本地内存高;


不过 MQ 模式需要引入新的组件,增加额外的复杂度。


5 Agent 服务 + MQ 模式


互联网大厂还有一种常见的异步的方案:Agent 服务 + MQ 模式。



教研服务器上部署 Agent 服务(独立的进程) , 教研服务接收写请求后,将请求按照固定的格式(比如 JSON )写入到本次磁盘中,然后给前端返回成功信息。


Agent 服务会监听文件变动,将文件内容发送到消息队列 , 消费者服务获取观看行为记录,将其存储到 MySQL 数据库中。


还有一种演进,假设我们不想在应用中依赖消息队列,不生成本地文件,可以采用如下的方式:



这种方案最大的优点是:架构分层清晰,业务服务不需要引入 MQ 组件。


笔者原来接触过的性能监控平台,或者日志分析平台都使用这种模式。


6 总结


学习需要一层一层递进的思考。


第一层:什么场景下需要异步



  • 大量写操作占用了过多的资源,影响了系统的正常运行;

  • 写操作异步后,不影响主流程,允许适当延迟;


第二层:异步的外功心法


本文提到了四种异步方式:



  • 线程池模式

  • 本地内存 + 定时任务

  • MQ 模式

  • Agent 服务 + MQ 模式


它们的共同特点是:将写操作命令存储在一个池子后,立刻响应给前端,减少写动作的耗时。任务服务异步从池子里获取任务后执行。


第三层:异步的本质


在笔者看来,异步是更细粒度的使用系统资源的一种方式


在教研课程详情场景里,数据库的资源是固定的,但写操作占据大量数据库资源,导致整个系统的阻塞,但写操作并不是最核心的业务流程,它不应该占用那么多的系统资源。


我们使用异步的解决方案时,无论是使用线程池,还是本地内存 + 定时任务 ,亦或是 MQ ,对数据库资源的使用都需要在合理的范围内,只有这样系统才能顺畅的运行。


作者:勇哥java实战分享
链接:https://juejin.cn/post/7118580043835506725
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

在线代码编辑器介绍与选型

web
引言 作为数据生产和管理的平台,数据平台的一大核心功能是在线数据开发,工欲善其事必先利其器,所以平台具备一个功能较为丰富、用户体验友好的在线代码编辑器,就成为了前提条件。 经历最近一两年的代码编辑器方案调研、选型和开发,我们对内部平台使用的代码编辑器进行了统一...
继续阅读 »

引言


作为数据生产和管理的平台,数据平台的一大核心功能是在线数据开发,工欲善其事必先利其器,所以平台具备一个功能较为丰富、用户体验友好的在线代码编辑器,就成为了前提条件。


经历最近一两年的代码编辑器方案调研、选型和开发,我们对内部平台使用的代码编辑器进行了统一和升级,并根据用户需求和业务场景进行了插件化定制,其底层是使用了 Monaco Editor 来进行二次开发。


本文主要是结合自己的理解,对代码编辑器相关知识进行整理,跟大家分享。


1. 在线代码编辑器是什么?


1.1 介绍


在线代码编辑器是一种基于 Web 技术开发的代码文本编辑器,可以在 Web 浏览器中直接使用。它通常包括用户界面模块、文本处理模块、插件扩展模块等模块;用户可以通过 Web 编辑器创建、编辑各种类型的文本文件,例如 HTML、CSS、JavaScript、Markdown 等。


1.2 分类


我们先来看看编辑器的分类:


类型描述典型产品优势劣势
远古编辑器textarea 或contentEditable+execCommand早期轻型编辑器(《100行代码带你实现一个编辑器》系列)门槛低,短时间内快速研发无法定制
contentEditable+文档模型借助contentEditable,各种拦截用户操作draftjs (react)、quilljs (vue)、prosemirror(util)站在浏览器的肩膀上,可以实现绝大多数的业内需求无法突破浏览器本身的限制(排版)
独立开发脱离浏览器自带编辑能力,独立做光标和排版引擎Google Docs、WPS等所有内容都把握在自己手上,排版随意个性化技术难度较高,研发成本较大

第一类编辑器,其劣势明显:由于重度依赖浏览器 execCommand 接口,而该接口支持的能力非常有限,故大多数功能无法订制,比如 fontSize 只能设置 1 - 7。另外兼容性也是一大问题,例如 Safari 并没有支持 heading 的设置。参考 MDN。而且该类编辑器基本都会直接将 HTML 作为数据模型(Model)来使用,这样会引发另外一个问题:相同的UI,可能对应了不同的DOM结构。举个例子,对于“加粗字体”这个用户输入,在 chrome 上,是添加了<blod>标签,ie11上则是添加了<strong>标签。


第二类编辑器与上一类编辑器最大的不同是定义了自己的 Model 层,所有视图(View)都与 Model 一一对应,并且一切 View 的变化都将由 Model 层的变化引发。为了做到这一点,需要拦截一切用户操作,准确识别用户意图,再对 Model 层进行正确的修改。坑点主要来自于对用户操作的拦截以及浏览器实现层面上的一些疑难杂症。故该类编辑器实现中的 hack 代码会非常多,理解起来比较困难。


第三类编辑器,采用隐藏textarea方案,它只负责接收输入事件,其他视图输出全靠自己,相对来说,更容易解耦。因为基本脱离了浏览器原生的光标,这块可以实现出更强大的功能。排版引擎可以自己搞,只要码力够强,想搞一个从从上往下从右往左的富文本编辑器也没问题,也带来了各种各样的可能,比如可以通过将 View 层用 canvas 实现,以规避很多兼容性问题。


2. 一款优秀的在线代码编辑器需要有哪些功能?


下面我们来看一下一个可用于生产环境的在线代码编辑器需要有哪些能力和模块:



2.1 核心模块


模块名模块描述
文本编辑用于处理用户输入的文本内容,管理文本状态,还包括实现文本的插入、删除、替换、撤销、重做等操作
语言实现语言高亮、代码分析、代码补全、代码提示&校验等能力
主题主要用于实现主题的管理、注册、切换、等功能
渲染主要完成编辑器的整体设计与生命周期管理
命令 & 快捷键管理注册和编辑的各种命令,比如查找文件、撤销、复制&粘贴等,同时也支持将命令以快捷键的形式暴露给用户
通信 & 数据流管理编辑器各模块之前的通信,以及数据存储、流转过程

2.2 扩展模块


模块名模块描述
文本能力扩展在现有处理文本的基础上进行功能扩展,比如修改获取文本方式。
语言扩展包括自定义新语言,扩展现有语言的关键字,完善代码解析、提示&校验等能力。
主题扩展包括自定义新主题,扩展现有主题的能力
命令扩展增加新命令,或者改写&扩展现有命令

3. 开源市场上有哪些代码编辑器?


目前开源市场使用较多的代码编辑器主要有 3 个,分别是 Monaco Editor(第三类)、Ace(第三类)和 Code Mirror(第二类)。本文也将带大家去了解他们的整体架构,做一些对比分析。


3.1 Monaco Editor


基本介绍:


类别描述
介绍是一个功能相对比较完整的代码编辑器,实现使用了 MVP 架构,采用了模块化和组件化的思想,其中编辑器核心代码部分是与 vscode 共用的,从源码目录中能看到有很多 browser 与 common 的目录区分。
仓库地址github.com/microsoft/v…
入口文件/editor/editor.main.ts
开始使用editor.create()方法来自 /editor/standalone/browser/standaloneEditor.ts

目录结构:


├── base        			# 通用工具/协议和UI库
│ ├── browser # 基础UI组件,DOM操作,事件
│ ├── common # diff计算、处理,markdown解析器,worker协议,各种工具函数
├── editor # 代码编辑器核心
| ├── browser # 在浏览器环境下的实现,包括了用于处理 DOM 事件、测量文本尺寸和位置、渲染文本等功能的代码。
| ├── common # 浏览器和 Node.js 环境下共用的代码,其中包括了文本模型、文本编辑操作、语法分析等功能的实现
| ├── contrib # 扩展模块,包含很多额外功能 查找&替换,代码片段,多光标编辑等等
| └── standalone # 实现了一个完整的编辑器界面,也是我们通常使用的完整编辑器
├── language # 前端需要的几种语言类型,与basic-languages不同的是,这里的实现语言功能更完整,包含关键字提示与语法校验等
├── basic-languages # 基础语言声明,里面只包含了关键字的罗列,主要用于关键字的高亮,不包含提示和语法校验

特点:



  • 多线程处理,主要分为 主线程 和 语言服务线程(使用了 Web Worker 技术 来模拟多线程,主要通过 postMessage 来进行消息传递)

    • 主线程:主要负责处理用户与编辑器的交互操作,以及渲染编辑器的 UI 界面,还负责管理编辑器的生命周期和资源,例如创建和销毁编辑器实例、加载和卸载语言服务、加载和卸载扩展等。

    • 语言服务线程:负责提供代码分析、语法检查等功能,以及处理与特定语言相关的操作。




DOM 结构:


<div class="monaco-editor" role="presentation">
<div class="overflow-guard" role="presentation">
<div class="monaco-scrollable-element editor-scrollable" role="presentation">
<!--实现行高亮-->
<div class="monaco-editor-background" role="presentation"></div>
<!--实现关键字背景高亮-->
<div class="view-overlays" role="presentation">
<div>...</div>
</div>
<!--每一行内容-->
<div class="view-lines" role="presentation">
<div>...</div>
</div>
<!--光标-->
<div class="monaco-cursor-layer" role="presentation"></div>
<!--文本输入框-->
<textarea class="monaco-editor-textarea"></textarea>
<!--横向滚动条-->
<div class="scrollbar horizontal"></div>
<!--纵向滚动条-->
<div class="scrollbar vertical"></div>
</div>
</div>
</div>


3.2 Code Mirror


基本介绍:


类别描述
介绍CodeMirror 6 是一款浏览器端代码编辑器,基于 TypeScript,该版本进行了完全的重写,核心思想是模块化和函数式,支持超过 14 种语言的语法高亮,亮点是高性能、可扩展性高以及支持移动端。
仓库地址github.com/codemirror
入口文件由于高度模块化,没有一个集成的入口文件,这里放上核心库@codemirror/view的入口文件:src/index.ts

开始使用


import { EditorState } from '@codemirror/state'; import { EditorView, keymap } from '@codemirror/view';
import { defaultKeymap } from '@codemirror/commands';
let startState = EditorState.create({
doc: 'console.log("hello, javascript!")',
extensions: [keymap.of(defaultKeymap)],
});
let view = new EditorView({
state: startState,
parent: document.body,
});

目录结构:


高度模块化(分为多个仓库),这里放上比较核心的库的分布和内部结构


核心模块:提供了编辑器视图(@codemirror/view)、编辑器状态(@codemirror/state)、基础命令(@codemirror/commands)等基础功能。


语言模块:提供了不同编程语言的语法高亮、自动补全、缩进等功能,例如@codemirror/lang-javascript@codemirror/lang-sql@codemirror/lang-python 等。


主题模块:提供了不同风格的编辑器主题,例如 @codemirror/theme-one-dark


扩展模块:提供了一些额外的编辑器功能,例如行号(@codemirror/gutter)、折叠(@codemirror/fold)、括号匹配(@codemirror/matchbrackets)等。


内部结构,以@codemirror/view为例:


├── src                         # 源文件夹
│ ├── editorview.ts # 编辑器视图层
│ ├── decoration.ts # 视图装饰
│ ├── cursor.ts # 光标的渲染
│ ├── domchange.ts # DOM 改变相关的逻辑
│ ├── domobserver.ts # 监听 DOM 的逻辑
│ ├── draw-selection.ts # 绘制选区
│ ├── placeholder.ts # placeholder的渲染
│ ├── ...
├── test # 测试用例
| ├── webtest-domchange.ts # 测试监听到 DOM 变化后的一系列处理。
| ├── ...

特点:


指导 CodeMirror 架构设计的核心观点是函数式代码(纯函数),它会创建一个没有副作用的新值,和命令式代码交互更方便。而浏览器 DOM 很明显也是命令式思维,和 CodeMirror 集成的大部分系统类似。


CodeMirror 6 的 state 表现层是严格函数式的 - 即 document 和 state 数据结构都是不可变的,而能操作它们的都是纯函数,view 包将它们封装在一个命令式接口中。


所以即使 editor 已经转到了新的 state,而旧的 state 依然原封不动的存在,保存旧状态和新状态在面对处理 state 改变的情况下极为有利,这也意味着直接改变一个 state 值,或者添加额外 state 属性的命令式扩展都是不建议的,后果也不太可控。


CodeMirror 处理状态更新的方式受 Redux 启发,除了极少数情况(如组合和拖拽处理),视图的状态完全是由 EditorState 里的 state 属性决定的。


通过创建一个描述改变document、selection 或其他 state 属性的 transaction,以这种函数调用方式来更新 state。这个 transaction 之后可以通过 dispatched 分发,告诉 view 更新 state,更新新 state 对应的 DOM 展示。


let transaction = view.state.update({ changes: { from: 0, insert: "0" }})
console.log(transaction.state.doc.toString()) // "0123"
// 此刻视图依然显示的旧状态
view.dispatch(transaction)
// 现在显示新状态了

典型的用户交互数据流如下图:



view 监听事件变化。当 DOM 事件发生时(或者快捷键触发的命令,或者由扩展注册的事件处理器),CodeMirror会把这些事件转换为新的状态 transcation,然后分发。此时生成一个新的 state,当接收到新 state 后就会去更新 DOM。


DOM 结构:


<div class="cm-editor [theme scope classes]">
<div class="cm-scroller">
<div class="cm-content" contenteditable="true">
<div class="cm-line">Content goes here</div>
<div class="cm-line">...</div>
</div>
</div>
</div>


cm-editor 为一个 editor view 实例(在 merge-view,也就是代码对比情况下,给做了一个合并,其实还是两个 editor view 合在一起)


cm-scroller 为编辑器主展示区,并且展示了滚动条


cm-tooltip-autocomplete 为展示一些独立的层,比如代码提示,代码补全等


cm-gutter 是行号


cm-content 是编辑器的内容区


cm-layer 是跟 content 平级的,主要负责自定义指针和选区的展示


view-port 为CodeMirror 的一个优化,只解析和渲染了这个可视区域内的 DOM


cm-line 是每一行的内容,里面就是真实的 DOM 了


line-decorator 是提供给插件使用,用来装饰每一行的


在这个架构下,每个 editor 比较独立,可以渲染多个



3.3 Ace


基本介绍:


类别描述
介绍基于 Web 技术的代码编辑器,可以在浏览器中运行,高性能,体积小,功能全是它的主要优点。支持了超过120种语言的语法高亮,超过20个不同风格的主题,与 Sublime,Vim 和 TextMate 等本地编辑器的功能和性能相匹配。
仓库地址github.com/ajaxorg/Ace
入口文件/src/Ace.js
开始使用Ace.edit()

目录结构:


Ace 的目录结构相对简单,按功能分成了一个个不同的 js 文件,我这里列举其中一部分,部分较为复杂的功能除了提供了入口 js 文件以外,还在对应同级建立了文件夹里面实现各种逻辑,这里列举了 layer (渲染层) 为例子。


src/
├── layer #渲染分层实现
├── cursor.js #鼠标滑入层
├── decorators.js #装饰层,例如波浪线
├── lines.js #行渲染层
├── text.js #文本内容层
├── ...
├── ... #其他功能,例如 keybord
├── Ace.js #入口文件
├── ...
├── autocomplete.js #定义了编辑器补全相关内容
├── clipboard.js #定义了pc移动端兼容的剪切板
├── config.js
├── document.js
├── edit_session.js #定义了 Session 对象
├── editor.js #定义了 editor 对象
├── editor_keybinding.js #键盘事件绑定
├── editor_mouse_handler.js
├── virtual_renderer.js #定义了渲染对象 Renderer,引用了 layer 中定义的个种类
├── ...
├── mode.js
├── search.js
├── selection.js
├── split.js
└── theme.js

特点:



  • 事件驱动

    • Ace 中提供了丰富的事件系统,以供使用者直接使用或者自定义,并且通过对事件的触发和响应来进行内部数据通信实现代码检查,数据更新等等



  • 多线程

    • Ace 编辑器将解析代码的任务交给 Web Worker 处理,以提高代码解析的速度并避免阻塞用户界面。在 Web Worke r中,Ace 使用 Acorn库来解析 JavaScript 代码,并将解析结果发送回主线程进行处理




DOM 结构:


<div class="ace-editor">

<textarea
class="ace_text-input"
wrap="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
>

</textarea>
<!-- 行号区域 -->
<div class="ace_gutter" aria-hidden="true">
<div
class="ace_layer ace_gutter-layer"
>

<div class="ace_gutter-cell" >1 <span></span></div>
</div>
</div>
<!-- 内容区域 -->
<div class="ace_scroller" >
<div class="ace_content">
<div class="ace_layer ace_print-margin-layer">
<div class="ace_print-margin" style="left: 580px; visibility: visible;"></div>
</div>
<div class="ace_layer ace_marker-layer">
<div class="ace_active-line"></div>
</div>
<div class="ace_layer ace_text-layer" >
<div class="ace_line" >
<span class="ace_keyword">select</span>
<span class="ace_keyword">from</span>
<span class="ace_string">'xxx'</span>
</div>
<div class="ace_line"></div>
</div>
<div class="ace_layer ace_marker-layer"></div>
<div class="ace_layer ace_cursor-layer ace_hidden-cursors">
<!-- 光标 -->
<div class="ace_cursor"></div>
</div>
</div>
</div>
<!-- 纵向滚动条 -->
<div class="ace_scrollbar ace_scrollbar-v">
<div class="ace_scrollbar-inner" >&nbsp;</div>
</div>
<!-- 横行滚动条 -->
<div class="ace_scrollbar ace_scrollbar-h">
<div class="ace_scrollbar-inner">&nbsp;</div>
</div>

</div>

4. 整体对比


4.1 功能完整度


类别Monaco EditorCode MirrorAce
代码主题内置 3 种,可扩展基于扩展来支持,现有官方 1 种内置 20+,可扩展
语言内置 70+, 可扩展基于扩展来支持,现有官方 16 种内置 110+,可扩展
代码提示/自动补全只支持 4 种语言,官方提供了自动补全的基础插件,可自行实现基于扩展来支持,官方提供了自动补全的基础插件只支持 4 种语言,官方提供了自动补全的基础插件,可自行实现
代码折叠
快捷键
多光标编辑
代码检查只支持 4 种语言,官方提供了自动补全的基础插件,可自行实现基于扩展来支持,官方提供了代码检查的基础插件只支持 4 种语言,官方提供了自动补全的基础插件,可自行实现
代码对比❌,需自己扩展
MiniMap❌,需自己扩展❌,需自己扩展
多文本管理❌,需自己扩展
多视图❌,需自己扩展
协同编辑可引入额外插件支持 github.com/convergence…架构支持
移动端支持

4.2 性能体验


类别Monaco EditorCode MirrorAce
核心包大小800KB 左右核心包 115 KB 左右(未压缩)200KB 左右(不同版本有轻微出入)
编辑器渲染 (无代码)400ms 左右仅核心包情况下,120ms 左右185 ms 左右(实际使用包)

5. 结论与展望


一年前我们因为Monaco Editor丰富的生态、迅猛的迭代速度、开箱即用的特性和 VSCode 同款编辑器背书等原因选择了基于它来进行二次开发和插件化定制(后续文章会对这些定制开发做分享)。但由于编辑器的使用场景日渐多样化,个性化,以及移动端的占比日渐增加,我们对 Monaco Editor 的底层支持也越来越感觉到不足和乏力。对于这些点,我们的计划是先使用CodeMirror 6来支持移动端的代码编辑,然后逐步实

作者:pdai0001525
来源:juejin.cn/post/7252589598152851517
现代码编辑器的自研。

收起阅读 »

人需借以虚名而成事

前言 自从我看冯唐老师推荐的《资治通鉴》之后,对历史有那么一丢丢的兴趣,直到在短视频里面刷到《百家讲坛》里面几位老师的讲解片段,以及跟自己经历结合,瞬间满满的共鸣,是啊时代一直在变化,而里面的为人处事、社会规则凡此种种却从来没有太大变化,这就是我们需要去学习...
继续阅读 »

bcf7ea99ded21c1a1ac9a1d85b59724c.jpeg


前言




自从我看冯唐老师推荐的《资治通鉴》之后,对历史有那么一丢丢的兴趣,直到在短视频里面刷到《百家讲坛》里面几位老师的讲解片段,以及跟自己经历结合,瞬间满满的共鸣,是啊时代一直在变化,而里面的为人处事、社会规则凡此种种却从来没有太大变化,这就是我们需要去学习历史的原因。


所以有了以下感慨。



<<百家讲坛>> 不愧是经典,整本历史歪歪斜斜写着两字:权谋,谋事、谋人,加上不确定因素,天时地利人和,最终变成命运。
人的一生就像书中一行字轻描淡写,绘成一笔~



人需借以虚名而成事




这里解释一下,人需要倚靠外部的这些名号、头衔从而能有比较大的作为。为什么这么讲呢,听我细细道来~


人生需要四行


这是王立群老师讲的,人生要想有点成就需要四行,首先你自己行,你自身本身够硬,能够做好事,其次是有人说你行,从古代的推荐机制,比如说优秀的人推荐其他优秀的人才对不对,李白的大名远扬,唐玄宗在公主引荐之下见了皇帝,第三个说你行的人要行,推荐你的那个人本身能力要强,不然没有说服力,第四是身体行,很多机会是熬出来的,比如司马懿伺候了3位君主,兢兢业业,最终才能完成自己的大业。


何为借以虚名?


1、刘备的例子,他在比较年长的时候还无法实现自己的基业,寄人篱下,他登场的时候是怎样的呢,名号叫刘皇叔,翻开长长的祖谱,八辈子打不到杆哈哈哈,不过因为当时被曹操挟持,所以需要有汉族帮忙自己,所以顺理成章的提拔上来。


2、项羽出生在一个贵族家庭。他的父亲项梁是楚国将领,曾经在楚汉战争中和刘邦一起作战。而他的母亲也是楚国的贵族,是楚王的女儿。


相比之下,刘邦的出身则比较低微。他的祖父是一个农民,父亲是一个小县官。他自己也曾经当过一个普通的县尉,但因为功绩不够而被罢免。


尽管项羽出身高贵,但他的性格比较豁达,喜欢豪情壮志和享乐人生,行事大胆不拘小节。刘邦则更加谨慎稳重,注重政治规划和长远发展。


3、王立群老师讲述过他自身的故事,小学的时候成绩优秀得到保送,后面名额被学校给了别人,自己去了私立的学校,半工半读,到了大学还是遇到了同样的遭遇,后面就到比较好的大学教书,但是有一天校长跟他讲他的成绩还是出身问题需要去另一所低一点的学校教书,他当时就哭了,命运比较坎坷的。


4、工作中,我们常常会听到,某位领导自我介绍某某大厂经历,还有某某大学毕业,还有当你投一些大厂的核心部门的时候,都会对你学历进行考核。


所以我们从上面3个例子来看,人生确实正如王立群老师讲的,需要四行,当你自己不行的时候,即使有机会也会最终流失掉;当没有推荐你的时候,也会埋没在茫茫人海中,并不是说是金子总会发光,现实是伯乐更加重要;当说你行的人没有能力的时候也不行,名牌大学、大厂经历也是这样,通过一个有名声的物体从而表示你也可以,也很厉害哈哈;最后身体是本钱,这个就不用过多的讲解了。


读历史的目的,从前人的故事、经历中,学习到点什么,化为自身的经验、阅历。


我发现现在很多人都喜欢出去玩,说的开阔视野,扩展见识,在我看来读历史才是真正的拓宽自己的眼界,原来几百年前也有人跟我一样遇到同样的问题,也有那些胸怀大志的三国名将郁郁不得志。


历史是一本书籍,它可以很薄,几句话、几行字就概括了一个朝代,它也可以很厚,厚到整个朝代的背景,比如气候情况、经济情况、外交情况、内政情况,各个出名的人物事迹,那岂是几页纸能讲得完的,是多么的厚重。


如果人不能借以虚名怎么做事?


修炼两件事情




同样百家讲坛也给出了一个比较好的见解,一个人想要成事,需要两个明,一个是高明、一是精明。高明是你有足够的眼光去发现,发现机会也好,发现危机也好,往往人生的成就需要这种高瞻远瞩的;另一个精明,是指在做事上打磨,细心主动,王阳明讲的知行合一,里面很关键的一点就是事上练,方法论在实战中去验证总结。


心态上练




李连杰在采访中说了这么一个耐人寻味的故事,他发现不管是有钱的人还是没有钱的人都会生气,比如一些老板在为几百万损失破口大骂,也有人因为几万的丢失是否生气,也有人丢了几块钱很愤怒。他觉得人都会愤怒,只是那个导致他愤怒的级别不一样而已。


他早年给自己定目标,发现当达到目标之后,发现还是有人比自己厉害,所以即使达到目标也没有想象快乐;即使有钱人无法解决痛苦的问题,比如生老病死。


所以人生意义在哪里?


我认为如果你奔着成事的想法,多修炼上面两件事,如果没有发展机会,不如躺平,积累自己的能力,就像道德经里面的无为而为,而心态的修炼是人生终极修炼,因为生死才是人生最大的难关,人应该怎么脱离时间束缚,不受衰老影响。


恰恰历史给了我们启示,要有所作为,能够为人类历史上留下精神粮食,做出浓厚贡献,就像我们都没有见过孔子、老子吧,但是他们的精神至今都在影响着我们。


作者:大鸡腿同学
来源:juejin.cn/post/7252251628090490917
收起阅读 »

前一阵闹得沸沸扬扬的IP归属地,到底是怎么实现的?

大家好,我是王老师,一直在准备写这篇稿子,但是事情太多一直耽误了,导致一直拖一直拖,结果就从最近变成了前一阵子。这下好了,不会有人说我蹭热度了。 大家都知道,前一阵子抖音和微博开始陆续上了IP归属地的功能,引起了众多热议。有大批在国外的老铁们开始"原形毕露...
继续阅读 »

大家好,我是王老师,一直在准备写这篇稿子,但是事情太多一直耽误了,导致一直拖一直拖,结果就从最近变成了前一阵子。这下好了,不会有人说我蹭热度了。


image.png


image.png


大家都知道,前一阵子抖音和微博开始陆续上了IP归属地的功能,引起了众多热议。有大批在国外的老铁们开始"原形毕露",被定位到国内来,那么IP归属到底是怎么实现的呢?那么网红们的归属地到底对不对呢?这篇文章帮大家揭晓。


一.第一步:如何拿到用户的真实IP


大家都知道,我们一般想访问公网,一般必须具备上网环境,那么我们开通宽带之后,运营商会给我们分配一个IP地址。一般IP地址我们都是自动分配的。所以我们不知道本机地址是什么?想知道自己的ip公网地址,可以通过百度搜索IP查看自己的ip位置
image.png


那么问题来了。百度是怎么知道我的公网IP的?


一般情况,用户访问我们的服务网络拓扑如下:


image.png


用户通过域名或者IP访问门户,然后请求到后端服务。这样的话后端服务就可以通过request.getRemoteAddr();方法获取用户的ip。


SpringBoot获取IP如下:


@RestController
public class IpController {

  @RequestMapping("/getIp")
  public String hello(HttpServletRequest request) {
      String ip = request.getRemoteAddr();
      System.out.println(ip);
      return ip;
  }
}

将服务部署到服务端,然后请求该接口,即可获取IP信息,如下图:


image.png


但是为什么我们获取的IP和百度搜出来的不一样呢?


1.1内网IP和外网IP


打开电脑CMD,输出ipconfig命令,查看本机的IP地址,发现我们本机地址和程序获取的地址是一样的。


image.png


其实,网络也是分内网IP和公网IP的。内网也成局域网。对于像公司,学校这种一般内部建立自己的局域网,对内部的信息进行传输时,都是通过内网相互通讯,建立局域网内网通讯节省了公网IP资源,并且通信效率也有很大的提升。当然非局域网内的设备则无法向内网的设备发送信息。


但是机器想要访问互联网的资源时,则需要机器拥有外网带宽,也就是我们所说的分配公网IP,负责也是无法访问互联网资源的。


image.png


因此,我们把服务部署在同一局域网内,客户端使用内网进行通信,因此获取的就是内网IP地址。但访问百度是需要使用公网访问,因此百度搜出来的IP就是公网IP地址。


1.2.为什么有时候获取到的客户端IP有问题?


当我们兴致勃勃的把IP获取的功能搞上去之后,发现获取的IP都是同一个?这是为什么呢?不可能只是一个用户在访问呀?查询IP信息之后发现,原来是我们部署的一台负载均衡的IP地址。


image.png


那么后端服务获取的地址都是负载均衡如nginx的地址。那么怎么透过负载均衡获取真实的地址呢?


透明的代理服务器在将客户端的访问请求转发到下一环节的服务器时,会在HTTP的请求头中添加一条X-Forwarded-For记录,用于记录客户端的IP,格式为X-Forwarded-For:客户端IP。如果客户端和服务器之间有多个代理服务器,则X-Forwarded-For记录使用以下格式记录客户端IP和依次经过的代理服务器IP:X-Forwarded-For:客户端IP, 代理服务器1的IP, 代理服务器2的IP, 代理服务器3的IP, ……


因此,常见的Web应用服务器可以通过解析X-Forwarded-For记录获取客户端真实IP。


public static String getIp(HttpServletRequest request) {
  String ip = request.getHeader("x-forwarded-for");

  if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
      ip = request.getRemoteAddr();
  } else if (ip.length() > 15) {
      //多次反向代理后会有多个ip值,第一个ip才是真实ip
      String[] ips = ip.split(",");
      for (int index = 0; index < ips.length; index++) {
          String strIp = ips[index];
          ip = strIp;
          break;
      }
  }
  return ip;
}

第二步:如何解析IP


IP来了,我们怎么解析呢:


IP的解析一般都要借助第三方软件使用了,第三方一般也分为离线库和在线库



  • 离线库支持的有如:IPIP,使用离线库的好处是解析效率高,性能好,问题就是IP库要经常更新。如果大家需要我私信我可以提供给大家比较新版本的ip库。

  • 在线库则各大云厂商接口能力都有支持。在线版本的好处是更新即时,问题就是接口查询性能和使用TPS有要求。


以下演示借助IP库离线IP解析方式:


借助IP库就可以帮我们实现ip地址的解析。


public static void main(String[] args) {
  IpAddrInfo IpAddrInfo = IPAddr.getInstance().putLocInfo("114.103.71.226");
  System.out.println(JSONObject.toJSONString(IpAddrInfo));
}

public IpAddrInfo putLocInfo(String ip) {
  IpAddrInfo info = new IpAddrInfo();
  if (StringUtils.isNotBlank(ip)) {
      try {
          DistrictInfo addrInfo = db.findInfo(ip, "CN");
          info.setCity(addrInfo.getCityName());
          info.setCountry(addrInfo.getCountryName());
          info.setCountryCode(addrInfo.getChinaAdminCode());
          info.setIsp(addrInfo.getIsp());
          info.setLat(addrInfo.getLatitude());
          info.setLon(addrInfo.getLongitude());
          info.setProvince(addrInfo.getRegionName());
          info.setTimeZone(addrInfo.getTimeZone());
          System.out.println(addrInfo.toString());
      } catch (IPFormatException e) {
          e.printStackTrace();
      } catch (InvalidDatabaseException e) {
          e.printStackTrace();
      }
  }
  return info;
}

image.png


其实IP的定位解析其实就是一个巨大的位置库,同时IP数量也是有限制的,因此同一个Ip也可能会分配到不同的区域,因此影响IP解析位置准确率的有几个方面
1、位置库不精准,导致解析偏差大或者地区字段确实
2、离线库更新不及时
并且海外的一般有专门的离线库去支持,使用同一套离线库并不一定支持海外IP的解析,所以本次受影响最大的海外网红门被解析到中国各个地区,被大家认为造假,当然也包括真的有造假。不过上线了这个功能也是有好处的,至少网络不是法外之地,大家也要有序的健康的冲浪,拒绝网络暴力。


好了,今天就到这里,我是王老狮,一个有想法有内涵的工程狮,关注我,学习更多技术知识。


收起阅读 »