注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

iOS16 中的 3 种新字体宽度样式

iOS
前言 在 iOS 16 中,Apple 引入了三种新的宽度样式字体到 SF 字体库。CompressedCondensedExpend UIFont.Width Apple 引入了新的结构体 UIFont.Width,这代表了一种新的宽度样式。 目前已有的四...
继续阅读 »

前言


在 iOS 16 中,Apple 引入了三种新的宽度样式字体到 SF 字体库。

  1. Compressed

  2. Condensed

  3. Expend



UIFont.Width


Apple 引入了新的结构体 UIFont.Width,这代表了一种新的宽度样式。


目前已有的四种样式。

  • standard:我们总是使用的默认宽度。

  • compressed:最窄的宽度样式。

  • condensed:介于压缩和标准之间的宽度样式。

  • expanded:最宽的宽度样式。



SF 字体和新的宽度样式


如何将 SF 字体和新的宽度样式一起使用


为了使用新的宽度样式,Apple 有一个新的 UIFont 的类方法来接收新的 UIFont.Width

class UIFont : NSObject {
class func systemFont(
ofSize fontSize: CGFloat,
weight: UIFont.Weight,
width: UIFont.Width
) -> UIFont
}

你可以像平常创建字体那样来使用新的方法。

let condensed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .condensed)
let compressed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .compressed)
let standard = UIFont.systemFont(ofSize: 46, weight: .bold, width: .standard)
let expanded = UIFont.systemFont(ofSize: 46, weight: .bold, width: .expanded)

SwiftUI



更新:在 Xcode 14.1 中,SwiftUI 提供了两个新的 API 设置这种新的宽度样式。
width(_:)fontWidth(_:)



目前(Xcode 16 beta 6),这种新的宽度样式和初始值设定只能在 UIKit 中使用,幸运的是,我们可以在 SwiftUI 中轻松的使用它。


有很多种方法可以将 UIKit 集成到 SwiftUI 。我将会展示在 SwiftUI 中使用新宽度样式的两种方法。

  1. 将 UIfont 转为 Font。
  2. 创建 Font 扩展。

将 UIfont 转为 Font


我们从 在 SwiftUI 中如何将 UIFont 转换为 Font 中了解到,Font 有初始化方法可以接收 UIFont 作为参数。


步骤如下

  1. 你需要创建一个带有新宽度样式的 UIFont。
  2. 使用该 UIFont 创建一个 Font 。
  3. 然后像普通 Font 一样使用它们。
struct NewFontExample: View {
// 1
let condensed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .condensed)
let compressed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .compressed)
let standard = UIFont.systemFont(ofSize: 46, weight: .bold, width: .standard)
let expanded = UIFont.systemFont(ofSize: 46, weight: .bold, width: .expanded)

var body: some View {
VStack {
// 2
Text("Compressed")
.font(Font(compressed))
Text("Condensed")
.font(Font(condensed))
Text("Standard")
.font(Font(standard))
Text("Expanded")
.font(Font(expanded))
}
}
}

  • 创建带有新宽度样式的 UIFont。
  • 用 UIFont 初始化 Font, 然后传递给 .font 修改。

创建一个 Font 扩展


这种方法实际上和将 UIfont 转为 Font 是同一种方法。我们只需要创建一个新的 Font 扩展在 SwiftUI 中使用起来更容易一些。

extension Font {
public static func system(
size: CGFloat,
weight: UIFont.Weight,
width: UIFont.Width) -> Font
{
// 1
return Font(
UIFont.systemFont(
ofSize: size,
weight: weight,
width: width)
)
}
}

创建一个静态函数传递 UIFont 需要的参数。然后,初始化 UIFont 和创建 Font


我们就可以像这样使用了。

Text("Compressed")
.font(.system(size: 46, weight: .bold, width: .compressed))
Text("Condensed")
.font(.system(size: 46, weight: .bold, width: .condensed))
Text("Standard")
.font(.system(size: 46, weight: .bold, width: .standard))
Text("Expanded")
.font(.system(size: 46, weight: .bold, width: .expanded))

如何使用新的宽度样式


你可以在你想使用的任何地方使用。不会有任何限制,所有的新宽度都有一样的尺寸,同样的高度,只会有宽度的变化。


这里是拥有同样文本,同样字体大小和同样字体样式的不同字体宽度样式展示。



新的宽度样式优点


你可以使用新的宽度样式在已经存在的字体样式上,比如 thin 或者 bold ,在你的 app 上创造出独一无二的体验。


Apple 将它使用在他们的照片app ,在 "回忆'' 功能中,通过组合不同的字体宽度和样式在标题或者子标题上。



这里有一些不同宽度和样式的字体组合,希望可以激发你的灵感。

Text("Pet Friends")
.font(Font(UIFont.systemFont(ofSize: 46, weight: .light, width: .expanded)))
Text("OVER THE YEARS")
.font(Font(UIFont.systemFont(ofSize: 30, weight: .thin, width: .compressed)))

Text("Pet Friends")
.font(Font(UIFont.systemFont(ofSize: 46, weight: .black, width: .condensed)))
Text("OVER THE YEARS")
.font(Font(UIFont.systemFont(ofSize: 20, weight: .light, width: .expanded)))


你也可以用新的宽度样式来控制文本的可读性。


下面的这个例子,说明不同宽度样式如何影响每行的字符数和段落长度



下载这种字体


你可以在 Apple 字体平台 来下载这种新的字体宽度样式。


下载安装后,你将会发现一种结合了现有宽度和新宽度样式的新样式。




基本上,除了在模拟器的模拟系统 UI 中,在任何地方都被禁止使用 SF 字体。请确保你在使用前阅读并理解许可证。


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

Go 负责人说以后不会有 Go2 了

大家好,我是煎鱼。 最近 Go 核心团队负责人 @Russ Cox(下称:rsc)专门写了一篇文章《Backward Compatibility, Go 1.21, and Go 2》为 Go 这门编程语言的 Go1 兼容性增强和 Go2 的情况说明做诠释和宣...
继续阅读 »

大家好,我是煎鱼。


最近 Go 核心团队负责人 @Russ Cox(下称:rsc)专门写了一篇文章《Backward Compatibility, Go 1.21, and Go 2》为 Go 这门编程语言的 Go1 兼容性增强和 Go2 的情况说明做诠释和宣传。


今天希望能够帮助你获悉 Go 未来的规划、方向以及 rsc 的思考。


Go1 破坏兼容性的往事


新增结构体字段


第一个案例,比较经典。在 Go1 的时候,这段代码是可以正常运行的。如下演示代码:

// 脑子进煎鱼了
package main

import "net"

var myAddr = &net.TCPAddr{
net.IPv4(18, 26, 4, 9),
80,
}

但在 Go1.1,这段代码就跑不起来。必须要改成如下代码:

var myAddr = &net.TCPAddr{
IP: net.IPv4(18, 26, 4, 9),
Port: 80,
}

因为在当时的新版本中,对 net.TCPAddr 新增了 Zone 字段。原先的未声明值对应字段的方式就会出现一些问题。


后续在新版本的规范中,官方直接对标准库提交的代码增加了要求,赋值时必须声明字段名。以此避免该问题的产生。


改进排序/压缩的算法实现


第二个案例,Go1.6 时,官方修改了 Sort 的排序实现,使得运行速度提高了 10% 左右。以下是演示代码,将根据名称长度对颜色列表进行排序并输出结果:

colors := strings.Fields(
`black white red orange yellow green blue indigo violet`)
sort.Sort(ByLen(colors))
fmt.Println(colors)

一切听起来是那么的美好。


真实世界是改变排序算法通常会改变相等元素的排序方式。导致了 Go1.5 和 Go1.6 所输出的结果不一致:

Go 1.5:  [red blue green white black yellow orange indigo violet]
Go 1.6: [red blue white green black orange yellow indigo violet]

按照顺序排序后,结果集的差异点在于:


  • Go1.5 返回 green, white, black。
  • Go1.6 返回 white, green, black。

如果说程序依赖了结果集的输出顺序,这将是一个影响不小的兼容性破坏。


第三个案例,类似的还有在 Go1.8 中,官方改进了 compress/flate 的算法,达到了在 CPU 和 Memory 没有什么明显变化下,压缩后的结果集更小了。听起来是个很好的成果。


但实际上自己内部却翻车了,因为 Google 内部有一个需要可重现归档构建的项目,依赖了原有的算法。最后自己 fork 了一份来解决。


Go1.21 起增强兼容性(GODEBUG)


从上面的部分破坏兼容性示例来看,可以知道 Go 官方也不是刻意破坏的。但又存在必然要修改的各种原因和考量。


为此在 Go1.21 起,正式输出了 GODEBUG 的机制,相当于是开了个官方 “后门” 了。将其作为破坏性变更后的门把手。


允许设置 GODEBUG,来开关新功能特性。例如以下选项:

  • GODEBUG=asyncpreemptoff=1:禁用基于信号的 Goroutine 抢占,这偶尔会发现操作系统的错误。
  • GODEBUG=cgocheck=0:禁用运行时的 CGO 指针检查。
  • GODEBUG=cpu.<extension>=off:在运行时禁止使用某个特定的 CPU 扩展。

也会根据根据 go.mod 中的 Go 版本号来设置对应 GODEBUG,以提供版本所约定的 Go1 兼容性保障策略。


如果对这块感兴趣,可以查看《加大力度!Go 将会增强 Go1 向后兼容性》,有完整的增强兼容性的规范说明。


Go2 的情况和规划


Go 官方(via @rsc)正式回答了之前画的饼,也就是什么时候可以看到 Go2 的规范推出,打破 Go1 程序?


答案是永远不会。从与过去决裂、不再编译旧程序的意义上来说,Go 2 永远不会出现。从 Go 在 2017 年开始对 Go 1 进行重大修订的意义上来说,Go 2 已经发生了。


简而言之,透露出来的意思是:硬要说的话,Go2 已经套壳 Go1 上市了。


在未来规划上,不会出现破坏 Go1 程序的 Go2。工作方向会往将加倍努力保证兼容性的基础上,开展新的新工作。


总结


整体上 rsc 对破坏 Go1 兼容性做了很长时间规划的回溯和规划,释出了一大堆手段,例如:GODEBUG、go.mod 版本约束等。


从而引导了 Go2 直接可以借壳上的方向,也更好兑现了 Go1 兼容性保障的规范承诺。单从这方面来讲,还是非常的深思熟虑的。


也可能会有同学说,看 Go 现在这样,说不定下次就变了。这可能比较难,其实 rsc 才上任做团队负责人没几年,工作履历上和其他几位骨干大佬在 Google 已经有非常长年的在职经验了。



我目测一时半会是不会变的了。


想变,得等 Go 核心团队这一班子换了才有可能了。阻力也会很多,因为社区人多,一般会比较注重规范。



文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blo… 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。



Go 图书系列


推荐阅读


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

一个上午,我明白了,为什么总说人挣不到认知以外的钱

你好,我是刘卡卡,94年的大厂奶爸程序员,探索副业中 01 接下来,以我昨天上午的一段经历,讲述下为什么说我挣不到认知以外的钱 在昨天上班的路上,我在微信的订阅号推荐里面看到了下图 当时我的想法是:这东西阅读量好高噢,不过养老金和目前的我没什么关系。于是,我...
继续阅读 »

你好,我是刘卡卡,94年的大厂奶爸程序员,探索副业中


01


接下来,以我昨天上午的一段经历,讲述下为什么说我挣不到认知以外的钱


在昨天上班的路上,我在微信的订阅号推荐里面看到了下图



当时我的想法是:这东西阅读量好高噢,不过养老金和目前的我没什么关系。于是,我就划过去了。



(读者可以先停5s 思考下,假设是你看到这张图,会有什么想法)



02


当我坐上工位后,我看到我参加的社群里也人有发了上图,并附上了一段文字:


“养老金类型的公众号容易出爆文。


小白玩转职场这个号,篇篇10w+,而且这并不是一个做了很久的老号,而是今年5月才注册不久的号。 之前这个号刚做的时候是往职场方向发展,所以取名叫小白玩转职场,但是发了一阵后数据不是很好于是就换风格做了养老金的内容。


换到养老金赛道后就几乎篇篇10w+。 这些内容一般从官方网站找就好,选一些内容再加上自己想法稍微改下,或者直接通过Chatgpt辅助,写好标题就行”。


同时,文字下面有社群圈友留下评论说:“这是个好方向啊,虽然公众号文章已经同质化很严重了,但可以往视频号、带货等方向发展”。



读者可以先停5s 思考下,假设是你看到这段文字,会有什么想法。如果你不知道公众号赚钱的模式,那你大概率看不出这段话中的赚钱信息的



我想了想,对噢,确实可以挣到钱,于是将这则信息发到了程序员副业交流的微信群里。



然后,就有群友在交流:“他这是转载还是什么,不可能自己天天写吧”,“这种怎么冷启动呢,不会全靠搜索吧,“做他这种类型的公众号挺多吧,怎么做到每篇10w的”



有没有发现,这3个问题都是关注的怎么做的问题?怎么写的,怎么启动的,怎么每篇10w。


这就是我们大部分人的认知习惯,看到一个信息或别人赚钱的点子后,我们大部分人习惯去思考别人是如何做到的,是不是真的,自己可不可以做到。


可一般来说,我们当下的认知是有限的,大概率是想不出完整的答案的的,想不出来以后,然后就会觉得这个事情有点假,很难或者不适合。从而就错过这条信息了。



我当时觉得就觉得可能大部分群友会错过这则信息了,于是,在群里发了下面这段话


“分享一个点子后


首先去看下点子背后的商业变现机会,如带货/流量主/涨粉/等一系列


而后,才去考虑执行的问题,执行的话


1、首先肯定要对公众号流量主的项目流程进行熟悉


2、对标模仿,可以去把这个公众号的内容全看一看,看看别人为什么起号


3、做出差异化内容。”


发完后还有点小激动(嗯,我又秀了波自己的认知)。可到中午饭点,我发现我还是的认知太低了。


03


在中午吃饭时,我看到亦仁(生财有术的老大)发了这段内容



我被这段话震撼到了,我发现我现在的思考习惯,还是只停留在最表面的看山是山的地步。


我仅仅看到了这张图流量大,没有去思考它为什么这么大流量?



因为他通过精心制作的文章,为老年用户介绍了养老金的方方面面,所以,才会有流量


简单说,因为他满足了用户的需求



同样,我也没有思考还有没有其他产品可以满足用户的这个需求,我仅仅是停留在了视频号和公众号这两个产品砂锅。



只要满足用户需求,就不只有一个产品形态,对于养老金这个信息,我们可以做直播,做课程,做工具,做咨询,做1对1私聊的。这么看,就能有无数的可能



同时,我想到了要做差异化,但没有想到要通过关键字挖掘,去挖掘长尾词。



而亦仁,则直接就挖掘了百万关键字,并无偿分享了。



这才知道,什么叫做看山不是山了。


之前知道了要从“需求 流量 营销 变现”的角度去看待信息,也知道“需求为王”的概念。


可我看到这则信息时,还是没有考虑到需求这一层,更没有形成完整的闭环思路。


因此,以后要在看到这些信息时,去刻意练习“需求 流量 营销 变现”这个武器库,去关注他用什么产品,解决了用户什么需求,从哪里获取到的流量的,怎么做营销的,怎么做变现的。


04


于是,我就把这些思考过程也同样分享到了群里。


接着,下午我就看到有群友在自己的公众号发了篇和养老金相关的文章,虽然文章看上去很粗糙,但至少是起步了。


同时,我也建了个项目交流群,方便感兴趣的小伙伴交流进步(一群人走的更远)


不过我觉得在起步之前,也至少得花一两天时间去调研下,去评估这个需求有哪些地方自己可以切入进去,值不值得切入,能切入的话,怎么切入。


对了,可能你会关心我写了这么多,自己有没有做养老金相关的?


我暂时还没有,因为我目前关心的领域还在出海工具和个人IP上。


全文完结,如果对你有收获的话,关注公众号 刘卡卡 和我一起交流进步


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

不用太深奥简单解决iOS上拉边界下拉白色空白问题

iOS
表现 手指按住屏幕下拉,屏幕顶部会多出一块白色区域。手指按住屏幕上拉,底部多出一块白色区域。 产生原因 在 iOS 中,手指按住屏幕上下拖动,会触发 touchmove 事件。这个事件触发的对象是整个 webview ...
继续阅读 »

表现


手指按住屏幕下拉,屏幕顶部会多出一块白色区域。手指按住屏幕上拉,底部多出一块白色区域。




产生原因


在 iOS 中,手指按住屏幕上下拖动,会触发 touchmove 事件。这个事件触发的对象是整个 webview 容器,容器自然会被拖动,剩下的部分会成空白。




解决方案


1. 监听事件禁止滑动


移动端触摸事件有三个,分别定义为

  1. touchstart :手指放在一个DOM元素上。

  2. touchmove :手指拖曳一个DOM元素。

  3. touchend :手指从一个DOM元素上移开。


显然我们需要控制的是 touchmove 事件,由此我在 W3C 文档中找到了这样一段话


Note that the rate at which the user agent sends touchmove events is implementation-defined, and may depend on hardware capabilities and other implementation details.(注意,用户代理发送touchmove事件的速率是实现定义的,并且可能取决于硬件功能和其他实现细节。)


If the preventDefault method is called on the first touchmove event of an active touch point, it should prevent any default action caused by any touchmove event associated with the same active touch point, such as scrolling.(如果在活动触摸点的第一个touchmove事件上调用preventDefault方法,它应该防止由与同一个活动触摸点关联的任何touchmove事件(如滚动)引起的任何默认操作。)


touchmove 事件的速度是可以实现定义的,取决于硬件性能和其他实现细节


preventDefault 方法,阻止同一触点上所有默认行为,比如滚动。




由此我们找到解决方案,通过监听 touchmove,让需要滑动的地方滑动,不需要滑动的地方禁止滑动。


值得注意的是我们要过滤掉具有滚动容器的元素。


实现如下:

document.body.addEventListener('touchmove', function(e) {
if (e._isScroller) return;
// 阻止默认事件
e.preventDefault();
}, {
passive: false
});

2. 滚动妥协填充空白,装饰成其他功能


在很多时候,我们可以不去解决这个问题,换一直思路。


根据场景,我们可以将下拉作为一个功能性的操作


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

可能是全网第一个适配iOS灵动岛的Toast库-JFPopup

iOS
前言 我去年的一篇文章详细的介绍了我编写的一套Swift弹窗组件库一个优雅的Swift弹窗组件-JFPopup。里面适配了一套ToastView,恰逢今年苹果iPhone14 Pro以上系列新出了一套灵动岛的交互风格,所以就意外想到能否把ToastView也适...
继续阅读 »

前言


我去年的一篇文章详细的介绍了我编写的一套Swift弹窗组件库一个优雅的Swift弹窗组件-JFPopup。里面适配了一套ToastView,恰逢今年苹果iPhone14 Pro以上系列新出了一套灵动岛的交互风格,所以就意外想到能否把ToastView也适配进去灵动岛,所以此文就应运而生。我上篇文章已经很详细的介绍了JFPopup具体用法,这篇文章主要讲解适配灵动岛的心路历程。


具体效果:




用法


虽然我上篇文章已经介绍了一遍,这里我还是再写一下。另外灵动岛Toast默认适配iPhone14 Pro以上机型,无需另外操作,若不是灵动岛机型,则是默认居中,还支持top及bottom。更多详细参数请看一个优雅的Swift弹窗组件-JFPopup


Toast:


//默认仅文案

JFPopupView.popup.toast(hit: "默认toast,支持灵动岛")

//带logo ,内置success or fail

JFPopupView.popup.toast(hit: "支付成功", icon: .success)

JFPopupView.popup.toast(hit: "支付失败", icon: .fail)

//自定义logo

JFPopupView.popup.toast(hit: "自定义", icon: .imageName(name: "face"))


Loading:


DispatchQueue.main.async {

JFPopupView.popup.loading()

}

DispatchQueue.main.asyncAfter(deadline: .now() + 3) {

JFPopupView.popup.hideLoading()

JFPopupView.popup.toast(hit: "刷新成功")

}


适配灵动岛具体过程


由于苹果官方已经说了要在下半年推出的ActivityKit才会加入适配灵动岛的Api。所以目前并没有官方的api可以给我们适配。所以只能硬着头皮自己去思考适配方案了。



- 首先要知道灵动岛的区域大小


我们用最笨的方法,直接给模拟器截个图自己去算大小。至少能还原99%的效果了。如图得知,灵动岛的区域大概是宽120dt,高34dp,那半圆圆角自然为17dt。居顶部大约10dp,以及在屏幕居中。有了这些信息,我们自然就能模拟灵动岛的放大缩小转场效果了。



- ToastView新增灵动岛动画


我们在原先基础上新增灵动岛动画枚举


public enum JFToastPosition {

case center

case top

case bottom

case dynamicIsland //新增灵动岛位置动画

}


重新实现下present 及 dismiss协议的转场动画代码如下


展开:


let originSize = contianerView.jf.size

if config.toastPosition == .dynamicIsland {

contianerView.jf_size = CGSize(width: 120, height: 34)

contianerView.center = CGPoint(x: CGSize.jf.screenSize().width / 2, y: 27)

}

let updateV = {

contianerView.center = CGPoint(x: CGSize.jf.screenSize().width / 2, y: CGSize.jf.screenSize().height / 2)

if config.toastPosition == .top {

contianerView.jf_top = CGFloat.jf.navigationBarHeight() + 15

} else if config.toastPosition == .bottom {

contianerView.jf_bottom = CGSize.jf.screenHeight() - CGFloat.jf.safeAreaBottomHeight() - 15

} else if config.toastPosition == .dynamicIsland {

contianerView.jf_size = originSize

contianerView.center = CGPoint(x: CGSize.jf.screenSize().width / 2, y: originSize.height / 2 + 10)

}

contianerView.layoutIfNeeded()

}

guard config.withoutAnimation == false else {

updateV()

transitonContext?.completeTransition(true)

completion?(true)

return

}

if config.toastPosition == .dynamicIsland {

UIView.animate(withDuration: 0.25) {

updateV()

} completion: { finished in

transitonContext?.completeTransition(true)

completion?(finished)

}

return

}


消失:


UIView.animate(withDuration: 0.25, animations: {

if config.toastPosition == .dynamicIsland {

contianerView?.layer.cornerRadius = 17

contianerView?.jf_size = CGSize(width: 120, height: 34)

contianerView?.center = CGPoint(x: CGSize.jf.screenSize().width / 2, y: 27)

}

contianerView?.subviews.forEach({ v in

if config.toastPosition == .dynamicIsland {

v.isHidden = true

} else {

v.alpha = 0

}

})

contianerView?.alpha = 0

}) { (finished) in

transitonContext?.completeTransition(true)

completion?(finished)

}


末尾


以上即是我JFPopup内置组件JFToastView适配灵动岛动画的全过程,假如下半年苹果更新了Api我也会第一时间重新适配。


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

客户端开发的我,准备认真学前端了

背景 我呢,一个Android开发工程师,从毕业到现在主要做的是客户端开发,目前在一个手机厂商任职。自己目前知识技能主要在客户端上,其他方面会一点点,会一点点前端知识,会一点点后端知识,会一点点脚本,用网络的一句话概括起来就是“有点东西,但是不多”😭。 为什么...
继续阅读 »

背景


我呢,一个Android开发工程师,从毕业到现在主要做的是客户端开发,目前在一个手机厂商任职。自己目前知识技能主要在客户端上,其他方面会一点点,会一点点前端知识,会一点点后端知识,会一点点脚本,用网络的一句话概括起来就是“有点东西,但是不多”😭。


为什么


决定学习前端,并不是心血来潮,一时自嗨,而是经过了比较长时间的思考。对于程序员来说,知识的更新迭代实在是很快,所以保持学习很重要。但是技术防线这么多,到底学什么?我相信这不是一个很容易做出抉择的问题。


对于前端之前有断断续续的学过一些,但是最后没有一直坚持下来。之所以这样,原因很多,比如没有很强的目标、没有足够的时间,前端涉及的知识点太多等。


但是我觉得对自己而言,最重要的一个原因是:**学习完前端,我能用它来干嘛?**如果没有想清楚这个原因,就很难找到目标。做事情没有目标,就无法拆解,也就无法长期坚持下去。直到最近,看了一些文章,碰到了一些事情,才慢慢想清楚这个问题。目前对我而言,开始决定认真学习前端的主要原因有两个:

  • 自己一直想做点什么
  • 工作上有需要

想做点什么


从我接触计算机开始,心底里一直有个梦,就是想利用自己手上技能,做点什么。我也和旁边的朋友同事交流过,大家都有类似的想法,从这看估计很多程序员朋友都会有这样的想法。我从一开始的捣鼓网站,论坛,到后来开发APP等,折腾了好多东西。但是到了最后,都没有折腾出点啥,都无疾而终。


前一段时间,看到一个博主写的一篇文章,文章大概是讲他如何从一个公司的后端开发工程师,走到今天成为一名独立开发者的故事。


其中有一段是说他一直心里念念不忘,想做一款 saas 应用,期间一直在学习和看其他人的产品,学习经验,尝试不同的想法。所谓念念不忘必有回响,终于从别人的产品中产生了一个点子,然后很快写好了后端服务,并自学前端边做边学,完成了这个产品。目前他的这个产品运作的很成功。


这个故事给我很大鼓舞,之前看到过很多这样的故事,有成功的,有失败的。我也去分析看了那些成功的,经过自己的观察,大部分成功的独立开发者,基本上都是多年前成功的那批,那段时间还是处于互联网的红利期,天时地利人和加在一起,造就了他们的成功。


当然这里并不是否认他们能力,觉得是他们运气好。能在当时那么多人中,脱颖而出,依然表明他们是佼佼者。这里只是想表达那个时间段,大环境对开发者来说,是比较友好的,阻力没有那么大。


很少看到最近两年成功的开发者(不排除自己不知道哈),但是从这位博主的经历来看,他确实在成功了,这给了我很大的鼓舞,说明这条路上还是有机会的,只是在现在这种大环境下,成功的难度在增加,阻力变大。如果我们自己始终坚持,寻找机会,不断地尝试,是否有一天可能会成功呢?


那这样的话,我主要关注哪个方向呢?我个人更加偏向于前端全栈方向,包括WebApp,小程序,P C 软件等。


为什么这么认为呢?看下现在的大环境,不提之前上架APP需要各种软件著作权,后来个人无法在各大商店上发布APP,再到现在新出的APP备案制,基本上个人想在Android App上发力,真的很难了。而且,经过自己在ProductHunt上观察,目前大部分独立开发者的作品都是聚焦于WebAppSAAS,或者是PC类软件,剩下就是IOSMAC平台的。


且学习前端技术栈是一个比较好的选择。JavaScript这门语言很强大,整个技术栈不仅可以做前端,也可以做后端开发,还可以做跨平台的 P C 软件开发, 提供的丰富的解决方案,对个人开发者来说极为合适。


当然,我们也可以找合适的人,一起组队合作,不用单打独斗,这样不仅节省期间和精力,也能有好的交流和碰撞。这条路我也经历过,但是说实话执行起来确实有一定的困难。首先就是人难找,要想找到一个三观差不多的伙伴,其实真的挺难的。还有一个就是个人时间和做事方式也很难契合。所以个人认为如果想做点什么,前期一个人自己去实现一个MVP出来,是一个合适的选择。后面如果有必要了,倒是可以考虑慢慢招人。


我们也要认识到技术只是最基础的第一步,要想做成一个产品,还有很多东西要学习。推广、运营,沟通交流无论哪个都是一道坎。但是作为登山者的我们不要关注前面路有多远,而是要确保自己一直在路上。


工作涉及


还有一个原因是,最近工作上和前端打交道有很多。因为项目内部接入了类似 React Native 的框架,有大量的业务场景是基于这个框架开发。这就导致了客户端涉及到大量和前端的交互,流程的优化,工程化等工作。客户端可以不用了解前端和框架的知识,也没什么问题。
但是想着如果后续这一块有什么问题,或者想对这块做一些性能优化、工程提效的事情,如果对前端知识没有一个很好的了解,估计也很难做出彩。


结尾


今天在这里絮絮叨叨这么多,并不是想要告诉大家选择前端技术栈学习就一定咋样,比如第一点说的独立开发者中,有很多的全栈开发者,他们有的已经失败了,有的还在路上,成功的毕竟还是少数。
我想分享的是我个人关于为什么选择前端技术栈作为学习方向,如何做出选择的一些思考。这都是我的一家之言,不一定正确,大家姑且一看。


同时自己心里也还是希望能像文章提到的那位博主一样,在做产品这条路上,也能“念念不忘,必有回响”。正如我一直相信秉持的“日拱一卒,功不唐捐”。


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

某法宝网站 js 逆向思路

web
本文章只做技术探讨, 请勿用于非法用途。 目标网站 近期为了深刻学习法律知识, 需要来下载一些法律条文相关的内容。 本文通过 某法宝网站来下载数据 http://www.pkulaw.com/。 网站分析 文章内容 详情页图片 可以看到下载的方式还蛮多的,...
继续阅读 »

本文章只做技术探讨, 请勿用于非法用途。



目标网站


近期为了深刻学习法律知识, 需要来下载一些法律条文相关的内容。


本文通过 某法宝网站来下载数据 http://www.pkulaw.com/。


网站分析


文章内容


image.png
详情页图片


可以看到下载的方式还蛮多的, 尝试复制全文, 得到内容保留原格式, 所以我选择使用复制全文的方式来得到文章内容。


同时详情页没有看到明显的反扒措施, 不需要特殊处理。


列表页


image.png


最终决定通过专题分类来获取所有的数据, 然后调试分析接口参数, 这里没有什么加密的参数, 确定需要关注的参数如下:


{
"Aggs.SpecialType": "", // 专题类型(编号)
"VerifyCodeResult": "", // 验证码值(后边讲解)
"Pager.PageIndex": "2", // 页码
"RecordShowType": "List", // 显示方式(List 方式显示所有数据, 保持该值即可)
"Pager.PageSize": 100, // 每页的数量(最大 100)
}


这里仅列出了需要关注的参数, 其他参数保持原值即可(需要的话可以自己调试对比参数值确定意义), 当 pageSize 设置为 100 时, 第 3 页之后的数据需要验证码才能查看。


验证码


为方便分析验证码的校验方式, 推荐使用无痕模式来调试, 获取验证码之前清空一次 cookie。


image.png


image.png


image.png


可以看到验证码的流程为:



  1. 请求验证码, 并返回 set-cookie 。

  2. 携带该 cookie 信息并校验验证码, 通过后得到 code。

  3. 携带 code 请求数据, 得到数据。


开整


确定了问题后, 就可以开始了, 一个一个解决。


文章内容


确定了要用复制的方式来得到数据, 那就分析下他的复制干了点啥。


image.png


查看他的页面元素, 发现了这两个东西, 全局搜索后很容易定位到处理函数(找不到的话刷新页面)。


image.png


然后接着定位这个 toCopyFulltext() 函数。


image.png


找到这个就对了, 然后可以看代码他是用的 jQuery 的选择器来做的, 我们只能来仿造一个页面结构来用它, 这里推荐使用 cheerio(nodejs) 库来做(jsdom 应该也可以?)。


image.png


伪造后直接调用就大功告成。


列表页


这个问题不大, 问题主要在于验证码。


image.png


通过查看页面元素可以得到专题编号。


验证码


请求


// 验证码图片请求返回结果
{
"errcode": 0,
"y": 131,
"array": "14,12,11,7,6,2,13,15,9,8,16,0,1,3,4,18,17,5,19,10",
"imgx": 300,
"imgy": 200,
"small": "data:image/jpg;base64,...", // 滑块图片
"normal": "data:image/jpg;base64,..." // 背景图片
}

image.png
得到的 base64 可以用 python 的 PIL 库来解析出来。


image.png
然后我解析出来的图片就是这个样子, 基本上就确定了这验证码就是老朋友了, 我们先来把他还原。



还是简单解释一下这个, 图片被切割为了上下两部分, 每个部分又被切割为了 10 等份, 原图为 300x200, 也就是说这里被切割为 20 张 30x100 的图片, 然后打乱顺序拼接后返回, 我们需要先完成切割然后再按正确的顺序来拼接还原。



"errcode": 0,
"y": 131,
"array": "14,12,11,7,6,2,13,15,9,8,16,0,1,3,4,18,17,5,19,10", // 图片的正确顺序
"imgx": 300, // 图片宽
"imgy": 200, // 图片高
"small": "data:image/jpg;base64,...", // 滑块图片
"normal": "data:image/jpg;base64,..." // 背景图片

image.png


image.png


还原后的图片就是这个样子了。


校验


// 校验接口请求参数
{
"act": "check",
"point": "197", // 缺口位置
"timespan": "1067", // 滑动耗时
"datelist": "1,1692848585282|10,1692848585288|20,1692848585295|34,1692848585305|50,1692848585312|58,1692848585320|74,1692848585328|90,1692848585338|95,1692848585344|107,1692848585355|117,1692848585360|124,1692848585371|126,1692848585376|130,1692848585388|133,1692848585393|136,1692848585404|137,1692848585409|138,1692848585416|139,1692848585425|139,1692848585433|140,1692848585441|140,1692848585449|140,1692848585457|141,1692848585465|141,1692848585473|141,1692848585481|142,1692848585489|142,1692848585498|142,1692848585506|143,1692848585514|143,1692848585522|143,1692848585530|144,1692848585539|145,1692848585546|146,1692848585554|146,1692848585562|149,1692848585572|151,1692848585579|154,1692848585589|157,1692848585596|160,1692848585604|161,1692848585611|164,1692848585621|167,1692848585627|170,1692848585639|172,1692848585643|174,1692848585655|177,1692848585659|179,1692848585667|181,1692848585676|182,1692848585683|184,1692848585692|185,1692848585699|186,1692848585710|187,1692848585716|188,1692848585724|189,1692848585732|189,1692848585740|190,1692848585748|191,1692848585758|192,1692848585764|192,1692848585773|193,1692848585781|194,1692848585789|195,1692848585797|195,1692848585806|196,1692848585813|196,1692848585821|197,1692848585829|197,1692848585839|197,1692848585845|197,1692848585855|197,1692848586219"
} // 滑动轨迹(位置,时间戳)

需要解决的参数为 point(缺口位置) 及 datelist(滑动轨迹)。


point

image.png


推荐使用 ddddocr 库来识别, 准确率还可以吧, 挺方便的。


datelist

image.png


轨迹方面自己设计算法来吧, 这里可以作为一个参考, 他的 datelist 的长度不固定, 一般也就是一百多些轨迹点吧, 可以通过调整参数来达到效果, 反正就是多测试吧, 这个方法大概有 百分之九十 左右的通过率吧, 暂时够用。


VerifyCodeResult


// 校验请求成功后返回数据
{
"state": 0,
"info": "正确",
"data": 197
}
// VerifyCodeResult: YmRmYl8xOTc=

解决了验证码, 惊喜的发现还是不咋对, 这个返回的 data 明显长得和要用的 VerifyCodeResult 不太像, 就接着来找。


image.png


全局搜索, 找到两个 js 文件, 都打上断点来调试。


image.png


可以看到我们成功断到, 并得到是由 (new Base64).encode("bdfb_" + y) 这种方式来生成的 code 值, 这个 y 值就是上一步返回的 data, 接下来只需要把 Base64 的代码那里扣下来, 或者自己实现就行了, 方便些, 这里直接抠下来了, 然后拿到 code 带上验证码请求返回的 cookie, 就能正常拿到数据了。


结语


整体来说不算困难, 没有什么加密啊混淆啊之类的东西, 确定方向之后很快就能搞定了, 用来练手还是很不错的, 有什么问题欢迎交流, 不知道这个得几年啊。。。



请洒潘江,各倾陆海云

作者:Glommer
来源:juejin.cn/post/7270702261293039635
尔。


收起阅读 »

数据抓取:抓取手机设备各种数据

目录 前言 一、DataCapture 1.通讯录集合数据 2.应用列表集合数据 3.日历事件信息数据 4.电量信息数据 5.sms短信信息数据 6.照片集合信息数据 7.传感器信息数据 8.wifi信息数据...等等数据 二、使用步骤 1.引入库 2....
继续阅读 »

目录


前言


一、DataCapture



  • 1.通讯录集合数据

  • 2.应用列表集合数据

  • 3.日历事件信息数据

  • 4.电量信息数据

  • 5.sms短信信息数据

  • 6.照片集合信息数据

  • 7.传感器信息数据

  • 8.wifi信息数据...等等数据


二、使用步骤



  • 1.引入库

  • 2.获取数据方法,目前因数据量庞大,暂推荐手动在子线程调用

  • 3.关于权限,待更新

  • 总结




前言


基于最近刚完结的外包项目功能——数据抓取,通过调用api和内容提供器来获取手机设备各种数据,诸如SMS短信数据、电量数据、手机应用数据等等,我尝试开发了一个开源库,希望能够帮助到大家来实现这个功能。


习惯性上图展示:


在这里插入图片描述


一、DataCapture


对手机设备的信息数据抓取,目前支持在子线程抓取数据,因为有些数据量过于庞大会阻塞线程,可抓取数据有:


1.通讯录集合数据


字段名详情
contact_display_name联系人名称
last_time_contacted上次通讯时间(毫秒)
number联系人手机号
times_contacted联系次数
up_time编辑时间(毫秒))
type通话类型

2.应用列表集合数据


字段名详情
app_nameAPP名称
app_type是否系统app 0:非系统app 1:系统app
app_versionAPP版本
in_time安装时间(毫秒)
obtain_time数据抓取时间(秒))
package_name包名
up_time更新时间 (毫秒)
version_code版本号

3.日历事件信息数据


字段名详情
description事件描述
end_time事件结束时间(毫秒)
event_id事件ID
event_title事件标题
start_time事件开始时间(毫秒))
reminders提醒列表

4.电量信息数据


字段名详情
battery_level电池电量
battery_max电池容量
battery_pct电池百分比
battery_state电池状态 充电0 不充电1
is_ac_charge是否交流充电(1:yes,0:no)
is_charging是否正在充电
is_usb_charge是否USB充电(1:yes,0:no)

5.sms短信信息数据


字段名详情
content短信消息体
other_phone收件⼈/发件⼈⼿机号
package_name包名
read短信状态 0-未读,1-已读
seen短信是否被用户看到 0-尚未查看,1-已查看
status短信状态:-1表示接收,0-complete,64-pending,128-failed
subject短信主题
time收到短信的时间戳(毫秒),long型
type短信类型:1-接收短信,2-已发出短信

6.照片集合信息数据


字段名详情
addTime添加数据库时间(保存)
author照片作者
createTime照片读取时间(毫秒数时间戳),即当前时间
date拍照时间(毫秒数时间戳)
flash闪光灯
focal_length镜头的实际焦距
gps_altitude海拔高度
gps_processing_method定位的方法名称
height照片高度
latitude照片拍摄时的经度
lens_make镜头制造商
lens_model镜头的序列号
longitude照片拍摄时的纬度
model拍照机型
name照片名称
orientation照片方向
save_time照片修改时间
software生成图像的相机或图像输入设备的软件或固件的名称和版本
take_time创建时间(毫秒数时间戳)
updateTime编辑时间
width照片宽度
x_resolutionX方向上每个分辨率的像素数
y_resolutionY方向上每个分辨率的像素数

7.传感器信息数据


字段名详情
id传感器id,0不支持功能,-1即其类型和名称的组合在系统中唯一标识。-2获取不到
maxRange传感器单元中传感器的最大量程
minDelay两个事件之间允许的最小延迟(以微秒为单位),如果此传感器仅在其测量的数据发生变化时返回值,则为零
name传感器名称
power使用时功率
resolution传感器单元中传感器的分辨率
type该传感器的通用类型
name传感器名称
vendor厂商字符串
version版本

8.wifi信息数据...等等数据


二、使用步骤


1.引入库


在seetings.gradle中引入


repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}

在build.gradle中引入


 implementation 'com.github.Android5730:DataCapture:v0.23'

2.获取数据方法,目前因数据量庞大,暂推荐手动在子线程调用


// 获取通讯录
List<AddressBookBean> addressBookBean = AddressBookUtil.getAddressBookBean(getBaseContext());
// 获取应用列表
List<AppListBean> appListBean = AppListUtil.getAppListBean(this);
// 获取日历事件
List<CalendarListBean> calendarListBean = CalendarListUtil.getCalendarListBean(this);
// 获取电量信息
BatteryStatusBean batteryState = BatteryStatusUtil.getBatteryState(this);
// 获取wifi信息
NetworkBean networkBean = NetworkBeanUtils.getNetworkBean(this);
// 获取sms短信信息
List<SmsBean> smsList = SmsUtil.getSmsList(this);
// 获取照片集合信息
List<PhotoInfosBean> photoInfosBean = PhotoInfosUtil.getPhotoInfosBean(this, LocationUtils.getInstance(this).showLocation());
// 获取传感器集合信息
List<SensorListBean> sensorListBean = SensorListUtil.getSensorListBean(this);


3.关于权限,待更新


注意:因为获取图片时需要外部存储的权限,我这里采取的取消分区存储的做法,所以大家不要忘记在application里添加android:requestLegacyExternalStorage="true"
如果有哪个权限碍眼,或者项目强制不需要,也可以进行删除,如去除读取外部存储的权限:


    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
tools:node="remove"/>


    <!-- 定位权限,需动态请求 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- 通讯录,需动态请求 -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- 日历信息,需动态请求 -->
<uses-permission android:name="android.permission.READ_CALENDAR" />
<!-- wifi信息,不用动态请求 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- SMS信息,需动态请求 -->
<uses-permission android:name="android.permission.READ_SMS" />
<!-- photo信息,需动态请求-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 取消分区存储-->
<meta-data
android:name="ScopedStorage"
android:value="true" />


最后附上开源库地址:数据抓取:https://github.com/Android5730/DataCapture
如果有帮助到各位,可以给个star,给我一点信心去完善这个开源库


总结


当然目前该库目前抓取的数据还不到外包项目抓取数据的一半,只是因为最近有点忙,没时间完善所以才匆匆忙忙推出,相信等开学后就有时间完善,现在实习太累了。如果大家有疑问,可以在评论区提出,也可以在issue提出来,如果

作者:人间正四月
来源:juejin.cn/post/7271659608011358264
受到大家欢迎,我会持续完善此库。

收起阅读 »

Android简单的两级评论功能实现

Android简单的两级评论功能实现 前言 在App开发过程中,做了文章页面,那评论的功能自然是必不可少的,怎么做呢,如果只是做一个简单的评论不带回复功能的话,那和之前做毕设时候的我也看不到进步呀,这怎么行呢?于是我打开‘稀土掘金’App,随便看了几个文章,试...
继续阅读 »

Android简单的两级评论功能实现


前言


在App开发过程中,做了文章页面,那评论的功能自然是必不可少的,怎么做呢,如果只是做一个简单的评论不带回复功能的话,那和之前做毕设时候的我也看不到进步呀,这怎么行呢?于是我打开‘稀土掘金’App,随便看了几个文章,试了试评论的功能,于是便开始了我的构思。我想要实现的效果如下图所示,如何实现这样一个页面呢?我使用的方法是RecyclerView中再嵌套一个RecyclerView,一个用来展示一级评论,另一个则用来展示相应的二级评论,思路有了,下面就开始我的实现。


1693188380832.png

一、数据库


1、构建数据库


要想做好一个功能,数据库的构建是重中之重。下图是我构造的评论实体类


image.png

评论表中包含如下字段:



  • id --评论主键(自动生成)

  • newsId -- 主键

  • number -- 评论的用户主键

  • content -- 评论内容

  • time -- 评论时间

  • level -- 评论级别(有两级,当评论的对象是文章作者时,level为1,当评论对象为文章内的评论时,level为2,默认level为1)

  • replyNumber -- 评论回复的用户主键

  • replyId -- 评论回复的评论主键(只有level为2的评论才会用到该字段,所以默认为空)



replyNumber其实这里不应该默认为空的,因为无论是那种类型的回复,都是有对应的用户的,这个疏忽也造成了我在后面构建“我的评论”界面时,无法展示出文章作者的详细信息。



2、封装数据库


数据访问层Dao主要封装了对数据库的访问:


image.png

很平常的SQL语句,只简单说明下:分别是添加评论、根据id删除评论、获取该文章的所有评论、获取该用户的所有评论、通过id获取该评论



(省略了CommentTask接口即实现)


最后仓库层将这些方法都封装起来,方便后续调用,如下图所示:
image.png


二、布局


1、文章详情界面的评论布局


1693191191816.png



就是个RecyclerView哈哈



2、评论的适配器布局


1693191324123.png



可以看到适配器布局中还包含了一个RecyclerView,这里面展示的就是二级评论



3、二级评论的适配器布局


image.png



这个布局很简单,就由几个TextView组件构成



三、代码逻辑


首先,在ViewModel层初始化该文章的所有评论,观察评论数据变化,给评论适配器数据赋值并刷新,在评论适配器中再对level为2的评论数据进行过滤并赋值给回复适配器。


1、获取评论数据


var comments = MutableLiveData<List<CommentInfo>>()
comments.value = commentStoreRepository.getCommentsByNewId(newsId)

通过文章的id获取到评论


2、给评论适配器数据赋值


image.png


3、在评论适配器处理数据



首先,评论适配器中的数据是通过文章的id获取到的所有评论,包含了一级和二级评论,在评论适配器展示的当然不能是所有的评论,而是所有一级的评论,而二级评论的数据需要再进行过滤传递给回复适配器



所以,在绑定ViewHolder以及getItemCount时,需要对传递的数据进行过滤,


image.png
如图所示,allList是通过文章的id获取到的所有评论,list是level为1的所有评论,replyList是level为2的所有评论。getItemCount返回的是一级评论的个数。在绑定ViewHolder时,将一些回调函数和一级评论和二级评论列表传递进去,接着就看ViewHolder中的数据处理逻辑,如下两张图


image.png



这张图只是一些简单的一级数据的赋值和一些回调参数的调用传参



image.png



这里首先对二级评论进行过滤,过滤出与该条一级评论相关联的二级评论,接着对布局进行一些操作,接着是赋值操作和回复适配器中一些函数的实现。



4、在回复适配器处理数据


image.png



在这里就不需要对数据进行处理了,只有简单的赋值和回调了



5、回调函数的实现


image.png


四、实现效果


1、评论功能


7edbc47b85fd05b9c25841161eb4ba8.jpg

2、我的评论展示


dd6d2787fbf6ebf4a397367fc91fd4e.jpg

这里的“@3333333333”就是因为replyNumber为空的导致无法展示出文章作者的详细信息,只有展示用户主键了,后面再进行修改。



五、结语


就这样,一个简单的二级评论功能就完成了。文章若出现错误,欢迎各位批评指正,写文不易,转载<

作者:遨游在代码海洋的鱼
来源:juejin.cn/post/7271991667246694437
/strong>请注明出处谢谢。

收起阅读 »

JS长任务(耗时操作)导致页面卡顿,优化方案大比拼!

web
抛出问题 前端是直接和客户交互的界面,前端的流畅性对于客户的体验和留存率至关重要,可以说,前端界面的流畅性是一款产品能不能成功的关键因素之一。要解决界面卡顿,首先我们先要知道造成页面卡顿的原因: 什么是长任务? 长任务是指JS代码执行耗时超过50ms,能让...
继续阅读 »

抛出问题


前端是直接和客户交互的界面,前端的流畅性对于客户的体验和留存率至关重要,可以说,前端界面的流畅性是一款产品能不能成功的关键因素之一。要解决界面卡顿,首先我们先要知道造成页面卡顿的原因:




  • 什么是长任务?


    长任务是指JS代码执行耗时超过50ms,能让用户感知到页面卡顿的代码执行流。




  • 长任务为什么会造成页面卡顿?


    UI界面的渲染由UI线程控制,UI线程和JS线程是互斥的,所以在执行JS代码时,UI线程无法工作,就表现出页面卡死状态。




我们现在来模拟一个长任务,看看它是怎么影响页面流畅性的:



  • 先看效果(GIF),这里我们给div加了个滚动的动画,当我们开始执行长任务后,页面卡住了,等待执行完后才恢复,总耗时3秒左右,记住总耗时,后面会用到。


动画.gif



  • 再看代码(有点长,主要看JS部分,后面的优化方案代码只展示优化过的JS函数)


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.myDiv {
width: 100px;
height: 100px;
margin: 50px;
background-color: blue;
position: relative;
animation: my-animation 5s linear infinite;
}
@keyframes my-animation {
from {
left: 0%;
rotate: 0deg;
}
to {
left: 100%;
rotate: 360deg;
}
}
</style>
</head>
<body>
<div class="myDiv"></div>
<button onclick="longTask()">执行长任务</button>

<script>
// 模拟耗时操作,大概10ms
function myFunc() {
const startTime = Date.now();
while (Date.now() - startTime < 10) {}
}

// 长任务,循环执行myFunc300次,耗时3秒左右
function longTask() {
console.log("开始长任务");
const startTime = Date.now();
for (let i = 0; i < 300; i++) {
myFunc();
}
console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
}
</script>
</body>
</html>


本段代码有一个模拟耗时的函数,有一个模拟长任务的函数(调用300次耗时函数),后面的优化方案都会基于这段代码来进行。


优化方案


setTimeout 宏任务方案


第一个优化方案,我们将长任务拆成多个宏任务来执行,这里我们用setTimeout函数。为什么拆成多个宏任务可以优化卡顿问题呢?


正如我们上文所说,页面卡顿的原因是因为JS执行线程占用了控制权,导致UI线程无法工作。在浏览器的事件轮询(EventLoop)机制中,每一个宏任务执行完之后会将控制权重新交给UI线程,待UI线程执行完渲染任务后,才会继续执行下一个宏任务。浏览器轮询机制流程图如下所示,想要深入了解浏览器轮询机制,可以参考我的另一篇文章:从进程和线程入手,我彻底明白了EventLoop的原理! image.png



  • 先看效果(GIF),执行长任务的同时,页面也很流畅,没有了先前卡顿的感觉,总耗时4.4秒。


动画.gif



  • 再看代码


// setTimeout方案 递归,循环300次
function timeOutTask(i, startTime) {
setTimeout(() => {
if (!startTime) {
console.log("开始长任务");
i = 0;
startTime = Date.now();
}
if (i === 300) {
console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
return;
}
myFunc();
timeOutTask(++i, startTime);
});
}

把代码改为多个宏任务之后,解决了页面卡顿的问题,但是总耗时比之前多了1.4秒,主要原因是因为递归调用需要不断地向下开栈,会增加开销。当我们每个任务都不依赖于上一个任务的执行结果时,就可以不使用递归,直接使用循环创建宏任务。



  • 先看效果(GIF),耗时缩短到了3.1秒,但是可以看到明显掉帧。


动画.gif



  • 再看代码


// setTimeout不递归方案,循环300次
function timeOutTask2() {
console.log("开始长任务");
const startTime = Date.now();

for (let i = 0; i < 300; i++) {
setTimeout(() => {
myFunc();
if (i === 300 - 1) {
console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
}
});
}
}

使用300个循环同时创建宏任务后,虽然耗时降低了,但是div滚动会出现明显掉帧,这也是我们不愿意看到的,那执行代码速度和页面流畅度就没办法兼得了吗?很幸运,requestIdleCallback函数可以帮你解决这个难题。


requestIdleCallback 函数方案


requestIdleCallback提供了由浏览器决定,在空闲的时候执行队列任务的能力,从而不会影响到UI线程的正常运行,保证了页面的流畅性。


它的用法也很简单,第一个参数是一个函数,浏览器空闲的时候就会把函数放到队列执行,第二个参数为options,包含一个timeout,则超时时间,即使浏览器非空闲,超时时间到了,也会将任务放到事件队列。
下面我们把setTimeout替换为requestIdleCallback



  • 先看效果(GIF),耗时3.1秒,也没有出现掉帧的情况。


动画.gif



  • 再看代码


// requestIdleCallback不递归方案,循环300次
function callbackTask() {
console.log("开始长任务");
const startTime = Date.now();

for (let i = 0; i < 300; i++) {
requestIdleCallback(() => {
myFunc();
if (i === 300 - 1) {
console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
}
});
}
}

requestIdleCallback解决了setTimeout方案掉帧的问题,这两种方案都需要拆分任务,有没有一种不需要拆分任务,还能不影响页面流畅度的方法呢?Web Worker满足你。


Web Worker 多线程方案


WebWorker是运行在后台的javascript,独立于其他脚本,不会影响页面的性能。



  • 先看效果,耗时不到3.1秒,页面也没有受到影响。


动画.gif



  • 再看代码,需要额外创建一个js文件。(注意,浏览器本地直接运行HTML会被当成跨域,需要开一个服务运行,我使用的http-server)


task.js 文件代码


// 模拟耗时
function myFunc() {
const startTime = Date.now();
while (Date.now() - startTime < 10) {}
}

// 循环执行300次
for (let i = 0; i < 300; i++) {
myFunc();
}

// 通知主线程已执行完
self.postMessage("我执行完啦");

主文件代码


// Web Worker 方案
function workerTask() {
console.log("开始长任务");
const startTime = Date.now();
const worker = new Worker("./task.js");

worker.addEventListener("message", (e) => {
console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
});
}

WebWorker方案额外增加的耗时很少,也不需要拆分代码,也不会影响页面性能,算是很完美的一种方案了。
但它也有一些缺点:



  • 浏览器兼容性差

  • 不能访问DOM,即不能更新UI

  • 不能跨域加载JS


总结


三种方案中,如果不需要访问DOM的话,我认为最好的方案为WebWorker方案,其次requestIdleCallback方案,最后是setTimeout方案。
WebWorker和requestIdleCallback属于比较新的特性,并非所有浏览器都支持,所以我们需要先进行判断,代码如下:


if (typeof Worker !== 'undefined') {
//使用 WebWorker
}else if(typeof requestIdleCallback !== 'undefined'){
//使用 requestIdleCallback
}else{
//使用 setTimeout
}

希望本文对您有帮助,其他所有代码可在下方直接执行。(WebWorker不支持)

作者:TuYuHao
来源:juejin.cn/post/7272632260180377634
n>

收起阅读 »

你看这个圆脸😁,又大又可爱~ (Compose低配版)

web
前言 阅读本文需要一定compose基础,如果没有请移步Jetpack Compose入门详解(实时更新) 在网上看到有人用css写出了下面这种效果;原文链接 我看到了觉得很好玩,于是用Compose撸了一个随手势移动眼珠的版本。 一、Canvas画图 这...
继续阅读 »


前言


阅读本文需要一定compose基础,如果没有请移步Jetpack Compose入门详解(实时更新)


在网上看到有人用css写出了下面这种效果;原文链接


请添加图片描述


我看到了觉得很好玩,于是用Compose撸了一个随手势移动眼珠的版本。




一、Canvas画图


这种笑脸常用的控件肯定实现不了,我们只能用Canvas自己画了


笑脸


我们先画脸



下例当中的size和center都是onDraw 的DrawScope提供的属性,drawCircle则是DrawScope提供的画圆的方法



Canvas(modifier = modifier
.size(300.dp),
onDraw = {

// 脸
drawCircle(
color = Color(0xfffecd00),
radius = size.width / 2,
center = center
)

})

属性解释



  • color:填充颜色

  • radius: 半径

  • center: 圆心坐标


这里我们半径取屏幕宽度一半,圆心取屏幕中心,画出来的脸效果如下


在这里插入图片描述


微笑


微笑是一个弧形,我们使用drawArc来画微笑


// 微笑
val smilePadding = size.width / 4

drawArc(
color = Color(0xffb57700),
startAngle = 0f,
sweepAngle = 180f,
useCenter = true,
topLeft = Offset(smilePadding, size.height / 2 - smilePadding / 2),
size = Size(size.width / 2, size.height / 4)
)

属性解释



  • color:填充颜色

  • startAngle: 弧形开始的角度,默认以3点钟方向为0度

  • sweepAngle:弧形结束的角度,默认以3点钟方向为0度

  • useCenter :指示圆弧是否要闭合边界中心的标志(上例加不加都无所谓)

  • topLeft :相对于当前平移从0的本地原点偏移,0开始

  • size:要绘制的圆弧的尺寸


效果如下
在这里插入图片描述


眼睛和眼珠子


眼睛也是drawCircle方法,只是位置不同,这边就不再多做解释


            // 左眼
drawCircle(
color = Color.White,
center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
radius = smilePadding / 2
)


//左眼珠子
drawCircle(
color = Color.Black,
center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
radius = smilePadding / 4
)



// 右眼
drawCircle(
color = Color.White,
center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
radius = smilePadding / 2
)


//右眼珠子
drawCircle(
color = Color.Black,
center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
radius = smilePadding / 4
)


整个笑脸就画出来了,效果如下


在这里插入图片描述


二、跟随手势移动


在实现功能之前我们需要介绍transformableanimateFloatAsStatetranslate


transformable


transformablemodifier用于平移、缩放和旋转的多点触控手势的修饰符,此修饰符本身不会转换元素,只会检测手势。


animateFloatAsState


animateFloatAsState 是通过Float状态变化来控制动画 的状态


知道了这两个玩意过后我们就可以先通过transformable监听手势滑动然后通过translate方法和animateFloatAsState方法组成一个平移动画来实现眼珠跟随手势移动


完整的代码:


@Composable
fun SmileyFaceCanvas(
modifier: Modifier
)
{

var x by remember {
mutableStateOf(0f)
}

var y by remember {
mutableStateOf(0f)
}

val state = rememberTransformableState { _, offsetChange, _ ->

x = offsetChange.x
if (offsetChange.x >50f){
x = 50f
}

if (offsetChange.x < -50f){
x=-50f
}

y = offsetChange.y
if (offsetChange.y >50f){
y= 50f
}

if (offsetChange.y < -50f){
y=-50f
}
}

val animTranslateX by animateFloatAsState(
targetValue = x,
animationSpec = TweenSpec(1000)
)

val animTranslateY by animateFloatAsState(
targetValue = y,
animationSpec = TweenSpec(1000)
)



Canvas(
modifier = modifier
.size(300.dp)
.transformable(state = state)
) {



// 脸
drawCircle(
Color(0xfffecd00),
radius = size.width / 2,
center = center
)

// 微笑
val smilePadding = size.width / 4

drawArc(
color = Color(0xffb57700),
startAngle = 0f,
sweepAngle = 180f,
useCenter = true,
topLeft = Offset(smilePadding, size.height / 2 - smilePadding / 2),
size = Size(size.width / 2, size.height / 4)
)

// 左眼
drawCircle(
color = Color.White,
center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
radius = smilePadding / 2
)

translate(left = animTranslateX, top = animTranslateY) {
//左眼珠子
drawCircle(
color = Color.Black,
center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
radius = smilePadding / 4
)
}


// 右眼
drawCircle(
color = Color.White,
center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
radius = smilePadding / 2
)

translate(left = animTranslateX, top = animTranslateY) {
//右眼珠子
drawCircle(
color = Color.Black,
center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
radius = smilePadding / 4
)
}


}
}

为了不让眼珠子从眼眶里蹦出来,我们将位移的范围限制在了50以内,运行效果如下


在这里插入图片描述




总结


通过Canvas中的一些方法配合简单的动画API实

作者:我怀里的猫
来源:juejin.cn/post/7272550100139098170
现了这个眼珠跟随手势移动的笑脸😁

收起阅读 »

入行十年,卷王也卷不动了,想对新人说

很多年前,当我还是一名学生的时候,有一次高我好几届已工作几年的师兄回校给我们做交流,听说他已经是“高级研发工程师”,在深圳某企业月入上万。那时候心里一阵崇拜,觉得“高级”开发该是多么厉害的存在,让我无数次憧憬着成为像他一样厉害且收入高的人群。 时光荏苒,一晃十...
继续阅读 »

很多年前,当我还是一名学生的时候,有一次高我好几届已工作几年的师兄回校给我们做交流,听说他已经是“高级研发工程师”,在深圳某企业月入上万。那时候心里一阵崇拜,觉得“高级”开发该是多么厉害的存在,让我无数次憧憬着成为像他一样厉害且收入高的人群。


时光荏苒,一晃十年过去了。自己也从当初的菜鸟,成长为“高级研发工程师”,然后做了管理、带了团队。然而,在互联网摸爬滚打多年后,发现很多事情跟自己当初想象的完全不一样:



  • 编程并不总是随着经验累积越多,你越发轻松

  • 长时间写一线业务代码并不有趣,甚至有些无聊

  • 如果你只想在你的职业生涯中安静的研究技术,那么你在这个行业很难走远


回溯过往,眼瞅着这个行业潮起潮落,仍然有越来越多的新人奋不顾身的涌入,在此以一个行业“老兵”的身份给即将或者刚入行的同学一些感悟和分享,希望能让你们少踩一些坑,踏上更坚实的职业征程。


珍惜前两年,用力去卷


我一直认为,这个行业(可能其他行业也是)新人成长最快的就是前两年。这是因为:



  • 新人刚刚步入职场,对于新的挑战和机会充满了热情。你们带着刚从学校获得的知识和技能,急切地想要将这些应用到实际工作中,从而迅速增长。

  • 在前两年,新人通常会承担较少的重要项目和高级别的责任。这为你们提供了一个相对安全的环境,可以更自由地学习和尝试新事物,而不必担心严重的后果。

  • 在最初的阶段,新人会得到较多的指导和反馈。这有助于你们更快速地纠正错误、学习新知识,并逐步提高自己的能力。

  • 新人进入工作环境后,需要迅速适应不同的工作情境、流程和团队文化。这种适应性的训练使你们能够更快速地适应变化,并培养出解决问题的能力。

  • 许多新人在前两年内通常没有太多家庭负担,生活开支较少。这使得你们能够更专注地投入到工作和学习中,从而更加快速地成长。相较于后续可能出现的家庭责任和花销,你们在这段时间能够更自由地选择投入时间和精力,去学习新技能、探索新领域,并积累宝贵的经验。


想起我职业生涯的第一年,对未来充满了无尽的焦虑。也是由于这种焦虑,让我牟足了劲去学习行业技能。我不记得有多少个晚上是学到了半夜2-3点,但我的收获是专业技能得到了快速成长,为自己的职业生涯开了一个好头。


image.png


后来我进了大厂,当了管理,大厂对新人一般都会有一个培养机制,比如:3个月入门指导、6个月辅导计划、年度提升规划等。同时,在做事的标准上对新人的宽容度也会更高,会给你试错的机会,但会要求你从错误中去复盘、成长。


然而,在多年的管理生涯中,也遇到了很多新人在短暂的努力过后,变得不思进取,早早的就退出了奋勇争先的行列,被同龄人快速超越。


印象最深的是22届的一位同学,暂且称为A某,成都电子科大毕业。有名校光环,学习能力也强。从实习到入职后前半年,非常积极、努力,成长也很快,半年过后就能独立支持中小项目的研发。但随着时间推移,他身上的劣势也越发显现。在掌握了工作的基本技能后,他开始变得有些不务正业:在工作中开始花大量时间学习安全技能,但本职工作中几乎用不到。相应的,他的工作产出越来越低,交付不及时,质量不合格,对他的投诉也越来越多。经过3个多月的沟通、辅导,再沟通、给改正机会后仍然看不到任何进步,最终被辞退。


写代码很简单,但写好代码很难


当你掌握了一定的专业技能后,实现业务功能对于大多数开发者来说都不是一件难事。但想写出好代码却很难。比方说下面的代码,你认为是好代码么?


func Deduplicate(input []string) []string {
  result := []string{}
  for _, item := range input {
      exists := false
      for _, r := range result {
          if r == item {
              exists = true
              break
          }
      }
      if !exists {
          result = append(result, item)
      }
  }
  return result
}

这段代码用于对字符串数组进行去重操作。虽然实现了去重功能,但从代码质量的角度来看,它存在一些问题:



  • 性能较差:result 切片中进行遍历查找是否已存在相同的元素,时间复杂度较高,特别是当输入切片较大时。

  • 可读性不高: 嵌套的循环和多个条件判断导致代码复杂,难以一眼理解逻辑。

  • 未使用现有工具: Golang 提供了 map 数据结构可以用来更高效地实现去重,但代码中未使用。


让我们试着使用map做出改进:


func Deduplicate(input []string) []string {
  unique := make(map[string]bool)
  result := []string{}
  for _, item := range input {
      if !unique[item] {
          unique[item] = true
          result = append(result, item)
      }
  }
  return result
}

是不是瞬间看起来都舒服多了😄。


那么该如何写出好代码呢?


好代码并不仅仅是实现功能,更是一种艺术和哲学,我们在写代码时,应该多思考代码的质量和可维护性。问自己以下问题:



  • 这段代码是否易于阅读?其他人能理解吗?

  • 是否有更简洁的实现方式?

  • 是否需要加入注释来解释实现思路?

  • 是否考虑了性能问题和异常情况?

  • 是否符合团队的代码规范和设计风格?


这些问题可以总结为以下的编码标准:



  • 可读性优先: 代码应该易于阅读和理解,变量名、函数名应该具有表意性。清晰的命名可以减少代码注释的需要。

  • 简洁明了: 避免冗余代码,使用合适的数据结构和算法,让代码尽可能简洁,同时保持功能完整。

  • 注重性能: 在保持代码可读性的前提下,考虑算法的效率和性能。避免不必要的循环和重复计算。

  • 注释解释: 代码中应添加适量的注释,解释代码的意图、实现思路和关键步骤。这有助于其他开发者理解和维护代码。

  • 模块化设计: 将代码拆分成小的、可复用的模块,提高代码的可维护性和可测试性。

  • 错误处理: 考虑异常情况和边界条件,进行适当的错误处理,避免潜在的问题。

  • 版本控制: 使用版本控制工具管理代码,保留历史记录,方便回溯和团队协作。


写好代码是一件需要时间和经验积累的事情,但始终保持对代码质量的追求,将会使你成为更优秀的开发者。


想拿高薪吗?首先要成为卷王


过去的十多年是互联网疯狂发展的年代,很多人包括我吃到了这个行业的红利,行业内动则薪资上百万、甚至几百万的大有人在。但随着行业红利的逐渐下滑,越来越多的新人涌入,这个行业肉眼可见的变得越来越卷。


我印象中的“卷”是从16开始的,这一年被称作直播大战的一年,也是“短视频”元年。随着智能手机的普及,移动化的加速,万物皆"上线",行业巨头(尤其AT)疯狂扩张和竞争。什么“996是福报”、"面向ICU编程"成为行业普遍的现象。行业变得越发内卷的同时,薪资也确实水涨船高,吸引了越来越多“用生命”换钱的卷王(😭)。


这就是行业的现实,特别是这两年红利期的减少,经济的下滑,大厂业务收缩、裁员加剧,对于新人来说,竞争变得更加剧烈了。在这样的背景下,我为什么推荐新人去“卷”呢,是因为:



  • 积累经验: 通过卷,你可以迅速接触到各种项目、技术和领域,积累宝贵的实战经验。虽然过度卷可能会疲惫,但在一段时间内,你会获得比其他人更多的锻炼机会。

  • 成为多面手: 卷王往往需要在短时间内涉猎多个领域,这培养了你的多面手技能。这对于职业发展和未来的岗位选择都有好处。

  • 快速成长: 卷王面对各种挑战,需要不断学习、解决问题。这种快节奏的环境可以让你迅速成长,积累的知识和技能会让你在职业生涯中受益匪浅。

  • 适应压力: 行业的快速发展和竞争带来了巨大的工作压力,通过卷王经历,你会逐渐适应并变得更加抗压。

  • 职级晋升:通过卷让自己在公司脱颖而出,快速晋升,晋升一定伴随着薪资的增加,就算是跳槽你晋升的职级也是你薪资谈判的重要亮点。


image.png


虽然成为卷王可能需要付出更多的时间和努力,但在现如今的竞争激烈环境下,通过卷王的方式可以更快地脱颖而出,为自己的职业生涯奠定坚实的基础。


搞技术可以,但不要只搞技术


”我就想安安静静地搞技术、敲代码,用技术思维解决技术问题,也用技术思维解决业务问题。我能实现业务功能就可以了,我不想也不愿花心思去搞懂业务“。这或许是许多研发者的心声,搞懂业务是产品、运营的事情,我是技术专注技术就行了。


曾几何时,我和千千万万的技术开发一样也是这么认为的。直到有一天我感觉卷不太动了(也有可能是年纪大了😂),我发现业界新的技术、框架层出不穷,技术之路永无止尽。而且也见证过一些技术牛人随时被下岗(有一位很厉害的架构师曾经是我下属),我突然觉得:技术思维很重要,但只动技术不懂业务你就随时可替代。


毕竟,任何技术都是要为业务服务的,只有有市场的业务才能活下来,只有活下来的业务才能让公司养活技术团队。脱离市场(业务)单纯只靠技术养活团队的毕竟是极少数(行业技术推动者)。


举两个鲜活的例子,我公司之前有两个只专注技术解决问题的团队:一个是infra,一个是data。前者负责公司基础设施建设,后者负责大数据处理。他们团队对也公司各领域业务都不熟,在公司业务还不错的情况下,是有足够资源养活的。但这两年公司业绩下滑、股价大跌,最终导致大规模裁员,首先开刀的就是这两个团队。因为纯技术给公司带来不了业务收益。


这是我入行十年的一些感悟,希望能帮助更多新人在这个行业中更好地成长。无论你选择的道路如何,保持对技术的热情,同时不断拓展自己的眼界,用心去创造价值,你将能在这个变化多端的行业中持续成长,迎接未来的挑战。希望这些感悟能够为你们的职业发展提供一些指引,

作者:程序员斌少
来源:juejin.cn/post/7271542820807442487
少踩一些坑,走得更加坚定。加油!

收起阅读 »

低增长的互联网意味着什么

今天想跟大家分享一下,低增长的互联网意味着什么?那提到低增长,那不得不提在互联网的高增长。1987年9月20日,西方世界第一次通过互联网收到了中国的来信。 众所周知,互联网在中国其实发展了25年左右,在这25年里面,互联网的大部分应用和业务都是处于高增长模式...
继续阅读 »

今天想跟大家分享一下,低增长的互联网意味着什么?那提到低增长,那不得不提在互联网的高增长。1987年9月20日,西方世界第一次通过互联网收到了中国的来信。



众所周知,互联网在中国其实发展了25年左右,在这25年里面,互联网的大部分应用和业务都是处于高增长模式。这个高增长模式主要指的是用户量,订单量,交易量,这些核心的指标都呈指数上升的阶段。


于是我们看到了很多很多令人惊奇和咋舌的情况,微信用户数从零个到几万到几百万到几亿。淘宝的订单量从几十百到几百万到几个亿。我们看到了无数的APP应用,从零快速积累到百万,甚至突破几个亿。


在这个过程中,我们会发现各行各业涌现出来了各种各样的公司和APP应用,我们也看到了无数的公司敲响了纳斯达克上市的钟声。互联网也不断利用规模效应来创造财富神话,我们看到身边的人,同行的人都能够快速的拿着股票和期权成为了百万千万富翁。


我毕业的时候,刚好2015年,我从中国科学技术大学硕士毕业拿到了阿里巴巴的Offer,而正是阿里巴巴上市之后,整个园区里充斥着财富自由的声音,虽然我啥都没有,但是还是沉浸其中,仿佛我也可以通过努力,在短短几年内和他们一样,可以过上不曾想过的生活。


很多牛人也轻轻松松的成为了创始人或者联合创始人,一遍高谈阔润,一遍指点江山。一边聊着增长飞轮,一边喝着咖啡,一边畅想未来。


突然疫情来了,突然人口红利见顶了,突然出生率断崖下跌,突然国际环境一片变差。


一切的一切,貌似美梦想来的那种感觉,仿佛依然不相信,说好的百年企业,说好的数字经济,说好的财富自由,怎么就突然变了。眼见他起高楼,眼见他宴宾客,眼见他楼塌了。古语的一句句撼动心魄的词句离我们如此之近。于是,我们不想看到的裁员、股市腰斩、失业率增高,一幕幕我们看不到的东西,全部浮现了出来。


于是在互联网程序员内心对于造富的神话还没有偃旗息鼓的时候,我们突然就见证了世界的巨变。


于是不得不承认,在2022年的今天,我们互联网进入了低增长时代。


那么低增长时代意味着什么?我们要从中里面能吸取什么样的教训?我们要如何去迎接低增长时代?


有一句话说得非常好,我们人类从历史里面学到了唯一的教训,就是不会吸取任何教训。世界上每一个生命,每一个事物,每个经济现象都有自己的周期。没有任何一个东西是可以无限增长的,自然界不允许这么牛逼的存在,互联网也是一样。


也就是互联网在成立和爆发的第一天,我们就应该能预知到互联网一定会走向平稳,甚至走向衰亡。可是人们眨眼几十年的时间,我们往往会被社会裹挟着往前面前进,我们甚至不知道自己一无所知。在我们猝不及防的时间,想不到低增长就来了,我们在不知不觉中进入了存量时代。只是,来得太快,我们还没有来得及反应。


那低增长到底意味着什么?我认为我们要做好下面的准备。


首先是精细化运营。我们进入了存量时代,我们不得不进入精细化运营。也就是高增长的用户没了,我们所面对的只有不断流失的存量用户。在高增长的时代,野蛮地生长,我们有些时候不会在乎老用户的体验,我们一直把重金都砸在新业务上面,力求能继续做大规模以吸引投资者,以讲一个更加美好的故事。可是到了存量时代,我们不得不精准的运营我们存量的用户。我们不像以前那样会砸大笔的钱投入大量的人力去做一些新的产品,反而我们应该进入对于存量用户的存量功能的精细化的运营。只有迎合了这一波存量用户的需求,我们才可以防止用户流失,我们才能够保持到那些仅有的利润。


其次就是降本增效。互联网进入了存量时代,估值逻辑就变化了,原来我们以规模为衡量,只要规模越多,不管盈利还是非盈利,我们都能够拿到巨额的投资。有一句话说得非常好,早期互联网员工的工资不是公司发的,而是投资人发的。所以就算在公司里面你的业务亏损得再多,但是员工依然能拿到非常高的工资,这就是互联网高增长下的底层逻辑。而到了低增长的时代,投资人的钱已经没有了,那么发工资的主角变成了企业。我们知道有老板来发工资,你一定会知之,比较一定是要发出去十块钱,他就要至少赚回一百块钱。因为毕竟投资人的钱等于从天上掉下来的,而企业主的钱主要是从老板的身上一点一点割下来的,多花一块钱,老板都肉疼。



所以以前有免费的咖啡,水果茶,不好意思,现在没有了,原来有一年一到两次的团建,不好意思,现在也没有了。原来奖金动不动十个月,20个月起,不好意思,现在撑死了只有三个月。以前随便出差全国飞,只要有增长就有审批额度,不好意思,现在能不出差尽量不出差,能远程视频就远程视频,实在搞不定的这个客户也可以考虑不要了。


原来是粗放型的管理,我们只在乎整体的增长和氛围,现在不好意思,我们要提高效率,我们希望员工的每一分钱都花在刀刃上,每一点时间都用在了对客户的架子上。


当然,降本增效,不得不说的是,裁员是最快最高效的手段。相信未来大厂特别是亏损的互联网公司会持续进入滚动式裁员,毕竟大厂员工的人均成本就超过百万,卖出多少产品才能有百万的收入呢?我想这点老板、财务和HR都门清的。资本家也是嗜血的,当然也是怕失血的。


最后高质量增长和价值创造。最后现实情况我们不得不去考虑高质量增长和价值创造。原来我们可能采取竭泽而渔的方式,我们撒钱到处撒币以获得最高的增长。而现在我们更要看重高质量增长和价值创造。所谓高质量增长,我们要求我们每付出的一分钱,付出的每一分劳动,我们要收获有价值,有质量的客户。


也就是不付费的客户或者薅羊毛的客户,不好意思,你再怎么投诉我,我都不欢迎你。或者说有一些我们长期在原来的免费补贴时代客户,到现在可能就成为了垃圾客户。


所谓价值创造,就是我不是为了能够获取你的关注,我确实要给你创造实实在在的价值,你才会为我的服务买单。所以我觉得也是一个好事,当然这也意味着我们可能整体包括企业也进入了一种躺平时代。我觉得大部分有追求的企业应该都会思考这个问题,能否安静下来做一些长期主义的事情。也许很多企业就在这个时候被迫精耕细作,成为百年老店。


当然还不得不要说这一点,互联网的蓬勃发展结束了,既然进入了真正的增长时代,进入了精细化运营时代,不好意思,我可能不需要这么多员工,我也不会给你发这么高的工资,我们只是成为一个普普通通,用数字化创造实际价值的公司。


但是中国的技术有这么多,还会有很多人前赴后继地走在这条路上。所以在我们中国整个产业没有转型的时候我们有这么多的人才。我想在一段时间内可能都经历过一阵阵痛。当然站在好处的地方是当前的这种业务机构会倒逼人才往其他行业流走,流向原来根本招不到人的行业,比如说制造业、工厂,甚至是其他的服务业。


当然,我们先回过头来,当前的程序员该怎么办?和我们说的增长模式一样,我们这些程序员也会变成了存量的程序员。在存量时代意味着我们持续要去内卷,去竞争存量的岗位。我相信竞争也会更加的激烈,当然等我们年纪越来越大的时候,依然创造不出市场的价值的时候,我相信可能会被逐渐淘汰。所以我觉得内心也要结合准备降薪,甚至是转行,甚至是寻找下一份职业的准备。



当然站在长远的角度来看未必不是好事,至少我们薪资降下来了,我们的时间也多出来了,我们更能去好好的去问问自己生命的意义什么?虽然高薪的岗位会越来越少,但是我觉得大家也不用焦虑,毕竟底薪的岗位一大堆。而且自古以来大部分人赚不到什么钱,是一个常态。


作者:ali老蒋
来源:juejin.cn/post/7270117041525129257

收起阅读 »

pdf为什么不能被修改

web
PDF简介 PDF是Portable Document Format 的缩写,可翻译为“便携文件格式”,由Adobe System Incorporated 公司在1992年发明。 PDF文件是一种编程形式的文档格式,它所有显示的内容,都是通过相应的操作符进...
继续阅读 »

PDF简介



  • PDF是Portable Document Format 的缩写,可翻译为“便携文件格式”,由Adobe System Incorporated 公司在1992年发明。

  • PDF文件是一种编程形式的文档格式,它所有显示的内容,都是通过相应的操作符进行绘制的。

  • PDF基本显示单元包括:文字,图片,矢量图,图片

  • PDF扩展单元包括:水印,电子署名,注释,表单,多媒体,3D

  • PDF动作单元:书签,超链接(拥有动作的单元有很多个,包括电子署名,多媒体等等)


PDF的优点



  • 一致性:在所有可以打开PDF的机器上,展示的效果是完全一致,不会出现段落错乱、文字乱码这些排版问题。尤其是文档中,本身可以嵌入字体,避免了客户端没有对应字体,而导致文字显示不一致的问题。所以,在印刷行业,绝大多数用的都是PDF格式。

  • 不易修改:用过PDF文件的人,都会知道,对已经保存之后的PDF文件,想要进行重新排版,基本上就不可能的,这就保证了从资料源发往外界的资料,不容易被篡改。

  • 安全性:PDF文档可以进行加密,包括以下几种加密形式:文档打开密码,文档权限密码,文档证书密码,加密的方法包括:RC4,AES,通过加密这种形式,可以达到资料防扩散等目的。

  • 不失真:PDF文件中,使用了矢量图,在文件浏览时,无论放大多少倍,都不会导致使用矢量图绘制的文字,图案的失真。

  • 支持多种压缩方式:为了减少PDF文件的size,PDF格式支持各种压缩方式:asciihex,ascii85,lzw,runlength,ccitt,jbig2,jpeg(DCT),jpeg2000(jpx)

  • 支持多种印刷标准:支持PDF-A,PDF-X


PDF格式


根据PDF官方指南,理解PDF格式可以从四个方面下手——Objects(对象)、File structure(物理文件结构)、Document structure(逻辑文件结构)、Content streams(内容流)。


对象


物理文件结构




  • 整体上分为文件头(Header)、对象集合(Body)、交叉引用表(Xref table)、文件尾(Trailer)四个部分,结构如图。修改过的PDF结构会有部分变化。




  • 未经修改






编辑


img




  • 经修改






编辑


img


文件头



  • 文件头是PDF文件的第一行,格式如下:


%PDF-1.7

复制



  • 这是个固定格式,表示这个PDF文件遵循的PDF规范版本,解析PDF的时候尽量支持高版本的规范,以保证支持大多数工具生成的PDF文件。1.7版本支持1.0-1.7之间的所有版本。


对象集合



  • 这是一个PDF文件最重要的部分,文件中用到的所有对象,包括文本、图象、音乐、视频、字体、超连接、加密信息、文档结构信息等等,都在这里定义。格式如下:


2 0 obj
...
end obj

复制



  • 一个对象的定义包含4个部分:前面的2是对象序号,其用来唯一标记一个对象;0是生成号,按照PDF规范,如果一个PDF文件被修改,那这个数字是累加的,它和对象序号一起标记是原始对象还是修改后的对象,但是实际开发中,很少有用这种方式修改PDF的,都是重新编排对象号;obj和endobj是对象的定义范围,可以抽象的理解为这就是一个左括号和右括号;省略号部分是PDF规定的任意合法对象。

  • 可以通过R关键字来引用任何一个对象,比如要引用上面的对象,可以使用2 0 R,需要主意的是,R关键字不仅可以引用一个已经定义的对象,还可以引用一个并不存在的对象,而且效果就和引用了一个空对象一样。

  • 对象主要有下面几种

  • booleam 用关键字true或false表示,可以是array对象的一个元素,或dictionary对象的一个条目。也可以用在PostScript计算函数里面,做为if或if esle的一个条件。

  • numeric


包括整形和实型,不支持非十进制数字,不支持指数形式的数字。例: 1)整数 123 4567 +111 -2 范围:正2的31次方-1到负的2的31次方 2)实数 12.3 0.8 +6.3 -4.01 -3. +.03 范围:±3.403 ×10的38次方 ±1.175 × 10的-38次方



  • 注意:如果整数超过表示范围将转化成实数,如果实数超过范围就会出错

  • string


由一系列0-255之间的字节组成,一个string总长度不能超过65535.string有以下两种方式:



  • 十六进制字串 由<>包含起来的一个16进制串,两位表示一个字符,不足两位用0补齐。例: \ 表示AA和BB两个字符 \ 表示AA和B0两个字符

  • 直接字串 由()包含起来的一个字串,中间可以使用转义符"/"。例:(abc) 表示abc (a//) 表示a/ 转义符的定义如下:


转义字符含义
/n换行
/r回车
/t水平制表符
/b退格
/f换页(Form feed (FF))
/(左括号
/)右括号
//反斜杠
/ddd八进制形式的字符



  • 对象类别(续)




  • name 由一个前导/和后面一系列字符组成,最大长度为127。和string不同的是,name是不可分割的并且是唯一的,不可分割就是说一个name对象就是一个原子,比如/name,不能说n就是这个name的一个元素;唯一就是指两个相同的name一定代表同一个对象。从pdf1.2开始,除了ascii的0,别的都可以用一个#加两个十六进制的数字表示。例: /name 表示name /name#20is 表示name is /name#200 表示name 0




  • array 用[]包含的一组对象,可以是任何pdf对象(包括array)。虽然pdf只支持一维array,但可以通过array的嵌套实现任意维数的array(但是一个array的元素不能超过8191)。例:[549 3.14 false (Ralph) /SomeName]




  • Dictionary 用"<<"和">>"包含的若干组条目,每组条目都由key和value组成,其中key必须是name对象,并且一个dictionary内的key是唯一的;value可以是任何pdf的合法对象(包括dictionary对象)。例: << /IntegerItem 12 /StringItem (a string) /Subdictionary << /Item1 0.4 /Item2 true /LastItem (not!) /VeryLastItem (OK) >> >>




  • stream 由一个字典和紧跟其后面的一组关键字stream和endstream以及这组关键字中间包含一系列字节组成。内容和string很相似,但有区别:stream可以分几次读取,分开使用不同的部分,string必须作为一个整体一次全部读取使用;string有长度限制,但stream却没有这个限制。一般较大的数据都用stream表示。需要注意的是,stream必须是间接对象,并且stream的字典必须是直接对象。从1.2规范以后,stream可以以外部文件形式存在,这种情况下,解析PDF的时候stream和endstream之间的内容就被忽略掉。例: dictionary stream…data…endstreamstream字典中常用的字段如下: 字段名类型值Length整形(必须)关键字stream和endstream之间的数据长度,endstream之前可能会有一个多余的EOL标记,这个不计算在数据的长度中。Filter名字 或 数组(可选)Stream的编码算法名称(列表)。如果有多个,则数组中的编码算法列表顺序就是数据被编码的顺序。DecodeParms字典 或 数组(可选)一个参数字典或由参数字典组成的一个数组,供Filter使用。如果仅有一个Filter并且这个Filter需要参数,除非这个Filter的所有参数都已经给了默认值,否则的话 DecodeParms必须设置给Filter。如果有多个Filter,并且任意一个Filter使用了非默认的参数, DecodeParms 必须是个数组,每个元素对应一个Filter的参数列表(如果某个Filter无需参数或所有参数都有了默认值,就用空对象代替)。如果没有Filter需要参数,或者所有Filter的参数都有默认值,DecodeParms 就被忽略了。F文件标识(可选)保存stream数据的文件。如果有这个字段, stream和endstream就被忽略,FFilter将会代替Filter, FDecodeParms将代替DecodeParms。Length字段还是表示stream和endstream之间数据的长度,但是通常此刻已经没有数据了,长度是0.FFilter名字 或 字典(可选)和filter类似,针对外部文件。FDecodeParms字典 或 数组(可选)和DecodeParams类似,针对外部文件。




  • Stream的编码算法名称(列表)。如果有多个,则数组中的编码算法列表顺序就是数据被编码的顺序。且需要被编码。编码算法主要如下:






编辑切换为居中


img


编码可视化主要显示为乱码,所以提供了隐藏信息的机会,如下图的steam内容为乱码。




编辑切换为居中


img



  • NULL 用null表示,代表空。如果一个key的值为null,则这个key可以被忽略;如果引用一个不存在的object则等价于引用一个空对象。


交叉引用表



  • 交叉引用表是PDf文件内部一种特殊的文件组织方式,可以很方便的根据对象号随机访问一个对象。其格式如下:


xref
0 1
0000000000 65535 f
4 1
0000000009 00000 n
8 3
0000000074 00000 n
0000000120 00000 n
0000000179 00000 n

复制



  • 其中,xref是开始标志,表示以下为一个交叉引用表的内容;每个交叉引用表又可以分为若干个子段,每个子段的第一行是两个数字,第一个是对象起始号,后面是连续的对象个数,接着每行是这个子段的每个对象的具体信息——每行的前10个数字代表这个这个对象相对文件头的偏移地址,后面的5位数字是生成号(用于标记PDF的更新信息,和对象的生成号作用类似),最后一位f或n表示对象是否被使用(n表示使用,f表示被删除或没有用)。上面这个交叉引用表一共有3个子段,分别有1个,1个,3个对象,第一个子段的对象不可用,其余子段对象可用。


文件尾



  • 通过trailer可以快速的找到交叉引用表的位置,进而可以精确定位每一个对象;还可以通过它本身的字典还可以获取文件的一些全局信息(作者,关键字,标题等),加密信息,等等。具体形式如下:


trailer
<<
key1 value1
key2 value2
key3 value3

>>
startxref
553
%%EOF

复制



  • trailer后面紧跟一个字典,包含若干键-值对。具体含义如下:


值类型值说明
Size整形数字所有间接对象的个数。一个PDF文件,如果被更新过,则会有多个对象集合、交叉引用表、trailer,最后一个trailer的这个字段记录了之前所有对象的个数。这个值必须是直接对象。
Prev整形数字当文件有多个对象集合、交叉引用表和trailer时,才会有这个键,它表示前一个相对于文件头的偏移位置。这个值必须是直接对象。
Root字典Catalog字典(文件的逻辑入口点)的对象号。必须是间接对象。
Encrypt字典文档被保护时,会有这个字段,加密字典的对象号。
Info字典存放文档信息的字典,必须是间接对象。
ID数组文件的ID


  • 上面代码中的startxref:后面的数字表示最后一个交叉引用表相对于文件起始位置的偏移量

  • %%EOF:文件结束符


逻辑文件结构




编辑切换为居中


img


catalog根节点



  • catalog是整个PDF逻辑结构的根节点,这个可以通过trailer的Root字段定位,虽然简单,但是相当重要,因为这里是PDF文件物理结构和逻辑结构的连接点。Catalog字典包含的信息非常多,这里仅就最主要的几个字段做个说明。 字段类型值Typename(必须)只能为Pages 。Parentdictionary(如果不是catalog里面指定的跟节点,则必须有,并且必须是间接对象) 当前节点的直接父节点。Kidsarray(必须)一个间接对象组成的数组,节点可能是page或page tree。Countinteger(必须) page tree里面所包含叶子节点(page 对象)的个数。从以上字段可以看出,Pages最主要的功能就是组织所有的page对象。Page对象描述了一个PDF页面的属性、资源等信息。Page对象是一个字典,它主要包含一下几个重要的属性:

  • Pages字段 这是个必须字段,是PDF里面所有页面的描述集合。Pages字段本身是个字典,它里面又包含了一下几个主要字段:


字段类型
Typename(必须)必须是Page。
Parentdictionary(必须;并且只能是间接对象)当前page节点的直接父节点page tree 。
LastModifieddate(如果存在PieceInfo字段,就必须有,否则可选)记录当前页面被最后一次修改的日期和时间。
Resourcesdictionary(必须; 可继承)记录了当前page用到的所有资源。如果当前页不用任何资源,则这是个空字典。忽略所有字段则表示继承父节点的资源。
MediaBoxrectangle(必须; 可继承)定义了要显示或打印页面的物理媒介的区域(default user space units)
CropBoxrectangle(可选; 可继承)定义了一个可视区域,当前页被显示或打印的时候,它的内容会被这个区域裁剪。默认值就是 MediaBox。
BleedBoxrectangle(可选) 定义了一个区域,当输出设备是个生产环境( production environment)的时候,页面显示的内容会被裁剪。默认值是 CropBox.
Contentsstream or array(可选) 描述页面内容的流。如果这个字段缺省,则页面上什么也不会显示。这个值可以是一个流,也可以是由几个流组成的一个数组。如果是数组,实际效果相当于所有的流是按顺序连在一起的一个流,这就允许PDF生成的时候可以随时插入图片或其他资源。流之间的分割只是词汇上的一个分割,并不是逻辑上或者组织形式的切割。
Rotateinteger(可选; 可继承) 顺时钟旋转的角度数,这个必须是90的整数倍,默认是0。
Thumbstream(可选)定义当前页的缩略图。
Annotsarray(可选) 和当前页面关联的注释。
Metadatastream(可选) 当前页包含的元数据。


  • 一个简单例子:


3 0 obj
<< /Type /Page
/Parent 4 0 R
/MediaBox [ 0 0 612 792 ]
/Resources <</Font<<
/F3 7 0 R /F5 9 0 R /F7 11 0 R
>>
/ProcSet [ /PDF ]
>>
/
Contents 12 0 R
/Thumb 14 0 R
/Annots [ 23 0 R 24 0 R]
>>
endobj

复制



  • Outlines字段 Outline是PDF里面为了方便用户从PDF的一部分跳转到另外一部分而设计的,有时候也叫书签(Bookmark),它是一个树状结构,可以直观的把PDF文件结构展现给用户。用户可以通过鼠标点击来打开或者关闭某个outline项来实现交互,当打开一个outline时,用户可以看到它的所有子节点,关闭一个outline的时候,这个outline的所有子节点会自动隐藏。并且,在点击的时候,阅读器会自动跳转到outline对应的页面位置。Outlines包含以下几个字段: 字段类型值Typename(可选)如果这个字段有值,则必须是Outlines。Firstdictionary(必须;必须是间接对象) 第一个顶层Outline item。Lastdictionary(必须;必须是间接对象)最后一个顶层outline item。Countinteger(必须)outline的所有层次的item的总数。

  • Outline是一个管理outline item的顶层对象,我们看到的,其实是outline item,这个里面才包含了文字、行为、目标区域等等。一个outline item主要有一下几个字段: 字段类型值Titletext string(必须)当前item要显示的标题。Parentdictionary(必须;必须是间接对象) outline层级中,当前item的父对象。如果item本身是顶级item,则父对象就是它本身。Prevdictionary(除了每层的第一个item外,其他item必须有这个字段;必须是间接对象)当前层级中,此item的前一个item。Nextdictionary(除了每层的最后一个item外,其他item必须有这个字段;必须是间接对象)当前层级中,此item的后一个item。Firstdictionary(如果当前item有任何子节点,则这个字段是必须的;必须是间接对象) 当前item的第一个直接子节点。Lastdictionary(如果当前item有任何子节点,则这个字段是必须的;必须是间接对象) 当前item的最后一个直接子节点。Destname,byte string, or array(可选; 如果A字段存在,则这个不能被会略)当前的outline item被激活的时候,要显示的区域。Adictionary(可选; 如果Dest 字段存在,则这个不能被忽略)当前的outline item被激活的时候,要执行的动作。

  • URI字段 URI(uniform resource identifier),定义了文档级别的统一资源标识符和相关链接信息。目录和文档中的链接就是通过这个字段来处理的.

  • Metadata字段 文档的一些附带信息,用xml表示,符合adobe的xmp规范。这个可以方便程序不用解析整个文件就能获得文件的大致信息。

  • 其他 Catalog字典中,常用的字段一般有以下一些:


字段类型
Typename(必须)必须为Catalog。
Versionname(可选)PDF文件所遵循的版本号(如果比文件头指定的版本号高的话)。如果这个字段缺省或者文件头指定的版本比这里的高,那就以文件头为准。一个PDF生成程序可以通过更新这个字段的值来修改PDF文件版本号。
Pagesdictionary(必须并且必须为间接对象)当前文档的页面集合入口。
PageLabelsnumber tree(可选) number tree,定义了页面和页面label对应关系。
Namesdictionary(可选)文档的name字典。
Destsdictionary(可选;必须是间接对象)name和相应目标对应关系字典。
ViewerPreferencesdictionary(可选)阅读参数配置字典,定义了文档被打开时候的行为。如果缺省,则使用阅读器自己的配置。
PageLayoutname(可选) 指定文档被打开的时候页面的布局方式。SinglePageDisplay 单页OneColumnDisplay 单列TwoColumnLeftDisplay 双列,奇数页在左TwoColumnRightDisplay 双列,奇数页在右TwoPageLeft 双页,奇数页在左TwoPageRight 双页,奇数页在右缺省值: SinglePage.
PageModename(可选) 当文档被打开时,指定文档怎么显示Use 目录和缩略图都不显示UseOutlines 显示目录UseThumbs 显示缩略图FullScreen 全屏模式,没有菜单,任何其他窗口UseOC 显示Optional content group 面板UseAttachments显示附件面板缺省值: Use.
Outlinesdictionary(可选;必须为间接对象)文档的目录字典
Threadsarray(可选;必须为间接对象)文章线索字典组成的数组。
OpenActionarray or dictionary(可选) 指定一个区域或一个action,在文档打开的时候显示(区域)或者执行(action)。如果缺省,则会用默认缩放率显示第一页的顶部。
AAdictionary(可选)一个附加的动作字典,在全局范围内定义了响应各种事件的action。
URIdictionary(可选)一个URI字典包含了文档级别的URI action信息。
AcroFormdictionary(可选)文档的交互式form (AcroForm)字典。
Metadatastream(可选;必须是间接对象)文档包含的元数据流。

具体组成


1 Header部分


PDF文件的第一行应是由5个字符“%PDF-”后跟“1.N”的版本号组成的标题,其中N是0到7之间的数字。例如下面的:


%PDF–1.0   %PDF–1.1   %PDF–1.2   %PDF–1.3   %PDF–1.4   %PDF–1.5   %PDF–1.6   %PDF–1.7


从PDF 1.4开始,应使用文档目录字典中的Version 条目(通过文件Trailer部分的Root条目指定版本),而不是标题中指定的版本。


2 Body部分


PDF文件的正文应由表示文件内容的一系列间接对象组成,例如字体、页面和采样图像。从PDF 1.5开始,Body还可以包含对象流,每个对象流包含一系列间接对象。例如下面这样:


1 0 obj
<< /Type /Catalog
  /Outlines 2 0 R
  /Pages 3 0 R
>>
endobj
2 0 obj
<< /Type Outlines
  /Count 0
>>
endobj
3 0 obj
<< /Type /Pages
/Kids [4 0 R]
/Count 1
>>
endobj
4 0 obj
<< /Type /Page
  /Parent 3 0 R
  /MediaBox [0 0 612 792]
  /Contents 5 0 R
  /Resources << /ProcSet 6 0 R >>
>>
endobj
5 0 obj
<< /Length 35 >>
stream
  …Page-marking operators…
endstream
endobj
6 0 obj
[/PDF]
endobj

3 Cross-Reference Table 交叉引用表部分


交叉引用表包含文件中间接对象的信息,以便允许对这些对象进行随机访问,因此无需读取整个文件即可定位任何特定对象。


交叉引用表以xref开始,紧接着是一个空格隔开的两个数字,然后每一行就是一个对象信息:


xref
0 7
0000000000 65535 f
0000000009 00000 n
0000000074 00000 n
0000000120 00000 n
0000000179 00000 n
0000000300 00000 n
0000000384 00000 n

上面第二行中的两个数字“0 7”,0表示下面的对象从0号对象开始,7表示对象的数量,也就是说表示从0到6共7个对象。


每行一个对象信息的格式如下:


nnnnnnnnnn ggggg n eol


  • nnnnnnnnnn 长度10个字节,表示对象在文件的偏移地址;

  • ggggg 长度5个字节,表示对象的生成号;

  • n (in-use)表示对象被引用,如果此值是f (free),表示对象未被引用;

  • eol 就是回车换行


交叉引用表中的第一个编号为0的对象始终是f(free)的,并且生成号为65535;除了编号0的对象外,交叉引用表中的所有对象最初的生成号应为0。删除间接对象时,应将其交叉引用条目标记为“free”,并将其添加到free条目的链表中。下次创建具有该对象编号的对象时,条目的生成号应增加1,最大生成号为65535;当交叉引用条目达到此值时,它将永远不会被重用。


交叉引用表也可以是这样的:


xref
0 1
0000000000 65535 f
3 1
0000025325 00000 n
23 2
0000025518 00002 n
0000025635 00000 n
30 1
0000025777 00000 n

[


4 Trailer部分


PDF阅读器是从PDF的尾部开始解析文件的,通过Trailer部分能够快速找到交叉引用表和某些特殊对象。如下所示:


trailer
<< /Size 7
/Root 1 0 R
>>
startxref
408
%%EOF

文件的最后一行应仅包含文件结束标记%%EOF。关键字startxref下面的数字表示最后一个交叉引用表的xref关键字开头的字节偏移量。trailer和startxref之间是尾部字典,由包含在双尖括号(<<…>>)中的键值对组成。


为什么不容易被修改



  1. 文件结构和编码:PDF文件采用了一种复杂的文件结构和编码方式,这使得在未经授权的情况下修改PDF文件变得非常困难。PDF文件采用二进制格式存储,而不是像文本文件那样以可读的形式存储。这导致无法直接编辑和修改PDF文件,需要使用特定的软件或工具。

  2. 加密和安全特性:PDF文件可以使用密码进行加密和保护,以确保只有授权的用户才能进行修改。加密可以防止未经授权的访问和修改,使得修改PDF文件变得更加困难和复杂。

  3. 文件签名和验证:PDF文件可以使用数字签名进行验证,以确保文件的完整性和可信性。数字签名可以证明文件的来源和真实性,一旦数字签名验证失败,即表明文件已被篡改。

  4. 版本兼容性和规范:PDF格式被国际标准化组织(ISO)制定为ISO 32000标准。这个标准确保了不同版本和软件之间的PDF文件的兼容性,并定义了丰富的功能和规范,包括页面布局、字体嵌入、图形和图像处理等。这些严格的规范使得对PDF文件进行修改变得复杂和具有挑战性。


如何修改pdf


因为pdf的局限性是无法进行修改的,所以我们只能通过将他转换为其他类型的文件进行查看修改,当然,转换的过程不可能是百分百完美的进行转换的。


下面推荐这俩个可以进行转换的网站


PDF转换成Word在线转换器 - 免费 - CleverPDF


Convert PDF to Word for free |

Smallpdf.com

收起阅读 »

人情世故职场社会生存实战篇(四)

人情人情世故职场社会生存实战篇(一)人情人情世故职场社会生存实战篇(二)人情人情世故职场社会生存实战篇(三) 31、问:领导推我得了第一,拿了5000奖金,给领导送多少合适? 答:钱从哪儿来的,还到哪儿去,这叫饮水思源。给他买5000元的华子,他100%要,...
继续阅读 »

人情人情世故职场社会生存实战篇(一)
人情人情世故职场社会生存实战篇(二)
人情人情世故职场社会生存实战篇(三)



31、问:领导推我得了第一,拿了5000奖金,给领导送多少合适?


答:钱从哪儿来的,还到哪儿去,这叫饮水思源。给他买5000元的华子,他100%要,然后你提个非常小非常小的要求就0K。比如请假3天,他说0K,然后就心安理得的收了。你回来了,他会想办法安排你,指点你的。然后他送你一句话,你说:这句话价值连城,一个亿都买不到。领导高兴啊,你看:我5000就卖给你了,我还亏了呢。然后,他会主动给自己加分,你看吧,我还是一个好领导。这样你既满足了领导的物质需求,同时还满足了领导的精神需求。领导觉得你知恩图报,此后有机会便开始提拔你了。


32、问:你说送礼交大哥真的有用吗,有的人收了不办事,会不会搞不好倾家荡产啊?


答:我从来没听说过:谁谁谁送礼倾家荡产的。前辈给我讲过他发家的故事:十几年前他给一个工程的大哥包20万红包,赚了180万,他又拿出100万分给了大哥,大哥说:那边的开发区也归你管了,就那两年时间前辈赚了1000多个,而别人只认为他是命好。


33、问:朋友给我介绍个活,挣了5千多,我说:谢谢啊,下次请你吃饭。但最近很少跟我联系,也不介绍活给我了是怎么回事?


答:如果你挣了五千,给他三千,你觉得你的活还会少吗?进四出六,不要怕吃亏,只要你给他转了,你天天都有活干!自己一人占尽利益,将没有长期合作!


34、问:公司领导生病了,我要不要过去看一下,去了有没有什么用?


答:前年的时候,前辈病了,小李发信息说了一些安慰前辈的话。我呢带了2条烟,去了协和医院,握着前辈的手,说了很多暖心的话。此后,我和前辈无话不谈。尽管小李跟了前辈很多年,但前辈遇到好事儿还是习惯性叫上我,前后几年带事我挣了至少120个朝上。


35、问:去年挣了点钱,想回家乡修个路啥的,你看有没有必要?


答:前年捐了50万在村里修路灯,从那以后,村里的人每天都在找我,这个生病了,那个想买房,还有个没钱交学费的。富贵不归故里。习惯性装穷,习惯性示弱,不信你看大衣哥。


36、问:昨天和朋友吹牛,说一年赚几百万,然后今天给我发信息,我没敢回,是不是找我借钱啊?


答:前年一场饭局,朋友问:一年赚多少?我说400多万。朋友说厉害厉害。第二天,朋友找我借钱,张口就是170万,而且还跪在我家,一直给我磕头。我给了他40万,这40万他100%不会还了。他是送外卖的,一月能赚多少钱我不知道,反正这笔借出去的钱收不回了。


37、问:昨天我对一个大哥说,哥你一年带我赚个几百万就行了,但是大哥直接转移话题是为什么?


答:一个外地的小朋友,非要见个前辈,我拗不过他,便约了他出来。饭局上,这个小朋友一句话就把局面给破坏了。他对前辈说:张叔,我要跟你混,你年赚2000万,我能年赚1000万就行。大家习惯给人家面子,前辈说:兄弟之才,绝对在我之上,来,喝喝喝.....


38、问:我公司干了几年了,不亏钱,但为什么就是做不大?


答:赚小钱,靠的是能力,赚大钱,靠的是关系,靠的是背景。所有的保险公司,都是关系的结果,跟市场运营没有一毛钱关系,赚钱,就是找个大哥,当他的夜壶,这没有什么好丢人的,找不到夜壶才丢人。


39、问:我打算开个实体店,请教一下什么叫会做生意,什么叫情商?


答:前段时间我在一家饭店吃饭,打碎个杯子,老板说:影响您用餐了,没有伤着吧。结账时,我多给了老板100元。好巧不巧,前两天我又去一家小饭店吃饭,打烂一个勺子,老板说:一个勺子100元。此后,这家饭店,我再没去过。


40、问:带我的大哥到我家喝茶,好像是看上我那副画了,正好大哥手里有个项目你说这是个机会吗?


答:前辈很喜欢我的摩托车,我就把摩托车借给他开了几天,然后他问我从哪儿买的,我说:叔你喜欢的话,我就送你了。前辈说:那谢谢了。随后没几天他告诉我:你给你张叔拿2万,我给他打过招呼了,这个绿化工程,由你来做。赚钱不赚钱看你自己。我说:叔,挣不挣钱都是个机会,到时候找你喝茶。



作者:公z号_纵横潜规则
来源:juejin.cn/post/7269588899499704332

收起阅读 »

Swift 中怎样更快地 reduce

iOS
在 Swift 中,对于集合类型,Swift 标准库提供了若干方便的方法,可以对数据进行处理,其中一个比较常见的就是 reduce。reduce 这个单词,通过查阅字典,可以发现其有“简化、归纳”的意思,也就是说,可以用 reduce 把一组数据归纳为一个数据...
继续阅读 »

在 Swift 中,对于集合类型,Swift 标准库提供了若干方便的方法,可以对数据进行处理,其中一个比较常见的就是 reduce。reduce 这个单词,通过查阅字典,可以发现其有“简化、归纳”的意思,也就是说,可以用 reduce 把一组数据归纳为一个数据,当然这个一个数据也可以是一个数组或任何类型。


比较常见的 reduce 使用案例,例如:


求和:

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0, +)
print(sum) // 输出 15

字符串拼接:

let words = ["hello", "world", "how", "are", "you"]
let sentence = words.reduce("", { $0 + " " + $1 })
print(sentence) // 输出 " hello world how are you"

两个 reduce API


观察 reduce 方法的声明,会发现有两个不同的 API,一个是 reduce 一个是 reduce(into:),他们的功能是一样的,但是却略有不同。


reduce 方法的函数签名如下:

func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) throws -> Result) rethrows -> Result

该方法接收一个初始值和一个闭包作为参数,该闭包将当前的结果值和集合中的下一个元素作为输入,并返回一个新的结果值。reduce 方法依次迭代集合中的每个元素,并根据闭包的返回值更新结果值,最终返回最终结果值。


还是回到最简单的求和上来,下面的代码使用 reduce 方法计算一个数组中所有元素的总和:

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0, { $0 + $1 })
print(sum) // 输出 15

reduce(into:) 方法的函数签名如下:

func reduce<Result>( into initialResult: __owned Result, _ updateAccumulatingResult: (inout Result, Element) throws -> Void ) rethrows -> Result

该方法接收一个初始值和一个闭包作为参数,该闭包将当前的结果值和集合中的下一个元素作为输入,并使用 inout 参数将更新后的结果值传递回去。reduce(into:) 方法依次迭代集合中的每个元素,并根据闭包的返回值更新结果值,最终返回最终结果值。


下面的代码使用 reduce(into:) 方法计算一个数组中所有元素的总和:

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(into: 0, { result, element in
result += element
})
print(sum) // 输出 15

可以看到,reduce(into:) 方法中闭包的参数使用了 inout 关键字,使得闭包内部可以直接修改结果值。这样可以避免不必要的内存分配和拷贝,因此在处理大量数据时,使用 reduce(into:) 方法可以提高性能。


观察源码


我们再通过观察源码证实这一结论


reduce 方法的源码实现如下:

public func reduce<Result>(
_ initialResult: Result,
_ nextPartialResult: (Result, Element) throws -> Result
) rethrows -> Result {
var accumulator = initialResult
for element in self {
accumulator = try nextPartialResult(accumulator, element)
}
return accumulator
}

可以发现这里有两处拷贝,一处是在 accumulator 传参给 nextPartialResult 时,一处是在把 nextPartialResult 的结果赋值给 accumulator 变量时,由于这里的 accumulator 的类型是一个值类型,每次赋值都会触发 Copy-on-Write 中的真正的拷贝。并且这两处拷贝都是在循环体中,如果循环的次数非常多,是会大大拖慢性能的。


再看 reduce(into:) 方法的源码:

func reduce<Result>( 
into initialResult: __owned Result,
_ updateAccumulatingResult: (inout Result, Element) throws -> Void
) rethrows -> Result {
var result = initialResult
for element in self {
try updateAccumulatingResult(&result, element)
}
return result
}

在方法的实现中,我们首先将 initialResult 复制到一个可变变量 result 中。然后,我们对序列中的每个元素调用 updateAccumulatingResult 闭包,使用 & 语法将 result 作为 inout 参数传递给该闭包。因此这里每次循环都是在原地修改 result 的值,并没有发生拷贝操作。


总结


因此,reduce 方法和 reduce(into:) 方法都可以用来将集合中的元素组合成单个值,但是对于会触发 Copy-on-Write 的类型来说, reduce(into:) 方法可以提供更好的性能。在实际使用中,应该根据具体情况选择合适的方法。


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

👨‍💻 14 个最佳免费编程字体

我们整天都在使用代码编辑器、终端和其他开发工具,使用一种让眼睛舒服的字体可以大大改善我们的工作效率。 这篇文章汇总了 14 个免费的等宽编程字体,包括了每个字体的介绍、评价和下载链接。 😝 分享几个好玩的 VSCode 主题 🗂 让你的 VSCode 文件图...
继续阅读 »

我们整天都在使用代码编辑器、终端和其他开发工具,使用一种让眼睛舒服的字体可以大大改善我们的工作效率。
这篇文章汇总了 14 个免费的等宽编程字体,包括了每个字体的介绍、评价和下载链接。


😝 分享几个好玩的 VSCode 主题


🗂 让你的 VSCode 文件图标更好看的10个文件图标主题


🌈 冷门但好看的 VSCode 主题推荐


1. Fira Code




我曾使用 Monaco 字体超过十年的时间,直到我遇到了 Fira Code。这个字体在 Github 上面有超过 53,600 个 star,它这么受欢迎是有原因的。字体作者 Nikita Prokopov 在连字符(Ligature)上花了很多功夫,连字符可以把单独的字符合并成单一的逻辑标记。Fira Code 是我现在最喜欢的字体。




(Fira Code 中的连字符)


下载链接 • Github链接


2. IBM Plex Mono




Plex 系列字体是在 IBM 使用了 50 多年的 Helvetica 字体之后,被创建出来作为替代品的。它有着非常优雅的斜体字体,以及非常清晰易读的字形。美中不足的是,它没有包含连字符。


下载链接 • Github链接


3. Source Code Pro




Source Code Pro 是 Adobe 首先制作的开源字体之一。自2012年发布后,该字体大受欢迎,并被许多开发人员使用。它保留了 Source Sans 的设计特征和垂直比例,但改变了字形宽度,使其在所有粗细中保持一致。


下载链接 • Github链接


4. Monoid


如果你是那种讨厌水平滚动的人,这就是适合你的字体(因为这款字体比较细长)。它针对编程进行了优化,即使在低分辨率的显示器上也有 12px/9pt 的类似位图的清晰度。该字体还有一个名为 Monoisome 的 Font Awesome 集成。


下载链接 • Github链接




5. Hack


Hack 是所有字体中最可定制的之一,拥有1573个字形,你可以自行更改每一个字形的细节。此外,Powerline 字形也包含在其常规字体套件中。


下载链接 • Github链接




6. Iosevka


Iosevka 默认提供了苗条的字体形状:其字形宽度正好为1/2em。相比于其他的字体,你可以在同样的屏幕宽度下放置更多列的文字。它有两种宽度:普通和扩展。如果你希望字体间隔更大一点的话,就选择扩展版本的宽度。


下载链接 • Github链接




7. JetBrains Mono


IntelliJ、WebStorm 等诸多IDE背后的公司 —— JetBrains,在2020年出人意料地推出了自己的字体。他们的字体力求让代码行长度更符合开发人员的期望,使每个字母占据更多的像素。他们在保持字符的宽度标准的基础上最大化了小写字母的高度,从而实现这个目标。


下载链接 • Github链接




8. Fantasque Sans Mono


Fantasque Sans Mono 的设计以实用性为重点,它可以给你的代码增添一丝不一样的感觉。它手写风格的模糊感使其成为一个很酷的选择。


下载链接 • Github链接




9. Ubuntu Mono


这款字体是专门为了补充 Ubuntu 的语气而设计的。它拥有一种现代风格,并具有独特的 Ubuntu 品牌特性,传达出一种精准、可靠和自由的态度。如果你喜欢 Linux,但需要在 Windows 或 MacOS 上工作,这款字体将给你带来一点小小的慰藉和快乐。


下载链接 • 官网




10. Anonymous Pro


这种字体的出色之处在于,它特别区分了那些容易被误认为相同的字符,比如“0”(零)和“O”(大写字母O)。它是一个由四种固定宽度字体组成的字体族,特别针对编程人员而设计。


下载链接 • 官网




11. Inconsolata


这款字体可以作为微软的 Consolas 字体的开源替代。它是一种用于显示代码、终端等使用场景的等宽字体。它提供了连字符,能够给用户出色的编码体验。


下载链接 • GitHub链接




12. Victor Mono


这种字体简洁、清新且细长,具有较大的字母高度和清晰的标点符号,因此易读性强并且适合用于编程。它具有七种不同粗细和 Roman、Italic 和 Oblique 样式。它还提供可选的半连接草书斜体和编程符号连字符。


下载链接 • Github链接




13. Space Mono


这款字体专门为了标题和显示器排版而开发,它拥有几何板块的核心与新颖的近乎过度的合理化形式。它支持拉丁扩展字形集,可以用于英语和其他西欧语言的排版。


下载链接 • GitHub链接




14. Hasklig


在Source Code Pro的基础上,这款字体通过连字符来解决不合适的字符的问题,这也是排版师们一直以来使用的方式。底层代码保持不变——只有表现形式发生变化。


下载链接 • Github链接




哪个字体是你的最爱?


字体,就像颜色主题一样,是一个非常因人而异的话题。不同的开发者喜欢不同的字体。有些人喜欢连词,有些人不喜欢。有些人喜欢斜体字,有些人则讨厌。


希望这篇文章能帮助你找到喜欢的字体,给它一个机会,尝试使用几天,你会有不一样的感觉。



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

flutter有哪些架构的框架?该怎么选择

flutter有哪些架构的框架? Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架:BLoC (Business Logic Comp...
继续阅读 »

flutter有哪些架构的框架?


Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架:

  1. BLoC (Business Logic Component):BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。在Flutter中,可以使用flutter_bloc库来实现BLoC架构。 Provider:Provider是Flutter中的一个轻量级状态管理库,它使用InheritedWidget实现状态共享,可以有效地解决Flutter应用中的状态管理问题。
  2. MobX:MobX是一种基于响应式编程的状态管理库,它使用可观察对象来管理应用程序的状态,并自动更新与之相关的UI组件。在Flutter中,可以使用mobx库来实现MobX架构。
  3. Redux:Redux是一种流行的状态管理模式,在Flutter中也有相应的实现库redux_flutter。Redux通过单一数据源管理应用程序的状态,并使用纯函数来处理状态的更新,可以有效地解决Flutter应用中的状态管理问题。 以上是常用的Flutter架构框架,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。

除了上面提到的框架之外,还有以下几个Flutter架构框架:

  1. GetX:GetX是一种轻量级的Flutter架构框架,它提供了路由管理、状态管理和依赖注入等功能,可以大大简化Flutter应用的开发。
  2. MVC:MVC是一种经典的软件架构模式,它将应用程序分为模型、视图和控制器三个部分,可以有效地分离关注点,使得应用程序更易于维护和扩展。
  3. MVP:MVP是一种衍生自MVC的架构模式,它将应用程序分为模型、视图和Presenter三个部分,Presenter负责处理业务逻辑,将模型数据展示到视图上。
  4. MVVM:MVVM是一种流行的架构模式,它将应用程序分为模型、视图和视图模型三个部分,视图模型负责处理业务逻辑,将模型数据展示到视图上。

总之,Flutter中有很多架构框架可供选择,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。


Flutter BLoC


Flutter BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。BLoC这个缩写代表 Business Logic Component,即业务逻辑组件。
BLoC的核心思想是将UI层和业务逻辑层分离,通过Stream或者Sink等异步编程方式,将UI层和业务逻辑层连接起来。具体来说,BLoC模式包含以下三个部分:
Events:事件,即UI层的用户操作或其他触发条件,例如按钮点击,网络请求完成等等。
Bloc:业务逻辑层,用于处理Events,处理业务逻辑,并向UI层提供状态更新。
State:状态,即UI层的显示状态,例如页面的loading状态,数据请求成功或失败状态等等。


BLoC的核心是Bloc和State之间的联系,Bloc接收Events,并根据业务逻辑处理后,通过Stream向UI层提供状态更新。UI层监听Bloc的Stream,获取最新的State,并根据State更新UI状态。
在Flutter中,可以使用StreamController来创建BLoC。StreamController是一个异步数据流控制器,可以创建一个Stream用于事件流,创建一个Sink用于事件的注入。
Flutter框架提供了一个非常好的BLoC框架flutter_bloc,它封装了BLoC的核心逻辑,使得开发者可以更加方便地使用BLoC模式进行状态管理。使用flutter_bloc框架,只需要定义好Events、Bloc和State,然后将它们组合起来,就可以实现状态管理,从而将UI层和业务逻辑层分离。


总之,Flutter BLoC是一种状态管理模式,它通过将UI层和业务逻辑层分离,使用Stream或Sink等异步编程方式将它们连接起来,实现了应用程序的业务逻辑和UI分离。


如何选择使用


选择使用哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。以下是一些常见的应用场景和推荐的架构框架:
对于小型应用程序,可以考虑使用轻量级的状态管理库,如Provider或GetX。
对于需要处理复杂业务逻辑的应用程序,可以使用BLoC、MobX或Redux等状态管理框架。
对于需要实现高度可测试性的应用程序,可以考虑使用MVC、MVP或MVVM等经典的软件架构模式。
总之,选择哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。开发者应该根据自己的需求和技能水平选择最适合的架构框架,以提高开发效率和代码质量。


GetX和BLoC的优缺点


GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点,下面是它们的主要特点和优缺点的比较:


GetX


优点:


简单易用:GetX是一种轻量级的Flutter框架,提供了简单易用的依赖注入、路由管理和状态管理等功能,可以大大简化Flutter应用的开发。
性能优秀:GetX使用原生的Dart语言构建,不需要任何代码生成,因此运行速度非常快,同时也具有很好的内存管理和性能优化能力。
功能完备:GetX提供了路由管理、依赖注入、状态管理、国际化、主题管理等功能,可以满足大多数应用程序的需求。


缺点:


社区相对较小:相比其他流行的Flutter框架,GetX的社区相对较小,相关文档和教程相对较少,需要一定的自学能力。
不适合大型应用:由于GetX是一种轻量级框架,不适合处理大型应用程序的复杂业务逻辑和状态管理,需要使用其他更加强大的框架。


BLoC


优点:


灵活可扩展:BLoC提供了灵活的状态管理和业务逻辑处理能力,可以适应各种应用程序的需求,同时也具有良好的扩展性。
可测试性强:BLoC将UI和业务逻辑分离,提高了代码的可测试性,可以更容易地编写和运行测试代码。
社区活跃:BLoC是一种流行的Flutter框架,拥有较大的社区和用户群体,相关文档和教程比较丰富,容易入手。


缺点:


学习曲线较陡峭:BLoC是一种相对复杂的框架,需要一定的学习曲线和编程经验,初学者可能需要花费较多的时间和精力。
代码量较大:由于BLoC需要处理UI和业务逻辑的分离,因此需要编写更多的代码来实现相同的功能,可能会增加开发成本和维护难度。
总之,GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点。选择哪种框架取决于具体的应用程序需求和开发团队的技术水平。


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

如何用原生的方式来定义Swift JSON Model

iOS
在Swift开发中,处理JSON数据序列化是一项常见任务。由于Swift的类型安全特性,处理类似JSON这样的弱类型数据一直是一个挑战。然而,Swift 4引入了一个令人欣喜的特性,即Codable协议。Codable协议为我们提供了一种简洁的方式来序列化和反...
继续阅读 »

在Swift开发中,处理JSON数据序列化是一项常见任务。由于Swift的类型安全特性,处理类似JSON这样的弱类型数据一直是一个挑战。然而,Swift 4引入了一个令人欣喜的特性,即Codable协议。Codable协议为我们提供了一种简洁的方式来序列化和反序列化JSON数据。


尽管Codable协议在处理大多数情况下表现得很出色,但它并不能完全满足所有需求。例如,它不支持自动类型转换,也无法友好地处理默认值。


如果我们能解决这些问题,就能更完美地处理JSON数据了。我们可以自定义解码器和编码器,以提供更高级的功能。通过自定义解码器,我们可以实现类型的自动转换,将JSON数据转换为目标类型,而无需手动处理。此外,我们还可以通过自定义编码器,在编码过程中为属性设置默认值,以确保生成的JSON数据符合预期。


总之,通过充分利用Swift的特性和自定义解码器、编码器,我们可以更好地处理JSON数据,满足我们更复杂的需求。
传送门ObjMapper


Codable坑点1:不支持类型转换

// JSON:
{
"uid":"123456",
"name":"Harry",
"age":10
}

// Model:
struct Dog: Codable{
var uid: Int
var name: String?
var age: Int?
}

在json转换过程中,我们常常与遇到类型模型与json的类型不一致的情况,就像上面的uid字段,uid在json中是String,但是我们的模型是Int,由于swift是类型安全的,所以,转换就不会成功。


Codable坑点2:不支持默认值


话不多说,上代码

struct Activity: Codable {
enum Status: Int {
case start = 1//活动开始
case processing = 2//活动进行中
case end = 3//活动结束
}

var name: String
var status: Status//活动状态
}

这儿有一个活动,活动现目前有三种状态,到目前为止,一切都很美好。有一天,突然说需要给活动添加已下架的状态,what?

//JSON
{
"name": "元旦迎新活动",
"status": 4
}

用Activity解析上面的JSON就会报错,我们如何规避呢,像下面一样

var status: Status?

答案是no、no、no,因为可选值的解码所表达的是“如果不存在,则置为 nil”,而不是“如果解码失败,则置为 nil”。


解决方案


有没有更好的方式来处理上面这两个问题呢?具体代码见ObjMapper,这儿简单描述下如何使用。


1、Model与JSON相互转换

// JSON:
{
"uid":888888,
"name":"Tom",
"age":10
}

// Model:
struct Dog: Codable{
//如果字段不是可选类型,则使用Default,提供一个默认值,像下面一样
@Default<Int.Zero> var uid: Int
//如果是可选类型,则使用Backed
@Backed var name: String?
@Backed var age: Int?
}

//JSON to model
let dog = Dog.decodeJSON(from: json)

//model to json
let json = dog.jsonString

当 JSON/Dictionary 中的对象类型与 Model 属性不一致时,ObjMapper 将会进行如下自动转换。自动转换不支持的值将会被设置为nil或者默认值。




2、Model的嵌套

let raw_json = """
{
"author":{
"id": 888888,
"name":"Alex",
"age":"10"
},
"title":"model与json互转",
"subTitle":"如何优雅的转换"
}
"""

// Model:
struct Author: Codable{
@Default<Int.Zero> var id: Int
@Default<String.Empty> var name: String
//使用Backed后,如果类型不匹配,则类型会自动转换
//比如,上面的json中,age是个字符串,我们定义的模型是Int,
//那么声明@Backed后,会自动转换成Int类型
@Backed var age: Int?
}

struct Article: Codable {
//如果json中的title为nil或者不存在,则会给title赋一个默认值
@Default<String.Empty> var title: String
var subTitle: String?
var author: Author
}

//JSON to model
let article = Article.decodeJSON(from: raw_json)

//model to json
let json = article.jsonString
print(article?.jsonString ?? "")

3、自定义类型的可选值


话不多说,上代码

struct Activity: Codable {
enum Status: Int {
case start = 1//活动开始
case processing = 2//活动进行中
case end = 3//活动结束
}

@Default<String.Empty> var name: String
var status: Status//活动状态
}

这儿有一个活动,活动现目前有三种状态,到目前为止,一切都很美好。有一天,突然说需要给活动添加已下架的状态,what?

//JSON
{
"name": "元旦迎新活动",
"status": 4
}

用Activity解析上面的JSON就会报错,我们如何规避呢,像下面一样

var status: Status?

答案是no、no、no,因为可选值的解码所表达的是“如果不存在,则置为 nil”,而不是“如果解码失败,则置为 nil”,那就用我们的Default吧,请看下面代码:

struct Activity: Codable {
///Step 1:让Status遵循DefaultValue协议
enum Status: Int, Codable, DefaultValue {
case start = 1//活动开始
case processing = 2//活动进行中
case end = 3//活动结束
case unknown = 0//默认值,无意义

///Step 2:实现DefaultValue协议,指定一个默认值
static func defaultValue() -> Status {
return Status.unknown
}
}

@Default<String.Empty> var name: String
///Step 3:使用Default
@Default<Status> var status: Status//活动状态
}

//{"name": "元旦迎新活动", "status": 4 }
//Activity将会把status解析成unknown

4、为普通类型设置不一样的默认值


本库已经内置了很多默认值,比如Int.Zero, Bool.True, String.Empty...,如果我们想为字段设置不一样的默认值,见下面代码:

public extension Int {
enum One: DefaultValue {
static func defaultValue() -> Int {
return 1
}
}
}

struct Dog: Codable{
@Backed var name: String?
@Default<Int.Zero> var uid: Int
//如果json中没有age字段或者解析失败,则模型的age被设置成默认值1
@Default<Int.One> var age: Int
}

5、数组支持


对于数组,可以使用@Backed,@Default来解析

// JSON:
let raw_json = """
{
"code":0,
"message":"success",
"data": [{
"name": "元旦迎新活动",
"status": 4
}]
}
"""

struct Activaty: Codable{
@Default<String.Empty> var name: String
@Default<Int.Zero> var status: Int
}

// 如果数组是可选类型,可以使用@Backed
struct Response1: Codable {
@Default<Int.Zero> var code: Int
@Default<String.Empty> var message: String
@Backed var data: [Activaty]?
}

// 为数组,设置默认值,如果数组不存在或者解析错误,则使用默认值
struct Response2: Codable {
@Default<Int.Zero> var code: Int
@Default<String.Empty> var message: String
@Default<Array.Empty> var data: [Activaty]
}
//JSON to model
let rsp1 = Response1.decodeJSON(from: raw_json)
let rsp2 = Response2.decodeJSON(from: raw_json)

//model to json
let json1 = rsp1.jsonString
let json2 = rsp2.jsonString
// print(rsp1?.jsonString ?? "")
// print(rsp2?.jsonString ?? "")

6、设置通用类型


我们在开发过程中,第一个遇到的json可能是这样的:

// JSON:
{
"code":0,
"message":"success",
"data":[]//这个data可以是任何类型
}

由于data字段的类型不固定,有时候为了统一处理,我们定义模型可以像下面这样,有枚举类型JsonValue来表示。

struct Response: Codable { 
var code: Int
var message: String
var data: JsonValue?
}

如果要取data字段的值,我们可以这样用data?.intValue或者data?.arrayValue等等,具体使用见源码。


注意:这种对于data是一个简单的model(比如就是一个整形、字符串等等),可以起到事半功倍的效果;如果data是一个大型model,建议还是将data指定为具体类型。


7、如果是从1.0.x升级到2.0版本,修改了DefaultValue协议。如果之前的代码中使用了DefaultValue协议,则会报错,修改如下:

原来为:
///Step 1:让Status遵循DefaultValue协议
enum Status: Int, Codable, DefaultValue {
case start = 1//活动开始

///Step 2:实现DefaultValue协议,指定一个默认值
static let defaultValue = Status.unknown
}

修改成:
///Step 1:让Status遵循DefaultValue协议
enum Status: Int, Codable, DefaultValue {
case start = 1//活动开始

///Step 2:实现DefaultValue协议,返回一个默认值
static func defaultValue() -> Status {
return Status.unknown
}
}


参考文档

  1. 用 Codable 协议实现快速 JSON 解析
  2. Swift 4 踩坑之 Codable 协议
  3. 使用 Property Wrapper

不喜勿喷,有问题请留言😁😁😁,欢迎✨✨✨star✨✨✨和PR


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

iOS整理: 关于动态库和静态库

iOS
之前对于这两者的概念仅仅停留在八股文的认知水平(可能八股都答的一塌糊涂)亦或者就是道听途说,知道下怎么用就完事儿了,看了很多相关的资料,看了就忘,索性自己整理一下,理顺一下自己的思路,体系化的理解一下,防止自己变成脑残。。。 在此之前,我们对一些常识性的东西复...
继续阅读 »

之前对于这两者的概念仅仅停留在八股文的认知水平(可能八股都答的一塌糊涂)亦或者就是道听途说,知道下怎么用就完事儿了,看了很多相关的资料,看了就忘,索性自己整理一下,理顺一下自己的思路,体系化的理解一下,防止自己变成脑残。。。


在此之前,我们对一些常识性的东西复习一下


.a .framework

.a 是单纯的二进制文件,.framework是二进制文件+资源文件。


程序执行的流程


预处理--->编译--->汇编--->链接(汇编程序生成的目标文件并不能被立即执行,还需要通过链接器(Linker),将有关的目标文件彼此相连接,使得所有的目标文件成为一个能够被操作系统载入执行的统一整体。)

  • 静态链接直接在编译阶段就把静态库加入到可执行文件当中去。优点:不用担心目标用户缺少库文件。缺点:最终的可执行文件会较大;且多个应用程序之间无法共享库文件,会造成内存浪费。
  • 动态链接在链接阶段只加入一些描述信息,等到程序执行时再从系统中把相应的动态库加载到内存中去。优点:可执行文件小;多个应用程序之间可以共享库文件。缺点:需要保证目标用户有相应的库文件。

关于iOS应用的启动流程


1. 解析Info.plist


2. Mach-O(可执行文件)加载


dylib loading time


rebase/binding time


3. 程序执行


....

这里其实还是想简述一下加载流程,因为我在这儿一直也有个误区,应用在启动前静态库已经存在于可执行的二进制文件当中了,而动态库在启动后才进行加载等一系列操作。


为什么要阐述这些老生常谈的东西呢,因为我以前一直对动态库的加载和编译时机存在误解,我们拿一个具体的工程举例 




我们从产物的包内容找到一路找到可执行文件,可以看到可执行文件和framework是单独存在的
静态库在编译的时候就被打到二进制文件当中了


怎么区分动态库还是静态库


一般来说,动态库以 .dylib 或者 .framework 后缀结尾;静态库以 .a 和 .framework 结尾。
这里列出几种方法区分动态库还是静态库

  1. 查看Mach-O Type来区分
  2. 查看ipa的目录结构
  3. 通过file工具查看

动态库/静态库的加载过程 & 两者之间的区别


一般来说,build一个项目的过程是先compile然后再link,然后才有一个可执行文件。link的时候要做的一件事情就是把各种函数符号转换成函数调用地址,然后最终生成的可执行文件就能够直接调用到函数了。




1.静态库在build的时候就把库里面的代码链接进可执行文件。这里还要再补充一句,会将静态库中 被使用的部分 都添加到应用程序的可执行文件,这意味着应用程序的可执行文件大小会随着静态库数量的增加而增大。在运行时,静态库会随着应用程序的可执行文件一起加载到同一代码区。在 iOS 开发中,应用程序的可执行文件就是 ipa 解压后,包内容中与 app 同名的可执行文件




2.动态库的做法不一样,不会在build的时候就把代码link进可执行文件,这里我们只对动态链接库进行阐述
对于动态链接库而言,build可执行文件的时候需要指定它依赖哪些库,当可执行文件运行时,如果操作系统没有加载过这些库,那就会把这些库随着可执行文件的加载而加载进内存中,供可执行程序运行。如果多个可执行文件依赖同一个动态链接库,那么内存中只会有一份动态链接库的代码,然后把它共享给所有相关可执行文件(APP)的进程使用,所以它也叫共享库


那简言之:动态链接库在可执行文件得到运行的时候就加载 这句话很有营养


在ios程序的启动流程中,我们会先加载应用的可执行文件(这就包括了静态库文件)然后才是动态库的一系列加载流程(程序执行
静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝,存在形式:.a和.framework
动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。存在形式:.dylib和.framework


之前对这里一直存在误区,这里的加载是以可执行文件(APP)为单位的。还有就是我们这里谈的动态库都都是系统层面的动态库,区别也是针对于静态库和系统动态库而言的。另外就是静态库在一开始就存在于可执行文件中,而动态库在运行时动态的进行绑定。


use_frameworks!


podfile中经常会加上这句话,我们来看一下实际的作用和效果
当使用 use_frameworks的时候 cocoapods会生成对应的 frameworks 文件(动态库)
在Link Binary With Libraries:会生成Pods_工程名.framework,包含了其它用cocoapods导入的第三方框架的.framework文件




当不使用use_frameworks!(静态库)cocoapods会生成相应的 .a文件(静态链接库)
Link Binary With Libraries: libPods-工程名.a 包含了其他用cocoapods导入有第三库的 .a 文件




当然我还注意到一些其他文件的diff 比较令我好奇的就是这个modulemap 之前也没了解过,以后有机会研究一下




Xcode Embed




对于这个设置,之前也是不太清楚,

  • 对于 系统动态库,可以将 Embed 属性设置成 Do Not Embed,因为 iOS 系统提供了相关的库,我们无需将它们再嵌入到应用程序的 ipa 包中,如:Foundation.frameworkUIKit.framework
  • 对于 用户动态库,需要将 Embed 属性设置成 Embed,因为链接发生在运行时,链接器需要从应用程序的 ipa 包中加载完整的动态库。
  • 对于 静态库,需要将 Embed 属性设置成 Do Not Embed,因为链接发生在编译时,编译完成后相关代码都已经包含在了应用程序的可执行文件中了,无需在应用程序的 bundle 中再保存一份。

动态库和静态库的使用场景


静态库

  1. 静态库主要应用于模块化,分工合作
  2. 避免少量改动经常导致大量的重复编译连接
  3. 也可以重用,注意不是共享使用

动态库


1.使用动态库,可以将最终可执行文件体积缩小


2.对于 iOS 开发来说, 因为我们只能使用 Embedding Frameworks 来使用动态库, 这样的动态库并不是真正的动态库, 其会在编译时全部置入 app, 然后再 app 启动时全部加载, 这样的话会导致体积大, 加载速度慢.


动静态库的混用

  • 静态库可以依赖静态库
  • 动态库可以依赖动态库
  • 动态库不能依赖静态库! 动态库不能依赖静态库是因为静态库不需要在运行时再次加载, 如果多个动态库依赖同一个静态库, 会出现多个静态库的拷贝, 而这些拷贝本身只是对于内存空间的消耗

但其实两者之间都是可以通过各种操作进行依赖的
静态库也是可以依赖动态库的
动态库也是可以依赖静态库的


总结


以上就是我对动态库以及静态库一些盲区的的具体总结和详细分析,总的来说,对每个角色的定位,有了更清晰的认知


补充


用户创建伪动态库 和静态库有什么区别呢,如果有区别 具体是怎么应用的呢 有知道的朋友可以帮我解释下吗?不胜感激


参考链接


blog.csdn.net/GeekLee609/…


juejin.cn/post/704110…


zhuanlan.zhihu.com/p/346683326


http://www.jianshu.com/p/662832e16…


chuquan.me/2021/02/14/…


zhuanlan.zhihu.com/p/346683326


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

解决 App Store 默认语言设置的问题

iOS
问题背景 一个很奇怪的问题,在没有支持多语言的时候,明明在 App Store Connect 上选择了 Primary Language 为 Chinese,为什么在 App Store 页面上还是显示主要语言为英文? 问题解决 实际上在做 App 多语...
继续阅读 »

问题背景


一个很奇怪的问题,在没有支持多语言的时候,明明在 App Store Connect 上选择了 Primary Language 为 Chinese,为什么在 App Store 页面上还是显示主要语言为英文?






问题解决


实际上在做 App 多语言适配之前,除了 App Store Connect 上需要选择对应的 Primary Language 以外,代码配置上也仍然需要做一些配置,将中文设置为默认语言。


首先在本地化 Locallization 处增加新语言,位于 Project -- Info -- Localizations



注意下图是增加成功之后的结果,这一步只需要增加新语言就行了,不需要关注 Development Localization 是具体哪个语言





第二步是找到 project.pbxproj 文件(右键点击 .xcodeproj 项目文件,然后 show package contents,参考:stack overflow - Vladimir's Answer),并修改其中的 developmentRegion 字段。


如果上一步中成功增加了新的语言,那么在 knownRegions 处就能找到对应的。




问题验证


上面这么修改一番之后,其实已经成功了,那么接下来正常发版就可以生效了。不过在发版之前,最好可以提前检查一次:


在 App Store Connect -- TestFlight 中找到对应修改过后的包,然后找到 Build Metadata:




然后找到 Localizations,如果这里的语言更新成功,那么就代表没问题了!




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

关于强制加班、人才培养、绩效考核的思考

来源于池老师星球里的一个提问,我也借此机会做一个小归纳,本来想直接贴问题截图的,想了想由于是池老师星球的里的提问,还是不要贴图片了。 大致问题描述:1.公司强制124加班,但是没那么多事情需要加班去做,如何让大家把这些时间利用起来去学习,提高团队能力2.作为研...
继续阅读 »

来源于池老师星球里的一个提问,我也借此机会做一个小归纳,本来想直接贴问题截图的,想了想由于是池老师星球的里的提问,还是不要贴图片了。


大致问题描述:

  • 1.公司强制124加班,但是没那么多事情需要加班去做,如何让大家把这些时间利用起来去学习,提高团队能力
  • 2.作为研发团队管理者绩效考核怎么设计,量化与非量化如何平衡

我的评论原文:


同样的情况我也有碰到,124强制加班,公司跟不上的意思猜测情况是“技术团队支撑业务需要绰绰有余,又或者是业务侧增速不够,总之就是没那么多工作需要加班去完成”


强制加班,至少公司看上去灯火通明的,很多时候是高管或老板要求的,有可能是一些对软件研发理解不足的老板他们需要安慰剂,也有可能是一些“政治原因”。这就不好揣测了,非心腹当然是没法知道,但此时尤为要注意做好管理工作,很多事情没法讲也讲不清楚,团队成员可能因此会对团队、公司失去信心。那么拥有健康的团队氛围,愿意帮助大家成长。规划有长期的团队目标,目标符合公司发展,符合团队成员成长需要,同时要具备一定的挑战性,具备这两点的团队这方面的问题会少很多。团队不可以长期处于磨洋工的状态,如果人心涣散,再聚极难。


研发团队怎么做人才培养:


有加班时间了才想到用这个时间帮助员工成长,之所以有这样的问题是不是平时做人才培养不到位,不够细致。比如项目空窗期的时候之前都是放任大家自由学习或“摸鱼“吗? 从团队管理者角度去看,大家自由学习不能算是好事,很有可能学完了就走了,毕竟你这里没啥挑战。


我的一些经验:


结合公司业务,比如toC 还是toB 去看公司下阶段的规模与增速,分析产研需要达到的能力,以此为基础去看行业内的标准与自己团队的落差,把落差放大一些 作为团队的长期目标,时间上至少是一个季度以上才够大。
这些目标的特点都是重要但不紧急,但具备一定的挑战性。既满足人才培养又能对应未来公司发展需要。


把这些目标作为OKR,分担到各个小组,各个小组再拆落实到个人,并至少最小以月为单位进行复核,协助他们分析解决碰到的阻力问题,同时很多一线同学向上管理做得不好,管理者需要时常主动了解情况,及时给予资源支持。


执行过程难免碰到阻力出现停滞,或速度不理想,那么配合KPI奖励或其他激励来提高成员的驱动力。
在完成的目标过程中,挑一些大里程碑收获拿出来做分享,做沉淀,结合业务做实际应用,大家也能感受到做这些事儿的实际意义,团队信任关系也会越牢固。


KPI设计权衡:


产研团队虽然很难量化指标,但是做一份大家都认可的KPI 是完全可行的。


员工自评+管理者补评只要达到双方的认可,保持公平,公开。


关于KPI第一是考虑清楚KPI是一个奖惩手段,奖惩要分开来看。不然他就单纯变成一把尺子,架在每个人都脖子上,为了KPI而KPI,黑魔法多了去了,比如池老师举过一个例子,为了提高pv,产品经理把一个页面拆成2个,得到2倍pv。


我个人倾向于把它的激励作用放大一些,惩罚效果降低。好的员工有荣辱心,反省能力强,反而是在出事时帮他一把作为激励效果很好。


展开挺复杂,我简单举2个例子表述我的理解:


开发侧好量化的软件交付质量:100分,那么出 bug,分测试阶段,线上阶段,等级从p1-p4 定级,定级之后要对应的扣分标准呢,主动或协助及时修复奖励多少分。这个比较典型,很好量化。


不好量化的沟通协作能力:


"能胜任跨部门沟通协调,高效协作 85~100分


能主导 1 V N 沟通协调 70~85分


能主导 1 V 1 沟通协调 60~70分


沟通能力一般 60分"


这可以用管理者+实例结合来做。员工自评写实例,在什么项目主导协作,这个大家都看得见,管理者再主观的做一些评价,稍微修正一下。


最后,以100为单位为每一项考核做好权重分配,假设考核一共分5项,软件交付质量,设置它的权重为25%,来控制它的影响幅度。


设置权重能很好的适应员工能力梯队,凸显他们各自的优点。


所有人都打完分了,不管是271,28的划分这对管理者都是个头疼的事情,这时候就需要花较多的时间综合考虑,绝对公平太难了,见仁见智了。


我见过的不好的例子就是:给A这个月打高分给B打低分,下个月给B打高分给A打低分,全年下来大家保个平安,上下串通一气,团队一片和谐。


一点题外话,我对设计流程,考核等工作内容有天然的兴趣,但不知道可以以此作为主要能力找工作吗?这会是什么样的工作呢?(我也去问下gpt)


GPT回复:


设计流程和考核等工作内容是非常重要的设计领域的核心能力,如果您对此有天然的兴趣,可以尝试在这方面深入学习和实践,成为一名设计项目经理、设计团队领导、设计流程咨询师等职业。


作为一名设计项目经理,您需要负责整个设计项目的流程管理、协调和监督,确保设计团队能够按时、按质完成设计任务。同时,您需要与客户和其他相关方保持良好的沟通,理解客户需求,并协调解决设计项目中的问题。


作为一名设计团队领导,您需要带领设计团队完成项目任务,并对团队成员进行管理和培训。您需要制定设计流程和考核标准,并监督团队成员按照标准完成任务。


作为一名设计流程咨询师,您需要帮助客户建立有效的设计流程,并优化设计流程中的各个环节,以提高设计效率和质量。您需要了解各种设计软件和工具,并根据客户需求推荐最佳的设计工具和流程。


需要注意的是,设计流程和考核等工作内容是设计领域的重要能力之一,但并不是唯一的能力。如果您想在设计领域发展,还需要具备良好的设计技能和创造力。


emm.....


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

一文理解贝塞尔曲线

贝塞尔曲线的来源 贝塞尔曲线最早是由贝塞尔在1962年提出来的,目的是获取汽车的外形。贝塞尔曲线看上去非常复杂,其实想法非常简单,(如下图1所示)就是先用折线先绘制出大致的轮廓,然后用曲线来逼近。 图1 这个方式是不是有点熟悉,刚看到的时候,我就想到了计算圆...
继续阅读 »

贝塞尔曲线的来源


贝塞尔曲线最早是由贝塞尔在1962年提出来的,目的是获取汽车的外形。贝塞尔曲线看上去非常复杂,其实想法非常简单,(如下图1所示)就是先用折线先绘制出大致的轮廓,然后用曲线来逼近。




图1


这个方式是不是有点熟悉,刚看到的时候,我就想到了计算圆的面积时,我们会使用多边形来逼近圆的曲线(如图2所示);贝塞尔曲线刚好相反,它是使用曲线来逼近多边形,刚好反着来了😂。




图2


构造贝塞尔曲线


思路虽然简单,但是如何把这个曲线画出来,或者说如何用一个函数来表示这条曲线就很困难了。不过这个不需要我们关心,有大佬已经解决了。我们直接来看看贝塞尔曲线的定义式,如下图3:




图3


先别急着划走,这个公式不用记,因为它太复杂而且计算量大,因此在工程开发中我们不会用它。一般在工程中,我们使用德卡斯特里奥算法(de Casteljau) 来构造贝塞尔曲线。听起来更复杂了,别急让我们举个🌰。下面以2次贝塞尔曲线为例。




图4




图5


看图4,德卡斯特里奥算法(de Casteljau) 的意义就是满足P0Q0P0P1=P1Q1P1P2=Q0BQ0Q1=t  \frac{P_0Q_0}{P_0P_1} = \frac{P_1Q_1}{P_1P_2} = \frac{Q_0B}{Q_0Q_1} = t 的情况下,随着 t 从 0 到 1 逐渐变大,B点经过的点组成的曲线就是我们需要的贝塞尔曲线了。图5是我们常见的动图,之前看的时候一直很懵逼,现在了解了贝塞尔曲线是如何画出来的,是不是清楚多了。


更高阶的贝塞尔曲线绘制方式和上面的一样,只是多了几条边,绘制的动图如下:




3次贝塞尔曲线




4次贝塞尔曲线




5次贝塞尔曲线


贝塞尔曲线的函数表示


看到这里,我们已经对贝塞尔曲线有了一个大概的了解。但是还是一个关键的问题,我们怎么画出贝塞尔曲线呢?或者说有什么函数可以让我们画出这个曲线吗?这个其实更简单,我们高中就学过了。还是以二次贝塞尔曲线为例,它的参数方程如下,其中 P0、P1、P2代表控制点。




我们假设三个控制点的坐标是 P0 (-1, 0)、 P1 (0, 1) 、P2 (1, 0),把值带入上面的参数方程,就可以得到如下结果:


(xy)=(1t)2(10)+2t(1t)(01)+t2(10)\left(\begin{array}{c}x\\ y\end{array}\right) = (1 - t)^{2} \left(\begin{array}{c}-1\\ 0\end{array}\right)
+ 2t(1 - t) \left(\begin{array}{c}0\\ 1\end{array}\right) + t^{2} \left(\begin{array}{c}1\\ 0\end{array}\right)

(xy)=((12t)2+t22t(1t))\left(\begin{array}{c}x\\ y\end{array}\right) = \left(\begin{array}{c}-(1 - 2t)^{2} + t ^ 2\\ 2t(1 - t)\end{array}\right)

{x=2t1y=2t2+2t\begin{cases} x = 2t - 1 \\ y = -2t^2 + 2t\end{cases}

最后化解可得到我们熟悉的 y = f(x) 函数y=12x2+12:y = -\frac{1}{2}x^2 + \frac{1}{2} 效果图如下图。可以看出二次贝塞尔曲线实际上就是我们高中学的抛物线。唯一不同的是,我们高中求的抛物线,会经过 P0、P1、P2三个点,而贝塞尔曲线只会经过 P0、P1两个端点。




类似的:


一次贝塞尔曲线就是一次函数y=a0x+a1:y = a_0x + a_1


三次贝塞尔曲线就是三次函数:y=a0x3+a1x2+a2x+a3y = a_0x^3 + a_1x^2 + a_2x + a_3


四次贝塞尔曲线就是四次函数:y=a0x4+a1x3+a2x2+a3x+a4y = a_0x^4 + a_1x^3 + a_2x^2 + a_3x + a_4


n次贝塞尔曲线就是n次函数:y=a0xn+a1xn1+...+an y = a_0x^n + a_1x^{n-1} + ... + a_{n}


总结


贝塞尔曲线实际上并不复杂,我们可以简单的把n次贝塞尔曲线看成对应的n次函数的曲线。因为贝塞尔曲线的这个特点,也造成了贝塞尔曲线的最大缺陷————不能局部修改,即改变其中一个参数时会改变整条曲线。后面为了解决贝塞尔曲线的这个问题,提出了B样条曲线,下篇文章我们就介绍B样条曲线。


最后这篇文章为了方便读者的理解,省略了很多贝塞尔曲线特性的介绍,如果对贝塞尔曲线感兴趣,可以在B站上看看它的完整课程。


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

技术主管是否需要什么段位的技术

今天来跟大家讨论一下技术主管需要什么样段位的技术? 首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然...
继续阅读 »

今天来跟大家讨论一下技术主管需要什么样段位的技术?


首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然是阿里云的CTO,一手缔造了阿里云。


那我们这里再详细讨论一下,作为一名技术主管,到底应该有什么样的一个技术的段位?或者换句话来说,你的主管的技术水平需要到达什么样的一个水位?


先说结论,作为一名技术主管,一定是整个团队的技术架构师。像其他的一些大家所讨论的条件我觉得都是次要的,比如说写代码的多少,对于技术深度的钻研多少,带的团队人数多少等等,最核心的是技术主管一定要把控整个团队整个业务技术发展的骨架。


为什么说掌控团队技术架构是最重要的?因为对于一个团队来说无非就两点,第一点就是业务价值,第二点就是技术价值。


对于业务价值来说,有各种各样的同学都可以去负责业务上面的一些导向和推进,比如说产品经理,比如说运营同学。技术主管可以在一定程度上去帮助业务成功,甚至是助力业务成功,但是一定要明白技术同学一定要有自己的主轴,就是你对于整个技术的把握。因为业务上的决策说到底技术主管是只能去影响而非去决策,否则就是你们整体业务同学太过拉胯,无法形成战术合力的目的。


对于一线开发同学来说,你只要完成一个接一个的技术项目即可。但是对于技术主管来说,你就要把握整体的技术发展脉络。要清晰的明白什么样的技术架构是和当前的业务匹配的,同时又具备未来业务发展的可扩展性。


那为什么不能把整个技术架构的设计交给某一个核心的骨干研发同学呢?


所以这里就要明白,对于名技术主管来说,未必一定要深刻的钻研技术本身,一定要把技术在业务上的价值发挥到最大。所以在一定程度上来说,可以让适当的同学参与或者主导整个技术架构的设计,但是作为主管必须要了解到所谓的技术投入的产出比是什么。但是如果不对技术架构有一个彻底的理解,如何能决定ROI?



也就是在技术方案的选型里面一定要有一个平衡,能够用最小的技术投入获取到最大的技术利益,而非深究于技术本身的实习方式。如果一名技术主管不了解技术的框架或者某一些主干流程,那么就根本谈不上怎么样去评估这投入的技术产出比。一旦一名技术主管无法衡量整个技术团队的投入产出比,那就意味着整个团队的管理都是在抓虾和浑水摸鱼的状态,这时候就看你团队同学是否自觉了。


出现了这种情况下的团队,可能换一头猪在主管的位置上,业务依然运行良好。如果在业务发展好的时候,可能一直能够顺利推动,你只要坐享其成就可以了,但是一旦到了要突破困难的时期,或者在业务走下行的时候,这个时候你技术上面的优势就一点就没有了。而且在这种情况下,如果你跳槽到其他公司,作为一名技术主管,对方的公司对你的要求也是非常高的,所以这个时候你如果都说不出来你的技术价值对于业务上面的贡献是什么那想当然,你可能大概率就凉凉了。


那问题又回到了什么样的水平才能到达架构师这个话题,可以出来另一篇文章来描述,但是整体上来说,架构的本质首先一定要明白,为的就是业务的增长。


其次,架构的设计其实就是建造一个软件体系的结构,使得具备清晰度,可维护性和可扩展性。另外要想做好架构,基本的基础知识也必不可少,比如说数据库选型、分布式缓存、分库分表、幂等、分布式锁、消息架构、异步架构等等。所以本身来说做好架构师本身难度就非常大,需要长期的积累,实现厚积而薄发。如何成为一名优秀的架构师可以看我的公众号的其他文章,这里就不再详细的介绍了。



第二点是技术主管需要对于技术细节有敏感度。很多人在问一名主管到底应该具备什么样的综合能力,能不能用一种更加形象的方式来概括,我认为就有一句话就可以概括了。技术主管应该是向战略轰炸机在平常的时候一直遨游在大气的最上层能够掌控整个全局,当到了必须要战斗的时候,可以快速的补充下去,定点打击。


我参加过一次TL培训课程,讲师是阿里云智能交付技术部总经理张瑞,他说他最喜欢的一句管理概括,就是“心有猛虎,细嗅蔷薇”,也就是技术主管在平常的时候会关注于更大的宏观战略或策略,也就是注重思考全局,但是在关键的时候一定要关注和落地实际的细节。


换句更加通俗的话来说,就是管理要像战略轰炸机,平常的时候飞在万丈高空巡视,当发生了战斗的时候,立即能够实现定点轰炸。



所以如果说架构上面的设计就是对于整个团队业务和技术骨架的把握,那么对于细节的敏感度就是对于解决问题的落地能力。


那怎么样能够保证你自己有一个技术细节的敏感度?


我认为必要的代码量是需要的,也就是说对于一个主管来说,不必要写太多低代码,但一定要保证一定的代码量,让自己能够最好的,最快的,最贴近实际的理解实际的业务项目。自己写一些代码,其实好处非常多,一方面能够去巩固和加深自己对技术的理解,另外一方面也能够通过代码去更加理解业务。


当然贴近技术的方式有很多种,不一定要全部靠写代码来完成,比如说做code review的方式来完成,做技术方案的评审来完成,这都是可以的。对我来说,我就会强迫自己在每一个迭代会写上一个需求,需求会涉及到各方各面的业务点。有前端的,有后端的,也有数据库设计的。


自己亲自参与写代码或者code review,会让自己更加贴近同学,能够感知到同学的痛点,而不至于只是在空谈说教。


总结


所以对于一个技术主管来说,我认为首要的就是具备架构设计的能力,其次就是要有代码细节的敏感度,对全局和对细节都要有很强大的把控能力。


当然再总结一下,这一套理论只是适用于基础的管理者,而非高层的CTO等,毕竟不同的层级要求的能力和影响力都是不一样的。


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

你会用nginx部署前端项目吗

web
前端项目的部署以前一直是把静态资源放到后端工程中,随后端部署一起部署。随着前后端分离开发模式的流行,前端项目可以单独部署了,目前最流行的方式使用nginx来部署。 对于前端项目来说,nginx主要有两个功能: 对静态资源做托管,即作为一个静态资源服务器; 对...
继续阅读 »

前端项目的部署以前一直是把静态资源放到后端工程中,随后端部署一起部署。随着前后端分离开发模式的流行,前端项目可以单独部署了,目前最流行的方式使用nginx来部署。


对于前端项目来说,nginx主要有两个功能:



  • 对静态资源做托管,即作为一个静态资源服务器

  • 对动态资源做反向代理,即代理后台接口服务,防止跨域


路由配置


nginx配置最多就是路由配置,路由配置又有几种写法。


1. =


location = /111/ {
default_type text/plain;
return 200 "111 success";
}

location 和路径之间加了个 =,代表精准匹配,也就是只有完全相同的 url 才会匹配这个路由。


image.png


在路径后面添加了aa,那么就不是精确匹配了,所以是404


image.png


2. 不带 =


代表根据前缀匹配,后面可以是任意路径


location /222 {
default_type text/plain;
// 这里的 $uri 是取当前路径。
return 200 $uri;
}

image.png


3. 支持正则匹配~


// 匹配以/333/bbb开头,以.html结尾的路径
location ~ ^/333/bbb.*\.html$ {
default_type text/plain;
return 200 $uri;
}

image.png


上面的~是区分大小写的,如果不区分大小写是~*


// 匹配以/333/bbb开头,以.html结尾的路径
location ~* ^/333/bbb.*\.html$ {
default_type text/plain;
return 200 $uri;
}

4. ^~代表优先级


下面的配置有两个路径是以/444开头的:


location ~* ^/444/AAA.*\.html$ {
default_type text/plain;
return 200 $uri;
}
location ~ /444/ {
default_type text/plain;
return 200 $uri;
}

如果访问/444/AAA45.html,就会直接命中第一个路由,如果我想命中/444/呢? 加上^就好了。


location ^~ /444/ {
default_type text/plain;
return 200 $uri;
}

也就是说 ^~ 能够提高前缀匹配的优先级。


总结一下,location 语法一共有四种:




  1. location = /aaa 是精确匹配 /aaa 的路由;




  2. location /bbb 是前缀匹配 /bbb 的路由。




  3. location ~ /ccc.*.html 是正则匹配,可以再加个 * 表示不区分大小写 location ~* /ccc.*.html;




  4. location ^~ /ddd 是前缀匹配,但是优先级更高。




这 4 种语法的优先级是这样的:


精确匹配(=) > 高优先级前缀匹配(^~) > 正则匹配(~ / ~*) > 普通前缀匹配


root 与 alias


nginx指定文件路径有两种方式rootaliasrootalias主要区别在于nginx如何解释location后面的uri,这会使两者以不同的方式将请求映射到服务器文件上。



  1. root的处理结果是:root路径 + location路径;

  2. alias的处理结果是:使用alias路径替换location路径;


alias是一个目录别名的定义,root则是最上层目录的定义。


需要注意的是alias后面必须要用/结束,否则会找不到文件的,而root则可有可无。另外,alias只能位于location块中。


root示例:


location ^~ /test/ {
root /www/root/html/;
}

如果一个请求的 uri 是 /test/a.html时,web服务器将会返回服务器上的/www/root/html/test/a.html的文件。


alias示例:


location ^~ /test/ {
alias /www/root/html/new_test/;
}

如果一个请求的 uri 是 /test/a.html 时,web服务器将会返回服务器上的/www/root/html/new_test/a.html的文件。


注意, 这里是new_test,因为alias会把location后面配置的路径丢弃掉,把当前匹配到的目录指向到指定的目录。


二级目录


有时候需要在一个端口下,部署多个项目,那么这时可以采用二级目录的形式来部署。


采用二级目录来部署会有一些坑,比如,当我请求 http://xxxxxxxx.com/views/basedata 的时候,浏览器自动跳转到了http://xxxxxxxxm:8100/store/views/basedata/


这是什么原因呢?


最根本的问题是因为http://xxxxxxxx.com/views/basedata后面没有/,所以就触发了nginx301重定向,重定向到了http://xxxxxxxxm:8100/store/views/basedata/,因此,只要避免触发重定向即可。


如果你能忍受直接使用如 http://xxxxxxxxm:8100/store/views/basedata/ 这样最后带/的地址,那也就没有什么问题了。


那为什么会触发重定向呢?


当用户请求 http.xxxxxx.cn/osp 时,这里的 $uri 就是 /ospnginx 会尝试到alias或 root 指定的目录下找这个文件。


如果存在名为 {alias指定目录}/osp 的文件,注意这里是文件,不是目录,就直接把这个文件的内容发送给用户。


很显然,目录中没有叫 osp 的文件,于是就看 osp/,增加了一个 /,也就是看有没有名为 {alias指定目录}/osp/ 的目录。


即,当我们访问 uri 时,如果访问资源为一个目录,并且 uri 没有以正斜杠 / 结尾,那么nginx 服务就会返回一个301跳转,目标地址就是要加一个正斜杠/


一种最简单的方式就是直接访问一个具体的文件,如 http.xxxxxx.cn/osp/index.html,这样就不会发生重定向了。但是,这样方式不够优雅,每次都要输入完整的文件路径。


另一种方式是调整 nginx 中关于重定向的配置,nginx 重定向中的三个配置:absolute_redirectserver_name_in_redirectport_in_redirect


absolute_redirect通过该指令控制 nginx 发出的重定向地址是相对地址还是绝对地址:


1、如果设置为 off,则 nginx 发出的重定向将是相对的,没有域名和端口, 也就没有server_name_in_redirectport_in_redirect什么事儿了。


image.png


加了这个配置后,尽管也会发生重定向,但是不会在路径上加上域名和端口了。


2、如果设置为 on,则 nginx 发出的重定向将是绝对的;只有 absolute_redirect 设置为 onserver_name_in_redirectport_in_redirect 的设置才有作用。


image.png


nginx 开启 gzip 静态压缩提升效率


gzip 是一种格式,也是一种 linux 下的解压缩工具,我们使用 gzipapp.js 文件压缩后,原始文件就变为了以.gz结尾的文件,同时文件大小从 42571 减小到 11862。


image.png


目前,对静态资源压缩有两种形式:



  • 动态压缩: 服务器在返回任何的静态文件之前,由服务器对每个请求压缩在进行输出。

  • 静态压缩:服务器直接使用现成的扩展名为 .gz 的预压缩文件,直接进行输出。


我们知道 gzipCPU 密集型的,实时动态压缩比较消耗 CPU 资源。为进一步提高 nginx 的性能,我们可以使用静态 gzip 压缩,提前将需要压缩的文件压缩好,当请求到达时,直接发送压缩好的.gz文件,如此就减轻了服务器 CPU 的压力,提高了性能。


因此,我们一般采用静态压缩的方式,实现静态压缩有以下两个步骤:


1. 生成gzip压缩文件


在利用webpack打包的时候,我们就把文件进行压缩,配置如下:


const isProduction = process.env.NODE_ENV === 'production'

if (isProduction) {
config.plugins.push(
new CompressionWebpackPlugin({
// 采用gzip进行压缩
algorithm: 'gzip',
test: /\.js$|\.html$|\.json$|\.css/,
threshold: 10240
})
)
}

可以看到,多生成了一个以.gz结尾的文件,然后把.gz后缀的文件上传到服务器中即可。


image.png


2. 在 nginx 开启支持静态压缩的模块


nginx配置中加上如下配置:


gzip_static on;

如果不加的话,访问的时候就会找不到,报404错误,因为服务器只有.gz的文件,没有原始文件。


总结


前端项目nginx部署主要的配置基本上就是上面提到的这些。


首先是location路由的四种写法;


接着就是分清楚rootalias的区别;


当项目较多时需要使用二级路由时,需要注意重定向的配置;


如果你的项目文件较大,可以开启gzip

作者:小p
来源:juejin.cn/post/7270902621065560120
压缩提升传输效率。

收起阅读 »

一次软考高项的经历分享

1 缘自同事 22年1月,去同事家聚餐,第一次听他说起软考高项,并向我讲述了考过的种种好处,如:认定市级人才,申请租房补贴,获得高级职称等等。 热心的同事还分享了一个他报的网课,不到300块钱,同时鼓励我试试一定能考过,多注意练练字因为有一科考论文写作(通常工...
继续阅读 »

1 缘自同事


22年1月,去同事家聚餐,第一次听他说起软考高项,并向我讲述了考过的种种好处,如:认定市级人才,申请租房补贴,获得高级职称等等。


热心的同事还分享了一个他报的网课,不到300块钱,同时鼓励我试试一定能考过,多注意练练字因为有一科考论文写作(通常工作以后,普遍使用电脑,书写会慢慢退化)


对软考高项一无所知的我,回去查了查:软考高项,全称信息系统项目管理师,是由工信部和人社部组织的,计算机技术与软件专业技术资格考试。通过率大概在15%左右,一年考两次,一次考一天,一天考三科:综合知识、案例分析、论文。每一科满分75分,都超过45分才算合格,还是很有难度的。


2 第一次备考


网上查了很多资料,验证了同事所说,思索再三,终于下定决心,买了书报了课开始学习。万事开头难,1月和2月,忙着年底总结和放假过年,没时间看,直到3月初才算正式开始。


工作后,真正属于自己的时间,并不多:早上(20分钟)、上下班路上(30+30分钟)、晚上(1.5小时),以及不加班的晚上和周末。这样算下来,平均每天可学习3个小时。早上背诵,上下班路上听课程音频,晚上仔细看视频课,记笔记做练习。


很快来到5月,越临近考试越焦虑,觉得很多知识点还没掌握,一点信心都没有。结果因为疫情,考试取消了,自己松了一大口气,不是自己不努力,天意如此。因为惯性,6月虽有放松,但还是坚持每天2小时的学习。7月,公寓搬进了一位同事,晚上不能像之前一样任性学习了。8月和9月,虽然没停止学习,实际却未进入专注的状态。十一之后,紧张了起来,认真学习了大半个月,开始有了点信心。11月5日考完,觉得综合知识能过,案例有风险,论文写了那么多,应该也没问题。12月查分,结果论文才30分,大跌眼镜。



查到分数的那一天,忽然感觉心累,付出了这么多时间,憧憬了这么久,这样的结果令人有些沮丧。


3 第二次备考


12月和1月,疫情肆虐。在阳和阳康之间,转眼到了2月,一直还没开始学。3月,听说要换教材,又慌了神,但视频课还是旧教材,尚未更新完。4月,重新看新教材的视频。5月,铆足了劲学习。5月27日,因为换新教材的事,觉得考不过也没事,享受过程,心态放开了,找考场时还随手拍了一张照。



这次考完,综合知识,尤其是前十几道题,是跟着感觉选的,基本都没复习到。案例题里的数据元,也是闻所未闻。论文,考之前在草纸上列了下结构,字写的比上次漂亮些,画了个表占了很多行,最后的字数反而多了。感觉三科都在边缘徘徊,后来老师讲了讲答案,对了对,感觉这次的关键还是在论文。


7月20日,成绩可以查了,在老四季里一遍又一遍的刷着网络,看着群里的消息,大部分都挂在论文,在20-30分之间,感觉自己这次也悬了,刷了很久终于刷出来了,论文46分,多一分过了,自己成了群里第一个过的。



这是查分时所在的老四季,一家新开的店,这两次软考,真的是跨了一年四季。



4 启示


启示1:去到新的城市,一定花时间了解政府的人才政策。除了埋头工作,有机会多和本地的同事交流,他们随口谈及的地方政策,也许会为自己打开一扇窗户。


启示2:目标确立后,无论路多么漫长,唯有坚持才是不懈的动力。原以为在工作之外,自己抽出时间学习,已经很不容易了,但看到几位群友的分享,感慨真是难外有难。


“第一次考试,挂在论文子题目甘特图上。这次二胎还在哺乳期,明显感觉脑子不够用,时间不够用,大宝需要妈妈,二宝也需要妈妈,鼓起很大勇气才决定二刷!每个起夜哺乳的夜晚都在听视频课,复习时间都是一点点挤出来的,中间有崩溃有大哭有想放弃的时刻,但还是坚持了4个多月…”


“扣除一次疫情不能考,考了7次,孩子没读初中,考到孩子初中毕业的时候终于过了!我抱着孩子哭的稀里哗啦,真的,我在想,如果还不过,我还有什么支撑下去的动力?老天看到了我的坚持,感谢”


“这是我考的第三次,52岁,退休前的任务终于完成”


启示3:以平常心看待运气。这次考试,很多群友的论文都在25-35分之间,群里统计的论文通过率低于18%。其实自己的论文准备的并不充分,感觉一些群友都比自己准备的好,尤其是前几次论文过了而这次没过的,也许这就是难以捉摸的运气吧。


5 后续


8月22日,电子证书可以下载了,真是七夕的好礼物。


在个税申报的app中,上传了证书,估计年末能退3600x20%或3600x25%,能把课程、书本和报名费赚回来。



在省级政府采购平台中,注册评审专家账号并上传了证书,提交后也审核通过了,等着体验一把政府采购项目的评审。

收起阅读 »

当程序员纠结中午应该吃什么,那就用pygame来解决吧

写多了kotlin和android,最近想搞点小东西,于是拿出了长期没有宠爱的python,打算搞个小项目 想想应该写什么,对了,该吃饭了,诶,刚好,写一个能随机选择吃什么的小程序吧,只需要点击按钮,就会随机出现菜谱,然后再点一下,就会得出今天吃什么的结论 思...
继续阅读 »

写多了kotlin和android,最近想搞点小东西,于是拿出了长期没有宠爱的python,打算搞个小项目


想想应该写什么,对了,该吃饭了,诶,刚好,写一个能随机选择吃什么的小程序吧,只需要点击按钮,就会随机出现菜谱,然后再点一下,就会得出今天吃什么的结论


思路是这样的,读入一个txt文件,文件中写满想吃的东西,做到数据和代码区分,然后开始写UI,UI通过按钮点击随机展示美食即可


麻辣香锅
糖醋排骨
红烧肉
...

    import pygame
import random

class App:
   def __init__(self):
       # 初始化 Pygame
       pygame.init()

       # 创建窗口和设置标题
       self.window_size = (600, 300)
       self.window = pygame.display.set_mode(self.window_size)
       pygame.display.set_caption("What to Eat Today")

       # 设置字体对象
       self.font = pygame.font.Font('myfont.ttf', 32)

       # 加载菜单数据
       self.menu = []
       with open("menu.txt", "r") as f:
           for line in f:
               line = line.strip()
               if line != "":
                   self.menu.append(line)
               print(line) # 打印数据

if __name__ == "__main__":
   app = App()


运行一下


image-20230828201918635.png


nice,文件已经读入


这个时候的UI是一闪而过的,因为程序瞬间就执行完毕了,ok,那么我们就需要用一个循环维持UI窗口,然后设置开始选择按键,以及键盘控制按键,同时设置变量


today_menu表示今天吃的东西,


btn_start_stop表示按键文字,


cur_menu表示正处于随机中的美食,当我们按下开始按键时,cur_menu开始变换,当我们按下关闭按键时,cur_menu的数据就赋值到today_menu中,


show_wheel表示当前正处于随机中,还是已经结束了


只要增加一个无限循环,一切就会好起来的



       # 随机选择一道菜
       self.today_menu = ""
       self.btn_start_stop = "start"
       self.cur_menu = ""

       # 游戏主循环
       self.running = True
       self.show_wheel = False


       # 开关程序
       while self.running:
           for event in pygame.event.get():
               if event.type == pygame.QUIT:
                   self.running = False
               elif event.type == pygame.MOUSEBUTTONDOWN:
               ...

               # 增加一个elif 按键s,show_wheel为true, 按下q, show_wheel为false
               elif event.type == pygame.KEYDOWN:
               ...

运行结果


image-20230828202631700.png


现在已经有了窗口,接下来需要在上面画东西了


所用到的就是draw函数



   def draw(self):
       # 绘制界面
       self.window.fill((255, 255, 255))

       # 绘制菜单
       menu_surface = self.font.render(f"Today's Menu: {self.today_menu}", True, (0, 0, 0))
       ...

       # 绘制按钮
       button_size = min(self.window_size[0] // 4, self.window_size[1] // 4)
    ...

       btn_start = self.font.render(f"{self.btn_start_stop}", True, (0, 0, 0))
        # 缩小start 文字字号 以适应按钮
       btn_start = pygame.transform.scale(btn_start, (button_size // 3 * 2, button_size // 3 * 2))
       self.window.blit(btn_start, (button_x + button_size // 2 - btn_start.get_width() // 2, button_y + button_size // 2 - btn_start.get_height() // 2))

       # 滚轮动画
       ...
       pygame.display.update()

运行


image-20230828202741990.png


上面的代码仅仅能够展示一个静态的页面,


虽然界面平平无奇,似乎只有两行字?不然,实际上暗藏玄🐔,只要我们加上这段,



       # 绘制滚轮动画
       if self.show_wheel:
           wheel_size = min(self.window_size) // 2
           wheel_x = self.window_size[0] // 2 - wheel_size // 2
           wheel_y = self.window_size[1] // 2 - wheel_size // 2
           wheel_rect = pygame.Rect(wheel_x, wheel_y, wheel_size, wheel_size)
...

           # 随机选择并显示菜谱
           menu_index = random.randint(0, len(self.menu) - 1)
           menu_surface = self.font.render(self.menu[menu_index], True, (0, 0, 0))
           self.window.blit(menu_surface, (wheel_x + wheel_size // 2 - menu_surface.get_width() // 2, wheel_y + wheel_size // 2 - menu_surface.get_height() // 2))
           self.cur_menu = self.menu[menu_index]

当我们点击“start"


QQ20230828-203131-HD.gif


发现中间的菜谱动了起来,动了起来!都是大家爱吃的,只需要点击右边的stop就可以固定结果!


真正麻烦的在于那个滚轮动画,可以想见,我们需要额外的一个无限循环,每一帧都要修改cur_menu,同时更新动画中的菜谱,然后点击stop后,将cur_menu赋值给到today_menu,最麻烦的不是这些逻辑,而是位置,滚轮动画的位置设置为窗口正中间,然后画了两条线,看起来更好看,有一种,狙击枪瞄准的感觉


最后,进行简单优化,比如设置定时关闭等,全代码如下,如果你也不知道吃什么,就用这段代码 + 在同目录自定义一个txt文件,把自己想吃的写上去吧



import pygame
import random

class App:
   def __init__(self):
       # 初始化 Pygame
       pygame.init()

       # 创建窗口和设置标题
       self.window_size = (600, 300)
       self.window = pygame.display.set_mode(self.window_size)
       pygame.display.set_caption("What to Eat Today")

       # 设置字体对象
       self.font = pygame.font.Font('myfont.ttf', 32)

       # 加载菜单数据
       self.menu = []
       with open("menu.txt", "r") as f:
           for line in f:
               line = line.strip()
               if line != "":
                   self.menu.append(line)

       # 随机选择一道菜
       self.today_menu = ""
       self.btn_start_stop = "start"
       self.cur_menu = ""

       # 游戏主循环
       self.running = True
       self.show_wheel = False
       self.wheel_count = 0     # 记录滚轮动画播放的帧数


       # 开关程序
       while self.running:
           for event in pygame.event.get():
               if event.type == pygame.QUIT:
                   self.running = False
               elif event.type == pygame.MOUSEBUTTONDOWN:
                   if not self.show_wheel:
                       self.show_wheel = True
                       self.wheel_count = 0  # 点击按钮后重置计数器为0
                       self.btn_start_stop = "stop"
                   else:
                       self.show_wheel = False
                       self.today_menu = self.cur_menu  # 点击停止赋值
                       self.btn_start_stop = "start"

               # 增加一个elif 按键s,show_wheel为true, 按下q, show_wheel为false
               elif event.type == pygame.KEYDOWN:
                   if event.key == pygame.K_s:  # 按下 s 键
                       self.show_wheel = True
                       self.wheel_count = 0  # 重置计数器为0
                   elif event.key == pygame.K_q:  # 按下 q 键
                       self.show_wheel = False
                       self.today_menu = self.cur_menu  # 停止赋值

           self.draw()

   def draw(self):
       # 绘制界面
       self.window.fill((255, 255, 255))

       # 绘制菜单
       menu_surface = self.font.render(f"Today's Menu: {self.today_menu}", True, (0, 0, 0))
       menu_x = self.window_size[0] // 2 - menu_surface.get_width() // 2
       menu_y = self.window_size[1] // 2 - menu_surface.get_height() // 2
       self.window.blit(menu_surface, (menu_x, menu_y))

       # 绘制按钮
       button_size = min(self.window_size[0] // 4, self.window_size[1] // 4)
       button_x = self.window_size[0] - button_size - 20
       button_y = self.window_size[1] - button_size - 20
       button_rect = pygame.Rect(button_x, button_y, button_size, button_size)
       pygame.draw.circle(self.window, (255, 0, 0), (button_x + button_size // 2, button_y + button_size // 2), button_size // 2)
       pygame.draw.rect(self.window, (255, 255, 255), button_rect.inflate(-button_size // 8, -button_size // 8))

       btn_start = self.font.render(f"{self.btn_start_stop}", True, (0, 0, 0))
        # 缩小start 文字字号 以适应按钮
       btn_start = pygame.transform.scale(btn_start, (button_size // 3 * 2, button_size // 3 * 2))
       self.window.blit(btn_start, (button_x + button_size // 2 - btn_start.get_width() // 2, button_y + button_size // 2 - btn_start.get_height() // 2))

       # 绘制滚轮动画
       if self.show_wheel:
           wheel_size = min(self.window_size) // 2
           wheel_x = self.window_size[0] // 2 - wheel_size // 2
           wheel_y = self.window_size[1] // 2 - wheel_size // 2
           wheel_rect = pygame.Rect(wheel_x, wheel_y, wheel_size, wheel_size)
           pygame.draw.circle(self.window, (0, 0, 0), (wheel_x + wheel_size // 2, wheel_y + wheel_size // 2), wheel_size // 2)
           pygame.draw.circle(self.window, (255, 255, 255), (wheel_x + wheel_size // 2, wheel_y + wheel_size // 2), wheel_size // 2 - 10)
           pygame.draw.line(self.window, (0, 0, 0), (wheel_x + 10, wheel_y + wheel_size // 2), (wheel_x + wheel_size - 10, wheel_y + wheel_size // 2))
           pygame.draw.line(self.window, (0, 0, 0), (wheel_x + wheel_size // 2, wheel_y + 10), (wheel_x + wheel_size // 2, wheel_y + wheel_size - 10))

           # 随机选择并显示菜谱
           menu_index = random.randint(0, len(self.menu) - 1)
           menu_surface = self.font.render(self.menu[menu_index], True, (0, 0, 0))
           self.window.blit(menu_surface, (wheel_x + wheel_size // 2 - menu_surface.get_width() // 2, wheel_y + wheel_size // 2 - menu_surface.get_height() // 2))
           self.cur_menu = self.menu[menu_index]
           # 播放一定帧数后停止动画
           self.wheel_count += 1
           if self.wheel_count > 500:
               self.show_wheel = False
               self.today_menu = self.cur_menu  # 自动停止赋值
       pygame.display.update()

if __name__ == "__main__":
   app = App()
作者:小松漫步
来源:juejin.cn/post/7272257829770887223


收起阅读 »

坏了!要长脑子了...这么多前端框架没听过

web
市面上有很多不同的 JavaScript 框架,很难对它们一一进行跟踪。在本文中,我们将重点介绍最受欢迎的几种,并探讨开发人员喜欢或不喜欢它们的原因。 React 官网链接 React 是一个用于构建用户界面的 JavaScript 库。它由 Faceboo...
继续阅读 »

市面上有很多不同的 JavaScript 框架,很难对它们一一进行跟踪。在本文中,我们将重点介绍最受欢迎的几种,并探讨开发人员喜欢或不喜欢它们的原因。


React



官网链接


React 是一个用于构建用户界面的 JavaScript 库。它由 Facebook 和一个由个人开发者和公司组成的社区维护。React 可以作为开发单页或移动应用程序的基础。然而,React 只关心将数据呈现给 DOM,因此创建 React 应用程序通常需要使用额外的库来进行状态管理、路由和与 API 的交互。React 还用于构建可重用的 UI 组件。从这个意义上讲,它的工作方式很像 Angular 或 Vue 等 JavaScript 框架。然而,React 组件通常以声明式方式编写,而不是使用命令式代码,这使得它们更容易阅读和调试。正因为如此,许多开发人员更喜欢使用 React 来构建 UI 组件,即使他们不使用它作为整个前端框架。


优点:



  • React 快速而高效,因为它使用虚拟 DOM 而不是操纵真实的 DOM。

  • React 很容易学习,因为它的声明性语法和清晰的文档。

  • React 组件是可重用的,使代码维护更容易。


缺点:



  • React 有一个很大的学习曲线,因为它是一个复杂的 JavaScript 库。

  • React 不是一个成熟的框架,因此它需要使用额外的库来完成许多任务。


Next.js



官网链接


Next.js 是一个 javascript 库,支持 React 应用程序的服务器端渲染。这意味着 next.js 可以在将 React 应用程序发送到客户端之前在服务器上渲染它。这有几个好处。首先,它允许您预呈现组件,以便当用户请求它们时,它们已经在客户机上可用。其次,它允许爬虫更容易地索引你的内容,从而为你的 React 应用程序提供更好的 SEO。最后,它可以通过减少客户机为呈现页面而必须执行的工作量来提高性能。


以下是开发者喜欢 Next.js 的原因:




  • js 使得无需做任何配置就可以轻松地开始服务器端渲染。




  • js 会自动对应用程序进行代码拆分,以便每个页面只在被请求时加载,这可以提高性能。
    缺点:




  • 如果不小心,next.js 会使应用程序代码库变得更复杂,更难以维护。




  • 一些开发人员发现 next.js 的内置特性固执己见且不灵活。




Vue.js



官网链接


Vue.js 是一个用于构建用户界面和单页应用程序的开源 JavaScript 框架。与 React 和 Angular 等其他框架不同,Vue.js 被设计为轻量级且易于使用。Vue.js 库可以与其他库和框架结合使用,也可以作为创建前端 web 应用程序的独立工具使用。Vue.js 的一个关键特性是它的双向数据绑定,当模型发生变化时,它会自动更新视图,反之亦然。这使得它成为构建动态用户界面的理想选择。此外,Vue.js 还提供了许多内置功能,如模板系统、响应系统和事件总线。这些特性使创建复杂的应用程序成为可能,而不必依赖第三方库。因此,Vue.js 已经成为近年来最流行的 JavaScript 框架之一。


优点:



  • Vue.js 很容易学习,因为它的小尺寸和清晰的文档。

  • Vue.js 组件是可重用的,这使得代码维护更容易。

  • 由于虚拟 DOM 和异步组件加载,Vue.js 应用程序非常快。


缺点:



  • 虽然 Vue.js 很容易学习,但如果你想掌握它的所有功能,它有一个很大的学习曲线。

  • Vue.js 没有像其他框架那样多的库和工具。


Angular



官网链接


Angular 是一个 JavaScript 框架,用于用 JavaScript、html 和 Typescript 构建 web 应用和应用。Angular 是由 Google 创建和维护的。Angular 提供了双向数据绑定,这样对模型的更改就会自动传播到视图。它还提供了一种声明性语法,使构建动态 ui 变得容易。最后,Angular 提供了许多有用的内置服务,比如 HTTP 请求处理,以及对路由和模板的支持。


优点:



  • Angular 有一个庞大的社区和许多可用的库和工具。

  • Angular 很容易学习,因为它有组织良好的文档和清晰的语法。


缺点:



  • 虽然 Angular 很容易学习,但如果你想掌握它的所有特性,它有一个很大的学习曲线。

  • Angular 不像其他一些框架那样轻量级。


Svelte



官网链接


简而言之,Svelte 是一个类似于 React、Vue 或 Angular 的 JavaScript 框架。然而,这些框架使用虚拟 DOM(文档对象模型)来区分视图之间的变化,而 Svelte 使用了一种称为 DOM 区分的技术。这意味着它只更新 DOM 中已更改的部分,从而实现更高效的呈现过程。此外,Svelte 还包括一些其他框架没有的内置优化,例如自动批处理 DOM 更新和代码分割。这些特性使 Svelte 成为高性能应用程序的一个很好的选择。


优点:




  • Svelte 有其他框架没有的内置优化,比如代码分割。




  • 由于其清晰的语法和组织良好的文档,Svelte 很容易学习。
    缺点:




  • 虽然 Svelte 很容易学习,但如果你想掌握它的所有功能,它有一个很大的学习曲线。




  • Svelte 没有像其他框架那样多的库和工具。




Gatsby



官网链接


Gatsby 是一个基于 React 的免费开源框架,可以帮助开发人员构建快速的网站和应用程序。它使用尖端技术,使建立网站和应用程序的过程更加高效。它的一个关键特性是能够预取资源,以便在需要时立即可用。这使得盖茨比网站非常快速和响应。使用 Gatsby 的另一个好处是,它允许开发人员使用 GraphQL 查询来自任何来源的数据,从而使构建复杂的数据驱动应用程序变得容易。此外,Gatsby 附带了许多插件,使其更易于使用,包括用于 SEO,分析和图像优化的插件。所有这些因素使 Gatsby 成为构建现代网站和应用程序的一个非常受欢迎的选择。


优点:




  • 由于使用了预取,Gatsby 网站的速度和响应速度非常快。




  • 由于对 GraphQL 的支持,Gatsby 使构建复杂的数据驱动应用程序变得容易。




  • Gatsby 附带了许多插件,使其更易于使用。
    缺点:




  • 虽然 Gatsby 很容易使用,但如果你想掌握它的所有功能,它有一个很大的学习曲线。




  • Gatsby 没有像其他框架那样多的库和工具。




Nuxt.js



官网链接


js 是一个用于构建 JavaScript 应用程序的渐进式框架。它基于 Vue.js,并附带了一组工具和库,可以轻松创建可以在服务器端和客户端呈现的通用应用程序。js 还提供了一种处理异步数据和路由的方法,这使得它非常适合构建高度交互的应用程序。此外,nuxt.js 附带了一个 CLI 工具,可以很容易地构建新项目并构建、运行和测试它们。使用 nuxt.js,您可以创建快速、可靠和可扩展的令人印象深刻的 JavaScript 应用程序。


优点:




  • js 易于使用和扩展。




  • 由于服务器端渲染,nuxt.js 应用程序快速响应。
    缺点:




  • 虽然 nuxt.js 很容易使用,但如果你想掌握它的所有功能,它有一个很大的学习曲线。




  • nuxt.js 没有像其他框架那样多的库和工具。




Ember.js



官网链接


Ember.js 以其优于配置的约定方法而闻名,这使得开发人员更容易开始使用该框架。它还为数据持久化和路由等常见任务提供了内置库,从而加快了开发速度。尽管 Ember.js 有一个陡峭的学习曲线,但它为开发人员提供了创建富 web 应用程序的灵活性和强大功能。如果你正在寻找一个前端 JavaScript 框架来构建 spa, Ember.js 绝对值得考虑。


优点:




  • Ember.js 使用约定而不是配置,这使得它更容易开始使用框架。




  • Ember.js 为数据持久化和路由等常见任务提供了内置库。




  • Ember.js 为开发人员创建富 web 应用程序提供了很大的灵活性和能力。
    缺点:




  • Ember.js 有一个陡峭的学习曲线。




  • Ember.js 没有像其他框架那样多的库和工具。




Backbone.js



官网链接


Backbone.js 是一个轻量级 JavaScript 库,允许开发人员创建单页面应用程序。它基于模型-视图-控制器(MVC)体系结构,这意味着它将数据和逻辑从用户界面中分离出来。这使得代码更易于维护和扩展,也使创建复杂的应用程序变得更容易。Backbone.js 还包含了许多使其成为开发移动应用程序的理想选择的特性,例如将数据绑定到 HTML 元素的能力以及对触摸事件的支持。因此,对于想要创建快速响应的应用程序的开发人员来说,Backbone.js 是一个受欢迎的选择。


优点:




  • js 是轻量级的,只是一个库,而不是一个完整的框架。




  • js 很容易学习和使用。




  • Backbone.js 具有很强的可扩展性,可以使用许多第三方库。
    缺点:




  • Backbone.js 不像其他框架那样提供那么多的内置功能。




  • 与其他一些框架相比,Backbone.js 只有一个小社区。




结论


总之,虽然有许多不同的 JavaScript 框架可供选择,但最流行的框架仍然相对稳定。每一种都有自己的优点和缺点,开发人员在决定在他们的项目中使用哪一种时必须权衡。虽然没有一个框架是完美的,但每个框架都有一些可以使开发更容易或更快的东西。


在选择框架时,每个人都应该考虑他们项目的具体需求,以及他们团队的技能和他们必须投入学习新框架的时间。通过考虑所有这些因素,您可以为您的项目选择最好的 JavaScript 框架!


参考链接:
blog.risingstack.com/

best-javasc…

收起阅读 »

程序员要学会“投资知识”

啊,富兰克林,那家伙总是说些深刻的道理。嗯,我们真的可以通过早睡早起变成优秀的程序员吗?早起的鸟儿可能抓住虫子,但早起的虫子会怎么样呢? 然而,富兰克林的开场白确实击中了要害 - 知识和经验确实是你最有价值的职业资产。 不幸的是,它们是有限的资产。随着新技术的...
继续阅读 »

啊,富兰克林,那家伙总是说些深刻的道理。嗯,我们真的可以通过早睡早起变成优秀的程序员吗?早起的鸟儿可能抓住虫子,但早起的虫子会怎么样呢?


然而,富兰克林的开场白确实击中了要害 - 知识和经验确实是你最有价值的职业资产。


不幸的是,它们是有限的资产。随着新技术的出现和语言环境的发展,你的知识可能会过时。不断变化的市场力量可能会使你的经验变得陈旧和无关紧要。考虑到技术和社会变革的加速步伐,这可能会发生得特别迅速。


随着你的知识价值的下降,你在公司或客户那里的价值也会降低。我们希望阻止所有这些情况的发生。


学习新知识的能力是你最关键的战略资产。但如何获取学习的方法,知道要学什么呢?


知识投资组合。


我们可以将程序员对计算过程、其工作应用领域的了解以及所有经验视为他们的知识投资组合。管理知识投资组合与管理金融投资组合非常相似:


1、定期的投资者有定期投资的习惯。


2、多样化是长期成功的关键。


3、聪明的投资者在投资组合中平衡保守和高风险高回报的投资。


4、投资者在低点买入,在高点卖出以获取最大回报。


5、需要定期审查和重新平衡投资组合。


为了在职业生涯中取得成功,你必须遵循相同的指导原则管理你的知识投资组合。


好消息是,管理这种类型的投资就像任何其他技能一样 - 它可以被学会。诀窍是从一开始就开始做,并养成习惯。制定一个你可以遵循并坚持的例行程序,直到它变成第二天性。一旦达到这一点,你会发现自己自动地吸收新的知识。


建立知识投资组合。


· 定期投资。 就像金融投资一样,你需要定期地投资你的知识投资组合,即使数量有限。习惯本身和总数量一样重要,所以设定一个固定的时间和地点 - 这有助于你克服常见的干扰。下一部分将列出一些示例目标。


· 多样化。 你知道的越多,你变得越有价值。至少,你应该了解你目前工作中特定技术的细节,但不要止步于此。计算机技术变化迅速 - 今天的热门话题可能在明天(或至少不那么受欢迎)变得几乎无用。你掌握的技能越多,你的适应能力就越强。


· 风险管理。 不同的技术均匀地分布在从高风险高回报到低风险低回报的范围内。把所有的钱都投资在高风险的股票上是不明智的,因为它们可能会突然崩盘。同样,你不应该把所有的钱都投资在保守的领域 - 你可能会错过机会。不要把你的技术鸡蛋都放在一个篮子里。


· 低买高卖。 在新兴技术变得流行之前开始学习可能就像寻找被低估的股票一样困难,但回报可能同样好。在Java刚刚发明出来后学习可能是有风险的,但那些早期用户在Java变得流行时获得了可观的回报。


· 重新评估和调整。 这是一个动态的行业。你上个月开始研究的时髦技术可能现在已经降温了。也许你需要刷新一下你很久没有使用过的数据库技术的知识。或者,你可能想尝试一种不同的语言,这可能使你在新的角色中处于更好的位置......


在所有这些指导原则中,下面这个是最简单实施的。


(程序员的软技能:ke.qq.com/course/6034346)


定期在你的知识投资组合中进行投资。


目标。


既然你有了一些指导原则,并知道何时添加什么到你的知识投资组合中,那么获取构成它的智力资产的最佳方法是什么呢?以下是一些建议:


· 每年学习一门新语言。


不同的语言以不同的方式解决相同的问题。学习多种不同的解决方案有助于拓宽你的思维,避免陷入常规模式。此外,由于充足的免费资源,学习多门语言变得更加容易。


· 每月阅读一本技术书籍。


尽管互联网上有大量的短文和偶尔可靠的答案,但要深入理解通常需要阅读更长的书籍。浏览书店页面,选择与你当前项目主题相关的技术书籍。一旦养成这个习惯,每月读一本书。当你掌握了所有当前使用的技术后,扩大你的视野,学习与你的项目无关的东西。


· 也阅读非技术书籍。


请记住,计算机是被人类使用的,而你所做的最终是为了满足人们的需求 - 这是至关重要的。你与人合作,被人雇佣,甚至可能会面临来自人们的批评。不要忘记这个方程式的人类一面,这需要完全不同的技能(通常被称为软技能,听起来可能很容易,但实际上非常具有挑战性)。


· 参加课程。


在当地大学或在线寻找有趣的课程,或者你可能会在下一个商业博览会或技术会议上找到一些课程。


· 加入当地的用户组和论坛。


不要只是作为观众成员;要积极参与。孤立自己对你的职业生涯是有害的;了解你公司之外的人在做什么。


· 尝试不同的环境。


如果你只在Windows上工作,花点时间在Linux上。如果你对简单的编辑器和Makefile感到舒适,尝试使用最新的复杂IDE,反之亦然。


· 保持更新。


关注不同于你当前工作的技术。阅读相关的新闻和技术文章。这是了解使用不同技术的人的经验以及他们使用的特定术语的极好方式,等等。


持续的投资是至关重要的。一旦你熟悉了一门新的语言或技术,继续前进并学习另一门。


无论你是否在项目中使用过这些技术,或者是否应该将它们放在你的简历上,都不重要。学习过程将拓展你的思维,开启新的可能性,并赋予你在处理任务时的新视角。思想的跨领域交流是至关重要的;尝试将你所学应用到你当前的项目中。即使项目不使用特定的技术,你仍然可以借鉴其中的思想。例如,理解面向对象编程可能会导致你编写更具结构的C代码,或者理解函数式编程范 paradigms 可能会影响你如何处理Java等等。


学习机会。


你正在狼吞虎咽地阅读,始终站在你领域的突破前沿(这并不是一项容易的任务)。然而,当有人问你一个问题,你真的不知道的时候,不要停在那里 - 把找到答案当做一个个人挑战。问问你周围的人或在网上搜索 - 不仅在主流圈子中,还要在学术领域中搜索。


如果你自己找不到答案,寻找能够找到答案的人,不要让问题无解地悬而未决。与他人互动有助于你建立你的人际网络,你可能会在这个过程中惊喜地找到解决其他无关问题的方法 - 你现有的知识投资组合将不断扩展。


所有的阅读和研究需要时间,而时间总是不够的。因此,提前准备,确保你在无聊的时候有东西可以阅读。在医院排队等候时,通常会有很好的机会来完成一本书 - 只需记得带上你的电子阅读器。否则,你可能会在医院翻阅旧年鉴,而里面的折叠页来自1973年的巴布亚新几内亚。


批判性思维。


最后一个要点是对你阅读和听到的内容进行批判性思考。你需要确保你投资组合中的知识是准确的,没有受到供应商或媒体炒作的影响。小心狂热的狂热分子,他们认为他们的观点是唯一正确的 - 他们的教条可能不适合你或你的项目。


不要低估商业主义的力量。搜索引擎有时只是优先考虑流行的内容,这并不一定意味着这是你最好的选择;内容提供者也可以支付费用来使他们的材料排名更高。书店有时会将一本书突出地摆放,但这并不意味着它是一本好书,甚至可能不受欢迎 - 这可能只是有人支付了那个位置。


(程序员的软技能:ke.qq.com/course/6034346)


批判性分析你所阅读和听到的内容。


批判性思维本身就是一个完整的学科,我们鼓励你深入研究和学习这门学科。让我们从这里开始,提出一些发人深省的问题。


· 五问“为什么”。


我最喜欢的咨询技术之一是至少连续问五次“为什么”。这意味着在得到一个答案后,你再次问“为什么”。像一个坚持不懈的四岁孩子提问一样重复这个过程,但请记住要比孩子更有礼貌。这样做可以让你更接近根本原因。


· 谁从中受益?


尽管听起来可能有点功利主义,但追踪金钱的流动往往可以帮助你理解潜在的联系。其他人或其他组织的利益可能与你的利益保持一致,也可能不一致。


· 背景是什么?


一切都发生在自己的背景下。这就是为什么声称“解决所有问题”的解决方案通常站不住脚,宣扬“最佳实践”的书籍或文章经不起审查的原因。 “对谁最好?” 是一个需要考虑的问题,以及关于前提条件、后果以及情况是短期还是长期的问题。


· 在何种情况下和何地可以起作用?


在什么情况下?是否已经太晚了?是否还太早了?不要只停留在一阶思维(接下来会发生什么);参与到二阶思维中:接下来会发生什么?


· 为什么这是一个问题?


是否有一个基础模型?这个基础模型是如何工作的?


不幸的是,如今找到简单的答案是具有挑战性的。然而,通过广泛的知识投资组合,并对你遇到的广泛技术出版物进行一些批判性分析,你可以理解那些复杂的答案。



作者:用心看世界Heart
来源:juejin.cn/post/7271908000414580776

收起阅读 »

兄弟,王者荣耀的段位排行榜是通过Redis实现的?

在王者荣耀中,我们会打排位赛,而且大家最关注的往往都是你的段位,还有在好友中的排名。 作为程序员的你,思考过吗,这个段位排行榜是怎么实现的?了解它的实现原理,会不会对上分有所帮助? 看看我的排名,你就知道了,答案是否定的,哈哈。 一、排行榜设计方案 从技...
继续阅读 »

在王者荣耀中,我们会打排位赛,而且大家最关注的往往都是你的段位,还有在好友中的排名。


作为程序员的你,思考过吗,这个段位排行榜是怎么实现的?了解它的实现原理,会不会对上分有所帮助?



看看我的排名,你就知道了,答案是否定的,哈哈。




一、排行榜设计方案


从技术角度而言,我们可以根据排行榜的类型来选择不同技术方案来进行排行榜设计。


1、数据库直接排序


在低数据量场景中,用数据库直接排序做排行榜的,有很多。


举个栗子,比如要做一个程序员薪资排行榜,看看哪个城市的程序员最有钱。


根据某招聘网站的数据,2023年中国国内程序员的平均月薪为1.2万元,其中最高的是北京,达到了2.1万元,最低的是西安,只有0.7万元。


以下是几个主要城市的程序员平均月薪排行榜:



  1. 北京:2.1万元

  2. 上海:1.9万元

  3. 深圳:1.8万元

  4. 杭州:1.6万元

  5. 广州:1.5万元

  6. 成都:1.3万元

  7. 南京:1.2万元

  8. 武汉:1.1万元

  9. 西安:0.7万元


从这个榜单中可以看出,我拖了大家的后腿,抱歉了。



这个就可以用数据库来做,一共也没有多少个城市,来个百大,撑死了。


对于这种量级的数据,加好索引,用好top,都不会超过100ms,在请求量小、数据量小的情况下,用数据库做排行榜是完全没有问题的。


2、王者荣耀好友排行


这类榜单是根据自己好友数据来进行排行的,这类榜单不用将每位好友的数据都存储在数据库中,而是通过获取自己的好友列表,获取好友的实时分数,在客户端本地进行本地排序,展现出王者荣耀好友排行榜,因为向数据库拉取数据是需要时间的,比如一分钟拉取一次,因为并非实时拉取,这类榜单对数据库的压力还是较小的。



下面探索一下在Java中使用Redis实现高性能的排行榜是如何实现的?



二、Redis实现计数器


1、什么是计数器功能?


计数器是一种常见的功能,用于记录某种事件的发生次数。在应用中,计数器可以用来跟踪用户行为、统计点击次数、浏览次数等。


例如,您可以使用计数器来记录一篇文章被阅读的次数,或者统计某个产品被购买的次数。通过跟踪计数,您可以了解数据的变化趋势,从而做出更明智的决策。


2、Redis实现计数器的原理


Redis是一款高性能的内存数据库,提供了丰富的数据结构和命令,非常适合实现计数器功能。在Redis中,我们可以使用字符串数据类型以及相关的命令来实现计数器。


(1)使用INCR命令实现计数器


Redis的INCR命令是一个原子操作,用于将存储在键中的数字递增1。如果键不存在,将会创建并初始化为0,然后再执行递增操作。这使得我们可以轻松地实现计数器功能。


让我们通过Java代码来演示如何使用Redis的INCR命令实现计数器:


import redis.clients.jedis.Jedis;

public class CounterExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String articleId = "article:123";
String viewsKey = "views:" + articleId;

// 使用INCR命令递增计数
long views = jedis.incr(viewsKey);

System.out.println("Article views: " + views);

jedis.close();
}
}

在上面的代码中,我们使用了Jedis客户端库来连接Redis服务器,并使用INCR命令递增一个存储在views:article:123键中的计数器。每次执行该代码,计数器的值都会递增,并且我们可以轻松地获取到文章的浏览次数。


(2)使用INCRBY命令实现计数器


除了单次递增1,我们还可以使用INCRBY命令一次性增加指定的数量。这对于一些需要一次性增加较大数量的场景非常有用。


让我们继续使用上面的例子,但这次我们使用INCRBY命令来增加浏览次数:


import redis.clients.jedis.Jedis;

public class CounterExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String articleId = "article:123";
String viewsKey = "views:" + articleId;

// 使用INCRBY命令递增计数
long views = jedis.incrBy(viewsKey, 10); // 一次增加10

System.out.println("Article views: " + views);

jedis.close();
}
}

在上述代码中,我们使用了INCRBY命令将文章浏览次数一次性增加了10。这在统计需要一次性增加较多计数的场景中非常有用。


通过使用Redis的INCRINCRBY命令,我们可以轻松实现高性能的计数器功能。这些命令的原子性操作保证了计数的准确性,而且非常适用于需要频繁更新计数的场景。


三、通过Redis实现“王者荣耀”排行榜?


王者荣耀的排行榜是不是用Redis做的,我不得而知,但,我的项目中,排行榜确实是用Redis做的,这是实打实的。



看见了吗?掌握算法的男人,到哪里都是无敌的。




1、什么是排行榜功能?


排行榜是一种常见的功能,用于记录某种项目的排名情况,通常按照某种规则对项目进行排序。在社交媒体、游戏、电商等领域,排行榜功能广泛应用,可以增强用户的参与度和竞争性。例如,社交媒体平台可以通过排行榜展示最活跃的用户,游戏中可以展示玩家的分数排名等。


2、Redis实现排行榜的原理


在Redis中,我们可以使用有序集合(Sorted Set)数据结构来实现高效的排行榜功能。有序集合是一种键值对的集合,每个成员都与一个分数相关联,Redis会根据成员的分数进行排序。这使得我们能够轻松地实现排行榜功能。


(1)使用ZADD命令添加成员和分数


Redis的ZADD命令用于向有序集合中添加成员和对应的分数。如果成员已存在,可以更新其分数。让我们通过Java代码演示如何使用ZADD命令来添加成员和分数到排行榜:


import redis.clients.jedis.Jedis;

public class LeaderboardExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
String player1 = "PlayerA";
String player2 = "PlayerB";

// 使用ZADD命令添加成员和分数
jedis.zadd(leaderboardKey, 1000, player1);
jedis.zadd(leaderboardKey, 800, player2);

jedis.close();
}
}

在上述代码中,我们使用ZADD命令将PlayerAPlayerB作为成员添加到leaderboard有序集合中,并分别赋予分数。这样,我们就在排行榜中创建了两名玩家的记录。


(2)使用ZINCRBY命令更新成员分数


除了添加成员,我们还可以使用ZINCRBY命令更新已有成员的分数。这在实时更新排行榜中的分数非常有用。


让我们继续使用上面的例子,但这次我们将使用ZINCRBY命令来增加玩家的分数:


import redis.clients.jedis.Jedis;

public class LeaderboardExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
String player1 = "PlayerA";
String player2 = "PlayerB";

// 使用ZINCRBY命令更新成员分数
jedis.zincrby(leaderboardKey, 200, player1); // 增加200分

jedis.close();
}
}

在上述代码中,我们使用了ZINCRBY命令将PlayerA的分数增加了200分。这种方式可以用于记录玩家的得分、积分等变化,从而实时更新排行榜数据。


通过使用Redis的有序集合以及ZADDZINCRBY等命令,我们可以轻松实现高性能的排行榜功能。这些命令的原子性操作保证了排行的准确性和一致性,非常适用于需要频繁更新排行榜的场景。



我的最强百里,12-5-6,这都能输?肯定是哪里出问题了,服务器性能?




四、计数器与排行榜的性能优化


在本节中,我们将重点讨论如何在高并发场景下优化计数器和排行榜功能的性能。通过合理的策略和技巧,我们可以确保系统在处理大量数据和用户请求时依然保持高性能。


1、如何优化计数器的性能?


(1)使用Redis事务


在高并发场景下,多个用户可能同时对同一个计数器进行操作,这可能引发并发冲突。为了避免这种情况,可以使用Redis的事务来确保原子性操作。事务将一组命令包装在一个原子性的操作中,保证这些命令要么全部执行成功,要么全部不执行。


下面是一个示例,演示如何使用Redis事务进行计数器操作:


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;

public class CounterOptimizationExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String counterKey = "view_count";
try {
// 开始事务
Transaction tx = jedis.multi();
// 对计数器执行加1操作
tx.incr(counterKey);
// 执行事务
tx.exec();
} catch (JedisException e) {
// 处理事务异常
e.printStackTrace();
} finally {
jedis.close();
}
}
}

在上述代码中,我们使用了Jedis客户端库,通过MULTI命令开启一个事务,然后在事务中执行INCR命令来增加计数器的值。最后,使用EXEC命令执行事务。如果在事务执行期间出现错误,我们可以通过捕获JedisException来处理异常。


(2)使用分布式锁


另一种优化计数器性能的方法是使用分布式锁。分布式锁可以确保在同一时刻只有一个线程能够对计数器进行操作,避免了并发冲突。这种机制可以保证计数器的更新是串行化的,从而避免了竞争条件。


以下是一个使用Redisson框架实现分布式锁的示例:


import org.redisson.Redisson;
import org.redisson.api.RLock;

public class CounterOptimizationWithLockExample {

public static void main(String[] args) {
Redisson redisson = Redisson.create();
RLock lock = redisson.getLock("counter_lock");

try {
lock.lock(); // 获取锁
// 执行计数器操作
} finally {
lock.unlock(); // 释放锁
redisson.shutdown();
}
}
}

在上述代码中,我们使用了Redisson框架来创建一个分布式锁。通过调用lock.lock()获取锁,然后执行计数器操作,最后通过lock.unlock()释放锁。这样可以保证在同一时间只有一个线程能够执行计数器操作。



2、如何优化排行榜的性能?


(1)分页查询


在排行榜中,通常会有大量的数据,如果一次性查询所有数据,可能会影响性能。为了解决这个问题,可以使用分页查询。将排行榜数据分成多个页,每次查询一小部分数据,以减轻数据库的负担。


以下是一个分页查询排行榜的示例:


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;

public class LeaderboardPaginationExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
int pageSize = 10; // 每页显示的数量
int pageIndex = 1; // 页码

// 获取指定页的排行榜数据
Set<Tuple> leaderboardPage = jedis.zrevrangeWithScores(leaderboardKey, (pageIndex - 1) * pageSize, pageIndex * pageSize - 1);

for (Tuple tuple : leaderboardPage) {
String member = tuple.getElement();
double score = tuple.getScore();
System.out.println("Member: " + member + ", Score: " + score);
}

jedis.close();
}
}

在上述代码中,我们使用zrevrangeWithScores命令来获取指定页的排行榜数据。通过计算起始索引和结束索引,我们可以实现分页查询功能。


(2)使用缓存


为了进一步提高排行榜的查询性能,可以将排行榜数据缓存起来,减少对数据库的访问。例如,可以使用Redis缓存最近的排行榜数据,定期更新缓存以保持数据的新鲜性。


以下是一个缓存排行榜数据的示例:


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;

public class LeaderboardCachingExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
String cacheKey = "cached_leaderboard";
int cacheExpiration = 300; // 缓存过期时间,单位:秒

// 尝试从缓存中获取排行榜数据
Set<Tuple> cachedLeaderboard = jedis.zrevrangeWithScores(cacheKey, 0, -1);

if (cachedLeaderboard.isEmpty()) {
// 如果缓存为空,从数据库获取数据并更新缓存
Set<Tuple> leaderboardData = jedis.zrevrangeWithScores(leaderboardKey, 0, -1);
jedis.zadd(cacheKey, leaderboardData);
jedis.expire(cacheKey, cacheExpiration);
cachedLeaderboard = leaderboardData;
}

for

(Tuple tuple : cachedLeaderboard) {
String member = tuple.getElement();
double score = tuple.getScore();
System.out.println("Member: " + member + ", Score: " + score);
}

jedis.close();
}
}

在上述代码中,我们首先尝试从缓存中获取排行榜数据。如果缓存为空,我们从数据库获取数据,并将数据存入缓存。使用expire命令来设置缓存的过期时间,以保持数据的新鲜性。


五、实际应用案例


在本节中,我们将通过两个实际的案例,展示如何使用Redis的计数器和排行榜功能来构建社交媒体点赞系统和游戏玩家排行榜系统。这些案例将帮助您更好地理解如何将Redis的功能应用于实际场景中。


1、社交媒体点赞系统案例


(1)问题背景


假设我们要构建一个社交媒体平台,用户可以在文章、照片等内容上点赞。我们希望能够统计每个内容的点赞数量,并实时显示最受欢迎的内容。


(2)系统架构



  • 每个内容的点赞数可以使用Redis的计数器功能进行维护。

  • 我们可以使用有序集合(Sorted Set)来维护内容的排名信息,将内容的点赞数作为分数。


(3)数据模型



  • 每个内容都有一个唯一的标识,如文章ID或照片ID。

  • 使用一个计数器来记录每个内容的点赞数。

  • 使用一个有序集合来记录内容的排名,以及与内容标识关联的分数。


(4)Redis操作步骤



  1. 用户点赞时,使用Redis的INCR命令增加对应内容的点赞数。

  2. 使用ZADD命令将内容的标识和点赞数作为分数添加到有序集合中。


Java代码示例


import redis.clients.jedis.Jedis;

public class SocialMediaLikeSystem {

private Jedis jedis;

public SocialMediaLikeSystem() {
jedis = new Jedis("localhost", 6379);
}

public void likeContent(String contentId) {
// 增加点赞数
jedis.incr("likes:" + contentId);

// 更新排名信息
jedis.zincrby("rankings", 1, contentId);
}

public long getLikes(String contentId) {
return Long.parseLong(jedis.get("likes:" + contentId));
}

public void showRankings() {
// 显示排名信息
System.out.println("Top content rankings:");
jedis.zrevrangeWithScores("rankings", 0, 4)
.forEach(tuple -> System.out.println(tuple.getElement() + ": " + tuple.getScore()));
}

public static void main(String[] args) {
SocialMediaLikeSystem system = new SocialMediaLikeSystem();
system.likeContent("post123");
system.likeContent("post456");
system.likeContent("post123");

System.out.println("Likes for post123: " + system.getLikes("post123"));
System.out.println("Likes for post456: " + system.getLikes("post456"));

system.showRankings();
}
}

在上述代码中,我们创建了一个名为SocialMediaLikeSystem的类来模拟社交媒体点赞系统。我们使用了Jedis客户端库来连接到Redis服务器,并实现了点赞、获取点赞数和展示排名的功能。每当用户点赞时,我们会使用INCR命令递增点赞数,并使用ZINCRBY命令更新有序集合中的排名信息。通过调用zrevrangeWithScores命令,我们可以获取到点赞数排名前几的内容。



2、游戏玩家排行榜案例


(1)问题背景


在一个多人在线游戏中,我们希望能够实时追踪和显示玩家的排行榜,以鼓励玩家参与并提升游戏的竞争性。


(2)系统架构



  • 每个玩家的得分可以使用Redis的计数器功能进行维护。

  • 我们可以使用有序集合来维护玩家的排名,将玩家的得分作为分数。


(3)数据模型



  • 每个玩家都有一个唯一的ID。

  • 使用一个计数器来记录每个玩家的得分。

  • 使用一个有序集合来记录玩家的排名,以及与玩家ID关联的得分。


(4)Redis操作步骤



  1. 玩家完成游戏时,使用Redis的ZINCRBY命令增加玩家的得分。

  2. 使用ZREVRANK命令获取玩家的排名。


(5)Java代码示例


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;

import java.util.Set;

public class GameLeaderboard {

private Jedis jedis;

public GameLeaderboard() {
jedis = new Jedis("localhost", 6379);
}

public void updateScore(String playerId, double score) {
jedis.zincrby("leaderboard", score, playerId);
}

public Long getPlayerRank(String playerId) {
return jedis.zrevrank("leaderboard", playerId);
}

public Set<Tuple> getTopPlayers(int count) {
return jedis.zrevrangeWithScores("leaderboard", 0, count - 1);
}

public static void main(String[] args) {
GameLeaderboard leaderboard = new GameLeaderboard();
leaderboard.updateScore("player123", 1500);
leaderboard.updateScore("player456", 1800);
leaderboard.updateScore("player789", 1600);

Long rank = leaderboard.getPlayerRank("player456");
System.out.println("Rank of player456: " + (rank != null ? rank + 1 : "Not ranked"));

Set<Tuple> topPlayers = leaderboard.getTopPlayers(3);
System.out.println("Top players:");
topPlayers.forEach(tuple -> System.out.println(tuple.getElement() + ": " + tuple.getScore()));
}
}

在上述代码中,我们创建了一个名为GameLeaderboard的类来模拟游戏玩家排行榜系统。我们同样使用Jedis客户端库来连接到Redis服务器,并实现了更新玩家得分、获取玩家排名和获取排名前几名玩家的功能。使用zincrby命令可以更新玩家的得分,而zrevrank命令则用于


获取玩家的排名,注意排名从0开始计数。通过调用zrevrangeWithScores命令,我们可以获取到排名前几名玩家以及他们的得分。


六、总结与最佳实践


在本篇博客中,我们深入探讨了如何使用Redis构建高性能的计数器和排行榜功能。通过实际案例和详细的Java代码示例,我们了解了如何在实际应用中应用这些功能,提升系统性能和用户体验。让我们在这一节总结Redis在计数器和排行榜功能中的价值,并提供一些最佳实践指南。


1、Redis在计数器和排行榜中的价值


通过使用Redis的计数器和排行榜功能,我们可以实现以下价值:




  • 实时性和高性能:Redis的内存存储和优化的数据结构使得计数器和排行榜功能能够以极高的性能实现。这对于需要实时更新和查询数据的场景非常重要。




  • 用户参与度提升:在社交媒体和游戏等应用中,计数器和排行榜功能可以激励用户参与。通过显示点赞数量或排行榜,用户感受到了更强的互动性和竞争性,从而增加了用户参与度。




  • 数据统计和分析:通过统计计数和排行数据,我们可以获得有价值的数据洞察。这些数据可以用于分析用户行为、优化内容推荐等,从而指导业务决策。




2、最佳实践指南


以下是一些使用Redis构建计数器和排行榜功能的最佳实践指南:




  • 合适的数据结构选择:根据实际需求,选择合适的数据结构。计数器可以使用简单的String类型,而排行榜可以使用有序集合(Sorted Set)来存储数据。




  • 保证数据准确性:在高并发环境下,使用Redis的事务、管道和分布式锁来保证计数器和排行榜的数据准确性。避免并发写入导致的竞争条件。




  • 定期数据清理:定期清理不再需要的计数器和排行数据,以减小数据量和提高查询效率。可以使用ZREMRANGEBYRANK命令来移除排行榜中的过期数据。




  • 适度的缓存:对于排行榜数据,可以考虑添加适度的缓存以提高查询效率。但要注意平衡缓存的更新和数据的一致性。




通过遵循这些最佳实践,您可以更好地应用Redis的计数器和排行榜功能,为您的应

作者:哪吒编程
来源:juejin.cn/post/7271908000414351400
用程序带来更好的性能和用户体验。

收起阅读 »

我专门注册了一家公司,带大家来看看 boss 视角下的求职者

你投了很多简历,没有面试机会,是因为你的简历根本就没被面试官看到! 那为什么简历没被看到,明明你已经发了自己的简历,并且也打了招呼啊? 打了很多招呼,但是却没被回复,不是对你不感兴趣,而是人太多,你的招呼又没 有快速抓住面试官的眼球,就被淹没了 这一次,我教你...
继续阅读 »

你投了很多简历,没有面试机会,是因为你的简历根本就没被面试官看到!


那为什么简历没被看到,明明你已经发了自己的简历,并且也打了招呼啊?


打了很多招呼,但是却没被回复,不是对你不感兴趣,而是人太多,你的招呼又没
有快速抓住面试官的眼球,就被淹没了


这一次,我教你写抓人眼球,boss都忍不住点开聊天框的打招呼话术!




hello,大家好,我是 Sunday。


很多小伙伴在招聘软件上打招呼,但是却没有任何回复。很多小伙伴对此都会非常困惑,甚至开始怀疑自己。


那么我为了搞明白,这到底是因为什么。


所以,我专门注册了一个招聘端的账号,让我们从一个招聘者的角度来看一看:你的消息到底是怎么被体现出来的。


我是在周天晚上 11 点开放了两个岗位:前端和java


咱们先来看前端,这是招聘 JD


前端招聘JD


截止到周一上午 11 点,我一共收到了整整152位打招呼信息:



作为对比,咱们来看下 java 岗,这是 java 岗的 JD



他的投递更夸张,达到了 287 人:



这还是在济南这个二线城市的招聘情况。如果是在一线城市,那么一天收到上千份的简历,绝对不是开玩笑的。


那么这么多的打招呼消息,在 boss 端是怎么体现的呢?咱们一起来看看,截图有点多,但是这就是boss视角下的真实情况:






如果你是 boss 的话,那么你会点开谁的消息来看呢?


通过截图我们可以看到。打招呼的消息在 boss 端只能展现 17 个字。所以我们必须要在这 17 字中展示出核心竞争力,不要说废话。


比如这种消息:



这种就是典型的浪费机会:“可以聊一聊吗?” 可以聊呀,那你倒是聊啊~~~
同理,我们来看这一页消息,如果你是 boss 的话,你会点开谁的消息来看?



如果是我的话,那么我肯定会优先对 倒数第二,倒数第三 条消息比较感兴趣,因为他们直接描述到了重点内容:



所以,如果你在招聘软件中打招呼,总是得不到回复,那么可以想一想,你是不是也犯了 没有描述重点的错误呢?


那么描述了问题之后,接下来,咱们就来看下,打招呼的语句怎么去说呢?


根据 boss 视角,咱们可以知道:打招呼的前17个字是非常重要的。所以我们一定要在前 17 个字中,把重点说出来。


比如,你可以这么说:



3 年前端开发经验,熟练 vue、react 技术栈,具备多个大型项目开发经验,详细情况您可以看下我的简历,期待您的回复!



简单的一句话,核心内容,在前17个字中,都描述清楚了。


这句话内含一个公式:描述 经验、能力、成就结果+明确指引


咱们举个栗子🌰:



描述经验(3 年前端开发经验)+描述能力(熟练 vue、react 技术栈)+描述成就结果(具备多个大型项目开发经验)+明确指引(看下我的简历,期待您的回复)



其中公式里的每一个要素具体怎么描述,每一个人,每一个职位可能不一样,篇幅有限这里就不详细讲述了。


如果你想要根据自己的经历定制出适合自己的高回复打招呼话术,或者你近期有跳槽的需求,可以直接与我私聊。

作者:程序员Sunday
来源:juejin.cn/post/7272290608655220755

收起阅读 »

客户端开发的我,准备认真学前端了

⏰ : 全文字数:2200+ 🥅 : 内容关键字:前端,独立开发者,思考 背景 我呢,一个Android开发工程师,从毕业到现在主要做的是客户端开发,目前在一个手机厂商任职。自己目前知识技能主要在客户端上,其他方面会一点点,会一点点前端知识,会一点点后端知识...
继续阅读 »

⏰ : 全文字数:2200+

🥅 : 内容关键字:前端,独立开发者,思考



背景


我呢,一个Android开发工程师,从毕业到现在主要做的是客户端开发,目前在一个手机厂商任职。自己目前知识技能主要在客户端上,其他方面会一点点,会一点点前端知识,会一点点后端知识,会一点点脚本,用网络的一句话概括起来就是“有点东西,但是不多”😭。


为什么


决定学习前端,并不是心血来潮,一时自嗨,而是经过了比较长时间的思考。对于程序员来说,知识的更新迭代实在是很快,所以保持学习很重要。但是技术防线这么多,到底学什么?我相信这不是一个很容易做出抉择的问题。


对于前端之前有断断续续的学过一些,但是最后没有一直坚持下来。之所以这样,原因很多,比如没有很强的目标、没有足够的时间,前端涉及的知识点太多等。


但是我觉得对自己而言,最重要的一个原因是:**学习完前端,我能用它来干嘛?**如果没有想清楚这个原因,就很难找到目标。做事情没有目标,就无法拆解,也就无法长期坚持下去。直到最近,看了一些文章,碰到了一些事情,才慢慢想清楚这个问题。目前对我而言,开始决定认真学习前端的主要原因有两个:



  • 自己一直想做点什么

  • 工作上有需要


想做点什么


从我接触计算机开始,心底里一直有个梦,就是想利用自己手上技能,做点什么。我也和旁边的朋友同事交流过,大家都有类似的想法,从这看估计很多程序员朋友都会有这样的想法。我从一开始的捣鼓网站,论坛,到后来开发APP等,折腾了好多东西。但是到了最后,都没有折腾出点啥,都无疾而终。


前一段时间,看到一个博主写的一篇文章,文章大概是讲他如何从一个公司的后端开发工程师,走到今天成为一名独立开发者的故事。


其中有一段是说他一直心里念念不忘,想做一款 saas 应用,期间一直在学习和看其他人的产品,学习经验,尝试不同的想法。所谓念念不忘必有回响,终于从别人的产品中产生了一个点子,然后很快写好了后端服务,并自学前端边做边学,完成了这个产品。目前他的这个产品运作的很成功。


这个故事给我很大鼓舞,之前看到过很多这样的故事,有成功的,有失败的。我也去分析看了那些成功的,经过自己的观察,大部分成功的独立开发者,基本上都是多年前成功的那批,那段时间还是处于互联网的红利期,天时地利人和加在一起,造就了他们的成功。


当然这里并不是否认他们能力,觉得是他们运气好。能在当时那么多人中,脱颖而出,依然表明他们是佼佼者。这里只是想表达那个时间段,大环境对开发者来说,是比较友好的,阻力没有那么大。


很少看到最近两年成功的开发者(不排除自己不知道哈),但是从这位博主的经历来看,他确实在成功了,这给了我很大的鼓舞,说明这条路上还是有机会的,只是在现在这种大环境下,成功的难度在增加,阻力变大。如果我们自己始终坚持,寻找机会,不断地尝试,是否有一天可能会成功呢?


那这样的话,我主要关注哪个方向呢?我个人更加偏向于前端全栈方向,包括WebApp,小程序,P C 软件等。


为什么这么认为呢?看下现在的大环境,不提之前上架APP需要各种软件著作权,后来个人无法在各大商店上发布APP,再到现在新出的APP备案制,基本上个人想在Android App上发力,真的很难了。而且,经过自己在ProductHunt上观察,目前大部分独立开发者的作品都是聚焦于WebAppSAAS,或者是PC类软件,剩下就是IOSMAC平台的。


且学习前端技术栈是一个比较好的选择。JavaScript这门语言很强大,整个技术栈不仅可以做前端,也可以做后端开发,还可以做跨平台的 P C 软件开发, 提供的丰富的解决方案,对个人开发者来说极为合适。


当然,我们也可以找合适的人,一起组队合作,不用单打独斗,这样不仅节省期间和精力,也能有好的交流和碰撞。这条路我也经历过,但是说实话执行起来确实有一定的困难。首先就是人难找,要想找到一个三观差不多的伙伴,其实真的挺难的。还有一个就是个人时间和做事方式也很难契合。所以个人认为如果想做点什么,前期一个人自己去实现一个MVP出来,是一个合适的选择。后面如果有必要了,倒是可以考虑慢慢招人。


我们也要认识到技术只是最基础的第一步,要想做成一个产品,还有很多东西要学习。推广、运营,沟通交流无论哪个都是一道坎。但是作为登山者的我们不要关注前面路有多远,而是要确保自己一直在路上。


工作涉及


还有一个原因是,最近工作上和前端打交道有很多。因为项目内部接入了类似 React Native 的框架,有大量的业务场景是基于这个框架开发。这就导致了客户端涉及到大量和前端的交互,流程的优化,工程化等工作。客户端可以不用了解前端和框架的知识,也没什么问题。
但是想着如果后续这一块有什么问题,或者想对这块做一些性能优化、工程提效的事情,如果对前端知识没有一个很好的了解,估计也很难做出彩。


结尾


今天在这里絮絮叨叨这么多,并不是想要告诉大家选择前端技术栈学习就一定咋样,比如第一点说的独立开发者中,有很多的全栈开发者,他们有的已经失败了,有的还在路上,成功的毕竟还是少数。
我想分享的是我个人关于为什么选择前端技术栈作为学习方向,如何做出选择的一些思考。这都是我的一家之言,不一定正确,大家姑且一看。


同时自己心里也还是希望能像文章提到的那位博主一样,在做产品这条路上,也能“念念不忘,必有回响”。正如我一直相信秉持的“日拱一卒,功不唐捐”。

作者:七郎的小院
来源:juejin.cn/post/7271248528999481384

收起阅读 »

七夕礼物

环信的七夕礼物收到了,T恤很好看,胸章的设计也很时尚,唯一的遗憾就是没有拍照,手机转电脑略麻烦哈哈哈,环信赶紧出个手机端app吧。

环信的七夕礼物收到了,T恤很好看,胸章的设计也很时尚,唯一的遗憾就是没有拍照,手机转电脑略麻烦哈哈哈,环信赶紧出个手机端app吧。

虚拟dom

vue中的虚拟dom 简介 首先vue会把模板编译成render函数,运行render函数生成虚拟dom 虚拟dom通过 diff算法 对比差异 渲染不同的部分 然后更新视图 为什么会需要虚拟dom 在主流框架 Angular , Vue.js (1.0)...
继续阅读 »

vue中的虚拟dom


简介



首先vue会把模板编译成render函数,运行render函数生成虚拟dom


虚拟dom通过 diff算法 对比差异 渲染不同的部分 然后更新视图



为什么会需要虚拟dom


在主流框架 Angular , Vue.js (1.0)React 中都有一个共同点,那就是它们都不知道哪些状态(state)变了。因此就需要进行比对,在React中使用的虚拟dom比对, Angular 中使用的是脏检查的流程



而在 Vue.js中使用的是变化侦测的方式,它在一定程度上知道具体哪些状态发生了变化,这样就可以通过更细粒度的绑定来更新视图。也就是说,在Vue.js中,当状态发生变化时,它在一定程度上知道哪些节点使用了这个状态,从而对这些节点进行更新操作,根本不需要比对



但是这样做的代价就是,粒度太细,每一个都有对应的 watcher 来观察状态变化,这样就会浪费一些内存开销,绑定的越多开销越大,如果这运用在一个大型项目中,那么他的开销无疑是非常大的


因此从 Vue.js 2.0 开始借鉴 React 中的虚拟DOM ,组件级别是一个watcher实例,就是说即便一个组件内有10个节点使用了某个状态,但其实也只有一个watcher在观察这个状态的变化。


什么是虚拟dom


虚拟DOM是通过状态生成一个虚拟节点树(vnode) ,然后使用虚拟节点树进行渲染。 在渲染之前,会使用新生成的虚拟节点树和上一次生成的虚拟节点树进行对比 (diff算法) ,只渲染不同的部分



虚拟节点树其实是由组件树建立起来的整个虚拟节点(Virtual Node,也经常简写为vnode)树



在Vue.js中,我们使用模板来描述状态DOM之间的映射关系。Vue.js通过编译将模板转换成渲染函数(render),执行渲染函数就可以得到一个虚拟节点树,使用这个虚拟节点树就可以渲染页面


模板编译成render函数


将模板编译成渲染函数可以分两个步骤,先将模板解析成AST(Abstract Syntax Tree,抽象语法树),然后再使用AST生成渲染函数。


但是由于静态节点不需要总是重新渲染,所以在生成AST之后、生成渲染函数之前这个阶段,需要做一个操作,那就是遍历一遍AST,给所有静态节点做一个标记,这样在虚拟DOM中更新节点时,如果发现节点有这个标记,就不会重新渲染它。所以,在大体逻辑上,模板编译分三部分内容:

  • 将模板解析为AST
  • 遍历AST标记静态节点
  • 使用AST生成渲染函数

虚拟dom做了什么


虚拟DOM在Vue.js中所做的事情其实并没有想象中那么复杂,它主要做了两件事。

  • 提供与真实DOM节点所对应的虚拟节点vnode。
  • 将虚拟节点vnode和旧虚拟节点oldVnode进行比对,然后更新视图。

对两个虚拟节点对比是虚拟dom 中最核心的算法 (diff),它可以判断出哪些节点发生了变化,从而只对发生了变化的节点进行更新操作


小结


虚拟DOM是将状态映射成视图的众多解决方案中的一种,它的运作原理是使用状态生成虚拟节点,然后使用虚拟节点渲染视图。


为什么会需要虚拟dom

框架设计

Vue 和 React 框架设计理念都是基于数据驱动的,当数据发生变化时 就要去更新视图,要想知道在页面众多元素中改动数据的元素 并根据改动后的数据去更新视图 是非常困难的



所以 Vue 和 React 中都会有一个 Render函数 或者类似于Render函数的功能,当数据变化时 全量生成Dom 元素
如果是直接操作 真实Dom 的话 是很昂贵的,就会严重拖累效率,所以就不生成真实的Dom,而是生成虚拟的Dom
当数据变化时就是 对象 和 对象 进行一个对比 ,这样就能知道哪些数据发生了改变 从而去操作改变的数据后的Dom元素



这也是一个 “妥协的结果”


跨平台

现阶段的框架他不仅仅能在浏览器里面使用,在小程序,移动端,或者桌面端也可以使用,但是真实Dom仅仅指的是在浏览器的环境下使用,因此他不能直接生成真实Dom ,所以选择生成一个在任何环境下都能被认识的虚拟Dom
最后根据不同的环境,使用虚拟Dom 去生成界面,从而实现跨平台的作用 --- 一套代码在多端运行


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

不会封装hook? 看下ahooks这6个hook是怎么做的

1 useUpdate 在react函数组件中如何强制组件进行刷新? 虽然react没有提供原生的方法,但是我们知道当state值变化的时候,react函数组件就会刷新,所以useUpdate就是利用了这一点,源码如下:import { useCallback...
继续阅读 »

1 useUpdate


在react函数组件中如何强制组件进行刷新? 虽然react没有提供原生的方法,但是我们知道当state值变化的时候,react函数组件就会刷新,所以useUpdate就是利用了这一点,源码如下:

import { useCallback, useState } from 'react';

const useUpdate = () => {
const [, setState] = useState({});

return useCallback(() => setState({}), []);
};

export default useUpdate;

可以看到useUpdate的返回值函数,就是每次都用一个新的对象调用setState,触发组件的更新。


2 useMount


react函数组件虽然没有了mount的生命周期,但是我们还会有这种需求,就是在组件第一次渲染之后执行一次的需求,就可以封装useEffect实现这个需求, 只需要把依赖项设置成空数组,那么就只在渲染结束后,执行一次回调:

import { useEffect } from 'react';

const useMount = (fn: () => void) => {

useEffect(() => {
fn?.();
}, []);
};

export default useMount;


3 useLatest


react函数组件是一个可中断,可重复执行的函数,所以在每次有state或者props变化的时候,函数都会重新执行,我们知道函数的作用域是创建函数的时候就固定下来的,如果其中的内部函数是不更新的,那么这些函数获取到的外部变量就是不会变的。例如:

import React, { useState, useEffect } from 'react';
import { useLatest } from 'ahooks';


export default () => {
const [count, setCount] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []);

return (
<>
<p>count: {count}</p>
</>
);
};

这是一个定时更新count值的例子,但是上边的代码只会让count一直是1,因为setInterval中的函数在创建的时候它的作用域就定下来了,它拿到的count永远是0, 当执行了setCount后,会触发函数的重新执行,重新执行的时候,虽然count值变成了1,但是这个count已经不是它作用域上的count变量了,函数的每次执行都会创建新的环境,而useState, useRef 等这些hooks 是提供了函数重新执行后保持状态的能力, 但是对于那些没有重新创建的函数,他们的作用域就永远的停留在了创建的时刻。 如何让count正确更新, 简单直接的方法如下,在setCount的同时,也直接更新count变量,也就是直接改变这个闭包变量的值,这在JS中也是允许的。

import React, { useState, useEffect } from 'react';
import { useLatest } from 'ahooks';


export default () => {
let [count, setCount] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
count = count + 1
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []);

return (
<>
<p>count: {count}</p>
</>
);
};

setCount是为了让函数刷新,并且更新函数的count值,而直接给count赋值,是为了更新定时任务函数中维护的闭包变量。 这显然不是一个好的解决办法,更好的办法应该是让定时任务函数能够拿到函数最新的count值。
useState返回的count每次都是新的变量,也就是变量地址是不同的,应该让定时任务函数中引用一个变量地址不变的对象,这个对象中再记录最新的count值,而实现这个功能就需要用到了useRef,它就能帮助我们在每次函数刷新都返回相同变量地址的对象, 实现方式如下:

import React, { useState, useEffect, useRef } from 'react'

export default () => {
const [count, setCount] = useState(0)

const latestCount = useRef(count)
latestCount.current = count

useEffect(() => {
const interval = setInterval(() => {
setCount(latestCount.current + 1)
}, 1000)
return () => clearInterval(interval)
}, [])

return (
<>
<p>count: {count}</p>
</>
)
}


可以看到定时函数获取的latestCount永远是定义时的变量,但因为useRef,每次函数执行它的变量地址都不变,并且还把count的最新值,赋值给了latestCount.current, 定时函数就可以获取到了最新的count值。
所以这个功能可以封装成了useLatest,获取最新值的功能。

import { useRef } from 'react';

function useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value;

return ref;
}

export default useLatest;


上边的例子是为了说明useLatest的作用,但针对这个例子,只是为了给count+1,还可以通过setCount方法本身获取,虽然定时任务函数中的setCount页一直是最开始的函数,但是它的功能是可以通过传递函数的方式获取到最新的count值,代码如下:

  const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setCount(count=>count+1)
}, 1000)
return () => clearInterval(interval)
}, [])

4 useUnMount


有了useMount就会有useUnmount,利用的就是useEffect的函数会返回一个cleanup函数,这个函数在组件卸载和useEffect的依赖项变化的时候触发。 正常情况 我们应该是useEffect的时候做了什么操作,返回的cleanup函数进行相应的清除,例如useEffect创建定时器,那么返回的cleanup函数就应该清除定时器:

 const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
count = count + 1
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []);

所以useUnMount就是利用了这个cleanup函数实现useUnmount的能力,代码如下:

import { useEffect } from 'react';
import useLatest from '../useLatest';

const useUnmount = (fn: () => void) => {
const fnRef = useLatest(fn);

useEffect(
() => () => {
fnRef.current();
},
[],
);
};

export default useUnmount;


使用了useLatest存放fn的最新值,写了一个空的useEffect,依赖是空数组,只在函数卸载的时候执行。


5 useToggle和useBoolean


useToggle 封装了可以state在2个值之间的变化,useBoolean则是利用了useToggle,固定2个值只能是true和false。 看下他们的源码:

function useToggle<D, R>(defaultValue: D = false as unknown as D, reverseValue?: R) {
const [state, setState] = useState<D | R>(defaultValue);

const actions = useMemo(() => {
const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R;

const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
const set = (value: D | R) => setState(value);
const setLeft = () => setState(defaultValue);
const setRight = () => setState(reverseValueOrigin);

return {
toggle,
set,
setLeft,
setRight,
};
// useToggle ignore value change
// }, [defaultValue, reverseValue]);
}, []);

return [state, actions];
}

可以看到,调用useToggle的时候可以设置初始值和相反值,默认初始值是false,actions用useMemo封装是为了提高性能,没有必要每次渲染都重新创建这些函数。setLeft是设置初始值,setRight是设置相反值,set是用户随意设置,toggle是切换2个值。
useBoolean则是在useToggle的基础上进行了封装,让我们用起来对更加的简洁方便。

export default function useBoolean(defaultValue = false): [boolean, Actions] {
const [state, { toggle, set }] = useToggle(defaultValue);

const actions: Actions = useMemo(() => {
const setTrue = () => set(true);
const setFalse = () => set(false);
return {
toggle,
set: (v) => set(!!v),
setTrue,
setFalse,
};
}, []);

return [state, actions];
}

总结


本文介绍了ahooks中封装的6个简单的hook,虽然简单,但是可以通过他们的做法,学习到自定义hook的思路和作用,就是把一些能够重用的逻辑封装起来,在实际项目中我们有这个意识就可以封装出适合项目的hook。


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

你的代码不堪一击!太烂了!

前言 小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返...
继续阅读 »

前言


小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返回数据格式错误也不能白屏!!” “好吧,千错万错都是前端的错。” 小王抱怨着把白屏修复了。


刚过不久,老李喊道:“小王,你的组件又渲染不出来了。” 小王不耐烦地过来去看了一下,“你这个属性data 格式不对,要数组,你传个对象干嘛呢。”老李反驳: “ 就算 data 格式传错,也不应该整个组件渲染不出来,至少展示暂无数据吧!” “行,你说什么就是什么吧。” 小王又抱怨着把问题修复了。


类似场景,小王时不时都要经历一次,久而久之,大家都觉得小王的技术太菜了。小王听到后,倍感委屈:“这都是别人的错误,反倒成为我的错了!”


等到小王离职后,我去看了一下他的代码,的确够烂的,不堪一击!太烂了!下面来吐槽一下。


一、变量解构一解就报错


优化前

const App = (props) => {
const { data } = props;
const { name, age } = data
}

如果你觉得以上代码没问题,我只能说你对你变量的解构赋值掌握的不扎实。



解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefinednull 无法转为对象,所以对它们进行解构赋值时都会报错。



所以当 dataundefinednull 时候,上述代码就会报错。


优化后

const App = (props) => {
const { data } = props || {};
const { name, age } = data || {};
}

二、不靠谱的默认值


估计有些同学,看到上小节的代码,感觉还可以再优化一下。


再优化一下

const App = (props = {}) => {
const { data = {} } = props;
const { name, age } = data ;
}

我看了摇摇头,只能说你对ES6默认值的掌握不扎实。



ES6 内部使用严格相等运算符(===)判断一个变量是否有值。所以,如果一个对象的属性值不严格等于 undefined ,默认值是不会生效的。



所以当 props.datanull,那么 const { name, age } = null 就会报错!


三、数组的方法只能用真数组调用


优化前:

const App = (props) => {
const { data } = props || {};
const nameList = (data || []).map(item => item.name);
}

那么问题来了,当 data123 , data || [] 的结果是 123123 作为一个 number 是没有 map 方法的,就会报错。


数组的方法只能用真数组调用,哪怕是类数组也不行。如何判断 data 是真数组,Array.isArray 是最靠谱的。


优化后:

const App = (props) => {
const { data } = props || {};
let nameList = [];
if (Array.isArray(data)) {
nameList = data.map(item => item.name);
}
}

四、数组中每项不一定都是对象


优化前:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item.name},今年${item.age}岁了`);
}
}

一旦 data 数组中某项值是 undefinednull,那么 item.name 必定报错,可能又白屏了。


优化后:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item?.name},今年${item?.age}岁了`);
}
}

? 可选链操作符,虽然好用,但也不能滥用。item?.name 会被编译成 item === null || item === void 0 ? void 0 : item.name,滥用会导致编辑后的代码大小增大。


二次优化后:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
}

五、对象的方法谁能调用


优化前:

const App = (props) => {
const { data } = props || {};
const nameList = Object.keys(data || {});
}

只要变量能被转成对象,就可以使用对象的方法,但是 undefinednull 无法转换成对象。对其使用对象方法时就会报错。


优化后:

const _toString = Object.prototype.toString;
const isPlainObject = (obj) => {
return _toString.call(obj) === '[object Object]';
}
const App = (props) => {
const { data } = props || {};
const nameList = [];
if (isPlainObject(data)) {
nameList = Object.keys(data);
}
}

六、async/await 错误捕获


优化前:

import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const res = await queryData();
setLoading(false);
}
}

如果 queryData() 执行报错,那是不是页面一直在转圈圈。


优化后:

import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
try {
const res = await queryData();
setLoading(false);
} catch (error) {
setLoading(false);
}
}
}

如果使用 trycatch 来捕获 await 的错误感觉不太优雅,可以使用 await-to-js 来优雅地捕获。


二次优化后:

import React, { useState } from 'react';
import to from 'await-to-js';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const [err, res] = await to(queryData());
setLoading(false);
}
}

七、不是什么都能用来JSON.parse


优化前:

const App = (props) => {
const { data } = props || {};
const dataObj = JSON.parse(data);
}

JSON.parse() 方法将一个有效的 JSON 字符串转换为 JavaScript 对象。这里没必要去判断一个字符串是否为有效的 JSON 字符串。只要利用 trycatch 来捕获错误即可。


优化后:

const App = (props) => {
const { data } = props || {};
let dataObj = {};
try {
dataObj = JSON.parse(data);
} catch (error) {
console.error('data不是一个有效的JSON字符串')
}
}

八、被修改的引用类型数据


优化前:

const App = (props) => {
const { data } = props || {};
if (Array.isArray(data)) {
data.forEach(item => {
if (item) item.age = 12;
})
}
}

如果谁用 App 这个函数后,他会搞不懂为啥 dataage 的值为啥一直为 12,在他的代码中找不到任何修改 dataage 值的地方。只因为 data 是引用类型数据。在公共函数中为了防止处理引用类型数据时不小心修改了数据,建议先使用 lodash.clonedeep 克隆一下。


优化后:

import cloneDeep from 'lodash.clonedeep';

const App = (props) => {
const { data } = props || {};
const dataCopy = cloneDeep(data);
if (Array.isArray(dataCopy)) {
dataCopy.forEach(item => {
if (item) item.age = 12;
})
}
}

九、并发异步执行赋值操作


优化前:

const App = (props) => {
const { data } = props || {};
let urlList = [];
if (Array.isArray(data)) {
data.forEach(item => {
const { id = '' } = item || {};
getUrl(id).then(res => {
if (res) urlList.push(res);
});
});
console.log(urlList);
}
}

上述代码中 console.log(urlList) 是无法打印出 urlList 的最终结果。因为 getUrl 是异步函数,执行完才给 urlList 添加一个值,而 data.forEach 循环是同步执行的,当 data.forEach 执行完成后,getUrl 可能还没执行完成,从而会导致 console.log(urlList) 打印出来的 urlList 不是最终结果。


所以我们要使用队列形式让异步函数并发执行,再用 Promise.all 监听所有异步函数执行完毕后,再打印 urlList 的值。


优化后:

const App = async (props) => {
const { data } = props || {};
let urlList = [];
if (Array.isArray(data)) {
const jobs = data.map(async item => {
const { id = '' } = item || {};
const res = await getUrl(id);
if (res) urlList.push(res);
return res;
});
await Promise.all(jobs);
console.log(urlList);
}
}

十、过度防御


优化前:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList?.join(',');
}

infoList 后面为什么要跟 ?,数组的 map 方法返回的一定是个数组。


优化后:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList.join(',');
}

后续


以上对小王代码的吐槽,最后我只想说一下,以上的错误都是一些 JS 基础知识,跟任何框架没有任何关系。如果你工作了几年还是犯这些错误,真的可以考虑转行。


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

天涯论坛倒闭,我给天涯续一秒

时代抛弃你,连句招呼都不会打 "时代抛弃你,甚至连句招呼都不会打",成立差不多23年,承载无数人青春回忆的社区就这样悄无声息的落幕了。在那个没有微博抖音的年代,天涯可谓是神一般的存在,当时还没有网络实名制,因此内容包罗万象五花八门,各路大神层出不穷,这里有:盗...
继续阅读 »

时代抛弃你,连句招呼都不会打


"时代抛弃你,甚至连句招呼都不会打",成立差不多23年,承载无数人青春回忆的社区就这样悄无声息的落幕了。在那个没有微博抖音的年代,天涯可谓是神一般的存在,当时还没有网络实名制,因此内容包罗万象五花八门,各路大神层出不穷,这里有:盗走麻花腾qq的黑客大神、高深莫测的民生探讨、波诡云谲的国际形势分析、最前沿最野的明星八卦、惊悚刺激的怪力乱神、脑洞大开的奇人异事 等等,让人眼花缭乱。甚至还有教你在家里养一只活生生的灵宠(见下文玄学类) 


今年4月初,天涯官微发布公告,因技术升级和数据重构,暂时无法访问。可直到现在,网站还是打不开。虽然后来,官微略带戏谑和无奈地表示:“我会回来的”。但其糟糕的财务状况预示着,这次很可能真是,咫尺天涯,永不再见了。 



神奇的天涯


当时还在读大一时候就接触到了 天涯,还记得特别喜欢逛的板块是 "莲蓬鬼话"、"天涯国际"。莲蓬鬼话老用户都知道,主要是一些真真假假的怪力乱神的惊险刺激的事情,比如 有名的双鱼玉佩,还有一些擦边的玩意,比如《风雪漫千山人之大欲》,懂得都懂,这些都在 pdf里面自取😁;天涯国际主要是各路大佬分析国际局势,每每看完总有种感觉 "原来在下一盘大棋",还有各种人生经验 比如kk大神对房产的预测,现在看到貌似还是挺准的。还有教你在家里养一只活生生的灵宠,神奇吧。 总共200+篇,这里先做下简单介绍





关注公众号,回复 「天涯」 海量社区经典文章双手奉上,感受一下昔日论坛的繁华



历史人文类


功底深厚,博古通今,引人入胜,实打实的的拓宽的你的知识面

  • (长篇)女性秘史◆那些风华绝代、风情万种的女人,为你打开女人的所有秘密.pdf
  • 办公室实用暴力美学——用《资治通鉴》的智慧打造职场金饭碗.pdf
  • 《二战秘史》——纵论二战全史——邀你一起与真相贴身肉搏.pdf
  • 不被理解的mzd(密码是123).zip
  • 地缘看世界——欧洲部分-温骏轩.pdf
  • 宝钗比黛玉大八岁!重解红楼全部诗词!血泪文字逐段解释!所有谜团完整公开!.pdf
  • 现代金融经济的眼重看历史-谁是谁非任评说.pdf
  • 蒋介石为什么失掉大陆:1945——1949-flp713.pdf

人生箴言类


开挂一般的人生,有的应该是体制内大佬闲来灌水,那时上网还无需实名

  • 职业如何规划?大城市,小城市,如何抉择?我来说说我的个人经历和思考-鸟树下睡懒觉的猪.pdf
  • kk所有内容合集(506页).pdf
  • 一个潜水多年的体制内的生意人来实际谈谈老百姓该怎么办?.pdf
  • 三年挣850万,你也可以复制!现在新书已出版,书名《我把一切告诉你》.pdf
  • 互联网“裁员”大潮将起:离开的不只是马云 可能还有你.pdf
  • 大鹏金翅明王合集.pdf
  • 解密社会中升官发财的经济学规律-屠龙有术.pdf

房产金融


上帝视角,感觉有的可能是参与制定的人

  • 从身边最简单的经济现象判断房价走势-招招是道.pdf
  • 大道至简,金融战并不复杂。道理和在县城开一个赌场一样。容我慢慢道来-战略定力.pdf
  • 沉浮房产十余载,谈房市心得.pdf
  • 现代金融的本质以及房价-curgdbd.pdf
  • 对当前房地产形势的判断和对一些现象的解释-loujinjing.pdf
  • 中国VS美国:决定世界命运的博弈 -不要二分法 .pdf
  • 大江论经济-大江宁静.pdf
  • 形势转变中,未来哪些行业有前景.pdf
  • 把握经济大势和个人财运必须读懂钱-现代金钱的魔幻之力.pdf
  • 烽烟四起,中美对决.pdf
  • 赚未来十年的钱-王海滨.pdf
  • 一个炒房人的终极预测——调控将撤底失败.pdf

故事连载小说类


小说爱好者的天堂,精彩绝伦不容错过

  • 人之大欲,那些房中术-风雪漫千山.pdf
  • 冒死记录中国神秘事件(真全本).pdf 五星推荐非常精彩
  • 六相十年浩劫中的灵异往事,颍水尸媾,太湖獭淫,开封鬼谷,山东杀坑-御风楼主人.pdf
  • 《内参记者》一名“非传统”记者颠覆你三观的采访实录-有骨难画.pdf
  • 中国式骗局大全-我是骗子他祖宗.pdf
  • 我是一名警察,说说我多年来破案遇到的灵异事件.pdf
  • 一个十年检察官所经历的无数奇葩案件.pdf
  • 宜昌鬼事 (三峡地区巫鬼轶事记录整理).pdf
  • 南韩往事——华人黑帮回忆录.pdf
  • 惊悚灵异《青囊尸衣》(斑竹推荐)-鲁班尺.pdf
  • 李幺傻江湖故事之《戚绝书》(那些湮没在岁月深处的江湖往事)-我是骗子他祖宗.pdf
  • 闲来8一下自己幽暗的成长经历-风雪漫千山.pdf
  • 阴阳眼(1976年江汉轶事).pdf
  • 民调局异闻录-儿东水寿.pdf
  • 我当道士那些年.pdf
  • 目睹殡仪馆之奇闻怪事.pdf

玄学类


怪力乱神,玄之又玄,虽然已经要求建国后不许成精了

  • 请块所谓的“开光”玉,不如养活的灵宠!.pdf
  • 写在脸上的风水-禅海商道.pdf
  • 谶纬、民谣、推背图-大江宁静.pdf
  • 拨开迷雾看未来.pdf
  • 改过命的玄教弟子帮你断别你的网名吉凶-大雨小水.pdf

天涯的败落


内容社区赚钱,首先还是得有人气,这是互联网商业模式的基础。天涯在PC互联网时代,依靠第一节说的几点因素,持续快速的吸引到用户,互联网热潮,吸引了大量的资本进入,作为有超高流量的天涯社区,自然也获得了资本的青睐。营收这块,主要分为两个部分:网络广告营销业务和互联网增值业务收入。广告的话,最大的广告主是百度,百度在2015年前5个月为天涯社区贡献了476万元,占总收入的比重达11.24%;百度在2014年为天涯社区贡献收入1328万元,占比12.76%。广告收入严重依赖于流量,天涯为了获得广告营收,大幅在社区内植入广告位,影响了用户体验,很有竭泽而渔的感觉。 但是在进入移动互联网时代没跟上时代步伐, 

2010年底,智能手机的出货量开始超过PC,另外,移动互联网走的是深度垂直创新,天涯还是大而全的综合社区模式,加上运营也不是很高明,一两个没工资的版主,肯定打不过别人公司化的运作,可以看到在细分领域被逐步蚕食:

  • 新闻娱乐,被**「微博、抖音」**抢走;
  • 职场天地,被**「Boss直聘」**抢走;
  • 跳蚤市场,被**「闲鱼、转转」**抢走;
  • 音乐交友,被**「网易云、qq音乐」**抢走;
  • 女性兴趣,被**「小红书」**抢走,等等

强如百度在移动互联网没占到优势,一直蛰伏到现在,在BAT中名存实亡,何况天涯,所以也能理解吧。"海内存知己,天涯若比邻",来到2023年,恐怕只剩物是人非,变成一个被遗忘的角落,一段被尘封的回忆罢了,期待天涯能够度过难关再度重来吧。


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

介绍一款CPP代码bug检测神器

最近使用C++开发的比较多,对于C++开发来说,内存管理是一个绕不开的永恒话题,因此在使用C++特别是指针时经常是慎之又慎, 最怕一不小心就给自己挖了一个坑,刚好最近发现了一个特别好用的C++静态代码分析利器,借助这个强大的分析工具,我们可以很好地将一些空指针...
继续阅读 »

最近使用C++开发的比较多,对于C++开发来说,内存管理是一个绕不开的永恒话题,因此在使用C++特别是指针时经常是慎之又慎,
最怕一不小心就给自己挖了一个坑,刚好最近发现了一个特别好用的C++静态代码分析利器,借助这个强大的分析工具,我们可以很好地将一些空指针,
数组越界等一些常见的bug扼杀在萌芽阶段,正所谓独乐了不如众乐乐,特将这个利器分享给大家。


这个利器就是cppcheck,它的官网是:cppcheck.sourceforge.io/


同时它还提供了在线分析的功能:cppcheck.sourceforge.io/demo/


在这个在线分析工具中,我们只需要将我们需要检测的代码拷贝粘贴到输入框,然后点击Check按钮即可进行代码分析。


当然啦,这个在线分析还是有很多不足的,比如最多只支持1024个字符,无法在线分析多文件,大项目等,如果要分析长文件,甚至是大项目,那就得安装本地使用啦,
下面我们就以CLion为例,简单介绍下cppcheck的安装和使用。


插件cppcheck的安装


首先这个强大的分析工具是有一个CLion插件的,它的连接是:plugins.jetbrains.com/plugin/8143…


我们可以直接在这个地址上进行在线安装,也可以在CLion的插件市场中搜索cppcheck进行安装。


需要注意的是这个插件仅仅是为了在CLion中自动帮我们分析项目代码,它是不包含cppcheck功能的,也就是要让这个插件正常工作,我们还得
手动安装cppcheck,然后在CLion配置好cppcheck的可执行文件的路径才行。


关于这个cppcheck核心功能的安装官网已经说得很清楚,也就是一句命令行的事情。


比如Debian系统可以通过一下命令安装:

sudo apt-get install cppcheck

Fedora的系统可以通过以下命令安装:

sudo yum install cppcheck

至于Mac系统,那肯定就是用神器包管理工具Homebrew进行安装啦:

brew install cppcheck

CLion插件配置cppcheck路径


安装好cppcheck核心工具包和CLion的cppcheck插件工具之后,我们只需要在CLion中配置一下cppcheck工具包的安装路径就可以正常使用啦。


以笔者的Mac系统的CLion为例子,打开CLion后,点击CLion-> Settings -> Other Settings -> Cppcheck Configuration



在弹出框中设置好cppcheck安装包的绝对路径即可。


如果你是使用Homebrew安装的话可以通过命令brew info cppcheck查找到cppcheck安装的具体路径。


功能实测


为了检测cppcheck这个分析工具的功能,我们新建了一个工程,输入以下代码:

void foo(int x)
{
int buf[10];
if (x == 1000)
buf[x] = 0; // <- ERROR
}

int main() {
int y[1];
y[2] = 1;
return 0;
}

当我们没有安装cppcheck插件时,它是这样子的,看起来没什么问题:



当我们安装了cppcheck插件之后,对于可能会发生潜在的空指针、数组越界、除数为0等等可能导致bug的地方会有高亮提示,
给人一看就有问题的感觉:



当然啦,cppcheck的功能远比这个这个例子所展示的强大,更多惊喜欢迎大家使用体验。


工具是智慧的延伸,在开发的过程选择适合你的工具,可以让我们的工作事半功倍,同行的你如果有好的开发辅助工具欢迎留言分享...


系统话学习博文推荐


音视频入门基础

C++进阶

NDK学习入门

安卓camera应用开发

ffmpeg系列

Opengl入门进阶

webRTC


关注我,一起进步,有全量音视频开发进阶路径、资料、踩坑记等你来学习...


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

Swift 周报 第三十期

iOS
前言 本期是 Swift 编辑组自主整理周报的第二十一期,每个模块已初步成型。各位读者如果有好的提议,欢迎在文末留言。 欢迎投稿或推荐内容。目前计划每两周周一发布,欢迎志同道合的朋友一起加入周报整理。 求人不如求己,你多一样本领,就少一点啊乞求;Swift社区...
继续阅读 »

前言


本期是 Swift 编辑组自主整理周报的第二十一期,每个模块已初步成型。各位读者如果有好的提议,欢迎在文末留言。


欢迎投稿或推荐内容。目前计划每两周周一发布,欢迎志同道合的朋友一起加入周报整理。


求人不如求己,你多一样本领,就少一点啊乞求;Swift社区让你多一样技能,少一些嘲讽!



周报精选


新闻和社区:码出新宇宙,WWDC23 就在眼前


提案:有 4 个提案通过,本期没有产生新的提案


Swift 论坛:PermutableCollection 协议


推荐博文:SwiftUI 中 LinearGradient的用法


话题讨论:


有博主在视频社交平台说,2023年已然迎来了经济危机,只是有些人不愿意相信而已,那么你认为国内2023年是否真的进入了经济危机?



上期话题结果



上期话题讨论结果表明,社交隔阂个人选择标准的提高是导致男女群体互不干涉的主要原因,而社会观念的变化也起到了一定的影响。这些因素共同作用导致了男群体和女群体相互独立地寻找伴侣的现象。


新闻和社区


App、App 内购买项目和订阅即将实行税率调整


App Store 的交易和支付机制旨在帮助你在覆盖全球的 175 个国家和地区的商店中,以 44 种货币为你的产品和服务便捷地进行定价与销售。Apple 会为开发者管理其中 70 多个国家和地区的税收,而且你还能够为 App 和 App 内购买项目分配税务类别。我们会根据税务法规的变化,定期更新你在某些地区的收益。


从 5 月 31 日起,你从 App 和 App 内购买项目 (包括自动续期订阅) 销售中获得的收益将进行调整,以反映以下税率调整。请注意,相关内容的价格将保持不变。


加纳:增值税率从 12.5% 上调至 15%。
立陶宛:对于符合条件的电子书和有声书,增值税率从 21% 下调至 9%。
摩尔多瓦:对于符合条件的电子书和期刊,增值税率从 20% 下调至 0%。
西班牙:收取 3% 的数字服务税。
由于巴西税务法规的变化,在巴西开展的所有 App Store 销售现由 Apple 代扣税款。我们会按月代扣代缴应向相应税务机关缴纳的税款。自 2023 年 6 月开始,你可以在 5 月份的收入中查看从你的收益中扣除的税款金额。巴西境内的开发者不会受到这一变化的影响。


以上调整生效后,App Store Connect 中“我的 App”的“价格与销售范围”部分会随即更新。一如既往,你可以随时更改你的 App 和 App 内购买项目的价格 (包括自动续期订阅)。现在,你可以从 900 个价格点中选择,为任何店面更改定价。


码出新宇宙



WWDC23 就在眼前。太平洋夏令时间 6 月 5 日上午 10 点,Apple 主题演讲将在 apple.com 和 Apple Developer App 线上提供,为本次大会拉开序幕。你还可以通过同播共享,邀请朋友一起观看。


现在,符合条件的开发者可以开始报名参加活动了。相关活动包括 Q&A、“会见演讲者”以及社区暖场活动等线上聊天室活动,旨在促进你与开发者社区和 Apple 专家的沟通和交流。


Apple 公证服务更新


正如去年在 WWDC (简体中文字幕) 上宣布的那样,如果你目前使用 altool 命令行工具或者 Xcode 13 或更早版本通过 Apple 公证服务对 Mac 软件进行公证,则需要改为使用 notarytool 命令行工具,或者升级到 Xcode 14 或更高版本。自 2023 年 11 月 1 日起,Apple 公证服务将不再接受从 altool 或者 Xcode 13 或更早版本上传的内容。已经过公证的现有软件可以继续正常工作。


Apple 公证服务是一个自动化系统,它会扫描 Mac 软件中有没有恶意内容,检查有没有代码签名问题,并快速返回结果。对软件进行公证可向用户保证,Apple 已检查且未发现软件中包含恶意软件。


为改进 Apple 平台的安全性和隐私保护,用于验证 App 和关联 App 内购买项目销售的 App Store 收据签名媒介证书将更新为使用 SHA-256 加密算法。此更新将分多个阶段完成,新的 App 和 App 更新可能会受影响,具体取决于它们验证收据的方式。


Apple 设计大奖入围名单公布



Apple 设计大奖旨在表彰在多元包容、乐趣横生、出色互动、社会影响、视觉图像,以及创新思维等类别中表现出色的 App 和游戏。马上一睹今年的入围作品,我们将在太平洋夏令时间 6 月 5 日下午 6:30 揭晓获奖者,敬请关注。


提案


通过的提案


SE-0399 value 包展开的元组 提案通过审查。该提案已在 二十九期周报 正在审查的提案模块做了详细介绍。


SE-0397 独立声明 Macros 提案通过审查。该提案已在 二十八期周报 正在审查的提案模块做了详细介绍。


SE-0392 自定义 Actor 执行器 提案通过审查。该提案已在 二十五期周报 正在审查的提案模块做了详细介绍。


SE-0390 **引入 @noncopyable ** 提案通过审查。该提案已在 二十四期周报 正在审查的提案模块做了详细介绍。


Swift论坛



  1. 讨论从 Realm 数据库迁移提示?


提问


目前正在寻求迁移到更轻量级的解决方案(realm 目前对我的用例来说太过分了)并且想迁移到 grdb,但不必将 realm 作为依赖项持续一年或更长时间......


回答


在没有 Realm 库的情况下,您是否能够读取 Realm 数据库文件的内容? 否则,您必须将 Realm 作为依赖项保留,直到您的用户迁移完毕。


您可以通过发布能够要求用户升级的应用程序版本来缩短时间跨度。 这将允许您使用 “Realm-only”、“Realm-to-GRDB” 和最终的 “GRDB-only” 版本进行过渡。



  1. 提议允许 protocol 嵌套在非通用上下文中


介绍


允许协议嵌套在非通用 struct/class/enum/actors 和函数中。


动机


将标称类型嵌套在其他标称类型中允许开发人员表达内部类型的自然范围——例如,String.UTF8View 是嵌套在 struct String 中的 struct UTF8View,它的名称清楚地传达了它作为 UTF-8 代码接口的用途 - 字符串值的单位。


但是,嵌套目前仅限于在其他 struct/class/enum/actors 中的 struct/class/enum/actors; 协议根本不能嵌套,因此必须始终是模块中的顶级类型。 这很不幸,我们应该放宽此限制,以便开发人员可以表达自然作用于某些外部类型的协议。


建议的解决方案


我们将允许在非泛型 struct/class/enum/actors 中以及在不属于泛型上下文的函数中嵌套协议。


例如,TableView.Delegate 自然是与表视图相关的委托协议。 开发人员应该这样声明它——嵌套在他们的 TableView 类中:

class TableView {
protocol Delegate: AnyObject {
func tableView(_: TableView, didSelectRowAtIndex: Int)
}
}

class DelegateConformer: TableView.Delegate {
func tableView(_: TableView, didSelectRowAtIndex: Int) {
// ...
}
}

目前,开发人员采用复合名称(例如 TableViewDelegate)来表达可以通过嵌套表达的相同自然范围。


作为一个额外的好处,在 TableView 的上下文中,可以使用更短的名称来引用嵌套协议委托(与所有其他嵌套类型一样):

class TableView {
weak var delegate: Delegate?

protocol Delegate { /* ... */ }
}

协议也可以嵌套在非泛型函数和闭包中。 不可否认,这在某种程度上是有限的实用性,因为对此类协议的所有一致性也必须在同一功能内。 但是,也没有理由人为地限制开发人员在函数中创建的模型的复杂性。 一些代码库(值得注意的是,Swift 编译器本身)使用带有嵌套类型的大型闭包,并且它们受益于使用协议的抽象。

func doSomething() {

protocol Abstraction {
associatedtype ResultType
func requirement() -> ResultType
}
struct SomeConformance: Abstraction {
func requirement() -> Int { ... }
}
struct AnotherConformance: Abstraction {
func requirement() -> String { ... }
}

func impl<T: Abstraction>(_ input: T) -> T.ResultType {
// ...
}

let _: Int = impl(SomeConformance())
let _: String = impl(AnotherConformance())
}


  1. 提议PermutableCollection 协议


简介


该提案旨在添加一个 PermutableCollection 协议,该协议将位于集合协议层次结构中的 Collection 和 MutableCollection 之间。


动机


在某些情况下,人们希望能够移动和排序元素,同时不允许(或限制)元素的突变。 鉴于大量不太重要的收集协议,这是一个值得注意的遗漏。 创建自定义集合类型时,PermutableCollection 协议在任何强制元素唯一性和/或身份的有序集合中都是首选。 用例将包括即将推出的 OrderedDictionary 和 OrderedSet。 对于不可变和可变集合,它还可以提供对 Swift 使用的底层(并且可能是高度优化的)排序算法的统一访问。


设计


协议设计简单,只需一个 swapAt 要求

/// A collection that supports sorting.
protocol PermutableCollection<Element> : Collection where Self.SubSequence : PermutableCollection {

mutable func swapAt(_ i: Index, _ j: Index)

}

通过 swapAt 函数,通过扩展添加额外的排序函数实现。

extension PermutableCollection {

mutating func move(fromOffsets source: IndexSet, toOffset destination: Int) {
// move algorithm enacts changes via swapAt()
}

mutating func partition(by belongsInSecondPartition: (Element) throws -> Bool) rethrows -> Index {
// partition algorithm enacts changes via swapAt()
}

mutating func sort() where Self: RandomAccessCollection, Self.Element: Comparable {
// partition algorithm enacts changes via swapAt()
}

// ... more permutation operations that mimic those available for MutableCollection

}



  1. 讨论 Vapor 和 query 缓存?




  2. 讨论在 Swift 系统中,如何将文件内容读取为字符串?




提问


我有一个文件的 FileDescriptor:


let fd = try FileDescriptor.open(<#filepath#>, .readOnly) 我可以使用 fd.read(into:) 将文件内容加载到 UnsafeMutableRawBufferPointer,但这是将文件内容加载到字符串中的正确第一步吗? 如果是这样,


在将它传递给 fd.read(into:) 之前,

  1. 我需要使用 .allocate(byteCount:alignment:) 分配 UnsafeMutableRawBufferPointer。 正确的 byteCount 取决于文件的大小。那么如何使用 Swift System 获取文件的大小呢?
  2. 如何从 UnsafeMutableRawBufferPointer 获取字符串?

回答


可以参考这个Git库:github.com/tayloraswif…




  1. 讨论为什么我不能使用 @dynamicMemberLookup 转发 enum cases?




  2. 讨论如何在 swift-foundation 中正确地进行性能测试?




提问


我想对比一下swift-foundation 和 Xcode 自带的 JSONDecoder 解码的速度。


我在一个新项目中使用单元测试和 measureBlock 以及在 swift-foundation 中使用 JSONEncoderTests 对其进行了测试。


swift-foundation 中的 JSONDecoder 看起来太慢了,我认为这是因为 swift-foundation 还没有作为一个库被引入。


推荐博文


iOS crash 报告分析系列 - 看懂 crash 报告的内容


摘要: 本篇文章主要介绍了iOS崩溃报告的解读方法,从报告的 Header、Exception information、Diagnostic messages、Backtraces、Thread state 和 Binary images 六个部分详细讲解了各字段含义,并提供示例代码帮助读者更好地理解。同时也引导读者去深入学习符号化的相关知识来获得更多信息。通过阅读本文,开发者可轻松看懂代码中产生的崩溃报告,并进行问题定位和处理。


SwiftUI 中 LinearGradient的用法


摘要: 这篇博文探讨了在 SwiftUI 中使用 LinearGradient 为对象创建渐变颜色效果。它展示了如何定义颜色数组、使用标准和自定义起点和终点,以及设置坐标以改进铅笔对象上的颜色笔尖。本文还包括用于创建具有各种起点终点组合的不同线性渐变的示例代码。文章以示例结束,展示了如何使用这些技术来自定义一支蓝色铅笔或整套铅笔的外观。


Swift 中的动态成员查找


摘要: 本文介绍了 Swift 语言中的动态成员查找(Dynamic Member Lookup)特性。通过在类型上使用 @dynamicMemberLookup 属性,我们可以重载该类型的 subscript 方法来更方便地访问其数据。但是,这也意味着缺乏编译时安全性。为了解决这个问题,本文提到了使用 KeyPath 作为参数的 subscript 方法来实现编译时安全检查。最后,作者建议我们可以谨慎地使用 @dynamicMemberLookup 特性来改进 API 设计。


话题讨论


有博主在视频社交平台说,2023年已然迎来了经济危机,只是有些人不愿意相信而已,那么你认为国内2023年是否真的进入了经济危机?


1.是的。确实已经经济危机了,今年工作很难找,同事比以前更卷啦,各种裁员消息不断。


2.经济危机不可能。五一淄博接待游客超过了100万人次,人挤人的旅游景象依然常在。


3.经济危机应该是相对的。对于大多数上班族来说,2023年很难,奉劝大家且行且珍惜。


关于我们


Swift社区是由 Swift 爱好者共同维护的公益组织,我们在国内以微信公众号的运营为主,我们会分享以 Swift实战SwiftUlSwift基础为核心的技术内容,也整理收集优秀的学习资料。


特别感谢 Swift社区 编辑部的每一位编辑,感谢大家的辛苦付出,为 Swift社区 提供优质内容,为 Swift 语言的发展贡献自己的力量。


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

iOS webview跳转链接带#问题

iOS
一、问题引出 在iOS中,如果WKWebview跳转的链接不带参数但是带了#网页锚点,而你这边项目因为要兼容所有跳转链接,对链接进行了百分比编码,将#编码为了23%, 那么将出现”无法显示网页“或空白网页的情况。 同时满足下面3个条件会出现这个问题:配置的广...
继续阅读 »

一、问题引出


在iOS中,如果WKWebview跳转的链接不带参数但是带了#网页锚点,而你这边项目因为要兼容所有跳转链接,对链接进行了百分比编码,将#编码为了23%, 那么将出现”无法显示网页“或空白网页的情况。



同时满足下面3个条件会出现这个问题:

  • 配置的广告跳转链接中带了#符号,即有网页锚点。
  • 链接中是没有参数部分的,即?param1=value1&之类的。
  • webview加载这个链接之前,对链接整体进行了百分比编码,“#”符号被编码为”23%“

在实际的场景中,产品或运维配置广告链接时,有时需要打开网页后跳转到某个元素节点的,也就是有链接中带#这种需求的。


为了兼容他们配置带#链接这种情况,我们iOS这边需要代码上做兼容。


二、问题根因


1. 链接中#的作用


一般用于较长网页中,跳转到网页中的某个节点。 



2. 对配置链接进行调试探索


拿一个链接进行举例:
"juejin.cn/post/717682…" ,


进行百分比编码后:

对于上述链接"#"不进行编码:
  • 直接能加载成功, 并且跳转到锚点‘heading-4’。
  • 如果锚点名称写错了,如‘heading-4’写成了‘heading-400’,那么也能加载成功,只不过不会跳到锚点。

那么为什么#被编码为23%之后,就不能请求成功呢?


3. 链接中#是否被编码,服务器收到请求时有何异同?


我们对链接进行百分比编码后,通过Charles抓包请求的结果: 


可以看到:

  • 如果#编码为23%,则服务器收到的请求路径也是带23%.
  • 如果是未编码#,则服务器收到的请求路径是不带#后面的内容的。

这也就是说,对于iOS端来说,客户端发送请求时未发送#及后面的内容,但是会发送23%及后面的内容。 具体的响应是服务器决定的。


其中#编码为23%的两种情况:

  • 23%后面还有/, 比如https:www.xxx.com/path1/path23%/
  • 23%后面没有/,比如https:www.xxx.com/path1/path23%https:www.xxx.com/path1/path23%section1

第一种情况下,有的网页能加载出来,有的网页会找不到网页,能否加载成功是根据服务器能否找到网页来定;第二种加载会失败,原因是23%也被服务器拿去查找资源路径。


我相信到这里,应该已经解释清楚了问题发生的原因。


三、兼容链接#的解决方案


我们客户端APP上显示的营销广告链接都是来源于后台配置的,有时配置的链接是有需要跳到锚点的需求的,那么我们该怎么兼容呢?

  • 需要对链接进行百分比编码.
  • 百分比编码时需要屏蔽掉#.

解决方案

let url = "https://juejin.cn/post/7176823567059779639#heading-4"
var notEncodeSet = CharacterSet.urlQueryAllowed

// 关键代码:
// 在对链接进行百分比编码时,不编码字符集中追加#
notEncodeSet.insert(charactersIn: "#")

if let urlPath = url.addingPercentEncoding(withAllowedCharacters: notEncodeSet) {
// 一般会有对path追加自定义公参或者设置自定义请求头之类的事情...
let URL = URL(string: urlPath)!
let request = MutableURLRequest(url: URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
// 具体的加载
webview.load(request as URLRequest)
}

使用Alamofire的字符编码不能解决问题


在找到上述原因后,我们可能会考虑使用Alamofire的字符集CharacterSet.afURLQueryAllowed使用来代替系统的CharacterSet.urlQueryAllowed去编码,但这样有用吗?


首先来看下CharacterSet.afURLQueryAllowed是怎么生成的:

public static let afURLQueryAllowed: CharacterSet = {
let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4

let subDelimitersToEncode = "!$&'()*+,;="

let encodableDelimiters = CharacterSet(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
return CharacterSet.urlQueryAllowed.subtracting(encodableDelimiters)
}()

可以看到是由CharacterSet.afURLQueryAllowed中除去通用分隔符和子分隔符后生成,也就是说是系统字符集的一个子集,对于这个问题也是行不通的!!!


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

🤭新的广告方式,很新很新!

哈哈 会爬树的金鱼,树上的金鱼呦😀 前言 老板:针对上半年业绩发表一下大家的看法,大家自由发言,已经定好晚餐和夜宵了有需要的自取୧(๑•̀⌄•́๑)૭ 产品A:根据大数据分析公司产品的广告收入较低拖后腿,建议扩宽曝光渠道! 产品B:对! 大数据分析公司产品的广...
继续阅读 »

哈哈 会爬树的金鱼,树上的金鱼呦😀


前言


老板:针对上半年业绩发表一下大家的看法,大家自由发言,已经定好晚餐和夜宵了有需要的自取୧(๑•̀⌄•́๑)૭


产品A:根据大数据分析公司产品的广告收入较低拖后腿,建议扩宽曝光渠道!


产品B:对! 大数据分析公司产品的广告大部分来自移动端,pc端曝光率较少,可以增加一下曝光率,据统计移动端大部分广告收入来自首屏广告,我觉得pc也可以加上


程序员: PC哪有首屏广告啊,行业都没有先例


老板:这个好,没有先例! 我们又可以申请专利了Ψ( ̄∀ ̄)Ψ 搞起!!今年公司专利指标有着落了


程序员: 这也太影响体验了


产品A:就说能不能做吧, 今天面试的那个应届生不错能倒背chromium源码,还能手写react源码并且指出优化方案


程序员: 我做!!! ╭( ̄m ̄*)╮


先来个全屏遮罩🤔


这还不简单,直接一个定位搞定

<!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>XX百货</title>
<style>
#ADBox {
background: #fff;
position: fixed;
width: 100%;
height: 100%;
display: none;
}
</style>
</head>
<body>
<div id="ADBox">广告</div>
</body>
</html>


搞定提测!


重来没接过这么简单的需求,送业绩这是╮(╯﹏╰)╭


第一次提测🤐


测试A: 送业绩这是? 产品说要和移动端一模一样,你这哪一样了?? 直系看需求文档!!


程序员: 需求文档就一句话, 和移动端一样的开屏广告, 这那不一样了?


测试A: 这哪一样了?? 你家移动端广告露个顶部出来?看看哪个app广告不是全屏的???


程序员: 啥? 还要全屏?? 行...


// 必须用点击事件触发才能全屏
document.addEventListener("click", async (elem) => {
const box = document.getElementById("ADBox");
if (box.requestFullscreen) {
box.requestFullscreen();
}
setTimeout(() => {
const state = !!document.fullscreenElement;
// 是否全屏状态
if (state) {
// 取消全屏
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}, 5 * 1000);
});

搞定提测


第二次提测🙄


产品A: 嗯...有点感觉了,这鼠标去掉都遮住广告了,万一广告商不满意投诉怎么办?


程序员: 鼠标这么小这么能遮住广告??


产品B: 看我鼠标? (大米老鼠标PC主题)


程序员: ...

<style>
#ADBox {
background: #fff;
position: fixed;
width: 100%;
height: 100%;
// 隐藏广告Box让用户点任意地方激活
opacity: 0;

}
</style>

提测...


第三次提测🤕


测试A: 为啥还有鼠标???


程序员: 怎么那可能还有?


测试A: 过来看,鼠标不动的话还是会显示鼠标哦,动一下才消失


程序员: ##..行, 那我直接锁指针

    <script>
let pointerLockElement = null;
// 指针锁定或解锁
document.addEventListener(
"pointerlockchange",
() => {
// 锁定的元素是否为当前的元素 -- 没啥也意义可以去掉
if (document.pointerLockElement === pointerLockElement) {
console.log("指针锁定成功了。");
} else {
pointerLockElement = null;
console.log("已经退出锁定。");
}
},
false
);
// 锁定失败事件
document.addEventListener(
"pointerlockerror",
() => {
console.log("锁定指针时出错。");
},
false
);

// 锁定指针,锁定指针的元素必须让用户点一下才能锁定
function lockPointer(elem) {
// 如果已经存锁定的元素则不操作
if (document.pointerLockElement) {
return;
}
if (elem) {
pointerLockElement = elem;
elem.requestPointerLock();
}
}

// 解除锁定
function unlockPointer() {
document.exitPointerLock();
}

// 必须用点击事件触发才能全屏
document.addEventListener("click", async () => {
const box = document.getElementById("ADBox");
if (box.requestFullscreen) {
box.requestFullscreen();
box.style.opacity = 1;
box.style.display = "block";
lockPointer(box);
}
// 5秒后解除锁定
setTimeout(() => {
const state = !!document.fullscreenElement;
// 是否全屏状态
if (state) {
// 取消全屏
if (document.exitFullscreen) {
document.exitFullscreen();
unlockPointer();
box.style.display = "none";
}
}
}, 5 * 1000);
});
</script>

提测...


第四次提测😤


测试A: Safari上失效哦


程序员: 额....

<script>

// requestFullscreen 方法兼容处理
function useRequestFullscreen(elem) {
const key = ['requestFullscreen', 'mozRequestFullScreen', 'webkitRequestFullscreen', 'msRequestFullscreen']
for (const value of key) {
if (elem[value]) {
elem[value]()
return true
}
}
return false
}

// document.exitFullscreen 方法兼容处理
document.exitFullscreenUniversal = document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen

// fullscreenElement 对象兼容处理
function getFullscreenElement() {
const key = ['fullscreenElement', 'webkitFullscreenElement']
for (const value of key) {
if (document[value]) {
return document[value]
}
}
return null
}

// fullscreenchange 事件兼容处理
addEventListener("fullscreenchange", endCallback);
addEventListener("webkitfullscreenchange", endCallback);

// requestPointerLock 方法在Safari下不可与 requestFullscreen 方法共用一个事件周期 暂无解决方法,必须让用户点两次鼠标,第一次全屏,第二次锁鼠标
// 同一事件周期内会出现的问题: 1.有小机率会正常执行, 2.顶部出现白条(实际上是个浏览器锁鼠标的提示语,但显示异常了) 3.锁定鼠标失败

</script>


结尾😩


产品A: 效果不错,但还有点小小的瑕疵,为啥要鼠标点一下才能弹广告,改成进入就弹窗吧


程序员: 要不还是找上次那个应届生来吧,改chromium源码应该能实现╭∩╮(︶︿︶)╭∩╮


效果预览: http://www.npmstart.top/BSOD.html


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

你知道什么是SaaS吗?

天天听SaaS,相信大家都知道什么叫SaaS系统!这不?领导安排下来了任务,说要去做SaaS系统,作为小白的我赶紧去看看什么是SaaS,大概收集整理(并非原创)了这部分内容,分享给大家。相信大家看了也会有很多收获。 本文从以下几个方面对SaaS系统召开介绍...
继续阅读 »

天天听SaaS,相信大家都知道什么叫SaaS系统!这不?领导安排下来了任务,说要去做SaaS系统,作为小白的我赶紧去看看什么是SaaS,大概收集整理(并非原创)了这部分内容,分享给大家。相信大家看了也会有很多收获。


  本文从以下几个方面对SaaS系统召开介绍:


  1. 云服务架构的三个概念

  2. SaaS系统的两大特征

  3. SaaS服务与传统服务、互联网服务的区别

  4. B2B2C

  5. SaaS系统的分类

  6. 如何SaaS化

  7. SaaS产品的核心组件

  8. SaaS多租户


一、云服务架构的三个概念


1.1 PaaS


英文就是 Platform-as-a-Service(平台即服务)


PaaS,某些时候也叫做中间件。就是把客户采用提供的开发语言和工具(例如Java,python, .Net等)开发的或收购的应用程序部署到供应商的云计算基础设施上去。
客户不需要管理或控制底层的云基础设施,包括网络、服务器、操作系统、存储等,但客户能控制部署的应用程序,也可能控制运行应用程序的托管环境配置。


PaaS 在网上提供各种开发和分发应用的解决方案,比如虚拟服务器和特定的操作系统。底层的平台3/4帮你铺建好了,你只需要开发自己的上层应用。这即节省了你在硬件上的费用,也让各类应用的开发更加便捷,不同的工作互相打通也变得容易,因为在同一平台上遵循的是同样的编程语言、协议和底层代码。


1.2 IaaS


英文就是 Infrastructure-as-a-Service(基础设施即服务)


IaaS 提供给消费者的服务是对所有计算基础设施的利用,包括处理 CPU、内存、存储、网络和其它基本的计算资源,用户能够部署和运行任意软件,包括操作系统和应用程序。
消费者不管理或控制任何云计算基础设施,但能控制操作系统的选择、存储空间、部署的应用,也有可能获得有限制的网络组件(例如路由器、防火墙、负载均衡器等)的控制。


IaaS 会提供场外服务器,存储和网络硬件,你可以租用。节省了维护成本和办公场地,公司可以在任何时候利用这些硬件来运行其应用。我们最熟悉的IaaS服务是我们服务器托管业务,多数的IDC都提供这样的服务,用户自己不想要再采购价格昂贵的服务器和磁盘阵列了,所有的硬件都由 IaaS 提供,你还能获得品质更高的网络资源。


1.3 SaaS


英文就是 Software-as-a-Service(软件即服务)


SaaS提供给客户的服务是运行在云计算基础设施上的应用程序,用户可以在各种设备上通过客户端界面访问,如浏览器。
消费者不需要管理或控制任何云计算基础设施,包括网络、服务器、操作系统、存储等等。


SaaS 与我们普通使用者联系可能是最直接的,简单地说任何一个远程服务器上的应用都可以通过网络来运行,就是SaaS了。国内的互联网巨头竭力推荐的 SaaS 应用想必大家已经耳熟能详了,比如阿里的钉钉,腾讯的企业微信,这些软件里面应用平台上的可供使用的各类SaaS小软件数不胜数,从OA,到ERP到CRM等等,涵盖了企业运行所需的几乎所用应用。


二、SaaS系统的两大特征



  1. 部署在供应商的服务器上,而不是部署在甲方的服务器上。

  2. 订购模式,服务商提供大量功能供客户选择,客户可以选择自己需要的进行组合,支付所需的价格,并支持按服务时间付费。


三、SaaS服务与传统服务、互联网服务的区别


3.1 SaaS服务


介于传统与互联网之间,通过租用的方式提供服务,服务部署在云端,任何用户通过注册后进行订购后获得需要的服务,可以理解成服务器及软件归供应商所有,用户通过付费获得使用权
image.png


3.2 传统软件


出售软件及配套设备,将软件部署在客户服务器或客户指定云服务器,出售的软件系统及运维服务为盈利来
image.png


3.3 互联网应用供应商


服务器部署在云端,所有用户可以通过客户端注册进行使用,广告及付费增值服务作为盈利来源
image.png


四、B2B2C


SaaS作为租户系统,需要为租户(C端)提供注册、购买、业务系统的入口,还得为B端(运营/运维)提供租户管理、流量监控、服务状态监控运维入口


五、SaaS系统的分类


5.1 业务型SaaS


定义:为客户的赚钱业务提供工具以及服务的SaaS,直面的是用户的生意,例如有赞微盟等电商SaaS以及销售CRM工具,为B2B2C企业;


架构以及商业模式:在产品的成长期阶段,为了扩充业务规模和体量,业务SaaS产品会拓展为“多场景+多行业”的产品模式,为不同行业或者不同场景提供适应的解决方案,例如做电商独立站的有赞,后期发展为“商城、零售、美业、教育”多行业的解决方案进行售卖。
image.png


5.2 效率型SaaS


定义:为客户效率提升工具的SaaS,如项目管理工具、Zoom等会议工具,提升办公或者生产效率,为B2B企业;


架构以及商业模式:不同于业务型的SaaS,效率SaaS思考得更多的是企业内存在一个大共性的效率的问题,不同的企业对于CRM销售系统的需求是不一样的,但都需要一个协同办公的产品来提升协作效率。对于效率类SaaS来说,从哪来到哪去是非常清晰的,就是要解决优化或者解决一个流程上的问题。
image.png


5.3 混合型SaaS


定义:即兼顾企业业务和效率效用SaaS,例如近几年在私域流量上大做文章的企业微信,其本身就是一个办公协同工具,但为企业提供了一整套的私域管理能力,实现业务的提升,同时也支持第三方服务。


架构以及商业模式:混合SaaS是业务和效率SaaS的结合体,负责企业业务以及企业管理流程的某类场景上的降本增效;因混合SaaS核心业务的使用场景是清晰且通用的,非核心业务是近似于锦上添花的存在,所以在中台产品架构上更接近为“1+X”组合方式——即1个核心业务+X个非核心功能,两者在产品层级上是属于同一层级的。
image.png


六、如何SaaS化



  1. 进行云化部署,性能升级,能够支持更大规模的用户访问

  2. 用户系统改造,支持2C用户登录(手机号一键登录、小程序登录、短信验证码登录)

  3. 网关服务,限流,接口防篡改等等

  4. 租户系统开发,包含租户基础信息管理、租户绑定资源(订购的功能)、租户服务期限等等

  5. 客户端改造(通常SaaS系统主要提供WEB端服务),页面权限控制,根据租户系统用户资源提供用户已购买的模块或页面

  6. 官网开发,功能报价单,功能试用、用户选购及支付

  7. 服务端接口数据权限改造、租户级别数据权限


七、SaaS产品的核心组件



  1. 安全组件:在SaaS产品中,系统安全永远是第一位需要考虑的事情

  2. 数据隔离组件:安全组件解决了用户数据安全可靠的问题,但数据往往还需要解决隐私问题,各企业之间的数据必须相互不可见,即相互隔离。

  3. 可配置组件:SaaS产品在设计之初就考虑了大多数通用的功能,让租户开箱即用,但任然有为数不少的租户需要定制服务自身业务需求的配置项,如UI布局、主题、标识(Logo)等信息

  4. 可扩展组件:SaaS产品应该具备水平扩展的能力。如通过网络负载均衡其和容器技术,在多个服务器上部署多个软件运行示例并提供相同的软件服务,以此实现水平扩展SaaS产品的整体服务性能

  5. 0停机时间升级产品:实现在不重启原有应用程序的情况下,完成应用程序的升级修复工作

  6. 多租户组件:SaaS产品需要同时容纳多个租户的数据,同时还需要保证各租户之间的数据不会相互干扰,保证租户中的用户能够按期望索引到正确的数据


八、SaaS多租户


8.1 多租户核心概念



  • 租户:一般指一个企业客户或个人客户,租户之间数据与行为是隔离的

  • 用户:在某个租户内的具体使用者,可以通过使用账户名、密码等登录信息,登录到SaaS系统使用软件服务

  • 组织:如果租户是一个企业客户,通常会拥有自己的组织架构

  • 员工:是指组织内部具体的某位员工。

  • 解决方案:为了解决客户的某类型业务问题,SaaS服务商将产品与服务组合在一起,为商家提供整体的打包方案。

  • 产品能力:指的是SaaS服务商对客户售卖的产品应用,特指能够帮助客户实现端到端场景解决方案闭环的能力。

  • 资源域:用来运行1个或多个产品应用的一套云资源环境

  • 云资源:SaaS产品一般都部署在各种云平台上,例如阿里云、腾讯云、华为云等。对这些云平台提供的计算、存储、网络、容器等资源,抽象为云资源。


8.2 三大模式


8.2.1 竖井隔离模式


image.png



  • 优势:



  1. 满足强隔离需求:一些客户为了系统和数据的安全性,可能提出非常严格的隔离需求,期望软件产品能够部署在一套完全独立的环境中,不和其他租户的应用实例、数据放在一起。

  2. 计费逻辑简单:SaaS服务商需要针对租户使用资源进行计费,对于复杂的业务场景,计算、存储、网络资源间的关系同样也会非常复杂,计费模型是很有挑战的,但在竖井模式下,计费模型相对来说是比较简单的。

  3. 降低故障影响面:因为每个客户的系统都部署在自己的环境中,如果其中一个环境出现故障,并不会影响其他客户使用软件服务。



  • 劣势:



  1. 规模化问题:由于租户的SaaS环境是独立的,所以每入驻一个租户,就需要创建和运营一套SaaS环境,如果只是少量的租户,还可能可以管理,但如果是成千上万的租户,管理和运营这些环境将会是非常大的挑战。

  2. 成本问题:每个租户都有独立的环境,花费在单个客户上的成本将非常高,会大幅削弱SaaS软件服务的盈利能力。

  3. 敏捷迭代问题:SaaS模式的一个优势是能够快速响应市场需求,迭代产品功能。但竖井隔离策略会阻碍这种敏捷迭代能力,因为更新、管理、支撑这些租户的SaaS环境,会变得非常复杂和低效。

  4. 统一管理与监控:在同一套环境中,对部署的基础设施进行管理与监控,是较为简单的。但每个租户都有独立的环境,在这种非中心化的模式下,对每个租户的基础设施进行管理与监控,同样也是非常复杂、困难的。


8.2.2 共享模式


image.png



  • 优势:



  1. 高效管理:在共享策略下,能够集中化地管理、运营所有租户,管理效率非常高。同时,对基础设施配置管理、监控,也将更加容易。相比竖井策略,产品的迭代更新会更快。

  2. 成本低:SaaS服务商的成本结构中,很大一块是基础设施的成本。在共享模型下,服务商可以根据租户们的实际资源负载情况,动态伸缩系统,这样基础设施的利用率将非常高。



  • 劣势:



  1. 租户相互影响:由于所有租户共享一套资源,当其中一个租户大量占用机器资源,其他租户的使用体验很可能受到影响,在这种场景下,需要在技术架构上设计一些限制措施(限流、降级、服务器隔离等),让影响面可控。

  2. 租户计费困难:在竖井模型下,非常容易统计租户的资源消耗。然而,在共享模型下,由于所有租户共享一套资源,需要投入更多的精力统计单个租户的合理费用。


8.2.3 分域隔离模式


image.png


8.3 多租户系统需要具备的能力



  1. 多个租户支持共享一套云资源,如计算、存储、网络资源等。单个租户也可以独占一套云资源。

  2. 多个租户间能够实现数据与行为的隔离,能够对租户进行分权分域控制。

  3. 租户内部能够支持基于组织架构的管理,可以对产品能力进行授权和管理。

  4. 不同的产品能力可以根据客户需求,支持运行在不同的云资源上。


8.4 多租户系统应用架构图


image.png

收起阅读 »

如何把一家创业公司搞垮

在拜读了耗子哥推荐的书《重来》之后,如何把一个创业公司搞垮,我得到了一些灵感。 追求完美的产品 我们都知道没有完美的产品,但是对于创业公司,想要做出完美的产品,至少需要付出很多的努力: 毫无 BUG:全面的产品交互设计、严格的编码过程、完整的用例测试等等。 ...
继续阅读 »

在拜读了耗子哥推荐的书《重来》之后,如何把一个创业公司搞垮,我得到了一些灵感。


追求完美的产品


我们都知道没有完美的产品,但是对于创业公司,想要做出完美的产品,至少需要付出很多的努力:



  1. 毫无 BUG:全面的产品交互设计、严格的编码过程、完整的用例测试等等。

  2. 大而全的功能:不要花时间去区分重要、次要,所有能力都得上,所有平台都得适配,所有功能都得支持。

  3. 延迟交付:追求完美产品,需要付出时间和精力,延期延期再延期。

  4. 沉默的反馈:开发完毕一个功能,要经过很久才能上线,迟迟得不到客户的真实反馈。


追求完美的产品,这个功能也要,那个功能也要,迟迟交付产品,磨灭团队信心,减少公司成功几率,钝刀子杀人


开会的技巧


会议是一种毒药,在开会的时候,我们要尽量扩大它的毒性:



  1. 没有明确的问题,没有确定的议程。

  2. 人员尽量扩大。不要精简会议人员,多增加无关人员。

  3. 每个人都发言。多听取一些低能儿的无效意见

  4. 去会议室。可以直接打断每个人的工作。

  5. 时长不限。不要限制开会的时间,时间越长越好。1 小时的会议,10 个人参加的话,就可以减少公司 10 小时的生产时间。


通过以上一些开会技巧,可以有效地增加会议的时长,多积累一些纸上谈兵的想法


做长期计划


做计划的本质是用过去指导未来,用以前的经验去圈套之后的变化。做长期计划,就可以把一个创业公司给套牢。


当创业公司按照一段时间去执行长期计划之后,如果发现事情不妙,可能会因为这么几个方面而硬着头皮继续执行:



  1. 沉没成本:我们都已经付出 4 个月的努力,不继续做下去很可惜吗?

  2. 傲慢自负:我们都已经定好目标了,再改变不是打脸吗?


满足客户


上线之后,要记住客户自上,来自客户的反馈都必须汲取,客户的要求都必须满足,让自己的产品成为一个臃肿的产品,成为一个臃肿产品的好处:



  1. 功能很多。用户所有的需求都能满足,意味着我们有对应处理需求的纷繁功能。

  2. 提高复杂性。每一个功能的增加,都需要对应交互,乱七八糟的功能可以让我们的产品交互变得复杂,界面花里胡哨。

  3. 拒绝新用户。通过提高产品复杂性,可以有效减少新的用户。

  4. 没有个性,平易近人。像一辆公交车一样,谁都可以上。


千万不要追求简洁,我们的目标是努力变成的微信,成就一款庞大、臃肿的垃圾产品


融资扩张,多招人手


当产品取得一定的成效,就需要马上融资,融资带来的好处太多:



  1. 更大的办公室,人数更多的公司

  2. 花别人的钱会上瘾

  3. 对公司失去控制权

  4. 投资人套现离场的风险

  5. 融资非常耗时耗力

  6. 产品可能偏向迎合投资人而不是客户


多招聘人手,新来的人:



  1. 对公司不了解

  2. 对项目不了解

  3. 互相谦让,互相客气

  4. 谁也不敢指出产品缺陷


集中力量办小事


人总是有限的,资源也总是有限的,我们需要正确地调用这些人力和资源,把他们都投入到小事中,如何做到呢?



  1. 不做取舍。在众多的事情中,不要去试图找到中心点,所有的任务都必须做,所有的需求都必须完成。

  2. 没有急事。把所有事情都当做急事,那也就没有急事。

  3. 唯唯诺诺。顺从永远比争锋相对容易,人们很容易同意添加一项新功能、接受一个过于乐观的最后期限、笑纳一个平庸的设计


不以盈利为目的


公司如何通过产品盈利的事情,尽量搁置,就像我们在设计神舟一号的时候,先假设地心引力不存在


一家企业不以盈利为目的,那么公司的可持续存活就有问题,可以给员工一些退出策略:



  • 如何破产清算,保障各位 n+1

  • 被其他公司收购


让员工少一点破釜沉舟的勇气,可以让公司早一点走向 Ending。






我是楷鹏,这是我阅读《重来》的读书笔记:wukaipeng.com/

read/rework

收起阅读 »

懂点心理学 - 曼德拉效应

最近在看电影 《消失的她》 ,里面提到了一个效应 - 曼德拉效应:修改他人记忆。 本文,我们来谈谈曼德拉效应。 什么是曼德拉效应 曼德拉效应,是指人们错误地记忆了某个特定的事件或情节的现象。产生的方式可以是让人们对新奇或者陌生事物的偏好会随着暴露的频率的增加...
继续阅读 »

最近在看电影 《消失的她》 ,里面提到了一个效应 - 曼德拉效应:修改他人记忆。


曼德拉效应.png


本文,我们来谈谈曼德拉效应


什么是曼德拉效应


曼德拉效应,是指人们错误地记忆了某个特定的事件或情节的现象。产生的方式可以是让人们对新奇或者陌生事物的偏好会随着暴露的频率的增加而增加。它表明通过重复和频繁的某种刺激,我们对于该刺激产生更积极的态度和更强烈的喜好。当然,也可以混淆/误导他人的思维 - 通常表示虚假的记忆。


总是穿着你老婆的衣服.png


跟在你身后.png


老公.png


那么这个酒店的工作人员.png


就会习惯性认为.png


她就是何太太.png


这个效应可以解析为什么人们倾向于更喜欢和接受他们熟悉的人、事物和概念。


如何应用曼德拉效应


曼德拉效应可以在广告、宣传和社交等领域中应用。


比如,上个星期笔者在京东 app 上浏览器一个牌子的茶壶🫖。然后,过了半个小时,自己刷朋友圈,微信推送了京东这个牌子的茶壶广告给我。给到我必须买这个牌子的错觉~


再比如,在工作中,你在现在这个公司遇到了一个问题。然后过了几天后,你将这个 issue 关闭掉。在某天,你向 leader 汇报工作演示操作的时候,却翻车了。因为你这个问题原来在上一家公司解决了,你却错误认为是在目前这家公司解决了。(大脑给到了错误的信号给你:这个问题你已经解决了,不必处理了)。当然,有一种很恐怖的职场现象:职场 PUA你什么都干不好,我什么都比你强...


如何避免曼德拉效应


曼德拉效应既然是错误的表象。那么我们可以:



  1. 检查自己的记忆:如果我们对某个情节的事件质疑,尝试回想并核对相关的证据。与他人交流,比较彼此的记忆。

  2. 养成记录重要的事项:对于比较重要的事情,比如借钱等,要写下日期、金额等重要信息,以减少记忆错误的风险。比如你朋友欠你 1000 块钱,期间他还了 100 块钱给你。然后过了几个月后,你问TA 还钱。TA 说:上一次,我不是全还给你了嘛。然后你会不会回忆下,期间确实还了一次,然后真以为他全还给你了。笔者也有记录的习惯,比如这篇文章 借点钱来“救急”【多图】

  3. 接受更正和反馈:如果他人提出了自己记忆不符的观点和事实,我们得深入了解事实,不仅要靠记忆和第三方证据,还要寻求更多来源的证据,比如录音等。了解了事实后,意识到自己的问题,要保持开放的心态接受讨论和反馈(这点要做到,太难了)。


参考


收起阅读 »

项目部署之后页面没有刷新怎么办?

web
最近项目部署成功之后,突然产品找我,上线之后,页面没有生效,这是怎么回事?我这是第一次部署这个项目,也不太清楚历史问题,接下来就慢慢寻找答案吧, 如果心急的可以直接看后面的总结,下面我们好好聊聊缓存的问题。 浏览器输入url之后,就会进行下面一系列判断,来实现...
继续阅读 »

最近项目部署成功之后,突然产品找我,上线之后,页面没有生效,这是怎么回事?我这是第一次部署这个项目,也不太清楚历史问题,接下来就慢慢寻找答案吧, 如果心急的可以直接看后面的总结,下面我们好好聊聊缓存的问题。


浏览器输入url之后,就会进行下面一系列判断,来实现页面渲染。



首先讲一下常见的http缓存~


HTTP缓存常见的有两类:



  • 强缓存:可以由这两个字段其中一个决定





    • expires

    • cache-control(优先级更高)





  • 协商缓存:可以由这两对字段中的一对决定





    • Last-Modified,If-Modified-Since

    • Etag,If--Match(优先级更高)




强缓存


使用的是express框架


expires


app.get('/login', function(req, res){
// 设置 Expires 响应头
const time = new Date(Date.now() + 300000).toUTCString()
res.header('Expires', time)
res.render('login');
});

然后我们在前端页面刷新,我们可以看到请求的资源的响应头里多了一个expires的字段, 取消Disable cache



刷新



勾选Disable cache



但是,Expires已经被废弃了。对于强缓存来说,Expires已经不是实现强缓存的首选。


因为Expires判断强缓存是否过期的机制是:获取本地时间戳,并对先前拿到的资源文件中的Expires字段的时间做比较。来判断是否需要对服务器发起请求。这里有一个巨大的漏洞:“如果我本地时间不准咋办?”


是的,Expires过度依赖本地时间,如果本地与服务器时间不同步,就会出现资源无法被缓存或者资源永远被缓存的情况。所以,Expires字段几乎不被使用了。现在的项目中,我们并不推荐使用Expires,强缓存功能通常使用cache-control字段来代替Expires字段。


cache-control


其实cache-control跟expires效果差不多,只不过这两个字段设置的值不一样而已,前者设置的是秒数,后者设置的是毫秒数


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'max-age=300')
res.render('login');
});

前端页面响应头多了cache-control这个字段,且300s内都走本地缓存,不会去请求服务端



Cache-Control:max-age=N,N就是需要缓存的秒数。从第一次请求资源的时候开始,往后N秒内,资源若再次请求,则直接从磁盘(或内存中读取),不与服务器做任何交互。


Cache-control中因为max-age后面的值是一个滑动时间,从服务器第一次返回该资源时开始倒计时。所以也就不需要比对客户端和服务端的时间,解决了Expires所存在的巨大漏洞。


Cache-control的多种属性:developer.mozilla.org/zh-CN/docs/…


但是使用最多的就是no-cache和no-store,接下来就重点学习这两种


no-cache和no-store


no_cache是Cache-control的一个属性。它并不像字面意思一样禁止缓存,实际上,no-cache的意思是强制进行协商缓存。如果某一资源的Cache-control中设置了no-cache,那么该资源会直接跳过强缓存的校验,直接去服务器进行协商缓存。而no-store就是禁止所有的缓存策略了。


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-cache')
res.render('login');
});

no-cache(进行协商缓存,下次再次请求,没有勾选控制台Disable cache,状态码是304)



app.get('/login', function(req, res){
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-store')
res.render('login');
});

no-store(每次都请求服务器的最新资源,没有缓存策略)



强制缓存就是以上这两种方法了。现在我们回过头来聊聊,Expires难道就一点用都没有了吗?也不是,虽然Cache-control是Expires的完全替代品,但是如果要考虑向下兼容的话,在Cache-control不支持的时候,还是要使用Expires,这也是我们当前使用的这个属性的唯一理由。


协商缓存


与强缓存不同的是,强缓存是在时效时间内,不走服务端,只走本地缓存;而协商缓存是要走服务端的,如果请求某个资源,去请求服务端时,发现命中缓存则返回304,否则则返回所请求的资源,那怎么才算命中缓存呢?接下来讲讲


Last-Modified,If-Modified-Since


简单来说就是:



  • 第一次请求资源时,服务端会把所请求的资源的最后一次修改时间当成响应头中Last-Modified的值发到浏览器并在浏览器存起来

  • 第二次请求资源时,浏览器会把刚刚存储的时间当成请求头中If-Modified-Since的值,传到服务端,服务端拿到这个时间跟所请求的资源的最后修改时间进行比对

  • 比对结果如果两个时间相同,则说明此资源没修改过,那就是命中缓存,那就返回304,如果不相同,则说明此资源修改过了,则没命中缓存,则返回修改过后的新资源


基于last-modified的协商缓存实现方式是:



  1. 首先需要在服务器端读出文件修改时间,

  2. 将读出来的修改时间赋给响应头的last-modified字段。

  3. 最后设置Cache-control:no-cache


三步缺一不可。


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)

const { mtime } = fs.statSync(path.join(__dirname, 'public/index.css')) // 读取最后修改时间
console.log(mtime.toUTCString(), '--------')
// 响应头的last-modified字段
res.header('last-modified', mtime.toUTCString())
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-cache')
res.render('login');
});


当index.css发生改变再次请求时



终端输出的时间变化



服务端的时间跟last-modified的值是一致的



使用以上方式的协商缓存已经存在两个非常明显的漏洞。这两个漏洞都是基于文件是通过比较修改时间来判断是否更改而产生的。


1.因为是更具文件修改时间来判断的,所以,在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。


2.当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样,即使文件内容修改了,依然不会 返回新的文件。


为了解决上述的这两个问题。从http1.1开始新增了一个头信息,ETag(Entity 实体标签)


Etag,If--Match


ETag就是将原先协商缓存的比较时间戳的形式修改成了比较文件指纹。


其实Etag,If--Match跟Last-Modified,If-Modified-Since大体一样,区别在于:



  • 后者是对比资源最后一次修改时间,来确定资源是否修改了

  • 前者是对比资源内容,来确定资源是否修改


那我们要怎么比对资源内容呢?我们只需要读取资源内容,转成hash值,前后进行比对就行了!


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)

// const { mtime } = fs.statSync(path.join(__dirname, 'public/index.css')) // 读取最后修改时间
// console.log(mtime.toUTCString(), '--------')
// 响应头的last-modified字段
// res.header('last-modified', mtime.toUTCString())


// 设置ETag
const ifMatch = req.header['if-none-match']
const hash = crypto.createHash('md5')
const fileBuf = fs.readFileSync(path.join(__dirname, 'public/index.css'))
hash.update(fileBuf, 'utf8')
const etag = `"${hash.digest('hex')}"`
console.log(etag, '---etag----')
// 对比hash值
if (ifMatch === etag) {
res.status = 304
} else {
res.header('etag', etag)
// ctx.body = fileBuffer
}
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-cache')
res.render('login');
});


当资源发生改变时,状态码变成200,更新缓存


比如更改css样式



ETag也有缺点



  • ETag需要计算文件指纹这样意味着,服务端需要更多的计算开销。如果文件尺寸大,数量多,并且计算频繁,那么ETag的计算就会影响服务器的性能。显然,ETag在这样的场景下就不是很适合。

  • ETag有强验证和弱验证,所谓将强验证,ETag生成的哈希码深入到每个字节。哪怕文件中只有一个字节改变了,也会生成不同的哈希值,它可以保证文件内容绝对的不变。但是,强验证非常消耗计算量。ETag还有一个弱验证,弱验证是提取文件的部分属性来生成哈希值。因为不必精确到每个字节,所以他的整体速度会比强验证快,但是准确率不高。会降低协商缓存的有效性。


值得注意的一点是,不同于cache-control是expires的完全替代方案(说人话:能用cache-control就不要用expiress)。ETag并不是last-modified的完全替代方案。而是last-modified的补充方案(说人话:项目中到底是用ETag还是last-modified完全取决于业务场景,这两个没有谁更好谁更坏)。


disk cache & memory cache


磁盘缓存+内存缓存,这两种缓存不属于http缓存,而是本地缓存了~


我们直接打开掘金官网,点击network,类型选择all



可以看的很多请求,这里请求包括了静态资源+接口请求


这里我们能够看的很多请求的size中有很多是disk cache(磁盘缓存)


也有一些图片是memory cache(内存缓存)



这两者有什么区别呢?


disk cache: 磁盘缓存,很明显将内容存储在计算机硬盘中,很明显,这种缓存可以占用比较大的空间,但是由于是读取硬盘,所以速度低于内存


memory cache: 内存缓存,速度快,优先级高,但是大小受限于计算机的内存大小,很大的资源还是缓存到硬盘中


上面的浏览器缓存已经有三个大点了,那它们的优先级是什么样的呢?


缓存的获取顺序如下:


1.内存缓存


2.磁盘缓存


3.强缓存


4.协商缓存


如果勾选了Disable cache,那磁盘缓存都不存在了,之还有内存缓存



我还发现,勾选了Disable cache,就base64图片一定会在内存缓存中,其他图片则会发起请求;而不勾选了Disable cache,则大多数图片都在内存缓存中




CDN缓存


CDN缓存是一种服务端缓存,CDN服务商可以将源站上的资源缓到其各地的边缘服务器节点上。当用户访问该资源时,CDN再通过负载均衡将用户的请求调度到最近的缓存节点上,有效减少了链路回源,提高了资源访问效率及可用性,降低带宽消耗。


如果客户端没有命中缓存,那接下来就要发起一次网络请求,根据网络环境,一般大型站点都会配置CDN,CDN会找一个最合适的服务节点接管网络请求。CDN节点都会在本地缓存静态文件数据,一旦命中直接返回,不会穿透去请求应用服务器。并且CDN会通过在不同的网络,策略性地通过部署边缘服务器和应用大量的技术和算法,把用户的请求指定到最佳响应节点上。所以会减少非常多的网络开销和响应延迟。


如果没有部署CDN或者CDN没有命中,请求最终才会落入应用服务器,现在的http服务器都会添加一层反向代理,例如nginx,在这一层同样会添加缓存层,代表技术是squid,varnish,当然nginx作为http服务器而言也支持静态文件访问和本地缓存技术,当然也可以使用远程缓存,如redis,memcache,这里缓存的内容一般为静态文件或者由服务器已经生成好的动态页面,在返回用户之前缓存。


如果前面的缓存机制全部失效,请求才会落入真正的服务器节点。


总结


1.如果页面是协商缓存,如何获取页面最新内容?


协商缓存比较好办,那就刷新页面,不过需要勾选Disable cache,但是用户不知道打开控制台怎么办?


那就右击页面的刷新按钮,然后选择硬性重新加载,或者清空缓存并硬性重新加载,页面就获取到最新资源了



2.如果页面没有设置cache-control,那默认的缓存机制是什么样的?



默认是协商缓存,这也符合浏览器设计,缓存可以减少宽度流量,加快响应速度


3.如果项目重新部署还是没有更新,怎么办?


在确定项目已经部署成功


这样子,可以去问一下公司的运维同事,这个项目是否有CDN缓存


如果项目的域名做了CDN缓存,就需要刷新CDN目录,来解决缓存问题了,不然就只能等,等CDN策略失效,来请求最新的内容


向如下配置的缓存策略,只有过30天才会去真正服务器去请求最新内容



当然你可以测试一下是否为CDN缓存,在url后面拼接一个参数,就能够获取到最新资源了,比如有缓存的链接是baidu.com/abc


你可以在浏览器中输入baidu.com/abc&t=1234来…


当然特定场景,我们不能随意给链接后面添加参数,所以这也只适用于测试一下是否有CDN缓存


所以最好的解决办法还是需要让运维同事去刷新目录,这样就能快速解决CDN缓存问题。


参考链接


juejin.cn/post/712719…


juejin.cn/post/717756…


xiaolincoding.com/

network/2_h…

收起阅读 »

我也惊呆了!关于数字广东对于 CEC-IDE 重大事件的道歉声明网友解读

喜大普奔 8 月 21 日,在某 gov.cn 官网上有一篇文章作出以下报告: 国内首款适配国产操作系统、自主可控的集成开发环境工具 CEC-IDE;国内首款数据安全极限生存保障产品——数据安全守护软硬件一体化产品;国内首款国密指纹认证鼠标…… 在《喜大...
继续阅读 »

喜大普奔


8 月 21 日,在某 gov.cn 官网上有一篇文章作出以下报告:



国内首款适配国产操作系统、自主可控的集成开发环境工具 CEC-IDE;国内首款数据安全极限生存保障产品——数据安全守护软硬件一体化产品;国内首款国密指纹认证鼠标……



image.png


《喜大普奔:全新自主研发的超强 CEC-IDE ,打破国外垄断》一文中有简要叙述。


网友挖掘


8 月 24 日,众多网友经过文件分析并在 vscode 官方仓库创建了编号为 #191279 和 #191229 的 issues,引来网友在该帖进行大量讨论。大量证据都在表明 CEC-IDE 涉嫌造假。


image.png


8 月 25 日,CEC-IDE 官网已无法访问。


image.png


import * as fs from "fs-extra";

const sourceExePath = "path/to/vscode.exe";
const iconFilePath = "path/to/new-icon.ico";

const sourceExeBuffer = fs.readFileSync(sourceExePath);
const iconFileBuffer = fs.readFileSync(iconFilePath);

const targetExeBuffer = replaceIconData(sourceExeBuffer, iconFileBuffer);

fs.writeFileSync("path/to/output.exe", targetExeBuffer);

function replaceIconData(sourceBuffer: Buffer, iconBuffer: Buffer): Buffer {
const targetBuffer = sourceBuffer.clone();
const iconDataOffset = 0x1234;
targetBuffer.fill(
iconBuffer,
iconDataOffset,
iconDataOffset + iconBuffer.length
);

return targetBuffer;
}

众所周知,对于软件开发,立项人是谁,目标是什么,开发人员是谁,测试人员是谁,验收人员是谁,这些都是很清楚的。


致歉声明


8 月 26 日,官方公众号发表致歉声明,这应该也侧面证实了此事。


CEC-IDE 道歉声明.png


声明解读



  • 8 月 24 日晚,我司获悉有网友发帖讨论我司 CEC-IDE 系统


在 8 月 21 日时各网络和电视媒体已进行 CEC-IDE 的宣传报告,表明在 21 日前按正常的开发流程来说,系统已经过测试、发布上线、验收。而我司获悉时是在 24 日,此前那么多时间都在做什么?流程都在做什么?众所周知,此类项目要走的时间和流程都是挺多的。



  • 公司管理层高度重视


从这个事件来看,至少在 26 日前是没有重视的。在 26 日后是不是真的重视?如何体现高度、体现重视,要采取什么样的措施,达到什么样的效果,只字未提。可能是保密调查?



  • 诚恳接受网友批评,并认真开展核查


image.png


诚恳接受网友批评这几个文字与未开启评论区形成鲜明对比。要知道发表当天 6 小时不到就有 6 万人阅读,但 0 评论。



  • CEC-IDE 系统由开发工具、后端系统和组件库组成...


作为一份致歉声明,有近 1/4 的内容是在讲述“列举工作”。



CEC-IDE 系统由开发工具、后端系统和组件库组成,其中开发工具使用开源 VSCode,进行了少量改造,增加了部分功能,后端系统开发了用户、权限、项目、需求等管理,以及任务协作和知识共享等功能,组件库中开发了公共能力组件。




  • 未用于商业用途


今年7月投入试运行,目前仍处在探索阶段,未用于商业用途。意思是不是在说:我们也才刚开始做就被发现了,所以问题不大。而且我们真的没有用于商业用途!但程序截图上的VIP登录和标志显得额外耀眼。


image.png



  • 因版本迭代更新中出现疏忽,近几个版本中缺失了 MIT 协议文件


出现疏忽导致近几个版本缺失 MIT 协议文件,疏忽一词避重就轻,表示我们只是不小心。但大家都知道从 近几个版本中缺失 来看,以前是有此文件(因为原仓库就有此文件)的,只是后面的版本中都被赤裸裸的删除了。


import * as fs from "fs-extra";
import * as yauzl from "yauzl";

const sourceExePath = "path/to/vscode.exe";
const targetExePath = "path/to/output.exe";
const mitLicenseText = "MIT License"; // 要删除的MIT协议文本

fs.copyFileSync(sourceExePath, targetExePath);

yauzl.open(targetExePath, { lazyEntries: true }, (error, zipfile) => {
if (error) throw error;

zipfile.readEntry();

zipfile.on("entry", (entry) => {
if (/\/$/.test(entry.fileName)) {
// 目录项,继续读取下一个entry
zipfile.readEntry();
} else {
// 文件项,处理文件内容
zipfile.openReadStream(entry, (error, readStream) => {
if (error) throw error;

let data = "";
readStream.on("data", (chunk) => {
data += chunk.toString("utf-8");
});

readStream.on("end", () => {
const updatedData = data.replace(mitLicenseText, "");
const writeStream = fs.createWriteStream(entry.fileName);
writeStream.write(updatedData, "utf-8");
writeStream.end();

zipfile.readEntry();
});
});
}
});

zipfile.on("end", () => {
console.log("MIT license removed successfully!");
});
});


  • 产品表述中“自主研发”等用语被网友质疑


被质疑,被 XX。等一系列的词,总让人有一种不能内省的感觉。而“自主研发”此类词语根本就不是单纯的自不自主那么简单。担忧从来不是自不自主开发,而是自信的磨灭、情怀的磨灭。



  • 数字广东公司向所有开源贡献者致以衷心



开源软件的使用极大提升了我司产品研发效率,开源项目为我司提供了巨大帮助,开源精神是程序员共同的同心圆,数字广东公司向所有开源贡献者致以衷心的感谢和崇高的敬意。



广大开源者可能不专门需要此敬意。但对 VSCODE 开发组应有,对默默真正投入自主研发的人应有此敬意。


相关链接


收起阅读 »

Android 时钟翻页效果

web
背景 今天逛掘金,发现了一个有意思的web view效果,想着Android能不能实现一下捏。 原文链接:juejin.cn/post/724435… 具体实现分析请看上文原文链接,那我们开始吧! 容器 val space = 10f //上下半间隔 val...
继续阅读 »

背景


今天逛掘金,发现了一个有意思的web view效果,想着Android能不能实现一下捏。


image.png
原文链接:juejin.cn/post/724435…


具体实现分析请看上文原文链接,那我们开始吧!


容器


val space = 10f //上下半间隔
val bgBorderR = 10f //背景圆角
//上半部分
val upperHalfBottom = height.toFloat() / 2 - space / 2
canvas.drawRoundRect(
0f,
0f,
width.toFloat(),
upperHalfBottom,
bgBorderR,
bgBorderR,
bgPaint
)
//下半部分
val lowerHalfTop = height.toFloat() / 2 + space / 2
canvas.drawRoundRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat(),
bgBorderR,
bgBorderR,
bgPaint
)

image.png


绘制数字


我们首先居中绘制数字4


val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
//居中显示
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom
canvas.drawText(number4, x, y, textPaint)

image.png


接下来我们将数字切分为上下两部分,分别绘制。


val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom
// 上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
// 下半部分裁剪
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
canvas.drawText(number4, x, y, textPaint)

image.png


翻转卡片


如何实现让其旋转呢?
而且还得是3d的效果了。我们选择Camera来实现。
我们先让数字'4'旋转起来。


准备工作,通过属性动画来改变旋转的角度。


private var degree = 0f //翻转角度
private val camera = Camera()
private var flipping = false //是否处于翻转状态
...
//动画
val animator = ValueAnimator.ofFloat(0f, 360f)
animator.addUpdateListener { animation ->
val animatedValue = animation.animatedValue as Float
setDegree(animatedValue)
}
animator.doOnStart {
flipping = true
}
animator.doOnEnd {
flipping = false
}
animator.duration = 1000
animator.interpolator = LinearInterpolator()
animator.start()
...

private fun setDegree(degree: Float) {
this.degree = degree
invalidate()
}

让数字'4'旋转起来:


  override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 居中绘制数字4
val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom

if (!flipping) {
canvas.drawText(number4, x, y, textPaint)
} else {
camera.save()
canvas.translate(width / 2f, height / 2f)
camera.rotateX(-degree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
}
}

file.gif

我们再来看一边效果图:
我们希望将卡片旋转180度,并且0度-90度由上半部分完成,90度-180度由下半部分完成。


我们调整一下代码,先处理一下上半部分:


...
val animator = ValueAnimator.ofFloat(0f, 180f)
...
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val space = 10f //上下半间隔
//上半部分
val upperHalfBottom = height.toFloat() / 2 - space / 2
...
// 居中绘制数字4
val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom

if (!flipping) {
//上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
} else {
if (degree < 90) {
//上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
camera.save()
canvas.translate(width / 2f, height / 2f)
camera.rotateX(-degree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
}
}
}

效果如下:


upper.gif

接下来我们再来看一下下半部分:


override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val space = 10f //上下半间隔
//下半部分
val lowerHalfTop = height.toFloat() / 2 + space / 2

// 居中绘制数字4
val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom

if (!flipping) {
// 下半部分裁剪
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
} else {
if (degree > 90) {
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
camera.save()
canvas.translate(width / 2f, height / 2f)
val bottomDegree = 180 - degree
camera.rotateX(bottomDegree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
}
}
}

lower.gif

那我们将上下部分结合起来,效果如下:


all.gif

数字变化


好!我们完成了翻转部分,现在需要在翻转的过程中将数字改变:

我们还是举例说明:数字由'4'变为'5'的情况。我们思考个问题,什么时候需要改变数字?

上半部分在翻转开始的时候,上半部分底部显示的数字就应该由'4'变为'5',但是旋转的部分还是应该为'4',
下半部分开始旋转的时候底部显示的数字还是应该为'4',而旋转的部分该为'5'。


canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
canvas.drawText(number5, x, y, textPaint)
canvas.restore()
// 下半部分裁剪
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
//=====⬆️=====上述的代码显示的上下底部显示的内容,即上半部分地步显示5,下半部分显示4
if (degree < 90) {
//上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
camera.save()
canvas.translate(width / 2f, height / 2f)
camera.rotateX(-degree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
//=====⬆️=====上述的代码表示上半部分旋转显示的内容,即数字4
} else {
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
camera.save()
canvas.translate(width / 2f, height / 2f)
val bottomDegree = 180 - degree
camera.rotateX(bottomDegree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number5, x, y, textPaint)
canvas.restore()
//=====⬆️=====上述的代码表示下半部分旋转显示的内容,即数字5
}

效果图如下:大伙可以在去理一下上面数字的变化的逻辑。


a.gif

最后我们加上背景再看一下效果:


a.gif

小结


上述代码仅仅提供个思路,仅为测试code,正式代码可不

作者:蹦蹦蹦
来源:juejin.cn/post/7271518821809438781
能这么写哦 >..<

收起阅读 »

程序员应避免自我安慰式的无效学习

重复学习了很多年 从事前端开发已经超过5年,自诩也是一个坚持学习的程序猿。 今年工作不是很忙,并且职场的35岁槛已经到来,既有时间又有压力,于是更多的时间在思考成长这件事。 最近在做的一件特别重要的事情是:做减法。 从事开发的这些年,因为待过的公司不同,后端的...
继续阅读 »

重复学习了很多年


从事前端开发已经超过5年,自诩也是一个坚持学习的程序猿。
今年工作不是很忙,并且职场的35岁槛已经到来,既有时间又有压力,于是更多的时间在思考成长这件事。


最近在做的一件特别重要的事情是:做减法。


从事开发的这些年,因为待过的公司不同,后端的语言不同,业务不同,加上给自己制定每年都要学习一门有价值的课程这样一个目标。先后学习了C#,PHP,JAVA。我这可不是走马观花式的学习,我是要求自己学习后至少能够使用相应语言的框架做简单基础开发。结果是除了C#外,我学会了PHP的Yii做后端开发,学会了Springboot做开发,虽然仅仅是常规的开发,但走过了从0-1的过程。


当然除了后端语言,前端技术栈从Vue、react、微信小程序、RN开发、Nodejs都有涉及,且都能进行日常开发。当然这里面最熟练还是Vue还有nodejs。后来我觉得做前端就得做全套,又花钱专门学了android app开发。虽然android平时不会涉及,学习的具体时间也是3年前了,但是也度过了从0-1的阶段。


学习了这些知识点,最大的一个结果是有道笔记我的知识笔记里面记录了大量的笔记。


学了这么多,照理说我应该对自己很有信心。但扪心自问我没有,我感觉我自己始终找不到让我特别自信的点,我想做自己的产品,但是始终没有做成。有段时间,我一直很迷茫。


现在回忆起来大概是因为我读了一本书《财富自由之路》,至于具体哪段内容我忘记了,反正我后来开始做减法,多个方面做减法,如下



  1. 收拾买的书籍,常用的放在明面上,不常用的收藏起来

  2. 不再买书,因为我发现我其实有大量的书只是看了开头

  3. 整理电脑桌面和文件夹,尤其整理做过的大量开发练习,分门别类并删除大量早期的和无用的

  4. 整理手机桌面和文件夹,手机从4屏变为2屏

  5. 整理浏览器的书签栏,分门别类

  6. 整理关注的股票,整理自选分类,坚决去掉自己不熟悉的,最后只留下不到10只

  7. 整理有道笔记里面笔记:共删除150多篇,重新划分目录


这里面感触最深的是整理有道笔记。我发现很多知识点我学了一遍又一遍,记了一次又一次,我每一年都会起很多诸如JavaScript学习笔记,Vue学习笔记,nodejs学习笔记等标题的笔记,但工作内容并没有特别大的变化,以前记住的知识点因为不经常温习和使用被忘掉,再次用到时候我会重新搜索出来然后再次记录。就这样反复着向前。


然而这样存在一个很大的问题:我在原地踏步。这个词很形象的形容了我的状况,看似学习了:记了笔记,但实际上根本没有进步,都是自我安慰,是对年龄带来的焦虑的缓解,是对社会给予的压力的缓解。


做减法之后,我想到盛传已久的一句话:太阳底下没有新鲜事。学习同样如此,任何学科都是有边界的。有边界意味着边界里面一定是在重复着某些知识点。只要找到这些知识点,总结这些知识点,迭代这些知识点,就可以避免重复无效的学习,进而真正进步。


划分知识结构


划分的原则:同一级不可以超过5个分类,因为人同时管理好的数量上限是5个左右。下面是一部分划分截图


开发技术.png


之后就是对最下级分类内容的填充和迭代。我是从2018年开始做的笔记。划分分类之后,我开始整理过去五年多的笔记,将笔记当中有用的属于对应分类的内容拿出来,填充到对应部分,同时删除原来的笔记。


我也将日常做了划分:


日常笔记.png


日常工作主要是一些日常的记录。日常分类和上述的开发技术,同属一个级别。都归属于我的文件夹下。我的文件夹:


我的文件夹.png


毛主席说过:好脑筋不如烂笔头。笔记的好处就是拓展思维的里程。


当然就划分来说,每个人的经历和认知是不同的,不同人有不同的划分标准。但是我觉得这不是重要的,重要的是聚焦注意力,最重要的是找到自己的世界,找到自己的内生动力。


找到自己的世界


刘青云出演的电视剧《大时代》有台词:一个人要成功,就一定要找到自己的世界。


猫腻的《择天记》男主有这样一句话:我修的是顺心意。这个时代谁修的不是顺心意呢?只有找到自己的世界顺自己的心意才能真正登堂入室,避免无效

作者:通往自由之路
来源:juejin.cn/post/7270906612339884093
的学习。因为此时才真正有了动力。

收起阅读 »

人情世故职场社会生存实战篇(三)

人情人情世故职场社会生存实战篇(一)人情人情世故职场社会生存实战篇(二) 21、问:我是做装饰行业的,我在多年前认识一个朋友,他呢混的比较好。经常给我介绍一些装修活儿,几十万的,一百多万的都交给我做。对我像自己人一样。我们萍水相逢 真的很感激他。因为我的不善...
继续阅读 »

人情人情世故职场社会生存实战篇(一)

人情人情世故职场社会生存实战篇(二)



21、问:我是做装饰行业的,我在多年前认识一个朋友,他呢混的比较好。经常给我介绍一些装修活儿,几十万的,一百多万的都交给我做。对我像自己人一样。我们萍水相逢 真的很感激他。因为我的不善交际,我们的来往并不多。想请教一下,在日常交往中,我该怎样做,以表现对他的感谢,以及怎样送礼给他,毕竟人家什么都不缺。


答:你认为富人不缺钱,这都是假象,富人的开销也是很大的。加油不要钱吗,物业不要钱吗,孩子读书不要钱吗,培训班不要钱吗,他们比我们这些屌丝更需要钱。因为我们一个月可能几千块就搞定了,他们一个月可能几万都不够。


你能给他添砖加瓦,习惯性的给他反馈,他也会觉得你是同道中人,是理解他的。王宝强搞定冯小刚,是因为他每年都给冯小刚送米送面。不要觉得没反馈,也可以获得资源,这会让你的路越走越窄。


反馈:1、你帮我介绍业务了,你就是我的大恩人,以后你的事儿就是我的事儿,需要我干啥您尽管吩咐。


2、我这个业务虽然本小利微,你可能也看不上,但是你确实帮我太多了,这点东西不成敬意,你一定要收下,不然我每晚每晚睡不着。


3、我们萍水相逢,能认识您,结交您是我的福气。您放心,只要您介绍的业务,我会百分百的让客户满意,亏钱也无所谓,不能坏了您的名声。(私人薇:xxxx)


22、问:明天晚上我们老板和我现任直接领导都在 ,我只能敬酒一次,怎么说比较好?既表达了对老板的敬意,又不得罪现任领导(老板,是以前的老领导)。


答:1、他们俩不在一个桌,你要先敬你的老板。


2、他俩要是在一个桌,你要对着领导介绍你原来的领导,说他多厉害多厉害。这个多厉害,用一句话说:以前我什么都听他的 ,现在他把我培养出来了。现在我又成了您的兵。一个是我的导师,一个是我的将军,我干了,您们二老随意。然后收摊就行了。


23、问:我是一名管理者。请问对于有本事,有才华,但名声不好的人怎么管理好呢?


答:看过水浒传吗,这样的人就是水浒传里的时迁,宋江怎么对时迁,你就怎么对他。给待遇,但不给级别。就是厚而不尊,因为时迁是个贼,所以不能把他排进领导班子,但是他贡献大,宋江给他丰厚的待遇和奖励,但不提拔他,这叫厚而不尊。你不厚,他不给你干;你要尊,队伍的名气就坏了。虽然时迁排名很靠后,不受尊重,没地位,但是时迁的个人待遇、工资奖金水平都很高。


24、问:我们处内有个女孩,小我10岁左右,她想提干,但是她没有得到上级认可,业务水平一般,但是家里有一定背景。她看到我要被提拔,就很嫉妒,拉拢处里另外一个刚入职不久的女孩, 孤立我。我现在必须要处理好同事关系,真要提拔,也要找处里同事谈话,不能出现反对的声音。现在我该怎么办?怎么和她相处呢?


答:你平时跟领导搞好关系,这事领导说了算,她说了不算,她那里你就示弱,在新来的那个姑娘那里多说她的好话,夸她能力强,情商高,说你们以前的一些事情,就是夸她,慢慢她就不好意思了,如果她再说你的坏话,新来的姑娘都鄙视她。


25、问:我们领导以前是财政局的,现在让我们每个口子都报表,因为我们下属也不只归我们管,所以他有气,我就成了夹板,下属公司这个月的报表已经报了5次了,他都不满意,每次都发火,而且每次标准不一样。怎么办?


答:先去拜师学艺,问问他标准是什么。按照他的标准去做,自然通过。买一条烟,去办公室请教∶领导,我这个人啊就是愚笨,还请您多指导指导,这个表到底是哪里不对,您指导一次我记住了,以后就按这个标准了,也不再惹您生气了,您指点指点我。


26、问:我们单位一把手要调走,我算是在他在位期间招入的和提起来的,去他办公室,如何表达感恩更完美?因为突然,我也没准备什么礼物,还有什么补救措施吗?


答:3个点,感恩+愧疚


1、没有你,就没有我的今天……


2、吃水不忘挖井人,我能帮你做点儿什么,我一定帮你去做……


3、我哪儿做的不对,你一定要说,我比较笨……


27、问:我是一个部门副职,遇到了这么个情况,我刚来任职,需要从下面的人那里获取数据信息,我才能开展我接下来的工作,然总是不给我主动汇报,总是要我催他们,怎么办呢?


答:你就问他们一个问题,就是上一届领导是怎么带他的,那么你就怎么带他们。也就是说你不要变动性太大,你要是变动性太大的话,大家都不屌你。最好的交接方式,大家问你问题的时候,你说以前你们怎么弄的,现在还怎么弄,我什么也不懂,我听大家的。那么这个问题就很快就解决了。新官上任三把火,第一把火,请员工吃饭,第二把火,表扬员工,第三个,你说我会为你们争取利益的。先把局面打开就OK了。


28、问:今天早上领导安排我去做工作份外的事,我就说我现在有事情做只做了一半,我就说我不去,领导就说叫你做什么就做什么,不做就叫我马上滚蛋,可是想一想,好我去做,我做了一上午才做完,问下我的做法有问题吗,接下来该怎么操作?


答:领导安排任务的时候不要拒绝,否则就是给领导难堪,先接受,对领导说:好的领导。你要是手头有事,可以问领导:领导,我现在手头上还有点事,那我现在是先做 A 还是先做 B,把皮球踢给老板, 领导如果问:你想做哪个?你说:我服从领导的安排。这样就是不得罪领导,自己做的慢也不受罚。你现在得罪了领导,领导生气了,去道歉就可以,说自己年轻不懂事,以后有不对的地方还请领导多多指教。


29、问:我们领导分配给我一个辅警辅助我工作,有时候他会和我使小脾气,他好像就是这性格习惯了,我有工作给他做,他不愿意做或者做的不仔细,我还得给他擦屁股。有时候我就不给他布置任务,我去跟领导说我忙不过来,某项工作给他做,然后他才会好好做。其实我特别想跟领导打他小报告,但是忍了。这该怎么办呢?


答:这个好办啊,你每天把工作分工,简单做个记录,你负责什么他负责什么。早上上班你跟他说:领导让我去汇报咱两的工作分工,您看一下没问题吧?你们两商量完了,写在本子上,然后你去跟领导请示汇报:领导,这是我们今天一天的工作,您还有没有其他的指示?回来你跟搭档说:领导说分工很好,晚上汇报完成情况。


30、问:老师 我最近走上了管理岗位。但是在人情世故方面还是有些胆怯。举个例子:被提拔了想五一想拜访领导,总是怕被拒绝,感觉踩不到领导在家的空闲点,时机如何把握呢?


答:这个太简单了,他的司机,你请了吗?他的秘书,你请了吗?先搞定他身边的人,让他身边的人帮你搞定领导,你+领导的司机+领导的秘书=你的队伍。你们3个人吃领导一个人,别心疼在小人物身上花钱,小人物有时候比大人物更有价值...

作者:公z号_纵横潜规则
来源:juejin.cn/post/7268260762401095699

收起阅读 »

虚拟列表 or 时间分片

前言 最近在做一个官网,原本接口做的都是分页的,但是客户提出不要分页,之前看过虚拟列表这个东西,所以进行一下了解。 为啥要用虚拟列表呢! 在日常工作中,所要渲染的也不单单只是一个li那么简单,会有很多嵌套在里面。但数据量过多,同时渲染式,会在 渲染样式 跟 布...
继续阅读 »

前言


最近在做一个官网,原本接口做的都是分页的,但是客户提出不要分页,之前看过虚拟列表这个东西,所以进行一下了解。


为啥要用虚拟列表呢!


在日常工作中,所要渲染的也不单单只是一个li那么简单,会有很多嵌套在里面。但数据量过多,同时渲染式,会在 渲染样式 跟 布局计算上花费太多时间,体验感不好,那你说要不要优化嘛,不是你被优化就是你优化它。


进入正题,啥是虚拟列表?


可以这么理解,根据你视图能显示多少就先渲染多少,对看不到的地方采取不渲染或者部分渲染。




这时候你完成首次加载,那么其他就是在你滑动时渲染,就可以通过计算,得知此时屏幕应该显示的列表项。


怎么弄?


备注:很多方案对于动态不固定高度、网络图片以及用户异常操作等形式处理的也并不好,了解下原理即可。


虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。


1、计算当前可视区域起始数据索引(startIndex)

2、计算当前可视区域结束数据索引(endIndex)

3、计算当前可视区域的数据,并渲染到页面中

4、计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上


由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可正常的触发滚动,将Html结构设计成如下结构:

<div class="infinite-list-container">
<div class="infinite-list-phantom"></div>
<div class="infinite-list">
<!-- item-1 -->
<!-- item-2 -->
<!-- ...... -->
<!-- item-n -->
</div>
</div>


  • infinite-list-container 为可视区域的容器

  • infinite-list-phantom 为容器内的占位,高度为总列表高度,用于形成滚动条

  • infinite-list 为列表项的渲染区域

    接着,监听infinite-list-containerscroll事件,获取滚动位置scrollTop

  • 假定可视区域高度固定,称之为screenHeight

  • 假定列表每项高度固定,称之为itemSize

  • 假定列表数据称之为listData

  • 假定当前滚动位置称之为scrollTop

  •   则可推算出:

    • 列表总高度listHeight = listData.length * itemSize
    • 可显示的列表项数visibleCount = Math.ceil(screenHeight / itemSize)
    • 数据的起始索引startIndex = Math.floor(scrollTop / itemSize)
    • 数据的结束索引endIndex = startIndex + visibleCount
    • 列表显示数据为visibleData = listData.slice(startIndex,endIndex)

      当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移至可视区域中。

    • 偏移量startOffset = scrollTop - (scrollTop % itemSize);

时间分片


那么虚拟列表是一方面可以优化的方式,另一个就是时间分片。


先看看我们平时的情况


1.直接开整,直接渲染。




诶???我们可以发现,js运行时间为113ms,但最终 完成时间是 1070ms,一共是 js 运行时间加上渲染总时间。

PS:

  • 在 JS 的 EventLoop中,当JS引擎所管理的执行栈中的事件以及所有微任务事件全部执行完后,才会触发渲染线程对页面进行渲染
  • 第一个 console.log的触发时间是在页面进行渲染之前,此时得到的间隔时间为JS运行所需要的时间
  • 第二个 console.log是放到 setTimeout 中的,它的触发时间是在渲染完成,在下一次 EventLoop中执行的

那我们改用定时器


上面看是因为我们同时渲染,那我们可以分批看看。

let once = 30
let ul = document.getElementById('testTime')
function loopRender (curTotal, curIndex) {
if (curTotal <= 0) return
let pageCount = Math.min(curTotal, once) // 每页最多20条
setTimeout(_ => {
for (let i=0; i<pageCount;i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i
ul.appendChild(li)
}
loopRender(curTotal - pageCount, curIndex + pageCount)
}, 0)
}
loopRender(100000, 0)

这时候可以感觉出来渲染很快,但是如果渲染复杂点的dom会闪屏,为什么会闪屏这就需要清楚电脑刷新的概念了,这里就不详细写了,有兴趣的小朋友可以自己去了解一下。

可以改用 requestAnimationFrame 去分批渲染,因为这个关于电脑自身刷新效率的,不管你代码的事,可以解决丢帧问题。

let once = 30
let ul = document.getElementById('container')
// 循环加载渲染数据
function loopRender (curTotal, curIndex) {
if (curTotal <= 0) return
let pageCount = Math.min(curTotal, once) // 每页最多20条
window.requestAnimationFrame(_ => {
for (let i=0; i<pageCount;i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i
ul.appendChild(li)
}
loopRender(curTotal - pageCount, curIndex + pageCount)
})
}
loopRender(100000, 0)

还可以改用 DocumentFragment


什么是 DocumentFragment



DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的 Document使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为 DocumentFragment不是真实DOM树的一部分,它的变化不会触发DOM树的(重新渲染) ,且不会导致性能等问题。

可以使用 document.createDocumentFragment方法或者构造函数来创建一个空的 DocumentFragment

ocumentFragments是DOM节点,但并不是DOM树的一部分,可以认为是存在内存中的,所以将子元素插入到文档片段时不会引起页面回流。



当 append元素到 document中时,被 append进去的元素的样式表的计算是同步发生的,此时调用 getComputedStyle 可以得到样式的计算值。而 append元素到 documentFragment 中时,是不会计算元素的样式表,所以 documentFragment 性能更优。当然现在浏览器的优化已经做的很好了, 当 append元素到 document中后,没有访问 getComputedStyle 之类的方法时,现代浏览器也可以把样式表的计算推迟到脚本执行之后。

let once = 30 
let ul = document.getElementById('container')
// 循环加载渲染数据
function loopRender (curTotal, curIndex) {
if (curTotal <= 0) return
let pageCount = Math.min(curTotal, once) // 每页最多20条
window.requestAnimationFrame(_ => {
let fragment = document.createDocumentFragment()
for (let i=0; i<pageCount;i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i
fragment.appendChild(li)
}
ul.appendChild(fragment)
loopRender(curTotal - pageCount, curIndex + pageCount)
})
}
loopRender(100000, 0)

其实同时渲染十万条数据这个情况还是比较少见的,就当做个了解吧。


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