注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Kotlin1.8新增特性,进来了解一下

大家好,之前我已经写过了分析kotlin1.5、1.6、1.7、1.9插件版本新增的一些特性,唯独kotlin1.8的特性还没好好讲讲,本篇文章就带大家好好分析下kotlin1.8新增了那些特性,能对我们日常开发带来哪些帮助。 其中Kotlin1.8.0提供的...
继续阅读 »

大家好,之前我已经写过了分析kotlin1.5、1.6、1.7、1.9插件版本新增的一些特性,唯独kotlin1.8的特性还没好好讲讲,本篇文章就带大家好好分析下kotlin1.8新增了那些特性,能对我们日常开发带来哪些帮助。


其中Kotlin1.8.0提供的特性有限,本篇文章主要是分析Kotlin1.8.20提供的一些新特性。下面是支持该插件的IDE对应版本:



一. 提供性能更好的Enum.entries替代Enum.values()



在之前,如果我们想遍历枚举内部元素,我们通常会写出以下代码:


enum class Color(val colorName: String, val rgb: String) {
RED("Red", "#FF0000"),
ORANGE("Orange", "#FF7F00"),
YELLOW("Yellow", "#FFFF00")
}

fun main() {
Color.values().forEach {
println("${it.rgb}--${it.colorName}--${it.name}")
}
}

但是不知道大家是否清楚,Color.values() 其实存在性能问题,换句话说,每调用一次该方法,就会触发重新分配一块内存,如果调用的频率过高,就很可能引发内存抖动


我们可以反编译下枚举类简单看下原因:



Color.values()每次都会调用Object.clone()方法重新创建一个新的数组,这就是上面说的潜在的性能问题,github上也有相关的问题链接,感兴趣的可以看下:HttpStatus.resolve allocates HttpStatus.values() once per invocation


同时Color.values()返回的是一个数组,而在我们大多开发场景中,可能集合使用的频率更高,这就可能涉及到一个数组转集合的操作。


基于以上考虑,Kotlin1.8.20官方提供了一个新的属性:Color.entries这个方法会预分配一块内存并返回一个不可变集合,多次调用也不会产生潜在的性能问题


我们简单看下使用:


fun main() {
Color.entries.forEach {
println("${it.rgb}--${it.colorName}--${it.name}")
}
}

输出:



同时我们也可以从反编译的代码中看出区别:



不会每次调用都重新分配一块内存并返回。


如果想要使用这个特性,可以加上下面配置:


compileKotlin.kotlinOptions {
languageVersion = "1.9"
}

另外多说一下,IntelliJ IDEA 2023.1版本也会检测代码中是否存在Enum.values()的使用,存在就提示使用Enum.entries代替。


二. 允许内联类声明次级构造函数



内联类在Kotlin1.8.20之前是不允许带body的次级构造函数存在的,也就是说下面的代码运行会报错:


@JvmInline
value class Person( val fullName: String) {
constructor(name: String, lastName: String) : this("$name $lastName") {
check(lastName.isNotBlank()) {
"Last name shouldn't be empty"
}
}
}

fun main() {
println(Person("a", "b").fullName)
}

运行看下结果:



如果没有次级构造函数body,下面这样写是没问题的:


    constructor(name: String, lastName: String) : this("$name $lastName") 

如果想要支持带body的次级构造函数,只需要在kotlin1.8.20插件版本上和上一个特性一样增加languageVersion = "1.9"配置即可。


然后上面的代码块运行就没问题了,我们看下输出:


fun main() {
println(Person("a", "").fullName)
}


准确的执行了次级构造函数body内的逻辑。


三. 支持java synthethic属性引用



这个特性用文字不好解释,我们直接通过代码去学习下该特性。


当前存在一个类Person1


public class Person1 {
private String name;
private int age;

public Person1(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}
}

在Kotlin1.8.20之前,以下这种写法是会报错的:



而是必须改成sortedBy(Person1::getAge)才能运行通过。


和上面特性一样,如果想要支持Person1::age这种引用方式,只需要在kotlin1.8.20插件版本上和上一个特性一样增加languageVersion = "2.1"配置即可。



PS:请注意,Kotlin官方网站提示配置languageVersion = "1.9" 就能使用上面的实验特性,但是编译器还是提示报错,然后你找报错提示信息改成了languageVersion = "2.1" 就正常了。




四. 新Kotlin K2编译器的更新



就是说目前Kotlin K2编译器还是一个实验阶段,不过Kotlin官方在其stable的路上又增加了一些更新:



  1. 序列化插件的预览版本;

  2. JS IR编译器的alpha支持;

  3. Kotlin2.0版本特性的引入;


如果大家想要体验下最新版的Kotlin K2编译器,增加配置:languageVersion ="2.0"即可。


五. Kotlin标准库支持AutoCloseable



这个AutoCloseable 接口就是用来支持资源关闭的,搭配提供的use扩展函数,就能帮助我们在资源流使用完毕后自动关闭。


Kotlin之所以在标准库中支持,应该是想要支持多平台吧。


六. Kotlin标准库支持Base64编解码


这里不做太多介绍,看下面的使用例子即可:



七. Kotlin标准库@Volatile支持Kotlin/Native


@Volatile注解在Kotlin/JVM就是保证线程之间可见性以及有序性的,kotlin官方在Kotlin/Native中也支持了该注解使用,有兴趣的可以实战试下效果。


总结


本篇文章主要是介绍了Kotlin1.8版本新增的一些特性,主要挑了一些我能理解的、常用的一些特性拉出来介绍,希望能对你有所帮助。


历史文章


两个Kotlin优化小技巧,你绝对用的上


浅析一下:kotlin委托背后的实现机制


Kotlin1.9.0-Beta,它来了!!


聊聊Kotlin1.7.0版本提供的一些特性


聊聊kotlin1.5和1.6版本提供的一些新特性


kotlin密封sealed class/interface的迭代之旅


优化@BuilderInference注解,Kotlin高版本下了这些“毒手”!


@JvmDefaultWithCompatibility优化小技巧

,了解一下~

收起阅读 »

在高德地图实现卷帘效果

web
介绍 今天介绍一个非常简单的入门级小案例,就是地图的卷帘效果实现,各大地图引擎供应商都有相关示例,很奇怪高德居然没有,我看了下文档发现其实也是可以简单实现的,演示代码放到文末。本文用到了图层掩模,即图层遮罩,让图层只在指定范围内显示。 实现思路 1.创建目标图...
继续阅读 »

介绍


今天介绍一个非常简单的入门级小案例,就是地图的卷帘效果实现,各大地图引擎供应商都有相关示例,很奇怪高德居然没有,我看了下文档发现其实也是可以简单实现的,演示代码放到文末。本文用到了图层掩模,即图层遮罩,让图层只在指定范围内显示。


实现思路


1.创建目标图层,这里除了有一个默认的底图,还增加了卫星影像图和路网图层,后两者是可以被掩模的。因此在创建图层时通过设置rejectMapMask(默认值false)让图层是否允许被掩模。


2.提供实时设置掩模的方法renderMask,核心代码只需要map.setMask(mask)。


3.实现拖拽交互逻辑,监听拖拽过程,实时触发 renderMask


实现代码


1.创建目标图层


// 基础底图
const baseLayer = new AMap.TileLayer({
zIndex: 1,
//拒绝被掩模
rejectMapMask: true,
})

map = new AMap.Map('container', {
center:[116.472804,39.995725],
viewMode:'3D',
labelzIndex:130,
zoom: 5,
cursor:'pointer',
layers:[
// 底图,不掩模
baseLayer,
// 路网图层
new AMap.TileLayer.RoadNet({
zIndex:7
}),
// 卫星影像图层
new AMap.TileLayer.Satellite()
]
});

2.提供实时设置掩模的方法


function renderMask(){
// 当前地图范围
const {northEast, southWest} = map.getBounds()
// 地理横向跨度
const width = northEast.lng - southWest.lng
// 拖拽条位置占比例
const dom = document.querySelector('#dragBar')
const ratio = Math.ceil(parseInt(dom.style.left) + 5) / map.getSize().width

let mask = [[
[northEast.lng, northEast.lat],
[southWest.lng+ width * ratio, northEast.lat],
[southWest.lng+ width * ratio, southWest.lat],
[northEast.lng, southWest.lat]
]]

map.setMask(mask)
}

3.实现拖拽交互逻辑


// 拖拽交互
function initDrag(){

const dom = document.querySelector('#dragBar')
dom.style.left = `${map.getSize().width/2}px`

// const position = {x:0, y:0}
interact('#dragBar').draggable({
listeners: {
start (event) {
// console.log(event.type, event.target)
},
move (event) {
// 改变拖拽条位置
const left = parseFloat(dom.style.left)
const targetLeft = Math.min(Math.max(left + event.dx, 0), map.getSize().width - 10)
dom.style.left = `${targetLeft}px`

if(event.dx !== 0){
renderMask()
//必须!强制地图重新渲染
map.render()
}
},
end(event){
// console.log(event.type, event.target)
}
}
})
}


  1. 启动相关方法,完善交互逻辑


initDrag()
renderMask()
map.on('mapmove', renderMask)
map.on('zoomchange', renderMask)
window.addEventListener('resize', renderMask)

相关链接


本文代码演示


jsfiddle.net/gyratesky/z…


maptalks 图层卷帘效果


maptalks.org/examples/cn…


卫星+区域掩模


lbs.amap.com/demo/j

avasc…

收起阅读 »

2023一只前端菜鸡的年中总结

关于一只前端菜鸡陈平安的年中总结 第一次写笔记还是多多少少有点紧张的 其实说实话总是感觉自己还在2022年 一转眼2023也过了一半了 不多bb 直接开说 平安的前半年的魔幻经历 裁员 虽然知道今年互联网行业工作不是特别好找 但是我当时也没想到裁员会跟我有关...
继续阅读 »

关于一只前端菜鸡陈平安的年中总结


第一次写笔记还是多多少少有点紧张的
其实说实话总是感觉自己还在2022年 一转眼2023也过了一半了 不多bb 直接开说 平安的前半年的魔幻经历


微信图片_20230626103447.jpg


裁员


虽然知道今年互联网行业工作不是特别好找 但是我当时也没想到裁员会跟我有关 时间还要回到今年5月
22日,当时的平安刚从老家回到公司(当时因为家里人出了点事情 所以请了几天假回了趟老家)大概就是在24日的时候,平安还在工位上安安心心的当码农码代码呢 然后就看见我对面的后端大哥(他说自己是李小白 那我们这里姑且也叫他小白大哥)被叫人事叫出去,当时平安还没想那么多 以为就是单纯的让搬个东西之类的(平安和小白大哥经常干的事情,毕竟大部分同事都是女生嘛)就在平安认真工作的时候技术负责人(其他板块的,因为我们公司就一个前端一个后端 没有技术负责人,让我去交接工作 我当时心里暗道不妙 会不会是平安经常摸鱼被谁告诉老板了(开个玩笑)然后平安正打算开始准备交接,小白大哥回来了,然后平安就被人事叫过去谈话了,巴拉巴拉一大堆(就是公司不做互联网这一块儿了 所以不需要技术人员了),然后就是喜提裁员offer一份,交接工作差不多进行到25号才彻底交接完。


放松


其实被裁当时没啥感觉,当时想的就是变相的给自己放个小长假,然后当时跟小白大哥久违的吃了顿饭(其实也是第一次)然后在吃饭附近的河边和小白大哥一起敞开心扉的聊天散步,顺便附上当时拍的照片(平安也是喜欢记录生活热爱生活的前端菜鸟)


微信图片_20230626105940.jpg


微信图片_20230626105947.jpg


微信图片_20230626105953.jpg


微信图片_20230626105959.jpg


投简历 面试


之后几天也就跟大家想的一样,在boss直骗上投简历、约面试 短短几天大概约了四五家面试,然后就是去面试 不面还好,面完之后平安都快怀疑人生了,其中有一家面的不错 也在约二面 可惜面完之后就没有消息了 所谓的二面也可能是当时的说辞吧,然后就是现在平安待的这家公司(当时面试的时候状态不太好,面试过程的话不太那啥 你们懂的)面完之后我对现在这家公司已经不抱太大希望了 当时面试心态出了点问题就想着摆烂一段时间再继续面试吧 然而在我刚开始摆的第二天,突然看见boss上有一条消息发过来,点开一看是之前面试的这家公司,老板说我的面试通过了 然后要加我vx说发一下offer和入职需要的材料(只能说平安的运气还是不错的 哈哈哈)


庆祝 准备入职


当时收到offer的时候,那种开心的喜悦,当天晚上也不想睡觉了 就直接叫小白大哥去海边(当时已经晚上10点多了) 附上聊天截图 哈哈哈


微信图片_20230626113622.jpg


微信图片_20230626113645.jpg


微信图片_20230626113648.jpg
然后就是去找小白大哥 一起去青岛的三浴(该说不说青岛的三浴风景还是不错的 就是大半夜视线不太好)


微信图片_20230626113925.jpg


微信图片_20230626113935.jpg


微信图片_20230626113942.jpg


微信图片_20230626113946.jpg


微信图片_20230626113957.jpg


微信图片_20230626114002.jpg


微信图片_20230626114007.jpg


入职


6月5日平安在现在这家公司入职了 总得来说这周是在公司的第四个周 马上就快一个月了(时间过的真快啊) 公司人都挺好的 出了问题问的话也都会跟自己讲 总的来说 希望自己在这家公司ke


作者:前端菜鸟陈平安
来源:juejin.cn/post/7248812926176362554
收起阅读 »

正则别光想着抄,看懂用法下次你也会写

web
前言 大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题。 日常开发中,应该很多人都经常会使用正则表达式去校验字符串。但是总是遇到复杂的表达式就从网上抄了就结束了,下次写还是不会,今天我们就来看两个稍微复杂一点的案例,从案例中学会一...
继续阅读 »

前言


大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题


日常开发中,应该很多人都经常会使用正则表达式去校验字符串。但是总是遇到复杂的表达式就从网上抄了就结束了,下次写还是不会,今天我们就来看两个稍微复杂一点的案例,从案例中学会一些高级的正则表达式用法。


校验字符串是否包含大小写字母+数字+特殊字符,并且长度为8-12。


如果想要使用单个正则表达式就解决上述问题,就需要稍微学习一下正则的一些高级用法了。


^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[a-zA-Z\d!@#$%^&*()_+]{8,12}$

先行断言(预搜索)


先行断言中不会获取任何内容,只是做一次筛查



  • 正向先行 指在某个位置向右看,该位置必须能匹配该表达式(?=表达式)。

  • 反向先行 指在某个位置往右看,该位置保证不能出现的表达式。

  • 正向后行 指在某个位置向左看,该位置必须能匹配该表达式,但不会获取表达式的内容(?<=表达式)

  • 反向后行 指在某个位置往左看,该位置保证不能出现的表达式(?<!表达式)


这个正则表达式使用了正向先行断言来同时检查字符串中是否包含大小写字母、数字和特殊符号。它的含义如下:



  • ^:匹配字符串的开头。

  • (?=.*[a-z]):正向先行断言,要求字符串中至少包含一个小写字母。

  • (?=.*[A-Z]):正向先行断言,要求字符串中至少包含一个大写字母。

  • (?=.*\d):正向先行断言,要求字符串中至少包含一个数字。

  • (?=.*[!@#$%^&*()_+]):正向先行断言,要求字符串中至少包含一个特殊符号(这里列出了一些常见的特殊符号,你可以根据需要添加或修改)。

  • [a-zA-Z\d!@#$%^&*()_+]:匹配允许的字符集合,包括大小写字母、数字和特殊符号。

  • {8,12}:限定字符串的长度在 8 到 12 位之间。

  • $:匹配字符串的结尾。


使用这个正则表达式可以对目标字符串进行检查,判断是否满足包含大小写、数字和特殊符号,并且长度为 8 到 12 位的要求。例如:


let regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[a-zA-Z\d!@#$%^&*()_+]{8,12}$/;
let str = "Password123!";
let isMatch = regex.test(str);
console.log(isMatch); // 输出: true

获取ip地址


当处理日志文件时,有时需要从日志文本中提取特定的信息。一个常见的场景是提取日志中的 IP 地址。


假设我们有一个日志文件,其中包含了多行日志记录,每行记录的格式如下:


[2023-06-26 10:15:25] [INFO] Access from IP: 192.168.0.1 to URL: /home

在上述示例中,我们使用 match 方法来执行正则表达式匹配,并将匹配的结果存储在 match 变量中。如果有匹配结果,我们可以从数组中取得第一个元素 match[0],即提取到的 IP 地址。


let logText = "[2023-06-26 10:15:25] [INFO] Access from IP: 192.168.0.1 to URL: /home";
let regex = /\b(?:\d{1,3}\.){3}\d{1,3}\b/;
let match = logText.match(regex);
if (match) {
let ipAddress = match[0];
console.log(ipAddress); // 输出: 192.168.0.1
} else {
console.log("No IP address found.");
}

非捕获型分组


非捕获型分组是正则表达式中的一种分组语法,用于对一组子表达式进行逻辑组合,但不会捕获匹配的结果。它以 (?: 开始,并以 ) 结束。


/\b(?:\d{1,3}\.){3}\d{1,3}\b/

解释一下这个正则表达式:



  • \b:单词边界,用于确保 IP 地址被完整匹配。

  • (?:\d{1,3}\.){3}:非捕获型分组,匹配由 1 到 3 个数字和一个点组成的序列,重复匹配 3 次,用于匹配 IP 地址的前三个数字和点的部分。

  • \d{1,3}:匹配由 1 到 3 个数字组成的序列,用于匹配 IP 地址的最后一个数字。

  • \b:单词边界,用于确保 IP 地址被完整
    作者:simple_lau
    来源:juejin.cn/post/7248832185808617509
    匹配。

收起阅读 »

阿里巴巴高管换血,吴永明接替张勇

🩸 阿里巴巴高管换血,吴永明接替张勇 Alibaba announced that Eddie Yongming Wu would replace Daniel Zhang as chief executive in September. Mr. Zhang...
继续阅读 »

🩸 阿里巴巴高管换血,吴永明接替张勇


Eddie Yongming Wu & Joe Tsai


Alibaba announced that Eddie Yongming Wu would replace Daniel Zhang as chief executive in September. Mr. Zhang will retain control of the cloud division. Mr. Wu is one of the founders of the Chinese internet giant and is a close friend of Jack Ma, another founder, as is Joe Tsai, who takes over Mr. Zhang’s other role as chairman. In March the company announced plans to turn itself into a holding company overseeing six divisions.


阿里巴巴宣布吴永明(Eddie Yongming Wu)将在 9 月份接替张勇(Daniel Zhange)成为首席执行官。张勇将保留对云事业部的控制权。吴永明是中国互联网巨头阿里巴巴的创始人之一,也是马云的好朋友,另外一个创始人,蔡崇信(Joe Tsai),将会接替张勇的另外一个董事长职位。 在三月份,阿里巴巴宣布计划将自身转化为管理六个事业部的控股公司。




阿里巴巴今年 3 月份的重组计划是?

阿里巴巴在今年3月份宣布了一项重大的重组计划。该公司计划将其业务拆分为六个独立的业务单元,每个单元都将由自己的首席执行官和董事会负责管理。这六个新的业务单元包括:云智能集团、淘宝天猫商业集团、本地服务集团、菜鸟智能物流、全球数字商务集团,以及数字媒体和娱乐集团。

阿里巴巴表示,这次重组的目的是为了提高公司的敏捷性,加快决策速度,以及更快地应对市场变化。此外,除了淘宝天猫商业集团将完全由阿里巴巴所有外,其他五个新的业务组都将有可能寻求外部资金,并有可能进行自己的首次公开募股(IPO)。




🌄 孙正义再出山


Son Masayoshi


Son Masayoshi used his first public appearance in seven months to talk up SoftBank’s investments in artificial intelligence. Displaying his typical exuberance, the boss of the Japanese tech conglomerate told shareholders that “a huge revolution is coming” in AI and that SoftBank would “rule the world” in its development. After a period of reflection following heavy losses at its flagship Vision Funds, Mr. Son said he now wants to become “an architect for the future of humanity”.


孙正义(Son Masayoshi)在七个月来首次公开露面时,大肆宣扬软银在人工智能方面的投资,这家日本科技集团的老板告诉股东,人工智能领域“一场巨大的革命即将到来”,软银将在其发展过程中“统治世界”。在旗舰愿景基金遭受重大损失后,经过一段时间的反思,孙正义表示,他现在想成为“人类未来的建筑师”。



  • exuberance /ɪɡˈzuːbərəns/
    源自拉丁词 "exuberare",意为 "充溢"。在英语中,"exuberance" 通常用来描述非常活泼、充满活力、兴奋或者情绪高昂的状态。
    当用于描述人时,"exuberance" 可以指一个人充满活力,情绪高涨,非常乐观和充满热情。例如,"a child's exuberance"(一个孩子的活力)或者 "his exuberance was infectious"(他的热情感染了所有人)。




软银是一家什么公司?

软银集团(SoftBank Group Corp.)是一家总部位于日本的全球领先的科技公司,由孙正义于 1981 年创立。起初,软银主要从事电脑软件销售和发布,而现在,软银已经扩展到各种不同的科技领域,包括电信、互联网服务、人工智能和机器人技术等。

软银最为人所知的可能是其投资活动。它设立了一系列的投资基金,最有名的就是其愿景基金,这是全球最大的科技领域私募股权基金,投资了包括阿里巴巴、滴滴、今日头条、饿了吗、贝壳找房、Uber、Slack 等在内的许多知名科技公司。






孙正义是谁?

孙正义(Masayoshi Son)是一位杰出的商业领袖和投资者,他是软银集团的创始人、董事长和首席执行官。他在科技投资领域有着重要影响力,并以他的大胆投资和对未来的远见而闻名。

孙正义于 1957 年出生在日本九州的一个朝鲜族移民家庭。他在青少年时期就展现出了对企业家精神的热情和对科技的兴趣。他在 17 岁时移居到美国,并在加利福尼亚大学伯克利分校学习计算机科学和经济学。

1981 年,孙正义创立了软银,起初是一个软件分销公司。然而,他很快看到了互联网可能带来的巨大机会,于是转向了投资领域。在 90 年代,他进行了一系列投资,其中最有影响力的就是对阿里巴巴的早期投资,使软银从阿里巴巴的 IPO 中获得了巨大收益。

在他的个人生活中,孙正义是一位已婚人士,他与他的妻子有两个孩子。他是世界上最富有的人之一,他的财富主要来源于他在软银和其投资的公司中的股权。




🐿️ 英特尔加码德国!投资330亿美元建两座芯片工厂!


Intel


Intel doubled the amount it is investing in two new chip-making factories in Germany to $33bn after the German government agreed to increase subsidies for the project to €10bn ($11bn). It is the largest-ever foreign investment in Germany and the biggest bid by a European country to enter the chip war, following America’s enticement of chipmakers.


在德国政府同意增加到 100 亿欧元(约合 110 亿美元)补贴后,英特尔将在德国的的两座新芯片制造工厂投资翻倍。这是德国有史以来最大的外国投资,也是欧洲国家进入芯片战争的最大努力,相应美国吸引芯片制造商的橘洲。



  • subsidy /ˈsʌbsədi/
    在经济和政策语境中通常被翻译为“补贴”。补贴是政府给予企业或个人的一种经济援助,目的是为了鼓励特定的经济活动或保护特定的行业。补贴可以以各种形式出现,例如税收优惠、低息贷款、直接现金支付等。补贴可以帮助降低生产成本,使得企业能够以低于市场价的价格生产商品或提供服务,或者鼓励企业进行某种特定的活动,例如研发、环保改造等。

  • bid /bɪd/
    在上文中,"bid" 的意思是尝试或竞争。这里的 "biggest bid" 指的是欧洲国家在尝试进入芯片战争方面的最大努力。
    在其他上下文中,"bid" 可能有不同的含义。例如,在商业环境中,"bid" 可以指出价或竞标,即在拍卖或合同竞标中提出的价格。在桥牌或其他一些卡牌游戏中,"bid" 可以指出牌或叫牌。在股票市场中,"bid" 可以指买方愿意支付的价格。

  • enticement /ɪnˈtaɪsmənt/
    指吸引或诱惑某人做某事的行为或方法。这个词通常带有积极的含义,描述的是通过提供奖励或激励来吸引某人做某事。在上文中,“America’s enticement of chipmakers” 可以理解为美国通过各种方式(例如税收优惠、补贴、优惠政策等)吸引芯片制造商的行为。




英特尔是一家什么公司?

英特尔(Intel)是一家总部位于美国加利福尼亚州圣克拉拉的跨国科技公司,成立于 1968 年。该公司是全球最大的半导体芯片设计和制造公司之一,以及计算机技术领域的领导者之一。

英特尔的产品范围包括中央处理器(CPU)、芯片组、网络处理器、闪存存储设备、以太网产品、无线通信产品等。其中,英特尔的处理器产品在全球范围内广泛应用于个人电脑、服务器、笔记本电脑、移动设备等领域。它们以高性能、低功耗、高可靠性和安全性著称,并且在业界有着广泛的应用和支持。

除了芯片设计和制造,英特尔还在计算机技术领域进行了广泛的研究和开发,包括人工智能、物联网、高性能计算、自动驾驶技术等。此外,英特尔还积极开展社会责任活动,致力于推动数字包容、可持续性和 STEM 教育等方面的发展。

作为一家跨国公司,英特尔在全球范围内设有多个研发中心、工厂和办事处,拥有来自各个国家和地区的员工。




🌊 亚马逊被指控强加 Prime 服务


Amazon


America’s Federal Trade Commission sued Amazon for allegedly enrolling customers into its Prime service without their consent and for making it hard for them to cancel.


美国联邦贸易委员会起诉亚马逊未经用户同意就擅自将用户注册到它的 Prime 服务中,并且让用户很难去取消这项服务。




Prime 服务是什么?

Prime服务是亚马逊提供的订阅服务,主要涵盖了快速免费配送、免费电子书借阅、电影和电视节目的流媒体服务、以及其他一些特权。Prime服务需要用户每年支付一定的订阅费用,不同国家和地区的价格可能会有所不同。





  • allegedly /əˈledʒɪdli/ 指某件事情尚未被证实或确定,仅仅是根据某些证据或指称所做的推测或猜测。因此,当我们说某事“allegedly”发生时,我们并不肯定它是否真的发生了,但是有足够的理由相信它发生了。在法律文书或新闻报道中,"allegedly"常被用来描述涉嫌犯罪的人或行为,以表明尚未被证实的情况。


✈️ 印度航空市场成长速度惊人,IndiGo 下单 500 架空客客机


IndiGo


IndiGo, a low­cost Indian airline, placed an order for 500 Airbus A320 passenger jets, the biggest ever deal in commercial aviation. India has become the fastest­-growing aviation market and IndiGo is the biggest domestic player.


IndiGo,一家印度廉价航空公司,订购 500 架空客 A320 乘客飞机,这是商业航空史上最大的交易。印度已经成为增长最快的航空市场,并且 IndiaGo 是国内市场最大的参与者。



  • place an order 是一个商业用语,指某个企业或个人向供应商或生产商提交了一份正式的订单,表示要购买其产品或服务。在这个语境中,IndiGo 向空客订购了 500 架客机,意味着他们已经向空客提交了一份正式的订单,表示要购买这些客机。

  • aviation /ˌeɪviˈeɪʃn/
    指航空,包括所有与飞行相关的事物。它涵盖了航空器设计、制造、维护和操作等方面的知识和技术。航空业是一个广泛的领域,包括商业航空、军事航空、私人航空、航空运输和通用航空等。




IndiGo 是印度一家知名的低成本航空公司,总部位于新德里。它成立于 2006 年,是印度最大的航空公司之一,也是全球最大的低成本航空公司之一。IndiGo 的航班网络覆盖印度国内以及亚洲、中东和东南亚等地区,运营着数百个航班航班,包括许多国内和国际航点。

IndiGo 的机队主要由空客 A320 系列飞机组成,这些飞机被广泛认为是最先进、最经济的单通道客机之一。IndiGo 以其高效的运营和优质的服务而闻名,旅客可以享受到舒适的座椅、免费的餐饮和便捷的登机体验。

IndiGo 在过去几年中一直保持着强劲的增长势头,成为了印度航空市场的领导者之一。它也曾多次获得国内和国际航空业的奖项和认可,包括 Skytrax 颁发的“印度最佳低成本航空公司”等奖项。




👑 丹麦再夺全球竞争力榜冠军


Annual World Competitiveness ranking


Denmark retained the top spot in the annual World Competitiveness ranking from the International Institute for Management Development (IMD). The criteria behind the ranking include international trade, government and business efficiency, and technological infrastructure. Ireland leaped from 11th place to 2nd while Britain tumbled from 23rd to 29th, dragged down by worsening productivity and rising bureaucracy.


丹麦在国际管理世界研究所的年度世界竞争力排行榜中位列第一。排名背后的标准包含国际贸易、政府和商业效率以及科技基础设施。爱尔兰从第 11 名跃升至第 2 名,同时英国从第 23 名跌至 29 名,受累于变糟糕的生产力和抬头的官僚主义。



  • top spot 英语中的一个习惯用语,意思是 "第一名" 或 "最高位置"。




年度世界竞争力排行榜(Annual World Competitiveness Ranking)是什么?

年度世界竞争力排行榜是由瑞士洛桑国际管理发展学院(IMD)发布的一份报告,旨在评估全球经济体的竞争力。IMD的竞争力排名采用了一系列指标,包括国际贸易、劳动力市场、财政政策、企业效率、技术基础设施、教育、健康、环境和创新等多个方面。

这些指标通常被认为是衡量一个国家经济实力和潜力的重要因素。通过对这些因素进行分析和比较,IMD的排名可以帮助企业和政府了解全球经济环境的趋势和变化,并制定相应的战略和政策。




🍀 国际能源署新报告:清洁能源投资需大幅增加,中国等新兴市场成为投资热点


The International Energy Agency


The International Energy Agency published a report on clean energy in developing and emerging­market economies. It said that investments in those countries would have to rise from $770bn a year to between $2.2trn and $2.8trn by the early 2030s if the Paris Agreement’s goals on carbon emissions are to be met. China accounts for two-­thirds of the current spending on clean energy, with Brazil and India taking a large bite of the rest.


国际能源署发布了一份关于发展中和新兴市场经济体中的清洁能源的报告。报告指出,如果要实现巴黎协议关于碳排放的目标,那么这些国家的投资必须从每年 7700 亿美元增加到 2030 年初的 2.2 至 2.8 万亿美元。中国占据了目前清洁能源投资的三分之二,巴西和印度占据了其余的大部分。


🤒 英国通胀率创近 31 年新高


Britain


Britain’s headline annual rate of inflation remained elevated at 8.7% in May, defying expectations that it would fall again. Core inflation, which strips out food and energy prices, rose to 7.1%, the highest it has been since March 1992. The government has promised to cut inflation by half this year from the 10.1% registered in January, but higher prices persist in food, clothing, recreation, health, communications, and travel.


在五月,英国的年度通胀率仍保持高位,达到 8.7%,违背了通胀率会再次下降的预期。剔除食物和能源价格的核心通胀率,升至 7.1%, 这是从 1997 年三月以来的最高水平。政府承诺今年将会把从一月份的 10.1% 的通胀率降至一半,但食物、服装、娱乐、健康、通信和旅行的价格仍然居高不下。




  • defy /dɪˈfaɪ/
    意思是“违抗;不服从;挑战”。在这个句子中,defy 的意思是“违反了预期”,也就是说,英国的年通胀率没有像预期那样下降,而是保持在一个较高的水平。在这种情况下,defy 表示通胀率的表现与人们的预期相反,形成了一种挑战或违反预期的局面。




  • headline 指的是新闻报道或文章的标题。通常情况下,headline 是用来吸引读者的注意力,传达文章或新闻的主要内容或亮点。在这个句子中,headline annual rate of inflation 指的是通胀率的年度平均值,是该新闻报道或文章的主要话题。




💰 最后


最新的文章会发在公众号「

作者:吴楷鹏
来源:juejin.cn/post/7248819906917122085
楷鹏」,欢迎关注 🤩

收起阅读 »

从张鑫旭大佬文章中发现了我前端知识的匮乏

web
最近翻看张鑫旭大佬的博客,发现了一篇叫《前端原生API实现条形码二维码的JS解析识别》的文章,觉得很不错,于是就把大佬的代码拷贝下来学习了下,结果就是看的我一脸懵,自信息大大受打击了。痛定思痛,于是把其中觉得有意思的地方记录下,整理成此文。 我们先看下页面是怎...
继续阅读 »

最近翻看张鑫旭大佬的博客,发现了一篇叫《前端原生API实现条形码二维码的JS解析识别》的文章,觉得很不错,于是就把大佬的代码拷贝下来学习了下,结果就是看的我一脸懵,自信息大大受打击了。痛定思痛,于是把其中觉得有意思的地方记录下,整理成此文。


我们先看下页面是怎么样的:


chrome-capture-2023-5-26.gif


功能很简单,就是复制下面的二维码图片,然后粘贴到文本框中,最后点击识别按钮,把识别二维码的结果展示到下面。


源代码:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>qrcode</title>
<style>
.area {
height: 200px;
border: 1px dashed skyblue;
background-color: #fff;
display: grid;
place-items: center;
margin-top: 20px;
}
.area:focus {
border-style: solid;
}
.area:empty::before {
content: '或粘贴图片到这里';
color: gray;
}
.button {
margin: 1rem auto;
width: 160px;
height: 40px;
font-size: 112.5%;
background-color: #eb4646;
color: #fff;
border: 0;
border-radius: 0.25rem;
margin-top: 1.5rem;
}
</style>
</head>
<body>
<div class="container">
<input id="file" class="file" type="file" accept="image/png" />
<div id="area" class="area" tabindex="-1"></div>
</div>
<p align="center">
<button id="button" class="button">识别</button>
</p>

<p id="result" align="center"></p>

<p align="center">
方便大家复制的示意图:<br /><img
src="./qrcode.png"
style="margin-top: 10px"
/>

</p>

<script>
var reader = new FileReader()
reader.onload = function (event) {
area.innerHTML = '<img src="' + event.target.result + '">'
}
document.addEventListener('paste', function (event) {
var items = event.clipboardData && event.clipboardData.items
var file = null
if (items && items.length) {
// 检索剪切板items
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile()
break
}
}
}
// 此时file就是剪切板中的图片文件
if (file) {
reader.readAsDataURL(file)
}
})

file.addEventListener('change', function (event) {
const file = event.target.files && event.target.files[0]
if (file) {
reader.readAsDataURL(file)
}
})

button.addEventListener('click', function () {
if ('BarcodeDetector' in window) {
// 创建检测器
const barcodeDetector = new BarcodeDetector({
formats: ['qr_code']
})

const eleImg = document.querySelector('#area img')
if (eleImg) {
barcodeDetector
.detect(eleImg)
.then(barcodes => {
console.log('barcodes', barcodes)
barcodes.forEach(barcode => {
result.innerHTML = `<span class="success">解析成功,结果是:</span>${barcode.rawValue}`
})
})
.catch(err => {
result.innerHTML = `<span class="error">解析出错:${err}</span>`
})
} else {
result.innerHTML = `<span class="error">请先粘贴二维码图片</span>`
}
} else {
result.innerHTML = `<span class="error">当前浏览器不支持二维码识别</span>`
}
})
</script>
</body>
</html>

背景交代完成,现在就一点一点的来分析其中代码的精妙之处。


CSS部分


tabindex = -1


<div id="area" class="area" tabindex="-1"></div>

当我看到tabindex这个属性时,完全不知道它的用法,于是我继续在张鑫旭大佬的博客中搜索,找到一篇叫《HTML tabindex属性与web网页键盘无障碍访问》的文章,这里简要说下这个属性的用法和作用。


tabindex属性是一个全局属性,也就是所有 HTML 标签都可以用的属性,比方说idclass属性等。所以,可以在div上使用。同时,这个属性是一个非常老的属性,没有兼容性问题,放心使用。


tabindex属性是一个与键盘访问行为息息相关的属性。平常可能感觉不到它的价值,但是一旦我们的鼠标坏掉了或者没电了,我们就只能使用键盘。亦或者在电视机上,或者投影设备上访问我们的网页的时候,我们只能使用遥控器。就算设备都完全正常,对于资深用户而言,键盘访问可以大大提高我们的使用效率。


当一个元素设置tabindex属性值为-1的时候,元素会变得focusable,所谓focusable指的是元素可以被鼠标或者JS focus,在 Chrome 浏览器下表现为会有outline发光效果,IE浏览器下是虚框,同时能够响应focus事件。默认的focusable元素有<a>, <area>, <button>, <input>, <object>, <select> 以及 <textarea>


但是,tabindex = -1不能被键盘的tab键进行focus。这种鼠标可以focus,但是键盘却不能focus的状态,只要tabindex属性值为负值就可以了。


因此,我们可以设置divfocus的样式,当鼠标点击div时,我们可以改变它的边框,如下:


.area:focus {
border-style: solid;
}

tabindex属性值是一个整数,它来决定被tabfocus的顺序,顺序越小越先被focus,但是 0除外,如下divfocus的顺序依次是:1,2,3。


<div id="area" class="area" tabindex="1"></div>
<div class="area" tabindex="3"></div>
<div class="area" tabindex="2"></div>

tabindex="0"又是怎么回事呢?


元素设置tabindex="-1",可以鼠标和JS可以focus,但键盘不能focus


tabindex="0"tabindex="-1"的唯一区别就是键盘也能focus,但是被focus的顺序是最后的。或者你可以这么理解,<div>设置了tabindex="0",从键盘访问的角度来讲,相对于<div>元素变成了<button>元素。


垂直居中


垂直居中是一个常用的需求了,我经常使用flex来完成:


display: flex;
align-items: center;
justify-content: center;

在大佬的文章中使用了一个新的用法:


display: grid;
place-items: center;

place-items 属性是以下属性的简写:align-itemsjustify-items


:empty::before


div元素没有内容时,.area:empty样式会生效,同时为了显示一段提示内容,使用了伪元素::before,在content写入提示内容。


.area:empty::before {
content: '或粘贴图片到这里';
color: gray;
}

JS部分


copy paste 事件


document.addEventListener('paste', function (event) {
var items = event.clipboardData && event.clipboardData.items
var file = null
if (items && items.length) {
// 检索剪切板items
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile()
break
}
}
}
// 此时file就是剪切板中的图片文件
if (file) {
reader.readAsDataURL(file)
}
})

这两个事件都属于ClipboardEvent事件(剪切板事件),还有一个cut剪切事件。


wrap.oncopy = function(event){}
wrap.oncut = function(event){}
wrap.onpaste = function(event) {}

任何软件上的内容,可以被复制粘贴,是因为软件对操作系统复制粘贴操作的实现,软件都会把复制剪切的内容存入操作系统的剪切板上。同样,浏览器也对操作系统的剪切板进行了实现,属于浏览器的自身的实现。


浏览器复制操作的默认行为是触发浏览器的 copy 事件,将 copy 的内容存入操作系统的剪切板中。


那如何干预浏览器的这种默认的复制粘贴操作呢?


可以通过event.preventDefault阻止事件的默认行为,即当触发这三个事件时,阻止对系统剪切板的数据操作。然后,我们对数据进行加工后,重新写入到剪贴板。


比如,当用户复制我们网站的内容时,可以在数据后面加一个版权的相关信息。


<div id="wrap">这是复制的复制内容</div>
<script>
var wrap = document.getElementById('wrap')
wrap.oncopy = function (event) {
// 通过copy事件监听,阻止将选中内容复制到系统剪切板上
event.preventDefault()
// 获取选中内容对象
const selection = document.getSelection()
// selection对象重构了toSring()方法,获取selection对象的选中内容
var selectContent = selection.toString()
var dealContent =
selectContent +
'转载请联系作者,内容地址:xxxxx'
// 把重写后的内容写入到剪贴板
event.clipboardData.setData('text/plain', dealContent)
}
</script>


ClipboardEvent 事件有个最重要的属性clipboardData,该属性值是DataTransfer对象,这个对象在拖拽场景中经常使用,后面会专门写一篇文章来说说这个对象。


new BarcodeDetector解析二维码


// 创建检测器
const barcodeDetector = new BarcodeDetector({
formats: ['qr_code']
})
barcodeDetector.detect(eleImg)
.then(barcodes => {
console.log('barcodes', barcodes)
barcodes.forEach(barcode => {
result.innerHTML = `<span class="success">解析成功,结果是:</span>${barcode.rawValue}`
})
})
.catch(err => {
result.innerHTML = `<span class="error">解析出错:${err}</span>`
})

浏览器提供了原生的API来解析二维码和条形码,即 Barcode Detection API


formats表示要解析那种码,如下图所示:


image.png


总结


通过学习上面的代码,可以发现自己在 css,js 方面上的不足,原因是缺乏探索性,老是用已有的知识来解决问题,或者直接去 github 上找第三方库,其实可以使

作者:小p
来源:juejin.cn/post/7248874230862233655
用最简单的方式实现。

收起阅读 »

借点钱来“救急”【多图】

背景 为什么来谈借钱这个话题呢? 是因为博主刚刚结束了追债的过程,很不愉快,而且追债跨度很长。而欠款人是自己的大学同学,这里我为了不侵犯 TA 的姓名权,称呼其为张三。 本文为了纪录整个过程,以及关于借钱话题的反思。欢迎读者留言,表达关于自己借钱的看法。 张三...
继续阅读 »

背景


为什么来谈借钱这个话题呢?


是因为博主刚刚结束了追债的过程,很不愉快,而且追债跨度很长。而欠款人是自己的大学同学,这里我为了不侵犯 TA 的姓名权,称呼其为张三


本文为了纪录整个过程,以及关于借钱话题的反思。欢迎读者留言,表达关于自己借钱的看法。


张三事件


我有一个习惯,借钱给别人或者我借别人的钱,我会有记录:记录借钱的日期,借钱的金额和归还的日期。 下面是借钱给张三的清单:


序号借钱的日期借钱的金额理应归还的日期实际归还
12019-11-27¥2200.00过两天还2020-11-29 还
22019-12-01¥1000.00下个月还2020-01-07 还
32019-12-03¥2500.00下个月还2020-01-07 还 ¥1000.00,2021-06-29 还 ¥800.00,2022-02-20 还款 ¥100【失信】
42019-12-14¥3500.00下个月还【严重失信】
52019-12-29¥2000.00下个月还【严重失信】
62020-01-17¥200.00下个月还【严重失信】
72020-04-07¥900.00(没借)--

在张三向我借第六次钱 - ¥200.00 的时候,我隐约感觉不对劲。但是他的理由是:项目款没下发,借 ¥200.00 当项目款的凑数,月底项目款到了转我。这个理由没毛病,疫情刚刚爆发,行情突然不好,可以理解;再加上大学玩得好的(一个小插曲:大学的时候,张三也借过我钱,不过都如约还我了 - 守信),所以我借给张三了。


2020-04-07.jpeg


但是,在 2020-04-07 日,张三第七次向我借款的时候,我直接明确拒绝,因为之前的钱逾期太严重了。可看下面 Deadline 系列图



中途因为延期太久,我直接按照银行大约的存款利息进行计算,不矫情了:约定2021年12月10日前一次性还清,带上利息。按照银行最低年利率1.7%, 算上一年的利息 6400 + 108 = 6508元。但是,最后的还款日期还是不能如期进行。很无语~



Deadline 一推再推:


timeline-01.jpeg


timeline-02.jpeg


timeline-03.jpeg


timeline-04.jpeg


timeline-05.jpeg


timeline-06.jpeg


timeline-07.jpeg


timeline-08.jpeg


timeline-09.jpeg


timeline-10.jpeg


timeline-11.jpeg


timeline_12.jpeg


timeline-13.jpeg


timeline-14.jpeg


timeline-15.jpeg


timeline-16.jpeg


timeline-17.jpeg


timeline__18.jpeg


timeline-19.jpeg


timeline-20.jpeg


在分期还款的过程也不是很顺利,总会或多或少延期几天半个月。而且还款的时候不主动,逾期了也不会主动跟你联系说原因,整个过程很不愉快。


好在~ 他终于按照一年期的时间还完给我了。这让我联想到了花呗、京东白条的分期付款牛逼🐮~


俗话说得好:借钱见人心,还钱见人品。 我不知道张三到底是做了什么,借了身边人不少人,而且惹得他哥和爸妈反感。自己也不想去了解,想便在之后人生漫漫路,自己应该和张三没有什么交集了~


延伸事件


也许读者会问,借钱给以前的同学而已,关系又不是很亲近。借给亲戚很安全的啦。


我再举个自家的真实例子:


俺妈在 2010 年左右借钱给她姐姐的儿子 两万八,暂且称他为李四


在借钱的一段时间内(2018年之前),这个 李四 过年都会过来给我妈 - 他亲姨 拜年。然后,在 2018 年的时候,我妈想着已经借钱过去给 李四 有七八年了,而且 李四 家里面已经盖起了新房子。我妈就跟她姐说,叫孩子也该还钱了,很久了。怎料,她姐来了句:



  • 她姐:没钱,才盖完楼;你怎么在我这个时候叫我还钱

  • 我妈:现在我也不是很富裕,啊弟借了这么多年了。你跟啊弟商量商量?



啊弟 -> 李四



双方挂了电话,过了几个小时~



  • 李四:我妈跟我讲,你叫她还钱

  • 我妈:我叫她跟你提下,不是叫她还,是叫她跟你商量,毕竟你借的

  • 李四:边度有钱,刚建楼,生意今年又不好做

  • 我妈:我这边也要用钱的,你多少还点?

  • 李四:我看你是妒忌我家建了楼吧,都说没有了...


我妈一听到 妒忌我家建了楼,气不打一处来,我要是嫉妒你,我还会借钱给你。你自己几斤几两你不知道吗。跌定心要他全部还钱...


期间还聊了什么话题我也不是很清楚。因为我妈是用方言跟娘家沟通的,我不是很懂该方言。总之,电话后,我发现我妈眼眶都红红的。


这还不是最过分的。在不久后,我妈回娘家探亲。竟然被那边的流言蜚语气哭回来了:她哥:你为什么管姐要回两万八。而且啊弟说,都还了钱给你了,打你很多次电话你都不接,你大牌吗?以后你不要回来~


完全不赞成,我妈根本不是不接电话,而是因为她忙。而且李四打我妈电话的时候,手机是放在家里面的,被我接到了:



  • 李四:我转了,你看到账没?

  • 我:我是*

  • 李四:你妈呢,叫她接电话

  • 我:在外面干活,不方便

  • 李四:我转了两万八给你妈卡里面了,叫她看看到账没

  • 我:我知道了,待会跟她说下


我可以作证,我妈不是大牌,而是忙。丟,这些人脑袋和嘴巴怎么长的~


打脸.webp


2018 年后,李四 过年再也没来过我家拜年过,在某年的国庆节来过一次,不过我不在家~本来自己就对他没什么好感,现在是心生厌恶~


当然,还是有很多准守约定人的案例:比如我妈借钱给她另外一个姐的女儿,人家就主动还款,并且每过年会来看看她的姨姨;比如我借钱给另外一个同学,TA 也很主动如约还款...


该不该借


所以,来到本文的最重点的内容了:钱,我们该不该借出去呢?


该借:



  • 对方的人品你了解

  • 对方的经济实力你清楚

  • 俗话说:借急不借穷

  • 不能影响自己的生活


不借:



  • 对方人品差,好吃懒做的人不要搭理

  • 对方信用差,比如在周围人眼里口碑不行

  • 对方跟你不常联系,一上来就管你借钱

  • 借了钱,不主动说还的人;不能再借第二次,及时止损

  • 不能被血缘关系影响你对一个人的判断,不好就是不好


pexels-karolina-grabowska-4968395.jpg

收起阅读 »

这一曲终落泉城,也始于泉城

22年的春季站在岔路口四顾茫然,灵魂里的信念仿佛已被层层迷雾遮蔽。与大多人一样,我开始了考研的复习,但是到暑假之前的种种原因,让我深刻认识到自己未来想走的路是什么,我坚信无论怎样未来还是会开启自己的代码人生。也许是冥冥之中的巧合,随意投了一份简历之后,见鬼了一...
继续阅读 »

22年的春季站在岔路口四顾茫然,灵魂里的信念仿佛已被层层迷雾遮蔽。与大多人一样,我开始了考研的复习,但是到暑假之前的种种原因,让我深刻认识到自己未来想走的路是什么,我坚信无论怎样未来还是会开启自己的代码人生。也许是冥冥之中的巧合,随意投了一份简历之后,见鬼了一样收到了一个offer,经过权衡我更加坚定了自己的选择提前入海。随后梁溪城(无锡)下开始了自己的第一份实习。


f4f07a90ecd1bfb57309dcc4b4cd243.jpg
仔细室下的春季


梁溪城内边余尺,姑苏城外不姑苏。


初出茅庐,满腔热血腾。初入社会的心态是充满着好奇和壮志满怀的,有点类似于刘姥姥进大观园的样子,但这也是大多数从农村走入社会该有的样子。


2158fb3d035ce57708e85a34b30fffc.jpg
梁溪城晴


然而实习的日子没过多久,就出现了疫情开始了居家办公的日子,居家办公的生活还挺好,哈哈,自己一直处于电脑从来没有开启的状态。但疫情下的生活也并不是很好,吃饭只能去吃盒饭,供应的关系失衡使得饭的质量以及价格都不尽人意,好在没过多久疫情就结束了,线下上班,开始接触微服务,此时此刻java狗也算是开始见识了传呼其神的微服务。

d59cf7d2e8dc4d2ab01833fc670cdb4.jpg
梁溪城雨


渴望改变的灵魂,安逸是禁锢。舒适的日子久了,总想着外面更广阔的天空,这也许是大多数青年的想法。由于长时间没有机会接触到实际的开发中,感觉天天浑浑噩噩,想着要改变的决心。第一份实习在草草的两个月结束,下一站钱塘(杭州)下看西湖。

7f0c0e7d6e1d1f29f8c735f54e15292.jpg
梁溪城黄昏


钱塘西湖波光粼,芜湖花街夜亮灯。


西湖悠悠水溢情,钱塘无情终相别。当一个人努力过后,最后没有得到自己想要的,会有少许的悲伤,微微一笑过去的终将过去。好在实习的最后一站遇到很多好的人,也算是不虚此行。


bfd71a4c180e34a9e3b8071e534ed1d.jpg
钱塘西湖黄昏


南行的最后一站,芜湖。偶然的巧合有机会去到芜湖,此次的经历也是让我想出行的心付出了行动,迈出了步伐(之前是一直很宅那种)。

961bea13150d5bc498a5634b8b80a7d.jpg
芜湖花街


白日炎炎游北平,夜幕潇潇离别行。


毕业最后一站,北京。这一站也是收获满满。现在也可以在别人面前zb,我可是见过北京城的人,虽然没有去到长城,我不是好汉,哈哈哈。


5079c035d17d5bb1e8d2c17153b8620.jpg
天坛晴


ef520aeac9d951a89abad90c78fe9d1.jpg
北京夜


曲终人落泉阳城,大明湖畔少年情。


毕业后的第一站,大明湖畔泉城。


08e656a0990b0f61557c4275163c6c8.jpg
大明湖夜


回首向来萧瑟处,归去,也无风雨也无晴。


回望过去一年,入世从懵懵懂懂到懵懂,个人的心态,观念都在改变。只要是自己想做的,认为可做的,毫不手软的作出选择,不再迷茫,不再畏手畏脚,同时也是一直坚信自己的选择。看待问题的角度,不能只看那些不好的一面,也要看到好的一面,这样的心境才会是乐观豁达。一个人真正的走向成熟的标志,就是他不愿意越来越多说,

作者:海贼梦想家
来源:juejin.cn/post/7247881750314451001
而是学会适当的闭嘴。

收起阅读 »

从 0 到 1 实现一个 Terminal 终端

web
前言 之前在我自己的项目中 打造属于你自己的 Mac(Next.js+Nest.js TS全栈项目)有同学问Terminal 组件是怎么实现的呢,现在我们就用 React+TS 写一个支持多种命令的 Terminal 终端吧。 每一步骤后都有对应的 com...
继续阅读 »

前言



之前在我自己的项目中 打造属于你自己的 Mac(Next.js+Nest.js TS全栈项目)有同学问Terminal 组件是怎么实现的呢,现在我们就用 React+TS 写一个支持多种命令的 Terminal 终端吧。



每一步骤后都有对应的 commit 记录;


源码地址:github.com/ljq0226/my-… 欢迎 Star ⭐️⭐️⭐️


体验地址: my-terminal.netlify.app/



搭建环境


我们使用 vite 构建项目,安装所需要的依赖库:



  • @neodrag/react (拖拽)

  • tailwindcss

  • lucide-react (图标)
    步骤:

  • pnpm create vite

  • 选择 React+TS 模版

  • 安装依赖:pnpm install @neodrag/react lucide-react && pnpm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p
    配置 tailwind.config.js:


/** @type {import('tailwindcss').Config} */
export default {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [],
}


仓库代码:commit1


开发流程


搭建页面


image.png


以上是终端的静态页面,样式这里就不在详细展开了,此次代码仓库 commit2 。 接下来我们为该终端添加拖拽效果:


//App.tsx
···
import type { DragOptions } from '@neodrag/react'
import { useRef, useState } from 'react'

function APP(){
const [position, setPosition] = useState({ x: 0, y: 0 })
const draggableRef = useRef(null)
// 初始化 dragable 拖拽设置
const options: DragOptions = {
position,
onDrag: ({ offsetX, offsetY }) => setPosition({ x: offsetX, y: offsetY }),
bounds: { bottom: -500, top: 32, left: -600, right: -600 },
handle: '.window-header',
cancel: '.traffic-lights',
}
useDraggable(draggableRef, options)

}

return (
<div ref={draggableRef}> //将 draggableRef 挂在到节点上

</div>

)
···

这样我们的 Terminal 终端就有了拖拽效果,其它 API 方法在@neodrag/react 官网中,代码仓库 commit3


terminal2.gif


输入命令


一个终端最重要的当然是输入命令了,在这我们使用 input 框来收集收集输入命令的内容。
由于我们每次执行完一次命令之后,都会生成新的行,所以我们将新行封装成一个组件,Row 组件接收两个参数(id:当前 Row 的唯一标识;onkeydown:监听 input 框的操作):


// components.tsx
interface RowProps {
id: number
onkeydown: (e: React.KeyboardEvent<HTMLInputElement>) => void
}
const Row: React.FC<RowProps> = ({ id, onkeydown }) => {

return (
<div className='flex flex-col w-full h-12'>
<div>
<span className="mr-2 text-yellow-400">funnycoder</span>
<span className="mr-2 text-green-400">@macbook-pro</span>
<span className="mr-2 text-blue-400">~{dir}</span>
<span id={`terminal-currentDirectory-${id}`} className="mr-2 text-blue-400"></span>
</div>
<div className='flex'>
<span className="mr-2 text-pink-400">$</span>
<input
type="text"
id={`terminal-input-${id}`}
autoComplete="off"
autoFocus={true}
className="flex-1 px-1 text-white bg-transparent outline-none"
onKeyDown={onkeydown}
/>

</div>

</div>

)
}

一开始的时候,我们通过初始化一个 Row 进行操作,我们所有生成的 Row 通过


//app.tsx
const [content, setContent] = useState<JSX.Element[]>(
[<Row
id={0}
key={key()} // React 渲染列表时需要key
onkeydown={(e: React.KeyboardEvent<HTMLInputElement>
) => executeCommand(e, 0)}
/>,
])

content 变量来存储,在后续我们经常要修改 content 的值,为了简化代码我们为 setContent 封装成 generateRow 方法:


// 生成内容
const generateRow = (row: JSX.Element) => {
setContent(s => [...s, row])
}

问题来了,当我们获取到了输入的命令时,怎么执行对应的方法呢?


每一个 Row 组件都有 onKeyDown事件监听,当按下按键时就调用 executeCommand 方法,通过 input 框的 id 获取该 input 框 dom 节点, const [cmd, args] = input.value.trim().split(' ') 获取执行命令 cmd 和 参数 args,此时根据 event.key 按键操作执行对应的方法:


 // 执行方法
function executeCommand(event: React.KeyboardEvent<HTMLInputElement>, id: number) {
const input = document.querySelector(`#terminal-input-${id}`) as HTMLInputElement
const [cmd, args] = input.value.trim().split(' ')
if (event.key === 'ArrowUp')
alert(`ArrowUp,Command is ${cmd} Args is ${args}`)

else if (event.key === 'ArrowDown')
alert(`ArrowDown,Command is ${cmd} Args is ${args}`)

else if (event.key === 'Tab')
alert(`Tab,Command is ${cmd} Args is ${args}`)

else if (event.key === 'Enter')
alert(`Enter,Command is ${cmd} Args is ${args}`)
}

接下来我们测试一下,输入cd desktop,按下 Enter 键:
terminal3.gif


代码仓库 commit3


构建文件夹系统


终端的最常用的功能就是操作文件,所以我们需要构建一个文件夹系统,起初,在我的项目中使用的是一个数组嵌套,类似下面这种


image.png


这种数据结构的话,每次寻找子项的都需要递归计算,非常麻烦。在这我们采用 map 进行存储,将数据扁平化:


image.png


代码仓库 commit4


执行命令


准备工作


我们先介绍一下几个变量:



  • currentFolderId :当前文件夹的 id,默认为 0 也就是最顶层的文件夹

  • currentDirectory : 当前路径

  • currentId : input 输入框的 id 标识


  const [currentId, setCurrentId] = useState<number>(0)
const [currentFolderId, setCurrentFolderId] = useState(0)
const [currentDirectory, setCurrentDirectory] = useState<string>('')

并把一些静态组件封装在 components.tsx 文件中:


image.png


核心介绍


我们用一个对象来存储需要执行对应的方法:


  const commandList: CommandList = {
cat,
cd,
clear,
ls,
help,
mkdir,
touch,
}

executeCommand 方法中,如果用户按下的是'Enter' 键,我们首先判断下输入的 cmd 是否在 commandlist 中,如果存在,就直接执行该方法,如果不存在,就生成一个 CommandNotFound
行:


//app.js 
function executeCommand(){
//...
else if (event.key === 'Enter') {
// 将新输入 command 加入 commandHistory 中
const newArr = commandHistory
newArr.push(input.value.trim())
setCommandHistory(newArr)
// 如果输入 command 符合就执行 ⭐️⭐️⭐️
if (cmd && Object.keys(commandList).includes(cmd))
commandList[cmd](args)
else if (cmd !== '')
generateRow(<CommandNotFound key={key()} command={input.value.trim()} />)
// 每次无论 command 符不符合,都需要生成一行新的 Row,并且 curentId++
setCurrentId(id => id + 1)
setTimeout(() => {
generateRow(
<Row
key={key()}
id={commandHistory.length}
onkeydown={(e: React.KeyboardEvent<HTMLInputElement>
) => executeCommand(e, commandHistory.length)}
/>,
)
}, 100)
}
//...
}

help


当输入的 cmd 识别为'help'时就会调用该方法,生成在 components.tsx 里 Help()中定义好的静态数据:


  // help 命令
const help = () => {
generateRow(<Help key={key()} />)
}

代码仓库:commit5


cd


首先,默认的currentFolderId为 0,也就是指向我们的根文件夹,我们可以通过 folderSysteam.get(currentFolderId) 来获取当前文件夹下的信息,包括该文件夹的 title,子文件的 id 数组 childIds
当我们获取到了参数 arg 时,首先要判断 是否为空或者'..',若是的话,即返回上一层目录,
如果是正常参数的话,通过 folderSysteam.get(currentFolderId) 获取子目录的 childIds 数组,遍历当前目录下的子目录,找到子目录中 title 和 arg 一样的目录并返回该子目录 id,将 currentFolderId 设置为该子目录 id 并且拼接文件路径:


  // cd 命令
const cd = (arg = '') => {
const dir: string = localStorage.getItem(CURRENTDIRECTORY) as string
//判断是否返回上一层目录
if (!arg || arg === '..') {
// 处理文件路径
const dirArr = dir.split('/')
dirArr.length = Math.max(0, dirArr.length - 2)
//区分是否是root层
if (!dirArr.length)
setCurrentDirectory(`${dirArr.join('')}`)
else
setCurrentDirectory(`${dirArr.join('')}/`)
// 将当前目录设置为上一层目录
setCurrentFolderId(folderSysteam.get(`${currentFolderId}`)?.parentId as number)
return
}
//若是正常的跳转子目录
//根据 arg 参数获取需跳转目录的 id
const id = searchFile(arg)
// 如果子目录存在,设置路径、更新当前目录id
if (id) {
const res = `${dir + folderSysteam.get(`${id}`)?.title}/`
setCurrentFolderId(id)
setCurrentDirectory(res)
}
// 否则返回 NoSuchFileOrDirectory
else { generateRow(<NoSuchFileOrDirectory key={key()} command={arg}/>) }
}
const searchFile = (arg: string) => {
// 对输入做一个优化,例如文件夹名为 Desktop,只要我们输入'Desktop'|'desktop'|'DESKTOP'都行
const args = [arg, arg.toUpperCase(), arg.toLowerCase(), arg.charAt(0).toUpperCase() + arg.slice(1)]
// 获取当前目录下子目录
const childIds = getStorage(CURRENTCHILDIDS)
// 遍历子目录,找到title 为 arg 的目录
for (const item of folderSysteam.entries()) {
if (childIds.includes(item[1].id) && args.includes(item[1].title))
return item[1].id
}
}


ls


  // ls 命令
const ls = () => {
let res = ''
// 获取当前目录下所有子目录 id
const ids = getStorage(CURRENTCHILDIDS)
// 遍历 id 进行拼接
for (const id of ids)
res = `${res + folderSysteam.get(`${id}`)?.title} `
if (!res) {
generateRow(<div key={key()} >There are no other folders or files in the current directory.</div>)
}
else {
res.split(' ').map((item: string) =>
generateRow(<div key={key()} className={item.includes('.') ? 'text-blue-500' : ''}>{item}</div>),
)
}
}

terminal6.gif


代码仓库:commit6| commit6.1


mkdir、touch


创建文件或文件夹,我们只需要创建该文件或文件夹对象,新对象的 parentId 指向当前目录,其新 id 加入到当前目录的 childIds 数组中,最后再更新一下 folderSysteam 变量:


  // mkdir 命令
const mkdir = (arg = '') => {
const currentFolderId = getStorage(CURRENTFOLDERID)
const size = folderSysteam.size.toString()
// 创建新对象
const newFolderSysteam = folderSysteam.set(`${size}`, {
id: +size,
title: arg,
childIds: [],
parentId: currentFolderId,
})
// 更新 当前文件夹下的 childIds
const childIds = (folderSysteam.get(`${currentFolderId}`) as FolderSysteamType).childIds as number[]
childIds && childIds.push(+size)
setStorage(CURRENTCHILDIDS, childIds)
setFolderSysteam(newFolderSysteam)
}
// touch 命令
const touch = (arg = '') => {
const currentFolderId = getStorage(CURRENTFOLDERID)
const size = folderSysteam.size.toString()
// 创建新对象
const newFolderSysteam = folderSysteam.set(`${size}`, {
id: +size,
title: arg,
content: <div ><h1>
This is <span className='text-red-400 underline'>{arg}</span> file!
</h1>
<p>Imagine there's a lot of content here...</p>
</div>
,
parentId: currentFolderId,
})
// 更新 当前文件夹下的 childIds
const childIds = (folderSysteam.get(`${currentFolderId}`) as FolderSysteamType).childIds as number[]
childIds && childIds.push(+size)
setStorage(CURRENTCHILDIDS, childIds)
setFolderSysteam(newFolderSysteam)
}


terminal7.gif


代码仓库:commit7


cat、clear


cat 命令只需要展示子文件的 content 属性值即可:


  // cat 命令
const cat = (arg = '') => {
//获取当前目录下 childIds 进行遍历
const ids = getStorage(CURRENTCHILDIDS)
ids.map((id: number) => {
const item = folderSysteam.get(`${id}`) as FolderSysteamType
//生成 title 为 arg 文件的 content Row 行
return item.title === arg ? generateRow(<div key={key()}>{item.content}</div> as JSX.Element) : ''
})
}

clear 命令只需要调用 setContent():


  // clear 命令
const clear = () => {
setContent([])
//清空 input 框内容
const input = document.querySelector('#terminal-input-0') as HTMLInputElement
input.value = ''
}

terminal8.gif
代码仓库:commit8


其它操作


准备工作


我们先介绍一下几个变量:



  • commandHistory : 用于存储输入过的 command数组

  • changeCount : 用来切换 command 计数


  const [changeCount, setChangeCount] = useState<number>(0)
const [commandHistory, setCommandHistory] = useState<string[]>([])

上下键切换 command


上面定义的 changeCount 变量默认为 0,当我们按上🔼键时,changeCount-1,当我们按下🔽键时,changeCount+1。
而当 changeCount 变量变化时,获取当前 input dom 节点,设置其值为commandHistory[commandHistory.length + changeCount],这样我们的上下键切换 command 就实现了:


    // 当按下上下键时 获取历史 command
useEffect(() => {
const input = document.querySelector(`#terminal-input-${commandHistory.length}`) as HTMLInputElement
if (commandHistory.length)
input.value = commandHistory[commandHistory.length + changeCount]
if (!changeCount) {
input.value = ''
setChangeCount(0)
}
}, [changeCount])

// 按向上🔼键
function handleArrowUp() {
setChangeCount(prev => Math.max(prev - 1, -commandHistory.length))
}
// 按向下🔽键
function handleArrowDown() {
setChangeCount(prev => Math.min(prev + 1, 0))
}
// 执行方法
function executeCommand(...) {
//...
if (event.key === 'ArrowUp') {
handleArrowUp()
}
else if (event.key === 'ArrowDown') {
handleArrowDown()
}
//...

Tab 键补全 command


根据历史记录补全 command ,利用 Array.filter() 和 String.startsWith() 就行:


  // 匹配历史 command 并补充
const matchCommand = (inputValue: string): string | null => {
// 遍历历史command 返回以当前输入 command 值开头(startsWith)的 command
const matchedCommands = commandHistory.filter(command => command.startsWith(inputValue))
return matchedCommands.length > 0 ? matchedCommands[matchedCommands.length - 1] : null
}


代码仓库:commit9


最后


大家有兴趣的话可以自己再去二次改造或添加一些新玩法,此组件已通过 Netlify 部署上线,地址为 my-terminal.netlify.app/
项目源代码:github.com/ljq0226/my-… 欢迎 S

作者:Aphelios_
来源:juejin.cn/post/7248599585735098405
tar ⭐️⭐️⭐️

收起阅读 »

前端面试题 - 96. hash 和 history 的区别?

web
hash和history是Web开发中常用的两个概念,它们都与浏览器URL相关。 Hash(哈希) URL中以#符号开始的部分被称为哈希部分。在Web开发中,通常使用哈希来实现页面内的导航或锚点定位。当浏览器的哈希发生变化时,页面不会重新加载,而是触发一个ha...
继续阅读 »

hashhistory是Web开发中常用的两个概念,它们都与浏览器URL相关。


Hash(哈希)


URL中以#符号开始的部分被称为哈希部分。在Web开发中,通常使用哈希来实现页面内的导航或锚点定位。当浏览器的哈希发生变化时,页面不会重新加载,而是触发一个hashchange事件。


// 监听 hashchange 事件
window.addEventListener('hashchange', function() {
var currentHash = window.location.hash;

// 根据不同的哈希值执行相应的操作
if (currentHash === '#section1') {
console.log('显示第一部分的内容')
} else if (currentHash === '#section2') {
console.log('显示第二部分的内容')
} else {
console.log('其他操作')
}
});

通过监听此事件,你可以根据哈希的变化来执行相应的操作,例如显示不同的内容或调用特定的函数。哈希可以直接通过JavaScript进行修改,例如window.location.hash = "section2",URL将变为(此时hashchange事件也会触发):


https://example.com/page.html#section2
// 输出 显示第二部分的内容

History(历史记录)


历史记录是浏览器跟踪用户访问过的URL的一种机制。通过history对象,你可以在JavaScript中操作浏览器的历史记录。一些常用的方法包括history.pushState()history.replaceState()history.back()。这些方法允许你添加、替换和移动浏览器的历史记录,并且不会导致页面的实际刷新。当历史记录发生变化时,浏览器不会重新加载页面,但可以通过popstate事件来捕获这些变化并做出响应。


示例:


// 添加新的历史记录
history.pushState({ page: "page2" }, "Page 2", "page2.html");

// 监听 popstate 事件
window.addEventListener('popstate', function(event) {
var state = event.state;
console.log(state)
// 根据历史记录的变化执行相应的操作
if (state.page === "page1") {
console.log('显示第一页的内容')
} else if (state.page === "page2") {
console.log('显示第二页的内容')
} else {
console.log('其他操作')
}
});

需要注意的是,使用pushState()方法修改历史记录并不会触发popstate事件。只有在用户点击浏览器的前进或后退按钮时,或者通过JavaScript代码调用history.back()history.forward()history.go()方法导致历史记录变化时,popstate

作者:总瓢把子
来源:juejin.cn/post/7248608019851755575
e>事件才会被触发。

收起阅读 »

面试官: 既然有了 cookie 为什么还要 localStorage?😕😕😕

web
Web Storage Web Storage 最终是网页超文本应用技术工作组在 Web Applications 1.0 规范中提出的。这个规范中的草案最终成为了 HTML5 的一部分,后来有独立称为自己的规范。Web Storage 的目的是解决通过客户端...
继续阅读 »

Web Storage


Web Storage 最终是网页超文本应用技术工作组在 Web Applications 1.0 规范中提出的。这个规范中的草案最终成为了 HTML5 的一部分,后来有独立称为自己的规范。Web Storage 的目的是解决通过客户端存储不需要频繁发送回服务器的数据时使用 cookie 的问题。


Web Storage 规范最新的版本是第 2 版,这一版规范主要有两个目标:



  1. 提供在 cookie 之外的存储会话数据的途径;

  2. 提供跨会话持久化存储大量数据的机制;


Web Storage 定义了两个对象: localStoragesessionStorage。前者是永久存储机制,而后者是跨会话的存储机制。这两个浏览器存储 API 提供了在浏览器中不收页面刷新影响而存储数据的两种方式。


Storage 类型


Storage 类型用于保存 名/值 对数据,直至存储空间上限(由浏览器决定)。Storage 的实例与其他对象一样,但增加了以下方法:



  1. clear(): 删除所有值;

  2. getItem(name): 取得给定 name 值;

  3. key(index): 取得给定数值位置的名称;

  4. removeItem(name): 删除给定 name名/值 对;

  5. setItem(name,value): 设置给定 name 的值;


getItem()removeItem(name)setItem() 方法可以直接或间接通过 Storage 对象调用。因为每个数据项都作为属性存储在该对象上,所以可以使用点或括号操作符访问这些属性,统统同样的操作来设置值,也可以使用 delete 操作符来删除属性。即便如此,通常还是建议使用方法而非属性来执行这些操作,以免意外重写某个已存在的对象成员。


localStorage 对象


在修订的 HTML5 规范里,localStorage 对象取代了 globalStorage,作为在客户端持久存储数据的机制,要访问同一个 localStorage 对象,页面必须来自同一个域(子域不可以)、在想用的端口上使用相同的协议。


因为 localStorageStorage 的实例,所以可以像使用 sessionStorage 一样使用 localStorage。具体实例请看下面几个例子:


// 使用方法存储数据
localStorage.setItem("moment", 777);

// 使用属性存储数据
localStorage.nickname = "moment";

// 使用方法获取数据
const name = localStorage.getItem("moment");

// 使用属性获得数据
const nickname = localStorage.nickname;

两种存储方法的区别在于,存储在 localStorage 中的数据会保留到通过 JavaScript 删除或者用户清除浏览器缓存。localStorage 数据不受页面刷新影响,也不会因关闭窗口,标签也或重新启动浏览器而丢失。


存储事件


每当 Storage 对象发生变化时,都会在文档上触发 storage 事件,使用属性或者 setItem() 设置值、使用 deleteremoveItem() 删除值,以及每次调用 clean() 时都会触发这个事件,这个事件的事件对象有如下四个属性:



  1. domain: 存储变化对应的域;

  2. key: 被设置或删除的键;

  3. newValue: 键被设置的新值,若键被删除则为 null;

  4. oldValue: 键变化之前的值。


我们可以使用如下代码监听 storage 事件:


window.addEventListener("storage", function (e) {
document.querySelector(".my-key").textContent = e.key;
});

对于 sessionStoragelocalStorage 上的任何更改都会触发 storage 事件,但 storage 事件不会区分这两者。


这是一道面试题


在不久前,被问到这样一个问题,我们通过后端返回来的 token 为什么是存储在 localStorage 而不是存储在 cookie 中?


考虑这个问题的首先我们应该知道,token 就是一个字符串,而使用 cookie 的话,大小是满足的,所以考察的点就不在这个内存上面了。


之所以使用 localStorage 存储 token,而不是使用 cookie,这可能基于以下几个方面考虑:



  1. 前后端分离架构: 在一些现代的 Web 应用程序中,前端和后端通常是通过 API 进行通信的,而不是使用传统的服务器端渲染。在这种情况下,前端可能是一个独立的应用程序,如基于 JavaScript 的单页应用或移动应用程序。由于前端和后端是分离的,Cookie 在这种架构中不太容易管理,因为跨域请求可能会遇到一些限制。localStorage 提供了一种更方便的解决方案,前端应用程序可以直接访问和管理存储在本地的令牌;

  2. 安全性需求: 在某些情况下,开发者可能认为将令牌存储在 Cookie 中存在一些安全风险,尤其是在面对跨站脚本攻击 XSS 时。使用 localStorage 可以减少某些安全风险,因为 LocalStorage 中的数据不会自动发送到服务器,且可以通过一些安全措施(如加密)来增强数据的安全性;

  3. 令牌过期处理: 使用 localStorage 存储令牌可以让令牌在浏览器关闭后仍然保持有效,这在某些应用场景下是有用的。例如,用户可能关闭了浏览器,然后再次打开时仍然保持登录状态,而不需要重新输入凭据;


值得注意的是,使用 localStorage 存储 token 也不是说百分百安全的,依然会存在一些问题和风险,如容易收到 XSS 攻击、不支持跨域贡献等。因此,在使用 localStorage 存储令牌时,开发者需要采取适当的安全措施,如加密存储数据、定期更新令牌等,以确保令牌的安全性和有效性。


localStorage 如何实现跨域


localStorage 是一直域限制的存储机制,通常只能在同一域名下的页面中访问。这意味着默认情况下,localStorage 的数据在不同域名或跨域的情况下是无法直接访问的。然而,有几种方法可以实现跨域访问 localStorage 中的数据:



  1. 域名映射(Domain Mapping): 将不同域名都指向同一个服务器 IP 地址。这样不同域名下的页面就可以共享同一个 localStorage 中的数据;

  2. postMessage API: postMessage 是一种浏览器提供的 API,用于在不同窗口或跨域的 iframe 之间进行安全的消息传递。你可以在不同域名的页面中使用 postMessage 将数据从一个窗口传递到另一个窗口,并在目标窗口中将数据存储到 localStorage 中;


使用 postMessage 将数据从一个窗口传递到另一个窗口,并在目标窗口中将数据存储到 localStorage 中,实例代码如下:


// 发送消息到目标窗口
window.postMessage(
{ key: "token", value: "1233211234567" },
"https://liangzai.com"
);

在接收消息的窗口中:


// 监听消息事件
window.addEventListener("message", function (event) {
if (event.origin === "https://sourcedomain.com") {
// 存储数据到 LocalStorage
localStorage.setItem(event.data.key, event.data.value);
}
});

这些方法提供了一些途径来实现跨域访问 localStorage 中的数据。具体选择哪种方法取决于你的需求和应用场景,以及你对目标域名的控制程度。需要注意的是,安全性是非常重要。


cookie 和 localStorage 的区别


CookieLocalStorage 是两种用于在浏览器中存储数据的机制,它们在以下方面有一些区别:



  1. 存储容量: Cookie 的存储容量通常较小,每个 Cookie 的大小限制在几 KB 左右。而 LocalStorage 的存储容量通常较大,一般限制在几 MB 左右。因此,如果需要存储大量数据,LocalStorage 通常更适合;

  2. 数据发送: Cookie 在每次 HTTP 请求中都会自动发送到服务器,这使得 Cookie 适合用于在客户端和服务器之间传递数据。而 localStorage 的数据不会自动发送到服务器,它仅在浏览器端存储数据,因此 LocalStorage 适合用于在同一域名下的不同页面之间共享数据;

  3. 生命周期:Cookie 可以设置一个过期时间,使得数据在指定时间后自动过期。而 LocalStorage 的数据将永久存储在浏览器中,除非通过 JavaScript 代码手动删除;

  4. 安全性:Cookie 的安全性较低,因为 Cookie 在每次 HTTP 请求中都会自动发送到服务器,存在被窃取或篡改的风险。而 LocalStorage 的数据仅在浏览器端存储,不会自动发送到服务器,相对而言更安全一些;


总结


Cookie 适合用于在客户端和服务器之间传递数据、跨域访问和设置过期时间,而 LocalStorage 适合用于在同一域名下的不同页面之间共享数据、存储大量数据和永久存储数据。选择使用哪种机制应根据具体的需

作者:Moment
来源:juejin.cn/post/7248623545219825723
求和使用场景来决定。

收起阅读 »

在高德地图中实现降雨图层

web
前言 有一天老板跑过来跟我说,我们接到一个水利局的项目,需要做一些天气效果,比如说降雨、河流汛期、洪涝灾害影响啥的,你怎么看。欸,我觉得很有意思,马上开整。 需求说明 在地图上实现降雨效果,画面尽量真实,比如天空、风云的变化与降雨场景契合; 可以结合当地天气预...
继续阅读 »

前言


有一天老板跑过来跟我说,我们接到一个水利局的项目,需要做一些天气效果,比如说降雨、河流汛期、洪涝灾害影响啥的,你怎么看。欸,我觉得很有意思,马上开整。


需求说明


在地图上实现降雨效果,画面尽量真实,比如天空、风云的变化与降雨场景契合;


可以结合当地天气预报情况,自动调节风速、风向、降雨量等参数。


需求分析


方案一:全局降雨


在用户视口面前加一层二维的降雨平面层。


优点: 只管二维图层就行了,不需要与地图同步坐标,实现起来比较简单,界面是全局的一劳永逸。


缺点:只适合从某些角度观看,没法再做更多定制了。


Honeycam_2023-06-16_11-10-37.gif


方案二:局部地区降雨


指定降雨范围,即一个三维空间,坐标与地图底图同步,仅在空间内实现降雨。


优点:降落的雨滴有远近关系,比较符合现实场景;可适用各种地图缩放程度。


缺点:需要考虑的参数比较多,比如降雨范围一项就必须考虑这个三维空间是什么形状,可能是立方体、圆柱体或者多边形挤压体;需要外部图层的配合,比如说下雨了,那么天空盒子的云层、建筑图层的明度是否跟着调整。


Honeycam_2023-06-16_11-20-08.gif


实现思路


根据上面利弊权衡,我选择了方案二进行开发,并尽量减少输入参数,降雨影响范围初步定为以地图中心为坐标中心的立方体,忽略风力影响,雨滴采用自由落体方式运动。


降雨采用自定义着色器的方式实现,充分利用GPU并行计算能力,刚好在网上搜到一位大佬写的three演示代码,改一下坐标轴(threejs空间坐标轴y轴朝上,高德GLCustomLayer空间坐标z轴朝上)就可以直接实现最基础的效果。这里为了演示方便增加坐标轴和影响范围的辅助线。


1.创建影响范围,并在该范围内创建降雨层的几何体Geometry,该几何体的构成就是在影响范围内随机位置的1000个平面,这些平面与地图底面垂直;


Honeycam_2023-06-24_15-40-31.gif


2.创建雨滴材质,雨滴不受光照影响,这里使用最基础的MeshBasicMaterial材质即可,半透明化且加上一张图片作为纹理;


Honeycam_2023-06-24_15-50-32.gif


3.为实现雨滴随着时间轴降落的动画效果,需要调整几何体的形状尺寸,并对MeshBasicMaterial材质进行改造,使其可以根据当前时间time改变顶点位置;


Honeycam_2023-06-24_16-01-39.gif



  1. 调整顶点和材质,使其可以根据风力风向改变面的倾斜角度和移动轨迹;


Honeycam_2023-06-24_16-16-52.gif



  1. 将图层叠加到地图3D场景中


Honeycam_2023-06-24_16-28-46.gif


基础代码实现


为降低学习难度,本模块只讲解最基础版本的降雨效果,雨滴做自由落体,忽略风力影响。这里的示例以高德地图上的空间坐标轴为例,即z轴朝上,three.js默认空间坐标系是y轴朝上。我把three.js示例代码演示放到文末链接中。


1.创建影响范围,并在该范围内创建降雨层的几何体Geometry


createGeometry () {
// 影响范围:只需要设定好立方体的size [width/2, depth/2, height/2]
//
const { count, scale, ratio } = this._conf.particleStyle
// 立方体的size [width/2, depth/2, height/2]
const { size } = this._conf.bound
const box = new THREE.Box3(
new THREE.Vector3(-size[0], -size[1], 0),
new THREE.Vector3(size[0], size[1], size[2])
)

const geometry = new THREE.BufferGeometry()
// 设置几何体的顶点、法线、UV
const vertices = []
const normals = []
const uvs = []
const indices = []

// 在影响范围内随机位置创建粒子
for (let i = 0; i < count; i++) {
const pos = new THREE.Vector3()
pos.x = Math.random() * (box.max.x - box.min.x) + box.min.x
pos.y = Math.random() * (box.max.y - box.min.y) + box.min.y
pos.z = Math.random() * (box.max.z - box.min.z) + box.min.z

const height = (box.max.z - box.min.z) * scale / 15
const width = height * ratio

// 创建当前粒子的顶点坐标
const rect = [
pos.x + width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z - height / 2,
pos.x + width,
pos.y,
pos.z - height / 2
]

vertices.push(...rect)

normals.push(
pos.x,
pos.y,
pos.z,
pos.x,
pos.y,
pos.z,
pos.x,
pos.y,
pos.z,
pos.x,
pos.y,
pos.z
)

uvs.push(1, 1, 0, 1, 0, 0, 1, 0)

indices.push(
i * 4 + 0,
i * 4 + 1,
i * 4 + 2,
i * 4 + 0,
i * 4 + 2,
i * 4 + 3
)
}

// 所有顶点的位置
geometry.setAttribute(
'position',
new THREE.BufferAttribute(new Float32Array(vertices), 3)
)
// 法线信息
geometry.setAttribute(
'normal',
new THREE.BufferAttribute(new Float32Array(normals), 3)
)
// 设置UV属性与顶点顺序一致
geometry.setAttribute(
'uv',
new THREE.BufferAttribute(new Float32Array(uvs), 2)
)
// 设置基本单元的顶点顺序
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1))

return geometry
}

2.创建材质


createMaterial () {
// 粒子透明度、贴图地址
const { opacity, textureUrl } = this._conf.particleStyle
// 实例化基础材质
const material = new THREE.MeshBasicMaterial({
transparent: true,
opacity,
alphaMap: new THREE.TextureLoader().load(textureUrl),
map: new THREE.TextureLoader().load(textureUrl),
depthWrite: false,
side: THREE.DoubleSide
})

// 降落起点高度
const top = this._conf.bound.size[2]

material.onBeforeCompile = function (shader, renderer) {
const getFoot = `
uniform float top; // 天花板高度
uniform float bottom; // 地面高度
uniform float time; // 时间轴进度[0,1]
#include <common>
float angle(float x, float y){
return atan(y, x);
}
// 让所有面始终朝向相机
vec2 getFoot(vec2 camera,vec2 normal,vec2 pos){
vec2 position;
// 计算法向量到点的距离
float distanceLen = distance(pos, normal);
// 计算相机位置与法向量之间的夹角
float a = angle(camera.x - normal.x, camera.y - normal.y);
// 根据点的位置和法向量的位置调整90度
pos.x > normal.x ? a -= 0.785 : a += 0.785;
// 计算投影值
position.x = cos(a) * distanceLen;
position.y = sin(a) * distanceLen;

return position + normal;
}
`

const begin_vertex = `
vec2 foot = getFoot(vec2(cameraPosition.x, cameraPosition.y), vec2(normal.x, normal.y), vec2(position.x, position.y));
float height = top - bottom;
// 计算目标当前高度
float z = normal.z - bottom - height * time;
// 落地后重新开始,保持运动循环
z = z + (z < 0.0 ? height : 0.0);
// 利用自由落体公式计算目标高度
float ratio = (1.0 - z / height) * (1.0 - z / height);
z = height * (1.0 - ratio);
// 调整坐标参考值
z += bottom;
z += position.z - normal.z;
// 生成变换矩阵
vec3 transformed = vec3( foot.x, foot.y, z );
`

shader.vertexShader = shader.vertexShader.replace(
'#include <common>',
getFoot
)
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
begin_vertex
)
// 设置着色器参数的初始值
shader.uniforms.cameraPosition = { value: new THREE.Vector3(0, 0, 0) }
shader.uniforms.top = { value: top }
shader.uniforms.bottom = { value: 0 }
shader.uniforms.time = { value: 0 }
material.uniforms = shader.uniforms
}

this._material = material

return material
}

3.创建模型



createScope () {
const material = this.createMaterial()
const geometry = this.createGeometry()

const mesh = new THREE.Mesh(geometry, material)

this.scene.add(mesh)

// 便于调试,显示轮廓
// const box1 = new THREE.BoxHelper(mesh, 0xffff00)
// this.scene.add(box1)
}

4.更新参数


// 该对象用于跟踪时间
_clock = new THREE.Clock()

update () {
const { _conf, _time, _clock, _material, camera } = this

// 调整时间轴进度,_time都值在[0,1]内不断递增循环
// particleStyle.speed为降落速度倍率,默认值1
// _clock.getElapsedTime() 为获取自时钟启动后的秒数
this._time = _clock.getElapsedTime() * _conf.particleStyle.speed / 2 % 1

if (_material.uniforms) {
// 更新镜头位置
_material.uniforms.cameraPosition.value = camera.position
// 更新进度
_material.uniforms.time.value = _time
}
}

animate (time) {
if (this.update) {
this.update(time)
}
if (this.map) {
// 叠加地图时才需要
this.map.render()
}
requestAnimationFrame(() => {
this.animate()
})
}

优化调整


修改场景效果


通过对图层粒子、风力等参数进行封装,只需简单地调整配置就可以实现额外的天气效果,比如让场景下雪也是可以的,广州下雪这种场景,估计有生之年只能在虚拟世界里看到了。


Honeycam_2023-06-24_17-00-11.gif


以下是配置数据结构,可供参考


const layer = new ParticleLayer({
map: getMap(),
center: mapConf.center,
zooms: [4, 30],
bound: {
type: 'cube',
size: [500, 500, 500]
},
particleStyle: {
textureUrl: './static/texture/snowflake.png', //粒子贴图
ratio: 0.9, //粒子宽高比,雨滴是长条形,雪花接近方形
speed: 0.04, // 直线降落速度倍率,默认值1
scale: 0.2, // 粒子尺寸倍率,默认1
opacity: 0.5, // 粒子透明度,默认0.5
count: 1000 // 粒子数量,默认值10000
}
})

添加风力影响


要实现该效果需要添加2个参数:风向和风力,这两个参数决定了粒子在降落过程中水平面上移动的方向和速度。



  1. 首先调整一下代码实际那一节步骤2运动的相关代码


const begin_vertex = `
...
// 利用自由落体公式计算目标高度
float ratio = (1.0 - z / height) * (1.0 - z / height);
z = height * (1.0 - ratio);
// 增加了下面这几行
float x = foot.x+ 200.0 * ratio; // 粒子最终在x轴的位移距离是200
float y = foot.y + 200.0 * ratio; // 粒子最终在y轴的位移距离是200
...
// 生成变换矩阵
vec3 transformed = vec3( foot.x, y, z );


  1. 如果粒子是长条形的雨滴,那么它在有风力影响的运动时,粒子就不是垂直地面的平面了,而是与地面有一定倾斜角度的平面,如图所示。


Untitled.png


我们调整调整一下代码实际那一节步骤1的代码,实现方式就是让每个粒子平面在创建之后,所有顶点绕着平面的法线中心轴旋转a角度。


本示例旋转轴(x, y, 1)与z轴(0,0,1)平行,这里有个技巧,我们在做平面绕轴旋转的时候先把平面从初始位置orgPos移到坐标原点,绕着z轴旋转后再移回orgPos,会让计算过程简单很多。


// 创建当前粒子的顶点坐标
const rect = [
pos.x + width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z - height / 2,
pos.x + width,
pos.y,
pos.z - height / 2
]

// 定义旋转轴
const axis = new THREE.Vector3(0, 0, 1).normalize();
//定义旋转角度
const angle = Math.PI / 6;
// 创建旋转矩阵
const rotationMatrix = new THREE.Matrix4().makeRotationAxis(axis, angle);

for(let index =0; index< rect.length; index +=3 ){
const vec = new THREE.Vector3(rect[index], rect[index + 1], rect[index + 2]);
//移动到中心点
vec.sub(new THREE.Vector3(pos.x, pos.y,pos.z))
//绕轴旋转
vec.applyMatrix4(rotationMatrix);
//移动到原位
vec.add(new THREE.Vector3(pos.x, pos.y, pos.z))
rect[index] = vec.x;
rect[index + 1] = vec.y;
rect[index + 2] = vec.z;
}

待改进的地方


本示例中有个需要完善的地方,就是加入了风力影响之后,如果绕垂直轴旋转一定的角度,会看到如下图的异常,雨点的倾斜角度和运动倾斜角度是水平相反的。


Honeycam_2023-06-24_21-06-51.gif


问题的原因是材质着色器中的“让所有面始终朝向相机”方法会一直维持粒子的倾斜状态不变,解决这个问题应该是调整这个方法就可以了。然而作为学渣的我还没摸索出来,果然可视化工程的尽头全是数学Orz。


相关链接


1.THREE.JS下雨进阶版,面只旋转Y轴朝向相机


http://www.wjceo.com/blog/threej…


2.演示代码在线DEMO


jsfiddle.net/gyrate

sky/5…

收起阅读 »

何处是吾乡?(前端人年中总结)

前言 我的老家是湖北黄州,对,就是子瞻兄被贬的那个黄州,我很喜欢苏轼的豪放派诗词,为他积极乐观的生活态度和人格魅力着迷。 本人不知不觉干前端快**5个年头**了,虽辗转了**3个城市**,依次在厦门,武汉,杭州历经了**4份工作...
继续阅读 »

微信图片_20230625193606.jpg


前言


我的老家是湖北黄州,对,就是子瞻兄被贬的那个黄州,我很喜欢苏轼的豪放派诗词,为他积极乐观的生活态度和人格魅力着迷。


本人不知不觉干前端快**5个年头**了,虽辗转了**3个城市**,依次在厦门,武汉,杭州历经了**4份工作**。但依然喜欢那句**少年的征途是星辰大海**,我不在乎之后我会在那儿,也正是苏轼说的,此心安处便是吾乡!


半年已过,有失有得:


第一: 被裁员。今年开年2月份,公司资金链断裂,开始大规模裁员,那段时间,整个公司都笼罩在裁员的黑色恐怖下,各种新闻又提醒我们大环境不好找工作,前端已死等等,我当时也有所预感我会在名单之类(没有安排工作任务),内心很难过,是那种感觉自己要被抛弃的悲伤,尽管室友安慰我:“说我来公司这么久(一年半),要裁也是裁你之后来的,就算被裁,你也还年轻,肯定能找到”,但那一天终归是来了,部门经理走过来,无奈的拍我肩膀说:“兄弟,实在是不好意思,我也没办法了”,叫我去办公室谈,我心里的靴子终于落地了,脸上苦笑,在一群同事不可思议的目送下,跟着经理来到办公室,总监也早早坐在里面了,看到我,一声长叹息,接着一些诸如上面的决定,我也没办法这类的话后,我一阵愣神,也在这无奈中被迫接受了。离职流程走的真快,赔偿n+1方案我也妥协了!


微信图片_20220606201942.png


第二: 失去了一位朋友。去年,一次同学聚会,认识了一个新朋友,是我同学的同学,聚会结束拍合照的时候,站在我身边,我看清她了,脸蛋圆圆的,双马尾,鼻子挺,戴个眼镜,长相中等,看到她的笑容,我内心OS:“我要找的不就是她吗?”,随后找我同学打听了下,单身可聊,我拿着零食,上去一番勇敢搭讪后,然后顺利要到了微信,向有经验的朋友请教怎么追女生。后来,我经常邀请她周末出来玩,看电影,动物园,火锅,也快速熟络起来,但在一个晚上,我突发奇想的向她表白了,不出意外被拒了,然后关系就慢慢地变得尴尬起来,找她聊微信也常常爱答不理了,我(纯情小处男)表示很受伤,问她原因,她说对我没感觉。我想大概是考验我?我得更加主动些,约她出来玩,找她聊天,可约不出来,聊天也不怎么回消息了,不知不觉到年底了,关系越来越拉胯了。


那段时间,我内心经历了什么,我已不想回忆了,至于她,今年2月15号那天发了一条朋友圈,她官宣脱单,算是在告诉我,别再烦她了,恰恰这天,也是我在公司的last day。。。我本以为我已经释怀了,不曾想我的心头还是有一股说不出的酸楚,晚上和几个要好的同事攒了个酒局,算是离别晚宴吧,期间,酒桌上说了些什么我已经记不得,只记得我多喝了几杯,有些醉。我隐隐的感觉,我心中追逐的光熄灭了, 就像是夜幕下,在茫茫的大海中孤独航行的船,突然看到远方有一座灯塔,于是乎努力的朝着那微弱的灯光划去,使出了浑身解数,精疲力竭,却看到了灯塔骤然熄灭了,又重回黑暗!我也发了一条朋友圈:失之东隅收之桑榆,塞翁失马焉知非福! 算是为 被公司裁员的自我安慰和鼓励,也是对失去了这个段关系的自我劝解,罢了~


src=http___5b0988e595225.cdn.sohucs.com_images_20180819_98b3c15d372a43fe9ad8a2b71444c7b0.png&refer=http___5b0988e595225.cdn.sohucs.webp
第三:股票亏3w。麻绳专挑细处断,噩运只找苦命,如果说上面两件事情给我的是暴击,那投资的股票连续的亏损就是给我的魔法伤害,一直出血。。。那些人说的没错,股票就是放大你的贪婪和恐惧。前年本着价值投资,加入这个血雨腥风的二级市场,没曾想,这个市场就是个无情的机器,不断收割你的财富和精力,劝那些心存幻想的佳人们,别来沾边,离得越远越好!


微信图片_20230625213901.jpg



第一:找到工作!在离职了2个多月后,我一直赋闲在家,调整心态、写简历,复习前端知识。外公带给我一只拉布拉多犬,我每天牵着它溜达,照顾它,给它洗澡,在它陪伴下,我内心得到了不少治愈。后来我一起被裁的同事内推了我一家公司,去杭州,内心还犹豫了一下,同父亲聊,他鼓励我去,告诉我还年轻,多出去走走也不是坏事,三轮面试,都还挺顺利的过了,薪资也在我的意料之外,确认上班时间后,我和朋友开车去了趟恩施自驾游,回来就收拾行李去杭州入职了。不过,这次我更多地是运气占了上风,被裁后,自己投递的都没有面试机会,内推还是给力!后面再周末,下班回家,还得多提升自己~


微信图片_20230625193610.jpg
第二:鼻骨矫正手术。大学时,打篮球,意外受伤导致鼻骨偏曲了一些,一直没时间做手术,这次终于是有机会了,在离职后的几天,我母亲就为我安排了一个在鼻子整形方面很有名的医生为我做手术。开始我是拒绝的,一个男人做整形方面的手术,作为直男,我很抗拒,后来在我母亲和父亲的说服下,我还是同意了,躺在手术台,我整个紧张到冒汗,局麻,五针打在上嘴唇和鼻翼周围,还有鼻梁上,疼的眼泪直流,整体手术下来,我已经虚脱了。在医院住了几天后就回家调养,等半个月拆线后,看到我的新鼻子,顿时感觉这手术没白做。鼻子整个都挺起来了,山根变高了,脸也小了很多。


微信图片_20230625223222.jpg


总结


今年上半年,算是我的转折点, 有一些人离开了我,有一些人又进入到了我的世界中,只是在经历了失与得的之后,我又站在了新的起点,开始新的征程,此时又正如东坡的那句:回首向来萧瑟去,归去,也无风雨也无晴

作者:掘金爱佛森
来源:juejin.cn/post/7248606482014732345

收起阅读 »

我有个气人的同事......

web
前段时间看到掘金上好几个 console 自定义的仓库玩法,就想到自己曾经也这么玩过。就想着把自己故事写出来。 曾经,我有个气人的同事,总是喜欢用 console.error() 来调试代码,搞得我和他合作,看到控制台老难受了,就为他特殊定制了一个工具库 ...
继续阅读 »

前段时间看到掘金上好几个 console 自定义的仓库玩法,就想到自己曾经也这么玩过。就想着把自己故事写出来。




曾经,我有个气人的同事,总是喜欢用 console.error() 来调试代码,搞得我和他合作,看到控制台老难受了,就为他特殊定制了一个工具库 console-custom。沉寂在个人仓库很久,前段时间看到别人也有类似仓库,也就想着把自己的也发出来。




其实,我个人不是很推荐在代码里 写 console.log 之类的来调试代码,更推荐去浏览器控制台去打断点来调试,更好的理清数据的流转,事件的先后顺序等。



背景


官方背景:



  • 方便大家调试代码的时候,在浏览器控制台输出自定义个性化日志。

  • 防止控制台输出密密麻麻的 console.log,一眼看不到想看的。

  • 防止某个气人的小伙伴老是使用 console.error,强迫症不允许。

  • ......


真实背景:


其实,是我之前有个小伙伴同事——“小白菜”(也是为啥函数名叫 blog 的原因之一,下边会看到),他调试代码,打印输出总是喜欢 console.error(),用完了还不自己清理,大家协同开发的时候,git pull 他的代码后,总是让人就很难受!看着一堆报错,一时半会看不清是程序自己的报错,还是调试的输出!强迫症就犯了!想骂街......


不......不......要冷静!



  • 编码千万行

  • 调试要输出

  • log不规范

  • 同事两行泪


效果


浏览器页面 page


tu1.jpg


浏览器控制台 console


image.png



有个重点、痛点是这个, console.log(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333); 打印的数据多的时候不换行,需要找半天,我专门给处理成 分行, 一行一行展示了,这样好看清数据。



这个工具库有以下几个特点:



  1. 支持输入多个数据,并分行打印出来,并能看出是个整体

  2. 支持自己修改自己的默认样式部分,配置自己的默认部分

  3. 支持额外的自定义部分,拓展用户更多的玩法

  4. ......


其实 console 由于可以自定义,其实会有很多玩法,我个人在此主要的思路是



  1. 一定要简单,因为 console.log 本身就很简单,尽量不要造成使用者心智负担。

  2. 就简单的默认定制一个彩色个性化的部分,能区分出来,解决那个气人同事所谓的痛点就好。

  3. 代码要少,不要侵入,不要影响用户的业务代码


源码


此处源码有借鉴 github 开源代码:github.com/Redstone-1/…


大家如有更多、更丰富的需求场景可去参考使用。


// src/utils/console-custom.js
const GourdBabyColorMap = new Map([
["1", "#FF0000"],
["2", "#FFA500"],
["3", "#FFFF00"],
["4", "#008000"],
["5", "#00FFFF"],
["6", "#0000FF"],
["7", "#800080"],
]);

const createBLog = (config) => {
const logType = config.logType || "default";
const username = config.username || "";
const logName = config.logName || "";
const usernameColor = config.usernameColor || "#41b883";
const logNameColor = config.logNameColor || "#35495e";
const padding = config.padding || 6;
const borderRadius = config.borderRadius || 6;
const fontColor = config.fontColor || "#FFFFFF";
const usernameStyle = config.usernameStyle || "";
const logNameStyle = config.logNameStyle || "";

const logTemplate = (username = "myLog", logName = "") =>
`${username ? '%c' + username : ''} ${logName ? '%c' + logName : ''} `;

const customLog = (...data) => {
console.log(
logTemplate(username, logName),
usernameStyle ? usernameStyle : `background: ${usernameColor}; padding: 6px; border-radius: 6px 0 0 6px; color: #fff`,
logNameStyle ? logNameStyle : `background: ${logNameColor}; padding: 6px; border-radius: 0 6px 6px 0; color: #fff`,
...data
);
};

const defaultLog = (...data) => {
const len = data.length;
if (len > 1) {
data.map((item, index) => {
let firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
border-radius: 0 0;
color: ${fontColor}
`
;
let secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
border-radius: 0 0;
color: ${fontColor}
`
;
if (index === 0) {
firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
margin-top: ${padding * 2}px;
border-radius: ${borderRadius}px 0 0 0;
color: ${fontColor}
`
;
secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
margin-top: ${padding * 2}px;
border-radius: 0 ${borderRadius}px 0 0;
color: ${fontColor}
`
;
} else if (index === len -1) {
firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
margin-bottom: ${padding * 2}px;
border-radius: 0 0 0 ${borderRadius}px;
color: ${fontColor}
`
;
secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
margin-bottom: ${padding * 2}px;
border-radius: 0 0 ${borderRadius}px 0;
color: ${fontColor}
`
;
}
console.log(
logTemplate(username, `数据${index+1}`),
firstStyle,
secondStyle,
item
);
});
} else {
const firstStyle = `
background: ${usernameColor};
padding: ${padding}px;
border-radius: ${borderRadius}px 0 0 ${borderRadius}px;
color: ${fontColor}
`
;

const secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
border-radius: 0 ${borderRadius}px ${borderRadius}px 0;
color: ${fontColor}
`
;

console.log(
logTemplate(username, logName),
firstStyle,
secondStyle,
...data
);
}
};

const log = (...data) => {
switch(logType) {
case 'custom':
customLog(...data)
break;
default:
defaultLog(...data)
}
};

return {
log,
};
};

export default createBLog

API


唯一API createBLog(对!简单!易用!用起来没有负担!)


import createBLog from '@/utils/console-custom'

const myLog = createBLog(config)

配置 config: Object


一次配置,全局使用。(该部分是借鉴开源代码重构了配置内容)


配置项说明类型默认值
logTypelog 日志类型default、customdefault
usernamelog 的主人,也就是谁打的日志string-
logNamelog 的名字,也就是打的谁的日志string-
usernameColorusername 自定义背景颜色,接受 CSS background 的其他书写形式,例如渐变string#41b883
logNameColorlogName 自定义背景颜色,接受 CSS background 的其他书写形式,例如渐变string#35495e
paddingusername 和 logName 内边距,单位 pxnumber6
borderRadiususername 和 logName 圆角边框,单位 pxnumber6
fontColorusername 和 logName 字体颜色string#FFFFFF
usernameStyleusername 自定义样式,logType 为 custom 时候设置才生效,设置后则 usernameColor 的设置会失效string-
logNameStylelogName 自定义样式,logType 为 custom 时候设置才生效,设置后则 usernameColor 的设置会失效string-

基本用法 default



也是默认用法(default),同时也是最推荐大家用的一种方法。



vue2 版本


// main.js
import createBLog from '@/utils/console-custom'

const myLog = createBLog({
username: "bigger",
logName: "data",
usernameColor: "orange",
logNameColor: "#000000",
padding: 6,
borderRadius: 12,
fontColor: "#aaa",
});

// 不需要使用时单独自定义 logName 的全局绑定
Vue.prototype.$blog = myLog.log;

// 需要使用时单独自定义 logName 的全局绑定
Vue.prototype.$nlog = (logName, ...data) => {
myLog.logName = logName;
myLog.log(...data);
};

// vue2 组件里边使用
// 同时输入多个日志数据,可帮用户按照行的形式分开,好一一对应看清 log
this.$blog(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333);
this.$blog(111231231231231);

this.$nlog("logName", 2212121212122);

vue3 版本


// main.ts
import createBLog from '@/utils/console-custom'

const myLog = createBLog({
username: "bigger",
logName: "data",
usernameColor: "orange",
logNameColor: "#000000",
padding: 6,
borderRadius: 12,
fontColor: "#aaa",
});

app.config.globalProperties.$blog = myLog.log;

// vue3 组件里边使用
import { getCurrentInstance } from 'vue'

export default {
setup () {
const { proxy } = getCurrentInstance()

proxy.$blog(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333);
proxy.$blog(111231231231231);

proxy.$nlog("logName", 2212121212122);
}
}

自定义用法 custom



这部分我没有很多玩法,下边的例子也是借鉴别人的,主要全靠用户自己扩展 css 样式了。做一套自己喜欢的样式。



// main.js

// ....
Vue.prototype.$clog = (logName, ...data) => {
myLog.logType = "custom";
myLog.logName = logName;
myLog.usernameStyle = `text-align: center;
padding: 10px;
background-image: -webkit-linear-gradient(left, blue,
#66ffff 10%, #cc00ff 20%,
#CC00CC 30%, #CCCCFF 40%,
#00FFFF 50%, #CCCCFF 60%,
#CC00CC 70%, #CC00FF 80%,
#66FFFF 90%, blue 100%);`
;
myLog.logNameStyle = `background-color: #d2d500;
padding: 10px;
text-shadow: -1px -1px 0px #e6e600,-2px -2px 0px #e6e600,
-3px -3px 0px #e6e600,1px 1px 0px #bfbf00,2px 2px 0px #bfbf00,3px 3px 0px #bfbf00;`
;
myLog.log(...data);
};

// 提供的其他 css 样式
myLog.usernameStyle = `background-color: darkgray;
color: white;
padding: 10px;
text-shadow: 0px 0px 15px #00FFFF,0px 0px 15px #00FFFF,0px 0px 15px #00FFFF;`
;
myLog.logNameStyle = `background-color: gray;
color: #eee;
padding: 10px;
text-shadow: 5px 5px 0 #666, 7px 7px 0 #eee;`
;

myLog.usernameStyle = `background-color: darkgray;
color: white;
padding: 10px;
text-shadow: 1px 1px 0px #0000FF,2px 2px 0px #0000FF,-1px -1px 0px #E31B4E,-2px -2px 0px #E31B4E;`
;
myLog.logNameStyle = `font-family: "Arial Rounded MT Bold", "Helvetica Rounded", Arial, sans-serif;
text-transform: uppercase;/* 全开大写 */
padding: 10px;
color: #f1ebe5;
text-shadow: 0 8px 9px #c4b59d, 0px -2px 1px #fff;
font-weight: bold;
letter-spacing: -4px;
background: linear-gradient(to bottom, #ece4d9 0%,#e9dfd1 100%);`
;
// ....

其中渐变色的玩法


myLog.usernameStyle = `background-image: linear-gradient(to right, #ff0000, #ff00ff); padding: 6px 12px; border-radius: 2px; font-size: 14px; color: #fff; text-transform: uppercase; font-weight: 600;`;
myLog.logNameStyle = `background-image: linear-gradient(to right, #66ff00 , #66ffff); padding: 6px 12px; border-radius: 2px; font-size: 14px; color: #fff; text-transform: uppercase; font-weight: 600;`;

其中输出 emoji 字符


this.$nlog("😭", 2212121212122);
this.$nlog("🤡", 2212121212122);
this.$nlog("💩", 2212121212122);
this.$nlog("🚀", 2212121212122);
this.$nlog("🎉", 2212121212122);
this.$nlog("🐷", 2212121212122);

小伙伴们你肯定还有什么好玩的玩法!尽情发挥吧!


最后


还是想极力劝阻那些用 console.error() 调试代码的人,同时也能尽量少用 console 来调试,可以选择控制台断点、编译器断点等。还是不是很推荐使用 console 来调试,不过本文也可以让大家知道,其实 console 还有这种玩法。如果写 JS 库的时候也可以使用,让自己

作者:Bigger
来源:juejin.cn/post/7248448028297855035
的库极具自己的特色。

收起阅读 »

Compose + Fragment是一个不错的选择

Compose很好用,但是在真正应用到项目时,我们还需要解决一些问题。 我要开发一个这样的页面,外层用Bottom Navigation Activity,每个tab对应的一个fragment,页面内容我用Compose来填充,不使用xml来布局,因为Comp...
继续阅读 »

Compose很好用,但是在真正应用到项目时,我们还需要解决一些问题。


我要开发一个这样的页面,外层用Bottom Navigation Activity,每个tab对应的一个fragment,页面内容我用Compose来填充,不使用xml来布局,因为Compose太好用了,如果不是页面缓存的原因,我可能会选择全部使用Compose来写。


我曾尝试使用material3的Navigation Bar来处理tab,但是每次点击tab后,页面总会重新加载,虽然功能实现了,但是这不是我想要的结果,我希望重新点回页面时,页面的位置、状态还是之前的样子。


这是最终的效果


Screenshot_20221221_192042.png


创建 Bottom Navigation Activity


New Project时,选择Bottom Navigation Activity,系统就会帮你创建一个带有3个tab页面的应用,此时都是正常的,但是我的应用需要有4个tab,当我将第4个tab加上去之后,我发现tab item,只有选中的,才会展示文字标题,就像下面这个样子。


Screenshot_20221221_1941322.png


那为什么会这样呢,其实是因为Android有意而为之,关于Bottom Navigation Item的设计规范,可以参考这里Bottom Navigation,那要怎么样,才能让所有的tab item都能够展示文字标题呢,只需要在onCreate中设置下显示模式即可

val navView: BottomNavigationView = binding.navView
navView.labelVisibilityMode = NavigationBarView.LABEL_VISIBILITY_LABELED

添加 Compose 到 fragment 里


现在的代码,又臭又长,让我们先把Frament对应的xml布局文件删掉,不需要在xml中写布局了,另外把Fragment中onCreateView也清理一下,填充上简洁的代码片段,如下

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
Text("HomeFragment")
}
}
}

瞬间整个世界都清净了,值得一提的是,ComposeView是普通视图和Compose视图的桥梁,起着至关重要的作用。


添加这段代码后,会报错,因为没有添加对应Compose库,等待Android Studio自动补全需要的库文件,补全之后,错误会消失,并且会在build.gradle添加好依赖。


这个时候运行项目,我们发现一个奇怪的问题,页面顶部会出现一部分空白,有一块白色区域


Screenshot_20221221_2012272.png


我们需要找到activity_main.xml文件,删除其中的这行代码,这个高度为56dp的空白就可以消失了。

android:paddingTop="?attr/actionBarSize"

填充列表页面


不得不说Compose实在比xml布局好用多了,写起来更像是写SwiftUI和Flutter。列表页面填充完之后,我发现列表最后一项,并不能完全展示,被Bottom Nav View给挡住了


Screenshot_20221221_2025262.png


怎么解决呢,我们还是需要找到activity_main.xml文件,将fragment的高度,由match_parent改为默认0dp,不要过早的撑满容器,这样页面就能够显示正常了

android:layout_height="0dp"

替换布局文件中的fragment


解决掉上面的那么些问题之后,这里还有一个黄色的小提示,Android Studio建议我们把fragment替换成FragmentContainerView,当我们根据建议点击替换后,再运行项目,我们发现应用崩溃了


Caused by: java.lang.IllegalStateException: Activity xx.xx.xx.MainActivity@7b7a278 does not have a NavController set on 2131231026


此时我们需要找到MainActivity,将其中的一行代码

val navController = findNavController(R.id.nav_host_fragment_activity_main)

替换为

val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_main) as NavHostFragment
val navController = navHostFragment.navController

此时就一切都正常了,可以进行下一步了。


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

Kotlin 函数接口与普通接口的区别

记一次编写Demo时SonarLint提示警告而关注到的kotlin1.4新增的接口声明方式.// SonarLint警告: Make this interface functional or replace it with a function type. ...
继续阅读 »

记一次编写Demo时SonarLint提示警告而关注到的kotlin1.4新增的接口声明方式.

// SonarLint警告: Make this interface functional or replace it with a function type.
interface GitHubService {

@GET("search/repositories?sort=stars&q=Android")
suspend fun searchRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): GitHubResponse

}


// 声明为函数接口后修复警告
fun interface GitHubService {

@GET("search/repositories?sort=stars&q=Android")
suspend fun searchRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): GitHubResponse

}

普通接口


当使用 interface 关键字定义接口时,可以声明抽象方法和默认方法。抽象方法是没有具体实现的方法,需要在实现接口的类中提供具体的实现。默认方法是在接口中提供了一个默认的实现,实现类可以选择重写或者直接使用默认实现。

interface GitHubService {
fun getUser(username: String): User // 抽象方法

fun getRepositories(username: String): List<Repository> { // 默认方法
val user = getUser(username)
// 通过用户获取仓库列表的具体实现
// ...
return repositories
}
}

函数接口


使用 fun interface 声明的接口只能包含一个抽象方法,并且不能包含默认方法。这种类型的接口通常用于函数式编程和 lambda 表达式的场景。实现这个接口的类可以通过 lambda 表达式或者函数引用来提供方法的具体实现。

fun interface GitHubService {
fun getUser(username: String): User
}

// 通过 lambda 表达式为 getUser 方法提供了具体的实现。
// lambda 表达式接收一个用户名参数,并返回对应的用户对象。
val service = GitHubService { username ->
// 通过用户名获取用户的具体实现
// ...
return user
}

常见使用场景


interface



  1. 定义回调接口:接口可以用作定义回调函数的契约。一个类可以实现接口并提供回调方法的具体实现,然后将实现类的实例传递给其他需要回调的组件。




  2. 实现多态行为:接口可以作为多态的手段,使得不同的类可以以不同的方式实现相同的接口。这种多态的特性允许在运行时根据对象的具体类型调用相应的方法。




  3. 定义服务接口:接口可以定义服务契约,描述系统的服务功能,并规定服务方法的签名。其他模块或组件可以实现接口,并提供具体的服务实现。




  4. 定义插件机制:接口可以用于定义插件的扩展点。主应用程序定义接口,并提供默认实现,而插件可以实现这个接口并提供自定义的行为。




  5. 实现策略模式:接口可以用于实现策略模式,其中不同的类实现相同的接口,并提供不同的算法或策略。




fun interface



  1. 定义函数式接口:函数式接口只包含一个抽象方法,通常用于表示某个操作或行为。这样的接口可以作为函数类型的参数或返回值,使得函数可以被传递、组合和使用。




  2. 使用 lambda 表达式:函数式接口可以通过 lambda 表达式提供方法的具体实现。这种方式使得代码更加简洁、易读,并支持函数式编程的风格。




  3. 支持函数引用:函数式接口可以与函数引用一起使用,允许直接引用已有的函数作为接口的实现。这样可以减少冗余的代码,并提高代码的可读性。




总而言之,interface 关键字适用于一般的接口定义和多态行为,而 fun interface 关键字则适用于函数式编程和 lambda 表达式的场景。
总结一下,interface 关键字用于定义常规的接口,可以包含抽象方法和默认方法。而 fun interface 关键字用于定义函数式接口,只能包含一个抽象方法,并且不能包含默认方法。


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

🎉学算法在业务开发中到底有没有用?

前言 作为一名大学多次打铁的前ACM-ICPC竞赛选手,对这个问题应该算多少有点话语权,首先先说一下结论:有用。抛开面试不谈,算法的收益可能没有学某项技术那么明显,它算是潜移默化的增强你的思维方式,拓展思路,分析业务逻辑的时候可能会更加迅速,处理起复杂业务相对...
继续阅读 »

前言


作为一名大学多次打铁的前ACM-ICPC竞赛选手,对这个问题应该算多少有点话语权,首先先说一下结论:有用。抛开面试不谈,算法的收益可能没有学某项技术那么明显,它算是潜移默化的增强你的思维方式,拓展思路,分析业务逻辑的时候可能会更加迅速,处理起复杂业务相对更加得心应手一些。


如果要说在业务开发中使用过哪些算法,那基本可以说是使用不上,使用上的也是一些相对基础的算法,像什么并查集、最小生成树、最短路径、图论等等当时学的时候抓耳挠腮的高级算法基本都用不到。


一些基础的算法还是能够经常遇到的,今天来盘一下业务开发中常见的算法。


桶排序


维基百科上的解释为,桶排序是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间O(n)


可以说是一堆废话了,根本看不懂,翻译成人话就是利用数组下标的有序性,通过空间换时间的思想,只要出现一次数就在数组对应的下标上+1,然后遍历数组中那些大于1即可。

public static void main(String[] args) {
int[] source = new int[]{8,3,7,12,5,6,9};
bucketSort(source);
}

public static void bucketSort(int[] array){
int[] reslut = new int[13];
for (int i = 0; i < array.length; i++) {
reslut[array[i]]++;
}

for (int i = 0; i < reslut.length; i++) {
for (int j = 1; j <= reslut[i]; j++) {
System.out.println(i);
}
}
}

这种思想在业务开发中经常会用到,可能不是在排序的场景,由于本身就很简单就不再举例子了。


DFS


Depth First Search深度优先搜索,简称DFS,通常会把数据抽象为一个树形结构,从根节点出发,按预定的顺序扩展到子节点,如果子节点还有子节点则继续递归这个过程,直到当前节点没有子节点。当到达最深的一个叶子结点后处理完当前节点逻辑,则需要返回上一个节点重新寻找一个新的扩展节点。如此搜索下去,直到找到目标节点,或者搜索完所有节点为止。


image.png


以这个树为例。假如要找的值为节点2,DFS会首先按一个路线走到不能再走,也就是0 -> 1 -> 3。因为节点3没有子节点了,DFS会回到上一级,也就是节点1的位置,然后按照另一条路走到黑。也就是0 -> 1 -> 3 -> 4。由于4没有子节点,DFS会回到节点1,然后节点1所有的子节点都已经去过了,于是乎再回到节点0,然后去到节点2,最终找到它,路线就是0 -> 1 -> 3 -> 4 -> 2。


我们可以用在一个真实的场景里,就拿文件夹与文件的结构,根据数据库三范式,我们简单定义一下文件夹与文件和文件夹关联关系

// 文件夹
public class Folder {
private String folderId;
private String parentId;
}

// 文件
public class File {
private String fileId;
private String fileName;
}

// 文件夹关联关系
public class FolderFielRel {
private String folderId;
private String fileId;
}


这样就定义出一个简单的文件夹结构实体,从结构上来看实现一个树形的逻辑还是很简单的。联表查询即可,用FolderFielRel关联出每个文件夹的文件,然后文件夹通过parentId组成树。


逻辑很清晰,但是如果产品老哥说每一层文件夹需要展示当前文件夹数量。很多同学肯定会脱口而出,让前端拿每一层的文件数量不完了吗。但如果老哥拿出不仅要当前文件夹的数量,还要知道本级文件夹及子级文件夹的文件数量,阁下应该怎么应对呢。


这时候就要DFS出手了,按照DFS的思路,如果要求文件夹本级及子级的文件数量,我们需要从以这个文件夹为根节点的子树的叶子结点开始处理。


我们假设最终VO为FolderTreeModel,并且已经组成了一个完整的树

public class FolderTreeModel {
private String folderId;
private String parentId;
private Integer fileCount;
private List<File> fileList;
private List<FolderTreeModel> subFolders;
}

dfs

private void dfsBuildFolderFileCount(Map<String, Integer> statBook,
FolderTreeModel rootTreeNode) {

List<Folder> sonTopicTrees = rootTreeNode.getSubFolders;
for (FolderTreeModel folderTreeModel : sonTopicTrees) {

this.dfsBuildFolderFileCount(statBook, folderTreeModel);
}

rootTreeNode.setFileCount(statBook.getOrDefault(rootTreeNode.getFolderId(), 0) + rootTreeNode.getFileCount());
statBook.put(rootTreeNode.getParentId(), statBook.getOrDefault(rootTreeNode.getParentId(), 0) + rootTreeNode.getFileCount());
}

statBook为记录每个文件夹的文件树,通过folder_id映射,当我们dfs到第一个叶子节点,我们把当前节点的文件数累加到当前节点的父级节点的id对应的文件数上。当整个dfs搜索结束之后每个节点的本级及子级的文件数都存在了statBook中。


BFS


bfs虽然好用,但他有个致命的弱点,时间复杂度高,按刚才的文件夹例子需要n*logn的复杂度。在一些性能要求较高的查询场景下基本上都不会用。


既然有深度优先搜索,那当然就得有广度优先搜索,广度优先搜索,顾名思义,跟深度的区别为处理数据以广度往外扩散,通常会借助队列先进先出的数据结构。


image.png


以上图为例也就是说,访问数据的过程为,1 -> 2,3 -> 4,5 -> 6。那么我们使用bfs来改写dfs中的统计文件夹树的代码。

private void buildFolderFileCount(Map<String, Integer> statBook,
FolderTreeModel rootTreeNode) {

LinkedBlockingQueue<FolderTreeModel> bfsQueue = new LinkedBlockingQueue<>(rootTreeNode.getSubFolders());

while (!bfsQueue.isEmpty()) {
FolderTreeModel firstObj = bfsQueue.poll();
statBook.put(firstObj.getParentId(), statBook.getOrDefault(firstObj.getParentId(), 0) + firstObj.getFileCount());
bfsQueue.addAll(firstObj.getSubFolders());
}
}

一波BFS下来,statBook的结果与DFS的结果一样。效率从n*logn直接飙升到n。除了这三个简单的算法之外实在想不到还有什么算法在日常的业务开发中使用的了。


算法的魅力还是很大的,大就大在学的时候难受的一比,用的时候拍案称奇,有时候在代码里露两手心里的成就感直接彪到Integer.MAX_VALUE


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

什么是序列化和反序列化?

1. 什么是序列化和反序列化? 序列化和反序列化是计算机科学中两个重要的概念,主要应用在数据存储和网络传输等场景。 序列化是将数据结构或对象状态转换为可以存储或传输的形式的过程。这种形式要求在重新构建原始对象时能够在其他环境或在程序运行的后续时间点使用。这个过...
继续阅读 »

1. 什么是序列化和反序列化?


序列化和反序列化是计算机科学中两个重要的概念,主要应用在数据存储和网络传输等场景。


序列化是将数据结构或对象状态转换为可以存储或传输的形式的过程。这种形式要求在重新构建原始对象时能够在其他环境或在程序运行的后续时间点使用。这个过程主要通过将对象的数据转化为字节流来实现,也可以将其转化为格式如 XML 或 JSON 的数据,以便在网络上进行传输或在磁盘上进行存储。


举个例子,假设你有一个复杂的数据结构,如一个包含多个字段和数组的对象。你不能直接将这个对象写入文件或通过网络发送。因此,你需要先将其转换为可以写入或发送的格式,这就是序列化。


反序列化是序列化的逆过程,也就是从一系列字节中提取出数据结构。在接收到序列化的数据(如从文件或网络)后,通过反序列化,可以将数据恢复为原始的对象或数据结构,从而可以在程序中使用。


以上述的例子,反序列化就是读取该文件或接收到的数据,并根据序列化时的格式将其恢复为原始的对象。


这两个过程在很多编程语言中都有内置的支持,例如在 Java 中,你可以使用 java.io.Serializable 接口来对对象进行序列化和反序列化;在 Python 中,你可以使用 pickle 模块进行序列化和反序列化;在 JavaScript 中,你可以使用 JSON 的 stringifyparse 方法进行序列化和反序列化等。


2. 在java中实现序列化和反序列化,为什么要实现Serializable接口?


Serializable 接口是一种标记接口,本身并没有定义任何方法,但是它向 JVM 提供了一个指示,表明实现该接口的类可以被序列化和反序列化。这意味着你可以将该类的对象转换为字节流(序列化),然后再将这个字节流转回为对象(反序列化)。


序列化的过程是 JVM 通过反射来完成的,它会查看对象的类是否实现了 Serializable 接口。如果没有实现,将会抛出一个 NotSerializableException 异常。


实现 Serializable 接口的主要原因如下:




  1. 允许 JVM 序列化对象:如上所述,JVM 只会序列化实现 Serializable 接口的对象。




  2. 表示类的实例可以被安全地序列化:实现 Serializable 接口的类表示它满足 JVM 对于序列化的要求。这不仅仅是类的实例可以被转换为字节流,还包括这个类的实例可以被反序列化,而且反序列化后的对象保持了原始对象的状态。




  3. 允许类的实例在 JVM 之间进行传输:序列化的一个重要应用是在网络应用或分布式系统中,允许对象在 JVM 之间进行传输。只有实现 Serializable 接口的对象才能通过网络进行传输。




  4. 持久化:序列化也被用于将对象的状态持久化,即将对象存储在数据库、文件或内存中,然后在需要的时候再进行恢复。实现 Serializable 接口的对象可以被持久化。




综上,实现 Serializable 接口是为了使对象可以被序列化和反序列化,以便在不同的环境或时间点恢复对象的状态,或在 JVM 之间传输对象,或将对象的状态持久化。


3. 案例


当然可以。下面这个简单的例子中,我们将创建一个实现了 Serializable 接口的类 Person,然后进行序列化和反序列化:


首先,我们创建一个实现了 Serializable 接口的 Person 类:

import java.io.Serializable;

public class Person implements Serializable {
private static final long serialVersionUID = 1L;

private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

接下来,我们创建一个序列化这个 Person 对象的类 SerializeDemo

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class SerializeDemo {
public static void main(String[] args) {
Person p1 = new Person("John Doe", 30);

try {
FileOutputStream fileOut = new FileOutputStream("/tmp/person.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(p1);
out.close();
fileOut.close();
System.out.println("Serialized data is saved in /tmp/person.ser");
} catch (Exception e) {
e.printStackTrace();
}
}
}

现在我们来反序列化这个 Person 对象,创建一个类 DeserializeDemo

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class DeserializeDemo {
public static void main(String[] args) {
Person p = null;

try {
FileInputStream fileIn = new FileInputStream("/tmp/person.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
p = (Person) in.readObject();
in.close();
fileIn.close();
} catch (Exception e) {
e.printStackTrace();
}

System.out.println("Deserialized Person...");
System.out.println("Name: " + p.getName());
System.out.println("Age: " + p.getAge());
}
}

以上就是一个完整的 Java 序列化和反序列化的例子。首先我们创建了一个 Person 对象并序列化到一个文件中,然后我们从这个文件中读取数据并反序列化回 Person 对象。


作者:一只爱撸猫的程序猿
链接:https://juejin.cn/post/7247740398563000380
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

为什么开发者应该多关注海外市场

这篇文章主要是分享一下为什么我认为作为国内的开发者,应该多关注海外市场、做面向海外用户的产品。 早在 2000 年(点 com 泡沫那阵),国外就有了 indie hacker / indie maker 这个名词来称呼做“独立”产品的人。这里的 indie...
继续阅读 »

这篇文章主要是分享一下为什么我认为作为国内的开发者,应该多关注海外市场、做面向海外用户的产品。




早在 2000 年(点 com 泡沫那阵),国外就有了 indie hacker / indie maker 这个名词来称呼做“独立”产品的人。这里的 indie 是 independent 的意思,意在独立(解脱)于各种束缚,比如:朝九晚五的工作时间、固定的办公室、领导、或者是投资人。


而国内在最近几年也涌现了一拨独立开发者,多数以工程师为主,当然做的产品也是面向国内的市场。有做地不错的,像 Baye 的熊猫吃短信、vulgur 的极简时钟、Kenshin 的简阅等;但综合我这两年来对海外一些独立产品的研究,海外市场或许是更好的选择。


当然凡是都有个前提,就是你没有一个豪华的创始团队或者是顶级投资人的背书,就是个人或者两三人的小团队。这个条件我觉得可以覆盖 90% 的中国的开发者;对于另外 10% 的拥有资源或者金主爸爸靠山的个人或者团队,不仅“可以”还“应该”去磕中国市场。但这不是今天要讨论的主题。


在 BAT TMD 等巨头和背靠资源的精英创业者们的夹缝里,我觉得只有做面向海外市场的小产品是更有胜率一点;做国内市场面临的四个问题:


第一、不存在足够的空间给个人/小团队做独立产品存活。


Slack 大家应该都知道,在美国已经上市了,市值 200 亿刀。Slack 一直是被 qiang 的,但是为什么国内没有出现 Slack 这样的产品作为一个信息中心来连接各个办公工具?


其实有,还不少,但都没活太久。一部分原因是腾讯阿里都非常重视这个“商业流量入口”,不想有可能被对方占有了。另外是国内互联网生态,从 BAT TMD 巨头到小软件公司,都太封闭;不仅不开放,还相互制约,都想把自己流量的守住,所以就同时出现了三个 Slack:



  • 微信出个企业微信(还封杀了 wetools)

  • 阿里出个钉钉

  • 字节出个飞书


在这种巨头虎视眈眈且相互对抗的格局里,作为三缺(缺钱、缺资源、缺核心门槛)的个人或者团队是无法存活的。或许在 2010 年至 2016 年间还有草根产品团队依靠“热钱”注入有爆发的可能性,时至今日,特别是这个蜜汁 2020 的局势,是不太可能的了。


即使,你找到了一个空白的利基市场(niche),你接下来面对三个问题:需求验证(试错)、推广、和商业化。


第二点、需求验证或者叫“试错”成本高。


由于国情不同,咱们需要经过一些不可避免的审核流程来保证互联网的干净。这个没话说,在哪做事就守哪的规矩。但这“需求验证”的门槛可就提高了不少。


比如要做个网站吧,备案最快也两周。做游戏?有没有版号?做 app ?有没有软著?小程序(从用户端来讲)是个不错的创新,但是你最烦看到的是不是“审核不通过,类目不符合”?稍微做点有用户互动的功能都需要公司主体。公司注册、银行开户、做帐、以及各种实名制等;这些虽然都不是不可达到的门槛,但是去完成这些要耗费大量的精力,对于本身就单打独斗的开发者来说 - 太累了。


再看看海外,简直不要太爽。做 app 还是需要经过苹果和谷歌的审核,但几乎不会对程序本身以外的东西设置门槛。网站注册个域名,30 秒改个 DNS 指到你的 IP,Netlify 或 Vercel 代码一推,就自动构建、部署、上线了。哪怕你不会写代码或者会写代码但是想先验证一下需求,看看潜在用户的响应如何,国外有不少非常值得一样的 no code 或 low code 平台。这个以后可以单独写一篇。


OK,国内你也通过重重难关项目终于上线了,你面临剩下的两个问题:推广和商业化。


第三点、推广渠道少 && 门槛高。


海外市场的推广渠道更多元,比如 ProductHunt, IndieHackers, BetaList 等。这些平台不仅国内没有,我想表达的更重要一点是,这些平台用户都比较真诚和热心会实在地给你提建议,给你写反馈。国内也有几个论坛/平台,但是用户氛围和友好度就和上述几个没法比了。


在付费推广方面,国内门槛(资质、资金)都挺高,感觉只有大品牌才能投得起广告,这对于缺资金的团队来讲,又是闭门羹。而国外,facebook 和 google 拿个 50、100 刀去做个推广计划都没问题。


可以以非常低的成本来获取种子用户或者验证需求。


行吧,推广也做得不错,得到了批种子用户并且涨势还不错;就最后一步了,商业化。


第四点、商业化选择少。


说商业化选择少可能说过了。国内是由于巨头间的竞争太激烈,出现各种补贴手段,导致互联网用户习惯于免费的互联网产品,甚至觉得应该倒贴给他来使用;伸手党、白 piao 党挺多;付费以及版权意识都还有改善空间。


想想你都给哪些浏览器插件付费过?“插件还需要付钱?!”


而海外用户的付费意愿足够强烈,在以后的案例分享中就能体会到。一个小小的浏览器插件,做得精美,触碰到了用户的购买欲望,解决了他一个痛点,他就愿意购买。


下一篇就分享国外浏览器插件的产品案例。


顺便再说一个「免费 vs 付费」的问题,这个不针对哪个国家,全世界都一样。免费(不愿意付费)的用户是最难伺候的,因为他们不 value 你的产品,觉得免费的就是创造者轻易做出来的、廉价的。如果依着这部分不愿意付费的客户来做需求,产品只会越做越难盈利。


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

一个悄然成为世界最流行的操作系统

一个悄然成为世界最流行的操作系统 1987 的今天,Minix 诞生了 Minix 介绍 Minix 是 Mini Unix 的缩写,一个迷你版类 Unix 操作系统(约 300MB)。 Minix 原来是荷兰阿姆斯特丹的 Vrije 大学计算机科学系的安德...
继续阅读 »

一个悄然成为世界最流行的操作系统


1987 的今天,Minix 诞生了


Minix 介绍


Minix 是 Mini Unix 的缩写,一个迷你版类 Unix 操作系统(约 300MB)。



Minix 原来是荷兰阿姆斯特丹的 Vrije 大学计算机科学系的安德鲁・塔能鲍姆(Andrew S. Tanenbaum )教授所开发的一个类 UNIX 操作系统,开发初衷是方便教学使用(因为 AT&T 推出 Version 7 Unix 之后,将 Unix 源码进行了私有化)。Minix 全部的源代码共约 12,000 行,并置于他的著作Operating Systems: Design and Implementation(ISBN 0-13-637331-3)的附录里作为范例。Minix 的系统要求在当时来说非常简单,只要三片磁片就可以启动。


安德鲁・塔能鲍姆(Andrew S. Tanenbaum,1944 年 3 月 16 日 ——)计算机科学家,阿姆斯特丹自由大学教授,专精操作系统,类 Unix 教学操作系统 Minix 作者,出版多部计算机科学教科书,如《现代操作系统》《计算机组成》等。



img



Minix 一开始向使用者收取极低的授权费,直到 2004 年,塔能鲍姆重新架构与设计了整个系统,更进一步的将程序模块化,推出 MINIX 3。重新以 BSD 许可协议发布,成为开放源代码软件。



MINIX 3 的目标是比 Windows 或 Linux 更安全,在当时塔能鲍姆那份获得欧盟研究委员会(EuropeanResearchCouncil)5 年 250 万欧元资助的研究计划书里,Tanenbaum 解释了为何他认为现有的操作系统不安全:



最严重的可靠性及安全问题是与操作系统相关的那些。核心问题在于现有操作系统都不符合 POLA —— 最低授权原则 (PrincipleOfLeastAuthority)。POLA 说的是系统划分组件的方式,应当使必然存在于某个组件中的缺陷,不至于波及其他组件。每个组件仅应该得到完成它本身工作所需的权限,不多不少。具体来说,它应该无权读写属于其他组件的数据,无权读取它自身地址空间之外的任何计算机内存,无 权执行与它无关的敏感操作指令,无权访问不该访问的 I/O 设备,诸如此类。现有操作系统完全违反以上原则,结果就是造成众多可靠性及安全问题。



Minix 的流行与威胁



说起最流行的操作系统,我们也许会下意识地想到 Linux、Windows、macOS、iOS 和 Android 等一些当下主流的操作系统。但事实恐怕不是我们以为的那样,你可能不知道,但在英特尔近些年推出的所有处理器中都运行着一个操作系统。



没错,这个系统正是MINIX,就是因为英特尔,它成了世界上最流行的操作系统,不过这引起了人们的注意和担忧。


img



之所以引起人们的担忧是因为现代英特尔处理器中都有一个核心部件 —— 英特尔管理引擎 (Intel ME-Intel's Management Engine),用来管理协调内部的诸多模块,尤其是传统芯片组整合进入之后,处理器已经差不多成了 SoC 单芯片系统,更需要一个 “总管”,MINIX 正是负责这个工作。


而一旦英特尔管理引擎受到危及,有可能给攻击者留下严重的后门。研究人员特别指出,由于其在初始化硬件、电源管理和启动主处理器等方面扮演重要角色,无法完全被禁用。这让安全研究人员甚为担忧,因为除了英特尔外,谁都无法审查有无后门(毕竟英特尔使用自己修改过的 MINIX 3 没有开源)



MINIX 在处理器内部拥有自己的 CPU 内核和专属固件,完全独立于其他部分,而且完全隐形,操作系统和用户均不可见,运行权限更是达到了 Ring -3。


img



要知道,我们日常使用的应用程序权限级别都是 Ring 3,操作系统内核的是 Ring 0,这也是一般用户能够接触到的最低权限,MINIX 竟然深入到了 Ring -3。


事实上,即便是在休眠乃至关机状态下,MINIX 都在不间断运行,因为英特尔管理引擎要在处理器启动的同时就开始执行管理工作,还要负责芯片级的安全功能。



这就使得 MINIX 拥有至高无上的地位,而且只要你的电脑使用的是英特尔近些年推出的处理器,都有一个它在默默运行,这使得它成为名副其实的世界上最流行的系统。


Minix 和 Linux



Linux 是Linus Torvalds受到 Minix 的影响而作成的(Linus 不喜欢他的 386 计算机上的 MS-DOS 操作系统,而安装了 Minix,并以它为样本开发了原始的 Linux 核心)。但是这种影响更多在于非技术层面,确切地说是一种精神上的 “鼓舞”。在设计上,Linux 则和 Minix 相差很大,在 Linux 系统还没有自己的原生文件系统之前,曾采用 Minix 的文件系统。Minix 在核心设计上采用微核心,即将操作系统分成微核心和其上的提供文件系统、存储器管理、驱动程序等服务的服务程序;而 Linux 则和原始的 Unix 都采用宏内核。在 Linux 发展之初,双方还于 1992 年在新闻组上有过一场精彩的争论,被称为塔能鲍姆 - 林纳斯辩论。Minix 的作者和支持者认为使用宏内核是技术上的退步,而 Linux 的支持者认为 Minix 本身没有实用性。


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

流量思维的觉醒,互联网原来是这么玩的

流量就是钱,这是一个很原始的认知。但最开始我并不清楚流量和钱之间是如何相互转化的。 微创业,认知很低 大学时期,不管是出于积累项目经验、还是折腾新技术的需要,我有做过一个相对完整的项目。 没记错的话,应该是在20年10月份启动的。当时在宿舍里买了一台激光打印机...
继续阅读 »

流量就是钱,这是一个很原始的认知。但最开始我并不清楚流量和钱之间是如何相互转化的。


微创业,认知很低


大学时期,不管是出于积累项目经验、还是折腾新技术的需要,我有做过一个相对完整的项目。


没记错的话,应该是在20年10月份启动的。当时在宿舍里买了一台激光打印机,做起了点小买卖。所以就发现如果我手动给同学处理订单会非常麻烦。他们把文件通过qq发给我,我这边打开,排版,确认格式没有问题之后算一个价格,然后打印。


所以根据痛点,我打算开发一个线上自助下单,商户自动打印的一整套系统。


百折不挠,项目终于上线


21年年中克服各种困难终于实现整套系统,提供了小程序端,商户客户端,web端。


用户在手机或网页上上传文件后会自动转换为pdf,还提供了在线预览,避免因为格式与用户本地不同的纠纷。可以自由调节单双面、打印范围、打印分数、色彩等参数。实时算出价格,自助下单。下单后服务器会通知商户客户端拉取新任务,拉取成功后将文件丢入打印队列中。打印完成后商户客户端发送信息,并由服务器转发,告知用户取件。


image.png


image.png


大三下学期,宿舍里通过线上平台,在期末考试最忙那段期间经过了“订单高峰”的考验,成交金额上千块钱。看着我商户端里面一个个跳动的文件,就像流入口袋里的💰,开心。


商业化的很失败


没想到,我自己就是我最大的客户。


期末考完,其实想拉上我的同学大干一场,让校里校外的所有的商户,都用上我们的软件,多好的东西啊。对于盈利模式的概念非常模糊,同时也有很强的竞品。我的同学并不看好我。


我对商业化的理解也源自美团模式,美团是外卖的流量入口,所以对商户抽佣很高。滴滴是打车的流量入口,对司机的抽佣也很高。所以我认为,假设我未来成为了自助打印的流量入口,那应该也可以试试抽佣模式。


而且就算我不能为商户引流,也能解放他们的双手。


当时的我,一个人做技术,做UI,还要做商业计划,去地推,真的搞得我精疲力尽。反正后面觉得短期内变现无望,就去腾讯实习了。


其实也推广了2个商户,但是他们因为各种原因不愿意用。一个是出于隐私合规风险的考虑,一个是订单量少,不需要。


所以基本这个自助打印只能框死在高校。大学生打印的文件私密性很低,但是单价低,量多,有自助打印的需求。还有一部分自助打印的场景是在行政办事大厅,这种估计没点门门道道是开不进去的。


看不懂的竞品玩法


商户通过我的平台走,我这边并不无本万利。


因为开通了微信支付、支付宝支付,做过的小伙伴应该都知道办这些手续也会花一些钱,公司还要每年花钱养。还有需要给用户的文档成转换成pdf,提供在线预览,这很消耗算力和带宽,如果用户的成交单价非常低,哪怕抽佣5%都是亏的。比如用户打印了100份1页的内容,和打印了1份100页的内容,对我来说成本差别很大,前者很低,后者很高。


当时学校里已经有一部分商户用上自助打印了。一共有3个竞品。


竞品A:不抽佣,但是每笔订单对用户收取固定的服务费,界面简陋,有广告。


竞品B:不抽佣,不收用户的服务费,界面清爽无广告。


竞品C:彻彻底底走无人模式,店铺内基本没有老板,店铺是自营或加盟的。


前期缺乏市场调研,后期缺乏商业认知


当时我在没有摸清自己商业模式,市场调研也没怎么做好的情况下。一心想的就是先把东西做出来再说,卖不成自己还能学到技术。毕竟技术这个玩意不在项目里历练,永远都是纸上谈兵。所以对于商业化的设想就是搞不成就不搞了。


我当时的想法就是要“轻”运营,就是最好我的利润是稳定的,不会亏损的。商户如果要用就得每笔订单都给我一笔钱。


后面为了补齐和竞品的功能差距,也耗费了大量心力。让我把项目从一个大学课程设计,变成了一个有商业化潜力的产品。


竞品玩法的底层逻辑


商业化的时候,就发现这个市场还是蛮卷的,不可能直接和商户收钱。竞品B不仅免费,还想着帮商户创造额外收入,做“增益”。那我确实是没有精力去对抗的。


我当时也没搞懂自己的定位,我究竟是tob还是toc。当时想着我精心设计的界面,怎么可以被广告侵蚀?那可是我的心血。所以一心想把产品体验做的比竞品好,就会有人用。但这个定位也很模糊,因为如果商户不用你的,用户怎么可能用你的下单呢。


其实应该to rmb。面向利润开发。美,是奢侈品,那是属于我内心的一种追求,但他很难具有说服力让商户使用。在国内的各种互联网产品,不盈利的产品最后都是越来越粗糙,越来越丑的,都要降本增效。而rmb是必需品,如果不能为各方创造价值,那就没有竞争力。


所以后续分析了一下各家的玩法:


竞品A:传统商业模式,依靠用户强制付费和广告,市占率一般,和第一差了10倍数量级。


竞品B:烧钱模式,免费给商户用,免费给用户用,自己想办法别的渠道做增益,还要补贴商户。市占率第一。先圈地,再养鱼,变现的事之后再说。


竞品C:不单单做打印软件,卖的是项目。一整套自助打印店的解决方案,不知道店铺能不能赚钱,但是可以先赚加盟商的钱。这个对商业运作的要求会很高,我一时半会做不了。


大佬指点了一下我


他说,你看现在什么自助贩卖机,其实就是一个流量入口。至于别的盈利不盈利再说,但是流量是值钱的。


我最近去查阿拉丁指数,了解到了买量和卖量的观念,重新认识了流量,因为知道价格了。


买量和卖量是什么?


买量说的就是你做了一个app,花钱让别人给你引流。


卖量就是你有一个日活很高的平台,可以为别人引流。


买量和卖量如何结算?


一般分为cpc和cpa两种计价方式。前者是只要用户点击了我的引流广告,广告主就得掏钱。后者是用户可能还需要注册并激活账号,完成一系列操作才掏钱。


一般价格在0.1-0.3元,每次引流。


后面我查了一下竞品B在卖量,每天可以提供10-30w的uv,单次引流报价0.1元。也就是理想情况下,每天可以有1-3w的广告费收入。


侧面说明了竞品B的市占率啊,在这个细分市场做到这个DAU……


关于流量,逆向思维的建立


流量是实现商业利益的工具。


工具类应用通过为别人引流将流量变现,内容类应用通过电商将流量变现的更贵。


依靠流量赚钱有两种姿势,主动迎合需求,和培养需求。前者就是你可以做一些大家必须要用的东西,来获得流量。比如自助打印小程序,只要商户接入了,那么他的所有顾客都会为这个小程序贡献流量。比如地铁乘车码,所有坐地铁的人都会用到,比如广州地铁就在卖量,每天有几百万的日活。


培养需求就是做自己看好的东西,但是当下不明朗,尝试发掘用户潜在的需求。


流量,如果不能利用好,那就是无效流量。所以正确的姿势是,发掘目标人群 -> 设计变现方案 -> 针对性的开发他们喜欢的内容或工具 -> 完成变现。而不是 自己发现有个东西不错 -> 开发出来 -> 测试一下市场反应 -> 期盼突然爆红,躺着收钱。


研究报告也蛮有意思,主打的就是一个研究如何将用户口袋里的钱转移到自己口袋里。做什么产品和个人喜好无关,和有没有市场前景相关。


互联网是基于实体的


互联网并不和实体脱钩,大部分平台依赖广告收入,但广告基本都是实体企业来掏钱。还有电商也是,消费不好,企业赚不到钱,就不愿意投更多推广费。


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

程序员的快乐与苦恼

随着大环境的下行,互联网行业也受到一定的冲击,哀鸿遍野。 笔者也没有幸免,培养起来的人马陆续被优化,留下一丢光杆司令,我也回到的业务一线,心里很不是滋味。留下来的人,也不知道这艘船什么时候会沉没… 为了活命而拼命挣扎(内卷) 负面情绪和焦虑不停侵扰,以至于怀疑...
继续阅读 »

随着大环境的下行,互联网行业也受到一定的冲击,哀鸿遍野。


笔者也没有幸免,培养起来的人马陆续被优化,留下一丢光杆司令,我也回到的业务一线,心里很不是滋味。留下来的人,也不知道这艘船什么时候会沉没… 为了活命而拼命挣扎(内卷)


负面情绪和焦虑不停侵扰,以至于怀疑,当初选的这条路是不是正确的。


捡起买了多年,但是一直没看的《人月神话》, 开篇就讲了程序员这个职业的乐趣和苦恼,颇有共鸣,所以拿出来给大家分享


不管过去多少年,不管你的程序载体是纸带、还是 JavaScript,不管程序跑在高对比(high contract)的终端、还是 iPhone,程序员的快乐和烦恼并没有变化。


尽管国内软件行业看起来不是那么健康。我相信很多人真正热爱的是编程,而不仅仅是一份工作,就是那种纯粹的热爱。你有没有:



  • 为了修改一个 Bug,茶饭不思

  • 为了一个 idea,可以凌晨爬起来,决战到天亮

  • 我们享受没有人打扰的午后

  • 梦想着参与到一个伟大的开源项目

  • 有强烈的分享欲,希望我们的作品可以帮助到更多人, 希望能得到用户的反馈,即使是一个点赞







我们的快乐



《人月神话》:


首先,这种快乐是一种创建事物的纯粹快乐。如同小孩在玩泥巴时感到快乐一样,成年人喜欢创建事物,特别是自己进行设计。我想这种快乐是上帝创造世界的折射,一种呈现在每片独特的、崭新的树叶和雪花上的喜悦。


其次,这种快乐来自于开发对他人有用的东西。内心深处,我们期望我们的劳动成果能够被他人使用,并能对他们有所帮助。从这一角度而言,这同小孩用粘士为“爸爸的办公室”捏制铅笔盒没有任何本质的区别。


第三,快乐来自于整个过程体现出的一股强大的魅力——将相互啮合的零部件组装在一起,看到它们以精妙的方式运行着,并收到了预期的效果。比起弹球游戏机或自动电唱机所具有的迷人魅力,程序化的计算机毫不逊色。


第四,这种快乐是持续学习的快乐,它来自于这项工作的非重复特性。人们所面临的问题总有这样那样的不同,因而解决问题的人可以从中学习新的事物,有时是实践上的,有时是理论上的,或者兼而有之。


最后,这种快乐还来自于在易于驾驭的介质上工作。程序员,就像诗人一样,几乎仅仅在单纯的思考中工作。程序员凭空地运用自己的想象,来建造自己的“城堡”。很少有创造介质如此灵活,如此易于精炼和重建,如此容易实现概念上的设想(不过我们将会看到,容易驾驭的特性也有它自己的问题)。


然而程序毕竞同诗歌不同,它是实实在在的东西;它可以移动和运行,能独立产生可见的输出;它能打印结果,绘制图形,发出声音,移动支架。神话和传说中的魔术在我们的时代已变成现实。在键盘上键入正确的咒语,屏幕会活动、变幻,显示出前所未有的也不可能存在的事物。





编程就是一种纯粹创造的快乐,而且它的成本很低,我们只需要一台电脑,一个趁手的编辑器,一段不被人打扰的整块时间,然后进入心流状态,脑海中的想法转换成屏幕上闪烁的字符。
这是多巴胺带给我们的快乐。


飞机引擎






我们也有「机械崇拜」,软件不亚于传统的机械的复杂构造。 它远比外界想象的要复杂和苛刻,而我们享受将无数零部件有机组合起来,点击——成功运行的快感。


我们享受复杂的问题,被抽象、拆解成一个个简单的问题, 认真描绘分层的弧线以及每个模块轮廓,谨慎设计它的每个锯齿和接口。


我们崇尚有序,赞赏清晰的边界, 为的就是我们创造的世界能够稳定发展。




我们认为懒惰是我们的优点,我们也崇拜自动化,享受我们数据通过我们建设的管道在不同模块、系统或者机器中传递和加工;享受程序像多米诺骨牌一样,自动构建、测试、发布、部署、分发到每个用户的手中,优雅地跑起来。


因为懒,我们时常追求创造出能够取代自己的工具,让我们能腾出时间在新的世界探索。比如可以制造出我们的 Moss,帮我们治理让每个程序的生命周期,让它们优雅地死去又重生。




我们是一群乐于分享和学习的群体,有繁荣的技术社区、各种技术大会、技术群…


不管是分享还是编程本身,其实都是希望我们的作品能被其他人用到,能产生价值:



  • 我们都有开源梦,多少人梦想着能参与那些广为人知开源项目。很少有哪个行业,有这么一群人, 能够自我组织,用爱发电、完全透明地做出一个个伟大的作品。

  • 我们总会怀揣着乐观的设想,基于这种设想,我们会趋向打造更完美的作品,想象未来各种高并发、极端的场景,我们的程序能够游刃有余。

  • 我们总是不满足于现有的东西,乐于不停地改进,造出更多的轮子,甚至不惜代价推翻重来

  • 我们更会懊恼,自己投入大量精力的项目,无人问津,甚至胎死腹中。




看着它们,从简单到繁杂,这是一种迭代的快乐。








我们的苦恼



《人月神话》
然而这个过程并不全都是快乐的。我们只有事先了解一些编程固有的苦恼,这样,当它们真的出现时,才能更加坦然地面对。


首先,苦恼来自追求完美。因为计算机是以这样的方式来变戏法的: 如果咒语中的一个字符、一个停顿,没有与正确的形式一致,魔术就不会出现(现实中,很少有人类活动会要求如此完美,所以人类对它本来就不习惯)。实际上,我认为,学习编程最困难的部分,是将做事的方式向追求完美的方向调整"。




其次, 苦恼来自由他人来设定目标、供给资源和提供信息。编程人员很少能控制工作环境和工作目标。用管理的术语来说,个人的权威和他所承担的责任是不相配的。不过,似乎在所有的领域中,对要完成的工作,很少能提供与责任相一致的正式权威。而现实情况中,实际(相对于形式)的权威来自于每次任务的完成。


对于系统编程人员而言,对其他人的依赖是一件非常痛苦的事情。他依靠其他人的程序,而这些程序往往设计得并不合理、实现拙劣、发布不完整(没有源代码或测试用例)或者文档记录得很糟。所以,系统编程人员不得不花费时间去研究和修改,而它们在理想情况下本应该是可拿的、完整的。




下一个苦恼 —— 概念性设计是有趣的,但寻找琐碎的bug却是一项重复性的活动。伴随着创造性活动的,往往是枯燥沉闷的时间和艰苦的劳动。程序编制工作也不例外。




另外,人们发现调试和查错往往是线性收敛的,或者更糟糕的是,具有二次方的复杂度。结果,测试一拖再拖,寻找最后一个错误比第一个错误将花费更多的时间。




最后一个苦恼,有时也是一种无奈 —— 当投入了大量辛苦的劳动,产品在即将完成或者终于完成的时候,却己显得陈旧过时。可能是同事和竞争对手己在追逐新的、更好的构思;也许替代方案不仅仅是在构思,而且己经在安排了。





前阵子读到了 @doodlewind全职开源,出海创业:我的 2022,说的是他 all in 去做 AFFiNE 。我眼里只有羡慕啊,能够找到 all in 的事业…






这些年 OKR 也很火,我们公司也跟风了一年; 后面又回到了 KPI,轰轰烈烈搞全员KPI, 抓着每个人, 要定自己的全年KPI; 再后来裁员,KPI 就不再提起了…


这三个阶段的演变很有意思,第一个阶段,期望通过 OKR 上下打通,将目标捆在一起,让团队自己驱动自己。实际上实施起来很难,让团队和个人自我驱动起来并不是一件容易的事情,虽然用的是 OKR,但内核还是 KPI,或者说 OKR 变成了领导的 OKR。


后面就变成了 KPI, 限定团队要承担多少销售额,交付多少项目;


再后来 KPI 都没有了,换成要求每个人设定自己工作日历,不能空转,哪里项目缺资源,就调配到哪里,彻底沦为了人矿…




能让我们 all in 的事情,首先得是我们认同的事情,其次我们能在这件事情上深度参与和发挥价值,并获得预期的回报。这才能实现「自我驱动」


对于大部分人来说,很少有这种工作机会,唯一值得 all in的,恐怕就只有自己了。






所以程序员的苦恼很多,虽然编程是一个创造性的工作,但是我们的工作是由其他人来设定目标和提供资源的。


也就是说我们只不过是困在敏捷循环里面的一颗螺丝钉,每天在早会上机械复读着:昨天干了什么,今天要干什么。


企业总会想法设法量化我们的工作,最好是像流水线一样透明、可预测。




培训机构四个月就能将高中生打造成可以上岗敲代码的程序员。我们这个行业已经不存在我们想象中高门槛。


程序员可能就是新时代的蓝领工人,如果我们的工作是重复的、可预见的,那本质上就没什么区别了。






追求完美是好事,也是坏事。苛刻的编译器会提高开发的门槛,但同样可以降低我们犯错的概率。


计算机几乎不会犯错的,只是我们不懂它,而人经常会犯错。相比苛刻的计算机,人更加可怕:



  • 应付领导或产品拍脑袋的需求

  • 接手屎山代码

  • 浪费时间的会议

  • 狼性文化











还有一个苦恼是技术的发展实在太快了,时尚的项目生命周期太短,而程序员又是一群喜新厌旧的群体。


比如在前端,可能两三年前的项目就可以被定义为”老古董”了,上下文切换到这种项目会比较痛苦。不幸的是,这些老古董可能会因为某些程序员的偏见,出现破窗效应,慢慢沦为屎山。


我们虽然苦恼于项目的腐败,而大多数情况我们也是推手。




我们还有很多苦恼:



  • 35 岁危机,继续做技术还是转管理

  • 面试的八股文

  • 内卷

  • 被 AI 取代







对于读者来说,是快乐多一些呢?还是苦恼多一些呢?


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

15岁女生的2022年年终总结|15岁啦,我也开始接触编程

高考上岸 2022年,15岁的我经历高考的磨砺,成功上岸。然而由于前期填志愿时我的迷茫再加上高考理科生的身份,因此收到录取通知书时我感到有些许疑惑但也有几分合理--自动化专业。自动化?什么是自动化?   接触编程 此前我只知道它属于工科专业,在后来...
继续阅读 »

高考上岸


2022年,15岁的我经历高考的磨砺,成功上岸。然而由于前期填志愿时我的迷茫再加上高考理科生的身份,因此收到录取通知书时我感到有些许疑惑但也有几分合理--自动化专业。自动化?什么是自动化?


 


疑惑.jpeg


接触编程


此前我只知道它属于工科专业,在后来进入学校后才突然发现居然有这么多带有“自动化”的专业—-机械制造与自动化专业、电气信息工程及其自动化专业等。但我是自动化专业,怎么和前面不一样?哎﹖而且我也确实没有看错,我学的专业确实只有三个字--“自动化”。也让我好奇,它到底是一个什么神奇的专业,居然还能"分装组合”?但在完全不了解的情况下,上网搜索发现,自动化在百度百科上是这样介绍的:自动化是中国普通高等学佼本科专业,主要学习电子技术、计算机技术、网络技术、软件技术、控制技术等知识,是一个多学科交叉的专业。自动化研究方向涉及到计算机科学与技术、信息与通信工程、人工智能、网络空间信息安全、电子科学与技术、微电子学、机械工程以及电气工程等多个学科领域,研究内容从传统的控制理论、工业控制系统到信息物理融合系统,以及计算机视觉、人工智能,自动驾驶,数据挖掘等。看了半天更加疑惑了,乍一看有点高端。冷静总结探究一下,其实说的通俗一点就是学科性质交融,需要学的多而杂。其实不用说,就能知道我们肯定不可避免的要具备一定编写程序的能力。于是15岁的我也开始接触编程了。


 


开心.jpeg


对智能小车开发的道路


后来又因为对于智能小车比较感兴趣,参加了学校的社团,进行了每周一次的培训。


 


 


组装.png


小车2.png


但由于原先没有一点点编程基础和电气知识,再加上社团培训时长较短。每节课听下来都让我吞咽困难,没法消化。于是便尝试去寻找和观看相关视频,但经常是云里雾里,还是太抽象了,没有实物也没有最基础的知识加持。慢慢的失去了信心和耐心,将他搁置在一旁,不想再理会。就在我快完全忘记它的时候,它再次闯入我的视野......一个周六的晚上,感觉无事可做便抱着玩一玩的态度,用自己浅薄的编程技术编写了一段程序。拿出放在抽屉深处的开发板,不抱希望的烧录进开发板后。却突然惊奇的看见上面成功点燃的正在缓慢闪烁的led灯,我有几分不知所措。等反应过来,我又赶紧改变参数,惊喜的发现小灯也随之变化着。我这才激动的快要跳起来,这何止点燃的是灯呀,点燃的是我内心的热情!!!

开发板.png


平静后我美滋滋的躺在椅子上,盯住着屏幕上的程序,却总感觉它是不够简洁也不方便更改。“要是可以很方便调整就好了”我是这样想的。这让我恍然想起来前几天C语言学习的知识,我拿出书才发现和屏幕上的程序有几分相似,于是想要试着改一改,但是毕竟有区别。果不其然,编译过后出现一堆错误。但是这次我耐住了性子,尝试着上网查找,过了许久终于发现一段相似的代码,经过对比更改。马上进行编译,正常!烧录程序,正常!!更改数据,正常! !!恍然间突然感觉一身轻,一种愉悦的心情让我难以表达。台灯在这个时候突然熄灭,周围陷入了黑暗和死寂静,转目看时间才发现原来早已进入深夜。但是我一点也不困,回想起来感觉总是许久没有去过社团培训了,再翻看上次学习小车的时间,居然已经是一个月前的事情!

如今看来那虽然只是短短的十几行程序,也许学过一些相关知识的伙伴也能轻松写出,但是它本身承载的太多太多。慢慢的我去图书馆翻阅相关的书,即便有相关的很多型号各异的开发板和书籍,我实在不懂。但后来我仍在慢慢探索。我想,愿意既然选择继续那就总是不算迟的。


 


数字.png


    2022快进入尾声,兔年将至,愿一切顺遂!

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

我有个气人的同事......

前段时间看到掘金上发了好几个 console 自定义的仓库玩法,就想到自己曾经也这么玩过。就想着把自己故事写出来。 曾经,我有个气人的同事,总是喜欢用 console.error() 来调试代码,搞得我和他合作,看到控制台老难受了,就为他特殊定制了一个工具...
继续阅读 »

前段时间看到掘金上发了好几个 console 自定义的仓库玩法,就想到自己曾经也这么玩过。就想着把自己故事写出来。




曾经,我有个气人的同事,总是喜欢用 console.error() 来调试代码,搞得我和他合作,看到控制台老难受了,就为他特殊定制了一个工具库 console-custom。沉寂在个人仓库很久,前段时间看到别人也有类似仓库,也就想着把自己的也发出来。




其实,我个人不是很推荐在代码里 写 console.log 之类的来调试代码,更推荐去浏览器控制台去打断点来调试,更好的理清数据的流转,事件的先后顺序等。



背景


官方背景:



  • 方便大家调试代码的时候,在浏览器控制台输出自定义个性化日志。

  • 防止控制台输出密密麻麻的 console.log,一眼看不到想看的。

  • 防止某个气人的小伙伴老是使用 console.error,强迫症不允许。

  • ......


真实背景:


其实,是我之前有个小伙伴同事——“小白菜”(也是为啥函数名叫 blog 的原因之一,下边会看到),他调试代码,打印输出总是喜欢 console.error(),用完了还不自己清理,大家协同开发的时候,git pull 他的代码后,总是让人就很难受!看着一堆报错,一时半会看不清是程序自己的报错,还是调试的输出!强迫症就犯了!想骂街......


不......不......要冷静!



  • 编码千万行

  • 调试要输出

  • log不规范

  • 同事两行泪


效果


浏览器页面 page


tu1.jpg


浏览器控制台 console


image.png



有个重点、痛点是这个, console.log(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333); 打印的数据多的时候不换行,需要找半天,我专门给处理成 分行, 一行一行展示了,这样好看清数据。



这个工具库有以下几个特点:



  1. 支持输入多个数据,并分行打印出来,并能看出是个整体

  2. 支持自己修改自己的默认样式部分,配置自己的默认部分

  3. 支持额外的自定义部分,拓展用户更多的玩法

  4. ......


其实 console 由于可以自定义,其实会有很多玩法,我个人在此主要的思路是



  1. 一定要简单,因为 console.log 本身就很简单,尽量不要造成使用者心智负担。

  2. 就简单的默认定制一个彩色个性化的部分,能区分出来,解决那个气人同事所谓的痛点就好。

  3. 代码要少,不要侵入,不要影响用户的业务代码


源码

// src/utils/console-custom.js
const GourdBabyColorMap = new Map([
["1", "#FF0000"],
["2", "#FFA500"],
["3", "#FFFF00"],
["4", "#008000"],
["5", "#00FFFF"],
["6", "#0000FF"],
["7", "#800080"],
]);

const createBLog = (config) => {
const logType = config.logType || "default";
const username = config.username || "";
const logName = config.logName || "";
const usernameColor = config.usernameColor || "#41b883";
const logNameColor = config.logNameColor || "#35495e";
const padding = config.padding || 6;
const borderRadius = config.borderRadius || 6;
const fontColor = config.fontColor || "#FFFFFF";
const usernameStyle = config.usernameStyle || "";
const logNameStyle = config.logNameStyle || "";

const logTemplate = (username = "myLog", logName = "") =>
`${username ? '%c' + username : ''} ${logName ? '%c' + logName : ''} `;

const customLog = (...data) => {
console.log(
logTemplate(username, logName),
usernameStyle ? usernameStyle : `background: ${usernameColor}; padding: 6px; border-radius: 6px 0 0 6px; color: #fff`,
logNameStyle ? logNameStyle : `background: ${logNameColor}; padding: 6px; border-radius: 0 6px 6px 0; color: #fff`,
...data
);
};

const defaultLog = (...data) => {
const len = data.length;
if (len > 1) {
data.map((item, index) => {
let firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
border-radius: 0 0;
color: ${fontColor}
`;
let secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
border-radius: 0 0;
color: ${fontColor}
`;
if (index === 0) {
firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
margin-top: ${padding * 2}px;
border-radius: ${borderRadius}px 0 0 0;
color: ${fontColor}
`;
secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
margin-top: ${padding * 2}px;
border-radius: 0 ${borderRadius}px 0 0;
color: ${fontColor}
`;
} else if (index === len -1) {
firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
margin-bottom: ${padding * 2}px;
border-radius: 0 0 0 ${borderRadius}px;
color: ${fontColor}
`;
secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
margin-bottom: ${padding * 2}px;
border-radius: 0 0 ${borderRadius}px 0;
color: ${fontColor}
`;
}
console.log(
logTemplate(username, `数据${index+1}`),
firstStyle,
secondStyle,
item
);
});
} else {
const firstStyle = `
background: ${usernameColor};
padding: ${padding}px;
border-radius: ${borderRadius}px 0 0 ${borderRadius}px;
color: ${fontColor}
`;

const secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
border-radius: 0 ${borderRadius}px ${borderRadius}px 0;
color: ${fontColor}
`;

console.log(
logTemplate(username, logName),
firstStyle,
secondStyle,
...data
);
}
};

const log = (...data) => {
switch(logType) {
case 'custom':
customLog(...data)
break;
default:
defaultLog(...data)
}
};

return {
log,
};
};

export default createBLog

API


唯一API createBLog(对!简单!易用!用起来没有负担!)

import createBLog from '@/utils/console-custom'

const myLog = createBLog(config)

配置 config: Object


一次配置,全局使用。(该部分是借鉴开源代码重构了配置内容)









































































配置项说明类型默认值
logTypelog 日志类型default、customdefault
usernamelog 的主人,也就是谁打的日志string-
logNamelog 的名字,也就是打的谁的日志string-
usernameColorusername 自定义背景颜色,接受 CSS background 的其他书写形式,例如渐变string#41b883
logNameColorlogName 自定义背景颜色,接受 CSS background 的其他书写形式,例如渐变string#35495e
paddingusername 和 logName 内边距,单位 pxnumber6
borderRadiususername 和 logName 圆角边框,单位 pxnumber6
fontColorusername 和 logName 字体颜色string#FFFFFF
usernameStyleusername 自定义样式,logType 为 custom 时候设置才生效,设置后则 usernameColor 的设置会失效string-
logNameStylelogName 自定义样式,logType 为 custom 时候设置才生效,设置后则 usernameColor 的设置会失效string-

基本用法 default



也是默认用法(default),同时也是最推荐大家用的一种方法。



vue2 版本

// main.js
import createBLog from '@/utils/console-custom'

const myLog = createBLog({
username: "bigger",
logName: "data",
usernameColor: "orange",
logNameColor: "#000000",
padding: 6,
borderRadius: 12,
fontColor: "#aaa",
});

// 不需要使用时单独自定义 logName 的全局绑定
Vue.prototype.$blog = myLog.log;

// 需要使用时单独自定义 logName 的全局绑定
Vue.prototype.$nlog = (logName, ...data) => {
myLog.logName = logName;
myLog.log(...data);
};

// vue2 组件里边使用
// 同时输入多个日志数据,可帮用户按照行的形式分开,好一一对应看清 log
this.$blog(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333);
this.$blog(111231231231231);

this.$nlog("logName", 2212121212122);

vue3 版本

// main.ts
import createBLog from '@/utils/console-custom'

const myLog = createBLog({
username: "bigger",
logName: "data",
usernameColor: "orange",
logNameColor: "#000000",
padding: 6,
borderRadius: 12,
fontColor: "#aaa",
});

app.config.globalProperties.$blog = myLog.log;

// vue3 组件里边使用
import { getCurrentInstance } from 'vue'

export default {
setup () {
const { proxy } = getCurrentInstance()

proxy.$blog(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333);
proxy.$blog(111231231231231);

proxy.$nlog("logName", 2212121212122);
}
}

自定义用法 custom



这部分我没有很多玩法,下边的例子也是借鉴别人的,主要全靠用户自己扩展 css 样式了。做一套自己喜欢的样式。

// main.js

// ....
Vue.prototype.$clog = (logName, ...data) => {
myLog.logType = "custom";
myLog.logName = logName;
myLog.usernameStyle = `text-align: center;
padding: 10px;
background-image: -webkit-linear-gradient(left, blue,
#66ffff 10%, #cc00ff 20%,
#CC00CC 30%, #CCCCFF 40%,
#00FFFF 50%, #CCCCFF 60%,
#CC00CC 70%, #CC00FF 80%,
#66FFFF 90%, blue 100%);`;
myLog.logNameStyle = `background-color: #d2d500;
padding: 10px;
text-shadow: -1px -1px 0px #e6e600,-2px -2px 0px #e6e600,
-3px -3px 0px #e6e600,1px 1px 0px #bfbf00,2px 2px 0px #bfbf00,3px 3px 0px #bfbf00;`;
myLog.log(...data);
};

// 提供的其他 css 样式
myLog.usernameStyle = `background-color: darkgray;
color: white;
padding: 10px;
text-shadow: 0px 0px 15px #00FFFF,0px 0px 15px #00FFFF,0px 0px 15px #00FFFF;`;
myLog.logNameStyle = `background-color: gray;
color: #eee;
padding: 10px;
text-shadow: 5px 5px 0 #666, 7px 7px 0 #eee;`;

myLog.usernameStyle = `background-color: darkgray;
color: white;
padding: 10px;
text-shadow: 1px 1px 0px #0000FF,2px 2px 0px #0000FF,-1px -1px 0px #E31B4E,-2px -2px 0px #E31B4E;`;
myLog.logNameStyle = `font-family: "Arial Rounded MT Bold", "Helvetica Rounded", Arial, sans-serif;
text-transform: uppercase;/* 全开大写 */
padding: 10px;
color: #f1ebe5;
text-shadow: 0 8px 9px #c4b59d, 0px -2px 1px #fff;
font-weight: bold;
letter-spacing: -4px;
background: linear-gradient(to bottom, #ece4d9 0%,#e9dfd1 100%);`;
// ....

其中渐变色的玩法

myLog.usernameStyle = `background-image: linear-gradient(to right, #ff0000, #ff00ff); padding: 6px 12px; border-radius: 2px; font-size: 14px; color: #fff; text-transform: uppercase; font-weight: 600;`;
myLog.logNameStyle = `background-image: linear-gradient(to right, #66ff00 , #66ffff); padding: 6px 12px; border-radius: 2px; font-size: 14px; color: #fff; text-transform: uppercase; font-weight: 600;`;

其中输出 emoji 字符

this.$nlog("😭", 2212121212122);
this.$nlog("🤡", 2212121212122);
this.$nlog("💩", 2212121212122);
this.$nlog("🚀", 2212121212122);
this.$nlog("🎉", 2212121212122);
this.$nlog("🐷", 2212121212122);

小伙伴们你肯定还有什么好玩的玩法!尽情发挥吧!


最后


还是想极力劝阻那些用 console.error() 调试代码的人,同时也能尽量少用 console 来调试,可以选择控制台断点、编译器断点等。还是不是很推荐使用 console 来调试,不过本文也可以让大家知道,其实 console 还有这种玩法。如果写 JS 库的时候也可以使用,让自己的库极具自己的特色。


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

你可能一直在kt文件中写Java代码

关注 Kotlin 的大多数开发中可能都是 Android 开发者吧,大家基本也都是慢慢从 Java 逐步迁移到 Kotlin。 得益于 Kotlin 与 Java 之间良好的互通性,有的时候可能我们写代码还是比较随性的,尤其是依旧按照自己过去写 Java 的...
继续阅读 »

关注 Kotlin 的大多数开发中可能都是 Android 开发者吧,大家基本也都是慢慢从 Java 逐步迁移到 Kotlin。


得益于 Kotlin 与 Java 之间良好的互通性,有的时候可能我们写代码还是比较随性的,尤其是依旧按照自己过去写 Java 的编程习惯,书写 Kotlin 代码。


但实际上 Kotlin 与 Java 之间编码风格还是有很大的差异的,你的代码可能还是 Java 的咖啡味。


空判断


你大概早就听腻了 Kotlin 的空安全,可是你在代码里是否还在写if (xx != null) 这样满是咖啡味的代码呢?


现在把你的空判断代码都删除掉吧。使用 ?. 安全调用来操作你的对象。

// before
fun authWechat() {
if (api != null) {
if (!api.isWXAppInstalled) {
ToastUtils.showErrorToast("您还未安装微信客户端")
return
}
val req = SendAuth.Req()
req.scope = "snsapi_userinfo"
req.state = "none"
api.sendReq(req)
}
}

这段代码粗略看没什么问题吧,判断 IWXAPI 实例是否存在,存在的话判断是否安装了微信,未安装就 toast 提示


但是更符合 Kotlin 味道的代码应该是这样的

// after
fun authWechat() {
api?.takeIf { it.isWXAppInstalled }?.let {
it.sendReq(
SendAuth.Req().apply {
scope = "snsapi_userinfo"
state = "none"
}
)
} ?: api?.run { ToastUtils.showErrorToast("您还未安装微信客户端") }
}

使用?.安全调用配合 ?: Elvis 表达式,可以覆盖全部的空判断场景,再配合 takeIf 函数,可以让你的代码更加易读(字面意思上的)


上述代码用文字表达其实就是:


可空对象?.takeIf{是否满足条件}?.let{不为空&满足条件时执行的代码块} ?: run { 为空|不满足条件执行的代码块 }


这样是不是更加符合语义呢?


作用域


还是上面的例子,实例化一个req对象

val req = SendAuth.Req()
req.scope = "snsapi_userinfo"
req.state = "none"

更有 Kotlin 味道的代码应该是:

SendAuth.Req().apply {
scope = "snsapi_userinfo"
state = "none"
}

使用apply{} 函数可以帮我们轻松的初始化对象,或者配置参数,它更好的组织了代码结构,明确了这个闭包处于某个对象的作用域内,所有的操作都是针对这个对象的。


在 Kotlin 的顶层函数中,提供了数个作用域函数,包括上文中的 let 函数,他们大同小异,具体的使用其实更多看编码风格的取舍,例如在我司我们有如下约定:




  • apply{} 用于,修改、配置对象




  • with(obj){} 用于,读取对象的字段,用于赋值给其他变量


    with() 可以显式的切换作用域,我们常将它用于某个大的闭包内,实现局部的作用域切换,


    而且仅用作读时无需考虑作用域的入参命名问题 (多个嵌套的作用域函数往往会带来it的冲突)




  • let{} 用于配合?.用于非空安全调用,安全调用对象的函数




  • run{} 执行代码块、对象映射


    run 函数是有返回值的,其返回值是 block块的最后一行,所以它具备对象映射的能力,即将当前作用域映射为另外的对象




  • also{} 对象,另作他用




当出现超过两行的同一对象使用,无论是读、写,我们就应该考虑使用作用域函数,规范组织我们的代码,使之更具有可读性。


这几个函数其实作用效果可以互相转换,故而这只关乎编码风格,而无关对错之分。


?: Elvis 表达式

非空赋值



虽然说在 Kotlin 中可空对象,使用 ?. 可以轻松的安全调用,但是有的时候我们需要一个默认值,这种情况我们就需要用到 ?: Elvis 表达式。


例如:

val name: String = getName() ?: "default"

假如 getName() 返回的是一个 String? 可空对象,当他为空时,通过 ?: Elvis 表达式直接给予一个默认值。


配合 takeIf{} 实现特殊的三元表达式


总所周知,kotlin 中没有三元表达式 条件 ? 真值 : 假值,这一点其实比较遗憾,可能是因为 ? 被用作了空表达。


在kotlin 中我们如果需要一个三元表达该怎么做呢?if 条件 真值 else 假值,这样看起来也很简洁明了。


还有一种比较特殊的情况,就是我们判断逻辑,实际上是这个对象是否满足什么条件,也就是说既要空判断,又要条件判断,返回的真值呢又是对象本身。


这种情况代码可能会是这样的:

fun getUser(): User? = null
fun useUser(user: User) {}
// 从一个函数中获得了可空对象
val _userNullable = getUser()
// 判断非空+条件,返回对象或者构造不符合条件的值
val user =  if (_userNullable != null && _userNullable.user == "admin") {
   _userNullable
} else {
   User("guess")
}
//使用对象
useUser(user)

这个语句如果我们将if-else塞到 useUser() 函数中作为三元也不是不可以,但是看起来就比较乱了,而且我们也不得不使用一个临时变量_userNullable


如果我们使用 ?: Elvis 表达式 配合 takeIf{} 可以看起来更为优雅的表达

fun getUser(): User? = null
fun useUser(user: User) {}
// 使用`?:` Elvis 表达式简化的写法
useUser(getUser()?.takeIf { it.user == "admin" } ?: User("guest"))

这看起来就像是一个特殊的三元 真值.takeIf(条件) ?: 假值,在这种语义表达下,使用?: Elvis 表达式起到了简化代码,清晰语义的作用。


提前返回


当然 ?: Elvis 表达式还有很多其他用途,例如代码块的提前返回

fun View.onClickLike(user: String?, isGroup: Boolean = false) = this.setOnClickListener {
user?.takeUnless { it.isEmpty() } ?: return@setOnClickListener
StatisticsUtils.onClickLike(this.context, user, isGroup)
}

这里我们对入参进行了非空判断与字符长度判断,在?: Elvis 表达式后提前 return 避免了后续代码被执行,这很优雅也更符合语义。


这里不是说不能用 if 判断,那样虽然可以实现相同效果,但是额外增加了一层代码块嵌套,看起来不够整洁明了。


这些应用本质上都是利用了 ?: Elvis 表达式的特性,即前者为空时,执行后者。


使用函数对象


很多时候我们的函数会被复用,或者作为参数传递,例如在 Android 一个点击事件的函数可能会被多次复用:

// before
btnA.setOnClickListener { sendEndCommand() }
btnB.setOnClickListener { sendEndCommand() }
btnC.setOnClickListener { sendEndCommand() }

例如这是三个不同帧布局中的三个结束按钮,他们对于的点击事件是同一个,这样写其实也没什么问题,但是他不够 Kotlin 味,我们可以进一步改写

btnA.setOnClickListener(::sendEndCommand)
btnB.setOnClickListener(::sendEndCommand)
btnC.setOnClickListener(::sendEndCommand)

使用 :: 双冒号,将函数作为函数对象直接传递给一个接收函数参数的函数(高阶函数),这对于大量使用高阶函数的链式调用场合更加清晰明了,也更加函数式


ps:这里需要注意函数签名要对应,例如setOnClickListener 的函数签名是View->Unit,故而我们要修改函数与之一致

@JvmOverloads
fun sendEndCommand(@Suppress("UNUSED_PARAMETER") v: View? = null) {

}

使用 KDoc


你还在用 BugKotlinDocument 这样的插件帮你生成函数注释么?你的函数注释看起来是这样的么?

/**
* 获取全部题目的正确率,x:题目序号,y:正确率数值(float)
* @param format ((quesNum: Int) -> String)? 格式化X轴label文字
* @param denominator Int 计算正确率使用的分母
* @return BarData?
*/

这样的注释看起来没什么问题,也能正确的定位到代码中的参数,但实际上这是 JavaDoc ,并不是 KDoc,KDoc使用的是类似 Markdown 语法,我们可以改写成这样:

/**
* 获取全部题目的正确率的BarData,其中,x:题目序号,y:正确率数值(float)。
* [format] 默认值为null,用于格式化X轴label文字,
* [denominator] 除数,作为计算正确率使用的分母,
* 返回值是直接可以用在BarChart中的[BarData]。
*/

KDoc 非常强大,你可以使用 ``` 在注释块中写示例代码,或者JSON格式


例如:

/**
* 使用json填充视图的默认实现,必须遵循下面的数据格式
* ```json
* [
* {"index":0,"answer":["对"]},
* {"index":1,"answer":["错"]},
* {"index":2,"answer":["对"]},
* ]
* ```
* [result] 必须是一个JSONArray字符串
*/

在AS中他会被折叠成非常美观的注释块:


image.png






写在最后


文章最后我们看一段 ”Java“ 代码与 Kotlin 代码的对比吧:

// before
override fun onResponse(
   call: Call,
   response: Response
) {
   val avatarPathResult = response.body()
   if (avatarPathResult != null) {
       val status = avatarPathResult.status
       if (status == 200) {
           val data = avatarPathResult.data
           MMKVUtils.saveAvatarPath(data)
      } else {
           MMKVUtils.saveAvatarPath("")
      }
  } else {
       MMKVUtils.saveAvatarPath("")
  }
}

// after
override fun onResponse(
   call: Call,
   response: Response,
) {
   with(response.body()) {
       MMKVUtils.saveAvatarPath(this?.data?.takeIf { status == 200 } ?: "")
  }
}

鉴于有些同学对本文的观点有一些疑惑,这里我贴上 JetBrains 官方开发的 Ktor 项目中对各种语法糖使用的统计(基于 main 分支,23-6-9)


语句计数
if.*!= null331
if.*== null216
.let {}1210
?.let {}441
.apply {}469
?.apply {}11
run {}37
with\(.*\) \{219
.also{}119
?:1066
?.1239
.takeIf54
\.takeIf.*\?:13
.takeUnless2


这个项目可以说很能代表 JetBrains 官方对 Kotlin 语法的一些看法与标准了吧,前文我们也说了,如何取舍只关乎编码风格,而无关对错之分。


用 Java 风格是错的吗?那自然不是,只是显然空判断与安全调用两者相比,安全调用更符合 Kotlin 的风格。


重复的写对象名是错误的么?自然也不是,只是使用 apply 更优雅更 Kotlin。


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

收起阅读 »

编译优化跌落神坛

最近在一次技术分享中,有网友问我小公司可以考虑做哪些编译优化?我觉得这个课题也还是挺有必要展开下讲讲的。 编译优化方面其实我个人觉得并不一定是特别高大上的东西,除了一些特别深水区的地方,还是有些东西还是能从细微处进行展开的。今天我们就尝试下拉他下水。 组件化 ...
继续阅读 »

最近在一次技术分享中,有网友问我小公司可以考虑做哪些编译优化?我觉得这个课题也还是挺有必要展开下讲讲的。


编译优化方面其实我个人觉得并不一定是特别高大上的东西,除了一些特别深水区的地方,还是有些东西还是能从细微处进行展开的。今天我们就尝试下拉他下水。


组件化


组件化和编译优化有啥关系? 有些人甚至觉得可能会拖慢整个工程编译速度吧。


在我的认知中gradle是一个并行编译的模式,所以当我们的工程的taskGraph确定之后,很多没有依赖关系的模块是可以并行编译的。这样的情况下我们就可以充分的使用并行编译的能力。


而且从另外一个角度gradle build cache出发,我们可以拥有更细致的buildcache。如果当前模块没有变更,那么在增量过程中也可以变得更快。


巧用DI或者SPI


以前我其实挺反感DI(依赖注入)的,我认为会大大的增加工程的复杂度。毕竟掌握一门DI(依赖注入)还是挺麻烦的。


但是最近我突然想通了,我之前看GE(Gradle Enterprise)的时候,发现有些业务模块之间有依赖关系,导致了这些模块的编译顺序必须在相对靠后的一个状态,但是因为com.android.application会依赖所有业务模块,这个时候就会触发水桶理论,该模块的编译就是整个水桶最短的短板。也让整个工程的并行编译有一段时间不可用。


那么这种问题我们可以通过DI(依赖注入)或者SPI(服务发现)的形式进行解决。模块并不直接依赖业务的实现而是依赖于业务的一个抽象接口进行编程,这样可以优化既有工程模块间的依赖关系。


道理虽然简单,但是真的去抽象也会考验对应开发的代码能力。


关注AGP优化方案


这部分我觉得是很容易被开发遗忘的,比如我们最近在做的buildConfig,AIDL编译时默认关闭,还有去年到今年一直在进行的非传递R文件的改造。官方的Configuration Cache,还有后续的全局AGP通用属性,还有默认关闭Jetfied等等,这些跟随AGP迭代的属性。


结果上看关闭非必要模块的buildConfig AIDL可以让全量编译时间缩短大概2min,当然主要是我们模块多。而非传递R可以让我们的工程的R文件变更的增量缓存更好管理。


Kotlin技术栈更新


kt已经发布很长时间了,最近最让我期待的是kt2.0带来的K2的release版本。能大大的提升kotlin compiler编译速度。


另外还有kt之前发布的ksp,是一个非常牛逼的kapt的替代方案。而且官方也在逐步对ksp进行支持。比如room这个框架就已经适配好了ksp。我自己也写过好几个ksp插件。我个人认为还是非常酷的。


最后还有一些废弃东西的下架,比如KAE(kotlin-android-extensions)这种已经被明确说是后续不继续进行支持的框架。我们最近尝试的方案是通过Android Lint把所有声明KAE的进行报错处理。剩下的就是业务自行决定是改成findViewById还是viewBinding



KAEDetector



花点钱接个GE


如果这个老板不太差这么点钱,我真的觉得GE(Gradle Enterprise)是个非常好的选择。可以很直观的看出一些工程的编译问题,而且对接的gradle同学也都很专业。


不要轻易魔改Gradle


非必要的情况下,个人是不太建议同学们改这个的。非常容易破坏整个编译缓存系统!这里不仅仅只针对Transform,还有对任意编译产物进行修改的,比如xml,资源等等。当然如果可以的话,尽可能的使用最新AGP提供的一些新的api去进行修改吧。这个可以多参考下2BAB大神的一些文章。


另外比如很多非必要的Transform转化建议本地编译的时候就直接关闭了,虽然可能会出现一些本地行为不一致的情况,但是可以大大的优化一些编译速度。字节码不是炫技的工具,谨记!


总结


最近基本没有做一些特别适合分享的内容,文章也就没有更新了。各位大佬们体谅啊,抱拳了。


在下封于修,前来讨教。既分生死,也决高下。


image.png


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

匿名内部类为什么泄漏,Lambda为什么不泄漏

在Android开发中,内存泄露发生的场景其实主要就两点,一是数据过大的问题,而是调用与被调用生命周期不一致问题,对于对象生命周期不一致导致的泄漏问题占90%,最常见的也不好分析的当属匿名内部类的内存泄漏,在文章《# 内存泄漏大集结:安卓开发者不可错过的性能优...
继续阅读 »

在Android开发中,内存泄露发生的场景其实主要就两点,一是数据过大的问题,而是调用与被调用生命周期不一致问题,对于对象生命周期不一致导致的泄漏问题占90%,最常见的也不好分析的当属匿名内部类的内存泄漏,在文章《# 内存泄漏大集结:安卓开发者不可错过的性能优化技巧》 中我大概进行了总结,最近在开发时遇到了一个问题,就是LeakCannry 检测到的内存泄漏,LeakCannry检测的原理大概就是GC 可达性算法实现的,我们产品中最多的一个问题就是匿名内部类导致的。


案例不涉及持有外部类引用的状态下


匿名内部类如何导致内存泄漏


在Java体系中,内部类有多种,最常见的就是静态内部类、匿名内部类,一般情况下,都推荐使用静态内部类,那这是为什么呢,先看一个例子:

public class Test {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {

}
}).start();
}
}


匿名内部类的泄漏原因:内部类持有外部类的引用,上述场景中,当外部类销毁时,匿名内部类Runnable 会导致内存泄漏,



验证这个结论


上述代码的class 文件通过Javap -c 查看后是这样的

Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: new #3 // class Test$1
7: dup
8: invokespecial #4 // Method Test$1."<init>":()V
11: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
14: invokevirtual #6 // Method java/lang/Thread.start:()V
17: return
}

我们直接看main 方法中的指令:

0: new #2 // 创建一个新的 Thread 对象 
3: dup // 复制栈顶的对象引用
4: new #3 // 创建一个匿名内部类 Test$1 的实例
7: dup // 复制栈顶的对象引用
8: invokespecial #4 // 调用匿名内部类 Test$1 的构造方法
11: invokespecial #5 // 调用 Thread 类的构造方法,传入匿名内部类对象
14: invokevirtual #6 // 调用 Thread 类的 start 方法,启动线程
17: return // 返回

我们可以看到,在第4步中 使用new 指令创建了一个Test$1的实例,并且在第8步中,通过invokespecial 指令调用匿名内部类的构造方法,这样一来生成的内部类就会持有外部类的引用,从而外部类不能回收,将导致内存泄漏。


Lambda为什么不泄漏


刚开始,我以为Lambda只是语法糖,不会有其他的作用,然而,哈哈 大家估计已经想到了,



匿名内部类使用Lambda 时不会造成内存泄漏。



看代码:

public class Test {
public static void main(String[] args) {
new Thread(() -> {

}).start();
}
}

将上面的代码改为Lambda 格式


class 文件:

Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: invokevirtual #5 // Method java/lang/Thread.start:()V
15: return
}

第一眼看上去就已经知道了答案,在这份字节码中没有生成内部类,


在Lambda格式中,没有生成内部类,而是直接使用invokedynamic 指令动态调用run方法,生成一个Runnable对象。再调用调用Thread类的构造方法,将生成的Runnable对象传入。从而避免了持有外部类的引用,也就避免了内存泄漏的发生。


在开发中,了解字节码知识还是非常有必要的,在关键时刻,我们查看字节码,确实能帮助自己解答一些疑惑,下面是常见的一些字节码指令


常见的字节码指令


Java 字节码指令是一组在 Java 虚拟机中执行的操作码,用于执行特定的计算、加载、存储、控制流等操作。以下是 Java 字节码指令的一些常见指令及其功能:



  1. 加载和存储指令:



  • aload:从局部变量表中加载引用类型到操作数栈。

  • astore:将引用类型存储到局部变量表中。

  • iload:从局部变量表中加载 int 类型到操作数栈。

  • istore:将 int 类型存储到局部变量表中。

  • fload:从局部变量表中加载 float 类型到操作数栈。

  • fstore:将 float 类型存储到局部变量表中。



  1. 算术和逻辑指令:



  • iadd:将栈顶两个 int 类型数值相加。

  • isub:将栈顶两个 int 类型数值相减。

  • imul:将栈顶两个 int 类型数值相乘。

  • idiv:将栈顶两个 int 类型数值相除。

  • iand:将栈顶两个 int 类型数值进行按位与操作。

  • ior:将栈顶两个 int 类型数值进行按位或操作。



  1. 类型转换指令:



  • i2l:将 int 类型转换为 long 类型。

  • l2i:将 long 类型转换为 int 类型。

  • f2d:将 float 类型转换为 double 类型。

  • d2i:将 double 类型转换为 int 类型。



  1. 控制流指令:



  • if_icmpeq:如果两个 int 类型数值相等,则跳转到指定位置。

  • goto:无条件跳转到指定位置。

  • tableswitch:根据索引值跳转到不同位置的指令。



  1. 方法调用和返回指令:



  • invokevirtual:调用实例方法。

  • invokestatic:调用静态方法。

  • invokeinterface:调用接口方法。

  • ireturn:从方法中返回 int 类型值。

  • invokedynamic: 运行时动态解析并绑定方法调用


详细的字节码指令列表和说明可参考 Java 虚拟机规范(Java Virtual Machine Specification)


总结


为了解决问题而储备知识,是最快的学习方式。


在开发中,也不要刻意去设计invokedynamic的代码,但是Java开发的同学,Lambda是必选项哦


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

分享Android开发中常见的代码优化方案

前言 首先要做相关优化,就得先要大致清晰影响性能的相关因素,这样可以做针对性调优会比较有条理。 比较常见的性能调优因素有: 内存:Java 一般通过 JVM 对内存进行分配管理,主要是用 JVM 中堆内存来存储 Java 创建的对象。系统堆内存的读写速度非常...
继续阅读 »

前言


首先要做相关优化,就得先要大致清晰影响性能的相关因素,这样可以做针对性调优会比较有条理。


比较常见的性能调优因素有:



  • 内存:Java 一般通过 JVM 对内存进行分配管理,主要是用 JVM 中堆内存来存储 Java 创建的对象。系统堆内存的读写速度非常快,所以基本不存在读写性能瓶颈。但由于内存成本要比磁盘高,相比磁盘,内存的存储空间又非常有限。所以当内存空间被占满,对象无法回收时,就会导致内存溢出、内存泄露等问题。

  • 异常:抛出异常需要构建异常栈,对异常进行捕获和处理,这个过程非常消耗系统性能。

  • 网络:对于传输数据比较大,或者是并发量比较大的系统,网络就很容易成为性能瓶颈。

  • CPU: 复杂的计算,会长时间,频繁地占用cpu执行资源;例如:代码递归调用,JVM频繁GC以及多线程情况下切换资源都会导致CPU资源繁忙。


对以上这些因素可以在代码中做相关优化处理。


延迟加载(懒加载)优化


了解预加载



  • ViewPager控件有预加载机制,即默认情况下当前页面左右相邻页面会被加载,以便用户滑动切换到相邻界面时,更加顺畅的显示出来

  • 通过ViewPager的setOffscreenPageLimit(int limit)可设置预加载页面数量


介绍延迟加载


等页面UI展示给用户时,再加载该页面数据(从网络、数据库等),而不是依靠ViewPager预加载机制提前加载部分,甚至更多页面数据。可提高所属Activity的初始化速度,另一方面也可以为用户节省流量.而这种延迟加载方案已经被诸多APP所采用。


相关概括



  • 没有打开页面,就不预加载数据,当页面可见时,才加载所需数据。

  • 换句话说延迟加载就是可见时才去请求数据。

  • 实际应用开发中有哪些延迟加载案例:

    • ViewPager+Fragment 搭配使用延迟加载

    • H5网页使用延迟加载




ViewPager与Fragment延迟加载的场景



  • ViewPager中setOffscreenPageLimit(int limit)部分源码
//默认的缓存页面数量(常量)
private static final int DEFAULT_OFFSCREEN_PAGES = 1;

//缓存页面数量(变量)
private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;

public void setOffscreenPageLimit(int limit) {
//当我们手动设置的limit数小于默认值1时,limit值会自动被赋值为默认值1(即DEFAULT_OFFSCREEN_PAGES)
if (limit < DEFAULT_OFFSCREEN_PAGES) {
Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "+ DEFAULT_OFFSCREEN_PAGES);
limit = DEFAULT_OFFSCREEN_PAGES;
}

if (limit != mOffscreenPageLimit) {
//经过前面的拦截判断后,将limit的值设置给mOffscreenPageLimit,用于
mOffscreenPageLimit = limit;
populate();
}
}


  • 思路分析:Fragment中setUserVisibleHint(),此方法会在onCreateView()之前执行,当viewPager中fragment改变可见状态时也会调用,当fragment 从可见到不见,或者从不可见切换到可见,都会调用此方法,使用getUserVisibleHint() 可返回fragment是否可见状态。在onActivityCreated()及setUserVisibleHint()方法中都调一次lazyLoad() 方法。
public abstract class BaseMVPLazyFragment<T extends IBasePresenter> extends BaseMVPFragment<T> {
/**
* Fragment的View加载完毕的标记
*/
protected boolean isViewInitiated;
/**
* Fragment对用户可见的标记
*/
protected boolean isVisibleToUser;
/**
* 是否懒加载
*/
protected boolean isDataInitiated;

...

/**
* 第一步,改变isViewInitiated标记
* 当onViewCreated()方法执行时,表明View已经加载完毕,此时改变isViewInitiated标记为true,并调用lazyLoad()方法
*/
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
isViewInitiated = true;
//只有Fragment onCreateView好了,
//另外这里调用一次lazyLoad()
prepareFetchData();
//lazyLoad();
}

/**
* 第二步
* 此方法会在onCreateView()之前执行
* 当viewPager中fragment改变可见状态时也会调用
* 当fragment 从可见到不见,或者从不可见切换到可见,都会调用此方法
*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
this.isVisibleToUser = isVisibleToUser;
prepareFetchData();
}

/**
* 第四步:定义抽象方法fetchData(),具体加载数据的工作,交给子类去完成
*/
public abstract void fetchData();

/**
* 第三步:在lazyLoad()方法中进行双重标记判断,通过后即可进行数据加载
* 第一种方法
* 调用懒加载,getUserVisibleHint()会返回是否可见状态
* 这是fragment实现懒加载的关键,只有fragment 可见才会调用onLazyLoad() 加载数据
*/
private void lazyLoad() {
if (getUserVisibleHint() && isViewInitiated && !isDataInitiated) {
fetchData();
isDataInitiated = true;
}
}

/**
* 第二种方法
* 调用懒加载
*/
public void prepareFetchData() {
prepareFetchData(false);
}

/**
* 第三步:在lazyLoad()方法中进行双重标记判断,通过后即可进行数据加载
*/
public void prepareFetchData(boolean forceUpdate) {
if (isVisibleToUser && isViewInitiated && (!isDataInitiated || forceUpdate)) {
fetchData();
isDataInitiated = true;
}
}
}

多线程优化: 建议使用线程池


用线程池的好处


可重用线程池中的线程,避免频繁地创建和销毁线程带来的性能消耗;有效控制线程的最大并发数量,防止线程过大导致抢占资源造成阻塞;可对线程进行有效管理



  • RxJava,RxAndroid,底层对线程池的封装管理非常值得参考。

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

IT入门深似海,入门到放弃你学废了嘛

我一直觉得IT行业 程序员行业。甚至觉得程序员人群 是一个特殊存在的群体。 入门到放弃,是真的,IT门槛高嘛。 其实吧,IT编程门槛,是有的,但是对于感兴趣的,想学习IT编程同学来说,也是一件容易事情其实。 我突然想讲一下我学编程的第一课,也是最难的。。。...
继续阅读 »

我一直觉得IT行业 程序员行业。甚至觉得程序员人群 是一个特殊存在的群体。



入门到放弃,是真的,IT门槛高嘛。



其实吧,IT编程门槛,是有的,但是对于感兴趣的,想学习IT编程同学来说,也是一件容易事情其实。


我突然想讲一下我学编程的第一课,也是最难的。。。。。最近又经常遇到这种问题


当然还有很多问题和。是巨坑是真坑。我来讲讲初学者在学习编程时候遇到的


拦路虎


环境配置


入门编程的第一课,惨痛的第一课。


做编程,做开发集成开发环境,IDEA是必不可少的。 我相信开始学习编程的同学,下定决心了会为了表示仪式感,会在买一个新电脑在电脑上花时间和功夫。。


哈哈哈哈哈(作为过来人告诉你没有这个必要,一个普通配置电脑就可以了,不用纠结那些,不然电脑有了,你会转移注意力,学习过程会对你信心有所打击)


我是做JAVA的 当然我现在是全栈 哈哈很高大上的样子,全栈,我时常沉浸在别人 一声声大佬声音中无法自拔。


其实我不是什么大佬,全栈,是工作环境需要。


说到正题 环境配置


环境配置,这一步就会劝退很多人。其实我到现在还深受,各种复杂环境配置的困扰。有莫名的恐惧症。初学第一步。各种开发工具安装,各种环境变量设置, 各种卸载,安装,报错。各种无法启动。启动了崩溃问题


最重要还是要有耐心,


新项目


在开发导入他人新项目时候,尤其在人家项目配置环境和你本地环境配置不一致的时候。 这个时候,其他人项目都可以轻松运行,你死活都运行不了。启动不了项目。


这个时候你就没有办法开发,也没办法了解项目。此时你就会很崩溃或者着急(😁)


明明你和别人一样的环境,一样操作你的不行人家的可以,。这时候没人帮的了你,只能靠你自己。


还有如果是那种比较老的旧的项目。(你见过屎山一样代码吗


你就是你运气不佳,可能你好不容易 运行起来了。装了一套环境自己弄好了,


第二天你再来他就不行了,或者你切换其他项目,其他项目环境坏了


测试上线


这个也是学习编程后面,后面工作让你头疼抓马地方,


本地环境正常,测试环境不正常。测试环境好不容易解决了问题。预发布环境正常。生产环境不正常。 我现在都信佛了


有时候确实怀疑人生,莫名奇妙项目,莫名奇妙代码。莫名奇妙bug


竟然莫名奇妙自己好了。我什么也没改。


然后第一天正常,第二天莫名奇妙无法运行了。


总之一天莫名其妙度过了。。。


尤其在你作为全栈。各种环境切换时候,更加酸爽。。。。


这些问题可能是初级程序员-----高级程序员。必回经历的。


精神疗法


当然这些问题其实并不可怕,可能作为程序员,还会一直伴随着你 职业生涯。。




遇到问题不可怕 ,一定要有耐心,耐心,耐心。今天无法解决




明天换一个心情继续。总会解决的。



以致于我现在有点病态,遇到问题我不解决。我会睡不着觉。。。。。。


睡在床上我也想着,这个问题该怎么解决。脑袋也无法休息。。。


如果这些问题,你都可以,那么你非常适合IT和开发。


大家有什么想法和故事吗,在工作中是否也遇到了和我一样的问题和困惑迷茫


我的故事


可以关注 程序员三时公众号 进行技术交流群讨论


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

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

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

就业之前


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


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


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


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


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


第一段实习 — 贵阳



早9晚5.30,双休,2.5k



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


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


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


第二段 — 重庆



995,3k



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


现在 - 贵阳



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



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


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



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




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



关于感情


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


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


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


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


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



顺便给你们避避坑:


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


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


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


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



附上几组图片:


DSC_3061.JPG


DSC_3106.JPG


最后


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


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

实习半年碎碎念+已裸辞+求职中~

时隔再半年,再次翻开之前记录的,怎么说,有点傻又有点可爱。之前记录在语雀中,作为个人的日记进行记录,如今想要分享出来给某些人一点点帮助吧。那些可能正在遭遇跟我一样经历情况的人吧! 友情提示:别轻易裸辞!想好最坏的结果你是否可以接收,如接下来可能近半年都找不到工...
继续阅读 »

时隔再半年,再次翻开之前记录的,怎么说,有点傻又有点可爱。之前记录在语雀中,作为个人的日记进行记录,如今想要分享出来给某些人一点点帮助吧。那些可能正在遭遇跟我一样经历情况的人吧!


友情提示:别轻易裸辞!想好最坏的结果你是否可以接收,如接下来可能近半年都找不到工作!


但辞了也不要一直去懊悔什么,我们还年轻,可以跌倒很多次,但不要跌得没有意义!我的意思是不要把时间浪费在懊悔自己,质疑自己上,有点蠢说真的

来浩鲸:2022年7月4日 --> 2023年1月15日。  
来福州:2022年6月29日 --> 2023年1月15日。总历时:200天!

“官方文档是最好的学习资料,但最好的不一定适合自己”


我不知道刚入门的小白是否跟我一样,总感觉官方文档像老太婆裹脚——又长又臭。明明按照步骤来了却根本没有正确跳进下一步。然后放弃官方文档去找网上找相关资料,学习博客或B站视频什么的都比这个强。


但是官方文档确实是最权威最全最面向大众的学习资料,为什么看不下去呢?是因为看文字不喜欢吗?但是我们有时候更情愿去看他人总结的学习博客。所以可能不是这个原因。官方文档一般是由该技术的创造者或创造团队编写而成,在尽可能用简短的语言向读者表达出该技术的便捷性,适用性。在这简短的语言中往往会渗透着本人的设计思路思路与丰富的经验。所以我们有时候在看官方文档的时候觉得晦涩难懂很正常。首先不要觉得自己很菜,这么基础都看不懂。很正常的哇,前辈所想所写的我们一下子就get到,那就不应该叫做我们的前辈了。但这个并不是意味着我们放弃看官方文档,选择去看他人的总结,因为这样代表我们可能失去了一个与前辈思想碰撞,交谈沟通的机会了。


怎样理解跟前辈思想碰撞和交谈沟通呢?这就涉及到下一点了。


那我们该怎么做呢?


尽力去看懂官方文档,在你时间和精力都有限的情况下。如果给你学习该技术的时间不够,精力有限,又或者你就是看不懂,那就不必勉强自己,看不懂就看不懂,网上总有学习视频,知识博客可供我们学习。我们不一定要走那条最短最难走的路,换条长路平坦点也是可以到达我们想要的罗马圣地。


请记住,一定不要勉强自己去看那些晦涩难懂的文档,千万不要!!不要觉得自己多看几遍就看懂了,当你投入过长的时间成本后,你会下意识觉得自己其实看懂了,学会了。那怎么才算真正学懂了呢?“能够传道受业解惑也”即代表你是真正学懂了。所以学会及时止损,看不懂没有什么可丢脸的。官方文档是最好的学习材料,但可能不是最适合自己的学习材料!【亲身感受】


“总结分享,贡献开源。最好的学习方式——费曼学习方式”


思想碰撞 :可以理解为我们去努力去get官方文档隐藏的前辈们的idea,如果get到了,那可以运用实践工作中,加工成自己的知识体系。前辈们历经多少项目积攒到的经验,可以帮助我们少走很多弯路,实现弯道大超越!


沟通交流 :可以理解为当我们遇到不理解的,或者有更独特的见解,可以在文档下方的开源社区中积极献言,一般都会得到前辈的回复,若是见解合理被采纳,你则是对开源社区有贡献的人,这样不仅在简历上是一个很大的加分项,甚至可以免费结交到一个超级牛逼的导师啦!


可能会有人认为:大佬写的怎么可能会有bug?还能跟大佬进行沟通交流?但其实不是这样的,大佬写的确实一般没有什么大bug,但是一个轻量便捷优秀的架构,肯定是基于众多基础的已知架构,谁能保证地基不会有更新迭代,楼宇会一直保持稳如泰山呢?架构不是设计完就稳定的不用去管了,它能够长期的发展其背后肯定需要大量的人力精力去维持。但人不可能24小时在线,任何一个程序都有潜在的bug漏洞。


你可能会觉得上述的思想碰撞与沟通交流实在是太过遥远,但不妨换个思路,作为读者的同时我们不能也当个作者呢?可是我们有这实力吗?有的,比如受众群体可以面向同样是刚步入职场的小白呀。而且更为重要的是,如果你能把知识体系系统完整的叙述出来,以老师讲课的方式向你读者介绍你的理解,这何尝不是复盘总结呢?且根据费曼学习来说,这种方法是最能检验你是否掌握了这门技术,且也是吸取知识最好的办法。正如古人所说:传道授业解惑也~


“实战”是最有效的捷径。


任何理论都要赋予给实践的。更何况是工科类的程序代码呢?尽管你做到上述的真正学懂了,但如你没有去实战演练,你还是不懂它实践的应用场景,且很多时候,你看几十遍都不如你去敲一遍来得印象深刻。甚至可能有些人做不到传道受业解惑也没事呀,只要知道什么时候用这个知识点且会用就好了哇。因此对比上述两点,第三点是更为最为重要的!!!一定一定要去实战!!刚入职的时候,有个为期两个月的实习配培训,因为要重新学一个框架,中间还涉及到其他语言技术,妄想通过看文档就能学完这个框架,然后后续看不下去就无意识地去边理解边敲。


要问最后有什么收获?我打字速度变快了,之前需要偶尔看键盘打字,现在完全不需要了。而所谓的知识入脑啊,都是左脑进右脑出了。后面在进行项目实战的时候,知识点完全忘光,基本从零开始,但发现边学边实践的效果比只看文档高效多了。虽然在开始磕磕绊绊又慢又痛苦,但却是在最短的时间内完成最有效的产出。


分享一种怪异学习方法 ——“向下学习”,其指的是打破常规思路,由易至难一步一步学习,而是在具备一定的基础知识后,再选中一种适宜的难度,开始由难至易学习。


“项目是螺旋式上升的,但对接是落地式交付”


在我见证我所参与的一个项目进行第n次需求改版封版后,我才发现一个大型企业项目并不是简单完成一次需求即可,其需要进行多版本的迭代更新,从收集市场需求,确定产品需求,设计产品架构,安排任务进度,设计产品图,后端搭建数据库,还原设计稿,核对测试,修复维护……整个过程都在进行螺旋式向上迭代。其中的每个人都在负责不同的模块,而模块间的耦合需要进行数次的对接。交付的闭合性,沟通的效率性,责任的明确性这些都在要求对接必须是是落地式直接对接!如何理解落地式呢?就是属于自己的职责不推脱,不属于自己的问题不背锅,事事有响必应,件件追踪落实。


“先设计构思再敲码,数据结构很重要”


在被独立扔到一个新的项目中独立开发模块的过程中,我深刻发觉理论永远要去联系实践,面对后端传来的众多接口参数中,如何高效地从数据库中检索出所需要的数据?又如何将已获取的数据进行不同页面的逻辑展示?数据处理是我最薄弱的一项。在刚刚开始面对海量数据时,只知道针对某模块的需求进行设计,并未考虑到其他页面中还有可能存在共用该接口的数据;只知道利用薄弱的已知数据处理方法对接口参数进行获取展示,并未考虑到如此设计会不会影响到产品的运转性能。一股脑投进去开始敲,并未在最初好好思考如此设计的意义在哪?还在大学学数据结构的朋友啊,一定一定要好好去学这门科目,他比高数离散啥的都要重要啊(因为它最难啊!!!)


“拒绝敏感,提高钝感力,“做”一个乐观向上的打工人”


在刚入职之前,其实很多哥哥姐姐就跟我说:要做一个乐观开朗的人,大家都喜欢这样的人。尽管你的性格可能不是这样的,但这是生存之道。实在做不到那么乐观开朗,那就做一个自信不卑不亢的人。


但可能是因为从小到大没有被骂过被批过,再加上这份实习是第一次步入社会,没有一点工作经验,基础也不够扎实,以至于在刚步入社会的两个月,我几乎每天都处于自我怀疑,精神内耗中。那时候家人朋友一直在开解,但他们越是开解自己越是想裸辞。我也知道在2022年,一个什么经验都不会的应届生,会在互联网这个行业中多难生存下来。但那时候总觉得还年轻,什么都没有的年纪就什么都输得起,然后找个借口请假两个星期跑到学校打算准备准备面经冲冲秋招,但因为人性的懒惰,又或者冠冕堂皇的说:两个星期准备秋招无疑说梦话,早点认清现实也不错?


不管你有什么理由,但在学校的两个星期,你确实毫无作为。后面你退缩了,最终还是老老实实回去上班。


很多人问我为什么要离职?我也问过自己好多遍好多遍,我可以说出无数的理由去说服别人跟我占在同一条战线,却说不出一个理由去遮盖实则懦弱逃避的心。


“多读书多看报,文笔不能没有锋利,语言不能没有力量”


细数,我好想自从上大学到现在,都没有好好得读一次书,更多的可能是在看小说,然后还是那种无脑的小说…


2023年,我希望自己可以真正的去读会书,无关是否有实在意义,但请可以给你带来力量。


2022年有两次独自奔溃又自愈时刻,第一次是在准备面试的时候,发现自己真的好菜好垃圾,临时抱佛脚背面经真的很痛苦,那天校园广播响起五月天的倔强,携带黄昏的余晖,带来的不甘心勇气。第二次是想裸辞却没有勇气,深夜的被窝,海底时光机唱《我爱你我的生活》,还夹杂着断断续续的猫猫哭泣声。


结语


这篇好长好长的碎碎念,写了很久很久,中间有好多次想放弃,甚至写到现在,我都不知道我到底在写什么。反正应该没人看莫得事嘿嘿!但最后还是希望可以写下,希望在这半年实习中留点什么。
我希望,当我明年再去看这篇实习回望的时候,会觉得又幼稚好笑,但又那么可爱!


而且在写这篇文章的过程中,我就抱着很矛盾的心理:我在写什么?为什么会那么拖拉?不能精简一点吗?咋都在重复?真的像口水账哇?写那么多你有做到吗?会说不会做吗?道理谁不懂,但是实际马上去出发的还有谁?写这些有用吗?有这时间为什么不去背背面经去实战呢?大家想听你碎碎念吗?

但是最后想了想,还是记录下来吧。记录此时此刻的心情状态,记录这实习半年多的感想感悟。


若能得到您的共勉,真好!


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

谈谈我的Google日历开发之路!

序言 介绍下自己,36岁,我是一名14年的Java服务器开发工程师。在小时候就有一个计算机的梦和愿景(小时候来在东莞,有一天家里人带在东莞长安步步高大道看步步高灯火通明,告诉我说那里面的人上班吹空调,工作只需要敲敲键盘就好了)-现在想来那时候他娘的就有996了...
继续阅读 »

序言


介绍下自己,36岁,我是一名14年的Java服务器开发工程师。在小时候就有一个计算机的梦和愿景(小时候来在东莞,有一天家里人带在东莞长安步步高大道看步步高灯火通明,告诉我说那里面的人上班吹空调,工作只需要敲敲键盘就好了)-现在想来那时候他娘的就有996了。从那时起这颗种子就埋下了。初中的时候还去报了电脑培训班,那里学会了DOS也学会了CS和热血传奇,同样也那以后自己也非常喜欢关注电脑,去新华书店看计算机相关的书。中间就不说了,说多了都是泪。终究如愿以偿自己还是走上了码农这条不归路-少壮不努力,老大搞IT。后面自己也是疯狂的一味的追求技术的宽度,深度。不管时C,C++,汇编,Js等语言(Java吃饭的家伙更不用说),还是Windows编程底层,网络编程,Linux核心翻了个底朝天(可是终究自己还是太年轻了,学的多懂得多与有没有工作之间差了可是一个NASA的哈勃空间望远镜的距离-没有好没学历,你说啥都没用)。转眼间自己已经是一个有着10多年经验的码农了,在这十多年期间,什么技术总监,经理,架构师咱都干过!但自己也是一直热衷于技术,从未放弃过,也知道自己喜欢一件事情想要放弃太困难,如果某一天谈到放弃这个词,心情会非常的艰难,甚至可能会哭的像个刚失恋的女孩一样!也曾有过自暴自弃,大部分的时间在撸游戏,放弃自己(因为在一个学历为王的时代,技术和能力显得那么的微不足道)。但奈何有一颗对技术钟爱的心,所以鞭抽着让自己一直在码代码这条路上一直走,不曾放下,也没有勇气去放下! 其实自己想过无数次,不走技术路还有其他路可走?多少个晚上都没有找到答案。但每每这样想后越发的发现自己更是无路可走,对未来的渺茫和害怕!也许正如《肖申克的救赎》所说-“体制化”真的很可怕,曾经尝试过问身边的人,现在的工作如果不干了会做什么,很多人的回答都是"去别的地方去干“,而事实上几乎不会再去想别的行业了,有的人甚至非常年轻。每个人都是自己的上帝,如果你自己都放弃自己了,还有谁会救你?每个人都在忙,有的忙着生,有的忙着死。忙着追逐名利的你,忙着柴米油盐的你,停下来想一秒:你的大脑,是不是已经被体制化了?你的上帝在哪里?


尝试着简单


很多时候也会三五好友聚在一起,在网吧开黑,在篮球场上畅快淋漓,他们之间有高中就闯荡江湖的,更有初中后就为生活奔波的。但在他们眼里看不到一丝的愁。在网吧肆意的笑声,球场上拼搏的样子,完全看不出一丝生活的不如意。或许是因为每个男人不得让别人看到自己的无助,也许总是要把好的一面留给朋友的每一次见面。生活谁都不易,可能是我要的太多了。但拿到篮球的那一刻是真的快乐了,发自内心的快乐。


的确该这样,我们应该追求自己初衷的东西。虽说爱好既不能饱腹,也不能裹体,更不能遮风挡雨,但每次我们在面对自己喜爱的事情时总是显得会更加自信。或许我该把自己的欲望都降低一点点,也许会变得更好。这里既没有肯定答案,更没有前人的经验来指导我。时间,人物,地点的变化都会让事物的变化都会让未来变得扑朔迷离。正如《复仇者联盟》 奇异博士预测未来和灭霸交手1400万次,只赢了一次那样。或者《萨利机长》 在计算机模拟成功降落的可能性,在避开所有的人为因素,时间因素,环境因素后。依旧模拟了17次后才成功的降落。与其每天活在幻想着如何成功,不如想清楚自己真的需要什么,也不必每天幻想能够去大厂挑战自我(或许这只是我想要更多的一个理由和借口,或许是最后的养老,至少大家都这么想的)但这估计比奇异博士打败灭霸1400万次的概率还要低吧。之后就是简单的过日子,柴米油盐酱醋茶,上班和下班,还有英雄联盟,绝地求生的陪伴。没有了远途的负担,生活和工作逐渐变得愈发的简单和平凡。


从10年到16年,这些年间一直在朋友的公司像青蛙跳荷叶游戏那样蹦来蹦去,估计整个职业生涯社保记录上的公司名称都能打全一张A4纸了。虽然很多时候自己也曾想过好好稳定,也有过稳定。也非常珍惜那一次稳定的机会。但奈何终究还是熬不过现实。或许大多数人和我一样,有些时候明明已经很努力了,工作也做得已经足够好了,可终究还是会留下很多的遗憾,却又无能为力。


突然间的醒悟


2016年,还是多少年来着。Google 公布了Plus关闭的消息。记得那时Google为了对抗Facebook ,Plus应运而生,他带着使命而来,可惜好景不长!很快就Google就不得已要关闭他了。而我却对Plus的钟爱有加,非常喜欢Plus的交互和体验,以及内容的呈现方式。对于Plus的关闭自己也很遗憾和惋惜,然后自己也萌生了一些想法-既然你关闭,我就开发一个出来自己玩! 这也许多年来第一次自己拿起了放下多年的自己了,以前忙着学技术,现在却想好好忙忙该如何做点东西了。


回到主题,话说plus关闭的日子越来越近,我也越来越迫不及待的要干一个plus出来。那年应该是16年吧,说干就干。虽说自己是服务器架构师,一直在Java这个世界里摸爬滚打,但一直相信技术是学来的,自己有这个学习能力去面对这些问题。而事实也是,我总能在工作中表现得游刃有余,任何一份工作都非常的顺利(所以几乎工作中没有过996,不管是研发还是管理)。在决定干plus,也用技术基础充分的证明了自己能做到!(刚好赶上16年从游戏公司离职)相信自己能做到、而后的日子就是每天像个屁股上长了钉子一样闷在房间里面研发Plus。时常也会玩游戏。花了接近1个半月的时间(平常还要玩游戏),做了3个版本,终于干出来了-这里为什么要做3个版本,因为每个版本做完后,发现体验和Google
plus体验相差悬殊,做完后总是不满意,所以为了保持一致,然后又重新开发。直到满意为止,最后却是让自己很满意!这种带来的成就感,结果却是让自己一发不可收拾,后面接连的把photos,mail也索性干了,sticky。曾经也有一段时间放在服务器上运行。终究发现这个东西水土不服,或者说就是我的一次心血来潮吧,运行了半年之后服务器不再续费,也随之不了了之!


2016年年中,回到了工作岗位中,这一次又是朋友的公司。在这里的2年,就再也没涉及过这事,安安静静的工作着。


日历转折点


2018年底呆了2年多的公司卖转手给了另一个老板。而接手的老板在接触几个月后发现志不同道不合不相为谋。因此决然选择了离开。 刚好赶上年底,大概还有1个月左右的时间过年,想着自己也能早点回家,年后也能好好休息下。在接下来没有上班的日历里,回首了这几年开发的Plus,mail,sticky,photos产品,总觉得自己像一个没有长大的小孩,总是对新鲜事物充满着好奇,总是在一次一次的尝试尝试的路上(这里倒不如换个词玩),而每次玩都没有一个真正的结果。功能和交互上都已经很完整了,但从细节和体验他们缺失的太多,同时这些似乎和用户本身的需求离的太远了,这些都是我自己异想天开的,再加上开发的东西觉得欠缺了太多的体验,似乎他们从一开始貌似就决定了他们的结束,我只是享受了这个作的过程-这不是典型的找罪受? 这是一个产品真正的痛。当然我能找一个让自己全身而退的理由-一个人前后端,一个人测试,一个人产品,一个人还要设计,我已经做的够多的了,自己不免会苦笑一下,承认自己的失败又如何。正如《绝望主妇》所说的:失败并不意味着你浪费了时间和生命,而是表明你有理由重新开始。而后再一次决定需要开发一个大家能真正意义上使用的产品,真的,真的,真的告诫自己,要做一件有头有尾的事情了-日程管理。之所以决定做日程管理,因为工作中发现自己每天都会用记事本记录自己的日程,工作任务。然后写完一本后,就会扔掉,总是觉得可惜和有一些遗憾,本想着用国内的一些平台,但VIP让我痛心疾首。于是更加坚定了自己开发一个日程平台来!之前也一直有用Google日历和mac上的苹果日历,也用过国内一些日程管理平台,思来想去还是日历是我的菜,所以索性决定自己干个日历出来!另一个理由是放眼看去这个国内市场和行业,一个日程管理软件,都是VIP,包月,包季,包年,甚至SVIP。免费的不好用,好用的不免费。


日历的开始之路


在仿苹果还是Google之间犯了难。后面索性选择一致好评度高和难度更高的Google。在别人的眼里的看来选择玩这个可能有点上头,终究会啪啪打脸!但自己总觉得这玩意没难度,是别人思想高度不够,还是自己高估自己了。带着疑问找了一些一直坚守前端朋友,结果答案一致的标准-有难度。也许是怕伤我自尊心吧!


心中的热血沸腾告诉自己是时候开始日历开发之旅了。第一次的尝试,先是在房间内把自己关了3天,没挪动过屁股。对整个架构进行了详细的分析和验证,证明了方法的可行性后,花了一个礼拜的时间做了第一个版本出来,只是些核心日程事件处理-当然目标也只是一些核心功能,日程渲染。动手做后发现了整个结构体系存在很多的问题,支撑不了日历的复杂交互以及在复杂交互中会产生很多事件的冲突带来极大体验麻烦。兵来将挡水来土掩,体系架构上的问题难不倒一个架构师(架构思想不管服务器还是视图页面都是一样的)。在重新花2天时间对其重新整理和分析后,新的方案随之出来了、心也更加的澎湃和坚定了! 接下来用15天证明了方案是没问题的,整个日历也是如期而至,功能上能满足日历的要求,但是在交互体验,动画效果,总是缺乏丝滑的效果。不过由于要回家过年了,也顾不上这些问题了。于是匆忙的部署在服务器上回家过年了。


年后(2019年)初又回到了找工作的路上。命还是幸运的,很快找到了一家硬件公司,智能穿戴行业,负责业务服务器这块。当时好几个offer,而这家硬件公司地方很偏,环境也不高大上,打动我的地方是因为上午面了2轮,在等和老板洽谈的时候到中午饭点了。而同仁却早已准备了午餐,水果和午睡床。告诉我说老板下午才有空,让我先等下!就这样被打动了,坚定了决心(上一次13年在华为面试架构师岗位也是这待遇,可惜学历这道硬伤)。本着对新工作的热情和做出一番业绩的渴望,身心很快投入到了工作中。同时日历变成了我的辅助,日程中的使用让我感觉到和Google的体验的差距。不过没关系,我已经不在乎日历了。 我更在乎的怎么做出更好的业绩出来。一番努力后在这里把一个人3个人的团队干到了30+,做出了非常多的业绩,很有成就感,公司氛围相当好!最满意的公司!可到后面大了之后政治斗争也越发严重,3年合同到期,被卸磨杀驴了!只能说无尽的不舍和不甘,但又无可奈何。 最后总结会发现当一个团队有一些没有能力干事的人混进来后就是要把干事的弄死!这就是俗话说解决不了问题就把提出问题的干掉道理一样吧!不过我走后这帮人陆续的在三个月的时间全部被干掉了,我只想说苍天饶过谁!


回到主题,既然肆业了,又想起了日历,其实在这3年的使用中发现了问题,但其实每次也都知道如何去解决。三年间它帮助我解决了非常多的问题,时间管理,日程管理。而后自己决定要开发在体验上能够极致的日历。决定之后,自己把自己锁在出租屋内,刚好又是疫情,一个月没出门对整个日历进行了重新架构设计,体验的交互,丝滑程度与架构有关。经过一个月在体验上的打磨!让它成为了一个真正可用的日历,从功能到到交互再到体验,终究算是了却了自己的一桩心事,算是这么多年唯一一次能交代自己了。 回头总结和看看过去有过的很多想法,很多次都能鼓起勇气尝试去做了,但事实上在有没有结果之间差着一个体验的距离,有体验即代表有结果。没有体验,即纯属到此一游,纵使无数次的开始和结束都将无济于事。


与大家分享下结果


日历导航
视图导航v1.gif


日程操作


日程编辑.gif


日历操作


日历编辑v2.gif


当然还有特别多的功能,修改创建日历,日历主题,订阅日历,日程协作共享,短信桌面通知提醒等。


由于录制屏幕的限制,很多体验无法一一说明,如有感兴趣的朋友希望能够帮助到您。本人还是将一如既往的凭一己之力尽可能提供更多的功能和体验。 后面自己将计划开发windows插件,用于嵌套在桌面上,更加简单灵活体验。


总结


做一件事路途可能很遥远,路途可能会迷路,甚至迷失自己,但坚持自己的初衷,我觉得总是会能达到的!生活中可能会有很多的欺骗和谎言,但始终我们的坚信自己!自己也会未来继续为免费这条路走的更宽和更远,同样提供极致体验。时间很短,也很长,我们可以一件事都不做,也可以做很多事。人生是一连串选择,都是一些常见选择题,最老套的选择就是当一个受害者-随波逐流; 或者选择反抗,也可以选择是忠诚,不论时局好坏。


一个人一台电脑,一个人设计,一个人撸代码,一个人测试,希望一个人能一直走下去。


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

98年菜鸡的碎碎念

最近几天参加了B站某位up主的模拟面试,被打击了。有点不开心,写个文章输出一下自己的情绪。 如果总结自己学编程的过程中犯过最大的错误是什么,那么肯定是迷之自信了,总以为自己学的够多了。虽然 “学的越多,不会的越多” 这个道理我也懂,但是我的内心想法竟然是,大...
继续阅读 »

最近几天参加了B站某位up主的模拟面试,被打击了。有点不开心,写个文章输出一下自己的情绪。



如果总结自己学编程的过程中犯过最大的错误是什么,那么肯定是迷之自信了,总以为自己学的够多了。虽然 “学的越多,不会的越多” 这个道理我也懂,但是我的内心想法竟然是,大家都是这样的,我与大部分同龄人相比起来,已经算好了。但是实际上,自己啥也不是。



印象比较深的主要有两件事


第一件事,20年春天找实习的时候


我高中不努力,老是去网吧玩,和同学相处也不愉快,就没有那种讨论问题的氛围。我大学只上了三本,当时还傻傻的以为计算机不看学历,只看能力。也许当时我刚读大学的时候,找工作是不看学历的,但是今年大家也知道了,普通本科211硕,都有很大概率直接简历挂。每次听群里的同学吐槽校招太难了的时候,都在想他们有学历都这么难,干脆自己躺平算了,就算努力也进不去大厂了。


读大学的时候,同学都不咋学习,也许大部分同学毕业后都不想当程序员吧。相比来说,只要我稍微比同学多学一点,就已经能在班里排名中上了。这就导致我有点迷之自信吧,哈哈哈,以为自己学的东西足够先去小公司锻炼锻炼了,所以大三到大四上学期握在寝室里和同学打LOL,有时候真的是直接打一天,现在想想也挺神奇的,竟然都不会腻。



到了大四的寒假,在家学完了一个培训班的实战项目,就觉得自己挺行了,写好简历后就打算找实习了,但是没想到疫情来了,虽然我现在也不清楚当年因为疫情少招了多少实习生。当时找工作真的找的我emo了,首先简历就很少能过的,就算简历过了,约面试了,我大部分都答不出来,当时对mysql的了解只是会crud而已,面试官问我表锁和行锁,我完全答不出来。说出来不怕大家笑,当时被打击很多次以后,怕自己找不到工作,还不争气的哭了。当时有一家外包公司,说他们找实习生没什么要求的,但是我当时期望薪资是4k,他们只能给2k,我就拒绝了,我大学里经常说,端盘子都有3k,2k的工作我是死也不会去的。但是过了3 4天,还是没找到工作,觉得自己也找不到工作了,所以厚着脸皮去问那个外包公司还愿不愿意要我,他说可以,就这样,2020年春天,我找了份2k的工作,一个月22.5个工作日,我工资一天100都没有。



第二件事,就是文章开头提的这几天参加了B站某位up主的模拟面试,被打击了。


大学毕业后,从外包公司去了一家150人左右的公司,相比之前外包公司来说已经好很多了。这家公司一点都不卷,有好处和坏处,好处就是没什么压力,坏处也是没什么压力,没有学习的氛围。我大学学的比较少,所以在部门里我也是学习劲头比较足的,部门里也会组织技术分享,我也会积极参与的。但是可能我效率低,过了一段时间回顾之前学了什么,总感觉没学啥。不过在这家公司也两年了,懂的肯定是比之前多了。



前段时间那个up主说可以免费模拟面试的时候,我第一时间就报名了,也是想检验一下自己的学习成果吧。不过我发给面试官的时候,他说,我的简历没啥好挖掘的的亮点,就问我一些通用的问题,虽然我也知道自己的简历很普通,但是直接被当面这样说,还是有点尴尬的。面试的过程中,因为我知道面试官会录屏发到b站,就有点紧张,很多都没答好。事后复盘了一下,我在想如果是我同事问我同样的问题,让我教他,那我肯定会回答的更好一些。反正面试的时候表现很差吧。。。其实一次面试失败没啥,可能是我太想表现的好了,表现不好了,就有点不好接受。甚至觉得自己是废物。当天面试完挺emo的,还好有女朋友陪我,这里也说明自己情绪管理是真的有点差的。这也是以后自己要成长的点。



说了这么多,还是要吸取教训,以后好好成长才行。





  1. 视野要宽阔一些,多和优秀程序员聊天,交流,发掘别人的优点找到自己的缺点




  2. 面试学习法:定时参加面试,通过面试发现自己的不足




  3. 以教代学:有机会的话多分享,还可以锻炼自己的表达能力,没有分享的机会就自己录音,然后自己听自己的录音,发现问题




  4. 参加开源or难度更高的项目,小公司里可能项目都比较简单,简历可能会对项目夸大一部分,但是这部分终究是假的,面试官很可能会发现。你想的一些技术难点,面试官会觉得你是背的八股文。




  5. 打算写一个系列的文章《假如面试官让你介绍一下XXX》系列,这次面试也发现了自己的问题,常见的面试题自己没有提前准备好答案,所以面试官问的时候,自己是从0开始组织语言,如果没讲好,面试官会觉得你没有逻辑。




最后,虽然我也在吐槽juejin的很多博客质量低,但是我自己也写了这种无聊的文章,emmmm,但是又想有个输出自己情绪的地方。我立正挨打。



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

Android全局的通知的弹窗

需求分析 如何创建一个全局通知的弹窗?如下图所示。 从手机顶部划入,短暂停留后,再从顶部划出。 首先需要明确的是: 1、这个弹窗的弹出逻辑不一定是当前界面编写的,比如用户上传文件,用户可能继续浏览其他页面的内容,但是监听文件是否上传完成还是在原来的Activ...
继续阅读 »

需求分析


如何创建一个全局通知的弹窗?如下图所示。


image.png


从手机顶部划入,短暂停留后,再从顶部划出。


首先需要明确的是:

1、这个弹窗的弹出逻辑不一定是当前界面编写的,比如用户上传文件,用户可能继续浏览其他页面的内容,但是监听文件是否上传完成还是在原来的Activity或Service,但是Dialog的弹出是需要当前页面的上下文Context的。


2、Dialog弹窗必须支持手势,用户在Dialog上向上滑时,Dialog需要退出,点击时可能需要处理点击事件。


一、Dialog的编写

/**
* 通知的自定义Dialog
*/
class NotificationDialog(context: Context, var title: String, var content: String) :
Dialog(context, R.style.dialog_notifacation_top) {

private var mListener: OnNotificationClick? = null
private var mStartY: Float = 0F
private var mView: View? = null
private var mHeight: Int? = 0

init {
mView = LayoutInflater.from(context).inflate(R.layout.common_layout_notifacation, null)
}


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mView!!)
window?.setGravity(Gravity.TOP)
val layoutParams = window?.attributes
layoutParams?.width = ViewGroup.LayoutParams.MATCH_PARENT
layoutParams?.height = ViewGroup.LayoutParams.WRAP_CONTENT
layoutParams?.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
window?.attributes = layoutParams
window?.setWindowAnimations(R.style.dialog_animation)
//按空白处不能取消
setCanceledOnTouchOutside(false)
//初始化界面数据
initData()
}

private fun initData() {
val tvTitle = findViewById<TextView>(R.id.tv_title)
val tvContent = findViewById<TextView>(R.id.tv_content)
if (title.isNotEmpty()) {
tvTitle.text = title
}

if (content.isNotEmpty()) {
tvContent.text = content
}
}


override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
if (isOutOfBounds(event)) {
mStartY = event.y
}
}

MotionEvent.ACTION_UP -> {
if (mStartY > 0 && isOutOfBounds(event)) {
val moveY = event.y
if (abs(mStartY - moveY) >= 15) { //滑动超过20认定为滑动事件
//Dialog消失
} else { //认定为点击事件
//Dialog的点击事件
mListener?.onClick()
}
dismiss()
}
}
}
return false
}

/**
* 点击是否在范围外
*/
private fun isOutOfBounds(event: MotionEvent): Boolean {
val yValue = event.y
if (yValue > 0 && yValue <= (mHeight ?: (0 + 40))) {
return true
}
return false
}


private fun setDialogSize() {
mView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
mHeight = v?.height
}
}

/**
* 显示Dialog但是不会自动退出
*/
fun showDialog() {
if (!isShowing) {
show()
setDialogSize()
}
}

/**
* 显示Dialog,3000毫秒后自动退出
*/
fun showDialogAutoDismiss() {
if (!isShowing) {
show()
setDialogSize()
//延迟3000毫秒后自动消失
Handler(Looper.getMainLooper()).postDelayed({
if (isShowing) {
dismiss()
}
}, 3000L)
}
}

//处理通知的点击事件
fun setOnNotificationClickListener(listener: OnNotificationClick) {
mListener = listener
}

interface OnNotificationClick {
fun onClick()
}
}

Dialog的主题

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">

<style name="dialog_notifacation_top">
<item name="android:windowIsTranslucent">true</item>
<!--设置背景透明-->
<item name="android:windowBackground">@android:color/transparent</item>
<!--设置dialog浮与activity上面-->
<item name="android:windowIsFloating">true</item>
<!--去掉背景模糊效果-->
<item name="android:backgroundDimEnabled">false</item>
<item name="android:windowNoTitle">true</item>
<!--去掉边框-->
<item name="android:windowFrame">@null</item>
</style>


<style name="dialog_animation" parent="@android:style/Animation.Dialog">
<!-- 进入时的动画 -->
<item name="android:windowEnterAnimation">@anim/dialog_enter</item>
<!-- 退出时的动画 -->
<item name="android:windowExitAnimation">@anim/dialog_exit</item>
</style>

</resources>

Dialog的动画

<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="600"
android:fromYDelta="-100%p"
android:toYDelta="0%p" />
</set>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="300"
android:fromYDelta="0%p"
android:toYDelta="-100%p" />
</set>

Dialog的布局,通CardView包裹一下就有立体阴影的效果

<androidx.cardview.widget.CardView
android:id="@+id/cd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/size_15dp"
app:cardCornerRadius="@dimen/size_15dp"
app:cardElevation="@dimen/size_15dp"
app:layout_constraintTop_toTopOf="parent">

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/et_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/size_15dp"
app:layout_constraintTop_toTopOf="parent">

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#000000"
android:textSize="@dimen/font_14sp" android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/size_15dp"
android:textColor="#333"
android:textSize="@dimen/font_12sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_title" />


</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.cardview.widget.CardView>

二、获取当前显示的Activity的弱引用

/**
* 前台Activity管理类
*/
class ForegroundActivityManager {

private var currentActivityWeakRef: WeakReference<Activity>? = null
private var mIsActive:Boolean = false

companion object {
val TAG = "ForegroundActivityManager"
private val instance = ForegroundActivityManager()

@JvmStatic
fun getInstance(): ForegroundActivityManager {
return instance
}
}


fun getCurrentActivity(): Activity? {
var currentActivity: Activity? = null
if (currentActivityWeakRef != null) {
currentActivity = currentActivityWeakRef?.get()
}
return currentActivity
}


fun setCurrentActivity(activity: Activity) {
currentActivityWeakRef = WeakReference(activity)
}

fun setActive(isActive:Boolean){
mIsActive = isActive
}

fun getActive():Boolean=mIsActive


}

监听所有Activity的生命周期,并判断当前页面是否可以显示Dialog,参考LiveData的源码,判断当前是否是Active状态,如果是Activie状态则可以显示Dialog,如果非Active状态则等待下次Active时显示Dialog

class AppLifecycleCallback:Application.ActivityLifecycleCallbacks {

companion object{
val TAG = "AppLifecycleCallback"
}

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
//获取Activity弱引用
ForegroundActivityManager.getInstance().setCurrentActivity(activity)
}

override fun onActivityStarted(activity: Activity) {
}

override fun onActivityResumed(activity: Activity) {
//获取Activity弱引用
ForegroundActivityManager.getInstance().setCurrentActivity(activity)
//设置当前Active状态为true
ForegroundActivityManager.getInstance().setActive(true)
}

override fun onActivityPaused(activity: Activity) {
//设置当前Active状态为false
ForegroundActivityManager.getInstance().setActive(false)
}

override fun onActivityStopped(activity: Activity) {
}

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}

override fun onActivityDestroyed(activity: Activity) {
}
}

在Application中注册

//注册Activity生命周期
registerActivityLifecycleCallbacks(AppLifecycleCallback())

三、封装和使用

/**
* 通知的管理类
* example:
* //发系统通知
* NotificationControlManager.getInstance()?.notify("文件上传完成", "文件上传完成,请点击查看详情")
* //发应用内通知
* NotificationControlManager.getInstance()?.showNotificationDialog("文件上传完成","文件上传完成,请点击查看详情",
* object : NotificationControlManager.OnNotificationCallback {
* override fun onCallback() {
* Toast.makeText(this@MainActivity, "被点击了", Toast.LENGTH_SHORT).show()
* }
* })
*/

class NotificationControlManager {

private var autoIncreament = AtomicInteger(1001)
private var contentMap = mutableListOf<NotificationInfo>()
private var dialogList = mutableListOf<NotificationDialog>()

companion object {
const val channelId = "app"
const val description = "my application"

@Volatile
private var sInstance: NotificationControlManager? = null

@JvmStatic
fun getInstance(): NotificationControlManager {
if (sInstance == null) {
synchronized(NotificationControlManager::class.java) {
if (sInstance == null) {
sInstance = NotificationControlManager()
}
}
}
return sInstance!!
}
}


/**
* 是否打开通知
*/
fun isOpenNotification(): Boolean {
val notificationManager: NotificationManagerCompat =
NotificationManagerCompat.from(
ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
)
return notificationManager.areNotificationsEnabled()
}


/**
* 跳转到系统设置页面去打开通知,注意在这之前应该有个Dialog提醒用户
*/
fun openNotificationInSys() {
val context = ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
val intent: Intent = Intent()
try {
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS

//8.0及以后版本使用这两个extra. >=API 26
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
intent.putExtra(Settings.EXTRA_CHANNEL_ID, context.applicationInfo.uid)

//5.0-7.1 使用这两个extra. <= API 25, >=API 21
intent.putExtra("app_package", context.packageName)
intent.putExtra("app_uid", context.applicationInfo.uid)

context.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()

//其他低版本或者异常情况,走该节点。进入APP设置界面
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
intent.putExtra("package", context.packageName)

//val uri = Uri.fromParts("package", packageName, null)
//intent.data = uri
context.startActivity(intent)
}
}

/**
* 发通知
* @param title 标题
* @param content 内容
* @param cls 通知点击后跳转的Activity,默认为null跳转到MainActivity
*/
fun notify(title: String, content: String, cls: Class<*>) {
val context = ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
val notificationManager =
context.getSystemService(AppCompatActivity.NOTIFICATION_SERVICE) as NotificationManager
val builder: Notification.Builder
val intent = Intent(context, cls)
val pendingIntent: PendingIntent? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel =
NotificationChannel(channelId, description, NotificationManager.IMPORTANCE_HIGH)
notificationChannel.enableLights(true);
notificationChannel.lightColor = Color.RED;
notificationChannel.enableVibration(true);
notificationChannel.vibrationPattern =
longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
notificationManager.createNotificationChannel(notificationChannel)
builder = Notification.Builder(context, channelId)
.setSmallIcon(R.drawable.jpush_notification_icon)
.setContentIntent(pendingIntent)
.setContentTitle(title)
.setContentText(content)
} else {
builder = Notification.Builder(context)
.setSmallIcon(R.drawable.jpush_notification_icon)
.setLargeIcon(
BitmapFactory.decodeResource(
context.resources,
R.drawable.jpush_notification_icon
)
)
.setContentIntent(pendingIntent)
.setContentTitle(title)
.setContentText(content)

}
notificationManager.notify(autoIncreament.incrementAndGet(), builder.build())
}


/**
* 显示应用内通知的Dialog,需要自己处理点击事件。listener默认为null,不处理也可以。dialog会在3000毫秒后自动消失
* @param title 标题
* @param content 内容
* @param filterActivityByClassNameList 过滤哪些类不显示Dialog
* @param listener 点击的回调
*/
fun showNotificationDialog(
title: String,
content: String,
filterActivityByClassNameList: MutableList<String>? = null,
listener: OnNotificationCallback? = null
) {
val currentActivity = ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
//判断是否需要过滤页面不显示Dialog
filterActivityByClassNameList?.forEach {
val className = currentActivity.javaClass.simpleName
if (className == it) {
return
}
}

val isActive = ForegroundActivityManager.getInstance()?.getActive() ?: false
if (isActive) { //Active状态
val dialog = NotificationDialog(currentActivity, title, content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
currentActivity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else { //如果当前Activity非Active状态则把要显示的内容存储到集合中
//存到集合中
contentMap.add(NotificationInfo(title, content))
}
}


/**
* 显示应用内通知的Dialog,需要自己处理点击事件。listener默认为null,不处理也可以。dialog会在3000毫秒后自动消失
* @param title 标题
* @param content 内容
* @param activity 需要传入Activity(主要碰到多进程的问题)
* @param listener 点击的回调
*/
fun showNotificationDialog(
title: String,
content: String,
activity: AppCompatActivity,
listener: OnNotificationCallback? = null
) {
val dialog = NotificationDialog(activity, title, content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
activity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
}


/**
* 显示应用内通知的Dialog,需要自己处理点击事件。listener默认为null,不处理也可以。dialog会在3000毫秒后自动消失
* @param title 标题
* @param content 内容
* @param activity 需要传入Activity(主要碰到多进程的问题)
* @param listener 点击的回调
*/
fun showNotificationDialogWithNotLifecycle(
title: String,
content: String,
activity: AppCompatActivity,
listener: OnNotificationCallback? = null
) {
val dialog = NotificationDialog(activity, title, content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
activity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
}


/**
* set dialog click
*/
private fun setDialogClick(
dialog: NotificationDialog?,
listener: OnNotificationCallback?
) {
if (listener != null) {
dialog?.setOnNotificationClickListener(object :
NotificationDialog.OnNotificationClick {
override fun onClick() = listener.onCallback()
})
}
}

/**
* 显示没有显示过的Dialog
*/
fun showDialogNeverVisible() {
if (contentMap.isNotEmpty()) {
val iterator = contentMap.iterator()
while (iterator.hasNext()) {
val info = iterator.next()
val currentActivity =
ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
val dialog =
NotificationDialog(currentActivity, info.title, info.content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
currentActivity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, null) //这里需要根据场景完善点击事件
iterator.remove()
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, null) //这里需要根据场景完善点击事件
iterator.remove()
}
}
}
}

/**
* dismiss Dialog
*/
fun dismissDialogWithLifecycle() {
if (dialogList.size > 0) {
val iterator = dialogList.iterator()
while (iterator.hasNext()) {
val dialog = iterator.next()
if (dialog != null && dialog.isShowing) {
dialog.dismiss()
}
iterator.remove()
}
}
}


interface OnNotificationCallback {
fun onCallback()
}

}
//根据需求封装
data class NotificationInfo(var title:String,var content:String)

需要注意的点是:

1、Activity处于转场时是不能显示Dialog的,此时会回调onPause方法,isActive处于false状态(这一点参考LiveData的源码),将需要显示的数据存储于集合中,待BaseActivity回调onResume时显示没有显示的Dialog。

override fun onResume() {
super.onResume()
NotificationControlManager.getInstance()?.showDialogNeverVisible()
}

2、因为dialog是延迟关闭的,可能用户立刻退出Activity,导致延迟时间到时dialog退出时报错,解决办法可以在BaseActivity的onPause方法中尝试关闭Dialog:

override fun onPause() {
super.onPause()
NotificationControlManager.getInstance()?.dismissDialogWithLifecycle()
}

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

常见 Node.js 版本管理器比较:nvm、Volta 和 asdf

web
常见 Node.js 版本管理器比较:nvm、Volta 和 asdf 随着 Node.js 的发展,能够管理不同版本的运行时以确保兼容性和稳定性非常重要。这就是 Node.js 版本管理器的用武之地!在本文中,我们将比较和对比三种流行的 Node.js 版本...
继续阅读 »

常见 Node.js 版本管理器比较:nvm、Volta 和 asdf


随着 Node.js 的发展,能够管理不同版本的运行时以确保兼容性和稳定性非常重要。这就是 Node.js 版本管理器的用武之地!在本文中,我们将比较和对比三种流行的 Node.js 版本管理器:nvm、voltaasdf 来帮助你为你的开发环境选择合适的版本管理器。


nvm


nvm(Node Version Manager)是最古老和最受欢迎的 Node.js 版本管理器之一,至今仍在积极维护。nvm 允许开发人员在一台机器上安装和管理多个版本的 Node.js。它还提供了一个方便的命令行界面,用于在可用版本之间切换。


nvm 的工作原理是将 Node.js 的每个版本安装到下的独立目录中。使用 nvm use 在版本之间切换时,它会更新环境变量以指向相应的目录。所以可以并行安装多个版本的 Node.js,并且每个版本都有自己的一组全局安装的软件包 ~/.nvm/versions/node/$PATH


nvm 的一个缺点是它只支持 Node.js。如果需要管理其他编程语言或工具,则需要使用单独的版本管理器。另外,nvm 需要手动安装和配置,这对于初学者来说可能有点难受。


要安装 nvm,可以使用以下命令:


curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

# or

wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

这将从官方 NVM GitHub 仓库下载并运行 NVM 安装脚本,该仓库将在你的系统上安装 NVM。安装完成后,你可以通过运行以下命令来验证是否已安装 NVM:


nvm --version

1.1 如何使用特定版本的 Node.js


要将特定版本的 Node.js 与 nvm 配合使用,你需要执行以下步骤:



  1. 列出可用的 Node.js 版本:要查看可以使用 nvm 安装的所有可用 Node.js 版本的列表,请运行以下命令:


nvm ls-remote


  1. 安装所需版本:要安装特定版本的 Node.js,例如版本 18,请使用以下命令:


nvm install 18


  1. 使用已安装的版本:安装所需的 Node.js 版本后,你可以通过运行以下命令来使用它:


nvm use 16

设置默认版本:如果要使用特定版本的 Node.js 默认情况下,可以使用以下命令将其设置为默认版本:


nvm alias default 18

Volta


volta 是一个较新的 Node.js 版本管理器,旨在简化 Node.js 和其他工具的安装和管理,在 2019 年出世,仍在积极开发中。Volta 采用了与 nvm 不同的方法:它不是管理 Node.js 的多个版本,而是管理项目及其依赖项。当你创建新项目时,volta 会自动检测所需的 Node.js 版本并为你安装它。


volta 还支持其他工具,如 Yarn 和 Rust,开箱即用(不仅仅是 Node.js!对于使用多种编程语言并需要单个工具来管理它们的开发人员来说,这使其成为一个不错的选择。与 nvm 一样,volta 提供了一个命令行界面,用于在 Node.js 版本之间切换,但它通过使用拦截对 node 可执行文件的调用的全局填充程序来实现。


要安装 volta,你可以使用以下命令:


curl https://get.volta.sh | bash

此命令将下载并执行安装 volta 的脚本。


如何使用特定版本的 Node.js



  1. 安装所需版本:安装 volta 后,你可以使用它创建一个新项目并使用 volta install 命令设置所需的 Node.js 版本,以下命令创建一个新项目并将所需的 Node.js 版本设置为 16.0.0:


volta install node@16.0.0


  1. 在该项目的上下文中运行命令:此命令使用所需版本的 Node.js 在项目的上下文中运行 app.js 文件。


volta run node app.js


  1. 切换版本:你也可以使用 Volta 在不同版本的 Node.js 之间切换。例如,要切换到版本 10.0.0,可以使用以下命令:


volta pin node@10.0.0


  1. 设置默认版本:最后,以下命令将你的环境切换为 Node.js 版本 16.0.0 并设置为 Node.js 的默认版本:


nvm alias default 16.0.0

volta 的一个潜在缺点是它仍然是一个相对较新的工具,因此它可能不像 nvm 那样经过实战测试,而且它的社区支持有限,插件和集成也较少。


ASDF


ASDF 是一个版本管理器,旨在成为“通用语言版本管理器”。在 2015 年出世,支持广泛的编程语言和工具,包括 Node.js。ASDF 设计为可扩展,因此可以轻松地添加新语言和工具的支持。


asdf 支持几种流行的编程语言,包括 Node.js,Ruby,Python,Elixir,Java,Rust,PHP,Perl,Haskell,R,Lua 和 Earlang。这意味着你可以在一个地方管理不同的语言版本!如果要在不同语言的项目之间切换,使 asdf 成为一个不错的选择。


与 volta 一样,ASDF 管理项目及其依赖项,而不是同一工具的多个版本。创建新项目时,asdf 会自动检测所需的 Node.js 版本并为你安装。asdf 提供了一个命令行界面,用于在 Node.js 版本以及其他工具之间切换。


asdf 的一个潜在缺点是它的设置可能比 nvm 或 volta 复杂一些。你需要安装多个插件来添加对不同语言和工具的支持,并且可能需要修改 shell 配置以正确使用 asdf。


下面是如何使用 ASDF 安装和使用特定 Node.js 版本的示例:



  1. 安装 ASDF:可以使用以下命令安装 ASDF:


brew install asdf


  1. 将 Node.js 插件添加到 ASDF:你必须安装插件才能将 Node.js 添加到你的项目中


asdf plugin add nodejs


  1. 安装 Node.js 版本 18:使用以下命令使用特定版本的 Node.js:


asdf install nodejs 18


  1. 使用特定版本:


asdf global nodejs 18

nvm,volta 和 asdf 之间的差异



  1. 目的: NVM,Volta 和 ASDF 有不同的用途。NVM 专注于管理多个版本的 Node.js。而 Volta 将 Node.js 版本管理和包管理结合在一个工具中。ASDF 是一个版本管理器,支持多种编程语言,包括 Node.js。

  2. 安装: NVM、Volta 和 ASDF 的安装过程不同。NVM 可以使用 curl 命令安装,而 Volta 要求你手动下载并安装它。ASDF 可以使用 Homebrew 等包管理器安装,也可以直接从 GitHub 下载。

  3. 配置: NVM、Volta 和 ASDF 的配置过程是不同的。NVM 要求你手动更新 shell 配置文件。Volta 不需要任何手动配置。ASDF 要求你手动设置所需的插件。

  4. 自动版本检测: Volta 是唯一通过读取项目的 package.json 文件自动检测项目所需的 Node.js 版本的版本管理器。

  5. 包管理: Volta 是唯一将 Node.js 版本管理和包管理结合在一个工具中的版本管理器。NVM 和 ASDF 仅管理 Node.js 版本。


相似之处



  1. 多节点.js版本: NVM、Volta 和 ASDF 都允许你在同一台机器上管理多个版本的 Node.js。

  2. 全局和本地 node.js 版本: 所有三个版本管理器都允许你全局或本地安装 Node.js 版本。

  3. 简单命令: NVM、Volta 和 ASDF 有简单的命令来管理 Node.js 版本。

  4. 兼容性: 所有三个版本管理器都与 macOS,Linux 和 Windows 操作系统兼容。

  5. 开源: NVM、Volta 和 ASDF 都是开源项目,这意味着它们可以免费使用,并且可以由社区贡献。

  6. 版本锁定: 所有三个版本管理器都允许你锁定特定项目的 Node.js 版本,确保所有团队成员使用相同的版本。


小结


总之,nvmVoltaasdf 都是很棒的 Node.js 版本管理器,可以帮助你更改,管理和更新 Node.js 的多个版本,还可以与新版本保持同步,包括 LTS 版本。Nvm 是最古老和最受欢迎的版本管理器之一,Volta 有不同的方法,它不是管理多个版本的 Node.js,而是管理项目及其依赖项,最后 asdf 管理不同的语言版本,如果你在使用不同语言的项目之间切换,这使得 asdf

作者:夏安君
来源:juejin.cn/post/7247543825535270968
是一个不错的选择。

收起阅读 »

35岁愿你我皆向阳而生

35岁是一个让程序员感到焦虑的年龄。这个年纪往往意味着成熟和稳定,但在技术领域,35岁可能意味着过时和被淘汰。在这篇文章中,我将探讨35岁程序员的思考,以及如何应对这种感受,这里我不想贩卖焦虑,只是对现实的一些思考。 当我在20多岁研究生刚刚毕业的时候,恰逢互...
继续阅读 »

35岁是一个让程序员感到焦虑的年龄。这个年纪往往意味着成熟和稳定,但在技术领域,35岁可能意味着过时和被淘汰。在这篇文章中,我将探讨35岁程序员的思考,以及如何应对这种感受,这里我不想贩卖焦虑,只是对现实的一些思考。


当我在20多岁研究生刚刚毕业的时候,恰逢互联网蓬勃发展,到处都是机会,那时候年轻气盛,我充满了能量和热情。我渴望学习新的技术和承担具有挑战性的项目。我花费了无数的时间编码、测试和调试,常常为了追求事业目标而牺牲了个人生活。


慢慢的当我接近30岁的时候,我开始意识到我的优先事项正在转变。我在职业生涯中取得了很多成就,但我也想专注于我的个人生活和关系。我想旅行、和亲人朋友共度时光,并追求曾经被工作忽视的爱好。


现在,35岁的我发现自己处于一个独特的位置。我在自己的领域中获得了丰富的经验,受到同事和同行的尊重。然而,我也感到一种不安和渴望尝试新事物的愿望。这时候我很少在代码上花费时间,而是更多的时间花到了项目管理上,一切似乎很好,但是疫情这几年,行业了很大的影响,公司的运营也变得步履维艰,在安静的会常常想到未来的的规划。


一、焦虑情绪的来源


35岁程序员的焦虑情绪源于其所处的行业环境。技术不断发展,新的编程语言、框架、工具层出不穷,要跟上这些变化需要付出大量的时间和精力。此外,随着年龄的增长,身体和心理健康也会面临各种问题。这些因素加在一起,让35岁程序员感到无从下手,不知道该如何面对未来。


二、面对焦虑情绪的方法


1学习新技能


学习新技能是应对技术革新的必经之路。与其等待公司提供培训或者等待机会,35岁程序员应该主动寻找新技术,并投入时间和精力去学习。通过参加课程、阅读文献,甚至是找到一位 mentor,35岁程序员可以更快地适应新技术,保持竞争力。


2关注行业动态


35岁程序员要时刻关注技术行业的最新动态。阅读技术博客、参加社区活动,以及了解公司的发展方向和战略规划,这些都是成为行业领跑者所必须的。通过增强对行业趋势的了解,35岁程序员可以更好地做出决策,同时也可以通过分享经验获得他人的认可和支持。


3 与年轻人合作


与年轻的程序员合作可以带来许多好处。他们可能拥有更新的知识和技能,并且乐于探索新事物。35岁的程序员应该通过与年轻人合作,学习他们的思考方式和方法论。这样不仅可以加速学习新技能,还可以提高自己的领导能力。


每周我都会组织公司内部的技术交流活动,并积极号召大家发表文章,通过这些技术分享,我发现每个人擅长的东西不同,交流下来大家的收获都很大。


4重新审视个人价值观


在35岁之后,程序员可能会重新审视自己的职业生涯和个人发展方向。当面临焦虑情绪时,建议去回顾一下自己的愿景和目标。这有助于确定下一步的工作方向和计划。此外,35岁程序员也应该考虑个人的非技术技能,例如领导力、沟通能力和团队合作精神,这些技能对长期职业成功至关重要。


5 敞开心扉学会沟通


 程序员给大家的一个刻板印象就是不爱沟通,刻板木讷,大家都是干活的好手,但是一道人际关系处理上就显得有些不够灵活,保持竞争力的一个很关键的点也在于多沟通,多社交,让自己显得更有价值,有一句老话说的好:多一个朋友,多一条路。沟通需要技巧,交朋友更是,这也是我们需要学习的。


三、总结


35岁是程序员生涯中的一个重要节点,同时也是一个充满挑战和机会的时期。如何应对焦虑情绪,保持竞争力并保持个人发展的连续性,这需要程序员深入思考自己的职业规划和发展方向。


通过学习新技能、关注行业动态、与年轻人合作以及审视个人价值观,35岁程序员可以在未来的职业生涯中不断成长和发展。


归根到底,无论如何生活的好与坏都在于我们对待生活的态度,幸福是一种感受,相由心生,无论你处于何种生活状态,

作者:mikezhu
来源:juejin.cn/post/7246778558248632378
都希望大家向阳而生。

收起阅读 »

句柄是什么?一文带你了解!

今天又学习了一个装X概念——句柄,看字面意思,感觉跟某种器具有关,但实际上,这个词可不是用来打造家居用品的。 相信不少人跟我一样,第一眼看到这个词,脑海里只有一个大大的问号。不过,没关系,我们可以一起学习,毕竟,装X路上永远没有止境。 1、官方一点儿的定义 在...
继续阅读 »

今天又学习了一个装X概念——句柄,看字面意思,感觉跟某种器具有关,但实际上,这个词可不是用来打造家居用品的。


相信不少人跟我一样,第一眼看到这个词,脑海里只有一个大大的问号。不过,没关系,我们可以一起学习,毕竟,装X路上永远没有止境。


1、官方一点儿的定义


在计算机科学中,句柄(Handle)是一种引用或标识对象的方式,它可以用来访问或操作底层系统资源。


不同的操作系统可能会有不同的实现和用途,下面我将以不同的操作系统为例来解释句柄的意义。


1. Windows操作系统


在 Windows 中,句柄是一种整数值,用于标识和访问系统对象或资源,如窗口、文件、设备等。


句柄充当了对象的唯一标识符,通过句柄可以对对象进行操作和管理。


示例代码(C++):


HWND hWnd = CreateWindow(L"Button", L"Click Me", WS_VISIBLE | WS_CHILD, 10, 10, 100, 30, hWndParent, NULL, hInstance, NULL);
if (hWnd != NULL) {
// 使用句柄操作窗口对象
ShowWindow(hWnd, SW_SHOW);
UpdateWindow(hWnd);
// ...
}

在上述代码中,通过 CreateWindow 函数创建一个按钮窗口,并将返回的句柄存储在 hWnd 变量中。然后,可以使用 hWnd 句柄来显示窗口、更新窗口等操作。


2. Linux操作系统


在 Linux 中,句柄通常称为文件描述符(File Descriptor),它是一个非负整数,用于标识打开的文件、设备、管道等。


Linux将所有的I/O操作都抽象为文件,并使用文件描述符来引用和操作这些文件。


示例代码(C):


int fd = open("file.txt", O_RDONLY);
if (fd != -1) {
// 使用文件描述符读取文件内容
char buffer[1024];
ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
// ...
close(fd);
}

上述代码中,通过 open 函数打开文件 file.txt,并将返回的文件描述符存储在 fd 变量中。然后,可以使用 fd 文件描述符来进行文件读取等操作。


3. macOS操作系统


在 macOS 中,句柄也称为文件描述符(File Descriptor),类似于 Linux 操作系统的文件描述符。它是一个整数,用于标识和访问打开的文件、设备等。


示例代码(Objective-C):


int fileDescriptor = open("/path/to/file.txt", O_RDONLY);
if (fileDescriptor != -1) {
// 使用文件描述符读取文件内容
char buffer[1024];
ssize_t bytesRead = read(fileDescriptor, buffer, sizeof(buffer));
// ...
close(fileDescriptor);
}

在上述代码中,通过 open 函数打开文件 /path/to/file.txt,并将返回的文件描述符存储在 fileDescriptor 变量中。然后,可以使用 fileDescriptor 文件描述符来进行文件读取等操作。


总结起来,句柄(Handle)是一种在操作系统中用于标识、访问和操作系统资源的方式。


不同的操作系统有不同的实现和命名,如 Windows 中的句柄、Linux 和 macOS 中的文件描述符。句柄提供了一种抽象层,使得程序可以使用标识符来引用和操作底层资源,从而实现对系统资源的管理和控制。


2、通俗易懂的理解


可以把句柄理解为一个中间媒介,通过这个中间媒介可控制、操作某样东西。


举个例子。door handle 是指门把手,通过门把手可以去控制门,但 door handle 并非 door 本身,只是一个中间媒介。


又比如 knife handle 是刀柄,通过刀柄可以使用刀。


跟 door handle 类似,我们可以用 file handle 去操作 file, 但 file handle 并非 file 本身。这个 file handle 就被翻译成文件句柄,同理还有各种资源句柄。


3、为什么要发明句柄?


句柄的引入主要是为了解决以下几个问题:




  1. 资源标识:操作系统中存在各种类型的资源,如窗口、文件、设备等。为了标识和引用这些资源,需要一种统一的方式。句柄提供了一个唯一的标识符,可以用于识别特定类型的资源。




  2. 封装和抽象:句柄将底层资源的具体实现进行了封装和抽象,提供了一种更高层次的接口供应用程序使用。这样,应用程序不需要了解资源的内部细节和底层实现,只需要通过句柄进行操作。




  3. 安全性和隔离:句柄可以充当一种权限验证的机制,通过句柄来访问资源可以进行权限检查,从而保证了资源的安全性。此外,句柄还可以实现资源的隔离,不同句柄之间的资源操作互不影响。




  4. 跨平台和兼容性:不同的操作系统和平台有各自的资源管理方式和实现,句柄提供了一种统一的方式来操作不同平台上的资源。这样,应用程序可以在不同的操作系统上运行,并使用相同的句柄接口来访问资源。




总之,句柄提供了一种统一、封装、安全和跨平台的解决方案,使得应用程序可以更方便地操作和管理底层系统资源。


好了,看完以上内容你也许会感叹:句柄?这不就是个把手吗!但是,别小瞧这个

作者:陈有余Tech
来源:juejin.cn/post/7246279539986743354
把手,它可是万能的!

收起阅读 »

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

楼主bg 211本,2023 的上半年一直在蔚来实习,在学校和公司两头跑,有些麻木,但也有些思考吧。 期间放一些在蔚来实习的图片吧~ 每次都是在考试周写这些东西(doge 这篇文章姑且算的上是我 2023 年的年中总结。 我最近看到了这样一句话:“网站的流...
继续阅读 »

楼主bg 211本,2023 的上半年一直在蔚来实习,在学校和公司两头跑,有些麻木,但也有些思考吧。


期间放一些在蔚来实习的图片吧~


每次都是在考试周写这些东西(doge



image.png


这篇文章姑且算的上是我 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


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

强制缓存这么暴力,为什么不使用协商缓存😡😡😡

web
前段时间在看面经的时候,发现很多份面经中都被问到了 强缓存 和 协商缓存。因此我觉得有必要写一篇文章来好好聊聊这两者。 强缓存和协商缓存 浏览器缓存是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档...
继续阅读 »

前段时间在看面经的时候,发现很多份面经中都被问到了 强缓存协商缓存。因此我觉得有必要写一篇文章来好好聊聊这两者。


强缓存和协商缓存


浏览器缓存是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档,其中浏览器缓存就分为 强缓存协商缓存:



  1. 强缓存: 当浏览器在请求资源时,根据响应头中的缓存策略信息,判断是否使用本地缓存副本而无需发送请求到服务器。如果资源被标记为强缓存,浏览器会直接从本地缓存中加载资源,而不发送请求到服务器,从而提高页面加载速度并减轻服务器负载;

  2. 协商缓存: 协商缓存是一种缓存策略,用于在资源未过期但可能已经发生变化时,通过与服务器进行协商确定是否使用本地缓存。协商缓存通过在请求中发送特定的条件信息,让服务器判断资源是否有更新,并返回相应的状态码和响应头信息,以指示浏览器是否可以使用本地缓存;


所以根据以上所聊到的特点,浏览器缓存有以下几个方面的优点:



  1. 减少冗余的数据传输;

  2. 减少服务器负担;

  3. 加快客户端加载网页的速度;


20230624082050


浏览器会首先获取该资源缓存的 header 信息,然后根据 Cache-Controlexpires 来判断是否过期。


如图,在浏览器第一次发送请求后,需要再次发送请求时,它会经过以下几个步骤:




  1. 首先,浏览器发送请求到服务器,请求的资源可能是一个网页、css 文件、JavaScript 文件或者其他类型的文件;




  2. 当服务器接收到请求后,首先检查请求中的缓存策略,例如请求头中的 Cache-Controlexpires 字段;




  3. 如果资源被标记为强缓存,服务器会进行以下判断:



    • 如果缓存有效,即资源的过期时间未到达或过期时间在当前时间之后,服务器返回状态码为 200 ok,并在响应头中设置适当的缓存策略,例如设置 Cache-ControlExpires 字段,告诉浏览器可以使用本地缓存;

    • 如果缓存无效,即资源的过期时间已过或过期时间在当前时间之前,服务器返回新的资源,状态码为 200 ok,并在响应头中设置适当的缓存策略;




  4. 如果资源未被标记为强缓存或缓存验证失败,服务器进行协商缓存的判断:



    • 如果请求头中包含 If-Modified-Since 字段,表示浏览器之前缓存了该组员并记录了最后修改时间,服务器会根据资源的最后修改时间进行判断;

      • 如果资源的最后修改时间与 If-Modified-Since 字段的值相同或更早,服务器返回状态码 304 Not Modified,并在响应头中清除实际的响应头;

      • 如果资源的最后修改时间晚于 If-Modified-Since 字段的值,表示资源已经发生了变化,服务器返回新的资源,状态码为 200 ok,并在响应头中设置新的最后修改时间;



    • 如果请求头中包含 If--Match 字段,表示浏览器之前缓存了该资源并记录资源的 ETag 值,服务器会根据资源的 ETag 进行判断:

      • 如果资源的 ETagIf--Match 字段的值相同,服务器返回状态码 304 Not Modified,并在响应头中清除实际的响应体;

      • 如果资源的 ETagIf--Match 字段的值不同,表示资源已经发生了变化,服务器返回新的资源,状态码为 200 OK,并在响应头中设置新的 ETag;






  5. 浏览器接收到服务器的响应之后,根据状态码和响应头信息进行相应的处理:



    • 如果状态码为 200 OK,表示服务器返回了新的资源,浏览器使用新的资源并更新本地缓存;

    • 如果状态码为 304 Not Modified,表示资源未发生变化,浏览器使用本地缓存的副本;

    • 浏览器根据响应头中的缓存策略进行进一步处理:

      • 如果响应头中包含 Cache-Control 字段,浏览器根据其指令执行缓存策略。例如,如果响应头中的 Cache-Control 包含 no-cache,浏览器将不使用本地缓存,而是向服务器发送请求获得最新的资源;

      • 如果响应头中包含 Expires 字段,浏览器将与当前时间比较,判断资源的过期时间。如果过期时间已过,浏览器将不使用本地缓存,而是向服务器发送请求获取最新的资源;

      • 如果响应头中包含其他相关的缓存控制字段(如 ETag),浏览器可以根据这些字段进行更精确的缓存控制和验证;






其中,在上面的流程中,又有几个令人难懂的字段,主要有以下几个:



  1. ETag: 它是通过对比浏览器和服务器资源的特征值来决定是否要发送文件内容,如果一样就只发送 304 Not Modified;

  2. Expires: 设置过期时间,是绝对时间;

  3. Last-Modified: 以时刻作为标识,无法识别一秒内进行多次修改的情况,只要资源修改,无论内容是否发生实质性变化,都会将该资源返回客户端;

  4. If--Match: 当客户端发送 GET 请求时,如果之前已经键相用资源的请求时,并且服务器返回了 ETag,那么客户端可以将 ETag 的值添加到 If--Match 头中,当再次请求该资源时,客户端会将 If--Match 头发送给服务器,服务器收到请求之后,会检查 If--Match 投中的值是否与当前资源的 ETag 值匹配:

    • 如果匹配,则表示客户端所请求的资源没有发生变化,服务器会返回状态码 304 Not Modified,并且不返回实际的资源内容;

    • 如果 If--Match 头中的值与服务器上资源的 ETag 值不匹配,说明资源发生了变化,服务器会正常返回资源,并返回状态码 200 OK;




图解强缓存和协商缓存


在上面的内容中讲了这么多的理论, 你是否还是不太理解什么是 强缓存协商缓存 啊,那么接下来我们就用几张图片来弄清楚这两者的区别。


强缓存


强缓存就是文件直接从本地缓存中获取,不需要发送请求。


首次请求


20230624103449


当浏览器发送初次请求时,浏览器会向服务器发起请求,服务器接收到浏览器的请求后,返回资源并返回一个 Cache-Control 字段给客户端,在该字段中设置一些缓存相关的信息,例如最大过期时间。


再次请求


20230624103906


在前面的基础上,浏览器再次发送请求,浏览器一节接收到 Cache-Control 的值,那么这个时候浏览器它会首先检查它的 Cache-Control 是否过期,如果没有过期则直接从本地缓存中拉取资源,返回割到客户端,则无需再经过服务器。


缓存失效


20230624104233


强缓存有过期时间,那么就意味着总有一天缓存会失效,如果客户端的 Cache-Control 失效了,那么它就会像首次请求中一样,重新向服务器发起请求,之后服务器会再次返回资源和 Cache-Control 的值。


协商缓存


协商缓存也叫做对比缓存,服务端判断客户端的资源是否和服务端的一样,如果一样则返回 304,反之返回 200 和最新的资源。


初次请求


20230624112243


如果客户端是第一次向服务器发出请求,则服务器返回资源和对应的资源标识给浏览器,该资源标识就是对当前所返回资源的唯一标识,可以是 ETag 或者是 Last-Modified


之后如果浏览器再次发送请求是,浏览器就会带上这个资源表,此时服务端就会通过这个资源标识,可以判断出浏览器的资源跟服务器此时的资源是否一致,如果一致则返回 304 Not Modified,如果不一致,则返回 200,并返回资源以及新的资源标识。


不同刷新操作方式,对强制缓存和协商缓存的影响


不同的刷新操作方式对于强制缓存和写上缓存的影响如下:




  1. 普通刷新(F5刷新按钮):



    • 强制缓存的影响: 浏览器忽略强制缓存,直接向服务器发送请求,获取最新的资源,也就是强制缓存失效;

    • 协商缓存的影响: 浏览器放带有缓存验证的字段的请求,浏览器会根据验证结果返回新的资源或者 304 Not Modified;




  2. 强制刷新(Ctrl+F5Shift+刷新按钮):



    • 强制缓存的影响: 同上,强制缓存失效;

    • 协商缓存的影响: 浏览器发送不带缓存验证字段的请求,服务器返回新的资源,不进行验证,也就是协商缓存失效;




  3. 禁用缓存刷新(DevTools 中的 Disable cacheNetwork 勾选 Disable cache):



    • 强制缓存的影响: 同上,强制缓存失效;

    • 协商缓存的影响: 浏览器会发送带有缓存验证字段的请求,服务器会根据验证结果返回新的资源或 304 Not Modified;




这玩意也就图一乐,一般出现了问题我都是直接重启......


总结


总的来说,强制缓存是通过在请求中添加缓存策略,判断缓存是否有效,避免发送请求到服务器。而协商缓存是通过条件请求与服务器进行通信,验证缓存是否仍然有效,并在服务器返回适当的响应状态码和缓存策略。


强制缓存可以减少对服务器的请求,加快资源加载速度,但可能无法获取到最新的资源。协商缓存能够验证资源的有效性,并在需要时获取最新的资源,但会增加对服务器的请求。选择使用哪种缓存策略取决于具体的应用场景和资源的特性。


参考资料


你知道 304 吗?图解强缓存和协商缓存


作者:Moment
来源:juejin.cn/post/7248235392284721209
收起阅读 »

你真的会用<a>标签下载文件吗?

web
最近和后端联调下载时忽然发现屡试不爽的 <a> 标签下载失灵了?这才感觉自己对文件下载一直处在一知半解的模糊状态中,趁端午前夕有空赶紧总结了一下,和大家一起讨论讨论。 <a> 标签 download 这应该是最常见,最受广大人民群众喜闻...
继续阅读 »

最近和后端联调下载时忽然发现屡试不爽的 <a> 标签下载失灵了?这才感觉自己对文件下载一直处在一知半解的模糊状态中,趁端午前夕有空赶紧总结了一下,和大家一起讨论讨论。


<a> 标签 download


这应该是最常见,最受广大人民群众喜闻乐见的一种下载方式了,搭配上 download 属性, 就能让浏览器将链接的 URL 视为下载资源,而不是导航到该资源。


如果 download 再指定个 filename ,那么就可以在下载文件时,将其作为预填充的文件名。不过名字中的 /\ 会被转化为下划线 _,而且文件系统可能会阻止文件名中的一些字符,因此浏览器会在必要时适当调整文件名。


封装下载方法


贴份儿我常用的下载方法:


const downloadByUrl = (url: string, filename: string) => {
if (!url) throw new Error('当前没有下载链接');

const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = filename;
// 使用target="_blank"时,添加rel="noopener noreferrer" 堵住钓鱼安全漏洞 防止新页面window指向之前的页面
a.rel = "noopener noreferrer";
document.body.append(a);
a.click();

setTimeout(() => {
a.remove();
}, 1000);
};

Firefox 不能一次点击多次下载


这里有个兼容性问题:在火狐浏览器中,当一个按钮同时下载多个文件(调用多次)时,只能下载第一个文件。所以,我们可以利用 <a> 标签的 target 属性,将其设置成 _blank 让火狐在一个新标签页中继续下载。


// 检查浏览器型号和版本
const useBrowser = () => {
const ua = navigator.userAgent.toLowerCase();
const re = /(msie|firefox|chrome|opera|version).*?([\d.]+)/;
const m = ua.match(re);
const Sys = {
browser: m[1].replace(/version/, "'safari"),
version: m[2]
};

return Sys;
};

添加一个浏览器判断:


const downloadByUrl = (url: string, filename: string) => {
// 略......

// 火狐兼容
if (useBrowser().browser === "firefox") {
a.target = "_blank";
}

document.body.append(a);
}

download 使用注意点


<a> 标签虽好,但还有一些值得注意的点:


1. 同源 URL 的限制



download 只在同源 URL 或 blob:data: 协议起作用



也就是说跨域是下载不了的......


首先,非同源 URL 会进行导航操作。其次,如果非要下载,那么正如上面的文档所说,可以先将其转换为 blob:data: 再进行下载,至于如何转换会在 Blob 章节中详细介绍。


2. 无法鉴权


使用 <a> 标签下载是带不了 Header 的,因此也不能携带登录态,所以无法进行鉴权。这里我们给出一个解决方案:



  1. 先发送请求获取 blob 文件流,这样就能在请求时进行鉴权;

  2. 鉴权通过后再执行下载操作。


这样是不是就能很好的同时解决问题1和问题2带来的两个痛点了呢😃


顺便提一下,location.hrefwindow.open 也存在同样的问题。


3. download 与 Content-Disposition 的优先级


这里需要关注一个响应标头 Content-Disposition,它会影响 <a>的 download 从而可能产生不同的下载行为,先看一个真实下载链接的 Response Headers


Snipaste_2023-06-20_18-19-21.png


如图所示,Content-Disposition 的 value 值为 attachment;filename=aaaa.bb。请记住,此时Content-Disposition 的 filename 优先级会大于 <a> download 的优先级。也就是说,当两者都指定了 filename 时,会优先使用 Content-Disposition 中的文件名。


接下来我们看看这个响应标头到底是什么。


Content-Disposition



在常规的 HTTP 应答中,Content-Disposition 响应标头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。



Content-Type 不同,后者用来指示资源的 MIME 类型,比如资源是图片(image/png)还是一段 JSON(application/json),而 Content-Disposition 则是用来指明该资源是直接展示在页面上的,还是应该当成附件下载保存到本地的。


当它作为 HTTP 消息主题的标头时,有以下三种写法:


Content-Disposition: inline
Content-Disposition: attachment
Content-Disposition: attachment; filename="filename.jpg"

inline


默认值,即指明资源是直接展示在页面上的。
但是在同源 URL 情况下,<a> 元素的 download 属性优先级比 inline 大,浏览器优先使用 download 属性来处理下载(Firefox 早期版本除外)。


attachment


即指明资源应该被下载到本地,大多数浏览器会呈现一个 “保存为” 的对话框,如果此时有 filename,那么它将其优于 download 属性成为下载的预填充文件名。


<a>标签 VS Content-Disposition


介绍完 Content-Disposition,我们做一个横向比对的归纳一下:




  • download VS inline/attachment


    优先级:attachment > download > inline




  • download 的值 VS filename


    优先级:filename > download 的值




Blob 转换


前文介绍到,在非同源请情况下可以将资源当成二进制的 blob 先拿到手,再进行 <a> 的下载处理。接下来,我们介绍两种 blob 的操作:


方法1. 用作 URL(blob:)


URL.createObjectURL 可以给 FileBlob 生成一个URL,形式为 blob:<origin>/<uuid>,此时浏览器内部就会为每个这样的 URL 存储一个 URL → Blob 的映射。因此,此类 URL 很短,但可以访问 Blob。


那这就好办多了,写成代码就三行:


import downloadByUrl from "@/utils/download";

const download = async () => {
const blob = await fetchFile();

// 生成访问 blob 的 URL
const url = URL.createObjectURL(blob);

// 调用刚刚封装的 a 标签下载方法
downloadByUrl(url, "表格文件.xlsx");

// 删除映射,释放内存
URL.revokeObjectURL(url);
};

不过它有个副作用。虽然这里有 Blob 的映射,但 Blob 本身只保存在内存中的。浏览器无法释放它。


在文档退出时(unload),该映射会被自动清除,因此 Blob 也相应被释放了。但是,如果应用程序寿命很长,那这个释放就不会很快发生。


因此,如果我们创建一个 URL,那么即使我们不再需要该 Blob 了,它也会被挂在内存中。


不过,URL.revokeObjectURL 可以从内部映射中移除引用,允许 Blob 被删除并释放内存。所以,在即时下载完资源后,不要忘记立即调用 URL.revokeObjectURL。


方法2. 转换为 base64(data:)


作为 URL.createObjectURL 的一个替代方法,我们也可以将 Blob 转换为 base64-编码的字符串。这种编码将二进制数据表示为一个由 0 到 64 的 ASCII 码组成的字符串,非常安全且“可读”。


更重要的是 —— 我们可以在 “data-url” 中使用此编码。“data-url” 的形式为 data:[<mediatype>][;base64],<data>。我们可以在任何地方使用这种 url,和使用“常规” url 一样。


FileReader 是一个对象,其唯一目的就是从 Blob 对象中读取数据,我们可以使用它的 readAsDataURL 方法将 Blob 读取为 base64。请看以下示例:


import downloadByUrl from "@/utils/download";

const download = async () => {
const blob = await fetchFile();

// 声明一个 fileReader
const fileReader = new FileReader();

// 将 blob 读取成 base64
fileReader.readAsDataURL(blob);

// 读取成功后 下载资源
fileReader.onload = function () {
downloadByUrl(fileReader.result);
};
};

在上述例子中,我们先实例化了一个 fileReader,用它来读取 blob。


一旦读取完成,就可以从 fileReader 的 result 属性中拿到一个data: URL 格式的 Base64 字符串。


最后,我们给 fileReader 注册了一个 onload 事件,在读取操作完成后开始下载。


两种方法总结与对比


URL.createObjectURL(blob) 可以直接访问,无需“编码/解码”,但需要记得撤销(revoke);


Data URL 无需撤销(revoke)任何操作,但对大的 Blob 进行编码时,性能和内存会有损耗。


总而言之,这两种从 Blob 创建 URL 的方法都可以用。但通常 URL.createObjectURL(blob) 更简单快捷。


responseType


最后,我们回头说一下请求的注意点:如果你的项目使用的是 XHR (比如 axios)而不是 fetch, 那么请记得在请求时添加上 responseType 为 'blob'。


export const fetchFile = async (params) => {
return axios.get(api, {
params,
responseType: "blob"
});
};

responseType 不是 axios 中的属性,而是 XMLHttpRequest 中的属性,它用于指定响应中包含的数据类型,当为 "blob" 时,表明 Response 是一个包含二进制数据的 Blob 对象。


除了 blob 之外,responseType 还有 arraybufferjsontext等其他枚举字符串值。


总结


一言以蔽之,同源就直接使用 <a> download 下载,跨域就先获取 blob,用 createObjectURLreadAsDataURL 读取链接,再用 <a> download 下载。


参考资料


收起阅读 »

北京十年,来深圳了

离开北京是计划 2013年去的北京,至今整十年,来去匆匆。 几年前就计划好了,赶在孩子上幼儿园之前离开北京,选一个城市定居。 给孩子一个稳定的环境,在这儿上学成长,建立稳定的、属于他自己的朋友圈。人一生中最珍贵的友谊都是在年少无知、天真烂漫的时候建立的。 我们...
继续阅读 »


离开北京是计划


2013年去的北京,至今整十年,来去匆匆。


几年前就计划好了,赶在孩子上幼儿园之前离开北京,选一个城市定居。


给孩子一个稳定的环境,在这儿上学成长,建立稳定的、属于他自己的朋友圈。人一生中最珍贵的友谊都是在年少无知、天真烂漫的时候建立的。


我们希望孩子从他有正式的社交关系开始-幼儿园阶段,尽早适应一个省市的教育理念和节奏,不要等到中小学、甚至高中阶段突然的打断孩子的节奏,插班到一个陌生的班级。他同时要面临环境和学业的压力,不是每个孩子都能很快调整过来的。


我自己小学阶段换了好几次学校,成绩的波动很明显,不希望孩子再面临同样的风险。


另一方面,基于我们年龄的考虑,也要尽快离开,岁数太大了,换城市跳槽不一定能找到合适的岗位。


19年,基于对移动端市场的悲观,我开始考虑换一个技术方向。2020年公司内转岗,开始从事图形相关技术开发,计划2023年离开北京,是考虑要留给自己3年的时间从零开始积累一个领域的技术。


来深圳市是意外


这几年一直在关注其他城市的"落户政策"、"互联网市场"、"房价"、"政府公共服务"。有几个城市,按优先级:杭州、广州、武汉、深圳。这些都是容易落户的城市,我们想尽快解决户口的困扰。


看几组数据:




2023年5月份数据


可以看到,杭州的房价排在第6位,但是收入和工作机会排进前4,所以首选杭州,性价比之王。


广州的房价和工作收入都排第5,中策。


武汉的工作机会排进前10,但是房价在10名开外,而且老家在那边,占尽地利,下策。


深圳的房价高的吓人,和这个城市提供的医疗、教育太不匹配,下下策。


最后选择深圳是形势所逼,今年行情史上最差,外面的机会很少。我和老婆都有机会内部转岗到深圳,所以很快就决定了。


初识深圳


来之前做了基本的调研,深圳本科45岁以内 + 1个月社保可以落户。我公司在南山,老婆的在福田,落户只能先落到对应的区。


我提前来深圳,一个星期租好了房子,确定了幼儿园。


老婆步行15分钟到公司,孩子步行500米到幼儿园,我步行 + 地铁1小时到公司。


福田和南山的教育资源相对充足,有些中小学名校今年都招不满,租房也能上,比龙华、宝安、龙岗等区要好很多。


听朋友说,在龙华一个很差的公立小学1000个小孩报名,只有500个学位。


有不少房东愿意把学位给租户使用,办理起来也不麻烦,到社区录入租房信息即可。和北京一样,采取学区划分政策,按积分排名录取,非常好的学校也要摇号碰运气。


租房


中介小哥陪我看了三四天房子,把这一片小区都看了个遍。考虑近地铁、近幼儿园、有电梯、装修良好等因素。


我本来想砍200房租,中介说先砍400,不行再加。结果我说少400,房东直接说好。我原地愣住了,之前排练的戏份都用不上了,或许今年行情不好,租房市场也很冷淡吧。


小区后面是小山,比较安静。


小区附近-0


小区附近-1


小区附近-2


小区附近-3


外出溜达,路过一所小学


深圳的很多小区里都有泳池
小区-泳池


夜晚的深圳,高楼林立,给人一种压迫感,和天空格格不入。明亮的霓虹灯,和北京一样忙碌。


晚上8点的深圳


晚上10点的深圳


对教育的看法



幸运的人一生都被童年治愈,不幸的人一生都在治愈童年--阿德勒



身边的朋友,有不少对孩子上什么学校有点焦虑,因为教育和高考的压力,有好友极力劝阻我来深圳。我认为在能力的范围内尽力就好,坦然面对一切。


焦虑是对自己无能为力的事情心存妄念。 如果一个人能坦然面对结果,重视当下,不虚度每一分每一秒,人生就不应该有遗憾。人生是来看风景的,结局都是一把灰,躺在盒子里,所以不要太纠结一定要结果怎么样。


学校是培养能力的地方,学历决定一个人的下限,性格和价值观决定上限,你终究成要为你想成为的人,不应该在自我介绍时除了学历能拿出手,一无是处。


不少人不能接受孩子比自己差。可是并没有什么科学依据能证明下一代的基因一定优于上一代吧,或许他们只是不能接受孩子比他们差,他们没有面子,老无所依。我天资一般,我也非常能接受孩子天资平庸,这是上天的旨意。


有些父母根本没有做好家庭教育,试图通过卷学校、一次性的努力把培养的责任寄托于学校。挣钱是成就自己,陪伴是成就孩子,成功的父母居中取舍。


陪伴是最好的家庭教育,如果因为工作而疏忽了孩子,我认为这个家庭是失败的,失败的家庭教育会导致家庭后半生矛盾重重,断送了全家人的幸福。


一个人缺少父爱,就缺少勇敢和力量,缺少母爱就缺少细腻与温和,孩子的性格很容易不健全。除非他自己很有天赋,能自己走出童年的阴影。


因为他长大面对的社会关系是复杂的,他需要在性格层面能融入不同的群体。性格不健全的孩子更容易走向偏激、自私、虚伪、或者懦弱,很多心理学家都是自我治疗的过程中,成为心理学大师。


一个人的一生中,学历不好带来的困扰是非常局部的,但是性格带来的问题将困扰其一生,包括工作、交朋结友、娶妻生子,并且还会传染给下一代。


榜样是最好的教育方法,没有人会喜欢听别人讲大道理,言传不如身教。有些人自己过的很可怜,拼命去鸡娃,那不是培养孩子,那是转移压力,过度投资,有赌棍的嫌疑。你自己过的很苦逼,你如何能说服孩子人生的意义是幸福?鸡娃的尽头是下一代鸡娃。


你只有自己充满能量,积极面对人生,你的孩子才会乐观向上;你只有自己持续的阅读、成长,你的孩子才会心悦诚服的学习;你只有自己做到追求卓越,你的孩子才会把优秀当成习惯。


不要给孩子传递一种信号,人生是苦的,要示范幸福的能力,培养孩子积极地入世观。


作者:sumsmile
来源:juejin.cn/post/7248199693934985272
收起阅读 »

写给毕业季的学生们|我的五次 offer 选择经历

最近临近毕业季,群里有好多朋友在问面试和 offer 选择的问题,我分享下我过往的相关经历,希望能给各位朋友有所启发。 我是谁? 大家好,我是拭心,内蒙古人,16 年本科毕业于西安电子科技大学,先后在创业公司、字节跳动和喜马拉雅工作,目前定居在上海。 &nbs...
继续阅读 »

最近临近毕业季,群里有好多朋友在问面试和 offer 选择的问题,我分享下我过往的相关经历,希望能给各位朋友有所启发。


我是谁?


大家好,我是拭心,内蒙古人,16 年本科毕业于西安电子科技大学,先后在创业公司、字节跳动和喜马拉雅工作,目前定居在上海。


 
2014 年开始在 CSDN 上写作,到目前为止博客访问量约 390万:



shixin.blog.csdn.net/


先后在 GitChat 和极客时间出过小课:


image.png


常在社区做 Android 相关的技术分享,比如最近在 OPPO 做的技术交流:



 
以上是我的基本信息,介绍这些经历是为了让后面的分享更有说服力,接下来看看我过往的五次工作选择经历。
 


五次工作选择经历



我从 2015 年开始找实习,到现在经历过五次换工作,分别是实习、校招和三次社招。


2015 春季实习:屌丝开局



我从 2014 年暑假开始学习 Android 开发,当时成体系的课程不多,学习的材料主要是图书馆里很老的书和校内网的视频,当时贪玩游戏,大部分时间都用来打 LOL,因此编程技术比较差。


2015 春看着舍友们开始找实习,我也投出了实习简历,结果很惨淡:网易阿里面试均失败,鼓起勇气去腾讯面试酒店做了次“霸面”,结果也不了了之没有下文。


好在后面通过了西安一家公司的面试,每天给 100 元工资,当时想着也没有更好的机会,就接了这个 offer。


现在来看,选择接受这个 offer 是个正确的选择,当时我的水平很菜,继续面试可能也没有更好的机会,反而耽误了大好的时间。与其临渊羡鱼,不如先拿到自己能拿到的,同时退而结网。


而做的不好的是,对校招面试重点不了解,没有重视基础。考试前刷了很多面试题,但没想到阿里腾讯压根没怎么问 Android 上层技术,反而问了很多 Java 基础和算法。


 
实习不像自己做玩具,真正的商业项目让我对用到的技术有了更多的了解,也认识到自己需要补充哪些知识点。


更重要的是,有了这个实习经验后,我在秋招时找工作容易了很多。


 


2015 秋季校招:拿下 offer 后犯了懒



秋招的时候,因为有实习的经验,同时我针对性的进行了查漏补缺,面试情况比春季好了很多,大概面了六七家公司,拿到了华为(base 西安)和两家创业公司的 offer(A base 北京,B base 上海)。


当时校招都是在线下进行,有的公司会来我们学校,还有些公司会在市里包下一个酒店进行校招(当时正值移动互联网的辉煌时期,校招很大阵仗),进行面试需要坐公交跑好几个地方,有点累人。
 


在拿到几个 offer 后,我心疼自己不愿意再辛苦,就再也没关注其他公司的招聘,而是去成都重庆九寨沟旅游、在宿舍里打游戏了。


当时选择公司 B 主要看中两个点:1. 工资还可以(三家最高) 2. 上海定居比北京容易,空气也好


实习结束后因为和同事相处的比较愉快,也没有再面试其他公司,就这样决定了自己的正式工作。


现在来看,当时选择上海是对的,因为北京拿户口真的太难了;做的不好的是,在拿到几个 offer 后就心满意足,没有再去面其他公司。


这其实不对,应该再看看有没有更好的机会。马太效应(强者越强)同样适用于程序员,那些在一开始就在更好环境的程序员,往往成长的更快更好,因为他们每天接触的信息、处理的问题,都会更有价值。我暗自和校友对比过,当时去了大厂的校友,有好几个已经成长为部门/业务负责人,反观自己,在比较努力的情况下,才没有差的特别远。
 


很多人会花时间在学习编程 技术 上,但对「去哪里、和谁、做什么样的工作」却没有该有的重视。 对于应届生来说,第一份工作很重要,它很大程度决定了我们的起始速度,不要像我当时一样懒得去面凑合了事,请记住:强者愈强。


 


2017 第一次社招:被一份盒饭感动了



 


2017 年九月我从第一家公司离职,完成了毕业后第一次社招跳槽。
 


为什么要离职呢?主要是因为创业太难了,公司的盈利模式在反复调整后还是不及预期,在我离职前两三个月里基本没什么活干,大家都在默默等着领大礼包。
 


那段时间我面试了不少上海的互联网公司,比如流利说、银天下、饿了么、喜马拉雅、美团点评等等,基本都拿到了 offer 。


当时非常纠结的是饿了么和喜马拉雅选哪个,两个岗位都比较喜欢,面试官给的感觉也都不错。最后考虑再三选了喜马,让天平倾斜的是一份午饭。


在喜马面试的那天,从上午十点面试到了下午四点多,中午 12 点到 2 点休息。在我准备出去找点饭吃时,面试官亲切的给我拿来一份饭,这让我觉得非常温暖,一对比前一天去某公司面了几个小时连水都没得喝,差距太明显了。


现在来看,当时面试通过率比较高,主要是因为这三点:


1.简历很清晰,重点很突出:「用什么技术、做了什么、有什么收获」


2.知识体系比较齐全,吸取春季实习的教训,从计算机基础、Java 集合/并发/虚拟机到 Android SDK/三方库都进行了系统的学习,面试的问题基本都能回答上


3.有博客展示自己,可以让面试官对我有更多的了解


 


当时做的不对的是这几点:


1.入职太快,上家周五离职,下家周一入职,没有多留点思考做规划


2.薪资没怎么涨,不会拿着 offer 要价


 
同时又一次犯了秋招的错误:入职一周后腾讯发来了二面邀请,当时因为不想再折腾,拒绝了后面的面试流程。


 


2020 第二次社招:伤了很多 HR 的心


 



 


2020 年六月我从第二家公司离职,完成了毕业后第二次社招跳槽。


 


为什么要离职呢?主要是因为在两年九个月的工作里,(当时自认为)项目用到的技术点基本都学差不多了。


 


这次面试了更多的公司,拿到了美团点评、阿里、B 圈、字节等公司 offer。


 


能够拿到这么多 offer 的原因:


1.2017 年换完工作后梳理了面试经验并出了个课程,算是有些方法;


2.CSDN 博客访问量增加较多,获得了“博客专家”认证,算是有了点背书;


3.学习的知识有体系,并且都写了文章,记忆很深,很多细节都回答上来了


 
在这些 offer 里,最早拿到的是点评的 offer,薪资不高不低,业务不太核心,先当备胎;后来拿到阿里的 offer,薪资不多,又要换城市,阿里梦敌不过现实,最终拒掉了;B 圈给的挺多,但担心被抓不敢去;最后选择了字节,因为做的是我喜欢的纯基础架构。


老实讲当时很想去阿里,但这个 offer 给的工资没涨多少。虽然部门 leader 和 HR 一个劲的说过去好好干会发股票,但思考再三,我还是决定先把目前该拿的拿到。
 


现在来看,当时做的对的点:


1.拿着 offer 和想去的公司谈价,证明自己的价值


2.没有吃饼,该有的和表现好才有的是两码事


3.选了基础架构,让我的技术有了很大的提升,对后面发展更好


 


 


2022 年第三次社招:华丽转身


 



 


2022 年中我从第三家公司离职,完成了毕业后第三次社招跳槽。


 


为什么要离职呢?主要是因为当时做的是纯架构,需要找到可优化点、进行优化并且推广到业务,在字节的“追求极致”文化下,很多事已经被别人做过了,如果有新的机会,会有大量的人瞬间涌入。经常出现做了好几个版本的实验,最后发现数据不符合预期,或者符合预期但是业务拒绝接入。这种状态久了,有些觉得累。


 


这次工作经历给我的感受是:完全脱离业务的架构,适合年轻人去提升技术,但想做出大成绩很难,需要放平心态(技术非常牛逼的大佬可以忽略这句话)。


 


这次换工作,没面太多公司,主要是因为心里已经有所属,回到熟悉的环境,做了更重要的事。


 


现在来看,能够回去并且担任更重要的责任,主要是因为之前在公司时秉承了“利他”精神,和同事领导们相处的比较融洽,同时自己的能力也被认可。互联网的圈子很小,之前的同事很有可能再续前缘,勿以善小而不为。


 


总结


 


好了,这就是我从实习到现在的五次工作选择经历,谢谢你的阅读。


 


总结下来主要有这些经验:


1.校招基础要扎实;社招要有亮点、有背书


2.选择城市很重要,一开始要去一线城市


3.开头很重要,不要懒得去面凑合了事,记住强者愈强


4.拿到 offer 不急着决定,拿着 offer argue


5.年轻选纯架构,年长选业务架构(有的选的话)


6.与人为善,圈子很小**


 


如果对你有什么启发,欢迎留言点赞,你的鼓励就是我创作的最大动力!



作者:张拭心
来源:juejin.cn/post/7247897453914816573

收起阅读 »

API开放生态平台如何赋能企业数字化转型?

数据服务的共享开放作为企业内部发展的重要因素,已经被越来越多的企业所重视,但在此过程中,不少企业内部存在很多问题:一方面存在严重的数据孤岛现象,多个业务系统之间数据难以打通,业务衔接不够顺畅,导致用户使用系统会更加复杂,数字化作用失效;另一方面数据共享开放的需...
继续阅读 »

数据服务的共享开放作为企业内部发展的重要因素,已经被越来越多的企业所重视,但在此过程中,不少企业内部存在很多问题:一方面存在严重的数据孤岛现象,多个业务系统之间数据难以打通,业务衔接不够顺畅,导致用户使用系统会更加复杂,数字化作用失效;另一方面数据共享开放的需求明显,但是数据安全无法保障,部分企业客户倾向于线下提供数据进行共享,会导致数据无法实时更新而且无法控制数据的流向。这两大问题既无法提高企业的业务效能,也影响到数据的安全。

基于此,数聚变开放生态平台帮助企业构建服务共享体系,促进资源打通,实现数字能力开放、协同、融合、聚变,驱动行业进化。这其中主要包含以下五个核心功能:


  • 一、数据采集转发

软硬件一体化方案,实现电力企业各类数据共享打通,全方位保障数据安全。穿透电力行业网络分区限制,符合电力行业安全标准。支持百余种电力及工业通讯协议的数据采集转发,一站式交付服务为项目落地保驾护航。设备、协议统一管理,设备模型快速映射,沉淀企业数据资产。

  • 二、数据集成共享

从数据层面解决数据孤岛和数据不一致性问题。通过集成各系统的多种异构数据源来实现数据的即时聚合、分发和共享。同时,我们实现了多个业务系统之间的即时数据同步,确保业务操作的连接性并精确传输数据。

  • 三、数据开放要素流通

覆盖“采”“存”“管”“用”“服”数据链路,实现数据高效便捷流通。整合数据资源,提升数据质量,保障数据安全,衍生数智应用,释放数据价值。通过数据共享、数据开放和数据交易促进数据流通,加快数据要素资源化-资产化-资本化进程。

  • 四、企业数字化咨询

深耕新能源领域,洞察行业先机,促进数字化重塑。提供覆盖行业政策解读,企业战略研究,业务调研及数智化提升的全链路数字化转型规划方案。帮助企业快速定位数字化转型痛点,从业务、应用、数据、技术多位切入,全方面规划转型蓝图,合理化布局转型演进路线。

  • 五、API全生命周期管理

建立贯穿API创建-分类-发布-调用-下架全生命周期标准化管理体系。全方位API服务调用监控告警,保障服务链路安全合规。构建企业API资产目录,帮助企业沉淀API能力资产,提升API服务可复用性,促进API价值最大化。

收起阅读 »

北漂五年,我回家了。后悔吗?

2017年毕业后,我来到了北京,成为了北漂一族。五年后,我决定回家了。也许是上下班一个多小时的通勤,拥挤的地铁压得我喘不过气;也许是北漂五年依然不能适应干燥得让人难受的气候。北京很大,大得和朋友机会要提前两个小时出门;北京很小,我每天的活动范围就只有公司、出租...
继续阅读 »

2017年毕业后,我来到了北京,成为了北漂一族。五年后,我决定回家了。也许是上下班一个多小时的通勤,拥挤的地铁压得我喘不过气;也许是北漂五年依然不能适应干燥得让人难受的气候。北京很大,大得和朋友机会要提前两个小时出门;北京很小,我每天的活动范围就只有公司、出租屋两点一线。今年我觉得是时候该回家乡了。


1280X1280 (1).JPEG


(在北京大兴机场,天微微亮)


有些工作你一面试就知道是坑


决定回家乡后,我开始更新自己的简历。我想过肯定会被降薪,但是没想到降薪幅度会这么大,成都前端岗位大多都是1w左右,想要双休那就更少了。最开始面试的一些岗位是单休或者大小周,后面考虑了一下最后都放弃了。那时候考虑得很简单,一是我没开始认真找工作,只是海投了几个公司,二是我觉得我找工作这儿时间还比较短,暂时找不到满意的很正常。


辞职后,我的工作还没有着落,于是决定先不找了,出去玩一个月再说。工作了这么久,休息一下不为过吧,于是在短暂休息了一个月后,我又开始认真找工作。


但是,但是没想到成都的就业环境还蛮差的,找工作的第二个月还是没有合适的,当时甚至有点怀疑人生了,难道我做的这个决定是错误的?记得我面试过一家公司,那家公司应该是刚刚成立的,boss上写的员工数是15个,当时我想着,刚成立的公司嘛,工资最开始低点也行,等公司后续发展起来了,升职加薪岂不美滋滋。


面试时,我等了老板快半小时,当时我对这家公司的观感就不太好了。但想着来都来了,总不能浪费走的这一趟。结果,在面试的时候老板开始疯狂diss我的技术不行,会的技能太少,企图用这种话来让我降薪。我是怎么知道他想通过这种方式让我降薪呢,因为最后那老板说“虽然你技术不行,但是我很看好你的学习能力,给你开xxx工资你愿意来吗?”


也是因为这次面试,我在招聘软件上看到那种小公司都不轻易去面试了,简直浪费我时间。


1280X1280.JPEG


(回家路上骑自行车等红绿灯,我的地铁卡被我甩出去了,好险,但是这张地铁卡最后还是掉了,还是在我刚充值完100后,微笑)


终于,找了大概3个月,终于找到一家还算不错的公司,在一家教育行业的公司做前端。双休,工资虽然有打折,但是在我能接受的范围内。


有些人你一见面就知道是正确的


其实我打算回家乡还有一个重要原因是通过大厂相亲角网恋了一个女孩子,她和我是一个家乡的。我们刚认识的时候几乎每天都在煲电话粥,基本上就是陪伴入眠,哈哈哈哈哈。语言的时候她还会唱歌给我听,偏爱、有可能的夜晚......都好好听,声音软绵绵的。认识一个月后,我们回了一趟成都和她面基。一路上很紧张,面基的时候也很害怕自己有哪里做得不好的地方,害怕给她留下不好的印象。我们面基之后一个月左右就在一起啦。有些人真的是你一见面就知道她是正确的那个人,一见面心里有一个声音告诉你“嗯,就是她了!”。万幸,我遇到了。


58895b3fc4db3554881bdbcaa35384f.jpg


1280X1280 (2).JPEG


说一些我们在一起后的甜蜜瞬间吧


打语言电话的时候,听着对方的呼吸声入睡;


走在路上的时候,我牵她的手,她会很顺其自然地与我十指相扣;


在一起吃饭的时候,她会把自己最好吃的一半分享给我;



总结


回到正题,北漂五年。我回家了,后悔吗?不后悔。离开北京快一年了,有时候还是会想念自己还呆在北京的不足10平米的小出租屋里的生活,又恍惚“噢,我已经回四川了啊”。北漂五年,我还是很感激那段时间,让刚毕业的我迅速成长成可以在工作上独当一面的合格的程序员,让我能有拿着不菲的收入,有一定的积蓄,有底气重新选择;感谢大厂相亲角,让我遇见我的女朋友,让我不再是单身狗。


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

在国企做程序员怎么样?

有读者咨询我,在国企做开发怎么样? 当然是有利有弊,国企相对稳定,加班总体没有互联网多,不过相对而言,工资收入没有互联网高,而且国企追求稳定,往往技术栈比较保守,很难接触新的技术,导致技术水平进步缓慢。 下面分享一位国企程序员的经历,希望能给大家一些参考价值。...
继续阅读 »

有读者咨询我,在国企做开发怎么样?


当然是有利有弊,国企相对稳定,加班总体没有互联网多,不过相对而言,工资收入没有互联网高,而且国企追求稳定,往往技术栈比较保守,很难接触新的技术,导致技术水平进步缓慢。


下面分享一位国企程序员的经历,希望能给大家一些参考价值。



下文中的“我”代表故事主人公



我校招加入了某垄断央企,在里面从事研发工程师的工作。下面我将分享一些入职后的一些心得体会。


在国企中,开发是最底层最苦B的存在,在互联网可能程序员还能够和产品经理argue,但是在国企中,基本都是领导拍脑袋的决定,即便这个需求不合理,或者会造成很多问题等等,你所需要的就是去执行,然后完成领导的任务。下面我会分享一些国企开发日常。


1、大量内部项目


在入职前几个月,我们都要基于一种国产编辑器培训,说白了集团的领导看市场上有eclipse,idea这样编译器,然后就说咱们内部也要搞一个国产的编译器,所有的项目都要强制基于这样一个编译器。


在国企里搞开发,通常会在项目中塞入一大堆其他项目插件,本来一个可能基于eclipse轻松搞定的事情,在国企需要经过2、3个项目跳转。但国企的项目本来就是领导导向,只需给领导演示即可,并不具备实用性。所以在一个项目集成多个项目后,可以被称为X山。你集成的其他项目会突然出一些非常奇怪的错误,从而导致自己项目报错。但是这也没有办法,在国企中搞开发,有些项目或者插件是被要求必须使用的。


2、外包


说到开发,在国企必然是离不开外包的。在我这个公司,可以分为直聘+劳务派遣两种用工形式,劳务派遣就是我们通常所说的外包,直聘就是通过校招进来的校招生。


直聘的优势在于会有公司的统一编制,可以在系统内部调动。当然这个调动是只存在于规定中,99.9%的普通员工是不会调动。劳务派遣通常是社招进来的或者外包。在我们公司中,项目干活的主力都是外包。我可能因为自身本来就比较喜欢技术,并且觉得总要干几年技术才能对项目会有比较深入的理解,所以主动要求干活,也就是和外包一起干活。一开始我认为外包可能学历都比较低或者都不行,但是在实际干活中,某些外包的技术执行力是很强的,大多数项目的实际控制权在外包上,我们负责管理给钱,也许对项目的了解的深度和颗粒度上不如外包。


上次我空闲时间与一个快40岁的外包聊天,才发现他之前在腾讯、京东等互联网公司都有工作过,架构设计方面都特别有经验。然后我问他为什么离开互联网公司,他就说身体受不了。所以身体如果不是特别好的话,国企也是一个不错的选择。


3、技术栈


在日常开发中,国企的技术一般不会特别新。我目前接触的技术,前端是JSP,后端是Springboot那一套。开发的过程一般不会涉及到多线程,高并发等技术。基本上都是些表的设计和增删改查。如果个人对技术没啥追求,可能一天的活2,3小时就干完了。如果你对技术有追求,可以在剩余时间去折腾新技术,自由度比较高。


所以在国企,作为普通基层员工,一般会有许多属于自己的时间,你可以用这些时间去刷手机,当然也可以去用这些时间去复盘,去学习新技术。在社会中,总有一种声音说在国企呆久了就待废了,很多时候并不是在国企待废了,而是自己让自己待废了。


4、升职空间


每个研发类央企都有自己的职级序列,一般分为技术和管理两种序列。


首先,管理序列你就不用想了,那是留给有关系+有能力的人的。其实,个人觉得在国企有关系也是一种有能力的表现,你的关系能够给公司解决问题那也行。


其次,技术序列大多数情况也是根据你的工龄长短和PPT能力。毕竟,国企研发大多数干的活不是研发与这个系统的接口,就是给某个成熟互联网产品套个壳。技术深度基本上就是一个大专生去培训机构培训3个月的结果。你想要往上走,那就要学会去PPT,学会锻炼自己的表达能力,学会如何讲到领导想听到的那个点。既然来了国企,就不要再想钻研技术了,除非你想跳槽互联网。


最后,在国企底层随着工龄增长工资增长(不当领导)还是比较容易的。但是,如果你想当领导,那还是天时地利人和缺一不可。


5、钱


在前面说到,我们公司属于成本单位,到工资这一块就体现为钱是总部发的。工资构成分由工资+年终奖+福利组成。


1.工资构成中没有绩效,没有绩效,没有绩效,重要的事情说三遍。工资是按照你的级别+职称来决定的,公司会有严格的等级晋升制度。但是基本可以概括为混年限。年限到了,你的级别就上去了,年限没到,你天天加班,与工资没有一毛钱关系。


2.年终奖,是总部给公司一个大的总包,然后大领导根据实际情况对不同部门分配,部门领导再根据每个人的工作情况将奖金分配到个人。所以,你干不干活,活干得好不好只和你的年终奖相关。据我了解一个部门内部员工的年终奖并不会相差太多。


3.最后就是福利了,以我们公司为例,大致可以分为通信补助+房补+饭补+一些七七八八的东西,大多数国企都是这样模式。


总结


1、老生常谈了。在国企,工资待遇可以保证你在一线城市吃吃喝喝和基本的生活需要没问题,当然房子是不用想的了。


2、国企搞开发,技术不会特别新,很多时候是项目管理的角色。工作内容基本体现为领导的决定。


3、国企研究技术没有意义,想当领导,就多学习做PPT和领导搞好关系。或者当一个平庸的人,混吃等死,把时间留给家人,也不乏是一种好选择。


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

Android 自定义View 之 饼状进度条

前言   前面写了圆环进度条,这次我们来写一个饼状进度条,首先看一下效果图: 正文   效果图感觉怎么样呢?下面我们来实现这个自定义View,依然是写在EasyView这个项目中,这是一个自定义View库,我会把自己写的自定义View都放在里面,文中如果代码...
继续阅读 »

前言


  前面写了圆环进度条,这次我们来写一个饼状进度条,首先看一下效果图:


在这里插入图片描述


正文


  效果图感觉怎么样呢?下面我们来实现这个自定义View,依然是写在EasyView这个项目中,这是一个自定义View库,我会把自己写的自定义View都放在里面,文中如果代码不是很全的话,你可以找到文章最后的源码去查看,话不多说,我们开始吧。


一、XML样式


  根据上面的效果图,我们首先来确定XML中的属性样式,在attrs.xml中添加如下代码:

	<!--饼状进度条-->
<declare-styleable name="PieProgressBar">
<!--半径-->
<attr name="radius" />
<!--最大进度-->
<attr name="maxProgress" />
<!--当前进度-->
<attr name="progress" />
<!--进度条进度颜色-->
<attr name="progressbarColor" />
<!--进度条描边宽度-->
<attr name="strokeWidth"/>
<!--进度是否渐变-->
<attr name="gradient" />
<!--渐变颜色数组-->
<attr name="gradientColorArray" />
<!--自定义开始角度 0 ,90,180,270-->
<attr name="customAngle">
<enum name="right" value="0" />
<enum name="bottom" value="90" />
<enum name="left" value="180" />
<enum name="top" value="270" />
</attr>
</declare-styleable>

  这里的公共属性我就抽离了出来,因为之前写过圆环进度条,有一些属性是可以通用的,并且我在饼状进度条中增加了开始的角度,之前是默认是从0°开始,现在可以根据属性设置开始的角度,并且我增加了渐变颜色。


二、构造方法


  现在属性样式已经有了,下一步就是写自定义View的构造方法了,在com.easy.view包下新建一个PieProgressBar 类,里面的代码如下所示:

public class PieProgressBar extends View {

/**
* 半径
*/
private int mRadius;
/**
* 进度条宽度
*/
private int mStrokeWidth;
/**
* 进度条进度颜色
*/
private int mProgressColor;
/**
* 开始角度
*/
private int mStartAngle = 0;

/**
* 当前角度
*/
private float mCurrentAngle = 0;
/**
* 结束角度
*/
private int mEndAngle = 360;
/**
* 最大进度
*/
private float mMaxProgress;
/**
* 当前进度
*/
private float mCurrentProgress;
/**
* 是否渐变
*/
private boolean isGradient;
/**
* 渐变颜色数组
*/
private int[] colorArray;
/**
* 动画的执行时长
*/
private long mDuration = 1000;
/**
* 是否执行动画
*/
private boolean isAnimation = false;

public PieProgressBar(Context context) {
this(context, null);
}

public PieProgressBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public PieProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.PieProgressBar);
mRadius = array.getDimensionPixelSize(R.styleable.PieProgressBar_radius, 80);
mStrokeWidth = array.getDimensionPixelSize(R.styleable.PieProgressBar_strokeWidth, 8);
mProgressColor = array.getColor(R.styleable.PieProgressBar_progressbarColor, ContextCompat.getColor(context, R.color.tx_default_color));
mMaxProgress = array.getInt(R.styleable.PieProgressBar_maxProgress, 100);
mCurrentProgress = array.getInt(R.styleable.PieProgressBar_progress, 0);
//是否渐变
isGradient = array.getBoolean(R.styleable.PieProgressBar_gradient, false);
//渐变颜色数组
CharSequence[] textArray = array.getTextArray(R.styleable.PieProgressBar_gradientColorArray);
if (textArray != null) {
colorArray = new int[textArray.length];
for (int i = 0; i < textArray.length; i++) {
colorArray[i] = Color.parseColor((String) textArray[i]);
}
}
mStartAngle = array.getInt(R.styleable.PieProgressBar_customAngle, 0);
array.recycle();
}
}

  这里声明了一些变量,然后写了3个构造方法,在第三个构造方法中进行属性的赋值。


三、测量


  这里测量就比较简单了,和之前的圆环进度条差不多,代码如下所示:

    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = 0;
switch (MeasureSpec.getMode(widthMeasureSpec)) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST: //wrap_content
width = mRadius * 2;
break;
case MeasureSpec.EXACTLY: //match_parent
width = MeasureSpec.getSize(widthMeasureSpec);
break;
}
//Set the measured width and height
setMeasuredDimension(width, width);
}

  因为不需要进行子控件处理,所以我们只要一个圆和描边就行了,下面看绘制的方法。


四、绘制


  绘制这里就是绘制描边和进度,绘制的代码如下所示:

    @Override
protected void onDraw(Canvas canvas) {
int centerX = getWidth() / 2;
@SuppressLint("DrawAllocation")
RectF rectF = new RectF(0,0,centerX * 2,centerX * 2);
//绘制描边
drawStroke(canvas, centerX);
//绘制进度
drawProgress(canvas, rectF);
}

  在绘制之前首先要确定中心点,因为我们是一个圆环,实际上也是一个圆,圆的宽高一样,所以中心点的x、y轴的位置就是一样的,然后是确定一个矩形的左上和右下两个位置的坐标点,通过这两个点就能绘制一个矩形,接下来就是绘制进度条背景。


① 绘制描边

    /**
* 绘制描边
*
* @param canvas 画布
* @param centerX 中心点
*/
private void drawStroke(Canvas canvas, int centerX) {
Paint paint = new Paint();
paint.setColor(mProgressColor);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(mStrokeWidth);
paint.setAntiAlias(true);
canvas.drawCircle(centerX, centerX, mRadius - (mStrokeWidth / 2), paint);
}

  这里的要点就是我们需要设置画笔的类型为描边,然后设置描边宽度,这样我们就可以画一个空心圆,就成了描边,然后我们绘制进度。


① 绘制进度

    /**
* 绘制进度条背景
*/
private void drawProgress(Canvas canvas, RectF rectF) {
Paint paint = new Paint();
//画笔的填充样式,Paint.Style.STROKE 描边
paint.setStyle(Paint.Style.FILL);
//抗锯齿
paint.setAntiAlias(true);
//画笔的颜色
paint.setColor(mProgressColor);
//是否设置渐变
if (isGradient && colorArray != null) {
paint.setShader(new RadialGradient(rectF.centerX(), rectF.centerY(), mRadius, colorArray, null, Shader.TileMode.MIRROR));
}
if (!isAnimation) {
mCurrentAngle = 360 * (mCurrentProgress / mMaxProgress);
}
//开始画圆弧
canvas.drawArc(rectF, mStartAngle, mCurrentAngle, true, paint);
}

  因为背景是一个圆环,所以这里的画笔设置就比较注意一些,看一下就会了,这里最重要的是drawArc,用于绘制及角度圆,像下图这样,画了4/1的进度,同时增加是否渐变的设置,这里的开始角度是动态的。


在这里插入图片描述


五、API方法


  还需要提供一些方法在代码中调用,下面是这些方法的代码:

    /**
* 设置角度
* @param angle 角度
*/
public void setCustomAngle(int angle) {
if (angle >= 0 && angle < 90) {
mStartAngle = 0;
} else if (angle >= 90 && angle < 180) {
mStartAngle = 90;
} else if (angle >= 180 && angle < 270) {
mStartAngle = 180;
} else if (angle >= 270 && angle < 360) {
mStartAngle = 270;
} else if (angle >= 360) {
mStartAngle = 0;
}
invalidate();
}

/**
* 设置是否渐变
*/
public void setGradient(boolean gradient) {
isGradient = gradient;
invalidate();
}

/**
* 设置渐变的颜色
*/
public void setColorArray(int[] colorArr) {
if (colorArr == null) return;
colorArray = colorArr;
}

/**
* 设置当前进度
*/
public void setProgress(float progress) {
if (progress < 0) {
throw new IllegalArgumentException("Progress value can not be less than 0");
}
if (progress > mMaxProgress) {
progress = mMaxProgress;
}
mCurrentProgress = progress;
mCurrentAngle = 360 * (mCurrentProgress / mMaxProgress);
setAnimator(mStartAngle, mCurrentAngle);
}

/**
* 设置动画
*
* @param start 开始位置
* @param target 结束位置
*/
private void setAnimator(float start, float target) {
isAnimation = true;
ValueAnimator animator = ValueAnimator.ofFloat(start, target);
animator.setDuration(mDuration);
animator.setTarget(mCurrentAngle);
//动画更新监听
animator.addUpdateListener(valueAnimator -> {
mCurrentAngle = (float) valueAnimator.getAnimatedValue();
invalidate();
});
animator.start();
}

  那么到此为止这个自定义View就完成了,下面我们可以在PieProgressBarActivity中使用了。


六、使用


   关于使用,我在写这个文章的时候这个自定义View已经加入到仓库中了,可以通过引入依赖的方式,例如在app模块中使用,则打开app模块下的build.gradle,在dependencies{}闭包下添加即可,之后记得要Sync Now

dependencies {
implementation 'io.github.lilongweidev:easyview:1.0.4'
}

   或者你在自己的项目中完成了刚才上述的所有步骤,那么你就不用引入依赖了,直接调用就好了,不过要注意更改对应的包名,否则会爆红的。


  先修改activity_pie_progress_bar.xml的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:gravity="center"
android:orientation="vertical"
tools:context=".used.PieProgressBarActivity">

<com.easy.view.PieProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:customAngle="right"
app:gradient="false"
app:gradientColorArray="@array/color"
app:maxProgress="100"
app:progress="5"
app:progressbarColor="@color/green"
app:radius="80dp" />

<CheckBox
android:id="@+id/cb_gradient"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="是否渐变" />

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始角度:"
android:textColor="@color/black" />

<RadioGroup
android:id="@+id/rg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">

<RadioButton
android:id="@+id/rb_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="0%" />

<RadioButton
android:id="@+id/rb_90"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="90%" />

<RadioButton
android:id="@+id/rb_180"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="180%" />

<RadioButton
android:id="@+id/rb_270"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="270%" />
</RadioGroup>
</LinearLayout>


<Button
android:id="@+id/btn_set_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="随机设置进度" />

<Button
android:id="@+id/btn_set_progress_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="设置0%进度" />

<Button
android:id="@+id/btn_set_progress_100"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="设置100%进度" />
</LinearLayout>

在strings.xml中增加渐变色,代码如下:

    <string-array name="color">
<item>#00FFF7</item>
<item>#FFDD00</item>
<item>#FF0000</item>
</string-array>

首先要注意看是否能够预览,我这里是可以预览的,如下图所示:


在这里插入图片描述


PieProgressBarActivity中使用,如下所示:

public class PieProgressBarActivity extends EasyActivity<ActivityPieProgressBarBinding> {

@SuppressLint("NonConstantResourceId")
@Override
protected void onCreate() {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
//是否渐变
binding.cbGradient.setOnCheckedChangeListener((buttonView, isChecked) -> {
binding.cbGradient.setText(isChecked ? "渐变" : "不渐变");
binding.progress.setGradient(isChecked);
});
//开始角度
binding.rg.setOnCheckedChangeListener((group, checkedId) -> {
int angle = 0;
switch (checkedId) {
case R.id.rb_0:
angle = 0;
break;
case R.id.rb_90:
angle = 90;
break;
case R.id.rb_180:
angle = 180;
break;
case R.id.rb_270:
angle = 270;
break;
}
binding.progress.setCustomAngle(angle);
});
//设置随机进度值
binding.btnSetProgress.setOnClickListener(v -> {
int progress = Math.abs(new Random().nextInt() % 100);
Toast.makeText(this, "" + progress, Toast.LENGTH_SHORT).show();
binding.progress.setProgress(progress);
});
//设置0%进度值
binding.btnSetProgress0.setOnClickListener(v -> binding.progress.setProgress(0));
//设置100%进度值
binding.btnSetProgress100.setOnClickListener(v -> binding.progress.setProgress(100));
}
}

运行效果如下图所示:


在这里插入图片描述


七、源码


如果对你有所帮助的话,不妨 Star 或 Fork,山高水长,后会有期~


源码地址:EasyView


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

Android斩首行动——接口预请求

前言 开发同学应该都很熟悉我们页面的渲染过程一般是从Activity#onCreate开始,再发起网络请求,等请求回调回来后,再基于网络数据渲染页面。可以用下面这幅图来粗略描述这个过程: 可以看到,目标页面渲染完成前必须得等待网络请求,导致渲染速度并没有那么...
继续阅读 »

前言


开发同学应该都很熟悉我们页面的渲染过程一般是从Activity#onCreate开始,再发起网络请求,等请求回调回来后,再基于网络数据渲染页面。可以用下面这幅图来粗略描述这个过程:


image.png


可以看到,目标页面渲染完成前必须得等待网络请求,导致渲染速度并没有那么快。尤其是当网络并不好的时候感受会更加明显。并且,当目标页面是H5页面或者是Flutter页面的时候,因为涉及到H5容器与Flutter容器的创建,白屏时间会更长。


那么有没有可能提前发起请求,来缩短网络请求这一部分的等待时间呢?这就是我们今天要讲的部分,接口预请求。


目标


我们要达到的目标很简单,就是提前异步发起目标页面的网络请求,从而加快目标页面的渲染速度。改善后的过程可以用下图表示:


image.png


并且,我们的预请求能力需要尽量少地侵入业务,与业务解耦,并保证能力的通用性,适用于工程内的任意页面(Android页面、H5页面、Flutter页面)。


方案


整体链路


首先给大家看一下整体链路,具体的细节可以先不用去抠,下面会一一讲到。


image.png


预请求时机


预请求时机一般有三种选择:



  1. 由业务层自行选择时机进行异步预请求

  2. 点击控件时进行异步预请求

  3. 路由最终跳转前进行异步预请求


第1种选择,由业务层自行选择时机进行预请求,需要涉及到业务层的改造,以及对时机合理性的把握。一方面是存在改造成本,另一方面是无法保证业务侧调用时机的合理性。


第2种选择,点击控件时进行预请求。若点击时进行预请求,点击事件监听并不是业务域统一的,无法形成有效封装。并且,若后续路由拦截器修改了参数,或是终止了跳转,这次预请求就失去了意义。


因此这里我们选择第3种,基于统一路由框架,在路由最终跳转前进行预请求。既保证了良好的封装性,也实现了对业务的零侵入,同时也做到了懒请求,即用户必然要发起该请求时才会去预请求。这里需要注意的是必须是在最终跳转前进行预请求,可以理解为是路由的最后一个前置异步拦截器。


预请求规则配置


我们通过本地的json文件(当然,有需要也可以上云通过配置后台下发),对预请求的规则进行配置,并将这份配置在App启动阶段异步读入到内存。后续在路由过程中,只有命中了预请求规则,才能发起预请求。配置demo如下:

{
"routeConfig":{
"scheme://domain/path?param1=true&itemId=123":["prefetchKey"],
"route2":["prefetchKey2"],
"route3":["prefetchKey3","prefetchKey4"]
},
"prefetcher":{
"prefetchKey":{
"prefetchType":"network",
"prefetchInfo":{
"api":"network.api.name",
"apiVersion":"1.0",
"method":"post",
"needLogin":"false",
"showLoginUI":"false",
"params": {
"itemId":"$route.itemId",
"firstTime":"true"
},
"headers": {

},
"prefetchImgInResponse": [
{
"imgUrl":"$data.imgData.img",
"imgWidth":"$data.imgData.imgWidth",
"imgHeight":150
}
]
}
},
"prefetchKey2":{
"prefetchType":"network",
"prefetchInfo":{
"api":"network.api.name2",
"apiVersion":"1.0",
"method":"post",
"needLogin":"false",
"showLoginUI":"false",
"params": {
"itemId":"$route.productId",
"firstTime":"false"
},
"headers": {

}
},
"prefetchKey3":{
"prefetchType":"image",
"prefetchInfo":{
"imgUrl":"$route.imgUrl",
"imgWidth":"$route.imgWidth",
"imgHeight": 150
}
},
"prefetchKey4":{
"prefetchInfo":{}
}
}
}


规则解读




















































参数名描述备注
routeConfig路由配置配置路由到预请求的映射
prefetcher预请求配置记录所有的预请求
prefetchKey预请求的key
prefetchType预请求类型分为network类型与image类型,两种类型所需要的参数不同
prefetchInfo预请求所需要的信息其中value若为route.param格式,那么该值从路由中获取;若为route.param格式,那么该值从路由中获取;若为data.param格式,则从响应数据中获取。
paramsnetwork请求所需要的请求params
headersnetwork请求所需要的请求headers
prefetchImgFromResponse预请求的响应返回后,需要预加载的图片用于需要预加载图片时,无法确定图片url,图片url只能从预请求响应中获取的场景。

举例说明


网络预请求


例如跳转目标页面,它的路由是scheme://domain/path?param1=true&itemId=123


首先我们在跳转路由时,若跳转的路由是这个目标页面,我们就会尝试去发起预请求。根据上面的demo配置文件,它将匹配到prefetchKey这个预请求。


那么我们详细看prefetchKey这个预请求,预请求类型prefetchTypenetwork,是一个网络预请求,prefetchInfo中具备了请求的基本参数(如apiName、apiVersion、method、请求params与请求headers,不同工程不一样,大家可以根据自己的工程项目进行修改)。具体看params中,有一个参数为itemId:$route.itemId。以$route.开头的意思,就是这个value值要从路由中获取,即itemId=123,那么这个值就是123。


图片预请求


在做网络预请求的过程中,我忽然想到图片做预请求也是可以大大提升用户体验的,尤其是当大图片首次下载到内存中渲染需要的时间会比较长。图片预请求分为url已知url未知两种场景,下面各举两个例子。


图片url已知

什么是图片url已知呢?比如我们在首页跳转首页的二级页面时,如果二级页面需要预加载的图片跟首页的某张图是一样的(尺寸可能不同),那么首页跳转路由时我们是能够提前知道这个图片的url的,所以我们看到prefetchKey3中配置了prefetchTypeimage的预请求。image的信息来自于路由参数,需要在跳转时将图片url和宽高作为路由参数之一。


比如scheme://domain/path?imgUrl=${encodeUrl}&imgWidth=200,那么根据配置项,我们将提前将encodeUrl这个图片以宽200,高150的尺寸,加载到内存中去。当目标页面用到这个图片时,将能很快渲染出来。


图片url未知

相反,当跳转目标页面时,目标页面所要加载的图片url没法取到,就对应了图片url未知的场景。


例如闪屏页跳转首页时,如果需要预加载首页顶部的图片,此时闪屏页是无法获取到图片的url的,因为这个图片url是首页接口返回的。这种情况下,我们只能依赖首页的预请求进行。


在demo配置文件中,我们可以看到prefetchImgFromResponse字段。这个字段代表着,当这个预请求响应回来之后,我需要去预请求某张图片。其中,imgUrl$data.param格式,以$data.开头,代表着这份数据是来自于响应数据的。响应数据就是一串json串,可以凭此,索引到预请求响应中图片url的位置,就能实现图片的提前加载了。


至于图片怎么提前加载到内存中,以及真实图片的加载怎么匹配到内存中的图片,这一部分是通过glide已有的preload机制实现的,感兴趣的同学可以去看一下源码了解一下,这里就不展开了。后面讲的预请求的方案细节,都只限于网络请求。


预请求匹配


预请求匹配指的是实际的业务请求怎样与已经执行的预请求匹配上,从而节省请求的空中时间,直接返回预请求的结果。


首先网络预请求执行前先在内存中生成一份PrefetchRecord,代表着已经执行的预请求,其中的字段跟配置文件中差不多,主要就是记录预请求相关的信息:

class PrefetchRecord {
// 请求信息
String api;
String apiVersion;
String method;
String needLogin;
String showLoginUI;
JSONObject params;
JSONObject headers;

// 预请求状态
int status;
// 预请求结果
ResponseModel response;
// 生成的请求id
String requestId;

boolean isMatch(RealRequest realRequest) {
requestId.equals(realRequest.requestId)
}
}

每一个PrefetchRecord生成时,都会生成一个requestId,用于跟实际业务请求进行匹配。requestId的生成规则可以自行制定,比如将所有请求信息包一起做一下md5处理之类。


在实际业务请求发起之前,也会根据同样的规则生成requestId。若内存中存在相同requestId对应的PrefetchRecord,那么就相当于匹配成功了。匹配成功后,再根据预请求的状态进行进一步的处理。


预请求状态


预请求状态分为START、FINISH、ABORT,对应“正在发起预请求”、“已经获得预请求结果”、“预请求被抛弃”。ABORT状态下一节再讲。


为什么要记录这个状态呢?因为我们无法保证,预请求的响应一定在实际请求之前。用图来表示:


image.png


因为预请求是一个并发行为。当预请求的空中时间特别长,长到目标页面已经发出实际请求了,预请求的响应还没回来,即预请求状态为START,而非FINISH。那么此时该怎么办?我们就需要让实际请求在一旁等着(记录到内存中,RealRequestRecord),等预请求接收到响应了,再根据requestId去进行匹配,匹配到RealRequestRecord了,就触发RealRequestRecord中的回调,返回数据。


另外,在匹配过程中需要注意一点,因为每次路由跳转,如果发起预请求了,总会生成一个Record在内存中等待匹配。因此在匹配结束后,不管是匹配成功还是匹配失败,都要及时释放将Record从内存中释放掉。


超时重试机制


基于实际请求等待预请求响应的场景,我们再延伸一下。若预请求请求超时,迟迟拿不到响应,该怎么办?用图表示:


image.png


假设目前的网络请求,端上默认的超时时间是30s。那么在超时场景下,实际的业务请求在30s内若拿不到预请求的结果,就需要重新发起业务请求,抛弃预请求,并将预请求的状态置为ABORT,这样即使后面预请求响应回来了也不做任何处理。


image.png


忽然想到一个很贴切的场景来比喻这个预请求方案。


我们把跳转页面理解为去柜台取餐。


预请求代表着我们人还没到柜台,就先远程下单让柜员去准备食物。


如果柜员准备得比较快,那么我们到柜台后就能直接把食物拿走了,就能快点吃上了(代表着页面渲染速度变快)。


如果柜员准备得比较慢,那么我们到柜台后还是得等一会儿才能取餐,但总体上吃上食物的速度还是要比到柜台后再点餐来得快。


但如果这个柜员消极怠工准备得太慢了,我们到柜台等了很久都没拿到食物,那么我们就只能换个柜员重新点了(超时后发起实际的业务请求),同时还不忘投诉一把(预请求空中时间太慢了)。


总结


通过这篇文章,我们知道了什么是接口预请求,怎么实现接口预请求。我们通过配置文件+统一路由处理+预请求发起、匹配、回调,实现了与业务解耦的,可适用于任意页面的轻量级预请求方案,从而提升页面的渲染速度。


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

Android:自定义View实现签名带笔锋效果

自定义签名工具相信大家都轻车熟路,通过监听屏幕onTouchEvent事件,分别在按下(ACTION_DOWN)、抬起(ACTION_UP)、移动(ACTION_MOVE) 动作中处理触碰点的收集和绘制,很容易就达到了手签笔的效果。其中无非又涉及到线条的撤回、...
继续阅读 »

自定义签名工具相信大家都轻车熟路,通过监听屏幕onTouchEvent事件,分别在按下(ACTION_DOWN)抬起(ACTION_UP)移动(ACTION_MOVE) 动作中处理触碰点的收集和绘制,很容易就达到了手签笔的效果。其中无非又涉及到线条的撤回、取消、清除、画笔的粗细,也就是对收集的点集合和线集合的增删操作以及画笔颜色宽的的更改。这些功能都在 实现一个自定义有限制区域的图例(角度自识别)涂鸦工具类(上) 中介绍过。


但就在前不久遇到一个需求是要求手签笔能够和咱们使用钢笔签名类似的效果,当然这个功能目前是有一些公司有成熟的SDK的,但我们的需求是要不借助SDK,自己实现笔锋效果。那么,如何使画笔带笔锋呢?废话不多说,先上效果图:


image.png


要实现笔锋效果我们需要考虑几个因素:笔速笔宽按压力度(针对手写笔)。因为在onTouchEvent回调的次数是不变的,一旦笔速变快两点之间距离就被拉长。此时的笔宽不能保持在上一笔的宽度,需要我们通过计算插入新的点,同时计算出对应点的宽度。同理当我们笔速慢的时候,需要通过计算删除信息相近的点。要想笔锋自然,当然贝塞尔曲线是必不可少的。


这里我们暂时没有将笔的按压值作为笔宽的计算,仅仅通过笔速来计算笔宽。

/**
* 计算新的宽度信息
*/
public double calcNewWidth(double curVel, double lastVel,double factor) {
double calVel = curVel * 0.6 + lastVel * (1 - 0.6);
double vfac = Math.log(factor * 2.0f) * (-calVel);
double calWidth = mBaseWidth * Math.exp(vfac);
return calWidth;
}

/**
* 获取点信息
*/
public ControllerPoint getPoint(double t) {
float x = (float) getX(t);
float y = (float) getY(t);
float w = (float) getW(t);
ControllerPoint point = new ControllerPoint();
point.set(x, y, w);
return point;
}

/**
* 三阶曲线的控制点
*/
private double getValue(double p0, double p1, double p2, double t) {
double a = p2 - 2 * p1 + p0;
double b = 2 * (p1 - p0);
double c = p0;
return a * t * t + b * t + c;
}

最后也是最关键的地方,不再使用drawLine方式画线,而是通过drawOval方式画椭圆。通过前后两点计算出椭圆的四个点,通过笔宽计算出绘制椭圆的个数并加入椭圆集。最后在onDraw方法中绘制。

/**
* 两点之间将视图收集的点转为椭圆矩阵 实现笔锋效果
*/
public static ArrayList<SvgPointBean> twoPointsTransRectF(double x0, double y0, double w0, double x1, double y1, double w1, float paintWidth, int color) {

ArrayList<SvgPointBean> list = new ArrayList<>();
//求两个数字的平方根 x的平方+y的平方在开方记得X的平方+y的平方=1,这就是一个园
double curDis = Math.hypot(x0 - x1, y0 - y1);
int steps;
//绘制的笔的宽度是多少,绘制多少个椭圆
if (paintWidth < 6) {
steps = 1 + (int) (curDis / 2);
} else if (paintWidth > 60) {
steps = 1 + (int) (curDis / 4);
} else {
steps = 1 + (int) (curDis / 3);
}
double deltaX = (x1 - x0) / steps;
double deltaY = (y1 - y0) / steps;
double deltaW = (w1 - w0) / steps;
double x = x0;
double y = y0;
double w = w0;

for (int i = 0; i < steps; i++) {
RectF oval = new RectF();
float top = (float) (y - w / 2.0f);
float left = (float) (x - w / 4.0f);
float right = (float) (x + w / 4.0f);
float bottom = (float) (y + w / 2.0f);
oval.set(left, top, right, bottom);
//收集椭圆矩阵信息
list.add(new SvgPointBean(oval, color));
x += deltaX;
y += deltaY;
w += deltaW;
}

return list;
}

至此一个简单的带笔锋的手写签名就实现了。 最后附上参考链接Github.


我是一个喜爱Jay、Vae的安卓开发者,喜欢结交五湖四海的兄弟姐妹,欢迎大家到沸点来点歌!


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

有多少人忘记了gb2312

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。 本想新周摸新鱼,却是早早入坑。看到群友千元求解一个叫当当网的索引瞬间来了兴趣 网站地址,大体一看没什么特别的地方就是一个关键字编码问题,打眼一看url编码没跑直接拿去解码无果 -有点惊讶看似url...
继续阅读 »

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
本想新周摸新鱼,却是早早入坑。看到群友千元求解一个叫当当网的索引瞬间来了兴趣



  1. 网站地址,大体一看没什么特别的地方就是一个关键字编码问题,打眼一看url编码没跑直接拿去解码无果


image.png


image.png
-有点惊讶看似url编码实则url编码只是这,滋滋滋...
VeryCapture_20230227174156.gif


有点东西,开始抓包,断点,追踪的逆向之路
VeryCapture_20230227170913.gif
2. 发现是ajax加载(不简单呀纯纯的吊胃口)先来一波关键字索引(keyword)等一系列基操轻而易举的找到了他
VeryCapture_20230227171325.gif


从此开始走向了一条不归路,经过一上午的时间啥也没追到,午休之后继续战斗,经过了一两个半小时+三支长白山牌香烟的努力终于


VeryCapture_20230227173627.gif
VeryCapture_20230227173457.gif


VeryCapture_20230227171715.gif

cihui = '哈哈哈'
js = open("./RSAAA.js", "r", encoding="gbk", errors='ignore')
line = js.readline()
htmlstr = ''
while line:
htmlstr = htmlstr + line
line = js.readline()
ctx = execjs.compile(htmlstr)
result = ctx.call('invokeServer', cihui)
print(result)
const jsdom = require("jsdom");
const {JSDOM} = jsdom;
const dom = new JSDOM('<head>\n' +
' <base href="//search.dangdang.com/Standard/Search/Extend/hosts/">\n' +
'<link rel="dns-prefetch" href="//search.dangdang.com">\n' +
'<link rel="dns-prefetch" href="//img4.ddimg.cn">\n' +
'<title>王子-当当网</title>\n' +
'<meta http-equiv="Content-Type" content="text/html; charset=GB2312">\n' +
'<meta name="description" content="当当网在线销售王子等商品,并为您购买王子等商品提供品牌、价格、图片、评论、促销等选购信息">\n' +
'<meta name="keywords" content="王子">\n' +
'<meta name="ddclick_ab" content="ver:429">\n' +
'<meta name="ddclick_search" content="key:王子|cat:|session_id:0b69f35cb6b9ca3e7dee9e1e9855ff7d|ab_ver:G|qinfo:119800_1_60|pinfo:_1_60">\n' +
'<link rel="canonical" href="//search.dangdang.com/?key=%CD%F5%D7%D3\&amp;act=input">\n' +
' <link rel="stylesheet" type="text/css" href="css/theme_1.css">\n' +
' <!--<link rel="Stylesheet" type="text/css" href="css/model/home.css" />-->\n' +
' <link rel="stylesheet" type="text/css" href="css/model/search_pub.css?20211117"> \n' +
'<style>.shop_button {height: 0px;}.children_bg01 a {\n' +
'margin-left: 0px;\n' +
'padding-left: 304px;\n' +
'width: 630px;\n' +
'}\n' +
'.children_bg02 a {\n' +
'margin-left: 0px;\n' +
'padding-left: 304px;\n' +
'width: 660px;\n' +
'}\n' +
'.children_bg03 a {\n' +
'margin-left: 0px;\n' +
'padding-left: 304px;\n' +
'width: 660px;\n' +
'}\n' +
'.narrow_page .children_bg01 a{\n' +
'width: 450px;\n' +
'}\n' +
'.narrow_page .children_bg02 a{\n' +
'width: 450px;\n' +
'}\n' +
'.narrow_page .children_bg03 a{\n' +
'width: 450px;\n' +
'}.price .search_e_price span {font-size: 12px;font-family: 微软雅黑;display: inline-block;background-color: #739cde;color: white;padding: 2px 3px;line-height: 12px;border-radius: 2px;margin: 0 4px 0 5px;}\n' +
'.price .search_e_price:hover {text-decoration: none;}</style> <link rel="stylesheet" href="http://product.dangdang.com/js/lib/layer/3.0.3/skin/default/layer.css?v=3.0.3.3303" id="layuicss-skinlayercss"><script id="temp_script" type="text/javascript" src="//schprompt.dangdang.com/suggest_new.php?keyword=好好&amp;pid=20230227105316030114015279129895799&amp;hw=1&amp;hwps=12&amp;catalog=&amp;guanid=&amp;0.918631418357919"></script><script id="json_script" type="text/javascript" src="//static.dangdang.com/js/header2012/categorydata_new.js?20211105"></script></head>');

window = dom.window;
document = window.document;
function invokeServer(url) {

var scriptOld = document.getElementById('temp_script');
if(scriptOld!=null && document.all)
{
scriptOld.src = url;
return script;
}
var head=document.documentElement.firstChild,script=document.createElement('script');
script.id='temp_script';
script.type = 'text/javascript';
script.src = url;
if(scriptOld!=null)
head.replaceChild(script,scriptOld);
else
head.appendChild(script);
return script
}



  1. 完事!当我以为都要结束了的时候恍惚直接看到了源码中的gb2312突然想起了之前做的一个萍乡房产网的网站有过类似经历赶快去尝试结果我**
    image.png
    image.png
    VeryCapture_20230227172815.gif




  2. 总结:提醒各位大佬在逆向之路中还是要先从基操开始,没必要一味的去搞攻克扒源码,当然还是要掌握相对全面的内容,其实除了个别大厂有些用些贵的东西据说某数5要20个W随着普遍某数不知道那些用了20w某数的大厂心里是什么感觉或许并不在乎这点零头哈哈毕竟是大厂,小网站的反扒手段并不是很难,俗话说条条大道通北京。


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

我,不是实习生

啊,一想我读书十几年,现在终将要脱离学校奔赴社会了。依照我这能力,我这性子,学术型人才是走不动了。 我知道,不继续深造,就要去工作赚钱。 可我一晃这几年,感觉啥也没学着。圈养在学校,老师只负责教授我课本知识,定向培养我成为一名合格的打工人。 经历面试才知道,市...
继续阅读 »

啊,一想我读书十几年,现在终将要脱离学校奔赴社会了。依照我这能力,我这性子,学术型人才是走不动了。


我知道,不继续深造,就要去工作赚钱。


可我一晃这几年,感觉啥也没学着。圈养在学校,老师只负责教授我课本知识,定向培养我成为一名合格的打工人。


经历面试才知道,市场的速度是光速,学校里是声速。企业技术走得这么前沿,我学的东西讲出来都有些羞涩。


技术和知识面落后这么多,要这么短的时间紧跟市场速度,且找到一份合适的工作,属实不容易。


没辙,遵循市场用人规则,我连夜整理面试知识和制作简历,并效仿当年八股文进士。


可一工作才发现,没完没了的工作,没完没了的 OKR,大家都没完没了,可是整体盈利又不怎么样。


作为一名实习生,实在是搞不通,谁顾着谁,谁又惧谁。我知道我会好好工作,内心还怀揣着抱负和理想,一心扑到工作上证明我自己。


电话里头我也是这么对父母说。


可是,几千块钱的薪资,我恍然不已,原来劳动用工成本可以这么低,我每天顾着怎么开销,怎么减少活动,怎么存钱,家里急用钱怎么办。


另一方面,我通过努力证明自己的实力,可以升职加薪,可是一冷静下来,怎么加班都能联想到旁边工作十几年经验老油条的现状。


联想周边企业刚加入的年轻生命,读了十几年书刚开始还没来得及做好准备,就结束在了加班不眠之夜里。


不禁在想,身边不乏努力的人,可越是努力劳动力越廉价,资源也就这么多。这些努力的成果到底在哪里,它会和我们的理想化文明向前推进挂钩吗,还是像小白鼠跑转轮一样。只要你在忙,在跑着就会有食物,仅此而已。


当我不断工作不断思考之后,我决定把一部分精力从工作拆分开来,用来做自己的事情并且赚钱的时候。


才发现,我已经离罗马中心十八里开外了。那些早就明白规则的人已经身价 A7+ 了。而我,还只是一个职场人,思想和个性逐渐被磨平的人。


好不容易开始有自主赚钱的觉悟,却又不知道从哪里开始。


眼见互联网的风吹草动,打算沾点风头做些衍生产品赚些小钱,可曾想相关情况一调查,各种衍生产品已经多如牛毛,自己的想法刚萌生就已经望尘莫及。


看来这个新时代,已经拼的主要不是个人努力,而是感知能力,谁的感知能力强,捕捉到风口和需求,谁就能够抢占先机,落后的残羹冷炙都吃不上。


回想起我同届的校友,几个校友的案例历历在目,他们在校的时候就喜欢自己捣鼓生意,周边能赚钱的项目都试了个遍,租摊做外卖、合租奶茶店、做中介介绍学生工、婚礼现场布置等等,这些都是他们课外喜欢动手做的事情,大二开始就已经有很强的人脉关系和团队,学长学姐老师都能够拉拢合作。


我曾好奇问过他,你为啥经常跑这跑那,经常上课缺席?


他告诉我,我不太喜欢课本上的东西,我喜欢捣鼓些小玩意,以后毕业自己能够做些小生意就够了。


当时我的角度跟他截然相反,上课认真听讲,班务事情积极做,国家和学校的奖学金我都拿了,但对于工作前景就是当一名程序员就行。


可以明说当时心态上有些看不起他们经常翘课,出去捣鼓小钱的行为。


事实上,是多么可笑的。确实是换了几家企业的程序员,每天殚精竭虑花在工作上,上面指哪我就打哪,固定薪资,每时每刻接收就业差,企业裁员情报,人人自危陷入焦虑和恐慌,学生时代的傲骨早就被企业文化磨平。


而那些从学生时代喜欢捣鼓生意,爱动手赚钱的人,就我所知道那些同届的校友。他们的店铺已经开连锁店了,做学生和年轻人的生意。有的已经赚国外的钱了。



▲图/ 学长已经开起了分店


这一类人,我身边认识和知道的没有一个过得不好的,他们善于利用信息和售卖信息。教你开店,拍视频的课程理论上都是售卖信息行为。


这一对比,仿佛他们才是懂社会规则的人,像是弯道超车般的越过了规则到了另外一层,他们的精力花在了资本运转身上,只要有人有需求就有机会。对于裁员、跳槽、就业,是打工人该担心的事情。


才明白,学校所教授的知识和培养的素质,大多都是培养我们成为一名工人具备的思维和能力,毕竟经济的推动和国家的发展,需要具备大量的工人。至于效果和进阶,那就让企业、社会来教你。


出来实习后,才知道自己有多么被动。跟不上社会的步伐,欠缺了多少实用的工具和能力。


被欺负了如何拿起法律的武器保护自己;找不到工作或失业如何自主赚钱;如何懂理财懂投资;脱离一定条件如何生存和陷入危险如何自救...


这些,长达十几年的学生时代里,没有专门的课程或者相关老师教授。


我所欠缺的这些知识,是屡次碰壁之后幡然醒悟,才有所接触这些内容,但此时已经千疮百孔,伤痕累累。与此同时,总能遇见大批初入社会的人依然走自己走过的路,叫,是叫不醒。在世界观和认知能力闭合之后,总是需要事教人的地步才能打开。因为,好言相劝已经不管用。



象牙塔里待了几年,一张白纸怎么渲染都好渲染,原本与人为善,感恩戴德,分清对与错,拥有理想与信念。


开始接轨社会之后,持着心善的态度连打数张好人牌,被坑蒙拐骗殆尽,才醒悟这些品质都是别人敛财的工具,得到教训之后还得分清谁值得给好人牌,谁永远坏人牌。


什么又是对与错,无人问津的人说什么话都只是一句话。反而屏幕出现权势,坐拥资源的人说的话有多不经推敲都觉得是对的,因为它是成功人士。


什么是理想,什么又是信念。去一趟公司吧,让你感受一番企业文化之后,再说你的理想是什么,有什么信念。


......


作为一名实习生,意味着即将进入社会,和不同人打交道了。或许你的内心秉持着工作的想法,又或许秉持着自己的热爱和目标。


工作从来也都不是一件轻松的事情,至少最近的环境里是这样。


工作或许能够加速让你融入社会,但同时加速你的痛苦,因为这是一个“丛林世界”,你没对错可言,也没有更多选择,或许看似有选择其实也只是一个看起来更大稍微舒适点的“囚牢”。


并且,工作本身难找的不是工作,而是放缓不了自己的心态,一头扎进内心向往的“高薪”,“体面”,“舒适” 出不来。人人向往这片区域,也就只有这么点区域,总有人失落且焦虑。


然而,大多不被看好的行业或被嫌弃的工作,往往能够带来与之热门职业持平的回报。网络出现的“北大毕业卖猪肉”,“高材生当保姆”的等被大众关注的案例屡见不鲜,甚至嗤之以鼻予以嘲讽。


殊不知“笑贫不笑娼”的社会环境,他们才是能屈能伸的强者。这些人和我那些校友有着同样的能力。动手能力强,生存能力强,就算被限制条件,也能够屈伸过的好。


职业并无贵贱区分,能掌握活法才是本质。


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