注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

尊嘟假嘟?三行代码提升接口性能600倍

一、背景   业务在群里反馈编辑结算单时有些账单明细查不出来,但是新建结算单可以,我第一反应是去测试环境试试有没有该问题,结果发现没任何问题!!!   然后我登录生产环境编辑业务反馈有问题的结算单,发现查询接口直接504网关超时了,此时心里已经猜到是代码性能问...
继续阅读 »

一、背景


  业务在群里反馈编辑结算单时有些账单明细查不出来,但是新建结算单可以,我第一反应是去测试环境试试有没有该问题,结果发现没任何问题!!!
  然后我登录生产环境编辑业务反馈有问题的结算单,发现查询接口直接504网关超时了,此时心里已经猜到是代码性能问题导致的,接来下就把重点放到排查接口超时的问题上了。


二、问题排查


遇到生产问题先查日志是基本操作,登录阿里云的日志平台,可以查到接口耗时竟然高达469245毫秒


这个结算单关联的账单数量也就800多条,所以可以肯定这个接口存在性能问题。


image


但是日志除了接口耗时,并没有其他报错信息或异常信息,看不出哪里导致了接口慢。


接口慢一般是由如下几个原因导致:



  1. 依赖的外部系统慢,比如同步调用外部系统的接口耗时比较久

  2. 处理的数据过多导致

  3. sql性能有问题,存在慢sql

  4. 有大循环存在循环处理的逻辑,如循环读取exel并处理

  5. 网络问题或者依赖的中间件比较慢

  6. 如果使用了锁,也可能由于长时间获取不到锁导致接口超时


当然也可以使用arthas的trace命令分析哪一块比较耗时。


由于安装arthas有点麻烦,就先猜测可能慢sql导致的,然后就登录阿里云RDS查看了慢sql监控日志。
image
好家伙一看吓一跳,sql耗时竟然高达66秒,而且执行次数还挺多!


我赶紧把sql语句放到数据库用explain命令看下执行计划,分析这条sql为啥这么慢。


EXPLAIN SELECT DISTINCT(bill_code) FROM `t_bill_detail_2023_4` WHERE  
(settlement_order_code IS NULL OR settlement_order_code = 'JS23122600000001');

分析结果如下:


image


如果不知道explain结果每个字段的含义,可以看看这篇文章《长达1.7万字的explain关键字指南!》。


可以看到扫描行数达到了250多万行,ref已经是最高效的const,但是看最后的Extra列
Using temporary 表明这个sql用到了临时表,顿时心里清楚什么原因了。


因为sql有个去重关键字DISTINCT,所以mysql在需要建临时表来完成查询结果集的去重操作,如果结果集数据量比较小没有超过buffer,就可以直接在内存中去重,这种效率也是比较高的。


但是如果结果集数据量很大,buffer存不下,那就需要借助磁盘完成去重了,我们都知道操作磁盘相比内存是非常慢的,时间差几个数量级


虽然这个表里的settlement_order_code字段是有索引的,但是线上也有很多settlement_order_code为null的数据,这就导致查出来的结果集非常大,然后又用到临时表,所以sql耗时才这么久!


同时,这里也解释了为什么测试环境没有发现这个问题,因为测试环境的数据不多,直接在内存就完成去重了。


三、问题解决


知道了问题原因就很好解决了,首先根据SQL和接口地址很快就找到出现问题的代码是下图红框圈出来的地方


image


可以看到代码前面有个判断,只有当isThreeOrderQuery=true时才会执行这个查询,判断方法代码如下


image


然后因为这是个编辑场景,前端会把当前结算单号(usedSettlementOrderCode字段)传给后端,所以这个方法就返回了true。


同理,拼接出来的sql就带了条件(settlement_order_code IS NULL OR settlement_order_code = 'JS23122600000001')。
image


解决起来也很简单,把isThreeOrderQuery()方法圈出来的代码去掉就行了,这样就不会执行那个查询,同时也不会影响原有的代码逻辑,因为后面会根据筛选条件再查一次t_bill_detail表。


改代码发布后,再编辑结算单,优化后的效果如下图:


image


只改了三行代码,接口耗时就立马从469245ms缩短到700ms,性能提升了600多倍


四、总结


感觉压测环境还是有必要的,有些问题数据量小了或者请求并发不够都没法暴露出来,同时以后写代码可以提前把sql在数据库explain下看看性能如何,毕竟能跑就行不是我们的追求😏。


作者:2YSP
来源:juejin.cn/post/7322156759443144713
收起阅读 »

不要再滥用可选链运算符(?.)啦!

web
前言 之前整理过 整理下最近做的产品里 比较典型的代码规范问题,里面有一个关于可选链运算符(?.)的规范,当时只是提了一下,今天详细说下想法,欢迎大佬参与讨论。 可选链运算符(?.),大家都很熟悉了,直接看个例子: const result = obj?.a?...
继续阅读 »

前言


之前整理过 整理下最近做的产品里 比较典型的代码规范问题,里面有一个关于可选链运算符(?.)的规范,当时只是提了一下,今天详细说下想法,欢迎大佬参与讨论。


可选链运算符(?.),大家都很熟悉了,直接看个例子:


const result = obj?.a?.b?.c?.d

很简单例子,上面代码?前面的属性如果是空值(null或undefined),则result值是undefined,反之如果都不是空值,则会返回最后一个d属性值。


本文不是讲解这种语法的用法,主要是想分析下日常开发中,这种语法 滥用、乱用 的问题。


滥用、乱用


最近在code review一个公司项目代码,发现代码里用到的可选链运算符,很多滥用,用的很无脑,经常遇到这种代码:


const userName = data?.items?.[0]?.user?.name

↑ 不管对象以及属性有没有可能是空值,无脑加上?.就完了。


// react class component
const name = this.state?.name

// react hooks
const [items, setItems] = useState([])
items?.map(...)
setItems?.([]) // 真有这么写的

↑ React框架下,this.state 值不可能是空值,初始化以及set的值都是数组,都无脑加上?.


const item1 = obj?.item1
console.log(item1.name)

↑ 第一行代码说明obj或item1可能是空值,但第二行也明显说明不可能是空值,否则依然会抛错,第一行的?.也就没意义了。


if (obj?.item1?.item2) {
const item2 = obj?.item1?.item2
const name = obj?.item1?.item2?.name
}

↑ if 里已经判断了非空了,内部就没必要判断非空了。


问题、缺点


如果不考虑 ?. 使用的必要性,无脑滥用其实也没问题,不会影响功能,优点也很多:



  1. 不用考虑是不是非空,每个变量或属性后面加 ?. 就完了。

  2. 由于不用思考,开发效率高。

  3. 不会有空引用错误,不会有页面点点就没反应或弹错问题。


但是问题和缺点也很明显,而且也会很严重。分两点分析下:



  1. 可读性、维护性:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。

  2. 隐式过滤了异常:把异常给隐式过滤掉了,导致不能快速定位问题。

  3. 编译后代码冗余。

  4. 护眼:一串?.看着难受,特别是以一个code reviewer 角度看。


1. 可读性、维护性


可读性和维护性其实是一回事,都是指不是源代码作者的开发维护人员,在捋这块代码逻辑、修改bug等情况时,处理问题的效率,代码写的好处理就快,写的烂就处理慢,很简单道理。


const onClick = () => {
const user = props.data?.items?.[0]?.user
if (user) {
// use user to do something
}
}

已这行代码为例,有个bug现象是点击按钮没反应,维护开发看到这块代码,就会想这一串链式属性里,是不是有可能有空值,所以导致了user是空值,没走进if里导致没反应。然后就继续分析上层组件props传输代码,看data值从哪儿传来的,看是不是哪块代码导致data或items空值了。。。


其实呢?从外部传过来的这一串属性里不会有空值的情况,导致bug问题根本不在这儿。


const user = props.data.items[0].user

那把?.都去掉呢?维护开发追踪问题看到这行代码,data items 这些属性肯定不能是空值,不然console就抛错了,但是bug现象里并没有抛错,所以只需要检查user能不能是空值就行了,很容易就排除了很多情况。


总结就是:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。


2. 隐式过滤了异常


api.get(...).then(result => {
const id = result?.id
// use id to do something
})

比如有个需求,从后台api获取数据时,需要把结果里id属性获取到,然后进行数据处理,从业务流程上看,这个api返回的result以及id必须有值,如果没值的话后续的流程就会走不通。


然后后台逻辑由于写的有问题,导致个别情况返回的 result=null,但是由于前端这里加了?.,导致页面没有任何反应,js不抛错,console也没有log,后续流程出错了,这时候如果想找原因就会很困难,对代码熟悉还行,如果不是自己写的就只能看代码捋逻辑,如果是生产环境压缩混淆了就更难排查了。


api.get(...).then(result => {
const id = result.id
// use id to do something
})

?.去掉呢?如果api返回值有问题,这里会立即抛错,后面的流程也就不能进行下去了,无论开发还是生产环境都能在console里快速定位问题,即使是压缩混淆的也能从error看出一二,或者在一些前端监控程序里也能监听到。


其实这种现象跟 try catch 里不加 throw 类似,把隐式异常错误完全给过滤掉了,比如下面例子:


// 这个try本意是处理api请求异常
try {
const data = getSaveData() // 这段js逻辑也在try里,所以如果这个方法内部抛错了,页面上就没任何反应,很难追踪问题
const result = await api.post(url, data)
// result 逻辑处理
} catch (e) {
// 好点的给弹个框,打个log,甚至有的啥都不处理
}

总结就是:把异常给隐式过滤掉了,导致不能快速定位问题。


3. 编译后代码冗余


如果代码是ts,并且编译目标是ES2016,编译后代码会很长。可以看下 http://www.typescriptlang.org/play 效果。


image.png


Babel在个别stage下,编译效果一样。


image.png


但并不是说一点都不用,意思是尽量减少滥用,这样使用的频率会少很多,这种编译代码沉余也会少不少。


应该怎么用?


说了这么多,.? 应该怎么用呢?意思是不用吗?当然不是不能用,这个特性对于开发肯定好处很多的,但是得合理用,不能滥用。



  1. 避免盲目用,滥用,有个点儿就加问号,特别是在一个比较长的链式代码里每个属性后面都加。

  2. 只有可能是空值,而且业务逻辑中有空值的情况,就用;其它情况尽量不要用。


其实说白了就是:什么时候需要判断一个变量或属性非空,什么时候不需要。首先在使用的时候得想下,问号前面的变量或属性值,有没有可能是空值:



  1. 很明显不可能是空值,比如 React类组件里的 this.state this.props,不要用;

  2. 自己定义的变量或属性,而且没有赋值为空值情况,不要用;

  3. 某些方法或者组件里,参数和属性不允许是空值,那方法和组件里就不需要判断非空。(对于比较common的,推荐写断言,或者判断空值情况throw error)

  4. 后台api请求结果里,要求result或其内部属性必须有值,那这些值就不需要判断非空。

  5. 按正常流程走,某个数据不会有空值情况,如果是空值说明前面的流程出问题了,这种情况就不需要在逻辑里判断非空。


const userName = data?.items?.[0]?.user?.name // 不要滥用,如果某个属性有可能是空值,则需要?.
const userName = data.items[0].user?.name // 比如data.items数组肯定不是空数组

const items2 = items1.filter(item => item.checked)
if (items2?.length) { } // 不需要?.

// react class component
const name = this.state?.name // 不需要?.

// react hooks
const [items, setItems] = useState([])
items?.map(...) // 如果setItems没有赋值空值情况,则不需要?.
setItems?.([]) // 不需要?.

const item1 = obj?.item1 // 不需要?.
console.log(item1.name)

const id = obj?.id // 下面代码已经说明不能是空值了,不需要?.
const name = obj.name

if (obj?.item1?.item2) {
const item2 = obj?.item1?.item2 // 不需要?.
const name = obj?.item1?.item2?.name // 不需要?.
}

const id = obj?.item?.id // 不需要?.
api.get(id).then(...) // 这个api如果id是空值,则api会抛错

当然,写代码时还得多想一下属性是否可能是空值,会一定程度的影响开发效率,也一定有开发会觉得很烦,不理解,无脑写?.多容易啊,但是我从另外两个角度分析下:



  1. 我觉得一个合格的开发应该对自己的代码逻辑很熟悉,应该有责任知道哪些值可能是空值,哪些不可能是空值(并不是说所有,也有大部分了),否则就是对自己的代码了解很少,觉得代码能跑就行,代码质量自然就低。

  2. 想想在这个新特性出来之前大家是怎么写的,会对每个变量和属性都加if非空判断或者用逻辑与(&&)吗?不会吧。


总结


本文以一个 code reviewer 角度,分析了 可选链运算符(?.) 特性的滥用情况,以及“正确使用方式”,只是代表我本人的看法,欢迎大佬参与讨论,无条件接受任何反驳。


滥用的缺点:



  1. 可读性、维护性:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。

  2. 隐式过滤了异常:把异常给隐式过滤掉了,导致不能快速定位问题。

  3. 编译后代码冗余。

  4. 护眼:一串?.看着难受,特别是以一个code reviewer 角度看。


“正确用法”:



  1. 避免盲目用,滥用,有个点儿就加问号,特别是在一个比较长的链式代码里每个属性后面都加。

  2. 只有可能是空值,而且业务逻辑中有空值的情况,就用;其它情况尽量不要用。




后记(09月25日更新)


从评论上看,对于可选链的看法,大多声音是能加就加,多加总比少加好,原因就是不想背锅,不想上线后JS动不动就崩了,无论根本原因是不是前端开发没加判断导致的,第一责任人就会找到你,有的甚至会被上级追责,问题就更严重了,而且很难解释清楚;另一方面就是为了赶工期,可选链的其中一个优点就是简单,提高开发效率。


我再从几个方面浅浅的扩展下我的看法,欢迎参与讨论


总之。。。对对对,你们说的都对!


作者:Mark大熊
来源:juejin.cn/post/7280747572707999799
收起阅读 »

对于Android开发,Jetpack Compose真的要开始学起来了?

Jetpack Compose 是个啥?为啥要学它? 谷歌对 Jetpack Compose 的定义: Jetpack Compose 是推荐用于构建原生 Android 界面的新工具包。它可简化并加快 Android 上的界面开发,使用更少的代码、强大的工...
继续阅读 »

Jetpack Compose 是个啥?为啥要学它?


谷歌对 Jetpack Compose 的定义:



Jetpack Compose 是推荐用于构建原生 Android 界面的新工具包。它可简化并加快 Android 上的界面开发,使用更少的代码、强大的工具和直观的 Kotlin API,快速打造生动而精彩的应用。



提取关键词:界面开发新工具包、简化并加快界面开发、Kotlin API


对于大部分Android项目来说,如果基础库(如网络库、hybird、图片加载、热修复库等)已经搭好,那么平时大部分时间就是跟 UI界面、需求逻辑 打交道了,而谷歌提供的 Jetpack Compose 正好是加快界面开发的工具包
对比



就跟魂斗罗里的子弹类型似的,使用普通子弹(XML方式)也可以通关,但是相比之下耗时更长;而换成超级子弹(Jetpack Compose)体验就不一样了,耗时更少,而且游戏体验更爽!



命令式UI vs 声明式UI


长期以来,Android 视图层次结构一直可以表示为界面 widget 树。最常见的界面更新方式是使用 findViewById() 等函数遍历树,并通过调用 button.setText(String)、container.addChild(View) 或 img.setImageBitmap(Bitmap) 等方法更改节点。这些方法会改变 widget 的内部状态,这种手动更新UI的方式即是命令式UI


在过去的几年中,整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程任务。该技术的工作原理是在概念上从头开始重新生成整个屏幕,然后仅执行必要的更改。此方法可避免手动更新有状态视图层次结构的复杂性。Compose 即是一个声明式UI框架。


Jetpack Compose要学起来了?


很遗憾,Jetpack Compose 确实要学起来了(快起来,你还能学!哈哈...),随着Jetpack Compose 版本的不断迭代,API 逐渐稳定了,性能也越来越好了。


优点



  • 更少的代码:编写代码只需要采用Kotlin,而不用拆分成 Kotlin + XML方式了。

  • 直观:只需描述界面,Compose会负责处理剩余工作。应用状态变化时,界面自动更新。

  • 加速开发View 与 Compose 之间可以相互调用,兼容现有的所有代码。借助AS可以实时预览界面,轻松执行界面检查。

  • 功能强大:直接访问Android API,内置对Material Design、主题、动画等的支持。


Jetpack Compose vs Flutter




  • Jetpack Compose的目的是为了提高 Android 原生的 UI 开发效率!声明式UI已经成为主流的开发方式了,就像当初谷歌将Kotlin定为Android主流语言时我们学习Kotlin一样,未来Jetpack Compose 一定会是Android UI开发的主流方式。

  • Flutter 的定位是多平台 UI 框架,优势在于跨平台。



大家很喜欢把Jetpack Compose 和 Flutter作对比,不知道该学哪一个?的确,某些场景下它们确实挺像的,而且还都是谷歌在推的。


个人理解是:如果你未来的主攻方向还是Android,那么无脑选择Jetpack Compose,虽然Compose目前也能实现跨端,但跨端目前看并不是它的主要工作;而如果你的方向是多平台开发,那么学习Flutter是首选吧


另外,与其一直纠结学哪一个,不如直接上手亲身感受下它们的不同,正所谓 “纸上得来终觉浅,绝知此事要躬行”。


Jetpack Compose入门


class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text("Hello World!")
}
}
}

其中,setContent()传入一个@Composable作用域,其作用跟之前的setContentView()一样用来设置界面。Text()用来描述一个UI元素,里面有各种参数,这里我们只把文案填上去,执行结果:


hello world


一个最简单的功能就完成了。


1、@Composable 可组合函数


还是上述展示一个文本的功能,我们换一种写法:


class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MessageCard("Android")
}
}
}

@Composable
fun MessageCard(name: String) {
Text(text = "Hello $name!")
}

执行效果跟上面一样。唯一的区别就是把文本展示单独抽离到一个方法中了,并且该方法上面加了@Composable 注解



@Composable注解用于标记一个函数为可组合函数。可组合函数是一种特殊的函数,不需要返回任何UI元素,因为可组合函数描述的是所需的屏幕状态,而不是构造界面widget;而如果按我们以前的XML编程方式,必须在方法中返回UI元素才能使用它(如返回View类型)。



@Composable注解的函数之间可以相互调用,因为这样Compose框架才能正确处理依赖关系。另外,@Composable函数中也可以调用普通函数,而普通函数中却不能直接调用@Composable函数。 这里可以类比下kotlin中suspend挂起函数的用法,其用法是相似的


几个定义:



  • 组合:对 Jetpack Compose 在执行可组合项时所构建界面的描述。

  • 初始组合:通过首次运行可组合项创建组合。

  • 重组在数据发生变化时重新运行可组合项以更新组合


可组合函数的特点:



  • 使用同一参数多次调用此函数时,它的行为方式相同,并且它不使用其他值,如全局变量或对 random() 的调用。

  • 此函数描述界面而没有任何副作用,如修改属性或全局变量、点击事件的处理等。当需要执行附带效应时,应通过回调触发。如:


@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}

可见通过回调,点击事件这个附带效应是在调用方触发的。


在编译时,Jetpack Compose会将标记为@Composable的函数编译成字节码,并生成一个专门的ComposeNode类来管理其状态和属性。这个类会自动处理依赖关系,并在需要时计算UI元素。这样,开发者就可以专注于编写UI逻辑,而不用担心状态管理和UI更新的细节


这里引出一个问题,Compose 是如何做 UI 更新的呢?总不能每次有一小部分数据的变化,整个UI都要跟着刷新一次吧,那性能肯定差的要死。其实,当有数据变化时,Compose实现的是增量更新,只会重新绘制数据有改动的UI(该过程称为重组),数据没有改动的则不会重新绘制了


2、布局基础知识


布局
Compose 通过元素组合、布局、绘制之后可以将状态转换为UI元素。


组合

在 Compose 中,可以通过从可组合函数中调用其他可组合函数来构建界面层次结构。
基础布局
如图所示:



  • Column :可以将多个项垂直地放置在屏幕上;

  • Row :可以将多个项水平地放置在屏幕上;

  • Box :可将元素放在其他元素上,还支持为其包含的元素配置特定的对齐方式。


排列及对齐方式:


/**
* @param verticalArrangement 竖直排列方式
* @param horizontalAlignment 水平对齐方式
*/

inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
)
{...}

/**
* @param horizontalArrangement 水平排列方式
* @param verticalAlignment 竖直对齐方式
*/

@Composable
inline fun Row(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
content: @Composable RowScope.() -> Unit
)
{...}

/**
* @param contentAlignment 内容对齐方式
*/

@Composable
inline fun Box(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content: @Composable BoxScope.() -> Unit
)
{...}

verticalArrangement、horizontalArrangement 排列方式及效果:
排列方式


布局

界面树布局通过单次传递即可完成。父节点会在其子节点之前进行测量,但会在其子节点的尺寸和放置位置确定之后再对自身进行调整
Layout流程
当界面树较深时,Compose 可以通过只测量一次子项来实现高性能。


3、Modifier修饰符


可以通过Modifier修饰符更改可组合项的大小、布局、外观,还可以添加高级互动,例如使元素可点击等。如:


  Image(
painter = painterResource(id = R.mipmap.icon_water_melon),
contentDescription = "",
modifier = Modifier
.size(50.dp)
.clip(CircleShape)
.border(width = 2.dp, Color.Red, CircleShape)
)

执行结果:Modifier,原图本身是长方形的,通过Modifier修饰符修饰后,很容易变成圆角图片。想一下如果用XML方式来写,是不是要写好多代码呢。


4、存储状态


可组合函数中可以使用 remember 将本地状态存储在内存中,并跟踪传递给 mutableStateOf 的值的变化。该值更新时,系统会自动重新绘制使用此状态的可组合项(及其子项),这也是上面所说的重组。如:


@Composable
fun MessageCard(msg: Message) {
// We keep track if the message is expanded or not in this variable
var isExpanded by remember { mutableStateOf(false) }
// We toggle the isExpanded variable when we click on this Column
Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {...}
}

当点击Column 元素时,每次都会重新执行MessageCard()可组合函数进行刷新,而通过remember和mutableStateOf可以保存了上次的isExpanded状态;如果不使用它们,重新执行 MessageCard() 时 isExpanded 也会重新初始化。


除了remember之外,还有rememberSaveable、savedStateHandle.saveable等。。。


总结


这篇文章主要讲了Compose是什么以及我们要开始学习它的必要性。作为Compose 第一篇介绍文章,本文旨在初步感受一下 Compose的能力,后续再详细研究 Compose 的精彩用法!


资料


【1】谷歌Jetpack Compose 教程
https://developer.android.com/jetpack/compose/tutorial?hl=zh-cn


作者:_小马快跑_
来源:juejin.cn/post/7271832299340202036
收起阅读 »

面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:???

web
扯皮 这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。 因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大...
继续阅读 »

扯皮


这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。


因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大致过一遍,直到看见关于 Promise 的取消以及监听进度...🤔


只能说以后要是我当上面试官一定让候选人来谈谈这两个点,然后顺势安利我这篇文章🤣


不过好像目前为止也没见哪个面试官出过...


正文


取消功能


我们都知道 Promise 的状态是不可逆的,也就是说只能从 pending -> fulfilled 或 pending -> rejected,这一点是毋庸置疑的。


但现在可能会有这样的需求,在状态转换过程当中我们可能不再想让它进行下去了,也就是说让它永远停留至 pending 状态


奇怪了,想要一直停留在 pending,那我不调用 resolve 和 reject 不就行了🤔


 const p = new Promise((resolve, reject) => {
setTimeout(() => {
// handler data, no resolve and reject
}, 1000);
});
console.log(p); // Promise {<pending>} 💡

但注意我们的需求条件,是在状态转换过程中,也就是说必须有调用 resolve 和 reject,只不过中间可能由于某种条件,阻止了这两个调用。


其实这个场景和超时中断有点类似但还是不太一样,我们先利用 Promise.race 来看看:模拟一个发送请求,如果超时则提示超时错误:


const getData = () =>
new Promise((resolve) => {
setTimeout(() => {
console.log("发送网络请求获取数据"); // ❗
resolve("success get Data");
}, 2500);
});

const timer = () =>
new Promise((_, reject) => {
setTimeout(() => {
reject("timeout");
}, 2000);
});

const p = Promise.race([getData(), timer()])
.then((res) => {
console.log("获取数据:", res);
})
.catch((err) => {
console.log("超时: ", err);
});

问题是现在确实能够确认超时了,但 race 的本质是内部会遍历传入的 promise 数组对它们的结果进行判断,那好像并没有实现网络请求的中断哎🤔,即使超时网络请求还会发出:


超时中断.png


而我们想要实现的取消功能是希望不借助 race 等其他方法并且不发送请求。


比如让用户进行控制,一个按钮用来表示发送请求,一个按钮表示取消,来中断 promise 的流程:



当然这里我们不讨论关于请求的取消操作,重点在 Promise 上



取消请求.png


其实按照我们的理解只用 Promise 是不可能实现这样的效果的,因为从一开始接触 Promise 就知道一旦调用了 resolve/reject 就代表着要进行状态转换。不过 取消 这两个字相信一定不会陌生,clearTimeoutclearInterval 嘛。


OK,如果你想到了这一点这个功能就出来了,我们直接先来看红宝书上给出的答案:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="send">Send</button>
<button id="cancel">Cancel</button>

<script>
class CancelToken {
constructor(cancelFn) {
this.promise = new Promise((resolve, reject) => {
cancelFn(() => {
console.log("delay cancelled");
resolve();
});
});
}
}
const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

function cancellableDelayedResolve(delay) {
console.log("prepare send request");
return new Promise((resolve, reject) => {
const id = setTimeout(() => {
console.log("ajax get data");
resolve();
}, delay);

const cancelToken = new CancelToken((cancelCallback) =>
cancelButton.addEventListener("click", cancelCallback)
);
cancelToken.promise.then(() => clearTimeout(id));
});
}
sendButton.addEventListener("click", () => cancellableDelayedResolve(1000));
</script>
</body>
</html>

这段代码说实话是有一点绕的,而且个人觉得是有多余的地方,我们一点一点来看:


首先针对于 sendButton 的事件处理函数,这里传入了一个 delay,可以把它理解为取消功能期限,超过期限就要真的发送请求了。我们看该处理函数内部返回了一个 Promise,而 Promise 的 executor 中首先开启了定时器,并且实例化了一个 CancelToken,而在 CancelToken 中才给 cancelButton 添加点击事件。


这里的 CancelToken 就是我觉得最奇怪的地方,可能没有体会到这个封装的技巧,路过的大佬如果有理解的希望能帮忙解释一下。它的内部创建了一个 Promise,绕了一圈后相当于 cancelButton 的点击处理函数是调用这个 Promise 的 resolve,最终是在其 pending -> fuilfilled,即 then 方法里才去取消定时器,那为什么不直接在事件处理函数中取消呢?难道是为了不影响主执行栈的执行所以才将其推到微任务处理🤔?


介于自己没理解,我就按照自己的思路封装个不一样的🤣:


const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

class CancelPromise {

// delay: 取消功能期限 request:获取数据请求(必须返回 promise)
constructor(delay, request) {
this.req = request;
this.delay = delay;
this.timer = null;
}

delayResolve() {
return new Promise((resolve, reject) => {
console.log("prepare request");
this.timer = setTimeout(() => {
console.log("send request");
this.timer = null;
this.req().then(
(res) => resolve(res),
(err) => reject(err)
);
}, this.delay);
});
}

cancelResolve() {
console.log("cancel promise");
this.timer && clearTimeout(this.timer);
}
}

// 模拟网络请求
function getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("this is data");
}, 2000);
});
}

const cp = new CancelPromise(1000, getData);

sendButton.addEventListener("click", () =>
cp.delayResolve().then((res) => {
console.log("拿到数据:", res);
})
);
cancelButton.addEventListener("click", () => cp.cancelResolve());

正常发送请求获取数据:


发送请求.gif


中断 promise:


取消请求.gif


没啥大毛病捏~


进度通知功能


进度通知?那不就是类似发布订阅嘛?还真是,我们来看红宝书针对这块的描述:



执行中的 Promise 可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控 Promise 的执行进度会很有用



这个需求就比较明确了,我们直接来看红宝书的实现吧,核心思想就是扩展之前的 Promise,为其添加 notify 方法作为监听,并且在 executor 中增加额外的参数来让用户进行通知操作:


class TrackablePromise extends Promise {
constructor(executor) {
const notifyHandlers = [];
super((resolve, reject) => {
return executor(resolve, reject, (status) => {
notifyHandlers.map((handler) => handler(status));
});
});
this.notifyHandlers = notifyHandlers;
}
notify(notifyHandler) {
this.notifyHandlers.push(notifyHandler);
return this;
}
}
let p = new TrackablePromise((resolve, reject, notify) => {
function countdown(x) {
if (x > 0) {
notify(`${20 * x}% remaining`);
setTimeout(() => countdown(x - 1), 1000);
} else {
resolve();
}
}
countdown(5);
});

p.notify((x) => setTimeout(console.log, 0, "progress:", x));
p.then(() => setTimeout(console.log, 0, "completed"));


emm 就是这个例子总感觉不太好,为了演示这种效果还用了递归,大伙们觉得呢?


不好就自己再写一个🤣!不过这次的实现就没有多大问题了,基本功能都具备也没有什么阅读障碍,我们再添加一个稍微带点实际场景的例子吧:



// 模拟数据请求
function getData(timer, value) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(value);
}, timer);
});
}

let p = new TrackablePromise(async (resolve, reject, notify) => {
try {
const res1 = await getData1();
notify("已获取到一阶段数据");
const res2 = await getData2();
notify("已获取到二阶段数据");
const res3 = await getData3();
notify("已获取到三阶段数据");
resolve([res1, res2, res3]);
} catch (error) {
notify("出错!");
reject(error);
}
});

p.notify((x) => console.log(x));
p.then((res) => console.log("Get All Data:", res));


notify获取数据.gif


对味儿了~😀


End


关于取消功能在红宝书上 TC39 委员会也曾准备增加这个特性,但相关提案最终被撤回了。结果 ES6 Promise 被认为是“激进的”:只要 Promise 的逻辑开始执行,就没有办法阻止它执行到完成。


实际上我们学了这么久的 Promise 也默认了这一点,因此这个取消功能反而就不太符合常理,而且十分鸡肋。比如说我们有使用 then 回调接收数据,但因为你点击了取消按钮造成 then 回调不执行,我们知道 Promise 支持链式调用,那如果还有后续操作都将会被中断,这种中断行为 debug 时也十分痛苦,更何况最麻烦的一点是你还需要传入一个 delay 来表示取消的期限,而这个期限到底要设置多少才合适呢...


至于说进度通知功能,仁者见仁智者见智吧...


但不管怎么样两个功能实现的思路都是比较有趣的,而且不太常见,不考虑实用性确实能够成为一道考题,只能说很符合面试官的口味😏


作者:討厭吃香菜
来源:juejin.cn/post/7312349904046735400
收起阅读 »

用脚本来写函数式弹窗,更快更爽

web
前言 在业务开发中,弹窗是我们常常能够遇到的开发需求,通常我们使用组件都是通过show变量来控制弹窗的开启或者隐藏。这样面对简单的业务需求的确可以满足了,但是面对深层嵌套,不同地方打开同一个弹窗便会让我们头疼。于是我想通过函数的方式来打开弹窗,通过函数参数的方...
继续阅读 »

前言


在业务开发中,弹窗是我们常常能够遇到的开发需求,通常我们使用组件都是通过show变量来控制弹窗的开启或者隐藏。这样面对简单的业务需求的确可以满足了,但是面对深层嵌套,不同地方打开同一个弹窗便会让我们头疼。于是我想通过函数的方式来打开弹窗,通过函数参数的方式来向弹窗内更新props和处理弹窗的emit事件。


iShot_2023-08-15_10.13.24.gif


<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


传统vue编写弹窗


通过变量来直接控制弹窗的开启和关闭。


<template>
<n-button @click="showModal = true">
来吧
</n-button>

<n-modal v-model:show="showModal" preset="dialog" title="Dialog">
<template #header>
<div>标题</div>
</template>
<div>内容</div>
<template #action>
<div>操作</div>
</template>
</n-modal>

</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
setup () {
return {
showModal: ref(false)
}
}
})
</script>


痛点



  • 深层次的传props让人有很大的心理负担,污染组件props

  • 要关注弹窗show变量的true,false


函数式弹窗


在主页面用Provider包裹一下


// RootPage.vue
<ModalProvider>
<ChildPage></ChildPage>
</ModalProvider>

<script setup lang="ts">
import ModalProvider from "./ModalProvider.vue"
import ChildPage from "./ChildPage.vue"
</script>


在页面内的某个子组件中,直接通过oepn方法打开弹窗


// ChidPage.vue
<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


优势



  • 对于使用者来说简单,没有控制show的心理负担

  • 弹窗内容和其他业务代码分离,不会污染其他组件props和结构

  • 能够充分复用同一个弹窗,通过函数参数的方式来更新弹窗内容,在不同地方打开同一个弹窗


劣势



  • 对于一些简单需求的弹窗,用这种函数式的弹窗会有些臃肿

  • 在使用函数式弹窗前需要做一些操作(Provider和Inject),会在后面提到以及解决方案。


如何使用这种函数式的弹窗


原理


通过在根页面将我们的Modal组件挂载上去,然后通过使用hook去管理这个modal组件的状态值(props,ref),然后通过provide将props和event和我们的modal组件联系起来,再在我们需要使用弹窗的地方使用inject更新props来启动弹窗。


步骤1(❌):编写Modal


这里我使用的是Naive Ui的Modal组件,按喜好选择就行。按照你的需求编写弹窗的内容。定义好props和emits,写好他们的类型申明。


// TestModal.vue
<template>
<n-modal
v-model:show="isShowModal"
preset="dialog"
@after-leave="handleClose"
>

...你的弹窗内容
</n-modal>

</template>

<script setup lang="ts">
import { ref, reactive, computed, watch } from "vue"
import { useVModel } from "@vueuse/core"
import { FormRules } from "naive-ui"
interface ModalProps {
show: boolean
testValue: string
}

// 中间区域不要修改
const props = defineProps<ModalProps>()
const formRef = ref()
const loading = ref(false)
const isShowModal = useVModel(props, "show")
// 中间区域不要修改

const rules: FormRules = []

const formData = reactive({
testValue: props.testValue,
})

const callBackData = computed(() => {
return {
formData,
}
})

watch(
() => props.show,
() => {
if (props.show) {
formData.testValue = props.testValue
} else {
formData.testValue = ""
}
}
)

const emits = defineEmits<{
(e: "update:show", value: boolean): void
(e: "close", param: typeof callBackData.value): void
(
e: "confirm",
param: typeof callBackData.value,
close: () => void,
endLoading: () => void
): void
(e: "cancel", param: typeof callBackData.value): void
}>()
function handleCancel() {
// 中间区域不要修改
emits("cancel", callBackData.value)
isShowModal.value = false
// 中间区域不要修改
}

function handleClose() {
// 中间区域不要修改
emits("close", callBackData.value)
isShowModal.value = false
// 中间区域不要修改
}

function handleConfirm() {
// 中间区域不要修改
loading.value = true
emits(
"confirm",
callBackData.value,
() => {
loading.value = false
isShowModal.value = false
},
() => {
loading.value = false
}
)
// 中间区域不要修改
}
</script>

步骤2(❌):编写hook来管理弹窗的状态


在这个文件里面,使用hook管理 TestModal 弹窗需要的props和event事件,然后我们通过ts类型体操来获取TestModal的props类型和event类型,我们向外inject一个 open 函数,这个函数可以更新 TestModal 的props和event,同时打开弹窗,他的参数有完整的类型提示,可以让使用者更加明确的使用我们的弹窗。


// use-test-modal.ts
import {
ref,
provide,
InjectionKey,
inject,
VNodeProps,
AllowedComponentProps,
reactive,
} from "vue";
import Modal from "./TestModal.vue";

/**
* 通过引入弹窗组件来获取组件的除show,update:show以外的props和emits来作为open函数的
*/

type ModalInstance = InstanceType<
typeof Modal extends abstract new (...args: any) => any ? typeof Modal : any
>["$props"];
type OpenParam = Omit<
{
readonly [K in keyof Omit<
ModalInstance,
keyof VNodeProps | keyof AllowedComponentProps
>]: ModalInstance[K];
},
"show" | "onUpdate:show"
>;

interface AnyFileChangeModal {
open: (param?: OpenParam) => Promise<void>;
}

/**
* 通过弹窗实例来获取弹窗组件内需要哪些props
*/

type AllProps = Omit<
OpenParam,
"onClose" | "onCancel" | "onConfirm" | "onUpdate:show"
> & { show: boolean };
const anyModalKey: InjectionKey<AnyFileChangeModal> = Symbol("ModalKey");
export function provideTestModal() {
const allProps: AllProps = reactive({
show: false,
} as AllProps);
const closeCallback = ref();
const cancelCallback = ref();
const confirmCallback = ref();
const handleUpdateShow = (value: boolean) => {
allProps.show = value;
};

/**
* @param param 通过函数来更新props
*/

function updateAllProps(param: OpenParam) {
const excludeKey = ["show", "onClose", "onConfirm", "onCancel"];
for (const [key, value] of Object.entries(param)) {
if (!excludeKey.includes(key)) {
allProps[key] = value;
}
}
}
function clearAllProps() {
for (const [key] of Object.entries(allProps)) {
allProps[key] = undefined;
}
}

async function open(param: OpenParam) {
clearAllProps();
updateAllProps(param);
allProps.show = true;
param.onClose && (closeCallback.value = param.onClose);
param.onConfirm && (confirmCallback.value = param.onConfirm);
param.onCancel && (cancelCallback.value = param.onCancel);
}
provide(anyModalKey, { open });
return {
allProps,
closeCallback,
confirmCallback,
cancelCallback,
handleUpdateShow,
};
}

export function injectTestModal() {
return inject(anyModalKey)
}
Ï

步骤3(❌):提供Provider


在这个文件里,我将TestModal放在了根页面,然后将hook返回的props和event绑定给TestModal


// ModalProvider.vue
<template>
<slot />

<TestModal
v-bind="allTestModalProps"
@update:show="handleTestModalUpdateShow"
@close="closeTestModalCallback"
@confirm="confirmTestModalCallback"
@cancel="cancelTestModalCallback"
/>

<!-- 新增Modal -->
</template>

<script setup lang="ts">
import TestModal from "./test-modal/TestModal.vue";
import { provideTestModal } from "./test-modal/use-test-modal";
/** 新增import */

const {
allProps: allTestModalProps,
handleUpdateShow: handleTestModalUpdateShow,
closeCallback: closeTestModalCallback,
confirmCallback: confirmTestModalCallback,
cancelCallback: cancelTestModalCallback,
} = provideTestModal();
/** 新增provide */
</script>


步骤4(❌):通过函数打开弹窗


<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


到这里也就结束了。如果这样写一个弹窗,确实可以达到函数式的打开弹窗,但是他实在是太繁琐了,要写这么一堆东西,不如直接修改弹窗的show来的快。如果看过上面的use-test-modal.ts的话,会发现里面有AllProps,这是为了减少方便使用脚本工具来写通用化的代码,可以大大减少人工的重复代码编写,接下来我们使用一个工具来减少这些繁琐的操作。


步骤1(✅):初始化Provider


通过使用工具生成根页面ModalProvder组件。
具体步骤:在你想放置当前业务版本的弹窗文件夹路径下使用终端执行脚本,选择initProvider


iShot_2023-08-15_15.16.14.gif


步骤2(✅):生成弹窗模板


通过继续使用脚本工具,生成弹窗组件以及hook文件。
具体步骤:在你想放该弹窗的文件夹路径下使用终端,使用脚本工具,选择genModal,然后跟着指令操作就行。比如例子中,我们生产名为test的弹窗,然后告诉脚本我们的ModalProvider组件的绝对路径,脚本帮我生产test-modal的文件夹,里面放着testModal.vueuse-test-modal.ts,这里的use-test-modal.ts文件已经是成品了,不需要你去修改,ModalProvider.vue也不需要你去修改,里面的路径关系也帮你处理好了。


iShot_2023-08-15_15.17.33.gif


步骤3(✅):修改弹窗内容


上一步操作中,脚本帮我写好了TestModal.vue组件,我们可以在他的基础上完善我们的业务需求。


步骤4(✅):调用弹窗


我们找到生成的use-test-modal.ts,里面有一个injectXXXModal的方法,我们在哪个地方用到弹窗就引用执行他,返回的对象中有open方法,通过open方法去开启弹窗并且更新弹窗props。


Demo


预览
Demo地址
里面有完整的demo代码


iShot_2023-08-15_18.32.39.gif


脚本工具


仓库地址
这个是我写的脚本工具,用来减少一些重复代码的工作。用法可看readme


总结


本文先通过比较函数式的弹窗和直接传统编写弹窗的优劣,然后引出本文的主旨(如何编写函数式弹窗),再考虑我这一版函数式弹窗的问题,最后通过脚本工具来解决这一版函数式弹窗的劣势,完整的配合脚本工具,可以极大的加快我们的工作效率。
大家不要看这么写要好几个文件夹,实际使用的时候其实就是脚本生成代码后,我们只需要去修改弹窗主题的内容。有什么疑惑或者建议评论区聊。


作者:恐怖屋
来源:juejin.cn/post/7267418473401057321
收起阅读 »

离职后,前领导突然找你回去帮忙写代码解决问题,该怎么办?

题目中的这个问题,我相信有遇到过这种情况的同学的第一反应是:"诶,是要白嫖我还是说解决完问题给钱呀",且听我接下来慢慢分析。 首先要说的是,这种没头没尾的突发情况,一般大部分人都是很难遇到的。 原因也很简单,老板大部分也都是打过工,当过员工的,也是一路从职场老...
继续阅读 »

题目中的这个问题,我相信有遇到过这种情况的同学的第一反应是:"诶,是要白嫖我还是说解决完问题给钱呀",且听我接下来慢慢分析。


首先要说的是,这种没头没尾的突发情况,一般大部分人都是很难遇到的。


原因也很简单,老板大部分也都是打过工,当过员工的,也是一路从职场老油子混成的老板,很多人情世故,员工的小心思,老板其实都门儿清,甚至比很多员工都更熟。


如果公司里的一些工作是交接时不太能完全搞定的,可能还需要离职的员工继续帮忙的,一般在员工离职前的时候,就各种协商好了。


而像这种“突发情况”,大部分老板在联系离职的员工回去帮忙前,一般也都会把员工会想到的那些事儿,早就想了很多遍了,基本上相关问题都会在联系员工的时候说明白。


比如很多人都提到的报酬问题,这个基本上都是作为老板不可能回避,也不可能不知道的。


如果老板在联系员工的时候什么都提了,就是没聊这个。


那肯定是老板不想给报酬,还在做着让员工回来白干活的美梦。


不可否认,现实中确实有挺多这样的老板


所以,我的经验就是,如果老板在主动联系离职员工回来帮忙的时候,都没提报酬的事儿,那基本上就是不打算给,基本上你问了也是白问。


当然,大部分人都会遇到的情况是,本来跟老板领导关系也不错,老板领导也知道这一点,所以才会跟已经离职的员工开这个口。


这种时候,大部分人看在老板领导人还不错的份儿上,还是愿意回去帮忙的。


至于会和现在的工作造成的一些冲突,比如时间上走不开,现在住得离公司远,这些也都是可以直接明说的事儿,说了后,要么老板可以帮你解决,要么老板心里会知道你回来帮这次忙的成本有多高。


我以前工作过的公司,别说离职走的同事了,有一次是碰到了一个实习生经手的项目,上面很多东西没按照公司规范写,后来看到这些资料的员工整不明白是怎么回事。


但是,部门领导在知道了这件事后,在知道了这个实习生的同学就在本部门工作的情况下,并没有说让这位同学去搞定这个问题。


而是让这位同学联系好那位实习生后,领导亲自开车带着这位同学和要用到这个资料的人,专门在下班时间守在这位实习生的工作单位门口,接着他去一家还不错的餐厅,边吃饭边解决了这个问题。


至于很多人提的,跟老板没啥交情,甚至关系还不怎么好的,那还纠结什么,直接不理或拒绝就行了,但也没必要把话说得太绝。


毕竟,如果老板真的意识到你这边不好搞,同时也只有找你来帮忙是最划算的选择后,一般都会开出更高的加码,如果加码合适,你还是可以考虑一下的。


但是一定要就是论事,划定要解决问题的范围,要不然赖上你了有问题就找你可还行,同时也要注意不要留下太多痕迹。比如你回来帮忙,是不是属于违规行为,再比如请你回来帮忙的时候,装作无意间打听你现在公司的一些事儿,这个事儿很可能属于工作机密,毕竟大家都是同行,这些一定要注意。


综上我觉得,解决这个问题的公式是:上来先拖字诀、加各种不容易各种不行,这种能挡掉99%的需求,毕竟这么大个公司离了我这个小兵还不能转了咋地;实在不行了在谈什么样的条件你才能去帮忙解决问题,而且记住是单次解决问题的条件。


作者:kevinyan
来源:juejin.cn/post/7322344486159826996
收起阅读 »

环信服务端下载消息文件---菜鸟教程

前言在服务端,下载消息文件是一个重要的功能。它允许您从服务器端获取并保存聊天消息、文件等数据,以便在本地进行进一步的处理和分析。本指南将指导您完成环信服务端下载消息文件的步骤。环信服务端下载消息文件是指在环信服务端上,通过调用相应的API接口,从服务器端下载聊...
继续阅读 »

前言

在服务端,下载消息文件是一个重要的功能。它允许您从服务器端获取并保存聊天消息、文件等数据,以便在本地进行进一步的处理和分析。本指南将指导您完成环信服务端下载消息文件的步骤。
环信服务端下载消息文件是指在环信服务端上,通过调用相应的API接口,从服务器端下载聊天消息、文件等数据的过程。因环信服务端保存的消息漫游是有时间限制,有用户需要漫游全部的消息或者自己服务端做所有消息记录的备份。可以从环信服务端下载消息文件来进行解压,读取消息文件内容进行存储到自己的服务端。

前提条件

一、下载消息文件

以下将介绍如何通过环信接口获取到的URL来进行下载文件,解压文件,读取文件。
注:
time参数: 历史消息记录查询的起始时间。UTC 时间,使用 ISO8601 标准,格式为 yyyyMMddHH。例如 time 为 2018112717,则表示查询 2018 年 11 月 27 日 17 时至 2018 年 11 月 27 日 18 时期间的历史消息。若海外集群为 UTC 时区,需要根据自己所在的时区进行时间转换。

上图是环信官方文档中给出的获取历史消息记录响应示例。从示例中可以看出我们请求以后可以得到一个URL,这个URL为消息文件的下载URL。

1、下载消息文件环信rest 接口请求代码如下:

String url = "https://{{RestApi}}/{{org_name}}/{{app_name}}/chatmessages/2023122010";
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type","application/json");
headers.add("Authorization","Bearer Authorization");
Map<String, String> body = new HashMap<>();
HttpEntity<Map<String, String>> entity = new HttpEntity<>(body, headers);
ResponseEntity<Map> response;
try {
response = restTemplate.exchange(url, HttpMethod.GET, entity, Map.class);

System.out.print("消息文件下载成功---"+response.toString());
} catch (Exception e) {
System.out.print("消息文件下载失败---"+e.toString());
}

2、消息文件下载,通过请求环信下载历史消息文件接口获取到的URL 进行下载。

示例代码:

String url = "";
String targetUrl = "";
download(url,targetUrl);
/**
* 根据url下载文件,保存到filepath中
*
* @param url 文件的url
* @param diskUrl 本地存储路径
* @return
*/

public static String download(String url, String diskUrl) {
String filepath = "";
String filename = "";
try {
HttpClient client = HttpClients.createDefault();
HttpGet httpget = new HttpGet(url);
// 加入Referer,防止防盗链 httpget.setHeader("Referer", url);
HttpResponse response = client.execute(httpget);
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();
if (StringUtils.isBlank(filepath)){
Map<String,String> map = getFilePath(response,url,diskUrl);
filepath = map.get("filepath");
filename = map.get("filename");
}
File file = new File(filepath);
file.getParentFile().mkdirs();
FileOutputStream fileout = new FileOutputStream(file);
byte[] buffer = new byte[cache];
int ch = 0;
while ((ch = is.read(buffer)) != -1) {
fileout.write(buffer, 0, ch);
}
is.close();
fileout.flush();
fileout.close();

} catch (Exception e) {
e.printStackTrace();
}
return filename;
}


/**
* 获取response要下载的文件的默认路径
*
* @param response
* @return
*/

public static Map<String,String> getFilePath(HttpResponse response, String url, String diskUrl) {
Map<String,String> map = new HashMap<>();
String filepath = diskUrl;
String filename = getFileName(response, url);
String contentType = response.getEntity().getContentType().getValue();
if(StringUtils.isNotEmpty(contentType)){
// 获取后缀 String regEx = ".+(.+)$";
Pattern p = Pattern.compile(regEx);
Matcher m = p.matcher(filename);
if (!m.find()) {
// 如果正则匹配后没有后缀,则需要通过response中的ContentType的值进行匹配 filename = filename +".gz";

}else{
if(filename.length()>20){
filename = getRandomFileName() + ".gz";
}
}
}
if (filename != null) {
filepath += filename;
} else {
filepath += getRandomFileName();
}
map.put("filename", filename);
map.put("filepath", filepath);
return map;
}



/**
* 获取response header中Content-Disposition中的filename值
* @param response
* @param url
* @return
*/

public static String getFileName(HttpResponse response,String url) {
Header contentHeader = response.getFirstHeader("Content-Disposition");
String filename = null;
if (contentHeader != null) {
// 如果contentHeader存在 HeaderElement[] values = contentHeader.getElements();
if (values.length == 1) {
NameValuePair param = values[0].getParameterByName("filename");
if (param != null) {
try {
filename = param.getValue();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}else{
// 正则匹配后缀 filename = getSuffix(url);
}

return filename;
}

/**
* 获取随机文件名
*
* @return
*/

public static String getRandomFileName() {
return String.valueOf(System.currentTimeMillis());
}

/**
* 获取文件名后缀
* @param url
* @return
*/

public static String getSuffix(String url) {
// 正则表达式“.+/(.+)$”的含义就是:被匹配的字符串以任意字符序列开始,后边紧跟着字符“/”, // 最后以任意字符序列结尾,“()”代表分组操作,这里就是把文件名做为分组,匹配完毕我们就可以通过Matcher // 类的group方法取到我们所定义的分组了。需要注意的这里的分组的索引值是从1开始的,所以取第一个分组的方法是m.group(1)而不是m.group(0)。 String regEx = ".+/(.+)$";
Pattern p = Pattern.compile(regEx);
Matcher m = p.matcher(url);
if (!m.find()) {
// 格式错误,则随机生成个文件名 return String.valueOf(System.currentTimeMillis());
}
return m.group(1);

}
  • url为第一步中从环信下载历史消息文件接口中请求返回的url(消息文件下载地址)
  • targetUrl 为下载的本地存储路径

下载以后从对应的路径下就可以看到所下载的文件。

3、消息文件解压,下载完的文件是以.gz结尾的压缩文件,需要对压缩文件进行解压

 public static void unGzipFile(String gzFilePath,String directoryPath) {
String ouputfile = "";
try {
//建立gzip压缩文件输入流 FileInputStream fin = new FileInputStream(gzFilePath);
//建立gzip解压工作流 GZIPInputStream gzin = new GZIPInputStream(fin);
//建立解压文件输出流// ouputfile = sourcedir.substring(0,sourcedir.lastIndexOf('.'));// ouputfile = ouputfile.substring(0,ouputfile.lastIndexOf('.')); FileOutputStream fout = new FileOutputStream(directoryPath);
int num;
byte[] buf=new byte[1024];
while ((num = gzin.read(buf,0,buf.length)) != -1) {
fout.write(buf,0,num);
}
gzin.close();
fout.close();
fin.close();
} catch (Exception ex){
System.err.println(ex.toString());
}
return;
}

gzFilePath:压缩文件路径
directoryPath:加压到的文件目录路径
解压后的文件如下图所示:

4、文件读取,将解压后的文件读取出来

FileInputStream inputStream = null;
try {
inputStream = new FileInputStream("/Users/liupeng/Downloads/download/1234567890");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String str = null;
long i = 0;
while(true){
try {
if (!((str = bufferedReader.readLine()) != null)) break;
} catch (IOException e) {
e.printStackTrace();
}
JSONObject jo = JSONObject.parseObject(str);
System.out.println("==========================================" + i);
System.out.println("消息id:" + jo.get("msg_id"));
System.out.println("发送id:" + jo.get("from"));
System.out.println("接收id:" + jo.get("to"));
System.out.println("服务器时间戳:" + jo.get("timestamp"));
System.out.println("会话类型:" + jo.get("chat_type"));
System.out.println("消息扩展:" + jo.getJSONObject("payload").get("ext"));
System.out.println("消息体:" + jo.getJSONObject("payload").getJSONArray("bodies").get(0));
i ++;
if (i > 100) break;
}
//close try {
inputStream.close();
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}

} catch (FileNotFoundException e) {
e.printStackTrace();
}

解析完以后日志打印如下:

至此,解析完以后可以将解析的数据进行存储。

相关文档:

注册环信即时通讯IM:https://console.easemob.com/user/register

IMGeek社区支持:https://www.imgeek.net/

收起阅读 »

漫画:成年人的社交潜台词

原则上可以=不可以原则上不可以=可以再说吧=没戏……花:学到了,看我活学活用!——————————————————————————————甜狗:hi花:?甜狗:最近过的怎么样花:还好吧甜狗:一起出去玩呀花:有空一定去甜狗:改天请你吃饭花:我比较相信缘分甜狗:我...
继续阅读 »




原则上可以=不可以
原则上不可以=可以
再说吧=没戏……


花:学到了,看我活学活用!
——————————————————————————————
甜狗:hi
花:?
甜狗:最近过的怎么样
花:还好吧
甜狗:一起出去玩呀
花:有空一定去
甜狗:改天请你吃饭
花:我比较相信缘分
甜狗:我们分手吧
花:我在考虑考虑
甜狗:考虑啥?你还爱我吗
花:哎呀、我不是这个意思
甜狗:晚安
——————————————————————————————
花:(朋友圈)最后还是自己默默承受

作者:灼见
来源:mp.weixin.qq.com/s/nvXTNj-GwNDW4zsvbBNTng

收起阅读 »

未来三年,请主动给生活降级

任正非曾在华为内部论坛发言时说:接下来3年,华为要将“活下去”作为主要纲领。企业寒意阵阵,其中的个人也在顶着寒气的侵袭,努力与生活周旋。未来的几年,普通人最好的应对方法,是主动给自己减负,给生活降级。01消费降级前阵子,话题#一件事说明你消费降级#登上微博热搜...
继续阅读 »

任正非曾在华为内部论坛发言时说:接下来3年,华为要将“活下去”作为主要纲领。


企业寒意阵阵,其中的个人也在顶着寒气的侵袭,努力与生活周旋。


未来的几年,普通人最好的应对方法,是主动给自己减负,给生活降级。


01

消费降级


前阵子,话题#一件事说明你消费降级#登上微博热搜。


以前人们把炫富当潮流,现在流行的是各种花式“抠门”。


深夜蹲在便利店等一份三折便当,作为第二天的早餐;


自己在家理发,全身上下没有超过100块的衣服;


洗面奶牙膏挤不动了,用剪刀剪开,接着再用个三四次……


谁也不想抠抠搜搜过日子,但感受过失业危机,承受过意外侵袭的我们,开始活得无比清醒。


相比起买买买的畅快,握在手里的存款,才是我们最大的安全感。


未来几年,各种考验依然在等着我们。


狠狠省钱,努力存钱,就是在不确定的未来,给自己攒一份确定性。


《极简生活》一书提供了一个购物标准,买东西前问自己3个问题:


我是否真的需要它?我使用它的频率是多少?我现有的物品中,是否有东西可以替代它?


想清楚这几个问题,就能帮你省下许多不必要的开支。


畅销书作家哈维·艾克说:你管理金钱的习惯,比你拥有的钱财数目更重要。


管理金钱的第一步,就是要养成记账的习惯。


你可以下载专门的记账APP,来记录你一天的开支。


然后每周或者每月进行复盘,看看什么地方该花,什么地方不该花。


当你清楚知道每一分钱的去处,自然能堵住出口,守住自己的钱包。


巴尔扎克说过:


对于浪费的人,金钱是圆的,可是对于节俭的人,金钱是扁的,是可以一块块堆积起来的。


学会省钱,你省的是风险;学会存钱,你存的是保障。


推荐几个我亲测有效的存钱方法:


百分比存钱法:每月把收入的10%存起来,强制储蓄,雷打不动;


365存钱法:画一个表格,每天挑1-365中的一个数字来存钱,一年后,能轻轻松松攒下66795元;



每周累计存钱法:一年52周,第一周存10块,第二周存20……


以此类推,一年下来,也能存住一万多。


消费有度,存钱上瘾,晴备雨伞,饱存饥粮,才是未来3年最好的金钱观。


02

投资降级


最近有个网络热词“中产不要命三件套”:投资商铺、辞职创业、全职炒股。


许多人对经济形势的判断太过乐观,盲目投资,最终连原本安稳的生活也赔了进去。


说两个我朋友的故事。


一位朋友是深圳一家贸易公司的高管,年薪百万。


妻子在家做全职太太,两人育有一双儿女。


去年,考虑到老大要上学了,他们想把手里的小三房卖了,置换一套好点的学区房。


可房子迟迟卖不出去,夫妻俩着急孩子上学的事,就和朋友借了150万,又贷款200万买下了学区房。


不曾想他们刚买完房子,朋友就被裁员了,此后投了上百份简历都杳无音讯。


可他还有一大家子要养活,每月还有近2万的房贷要还,离职的赔偿也很快被花光。


曾经的中产精英,只得选择断供,四处借钱度日。


另外一个朋友,是大型银行的技术专员,每月一万多的工资。


但某天他听一个朋友建议,投资了一个连锁餐饮项目。


他花光自己40多万的积蓄,还向3家银行借了贷。


但后来疫情来袭,朋友的投资也打了水漂,亏光所有本金不说,还欠了银行一屁股债。


作家连岳说:


投资的标准是,你要有本事先安置好家人的生活,此后还有闲钱,才能用来投资;


投资失败后,还要保证家人丰衣足食,不能遵循这个标准,就会被投资害死。


未来几年,各种不确定性依然存在,我建议你:


1. 清空负债,减少信用卡的使用频率;


2. 不要盲目投资,尽量选择稳健的投资策略;


3. 做任何事情都不要全押,卡上至少要有家庭储蓄一两年的生活资金。


03

就业降级


《凉子访谈录》中有位35岁的受访者,被大公司裁员后,收到一家资历尚浅的公司offer。


他直言自己还有一些行业自尊心,还是想去更大的平台,就拒绝了。


他认为凭自己的资历,找个跟之前差不多的公司不在话下,可现实却是他投的简历回应者寥寥无几。


大家应该都感觉到了,这几年工作越来越难找。


台湾劳动部《劳工失业后再就业情形》就有调查数据显示:


45岁以上职场人一旦失业,想要找到新工作平均得花6个月,还有33.6%的人找不到新工作或放弃不找了。


我身边有失业的朋友,找工作时也是一再降薪,不求岗位对口,只求尽快入职,因为房贷不等人。


未来3年,就业形势会更加残酷,你需要遵循以下三个法则。


1. 先活下去再说


一朝失业,才懂什么叫焦头烂额。


没有收入的日子,车贷房贷、孩子的教育支出、日常生活开支,样样都成了难题。


诚如俞敏洪所讲:当一个人面临生存问题,先活下去再说。


“只要这份工作不玷污你的人格,你再劳累再不喜欢,只要可以给你带来一份收入,你可以先做。”


世道艰难,暂时苟着,并不丢人。


2. 珍惜现在的单位


去年1月,Google突然宣布裁员12000人,紧接着,IBM加入了裁员大军,裁员3900人,3月底,微软也宣布裁员1万名员工……


在这个瞬息万变的时代,如果你还有班可上,其实就已经跑赢了大多数人。


所以,要珍惜现有的工作,善待你所在的单位。


一份按时到来的工资,能让你维持生计,一份不错的工作,可以为你遮风挡雨。


寒意尚未褪去之际,和现有单位一起抗住压力,和同事抱团取暖,才不至于被冻僵。


3. 保持归零心态


在找工作的心态上,你要抛弃走到今天为止你所有的成就、地位和光环。


大家总有“35岁焦虑”,是因为大家总认为自己应该越挣越多。


但是,年龄与成就并非线性关系。


前面跑得快,后面跑得慢,你会被后面的人超越,这是必然会发生的。


未来3年,什么样的人会活得很好?


热爱变化,主动拥抱不确定性,勇于走出舒适圈的人。


愿你在反复归零的状态下,依然能充满勇气,义无反顾迎接下一个变化。


04

社交降级


英国作家普利斯特利说:


社交性聚会就是去不去你都会感到后悔的一种活动,不去也没人注意你的缺席,去了就是参加一种虚情假意的游戏。


一场疫情,更是让许多人在自我隔离中,逐渐发现了社交非必要性。


事实上,过度的社交不仅无法排解情感上的孤独,无法带来所谓的人脉,反而是一种自我消耗。


以后,请给自己的社交降级。


1. 不去无意义的饭局


当我们参加热热闹闹的饭局,以为在吃喝玩乐中就把人脉搞定了。


却不明白,酒桌上的交情无法延伸到酒桌外。


热衷于这种聚会,浪费精力不说,还会有一种“朋友遍天下”的错觉。


等到现实的一个浪头打过来,你才会明白,逢场作戏的友谊,根本不堪一击。


2. 走出虚假的名利场


苏芒在《芭莎》杂志任主编时,每次与一众名媛合照都站在C位。


可当她在时尚圈地位不保之后,别人发合照都会把她裁掉。


经历过动荡起伏的人,更能懂什么叫人走茶凉。


成年人的世界向来现实,自己没有价值,所有的社交都是浮云。


与其在名利场上费心攀关系,不如好好修炼自己的本事。


3. 远离低层次圈子


周国平曾讲:


“为了尽兴而聚在一起的人,要么债台高筑,要么百病缠身,最终往往不能尽兴;


反倒是那些聚在一起吃苦的人,身体和心灵都得到锻炼,最终过得幸福圆满。”


低层次的圈子,会不断消耗你、腐蚀你,直到你沉沦其中;


融入更优秀的群体,才是成长的最佳路径。



诗人里尔克说:哪有什么胜利可言,挺住意味着一切。


生活的海域从不平静,你要以稳健的姿态迎接风浪,挺过风浪。


未来3年,请捂紧钱包,低配欲望。


请相信,你对生活的每一次低头,都是为了以后更好地昂首。


你当下的每一份积累,终将换来命运的厚待。


作者:每晚安娜贝苏
来源:每晚一卷书(JYXZ89896)

收起阅读 »

Java切换到Kotlin,Crash率上升了?

前言 最近对一个Java写的老项目进行了部分重构,测试过程中波澜不惊,顺利上线后几天通过APM平台查看发现Crash率上升了,查看堆栈定位到NPE类型的Crash,大部分发生在Java调用Kotlin的函数里,本篇将会分析具体的场景以及规避方式。 通过本篇文章...
继续阅读 »

前言


最近对一个Java写的老项目进行了部分重构,测试过程中波澜不惊,顺利上线后几天通过APM平台查看发现Crash率上升了,查看堆栈定位到NPE类型的Crash,大部分发生在Java调用Kotlin的函数里,本篇将会分析具体的场景以及规避方式。

通过本篇文章,你将了解到:




  1. NPE(空指针 NullPointerException)的本质

  2. Java 如何预防NPE?

  3. Kotlin NPE检测

  4. Java/Kotlin 混合调用

  5. 常见的Java/Kotlin互调场景



1. NPE(空指针 NullPointerException)的本质


变量的本质


    val name: String = "fish"

name是什么?

对此问题你可能嗤之以鼻:



不就是变量吗?更进一步说如果是在对象里声明,那就是成员变量(属性),如果在方法(函数)里声明,那就是局部变量,如果是静态声明的,那就是全局变量。



回答没问题很稳当。

那再问为什么通过变量就能找到对应的值呢?



答案:变量就是地址,通过该地址即可寻址到内存里真正的值



无法访问的地址



在这里插入图片描述


如上图,若是name="fish",表示的是name所指向的内存地址里存放着"fish"的字符串。

若是name=null,则说明name没有指向任何地址,当然无法通过它访问任何有用的信息了。


无论C/C++亦或是Java/Kotlin,如果一个引用=null,那么这个引用将毫无意义,无法通过它访问任何内存信息,因此这些语言在设计的过程中会将通过null访问变量/方法的行为都会显式(抛出异常)提醒开发者。


2. Java 如何预防NPE?


运行时规避


先看Demo:


public class TestJava {
public static void main(String args[]) {
(new TestJava()).test();
}

void test() {
String str = getString();
System.out.println(str.length());
}

String getString() {
return null;
}
}

执行上述代码将会抛出异常,导致程序Crash:



在这里插入图片描述


我们有两种解决方式:




  1. try...catch

  2. 对象判空



try...catch 方式


public class TestJava {
public static void main(String args[]) {
(new TestJava()).testTryCatch();
}

void testTryCatch() {
try {
String str = getString();
System.out.println(str.length());
} catch (Exception e) {
}
}

String getString() {
return null;
}
}

NPE被捕获,程序没有Crash。


对象判空


public class TestJava {
public static void main(String args[]) {
(new TestJava()).testJudgeNull();
}

void testJudgeNull() {
String str = getString();
if (str != null) {
System.out.println(str.length());
}
}

String getString() {
return null;
}
}

因为提前判空,所以程序没有Crash。


编译时检测


在运行时再去做判断的缺点:



无法提前发现NPE问题,想要覆盖大部分的场景需要随时try...catch或是判空
总有忘记遗漏的时候,发布到线上就是个生产事故



那能否在编译时进行检测呢?

答案是使用注解。


public class TestJava {
public static void main(String args[]) {
(new TestJava()).test();
}

void test() {
String str = getString();
System.out.println(str.length());
}

@Nullable String getString() {
return null;
}
}

在编写getString()方法时发现其可能为空,于是给方法加上一个"可能为空"的注解:@Nullable


当调用getString()方法时,编译器给出如下提示:



在这里插入图片描述


意思是访问的getString()可能为空,最后访问String.length()时可能会抛出NPE。

看到编译器的提示我们就知道此处有NPE的隐患,因此可以针对性的进行处理(try...catch或是判空)。


当然此处的注解仅仅只是个"弱提示",你即使没有进行针对性的处理也能编译通过,只是问题最后都流转到运行时更难挽回了。


有"可空"的注解,当然也有"非空"的注解:



在这里插入图片描述


@Nonnull 注解修饰了方法后,若是检测到方法返回null,则会提示修改,当然也是"弱提示"。


3. Kotlin NPE检测


编译时检测


Kotlin 核心优势之一:



空安全检测,变量分为可空型/非空型,能够在编译期检测潜在的NPE,并强制开发者确保类型一致,将问题在编译期暴露并解决



先看非空类型的变量声明:


class TestKotlin {

fun test() {
val str = getString()
println("${str.length}")
}

private fun getString():String {
return "fish"
}
}

fun main() {
TestKotlin().test()
}


此种场景下,我们能确保getString()函数的返回一定非空,因此在调用该函数时无需进行判空也无需try...catch。


你可能会说,你这里写死了"fish",那我写成null如何?



在这里插入图片描述


编译期直接提示不能这么写,因为我们声明getString()的返回值为String,是非空的String类型,既然声明了非空,那么就需要言行一致,返回的也是非空的。


有非空场景,那也得有空的场景啊:


class TestKotlin {

fun test() {
val str = getString()
println("${str.length}")
}

private fun getString():String? {
return null
}
}

fun main() {
TestKotlin().test()
}

此时将getString()声明为非空,因此可以在函数里返回null。

然而调用之处就无法编译通过了:



在这里插入图片描述


意思是既然getString()可能返回null,那么就不能直接通过String.length访问,需要改为可空方式的访问:


class TestKotlin {

fun test() {
val str = getString()
println("${str?.length}")
}

private fun getString():String? {
return null
}
}

str?.length 意思是:如果str==null,就不去访问其成员变量/函数,若不为空则可以访问,于是就避免了NPE问题。


由此可以看出:



Kotlin 通过检测声明与实现,确保了函数一定要言行一致(声明与实现),也确保了调用者与被调用者的言行一致



因此,若是用Kotlin编写代码,我们无需花太多时间去预防和排查NPE问题,在编译期都会有强提示。


4. Java/Kotlin 混合调用


回到最初的问题:既然Kotlin都能在编译期避免了NPE,那为啥使用Kotlin重构后的代码反而导致Crash率上升呢?


原因是:项目里同时存在了Java和Kotlin代码,由上可知两者在NPE的检测上有所差异导致了一些兼容问题。


Kotlin 调用 Java


调用无返回值的函数


Kotlin虽然有空安全检测,但是Java并没有,因此对于Java方法来说,不论你传入空还是非空,在编译期我都没法检测出来。


public class TestJava {
void invokeFromKotlin(String str) {
System.out.println(str.length());
}
}

class TestKotlin {

fun test() {
TestJava().invokeFromKotlin(null)
}
}

fun main() {
TestKotlin().test()
}

如上无论是Kotlin调用Java还是Java之间互调,都没法确保空安全,只能由被调用者(Java)自己处理可能的异常情况。


调用有返回值的函数


public class TestJava {
public String getStr() {
return null;
}
}

class TestKotlin {
fun testReturn() {
println(TestJava().str.length)
}
}

fun main() {
TestKotlin().testReturn()
}

如上,Kotlin调用Java的方法获取返回值,由于在编译期Kotlin无法确定返回值,因此默认把它当做非空处理,若是Java返回了null,那么将会Crash。


Java 调用 Kotlin


调用无返回值的函数


先定义Kotlin类:


class TestKotlin {

fun testWithoutNull(str: String) {
println("len:${str.length}")
}

fun testWithNull(str: String?) {
println("len:${str?.length}")
}
}

有两个函数,分别接收可空/非空参数。


在Java里调用,先调用可空函数:


public class TestJava {
public static void main(String args[]) {
(new TestKotlin()).testWithNull(null);
}
}

因为被调用方是Kotlin的可空函数,因此即使Java传入了null,也不会有Crash。


再换个方式,在Java里调用非空函数:


public class TestJava {
public static void main(String args[]) {
(new TestKotlin()).testWithoutNull(null);
}
}

却发现Crash了!



在这里插入图片描述


为什么会Crash呢?反编译查看Kotlin代码:


public final class TestKotlin {
public final void testWithoutNull(@NotNull String str) {
Intrinsics.checkNotNullParameter(str, "str");
String var2 = "len:" + str.length();
System.out.println(var2);
}

public final void testWithNull(@Nullable String str) {
String var2 = "len:" + (str != null ? str.length() : null);
System.out.println(var2);
}
}

对于非空的函数来说,会有检测代码:

Intrinsics.checkNotNullParameter(str, "str"):


    public static void checkNotNullParameter(Object value, String paramName) {
if (value == null) {
throwParameterIsNullNPE(paramName);
}
}
private static void throwParameterIsNullNPE(String paramName) {
throw sanitizeStackTrace(new NullPointerException(createParameterIsNullExceptionMessage(paramName)));
}

可以看出:




  1. Kotlin对于非空的函数参数,先判断其是否为空,若是为空则直接抛出NPE

  2. Kotlin对于可空的函数参数,没有强制检测是否为空



调用有返回值的函数


Java 本身就没有空安全,只能在运行时进行处理。


小结


很容看出来:




  1. Java 调用Kotlin的非空函数有Crash的风险,编译器无法检查到传入的参数是否为空

  2. Java 调用Kotlin的可空函数没有Crash风险,Kotlin编译期检查空安全

  3. Kotlin 调用Java的函数有Crash风险,由Java代码规避风险

  4. Kotlin 调用Java有返回值的函数有Crash风险,编译器无法检查到返回值是否为空



回到文章的标题,我们已经大致知道了Java切换到Kotlin,为啥Crash就升上了的原因了,接下来再详细分析。


5. 常见的Java/Kotlin互调场景


Android里的Java代码分布



在这里插入图片描述


在Kotlin出现之前,Java就是Android开发的唯一语言,Android Framework、Androidx很多是Java代码编写的,因此现在依然有很多API是Java编写的。


而不少的第三方SDK因为稳定性、迁移代价的考虑依然使用的是Java代码。


我们自身项目里也因为一些历史原因存在Java代码。


以下讨论的前提是假设现有Java代码我们都无法更改。


Kotlin 调用Java获取返回值


由于编译期无法判定Java返回的值是空还是非空,因此若是确认Java函数可能返回空,则可以通过在Kotlin里使用可空的变量接收Java的返回值。


class TestKotlin {
fun testReturn() {
val str: String? = TestJava().str
println(str?.length)
}
}

fun main() {
TestKotlin().testReturn()
}

Java 调用Kotlin函数


LiveData Crash的原因与预防


之前已经假设过我们无法改动Java代码,那么Java调用Kotlin函数的场景只有一个了:回调。

上面的有返回值场景还是比较容易防备,回调的场景就比较难发现,尤其是层层封装之后的代码。

这也是特别常见的场景,典型的例子如LiveData。


Crash原因


class TestKotlin(val lifecycleOwner: LifecycleOwner) {
val liveData: MutableLiveData = MutableLiveData()
fun testLiveData() {
liveData.observe(lifecycleOwner) {
println(it.length)
}
}

init {
testLiveData()
}
}

如上,使用Kotlin声明LiveData,其类型是非空的,并监听LiveData的变化。


在另一个地方给LiveData赋值:


TestKotlin(this@MainActivity).liveData.value = null

虽然LiveData的监听和赋值的都是使用Kotlin编写的,但不幸的是还是Crash了。

发送和接收都是用Kotlin编写的,为啥还会Crash呢?

看看打印:



在这里插入图片描述


意思是接收到的字符串是空值(null),看看编译器提示:



在这里插入图片描述


原来此处的回调传入的值被认为是非空的,因此当使用it.length访问的时候编译器不会有空安全提示。


再看看调用的地方:



在这里插入图片描述


可以看出,这回调是Java触发的。


Crash 预防


第一种方式:

我们看到LiveData的数据类型是泛型,因此可以考虑在声明数据的时候定为非空:


class TestKotlin(val lifecycleOwner: LifecycleOwner) {
val liveData = MutableLiveData()
fun testLiveData() {
liveData.observe(lifecycleOwner) {
println(it?.length)
}
}

init {
testLiveData()
}
}

如此一来,当访问it.length时编译器就会提示可空调用。


第二种方式:

不修改数据类型,但在接收的地方使用可空类型接收:


class TestKotlin(val lifecycleOwner: LifecycleOwner) {
val liveData = MutableLiveData()
fun testLiveData() {
liveData.observe(lifecycleOwner) {
val dataStr:String? = it
println(dataStr?.length)
}
}

init {
testLiveData()
}
}

第三种方式:

使用Flow替换LiveData。


LiveData 修改建议:




  1. 若是新写的API,建议使用第三种方式

  2. 若是修改老的代码,建议使用第一种方式,因为可能有多个地方监听LiveData值的变化,如果第一种方式的话需要写好几个地方。



其它场景的Crash预防:


与后端交互的数据结构
比如与后端交互声明的类,后端有可能返回null,此时在客户端接收时若是使用了非空类型的字段去接收,那么会发生Crash。

通常来说,我们会使用网络框架(如retrofit)接收数据,数据的转换并不是由我们控制,因此无法使用针对LivedData的第二种方式。

有两种方式解决:




  1. 与后端约定,不能返回null(等于白说)

  2. 客户端声明的类的字段声明为可空(类似针对LivedData的第一种方式)



Json序列化/反序列化

Json字符串转换为对象时,有些字段可能为空,也需要声明为可空字段。


小结



在这里插入图片描述


作者:小鱼人爱编程
来源:juejin.cn/post/7274163003158511616
收起阅读 »

当遇到需要在Activity间传递大量的数据怎么办?

在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办? Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大...
继续阅读 »

在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办?


Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大小就是1M-8K.一般activity间传递数据会要使用到binder,因此这个就成为了数据传递的大小的限制。那么当activity间要传递大数据采用什么方式呢?其实方式很多,我们就举几个例子给大家说明一下,但是无非就是使用数据持久化,或者内存共享方案。一般大数据的存储不适宜使用SP, MMKV,DataStore。


Activity之间传递大量数据主要有如下几种方式实现:


  • LruCache

  • 持久化(sqlite、file等)

  • 匿名共享内存


使用LruCache

LruCache是一种缓存策略,可以帮助我们管理缓存,想具体了解的同学可以去Glide章节中具体先了解下。在当前的问题下,我们可以利用LruCache存储我们数据作为一个中转,好比我们需要Activity A向Activity B传递大量数据,我们可以Activity A先向LruCache先写入数据,之后Activity B从LruCache读取。


首先我们定义好写入读出规则:


public interface IOHandler {
   //保存数据
   void put(String key, String value);
   void put(String key, int value);
   void put(String key, double value);
   void put(String key, float value);
   void put(String key, boolean value);
   void put(String key, Object value);

   //读取数据
   String getString(String key);
   double getDouble(String key);
   boolean getBoolean(String key);
   float getFloat(String key);
   int getInt(String key);
   Object getObject(String key);
}

我们可以根据规则也就是接口,写出具体的实现类。实现类中我们保存数据使用到LruCache,这里面我们一定要设置一个大小,因为内存中数据的最大值是确定,我们保存数据的大小最好不要超过最大值的1/8.


LruCache<String, Object> mCache = new LruCache<>( 10 * 1024*1024);

写入数据我们使用比较简单:


@Override
public void put(String key, String value) {
   mCache.put(key, value);
}

好比上面写入String类型的数据,只需要接收到的数据全部put到mCache中去。


读取数据也是比较简单方便:


@Override
public String getString(String key) {
   return String.valueOf(mCache.get(key));
}

持久化数据

那就是sqlite、file等方式。将需要传递的数据写在临时文件或者数据库中,再跳转到另外一个组件的时候再去读取这些数据信息,这种处理方式会由于读写文件较为耗时导致程序运行效率较低。这种方式特点如下:


优势:


(1)应用中全部地方均可以访问


(2)即便应用被强杀也不是问题了


缺点:


(1)操做麻烦


(2)效率低下


匿名共享内存

在跨进程传递大数据的时候,我们一般会采用binder传递数据,但是Binder只能传递1M一下的数据,所以我们需要采用其他方式完成数据的传递,这个方式就是匿名共享内存。


Anonymous Shared Memory 匿名共享内存」是 Android 特有的内存共享机制,它可以将指定的物理内存分别映射到各个进程自己的虚拟地址空间中,从而便捷的实现进程间内存共享。


Android 上层提供了一些内存共享工具类,就是基于 Ashmem 来实现的,比如 MemoryFile、 SharedMemory。



今日分享到此结束,对你有帮助的话,点个赞再走呗,每日一个面试小技巧




关注公众号:Android老皮

解锁  《Android十大板块文档》 ,让学习更贴近未来实战。已形成PDF版



内容如下



1.Android车载应用开发系统学习指南(附项目实战)

2.Android Framework学习指南,助力成为系统级开发高手

3.2023最新Android中高级面试题汇总+解析,告别零offer

4.企业级Android音视频开发学习路线+项目实战(附源码)

5.Android Jetpack从入门到精通,构建高质量UI界面

6.Flutter技术解析与实战,跨平台首要之选

7.Kotlin从入门到实战,全方面提升架构基础

8.高级Android插件化与组件化(含实战教程和源码)

9.Android 性能优化实战+360°全方面性能调优

10.Android零基础入门到精通,高手进阶之路



作者:派大星不吃蟹
来源:juejin.cn/post/7264503091116965940
收起阅读 »

如何让 Android 网络请求像诗一样优雅

在 Android 应用开发中,网络请求必不可少,如何去封装才能使自己的请求代码显得更加简洁优雅,更加方便于以后的开发呢?这里利用 Kotlin 的函数式编程和 Retrofit 来从零开始封装一个网络请求框架,下面就一起来瞧瞧吧! 首先,引入网络请求框架的依...
继续阅读 »

在 Android 应用开发中,网络请求必不可少,如何去封装才能使自己的请求代码显得更加简洁优雅,更加方便于以后的开发呢?这里利用 Kotlin 的函数式编程和 Retrofit 来从零开始封装一个网络请求框架,下面就一起来瞧瞧吧!


首先,引入网络请求框架的依赖。


implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

定义拦截器


我们可以先自定义一些拦截器,对一些公共提交的字段做封装,比如 token。在服务器注册成功或者登录成功之后获取 token,过期之后便无法正常请求接口,所以需要在请求接口时判断 token 是否过期,由于接口众多,不可能每个接口都进行判断,所以需要全局设置一个拦截器判断 token。


class TokenInterceptor : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
// 当前拦截器中收到的请求对象
val request = chain.request()
// 执行请求
var response = chain.proceed(request)
if (response.body == null) {
return response
}
val mediaType = response.body!!.contentType() ?: return response
val type = mediaType.toString()
if (!type.contains("application/json")) {
return response
}
val result = response.body!!.string()
var code = ""
try {
val jsonObject = JSONObject(result)
code = jsonObject.getString("code")
} catch (e: Exception) {
e.printStackTrace()
}
// 重新构建 response
response = response.newBuilder().body(result.toResponseBody(null)).code(200).build()
if (isTokenExpired(code)) {
// token 过期,需要获取新的 token
val newToken = getNewToken() ?: return response
// 重新构建新的 token 请求
val builder = request.url.newBuilder().setEncodedQueryParameter("token", newToken)
val newRequest = request.newBuilder().method(request.method, request.body)
.url(builder.build()).build()
return chain.proceed(newRequest)
}
return response
}

// 判断 token 是否过期
private fun isTokenExpired(code: String) =
TextUtils.equals(code, "401") || TextUtils.equals(code, "402")

// 刷新 token
private fun getNewToken() = ServiceManager.instance.refreshToken()

}

这里是 token 过期之后直接重新请求接口获取新的 token,这需要根据具体业务需求来,有些可能是过期之后跳转到登录页面,让用户重新登录等等。


我们还可以再定义一个拦截器,全局添加 token。


class TokenHeaderInterceptor : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val headers = request.headers
var token = headers["token"]
if (TextUtils.isEmpty(token)) {
token = ServiceManager.instance.getToken()
request = request.newBuilder().addHeader("token", token).build()
}
return chain.proceed(request)
}

}

创建 retrofit


class RetrofitUtil {

companion object {

private const val TIME_OUT = 20L

private fun createRetrofit(): Retrofit {

// OkHttp 提供的一个拦截器,用于记录和查看网络请求和响应的日志信息。
val interceptor = HttpLoggingInterceptor()
// 打印请求和响应的所有内容,响应状态码和执行时间等等。
interceptor.level = HttpLoggingInterceptor.Level.BODY

val okHttpClient = OkHttpClient().newBuilder().apply {
addInterceptor(interceptor)
addInterceptor(TokenInterceptor())
addInterceptor(TokenHeaderInterceptor())
retryOnConnectionFailure(true)
connectTimeout(TIME_OUT, TimeUnit.SECONDS)
writeTimeout(TIME_OUT, TimeUnit.SECONDS)
readTimeout(TIME_OUT, TimeUnit.SECONDS)
}.build()

return Retrofit.Builder().apply {
addConverterFactory(GsonConverterFactory.create())
baseUrl(ServiceManager.instance.baseHttpUrl)
client(okHttpClient)
}.build()

}

fun <T> getAPI(clazz: Class<T>): T {
return createRetrofit().create(clazz)
}

}
}

网络请求封装


定义通用基础请求返回的数据结构


private const val SERVER_SUCCESS = "200"

data class BaseResp<T>(val code: String, val message: String, val data: T)

fun <T> BaseResp<T>?.isSuccess() = this?.code == SERVER_SUCCESS

请求状态流程封装,可以根据具体业务流程实现方法。


class RequestAction<T> {

// 开始请求
var start: (() -> Unit)? = null
private set

// 发起请求
var request: (suspend () -> BaseResp<T>)? = null
private set

// 请求成功
var success: ((T?) -> Unit)? = null
private set

// 请求失败
var error: ((String) -> Unit)? = null
private set

// 请求结束
var finish: (() -> Unit)? = null
private set

fun request(block: suspend () -> BaseResp<T>) {
request = block
}

fun start(block: () -> Unit) {
start = block
}

fun success(block: (T?) -> Unit) {
success = block
}

fun error(block: (String) -> Unit) {
error = block
}

fun finish(block: () -> Unit) {
finish = block
}

}

因为网络请求都是在 ViewModel 中进行的,我们可以定义一个 ViewModel 的扩展函数,用来处理网络请求。


fun <T> ViewModel.netRequest(block: RequestAction<T>.() -> Unit) {

val action = RequestAction<T>().apply(block)

viewModelScope.launch {
try {
action.start?.invoke()
val result = action.request?.invoke()
if (result.isSuccess()) {
action.success?.invoke(result!!.data)
} else {
action.error?.invoke(result!!.message)
}
} catch (ex: Exception) {
// 可以做一些定制化的返回错误提示
action.error?.invoke(getErrorTipContent(ex))
} finally {
action.finish?.invoke()
}
}

}

private const val SERVER_ERROR = "HTTP 500 Internal Server Error"
private const val HTTP_ERROR_TIP = "服务器或者网络连接错误"

fun getErrorTipContent(ex: Throwable) = if (ex is ConnectException || ex is UnknownHostException
|| ex is SocketTimeoutException || SERVER_ERROR == ex.message.toString()
) HTTP_ERROR_TIP else ex.message.toString()

使用案例


定义网络请求接口


interface HttpApi {

@GET("/exampleA/exampleP/exampleI/exampleApi/getNetData")
suspend fun getNetData(@QueryMap params: HashMap<String, String>): BaseResp<NetDataBean>

@GET("/exampleA/exampleP/exampleI/exampleApi/getTestData")
suspend fun getTestData(
@Query("param1") param1: String,
@Query("param2") param2: String
)
: BaseResp<NetDataBean>

@GET("/exampleA/exampleP/exampleI/exampleApi/{id}")
fun getNetTask(
@Path("id") id: String,
@QueryMap params: HashMap<String, String>,
)
: Call<BaseResp<TaskBean>>

@FormUrlEncoded
@POST("/exampleA/exampleP/exampleI/exampleApi/confirm")
suspend fun confirm(@Field("id") id: String, @Field("token") token: String): BaseResp<String>

@FormUrlEncoded
@POST("/exampleA/exampleP/exampleI/exampleApi/upload")
suspend fun upload(@FieldMap params: Map<String, String>): BaseResp<String>

}

我们可以写一个网络请求帮助类,用于请求的创建。


class RequestHelper {

private val httpApi = RetrofitUtil.getAPI(HttpApi::class.java)

companion object {
val instance: RequestHelper by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
RequestHelper()
}
}

suspend fun getNetData(params: HashMap<String, String>) = httpApi.getNetData(params)

suspend fun getTestData(branchCode: String, token: String) =
httpApi.getTestData(branchCode, token)

suspend fun getNetTask(id: String, params: HashMap<String, String>) =
httpApi.getNetTask(id, params)

suspend fun confirm(id: String, token: String) = httpApi.confirm(id, token)

suspend fun upload(params: HashMap<String, String>) = httpApi.upload(params)

}

定义用户的意图和 UI 状态


// 定义用户意图
sealed class MainIntent {
object FetchData : MainIntent()
}

// 定义 UI 状态
sealed class MainUIState {
object Loading : MainUIState()
data class NetData(val data: NetDataBean?) : MainUIState()
data class Error(val error: String?) : MainUIState()
}

ViewModel 中做意图的处理和 UI 状态的变更,根据网络请求结果传递不同的状态,使用定义的扩展方法去执行网络请求,封装过后的网络请求就很简洁方便了,下面演示下具体使用。


class MainViewModel : ViewModel() {

val mainIntent = Channel<MainIntent>(Channel.UNLIMITED)

private val _mainUIState = MutableStateFlow<MainUIState>(MainUIState.Loading)
val mainUIState: StateFlow<MainUIState>
get() = _mainUIState

init {
viewModelScope.launch {
mainIntent.consumeAsFlow().collect {
if (it is MainIntent.FetchData) {
getNetDataResult()
}
}
}
}
// 使用
private fun getNetDataResult() = netRequest {
start { _mainUIState.value = MainUIState.Loading }
request {
val paramMap = hashMapOf<String, String>()
paramMap["param1"] = "param1"
paramMap["param2"] = "param2"
RequestHelper.instance.getNetData(paramMap)
}
success { _mainUIState.value = MainUIState.NetData(it) }
error { _mainUIState.value = MainUIState.Error(it) }
}

}

这样是不是看起来很简洁呢?接下来,Activity 负责发送意图和接收 UI 状态进行相关的处理就行啦!


class MainActivity : AppCompatActivity() {

private val viewModel by viewModels<MainViewModel>()
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
initData()
observeViewModel()
}

private fun initData() {
lifecycleScope.launch {
// 发送意图
viewModel.mainIntent.send(MainIntent.FetchData)
}
}

private fun observeViewModel() {
lifecycleScope.launch {
viewModel.mainUIState.collect {
when (it) {
is MainUIState.Loading -> showLoading()
// 这里拿到网络请求返回的数据,根据业务自行操作,这里只做简单的显示。
is MainUIState.NetData -> showText(it.data.toString())
is MainUIState.Error -> showText(it.error)
}
}
}
}

private fun showLoading() {
binding.progressBar.visibility = View.VISIBLE
binding.netText.visibility = View.GONE
}

private fun showText(result: String?) {
binding.progressBar.visibility = View.GONE
binding.netText.visibility = View.VISIBLE
binding.netText.text = result
}

}

文件的上传与下载


如果是文件的上传和下载呢?其实文件还不太一样,这涉及到上传进度,文件的处理等方面,所以,为了方便开发使用,我们可以针对文件单独再做一下封装。


定义文件上传对象


data class UpLoadFileBean(val file: File, val fileKey: String)

自定义 RequestBody,从中获取上传进度。


class ProgressRequestBody(
private var requestBody: RequestBody,
var onProgress: ((Int) -> Unit)?,
) : RequestBody() {

private var bufferedSink: BufferedSink? = null

override fun contentType(): MediaType? = requestBody.contentType()

override fun contentLength(): Long {
return requestBody.contentLength()
}

override fun writeTo(sink: BufferedSink) {
if (bufferedSink == null) bufferedSink = createSink(sink).buffer()
bufferedSink?.let {
requestBody.writeTo(it)
it.flush()
}
}

private fun createSink(sink: Sink): Sink = object : ForwardingSink(sink) {
// 当前写入字节数
var bytesWritten = 0L

// 总字节长度
var contentLength = 0L

override fun write(source: Buffer, byteCount: Long) {
super.write(source, byteCount)

if (contentLength == 0L) {
contentLength = contentLength()
}

// 增加当前写入的字节数
bytesWritten += byteCount

CoroutineScope(Dispatchers.Main).launch {
// 进度回调
onProgress?.invoke((bytesWritten * 100 / contentLength).toInt())
}
}
}

}

创建 MultipartBody.Part


fun <T> createPartList(action: UpLoadFileAction<T>): List<MultipartBody.Part> =
MultipartBody.Builder().apply {
// 公共参数 token
addFormDataPart("token", ServiceManager.instance.getToken())

// 其他基本参数
action.params?.forEach {
if (it.key.isNotBlank() && it.value.isNotBlank()) {
addFormDataPart(it.key, it.value)
}
}

// 文件校验
action.fileData?.let {
addFormDataPart(
it.fileKey, it.file.name, ProgressRequestBody(
requestBody = it.file
.asRequestBody("application/octet-stream".toMediaTypeOrNull()),
onProgress = action.progress
)
)
}
}.build().parts

定义文件上传行为


class UpLoadFileAction<T> {

// 请求体
lateinit var request: (suspend () -> BaseResp<T>)
private set

lateinit var parts: List<MultipartBody.Part>

// 其他普通参数
var params: HashMap<String, String>? = null
private set

// 文件参数
var fileData: UpLoadFileBean? = null
private set

// 初始化参数
fun init(params: HashMap<String, String>?, fileData: UpLoadFileBean?) {
this.params = params
this.fileData = fileData
parts = createPartList(this)
}

var start: (() -> Unit)? = null
private set

var success: (() -> Unit)? = null
private set

var error: ((String) -> Unit)? = null
private set

var progress: ((Int) -> Unit)? = null
private set

var finish: (() -> Unit)? = null
private set

fun start(block: () -> Unit) {
start = block
}

fun success(block: () -> Unit) {
success = block
}

fun error(block: (String) -> Unit) {
error = block
}

fun progress(block: (Int) -> Unit) {
progress = block
}

fun finish(block: () -> Unit) {
finish = block
}

fun request(block: suspend () -> BaseResp<T>) {
request = block
}

}

同样,定义 ViewModel 的扩展函数,用来执行文件上传。


fun <T> ViewModel.upLoadFile(
block: UpLoadFileAction<T>.() -> Unit,
params: HashMap<String, String>?,
fileData: UpLoadFileBean?,
)
= viewModelScope.launch {
val action = UpLoadFileAction<T>().apply(block)
try {
action.init(params, fileData)
action.start?.invoke()
val result = action.request.invoke()
if (result.isSuccess()) {
action.success?.invoke()
} else {
action.error?.invoke(result.message)
}
} catch (ex: Exception) {
action.error?.invoke(getErrorTipContent(ex))
} finally {
action.finish?.invoke()
}
}

定义文件上传接口


interface HttpApi {
//...

@Multipart
@POST("/exampleA/exampleP/exampleI/exampleApi/uploadFile")
suspend fun uploadFile(@Part partLis: List<MultipartBody.Part>): BaseResp<String>

}

在 RequestHelper 中定义上传文件方法


class RequestHelper {

private val httpApi = RetrofitUtil.getAPI(HttpApi::class.java)

companion object {
val instance: RequestHelper by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
RequestHelper()
}
}

//...

suspend fun uploadFile(partList: List<MultipartBody.Part>) = httpApi.uploadFile(partList)

}

封装过后的文件上传就很简洁方便了,下面演示下具体使用。


private fun uploadMyFile() = upLoadFile(
params = hashMapOf("param1" to "param1", "param2" to "param2"),
fileData = UpLoadFileBean(File(absoluteFilePath), "file"),
) {
start {
// TODO: 开始上传,此处可以显示加载动画
}
request { RequestHelper.instance.uploadFile(parts) }
success {
// TODO: 上传成功
}
error {
// TODO: 上传失败
}
finish {
// TODO: 上传结束,此处可以关闭加载动画
}
}

既然上传文件都有了,那怎么少得了下载呢?其实,下载比上传更简单,下面就来写一下,同样利用了 kotlin 的函数式编程,我们添加 ViewModel 的扩展函数,需要注意的是,由于这边是直接使用 OkHttp 的同步请求,所以把这部分代码放在了 IO 线程中。


fun ViewModel.downLoadFile(
downLoadUrl: String,
dirPath: String,
fileName: String,
progress: ((Int) -> Unit)?,
success: (File) -> Unit,
failed: (String) -> Unit,
)
= viewModelScope.launch(Dispatchers.IO) {
try {
val fileDir = File(dirPath)
if (!fileDir.exists()) {
fileDir.mkdirs()
}
val downLoadFile = File(fileDir, fileName)
val request = Request.Builder().url(downLoadUrl).get().build()
val response = OkHttpClient.Builder().build().newCall(request).execute()
if (response.isSuccessful) {
response.body?.let {
val totalLength = it.contentLength().toDouble()
val stream = it.byteStream()
stream.copyTo(downLoadFile.outputStream()) { currentLength ->
// 当前下载进度
val process = currentLength / totalLength * 100
progress?.invoke(process.toInt())
}
success.invoke(downLoadFile)
} ?: failed.invoke("response body is null")
} else failed.invoke("download failed:$response")
} catch (ex: Exception) {
failed.invoke("download failed:${getErrorTipContent(ex)}")
}
}


// InputStream 添加扩展函数,实现字节拷贝。
private fun InputStream.copyTo(
out: OutputStream,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
progress: (Long) -> Unit,
)
: Long {
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = read(buffer)
progress(bytesCopied)
}
return bytesCopied
}

然后,使用就会变得很简洁了,如下所示:


fun downloadMyFile(downLoadUrl: String, dirPath: String, fileName: String) =
downLoadFile(
downLoadUrl = downLoadUrl,
dirPath = dirPath,
fileName = fileName,
progress = {
// TODO: 这里可以拿到进度
},
success = {
// TODO: 下载成功,拿到下载的文件对象 File
},
failed = {
// TODO: 下载失败,返回原因
}

)

作者:阿健君
来源:juejin.cn/post/7266768708139434045
收起阅读 »

Service 层异常抛到 Controller 层处理还是直接处理?

0 前言 一般初学者学习编码和[错误处理]时,先知道[编程语言]有一种处理错误的形式或约定(如Java就抛异常),然后就开始用这些工具。但却忽视这问题本质:处理错误是为了写正确程序。可是 1 啥叫“正确”? 由解决的问题决定的。问题不同,解决方案不同。 如一个...
继续阅读 »

0 前言


一般初学者学习编码和[错误处理]时,先知道[编程语言]有一种处理错误的形式或约定(如Java就抛异常),然后就开始用这些工具。但却忽视这问题本质:处理错误是为了写正确程序。可是


1 啥叫“正确”?


由解决的问题决定的。问题不同,解决方案不同。


如一个web接口接受用户请求,参数age,也许业务要求字段是0~150之间整数。如输入字符串或负数就肯定不接受。一般在后端某地做输入合法性检查,不过就抛异常。


但归根到底这问题“正确”解决方法总是要以某种形式提示用户。而提示用户是某种前端工作,就要看界面是app,H5+AJAX还是类似于[jsp]的服务器产生界面。不管啥,你要根据需求去”设计一个修复错误“的流程。如一个常见的流程要后端抛异常,然后一路到某个集中处理错误的代码,将其转换为某个HTTP的错误(业务错误码)提供给前端,前端再映射做”提示“。如用户输入非法请求,从逻辑上后端都没法自己修复,这是个“正确”的策略。


2 报500了嘞!


如用户上传一个头像,后端将图片发给[云存储],结果云存储报500,咋办?你可能想重试,因为也许仅是[网络抖动],重试就能正常执行。但若重试多次无效,若设计了某种热备方案,可能改为发到另一个服务器。“重试”和“使用备份的依赖”都是“立刻处理“。


但若重试无效,所有的[备份服务]也无效,也许就能像上面那样把错误抛给前端,提示用户“服务器开小差”。从这方案易看出,你想把错误抛到哪里是因为那个catch的地方是处理问题最方便的地方。一个问题的解决方案可能要几个不同的错误处理组合起来才能办到。


3 NPE了!


你的程序抛个NPE。这一般就是程序员的bug:



  • 要不就是程序员想表达一个东西”没有“,结果在后续处理中忘判断是否为null

  • 要不就是在写代码时觉得100%不可能为null的地方出现了一个null


不管哪种,这错误用户总会看到一个很含糊的报错信息,这远远不够。“正确”办法是程序员自己能尽快发现它,并尽快修复。要做到这点,需要[监控系统]不断爬log,把问题报警出来。而非等用户找客服投诉。


4 OOM了!


比如你的[后端程序]突然OOM挂了。挂的程序没法恢复自己。要做到“正确”,须在服务之外的容器考虑这问题。


如你的服务跑在[k8s],他们会监控你程序状态,然后重启新的服务实例弥补挂掉的服务,还得调整流量,把去往宕机服务的流量切换到新实例。这的恢复因为跨系统所以不能仅用异常实现,但道理一样。


但光靠重启就“正确”了?若服务是完全无状态,问题不大。但若有状态,部分用户数据可能被执行一半的请求搞乱。因此重启要留意先“恢复数据到合法状态”。这又回到你要知道咋样才是“正确”的做法。只依靠简单的语法功能不能无脑解决这事。


5 提升维度



  • 一个工作线程的“外部容器“是管理工作线程的“master”

  • 一个网络请求的“外部容器”是一个Web Server

  • 一个用户进程的“外部容器”是[操作系统]

  • Erlang把这种supervisor-worker的机制融入到语言的设计


Web程序很大程度能把异常抛给顶层,是因为:



  • 请求来自前端,对因为用户请求有误(数据合法性、权限、用户上下文状态)造成的问题,最终基本只能告诉用户。因此抛异常到一个集中处理错误的地方,把异常转换为某个业务错误码的方法,合理

  • 后端服务一般无状态。这也是软件系统设计的一般原则。无状态才意味着可随时随地安心重启。用户数据不会因为因为下一条而会出问题

  • 后端对数据的修改依赖DB的事务。因此一个改一半的、没提交的事务不会造成副作用。


但这3条件并非总成立。总能遇到:



  • 一些处理逻辑并非无状态

  • 也并非所有的数据修改都能用一个事务保护


尤其要注意对[微服务]的调用,对内存状态的修改是没有事务保护的,一不留神就会搞乱用户数据。比如下面代码段


6 难以排查的代码段


 try {
int res1 = doStep1();
this.status1 += res1;
int res2 = doStep2();
this.status2 += res2;
// 抛个异常
int res3 = doStep3();
this.status3 = status1 + status2 + res3;
} catch ( ...) {
// ...
}

先假设status1、status2、status3之间需维护某种不变的约束(invariant)。然后执行这段代码时,如在doStep3抛异常,下面对status3的赋值就不会执行。这时如不能将status1、status2的修改rollback,就会造成数据违反约束的问题。


而程序员很难发现这个数据被改坏了。坏数据还可能导致其他依赖这数据的代码逻辑出错(如原本应该给积分的,却没给)。而这种错误一般很难排查,从大量数据里找到不正确的那一小段何其困难。


7 更难搞定的代码段


// controller
void controllerMethod(/* 参数 */) {
try {
return svc.doWorkAndGetResult(/* 参数 */);
} catch (Exception e) {
return ErrorJsonObject.of(e);
}
}

// svc
void doWorkAndGetResult(/* some params*/) {
int res1 = otherSvc1.doStep1(/* some params */);
this.status1 += res1;
int res2 = otherSvc2.doStep2(/* some params */);
this.status2 += res2;
int res3 = otherSvc3.doStep3(/* some params */);
this.status3 = status1 + status2 + res3;
return SomeResult.of(this.status1, this.status2, this.status3);
}

难搞在于你写的时候可能以为doStep1~3这种东西即使抛异常也能被Controller里的catch。


在svc这层是不用处理任何异常,因此不写[try……catch]天经地义。但实际上doStep1、doStep2、doStep3任何一个抛异常都会造成svc的数据状态不一致。甚至你一开始都可以通过文档或其他沟通确定doStep1、doStep2、doStep3一开始都是必然可成功,不会抛错的,因此你写的代码一开始是对的。


但你可能无法控制他们的实现(如他们是另外一个团队开发的[jar]提供的),而他们的实现可能会改成抛错。你的代码可能在完全不自知情况下从“不会出问题”变成“可能出问题”…… 更可怕的类似代码不能正确工作:


void doWorkAndGetResult(/* some params*/) {
try {
int res1 = otherSvc1.doStep1(/* some params */);
this.status1 += res1;
int res2 = otherSvc2.doStep2(/* some params */);
this.status2 += res2;
int res3 = otherSvc3.doStep3(/* some params */);
this.status3 = status1 + status2 + res3;
return SomeResult.of(this.status1, this.status2, this.status3);
} catch (Exception e) {
// do rollback
}
}

你以为这样就会处理好数据rollback,甚至觉得这种代码优雅。但实际上doStep1~3每一个地方抛错,rollback的代码都不一样。


得这么写


void doWorkAndGetResult(/* some params*/) {
int res1, res2, res3;
try {
res1 = otherSvc1.doStep1(/* some params */);
this.status1 += res1;
} catch (Exception e) {
throw e;
}

try {
res2 = otherSvc2.doStep2(/* some params */);
this.status2 += res2;
} catch (Exception e) {
// rollback status1
this.status1 -= res1;
throw e;
}

try {
res3 = otherSvc3.doStep3(/* some params */);
this.status3 = status1 + status2 + res3;
} catch (Exception e) {
// rollback status1 & status2
this.status1 -= res1;
this.status2 -= res2;
throw e;
}
}

这才是得到正确结果的代码,在任何地方出错都能维护数据一致性。优雅吗?


看起来很丑。比go的if err != nil还丑。但要在正确性和优雅性取舍,肯定毫不犹豫选前者。作为程序员不能直接认为抛异常可解决任何问题,须学会写出有正确逻辑的程序,哪怕很难且看起来丑。


为达成高正确性,你不能总将自己大部分注意力放在“一切都OK的流程“,而把错误看作是可随便应付了事的工作或简单的相信exception可自动搞定一切。


8 总结


对错误处理要有敬畏之心:



  • Java因为Checked Exception设计问题不得不避免使用

  • 而Uncaughted Exception实在弱鸡,不能给程序员提供更好帮助


因此,程序员在每次抛错或者处理错误的时候都要三省吾身:



  • 这个错误的处理是正确吗?

  • 会让用户看到啥?

  • 会不会搞乱数据?


不要以为自己抛个异常就完事了。在[编译器]不能帮上太多忙时,好好写UT来保护代码可怜的正确性。


请多写正确的代码


作者:JavaEdge在掘金
来源:juejin.cn/post/7280050832949968954
收起阅读 »

当别人因为React、Vue吵起来时,我们应该做什么

web
大家好,我卡颂。 最近尤大的一个推文引起了不小热议,大概经过是: 有人在推上夸React文档写的好,把可能的坑点都列出来 尤看到后批评道:框架应该自己处理这些坑点,而不是把他们暴露给用户 尤大在推上的发言一直比较耿直,这次又涉及到React这个老对手,关...
继续阅读 »

大家好,我卡颂。


最近尤大的一个推文引起了不小热议,大概经过是:



  1. 有人在推上夸React文档写的好,把可能的坑点都列出来

  2. 尤看到后批评道:框架应该自己处理这些坑点,而不是把他们暴露给用户



尤大在推上的发言一直比较耿直,这次又涉及到React这个老对手,关注度自然不低。


再加上国内前端自媒体的一波引导发酵,比如知乎下这个话题相关的问题中的措辞是怒喷,懂得都懂。



在这样氛围与二手信源的影响下,会给人一种大佬都亲手下场撕了的感觉,自然会引来ReactVue各自拥趸的一番激烈讨论。


年年都是一样的套路,毫无新意......


面对这样的争吵,我们应该做什么呢?


首先,回到源头本身,尤大diss的有道理么?有。


React的心智负担重么?确实重。比如useEffec这个API,你能想象文档中一个章节居然有6篇文章是教你如何正确使用useEffec的么?



造成这一现象的原因有很多,比如:



  1. Hooks的实现原理使得必须显式声明依赖

  2. 显式声明依赖无法覆盖useEffect所有场景,为此专门提出一个叫Effect Event的概念,以及一个对应的新hook —— useEffectEvent

  3. useEffect承载了太多功能,比如未来Offscreen的显隐回调(类似Vue中的Keep-Alive)也是通过useEffect实现


当我们继续往前回溯,Hooks必须显式声明依赖React更新机制决定的,而React更新机制又是React实现原理的核心。


本质来说,还是React既往的成功、庞大的社区生态让他积重难返,无法从底层重写。


这是历史必然的进程,如果Vue所有新特性都在Vue2基础上迭代(而不是完全重写的Vue3),我相信也是同样的局面。


所以,当前React的迭代方向是 —— 支持上层框架(比如Next.jsRemix),寄希望于靠这些框架的封装能力弥补React自身心智负担重的缺点。这个策略显然也是成功的。


回到这次争吵本身,尤大不知道React文档为什么要花大篇幅帮开发者避坑(以及背后反映的积重难返)么?他显然是知道的。


他如此回复是因为他所处的位置是框架作者React是他的竞争对手。设想一下,如果你的竞争对手在一些方面确实不如你,但他的用户对此的反应不是“太难用了,我要换个好用的”,而是“一定是我用的姿势不对,你快出个文档好好教教我”


面对这样的用户,换谁都得有一肚子牢骚吧~



让我们再把视角转到React的用户(也就是我们这些普通开发者)上。我们为什么选择React呢?


可能有些人是处于喜好。但大部分开发者之所以用React,完全是因为公司要求用React


React的公司多,招React的岗位多,自然选择React的开发者就多了。


那么为什么用React的公司多呢?这显然是多年前React在先发优势、社区生态两场战役取胜后得到的结果。


总结


所以,我们需要尊重两个事实:



  1. React心智负担重是事实

  2. React的公司多也是事实


两者并不矛盾,他们都是历史进程的产物。


VueReact之间的讨论,即使是从技术层面出发,最后也容易陷入“React心智负担这么重,你们还甘之如饴,你们React党是不是傻”这样的争吵中。


这显然就是忽略了历史的进程。


正确的应对方式是多关心关心自己未来的发展:



  • 如果我的重心在海外,那应该给Next.js更多关注。海外远程团队不是Next就是Nest

  • 如果我的重心在国内,国内流量都被小程序分割了。一个长远的增长点应该是鸿蒙


作者:魔术师卡颂
来源:juejin.cn/post/7321589055883427855
收起阅读 »

线程数突增!领导说再这么写就gc掉我

线程数突增!领导说再这么写就gc掉我 前言 大家好,我是魔性的茶叶,今天给大家分享一个线上问题引出的一次思考,过程比较长,但是挺有意思。 今天上班把需求写完,出于学习(摸鱼)的心理上skywalking看看,突然发现我们的一个应用,应用内线程数超过900条,接...
继续阅读 »

线程数突增!领导说再这么写就gc掉我


前言


大家好,我是魔性的茶叶,今天给大家分享一个线上问题引出的一次思考,过程比较长,但是挺有意思。


今天上班把需求写完,出于学习(摸鱼)的心理上skywalking看看,突然发现我们的一个应用,应用内线程数超过900条,接近1000条,但是cpu并没有高涨,内存也不算高峰。但是敏锐的我还是立刻意识到这个应用有不妥,因为线程数太多了,不符合我们一个正常健康的应用数量。熟练的打出cpu dump观察,首先看线程组名的概览。


image-20230112200957387


从线程分组看,pool名开头线程占616条,而且waiting状态也是616条,这个点就非常可疑了,我断定就是这个pool开头线程池导致的问题。我们先排查为何这个线程池中会有600+的线程处于waiting状态并且无法释放,记接下来我们找几条线程的堆栈观察具体堆栈:


image-20230112201456234


这个堆栈看上去很合理,线程在线程池中不断的循环获取任务,因为获取不到任务所以进入了waiting状态,等待着有任务后被唤醒。


看上去不只一个线程池,并且这些线程池的名字居然是一样的,我大胆的猜测一下,是不断的创建同样的线程池,但是线程池无法被回收导致的线程数,所以接下来我们要分析两个问题,首先这个线程池在代码里是哪个线程池,第二这个线程池是怎么被创建的?为啥释放不了?


我在idea搜索new ThreadPoolExecutor()得到的结果是这样的:


image-20230112202915219


于是我陷入懵逼的状态,难道还有其他骚操作?


正在这时,一位不知名的郑网友发来一张截图:


image-20230112203527173


好家伙!竟然是用new FixedTreadPool()整出来的。难怪我完全搜不到,因为用的new FixedTreadPool(),所以线程池中的线程名是默认的pool(又多了一个不使用Executors来创建线程池的理由)。


然后我迫不及die的打开代码,试图找到罪魁祸首,结果发现作者居然是我自己。这是另一个惊喜,惊吓的惊。


冷静下来后我梳理一遍代码,这个接口是我两年前写的,主要是功能是统计用户的钱包每个月的流水,因为担心统计比较慢,所以使用了线程池,做了批量的处理,没想到居然导致了线程数过高,虽然没有导致事故,但是确实是潜在的隐患,现在没出事不代表以后不会出事。


去掉多余业务逻辑,我简单的还原一个代码给大家看,还原现场:


private static void threadDontGcDemo(){
      ExecutorService executorService = Executors.newFixedThreadPool(10);
      executorService.submit(() -> {
           System.out.println("111");
       });
   }

那么为啥线程池里面的线程和线程池都没释放呢


难道是因为没有调用shutdown?我大概能理解我两年前当时为啥不调用shutdown,是因为当初我觉得接口跑完,方法走到结束,理论上栈帧出栈,局部变量应该都销毁了,按理说executorService这个变量应该直接GG了,那么按理说我是不用调用shutdown方法的。


我简单的跑了个demo,循环的去new线程池,不调用shutdown方法,看看线程池能不能被回收


image-20230113142322106


打开java visual vm查看实时线程:


image-20230113142304644


可以看到线程数和线程池都一直在增加,但是一直没有被回收,确实符合发生的问题状况,那么假如我在方法结束前调用shutdown方法呢,会不会回收线程池和线程呢?


简单写个demo结合jvisualvm验证下:


image-20230113142902514


image-20230113142915722


结果是线程和线程池都被回收了。也就是说,执行了shutdown的线程池最后会回收线程池和线程对象


我们知道,一个对象能不能回收,是看它到gc root之间有没有可达路径,线程池不能回收说明到达线程池的gc root还是有可达路径的。这里讲个冷知识,这里的线程池的gc root是线程,具体的gc路径是thread->workers->线程池。线程对象是线程池的gc root,假如线程对象能被gc,那么线程池对象肯定也能被gc掉(因为线程池对象已经没有到gc root的可达路径了)。


那么现在问题就转为线程对象是在什么时候gc


郑网友给了一个粗浅但是合理的解释,线程对象肯定不是在运行中的时候被回收的,因为jvm肯定不可能去回收一条在运行中的线程,至少runnalbe状态的线程jvm不可能去回收。


在stackoverflow上我找到了更准确的答案:stackoverflow.com/questions/2…


image-20230113152802164


A running thread is considered a so called garbage collection root and is one of those things keeping stuff from being garbage collected。


这句话的意思是,一条正在运行的线程是gc root,注意,是正在运行,这个正在运行我先透露下,即使是waiting状态,也算正在运行。这个回答的整体的意思是,运行的线程是gc root,但是非运行的线程不是gc root(可以被回收)。


现在比较清楚了,线程池和线程被回收的关键就在于线程能不能被回收,那么回到原来的起点,为何调用线程池的shutdown方法能够导致线程和线程池被回收呢?难道是shutdown方法把线程变成了非运行状态吗


talk is cheap,show me the code


我们直接看看线程池的shutdown方法的源码


public void shutdown() {
       final ReentrantLock mainLock = this.mainLock;
       mainLock.lock();
       try {
           checkShutdownAccess();
           advanceRunState(SHUTDOWN);
           interruptIdleWorkers();
           onShutdown(); // hook for ScheduledThreadPoolExecutor
      } finally {
           mainLock.unlock();
      }
       tryTerminate();
}

private void interruptIdleWorkers() {
       interruptIdleWorkers(false);
}

private void interruptIdleWorkers(boolean onlyOne) {
       final ReentrantLock mainLock = this.mainLock;
       mainLock.lock();
       try {
           for (Worker w : workers) {
               Thread t = w.thread;
               if (!t.isInterrupted() && w.tryLock()) {
                   try {
                       t.interrupt();
                  } catch (SecurityException ignore) {
                  } finally {
                       w.unlock();
                  }
              }
               if (onlyOne)
                   break;
          }
      } finally {
           mainLock.unlock();
      }
}

我们从interruptIdleWorkers方法入手,这方法看上去最可疑,看到interruptIdleWorkers方法,这个方法里面主要就做了一件事,遍历当前线程池中的线程,并且调用线程的interrupt()方法,通知线程中断,也就是说shutdown方法只是去遍历所有线程池中的线程,然后通知线程中断。所以我们需要了解线程池里的线程是怎么处理中断的通知的。


我们点开worker对象,这个worker对象是线程池中实际运行的线程,所以我们直接看worker的run方法,中断通知肯定是在里面被处理了


//WOrker的run方法里面直接调用的是这个方法
final void runWorker(Worker w) {
       Thread wt = Thread.currentThread();
       Runnable task = w.firstTask;
       w.firstTask = null;
       w.unlock(); // allow interrupts
       boolean completedAbruptly = true;
       try {
           while (task != null || (task = getTask()) != null) {
               w.lock();
               // If pool is stopping, ensure thread is interrupted;
               // if not, ensure thread is not interrupted. This
               // requires a recheck in second case to deal with
               // shutdownNow race while clearing interrupt
               if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                     runStateAtLeast(ctl.get(), STOP))) &&
                   !wt.isInterrupted())
                   wt.interrupt();
               try {
                   beforeExecute(wt, task);
                   Throwable thrown = null;
                   try {
                       task.run();
                  } catch (RuntimeException x) {
                       thrown = x; throw x;
                  } catch (Error x) {
                       thrown = x; throw x;
                  } catch (Throwable x) {
                       thrown = x; throw new Error(x);
                  } finally {
                       afterExecute(task, thrown);
                  }
              } finally {
                   task = null;
                   w.completedTasks++;
                   w.unlock();
              }
          }
           completedAbruptly = false;
      } finally {
           processWorkerExit(w, completedAbruptly);
      }
}



这个runwoker属于是线程池的核心方法了,相当的有意思,线程池能不断运作的原理就是这里,我们一点点看。


首先最外层用一个while循环套住,然后不断的调用gettask()方法不断从队列中取任务,假如拿不到任务或者任务执行发生异常(抛出异常了)那就属于异常情况,直接将completedAbruptly 设置为true,并且进入异常的processWorkerExit流程。


我们看看gettask()方法,了解下啥时候可能会抛出异常:


private Runnable getTask() {
       boolean timedOut = false; // Did the last poll() time out?

       for (;;) {
           int c = ctl.get();
           int rs = runStateOf(c);

           // Check if queue empty only if necessary.
           if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
               decrementWorkerCount();
               return null;
          }

           int wc = workerCountOf(c);

           // Are workers subject to culling?
           boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

           if ((wc > maximumPoolSize || (timed && timedOut))
               && (wc > 1 || workQueue.isEmpty())) {
               if (compareAndDecrementWorkerCount(c))
                   return null;
               continue;
          }

           try {
               Runnable r = timed ?
                   workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                   workQueue.take();
               if (r != null)
                   return r;
               timedOut = true;
          } catch (InterruptedException retry) {
               timedOut = false;
          }
      }
  }

这样很清楚了,抛去前面的大部分代码不看,这句代码解释了gettask的作用:


Runnable r = timed ?
  workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
  workQueue.take()

gettask就是从工作队列中取任务,但是前面还有个timed,这个timed的语义是这样的:如果allowCoreThreadTimeOut参数为true(一般为false)或者当前工作线程数超过核心线程数,那么使用队列的poll方法取任务,反之使用take方法。这两个方法不是重点,重点是poll方法和take方法都会让当前线程进入time_waiting或者waiting状态。而当线程处于在等待状态的时候,我们调用线程的interrupt方法,毫无疑问会使线程当场抛出异常


也就是说线程池的shutdownnow方法调用interruptIdleWorkers去对线程对象interrupt是为了让处于waiting或者是time_waiting的线程抛出异常


那么线程池是在哪里处理这个异常的呢?我们看runwoker中的调用的processWorkerExit方法,说实话这个方法看着就像处理抛出异常的方法:


private void processWorkerExit(Worker w, boolean completedAbruptly) {
       if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
           decrementWorkerCount();

       final ReentrantLock mainLock = this.mainLock;
       mainLock.lock();
       try {
           completedTaskCount += w.completedTasks;
           workers.remove(w);
      } finally {
           mainLock.unlock();
      }

       tryTerminate();

       int c = ctl.get();
       if (runStateLessThan(c, STOP)) {
           if (!completedAbruptly) {
               int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
               if (min == 0 && ! workQueue.isEmpty())
                   min = 1;
               if (workerCountOf(c) >= min)
                   return; // replacement not needed
          }
           addWorker(null, false);
      }
}

我们可以看到,在这个方法里有一个很明显的 workers.remove(w)方法,也就是在这里,这个w的变量,被移出了workers这个集合,导致worker对象不能到达gc root,于是workder对象顺理成章的变成了一个垃圾对象,被回收掉了。然后等到worker中所有的worker都被移出works后,并且当前请求线程也完成后,线程池对象也成为了一个孤儿对象,没办法到达gc root,于是线程池对象也被gc掉了。


写了挺长的篇幅,我小结一下:



  1. 线程池调用shutdownnow方法是为了调用worker对象的interrupt方法,来打断那些沉睡中的线程(waiting或者time_waiting状态),使其抛出异常

  2. 线程池会把抛出异常的worker对象从workers集合中移除引用,此时被移除的worker对象因为没有到达gc root的路径已经可以被gc掉了

  3. 等到workers对象空了,并且当前tomcat线程也结束,此时线程池对象也可以被gc掉,整个线程池对象成功释放


最后总结:


如果只是在局部方法中使用线程池,线程池对象不是bean的情况时,记得要合理的使用shutdown或者shutdownnow方法来释放线程和线程池对象,如果不使用,会造成线程池和线程对象的堆积。


作者:魔性的茶叶
来源:juejin.cn/post/7197424371991855159
收起阅读 »

面试官:手写一个“发布-订阅模式”

web
发布-订阅模式又被称为观察者模式,它是定义在对象之间一对多的关系中,当一个对象发生变化,其他依赖于它的对象收到通知。在javascript的开发中,我们一般用事件模型替代发布-订阅模式。 DOM事件 document.body.addEventListener...
继续阅读 »

发布-订阅模式又被称为观察者模式,它是定义在对象之间一对多的关系中,当一个对象发生变化,其他依赖于它的对象收到通知。在javascript的开发中,我们一般用事件模型替代发布-订阅模式。


DOM事件


document.body.addEventListener('click',function(){

alert(绑定1);

},false);

document.body.click(); //模拟点击

document.body.addEventListener('click',function(){

alert(绑定2);

},false);

document.body.addEventListener('click',function(){

alert(绑定3);

},false);

document.body.click(); //模拟点击

我们可以增加更多订阅者,不会对发布者的代码造成影响。注意,标准浏览器下用dispatchEvent实现。


自定义事件


① 确定发布者。(例如售票处)


② 添加缓存列表,便于通知订阅者。(预订车票列表)


③ 发布消息。遍历缓存列表。依次触发里面存放的订阅者回调函数(遍历列表,逐个发送短信)。


另外,我们还可以在回调函数填入一些参数,例如车票的价格之类信息。


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (fn) { //增加订阅者
this.clientList.push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
for(var i = 0, fn; fn = this.clientList[i++];){
fn.apply(this, arguments); //arguments 是发布消息时带上的参数
}
}

// 下面进行简单测试:

ticketOffice.on(function(time, path){ //小刚订阅消息
console.log('时间:' + time);
console.log('路线:' + path);
});


ticketOffice.on(function(time, path){ //小强订阅消息
console.log('时间:' + time);
console.log('路线:' + path);
});

ticketOffice.emit('晚上8:00','深圳-上海');
ticketOffice.emit('晚上8:10','上海-深圳');

至此,我们实现了一个最简单发布-订阅模式。不过这里存在一些问题,我们运行代码可以看到订阅者接收到了所有发布的消息。


// 时间:晚上8:00
// 路线:深圳-上海
// 时间:晚上8:00
// 路线:深圳-上海
// 时间:晚上8:10
// 路线:上海-深圳
// 时间:晚上8:10
// 路线:上海-深圳

我们有必要加个key让订阅者只订阅 自己感兴趣的消息。改写后的代码如下:


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

// --------- 测试数据 --------------
ticketOffice.on('上海-深圳', function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
});

ticketOffice.on('深圳-上海', function(time){ //小强订阅消息
console.log('小强时间:' + time);
});

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小强时间:晚上8:00
// 小刚时间:晚上8:10

这样子,订阅者就可以只订阅自己感兴趣的事件了。


小强临时行程有变,不想订阅对应的消息了,我们还需要再新增一个移除订阅的方法


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

ticketOffice.remove = function (key, fn) {
let fns = this.clientList[key]
if (!fns) return false

if (!fn) {
fns && (fns.length = 0)
} else {
fns.forEach((cb, i) => {
if (cb === fn) {
fns.splice(i, 1)
}
})
}
}

// --------- 测试数据 --------------
const xiaoGangOn = function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
}
const xiaoQiangOn = function(time){ //小强订阅消息
console.log('小强时间:' + time);
}

ticketOffice.on('上海-深圳', xiaoGangOn);
ticketOffice.on('深圳-上海', xiaoQiangOn);

ticketOffice.remove('深圳-上海', xiaoQiangOn);

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小刚时间:晚上8:10

至此,我们实现了一个相对完善的发布-订阅模式


但是可以看到使用数组进行时间的push和remove可能绑定相同的事件,且事件remove的效率低,我们可以用Set来替换Array


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = new Set();
}
this.clientList[key].add(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

// 移除路线的单个订阅
ticketOffice.remove = function (key, fn) {
this.clientList[key]?.delete(fn)
}
// 移除路线的所有订阅
ticketOffice.removeAll = function (key) {
delete this.clientList[key]
}

// --------- 测试数据 --------------
const xiaoGangOn = function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
}
const xiaoQiangOn = function(time){ //小强订阅消息
console.log('小强时间:' + time);
}

ticketOffice.on('上海-深圳', xiaoGangOn);
ticketOffice.on('深圳-上海', xiaoQiangOn);

ticketOffice.remove('深圳-上海', xiaoQiangOn);

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小刚时间:晚上8:10

参考资料


《JavaScript 设计模式与开发实践》


作者:dudulala
来源:juejin.cn/post/7320075000702533671
收起阅读 »

思辨:移动开发的未来在哪?

前段时间在知乎看到关于移动开发未来的问题,就尝试回答了一下,也触发了我对移动开发未来的思考。移动开发未来怎么样? - 知乎 http://www.zhihu.com/question/61…什么是移动开发?我们口中说的移动开发是什么,从广义和狭义的角...
继续阅读 »

前段时间在知乎看到关于移动开发未来的问题,就尝试回答了一下,也触发了我对移动开发未来的思考。

image.png

移动开发未来怎么样? - 知乎 http://www.zhihu.com/question/61…

什么是移动开发?

我们口中说的移动开发是什么,从广义和狭义的角度分别来看下:

从广义角度来看,移动开发是指为移动设备(如智能手机、平板电脑等)创建软件、应用程序和服务的过程。这包括了为各种移动操作系统(如 iOS、Android 和 Windows Phone)设计、开发、测试和发布应用程序。移动开发旨在为用户提供高质量的、功能丰富的移动体验,以满足其日常需求和娱乐需求。广义上的移动开发可以包括原生应用程序开发、跨平台应用程序开发、移动网页应用程序开发,以及相关的后端服务和API开发等。

从狭义角度来看,移动开发通常指开发针对特定移动操作系统的应用程序,如 iOS 和 Android。这些应用程序通常使用特定于平台的编程语言(如 Swift 或 Kotlin)开发,并利用该平台的特性和功能。狭义的移动开发关注于为特定平台提供最佳的性能、用户体验和原生功能集成。这种开发方法需要对目标平台的技术细节和设计原则有深入了解,以便充分发挥其潜力。

这段内容是我问GPT4的生成的,针对移动开发的定义基本准确。移动开发涉及的的细分领域有非常多,比如:

  • 混合开发和跨平台框架
  • Framework和Kernel
  • 逆向安全
  • 音视频
  • 移动Web
  • 嵌入式

大家可以对照着自己的岗位要求,给自己所涉及的技术领域归个类,分析下市场的需求如何。

简单回顾一下

移动开发辉煌的十年也是移动互联网快速发展的十年,我还记得2015当年o2o百“团”大战的时候,各种创业公司,各行各业,只要你懂点移动开发就能找到不错的开发工作,那时候移动开发的培训机构也如雨后春笋一般诞生,培训个几个月可能就能获得offer。现在的滴滴、美团都是当年烧钱大户,通过庞大的资本,持续打补贴战,最后才活下来,也是寥寥无几的几家独角兽创业公司。通过烧钱的方式毕竟是不可持续的,经营一家公司必须有足够竞争力的产品和可持续的商业模式,当年的泡沫被刺破之后,你才知道什么公司在裸奔,回过头想想现在还有多少家公司能幸存至今呢。

回到今年2023年,疫情三年让整个中国经济都是千疮百孔,不知道大家是否发现这些年基本没有什么新的独角兽出现了,基本上10年前的成立的公司,跑出来成为新的大厂的我们手指头能数得过来,比如我们熟知字节跳动,因为抖音短视频,直接在短视频领域突围成为了打破了老牌大厂腾讯在社交垄断下的新的巨头,成为新的BAT中的B。

另外附上一张2022年中国互联网综合实力企业排名:

image.png

大家是否发现自己手机上常用的App基本集中在这些我们耳熟能详的企业里面。其他的App要么访问量很少,要么永远消失在你的应用列表当中,可叹可惜。所以App的消亡带来的就是移动端的夕阳西下,除了大厂和中厂还有移动客户端的需求,但也是一坑难求,对求职者的要求基本上是要中高级别的,初级的刚毕业的基本上很难拿到offer。

这里我从自己的理解分析了移动开发目前的情况,从历史进程和供需关系,我们可以看到移动开发的求职环境已经大不如前,所以如果还想进入互联网从事移动开发就要结合自身情况去考虑,或许你需要积累得更多才能在残酷的求职环境中脱颖而出拿到心仪的offer。

个人的一些思考

先说说我个人的情况,自从14年毕业之后一直从事移动开发,岗位是Android工程师,基本也算是赶上了移动互联网发展的快车道,求职路上基本上也没遇到什么坎坷,当时也算是比较幸运毕业一年半左右,以社招的身份面试进入到了腾讯,然后就一直待到现在。期间做过研发工具,比如Bugly Crash上报,应用更新和热更新;做过教育产品,比如腾讯课堂;目前投身于金融科技领域,做创新硬件上层应用相关的开发。主要的技术栈还是Android、Java/Kotlin,目前因为业务的需要,技术栈就开始涉足Linux嵌入式和C/C++。其实我个人也一直求变,不管是业务方向还是技术,危机感也在驱使着我去在专业领域获得更多的成长。作为技术人只能保持饥饿感,不停的更新自己的知识体系。

针对移动开发的未来,我个人还是保持谨慎乐观的态度的,虽然当下的求职环境发生了变化,但存量市场需求依然有很多机会,以下是我认为值得我们去关注的技术方向,但不作为任何求职建议:

  1. AIGC+移动端

2023年的AIGC的火热空前绝后,它带来的影响是非常深远的,甚至能够变革整个互联网行业,很多产品可能将会以新的思路去重构和延伸,这里面就会产生相应的在移动端和AIGC结合相关产品和业务,公司层面也会有相应的投入意愿,这也许会给我们带来新的机会。

  1. 元宇宙:VR/AR/XR

元宇宙虽然被炒概念,一直不温不火的,但这里面涉及的技术是比较前沿的,在游戏领域跟元宇宙的结合,如果能找到愿意投入企业,未尝不是一个不错的方向。

  1. IoT物联网

万物互联方向,比如智能家居,智能创新硬件产品,类似小米IoT相关的产品,智能手环、扫地机器人等等。这里面也有庞大的市场需求,另外软硬件结合对开发人员要求更高,更接近底层。

  1. 新能源车载系统

新能源车的其中一个核心就是智能中控,比如特斯拉的中控系统是Linux,比亚迪还有蔚小理和大多数造车新势力用的是Android系统,这里面也有很多车载系统应用的需求,也是很多人都求职热门方向。

  1. 音视频技术领域

当下流行的短视频,涉及到的核心就是音视频技术,有这方面的技术积累的同学应该也能获得不错的发展机会,而且这方面的人才相对而言比较稀缺。

  1. 跨平台技术

从企业降本的角度,未来可能会更倾向招聘懂跨平台开发的,希望能统一技术栈能够实现多端发布的能力。比如Flutter、React Native、UniApp等。

  1. 鸿蒙OS应用开发

国产替代是个很深远的话题,卡脖子问题现在越演越烈,从软件产业我们跟漂亮国还存在很多差距,我们能够正视这些差距并且迎头突围是一个非常值得敬佩和骄傲的事情。鸿蒙OS有望成为第一个完全去Android化的操作系统,Mate60系列手机产品我认为是一个标志性里程碑,我们不谈什么遥遥领先,我相信华为一定会越来越好,鸿蒙OS应用开发也是我觉得有较好前景的方向。

当然还有很多其他技术方向无法一一列举,我个人觉得一专多能可能是未来我们更应追求的目标,仅靠写几个UI页面就能打天下的时代已经不再适用了,想让自己有足够的竞争力,就必须要多涉猎各种技术,打通任督二脉,很多时候单一视角很难获得创新,只有多维度思考才有可能让自己突围。

最后

作为互联网从业人员,保持一定的危机感是必要的,另外多扩展自己的视野,除了专注于本身的专业领域,也要多关注技术趋势的变化,很多时候技术的价值是需要匹配业务的。移动开发有没有未来这个问题可以转化为:我们自己当前要做哪些选择,才能让自己拥有更多的未来。最后跟大家分享一句话作为结尾:

个人努力固然重要,也要考虑历史进程。


作者:巫山老妖
来源:juejin.cn/post/7292347319431790607
收起阅读 »

勇闯体制内00后:丢自己的脸,要领导的命

最近刷到00后在体制内上班,差点没给我人笑没。别的00后忙着整顿职场,而体制内的00后都在出尽洋相。众所周知,体制内工作跟普通的职场不大一样。工作内容比较接地气,工作能力比较看交际。这批进了体制内的00后,看着是端起了铁饭碗,实际上端碗的手,没有一天不在抖。三...
继续阅读 »


最近刷到00后在体制内上班,差点没给我人笑没。


别的00后忙着整顿职场,而体制内的00后都在出尽洋相。


众所周知,体制内工作跟普通的职场不大一样。


工作内容比较接地气,工作能力比较看交际。


这批进了体制内的00后,看着是端起了铁饭碗,实际上端碗的手,没有一天不在抖。


三天一大错两天一小错,每天都在勇闯体制的边缘嘚瑟。

00后前脚上岸,后脚怀疑自己该不会是个原装的傻子。


基本特征是“沉默寡言、体弱多病、孤僻内向且不善交际。”


干活主打的就是一个迷茫,说话前不着村后不着店儿。


领导上一秒说完,他下一秒就忘。



开会的时候他人五人六,把小本摆出来咔咔往上写;


完事说看看你整的会议纪要,他开始阿巴阿巴不敢吱声。



表面奋笔疾书,实际上00后的小本打开是这样的:



还有这样的:



主打一个领导说前门楼子,他在那扯胯骨轴子。


初进了体制内的00后,觉得自己像个傻子,又不只是个傻子;


还有点像腼腆的哑巴,和想努力但就是做不好的笨蛋。



周一上班碰到领导,迎面忘了领导叫啥;


轻则直接摆摆手一个“嗨”,重则四目相对毫无反应,原地飘过去了。


隔天见面敢打招呼,但又记错了领导的职称,张嘴直接给人降了半个级别。


刚进单位时头像还是这种不被重用的傻大姐人设:



后经领导点拨,改成静待花开风格,就算是当笨蛋,不如当个看起来沉稳点的笨蛋:



但头像的玄学作用,在体制内明显受限。


由于太没有眼力价,还不会跟人打交道,部分00后的愚蠢人设还是焊死在身上了。



典型的就是让众多00后显眼包,又爱又恨的酒局修罗场。


爱的是可以把酒局当搂席,恨的是自己酒量真不咋地。


凡开席必须把好吃的端自己跟前,不爱吃的放在领导前面。


前不久有个新闻,某国企会议结束有个晚宴,领导让新来的00后安排;


00后风风火火把晚宴安排到了自己爱的重庆火锅饭店,领导的心情跟怨种特效完美搭配上。


说到酒局,体制里的00后,酒量也不是不行,而是压根就没有,吃饭基本就得坐小孩那桌。



周围人花式敬酒,他低头扒拉米饭。


周围人换上白酒,他拿白水伪装白酒,还拿的热水,满桌子就他一个酒杯里冒热气。


不会喝酒也没事,问题是打进门他一屁股坐到主位上;


别人敬酒寒暄讨好领导,中间还隔着个他这个怨种。



还有的朋友更离谱,领导敬酒他端起饮料,领导低头他把酒往领导鞋上倒。


领导端酒杯致辞,他端个空杯还来回晃荡。


老员工以为这莫不是传说中的00后来整顿职场了?


00后听完把心一沉,想着自己哪有那个心眼子,不过是没有眼力价罢了。


喝酒他不行,但干饭他第一名;


别人吃完半天了,起身前还拍拍他问问吃饱了吗?不行咱就打包。



有的饭局结束了当事人还纳闷,为啥整个晚上自己的饮料杯从来没空过?


后来破案了,副局长全程给他倒了五次豆奶,同事直呼还得是00后牛逼。


不论e人还是i人,进了体制内一律按i人处理。


00后睡前都在给自己洗脑,告诉自己明天会更好;



隔天见了人还是想躲,结果不是去食堂碰到主任,就是去洗澡碰到书记,命运就是如此眷顾,想逃都逃不掉。


为了避免跟领导有眼神接触,有的路过局长办公室,浑身僵硬眼神失焦不敢歪头;


有的在乡镇工作,地方不大还研究躲避路线,真是外向不了一点。


还有的被点名参加合唱比赛,主任问她“你想参加吗?”


她直接用问题回答问题,打了主任一个措手不及“我想参加吗?”


心里其实想的是让我登台献艺,比杀了我还难受。


你永远想象不到00后在体制内是怎么活下来的,毕竟他们这新脑子完全不够用。



不是走廊里走路给老领导一杵子的,就是抬手倒水把领导茶杯盖给碰掉。


职场的打工人上班如上坟,最多就是钱难挣屎难吃;


而体制内的00后,月薪1800拿命往里搭;


每天都觉得脑子有点痒痒的,期待着赶紧长个新脑子吧。

为啥体制内的00后总担心自己闯大祸?


上岸来之不易,两眼一睁,担心竞争。


在某书上搜索体制内的00后,个个都像热锅上的蚂蚁,整天琢磨如何快速适应工作环境。


有人担心单位不让染发,也不能美甲;


有人上网寻求穿搭秘籍,准备放弃穿衣自由,走向局里局气;



有人担心听不明白话,转而研究领导语言习惯和工作中的花式暗语;


也有人按时按点写自己的闯祸日记,有的按天写,有的是周记;

重点记录每日上班遭遇,研究今天丢脸有没有比昨天少那么一点。



偶尔发现隔壁同事姐姐也会把茶水浇到副书记身上,顿时就变得很安心了,看到大家都和自己一样呆呆的,真好啊。


从小科员要掌握的办公字体,到对付老油条停止自我内耗。


再到上传下达“文经我手必熟悉”,硬着头皮记住各种可以提高效率的铁律。


日常给自己加油打气,隔天出了问题立马又泄气。



想起白天犯蠢想到失眠,打开手机又不小心点开高情商问题:


“和领导打羽毛球你赢了,领导说,我老了不中用了,你如何回答。”


看到坐标山东的网友油腻且不失风趣的回答,00后默默赞叹不愧是命里带编,下一秒赶紧把模版熟记于心。


这届进了体制内的00后,一边担心闯祸丢脸,一边又害怕自己过于被边缘;


上班前还以为体制内工作会很清闲,做好了泡壶茶水坐一天的心理预期,结果真上了班发现并不是这么简单。


基层工作跑断腿,总结汇报想流泪,更别说复杂的人际关系,直接让人身心俱疲。


甭管是体制内还是职场里,对刚工作的新人来说,总是最胆战心惊的那个。


不过话又说回来,涉世未深才有资格闯祸;


也只有清澈愚蠢的年轻人,对待人生第一份工作还肯花心思,瞎琢磨。


总有一天,爱闯祸的笨蛋,会变成真正的“大人”。


把头发梳成帅气的模样,在各种场合里游刃有余。


做着曾经最不擅长的事儿,也是最不喜欢的事儿。


或早或晚,都会长大。


眼下不如放轻松,“无论你多早迎接这清晨,在路上,都会有人在。”



作者:英才校园招聘
来源:mp.weixin.qq.com/s/UIKucQDDD5CAglfuTIzqlA

收起阅读 »

史上最全的2024罗振宇跨年演讲思维导图

作者:PMO前沿来源:mp.weixin.qq.com/s/hgB7g_F6ArrPgnAkg0tuyA















作者:PMO前沿
来源:mp.weixin.qq.com/s/hgB7g_F6ArrPgnAkg0tuyA

普通的文本输入框无法实现文字高亮?试试这个highlightInput吧!

web
背景 前几天在需求评审的时候,产品问我能不能把输入框也做成富文本那样,在输入一些敏感词、禁用词的时候,给它标红。我听完心里一颤,心想:好家伙,又来给我整活。本着能砍就砍的求生法则,我和产品说:输入框输入的文字都会被转成字符串,这没办法去标红呀!产品很硬气的回到...
继续阅读 »

背景


前几天在需求评审的时候,产品问我能不能把输入框也做成富文本那样,在输入一些敏感词、禁用词的时候,给它标红。我听完心里一颤,心想:好家伙,又来给我整活。本着能砍就砍的求生法则,我和产品说:输入框输入的文字都会被转成字符串,这没办法去标红呀!产品很硬气的回到:没办法,这是老板提的需求,你下去研究研究吧。行吧,老板发话说啥也没用,开干吧!


实现思路


实现标红就需要给文字加上html标签和样式,但是输入框会将html都转为字符串,既然输入框无法实现,那么我们换一种思路,通过div代替输入框来显示输入的文本,那我们是不是就可以实现文本标红了?话不多说,直接上代码(文章结尾会附上demo):


<div class="main">
<div id="shadowInput" class="highlight-shadow-input"></div>
<textarea
id="textarea"
cols="30"
rows="10"
class="highlight-input"
>
</textarea>
</div>

.main {
position: relative;
}
.highlight-shadow-input {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
padding: 8px;
border: 1px;
box-sizing: border-box;
font-size: 12px;
font-family: monospace;
overflow-y: auto;
word-break: break-all;
white-space: pre-wrap;
}
.highlight-input {
position: relative;
width: 100%;
padding: 8px;
box-sizing: border-box;
font-size: 12px;
background: rgba(0, 0, 0, 0);
-webkit-text-fill-color: transparent;
z-index: 999;
word-break: break-all;
}

实现这个功能的精髓就在于将输入框的背景和输入的文字设置为透明,然后将其层级设置在div之上,这样用户既可以在输入框中输入,而输入的文字又不会展示出来,然后将输入的文本处理后渲染到div上。


const textarea = document.getElementById("textarea");
const shadowInput = document.getElementById("shadowInput");
const sensitive = ["敏感词", "禁用词"];
textarea.oninput = (e) => {
let value = e.target.value;
sensitive.forEach((word) => {
value = value.replaceAll(
word,
`<span style="color:#e52e2e">${word}</span>`
).replaceAll("\n", "<br>");;
});
shadowInput.innerHTML = value;
};

监听输入框oninput事件,用replaceAll匹配到敏感词并转为html后渲染到shadowInput上。此外,我们还需要对输入框的滚动进行监听,因为shadowInput是固定高度的,如果用户输入的文本出现滚动条,则需要让shadowInput也滚动到对应的位置


<div><div id="shadowInput" class="highlight-shadow-input"></div></div>

textarea.onscroll = (e) => {
shadowInput.scrollTop = e.target.scrollTop;
};
// 此处输入时也需要同步是因为输入触底换行时,div的高度不会自动滚动
textarea.onkeydown = (e) => {
shadowInput.scrollTop = e.target.scrollTop;
};

最终实现效果:


至此一个简单的文本输入框实现文字高亮的功能就完成了,上述代码只是简单示例,在实际业务场景中还需要考虑xss注入、特殊字符处理、特殊字符高亮等等复杂问题。


总结


这篇文章主要给遇到有类似业务需求的同学一个参考,以及激发大家的灵感,用这种方法是不是还可以实现一些简单的富文本功能呢?例如文字加删除线、文字斜体加粗等等。有想法或有问题的小伙伴可以在评论区留言一起探讨哦!


demo



作者:宇智波一打七
来源:juejin.cn/post/7295169886177918985
收起阅读 »

多行标签超出展开折叠功能

web
前言  记录分享每一个日常开发项目中的实用小知识,不整那些虚头巴脑的框架理论与原理,之前分享过抽奖功能、签字功能等,有兴趣的可以看看本人以前的分享。  今天要分享的实用小知识是最近项目中遇到的标签相关的功能,我不知道叫啥,姑且称之为【多行标签展开隐藏】功能吧,...
继续阅读 »

前言


 记录分享每一个日常开发项目中的实用小知识,不整那些虚头巴脑的框架理论与原理,之前分享过抽奖功能、签字功能等,有兴趣的可以看看本人以前的分享。
 今天要分享的实用小知识是最近项目中遇到的标签相关的功能,我不知道叫啥,姑且称之为【多行标签展开隐藏】功能吧,类似于多行文本展开折叠功能,如果超过最大行数则显示展开隐藏按钮,如果不超过则不显示按钮。多行文本展开与折叠功能在网上有相当多的文章了,也有许多开源的封装组件,而多行标签展开隐藏的文章却比较少,刚好最近我也遇到了这个功能,所以就单独拿出来与大家分享如何实现。


出处


 【多行标签展开与隐藏】该功能我们平时可能没注意一般在哪里会有,其实最常见的就是各种APP的搜索页面的历史记录这里,下面是我从拼多多(左)和腾讯学堂小程序(右)截下来的功能样式:


多行标签案列图(pdd/txxt)


其它APP一般搜索的历史记录这里都有这个小功能,比如京东、支付宝、淘宝、抖音、快手等,可能稍有点儿不一样,有的是按钮样式,有的是只有展开没有收起功能,可能我们用过了很多年平时都没有注意到这个小功能,有想了解的可以去看一看哈。如果有一天你们需要开发一个搜索页面的话产品就很有可能出这样的一个功能,接下来我们就来看看这种功能我们该如何实现。


功能实现


我们先看实现的效果图,然后再分析如何实现,效果图如下:



【样式一】:标签容器和展开隐藏按钮分开(效果图样式一)


 标签容器和按钮分开的这种样式功能实现起来的话我个人觉得难度稍微简单一些,下面我们看看如何实现这种分开的功能。


第一种方法:通过与第一个标签左偏移值对比实现

原理:遍历每个标签然后通过与第一个标签左偏移值对比,如果有几个相同偏移值则说明有几个换行


具体实现上代码:


<div class="list-con list-con-1">
<div class="label">人工智能div>
<div class="label">人工智能与应用div>
<div class="label">行业分析与市场数据div>
<div class="label">标签标签标签标签标签标签标签标签div>
<div class="label">标签div>
<div class="label">啊啊啊div>
<div class="label">宝宝贝贝div>
<div class="label">微信div>
<div class="label">吧啊啊div>
<div class="label">哦哦哦哦哦哦哦哦div>
div>
<div class="expand expand-1">展开 ∨div>



解析:HTML布局就不用多说了,是个前端都知道该怎么搞,如果不知道趁早送外卖去吧,多说无益,把机会留给其他人。其次CSS应该也是比较简单的,注意的是有个前提需要先规定容器的最大高度,然后使用overflow超出隐藏,这样展开就直接去掉该属性,让标签自己撑开即可。JavaScript部分我这里没有使用啥框架,因为这块实现就是个简单的Demo所以就用纯原生写比较方便,这里我们先获取容器,然后获取容器的孩子节点(这里我们也可以直接通过className查询出所有标签元素),返回的是一个可遍历的变签对象,然后我们记录第一个标签的offsetLeft左偏移值,接下来遍历所有的标签元素,如果有与第一个标签相同的值则累加,最终line表示有几行,如果超过我们最大行数(demo超出2行隐藏)则显示展开隐藏按钮。


第二种方法:通过计算容器高度对比

原理:通过容器底部与标签top比较,如果有top值大于容器底部bottom则表示超出容器隐藏。


具体上代码:




解析:HTMLCSS同方法一同,不同点在于这里是通过getBoundingClientRect()方法来判断,还是遍历所有标签,不同的是如果有标签的top值大于等于了容器的bottom值,则说明了标签已超出容器,则要显示展开隐藏按钮,展开隐藏还是通过容器overflow属性来实现比较简单。


【样式二】:展开隐藏按钮和标签同级(效果图样式二)


 这种样式也是绝大部分APP产品使用的风格,不信你可以打开抖音商城或汽车之家的搜索历史,十个产品九个是这样设计的,不是这样的我倒立洗头。
 这种放在同级的就相对稍微难一点,因为要把展开隐藏按钮塞到标签的最后,如果是隐藏的话就要切割标签展示数量,那下面我就带大家看看我是是如何实现的。


方法一:通过遍历高度判断

原理:同样式一的高度判断一样,通过容器底部bottom与标签top比较,如果有top值大于容器顶部bottom则表示超出容器隐藏,不同的是如何计算标签展示的长度。有个前提是按钮和标签的的宽度要做限制,最好是一行能放一个标签和按钮。


具体实现上代码:


<div id="app3">
<div class="list-con list-con-3" :class="{'list-expand': isExpand}">
<div class="label" v-for="item in labelArr.slice(0, labelLength)">{{ item }}div>
<div class="label expand-btn" v-if="showExpandBtn" @click="changeExpand">{{ !isExpand ? '展开 ▼' : '隐藏 ▲' }}div>
div>
div>


<script>
const { createApp, nextTick } = Vue
createApp({
props: {
maxLine: {
type: Number,
default: 2
}
},
data () {
return {
labelArr: [],
isExpand: false,
showExpandBtn: false,
labelLength: 0,
hideLength: 0
}
},
mounted () {
const labels = ['人工智能', '人工智能与应用', '行业分析与市场数据', '标签标签标签标签标签标签标签', '标签A', '啊啊啊', '宝宝贝贝', '微信', '吧啊啊', '哦哦哦哦哦哦哦哦', '人工智能', '人工智能与应用']

this.labelArr = labels
this.labelLength = labels.length
nextTick(() => {
this.init()
})
},
methods: {
init () {
const listCon = document.querySelector('.list-con-3')
const labels = listCon.querySelectorAll('.label:not(.expand-btn)')
const expandBtn = listCon.querySelector('.expand-btn')

let labelIndex = 0 // 渲染到第几个
const listConBottom = listCon.getBoundingClientRect().bottom // 容器底部距视口顶部距离
for(let i = 0; i < labels.length; i++) {
const _top = labels[i].getBoundingClientRect().top
if (_top >= listConBottom ) { // 如果有标签顶部距离超过容器底部则表示超出容器隐藏
this.showExpandBtn = true
console.log('第几个索引标签停止', i)
labelIndex = i
break
} else {
this.showExpandBtn = false
}
}
if (!this.showExpandBtn) {
return
}
nextTick(() => {
const listConRect = listCon.getBoundingClientRect()
const expandBtn = listCon.querySelector('.expand-btn')
const expandBtnWidth = expandBtn.getBoundingClientRect().width
const labelMaringRight = parseInt(window.getComputedStyle(labels[0]).marginRight)
for (let i = labelIndex -1; i >= 0; i--) {
const labelRight = labels[i].getBoundingClientRect().right - listConRect.left
if (labelRight + labelMaringRight + expandBtnWidth <= listConRect.width) {
this.hideLength = i + 1
this.labelLength = this.hideLength
break
}
}
})
},
changeExpand () {
this.isExpand = !this.isExpand
console.log(this.labelLength)
if (this.isExpand) {
this.labelLength = this.labelArr.length
} else {
this.labelLength = this.hideLength
}
}
}
}).mount('#app3')
script>


解析:同级样式Demo我们使用vue来实现,HTML布局和CSS样式没有啥可说的,还是那就话,不行真就送外卖去比较合适,这里我们主要分析一下Javascript部分,还是先通过getBoundingClientRect()方法来获取容器的bottom和标签的top,通过遍历每个标签来对比是否超出容器,然后我们拿到第一个超出容器的标签序号,就是我们要截断的长度,这里是通过数组的slice()方法来截取标签长度,接下来最关建的如何把按钮拼接上去,因为标签的宽度是不定的,我们要把按钮显示在最后,我们并不确定按钮拼接到最后是不是会导致宽度不够超出,所以我们倒叙遍历标签,如果(最后一个标签的右边到容器的距离right值+标签的margin值+按钮的width)和小于容器宽度,则说明展示隐藏按钮可以直接拼接在后面,否则标签数组长度就要再减一位来判断是否满足。然后展开隐藏功能就通过切换原标签长度和截取的标签长度来完成即可。


方法二:通过与第一个标签左偏移值对比实现

原理:同样式一的方法原理,遍历每个标签然后通过与第一个标签左偏移值对比判断是否超出行数,然后长度截取同方法一一致。


直接上代码:




这里也无需多做解释了,直接看代码即可。


结尾


上面就是【多行标签展开隐藏】功能的基本实现原理,网上相关实现比较少,我也是只用了Javascript来实现,如果可以纯靠CSS实现,有更简单或更好的方法实现可以留言相互交流学。代码没有封装成组件,但是具有一些参考意义,用于生产可以自己去封装成组件使用,完整的代码在我的GitHub仓库。




作者:Liben
来源:juejin.cn/post/7251394142683742269
收起阅读 »

村超,淄博烧烤,哈尔滨,本质都在做情绪价值这门生意

最近哈尔滨火了,人们对它的称呼也从之前的哈尔滨变成了尔滨,一个字就把南北的距离拉近了。 还有什么南方小土豆,在我看来也是挺讲究的,虽然网络人会有一部分人会反感这个称呼,但是大多数人还是比较吃这一套的。 如果叫南方小豆角,南方小冬瓜,我相信大多数人就会急了,因为...
继续阅读 »

最近哈尔滨火了,人们对它的称呼也从之前的哈尔滨变成了尔滨,一个字就把南北的距离拉近了。


还有什么南方小土豆,在我看来也是挺讲究的,虽然网络人会有一部分人会反感这个称呼,但是大多数人还是比较吃这一套的。


如果叫南方小豆角南方小冬瓜,我相信大多数人就会急了,因为这两个词听起来都不可爱,还会有地域歧视的意思。


虽然都是蔬菜,但是南方小土豆就不一样,虽然大家都知道里面有南方人矮的意思,但是大多数人不反感,因为现在很多家长都会叫自己的孩子小土豆,养一个宠物也有叫土豆,所以说土豆,其实带有可爱的意思。


所以就南方小土豆这个称呼,就带动了很大的流量,就打破了这么多年来对东北的刻板映像。


但是就因为一个称呼就能带动那么多的人赶往东北吗?就为了去听一句南方小土豆吗?


我想显然不是,也并不是谁都愿意听的,也有一些游客去以后说反感这个称呼。


那么究其原因,其实本质还是在做情绪价值这门生意。


而情绪价值的背后是什么?


是长久积攒的急需释放的情绪和看透生活后的无能为力。


怎么理解呢?


我发现一个现象,包括我本人也是这样。


在疫情之前,我在很多地方看到有人卖唱,下面的人基本都在听,跟着唱的人不多。


但是近年来,只要有卖唱歌手的地方,基本上大家都会蜂拥上去吼上几句,有甚者直接流着眼泪大声歌唱。


因为大家都从之前的内卷中失望了,生活很大程度上并不会因为努力而发生变化,就不太和自己较真了,从而将重心移到了生活中来。


而市井,热闹就是生活的最真实写照,不需要花多少钱就能释放情绪,收获快乐。


贵州村超淄博烧烤,再到哈尔滨,都能得到很好的体现。


我们还发现一个问题,这几个城市都是比较落后的,其实并没有什么吸引人的地方,景区,经济,文化其实都没有什么突出的地方。


但是有一个特点,那就是消费便宜


你想一下,如果要在香港,澳门,上海这些城市打造这样的活动,做这样的城市IP,现实吗?


我想不现实,因为消费太高,大多数人承受不起。


你想,开一个好一点的酒店都要不少钱,还有吃和也是很贵,加上处于经济高速发展的地区,本地人比较少。


所以情感并不浓,消费并不低。


可能会像村超那样直接免费接游客去自己家里住宿,游客离开后还深情相拥吗?


可能会像哈尔滨这样一到位就一口一个南方小土豆,然后排着队接送吗?


我想基本上不会。


因为多数人的消费能力是有限的,肯定会选择热闹,便宜且好玩的地方。


所以这样的火热IP,大概只会出现在消费相对来说比较低的城市。


所以,现在的生意大多都围绕着提供情绪价值这个方向出发。


前段时间火爆全网的海底捞科目三,虽然海底捞的价格高了一点,但是在你累了,失落了的时候,突然在你面前响起了生日快乐歌,随后又跳起了科目三。


在冰冷的建筑下瞬间热泪盈眶,脑海中蹦出一句:人间值得


要知道,在外面花几百块钱是买不到这种服务的。


而这些服务本质就是提供情绪价值。


特别是在今天这样的现状下,大家兜里都没几个子,生活也都不太如意,所以这时候情绪价值对于一个人来说尤为重要。


所以以后这样火爆的城市IP还会持续出现,这是毋庸置疑的!


作者:苏格拉的底牌
来源:juejin.cn/post/7321943946309124136
收起阅读 »

IT外传:老郑和老钱

正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。 老郑出门去厕所,瞥了一眼旁边测试工程师的屏幕。 他还在整理Excel表格,里面是老郑参与的项目,上面列满了红红的风险点,像是堵车时的尾灯一般。 老郑刚做了一个智能识别的AI项目。这个项目快...
继续阅读 »

正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。



老郑出门去厕所,瞥了一眼旁边测试工程师的屏幕。


他还在整理Excel表格,里面是老郑参与的项目,上面列满了红红的风险点,像是堵车时的尾灯一般。


老郑刚做了一个智能识别的AI项目。这个项目快提交测试时,老郑还在别的项目组干活。这个智能识别干了没几周,又被调走干别的事情了。


即便如此,老郑开发的智能识别项目,在识别率和识别能力上,在整个业内也是领先。这得益于他长久以来的经验积累以及巧妙的算法设计。


但是,大家却不这么看。即便业内识别率只能到50%,老郑做出了85%,但是大家也会盯着那无法识别的15%。


刚刚测试工程师就在整理那15%,他们要把这15%里的100%全部汇报给领导:你看,这些一塌糊涂,这种情况能不能用?请领导定夺。


言外之意:上线后有问题跟我们无关,风险已经全部抛出来了。


其实……老郑也习惯了。


上次另一个识别项目,老郑把准确率从刚提交测试时的50%提高到97%。而测试工程师在给领导汇报时,开头说识别率很差,只有50%。领导很忙,听完这个结论就走了。剩下一些中层,又听了40分钟他是如何通过围追堵截的测试方法一步步将识别率提高的。


大家都没错,也都很辛苦,这些老郑并不关心。老郑的心情很差,因为有一个同事离职了。


老郑的这个同事,技术能力很强,强到一个人可以顶一个团队。


在体力劳动上,一个人顶一队人可能很难。比如普通的劳工一次扛3袋水泥,有个大力士可以一次扛30袋,而且速度还很快。这很罕见。


但是在科技或者软件行业,这种情况很普遍,但是很少有人被认可。


老郑的这个同事老钱,就是这样一个人。


老钱设计的代码,简洁纯净,他擅长使用中间件和编程语言的特性,代替大量的代码逻辑。其代码格式规范、文档注释清晰。也正是得益于简洁和巧妙,他的效率还很高。同样的功能,其他同事需要3个人写两周,老钱1个人一周就能搞定。


对于速度,这顶多算是多扛几袋水泥,在软件行业,这点贡献算不了什么巨大改善。


关键是老钱写的代码很少出问题。程序员写的代码出了问题(bug),会引发后续一堆人的投入。测试同事会测试,前后端要排查、修改,产品经理要做决策,市场要应对用户的投诉。这bug就像是喂鸽子时的粮食,往东边撒一把儿,一群鸽子蜂拥而至,往西边撒一把儿,西边又密密麻麻。


老钱设计的代码,很少有bug,这一点就避免了三四个部门、10多个人白忙活几周的情况。这个隐形成本的节省是巨大的。


除了代码的设计,老钱还有个优势,那就是有远见和守原则。


项目开发中,会面临很多的技术选型和方案选定。大到使用什么框架,小到一个参数选用何种数据类型。


很多的时候,在进行技术讨论时,老钱会对其他人的方案提出建议。比如一个参数不要传来传去,就要以一方为准,否则会出问题。


其他人一般会有自己的理由,比如,传来传去不用给数据库增加额外字段。但是,往往过不了多久,问题就出现了,传着传着就传乱了。于是大家又聚到一起调试:你传给我啥,我收到啥,又传给了他啥……在广场的空地撒了一把粮食,远处的鸽群放弃了旧粮,急忙朝这里飞奔而来。


老郑和老钱也合作过一个项目。老钱曾经建议老郑不要那么搞,否则会出问题。老郑没听,结果后面确实走不通了,最终老郑还是改了回去,那一个星期白干了。


然而,老钱却离职了。


他的离职半含被迫,半含自愿。首先,经济形势不好,导致公司出现了拖延工资的情况。


其次,在拖延工资的背景下,不同员工的发放情况参差不齐。有的人拖延5个月,有的人拖延3个月,有的人正常发放。而老钱的工资,拖得最久,向领导反馈也没有结果。


领导说,公司现在的回款出现延迟,前年该给的钱,去年才刚刚给。不过,每个月也都是有回款的。这点回款,首先要保证公司的日常运作,其次保证新员工,再次保证有贡献的员工。其他人,只能克服一下了。


好像意图比较明显。老钱也是个智商和情商都在线的人。


老钱提出了离职,领导立马批准,限两周内办好手续。老钱说,我原本打算能有1个月缓冲期的。


其实,老钱和老郑早就被投诉多次了。


甚至连人事都看不惯他们:凭什么这俩人工资比我们高,还不拼命加班?我不平衡……不是,他们没有大局意识!


而同为技术人员,兄弟部门的意见就更多了:再复制一份接口,随便改个字段都不配合!群里半夜@你的消息,为什么没有及时回复?我们换个对接人问你问题,你不培训他,让他看接口文档是什么意思!


老钱和老郑有个观念:用工作时间的高效率工作,换取下班后的安心休息。但是,似乎大家并不都想这样,往往是白天静悄悄,只要一下班,工作群里立刻变得活跃起来。


老郑和老钱有时候就讨论,你说领导是否知道一个员工的真实水平或者价值。


比如,A员工干的活能顶B、C、D,3个人。或者,他手下有个员工的水平在整个行业中处于上层还是下层。


“好像不知道!”


老钱说,交接工作期间,有个问题找来,领导还问他:你也参与这个项目了?


其实就上个月,老钱还在这个项目上干了半个月,日报、周报、早会、周会地定期汇报。显然领导没有关注过,因为没有发生过大的问题。一贯零失误的工作,让老钱变成了一个小透明,而且还是经常提意见的那种问题员工。


一线的员工常常辗转于项目代码之中。领导们则开会,看书,制定考核KPI指标。长期脱离一线阵地,会让领导从业务管理上浮到任务管理(从如何带领人解决问题,变成安排人去解决问题)。


软件其实是一门工程学,而非玄学。


软件工程的最佳的实践是多进行工程管理,而非思想管理。


现实很多情况都是反过来的,大家都很重视思想管理。


如果把完成一个软件项目比作攻下一座城池。那么,策略要比士气更重要。


讲策略的将军会规划好完整的攻城计划。首先,他会盘点自己有多少人员和器械,会分析对方城池有几个薄弱点。然后,部署几个分队:哪个队伍扛着云梯往城墙上驾,哪个队伍推着木车从东门撞击。其实,队伍主力要从北门水路强攻。等到把敌方守卫都引到东门时,以山坡黑烟为号,北门发起进攻。最终全面进攻,一举取得胜利。


类比到项目开发中,其实就是各个工种的配合,结构的定义和数据的流转。安排好整个项目每个端口,从上游拿到什么数据,做怎样的处理,然后给下游如何提供。最终,定好流程和时间节点,一气呵成。肯定没法想得完全周到,不过即便有问题,也都是局部问题,整体还是丝滑的。


4cb98f20-a6d4-4872-98a5-51113d85858a.jpeg


讲士气的将军则不然,他不考虑每个环节,或者技术更新太快,他已经不擅长每个环节了。他的主要精力是给士兵做思想工作。他告诫士兵们,我们又要攻打一座城池了,大家要有大局意识,不想当将军的士兵不是好士兵,士兵就是要解决问题的,不是提出问题的。他不关注粮草,不关注器械,不关注目标城池的特征,主要强调大家一定要攻下城池,这是所有人的目标和责任。然后,一声令下,众士兵蜂拥上前,去哪儿的都有。


最后没有攻下城池。将军要求手下将领做复盘,开会讨论为什么没有成功。然后,再次鼓舞大家要有建功立业的雄心。而手下的将领回去也纷纷效仿,告诉士兵们,一定要有建功立业的雄心壮志,遇到问题要解决问题而非吐槽问题,人人都是主人翁,没有粮草你要想办法去搞些粮草。第二次,士兵们又向敌方发起总攻……


这不仅仅体现在软件行业,其他行业也一样,正如一些专家、教授频频发出雷人的言论。


在国内,大家都有上级崇拜。针对如上言论,一般会有人怼你:能当领导的人,必然有过人之处,否则为什么不是你当领导?


其实这句话也没错,还真不是一个把产品做得越好就能生存得越好的环境。


老郑不知道老钱以后会不会改变,正如他不知道自己还能坚持多久。


老钱离职前,曾经问过老郑:老郑,你说那帮“埋头苦干”的年轻人,是以前的我们呢?还是我们的以后呢?


作者:TF男孩
来源:juejin.cn/post/7322356470253731859
收起阅读 »

MyBatis实战指南(二):工作原理与基础使用详解

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。那么,它是如何工作的呢?又如何进行基础的使用呢?本文将带你了解MyBatis的工作原理及基础使用。一、MyBatis的工作原理1.1 MyBatis的工作原理工作原理图示:1、读取...
继续阅读 »

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。那么,它是如何工作的呢?又如何进行基础的使用呢?本文将带你了解MyBatis的工作原理及基础使用。

一、MyBatis的工作原理

1.1 MyBatis的工作原理

工作原理图示:
Description

1、读取MyBatis配置文件

mybatis-config.xml为MyBatis的全局配置文件,配置了MyBatis的运行环境等信息,例如数据库连接信息。

2、加载映射文件(SQL映射文件,一般是XXXMapper.xml)

该文件中配置了操作数据库的SQL语句,需要在MyBatis配置文件mybatis-config.xml中加载。

XXXMapper.xml可以在mybatis-config.xml文件可以加载多个映射文件,每个文件对应数据库中的一张表。

3、构造会话工厂

通过MyBatis的环境等配置信息构建会话工厂SqlSessionFactory。

4、创建会话对象

由会话工厂创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。

5、Executor执行器

MyBatis底层定义了一个Executor接口来操作数据库,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。

6、MappedStatement对象

在 Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。

7、输入参数映射

输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。

8、输出结果映射

输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。

1.2 MyBatis架构

Description

API接口层

提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。

MyBatis和数据库的交互有两种方式:使用传统的MyBatis提供的API、使用Mapper接口。

1)使用传统的MyBatis提供的API

这是传统的传递Statement Id和查询参数给SqlSession对象,使用SqlSession对象完成和数据库的交互;
Description
MyBatis提供了非常方便和简单的API,供用户实现对数据库的增删改查数据操作,以及对数据库连接信息和MyBatis自身配置信息的维护操作。
示例:

SqlSession session = sqlSessionFactory.openSession();
Category c = new Category();
c.setName("新增加的Category");
session.insert("addCategory",c);

上述使用MyBatis的方法,是创建一个和数据库打交道的SqlSession对象,然后根据Statement Id和参数来操作数据库,这种方式固然很简单和实用,但是它不符合面向对象语言的概念和面向接口编程的编程习惯。

2)使用Mapper接口

MyBatis将配置文件中的每一个<mapper>节点抽象为一个Mapper接口,而这个接口中声明的方法和跟<mapper>节点中的<select|update|delete|insert>节点项对应,

即<select|update|delete|insert>节点的id值为Mapper接口中的方法名称,parameterType值表示Mapper对应方法的入参类型,而resultMap值则对应了Mapper接口表示的返回值类型或者返回结果集的元素类型。

示例:

SqlSession session = sqlSessionFactory.openSession();
CategoryMapper mapper = session.getMapper(CategoryMapper.class);
List<Category> cs = mapper.list();
for (Category c : cs) {
System.out.println(c.getName());
}

根据MyBatis的配置规范配置后,通过SqlSession.getMapper(XXXMapper.class)方法,MyBatis会根据相应的接口声明的方法信息,通过动态代理机制生成一个Mapper实例。
Description

使用Mapper接口的某一个方法时,MyBatis会根据这个方法的方法名和参数类型,确定Statement Id,底层还是通过SqlSession.select(“statementId”,parameterObject)或者SqlSession.update(“statementId”,parameterObject)等等来实现对数据库的操作。

MyBatis引用Mapper接口这种调用方式,纯粹是为了满足面向接口编程的需要。

数据处理层

负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。

1)参数映射和动态SQL语句生成

动态语句生成可以说是MyBatis框架非常优雅的一个设计,MyBatis通过传入的参数值,使用OGNL表达式来动态地构造SQL语句,使得MyBatis有很强的灵活性和扩展性。
Description
参数映射指的是对于Java数据类型和JDBC数据类型之间的转换,这里包括两个过程:

  • 查询阶段,我们要将java类型的数据,转换成JDBC类型的数据,通过preparedStatement.setXXX()来设值;

  • 另一个就是对ResultSet查询结果集的JdbcType 数据转换成Java数据类型。

2)SQL语句的执行以及封装查询结果集成List< E>

动态SQL语句生成之后,MyBatis将执行SQL语句并将可能返回的结果集转换成List<E> 。

MyBatis 在对结果集的处理中,支持结果集关系一对多和多对一的转换,并且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询。

基础支撑层

负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。

MyBatis层次结构

Description

1.3 Executor执行器

Executor的类别

Mybatis有三种基本的Executor执行器:SimpleExecutor、ReuseExecutor和BatchExecutor。

Description

1、SimpleExecutor

每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。

2、ReuseExecutor

执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。

3、BatchExecutor

执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

Executor的配置

指定Executor方式有两种:

1、在配置文件中指定

<settings>
<setting name="defaultExecutorType" value="BATCH" />
</settings>

2、在代码中指定

在获取SqlSession时设置,需要注意的时是,如果选择的是批量执行器时,需要手工提交事务(默认不传参就是SimpleExecutor)。

示例:

// 获取指定执行器的sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
// 获取批量执行器时, 需要手动提交事务
sqlSession.commit();

1.4 Mybatis是否支持延迟加载

延迟加载是什么

MyBatis中的延迟加载,也称为懒加载,是指在进行表的关联查询时,按照设置延迟规则推迟对关联对象的select查询。

例如在进行一对多查询的时候,只查询出一方,当程序中需要多方的数据时,mybatis再发出sql语句进行查询,这样子延迟加载就可以的减少数据库压力。

MyBatis 的延迟加载只是对关联对象的查询有迟延设置,对于主加载对象都是直接执行查询语句的。

假如Clazz 类中有子对象HeadTeacher。两者的关系:

public class Clazz {
private Set<HeadTeacher> headTeacher;
//...
}

是否查出关联对象的示例:

@Test
public void testClazz() {
ClazzDao clazzDao = sqlSession.getMapper(ClazzDao.class);
Clazz clazz = clazzDao.queryClazzById(1);
//只查出主对象
System.out.println(clazz.getClassName());
//需要查出关联对象
System.out.println(clazz.getHeadTeacher().size());
}

延迟加载的设置

在Mybatis中,延迟加载可以分为两种:延迟加载属性和延迟加载集合,association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。

在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false

1)延迟加载的全局设置

延迟加载默认是关闭的。如果需要打开,需要在mybatis-config.xml中修改:

<settings>

<setting name="lazyLoadingEnabled" value="true" />

<setting name="aggressiveLazyLoading" value="false"/>
</settings>

比如class班级与student学生之间是一对多关系。在加载时,可以先加载class数据,当需要使用到student数据时,我们再加载 student 的相关数据。

  • 侵入式延迟加载:指的是只要主表的任一属性加载,就会触发延迟加载,比如:class的name被加载,student信息就会被触发加载。

  • 深度延迟加载: 指的是只有关联的从表信息被加载,延迟加载才会被触发。通常,更倾向使用深度延迟加载。

2)延迟加载的局部设置

如果设置了全局加载,但是希望在某一个sql语句查询的时候不适用延时策略,可以配置局部的加载策略。

示例:

 <association
property="dept" select="com.test.dao.DeptDao.getDeptAndEmpsBySimple"
column="deptno" fetchType="eager"/>

etchType值有2种,

  • eager:立即加载;

  • lazy:延迟加载。

由于局部的加载策略的优先级高于全局的加载策略。指定属性后,将在映射中忽略全局配置参数lazyLoadingEnabled,使用属性的值。

延迟加载的原理

MyBatis使用Java动态代理来为查询对象生成一个代理对象。当访问代理对象的属性时,MyBatis会检查该属性是否需要进行延迟加载。

如果需要延迟加载,则MyBatis将再次执行SQL查询,并将查询结果填充到代理对象中。

二、MyBatis基础使用示例

1、添加MyBatis依赖

首先,我们需要在项目中添加MyBatis的依赖。如果你使用的是Maven项目,可以在pom.xml文件中添加以下依赖:

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>

2、创建实体类

假设我们有一个用户表(user),我们可以创建一个对应的实体类User:

public class User {
private int id;
private String name;
private int age;
// getter和setter方法省略
}

3、创建映射文件UserMapper.xml

在MyBatis的映射文件中,我们需要定义一个与实体类对应的接口。例如,我们可以创建一个名为UserMapper的接口:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
<select id="getUserById" parameterType="int" resultType="com.example.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>

4、创建接口UserMapper.java

接下来,我们需要创建一个与映射文件对应的接口。例如,我们可以创建一个名为UserMapper的接口:

package com.example;

import com.example.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User getUserById(@Param("id") int id);
}

5、使用MyBatis进行数据库操作

最后,我们可以在业务代码中使用MyBatis进行数据库操作。例如,我们可以在一个名为UserService的类中调用UserMapper接口的方法:

public class UserService {
public User getUserById(int id) {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUserById(id);
sqlSession.close();
return user;
}
}

总结:

MyBatis是一个非常强大的持久层框架,它可以帮助我们简化数据库操作,提高开发效率。在实际开发中,我们还可以使用MyBatis进行更复杂的数据库操作,如插入、更新、删除等。希望这篇文章能帮助你更好地理解和使用MyBatis。

收起阅读 »

2024律师课程推荐:iCourt律师执行实务集训营(赠《执行实务大礼包》)

律师行业竞争激烈,想要突破困境,就一定要把握蓝海机遇,实现提早布局。如今,还有哪些业务是尚未被“卷起来”的“蓝海业务”?从数据来看,执行业务一定是其中之一。在 Alpha 系统中,以“执行”为关键词检索最近三年的案例,显示有 1,729,5317 条结果;根据...
继续阅读 »

律师行业竞争激烈,想要突破困境,就一定要把握蓝海机遇,实现提早布局。


如今,还有哪些业务是尚未被“卷起来”的“蓝海业务”?


从数据来看,执行业务一定是其中之一。


在 Alpha 系统中,以“执行”为关键词检索最近三年的案例,显示有 1,729,5317 条结果;


根据中国执行信息公开网数据,截止到 12 月 28 日,公布的失信被执行人高达 8,618,870 位;


根据最高法公布的数据显示,得益于全国法院持续推进执行难综合治理、源头治理,仅 2023 年上半年,我国执行到位金额高达 1.2 万亿元,同比增长 23.03% ......



在业内,执行案件是公认的数量多、案件标的大、个案收费高。同时,作为诉讼案件的“最后一公里”,执行能够将司法裁判真正的“落到实处”,维护客户合法利益,实现律师职业价值。因此,一直以来,都有大量律师对执行业务“跃跃欲试”。


但,为什么现实中并没有那么多律师真正从事执行业务?


蓝海是真,“执行难”也是真:线索难找、到位率低、执行周期长、执行程序复杂多变......执行背后的难题让人望而却步。


啃下执行这块“硬骨头”,就能开拓业务范围,拓宽职业路径,在广阔的蓝海中寻找发展新机遇。


基于此,3 月 2 日 - 3 月 3 日,「执行实务集训营 07 期」将在古都西安与各位校友相见。2 天一夜的课程,将对执行实务的疑难问题进行思路点拨和深入解析,以“两个拳头” + “四大利刃”全方位切入执行案件,系统梳理相关法规案例,详细剖析执行专业知识和实务要点。


老赖隐匿财产,当事人无计可施,有哪些途径可以挖掘被执行人财产线索?

处置财产,如何形成更专业化、标准化、流程化的执行案件办理流程?

“借名买房”、“股权代持”等疑难案件该如何拆解破局、一击即中?


经过了 6 期课程的升级迭代和精心打磨,「执行实务集训营 07 期」将直击痛点,打通诉讼最后一公里。


同时,该集训营也将结合刚刚审议通过、即将在 2024 年 7 月 1 日起施行的最新修订的《公司法》内容,对执行相关的新变化、新趋势、新动向进行深入解读和思路点拨。


课程大纲


(一)查找执行财产的三大视角与方法


本次课程将会详细剖析十多种常见与非常见财产类型,并从执行法院、执行律师和当事人三个视角出发,分享查询被执行人财产的途径和方法。


(二)处置被执行人财产的标准化流程


本次课程将通过细致讲解,帮助大家形成自己、团队的执行案件办理流程,以便更好地展现代理执行业务的专业化、标准化、流程化。


(三)执行异议之诉案件的破局之道


本次课程会对案外人执行异议之诉、申请执行人执行异议之诉、追加股东为被执行人异议之诉、执行分配方案异议之诉四个板块进行逐一详细解读。此外,本次课程还将对“借名买房”能否排除强制执行、“股权代持”的隐名股东能否排除强制执行等关联问题进行拆解式分享。


(四)破解执行难的“两个拳头”与“四大利刃”


推进执行案件,常规做法是“两个拳头”:一拳打向的是被执行人财产;另一拳则打向的是被执行人。对于执行案件,除了以上两种常规打法,本次课程还提炼出了破解执行难的“四大利刃”。这些“利刃”并非会用在每一个执行案件中,但却可以成为某个具体执行案件中的“大杀器”,在关键时刻真正做到执行无阻,使命必达。



课程安排




课程讲师




往期现场:


(课程现场)


在两天一夜的课程中,除了干货满满、深入浅出的内容讲解外,校友们还将通过紧张刺激的小组比赛将课程内容串联起来,达到“融会贯通”的效果。


为了团队的荣誉,各个小组直接“卷起来了”,最终的作业展示环节简直“神仙打架”、惊喜连连。经过系统的学习和模拟实践,校友们也对破解执行难题,办理执行案件有了全新的思路和理解。


(比赛环节现场)


北京、武汉、广州...... 2023 年,校友们与「执行实务集训营」共同度过了 6 期的时光。每一期校友们都满载而归、直言不虚此行。该课程也是 2023 年 iCourt 线下集训营参与人数最多、最火爆的课程之一。

(学员合影)


课程资料


除精彩内容外,现在报名集训营,更有「执行实务干货大礼包」全部送送送!


• 1 节线上课程,业务品牌双管齐下

校友报名后均可获得《执行律师如何打造个人品牌》录播课程,方便大家随时随地进行学习提升。课程围绕写作与讲课,这两个法律人必备的技能,结合实务案例,帮助执行律师打造专业品牌,赋能业务开拓。


• 2 套项目模板,标准化办案新思路

随课程赠送《执行与执行异议》、《执行》两套项目模板,涵盖丰富的任务与任务附件,帮助校友规范办案流程,建立团队知识宝库。


• 4 本实务指引,配套学习事半功倍

《执行实务操作指引》( 4.0 版)、《执行实务 108 问》、《执行一本通法律法规汇编》( 2023 版)、《律师代理执行案件 168 个执行步骤》。4 本执行办案实务操作指引,从法律规范、常见问题出发,帮助校友解决服务中的文本困扰。


• 多份文书模板,高效应对执行难题

我们汇总了办案过程中常用的变更申请执行人、查封申请书、到期债务通知书模板等等,希望能够帮助校友提升执行办案效率,高效破解执行难题。

执行虽难,但抽丝剥茧,便能破解难题,实现职业突破。在「执行实务集训营」07 期,拥有丰富执行实务经验的韩锦超老师将带领大家探讨疑难案例,学习方法,启发思路。在前六期的基础上,iCourt 课程中心也对课程内容、课程环节、课程资料、课程体验等环节进行了全面系统的升级,结合最新法律动态,带给大家焕然一新的体验。

机遇往往与挑战并存,机遇也往往留给做足了准备的人。

眼前,是一片可待征服的蓝海。跨过这座大山,是属于我们的广阔的征途。2024 年春节“节后第一课”,「执行实务集训营」将带领大家破解执行难题,挑战业务蓝海。

突破执业困境,把握发展机遇。3 月 2 日 - 3 月 3 日,我们西安见~

了解课程详情:iCourt集训营

原文链接:https://www.icourt.cc/prac-article/728.html

收起阅读 »

探索发展,融合共生|惠州OpenHarmony城市大会圆满举行

1月9日,以“探索发展,融合共生”为主题的OpenHarmony城市大会在惠州隆重举行。本次大会由惠州市工业和信息化局、惠州市政务服务数据管理局、惠州仲恺高新技术产业开发区管理委员会主办,惠州仲恺民营投资集团有限公司、OpenAtomOpenHarmony(简...
继续阅读 »

1月9日,以“探索发展,融合共生”为主题的OpenHarmony城市大会在惠州隆重举行。本次大会由惠州市工业和信息化局、惠州市政务服务数据管理局、惠州仲恺高新技术产业开发区管理委员会主办,惠州仲恺民营投资集团有限公司、OpenAtomOpenHarmony(简称“OpenHarmony”)超高清专委会承办,惠州市电子信息产业协会协办。

(图片:惠州OpenHarmony城市大会现场)

广东省政务数据管理局局领导、一级调研员姚进,广东省工业和信息化厅信息化与软件服务业处副处长陈古典,惠州市政府副秘书长程坤,惠州市工业和信息化局局长廖巍,惠州市政务服务数据管理局局长杨伟斌,仲恺高新区管委会副主任、潼湖生态智慧区党工委副书记、管委会主任汤俊,广东九联科技股份有限公司董事长詹启军,OpenHarmony社区工作委员会执行主席、华为终端BG软件部副总裁柳晓见,鸿蒙生态服务有限公司总经理杜金彪,中国信通院泰尔终端实验室副主任果敢,开放原子开源基金会教育培训部部长王岩广,京东方高级副总裁荆林峰,OpenHarmony生态伙伴企业代表,惠州市OpenHarmony潜在政企客户,超高清领域企业代表,专家学者,高校代表及研究机构代表等300余人出席了本次大会。大会旨在分享及探索OpenHarmony生态发展路径,着力搭建地方产业及OpenHarmony生态领域的交流合作平台,助推OpenHarmony技术创新和生态繁荣。

广东省政务服务数据管理局局领导姚进为大会在开场致辞中强调,广东作为OpenHarmony的发源地,相关工作起步早、优势大,其在开源软件贡献、人才培养、生态应用和政策支持等方面的OpenHarmony生态体系建设上已经位居全国前列。特别指出,惠州在OpenHarmony建设方面走在全省前列,本次大会在惠州市的召开,不仅为广东省乃至全国的OpenHarmony系统发展提供了宝贵的交流平台,也为其他城市发展OpenHarmony提供了典范。未来,广东省政务服务数据管理局将继续通过成立产业协会、制定标准规范以及开展应用示范等措施,全力推动OpenHarmony产业的繁荣壮大。

(图:广东省政务服务数据管理局局领导姚进)

惠州市人民政府副秘书长程坤在致辞中强调,OpenHarmony作为构建智能终端操作系统的重要基础能力平台和安全底座,对于打造自主可控的国产操作系统和构建新的智能终端产业生态具有深远意义。惠州凭借其坚实的电子信息产业基础,积极把握OpenHarmony发展的战略机遇,并已取得了一系列实践成果。例如,支持和指导华为终端、九联科技等企业牵头成立了“OpenHarmony超高清专委会”;印发实施了加快OpenHarmony生态产业发展行动计划,成效初显。程坤表示,惠州将继续强化对OpenHarmony产业的宣传引导、主体培育、示范应用、环境优化和政策谋划等工作,为OpenHarmony生态在惠落地发展提供有力支持。

(图:惠州市人民政府副秘书长程坤)

OpenHarmony打造下一代智能终端操作系统根社区

OpenHarmony社区工作委员会执行主席、华为终端BG软件部副总裁柳晓见分享了OpenHarmony项目及生态进展。目前已有超过220家伙伴加入OpenHarmony生态共建,累计落地超过460款软硬件产品通过OpenHarmony兼容性测评,覆盖金融、教育、交通、医疗、公共安全、智慧城市等多个行业。随着行业标准规范的推进,OpenHarmony已成为各行各业的优选。

同时,深圳、福州、惠州、北京、重庆、南京等城市率先出台相关产业政策支持OpenHarmony发展,从供给侧和需求侧推动生态建设。为培育与产业发展契合的创新型人才,壮大OpenHarmony生态新兴力量,生态伙伴联合高校共同打造人才培养闭环生态链。柳晓见表示,希望OpenHarmony生态伙伴们聚力前行,期待更多伙伴加入OpenHarmony共建中,共筑下一代智能终端操作系统根社区。

(图:OpenHarmony社区工作委员会执行主席、华为终端BG软件部副总裁柳晓见)

OpenHarmony生态服务助力生态商业成功

鸿蒙生态服务公司总经理杜金彪分享了在各地政府大力支持下出台的相关OpenHarmony产业扶持政策,并介绍鸿蒙生态服务公司围绕“行业集采、政府集采、政府奖补、联盟标准”四大商业机遇,开展测评认证、市场拓展、商机对接、活动承办等服务,协助生态伙伴实现降本增效。通过搭建相关联盟平台推动产业标准建设,促进良性竞争。同时也期待更多伙伴加入,共同加速OpenHarmony生态产业健康发展。

(图:鸿蒙生态服务公司总经理杜金彪)

两位特邀嘉宾专题分享完毕后,在参会嘉宾的共同见证下,举办了OpenHarmony超高清专委会揭牌仪式、OpenHarmony人才培养战略行动启动仪式。本地政府、院校、企业等多方力量表示,将全力以赴支持OpenHarmony的发展,共同推动其在更多领域的应用和普及。此次相关仪式的成功举行,为惠州市OpenHarmony生态的发展开启了新的篇章。

OpenHarmony超高清专委会揭牌仪式

2022年,在开放原子开源基金会的指导下,在惠州市政府、惠州仲恺高新区管委会的支持下,由华为终端有限公司、广东九联科技股份有限公司、惠州开鸿数字产业发展有限公司牵头推动成立了“OpenHarmony超高清专委会”(OpenHarmony生态委员会下属负责推进OpenHarmony在超高清领域生态发展的唯一主体)。专委会将面向超高清全行业,推动 OpenHarmony的研发、装机、应用等工作,打造OpenHarmony超高清生态圈。

(图:OpenHarmony超高清专委会揭牌仪式合影)

OpenHarmony人才培养战略行动启动仪式

由开放原子开源基金会、惠州学院、惠州市技师学院、惠州城市职业学院、惠州经济职业技术学院、惠州工程职业学院、深圳技术大学、广东九联科技股份有限公司、惠州开鸿数字产业发展有限公司(OpenHarmony超高清专委会秘书处单位)共同开启OpenHarmony人才培养战略行动启动仪式,承诺合力开展OpenHarmony新型操作系统的产业及技术培训等相应的活动,为企业培养更多的实用性、复合型的软件人才。仪式结束后,大会邀请了OpenHarmony生态专家及伙伴进行主题演讲。

(图:OpenHarmony人才培养战略行动启动仪式合影)

OpenHarmony的基本设计理念和关键技术最新进展

OpenHarmony项目管理委员会(PMC)主席任革林分享了OpenHarmony的基本设计理念和关键技术最新进展,并介绍了基于OpenHarmony使能的金融、智慧教室、智能化公路建设等一系列行业场景创新解决方案。他表示,OpenHarmony技术底座能力越来越成熟,一个面向全场景、全连接、全智能时代OS的阶段目标已经达成,既可以满足生态伙伴开发丰富多彩的创新设备,也可以满足应用开发者开发复杂大型应用和极致高性能应用。

(图:OpenHarmony项目管理委员会主席任革林)

深开鸿基于OpenHarmony高校人才培养实践

深圳开鸿数字产业发展有限公司(简称“深开鸿”)OpenHarmony社区开发部总经理、OpenHarmony项目管理委员会(PMC)成员巴延兴分享了深开鸿基于OpenHarmony高校人才培养实践。作为一家立足于OpenHarmony生态,为行业数字化、智慧化提供基础软件的生态平台型企业,深开鸿积极响应国家切实推动“深化产教融合、校企人才共育”的号召,与北京理工大学、哈尔滨工业大学、东南大学、深圳信息职业技术学院、深圳技术大学等众多高校开展合作,计划在未来几年内培养大量优秀的OpenHarmony技术人才。

(图:深开鸿OpenHarmony社区开发部总经理、OpenHarmony项目管理委员会成员巴延兴)

基于OpenHarmony的全场景解决方案实践

江苏润开鸿数字科技有限公司(简称“润开鸿”)生态技术总监、OpenHarmony社区龙芯架构SIG组长连志安分享了润开鸿基于OpenHarmony面向行业的HiHopeOS发行版使能千行百业的场景创新解决方案及商业落地实践,并重点介绍了基于龙芯+OpenHarmony的适配进展及工业场景探索。

(图:润开鸿生态技术总监、OpenHarmony社区龙芯架构SIG组长连志安)

新功能,新形态,新场景

京东方科技集团股份有限公司视觉艺术事业部总经理吴坚围绕京东方集团业务新功能、新形态、新场景展开讨论,探讨京东方屏之物联与OpenHarmony结合所带来的创新与变革。

(图:京东方视觉艺术事业部总经理吴坚)

基于OpenHarmony的Nearlink(星闪)赋能智能空间实践探索

广东九联科技股份有限公司产品研究院院长、广东九联开鸿科技发展有限公司CEO钟义秀分享了基于OpenHarmony的Nearlink(星闪)赋能智能空间实践探索。围绕OpenHarmony和星闪的特性在智慧空间场景中应用以及场景中特性闭环,低时延、近场联接联动让分布式空间场景无处不在。

(图:广东九联科技股份有限公司产品研究院院长、广东九联开鸿科技发展有限公司CEO钟义秀)

在盛大的OpenHarmony城市大会上,我们见证了科技与生活的深度融合,感受到了开源文化与创新精神的激情碰撞。通过深入的探讨与交流,我们更加坚信,OpenHarmony所倡导的开放、共享、互联的理念,将引领我们迈向一个更加智能、高效、和谐的美好未来,共同开创一个万物智联、万物互融的新时代。让我们携手并进,以“探索发展,融合共生”的精神,为开源生态的繁荣与辉煌而努力!

(图:惠州OpenHarmony城市大会现场)

OpenHarmony是由开放原子开源基金会(OpenAtomFoundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代、基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。OpenHarmony开源三年多来,社区快速成长,版本已迭代到OpenHarmony 4.1beta1 Release,超过 6700 名共建者、70家共建单位,贡献代码行数超过1亿行。截至2023年底,OpenHarmony开源社区已有250多家生态伙伴加入,OpenHarmony项目捐赠人达35家,通过OpenHarmony兼容性评测的伙伴达170余个,累计落地230余款商用设备,涵盖金融、教育、智能家居、交通、数字政府业、医疗等各个领域,OpenHarmony已成为下一代智能终端操作系统根社区。

收起阅读 »

2024年,现在去开发一款App需要投入多少资金?

前言 本文主要探讨跨平台应用的开发成本,原生与小程序不在探讨范围之内,为什么呢?请接着往下看~ 选择大于努力 原生开发的现状 先来看下目前原生开发存在的问题以及国内的现状。 开发人员的人力成本相对高于跨平台开发人员,对于纯原生的项目,企业通常需要招两个端的开...
继续阅读 »

1.jpg


前言


本文主要探讨跨平台应用的开发成本,原生与小程序不在探讨范围之内,为什么呢?请接着往下看~


选择大于努力


原生开发的现状


先来看下目前原生开发存在的问题以及国内的现状。



  1. 开发人员的人力成本相对高于跨平台开发人员,对于纯原生的项目,企业通常需要招两个端的开发人员。这也是导致很多企业不愿意选择原生开发的重要原因之一(Android、iOS)。

  2. 原生应用开发成本高,开发周期慢,如果不招人(提高用人成本)较难跟上市场节奏。

  3. 原生应用推广成本也高(与小程序相对比)。

  4. 对于我们开发人员来说,需要掌握一种语言Java或者kotlin,ios开发需要oc或者swift,难度相对于跨平台学习成本较高。


企业对于技术上的选择,目前需要的就是能节省成本、同时开发效率高的,跨平台已经是大势所趋


国内特有的小程序


小程序的优势很大,自从小程序出来后,蚕食了很大一部分手机应用的市场份额。


小程序相较于原生应用具有显著的优势,其中最大的优势在于成本的降低。相比于开发原生应用,小程序的开发成本更低,同时也更加省时省力。此外,小程序还能够充分利用微信等大型平台的庞大用户流量入口,从而降低企业在推广方面的成本。这种降低成本的好处不仅体现在企业推广方面,也使得用户在使用小程序时所需投入的成本降低(不用去下载app,也不用再走一遍注册流程)。


正因为如此,许多中小企业不愿再开发原生应用,或者说,“没能力”开发原生应用。更倾向于选择小程序。小程序的低成本开发和推广,使得中小企业能够以更少的投入获得更大的回报。此外,小程序还可以借助微信等平台的用户基础,更容易吸引和留住用户。


如何计算一款App开发的成本


本文选择跨平台技术作为开发成本的参考。那在跨平台中,从Statista(一家全球领先的统计数据平台和市场研究公司)收集的数据来看,很明显 Flutter 继续脱颖而出,成为跨平台框架中的首选。截止2023年6月,Flutter占跨平台份额的46%,在跨平台中占比第一,React Native占32%,居第二。



长话短说,开发 Flutter 应用程序的相关费用基本在 10,000 到 450,000 人民币之间,甚至更高。在这本文中,我们将分解各种成本因素,去计算 Flutter 应用程序开发成本。


那如何去计算一款应用的开发成本呢,开发一款应用一共分一下几个阶段,每个阶段都会影响总成本。



  • 第一阶段:需求分析与规划

  • 第二阶段:原型设计(UI/UX)

  • 第三阶段:正式编码(此时应用已经基本成型)

  • 第四阶段:测试

  • 第五阶段:部署与维护


Flutter技术在国内多用于外包项目,所以通常三四五(有些项目会包含二)的几个阶段都由开发者全权负责完成,应用的总体成本通常是通过将总工时乘以开发者的小时费来估算的。


影响一款应用开发成本的因素


不同的应用开发的成本可能会因多种因素而有很大差异,每个因素都会直接影响项目的预算和时间表。最终价格可能受到一系列因素的影响,例如应用程序的复杂程度、要纳入的功能总数、开发人员的每小时费以及许多其他方面。


主要的因素也是对应到开发阶段中,主要是以下这些:



  • 在需求分析时,应用程序的范围和复杂性

  • 在UI设计时,UI的动画、复杂的布局、对设计风格的要求

  • 在开发时,选择的开发方式(1.外包给自由职业者。2.外包给专业软件公司。3.自己招人干)。选择外包开发者(开发商)的地理位置,假设你在美国,找一个中国开发者,成本就会降低许多

  • 在测试时,跨平台的设备成本,功能测试的范围

  • 部署维护时,服务器的成本,bug的修复,添加新的功能


那让我们再来详细聊聊每个阶段具体要花多少费用。


需求分析设计阶段


项目的需求和范围是开发成本的主要决定因素,例如,开发一个基本的笔记应用程序比开发一个功能齐全的电商平台便宜得多。因此,在App开发的初始阶段定义项目需求和应用程序复杂性对于估算总体成本至关重要。App在刚开始需要舍弃掉一些不重要的功能。


UI设计阶段


如果有一个高质量的 UI/UX 设计,那对于App的成功是很有帮助的。但它也会影响成本,一款简单、简约的设计比具有独特图形、复杂动画动画的定制成本更低。如果需要高度定制的设计或想要实现特定的品牌元素,这将极大增加的应用程序开发成本。根据应用程序的复杂程度,设计一款完整的App平均需要 40 到 90 多个小时。设计一款App的UI,价格平均在5000-25000左右,让我们对应到每项工作中去。



  1. 前期的需求交流和沟通。此阶段涉及创建草图和线框图。所需的时间和成本取决于设计的复杂程度。创建草图和线框图可能需要 200 至 1000 的预算分配

  2. UI/UX 设计视觉效果的创建。 此阶段为整个App的内容设计,例如登录界面、注册界面等。同样,实际所需时间取决于App的复杂性。此阶段的预算范围从 5,000 到 15,000或更多

  3. logo设计。在这个阶段阶段,设计师根据之前设计的App内容和、我们的品牌配色和其他设计元素。这项共工作需要相当大的预算,大约需要 5,000 到 10,000 的预算甚至更多。当然,为了节省成本也可以放弃这一阶段,由我们自己设计


代码开发阶段


选择不同的开发人员或开发团队也会影响成本。如果选择经验丰富的专业人员团队会花费更多的前期成本,但可以带来更高的效率和更高质量的产品。如果,雇用经验不足的开发人员刚开始可能会省钱,但可能会导致开发时间更长或日后出现潜在问题。目前主流的方式为以下三种:


自由职业者(外包给程序员做私活)


这种方式可以很好的降低成本,身边也有很多朋友会接私活,确实是一个很不错的选择。但是,这种方式可能会遇到许多不确定性,例如没法按时交付。此外,如果这个项目后期需要进行维护、更新,那这个方案可能就不是最可靠选择了,因为他们可能会转移到其他项目(或者跑路),从而使持续协作变得具有挑战性。如果选择这个方案,建议是朋友推荐,或者是网上具有一定知名度的开发者。在国内,跨平台应用开发者(Flutter开发)的时薪通常在每小时150到350人民币不等。如果选择这个方式,开发成本在10000到50000之间。


外包公司


这种方法是节省开发资金而又不影响产品质量的绝佳方法,通常开发成本在50000到150000之间。如果项目需要后期的维护,迭代,那么可以优先选择这样的方式。(现在的外包公司也比较卷)


自己组团队


如果是想要真的以一种创业的方式,那么开发成本的范围是0到无上限。如果自身就是一个技术人员,那么只需要一台笔记本就可以完成对应用的开发,所花的只是时间成本。如果要招人组团队,那成本就不可估计了。


测试阶段


这部分在大多数App开发过程中,已经由开发者自己测试解决的。稍微正规些的应用可以将测试的工作外包给测试公司。成本在0~20000人民币之间。


维护与迭代


开发一款App不是短跑,而更像是一场马拉松。即使在App第一版上线后,这个旅程仍在继续。定期更新、bug修复和UI修改只是维护App的冰山一角。最好预留总成本的 15-20% 的额外费用,用来进行维护。


其他因素


——每个项目都是独特的,具体要求将决定最终成本。因此,在规划App开发预算时,必须彻底了解这些因素并加以考虑。


第三方API集成


如果项目中需要集成即时通讯等功能模块,那么第三方API集成的这部分的花销也是不可忽略。


软著申请、应用商店发布


软著申请是免费的,自行准备材料申请即可,但是通常会有2~3个月的时间,才能申请成功。如果想快速申请,可以找专门的三方申请机构,价格在500-2000左右。如果App需要上架Google Play和App Store,那么,开通Google Play 开发者账户一次性收取 25 美元费用,Apple Store 个人开发者账号每年收取 99 美元费用。此外,还会从应用内购买或订阅中扣除部分费用。申请软著和App上架的材料准备工作,通常需要10-20小时的工作。按每小时50元,此部分工作需要500-1000元的费用。


后端开发和服务器的费用


如果App只会进行一些本地操作,那么这部分的费用基本为0。如果需要后端提供服务,则需要在拿出一大笔钱进行后端的开发和服务器的购买费用。


如何降低开发成本


外包项目


这种模式允许利用全球人才库,通常以比雇用本地人才更具竞争力的价格获得服务。这点如果你在美国等发达国家可以考虑。如果在大陆,可以看看三哥他们。此外,这种方式还减少了对办公空间和设备的需求,并减少了与员工福利和津贴相关的管理费用。


明确项目要求


还是那句话,最后的成本一定与开始的需求有着很大关联。所以一定要精简需求,明确App到底要做什么。


专注于敏捷方法


如果你是个人开发者或者要带领团队开发,那一定要注重敏捷开发,确定任务优先级、经常重新评估和调整项目目标。


结论 — 关于开发一款App的成本


关于开发一款App的成本,为了让大家能更直观的感受,让我们具体数字来说明这一点。(采用Flutter跨平台)



  1. 对于简单功能的App(例如提供膳食计划App、日记App、记账App等),估计开发成本约为 10,000 — 50,000人民币之间,根据项目的复杂度来决定。

  2. 对于中等复杂度的App(例如具有即时通讯、语音通话等功能)预计成本约为 50,000 — 150,000人民币之间。

  3. 对于开发高复杂度的应用,例如抖音(简化版,真抖音现在哪个团队能从0开始做一个...),起价基本在150,000,上不封顶。


那这就是当前开发一款App的成本,以及对应的工作。


免责声明:本文中提供的数字是大致的、调研来的,可能会根据具体项目要求而有所不同!!!


作者:编程的平行世界
来源:juejin.cn/post/7312353213348347916
收起阅读 »

ThreadLocal:你不知道的优化技巧,Android开发者都在用

引言 在Android开发中,多线程是一个常见的话题。为了有效地处理多线程的并发问题,Android提供了一些工具和机制。其中,ThreadLocal是一个强大的工具,它可以使得每个线程都拥有自己独立的变量副本,从而避免了线程安全问题。 本文将深入探讨Andr...
继续阅读 »

引言


Android开发中,多线程是一个常见的话题。为了有效地处理多线程的并发问题,Android提供了一些工具和机制。其中,ThreadLocal是一个强大的工具,它可以使得每个线程都拥有自己独立的变量副本,从而避免了线程安全问题。


本文将深入探讨Android中的ThreadLocal原理及其使用技巧, 帮助你更好的理解和使用ThreadLocal


ThreadLocal的原理


public class Thread implements Runnable {

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */

ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal的原理是基于每个线程都有一个独立的ThreadLocalMap对象。ThreadLocalMap对象是一个Map,它的键是ThreadLocal对象,值是ThreadLocal对象保存的值。


public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

当我们调用ThreadLocalset()方法时,会将值存储到当前线程的ThreadLocalMap对象中。当我们调用ThreadLocalget()方法时,会从当前线程的ThreadLocalMap对象中获取值。


ThreadLocal的使用


使用ThreadLocal非常简单,首先需要创建一个ThreadLocal对象,然后通过setget方法来设置和获取线程的局部变量。以下是一个简单的例子:


val threadLocal = ThreadLocal<String>()

fun setThreadName(name: String) {
threadLocal.set(name)
}

fun getThreadName(): String {
return threadLocal.get() ?: "DefaultThreadName"
}

Android开发中,ThreadLocal的使用场景非常多,比如:



  • Activity中存储Fragment的状态

  • Handler中存储消息的上下文

  • RecyclerView中存储滚动位置


实际应用场景


// 在 Activity 中存储 Fragment 的状态
class MyActivity : AppCompatActivity() {

private val mFragmentState = ThreadLocal<FragmentState>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)

// 获取 Fragment 的状态
val fragmentState = mFragmentState.get()
if (fragmentState == null) {
// 初始化 Fragment 的状态
fragmentState = FragmentState()
}

// 设置 Fragment 的状态
mFragmentState.set(fragmentState)

// 创建 Fragment
val fragment = MyFragment()
fragment.arguments = fragmentState.toBundle()
supportFragmentManager.beginTransaction().add(R.id.container, fragment).commit()
}

}

class FragmentState {

var name: String? = null
var age: Int? = null

fun toBundle(): Bundle {
val bundle = Bundle()
bundle.putString("name", name)
bundle.putInt("age", age)
return bundle
}

}

这段代码在Activity中使用ThreadLocal来存储Fragment的状态。当Activity第一次启动时,会初始化Fragment的状态。当Activity重新启动时,会从ThreadLocal中获取Fragment的状态,并将其传递给Fragment


注意事项



  • 内存泄漏风险:


ThreadLocal变量的生命周期与线程的生命周期是一致的。这意味着,如果一个线程一直不结束,那么它所持有的ThreadLocal变量也不会被释放。这可能会导致内存泄漏。


为了避免内存泄漏,我们应该在不再需要ThreadLocal变量时,显式地将其移除。


threadLocal.remove()


  • 不适合全局变量: ThreadLocal适用于需要在线程间传递的局部变量,但不适合作为全局变量的替代品。


优化技巧



  • 合理使用默认值: 在获取ThreadLocal值时,可以通过提供默认值来避免返回null,确保代码的健壮性。


fun getThreadName(): String {
return threadLocal.get() ?: "DefaultThreadName"
}


  • 懒加载初始化: 避免在声明ThreadLocal时就初始化,可以使用initialValue方法进行懒加载,提高性能。


val threadLocal = object : ThreadLocal<String>() {
override fun initialValue(): String {
return "DefaultValue"
}
}


  • 尽量避免在ThreadLocal中保存大对象


结论


在本文中,我们介绍了ThreadLocal的原理和使用技巧,希望这些知识能够帮助你更好地理解和使用它。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


作者:午后一小憩
来源:juejin.cn/post/7317859658285858842
收起阅读 »

Android 通知文本颜色获取

前言 Android Notification 几乎每个版本都有改动,因此有很多兼容性问题,摆在开发者面前的难题是每个版本的展示效果不同,再加app保活能力的日渐式微和google大力推进WorkManager、JobScheduler、前台进程的行为,即便A...
继续阅读 »

前言


Android Notification 几乎每个版本都有改动,因此有很多兼容性问题,摆在开发者面前的难题是每个版本的展示效果不同,再加app保活能力的日渐式微和google大力推进WorkManager、JobScheduler、前台进程的行为,即便AlarmManager#setAlarmClock这种可以解除Dozen模式的超级工具,也无法对抗进程死亡的问题,通知到达率和及时性的效果已经大幅减弱。


Screenshot_20190403-131551.png


自定义通知是否仍有必要?

实际上,目前大多推送通知都被系统厂商代理展示了,导致实现效果雷同且没有新意。众多一样的效果,站在用户角度也产生了很多厌恶情绪,对用户的吸引点也是逐渐减弱,这其实和自定义通知的初衷是相背离的,因为自定义通知首要解决的是特色功能的展示,而通用通知却很难做到这一点。因此,在一些app中,自定义通知仍然是有必要的,但必要性没有那么强了。


当前的使用场景:



  • 前台进程常驻类型app,比如直播、音乐类等

  • 类似QQ的app浮动弹窗提醒 (这类不算通知,但是可以使用统一的方法适配)

  • 系统白名单中的app


现状


通知首要解决的是功能问题,其次是主题问题。当前,大部分app已经习惯使用系统通知栏而不使用自定义的通知,主要原因是适配难度问题。


对于自定义通知的适配,目前有两条路线:



  • 统一样式:

    是定义一套深色模式和浅色模式都能通用的色彩搭配,一些音视频app也是这么做的,巧妙的避免了因系统主题不一致造成的现实效果不同的问题,但仍然在部分手机上展示的比较突兀。

  • 读取系统通知颜色进行适配:

    遗憾的是,在Android 7.0之后,正常的通知是拿不到notification.contentView,但似乎没有看到相关的文章来解决这个问题。


两种方案可以搭配使用,但方案二目前存在无法提取颜色的问题,关键是怎么解决contentView拿不到的问题呢?接下来我们重点解决方案二的这个问题。


问题点


我们在无论使用NotificationBuilder或者NotificationCompatBuilder,其内部的build方法存在targetSdkVersion的判断,而在大于Android 7.0 的版本中,不会立即创建ContentView


protected Notification buildInternal() {
if (Build.VERSION.SDK_INT >= 26) {
return mBuilder.build();
} else if (Build.VERSION.SDK_INT >= 24) {
Notification notification = mBuilder.build();

if (mGr0upAlertBehavior != GR0UP_ALERT_ALL) {
// if is summary and only children should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) != 0
&& mGr0upAlertBehavior == GR0UP_ALERT_CHILDREN) {
removeSoundAndVibration(notification);
}
// if is group child and only summary should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) == 0
&& mGr0upAlertBehavior == GR0UP_ALERT_SUMMARY) {
removeSoundAndVibration(notification);
}
}

return notification;
} else if (Build.VERSION.SDK_INT >= 21) {
mBuilder.setExtras(mExtras);
Notification notification = mBuilder.build();
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}
if (mHeadsUpContentView != null) {
notification.headsUpContentView = mHeadsUpContentView;
}

if (mGr0upAlertBehavior != GR0UP_ALERT_ALL) {
// if is summary and only children should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) != 0
&& mGr0upAlertBehavior == GR0UP_ALERT_CHILDREN) {
removeSoundAndVibration(notification);
}
// if is group child and only summary should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) == 0
&& mGr0upAlertBehavior == GR0UP_ALERT_SUMMARY) {
removeSoundAndVibration(notification);
}
}
return notification;
} else if (Build.VERSION.SDK_INT >= 20) {
mBuilder.setExtras(mExtras);
Notification notification = mBuilder.build();
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}

if (mGr0upAlertBehavior != GR0UP_ALERT_ALL) {
// if is summary and only children should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) != 0
&& mGr0upAlertBehavior == GR0UP_ALERT_CHILDREN) {
removeSoundAndVibration(notification);
}
// if is group child and only summary should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) == 0
&& mGr0upAlertBehavior == GR0UP_ALERT_SUMMARY) {
removeSoundAndVibration(notification);
}
}

return notification;
} else if (Build.VERSION.SDK_INT >= 19) {
SparseArray<Bundle> actionExtrasMap =
NotificationCompatJellybean.buildActionExtrasMap(mActionExtrasList);
if (actionExtrasMap != null) {
// Add the action extras sparse array if any action was added with extras.
mExtras.putSparseParcelableArray(
NotificationCompatExtras.EXTRA_ACTION_EXTRAS, actionExtrasMap);
}
mBuilder.setExtras(mExtras);
Notification notification = mBuilder.build();
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}
return notification;
} else if (Build.VERSION.SDK_INT >= 16) {
Notification notification = mBuilder.build();
// Merge in developer provided extras, but let the values already set
// for keys take precedence.
Bundle extras = NotificationCompat.getExtras(notification);
Bundle mergeBundle = new Bundle(mExtras);
for (String key : mExtras.keySet()) {
if (extras.containsKey(key)) {
mergeBundle.remove(key);
}
}
extras.putAll(mergeBundle);
SparseArray<Bundle> actionExtrasMap =
NotificationCompatJellybean.buildActionExtrasMap(mActionExtrasList);
if (actionExtrasMap != null) {
// Add the action extras sparse array if any action was added with extras.
NotificationCompat.getExtras(notification).putSparseParcelableArray(
NotificationCompatExtras.EXTRA_ACTION_EXTRAS, actionExtrasMap);
}
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}
return notification;
} else {
return mBuilder.getNotification();
}
}

那么我们怎么解决这个问题呢?


Context Wrapper


在App 开发中,Context Wrapper是常见的事情,比如用在预加载Layout、模拟Service运行、插件加载等方面有大量使用。


本文思路是要hack targetSdkVersion,但targetSdkVersion是保存在ApplicationInfo中的,不过没关系,它是通过Context获取的,因此我们在它获取前将其修改为android 5.0的不就行了?


为什么可以修改ApplicationInfo,因为其事Parcelable的子类,看到Parcleable的子类你就能明白,该类的修改是不会触发系统服务的调度,但会影响部分功能,安全起见,我们可以拷贝一下。


public class NotificationContext extends ContextWrapper {
private Context mContextBase;
private ApplicationInfo mApplicationInfo;
private NotificationContext(Context base) {
super(base);
this.mContextBase = base;
}

@Override
public ApplicationInfo getApplicationInfo() {
if(mApplicationInfo!=null) return mApplicationInfo;
ApplicationInfo applicationInfo = super.getApplicationInfo();
mApplicationInfo = new ApplicationInfo(applicationInfo);
return mApplicationInfo;
}

public static NotificationContext from(Context context) {
return new NotificationContext(context);
}
}

targetSdkVersion hack


下一步,修改targetSdkVersion 为android 5.0版本


NotificationContext notificationContext = NotificationContext.from(context);
ApplicationInfo applicationInfo = notificationContext.getApplicationInfo();
int targetSdkVersion = applicationInfo.targetSdkVersion;

applicationInfo.targetSdkVersion = Math.min(21, targetSdkVersion);

完整的代码


要获取的属性


class NotificationResourceInfo {
String titleResourceName;
int titleColor;
float titleTextSize;
ViewGr0up.LayoutParams titleLayoutParams;
String descResourceName;
int descColor;
float descTextSize;
ViewGr0up.LayoutParams descLayoutParams;
long updateTime;

}

获取颜色,用于判断是不是深色模式,这里其实利用的是标记查找方法,先给标题和内容设置Text,然后查找具备此Text的TextView


private static String TITLE_TEXT = "APP_TITLE_TEXT";
private static String CONTENT_TEXT = "APP_CONTENT_TEXT";

下面是核心查找逻辑


  //遍历布局找到字体最大的两个textView,视其为主副标题
private <T extends View> T findView(ViewGr0up viewGr0upSource, CharSequence locatorTextId) {

Queue<ViewGr0up> queue = new ArrayDeque<>();
queue.add(viewGr0upSource);
while (!queue.isEmpty()) {
ViewGr0up parentGr0up = queue.poll();
if (parentGr0up == null) {
continue;
}
int childViewCount = parentGr0up.getChildCount();
for (int num = 0; num < childViewCount; ++num) {
View childView = parentGr0up.getChildAt(num);
String resourceIdName = getResourceIdName(childView.getContext(), childView.getId());
Log.d("NotificationManager", "--" + resourceIdName);
if (TextUtils.equals(resourceIdName, locatorTextId)) {
Log.d("NotificationManager", "findView");
return (T) childView;
}
if (childView instanceof ViewGr0up) {
queue.add((ViewGr0up) childView);
}

}
}
return null;

}

NotificationThemeHelper 实现


public class NotificationThemeHelper {
private static String TITLE_TEXT = "APP_TITLE_TEXT";
private static String CONTENT_TEXT = "APP_CONTENT_TEXT";

final static String TAG = "NotificationThemeHelper";
static SoftReference<NotificationResourceInfo> notificationInfoReference = null;
private static final String CHANNEL_NOTIFICATION_ID = "CHANNEL_NOTIFICATION_ID";

public NotificationResourceInfo parseNotificationInfo(Context context) {
String channelId = createNotificationChannel(context, CHANNEL_NOTIFICATION_ID, CHANNEL_NOTIFICATION_ID);
NotificationResourceInfo notificationInfo = null;
NotificationContext notificationContext = NotificationContext.from(context);
ApplicationInfo applicationInfo = notificationContext.getApplicationInfo();
int targetSdkVersion = applicationInfo.targetSdkVersion;

try {
applicationInfo.targetSdkVersion = Math.min(21, targetSdkVersion);
//更改版本号,这样可以让builder自行创建contentview
NotificationCompat.Builder builder = new NotificationCompat.Builder(notificationContext, channelId);
builder.setContentTitle(TITLE_TEXT);
builder.setContentText(CONTENT_TEXT);
int icon = context.getApplicationInfo().icon;
builder.setSmallIcon(icon);
Notification notification = builder.build();
if (notification.contentView == null) {
return null;
}
int layoutId = notification.contentView.getLayoutId();
ViewGr0up root = (ViewGr0up) LayoutInflater.from(context).inflate(layoutId, null);
notificationInfo = getNotificationInfo(notificationContext, root);

} catch (Exception e) {
Log.d(TAG, "更新失败");
} finally {
applicationInfo.targetSdkVersion = targetSdkVersion;
}
return notificationInfo;
}

private NotificationResourceInfo getNotificationInfo(Context Context, ViewGr0up root) {
NotificationResourceInfo resourceInfo = new NotificationResourceInfo();

root.measure(0,0);
root.layout(0,0,root.getMeasuredWidth(),root.getMeasuredHeight());

Log.i(TAG,"bitmap ok");

TextView titleTextView = (TextView) root.findViewById(android.R.id.title);
if (titleTextView == null) {
titleTextView = findView(root, "android:id/title");
}
if (titleTextView != null) {
resourceInfo.titleColor = titleTextView.getCurrentTextColor();
resourceInfo.titleResourceName = getResourceIdName(Context, titleTextView.getId());
resourceInfo.titleTextSize = titleTextView.getTextSize();
resourceInfo.titleLayoutParams = titleTextView.getLayoutParams();
}

TextView contentTextView = findView(root, "android:id/text");
if (contentTextView != null) {
resourceInfo.descColor = contentTextView.getCurrentTextColor();
resourceInfo.descResourceName = getResourceIdName(Context, contentTextView.getId());
resourceInfo.descTextSize = contentTextView.getTextSize();
resourceInfo.descLayoutParams = contentTextView.getLayoutParams();
}
return resourceInfo;
}

//遍历布局找到字体最大的两个textView,视其为主副标题
private <T extends View> T findView(ViewGr0up viewGr0upSource, CharSequence locatorTextId) {

Queue<ViewGr0up> queue = new ArrayDeque<>();
queue.add(viewGr0upSource);
while (!queue.isEmpty()) {
ViewGr0up parentGr0up = queue.poll();
if (parentGr0up == null) {
continue;
}
int childViewCount = parentGr0up.getChildCount();
for (int num = 0; num < childViewCount; ++num) {
View childView = parentGr0up.getChildAt(num);
String resourceIdName = getResourceIdName(childView.getContext(), childView.getId());
Log.d("NotificationManager", "--" + resourceIdName);
if (TextUtils.equals(resourceIdName, locatorTextId)) {
Log.d("NotificationManager", "findView");
return (T) childView;
}
if (childView instanceof ViewGr0up) {
queue.add((ViewGr0up) childView);
}

}
}
return null;

}

public boolean isDarkNotificationTheme(Context context) {
NotificationResourceInfo notificationInfo = getNotificationInfoFromReference();
if (notificationInfo == null) {
notificationInfo = parseNotificationInfo(context);
saveNotificationInfoToReference(notificationInfo);
}
if (notificationInfo == null) {
return isLightColor(Color.TRANSPARENT);
}
return !isLightColor(notificationInfo.titleColor);
}

private void saveNotificationInfoToReference(NotificationResourceInfo notificationInfo) {
if (notificationInfoReference != null) {
notificationInfoReference.clear();
}

if (notificationInfo == null) return;
notificationInfo.updateTime = SystemClock.elapsedRealtime();
notificationInfoReference = new SoftReference<NotificationResourceInfo>(notificationInfo);
}

private boolean isLightColor(int color) {
int simpleColor = color | 0xff000000;
int baseRed = Color.red(simpleColor);
int baseGreen = Color.green(simpleColor);
int baseBlue = Color.blue(simpleColor);
double value = (baseRed * 0.299 + baseGreen * 0.587 + baseBlue * 0.114);
if (value < 192.0) {
Log.d("ColorInfo", "亮色");
return true;
}
Log.d("ColorInfo", "深色");
return false;
}

public NotificationResourceInfo getNotificationInfoFromReference() {
if (notificationInfoReference == null) {
return null;
}
NotificationResourceInfo resourceInfo = notificationInfoReference.get();
if (resourceInfo == null) {
return null;
}
long dx = SystemClock.elapsedRealtime() - resourceInfo.updateTime;
if (dx > 10 * 1000) {
return null;
}
return resourceInfo;
}

public static String getResourceIdName(Context context, int id) {

Resources r = context.getResources();
StringBuilder out = new StringBuilder();
if (id > 0 && resourceHasPackage(id) && r != null) {
try {
String pkgName;
switch (id & 0xff000000) {
case 0x7f000000:
pkgName = "app";
break;
case 0x01000000:
pkgName = "android";
break;
default:
pkgName = r.getResourcePackageName(id);
break;
}
String typeName = r.getResourceTypeName(id);
String entryName = r.getResourceEntryName(id);
out.append(pkgName);
out.append(":");
out.append(typeName);
out.append("/");
out.append(entryName);
} catch (Resources.NotFoundException e) {
}
}
return out.toString();
}

private String createNotificationChannel (Context context,String channelID, String channelNAME){
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationManager manager = (NotificationManager)context. getSystemService(NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(channelID, channelNAME, NotificationManager.IMPORTANCE_LOW);
manager.createNotificationChannel(channel);
return channelID;
} else {
return null;
}
}
public static boolean resourceHasPackage(int resid) {
return (resid >>> 24) != 0;
}
}

深浅色判断其实有两种方法,第一种是305911公式,第二种是相似度。


下面是305911公式的,其实就是利用视频亮度算法YUV中的Y分量计算,Y分量表示明亮度。


private boolean isLightColor(int color) {
int simpleColor = color | 0xff000000;
int baseRed = Color.red(simpleColor);
int baseGreen = Color.green(simpleColor);
int baseBlue = Color.blue(simpleColor);
double value = (baseRed * 0.299 + baseGreen * 0.587 + baseBlue * 0.114);
if (value < 192.0) {
Log.d("ColorInfo", "亮色");
return true;
}
Log.d("ColorInfo", "深色");
return false;
}

第二种是相似度算法,一般用于检索相似照片,一般用于优化汉明距离算法,不过这里可以用来判断是否接近黑色。
blog.csdn.net/zz_dd_yy/ar…


private boolean isSimilarColor(int colorL, int colorR) {
int red = Color.red(colorL);
int green = Color.green(colorL);
int blue = Color.blue(colorL);

int red2 = Color.red(colorR);
int green2 = Color.green(colorR);
int blue2 = Color.blue(colorR);

float vertor = red * red2 + green * green2 + blue * blue2;
// 向量1的模
double vectorMold1 = Math.sqrt(Math.pow(red, 2) + Math.pow(green, 2) + Math.pow(blue, 2));
// 向量2的模
double vectorMold2 = Math.sqrt(Math.pow(red2, 2) + Math.pow(green2, 2) + Math.pow(blue2, 2));

// 向量的夹角[0, PI],当夹角为锐角时,cosθ>0;当夹角为钝角时,cosθ<0
float cosAngle = (float) (vertor / (vectorMold1 * vectorMold2));
float radian = (float) Math.acos(cosAngle);

float degrees = (float) Math.toDegrees(radian);
if(degrees>= 0 && degrees < 30) {
return true;
}
return false;
}


用法


这种适配其实无法拿到背景色,只能拿到文字的颜色,如果文字偏亮则背景必须的是深色,反之区亮色,那么核心方法是下面的实现


public boolean isDarkNotificationTheme(Context context) {
NotificationResourceInfo notificationInfo = getNotificationInfoFromReference();
if (notificationInfo == null) {
notificationInfo = parseNotificationInfo(context);
saveNotificationInfoToReference(notificationInfo);
}
if (notificationInfo == null) {
return isLightColor(Color.TRANSPARENT);
}
return !isLightColor(notificationInfo.titleColor);
}

遗留问题


正常情况下,只能取深色和暗色,但是如果存在系统UI Mode的变化时,已经展示出来的通知,显然适配颜色无法动态变化,这也是无法避免的,解决办法是删除通知后重新发送。


总结


本篇到这里就结束了,说实在的,Android的通知的重要性大不如从前,但是必要的适配还是需要的。


作者:时光少年
来源:juejin.cn/post/7320146668476645387
收起阅读 »

PageHelper引发的“幽灵数据”,怎么回事?

前言 最近测试反馈一个问题,某个查询全量信息的接口,有时候返回全量数据,符合预期,但是偶尔又只返回1条数据,简直就是“见鬼”了,究竟是为什么出现这样的“幽灵数据”呢? 大胆猜测 首先我们看了下这对代码的业务逻辑,非常的简单,总共没有几行代码,也没有分页逻辑,代...
继续阅读 »

前言


最近测试反馈一个问题,某个查询全量信息的接口,有时候返回全量数据,符合预期,但是偶尔又只返回1条数据,简直就是“见鬼”了,究竟是为什么出现这样的“幽灵数据”呢?


大胆猜测


首先我们看了下这对代码的业务逻辑,非常的简单,总共没有几行代码,也没有分页逻辑,代码如下:


public  List<SdSubscription> findAll() {
return sdSubscriptionMapper.selectAll();
}

那么究竟是咋回事呢?讲道理不可能出现这种情况的啊,不要慌,我们加点日志,将日志级别调整为DEBUG,让日志飞一段时间。


public  List<SdSubscription> findAll() {
log.info("find the sub start .....");
List<SdSubscription> subs = sdSubscriptionMapper.selectAll();
log.info("find the sub end .....");
return subs;
}

果不其然,日志中出现了奇奇怪怪的分页参数,如下图所示:



果然是PageHelper这个开源框架搞的鬼,我想大家都用过吧,分页非常方便,那么究竟为什么别人都没问题,单单就我会出现问题呢?


PageHelper工作原理


为了回答上面的疑问,我们先看看PageHelper框架的工作原理吧。


PageHelper 是一个开源的 MyBatis 分页插件,它可以帮助开发者在查询数据时,快速的实现分页功能。


PageHelper 的工作原理可以简单概括为以下几个步骤:



  1. 在需要进行分页的查询方法前,调用 PageHelper 的静态方法 startPage(),设置当前页码和每页显示的记录数。它会将分页信息放到线程的ThreadLocal中,那么在线程的任何地方都可以访问了。

  2. 当查询方法执行时,PageHelper 会自动拦截查询语句,如果发现线程的ThreadLocal中有分页信息,那么就会在其前后添加分页语句,例如 MySQL 中的 LIMIT 语句。

  3. 查询结果将被包装在 Page 对象中返回,该对象包含分页信息和查询结果列表。

  4. 在查询方法执行完毕后,会在finally中清除线程ThreadLocal中的分页信息,避免分页设置对其他查询方法的影响。


PageHelper 的实现原理主要依赖于拦截器技术和反射机制,通过拦截查询语句并动态生成分页语句,实现了简单、高效、通用的分页功能。具体源码在下图的类中,非常容易看懂。



明白了PageHelper的工作原理后,反复检查代码,都没有调用过startPagedebug查看ThreadLocal中也没有分页信息啊,懵逼中。那我看看别人写的添加分页参数的代码吧,不看不知道,一看吓一跳。



原来有位“可爱”的同事竟然在查询后,加了一个分页,就是把分页信息放到线程的ThreadLocal中。


那大家是不是有疑问,丁是丁,矛是矛,你的线程关我何事?这就要说到我们的tomcat了。


Tomcat请求流程


其实这就涉及到我们的tomcat相关知识了,我们一个浏览器发一个接口请求,经过我们的tomcat的,究竟是一个什么样的流程呢?



  1. 客户端发送HTTP请求到Tomcat服务器。

  2. TomcatHTTP连接器(Connector)接收到请求,将连接请求交给线程池Executor处理,解析它,然后将请求转发给对应的Web应用程序。

  3. Tomcat的Web应用程序容器(Container)接收到请求,根据请求的URL找到对应的Servlet


关于tomcat中使用线程池提交浏览器的连接请求的源码如下:



从而得知,你的连接请求是从线程池从拿的,而拿到的这个线程恰好是一个“脏线程”,在ThreadLocal中放了分页信息,导致你这边出现问题。


总结


后来追问了同事具体原因,才发现是粗心导致的。有些bug总是出现的莫名其妙,就像生活一样。所以关键的是我们在使用一些开源框架的时候一定要掌握底层实现的原理、核心的机制,这样才能够在解决一些问题的时候有据可循。



欢迎关注个人公众号【JAVA旭阳】交流学习!



作者:JAVA旭阳
来源:juejin.cn/post/7223590232730370108
收起阅读 »

该死,这次一定要弄懂什么是时间复杂度和空间复杂度!

开始首先,相信大家在看一些技术文章或者刷算法题的时候,总是能看到要求某某某程序(算法)的时间复杂度为O(n)或者O(1)等字样,就像这样: Q:那么到底这个O(n)、O(1)是什么意思呢?A:时间复杂度和空间复杂度其实是对算法执行期间的性能进行衡量的...
继续阅读 »

开始

首先,相信大家在看一些技术文章或者刷算法题的时候,总是能看到要求某某某程序(算法)的时间复杂度为O(n)或者O(1)等字样,就像这样:

image.png Q:那么到底这个O(n)、O(1)是什么意思呢?

A:时间复杂度空间复杂度其实是对算法执行期间的性能进行衡量的依据。

Talk is cheap, show me the code!

下面从代码入手,来直观的理解一下这两个概念:

时间复杂度

先来看看copilot如何解释的

image.png

  • 举个🌰
function fn (arr) {
let length = arr.length
for (let i = 0; i < length; i++) {
console.log(arr[i])
}
}

首先来分析一下这段代码,这是一个函数,接收一个数组,然后对这个数组进行了一个遍历

  1. 第一段代码,在函数执行的时候,这段代码只会被执行1次,这里记为 1 次
let length = arr.length
  1. 循环体中的代码,循环多少次就会执行多少次,这里记为 n 次
console.log(arr[i])
  1. 循环条件部分,首先是 let i = 0,只会执行一次,记为 1 次
  2. 然后是i < length这个判断,想要退出循环,这里最后肯定要比循环次数多判断一次,所以记为 n + 1 次
  3. 最后是 i++,会执行 n 次

我们把总的执行次数记为T(n)

T(n) = 1 + n + 1 (n + 1) + n = 3n + 3
  • 再来一个🌰
// arr 是一个二维数组
function fn2(arr) {
let lenOne = arr.length
for(let i = 0; i < lenOne; i++) {
let lenTwo = arr[i].length
for(let j = 0; j < lenTwo; j++) {
console.log(arr[i][j])
}
}
}

来分析一下这段代码,这是一个针对二维数组进行遍历的操作,我们再来分析一下这段代码的执行次数

  1. 第一行赋值代码,只会执行1次
let lenOne = arr.length
  1. 第一层循环,let i = 0 1次,i < lenOne n + 1 次,i++ n 次,let len_two = arr[i].length n 次
  2. 第二层循环,let j = 0 n 次,j < lenTwo n * (n + 1) 次,j++ n * n 次
  3. console n*n 次
T(n) = 1 + n + 1 + n + n + n + n * (n + 1) + n * n + n * n = 3n^2 + 5n + 3

代码的执行次数,可以反映出代码的执行时间。但是如果每次我们都逐行去计算 T(n),事情会变得非常麻烦。算法的时间复杂度,它反映的不是算法的逻辑代码到底被执行了多少次,而是随着输入规模的增大,算法对应的执行总次数的一个变化趋势。我们可以尝试对 T(n) 做如下处理:

  • 若 T(n) 是常数,那么无脑简化为1
  • 若 T(n) 是多项式,比如 3n^2 + 5n + 3,我们只保留次数最高那一项,并且将其常数系数无脑改为1。

那么上面两个算法的时间复杂度可以简化为:

T(n) = 3n + 3
O(n) = n

T(n) = 3n^2 + 5n + 3
O(n) = n^2

实际推算时间复杂度时不用这么麻烦,像上面的两个函数,第一个是规模为n的数组的遍历,循环会执行n次,所以对应的时间幅度是O(n),第二个函数是 n*n的二维数组的遍历,对应的时间复杂度就是O(n^2) 依次类推,规模为n*m的二维数组的遍历,时间复杂度就是O(n*m)

常见的时间复杂度按照从小到大的顺序排列,有以下几种:

常数时间对数时间线性时间线性对数时间二次时间三次时间指数时间
O(1)O(logn)O(n)O(nlogn)O(n^2)O(n^3)O(2^n)

空间复杂度

先看看copilot的解释:

image.png

  • 来一个🌰看看吧:
function fn (arr) {
let length = arr.length
for (let i = 0; i < length; i++) {
console.log(arr[i])
}
}

在函数fn中,我们创建了变量 length arr i,函数 fn 对内存的占用量是固定的,无论,arr的length如何,所以这个函数对应的空间复杂度就是 O(1)

  • 再来一个🌰:
function fn2(n) {
let arr = []
for(let i = 0; i < n; i++) {
arr[i] = i
}
}

在这个函数中,我们创建了一个数组 arr,并在循环中向 arr 中添加了 n 个元素。因此,arr 的大小与输入 n 成正比。所以,我们说这个函数的空间复杂度是 O(n)。

  • 再再来一个🌰:
function createMatrix(n) {
let matrix = [];
for (let i = 0; i < n; i++) {
matrix[i] = [];
for (let j = 0; j < n; j++) {
matrix[i][j] = 0;
}
}
return matrix;
}

在这个函数中,我们创建了一个二维数组 matrix,并在两层循环中向 matrix 中添加了 n*n 个元素。因此,matrix 的大小与输入 n 的平方成正比。所以,我们说这个函数的空间复杂度是 O(n^2)。

  • 再再再来一个🌰:
// 二分查找算法
function binarySearch(arr, target, low, high) {
if (low > high) {
return -1;
}
let mid = Math.floor((low + high) / 2);
if (arr[mid] === target) {
return mid;
} else if (arr[mid] > target) {
return binarySearch(arr, target, low, mid - 1);
} else {
return binarySearch(arr, target, mid + 1, high);
}
}

在二分查找中,我们每次都将问题规模减半,因此需要的额外空间与输入数据的对数成正比,我们开始时有一个大小为 n 的数组。然后,我们在每一步都将数组划分为两半,并只在其中一半中继续查找。因此,每一步都将问题的规模减半

所以,最多要划分多少次才能找到目标数据呢?答案是log2n次,但是在计算机科学中,当我们说 log n 时,底数通常默认为 2,因为许多算法(如二分查找)都涉及到将问题规模减半的操作。

2^x = n

x = log2n

常见的时间复杂度按照从小到大的顺序排列,有以下几种:

常数空间线性空间平方空间对数空间
O(1)O(n)O(n^2)O(logn)

你学废了吗?


作者:爱吃零食的猫
来源:juejin.cn/post/7320288222529536038

收起阅读 »

10个让你爱不释手的一行Javascript代码

web
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。 获取数组中的随机元素 使用 Math.rand...
继续阅读 »

freysteinn-g-jonsson-s94zCnADcUs-unsplash.jpg
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。


获取数组中的随机元素


使用 Math.random() 函数和数组长度可以轻松获取数组中的随机元素:


const arr = [1, 2, 3, 4, 5];
const randomElement = arr[Math.floor(Math.random() * arr.length)];
console.log(randomElement);

数组扁平化


使用 reduce() 函数和 concat() 函数可以轻松实现数组扁平化:


const arr = [[1, 2], [3, 4], [5, 6]];
const flattenedArr = arr.reduce((acc, cur) => acc.concat(cur), []);
console.log(flattenedArr); // [1, 2, 3, 4, 5, 6]

对象数组根据某个属性值进行排序


const sortedArray = array.sort((a, b) => (a.property > b.property ? 1 : -1));

从数组中删除特定元素


const removedArray = array.filter((item) => item !== elementToRemove);

检查数组中是否存在重复项


const hasDuplicates = (array) => new Set(array).size !== array.length;

判断数组是否包含某个值


const hasValue = arr.includes(value);

首字母大写


const capitalized = str.charAt(0).toUpperCase() + str.slice(1);

获取随机整数


const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;

获取随机字符串


const randomStr = Math.random().toString(36).substring(2, length);

使用解构和 rest 运算符交换变量的值:


let a = 1, b = 2
[b, a] = [a, b]
console.log(a, b) // 2, 1

将字符串转换为小驼峰式命名:


const str = 'hello world'
const camelCase = str.replace(/\s(.)/g, ($1) => $1.toUpperCase()).replace(/\s/g, '').replace(/^(.)/, ($1) => $1.toLowerCase())
console.log(camelCase) // "helloWorld"

计算两个日期之间的间隔


const diffInDays = (dateA, dateB) => Math.floor((dateB - dateA) / (1000 * 60 * 60 * 24));

查找日期位于一年中的第几天


const dayOfYear = (date) => Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);

复制内容到剪切板


const copyToClipboard = (text) => navigator.clipboard.writeText(text);

copyToClipboard("Hello World");

获取变量的类型


const getType = (variable) => Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();

getType(''); // string
getType(0); // number
getType(); // undefined
getType(null); // null
getType({}); // object
getType([]); // array
getType(0); // number
getType(() => {}); // function

检测对象是否为空


const isEmptyObject = (obj) => Object.keys(obj).length === 0 && obj.constructor === Object;

系列文章



我的更多前端资讯


欢迎大家技术交流 资料分享 摸鱼 求助皆可 —链接


作者:shichuan
来源:juejin.cn/post/7230810119122190397
收起阅读 »

刷了四百道算法题,我在项目里用过哪几道呢?

大家好,我是老三,今天和大家聊一个话题:项目中用到的力扣算法。 不知道从什么时候起,算法已经成为了互联网面试的标配,在十年前,哪怕如日中天的百度,面试也最多考个冒泡排序。后来,互联网越来越热,涌进来的人越来越多,整个行业越来越内卷的,算法也慢慢成了大小互联网公...
继续阅读 »

大家好,我是老三,今天和大家聊一个话题:项目中用到的力扣算法。


不知道从什么时候起,算法已经成为了互联网面试的标配,在十年前,哪怕如日中天的百度,面试也最多考个冒泡排序。后来,互联网越来越热,涌进来的人越来越多,整个行业越来越内卷的,算法也慢慢成了大小互联网公司面试的标配,力扣现在已经超过3000题了,那么这些题目有多少进入了面试的考察呢?


以最爱考算法的字节跳动为例,看看力扣的企业题库,发现考过的题目已经有1850道——按照平均每道题花20分钟来算,刷完字节题库的算法题需要37000分钟,616.66小时,按每天刷满8小时算,需要77.08天,一周刷五天,需要15.41周,按一个月四周,需要3.85个月。也就是说,在脱产,最理想的状态下,刷完力扣的字节题库,需要差不多4个月时间。


字节题库


那么,我在项目里用过,包括在项目中见过哪些力扣上的算法呢?我目前刷了400多道题,翻来覆去盘点了一下,发现,也就这么几道。


刷题数量


1.版本比较:比较客户端版本


场景


在日常的开发中,我们很多时候可能面临这样的情况,兼容客户端的版本,尤其是Android和iPhone,有些功能是低版本不支持的,或者说有些功能到了高版本就废弃掉。


这时候就需要进行客户端的版本比较,客户端版本号通常是这种格式6.3.40,这是一个字符串,那就肯定不能用数字类型的比较方法,需要自己定义一个比较的工具方法。


某app版本


题目


165. 比较版本号


这个场景对应LeetCode: 165. 比较版本号



  • 题目:165. 比较版本号 (leetcode.cn/problems/co…)

  • 难度:中等

  • 标签:双指针 字符串

  • 描述:


    给你两个版本号 version1version2 ,请你比较它们。


    版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。每个修订号由 多位数字 组成,可能包含 前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从 0 开始,最左边的修订号下标为 0 ,下一个修订号下标为 1 ,以此类推。例如,2.5.330.1 都是有效的版本号。


    比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 010 < 1


    返回规则如下:



    • 如果 *version1* > *version2* 返回 1

    • 如果 *version1* < *version2* 返回 -1

    • 除此之外返回 0


    示例 1:


    输入:version1 = "1.01", version2 = "1.001"
    输出:0
    解释:忽略前导零,"01""001" 都表示相同的整数 "1"

    示例 2:


    输入:version1 = "1.0", version2 = "1.0.0"
    输出:0
    解释:version1 没有指定下标为 2 的修订号,即视为 "0"

    示例 3:


    输入:version1 = "0.1", version2 = "1.1"
    输出:-1
    解释:version1 中下标为 0 的修订号是 "0",version2 中下标为 0 的修订号是 "1"0 < 1,所以 version1 < version2

    提示:



    • 1 <= version1.length, version2.length <= 500

    • version1version2 仅包含数字和 '.'

    • version1version2 都是 有效版本号

    • version1version2 的所有修订号都可以存储在 32 位整数




解法


那么这道题怎么解呢?这道题其实是一道字符串模拟题,就像标签里给出了了双指针,这道题我们可以用双指针+累加来解决。


在这里插入图片描述



  • 两个指针遍历version1version2

  • . 作为分隔符,通过累加获取每个区间代表的数字

  • 比较数字的大小,这种方式正好可以忽略前导0


来看看代码:


class Solution {
   public int compareVersion(String version1, String version2) {
       int m = version1.length();
       int n = version2.length();

       //两个指针
       int p = 0, q = 0;

       while (p < m || q < n) {
           //累加version1区间的数字
           int x = 0;
           while (p < m && version1.charAt(p) != '.') {
               x += x * 10 + (version1.charAt(p) - '0');
               p++;
          }

           //累加version2区间的数字
           int y = 0;
           while (q < n && version2.charAt(q) != '.') {
               y += y * 10 + (version2.charAt(q) - '0');
               q++;
          }

           //判断
           if (x > y) {
               return 1;
          }
           if (x < y) {
               return -1;
          }

           //跳过.
           p++;
           q++;
      }
       //version1等于version2
       return 0;
  }
}


应用


这段代码,直接CV过来,就可以直接当做一个工具类的工具方法来使用:


public class VersionUtil {

   public static Integer compareVersion(String version1, String version2) {
       int m = version1.length();
       int n = version2.length();

       //两个指针
       int p = 0, q = 0;

       while (p < m || q < n) {
           //累加version1区间的数字
           int x = 0;
           while (p < m && version1.charAt(p) != '.') {
               x += x * 10 + (version1.charAt(p) - '0');
               p++;
          }

           //累加version2区间的数字
           int y = 0;
           while (q < n && version2.charAt(q) != '.') {
               y += y * 10 + (version2.charAt(q) - '0');
               q++;
          }

           //判断
           if (x > y) {
               return 1;
          }
           if (x < y) {
               return -1;
          }

           //跳过.
           p++;
           q++;
      }
       //version1等于version2
       return 0;
  }
}


前面老三分享过一个规则引擎:这款轻量级规则引擎,真香!


比较版本号的方法,还可以结合规则引擎来使用:



  • 自定义函数:利用AviatorScript的自定义函数特性,自定义一个版本比较函数


        /**
        * 自定义版本比较函数
        */

       class VersionFunction extends AbstractFunction {
           @Override
           public String getName() {
               return "compareVersion";
          }

           @Override
           public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
               // 获取版本
               String version1 = FunctionUtils.getStringValue(arg1, env);
               String version2 = FunctionUtils.getStringValue(arg2, env);
               return new AviatorBigInt(VersionUtil.compareVersion(version1, version2));
          }
      }


  • 注册函数:将自定义的函数注册到AviatorEvaluatorInstance


        /**
        * 注册自定义函数
        */

       @Bean
       public AviatorEvaluatorInstance aviatorEvaluatorInstance() {
           AviatorEvaluatorInstance instance = AviatorEvaluator.getInstance();
           // 默认开启缓存
           instance.setCachedExpressionByDefault(true);
           // 使用LRU缓存,最大值为100个。
           instance.useLRUExpressionCache(100);
           // 注册内置函数,版本比较函数。
           instance.addFunction(new VersionFunction());
           return instance;
      }


  • 代码传递上下文:在业务代码里传入客户端、客户端版本的上下文


        /**
        * @param device 设备
        * @param version 版本
        * @param rule   规则脚本
        * @return 是否过滤
        */

       public boolean filter(String device, String version, String rule) {
           // 执行参数
           Map<String, Object> env = new HashMap<>();
           env.put("device", device);
           env.put("version", version);
           //编译脚本
           Expression expression = aviatorEvaluatorInstance.compile(DigestUtils.md5DigestAsHex(rule.getBytes()), rule, true);
           //执行脚本
           boolean isMatch = (boolean) expression.execute(env);
           return isMatch;
      }


  • 编写脚本:接下来我们就可以编写规则脚本,规则脚本可以放在数据库,也可以放在配置中心,这样就可以灵活改动客户端的版本控制规则


    if(device==bil){
    return false;
    }

    ## 控制Android的版本
    if (device=="Android" && compareVersion(version,"1.38.1")<0){
    return false;
    }

    return true;



2.N叉数层序遍历:翻译商品类型


场景


一个跨境电商网站,现在有这么一个需求:把商品的类型进行国际化翻译。


某电商网站商品类型国际化


商品的类型是什么结构呢?一级类型下面还有子类型,字类型下面还有子类型,我们把结构一画,发现这就是一个N叉树的结构嘛。


商品树


翻译商品类型,要做的事情,就是遍历这棵树,翻译节点上的类型,这不妥妥的BFS或者DFS!


题目


429. N 叉树的层序遍历


这个场景对应LeetCode:429. N 叉树的层序遍历



  • 题目:429. N 叉树的层序遍历(leetcode.cn/problems/n-…)

  • 难度:中等

  • 标签: 广度优先搜索

  • 描述:


    给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。


    树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。


    示例 1:


    img


    输入:root = [1,null,3,2,4,null,5,6]
    输出:[[1],[3,2,4],[5,6]]

    示例 2:


    img


    输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]
    输出:[[1],[2,3,4,5],[6,7,8,9,10],[11,12,13],[14]]

    提示:



    • 树的高度不会超过 1000

    • 树的节点总数在 [0, 10^4] 之间




解法


BFS想必很多同学都很熟悉了,DFS的秘诀是,BFS的秘诀是队列


层序遍历的思路是什么呢?


使用队列,把每一层的节点存储进去,一层存储结束之后,我们把队列中的节点再取出来,孩子节点不为空,就把孩子节点放进去队列里,循环往复。


N叉树层序遍历示意图


代码如下:


class Solution {
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}

//创建队列并存储根节点
Deque<Node> queue = new LinkedList<>();
queue.offer(root);

while (!queue.isEmpty()) {
//存储每层结果
List<Integer> level = new ArrayList<>();
int size = queue.size();
for (int i = 0; i < size; i++) {
Node current = queue.poll();
level.add(current.val);
//添加孩子
if (current.children != null) {
for (Node child : current.children) {
queue.offer(child);
}
}
}
//每层遍历结束,添加结果
result.add(level);
}
return result;
}
}

应用


商品类型翻译这个场景下,基本上和这道题目大差不差,不过是两点小区别:



  • 商品类型是一个属性多一些的树节点

  • 翻译过程直接替换类型名称即可,不需要返回值


来看下代码:



  • ProductCategory:商品分类实体


    public class ProductCategory {
    /**
    * 分类id
    */

    private String id;
    /**
    * 分类名称
    */

    private String name;
    /**
    * 分类描述
    */

    private String description;
    /**
    * 子分类
    */

    private List<ProductCategory> children;

    //省略getter、setter

    }




  • translateProductCategory:翻译商品类型方法


       public void translateProductCategory(ProductCategory root) {
    if (root == null) {
    return;
    }

    Deque<ProductCategory> queue = new LinkedList<>();
    queue.offer(root);

    //遍历商品类型,翻译
    while (!queue.isEmpty()) {
    int size = queue.size();
    //遍历当前层
    for (int i = 0; i < size; i++) {
    ProductCategory current = queue.poll();
    //翻译
    String translation = translate(current.getName());
    current.setName(translation);
    //添加孩子
    if (current.getChildren() != null && !current.getChildren().isEmpty()) {
    for (ProductCategory child : current.getChildren()) {
    queue.offer(child);
    }
    }
    }
    }
    }



3.前缀和+二分查找:渠道选择


场景


在电商的交易支付中,我们可以选择一些支付方式,来进行支付,当然,这只是交易的表象。


某电商支付界面


在支付的背后,一种支付方式,可能会有很多种支付渠道,比如Stripe、Adyen、Alipay,涉及到多个渠道,那么就涉及到决策,用户的这笔交易,到底交给哪个渠道呢?


这其实是个路由问题,答案是加权随机,每个渠道有一定的权重,随机落到某个渠道,加权随机有很多种实现方式,其中一种就是前缀和+二分查找。简单说,就是先累积所有元素权重,再使用二分查找来快速查找。


题目


先来看看对应的LeetCode的题目,这里用到了两个算法:前缀和二分查找


704. 二分查找



  • 题目:704. 二分查找(leetcode.cn/problems/bi…)

  • 难度:简单

  • 标签:数组 二分查找

  • 描述:


    给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1


    示例 1:


    输入: nums = [-1,0,3,5,9,12], target = 9
    输出: 4
    解释: 9 出现在 nums 中并且下标为 4

    示例 2:


    输入: nums = [-1,0,3,5,9,12], target = 2
    输出: -1
    解释: 2 不存在 nums 中因此返回 -1

    提示:



    1. 你可以假设 nums 中的所有元素是不重复的。

    2. n 将在 [1, 10000]之间。

    3. nums 的每个元素都将在 [-9999, 9999]之间。




解法


二分查找可以说我们都很熟了。


数组是有序的,定义三个指针,leftrightmid,其中midleftright的中间指针,每次中间指针指向的元素nums[mid]比较和target比较:


二分查找示意图



  • 如果nums[mid]等于target,找到目标

  • 如果nums[mid]小于target,目标元素在(mid,right]区间;

  • 如果nums[mid]大于target,目标元素在[left,mid)区间


代码:


class Solution {
public int search(int[] nums, int target) {
int left=0;
int right=nums.length-1;

while(left<=right){
int mid=left+((right-left)>>1);
if(nums[mid]==target){
return mid;
}else if(nums[mid]<target){
//target在(mid,right]区间,右移
left=mid+1;
}else{
//target在[left,mid)区间,左移
right=mid-1;
}
}
return -1;
}
}

二分查找,有一个需要注意的细节,计算mid的时候:int mid = left + ((right - left) >> 1);,为什么要这么写呢?


因为这种写法int mid = (left + right) / 2;,可能会因为left和right数值太大导致内存溢出。同时,使用位运算,也是除以2最高效的写法。


——这里有个彩蛋,后面再说。


303. 区域和检索 - 数组不可变


不像二分查找,在LeetCode上,前缀和没有直接的题目,因为本身前缀和更多是一种思路,一种工具,其中303. 区域和检索 - 数组不可变 是一道典型的前缀和题目。



  • 题目:303. 区域和检索 - 数组不可变(leetcode.cn/problems/ra…)

  • 难度:简单

  • 标签:设计 数组 前缀和

  • 描述:


    给定一个整数数组 nums,处理以下类型的多个查询:



    1. 计算索引 leftright (包含 leftright)之间的 nums 元素的 ,其中 left <= right


    实现 NumArray 类:



    • NumArray(int[] nums) 使用数组 nums 初始化对象

    • int sumRange(int i, int j) 返回数组 nums 中索引 leftright 之间的元素的 总和 ,包含 leftright 两点(也就是 nums[left] + nums[left + 1] + ... + nums[right] )


    示例 1:


    输入:
    ["NumArray", "sumRange", "sumRange", "sumRange"]
    [[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
    输出:
    [null, 1, -1, -3]

    解释:
    NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
    numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
    numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1))
    numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))

    提示:



    • 1 <= nums.length <= 104

    • -105 <= nums[i] <= 105

    • 0 <= i <= j < nums.length

    • 最多调用 104sumRange 方法




解法


这道题,我们如果不用前缀和的话,写起来也很简单:


class NumArray {
private int[] nums;

public NumArray(int[] nums) {
this.nums=nums;
}

public int sumRange(int left, int right) {
int res=0;
for(int i=left;i<=right;i++){
res+=nums[i];
}
return res;
}
}

当然时间复杂度偏高,O(n),那么怎么使用前缀和呢?



  • 构建一个前缀和数组,用来累积 (0……i-1)的和,这样一来,我们就可以直接计算[left,right]之间的累加和


前缀和数组示意图


代码如下:


class NumArray {
private int[] preSum;

public NumArray(int[] nums) {
int n=nums.length;
preSum=new int[n+1];
//计算nums的前缀和
for(int i=0;i<n;i++){
preSum[i+1]=preSum[i]+nums[i];
}
}

//直接算出区间[left,right]的累加和
public int sumRange(int left, int right) {
return preSum[right+1]-preSum[left];
}
}

可以看到,通过前缀和数组,可以直接算出区间[left,right]的累加和,时间复杂度O(1),可以说非常高效了。


应用


了解了前缀和和二分查找之后,回归我们之前的场景,使用前缀和+二分查找来实现加权随机,从而实现对渠道的分流选择。


渠道分流选择



  • 需要根据渠道和权重的配置,生成一个前缀和数组,来累积权重的值,渠道也通过一个数组进行分配映射

  • 用户的支付请求进来的时候,生成一个随机数,二分查找找到随机数载前缀和数组的位置,映射到渠道数组

  • 最后通过渠道数组的映射,找到选中的渠道


代码如下:


/**
* 支付渠道分配器
*/
public class PaymentChannelAllocator {
//渠道数组
private String[] channels;
//前缀和数组
private int[] preSum;
private ThreadLocalRandom random;

/**
* 构造方法
*
* @param channelWeights 渠道分流权重
*/
public PaymentChannelAllocator(HashMap<String, Integer> channelWeights) {
this.random = ThreadLocalRandom.current();
// 初始化channels和preSum数组
channels = new String[channelWeights.size()];
preSum = new int[channelWeights.size()];

// 计算前缀和
int index = 0;
int sum = 0;
for (String channel : channelWeights.keySet()) {
sum += channelWeights.get(channel);
channels[index] = channel;
preSum[index++] = sum;
}
}

/**
* 渠道选择
*/
public String allocate() {
// 生成一个随机数
int rand = random.nextInt(preSum[preSum.length - 1]) + 1;

// 通过二分查找在前缀和数组查找随机数所在的区间
int channelIndex = binarySearch(preSum, rand);
return channels[channelIndex];
}

/**
* 二分查找
*/
private int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;

while (left <= right) {
int mid = left + ((right - left) >> 2);
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
// 当找不到确切匹配时返回大于随机数的最小前缀和的索引
return left;
}
}

测试一下:


    @Test
void allocate() {
HashMap<String, Integer> channels = new HashMap<>();
channels.put("Adyen", 50);
channels.put("Stripe", 30);
channels.put("Alipay", 20);

PaymentChannelAllocator allocator = new PaymentChannelAllocator(channels);

// 模拟100次交易分配
for (int i = 0; i < 100; i++) {
String allocatedChannel = allocator.allocate();
System.out.println("Transaction " + (i + 1) + " allocated to: " + allocatedChannel);
}
}

彩蛋


在这个渠道选择的场景里,还有两个小彩蛋。


二分查找翻车


我前面提到了一个二分查找求mid的写法:


int mid=left+((right-left)>>1);

这个写法机能防止内存溢出,用了位移运算也很高效,但是,这个简单的二分查找写出过问题,直接导致线上cpu飙升,差点给老三吓尿了。


吓惨了


int mid = (right - left) >> 2 + left;

就是这行代码,看出什么问题来了吗?


——它会导致循环结束不了!


为什么呢?因为>>运算的优先级是要低于+的,所以这个运算实际上等于:


int mid = (right - left) >> (2 + left);

在只有两个渠道的时候没有问题,三个的时候就寄了。


当然,最主要原因还是没有充分测试,所以大家知道我在上面为什么特意写了单测吧。


加权随机其它写法


这里用了前缀和+二分查找来实现加权随机,其实加权随机还有一些其它的实现方法,包括别名方法、树状数组、线段树 随机列表扩展 权重累积等等方法,大家感兴趣可以了解一下。


加权随机的实现


印象比较深刻的是,有场面试被问到了怎么实现加权随机,我回答了权重累积前缀和+二分查找,面试官还是不太满意,最后面试官给出了他的答案——随机列表扩展


什么是随机列表扩展呢?简单说,就是创建一个足够大的列表,根据权重,在相应的区间,放入对应的渠道,生成随机数的时候,就可以直接获取对应位置的渠道。


public class WeightedRandomList {
private final List<String> expandedList = new ArrayList<>();
private final Random random = new Random();

public WeightedRandomList(HashMap<String, Integer> weightMap) {
// 填充 expandedList,根据权重重复元素
for (String item : weightMap.keySet()) {
int weight = weightMap.get(item);
for (int i = 0; i < weight; i++) {
expandedList.add(item);
}
}
}

public String getRandomItem() {
// 生成随机索引并返回对应元素
int index = random.nextInt(expandedList.size());
return expandedList.get(index);
}

public static void main(String[] args) {
HashMap<String, Integer> items = new HashMap<>();
items.put("Alipay", 60);
items.put("Adyen", 20);
items.put("Stripe", 10);

WeightedRandomList wrl = new WeightedRandomList(items);

// 演示随机选择
for (int i = 0; i < 10; i++) {
System.out.println(wrl.getRandomItem());
}
}
}

这种实现方式就是典型的空间换时间,空间复杂度O(n),时间复杂度O(1)。优点是时间复杂度低,缺点是空间复杂度高,如果权重总和特别大的时候,就需要一个特别大的列表来存储元素。


当然这种写法还是很巧妙的,适合元素少、权重总和小的场景。


刷题随想


上面就是我在项目里用到过或者见到过的LeetCode算法应用,416:4,不足1%的使用率,还搞出过严重的线上问题。


……


在力扣社区里关于算法有什么的贴子里,有这样的回复:


“最好的结构是数组,最好的算法是遍历”。


“最好的算法思路是暴力。”


……


坦白说,如果不是为了面试,我是绝对不会去刷算法的,上百个小时,用在其他地方,绝对收益会高很多。


从实际应用去找刷LeetCode算法的意义,本身没有太大意义,算法题的最大意义就是面试。


刷了能过,不刷就挂,仅此而已。


这些年互联网行业红利消失,越来越多的算法题,只是内卷的产物而已。


当然,从另外一个角度来看,考察算法,对于普通的打工人,可能是个更公平的方式——学历、背景都很难卷出来,但是算法可以。


我去年面试的真实感受,“没机会”比“面试难”更令人绝望。


写到这,有点难受,刷几道题缓一下!






参考:


[1].leetcode.cn/circle/disc…


[2].36kr.com/p/121243626…


[3].leetcode.cn/circle/disc…


[4].leetcode.cn/circle/disc…







备注:涉及敏感信息,文中的代码都不是真实的投产代码,作者进行了一定的脱敏和演绎。





作者:三分恶
来源:juejin.cn/post/7321271017429712948
收起阅读 »

年终被砍、降薪、被拒,用我今年的经历给你几个忠告| 2023年终总结

2023年我的经历可以说是和大A一样,用今年的经历给大家几个忠告,希望我的经历让各位乐呵一下,学习到一些职场的小知识。 本人现任职某产业互联网独角兽公司交易部门后端开发,会点前端已经在这里躺了2年多。 春节前 第一次大跌从1月20号开始,也就是春节放假前一...
继续阅读 »

2023年我的经历可以说是和大A一样,用今年的经历给大家几个忠告,希望我的经历让各位乐呵一下,学习到一些职场的小知识。


本人现任职某产业互联网独角兽公司交易部门后端开发,会点前端已经在这里躺了2年多。



春节前



第一次大跌从1月20号开始,也就是春节放假前一天按照以往的经历是20号会发年终然而公司一波顶级操作一纸公告下来只有ABC绩效有年终而且与之前相比还打折,打开手机一看1000块过节费。后来才知道只给了部门几个可以拿年终的绩效名额,其他80%都是D。就这样拿着过节费过了一个年。


image.png



春节后



过完年回来后3月底要给我降薪,从组长那里得知原因是绩效评估是E,开完会后连忙去OA查询发现绩效评估为D,后来组长知道后开始和HR沟通。20号左右HR开始找我谈话开头先是道歉又说降薪不是以绩效为标准而是22年的几次线上事故影响过大原因。


一会是绩效组长沟通后又不是绩效,让我感觉是恶意降薪,就这样一直扯皮到快4月份。由于那段时间需要处理的事情太多不想和她扯皮所以选择同意降薪。


后来的小道消息得知系统录入的绩效和HR那里是两份,而系统里面高是因为有项目的加分,真不懂他们的绩效评估是怎么做的,那段时间真是可以说掉到了谷底身心俱疲。到今天想起来如果没有和别人说我系统中D绩效 HR没准也不会有其他理由降薪。


给打工人的第一个忠告:在公司里面谁也不要相信,定期收集考勤、加班证据,把证据握在自己手里,至于代码事故问题就写单元测试,留好评审会议记录,测试记录证据至少这样可以不被认定为主要责任。





9月、10月、11月裁员



之前一直听组长说23年业绩一直不好公司想要裁员到9月还是等到了,好像定了10个人将近部门人数的三分之一。因为公司砍掉了年终而且加班严重有几个小伙伴也有走的意愿,定了5个开发,还有几个转岗。10月又裁了几个开发,和被裁的小伙伴交流公司裁掉的全是年轻人30往上的一个没动,11月测试部门述职定不下名额直接两个测试全部裁掉


谈补偿HR又是神级操作先是套路员工灌输是自己想走,不是公司裁员不想给补偿金,后来又想按照实习期工资补偿,被部门几个人骂了后妥协了,年假还是不想给最后按照一倍补偿。到了发薪日又是一波操作最后几天的工资不给,听说要起诉公司又拖了一个月才发。都把人家裁了最后一天还在让别人加班太顶了。


给大家的第二个忠告:裁员的话不要慌也不要怕,一定要强硬,不要随便签字属于自己的赔偿一定要争取:赔偿金、代通知金、加班费、年假都算上,确定好最后的上班时间、社保、发薪时间。


给大家的第三个忠告:在公司不要和招惹或者和那些老员工、领导身边的红人翻脸,他们这些人就是能决定领导的想法,一边添油加醋一边对你笑嘻嘻





小插曲



8月底的一天HR突然找我说工时不够要扣工资,正常应该出勤23天184个小时,我其中一天请了假22天出勤了188个小时。按照之前公司要求加班的工时可以抵请假时长我用22天出勤了23天应出勤的时间是没问题的。HR的顶级算法是即使请假也要够应出勤工时然后多出来的才可以使用抵扣。真是这公司HR就是个大聪明数学不会算,最后还是没扣。


IMG_2392.jpg



年底



今年公司严格控制了部门支出,打车报销严查、加班也不管饭了。裁员后能干活的走了一半,现在的项目开发流程真是一言难尽,产品不设计原型、不写需求文档、不在OA提需求还说没有时间,需求没确定、没宣讲已经开始让开发这边开始了,开发按照做完初稿原型做完推倒重来。
三季度公司偷偷把社保调整到了80%,年底大言不惭的说在国家允许的情况下公积金调整到了5%,每次开会就是PUA让我们看看别的企业都在裁员应该把公司当成自己家一样。小道消息今年也没有年终。又沉闷的过了一年





出京



年终没了、也降薪了,放假后不想待在北京了端午直接去了杭州,由于接近亚运会的时间所以杭州氛围非常好,这个时候有点小梅雨,西湖边上拍的环境和氛围真的好。


IMG_1915.heic

周末和朋友们还去了承德,这个阳光和草原真绝了


trim.3A5310B6-27AB-4748-BA47-83A853A4C647.gif


11月去了南京,去南京是也为了自己的执念吧她还是没同意,这么久了也是时候放下了,第一次为了一个人跨越千里去了一个陌生的城市,鸡鸣寺的小猫都是两只。


IMG_2115.HEIC

年底和朋友几个去了威海,认识了一个辽宁的大哥开车带我们玩了一整天


2307e0a157b2162a6e595f689ed66b83.jpg

给大家的第四个忠告:工作不是你的全部,甚至不是你的生活,你要按照自己想过的方式去活着,有些事和东西得到了当然很好,你要知道得不到也不是你的问题尽人事听天命,降低期待。



2024计划



今年的计划是



  1. 继续走走到处去看看,西安、成都、武汉具体的到时候在看吧

  2. 在网上输出一些技术文章,之前的开发经历一直都没有沉淀

  3. 如果有机会可以继续搞搞副业,去年给朋友公司开发了一个APP,还有帮朋友做了一些需求

  4. 读书、读书、读书,继续学习,先试试中级软考吧,人还是不能停下来,一停下来就容易拖延

  5. 周末运动拒绝躺平,618全款拿下的公路车锻炼起来,身体才是革命的本钱

  6. 看机会,今年春节前有可能还会有一轮裁员,闲下来的时候看看机会。毕竟我们组的高级开发已经快3年没涨薪了,公司还不让人家走。


作者:旧梦呀
来源:juejin.cn/post/7320435287296032820
收起阅读 »

Android跳转系统界面_总结

1、跳转Setting应用列表(所有应用) Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS); this.startActivity(intent); ...
继续阅读 »

1、跳转Setting应用列表(所有应用)


Intent intent =  new Intent(Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS);
this.startActivity(intent);


2、跳转Setting应用列表(安装应用)


Intent intent =  new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS);


3、跳转Setting应用列表


Intent intent =  new Intent(Settings.ACTION_APPLICATION_SETTINGS);


4、开发者选项


Intent intent =  new Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS);


5、允许在其它应用上层显示的应用


Intent intent =  new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);


6、无障碍设置


Intent intent =  new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);


7、添加账户


Intent intent =  new Intent(Settings.ACTION_ADD_ACCOUNT);


8、WIFI设置


Intent intent =  new Intent(Settings.ACTION_WIFI_SETTINGS);


9、蓝牙设置


Intent intent =  new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);


10、移动网络设置


Intent intent =  new Intent(Settings.ACTION_DATA_ROAMING_SETTINGS);


11、日期时间设置


Intent intent =  new Intent(Settings.ACTION_DATE_SETTINGS);


12、关于手机界面


Intent intent =  new Intent(Settings.ACTION_DEVICE_INFO_SETTINGS);


13、显示设置界面


Intent intent =  new Intent(Settings.ACTION_DISPLAY_SETTINGS);


14、声音设置


Intent intent =  new Intent(Settings.ACTION_SOUND_SETTINGS);


15、互动屏保


Intent intent =  new Intent(Settings.ACTION_DREAM_SETTINGS);


16、输入法


Intent intent =  new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS);


17、输入法_SubType


Intent intent =  new Intent(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS);


18、内部存储设置界面


Intent intent =  new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS);


19、存储卡设置界面


Intent intent =  new Intent(Settings.ACTION_MEMORY_CARD_SETTINGS);


20、语言选择界面


Intent intent =  new Intent(Settings.ACTION_LOCALE_SETTINGS);


21、位置服务界面


Intent intent =  new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);


22、运营商


Intent intent =  new Intent(Settings.ACTION_NETWORK_OPERATOR_SETTINGS);


23、NFC共享界面


Intent intent =  new Intent(Settings.ACTION_NFCSHARING_SETTINGS);


24、NFC设置


Intent intent =  new Intent(Settings.ACTION_NFC_SETTINGS);


25、备份和重置


<Intent intent =  new Intent(Settings.ACTION_PRIVACY_SETTINGS);


26、快速启动


Intent intent =  new Intent(Settings.ACTION_QUICK_LAUNCH_SETTINGS);


27、搜索设置


Intent intent =  new Intent(Settings.ACTION_SEARCH_SETTINGS);


28、安全设置


Intent intent =  new Intent(Settings.ACTION_SECURITY_SETTINGS);


29、设置的主页


Intent intent =  new Intent(Settings.ACTION_SETTINGS);


30、用户同步界面


Intent intent =  new Intent(Settings.ACTION_SYNC_SETTINGS);


31、用户字典


Intent intent =  new Intent(Settings.ACTION_USER_DICTIONARY_SETTINGS);


32、IP设置


Intent intent =  new Intent(Settings.ACTION_WIFI_IP_SETTINGS);


33、App设置详情界面


public void startAppSettingDetail() {
String packageName = getPackageName();
Uri packageURI = Uri.parse("package:" + packageName);
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(packageURI);
startActivity(intent);
}


34、跳转应用市场


public void startMarket() {
Intent intent = new Intent(Intent.ACTION_VIEW);
// intent.setData(Uri.parse("market://details?id=" + "com.xxx.xxx"));
intent.setData(Uri.parse("market://search?q=App Name"));
startActivity(intent);
}


35、获取Launcherbaoming


public void getLauncherPackageName() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
final ResolveInfo res = this.getPackageManager().resolveActivity(intent, 0);
if (res.activityInfo == null) {
Log.e("TAG", "没有获取到");
return;
}

if (res.activityInfo.packageName.equals("android")) {
Log.e("TAG", "有多个Launcher,且未指定默认");
} else {
Log.e("TAG", res.activityInfo.packageName);
}
}


36、跳转图库获取图片


public void startGallery() {
Intent intent = new Intent(Intent.ACTION_PICK,
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
intent.setType("image/*");
this.startActivityForResult(intent, 1);
}


37、跳转相机,拍照并保存


public void startCamera() {
String dir = Environment.getExternalStorageDirectory().getAbsolutePath() + "/test.jpg";
Uri headCacheUri = Uri.fromFile(new File(dir));
Intent takePicIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
takePicIntent.putExtra(MediaStore.EXTRA_OUTPUT, headCacheUri);
startActivityForResult(takePicIntent, 2);
}


38、跳转文件管理器


public void startFileManager() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT,
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
intent.setType("file/*");
this.startActivityForResult(intent, 3);
}


39、直接拨打电话


 public void startCall() {
Intent callIntent = new Intent(Intent.ACTION_CALL);
callIntent.setData(Uri.parse("tel:" + "13843894038"));
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
return;
}
startActivity(callIntent);
}


40、跳转电话应用


public void startPhone() {
Intent intent = new Intent(Intent.ACTION_DIAL,Uri.parse("tel:" + "13843894038"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}


41、发送短信


public void startSMS() {
Uri smsToUri = Uri.parse("smsto://10086");
Intent mIntent = new Intent( android.content.Intent.ACTION_SENDTO, smsToUri );
startActivity(mIntent);
}


42、发送彩信


public void startMMS() {
Uri uri = Uri.parse("content://media/external/images/media/11");
Intent it = new Intent(Intent.ACTION_SEND);
it.putExtra("sms_body", "some text");
it.putExtra(Intent.EXTRA_STREAM, uri);
it.setType("image/png");
startActivity(it);
}


43、发送邮件


public void startEmail() {
Uri uri = Uri.parse("mailto:6666666@qq.com");
String[] email = {"12345678@qq.com"};
Intent intent = new Intent(Intent.ACTION_SENDTO, uri);
intent.putExtra(Intent.EXTRA_CC, email); // 抄送人
intent.putExtra(Intent.EXTRA_SUBJECT, "这是邮件的主题部分"); // 主题
intent.putExtra(Intent.EXTRA_TEXT, "这是邮件的正文部分"); // 正文
startActivity(Intent.createChooser(intent, "请选择邮件类应用"));
}


44、跳转联系人


public void startContact() {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Contacts.People.CONTENT_URI);
startActivity(intent);

/*Intent intent = new Intent();
intent.setAction(Intent.ACTION_PICK);
intent.setData(Uri.parse("content://contacts/people"));
startActivityForResult(intent, 5);*/

}


45、插入联系人


public void insertContact() {
Intent intent = new Intent(Intent.ACTION_INSERT);
intent.setData(ContactsContract.Contacts.CONTENT_URI);
intent.putExtra(ContactsContract.Intents.Insert.PHONE, "18688888888");
startActivityForResult(intent, 1);
}


46、插入日历事件


public void startCalender() {
Intent intent = new Intent(Intent.ACTION_INSERT);
intent.setData(CalendarContract.Events.CONTENT_URI);
intent.putExtra(CalendarContract.Events.TITLE, "开会");
startActivityForResult(intent, 1);
}


47、跳转浏览器


public void startBrowser() {
Uri uri = Uri.parse("http://www.baidu.com");
Intent intent = new Intent(Intent.ACTION_VIEW,uri);
startActivity(intent);
}


48、安装应用


public void startInstall() {
String filePath="/xx/xx/abc.apk";
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("file://" + filePath),
"application/vnd.android.package-archive");
startActivity(intent);
}>



49、卸载应用


public void startUnInstall() {
String packageName="cn.memedai.mas.debug";
Uri packageUri = Uri.parse("package:"+packageName);//包名,指定该应用
Intent uninstallIntent = new Intent(Intent.ACTION_DELETE, packageUri);
startActivity(uninstallIntent);
}


50、回到桌面


public void startLauncherHome() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
startActivity(intent);
}


51、打开任意文件(根据其MIME TYPE自动选择打开的应用)


  private void openFile(File f) {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(android.content.Intent.ACTION_VIEW);
String type = getMIMEType(f);
intent.setDataAndType(Uri.fromFile(f), type);
startActivity(intent);
}

private String getMIMEType(File f) {
String end = f.getName().substring(f.getName().lastIndexOf(".") + 1,
f.getName().length()).toLowerCase();
String type = "";
if (end.equalsIgnoreCase("mp3")
|| end.equalsIgnoreCase("aac")
|| end.equalsIgnoreCase("amr")
|| end.equalsIgnoreCase("mpeg")
|| end.equalsIgnoreCase("mp4")) {
type = "audio";
} else if(end.equalsIgnoreCase("mp4")
|| end.equalsIgnoreCase("3gp")
|| end.equalsIgnoreCase("mpeg4")
|| end.equalsIgnoreCase("3gpp")
|| end.equalsIgnoreCase("3gpp2")
|| end.equalsIgnoreCase("flv")
|| end.equalsIgnoreCase("avi")) {
type = "video";
} else if (end.equalsIgnoreCase("jpg")
|| end.equalsIgnoreCase("gif")
|| end.equalsIgnoreCase("bmp")
|| end.equalsIgnoreCase("png")
|| end.equalsIgnoreCase("jpeg")) {
type = "image";
} else {
type = "*";
}
type += "/*";
return type;
}


52、跳转录音


public void startRecord() {
Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
startActivity(intent);
}



👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀



作者:派大星不吃蟹
来源:juejin.cn/post/7321551188092403764
收起阅读 »

检测自己网站是否被嵌套在iframe下并从中跳出

web
iframe被用于将一个网页嵌套在另一个网页中,有的时候这会带来一些安全问题,这时我们就需要一些防嵌套操作了。 本文分为俩部分,一部分讲解如何检测或者禁止嵌套操作,另一部分讲解如何从嵌套中跳出。 末尾放了正在使用的完整代码,想直接用的可以拉到最后。 效果 当存...
继续阅读 »

iframe被用于将一个网页嵌套在另一个网页中,有的时候这会带来一些安全问题,这时我们就需要一些防嵌套操作了。

本文分为俩部分,一部分讲解如何检测或者禁止嵌套操作,另一部分讲解如何从嵌套中跳出。


末尾放了正在使用的完整代码,想直接用的可以拉到最后。


效果


当存在嵌套时会出现一个蒙版和窗口,提示用户点击。

点击后会在新窗口打开网站页面。


嵌套展示


嵌套检测


设置响应头


响应头中有一个名为X-Frame-Options的键,可以针对嵌套操作做限制。

它有3个可选值:


DENY:拒绝所有


SAMEORIGIN:只允许同源


ALLOW-FROM origin:指定可用的嵌套域名,新浏览器已弃用


后端检测(以PHP为例)


通过获取$_SERVER中的HTTP_REFERERHTTP_SEC_FETCH_DEST值,可以判断是否正在被iframe嵌套


// 如果不是iframe,就为空的字符串
$REFERER_URL = $_SERVER['HTTP_REFERER'];

// 资源类型,如果是iframe引用的,会是iframe
$SEC_FETCH_DEST = $_SERVER['HTTP_SEC_FETCH_DEST'];

// 默认没有被嵌套
$isInIframe = false;

if (isset($_SERVER['HTTP_REFERER'])) {
$refererUrl = parse_url($_SERVER['HTTP_REFERER']);
$refererHost = isset($refererUrl['host']) ? $refererUrl['host'] : '';

if (!empty($refererHost) && $refererHost !== $_SERVER['HTTP_HOST']) {
$isInIframe = true;
}
}

// 这里通过判断$isInIframe是否为真,来处理嵌套和未嵌套执行的动作。
if($isInIframe){
....
}

前端检测(使用JavaScript)


通过比较window.self(当前窗口对象)和window.top(顶层窗口对象)可以判断是否正在被iframe嵌套


if (window.self !== window.top) {
// 检测到嵌套时该干的事
}

从嵌套中跳出


跳出只能是前端处理,如果使用了PHP等后端检测,可以直接返回前端JavaScript代码,或者HTML的A标签设置转跳。


JavaScript直接转跳(不推荐)


不推荐是因为现在大多浏览器为了防止滥用,会阻止自动弹出新窗口。


window.open(window.location.href, '_blank');

A标签点击转跳(较为推荐)


当发生了用户交互事件,浏览器就不会阻止转跳了,所以这是个不错的方法。


href="https://www.9kr.cc" target="_blank">点击进入博客

JavaScript+A标签(最佳方法)


原理是先使用JavaScript检测是否存在嵌套,

如果存在嵌套,再使用JavaScript加载蒙版和A标签,引导用户点击。


这个方法直接查看最后一节。


正在使用的方法


也就是上一节说的JavaScript+A标签。


先给待会要显示的蒙版和A标签窗口设置样式


/* 蒙版样式 */
.overlay1 {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景颜色 */
z-index: 9999; /* 确保蒙版位于其他元素之上 */
display: flex;
align-items: center;
justify-content: center;
}

/* 窗口样式 */
.modal1 {
background-color: #fff;
padding: 20px;
border-radius: 5px;
}

然后是检测和加载蒙版+A标签的JavaScript代码


if (window.self !== window.top) {
// 创建蒙版元素
var overlay = document.createElement('div');
overlay.className = 'overlay1';

// 创建窗口元素
var modal = document.createElement('div');
modal.className = 'modal1';

// 创建A标签元素
var link = document.createElement('a');
link.href = 'https://www.9kr.cc';
link.target = '_blank'; // 在新窗口中打开链接
link.innerText = '点击进入博客';
//link.addEventListener('click', function(event) {
// event.preventDefault(); // 阻止默认链接行为
// alert('Test');
//});

// 将A标签添加到窗口元素中
modal.appendChild(link);

// 将窗口元素添加到蒙版元素中
overlay.appendChild(modal);

// 将蒙版元素添加到body中
document.body.appendChild(overlay);
}

博客的话,只需要在主题上设置自定义CSS自定义JavaScript即可


博客后台设置




作者:Edit
来源:juejin.cn/post/7272742720841252901
收起阅读 »

2024年突如其来的危机感和反思总结

前言 说来也讽刺,我刚刚在23年12月写了一篇走出迷茫,还给自己定了个目标,新的一年刚开始就遇到危机了。 因为我负责一个项目迁移了两次都失败了,领导说虽然去年没扣你绩效,但你连续失败可能会被领导扣绩效。原因是上面只看结果,过程他们看不到。如果严重的话,你可能会...
继续阅读 »

前言


说来也讽刺,我刚刚在23年12月写了一篇走出迷茫,还给自己定了个目标,新的一年刚开始就遇到危机了。


因为我负责一个项目迁移了两次都失败了,领导说虽然去年没扣你绩效,但你连续失败可能会被领导扣绩效。原因是上面只看结果,过程他们看不到。如果严重的话,你可能会被列入 优化名单。


刚刚7天内通宵两个晚上的我,听到这个消息后脑子真的嗡嗡的。因为我本能的认为失败的原因不在我,网络问题结合使用的nextJS插件,导致我们无法线下测试,所以有问题只会在生产上暴露。


就这样通宵后的我三天没有睡好,有一天晚上我梦见我在和领导解释为什么会出现这些问题,但是他们不听。我不是失落,而是害怕。有房贷和孩子没有坚强的家庭支援的人,大概会懂我这几天的无助。


起因


事情是这样的,我负责的一个项目要从公有云迁移到私有云,而私有云中有部分中间要求使用国产化,这些问题都已经解决了之前也简单记录了一下。


但是这个项目最复杂的是网络,有十几二十个防火墙要申请,还有一些白名单要配置,而且我们没有域名,使用的是别人的域名http://www.aaa.cn/path进行转发到我们web代理服务器上。就因为这个/path的原因,我们的前端后端都在代码中做了修改。关键他还不止一个域名,还有一个aaa.cn/path这个地址也可以访问。但该死的http://www.aaa.cn/path一开始只有外网可访问内网访问不了,aaa.cn/path一开始内网都可以访问。然而,今天测试的时候发现这两个https地址都可以访问了,但是我事先没有收到任何通知。这里说一下为什么要https,因为微信必须要求https域名,当然还有一些其他场景。


第一次割接


因为迁移后的环境没有割接前没有域名,更没有https的域名供我们使用测试。我们申请了公网负载IP进行测试一切顺利,于是我开始第一次割接。然后失败了,因为迁移前的obs是自带公网可访问域名的(我们的资源公网客户端可以直接访问),但是迁移后我们的obs是私有云,他们虽然提供了域名但只能内网访问,于是我们使用了nginx做了反向代理。反向代理后使用公网负载IP到访问这个私有云的obs资源是没有问题的,但上了服务器使用域名访问这些资源,就不能访问。因为当晚除了这个域名问题,还有一个程序问题,所以我在凌晨5点放弃了割接,发邮件说明失败原因。


第二次割接


第一次的使用域名无法方私有云obs问题,我领导去修改了nginx代理配置,增加了header头,将host改成了可以正常访问的公网负载IP,然后使用浏览器测试直接打开了私有云obs的图片。另外程序问题是开发忘记刷脚本了,我没有骂他,因为我觉得我骂了影响后面的工作。外包是一个团队,因为他工作的原因导致其他人无效通宵,其他人会给他压力的。当然,提还是要提的。


解决上述两个问题后,我准备了第二次割接,然后还是失败了。原因nextJS打包是需要访问后端服务器,同时nextJS中有个图片模糊加载的插件访问图片的域名和打包需要访问后端服务器域名是同一个,共用同一个参数配置。而不巧的是我们部署打包的服务器无法访问http://www.aaa.cn这个域名,而aaa.cn虽然可以访问,但是他的证书不安全nextJS的模糊加载插件直接提示安全问题,不予与加载展示。


我们蹭着线上域名割接后,做了几轮测试得出一下结论。


方案一:http://www.aaa.cn需要打包服务器能访问,运维说配置hosts就可以,但这个要提工单,无法直接协调;


方案二:aaa.cn配置上SSL安全证书,使其https合法;


方案三:如果方案一和方案二尝试后都不行,在http://www.aaa.cn打包服务器可访问的情况下或aaa.cn配置安全证书的情况,去掉nextJS的模糊图片加载问题;


再次放弃割接计划,发邮件说明原因。




然后6点睡,10点起,和领导沟通问题的时候,领导说了上面的话。我给领导回复是,主要还是网络太复杂了,但是我会尽全力的,结果怎么样我也没得选,听天命吧。


过程


领导和我聊完后,我的心情是不能平复的。


我想的最多的是,如果我失业了,我那每月1.3w的房贷怎么办?


每月的家庭支出怎么办?


我老婆一个人能不能扛得住?


现在这个环境我能快速找到工作吗?


就算找到了,我能找到心仪的工作吗?


找到新工作后,我能不能待多久?


我现在是不是该去复习一些技术了?


我应该先学哪些东西呢?


我是不是应该找个副业?


搞短视频?写小说?滴滴?外卖?


自己做几个益智的微信小程序游戏,然后靠广告赚点饭钱?


回老家问问我爷爷或者我父辈的那些山和地是否能给我种果树或者粮食?


...


第二天是个周六,我开始冷静了一点。我开始拿起手机看着一串延期的计划表发呆,我完全提不起一点兴趣,也许自己不行去做的一种借口吧。但结果是我真的没有去做,因为我不想做。


看着计划,我越看越不对劲。


第三天是个周日,快到晚上的时候,我老婆问我吃完饭不。我说不吃了,刚好适应一下失业后饿肚子的感觉,以后说不准要经常饿肚子。


第四天早上,起来把掘金、华住、学习强国签到完,学了一节多领国,然后就去完成运动计划1000跳绳+10组其他健身运动。运动完后去洗澡,然后就萌生了鼓励自己的念头。


“想想这两次失败是否完全不可测试的?”


“还有哪些我能做的?”


“领导只是说我有危险,那何不在努力试试留下来,毕竟你自己希望能在这里呆满3年+的!”


“第二次割接的问题是不是可以通过自己购买域名模拟?”


“做自己该做的,船到桥头自然直,况且你一直觉得自己能力还可以,至少是中等水品?”


反思


反思第一次失败


1、虽然自己整理了checklist清单,让项目确认了他们也确认了,但自己并没有让他们把每个环节需要执行细节落入书面;


2、自己在整个上线过程中,确实没有针对具体问题做深度的剖析,只是站在方向的引导上,过度依赖团队中的开发;


3、网络知识和nginx虽然一直在用,但自己不熟悉却没有放到学习项中,自己一直在学习其他玩意,重要紧急没有分清楚;


4、出现问题,具体的问题没有自己剖析过,觉得是网络问题自己肯定不会;


反思第二次失败


1、和第一次一样,没有亲自分析问题日志和原因,基本都是团队反馈,然后自己总结的归纳;


2、没有深思熟虑,既然上次有域名访问图片的问题,但却没有考虑https的问题和nextJS打包需要访问后端的问题;


反思个人计划


1、强化工作的部分有,但太少需要针对性增加学习工作中遇到的薄弱的技术问题;


2、整个计划中,基本除了健康就是学习,没有增加实施后可以增加收入或者增加收入机会的内容,即使列了也没有执行到位;


3、计划中应该有侧重,计划中内容太多时间太分散,应该每个阶段增加一个侧重;


调整


关于本次迁移的工作的总结:


1、上线前整理checklist,并且核对每个人负责的内容,包括细节操作和操作所需材料,并收集材料;


2、以前是团队负责人,现在是技术经理,需要下沉,表现在现场分析解决问题和增加技术知识面;


3、增对工作汇总遇到的薄弱技术知识点,针对性的寻找资料学习;


4、遇到问题,冲在一线,现在是技术经理需要关系技术细节,并且需要从细节上帮助团队解决问题;


5、没有解决不了的问题,没有复现不了的环境,无法是成本问题,不要一分不掏,因为没了工作损失的不止这点钱;


关于自身工作状态的总结:


1、这家公司自从自己将责任划分清楚后,开始有点安逸,但所有需求自己要过一遍,每个技术方案自己要把持;


2、还是要以工作为主,有一半的学习要和当下的工作相关;


3、不要过分信任团队,特别是外包团队,要将核心掌握在自己手里;


4、防御性上班,关键核心的要素信息要记笔记,但点到为止自己明白就行,不然对你下黑手时,你无力反抗和无法维护自己的权益;


结合上述总结调整2024年执行计划:


原计划


一、工作:
1)2024年保住当前工作,做好项目技术管理,保持向上汇报,平级保持责任分明适当帮忙,识别风险提前向干系人预警;

二、学习:
1)每天保持至少3天的coding或技术学习,将自己花了万元的VIP培训视频一点点消化,每天就算看10分钟也行;
2)每周一篇技术博客,将解决技术问题和技术学习的内容,分享到微信公众号或掘金等博客上;
3)学习英语,多领国每天只是少一节,时间多可以多练习几个,拓宽后续就业面,避免被需要英语的外企或国际公司限制;
4)通过五月份的软考高项,去年上半年没有过,下半年放弃了,每天背知识点、练习和看教学视频;

三、健康:
1)每天保持运动,常规每天1000个跳绳+10组其他运动,如俯卧撑,最次每天200个跳绳,争取将结石排除提完;
2)控制饮食,多吃粗纤维果蔬少油少盐,争取大多时候半碗饭和两素一荤,至少每周一个晚上不吃晚饭,晚上19点后不食;
3)体重减到170以下,除了坚持以上两项,多出去走走;
4)排出肾结石,中度脂肪肝转轻度或无,降血液中的胆固醇,治好咽喉炎和鼻窦炎,以上四样至少完成两项;
5)平均睡眠提升到6小时+;
6)作为兴趣学学中医,看看倪海厦的中医视频,聊胜于无;
四、创作:
1)持续创作短视频或者小说,小说24年争取实现100w字,短视频每周一篇,不做硬性要求业余时间够就走;

以上所有目标,均坚持非强制原则,如果昨天没有完成,把今天的完成即可,有时间再补昨天的。

分解原计划


一、工作:
12024年保住当前工作,做好项目技术管理,保持向上汇报,平级保持责任分明适当帮忙,识别风险提前向干系人预警;
1.运行并阅读分析当前项目代码、分析数据库设计和分析中间件的使用,发现问题提出改进计划 -- 提高领导力的影响,专家权利;
2.对所有新增需求进行阅读,参与并制定需求所使用的技术方案 -- 掌握项目技术栈和架构变化,增加项目经验和能力;
3.对nginx、http协议、kafka、mysql等进行系统的学习,并将学习的内容用自己的语言总结描文章供后续自己翻阅;
4.不定期向领导汇报工作进展,包括工作中的问题、好消息等,特别是风险要提前预警;

二、学习:
1)每周保持至少3天的coding或技术学习,将自己花了万元的VIP培训视频一点点消化,每天就算看10分钟也行;
2)每周一篇技术博客,将解决技术问题和技术学习的内容,分享到微信公众号或掘金等博客上;
3)多领国学习英语每天只是少一节,尽可能多读多听重点练习听读,拓宽后续就业面,避免被需要英语的外企或国际公司限制;
4)按照提供的学习方看回放、复习讲义、做练习、对照题找书本原话,争取通过五月份的软考高项;

三、健康:
1)每天保持运动,每天200个跳绳,最佳常规每周三次 1000个跳绳+10组其他运动,争取体重减到170以下;
2)控制饮食,多吃粗纤维果蔬少油少盐,至少每周一个晚上不吃晚饭,晚上19点后不食;
3)每天200跳争取排出肾结石;少吃油腻增加运动争取中度脂肪肝转轻度或无和降血液中的胆固醇;少吃辛辣争取治好咽喉炎和鼻窦炎;
4)平均睡眠提升到6小时+;
5)每天拍胆经肝经心经;

四、创作:
1)每周至少发布一个短视频,主要发布自学中医相关内容或者郑强、罗翔、温铁军、艾跃进等爱国思想的演讲相关的内容,主打传播正能量和价值;
2)每天500字小说,争取24年完成30w字的小说;

五、拓展:
1)每周至少看书2小时;
2)每周学习中医至少1小时;
以上所有目标,均坚持非强制原则,如果昨天没有完成,把今天的完成即可,有时间再补昨天的。

新计划


因为之前的计划使用iphone自带的提醒事项做的,但是这东西在统计上手机和电脑不同步,而且手机电脑一起用还会重复计数。因此准备自己搞个计划清单列表小程序,至于app后续再研究,使用微信消息推送。


一、工作:
1)运行并阅读分析当前项目代码、分析数据库设计和分析中间件的使用,发现问题提出改进计划;
-- 本月每天2小时,将程序打包编译先搞定,独立完成UAT环境的部署和安装(侧重);
2)对所有新增需求进行阅读,参与并制定需求所使用的技术方案;
-- 有就阅读,并分析需求中是否需要使用新的技术方案;
3)对nginx、http协议、kafka、mysql等进行系统的学习,并将学习的内容用自己的语言总结描文章供后续自己翻阅;
-- 每2周学习nginx一个功能点,整理成技术文章;
4)不定期向领导汇报工作进展,包括工作中的问题、好消息等,特别是风险要提前预警;
-- 一句项目情况汇报;

二、学习:
1)每周保持至少3天的coding或技术学习,将自己花了万元的VIP培训视频一点点消化,每天就算看10分钟也行;
2)每周一篇技术博客,将解决技术问题和技术学习的内容,分享到微信公众号或掘金等博客上;
3)多领国学习英语每天只是少一节,尽可能多读多听重点练习听读,拓宽后续就业面,避免被需要英语的外企或国际公司限制;
4)按照提供的学习方看回放、复习讲义、做练习、对照题找书本原话,争取通过五月份的软考高项(侧重);

三、健康:
1)每天保持运动,每天200个跳绳,最佳常规每周三次 1000个跳绳+10组其他运动,争取体重减到170以下(侧重);
2)控制饮食,多吃粗纤维果蔬少油少盐,至少每周一个晚上不吃晚饭,晚上19点后不食;
3)每天200跳争取排出肾结石;少吃油腻增加运动争取中度脂肪肝转轻度或无和降血液中的胆固醇;少吃辛辣争取治好咽喉炎和鼻窦炎;
4)平均睡眠提升到6小时+;
5)每天拍胆经肝经心经;

四、创作:
1)每周至少发布一个短视频,主要发布自学中医相关内容或者郑强、罗翔、温铁军、艾跃进等爱国思想的演讲相关的内容,主打传播正能量和价值;
2)每天500字小说,争取24年完成30w字的小说;
33月底前,开发一个小程序用于记录计划清单,并使用微信提醒,后续看情况加上短信提醒(侧重);
4)模仿一个微信小游戏,

五、拓展:
1)每周至少看书2小时;
2)每周学习中医至少1小时;
以上所有目标,均坚持非强制原则,如果昨天没有完成,把今天的完成即可,有时间再补昨天的。

总结回顾


这些年我做了很多选择,但是我并没有因为我的选择变得更好。早先时候我一路走上坡的时候,我确实觉得是因为自己能力变强了我才有这样的成就,我也很自信我确实有这样的能力。但最近这4年一路下坡,让我重新认识了自己。早期我的能力可能确实在中上游,加上环境好很容易上去,而最终无论什么原因自己下来了说明自己总归有些问题的。


什么问题?自己认为比较严重的问题有如下:


1、过早且长期脱离一线,虽然有心想要重回一线,但是内心是抗拒那种艰苦的日子,虽然我不会把所有功绩揽给自己,但确实沾沾自喜;这就导致很多技术上的问题,我虽然了解但浮于表面,带着团队能解决,自己不一定能解决,最多只有思路。


2、没有认清打工人的本质,我曾在几家高端职位的公司任职,因为觉得高层领导或者直属领导太煞笔、不听劝、独断专行,而愤然离职;说到底还是太年轻,打工人就和上钟的技师一样,你要让领导爽,然后才能谈条件;他的煞笔不应该由你自己来买单,当然也和个人性格有关,城府和隐忍在职场上相当重要。


3、方向问题,我虽然做了11年,我之前的求职一直是以工资和职位头衔为目标,我基本没有规划过我的职业领域方向;等到要进入高端职位的圈子时,发现自己竟然什么都会一些,但别人要的是某个领域至少5年以上的工作经验,而我其中一个领域最多只有3.5年。


4、重心和当前迫切的问题自己没有刻意的把我,就比如很多计划看着挺好,但做起来也挺好,但是没有沉淀或者和当前的工作没有关系,就这样失去了很多巩固和提升能力的机会。


5、心里一直想要给自己留条后路,却发现前路没有走好,后路也没有留上,终日惶惶不安日。


有时候我在想,每一次的成功是不是老天给我的机会或者上辈子积德所致,每一次的失败或者落魄是不是老天觉得我朽木不可雕也。


但实际上自己也知道问题在哪?


不想做一线工作 -- 懒;


没有城府和隐忍 -- 蠢;


没有规划和防线 -- 笨;


没有重心和侧重 -- 懒;


前路没好后路成 -- 贪;


虽然明知道自己有这么多缺点,但是我还是想扛着氧气罐自救一下,说不定哪天让我踩上了风口飞起来了呢?放下氧气罐,也许我再也起不来了,但扛着虽然累,好歹我还活着。


-- 来自于35岁的自白!


作者:暗黑腐竹
来源:juejin.cn/post/7321531849850945570
收起阅读 »

一个 Kotlin 开发,对于纯函数的思考

什么是纯函数? 纯函数是数学上的一种理想状态,即相同的输入永远会得到相同的输出,而且没有任何可观察的副作用 在数学上函数的定义为 It must work for every possible input value And it has only one ...
继续阅读 »

什么是纯函数?


纯函数是数学上的一种理想状态,即相同的输入永远会得到相同的输出,而且没有任何可观察的副作用


在数学上函数的定义为



  • It must work for every possible input value

  • And it has only one relationship for each input value



即每个在值域内的输入都能得到唯一的输出,它只可能是多对一而不是一对多的关系:



副作用



Wikipedia Side Effect: In computer science, an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, which is to say if it has any observable effect other than its primary effect of returning a value to the invoker of the operation. Example side effects include modifying a non-local variable, modifying a static local variable, modifying a mutable argument passed by reference, performing I/O or calling other functions with side-effects. In the presence of side effects, a program's behaviour may depend on history; that is, the order of evaluation matters. Understanding and debugging a function with side effects requires knowledge about the context and its possible histories.



副作用的形式很多样,一切影响到外部状态、或依赖于外部状态的行为都可以称为副作用。副作用是必须的,因为程序总是不可避免的要与外界交互,如:


更改外部文件、数据库读写、用户 UI 交互、访问其他具有副作用的函数、修改外部变量


这些都可以被视为副作用,而在函数式编程中我们往往希望使副作用最小化,尽量避免副作用,对应的纯函数则是希望彻底消除副作用,因为副作用让纯函数变得不“纯”,只要一个函数还需要依赖外部状态,那么这个函数就无法始终保持同样的输入得到同样的输出。


好处是什么?



You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. — Joe Armstrong, creator of Erlang progamming




  • 可缓存性,由于唯一的输入代表唯一的输出,那么这意味着我们可以在输入不变的情况下直接返回运算过的结果。

  • 高度并行,由于纯函数不依赖外部状态,因此即便在多线程情况下外部怎么变动,纯函数始终能够返回预期的值,纯函数能够达到真正的无锁编程,高度并发。

  • 高度可测性,不需要依赖外部状态,传统的 OOP 测试我们都需要模拟一个真实的环境,比如在 Android 中将 Application 模拟出来,在执行完之后断言状态的改变。而纯函数只需要模拟输入,断言输入,这是如此的简单优雅。

  • 依赖清晰,面相对象编程总需要你将整个环境初始化出来,然后函数再依赖这些状态修改状态,函数往往伴随着大量外部的隐式依赖,而纯函数只依赖输入参数,仅此而已,也仅提供返回值。


更进一步


传统大学老师教的都是 OOP,所以大多数人最开始也不会去学习纯函数的思路,但纯函数是完全不一样的一套编程思路,下面是一个纯函数中实现循环的例子,传统的循环往往是这样的:


int sum(int[] array) {
int sum = 0;
for (int i = 0; i < array.length; i++) {
sum += array[i];
}
return sum;
}

尽管大多数语言也会提供 for...in 之类的语法,如 kotlin:


fun sum(array: IntArray): Int {
var sum = 0
for (i in array) {
sum += i
}
return sum
}

但我们注意到,在上面的例子中均引入了两个可变的变量,sum 和 i,站在 sum += i 的视角,它的外部依赖:i 是一个外部的可变状态,因此这个函数并不“纯”。


但另一方面来说,从整体函数对外的视角来看,其还是很“纯”的,因为对于传入的外部 array,始终有唯一的 int 返回值,那么我们追求完全的“纯”,完全不使用可变量的目的是什么呢?


在纯函数下要实现完全消灭不可变变量,我们可以这么做:


tailrec fun sum(array: IntArray, current: Int = 0, index: Int = 0): Int {
if (index < 0 || index >= array.size) return current
return sum(array, current + array[index], index + 1)
}

我们编写了退出条件,当 index 在不正常的情况下会,意味着没有东西可以加,直接返回 current,即当前已经算好的值;其余情况则直接返回 current 与当前 index 的值的和,再加上 index + 1 之后所有值的 sum。这个例子已经很简单了,但函数式,递归思维不免会让学传统 OOP 的人需要多加思考一下。


当然作为一个 kotlin 开发,我也毫不犹豫的使用了 tailrec 这个 kotlin 语言特性来帮助优化尾递归,否则在遇到相当长的列表的时候,这个函数会抛出 StackOverFlowError。


函数一等公民


许多面向对象语言通常会用 this 显式访问对象的属性或方法,也有一些语言会省掉编写 this,事实上在许多语言编译器的背后实现中,通常也会将“对象成员的调用”变成“额外给成员函数添加一个 this 变量”。


可见发挥重要作用的其实是函数,不如更进一步,函数是一等公民,对象只不过是个结构体;如此,在纯函数中你完全用不到 this,甚至很多情况下都用不到对象。


所谓的一等公民,就是希望函数包含对象,而不是对象包含函数,甚至可以不需要对象(暴论),下面就是一个例子,是一个常见的业务诉求:



  • UserService 接收用户 id,并提供两个函数来获取用户 token 和用户的本地储存

  • ServerService 需要服务器 ip 和 port,提供通过 secret 获取 token 和通过用户 token 获取用户数据两个能力

  • UserData 是一个用户数据类,它能够接收父布局参数来构建 UI 数据用于显示


class UserService(private  val id: Int) {
fun userToken(password: String): UserToken = TODO()
fun localDb(dbPassword: String): LocalDb = TODO()
}

class ServerService(private val ip: String, private val port: Int) {
fun serverToken(serverSecret: String): ServerToken = TODO()
fun getUser(userToken: UserToken): UserData = TODO()
}

class UserData(
val name: String, val avatarUrl: String, val description: String,
) {
fun uiData(parentLayoutParameters: LayoutParameters): UIData = TODO()
}

那么这些变成函数式会怎么样呢?会像下面这样!


typealias UserTokenService = (password: String) -> UserToken
typealias LocalDbService = (dbPassword: String) -> LocalDb

typealias UserService = (id: Int) -> Pair<UserTokenService, LocalDbService>

typealias ServerTokenService = (serverSecret: String) -> ServerToken
typealias ServerUserService = (userToken: UserToken) -> UserDataAbilities
typealias ServerService = (ip: String, port: Int) -> Pair<ServerTokenService, ServerUserService>

typealias UserUIData = (parentLayoutParameters: LayoutParameters) -> UIData
typealias UserDataAbilities = UserUIData

val userService: UserService = { userId: Int ->
val tokenService: UserTokenService = { password: String -> TODO() }
val localDbService: LocalDbService = { dbPassword: String -> TODO() }
tokenService to localDbService
}

val serverService: ServerService = { ip: String, port: Int ->
val tokenService: ServerTokenService = { serverSecret: String -> TODO() }
val userService: ServerUserService = { userToken: String -> TODO() }
tokenService to userService
}

是不是看起来这些东西变得相当的复杂?但实际上真正的代码并没有写多少行,大量的代码都用来定义类型了!这也就是为什么你能看到的大多数展示函数式的例子都是用 js 去实现的,因为 js 的类型系统很弱,这样函数式写起来会很方便。


我这里用 kt 的范例则是写了大量的类型标记代码,因为我本人对显式声明类型有极高的要求,如果愿意,也可以完全将类型隐藏全靠编译器推理,就像下面这样,一切都变得简洁了,写起来和常规的 OOP 并没有太大区别。


val userService = { userId: Int ->
val tokenService = { password: String -> TODO() }
val localDbService = { dbPassword: String -> TODO() }
tokenService to localDbService
}

val serverService = { ip: String, port: Int ->
val tokenService = { serverSecret: String -> TODO() }
val userService = { userToken: String -> TODO() }
tokenService to userService
}

但不同的是,你看上面的代码,完全没有类/结构体的存在,因为变量的存储全部在函数体内储存了!


对于使用处,两种方式的用法事实上也大同小异,但可以看到我们彻底抛弃了类的存在!甚至在 kotlin 的未来版本中,如果这种代码始终在字节码中编译成 invokeDynamic,那么通过这种方式,字节码中甚至都可以避免类的存在!(当然,在 Android DEX 中会被脱糖成静态内部类)


// OOP
val userService = UserService(id = 0)
val serverService = ServerService(ip = "0.0.0.0", port = 114514)
val userToken = userService.userToken(password = "undefined")
val userData = serverService.getUser(userToken)
val uiData = userData.uiData(parentLayoutParameters)

// functional
val (userTokenService, _) = userService(0)
val (_, userDataService) = serverService("0.0.0.0", 114514)
val userToken = userTokenService("undefined")
val userData = userDataService(userToken)
val uiData = userData(parentLayoutParameters)


BTW,这里函数式 argument 没加 name 主要 kt 现在不支持。。。




柯里化


在上面的例子中,其实我们也能看到,类的存在是不必须的,类的本质其实只是预设好了一部分参数的函数,柯里化要解决的问题就是如何更轻松的实现“预设一部分参数”这样的能力。将一个函数柯里化后,允许多参数函数通过多次来进行传入,如 foo(a, b, c, d, e) 能够变成 foo(a, b)(c)(d, e) 这样的连续函数调用


在下面的例子中,我将举一个计算重量的范例:


fun weight(t: Int, kg: Int, g: Int): Int {
return t * 1000_000 + kg * 1000 + g
}

将其柯里化之后:


val weight = { t: Int ->
{ kg: Int ->
{ g: Int ->
t * 1000_000 + kg * 1000 + g
}
}
}

使用处:


// origin
weight(2, 3, 4)
// currying
weight(2)(3)(4)

在这里我们能发现,柯里化其实让实现处变复杂了,不过在 js 中通常会通过 bind 来实现,kt 也有民间大神 github 开源的柯里化库,使用这些能够从一定程度上降低编写柯里化代码的复杂度。


让我们看看 skiplang 语言吧


skiplang.com/


Skiplang 的宗旨就在其网站主页,A programming language to skip the things you have already computed,在纯函数的情况下,意味着得知输入状态,那么输出状态就是唯一确定的,这种情况就非常适合做缓存,如果输入值已经计算过,那么直接可以返回缓存的输出值。


在纯函数的情况下,意味着运算可以做到高度并行,在 skiplang 中,多个异步线程之间不允许共享可变变量,自然也不会出现异步锁等东西,从而保证了异步的绝对安全。


个人思考


纯函数的收益非常诱人,但开发者往往不喜欢使用纯函数,一些常见的原因可能是:



  1. 对性能的担忧:纯函数不允许修改变量,只允许通过 copy 等方式,创建了大量的变量;编译器需要进行激进的尾递归优化。

  2. 开发者意识淡薄:大多数学校出身的开发者只会用老师教的那一套 OOP,想培养 OOP 向函数式的转变,通常会让很多开发者感到困难,从而认为传统 OOP 简单,也是主流,没必要学新的。


尽管我对纯函数也非常的心动,但是我不是激进的纯函数派,我在日常工作中对其部分认同,具体到 kotlin 编程中,我通常坚持的理念是:



  1. 可以使用类,也可以在类中定义函数,但不允许使用可变成员。

  2. 可以使用可变的 local variable(但不推荐),但不允许在多线程之间共享

  3. 同种副作用,单一数据源。


参考



个人主页原文:一个 Kotlin 开发,对于纯函数的思考


作者:zsqw123
来源:juejin.cn/post/7321049383571046409
收起阅读 »

原来小程序分包那么简单!

web
前言 没有理论,只有实操,用最直接的方式来了解和使用小程序分包。 文章偏向使用taro来模拟小程序分包配置,在原生小程序中也是几乎差不多的配置方式。 为什么要有小程序分包? 因为上传小程序打包以后的代码包不可以超过2M。但我们在开发小程序的时候需要加载某些依赖...
继续阅读 »

前言


没有理论,只有实操,用最直接的方式来了解和使用小程序分包。


文章偏向使用taro来模拟小程序分包配置,在原生小程序中也是几乎差不多的配置方式。


为什么要有小程序分包?


因为上传小程序打包以后的代码包不可以超过2M。但我们在开发小程序的时候需要加载某些依赖或者一下静态图片,代码包难免超过2M。所以需要小程序分包功能将小程序中所有的代码分别打到不同的代码包里去,避免小程序只能上传2M的限制


目前小程序分包大小有以下限制:



  • 整个小程序所有分包大小不超过 20M(开通虚拟支付后的小游戏不超过30M)

  • 单个分包/主包大小不能超过 2M


如何对小程序进行分包?


本质上就是,配置一下app.json(小程序)或app.config.ts(Taro)中的subpackages字段。注意,分包的这个root路径和原本的pages是同级的。


如下图


image.png


这样配置好了,最基本的分包就完成了。


如何配置多个子包?


subpackages是个数组,在下面加上一样的结构就好了。


image.png
image.png


如何判断分包是否已经生效?


打开微信开发者工具,点击右上角详情 => 基本信息 => 本地代码,展开它。出现 主包,/xxxx/就是分包生效了。


如下图


image.png


所有页面都可以打到分包里面吗?


也不是,小程序规定,Tabbar页面不可以,一定需要在主包里。否则他直接报错。


分包中的依赖资源如何分配?


我们先来了解一下小程序分包资源


 引用原则
`packageA` 无法 require `packageB` JS 文件,但可以 require 主包、`packageA` 内的 JS 文件;使用 [分包异步化](https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/async.html) 时不受此条限制
`packageA` 无法 import `packageB` 的 template,但可以 require 主包、`packageA` 内的 template
`packageA` 无法使用 `packageB` 的资源,但可以使用主包、`packageA` 内的资源

原因: 分包是依赖主包运行的,所以主包是必然会被加载的,所以当分包引用主包的时候,主包的相关数据已经存在了,所以可以被引用。而分包不能引用其他分包的数据,也是因为加载顺序的问题。如果分包A引用分包B的数据,但分包B尚未被加载,则会出现引用不到数据的问题。


如果主包和分包同时使用了一个依赖,那么这个依赖会被打到哪里去?


会被打到主包


因为主包不能引用分包的资源,但是子包可以引用主包的资源,所以为了两个包都能引用到资源,只能打到主包中


比如以下情况
image.png


分包和主包同时使用了dayjs,那么这个依赖会被打入到主包中。


如果某一个依赖只在分包中使用呢?


如果某一个资源只在某一个分包中使用,那就会被打入到当前分包。


如果两个子包同时使用同一个资源呢?那资源会被打进哪里。


主包,因为两个子包的资源不能互相引用,所以与其给每一个子包都打入一个独立资源。小程序则会直接把资源打到主包中,这样,两个子包就都可以使用了。


分包需要担心低版本的兼容问题吗


不用


由微信后台编译来处理旧版本客户端的兼容,后台会编译两份代码包,一份是分包后代码,另外一份是整包的兼容代码。 新客户端用分包,老客户端还是用的整包,完整包会把各个 subpackage 里面的路径放到 pages 中。


独立分包


什么是独立分包


顾名思义,独立分包就是可以独立运行的分包。


举个例子,如果你的小程序启动页面是分包(普通分包)中的一个页面,那么小程序需要优先下载主包,然后再加载普通分包,因为普通分包依赖主包运行。但是如果小程序从独立分包进入进入小程序,则不需要下载主包,独立分包自己就可以运行。


普通分包所有的限制对独立分包都有效。


为什么要有独立分包,普通分包不够吗


因为独立分包不需要依赖主包,如果有作为打开小程序的的入口的必要,加载速度会比普通分包快,给客户的体验感更好。毕竟谁也不想打开一个页面等半天。


举个例子,如果小程序启动的时候是打开一个普通分包页面。则加载顺序是:加载主包 => 再加载当前分包


但如果小程序启动的时候是打开一个独立分包页面,则加载顺序是:直接加载独立分包,无需加载主包


独立分包相对于普通分包,就是省去了加载主包的时间和消耗。


独立分包如何配置


配置和普通分包一样,加一个independent属性设为true即可。


image.png


独立分包的缺点


既然独立分包可以不依赖主包,那我把每个分包都打成独立分包可以吗。


最好别那么干


理由有四点


1.独立分包因为不依赖主包,所以他不一定能获取到小程序层面的全局状态,比如getApp().也不是完全获取不到,主包被加载的时候还是可以获取到的。概率性出问题,最好别用。


2.独立分包不支持使用插件


3.小程序的公共文件不适用独立分包。比如Taro的app.less或小程序的app.wxss


上述三个,我觉的都挺麻烦的。所以不是作为入口包这种必要的情况下,确实没有使用独立分包的需求。


PS:一个小程序里可以有多个独立分包


独立分包有版本兼容问题吗


有滴,但你不用这个兼容问题直接让你报错
在低于 6.7.2 版本的微信中运行时,独立分包视为普通分包处理,不具备独立运行的特性。


所以,即使在低版本的微信中,也只是会编译成普通分包而已。


注意!!! 这里有一个可能会遇到的,就是如果你在独立分包中使用了app.wxss或者app.less这些小程序层面的公共css文件,那么在低版本(<6.7.2)进行兼容的时候,你就会发现,独立分包的页面会被这些全局的CSS影响。因为那时候独立分包被编译成了普通分包。而普通分包是适用全局公共文件的。


分包预下载


首先我们需要了解,分包是基本功能是,在下程序打包的时候不去加载分包,然后在进入当前分包页面的时候才开始下载分包。一方面目的是为了加快小程序的响应速度。另一方面的原因是避开微信小程序本身只能上传2M的限制。


这里有一个问题,就是我在首次跳转某个分包的某个页面的时候,出现短暂的白屏怎么办?(下载分包的时间+运行接口的时间+渲染视图的时间)。


后两者没法彻底避免,只能优化代码,第一个下载分包的时间可以使用分包预下载功能解决。


我们可以通过分包预下载在进入分包页面之前就开始下载分包,来减少进入分包页面的时间。


如何配置分包预下载


当前的分包预下载只能在app.config(Taro)或者app.json(原生小程序)通过preloadRule字段去配置。


preloadRule字段是一个对象,key是页面的路径,value是进行预加载的分包name或者key,__APP__代表主包


上案例


image.png


通过preloadRule字段去配置


”packageB/pages/user/index“是key


packages:["packageA"]是value


案例上的意思是当进入packageA分包的时候,开始下载分包packageB


如果要某一个分包在加载主包的就开始下载,那么就设置packages:["APP"]即可。


总结



  1. 分包是为了解决小程序超过2m无法上传的问题

  2. 分包依赖于主包,进入分包页面,主包必然需要优先被加在

  3. 主包和分包同时引用一个依赖或资源,则当前依赖或资源会被打入到主包

  4. 两个分包使用了同一个依赖或资源,则该依赖和资源会被打入到主包

  5. 某资源或依赖只在某一个分包中使用,则该资源和依赖会被打入到该分包中

  6. 独立分包的配置相对于普通分包只是多了一个independent字段,设置为true

  7. 独立分包无需依赖主包,可独立加载。

  8. 独立分包中谨慎使用全局属性,最好别用,可能获取不到

  9. 分包可以被预加载,用于解决进入分包页面时才开始加载分包导致页面可能出现的(取决于加载速度)短暂白屏的问题。


分包官方文档


分包官方分包demo-小程序版


如果您认为对您有用的话,留个赞或收藏一下吧~


image.png


作者:工边页字
来源:juejin.cn/post/7321049399281958922
收起阅读 »

现代 CSS 解决方案:文字颜色自动适配背景色!

web
在 23 年的 CSS 新特性中,有一个非常重要的功能更新 -- 相对颜色。简单而言,相对颜色的功能,让我们在 CSS 中,对颜色有了更为强大的掌控能力。其核心功能就是,让我们能够基于一个现有颜色 A,通过一定的转换规则,快速生成我们想要的颜色 B。...
继续阅读 »

在 23 年的 CSS 新特性中,有一个非常重要的功能更新 -- 相对颜色

简单而言,相对颜色的功能,让我们在 CSS 中,对颜色有了更为强大的掌控能力。

其核心功能就是,让我们能够基于一个现有颜色 A,通过一定的转换规则,快速生成我们想要的颜色 B

其功能能够涵盖:

完整的教程,你可以看这里 -- Chrome for Developers- CSS 相对颜色语法

当然,今天我们不会一个一个去过这些功能,更多的时候,我们只需要知道我们能够实现这些功能。

本文,我们将从实际实用角度出发,基于实际的案例,看看 CSS 相对颜色,能够如何解决我们的一些实际问题。

快速语法入门

首先,我们通过一张图,一个案例,快速入门 CSS 相对颜色语法:

相对颜色语法的目标是允许从另一种颜色派生颜色。

上图显示了将原始颜色 green 转换为新颜色的颜色空间后,该颜色会转换为以 r、g、b 和 alpha 变量表示的各个数字,这些数字随后会直接用作新的 rgb() 颜色的值。

举个例子:

<p> CSS Relative Color p>
p {
color: rgb(255, 0, 0);
}

实现一个 color 为红色(rgb 值为 rgb(255, 0, 0))的字体:

基于上面的相对颜色语法,我如何通过一个红色生成绿色文字呢?示意如下:

p {
--color: rgb(255, 0, 0);
color: rgb(from var(--color) calc(r - 255) calc(g + 255) b); /* result = rgb(0, 255, 0) */
}

效果如下,我们就得到绿色字体:

解释一下:

  1. 原本的红色颜色,我们把它设置为 CSS 变量 --color: rgb(255, 0, 0)
  2. 想通过红色得到绿色,对于红色的 rgb 值 rgb(255, 0, 0) 而言,需要转换成 rgb(0, 255, 0)
  3. 使用 CSS 相对颜色语法,就是 rgb(from var(--color) calc(r - 255) calc(g + 255) b)

通过这个 DEMO,我们把几个核心基础语法点学习一下:

  1. from 关键字

from 关键字,它是相对颜色的核心。它表示会将 from 关键字后的颜色定义转换为相对颜色!在 from 关键字后面,CSS 会期待一种颜色,即能够启发生成另一种颜色

  1. from 关键字 后的颜色表示,支持不同颜色表示或者是 CSS 变量

第二个关键点,from 后面通常会接一个颜色值,这个颜色值可以是任意颜色表示法,或者是一个 CSS 变量,下面的写法都是合法的:

p {
color: rgba(from #ff0000) r g b);
color: rgb(from rgb(255, 0, 0) r g b);
color: rgb(from hsl(0deg, 100%, 50%) r g b);
color: rgb(from var(--hotpink) r g b);
}
  1. 对转换后的变量使用 calc() 或其他 CSS 函数

另外一个非常重要的基础概念就是,我们可以对 (from color r g b) 后的转换变量 r g b 使用 calc() 或其他 CSS 函数。

就是我们上面的例子:

p {
--color: rgb(255, 0, 0);
color: rgb(from var(--color) calc(r - 255) calc(g + 255) b); /* result = rgb(0, 255, 0) */
}
  1. 相对颜色语法支持,各种颜色表示函数:

相对颜色的基础的使用规则就是这样,它不仅支持 rgb 颜色表示法,它支持所有的颜色表示法:

使用 CSS 相对颜色,实现统一按钮点击背景切换

通常页面上的按钮,都会有 hover/active 的颜色变化,以增强与用户的交互。

像是这样:

最常见的写法,就是我们需要在 Normal 状态、Hover 状态、Active 状态下写 3 种颜色:

p {
color: #ffcc00;
transition: .3s all;
}
/* Hover 伪类下为 B 颜色 */
p:hover {
color: #ffd21f;
}
/** Active 伪类下为 C 颜色 **/
p:active {
color: #ab8a05;
}

在之前,我们介绍过一种利用滤镜 filter: contrast() 或者 filter: brightness() 的统一解决方案,无需写多个颜色值,可以根据 Normal 状态下的色值,通过滤镜统一实现更亮、或者更暗的伪类颜色。

在今天,我们也可以利用 CSS 相对颜色来做这个事情:

div {
--bg: #fc0;
background: var(--bg);
transition: .3s all;
}

div:hover {
background: hsl(from var(--bg) h s calc(l * 1.2));
}
div:active {
background: hsl(from var(--bg) h s calc(l * 0.8));
}

我们通过 hsl 色相、饱和度、亮度颜色表示法表示颜色。实现:

  1. 在 :hover 状态下,根据背景色,将背景亮度 l 调整为原背景色的 1.2 倍
  2. 在 :avtive 状态下,根据背景色,将背景亮度 l 调整为原背景色的 0.8 倍

在实际业务中,这是一个非常有用的用法。

完整的 DEMO,你可以戳这里:CodePen Demo -- https://codepen.io/Chokcoco/pen/KKEdOeb

使用 CSS 相对颜色,实现文字颜色自适应背景

相对颜色,还有一个非常有意思的场景 -- 让文字颜色能够自适应背景颜色进行展示。

有这么一种场景,有的时候,无法确定文案的背景颜色的最终表现值(因为背景颜色的值可能是后台配置,通过接口传给前端),但是,我们又需要能够让文字在任何背景颜色下都正常展现(譬如当底色为黑色时文字应该是白色,当背景为白色时,文字应该为黑色)。

像是这样:

在不确定背景颜色的情况下,无论什么情况,文字颜色都能够适配背景的颜色。

在之前,纯 CSS 没有特别好的方案,可以利用 mix-blend-mode: difference 进行一定程度的适配:

div {
// 不确定的背景色
}
p {
color: #fff;
mix-blend-mode: difference;
}

实操过这个方案的同学都会知道,在一定情况下,前景文字颜色还是会有一点瑕疵。并且,混合模式这个方案最大的问题是会影响清晰度

有了 CSS 相对颜色后,我们有了更多的纯 CSS 方案。

利用 CSS 相对颜色,反转颜色

我们可以利用相对颜色的能力,基于背景色颜色进行反转,赋值给 color。

一种方法是将颜色转换为 RGB,然后从 1 中减去每个通道的值。

代码非常简单:

p {
/** 任意背景色 **/
--bg: #ffcc00;
background: var(--bg);

color: rgb(from var(--bg) calc(1 - r) calc(1 - g) calc(1 - b)); /** 基于背景反转颜色 **/
}

用 1 去减,而不是用 255 去,是因为此刻,会将 rgb() 表示法中的 0~255 映射到 0~1

效果如下:

配个动图,我们利用背景色的反色当 Color 颜色,适配所有背景情况:

完整的 DEMO 和代码,你可以戳这里:CodePen Demo -- CSS Relatvie Color Adapt BG

当然,这个方案还有两个问题:

  1. 如果颜色恰好是在 #808080 灰色附近,它的反色,其实还是它自己!会导致在灰色背景下,前景文字不可见;
  2. 绝大部分情况虽然可以正常展示,但是并不是非常美观好看

为了解决这两个问题,CSS 颜色规范在 CSS Color Module Level 6 又推出了一个新的规范 -- color-contrast()

利用 color-contrast(),选择高对比度颜色

color-contrast() 函数标记接收一个 color 值,并将其与其他的 color 值比较,从列表中选择最高对比度的颜色。

利用这个 CSS 颜色函数,可以完美的解决上述的问题。

我们只需要提供 #fff 白色和 #000 黑色两种可选颜色,将这两种颜色和提供的背景色进行比较,系统会自动选取对比度更高的颜色。

改造一下,上面的代码,它就变成了:

p {
/** 任意背景色 **/
--bg: #ffcc00;
background: var(--bg);

color: color-contrast(var(--bg) vs #fff, #000); /** 基于背景色,自动选择对比度更高的颜色 **/
}

这样,上面的 DEMO 最终效果就变成了:

完整的 DEMO 和代码,你可以戳这里:CodePen Demo -- CSS Relatvie Color Adapt BG

此方案的优势在于:

  1. 可以限定前景 color 颜色为固定的几个色值,以保证 UI 层面的统一及美观
  2. 满足任何情况下的背景色

当然,唯一限制这个方案的最大问题在于,当前,color-contrast 还只是一个实验室功能,未大规模被兼容。

总结一下

到今天,我们可以利用 CSS 提供的各类颜色函数,对颜色有了更为强大的掌控力。

很多交互效果,不借助 JavaScript 的运算,也能计算出我们想要的最终颜色值。本文简单的借助:

  1. 使用 CSS 相对颜色,实现统一按钮点击背景切换
  2. 使用 CSS 相对颜色,实现文字颜色自适应背景

两个案例,介绍了 CSS 相对颜色的功能。但它其实还有更为广阔的应用场景,完整的教程,你可以看这里 -- Chrome for Developers- CSS 相对颜色语法

最后

好了,本文到此结束,希望本文对你有所帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。


作者:Chokcoco
来源:juejin.cn/post/7321410822789742618
收起阅读 »

产品经理:“一个简单的复制功能也能写出bug?”

web
问题 刚入职时,遇到了一个线上 bug,用户点击复制按钮没办法复制文本,产品经理震怒,“这么简单的一个功能也能出问题?当时是谁验收的?”,因为我刚来还闲着,就把我派去解决这个问题。 我在排查问题时,发现该复制方法写在了一个自定义 hook 中(这里复制方法写在...
继续阅读 »

问题


刚入职时,遇到了一个线上 bug,用户点击复制按钮没办法复制文本,产品经理震怒,“这么简单的一个功能也能出问题?当时是谁验收的?”,因为我刚来还闲着,就把我派去解决这个问题。


我在排查问题时,发现该复制方法写在了一个自定义 hook 中(这里复制方法写在 hook 里没啥意义,但是乙方交付过来的代码好像特别喜欢把工具函数写成个 hook 来用),点进去查看就是简单的一个 navigator.clipboard.writeText()的方法,本地运行我又能复制成功。于是我怀疑是手机浏览器不支持这个 api 便去搜索了一下。


Clipboard


MDN 上的解释:


剪贴板 Clipboard APINavigator 接口添加了只读属性 clipboard,该属性返回一个可以读写剪切板内容的 Clipboard 对象。在 Web 应用中,剪切板 API 可用于实现剪切、复制、粘贴的功能。


只有在用户事先授予网站或应用对剪切板的访问许可之后,才能使用异步剪切板读写方法。许可操作必须通过取得权限 Permissions API (en-US)"clipboard-read" 和/或 "clipboard-write" 项获得。


浏览器兼容性


image.png


使用 document.execCommand() 降级处理


这里我也不清楚用户手机浏览器的版本是多少,那么这个 api 出现之前,是用的什么方法呢?总是可以 polyfill 降级处理的吧!于是我就查到了document.execCommand()这个方法:



  • document.execCommand("copy") : 复制;

  • document.execCommand("cut") : 剪切;

  • document.execCommand("paste") : 粘贴。


对比


Clipboard 的所有方法都是异步的,返回 Promise 对象,复制较大数据时不会造成页面卡顿。但是其支持的浏览器版本较新,且只允许 https 和 localhost 这些安全网络环境可以使用,限制较多。


document.execCommand() 限制较少,使用起来相对麻烦。但是 MDN 上提到该 api 已经废弃:


image.png


image.png


浏览器很可能在某个版本弃用该 api ,不过当前 2023/12/29 ,该复制 api 还是可以正常使用的。


具体代码修改


于是我修改了一下原来的 hook:


import Toast from "~@/components/Toast";

export const useCopy = () => {

const copy = async (text: string, toast?: string) => {

const fallbackCopyTextToClipboard = (text: string, toast?: string) => {
let textArea = document.createElement("textarea");
textArea.value = text;

// Avoid scrolling to bottom
textArea.style.top = "-200";
textArea.style.left = "0";
textArea.style.position = "fixed";
textArea.style.opacity = "0"

document.body.appendChild(textArea);
// textArea.focus();
textArea.select();
let msg;
try {
let successful = document.execCommand("copy");
msg = successful ? toast ? toast : "复制成功" : "复制失败";
} catch (err) {
msg = "复制失败";
}
Toast.dispatch({
content: msg,
});
document.body.removeChild(textArea);
};

const copyTextToClipboard = (text: string, toast?: string) => {
if (!navigator.clipboard || !window.isSecureContext) {
fallbackCopyTextToClipboard(text, toast);
return;
}
navigator.clipboard
.writeText(text)
.then(() => {
Toast.dispatch({
content: toast ? toast : "复制成功",
});
})
.catch(() => {
fallbackCopyTextToClipboard(text, toast)
});
};
copyTextToClipboard(text, toast);
};

return copy;
};

上线近一年,这个复制方法没出现异常问题。


作者:HyaCinth
来源:juejin.cn/post/7317577665014448167
收起阅读 »

高管违法开除我的一些想法

昨天(1月7日),公历年初,农历临近过年。这本应大家辛苦了一年,进行年会、团聚、收款,盼望着领取薪资,与远在千里的家人团聚的日子。 然而现实是,被井某指着鼻子威胁骂到: 我等劳动局的! 我现在就违法解除! 我告诉你了,我现在就违法解除! 你试试看,你他妈两年...
继续阅读 »

昨天(1月7日),公历年初,农历临近过年。这本应大家辛苦了一年,进行年会、团聚、收款,盼望着领取薪资,与远在千里的家人团聚的日子。


然而现实是,被井某指着鼻子威胁骂到:



  • 我等劳动局的!

  • 我现在就违法解除!

  • 我告诉你了,我现在就违法解除!

  • 你试试看,你他妈两年半找不着工作!你试试看!

  • 劳动仲裁一审二审,我有的是人!我慢慢等你,你试试看。

  • 你敢上传一个试试,我现在就打110!


事件和视频


原视频很大媒体平台都有,包括百度、抖音等。



本来想知道到底是什么原因,视频里的女高管如此有恃无恐,一时不知道到底是谁违法了。难道是因为孙某有什么错误,导致她认为就算是自己根据没有违法,所以法律拿她无可奈何,或者是她认为违法了又如何?


但是随着视频的播放,她放话:劳动仲裁一审二审,我有的是人!我慢慢等你,你试试看……,从这里看来意思是说假设真的是官司打起来了,我有是办法(让你时间耗不起、工作找不起)。虽然孙某有在拍视频,她也知道有在拍,但依然如此出言不逊,甚至你敢上传一个试试


当晚,此视频在和微信群被转发,各在文章平台、短视频平台迅速跟进,次日,名为北京尼欧XX科技有限公司发表了声明。


公司声明


image.png


如果只有视频,确实也不知事情原委(万一高管只是在视频里说说玩,其实是孙某违法了呢?),所以就没继续想些什么。但次日看到这个声明之后,从声明上看:高管以停职反省处理、孙某以足额支付补偿金处理,并强调章程均合法合规。


假设此声明没有问题,我有以下疑问:



  • 员工是否真是不能通过试用期


据我了解,假设试用期为6个月,一般公司会在第二三个月就会有相关的述职会议,以评估你是否能胜任工作。也就是说,能不能胜任工作,能不能通过试用期,通常2-3个月就能知晓了。但是为什么要在最后一个月才因某能力不足裁员呢?半年的时候,普通项目都做得差不多了。



  • 视频中的日期是什么时候


视频是在1月7号流传的。如果是热点事件,通常在一天内基本大家都能知道了。


但声明上说是根据12月8日足额支付补偿,依法合规,也就是说一个月之前就已经合规处理完此事了?


个人想法


大家有知道马云说了离职不是钱不够,就是心受委屈了。虽然有比较多的人补充说还有其他自己想走之类的原因云云。


但细究的话,各有各的原因,这就不便分析问题了。


总的说来,不管是自己离职,还是公司裁员,应该都能归纳于:愿意的、不愿意的。


这东西,就像是谈恋爱一样,如果双方不喜欢,不愿意,或开始愿意,后面不愿意了,终究就会产生破窗效应,最终摆烂或分道扬镳。


如果发现员工能力不足,或公司运营困难了,需要裁员时,应早日给出处理方案,例如培训、转岗、或直言等,从双方平等的角度获得对方的理解。


如果员工认为公司有哪些地方不合理,也早点提出相关方案,为什么不喜欢?有没有建议方案?尝试过哪些努力?比如公司人员结构、工作强度、代码可维护性……提出来看看,假设表达合理,公司也重视你,自然能给你相应的说法。


如果公司的解决问题的方案是,解决提出问题的人,那早点离开又何尝不对呢?说小一点,这是为了自己洒脱一点,说大一点,这是人择良友而交,禽择良木而栖,让环境越来越好。


当然,很多时候作为人确实也是身不由己,太多羁绊。但是有没有认真考虑过,有的东西是值得的吗?当发现不值得的时候,自己还有退路吗?是健康快乐更重要还是别人的看法更重要?


如何维护自身利益


那么,作为一个员工,我们应如何保护自己?不一定解决问题,但可能解决问题。


作为员工如何维护自己的利益


搞清楚劳动法和公司规定,知道自己有什么权利和责任。了解工资、工时、福利、休假等方面的规定,确保公司别违法。留着跟工作有关的文件,合同、工资单、绩效评估之类的记录。这些东西能当证据,帮你维护自己的权益。


如果有问题或烦恼,及时跟相关人员沟通,提供明确的事实和证据。参与公司的反馈机制,提建议和意见。继续学习,提升自己的技能,增加竞争力。参加培训课程、专业发展计划,提高职业能力和知识水平。


关注职业发展机会和市场趋势,找适合自己的发展方向。积极参与职业培训、跨部门项目之类的,提高竞争力。
平衡工作和个人生活的需求,保持身心健康。合理安排工作时间和休息时间,别太累和压力太大


被违法裁员时应如何处理


搞明白劳动法和规定,尤其是关于裁员的规定。这样你就知道自己有什么权利,雇主有什么责任。收集跟裁员有关的所有证据,比如裁员通知、合同、工资单、绩效评估、公司规定之类的文件。这些东西在后面的法律行动中可能很重要。


找专业的劳动法律顾问或律师咨询,让他们给你解释权益和法律选项。他们能帮你评估情况,提供适当的建议和法律支持。跟雇主沟通,表达你对裁员决定的担忧和不满。写份申诉信或要求重新考虑决定。有时候,通过沟通和谈判,可能会找到解决问题的办法。


根据当地的法律程序,你还有机会通过调解或仲裁来解决争议。律师会给你专业的法律建议,并在法庭上代表你维护权益。


最后,为勇敢维护自身利益的人们点赞!


相关信息



作者:四叶草会开花
来源:juejin.cn/post/7320959103932989451
收起阅读 »

环信Web端IM Demo登录方式如何修改

在环信即时通讯云IM 官网下载Demo,本地运行只有手机+验证码的方式登录?怎么更改为自己项目的Appkey和用户去进行登录呢?👇👇👇本文以Web端为例,教大家如何更改代码来实现1、 VUE2 Demovue2 demo源码下载vue2 demo线上...
继续阅读 »

在环信即时通讯云IM 官网下载Demo,本地运行只有手机+验证码的方式登录?怎么更改为自己项目的Appkey和用户去进行登录呢?

👇👇👇本文以Web端为例,教大家如何更改代码来实现

1、 VUE2 Demo

第一步:更改appkey

webim-vue-demo===>src===>utils===>WebIMConfig.js
bcf8e92b4df988de5df647cddd0b8ce5.png

第二步:更改代码

webim-vue-demo===>src===>pages===>login===>index.vue

<template>
<a-layout>
<div class="login">
<div class="login-panel">
<div class="logo">Web IM</div>
<a-input v-model="username" :maxLength="64" placeholder="用户名" />
<a-input v-model="password" :maxLength="64" v-on:keyup.13="toLogin" type="password" placeholder="密码" />
<a-input v-model="nickname" :maxLength="64" placeholder="昵称" v-show="isRegister == true" />

<a-button type="primary" @click="toRegister" v-if="isRegister == true">注册</a-button>
<a-button type="primary" @click="toLogin" v-else>登录</a-button>
</div>
<p class="tip" v-if="isRegister == true">
已有账号?
<span class="green" v-on:click="changeType">去登录</span>
</p>
<p class="tip" v-else>
没有账号?
<span class="green" v-on:click="changeType">注册</span>
</p>

<!-- <div class="login-panel">
<div class="logo">Web IM</div>
<a-form :form="form" >
<a-form-item has-feedback>
<a-input
placeholder
="手机号码"
v
-decorator="[
'phone',
{
rules: [{ required: true, message: 'Please input your phone number!' }],
},
]"
style
="width: 100%"
>
<a-select
initialValue
="86"
slot
="addonBefore"
v
-decorator="['prefix', { initialValue: '86' }]"
style
="width: 70px"
>
<a-select-option value="86">
+86
</a-select-option>
</a-select>
</a-input>
</a-form-item>

<a-form-item>
<a-row :gutter="8">
<a-col :span="14">
<a-input
placeholder
="短信验证码"
v
-decorator="[
'captcha',
{ rules: [{ required: true, message: 'Please input the captcha you got!' }] },
]"
/>
</a-col>
<a-col :span="10">
<a-button v-on:click="getSmsCode" class="getSmsCodeBtn">{{btnTxt}}</a-button>
</a-col>
</a-row>
</a-form-item>
<a-button style="width: 100%" type="primary" @click="toLogin" class="login-rigester-btn">登录</a-button>

</a-form> -->
<!-- </div> -->
</div>
</a-layout>
</template>

<script>
import './index.less';
import { mapState, mapActions } from 'vuex';
import axios from 'axios'
import { Message } from 'ant-design-vue';
const domain = window.location.protocol+'//a1.easemob.com'
const userInfo = localStorage.getItem('userInfo') && JSON.parse(localStorage.getItem('userInfo'));
let times = 60;
let timer
export default{
data(){
return {
username: userInfo && userInfo.userId || '',
password: userInfo && userInfo.password || '',
nickname: '',
btnTxt: '获取验证码'
};
},
beforeCreate() {
this.form = this.$form.createForm(this, { name: 'register' });
},
mounted: function(){
const path = this.isRegister ? '/register' : '/login';

if(path !== location.pathname){
this.$router.push(path);
}
if(this.isRegister){
this.getImageVerification()
}
},
watch: {
isRegister(result){
if(result){
this.getImageVerification()
}
}
},
components: {},
computed: {
isRegister(){
return this.$store.state.login.isRegister;
},
imageUrl(){
return this.$store.state.login.imageUrl
},
imageId(){
return this.$store.state.login.imageId
}
},
methods: {
...mapActions(['onLogin', 'setRegisterFlag', 'onRegister', 'getImageVerification', 'registerUser', 'loginWithToken']),
toLogin(){
this.onLogin({
username: this.username.toLowerCase(),
password: this.password
});
// const form = this.form;
// form.validateFields(['phone', 'captcha'], { force: true }, (err, value) => {
// if(!err){
// const {phone, captcha} = value
// this.loginWithToken({phone, captcha})
// }
// });
},
toReset(){
this.$router.push('/resetpassword')
},
toRegister(e){
e
.preventDefault(e);
// this.form.validateFieldsAndScroll((err, values) => {
// if (!err) {
// this.registerUser({
// userId: values.username,
// userPassword: values.password,
// phoneNumber: values.phone,
// smsCode: values.captcha,
// })
// }
// });

this.onRegister({
username: this.username.toLowerCase(),
password: this.password,
nickname: this.nickname.toLowerCase(),
});
},
changeType(){
this.setRegisterFlag(!this.isRegister);
},
getSmsCode(){
if(this.$data.btnTxt != '获取验证码') return
const form = this.form;
form
.validateFields(['phone'], { force: true }, (err, value) => {
if(!err){
const {phone, imageCode} = value
this.getCaptcha({phoneNumber: phone, imageCode})
}
});
},
getCaptcha(payload){
const self = this
const imageId = this.imageId
axios
.post(domain+`/inside/app/sms/send/${payload.phoneNumber}`, {
phoneNumber: payload.phoneNumber,
})
.then(function (response) {
Message
.success('短信已发送')
self
.countDown()
})
.catch(function (error) {
if(error.response && error.response.status == 400){
if(error.response.data.errorInfo == 'Image verification code error.'){
self
.getImageVerification()
}
if(error.response.data.errorInfo == 'phone number illegal'){
Message
.error('请输入正确的手机号!')
}else if(error.response.data.errorInfo == 'Please wait a moment while trying to send.'){
Message
.error('你的操作过于频繁,请稍后再试!')
}else if(error.response.data.errorInfo.includes('exceed the limit')){
Message
.error('获取已达上限!')
}else{
Message
.error(error.response.data.errorInfo)
}
}
});
},
countDown(){
this.$data.btnTxt = times
timer
= setTimeout(() => {
this.$data.btnTxt--
times
--
if(this.$data.btnTxt === 0){
times
= 60
this.$data.btnTxt = '获取验证码'
return clearTimeout(timer)
}
this.countDown()
}, 1000)
}
}
};
</script>

webim-vue-demo===>src===>store===>login.js
只用更改actions下的onLogin,其余不用动

onLogin: function(context, payload){
context.commit('setUserName', payload.username);
let options = {
user: payload.username,
pwd: payload.password,
appKey: WebIM.config.appkey,
apiUrl: 'https://a1.easecdn.com'
};
WebIM.conn.open(options).then((res)=>{
localStorage.setItem('userInfo', JSON.stringify({ userId: payload.username, password: payload.password,accessToken:res.accessToken}));
});

},

2、VUE3 DEMO:

第一步:更改appkey

webim-vue-demo===>src===>IM===>config===>index.js

c27eca4aefd5861bb4014d86d7b080de.png

第二步:更改代码

webim-vue-demo===>src===>views===>Login===>components===>LoginInput===>index.vue

<script setup>
import { ref, reactive, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { EaseChatClient } from '@/IM/initwebsdk'
import { handleSDKErrorNotifi } from '@/utils/handleSomeData'
import { fetchUserLoginSmsCode, fetchUserLoginToken } from '@/api/login'
import { useStore } from 'vuex'
import { usePlayRing } from '@/hooks'
const store = useStore()
const loginValue = reactive({
phoneNumber: '',
smsCode: ''
})
const buttonLoading = ref(false)
//根据登陆初始化一部分状态
const loginState = computed(() => store.state.loginState)
watch(loginState, (newVal) => {
if (newVal) {
buttonLoading
.value = false
loginValue
.phoneNumber = ''
loginValue
.smsCode = ''
}
})
const rules = reactive({
phoneNumber: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号',
trigger: ['blur', 'change']
}
],
smsCode: [
{
required: true,
message: '请输入短信验证码',
trigger: ['blur', 'change']
}
]
})
//登陆接口调用
const loginIM = async () => {
const { clickRing } = usePlayRing()
clickRing()
buttonLoading
.value = true
/* SDK 登陆的方式 */
try {
let { accessToken } = await EaseChatClient.open({
user: loginValue.phoneNumber.toLowerCase(),
pwd: loginValue.smsCode.toLowerCase(),
});
window
.localStorage.setItem(`EASEIM_loginUser`, JSON.stringify({ user: loginValue.phoneNumber, accessToken: accessToken }))
} catch (error) {
console
.log('>>>>登陆失败', error);
const { data: { extraInfo } } = error
handleSDKErrorNotifi(error.type, extraInfo.errDesc);
loginValue
.phoneNumber = '';
loginValue
.smsCode = '';
}
finally {
buttonLoading
.value = false;
}
/* !环信后台接口登陆(仅供环信线上demo使用!) */
// const params = {
// phoneNumber: loginValue.phoneNumber.toString(),
// smsCode: loginValue.smsCode.toString()
// }
// try {
// const res = await fetchUserLoginToken(params)
// if (res?.code === 200) {
// console.log('>>>>>>登陆token获取成功', res.token)
// EaseChatClient.open({
// user: res.chatUserName.toLowerCase(),
// accessToken: res.token
// })
// window.localStorage.setItem(
// 'EASEIM_loginUser',
// JSON.stringify({
// user: res.chatUserName.toLowerCase(),
// accessToken: res.token
// })
// )
// }
// } catch (error) {
// console.log('>>>>登陆失败', error)
// if (error.response?.data) {
// const { code, errorInfo } = error.response.data
// if (errorInfo.includes('does not exist.')) {
// ElMessage({
// center: true,
// message: `用户${loginValue.username}不存在!`,
// type: 'error'
// })
// } else {
// handleSDKErrorNotifi(code, errorInfo)
// }
// }
// } finally {
// buttonLoading.value = false
// }
}
/* 短信验证码相关 */
const isSenedAuthCode = ref(false)
const authCodeNextCansendTime = ref(60)
const sendMessageAuthCode = async () => {
const phoneNumber = loginValue.phoneNumber
try {
await fetchUserLoginSmsCode(phoneNumber)
ElMessage({
type: 'success',
message: '验证码获取成功!',
center: true
})
startCountDown()
} catch (error) {
ElMessage({ type: 'error', message: '验证码获取失败!', center: true })
}
}
const startCountDown = () => {
isSenedAuthCode
.value = true
let timer = null
timer
= setInterval(() => {
if (
authCodeNextCansendTime
.value <= 60 &&
authCodeNextCansendTime
.value > 0
) {
authCodeNextCansendTime
.value--
} else {
clearInterval(timer)
timer
= null
authCodeNextCansendTime
.value = 60
isSenedAuthCode
.value = false
}
}, 1000)
}
</script>

<template>
<el-form :model="loginValue" :rules="rules">
<el-form-item prop="phoneNumber">
<el-input
class="login_input_style"
v
-model="loginValue.phoneNumber"
placeholder
="手机号"
clearable
/>
</el-form-item>
<el-form-item prop="smsCode">
<el-input
class="login_input_style"
v
-model="loginValue.smsCode"
placeholder
="请输入短信验证码"
>
<template #append>
<el-button
type
="primary"
:disabled="loginValue.phoneNumber && isSenedAuthCode"
@
click
="sendMessageAuthCode"
v
-text="
isSenedAuthCode
? `${authCodeNextCansendTime}S`
: '获取验证码'
"

></el-button>
</template>
</el-input>
</el-form-item>
<el-form-item>
<div class="function_button_box">
<el-button
v
-if="loginValue.phoneNumber && loginValue.smsCode"
class="haveValueBtn"
:loading="buttonLoading"
@
click
="loginIM"
>登录</el-button
>
<el-button v-else class="notValueBtn">登录</el-button>
</div>
</el-form-item>
</el-form>
</template>

<style lang="scss" scoped>
.login_input_style {
margin: 10px 0;
width: 400px;
height: 50px;
padding: 0 16px;
}

::v-deep .el-input__inner {
padding: 0 20px;
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 1.75px;
color: #3a3a3a;

&::placeholder {
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
/* identical to box height */
letter-spacing: 1.75px;
color: #cccccc;
}
}

::v-deep .el-input__suffix-inner {
font-size: 20px;
margin-right: 15px;
}

::v-deep .el-form-item__error {
margin-left: 16px;
}

::v-deep .el-input-group__append {
background: linear-gradient(90deg, #04aef0 0%, #5a5dd0 100%);
width: 60px;
color: #fff;
border: none;
font-weight: 400;

button {
font-weight: 300;
}
}

.login_text {
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 17px;
text-align: right;

.login_text_isuserid {
display: inline-block;
// width: 100px;
color: #f9f9f9;
}

.login_text_tologin {
margin-right: 20px;
width: 80px;
color: #05b5f1;
cursor: pointer;

&:hover {
text-decoration: underline;
}
}
}

.function_button_box {
margin-top: 10px;
width: 400px;

button {
margin: 10px;
width: 380px;
height: 50px;
border-radius: 57px;
}

.haveValueBtn {
background: linear-gradient(90deg, #04aef0 0%, #5a5dd0 100%);
border: none;
font-weight: 300;
font-size: 17px;
color: #f4f4f4;

&:active {
background: linear-gradient(90deg, #0b83b2 0%, #363df4 100%);
}
}

.notValueBtn {
border: none;
font-weight: 300;
font-size: 17px;
background: #000000;
mix-blend-mode: normal;
opacity: 0.3;
color: #ffffff;
cursor: not-allowed;
}
}
</style>

3、React DEMO:

第一步:更改appkey

webim-dev===>demo===>src===>config===>WebIMConfig.js
c23c60aa28a75ddd2c743497453736d2.png

第二步:更改代码

webim-dev===>demo===>src===>config===>WebIMConfig.js
将usePassword改为true

6a1f227099bd124db2a2a4611e5bbc4b.png

4、Uniapp Demo:

第一步:更改appkey

uniapp vue2 demo
webim-uniapp-demo===>utils===>WebIMConfig.js

a1041f6e463396da3db74110f5477863.png

uniapp vue3 demo
webim-uniapp-demo===>EaseIM===>config===>index.js

7fc3d3d4077b50be4fdba689acb2f9a4.png

第二步:更改代码

webim-uniapp-demo===>pages===>login===>login.vue

c18a811eba8546dc53bebca8fa0b31da.png

5、微信小程序 Demo:

第一步:更改appkey

webim-weixin-demo===>src===>utils===>WebIMConfig.js

109f65d6690a9ace085c47e9b5e9eb49.png

第二步:更改代码

webim-weixin-demo===>src===>pages===>login===>login.wxml

<import src="../../comps/toast/toast.wxml" />
<view class="login">
<view class="login_title">
<text bindlongpress="longpress">登录</text>
</view>

<!-- 测试用 请忽略 -->
<view class="config" wx:if="{{ show_config }}">
<view>
<text>使用沙箱环境</text>
<switch class="config_swich" checked="{{isSandBox? true: false}}" color="#0873DE" bindchange="changeConfig" />
</view>
</view>

<view class="login_user {{nameFocus}}">
<input type="text" placeholder="请输入用户名" placeholder-style="color:rgb(173,185,193)" bindinput="bindUsername" bindfocus="onFocusName" bindblur="onBlurName" />
</view>
<view class="login_pwd {{psdFocus}}">
<input type="text" password placeholder="用户密码" placeholder-style="color:rgb(173,185,193)" bindinput="bindPassword" bindfocus="onFocusPsd" bindblur="onBlurPsd"/>
</view>
<view class="login_btn">
<button hover-class="btn_hover" bind:tap="login">登录</button>
</view>
<template is="toast" data="{{ ..._toast_ }}"></template>
</view>

webim-weixin-demo===>src===>pages===>login===>login.js

let WebIM = require("../../utils/WebIM")["default"];
let __test_account__, __test_psword__;
let disp = require("../../utils/broadcast");

let runAnimation = true
Page({
data: {
name: "",
psd: "",
grant_type: "password",
rtcUrl: '',
show_config: false,
isSandBox: false
},

statechange(e) {
console.log('live-player code:', e.detail.code)
},

error(e) {
console.error('live-player error:', e.detail.errMsg)
},

onLoad: function(option){
const me = this;
const app = getApp();
new app.ToastPannel.ToastPannel();

disp.on("em.xmpp.error.passwordErr", function(){
me.toastFilled('用户名或密码错误');
});
disp.on("em.xmpp.error.activatedErr", function(){
me.toastFilled('用户被封禁');
});

wx.getStorage({
key: 'isSandBox',
success (res) {
console.log(res.data)
me.setData({
isSandBox: !!res.data
})
}
})

if (option.username && option.password != '') {
this.setData({
name: option.username,
psd: option.password
})
}
},

bindUsername: function(e){
this.setData({
name: e.detail.value
});
},

bindPassword: function(e){
this.setData({
psd: e.detail.value
});
},
onFocusPsd: function(){
this.setData({
psdFocus: 'psdFocus'
})
},
onBlurPsd: function(){
this.setData({
psdFocus: ''
})
},
onFocusName: function(){
this.setData({
nameFocus: 'nameFocus'
})
},
onBlurName: function(){
this.setData({
nameFocus: ''
})
},

login: function(){
runAnimation = !runAnimation
if(!__test_account__ && this.data.name == ""){
this.toastFilled('请输入用户名!')
return;
}
else if(!__test_account__ && this.data.psd == ""){
this.toastFilled('请输入密码!')
return;
}
wx.setStorage({
key: "myUsername",
data: __test_account__ || this.data.name.toLowerCase()
});

getApp().conn.open({
user: __test_account__ || this.data.name.toLowerCase(),
pwd: __test_psword__ || this.data.psd,
grant_type: this.data.grant_type,
appKey: WebIM.config.appkey
});
},

longpress: function(){
console.log('长按')
this.setData({
show_config: !this.data.show_config
})
},

changeConfig: function(){
this.setData({
isSandBox: !this.data.isSandBox
}, ()=>{
wx.setStorage({
key: "isSandBox",
data: this.data.isSandBox
});
})

}

});


相关文档:

收起阅读 »

MyBatis实战指南(一):从概念到特点,助你快速上手,提升开发效率!

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集的过程。大家好,今天我们要来聊聊一个在Java开发中非常实用的框架——MyBatis。你是否曾经因为数据库操作...
继续阅读 »

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集的过程。

大家好,今天我们要来聊聊一个在Java开发中非常实用的框架——MyBatis。你是否曾经因为数据库操作而感到困扰?是否曾经因为SQL语句的编写而烦恼?那么,MyBatis或许就是你的救星。

接下来,让我们一起来了解一下MyBatis的概念与特点吧!

一、MyBatis基本概念

MyBatis 是一款优秀的半自动的ORM持久层框架,它支持自定义 SQL、存储过程以及高级映射。

MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。

MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

那么,什么是ORM?

要了解ORM,先了解下面概念:

持久化

把数据(如内存中的对象)保存到可永久保存的存储设备中。持久化的主要应用是将内存中的数据存储在关系型的数据库中,当然也可以存储在磁盘文件中、XML数据文件中等等。

持久层

即专注于实现数据持久化应用领域的某个特定系统的一个逻辑层面,将数据使用者和数据实体相关联。

ORM, 即Object-Relational Mapping(对象关系映射),它的作用是在关系型数据库和业务实体对象之间作一个映射。这样在具体的操作业务对象的时候,就不需要再去和复杂的SQL语句打交道,只需简单的操作对象的属性和方法。

Description

总结:

  • 它是一种将内存中的对象保存到关系型数据库中的技术;

  • 主要负责实体对象的持久化,封装数据库访问细节;

  • 提供了实现持久化层的另一种模式,采用映射元数据(XML)来描述对象-关系的映射细节,使得ORM中间件能在任何一个Java应用的业务逻辑层和数据库之间充当桥梁。

Java典型的ORM框架:

  • hibernate:全自动的框架,强大、复杂、笨重、学习成本较高;

  • Mybatis:半自动的框架, 必须要自己写sql;

  • JPA:JPA全称Java Persistence API、JPA通过JDK 5.0注解或XML描述对象-表的映射关系,是Java自带的框架。

二、Mybatis的作用

Mybatis是一个Java持久层框架,它主要用于简化与数据库的交互操作。Mybatis的主要作用有以下几点:

  • 将Java对象与数据库表进行映射,通过配置XML文件实现SQL语句的定义和执行,使得开发者可以专注于业务逻辑的实现而无需编写繁琐的JDBC代码。

  • 提供了灵活的SQL映射功能,可以根据需要编写动态SQL,支持复杂的查询条件和更新操作。

  • 支持事务管理,可以确保数据的一致性和完整性。

  • 提供了缓存机制,可以提高数据库查询性能。

  • 可以与Spring、Hibernate等其他框架无缝集成,方便开发者在项目中使用。

Mybatis就是帮助程序员将数据存取到数据库里面。传统的jdbc操作,有很多重复代码块比如: 数据取出时的封装, 数据库的建立连接等等,通过框架可以减少重复代码,提高开发效率 。

MyBatis 是一个半自动化的ORM框架 (Object Relationship Mapping) -->对象关系映射。

所有的事情,不用Mybatis依旧可以做到,只是用了它,会更加方便更加简单,开发更快速。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

三、MyBatis特点

1、定制化SQL

同为持久层框架的Hibernate,对操作数据库的支持方式较多,完全面向对象的、原生SQL的和HQL的方式。MyBatis只支持原生的SQL语句,这个“定制化”是相对Hibernate完全面向对象的操作方式的。

2、存储过程

储存过程是实现某个特定功能的一组sql语句集,是经过编译后存储在数据库中。当出现大量的事务回滚或经常出现某条语句时,使用存储过程的效率往往比批量操作要高得多。

MyBatis是支持存储过程的,可以看个例子。假设有一张表student:

create table student
(
id bigint not null,
name varchar(30),
sex char(1),
primary key (id)
);

有一个添加记录的存储过程:

create procedure pro_addStudent (IN id bigint, IN name varchar(30), IN sex char(1))
begin
insert into student values (id, name, sex);
end

此时就可以在mapper.xml文件中调用存储过程:

<!-- 调用存储过程 -->
<!-- 第一种方式,参数使用parameterType -->
<select id="findStudentById" parameterType="java.lang.Long" statementType="CALLABLE"
resultType="com.mybatis.entity.Student">
{call pro_getStudent(#{id,jdbcType=BIGINT,mode=IN})}
</select>

<parameterMap type="java.util.Map" id="studentMap">
<parameter property="id" mode="IN" jdbcType="BIGINT"/>
</parameterMap>

<!-- 调用存储过程 -->
<!-- 第二种方式,参数使用parameterMap -->
<select id="findStudentById" parameterMap="studentMap" statementType="CALLABLE"
resultType="com.mybatis.entity.Student">
{call pro_getStudent(?)}
</select>

3、高级映射

可以简单理解为支持关联查询。

4、避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。

使用Mybatis时,数据库的连接配置信息,是在mybatis-config.xml文件中配置的。同时,获取查询结果的代码,也是尽量做到了简洁。以模糊查询为例,需要做两步工作:

1)首先在配置文件中写上SQL语句,示例:

 <mapper namespace="com.test.pojo">
<select id="listCategoryByName" parameterType="string" resultType="Category">
select * from category_ where name like concat('%',#{0},'%')
</select>
</mapper>

2)在Java代码中调用此语句,示例:

        List<Category> cs = session.selectList("listCategoryByName","cat");
for (Category c : cs) {
System.out.println(c.getName());
}

5、Mybatis中ORM的映射方式也是比较简单的

"resultType"参数的值指定了SQL语句返回对象的类型。示例代码:
<mapper namespace="com.test.pojo">
<select id="listCategory" resultType="Category">
select * from category_
</select>
</mapper>

四、Mybatis的适用场景

MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。MyBatis因其简单易用、灵活高效的特点,广泛应用于各种Java项目中。

以下是一些常见的应用场景:

  • 数据查询:MyBatis可以执行复杂的SQL查询,返回Java对象或者结果集。

  • 数据插入、更新和删除:MyBatis可以执行INSERT、UPDATE和DELETE等SQL语句。

  • 存储过程和函数调用:MyBatis可以调用数据库的存储过程和函数。

  • 高级映射:MyBatis支持一对一、一对多、多对一等复杂关系的映射。

  • 懒加载:MyBatis支持懒加载,只有在真正需要数据时才会去数据库查询。

  • 缓存机制:MyBatis内置了一级缓存和二级缓存,可以提高查询效率。

为什么说Mybatis是半自动ORM映射工具

Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。

而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。

MyBatis作为半自动ORM映射工具与全自动ORM工具相比,有几个主要的区别点:

1.SQL的灵活性

MyBatis作为半自动ORM映射工具,开发人员可以灵活地编写SQL语句,充分发挥数据库的特性和优势。而全自动ORM工具通常会在一定程度上限制开发人员对SQL的灵活控制。

2.映射关系的可定制性

MyBatis允许开发人员通过配置文件(或注解)自定义对象和数据库表之间的映射关系,可以满足各种复杂的映射需求。而全自动ORM工具通常根据约定和规则自动生成映射关系,对于某些特殊需求无法满足。

3.SQL的可复用性

MyBatis支持SQL的可复用性,可以将常用的SQL语句定义为独立的SQL片段,并在需要的地方进行引用。而全自动ORM工具通常将SQL语句直接与对象的属性绑定在一起,缺乏可复用性。

4.性能调优的灵活性

MyBatis作为半自动ORM映射工具,允许开发人员对SQL语句进行灵活的调优,通过手动编写SQL语句和使用高级特性进行性能优化。而全自动ORM工具通常将性能优化的控制权交给框架,开发人员无法灵活地对SQL进行调优。

MyBatis作为一种半自动ORM映射工具,相对于全自动ORM工具具有更高的灵活性和可定制性。通过灵活的SQL控制、自定义的映射关系、可复用的SQL以及灵活的性能调优,MyBatis可以满足各种复杂的映射需求和性能优化需求。

虽然MyBatis相对于全自动ORM工具需要开发人员编写更多的SQL语句,但正是由于这种半自动的特性,使得MyBatis在某些复杂场景下更加灵活和可控。

因此,我们可以说MyBatis是一种半自动ORM映射工具,与全自动的ORM工具相比,它更适用于那些对SQL灵活性和性能调优需求较高的场景。

五、Mybatis的优缺点

Mybatis有以下优点:

1.基于SQL语句编程,相当灵活

SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。

2. 代码量少

与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接。

3.很好的与各种数据库兼容

4.数据库字段和对象之间可以有映射关系

提供映射标签,支持对象与数据库的ORM字段关系映射。

5.能够与Spring很好的集成

Mybatis有以下缺点:

1.SQL语句的编写工作量较大

尤其当字段多、关联表多时,SQL语句较复杂。

2.数据库移植性差

SQL语句依赖于数据库,不能随意更换数据库(可以通过在mybatis-config.xml配置databaseIdProvider来弥补)。

示例:

 <databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="SQL Server" value="sqlserver"/>
<property name="Oracle" value="oracle"/>
</databaseIdProvider>

然后在xml文件中,就可以针对不同的数据库,写不同的sql语句。

3.字段映射标签和对象关系映射标签仅仅是对映射关系的描述,具体实现仍然依赖于sql。

示例:

public class Student{
String name;
List<Interest> interests;
}

public class Interest{
String studentId;
String name;
String direction;
}

<resultMap id="ResultMap" type="com.test.Student">
<result column="name" property="name" />
<collection property="interests" ofType="com.test.Interest">
<result column="name" property="name" />
<result column="direction" property="direction" />
</collection>
</resultMap>

在该例子中,如果查询sql中,没有关联Interest对应的表,则查询出数据映出的Student对象中,interests属性值就会为空。

4.DAO层过于简单,对象组装的工作量较大

即Mapper层Java代码过少,XxxMapper.xml文件中维护数据库字段和实体类字段的工作量较大。

5.不支持级联更新、级联删除

仍以上面的Student和Interest为例,当要更新/删除某个Student的信息时,需要在两个表进行手动更新/删除。

通过以上的介绍,相信大家对MyBatis已经有了更深入的了解。MyBatis是一个非常强大的持久层框架,它的灵活性、易用性、解耦性、高效性和全面性都使得它在Java开发中得到了广泛的应用。

收起阅读 »

我困在考研的这两年

我困在考研的这两年 2024考研结束了,我想对我这两年的考研之路做个总结。 2021年冬 2021年冬天的某一天,突然决定要考研。这时候已经距离我2017年大学毕业过去了四年,毕业四年之后再拿起书本准备考研,现在想想也感觉有些疯狂。 大学毕业的这四年,在北京有...
继续阅读 »

我困在考研的这两年


2024考研结束了,我想对我这两年的考研之路做个总结。


2021年冬


2021年冬天的某一天,突然决定要考研。这时候已经距离我2017年大学毕业过去了四年,毕业四年之后再拿起书本准备考研,现在想想也感觉有些疯狂。


大学毕业的这四年,在北京有了一份相对稳定的工作。从一开始干劲十足,到现在心累无力。看到过各种各样的中年领导,有时候也会想,这会不会就是自己未来三十岁、四十岁的样子。当有这种念头的时候,就会忍不住想逃离。想去改变,想把自己生活、职业的天花板再调高些。


当脑袋里冒出考研的时候,我也问过我自己,28上研究生真的有意义吗?毕业已经30+了,在这个35岁就是职业生涯末期的行业,这不是毕业就失业嘛。干嘛执着去考研呐。但是想到遇到过的那些中年领导,又害怕自己走他们的老路。不想被困死,只能改变。


2022年春夏秋冬


由于2021年末决定考研,我对考研形势也没有清晰的认知,转年三月份才开始准备。后来得知人家三月份早就已经把数学基础过完一遍了,我那时候还不知道定积分是个数吶!三、四月份还过了一遍高中数学知识,现在想来也是可笑,准备当年的研究生考试,还有时间去过高中数学知识。五月份报了班学习数学和英语,早上起来学英语,白天上班,晚上学习数学,地铁里看专业课知识。


那时候什么都不懂,就知道基础阶段、强化阶段、冲刺阶段。拼命赶进度,练习很少,学了后面忘了前面。也都顾不上了,先学了再说。也不管知识的掌握情况,就是闷头往前冲,英语大纲词汇过了一遍之后就不看了,学长难句,学语法。根本不想英语单词是不是要天天去背,没有时间去想那么多。


追进度、干糙活。中途又经历搬家。搬家之后,每天早起做英语阅读,坐地铁时看专业课知识,晚上写写数学。没有学习计划,没有复习计划。现在想那要是能学好,那可真就是天才了。可惜我不是


十二月初,我阳了。很难受,好像打摆子一样,身体抖个不停。发烧,头疼,感觉脑袋里边的脑仁疼。那几天我没怎么看书,也没看进去书。好在是考试之前阳了,没有因为这个原因缺考。


十二月二十四号,研究生考试开考。那天早上出来,天还是黑的。我有过一阵恍惚,不知道是为啥。到了考场,第一科开考了一个小时,我看空了大半的考场,缺考者很多。心里还窃喜,竞争者少了这么多,这我不上岸谁上岸。现在想来真是无知者无畏。


2023年春


考试成绩下来了,没有过线,国家线都没过,彻底失败。出分当天爸爸做手术,我正在忙着跑前跑后。看到群里有研友说可以查到分了,我急忙点开微信里保存的查分网址。没有登录、没有输入报名号,就那么直接的把我的考试成绩展示出来了。对我冲击很大,我一时间不知道怎么应对。幸亏我当时在上行的扶梯,不需要思考,它带着我向上。


和爸爸妈妈说了成绩之后,爸爸妈妈也没有责备我。反而鼓励我再考,“一年不行就两年,两年不行就三年。咱总得给它考上”。我当时很感动,我爸妈总是很坚定的支持我追寻自己的人生,真幸运遇到他们呀。那天也没时间考虑这个成绩,爸爸的手术从早上一直等到晚上七点多才做,晚上十一点才做完。很幸运,手术很成功。这是那天最好的消息


收拾挫败的心情,把不甘心化为动力。再大干一场吧。这毕竟是我自己的人生那!


2023年夏秋冬


回北京后,自己内心复盘了一下去年的学习方法,列了几条自己的问题,开始有针对的改变。



  • 英语读不懂就背单词,大纲单词四千+,每天背不了多的,那就背五十个。几个月怎么也过一遍。第二遍就每天两百个,再第三遍。第四遍...记不住具体词就记大体结构,先知道这个词的意思。作文我也用不了四千多个单词,用不上全部都背全词。再不行就写,联想着记。

  • 数学把去年没懂的地方都记录下来,先总体过一遍基础。再针对学不懂的章节。跟一个老师听不懂,就上B站、找网盘,看其他老师怎么讲的。对比验证着理解。

  • 专业课划出近几年的热点考题,着重了解对应的章节。

  • 学习方法不对就改,用艾宾浩斯曲线复习。


中午吃完饭,从办公区出来找一个阴凉地方看各科视频。夏天很热,周边饭店后厨的抽油烟机声音很响。买了个降噪耳机。每天中午要回去上班的时候,把耳机一摘,全是汗。


七月份,找了个小房子,自己搬出来单独住。每天学习、工作,时间安排的很满,很充实。每天也没有那么多时间去胡思乱想。七、八、九这三个月过的很快,没什么感觉就来到了十月份


九月底,接到了裁员通知。整个部门砍掉,人员全部辞退。“疫情的风”终于是吹到了我,我其实早就做好了心理准备。本来打算十月一假期回来提离职,十月底走人。专心十一月、十二月复习考研。接到通知后,没什么大的情绪起伏,坦然接受。通知是上午发的,赔偿是下午谈好的。emmm...说没情绪起伏是假的,这笔赔偿对我来说还是挺可观的。真香


这世界上唯一不会变的,就是一切都在变


坦然接受变化,因为迟早会有这么一天。


进入到十一月份,可能是临近考试了。突然感觉到焦躁,好多之前会做、能做的题。突然在真题这就不会做了。不能说一点思路没有吧,有点但是有限。翻开答案,看了就明白。但是让自己再做类似的题,还是和之前一样的感觉,人都麻了。抓紧复习知识点,再把强化阶段这个题相关章节的题拿出来重做。人更麻了。原来能做对的题,现在也做不对了。一点思路都没有,直接卡壳。尝试过总结题型,总结做题步骤。对我来说毫无用处,再遇到这种题,第一感觉还是大脑一片空白。多元函数积分学你在听吗?我说的就是你!


心里越来越焦虑,晚上躺床上也睡不着觉。基本每天都是满脑子乱想,然后迷迷糊糊睡着。


越到考试日期,越焦虑


我尝试开解自己“你不是不会,你是太紧张了,你是累了,你是没休息好,你是头脑不清醒”,我每天都下楼借着中午吃饭的时候吹吹风,放空下头脑。虽然效果不大,但是有效果就行。


十二月份,北京下雪了。那天中午吃完饭回来,我在小区的花园里走了好久。踩踩雪,感受下这真实的世界,这真实的生活,这真实的人生。


十二月二十二号,考试前一天,这一天我一点学习状态都没有。激动、颤抖、焦虑各种各样的情绪交杂在一起,肖四是一点也看不下去,更别提背了。为了第二天能按时起来。我手表订了五个闹钟,手机订了一个闹钟。狠怕自己起不来。


十二月二十三号,研究生考试的日子又到了。今年北京这边新增了安检门,由于不知道具体什么流程,所以考试的第一天我去的特别早。还是天蒙蒙亮的早晨。在电梯到一楼开门的那一刹那,我内心告诉自己“这是通往你波澜壮阔人生的一刻,加油去干吧”。


本来打算在考场外边背一会肖四再进去。但是因为今年新增了安检门,手机、书包什么都带不进楼里。只能放在楼外边的柜子里。外边好多人都守在柜子旁边背肖四,我本来也想趁这个时候再背背。但是我实在是有些太焦虑了,根本静不下心来。知识不进脑,外边还冷。索性就不背了,直接进考场。


该来的总会来,担忧那么多干嘛


找到考场后,发现就来了三个人。一看表,哦,才七点四十,八点半开始考试。在座位那硬坐了五十分钟。


政治、英语、数学、专业课


这四门考试之前,我坐在考场都很紧张。手心里都是汗,双手张开在桌子上摩擦一遍又一遍,做着深呼吸。告诉自己“没问题,我可以”。


每科考完试,感觉脑袋和身体都像被掏空了一样。那个时候没有太多想法,就是想吐槽一下考试题,哈哈哈。


本来我打算吐槽一下英语、数学、专业课(如果这次自己失败了,也好有一个赖的理由)。但是想想还是算了,强者从来都不抱怨环境。虽然我还不是强者,但是该有的格局咱们还是得有滴。那我就说说对这几科我感受到的优点吧。



  • 政治:中规中矩

  • 英语:英语一图画、图表作文首次结合,阅读AI模型、新题型博物馆、翻译大象都挺跟时事的。不得不说还是英语命题组会玩,很好,很新颖。

  • 数学:“60+老头”“坏滴很”,你哪里薄弱就往你哪里猛攻

  • 专业课:近十年来出的最好的一套卷子。出的题有深度,不偏不怪,不机械不套路。更注重理解而不是死记硬背。


该走的总要走,挽留也是徒劳


考完了,今年考完的感觉和去年完全不一样。今年少了无知无畏的乐观,更多的是如释重负的释然。不管最终结果如何,我已用尽了我的力气。我不想做悲情英雄,今年我上岸吧。梦中情校变母校,去到我想去的地方。


宇哥改编的这句歌词真好:“你看我多平常,困难一堆散落地上,但是我的眼中有光亮,换上坚强,气宇轩昂上战场,终将去到我想去的地方。”


青春,就是那些认为自己与众不同的日子


感谢我对象对我的支持理解,感谢我爸妈对我的包容和鼓励,也谢谢那个不放弃人生的自己


最后


我还想写好多话,写好多感受。但是现在已经凌晨一点多了。我明天还有事要早起。言尽至此


作者:用户4109461204928
来源:juejin.cn/post/7316202725330419739
收起阅读 »

开发距离生活有多远

相信做开发的同学,生活中会遇到一个频率非常高的问题。通常这个问题涉及的对话是这样的: 亲朋:“你在做什么工作呀?” 本人:“我是做软件开发工作的。” 亲朋:“噢!搞电脑的呀,好高端呀,你们这个行业具体是做什么呢?” 本人:“唔......就比如手机上的 AP...
继续阅读 »

相信做开发的同学,生活中会遇到一个频率非常高的问题。通常这个问题涉及的对话是这样的:



亲朋:“你在做什么工作呀?”

本人:“我是做软件开发工作的。”

亲朋:“噢!搞电脑的呀,好高端呀,你们这个行业具体是做什么呢?”

本人:“唔......就比如手机上的 APP ,微信、淘宝你用过吧?类似这种。”

亲朋:“哇,好厉害呀!”



上面这段,是我本人过往对于这个问题的回答。其实,每次我这么回答完以后,总觉得不得劲儿。感觉好像解释了一通,却又好像没让对方理解什么是开发工作。


image.png


直到最近,我的表妹又问了我这个问题:



表妹:“我其实一直没搞明白,你们写程序到底在做什么,所以,是在做什么呢?”



开发真的距离生活有那么远吗?


直到再次思考这个问题,我似乎找到了这个问题难以回答的根源:我压根没明白程序跟生活到底有什么关系。


在这个时代,编程的产物充斥着生活的各个角落:网购、聊天、支付等。但生活和程序,好像两条相互缠绕,却又难以相交的曲线。开发的产物服务于生活,但要用生活去解释开发,却又不是那么容易的事情。程序和生活中间,难道真的相隔着一个未知的距离吗?


程序不是无中生有,而是提高效率


我们开发的程序从来不是无中生有,从来不是创造不曾存在的东西,而是有围绕某个业务做的提效工具。


例如饮品店的店员操作的机器,上面就搭载了点单、收银两大功能的程序。你说这个程序没被开发出来以前,难道店员就不点单吗,就不收银吗?当然不是,让我们回忆一下,过往饮品店收银员是怎么工作的:




  1. 询问客户要买什么饮品,客户点单后,收银员用小纸条写下饮品的名称,递给做饮品的小哥;

  2. 收银员用计算器算好价格,客户递给纸币,收银员找零;

  3. 饮品做好后,收银员思考将饮品给哪位客户;



当点单量巨大时,在这套操作中,有几个痛点出现了:




  1. 写小纸条给制作饮品的小哥,这个操作会变得很耗时;

  2. 人工计算价格、收银、找零,容易出差错;

  3. 在收银员思考将饮品交给哪位客户这件事上,需要耗费巨大的脑力;



而现在的程序的流程是这样的:




  1. 客户点单,收银员在屏幕上选择客户购买的饮品,生成价格;

  2. 客户亮出付款码进行付款,生成订单号;

  3. 客户通过订单号领取饮品;



看,这就是程序做的事情,程序只是优化了生活中繁琐的步骤,提高了生活、工作的效率。人类社会向前发展,实质上就是要提高效率,把更多的时间放在更重要的人或事情上。


作为开发工作者,我们应该是更先进的


作为开发工作者,我们应该培养解决问题的能力,应该把提升效率的思考放在日常生活中,不要做只会敲代码的程序员。这是开发工作带给我们的优势和能力,让我们在生活中,多一些思考和实践。


开发也好,程序也好,离我们的生活真的很近,近到我们随时可以触摸,近到离不开我们的生活。用开发的思维为生活插上翅膀,毕竟,各个学科、行业都是从实际生活中孕育而出,最终也应回归生活,服务生活。


作者:水果小贩
来源:juejin.cn/post/7320655446100115506
收起阅读 »

为什么mysql最好不要只用limit做分页查询?

在项目中遇到的真实问题,以及我的解决方案,部分数据做了脱敏处理。 问题 最近在做项目时需要写sql做单表查询,每次查出来的数据有几百万甚至上千万条,公司用的数据库是MySQL5.7,做了分库分表,部分数据库设置了查询超时时间,比如查询超过15s直接报超时错误,...
继续阅读 »

在项目中遇到的真实问题,以及我的解决方案,部分数据做了脱敏处理。


问题


最近在做项目时需要写sql做单表查询,每次查出来的数据有几百万甚至上千万条,公司用的数据库是MySQL5.7,做了分库分表,部分数据库设置了查询超时时间,比如查询超过15s直接报超时错误,如下图:


image.png


可以通过show variables like 'max_statement_time';命令查看数据库超时时间(单位:毫秒):


image.png


方案1


尝试使用索引加速sql,从下图可以看到该sql已经走了主键索引,但还是需要扫描150万行,无法从这方面进行优化。


image.png


方案2


尝试使用limit语句进行分页查询,语句为:


SELECT * FROM table WHERE user_id = 123456789 limit 0, 300000;

像这样每次查30万条肯定就不会超时了,但这会引出另一个问题--查询耗时与起始位置成正比,如下图:


image.png


第二条语句实际上查了60w条记录,不过把前30w条丢弃了,只返回后30w条,所以耗时会递增,最终仍会超时。


方案3


使用指定主键范围的分页查询,主要思想是将条件语句改为如下形式(其中id为自增主键):


WHERE user_id = 123456789 AND id > 0 LIMIT 300000;
WHERE user_id = 123456789 AND id > (上次查询结果中最后一条记录的id值) LIMIT 300000;

也可以将上述语句简化成如下形式(注意:带了子查询会变慢):


WHERE user_id = 123456789 AND id >= (SELECT id FROM table LIMIT 300000, 1) limit 300000;

每次查询只需要修改子查询limit语句的起始位置即可,但我发现表中并没有自增主键id这个字段,表内主键是fs_id,而且是无序的。


这个方案还是不行,组内高工都感觉无解了。


方案4


既然fs_id是无序的,那么就给它排序吧,加了个ORDER BY fs_id,最终解决方案如下:


WHERE user_id = 123456789 AND fs_id > 0 ORDER BY fs_id LIMIT 300000;
WHERE user_id = 123456789 AND fs_id > (上次查询结果中最后一条记录的id值) ORDER BY fs_id LIMIT 300000;

效果如下图:


image.png


查询时间非常稳定,每条查询的fs_id都大于上次查询结果中最后一条记录的fs_id值。正常查30w条需要3.88s,排序后查30w条需要6.48s,确实慢了许多,但总算能把问题解决了。目前代码还在线上跑着哈哈,如果有更好的解决方案可以在评论区讨论哟。


作者:我要出去乱说
来源:juejin.cn/post/7209612932366270519
收起阅读 »

啊?两个vite项目怎么共用一个端口号啊

web
问题: 最近在业务开发中遇到一个问题,问题是这样的,当前有一个主项目和一个子项目,主项目通过微前端wujie来嵌套这个子项目,其中呢为了方便项目之间进行通信,所以规定该子项目的端口号必须为5173,否则通信失败,但是这时候发现一个问题,当我启动了该子项目后: ...
继续阅读 »

问题:


最近在业务开发中遇到一个问题,问题是这样的,当前有一个主项目和一个子项目,主项目通过微前端wujie来嵌套这个子项目,其中呢为了方便项目之间进行通信,所以规定该子项目的端口号必须为5173,否则通信失败,但是这时候发现一个问题,当我启动了该子项目后:


image.png


该项目的端口号为5173,但是此时我再次通过vite的官方搭建一个react+ts+vite项目:npm create vite@latest react_demos -- --template react-ts,之后通过npm run dev启动项目,发现端口号并没有更新:


image.png


这是什么原因呢?


寻因:


查阅官方文档,我发现:


image.png


那么我主动在vite.config.ts中添加这个配置:


image.png


正常来说,会出现这个报错:


image.png


但是此时结果依然为:


image.png


我百思不得不得其解,于是再次查阅官方文档:


image.png
我寻思这也与文档描述不一致啊,于是我再次尝试,思考是不是vite版本号的问题,两个项目的版本号分别为:


image.png


image.png


我决定创建一个4版本的项目npm create vite@^4.1.4 react_demos3 -- --template react-ts


image.png


结果发现,还是有这个问题,跟版本号没有关系,于是我又耐心继续看官方文档,看到了这个配置:


image.png
我抱着试试的态度,在其中一个vite项目中添加这个配置:


image.png


发现,果然是这个配置的锅,当其中一个项目host配置为0.0.0.0时,vite不会自动尝试更新端口号


难道vite的端口监测机制与host也有关?


结果:


不甘心的我再次进行尝试,将两个项目的host都设置成:


image.png


image.png


vite会自动尝试更新端口号


原来如此,vite的端口号检测机制在对比端口号之前,会先对比host,由于我的微前端项目中设置了host,而新建的项目中没有设置host,新建的项目host默认值为localhost对比不成功,vite不会自动尝试下一个可用端口,而是共用一个端口


总结:


在遇到问题时,要多多去猜,去想各种可能,并且最重要的是去尝试各种可能,还要加上积极去翻阅官方文档,问题一定会得到解决的;哪怕不能解决,那也会在尝试中,学到很多东西


作者:进阶的鱼
来源:juejin.cn/post/7319699173740363802
收起阅读 »

谈谈我家的奇葩买房经历

我是 2017 年毕业的,18 年买的房。 当时 IT 行业还是如日中天,薪资确实很高,我刚毕业就有接近 40 万。 当时的房价也是一路飙升,一周一个价那种。 我有个同事那年在北京买了房,犹豫了一周,涨了 20 多万。 那年过年回家的时候,我爸问我存了多少钱,...
继续阅读 »

我是 2017 年毕业的,18 年买的房。


当时 IT 行业还是如日中天,薪资确实很高,我刚毕业就有接近 40 万。


当时的房价也是一路飙升,一周一个价那种。


我有个同事那年在北京买了房,犹豫了一周,涨了 20 多万。


那年过年回家的时候,我爸问我存了多少钱,我说没有存多少,不知道钱花在哪里了。


我爸嫌我花的太多了,说要不买个房吧,这样每月还房贷还能存下点。


我说北京的房子需要交 5 年公积金才能买,而且首付二百多万呢,还没那么多钱。况且以后我也不一定留在北京,可能回青岛干。


年后我就回京继续上班了。


我爸在家开了一个门店,每天坐在门口和邻居聊天。


邻居聊起他儿子读完博士在青岛工作了,在黄岛区买了个房子,两周涨了十多万呢。


然后我爸就急了,非让我妈也去买一个,说是现在买还便宜点,就算我以后不回青岛,也可以卖了去北京再买。


我爸和我妈其实关系并不好,几乎是连吵带骂的逼着我妈去买。


为什么他不自己去呢?


因为我爸有严重的晕车,一坐车就吐。


我妈其实也没出过远门,自己一个人坐车从潍坊去青岛买房确实难为她了。


我妈还有点迷信,临走之前找算卦的占了一卦,算出一个方位,说是去青岛的城阳区买。


然后我妈就去了。


我妈啥也不懂,就在一个小区门口转悠。


然后保安过来问她干啥的。


她说想来买房,但是不知道去哪里买。


保安说我给你介绍一个人,可以找他买。


然后就给我妈介绍了一个中介。


那个中介说现在青岛都限购,需要交 2 年社保,只有即墨不限购,因为它刚撤市划区,划入青岛。


然后我妈找了个出租,并且给了出租的 200 块钱,让他一起去。


之后就到了即墨观澜国际的售楼处,人家介绍说这个房子是楼王,也就是最中间的那栋楼,是最好的,而且只有几套了。


我妈还在纠结,但是那个出租不耐烦了,要走。


然后我妈就定下来了,交了 70 万首付。


之后要办理手续,我从北京回家了一趟,和我爸我妈一起打车去了青岛。


我爸一路吐了有几十次,他说把胆汁都吐出来了。


就这样,我们就在青岛买下了这套房子。


13380 一平,首付 70 万,贷款 100 万,还 15 年,总共还 150 万。


然后我又回北京上班去了,只不过开始了还房贷的日子,一个月 1 万。


之后我爸又给了我 30 万,加上我自己还的,差不多在 2021 年就把 100 多万贷款还完了。因为提前还还的少。


差不多我爸 100 万,我拿了 100 万。


其实我还挺震惊的,我爸这样一个吃喝都那么节俭的人,竟然能拿出 100 万现金来。


后来在 2022 年年中的时候,我爸浑身疼的厉害,在地上打滚那种疼,去医院查出来是淋巴癌晚期。


然后 2023 年也就是今年年初的时候,我爸去世了。


去世前交代了一些事情,这套房子给我的 100 万就是他一辈子的积蓄了。


二手房要满 5 年才能卖,正好今年可以卖了。


但是问了下房价,现在观澜国际的均价是 7000 多,我们 2018 年买的时候是 13380 呢。而且 200 万的房子现在 90 万都不一定卖出去。


那我能咋办?


怪我爸?但我爸已经没了。


怪我妈?我妈也经常犯愁,而且当年是我爸逼她去的。


而且当年那种情况,我爸做的决定并没有错,当年大多数人都会认为房价会一直涨,早上车省很多钱。


我身边有一些朋友也是为了这个刚毕业不久就买房了。


其实住的话倒也没啥问题,关键是我并不去青岛工作,而且即墨那边也找不到前端的工作,互联网公司就集中在那几个城市。


租的话,一年才 1 万 5,而且装修还要投入好几万。


所以只能卖了。


本来是我们打算 5 年后卖了,正好在北京交满了 5 年公积金,然后再去北京买。


现在这情况,估计 200 万可能一分也收不回来。


遇到这事,正常人都会难受吧,我也一样。


那天我公众号发了条卖房消息:



真的是为了卖房么?


肯定不是啊,这样能把房子卖出去就怪了。


我只是想把它讲出来,仅此而已。讲出来之后确实好多了。


这几年我这种情况的全国也有不少:



并不是为了炒房,但确实因为各种原因不去住了。结果再卖的时候腰斩都卖不出去。


后来我也释然了,我本身物欲就很低,一辈子也用不了多少钱。


而且我还年轻,赚的也不少,可以再攒。


更重要的是,我一直觉得人这一生不能只是为了赚钱,要找到自己热爱的事业,在这个方向上持续开拓,创造自己的价值。


所幸我找到了。它才是支撑起我后半生的骨架。


最后,这段经历也不是完全没价值,至少我可以把它写下来,当做故事讲给你们听。


作者:zxg_神说要有光
来源:juejin.cn/post/7281833142104948776
收起阅读 »

大环境越不好 人就越玄学

二零零几年,大环境还没像现在这么拉垮的时候,有个面向学生的网站叫校内网,里面曾有人发起了一次大范围投票。 问广大学子毕业后最想从事什么工作。 当时超过一半的人都选择了大型外企,排名第二的是大型国企民企,然后是自主创业。 只有很少一部分选择了事业单位和公务员,这...
继续阅读 »

二零零几年,大环境还没像现在这么拉垮的时候,有个面向学生的网站叫校内网,里面曾有人发起了一次大范围投票。


问广大学子毕业后最想从事什么工作。


当时超过一半的人都选择了大型外企,排名第二的是大型国企民企,然后是自主创业。


只有很少一部分选择了事业单位和公务员,这部分同学还有相当比例来自对考公自古有执念的山东。


而在其他省份,多数同学都认为自己能拥有光明的未来,当然不会喜欢公务员这种工资稳定得低,日复一日枯坐案前,早早就能一眼望到头的工作。


在当时年轻人眼里,公务员属于“实在不行就只能回家考公“的备胎,地位约等于“实在不行就找个老实人嫁了“的级别。


但后来的故事我们都知道了,经济大船这几年驶入了深水区,风浪越来越大,鱼也越来越贵。


于是四平八稳旱涝保收的体制内,这几年摇身一变,一跃成为了那个最靓的仔。不得不说,人确实是时代的产物,环境的变化可以完全改变一个人的决策。


大环境好的时候,人们会不自觉地高估自身的努力,那时候人们是相信努力一定会有收获的。有时候过于相信了,但这在经济高速增长的年代并不会有太大问题,你还是会得到属于自己的那块蛋糕的。


但当经济增速换档时,付出与回报的比例开始失衡,努力就能收获的简单逻辑不攻自破。变成了努力也不一定有收获,进而发展成努力大概率不会有收获,最后演变成一命二运三风水,努力奋斗算个鬼


这种心态的转变也解释了为啥从去年以来,越来越多的年轻人开始扎堆去寺庙求签祈福,排的长队连起来能绕地球三圈,看得旁观的老大爷直摇头说,“真搞不懂这些小年轻是怎么想的,偶像粉丝见面会咋还跑到庙里来开了?!”


人在逆境迷茫时,是容易被玄学吸引。逆境意味着前路遇阻,意味着你迫切需要一些指引,而玄学恰好满足了这方面需求。


命运这个东西,有时候真蛮捉摸不透的。


我认识一小姐姐,为一场决定人生的重要考试做足了准备,结果在赶往考场的路上,书包就这么巧被扒手偷了,里面开卷考试所有的资料全部丢失,直接导致她逃汰出局,泪洒当场。


还有一大哥,在升职加薪岗位竞争的关键阶段,突然一场急病,好巧不巧失声了,一句话也说不出来,参加不了竞聘答辩,眼睁睁看着大好机会就此溜走。


等这事过去了,他一下子又能正常说话,跟被老天上了沉默debuff一样,你说他找谁说理去呢。


人活得时间越长,就越信“命“这个东西,越能意识到自己真正能把控的其实少得可怜,随便一点意外都能直接改变整个人生走向。


这种感悟放在以前,一般都是上了些年纪的人才会有的,但随着这两年经济增速换挡,年轻人频繁碰壁,被命运按在地上摩擦的次数多了,自然也就信了“命”,求签问道的也就跟着多起来了。


说句不好听的话,我觉得这样挺好的。不是说求签问道这个行为好,而是这种行为背后暗含着一个巨大的心理转变,我认为很好。


那就是放过自己。亚洲人尤其是我们特别不愿意放过自己,从出生开始就活在比较中,长辈们连夸个人都要这么夸,说哎呀,你学习真用功,比学习委员还用功;哎呀,你工资挺高,比隔壁小王还要高。


骂你的时候也一定要捎带上别人,说你看谁谁谁多厉害,你再看看你,一定是你还不够努力。


就是这种搞法很容易让人把责任全揽自己身上,对自我要求过高,最后的结果就是崩掉,就累嘛!


但现在不一样了,现代人在网络上看了太多含着金汤匙出生在罗马的人,和那些老天爷追着赏饭吃的人。


他们跟我们之间的差距大到几辈子都弥补不上,那努力万能论也就不攻自破了嘛。


于是越来越多的小伙伴开始承认自我的局限,承认努力也不一定有收获,承认人生不如意十之八九,慢慢也就承认了“命运”这个东西,开始顺其自然,没那么多执念了。


不过有些人过于放飞自我,摆烂走了另一个极端,那也是要出问题的。


即便是玄学,它也没有彻底否定个人奋斗,大富靠命没错,但小富靠勤,靠双手取得一些小成就,让日子过得舒服些还是没啥问题的。


其实我觉得一个比较合适的世界观应该是这个样子:首先咱得承认不可抗力,承认“命”与“运”这个东西是真实存在的,如果你不喜欢这两个玄乎的字,可以用“概率”代替,我们永远得做好小概率事件砸到头上的准备。


有时候拼尽一切就是没有好的结果,这咱得承认,但同时这也并不意味着从此放弃一切行动,落入虚无主义的陷阱。


人还是要去做一些什么的。比如精进某项专业技能,逐步提升自身能力,为的不是那点工资,而是一件更重要的事,抓住运气。


运气有多重要,大家都明白,它比努力重要得多。


运气这东西打比方的话,就像一个宝箱,会随机在你面前掉落,但这些宝箱自带隐形属性,你等级太低的话就看不见它,自然也就抓不住这些运气。


用现实举例,“运气”就像你在工作中遇到了某个本来还可以拉你一把的贵人,结果你的等级太低,工作能力稀碎,贵人一看,这货不值得我帮,转身走了。他这个宝箱对你而言就隐形了,消失了。


而且最讽刺的是你从头到尾都被蒙在鼓里,根本不知道自己错失了一次宝贵的机会,所以为了避免运气来了你抓不住,又溜走的这种尴尬情况出现,我们还是要去精进和磨练一下社会技能,尽量达到能在某些场合被人夸奖的程度。


把等级刷高一些,之后该吃吃该喝喝,耐心等待宝箱的出现。这可能也是以前人们常说的,“尽人事听天命”的另一种解释吧。


也希望今天聊的关于命和运的这些内容,能启发到一些小伙伴,大家一起认认真真,平平淡淡的生活。


作者:程序员Winn
来源:juejin.cn/post/7317704462436139058
收起阅读 »